树莓派4+OpenCV HOG行人检测:嵌入式实时人体追踪系统实践
1. 项目概述与核心思路你是否想过让一个装置能“看”到你并且能实时追踪你的位置比如一个会跟随你转动头部的玩偶或者一幅眼睛会“注视”着你的画作。这个想法听起来像是科幻电影里的场景但其实利用我们手边常见的硬件——一块树莓派4、一个普通的USB摄像头再加上一些开源的计算机视觉库完全可以在自己的工作室里把它实现出来。我最近就完成了这样一个项目一个基于树莓派4和OpenCV的实时人体位置检测与追踪系统。它的核心目标很简单让摄像头“认出”画面中的人并判断这个人是位于画面的左侧、中心还是右侧然后将这个位置信息实时地发送出去用以控制其他设备比如点亮不同位置的LED灯或者驱动舵机转动一个玩偶的头。整个系统的关键在于如何在树莓派这种计算资源相对有限的嵌入式平台上实现稳定、快速且准确的人体检测。树莓派4的性能虽然相比前代大幅提升但直接运行复杂的深度学习模型进行实时检测依然非常吃力会导致严重的卡顿。因此我选择了OpenCV中一个经典且高效的方法方向梯度直方图Histogram of Oriented Gradients, HOG结合支持向量机SVM的行人检测器。这个方法虽然不是最新的但在CPU上运行速度很快精度对于本项目来说也完全够用。整个系统的架构清晰树莓派负责“看”和“想”图像采集与处理而将“动”的任务控制外部设备交给了更擅长此道的Arduino通过串口进行通信各司其职。2. 核心原理HOG特征与行人检测在深入代码之前有必要先搞明白我们依赖的核心算法——HOG到底是如何工作的。这能帮助我们在后续调优时知道该动哪些“旋钮”。2.1 HOG特征是什么简单来说HOG特征描述的是图像局部区域的梯度方向分布。梯度可以理解为图像中像素值变化最快的方向和大小它能很好地刻画物体的边缘和轮廓。HOG算法的核心思想是物体的外观和形状能够被其局部区域的梯度或边缘方向分布很好地描述。它的计算过程可以拆解为以下几步图像预处理通常会将图像转换为灰度图并进行伽马校正来降低光照影响。计算梯度对每个像素计算其在水平和垂直方向上的梯度值进而得到梯度的幅值和方向。划分细胞单元将图像划分成小的、互不重叠的“细胞”例如8x8像素为一个细胞。对每个细胞统计其内部所有像素的梯度方向形成一个方向直方图比如把0-180度分成9个区间。块内归一化将相邻的若干个细胞组合成一个“块”。由于局部光照变化和背景对比度差异梯度强度的绝对值变化会很大。为了消除这种影响HOG会对一个块内所有细胞的直方图向量进行归一化处理。这是HOG算法性能优异的关键一步极大地提升了特征对光照和阴影的鲁棒性。收集HOG特征将所有块的归一化直方图向量拼接起来就得到了整张图像的HOG特征描述符一个高维度的向量。2.2 OpenCV中的HOG行人检测器OpenCV库中已经内置了一个基于HOG特征和线性SVM分类器的预训练行人检测模型。当我们调用cv2.HOGDescriptor_getDefaultPeopleDetector()时加载的就是这个模型。这个模型是在一个大型的行人数据集上训练好的它“学会”了行人的HOG特征应该是什么样子。在检测时算法会用一个滑动窗口遍历图像的不同位置和不同尺度这就是detectMultiScale函数在做的事对每个窗口提取HOG特征然后用SVM分类器判断这个窗口里是否包含行人。如果包含就返回这个窗口的位置和大小也就是我们代码中得到的“边界框”。注意OpenCV默认的HOG检测器是针对站立、全身的行人进行优化的。如果你的应用场景中人是坐着的、或者只出现半身检测效果可能会下降。这时可能需要寻找更专用的数据集训练的模型或者考虑使用基于深度学习的方法当然这对树莓派4的算力要求更高。2.3 为什么选择HOG而不是深度学习模型这是一个很实际的工程权衡。在树莓派4上我们有以下几个考虑速度像YOLO、SSD这类深度学习模型虽然精度高但即使使用轻量级版本在树莓派4上想达到实时例如15 FPS也非常困难需要借助神经计算棒等外设。而HOGSVM的纯CPU计算在低分辨率图像上可以轻松跑到20-30 FPS。资源占用深度学习模型动辄几十甚至上百MB而HOG检测器的模型非常小几乎不占用额外内存。精度与需求匹配对于“判断画面中是否有人以及大致位置”这个需求HOG的精度已经足够。我们的目标不是做高精度的姿态估计或身份识别而是实现一个稳定、低延迟的交互信号。因此HOG方案在资源受限的嵌入式视觉场景中依然是一个简单、可靠且高效的起点。3. 硬件准备与系统搭建3.1 硬件清单与选型考量你需要准备以下硬件其中一些型号可以有替代品树莓派4 Model B建议使用2GB内存版本及以上。这是整个系统的大脑负责运行操作系统和Python检测程序。选择4是因为其CPU和内存性能足以流畅处理HOG检测任务。树莓派外围设备至少需要一张MicroSD卡建议16GB以上Class 10、一个5V/3A的USB-C电源适配器、一个HDMI显示器用于初次设置后续可无头运行、键盘和鼠标。USB摄像头我使用的是Logitech C615因为它即插即用驱动兼容性好。你也可以使用树莓派官方摄像头模块但需要注意其排线连接和不同的OpenCV调用方式cv2.VideoCapture(0)会失效需使用Picamera2库。Arduino Uno负责接收树莓派的指令并控制外部设备。选择Uno是因为其经典、稳定且与5V LED连接无需电平转换。USB Type A to Type B 数据线用于连接树莓派和Arduino同时为Arduino供电并建立串口通信。面包板、LED灯、跳线用于搭建一个简单的状态指示电路。我用了3个LED左、中、右来直观显示检测到的位置。实操心得摄像头选择尽量选择免驱的UVC摄像头。在购买前可以搜索“摄像头型号 Linux UVC”来确认兼容性。高分辨率不一定好因为我们的处理帧会缩放到很小140x140高分辨率只会增加无谓的USB总线带宽占用和初始缩放开销。一个支持640x480的普通摄像头就完全足够。3.2 电路连接详解连接分为两部分树莓派基础连接和Arduino控制电路。树莓派基础连接将安装好系统的MicroSD卡插入树莓派。使用USB-C电源适配器供电。通过HDMI线连接显示器。插入USB摄像头和键盘鼠标。Arduino与LED电路连接这个电路非常简单目的是用三个LED分别代表“检测到人在左侧”、“在中心”、“在右侧”。将Arduino Uno通过USB线连接到树莓派的USB端口。在面包板上并排放置三个LED确保它们彼此不共用同一行插孔。每个LED的阳极长脚通过一根跳线分别连接到Arduino的数字引脚4、8、12。我选择这几个引脚是因为它们在Uno上分布较开方便布线且都不是具有特殊功能的PWM引脚本例中不需要PWM。每个LED的阴极短脚通过跳线连接到面包板的负极总线通常标有“-”或蓝色线。最后用一根跳线将Arduino的GND引脚连接到面包板的负极总线。这样就完成了“共地”连接。整个电路没有使用电阻是因为Arduino Uno的数字引脚输出电流有限约20mA直接驱动LED不会烧毁芯片或LED但为了更规范和安全强烈建议为每个LED串联一个220Ω的限流电阻接在阳极和Arduino引脚之间。连接示意图如下文字描述Arduino Pin 4 ---[LED1阳极] LED1 [阴极]--- GND总线 Arduino Pin 8 ---[LED2阳极] LED2 [阴极]--- GND总线 Arduino Pin 12--[LED3阳极] LED3 [阴极]--- GND总线 Arduino GND ----------------------------------- GND总线4. 软件环境配置系统运行在树莓派操作系统上。我使用的是Raspberry Pi OS (基于Debian的Bullseye版本)。以下所有操作均在终端中完成。4.1 系统更新与Python确认首先打开终端更新软件包列表并升级现有软件这是一个好习惯sudo apt update sudo apt upgrade -y升级过程可能需要一些时间。接着检查Python3的版本本项目基于Python3python3 --versionRaspberry Pi OS通常预装了Python 3.9这完全够用。如果显示版本过低比如3.7可以考虑升级。但一般情况下使用系统自带的版本兼容性最好。4.2 安装必要的Python库我们需要三个关键的Python库NumPyPython科学计算的基础包OpenCV依赖它来处理图像数据数组。OpenCV核心的计算机视觉库。PySerial用于树莓派和Arduino之间的串口通信。使用pip3Python3的包管理器进行安装# 首先确保pip3已安装 sudo apt install python3-pip -y # 安装NumPy和OpenCV pip3 install numpy opencv-python # 安装PySerial pip3 install pyserialopencv-python这个包是OpenCV官方为Python预编译的版本安装最方便。如果安装速度慢可以考虑使用国内镜像源例如pip3 install numpy opencv-python pyserial -i https://pypi.tuna.tsinghua.edu.cn/simple4.3 安装Arduino IDE用于上传固件到Arduino虽然我们主要编程在树莓派上进行但需要给Arduino上传一个简单的固件程序。在树莓派上安装Arduino IDE是最直接的方法。确定树莓派架构uname -m如果输出是aarch64则是64位系统如果是armv7l则是32位系统。树莓派4的64位系统通常显示为aarch64。前往Arduino官网下载对应的Linux ARM版本。你可以直接在树莓派终端里用wget下载例如以下链接以64位为例请根据你的实际情况选择wget https://downloads.arduino.cc/arduino-ide/arduino-ide_2.3.2_Linux_ARM64.tar.xz解压并安装tar -xf arduino-ide_*.tar.xz cd arduino-ide_* ./install.sh安装脚本会在系统中创建快捷方式。安装完成后你可以在图形界面的编程菜单中找到Arduino IDE。注意事项安装OpenCV的替代方案使用pip install opencv-python安装的是最精简的版本。如果你后续需要用到OpenCV的更多功能例如GPU加速但树莓派上有限或者遇到兼容性问题可以选择从源码编译OpenCV。但编译过程耗时很长数小时且需要解决大量依赖。对于本项目pip安装的版本是最佳选择。5. Arduino端程序解析与上传Arduino在这里扮演一个“听话的执行者”角色。它不断监听来自树莓派串口的数据并根据收到的简单指令0,1,2,3来控制LED的亮灭。5.1 代码逐行解析打开Arduino IDE创建一个新项目并输入以下代码// 变量用于存储从串口接收到的数据 int incomingData; // 声明LED所连接的引脚编号 int LED_Left 4; int LED_Center 8; int LED_Right 12; void setup() { // 将三个引脚都设置为输出模式这样才能驱动LED pinMode(LED_Left, OUTPUT); pinMode(LED_Center, OUTPUT); pinMode(LED_Right, OUTPUT); // 启动串口通信波特率设置为115200。这个值必须与树莓派Python代码中的设置完全一致 Serial.begin(115200); // 设置串口读取超时为1毫秒。这很重要防止程序卡在等待串口数据上。 Serial.setTimeout(1); } void loop() { // 这是一个等待循环。当串口缓冲区没有数据时程序会停在这里等待。 while (!Serial.available()) { // 空循环直到有数据到来 } // 读取串口数据。数据以字符串形式发送我们将其转换为整数。 incomingData Serial.readString().toInt(); // 根据接收到的整数值执行不同的动作 // 0: 未检测到人关闭所有LED if (incomingData 0) { digitalWrite(LED_Left, LOW); digitalWrite(LED_Center, LOW); digitalWrite(LED_Right, LOW); } // 1: 人在左侧点亮左侧LED else if (incomingData 1) { digitalWrite(LED_Left, HIGH); digitalWrite(LED_Center, LOW); digitalWrite(LED_Right, LOW); } // 2: 人在中心点亮中心LED else if (incomingData 2) { digitalWrite(LED_Left, LOW); digitalWrite(LED_Center, HIGH); digitalWrite(LED_Right, LOW); } // 3: 人在右侧点亮右侧LED else if (incomingData 3) { digitalWrite(LED_Left, LOW); digitalWrite(LED_Center, LOW); digitalWrite(LED_Right, HIGH); } // 可选将接收到的数据原样打印回串口用于调试 // Serial.print(incomingData); }5.2 上传代码到Arduino用USB线将Arduino Uno连接到树莓派的USB口。在Arduino IDE中选择正确的板卡类型工具-开发板-Arduino Uno。选择正确的端口工具-端口。通常会显示为/dev/ttyACM0或/dev/ttyUSB0。记下这个端口号比如/dev/ttyACM0这在后面的Python代码中会用到。点击上传按钮向右的箭头。上传成功后Arduino就准备好了。此时即使断开与电脑树莓派的串口监视器连接程序也会在Arduino上独立运行。6. 树莓派Python核心程序深度解析这是整个项目的灵魂。程序持续从摄像头读取帧使用HOG检测人体计算位置并通过串口发送指令。6.1 代码结构与初始化创建一个新文件例如human_tracker.py并开始编写。# 导入必要的库 import numpy as np # 用于处理检测框的数组操作 import cv2 # OpenCV核心视觉库 import serial # 用于与Arduino串口通信 import time # 可选用于添加帧率计算或延迟 # 串口配置 # 重要将这里的端口号替换为你之前记下的Arduino端口 arduino_port /dev/ttyACM0 # 创建串口对象波特率115200与Arduino端匹配。超时设置防止读串口阻塞。 arduino serial.Serial(portarduino_port, baudrate115200, timeout0.01) # 中心容差阈值。单位是像素在缩放后的图像上。 # 当人的中心点与画面中心的水平距离小于此值时认为人已“居中”。 center_tolerance 5 # 初始化HOG描述符并设置为使用OpenCV默认的行人检测器 hog cv2.HOGDescriptor() hog.setSVMDetector(cv2.HOGDescriptor_getDefaultPeopleDetector()) # 打开摄像头。参数0通常代表系统默认的第一个摄像头。 cap cv2.VideoCapture(0) # 可以设置摄像头分辨率降低分辨率能显著提升处理速度 # cap.set(cv2.CAP_PROP_FRAME_WIDTH, 320) # cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 240) # 定义一个简单的串口读写函数 def write_read(x): 向Arduino发送字符串x并读取其回复本例中未使用回复 arduino.write(bytes(x, utf-8)) data arduino.readline() # 读取一行返回数据用于调试确认 return data6.2 主循环检测、计算与追踪接下来是程序的主循环它会一直运行直到按下‘q’键。while True: # 1. 捕获一帧图像 ret, frame cap.read() if not ret: print(无法从摄像头读取帧) break # 2. 图像预处理为了加速处理我们将图像大幅缩小。 # 原始图像可能是640x480我们缩放到140x140。 # HOG检测在小图上进行因为行人特征在大尺度上依然明显且计算量平方级减少。 small_frame cv2.resize(frame, (140, 140)) # 也可以转换为灰度图HOG本身会处理灰度信息但OpenCV的检测器输入可以是彩色。 # gray cv2.cvtColor(small_frame, cv2.COLOR_BGR2GRAY) # 3. HOG多尺度检测 # boxes: 检测到的边界框列表每个框格式为 [x, y, width, height] # weights: 检测置信度本例未使用 # winStride: 滑动窗口的步长越小检测越细密但越慢。(1,1)最慢但最准。 # scale: 图像金字塔的缩放系数用于多尺度检测。1.0值越小检测尺度越多越慢。 boxes, weights hog.detectMultiScale(small_frame, winStride(1,1), scale1.05) # 4. 转换框的格式并计算中心点 # 将 (x, y, w, h) 格式转换为 (x1, y1, x2, y2) 格式方便后续计算 boxes np.array([[x, y, x w, y h] for (x, y, w, h) in boxes]) centers [] # 用于存储每个检测框的中心点信息 for box in boxes: # 计算框的水平中心点坐标 center_x (box[0] box[2]) / 2.0 # 计算中心点相对于图像中心70因为140/270的水平偏移量 x_pos_rel_center center_x - 70 # 计算到中心的绝对距离用于排序找到“最近”的人 dist_to_center_x abs(x_pos_rel_center) # 将信息存入字典添加到列表 centers.append({ box: box, x_pos_rel_center: x_pos_rel_center, dist_to_center_x: dist_to_center_x }) # 5. 决策与通信 if len(centers) 0: # 5.1 按距离画面中心的远近对检测到的人进行排序 sorted_boxes sorted(centers, keylambda i: i[dist_to_center_x]) # 5.2 在图像上绘制检测框在原始的小帧上绘制后续会放大显示 # 离中心最近的人用绿色框其他人用红色框 for i, item in enumerate(sorted_boxes): box item[box] color (0, 255, 0) if i 0 else (0, 0, 255) # BGR格式绿色和红色 cv2.rectangle(small_frame, (box[0], box[1]), (box[2], box[3]), color, 1) # 线宽为1 # 5.3 获取“追踪目标”最近的人的水平偏移量 target_x_offset sorted_boxes[0][x_pos_rel_center] # 5.4 根据偏移量和容差阈值决定发送什么指令 if -center_tolerance target_x_offset center_tolerance: command 2 # 居中 print(f中心 | 偏移量: {target_x_offset:.1f}) elif target_x_offset center_tolerance: command 3 # 人在右侧需要向右转 print(f右侧 | 偏移量: {target_x_offset:.1f}) else: # target_x_offset -center_tolerance command 1 # 人在左侧需要向左转 print(f左侧 | 偏移量: {target_x_offset:.1f}) # 发送指令给Arduino write_read(command) else: # 没有检测到任何人 command 0 print(未检测到人) write_read(command) # 6. 显示结果为了看得清楚将处理后的图像放大显示 display_frame cv2.resize(small_frame, (560, 560)) # 放大4倍显示 cv2.imshow(Human Tracker, display_frame) # 7. 退出条件按下‘q’键 if cv2.waitKey(1) 0xFF ord(q): break # 8. 释放资源 cap.release() cv2.destroyAllWindows() arduino.close() # 关闭串口6.3 关键参数调优与解释代码中有几个关键参数直接影响检测效果和性能center_tolerance 5中心容差。这是在缩放后图像140x140上的像素值。设置为5意味着当人的中心点在画面中心左右5个像素范围内都认为是“已居中”。这个值太小会导致系统在中心点附近频繁切换状态抖动太大则会导致追踪不精确。需要根据实际应用场景调整。hog.detectMultiScale参数winStride(1,1)滑动窗口的步长。设置为(1,1)意味着窗口每次移动1个像素这会进行最密集的检测结果最准确但计算量最大最慢。可以尝试增大到(4,4)或(8,8)来大幅提升速度但可能会错过一些目标。scale1.05图像金字塔的缩放因子。1.05意味着每次将图像缩小到原来的95.2%1/1.05然后在新尺度上检测。这个值越接近1.0检测的尺度越多对小尺寸或大尺寸的行人检测效果更好但计算量也越大。可以尝试增大到1.1或1.2来提速。图像缩放尺寸(140, 140)这是性能提升最关键的一步。在640x480的原图上直接运行HOG检测会非常慢。将其缩放到一个很小的尺寸如140x140检测速度会有数量级的提升。虽然会损失一些对小尺寸行人的检测能力但对于中近距离的追踪应用完全足够。你可以根据摄像头与人的距离尝试(160,120)、(120,90)等尺寸在速度和检测范围间取得平衡。实操心得性能与精度的权衡在树莓派4上我的目标是流畅15fps。经过测试将图像缩放到140x140winStride设为(4,4)scale设为1.1可以在保证不错检出率的同时达到约20fps的处理速度。如果你的场景中人移动较慢或者距离固定可以进一步降低分辨率或增大步长来换取更高的帧率。7. 系统联调与效果优化将Python程序保存后在终端中运行python3 human_tracker.py你应该会看到一个显示窗口里面是缩放并处理后的画面。当人进入画面时会被绿色或红色的框标出。同时根据你的位置对应的LED灯会被点亮。7.1 常见问题与排查错误serial.serialutil.SerialException: [Errno 13] could not open port /dev/ttyACM0: [Errno 13] Permission denied: /dev/ttyACM0原因当前用户没有访问串口设备的权限。解决将用户加入dialout组然后注销重新登录。sudo usermod -a -G dialout $USER或者临时使用sudo运行程序不推荐长期使用sudo python3 human_tracker.py错误cv2.error: OpenCV(4.x) ...: error: (-215:Assertion failed) !ssize.empty() in function resize原因cap.read()没有成功读取到帧frame是空的导致cv2.resize出错。解决检查摄像头连接并确保在cv2.resize前判断ret是否为True代码中已添加。检测不到人或者检测框乱跳可能原因1光照条件差。HOG对边缘敏感在光线昏暗或背景杂乱时效果差。确保环境光线充足背景相对简洁。可能原因2参数过于激进。winStride和scale设置导致检测速度过快漏检。尝试将winStride改为(1,1)scale改为1.05观察效果再逐步调整至速度可接受。可能原因3人距离摄像头太远。在140x140的图像中人可能只有几十个像素高。尝试让人离摄像头近一些或者稍微增大scale参数如1.03来检测更小的目标。LED灯状态更新延迟或卡顿原因树莓派处理一帧的时间过长导致串口指令发送频率低。解决按照“6.3关键参数调优”中的建议降低处理分辨率增大winStride和scale值。你可以在代码循环开始和结束记录时间计算并打印帧率FPS直观了解性能。start_time time.time() # ... 主循环处理 ... end_time time.time() fps 1 / (end_time - start_time) print(fFPS: {fps:.2f})检测框在人物周围抖动原因这是基于滑动窗口检测的固有现象相邻帧间检测框的位置和大小可能有细微变化。优化可以引入简单的跟踪算法比如使用OpenCV的cv2.TrackerKCF或cv2.TrackerCSRT。在第一帧用HOG检测到人后初始化一个跟踪器后续帧使用跟踪器预测位置而不是每一帧都重新运行耗时的HOG检测。当跟踪置信度低时再重新触发HOG检测。这能极大提升流畅度并减少抖动。7.2 从演示到实用扩展思路当前的系统只是一个原理演示。你可以基于此进行很多有趣的扩展控制舵机将Arduino控制的LED换成两个舵机分别控制水平和垂直方向。Python程序除了计算水平偏移x_pos_rel_center还可以计算垂直偏移。将两个偏移量映射成舵机角度通过串口发送给Arduino就能实现一个真正能“转头”跟随的摄像头云台。增加反馈机制让Arduino控制舵机转动后将当前舵机角度回传给树莓派。树莓派可以结合这个角度和检测到的人体位置实现更复杂的闭环控制逻辑比如PID控制使跟随更平滑。多目标选择策略当前是追踪“最近中心”的人。你可以修改策略比如追踪画面中最大的人可能最近或者持续追踪第一个出现的人需要为每个检测框分配ID这涉及到更复杂的多目标跟踪算法如SORT或DeepSORT。集成其他传感器结合超声波传感器或红外传感器可以判断人与摄像头的距离实现“走近时后退走远时跟随”的互动效果。使用更高效的检测方法如果树莓派4的性能仍感不足可以考虑使用OpenCV的DNN模块加载轻量级的深度学习模型如MobileNet-SSD并搭配Intel Movidius神经计算棒进行加速在保持精度的同时获得更高的帧率。这个项目就像打开了一扇门展示了在廉价、小巧的嵌入式硬件上实现实时计算机视觉的可能性。从让玩偶转头到智能安防提醒其核心逻辑都是相通的。希望这份详细的解析和实操记录能帮助你顺利搭建自己的系统并激发出更多创意。