ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

树莓派摄像头视频流实时传输到PC:基于Python与TCP的低延迟方案

树莓派摄像头视频流实时传输到PC:基于Python与TCP的低延迟方案 1. 项目缘起与整体设计思路1.1 为什么会有这个需求手里攒了一块树莓派和几个摄像头模块平时想拿它做点视觉相关的小实验比如定时抓拍、运动检测、远程看护之类的。但实际用起来有个很别扭的地方树莓派本身算力有限跑个轻量推理还行真要实时看画面、做复杂处理还是得把图像数据传到 PC 上用 PC 的算力和大屏幕来处理。于是“树莓派采集、PC 端实时显示和处理”就成了一个很自然的分工模式。这个项目的核心目标很明确用树莓派上的摄像头模块采集视频流通过 Python 把数据实时共享到 PC 端PC 端能够低延迟地接收并显示画面。听起来简单但真做起来从摄像头选型、采集库选择、传输协议到 PC 端解码显示每一步都有坑。我前后折腾了好几套方案最后沉淀出一套相对稳定、延迟可控、代码量也不大的做法这里完整分享出来。适合谁来参考如果你手上有树莓派3B、4B、5 都行和任意一款 CSI 摄像头模块会一点 Python 基础想做一个“采集端 显示端”的实时图像传输小系统那这篇内容基本可以照着抄。哪怕你之前没碰过 picamera只要跟着步骤走也能跑通。1.2 整体架构怎么定先把这个系统的骨架说清楚不然后面细节容易迷。整个链路分三段采集端树莓派摄像头模块通过 CSI 排线接到树莓派用 Python 的 picamera 库或新版 picamera2读取帧数据。传输层把帧数据通过网络从树莓派发到 PC。这里我选的是 TCP socket而不是 HTTP 或 RTSP。显示端PCPC 上用 Python 接收数据解码后用 OpenCV 显示。为什么是 TCP 而不是别的我试过几种方案延迟实现难度稳定性适用场景TCP Socket 裸传低低高局域网实时传输HTTP MJPEG中低中浏览器直接看RTSP 推流低高高多客户端、专业场景UDP 裸传极低中低丢包对丢包不敏感局域网内、单客户端、要低延迟又要简单TCP 是最平衡的选择。UDP 虽然延迟更低但丢包后画面会花还得自己做重传或纠错得不偿失。RTSP 要装额外的流媒体服务配置复杂对只想快速跑通的人不友好。HTTP MJPEG 延迟偏高而且每帧都要走 HTTP 头效率一般。所以最终方案就是picamera 采集 → JPEG 编码 → TCP 发送 → PC 接收 → OpenCV 解码显示。整条链路清晰代码量小延迟在局域网里能压到 100ms 以内。1.3 摄像头模块与库的选择树莓派能用的摄像头模块不少常见的有 OV5647500万像素老款、IMX219800万像素官方 V2、IMX4771200万像素HQ Camera。我手头用的是 OV5647便宜、够用做实时传输 640x480 或 1280x720 完全没问题。库的选择上有个分水岭老版 picamera 和新版 picamera2。老版 picamera 基于旧的相机栈在 Raspberry Pi OS Bullseye 之前的系统上很稳新版 picamera2 是官方主推的基于 libcamera支持新系统和新硬件。如果你用的是树莓派 5 或者较新的系统picamera2 是唯一选择如果是树莓派 4B 跑老系统picamera 也能用。我这里以 picamera老版为主线讲因为它的 API 更简单直观适合快速上手同时会在关键处说明 picamera2 的差异方便你用新系统时切换。提示树莓派 5 上老版 picamera 已经无法正常工作必须用 picamera2。如果你是新买的树莓派 5直接看 picamera2 的部分。2. 环境准备与核心细节解析2.1 树莓派端环境搭建先把树莓派系统搞定。烧录系统这一步不展开用官方 Imager 工具选 Raspberry Pi OS32 位或 64 位都行烧到 TF 卡插卡开机连上网。开机后第一件事是更新源和系统sudo apt update sudo apt upgrade -y然后启用摄像头接口。老系统用raspi-configsudo raspi-config进去后选Interface Options→Camera→ 启用然后重启。新系统Bullseye 之后默认就支持 libcamera不需要单独启用但建议确认一下libcamera-hello --list-cameras如果能看到摄像头信息说明硬件识别正常。这一步很关键很多人卡在“摄像头没识别”上其实是排线没插好或者接口没启用。CSI 排线插的时候注意方向金属触点朝向网口那一侧树莓派 4B插反了识别不到。接下来装 Python 依赖。老版 picamera 直接 pip 装sudo apt install python3-pip -y pip3 install picamera新版 picamera2 一般系统自带如果没有sudo apt install python3-picamera2 -yPC 端需要 OpenCV 和 numpypip install opencv-python numpy2.2 为什么用 JPEG 而不是原始帧这里有个关键决策传输的时候传什么格式。摄像头采集出来的原始数据是 YUV 或 RGB一帧 640x480 的 RGB 数据是 640×480×3 921600 字节接近 1MB。如果按 30fps 传带宽要 27MB/s局域网虽然扛得住但编码解码开销大而且没必要。JPEG 压缩后同样一帧 640x480 的图质量设 80 左右大概 30-50KB压缩比 20 倍以上。带宽降到 1-1.5MB/s延迟也低。而且 OpenCV 和 picamera 都原生支持 JPEG 编解码不用额外装库。代价是每次编解码有 CPU 开销但树莓派的 GPU 其实能硬件编码 JPEGpicamera 的capture方法在输出到流时会自动利用硬件加速实际 CPU 占用并不高。PC 端解码 JPEG 更是小菜一碟。所以格式定为采集端编码成 JPEG传输 JPEG 字节流PC 端解码 JPEG。这是整个方案里最省事又高效的选择。2.3 传输协议的数据帧设计TCP 是字节流协议没有消息边界。如果直接发 JPEG 数据接收端不知道一帧从哪开始、到哪结束。所以必须自己定一个简单的帧协议。我的做法是每帧前面加一个固定长度的头部头部里包含这一帧的长度。接收端先读固定长度的头部解析出长度再按长度读满整帧数据。头部设计成 4 字节的大端无符号整数struct 格式I表示后续 JPEG 数据的字节数。这样一帧的传输结构就是[4字节长度][N字节JPEG数据][4字节长度][N字节JPEG数据]...为什么用 4 字节因为 JPEG 帧最大可能到几 MB2 字节最大 65535不够用4 字节最大能表示 4GB绰绰有余。为什么用大端网络字节序就是大端Python 的struct.pack(I, n)直接搞定跨平台不会有歧义。这个设计简单到不能再简单但足够可靠。接收端用recv循环读满 4 字节再循环读满 N 字节就不会出现半帧的问题。注意socket.recv(n)不保证一次读满 n 字节可能只返回一部分。必须写一个recv_all辅助函数循环读取这是新手最容易踩的坑。3. 实操过程与核心环节实现3.1 树莓派端采集与发送代码先上完整代码再逐段解释。保存为pi_server.pyimport socket import struct import time from picamera import PiCamera from io import BytesIO # 配置 HOST 0.0.0.0 PORT 8888 FRAME_RATE 30 RESOLUTION (640, 480) JPEG_QUALITY 80 def main(): # 初始化摄像头 camera PiCamera() camera.resolution RESOLUTION camera.framerate FRAME_RATE # 让摄像头预热自动曝光稳定需要时间 time.sleep(2) # 创建 TCP 服务端 server socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server.bind((HOST, PORT)) server.listen(1) print(f等待 PC 连接端口 {PORT}...) conn, addr server.accept() print(fPC 已连接: {addr}) stream BytesIO() try: for _ in camera.capture_continuous(stream, formatjpeg, qualityJPEG_QUALITY, use_video_portTrue): # 获取当前帧数据 frame_data stream.getvalue() # 发送长度头 数据 header struct.pack(I, len(frame_data)) conn.sendall(header frame_data) # 清空流准备下一帧 stream.seek(0) stream.truncate() except (BrokenPipeError, ConnectionResetError): print(PC 断开连接) finally: conn.close() server.close() camera.close() if __name__ __main__: main()几个关键点解释一下。camera.capture_continuous是 picamera 提供的连续采集方法它会不断往流里写 JPEG 数据配合use_video_portTrue使用视频端口帧率更稳。每次循环拿到的是当前帧的完整 JPEG 字节。stream.seek(0)和stream.truncate()是必须的否则流会越积越大内存爆掉。这个坑我踩过跑了几分钟树莓派就卡死了一查是 BytesIO 没清空。conn.sendall而不是conn.send因为send可能只发一部分sendall会保证全部发完。头部和数据拼在一起发减少系统调用次数。SO_REUSEADDR是为了重启程序时端口能立刻复用不然会报“Address already in use”。3.2 PC 端接收与显示代码PC 端保存为pc_client.pyimport socket import struct import cv2 import numpy as np HOST 192.168.1.100 # 改成树莓派的 IP PORT 8888 def recv_all(sock, n): 循环读取直到读满 n 字节 data b while len(data) n: packet sock.recv(n - len(data)) if not packet: return None data packet return data def main(): client socket.socket(socket.AF_INET, socket.SOCK_STREAM) client.connect((HOST, PORT)) print(f已连接到树莓派 {HOST}:{PORT}) try: while True: # 先读 4 字节长度头 header recv_all(client, 4) if header is None: print(连接断开) break frame_len struct.unpack(I, header)[0] # 再读整帧数据 frame_data recv_all(client, frame_len) if frame_data is None: print(连接断开) break # 解码 JPEG np_arr np.frombuffer(frame_data, dtypenp.uint8) frame cv2.imdecode(np_arr, cv2.IMREAD_COLOR) if frame is not None: cv2.imshow(Raspberry Pi Camera, frame) # 按 q 退出 if cv2.waitKey(1) 0xFF ord(q): break finally: client.close() cv2.destroyAllWindows() if __name__ __main__: main()recv_all是核心辅助函数解决 TCP 粘包和半包问题。struct.unpack(I, header)解析出长度注意格式要和发送端一致都是大端无符号整数。np.frombuffer把字节转成 numpy 数组cv2.imdecode解码成图像矩阵。这里用imdecode而不是imread因为数据在内存里不在文件里。cv2.waitKey(1)里的 1 表示等待 1ms保证窗口能刷新。如果设成 0 会阻塞画面就不动了。3.3 运行与联调步骤先把树莓派和 PC 连到同一个局域网查树莓派 IPhostname -I假设输出192.168.1.100把它填到 PC 端代码的HOST里。先在树莓派上跑服务端python3 pi_server.py看到“等待 PC 连接”后在 PC 上跑客户端python pc_client.py如果一切正常PC 上会弹出一个窗口显示树莓派摄像头的实时画面。按 q 退出。第一次跑可能会遇到防火墙拦截Windows 上要允许 Python 通过防火墙或者临时关掉防火墙测试。Linux 上检查ufw状态。3.4 参数调优与延迟测量跑通之后下一步是调延迟和画质。影响延迟的主要参数参数影响建议值分辨率越大延迟越高640x480 起步帧率越高越流畅但占带宽15-30JPEG 质量越高画质好但数据大70-85网络有线优于无线尽量有线我实测下来640x480、30fps、质量 80在千兆有线局域网里延迟大概 60-80ms肉眼几乎感觉不到。换成 WiFi 会涨到 100-150ms偶尔有波动。如果对延迟极敏感降到 320x240、质量 60能压到 40ms 左右但画质就一般了。测延迟有个土办法把树莓派摄像头对着 PC 屏幕上的秒表PC 端显示的画面里秒表和真实秒表的差值就是端到端延迟。这个方法直观不用写额外代码。4. 常见问题与排查技巧实录4.1 摄像头相关的问题问题一picamera.exc.PiCameraError: Camera is not enabled这是最常见的问题原因是摄像头接口没启用。老系统跑sudo raspi-config启用 Camera新系统确认libcamera-hello能出画面。如果还不行检查 CSI 排线是否插紧、方向是否正确。排线插反是高频错误金属面朝向要对着网口方向。问题二画面偏色或全黑OV5647 这类模块需要预热自动曝光和白平衡要几秒钟才稳定。代码里time.sleep(2)就是干这个的。如果一直全黑可能是镜头盖没摘真有这种事或者光线太暗。可以手动设camera.awb_mode和camera.exposure_mode来固定参数。问题三树莓派 5 上 picamera 报错树莓派 5 不支持老版 picamera必须换 picamera2。picamera2 的采集代码结构不同from picamera2 import Picamera2 import cv2 picam2 Picamera2() config picam2.create_video_configuration(main{size: (640, 480)}) picam2.configure(config) picam2.start() while True: frame picam2.capture_array() # frame 是 RGB numpy 数组可以直接编码 _, jpeg cv2.imencode(.jpg, frame, [cv2.IMWRITE_JPEG_QUALITY, 80]) data jpeg.tobytes() # 后续发送逻辑一样注意 picamera2 返回的是 numpy 数组需要自己用 OpenCV 编码成 JPEG不像老版 picamera 直接输出 JPEG 流。4.2 网络传输的问题问题一画面卡顿、延迟越来越大多半是接收端读得比发送端慢数据在缓冲区堆积。检查 PC 端解码和显示是否耗时过长可以关掉imshow测试纯接收速度。另外确认树莓派端stream有没有正确清空没清空会导致每帧数据越来越大。问题二ConnectionResetError或BrokenPipeErrorPC 端异常退出后树莓派端还在发数据就会报这个。代码里已经用 try-except 捕获了捕获后关闭连接即可。如果想更健壮可以加个重连机制让树莓派端 accept 循环等待新连接。问题三只能连一次断开后要重启树莓派程序因为server.accept()只调用了一次。改成循环while True: conn, addr server.accept() print(fPC 已连接: {addr}) try: # 发送循环 except (BrokenPipeError, ConnectionResetError): print(PC 断开等待重连) finally: conn.close()这样 PC 端断开后树莓派端会自动回到 accept 状态等下一次连接。4.3 性能与稳定性问题问题一树莓派 CPU 占用高JPEG 编码如果走 CPU 会很吃资源。picamera 的use_video_portTrue会尽量用 GPU 编码但如果分辨率高还是会有压力。降低分辨率或帧率是最直接的办法。另外确认没有同时跑其他吃 CPU 的任务。问题二长时间运行后内存泄漏主要是 BytesIO 没清空或者 OpenCV 窗口没释放。确保每帧后stream.seek(0); stream.truncate()退出时cv2.destroyAllWindows()。可以用htop监控树莓派内存跑几小时看有没有持续增长。问题三WiFi 下画面偶尔花屏WiFi 丢包或抖动导致。TCP 本身会重传但重传期间画面会卡一下。如果对流畅度要求高尽量用有线或者降低码率减小单帧数据量降低对网络瞬时带宽的要求。4.4 常见问题速查表现象可能原因排查方向摄像头不识别排线/接口未启用检查排线方向raspi-config画面全黑未预热/镜头盖/光线加 sleep检查镜头连接被拒绝IP/端口错/防火墙确认 IP关防火墙测试画面卡顿缓冲区堆积/解码慢检查流清空关显示测试延迟高分辨率高/WiFi降分辨率用有线内存增长BytesIO 未清空加 seek/truncate断开后无法重连accept 只调一次改循环 accept5. 进阶扩展与个人经验5.1 从单帧传输到多客户端现在的架构是单服务端单客户端。如果想多个 PC 同时看可以在树莓派端维护一个客户端列表每帧发给所有已连接的客户端。用select或threading处理多连接。不过要注意客户端越多树莓派发送压力越大帧率可能要降。一个更省资源的做法是树莓派只发一份数据给一个中转节点比如另一台性能好的机器由中转节点分发给多个客户端。但这超出了本文范围知道有这个思路就行。5.2 在 PC 端做实时处理传输只是第一步PC 端拿到画面后可以做很多事。比如接 OpenCV 的人脸检测、运动检测或者接 YOLO 做目标识别。因为数据已经是 numpy 数组直接喂给模型就行。我试过在 PC 端跑一个轻量的人脸检测帧率还能保持在 20fps 以上树莓派完全不受影响这就是分工的好处。如果要做录像可以在 PC 端用cv2.VideoWriter把帧写进视频文件或者按时间戳存 JPEG 序列。存 JPEG 序列更灵活后期处理方便。5.3 几个我踩过的坑第一个坑是忘了清空 BytesIO跑了五分钟树莓派就卡死一开始以为是网络问题查了半天才发现是内存爆了。这个错误很隐蔽因为短时间测试看不出来。第二个坑是recv 没读满。早期版本我直接client.recv(frame_len)结果偶尔画面解码失败因为 recv 返回的字节数不够。后来加了recv_all循环读取才解决。TCP 是流协议这个特性新手一定要记牢。第三个坑是树莓派 5 上直接套用老代码。我习惯性用 picamera结果在树莓派 5 上死活跑不起来报的错还很模糊。后来查文档才知道树莓派 5 只支持 picamera2API 完全不一样。换硬件的时候一定要确认库的兼容性。第四个坑是WiFi 下延迟波动大。同样的代码有线稳定在 70msWiFi 能到 150ms 还偶尔卡顿。如果做实时性要求高的项目有线是刚需别省那根网线。5.4 后续可以怎么扩展这套东西跑通后可以往上叠很多功能。比如加个 Web 界面用 Flask 把画面推到浏览器这样手机也能看或者加个控制通道PC 端发指令让树莓派调整分辨率、帧率、拍照再或者接个云台PC 端用键盘控制摄像头转动。我个人觉得最有价值的扩展是双向通信。现在的架构是单向的树莓派只管发。如果加一条反向通道PC 端可以发命令给树莓派比如“开始录像”“切换分辨率”“触发拍照”整个系统就从“看”变成了“控”实用性提升一大截。实现上也不难在同一个 socket 上双向读写就行但要注意收发线程分离避免阻塞。最后分享一个小技巧调试的时候先在树莓派本地用raspistill -o test.jpg拍一张确认摄像头本身没问题再跑传输代码。这样能把“摄像头问题”和“网络问题”分开排查省很多时间。很多人一上来就跑完整链路出错了不知道是哪一段的问题分层排查是最高效的调试策略。
返回列表