|
1 | 1 | # pyre-strict |
2 | | -from typing import overload, Sequence, Tuple, Union |
| 2 | +from typing import List, overload, Sequence, Tuple, Union |
3 | 3 |
|
4 | 4 | import numpy as np |
| 5 | +import pyproj |
5 | 6 | from numpy.typing import NDArray |
6 | 7 |
|
7 | 8 | Scalars = Union[float, NDArray] |
@@ -250,26 +251,6 @@ def gps_distance( |
250 | 251 | def gps_distance(latlon_1: NDArray, latlon_2: NDArray) -> NDArray: ... |
251 | 252 |
|
252 | 253 |
|
253 | | -def gps_distance( |
254 | | - latlon_1: Union[Sequence[Scalars], NDArray], |
255 | | - latlon_2: Union[Sequence[Scalars], NDArray], |
256 | | -) -> Scalars: |
257 | | - """ |
258 | | - Distance between two (lat,lon) pairs. |
259 | | -
|
260 | | - >>> p1 = (42.1, -11.1) |
261 | | - >>> p2 = (42.2, -11.3) |
262 | | - >>> 19000 < gps_distance(p1, p2) < 20000 |
263 | | - True |
264 | | - """ |
265 | | - x1, y1, z1 = ecef_from_lla(latlon_1[0], latlon_1[1], 0.0) |
266 | | - x2, y2, z2 = ecef_from_lla(latlon_2[0], latlon_2[1], 0.0) |
267 | | - |
268 | | - dis = np.sqrt((x1 - x2) ** 2 + (y1 - y2) ** 2 + (z1 - z2) ** 2) |
269 | | - |
270 | | - return dis |
271 | | - |
272 | | - |
273 | 254 | class TopocentricConverter: |
274 | 255 | """Convert to and from a topocentric reference frame.""" |
275 | 256 |
|
@@ -311,3 +292,108 @@ def to_lla( |
311 | 292 |
|
312 | 293 | def __eq__(self, o: "TopocentricConverter") -> bool: |
313 | 294 | return np.allclose([self.lat, self.lon, self.alt], (o.lat, o.lon, o.alt)) |
| 295 | + |
| 296 | + |
| 297 | +def construct_proj_transformer(proj_str: str, inverse: bool = False) -> pyproj.Transformer: |
| 298 | + """ |
| 299 | + Construct a pyproj Transformer object, converting between the given projection antod WGS84 (EPSG:4326). |
| 300 | + If inverse is True, the transformation is from WGS84 to the given projection. |
| 301 | + """ |
| 302 | + crs_4326 = pyproj.CRS.from_epsg(4326) |
| 303 | + if inverse: |
| 304 | + return pyproj.Transformer.from_proj(crs_4326, pyproj.CRS(proj_str)) |
| 305 | + else: |
| 306 | + return pyproj.Transformer.from_proj(pyproj.CRS(proj_str), crs_4326) |
| 307 | + |
| 308 | + |
| 309 | +def transform_to_proj( |
| 310 | + point: Sequence[float], reference: TopocentricConverter, projection: pyproj.Transformer |
| 311 | +) -> List[float]: |
| 312 | + """ |
| 313 | + Transform a point defined wrt. the local topocentric frame to a projection |
| 314 | + defined by the given Transformer. We assume the Transformer goes from |
| 315 | + WGS84 to the desired projection. |
| 316 | + """ |
| 317 | + assert projection.source_crs.to_epsg() == 4326, "Transformer source CRS must be WGS84 (EPSG:4326)" |
| 318 | + |
| 319 | + lat, lon, altitude = reference.to_lla(point[0], point[1], point[2]) |
| 320 | + easting, northing = projection.transform(lat, lon) |
| 321 | + return [easting, northing, altitude] |
| 322 | + |
| 323 | + |
| 324 | +def get_proj_transform_matrix( |
| 325 | + reference: TopocentricConverter, projection: pyproj.Transformer |
| 326 | +) -> NDArray: |
| 327 | + """Get the linear transform from reconstruction coords to geocoords.""" |
| 328 | + p = [[1, 0, 0], [0, 1, 0], [0, 0, 1], [0, 0, 0]] |
| 329 | + q = [transform_to_proj(point, reference, projection) for point in p] |
| 330 | + |
| 331 | + transformation = np.array( |
| 332 | + [ |
| 333 | + [q[0][0] - q[3][0], q[1][0] - q[3][0], q[2][0] - q[3][0], q[3][0]], |
| 334 | + [q[0][1] - q[3][1], q[1][1] - q[3][1], q[2][1] - q[3][1], q[3][1]], |
| 335 | + [q[0][2] - q[3][2], q[1][2] - q[3][2], q[2][2] - q[3][2], q[3][2]], |
| 336 | + [0, 0, 0, 1], |
| 337 | + ] |
| 338 | + ) |
| 339 | + return transformation |
| 340 | + |
| 341 | + |
| 342 | +def transform_reconstruction_with_matrix( |
| 343 | + reconstruction: "types.Reconstruction", transformation: NDArray |
| 344 | +) -> None: |
| 345 | + """Apply a rigid transformation to a reconstruction in-place.""" |
| 346 | + A, b = transformation[:3, :3], transformation[:3, 3] |
| 347 | + A1 = np.linalg.inv(A) |
| 348 | + |
| 349 | + for shot in reconstruction.shots.values(): |
| 350 | + R = shot.pose.get_rotation_matrix() |
| 351 | + shot.pose.set_rotation_matrix(np.dot(R, A1)) |
| 352 | + shot.pose.set_origin(np.dot(A, shot.pose.get_origin()) + b) |
| 353 | + |
| 354 | + for point in reconstruction.points.values(): |
| 355 | + point.coordinates = list(np.dot(A, point.coordinates) + b) |
| 356 | + |
| 357 | + |
| 358 | +def transform_reconstruction_with_proj( |
| 359 | + reconstruction: "types.Reconstruction", transformation: pyproj.Transformer |
| 360 | +) -> None: |
| 361 | + """Apply a proj Transformer to a reconstruction in-place.""" |
| 362 | + eps = 1e-3 |
| 363 | + for shot in reconstruction.shots.values(): |
| 364 | + origin = shot.pose.get_origin() |
| 365 | + |
| 366 | + # Jacobian for rotation update |
| 367 | + p0 = np.array(transform_to_proj(origin, reconstruction.reference, transformation)) |
| 368 | + px = np.array(transform_to_proj(origin + [eps, 0, 0], reconstruction.reference, transformation)) |
| 369 | + py = np.array(transform_to_proj(origin + [0, eps, 0], reconstruction.reference, transformation)) |
| 370 | + pz = np.array(transform_to_proj(origin + [0, 0, eps], reconstruction.reference, transformation)) |
| 371 | + J = np.column_stack(((px - p0) / eps, (py - p0) / eps, (pz - p0) / eps)) |
| 372 | + |
| 373 | + shot.pose.set_origin(p0) |
| 374 | + shot.pose.set_rotation_matrix(shot.pose.get_rotation_matrix() @ np.linalg.inv(J)) |
| 375 | + |
| 376 | + for point in reconstruction.points.values(): |
| 377 | + point.coordinates = transform_to_proj( |
| 378 | + point.coordinates, reconstruction.reference, transformation |
| 379 | + ) |
| 380 | + |
| 381 | + |
| 382 | +def gps_distance( |
| 383 | + latlon_1: Union[Sequence[Scalars], NDArray], |
| 384 | + latlon_2: Union[Sequence[Scalars], NDArray], |
| 385 | +) -> Scalars: |
| 386 | + """ |
| 387 | + Distance between two (lat,lon) pairs. |
| 388 | +
|
| 389 | + >>> p1 = (42.1, -11.1) |
| 390 | + >>> p2 = (42.2, -11.3) |
| 391 | + >>> 19000 < gps_distance(p1, p2) < 20000 |
| 392 | + True |
| 393 | + """ |
| 394 | + x1, y1, z1 = ecef_from_lla(latlon_1[0], latlon_1[1], 0.0) |
| 395 | + x2, y2, z2 = ecef_from_lla(latlon_2[0], latlon_2[1], 0.0) |
| 396 | + |
| 397 | + dis = np.sqrt((x1 - x2) ** 2 + (y1 - y2) ** 2 + (z1 - z2) ** 2) |
| 398 | + |
| 399 | + return dis |
0 commit comments