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
35 changes: 14 additions & 21 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,43 +21,39 @@ jobs:
- name: Checkout
uses: actions/checkout@v6

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v6
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
python-version: ${{ matrix.python-version }}

- name: Install package + dev extras
run: |
python -m pip install --upgrade pip
# [dependency-groups] is uv syntax; pip doesn't resolve it, so install tools explicitly
python -m pip install -e . pytest ruff
run: uv sync --group dev

- name: Lint (ruff)
run: ruff check .
run: uv run ruff check .

# Optional but recommended: verifies console scripts + dispatch without launching GUI.
# NOTE: calling plain `cosmo` (no args) attempts to start the GUI by default.
- name: CLI sanity check (no GUI)
run: |
cosmo --version
cosmo convert --help
cosmo calibrate --help
uv run cosmo --version
uv run cosmo convert --help
uv run cosmo calibrate --help

# Run smoke tests first for fast feedback, then run the rest (skip smoke).
- name: Run smoke tests (CLI / entrypoints)
env:
MPLBACKEND: Agg
run: pytest -q -m smoke
run: uv run pytest -q -m smoke

- name: Run full test suite (skip smoke)
env:
MPLBACKEND: Agg
run: pytest -q -m "not smoke"
run: uv run pytest -q -m "not smoke"

- name: Build wheel/sdist
run: |
python -m pip install build
python -m build
uv build
ls -la dist

integration:
Expand All @@ -66,16 +62,13 @@ jobs:
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
python-version: "3.12"
- name: Install package + dev extras
run: |
python -m pip install --upgrade pip
python -m pip install -e . pytest ruff
# ensure optional deps are available
python -m pip install betterosi mcap
run: uv sync --group dev --group mcap
- name: Run integration tests
env:
MPLBACKEND: Agg
run: pytest -q -m integration
run: uv run pytest -q -m integration
10 changes: 8 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ build-backend = "hatchling.build"

[project]
name = "cosmo"
version = "0.4.1"
version = "0.5.0"
description = "COSMO conversion tools (OpenLABEL -> OSI/MCAP + Omega-Prime CSV) with GUI"
readme = "README.md"
requires-python = ">=3.10"
dependencies = ["numpy", "pandas", "pillow", "matplotlib", "dataprov", "reverse_geocoder", "pycountry"]
dependencies = ["numpy", "pandas", "pillow", "matplotlib", "dataprov", "reverse_geocoder", "pycountry", "orbit-georef", "opendrive-map"]


[dependency-groups]
Expand All @@ -26,6 +26,12 @@ cosmo-cli = [{include-group = "mcap"}, {include-group = "plot"}]
cosmo-gui = [{include-group = "gui"}, {include-group = "mcap"}, {include-group = "plot"}]
trajectory-explorer = [{include-group = "gui"}, {include-group = "mcap"}, {include-group = "ontology"}]

# orbit-georef and opendrive-map are standalone packages living as subdirectories
# of the ORBIT repo, pinned here to per-lib git tags. Bump the tag to upgrade.
[tool.uv.sources]
orbit-georef = { git = "https://github.com/RI-SE/ORBIT.git", subdirectory = "orbit-georef", tag = "orbit-georef-v0.1.2" }
opendrive-map = { git = "https://github.com/RI-SE/ORBIT.git", subdirectory = "opendrive-map", tag = "opendrive-map-v0.1.0" }

[project.scripts]
cosmo = "cosmo.cli.main:main"
cosmo-gui = "cosmo.cli.gui:main"
Expand Down
5 changes: 5 additions & 0 deletions src/cosmo/app/convert_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ class ConvertConfig:
# ISO 3166-1 numeric country code; auto-derived from georef lat/lon if None
country_code: Optional[int] = None

# object output frame: "global" (absolute proj_string CRS) or "local"
# (map-local; offset recorded in OSI proj_frame_offset)
object_frame: str = "global"

# output control
out_dir: Optional[str] = None # If None => <project>/runs/<timestamp>_convert_<stem>/
run_name: Optional[str] = None # Optional override for the run folder name
Expand Down Expand Up @@ -277,6 +281,7 @@ def run_convert(cfg: ConvertConfig, log_fn: Optional[LogFn] = None) -> ConvertRe
corrector=corrector,
stabilize_size=cfg.stabilize_size,
country_code=cfg.country_code,
object_frame=cfg.object_frame,
)

# determine produced outputs
Expand Down
6 changes: 6 additions & 0 deletions src/cosmo/cli/convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,11 @@ def build_parser() -> argparse.ArgumentParser:
help="ISO 3166-1 numeric country code (e.g. 752=Sweden); "
"auto-derived from georef lat/lon if omitted.")

ap.add_argument("--object-frame", choices=["global", "local"], default="global",
help="Object coordinate frame: 'global' = absolute proj_string CRS "
"(default); 'local' = map-local, shifted by the OpenDRIVE header "
"<offset> and recorded in OSI proj_frame_offset (requires --odr).")

# Run folder naming
ap.add_argument("--run-name", required=False, help="Optional override for run folder name")

Expand Down Expand Up @@ -279,6 +284,7 @@ def main(argv=None) -> int:
output_prefix=getattr(args, "output_prefix", None),
stabilize_size=args.stabilize_size,
country_code=args.country_code,
object_frame=args.object_frame,
)

def _log(line: str) -> None:
Expand Down
47 changes: 46 additions & 1 deletion src/cosmo/converters/openlabel_to_omega.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,18 @@
except ImportError: # pragma: no cover
betterosi = None

# orbit-georef owns the ORBIT georef JSON format + pixel→ground transform.
try:
from orbit_georef import GeoTransformer
except ImportError: # pragma: no cover
GeoTransformer = None

# opendrive-map owns the authoritative OpenDRIVE header <offset> (map frame origin).
try:
from opendrive_map import read_offset as _read_map_offset
except ImportError: # pragma: no cover
_read_map_offset = None

from cosmo.converters.ontology_mapper import OntologyMapper

# -----------------------------------------------------------------------------
Expand Down Expand Up @@ -308,7 +320,14 @@ def load_alignment(
f"Unsupported ORBIT georef transform_method='{georef.get('transform_method')}'. "
"Expected homography-style pixel->ground transform."
)
if "transformation_matrix" in georef:
# orbit-georef owns the ORBIT georef format; use it for full exports and
# fall back to inline parsing for partial/legacy calibration-style files.
is_full_orbit_georef = all(
k in georef for k in ("transformation_matrix", "inverse_matrix", "reference_point")
)
if GeoTransformer is not None and georef_data_path and is_full_orbit_georef:
H = GeoTransformer.from_file(georef_data_path).transform_matrix
elif "transformation_matrix" in georef:
H = np.array(georef["transformation_matrix"], dtype=np.float64)
elif "homography" in georef:
H = np.array(georef["homography"], dtype=np.float64)
Expand Down Expand Up @@ -556,6 +575,7 @@ def convert_openlabel_to_omega(
corrector=None,
stabilize_size: bool = False,
country_code: Optional[int] = None,
object_frame: str = "global",
):
"""
Convert OpenLABEL -> Omega-Prime CSV and optionally OSI GroundTruth MCAP.
Expand Down Expand Up @@ -598,6 +618,20 @@ def _log(msg: str) -> None:
if write_mcap and betterosi is None:
_log('[COSMO] MCAP requested but betterosi is not installed; will write CSV only.')

# Object output frame. "global": absolute coords in the proj_string CRS (default).
# "local": coords shifted by the map's header <offset> so objects and the embedded
# OpenDRIVE share one frame; the offset is recorded in OSI proj_frame_offset.
frame_off = (0.0, 0.0, 0.0)
if object_frame == "local":
if not (odr_path and _read_map_offset and os.path.isfile(odr_path)):
raise ValueError(
"object_frame='local' requires --odr with a header <offset> "
"(and opendrive-map installed)."
)
frame_off = _read_map_offset(odr_path)
_log(f"[COSMO] object_frame=local; subtracting map offset {frame_off}")
elif object_frame != "global":
raise ValueError(f"object_frame must be 'global' or 'local', got {object_frame!r}")

vt_name_to_code, vr_name_to_code, vt_default, vr_default = build_enum_code_maps()

Expand Down Expand Up @@ -752,6 +786,13 @@ def _write_ground_truth(writer_mcap, t_sec: float, total_nanos: int, moving_obje
gt.proj_string = _gt_proj_string
if _gt_country_code:
gt.country_code = _gt_country_code
if object_frame == "local" and any(frame_off):
# OSI: world = proj_frame_offset + osi, then proj_string. Records the
# local→projected-CRS shift so local coords stay fully georeferenced.
gt.proj_frame_offset = betterosi.GroundTruthProjFrameOffset(
position=betterosi.Vector3D(x=frame_off[0], y=frame_off[1], z=frame_off[2]),
yaw=0.0,
)
# Provide log_time explicitly; readers often build indices from it. [4](https://ika-rwth-aachen.github.io/omega-prime/notebooks/tutorial/)
writer_mcap.add(gt, topic="ground_truth", log_time=total_nanos)

Expand Down Expand Up @@ -809,6 +850,8 @@ def _write_ground_truth(writer_mcap, t_sec: float, total_nanos: int, moving_obje
else:
length, width, height = estimate_dims(label_type, w_px, h_px, oid)

X, Y, Z = X - frame_off[0], Y - frame_off[1], Z - frame_off[2]

if stabilize_size:
length, width, height = size_avg.get(oid, (length, width, height))

Expand Down Expand Up @@ -911,6 +954,8 @@ def _write_ground_truth(writer_mcap, t_sec: float, total_nanos: int, moving_obje
else:
length, width, height = estimate_dims(label_type, w_px, h_px, oid)

X, Y, Z = X - frame_off[0], Y - frame_off[1], Z - frame_off[2]

if stabilize_size:
length, width, height = size_avg.get(oid, (length, width, height))

Expand Down
Loading
Loading