简介:针对OpenMV4测距与物体大小测量场景,这份资料面向嵌入式机器视觉初学者及需要快速实现测距功能的开发者,以MicroPython代码为核心解决了图像中目标距离与尺寸估算问题。压缩包共含2个文件,包括1个Python脚本和1个Word说明文档,体积仅13KB。Python脚本从摄像头初始化、图像采集到特征点识别,逐步完成了基于三角测距原理的距离计算与物体大小换算;Word文档则补充了焦距、像素尺寸、畸变校正系数K1/K2/K3等核心参数的含义与校准思路,方便开发者对照原理理解代码逻辑,并根据不同目标及时调整参数。目前已有2657人学习下载,适合正在做巡检小车、智能货架或视觉定位项目的读者直接参考。借助这套代码,能够快速理解OpenMV4的图像处理流程,掌握像素距离与实际距离的比例换算方法,在实际项目中有效减少测量误差,提升测量稳定性。
1. OpenMV4测距和测量物体大小:先把两个标定数填对,代码只是除法
OpenMV4是一块运行MicroPython的嵌入式视觉模块,很多开发者拿到手第一个想解决的问题就是“怎么测距离”“怎么测物体长宽”。真正写代码之前会遇到一个反直觉的事实:固件里没有现成的get_distance(),摄像头只会输出色块的像素宽度、AprilTag的像素边长,不会输出毫米。像素到毫米之间隔着两个必须由你提供的先验值——镜头的等效像素焦距,以及目标物体的实际物理尺寸。焦距标定完成之后,测距和测大小本质上就是同一道除法:距离 = 物理宽度 × 焦距 ÷ 像素宽度。这篇文章从针孔相机模型切入,给出焦距标定、颜色blob测距、物体尺寸测量和AprilTag融合测距的可运行代码,最后落在我实际调试中最影响精度的曝光锁定、畸变校正和现场误差验证上。
2. OpenMV4单目测距的针孔模型与焦距标定
2.1 针孔相机模型:像素宽度和物理宽度只差一个比例常数
单目测距的基本关系来自相似三角形。假设一个物体的实际宽度为 W(毫米),它到镜头光心的距离为 Z(毫米),在传感器上成像后的像素宽度为 w(像素),镜头等效焦距为 f(像素),那么物体在空间中的宽度和距离之比,等于成像面上的像素宽度和焦距之比:
W / Z = w / f变换一下就能得到两个最常用的公式。已知物体实际宽度 W,测出当前帧里的像素宽度 w,距离就是:
Z = W × f / w已知距离 Z,要反推物体的实际宽度,公式变成:
W = w × Z / f这里的 f 不是镜头包装上印的 2.8mm 或 6mm。那个光学焦距是镜头本身的物理参数,而公式里要求的是等价的像素焦距,它由镜头焦距、感光芯片像元尺寸、图像分辨率共同决定。同一个镜头,在 QVGA(320×240)和 VGA(640×480)两种分辨率下,像素焦距大约是 2 倍关系。换句话说,换一次分辨率就要重新标定一次 f,直接沿用旧值会让测距结果系统性偏移。
还要注意公式的成立条件:目标平面要和镜头光轴垂直,目标最好位于画面中央附近。目标倾斜超过 15 度时,像素宽度会按余弦关系收缩,测出来的距离会比实际值大,这部分误差在后面测大小方案里同样存在。
2.2 为什么 OpenMV4 固件不提供直接的测距函数
单目视觉本身存在尺度模糊问题。一张画面里宽度为 100 像素的红色色块,可能是距离 50mm、宽度 10mm 的小物体,也可能是距离 500mm、宽度 100mm 的大物体。仅凭这一帧图像,OpenMV4 无法区分这两种情况,因为两者的成像结果完全一样。固件里的find_blobs、find_apriltags只返回像素坐标系下的位置、宽度、面积这些无量纲结果,物理尺度必须由外层程序补充。
所以正确的理解是:OpenMV4 负责“看到”目标并给出稳定的像素数据,你负责“告诉”它目标在世界里的一个真实尺寸。这就是为什么测距代码里必然有一个KNOWN_WIDTH_MM常量,也是为什么 AprilTag 测距会比颜色测距更省心——AprilTag 的物理边长可以直接在代码里声明,不需要临时测量。
2.3 用标定板完成焦距标定的步骤与脚本
标定焦距不需要特殊设备,一张白纸、一把卡尺就能完成。常见的做法是打印一个黑色矩形,量出它的实际宽度,放在镜头前方某个精确测过的距离处,然后在 OpenMV4 IDE 的“工具 → 阈值编辑器”里读出这个矩形的像素宽度,代入公式:
f = 像素宽度 × 实际距离 / 实际宽度更省事的办法是让程序自动读取黑块的像素宽度,下面这段脚本可以在 QVGA 分辨率下完成标定:
# focal_calibration.py - 标定当前镜头的等效像素焦距 import sensor, image, time sensor.reset() sensor.set_pixformat(sensor.RGB565) sensor.set_framesize(sensor.QVGA) # 320x240,标定和运行必须用同一种分辨率 sensor.skip_frames(time=2000) # 已知量:黑块实际尺寸和到镜头的距离,用尺子实测 KNOWN_WIDTH_MM = 80.0 # 标定黑块的物理宽度 DISTANCE_MM = 300.0 # 黑块平面到镜头前表面的距离 # 黑色阈值,在LAB空间下取一个能覆盖黑块的区间 black_threshold = (0, 35, -128, 127, -128, 127) # 在画面中央取一块ROI,避免广角边缘畸变影响宽度读数 roi = (60, 60, 200, 120) img = sensor.snapshot() blobs = img.find_blobs([black_threshold], roi=roi, pixels_threshold=500, merge=True) if blobs: # 取面积最大的黑块,防止反光点干扰 b = max(blobs, key=lambda x: x.area()) focal_pixel = b.w() * DISTANCE_MM / KNOWN_WIDTH_MM print("pixel_width=%d focal_pixel=%.1f" % (b.w(), focal_pixel)) else: print("no blob found, adjust threshold or roi")参数说明:roi=(60, 60, 200, 120)在 320×240 画面里框出中央偏上的区域,舍掉边缘,减少广角畸变对宽度测量的影响;pixels_threshold=500过滤掉小噪声;取max(blobs, key=lambda x: x.area())是防止黑块上的白色反光被拆分成多个小 blob,最大面积的那个通常是真实目标。标定距离建议在 300~500mm 之间,太近对焦不实,太远则像素宽度过小,1 个像素的量化误差就会带来百分之几的距离偏差。
为了得到更稳的结果,我一般会取三个距离各测一次,取焦距平均值:
| 标定参数 | 推荐设置 | 原因 |
|---|---|---|
| 帧尺寸 | QVGA | 与正式测距代码保持一致,严禁标定用 QVGA、测距切到 VGA |
| 曝光模式 | 手动固定 | 自动曝光下黑块面积随亮度波动,导致标定值漂移 |
| 距离档位 | 300mm / 400mm / 500mm | 多档取平均可抵消单次读数的量化误差 |
| 目标像素宽度 | 大于 60px | 低于该阈值时 1px 误差占比超过 1.5% |
标定完成后把focal_pixel写进正式代码,并注释掉标定段,避免每次开机都重复标定。
3. OpenMV4色块测距代码:从阈值配置到毫米输出的可运行方案
3.1 颜色阈值与 blob 提取参数怎么配置
颜色测距的第一步是把目标从背景里分离出来。OpenMV4 的find_blobs默认在 LAB 色彩空间工作,阈值列表(L_min, L_max, A_min, A_max, B_min, B_max)六个值依次对应亮度、绿红轴、蓝黄轴的上下界。相比 RGB,LAB 把亮度分离到单独通道,光照变化时阈值不容易整个失效。
以红色物体为例,一个能用的初始阈值是:
red_threshold = (25, 80, 30, 127, 20, 127)这个区间表示亮度在 25~80 之间、红轴分量大于 30 的像素会被认为是目标。实际场景里需要打开 IDE 的阈值编辑器,对着目标物体微调 A、B 通道边界。要注意的是,阈值太宽会把背景里相近颜色的区域一起框进来,阈值太窄则目标在光照波动时会被切碎,两者都会让 blob 的像素宽度不稳定,进而让测距结果产生跳动。
find_blobs的几个关键参数直接影响后续测距质量:pixels_threshold=150过滤掉小于 150 像素的碎片;area_threshold=150和它作用类似,合并时会参考;merge=True会把相邻的多个 blob 合并成一个整体,适合表面有反光或图案的目标,但如果场景里有多个相同颜色的物体,合并会把它们连成一个宽带,测距结果会严重偏大。
3.2 完整的颜色测距循环代码
下面这段脚本可以在 QVGA 分辨率下锁定一个红色目标,并实时输出毫米距离:
# color_distance.py - 基于色块宽度的单目测距 import sensor, image, time sensor.reset() sensor.set_pixformat(sensor.RGB565) sensor.set_framesize(sensor.QVGA) # 320x240 sensor.skip_frames(time=2000) # 固定曝光和白平衡,避免blob面积随风光波动 sensor.set_auto_exposure(False, exposure_us=20000) sensor.set_auto_whitebal(False, rgb_gain_db=(55, 50, 45)) # 标定值:焦距来自第二章脚本,目标实际宽度来自卡尺 FOCAL_PIXEL = 460.0 KNOWN_WIDTH_MM = 50.0 red_threshold = (25, 80, 30, 127, 20, 127) def calc_distance(pixel_width): if pixel_width <= 1: return -1.0 return (KNOWN_WIDTH_MM * FOCAL_PIXEL) / pixel_width while True: img = sensor.snapshot() blobs = img.find_blobs([red_threshold], pixels_threshold=150, area_threshold=150, merge=True) if not blobs: continue # 多目标时只取面积最大的,通常是离镜头最近或实际最大的目标 b = max(blobs, key=lambda x: x.area()) # 像素宽度过小说明目标太远或太小,测量不可信 if b.w() < 15: continue dist_mm = calc_distance(b.w()) img.draw_rectangle(b.rect(), color=(255, 0, 0)) img.draw_cross(b.cx(), b.cy(), color=(255, 0, 0)) img.draw_string(b.x(), b.y() - 12, "%.0fmm" % dist_mm, color=(255, 0, 0)) print("dist=%.1fmm w=%dpx" % (dist_mm, b.w())) time.sleep_ms(50)代码逻辑分四步:find_blobs取出所有候选目标;max(blobs, key=lambda x: x.area())只保留面积最大的 blob,避免多个小色块干扰;b.w()拿到目标的像素宽度;calc_distance里的除法把像素宽度还原成物理距离。其中if b.w() < 15这个过滤很关键,目标离得太远时它的像素宽度只有几个像素,w的微小波动会被除法放大,输出结果会大幅抖动,这时与其给一个错误数值,不如直接不显示。
提示:
merge=True在近距离多目标场景下会把不同物体合并成一个宽带。如果实测发现近处物体输出距离明显偏小,把merge改成False,并只取最大 blob 即可。
3.3 分辨率、曝光和镜头畸变对测距精度的实际影响
测距结果稳定不稳定,很多时候不是代码问题,而是图像参数在变。以下是影响最大的三个因素:
| 因素 | 影响表现 | 建议 |
|---|---|---|
| 分辨率切换 | QVGA 与 VGA 的像素焦距不同,公式里的FOCAL_PIXEL失效 | 测距分辨率固定不变,标定与运行保持一致 |
| 自动曝光 | 暗处自动拉高增益,目标边缘被提亮,blob 面积变大,距离读数跳变 | 手动固定曝光时间,必要时关闭自动增益 |
| 广角镜头畸变 | 画面边缘物体被拉伸,相同物理宽度的 blob 在边缘比中央宽 | 只使用画面中央 60%~70% 区域,或用lens_corr校正后再测 |
曝光影响是最隐蔽的。OpenMV4 默认开启自动曝光,当镜头对着窗外或亮背景时,自动曝光会压低亮度,红色目标的暗部直接低于阈值下限,blob 的宽度瞬间萎缩,测距结果可能从 300mm 跳到 350mm。固定曝光之后这种跳变基本消失,代价是画面可能在强光下过曝、暗光下偏暗,但只要目标轮廓仍能被阈值完整分割,固定曝光永远利大于弊。
4. OpenMV4测量物体大小代码:三种路径把像素还原成毫米
4.1 已知距离时的物体长宽测量代码
测距和测大小是同一个公式的两种用法。当目标离镜头的距离已知时,物体的实际宽度和高度用下面的循环代码就能得到:
# object_size.py - 已知距离,测量物体实际长宽 import sensor, image, time sensor.reset() sensor.set_pixformat(sensor.RGB565) sensor.set_framesize(sensor.QVGA) sensor.skip_frames(time=2000) FOCAL_PIXEL = 460.0 DIST_MM = 500.0 # 目标平面到镜头的固定距离,用尺子量好后写死 blue_threshold = (20, 70, 0, 60, 60, 127) while True: img = sensor.snapshot() blobs = img.find_blobs([blue_threshold], pixels_threshold=200, merge=True) for b in blobs: width_mm = b.w() * DIST_MM / FOCAL_PIXEL height_mm = b.h() * DIST_MM / FOCAL_PIXEL img.draw_rectangle(b.rect(), color=(0, 0, 255)) img.draw_string(b.x(), b.y() - 12, "%.1fx%.1fmm" % (width_mm, height_mm)) print("width=%.1fmm height=%.1fmm" % (width_mm, height_mm))b.w()和b.h()是色块的像素宽高,乘以DIST_MM / FOCAL_PIXEL这个比例因子后直接变成毫米。这个比例因子在固定距离下是常量,整段代码不需要除法之外的任何复杂运算。
必须留意两个误差源:一是目标必须正对镜头,目标平面相对光轴倾斜 15 度时,宽度误差约 3.4%,倾斜 30 度时误差接近 14%;二是find_blobs返回的是外接矩形宽高,如果目标本身有弧度或遮挡,b.w()会包含缺口,测量值会小于真实包络尺寸。
4.2 AprilTag 融合方案:一套代码同时输出距离和物体大小
AprilTag 的优势在于它的物理边长是已知的。OpenMV4 的find_apriltags返回每个 tag 的像素宽度tag.w(),配合标定好的焦距,就能算出 tag 到镜头的距离。更实用的是,把待测物体放到 tag 旁边同一个平面上,算出这个平面的距离后,再用同一条公式测量物体大小:
# apriltag_distance_size.py - AprilTag 测距 + 同平面物体测大小 import sensor, image, time sensor.reset() sensor.set_pixformat(sensor.GRAYSCALE) # AprilTag 用灰度图识别率更稳定 sensor.set_framesize(sensor.VGA) # 高分辨率下的 tag 像素宽度更精确 sensor.skip_frames(time=2000) sensor.set_auto_exposure(False, exposure_us=25000) TAG_SIZE_MM = 30.0 # AprilTag 的实际边长,打印时已确定 FOCAL_PIXEL = 900.0 # VGA 分辨率下重新标定过的焦距 while True: img = sensor.snapshot() tags = img.find_apriltags(families=image.TAG36H11) for tag in tags: # 由 tag 的像素宽度反推距离 z_mm = (TAG_SIZE_MM * FOCAL_PIXEL) / tag.w() img.draw_rectangle(tag.rect(), color=(255, 0, 0)) img.draw_string(tag.cx(), tag.cy() - 15, "z=%.0fmm" % z_mm, color=(255, 0, 0)) # 在 tag 同一平面内,紧贴 tag 右侧的红色物体 red_threshold = (25, 80, 30, 127, 20, 127) search_roi = (tag.x() + tag.w() + 5, tag.y() - 10, img.width() - tag.x() - tag.w() - 5, tag.h() + 20) blobs = img.find_blobs([red_threshold], roi=search_roi, pixels_threshold=100, merge=True) for b in blobs: width_mm = b.w() * z_mm / FOCAL_PIXEL height_mm = b.h() * z_mm / FOCAL_PIXEL img.draw_rectangle(b.rect(), color=(0, 255, 0)) img.draw_string(b.x(), b.y() - 12, "%.1fx%.1fmm" % (width_mm, height_mm), color=(0, 255, 0))这个方案的实用价值在于:tag 本身提供了尺度基准,目标物体只要与 tag 共面,就可以直接用同一个z_mm换算尺寸,不需要额外测量距离。search_roi把色块搜索范围限制在 tag 右侧的狭长区域,避免把其他无关色块也拉进来。tag.w()在距离很近或 tag 被遮挡时会失真,实测中如果输出距离明显跳变,优先检查 tag 是否完整露出,而不是怀疑焦距标定错了。
4.3 硬件辅助方案:OpenMV4 搭配 ToF 或超声波模块补距离
纯单目测距依赖已知目标宽度,如果目标物体尺寸不固定,最可靠的办法是外接一个测距传感器拿到真实距离,视觉只负责测量目标在画面中的像素占比。OpenMV4 通过 I2C 接 VL53L0X 激光测距模块,就能在一个循环里同时拿到真实距离和物体像素宽度:
# tof_vision_size.py - VL53L0X 提供距离,视觉提供像素宽度 from machine import I2C import sensor, image, time # VL53L0X 的驱动库放在 openmv 根目录的 vl53l0x.py 里 from vl53l0x import VL53L0X sensor.reset() sensor.set_pixformat(sensor.RGB565) sensor.set_framesize(sensor.QVGA) sensor.skip_frames(time=2000) i2c = I2C(2) # OpenMV4 的 I2C2 引脚 tof = VL53L0X(i2c) FOCAL_PIXEL = 460.0 red_threshold = (25, 80, 30, 127, 20, 127) while True: img = sensor.snapshot() dist_mm = tof.read() # 单位毫米,量程在 30~1200mm 之间 blobs = img.find_blobs([red_threshold], pixels_threshold=100, merge=True) for b in blobs: width_mm = b.w() * dist_mm / FOCAL_PIXEL height_mm = b.h() * dist_mm / FOCAL_PIXEL img.draw_string(b.x(), b.y() - 12, "%.1fx%.1fmm" % (width_mm, height_mm), color=(255, 0, 0)) print("dist=%dmm size=%.1fx%.1fmm" % (dist_mm, width_mm, height_mm)) time.sleep_ms(30)这种组合把尺度问题交给硬件解决,视觉只负责识别和分割,整体可靠性明显高于纯视觉方案。要注意的是 VL53L0X 的测距点是镜头旁边的固定位置,不是画面中心,目标在画面里左右移动时,测到的距离与目标真实距离会有偏差,摆放时尽量让 ToF 的激光点和目标在同一个水平面上。
5. OpenMV4测距与尺寸测量现场精度调优的5个细节
5.1 锁定曝光、白平衡和增益,画面参数全部固定
自动曝光加上自动白平衡会让阈值分割结果随环境光波动,这是测距跳变的头号原因。固定参数有三行代码,放在skip_frames之后:
sensor.set_auto_whitebal(False, rgb_gain_db=(55, 50, 45)) sensor.set_auto_exposure(False, exposure_us=20000) sensor.set_auto_gain(False, gain_db=20)exposure_us=20000表示曝光时间 20 毫秒,暗光下可以提高到 35 毫秒,但帧率会下降;gain_db=20限制模拟增益上限,防止夜间噪点把 blob 边缘打碎。现场环境光照变了,重新调一次这三个值,不要只调颜色阈值。
5.2 用 lens_corr 校正广角镜头边缘畸变
OpenMV4 常见的 2.8mm 广角镜头在画面边缘畸变明显,同一个物体从画面中央移到边缘,测出来的像素宽度会变化。等测距代码跑通后,在find_blobs之前加一行:
img.lens_corr(1.8) # strength 取值 1.5~2.5,按镜头实测调整lens_corr的作用是桶形畸变补偿,参数1.8是校正强度。标定方法很简单:用尺子量一个固定宽度的物体,把它在画面里从中央移到边缘,观察输出宽度变化;如果边缘宽度明显大于中央,增大 strength,直到两者差异低于 2%。注意lens_corr会重采样图像,略微增加运算时间,对 QVGA 分辨率影响可忽略。
5.3 对测距结果做 5 帧中值滤波,而不是平均值
单帧的b.w()会有±1~2 像素的抖动,换算成距离就是好几毫米。常见的做法是求平均值,但平均值对偶发的离群值不够稳,比如某帧反光导致 blob 宽度突然增加 20 像素,平均值会被拉偏。改用中值滤波,保留最近 5 帧的距离做个排序取中间值:
from stats import median dist_history = [] while True: dist_mm = calc_distance(b.w()) # 沿用第三章的测距函数 dist_history.append(dist_mm) if len(dist_history) > 5: dist_history.pop(0) stable_dist = median(dist_history) print("stable=%.1fmm raw=%.1fmm" % (stable_dist, dist_mm))median取中间值,即使 5 帧里有一帧完全失真,输出也不会被带偏。这个稳定值再送给显示、串口或电机控制逻辑,比直接使用dist_mm可靠得多。
5.4 现场验证:三档距离误差表最直接
正式验收之前,用一把尺子摆三个距离档位,把实测值对比写下来:
| 标尺距离 | 代码输出 | 误差 | 备注 |
|---|---|---|---|
| 300mm | 312mm | +4.0% | 目标离镜头近,blob 宽度大,量化误差占比低 |
| 500mm | 508mm | +1.6% | 接近标定距离,精度最好 |
| 800mm | 842mm | +5.3% | 像素宽度变小,边缘模糊影响阈值分割 |
如果误差随距离整体线性偏大,优先怀疑焦距标定值偏小,回到第二章重标;如果只有远距离误差大,检查是否该换更高分辨率并重新标定焦距;如果误差分布无规律,先固定曝光,再看是不是光线让阈值分割不稳定,最后才考虑换镜头。这个排查顺序能覆盖现场 90% 的测距精度问题。
本文还有配套的精品资源,点击获取