简介:面向智能行李箱设计需求,这份资料以UWB超宽带定位与OpenMV视觉识别为核心,提供了一套可借鉴的完整技术方案。文档深入拆解了系统架构:从UWB脉冲测距与多边形定位算法、OpenMV对用户衣物或面部特征的辅助识别,到PID/模糊控制策略制定、超声波/红外避障、电机驱动与轮系结构,再到电源续航、蓝牙/WiFi交互及安全加密设计均有涉及。对于物联网、嵌入式或智能硬件方向的开发者,以及毕业设计、电子设计竞赛团队,都能从中获得系统级的工程思路。压缩包内仅1个docx文档,大小82KB,内容紧凑但覆盖全面。目前已有406人学习浏览。借助该文档可快速理清定位、识别、控制与硬件集成的技术脉络,还可直接参考其中的传感器选型、控制算法流程和安全设计方案,为后续原型搭建或论文撰写提供有效支撑。
1. 机场值机厅里,视觉和测距谁更可靠
智能跟随行李箱最尴尬的一幕是:在阳光直射的地砖上,摄像头把旁边乘客的阴影当成了主人;进了安检通道,金属立柱又把UWB信号反射得满屏跳变。摄像头在强光、逆光和低纹理场景下会失明,UWB在金属遮挡和多径环境里会漂移,两者单独用都没有宣传片里那么从容。这个标题给的解法是双链路冗余:OpenMV负责“认出目标、给出方位角”,UWB负责“测出距离、校验接近速度”,主控根据两边数据质量自动切换跟随策略。对要落地这个方案的工程师来说,核心不是堆硬件,而是定好传感器之间的信任边界:谁在什么条件下说了算,信号丢失时怎么安全降级。
2. UWB定位链路:从测距原理到主控数据清洗
2.1 为什么选UWB而不是蓝牙RSSI或激光雷达
UWB(超宽带)定位的核心不是“精度高”三个字,而是它的测距方式不依赖信号强度。蓝牙RSSI把接收信号强度换算成距离,遇到人体吸收、多径反射就剧烈抖动,在行李箱这种低高度、贴地运行的环境里误差能到一米以上。UWB用的是飞行时间测距,典型实现是双边双向测距(DS-TWR):标签发一个测距请求,基站回一个响应,再交换一轮时间戳,把四次收发的时间差相减得到信号飞行时间,乘以光速除以2就是距离。这个过程不读RSSI,所以对抗多径和衰减的能力强很多。
DWM1000模块是这类设计里最常见的选型,支持6.5GHz和4GHz两个频段,室内测距精度标称±10cm。对行李箱场景,我一般用单基站+单标签的极简拓扑:人身上带标签,箱体里装基站,只测直线距离。两个基站做TDoA三角定位当然能出二维坐标,但行李箱跟随不需要全局坐标,它只需要“人离我多远、在哪个方向”——方向交给OpenMV,UWB专心输出距离。这个分工能省一个基站,还避免了TDoA同步时钟的额外校准工作。
2.2 先滤掉野值,再谈卡尔曼
DWM1000的原始测距输出不能直接喂给PID。行李箱移动时标签会摆动、人体会遮挡天线,偶尔跳出一个偏离真实值半米的野值。我习惯在UART解析之后先做两级滤波:第一级是限幅——如果当前值与上一次有效值之差超过0.5m,直接丢弃;第二级是滑动窗口取中值——窗口大小取5,对排序后的中间三个值求平均。这样处理后的数据已经能用了,但跟踪响应还是有约一个窗口的滞后。
#define UWB_WINDOW 5 float uwb_median_filter(float new_val) { static float buf[UWB_WINDOW]; static uint8_t idx = 0; float tmp[UWB_WINDOW]; buf[idx] = new_val; idx = (idx + 1) % UWB_WINDOW; memcpy(tmp, buf, sizeof(buf)); // 简单的插入排序,窗口小,不需要快排 for (int i = 1; i < UWB_WINDOW; i++) { float key = tmp[i]; int j = i - 1; while (j >= 0 && tmp[j] > key) { tmp[j + 1] = tmp[j]; j--; } tmp[j + 1] = key; } // 取中值附近三个点平均 return (tmp[2] + tmp[1] + tmp[3]) / 3.0f; }这段代码做了两件事:用一个环形缓冲区滚动保存最近5个原始测距值,每次新值进来后对缓冲区拷贝做一次插入排序,返回中间三个数的均值。限幅判断应该在调用这个函数之前做——解析串口时发现当前值比上次有效值大0.5m以上,直接return,不进入缓冲区。这样既滤掉了脉冲噪声,又不会让正常快速靠近的真实变化被中值滤波吞掉太多。
2.3 UWB数据帧解析和异常判定
UWB模块通过UART输出给主控时,不同厂商的帧格式差异很大,但多数遵循类NMEA的ASCII帧。我在STM32侧的做法是按$开头、*结尾来切帧,中间用逗号分隔字段:模块ID、距离值、信号质量、校验位。信号质量字段必须解析出来——当SNR低于某个阈值时,即使距离值看起来合理,也应该把这条数据标记为“不可信”,而不是直接参与滤波,否则金属门、墙角这些场景会把错误数据带进均值窗口。
提示:UWB模块的安装位置决定了多径干扰的上限。不要把它埋在行李箱金属拉杆正下方,也不要贴着脚轮电机。常见做法是放在箱体顶部靠后方,天线朝上偏外,让标签和基站之间的视距路径尽量不被行李箱自己的金属结构切断。
3. OpenMV识别链路:目标检测、方位角估计与串口协议
3.1 OpenMV擅长什么、不擅长什么
OpenMV的定位是“嵌入式机器视觉”,它跑的是MicroPython,芯片算力比树莓派低一个量级。很多人拿它做人脸识别、巡线,这些经典任务没问题;但指望它做实时行人重识别或者动作语义理解,方向就错了。在这个项目里,它的任务是两层:识别出“跟随目标这个人”,并估计目标在画面中的水平方位角。
正面朝向时,我常用OpenMV内置的Haar级联人脸检测器——它对正脸检测很稳,且输出直接是矩形框。背面朝向时人脸检测失效,常见的替代做法是提取目标上半身的颜色直方图,配合灰度特征点做模板匹配。实际工程里更可靠的是在行李箱上装一个低功耗蓝牙信标做粗同步:OpenMV先用颜色框住候选目标,再用蓝牙信号强度做二次确认,确认通过才开始跟随。这个策略把视觉从“辨认身份”降级为“锁定位置”,可靠性高很多。
3.2 把像素偏移换算成跟随角度
OpenMV识别到目标后,输出的是目标框中心点的像素x坐标。跟随控制需要的是方位角,所以要把像素坐标映射成角度:
import sensor, image, time from pyb import UART sensor.reset() sensor.set_pixformat(sensor.RGB565) sensor.set_framesize(sensor.QVGA) sensor.skip_frames(time=2000) # 固定曝光,避免强光下目标框抖动 sensor.set_auto_exposure(False, exposure_us=20000) sensor.set_auto_whitebal(False) H_FOV_DEG = 70.0 # 根据镜头实际视场角标定 IMG_WIDTH = 320 uart = UART(3, 115200, timeout_char=100) while True: img = sensor.snapshot() # 优先尝试人脸检测,失败后回退到色块追踪 face = img.find_features(image.HaarCascade("frontalface_default")) target_x = None if face: # 取面积最大的框 face.sort(key=lambda r: r[2] * r[3], reverse=True) x, y, w, h = face[0] target_x = x + w // 2 else: blob = img.find_blobs([(30, 60, 20, 60, 10, 40)], pixels_threshold=200) if blob: blob.sort(key=lambda b: b.pixels(), reverse=True) target_x = blob[0].cx() if target_x is not None: angle = (target_x - IMG_WIDTH / 2.0) / IMG_WIDTH * H_FOV_DEG # 帧格式:帧头0xA5,方位角(有符号整数,单位0.1度),校验字节 angle_int = int(angle * 10) payload = bytes([0xA5, (angle_int >> 8) & 0xFF, angle_int & 0xFF]) crc = 0 for b in payload: crc ^= b uart.write(payload + bytes([crc]))这段代码的关键逻辑有三处:一是把曝光和白平衡固定住,否则摄像头在逆光和顺光切换时会自动调整参数,导致颜色直方图匹配瞬间失效,目标框跟着乱跳;二是人脸检测失败后的降级路径不是盲目报“丢失”,而是切到色块追踪;三是方位角取的是目标框中心相对画面中心的偏移量,这个量对跟随控制足够干净。固定曝光也有代价——暗光环境画面会偏暗,所以色块阈值的选取要在目标穿着的实际光照下采样。
3.3 OpenMV与主控的UART通信边界
OpenMV和STM32之间走UART,波特率一般选115200,这是两边都稳的速率。通信周期要匹配视觉帧率:OpenMV在QVGA分辨率下人脸检测大约能跑10~15帧每秒,所以主控端不要试图用太高的频率读串口,否则拿到的多半是重复帧。我习惯在帧尾加一个单字节异或校验——视觉数据出错时宁可丢一帧,也不要让一个错误的角度值送进PID。主控侧解析时先验证帧头,再校验CRC,然后判断角度绝对值是否超过60度。超过就只旋转不前进,防止行李箱在目标还没有正面朝向它时就莽撞地冲过去。
4. 双链路融合策略:信任切换和跟随状态机
4.1 互补关系不是叠加,是分工
融合不是把UWB距离和视觉角度简单加权平均,因为两者的误差特性完全不同。视觉角度在目标出现在画面中心时最准确,一旦目标靠近画面边缘,镜头畸变和检测框偏移会让误差迅速放大;UWB距离则相反,它在近距离和远距离都稳定,但完全给不出方向信息。所以正确的融合方式是:UWB距离作为纵向控制的输入,视觉方位角作为横向控制的输入,两条链路独立作用于两个控制通道。
这个分工还能避免一个常见陷阱——如果把视觉角度直接加进UWB坐标解算,摄像头抖动产生的噪声会被当成真实位移进入定位解算,最后输出一个剧烈抖动的二维坐标。我的做法是各算各的:距离通道完全由UWB负责,角度通道完全由OpenMV负责,融合只发生在状态判断层,不发生在数据层。
4.2 状态机的四个状态和切换条件
跟随逻辑用状态机实现,比用连续控制更安全,因为行李箱在跟丢、靠近、避让这些场景下需要的是明确的动作变更,而不是同一套PID参数硬撑。下表是状态定义:
| 状态 | 进入条件 | 动作 | 退出条件 |
|---|---|---|---|
| SEARCH 搜索 | 视觉丢失且UWB距离>2.5m | 原地左右旋转扫描 | 视觉重新锁定目标 |
| TRACK 跟随 | 视觉锁定且0.4m<距离<2.0m | 差速跟随,PID控制 | 距离进入急停区或视觉丢失 |
| STOP 急停 | 距离<0.35m | 停止前进,原地等待 | 距离回升到0.4m以上 |
| CHASE 追近 | 视觉锁定且距离>2.0m | 加速前进,但限制最大速度 | 距离回到2.0m以内 |
超过5秒没有进入TRACK,就降级为STOP而不是继续乱转,避免在人群里反复画圈。这个超时值要看机场、展厅这类场景的通道宽度调整,通道越窄时间越短。
typedef enum { SEARCH, TRACK, STOP, CHASE } FollowState; FollowState follow_state_machine(float uwb_dist, bool visual_locked) { static FollowState state = SEARCH; static uint8_t search_timeout = 0; switch (state) { case TRACK: if (uwb_dist < 0.35f) state = STOP; else if (!visual_locked && uwb_dist > 2.5f) state = SEARCH; break; case SEARCH: if (visual_locked && uwb_dist < 2.0f) state = TRACK; else if (++search_timeout > 50) { state = STOP; search_timeout = 0; } break; case STOP: if (uwb_dist > 0.4f && visual_locked) state = TRACK; break; case CHASE: if (uwb_dist < 2.0f) state = TRACK; break; } return state; }这段状态机代码最值得注意的地方是状态优先级——急停条件永远优先于视觉丢失条件。实际运行中可能出现一种危险情形:目标快速靠近,OpenMV因为距离太近导致人脸超出画面上边缘而失锁,这时候如果先判定“视觉丢失”然后进入SEARCH,行李箱会旋转,而旋转可能撞到就在脚边的人。所以先判断距离,再判断视觉。
4.3 链路质量标记和降级策略
我在主控里维护两个质量标记:uwb_confident和vision_confident。UWB的质量来自信噪比和野值丢弃率,连续10个测距周期里丢弃率超过30%就置为不可信;视觉的质量来自检测框的连续性和面积变化率——前后两帧检测框中心跳变超过画面宽度20%时,认为检测不稳定。这两个标记决定状态机能否进入TRACK。
双链路同时丢失时的行为最考验设计:如果只有视觉丢失但UWB距离还在2m以内,我一般让行李箱减速到原速度的30%继续直行最多0.5秒,而不是立刻刹停或乱转——很多遮挡只是暂时的,比如目标走到一根立柱后面。如果UWB同时丢失,立即原地刹车并发出蜂鸣提示。这个逻辑避免了一个常见问题:摄像头一帧花屏就急停,在光滑地砖上容易造成行李箱自身侧滑摔倒。
5. 跟随算法工程化:死区、PID参数和急停冗余
5.1 控制周期的选择与数据对齐
控制周期是跟随手感的分水岭。我习惯把主控的控制周期定在50ms,也就是20Hz。这个频率能跟上人的正常步行速度变化——人步频大约1.8Hz,20Hz采样能覆盖步态周期的绝大部分谐波。OpenMV的帧率大约是10~15Hz,UWB测距更新率可以调到15Hz左右,两个传感器在每个控制周期内不一定都有新数据,所以主控要做“最近值保持”:每个传感器维护一个时间戳,控制周期读取时取最近一次有效值,如果超过150ms没有新数据,该通道标记为陈旧。
5.2 死区让行李箱不抖,PID让行李箱跟得上
如果对距离误差直接做比例控制,行李箱会在目标静止时来回振荡——误差小于1cm时还在调整轮速,表现出来就是车身抖动。所以前后方向必须加死区:
#define DEAD_ZONE_M 0.15f #define TARGET_DIST_M 1.2f #define PID_KP 1.8f #define PID_KI 0.2f #define PID_KD 0.5f #define MAX_WHEEL_SPEED 0.8f typedef struct { float integral; float last_error; } PidState; float pid_update(PidState *pid, float target, float current, float dt) { float err = target - current; float drive = 0.0f; if (err > DEAD_ZONE_M || err < -DEAD_ZONE_M) { pid->integral += err * dt; // 积分限幅,防止长时间跟丢后积分饱和 if (pid->integral > 1.0f) pid->integral = 1.0f; if (pid->integral < -1.0f) pid->integral = -1.0f; float derivative = (err - pid->last_error) / dt; drive = PID_KP * err + PID_KI * pid->integral + PID_KD * derivative; pid->last_error = err; } else { drive = 0.0f; pid->integral = 0.0f; pid->last_error = 0.0f; } return drive; }三个参数的作用要分清楚:Kp决定主响应速度,设得太大容易出现“追一下、顿一下”的步进感;Ki负责消除目标匀速行走时的稳态误差,但行李箱这种场景积分作用要弱;Kd在人体突然停止时提供反向阻尼,防止行李箱冲过头。具体数值不是万能的——行李箱重量、轮子摩擦力、地面材质都会改变系统增益,上表的数值适合空载塑料箱体重约4kg的场景,装进重物后要线性下调Kp。
5.3 转向控制与速度耦合
转向控制用视觉方位角作为误差源。角度误差经过比例控制后输出左右轮速差,但要注意转向和前进是耦合的:如果行李箱在跟随过程中目标突然横移,视觉角度误差变大,转向机构会请求一个很大的差速——这时候前进速度也应该同步降低,否则转弯半径会急剧变小,车身容易侧翻。我在代码里用的是“速度-转向解耦”:前进速度由距离PID决定,转向量由角度PID决定,合成左右轮速时先缩放转向量到不超过前进速度的60%。这个比例是经验值,重心高、轮距窄的箱子要调到30%。
6. 验证方法和三个工程上的坑位
6.1 用最小化复现来验证降级逻辑
融合算法的验证不能只看最终能不能跟住人,要看“传感器撒谎时系统怎么反应”。我的做法是跑一组最小化复现实验:让测试者在走廊里匀速走直线,中途用一块泡沫板遮住OpenMV镜头2秒,检查行李箱应该减速直行而不是急停;再让测试者走到金属货架后面遮挡UWB路径,检查行李箱应该刹停而不是继续冲。下面是融合前后距离误差的对比分析脚本:
import re def parse_log(path): dist_uwb = [] dist_fused = [] with open(path) as f: for line in f: m = re.match(r"t=(\d+)\s+uwb=([\d.]+)\s+fused=([\d.]+)\s+truth=([\d.]+)", line) if m: dist_uwb.append(abs(float(m.group(2)) - float(m.group(4)))) dist_fused.append(abs(float(m.group(3)) - float(m.group(4)))) return dist_uwb, dist_fused uwb_err, fused_err = parse_log("log.txt") print(f"UWB 平均误差: {sum(uwb_err)/len(uwb_err):.3f} m") print(f"融合后平均误差: {sum(fused_err)/len(fused_err):.3f} m")真实跑下来,UWB单独测距在开阔走廊的误差约8cm,视觉+UWB融合后整体定位误差约12cm——融合后的误差比UWB单独大一点是正常的,因为视觉角度误差在目标偏左偏右时会引入径向误差分量。关注点应该是:遮挡后恢复的收敛时间,我一般要求在3秒内重新进入稳定跟随,超过5秒就需要检查滤波窗口是不是太大了。
6.2 坑位一:脚轮金属框架反射UWB
行李箱的脚轮支架、拉杆是金属的,UWB天线放在箱体内部时,金属结构会把信号反射后叠加到直射路径上,测距值呈现周期性的锯齿状波动——箱子推起来后轮子转动,反射路径长度随之变化。不要指望滤波能完全吃掉这个误差,要从安装位置解决:天线尽量伸出箱体外壳,或至少让天线与金属结构之间保持5cm以上的间距,并用尼龙柱隔离。
6.3 坑位二:OpenMV的自动曝光是跟随的敌人
OpenMV默认开启自动曝光,这在地面反光的走廊里会让画面亮度持续波动,导致颜色空间阈值匹配结果忽大忽小,检测框面积随之上下跳动。我上面的代码用了固定曝光,但仍然建议在Boot阶段对环境光做一次采样,然后设定固定的曝光值和白平衡值。逆光场景下即使固定曝光也无法解决目标面部过暗的问题,这时应提前降级到色块追踪模式,而不是继续用低置信度的人脸检测结果。
6.4 坑位三:传感器时间基准不一致
UWB模块和OpenMV各自有晶振,主控用轮询方式读取时,两个数据天然的相差几十毫秒。这个时延在目标慢走时不明显,但目标突然加速时,老旧的UWB距离配上新鲜的视觉角度,会让PID同时看到“距离偏大”和“角度突变”两个矛盾的误差,产生一次抽动。常见做法是在主控侧给每个传感器数据打上时间戳,控制周期内把超过100ms的数据视为不可用,宁可保持上一周期的输出,也不混用时间跨度太大的数据。
本文还有配套的精品资源,点击获取