前言
使用OpenCV进行保温杯尺寸测量可以通过图像处理和计算机视觉技术来实现。
1. 准备工作
- 获取图像:首先,需要一张清晰的保温杯图片。最好是从正上方或侧面拍摄的,以减少透视变形。
- 校准相机(可选):如果需要高精度测量,可以考虑进行相机校准以消除镜头畸变。
2. 图像预处理
- 灰度化:将彩色图像转换为灰度图,简化后续处理。
- 去噪:使用如高斯模糊等方法去除噪声,提高边缘检测的准确性。
- 边缘检测:使用Canny、Sobel或其他边缘检测算子找到图像中的边缘等。
3. 特征提取
- 轮廓检测 :使用
findContours
函数找到图像中所有封闭的轮廓,并筛选出最有可能是保温杯的轮廓。 - 形状匹配(可选):如果你有保温杯的大致形状模型,可以使用形状匹配算法进一步确认哪个轮廓属于保温杯。
4. 尺寸测量
- 确定参考尺度:在图像中选择一个已知尺寸的对象作为参考,例如直尺上的刻度线,通过它来标定像素与实际长度的比例关系。
- 计算尺寸:根据上述比例关系以及保温杯轮廓的边界点,计算保温杯的实际尺寸,比如高度、直径等。
5. 结果输出
- 可视化结果:可以在原图上画出测量的结果,包括线条、文本标签等。
- 保存或展示结果:将最终图像保存下来或者显示给用户。
注意事项
- 确保照明条件良好且一致,避免阴影影响边缘检测效果。
- 如果保温杯表面反光严重,可能需要调整拍摄角度或增加漫射光源。
- 对于非规则形状的保温杯,可能需要更复杂的算法来准确捕捉其外形特征。
项目需求
- 杯身高度测量:产品高度160mm~280mm范围,偏差要求不超过0.3mm
- 杯口直径测量:产品杯口60~120mm范围,偏差要求不超过0.1mm
- 杯口内螺纹直径测量:与杯口直径一样
- 杯口直径需要测量最大、最小值,有的不是标准圆
相机、镜头、光源
由于需要同时测量杯身高度和杯口,初步设计产品竖直方向放置,正上方和正前方两个相机。为了将畸变消除到最小,并且要能清晰看到内螺纹,正上方需要选择远心镜头,正前方使用线扫相机即可。
成像效果
测量效果
功能实现
本节介绍具体功能实现,基于OpenCV进行图像处理,进行测量后,保存测量结果,然后通过基于Pyside6开发的上位机显示测量结果。
杯身高度测量
python
def calculateMeasureRange(self):
h_range = self.cupHeightRange.split(',')
self.cupHMin = int(int(h_range[0]) / self.pixelAccuracy1) - 50
self.cupHMax = int(int(h_range[1]) / self.pixelAccuracy1) + 50
log_message(f'杯身高度有效测量范围:{self.cupHMin}px - {self.cupHMax}px')
self.cupHeightRange是项目需求中规定的范围:120,280
self.pixelAccuracy1是单位像素精度:由相机短边像素大小和相机实际视野范围计算出,例如:短边像素值为6000(我们使用的是4800万像素相机),一定高度下的实际视野大小为288mm,那么单位像素大小为:0.48...
python
frame1 = cv2.imread('images/cup_h1.bmp')
image1 = frame1
image1, h = measure_cup_body(frame1, self.cupHMin, self.cupHMax, self.pixelAccuracy1)
self.cupHMin和self.cupHMax分别为有效范围最小值、最大值
python
def measure_cup_body(frame, h_min, h_max, pixel_accuracy=0.0481316):
contours = get_contours(frame)
contours = sorted([c for c in contours if h_min <= cv2.boundingRect(c)[3] <= h_max],
key=lambda ct: cv2.boundingRect(ct)[3])
cnt = contours[-1]
rect = cv2.minAreaRect(cnt)
if 5 >= rect[2] >= 0.5:
M = cv2.getRotationMatrix2D(rect[0], rect[2], 1)
frame = cv2.warpAffine(frame, M, (frame.shape[1], frame.shape[0]))
return measure_cup_body(frame, h_min, h_max, pixel_accuracy)
cnt_x_l = sorted(cnt, key=lambda c: c[0][0])
cnt_pt_left = cnt_x_l[0][-1]
cnt_x_t = [c for c in cnt_x_l if c[0][0] - cnt_pt_left[0] <= 15 and c[0][1] < cnt_pt_left[1]]
cnt_x_t = sorted(cnt_x_t, key=lambda c: c[0][1])
cnt_pt_top = cnt_x_t[0][-1]
cnt_y_b = sorted(cnt, key=lambda c: c[0][1], reverse=True)
cnt_pt_b = cnt_y_b[0][-1]
cnt_y_bottom = [c for c in cnt_y_b if abs(c[0][1] - cnt_pt_b[1]) <= 15]
cnt_y_bottom = sorted(cnt_y_bottom, key=lambda c: c[0][0])
cnt_pt_bottom = cnt_y_bottom[1][-1]
rh = cnt_pt_bottom[1] - cnt_pt_top[1]
h_value = rh * pixel_accuracy
log_message('杯身高度:%dpx %.3fmm' % (rh, h_value))
draw_image_cup_height(frame, cnt_pt_top, cnt_pt_bottom, '%.3f mm' % h_value)
return frame, '%.3f mm' % h_value
函数定义(一)
measure_cup_body(frame, h_min, h_max, pixel_accuracy=0.0481316)
:
frame
: 输入图像,即要分析的帧。h_min
,h_max
: 定义了轮廓高度的最小和最大阈值,用于筛选出合适的轮廓。pixel_accuracy
: 每个像素代表的实际长度(单位:毫米),用于将像素数转换成实际尺寸。
主要逻辑(一)
-
获取并筛选轮廓:
- 使用
get_contours(frame)
函数获取图像中所有可能的轮廓。 - 筛选出高度在
h_min
到h_max
之间的轮廓,并按高度排序,选择最高的轮廓作为保温杯的轮廓。
- 使用
-
旋转校正:
- 计算最小外接矩形(
cv2.minAreaRect
)以找到保温杯的倾斜角度。 - 如果保温杯倾斜角度不在0.5度到5度之间,则通过旋转矩阵(
cv2.getRotationMatrix2D
)对图像进行旋转变换,使保温杯垂直于图像平面,并递归调用自身来重新测量。
- 计算最小外接矩形(
-
确定关键点:
- 一旦保温杯被正确地定位和校正,代码接下来会找出保温杯顶部和底部的关键点。
- 通过排序和过滤,分别找到最左边的点、最顶部的点和最底部的点。
-
计算高度:
- 根据顶部和底部关键点的Y坐标差计算保温杯的高度(像素单位),然后乘以
pixel_accuracy
得到实际高度(毫米)。
- 根据顶部和底部关键点的Y坐标差计算保温杯的高度(像素单位),然后乘以
-
日志记录与绘图:
- 使用
log_message
打印保温杯高度的日志信息。 - 调用
draw_image_cup_height
函数在图像上绘制保温杯的高度线和标注。
- 使用
-
返回结果:
- 返回处理后的图像和保温杯高度(字符串格式,带单位mm)。
python
def get_contours(frame, mode=cv2.RETR_EXTERNAL):
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
blur = cv2.GaussianBlur(gray, (5, 5), sigmaX=1)
_, threshold = cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
canny = cv2.Canny(threshold, 0, 255, apertureSize=3, L2gradient=False)
contours, _ = cv2.findContours(canny, mode, cv2.CHAIN_APPROX_SIMPLE)
return contours
函数定义(二)
def get_contours(frame, mode=cv2.RETR_EXTERNAL):
frame
: 输入图像,即要分析的帧。mode
: 指定检索轮廓的方式,默认为cv2.RETR_EXTERNAL
,只检索最外层的轮廓。
主要逻辑(二)
-
灰度转换:
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
:将彩色图像(BGR格式)转换为灰度图像(单通道)。这一步简化了后续的图像处理过程,因为灰度图像是单通道的,减少了数据量和计算复杂度。
-
高斯模糊:
blur = cv2.GaussianBlur(gray, (5, 5), sigmaX=1)
:应用一个5x5的高斯核对灰度图像进行模糊处理。目的是减少噪声,使边缘更加平滑,从而提高后续边缘检测的准确性。
-
二值化处理:
_, threshold = cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
:使用大津法(Otsu's method)自动确定阈值,将图像转换为黑白二值图像。这里cv2.THRESH_BINARY
表示采用二值化的阈值处理方式,而cv2.THRESH_OTSU
会自动计算最佳阈值。输出的threshold
是二值化后的图像。
-
边缘检测:
canny = cv2.Canny(threshold, 0, 255, apertureSize=3, L2gradient=False)
:使用Canny算法检测图像中的边缘。Canny算子通过寻找图像强度梯度的最大值来定位边缘。这里的参数设置为低阈值0
和高阈值255
,apertureSize=3
指定了Sobel算子的窗口大小,L2gradient=False
则选择了更简单的梯度幅度计算方法(L1范数)。
-
查找轮廓:
contours, _ = cv2.findContours(canny, mode, cv2.CHAIN_APPROX_SIMPLE)
:在经过边缘检测的图像上查找轮廓。cv2.findContours
函数返回两个值,第一个是找到的所有轮廓,第二个是每个轮廓的层次结构信息(在这个例子中未使用)。mode
参数指定如何检索轮廓,cv2.CHAIN_APPROX_SIMPLE
压缩水平、垂直和对角方向的元素,只保留端点,从而简化了轮廓。
-
返回结果:
return contours
:最终返回的是图像中所有找到的轮廓列表。
杯口直径测量
python
frame2 = cv2.imread('images/cup_t1.bmp')
lower_b = int(cf.getConf('sys', 'lowerb', '80'))
upper_b = int(cf.getConf('sys', 'upperb', '190'))
image2, d_max1, d_min1, d_max2, d_min2 = measure_cup_mouth(frame2, self.threadDiameterChecked, lower_b,upper_b, self.pixelAccuracy2)
lower_b、upper_b:项目需求杯口直径范围。
self.threadDiameterChecked:是否进行内螺纹测量。
杯口测量分为杯口最大、最小直径测量和内螺纹最大、最小直径测量。
python
def measure_cup_mouth(frame, thread_flag=True, lower_b=80, upper_b=190, pixel_accuracy=0.049, debug=False):
mouth_frame = frame.copy()
thread_frame = frame.copy()
# ==========================杯口处理============================
res_max1 = '-'
res_min1 = '-'
res_max2 = '-'
res_min2 = '-'
contours = get_contours(mouth_frame, mode=cv2.RETR_EXTERNAL)
cnt = None
for contour in contours:
if is_circle(contour):
cnt = contour
break
if cnt is not None:
cv2.drawContours(frame, [cnt], -1, (255, 0, 0), thickness)
rect = cv2.minAreaRect(cnt)
log_message('杯口测量:' + str(rect))
box = np.intp(cv2.boxPoints(rect))
cv2.drawContours(frame, [box], -1, (255, 0, 0), thickness)
max_diameter = rect[1][0]
min_diameter = rect[1][1]
if min_diameter > max_diameter:
max_diameter, min_diameter = min_diameter, max_diameter
di_max = max_diameter * pixel_accuracy
di_min = min_diameter * pixel_accuracy
log_message('杯口最大直径:%dpx %.3fmm' % (round(max_diameter), di_max))
log_message('杯口最小直径:%dpx %.3fmm' % (round(min_diameter), di_min))
(x_c, y_c) = (round(rect[0][0]), round(rect[0][1]))
res_max1 = '%.3f mm' % di_max
res_min1 = '%.3f mm' % di_min
draw_image_cup_mouth(frame, x_c, y_c, res_max1, res_min1)
# ==========================内螺纹处理============================
if thread_flag:
gray = cv2.cvtColor(thread_frame, cv2.COLOR_BGR2GRAY)
blur = cv2.GaussianBlur(gray, (5, 5), sigmaX=1)
adjusted = cv2.convertScaleAbs(blur, alpha=1.5, beta=0)
mask = cv2.inRange(adjusted, np.array(lower_b), np.array(upper_b))
mask = cv2.bitwise_not(mask)
close = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel, iterations=4)
canny = cv2.Canny(close, 0, 255, apertureSize=3, L2gradient=False)
if debug:
cv2.imwrite('../images/mask.png', mask)
cv2.imwrite('../images/close.png', close)
cv2.imwrite('../images/canny.png', canny)
contours, hierarchy = cv2.findContours(canny, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
hierarchy = hierarchy.squeeze()
hchy = {}
for idx, hi in enumerate(hierarchy):
if hi[2] != -1 and hi[3] != -1:
hchy[idx] = hi[2]
idx = min(hchy, key=hchy.get)
cnt = contours[idx]
ellipse = cv2.fitEllipse(cnt)
log_message('内螺纹测量:' + str(ellipse))
cv2.ellipse(frame, ellipse, (0, 0, 255), thickness)
box = np.intp(cv2.boxPoints(ellipse))
cv2.drawContours(frame, [box], -1, (0, 0, 255), thickness)
max_diameter = ellipse[1][0]
min_diameter = ellipse[1][1]
if min_diameter > max_diameter:
max_diameter, min_diameter = min_diameter, max_diameter
dt_max = max_diameter * pixel_accuracy
dt_min = min_diameter * pixel_accuracy
log_message('内螺纹最大直径:%dpx %.3fmm' % (round(max_diameter), dt_max))
log_message('内螺纹最小直径:%dpx %.3fmm' % (round(min_diameter), dt_min))
(x_c, y_c) = (round(ellipse[0][0]), round(ellipse[0][1]))
res_max2 = '%.3f mm' % dt_max
res_min2 = '%.3f mm' % dt_min
draw_image_cup_thread(frame, x_c, y_c, res_max2, res_min2)
return frame, res_max1, res_min1, res_max2, res_min2
函数定义
def measure_cup_mouth(frame, thread_flag=True, lower_b=80, upper_b=190, pixel_accuracy=0.049, debug=False):
frame
: 输入图像,即要分析的帧。thread_flag
: 布尔值,指示是否进行内螺纹测量,默认为True
。lower_b
,upper_b
: 定义了内螺纹区域亮度范围的下限和上限,默认分别为80
和190
。pixel_accuracy
: 每个像素代表的实际长度(单位:毫米),用于将像素数转换成实际尺寸。debug
: 布尔值,指示是否保存调试图像,默认为False
。
主要逻辑
杯口处理
-
复制图像:
- 为了不影响原始图像,创建了两个副本
mouth_frame
和thread_frame
分别用于杯口和内螺纹的处理。
- 为了不影响原始图像,创建了两个副本
-
查找轮廓:
- 使用
get_contours
函数获取图像中所有可能的轮廓,并筛选出最接近圆形的轮廓作为杯口的轮廓。这通过is_circle(contour)
来判断。
- 使用
-
绘制轮廓与矩形:
- 如果找到了合适的杯口轮廓,使用
cv2.drawContours
在原始图像上绘制该轮廓,并用蓝色标识。 - 计算最小外接矩形(
cv2.minAreaRect
)以获得杯口的几何信息,并再次绘制矩形框。
- 如果找到了合适的杯口轮廓,使用
-
计算直径:
- 根据最小外接矩形的信息,计算杯口的最大和最小直径,并将其转换为实际尺寸(毫米)。
- 日志记录和可视化这些测量结果。
-
结果输出:
- 将最大和最小直径的结果格式化为字符串并存储到变量
res_max1
和res_min1
中。
- 将最大和最小直径的结果格式化为字符串并存储到变量
内螺纹处理
-
预处理:
- 如果
thread_flag
为真,则继续处理内螺纹部分。 - 对图像进行灰度化、高斯模糊、对比度调整等预处理操作,以增强特征的可见性。
- 如果
-
创建掩码:
- 使用
cv2.inRange
创建一个掩码,仅保留亮度在指定范围内的像素。 - 应用形态学闭运算(
cv2.morphologyEx
)填充小孔洞,使内螺纹轮廓更加完整。
- 使用
-
边缘检测与轮廓查找:
- 使用Canny算法进行边缘检测。
- 查找轮廓,并根据层次结构选择内部轮廓作为内螺纹的候选。
-
椭圆拟合:
- 使用
cv2.fitEllipse
对选定的内螺纹轮廓进行椭圆拟合。 - 绘制椭圆并在日志中记录其参数。
- 使用
-
计算直径:
- 类似于杯口处理,计算内螺纹的最大和最小直径,并转换为实际尺寸。
- 日志记录和可视化这些测量结果。
-
结果输出:
- 将最大和最小直径的结果格式化为字符串并存储到变量
res_max2
和res_min2
中。
- 将最大和最小直径的结果格式化为字符串并存储到变量
返回结果
- 最后,函数返回处理后的图像以及四个字符串形式的测量结果:杯口最大直径、杯口最小直径、内螺纹最大直径、内螺纹最小直径。
数据存储和显示
由于图像数据较大,所以单独创建一个线程来保存每次测量的图片和数据。
数据存储
python
threading.Thread(target=save_data,args=(image1, image2, h, d_max1, d_min1, d_max2, d_min2, res,)).start()
python
def save_data(image1, image2, h, d_max1, d_min1, d_max2, d_min2, res):
try:
filename = datetime.datetime.now().strftime('%Y%m%d%H%M%S')
data = {'filename1': filename + '-1.jpg', 'filename2': filename + '-2.jpg',
'value': [h, d_max1, d_min1, d_max2, d_min2], 'result': res}
today = datetime.datetime.now().strftime('%Y%m%d')
save_image(image1, today, filename + '-1', filename + '-1m')
save_image(image2, today, filename + '-2', filename + '-2m')
create_dir('data/' + today)
data_file = 'data/' + today + '/' + filename + '.data'
if not os.path.exists(data_file):
with open(data_file, 'w') as file:
file.write('')
with open(data_file, 'a') as file:
file.write(str(data) + '\n')
log_message('测量数据已保存!' + filename + '.data')
except Exception as e:
log_message('save_data() error: %s' % e.__cause__)
python
def save_image(image, today, filename, filename_m):
try:
if image is not None:
filename += '.jpg'
filename_m += '.jpg'
create_dir('images/' + today)
cv2.imwrite('images/' + today + '/' + filename, image)
cv2.imwrite('images/' + today + '/' + filename_m, cv2.resize(image, (128, 96)))
except Exception as e:
log_message('save_image() error: %s' % e.__cause__)
数据查看
可前往我的另一篇博文实现:Pyside6-QTableView实战-CSDN博客