计算机视觉 - 计算图像中的小圆圈
computer vision - Counting small circles in an image
下图有很多圆圈。单击并放大以查看圆圈。
https://drive.google.com/open?id=1ox3kiRX5hf2tHDptWfgcbMTAHKCDizSI
我想要的是使用任何自由语言计算圆圈,例如python。
是否有功能或想法可以做到这一点?
一种可能的解决方案是使用 OpenCV 读取图像,获取其灰度,然后使用 Canny 边缘检测并在 OpenCV 中执行计数查找。这将 return 国家列表。它看起来像:
import cv2
image = cv2.imread('path-to-your-image')
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# tweak the parameters of the GaussianBlur for best performance
blurred = cv2.GaussianBlur(gray, (7, 7), 0)
# again, try different values here
edged = cv2.Canny(blurred, 20, 140)
(_, contours, _) = cv2.findContours(edged.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
print(len(contours))
编辑:我想出了一个更好的解决方案,部分灵感来自 。我最初想到了这种方法(如 OP 评论中所述),但我决定反对它。原始图像的质量不够好。不过,我改进了该方法,它 出色地 工作,以获得更好质量的图像。先是原来的方法,然后是最下面的新方法。
第一种方法
所以这里有一个似乎很有效的通用方法,但绝对只是给出了估计值。这假设圆圈的大小大致相同。
首先,图像大部分是蓝色的---所以只在蓝色通道上进行分析似乎是合理的。对蓝色通道进行阈值处理,在这种情况下,使用 Otsu 阈值处理(在没有输入的情况下确定最佳阈值)似乎效果很好。这并不奇怪,因为颜色值的分布几乎是二元的。检查由此产生的掩码!
然后,对掩码进行连通分量分析,得到每个分量的面积(分量 = 掩码中的白色斑点)。 connectedComponentsWithStats()
返回的统计数据(除其他外)给出了面积,这正是我们所需要的。然后我们可以根据给定组件的面积估算有多少个圆圈适合给定组件,从而简单地计算圆圈数。另请注意,我正在对除第一个标签之外的每个标签进行统计:这是背景标签 0
,而不是任何白色斑点。
现在,一个圆的面积有多大?最好让数据告诉我们。因此,您可以计算所有区域的直方图,并且由于单个圆圈比其他任何圆圈都多,因此该区域将高度集中在 250-270 像素左右。或者你可以取 50 到 350 之间所有区域的平均值,这也应该让你处于类似的范围内。
真的在这个直方图中你可以很容易地看到单圈、双圈、三圈等之间的分界。只有较大的组件才会给出相当粗略的估计。事实上,该区域似乎并没有完全线性扩展。两个圆的斑点比两个单圆略大,三个圆的斑点仍然比三个单圆大,等等,所以这使得它有点难以很好地估计,但四舍五入应该让我们接近。如果您愿意,您可以包括一个小的乘法参数,该参数会随着面积的增加而增加以解决这个问题,但是如果不分析直方图就很难量化……所以,我不担心这个。
单圈面积除以单圈平均面积应该接近1,5圈组面积除以平均圈面积应该接近5,这也意味着小的无足轻重自 round(50/avg_circle_size) < 1/2
以来,面积为 1 或 10 甚至 100 像素的组件将不计入总数,因此这些组件将向下舍入为 0。因此我应该能够获取所有组件区域,将它们除以平均圆的大小,round,然后将它们相加得到一个不错的估计值。
import cv2
import numpy as np
img = cv2.imread('circles.png')
mask = cv2.threshold(img[:, :, 0], 255, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)[1]
stats = cv2.connectedComponentsWithStats(mask, 8)[2]
label_area = stats[1:, cv2.CC_STAT_AREA]
min_area, max_area = 50, 350 # min/max for a single circle
singular_mask = (min_area < label_area) & (label_area <= max_area)
circle_area = np.mean(label_area[singular_mask])
n_circles = int(np.sum(np.round(label_area / circle_area)))
print('Total circles:', n_circles)
此代码简单有效,可用于粗略计数。
但是,与正常的圆圈大小相比,这里肯定有一些关于圆圈组的假设,并且存在边界处的圆圈无法正确计数的问题(这些没有明确定义 - - 被切掉一半的两个圆圈看起来更像一个圆圈——没有明确的方法可以用这种方法计算或不计算这些)。此外,我只是在这里通过 Otsu 使用了自动阈值处理;通过更仔细的颜色过滤,您可以获得(可能更好)的结果。此外,在 Otsu 生成的蒙版中,一些被蒙版的圆从中心移除了一些像素。形态学可以将这些像素重新添加进去,这将为您提供(稍大)更准确的单个圆组件区域。无论哪种方式,我只是想给出关于如何用最少的代码轻松估计它的一般概念。
新方法
以前,目标是数圈。这种新方法改为计算圆的 centers。一般的想法是你设置阈值,然后从背景像素进行洪水填充以填充背景(洪水填充就像照片编辑应用程序中的油漆桶工具),这样你只能看到中心,如 所示.
但是,这依赖于全局阈值,它对局部光照变化不稳健。这意味着由于某些中心比其他中心 brighter/darker,您不会总是通过单一阈值获得良好的结果。
这里我创建了一个动画来显示循环通过不同的阈值;观察一些中心在不同时间出现和消失,这意味着你会得到不同的计数n 你选择的阈值(这只是图像的一小块,它无处不在):
请注意,随着阈值的增加,出现在左上角的第一个斑点实际上消失了。但是,如果我们实际上将每个帧都或在一起,那么每个检测到的像素都会持续存在:
但是现在每个斑点都出现了,所以我们应该在每一帧清理蒙版,以便我们在单个像素出现时将其移除(否则它们可能会堆积起来,以后很难移除)。带有小内核的简单 morphological opening 将删除它们:
应用于整个图像,此方法效果非常好并且几乎可以找到每个单元格。我只能发现三个误报(检测到的不是中心的斑点)和两个未命中,代码非常简单。创建蒙版后要做的最后一件事就是简单地计算组件数量,减去背景组件。此处唯一需要的用户输入是在后台进行洪水填充的单个点(代码中的seed_pt
)。
img = cv2.imread('circles.png', 0)
seed_pt = (25, 25)
fill_color = 0
mask = np.zeros_like(img)
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
for th in range(60, 120):
prev_mask = mask.copy()
mask = cv2.threshold(img, th, 255, cv2.THRESH_BINARY)[1]
mask = cv2.floodFill(mask, None, seed_pt, fill_color)[1]
mask = cv2.bitwise_or(mask, prev_mask)
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
n_centers = cv2.connectedComponents(mask)[0] - 1
print('There are %d cells in the image.'%n_centers)
There are 874 cells in the image.
如果您有所有这样的图像 - 考虑对其进行阈值处理,不一定是像 Otsu 这样的自动阈值搜索算法,而是使用给定阈值的最简单阈值。是的,在设置阈值之前,您必须将颜色输入转换为灰度,或者采用其中一个颜色通道。然后基于对通道和阈值的少数实验 - 确定阈值以在单色阈值处理结果中有带孔的圆圈。根据您的 png 图像,我发现 81 的值(灰色强度从 0 到 255 不等)非常适合您输入的阈值灰度版本,以便具有这样的带有孔的二进制图像,如上所述。
然后简单地数那些洞。
空洞可以通过种子填充白色区域来确定,连接到图像边框。结果你将在黑色背景上有白洞连接的组件 - 所以简单地计算它们。
您可以在此处找到更多详细信息 http://www.leptonica.com/filling.html 并使用 leptonica 基元进行阈值处理、孔计数等。
下图有很多圆圈。单击并放大以查看圆圈。
https://drive.google.com/open?id=1ox3kiRX5hf2tHDptWfgcbMTAHKCDizSI
我想要的是使用任何自由语言计算圆圈,例如python。
是否有功能或想法可以做到这一点?
一种可能的解决方案是使用 OpenCV 读取图像,获取其灰度,然后使用 Canny 边缘检测并在 OpenCV 中执行计数查找。这将 return 国家列表。它看起来像:
import cv2
image = cv2.imread('path-to-your-image')
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# tweak the parameters of the GaussianBlur for best performance
blurred = cv2.GaussianBlur(gray, (7, 7), 0)
# again, try different values here
edged = cv2.Canny(blurred, 20, 140)
(_, contours, _) = cv2.findContours(edged.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
print(len(contours))
编辑:我想出了一个更好的解决方案,部分灵感来自
第一种方法
所以这里有一个似乎很有效的通用方法,但绝对只是给出了估计值。这假设圆圈的大小大致相同。
首先,图像大部分是蓝色的---所以只在蓝色通道上进行分析似乎是合理的。对蓝色通道进行阈值处理,在这种情况下,使用 Otsu 阈值处理(在没有输入的情况下确定最佳阈值)似乎效果很好。这并不奇怪,因为颜色值的分布几乎是二元的。检查由此产生的掩码!
然后,对掩码进行连通分量分析,得到每个分量的面积(分量 = 掩码中的白色斑点)。 connectedComponentsWithStats()
返回的统计数据(除其他外)给出了面积,这正是我们所需要的。然后我们可以根据给定组件的面积估算有多少个圆圈适合给定组件,从而简单地计算圆圈数。另请注意,我正在对除第一个标签之外的每个标签进行统计:这是背景标签 0
,而不是任何白色斑点。
现在,一个圆的面积有多大?最好让数据告诉我们。因此,您可以计算所有区域的直方图,并且由于单个圆圈比其他任何圆圈都多,因此该区域将高度集中在 250-270 像素左右。或者你可以取 50 到 350 之间所有区域的平均值,这也应该让你处于类似的范围内。
真的在这个直方图中你可以很容易地看到单圈、双圈、三圈等之间的分界。只有较大的组件才会给出相当粗略的估计。事实上,该区域似乎并没有完全线性扩展。两个圆的斑点比两个单圆略大,三个圆的斑点仍然比三个单圆大,等等,所以这使得它有点难以很好地估计,但四舍五入应该让我们接近。如果您愿意,您可以包括一个小的乘法参数,该参数会随着面积的增加而增加以解决这个问题,但是如果不分析直方图就很难量化……所以,我不担心这个。
单圈面积除以单圈平均面积应该接近1,5圈组面积除以平均圈面积应该接近5,这也意味着小的无足轻重自 round(50/avg_circle_size) < 1/2
以来,面积为 1 或 10 甚至 100 像素的组件将不计入总数,因此这些组件将向下舍入为 0。因此我应该能够获取所有组件区域,将它们除以平均圆的大小,round,然后将它们相加得到一个不错的估计值。
import cv2
import numpy as np
img = cv2.imread('circles.png')
mask = cv2.threshold(img[:, :, 0], 255, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)[1]
stats = cv2.connectedComponentsWithStats(mask, 8)[2]
label_area = stats[1:, cv2.CC_STAT_AREA]
min_area, max_area = 50, 350 # min/max for a single circle
singular_mask = (min_area < label_area) & (label_area <= max_area)
circle_area = np.mean(label_area[singular_mask])
n_circles = int(np.sum(np.round(label_area / circle_area)))
print('Total circles:', n_circles)
此代码简单有效,可用于粗略计数。
但是,与正常的圆圈大小相比,这里肯定有一些关于圆圈组的假设,并且存在边界处的圆圈无法正确计数的问题(这些没有明确定义 - - 被切掉一半的两个圆圈看起来更像一个圆圈——没有明确的方法可以用这种方法计算或不计算这些)。此外,我只是在这里通过 Otsu 使用了自动阈值处理;通过更仔细的颜色过滤,您可以获得(可能更好)的结果。此外,在 Otsu 生成的蒙版中,一些被蒙版的圆从中心移除了一些像素。形态学可以将这些像素重新添加进去,这将为您提供(稍大)更准确的单个圆组件区域。无论哪种方式,我只是想给出关于如何用最少的代码轻松估计它的一般概念。
新方法
以前,目标是数圈。这种新方法改为计算圆的 centers。一般的想法是你设置阈值,然后从背景像素进行洪水填充以填充背景(洪水填充就像照片编辑应用程序中的油漆桶工具),这样你只能看到中心,如
但是,这依赖于全局阈值,它对局部光照变化不稳健。这意味着由于某些中心比其他中心 brighter/darker,您不会总是通过单一阈值获得良好的结果。
这里我创建了一个动画来显示循环通过不同的阈值;观察一些中心在不同时间出现和消失,这意味着你会得到不同的计数n 你选择的阈值(这只是图像的一小块,它无处不在):
请注意,随着阈值的增加,出现在左上角的第一个斑点实际上消失了。但是,如果我们实际上将每个帧都或在一起,那么每个检测到的像素都会持续存在:
但是现在每个斑点都出现了,所以我们应该在每一帧清理蒙版,以便我们在单个像素出现时将其移除(否则它们可能会堆积起来,以后很难移除)。带有小内核的简单 morphological opening 将删除它们:
应用于整个图像,此方法效果非常好并且几乎可以找到每个单元格。我只能发现三个误报(检测到的不是中心的斑点)和两个未命中,代码非常简单。创建蒙版后要做的最后一件事就是简单地计算组件数量,减去背景组件。此处唯一需要的用户输入是在后台进行洪水填充的单个点(代码中的seed_pt
)。
img = cv2.imread('circles.png', 0)
seed_pt = (25, 25)
fill_color = 0
mask = np.zeros_like(img)
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
for th in range(60, 120):
prev_mask = mask.copy()
mask = cv2.threshold(img, th, 255, cv2.THRESH_BINARY)[1]
mask = cv2.floodFill(mask, None, seed_pt, fill_color)[1]
mask = cv2.bitwise_or(mask, prev_mask)
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
n_centers = cv2.connectedComponents(mask)[0] - 1
print('There are %d cells in the image.'%n_centers)
There are 874 cells in the image.
如果您有所有这样的图像 - 考虑对其进行阈值处理,不一定是像 Otsu 这样的自动阈值搜索算法,而是使用给定阈值的最简单阈值。是的,在设置阈值之前,您必须将颜色输入转换为灰度,或者采用其中一个颜色通道。然后基于对通道和阈值的少数实验 - 确定阈值以在单色阈值处理结果中有带孔的圆圈。根据您的 png 图像,我发现 81 的值(灰色强度从 0 到 255 不等)非常适合您输入的阈值灰度版本,以便具有这样的带有孔的二进制图像,如上所述。
然后简单地数那些洞。
空洞可以通过种子填充白色区域来确定,连接到图像边框。结果你将在黑色背景上有白洞连接的组件 - 所以简单地计算它们。
您可以在此处找到更多详细信息 http://www.leptonica.com/filling.html 并使用 leptonica 基元进行阈值处理、孔计数等。