|
|
@@ -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
|