generate_calibration_group1.py 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  1. #!/usr/bin/env python3
  2. """
  3. 根据 PTZ 扫描的 pan/tilt 角度直接生成校准文件。
  4. 假设全景图水平方向与 PTZ pan 角度成线性对应:
  5. x_ratio = (360 - pan) / 180 (pan=180 -> x=1, pan=360 -> x=0)
  6. 竖直方向使用 config/camera.py 中的 tilt 曲线反推 y_ratio。
  7. """
  8. import json
  9. import math
  10. from pathlib import Path
  11. from typing import List, Tuple, Dict, Any
  12. import numpy as np
  13. def tilt_to_y(tilt: float, y0: float = 13.0, y1: float = -5.0, power: float = 0.8) -> float:
  14. """根据 tilt 曲线反推 y_ratio"""
  15. denom = y0 - y1
  16. if abs(denom) < 1e-9:
  17. return 0.5
  18. if tilt >= y0:
  19. return 0.0
  20. if tilt <= y1:
  21. return 1.0
  22. return ((y0 - tilt) / denom) ** (1.0 / power)
  23. def main():
  24. base = Path('/Users/wenhongquan/Desktop/阿里云同步/项目/dnn/德胜河 AI/dsh/calibration_scan_180_360_z1')
  25. raw_path = base / 'mapping_raw.json'
  26. out_path = base / 'calibration_group1.json'
  27. with open(raw_path, 'r', encoding='utf-8') as f:
  28. raw = json.load(f)
  29. records = raw['records']
  30. pano_w = raw['panorama_size']['width']
  31. pano_h = raw['panorama_size']['height']
  32. points: List[Dict[str, float]] = []
  33. for r in records:
  34. pan = float(r['pan'])
  35. tilt = float(r['tilt'])
  36. # pan 180~360 线性映射到 x 1~0
  37. x_ratio = (360.0 - pan) / 180.0
  38. x_ratio = float(np.clip(x_ratio, 0.0, 1.0))
  39. # 根据配置 tilt 曲线反推 y
  40. y_ratio = tilt_to_y(tilt)
  41. y_ratio = float(np.clip(y_ratio, 0.0, 1.0))
  42. points.append({
  43. 'pan': pan,
  44. 'tilt': tilt,
  45. 'x_ratio': round(x_ratio, 4),
  46. 'y_ratio': round(y_ratio, 4),
  47. 'panorama_x': int(round(x_ratio * pano_w)),
  48. 'panorama_y': int(round(y_ratio * pano_h)),
  49. })
  50. # 拟合 pan = offset + scale_x * x + scale_y * y
  51. X = np.array([[1.0, p['x_ratio'], p['y_ratio']] for p in points])
  52. pans = np.array([p['pan'] for p in points])
  53. tilts = np.array([p['tilt'] for p in points])
  54. pan_params, _, _, _ = np.linalg.lstsq(X, pans, rcond=None)
  55. tilt_params, _, _, _ = np.linalg.lstsq(X, tilts, rcond=None)
  56. # 构建 pan_lookup:按 x 排序,同一 x 取平均 pan
  57. x_to_pans: Dict[float, List[float]] = {}
  58. for p in points:
  59. x_to_pans.setdefault(p['x_ratio'], []).append(p['pan'])
  60. pan_lookup = sorted(
  61. [[x, float(np.mean(ps))] for x, ps in x_to_pans.items()],
  62. key=lambda item: item[0]
  63. )
  64. # 构建 tilt_lookup:按 y 分桶取中位数
  65. grid = 0.05
  66. y_to_tilts: Dict[float, List[float]] = {}
  67. for p in points:
  68. key = round(p['y_ratio'] / grid) * grid
  69. y_to_tilts.setdefault(key, []).append(p['tilt'])
  70. tilt_lookup = sorted(
  71. [[y, float(np.median(ts))] for y, ts in y_to_tilts.items()],
  72. key=lambda item: item[0]
  73. )
  74. # 重叠区间:直接使用扫描范围
  75. overlap_ranges = [{
  76. 'pan_start': 180.0,
  77. 'pan_end': 360.0,
  78. 'tilt_start': -35.0,
  79. 'tilt_end': 45.0,
  80. 'match_count': len(points),
  81. }]
  82. calibration = {
  83. 'pan_offset': float(pan_params[0]),
  84. 'pan_scale_x': float(pan_params[1]),
  85. 'pan_scale_y': float(pan_params[2]),
  86. 'tilt_offset': float(tilt_params[0]),
  87. 'tilt_scale_x': float(tilt_params[1]),
  88. 'tilt_scale_y': float(tilt_params[2]),
  89. 'rms_error': 0.0,
  90. 'overlap_ranges': overlap_ranges,
  91. 'pan_lookup': pan_lookup,
  92. 'tilt_lookup': tilt_lookup,
  93. 'mount_type': 'wall',
  94. 'tilt_flip': False,
  95. 'pan_flip': False,
  96. 'generated_from': 'ptz_pan_angle_linear_mapping',
  97. 'note': 'x_ratio=(360-pan)/180, y_ratio 由 config tilt 曲线反推',
  98. }
  99. with open(out_path, 'w', encoding='utf-8') as f:
  100. json.dump(calibration, f, indent=2, ensure_ascii=False)
  101. print(f'Generated {out_path}')
  102. print(f' points: {len(points)}')
  103. print(f' pan fit: {pan_params[0]:.2f} + {pan_params[1]:.2f}*x + {pan_params[2]:.2f}*y')
  104. print(f' tilt fit: {tilt_params[0]:.2f} + {tilt_params[1]:.2f}*x + {tilt_params[2]:.2f}*y')
  105. print(f' pan_lookup entries: {len(pan_lookup)}')
  106. print(f' tilt_lookup entries: {len(tilt_lookup)}')
  107. # 保存 human readable CSV
  108. csv_path = base / 'calibration_group1_points.csv'
  109. with open(csv_path, 'w', encoding='utf-8') as f:
  110. f.write('pan,tilt,x_ratio,y_ratio,panorama_x,panorama_y\n')
  111. for p in points:
  112. f.write(f"{p['pan']},{p['tilt']},{p['x_ratio']},{p['y_ratio']},{p['panorama_x']},{p['panorama_y']}\n")
  113. print(f' CSV: {csv_path}')
  114. if __name__ == '__main__':
  115. main()