C# – 从照片中识别黑点

我有一些白页的照片,上面画着一些黑点,像这样: 照片 (点不是很圆,我可以画得更好),我会找到这些点的坐标。 我可以将图像二值化(上一张照片二值化: 图像 ),但是如何找到这些黑点的坐标? 我只需要每个点的一个像素的坐标,即大致中心。

这是为了学校作业。

处理图像数据的基础知识可以在其他问题中找到,所以我不会详细介绍它,但具体来说,对于阈值检查,我会通过收集每个像素的红色,绿色和蓝色字节来做到这一点(如我所链接的答案中所示,然后将它们组合成Color c = Color.FromArgb(r,g,b)并使用c.GetBrightness() < brightnessThreshold测试为“暗”。 值为0.4是测试图像的良好阈值。

您应该将此阈值检测的结果存储在一个数组中,其中每个项都是一个值,指示阈值检查是通过还是失败。 这意味着您可以使用与原始图像的高度和宽度一样简单的二Boolean数组。

如果你已经有了做所有这些的方法,那就更好了。 只要确保你有一些数组,你可以很容易地查找二值化的结果。 如果你的方法给你的结果是图像,你将更有可能最终得到一个简单的一维字节数组,但你的查找将只是一个像imagedata[y * stride + x]的格式。 这在function上与二维数组中的内部查找发生的方式相同,因此效率不会太低。


现在,正如我在评论中所说的,这里的真实内容将是一种算法,用于检测哪些像素应该组合成一个“blob”。

此算法的一般用法是循环覆盖图像上的每个像素,然后检查A)是否清除了阈值,B)它是否已经存在于您现有的某个检测到的斑点中。 如果像素符合条件,则生成连接到此像素的所有阈值传递像素的新列表,并将该新列表添加到检测到的blob列表中。 我使用Point类来收集坐标,使我的每个blob成为List ,我的blob集合成为List>

至于算法本身,你要做的是制作两个点集合。 一个是您正在构建的相邻点的完整集合( 点列表 ),另一个是您正在扫描的当前边缘当前边缘列表 )。 当前边缘列表将以包含原点的方式开始,只要当前边缘列表中有项目,以下步骤就会循环:

  • 当前边缘列表中的所有项目添加到完整点列表中
  • 为下一个边缘( 下一个边缘列表 )创建一个新的集合。
  • 对于当前边缘列表中的每个点,获取其直接相邻点的列表(不包括任何超出图像边界的点),并检查所有这些点是否清除阈值,以及它们是否已经存在点列表下一个边列表 。 将通过检查的点添加到下一个边列表
  • 在此循环通过当前边缘列表结束后,将原始当前边缘列表替换为下一个边缘列表

......并且,正如我所说,只要在最后一步之后当前的边缘列表不为空,就循环这些步骤。

这将创建一个扩展边缘,直到它匹配所有阈值清除像素,并将它们全部添加到列表中。 最终,由于所有相邻像素最终都在主列表中,新生成的边缘列表将变为空,算法将结束。 然后将新的点列表添加到blob列表中,之后循环的任何像素都可以被检测为已存在于这些blob中,因此不会对它们重复算法。

有两种方法可以做邻近点; 你得到它周围的四个点,或者全部八个点。 不同的是,使用四个不会使算法做对角线跳跃,而使用八个会。 (另外一个效果是,一个导致算法以菱形扩展,而另一个扩展为正方形。)因为你的blob周围似乎有一些杂散像素,所以我建议你得到所有八个。

正如史蒂夫在他的回答中指出的那样,一种非常快速的方法来检查集合中是否存在一个点是创建一个具有图像尺寸的二Boolean[,] inBlob = new Boolean[height, width];数组,例如Boolean[,] inBlob = new Boolean[height, width]; ,您与实际的点列表保持同步。 因此,无论何时添加点,还要将布尔数组中的[y, x]位置标记为true 。 这将对if (collection.contains(point))类型进行相当大量的检查,就像if (inBlob[y,x])一样简单, 根本不需要迭代

我有一个List inBlobs ,我与我建立的List> blobs保持同步,在扩展边缘算法中,我为下一个边缘列表保留了这样的Boolean[,] 点列表 (后者在最后添加到inBlobs中)。

正如我评论的那样,一旦你获得了blob,只需在每个blob上遍历它们内部的点,并获得X和Y的最小值和最大值,这样就可以得到blob的边界。 然后只需取平均值即可获得blob的中心。

附加function:

  • 如果所有的点都保证相距很远,那么摆脱浮动边缘像素的一种非常简单的方法是获取每个blob的边缘边界,将它们全部扩展一定的阈值(我花了2个像素),然后循环遍历这些矩形并检查是否有任何相交,并合并那些。 Rectangle类既有一个IntersectsWith() ,可以方便地检查,还有一个静态的Rectangle.Inflate用于增加矩形的大小。

  • 您可以通过仅在主列表中存储边缘点(在四个主要方向中的任何一个中具有不匹配邻居的阈值匹配点)来优化填充方法的内存使用。 最终的边界,也就是中心,将保持不变。 需要记住的重要一点是,当你从blob列表中排除一堆点时,你应该Boolean[,]数组中标记所有这些点,这些数组用于检查已处理的像素。 无论如何,这不占用任何额外的内存。

使用0.4作为亮度阈值,对照片执行完整算法(包括优化):

检测到斑点

蓝色是检测到的斑点,红色是检测到的轮廓(通过使用内存优化方法),单个绿色像素表示所有斑点的中心点。

自从它用于学校工作以来,我只会为您提供高级算法。

由于背景是白色的保证,你很幸运。

首先,您需要在黑色等级上定义一个阈值,您要将其视为黑点的颜色。

#ffffff是纯白色, #000000是纯黑色。 我会建议像#383838这样的地方作为你的门槛。

然后你制作一个二维bool数组来跟踪你已经访问过的像素。

现在我们可以开始看图了。

您可以水平读取一个像素,并查看像素是否>阈值。 如果是,则执行DFS或BFS以查找像素的邻居也>阈值的整个区域。

在此过程中,您将标记我们之前创建的bool数组,以指示您已访问过该像素。

因为它是一个圆点,你可以只取最小值x和y坐标并计算中心点。

一旦完成一个点,你将继续通过图片的像素循环并找到你没有访问过的点(在bool数组中为false)

由于您在照片上的点包含边缘上没有连接到大点的一些小点,您可能需要做一些数学运算来查看半径是否>某个数字以考虑有效点。 或者不是半径为1的邻居,而是使用5-10像素的邻居BFS / DFS来包含那些非常接近主要点的邻居。