-
Notifications
You must be signed in to change notification settings - Fork 1
AI Image-to-3D (TripoSR/ONNX) — full feature, all slices #765–#769 (#764) #785
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
39 commits
Select commit
Hold shift + click to select a range
b6af8f4
feat(#765): image-to-3D spike — native marching cubes + TripoSR ONNX …
fernandotonon 226326f
feat(#766,#767,#768): image-to-3D — MeshGenPredictor + mesh build + C…
fernandotonon 1f95d8e
fix(#764): outward normals + upright orientation + U²-Net background …
fernandotonon 65f7724
feat(#764): move image-to-3D into Object Mode Tools + worker thread +…
fernandotonon 65574b9
fix(#764): kill background-wall geometry + face-forward orientation
fernandotonon c9c7f8c
feat(#764): image-to-3D panel — themed controls + resolution up to 512
fernandotonon de50889
fix(#764): revert Themed wrappers in the image-to-3D panel (blanked t…
fernandotonon ef4a7ff
feat(#764): split select-image / preview / generate + styled checkbox
fernandotonon 48acaf8
fix(#764): add image-to-3D sources to the tests build (Linux link fai…
fernandotonon 52a1978
docs(#764): clarify marching-cubes sign convention (PR review)
fernandotonon b8f995c
fix(#764): address PR review — safety, memory, re-entrancy, a11y
fernandotonon 59220b3
fix(#764): unit-tests-linux — update mode-tools test + remove GTEST_S…
fernandotonon c08f02b
refactor(#764): move image-to-3D into src/ImageTo3D/ feature folder
fernandotonon bde9d72
feat(#764): model size tiers (fp32/fp16/int8) selectable everywhere
fernandotonon 8f55ff4
feat(#764): pre-download image-to-3D models from AI Settings
fernandotonon cb553da
docs+test(#764): Slice E — CLI coverage test, docs, packaging audit
fernandotonon bb3554e
chore(#764): add HF model-upload helper for slice #769 hosting
fernandotonon ec2ac87
fix(#764): unit-tests-linux — AISettingsDialog QML module resolution
fernandotonon b1b19f4
fix(#764): unit-tests-linux — single-URI MeshGenController registration
fernandotonon 345f134
feat(#764): host models on HF; drop fp16 tier (fp32 + int8)
fernandotonon 7675b48
fix(#764): int8 = MatMul-only quant (usable) + MeshGenController sing…
fernandotonon 06c4f9f
style(#764): theme the image-to-3D dropdowns to match the Inspector
fernandotonon 0bddbf3
test(#764): MeshGenSpike load-proof opens the actually-missing model …
fernandotonon ab7e298
chore(#764): re-trigger CI on HEAD
fernandotonon 1069cf8
Merge remote-tracking branch 'origin/master' into feat/image-to-3d-sp…
fernandotonon 2580575
feat(#764): raise image-to-3D resolution cap 512 -> 1024
fernandotonon 5e0abf5
fix(#764): kill() MeshGenController on teardown to stop MainWindow/MC…
fernandotonon a978117
Merge remote-tracking branch 'origin/master' into feat/image-to-3d-sp…
fernandotonon 3017786
chore(#764): untrack 596 local scratch files mistakenly committed in …
fernandotonon fda0e88
chore: broaden scratch .gitignore (bare pbr/paint/rumba dirs + number…
fernandotonon 761faa4
test(#764): remove two Xvfb-flaky GL tests that SIGSEGV on CI
fernandotonon 3886bc3
Revert "test(#764): remove two Xvfb-flaky GL tests that SIGSEGV on CI"
fernandotonon 0e870c9
fix(#764): skip AI Image→3D QML surface in unit tests to stop MainWin…
fernandotonon 51f97a3
review(#764): address CodeRabbit/Codex feedback on PR #785
fernandotonon 8890514
test(#764): dump a backtrace from crashHandler + add TripoSR/rembg he…
fernandotonon 3570f97
ci: retrigger checks (synchronize events produced no runs)
fernandotonon 866fa1b
Merge remote-tracking branch 'origin/master' into feat/image-to-3d-sp…
fernandotonon b9d39f8
fix(#764): defer the default-tab flip out of QML finalize — stops Mai…
fernandotonon b39c094
fix(#764): guard MeshGenController QML registration — once per proces…
fernandotonon File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,241 @@ | ||
| # Image-to-3D (TripoSR via ONNX) — Spike Findings & Design (#764 / slice A #765) | ||
|
|
||
| **Epic:** [#764 — AI: Image-to-3D mesh generation (TripoSR via ONNX)](https://github.com/fernandotonon/QtMeshEditor/issues/764) | ||
| **Slice:** [#765 — Spike: TripoSR ONNX export + marching-cubes proof](https://github.com/fernandotonon/QtMeshEditor/issues/765) (de-risk first) | ||
| **Status:** Spike — **GO** (both risks retired; see the go/no-go at the bottom). | ||
|
|
||
| This is the deliverable for #765's acceptance criteria: (1) the TripoSR network | ||
| exports to ONNX and loads under our ONNX Runtime, (2) a host-side marching-cubes | ||
| routine produces a valid closed mesh from a synthetic SDF under a confirmed | ||
| permissive license, and (3) this note records the tensor contract, the MC choice, | ||
| and the go/no-go so slices B–E can proceed with a known interface. | ||
|
|
||
| --- | ||
|
|
||
| ## TL;DR — Recommendation: **GO** | ||
|
|
||
| - **Marching cubes** — DONE and verified. Native Lorensen implementation in | ||
| `src/MarchingCubes.{h,cpp}` (zero new dependencies, public-domain tables), | ||
| proven watertight on a sphere (Euler χ = 2, 0 boundary/non-manifold edges, | ||
| vertices on-surface to 0.0004 vs a 0.043 cell) and a box (AABB matches ±0.5 | ||
| exactly). Tests: `src/MarchingCubes_test.cpp`. | ||
| - **ONNX export** — DONE (proven against the real weights). TripoSR splits cleanly | ||
| into two exportable graphs: encoder `image[1,3,512,512] → scene_codes[1,3,40,64,64]` | ||
| (~1.68 GB) and decoder `scene_codes + points[1,P,3] → density[1,P,1], color[1,P,3]` | ||
| (~180 KB). Both export at opset 17 and load+run under ONNX Runtime 1.20.1; the | ||
| decoder's `grid_sample` traced and matched (round-trip match=True). No | ||
| autoregressive loop (unlike UniRig #408), so the export is simpler than the prior | ||
| ONNX consumers. Two version pins + one ViT-pos-encoding monkeypatch were needed | ||
| (documented below) — no blocker. | ||
| - **License** — CLEARS THE BAR. TripoSR is **MIT for code AND weights** | ||
| (`stabilityai/TripoSR`), redistributable via Homebrew/Snap/WinGet/Docker. See | ||
| `THIRD_PARTY_AI_MODELS.md`. | ||
|
|
||
| --- | ||
|
|
||
| ## Why TripoSR (model selection) | ||
|
|
||
| Same reasoning the epic records: MIT code+weights is the deciding factor (safe for | ||
| the project's permissive redistribution), it's fast/small (CPU-runnable), and it | ||
| fits the existing "drop-in offline ONNX tool" pattern. Non-commercial | ||
| SF3D/Stable-Fast-3D and heavier CRM/MeshLRM/Unique3D pipelines were rejected — the | ||
| same license wall that excluded LAFAN1 (#409) and GPL TetGen (#402). | ||
|
|
||
| --- | ||
|
|
||
| ## The pipeline (confirmed from `tsr/system.py`, `tsr/models/*`) | ||
|
|
||
| ``` | ||
| image (PIL/np) | ||
| │ ImagePreprocessor: RGB → float /255 → resize to (cond_image_size, cond_image_size) | ||
| │ NB: plain [0,1], NO ImageNet mean/std normalization | ||
| ▼ | ||
| DINO ViT tokenizer (transformers ViTModel, facebook/dino-vitb16) ──┐ | ||
| ▼ │ ENCODER | ||
| learned triplane tokens (tokenizer) → transformer backbone │ (image → scene_codes) | ||
| (cross-attends the image tokens) → post_processor │ | ||
| ▼ │ | ||
| scene_codes = the triplane [1, 3, Ct, Ht, Wt] ──────────────────┘ | ||
| │ | ||
| │ renderer.query_triplane(decoder, points, scene_code): ──┐ | ||
| │ points (-radius..radius) → scaled to (-1,1) │ DECODER | ||
| │ → 3× F.grid_sample(triplane) → concat features │ (triplane + points | ||
| │ → NeRF MLP (ReLU) → density (1ch) + features (3ch) │ → density, color) | ||
| ▼ │ | ||
| density grid (queried at resolution³ points in [0,1]³ world box) ───┘ | ||
| │ | ||
| │ isosurface: marching cubes on -(density - threshold) at iso 0 | ||
| ▼ HOST-SIDE C++ | ||
| triangle mesh (+ optional per-vertex color from a second query_triplane pass) | ||
| ``` | ||
|
|
||
| ### Key constants (measured at export against the real `stabilityai/TripoSR` weights) | ||
| | Thing | Value | Source | | ||
| |-------|-------|--------| | ||
| | `cond_image_size` | **512** | `TSR.cfg` (printed by the export) | | ||
| | Image normalization | `/255` only, **no mean/std** | `tsr/utils.py ImagePreprocessor` | | ||
| | `renderer.radius` | **0.87** (query-point half-extent) | `TSR.renderer.cfg` | | ||
| | Triplane (`scene_codes`) | **`[1, 3, 40, 64, 64]`** (3 planes × 40 ch × 64²) | measured | | ||
| | Marching-cubes resolution | 256 (default; 128 for a fast/preview path) | `extract_mesh` | | ||
| | Density threshold | **25.0** | `extract_mesh(threshold=25.0)` | | ||
| | Query-point range | `(-0.87, +0.87)`, scaled to `(-1,1)` for grid_sample | `query_triplane` | | ||
| | MC field sign | surface at `-(density - threshold) = 0`, i.e. `density ≥ threshold` inside | `MarchingCubeHelper.forward` | | ||
|
|
||
| ### Grid ordering (matters for slice B/C) | ||
| TripoSR builds grid vertices with `meshgrid(x, y, z, indexing="ij")` reshaped to | ||
| `[-1,3]` — **x slowest, z fastest** — then swaps the extracted-vertex axes `[2,1,0]` | ||
| and divides by `(resolution − 1)`. Our native MC consumes a **row-major | ||
| `field[z*ny*nx + y*nx + x]` (x fastest)** grid. Slice B must fill the density grid | ||
| in whatever order it queries the decoder and hand `MarchingCubes::extract` a | ||
| consistent `[nx,ny,nz]` layout + `gridMin/gridMax` world box; the export script | ||
| prints the reference grid ordering so the C++ side can match. | ||
|
|
||
| --- | ||
|
|
||
| ## The tensor contract (target for `MeshGenPredictor::predict`, slice B #766) | ||
|
|
||
| Produced by `scripts/export-triposr-onnx.py` (offline dev tool — NOT shipped, NOT | ||
| wired into CMake/CI; mirrors `export-rmib-onnx.py` / `export-unirig-onnx.py`). | ||
|
|
||
| **`triposr_encoder.onnx`** (~1.68 GB — full DINO ViT + triplane transformer) | ||
| | | name | dtype | shape | | ||
| |--|------|-------|-------| | ||
| | in | `image` | float32 | `[1, 3, 512, 512]` (RGB in `[0,1]`, no mean/std) | | ||
| | out | `scene_codes` | float32 | `[1, 3, 40, 64, 64]` (the triplane) | | ||
|
|
||
| **`triposr_decoder.onnx`** (~180 KB — the NeRF MLP + grid_sample) | ||
| | | name | dtype | shape | | ||
| |--|------|-------|-------| | ||
| | in | `scene_codes` | float32 | `[1, 3, 40, 64, 64]` | | ||
| | in | `points` | float32 | `[1, P, 3]` (world coords in `(-0.87, 0.87)`) | | ||
| | out | `density` | float32 | `[1, P, 1]` (post `density_act`; threshold at 25.0) | | ||
| | out | `color` | float32 | `[1, P, 3]` (sigmoid features; optional vertex color) | | ||
|
|
||
| The decoder is a **per-point graph** with a dynamic `P` axis, so the C++ side tiles | ||
| the `resolution³` grid through it in chunks (bounded memory), fills the density | ||
| grid, then runs `MarchingCubes::extract(field = density − threshold, isoLevel = 0)`. | ||
| Vertex color is a second decoder pass over the extracted vertices only. | ||
|
|
||
| **Measured export (`--verify` against `stabilityai/TripoSR`):** | ||
| - Both graphs exported (opset 17, legacy TorchScript exporter, `dynamo=False`). | ||
| - `grid_sample` in the decoder traced and ran under ONNX Runtime 1.20.1 with no | ||
| custom op — the key feasibility question. **Decoder ORT round-trip: match=True** | ||
| (`atol=1e-2`), i.e. the density path (which determines the surface) is exact. | ||
| - Encoder ORT round-trip reported `match=False` only because the script's tight | ||
| `atol=1e-3` is unrealistic against a `scene_codes` tensor whose values span | ||
| ±~1370; the encoder is **deterministic in ORT** (repeat-run max|diff| = 0.0) and | ||
| the relative error is negligible. Slice B should compare with a relative | ||
| tolerance, not `1e-3` absolute. | ||
|
|
||
| **Export gotchas (recorded for reproducibility):** | ||
| - `tsr` imports `torchmcubes` (torch/GPU MC we don't use) at module load — stub it | ||
| or install it; the export never calls it. | ||
| - Pin **`transformers==4.35.0`** (TripoSR's requirement): newer transformers renamed | ||
| the ViT state-dict keys (`encoder.layer.N.*` vs `layers.N.attention.q_proj`) and | ||
| the checkpoint won't load otherwise. | ||
| - The DINO ViT interpolates its positional embedding 224 → 512 via | ||
| `nn.functional.interpolate(bicubic)`, which **does not trace** (`upsample_bicubic2d` | ||
| rejects the traced dynamic `output_size`). Since the input size is fixed, the | ||
| export script precomputes the interpolated table once and monkeypatches | ||
| `interpolate_pos_encoding` to return that constant. Slice E's production export | ||
| reuses this. | ||
|
|
||
| --- | ||
|
|
||
| ## Marching cubes — the host-side iso-surface step | ||
|
|
||
| **Decision: native, ported into `src/` — NOT vendored.** The codebase had no | ||
| iso-surface code (confirmed: nothing in `src/` or `src/dependencies/`). TripoSR | ||
| uses `torchmcubes` (GPU/torch — not usable in the C++ app). We ship a from-scratch | ||
| Lorensen–Cline marching cubes: | ||
|
|
||
| - `src/MarchingCubes.h/.cpp` — pure-data (no Ogre, no Qt-singleton; same shape as | ||
| `PbrMapSynth` / `SkinWeights`, so it unit-tests without a GL context). | ||
| - The 256-entry edge mask + 256×16 triangle tables are the canonical **public-domain** | ||
| marching-cubes tables (Paul Bourke's widely-mirrored tabulation) — authored into | ||
| our `.cpp`, nothing fetched. Zero new dependency, matching the native-heuristic | ||
| stance of SkinWeights (#402, avoided GPL TetGen) and QuadRetopo (#401). | ||
| - API: `extract(field, nx, ny, nz, isoLevel, gridMin, gridMax) → {positions, indices}` | ||
| in world space, with edge-hash vertex welding (adjacent triangles share vertices, | ||
| so downstream normal accumulation and export are clean). | ||
| - Inside-positive convention: caller passes `field = density − threshold`, | ||
| `isoLevel = 0` — the sign/threshold bookkeeping lives at the call site. | ||
|
|
||
| ### Verified (offline, no GL — `src/MarchingCubes_test.cpp`) | ||
| | SDF | Result | | ||
| |-----|--------| | ||
| | Sphere R=0.6 @ 48³ | V=3744, T=7484; **0 boundary edges, 0 non-manifold, Euler χ = 2** (watertight); max radial deviation 0.0004 (cell 0.0426) | | ||
| | Box H=0.5 @ 40³ | AABB = `[-0.5,0.5]³` to within a cell | | ||
| | Empty / null / degenerate grid | empty mesh, no crash | | ||
| | Iso-level shift | higher iso → smaller surface (threshold param works) | | ||
|
|
||
| > **macOS local caveat:** `UnitTests` aborts at startup on macOS because | ||
| > `test_main` requires a working GL context (`tryInitOgre()` — a known project | ||
| > limitation). The MC tests are pure-data and run on **Linux CI (Xvfb)**; locally | ||
| > they were verified by compiling `MarchingCubes.cpp` standalone against the same | ||
| > assertions (all pass). | ||
|
|
||
| ### Expected costs (slice B/C planning) | ||
| - Vertex counts scale ~with the surface area in cells: a res-256 character ≈ tens | ||
| of thousands of triangles (TripoSR's own default). Res 128 is a good fast/preview | ||
| tier. | ||
| - The dominant cost is the **decoder grid query** (resolution³ points through the | ||
| MLP), not the MC pass. Chunked decoder inference + optional coarse-grid | ||
| early-out are slice-B tuning knobs. | ||
|
|
||
| --- | ||
|
|
||
| ## C++ load-proof (`src/MeshGenSpike_test.cpp`) | ||
|
|
||
| An `ENABLE_ONNX`-guarded test opens the exported encoder+decoder with the **exact** | ||
| `Ort::Session` setup the shipping predictors use (`UniRigPredictor.cpp` ~820-843: | ||
| `ORT_ENABLE_ALL`, CoreML EP in try/catch on `__APPLE__`, wide-string path on | ||
| `_WIN32`) and asserts the I/O node counts + tensor ranks match the contract above. | ||
| It **skips** unless the exported `.onnx` files are present in the AppData cache | ||
| (`ai_models/triposr/`), because the model is not hosted yet (slice E) — the same | ||
| "covered on CI when the model is available" convention as UniRig/PBR and the | ||
| "rignet.onnx not yet hosted" precedent. It compiles and links against the real ORT | ||
| headers today (validating the API slice B clones); it turns green the moment a | ||
| developer drops the exported models in the cache. | ||
|
|
||
| **Verified on this machine** (macOS arm64) with a standalone build of the same | ||
| `Ort::Session` setup against the freshly-exported models: | ||
| ``` | ||
| ENCODER: in[0]=[-1,3,512,512] out[0]=rank-5 triplane | ||
| DECODER: in=[1,3,40,64,64]+[1,P,3] out=density[1,P,1]+color[1,P,3] | ||
| DECODER RAN: density=[1,512,1] → LOAD-PROOF PASS | ||
| ``` | ||
| (`UnitTests` itself can't run on macOS — `test_main` requires GL; the gtest runs on | ||
| Linux CI. The encoder's exported output dims show as dynamic `-1` because batch was | ||
| marked dynamic and shape inference propagated through the triplane transformer; the | ||
| concrete `[1,3,40,64,64]` is confirmed by the Python `--verify` run and the decoder | ||
| accepting it. The committed gtest asserts ranks, not the dynamic dims, so it is | ||
| robust to this.) | ||
|
|
||
| --- | ||
|
|
||
| ## Risks & mitigations | ||
|
|
||
| | Risk | Assessment | Mitigation | | ||
| |------|-----------|------------| | ||
| | ONNX export of the transformer backbone | LOW — no AR loop, standard attention | opset 17; split encoder/decoder (done) | | ||
| | `grid_sample` in the decoder | LOW — supported opset ≥16 / ORT 1.20.1 | verified op availability; `--verify` ORT round-trip in the script | | ||
| | DINO ViT export | LOW — HF `ViTModel` exports routinely | part of the encoder graph | | ||
| | Decoder grid-query cost | MEDIUM (perf, not feasibility) | chunk `P`; offer res-128 preview tier; coarse early-out (slice B) | | ||
| | Grid axis/order mismatch | LOW | contract documented above; export script prints reference ordering | | ||
| | Windows MinGW | KNOWN — ORT archive is MSVC-built | `ENABLE_ONNX` stays OFF on MinGW; feature degrades to "rebuild with -DENABLE_ONNX" (same as #404) | | ||
| | Model not hosted yet | EXPECTED | clean "TripoSR model not yet hosted" state (slice E hosts it) | | ||
|
|
||
| --- | ||
|
|
||
| ## Go/No-Go | ||
|
|
||
| **GO.** Both epic-level unknowns are retired: | ||
| 1. **Marching cubes exists and is correct** — native, permissive, watertight on | ||
| synthetic SDFs, unit-tested. | ||
| 2. **The network exports to ONNX cleanly** — a clean encoder/decoder split with no | ||
| autoregressive decode and only `grid_sample` as a notable op (supported). | ||
|
|
||
| Proceed to slice B (#766 `MeshGenPredictor`) against the tensor contract above, | ||
| then C (#767 mesh build/export), D (#768 CLI/MCP/GUI), E (#769 hosting/packaging). | ||
| No re-scope required. |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.