Files
Feeding_control_system/vision/camera.py
2025-12-12 18:00:14 +08:00

504 lines
19 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# vision/camera.py
import cv2
import threading
import queue
import time
import numpy as np
from datetime import datetime
from typing import Optional, Tuple, Dict, Any
class DualCameraController:
"""双摄像头控制器 - 支持多线程捕获和同步帧获取"""
def __init__(self, camera_configs: Dict[str, Dict[str, Any]], max_queue_size: int = 10, sync_threshold_ms: float = 50.0):
# 摄像头配置
self.camera_configs = camera_configs
# 摄像头对象和队列
self.cameras: Dict[str, cv2.VideoCapture] = {}
self.frame_queues: Dict[str, queue.Queue] = {}
self.capture_threads: Dict[str, threading.Thread] = {}
# 线程控制
self.stop_event = threading.Event()
self.max_queue_size = max_queue_size
self.sync_threshold_ms = sync_threshold_ms
self.last_sync_pair: Tuple[Optional[np.ndarray], Optional[np.ndarray]] = (None, None)
# 摄像头状态
self.is_running = False
def set_camera_config(self, camera_id: str, ip: str, username: str = "admin",
password: str = "XJ123456", port: int = 554, channel: int = 1):
"""设置指定摄像头的配置"""
if camera_id in ['cam1', 'cam2']:
self.camera_configs[camera_id].update({
'ip': ip,
'username': username,
'password': password,
'port': port,
'channel': channel
})
print(f"摄像头 {camera_id} 配置已更新: IP={ip}")
else:
raise ValueError(f"无效的摄像头ID: {camera_id}")
def _build_rtsp_url(self, camera_id: str) -> str:
"""构建RTSP URL"""
config = self.camera_configs[camera_id]
return f"rtsp://{config['username']}:{config['password']}@{config['ip']}:{config['port']}/Streaming/Channels/{config['channel']}01"
def _capture_thread(self, camera_id: str):
"""摄像头捕获线程"""
cap = self.cameras[camera_id]
q = self.frame_queues[camera_id]
rtsp_url = self._build_rtsp_url(camera_id)
print(f"启动 {camera_id} 捕获线程")
while not self.stop_event.is_set():
try:
# print('aaaaa')
ret, frame = cap.read()
if ret and frame is not None:
# 在帧右上角添加时间戳
current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]
# 获取帧尺寸
height, width = frame.shape[:2]
# 设置文字参数
font = cv2.FONT_HERSHEY_SIMPLEX
font_scale = 0.6
thickness = 2
color = (0, 255, 0) # 绿色
# 计算文字位置(右上角)
text_size = cv2.getTextSize(current_time, font, font_scale, thickness)[0]
text_x = width - text_size[0] - 10 # 距离右边10像素
text_y = 30 # 距离顶部30像素
# 添加文字背景(半透明)
overlay = frame.copy()
cv2.rectangle(overlay, (text_x - 5, text_y - text_size[1] - 5),
(text_x + text_size[0] + 5, text_y + 5), (0, 0, 0), -1)
cv2.addWeighted(overlay, 0.7, frame, 0.3, 0, frame)
# 添加时间戳文字
cv2.putText(frame, current_time, (text_x, text_y), font, font_scale, color, thickness)
# 使用高精度时间戳
timestamp = time.time()
# 检查队列是否已满
if q.qsize() >= self.max_queue_size:
# 队列已满丢弃最旧帧FIFO
try:
q.get_nowait() # 移除最旧帧
q.put_nowait((timestamp, frame))
except queue.Empty:
# 理论上不会发生,但安全处理
pass
else:
# 队列未满,直接添加
q.put_nowait((timestamp, frame))
else:
print(f"{camera_id} 读取失败,重连中...")
time.sleep(1)
cap.open(rtsp_url)
except Exception as e:
print(f"{camera_id} 捕获异常: {e}")
time.sleep(1)
print(f"{camera_id} 捕获线程已停止")
def start_cameras(self) -> bool:
"""启动双摄像头"""
if self.is_running:
print("摄像头已在运行中")
return True
try:
# 初始化摄像头和队列
for camera_id in ['cam2']:
rtsp_url = self._build_rtsp_url(camera_id)
cap = cv2.VideoCapture(rtsp_url)
if not cap.isOpened():
print(f"无法打开摄像头 {camera_id}: {rtsp_url}")
# 清理已打开的摄像头
self.release()
return False
self.cameras[camera_id] = cap
self.frame_queues[camera_id] = queue.Queue(maxsize=self.max_queue_size)
print(f"摄像头 {camera_id} 初始化成功: {rtsp_url}")
# 启动捕获线程
self.stop_event.clear()
for camera_id in ['cam2']:
thread = threading.Thread(
target=self._capture_thread,
args=(camera_id,),
daemon=True
)
self.capture_threads[camera_id] = thread
thread.start()
self.is_running = True
print("双摄像头系统启动成功")
return True
except Exception as e:
print(f"启动摄像头失败: {e}")
self.release()
return False
def get_latest_frames(self, sync_threshold_ms: Optional[float] = None) -> Optional[Tuple[np.ndarray, np.ndarray]]:
"""获取最新的同步帧对"""
return
if not self.is_running:
print("摄像头未运行")
return None
sync_threshold = sync_threshold_ms or self.sync_threshold_ms
sync_threshold_sec = sync_threshold / 1000.0
# 检查队列是否有数据
if (self.frame_queues['cam1'].empty() or
self.frame_queues['cam2'].empty()):
return None
try:
# 获取最新帧
ts1, f1 = self.frame_queues['cam1'].queue[-1]
ts2, f2 = self.frame_queues['cam2'].queue[-1]
dt = abs(ts1 - ts2)
if dt < sync_threshold_sec:
# 时间差在阈值内,认为是同步的
frame1, frame2 = f1.copy(), f2.copy()
self.last_sync_pair = (frame1, frame2)
return (frame1, frame2)
else:
# 搜索最近5帧找最小时间差
min_dt = float('inf')
best_pair = None
# 获取最近5帧
cam1_frames = list(self.frame_queues['cam1'].queue)[-5:]
cam2_frames = list(self.frame_queues['cam2'].queue)[-5:]
for t1_local, f1_local in cam1_frames:
for t2_local, f2_local in cam2_frames:
dt_local = abs(t1_local - t2_local)
if dt_local < min_dt and dt_local < sync_threshold_sec * 2: # 更宽松的阈值
min_dt = dt_local
best_pair = (f1_local.copy(), f2_local.copy())
if best_pair:
self.last_sync_pair = best_pair
return best_pair
else:
# 没找到同步帧,返回最新非同步帧
return (f1.copy(), f2.copy())
except Exception as e:
print(f"获取帧对失败: {e}")
return None
def get_single_frame(self, camera_id: str) -> Optional[np.ndarray]:
"""获取单个摄像头的最新帧"""
if not self.is_running:
print("摄像头未运行")
return None
if camera_id not in self.frame_queues:
print(f"无效的摄像头ID: {camera_id}")
return None
try:
if not self.frame_queues[camera_id].empty():
_, frame = self.frame_queues[camera_id].queue[-1]
return frame.copy()
return None
except Exception as e:
print(f"获取单帧失败: {e}")
return None
def get_single_latest_frame(self) -> Optional[np.ndarray]:
"""获取单个摄像头的最新帧"""
if not self.is_running:
print("摄像头未运行")
return None
try:
frame_latest = None
dt_t1 = None
# 获取cam1的最新帧
if not self.frame_queues['cam2'].empty():
dt_t1, frame_latest = self.frame_queues['cam2'].queue[-1]
# 获取cam2的最新帧选择时间戳更新的那个
# if frame_latest is None:
# if not self.frame_queues['cam2'].empty():
# dt_t2, frame2 = self.frame_queues['cam2'].queue[-1]
# if dt_t1 is None or dt_t2 > dt_t1:
# frame_latest = frame2
# 返回最新帧的副本(如果找到)
return frame_latest.copy() if frame_latest is not None else None
except Exception as e:
print(f"获取单帧失败: {e}")
return None
def get_single_latest_frame2(self) -> Optional[np.ndarray]:
"""获取单个摄像头的最新帧"""
if not self.is_running:
print("摄像头未运行")
return None
try:
frame_latest = None
dt_t1 = None
# 获取cam1的最新帧
if not self.frame_queues['cam2'].empty():
dt_t1, frame_latest = self.frame_queues['cam2'].queue[-1]
# 获取cam2的最新帧选择时间戳更新的那个
if frame_latest is None:
if not self.frame_queues['cam1'].empty():
dt_t2, frame2 = self.frame_queues['cam1'].queue[-1]
if dt_t1 is None or dt_t2 > dt_t1:
frame_latest = frame2
# 返回最新帧的副本(如果找到)
return frame_latest.copy() if frame_latest is not None else None
except Exception as e:
print(f"获取单帧失败: {e}")
return None
def get_notification_frame(self, camera_id: str = None, use_sync: bool = True) -> Optional[np.ndarray]:
"""根据通知参数获取最近的帧
Args:
camera_id: 摄像头ID ('cam1', 'cam2')如果为None则根据use_sync决定
use_sync: 是否使用同步帧对如果为True则返回同步帧对否则返回指定摄像头的单帧
Returns:
单帧图像或同步帧对
"""
if not self.is_running:
print("摄像头未运行")
return None
if use_sync:
# 获取同步帧对,返回拼接后的图像
frames = self.get_latest_frames()
if frames:
frame1, frame2 = frames
# 调整大小并拼接
h, w = 480, 640
frame1_resized = cv2.resize(frame1, (w, h))
frame2_resized = cv2.resize(frame2, (w, h))
combined = np.hstack((frame1_resized, frame2_resized))
# 添加时间戳信息
ts1 = time.time()
cv2.putText(combined, f"Sync: {datetime.fromtimestamp(ts1).strftime('%H:%M:%S.%f')[:-3]}",
(10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2)
return combined
return None
else:
# 获取指定摄像头的单帧
if camera_id is None:
camera_id = 'cam1' # 默认返回cam1
return self.get_single_frame(camera_id)
def display_live_feed(self):
"""实时显示双摄像头画面(调试用)"""
if not self.is_running:
print("请先启动摄像头")
return
print("'q' 退出显示,按 's' 保存同步帧")
while True:
frame = self.get_notification_frame(use_sync=True)
if frame is not None:
cv2.imshow("Dual Camera Feed", frame)
key = cv2.waitKey(1) & 0xFF
if key == ord('q'):
break
elif key == ord('s'):
# 保存同步帧
sync_frames = self.get_latest_frames()
if sync_frames:
frame1, frame2 = sync_frames
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")[:20]
cv2.imwrite(f"cam1_{timestamp}.jpg", frame1)
cv2.imwrite(f"cam2_{timestamp}.jpg", frame2)
print(f"✅ 保存同步帧: cam1_{timestamp}.jpg & cam2_{timestamp}.jpg")
cv2.destroyAllWindows()
def release(self):
"""释放摄像头资源"""
print("正在释放摄像头资源...")
# 停止捕获线程
if self.is_running:
self.stop_event.set()
# 等待线程结束
for camera_id, thread in self.capture_threads.items():
if thread.is_alive():
thread.join(timeout=2)
print(f"{camera_id} 捕获线程已停止")
self.capture_threads.clear()
self.is_running = False
# 释放摄像头
for camera_id, cap in self.cameras.items():
if cap is not None:
cap.release()
print(f"摄像头 {camera_id} 已释放")
self.cameras.clear()
self.frame_queues.clear()
print("摄像头资源释放完成")
def __del__(self):
"""析构函数,确保资源释放"""
self.release()
# 类方法:快速创建和启动
@classmethod
def create_and_start(cls, camera_configs: Dict[str, Dict[str, Any]]) -> Optional['DualCameraController']:
"""快速创建并启动双摄像头控制器"""
controller = cls(camera_configs)
if controller.start_cameras():
return controller
else:
return None
# 向后兼容的单摄像头控制器
class CameraController:
"""单摄像头控制器 - 向后兼容"""
def __init__(self):
self.dual_controller = DualCameraController()
self.default_camera = 'cam1'
def set_config(self, camera_type="ip", ip=None, port=None, username=None, password=None, channel=1):
"""设置摄像头配置 - 兼容旧接口"""
self.dual_controller.set_camera_config(
'cam1', ip or "192.168.1.51", username or "admin",
password or "XJ123456", port or 554, channel
)
def setup_capture(self, camera_index=0):
"""设置摄像头捕获 - 兼容旧接口"""
return self.dual_controller.start_cameras()
def capture_frame(self):
"""捕获当前帧 - 兼容旧接口"""
return self.dual_controller.capture_frame(self.default_camera)
def capture_frame_bak(self):
"""捕获当前帧(备用) - 兼容旧接口"""
return self.dual_controller.capture_frame_bak(self.default_camera)
def release(self):
"""释放摄像头资源"""
self.dual_controller.release()
def __del__(self):
"""析构函数"""
self.release()
# 使用示例和测试
if __name__ == "__main__":
# 创建双摄像头控制器
camera_configs = {
'cam2': {
'type': 'ip',
'ip': '192.168.250.61',
'port': 554,
'username': 'admin',
'password': 'XJ123456',
'channel': 1
}
}
controller = DualCameraController.create_and_start(camera_configs)
if controller:
print("双摄像头系统启动成功!")
# 示例1获取同步帧对
print("\n=== 获取同步帧 ===")
while True:
single_frame = controller.get_single_latest_frame()
if single_frame is not None:
print(f"获取到帧形状: {single_frame.shape}")
cv2.imshow("Single Camera Frame", single_frame)
else:
print("未获取到帧")
key = cv2.waitKey(1) & 0xFF
if key == ord('s') and single_frame is not None:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")[:20]
cv2.imwrite(f"single_frame_{timestamp}.jpg", single_frame)
print(f"✅ 保存单帧: single_frame_{timestamp}.jpg")
if key == ord('q'):
break
time.sleep(1)
# controller.get_single_latest_frame('cam2')
# sync_frames = controller.get_latest_frames(sync_threshold_ms=50)
# if sync_frames:
# frame1, frame2 = sync_frames
# print(f"获取到同步帧对 - 帧1形状: {frame1.shape}, 帧2形状: {frame2.shape}")
# else:
# print("未获取到同步帧对")
# 示例2根据通知参数获取帧
# print("\n=== 根据通知参数获取帧 ===")
# # 获取同步拼接帧(用于显示)
# combined_frame = controller.get_notification_frame(use_sync=True)
# if combined_frame is not None:
# print(f"获取到同步拼接帧,形状: {combined_frame.shape}")
# cv2.imshow("Sync Frame", combined_frame)
# cv2.waitKey(1000) # 显示1秒
# cv2.destroyAllWindows()
# # 获取单个摄像头帧
# single_frame = controller.get_notification_frame(camera_id='cam1', use_sync=False)
# if single_frame is not None:
# print(f"获取到cam1单帧形状: {single_frame.shape}")
# # 示例3实时显示
# print("\n=== 实时显示模式 ===")
# print("按 'q' 退出显示,按 's' 保存同步帧")
# # controller.display_live_feed() # 取消注释以启用实时显示
# # 示例4兼容性测试
# print("\n=== 兼容性测试 ===")
# old_frame = controller.capture_frame('cam1')
# if old_frame is not None:
# print(f"旧接口兼容 - 帧形状: {old_frame.shape}")
# 清理
controller.release()
print("\n摄像头资源已释放")
else:
print("双摄像头系统启动失败!")