ARTICLE DETAIL

资讯详情

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

树莓派与PC摄像头数据共享:Python+picamera实现MJPEG over TCP实时传输

树莓派与PC摄像头数据共享:Python+picamera实现MJPEG over TCP实时传输 1. 项目缘起与整体设计思路1.1 为什么要在树莓派和PC之间做摄像头数据共享手里有一块树莓派和几个摄像头模块想让它拍到的画面实时出现在PC屏幕上这个需求听起来简单但真动手做的时候会发现路径选择挺多。有人用RTSP推流有人用MJPG-Streamer有人直接上OpenCV的VideoCapture配合网络传输。我前后试过四五种方案最后稳定跑下来的还是Python加picamera这条路线原因很直接代码量少、依赖清晰、延迟可控而且树莓派端几乎不占什么CPU。这个项目的本质是把树莓派当作一个网络摄像头服务器PC端作为客户端去拉取视频帧。树莓派负责采集图像并通过网络发送PC负责接收并显示。听起来像是“视频通话”的简化版只不过一端是固定机位另一端是观看端。适合做远程监控、延时摄影预览、机器视觉项目的图像采集前端也适合刚接触树莓派和Python网络编程的朋友拿来练手。我选择picamera这个库而不是直接调OpenCV的cv2.VideoCapture是因为picamera是树莓派官方摄像头模块的原生Python接口对硬件层的控制更细能直接设置分辨率、帧率、曝光模式、白平衡等参数而且它底层走的是GPU的ISP管线CPU占用极低。相比之下用OpenCV去读树莓派摄像头很多时候底层还是调用了picamera或者V4L2多了一层封装反而容易出兼容性问题。1.2 整体架构与数据流向整个系统的数据流向可以拆成四段摄像头采集、树莓派端编码、网络传输、PC端解码显示。树莓派端的picamera负责第一段和第二段它把摄像头传感器输出的RAW数据经过ISP处理后编码成H.264或者MJPEG格式。网络传输这一段我用的是TCP socket因为TCP有重传机制在局域网内丢包率极低画面不会出现花屏或者撕裂。PC端收到数据后用OpenCV的imdecode把JPEG帧解出来再用imshow显示。这里有一个关键选择传输MJPEG还是H.264。MJPEG的本质是一帧一帧的JPEG图片连续发送每一帧都是独立的解码简单延迟低但带宽占用高。H.264是帧间压缩带宽省很多但需要解码器而且如果网络抖动导致丢帧后续画面会花一段时间。我实测在局域网内640x480分辨率、30帧的情况下MJPEG大概占用10-15Mbps带宽千兆局域网完全无压力。如果你要跑在WiFi上或者带宽受限可以考虑H.264但代码复杂度会上升。我最终选的是MJPEG over TCP因为它的实现最直接PC端不需要装额外的解码器OpenCV直接就能处理。而且MJPEG的每一帧都是完整的即使中间丢了几帧后面的画面照样能正常显示不会出现H.264那种“一丢丢一串”的问题。1.3 方案选型的几个关键考量在动手之前我对比了几种常见方案。第一种是树莓派端跑一个HTTP服务器PC端用浏览器看这种方案最简单但延迟通常在1-2秒而且浏览器端不好做后续的图像处理。第二种是RTSP推流用ffmpeg或者gstreamer延迟能压到200毫秒以内但配置复杂树莓派端CPU占用也高。第三种就是我现在用的TCP socket直传延迟在100-300毫秒之间代码可控PC端拿到的是原始帧数据方便后续接OpenCV做识别或者处理。还有一个考量是树莓派的型号。我手头有树莓派4B和树莓派5两个都试过。树莓派4B跑640x48030fps的MJPEG传输CPU占用大概在15%-20%树莓派5因为CPU更强占用降到8%左右。如果你用的是树莓派Zero或者更老的型号建议把分辨率降到320x240帧率降到15fps否则CPU会成为瓶颈。摄像头模块方面我用的是一块OV5647这是树莓派早期官方摄像头模块的传感器500万像素支持1080p30fps。picamera对这个模块的支持很成熟基本不需要额外配置。如果你用的是IMX219或者IMX477picamera也能直接识别但需要在代码里确认一下传感器型号因为不同传感器的最大分辨率和帧率组合不一样。2. 环境准备与核心细节解析2.1 树莓派端的系统配置与依赖安装树莓派端的系统我推荐用Raspberry Pi OS Lite版本不带桌面环境省资源。如果你已经装了带桌面的版本也没关系把桌面关掉或者不启动就行。系统烧录这一步我就不展开了网上教程很多重点说一下烧录完之后要做的几件事。第一件事是开启摄像头接口。在Raspberry Pi OS的较新版本里摄像头接口默认是关闭的需要手动开启。你可以用raspi-config命令进入Interface Options找到Camera选择Enable。或者直接编辑/boot/config.txt确保里面有start_x1这一行如果没有就加上然后重启。第二件事是更新软件源和安装picamera。树莓派的默认源在国内访问速度一般建议换成国内镜像源。编辑/etc/apt/sources.list把里面的raspbian.raspberrypi.org替换成你常用的镜像地址。然后执行sudo apt update sudo apt install python3-picamera python3-opencv -y这里我同时装了python3-opencv虽然树莓派端主要用picamera采集但有时候需要做一点简单的图像处理比如画个时间戳或者缩放一下OpenCV用起来方便。如果你只想做纯传输不装OpenCV也行能省几十兆空间。第三件事是确认摄像头能被识别。执行vcgencmd get_camera如果输出supported1 detected1说明摄像头已经正常连接。如果detected0检查一下排线是不是插反了树莓派的摄像头排线是金手指朝向HDMI接口那一侧插反了不会烧但识别不到。注意树莓派5的摄像头接口变成了两个MIPI接口排线规格和树莓派4B不一样买摄像头模块的时候要确认附带的是15pin还是22pin的排线。我一开始用树莓派4B的排线插树莓派5插不进去后来单独买了22pin转15pin的转接线才搞定。2.2 PC端的Python环境与依赖PC端我用的Python 3.9太老的版本比如3.6也能跑但OpenCV的某些函数可能不支持。建议用3.8以上的版本。依赖就两个opencv-python和numpy。如果你用的是Anaconda直接conda install opencv就行。如果用pip执行pip install opencv-python numpy这里有一个坑opencv-python有多个版本opencv-python是完整版opencv-python-headless是不带GUI的版本。PC端要显示画面必须装完整版否则cv2.imshow会报错。我一开始图省事装了headless版本结果调试了半天才发现是版本问题。PC端的网络配置要注意防火墙。Windows Defender默认会拦截入站的TCP连接如果你在PC上跑服务端、树莓派主动连过来需要在防火墙里放行对应的端口。我这里是PC主动连树莓派所以树莓派端不需要额外配置防火墙PC端的出站连接默认是允许的。2.3 picamera的核心参数与采集原理picamera的采集流程是这样的摄像头传感器输出Bayer格式的RAW数据树莓派的GPU里的ISP图像信号处理器把RAW数据转换成YUV或者RGB然后picamera的编码器把YUV数据压缩成JPEG或者H.264。整个过程CPU几乎不参与所以树莓派的CPU占用很低。picamera有几个关键参数需要理解。resolution是采集分辨率注意这个分辨率是摄像头传感器输出的分辨率不是你最终看到的分辨率。framerate是帧率picamera支持的最大帧率和分辨率有关比如1080p最高30fps720p最高60fps640x480可以到90fps。format是输出格式MJPEG传输的话用jpeg如果要做后续处理可以用bgr直接输出numpy数组。还有一个参数是use_video_port。picamera有两个端口still port和video port。still port适合拍单张高分辨率照片video port适合连续采集视频。做实时传输必须用video port否则帧率上不去。设置方式是camera.start_recording(..., use_video_portTrue)或者在capture的时候指定。我实测下来640x48030fps的MJPEG每帧JPEG大小大概在20-30KB换算成带宽是5-7Mbps。如果你把分辨率提到1280x720每帧大概60-80KB带宽需求翻倍到15-20Mbps。所以在WiFi环境下640x480是比较稳妥的选择。2.4 网络传输协议的选择与参数调优TCP socket的默认参数对实时视频传输来说不是最优的。TCP有Nagle算法会把小包攒在一起发送这对视频流来说会增加延迟。我一般会在树莓派端设置TCP_NODELAY禁用Nagle算法让每一帧数据立即发送。import socket server_socket socket.socket(socket.AF_INET, socket.SOCK_STREAM) server_socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)另一个参数是发送缓冲区大小。默认的SO_SNDBUF可能只有几十KB对于大帧来说不够用会导致发送阻塞。我一般设置成256KB或者512KBserver_socket.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 512 * 1024)PC端的接收缓冲区也要相应调大否则接收端处理不过来的时候会丢数据。设置方式和发送端类似把SO_SNDBUF换成SO_RCVBUF。还有一个细节是帧的分隔。TCP是字节流没有消息边界所以你需要自己定义帧的分隔方式。我用的方案是在每帧数据前面加一个固定长度的头部头部里包含帧的长度信息。PC端先读固定长度的头部解析出帧长度再读对应长度的数据。这样即使TCP把多个帧粘在一起PC端也能正确拆分。3. 实操过程与核心环节实现3.1 树莓派端摄像头采集与TCP发送的完整实现树莓派端的代码我拆成三个部分初始化摄像头、建立TCP服务、循环采集发送。先看初始化部分import socket import struct import picamera import time # 摄像头初始化 camera picamera.PiCamera() camera.resolution (640, 480) camera.framerate 30 camera.rotation 0 camera.brightness 50 camera.contrast 0 camera.saturation 0 camera.awb_mode auto camera.exposure_mode auto time.sleep(2) # 等待摄像头稳定这里time.sleep(2)很重要。picamera初始化之后自动曝光和自动白平衡需要几秒钟才能收敛如果立刻开始采集前几帧会偏暗或者偏色。我一般等2秒如果环境光线变化大可以等3秒。接下来是TCP服务端HOST 0.0.0.0 PORT 8000 server_socket socket.socket(socket.AF_INET, socket.SOCK_STREAM) server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server_socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 512 * 1024) server_socket.bind((HOST, PORT)) server_socket.listen(1) print(f等待PC连接端口 {PORT}...) conn, addr server_socket.accept() print(fPC已连接: {addr})SO_REUSEADDR是为了让程序重启后能立刻绑定端口不然会报“Address already in use”。listen(1)表示只允许一个客户端连接因为视频流通常是一对一的。然后是采集发送循环try: for frame in camera.capture_continuous( conn.makefile(wb), formatjpeg, use_video_portTrue, quality70 ): pass # capture_continuous会直接把数据写入文件对象 except Exception as e: print(f传输中断: {e}) finally: conn.close() server_socket.close() camera.close()这里我用了capture_continuous配合conn.makefile(wb)picamera会直接把JPEG数据写入socket的文件对象。这种方式最简单但有一个问题它没有帧长度头部PC端需要自己找JPEG的起始标记0xFFD8和结束标记0xFFD9来拆分帧。如果网络稳定这种方式没问题但如果出现丢包或者粘包解析会出错。更稳妥的方式是手动控制每一帧的发送import io stream io.BytesIO() for _ in camera.capture_continuous(stream, formatjpeg, use_video_portTrue, quality70): stream.seek(0) frame_data stream.read() # 发送帧长度4字节大端整数 conn.sendall(struct.pack(I, len(frame_data))) # 发送帧数据 conn.sendall(frame_data) stream.seek(0) stream.truncate()这种方式每一帧都有明确的长度头部PC端解析起来不会出错。struct.pack(I, len(frame_data))把帧长度打包成4字节的大端整数PC端用struct.unpack(I, header)解出来。quality70是JPEG压缩质量70在画质和带宽之间比较平衡如果你想要更清晰的画面可以调到85但带宽会增加30%左右。实操心得capture_continuous的循环里不要做耗时操作否则会丢帧。我一开始在循环里加了一个打印帧率的语句结果帧率从30掉到15因为print本身有IO开销。后来改成每100帧打印一次就正常了。3.2 PC端接收、解码与显示的完整实现PC端的代码也分三部分连接树莓派、接收帧数据、解码显示。先看连接部分import socket import struct import cv2 import numpy as np HOST 192.168.1.100 # 树莓派的IP地址 PORT 8000 client_socket socket.socket(socket.AF_INET, socket.SOCK_STREAM) client_socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) client_socket.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 512 * 1024) client_socket.connect((HOST, PORT)) print(f已连接到树莓派 {HOST}:{PORT})树莓派的IP地址可以在树莓派上执行hostname -I查看或者在路由器管理界面里找。建议在路由器里给树莓派设置静态IP不然每次重启IP变了都要改代码。接收帧数据的函数def recv_exact(sock, n): 从socket精确接收n个字节 data b while len(data) n: packet sock.recv(n - len(data)) if not packet: return None data packet return data def receive_frame(sock): 接收一帧数据 header recv_exact(sock, 4) if header is None: return None frame_len struct.unpack(I, header)[0] frame_data recv_exact(sock, frame_len) return frame_datarecv_exact这个函数很关键。TCP的recv不保证一次返回你请求的字节数可能只返回一部分。所以需要循环接收直到凑够需要的字节数。我一开始直接用sock.recv(4)读头部结果有时候只读到2个字节解析出来的帧长度是错的画面就乱了。解码和显示部分try: while True: frame_data receive_frame(client_socket) if frame_data is None: print(连接断开) break # 把JPEG数据解码成numpy数组 frame_array np.frombuffer(frame_data, dtypenp.uint8) frame cv2.imdecode(frame_array, cv2.IMREAD_COLOR) if frame is None: continue cv2.imshow(Raspberry Pi Camera, frame) if cv2.waitKey(1) 0xFF ord(q): break except KeyboardInterrupt: print(用户中断) finally: client_socket.close() cv2.destroyAllWindows()cv2.imdecode把JPEG字节流解码成BGR格式的numpy数组cv2.imshow显示出来。cv2.waitKey(1)是必须的它让OpenCV处理窗口事件如果不加这一行窗口会卡死。按q键退出。注意cv2.imdecode返回的可能是None如果JPEG数据不完整或者损坏。我在代码里加了一个判断如果解码失败就跳过这一帧继续接收下一帧。这样即使偶尔丢了一帧程序也不会崩溃。3.3 延迟优化与帧率实测延迟是实时视频传输的核心指标。我实测下来从树莓派摄像头采集到PC端显示端到端延迟大概在150-250毫秒之间。这个延迟主要来自四个部分摄像头曝光和ISP处理约30-50ms、JPEG编码约10-20ms、网络传输约5-10ms、PC端解码和显示约20-30ms。如果你觉得延迟还是高可以尝试以下几个优化。第一降低分辨率。640x480的延迟比1280x720低大概50毫秒因为编码和解码的数据量小了。第二提高JPEG压缩质量参数里的“质量”值反而会增加延迟因为压缩率低了数据量大了。我一般用quality70如果网络带宽充足可以用quality50延迟会再降一点。第三PC端用cv2.imshow显示的时候窗口大小不要超过原始分辨率否则OpenCV会做缩放增加几毫秒的延迟。帧率方面树莓派端设置30fpsPC端实际接收到的帧率大概在28-30fps之间偶尔会掉到25fps。掉帧的原因通常是WiFi信号波动或者PC端CPU被其他程序占用。如果你对帧率稳定性要求高建议用有线网络连接树莓派和PC有线网络的丢包率和抖动都比WiFi小很多。我用树莓派4B和树莓派5分别跑过这个方案。树莓派4B在640x48030fps下CPU占用约18%内存占用约12%。树莓派5的CPU占用约8%内存占用约10%。两个型号都能稳定跑几个小时不崩溃。如果你用树莓派Zero 2 W建议把分辨率降到320x240帧率降到15fps否则CPU会满载。3.4 多客户端与断线重连的处理上面的代码只支持一个PC客户端。如果你想让多个PC同时看树莓派的画面需要改成多线程或者用select/epoll做IO多路复用。我试过用多线程的方式每个客户端一个线程树莓派端采集一帧就发给所有客户端。这种方式在客户端数量少2-3个的时候没问题客户端多了之后树莓派端的网络发送会成为瓶颈。断线重连是另一个需要处理的问题。如果PC端程序崩溃或者网络中断树莓派端的sendall会抛出异常程序就退出了。我一般会在树莓派端加一个外层循环捕获异常后重新等待连接while True: try: conn, addr server_socket.accept() print(fPC已连接: {addr}) stream io.BytesIO() for _ in camera.capture_continuous(stream, formatjpeg, use_video_portTrue, quality70): stream.seek(0) frame_data stream.read() conn.sendall(struct.pack(I, len(frame_data))) conn.sendall(frame_data) stream.seek(0) stream.truncate() except Exception as e: print(f连接断开: {e}) try: conn.close() except: pass continue这样即使PC端断开树莓派端也能继续等待下一个连接。PC端也加一个重连逻辑连接失败后每隔几秒重试一次。4. 常见问题与排查技巧实录4.1 摄像头识别不到或画面全黑这是最常见的问题。首先检查排线是否插紧树莓派的摄像头排线插座是翻盖式的要把黑色卡扣拔起来插入排线再按下去。排线的金手指朝向HDMI接口那一侧插反了虽然不会烧但识别不到。然后执行vcgencmd get_camera如果detected0说明硬件层没识别到。如果detected1但画面全黑可能是镜头盖没摘或者曝光时间太短。可以在代码里设置camera.exposure_mode night试试这个模式会延长曝光时间适合暗光环境。还有一个可能是摄像头模块和树莓派型号不兼容。比如OV5647在树莓派5上需要更新固件才能用老版本的Raspberry Pi OS可能不带这个驱动。解决办法是执行sudo apt update sudo apt full-upgrade把系统更新到最新版。4.2 画面卡顿、延迟高、帧率不稳定卡顿的原因通常有三个网络带宽不足、CPU占用过高、JPEG编码质量太高。先检查网络在PC上ping树莓派的IP看延迟和丢包率。如果ping的延迟超过10ms或者有丢包说明网络质量不行建议换有线或者换个WiFi信道。然后检查树莓派的CPU占用用top命令看picamera进程的CPU使用率如果超过50%说明分辨率或者帧率设高了降一档试试。最后检查JPEG质量参数quality90比quality70的数据量大一倍带宽不够的话就会卡。我遇到过一次画面每隔几秒卡一下的情况排查了半天发现是PC端的杀毒软件在扫描网络流量把杀毒软件的实时监控关掉就正常了。所以如果你在Windows上跑PC端注意一下安全软件的影响。4.3 连接被拒绝或连接超时PC端报“Connection refused”说明树莓派端的服务没起来或者端口不对。先在树莓派上执行netstat -tlnp | grep 8000看8000端口有没有在监听。如果没有说明树莓派端程序没跑起来或者绑定的IP不对。如果树莓派端程序在跑但PC连不上检查两边的IP是不是在同一个网段树莓派的防火墙有没有拦截。Raspberry Pi OS默认没有防火墙但如果你装了ufw或者iptables需要放行8000端口。“Connection timed out”通常是网络不通。先ping一下树莓派的IP如果ping不通检查网线或者WiFi连接。如果ping得通但连不上端口可能是路由器做了AP隔离禁止了设备之间的通信。在路由器管理界面里关掉AP隔离或者客户端隔离。4.4 画面颜色异常或偏色picamera的自动白平衡和自动曝光需要几秒钟收敛如果程序启动后立刻采集前几帧会偏色。解决办法是在初始化摄像头之后加time.sleep(2)等自动调节稳定。如果画面一直偏色可以手动设置白平衡模式比如camera.awb_mode fluorescent或者camera.awb_mode incandescent根据实际光源选择。也可以手动设置红蓝增益camera.awb_mode off camera.awb_gains (1.5, 1.2) # 红色增益, 蓝色增益这两个值需要根据实际画面微调一般从(1.0, 1.0)开始慢慢加。4.5 常见问题速查表问题现象可能原因排查方法解决方案摄像头识别不到排线插反或没插紧vcgencmd get_camera重新插拔排线金手指朝向HDMI画面全黑镜头盖没摘或曝光太短检查镜头盖换曝光模式设置exposure_modenight画面卡顿带宽不足或CPU满载ping测试top查看CPU降分辨率或帧率换有线网络连接被拒绝服务没启动或端口不对netstat -tlnp启动服务检查端口和IP连接超时网络不通或AP隔离ping树莓派IP检查网络关闭AP隔离画面偏色白平衡未收敛观察前几帧加延时手动设awb_gains程序跑一段时间崩溃内存泄漏或socket异常查看错误日志加异常捕获定期重启避坑技巧树莓派的capture_continuous在长时间运行后可能会因为内存碎片导致崩溃。我一般会在树莓派端加一个定时重启的逻辑比如每24小时重启一次程序。用crontab设置每天凌晨3点重启基本可以做到无人值守长期运行。4.6 长期运行稳定性的一些经验这个方案我连续跑过72小时中间没有重启画面一直稳定。但有几个细节需要注意。第一树莓派的电源要够官方推荐5V 3A如果电源功率不够摄像头高负载工作的时候可能会掉电重启。第二树莓派的散热要做好长时间跑视频编码CPU温度会到60-70度加个散热片或者小风扇温度能降到50度以下。第三PC端的cv2.imshow窗口如果被最小化OpenCV可能会停止刷新恢复窗口后画面会卡住。解决办法是不要最小化窗口或者用cv2.namedWindow创建一个固定大小的窗口。如果你需要把接收到的画面保存下来可以在PC端加一个cv2.VideoWriter把帧写入视频文件。但注意VideoWriter的帧率要和实际接收帧率匹配否则保存的视频会快放或者慢放。我一般用cv2.VideoWriter_fourcc(*mp4v)编码成MP4帧率设成25或者30根据实际接收帧率调整。这个方案后续还可以扩展成双向通信比如PC端发送指令控制树莓派的摄像头参数或者树莓派端加一个运动检测只在有物体移动的时候才传输画面省带宽。我试过用OpenCV的背景减除法做运动检测树莓派端CPU占用会增加10%左右但带宽能省70%适合网络条件不好的场景。
返回列表