You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Python+OpenCV多RTSP摄像头同窗口显示卡顿问题排查

问题分析与解决方案

嘿,我帮你拆解下这个卡顿和时间戳跳变的问题——核心原因是视频帧队列无限制积压,加上主循环没有优先获取最新帧,导致你看到的是积压的旧帧(时间戳跳变就是因为突然拿到了更早的旧帧)。另外代码里还有几个小bug会影响稳定性,我来逐一说明并修复:

主要问题点

  • 队列无限制堆积:每个摄像头线程不断往队列塞帧,但主循环每次只取一帧,队列里会攒下大量旧帧。后续取帧时拿到的不是实时画面,而是之前积压的,时间戳自然跳变。
  • 未处理读取失败场景cap.read()返回的ret为False时(比如摄像头断流、读取超时),你没有做任何处理,会把无效帧塞进队列,导致后续拼接出错。
  • 未定义变量错误:你的video_handler类里用了self.inputqueue_return.put(ret1),但这个inputqueue_return从来没初始化过,运行时会抛出异常,只是被主循环的try捕获了,会导致该摄像头的读取线程直接崩溃。
  • 主循环变量初始化缺失:如果某个队列第一次是空的,对应的frame变量没定义,拼接时会报错(虽然被try捕获,但影响正常运行)。

修复后的代码

我把这些问题都修复了,重点优化了队列管理、实时性和稳定性:

import cv2 as cv
import numpy as np
import queue
import threading
import time

# 限制队列最大长度,只保留最新的2帧,避免旧帧积压
MAX_QUEUE_SIZE = 2

vcap1_data_queue = queue.Queue(maxsize=MAX_QUEUE_SIZE)
vcap2_data_queue = queue.Queue(maxsize=MAX_QUEUE_SIZE)
vcap3_data_queue = queue.Queue(maxsize=MAX_QUEUE_SIZE)
vcap4_data_queue = queue.Queue(maxsize=MAX_QUEUE_SIZE)

class video_handler():
    def __init__(self, name, videosource, data_queue):
        self.name = name
        # 初始化VideoCapture并设置RTSP优化参数
        self.videosource = cv.VideoCapture(videosource)
        # 减少OpenCV内部缓存,降低延迟
        self.videosource.set(cv.CAP_PROP_BUFFERSIZE, 1)
        self.data_queue = data_queue
        self.running = True

    def read_videostream(self):
        while self.running:
            ret, frame = self.videosource.read()
            if not ret:
                print(f"[{self.name}] 读取失败,重试中...")
                time.sleep(0.1)
                # 尝试重新打开摄像头
                self.videosource.release()
                self.videosource = cv.VideoCapture(videosource)
                continue
            # 队列满时先丢弃旧帧,再放入新帧
            if self.data_queue.full():
                try:
                    self.data_queue.get_nowait()
                except queue.Empty:
                    pass
            self.data_queue.put(frame)
        # 退出时释放摄像头资源
        self.videosource.release()

# 启动所有摄像头线程(设置为守护线程,主程序退出时自动结束)
video1 = video_handler("Test1", "<RTSP_Address1>", vcap1_data_queue)
video1_thread = threading.Thread(target=video1.read_videostream, daemon=True)
video1_thread.start()

video2 = video_handler("Test2", "RTSP_Address2", vcap2_data_queue)
video2_thread = threading.Thread(target=video2.read_videostream, daemon=True)
video2_thread.start()

video3 = video_handler("Test3", "RTSP_Address3", vcap3_data_queue)
video3_thread = threading.Thread(target=video3.read_videostream, daemon=True)
video3_thread.start()

video4 = video_handler("Test4", "RTSP_Address4", vcap4_data_queue)
video4_thread = threading.Thread(target=video4.read_videostream, daemon=True)
video4_thread.start()

# 初始化帧变量,避免未定义报错
frame1 = frame2 = frame3 = frame4 = None

try:
    while True:
        # 自定义函数:获取队列里的最新帧(清空队列只保留最后一帧)
        def get_latest_frame(q):
            latest_frame = None
            while not q.empty():
                latest_frame = q.get()
            return latest_frame

        # 更新各摄像头的最新帧,若队列空则保留上一帧
        frame1 = get_latest_frame(vcap1_data_queue) or frame1
        frame2 = get_latest_frame(vcap2_data_queue) or frame2
        frame3 = get_latest_frame(vcap3_data_queue) or frame3
        frame4_small = get_latest_frame(vcap4_data_queue) or frame4

        # 处理frame4的填充逻辑(匹配frame3的分辨率)
        if frame3 is not None and frame4_small is not None:
            height, width = frame3.shape[:2]
            frame4 = np.zeros((height, width, 3), np.uint8)
            h_small, w_small = frame4_small.shape[:2]
            frame4[:h_small, :w_small] = frame4_small
        elif frame4 is None:
            # 无帧时用黑色占位
            frame4 = np.zeros((480, 640, 3), np.uint8)

        # 所有必要帧存在时才拼接显示
        if all([frame1, frame2, frame3, frame4]):
            numpy_horizontal_concat1 = np.concatenate((frame1, frame2), axis=1)
            numpy_horizontal_concat2 = np.concatenate((frame3, frame4), axis=1)
            # 统一两个横向拼接帧的高度,避免拼接报错
            min_height = min(numpy_horizontal_concat1.shape[0], numpy_horizontal_concat2.shape[0])
            numpy_horizontal_concat1 = numpy_horizontal_concat1[:min_height, :]
            numpy_horizontal_concat2 = numpy_horizontal_concat2[:min_height, :]
            frame = np.concatenate((numpy_horizontal_concat1, numpy_horizontal_concat2), axis=0)
            cv.imshow('VIDEO', frame)
        
        # 控制帧率+处理退出事件
        if cv.waitKey(30) & 0xFF == ord('q'):
            break

finally:
    # 停止所有线程
    video1.running = False
    video2.running = False
    video3.running = False
    video4.running = False
    # 等待线程结束
    video1_thread.join()
    video2_thread.join()
    video3_thread.join()
    video4_thread.join()
    cv.destroyAllWindows()

关键修改说明

  1. 队列实时性优化
    • 给队列设置maxsize,满时自动丢弃旧帧,保证队列只留最新帧。
    • 主循环用get_latest_frame清空队列,只取最后一帧,确保显示的是实时画面,解决时间戳跳变问题。
  2. 修复bug与稳定性提升
    • 移除了未初始化的inputqueue_return变量,线程只处理有效帧。
    • 初始化所有frame变量,避免拼接时的未定义错误。
    • 添加摄像头读取失败的重试逻辑,防止线程崩溃。
  3. RTSP性能优化
    • 设置CAP_PROP_BUFFERSIZE为1,减少OpenCV内部缓存,降低延迟。
    • 线程设为守护线程,主程序退出时自动清理资源。

额外建议

  • 如果摄像头分辨率差异大,可以用cv.resize统一缩放后再拼接,避免黑色占位过多。
  • 可以给每个线程添加更详细的日志,方便排查单摄像头的异常问题。
  • 若机器支持,尝试启用OpenCV的FFMPEG后端(cv.VideoCapture(videosource, cv.CAP_FFMPEG)),提升RTSP解码速度。

内容的提问来源于stack exchange,提问作者Raspi User

火山引擎 最新活动