前言

2025 年电赛 C 题的难点不在于识别图形,而在于如何在单目条件下把识别结果稳定地换算成真实尺寸。纯目标检测很容易得到类别和像素框,但题目要的是距离和边长。这套方案的思路是先用 A4 参考物建立尺度,再用几何分析和 YOLO 做目标筛选与测量。

下面直接讲方案里真正有用的部分。

整体方案

系统围绕一张带黑边的 A4 参考板工作,完整链路如下:

  1. 摄像头采集固定分辨率图像。
  2. 在预设 ROI 内寻找 A4 黑边内轮廓。
  3. 对 A4 做透视校正,得到统一尺度的俯视图。
  4. 基础题直接在俯视图中做几何测量。
  5. 发挥题和发挥题二先在俯视图中运行 YOLO,再结合几何分析计算真实边长。

YOLO 只负责定位目标,尺度换算还是走 A4 参考物,这样可以绕开直接用检测框回推尺寸时精度不稳的问题。

基础题:先把 A4 参考系建立起来

识别黑边内轮廓,不用外轮廓

题目场景里有一条 5 mm 黑色基准线。参考板贴近地面时,外轮廓容易和背景基准线混在一起,所以代码直接用黑边内轮廓的实际尺寸:

1
2
A4_WIDTH_MM = 170
A4_HEIGHT_MM = 257

注意这里不是用标准 A4 的 210 mm × 297 mm,而是黑边内框对应的可识别区域,后续距离计算都依赖这个数值。

固定 ROI + 轮廓筛选

项目没有做全画面搜索,而是在主画面里裁出一个固定 ROI,再在其中做灰度化、高斯滤波和二值化轮廓提取。随后保留面积足够大的候选轮廓,并取排序后的第二大轮廓作为黑边内框目标。这个策略建立在比赛场景相对固定的前提上,优点是快,缺点是对摆位比较敏感。

核心逻辑可以概括成下面这段:

1
2
3
4
5
roi_x, roi_y, roi_w, roi_h = (526, 222, 227, 276)
gray = cv2.cvtColor(roi_image, cv2.COLOR_BGR2GRAY)
blur = cv2.GaussianBlur(gray, (5, 5), 0)
edged = cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)[1]
contours, _ = cv2.findContours(edged, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

后续再通过面积阈值、四边形逼近和长宽比约束,把不合理轮廓筛掉。

透视变换:按高推宽

找到 A4 之后做透视变换。代码没有直接用轮廓的上下边长度定目标宽度,而是先算左右边的平均高度,再按 A4 已知比例反推矫正后的宽度。A4 水平旋转时图像里”宽”的变化比”高”更明显,所以用高来反推更稳定:

1
2
max_height = int((height_left + height_right) / 2)
max_width = int(max_height * (A4_WIDTH_MM / A4_HEIGHT_MM))

这样得到的俯视图尺度稳定一些,后面的距离和边长估计误差也会小一点。

距离和尺寸如何换算

距离估计采用标准针孔模型:

1
distance = (real_height_mm * focal_length_px) / image_height_px

程序分别用 A4 的宽和高估出两个距离,再取平均值,降低单方向误差。基础题模式下,DataCollectionThread 会在限定次数和超时内采集最多 8 张有效数据,再输出平均距离和平均边长。

发挥题:YOLO 负责找目标,几何分析负责算边长

发挥题没有直接拿 YOLO 检测框宽高当结果。先在 A4 俯视图里运行 YOLO 定位候选目标,再对每个候选框做轮廓分析,找出最长垂直线段作为真实边长的估计依据。检测框受外接矩形和旋转角度影响,直接换算误差偏大,所以这里绕开了框尺寸。

最长垂直线段作为边长候选

find_short.py 里的核心函数会先对目标轮廓做 Douglas-Peucker 简化,再遍历相邻边,找出同时与前后边近似垂直的线段。满足约束的线段中,最长的一条被视为最可信的边长候选:

1
2
epsilon = 0.01 * cv2.arcLength(main_contour, True)
approx = cv2.approxPolyDP(main_contour, epsilon, True)
1
2
3
4
is_perpendicular = (
abs(angle_prev - 90) < angle_threshold and
abs(angle_next - 90) < angle_threshold
)

在发挥题里,系统会比较每个目标的最长垂直线段长度,选出“最小正方形”;然后再结合 A4 提供的距离与焦距参数,把像素长度换算成真实边长。当前实现还会对结果做一个很小的经验补偿,以抵消实际拍摄误差。

为什么要预加载和多帧平均

如果 YOLO 在进入发挥题时才首次加载,界面会明显卡顿。所以程序在 CameraThread 初始化时就预加载 best.pt,窗口启动后用一张空白图做一次预热推理,把首次延迟转移到后台。

发挥题不是单帧出结果,而是在超时和最大尝试次数限制内采集 5 张有效数据再取平均。单帧偶发误差基本被抹掉了,等待时间也还在能接受的范围里。

发挥题二:把“识别全部”改成“只测一个”

发挥题二的逻辑和发挥题类似,但增加了一个用户交互步骤:先在界面选择数字 0-9,再只保留对应类别的检测结果。程序在 YOLO 输出后,会把 cls_name 转成数字,只分析用户指定的类别。

这套模式适合比赛里“指定目标测量”的场景。它并没有改变测量方法,改变的只是目标筛选策略,因此整体复用度很高。