单目视觉测量实战:YOLOv8分割与LeNet-5实现工业零件尺寸测距与功耗监测
2026/9/11 10:34:05 网站建设 项目流程

简介:本资源是一套面向嵌入式视觉开发者的单目视觉测量与功耗监测系统完整实现,适用于智能交通、工业检测及智能家居等场景下的非接触式目标距离估算、几何尺寸识别与设备功耗实时分析任务。系统融合传统图像处理(轮廓提取、形状识别)与轻量级深度学习模型(YOLOv8n用于目标分割定位,LeNet-5用于电表数字识别),并在STM32F1系列MCU上完成端侧部署,兼顾实时性与精度。压缩包共202个文件,含55个C/C++头文件(.h)、21个源码文件(.c)、20个HAL驱动模块(如stm32f1xx_hal_adc.c、uart.c等)、10个Python脚本(用于训练/评估)、2个ONNX模型文件及配套PDF说明、Makefile与工程配置文件,整体大小为18.75MB。目前已有58人学习下载,提供从图像采集、预处理、YOLOv8n分割推理、轮廓拟合测距到LeNet-5数字识别的全链路代码与硬件适配支持,具备可复现、可移植、可扩展的工程实践价值。

1. 项目缘起:一个“看得见”的工业质检需求

最近在帮一个做小型工业零部件生产的朋友解决一个头疼的问题。他们产线上有一批零件,比如螺丝、垫片、小轴承,需要快速检测尺寸是否合格,同时还要估算一下从传送带末端到分拣机械臂的距离,好让机械臂能准确抓取。听起来简单,但传统方案要么是上激光测距仪+高精度卡尺相机,成本直接起飞;要么是人工抽检,效率低还容易看走眼。朋友问我,能不能用最普通的USB摄像头(也就是单目视觉)搞定这两件事,顺便再把检测工位那台工控机的功耗给监控起来,看看算法跑起来到底费不费电。

这不就是典型的“既要、又要、还要”嘛——要低成本、要非接触、要自动化,还得带点能耗管理。单目视觉测量确实是条路子,它不依赖昂贵的立体相机或激光雷达,靠一个摄像头,结合图像处理和几何模型,就能反推物体的距离和尺寸。但这里面坑不少,比如摄像头怎么标定才准、光照变化咋办、物体遮挡了怎么处理、算法实时性够不够。正好,最近YOLOv8n分割模型和经典的LeNet-5在手头项目里都用过,就想着能不能搭个系统,把目标检测分割、尺寸距离计算、数字识别(用于读功耗仪表)这几个模块串起来,做个一体化的验证原型。

这个项目,我把它叫做“基于单目视觉的目标距离与尺寸测量及功耗监测系统”。名字有点长,但说白了,就是让一个普通摄像头学会“目测”物体的远近和大小,同时还能“瞟一眼”旁边的功率计读数,实现生产环节的视觉感知与能耗感知联动。下面,我就把从方案选型、核心算法实现、到实际调试踩坑的全过程,毫无保留地分享出来。

2. 核心原理拆解:单目视觉如何“看见”距离与尺寸

很多人一听单目测距就觉得是“黑科技”,其实它的基本原理离不开初中几何——相似三角形。关键在于,我们需要一些先验信息作为“已知量”。

2.1 单目测距的核心:从像素到真实世界的映射

单目摄像头本身无法直接获得深度信息。它拍下的是一张2D照片,我们丢失了“远近”这个维度。要想找回深度,必须引入额外的约束条件。在这个项目里,我们主要用两种方法:

方法一:基于已知物体尺寸的测距(Pinhole Camera Model)

这是最直观的方法。假设我们已知某个物体的真实宽度(或高度)为W_real(单位:米)。当这个物体放在距离摄像头Z米远的地方时,它在图像上会呈现一个宽度为W_pixel(单位:像素)的包围框。根据小孔成像模型和相似三角形原理,存在以下关系:

焦距 f (像素) = (W_pixel * Z) / W_real

这里有个关键步骤:相机标定。我们需要事先通过拍摄标定板(比如棋盘格),计算出摄像头的内参矩阵,其中就包含了以像素为单位的焦距f和主点坐标(cx, cy)。一旦通过标定知道了f,当我们在图像中检测到一个已知真实尺寸W_real的物体,并测出它的像素宽度W_pixel时,距离Z就可以反推出来:

Z = (W_real * f) / W_pixel

方法二:基于相机高度的测距(适用于地面物体)

如果摄像头固定安装,并且它的高度H已知,镜头光轴与地面有一定夹角(通常不是垂直向下)。当检测到物体底部接触地面的那个点(即接地点)时,我们可以通过这个点在图像中的像素坐标(u, v),结合相机内参和安装俯仰角,利用几何关系解算出物体到相机下方的水平距离。这种方法在自动驾驶中用于检测车辆、行人等地面目标非常常见。

在本项目中,由于测量对象是传送带上的零件,传送带平面可以近似视为一个已知高度的平面,因此我采用了第一种与第二种结合的方法。先通过标定确定相机参数,再通过已知的零件尺寸或传送带平面几何来求解距离。

2.2 几何尺寸识别:从轮廓到毫米

测出了距离,尺寸识别其实已经完成了一半。当知道了相机到物体的距离Z和焦距f,物体在图像中的像素尺寸S_pixel到真实尺寸S_real的转换就是线性的:

S_real = (S_pixel * Z) / f

问题的核心就变成了:如何从图像中高精度地提取出物体的像素尺寸S_pixel简单用目标检测的包围框(Bounding Box)行不行?对于近似矩形的物体可能勉强可以,但对于不规则零件,包围框会包含大量背景,误差很大。这就需要用到实例分割——得到物体精确的像素级轮廓。

我们用分割模型(如YOLOv8n-seg)得到物体的掩膜(Mask)。这个掩膜是一个二值图,物体区域为1,背景为0。对这个掩膜进行轮廓分析,可以拟合出最小外接矩形、计算像素面积、周长,甚至识别形状(圆形、六边形等)。例如,计算最小外接矩形的宽和高,作为W_pixelH_pixel,再利用上面的公式,就能换算出真实的宽和高。

注意:这里有一个极易忽略的细节。相机镜头通常存在畸变(尤其是广角镜头),图像边缘的物体会被拉伸或弯曲。直接用原始图像中的像素距离计算会导致误差。必须在尺寸计算前,先使用相机标定得到的畸变系数对检测到的轮廓点进行去畸变校正,然后再进行后续计算。这一步对提升测量精度至关重要。

2.3 系统工作流设计

整个系统的Pipeline可以概括为以下几步:

  1. 图像采集与预处理:摄像头抓取一帧图像,进行去畸变、色彩增强、降噪等处理。
  2. 目标检测与分割:使用YOLOv8n-seg模型识别出图像中的目标零件,并输出其像素级掩膜。
  3. 轮廓提取与几何分析:从掩膜中提取精确轮廓,计算其像素尺寸(宽、高、面积等),并识别基础形状。
  4. 距离解算:结合已知的真实参考尺寸(或相机-传送带平面几何模型)以及标定好的相机内参,解算出每个目标到相机的距离。
  5. 真实尺寸计算:利用距离和像素尺寸,换算得到目标的真实几何尺寸。
  6. 功耗监测:在另一路图像中,使用LeNet-5模型识别数字式功率计的读数,实现实时功耗采集。
  7. 数据融合与输出:将距离、尺寸、功耗数据绑定时间戳,输出到日志或可视化界面。

3. 工具选型与模型训练:为什么是YOLOv8n和LeNet-5?

搭建这个系统,算法模型是核心。选型不仅要看精度,更要权衡速度、资源消耗和易用性。

3.1 目标分割:YOLOv8n-seg的轻量之道

YOLO系列一直是实时目标检测的标杆。YOLOv8是Ultralytics公司推出的最新版本,提供了从n(nano)到x(extra large)不同大小的模型。对于工控机或边缘设备部署,YOLOv8n(nano)是平衡精度与速度的绝佳选择。

  • 为什么选-seg(分割)版本而不是-det(检测)?如前所述,检测框对于尺寸测量太粗糙。分割模型能提供像素级掩膜,让我们能进行精确的轮廓分析,这是高精度尺寸测量的基础。YOLOv8n-seg在保持轻量化的同时,分割精度对于工业零件这类轮廓相对清晰的物体已经足够。
  • 训练数据准备:我们不需要像COCO数据集那样标注80类。只需要收集包含目标零件(如螺丝、垫片)的图像,至少几百张,覆盖不同的光照、角度、遮挡情况。使用标注工具(如LabelStudio、CVAT)进行多边形(Polygon)标注,得到每个实例的轮廓点。这才是最耗时但最重要的环节。
  • 训练技巧
    • 数据增强:必须做。包括随机旋转、亮度对比度调整、添加噪声、模拟运动模糊等,可以极大地提升模型在复杂工业环境下的鲁棒性。
    • 预训练权重:强烈建议使用在COCO数据集上预训练的yolov8n-seg.pt权重进行迁移学习,这比从零训练快得多,效果也好。
    • 关键参数imgsz(图像尺寸)不宜过大,640x640对于小零件检测通常足够,太大影响速度;epochs根据数据集大小调整,一般100-300轮;重点关注mask_loss的下降情况。
# 示例的data.yaml 数据集配置文件 path: ../datasets/parts train: images/train val: images/val names: 0: screw 1: washer 2: bearing
# 训练命令示例 yolo task=segment mode=train model=yolov8n-seg.pt data=data.yaml epochs=150 imgsz=640 batch=16

3.2 数字识别:LeNet-5的稳定与高效

功耗监测模块需要从功率计(假设是7段数码管或数字显示屏)上读取数字。这是一个标准的数字识别任务。虽然可以用更复杂的CRNN或Transformer,但对于固定的仪表字体、相对简单的背景,经典的LeNet-5卷积神经网络完全够用,而且它结构简单、参数少、推理速度极快,非常适合作为系统中的一个轻量级子模块。

  • 为什么不用YOLO做数字检测?可以,但有点“杀鸡用牛刀”。数字识别任务更关注字符本身的分类,而不是定位(仪表位置通常是固定的)。LeNet-5专为手写数字分类设计,经过调整(输入尺寸、类别数),对仪表数字的识别效果很好。
  • 数据准备与训练
    1. 采集功率计在不同读数下的图像。
    2. 由于数字位置相对固定,可以先用一个固定的ROI(感兴趣区域)裁剪出数字显示区域。
    3. 如果显示多位数字,需要先进行数字分割,将每个数字字符单独切分出来。这可以通过垂直投影法(分析每一列黑色像素点的数量,找到数字间的空隙)实现。
    4. 将分割出的单个数字图像,统一缩放到32x32大小,归一化,作为LeNet-5的输入。
    5. 标注数据(0-9),训练一个10分类的LeNet-5模型。

实操心得:数字分割的稳定性。实际环境中,光照可能导致数码管亮度不均,投影法分割可能失效。我的经验是,在分割前先对ROI区域进行二值化形态学闭操作(先膨胀后腐蚀),连接同一个数字内部可能断裂的笔画,能让投影分割的效果更稳定。

4. 系统实现关键步骤与代码剖析

理论说完了,我们上点干货,看看关键步骤的代码怎么实现。

4.1 相机标定:所有测量的基石

标定不准,后面全错。我使用OpenCV的cv2.calibrateCamera函数。

import cv2 import numpy as np import glob # 准备标定板(这里以10x7的棋盘格为例,内角点数为9x6) pattern_size = (9, 6) objp = np.zeros((pattern_size[0]*pattern_size[1], 3), np.float32) objp[:, :2] = np.mgrid[0:pattern_size[0], 0:pattern_size[1]].T.reshape(-1, 2) objp *= 25 # 假设每个方格边长25mm obj_points = [] # 3D点(世界坐标系) img_points = [] # 2D点(图像坐标系) images = glob.glob('calibration_images/*.jpg') for fname in images: img = cv2.imread(fname) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ret, corners = cv2.findChessboardCorners(gray, pattern_size, None) if ret: # 亚像素级角点精确化 criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) corners_refined = cv2.cornerSubPix(gray, corners, (11,11), (-1,-1), criteria) obj_points.append(objp) img_points.append(corners_refined) # 执行标定 ret, camera_matrix, dist_coeffs, rvecs, tvecs = cv2.calibrateCamera( obj_points, img_points, gray.shape[::-1], None, None ) print("相机内参矩阵 K:\n", camera_matrix) print("畸变系数 dist:\n", dist_coeffs) # 保存标定结果 np.savez('calibration_params.npz', camera_matrix=camera_matrix, dist_coeffs=dist_coeffs)

关键点

  • 标定板要平整,拍摄角度要多样(覆盖图像各个区域)。
  • 角点检测后一定要做cornerSubPix亚像素优化,提升标定精度。
  • 畸变系数dist_coeffs很重要,后续所有用于测量的图像都必须先调用cv2.undistort去畸变。

4.2 YOLOv8n分割推理与轮廓提取

加载训练好的模型,进行推理并后处理。

from ultralytics import YOLO import cv2 # 加载模型 model = YOLO('best.pt') # 你训练好的分割模型权重 # 加载标定参数 calib_data = np.load('calibration_params.npz') camera_matrix = calib_data['camera_matrix'] dist_coeffs = calib_data['dist_coeffs'] def process_frame(frame): # 1. 图像去畸变 frame_undistorted = cv2.undistort(frame, camera_matrix, dist_coeffs) # 2. YOLOv8推理 results = model(frame_undistorted, conf=0.5) # 设置置信度阈值 for result in results: if result.masks is not None: masks = result.masks.data.cpu().numpy() boxes = result.boxes.xyxy.cpu().numpy() classes = result.boxes.cls.cpu().numpy() for idx, mask in enumerate(masks): class_id = int(classes[idx]) # 将mask转换为二值图像 (0-255) mask_img = (mask > 0.5).astype(np.uint8) * 255 # 3. 轮廓提取 contours, _ = cv2.findContours(mask_img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if contours: # 取面积最大的轮廓(防止mask有多个离散区域) cnt = max(contours, key=cv2.contourArea) # 4. 几何分析:最小外接矩形 rect = cv2.minAreaRect(cnt) width_pixel, height_pixel = rect[1] # 像素宽高 # 注意:minAreaRect返回的宽高不一定有序,长边可能是宽或高 width_pixel, height_pixel = sorted([width_pixel, height_pixel], reverse=True) # 5. 距离与尺寸计算(假设已知真实宽度W_real) W_real = 0.01 # 例如,零件真实宽度为10mm = 0.01m f_pixel = camera_matrix[0, 0] # 焦距fx (像素) distance_Z = (W_real * f_pixel) / width_pixel # 计算真实高度 H_real = (height_pixel * distance_Z) / f_pixel print(f"目标{idx}: 类别{class_id}, 像素尺寸({width_pixel:.1f}, {height_pixel:.1f}), " f"距离{distance_Z:.3f}m, 真实尺寸({W_real*1000:.1f}mm, {H_real*1000:.1f}mm)")

4.3 LeNet-5数字识别集成

这里展示一个简化版的LeNet-5模型定义和推理流程。

import torch import torch.nn as nn import torch.nn.functional as F class LeNet5(nn.Module): def __init__(self, num_classes=10): super(LeNet5, self).__init__() self.conv1 = nn.Conv2d(1, 6, kernel_size=5, stride=1, padding=2) # 输入1通道(灰度) self.pool1 = nn.AvgPool2d(kernel_size=2, stride=2) self.conv2 = nn.Conv2d(6, 16, kernel_size=5, stride=1) self.pool2 = nn.AvgPool2d(kernel_size=2, stride=2) self.fc1 = nn.Linear(16*5*5, 120) # 假设输入是32x32,经过两次池化后为5x5 self.fc2 = nn.Linear(120, 84) self.fc3 = nn.Linear(84, num_classes) def forward(self, x): x = self.pool1(F.relu(self.conv1(x))) x = self.pool2(F.relu(self.conv2(x))) x = x.view(-1, 16*5*5) x = F.relu(self.fc1(x)) x = F.relu(self.fc2(x)) x = self.fc3(x) return x # 加载训练好的数字识别模型 digit_model = LeNet5(num_classes=10) digit_model.load_state_dict(torch.load('lenet5_digit_best.pth')) digit_model.eval() def read_power_meter(roi_image): # roi_image 是预先裁剪好的功率计数字区域图像 # 1. 预处理:转灰度、二值化、形态学操作 gray = cv2.cvtColor(roi_image, cv2.COLOR_BGR2GRAY) _, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY_INV) kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3,3)) binary = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel) # 2. 数字分割:垂直投影法 vertical_projection = np.sum(binary, axis=0) # 寻找投影为0的列作为分割点(简化逻辑,实际需处理粘连等) digit_images = [] # ... (具体分割代码,将每个数字切分成单独的32x32图像) # 3. 识别每个数字 power_reading = '' for digit_img in digit_images: digit_tensor = torch.from_numpy(digit_img).float().unsqueeze(0).unsqueeze(0) / 255.0 with torch.no_grad(): output = digit_model(digit_tensor) pred = output.argmax(dim=1).item() power_reading += str(pred) return float(power_reading) # 转换为浮点数,单位根据仪表定

5. 实测中的挑战与调优经验

系统搭起来容易,跑稳定难。下面是我在调试过程中遇到的几个典型问题及解决方法。

5.1 测量精度波动:光源是“头号敌人”

最初在实验室稳定光源下测试,精度能达到±0.5mm。一到车间,精度直接飘到±3mm以上。问题就出在光照不均和反光上。零件表面的金属反光会让轮廓提取出现严重错误。

  • 解决方案
    1. 增加光源:使用环形LED无影灯,从多个角度均匀照射被测物体,消除阴影和镜面反光。
    2. 偏振片:在镜头前加装偏振镜,可以有效抑制金属表面的高光。
    3. 图像预处理增强:在代码中,除了去畸变,增加了CLAHE(限制对比度自适应直方图均衡化)来处理光照不均,并用非局部均值去噪替代高斯模糊,在平滑噪声的同时更好地保留边缘。
    4. 动态二值化:在提取轮廓前,对分割掩膜所在的局部区域采用自适应阈值(如cv2.adaptiveThreshold)而非固定阈值,以适应局部亮度变化。

5.2 距离计算误差:标定与参考物之谜

即使轮廓提取准了,距离算出来还是不对。排查发现两个问题:

  1. 标定板方格尺寸输入错误:在标定代码中,objp *= 25这里的25是方格的真实物理尺寸(毫米)。我一开始误以为是厘米,导致整个标定尺度错误。务必用游标卡尺精确测量标定板方格尺寸。
  2. 已知参考尺寸不准确:公式Z = (W_real * f) / W_pixel中的W_real必须是物体在测量方向上的真实尺寸。如果零件摆放有旋转,其图像宽度可能并非对应真实宽度。解决方法:要么确保零件方向固定(使用夹具),要么通过轮廓分析计算出物体的方向角,进行几何校正。

5.3 实时性瓶颈:模型推理与代码优化

在工控机(i5-8代,无GPU)上同时跑YOLOv8n分割和LeNet-5,帧率只有8-10 FPS,达不到实时(15+FPS)要求。

  • 性能剖析:使用Python的cProfile工具分析,发现80%的时间花在YOLOv8的推理上。
  • 优化措施
    1. 模型量化:使用PyTorch的量化工具将YOLOv8n模型从FP32转换为INT8精度。推理速度提升近一倍,精度损失在可接受范围内(<1% mAP下降)。
    2. OpenCV DNN推理:将PyTorch模型转换为ONNX格式,用OpenCV的dnn模块加载推理。OpenCV C++后端在某些CPU上优化更好,比原版PyTorch推理快约30%。
    3. 多线程处理:将图像采集、YOLO推理、后处理计算、功耗识别分别放在不同的线程中,形成流水线,避免相互阻塞。
    4. 降低分辨率:在保证检测精度的前提下,将模型输入分辨率从640x640降至480x480,速度又有显著提升。

经过优化,最终在同一个工控机上达到了约22 FPS,满足了实时性需求。

5.4 功耗监测的数字识别“跳变”

功率计读数识别时,偶尔会出现数字跳变(如从“120”突然跳到“92”)。原因是数字分割时,某个数字字符(如“1”)因为笔画细,在二值化时断裂,导致投影分割出错,切分出了错误区域或漏切。

  • 解决方案
    1. 形态学操作调参:调整闭操作(MORPH_CLOSE)的核大小,让细笔画能够连接起来。对于“1”这种字符,可能需要使用细长的竖向核。
    2. 加入数字位置先验:如果功率计数字显示位置非常固定,可以预先定义好每个数字的固定区域(ROI),直接按位置裁剪,完全避免动态分割的不可靠性。这是最稳定的一招。
    3. 结果滤波:对连续几帧的识别结果进行中值滤波或滑动平均,瞬间的跳变会被平滑掉。例如,取最近5帧读数的中值作为当前输出。

6. 系统集成与部署思考

把各个模块拼装成一个稳定运行的系统,还需要考虑一些工程化问题。

数据流与同步:距离尺寸数据(来自主摄像头)和功耗数据(来自监测摄像头)需要严格的时间同步。我采用的方式是给每一帧主图像打上时间戳,功耗识别线程以不低于主帧率的频率读取仪表,并将最近时间戳匹配的读数与主数据绑定。更严谨的做法是使用硬件触发,让两个摄像头同步采集。

结果可视化与输出:使用OpenCV的cv2.putTextcv2.drawContours将测量结果实时绘制在图像上,方便调试。最终数据通过JSON格式输出到文件或通过网络Socket发送给上位机(如MES系统)。格式类似:

{ "timestamp": "2023-10-27T14:30:25.123", "objects": [ { "class": "screw", "distance_m": 0.856, "width_mm": 5.12, "height_mm": 20.34 } ], "power_watt": 125.6 }

误差分析与标定维护:任何测量系统都有误差。我们需要定期(如每周或每月)用标准量块对系统进行验证和复标定。记录测量误差,如果误差超出阈值(如±1%),则需要检查摄像头是否松动、光源是否变化,并重新进行相机标定。这是一个工业系统能长期可靠运行的必要维护流程。

这个项目从构思到最终在产线上试运行,前后折腾了一个多月。最大的体会是,单目视觉方案在成本敏感、精度要求不是极端苛刻(亚毫米级)的场合,完全有实用价值。但它的稳定性严重依赖精细的标定、可控的光照环境以及鲁棒的图像预处理算法。模型反而不是最难的,YOLOv8和LeNet-5这些现成的工具已经很强大了。真正的功夫,都下在了那些看起来不那么“智能”的细节处理上。如果你也打算做类似的项目,希望我踩过的这些坑,能帮你把路铺得平一点。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询