Skip to content

Commit 303877c

Browse files
committed
Match DIVE registration.json input format
1 parent a887e02 commit 303877c

3 files changed

Lines changed: 131 additions & 52 deletions

File tree

kamera/postflight/registration_homography.py

Lines changed: 82 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,26 @@
33
Co-located cameras are visualized (e.g. in DIVE) by warping one modality
44
onto another with a plane homography. This module derives that homography
55
for each non-reference camera at a station straight from the calibrated
6-
camera models -- no scene points or hand-clicked correspondences. It
7-
writes one ``<camera>_to_<reference_camera>_registration.json`` per
8-
non-reference camera (matching the registration gif naming):
9-
10-
{"camera": "<camera folder>",
11-
"reference_camera": "<reference camera folder>",
12-
"camera_to_reference": <3x3>,
13-
"reference_to_camera": <3x3>}
14-
15-
``camera_to_reference`` maps a pixel in the camera's image to the
16-
reference (EO) camera's pixel seeing the same scene point, and
17-
``reference_to_camera`` is its inverse. The cameras are rigidly
18-
co-located, so the pixel map is a homography to within a fraction of a
19-
pixel; it is for rendering, not an accuracy metric (use the registration
20-
gifs for that).
6+
camera models -- no scene points or hand-clicked correspondences -- and
7+
writes one DIVE camera-registration file per station
8+
(``<station>_registration.json``):
9+
10+
{"type": "dive-camera-registration",
11+
"version": 1,
12+
"source": {"model": "kamera-v3", "flight": "<flight>"},
13+
"pairs": [{"left": "eo", "right": "ir",
14+
"points": [[xl, yl, xr, yr], ...],
15+
"leftToRight": <3x3>, "rightToLeft": <3x3>,
16+
"transformType": "homography"}, ...]}
17+
18+
``left`` is always the station's reference camera under its DIVE name
19+
(rgb -> "eo"), and ``leftToRight`` maps a left pixel to the right
20+
camera's pixel seeing the same scene point. ``points`` are exact
21+
correspondences under the homography, sampled on a grid over the right
22+
image, so a consumer can refit its own transform type. The cameras are
23+
rigidly co-located, so the pixel map is a homography to within a
24+
fraction of a pixel; it is for rendering, not an accuracy metric (use
25+
the registration gifs for that).
2126
"""
2227

2328
import json
@@ -80,17 +85,40 @@ def pixel_homography(
8085
return h
8186

8287

88+
# DIVE's camera names where they differ from KAMERA modality names.
89+
_DIVE_CAMERA_NAMES = {"rgb": "eo"}
90+
91+
92+
def _grid_correspondences(
93+
cam_to_ref: np.ndarray, width: int, height: int, n: int = 3
94+
) -> List[List[float]]:
95+
"""Exact [x_left, y_left, x_right, y_right] correspondences under the
96+
homography, on an ``n x n`` grid over the right (non-reference) image.
97+
The right footprint sits inside the reference view for co-located
98+
KAMERA stations, so gridding the right image keeps both ends in frame.
99+
"""
100+
x = np.linspace(0, width - 1, n)
101+
y = np.linspace(0, height - 1, n)
102+
X, Y = np.meshgrid(x, y)
103+
right = np.vstack([X.ravel(), Y.ravel(), np.ones(X.size)])
104+
left = cam_to_ref @ right
105+
left = left[:2] / left[2]
106+
return np.round(np.vstack([left, right[:2]]).T, 2).tolist()
107+
108+
83109
def write_registration_homographies(
84110
models: Dict[str, StandardCamera],
85111
save_dir: str,
86112
reference_modality: str = "rgb",
113+
flight: str = "",
114+
source_model: str = "kamera-v3",
87115
) -> List[str]:
88-
"""Write one ``<camera>_to_<reference_camera>_registration.json`` per
89-
non-reference camera under ``save_dir``.
116+
"""Write one DIVE camera-registration json per station under
117+
``save_dir`` (``<station>_registration.json``).
90118
91119
``models`` maps camera folder (e.g. ``21deg_N56RF_center_rgb``) to its
92-
StandardCamera. Cameras are grouped by station; each non-reference
93-
camera gets its own file with the homography to its station's
120+
StandardCamera. Cameras are grouped by station; each station's file
121+
holds one ``pairs`` entry per non-reference camera, left = the
94122
reference (EO) camera. Returns the paths written.
95123
"""
96124
by_station: Dict[str, Dict[str, str]] = {}
@@ -108,7 +136,9 @@ def write_registration_homographies(
108136
)
109137
continue
110138
ref = models[ref_folder]
139+
left = _DIVE_CAMERA_NAMES.get(reference_modality, reference_modality)
111140

141+
pairs = []
112142
for modality, folder in sorted(folders.items()):
113143
if modality == reference_modality:
114144
continue
@@ -117,21 +147,39 @@ def write_registration_homographies(
117147
except ValueError as e:
118148
print(f"[yellow]Station {station} {modality}: {e}; skipping pair.")
119149
continue
120-
os.makedirs(save_dir, exist_ok=True)
121-
out_path = os.path.join(
122-
save_dir, f"{folder}_to_{ref_folder}_registration.json"
150+
ref_to_cam = np.linalg.inv(cam_to_ref)
151+
ref_to_cam /= ref_to_cam[2, 2]
152+
cam = models[folder]
153+
pairs.append(
154+
{
155+
"left": left,
156+
"right": _DIVE_CAMERA_NAMES.get(modality, modality),
157+
"points": _grid_correspondences(
158+
cam_to_ref, cam.width, cam.height
159+
),
160+
"leftToRight": ref_to_cam.tolist(),
161+
"rightToLeft": cam_to_ref.tolist(),
162+
"transformType": "homography",
163+
}
164+
)
165+
if not pairs:
166+
continue
167+
168+
name = KameraCameraName.parse(ref_folder)
169+
station_slug = f"{name.prefix}_{name.channel}" if name.prefix else name.channel
170+
os.makedirs(save_dir, exist_ok=True)
171+
out_path = os.path.join(save_dir, f"{station_slug}_registration.json")
172+
with open(out_path, "w") as f:
173+
json.dump(
174+
{
175+
"type": "dive-camera-registration",
176+
"version": 1,
177+
"source": {"model": source_model, "flight": flight},
178+
"pairs": pairs,
179+
},
180+
f,
181+
indent=2,
123182
)
124-
with open(out_path, "w") as f:
125-
json.dump(
126-
{
127-
"camera": folder,
128-
"reference_camera": ref_folder,
129-
"camera_to_reference": cam_to_ref.tolist(),
130-
"reference_to_camera": np.linalg.inv(cam_to_ref).tolist(),
131-
},
132-
f,
133-
indent=2,
134-
)
135-
written.append(out_path)
183+
written.append(out_path)
136184

137185
return written

kamera/postflight/scripts/calibrate_rig.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -596,7 +596,11 @@ def main():
596596
print(f"Wrote calibration error report: {report_path}")
597597

598598
if all_models:
599-
cal_paths = write_registration_homographies(all_models, save_dir)
599+
cal_paths = write_registration_homographies(
600+
all_models,
601+
save_dir,
602+
flight=os.path.basename(os.path.normpath(flight_dir)),
603+
)
600604
if cal_paths:
601605
print(f"Wrote {len(cal_paths)} registration homography files:")
602606
for path in cal_paths:
Lines changed: 44 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,64 @@
11
import json
2+
from types import SimpleNamespace
23

34
import numpy as np
45

56
from kamera.postflight import registration_homography as rh
67

78

8-
def test_write_registration_homographies_per_camera(tmp_path, monkeypatch):
9+
def test_write_registration_homographies_per_station(tmp_path, monkeypatch):
910
h = np.diag([1.0, 2.0, 1.0])
1011
monkeypatch.setattr(rh, "pixel_homography", lambda src, dst: h)
1112

12-
# models are only looked up, never called, thanks to the stub
13+
cam = SimpleNamespace(width=64, height=32)
1314
models = {
14-
"21deg_N56RF_center_rgb": "center-rgb",
15-
"21deg_N56RF_center_uv": "center-uv",
16-
"21deg_N56RF_center_ir": "center-ir",
17-
"25deg_N56RF_left_rgb": "left-rgb",
18-
"25deg_N56RF_left_uv": "left-uv",
19-
"30deg_N56RF_right_uv": "right-uv", # no rgb reference -> skipped
15+
"21deg_N56RF_center_rgb": cam,
16+
"21deg_N56RF_center_uv": cam,
17+
"21deg_N56RF_center_ir": cam,
18+
"25deg_N56RF_left_rgb": cam,
19+
"25deg_N56RF_left_uv": cam,
20+
"30deg_N56RF_right_uv": cam, # no rgb reference -> skipped
2021
}
21-
written = rh.write_registration_homographies(models, str(tmp_path))
22+
written = rh.write_registration_homographies(
23+
models, str(tmp_path), flight="fl07"
24+
)
2225

2326
assert sorted(written) == [
2427
str(tmp_path / name)
2528
for name in [
26-
"21deg_N56RF_center_ir_to_21deg_N56RF_center_rgb_registration.json",
27-
"21deg_N56RF_center_uv_to_21deg_N56RF_center_rgb_registration.json",
28-
"25deg_N56RF_left_uv_to_25deg_N56RF_left_rgb_registration.json",
29+
"21deg_N56RF_center_registration.json",
30+
"25deg_N56RF_left_registration.json",
2931
]
3032
]
3133

32-
with open(written[0]) as f:
34+
with open(tmp_path / "21deg_N56RF_center_registration.json") as f:
3335
d = json.load(f)
34-
assert d["camera"] == "21deg_N56RF_center_ir"
35-
assert d["reference_camera"] == "21deg_N56RF_center_rgb"
36-
assert np.allclose(d["camera_to_reference"], h)
37-
assert np.allclose(d["reference_to_camera"], np.linalg.inv(h))
36+
assert d["type"] == "dive-camera-registration"
37+
assert d["version"] == 1
38+
assert d["source"] == {"model": "kamera-v3", "flight": "fl07"}
39+
assert [(p["left"], p["right"]) for p in d["pairs"]] == [
40+
("eo", "ir"),
41+
("eo", "uv"),
42+
]
43+
44+
pair = d["pairs"][0]
45+
assert pair["transformType"] == "homography"
46+
# rightToLeft is the camera->reference homography; leftToRight its
47+
# inverse, normalized so [2][2] == 1.
48+
assert np.allclose(pair["rightToLeft"], h)
49+
assert np.allclose(pair["leftToRight"], np.linalg.inv(h))
50+
assert np.array(pair["leftToRight"])[2, 2] == 1.0
51+
52+
# points are [x_left, y_left, x_right, y_right] correspondences that
53+
# satisfy the homography exactly, gridded over the right image.
54+
pts = np.array(pair["points"])
55+
assert pts.shape == (9, 4)
56+
assert pts[:, 2].min() == 0 and pts[:, 2].max() == cam.width - 1
57+
assert pts[:, 3].min() == 0 and pts[:, 3].max() == cam.height - 1
58+
right_h = np.vstack([pts[:, 2], pts[:, 3], np.ones(len(pts))])
59+
left = h @ right_h
60+
assert np.allclose(pts[:, :2], (left[:2] / left[2]).T)
61+
62+
with open(tmp_path / "25deg_N56RF_left_registration.json") as f:
63+
d = json.load(f)
64+
assert [(p["left"], p["right"]) for p in d["pairs"]] == [("eo", "uv")]

0 commit comments

Comments
 (0)