
简介基于树莓派CSI摄像头模块的Web监控视频推流项目是一套可直接运行的视频监控示例代码重点解决树莓派CSI摄像头在Web端实时预览与推流的常见问题适合正在学习树莓派、Python Flask以及摄像头应用开发的初学者和进阶者也可作为毕业设计、课程设计或工程实训的参考原型。包内共7个文件包含2个Python脚本分别负责Flask服务与摄像头画面采集、1个HTML页面、1个CSS样式表、项目图片、说明文档以及Git忽略配置整体压缩包仅2.1MB结构紧凑便于快速阅读和二次修改。目前已有247人浏览学习。通过该项目读者能够掌握树莓派CSI摄像头的初始化调用、基于Flask的视频流路由设计以及监控页面的完整搭建流程同时借助说明文档中的安装与运行命令可快速在本机或树莓派上启动服务并通过浏览器实时观看监控画面。整套代码逻辑清晰可作为嵌入式Web视频监控方向的项目起点。1. 为什么是 Flask picameraCSI 摄像头 Web 推流的取舍手头有一块闲置的树莓派 4B 和一块 CSI 接口的 OV5647 摄像头模块想做成一个局域网内随时可看的监控推流服务。市面上现成的方案不少但大多依赖 motion、mjpg-streamer直接拿来用虽然省事可一旦想改分辨率、叠加水印、做运动检测就得回头啃 C 源码。用 Flask 搭配 picamera 做 Web 监控视频推流好处是推流逻辑完全由 Python 控制前端只用一个img标签就能渲染实时画面联调起来比编译任何 C 程序都快。这个工程非常适合毕业设计、课程大作业或者第一次想把摄像头和 Web 服务打通的学习者。它不解决录像回放和云存储问题专注做「实时预览」这一件事但也正因为边界清晰它才容易跑通、容易改。2. CSI 摄像头驱动与 picamera 帧捕获原理CSI 摄像头模块在树莓派上并不是即插即用的 USB 摄像头。它通过摄像头串行接口直接连接 SoC 内部的 GPU 信号处理器所以 CPU 参与度低帧数据走专用通道不像 USB 摄像头那样占用 USB 控制器的带宽。树莓派默认内核已经包含驱动但要在/boot/config.txt中开启摄像头start_x1或较新系统用camera_auto_detect1。接线、开启驱动、重启后第一步先验证硬件事物用命令行工具raspistill -o test.jpg抓一张照片如果这一步报错后边 Flask 代码再对也没用。2.1 picamera 初始化分辨率、帧率与曝光picamera.PiCamera是上层封装它调用底层mmalAPI 控制 GPU 摄像头管线。初始化时最关键的三个参数是resolution、framerate和sensor_mode。分辨率决定单帧 JPEG 大小帧率决定每秒出多少帧而sensor_mode会影响摄像头感光元件的裁切方式。常用的是 640x480这个尺寸下带宽需求和延迟都容易接受也方便后期加数字变焦。# camera_pi.py import io import time import picamera class Camera: def __init__(self): self.camera picamera.PiCamera() self.camera.resolution (640, 480) # 宽高 self.camera.framerate 24 # 帧率 self.camera.iso 200 # 感光度 time.sleep(2) # 让自动曝光稳定 def get_frame(self): stream io.BytesIO() self.camera.capture(stream, formatjpeg, use_video_portTrue) stream.seek(0) return stream.read()这段代码是推流服务的底层帧源。capture(stream, formatjpeg, use_video_portTrue)表示从视频端口取一帧编码成 JPEG 后写入内存中的 BytesIO。use_video_portTrue是关键它告诉摄像头使用视频编码的连续输出端口而不是静止图像端口这样帧率高且不会频繁切换模式。iso200让画面亮度稳定如果放在室外强光或夜间低光环境下还需要根据情况调快门速度。time.sleep(2)不能省picamera 在初始化后需要等待一段时间AWB自动白平衡和 AEC自动曝光才会收敛否则前几帧会发暗。2.2 JPEG 编码的参数边界picamera 在捕获 JPEG 时会使用内部的编码器品质可以通过camera.jpeg_quality调整取值范围 1~100默认 85。如果你想降低带宽占用把质量调到 70 以下肉眼几乎看不出区别但单帧体积会明显下降。这里有一个实测过的对照参考分辨率质量单帧 JPEG 体积10fps 时带宽320x24070~8 KB~0.64 Mbps640x48085~35 KB~2.8 Mbps1280x72085~120 KB~9.6 Mbps从表格可以看到720p 的带宽在无线网络下已经接近瓶颈而且编码大分辨率 JPEG 时树莓派 CPU 占用率会显著上升。所以做局域网监控640x480 是性价比最高的起点。帧率再高浏览器端如果只是显示一个img往往因为解码速度跟不上反而显得卡顿不如控制在 15~24fps。2.3 帧捕获的常见坑摄像头被占用一个常见的错误是启动脚本后报错PiCameraMMALError: Failed to enable connection: Out of resources。这通常是因为之前跑过raspistill或者另一个 Python 进程还没有释放摄像头。CSI 摄像头同一时间只能被一个进程持有。解决办法是杀掉正在占用摄像头的进程pkill -f raspistill或者pkill -f picamera。如果是不小心在系统服务里启动了两次就需要用ps aux | grep python排查。另外树莓派的电源质量也直接影响摄像头稳定性如果供电不足初始化可能时好时坏。3. Flask 流式响应实现 MJPEG 监控推流有了凭get_frame()不断产出 JPEG 的摄像头类下一步就是把它变成一个浏览器能持续读取的 HTTP 流。MJPEG 不是一种压缩视频编码它只是把一系列 JPEG 图片连续通过 HTTP multipart 协议传出去。浏览器看到multipart/x-mixed-replace; boundaryframe响应后会不断解析其中的 JPEG 数据并刷新img标签这就是无需任何插件就能实现视频推送的经典方式。3.1 为什么选 MJPEG 而不是 H.264 over HTTPH.264 编码效率更高理论上更适合网络传输但浏览器原生播放 H.264 流需要 HLS、WebRTC 或 MSE 等复杂封装。在树莓派这种性能受限的板子上跑 HLS 分段延迟通常在 2~3 秒以上而 MJPEG 每一帧都是独立图片只要编码完成就能立刻发送延迟可以做到 0.2 秒以内。MJPEG 的代价是带宽较高但局域网内完全够用。很多 NAS 自带的摄像头预览也用的是 MJPEG它简单到任何语言都能解析也方便用 Python 做逐帧处理。3.2 appCam.py 的完整推流实现Flask 的Response对象支持传入一个生成器这个生成器不断向客户端写入字节。下面这段代码就是appCam.py的核心逻辑它把camera_pi.Camera的帧包装成多段 MJPEG 响应体。# appCam.py import camera_pi from flask import Flask, Response, render_template app Flask(__name__) camera camera_pi.Camera() def gen_frames(): while True: try: frame camera.get_frame() except Exception as e: break yield (b--frame\r\n bContent-Type: image/jpeg\r\n bContent-Length: str(len(frame)).encode() b\r\n\r\n frame b\r\n) app.route(/) def index(): return render_template(index.html) app.route(/video_feed) def video_feed(): return Response(gen_frames(), mimetypemultipart/x-mixed-replace; boundaryframe) if __name__ __main__: app.run(host0.0.0.0, port5000, threadedTrue)注意生成器里yield的字节串结构先写分隔符--frame\r\n再写Content-Type和Content-Length空一行后才是 JPEG 二进制数据最后以\r\n结束。这个格式是 MJPEG 的标准封装不能缺少Content-Length否则某些浏览器会无法判断每帧结束位置。boundary参数必须与生成器内的分隔符一致Flask 的Response.mimetype只需要声明边界名实际分隔符由生成器控制。3.3 线程模型与多客户端并发app.run(threadedTrue)让每个请求在独立线程中处理。如果只有一个客户端访问/video_feed生成器会在该连接断开时被销毁。但要小心多个客户端同时访问时每个线程都会调用同一个camera.get_frame()picamera 的capture方法在并发调用时可能存在内部竞争。实践中 2~3 个客户端同时查看通常没有问题因为 picamera 的 capture 过程是在 GPU 管线里排队执行的。如果想要彻底避免线程安全问题可以引入一个锁from threading import Lock camera_lock Lock() def gen_frames(): while True: with camera_lock: frame camera.get_frame() # 后续 yield 不变锁粒度只覆盖帧捕获和编码阶段一旦 JPEG 被读进内存后续网络发送就可以并发。如果尝试不加锁并开到 5 个以上客户端可能会看到偶发的绿色条纹或 OOM 报错这时加锁是最直接的兜底方案。4. 前端展示、延迟测试与参数调优推流服务端就绪以后前端页面反而成为关键短板。index.html只需要一行代码就能把 MJPEG 流挂到页面上img src{{ url_for(video_feed) }}。但浏览器对img的刷新逻辑有自身限制如果网络环境差图片加载可能会中途断开重连。为了减少刷新闪烁建议在页面加载时先设定好图片尺寸避免每帧更新导致页面抖动。4.1 模板结构与静态资源这个项目其实只有一个index.html和一个style.css但模板的写法会影响用户体验。下面是一个可以在树莓派本地运行的模板它同时显示实时画面和预览延迟。!-- templates/index.html -- !DOCTYPE html html head meta charsetUTF-8 titleRPi 监控/title link relstylesheet href{{ url_for(static, filenamestyle.css) }} /head body h1CSI Camera Monitor/h1 div classvideo-wrap img src{{ url_for(video_feed) }} altvideo feed /div p分辨率: {{ resolution }} | 帧率: {{ framerate }} fps/p /body /html对应的appCam.py中渲染模板时传参app.route(/) def index(): return render_template(index.html, resolutioncamera.camera.resolution, frameratecamera.camera.framerate)注意这里直接访问camera.camera得到picamera.PiCamera实例拿到用户当前配置。动态展示这些参数后续调优时就不用 SSH 上去看配置了。style.css里其实只需要控制.video-wrap的最大宽度和居中避免图片把页面撑爆。4.2 延迟测试从摄像头到浏览器到底慢了多久想降低 Web 监控的延迟得先知道延迟出在哪。我常用一个很土但有效的方法把摄像头对准手机的秒表应用然后拍下浏览器画面里秒表的读数与真实时间对比。一次典型测试得到的结果可能是 0.3~0.5 秒下面这张表展示延迟组成阶段典型耗时摄像头曝光JPEG 编码30~80 msFlask 生成器分发5 ms局域网传输2~10 ms浏览器解码渲染40~100 ms所以主要优化目标是前两个阶段。曝光时间在暗光环境会自动拉长可以把shutter_speed固定到 1/60 秒再用较大iso补偿。JPEG 编码耗时会随分辨率指数上升720p 明显比 640x480 慢一倍以上。如果延迟还是高那就降低 framerate 到 15帧率下降并不会改变单帧延迟但能减少传输排队。4.3 常见误用直接把每帧存成文件有的初学者会想先camera.capture(/tmp/frame.jpg)然后用send_file发送再用 JavaScript 轮询。这种做法有两个问题一是频繁写 SD 卡会缩短寿命二是每次轮询创建一个 HTTP 请求带来几十毫秒的额外往返。MJPEG 本质上是长连接请求次数少所以本项目直接从内存读帧返回完全不落盘。如果你想做运动检测、人脸识别再去帧上跑 OpenCV 也不迟但注意不要在gen_frames的 while 循环里做耗时推理否则喂给浏览器的帧间隔会不均匀。正确做法是把推理放到另一个线程用锁保护最新帧推流线程只负责把最近的一帧发出去。5. 进阶开机自启、访问控制与几个排错细节监控服务写完还不算完真正的监控应该断电重启后自己跑起来而且不能裸奔在对局域网内所有设备开放的状态。下面这三个技巧能让这个树莓派监控工程从「能跑」变成「可以放在实验室或家里稳定运行」。5.1 用 systemd 守护推流服务手动 SSH 启动总归不是长久之计。创建一个 systemd 服务文件让appCam.py随系统启动并在崩溃后自动重启。sudo nano /etc/systemd/system/rpicam.service内容如下[Unit] DescriptionRaspberry Pi CSI Camera MJPEG Streamer Afternetwork.target [Service] ExecStart/usr/bin/python3 /home/pi/raspberry-camera/appCam.py WorkingDirectory/home/pi/raspberry-camera Restartalways Userpi [Install] WantedBymulti-user.target执行sudo systemctl daemon-reload sudo systemctl enable --now rpicam即可开机自启。Restartalways意味着即使生成器抛出异常导致进程退出systemd 也会在 5 秒后重新拉起。注意WorkingDirectory必须设置否则模板和静态文件路径会错乱。如果改过 Python 路径用which python3确认。5.2 给监控页面加 Basic Auth内网监控也不是谁都能看。最轻量等级的做法是给 Flask 加上认证装饰器直接复用 HTTP Basic Auth不需要引入额外数据库。下面用functools.wraps实现一个简单拦截from functools import wraps from flask import request, Response def check_auth(username, password): return username admin and password rpi2024 def requires_auth(f): wraps(f) def decorated(*args, **kwargs): auth request.authorization if not auth or not check_auth(auth.username, auth.password): return Response( Authentication required, 401, {WWW-Authenticate: Basic realmVideo Login}) return f(*args, **kwargs) return decorated app.route(/video_feed) requires_auth def video_feed(): return Response(gen_frames(), mimetypemultipart/x-mixed-replace; boundaryframe)注意Basic Auth 的账号密码是明文传输的只在可信局域网里这样用。如果你希望更安全把树莓派放到公网那就应该用 Nginx 做反向代理并启用 TLS这样 Basic Auth 才有意义。5.3 三个排错细节第一Web 页面打开一会儿后图片停止刷新但 CPU 没有打满。这种情况多数是生成器异常退出gen_frames里没有捕获BrokenPipeError当客户端失去连接后下一次yield就会抛出异常。我已经在示例代码里加了except Exception处理正式环境应该把异常日志写进一个文件。第二画面很暗或严重过曝。先检查摄像头镜头的排线是否插紧其次用vcgencmd get_camera确认摄像头被识别最后再调iso和shutter_speed。第三系统更新后摄像头无法打开。树莓派的新内核可能改变了 CSI 驱动在/boot/config.txt里加上dtoverlayov5647通常能解决兼容问题。检查当前曝光和帧率是否生效时直接测一下树莓派的电能状况使用vcgencmd measure_temp看温度是否超过 80°C如果温度异常摄像头也容易间歇性失去响应。本文还有配套的精品资源点击获取