calibration.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729
  1. """
  2. 相机校准模块
  3. 实现全景相机与球机的自动校准
  4. 建立画面坐标到PTZ角度的映射关系
  5. """
  6. import time
  7. import math
  8. import threading
  9. import numpy as np
  10. import cv2
  11. from typing import List, Tuple, Dict, Optional, Callable
  12. from dataclasses import dataclass, field
  13. from enum import Enum
  14. from ptz_camera import PTZCamera
  15. class CalibrationState(Enum):
  16. """校准状态"""
  17. IDLE = 0 # 空闲
  18. RUNNING = 1 # 校准中
  19. SUCCESS = 2 # 校准成功
  20. FAILED = 3 # 校准失败
  21. @dataclass
  22. class CalibrationPoint:
  23. """校准点"""
  24. pan: float # 球机水平角度
  25. tilt: float # 球机垂直角度
  26. zoom: float # 变倍
  27. x_ratio: float = 0.0 # 全景画面X比例
  28. y_ratio: float = 0.0 # 全景画面Y比例
  29. detected: bool = False # 是否已检测到
  30. @dataclass
  31. class CalibrationResult:
  32. """校准结果"""
  33. success: bool
  34. points: List[CalibrationPoint]
  35. transform_matrix: Optional[np.ndarray] = None # 变换矩阵
  36. error_message: str = ""
  37. rms_error: float = 0.0 # 均方根误差
  38. class VisualCalibrationDetector:
  39. """
  40. 视觉校准检测器
  41. 通过视觉方法检测球机在全景画面中的位置
  42. """
  43. def __init__(self):
  44. """初始化检测器"""
  45. # 特征匹配器 - 优先SIFT,备选ORB
  46. try:
  47. self.feature_detector = cv2.SIFT_create()
  48. self.feature_type = 'SIFT'
  49. except AttributeError:
  50. # SIFT需要opencv-contrib,ORB是基础版自带的
  51. self.feature_detector = cv2.ORB_create(nfeatures=500)
  52. self.feature_type = 'ORB'
  53. print("提示: 使用ORB特征检测器 (安装opencv-contrib可启用SIFT)")
  54. self.matcher = cv2.BFMatcher(cv2.NORM_L2 if self.feature_type == 'SIFT' else cv2.NORM_HAMMING)
  55. # 校准模式
  56. self.use_motion_detection = True
  57. self.use_feature_matching = True
  58. def detect_by_motion(self, frames_before: np.ndarray,
  59. frames_after: np.ndarray) -> Optional[Tuple[float, float]]:
  60. """
  61. 通过运动检测定位球机指向位置
  62. 原理: 球机移动会在全景画面中产生运动区域
  63. Args:
  64. frames_before: 球机移动前的全景帧 (多帧平均)
  65. frames_after: 球机移动后的全景帧 (多帧平均)
  66. Returns:
  67. (x_ratio, y_ratio) 或 None
  68. """
  69. if frames_before is None or frames_after is None:
  70. return None
  71. # 计算帧差
  72. if len(frames_before.shape) == 3:
  73. before_gray = cv2.cvtColor(frames_before, cv2.COLOR_BGR2GRAY)
  74. else:
  75. before_gray = frames_before
  76. if len(frames_after.shape) == 3:
  77. after_gray = cv2.cvtColor(frames_after, cv2.COLOR_BGR2GRAY)
  78. else:
  79. after_gray = frames_after
  80. # 计算差异
  81. diff = cv2.absdiff(before_gray, after_gray)
  82. # 二值化
  83. _, thresh = cv2.threshold(diff, 25, 255, cv2.THRESH_BINARY)
  84. # 形态学操作去噪
  85. kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (15, 15))
  86. thresh = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, kernel)
  87. thresh = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel)
  88. # 查找轮廓
  89. contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
  90. if not contours:
  91. return None
  92. # 找到最大的运动区域
  93. max_contour = max(contours, key=cv2.contourArea)
  94. area = cv2.contourArea(max_contour)
  95. # 过滤太小的区域
  96. if area < 500: # 最小面积阈值
  97. return None
  98. # 计算中心点
  99. M = cv2.moments(max_contour)
  100. if M["m00"] == 0:
  101. return None
  102. cx = M["m10"] / M["m00"]
  103. cy = M["m01"] / M["m00"]
  104. # 转换为比例
  105. h, w = before_gray.shape
  106. x_ratio = cx / w
  107. y_ratio = cy / h
  108. print(f" 运动检测: 中心=({cx:.1f}, {cy:.1f}), 面积={area:.0f}")
  109. return (x_ratio, y_ratio)
  110. def detect_by_feature_match(self, panorama_frame: np.ndarray,
  111. ptz_frame: np.ndarray) -> Optional[Tuple[float, float]]:
  112. """
  113. 通过特征匹配定位
  114. 将球机抓拍的图像与全景画面进行特征匹配
  115. Args:
  116. panorama_frame: 全景画面
  117. ptz_frame: 球机抓拍画面
  118. Returns:
  119. (x_ratio, y_ratio) 或 None
  120. """
  121. if panorama_frame is None or ptz_frame is None:
  122. return None
  123. # 转换为灰度
  124. if len(panorama_frame.shape) == 3:
  125. pan_gray = cv2.cvtColor(panorama_frame, cv2.COLOR_BGR2GRAY)
  126. else:
  127. pan_gray = panorama_frame
  128. if len(ptz_frame.shape) == 3:
  129. ptz_gray = cv2.cvtColor(ptz_frame, cv2.COLOR_BGR2GRAY)
  130. else:
  131. ptz_gray = ptz_frame
  132. # 检测特征点
  133. try:
  134. kp1, des1 = self.feature_detector.detectAndCompute(ptz_gray, None)
  135. kp2, des2 = self.feature_detector.detectAndCompute(pan_gray, None)
  136. if des1 is None or des2 is None:
  137. return None
  138. if len(kp1) < 4 or len(kp2) < 4:
  139. return None
  140. # 特征匹配
  141. matches = self.matcher.knnMatch(des1, des2, k=2)
  142. # 筛选好的匹配
  143. good_matches = []
  144. for m, n in matches:
  145. if m.distance < 0.75 * n.distance:
  146. good_matches.append(m)
  147. if len(good_matches) < 4:
  148. return None
  149. # 获取匹配点坐标
  150. ptz_pts = np.float32([kp1[m.queryIdx].pt for m in good_matches])
  151. pan_pts = np.float32([kp2[m.trainIdx].pt for m in good_matches])
  152. # 计算全景画面中匹配点的中心
  153. center_x = np.mean(pan_pts[:, 0])
  154. center_y = np.mean(pan_pts[:, 1])
  155. # 转换为比例
  156. h, w = pan_gray.shape
  157. x_ratio = center_x / w
  158. y_ratio = center_y / h
  159. print(f" 特征匹配: 匹配点={len(good_matches)}, 中心=({center_x:.1f}, {center_y:.1f})")
  160. return (x_ratio, y_ratio)
  161. except Exception as e:
  162. print(f" 特征匹配错误: {e}")
  163. return None
  164. def detect_position(self, panorama_frame: np.ndarray,
  165. frames_before: np.ndarray = None,
  166. frames_after: np.ndarray = None,
  167. ptz_frame: np.ndarray = None) -> Tuple[bool, float, float]:
  168. """
  169. 综合检测球机在全景画面中的位置
  170. Args:
  171. panorama_frame: 全景画面
  172. frames_before: 移动前的帧 (用于运动检测)
  173. frames_after: 移动后的帧 (用于运动检测)
  174. ptz_frame: 球机抓拍画面 (用于特征匹配)
  175. Returns:
  176. (是否成功, x_ratio, y_ratio)
  177. """
  178. results = []
  179. # 方法1: 运动检测
  180. if self.use_motion_detection and frames_before is not None and frames_after is not None:
  181. motion_result = self.detect_by_motion(frames_before, frames_after)
  182. if motion_result:
  183. results.append(('motion', motion_result, 0.6)) # 权重0.6
  184. # 方法2: 特征匹配
  185. if self.use_feature_matching and ptz_frame is not None:
  186. feature_result = self.detect_by_feature_match(panorama_frame, ptz_frame)
  187. if feature_result:
  188. results.append(('feature', feature_result, 0.4)) # 权重0.4
  189. if not results:
  190. return (False, 0.0, 0.0)
  191. # 加权融合结果
  192. total_weight = sum(r[2] for r in results)
  193. x_sum = sum(r[1][0] * r[2] for r in results)
  194. y_sum = sum(r[1][1] * r[2] for r in results)
  195. x_ratio = x_sum / total_weight
  196. y_ratio = y_sum / total_weight
  197. print(f" 融合结果: ({x_ratio:.3f}, {y_ratio:.3f})")
  198. return (True, x_ratio, y_ratio)
  199. class CameraCalibrator:
  200. """
  201. 相机校准器
  202. 建立全景相机坐标与球机PTZ角度的映射关系
  203. 使用视觉方法进行精确校准
  204. """
  205. def __init__(self, ptz_camera: PTZCamera,
  206. get_frame_func: Callable[[], np.ndarray],
  207. detect_marker_func: Callable[[np.ndarray], Optional[Tuple[float, float]]] = None,
  208. ptz_capture_func: Callable[[], np.ndarray] = None):
  209. """
  210. 初始化校准器
  211. Args:
  212. ptz_camera: 球机实例
  213. get_frame_func: 获取全景画面的函数
  214. detect_marker_func: 检测标记点的函数 (返回x_ratio, y_ratio)
  215. ptz_capture_func: 球机抓拍函数 (用于特征匹配)
  216. """
  217. self.ptz = ptz_camera
  218. self.get_frame = get_frame_func
  219. self.detect_marker = detect_marker_func
  220. self.ptz_capture = ptz_capture_func
  221. # 视觉检测器
  222. self.visual_detector = VisualCalibrationDetector()
  223. self.state = CalibrationState.IDLE
  224. self.result: Optional[CalibrationResult] = None
  225. # 校准点配置 (覆盖画面不同区域)
  226. self.calibration_points = self._generate_calibration_points()
  227. # 变换参数
  228. self.pan_offset = 0.0 # 水平偏移
  229. self.tilt_offset = 0.0 # 垂直偏移
  230. self.pan_scale = 1.0 # 水平缩放
  231. self.tilt_scale = 1.0 # 垂直缩放
  232. # 校准配置
  233. self.stabilize_time = 2.0 # 球机稳定等待时间
  234. self.use_motion_detection = True
  235. self.use_feature_matching = True
  236. # 回调
  237. self.on_progress: Optional[Callable[[int, int, str], None]] = None
  238. self.on_complete: Optional[Callable[[CalibrationResult], None]] = None
  239. def _generate_calibration_points(self) -> List[CalibrationPoint]:
  240. """
  241. 生成校准点
  242. 在球机视野范围内均匀分布
  243. Returns:
  244. 校准点列表
  245. """
  246. points = []
  247. # 5x5网格校准点 (共25个点)
  248. # 水平角度范围: 0-180度 (全景相机通常覆盖180度)
  249. # 垂直角度范围: -30到30度
  250. pan_range = (0, 180)
  251. tilt_range = (-30, 30)
  252. # 使用较少的点进行快速校准
  253. grid_size = 3 # 3x3 = 9个点
  254. for i in range(grid_size):
  255. for j in range(grid_size):
  256. pan = pan_range[0] + (pan_range[1] - pan_range[0]) * i / (grid_size - 1)
  257. tilt = tilt_range[0] + (tilt_range[1] - tilt_range[0]) * j / (grid_size - 1)
  258. points.append(CalibrationPoint(
  259. pan=pan,
  260. tilt=tilt,
  261. zoom=1.0 # 校准时使用最小变倍
  262. ))
  263. return points
  264. def calibrate(self, quick_mode: bool = True) -> CalibrationResult:
  265. """
  266. 执行校准 - 使用视觉检测方法
  267. Args:
  268. quick_mode: 快速模式 (使用较少校准点)
  269. Returns:
  270. 校准结果
  271. """
  272. self.state = CalibrationState.RUNNING
  273. if quick_mode:
  274. calib_points = self._get_quick_calibration_points()
  275. else:
  276. calib_points = self.calibration_points
  277. total_points = len(calib_points)
  278. valid_points = []
  279. print(f"开始视觉校准, 共 {total_points} 个校准点...")
  280. print(f"检测方法: 运动检测={'开启' if self.use_motion_detection else '关闭'}, "
  281. f"特征匹配={'开启' if self.use_feature_matching else '关闭'}")
  282. for idx, point in enumerate(calib_points):
  283. if self.on_progress:
  284. self.on_progress(idx + 1, total_points,
  285. f"正在校准点 {idx + 1}/{total_points}: pan={point.pan:.1f}°, tilt={point.tilt:.1f}°")
  286. print(f" 校准点 {idx + 1}/{total_points}: pan={point.pan:.1f}°, tilt={point.tilt:.1f}°")
  287. # ===== 步骤1: 获取移动前的全景帧 =====
  288. print(f" [1/4] 获取移动前的全景帧...")
  289. frames_before_list = []
  290. for _ in range(3): # 取3帧平均
  291. frame = self.get_frame()
  292. if frame is not None:
  293. frames_before_list.append(frame)
  294. time.sleep(0.1)
  295. if not frames_before_list:
  296. print(f" 警告: 无法获取移动前的全景画面")
  297. continue
  298. frames_before = np.mean(frames_before_list, axis=0).astype(np.uint8)
  299. # ===== 步骤2: 移动球机到指定位置 =====
  300. print(f" [2/4] 移动球机到目标位置...")
  301. if not self.ptz.goto_exact_position(point.pan, point.tilt, 1):
  302. print(f" 警告: 移动球机失败")
  303. continue
  304. # 等待球机稳定
  305. time.sleep(self.stabilize_time)
  306. # ===== 步骤3: 获取移动后的全景帧和球机抓拍 =====
  307. print(f" [3/4] 获取移动后的帧...")
  308. frames_after_list = []
  309. for _ in range(3):
  310. frame = self.get_frame()
  311. if frame is not None:
  312. frames_after_list.append(frame)
  313. time.sleep(0.1)
  314. if not frames_after_list:
  315. print(f" 警告: 无法获取移动后的全景画面")
  316. continue
  317. frames_after = np.mean(frames_after_list, axis=0).astype(np.uint8)
  318. panorama_frame = frames_after # 当前全景帧
  319. # 球机抓拍 (用于特征匹配)
  320. ptz_frame = None
  321. if self.use_feature_matching and self.ptz_capture:
  322. try:
  323. ptz_frame = self.ptz_capture()
  324. if ptz_frame is not None:
  325. print(f" 球机抓拍成功: {ptz_frame.shape}")
  326. except Exception as e:
  327. print(f" 球机抓拍失败: {e}")
  328. # ===== 步骤4: 视觉检测位置 =====
  329. print(f" [4/4] 视觉检测位置...")
  330. # 优先使用自定义标记检测
  331. if self.detect_marker:
  332. marker_pos = self.detect_marker(panorama_frame)
  333. if marker_pos:
  334. point.x_ratio, point.y_ratio = marker_pos
  335. point.detected = True
  336. valid_points.append(point)
  337. print(f" 标记检测成功: ({point.x_ratio:.3f}, {point.y_ratio:.3f})")
  338. continue
  339. # 使用视觉检测器
  340. detected, x_ratio, y_ratio = self.visual_detector.detect_position(
  341. panorama_frame=panorama_frame,
  342. frames_before=frames_before,
  343. frames_after=frames_after,
  344. ptz_frame=ptz_frame
  345. )
  346. if detected:
  347. point.x_ratio = x_ratio
  348. point.y_ratio = y_ratio
  349. point.detected = True
  350. valid_points.append(point)
  351. print(f" ✓ 视觉检测成功: ({point.x_ratio:.3f}, {point.y_ratio:.3f})")
  352. else:
  353. # 降级: 使用估算方法
  354. print(f" 视觉检测失败, 使用估算方法...")
  355. x_ratio = point.pan / 180.0
  356. y_ratio = 0.5 + point.tilt / 90.0
  357. x_ratio = max(0, min(1, x_ratio))
  358. y_ratio = max(0, min(1, y_ratio))
  359. point.x_ratio = x_ratio
  360. point.y_ratio = y_ratio
  361. point.detected = True
  362. valid_points.append(point)
  363. print(f" △ 使用估算: ({point.x_ratio:.3f}, {point.y_ratio:.3f})")
  364. # 检查校准点数量
  365. if len(valid_points) < 4:
  366. self.state = CalibrationState.FAILED
  367. self.result = CalibrationResult(
  368. success=False,
  369. points=valid_points,
  370. error_message=f"有效校准点不足 (需要至少4个, 实际{len(valid_points)}个)"
  371. )
  372. print(f"校准失败: {self.result.error_message}")
  373. if self.on_complete:
  374. self.on_complete(self.result)
  375. return self.result
  376. # 计算变换参数
  377. success = self._calculate_transform(valid_points)
  378. if success:
  379. self.state = CalibrationState.SUCCESS
  380. rms_error = self._calculate_rms_error(valid_points)
  381. self.result = CalibrationResult(
  382. success=True,
  383. points=valid_points,
  384. rms_error=rms_error
  385. )
  386. print(f"校准成功! RMS误差: {rms_error:.4f}")
  387. else:
  388. self.state = CalibrationState.FAILED
  389. self.result = CalibrationResult(
  390. success=False,
  391. points=valid_points,
  392. error_message="变换参数计算失败"
  393. )
  394. print(f"校准失败: {self.result.error_message}")
  395. if self.on_complete:
  396. self.on_complete(self.result)
  397. return self.result
  398. def _get_quick_calibration_points(self) -> List[CalibrationPoint]:
  399. """获取快速校准点 (5个点)"""
  400. return [
  401. CalibrationPoint(pan=0, tilt=0, zoom=1), # 左侧
  402. CalibrationPoint(pan=90, tilt=0, zoom=1), # 中心
  403. CalibrationPoint(pan=180, tilt=0, zoom=1), # 右侧
  404. CalibrationPoint(pan=90, tilt=-20, zoom=1), # 上方
  405. CalibrationPoint(pan=90, tilt=20, zoom=1), # 下方
  406. ]
  407. def _calculate_transform(self, points: List[CalibrationPoint]) -> bool:
  408. """
  409. 计算坐标变换参数
  410. Args:
  411. points: 有效校准点
  412. Returns:
  413. 是否成功
  414. """
  415. try:
  416. # 使用最小二乘法拟合变换关系
  417. # 简单线性模型: pan = a0 + a1*x + a2*y
  418. # tilt = b0 + b1*x + b2*y
  419. n = len(points)
  420. # 构建矩阵
  421. A = np.ones((n, 3))
  422. A[:, 1] = [p.x_ratio for p in points]
  423. A[:, 2] = [p.y_ratio for p in points]
  424. pan_values = np.array([p.pan for p in points])
  425. tilt_values = np.array([p.tilt for p in points])
  426. # 最小二乘求解
  427. pan_params, _, _, _ = np.linalg.lstsq(A, pan_values, rcond=None)
  428. tilt_params, _, _, _ = np.linalg.lstsq(A, tilt_values, rcond=None)
  429. # 存储变换参数
  430. self.pan_offset = pan_params[0]
  431. self.pan_scale_x = pan_params[1]
  432. self.pan_scale_y = pan_params[2]
  433. self.tilt_offset = tilt_params[0]
  434. self.tilt_scale_x = tilt_params[1]
  435. self.tilt_scale_y = tilt_params[2]
  436. print(f"变换参数:")
  437. print(f" pan = {self.pan_offset:.2f} + {self.pan_scale_x:.2f}*x + {self.pan_scale_y:.2f}*y")
  438. print(f" tilt = {self.tilt_offset:.2f} + {self.tilt_scale_x:.2f}*x + {self.tilt_scale_y:.2f}*y")
  439. return True
  440. except Exception as e:
  441. print(f"计算变换参数错误: {e}")
  442. return False
  443. def _calculate_rms_error(self, points: List[CalibrationPoint]) -> float:
  444. """计算均方根误差"""
  445. total_error = 0.0
  446. for p in points:
  447. pred_pan, pred_tilt = self.transform(p.x_ratio, p.y_ratio)
  448. error = math.sqrt((pred_pan - p.pan)**2 + (pred_tilt - p.tilt)**2)
  449. total_error += error**2
  450. return math.sqrt(total_error / len(points))
  451. def transform(self, x_ratio: float, y_ratio: float) -> Tuple[float, float]:
  452. """
  453. 将全景坐标转换为PTZ角度
  454. Args:
  455. x_ratio: X方向比例 (0-1)
  456. y_ratio: Y方向比例 (0-1)
  457. Returns:
  458. (pan, tilt) 角度
  459. """
  460. pan = self.pan_offset + self.pan_scale_x * x_ratio + self.pan_scale_y * y_ratio
  461. tilt = self.tilt_offset + self.tilt_scale_x * x_ratio + self.tilt_scale_y * y_ratio
  462. return (pan, tilt)
  463. def inverse_transform(self, pan: float, tilt: float) -> Tuple[float, float]:
  464. """
  465. 将PTZ角度转换为全景坐标 (近似)
  466. Args:
  467. pan: 水平角度
  468. tilt: 垂直角度
  469. Returns:
  470. (x_ratio, y_ratio)
  471. """
  472. # 简化逆变换
  473. x_ratio = (pan - self.pan_offset) / self.pan_scale_x if self.pan_scale_x != 0 else 0.5
  474. y_ratio = (tilt - self.tilt_offset) / self.tilt_scale_y if self.tilt_scale_y != 0 else 0.5
  475. return (max(0, min(1, x_ratio)), max(0, min(1, y_ratio)))
  476. def is_calibrated(self) -> bool:
  477. """是否已完成校准"""
  478. return self.state == CalibrationState.SUCCESS
  479. def get_state(self) -> CalibrationState:
  480. """获取当前状态"""
  481. return self.state
  482. def get_result(self) -> Optional[CalibrationResult]:
  483. """获取校准结果"""
  484. return self.result
  485. def save_calibration(self, filepath: str) -> bool:
  486. """
  487. 保存校准结果
  488. Args:
  489. filepath: 文件路径
  490. Returns:
  491. 是否成功
  492. """
  493. if not self.is_calibrated():
  494. return False
  495. try:
  496. import json
  497. data = {
  498. 'pan_offset': self.pan_offset,
  499. 'pan_scale_x': self.pan_scale_x,
  500. 'pan_scale_y': self.pan_scale_y,
  501. 'tilt_offset': self.tilt_offset,
  502. 'tilt_scale_x': self.tilt_scale_x,
  503. 'tilt_scale_y': self.tilt_scale_y,
  504. 'rms_error': self.result.rms_error if self.result else 0,
  505. }
  506. with open(filepath, 'w') as f:
  507. json.dump(data, f, indent=2)
  508. print(f"校准结果已保存: {filepath}")
  509. return True
  510. except Exception as e:
  511. print(f"保存校准结果失败: {e}")
  512. return False
  513. def load_calibration(self, filepath: str) -> bool:
  514. """
  515. 加载校准结果
  516. Args:
  517. filepath: 文件路径
  518. Returns:
  519. 是否成功
  520. """
  521. try:
  522. import json
  523. with open(filepath, 'r') as f:
  524. data = json.load(f)
  525. self.pan_offset = data['pan_offset']
  526. self.pan_scale_x = data['pan_scale_x']
  527. self.pan_scale_y = data['pan_scale_y']
  528. self.tilt_offset = data['tilt_offset']
  529. self.tilt_scale_x = data['tilt_scale_x']
  530. self.tilt_scale_y = data['tilt_scale_y']
  531. self.state = CalibrationState.SUCCESS
  532. self.result = CalibrationResult(
  533. success=True,
  534. points=[],
  535. rms_error=data.get('rms_error', 0)
  536. )
  537. print(f"校准结果已加载: {filepath}")
  538. return True
  539. except FileNotFoundError:
  540. print(f"校准文件不存在: {filepath}")
  541. return False
  542. except Exception as e:
  543. print(f"加载校准结果失败: {e}")
  544. return False
  545. class CalibrationManager:
  546. """
  547. 校准管理器
  548. 管理校准流程和状态
  549. """
  550. def __init__(self, calibrator: CameraCalibrator):
  551. """
  552. 初始化管理器
  553. Args:
  554. calibrator: 校准器实例
  555. """
  556. self.calibrator = calibrator
  557. self.calibration_file = "calibration.json"
  558. def auto_calibrate(self, force: bool = False) -> CalibrationResult:
  559. """
  560. 自动校准
  561. Args:
  562. force: 是否强制重新校准
  563. Returns:
  564. 校准结果
  565. """
  566. # 尝试加载已有校准结果
  567. if not force:
  568. if self.calibrator.load_calibration(self.calibration_file):
  569. print("使用已有校准结果")
  570. return self.calibrator.get_result()
  571. # 执行校准
  572. print("开始自动校准...")
  573. result = self.calibrator.calibrate(quick_mode=True)
  574. if result.success:
  575. # 保存校准结果
  576. self.calibrator.save_calibration(self.calibration_file)
  577. return result
  578. def check_calibration(self) -> Tuple[bool, str]:
  579. """
  580. 检查校准状态
  581. Returns:
  582. (是否有效, 状态信息)
  583. """
  584. state = self.calibrator.get_state()
  585. if state == CalibrationState.SUCCESS:
  586. result = self.calibrator.get_result()
  587. return (True, f"校准有效, RMS误差: {result.rms_error:.4f}")
  588. elif state == CalibrationState.FAILED:
  589. return (False, "校准失败")
  590. elif state == CalibrationState.RUNNING:
  591. return (False, "校准进行中")
  592. else:
  593. return (False, "未校准")