在我每周的标准作业清单中,有一项是编写计算机视觉算法来计算该图像中米粒的数量:
因此,当我的一个好朋友M给我发了一张纸上的扁豆照片(显然是受到上述转发的启发),请我帮他数一下谷物的数量时,它勾起了我怀旧的回忆。因此,我在我的旧硬盘上寻找很久以前编写的代码作为上述问题的参考解决方案。花了一些时间才找到他们。
旧代码是用C 编写的,并使用现已过时的 OpenCV 1.x API。我当前的 PC 中不再安装旧的库版本,而且由于 Python 现在很流行,我决定使用最新的 OpenCV API 将逻辑移植到 Python 3 代码。
在这篇文章中,我将演示实现上述解决方案的非常简单的步骤,解释所做出的一些算法选择、此处介绍的解决方案的一些替代方案和局限性。请注意,这是一个纯粹的计算机视觉算法解决方案。联系qq1309399183
正如您可能已经推断出的,这假设可能存在一个特定值,使得所有米粒像素都比该值更亮,而所有背景像素都更暗。为简单起见,我们认为本例中的值为 127(0-255 范围的中间)。但查看输出,我们确实意识到相当多的背景像素已被标记为白色,而许多米粒像素已被标记为黑色。现在的问题是,我们是否必须通过反复试验得出一个合适的阈值,或者是否有更结构化的方法来做到这一点?
thresh, output_binthresh = cv.threshold(input_rice, 127, 255, cv.THRESH_BINARY)
print("固定阈值", thresh)
cv.imshow("二进制阈值(固定)", output_binthresh)
使用 Otsu 方法进行二元阈值处理
Nobuyuki Otsu 的阈值选择方法(更广泛地称为Otsu 方法)通过最大化类间方差来统计计算合适的阈值。我们可以使用相同的方法来获得更好质量的二值分割结果。
thresh, output_otsuthresh = cv.threshold(input_rice, 0, 255, cv.THRESH_BINARY | cv.THRESH_OTSU)
print("大津阈值", thresh)
cv.imshow("二进制阈值 (otsu)", output_otsuthresh)
改变照明的解决方案是需要确定仅适合图像的有限部分的不同阈值。局部自适应阈值的作用是根据像素周围有限矩形区域内像素阴影的分布,为每个像素找到统计上合适的阈值。这抵消了照明梯度。
output_adapthresh = cv.adaptiveThreshold(input_rice,255.0,cv.ADAPTIVE_THRESH_MEAN_C,cv.THRESH_BINARY,51,-20.0)
cv.imshow("自适应阈值", output_adapthresh)
矩形区域的大小是一个可调参数。有一些直观的过程可以让它正确。我们选择的区域大小为每个像素周围的 51X51 像素,大约是 512X512 图像尺寸的 10%。这似乎适用于我们的输入样本中的缓慢照明变化。对于快速变化的照明,需要较小的区域。
即使使用局部自适应阈值,似乎可以很好地执行二进制分割,但我们在输出中仍然存在一些小问题。在应该有背景的地方有随机的明亮像素斑点。另外,有些谷物对象是连体的,这可能会导致计数结果出现偏差。我们需要清理分割的图像,使其更适合准确计数。
侵蚀是一种几何变换,旨在减少又名侵蚀前景形状。我们使用 5 像素矩形算子腐蚀图像。这有助于去除外围斑点。也可以分离出连体的颗粒物体。
kernel = np.ones((5,5),np.uint8)
output_erosion = cv.erode(output_adapthresh, kernel)
cv.imshow(“Morphological Erosion”, output_erosion)
由此产生的输出现在已完全准备好用于我们的计数算法。
正如我之前提到的,通过适当的简化和后处理,看似复杂的问题已简化为仅计算不同纯白色物体的数量。我将提出两种替代方案来完成同样的任务。
label_image = output_erosion.copy()
label_count = 0
rows, cols = label_image.shape
for j in range(rows):
for i in range(cols):
pixel = label_image[j, i]
if 255 == pixel:
label_count += 1
cv.floodFill(label_image, None, (i, j), label_count)
print("Number of foreground objects", label_count)
cv.imshow("Connected Components", label_image)
_, contours, _ = cv.findContours(output_erosion, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)
output_contour = cv.cvtColor(input_rice, cv.COLOR_GRAY2BGR)
cv.drawContours(output_contour, contours, -1, (0, 0, 255), 2)
print(“Number of detected contours”, len(contours))
cv.imshow(“Contours”, output_contour)
轮廓检测逻辑的工作原理是找到前景/背景边界像素,然后执行边界跟随逻辑,以链码的形式对外部轮廓形状进行编码.
执行此操作直到覆盖所有前景对象的边界。检测到的独特外部轮廓的数量与米粒的数量相同。
上述算法在给定的输入图像上完成其工作,但仍然需要手动调整一些参数。相同的解决方案可能不足以概括到不同的输入或不同的照明条件下。此外,如果图像中颗粒的密度较高,即大多数颗粒彼此相邻或相互遮挡,则这也会严重失败。