简介:面向参加2023年全国大学生电子设计竞赛E题及学习机器视觉的读者,这份代码包聚焦K210视觉识别与STC32G主控云台协同的完整实现,覆盖图像采集处理、颜色与目标识别、距离测量、主机与从机云台控制、嵌入式工程配置等环节,可用于赛题方案复现、模块拆分学习与现场调试排错。压缩包共352个文件、约19.47MB,内含C与H源文件、Python视觉脚本、Keil工程文件、编译中间文件及PDF/DOCX说明文档,并保留lst、obj、crf等编译过程文件,可辅助排查Keil构建问题;其中K210_Vision_code对应Sipeed Maix Bit开发板,主控部分采用STC32G12K128,目录按视觉、主控、云台分工组织,便于对照阅读和二次修改。已有619人学习,适合希望理解电赛E题视觉链路、复用工程框架、查找外设配置细节或补足嵌入式视觉实践经验的参赛者与入门者。
1. 光斑明明在屏幕正中间,云台却往左上角甩
2023年全国大学生电子设计竞赛E题的视觉部分,很多人以为是图像识别的比拼,真正翻车的地方却大多不在识别本身。光斑在屏幕正中、云台偏往左上甩的例子年年都有:像素坐标系 y 轴朝下,屏幕坐标朝上,中间少了一行取反。电赛E题视觉部分代码要跑通的核心链路只有四步——采图、找光斑、算坐标、发指令,四步里任何一步差一点,闭环都会以抖动或超调的形式放大给你看。拿分逻辑也很直白:基本要求吃的是稳定性和重复精度,发挥部分吃的是延迟和抗干扰,视觉模块能不能把误差压到像素级、把延迟压到两帧以内,直接决定测试现场那几分钟的成绩。这份材料适合正在准备控制类、光电类赛题,以及需要在答辩里把视觉链路讲清楚的同学。
2. 考点分布:视觉链路四层拆解,每层都有对应的问法
这场比赛的视觉部分不像算法竞赛,它不比 mAP,比的是“在几十厘米见方的屏幕上把光斑中心稳定报到 1 毫米以内,并且每十几毫秒报一次”。按这个目标倒推,整条链路可以切成四层:采图层、分割层、解算层、执行层。四层的误差不是平均分配的,越靠后放大越狠——分割层差 3 个像素,解算层可能放大成 2 毫米,到了执行层就是云台来回摆。所以时间分配上不要一上来就死磕识别算法,先把坐标方向和标定表弄对,往往十分钟能救回十几分。
2.1 采图层:分辨率、曝光、帧率只能三选二
先说分辨率。640×480 覆盖一块 40 厘米宽的屏幕,像素当量大约是 0.6 毫米一个像素;换到 1280×720,像素当量减半,质心精度确实更好,但单帧处理时间大约翻倍,帧率掉下来,闭环反而更肉。经验值是把分辨率定在能覆盖屏幕且留 10% 余量的档位,别为了“看着清楚”盲目上高分辨率。像素当量这个数要自己算出来写在纸上:屏幕有效宽度 ÷ 图像宽度,后面所有精度讨论都从它出发。
曝光比分辨率更容易出事。默认的自动曝光会让光斑面积随环境光变化:环境一亮,摄像头自动压曝光,光斑从一大团缩成几个像素,面积阈值直接失效;环境一暗,自动拉高增益,噪点全被当成光斑。更麻烦的是激光光斑在过曝时会“烧白”,红色分量的色相被压到接近 0 或乱跳,HSV 里的 H 值就不稳了。正确做法是关掉自动曝光、关掉自动增益,把曝光时间手动压到光斑刚好不过曝的程度。
帧率是最容易被高估的参数。30 fps 和 60 fps 差 16 毫秒,听起来不少,但舵机本身的响应时间就在 100 毫秒量级,光靠堆帧率救不了“慢半拍”。帧率真正的意义是让控制周期稳定,而不是追求数字好看。
| 参数 | 保守配置 | 激进配置 | 主要影响 |
|---|---|---|---|
| 分辨率 | 640×480 | 1280×720 | 像素当量、单帧耗时 |
| 帧率 | 30 fps | 60 fps | 延迟、运动模糊 |
| 曝光 | 手动短曝 | 手动更短 | 光斑面积稳定性、噪声 |
| 增益 | 最低档 | 最低档 | 暗部噪声 |
答辩常问的一句是“你为什么选这个分辨率”,答法不要只说“够用”,要把像素当量和执行机构分辨率摆在一起比:像素当量 0.6 毫米,云台死区折算到屏幕上可能是十几毫米,视觉精度早就过剩了,再往上加分辨率只是给 CPU 添负担。
2.2 分割层:阈值不是调出来的,是框出来的
阈值分割这步,绝大多数队伍的现场操作是打开滑块拖到“看起来差不多”。这种做法在调试台上没事,换到测试现场灯光一变就崩。稳一点的做法是先把现场条件框死:环境光用遮光罩或挡板压到稳定,再在 HSV 空间里框定范围。红色激光在 HSV 里跨 0 度,要拆成 0~10 和 170~180 两段取并集;绿色一般在 45~75 之间。S 和 V 的下限用来挡白色反光和灰色背景,通常 S 卡在 100~150、V 卡在 150 以上比较稳。
阈值确定之后要把它写进配置文件,而不是散落在代码里。现场只允许改一处,改完立刻存盘并重启验证,否则调试时改对了、正式测试跑的是旧参数,这种事故每年都有。
这里还有一个常被忽略的点:光斑不是越大越好。激光打在屏幕上会有一个亮核加一圈光晕,光晕的边缘色相已经偏了,把它算进质心会把中心往一边拽。所以形态学处理之后要挑最大连通域,再用面积上下限卡一道——面积太小是反光碎点,面积太大是有人拿手电或者顶灯直射。
答辩常问“你的阈值怎么确定的”,好的答法是给出确定流程:先固定环境光,再采集若干帧无光斑背景统计噪声上限,然后让光斑落在屏幕四角和中心各采一组,取交集作为最终区间。有流程,就说明不是碰运气。
2.3 解算层:像素坐标到屏幕坐标的那张表
从像素坐标到屏幕物理坐标,最省事的做法是当成线性映射,两个点就能定。问题是镜头有畸变,摄像头也不是正对屏幕,边缘的线性误差能有几个毫米,画正方形的时候四个角会明显不方。比较稳的做法是四点透视变换加二次项补偿,或者干脆用二次多项式做曲面拟合:
x = a0 + a1·u + a2·v + a3·u·v + a4·u² + a5·v² y = b0 + b1·u + b2·v + b3·u·v + b4·u² + b5·v²
每个方向 6 个未知数,至少 6 个标定点,实际做 9 点或 16 点更保险,用最小二乘解。标定点均匀铺满屏幕,别全挤在中间——中间准、边缘飘是这类题最典型的失分形态。屏幕上画好网格,让光斑依次停到交叉点上,记录像素坐标和物理坐标,一次标定存一张表,换屏幕或者动了摄像头立刻重标。
这一步还有一个方向问题必须当场解决:像素坐标 y 轴向下,屏幕坐标习惯上向上,中间要么取反,要么标定的时候就按同一套方向走。别小看这一行,方向反了的表现是“越调越远”,很多人会误判成 PID 参数不对,越调越乱。
答辩常问“你标定做了几个点、为什么”,答法是讲清楚未知数个数和冗余量的关系:6 个未知数至少要 6 点,做 9 点是为了让最小二乘有残差可看,残差大说明镜头畸变超出二次模型能力,那就该考虑加三次项或者换镜头。
2.4 执行层:角度、周期和那条串口线
执行层最容易出现“视觉很准、动作很糙”。二维舵机云台的 PWM 周期 20 毫秒,脉宽 0.5~2.5 毫秒对应 0~180 度,理论角度分辨率不到 0.1 度——但舵机有死区,实际能分辨的最小角度通常在 0.5 度左右。换算一下:屏幕在一米开外,0.5 度对应大约 8.7 毫米。也就是说,指望纯开环“算好角度直接打过去”是不可能准的,必须靠视觉闭环把这个死区盖掉。
闭环带来的副作用是抖动:视觉能分辨 1 个像素(0.6 毫米),执行机构只能分辨 8 毫米,误差落在死区里时控制器会一直输出微小变化,云台就在那儿来回蹭。解法是加死区抑制——误差小于某个阈值就不更新输出,或者用积分分离。这个阈值不能拍脑袋,按执行机构的分辨率反算出来最靠谱。
串口这条线看似简单,现场出问题最多。波特率先定 115200,帧头加校验,指令里带指令码,下位机只认完整帧,校验错就丢弃。这样即使现场有电机干扰导致丢字节,也不会把云台打到限位。控制周期要和相机同步,别用“处理完就发”的写法,那样帧率一波动,PID 的微分项就全乱了。
2.5 分值权重:哪一层值得多花两小时
按这类题目的评分习惯,基本要求考的是“能不能稳定复现”,发挥部分考的是“快不快、抗不抗干扰”。落到四层上:采图层和解算层决定基本要求能不能过,执行层和分割层决定发挥部分能拿多少。所以时间紧的时候,先保证坐标方向对、标定表准、串口协议稳,这三件事做完基本要求就有底;再去抠形态学核大小、亮度加权质心这些提精度的细节。
一个反直觉的建议:调试前期不要在识别算法上花超过三成时间。识别做得再花哨,坐标映射错一行,全部白搭。反过来,识别用最朴素的 HSV 加连通域,只要标定准、闭环稳,成绩一样能看。
3. 高频考点:光斑识别与抗干扰的五个必答点
3.1 为什么 HSV 比 RGB 稳,红色为什么要拆两段
RGB 三个通道都随亮度线性变化,环境光一变,三个数一起动,阈值就得跟着改。HSV 把色相和亮度解耦,H 只描述“是什么颜色”,光强变化主要影响 V,S 受反光影响。激光光斑的 H 非常集中,所以 HSV 在变光环境下稳得多。红色特殊在于它在色相环上跨了 0 度,0~10 和 170~180 是同一类红,必须两段掩膜取并集,只用一段会漏掉一半光斑。
这里有个细节:激光过曝时像素接近纯白,H 会跳变。解决办法是曝光压下来,让光斑核心不饱和,同时把 S 下限调高——过曝白点的 S 很低,会被 S 阈值直接筛掉。用“H 在红色区间且 S 足够高”这个组合条件,比单看 H 稳得多。
答辩追问“为什么不用 RGB 阈值”,可以反问一句:环境光从 300 lux 变到 800 lux,你的 RGB 阈值要不要重调?要重调就说明方法不具备现场鲁棒性。
3.2 形态学核大小不是越大越好
开运算去噪、闭运算补洞,核大小直接影响质心位置。核太大,小光斑会被整个吃掉,尤其是光斑在屏幕边角、被边框切掉一半的时候;核太小,噪点去不干净。经验做法是先开运算去掉孤立点(3×3 足够),再闭运算补断裂(5×5 或 7×7),核再大就要看光斑实际直径了——核直径不该超过光斑直径的三分之一。
还有一个更省事的替代:不做闭运算,直接用轮廓面积下限筛。开运算后保留所有轮廓,按面积排序取最大的那个,面积太小直接判为无目标。这样既避免了闭运算把两个邻近光斑连成一团,又省了一次滤波耗时。
答辩追问“光斑被屏幕边框切掉一半怎么办”,这是真问题。答法是承认它会偏心,然后给出对策:边缘区域用上一帧位置做预测补全,或者干脆在控制上把边框附近设为禁区,让轨迹不贴边。
3.3 二值质心不够用,试试亮度加权
cv2.moments算的是二值掩膜的几何质心,光斑形状不对称时会有偏。激光光斑真实中心在亮度最高点,用灰度加权质心更准:把掩膜内的亮度值当权重,对坐标做加权平均。代价是多一次乘加,640×480 的 ROI 上跑完全无压力。
import cv2 import numpy as np def spot_centroid(frame, h_ranges, s_min=120, v_min=150, area_min=20): """返回亮度加权质心 (u, v) 与面积;找不到目标返回 None。""" hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) mask = np.zeros(hsv.shape[:2], np.uint8) # 红色跨 0 度,两段取并集;绿色只有一段,传一个区间即可 for lo, hi in h_ranges: mask |= cv2.inRange(hsv, (lo, s_min, v_min), (hi, 255, 255)) # 3×3 开运算足够去掉孤立噪点,核再大会吃掉边角的小光斑 mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, np.ones((3, 3), np.uint8)) cnts, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not cnts: return None c = max(cnts, key=cv2.contourArea) area = cv2.contourArea(c) if area < area_min: # 面积下限:滤掉反光碎点 return None # 用掩膜内的 V 通道当权重,比二值几何质心更贴近真实光斑中心 roi = cv2.bitwise_and(hsv[:, :, 2], hsv[:, :, 2], mask=mask) w = float(roi.sum()) if w <= 0: return None ys, xs = np.nonzero(mask) u = float((xs * roi[ys, xs]).sum() / w) v = float((ys * roi[ys, xs]).sum() / w) return u, v, area这段的关键有三处。h_ranges用列表传区间,红色传[(0,10),(170,180)],绿色传[(45,75)],同一份代码复用。area_min是面积下限,按分辨率换算——640×480 上光斑直径通常十几像素,面积两三百,下限设 20 只挡碎点不挡真目标。加权那几行用 V 通道做权重,注意roi[ys, xs]是花式索引,别写成双重循环,否则帧率直接掉一半。
3.4 环境光、反光、屏幕边框:三类干扰各有对策
环境光干扰表现为整片区域被点亮,掩膜里出现大面积连通域。对策是加面积上限,超过阈值直接判为无效;同时把摄像头曝光压到只有激光能过阈值,顶灯再亮也进不来。
反光干扰多来自屏幕边框、支架、选手的手表。这类反光的特点是形状不规则、色相不稳、面积忽大忽小。除了面积和圆度筛选,还可以加一条“上一帧附近”的门限:新光斑位置和上一帧预测位置距离超过阈值就丢弃,等下一帧确认。这条规则在追踪阶段特别有效,因为光斑是连续运动的。
边框干扰是光斑贴边时被切掉一部分,质心被拉向屏幕内侧。对策是把屏幕有效区往里缩 5 个像素作为判断边界,贴边时用预测值代替测量值,并给控制输出限幅,别让云台一头撞到限位。
3.5 光斑丢失不是异常,是常态
追踪过程中光斑被人挡住、被快速甩出视野、两个光斑重叠,都是几帧之内必然发生的事。代码里不能把“找不到”当成异常抛出去,而要当成一个正常状态处理:连续丢失 N 帧以内,用匀速模型外推位置继续发指令;超过 N 帧,停发指令并清空积分项,等目标重新出现再平滑接管。
N 的取值和帧率有关,30 fps 下取 3~5 帧比较合适,也就是 100~160 毫秒。太短会在快速运动时频繁中断,太长会在目标真的消失后云台还在乱跑。清积分这一条尤其重要,光斑丢失期间误差一直是最大值,积分项会迅速饱和,等目标回来时云台已经冲到限位了,这个坑后面还会再提一次。
答辩追问“目标丢失怎么处理”,把上面这套“短时预测 + 长时停机 + 积分清零”讲出来,比说“加个 try except”强太多。
4. 真题演练:六道典型问法与答题思路
4.1 “你画的正方形为什么闭合不上”
这是基本要求里最常被问到的一句。闭合差通常有三个来源:坐标系方向不一致、累计误差没有纠正、顶点处没有停顿导致惯性过冲。
答题顺序应该是先定位再解释。第一步确认坐标系,让光斑停在屏幕正中,看程序输出的屏幕坐标是不是接近几何中心,偏了就说明映射或者方向错了。第二步看累计误差,画完三条边回到起点差多少,差两三毫米属于正常,差一厘米以上就是标定表在边缘不准。第三步看顶点,很多队伍在顶点处不做减速,云台带着惯性冲过去,回来时已经偏了。
对策部分要讲得具体:把正方形拆成四条边加四个顶点,每条边用匀速插值,顶点处停 100~200 毫秒让机械稳定,并且在每条边结束时用视觉测一次实际位置,把偏差摊到下一段。这样即使单段有误差,也不会一路累加。
面试官最想听的是“你怎么知道误差从哪来”,而不是“我们调了很久终于好了”。给出一条可复现的排查路径,比给一个漂亮结果更加分。
4.2 “追踪的时候为什么慢半拍”
延迟要拆开算,不能笼统说“摄像头慢”。一帧曝光大约 16 毫秒(60 fps),USB 传输一帧又是十几毫秒,图像处理 5~10 毫秒,串口下发 1 毫秒,舵机从收到指令到转到位 100~200 毫秒。加起来 150~250 毫秒,这就是“慢半拍”的全部来源,而大头在执行机构,不在视觉。
测量方法要能说出来:让光斑做一次阶跃(从 A 点瞬间跳到 B 点),录一段高帧率视频,逐帧看绿色光斑什么时候开始动、什么时候到位,就能把纯延迟和响应时间分开。没有高速相机就用手机慢动作,240 fps 够用了。
补偿手段分两类。前馈类是在指令里叠加目标速度的估计值,让云台提前一点动;预测类是用历史位置外推下一帧目标位置,把视觉延迟补掉。两类都能改善跟随误差,但都不能消除阶跃响应里的超调,超调还得靠 PID 参数压。
4.3 “红色和绿色光斑重叠了怎么办”
重叠时两个掩膜会连成一片,或者绿色盖住红色导致红色面积骤降。处理思路是先分离再仲裁。
分离靠 HSV 本身:红绿 H 差得远,正常情况下不会混。真正的问题是光斑边缘混色,交界处出现黄色或白色像素,两个掩膜在边界上互相咬。对策是把 S 下限提高一点,只保留色相纯正的像素,边界那些混色点自然被滤掉。
仲裁靠历史:当某个颜色面积突然掉到正常值的 30% 以下,就认为它被遮挡,用上一帧位置加匀速外推顶上,同时把控制增益降一半,避免在不确定的位置上猛打方向。等面积恢复再切回测量值。
这条被问到的概率不低,因为它考的是你有没有考虑过“传感器不可靠”这件事,而不是单纯会不会调阈值。
4.4 “把视觉部分的代码结构讲一下”
别从 import 开始念。按四层讲:采集封装成一个取帧函数,识别封装成一个返回(u, v, area)的函数,解算封装成像素到屏幕的映射,控制封装成 PID 加下发。层与层之间只传结构体或者元组,不传全局变量。
主循环要能背下来,因为它是所有讨论的落点:
import time DT = 1.0 / 60.0 # 控制周期与相机帧率对齐 TARGET = (0.0, 0.0) # 屏幕中心,单位 mm while running: t0 = time.perf_counter() ok, frame = cap.read() if not ok: continue red = spot_centroid(frame, [(0, 10), (170, 180)]) green = spot_centroid(frame, [(45, 75)]) if red is None: lost += 1 if lost > 5: # 连续丢失超过 5 帧才真正停机 pid_x.reset() pid_y.reset() continue lost = 0 sx, sy = pixel_to_screen(red[0], red[1]) # 二次多项式映射 ex, ey = sx - TARGET[0], sy - TARGET[1] # 死区抑制:误差小于执行机构分辨率时不动,避免云台来回蹭 if abs(ex) < DEADBAND_X: ex = 0.0 if abs(ey) < DEADBAND_Y: ey = 0.0 ax = pid_x.step(ex, DT) ay = pid_y.step(ey, DT) send_cmd(ax, ay) # 下发增量角,绝对角由下位机维护 rest = DT - (time.perf_counter() - t0) if rest > 0: time.sleep(rest) # 固定周期,微分项才有意义三点要解释清楚。第一,send_cmd下发的是增量角不是绝对角,下位机自己累加并限幅,这样上位机重启不会把云台打到奇怪的位置。第二,lost计数到 5 才复位 PID,是因为偶发丢帧很正常,一丢就复位会让控制断断续续。第三,time.sleep补齐周期,PID 的微分项按固定DT算才稳定,靠“处理完就发”会让微分项随负载抖动。
顺带说一句pid.reset()的实现:把积分项和上一帧误差都清掉,只清积分不清上一帧,下一帧的微分项会出现一个巨大的尖峰,这个坑很隐蔽。
class AxisPID: def __init__(self, kp, ki, kd, limit=300.0): self.kp, self.ki, self.kd = kp, ki, kd self.limit = limit self.integ = 0.0 self.prev = 0.0 def step(self, err, dt): self.integ += err * dt # 积分限幅:目标丢失期间误差恒为最大,不限幅会把输出顶到限位 self.integ = max(-self.limit, min(self.limit, self.integ)) d = (err - self.prev) / dt if dt > 0 else 0.0 self.prev = err return self.kp * err + self.ki * self.integ + self.kd * d def reset(self): self.integ = 0.0 self.prev = 0.0 # 必须一起清,否则下一帧微分项会打出尖峰4.5 “标定怎么做的,换一块屏幕怎么办”
标定的完整流程要能一口气讲完:摄像头固定、曝光锁定、屏幕铺网格、光斑依次停在 N 个交叉点、每点采 20 帧取中位数、最小二乘解二次多项式系数、存表、验证残差。采 20 帧取中位数是为了压随机噪声,单帧标定在噪声大的时候能把系数带偏。
验证残差这一步不能省。解完系数后拿几个没参与拟合的点试一下,残差超过 2 毫米就说明模型不够或者标定点分布不均,得加点或者换模型。残差表是答辩时最有说服力的东西,比说“我们精度很高”强得多。
换屏幕要重标,这个没什么可商量的,因为像素当量和平面度都变了。但可以提前做一件事:把标定参数存成单独的配置文件,换屏只改这一个文件,代码不动。现场时间紧的时候,这个设计能省十几分钟。
4.6 “上位机和下位机的协议怎么定”
协议的核心是“下位机永远不信任上位机”。帧头用来找同步,指令码用来区分动作,校验用来挡干扰,长度固定方便解析。
| 字节 | 含义 | 说明 |
|---|---|---|
| 0 | 帧头 0xAA | 连续两帧 0xAA 才认为同步 |
| 1 | 指令码 | 0x01 增量角,0x02 复位,0x03 急停 |
| 2~3 | 水平增量 ×10 | 单位 0.1 度,有符号,低字节在前 |
| 4~5 | 俯仰增量 ×10 | 同上 |
| 6 | 异或校验 | 字节 1~5 逐字节异或 |
乘 10 存整数是为了避开浮点解析,下位机是单片机的时候这条很实用。急停指令单独留一个码,任何时候收到就切断 PWM 输出,这个在调试阶段救过不少人——代码跑飞时云台会一直转,急停比拔电源体面。
解析端要有超时机制:超过 200 毫秒没收到有效帧就保持当前角度不动,别当成零增量继续累加。
答辩追问“为什么不直接发绝对角度”,答法是指出责任划分:上位机管“往哪走”,下位机管“能走多远”,限位保护放在下位机,上位机算错了也不会把机械结构撞坏。
5. 易错点:五个在测试现场最容易翻车的地方
5.1 像素 y 轴没取反,越调越远
错在哪:屏幕坐标向上为正,像素坐标向下为正,映射函数里少了一个负号,或者标定时两个坐标系方向没统一。
为什么错:这个错误的表现不是“完全不对”,而是“上下颠倒但左右正常”,看起来像 PID 的 P 值符号反了。很多人第一反应是改增益符号,改完上下对了左右又反了,来回折腾半小时。更坑的是,如果标定点采样时习惯性地只取水平方向,垂直方向的问题根本暴露不出来。
怎么答对:调试第一步就做单点验证——让光斑停在屏幕正中偏上,看程序输出的屏幕 y 是不是正的。这一步花不了一分钟,能省掉半小时的瞎调。标定采样时四个角都要取,别只取中间一行。
5.2 现场调了阈值,跑的还是旧参数
错在哪:阈值硬编码在源码里,现场改完忘了重新编译;或者配置文件有多个副本,程序读的是另一个。
为什么错:调试时改一处看一眼效果,改得多了就记不清哪份是最终版。测试现场时间紧,重新编译一次要等,很容易就着旧程序跑完。
怎么答对:阈值、标定表、PID 参数全部外置成一个配置文件,程序启动时打印一行“已加载配置,版本哈希 xxxx”,现场一眼能看出加载的是哪份。改完必须重启验证一次,不能只靠热加载——热加载失败时程序往往不报错,继续用旧值。
5.3 只有 P 没有 D,或者积分不限幅
错在哪:P 给大一点响应快,但过冲;积分用来消静差,但不限幅,目标丢失时积分迅速饱和。
为什么错:调 PID 的人容易盯着阶跃响应看超调,忽略了连续跟踪时的相位滞后。更隐蔽的是积分饱和——正常跟踪时表现良好,一旦光斑丢失几百毫秒,积分项累到限幅,目标回来时云台要先“退饱和”才能跟上,表现为一段明显的滞后。
怎么答对:先 P 后 D 再 I,加 I 的时候同时加限幅和积分分离(误差大时不上积分)。测试时专门做一次“遮挡 500 毫秒再放开”的实验,看恢复过程有没有滞后,这一步能提前发现饱和问题。
5.4 把处理帧率当成了系统延迟
错在哪:程序里打印出 60 fps,就认为延迟是 16 毫秒。
为什么错:帧率描述的是吞吐,延迟描述的是从光子进镜头到云台动起来的时间,两者差着曝光、传输、处理和机械响应四大块。60 fps 的系统完全可能有 200 毫秒延迟。
怎么答对:用阶跃实验单独测延迟,别用帧率推算。测出来之后把各段延迟列成表,答辩时直接给数字,比说“我们帧率很高”有说服力。真要压延迟,优先动执行机构——换响应更快的舵机,比优化识别算法收益大得多。
5.5 只在屏幕中心标定,边缘全靠猜
错在哪:标定时光斑一直在中心附近转,二次多项式的系数在边缘区域基本是外推出来的。
为什么错:镜头畸变和透视变形都是越靠边越明显,中心残差 0.5 毫米,边缘可能到 5 毫米。而画正方形的四个顶点恰恰都在靠边的位置,误差全落在最要命的地方。
怎么答对:标定点必须铺满整个有效区域,四角各取一点,边缘中点各取一点,中心再补几点。解完系数后专门看四个角的残差,超过 2 毫米就得加三次项或者改善摄像头姿态——把摄像头尽量正对屏幕中心,能从根本上减小透视变形。
6. 上场前的那张自检表:把四层链路压成三句话
答辩和现场调试是两件事,但用的是同一套东西。口述部分建议压成三句话:第一句给结构——“视觉链路分采图、分割、解算、执行四层”;第二句给数字——“640×480、60 fps、端到端延迟约 180 毫秒、屏幕中心定位误差 1 毫米以内”;第三句给坑——“我们踩过的最大的坑是像素 y 轴方向,后来固定用单点验证来卡”。三句话说完,后面所有追问都只是在这三句上展开,不会散。
现场调试则要有一张能照着走的表。这张表不是流程文档,是“每项合格线是多少、现场怎么验、不过怎么办”的三列清单:
| 检查项 | 合格线 | 现场怎么验 | 不过怎么办 |
|---|---|---|---|
| 坐标方向 | 光斑上移输出 y 增大 | 手动挡光斑到中心偏上 | 检查映射符号或标定方向 |
| 曝光锁定 | 光斑面积波动小于 20% | 开关顶灯看面积变化 | 关自动曝光、降增益 |
| 标定残差 | 四角残差小于 2 mm | 跑一遍验证点 | 补点或改摄像头姿态 |
| 死区阈值 | 小于执行机构分辨率折算值 | 静置看云台是否自激 | 加大死区或降增益 |
| 丢失保护 | 遮挡 500 ms 后无过冲 | 手挡一下再放开 | 清积分、降增益 |
| 串口容错 | 校验错帧被丢弃 | 故意发一帧坏数据 | 检查帧头同步逻辑 |
调试节奏上还有一个押题思路值得说:把时间切成“过基本要求”和“冲发挥部分”两段,前一段只碰坐标、标定、协议这三件事,后一段才动识别精度和控制参数。很多人反过来,前几天一直在调形态学核大小,最后一天才发现正方形闭不上,那时候已经没时间重标了。
从那以后我每次上场前,都会强制把这张表从头走一遍,哪怕时间再紧也不跳步——不是因为谨慎,是因为跳过的那一项总会在测试的那五分钟里准时出现。希望帮到你。
这套 E 题视觉部分的代码就是按上面四层拆开组织的,阈值、标定表和串口协议各自独立,换屏幕或者换场地只需要改配置不动逻辑,拿去对着自己的硬件改一遍,比从零搭要快得多。
本文还有配套的精品资源,点击获取