texture3d 0.2.0__py3-none-any.whl

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,362 @@
1
+ Metadata-Version: 2.4
2
+ Name: texture3d
3
+ Version: 0.2.0
4
+ Summary: Texture a triangle mesh from posed RGB-D frames: one keyframe per face with seam leveling
5
+ Author: Sai Kiran
6
+ License: MIT
7
+ Keywords: texturing,rgbd,mesh,texture-mapping,3d-reconstruction
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3 :: Only
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Topic :: Multimedia :: Graphics :: 3D Modeling
12
+ Classifier: Topic :: Scientific/Engineering :: Image Processing
13
+ Requires-Python: >=3.9
14
+ Description-Content-Type: text/markdown
15
+ Requires-Dist: numpy>=1.24
16
+ Requires-Dist: scipy>=1.10
17
+ Requires-Dist: opencv-python>=4.5
18
+ Requires-Dist: trimesh>=4.0
19
+ Requires-Dist: pillow>=9.0
20
+ Provides-Extra: dev
21
+ Requires-Dist: pytest>=7; extra == "dev"
22
+ Requires-Dist: ruff>=0.4; extra == "dev"
23
+
24
+ # texture3d
25
+
26
+ Texture an aligned triangle mesh from posed RGB-D frames — one photograph per face, seams levelled, one atlas out.
27
+
28
+ [![Python 3.9+](https://img.shields.io/badge/python-3.9%2B-blue.svg)](#installation)
29
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](#license)
30
+ [![Version 0.2.0](https://img.shields.io/badge/version-0.2.0-informational.svg)](https://github.com/saikiran321/texture3d)
31
+
32
+ Not on PyPI yet. Install from this repo. The texturing library is trimesh-based and does not depend on Open3D; reconstruction (optional, same repo) does.
33
+
34
+ ![A grey untextured scan of a room, plus three posed photographs of the same room, equals the scan rendered with the photographs applied as a texture](docs/figures/purpose.png)
35
+
36
+ *A scanner gives you geometry; a camera gives you posed photos. `texture3d` puts each photo onto the faces it sees best, levels the seams, and packs one atlas. `apt0_seq0`: 977k faces, 1000 frames, 58 keyframes, ~2 min on a laptop.*
37
+
38
+ ## Quick start
39
+
40
+ ```bash
41
+ python3 -m venv .venv
42
+ .venv/bin/pip install -e ".[dev]"
43
+ ```
44
+
45
+ ```python
46
+ import texture3d
47
+
48
+ frames = texture3d.load_dataset("path/to/dataset") # directory with dataset.json
49
+ result = texture3d.bake("mesh.ply", frames)
50
+ result.export("textured_mesh.obj") # or .glb
51
+ print(result.stats)
52
+ ```
53
+
54
+ ```bash
55
+ .venv/bin/texture3d path/to/dataset --mesh mesh.ply --out textured/run
56
+ ```
57
+
58
+ You need a triangle mesh in the same world frame as the camera poses, plus RGB-D frames. See [Dataset layout](#dataset-layout). Without a `dataset.json` folder, build `RGBDFrame` objects yourself ([Usage](#usage)).
59
+
60
+ ## Table of contents
61
+
62
+ - [Why](#why)
63
+ - [Features](#features)
64
+ - [Installation](#installation)
65
+ - [Usage](#usage)
66
+ - [CLI](#cli)
67
+ - [Configuration](#configuration)
68
+ - [How it works](#how-it-works)
69
+ - [RGB-D reconstruction](#rgb-d-reconstruction)
70
+ - [Dataset layout](#dataset-layout)
71
+ - [Testing](#testing)
72
+ - [Limitations](#limitations)
73
+ - [License](#license)
74
+
75
+ ## Why
76
+
77
+ Averaging every view onto a texel blurs photograph detail as soon as poses or geometry are off by a few millimetres. **texture3d** assigns each face one keyframe (the view that sees it best), cuts connected same-view faces out as patches, then hides the seams with a global colour-offset solve and a local Poisson blend. The result is a single-material textured mesh (OBJ or GLB). Input UVs are ignored; output UVs follow the labelling, with vertices split along patch borders.
78
+
79
+ ![Three views of the same stairs: the photograph, the reconstruction's per-vertex colour averaged over all frames, and the texture3d result; zoomed crops show the averaged version is blurred while texture3d keeps the carpet weave and shoe detail](docs/figures/why.png)
80
+
81
+ *Left: a captured frame. Middle: the TSDF reconstruction's per-vertex colour, i.e. every frame averaged — the carpet weave and the shoes smear. Right: `texture3d`, one photograph per face. Bottom row is the boxed region at native resolution.*
82
+
83
+ This repo also ships an Open3D TSDF reconstruction pipeline for turning a `dataset.json` sequence into `mesh.ply`. You can texture that mesh, or an untextured STL/OBJ from another engine, as long as it is aligned to the same poses.
84
+
85
+ **Status:** 0.2.0 — end-to-end usable, public API may still change.
86
+
87
+ ## Features
88
+
89
+ - **One keyframe per face** — sharpness-weighted view scoring, outlier rejection on mean face colour, MRF labelling with Potts smoothness
90
+ - **Seam leveling** — global least-squares offsets in YCbCr, then a Poisson blend along a configurable border strip
91
+ - **Single atlas** — MaxRects packing (shelf packer above 4,000 patches), optional unsharp mask
92
+ - **Unseen-face fill** — sub-pixel faces inherit a neighbour's view instead of speckling the empty colour
93
+ - **Dataset-agnostic inputs** — `RGBDFrame` + `PinholeCamera`, or `load_dataset()` for the `dataset.json` layout
94
+ - **OBJ and GLB export** via trimesh; `result.stats` reports coverage and per-stage timings
95
+
96
+ ## Installation
97
+
98
+ **Prerequisites:** Python 3.9 or newer. Texturing needs NumPy, SciPy, OpenCV, trimesh, and Pillow (pulled in by the package). Reconstruction additionally needs Open3D 0.18 and NumPy 1.x (`requirements.txt`).
99
+
100
+ ### Texturing library + CLI
101
+
102
+ ```bash
103
+ git clone https://github.com/saikiran321/texture3d.git
104
+ cd texture3d
105
+ python3 -m venv .venv
106
+ .venv/bin/pip install -e .
107
+ ```
108
+
109
+ Dev extras (pytest, ruff):
110
+
111
+ ```bash
112
+ .venv/bin/pip install -e ".[dev]"
113
+ ```
114
+
115
+ ### Reconstruction extras
116
+
117
+ ```bash
118
+ .venv/bin/pip install -r requirements.txt
119
+ ```
120
+
121
+ Open3D 0.18 is built against the NumPy 1.x ABI. Keep `numpy>=1.24,<2` in that environment; NumPy 2.x can segfault during TSDF integrate.
122
+
123
+ <!-- TODO: add a PyPI one-liner when the package is published -->
124
+
125
+ ## Usage
126
+
127
+ ### From a `dataset.json` directory
128
+
129
+ ```python
130
+ import texture3d
131
+
132
+ frames = texture3d.load_dataset("assets/datasets/apt0_seq0")
133
+ result = texture3d.bake(
134
+ "assets/reconstructions/apt0_seq0/mesh.ply",
135
+ frames,
136
+ )
137
+ result.export("textured/apt0_seq0/textured_mesh.obj")
138
+ print(result.stats["textured_fraction"], result.stats["stage_seconds"])
139
+ ```
140
+
141
+ `BakeResult` has `.mesh` (textured `trimesh.Trimesh`), `.texture` (uint8 RGB atlas), and `.stats`.
142
+
143
+ ### From frames you already have in memory
144
+
145
+ ```python
146
+ import numpy as np
147
+ import texture3d
148
+ from texture3d import BakeConfig, PinholeCamera, RGBDFrame
149
+
150
+ camera = PinholeCamera(width=640, height=480, fx=582.871, fy=582.871, cx=320.0, cy=240.0)
151
+ pose_c2w = np.eye(4) # camera-to-world, OpenCV convention (+z forward, +y down)
152
+ rgb = ... # HxWx3 uint8, RGB
153
+ depth_m = ... # HxW float32 meters, 0 = invalid
154
+
155
+ frames = [
156
+ RGBDFrame(pose_c2w=pose_c2w, camera=camera, rgb=rgb, depth=depth_m, depth_scale=1.0),
157
+ ]
158
+ cfg = BakeConfig()
159
+ cfg.keyframes.rgbd_frame_gap = 1
160
+ result = texture3d.bake("mesh.ply", frames, cfg)
161
+ result.export("textured_mesh.glb")
162
+ ```
163
+
164
+ Poses are 4×4 **camera-to-world**. Integer depth images (and integer files) are multiplied by `depth_scale` (default `1e-3` for millimetre PNGs). Float arrays are treated as metres already.
165
+
166
+ ### Tuning
167
+
168
+ ```python
169
+ from texture3d import BakeConfig
170
+
171
+ cfg = BakeConfig()
172
+ cfg.keyframes.rgbd_frame_gap = 5 # 1 keyframe per N frames
173
+ cfg.mapping.data_smoothness_ratio = 0.05 # lower → larger patches
174
+ cfg.mapping.sharpness_weight = 0.0 # disable unsharp mask
175
+ cfg.mapping.local_leveling_strip_width = 30
176
+ cfg.mapping.fill_unseen = True
177
+ ```
178
+
179
+ ## CLI
180
+
181
+ ```
182
+ texture3d DATASET_DIR --mesh mesh.ply [--out DIR] [--format obj|glb]
183
+ ```
184
+
185
+ | Flag | Default | Meaning |
186
+ | --- | --- | --- |
187
+ | `--mesh` | `reconstructions/<dataset-name>/mesh.ply` | Mesh to texture (`.ply` / `.obj` / `.stl`) |
188
+ | `--out` | `textured/<dataset-name>` | Output directory |
189
+ | `--format` | `obj` | `obj` or `glb` |
190
+ | `--gap` | `5` | One keyframe per N frames (`1` = all) |
191
+ | `--outlier` | `0.06` | Drop views below this colour likelihood (`0` = off) |
192
+ | `--data-ratio` | `0.1` | Data cost vs smoothness; lower = larger patches |
193
+ | `--no-global-leveling` | off | Skip the global colour-offset solve |
194
+ | `--no-local-leveling` | off | Skip the Poisson border blend |
195
+ | `--strip-width` | `20` | Local leveling strip width in pixels |
196
+ | `--sharpness` | `0.5` | Unsharp-mask weight (`0` = off) |
197
+ | `--max-texture` | `8192` | Largest atlas edge in pixels |
198
+ | `--no-fill-unseen` | off | Leave sub-pixel faces in the empty colour |
199
+
200
+ Writes `textured_mesh.<format>`, `config.json`, and `summary.json`. If the default mesh path does not exist, pass `--mesh` explicitly.
201
+
202
+ ```bash
203
+ .venv/bin/texture3d assets/datasets/apt0_seq0 \
204
+ --mesh assets/reconstructions/apt0_seq0/mesh.ply \
205
+ --out textured/apt0_seq0
206
+ ```
207
+
208
+ On a 977k-face mesh with 58 keyframes (640×480, 18-core Mac) this run was about **120 s** (scoring 8 s, labelling 9 s, global leveling 15 s, local leveling 82 s, atlas 1 s), **85%** of faces textured (the rest is the TSDF back shell no camera saw), 4096 atlas.
209
+
210
+ `run_texturing.py` is a thin shim around the same CLI.
211
+
212
+ ## Configuration
213
+
214
+ `BakeConfig` / `KeyframeConfig` / `MappingConfig` (`texture3d.config`):
215
+
216
+ | Name | Type | Default | Description |
217
+ | --- | --- | --- | --- |
218
+ | `keyframes.rgbd_frame_gap` | int | `5` | Least-blurry frame in every window of this many frames |
219
+ | `keyframes.closest_pose_translation` | float | `0.05` | Skip a candidate within this many metres of the last keyframe |
220
+ | `keyframes.closest_pose_rotation_angle` | float | `0.09` | Same, for relative rotation (radians) |
221
+ | `mapping.outlier_threshold` | float | `0.06` | Gaussian colour likelihood; `0` disables |
222
+ | `mapping.data_smoothness_ratio` | float | `0.1` | Data cost scale vs Potts smoothness |
223
+ | `mapping.global_seam_leveling` | bool | `True` | Least-squares colour offsets per (vertex, patch) |
224
+ | `mapping.local_seam_leveling` | bool | `True` | Poisson blend along patch borders |
225
+ | `mapping.local_leveling_strip_width` | int | `20` | Blend strip width in pixels |
226
+ | `mapping.sharpness_weight` | float | `0.5` | Unsharp mask; `0` disables |
227
+ | `mapping.max_texture_size` | int | `8192` | Max atlas edge (power of two) |
228
+ | `mapping.empty_color` | RGB tuple | `(255, 255, 255)` | Texels no patch covers |
229
+ | `mapping.scoring_workers` | int | `4` | Views scored in parallel |
230
+ | `mapping.fill_unseen` | bool | `True` | Neighbour view for sub-pixel faces |
231
+ | `mapping.exact_packing_limit` | int | `4000` | MaxRects up to this many patches; shelf packer beyond. `0` = always MaxRects |
232
+
233
+ ## How it works
234
+
235
+ ```mermaid
236
+ flowchart LR
237
+ frames[RGB-D frames] --> kf[Keyframes]
238
+ mesh[Triangle mesh] --> topo[Topology]
239
+ kf --> score[Score views]
240
+ topo --> score
241
+ score --> label[Label faces]
242
+ label --> patches[Patches + seams]
243
+ patches --> level[Seam leveling]
244
+ level --> atlas[Pack atlas]
245
+ atlas --> out[Textured mesh]
246
+ ```
247
+
248
+ 1. **Keyframes** — least blurry frame in each `--gap` window (Crete metric); near-duplicate poses are skipped.
249
+ 2. **Topology** — incidence, edge adjacency, face normals smoothed over neighbours within 25°.
250
+ 3. **Scoring** — depth-tested face maps; quality is blurred Sobel magnitude times squared cosine to the normal. Colour outliers are dropped.
251
+ 4. **Labelling** — MRF over the face graph (views + “none”), loopy belief propagation.
252
+ 5. **Patches** — connected same-view faces, 2 px border in that view; seam vertices where patches meet.
253
+ 6. **Leveling** — global Tikhonov-regularised offsets (λ = 0.1), then a 20 px Poisson strip.
254
+ 7. **Atlas** — pack, copy levelled pixels, write per-corner UVs, optional sharpen.
255
+
256
+ ![Four panels: the per-face keyframe label map, the raw bake with visible seams, the levelled bake, and the top-left corner of the packed atlas where the largest patches (stairs, wall, door) land; a zoomed strip shows the wall mottling disappearing after leveling](docs/figures/how.png)
257
+
258
+ *Stages 4–7 on one view of `apt0_seq0`. Column 4 is the atlas itself: patches are packed largest-first, so the top-left corner holds whole surfaces (stairs, picture wall, door) at full resolution; the long tail of small patches fills the rows below. 48 % of the 4096² atlas is used.*
259
+
260
+ Faces no camera sees stay the empty colour (typical TSDF back shell). `result.stats["stage_seconds"]` times each stage.
261
+
262
+ To check a bake visually, render the textured mesh back from dataset poses and compare with the photos:
263
+
264
+ ![Posed RGB-D frames and camera trajectory go in; the mesh textured by texture3d is rendered back from the same poses](docs/figures/pipeline.png)
265
+
266
+ ```bash
267
+ # input/output check above
268
+ .venv/bin/python tools/make_readme_figure.py assets/datasets/apt0_seq0 textured/apt0_seq0 \
269
+ --frames 100,350,600,850 --out docs/figures/pipeline.png
270
+ # purpose / why / how figures used in this README
271
+ .venv/bin/python tools/make_story_figures.py assets/datasets/apt0_seq0 \
272
+ assets/reconstructions/apt0_seq0/mesh.ply textured/apt0_seq0 --out docs/figures
273
+ ```
274
+
275
+ Package layout: `src/texture3d/` is the installable library (`bake`, `camera`, `io`, `mapping/`). `rgbd_recon/` is reconstruction only.
276
+
277
+ ## RGB-D reconstruction
278
+
279
+ Optional. Fuses a `dataset.json` sequence with Open3D TSDF (OpenCV depth cleanup first).
280
+
281
+ ```bash
282
+ .venv/bin/pip install -r requirements.txt
283
+ .venv/bin/python run_reconstruction.py assets/datasets/apt0_seq0
284
+ .venv/bin/python run_reconstruction.py assets/datasets/apt0_seq0 --stride 3 --voxel 0.02
285
+ ```
286
+
287
+ Writes to `reconstructions/<name>/` by default: `mesh.ply`, `point_cloud.ply`, `trajectory_tum.txt`, `config.json`, `summary.json`.
288
+
289
+ `create_rgbd_dataset.py` cuts contiguous windows from a BundleFusion-style capture into that layout. Three `apt0` windows used while developing:
290
+
291
+ | Subset | Source frames |
292
+ | --- | --- |
293
+ | `apt0_seq0` | 0–999 |
294
+ | `apt0_seq1` | 3780–4779 |
295
+ | `apt0_seq2` | 7560–8559 |
296
+
297
+ Those sequences are not in the git tree (`assets/` is gitignored). Point `--output-root` / dataset paths at your own copy.
298
+
299
+ ## Dataset layout
300
+
301
+ `texture3d.load_dataset(dir)` expects `dir/dataset.json`:
302
+
303
+ ```json
304
+ {
305
+ "image_size": {"width": 640, "height": 480},
306
+ "intrinsics": {
307
+ "color_matrix_3x3": [[fx, 0, cx], [0, fy, cy], [0, 0, 1]],
308
+ "depth_matrix_3x3": null
309
+ },
310
+ "depth": {"scale_to_meters": 0.001},
311
+ "frames": [
312
+ {"frame_id": "000000", "rgb": "000000/rgb.jpg", "depth": "000000/depth.png", "pose": "000000/pose.txt"}
313
+ ]
314
+ }
315
+ ```
316
+
317
+ Paths are relative to the dataset directory. `pose.txt` is a 4×4 camera-to-world matrix. Depth PNGs are typically uint16 millimetres.
318
+
319
+ ## Testing
320
+
321
+ ```bash
322
+ .venv/bin/pip install -e ".[dev]"
323
+ .venv/bin/python -m pytest -q
324
+ ```
325
+
326
+ 15 tests: rasterisers, labelling, atlas packing, seam-leveling helpers, and a synthetic checker-plane bake. No GPU required.
327
+
328
+ ```bash
329
+ .venv/bin/ruff check src tests
330
+ ```
331
+
332
+ ## Limitations
333
+
334
+ - Mesh and cameras must already share a world frame. This library does not estimate poses or reconstruct geometry (use `rgbd_recon` or another engine).
335
+ - Input UVs are discarded.
336
+ - Unseen faces (back of a TSDF shell, or never projected) stay `empty_color` unless `fill_unseen` can borrow a neighbour.
337
+ - Not published to PyPI; install from git.
338
+ - Measured timings are from one Mac; local leveling dominates on dense meshes.
339
+
340
+ ## Contributing
341
+
342
+ Issues and PRs are welcome.
343
+
344
+ ```bash
345
+ git clone https://github.com/saikiran321/texture3d.git
346
+ cd texture3d
347
+ python3 -m venv .venv
348
+ .venv/bin/pip install -e ".[dev]"
349
+ .venv/bin/python -m pytest -q
350
+ ```
351
+
352
+ <!-- TODO: add CONTRIBUTING.md / CODE_OF_CONDUCT.md if you want a formal process -->
353
+
354
+ ## License
355
+
356
+ MIT — see `pyproject.toml`.
357
+
358
+ <!-- TODO: add a LICENSE file at the repo root so GitHub detects the license automatically -->
359
+
360
+ —
361
+
362
+ [Sai Kiran](https://github.com/saikiran321)
@@ -0,0 +1,24 @@
1
+ texture3d/__init__.py,sha256=Wc4qxDAXbEs2c0jw-FOdMDa9i_e_KIkDYqQyni9wnFY,919
2
+ texture3d/bake.py,sha256=MrMAgK3bDvYpPE_rw-xtzQikiW7z64viLkMxEq4mOFs,6303
3
+ texture3d/blur.py,sha256=7EqjsAEtPwLcj8AEtogxkMqPsRo2O1Xla6L0JPCKiGk,1886
4
+ texture3d/camera.py,sha256=049c8pS2vfLfjkfyEpywuJ8HG7UQrm45LK1Rn6VVjbU,3556
5
+ texture3d/cli.py,sha256=KIa7GshCjwSHEwt8wiilMil5gj-S0wDqBhxDZur6Xho,3422
6
+ texture3d/config.py,sha256=bTHY_SAbzz5ujPiEGpyGUfo5yspnVy3niVnpgcdqAjg,2228
7
+ texture3d/io.py,sha256=-7lI7azLCnm3OWkWR7P3W9V8U6m1WbFyRMpptkg8tV0,1610
8
+ texture3d/keyframes.py,sha256=2yrPcUHr2QVWCIFXaaFb6ioic_pn1zpI8B8tQMTzQ5k,1458
9
+ texture3d/mapping/__init__.py,sha256=gFOQiR1K3clO1foU989UcOdPSa-ndgqOMD0OkhCk_c0,538
10
+ texture3d/mapping/atlas.py,sha256=my0KYxLJJKxzz2kpsPl2bm7hvWUwQgNUsYm4GwynbeQ,10615
11
+ texture3d/mapping/imageops.py,sha256=Ns6BsDvJW8kCfVj-2T4JC6V6uMwh9EMQGQQusNiytTQ,3191
12
+ texture3d/mapping/labeling.py,sha256=234HfhYWONvLQARisyEQdFvT7l6VGMOkX54nVtMY6lg,12577
13
+ texture3d/mapping/leveling.py,sha256=k2m3V4495BWAgnC8nAVwd0RVlHk6WuuocRj_8yi3u4k,23371
14
+ texture3d/mapping/mapper.py,sha256=lmKK36QXEhvy4ruVeMkvwsIyiujD9jMEFbVFQ9Cy1Rk,4460
15
+ texture3d/mapping/patches.py,sha256=ZEAurXcODAoscXBFDglzlZi72gEQCPT5I0HR440Jy3E,12192
16
+ texture3d/mapping/raster.py,sha256=rzOZyKuz0X8wTTpQV4wtgYckDWl7wv4sCWi_dMFLefQ,7415
17
+ texture3d/mapping/scoring.py,sha256=OfNN1fXMpBDcltxO7r1gD76u96TK56fN_wi_crgh3xI,9669
18
+ texture3d/mapping/topology.py,sha256=DGijeyLv86Q4bQJB4Zs71mOhIg5LeGW9xf2jl5yrUMk,3941
19
+ texture3d/mapping/types.py,sha256=DESKKM5lAk2bbRvDpIZsHZvFbpif7HYc2uLPpNBlNbU,5743
20
+ texture3d-0.2.0.dist-info/METADATA,sha256=gwvPUa5WVmgSEKQtw4bZgSSBgEB7mjL5ikt405iP1yc,15328
21
+ texture3d-0.2.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
22
+ texture3d-0.2.0.dist-info/entry_points.txt,sha256=jfJ4iu6MIEixjwcc0mwqAOXtO9dY9RgtNdsxpA3XbhQ,49
23
+ texture3d-0.2.0.dist-info/top_level.txt,sha256=LD-odyF5miBIo3Ct4cl_HgMiufNuiUbpJUhlqkcHTbQ,10
24
+ texture3d-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ texture3d = texture3d.cli:main
@@ -0,0 +1 @@
1
+ texture3d