Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 110 additions & 0 deletions development.md

Large diffs are not rendered by default.

11 changes: 3 additions & 8 deletions orbit/export/object_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

from orbit.models import Road
from orbit.models.object import ObjectType, RoadObject
from orbit.utils.geo_sync import polyline_to_metric_points


class ObjectBuilder:
Expand Down Expand Up @@ -106,14 +107,8 @@ def _get_meter_centerline(self, road: Road) -> tuple:
if not centerline:
return [], 0.0

# Use geo coords directly if available (more precise)
if centerline.geo_points:
all_points_meters = [
self.transformer.latlon_to_meters(lat, lon)
for lon, lat in centerline.geo_points
]
else:
all_points_meters = self.transformer.pixels_to_meters_batch(centerline.points)
# Uses geo coords when consistent with pixels (more precise)
all_points_meters = polyline_to_metric_points(centerline, self.transformer)
geometry_elements = self.curve_fitter.fit_polyline(all_points_meters)
road_length = sum(elem.length for elem in geometry_elements)
return all_points_meters, road_length
Expand Down
103 changes: 8 additions & 95 deletions orbit/export/opendrive_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

from orbit.models import Junction, Project, Road
from orbit.utils import CoordinateTransformer
from orbit.utils.geo_sync import polyline_to_metric_points, refresh_stale_geo_points
from orbit.utils.logging_config import get_logger

from .curve_fitting import CurveFitter, GeometryElement, GeometryType
Expand Down Expand Up @@ -150,83 +151,7 @@ def __init__(
self.reference_warnings: List[str] = []

# Ensure geo_points are consistent with the current transformer
self._validate_and_refresh_geo_points()

# ------------------------------------------------------------------
# Geo-point consistency
# ------------------------------------------------------------------

def _validate_and_refresh_geo_points(self):
"""Refresh stale geo_points so the export uses accurate geo coords.

Checks each polyline's geo_points against the current transformer
by comparing ``geo_to_pixel(geo_point)`` with the stored pixel
position. Points that diverge beyond a threshold are recomputed
from pixels via ``pixel_to_geo``.

Also validates ``inline_geo_path`` on connecting roads.
"""
if self.transformer is None:
return

PIXEL_THRESHOLD = 2.0 # px — flags even ~0.2 m drift at typical resolution

refreshed_polylines = 0
refreshed_points = 0

for polyline in self.project.polylines:
if not polyline.geo_points or len(polyline.geo_points) != len(polyline.points):
continue

stale_indices = []
for i, (px, py) in enumerate(polyline.points):
lon, lat = polyline.geo_points[i]
try:
rpx, rpy = self.transformer.geo_to_pixel(lon, lat)
except Exception:
stale_indices.append(i)
continue
if abs(rpx - px) > PIXEL_THRESHOLD or abs(rpy - py) > PIXEL_THRESHOLD:
stale_indices.append(i)

if stale_indices:
refreshed_polylines += 1
refreshed_points += len(stale_indices)
for i in stale_indices:
px, py = polyline.points[i]
new_lon, new_lat = self.transformer.pixel_to_geo(px, py)
polyline.geo_points[i] = (new_lon, new_lat)

# Validate inline_geo_path on connecting roads
for road in self.project.roads:
if not road.inline_geo_path or not road.inline_path:
continue
if len(road.inline_geo_path) != len(road.inline_path):
continue

stale_indices = []
for i, (px, py) in enumerate(road.inline_path):
lon, lat = road.inline_geo_path[i]
try:
rpx, rpy = self.transformer.geo_to_pixel(lon, lat)
except Exception:
stale_indices.append(i)
continue
if abs(rpx - px) > PIXEL_THRESHOLD or abs(rpy - py) > PIXEL_THRESHOLD:
stale_indices.append(i)

if stale_indices:
refreshed_points += len(stale_indices)
for i in stale_indices:
px, py = road.inline_path[i]
new_lon, new_lat = self.transformer.pixel_to_geo(px, py)
road.inline_geo_path[i] = (new_lon, new_lat)

if refreshed_points > 0:
logger.info(
"Refreshed %d stale geo_points across %d polyline(s)",
refreshed_points, refreshed_polylines,
)
refresh_stale_geo_points(self.project, self.transformer)

def write(self, output_path: str) -> bool:
"""
Expand Down Expand Up @@ -652,14 +577,8 @@ def _calculate_bounds(self) -> dict:

# Collect all polyline points and convert to meters
for polyline in self.project.polylines:
# Use geo coords directly if available (more precise)
if polyline.geo_points:
for lon, lat in polyline.geo_points:
x_m, y_m = self.transformer.latlon_to_meters(lat, lon)
all_points_meters.append((x_m, y_m))
else:
points_meters = self.transformer.pixels_to_meters_batch(polyline.points)
all_points_meters.extend(points_meters)
all_points_meters.extend(
polyline_to_metric_points(polyline, self.transformer))

if not all_points_meters:
return {'north': 0.0, 'south': 0.0, 'east': 0.0, 'west': 0.0}
Expand Down Expand Up @@ -689,14 +608,8 @@ def _create_road(self, road: Road) -> Optional[etree.Element]:
centerline_points_pixel = centerline.points

# Transform centerline points to metric coordinates (meters)
# Use geo coords directly if available (more precise, avoids double conversion)
if centerline.geo_points:
all_points_meters = [
self.transformer.latlon_to_meters(lat, lon)
for lon, lat in centerline.geo_points
]
else:
all_points_meters = self.transformer.pixels_to_meters_batch(centerline_points_pixel)
# Uses geo coords when consistent with pixels (more precise)
all_points_meters = polyline_to_metric_points(centerline, self.transformer)

# Fit curves to metric coordinates
geometry_elements = self.curve_fitter.fit_polyline(all_points_meters)
Expand Down Expand Up @@ -1341,7 +1254,7 @@ def _get_road_heading_at_contact_meters(self, centerline_id, contact_point):

use_end = (contact_point == "end")

if polyline.geo_points:
if polyline.geo_points and len(polyline.geo_points) == len(polyline.points):
idx0 = -2 if use_end else 0
idx1 = -1 if use_end else 1
lon0, lat0 = polyline.geo_points[idx0]
Expand Down Expand Up @@ -1378,7 +1291,7 @@ def _get_road_endpoint_meters(

use_end = (contact_point == "end")

if polyline.geo_points:
if polyline.geo_points and len(polyline.geo_points) == len(polyline.points):
idx = -1 if use_end else 0
lon, lat = polyline.geo_points[idx]
return self.transformer.latlon_to_meters(lat, lon)
Expand Down
11 changes: 3 additions & 8 deletions orbit/export/parking_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

from orbit.models import Road
from orbit.models.parking import ParkingSpace
from orbit.utils.geo_sync import polyline_to_metric_points


def _project_point_onto_polyline(px: float, py: float, pts: List[tuple]):
Expand Down Expand Up @@ -137,14 +138,8 @@ def _get_road_metrics(self, road: Road):
if not centerline:
return 0.0, [], []

# Use geo coords directly if available (more precise)
if centerline.geo_points:
all_points_meters = [
self.transformer.latlon_to_meters(lat, lon)
for lon, lat in centerline.geo_points
]
else:
all_points_meters = self.transformer.pixels_to_meters_batch(centerline.points)
# Uses geo coords when consistent with pixels (more precise)
all_points_meters = polyline_to_metric_points(centerline, self.transformer)
geometry_elements = self.curve_fitter.fit_polyline(all_points_meters)
return sum(elem.length for elem in geometry_elements), all_points_meters, geometry_elements

Expand Down
20 changes: 18 additions & 2 deletions orbit/gui/main_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -902,6 +902,8 @@ def export_to_opendrive(self):
"""Export project to OpenDrive format."""
from .dialogs.export_dialog import ExportDialog

self._ensure_original_view_for_save()

if not self._prompt_and_handle_unapplied_adjustment():
return

Expand Down Expand Up @@ -935,6 +937,8 @@ def export_to_osm(self):

from orbit.export.osm_writer import export_to_osm

self._ensure_original_view_for_save()

if not self._prompt_and_handle_unapplied_adjustment():
return

Expand Down Expand Up @@ -1021,6 +1025,8 @@ def export_georeferencing(self):
"""Export georeferencing parameters to JSON file."""
from orbit.export import export_georeferencing

self._ensure_original_view_for_save()

if not self._check_provenance_ready():
return

Expand Down Expand Up @@ -1129,6 +1135,8 @@ def export_georeferencing(self):

def export_layout_mask(self):
"""Export lane segmentation mask and metadata JSON."""
self._ensure_original_view_for_save()

# Validate prerequisites
if not self.image_view.image_item:
show_warning(self, "Cannot export: No image loaded.", "No Image")
Expand Down Expand Up @@ -3114,12 +3122,20 @@ def _switch_to_aerial(self):
return

# Resize aerial image so its pixels/meter matches the original image.
# Geometric mean of x/y scales: oblique source imagery has
# anisotropic scale, so neither axis alone is representative.
import math as _math

import cv2 as _cv2
orig_scale_x, orig_scale_y = self._original_transformer.get_scale_factor()
aerial_scale_x, aerial_scale_y = aerial_transformer_raw.get_scale_factor()
aerial_image = result.image
if orig_scale_x > 0 and aerial_scale_x > 0:
resize_ratio = aerial_scale_x / orig_scale_x
orig_scale = (_math.sqrt(orig_scale_x * orig_scale_y)
if orig_scale_x > 0 and orig_scale_y > 0 else 0.0)
aerial_scale = (_math.sqrt(aerial_scale_x * aerial_scale_y)
if aerial_scale_x > 0 and aerial_scale_y > 0 else 0.0)
if orig_scale > 0 and aerial_scale > 0:
resize_ratio = aerial_scale / orig_scale
if abs(resize_ratio - 1.0) > 0.01:
new_w = max(1, round(w * resize_ratio))
new_h = max(1, round(h * resize_ratio))
Expand Down
28 changes: 21 additions & 7 deletions orbit/import/opendrive_coordinate_transform.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
from dataclasses import dataclass
from typing import List, Optional, Tuple

from orbit.utils.geo_constants import METERS_PER_DEGREE


@dataclass
class TransformMode:
Expand Down Expand Up @@ -237,6 +239,9 @@ def _metric_to_latlon(self, x_meters: float, y_meters: float) -> Tuple[float, fl
Applies header offset before conversion - OpenDRIVE coordinates are
relative to the offset specified in the header.

Assumes the offset is a rigid transform applied rotate-first:
absolute = R(hdg) * (x, y) + (offset_x, offset_y).

Args:
x_meters: X coordinate in meters (relative to header offset)
y_meters: Y coordinate in meters (relative to header offset)
Expand All @@ -245,6 +250,13 @@ def _metric_to_latlon(self, x_meters: float, y_meters: float) -> Tuple[float, fl
Tuple of (longitude, latitude) in decimal degrees
"""
# Apply header offset - coordinates in file are relative to this offset
if self.header_offset_hdg:
cos_h = math.cos(self.header_offset_hdg)
sin_h = math.sin(self.header_offset_hdg)
x_meters, y_meters = (
x_meters * cos_h - y_meters * sin_h,
x_meters * sin_h + y_meters * cos_h,
)
x_absolute = x_meters + self.header_offset_x
y_absolute = y_meters + self.header_offset_y

Expand All @@ -268,18 +280,19 @@ def _metric_to_latlon(self, x_meters: float, y_meters: float) -> Tuple[float, fl
origin_lat, origin_lon = self._extract_origin_from_proj4()

if origin_lat and origin_lon:
# Simple approximation: meters to degrees
# At mid-latitudes: ~111000 m per degree latitude, ~111000*cos(lat) m per degree longitude
meters_per_degree_lat = 111000.0
meters_per_degree_lon = 111000.0 * math.cos(math.radians(origin_lat))
# Simple equirectangular approximation: meters to degrees
meters_per_degree_lat = METERS_PER_DEGREE
meters_per_degree_lon = METERS_PER_DEGREE * math.cos(math.radians(origin_lat))

lat = origin_lat + (y_absolute / meters_per_degree_lat)
lon = origin_lon + (x_absolute / meters_per_degree_lon)

return (lon, lat)

# Last resort: return metric coords as if they were degrees (will be wrong)
return (x_absolute, y_absolute)
raise ValueError(
"Cannot convert OpenDRIVE coordinates to lat/lon: pyproj is not "
"installed and the geoReference string has no +lat_0/+lon_0 origin."
)

def _extract_origin_from_proj4(self) -> Tuple[Optional[float], Optional[float]]:
"""
Expand Down Expand Up @@ -319,7 +332,8 @@ def _generate_suggested_control_points(
Returns:
List of (pixel_x, pixel_y, lon, lat) tuples
"""
if not self.data_min_x or not self.data_max_x:
if (self.data_min_x is None or self.data_max_x is None
or self.data_min_y is None or self.data_max_y is None):
return []

# Create 4 control points at corners of data bounds
Expand Down
4 changes: 4 additions & 0 deletions orbit/import/opendrive_importer.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,10 @@ def _setup_coordinate_transform(self, options: ImportOptions, result: ImportResu
transform_result = self.coord_transform.setup_transform(sample_points)
result.transform_mode = transform_result.mode
result.scale_used = transform_result.scale_pixels_per_meter
# Persist the synthetic pixel scale so it survives save/load
if transform_result.scale_pixels_per_meter is not None:
self.project.import_scale_pixels_per_meter = (
transform_result.scale_pixels_per_meter)

if not transform_result.success:
if transform_result.mode == TransformMode.AUTO_GEOREFERENCE:
Expand Down
5 changes: 3 additions & 2 deletions orbit/import/osm_to_orbit.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from orbit.models.signal import SignalType
from orbit.utils import CoordinateTransformer
from orbit.utils.enum_formatting import format_enum_name
from orbit.utils.geo_constants import METERS_PER_DEGREE
from orbit.utils.geometry import calculate_path_length, find_point_at_distance_along_path, shorten_geo_points
from orbit.utils.logging_config import get_logger

Expand Down Expand Up @@ -92,8 +93,8 @@ def calculate_bbox_from_center(center_lat: float, center_lon: float,
Returns:
Tuple of (min_lat, min_lon, max_lat, max_lon)
"""
dlat = radius_m / 111_000
dlon = radius_m / (111_000 * math.cos(math.radians(center_lat)))
dlat = radius_m / METERS_PER_DEGREE
dlon = radius_m / (METERS_PER_DEGREE * math.cos(math.radians(center_lat)))
return (center_lat - dlat, center_lon - dlon,
center_lat + dlat, center_lon + dlon)

Expand Down
Loading
Loading