使用OpenCV通过RTSP读取CCTV摄像头帧的异常问题排查
RTSP多摄像头管理:延迟/耗时操作导致解码错误的问题解决
问题描述
我编写了一段通过OpenCV管理多台RTSP摄像头的代码,功能是捕获帧并合并成网格显示。当在主循环中添加time.sleep(0.5)或者替换为YOLO目标检测这类耗时操作时,程序会出现大量解码错误;但移除延迟/耗时操作后,程序运行正常。
原始代码
import sys import time import cv2 import numpy as np class CameraManager: def __init__(self, camera_uris): self.camera_uris = camera_uris self.cameras = self._initialize_cameras() def _initialize_cameras(self): cameras = [] for uri in self.camera_uris: cap = cv2.VideoCapture(uri) if not cap.isOpened(): print("Cannot open camera") sys.exit() cameras.append(cap) return cameras def read_frames(self,skip_frames=3): frames = [] for index, cam in enumerate(self.cameras): for _ in range(skip_frames): cam.read() ret, frame = cam.read() # print(int(cam.get(cv2.CAP_PROP_POS_FRAMES))) if not ret: print("Can't receive frame (stream end?). Exiting ...") self.cameras[index] = cv2.VideoCapture(self.camera_uris[index]) frame = np.zeros((1080, 1920, 3), dtype=np.uint8) frames.append(frame) return frames def resize_images(images, new_width=640, new_height=480): """ Resize all images in the list to the specified dimensions. """ resized_images = [] for img in images: resized = cv2.resize(img, (new_width, new_height)) resized_images.append(resized) return resized_images def calculate_grid_size(num_images): """ Calculate the grid size based on the number of images. """ if num_images == 0: return 0, 0 rows = ((num_images - 1) // 3) + 1 cols = 3 return rows, cols def merge_images_in_grid(images): """ Merge images into a grid layout, with the grid size dynamically calculated. """ if not images: raise ValueError("No images to merge") # Resize images images = resize_images(images, 640, 480) # Calculate grid size grid_rows, grid_cols = calculate_grid_size(len(images)) # Get dimensions of the resized images img_height, img_width, _ = images[0].shape # Grid dimensions grid_width = img_width * grid_cols grid_height = img_height * grid_rows # Create an empty black image for the grid merged_image = np.zeros((grid_height, grid_width, 3), dtype=np.uint8) # Place each image in the grid for i, img in enumerate(images): row = i // grid_cols col = i % grid_cols merged_image[row * img_height:(row + 1) * img_height, col * img_width:(col + 1) * img_width, :] = img return merged_image class MainApplication: def __init__(self): self.camera_uris = ["rtsp://admin:srivas123@192.168.1.23","rtsp://admin:srivas123@192.168.1.22","rtsp://admin:srivas123@192.168.1.21","rtsp://admin:srivas123@192.168.1.20"] self.camera_manager = CameraManager(camera_uris=self.camera_uris) def run(self): frame_number = 0 while True: frames = self.camera_manager.read_frames() time.sleep(0.5) # 导致错误的延迟操作 merge_image = merge_images_in_grid(images=frames) cv2.namedWindow('output', cv2.WINDOW_NORMAL) cv2.imshow('output', merge_image) if cv2.waitKey(1) & 0xFF == ord('q'): exit() if __name__ == "__main__": try: app = MainApplication() app.run() except KeyboardInterrupt: print("Exiting ..")
错误日志
Can't receive frame (stream end?). Exiting ... [h264 @ 0x1c6bbc0] error while decoding MB 114 18, bytestream -35 Can't receive frame (stream end?). Exiting ... Can't receive frame (stream end?). Exiting ... [hevc @ 0x13d3840] Could not find ref with POC 6 [h264 @ 0x1c2e9c0] error while decoding MB 102 13, bytestream -5 [h264 @ 0x244bfc0] error while decoding MB 12 31, bytestream -7 Can't receive frame (stream end?). Exiting ... Can't receive frame (stream end?). Exiting ... [hevc @ 0x1c42900] Could not find ref with POC 0 [rtsp @ 0x1ade880] RTP: PT=60: bad cseq 1d84 expected=0ba8 [hevc @ 0x1c27880] Could not find ref with POC 36
根因分析
- RTSP流实时性冲突:RTSP摄像头会持续向客户端推送帧数据,OpenCV的
VideoCapture内部维护了帧缓冲区。当主线程被time.sleep或YOLO检测阻塞时,无法及时调用read()清空缓冲区,导致缓冲区溢出,旧帧被覆盖甚至丢失解码必需的关键帧(I帧),引发解码错误。 - RTP同步丢失:RTSP基于RTP传输数据,每个RTP包有序列号。如果客户端长时间不接收数据,服务器发送的序列号会持续递增,当客户端恢复读取时,收到的包序列号与预期不符,出现
bad cseq同步错误,导致流中断。 - 读写耦合阻塞:当前代码的帧读取和处理(延迟/检测)在同一线程,处理耗时直接阻塞帧读取,破坏了RTSP流的同步机制。
解决方案
方案1:减小OpenCV缓冲区大小
通过设置CAP_PROP_BUFFERSIZE,让VideoCapture只保留最新的1帧,避免缓冲区积压。修改_initialize_cameras方法:
def _initialize_cameras(self): cameras = [] for uri in self.camera_uris: cap = cv2.VideoCapture(uri) if not cap.isOpened(): print("Cannot open camera") sys.exit() # 设置缓冲区大小为1,只保留最新帧 cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) cameras.append(cap) return cameras
同时移除read_frames中的跳过帧逻辑,因为缓冲区已只保留最新帧:
def read_frames(self): frames = [] for index, cam in enumerate(self.cameras): ret, frame = cam.read() if not ret: print("Can't receive frame (stream end?). Reconnecting ...") self.cameras[index] = cv2.VideoCapture(self.camera_uris[index]) self.cameras[index].set(cv2.CAP_PROP_BUFFERSIZE, 1) frame = np.zeros((1080, 1920, 3), dtype=np.uint8) frames.append(frame) return frames
方案2:多线程独立读取摄像头帧
为每个摄像头创建独立线程,持续读取帧并保存到线程安全的队列中,主线程只负责从队列取帧处理。即使主线程处理耗时,读取线程也能及时清空缓冲区。
修改后的代码示例:
import sys import time import cv2 import numpy as np import threading from queue import Queue class CameraReader: def __init__(self, uri): self.uri = uri self.frame_queue = Queue(maxsize=1) # 只保留最新1帧 self.running = True self.cap = None self._connect() # 启动读取线程 threading.Thread(target=self._read_loop, daemon=True).start() def _connect(self): self.cap = cv2.VideoCapture(self.uri) if not self.cap.isOpened(): print(f"Cannot open camera {self.uri}") sys.exit() self.cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) def _read_loop(self): while self.running: ret, frame = self.cap.read() if not ret: print(f"Lost connection to {self.uri}, reconnecting...") self._connect() continue # 队列满时丢弃旧帧,存入新帧 if self.frame_queue.full(): self.frame_queue.get() self.frame_queue.put(frame) time.sleep(0.01) # 控制读取频率,避免占用过高CPU def get_frame(self): if not self.frame_queue.empty(): return self.frame_queue.get() # 无帧时返回空帧 return np.zeros((1080, 1920, 3), dtype=np.uint8) class CameraManager: def __init__(self, camera_uris): self.camera_readers = [CameraReader(uri) for uri in camera_uris] def read_frames(self): return [reader.get_frame() for reader in self.camera_readers] # 其余resize_images、calculate_grid_size、merge_images_in_grid函数保持不变 class MainApplication: def __init__(self): self.camera_uris = ["rtsp://admin:srivas123@192.168.1.23","rtsp://admin:srivas123@192.168.1.22","rtsp://admin:srivas123@192.168.1.21","rtsp://admin:srivas123@192.168.1.20"] self.camera_manager = CameraManager(camera_uris=self.camera_uris) def run(self): while True: frames = self.camera_manager.read_frames() # 这里可以添加YOLO检测等耗时操作,不会阻塞帧读取 # yolo_results = run_yolo_detection(frames) merge_image = merge_images_in_grid(images=frames) cv2.namedWindow('output', cv2.WINDOW_NORMAL) cv2.imshow('output', merge_image) if cv2.waitKey(1) & 0xFF == ord('q'): exit() if __name__ == "__main__": try: app = MainApplication() app.run() except KeyboardInterrupt: print("Exiting ..")
方案3:异步处理耗时任务
将YOLO检测这类耗时操作放到单独的线程或进程中,主线程只负责帧读取和显示,处理任务异步执行,不阻塞帧读取流程。可以用concurrent.futures.ThreadPoolExecutor实现。
总结
核心问题是帧读取与耗时处理的耦合导致RTSP流同步丢失,通过解耦读取和处理流程(多线程)或限制缓冲区大小,就能解决解码错误问题。其中多线程方案更适合需要处理大量耗时任务的场景,稳定性更高。
内容的提问来源于stack exchange,提问作者RajeshKumar S
相关产品推荐
相关产品推荐

