This commit is contained in:
2025-09-26 20:41:44 +08:00
commit 8c28da9300
16 changed files with 2152 additions and 0 deletions

3
.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,3 @@
# 默认忽略的文件
/shelf/
/workspace.xml

12
.idea/data_collection.iml generated Normal file
View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="jdk" jdkName="pytorch_pq1" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="PyDocumentationSettings">
<option name="format" value="PLAIN" />
<option name="myDocStringFormat" value="Plain" />
</component>
</module>

View File

@ -0,0 +1,18 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="PyPackageRequirementsInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoredPackages">
<value>
<list size="5">
<item index="0" class="java.lang.String" itemvalue="scipy" />
<item index="1" class="java.lang.String" itemvalue="numpy" />
<item index="2" class="java.lang.String" itemvalue="snap7" />
<item index="3" class="java.lang.String" itemvalue="jsonchema" />
<item index="4" class="java.lang.String" itemvalue="werkzeung" />
</list>
</value>
</option>
</inspection_tool>
</profile>
</component>

View File

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

7
.idea/misc.xml generated Normal file
View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="image-classification-flower" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="pytorch_pq1" project-jdk-type="Python SDK" />
</project>

8
.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/data_collection.iml" filepath="$PROJECT_DIR$/.idea/data_collection.iml" />
</modules>
</component>
</project>

View File

@ -0,0 +1,166 @@
import cv2
import numpy as np
import platform
from .labels import labels # 确保这个文件存在
from rknnlite.api import RKNNLite
# ------------------- 核心全局变量存储RKNN模型实例确保只加载一次 -------------------
# 初始化为None首次调用时加载模型后续直接复用
_global_rknn_instance = None
# device tree for RK356x/RK3576/RK3588
DEVICE_COMPATIBLE_NODE = '/proc/device-tree/compatible'
def get_host():
# get platform and device type
system = platform.system()
machine = platform.machine()
os_machine = system + '-' + machine
if os_machine == 'Linux-aarch64':
try:
with open(DEVICE_COMPATIBLE_NODE) as f:
device_compatible_str = f.read()
if 'rk3562' in device_compatible_str:
host = 'RK3562'
elif 'rk3576' in device_compatible_str:
host = 'RK3576'
elif 'rk3588' in device_compatible_str:
host = 'RK3588'
else:
host = 'RK3566_RK3568'
except IOError:
print('Read device node {} failed.'.format(DEVICE_COMPATIBLE_NODE))
exit(-1)
else:
host = os_machine
return host
def get_top1_class_str(result):
"""
从推理结果中提取出得分最高的类别,并返回字符串
参数:
result (list): 模型推理输出结果(格式需与原函数一致,如 [np.ndarray]
返回:
str:得分最高类别的格式化字符串
若推理失败,返回错误提示字符串
"""
if result is None:
print("Inference failed: result is None")
return
# 解析推理输出与原逻辑一致展平输出为1维数组
output = result[0].reshape(-1)
# 获取得分最高的类别索引np.argmax 直接返回最大值索引,比排序更高效)
top1_index = np.argmax(output)
# 处理标签(确保索引在 labels 列表范围内,避免越界)
if 0 <= top1_index < len(labels):
top1_class_name = labels[top1_index]
else:
top1_class_name = "Unknown Class" # 应对索引异常的边界情况
# 5. 格式化返回字符串包含索引、得分、类别名称得分保留6位小数
return top1_class_name
def preprocess(raw_image, target_size=(640, 640)):
"""
读取图像并执行预处理BGR转RGB、调整尺寸、添加Batch维度
参数:
image_path (str): 图像文件的完整路径(如 "C:/test.jpg""/home/user/test.jpg"
target_size (tuple): 预处理后图像的目标尺寸,格式为 (width, height),默认 (640, 640)
返回:
img (numpy.ndarray): 预处理后的图像
异常:
FileNotFoundError: 图像路径不存在或无法读取时抛出
ValueError: 图像读取成功但为空(如文件损坏)时抛出
"""
# img = cv2.cvtColor(raw_image, cv2.COLOR_BGR2RGB)
# 调整尺寸
img = cv2.resize(raw_image, target_size)
img = np.expand_dims(img, 0) # 添加batch维度
return img
# ------------------- 新增:模型初始化函数(控制只加载一次) -------------------
def init_rknn_model(model_path):
"""
初始化RKNN模型全局唯一实例
- 首次调用:加载模型+初始化运行时,返回模型实例
- 后续调用:直接返回已加载的全局实例,避免重复加载
"""
global _global_rknn_instance # 声明使用全局变量
# 若模型未加载过,执行加载逻辑
if _global_rknn_instance is None:
# 1. 创建RKNN实例关闭内置日志
rknn_lite = RKNNLite(verbose=False)
# 2. 加载RKNN模型
ret = rknn_lite.load_rknn(model_path)
if ret != 0:
print(f'[ERROR] Load CLS_RKNN model failed (code: {ret})')
exit(ret)
# 3. 初始化运行时绑定NPU核心0
ret = rknn_lite.init_runtime(core_mask=RKNNLite.NPU_CORE_0)
if ret != 0:
print(f'[ERROR] Init CLS_RKNN runtime failed (code: {ret})')
exit(ret)
# 4. 将加载好的实例赋值给全局变量
_global_rknn_instance = rknn_lite
print(f'[INFO] CLS_RKNN model loaded successfully (path: {model_path})')
return _global_rknn_instance
def yolov11_cls_inference(model_path, raw_image, target_size=(640, 640)):
"""
根据平台进行推理,并返回最终的分类结果
参数:
model_path (str): RKNN模型文件路径
image_path (str): 图像文件的完整路径(如 "C:/test.jpg""/home/user/test.jpg"
target_size (tuple): 预处理后图像的目标尺寸,格式为 (width, height),默认 (640, 640)
"""
rknn_model = model_path
img = preprocess(raw_image, target_size)
# 只加载一次模型,避免重复加载
rknn = init_rknn_model(rknn_model)
if rknn is None:
return None, img
outputs = rknn.inference([img])
# Show the classification results
class_name = get_top1_class_str(outputs)
# rknn_lite.release()
return class_name
if __name__ == '__main__':
# 调用yolov11_cls_inference函数target_size使用默认值640x640也可显式传参如(112,112)
image_path = "/userdata/reenrr/inference_with_lite/cover_ready.jpg"
bgr_image = cv2.imread(image_path)
if bgr_image is None:
print(f"Failed to read image from {image_path}")
exit(-1)
rgb_frame = cv2.cvtColor(bgr_image, cv2.COLOR_BGR2RGB)
print(f"Read image from {image_path}, shape: {rgb_frame.shape}")
result = yolov11_cls_inference(
model_path="/userdata/PyQt_main_test/app/view/yolo/yolov11_cls.rknn",
raw_image=rgb_frame,
target_size=(640, 640)
)
# 打印最终结果
print(f"\n最终分类结果:{result}")

6
cls_inference/labels.py Normal file
View File

@ -0,0 +1,6 @@
# the labels come from synset.txt, download link: https://s3.amazonaws.com/onnx-model-zoo/synset.txt
labels = \
{0: 'cover_noready',
1: 'cover_ready'
}

Binary file not shown.

157
image_new.py Normal file
View File

@ -0,0 +1,157 @@
import cv2
import time
import os
import numpy as np
from PIL import Image
from skimage.metrics import structural_similarity as ssim
import shutil # 新增:用于检查磁盘空间
# ================== 配置参数 ==================
url = "rtsp://admin:XJ123456@192.168.1.50:554/streaming/channels/101"
save_interval = 15 # 每隔 N 帧处理一次(可调)
SSIM_THRESHOLD = 0.9 # SSIM 相似度阈值,>0.9 认为太像
output_dir = os.path.join("camera01") # 固定路径userdata/image
# 灰色判断参数
GRAY_LOWER = 70
GRAY_UPPER = 230
GRAY_RATIO_THRESHOLD = 0.7
# 创建输出目录
if not os.path.exists(output_dir):
os.makedirs(output_dir)
print(f"已创建目录: {output_dir}")
def is_large_gray(image, gray_lower=GRAY_LOWER, gray_upper=GRAY_UPPER, ratio_thresh=GRAY_RATIO_THRESHOLD):
"""
判断图片是否大面积为灰色R/G/B 都在 [gray_lower, gray_upper] 区间)
"""
img_array = np.array(image)
if len(img_array.shape) != 3 or img_array.shape[2] != 3:
return True # 非三通道图视为无效/灰色
h, w, _ = img_array.shape
total = h * w
gray_mask = (
(img_array[:, :, 0] >= gray_lower) & (img_array[:, :, 0] <= gray_upper) &
(img_array[:, :, 1] >= gray_lower) & (img_array[:, :, 1] <= gray_upper) &
(img_array[:, :, 2] >= gray_lower) & (img_array[:, :, 2] <= gray_upper)
)
gray_pixels = np.sum(gray_mask)
gray_ratio = gray_pixels / total
return gray_ratio > ratio_thresh
max_retry_seconds = 10 # 最大重试时间为10秒
retry_interval_seconds = 1 # 每隔1秒尝试重新连接一次
while True: # 外层循环用于处理重新连接逻辑
cap = cv2.VideoCapture(url)
start_time = time.time() # 记录开始尝试连接的时间
while not cap.isOpened(): # 如果无法打开摄像头,则进入重试逻辑
if time.time() - start_time >= max_retry_seconds:
print(f"已尝试重新连接 {max_retry_seconds} 秒,但仍无法获取视频流。")
exit()
print("无法打开摄像头,正在尝试重新连接...")
time.sleep(retry_interval_seconds) # 等待一段时间后再次尝试
cap = cv2.VideoCapture(url)
print("✅ 开始读取视频流...")
frame_count = 0
last_gray = None # 用于 SSIM 去重
try:
while True:
ret, frame = cap.read()
if not ret:
print("读取帧失败,可能是流中断或摄像头断开")
cap.release() # 释放资源以便重新连接
break # 跳出内层循环尝试重新连接
frame_count += 1
# 仅在指定间隔处理保存逻辑
if frame_count % save_interval != 0:
cv2.imshow('Camera Stream (Live)', frame)
if cv2.waitKey(1) == ord('q'):
raise KeyboardInterrupt
continue
print(f"处理帧 {frame_count}")
# 转为 PIL 图像(用于后续判断和旋转)
rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
pil_image = Image.fromarray(rgb_frame)
# STEP 1: 判断是否为大面积灰色(优先级最高)
if is_large_gray(pil_image):
print(f"跳过:大面积灰色图像 (frame_{frame_count})")
cv2.imshow('Camera Stream (Live)', frame)
if cv2.waitKey(1) == ord('q'):
raise KeyboardInterrupt
continue
# STEP 2: 判断是否为重复帧(基于 SSIM
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
if last_gray is not None:
try:
similarity = ssim(gray, last_gray)
if similarity > SSIM_THRESHOLD:
print(f"跳过:与上一帧太相似 (SSIM={similarity:.3f})")
cv2.imshow('Camera Stream (Live)', frame)
if cv2.waitKey(1) == ord('q'):
raise KeyboardInterrupt
continue
except Exception as e:
print(f"SSIM 计算异常: {e}")
# 更新 last_gray 用于下一帧比较
last_gray = gray.copy()
# STEP 3: 旋转 180 度
rotated_pil = pil_image.rotate(180, expand=False)
# 生成文件名(时间戳 + 毫秒防重),使用 .png 扩展名
timestamp = time.strftime("%Y%m%d_%H%M%S")
ms = int((time.time() % 1) * 1000)
filename = f"frame_{timestamp}_{ms:03d}.png"
filepath = os.path.join(output_dir, filename)
# ✅ 新增:检查磁盘可用空间
total, used, free = shutil.disk_usage(output_dir)
if free < 1024 * 1024 * 1024* 5: # 小于 5GB 就停止
print(f"❌ 磁盘空间严重不足(仅剩 {free / (1024**3):.2f} GB停止运行。")
raise SystemExit(1)
# 保存图像为 PNG 格式(无损)
try:
rotated_pil.save(filepath, format='PNG')
print(f"已保存: {filepath}")
except (OSError, IOError) as e:
error_msg = str(e)
if "No space left on device" in error_msg or "disk full" in error_msg.lower() or "quota" in error_msg.lower():
print(f"磁盘空间不足,无法保存 {filepath}!错误: {e}")
print("停止程序以防止无限错误。")
raise SystemExit(1) # 或者使用break来跳出循环
else:
print(f"保存失败 {filename}: {e}(非磁盘空间问题,继续运行)")
# 显示画面
cv2.imshow('Camera Stream (Live)', frame)
if cv2.waitKey(1) == ord('q'):
raise KeyboardInterrupt
except KeyboardInterrupt:
print("\n用户中断")
break # 跳出外层循环并退出程序
finally:
cap.release()
cv2.destroyAllWindows()
print(f"视频流已关闭,共处理 {frame_count} 帧。")
print("程序结束")

179
merge_video.py Normal file
View File

@ -0,0 +1,179 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
'''
# @Time : 2025/9/25 15:30
# @Author : reenrr
# @File : merge_video.py
'''
# !/usr/bin/env python
# -*- coding: utf-8 -*-
'''
# @Time : 2025/09/25
# @Author : reenrr
# @File : video_merger.py
# 功能描述:基于 FFmpeg 合并多个视频文件(支持跨格式、进度显示)
# 依赖:需先安装 FFmpeg 并配置环境变量
'''
import os
import subprocess
import platform
from typing import List
class VideoMerger:
def __init__(self):
# 检查 FFmpeg 是否安装(通过调用 ffmpeg -version 验证)
self._check_ffmpeg_installed()
def _check_ffmpeg_installed(self) -> None:
"""检查 FFmpeg 是否已安装并配置环境变量"""
try:
# 调用 FFmpeg 版本命令,无异常则说明安装成功
subprocess.run(
["ffmpeg", "-version"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
check=True
)
except FileNotFoundError:
raise EnvironmentError(
"未找到 FFmpeg请先安装 FFmpeg 并配置系统环境变量。\n"
"下载地址https://ffmpeg.org/download.html\n"
"Windows 配置:将 FFmpeg 的 bin 目录添加到 PATH 环境变量;\n"
"macOS使用 brew install ffmpeg\n"
"Linux使用 sudo apt install ffmpegUbuntu"
)
except subprocess.CalledProcessError:
raise RuntimeError("FFmpeg 安装异常,请重新安装。")
def _create_video_list_file(self, video_paths: List[str], list_file_path: str = "video_list.txt") -> str:
"""
创建 FFmpeg 所需的视频列表文件(用于合并多个视频)
FFmpeg 合并需通过文本文件指定视频路径格式为file '视频路径'
"""
# 检查所有视频文件是否存在
for idx, path in enumerate(video_paths):
if not os.path.exists(path):
raise FileNotFoundError(f"{idx + 1} 个视频文件不存在:{path}")
# 检查是否为文件(而非目录)
if not os.path.isfile(path):
raise IsADirectoryError(f"{idx + 1} 个路径不是文件:{path}")
# 写入视频列表(处理路径中的特殊字符,兼容 Windows 反斜杠)
with open(list_file_path, "w", encoding="utf-8") as f:
for path in video_paths:
# 统一路径分隔符为正斜杠FFmpeg 兼容正斜杠)
normalized_path = path.replace("\\", "/")
# 写入格式file '路径'(单引号避免路径含空格/特殊字符)
f.write(f"file '{normalized_path}'\n")
return list_file_path
def merge_videos(self,
video_paths: List[str],
output_path: str = "merged_output.mp4",
overwrite: bool = False) -> None:
"""
合并多个视频文件
:param video_paths: 待合并的视频路径列表(顺序即合并顺序)
:param output_path: 输出视频路径(默认当前目录 merged_output.mp4
:param overwrite: 是否覆盖已存在的输出文件(默认不覆盖)
"""
# 1. 基础参数校验
if len(video_paths) < 2:
raise ValueError("至少需要 2 个视频文件才能合并!")
# 2. 处理输出文件(若已存在且不允许覆盖,直接退出)
if os.path.exists(output_path) and not overwrite:
raise FileExistsError(f"输出文件已存在:{output_path},若需覆盖请设置 overwrite=True。")
# 3. 创建 FFmpeg 视频列表文件
list_file = self._create_video_list_file(video_paths)
print(f"已创建视频列表文件:{list_file}")
# 4. 构造 FFmpeg 合并命令
# -f concat使用 concat 协议(合并多个文件)
# -safe 0允许处理任意路径的文件避免 FFmpeg 限制相对路径)
# -i {list_file}:输入视频列表文件
# -c copy直接复制音视频流不重新编码速度快若格式不兼容会自动 fallback 编码)
# -y强制覆盖输出文件仅 overwrite=True 时启用)
ffmpeg_cmd = [
"ffmpeg",
"-f", "concat",
"-safe", "0",
"-i", list_file,
"-c", "copy", # 核心参数:复制流(快速合并),若格式不兼容可删除此句(自动编码)
"-hide_banner", # 隐藏 FFmpeg 版本横幅(简化输出)
"-loglevel", "info" # 显示进度和关键日志(可改为 warning 减少输出)
]
# 若允许覆盖,添加 -y 参数
if overwrite:
ffmpeg_cmd.append("-y")
# 添加输出路径
ffmpeg_cmd.append(output_path)
try:
print(f"\n开始合并视频(共 {len(video_paths)} 个):")
for idx, path in enumerate(video_paths, 1):
print(f" {idx}. {os.path.basename(path)}")
print(f"\n输出路径:{os.path.abspath(output_path)}")
print("=" * 50)
# 5. 执行 FFmpeg 命令(实时打印进度)
process = subprocess.Popen(
ffmpeg_cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, # 合并 stdout 和 stderr统一捕获进度
text=True,
bufsize=1 # 行缓冲,实时输出日志
)
# 实时打印 FFmpeg 输出(显示合并进度)
for line in process.stdout:
print(line.strip(), end="\r" if "time=" in line else "\n") # 进度行覆盖显示,其他行换行
# 等待命令执行完成,获取返回码
process.wait()
if process.returncode == 0:
print("\n" + "=" * 50)
print(f"视频合并成功!输出文件:{os.path.abspath(output_path)}")
else:
raise RuntimeError(f"FFmpeg 执行失败(返回码:{process.returncode}),请检查日志排查问题。")
finally:
# 6. 清理临时文件(视频列表文件)
if os.path.exists(list_file):
os.remove(list_file)
print(f"\n已清理临时列表文件:{list_file}")
if __name__ == "__main__":
# -------------------------- 配置参数(根据需求修改) --------------------------
# 1. 待合并的视频路径列表(顺序即合并顺序,支持绝对路径/相对路径)
# 示例1相对路径视频文件与脚本在同一目录
# VIDEO_PATHS = [
# "video1.mp4",
# "video2.mkv",
# "video3.avi"
# ]
# 示例2绝对路径Windows 需用双反斜杠或正斜杠)
VIDEO_PATHS = [
r"C:\Project\zjsh\data_collection\data_collection\camera01_videos\video_20250925_062802_part1.mp4", # Windows 绝对路径r 表示原始字符串,避免转义)
r"C:\Project\zjsh\data_collection\data_collection\camera01_videos\video_20250925_062905_part2.mp4"
]
# 2. 输出视频路径(默认当前目录,可自定义)
OUTPUT_PATH = "merged_video.mp4"
# 3. 是否覆盖已存在的输出文件True=覆盖False=不覆盖)
OVERWRITE = True
# -------------------------- 执行合并 --------------------------
try:
merger = VideoMerger()
merger.merge_videos(
video_paths=VIDEO_PATHS,
output_path=OUTPUT_PATH,
overwrite=OVERWRITE
)
except Exception as e:
print(f"\n合并失败:{str(e)}")

306
test.py Normal file
View File

@ -0,0 +1,306 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
'''
# @Time : 2025/9/11 16:48
# @Author : reenrr
# @File : test_01.py
'''
import cv2
import time
import os
from PIL import Image
import shutil
from cls_inference.cls_inference import yolov11_cls_inference
import numpy as np
# ================== 配置参数(严格按“定义在前,使用在后”排序)==================
# 1. 检测核心参数(先定义,后续打印和逻辑会用到)
detection_interval = 10 # 每隔10秒检查一次
detection_frame_count = 3 # 每次检测抽取3帧
required_all_noready = True # 要求所有帧都为“盖板不对齐”
# 2. 视频存储与摄像头基础参数
url = "rtsp://admin:XJ123456@192.168.1.50:554/streaming/channels/101"
output_video_dir = os.path.join("camera01_videos") # 视频保存目录
# 3. 摄像头重连参数
max_retry_seconds = 10
retry_interval_seconds = 1
# 4. 分类模型参数
cls_model_path = "/userdata/data_collection/cls_inference/yolov11_cls.rknn"
target_size = (640, 640)
# 5. 视频录制参数
video_fps = 25
video_codec = cv2.VideoWriter_fourcc(*'mp4v')
single_recording_duration = 10 # 每次录制10秒
total_target_duration = 60 # 累计目标60秒
single_recording_frames = video_fps * single_recording_duration
total_target_frames = video_fps * total_target_duration
def rotate_frame_180(pil_image):
"""将PIL图像旋转180度并转为OpenCV的BGR格式"""
rotated_pil = pil_image.rotate(180, expand=True)
rotated_rgb = np.array(rotated_pil)
rotated_bgr = cv2.cvtColor(rotated_rgb, cv2.COLOR_RGB2BGR)
return rotated_bgr
if __name__ == '__main__':
# 全局状态变量(断连重连后保留)
total_recorded_frames = 0 # 累计录制总帧数
current_segment = 0 # 当前视频分段编号
is_recording = False # 是否正在录制
current_video_filepath = None# 当前录制视频路径
# 单次连接内的临时状态变量
video_writer = None # 视频写入对象
recorded_frames = 0 # 当前分段已录制帧数
frame_count = 0 # 摄像头总读取帧数
confirmed_frames = [] # 检测通过的确认帧(用于录制起始)
last_detection_time = time.time() # 上次检测时间
detection_window_frames = [] # 检测窗口帧缓存
# 创建视频目录(确保目录存在)
os.makedirs(output_video_dir, exist_ok=True)
# 打印目标信息此时detection_frame_count已提前定义
print(f"✅ 已创建/确认视频目录: {output_video_dir}")
print(f"🎯 目标:累计录制{total_target_duration}秒,每次录制{single_recording_duration}秒,需{detection_frame_count}帧全为盖板不对齐")
# 外层循环:处理摄像头断连与重连
while True:
# 初始化摄像头连接
cap = cv2.VideoCapture(url)
cap.set(cv2.CAP_PROP_BUFFERSIZE, 5) # 设置RTSP缓存为5MB
cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(*'H264')) # 强制H264解码
# 摄像头连接重试逻辑
start_retry_time = time.time()
while not cap.isOpened():
# 超过最大重试时间则退出程序
if time.time() - start_retry_time >= max_retry_seconds:
print(f"\n❌ 已尝试重新连接 {max_retry_seconds} 秒,仍无法获取视频流,程序退出。")
# 退出前释放未关闭的视频写入器
if video_writer is not None:
video_writer.release()
print(f"📊 程序退出时累计录制:{total_recorded_frames/video_fps:.1f}")
exit()
# 每隔1秒重试一次
print(f"🔄 无法打开摄像头,正在尝试重新连接...(已重试{int(time.time()-start_retry_time)}秒)")
time.sleep(retry_interval_seconds)
cap.release() # 释放旧连接
cap = cv2.VideoCapture(url) # 重新创建连接
# 获取摄像头实际参数(可能与配置值不同)
frame_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
frame_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
actual_fps = cap.get(cv2.CAP_PROP_FPS)
# 修正帧率(以摄像头实际帧率为准)
if actual_fps > 0:
video_fps = int(actual_fps)
single_recording_frames = video_fps * single_recording_duration
total_target_frames = video_fps * total_target_duration
# 打印重连成功信息
print(f"\n✅ 摄像头重连成功(分辨率:{frame_width}x{frame_height},实际帧率:{video_fps}")
print(f"📊 当前累计录制:{total_recorded_frames / video_fps:.1f}/{total_target_duration}")
# 重连后恢复录制状态(如果断连前正在录制)
if is_recording and current_video_filepath:
print(f"🔄 恢复录制状态,继续录制视频:{current_video_filepath}")
# 重新初始化视频写入器(确保参数与摄像头匹配)
video_writer = cv2.VideoWriter(
current_video_filepath, video_codec, video_fps, (frame_width, frame_height)
)
# 恢复失败则重置录制状态
if not video_writer.isOpened():
print(f"⚠️ 视频写入器重新初始化失败,无法继续录制")
is_recording = False
video_writer = None
recorded_frames = 0
# 内层循环:读取摄像头帧并处理(检测/录制)
try:
while True:
# 读取一帧图像
ret, frame = cap.read()
if not ret:
print(f"\n⚠️ 读取帧失败,可能是流中断或摄像头断开")
# 断连时保存当前录制进度(不释放全局状态)
if video_writer is not None:
video_writer.release()
video_writer = None
print(f"⏸️ 流中断,暂停录制(已保存当前进度)")
# 重置单次连接的临时状态(全局状态保留)
frame_count = 0
detection_window_frames = []
cap.release() # 释放当前摄像头连接
break # 跳出内层循环,进入重连流程
# 累计总帧数
frame_count += 1
# 预处理帧转为RGB→PIL→旋转180度→转为BGR适配录制和模型
rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
pil_image = Image.fromarray(rgb_frame)
rotated_bgr = rotate_frame_180(pil_image)
# -------------------------- 1. 未录制时:执行检测逻辑 --------------------------
if not is_recording:
# 将帧加入检测窗口缓存用于10秒后的检测
detection_window_frames.append(rotated_bgr)
# 限制缓存大小避免内存占用过高最多保留2倍检测帧数
if len(detection_window_frames) > detection_frame_count * 2:
detection_window_frames = detection_window_frames[-detection_frame_count * 2:]
# 触发检测的条件:
# 1. 距离上次检测超过10秒2. 检测窗口有足够帧3. 累计录制未完成
if (time.time() - last_detection_time) >= detection_interval and \
len(detection_window_frames) >= detection_frame_count and \
total_recorded_frames < total_target_frames:
try:
print(f"\n==== 开始10秒间隔检测总帧{frame_count},累计已录:{total_recorded_frames/video_fps:.1f}秒) ====")
# 从检测窗口中均匀抽取3帧避免连续帧重复
sample_step = max(1, len(detection_window_frames) // detection_frame_count)
sample_frames = detection_window_frames[::sample_step][:detection_frame_count]
print(f"📋 检测窗口共{len(detection_window_frames)}帧,均匀抽取{len(sample_frames)}帧进行判断")
# 统计“盖板不对齐”的帧数
noready_frame_count = 0
valid_detection = True # 标记检测是否有效(无无效帧)
for idx, sample_frame in enumerate(sample_frames):
# 调用模型获取分类结果
class_name = yolov11_cls_inference(cls_model_path, sample_frame, target_size)
# 校验分类结果有效性
if not isinstance(class_name, str) or class_name not in ["cover_ready", "cover_noready"]:
print(f"❌ 抽取帧{idx+1}:分类结果无效({class_name}),本次检测失败")
valid_detection = False
break # 有无效帧则终止本次检测
# 统计不对齐帧
if class_name == "cover_noready":
noready_frame_count += 1
print(f"✅ 抽取帧{idx+1}:分类结果={class_name}(符合条件)")
else:
print(f"❌ 抽取帧{idx+1}:分类结果={class_name}(不符合条件)")
# 检测通过条件:所有帧有效 + 3帧全为不对齐
if valid_detection and noready_frame_count == detection_frame_count:
print(f"\n✅ 本次检测通过({noready_frame_count}/{detection_frame_count}帧均为盖板不对齐)")
# 保存检测通过的帧(用于录制起始,避免丢失检测阶段的画面)
confirmed_frames.extend(sample_frames)
# 检查磁盘空间(剩余<5GB则停止
total_disk, used_disk, free_disk = shutil.disk_usage(output_video_dir)
if free_disk < 1024 * 1024 * 1024 * 5:
print(f"❌ 磁盘空间严重不足(仅剩 {free_disk / (1024 ** 3):.2f} GB停止录制并退出。")
raise SystemExit(1)
# 生成当前分段的视频文件名(包含时间戳和分段号)
current_segment = total_recorded_frames // single_recording_frames + 1
timestamp = time.strftime("%Y%m%d_%H%M%S")
current_video_filepath = os.path.join(
output_video_dir, f"video_{timestamp}_part{current_segment}.mp4"
)
# 初始化视频写入器
video_writer = cv2.VideoWriter(
current_video_filepath, video_codec, video_fps, (frame_width, frame_height)
)
if not video_writer.isOpened():
print(f"⚠️ 视频写入器初始化失败(路径:{current_video_filepath}),跳过本次录制")
confirmed_frames = []
continue
# 写入检测阶段的确认帧(录制起始画面)
for frame in confirmed_frames:
video_writer.write(frame)
recorded_frames = len(confirmed_frames)
is_recording = True # 标记为正在录制
# 打印录制开始信息
print(f"\n📹 开始录制第{current_segment}段视频目标10秒")
print(f"📁 视频保存路径:{current_video_filepath}")
print(f"🔢 已写入检测阶段的确认帧:{recorded_frames}")
# 重置检测相关的临时状态
confirmed_frames = []
# 检测未通过未满足“3帧全不对齐”或有无效帧
else:
if valid_detection:
print(f"\n❌ 本次检测未通过(仅{noready_frame_count}/{detection_frame_count}帧为盖板不对齐,需全部符合)")
else:
print(f"\n❌ 本次检测未通过(存在无效分类结果)")
# 重置检测临时状态
confirmed_frames = []
# 更新检测时间,清空检测窗口(准备下一次检测)
last_detection_time = time.time()
detection_window_frames = []
# 捕获模型调用异常(不终止程序,仅重置检测状态)
except Exception as e:
print(f"\n⚠️ 分类模型调用异常: {str(e)}(总帧:{frame_count}")
confirmed_frames = []
last_detection_time = time.time()
detection_window_frames = []
continue
# -------------------------- 2. 正在录制时:执行写入逻辑 --------------------------
if is_recording and video_writer is not None:
# 写入当前帧已预处理为旋转180度的BGR格式
video_writer.write(rotated_bgr)
recorded_frames += 1
# 检查当前分段是否录制完成达到10秒的帧数
if recorded_frames >= single_recording_frames:
# 释放当前视频写入器(完成当前分段)
video_writer.release()
video_writer = None
is_recording = False # 标记为未录制
# 更新累计录制进度
total_recorded_frames += recorded_frames
actual_recording_time = recorded_frames / video_fps
# 打印分段完成信息
print(f"\n✅ 第{current_segment}段视频录制完成")
print(f"🔢 实际录制:{recorded_frames}帧 ≈ {actual_recording_time:.1f}")
print(f"📊 累计录制:{total_recorded_frames/video_fps:.1f}/{total_target_duration}")
# 检查是否达到总目标60秒
if total_recorded_frames >= total_target_frames:
print(f"\n🎉 已完成累计{total_target_duration}秒的录制目标!")
# 重置累计状态如需重复录制保留此逻辑如需单次录制可添加break退出
total_recorded_frames = 0
current_segment = 0
# 重置录制相关的临时状态,准备下一次检测
recorded_frames = 0
last_detection_time = time.time()
detection_window_frames = []
# 捕获用户中断Ctrl+C
except KeyboardInterrupt:
print(f"\n\n👋 用户中断程序")
# 中断前保存当前录制的视频
if video_writer is not None:
video_writer.release()
print(f"⚠️ 已保存当前录制的视频:{current_video_filepath}")
print(f"📊 中断时累计录制:{total_recorded_frames/video_fps:.1f}")
break
# 最终释放资源(无论内层循环因何退出)
finally:
cap.release() # 释放摄像头连接
if video_writer is not None:
video_writer.release() # 释放视频写入器
print(f"\n🔌 视频流已关闭,累计已录:{total_recorded_frames/video_fps:.1f}")
# 程序结束
print("\n📋 程序结束")

423
test_01.py Normal file
View File

@ -0,0 +1,423 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
'''
# @Time : 2025/9/11 16:48
# @Author : reenrr
# @File : test_01.py
# @Description: 结构化重构 - 录制6个10秒子视频累计60秒
'''
import cv2
import time
import os
from PIL import Image
import shutil
from cls_inference.cls_inference import yolov11_cls_inference
import numpy as np
class VideoRecorder:
"""视频录制器类:封装配置、状态和核心逻辑"""
def __init__(self):
# -------------------------- 1. 配置参数(集中管理,便于修改) --------------------------
self.config = {
# 检测参数
"detection_interval": 10, # 检测间隔(秒)
"detection_frame_count": 3, # 每次检测抽帧数量
"required_all_noready": True, # 需所有帧为“盖板不对齐”
# 摄像头参数
"rtsp_url": "rtsp://admin:XJ123456@192.168.1.50:554/streaming/channels/101",
"output_dir": "camera01_videos", # 视频保存目录
"max_retry_sec": 10, # 摄像头重连超时(秒)
"retry_interval_sec": 1, # 重连间隔(秒)
# 模型参数
"cls_model_path": "/userdata/data_collection/cls_inference/yolov11_cls.rknn",
"model_target_size": (640, 640),
# 录制参数
"video_fps": 25, # 初始帧率
"video_codec": cv2.VideoWriter_fourcc(*'mp4v'),
"single_duration": 10, # 单段视频时长(秒)
"total_duration": 60, # 总目标时长(秒)
"min_valid_duration": 10 # 最小有效视频时长(秒)
}
# 计算衍生配置(避免重复计算)
self.config["single_frames"] = self.config["video_fps"] * self.config["single_duration"]
self.config["total_frames"] = self.config["video_fps"] * self.config["total_duration"]
# -------------------------- 2. 全局状态(断连后需保留) --------------------------
self.state = {
"total_recorded_frames": 0, # 累计录制帧数
"current_segment": 0, # 当前分段编号
"is_recording": False, # 是否正在录制
"current_video_path": None, # 当前分段视频路径
"cached_frames": [], # 断连时缓存的帧
"last_resolution": (0, 0), # 上次摄像头分辨率(宽,高)
"recording_start_time": 0 # 当前分段录制开始时间
}
# -------------------------- 3. 临时状态(单次连接内有效,断连后重置) --------------------------
self.temp = {
"cap": None, # 摄像头对象
"video_writer": None, # 视频写入对象
"recorded_frames": 0, # 当前分段已录帧数
"frame_count": 0, # 摄像头总读帧数
"confirmed_frames": [], # 检测通过的起始帧
"last_detection_time": time.time(),# 上次检测时间
"detection_window": [] # 检测用帧缓存窗口
}
def init_environment(self):
"""初始化环境:创建目录、打印配置信息"""
# 创建视频保存目录
os.makedirs(self.config["output_dir"], exist_ok=True)
# 打印目标信息
print("=" * 60)
print(f"✅ 环境初始化完成")
print(f"📁 视频保存目录:{os.path.abspath(self.config['output_dir'])}")
print(f"🎯 录制目标:{self.config['total_duration']}秒({self.config['total_duration']//self.config['single_duration']}段×{self.config['single_duration']}秒)")
print(f"🔍 检测条件:每{self.config['detection_interval']}秒检测,需{self.config['detection_frame_count']}帧全为'盖板不对齐'")
print("=" * 60)
def rotate_frame(self, pil_image):
"""工具函数将PIL图像旋转180度并转为OpenCV的BGR格式"""
rotated_pil = pil_image.rotate(180, expand=True)
rotated_rgb = np.array(rotated_pil)
return cv2.cvtColor(rotated_rgb, cv2.COLOR_RGB2BGR)
def connect_camera(self):
"""摄像头连接:含重连逻辑,返回是否连接成功"""
# 初始化摄像头
self.temp["cap"] = cv2.VideoCapture(self.config["rtsp_url"])
self.temp["cap"].set(cv2.CAP_PROP_BUFFERSIZE, 5) # 设置RTSP缓存
self.temp["cap"].set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(*'H264')) # 强制H264解码
# 重连逻辑
start_retry_time = time.time()
while not self.temp["cap"].isOpened():
# 超时退出
if time.time() - start_retry_time >= self.config["max_retry_sec"]:
print(f"\n❌ 摄像头重连超时({self.config['max_retry_sec']}秒),程序退出")
self.release_resources() # 释放资源
print(f"📊 退出时累计录制:{self.state['total_recorded_frames']/self.config['video_fps']:.1f}")
return False
# 重试提示
retry_sec = int(time.time() - start_retry_time)
print(f"🔄 正在重连摄像头(已重试{retry_sec}秒)...")
time.sleep(self.config["retry_interval_sec"])
# 释放旧连接,重新初始化
self.temp["cap"].release()
self.temp["cap"] = cv2.VideoCapture(self.config["rtsp_url"])
# 连接成功:获取实际摄像头参数
frame_width = int(self.temp["cap"].get(cv2.CAP_PROP_FRAME_WIDTH))
frame_height = int(self.temp["cap"].get(cv2.CAP_PROP_FRAME_HEIGHT))
actual_fps = self.temp["cap"].get(cv2.CAP_PROP_FPS)
# 修正帧率(以摄像头实际帧率为准)
if actual_fps > 0:
self.config["video_fps"] = int(actual_fps)
self.config["single_frames"] = self.config["video_fps"] * self.config["single_duration"]
self.config["total_frames"] = self.config["video_fps"] * self.config["total_duration"]
# 检查分辨率变化
resolution_changed = False
if self.state["last_resolution"] != (0, 0):
if (frame_width, frame_height) != self.state["last_resolution"]:
print(f"⚠️ 摄像头分辨率变化:{self.state['last_resolution']} → ({frame_width},{frame_height})")
resolution_changed = True
# 分辨率变化:重置录制状态
self.state["is_recording"] = False
self.state["cached_frames"] = []
self.temp["detection_window"] = []
# 更新分辨率记录
self.state["last_resolution"] = (frame_width, frame_height)
# 打印连接成功信息
print(f"\n✅ 摄像头连接成功")
print(f"📊 分辨率:{frame_width}×{frame_height} | 实际帧率:{self.config['video_fps']}fps")
print(f"📈 当前累计进度:{self.state['total_recorded_frames']/self.config['video_fps']:.1f}/{self.config['total_duration']}")
return True, resolution_changed
def restore_recording(self, resolution_changed):
"""重连后恢复录制状态:重新初始化写入器并写入缓存帧"""
if not (self.state["is_recording"] and self.state["current_video_path"] and not resolution_changed):
return # 无需恢复
print(f"🔄 恢复录制:{self.state['current_video_path']}")
# 重新初始化视频写入器
frame_width, frame_height = self.state["last_resolution"]
self.temp["video_writer"] = cv2.VideoWriter(
self.state["current_video_path"],
self.config["video_codec"],
self.config["video_fps"],
(frame_width, frame_height)
)
# 恢复失败:重置状态
if not self.temp["video_writer"].isOpened():
print(f"⚠️ 视频写入器恢复失败,放弃当前分段")
self.state["is_recording"] = False
self.temp["recorded_frames"] = 0
self.state["cached_frames"] = []
return
# 写入缓存帧
if self.state["cached_frames"]:
print(f"🔄 恢复缓存的{len(self.state['cached_frames'])}")
for frame in self.state["cached_frames"]:
self.temp["video_writer"].write(frame)
self.temp["recorded_frames"] = len(self.state["cached_frames"])
self.state["cached_frames"] = [] # 清空缓存
def run_detection(self):
"""执行检测逻辑:从缓存窗口抽帧,调用模型判断是否开始录制"""
# 检测触发条件:时间间隔达标 + 缓存帧足够 + 未录满总目标
if (time.time() - self.temp["last_detection_time"] < self.config["detection_interval"]) or \
(len(self.temp["detection_window"]) < self.config["detection_frame_count"]) or \
(self.state["total_recorded_frames"] >= self.config["total_frames"]):
return
print(f"\n==== 开始检测(总读帧:{self.temp['frame_count']} | 累计进度:{self.state['total_recorded_frames']/self.config['video_fps']:.1f}秒) ====")
# 均匀抽帧(避免连续帧重复)
sample_step = max(1, len(self.temp["detection_window"]) // self.config["detection_frame_count"])
sample_frames = self.temp["detection_window"][::sample_step][:self.config["detection_frame_count"]]
print(f"📋 检测窗口:{len(self.temp['detection_window'])}帧 → 抽帧:{len(sample_frames)}")
# 模型分类:统计“盖板不对齐”帧数
noready_count = 0
valid_detection = True
for idx, frame in enumerate(sample_frames):
try:
class_name = yolov11_cls_inference(
self.config["cls_model_path"], frame, self.config["model_target_size"]
)
except Exception as e:
print(f"❌ 模型调用异常:{str(e)}")
valid_detection = False
break
# 校验分类结果有效性
if not isinstance(class_name, str) or class_name not in ["cover_ready", "cover_noready"]:
print(f"❌ 抽帧{idx+1}:分类结果无效({class_name}")
valid_detection = False
break
# 统计结果
if class_name == "cover_noready":
noready_count += 1
print(f"✅ 抽帧{idx+1}{class_name}(符合条件)")
else:
print(f"❌ 抽帧{idx+1}{class_name}(不符合)")
# 检测未通过:重置状态
if not valid_detection or noready_count != self.config["detection_frame_count"]:
if valid_detection:
print(f"❌ 检测未通过(仅{noready_count}/{self.config['detection_frame_count']}帧符合条件)")
else:
print(f"❌ 检测未通过(存在无效结果)")
self.temp["confirmed_frames"] = []
self.temp["last_detection_time"] = time.time()
self.temp["detection_window"] = []
return
# 检测通过:准备开始录制
print(f"✅ 检测通过!准备录制新分段")
self.temp["confirmed_frames"] = sample_frames # 保存起始帧
self.start_recording() # 启动录制
# 重置检测状态
self.temp["last_detection_time"] = time.time()
self.temp["detection_window"] = []
def start_recording(self):
"""启动新分段录制:初始化写入器、写入起始帧"""
# 检查磁盘空间
total_disk, used_disk, free_disk = shutil.disk_usage(self.config["output_dir"])
if free_disk < 1024 * 1024 * 1024 * 5: # 剩余<5GB
print(f"❌ 磁盘空间不足(仅剩{free_disk/(1024**3):.2f}GB退出程序")
self.release_resources()
raise SystemExit(1)
# 生成分段视频路径(含时间戳和分段号)
self.state["current_segment"] = self.state["total_recorded_frames"] // self.config["single_frames"] + 1
timestamp = time.strftime("%Y%m%d_%H%M%S")
self.state["current_video_path"] = os.path.join(
self.config["output_dir"], f"video_{timestamp}_part{self.state['current_segment']}.mp4"
)
# 初始化视频写入器
frame_width, frame_height = self.state["last_resolution"]
self.temp["video_writer"] = cv2.VideoWriter(
self.state["current_video_path"],
self.config["video_codec"],
self.config["video_fps"],
(frame_width, frame_height)
)
# 写入器初始化失败:跳过本次录制
if not self.temp["video_writer"].isOpened():
print(f"⚠️ 视频写入器初始化失败(路径:{self.state['current_video_path']}")
self.temp["confirmed_frames"] = []
return
# 写入检测通过的起始帧
for frame in self.temp["confirmed_frames"]:
self.temp["video_writer"].write(frame)
self.temp["recorded_frames"] = len(self.temp["confirmed_frames"])
self.state["is_recording"] = True
self.state["recording_start_time"] = time.time()
# 打印录制启动信息
print(f"\n📹 开始录制第{self.state['current_segment']}段视频(目标{self.config['single_duration']}秒)")
print(f"📁 视频路径:{self.state['current_video_path']}")
print(f"🔢 已写入起始帧:{self.temp['recorded_frames']}")
self.temp["confirmed_frames"] = [] # 清空起始帧缓存
def process_recording(self, rotated_frame):
"""处理录制逻辑:写入当前帧,判断分段是否完成"""
if not (self.state["is_recording"] and self.temp["video_writer"]):
return
# 写入当前帧
self.temp["video_writer"].write(rotated_frame)
self.temp["recorded_frames"] += 1
# 检查分段是否完成(帧数达标 或 时间达标,取其一)
actual_duration = time.time() - self.state["recording_start_time"]
if self.temp["recorded_frames"] >= self.config["single_frames"] or actual_duration >= self.config["single_duration"]:
self.finish_segment()
def finish_segment(self):
"""完成当前分段录制:释放写入器、更新累计进度、检查总目标"""
# 释放当前分段写入器
self.temp["video_writer"].release()
self.temp["video_writer"] = None
self.state["is_recording"] = False
# 计算实际录制信息
actual_duration = self.temp["recorded_frames"] / self.config["video_fps"]
self.state["total_recorded_frames"] += self.temp["recorded_frames"]
# 打印分段完成信息
print(f"\n✅ 第{self.state['current_segment']}段录制完成")
print(f"🔍 实际录制:{self.temp['recorded_frames']}帧 ≈ {actual_duration:.1f}秒(目标{self.config['single_duration']}秒)")
print(f"📊 累计进度:{self.state['total_recorded_frames']/self.config['video_fps']:.1f}/{self.config['total_duration']}")
# 检查是否达到总目标
if self.state["total_recorded_frames"] >= self.config["total_frames"]:
print(f"\n🎉 已完成{self.config['total_duration']}秒录制目标!")
# 重置累计状态(如需重复录制,保留此逻辑;单次录制可改为退出)
self.state["total_recorded_frames"] = 0
self.state["current_segment"] = 0
# 重置录制临时状态
self.temp["recorded_frames"] = 0
self.temp["last_detection_time"] = time.time()
self.temp["detection_window"] = []
self.state["recording_start_time"] = 0
def handle_disconnect(self):
"""处理摄像头断连:缓存帧、释放资源"""
print(f"\n⚠️ 摄像头断连(读取帧失败)")
# 缓存当前录制的帧
if self.state["is_recording"]:
# 收集已录帧(含当前分段所有帧)
if self.temp["video_writer"]:
self.temp["video_writer"].release()
self.temp["video_writer"] = None
# 缓存已录帧(重连后恢复)
self.state["cached_frames"] = self.temp["detection_window"].copy() # 用检测窗口缓存当前帧
print(f"⏸️ 缓存{len(self.state['cached_frames'])}帧,重连后继续录制")
# 重置单次连接临时状态
self.temp["frame_count"] = 0
self.temp["detection_window"] = []
if self.temp["cap"]:
self.temp["cap"].release()
self.temp["cap"] = None
def release_resources(self):
"""释放所有资源:摄像头、视频写入器"""
if self.temp["cap"] and self.temp["cap"].isOpened():
self.temp["cap"].release()
if self.temp["video_writer"]:
self.temp["video_writer"].release()
print(f"\n🔌 所有资源已释放")
def run(self):
"""主运行逻辑:初始化→循环(连接→录制→断连处理)"""
# 初始化环境
self.init_environment()
try:
while True:
# 1. 连接摄像头(含重连)
connect_success, resolution_changed = self.connect_camera()
if not connect_success:
break # 连接失败,退出程序
# 2. 重连后恢复录制状态
self.restore_recording(resolution_changed)
# 3. 主循环:读取帧→处理
last_frame_time = time.time()
while True:
# 读取帧
ret, frame = self.temp["cap"].read()
if not ret:
self.handle_disconnect()
break # 断连,跳出内层循环重连
# 打印实时帧率每10帧更新一次
current_time = time.time()
fps = 1 / (current_time - last_frame_time) if (current_time - last_frame_time) > 0 else 0
last_frame_time = current_time
if self.temp["frame_count"] % 10 == 0 and self.temp["frame_count"] > 0:
print(f"📊 帧率:{fps:.1f} | 总读帧:{self.temp['frame_count']}", end='\r')
self.temp["frame_count"] += 1
# 帧预处理旋转180度
rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
rotated_frame = self.rotate_frame(Image.fromarray(rgb_frame))
# 加入检测窗口缓存(用于检测和断连缓存)
self.temp["detection_window"].append(rotated_frame)
# 限制缓存大小(避免内存溢出)
if len(self.temp["detection_window"]) > self.config["detection_frame_count"] * 2:
self.temp["detection_window"] = self.temp["detection_window"][-self.config["detection_frame_count"] * 2:]
# 未录制:执行检测
if not self.state["is_recording"]:
self.run_detection()
# 正在录制:执行写入
else:
self.process_recording(rotated_frame)
# 捕获用户中断Ctrl+C
except KeyboardInterrupt:
print(f"\n\n👋 用户主动中断程序")
# 处理未完成视频
if self.state["is_recording"] and self.state["current_video_path"] and os.path.exists(self.state["current_video_path"]):
duration = self.temp["recorded_frames"] / self.config["video_fps"]
if duration < self.config["min_valid_duration"]:
os.remove(self.state["current_video_path"])
print(f"🗑️ 删除不足{self.config['min_valid_duration']}秒的视频:{self.state['current_video_path']}")
else:
print(f"⚠️ 保存未完成视频:{self.state['current_video_path']}{duration:.1f}秒)")
print(f"📊 中断时累计录制:{self.state['total_recorded_frames']/self.config['video_fps']:.1f}")
# 捕获其他异常
except Exception as e:
print(f"\n⚠️ 程序异常:{str(e)}")
# 最终释放资源
finally:
self.release_resources()
print("\n📋 程序结束")
if __name__ == '__main__':
# 初始化并运行录制器
recorder = VideoRecorder()
recorder.run()

386
test_02.py Normal file
View File

@ -0,0 +1,386 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
'''
# @Time : 2025/9/11 16:48
# @Author : reenrr
# @File : test_01.py
# @Description: 录制60秒视频存在6个子视频每个子视频10秒一定会是10秒
'''
import cv2
import time
import os
from PIL import Image
import shutil
from cls_inference.cls_inference import yolov11_cls_inference
import numpy as np
# ================== 配置参数(严格按“定义在前,使用在后”排序)==================
# 1. 检测核心参数(先定义,后续打印和逻辑会用到)
detection_interval = 10 # 每隔10秒检查一次
detection_frame_count = 3 # 每次检测抽取3帧
required_all_noready = True # 要求所有帧都为“盖板不对齐”
# 2. 视频存储与摄像头基础参数
url = "rtsp://admin:XJ123456@192.168.1.50:554/streaming/channels/101"
output_video_dir = os.path.join("camera01_videos") # 视频保存目录
# 3. 摄像头重连参数
max_retry_seconds = 10
retry_interval_seconds = 1
# 4. 分类模型参数
cls_model_path = "/userdata/data_collection/cls_inference/yolov11_cls.rknn"
target_size = (640, 640)
# 5. 视频录制参数
video_fps = 25
video_codec = cv2.VideoWriter_fourcc(*'mp4v')
single_recording_duration = 10 # 每次录制10秒
total_target_duration = 60 # 累计目标60秒
single_recording_frames = video_fps * single_recording_duration
total_target_frames = video_fps * total_target_duration
# 6.最小有效视频时长,低于此值的视频会被删除
min_valid_video_duration = 10
def rotate_frame_180(pil_image):
"""将PIL图像旋转180度并转为OpenCV的BGR格式"""
rotated_pil = pil_image.rotate(180, expand=True)
rotated_rgb = np.array(rotated_pil)
rotated_bgr = cv2.cvtColor(rotated_rgb, cv2.COLOR_RGB2BGR)
return rotated_bgr
if __name__ == '__main__':
# 全局状态变量(断连重连后保留)
total_recorded_frames = 0 # 累计录制总帧数
current_segment = 0 # 当前视频分段编号
is_recording = False # 是否正在录制
current_video_filepath = None# 当前录制视频路径
global_cached_frames = [] # 断连时缓存的已录的帧
last_width, last_height = 0, 0 # 上次摄像头参数(用于断连后恢复)
recording_start_time = 0 # 录制开始时间
# 单次连接内的临时状态变量
video_writer = None # 视频写入对象
recorded_frames = 0 # 当前分段已录制帧数
frame_count = 0 # 摄像头总读取帧数
confirmed_frames = [] # 检测通过的确认帧(用于录制起始)
last_detection_time = time.time() # 上次检测时间
detection_window_frames = [] # 检测窗口帧缓存
recorded_frames_list = [] # 已录制的帧列表(用于断连后恢复)
# 创建视频目录(确保目录存在)
os.makedirs(output_video_dir, exist_ok=True)
# 打印目标信息此时detection_frame_count已提前定义
print(f"✅ 已创建/确认视频目录: {output_video_dir}")
print(f"🎯 目标:累计录制{total_target_duration}秒,每次录制{single_recording_duration}秒,需{detection_frame_count}帧全为盖板不对齐")
# 外层循环:处理摄像头断连与重连
while True:
# 初始化摄像头连接
cap = cv2.VideoCapture(url)
cap.set(cv2.CAP_PROP_BUFFERSIZE, 5) # 设置RTSP缓存为5MB
cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(*'H264')) # 强制H264解码
# 摄像头连接重试逻辑
start_retry_time = time.time()
while not cap.isOpened():
# 超过最大重试时间则退出程序
if time.time() - start_retry_time >= max_retry_seconds:
print(f"\n❌ 已尝试重新连接 {max_retry_seconds} 秒,仍无法获取视频流,程序退出。")
# 退出前释放未关闭的视频写入器
if video_writer is not None:
video_writer.release()
print(f"📊 程序退出时累计录制:{total_recorded_frames/video_fps:.1f}")
exit()
# 每隔1秒重试一次
print(f"🔄 无法打开摄像头,正在尝试重新连接...(已重试{int(time.time()-start_retry_time)}秒)")
time.sleep(retry_interval_seconds)
cap.release() # 释放旧连接
cap = cv2.VideoCapture(url) # 重新创建连接
# 获取摄像头实际参数(可能与配置值不同)
frame_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
frame_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
actual_fps = cap.get(cv2.CAP_PROP_FPS)
# 修正帧率(以摄像头实际帧率为准)
if actual_fps > 0:
video_fps = int(actual_fps)
single_recording_frames = video_fps * single_recording_duration
total_target_frames = video_fps * total_target_duration
# 检查分辨率是否变化
resolution_changed = False
if last_width > 0 and last_height > 0:
if frame_width != last_width or frame_height != last_height:
print(f"⚠️ 摄像头分辨率变化({last_width}×{last_height}{frame_width}×{frame_height}")
resolution_changed = True
# 分辨率变化时重置录制状态
is_recording = False
global_cached_frames = []
recorded_frames_list = []
# 更新分辨率记录
last_width, last_height = frame_width, frame_height
# 打印重连成功信息
print(f"\n✅ 摄像头重连成功(分辨率:{frame_width}x{frame_height},实际帧率:{video_fps}")
print(f"📊 当前累计录制:{total_recorded_frames / video_fps:.1f}/{total_target_duration}")
# 重连后恢复录制状态(如果断连前正在录制且分辨率未变化)
if is_recording and current_video_filepath and not resolution_changed:
print(f"🔄 恢复录制状态,继续录制视频:{current_video_filepath}")
# 重新初始化视频写入器(确保参数与摄像头匹配)
video_writer = cv2.VideoWriter(
current_video_filepath, video_codec, video_fps, (frame_width, frame_height)
)
# 恢复失败则重置录制状态
if not video_writer.isOpened():
print(f"⚠️ 视频写入器重新初始化失败,无法继续录制")
is_recording = False
video_writer = None
recorded_frames = 0
recorded_frames_list = []
else:
# 写入缓存的帧
if len(global_cached_frames) > 0:
print(f"🔄 恢复缓存的{len(global_cached_frames)}")
for frame in global_cached_frames:
video_writer.write(frame)
recorded_frames = len(global_cached_frames)
recorded_frames_list = global_cached_frames.copy()
global_cached_frames = []
# 内层循环:读取摄像头帧并处理(检测/录制)
try:
last_frame_time = time.time()
while True:
# 读取一帧图像
ret, frame = cap.read()
if not ret:
print(f"\n⚠️ 读取帧失败,可能是流中断或摄像头断开")
# 断连时保存当前录制进度(不释放全局状态)
if video_writer is not None:
video_writer.release()
video_writer = None
if is_recording:
global_cached_frames = recorded_frames_list.copy()
print(f"⏸️ 流中断,缓存{len(global_cached_frames)}帧,重连后继续录制")
# 重置单次连接的临时状态(全局状态保留)
frame_count = 0
detection_window_frames = []
cap.release() # 释放当前摄像头连接
break # 跳出内层循环,进入重连流程
# 计算并打印实时帧率
current_time = time.time()
fps = 1 / (current_time - last_frame_time) if (current_time - last_frame_time) > 0 else 0
last_frame_time = current_time
if frame_count % 10 == 0 and frame_count > 0:
print(f"📊 实时帧率: {fps:.1f}fps, 累计帧: {frame_count}", end='\r')
# 累计总帧数
frame_count += 1
# 预处理帧转为RGB→PIL→旋转180度→转为BGR适配录制和模型
rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
pil_image = Image.fromarray(rgb_frame)
rotated_bgr = rotate_frame_180(pil_image)
# -------------------------- 1. 未录制时:执行检测逻辑 --------------------------
if not is_recording:
# 将帧加入检测窗口缓存用于10秒后的检测
detection_window_frames.append(rotated_bgr)
# 限制缓存大小避免内存占用过高最多保留2倍检测帧数
if len(detection_window_frames) > detection_frame_count * 2:
detection_window_frames = detection_window_frames[-detection_frame_count * 2:]
# 触发检测的条件:
# 1. 距离上次检测超过10秒2. 检测窗口有足够帧3. 累计录制未完成
if (time.time() - last_detection_time) >= detection_interval and \
len(detection_window_frames) >= detection_frame_count and \
total_recorded_frames < total_target_frames:
try:
print(f"\n==== 开始10秒间隔检测总帧{frame_count},累计已录:{total_recorded_frames/video_fps:.1f}秒) ====")
# 从检测窗口中均匀抽取3帧避免连续帧重复
sample_step = max(1, len(detection_window_frames) // detection_frame_count)
sample_frames = detection_window_frames[::sample_step][:detection_frame_count]
print(f"📋 检测窗口共{len(detection_window_frames)}帧,均匀抽取{len(sample_frames)}帧进行判断")
# 统计“盖板不对齐”的帧数
noready_frame_count = 0
valid_detection = True # 标记检测是否有效(无无效帧)
for idx, sample_frame in enumerate(sample_frames):
# 调用模型获取分类结果
class_name = yolov11_cls_inference(cls_model_path, sample_frame, target_size)
# 校验分类结果有效性
if not isinstance(class_name, str) or class_name not in ["cover_ready", "cover_noready"]:
print(f"❌ 抽取帧{idx+1}:分类结果无效({class_name}),本次检测失败")
valid_detection = False
break # 有无效帧则终止本次检测
# 统计不对齐帧
if class_name == "cover_noready":
noready_frame_count += 1
print(f"✅ 抽取帧{idx+1}:分类结果={class_name}(符合条件)")
else:
print(f"❌ 抽取帧{idx+1}:分类结果={class_name}(不符合条件)")
# 检测通过条件:所有帧有效 + 3帧全为不对齐
if valid_detection and noready_frame_count == detection_frame_count:
print(f"\n✅ 本次检测通过({noready_frame_count}/{detection_frame_count}帧均为盖板不对齐)")
# 保存检测通过的帧(用于录制起始,避免丢失检测阶段的画面)
confirmed_frames.extend(sample_frames)
# 检查磁盘空间(剩余<5GB则停止
total_disk, used_disk, free_disk = shutil.disk_usage(output_video_dir)
if free_disk < 1024 * 1024 * 1024 * 5:
print(f"❌ 磁盘空间严重不足(仅剩 {free_disk / (1024 ** 3):.2f} GB停止录制并退出。")
raise SystemExit(1)
# 生成当前分段的视频文件名(包含时间戳和分段号)
current_segment = total_recorded_frames // single_recording_frames + 1
timestamp = time.strftime("%Y%m%d_%H%M%S")
current_video_filepath = os.path.join(
output_video_dir, f"video_{timestamp}_part{current_segment}.mp4"
)
# 初始化视频写入器
video_writer = cv2.VideoWriter(
current_video_filepath, video_codec, video_fps, (frame_width, frame_height)
)
if not video_writer.isOpened():
print(f"⚠️ 视频写入器初始化失败(路径:{current_video_filepath}),跳过本次录制")
confirmed_frames = []
recorded_frames_List = []
continue
# 写入检测阶段的确认帧(录制起始画面)
for frame in confirmed_frames:
video_writer.write(frame)
recorded_frames = len(confirmed_frames)
recorded_frames_list = confirmed_frames.copy()
is_recording = True # 标记为正在录制
recording_start_time = time.time()
# 打印录制开始信息
print(f"\n📹 开始录制第{current_segment}段视频目标10秒")
print(f"📁 视频保存路径:{current_video_filepath}")
print(f"🔢 已写入检测阶段的确认帧:{recorded_frames}")
# 重置检测相关的临时状态
confirmed_frames = []
# 检测未通过未满足“3帧全不对齐”或有无效帧
else:
if valid_detection:
print(f"\n❌ 本次检测未通过(仅{noready_frame_count}/{detection_frame_count}帧为盖板不对齐,需全部符合)")
else:
print(f"\n❌ 本次检测未通过(存在无效分类结果)")
# 重置检测临时状态
confirmed_frames = []
# 更新检测时间,清空检测窗口(准备下一次检测)
last_detection_time = time.time()
detection_window_frames = []
# 捕获模型调用异常(不终止程序,仅重置检测状态)
except Exception as e:
print(f"\n⚠️ 分类模型调用异常: {str(e)}(总帧:{frame_count}")
confirmed_frames = []
last_detection_time = time.time()
detection_window_frames = []
continue
# -------------------------- 2. 正在录制时:执行写入逻辑 --------------------------
if is_recording and video_writer is not None:
# 写入当前帧已预处理为旋转180度的BGR格式
video_writer.write(rotated_bgr)
recorded_frames += 1
recorded_frames_list.append(rotated_bgr)
# 检查当前分段是否录制完成达到10秒的帧数
actual_recording_duration = (time.time() - recording_start_time)
if actual_recording_duration >= single_recording_frames or recorded_frames >= single_recording_frames:
# 释放当前视频写入器(完成当前分段)
video_writer.release()
video_writer = None
is_recording = False # 标记为未录制
# 更新累计录制进度
total_recorded_frames += recorded_frames
# 打印分段完成信息
print(f"\n✅ 第{current_segment}段视频录制完成")
print(f"🔢 实际录制:{recorded_frames}帧 ≈ {actual_recording_duration:.1f}")
print(f"📊 累计录制:{total_recorded_frames/video_fps:.1f}/{total_target_duration}")
# 检查是否达到总目标60秒
if total_recorded_frames >= total_target_frames:
print(f"\n🎉 已完成累计{total_target_duration}秒的录制目标!")
# 重置累计状态如需重复录制保留此逻辑如需单次录制可添加break退出
total_recorded_frames = 0
current_segment = 0
# 重置录制相关的临时状态,准备下一次检测
recorded_frames = 0
recorded_frames_list = []
last_detection_time = time.time()
detection_window_frames = []
recording_start_time = 0
# 捕获用户中断Ctrl+C
except KeyboardInterrupt:
print(f"\n\n👋 用户中断程序")
# 中断前保存当前录制的视频
if video_writer is not None:
video_writer.release()
# 检查视频时长,过短则删除
if is_recording and recorded_frames > 0:
duration = recorded_frames / video_fps
if duration < min_valid_video_duration:
print(f"🗑️ 删除不足{min_valid_video_duration}秒的视频:{current_video_filepath}")
os.remove(current_video_filepath)
else:
print(f"⚠️ 已保存当前录制的视频:{current_video_filepath}")
print(f"📊 中断时累计录制:{total_recorded_frames/video_fps:.1f}")
break
# 捕获所有其他异常
except Exception as e:
print(f"\n⚠️ 录制过程异常:{str(e)}")
if is_recording and video_writer is not None:
video_writer.release()
# 检查视频时长,过短则删除
if recorded_frames > 0:
duration = recorded_frames / video_fps
if duration < min_valid_video_duration:
print(f"🗑️ 删除不足{min_valid_video_duration}秒的无效视频:{current_video_filepath}")
try:
os.remove(current_video_filepath)
except:
pass
else:
print(f"⚠️ 保存未完成视频:{current_video_filepath}(时长:{duration:.1f}秒)")
# 重置录制状态
is_recording = False
recorded_frames = 0
recorded_frames_list = []
global_cached_frames = []
cap.release()
continue
# 最终释放资源(无论内层循环因何退出)
finally:
if 'cap' in locals() and cap.isOpened():
cap.release() # 释放摄像头连接
if video_writer is not None:
video_writer.release() # 释放视频写入器
print(f"\n🔌 视频流已关闭,累计已录:{total_recorded_frames/video_fps:.1f}")
# 程序结束
print("\n📋 程序结束")

240
video_new.py Normal file
View File

@ -0,0 +1,240 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
'''
# @Time : 2025/9/11 16:48
# @Author : reenrr
# @File : video_new.py
'''
import cv2
import time
import os
from PIL import Image
import shutil # 用于检查磁盘空间
from cls_inference.cls_inference import yolov11_cls_inference
import numpy as np
# ================== 配置参数 ==================
url = "rtsp://admin:XJ123456@192.168.1.50:554/streaming/channels/101"
check_interval = 100 # 每隔 N 帧检查一次“盖板对齐”状态
output_video_dir = os.path.join("camera01_videos") # 视频保存目录
# 多帧确认参数
required_consecutive_frames = 3 # 需要连续 N 帧检测为"盖板对齐"才开始录制
# 摄像头重连参数
max_retry_seconds = 10 # 最大重试时间为10秒
retry_interval_seconds = 1 # 每隔1秒尝试重新连接一次
# 分类模型参数
cls_model_path = "/userdata/data_collection/cls_inference/yolov11_cls.rknn" # 分类模型路径
target_size = (640, 640)
# 视频录制参数
video_fps = 25 # 视频帧率
video_codec = cv2.VideoWriter_fourcc(*'mp4v') # MP4 编码
video_duration = 60 # 每次检测到符合条件后录制的秒数
frame_per_video = video_fps * video_duration # 每个视频的总帧数
def rotate_frame_180(pil_image):
"""
统一处理将PIL图像旋转180度并转为OpenCV录制所需的BGR格式
input: pil_image (PIL.Image对象RGB格式)
output: rotated_bgr (numpy数组BGR格式旋转180度后)
"""
# 1. 旋转180度expand=True避免图像被裁剪
rotated_pil = pil_image.rotate(180, expand=True)
# 2. 转为numpy数组此时是RGB格式
rotated_rgb = np.array(rotated_pil)
# 3. 转为BGR格式OpenCV VideoWriter要求BGR输入
rotated_bgr = cv2.cvtColor(rotated_rgb, cv2.COLOR_RGB2BGR)
return rotated_bgr
if __name__ == '__main__':
# 视频录制状态变量
is_recording = False # 是否正在录制视频
video_writer = None # 视频写入对象
recorded_frames = 0 # 当前视频已录制帧数
# 创建视频目录
os.makedirs(output_video_dir, exist_ok=True)
print(f"✅ 已创建/确认视频目录: {output_video_dir}")
while True: # 外层循环:处理摄像头断连重连
cap = cv2.VideoCapture(url)
# 设置RTSP流缓存大小
cap.set(cv2.CAP_PROP_BUFFERSIZE, 5) # 5MB缓存
# 强制指定解码方式
cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(*'H264'))
start_time = time.time()
# 摄像头连接重试逻辑
while not cap.isOpened():
if time.time() - start_time >= max_retry_seconds:
print(f"已尝试重新连接 {max_retry_seconds} 秒,仍无法获取视频流,程序退出。")
# 若退出时正在录制,先释放视频写入对象
if video_writer is not None:
video_writer.release()
video_writer = None
cap.release()
exit()
print("无法打开摄像头,正在尝试重新连接...")
time.sleep(retry_interval_seconds)
cap.release()
cap = cv2.VideoCapture(url)
# 获取摄像头实际参数
frame_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
frame_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
actual_fps = cap.get(cv2.CAP_PROP_FPS)
# 修正帧率
if actual_fps > 0:
video_fps = int(actual_fps)
frame_per_video = video_fps * video_duration
print(f"✅ 开始读取视频流(分辨率:{frame_width}x{frame_height},帧率:{video_fps}...")
frame_count = 0 # 总帧计数器
consecutive_ready_count = 0 # 连续"盖板对齐"计数
confirmed_frames = [] # 存储确认的"盖板对齐"帧(用于录制开始前的帧)
try:
while True:
ret, frame = cap.read()
if not ret:
print("读取帧失败,可能是流中断或摄像头断开")
# 若断连时正在录制,先释放视频写入对象
if video_writer is not None:
video_writer.release()
video_writer = None
is_recording = False
recorded_frames = 0
print("⚠️ 流中断时正在录制,已保存当前视频。")
# 重置检测状态
consecutive_ready_count = 0
confirmed_frames = []
cap.release()
break # 跳出内层循环,重新连接摄像头
frame_count += 1
# 转换为RGB并创建PIL图像用于后续处理
rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
pil_image = Image.fromarray(rgb_frame)
# 预先旋转180度为后续可能的录制做准备
rotated_bgr = rotate_frame_180(pil_image)
# 按间隔检测“盖板对齐”状态(仅未录制时执行)
if not is_recording and frame_count % check_interval == 0:
try:
# 调用模型判断状态(使用旋转后的图像)
class_name = yolov11_cls_inference(cls_model_path, rotated_bgr, target_size)
# 校验类别有效性
if not isinstance(class_name, str) or class_name not in ["cover_ready", "cover_noready"]:
print(f"跳过检测:模型返回无效类别({class_name} (总帧:{frame_count})")
consecutive_ready_count = 0
confirmed_frames = []
continue
# 检测到"盖板对齐"
if class_name == "cover_ready":
consecutive_ready_count += 1
# 保存当前确认的帧
confirmed_frames.append(rotated_bgr)
print(
f"检测到'盖板对齐',连续计数: {consecutive_ready_count}/{required_consecutive_frames} (总帧:{frame_count})")
# 达到所需连续帧数,开始录制
if consecutive_ready_count >= required_consecutive_frames:
# 检查磁盘空间
total, used, free = shutil.disk_usage(output_video_dir)
if free < 1024 * 1024 * 1024 * 5:
print(f"❌ 磁盘空间严重不足(仅剩 {free / (1024 ** 3):.2f} GB停止录制。")
consecutive_ready_count = 0
confirmed_frames = []
raise SystemExit(1)
# 生成视频文件名
timestamp = time.strftime("%Y%m%d_%H%M%S")
video_filename = f"video_{timestamp}.mp4"
video_filepath = os.path.join(output_video_dir, video_filename)
# 初始化视频写入器
video_writer = cv2.VideoWriter(
video_filepath, video_codec, video_fps, (frame_width, frame_height)
)
if not video_writer.isOpened():
print(f"⚠️ 视频写入器初始化失败,无法录制视频(路径:{video_filepath}")
consecutive_ready_count = 0
confirmed_frames = []
continue
# 写入之前确认的帧
for confirmed_frame in confirmed_frames:
video_writer.write(confirmed_frame)
is_recording = True
# 已录制帧数为确认帧数量
recorded_frames = len(confirmed_frames)
print(
f"📹 开始录制视频(已连续{required_consecutive_frames}'盖板对齐',保存路径:{video_filepath}")
print(f"已写入 {recorded_frames} 帧确认帧")
# 重置计数器,为下一次检测做准备
consecutive_ready_count = 0
confirmed_frames = []
# 检测到"盖板不对齐",重置计数器
else:
print(f"盖板状态:{class_name} (总帧:{frame_count})")
if consecutive_ready_count > 0:
print(
f"检测到'盖板不对齐',重置连续计数 (当前计数: {consecutive_ready_count}/{required_consecutive_frames})")
consecutive_ready_count = 0
confirmed_frames = []
except Exception as e:
print(f"分类模型调用异常: {e}(总帧:{frame_count}")
consecutive_ready_count = 0
confirmed_frames = []
continue
# 若正在录制,持续写入旋转后的帧
if is_recording:
video_writer.write(rotated_bgr)
recorded_frames += 1
# 检查是否达到录制时长
if recorded_frames >= frame_per_video:
video_writer.release()
video_writer = None
is_recording = False
recorded_frames = 0
print(f"✅ 视频录制完成(已达 {video_duration} 秒,共录制 {frame_per_video} 帧)")
except KeyboardInterrupt:
print("\n用户中断程序")
# 中断时若正在录制,先保存视频
if video_writer is not None:
video_writer.release()
print("⚠️ 用户中断时正在录制,已保存当前视频。")
break
finally:
# 释放资源
cap.release()
cv2.destroyAllWindows()
if video_writer is not None:
video_writer.release()
is_recording = False
recorded_frames = 0
consecutive_ready_count = 0
confirmed_frames = []
print(f"视频流已关闭,共处理总帧:{frame_count}")
print("程序结束")

235
video_new_test.py Normal file
View File

@ -0,0 +1,235 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
'''
# @Time : 2025/9/11 16:48
# @Author : reenrr
# @File : video_new.py
'''
import cv2
import time
import os
from PIL import Image
import shutil # 用于检查磁盘空间
from cls_inference.cls_inference import yolov11_cls_inference
import numpy as np
# ================== 配置参数 ==================
url = "rtsp://admin:XJ123456@192.168.1.50:554/streaming/channels/101"
check_interval = 100 # 每隔 N 帧检查一次“盖板对齐”状态
output_video_dir = os.path.join("camera01_videos") # 视频保存目录
# 多帧确认参数
required_consecutive_frames = 3 # 需要连续 N 帧检测为"盖板不对齐"才开始录制
# 摄像头重连参数
max_retry_seconds = 10 # 最大重试时间为10秒
retry_interval_seconds = 1 # 每隔1秒尝试重新连接一次
# 分类模型参数
cls_model_path = "/userdata/data_collection/cls_inference/yolov11_cls.rknn" # 分类模型路径
target_size = (640, 640)
# 视频录制参数
video_fps = 25 # 视频帧率
video_codec = cv2.VideoWriter_fourcc(*'mp4v') # MP4 编码
video_duration = 20 # 每次检测到符合条件后录制的秒数
frame_per_video = video_fps * video_duration # 每个视频的总帧数
def rotate_frame_180(pil_image):
"""
统一处理将PIL图像旋转180度并转为OpenCV录制所需的BGR格式
input: pil_image (PIL.Image对象RGB格式)
output: rotated_bgr (numpy数组BGR格式旋转180度后)
"""
# 1. 旋转180度expand=True避免图像被裁剪
rotated_pil = pil_image.rotate(180, expand=True)
# 2. 转为numpy数组此时是RGB格式
rotated_rgb = np.array(rotated_pil)
# 3. 转为BGR格式OpenCV VideoWriter要求BGR输入
rotated_bgr = cv2.cvtColor(rotated_rgb, cv2.COLOR_RGB2BGR)
return rotated_bgr
if __name__ == '__main__':
# 视频录制状态变量
is_recording = False # 是否正在录制视频
video_writer = None # 视频写入对象
recorded_frames = 0 # 当前视频已录制帧数
# 创建视频目录
os.makedirs(output_video_dir, exist_ok=True)
print(f"✅ 已创建/确认视频目录: {output_video_dir}")
while True: # 外层循环:处理摄像头断连重连
cap = cv2.VideoCapture(url)
# 设置RTSP流缓存大小
cap.set(cv2.CAP_PROP_BUFFERSIZE, 5) # 5MB缓存
# 强制指定解码方式
cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(*'H264'))
start_time = time.time()
# 摄像头连接重试逻辑
while not cap.isOpened():
if time.time() - start_time >= max_retry_seconds:
print(f"已尝试重新连接 {max_retry_seconds} 秒,仍无法获取视频流,程序退出。")
# 若退出时正在录制,先释放视频写入对象
if video_writer is not None:
video_writer.release()
video_writer = None
cap.release()
exit()
print("无法打开摄像头,正在尝试重新连接...")
time.sleep(retry_interval_seconds)
cap.release()
cap = cv2.VideoCapture(url)
# 获取摄像头实际参数
frame_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
frame_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
actual_fps = cap.get(cv2.CAP_PROP_FPS)
# 修正帧率
if actual_fps > 0:
video_fps = int(actual_fps)
frame_per_video = video_fps * video_duration
print(f"✅ 开始读取视频流(分辨率:{frame_width}x{frame_height},帧率:{video_fps}...")
frame_count = 0 # 总帧计数器
consecutive_noready_count = 0 # 连续"盖板不对齐"计数
confirmed_frames = [] # 存储确认的"盖板不对齐"帧(用于录制开始前的帧)
try:
while True:
ret, frame = cap.read()
if not ret:
print("读取帧失败,可能是流中断或摄像头断开")
# 若断连时正在录制,先释放视频写入对象
if video_writer is not None:
video_writer.release()
video_writer = None
is_recording = False
recorded_frames = 0
print("⚠️ 流中断时正在录制,已保存当前视频。")
# 重置检测状态
consecutive_noready_count = 0
confirmed_frames = []
cap.release()
break # 跳出内层循环,重新连接摄像头
frame_count += 1
# 转换为RGB并创建PIL图像用于后续处理
rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
pil_image = Image.fromarray(rgb_frame)
# 预先旋转180度为后续可能的录制做准备
rotated_bgr = rotate_frame_180(pil_image)
# 按间隔检测“盖板对齐”状态(仅未录制时执行)
if not is_recording and frame_count % check_interval == 0:
try:
# 调用模型判断状态(使用旋转后的图像)
class_name = yolov11_cls_inference(cls_model_path, rotated_bgr, target_size)
# 校验类别有效性
if not isinstance(class_name, str) or class_name not in ["cover_ready", "cover_noready"]:
print(f"跳过检测:模型返回无效类别({class_name} (总帧:{frame_count})")
consecutive_noready_count = 0
confirmed_frames = []
continue
# 检测到"盖板不对齐"
if class_name == "cover_noready":
consecutive_noready_count += 1
# 保存当前确认的帧
confirmed_frames.append(rotated_bgr)
print(f"检测到'盖板不对齐',连续计数: {consecutive_noready_count}/{required_consecutive_frames} (总帧:{frame_count})")
# 达到所需连续帧数,开始录制
if consecutive_noready_count >= required_consecutive_frames:
# 检查磁盘空间
total, used, free = shutil.disk_usage(output_video_dir)
if free < 1024 * 1024 * 1024 * 5:
print(f"❌ 磁盘空间严重不足(仅剩 {free / (1024 ** 3):.2f} GB停止录制。")
consecutive_noready_count = 0
confirmed_frames = []
raise SystemExit(1)
# 生成视频文件名
timestamp = time.strftime("%Y%m%d_%H%M%S")
video_filename = f"video_{timestamp}.mp4"
video_filepath = os.path.join(output_video_dir, video_filename)
# 初始化视频写入器
video_writer = cv2.VideoWriter(
video_filepath, video_codec, video_fps, (frame_width, frame_height)
)
if not video_writer.isOpened():
print(f"⚠️ 视频写入器初始化失败,无法录制视频(路径:{video_filepath}")
consecutive_noready_count = 0
confirmed_frames = []
continue
# 写入之前确认的帧
for confirmed_frame in confirmed_frames:
video_writer.write(confirmed_frame)
is_recording = True
# 已录制帧数为确认帧数量
recorded_frames = len(confirmed_frames)
print(f"📹 开始录制视频(已连续{required_consecutive_frames}'盖板不对齐',保存路径:{video_filepath}")
print(f"已写入 {recorded_frames} 帧确认帧")
# 重置计数器,为下一次检测做准备
consecutive_noready_count = 0
confirmed_frames = []
# 检测到"盖板对齐",重置计数器
else:
print(f"盖板状态:{class_name} (总帧:{frame_count})")
if consecutive_noready_count > 0:
print(f"检测到'盖板对齐',重置连续计数 (当前计数: {consecutive_noready_count}/{required_consecutive_frames})")
consecutive_noready_count = 0
confirmed_frames = []
except Exception as e:
print(f"分类模型调用异常: {e}(总帧:{frame_count}")
consecutive_noready_count = 0
confirmed_frames = []
continue
# 若正在录制,持续写入旋转后的帧
if is_recording:
video_writer.write(rotated_bgr)
recorded_frames += 1
# 检查是否达到录制时长
if recorded_frames >= frame_per_video:
video_writer.release()
video_writer = None
is_recording = False
recorded_frames = 0
print(f"✅ 视频录制完成(已达 {video_duration} 秒,共录制 {frame_per_video} 帧)")
except KeyboardInterrupt:
print("\n用户中断程序")
# 中断时若正在录制,先保存视频
if video_writer is not None:
video_writer.release()
print("⚠️ 用户中断时正在录制,已保存当前视频。")
break
finally:
# 释放资源
cap.release()
cv2.destroyAllWindows()
if video_writer is not None:
video_writer.release()
is_recording = False
recorded_frames = 0
consecutive_noready_count = 0
confirmed_frames = []
print(f"视频流已关闭,共处理总帧:{frame_count}")
print("程序结束")