在计算机视觉领域,图像测量是一项常见而重要的任务。本文将详细介绍两种基于OpenCV的图像测量方法,并对它们的实现原理、优缺点及适用场景进行深入分析。

方法一:基于参考物体宽度的测量方法

核心原理:第一种方法通过设定已知宽度的参考物体(通常放置在图像最左侧),计算像素与实际尺寸的比例关系,从而测量图像中其他物体的尺寸。

代码实现分析:

# 导入必要的库
from scipy.spatial import distance as dist
from imutils import perspective
from imutils import contours
import numpy as np
import argparse
import imutils
import cv2

# 定义中点函数
def midpoint(ptA, ptB):
    return ((ptA[0] + ptB[0]) * 0.5, (ptA[1] + ptB[1]) * 0.5)

# 设置内部参数
args = {
    "image": r"D:\data\05.bmp",
    "width": 25.0  # 参考物体的实际宽度
}

以上代码段首先导入了必要的库,并定义了中点计算函数和参数设置。

# 图像预处理
img = cv2.imread(args["image"])
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
_, thresh = cv2.threshold(gray, 120, 255, cv2.THRESH_BINARY_INV)

# 膨胀操作增强轮廓
kernel = np.ones((3, 3), np.uint8)
thresh = cv2.dilate(thresh, kernel, iterations=1)

# 填充图像四角以消除边缘干扰
height, width = thresh.shape
thresh[0:120, 0:160] = 0  # 左上角
thresh[0:150, width - 180:] = 0  # 右上角
thresh[height - 120:, 0:160] = 0  # 左下角
thresh[height - 150:, width - 160:] = 0  # 右下角

预处理阶段包括读取图像、灰度转换、二值化和膨胀操作,最后填充图像四角以消除边缘干扰。

# 寻找物体轮廓并排序
cnts = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = imutils.grab_contours(cnts)
(cnts, _) = contours.sort_contours(cnts)

# 初始化像素与实际尺寸的比例
pixelsPerMetric = None

# 遍历每个轮廓进行测量
for c in cnts:
    if cv2.contourArea(c) < 10000:  # 忽略面积过小的轮廓
        continue
   
    # 计算最小外接矩形
    box = cv2.minAreaRect(c)
    box = cv2.boxPoints(box)
    box = np.array(box, dtype="int")
    box = perspective.order_points(box)

    # 计算各边中点并绘制
    (tl, tr, br, bl) = box
    (tltrX, tltrY) = midpoint(tl, tr)
    (blbrX, blbrY) = midpoint(bl, br)
    (tlblX, tlblY) = midpoint(tl, bl)
    (trbrX, trbrY) = midpoint(tr, br)
   
    # 计算像素距离
    dA = dist.euclidean((tltrX, tltrY), (blbrX, blbrY))
    dB = dist.euclidean((tlblX, tlblY), (trbrX, trbrY))

    # 设置比例(使用最左侧物体的已知宽度)
    if pixelsPerMetric is None:
        pixelsPerMetric = dB / args["width"]

    # 计算实际尺寸
    dimA = dA / pixelsPerMetric * 0.98  # 0.98为修正系数
    dimB = dB / pixelsPerMetric * 0.98
   
    # 绘制测量结果
    cv2.putText(orig, "{:.3f}mm".format(dimA), (int(tltrX - 15), int(tltrY - 10)),
                cv2.FONT_HERSHEY_SIMPLEX, 0.65, (255, 255, 255), 1)

    cv2.putText(orig, "{:.3f}mm".format(dimB), (int(trbrX + 10), int(trbrY)),
                cv2.FONT_HERSHEY_SIMPLEX, 0.65, (255, 255, 255), 1)

测量阶段首先寻找并排序轮廓,然后为每个轮廓计算最小外接矩形,通过参考物体的宽度建立像素与实际尺寸的转换关系,最后计算并显示实际尺寸。

方法二:基于硬币作为参考的测量方法

核心原理:第二种方法使用硬币作为参考物体(已知实际直径),通过检测图像中最大的圆形轮廓来识别硬币,然后计算像素与实际尺寸的比例关系。

代码实现分析:

import cv2
import numpy as np
import matplotlib.pyplot as plt

def find_and_measure_objects(image_path, coin_real_diameter_mm=25.0):
    # 读取图像
    img = cv2.imread(image_path)
    if img is None:
        raise ValueError("图像加载失败,请检查路径")

    # 转换为灰度图并二值化
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    _, thresh = cv2.threshold(gray, 120, 255, cv2.THRESH_BINARY_INV)
   
    # 填充四角消除干扰
    height, width = thresh.shape
    thresh[0:120, 0:160] = 0  # 左上角
    thresh[0:140, width - 180:] = 0  # 右上角
    thresh[height - 120:, 0:160] = 0  # 左下角
    thresh[height - 150:, width - 160:] = 0  # 右下角

该方法封装在一个函数中,首先进行图像读取、灰度转换、二值化和边缘处理。

 # 查找轮廓并找到最大的轮廓(假设是硬币)
    contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    max_contour = max(contours, key=cv2.contourArea)

    # 获取硬币的最小外接圆
    (x, y), radius = cv2.minEnclosingCircle(max_contour)
    center = (int(x), int(y))
    radius = int(radius)
    diameter_px = 2 * radius
   
    # 计算像素到毫米的转换比例
    px_to_mm = coin_real_diameter_mm / coin_diameter_px

这部分代码查找轮廓并找到最大的轮廓(假设为硬币),然后计算硬币的最小外接圆,基于已知的硬币实际直径建立像素与毫米的转换关系。

 # 遍历所有轮廓并测量
    measurements = []

    for i, contour in enumerate(contours):
        area = cv2.contourArea(contour)
        if area < 20000 or contour is max_contour:  # 忽略小物体和硬币本身
            continue

        # 获取最小外接矩形
        rect = cv2.minAreaRect(contour)
        box = cv2.boxPoints(rect)
        box = np.int0(box)
       
        # 计算尺寸
        edge1 = np.linalg.norm(box[0] - box[1])
        edge2 = np.linalg.norm(box[1] - box[2])
        width_px, height_px = max(edge1, edge2), min(edge1, edge2)
       
        # 转换为真实尺寸
        width_mm = width_px * px_to_mm
        height_mm = height_px * px_to_mm
       
        # 存储测量结果
        measurements.append({
            'id': i + 1,
            'width_px': width_px,
            'height_px': height_px,
            'width_mm': width_mm,
            'height_mm': height_mm
        })

测量阶段遍历所有轮廓,计算每个物体的最小外接矩形,并根据像素与毫米的转换关系计算实际尺寸。

 # 使用matplotlib显示结果
    plt.figure(figsize=(15, 5))
    plt.subplot(131), plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
    plt.title('Original Image'), plt.axis('off')
    plt.subplot(132), plt.imshow(thresh, cmap='gray')
    plt.title('Threshold Image'), plt.axis('off')
    plt.subplot(133), plt.imshow(cv2.cvtColor(result_img, cv2.COLOR_BGR2RGB))
    plt.title('Measurement Results'), plt.axis('off')
    plt.tight_layout()
    plt.show()

    # 打印测量结果表格
    print("\n物体测量结果:")
    print(f"{'ID':<5}{'宽度(mm)':<10}{'高度(mm)':<10}{'宽度(px)':<10}{'高度(px)':<10}")
    print("-" * 45)
    for m in measurements:
        print(f"{m['id']:<5}{m['width_mm']:<10.3f}{m['height_mm']:<10.13}{m['width_px']:<10.3f}{m['height_px']:<10.3f}")
    return result_img, measurements

最后,该方法使用matplotlib显示原始图像、二值化图像和测量结果,并以表格形式打印测量数据。

两种方法的对比分析

1、相同点

1. 都使用OpenCV进行图像处理和轮廓检测

2. 都需要参考物体来建立像素与实际尺寸的转换关系
3. 都采用二值化方法突出物体轮廓
4. 都使用最小外接矩形来表示物体的尺寸
5. 都填充图像四角以消除边缘干扰

2、不同点

对比项方法一方法二
参考物体已知宽度的物体(通常放在最左侧)硬币(已知直径)
代码结构线性执行结构函数封装结构
轮廓排序从左到右排序无需排序,通过面积识别参考物体
显示方式使用 OpenCV 的 imshow使用 matplotlib 显示多图对比
结果输出仅在图像上显示图像显示 + 表格数据输出
适用场景有规则排列的物体测量包含圆形参考物的任意场景

优缺点分析

方法一优点:

  • 实现简单直观
  • 对于规则排列的物体测量效率高
  • 无需复杂的轮廓识别算法

方法一缺点:

  • 依赖参考物体的位置(必须在最左侧)
  • 代码结构较为松散
  • 仅支持在图像上显示结果

方法二优点:

  • 封装性更好,易于复用
  • 参考物体(硬币)放置位置更灵活
  • 结果可视化更丰富(多图对比 + 表格数据)
  • 代码结构更清晰

方法二缺点:

  • 依赖于硬币在图像中是最大的圆形物体
  • 当图像中有多个类似大小的圆形物体时可能误判
  • 实现相对复杂

适用场景推荐

方法一适用场景:

  • 生产线检测中物体规则排列的场景
  • 需要快速实现简单测量的场景
  • 对代码封装要求不高的场景

方法二适用场景:

  • 通用物体测量场景
  • 需要美观结果展示的场景
  • 多次复用测量功能的场景
  • 有圆形参考物体(如硬币)的场景

代码优化建议

方法一优化建议

1. 封装为函数:

def measure_objects_by_reference(image_path, ref_width_mm, min_area=10000):
    # 将原代码封装为函数
    # 返回测量结果和处理后的图像

2. 增加异常处理:

try:
    img = cv2.imread(image_path)
    if img is None:
        raise FileNotFoundError(f"无法读取图像:{image_path}")
except Exception as e:
    print(f"图像读取错误:{e}")
    return None, None

3. 增加参数可调性:

def measure_objects_by_reference(image_path, ref_width_mm,
                                threshold_value=120,
                                min_area=10000,
                                correction_factor=0.98):
    # 实现代码
方法二优化建议

1. 增加硬币检测的鲁棒性:

def find_coin_contour(contours):
    # 不仅仅依靠面积,还可以结合圆形度等特征
    coin_contour = None
    max_circularity = -1

    for contour in contours:
        if cv2.contourArea(contour) < 10000:  # 忽略小轮廓
            continue
           
        # 计算圆形度 (4π*面积/周长²)
        perimeter = cv2.arcLength(contour, True)
        if perimeter == 0:
            continue
        circularity = 4 * np.pi * cv2.contourArea(contour) / (perimeter ** 2)

        # 选择圆形度最高且面积较大的轮廓作为硬币
        if circularity > max_circularity and circularity > 0.8:  # 圆形度阈值
            max_circularity = circularity
            coin_contour = contour

    return coin_contour

2. 增加图像预处理的灵活性:

def preprocess_image(img, threshold_value=120,
                    fill_corners=True,
                    corner_fill_sizes=None):
    # 实现灵活的图像预处理

3. 支持多种显示模式:

def display_results(img, thresh, result_img, display_mode='matplotlib'):
    if display_mode == 'opencv':
        cv2.imshow("Original", img)
        cv2.imshow("Threshold", thresh)
        cv2.imshow("Results", result_img)
        cv2.waitKey(0)
    else:
        # matplotlib显示代码

总结

本文详细介绍了两种基于OpenCV的图像测量方法,它们各有优缺点和适用场景。

方法一实现简单,适合规则排列物体的测量;

方法二封装性更好,结果展示更丰富,适合更通用的测量场景。

在实际应用中,可以根据具体需求选择合适的方法,或者结合两种方法的优点进行改进。例如,可以使用方法二的封装结构,同时允许用户选择不同类型的参考物体,从而提高测量的灵活性和准确性。

图像测量技术在工业检测、物体识别、机器人视觉等领域有着广泛的应用前景,通过不断优化算法和提高测量精度,可以进一步拓展其应用范围。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐