trimble_geodesy/modules/georeferencing.py

370 lines
13 KiB
Python

"""
Georeferenzierungs-Modul
Transformation mit Passpunkten (mind. 3 Punkte)
NUR Rotation und Translation - KEINE Maßstabsänderung
"""
import math
import numpy as np
from typing import List, Tuple, Optional, Dict
from dataclasses import dataclass
from .cor_generator import CORPoint
@dataclass
class ControlPoint:
"""Passpunkt mit Soll- und Ist-Koordinaten"""
name: str
# Ist-Koordinaten (lokales System)
local_x: float
local_y: float
local_z: float
# Soll-Koordinaten (Zielsystem, z.B. UTM)
target_x: float
target_y: float
target_z: float
# Residuen nach Transformation
residual_x: float = 0.0
residual_y: float = 0.0
residual_z: float = 0.0
@property
def residual_2d(self) -> float:
"""2D-Residuum"""
return math.sqrt(self.residual_x**2 + self.residual_y**2)
@property
def residual_3d(self) -> float:
"""3D-Residuum"""
return math.sqrt(self.residual_x**2 + self.residual_y**2 + self.residual_z**2)
@dataclass
class GeoreferenceResult:
"""Ergebnis der Georeferenzierung"""
# Transformationsparameter
translation_x: float = 0.0
translation_y: float = 0.0
translation_z: float = 0.0
rotation_rad: float = 0.0
# Qualitätsparameter
rmse_x: float = 0.0
rmse_y: float = 0.0
rmse_z: float = 0.0
rmse_2d: float = 0.0
rmse_3d: float = 0.0
max_residual_2d: float = 0.0
max_residual_3d: float = 0.0
# Passpunkte mit Residuen
control_points: List[ControlPoint] = None
def __post_init__(self):
if self.control_points is None:
self.control_points = []
@property
def rotation_gon(self) -> float:
"""Rotation in Gon"""
return self.rotation_rad * 200.0 / math.pi
@property
def rotation_deg(self) -> float:
"""Rotation in Grad"""
return math.degrees(self.rotation_rad)
class Georeferencer:
"""
Georeferenzierung mit mindestens 3 Passpunkten
Verwendet Methode der kleinsten Quadrate für Überbestimmung
NUR Rotation und Translation (4 Parameter)
"""
def __init__(self):
self.control_points: List[ControlPoint] = []
self.result: Optional[GeoreferenceResult] = None
self.points_to_transform: List[CORPoint] = []
self.transformed_points: List[CORPoint] = []
def add_control_point(self, name: str,
local_x: float, local_y: float, local_z: float,
target_x: float, target_y: float, target_z: float):
"""Fügt einen Passpunkt hinzu"""
self.control_points.append(ControlPoint(
name=name,
local_x=local_x, local_y=local_y, local_z=local_z,
target_x=target_x, target_y=target_y, target_z=target_z
))
def clear_control_points(self):
"""Löscht alle Passpunkte"""
self.control_points = []
self.result = None
def set_points_to_transform(self, points: List[CORPoint]):
"""Setzt die zu transformierenden Punkte"""
self.points_to_transform = points.copy()
def compute_transformation(self) -> GeoreferenceResult:
"""
Berechnet die Transformationsparameter
4-Parameter-Transformation: Rotation + Translation (kein Maßstab)
Methode der kleinsten Quadrate
"""
if len(self.control_points) < 3:
raise ValueError("Mindestens 3 Passpunkte erforderlich!")
n = len(self.control_points)
# Koordinaten extrahieren
local_coords = np.array([[cp.local_x, cp.local_y] for cp in self.control_points])
target_coords = np.array([[cp.target_x, cp.target_y] for cp in self.control_points])
# Schwerpunkte berechnen
local_centroid = np.mean(local_coords, axis=0)
target_centroid = np.mean(target_coords, axis=0)
# Zum Schwerpunkt verschieben
local_centered = local_coords - local_centroid
target_centered = target_coords - target_centroid
# Rotation berechnen mit SVD (Procrustes ohne Skalierung)
H = local_centered.T @ target_centered
U, S, Vt = np.linalg.svd(H)
# Rotationsmatrix
R = Vt.T @ U.T
# Reflexion korrigieren falls nötig
if np.linalg.det(R) < 0:
Vt[-1, :] *= -1
R = Vt.T @ U.T
# Rotationswinkel aus Matrix
rotation_rad = math.atan2(R[1, 0], R[0, 0])
# Translation berechnen
translation = target_centroid - R @ local_centroid
# Ergebnis speichern
self.result = GeoreferenceResult(
translation_x=translation[0],
translation_y=translation[1],
rotation_rad=rotation_rad
)
# Z-Translation (Mittelwert der Höhendifferenzen)
z_diffs = [cp.target_z - cp.local_z for cp in self.control_points]
self.result.translation_z = np.mean(z_diffs)
# Residuen berechnen
self._compute_residuals()
return self.result
def _compute_residuals(self):
"""Berechnet Residuen für alle Passpunkte"""
if self.result is None:
return
cos_r = math.cos(self.result.rotation_rad)
sin_r = math.sin(self.result.rotation_rad)
sum_res_x2 = 0.0
sum_res_y2 = 0.0
sum_res_z2 = 0.0
max_res_2d = 0.0
max_res_3d = 0.0
for cp in self.control_points:
# Transformation anwenden
x_trans = cos_r * cp.local_x - sin_r * cp.local_y + self.result.translation_x
y_trans = sin_r * cp.local_x + cos_r * cp.local_y + self.result.translation_y
z_trans = cp.local_z + self.result.translation_z
# Residuen
cp.residual_x = cp.target_x - x_trans
cp.residual_y = cp.target_y - y_trans
cp.residual_z = cp.target_z - z_trans
sum_res_x2 += cp.residual_x**2
sum_res_y2 += cp.residual_y**2
sum_res_z2 += cp.residual_z**2
max_res_2d = max(max_res_2d, cp.residual_2d)
max_res_3d = max(max_res_3d, cp.residual_3d)
n = len(self.control_points)
self.result.rmse_x = math.sqrt(sum_res_x2 / n)
self.result.rmse_y = math.sqrt(sum_res_y2 / n)
self.result.rmse_z = math.sqrt(sum_res_z2 / n)
self.result.rmse_2d = math.sqrt((sum_res_x2 + sum_res_y2) / n)
self.result.rmse_3d = math.sqrt((sum_res_x2 + sum_res_y2 + sum_res_z2) / n)
self.result.max_residual_2d = max_res_2d
self.result.max_residual_3d = max_res_3d
self.result.control_points = self.control_points
def transform_points(self) -> List[CORPoint]:
"""Transformiert alle Punkte"""
if self.result is None:
raise ValueError("Transformation noch nicht berechnet!")
self.transformed_points = []
cos_r = math.cos(self.result.rotation_rad)
sin_r = math.sin(self.result.rotation_rad)
for p in self.points_to_transform:
x_trans = cos_r * p.x - sin_r * p.y + self.result.translation_x
y_trans = sin_r * p.x + cos_r * p.y + self.result.translation_y
z_trans = p.z + self.result.translation_z
self.transformed_points.append(CORPoint(
name=p.name,
x=x_trans,
y=y_trans,
z=z_trans
))
return self.transformed_points
def transform_single_point(self, x: float, y: float, z: float) -> Tuple[float, float, float]:
"""Transformiert einen einzelnen Punkt"""
if self.result is None:
raise ValueError("Transformation noch nicht berechnet!")
cos_r = math.cos(self.result.rotation_rad)
sin_r = math.sin(self.result.rotation_rad)
x_trans = cos_r * x - sin_r * y + self.result.translation_x
y_trans = sin_r * x + cos_r * y + self.result.translation_y
z_trans = z + self.result.translation_z
return x_trans, y_trans, z_trans
def get_transformation_report(self) -> str:
"""Erstellt einen detaillierten Transformationsbericht"""
if self.result is None:
return "Keine Transformation berechnet."
lines = []
lines.append("=" * 70)
lines.append("GEOREFERENZIERUNG - TRANSFORMATIONSBERICHT")
lines.append("=" * 70)
lines.append("")
lines.append("TRANSFORMATIONSPARAMETER (4-Parameter, ohne Maßstab)")
lines.append("-" * 70)
lines.append(f" Translation X: {self.result.translation_x:>15.4f} m")
lines.append(f" Translation Y: {self.result.translation_y:>15.4f} m")
lines.append(f" Translation Z: {self.result.translation_z:>15.4f} m")
lines.append(f" Rotation: {self.result.rotation_gon:>15.6f} gon")
lines.append(f" {self.result.rotation_deg:>15.6f}°")
lines.append(f" {self.result.rotation_rad:>15.8f} rad")
lines.append(f" Maßstab: {1.0:>15.6f} (fest)")
lines.append("")
lines.append("QUALITÄTSPARAMETER")
lines.append("-" * 70)
lines.append(f" RMSE X: {self.result.rmse_x*1000:>15.2f} mm")
lines.append(f" RMSE Y: {self.result.rmse_y*1000:>15.2f} mm")
lines.append(f" RMSE Z: {self.result.rmse_z*1000:>15.2f} mm")
lines.append(f" RMSE 2D: {self.result.rmse_2d*1000:>15.2f} mm")
lines.append(f" RMSE 3D: {self.result.rmse_3d*1000:>15.2f} mm")
lines.append(f" Max. Residuum 2D: {self.result.max_residual_2d*1000:>15.2f} mm")
lines.append(f" Max. Residuum 3D: {self.result.max_residual_3d*1000:>15.2f} mm")
lines.append("")
lines.append("PASSPUNKTE UND RESIDUEN")
lines.append("-" * 70)
lines.append(f"{'Punkt':<10} {'vX [mm]':>10} {'vY [mm]':>10} {'vZ [mm]':>10} {'v2D [mm]':>10} {'v3D [mm]':>10}")
lines.append("-" * 70)
for cp in self.result.control_points:
lines.append(f"{cp.name:<10} {cp.residual_x*1000:>10.2f} {cp.residual_y*1000:>10.2f} "
f"{cp.residual_z*1000:>10.2f} {cp.residual_2d*1000:>10.2f} {cp.residual_3d*1000:>10.2f}")
lines.append("-" * 70)
lines.append(f"Anzahl Passpunkte: {len(self.control_points)}")
lines.append(f"Redundanz (2D): {2 * len(self.control_points) - 4}")
lines.append("")
lines.append("=" * 70)
return "\n".join(lines)
def get_control_points_comparison(self) -> str:
"""Erstellt eine Vergleichstabelle für Passpunkte"""
if self.result is None:
return "Keine Transformation berechnet."
lines = []
lines.append("PASSPUNKT-KOORDINATEN: LOKAL → ZIEL")
lines.append("=" * 100)
lines.append(f"{'Punkt':<8} {'X_lokal':>12} {'Y_lokal':>12} {'Z_lokal':>10} | "
f"{'X_Ziel':>12} {'Y_Ziel':>12} {'Z_Ziel':>10}")
lines.append("-" * 100)
for cp in self.control_points:
lines.append(f"{cp.name:<8} {cp.local_x:>12.3f} {cp.local_y:>12.3f} {cp.local_z:>10.3f} | "
f"{cp.target_x:>12.3f} {cp.target_y:>12.3f} {cp.target_z:>10.3f}")
lines.append("=" * 100)
return "\n".join(lines)
def export_report(self, filepath: str):
"""Exportiert den vollständigen Bericht"""
report = self.get_transformation_report()
with open(filepath, 'w', encoding='utf-8') as f:
f.write(report)
class RigidBodyTransformation:
"""
Alternative Implementierung: Rigide Körpertransformation
6 Parameter: 3 Translationen + 3 Rotationen
Für 3D-Daten mit mehreren Rotationsachsen
"""
def __init__(self):
self.control_points: List[ControlPoint] = []
self.translation = np.zeros(3)
self.rotation_matrix = np.eye(3)
def compute(self, local_points: np.ndarray, target_points: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
"""
Berechnet rigide Transformation mit Kabsch-Algorithmus
Keine Maßstabsänderung
"""
# Schwerpunkte
centroid_local = np.mean(local_points, axis=0)
centroid_target = np.mean(target_points, axis=0)
# Zentrieren
local_centered = local_points - centroid_local
target_centered = target_points - centroid_target
# Kovarianzmatrix
H = local_centered.T @ target_centered
# SVD
U, S, Vt = np.linalg.svd(H)
# Rotation
R = Vt.T @ U.T
# Reflexionskorrektur
if np.linalg.det(R) < 0:
Vt[-1, :] *= -1
R = Vt.T @ U.T
# Translation
t = centroid_target - R @ centroid_local
self.rotation_matrix = R
self.translation = t
return R, t
def transform(self, points: np.ndarray) -> np.ndarray:
"""Transformiert Punkte"""
return (self.rotation_matrix @ points.T).T + self.translation