测量带有opencv的远心单色相机拍摄的金属零件孔的直径图片

Ben*_*s.M 6 python opencv image-processing computer-vision hough-transform

设定:

  • 相机:Blackfly S Mono 20.0 MP
  • 镜头:光远心镜头TC23080
  • 灯:16个绿色LED
  • 的Python:3.7.3
  • openCV:4.0以上

图片链接很抱歉,但是一张图片大约20MB,也不想丢失任何质量

图片样本:

https://drive.google.com/file/d/11PU-5fzvSJt1lKlmP-lQXhdsuCJPGKbN/view?usp=sharing https://drive.google.com/file/d/1B3lSFx8YvTYv3hzuuuYtphoHBuyEdc4o/view

案例:会有大小从5x5到10x10的不同形状的金属零件。在这些金属零件内部,有许多2至10〜的圆形孔,必须非常准确地检测到。孔的实际大小是未知的,因为可能的零件种类繁多。目的是使用OpenCV编写通用算法,该算法可与任何金属零件配合使用并检测圆形孔。

我们尝试过的方法:我们尝试使用HoughCircles算法检测孔,但几乎没有成功。该算法过于敏感,或者根本无法检测到孔。我们已经尝试了不同的param1和param2值,但没有成功。在使用HoughCircles之前,我们还尝试过使图像模糊并通过Canny,但是这种方法并未产生更好的结果。对于较低分辨率的图片,完全相同的算法效果更好。但是,不能牺牲分辨率,因为精度在此项目中极为重要。

https://drive.google.com/file/d/1TRdDbperi37bha0uJVALS4C2dBuaNz6u/view?usp=sharing

使用以下参数检测到上述圆圈:

minradius=0
maxradius=0
dp=1
param1=100
param2=21
Run Code Online (Sandbox Code Playgroud)

通过使用以上参数,我们几乎可以获得所需的结果。当我们对不同的图片使用相同的参数时,就会出现问题。

我们想要得到的最终结果是给定圆的直径具有很高的精度,并且我们希望在不同的零件图片上可以使用相同的算法

使这个问题与其他发布的问题不同的原因是,我们不知道给定圆的近似半径(因此我们无法操纵minradius,maxradius,param1,param2或任何其他值)。

Cri*_*ngo 5

关于这些图像,我们知道两件事:

  1. 物体在明亮的背景上是暗的。
  2. 孔都是圆的,我们要测量所有的孔。

所以我们需要做的就是检测漏洞。这实际上非常简单:

  1. 阈值(背景变成物体,因为它很亮)
  2. 移除边缘对象

剩下的是洞。将不包括任何接触图像边缘的孔。我们现在可以轻松测量这些孔。由于我们假设它们是圆形的,我们可以做三件事:

  1. 计算对象像素,这是对区域的无偏估计。根据面积,我们确定孔的直径。
  2. 检测轮廓,找到质心,然后使用例如轮廓点到质心的平均距离作为半径。
  3. 对图像强度进行归一化,使背景照明的强度为 1,其中有孔的物体的强度为 0。每个孔的强度积分是一个子像素——区域的精确估计(参见在底部快速解释此方法)。

此 Python 代码使用DIPlib(我是作者)展示了如何执行这三种方法:

import PyDIP as dip
import numpy as np

img = dip.ImageRead('geriausias.bmp')
img.SetPixelSize(dip.PixelSize(dip.PhysicalQuantity(1,'um'))) # Usually this info is in the image file
bin, thresh = dip.Threshold(img)
bin = dip.EdgeObjectsRemove(bin)
bin = dip.Label(bin)
msr = dip.MeasurementTool.Measure(bin, features=['Size','Radius'])
print(msr)
d1 = np.sqrt(np.array(msr['Size'])[:,0] * 4 / np.pi)
print("method 1:", d1)
d2 = np.array(msr['Radius'])[:,1] * 2
print("method 2:", d2)

bin = dip.Dilation(bin, 10) # we need larger regions to average over so we take all of the light
                            # coming through the hole into account.
img = (dip.ErfClip(img, thresh, thresh/4, "range") - (thresh*7/8)) / (thresh/4)
msr = dip.MeasurementTool.Measure(bin, img, features=['Mass'])
d3 = np.sqrt(np.array(msr['Mass'])[:,0] * 4 / np.pi)
print("method 3:", d3)
Run Code Online (Sandbox Code Playgroud)

这给出了输出:

  |       Size |                                            Radius | 
- | ---------- | ------------------------------------------------- | 
  |            |        Max |       Mean |        Min |     StdDev | 
  |      (µm²) |       (µm) |       (µm) |       (µm) |       (µm) | 
- | ---------- | ---------- | ---------- | ---------- | ---------- | 
1 |  6.282e+04 |      143.9 |      141.4 |      134.4 |      1.628 | 
2 |  9.110e+04 |      171.5 |      170.3 |      168.3 |     0.5643 | 
3 |  6.303e+04 |      143.5 |      141.6 |      133.9 |      1.212 | 
4 |  9.103e+04 |      171.6 |      170.2 |      167.3 |     0.6292 | 
5 |  6.306e+04 |      143.9 |      141.6 |      126.5 |      2.320 | 
6 |  2.495e+05 |      283.5 |      281.8 |      274.4 |     0.9805 | 
7 |  1.176e+05 |      194.4 |      193.5 |      187.1 |     0.6303 | 
8 |  1.595e+05 |      226.7 |      225.3 |      219.8 |     0.8629 | 
9 |  9.063e+04 |      171.0 |      169.8 |      167.6 |     0.5457 | 

method 1: [282.8250363  340.57242408 283.28834869 340.45277017 283.36249824
 563.64770132 386.9715443  450.65294139 339.70023023]
method 2: [282.74577033 340.58808144 283.24878097 340.43862835 283.1641869
 563.59706479 386.95245928 450.65392268 339.68617582]
method 3: [282.74836803 340.56787463 283.24627163 340.39568372 283.31396961
 563.601641   386.89884807 450.62167913 339.68954136]
Run Code Online (Sandbox Code Playgroud)

bin调用后的图像dip.Label是一个整数图像,其中孔 1 的像素值都为 1,孔 2 的像素值为 2,等等。所以我们仍然保留测量尺寸和它们是哪个孔之间的关系。我没有费心制作显示图像大小的标记图像,但是正如您在其他答案中看到的那样,这可以轻松完成。

因为图像文件中没有像素大小信息,所以我强加了每像素 1 微米。这可能不正确,您必须进行校准才能获得像素大小信息

这里的一个问题是背景照明太亮,导致像素饱和。这会导致孔看起来比实际大。校准系统很重要,以便背景照明接近相机可以记录的最大值,但不是最大值也不是更高。例如,尝试将背景强度设为 245 或 250。第 3 种方法受不良照明影响最大。

对于第二幅图像,亮度非常低,给出了不必要的噪声图像。我需要将该行修改bin = dip.Label(bin)为:

bin = dip.Label(bin, 2, 500) # Imposing minimum object size rather than filtering
Run Code Online (Sandbox Code Playgroud)

相反,进行一些噪声过滤可能更容易。输出是:

  |       Size |                                            Radius | 
- | ---------- | ------------------------------------------------- | 
  |            |        Max |       Mean |        Min |     StdDev | 
  |      (µm²) |       (µm) |       (µm) |       (µm) |       (µm) | 
- | ---------- | ---------- | ---------- | ---------- | ---------- | 
1 |  4.023e+06 |      1133. |      1132. |      1125. |     0.4989 | 

method 1: [2263.24621554]
method 2: [2263.22724164]
method 3: [2262.90068056]
Run Code Online (Sandbox Code Playgroud)

方法#3的快速解释

该方法在Lucas van Vliet(代尔夫特理工大学,1993 年)的博士论文第 6 章中有所描述。

可以这样想:穿过孔的光量与孔的面积成正比(实际上它由“面积”x“光强度”给出)。通过将穿过孔的所有光加起来,我们就知道了孔的面积。该代码将对象的所有像素强度以及对象外部的一些像素相加(我在那里使用了 10 个像素,要走多远取决于模糊)。

erfclip函数称为“软剪辑”函数,它保证孔内的强度统一为1,孔外的强度统一为0,并且仅在边缘周围留下中间灰度值。在这种特殊情况下,这种软剪辑避免了成像系统中的一些偏移问题以及对光强度的错误估计。在其他情况下,避免被测物体颜色不均匀的问题更为重要。它还减少了噪音的影响。