feeding
This commit is contained in:
531
vision/camera.py
531
vision/camera.py
@ -1,67 +1,486 @@
|
||||
# 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 CameraController:
|
||||
def __init__(self):
|
||||
self.camera = None
|
||||
self.camera_type = "ip"
|
||||
self.camera_ip = "192.168.1.51"
|
||||
self.camera_port = 554
|
||||
self.camera_username = "admin"
|
||||
self.camera_password = "XJ123456"
|
||||
self.camera_channel = 1
|
||||
|
||||
def set_config(self, camera_type="ip", ip=None, port=None, username=None, password=None, channel=1):
|
||||
"""
|
||||
设置摄像头配置
|
||||
"""
|
||||
self.camera_type = camera_type
|
||||
if ip:
|
||||
self.camera_ip = ip
|
||||
if port:
|
||||
self.camera_port = port
|
||||
if username:
|
||||
self.camera_username = username
|
||||
if password:
|
||||
self.camera_password = password
|
||||
self.camera_channel = channel
|
||||
|
||||
def setup_capture(self, camera_index=0):
|
||||
"""
|
||||
设置摄像头捕获
|
||||
"""
|
||||
try:
|
||||
rtsp_url = f"rtsp://{self.camera_username}:{self.camera_password}@{self.camera_ip}:{self.camera_port}/streaming/channels/{self.camera_channel}01"
|
||||
self.camera = cv2.VideoCapture(rtsp_url)
|
||||
|
||||
if not self.camera.isOpened():
|
||||
print(f"无法打开网络摄像头: {rtsp_url}")
|
||||
return False
|
||||
print(f"网络摄像头初始化成功,地址: {rtsp_url}")
|
||||
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:
|
||||
# 使用高精度时间戳
|
||||
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
|
||||
except Exception as e:
|
||||
print(f"摄像头设置失败: {e}")
|
||||
return False
|
||||
|
||||
def capture_frame(self):
|
||||
"""捕获当前帧并返回numpy数组"""
|
||||
|
||||
try:
|
||||
if self.camera is None:
|
||||
print("摄像头未初始化")
|
||||
return None
|
||||
|
||||
ret, frame = self.camera.read()
|
||||
if ret:
|
||||
return frame
|
||||
else:
|
||||
print("无法捕获图像帧")
|
||||
return None
|
||||
# 初始化摄像头和队列
|
||||
for camera_id in ['cam1', '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 ['cam1', '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}")
|
||||
print(f"启动摄像头失败: {e}")
|
||||
self.release()
|
||||
return False
|
||||
|
||||
def get_latest_frames(self, sync_threshold_ms: Optional[float] = None) -> Optional[Tuple[np.ndarray, np.ndarray]]:
|
||||
"""获取最新的同步帧对"""
|
||||
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['cam1'].empty():
|
||||
dt_t1, frame_latest = self.frame_queues['cam1'].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):
|
||||
"""释放摄像头资源"""
|
||||
if self.camera is not None:
|
||||
self.camera.release()
|
||||
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 = {
|
||||
'cam1': {
|
||||
'type': 'ip',
|
||||
'ip': '192.168.250.60',
|
||||
'port': 554,
|
||||
'username': 'admin',
|
||||
'password': 'XJ123456',
|
||||
'channel': 1
|
||||
},
|
||||
'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("双摄像头系统启动失败!")
|
||||
|
||||
Reference in New Issue
Block a user