Просмотр исходного кода

fix: address mapping model edge cases and test accuracy

wenhongquan 1 неделя назад
Родитель
Сommit
ab97832d1f

+ 21 - 14
calibration_scan_180_360/mapping_model.py

@@ -27,10 +27,12 @@ class MappingModel:
         return diff
 
     @staticmethod
-    def _unwrap_pan_angles(pan_values: np.ndarray) -> np.ndarray:
+    def _unwrap_pan_angles(pan_values: np.ndarray,
+                           ref: Optional[float] = None) -> np.ndarray:
         if len(pan_values) == 0:
             return pan_values
-        ref = float(np.median(pan_values))
+        if ref is None:
+            ref = float(np.median(pan_values))
         unwrapped = pan_values.astype(float).copy()
         for i in range(len(unwrapped)):
             diff = unwrapped[i] - ref
@@ -43,7 +45,8 @@ class MappingModel:
         return unwrapped
 
     def fit(self, records: List[Dict]) -> bool:
-        valid = [r for r in records if 'x_ratio' in r and 'y_ratio' in r]
+        required = ('pan', 'tilt', 'x_ratio', 'y_ratio')
+        valid = [r for r in records if all(k in r for k in required)]
         if len(valid) < 4:
             return False
 
@@ -65,7 +68,8 @@ class MappingModel:
 
     def _ransac_filter(self, records: List[Dict],
                        iterations: int = 200,
-                       threshold: float = 15.0) -> np.ndarray:
+                       pan_threshold: float = 15.0,
+                       tilt_threshold: float = 15.0) -> np.ndarray:
         n = len(records)
         if n < 8:
             return np.ones(n, dtype=bool)
@@ -94,8 +98,7 @@ class MappingModel:
             pred_tilt = tilt_params[0] + tilt_params[1] * x + tilt_params[2] * y
             pan_err = np.array([self._angular_diff(float(pred_pan[i]), float(pan[i])) for i in range(n)])
             tilt_err = pred_tilt - tilt
-            errors = np.sqrt(pan_err ** 2 + tilt_err ** 2)
-            inliers = errors < threshold
+            inliers = (np.abs(pan_err) < pan_threshold) & (np.abs(tilt_err) < tilt_threshold)
             count = int(np.sum(inliers))
             if count > best_count:
                 best_count = count
@@ -150,7 +153,10 @@ class MappingModel:
         raw = []
         for x_key in sorted(x_buckets.keys()):
             pans = [p for p, _ in x_buckets[x_key]]
-            raw.append((x_key, float(np.median(pans)), len(pans)))
+            pans_arr = np.array(pans, dtype=float)
+            unwrapped = self._unwrap_pan_angles(pans_arr, ref=pans_arr[0])
+            median_pan = float(np.median(unwrapped)) % 360
+            raw.append((x_key, median_pan, len(pans)))
 
         filtered = self._filter_continuous_monotonic(raw)
         self.pan_lookup = [(x, pan) for x, pan, _ in filtered]
@@ -266,19 +272,15 @@ class MappingModel:
     def _calculate_rms_error(self, records: List[Dict]) -> float:
         total = 0.0
         for r in records:
-            pred_pan, pred_tilt = self.transform(
-                int(r['x_ratio'] * self.panorama_width),
-                int(r['y_ratio'] * self.panorama_height)
+            pred_pan, pred_tilt = self._predict_from_ratios(
+                float(r['x_ratio']), float(r['y_ratio'])
             )
             pan_err = self._angular_diff(pred_pan, r['pan'])
             tilt_err = pred_tilt - r['tilt']
             total += pan_err ** 2 + tilt_err ** 2
         return math.sqrt(total / len(records))
 
-    def transform(self, panorama_x: int, panorama_y: int) -> Tuple[float, float]:
-        x_ratio = panorama_x / self.panorama_width
-        y_ratio = panorama_y / self.panorama_height
-
+    def _predict_from_ratios(self, x_ratio: float, y_ratio: float) -> Tuple[float, float]:
         if self.pan_lookup:
             pan = self._interp_lookup(self.pan_lookup, x_ratio)
         else:
@@ -293,6 +295,11 @@ class MappingModel:
         tilt = max(-90, min(90, tilt))
         return pan, tilt
 
+    def transform(self, panorama_x: int, panorama_y: int) -> Tuple[float, float]:
+        x_ratio = panorama_x / self.panorama_width
+        y_ratio = panorama_y / self.panorama_height
+        return self._predict_from_ratios(x_ratio, y_ratio)
+
     def _interp_lookup(self, lookup: List[Tuple[float, float]], ratio: float) -> float:
         if not lookup:
             return 0.0

+ 2 - 2
calibration_scan_180_360/test_mapping_model.py

@@ -3,7 +3,7 @@ from mapping_model import MappingModel
 
 
 def test_mapping_model_fits_linear_pan_tilt():
-    # 构造 9 个校准点:x_ratio 与 pan 线性递减,y_ratio 与 tilt 线性递减
+    # 构造 15 个校准点:x_ratio 与 pan 线性递减,y_ratio 与 tilt 线性递减
     records = []
     for i, pan in enumerate([340, 300, 260, 220, 180]):
         for j, tilt in enumerate([45, 5, -35]):
@@ -22,7 +22,7 @@ def test_mapping_model_fits_linear_pan_tilt():
 
     pan, tilt = model.transform(1920, 540)  # 中心点
     # 根据 x_ratio=(360-pan)/180 的线性关系,中心 x=0.5 对应 pan≈270
-    assert 250 <= pan <= 290
+    assert 265 <= pan <= 275
     assert -10 <= tilt <= 15
 
     # 验证保存/加载格式