资讯动态

基于OpenCV与STM32的二自由度人脸跟踪云台设计与实现

发布时间:2026/9/11 11:29:21 来源:尧图企业网站定制
简介基于OpenCV与STM32的二自由度舵机云台人脸跟踪项目通过视觉识别与嵌入式控制协同实现对移动人脸的实时追踪面向电子信息、自动化、物联网等专业的毕设、课设及项目演示场景。包内共96个文件包含36个h头文件、35个c源文件的嵌入式控制代码以及uvprojx工程文件、Python人脸跟踪脚本、图片/GIF示例、README说明文档等压缩包约9.54MB既可直接烧录运行也便于按目录拆解学习。项目源自高分优秀作品代码经过真机调试运行能在人脸移动时驱动云台平滑转动核心思路涵盖图像处理、串口通信、PID舵机控制和STM32底层驱动具有较强的参考价值。目前已有93人学习下载适合有C与Python基础、想做视觉与单片机联动开发的读者。1. 二自由度人脸跟踪云台关键是把像素坐标换算成舵机角度二自由度人脸跟踪云台是说一只舵机负责水平转动Pan、一只舵机负责俯仰Tilt让摄像头始终跟着人脸中心走。基于OpenCV和STM32的实现并不是让单片机去跑人脸识别——STM32的算力和内存都不够而是把任务拆给两端PC端的OpenCV先检测出人脸位置把像素偏差换算成舵机角度再通过串口把角度帧发给STM32STM32只负责解析协议帧、更新PWM占空比用舵机把摄像头转过去。视觉端换算法不影响控制端这是这类项目最值得学的架构边界。适合做毕业设计或者想从单板串口通信进阶到视觉加运动控制闭环的人。2. 系统架构和硬件选型为什么是OpenCVSTM32而不是全单片机2.1 视觉端与控制端的分工边界很多人想当然觉得跟踪云台应该让STM32完成全部工作摄像头接在单片机上识别到人脸后自己转舵机。这个思路在K210、OpenMV这种带视觉加速的芯片上行得通但在通用STM32上并不可行。以STM32F103C8T6为例主频72MHz、RAM只有20KB跑人脸检测涉及多尺度窗口扫描和图像金字塔帧率很难超过10更不用说还要留资源给PWM和串口。因此业内的常规做法是把视觉负荷放到OpenCV侧PC或树莓派读取摄像头OpenCV做人脸检测计算出目标中心与画面中心的像素偏差再映射成两个舵机的目标角度。控制端STM32则只需要做三件事收串口帧、解析角度、输出对应占空比的PWM信号。这样视觉算法迭代和下位机调试可以完全解耦——想换更准的检测模型改上位机代码想换舵机或加限位保护改下位机代码。2.2 STM32选型、舵机选型与电源拓扑2.2.1 主控与云台机械结构STM32F103C8T6最小系统板是这类项目的“标配”。它提供3个通用定时器和USART1云台项目只占用很小一部分资源USART1接串口TIM3的两个通道输出两路PWM剩下几个GPIO接指示灯。国产APM32F103、GD32F103引脚兼容同工程可以直接替换想压缩成本时可以换过去。拿到这类完整资料包时第一件事是分开上位机源码Python和下位机源码Keil工程不要试图在STM32工程里找OpenCV人脸检测那本来就跑不动。云台支架建议买成品的二自由度云台架或者用3D打印件关键是Pan轴和Tilt轴要能锁紧否则舵机转动时摄像头会因惯性晃动。2.2.2 舵机供电是最大的坑器件推荐型号关键参数注意事项视觉端PC USB摄像头640×48030fps足够OpenCV先读设备号主控STM32F103C8T672MHz / 64KB Flash引脚兼容APM32/GD32Pan/Tilt舵机SG90 或 MG90S50Hz PWM0.5~2.5ms脉宽必须独立5V电源串口模块CH340 USB转TTL3.3V TTL电平必须与MCU共地云台架二自由度金属/打印件根据舵机臂尺寸注意孔位公差舵机启动瞬间电流很大SG90在堵转时可能到500mA甚至更高。STM32板载3.3V LDO输出能力有限不能直接给舵机供电从USB口5V直连驱动单只舵机也容易导致电压跌落进而引发复位或死机。我一般会让STM32从USB取5V经板载LDO转3.3V舵机单独接一路5V 1A以上的适配器二者GND必须短接。如果是MG90S这种金属齿轮舵机连续往返运动比SG90耐用答辩演示前有条件尽量换。2.3 串口链路先枚举端口再谈协议最简单的链路是USB转TTL模块连接STM32的USART1上位机用pyserial打开串口。波特率选115200帧长度5字节即使每秒发30帧数据量也只有150字节串口负载很小完全不需要复杂的传输层。调试时如果串口设备号不固定可以先用一小段代码枚举import serial.tools.list_ports ports serial.tools.list_ports.comports() for p in ports: print(p.device, p.description)这段代码的作用是列出当前系统里所有可用串口避免代码里硬编码COM3后插拔USB导致设备号漂移在Windows和Linux下都能用。3. OpenCV识别与人脸跟踪算法从框出人脸到算出角度3.1 用Haar Cascade先把最小闭环跑通人脸检测在OpenCV里最简方案是Haar Cascade一个XML分类器文件加载后直接调用detectMultiScale。opencv-python安装后自带模型文件代码里可以通过cv2.data.haarcascades路径引用不需要额外下载。完整上位机代码如下import cv2 import serial cap cv2.VideoCapture(0) # 0 是默认摄像头设备号 if not cap.isOpened(): raise SystemExit(无法打开摄像头先检查设备编号) face_cascade cv2.CascadeClassifier( cv2.data.haarcascades haarcascade_frontalface_default.xml) ser serial.Serial(/dev/ttyUSB0, 115200, timeout0.05) pan, tilt 90, 90 KP 0.3 # 像素到角度的比例系数需按实际标定 DEAD_ZONE 15 # 死区像素数防止微抖 def clamp(v, lo, hi): return max(lo, min(hi, v)) while True: ret, frame cap.read() if not ret: continue h, w frame.shape[:2] gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces face_cascade.detectMultiScale( gray, scaleFactor1.1, minNeighbors5, minSize(80, 80)) if len(faces) 0: # 多个目标时取面积最大的避免云台来回摆 x, y, fw, fh max(faces, keylambda f: f[2] * f[3]) cx x fw // 2 cy y fh // 2 dx cx - w // 2 dy cy - h // 2 # 死区内的微小偏差不触发转动 if abs(dx) DEAD_ZONE: pan clamp(pan dx * KP, 0, 170) if abs(dy) DEAD_ZONE: tilt clamp(tilt - dy * KP, 0, 170) # 协议帧: 0xFF 0xAA PAN TILT CHECK frame_data bytes([0xFF, 0xAA, int(pan), int(tilt), (int(pan) int(tilt)) 0xFF]) ser.write(frame_data) cv2.rectangle(frame, (x, y), (x fw, y fh), (0, 255, 0), 2) cv2.imshow(face tracking, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()这里有几个参数需要重点说明。scaleFactor1.1表示每轮检测将图像缩小10%值越大检测越快但越容易漏检minNeighbors5表示候选框至少被5个相邻检测结果确认才保留调大能降低误检率minSize(80, 80)过滤掉小于80×80像素的检测框防止远处背景误触发。死区DEAD_ZONE15的作用是人脸在画面中心附近小幅移动时不发角度指令否则舵机会一直做微小修正产生高频抖动和噪音。3.2 像素偏差到角度比例控制与标定方法舵机角度不是由像素坐标直接决定的因为摄像头光学参数、分辨率、人脸距离都会影响同一个像素偏差对应的实际角度。所以工程里很少做完整的相机标定而是用比例控制angle center_angle (pixel_error * KP)KP是标定量640×480分辨率下经验值在0.2~0.5之间。云台来回震荡时缩小KP人脸移动时跟不上就适当增大。更严谨的标定做法是人脸站在已知距离上左右移动固定距离记录像素偏差和实际偏转角按三角函数换算出每像素对应角度。这个方法写进设计说明书比较加分但多数课程设计用固定比例控制已经足够。需要注意图像坐标系的符号问题。图像y轴向下人脸在画面上方时dy为负Tilt舵机需要抬头。代码里用的是tilt - dy * KP即dy为负时tilt增大。如果实际安装方向相反把这个符号反过来即可。Pan方向的检查更简单人脸往左边移动观察云台是否往左转不对就改dx前的符号。3.3 想更稳一点把Haar换成OpenCV DNN或YuNetHaar Cascade对正面人脸效果好但侧脸、暗光、戴眼镜场景下漏检率偏高。OpenCV 4.x的DNN模块可以直接加载YuNet人脸检测模型检测精度和速度都比Haar好代码改动也不大detector cv2.FaceDetectorYN.create( face_detection_yunet_2023mar.onnx, , (320, 320), # 输入尺寸 0.6, # 置信度阈值 0.3, # NMS阈值 5000) # 最大检测目标数YuNet的输出是一个N×15的数组前4列是x、y、w、h后面包含人脸关键点坐标和置信度。选最大框后后续的像素偏差计算和串口帧发送逻辑完全复用不需要动下位机。如果FPS偏低可以先把图像cv2.resize到640×360检测面积变小速度提升明显云台跟手度更高。检测器易用性CPU实时性抗侧脸/暗光适用场景Haar Cascade极高高中快速验证、课程设计YuNet需要下载onnx高高答辩演示、实机部署MediaPipe需额外安装中高有移动端铺垫的选题4. STM32端串口协议解析、PWM生成和运动平滑4.1 串口中断状态机解析5字节帧上位机发的是5字节定长帧0xFF, 0xAA, PAN, TILT, CHECK。其中CHECK是PAN和TILT之和的低8位。为什么不用换行符当帧边界因为角度以纯二进制发送数据里可能出现0x0A或0x0D用换行容易截断。固定的双帧头加校验和是MCU串口通信最朴素的防错方式。在HAL库工程里先在main中调用一次HAL_UART_Receive_IT(huart1, rx_byte, 1)然后在回调函数中处理状态机// 全局变量 uint8_t rx_byte; volatile uint8_t rx_state 0; // 0:等0xFF 1:等0xAA 2:角度1 3:角度2 4:校验 uint8_t rx_buf[3]; volatile uint8_t new_frame_flag 0; volatile uint8_t target_pan 90, target_tilt 90; // 主程序初始化时调用一次 // HAL_UART_Receive_IT(huart1, rx_byte, 1); void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART1) { switch (rx_state) { case 0: if (rx_byte 0xFF) rx_state 1; break; case 1: if (rx_byte 0xAA) rx_state 2; else rx_state 0; break; case 2: if (rx_byte 170) { rx_buf[0] rx_byte; rx_state 3; } else rx_state 0; break; case 3: if (rx_byte 170) { rx_buf[1] rx_byte; rx_state 4; } else rx_state 0; break; case 4: if ((uint8_t)(rx_buf[0] rx_buf[1]) rx_byte) { target_pan rx_buf[0]; target_tilt rx_buf[1]; new_frame_flag 1; } rx_state 0; break; default: rx_state 0; break; } // 关键必须重新挂起接收否则只收一帧 HAL_UART_Receive_IT(huart1, rx_byte, 1); } }状态机的核心价值在于丢字节后能自动回到帧头等待状态不需要手动清缓冲区。PAN和TILT在上位机被限制在0到170因此永远不会等于0xFF或0xAA天然避开了“数据里混入帧头”的歧义。每次回调结束必须重新调用HAL_UART_Receive_IT因为HAL库的单字节接收是一次性的不重新挂起串口就只收第一帧。4.2 用定时器PWM产生舵机需要的50Hz脉冲舵机不是按占空比百分比控制的它响应的是脉冲宽度SG90在0.5ms脉宽对应0度2.5ms对应180度周期20ms对应50Hz。这个信号由STM32定时器输出比较产生。在CubeMX中配置TIM3配置项值说明Clock SourceInternal Clock内部时钟Prescaler7172MHz/(711)1MHzCounter Period1999920000计数×1us20msChannel 1PWM Generation CH1PA6Pan舵机Channel 2PWM Generation CH2PA7Tilt舵机Pulse初始值150090度对应的1.5ms脉宽对应到HAL库写一个通用的角度设置函数void servo_set_angle(uint8_t channel, uint8_t angle) { // 0~180度映射到500us~2500us脉宽 uint32_t pulse_us 500 (uint32_t)angle * 2000 / 180; if (channel 1) { __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_1, pulse_us); } else if (channel 2) { __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_2, pulse_us); } }由于预分频设置为71计数器每1us加一所以pulse_us直接以微秒为单位写入比较寄存器。90度时pulse_us等于1500正好是1.5ms脉宽。超过170度的角度指令不应该出现因为大部分180度舵机的实际行程达不到标称值强制转到极限位置可能导致堵转发热。4.3 运动平滑限速与超时保护上位机每帧都发目标角度但差值过大时舵机会快速甩动看起来像抽搐机械结构也会承受很大冲击。下位机需要做限速常见的做法是每个控制周期内角度最多变化固定步长#define STEP_MAX 2 #define TIMEOUT_MS 2000 void yuntai_smooth_loop(void) { // 限速每轮最多变化2度 if (target_pan current_pan STEP_MAX) current_pan STEP_MAX; else if (target_pan current_pan - STEP_MAX) current_pan - STEP_MAX; else current_pan target_pan; if (target_tilt current_tilt STEP_MAX) current_tilt STEP_MAX; else if (target_tilt current_tilt - STEP_MAX) current_tilt - STEP_MAX; else current_tilt target_tilt; // 超时保护2秒没收到新帧回到中位90度 if (HAL_GetTick() - last_frame_tick TIMEOUT_MS) { target_pan 90; target_tilt 90; } servo_set_angle(1, (uint8_t)current_pan); servo_set_angle(2, (uint8_t)current_tilt); }主循环每10ms调用一次yuntai_smooth_loop最大角速度就是每10ms 2度走完全行程约0.9秒实际跟人速度足够。STEP_MAX调大云台响应更激进调小则更柔和。超时保护非常关键串口突然拔出或上位机崩溃时舵机会停在最后收到角度上有了这一条就会自动回中避免长时间堵转。5. 人脸跟踪云台的三步联调法先手动、再看日志、最后追目标5.1 第一步不跑人脸检测先发固定角度联调最容易犯的错误是一上来就跑完整的人脸跟踪结果云台不动无法判断是OpenCV没检测到人脸还是STM32没收到串口帧还是PWM没输出。我习惯先把视觉部分摘掉用一小段脚本直接发角度序列import serial import time ser serial.Serial(/dev/ttyUSB0, 115200, timeout0.1) for ang in [60, 90, 120, 90]: data bytes([0xFF, 0xAA, ang, ang, (ang * 2) 0xFF]) ser.write(data) time.sleep(1)如果舵机能依次转到60、90、120度再回中说明STM32端串口协议和PWM链路都没问题。不动就按顺序查先量舵机电源是否在4.8V以上再看PA6/PA7有没有接到舵机信号线最后用逻辑分析仪抓TIM3的CH1输出确认有没有20ms周期脉冲。这一步把问题范围缩到最小。5.2 第二步上位机打印角度看参数是不是合理固定角度验证通过后把上位机脚本跑起来但先不要看云台而是看控制台打印的pan、tilt、dx、dy。如果dx在持续变化但pan一直不变检查死区阈值逻辑或者串口帧写入条件如果pan突变幅度很大说明KP偏大云台在临界震荡如果pan变化但舵机不动问题在下位机限速段检查current_pan是否真的被更新。5.3 第三步先追颜色目标再换人脸检测人脸受到光照和角度干扰大调试阶段可以先找一个高饱和度的彩色物体比如红色小球用inRange做颜色检测替代人脸检测import cv2 import numpy as np mask cv2.inRange(frame, (0, 0, 120), (80, 80, 255)) cnts, _ cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) # 取最大轮廓的中心后续偏差与串口发送逻辑复用颜色目标的中心坐标计算、死区判断、串口发送逻辑与人脸完全一致先调稳整条链路再换回Haar或YuNet检测人脸。这样即使换检测器也不会引入新的变量。故障速查表现象优先排查点处理办法串口无数据共地/接线/波特率用串口助手回环测试舵机不动且发热角度越界、PWM未启动查ARR和比较寄存器云台震荡KP过大、机械虚位减小KP或加大死区轻微抖动带蜂鸣舵机电源不足独立5V供电并共地跟丢后找不到目标检测帧率低、死区过大降分辨率、缩小DEAD_ZONE最后补一个标定技巧在墙壁上标记画面中心点和±100像素位置把云台固定后实际测量偏转角度反推像素到角度的比例系数。这个方法在写设计说明书时能提供实测数据支撑比直接写“经验值KP0.3”更有说服力。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价