OpenCV cv2.moments返回所有矩为零。

4
由于某种我无法理解的原因,open cv函数cv2.moments对于我提供的轮廓返回一个包含全零值的字典。 以下是一个最小工作示例:
contour = [[[271, 67]],
           [[274, 67]],
           [[275, 68]],
           [[278, 68]],
           [[279, 69]],
           [[283, 69]],
           [[284, 70]],
           [[287, 70]],
           [[288, 71]],
           [[291, 71]],
           [[292, 72]],
           [[295, 72]],
           [[292, 72]],
           [[291, 71]],
           [[288, 71]],
           [[287, 70]],
           [[284, 70]],
           [[283, 69]],
           [[279, 69]],
           [[278, 68]],
           [[275, 68]],
           [[274, 67]]]

contour = np.asarray(contour)
moments = cv2.moments(contour)

结果如下:
print(moments)

{'m00': 0.0, 'm10': 0.0, 'm01': 0.0, 'm20': 0.0, 'm11': 0.0, 'm02': 0.0, 'm30': 0.0, 'm21': 0.0, 'm12': 0.0, 'm03': 0.0, 'mu20': 0.0, 'mu11': 0.0, 'mu02': 0.0, 'mu30': 0.0, 'mu21': 0.0, 'mu12': 0.0, 'mu03': 0.0, 'nu20': 0.0, 'nu11': 0.0, 'nu02': 0.0, 'nu30': 0.0, 'nu21': 0.0, 'nu12': 0.0, 'nu03': 0.0}

这种行为的意义是什么? 我认为这是因为轮廓是开放的,但我不确定。 有没有一种标准的方法可以通过事先检查轮廓是否开放或闭合来消除这种行为?
3个回答

4

contour points

这些是你的轮廓点。GIF不会来回循环播放,它只会向前播放。当点“再次上升”时,那些不是相同的点,它们是刚好与之前的点重合的新点。检查一下你的轮廓点,你就会明白。
你的轮廓并不是“开放”的。这不是问题所在。在OpenCV中,所有的轮廓都被认为是闭合的。它们也不需要密集(点之间的距离为1像素)。
问题在于cv.moments()存在一个bug,而且OpenCV的轮廓很奇怪。
而且,由于它是从一个1像素的线计算出来的,所以你的轮廓在严格的数学“多边形”意义上没有面积。
这就是cv.moments()中的bug。它计算这些矩时,就好像轮廓是一个实际的多边形一样。对于这个轮廓,它应该返回一个非零的面积(m00),因为轮廓描述了一个非零面积的连通组件。
在OpenCV中,轮廓描述了一个多边形,该多边形需要用1像素的笔画来重现该连接组件的边界像素。轮廓沿着边界像素运行,通过它们的中心点。
这意味着,如果你有一个2x2像素的矩形,你会得到轮廓点在这些边界像素上,比如(1,1),(2,1),(2,2),(1,2)。这个区域的面积只是1(作为数学多边形),而实际上应该是4(在OpenCV的轮廓概念中)。对于一条直线,情况也是一样,但现在你得到的面积是零。
而且你的多边形确实有零面积。你之所以看不到它,是因为这条线不是直的。
OpenCV本可以定义轮廓为围绕连接组件的外部像素的零宽度线条,即沿着像素边缘精确运行。这将需要:
1. OpenCV中的绘图调用不像现在这样错误和被忽视(我直言不讳,但这是事实)。 2. 轮廓的约定允许非整数坐标。
随时可以在OpenCV的github页面上提交问题。cv.moments()的实现需要修复。修复应该是对调用的附加标志,以区分“轮廓”和正确的多边形。这样,数学上正确的行为不会丢失,而是变成可选的。每个人都在轮廓上调用该函数,所以这应该是默认设置。
另一个答案“有效”是因为现在cv.moments()是在从轮廓中绘制的掩码上调用的,而不是在轮廓本身上调用。您可以直接跳过整个轮廓计算。也许可以尝试使用connectedComponents()
另一个答案的解释是错误的,但它没有提出修复建议,只是建议跳过这种情况。

好答案。如果你发现其他答案是错误的,你应该给它们点个踩。踩错的答案是系统希望将这些错误答案视为正确答案的方式。 - Cris Luengo
我保留了包含实际解决方案的答案,即使它只是一个"权宜之计" :) - Christoph Rackwitz
……因为某种原因,错误的答案也得到了另一个赞同票。唉…… - Cris Luengo

2
我认为你得到零的原因是,正如opencv文档所述,你的轮廓是开放的:“轮廓的矩与相同的方式定义,但使用Green's公式计算。”
绿色公式:在数学中,格林定理给出了简单闭合曲线C周围的线积分和平面区域D 由C限制的双重积分之间的关系。(维基百科)
因此,由于轮廓是开放的,它没有被任何区域限制,换句话说,轮廓所包围的区域为零。
为了找到这个问题,我们可以使用cv2.contourArea预先计算轮廓内部的区域。如果该区域为零,则轮廓是开放的,无需继续。
为了检查轮廓是否不自交,我不知道使用opencv的简单方法,但您可以使用Shapely
  1. 将 OpenCV 中的轮廓转换为 Shapely。
  2. 使用 is_simple 检查多边形是否自相交。

在OpenCV中有没有一个函数可以检查轮廓是否简单?据说如果轮廓存在自交点,cv2.contourArea的结果是不确定的。 - linello
我在答案中添加了一种检查自相交的方法。 - Amitay Nachmani

-1

当你有打开的轮廓时,我会给出解决方案。

解决方案

  1. 将轮廓转换为二进制掩码。
  2. 使用二进制掩码计算矩。

关键代码

mask = np.zeros(image.shape[:2], np.uint8)
cv2.fillPoly(mask, pts =[np.asarray(contour)], color=(1))
M = cv2.moments(mask,binaryImage = True)

1
或者通过将第一个顶点附加到列表末尾来关闭轮廓。 - Cris Luengo
1
错误的解释。轮廓并不是开放的。这是在对轮廓调用cv.moments()时出现的错误。根本不需要找轮廓,只需使用connectedComponents()并完全处理掩码。请查看我的答案以获得详细解释。 - Christoph Rackwitz

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接