colmap4d 1.0.2__tar.gz → 1.0.3__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/src/colmap4d.egg-info → colmap4d-1.0.3}/PKG-INFO +1 -1
- {colmap4d-1.0.2 → colmap4d-1.0.3}/pyproject.toml +1 -1
- {colmap4d-1.0.2 → colmap4d-1.0.3}/src/colmap4d/__init__.py +1 -1
- {colmap4d-1.0.2 → colmap4d-1.0.3}/src/colmap4d/colmap_io.py +128 -35
- {colmap4d-1.0.2 → colmap4d-1.0.3}/src/colmap4d/model.py +30 -8
- {colmap4d-1.0.2 → colmap4d-1.0.3/src/colmap4d.egg-info}/PKG-INFO +1 -1
- {colmap4d-1.0.2 → colmap4d-1.0.3}/LICENSE +0 -0
- {colmap4d-1.0.2 → colmap4d-1.0.3}/README.md +0 -0
- {colmap4d-1.0.2 → colmap4d-1.0.3}/setup.cfg +0 -0
- {colmap4d-1.0.2 → colmap4d-1.0.3}/src/colmap4d/convert/__init__.py +0 -0
- {colmap4d-1.0.2 → colmap4d-1.0.3}/src/colmap4d/convert/fixed_rig.py +0 -0
- {colmap4d-1.0.2 → colmap4d-1.0.3}/src/colmap4d/convert/per_frame_colmap.py +0 -0
- {colmap4d-1.0.2 → colmap4d-1.0.3}/src/colmap4d/groups.py +0 -0
- {colmap4d-1.0.2 → colmap4d-1.0.3}/src/colmap4d/rig_calibration.py +0 -0
- {colmap4d-1.0.2 → colmap4d-1.0.3}/src/colmap4d/sidecar.py +0 -0
- {colmap4d-1.0.2 → colmap4d-1.0.3}/src/colmap4d/validate.py +0 -0
- {colmap4d-1.0.2 → colmap4d-1.0.3}/src/colmap4d.egg-info/SOURCES.txt +0 -0
- {colmap4d-1.0.2 → colmap4d-1.0.3}/src/colmap4d.egg-info/dependency_links.txt +0 -0
- {colmap4d-1.0.2 → colmap4d-1.0.3}/src/colmap4d.egg-info/requires.txt +0 -0
- {colmap4d-1.0.2 → colmap4d-1.0.3}/src/colmap4d.egg-info/top_level.txt +0 -0
|
@@ -12,11 +12,20 @@ Binary layout follows COLMAP's ``reconstruction_io`` (little-endian, count-prefi
|
|
|
12
12
|
name\\0, u64 n2d, [f64 x, f64 y, u64 point3D_id] * n2d]
|
|
13
13
|
points3D.bin : u64 count, then [u64 id, f64 x y z, u8 r g b, f64 error,
|
|
14
14
|
u64 track_len, [u32 image_id, u32 point2D_idx] * track_len]
|
|
15
|
+
|
|
16
|
+
The binary readers decode each file from one in-memory buffer, slicing the fixed-size runs
|
|
17
|
+
(2D observations, tracks) in bulk instead of one ``struct`` call per element. Callers that only
|
|
18
|
+
need poses / point positions can skip the per-observation data with ``include_observations`` /
|
|
19
|
+
``include_tracks``; the skipped fields are then ``None`` rather than lists.
|
|
15
20
|
"""
|
|
16
21
|
|
|
17
22
|
from __future__ import annotations
|
|
18
23
|
|
|
24
|
+
import gc
|
|
19
25
|
import struct
|
|
26
|
+
import sys
|
|
27
|
+
from collections.abc import Iterator
|
|
28
|
+
from contextlib import contextmanager
|
|
20
29
|
from dataclasses import dataclass, field
|
|
21
30
|
from pathlib import Path
|
|
22
31
|
|
|
@@ -57,8 +66,9 @@ class Image:
|
|
|
57
66
|
camera_id: int
|
|
58
67
|
qvec: list[float] # [qw, qx, qy, qz]
|
|
59
68
|
tvec: list[float] # [tx, ty, tz]
|
|
60
|
-
|
|
61
|
-
|
|
69
|
+
# Both are None when read with include_observations=False.
|
|
70
|
+
xys: list[tuple[float, float]] | None = field(default_factory=list)
|
|
71
|
+
point3D_ids: list[int] | None = field(default_factory=list) # INVALID_POINT3D_ID where none
|
|
62
72
|
|
|
63
73
|
|
|
64
74
|
@dataclass
|
|
@@ -67,7 +77,7 @@ class Point3D:
|
|
|
67
77
|
xyz: list[float]
|
|
68
78
|
rgb: list[int]
|
|
69
79
|
error: float
|
|
70
|
-
track: list[tuple[int, int]] # (image_id, point2D_idx)
|
|
80
|
+
track: list[tuple[int, int]] | None # (image_id, point2D_idx); None if not read
|
|
71
81
|
|
|
72
82
|
|
|
73
83
|
@dataclass
|
|
@@ -95,6 +105,53 @@ def _w(f, fmt: str, *vals) -> None:
|
|
|
95
105
|
f.write(struct.pack("<" + fmt, *vals))
|
|
96
106
|
|
|
97
107
|
|
|
108
|
+
_U64 = struct.Struct("<Q")
|
|
109
|
+
_IMAGE_HEAD = struct.Struct("<IdddddddI")
|
|
110
|
+
_OBSERVATION = struct.Struct("<ddQ")
|
|
111
|
+
_POINT_HEAD = struct.Struct("<QdddBBBdQ")
|
|
112
|
+
_TRACK_ELEM = struct.Struct("<II")
|
|
113
|
+
# memoryview.cast decodes in native byte order, which matches the file only on little-endian.
|
|
114
|
+
_NATIVE_LITTLE_ENDIAN = sys.byteorder == "little"
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@contextmanager
|
|
118
|
+
def _gc_paused() -> Iterator[None]:
|
|
119
|
+
"""Pause the cyclic GC while building millions of acyclic records; repeated collections
|
|
120
|
+
over the growing heap otherwise cost more than the parsing itself."""
|
|
121
|
+
was_enabled = gc.isenabled()
|
|
122
|
+
gc.disable()
|
|
123
|
+
try:
|
|
124
|
+
yield
|
|
125
|
+
finally:
|
|
126
|
+
if was_enabled:
|
|
127
|
+
gc.enable()
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _span(data: bytes, off: int, size: int) -> int:
|
|
131
|
+
"""End offset of ``size`` bytes at ``off``; raise like a short ``struct`` read if past EOF."""
|
|
132
|
+
end = off + size
|
|
133
|
+
if end > len(data):
|
|
134
|
+
raise struct.error(f"unpack requires a buffer of {size} bytes")
|
|
135
|
+
return end
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _observations(mv: memoryview, off: int, n2d: int) -> tuple[list, list]:
|
|
139
|
+
"""``(xys, point3D_ids)`` from ``n2d`` packed ``[f64 x, f64 y, u64 id]`` records."""
|
|
140
|
+
block = mv[off : off + n2d * _OBSERVATION.size]
|
|
141
|
+
if _NATIVE_LITTLE_ENDIAN:
|
|
142
|
+
coords = block.cast("d")
|
|
143
|
+
xys = list(zip(coords[0::3].tolist(), coords[1::3].tolist()))
|
|
144
|
+
return xys, block.cast("Q")[2::3].tolist()
|
|
145
|
+
rows = list(_OBSERVATION.iter_unpack(block))
|
|
146
|
+
return [(x, y) for x, y, _ in rows], [pid for _, _, pid in rows]
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _require_loaded(value, what: str, record_id: int):
|
|
150
|
+
if value is None:
|
|
151
|
+
raise ValueError(f"{what} {record_id} was read without that data; cannot write it")
|
|
152
|
+
return value
|
|
153
|
+
|
|
154
|
+
|
|
98
155
|
# --------------------------------------------------------------------------- #
|
|
99
156
|
# text parsing helpers
|
|
100
157
|
# --------------------------------------------------------------------------- #
|
|
@@ -144,7 +201,7 @@ def write_cameras_bin(path: str | Path, cams: dict[int, Camera]) -> None:
|
|
|
144
201
|
# --------------------------------------------------------------------------- #
|
|
145
202
|
# images
|
|
146
203
|
# --------------------------------------------------------------------------- #
|
|
147
|
-
def read_images_txt(path: str | Path) -> dict[int, Image]:
|
|
204
|
+
def read_images_txt(path: str | Path, *, include_observations: bool = True) -> dict[int, Image]:
|
|
148
205
|
lines = _data_lines(path)
|
|
149
206
|
images = {}
|
|
150
207
|
for i in range(0, len(lines), 2):
|
|
@@ -154,6 +211,9 @@ def read_images_txt(path: str | Path) -> dict[int, Image]:
|
|
|
154
211
|
tvec = [float(x) for x in head[5:8]]
|
|
155
212
|
cam_id = int(head[8])
|
|
156
213
|
name = head[9]
|
|
214
|
+
if not include_observations:
|
|
215
|
+
images[iid] = Image(iid, name, cam_id, qvec, tvec, None, None)
|
|
216
|
+
continue
|
|
157
217
|
xys, ids = [], []
|
|
158
218
|
pts = lines[i + 1].split() if i + 1 < len(lines) else []
|
|
159
219
|
for j in range(0, len(pts), 3):
|
|
@@ -164,24 +224,29 @@ def read_images_txt(path: str | Path) -> dict[int, Image]:
|
|
|
164
224
|
return images
|
|
165
225
|
|
|
166
226
|
|
|
167
|
-
def read_images_bin(path: str | Path) -> dict[int, Image]:
|
|
227
|
+
def read_images_bin(path: str | Path, *, include_observations: bool = True) -> dict[int, Image]:
|
|
228
|
+
"""Read images.bin. With ``include_observations=False`` the 2D observations are skipped
|
|
229
|
+
and ``xys`` / ``point3D_ids`` are ``None``."""
|
|
230
|
+
data = Path(path).read_bytes()
|
|
231
|
+
mv = memoryview(data)
|
|
168
232
|
images = {}
|
|
169
|
-
|
|
170
|
-
|
|
233
|
+
(n,) = _U64.unpack_from(data, 0)
|
|
234
|
+
off = _U64.size
|
|
235
|
+
with _gc_paused():
|
|
171
236
|
for _ in range(n):
|
|
172
|
-
iid, qw, qx, qy, qz, tx, ty, tz, cam_id =
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
)
|
|
237
|
+
iid, qw, qx, qy, qz, tx, ty, tz, cam_id = _IMAGE_HEAD.unpack_from(data, off)
|
|
238
|
+
off += _IMAGE_HEAD.size
|
|
239
|
+
name_end = data.find(b"\x00", off)
|
|
240
|
+
if name_end < 0:
|
|
241
|
+
name_end = len(data)
|
|
242
|
+
name = data[off:name_end].decode("utf-8")
|
|
243
|
+
off = name_end + 1
|
|
244
|
+
(n2d,) = _U64.unpack_from(data, off)
|
|
245
|
+
off += _U64.size
|
|
246
|
+
end = _span(data, off, n2d * _OBSERVATION.size)
|
|
247
|
+
xys, ids = _observations(mv, off, n2d) if include_observations else (None, None)
|
|
248
|
+
off = end
|
|
249
|
+
images[iid] = Image(iid, name, cam_id, [qw, qx, qy, qz], [tx, ty, tz], xys, ids)
|
|
185
250
|
return images
|
|
186
251
|
|
|
187
252
|
|
|
@@ -193,15 +258,16 @@ def write_images_bin(path: str | Path, images: dict[int, Image]) -> None:
|
|
|
193
258
|
_w(f, "Iddddddd", iid, *im.qvec, *im.tvec)
|
|
194
259
|
_w(f, "I", im.camera_id)
|
|
195
260
|
f.write(im.name.encode("utf-8") + b"\x00")
|
|
196
|
-
|
|
197
|
-
|
|
261
|
+
xys = _require_loaded(im.xys, "image", iid)
|
|
262
|
+
_w(f, "Q", len(xys))
|
|
263
|
+
for (x, y), pid in zip(xys, im.point3D_ids):
|
|
198
264
|
_w(f, "ddQ", x, y, pid)
|
|
199
265
|
|
|
200
266
|
|
|
201
267
|
# --------------------------------------------------------------------------- #
|
|
202
268
|
# points3D
|
|
203
269
|
# --------------------------------------------------------------------------- #
|
|
204
|
-
def read_points3D_txt(path: str | Path) -> dict[int, Point3D]:
|
|
270
|
+
def read_points3D_txt(path: str | Path, *, include_tracks: bool = True) -> dict[int, Point3D]:
|
|
205
271
|
pts = {}
|
|
206
272
|
for line in _data_lines(path):
|
|
207
273
|
p = line.split()
|
|
@@ -210,18 +276,36 @@ def read_points3D_txt(path: str | Path) -> dict[int, Point3D]:
|
|
|
210
276
|
rgb = [int(x) for x in p[4:7]]
|
|
211
277
|
error = float(p[7])
|
|
212
278
|
rest = p[8:]
|
|
213
|
-
track =
|
|
279
|
+
track = (
|
|
280
|
+
[(int(rest[k]), int(rest[k + 1])) for k in range(0, len(rest), 2)]
|
|
281
|
+
if include_tracks
|
|
282
|
+
else None
|
|
283
|
+
)
|
|
214
284
|
pts[pid] = Point3D(pid, xyz, rgb, error, track)
|
|
215
285
|
return pts
|
|
216
286
|
|
|
217
287
|
|
|
218
|
-
def read_points3D_bin(path: str | Path) -> dict[int, Point3D]:
|
|
288
|
+
def read_points3D_bin(path: str | Path, *, include_tracks: bool = True) -> dict[int, Point3D]:
|
|
289
|
+
"""Read points3D.bin. With ``include_tracks=False`` the tracks are skipped and
|
|
290
|
+
``track`` is ``None``."""
|
|
291
|
+
data = Path(path).read_bytes()
|
|
292
|
+
mv = memoryview(data)
|
|
293
|
+
head = _POINT_HEAD.unpack_from
|
|
294
|
+
head_size = _POINT_HEAD.size
|
|
295
|
+
track_rows = _TRACK_ELEM.iter_unpack
|
|
296
|
+
size = len(data)
|
|
219
297
|
pts = {}
|
|
220
|
-
|
|
221
|
-
|
|
298
|
+
(n,) = _U64.unpack_from(data, 0)
|
|
299
|
+
off = _U64.size
|
|
300
|
+
with _gc_paused():
|
|
222
301
|
for _ in range(n):
|
|
223
|
-
pid, x, y, z, r, g, b, error, tl =
|
|
224
|
-
|
|
302
|
+
pid, x, y, z, r, g, b, error, tl = head(data, off)
|
|
303
|
+
off += head_size
|
|
304
|
+
end = off + tl * 8 # u32 image_id + u32 point2D_idx; inline: hot loop
|
|
305
|
+
if end > size:
|
|
306
|
+
_span(data, off, tl * 8)
|
|
307
|
+
track = list(track_rows(mv[off:end])) if include_tracks else None
|
|
308
|
+
off = end
|
|
225
309
|
pts[pid] = Point3D(pid, [x, y, z], [r, g, b], error, track)
|
|
226
310
|
return pts
|
|
227
311
|
|
|
@@ -231,17 +315,24 @@ def write_points3D_bin(path: str | Path, pts: dict[int, Point3D]) -> None:
|
|
|
231
315
|
_w(f, "Q", len(pts))
|
|
232
316
|
for pid in sorted(pts):
|
|
233
317
|
p = pts[pid]
|
|
234
|
-
|
|
235
|
-
|
|
318
|
+
track = _require_loaded(p.track, "point3D", pid)
|
|
319
|
+
_w(f, "QdddBBBdQ", pid, *p.xyz, *p.rgb, p.error, len(track))
|
|
320
|
+
for img_id, idx in track:
|
|
236
321
|
_w(f, "II", img_id, idx)
|
|
237
322
|
|
|
238
323
|
|
|
239
324
|
# --------------------------------------------------------------------------- #
|
|
240
325
|
# whole model
|
|
241
326
|
# --------------------------------------------------------------------------- #
|
|
242
|
-
def read_model(
|
|
327
|
+
def read_model(
|
|
328
|
+
model_dir: str | Path, *, include_observations: bool = True, include_tracks: bool = True
|
|
329
|
+
) -> BaseModel:
|
|
243
330
|
"""Read cameras/images/points3D from a COLMAP model dir. Binary is preferred over text
|
|
244
|
-
when both are present (matching COLMAP).
|
|
331
|
+
when both are present (matching COLMAP).
|
|
332
|
+
|
|
333
|
+
``include_observations=False`` skips the per-image 2D observations and
|
|
334
|
+
``include_tracks=False`` the per-point tracks (those fields are then ``None``); use them
|
|
335
|
+
when only poses, point positions and ids are needed — they dominate time and memory."""
|
|
245
336
|
d = Path(model_dir)
|
|
246
337
|
|
|
247
338
|
def pick(stem: str):
|
|
@@ -254,8 +345,10 @@ def read_model(model_dir: str | Path) -> BaseModel:
|
|
|
254
345
|
cpath, ck = pick("cameras")
|
|
255
346
|
ipath, ik = pick("images")
|
|
256
347
|
ppath, pk = pick("points3D")
|
|
348
|
+
read_images = read_images_bin if ik == "bin" else read_images_txt
|
|
349
|
+
read_points = read_points3D_bin if pk == "bin" else read_points3D_txt
|
|
257
350
|
return BaseModel(
|
|
258
351
|
cameras=read_cameras_bin(cpath) if ck == "bin" else read_cameras_txt(cpath),
|
|
259
|
-
images=
|
|
260
|
-
points3D=
|
|
352
|
+
images=read_images(ipath, include_observations=include_observations),
|
|
353
|
+
points3D=read_points(ppath, include_tracks=include_tracks),
|
|
261
354
|
)
|
|
@@ -25,9 +25,17 @@ from colmap4d.colmap_io import BaseModel
|
|
|
25
25
|
from colmap4d.sidecar import TIMELESS, Sidecars
|
|
26
26
|
|
|
27
27
|
|
|
28
|
-
def read_base_model(
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
def read_base_model(
|
|
29
|
+
model_dir: str | Path, *, include_observations: bool = True, include_tracks: bool = True
|
|
30
|
+
) -> BaseModel:
|
|
31
|
+
"""Read the COLMAP base model with the zero-dependency pure-Python reader.
|
|
32
|
+
|
|
33
|
+
Pass ``include_observations=False`` / ``include_tracks=False`` to skip the 2D observations /
|
|
34
|
+
point tracks when only poses, point positions and ids are needed (see
|
|
35
|
+
:func:`colmap4d.colmap_io.read_model`)."""
|
|
36
|
+
return colmap_io.read_model(
|
|
37
|
+
model_dir, include_observations=include_observations, include_tracks=include_tracks
|
|
38
|
+
)
|
|
31
39
|
|
|
32
40
|
|
|
33
41
|
def read_reconstruction(model_dir: str | Path) -> Any:
|
|
@@ -97,15 +105,29 @@ class ModelView:
|
|
|
97
105
|
return min(vals) if vals else None
|
|
98
106
|
|
|
99
107
|
|
|
100
|
-
def load_model_view(
|
|
101
|
-
|
|
102
|
-
|
|
108
|
+
def load_model_view(
|
|
109
|
+
model_dir: str | Path,
|
|
110
|
+
strict: bool = False,
|
|
111
|
+
*,
|
|
112
|
+
include_observations: bool = True,
|
|
113
|
+
include_tracks: bool = True,
|
|
114
|
+
) -> ModelView:
|
|
115
|
+
"""Read the COLMAP model (zero-dep) + sidecars and return a joined :class:`ModelView`.
|
|
116
|
+
|
|
117
|
+
``include_observations`` / ``include_tracks`` are forwarded to :func:`read_base_model`."""
|
|
118
|
+
base = read_base_model(
|
|
119
|
+
model_dir, include_observations=include_observations, include_tracks=include_tracks
|
|
120
|
+
)
|
|
103
121
|
sc = sidecar.load_sidecars(model_dir, strict=strict)
|
|
104
122
|
return ModelView(base, sc)
|
|
105
123
|
|
|
106
124
|
|
|
107
125
|
def validate_full(model_dir: str | Path) -> list[validate.Problem]:
|
|
108
126
|
"""Run the complete validate suite, wiring the model's id sets (read zero-dep) so the
|
|
109
|
-
dangling-id checks (Part I.D) actually fire. Compute status with ``validate.exit_code``.
|
|
110
|
-
|
|
127
|
+
dangling-id checks (Part I.D) actually fire. Compute status with ``validate.exit_code``.
|
|
128
|
+
|
|
129
|
+
Only the id sets are needed, so observations and tracks are not read. A caller that
|
|
130
|
+
already holds a :class:`ModelView` can call ``validate.validate`` with its id sets instead
|
|
131
|
+
of re-reading the model."""
|
|
132
|
+
base = read_base_model(model_dir, include_observations=False, include_tracks=False)
|
|
111
133
|
return validate.validate(model_dir, image_ids=base.image_ids(), point_ids=base.point_ids())
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|