【OpenCV】保温杯尺寸测量

前言

使用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: 每个像素代表的实际长度(单位:毫米),用于将像素数转换成实际尺寸。

主要逻辑(一)

  1. 获取并筛选轮廓

    • 使用get_contours(frame)函数获取图像中所有可能的轮廓。
    • 筛选出高度在h_minh_max之间的轮廓,并按高度排序,选择最高的轮廓作为保温杯的轮廓。
  2. 旋转校正

    • 计算最小外接矩形(cv2.minAreaRect)以找到保温杯的倾斜角度。
    • 如果保温杯倾斜角度不在0.5度到5度之间,则通过旋转矩阵(cv2.getRotationMatrix2D)对图像进行旋转变换,使保温杯垂直于图像平面,并递归调用自身来重新测量。
  3. 确定关键点

    • 一旦保温杯被正确地定位和校正,代码接下来会找出保温杯顶部和底部的关键点。
    • 通过排序和过滤,分别找到最左边的点、最顶部的点和最底部的点。
  4. 计算高度

    • 根据顶部和底部关键点的Y坐标差计算保温杯的高度(像素单位),然后乘以pixel_accuracy得到实际高度(毫米)。
  5. 日志记录与绘图

    • 使用log_message打印保温杯高度的日志信息。
    • 调用draw_image_cup_height函数在图像上绘制保温杯的高度线和标注。
  6. 返回结果

    • 返回处理后的图像和保温杯高度(字符串格式,带单位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,只检索最外层的轮廓。

主要逻辑(二)

  1. 灰度转换

    • gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY):将彩色图像(BGR格式)转换为灰度图像(单通道)。这一步简化了后续的图像处理过程,因为灰度图像是单通道的,减少了数据量和计算复杂度。
  2. 高斯模糊

    • blur = cv2.GaussianBlur(gray, (5, 5), sigmaX=1):应用一个5x5的高斯核对灰度图像进行模糊处理。目的是减少噪声,使边缘更加平滑,从而提高后续边缘检测的准确性。
  3. 二值化处理

    • _, threshold = cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU):使用大津法(Otsu's method)自动确定阈值,将图像转换为黑白二值图像。这里cv2.THRESH_BINARY表示采用二值化的阈值处理方式,而cv2.THRESH_OTSU会自动计算最佳阈值。输出的threshold是二值化后的图像。
  4. 边缘检测

    • canny = cv2.Canny(threshold, 0, 255, apertureSize=3, L2gradient=False):使用Canny算法检测图像中的边缘。Canny算子通过寻找图像强度梯度的最大值来定位边缘。这里的参数设置为低阈值0和高阈值255apertureSize=3指定了Sobel算子的窗口大小,L2gradient=False则选择了更简单的梯度幅度计算方法(L1范数)。
  5. 查找轮廓

    • contours, _ = cv2.findContours(canny, mode, cv2.CHAIN_APPROX_SIMPLE):在经过边缘检测的图像上查找轮廓。cv2.findContours函数返回两个值,第一个是找到的所有轮廓,第二个是每个轮廓的层次结构信息(在这个例子中未使用)。mode参数指定如何检索轮廓,cv2.CHAIN_APPROX_SIMPLE压缩水平、垂直和对角方向的元素,只保留端点,从而简化了轮廓。
  6. 返回结果

    • 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: 定义了内螺纹区域亮度范围的下限和上限,默认分别为80190
  • pixel_accuracy: 每个像素代表的实际长度(单位:毫米),用于将像素数转换成实际尺寸。
  • debug: 布尔值,指示是否保存调试图像,默认为False

主要逻辑

杯口处理

  1. 复制图像

    • 为了不影响原始图像,创建了两个副本mouth_framethread_frame分别用于杯口和内螺纹的处理。
  2. 查找轮廓

    • 使用get_contours函数获取图像中所有可能的轮廓,并筛选出最接近圆形的轮廓作为杯口的轮廓。这通过is_circle(contour)来判断。
  3. 绘制轮廓与矩形

    • 如果找到了合适的杯口轮廓,使用cv2.drawContours在原始图像上绘制该轮廓,并用蓝色标识。
    • 计算最小外接矩形(cv2.minAreaRect)以获得杯口的几何信息,并再次绘制矩形框。
  4. 计算直径

    • 根据最小外接矩形的信息,计算杯口的最大和最小直径,并将其转换为实际尺寸(毫米)。
    • 日志记录和可视化这些测量结果。
  5. 结果输出

    • 将最大和最小直径的结果格式化为字符串并存储到变量res_max1res_min1中。

内螺纹处理

  1. 预处理

    • 如果thread_flag为真,则继续处理内螺纹部分。
    • 对图像进行灰度化、高斯模糊、对比度调整等预处理操作,以增强特征的可见性。
  2. 创建掩码

    • 使用cv2.inRange创建一个掩码,仅保留亮度在指定范围内的像素。
    • 应用形态学闭运算(cv2.morphologyEx)填充小孔洞,使内螺纹轮廓更加完整。
  3. 边缘检测与轮廓查找

    • 使用Canny算法进行边缘检测。
    • 查找轮廓,并根据层次结构选择内部轮廓作为内螺纹的候选。
  4. 椭圆拟合

    • 使用cv2.fitEllipse对选定的内螺纹轮廓进行椭圆拟合。
    • 绘制椭圆并在日志中记录其参数。
  5. 计算直径

    • 类似于杯口处理,计算内螺纹的最大和最小直径,并转换为实际尺寸。
    • 日志记录和可视化这些测量结果。
  6. 结果输出

    • 将最大和最小直径的结果格式化为字符串并存储到变量res_max2res_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博客

相关推荐
高山莫衣4 分钟前
【返璞归真】-Lasso 回归(Least Absolute Shrinkage and Selection Operator,最小绝对值收缩和选择算子)
人工智能·数据挖掘·回归
Yeats_Liao10 分钟前
华为开源自研AI框架昇思MindSpore应用案例:基于MindSpore框架的SGD优化器案例实现
人工智能
AI浩32 分钟前
激活函数在神经网络中的作用,以及Tramformer中的激活函数
人工智能·深度学习·神经网络
杨善锦37 分钟前
mobile one神经网络
人工智能·深度学习·神经网络
Thanks_ks1 小时前
深入探索现代 IT 技术:从云计算到人工智能的全面解析
大数据·人工智能·物联网·云计算·区块链·数字化转型·it 技术
东方佑2 小时前
给图像去除水印攻
人工智能·python
知来者逆2 小时前
Layer-Condensed KV——利用跨层注意(CLA)减少 KV 缓存中的内存保持 Transformer 1B 和 3B 参数模型的准确性
人工智能·深度学习·机器学习·transformer
tangjunjun-owen2 小时前
异常安全重启运行机制:健壮的Ai模型训练自动化
人工智能·python·安全·异常重运行或重启
爱研究的小牛2 小时前
Rerender A Video 技术浅析(二):视频增强
人工智能·深度学习·aigc
Bdawn2 小时前
【通义实验室】开源【文本生成图片】大模型
人工智能·python·llm