2026年湖北电赛H题省奖方案:钢珠平衡小车的设计与视觉识别
2026/9/19 3:22:37 网站建设 项目流程

前言

本文主要介绍我们参加2026年全国大学生电子设计竞赛H题时的设计思路,包括赛题分析、系统总体方案、小车底盘、机械结构,以及钢珠视觉识别程序的具体实现。

这道题表面上看是一个钢珠平衡问题,但真正开始调试以后就会发现,最终效果并不只取决于平衡控制算法。

小车启动和制动是否平稳、摆杆有没有机械间隙、舵机连杆是否存在空程、摄像头曝光是否稳定,都会直接影响钢珠的位置控制精度。

因此,我们的整体思路并不是一上来就堆复杂算法,而是先让底盘、机械结构和视觉测量尽量稳定,再利用视觉反馈和闭环控制进一步提高系统精度。

图1 钢珠平衡小车整体实物图


一、赛题任务与核心难点

本题是在智能小车运动控制的基础上,增加钢珠位置识别与动态平衡任务。

系统不仅需要控制小车完成规定路线,还需要实时检测凹槽内钢珠的位置,并通过摆杆机构让钢珠移动到指定区域。

实际调试时,我们把整个问题分成了四个部分。

1.1 小车运行平稳性

小车启动、制动和转向时,如果左右轮速度变化过快,车身就会产生明显的加速度和振动,钢珠也会随之发生较大位移。

因此,小车控制不能只追求响应速度,还需要限制加速度和转向修正量,使整个运行过程尽量平滑。

1.2 机械结构一致性

摆杆、合页和舵机连杆需要尽量减小间隙,并保证左右两个方向的运动阻力基本一致。

如果一侧阻力大、另一侧阻力小,控制器就很难使用同一组参数获得对称的响应效果。

1.3 动态视觉识别

视觉系统不仅要在静止状态下识别钢珠,还要适应以下情况:

  • 车体振动;

  • 摆杆倾斜;

  • 钢珠快速运动;

  • 钢珠表面反光;

  • 现场光照变化;

  • 画面中的支架、端件和阴影干扰。

视觉部分很容易出现一个误区:静止时能够框到钢珠,就认为识别程序已经调好了。

真正运行起来以后,钢珠快速运动会产生拖影和反光;摆杆倾斜后,固定识别区域可能偏离凹槽;小车振动还会让摄像头画面产生轻微位移。

1.4 视觉数据稳定性

视觉程序不能把某一帧的误识别结果直接交给后续控制程序。

除了正确识别钢珠,还需要通过物理范围限制、上一帧位置约束和目标丢失判断,减少坐标跳变。

因此,我们最终采用的不是简单的“在整幅图中找圆”,而是一套结合摆杆结构、钢珠尺寸和运动范围设计的动态识别方案。


二、系统总体设计

2.1 系统总体框架

整个系统主要由以下四个部分组成:

  1. 小车运动控制模块;

  2. 摆杆机械执行模块;

  3. 钢珠视觉识别模块;

  4. 任务控制与数据通信模块。

Linux开发板通过摄像头获取摆杆和钢珠图像,运行C++和OpenCV视觉程序,完成图像解码、ROI裁剪、摆杆中心线跟踪、钢珠位置提取和像素坐标标定。

MSPM0G3519负责读取循迹传感器和编码器数据,同时完成小车运动控制及舵机控制。

整个系统的数据关系可以概括为:

摄像头采集图像 ↓ Linux开发板运行视觉程序 ↓ 计算钢珠实际位置 ↓ 任务模块或主控读取位置结果 ↓ 控制舵机调整摆杆角度

图2 系统总体结构框图

2.2 小车底盘方案

常见的小车底盘主要包括三轮差速结构和四轮结构。

四轮底盘可以采用差速转向或舵机转向,而三轮底盘通常使用左右驱动轮完成差速转向,再通过万向轮提供支撑。

综合考虑转向灵活性、机械复杂度和低速运行稳定性,我们最终选择了三轮差速底盘。

三轮结构的转向阻力相对较小,对电机扭矩的要求不高,也比较适合本题低速、平稳运行的需求。

小车运动控制采用速度闭环和转向闭环相结合的方式。编码器负责反馈左右轮的实际速度,循迹传感器负责计算小车相对于赛道中心线的位置偏差。

左右电机的控制量可以表示为:

$$
PWM_L=PWM_{speed}+PWM_{turn}
$$

$$
PWM_R=PWM_{speed}-PWM_{turn}
$$

其中:

  • $PWM_{speed}$ 为速度环输出;

  • $PWM_{turn}$ 为转向修正量;

  • $PWM_L$ 和 $PWM_R$ 分别为左右电机的最终控制量。

通过两部分叠加,可以在维持运行速度基本稳定的同时完成方向调整。

这里需要注意,本题中的小车控制不能只追求“转得快”或者“响应灵敏”。小车动作越平滑,钢珠平衡部分的控制压力就越小。

2.3 机械结构方案

机械结构对钢珠平衡效果的影响非常明显。

如果摆杆存在较大间隙、转轴摩擦不均匀或者整体刚度不足,即使后续使用比较复杂的PID控制,也很难得到稳定效果。

根据题目要求,摆杆一端需要使用合页、链条或类似结构连接,另一端可以使用舵机或步进电机驱动。

考虑到步进电机的体积和重量相对较大,而舵机的速度、扭矩和控制精度已经能够满足需求,因此我们最终采用了“自制合页+舵机连杆”的机械结构。

机械部分调试时,建议优先检查下面几个问题:

  1. 合页或转轴能否顺畅转动;

  2. 摆杆向左右两侧运动时,阻力是否基本一致;

  3. 舵机摇臂和连杆之间是否存在明显空程;

  4. 连杆与摆杆的连接点是否会松动;

  5. 舵机动作时,摄像头支架是否会随之振动;

  6. 摆杆运动到极限位置时,是否仍处于摄像头画面内。

这些问题如果没有处理好,摄像头画面和摆杆中心位置都会发生变化。此时无论怎样修改视觉阈值,都很难获得真正稳定的识别结果。

图3 摆杆与舵机连杆机械结构

2.4 主要硬件组成

本系统使用的主要硬件如下:

硬件主要作用
MSPM0G3519主控板小车运动控制、编码器采集和舵机控制
Linux视觉开发板运行C++与OpenCV视觉程序
USB或CSI摄像头采集摆杆及钢珠画面
直流减速电机驱动小车运动
编码器测量左右轮实际速度
电机驱动模块驱动左右电机
循迹或灰度传感器检测赛道位置偏差
舵机调整摆杆角度
自制合页与连杆构成摆杆执行机构
电池与电源模块为主控、开发板和执行机构供电

主控最终选择MSPM0G3519,主要是因为它的片上资源可以满足双电机编码器采集、电机PWM控制、循迹传感器读取、舵机控制和通信等需求。

结合实际接口分配情况,MSPM0G3519比MSPM0G3507更加适合完成双编码器闭环控制。


三、视觉识别方案

3.1 方案选择

钢珠识别常见的方案主要有YOLO目标检测和OpenCV传统图像处理两种。

YOLO具有较强的环境适应能力,但需要提前采集、标注和训练数据,对视觉处理平台的算力也有一定要求。

本题中的钢珠始终位于一条已知摆杆附近,钢珠尺寸和活动范围相对固定。因此,我们最终选择了更加轻量的OpenCV灰度图像处理方案。

这套方案的核心思路可以概括为一句话:

先找到摆杆,再沿着摆杆寻找钢珠。

这样做主要有两个优势:

  1. 可以大幅缩小搜索范围,减少底盘、舵机、支架和背景对识别结果的影响;

  2. 当摆杆发生倾斜时,钢珠检测区域可以跟随摆杆移动,不需要依赖完全固定的矩形ROI。

3.2 视觉程序整体流程

这套视觉工程使用C++和OpenCV实现,与视觉识别直接相关的文件主要包括:

文件主要作用
vision.cpp / vision.h摄像头初始化、MJPEG采集、灰度解码、ROI裁剪和曝光调整
ball_detect.cpp / ball_detect.h摆杆中心线跟踪、钢珠特征提取、目标筛选和位置标定
image_server.cpp / image_server.h发送调试图像和识别元数据
intrrupt.cpp启动视觉线程、注册回调和刷新调试画面

视觉程序的主要流程如下:

摄像头采集MJPEG图像 ↓ 解码为灰度图 ↓ 裁剪摆杆活动区域 ↓ 跟踪摆杆中心线 ↓ 沿中心线建立动态ROI ↓ 计算局部背景亮度 ↓ 提取钢珠暗度特征 ↓ 使用球径匹配滤波 ↓ 筛选候选目标 ↓ 计算钢珠亚像素中心 ↓ 转换为实际厘米位置

为了避免文章被大量工程代码占满,下面只保留几段最能体现视觉思路的核心代码。边界检查、日志输出和调试统计等辅助部分不再全部展示。


四、视觉核心代码讲解

4.1 图像采集与ROI裁剪

摄像头以MJPEG格式输出图像。

程序获得JPEG数据以后,直接使用OpenCV将其解码为单通道灰度图。后续算法主要利用摆杆和钢珠之间的亮度差异,因此没有必要一直保留彩色图像。

核心代码如下:

const uint8_t *jpg = nullptr; size_t jlen = 0; uint64_t ts = 0; if (!cam.grab(&jpg, &jlen, &ts, 500)) { return; } cv::Mat raw( 1, (int)jlen, CV_8UC1, const_cast<uint8_t *>(jpg) ); detect_gray = cv::imdecode( raw, DETECT_IMREAD_FLAG ); int cy0 = DETECT_CROP_TOP / DETECT_REDUCE; int cy1 = detect_gray.rows - DETECT_CROP_BOTTOM / DETECT_REDUCE; int cx0 = DETECT_CROP_LEFT / DETECT_REDUCE; int cx1 = detect_gray.cols - DETECT_CROP_RIGHT / DETECT_REDUCE; cv::Mat strip = detect_gray( cv::Rect( cx0, cy0, cx1 - cx0, cy1 - cy0 ) );

这里真正需要注意的不是cv::imdecode(),而是后面的ROI裁剪。

摄像头原始画面中通常会包含底盘、桌面、支架、铰链座和舵机连杆。这些物体有时比钢珠更黑,如果直接处理整幅图像,很容易形成错误目标。

因此,我们只保留摆杆可能出现的区域。

裁剪以后,一方面可以减少背景干扰,另一方面也能降低后续逐像素扫描的运算量。

图4 摄像头采集到的摆杆与钢珠原始画面

除了裁剪范围,曝光时间也非常重要。

如果曝光时间太长,画面确实会变亮,但快速运动的钢珠会出现明显拖影;如果曝光时间太短,图像噪声又会增加。

所以调试曝光时,不要只看画面够不够亮,还需要观察运动中的钢珠有没有被拉长,以及钢珠和管体之间是否仍然具有稳定的灰度差异。

4.2 摆杆中心线跟踪

如果使用完全固定的ROI,摆杆一旦发生倾斜,检测区域就可能偏离凹槽。

因此,程序会先对摆杆位置进行跟踪。

具体做法是逐列扫描图像,在每一列中寻找最长的一段连续亮像素,再把这段亮区域的中心作为当前列的摆杆中心点。

核心代码如下:

const int thr = (cmax * BALL_RUN_KEEP_Q8) >> 8; int bestLen = 0; int bestStart = 0; int curLen = 0; int curStart = 0; for (int y = y0; y <= y1; ++y, p += stride) { if (*p >= thr) { if (curLen == 0) { curStart = y; } if (++curLen > bestLen) { bestLen = curLen; bestStart = curStart; } } else { curLen = 0; } } s_trkY[n] = (float)bestStart + 0.5f * (float)(bestLen - 1);

这里没有直接寻找“最亮的一个像素”,而是寻找“最长的一段亮像素”。

这是因为钢珠表面可能存在很亮的反光点。如果只寻找最大灰度值,钢珠上的高光就可能把摆杆中心位置带偏。

白色摆杆在每一列中通常会形成一段连续亮区域,因此使用最长亮段的中心会更加可靠。

获得多列中心点以后,程序使用二次曲线拟合摆杆中心线:

$$
y=At^2+Bt+C
$$

其中,$t$ 为归一化后的横坐标。

使用二次曲线而不是简单直线,主要是为了适应广角摄像头产生的桶形畸变。原本笔直的摆杆在画面中可能呈现出轻微弯曲,二次曲线能够更好地贴合实际图像。

拟合过程中还可以使用绝对偏差中位数,也就是MAD,剔除少量离群点,避免钢珠、支架和局部反光把整条中心线拉偏。

4.3 沿中心线建立动态ROI

得到摆杆中心线以后,程序不会继续处理整个裁剪区域,而是沿拟合曲线建立一条窄带,作为钢珠检测区域。

int roiH = (int)lroundf( s_band * BALL_ROI_FILL ) | 1; roiH = clampi( roiH, 3, BD_MAX_H );

检测区域的高度根据摆杆亮带的实际厚度动态计算。

ROI并不是越大越好。

如果ROI太高,管道外沿、黑色底盘和刻度线都会被包含进来;如果ROI太低,又可能只能看到钢珠的一部分。

比较合适的范围是覆盖凹槽内部,同时尽量避开水管外沿和其他机械结构。

由于ROI沿着拟合后的中心线建立,所以无论摆杆处于水平状态还是倾斜状态,检测区域都可以跟着摆杆移动。

4.4 使用局部中位数提取钢珠特征

钢珠通常比白色凹槽更暗,但不能简单使用一个固定灰度阈值。

例如,在较亮环境中,白色管体灰度可能接近180,钢珠灰度可能为60;换到较暗环境后,管体可能下降到100,钢珠也可能下降到40。

如果一直使用固定阈值,阴影和管壁都有可能被错误识别成钢珠。

因此,程序会计算当前位置附近的局部中位数,并将它作为背景亮度参考。

钢珠的暗度特征可以表示为:

$$
f(x,k)=\max\left(0,\alpha m(x,k)-I(x,k)\right)
$$

其中:

  • $m(x,k)$ 表示当前位置附近的局部中位数;

  • $I(x,k)$ 表示当前像素灰度;

  • $\alpha$ 表示背景亮度比例参数。

对应的核心代码如下:

const int32_t d = ((med * BALL_DARK_Q8) >> 8) - (int32_t)s_row[i]; if (d > 0) { ++s_cnt[i]; s_feat[i] += (float)d; }

这里使用中位数而不是均值,是一个比较关键的细节。

当窗口经过阴影边缘时,均值很容易落在明暗区域之间,导致阴影附近的一大片像素都被判断为暗目标。

中位数通常更接近明区或暗区中的一侧,不容易在阴影边缘形成假的钢珠特征。

另外,钢珠只占局部窗口中的一小部分,对中位数的影响相对较小。如果使用均值,钢珠自身反而可能把背景参考亮度拉低。

为了提高实时性,程序使用直方图维护滑动中位数。窗口每向右移动一列,只需要加入一个像素、移出一个像素,不需要重新对整个窗口排序。

4.5 球径匹配与候选目标筛选

钢珠表面经常会出现高光。

因此,钢珠在灰度图中的暗区不一定是一个完整的实心圆,有时会被中间的高光分成左右两部分。

如果程序只寻找最暗的一列,检测坐标就容易在钢珠左右两侧来回跳动。

我们的处理方法是使用一个接近钢珠直径的窗口,将窗口内的暗度特征累加起来。

匹配得分可以表示为:

$$
S(i)=
\sum_{j=i-D/2}^{i+D/2}
\sum_k f(j,k)
$$

其中,$D$ 表示钢珠直径对应的像素宽度。

这个操作可以理解成一个一维匹配滤波。

孤立噪点在球径窗口内无法积累出足够高的得分,而被高光分开的两部分暗区仍然会落在同一个窗口中,因此钢珠整体仍然能够形成比较明显的峰值。

程序还会结合以下条件筛选候选目标:

  1. 候选位置不能超出摆杆的实际量程;

  2. 暗像素填充率不能太低;

  3. 填充率不能接近整片全黑;

  4. 目标宽度需要与钢珠尺寸基本一致;

  5. 目标与周围背景之间需要具有足够的亮度差异;

  6. 连续跟踪时,优先检查上一帧位置附近的候选目标;

  7. 偏离上一帧过远的目标不能立即替代原目标。

这里还有一个比较实用的处理方式。

如果当前得分最高的目标不满足条件,程序只淘汰当前候选,然后继续检查其他目标,而不是直接把整帧判断为识别失败。

这样可以避免铰链、舵机端件或者阴影获得最高得分以后,让真正的钢珠失去被检查的机会。

图5 钢珠视觉识别与位置计算结果

4.6 亚像素定位与厘米标定

找到最佳候选区域以后,程序不会直接使用整数峰值位置,而是在峰值附近计算暗度加权质心。

核心代码如下:

const int lo = std::max(0, iPeak - D); const int hi = std::min(nAll - 1, iPeak + D); double sw = 0.0; double swx = 0.0; for (int i = lo; i <= hi; ++i) { sw += s_feat[i]; swx += (double)s_feat[i] * i; } const float xBall = (float)x0 + (float)(swx / sw);

这里将质心计算限制在匹配峰值附近,而不是对整个ROI计算全局质心。

如果使用全局质心,摆杆另一端只要出现一个较暗的杂点,就可能把最终坐标拉偏。

限制计算范围以后,既可以获得浮点数形式的亚像素坐标,也能减少远处干扰。

得到钢珠像素坐标后,还需要将其转换成摆杆上的实际厘米位置。

程序首先将像素坐标归一化:

$$
u=\frac{x}{W}-u_0
$$

然后使用二次多项式完成标定:

$$
p=C_2u^2+C_1u+C_0
$$

对应代码如下:

static inline float uToCm(float u) { return BALL_PX2CM_C2 * u * u + BALL_PX2CM_C1 * u + BALL_PX2CM_C0; }

这里保留二次项,是因为摄像头可能存在广角畸变和斜视问题。

画面左侧的1 cm和画面右侧的1 cm,不一定对应完全相同的像素数量。使用多点标定和二次拟合,可以减小这种非线性误差。

标定时可以按照下面的方法进行:

  1. 将钢珠依次放在摆杆上的多个已知刻度位置;

  2. 记录每个位置对应的像素坐标;

  3. 对像素坐标进行归一化;

  4. 使用最小二乘法拟合二次多项式;

  5. 使用没有参与拟合的位置检查实际误差。

需要特别注意,ROI裁剪范围和标定参数是相互关联的。

只要修改了左右裁剪范围、图像缩放比例或者摄像头安装位置,就应该重新检查像素—厘米标定参数。

否则可能出现一种比较隐蔽的问题:检测框看起来始终套在钢珠上,但输出的厘米位置存在整体偏移或比例误差。

4.7 时序约束与识别失败处理

动态画面中偶尔漏掉一帧并不可怕,真正危险的是把远处的伪影突然当成钢珠。

因此,程序会保存上一帧的钢珠位置。

正常跟踪时,程序优先在上一帧位置附近寻找候选目标;如果连续几帧没有找到可信目标,再逐渐扩大搜索范围。

连续丢失达到设定次数以后,程序会清除旧轨迹并重新进行全局搜索。

这种方式既可以减少坐标突然跳变,也能避免钢珠真正移动到其他位置以后,程序一直被旧坐标锁住。

这里还需要明确区分两个状态:

  • 误差为0:表示钢珠已经到达目标位置;

  • 识别失败:表示当前没有可靠的视觉测量结果。

两者在后续程序中不能采用相同的处理方式。

识别失败时,更合理的做法是设置独立的无效标志,并采用短时保持、安全回中或者暂时停止更新控制量等策略。


五、系统调试与日志分析

5.1 推荐调试顺序

视觉系统建议按照下面的顺序逐步调试:

  1. 固定摄像头和机械结构;

  2. 确认摆杆在极限角度时仍然位于裁剪区域内;

  3. 检查摆杆中心线能否稳定跟踪;

  4. 观察动态ROI是否能够跟随摆杆移动;

  5. 固定钢珠,调整局部暗度比例和候选门限;

  6. 排除端件、阴影和刻度线产生的假目标;

  7. 缓慢移动钢珠,检查输出坐标是否连续;

  8. 完成像素—厘米多点标定;

  9. 使用直尺复查没有参与拟合的位置;

  10. 让钢珠快速运动,调整曝光时间和识别门限;

  11. 最后完成视觉、舵机和小车运动的联合调试。

调试时不要只看检测框有没有套住钢珠,还应该同时记录以下数据:

  • ROI平均亮度;

  • 管体参考亮度;

  • 钢珠匹配峰值;

  • 暗像素填充率;

  • 连续丢失帧数;

  • 单帧解码时间;

  • 单帧扫描时间;

  • 实际识别帧率。

只有知道某一帧为什么识别成功、为什么识别失败,参数调整才有依据。

5.2 日志曲线分析

为了观察钢珠在实际运行过程中的位置变化,我们记录了目标位置、钢珠实测位置、运动速度、PWM输出以及不同运行阶段的状态。

图6 调参阶段目标位置与钢珠实测位置对比

通过这张曲线,可以重点观察以下几个问题:

  1. 实测位置能否跟随目标位置变化;

  2. 钢珠到达目标附近时是否存在明显超调;

  3. 系统是否发生持续振荡;

  4. 最终位置能否进入允许误差范围;

  5. 不同运动阶段的响应速度是否一致。

需要说明的是,这组日志属于调参阶段记录,不能直接作为最终达标结果。

在该次测试结束时,目标位置约为 $-5.0\text{ cm}$,钢珠实测位置约为 $-3.45\text{ cm}$,仍然存在约 $1.55\text{ cm}$ 的误差,日志最终事件也标记为verification_fail

不过,这张曲线依然具有较大的分析价值。

它说明视觉测量能够连续反映钢珠的位置变化,但最终控制效果还会受到舵机响应、机械摩擦、连杆间隙以及控制参数的影响。

换句话说,视觉曲线连续只能说明测量链路基本正常,并不代表整个系统已经完成调试。

5.3 常见问题总结

1. 修改裁剪范围后没有重新标定

这种情况下,检测框可能仍然能够准确跟随钢珠,但厘米坐标会出现整体偏移或比例误差。

2. 曝光时间只看画面亮度,不看运动拖影

画面虽然更亮,但快速运动的钢珠会被拉长,匹配滤波得分反而可能下降。

3. 使用完全固定的灰度阈值

现场光照发生变化后,管体和钢珠的灰度会一起变化,固定阈值很容易失效。

4. 只保留全局最强峰

铰链、舵机端件或阴影可能比钢珠更黑。正确做法是先判断候选目标是否符合钢珠特征,再比较候选得分。

5. 把识别失败当成误差为0

这会让系统误以为钢珠已经到达目标位置。识别失败必须保留独立的无效状态。

6. 只调整控制参数,不检查机械结构

如果摆杆摩擦不均匀或者连杆存在明显空程,控制器就需要不断补偿这些机械误差,最终效果很难稳定。


六、总结

本系统采用三轮差速底盘,以MSPM0G3519作为运动控制核心,机械部分采用合页、摆杆和舵机构成钢珠位置调整机构。

视觉程序运行在Linux开发板上,通过OpenCV完成图像采集、摆杆跟踪、钢珠检测和位置标定。

这套视觉算法最核心的地方,并不是使用了某一个特别复杂的图像处理算子,而是充分利用了题目本身的结构特点:

  • 钢珠始终位于摆杆凹槽附近;

  • 摆杆主体通常比周围背景更亮;

  • 钢珠又比局部管体更暗;

  • 钢珠直径和运动范围是已知的;

  • 连续两帧之间的位置变化存在物理限制。

程序首先通过最长亮段和二次曲线拟合跟踪摆杆,然后沿摆杆中心线建立动态ROI。

接着利用局部中位数抵消光照变化,通过球径匹配滤波累积钢珠暗度特征,并结合物理量程、目标形态和时序约束排除干扰。

最后通过亚像素质心计算钢珠中心坐标,再利用二次多项式将像素位置转换为实际厘米位置。

相比固定阈值或者直接在整幅图中找圆,这种方法更加适合装车后的动态环境。

整个项目调试下来,我们最大的体会是:视觉、机械和控制并不是三个互相独立的部分。

视觉测量稳定以后,控制器才有可靠输入;机械结构足够顺畅以后,控制器才不需要不断补偿摩擦和间隙;小车运行足够平稳以后,钢珠平衡的难度也会明显降低。

只有将底盘、机械结构、视觉识别和控制系统放在一起联合调试,才能真正提高钢珠的位置控制精度和整个系统的环境适应能力。

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

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

立即咨询