测量金属零件孔径的图片,远心拍摄,单色相机用opencv
Measuring the diameter pictures of holes in metal parts, photographed with telecentric, monochrome camera with opencv
设置:
- 相机: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
通过以上参数的摆弄,我们几乎可以得到我们想要的结果。当我们对不同的图片使用相同的参数时,问题就出现了。
我们想要得到的最终结果是非常精确的给定圆的直径,我们希望相同的算法可用于不同的部分图片
这个问题与发布的其他问题的不同之处在于我们不知道给定圆的大致半径(因此我们无法操纵 minradius、maxradius、param1、param2 或任何其他值)。
您可以对图像进行阈值处理并使用 findContours
找到孔的轮廓,然后使用 minEnclosingCircle
将圆拟合到它们上。可以通过将拟合的圆与等高线的面积进行比较来检查拟合圆的完整性。
import cv2 as cv
import math
import numpy as np
from matplotlib import pyplot as pl
gray = cv.imread('geriausias.bmp', cv.IMREAD_GRAYSCALE)
_,mask = cv.threshold(gray, 127, 255, cv.THRESH_BINARY)
contours,_ = cv.findContours(mask, cv.RETR_LIST, cv.CHAIN_APPROX_NONE)
contours = [contour for contour in contours if len(contour) > 15]
circles = [cv.minEnclosingCircle(contour) for contour in contours]
areas = [cv.contourArea(contour) for contour in contours]
radiuses = [math.sqrt(area / math.pi) for area in areas]
# Render contours blue and circles green.
canvas = cv.cvtColor(mask, cv.COLOR_GRAY2BGR)
cv.drawContours(canvas, contours, -1, (255, 0, 0), 10)
for circle, radius_from_area in zip(circles, radiuses):
if 0.9 <= circle[1] / radius_from_area <= 1.1: # Only allow 10% error in radius.
p = (round(circle[0][0]), round(circle[0][1]))
r = round(circle[1])
cv.circle(canvas, p, r, (0, 255, 0), 10)
cv.imwrite('geriausias_circles.png', canvas)
canvas_small = cv.resize(canvas, None, None, 0.25, 0.25, cv.INTER_AREA)
cv.imwrite('geriausias_circles_small.png', canvas_small)
通过健全性检查的圆在所有以蓝色显示的等高线之上以绿色显示。
这是一个方法
- 将图像转换为灰度和高斯模糊
- 自适应阈值
- 对smooth/filter图像进行形态学变换
- 寻找轮廓
- 求等高线的周长并进行等高线近似
- 获取边界矩形和质心得到直径
找到轮廓后,我们进行轮廓近似。这个想法是,如果近似轮廓有三个个顶点,那么它一定是一个三角形。同样,如果它有四个,它一定是正方形或长方形。因此我们可以假设,如果它的顶点数大于一定数量,那么它就是一个圆。
有几种获取直径的方法,一种方法是找到轮廓的边界矩形并使用其宽度。另一种方法是从质心坐标计算它。
import cv2
image = cv2.imread('1.bmp')
# Gray, blur, adaptive threshold
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
blur = cv2.GaussianBlur(gray, (3,3), 0)
thresh = cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)[1]
# Morphological transformations
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5,5))
opening = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel)
# Find contours
cnts = cv2.findContours(opening, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
cnts = cnts[0] if len(cnts) == 2 else cnts[1]
for c in cnts:
# Find perimeter of contour
perimeter = cv2.arcLength(c, True)
# Perform contour approximation
approx = cv2.approxPolyDP(c, 0.04 * perimeter, True)
# We assume that if the contour has more than a certain
# number of verticies, we can make the assumption
# that the contour shape is a circle
if len(approx) > 6:
# Obtain bounding rectangle to get measurements
x,y,w,h = cv2.boundingRect(c)
# Find measurements
diameter = w
radius = w/2
# Find centroid
M = cv2.moments(c)
cX = int(M["m10"] / M["m00"])
cY = int(M["m01"] / M["m00"])
# Draw the contour and center of the shape on the image
cv2.rectangle(image,(x,y),(x+w,y+h),(0,255,0),4)
cv2.drawContours(image,[c], 0, (36,255,12), 4)
cv2.circle(image, (cX, cY), 15, (320, 159, 22), -1)
# Draw line and diameter information
cv2.line(image, (x, y + int(h/2)), (x + w, y + int(h/2)), (156, 188, 24), 3)
cv2.putText(image, "Diameter: {}".format(diameter), (cX - 50, cY - 50), cv2.FONT_HERSHEY_SIMPLEX, 3, (156, 188, 24), 3)
cv2.imwrite('image.png', image)
cv2.imwrite('thresh.png', thresh)
cv2.imwrite('opening.png', opening)
关于这些图像我们知道两件事:
- 对象是黑暗的,在明亮的背景上。
- 孔都是圆的,我们要测量所有的孔。
所以我们需要做的就是检测漏洞。这实际上很简单:
- 阈值(背景成为对象,因为它很亮)
- 删除边缘对象
剩下的就是坑了。不包括任何接触图像边缘的孔。我们现在可以很容易地测量这些孔。由于我们假设它们是圆形的,我们可以做三件事:
- 计算对象像素,这是对面积的无偏估计。从面积我们确定孔径。
- 检测轮廓,找到质心,然后使用例如轮廓点到质心的平均距离为半径。
- 归一化图像强度,使背景照明的强度为 1,其中有孔的物体强度为 0。每个孔的强度积分是区域的亚像素精度估计(有关此方法的快速说明,请参阅底部)。
此 Python 代码使用 DIPlib(免责声明:我是作者)展示了如何执行这三种方法:
import diplib as dip
import numpy as np
img = dip.ImageRead('geriausias.bmp')
img.SetPixelSize(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)
这给出了输出:
| 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]
图像bin
在调用dip.Label
后是一个整数图像,其中孔1的像素都具有值1,孔2的像素具有值2,等等。所以我们仍然保留测量尺寸与它们是哪些孔之间的关系。我没有费心制作一个显示图像尺寸的标记图像,但这可以很容易地完成,正如您在其他答案中看到的那样。
因为图像文件中没有像素大小信息,我强加了每像素 1 微米。这可能不正确,您必须进行 校准以获得像素大小信息 。
这里的一个问题是背景照明太亮,导致像素饱和。这会导致孔看起来比实际大。 校准系统很重要,这样背景照明接近相机可以记录的最大值,但不是那个最大值也不高于。例如,尝试将背景强度设置为245或250。第三种方法受照明不良影响最大。
对于第二张图片,亮度非常低,图像噪点过多。我需要将行 bin = dip.Label(bin)
修改为:
bin = dip.Label(bin, 2, 500) # Imposing minimum object size rather than filtering
相反,进行一些噪音过滤可能更容易。输出是:
| 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]
方法#3 的快速解释
方法在the PhD thesis of Lucas van Vliet (Delft University of Technology, 1993), chapter 6中有描述。
这样想:通过孔的光量与孔的面积成正比(实际上它由 'area' x 'light intensity' 给出)。通过将所有穿过孔的光相加,我们可以知道孔的面积。该代码将对象的所有像素强度以及对象外部的一些像素相加(我在那里使用 10 个像素,要走多远取决于模糊)。
erfclip
函数称为“软剪辑”函数,它保证孔内的强度统一为1,孔外的强度统一为0,并且只在它留下的边缘周围中间灰度值。在这种特殊情况下,这个软剪辑避免了成像系统中的一些偏移问题,以及光强度估计不佳的问题。在其他情况下,更重要的是避免被测物体颜色不均匀的问题。它还可以减少噪音的影响。
设置:
- 相机: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
通过以上参数的摆弄,我们几乎可以得到我们想要的结果。当我们对不同的图片使用相同的参数时,问题就出现了。
我们想要得到的最终结果是非常精确的给定圆的直径,我们希望相同的算法可用于不同的部分图片
这个问题与发布的其他问题的不同之处在于我们不知道给定圆的大致半径(因此我们无法操纵 minradius、maxradius、param1、param2 或任何其他值)。
您可以对图像进行阈值处理并使用 findContours
找到孔的轮廓,然后使用 minEnclosingCircle
将圆拟合到它们上。可以通过将拟合的圆与等高线的面积进行比较来检查拟合圆的完整性。
import cv2 as cv
import math
import numpy as np
from matplotlib import pyplot as pl
gray = cv.imread('geriausias.bmp', cv.IMREAD_GRAYSCALE)
_,mask = cv.threshold(gray, 127, 255, cv.THRESH_BINARY)
contours,_ = cv.findContours(mask, cv.RETR_LIST, cv.CHAIN_APPROX_NONE)
contours = [contour for contour in contours if len(contour) > 15]
circles = [cv.minEnclosingCircle(contour) for contour in contours]
areas = [cv.contourArea(contour) for contour in contours]
radiuses = [math.sqrt(area / math.pi) for area in areas]
# Render contours blue and circles green.
canvas = cv.cvtColor(mask, cv.COLOR_GRAY2BGR)
cv.drawContours(canvas, contours, -1, (255, 0, 0), 10)
for circle, radius_from_area in zip(circles, radiuses):
if 0.9 <= circle[1] / radius_from_area <= 1.1: # Only allow 10% error in radius.
p = (round(circle[0][0]), round(circle[0][1]))
r = round(circle[1])
cv.circle(canvas, p, r, (0, 255, 0), 10)
cv.imwrite('geriausias_circles.png', canvas)
canvas_small = cv.resize(canvas, None, None, 0.25, 0.25, cv.INTER_AREA)
cv.imwrite('geriausias_circles_small.png', canvas_small)
通过健全性检查的圆在所有以蓝色显示的等高线之上以绿色显示。
这是一个方法
- 将图像转换为灰度和高斯模糊
- 自适应阈值
- 对smooth/filter图像进行形态学变换
- 寻找轮廓
- 求等高线的周长并进行等高线近似
- 获取边界矩形和质心得到直径
找到轮廓后,我们进行轮廓近似。这个想法是,如果近似轮廓有三个个顶点,那么它一定是一个三角形。同样,如果它有四个,它一定是正方形或长方形。因此我们可以假设,如果它的顶点数大于一定数量,那么它就是一个圆。
有几种获取直径的方法,一种方法是找到轮廓的边界矩形并使用其宽度。另一种方法是从质心坐标计算它。
import cv2
image = cv2.imread('1.bmp')
# Gray, blur, adaptive threshold
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
blur = cv2.GaussianBlur(gray, (3,3), 0)
thresh = cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)[1]
# Morphological transformations
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5,5))
opening = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel)
# Find contours
cnts = cv2.findContours(opening, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
cnts = cnts[0] if len(cnts) == 2 else cnts[1]
for c in cnts:
# Find perimeter of contour
perimeter = cv2.arcLength(c, True)
# Perform contour approximation
approx = cv2.approxPolyDP(c, 0.04 * perimeter, True)
# We assume that if the contour has more than a certain
# number of verticies, we can make the assumption
# that the contour shape is a circle
if len(approx) > 6:
# Obtain bounding rectangle to get measurements
x,y,w,h = cv2.boundingRect(c)
# Find measurements
diameter = w
radius = w/2
# Find centroid
M = cv2.moments(c)
cX = int(M["m10"] / M["m00"])
cY = int(M["m01"] / M["m00"])
# Draw the contour and center of the shape on the image
cv2.rectangle(image,(x,y),(x+w,y+h),(0,255,0),4)
cv2.drawContours(image,[c], 0, (36,255,12), 4)
cv2.circle(image, (cX, cY), 15, (320, 159, 22), -1)
# Draw line and diameter information
cv2.line(image, (x, y + int(h/2)), (x + w, y + int(h/2)), (156, 188, 24), 3)
cv2.putText(image, "Diameter: {}".format(diameter), (cX - 50, cY - 50), cv2.FONT_HERSHEY_SIMPLEX, 3, (156, 188, 24), 3)
cv2.imwrite('image.png', image)
cv2.imwrite('thresh.png', thresh)
cv2.imwrite('opening.png', opening)
关于这些图像我们知道两件事:
- 对象是黑暗的,在明亮的背景上。
- 孔都是圆的,我们要测量所有的孔。
所以我们需要做的就是检测漏洞。这实际上很简单:
- 阈值(背景成为对象,因为它很亮)
- 删除边缘对象
剩下的就是坑了。不包括任何接触图像边缘的孔。我们现在可以很容易地测量这些孔。由于我们假设它们是圆形的,我们可以做三件事:
- 计算对象像素,这是对面积的无偏估计。从面积我们确定孔径。
- 检测轮廓,找到质心,然后使用例如轮廓点到质心的平均距离为半径。
- 归一化图像强度,使背景照明的强度为 1,其中有孔的物体强度为 0。每个孔的强度积分是区域的亚像素精度估计(有关此方法的快速说明,请参阅底部)。
此 Python 代码使用 DIPlib(免责声明:我是作者)展示了如何执行这三种方法:
import diplib as dip
import numpy as np
img = dip.ImageRead('geriausias.bmp')
img.SetPixelSize(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)
这给出了输出:
| 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]
图像bin
在调用dip.Label
后是一个整数图像,其中孔1的像素都具有值1,孔2的像素具有值2,等等。所以我们仍然保留测量尺寸与它们是哪些孔之间的关系。我没有费心制作一个显示图像尺寸的标记图像,但这可以很容易地完成,正如您在其他答案中看到的那样。
因为图像文件中没有像素大小信息,我强加了每像素 1 微米。这可能不正确,您必须进行 校准以获得像素大小信息 。
这里的一个问题是背景照明太亮,导致像素饱和。这会导致孔看起来比实际大。 校准系统很重要,这样背景照明接近相机可以记录的最大值,但不是那个最大值也不高于。例如,尝试将背景强度设置为245或250。第三种方法受照明不良影响最大。
对于第二张图片,亮度非常低,图像噪点过多。我需要将行 bin = dip.Label(bin)
修改为:
bin = dip.Label(bin, 2, 500) # Imposing minimum object size rather than filtering
相反,进行一些噪音过滤可能更容易。输出是:
| 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]
方法#3 的快速解释
方法在the PhD thesis of Lucas van Vliet (Delft University of Technology, 1993), chapter 6中有描述。
这样想:通过孔的光量与孔的面积成正比(实际上它由 'area' x 'light intensity' 给出)。通过将所有穿过孔的光相加,我们可以知道孔的面积。该代码将对象的所有像素强度以及对象外部的一些像素相加(我在那里使用 10 个像素,要走多远取决于模糊)。
erfclip
函数称为“软剪辑”函数,它保证孔内的强度统一为1,孔外的强度统一为0,并且只在它留下的边缘周围中间灰度值。在这种特殊情况下,这个软剪辑避免了成像系统中的一些偏移问题,以及光强度估计不佳的问题。在其他情况下,更重要的是避免被测物体颜色不均匀的问题。它还可以减少噪音的影响。