colmap4d 1.0.1__tar.gz → 1.0.2__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.
- colmap4d-1.0.2/PKG-INFO +96 -0
- colmap4d-1.0.2/README.md +64 -0
- {colmap4d-1.0.1 → colmap4d-1.0.2}/pyproject.toml +1 -1
- {colmap4d-1.0.1 → colmap4d-1.0.2}/src/colmap4d/__init__.py +7 -1
- colmap4d-1.0.2/src/colmap4d/convert/fixed_rig.py +204 -0
- colmap4d-1.0.2/src/colmap4d/groups.py +359 -0
- colmap4d-1.0.2/src/colmap4d/rig_calibration.py +178 -0
- colmap4d-1.0.2/src/colmap4d.egg-info/PKG-INFO +96 -0
- {colmap4d-1.0.1 → colmap4d-1.0.2}/src/colmap4d.egg-info/SOURCES.txt +2 -0
- colmap4d-1.0.1/PKG-INFO +0 -138
- colmap4d-1.0.1/README.md +0 -106
- colmap4d-1.0.1/src/colmap4d/groups.py +0 -14
- colmap4d-1.0.1/src/colmap4d.egg-info/PKG-INFO +0 -138
- {colmap4d-1.0.1 → colmap4d-1.0.2}/LICENSE +0 -0
- {colmap4d-1.0.1 → colmap4d-1.0.2}/setup.cfg +0 -0
- {colmap4d-1.0.1 → colmap4d-1.0.2}/src/colmap4d/colmap_io.py +0 -0
- {colmap4d-1.0.1 → colmap4d-1.0.2}/src/colmap4d/convert/__init__.py +0 -0
- {colmap4d-1.0.1 → colmap4d-1.0.2}/src/colmap4d/convert/per_frame_colmap.py +0 -0
- {colmap4d-1.0.1 → colmap4d-1.0.2}/src/colmap4d/model.py +0 -0
- {colmap4d-1.0.1 → colmap4d-1.0.2}/src/colmap4d/sidecar.py +0 -0
- {colmap4d-1.0.1 → colmap4d-1.0.2}/src/colmap4d/validate.py +0 -0
- {colmap4d-1.0.1 → colmap4d-1.0.2}/src/colmap4d.egg-info/dependency_links.txt +0 -0
- {colmap4d-1.0.1 → colmap4d-1.0.2}/src/colmap4d.egg-info/requires.txt +0 -0
- {colmap4d-1.0.1 → colmap4d-1.0.2}/src/colmap4d.egg-info/top_level.txt +0 -0
colmap4d-1.0.2/PKG-INFO
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: colmap4d
|
|
3
|
+
Version: 1.0.2
|
|
4
|
+
Summary: Standard COLMAP sparse model + per-image and per-point timestamps (x, y, z, t).
|
|
5
|
+
Author: colmap4d authors
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/Kiri-Innovation/colmap4d
|
|
8
|
+
Project-URL: Repository, https://github.com/Kiri-Innovation/colmap4d
|
|
9
|
+
Project-URL: Specification, https://github.com/Kiri-Innovation/colmap4d/blob/main/spec/colmap4d-v1.0.md
|
|
10
|
+
Project-URL: Changelog, https://github.com/Kiri-Innovation/colmap4d/blob/main/spec/CHANGELOG.md
|
|
11
|
+
Keywords: colmap,sfm,4d,dynamic-scene,gaussian-splatting
|
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
13
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Intended Audience :: Science/Research
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Image Recognition
|
|
21
|
+
Classifier: Topic :: Multimedia :: Graphics :: 3D Modeling
|
|
22
|
+
Classifier: Operating System :: OS Independent
|
|
23
|
+
Requires-Python: >=3.9
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Provides-Extra: model
|
|
27
|
+
Requires-Dist: pycolmap>=0.6; extra == "model"
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
30
|
+
Requires-Dist: ruff>=0.4; extra == "dev"
|
|
31
|
+
Dynamic: license-file
|
|
32
|
+
|
|
33
|
+
# colmap4d
|
|
34
|
+
|
|
35
|
+
[](https://pypi.org/project/colmap4d/)
|
|
36
|
+
[](https://github.com/Kiri-Innovation/colmap4d/actions/workflows/ci.yml)
|
|
37
|
+
[](https://github.com/Kiri-Innovation/colmap4d/blob/main/spec/colmap4d-v1.0.md)
|
|
38
|
+
|
|
39
|
+
**A standard COLMAP sparse model + one timestamp per image + one timestamp per point.**
|
|
40
|
+
Every image and every point becomes an independent sample in spacetime `(x, y, z, t)`.
|
|
41
|
+
Nothing else: frame sync is just timestamps being close, and any discretization is a
|
|
42
|
+
derived view or a render parameter, never stored data.
|
|
43
|
+
|
|
44
|
+
colmap4d adds up to three **sidecar** files to an ordinary COLMAP model and changes no
|
|
45
|
+
existing file — so old tools open it as a perfectly normal (static) reconstruction, while a
|
|
46
|
+
4D-aware viewer unlocks a free 3D view + a draggable time axis.
|
|
47
|
+
|
|
48
|
+
> Spec **v1.0 — frozen.** See [the specification](https://github.com/Kiri-Innovation/colmap4d/blob/main/spec/colmap4d-v1.0.md);
|
|
49
|
+
> Part I and the conformance goldens are normative, and changes follow
|
|
50
|
+
> [the CHANGELOG](https://github.com/Kiri-Innovation/colmap4d/blob/main/spec/CHANGELOG.md).
|
|
51
|
+
>
|
|
52
|
+
> colmap4d is an **independent, community extension** of the COLMAP output format. It is **not
|
|
53
|
+
> affiliated with or endorsed by** the COLMAP project; "COLMAP" is used only to name the format
|
|
54
|
+
> it extends.
|
|
55
|
+
|
|
56
|
+
## Which one are you?
|
|
57
|
+
|
|
58
|
+
| I want to… | go to |
|
|
59
|
+
|------------|-------|
|
|
60
|
+
| **Read** colmap4d data (the common case) | [Quick start](#read-colmap4d-data-quick-start) below — `pip install colmap4d`, three lines |
|
|
61
|
+
| **Produce** colmap4d data (convert / write) | [`docs/converters.md`](https://github.com/Kiri-Innovation/colmap4d/blob/main/docs/converters.md) |
|
|
62
|
+
| **Build a second implementation** (C++/Rust/JS) | [the spec](https://github.com/Kiri-Innovation/colmap4d/blob/main/spec/colmap4d-v1.0.md): implement Part I, then pass every fixture in [`conformance/golden/`](https://github.com/Kiri-Innovation/colmap4d/tree/main/conformance/golden) at the `v1.0` tag |
|
|
63
|
+
| **Contribute** (human or AI agent) | [`CONTRIBUTING.md`](https://github.com/Kiri-Innovation/colmap4d/blob/main/CONTRIBUTING.md) + [`AGENTS.md`](https://github.com/Kiri-Innovation/colmap4d/blob/main/AGENTS.md) |
|
|
64
|
+
| **Drive colmap4d from an AI agent** | the [colmap4d skill](https://github.com/Kiri-Innovation/colmap4d/blob/main/skills/colmap4d/SKILL.md) |
|
|
65
|
+
|
|
66
|
+
More detail: [repository layout, dev commands & versioning](https://github.com/Kiri-Innovation/colmap4d/blob/main/docs/repo-layout.md).
|
|
67
|
+
|
|
68
|
+
## Read colmap4d data (quick start)
|
|
69
|
+
|
|
70
|
+
Read a COLMAP model together with its time layer — **zero dependencies**:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
import colmap4d
|
|
74
|
+
|
|
75
|
+
mv = colmap4d.load_model_view("path/to/sparse") # COLMAP model dir, with or without sidecars
|
|
76
|
+
|
|
77
|
+
mv.point_time(42) # -> t_ns (int64 ns), or None if the point is temporally-unbounded
|
|
78
|
+
mv.image_time(1) # -> t_ns, or None
|
|
79
|
+
mv.effective_points_t() # {point3d_id: t_ns}, dangling ids already dropped
|
|
80
|
+
mv.sidecars.time_meta # {"clock_domain": "utc_ntp", ...} or None
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Just the timestamps, without the base model: `colmap4d.load_sidecars("path/to/sparse")`.
|
|
84
|
+
|
|
85
|
+
A plain COLMAP model (no sidecars) loads fine: empty times, all points temporally-unbounded —
|
|
86
|
+
the backward-compatibility baseline. The reader is standard-library only; `pip install
|
|
87
|
+
'colmap4d[model]'` adds pycolmap for authoritative parsing and 3.12 rigs/frames support.
|
|
88
|
+
|
|
89
|
+
Uploading time to a GPU (4DGS/NeRF)? Rebase first — raw epoch ns overflows float32; upload
|
|
90
|
+
`(t − t0)` seconds as float32 via `colmap4d.rebase_to_seconds_f32(t, mv.t0_ns())`.
|
|
91
|
+
|
|
92
|
+
## License
|
|
93
|
+
|
|
94
|
+
[Apache-2.0](https://github.com/Kiri-Innovation/colmap4d/blob/main/LICENSE).
|
|
95
|
+
|
|
96
|
+
<!-- link-audit: all README external links verified absolute (2026-09-04, PyPI render safety) -->
|
colmap4d-1.0.2/README.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# colmap4d
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/colmap4d/)
|
|
4
|
+
[](https://github.com/Kiri-Innovation/colmap4d/actions/workflows/ci.yml)
|
|
5
|
+
[](https://github.com/Kiri-Innovation/colmap4d/blob/main/spec/colmap4d-v1.0.md)
|
|
6
|
+
|
|
7
|
+
**A standard COLMAP sparse model + one timestamp per image + one timestamp per point.**
|
|
8
|
+
Every image and every point becomes an independent sample in spacetime `(x, y, z, t)`.
|
|
9
|
+
Nothing else: frame sync is just timestamps being close, and any discretization is a
|
|
10
|
+
derived view or a render parameter, never stored data.
|
|
11
|
+
|
|
12
|
+
colmap4d adds up to three **sidecar** files to an ordinary COLMAP model and changes no
|
|
13
|
+
existing file — so old tools open it as a perfectly normal (static) reconstruction, while a
|
|
14
|
+
4D-aware viewer unlocks a free 3D view + a draggable time axis.
|
|
15
|
+
|
|
16
|
+
> Spec **v1.0 — frozen.** See [the specification](https://github.com/Kiri-Innovation/colmap4d/blob/main/spec/colmap4d-v1.0.md);
|
|
17
|
+
> Part I and the conformance goldens are normative, and changes follow
|
|
18
|
+
> [the CHANGELOG](https://github.com/Kiri-Innovation/colmap4d/blob/main/spec/CHANGELOG.md).
|
|
19
|
+
>
|
|
20
|
+
> colmap4d is an **independent, community extension** of the COLMAP output format. It is **not
|
|
21
|
+
> affiliated with or endorsed by** the COLMAP project; "COLMAP" is used only to name the format
|
|
22
|
+
> it extends.
|
|
23
|
+
|
|
24
|
+
## Which one are you?
|
|
25
|
+
|
|
26
|
+
| I want to… | go to |
|
|
27
|
+
|------------|-------|
|
|
28
|
+
| **Read** colmap4d data (the common case) | [Quick start](#read-colmap4d-data-quick-start) below — `pip install colmap4d`, three lines |
|
|
29
|
+
| **Produce** colmap4d data (convert / write) | [`docs/converters.md`](https://github.com/Kiri-Innovation/colmap4d/blob/main/docs/converters.md) |
|
|
30
|
+
| **Build a second implementation** (C++/Rust/JS) | [the spec](https://github.com/Kiri-Innovation/colmap4d/blob/main/spec/colmap4d-v1.0.md): implement Part I, then pass every fixture in [`conformance/golden/`](https://github.com/Kiri-Innovation/colmap4d/tree/main/conformance/golden) at the `v1.0` tag |
|
|
31
|
+
| **Contribute** (human or AI agent) | [`CONTRIBUTING.md`](https://github.com/Kiri-Innovation/colmap4d/blob/main/CONTRIBUTING.md) + [`AGENTS.md`](https://github.com/Kiri-Innovation/colmap4d/blob/main/AGENTS.md) |
|
|
32
|
+
| **Drive colmap4d from an AI agent** | the [colmap4d skill](https://github.com/Kiri-Innovation/colmap4d/blob/main/skills/colmap4d/SKILL.md) |
|
|
33
|
+
|
|
34
|
+
More detail: [repository layout, dev commands & versioning](https://github.com/Kiri-Innovation/colmap4d/blob/main/docs/repo-layout.md).
|
|
35
|
+
|
|
36
|
+
## Read colmap4d data (quick start)
|
|
37
|
+
|
|
38
|
+
Read a COLMAP model together with its time layer — **zero dependencies**:
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
import colmap4d
|
|
42
|
+
|
|
43
|
+
mv = colmap4d.load_model_view("path/to/sparse") # COLMAP model dir, with or without sidecars
|
|
44
|
+
|
|
45
|
+
mv.point_time(42) # -> t_ns (int64 ns), or None if the point is temporally-unbounded
|
|
46
|
+
mv.image_time(1) # -> t_ns, or None
|
|
47
|
+
mv.effective_points_t() # {point3d_id: t_ns}, dangling ids already dropped
|
|
48
|
+
mv.sidecars.time_meta # {"clock_domain": "utc_ntp", ...} or None
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Just the timestamps, without the base model: `colmap4d.load_sidecars("path/to/sparse")`.
|
|
52
|
+
|
|
53
|
+
A plain COLMAP model (no sidecars) loads fine: empty times, all points temporally-unbounded —
|
|
54
|
+
the backward-compatibility baseline. The reader is standard-library only; `pip install
|
|
55
|
+
'colmap4d[model]'` adds pycolmap for authoritative parsing and 3.12 rigs/frames support.
|
|
56
|
+
|
|
57
|
+
Uploading time to a GPU (4DGS/NeRF)? Rebase first — raw epoch ns overflows float32; upload
|
|
58
|
+
`(t − t0)` seconds as float32 via `colmap4d.rebase_to_seconds_f32(t, mv.t0_ns())`.
|
|
59
|
+
|
|
60
|
+
## License
|
|
61
|
+
|
|
62
|
+
[Apache-2.0](https://github.com/Kiri-Innovation/colmap4d/blob/main/LICENSE).
|
|
63
|
+
|
|
64
|
+
<!-- link-audit: all README external links verified absolute (2026-09-04, PyPI render safety) -->
|
|
@@ -7,6 +7,7 @@ dependency, not implemented yet).
|
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
+
from colmap4d.groups import GridDiscard, GridGroup, GridGrouping, GridSample, group_by_grid
|
|
10
11
|
from colmap4d.model import (
|
|
11
12
|
ModelView,
|
|
12
13
|
load_model_view,
|
|
@@ -31,13 +32,18 @@ from colmap4d.sidecar import (
|
|
|
31
32
|
write_times_txt,
|
|
32
33
|
)
|
|
33
34
|
|
|
34
|
-
__version__ = "1.0.
|
|
35
|
+
__version__ = "1.0.2"
|
|
35
36
|
|
|
36
37
|
__all__ = [
|
|
37
38
|
"TIMELESS",
|
|
39
|
+
"GridDiscard",
|
|
40
|
+
"GridGroup",
|
|
41
|
+
"GridGrouping",
|
|
42
|
+
"GridSample",
|
|
38
43
|
"ModelView",
|
|
39
44
|
"Sidecars",
|
|
40
45
|
"is_temporally_unbounded",
|
|
46
|
+
"group_by_grid",
|
|
41
47
|
"load_model_view",
|
|
42
48
|
"load_sidecars",
|
|
43
49
|
"read_base_model",
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""Convert fixed-rig multi-camera captures to colmap4d using pre-calibrated poses.
|
|
2
|
+
|
|
3
|
+
For fixed camera arrays where intrinsics and extrinsics are calibrated once and reused,
|
|
4
|
+
this converter takes a rig calibration file and timestamped images, and produces a
|
|
5
|
+
colmap4d model where camera poses are from the calibration (not re-estimated).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from colmap4d import sidecar
|
|
14
|
+
from colmap4d.rig_calibration import RigCalibration
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class TimestampedImage:
|
|
19
|
+
"""Image with timestamp."""
|
|
20
|
+
|
|
21
|
+
camera_name: str # e.g., "cam1"
|
|
22
|
+
frame_index: int # Frame number in sequence
|
|
23
|
+
timestamp_ns: int # Nanosecond timestamp
|
|
24
|
+
image_path: Path | None = None # Path to image file (optional)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _fmt(x: float) -> str:
|
|
28
|
+
"""Format float for COLMAP text files."""
|
|
29
|
+
return f"{x:.12g}"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _write_cameras_txt(path: Path, calibration: RigCalibration) -> dict[str, int]:
|
|
33
|
+
"""Write cameras.txt from rig calibration.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
Mapping from camera_name to CAMERA_ID
|
|
37
|
+
"""
|
|
38
|
+
lines = ["# CAMERA_ID, MODEL, WIDTH, HEIGHT, PARAMS[]\n"]
|
|
39
|
+
|
|
40
|
+
camera_id_map = {}
|
|
41
|
+
for cam_id, (name, cam) in enumerate(sorted(calibration.cameras.items()), start=1):
|
|
42
|
+
intr = cam.intrinsics
|
|
43
|
+
lines.append(
|
|
44
|
+
f"{cam_id} {intr.model} {intr.width} {intr.height} "
|
|
45
|
+
+ " ".join(_fmt(p) for p in intr.params)
|
|
46
|
+
+ "\n"
|
|
47
|
+
)
|
|
48
|
+
camera_id_map[name] = cam_id
|
|
49
|
+
|
|
50
|
+
lines.insert(1, f"# Number of cameras: {len(camera_id_map)}\n")
|
|
51
|
+
path.write_text("".join(lines), encoding="utf-8")
|
|
52
|
+
return camera_id_map
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _write_images_txt(
|
|
56
|
+
path: Path,
|
|
57
|
+
images: list[TimestampedImage],
|
|
58
|
+
calibration: RigCalibration,
|
|
59
|
+
camera_id_map: dict[str, int],
|
|
60
|
+
) -> dict[int, int]:
|
|
61
|
+
"""Write images.txt with poses from rig calibration.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
Mapping from IMAGE_ID to timestamp_ns
|
|
65
|
+
"""
|
|
66
|
+
lines = [
|
|
67
|
+
"# IMAGE_ID, QW, QX, QY, QZ, TX, TY, TZ, CAMERA_ID, NAME\n",
|
|
68
|
+
"# POINTS2D[] as (X, Y, POINT3D_ID)\n",
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
times = {}
|
|
72
|
+
image_id = 1
|
|
73
|
+
|
|
74
|
+
for img in sorted(images, key=lambda x: (x.frame_index, x.camera_name)):
|
|
75
|
+
cam_name = img.camera_name
|
|
76
|
+
if cam_name not in calibration.cameras:
|
|
77
|
+
raise ValueError(f"Camera {cam_name} not in rig calibration")
|
|
78
|
+
|
|
79
|
+
cam = calibration.cameras[cam_name]
|
|
80
|
+
ext = cam.extrinsics
|
|
81
|
+
cam_id = camera_id_map[cam_name]
|
|
82
|
+
|
|
83
|
+
# Image name includes frame index for clarity
|
|
84
|
+
image_name = f"frame_{img.frame_index:04d}/{cam_name}.jpg"
|
|
85
|
+
|
|
86
|
+
lines.append(
|
|
87
|
+
f"{image_id} {_fmt(ext.qw)} {_fmt(ext.qx)} {_fmt(ext.qy)} {_fmt(ext.qz)} "
|
|
88
|
+
f"{_fmt(ext.tx)} {_fmt(ext.ty)} {_fmt(ext.tz)} {cam_id} {image_name}\n"
|
|
89
|
+
)
|
|
90
|
+
lines.append("\n") # Empty POINTS2D line (no observations yet)
|
|
91
|
+
|
|
92
|
+
times[image_id] = img.timestamp_ns
|
|
93
|
+
image_id += 1
|
|
94
|
+
|
|
95
|
+
lines.insert(2, f"# Number of images: {len(images)}\n")
|
|
96
|
+
path.write_text("".join(lines), encoding="utf-8")
|
|
97
|
+
return times
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _write_points3d_txt(path: Path) -> None:
|
|
101
|
+
"""Write empty points3D.txt (placeholder for now)."""
|
|
102
|
+
lines = [
|
|
103
|
+
"# POINT3D_ID, X, Y, Z, R, G, B, ERROR, TRACK[] as (IMAGE_ID, POINT2D_IDX)\n",
|
|
104
|
+
"# Number of points: 0\n",
|
|
105
|
+
]
|
|
106
|
+
path.write_text("".join(lines), encoding="utf-8")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def convert_fixed_rig_to_colmap4d(
|
|
110
|
+
calibration_path: Path,
|
|
111
|
+
images: list[TimestampedImage],
|
|
112
|
+
output_dir: Path,
|
|
113
|
+
clock_domain: str = "utc_ntp",
|
|
114
|
+
) -> None:
|
|
115
|
+
"""Convert fixed-rig captures to colmap4d using pre-calibrated poses.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
calibration_path: Path to rig calibration JSON
|
|
119
|
+
images: List of timestamped images
|
|
120
|
+
output_dir: Output directory for colmap4d model
|
|
121
|
+
clock_domain: Time synchronization method (e.g., "utc_ntp")
|
|
122
|
+
"""
|
|
123
|
+
# Load calibration
|
|
124
|
+
calibration = RigCalibration.load(calibration_path)
|
|
125
|
+
print(f"📐 Loaded rig calibration: {calibration.rig_id}")
|
|
126
|
+
print(f" Calibrated: {calibration.calibrated_at}")
|
|
127
|
+
print(f" Cameras: {len(calibration.cameras)}")
|
|
128
|
+
|
|
129
|
+
# Validate images match calibration
|
|
130
|
+
image_cameras = {img.camera_name for img in images}
|
|
131
|
+
calib_cameras = set(calibration.cameras.keys())
|
|
132
|
+
missing = calib_cameras - image_cameras
|
|
133
|
+
if missing:
|
|
134
|
+
print(f"⚠️ Warning: calibration has cameras not in images: {missing}")
|
|
135
|
+
|
|
136
|
+
extra = image_cameras - calib_cameras
|
|
137
|
+
if extra:
|
|
138
|
+
raise ValueError(f"Images contain cameras not in calibration: {extra}")
|
|
139
|
+
|
|
140
|
+
# Create output directory
|
|
141
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
142
|
+
|
|
143
|
+
# Write COLMAP base model with fixed poses (binary format to handle empty POINTS2D)
|
|
144
|
+
print(f"\n📝 Writing COLMAP model to {output_dir}...")
|
|
145
|
+
camera_id_map = _write_cameras_txt(output_dir / "cameras.txt", calibration)
|
|
146
|
+
|
|
147
|
+
# Build images using colmap_io structures
|
|
148
|
+
from colmap4d.colmap_io import Image as ColmapImage
|
|
149
|
+
from colmap4d.colmap_io import write_images_bin
|
|
150
|
+
|
|
151
|
+
colmap_images = {}
|
|
152
|
+
times_dict = {}
|
|
153
|
+
image_id = 1
|
|
154
|
+
|
|
155
|
+
for img in sorted(images, key=lambda x: (x.frame_index, x.camera_name)):
|
|
156
|
+
cam_name = img.camera_name
|
|
157
|
+
if cam_name not in calibration.cameras:
|
|
158
|
+
raise ValueError(f"Camera {cam_name} not in rig calibration")
|
|
159
|
+
|
|
160
|
+
cam = calibration.cameras[cam_name]
|
|
161
|
+
ext = cam.extrinsics
|
|
162
|
+
cam_id = camera_id_map[cam_name]
|
|
163
|
+
image_name = f"frame_{img.frame_index:04d}/{cam_name}.jpg"
|
|
164
|
+
|
|
165
|
+
# ColmapImage with empty POINTS2D
|
|
166
|
+
colmap_images[image_id] = ColmapImage(
|
|
167
|
+
id=image_id,
|
|
168
|
+
name=image_name,
|
|
169
|
+
camera_id=cam_id,
|
|
170
|
+
qvec=[ext.qw, ext.qx, ext.qy, ext.qz],
|
|
171
|
+
tvec=[ext.tx, ext.ty, ext.tz],
|
|
172
|
+
xys=[],
|
|
173
|
+
point3D_ids=[],
|
|
174
|
+
)
|
|
175
|
+
times_dict[image_id] = img.timestamp_ns
|
|
176
|
+
image_id += 1
|
|
177
|
+
|
|
178
|
+
write_images_bin(output_dir / "images.bin", colmap_images)
|
|
179
|
+
image_times = times_dict
|
|
180
|
+
|
|
181
|
+
_write_points3d_txt(output_dir / "points3D.txt")
|
|
182
|
+
|
|
183
|
+
# Write colmap4d sidecars
|
|
184
|
+
print("📝 Writing colmap4d sidecars...")
|
|
185
|
+
|
|
186
|
+
# times.txt
|
|
187
|
+
sidecar.write_times_txt(output_dir / "times.txt", image_times)
|
|
188
|
+
|
|
189
|
+
# time_meta.json
|
|
190
|
+
meta = {
|
|
191
|
+
"colmap4d_spec": "1.0",
|
|
192
|
+
"time_convention": "mid_exposure",
|
|
193
|
+
"clock_domain": clock_domain,
|
|
194
|
+
"rig_id": calibration.rig_id,
|
|
195
|
+
"rig_calibration_source": str(calibration_path),
|
|
196
|
+
"note": "Fixed rig - poses from pre-calibration, not SfM",
|
|
197
|
+
}
|
|
198
|
+
sidecar.write_time_meta(output_dir / "time_meta.json", meta)
|
|
199
|
+
|
|
200
|
+
print("\n✅ Conversion complete!")
|
|
201
|
+
print(f" Output: {output_dir}")
|
|
202
|
+
print(f" Images: {len(images)}")
|
|
203
|
+
print(f" Frames: {len(set(img.frame_index for img in images))}")
|
|
204
|
+
print(f" Cameras per frame: {len(image_cameras)}")
|