scar-calib 2.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. scar_calib-2.0.0/LICENSE +21 -0
  2. scar_calib-2.0.0/PKG-INFO +176 -0
  3. scar_calib-2.0.0/README.md +123 -0
  4. scar_calib-2.0.0/pyproject.toml +104 -0
  5. scar_calib-2.0.0/scar/__init__.py +10 -0
  6. scar_calib-2.0.0/scar/cli.py +234 -0
  7. scar_calib-2.0.0/scar/collect/__init__.py +22 -0
  8. scar_calib-2.0.0/scar/collect/collector.py +287 -0
  9. scar_calib-2.0.0/scar/collect/filters.py +84 -0
  10. scar_calib-2.0.0/scar/collect/matching.py +112 -0
  11. scar_calib-2.0.0/scar/collect/tracking.py +62 -0
  12. scar_calib-2.0.0/scar/config/__init__.py +5 -0
  13. scar_calib-2.0.0/scar/config/loader.py +113 -0
  14. scar_calib-2.0.0/scar/config/presets/cvprw2026.yaml +54 -0
  15. scar_calib-2.0.0/scar/config/presets/default.yaml +9 -0
  16. scar_calib-2.0.0/scar/config/presets/joint.yaml +20 -0
  17. scar_calib-2.0.0/scar/config/schema.py +159 -0
  18. scar_calib-2.0.0/scar/config/templates/csv.yaml +68 -0
  19. scar_calib-2.0.0/scar/config/templates/event.yaml +61 -0
  20. scar_calib-2.0.0/scar/config/templates/ltc.yaml +46 -0
  21. scar_calib-2.0.0/scar/core/__init__.py +23 -0
  22. scar_calib-2.0.0/scar/core/camera.py +104 -0
  23. scar_calib-2.0.0/scar/core/se3.py +119 -0
  24. scar_calib-2.0.0/scar/core/types.py +184 -0
  25. scar_calib-2.0.0/scar/datasets/__init__.py +1 -0
  26. scar_calib-2.0.0/scar/datasets/generic_csv.py +166 -0
  27. scar_calib-2.0.0/scar/datasets/ltc.py +108 -0
  28. scar_calib-2.0.0/scar/evaluate/__init__.py +7 -0
  29. scar_calib-2.0.0/scar/evaluate/localization.py +106 -0
  30. scar_calib-2.0.0/scar/evaluate/offsets.py +80 -0
  31. scar_calib-2.0.0/scar/evaluate/reprojection.py +53 -0
  32. scar_calib-2.0.0/scar/geodesy/__init__.py +24 -0
  33. scar_calib-2.0.0/scar/geodesy/convert.py +140 -0
  34. scar_calib-2.0.0/scar/geodesy/world_frame.py +127 -0
  35. scar_calib-2.0.0/scar/io/__init__.py +28 -0
  36. scar_calib-2.0.0/scar/io/calibration.py +166 -0
  37. scar_calib-2.0.0/scar/io/legacy.py +218 -0
  38. scar_calib-2.0.0/scar/io/results.py +60 -0
  39. scar_calib-2.0.0/scar/io/sequence.py +101 -0
  40. scar_calib-2.0.0/scar/optimize/__init__.py +32 -0
  41. scar_calib-2.0.0/scar/optimize/extrinsics.py +120 -0
  42. scar_calib-2.0.0/scar/optimize/graph.py +268 -0
  43. scar_calib-2.0.0/scar/optimize/problem.py +124 -0
  44. scar_calib-2.0.0/scar/optimize/runner.py +103 -0
  45. scar_calib-2.0.0/scar/optimize/stages.py +172 -0
  46. scar_calib-2.0.0/scar/pipeline.py +176 -0
  47. scar_calib-2.0.0/scar/references/__init__.py +6 -0
  48. scar_calib-2.0.0/scar/references/base.py +92 -0
  49. scar_calib-2.0.0/scar/references/georay.py +195 -0
  50. scar_calib-2.0.0/scar/sources/__init__.py +11 -0
  51. scar_calib-2.0.0/scar/sources/images.py +103 -0
  52. scar_calib-2.0.0/scar/sources/poses.py +194 -0
  53. scar_calib-2.0.0/scar/viz/__init__.py +1 -0
  54. scar_calib-2.0.0/scar/viz/rerun_viewer.py +92 -0
  55. scar_calib-2.0.0/scar_calib.egg-info/PKG-INFO +176 -0
  56. scar_calib-2.0.0/scar_calib.egg-info/SOURCES.txt +59 -0
  57. scar_calib-2.0.0/scar_calib.egg-info/dependency_links.txt +1 -0
  58. scar_calib-2.0.0/scar_calib.egg-info/entry_points.txt +2 -0
  59. scar_calib-2.0.0/scar_calib.egg-info/requires.txt +27 -0
  60. scar_calib-2.0.0/scar_calib.egg-info/top_level.txt +1 -0
  61. scar_calib-2.0.0/setup.cfg +4 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Henry Hölzemann, Michael Schleiss
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,176 @@
1
+ Metadata-Version: 2.4
2
+ Name: scar-calib
3
+ Version: 2.0.0
4
+ Summary: Satellite-based Calibration for Aerial Recordings
5
+ Author-email: Henry Hölzemann <henry.hoelzemann@fkie.fraunhofer.de>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 Henry Hölzemann, Michael Schleiss
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Requires-Python: >=3.11
29
+ Description-Content-Type: text/markdown
30
+ License-File: LICENSE
31
+ Requires-Dist: numpy>=1.24
32
+ Requires-Dist: scipy>=1.10
33
+ Requires-Dist: gtsam>=4.2
34
+ Requires-Dist: pyproj>=3.6
35
+ Requires-Dist: opencv-python>=4.8
36
+ Requires-Dist: pyyaml>=6.0
37
+ Provides-Extra: legacy
38
+ Requires-Dist: utm==0.8.1; extra == "legacy"
39
+ Provides-Extra: references
40
+ Requires-Dist: georay>=0.1; extra == "references"
41
+ Requires-Dist: rasterio>=1.3; extra == "references"
42
+ Provides-Extra: germany
43
+ Requires-Dist: georay[germany]>=0.1; extra == "germany"
44
+ Provides-Extra: matching
45
+ Requires-Dist: torch; extra == "matching"
46
+ Requires-Dist: vismatch>=1.3; extra == "matching"
47
+ Provides-Extra: viewer
48
+ Requires-Dist: rerun-sdk>=0.23; extra == "viewer"
49
+ Provides-Extra: dev
50
+ Requires-Dist: pytest>=7.0; extra == "dev"
51
+ Requires-Dist: ruff>=0.6; extra == "dev"
52
+ Dynamic: license-file
53
+
54
+ <table align="center"><tr><td align="center" width="9999">
55
+ <img src="assets/scar_icon.png" align="center" width="150" alt="SCAR icon">
56
+
57
+ # SCAR
58
+
59
+ **S**atellite-based **C**alibration for **A**erial **R**ecordings
60
+ </td></tr></table>
61
+
62
+ SCAR refines the intrinsics of an aerial camera and its extrinsics relative to a GNSS/INS from the
63
+ recording itself: it matches the images against georeferenced orthophotos, lifts the matches to 3D
64
+ anchors with a surface model and optimizes a factor graph (GTSAM) over cameras, anchors and the
65
+ calibration. No calibration target or ground control points are needed.
66
+
67
+ You provide images with timestamps, INS poses, an initial calibration and reference data:
68
+
69
+ - **Germany:** official orthophotos and DSMs of the federal states supported by
70
+ [georaffer](https://github.com/michaelschleiss/georaffer) (NRW, RLP, BW, BY, BB, ...) are downloaded on
71
+ demand.
72
+ - **Anywhere else:** your own orthophoto and surface model as GeoTIFF/COG/VRT in any CRS (e.g. NAIP +
73
+ 3DEP in the US, national mapping data, your own survey), or your own reference provider.
74
+
75
+ Frame cameras and event cameras (as reconstructed frames) are supported. The paper version is kept
76
+ frozen as the preset `cvprw2026` and the tag `v1.0.0-cvprw2026`.
77
+
78
+ ## Installation
79
+
80
+ SCAR needs Python 3.11+.
81
+
82
+ ```bash
83
+ git clone https://github.com/hlzmnhnry/scar.git && cd scar
84
+ pixi install # environment with GTSAM, pyproj, rasterio, georay; CPU only
85
+ pixi install -e gpu # + PyTorch/CUDA, learned matchers (vismatch), georaffer, rerun viewer
86
+ ```
87
+
88
+ With pip: `pip install -e ".[references,matching,viewer]"` (add `germany` for German state data).
89
+ [georay](https://github.com/hlzmnhnry/georay) renders the reference views; the `germany` extra adds
90
+ georaffer.
91
+
92
+ ## Quick start
93
+
94
+ A complete run on a synthetic flight with local GeoTIFF reference data, CPU only and without downloads:
95
+
96
+ ```bash
97
+ pixi run python examples/custom_region_geotiff/make_demo_data.py examples/custom_region_geotiff/demo
98
+ pixi run scar run examples/custom_region_geotiff/config.yaml
99
+ pixi run python examples/custom_region_geotiff/make_demo_data.py --compare examples/custom_region_geotiff/demo
100
+ ```
101
+
102
+ For your own flight:
103
+
104
+ ```bash
105
+ scar init my_flight.yaml --template csv # or: ltc, event
106
+ scar check my_flight.yaml --render # time coverage, CRS, reference availability, footprint overlays
107
+ scar run my_flight.yaml --viewer # collect anchors and optimize; live view in rerun
108
+ scar export runs/my_flight/calibration/calibration.yaml --format kalibr --output camchain.yaml
109
+ ```
110
+
111
+ `scar collect` and `scar optimize` run the two steps separately; `scar evaluate` scores a calibration on
112
+ anchors of an independent test sequence (reprojection error, optionally visual localization). See
113
+ [`examples/`](examples) for an LTC campaign; `scar init --template event` starts an event camera setup.
114
+
115
+ ## Inputs
116
+
117
+ All inputs are described in one YAML file; the templates in
118
+ [`scar/config/templates`](scar/config/templates) document every option.
119
+
120
+ | Input | Format |
121
+ | --- | --- |
122
+ | Images | CSV with timestamp and path, a directory of files named by timestamp, or LTC `camera.csv` |
123
+ | Poses | CSV with geodetic (lat, lon, height) or projected positions in any CRS; quaternion or Euler orientation w.r.t. NED or ENU; optional per-sample accuracies |
124
+ | Calibration | SCAR YAML, Kalibr camchain or LTC `camera.json`/`extrinsics.json`; pinhole with radial-tangential distortion |
125
+ | Reference data | georaffer regions (Germany) or local orthophoto and DSM rasters |
126
+
127
+ Poses are interpolated to the image timestamps (linear position, SLERP rotation), optionally with a
128
+ time offset. The conventions for frames, heights, datums and the meridian convergence are described in
129
+ [docs/coordinate_systems.md](docs/coordinate_systems.md); please read it before using your own data.
130
+
131
+ ## Outputs
132
+
133
+ A run directory contains the anchors (`anchors.json`), the per-stage results (`calibration/stages.json`),
134
+ the final calibration with the marginal covariance of the intrinsics (`calibration/calibration.yaml`)
135
+ and, with `--viewer`, a rerun recording. `scar export` writes Kalibr, OpenCV and LTC formats.
136
+
137
+ ## Optimization presets
138
+
139
+ | Preset | Description |
140
+ | --- | --- |
141
+ | `default` | Staged schedule of the paper (rotation, translation, landmarks, calibration, full) followed by a least-squares fit of the extrinsics; use with the corrected pose processing (default of all templates) |
142
+ | `cvprw2026` | Frozen paper configuration; together with `paper/cvprw2026` it reproduces the published results |
143
+ | `joint` | Experimental: extrinsics as a variable of the factor graph instead of the least-squares fit |
144
+
145
+ ## Reproducing the paper
146
+
147
+ [`paper/cvprw2026`](paper/cvprw2026) contains the scripts, campaign definitions and initial calibrations
148
+ of the CVPRW 2026 evaluation, the reproduction log and an ablation of geodetic corrections that were not
149
+ part of the paper version (float32 coordinates, meridian convergence, heights, interpolation).
150
+ The meridian convergence correction alone lowers the test reprojection error by 21 % on average over the
151
+ six LTC campaigns.
152
+
153
+ ## Development
154
+
155
+ ```bash
156
+ pixi run test # unit, synthetic and system tests; regression tests need local LTC data
157
+ pixi run -e repro test # bit-exact paper regression with GTSAM 4.2.0
158
+ ```
159
+
160
+ ## Citation
161
+
162
+ If you use SCAR in academic work, please cite:
163
+
164
+ ```bibtex
165
+ @InProceedings{Holzemann_2026_CVPR,
166
+ author = {H\"olzemann, Henry and Schleiss, Michael},
167
+ title = {SCAR: Satellite Imagery-Based Calibration for Aerial Recordings},
168
+ booktitle = {Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition (CVPR) Workshops},
169
+ month = {June},
170
+ year = {2026},
171
+ pages = {987-996}
172
+ }
173
+ ```
174
+
175
+ Reference data obtained through georaffer are subject to the licenses of the respective providers
176
+ (e.g. NRW: dl-de/zero-2.0, RLP: dl-de/by-2.0); SCAR does not redistribute them.
@@ -0,0 +1,123 @@
1
+ <table align="center"><tr><td align="center" width="9999">
2
+ <img src="assets/scar_icon.png" align="center" width="150" alt="SCAR icon">
3
+
4
+ # SCAR
5
+
6
+ **S**atellite-based **C**alibration for **A**erial **R**ecordings
7
+ </td></tr></table>
8
+
9
+ SCAR refines the intrinsics of an aerial camera and its extrinsics relative to a GNSS/INS from the
10
+ recording itself: it matches the images against georeferenced orthophotos, lifts the matches to 3D
11
+ anchors with a surface model and optimizes a factor graph (GTSAM) over cameras, anchors and the
12
+ calibration. No calibration target or ground control points are needed.
13
+
14
+ You provide images with timestamps, INS poses, an initial calibration and reference data:
15
+
16
+ - **Germany:** official orthophotos and DSMs of the federal states supported by
17
+ [georaffer](https://github.com/michaelschleiss/georaffer) (NRW, RLP, BW, BY, BB, ...) are downloaded on
18
+ demand.
19
+ - **Anywhere else:** your own orthophoto and surface model as GeoTIFF/COG/VRT in any CRS (e.g. NAIP +
20
+ 3DEP in the US, national mapping data, your own survey), or your own reference provider.
21
+
22
+ Frame cameras and event cameras (as reconstructed frames) are supported. The paper version is kept
23
+ frozen as the preset `cvprw2026` and the tag `v1.0.0-cvprw2026`.
24
+
25
+ ## Installation
26
+
27
+ SCAR needs Python 3.11+.
28
+
29
+ ```bash
30
+ git clone https://github.com/hlzmnhnry/scar.git && cd scar
31
+ pixi install # environment with GTSAM, pyproj, rasterio, georay; CPU only
32
+ pixi install -e gpu # + PyTorch/CUDA, learned matchers (vismatch), georaffer, rerun viewer
33
+ ```
34
+
35
+ With pip: `pip install -e ".[references,matching,viewer]"` (add `germany` for German state data).
36
+ [georay](https://github.com/hlzmnhnry/georay) renders the reference views; the `germany` extra adds
37
+ georaffer.
38
+
39
+ ## Quick start
40
+
41
+ A complete run on a synthetic flight with local GeoTIFF reference data, CPU only and without downloads:
42
+
43
+ ```bash
44
+ pixi run python examples/custom_region_geotiff/make_demo_data.py examples/custom_region_geotiff/demo
45
+ pixi run scar run examples/custom_region_geotiff/config.yaml
46
+ pixi run python examples/custom_region_geotiff/make_demo_data.py --compare examples/custom_region_geotiff/demo
47
+ ```
48
+
49
+ For your own flight:
50
+
51
+ ```bash
52
+ scar init my_flight.yaml --template csv # or: ltc, event
53
+ scar check my_flight.yaml --render # time coverage, CRS, reference availability, footprint overlays
54
+ scar run my_flight.yaml --viewer # collect anchors and optimize; live view in rerun
55
+ scar export runs/my_flight/calibration/calibration.yaml --format kalibr --output camchain.yaml
56
+ ```
57
+
58
+ `scar collect` and `scar optimize` run the two steps separately; `scar evaluate` scores a calibration on
59
+ anchors of an independent test sequence (reprojection error, optionally visual localization). See
60
+ [`examples/`](examples) for an LTC campaign; `scar init --template event` starts an event camera setup.
61
+
62
+ ## Inputs
63
+
64
+ All inputs are described in one YAML file; the templates in
65
+ [`scar/config/templates`](scar/config/templates) document every option.
66
+
67
+ | Input | Format |
68
+ | --- | --- |
69
+ | Images | CSV with timestamp and path, a directory of files named by timestamp, or LTC `camera.csv` |
70
+ | Poses | CSV with geodetic (lat, lon, height) or projected positions in any CRS; quaternion or Euler orientation w.r.t. NED or ENU; optional per-sample accuracies |
71
+ | Calibration | SCAR YAML, Kalibr camchain or LTC `camera.json`/`extrinsics.json`; pinhole with radial-tangential distortion |
72
+ | Reference data | georaffer regions (Germany) or local orthophoto and DSM rasters |
73
+
74
+ Poses are interpolated to the image timestamps (linear position, SLERP rotation), optionally with a
75
+ time offset. The conventions for frames, heights, datums and the meridian convergence are described in
76
+ [docs/coordinate_systems.md](docs/coordinate_systems.md); please read it before using your own data.
77
+
78
+ ## Outputs
79
+
80
+ A run directory contains the anchors (`anchors.json`), the per-stage results (`calibration/stages.json`),
81
+ the final calibration with the marginal covariance of the intrinsics (`calibration/calibration.yaml`)
82
+ and, with `--viewer`, a rerun recording. `scar export` writes Kalibr, OpenCV and LTC formats.
83
+
84
+ ## Optimization presets
85
+
86
+ | Preset | Description |
87
+ | --- | --- |
88
+ | `default` | Staged schedule of the paper (rotation, translation, landmarks, calibration, full) followed by a least-squares fit of the extrinsics; use with the corrected pose processing (default of all templates) |
89
+ | `cvprw2026` | Frozen paper configuration; together with `paper/cvprw2026` it reproduces the published results |
90
+ | `joint` | Experimental: extrinsics as a variable of the factor graph instead of the least-squares fit |
91
+
92
+ ## Reproducing the paper
93
+
94
+ [`paper/cvprw2026`](paper/cvprw2026) contains the scripts, campaign definitions and initial calibrations
95
+ of the CVPRW 2026 evaluation, the reproduction log and an ablation of geodetic corrections that were not
96
+ part of the paper version (float32 coordinates, meridian convergence, heights, interpolation).
97
+ The meridian convergence correction alone lowers the test reprojection error by 21 % on average over the
98
+ six LTC campaigns.
99
+
100
+ ## Development
101
+
102
+ ```bash
103
+ pixi run test # unit, synthetic and system tests; regression tests need local LTC data
104
+ pixi run -e repro test # bit-exact paper regression with GTSAM 4.2.0
105
+ ```
106
+
107
+ ## Citation
108
+
109
+ If you use SCAR in academic work, please cite:
110
+
111
+ ```bibtex
112
+ @InProceedings{Holzemann_2026_CVPR,
113
+ author = {H\"olzemann, Henry and Schleiss, Michael},
114
+ title = {SCAR: Satellite Imagery-Based Calibration for Aerial Recordings},
115
+ booktitle = {Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition (CVPR) Workshops},
116
+ month = {June},
117
+ year = {2026},
118
+ pages = {987-996}
119
+ }
120
+ ```
121
+
122
+ Reference data obtained through georaffer are subject to the licenses of the respective providers
123
+ (e.g. NRW: dl-de/zero-2.0, RLP: dl-de/by-2.0); SCAR does not redistribute them.
@@ -0,0 +1,104 @@
1
+ [build-system]
2
+ requires = ["setuptools>=64", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "scar-calib"
7
+ version = "2.0.0"
8
+ description = "Satellite-based Calibration for Aerial Recordings"
9
+ authors = [{ name = "Henry Hölzemann", email = "henry.hoelzemann@fkie.fraunhofer.de" }]
10
+ readme = "README.md"
11
+ license = { file = "LICENSE" }
12
+ requires-python = ">=3.11"
13
+ dependencies = [
14
+ "numpy>=1.24",
15
+ "scipy>=1.10",
16
+ "gtsam>=4.2",
17
+ "pyproj>=3.6",
18
+ "opencv-python>=4.8",
19
+ "pyyaml>=6.0",
20
+ ]
21
+
22
+ [project.optional-dependencies]
23
+ legacy = ["utm==0.8.1"]
24
+ references = ["georay>=0.1", "rasterio>=1.3"]
25
+ germany = ["georay[germany]>=0.1"]
26
+ matching = ["torch", "vismatch>=1.3"]
27
+ viewer = ["rerun-sdk>=0.23"]
28
+ dev = ["pytest>=7.0", "ruff>=0.6"]
29
+
30
+ [project.scripts]
31
+ scar = "scar.cli:main"
32
+
33
+ [tool.setuptools.packages.find]
34
+ where = ["."]
35
+ include = ["scar*"]
36
+
37
+ [tool.setuptools.package-data]
38
+ scar = ["config/presets/*.yaml", "config/templates/*.yaml"]
39
+
40
+ [tool.pytest.ini_options]
41
+ testpaths = ["tests"]
42
+ markers = [
43
+ "regression: needs local legacy anchors and INS data (set SCAR_TEST_DATA)",
44
+ ]
45
+
46
+ [tool.pixi.workspace]
47
+ channels = ["conda-forge"]
48
+ platforms = ["linux-64"]
49
+
50
+ [tool.pixi.dependencies]
51
+ rasterio = ">=1.3"
52
+ numpy = ">=1.24"
53
+ scipy = ">=1.10"
54
+ pyproj = ">=3.6"
55
+ pyyaml = ">=6.0"
56
+
57
+ [tool.pixi.feature.latest.dependencies]
58
+ python = "3.12.*"
59
+ gtsam = ">=4.3"
60
+
61
+ # bit-exact reproduction of the CVPRW 2026 results (GTSAM version of the original environment)
62
+ [tool.pixi.feature.repro.dependencies]
63
+ python = "3.11.*"
64
+ gtsam = { version = "==4.2.0", build = "py311hc9e34d7_12" }
65
+
66
+ [tool.pixi.feature.dev.dependencies]
67
+ pytest = ">=7.0"
68
+ ruff = ">=0.6"
69
+
70
+ [tool.pixi.pypi-dependencies]
71
+ scar-calib = { path = ".", editable = true, extras = ["legacy"] }
72
+ georay = ">=0.1"
73
+
74
+ [tool.pixi.activation.env]
75
+ # keep system-wide Python paths (e.g. ROS) out of the environment
76
+ PYTHONPATH = ""
77
+
78
+ # GPU matchers (torch + vismatch); several GB, install on demand: pixi install -e gpu
79
+ [tool.pixi.feature.gpu.system-requirements]
80
+ cuda = "12"
81
+
82
+ [tool.pixi.feature.gpu.dependencies]
83
+ pytorch-gpu = ">=2.4"
84
+ torchvision = "*"
85
+ setuptools = ">=83"
86
+ rerun-sdk = ">=0.23"
87
+
88
+ [tool.pixi.feature.gpu.pypi-dependencies]
89
+ vismatch = ">=1.3"
90
+
91
+ # German state tiles (NRW, RLP, ...) for the georay reference provider
92
+ [tool.pixi.feature.germany.dependencies]
93
+ libgdal-jp2openjpeg = "*" # GDAL driver for the JP2 orthophotos of NRW and others
94
+
95
+ [tool.pixi.feature.germany.pypi-dependencies]
96
+ georaffer = ">=0.2"
97
+
98
+ [tool.pixi.environments]
99
+ default = ["latest", "dev"]
100
+ repro = ["repro", "dev"]
101
+ gpu = ["latest", "gpu", "germany", "dev"]
102
+
103
+ [tool.pixi.tasks]
104
+ test = "python -m pytest"
@@ -0,0 +1,10 @@
1
+ """SCAR: satellite-based calibration for aerial recordings.
2
+
3
+ Typical use goes through a run configuration (``scar init``/``scar run``); the library API:
4
+
5
+ - :mod:`scar.core` data model (camera, poses, frames, anchors), :mod:`scar.geodesy` world frames,
6
+ - :mod:`scar.datasets` / :mod:`scar.sources` inputs, :mod:`scar.references` reference data,
7
+ - :mod:`scar.collect` anchor collection, :mod:`scar.optimize` calibration, :mod:`scar.evaluate` metrics.
8
+ """
9
+
10
+ __version__ = "2.0.0"
@@ -0,0 +1,234 @@
1
+ """Command line interface: ``scar init | check | collect | optimize | run | export``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import logging
7
+ import shutil
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ TEMPLATES = Path(__file__).resolve().parent / "config" / "templates"
12
+
13
+
14
+ def _viewer(config, enabled: bool):
15
+ if not (enabled or config.viewer.enabled):
16
+ return None
17
+ from scar.viz.rerun_viewer import RerunViewer
18
+
19
+ save = str(config.path(config.viewer.save)) if config.viewer.save else None
20
+ return RerunViewer(application_id=config.name, spawn=config.viewer.spawn, save=save)
21
+
22
+
23
+ def cmd_init(args) -> int:
24
+ source = TEMPLATES / f"{args.template}.yaml"
25
+ target = Path(args.path)
26
+ if target.exists() and not args.force:
27
+ print(f"{target} exists (use --force to overwrite)", file=sys.stderr)
28
+ return 1
29
+ target.parent.mkdir(parents=True, exist_ok=True)
30
+ shutil.copyfile(source, target)
31
+ print(f"wrote {target}; edit the paths and run 'scar check {target}'")
32
+ return 0
33
+
34
+
35
+ def cmd_check(args) -> int:
36
+ import numpy as np
37
+
38
+ from scar.config.schema import load_run_config
39
+ from scar.pipeline import prepare_sequence
40
+
41
+ config = load_run_config(args.config)
42
+ prepared = prepare_sequence(config)
43
+ sequence = prepared.sequence
44
+ frames = sequence.frames
45
+ positions = np.asarray([f.pose_prior.T_world_body[:3, 3] for f in frames])
46
+ duration = (frames[-1].timestamp_ns - frames[0].timestamp_ns) * 1e-9
47
+ camera = sequence.camera
48
+
49
+ print(f"sequence {len(frames)} frames over {duration:.1f} s, path {np.sum(np.linalg.norm(np.diff(positions, axis=0), axis=1)):.0f} m")
50
+ print(f"camera {camera.width}x{camera.height}, f = ({camera.fx:.1f}, {camera.fy:.1f}), c = ({camera.cx:.1f}, {camera.cy:.1f}), "
51
+ f"distortion {[round(float(d), 5) for d in camera.distortion]}")
52
+ print(f"world {sequence.world_frame.crs}, heights {sequence.world_frame.height.kind}"
53
+ f"{' (' + sequence.world_frame.height.name + ')' if sequence.world_frame.height.name else ''}, "
54
+ f"meridian convergence {'on' if sequence.world_frame.apply_meridian_convergence else 'off'}")
55
+ print(f"positions x {positions[:, 0].min():.1f}..{positions[:, 0].max():.1f}, y {positions[:, 1].min():.1f}..{positions[:, 1].max():.1f}, "
56
+ f"height {positions[:, 2].min():.1f}..{positions[:, 2].max():.1f}")
57
+ missing = [f.image_path for f in frames[:: max(1, len(frames) // 20)] if not prepared.images.resolve(f.image_path).exists()]
58
+ if missing:
59
+ print(f"images MISSING, e.g. {prepared.images.resolve(missing[0])}")
60
+ return 1
61
+ print("images found")
62
+
63
+ if args.render:
64
+ import cv2
65
+
66
+ from scar.pipeline import reference_provider
67
+
68
+ references = reference_provider(config, sequence.world_frame)
69
+ output = config.path(config.output) / "check"
70
+ output.mkdir(parents=True, exist_ok=True)
71
+ for frame in (frames[0], frames[len(frames) // 2], frames[-1]):
72
+ view = references.render(frame.pose_prior.T_world_camera(sequence.extrinsics), camera)
73
+ image = cv2.cvtColor(prepared.read_image(frame), cv2.COLOR_GRAY2BGR)
74
+ reference = cv2.cvtColor(view.image, cv2.COLOR_RGB2BGR)
75
+ scale = image.shape[0] / reference.shape[0]
76
+ reference = cv2.resize(reference, (int(reference.shape[1] * scale), image.shape[0]))
77
+ path = output / f"frame_{frame.frame_id:05d}.png"
78
+ cv2.imwrite(str(path), np.hstack([image, reference]))
79
+ print(f"reference frame {frame.frame_id}: {view.image.shape[1]}x{view.image.shape[0]} px at "
80
+ f"{view.metadata['pixels_per_meter']} px/m -> {path}")
81
+ return 0
82
+
83
+
84
+ def _collect(config, args):
85
+ from scar.pipeline import prepare_sequence, run_collect
86
+
87
+ prepared = prepare_sequence(config)
88
+ viewer = _viewer(config, args.viewer)
89
+ on_frame = None
90
+ if viewer is not None:
91
+ sequence = prepared.sequence
92
+
93
+ def on_frame(report):
94
+ viewer.on_frame(report, report.frame.pose_prior.T_world_camera(sequence.extrinsics), sequence.camera)
95
+ return run_collect(config, resume=args.resume, on_frame=on_frame, prepared=prepared), viewer
96
+
97
+
98
+ def cmd_collect(args) -> int:
99
+ from scar.config.schema import load_run_config
100
+
101
+ config = load_run_config(args.config)
102
+ result, _ = _collect(config, args)
103
+ print(f"{len(result.landmarks)} anchors, {len(result.observations)} observations -> "
104
+ f"{config.path(config.output) / 'anchors.json'}")
105
+ return 0
106
+
107
+
108
+ def _report_calibration(result, config) -> None:
109
+ from scar.core.se3 import rotation_angle
110
+
111
+ camera = result.camera
112
+ problem = result.problem
113
+ change = rotation_angle(result.T_body_camera[:3, :3], problem.extrinsics.T_body_camera[:3, :3])
114
+ print(f"optimized {problem.num_frames} frames, {problem.num_landmarks} anchors, {problem.num_observations} observations")
115
+ print(f"intrinsics f = ({camera.fx:.2f}, {camera.fy:.2f}), c = ({camera.cx:.2f}, {camera.cy:.2f}), "
116
+ f"distortion {[round(float(d), 5) for d in camera.distortion]}")
117
+ print(f"extrinsics rotation changed by {change * 57.29577951308232:.3f} deg")
118
+ print(f"written {config.path(config.output) / 'calibration' / 'calibration.yaml'}")
119
+
120
+
121
+ def cmd_optimize(args) -> int:
122
+ from scar.config.schema import load_run_config
123
+ from scar.pipeline import run_optimize
124
+
125
+ config = load_run_config(args.config)
126
+ result = run_optimize(config)
127
+ viewer = _viewer(config, args.viewer)
128
+ if viewer is not None:
129
+ viewer.on_calibration(result)
130
+ _report_calibration(result, config)
131
+ return 0
132
+
133
+
134
+ def cmd_run(args) -> int:
135
+ from scar.config.schema import load_run_config
136
+ from scar.pipeline import run_optimize
137
+
138
+ config = load_run_config(args.config)
139
+ anchors, viewer = _collect(config, args)
140
+ result = run_optimize(config, anchors)
141
+ if viewer is not None:
142
+ viewer.on_calibration(result)
143
+ _report_calibration(result, config)
144
+ return 0
145
+
146
+
147
+ def cmd_evaluate(args) -> int:
148
+ import numpy as np
149
+
150
+ from scar.evaluate import (
151
+ ReprojectionStatistics,
152
+ estimate_pose_offset,
153
+ localize,
154
+ reprojection_errors,
155
+ )
156
+ from scar.io import load_camera, load_extrinsics, load_sequence
157
+
158
+ anchors = load_sequence(args.anchors)
159
+ camera, T = load_camera(args.calibration), load_extrinsics(args.calibration).T_body_camera
160
+ print(f"anchors {len(anchors.frames)} frames, {len(anchors.landmarks)} anchors, {len(anchors.observations)} observations")
161
+ print(f"reprojection {ReprojectionStatistics.of(reprojection_errors(anchors, camera, T))}")
162
+ if args.offsets:
163
+ offset = estimate_pose_offset(anchors, camera, T, estimate_rotation=True)
164
+ print(f"offsets translation {np.round(offset.delta, 3).tolist()} m, time {1000 * offset.dt:+.1f} ms, "
165
+ f"camera rotation {np.round(offset.omega_deg, 3).tolist()} deg (reprojection then {offset.after})")
166
+ if args.localize:
167
+ summary = localize(anchors, camera, T).summary()
168
+ accuracy = ", ".join(f"{key} {value:.3f}" for key, value in summary["accuracy"].items())
169
+ print(f"localization {summary['localized']}/{summary['frames']} frames, median rotation "
170
+ f"{summary['median_rotation_error_deg']:.3f} deg, translation {summary['median_translation_error_m']:.2f} m; {accuracy}")
171
+ return 0
172
+
173
+
174
+ def cmd_export(args) -> int:
175
+ from scar.io import export_calibration, load_camera, load_extrinsics
176
+
177
+ files = export_calibration(args.output, load_camera(args.calibration), load_extrinsics(args.calibration), args.format)
178
+ for path in files:
179
+ print(f"wrote {path}")
180
+ return 0
181
+
182
+
183
+ def main(argv: list[str] | None = None) -> int:
184
+ parser = argparse.ArgumentParser(prog="scar", description="Satellite-based calibration for aerial recordings")
185
+ parser.add_argument("-v", "--verbose", action="store_true", help="log progress (INFO)")
186
+ commands = parser.add_subparsers(dest="command", required=True)
187
+
188
+ init = commands.add_parser("init", help="write a configuration template")
189
+ init.add_argument("path")
190
+ init.add_argument("--template", choices=sorted(p.stem for p in TEMPLATES.glob("*.yaml")), default="csv")
191
+ init.add_argument("--force", action="store_true")
192
+ init.set_defaults(func=cmd_init)
193
+
194
+ check = commands.add_parser("check", help="validate a configuration and its inputs")
195
+ check.add_argument("config")
196
+ check.add_argument("--render", action="store_true", help="render references for three frames (needs georay)")
197
+ check.set_defaults(func=cmd_check)
198
+
199
+ for name, func, help_text in (("collect", cmd_collect, "collect anchors"),
200
+ ("optimize", cmd_optimize, "optimize collected anchors"), ("run", cmd_run, "collect and optimize")):
201
+ command = commands.add_parser(name, help=help_text)
202
+ command.add_argument("config")
203
+ command.add_argument("--viewer", action="store_true", help="show progress in rerun")
204
+ if name != "optimize":
205
+ command.add_argument("--resume", action="store_true", help="continue from the last checkpoint")
206
+ command.set_defaults(func=func)
207
+
208
+ evaluate = commands.add_parser("evaluate", help="evaluate a calibration on independent anchors")
209
+ evaluate.add_argument("anchors", help="anchors.json of a test sequence (collected with the same pose processing)")
210
+ evaluate.add_argument("--calibration", required=True, help="calibration.yaml to evaluate")
211
+ evaluate.add_argument("--localize", action="store_true", help="visual localization (PnP) per frame; needs "
212
+ "per-frame anchors, e.g. collected with tracking disabled")
213
+ evaluate.add_argument("--offsets", action="store_true", help="estimate translation/time/rotation offsets")
214
+ evaluate.set_defaults(func=cmd_evaluate)
215
+
216
+ export = commands.add_parser("export", help="write a calibration for other tools")
217
+ export.add_argument("calibration", help="calibration.yaml of a run")
218
+ export.add_argument("--format", choices=["kalibr", "opencv", "ltc"], required=True)
219
+ export.add_argument("--output", required=True)
220
+ export.set_defaults(func=cmd_export)
221
+
222
+ args = parser.parse_args(argv)
223
+ logging.basicConfig(level=logging.INFO if args.verbose else logging.WARNING, format="%(asctime)s %(name)s: %(message)s")
224
+ try:
225
+ return args.func(args)
226
+ except Exception as error:
227
+ if args.verbose:
228
+ raise
229
+ print(f"error: {error}", file=sys.stderr)
230
+ return 1
231
+
232
+
233
+ if __name__ == "__main__":
234
+ sys.exit(main())