import cv2 import sophon_chakcy.sail as sail import threading import time from logging import getLogger, Logger from typing import Optional, Tuple import os class VideoStreamInfo: @staticmethod def get_video_info(video_path: str) -> Tuple[bool, dict]: """ 获取视频流信息 Args: video_path: 视频文件路径或 RTSP URL timeout: 连接超时时间(秒) Returns: (success, info_dict) """ cap = None try: # 设置 OpenCV RTSP参数 if video_path.startswith("rtsp://"): # RTSP 流参数优化 cap = cv2.VideoCapture(video_path) cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) os.environ["OPENCV_FFMPEG_CAPTURE_OPTIONS"] = "rtsp_transport;tcp" else: cap = cv2.VideoCapture(video_path) if not cap.isOpened(): return False, {"error": "无法打开视频源"} # 获取基本信息 fps = cap.get(cv2.CAP_PROP_FPS) width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) # 尝试读取一帧确认能正常解码 success, frame = cap.read() if not success: return False, {"error": "无法读取视频帧"} # 获取编码器信息 fourcc = int(cap.get(cv2.CAP_PROP_FOURCC)) codec = chr(fourcc & 0xFF) + chr((fourcc >> 8) & 0xFF) + \ chr((fourcc >> 16) & 0xFF) + chr((fourcc >> 24) & 0xFF) info = { "fps": fps if fps > 0 else 25.0, # 默认25fps "width": width, "height": height, "frame_count": frame_count if frame_count > 0 else 0, "codec": codec, "is_rtsp": video_path.startswith("rtsp://"), "is_file": video_path.endswith(('.mp4', '.avi', '.mkv', '.mov', '.flv')), "success": True } return True, info except Exception as e: return False, {"error": str(e)} finally: if cap is not None: cap.release() class DetectVideoStream: def __init__(self, detectors, video_path: str, max_frames: int = 0, reconnect_attempts: int = 3, reconnect_delay: int = 5, is_save: bool = False, output: str = "./result/output.mp4", logger: Optional[Logger] = None, target_fps: float = 2.0, use_opencv_info: bool = True): """ 处理视频流(视频文件或RTSP流) Args: detector: 检测器 video_path: 视频文件路径或 RTSP URL max_frames: 最大处理帧数, 0表示处理所有帧 reconnect_attempts: RTSP断流重连尝试次数 reconnect_delay: 重连延迟时间 (秒) Returns: 处理的帧数, 检测到的物体总数 """ if logger is None: self.logger = getLogger() else: self.logger = logger self.detectors = detectors self.dev_id = self.detectors[0].dev_id self.handle = sail.Handle(self.dev_id) self.video_path = video_path self.max_frames = max_frames self.target_fps = target_fps # 使用OpenCV获取视频信息 self.video_info = {} if use_opencv_info: success, info = VideoStreamInfo.get_video_info(video_path) if success: self.video_info = info self.logger.info(f"视频信息: {info}") else: self.logger.warning(f"无法获取视频信息: {info.get('error', '未知错误')}") # 设置默认值 if video_path.startswith("rtsp://"): self.video_info = {"fps": 25.0, "is_rtsp": True} else: self.video_info = {"fps": 30.0, "is_rtsp": False} # 获取原始帧率 self.original_fps = self.video_info.get("fps", 25.0) self.logger.info(f"原始帧率: {self.original_fps:.2f}FPS, 目标帧率: {self.target_fps:.2f}FPS") if not video_path.startswith("rtsp"): self.reconnect_attempts = 0 else: self.reconnect_attempts = reconnect_attempts self.reconnect_delay = reconnect_delay self.decoder = sail.Decoder(video_path, True, self.dev_id) self.stop_event = threading.Event() self.is_save = is_save if self.is_save: import os from pathlib import Path result_dir = r"results" # 清理旧的目录 if not os.path.exists(result_dir): # 创建目录 result_dir_path = Path(result_dir) if not result_dir_path.exists(): result_dir_path.mkdir(parents=True, exist_ok=True) enc_params_dict = { "width": self.video_info.get("width", 1920), "height": self.video_info.get("height", 1080), "bitrate": 2000, "gop": 32, "gop_preset": 2, "framerate": self.target_fps } self.logger.info(f"输出:{output}") enc_params = ":".join({f"{key}={value}" for key, value in enc_params_dict.items()}) self.encoder = sail.Encoder(output, self.dev_id, 'h264_bm', 'NV12', enc_params, 10) self.discard_count = self._get_discard_frames_based_on_time() def _get_discard_frames_based_on_time(self) -> int: """ 基于时间计算需要丢弃的帧数 原理:根据经过的时间和原始帧率,计算应该跳过的帧数 """ if self.original_fps <= 0 or self.target_fps <= 0: return 0 if self.target_fps >= self.original_fps: return 0 # 目标帧率高于原始帧率,不丢弃 # 计算采样间隔 sample_interval = self.original_fps / self.target_fps # 需要丢弃的帧数 = 采样间隔 - 1(因为要保留1帧) discard_frames = int(sample_interval - 1) # 确保非负 discard_frames = max(0, discard_frames) return discard_frames def _read_and_discard_frames(self): """读取并丢弃指定数量的帧""" discarded = 0 for i in range(self.discard_count): try: # 读取一帧但不处理 bmimg = sail.BMImage() ret = self.decoder.read(self.handle, bmimg) if ret == 0: discarded += 1 else: self.logger.warning(f"丢弃第 {i+1} 帧失败,错误码: {ret}") break except Exception as e: self.logger.warning(f"丢弃帧时发生错误: {e}") break if discarded > 0: self.logger.debug(f"已丢弃 {discarded} 帧") def _get_discard_frame_interval(self) -> int: """计算跳帧间隔,而不是丢弃帧数""" if self.original_fps <= 0 or self.target_fps <= 0: return 1 if self.target_fps >= self.original_fps: return 1 # 计算采样间隔 sample_interval = max(1, int(round(self.original_fps / self.target_fps))) return sample_interval def run_task(self) -> bool: result_flog = True if not self.decoder.is_opened(): self.logger.error(f"错误: 无法打开视频流 {self.video_path}") result_flog = False frame_count = 0 reconnect_count = 0 skip_interval = self._get_discard_frame_interval() skip_counter = 0 # 跳帧计数器 try: while not self.stop_event.is_set(): if self.max_frames > 0 and frame_count >= self.max_frames: self.logger.warning(f"\n达到最大帧数限制: {self.max_frames}") result_flog = True break # 3. 计算需要丢弃的帧数 # if self.discard_count > 0: # self._read_and_discard_frames() # 读取一帧 bmimg = sail.BMImage() ret = self.decoder.read(self.handle, bmimg) if ret != 0: self.logger.error(f"读取帧失败,错误码: {ret}") reconnect_count += 1 if reconnect_count > self.reconnect_attempts: self.logger.error("视频流无法继续读取") result_flog = False break self.logger.info(f"尝试重连 ({reconnect_count}/{self.reconnect_attempts})...") time.sleep(self.reconnect_delay) self.decoder.release() self.decoder = sail.Decoder(self.video_path, True, self.dev_id) if self.decoder.is_opened(): reconnect_count = 0 self.logger.info("重连成功,继续处理...") continue else: self.logger.warning("重连失败") continue # 5. 重置重连计数器 reconnect_count = 0 # 跳帧逻辑:只处理每skip_interval帧中的第1帧 skip_counter += 1 if skip_counter % skip_interval != 1: continue # 跳过此帧 # 检测 for detector in self.detectors: filtered_detections = detector.detect(bmimg) bmimg = detector.draw_detections(bmimg, filtered_detections, None) if self.is_save: self.encoder.video_write(bmimg) # type: ignore frame_count += 1 except KeyboardInterrupt: self.logger.info("\n检测到键盘中断,停止处理") result_flog = True except Exception as e: self.logger.error(f"处理视频流时发生错误: {e}") result_flog = False finally: # 清理资源 self.decoder.release() if self.is_save: self.encoder.release() self.logger.info("视频解码器已释放") return result_flog def stop_task(self): self.logger.info("提前关闭") self.stop_event.set()