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.
@@ -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
+ [![PyPI](https://img.shields.io/pypi/v/colmap4d)](https://pypi.org/project/colmap4d/)
36
+ [![CI](https://github.com/Kiri-Innovation/colmap4d/actions/workflows/ci.yml/badge.svg)](https://github.com/Kiri-Innovation/colmap4d/actions/workflows/ci.yml)
37
+ [![spec v1.0 frozen](https://img.shields.io/badge/spec-v1.0%20frozen-blue)](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) -->
@@ -0,0 +1,64 @@
1
+ # colmap4d
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/colmap4d)](https://pypi.org/project/colmap4d/)
4
+ [![CI](https://github.com/Kiri-Innovation/colmap4d/actions/workflows/ci.yml/badge.svg)](https://github.com/Kiri-Innovation/colmap4d/actions/workflows/ci.yml)
5
+ [![spec v1.0 frozen](https://img.shields.io/badge/spec-v1.0%20frozen-blue)](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) -->
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "colmap4d"
7
- version = "1.0.1"
7
+ version = "1.0.2"
8
8
  description = "Standard COLMAP sparse model + per-image and per-point timestamps (x, y, z, t)."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -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.1"
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)}")