ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

树莓派4B机器视觉实战:OpenCV目标检测与运动识别详解

2026/9/16 10:10:04 拓冰建站 浏览量
树莓派4B机器视觉实战:OpenCV目标检测与运动识别详解 简介面向智能硬件视觉方向以树莓派为硬件平台结合Python语言与OpenCV视觉库完成视觉项目开发可应用于毕业设计、课程设计、工程实训、学科竞赛及初学练手。包内共180个文件压缩包约336MB主体由49个Python脚本、18个H264视频流片段、Avi/Mp4演示视频、Caffe模型文件、XML配置、图片数据集等组成覆盖模型调用、图像处理与结果演示的完整流程。模型部分包含MobileNetSSD目标检测网络及其prototxt配置配合视频与图片数据可快速看到运行结果脚本按功能拆分便于学习和二次开发。项目代码经测试可运行并附说明文件与清晰目录结构方便搭建环境复现实验效果。已有135人学习/下载项目完成度较高据资料介绍答辩评审平均分达96分适合需要完整视觉项目参考的学习者也能借鉴整体设计完成设计报告与答辩展示。1. 树莓派跑机器视觉先从这份工程文件里读懂程序主线拿到这个压缩包时一眼扫过去就知道它不是什么教学玩具。MobileNetSSD_deploy.caffemodel负责目标检测motion.data和image_rgb.data是运行过程中保存的中间结果highres.h264是输入视频output_01.avi是最终输出。整条链路非常清晰摄像头或视频文件进入 OpenCV 处理检测和运动识别并行最后把标注画面写回 AVI。这种结构直接对应一套智能安防或智能小车视觉系统适合正在做毕业设计、课程大作业或者电子设计竞赛的人复现。最有价值的地方在于它没有把“检测”和“运动识别”耦合在一起而是拆成两个模块树莓派4B 的 CPU 性能有限拆开后哪个环节拖慢帧率一眼就能定位到瓶颈。如果你正准备开始机器视觉学习路线这个工程比零散地看 OpenCV 官方示例更贴近真实嵌入式项目。2. 树莓派4B上的OpenCV环境与OV5647摄像头初始化2.1 系统镜像、换源与Python虚拟环境树莓派4B 跑这套工程的基准系统最好选择 Raspberry Pi OS Bullseye 或更新版本。我习惯用 Lite 版做视觉项目不带桌面可以减少显存占用让 OpenCV 拿到更多内存做图像处理。刷好系统后第一件事是修改源否则apt install的速度会让人失去耐心。sudo sed -i s|deb.debian.org|mirrors.tuna.tsinghua.edu.cn|g /etc/apt/sources.list sudo apt update sudo apt full-upgrade -y修改源之后不要急着装 OpenCV先用raspi-config打开摄像头接口。注意树莓派5 和树莓派4B 的菜单项略有不同老版本叫Interfacing Options - Camera新系统里集成到Interface Options - Camera/CSI里。开启后重启再用vcgencmd get_camera查看摄像头是否被识别。vcgencmd get_camera # supported1 detected1如果返回detected0检查 FPC 排线方向蓝色标签一定要朝向网口或 USB 口一侧。这里还涉及一个常见误区树莓派修改源只改/etc/apt/sources.list并不够新版系统还要改/etc/apt/sources.list.d/raspi.list也就是 Raspberry Pi 官方软件源的地址。否则安装raspistill等工具时仍然走海外源。2.2 在树莓派上安装Python和OpenCV树莓派4B 自带的 Python 版本可能是 3.9 或 3.11取决于镜像版本。不要直接在系统 Python 里装 OpenCV我一般会建一个专用虚拟环境然后通过 pip 安装预编译好的opencv-python-headless或opencv-python。这套项目里还涉及视频文件读取和写 AVI用带 GUI 的opencv-python更稳妥因为树莓派桌面环境下imshow调试也是有用的。python -m venv --system-site-packages visenv source visenv/bin/activate sudo apt install -y libatlas-base-dev libhdf5-dev libhdf5-103 pip install --upgrade pip pip install opencv-python numpy我把三种安装方式的差异列成一张表方便在课设答辩时解释为什么没用源码编译。安装方式编译时间适用场景风险点apt install python3-opencv0快速验证版本太老DNN 模块可能不完整pip install opencv-python0大多数视觉项目需要系统有正确的动态库依赖源码编译2-4小时需要自定义优化内存不足可能编译失败注意--system-site-packages会让虚拟环境继承系统里已安装的包这样 apt 安装的libcblas、libhdf5都能被 Python 引用避免 OpenCV 导入时报libhdf5.so: cannot open shared object file。如果不想继承系统包也可以直接在系统层面安装但后续换 Python 版本时会很痛苦。2.3 OV5647摄像头模块的初始化与树莓派4B引脚功能图项目里如果接的是树莓派官方 OV5647 摄像头模块在新版系统里要用libcamera-hello来测试。老的raspistill命令在 Bullseye 之后虽然还存在但官方已经不再推荐。libcamera-hello --list-cameras libcamera-still -o test.jpg如果正常输出Available cameras列表就说明/dev/video0已经被驱动。OpenCV 里读取摄像头的方式和普通 USB 摄像头没有区别import cv2 cap cv2.VideoCapture(0, cv2.CAP_V4L2) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) cap.set(cv2.CAP_PROP_FPS, 30)CAP_V4L2是显式指定 Video4Linux2 后端避免 OpenCV 自动选择行为在树莓派上不稳定。如果在调用cap.read()时返回全黑帧大概率是分辨率设置得超出了摄像头支持范围。OV5647 在树莓派4B 上常见模式是 640x480、1280x720 和 2592x1944其中 2592x1944 只能拿到 15fps而且占用内存高不适合跑实时检测。至于树莓派4B 引脚功能图建议把这张图放在手边BCM 编号 18 是 PWM 输出BCM 编号 23/24 通常接 LED 或蜂鸣器。后文做 GPIO 联动时我们用的是 BCM 编号而不是物理引脚号这一点在接线时千万不要搞混。3. MobileNetSSD物体检测Caffe模型在OpenCV DNN模块中的正确打开方式3.1 先搞清楚caffemodel和prototxt压缩包里出现MobileNetSSD_deploy.caffemodel很多人以为有这一个文件就够了其实这是最容易掉的坑。Caffe 模型分两部分prototxt描述网络结构caffemodel存储训练好的权重。OpenCV 的cv2.dnn.readNetFromCaffe需要同时传入这两个文件否则会直接抛出找不到网络层的异常。项目压缩包的文件列表里没有列出deploy.prototxt但工程根目录或子目录里通常会有运行时必须保证它和 caffemodel 放在同一路径下。如果你检查后发现确实缺失可以自己补一个 MobileNetSSD 的标准 deploy 文件百度上能搜到的版本很多但要注意num_classes必须和你想要的检测类别数一致。这个模型默认是在 PASCAL VOC 数据集上训练的能识别 20 类物体包括人、车、猫、狗、瓶子、椅子等。3.2 用OpenCV DNN模块调用MobileNetSSD树莓派4B 的计算力跑 YOLOv4 或 YOLOv8 会比较吃力而 MobileNetSSD 的轻量特性恰好适合它的 ARM Cortex-A72 架构。以下是项目中最核心的推理代码import cv2 import numpy as np # 加载Caffe模型两个文件缺一不可 net cv2.dnn.readNetFromCaffe(deploy.prototxt, MobileNetSSD_deploy.caffemodel) net.setPreferableBackend(cv2.dnn.DNN_BACKEND_OPENCV) net.setPreferableTarget(cv2.dnn.DNN_TARGET_CPU) # MobileNetSSD在VOC数据集上的类别定义 CLASSES [background, aeroplane, bicycle, bird, boat, bottle, bus, car, cat, chair, cow, diningtable, dog, horse, motorbike, person, pottedplant, sheep, sofa, train, tvmonitor] def detect_object(frame, confidence_threshold0.5): h, w frame.shape[:2] # 输入尺寸固定为300x300缩放因子来自原始Caffe部署文档 blob cv2.dnn.blobFromImage(frame, 0.007843, (300, 300), 127.5) net.setInput(blob) detections net.forward() for i in range(detections.shape[2]): confidence detections[0, 0, i, 2] if confidence confidence_threshold: idx int(detections[0, 0, i, 1]) box detections[0, 0, i, 3:7] * np.array([w, h, w, h]) (x1, y1, x2, y2) box.astype(int) cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) label {}: {:.2f}.format(CLASSES[idx], confidence) cv2.putText(frame, label, (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2) return frameblobFromImage的第二个参数0.007843是缩放因子等价于1/127.5配合第三个参数127.5做均值减除这是 MobileNetSSD 官方推理脚本里的标准写法。如果你在树莓派上使用 OpenCV 4.8 以上版本这种写法依然兼容。net.forward()返回的形状是(1, 1, N, 7)其中N是模型输出的候选框数量每行 7 个值分别是 batch 索引、类别索引、置信度、框的归一化坐标。这里用np.array([w, h, w, h])乘以归一化坐标得到的是像素级的矩形框。3.3 推理速度与线程优化树莓派4B 上跑 300x300 输入没有加速棒的情况下OpenCV DNN 后端单帧推理大概需要 300 到 600 毫秒具体看 CPU 散热和当前主频。这个速度意味着只能在 2FPS 左右做全画面的物体检测不可能做到流畅视频。项目里给出的思路是检测和运动识别分开运动检测用实时帧差检测用关键帧。这种做法在工业上叫多级级联先粗后精。实践时我会把detect_object里confidence_threshold设置成 0.4 而不是 0.5因为树莓派上模型对远处小目标的置信度普遍偏低阈值卡得太死容易漏检。但代价是误检也会变多所以在智能小车场景里通常还会加一个“目标中心点是否落在 ROI 区域”的判断。如果你沿着机器视觉学习路线往深处走会发现 OpenCV DNN 模块的 CPU 后端只支持某些层加速很多层还是串行执行的所以单纯调setPreferableBackend或setPreferableTarget并不能让推理获得质的飞越。真正可行的手段是后面要讲的跳帧和双线程而不是执着于模型本身的硬件加速。4. 运动检测与H.264视频流处理帧差法、MOG2和.data落盘4.1 为什么运动检测要单独做物体检测模型再轻量也不可能在树莓派4B 上对每一帧都跑一遍。运动检测则完全在 CPU 上做像素级运算速度快得多而且能捕捉到摄像头视野里“有东西在动”这个事实不关心动的是人还是车。项目里的motion.data和image_rgb.data其实就是为了把运动检测的结果保存下来方便在课程设计报告里做数据复盘。这个思路非常实用先用运动检测找到有变化的帧再用目标检测确认到底是哪一类物体本质上是把计算资源用在刀刃上。4.2 用OpenCV读取highres.h264并做背景减除树莓派摄像头录出来的原始文件多是highres.h264这种纯 H.264 流OpenCV 需要 FFmpeg 后端才能直接读取。如果你的 OpenCV 是pip install opencv-python装的通常已经支持 H.264 解码。下面代码实现了读取 H.264 视频、进行背景减除、并保存运动区域数据import cv2 import numpy as np cap cv2.VideoCapture(highres.h264) backSub cv2.createBackgroundSubtractorMOG2(history500, varThreshold16, detectShadowsTrue) motion_regions [] frame_count 0 while True: ret, frame cap.read() if not ret: break frame_count 1 # 每两帧做一次运动检测 if frame_count % 2 ! 0: continue fg_mask backSub.apply(frame) # 去掉阴影避免把影子当成运动目标 _, fg_mask cv2.threshold(fg_mask, 200, 255, cv2.THRESH_BINARY) contours, _ cv2.findContours(fg_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) for cnt in contours: area cv2.contourArea(cnt) if area 800: x, y, w, h cv2.boundingRect(cnt) motion_regions.append([frame_count, x, y, w, h]) cv2.rectangle(frame, (x, y), (x w, y h), (0, 0, 255), 2) if frame_count % 30 0: print(processed frames:, frame_count) np.save(motion.data, np.array(motion_regions)) cap.release()createBackgroundSubtractorMOG2里的history500指定了用于建模的背景帧数超过 500 帧后模型会自适应更新。varThreshold16是一个很重要的参数它表示像素值与背景模型之间的标准差阈值值越小对光照变化越敏感值越大越容易忽略细小运动。树莓派摄像头如果摆放在室外建议把这个值调到 25 左右否则风吹树叶造成的震动会产生大量无意义轮廓。detectShadowsTrue会产生灰色的阴影区域我后面用threshold(fg_mask, 200, 255, cv2.THRESH_BINARY)把灰色阴影完全滤掉只留下纯白色前景区域。如果你在项目里看到image_rgb.data它通常是用np.save保存了某帧的 RGB 像素矩阵供报告里展示运动区域与 RGB 图像的重叠状态。4.3 H.264解码失败时的兜底方案树莓派4B 上偶尔会遇到cv2.VideoCapture(highres.h264)打开失败但文件真实存在的情况。这通常是 OpenCV 编译时没有启用 FFmpeg 的 H.264 解码器。此时不要急着重装 OpenCV可以用系统自带的ffmpeg把 H.264 转成 MP4再用 OpenCV 读取ffmpeg -i highres.h264 -c:v copy highres.mp4-c:v copy是直接复制视频流不做重新编码所以速度非常快。转换后把代码里的文件名改成highres.mp4即可。如果你的树莓派上连 ffmpeg 都没有可以sudo apt install ffmpeg。另外注意 H.264 裸流没有时间戳OpenCV 读取到的CAP_PROP_FPS可能不准确所以我一般在处理这类文件时不会依赖视频自带帧率而是用系统时间或固定假设帧率为 30FPS。下面是一张运动检测算法的对比表可以帮助在答辩时解释为什么选 MOG2 而不是 KNN算法内存占用阴影处理抗光照变化能力适合场景帧差法极低无弱固定摄像头快速响应MOG2中自动标记阴影中室内人形走动检测KNN高可配置较强复杂背景、树叶晃动这个工程采用 MOG2 是因为树莓派4B 只有 2GB 或 4GB 内存KNN 长期运行会产生更高的内存碎片。在写课程设计报告时这也可以作为“内存优化选型”的一个论证点。5. 从输出AVI到质量验证编码器、跳帧与文件检查5.1 用VideoWriter写AVI文件检测结果要保存成output_01.aviOpenCV 的VideoWriter写法不复杂但编码器选错会导致文件几秒后损坏。在树莓派4B 上我强烈建议使用MJPG编码而不是XVID或MP4V。MJPG是 Motion JPEG 的 AVI 封装CPU 编码压力小对帧率不敏感而且几乎所有播放器都能兼容。out cv2.VideoWriter(output_01.avi, cv2.VideoWriter_fourcc(*MJPG), 25, (width, height)) # width, height 必须是偶数否则编码器会报错VideoWriter的第一个参数是输出路径第二个是 fourcc 编码标识第三个是目标帧率第四个是输出尺寸。如果读取的原始帧尺寸是奇数MJPG 编码器大概率会失败此时要对帧做一次cv2.resize强制改成偶数。另外width和height必须和传入的帧分辨率完全一致不能之前在摄像头里设置了 640x480写 AVI 时却传了 1280x720。这个错误在树莓派上很常见因为cap.get(CAP_PROP_FRAME_WIDTH)返回的是浮点数直接把浮点装箱成 int 没有风险但如果用了整除或者位运算就可能出问题。5.2 跳帧策略与处理速度匹配物体检测每帧需要几百毫秒运动检测很快二者直接串行会导致整个视频处理时间变成原来的三倍以上。项目里通常会加一个跳帧计数让运动检测实时跑物体检测只在奇数帧或每隔 5 帧跑一次。下面是一种折中方案detect_every_n 3 detect_counter 0 while True: ret, frame cap.read() if not ret: break detect_counter 1 if detect_counter % detect_every_n 0: frame detect_object(frame) # 运动检测始终执行 motion_mask backSub.apply(frame) frame cv2.addWeighted(frame, 1, cv2.cvtColor(motion_mask, cv2.COLOR_GRAY2BGR), 0.3, 0) out.write(frame)detect_every_n是一个需要权衡的参数。如果视频里目标运动速度较快跳帧太多会导致目标在检测时已经离开原区域如果目标基本静止跳帧价值就很大。我一般从detect_every_n 3开始测然后看 CPU 占用和检测框密度。树莓派4B 上运行htop能发现四个核心中有一个常年跑满这说明 DNN 推理是单线程的其余核心可以留给运动检测和视频解码。如果你希望进一步压榨性能可以把视频读取放到一个 Pythonthreading.Thread里让它提前把帧读入队列主线程只做检测和写文件。OpenCV 的VideoCapture在大多数 Linux 后端不是线程安全的但将读取操作单独放在一个线程并配合queue.Queue在树莓派上实测能减少 30% 的等待时间。5.3 验证output_01.avi可读性处理完成不等于文件一定有效尤其是树莓派如果突然断电或内存不足AVI 的索引块没有写完播放器会直接打不开。因此处理完必须用脚本验证cap_check cv2.VideoCapture(output_01.avi) if not cap_check.isOpened(): print(AVI file is broken) else: count cap_check.get(cv2.CAP_PROP_FRAME_COUNT) fps cap_check.get(cv2.CAP_PROP_FPS) w int(cap_check.get(cv2.CAP_PROP_FRAME_WIDTH)) h int(cap_check.get(cv2.CAP_PROP_FRAME_HEIGHT)) print(frames:, count, fps:, fps, size:, w, h) for _ in range(int(count)): ret, _ cap_check.read() if not ret: print(read failed at frame, _) break cap_check.release()逐帧读取完后再用ls -l output_01.avi看文件大小是否合理。一个 640x480、30fps、时长 10 秒的 MJPG AVI 通常在 20MB 到 50MB 之间。如果文件只有几百 KB大概率是编码过程中大量丢帧这时需要回头检查二值化阈值和轮廓面积下限因为轮廓过滤条件设得过大会导致很多帧没有任何绘图操作但out.write(frame)仍然会写入所以不会产生零字节文件。这算是一个隐蔽的逻辑问题写报告时可以强调“输出帧率和检测帧率需要分别统计”。6. 进阶用树莓派GPIO联动蜂鸣器和舵机做报警云台6.1 PWM控制舵机与GPIO引脚映射树莓派4B 的 BCM 编号 18 支持硬件 PWM非常适合用来控制 SG90 舵机。把检测到人这一事件转化为舵机转动关键代码如下import RPi.GPIO as GPIO import time SERVO_PIN 18 GPIO.setmode(GPIO.BCM) GPIO.setup(SERVO_PIN, GPIO.OUT) pwm GPIO.PWM(SERVO_PIN, 50) # 50Hz 即20ms周期 pwm.start(7.5) # 1.5ms高电平舵机到中间位置 def set_angle(angle): # 0度~180度映射到0.5ms~2.5ms除以20ms得到占空比 duty 0.5 (angle / 180.0) * 2.0 pwm.ChangeDutyCycle(duty) time.sleep(0.3)舵机在 50Hz 频率下的周期是 20ms高电平时间 1.5ms 对应 90 度也就是中间位置。pwm.start(7.5)里的 7.5 是占空比百分比等于 1.5ms / 20ms。注意 GPIO.PWM 创建的 PWM 实例不能在一段程序里反复创建否则会造成线程资源泄漏正确做法是程序初始化时只创建一次。这个 PWM 波的占空比和频率参数在接线不同时也需要微调。6.2 检测状态LED提示如果你不想用舵机可以接一个全彩 LED 模块将检测结果用颜色体现。树莓派 5 引脚和 4B 存在差异以下代码以 4B 的 BCM 23、24 为例LED_GREEN_PIN 23 LED_RED_PIN 24 GPIO.setup(LED_GREEN_PIN, GPIO.OUT) GPIO.setup(LED_RED_PIN, GPIO.OUT) if person_detected: GPIO.output(LED_GREEN_PIN, GPIO.LOW) GPIO.output(LED_RED_PIN, GPIO.HIGH) else: GPIO.output(LED_GREEN_PIN, GPIO.HIGH) GPIO.output(LED_RED_PIN, GPIO.LOW)这里用高电平点亮红灯的逻辑实际上取决于你的 LED 模块是共阳还是共阴。共阳模块通常需要输出低电平点亮所以最好在设计电路前先查清楚模块原理图。6.3 端到端自检清单下面是尝试扩展此项目时我会给学生的自检清单每一条都能映射到树莓派上的命令行或代码vcgencmd get_camera输出detected1否则检查排线接触。python -c import cv2; print(cv2.__version__)能正常打印否则检查虚拟环境和系统库。ffprobe highres.h264能看到视频流信息确认输入文件没有损坏。libcamera-still -o test.jpg拍出照片且照片不是全黑或全灰。在motion.data所在目录运行python -c import numpy as np; dnp.load(motion.data); print(d.shape)确认落盘数据不是空数组。用ls -l output_01.avi检查输出文件大小再用前面提供的逐帧读取脚本循环验证。跑完这套检查再把 GPIO 引脚按 4B 的 BCM 编号接好运行python3 main.py --source highres.h264观察舵机是否随着画面中出现人或车而转动。调好这一整条链路后你再回头看这个压缩包里的文件名会发现从 H.264 输入到 AVI 输出再到image.data和motion.data每一份文件都有明确的落点工程骨架清晰得可以直接开题。本文还有配套的精品资源点击获取