You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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

根因分析

  1. RTSP流实时性冲突:RTSP摄像头会持续向客户端推送帧数据,OpenCV的VideoCapture内部维护了帧缓冲区。当主线程被time.sleep或YOLO检测阻塞时,无法及时调用read()清空缓冲区,导致缓冲区溢出,旧帧被覆盖甚至丢失解码必需的关键帧(I帧),引发解码错误。
  2. RTP同步丢失:RTSP基于RTP传输数据,每个RTP包有序列号。如果客户端长时间不接收数据,服务器发送的序列号会持续递增,当客户端恢复读取时,收到的包序列号与预期不符,出现bad cseq同步错误,导致流中断。
  3. 读写耦合阻塞:当前代码的帧读取和处理(延迟/检测)在同一线程,处理耗时直接阻塞帧读取,破坏了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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.02 19:25:00