编辑:我想出了一个更好的解决方案,部分灵感来自this answer below。我最初想到了这种方法(如 OP cmets 中所述),但我决定反对它。原始图像的质量不够好。不过,我改进了该方法,它非常出色地工作,以获得更好的图像质量。原来的方法是第一位的,然后是底部的新方法。
第一种方法
所以这里有一个通用的方法,看起来效果很好,但肯定只是给出了估计。这假设圆圈的大小大致相同。
首先,图像大部分是蓝色的——所以只对蓝色通道进行分析似乎是合理的。对蓝色通道进行阈值处理,在这种情况下,使用 Otsu 阈值(无需输入即可确定最佳阈值)似乎效果很好。这并不奇怪,因为颜色值的分布几乎是二进制的。检查由此产生的掩码!
然后,对 mask 进行连通分量分析,得到每个分量的面积(分量 = mask 中的白色斑点)。从connectedComponentsWithStats() 返回的统计信息(除其他外)给出了该区域,这正是我们所需要的。然后,我们可以通过根据其面积估计给定组件中有多少圆适合来简单地计算圆。另请注意,除了第一个标签之外,我正在统计每个标签:这是背景标签 0,而不是任何白色斑点。
现在,一个圆的面积有多大?最好让数据告诉我们。因此,您可以计算所有区域的直方图,并且由于单个圆圈比其他任何东西都多,因此该区域将高度集中在 250-270 像素左右。或者,您可以只取 50 到 350 之间的所有区域的平均值,这也应该让您进入类似的范围。
真的,在这个直方图中,您可以很容易地看到单圈、双圈、三圈等之间的分界线。只有较大的组件才会给出相当粗略的估计。事实上,该区域似乎并没有完全线性地缩放。两个圆的斑点比两个单圆稍大,三个圆的斑点仍然比三个单圆大,依此类推,因此很难很好地估计,但四舍五入仍应使我们保持接近。如果您愿意,您可以包含一个小的乘法参数,该参数会随着面积的增加而增加以说明这一点,但如果不通过直方图进行分析,这将很难量化......所以,我不担心这个。
单圈面积除以平均单圈面积应该接近1。而5圈组的面积除以平均圈面积应该接近5。这也意味着小的不显着的成分,面积为 1 或 10 甚至 100 像素,自round(50/avg_circle_size) < 1/2 以来将不计入总数,因此这些将向下舍入为 0。因此我应该能够获取所有组件区域,将它们除以平均圆的大小,圆形,并通过将它们全部加起来得到一个不错的估计。
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)
此代码简单有效,可用于粗略计数。
但是,与正常的圆圈大小相比,这里肯定有一些关于圆圈组的假设,并且存在无法正确计算边界处的圆圈的问题(这些没有明确定义---a两个半截掉的圆形斑点看起来更像一个圆圈——没有明确的方法来计算或不计算这些用这种方法)。此外,我在这里通过 Otsu 使用了自动阈值;通过更仔细的颜色过滤,您可以获得(可能更好)的结果。此外,在 Otsu 生成的蒙版中,一些被蒙版的圆圈从其中心移除了一些像素。形态学可以重新添加这些像素,这将为您提供(稍大)更准确的单个圆形组件区域。无论哪种方式,我只是想就如何用最少的代码轻松估计这一点给出一个总体思路。
新方法
以前,目标是计算圈数。这种新方法改为计算圆的中心。一般的想法是你设置阈值,然后从背景像素填充填充以填充背景(填充填充就像照片编辑应用程序中的油漆桶工具一样),这样你只能看到中心,如this answer below 所示。
但是,这依赖于全局阈值,它对局部光照变化并不可靠。这意味着由于某些中心比其他中心更亮/更暗,因此您不会总是通过单一阈值获得良好的结果。
在这里,我创建了一个动画来显示循环通过不同的阈值;观察一些中心在不同时间出现和消失,这意味着您会根据您选择的阈值获得不同的计数(这只是图像的一小部分,它无处不在):
请注意,出现在左上角的第一个斑点实际上会随着阈值的增加而消失。但是,如果我们实际上将每一帧 OR 在一起,那么每个检测到的像素都会持续存在:
但是现在每一个斑点都出现了,所以我们应该清理每一帧的遮罩,以便在它们出现时移除单个像素(否则它们可能会堆积起来,以后很难移除)。带有小内核的简单morphological opening 将删除它们:
应用于整个图像,此方法效果非常好,几乎可以找到每个单元格。我可以发现只有三个误报(检测到的不是中心的 blob)和两个未命中,而且代码非常简单。创建蒙版后要做的最后一件事是简单地计算组件,减去背景。此处唯一需要的用户输入是在后台(代码中的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)
图像中有 874 个单元格。