Spaces:
Configuration error
Configuration error
Commit
·
31dbf53
1
Parent(s):
f1e0138
voxel reduction
Browse files- design_docs/post_reduction_meshing.md +120 -0
- design_docs/splat_rendering.md +124 -0
- design_docs/voxel_reduction.md +212 -0
- design_docs/voxel_reinflation.md +106 -0
- requirements.txt +1 -0
- stream3r/utils/__pycache__/visual_utils.cpython-311.pyc +0 -0
- stream3r/utils/visual_utils.py +281 -6
- stream3r/worker/config.py +7 -3
- stream3r/worker/tasks.py +44 -2
- tests/test_voxel_reduction.py +114 -0
design_docs/post_reduction_meshing.md
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
```markdown
|
| 2 |
+
# Design Doc: Post-Reduction Meshing for Clean Surface Export
|
| 3 |
+
|
| 4 |
+
**Author:** Brian Clark
|
| 5 |
+
**Last Updated:** 2025-11-07
|
| 6 |
+
**Target Component:** Optional stage after `predictions_to_glb` point processing
|
| 7 |
+
**Goal:** Convert cleaned point clouds into lightweight surface meshes for users who prefer shaded geometry over splatty point clouds.
|
| 8 |
+
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
## 1. Overview
|
| 12 |
+
|
| 13 |
+
After confidence filtering, voxel reduction, and denoising we hold a sparse, high-confidence point cloud.
|
| 14 |
+
This design adds an optional meshing stage (ball-pivoting, Poisson, marching cubes) to produce a triangle mesh that can be exported alongside or instead of the GLB point cloud.
|
| 15 |
+
|
| 16 |
+
---
|
| 17 |
+
|
| 18 |
+
## 2. Use Cases
|
| 19 |
+
|
| 20 |
+
- Interactive viewers requiring solid surfaces (lighting, shadowing).
|
| 21 |
+
- Downstream pipelines that expect meshes (CAD tools, 3D printing previews).
|
| 22 |
+
- Scenes where splatty points look sparse even after reinflation.
|
| 23 |
+
|
| 24 |
+
We do **not** aim for watertight printable models—just visually continuous surfaces.
|
| 25 |
+
|
| 26 |
+
---
|
| 27 |
+
|
| 28 |
+
## 3. Meshing Strategies
|
| 29 |
+
|
| 30 |
+
| Method | Pros | Cons | Dependencies |
|
| 31 |
+
| ------ | ---- | ---- | ------------ |
|
| 32 |
+
| **Ball-Pivoting (BPA)** | Stable for uneven sampling, preserves detail | Needs normals, parameter tuning | Open3D |
|
| 33 |
+
| **Poisson Reconstruction** | Smooth surfaces, fills gaps | Blurs thin structures, more compute | Open3D |
|
| 34 |
+
| **Marching Cubes on fused depth** | Works without normals | Requires voxel grid, might need extra fusion step | Open3D / custom |
|
| 35 |
+
|
| 36 |
+
Initial plan: start with Open3D’s **Ball-Pivoting**, fall back to Poisson when BPA fails.
|
| 37 |
+
|
| 38 |
+
---
|
| 39 |
+
|
| 40 |
+
## 4. Pipeline Integration
|
| 41 |
+
|
| 42 |
+
1. Existing point-cleaning (voxel, support filtering, optional reinflation).
|
| 43 |
+
2. Normal estimation (Open3D `estimate_normals`, with KD-tree search radius tied to voxel size).
|
| 44 |
+
3. Mesh reconstruction (BPA default, Poisson fallback).
|
| 45 |
+
4. Mesh simplification (`simplify_quadric_decimation`) to hit target face count.
|
| 46 |
+
5. Export as GLB/OBJ/PLY; attach materials/colors using per-vertex colors (sampled from original points).
|
| 47 |
+
|
| 48 |
+
### Configuration options
|
| 49 |
+
| Option | Default | Notes |
|
| 50 |
+
| ------ | ------- | ----- |
|
| 51 |
+
| `meshing_enabled` | `False` | Opt-in feature |
|
| 52 |
+
| `meshing_method` | `"ball_pivoting"` | `"poisson"` available |
|
| 53 |
+
| `bpa_radii` | `[voxel_size, 2×, 4×]` | Radii list for BPA |
|
| 54 |
+
| `poisson_depth` | 8 | Tree depth, controls detail |
|
| 55 |
+
| `target_face_count` | 200k | Post-simplification triangle budget |
|
| 56 |
+
| `keep_point_cloud` | `True` | Export both mesh + original cloud |
|
| 57 |
+
|
| 58 |
+
---
|
| 59 |
+
|
| 60 |
+
## 5. Implementation Plan
|
| 61 |
+
|
| 62 |
+
### 5.1 Helper module
|
| 63 |
+
`stream3r/utils/mesh_utils.py`
|
| 64 |
+
```python
|
| 65 |
+
def build_surface_mesh(points, colors, *, config) -> trimesh.Trimesh:
|
| 66 |
+
# open3d conversions, normal estimation
|
| 67 |
+
# reconstruction via BPA/Poisson
|
| 68 |
+
# color baking (nearest neighbor)
|
| 69 |
+
```
|
| 70 |
+
|
| 71 |
+
### 5.2 Export integration
|
| 72 |
+
In `_generate_core_outputs` after point cloud saved:
|
| 73 |
+
```python
|
| 74 |
+
if settings.meshing_enabled:
|
| 75 |
+
mesh = build_surface_mesh(vertices_3d, colors_rgb, config)
|
| 76 |
+
mesh_url = _save_mesh(runtime, scene_id, mesh, temp_dir)
|
| 77 |
+
artifacts["mesh_url"] = mesh_url
|
| 78 |
+
```
|
| 79 |
+
|
| 80 |
+
### 5.3 Storage
|
| 81 |
+
- Upload under `models/meshes/scene_mesh.glb`
|
| 82 |
+
- Optionally provide OBJ + MTL for compatibility.
|
| 83 |
+
|
| 84 |
+
---
|
| 85 |
+
|
| 86 |
+
## 6. Validation
|
| 87 |
+
|
| 88 |
+
1. Compare GLB mesh size vs. original point cloud.
|
| 89 |
+
2. Manual visual QA (Viewer, Blender) to spot holes or artifacts.
|
| 90 |
+
3. Automated checks:
|
| 91 |
+
- Mesh exists and contains triangles.
|
| 92 |
+
- Vertex count under budget.
|
| 93 |
+
- Color channels preserved (mean deviation < ε).
|
| 94 |
+
4. Benchmark runtime per scene and ensure it fits within job timeouts.
|
| 95 |
+
|
| 96 |
+
---
|
| 97 |
+
|
| 98 |
+
## 7. Risks & Mitigations
|
| 99 |
+
|
| 100 |
+
| Risk | Mitigation |
|
| 101 |
+
| ---- | ---------- |
|
| 102 |
+
| Thin structures lost | Tune BPA radii; detect failure and revert to point cloud |
|
| 103 |
+
| Open3D dependency bloat | Gate meshing behind `pip install open3d`; log when unavailable |
|
| 104 |
+
| Runtime overhead | Make stage optional; expose `meshing_timeout` |
|
| 105 |
+
| Large meshes | Apply decimation & optional texture baking |
|
| 106 |
+
|
| 107 |
+
---
|
| 108 |
+
|
| 109 |
+
## 8. Deliverables
|
| 110 |
+
|
| 111 |
+
1. Helper module for meshing + tests.
|
| 112 |
+
2. Scene artifacts update (mesh export, metadata).
|
| 113 |
+
3. New config flags (`STREAM3R_MESHING_ENABLED`, etc.).
|
| 114 |
+
4. Documentation/tutorial for users toggling the mesh output.
|
| 115 |
+
|
| 116 |
+
---
|
| 117 |
+
|
| 118 |
+
**Outcome:** An optional mesh artifact that gives viewers a solid-looking scene without fully abandoning the point-based pipeline.
|
| 119 |
+
|
| 120 |
+
```
|
design_docs/splat_rendering.md
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
```markdown
|
| 2 |
+
# Design Doc: Splat-Aware Export with Per-Point Radii & View-Dependent Color
|
| 3 |
+
|
| 4 |
+
**Author:** Brian Clark
|
| 5 |
+
**Last Updated:** 2025-11-07
|
| 6 |
+
**Target Components:** Export pipeline (`predictions_to_glb`), downstream visualization stack
|
| 7 |
+
**Goal:** Preserve the rich “Gaussian splat” appearance by exporting per-point radius/orientation/color information and using a splat-aware renderer instead of vanilla GLB point clouds.
|
| 8 |
+
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
## 1. Overview
|
| 12 |
+
|
| 13 |
+
The current GLB export stores XYZ + RGB points. Rendering them as fixed-size pixels loses the soft, blended look achieved by overlapping splats in the original predictions.
|
| 14 |
+
This design introduces a splat representation (Gaussian or disk) with explicit radius and optional anisotropy, and changes the rendering path to honor those attributes.
|
| 15 |
+
|
| 16 |
+
---
|
| 17 |
+
|
| 18 |
+
## 2. Requirements
|
| 19 |
+
|
| 20 |
+
1. **Data export**
|
| 21 |
+
- Include at least:
|
| 22 |
+
- Center (`x,y,z`)
|
| 23 |
+
- Radius (`r`) or covariance matrix (`Σ`)
|
| 24 |
+
- Base color (RGB) and optional view-dependent coefficients (e.g., SH).
|
| 25 |
+
- Allow fallback to disk splats (isotropic) if covariance unavailable.
|
| 26 |
+
|
| 27 |
+
2. **File format**
|
| 28 |
+
- Options:
|
| 29 |
+
- Extend GLB with custom vertex attributes + custom shader (three.js, deck.gl).
|
| 30 |
+
- Use established Gaussian splat formats (e.g., `.splat`, `.ply` with extra attributes, or `gaussian-splatting` binary).
|
| 31 |
+
- Include metadata describing attribute semantics for the viewer.
|
| 32 |
+
|
| 33 |
+
3. **Renderer**
|
| 34 |
+
- Web: Three.js shader or deck.gl layer capable of drawing splats.
|
| 35 |
+
- Native: Optionally integrate with existing Gaussian splatting viewers (Instant-NGP, Splatfacto, etc.).
|
| 36 |
+
|
| 37 |
+
4. **Performance**
|
| 38 |
+
- Maintain reasonable splat counts (prefiltered via voxel reduction & density tests).
|
| 39 |
+
- Provide LOD strategies (e.g., radius-aware culling, multi-resolution export).
|
| 40 |
+
|
| 41 |
+
---
|
| 42 |
+
|
| 43 |
+
## 3. Proposed Pipeline
|
| 44 |
+
|
| 45 |
+
### 3.1 Data preparation
|
| 46 |
+
1. Start with voxel-reduced points.
|
| 47 |
+
2. Estimate per-point covariance:
|
| 48 |
+
- Option A: Use original support’s covariance (requires storing full neighbor set).
|
| 49 |
+
- Option B: Compute isotropic radius from average neighbor distance (k-NN) or voxel size.
|
| 50 |
+
3. Store color as linear RGB; optionally store SH coefficients computed during inference (if available).
|
| 51 |
+
|
| 52 |
+
### 3.2 Export formats
|
| 53 |
+
|
| 54 |
+
| Format | Pros | Cons |
|
| 55 |
+
| ------ | ---- | ---- |
|
| 56 |
+
| **GLB + custom vertex attributes** | Simple integration with existing pipeline | Requires custom shader; not portable |
|
| 57 |
+
| **.splat binary (Gaussian Splatting)** | Compatibility with emerging viewers | New dependency; not widely standardized |
|
| 58 |
+
| **PLY/NPZ with attributes** | Readable, quick prototyping | Requires loader adaptation |
|
| 59 |
+
|
| 60 |
+
Initial recommendation: use GLB with custom attributes (e.g., `RADIUS`, `COV3x3`) and provide sample shaders for three.js.
|
| 61 |
+
|
| 62 |
+
### 3.3 Rendering changes
|
| 63 |
+
1. Publish a three.js script that:
|
| 64 |
+
- Loads GLB.
|
| 65 |
+
- Extracts attributes into buffers.
|
| 66 |
+
- Renders via custom shader (screen-space splatting).
|
| 67 |
+
2. Provide deck.gl example using `PointCloudLayer` with `radiusPixels` or custom shader module.
|
| 68 |
+
3. Eventually support native Gaussian Splat format for plug-and-play compatibility.
|
| 69 |
+
|
| 70 |
+
---
|
| 71 |
+
|
| 72 |
+
## 4. API changes
|
| 73 |
+
|
| 74 |
+
Add new export options:
|
| 75 |
+
- `export_mode: {"glb", "splat_glb", "gaussian_binary"}`
|
| 76 |
+
- `splat_settings` (radius multiplier, anisotropy toggle, max anisotropy)
|
| 77 |
+
- `lod_strategy` (none, subsample, multi-file)
|
| 78 |
+
|
| 79 |
+
Ensure backward compatibility by keeping current GLB path as default.
|
| 80 |
+
|
| 81 |
+
---
|
| 82 |
+
|
| 83 |
+
## 5. Validation & Testing
|
| 84 |
+
|
| 85 |
+
1. Compare visual output in existing viewer vs. splat viewer (visual parity).
|
| 86 |
+
2. Measure load time, FPS for large scenes.
|
| 87 |
+
3. Unit tests for:
|
| 88 |
+
- Attribute packing/unpacking.
|
| 89 |
+
- Radius estimation.
|
| 90 |
+
- Export file integrity.
|
| 91 |
+
4. Integration test: pipeline -> viewer round-trip (screenshot diff, metrics).
|
| 92 |
+
|
| 93 |
+
---
|
| 94 |
+
|
| 95 |
+
## 6. Risks & Mitigations
|
| 96 |
+
|
| 97 |
+
| Risk | Mitigation |
|
| 98 |
+
| ---- | ---------- |
|
| 99 |
+
| Custom GLB attributes unsupported by some tools | Provide fallback GLB path, document requirements |
|
| 100 |
+
| Increased file size due to extra attributes | Leverage quantization/compression, allow isotropic mode |
|
| 101 |
+
| Viewer complexity | Ship reference shader + deck.gl layer; adopt existing open-source splat renderer |
|
| 102 |
+
| Lack of covariance data | Start with isotropic radii derived from voxel size |
|
| 103 |
+
|
| 104 |
+
---
|
| 105 |
+
|
| 106 |
+
## 7. Timeline (rough)
|
| 107 |
+
|
| 108 |
+
1. Week 1: Data prep & attribute computation (radius/covariance).
|
| 109 |
+
2. Week 2: GLB exporter modifications + tests.
|
| 110 |
+
3. Week 3: Viewer shader integration, documentation.
|
| 111 |
+
4. Week 4: Optional Gaussian binary export + performance tuning.
|
| 112 |
+
|
| 113 |
+
---
|
| 114 |
+
|
| 115 |
+
## 8. Deliverables
|
| 116 |
+
|
| 117 |
+
1. Updated export pipeline producing splat-aware asset.
|
| 118 |
+
2. Viewer example repo (three.js + deck.gl).
|
| 119 |
+
3. Documentation covering format, tuning knobs, and integration steps.
|
| 120 |
+
4. Automated tests validating attribute correctness & rendering.
|
| 121 |
+
|
| 122 |
+
---
|
| 123 |
+
|
| 124 |
+
```
|
design_docs/voxel_reduction.md
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
```markdown
|
| 2 |
+
# Design Doc: Stream3R → Clean GLB Export with Voxel Reduction + Open3D Outlier Removal
|
| 3 |
+
|
| 4 |
+
**Author:** Brian Clark
|
| 5 |
+
**Last Updated:** 2025-11-03
|
| 6 |
+
**Target Component:** `predictions_to_glb()` (Stream3R repo)
|
| 7 |
+
**Goal:** Integrate a high-fidelity voxel reduction and denoising stage for cleaner, lighter `.glb` outputs suitable for downstream r3f visualization.
|
| 8 |
+
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
## 1. Overview
|
| 12 |
+
|
| 13 |
+
The existing `predictions_to_glb()` function builds a large unfiltered point cloud directly from Stream3R predictions.
|
| 14 |
+
This design adds two optional cleanup stages that:
|
| 15 |
+
- **(1) Voxel-reduce** redundant points into weighted centroids.
|
| 16 |
+
- **(2) Filter residual noise** with Open3D’s statistical & radius outlier removal.
|
| 17 |
+
|
| 18 |
+
This yields cleaner geometry, lower GLB size, and faster loading, while preserving color and geometry fidelity.
|
| 19 |
+
|
| 20 |
+
---
|
| 21 |
+
|
| 22 |
+
## 2. Key Additions
|
| 23 |
+
|
| 24 |
+
### 2.1 New helpers
|
| 25 |
+
|
| 26 |
+
#### `voxel_reduce(points_f32, colors_u8, conf_f32, voxel_size)`
|
| 27 |
+
- Merges points falling within the same voxel grid cell.
|
| 28 |
+
- Weighted average of position, color (in *linear* space), and optional confidence.
|
| 29 |
+
- Returns `(points, colors, support)`.
|
| 30 |
+
|
| 31 |
+
#### `o3d_outlier_filter(points_f32, colors_u8, voxel_size, radius_mult, nb_points, nb_neighbors, std_ratio)`
|
| 32 |
+
- Converts NumPy arrays → Open3D `PointCloud`.
|
| 33 |
+
- Applies:
|
| 34 |
+
1. **Radius Outlier Removal:** ensures local density.
|
| 35 |
+
2. **Statistical Outlier Removal:** drops sparse noise.
|
| 36 |
+
- Converts filtered result back to NumPy + `uint8` sRGB.
|
| 37 |
+
|
| 38 |
+
### 2.2 Optional parameters added to `predictions_to_glb()`
|
| 39 |
+
| Parameter | Type | Default | Description |
|
| 40 |
+
|------------|------|----------|--------------|
|
| 41 |
+
| `voxel_size` | float \| None | None | Enables voxel reduction if >0 |
|
| 42 |
+
| `voxel_after_conf` | bool | True | Reduce after confidence & mask filtering |
|
| 43 |
+
| `o3d_denoise` | bool | True | Enables Open3D outlier filtering |
|
| 44 |
+
| `o3d_params` | dict \| None | None | Override Open3D defaults (e.g. radius_mult) |
|
| 45 |
+
|
| 46 |
+
### 2.3 Processing order
|
| 47 |
+
|
| 48 |
+
```
|
| 49 |
+
|
| 50 |
+
predictions → confidence/bg masking
|
| 51 |
+
→ [optional] voxel_reduce()
|
| 52 |
+
→ [optional] o3d_outlier_filter()
|
| 53 |
+
→ trimesh.PointCloud → GLB
|
| 54 |
+
|
| 55 |
+
````
|
| 56 |
+
|
| 57 |
+
Both stages are fully optional; default behavior is unchanged.
|
| 58 |
+
|
| 59 |
+
---
|
| 60 |
+
|
| 61 |
+
## 3. Implementation Plan
|
| 62 |
+
|
| 63 |
+
### 3.1 Import dependencies
|
| 64 |
+
```python
|
| 65 |
+
import open3d as o3d # optional heavy dependency
|
| 66 |
+
import numpy as np
|
| 67 |
+
import trimesh
|
| 68 |
+
````
|
| 69 |
+
|
| 70 |
+
### 3.2 Helper: `voxel_reduce()`
|
| 71 |
+
|
| 72 |
+
```python
|
| 73 |
+
def voxel_reduce(points_f32, colors_u8, conf_f32=None, voxel_size=0.02, origin=None):
|
| 74 |
+
# sRGB→linear, weighted average, linear→sRGB
|
| 75 |
+
# Hash each voxel using large primes (avoids collisions)
|
| 76 |
+
# Return reduced arrays
|
| 77 |
+
```
|
| 78 |
+
|
| 79 |
+
### 3.3 Helper: `o3d_outlier_filter()`
|
| 80 |
+
|
| 81 |
+
```python
|
| 82 |
+
def o3d_outlier_filter(points_f32, colors_u8,
|
| 83 |
+
voxel_size=0.02,
|
| 84 |
+
radius_mult=3.0,
|
| 85 |
+
nb_points=16,
|
| 86 |
+
nb_neighbors=48,
|
| 87 |
+
std_ratio=1.5):
|
| 88 |
+
# Construct Open3D cloud, remove outliers, return filtered arrays
|
| 89 |
+
```
|
| 90 |
+
|
| 91 |
+
### 3.4 Patch in `predictions_to_glb()`
|
| 92 |
+
|
| 93 |
+
Insert after confidence masking:
|
| 94 |
+
|
| 95 |
+
```python
|
| 96 |
+
if voxel_size is not None and voxel_size > 0:
|
| 97 |
+
vertices_3d, colors_rgb, _support = voxel_reduce(
|
| 98 |
+
vertices_3d, colors_rgb, conf_f32=conf_used, voxel_size=float(voxel_size)
|
| 99 |
+
)
|
| 100 |
+
|
| 101 |
+
if o3d_denoise and vertices_3d.size:
|
| 102 |
+
params = dict(
|
| 103 |
+
voxel_size=float(voxel_size or 0.02),
|
| 104 |
+
radius_mult=3.0,
|
| 105 |
+
nb_points=16,
|
| 106 |
+
nb_neighbors=48,
|
| 107 |
+
std_ratio=1.5,
|
| 108 |
+
)
|
| 109 |
+
if o3d_params: params.update(o3d_params)
|
| 110 |
+
vertices_3d, colors_rgb = o3d_outlier_filter(vertices_3d, colors_rgb, **params)
|
| 111 |
+
```
|
| 112 |
+
|
| 113 |
+
The rest of the GLB creation (scene scale, camera meshes, alignment) remains unchanged.
|
| 114 |
+
|
| 115 |
+
---
|
| 116 |
+
|
| 117 |
+
## 4. Default Parameters & Behavior
|
| 118 |
+
|
| 119 |
+
| Context | Setting | Recommended |
|
| 120 |
+
| ------------------ | --------------------------------------- | -------------------- |
|
| 121 |
+
| Indoor scenes | `voxel_size=0.02` | 2 cm grid |
|
| 122 |
+
| Fast preview | `voxel_size=0.06` | Coarse 6 cm grid |
|
| 123 |
+
| Radius filter | `radius = 3×voxel_size` | 0.06 m for 2 cm grid |
|
| 124 |
+
| Statistical filter | `nb_neighbors=48`, `std_ratio=1.5` | Safe defaults |
|
| 125 |
+
| Weighting | Confidence scores (`world_points_conf`) | Use for averages |
|
| 126 |
+
|
| 127 |
+
---
|
| 128 |
+
|
| 129 |
+
## 5. Expected Outcomes
|
| 130 |
+
|
| 131 |
+
| Metric | Before | After |
|
| 132 |
+
| ---------------------- | --------- | ---------------- |
|
| 133 |
+
| GLB file size | 1× | ↓ 3–8× |
|
| 134 |
+
| Visual duplicates | High | Minimal |
|
| 135 |
+
| Noise/speckle | Frequent | Strongly reduced |
|
| 136 |
+
| Load time (r3f viewer) | Long | Near-instant |
|
| 137 |
+
| Fidelity | Unchanged | Preserved |
|
| 138 |
+
|
| 139 |
+
---
|
| 140 |
+
|
| 141 |
+
## 6. Validation Steps
|
| 142 |
+
|
| 143 |
+
1. **Run baseline:**
|
| 144 |
+
`predictions_to_glb(preds, voxel_size=None, o3d_denoise=False)`
|
| 145 |
+
→ export size / load time baseline.
|
| 146 |
+
|
| 147 |
+
2. **Run optimized:**
|
| 148 |
+
`predictions_to_glb(preds, voxel_size=0.02, o3d_denoise=True)`
|
| 149 |
+
→ compare GLB size, visual quality, and FPS in viewer.
|
| 150 |
+
|
| 151 |
+
3. **Stress-test:**
|
| 152 |
+
|
| 153 |
+
* High-conf scenes with many frames.
|
| 154 |
+
* Scenes with thin structures (shelves, walls).
|
| 155 |
+
* Ensure no noticeable geometric bias or color shift.
|
| 156 |
+
|
| 157 |
+
---
|
| 158 |
+
|
| 159 |
+
## 7. Future Extensions
|
| 160 |
+
|
| 161 |
+
| Feature | Description |
|
| 162 |
+
| --------------------- | ------------------------------------------------------------------ |
|
| 163 |
+
| **Normals averaging** | Extend `voxel_reduce()` to merge normals & store as GLB attributes |
|
| 164 |
+
| **Support weighting** | Save per-voxel support count → possible LOD weighting |
|
| 165 |
+
| **Covariance export** | Optionally compute per-voxel covariance for Gaussian splats |
|
| 166 |
+
| **Tile-based batch** | Enable out-of-core fusion for huge rooms |
|
| 167 |
+
| **Dual GLB export** | Auto-save coarse (preview) + fine (full-res) versions |
|
| 168 |
+
|
| 169 |
+
---
|
| 170 |
+
|
| 171 |
+
## 8. Example Usage
|
| 172 |
+
|
| 173 |
+
```python
|
| 174 |
+
scene = predictions_to_glb(
|
| 175 |
+
preds,
|
| 176 |
+
conf_thres=50.0,
|
| 177 |
+
mask_white_bg=True,
|
| 178 |
+
voxel_size=0.02,
|
| 179 |
+
o3d_denoise=True,
|
| 180 |
+
o3d_params={"nb_neighbors": 64, "std_ratio": 1.3}
|
| 181 |
+
)
|
| 182 |
+
trimesh.exchange.gltf.export_glb(scene, "room_clean.glb")
|
| 183 |
+
```
|
| 184 |
+
|
| 185 |
+
---
|
| 186 |
+
|
| 187 |
+
## 9. Deliverables for Codex Implementation
|
| 188 |
+
|
| 189 |
+
1. **New helper functions**
|
| 190 |
+
|
| 191 |
+
* `voxel_reduce()`
|
| 192 |
+
* `o3d_outlier_filter()`
|
| 193 |
+
|
| 194 |
+
2. **Modified signature** of `predictions_to_glb()` to include new optional args.
|
| 195 |
+
|
| 196 |
+
3. **Integration** of both steps before `trimesh.PointCloud`.
|
| 197 |
+
|
| 198 |
+
4. **Minimal dependency injection**
|
| 199 |
+
(`open3d` imported lazily; safe fail if missing).
|
| 200 |
+
|
| 201 |
+
5. **Unit test / validation script**
|
| 202 |
+
|
| 203 |
+
* Compare point counts & file sizes pre-/post-cleanup.
|
| 204 |
+
* Assert geometry type remains `PointCloud`.
|
| 205 |
+
|
| 206 |
+
---
|
| 207 |
+
|
| 208 |
+
**Outcome:**
|
| 209 |
+
A drop-in replacement for `predictions_to_glb()` producing denser, visually identical but much smaller and cleaner `.glb` point clouds for Stream3R → r3f workflows.
|
| 210 |
+
|
| 211 |
+
```
|
| 212 |
+
```
|
design_docs/voxel_reinflation.md
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
```markdown
|
| 2 |
+
# Design Doc: Voxel Reinflation to Recover Dense Splat Coverage
|
| 3 |
+
|
| 4 |
+
**Author:** Brian Clark
|
| 5 |
+
**Last Updated:** 2025-11-07
|
| 6 |
+
**Target Component:** `predictions_to_glb()` (Stream3R repo)
|
| 7 |
+
**Goal:** Restore the “splatty” look of point clouds after voxel reduction by respawning micro-clusters around each reduced voxel center without bloating GLB size back to the original.
|
| 8 |
+
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
## 1. Overview
|
| 12 |
+
|
| 13 |
+
The current voxel reduction keeps one weighted centroid per cell. Great for size, bad for perceived density.
|
| 14 |
+
This design “re-inflates” each voxel into a small jittered cluster proportional to its support, so standard GLB point rendering still looks like a filled surface.
|
| 15 |
+
|
| 16 |
+
---
|
| 17 |
+
|
| 18 |
+
## 2. Key Concepts
|
| 19 |
+
|
| 20 |
+
### 2.1 Inputs from existing pipeline
|
| 21 |
+
- `voxel_reduce()` already returns:
|
| 22 |
+
- `points`: voxel centroids
|
| 23 |
+
- `support`: sum of confidences (or point counts) per voxel
|
| 24 |
+
- We can use `support` to estimate how many original samples a voxel represents.
|
| 25 |
+
|
| 26 |
+
### 2.2 Re-inflation strategy
|
| 27 |
+
- Emit `k = clamp(round(alpha * support), min_samples, max_samples)` points per voxel.
|
| 28 |
+
- Each point = centroid + jitter sampled uniformly within the voxel cube (or Gaussian with σ tied to voxel size).
|
| 29 |
+
- Jittered colors copied from centroid (optionally add mild hue jitter for variety).
|
| 30 |
+
- Optional normal estimation by reusing local PCA from original neighbors (future extension).
|
| 31 |
+
|
| 32 |
+
### 2.3 Controls
|
| 33 |
+
| Parameter | Description | Default |
|
| 34 |
+
| --------- | ----------- | ------- |
|
| 35 |
+
| `reinflate_enabled` | Toggle the entire stage | `False` (opt-in) |
|
| 36 |
+
| `support_scale` | α multiplier converting support → sample count | 0.5 |
|
| 37 |
+
| `min_samples` | Minimum points per voxel | 1 |
|
| 38 |
+
| `max_samples` | Ceiling per voxel to cap explosion | 12 |
|
| 39 |
+
| `jitter_mode` | `cube` (uniform) or `gaussian` | `cube` |
|
| 40 |
+
| `jitter_sigma` | For gaussian mode; fraction of voxel size | 0.35 |
|
| 41 |
+
| `seed` | Deterministic RNG seed for reproducibility | 0 (disabled) |
|
| 42 |
+
|
| 43 |
+
---
|
| 44 |
+
|
| 45 |
+
## 3. Implementation Plan
|
| 46 |
+
|
| 47 |
+
### 3.1 Helper function
|
| 48 |
+
```python
|
| 49 |
+
def reinflate_voxels(points, colors, support, voxel_size, *, config):
|
| 50 |
+
# Determine sample counts per voxel
|
| 51 |
+
# Generate jitter offsets (vectorized, deterministic optional)
|
| 52 |
+
# Repeat colors + points with offsets
|
| 53 |
+
# Return expanded arrays
|
| 54 |
+
```
|
| 55 |
+
- Use `np.repeat` to build index arrays, avoid Python loops.
|
| 56 |
+
- Optional RNG seeded via `np.random.default_rng`.
|
| 57 |
+
|
| 58 |
+
### 3.2 Integration point
|
| 59 |
+
Insert after voxel reduction & support filtering, before Open3D/density filter.
|
| 60 |
+
Why? We want re-inflated points to still be denoised and deduped if needed.
|
| 61 |
+
|
| 62 |
+
```
|
| 63 |
+
if reinflate_enabled and vertices_3d.size:
|
| 64 |
+
vertices_3d, colors_rgb = reinflate_voxels(..., support=conf_used, ...)
|
| 65 |
+
```
|
| 66 |
+
|
| 67 |
+
### 3.3 Performance considerations
|
| 68 |
+
- Re-inflation runs in-memory; ensure we pre-allocate arrays (`np.empty`) to avoid Python loops.
|
| 69 |
+
- Keep `max_samples` conservative to prevent 2 GB GLBs.
|
| 70 |
+
- Skip stage when `support` missing (e.g., streaming mode without confidences).
|
| 71 |
+
|
| 72 |
+
---
|
| 73 |
+
|
| 74 |
+
## 4. Expected Outcomes
|
| 75 |
+
| Metric | Before | After |
|
| 76 |
+
| ------ | ------ | ----- |
|
| 77 |
+
| GLB size | ~0.5× raw | ~0.7× raw (depends on support_scale) |
|
| 78 |
+
| Visual density | Sparse | Near original “splat” look |
|
| 79 |
+
| Compute cost | negligible | + small vectorized jitter step |
|
| 80 |
+
|
| 81 |
+
---
|
| 82 |
+
|
| 83 |
+
## 5. Validation Plan
|
| 84 |
+
1. Compare point counts vs. original per-scene.
|
| 85 |
+
2. Visual inspection in r3f viewer for surface coverage.
|
| 86 |
+
3. Ensure jitter uses consistent seed for reproducible exports.
|
| 87 |
+
4. Capture metrics: GLB size, avg points per voxel, render FPS.
|
| 88 |
+
|
| 89 |
+
---
|
| 90 |
+
|
| 91 |
+
## 6. Future Extensions
|
| 92 |
+
- Encode support as per-point radius attribute for richer viewers.
|
| 93 |
+
- Adaptive jitter: anisotropic offsets aligned with local normals.
|
| 94 |
+
- Auto-tune `support_scale` based on desired point budget.
|
| 95 |
+
|
| 96 |
+
---
|
| 97 |
+
|
| 98 |
+
**Deliverables**
|
| 99 |
+
1. `reinflate_voxels()` helper with tests.
|
| 100 |
+
2. Config plumbing (env vars & `WorkerSettings`).
|
| 101 |
+
3. Integration hooks + logging of pre/post point counts.
|
| 102 |
+
4. Update docs / README to explain new options.
|
| 103 |
+
|
| 104 |
+
---
|
| 105 |
+
|
| 106 |
+
```
|
requirements.txt
CHANGED
|
@@ -42,6 +42,7 @@ seaborn
|
|
| 42 |
pyglet<2
|
| 43 |
huggingface-hub[torch]>=0.22
|
| 44 |
spaces
|
|
|
|
| 45 |
|
| 46 |
# --------- worker --------- #
|
| 47 |
redis
|
|
|
|
| 42 |
pyglet<2
|
| 43 |
huggingface-hub[torch]>=0.22
|
| 44 |
spaces
|
| 45 |
+
open3d
|
| 46 |
|
| 47 |
# --------- worker --------- #
|
| 48 |
redis
|
stream3r/utils/__pycache__/visual_utils.cpython-311.pyc
CHANGED
|
Binary files a/stream3r/utils/__pycache__/visual_utils.cpython-311.pyc and b/stream3r/utils/__pycache__/visual_utils.cpython-311.pyc differ
|
|
|
stream3r/utils/visual_utils.py
CHANGED
|
@@ -4,14 +4,213 @@
|
|
| 4 |
# This source code is licensed under the license found in the
|
| 5 |
# LICENSE file in the root directory of this source tree.
|
| 6 |
|
| 7 |
-
import
|
| 8 |
-
import numpy as np
|
| 9 |
-
import matplotlib
|
| 10 |
-
from scipy.spatial.transform import Rotation
|
| 11 |
import copy
|
| 12 |
-
import cv2
|
| 13 |
import os
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
import requests
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
|
| 17 |
def predictions_to_glb(
|
|
@@ -26,6 +225,13 @@ def predictions_to_glb(
|
|
| 26 |
prediction_mode="Predicted Pointmap",
|
| 27 |
extra_cameras=None,
|
| 28 |
extra_camera_color=(255, 0, 0),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
) -> trimesh.Scene:
|
| 30 |
"""
|
| 31 |
Converts predictions to a 3D scene represented as a GLB file.
|
|
@@ -47,6 +253,13 @@ def predictions_to_glb(
|
|
| 47 |
extra_cameras (Optional[List[np.ndarray]]): Additional camera extrinsics (3x4 or 4x4)
|
| 48 |
to visualize even when show_cam=False. Useful for highlighting localized poses.
|
| 49 |
extra_camera_color (tuple or list[tuple]): RGB color(s) for extra cameras.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
|
| 51 |
Returns:
|
| 52 |
trimesh.Scene: Processed 3D scene containing point cloud and cameras
|
|
@@ -152,7 +365,27 @@ def predictions_to_glb(
|
|
| 152 |
colors_rgb = images
|
| 153 |
colors_rgb = (colors_rgb.reshape(-1, 3) * 255).astype(np.uint8)
|
| 154 |
|
| 155 |
-
conf = pred_world_points_conf.reshape(-1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
# Convert percentage threshold to actual confidence value
|
| 157 |
if conf_thres == 0.0:
|
| 158 |
conf_threshold = 0.0
|
|
@@ -173,6 +406,48 @@ def predictions_to_glb(
|
|
| 173 |
|
| 174 |
vertices_3d = vertices_3d[conf_mask]
|
| 175 |
colors_rgb = colors_rgb[conf_mask]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 176 |
|
| 177 |
if vertices_3d is None or np.asarray(vertices_3d).size == 0:
|
| 178 |
vertices_3d = np.array([[1, 0, 0]])
|
|
|
|
| 4 |
# This source code is licensed under the license found in the
|
| 5 |
# LICENSE file in the root directory of this source tree.
|
| 6 |
|
| 7 |
+
import logging
|
|
|
|
|
|
|
|
|
|
| 8 |
import copy
|
|
|
|
| 9 |
import os
|
| 10 |
+
|
| 11 |
+
import cv2
|
| 12 |
+
import matplotlib
|
| 13 |
+
import numpy as np
|
| 14 |
import requests
|
| 15 |
+
import trimesh
|
| 16 |
+
from scipy.spatial import cKDTree
|
| 17 |
+
from scipy.spatial.transform import Rotation
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
logger = logging.getLogger(__name__)
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def _srgb_to_linear(colors: np.ndarray) -> np.ndarray:
|
| 24 |
+
colors = np.clip(colors, 0.0, 1.0)
|
| 25 |
+
threshold = 0.04045
|
| 26 |
+
below = colors <= threshold
|
| 27 |
+
linear = np.empty_like(colors, dtype=np.float64)
|
| 28 |
+
linear[below] = colors[below] / 12.92
|
| 29 |
+
linear[~below] = ((colors[~below] + 0.055) / 1.055) ** 2.4
|
| 30 |
+
return linear
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def _linear_to_srgb(colors: np.ndarray) -> np.ndarray:
|
| 34 |
+
colors = np.clip(colors, 0.0, 1.0)
|
| 35 |
+
threshold = 0.0031308
|
| 36 |
+
srgb = np.empty_like(colors, dtype=np.float64)
|
| 37 |
+
below = colors <= threshold
|
| 38 |
+
srgb[below] = colors[below] * 12.92
|
| 39 |
+
srgb[~below] = 1.055 * np.power(colors[~below], 1 / 2.4) - 0.055
|
| 40 |
+
return np.clip(np.round(srgb * 255.0), 0, 255).astype(np.uint8)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def voxel_reduce(
|
| 44 |
+
points_f32: np.ndarray,
|
| 45 |
+
colors_u8: np.ndarray,
|
| 46 |
+
conf_f32: np.ndarray | None = None,
|
| 47 |
+
voxel_size: float = 0.02,
|
| 48 |
+
origin: np.ndarray | None = None,
|
| 49 |
+
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
| 50 |
+
points = np.asarray(points_f32, dtype=np.float32)
|
| 51 |
+
colors = np.asarray(colors_u8, dtype=np.uint8)
|
| 52 |
+
|
| 53 |
+
if points.size == 0:
|
| 54 |
+
return (
|
| 55 |
+
points.reshape(-1, 3).astype(np.float32),
|
| 56 |
+
colors.reshape(-1, 3).astype(np.uint8),
|
| 57 |
+
np.zeros((points.shape[0],), dtype=np.float32),
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
if voxel_size is None or voxel_size <= 0:
|
| 61 |
+
weights = (
|
| 62 |
+
np.asarray(conf_f32, dtype=np.float32).reshape(-1)
|
| 63 |
+
if conf_f32 is not None
|
| 64 |
+
else np.ones(points.shape[0], dtype=np.float32)
|
| 65 |
+
)
|
| 66 |
+
return points.astype(np.float32), colors.astype(np.uint8), weights
|
| 67 |
+
|
| 68 |
+
weights = (
|
| 69 |
+
np.asarray(conf_f32, dtype=np.float32).reshape(-1)
|
| 70 |
+
if conf_f32 is not None
|
| 71 |
+
else np.ones(points.shape[0], dtype=np.float32)
|
| 72 |
+
)
|
| 73 |
+
if weights.shape[0] != points.shape[0]:
|
| 74 |
+
raise ValueError("conf_f32 must match the shape of points.")
|
| 75 |
+
|
| 76 |
+
base = (
|
| 77 |
+
np.asarray(origin, dtype=np.float32)
|
| 78 |
+
if origin is not None
|
| 79 |
+
else points.min(axis=0).astype(np.float32)
|
| 80 |
+
)
|
| 81 |
+
voxel_indices = np.floor((points - base) / voxel_size).astype(np.int64)
|
| 82 |
+
voxel_keys, inverse_indices, counts = np.unique(
|
| 83 |
+
voxel_indices, axis=0, return_inverse=True, return_counts=True
|
| 84 |
+
)
|
| 85 |
+
|
| 86 |
+
reduced_count = voxel_keys.shape[0]
|
| 87 |
+
accum_weights = np.bincount(inverse_indices, weights=weights, minlength=reduced_count)
|
| 88 |
+
accum_weights = np.where(accum_weights <= 0, 1e-6, accum_weights)
|
| 89 |
+
|
| 90 |
+
reduced_points = np.zeros((reduced_count, 3), dtype=np.float64)
|
| 91 |
+
for dim in range(3):
|
| 92 |
+
reduced_points[:, dim] = np.bincount(
|
| 93 |
+
inverse_indices,
|
| 94 |
+
weights=weights * points[:, dim],
|
| 95 |
+
minlength=reduced_count,
|
| 96 |
+
)
|
| 97 |
+
reduced_points /= accum_weights[:, None]
|
| 98 |
+
|
| 99 |
+
colors_linear = _srgb_to_linear(colors.astype(np.float32) / 255.0)
|
| 100 |
+
reduced_colors_linear = np.zeros((reduced_count, 3), dtype=np.float64)
|
| 101 |
+
for dim in range(3):
|
| 102 |
+
reduced_colors_linear[:, dim] = np.bincount(
|
| 103 |
+
inverse_indices,
|
| 104 |
+
weights=weights * colors_linear[:, dim],
|
| 105 |
+
minlength=reduced_count,
|
| 106 |
+
)
|
| 107 |
+
reduced_colors_linear /= accum_weights[:, None]
|
| 108 |
+
|
| 109 |
+
reduced_colors = _linear_to_srgb(reduced_colors_linear)
|
| 110 |
+
support = (
|
| 111 |
+
accum_weights.astype(np.float32)
|
| 112 |
+
if conf_f32 is not None
|
| 113 |
+
else counts.astype(np.float32)
|
| 114 |
+
)
|
| 115 |
+
|
| 116 |
+
return reduced_points.astype(np.float32), reduced_colors.astype(np.uint8), support
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def _filter_by_support(
|
| 120 |
+
points: np.ndarray,
|
| 121 |
+
colors: np.ndarray,
|
| 122 |
+
support: np.ndarray,
|
| 123 |
+
min_support: float | None,
|
| 124 |
+
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
| 125 |
+
if (
|
| 126 |
+
support is None
|
| 127 |
+
or support.size == 0
|
| 128 |
+
or min_support is None
|
| 129 |
+
or min_support <= 0
|
| 130 |
+
):
|
| 131 |
+
return points, colors, support
|
| 132 |
+
|
| 133 |
+
mask = support >= float(min_support)
|
| 134 |
+
if not np.any(mask):
|
| 135 |
+
return points, colors, support
|
| 136 |
+
return points[mask], colors[mask], support[mask]
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
def _log_point_count(stage: str, before: int, after: int) -> None:
|
| 140 |
+
if logger.isEnabledFor(logging.INFO):
|
| 141 |
+
logger.info("Point cloud %s: %d -> %d", stage, before, after)
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
def o3d_outlier_filter(
|
| 145 |
+
points_f32: np.ndarray,
|
| 146 |
+
colors_u8: np.ndarray,
|
| 147 |
+
*,
|
| 148 |
+
voxel_size: float = 0.02,
|
| 149 |
+
radius_mult: float = 3.0,
|
| 150 |
+
nb_points: int = 16,
|
| 151 |
+
nb_neighbors: int = 48,
|
| 152 |
+
std_ratio: float = 1.5,
|
| 153 |
+
) -> tuple[np.ndarray, np.ndarray]:
|
| 154 |
+
points = np.asarray(points_f32, dtype=np.float32)
|
| 155 |
+
colors = np.asarray(colors_u8, dtype=np.uint8)
|
| 156 |
+
|
| 157 |
+
if points.size == 0:
|
| 158 |
+
return points.reshape(-1, 3), colors.reshape(-1, 3)
|
| 159 |
+
|
| 160 |
+
try:
|
| 161 |
+
import open3d as o3d # type: ignore
|
| 162 |
+
except ImportError:
|
| 163 |
+
logger.warning("Open3D not available; skipping outlier filtering.")
|
| 164 |
+
return points.astype(np.float32), colors.astype(np.uint8)
|
| 165 |
+
|
| 166 |
+
pcd = o3d.geometry.PointCloud()
|
| 167 |
+
pcd.points = o3d.utility.Vector3dVector(points.astype(np.float64))
|
| 168 |
+
pcd.colors = o3d.utility.Vector3dVector(colors.astype(np.float32) / 255.0)
|
| 169 |
+
|
| 170 |
+
effective_voxel = float(voxel_size) if voxel_size and voxel_size > 0 else 0.02
|
| 171 |
+
radius = max(float(radius_mult) * effective_voxel, 1e-4)
|
| 172 |
+
|
| 173 |
+
if nb_points > 0:
|
| 174 |
+
pcd, _ = pcd.remove_radius_outlier(nb_points=int(nb_points), radius=radius)
|
| 175 |
+
if len(pcd.points) == 0:
|
| 176 |
+
return np.empty((0, 3), dtype=np.float32), np.empty((0, 3), dtype=np.uint8)
|
| 177 |
+
|
| 178 |
+
if nb_neighbors > 0:
|
| 179 |
+
pcd, _ = pcd.remove_statistical_outlier(
|
| 180 |
+
nb_neighbors=int(nb_neighbors),
|
| 181 |
+
std_ratio=float(std_ratio),
|
| 182 |
+
)
|
| 183 |
+
if len(pcd.points) == 0:
|
| 184 |
+
return np.empty((0, 3), dtype=np.float32), np.empty((0, 3), dtype=np.uint8)
|
| 185 |
+
|
| 186 |
+
filtered_points = np.asarray(pcd.points, dtype=np.float32)
|
| 187 |
+
filtered_colors = np.asarray(pcd.colors, dtype=np.float32)
|
| 188 |
+
filtered_colors = np.clip(np.round(filtered_colors * 255.0), 0, 255).astype(np.uint8)
|
| 189 |
+
|
| 190 |
+
return filtered_points, filtered_colors
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
def density_filter_points(
|
| 194 |
+
points_f32: np.ndarray,
|
| 195 |
+
colors_u8: np.ndarray,
|
| 196 |
+
*,
|
| 197 |
+
radius: float,
|
| 198 |
+
min_neighbors: int,
|
| 199 |
+
) -> tuple[np.ndarray, np.ndarray]:
|
| 200 |
+
points = np.asarray(points_f32, dtype=np.float32)
|
| 201 |
+
colors = np.asarray(colors_u8, dtype=np.uint8)
|
| 202 |
+
|
| 203 |
+
if points.size == 0:
|
| 204 |
+
return points.reshape(-1, 3), colors.reshape(-1, 3)
|
| 205 |
+
|
| 206 |
+
radius = max(float(radius), 1e-4)
|
| 207 |
+
min_neighbors = max(int(min_neighbors), 1)
|
| 208 |
+
|
| 209 |
+
tree = cKDTree(points)
|
| 210 |
+
neighbor_lists = tree.query_ball_point(points, radius)
|
| 211 |
+
mask = np.fromiter((len(nlist) >= min_neighbors for nlist in neighbor_lists), dtype=bool, count=len(neighbor_lists))
|
| 212 |
+
|
| 213 |
+
return points[mask], colors[mask]
|
| 214 |
|
| 215 |
|
| 216 |
def predictions_to_glb(
|
|
|
|
| 225 |
prediction_mode="Predicted Pointmap",
|
| 226 |
extra_cameras=None,
|
| 227 |
extra_camera_color=(255, 0, 0),
|
| 228 |
+
voxel_size: float | None = 0.01,
|
| 229 |
+
voxel_after_conf: bool = True,
|
| 230 |
+
min_voxel_support: float | None = 3,
|
| 231 |
+
o3d_denoise: bool = True,
|
| 232 |
+
o3d_params: dict | None = {"radius_mult": 3.0, "nb_points": 16, "nb_neighbors": 48, "std_ratio": 1.5},
|
| 233 |
+
density_filter: bool = True,
|
| 234 |
+
density_params: dict | None = {"radius": 0.05, "min_neighbors": 6},
|
| 235 |
) -> trimesh.Scene:
|
| 236 |
"""
|
| 237 |
Converts predictions to a 3D scene represented as a GLB file.
|
|
|
|
| 253 |
extra_cameras (Optional[List[np.ndarray]]): Additional camera extrinsics (3x4 or 4x4)
|
| 254 |
to visualize even when show_cam=False. Useful for highlighting localized poses.
|
| 255 |
extra_camera_color (tuple or list[tuple]): RGB color(s) for extra cameras.
|
| 256 |
+
voxel_size (Optional[float]): Size of voxel grid cells (>0 enables reduction).
|
| 257 |
+
voxel_after_conf (bool): Apply voxel reduction after confidence/background filtering.
|
| 258 |
+
min_voxel_support (Optional[float]): Minimum aggregated support (confidence/count) per voxel.
|
| 259 |
+
o3d_denoise (bool): Enable Open3D outlier filtering.
|
| 260 |
+
o3d_params (Optional[dict]): Overrides for Open3D filtering parameters.
|
| 261 |
+
density_filter (bool): Apply KD-tree based density filtering.
|
| 262 |
+
density_params (Optional[dict]): Overrides for density filter parameters.
|
| 263 |
|
| 264 |
Returns:
|
| 265 |
trimesh.Scene: Processed 3D scene containing point cloud and cameras
|
|
|
|
| 365 |
colors_rgb = images
|
| 366 |
colors_rgb = (colors_rgb.reshape(-1, 3) * 255).astype(np.uint8)
|
| 367 |
|
| 368 |
+
conf = pred_world_points_conf.reshape(-1).astype(np.float32)
|
| 369 |
+
|
| 370 |
+
effective_voxel_size = float(voxel_size) if voxel_size is not None else None
|
| 371 |
+
if effective_voxel_size is not None and effective_voxel_size <= 0:
|
| 372 |
+
effective_voxel_size = None
|
| 373 |
+
|
| 374 |
+
if effective_voxel_size is not None and not voxel_after_conf:
|
| 375 |
+
before_count = vertices_3d.shape[0]
|
| 376 |
+
vertices_3d, colors_rgb, conf = voxel_reduce(
|
| 377 |
+
vertices_3d,
|
| 378 |
+
colors_rgb,
|
| 379 |
+
conf,
|
| 380 |
+
voxel_size=effective_voxel_size,
|
| 381 |
+
)
|
| 382 |
+
vertices_3d, colors_rgb, conf = _filter_by_support(
|
| 383 |
+
vertices_3d,
|
| 384 |
+
colors_rgb,
|
| 385 |
+
conf,
|
| 386 |
+
min_voxel_support,
|
| 387 |
+
)
|
| 388 |
+
_log_point_count("voxel_reduce_pre_conf", before_count, vertices_3d.shape[0])
|
| 389 |
# Convert percentage threshold to actual confidence value
|
| 390 |
if conf_thres == 0.0:
|
| 391 |
conf_threshold = 0.0
|
|
|
|
| 406 |
|
| 407 |
vertices_3d = vertices_3d[conf_mask]
|
| 408 |
colors_rgb = colors_rgb[conf_mask]
|
| 409 |
+
conf_used = conf[conf_mask]
|
| 410 |
+
|
| 411 |
+
if effective_voxel_size is not None and voxel_after_conf and vertices_3d.size:
|
| 412 |
+
before_count = vertices_3d.shape[0]
|
| 413 |
+
vertices_3d, colors_rgb, conf_used = voxel_reduce(
|
| 414 |
+
vertices_3d,
|
| 415 |
+
colors_rgb,
|
| 416 |
+
conf_used,
|
| 417 |
+
voxel_size=effective_voxel_size,
|
| 418 |
+
)
|
| 419 |
+
vertices_3d, colors_rgb, conf_used = _filter_by_support(
|
| 420 |
+
vertices_3d,
|
| 421 |
+
colors_rgb,
|
| 422 |
+
conf_used,
|
| 423 |
+
min_voxel_support,
|
| 424 |
+
)
|
| 425 |
+
_log_point_count("voxel_reduce_post_conf", before_count, vertices_3d.shape[0])
|
| 426 |
+
|
| 427 |
+
if o3d_denoise and vertices_3d.size:
|
| 428 |
+
before_count = vertices_3d.shape[0]
|
| 429 |
+
params = {
|
| 430 |
+
"voxel_size": effective_voxel_size or 0.02,
|
| 431 |
+
"radius_mult": 3.0,
|
| 432 |
+
"nb_points": 16,
|
| 433 |
+
"nb_neighbors": 48,
|
| 434 |
+
"std_ratio": 1.5,
|
| 435 |
+
}
|
| 436 |
+
if o3d_params:
|
| 437 |
+
params.update(o3d_params)
|
| 438 |
+
vertices_3d, colors_rgb = o3d_outlier_filter(vertices_3d, colors_rgb, **params)
|
| 439 |
+
_log_point_count("o3d_denoise", before_count, vertices_3d.shape[0])
|
| 440 |
+
|
| 441 |
+
if density_filter and vertices_3d.size:
|
| 442 |
+
before_count = vertices_3d.shape[0]
|
| 443 |
+
params = {
|
| 444 |
+
"radius": (effective_voxel_size or 0.02) * 2.5,
|
| 445 |
+
"min_neighbors": 5,
|
| 446 |
+
}
|
| 447 |
+
if density_params:
|
| 448 |
+
params.update(density_params)
|
| 449 |
+
vertices_3d, colors_rgb = density_filter_points(vertices_3d, colors_rgb, **params)
|
| 450 |
+
_log_point_count("density_filter", before_count, vertices_3d.shape[0])
|
| 451 |
|
| 452 |
if vertices_3d is None or np.asarray(vertices_3d).size == 0:
|
| 453 |
vertices_3d = np.array([[1, 0, 0]])
|
stream3r/worker/config.py
CHANGED
|
@@ -73,7 +73,7 @@ class WorkerSettings:
|
|
| 73 |
gpu_lock_timeout: int = 3600
|
| 74 |
gpu_lock_blocking_timeout: int = 600
|
| 75 |
|
| 76 |
-
storage_prefix: str = "
|
| 77 |
s3_bucket: str | None = None
|
| 78 |
s3_endpoint_url: str | None = None
|
| 79 |
s3_region: str | None = None
|
|
@@ -114,9 +114,10 @@ class WorkerSettings:
|
|
| 114 |
scene_media_api_base_url: str | None = None
|
| 115 |
scene_media_api_token: str | None = None
|
| 116 |
scene_media_page_size: int = 200
|
| 117 |
-
stream_window_size: int =
|
| 118 |
max_frames_per_job: int = 0
|
| 119 |
-
default_job_timeout: int =
|
|
|
|
| 120 |
|
| 121 |
@classmethod
|
| 122 |
def from_env(cls) -> "WorkerSettings":
|
|
@@ -212,6 +213,9 @@ class WorkerSettings:
|
|
| 212 |
"default_job_timeout": _env_int(
|
| 213 |
"STREAM3R_JOB_TIMEOUT", base.default_job_timeout
|
| 214 |
),
|
|
|
|
|
|
|
|
|
|
| 215 |
}
|
| 216 |
|
| 217 |
return cls(**kwargs)
|
|
|
|
| 73 |
gpu_lock_timeout: int = 3600
|
| 74 |
gpu_lock_blocking_timeout: int = 600
|
| 75 |
|
| 76 |
+
storage_prefix: str = "scenes"
|
| 77 |
s3_bucket: str | None = None
|
| 78 |
s3_endpoint_url: str | None = None
|
| 79 |
s3_region: str | None = None
|
|
|
|
| 114 |
scene_media_api_base_url: str | None = None
|
| 115 |
scene_media_api_token: str | None = None
|
| 116 |
scene_media_page_size: int = 200
|
| 117 |
+
stream_window_size: int = 14
|
| 118 |
max_frames_per_job: int = 0
|
| 119 |
+
default_job_timeout: int = 45 * 60
|
| 120 |
+
upload_session_cache: bool = True
|
| 121 |
|
| 122 |
@classmethod
|
| 123 |
def from_env(cls) -> "WorkerSettings":
|
|
|
|
| 213 |
"default_job_timeout": _env_int(
|
| 214 |
"STREAM3R_JOB_TIMEOUT", base.default_job_timeout
|
| 215 |
),
|
| 216 |
+
"upload_session_cache": _env_bool(
|
| 217 |
+
"STREAM3R_UPLOAD_CACHE", base.upload_session_cache
|
| 218 |
+
),
|
| 219 |
}
|
| 220 |
|
| 221 |
return cls(**kwargs)
|
stream3r/worker/tasks.py
CHANGED
|
@@ -15,6 +15,7 @@ from dataclasses import dataclass, field
|
|
| 15 |
from datetime import datetime, timezone
|
| 16 |
from pathlib import Path
|
| 17 |
from contextlib import nullcontext
|
|
|
|
| 18 |
from typing import Any, Callable, Mapping
|
| 19 |
|
| 20 |
import numpy as np
|
|
@@ -471,6 +472,12 @@ def _upload_cache(
|
|
| 471 |
) -> str | None:
|
| 472 |
if cache_path is None or not cache_path.exists():
|
| 473 |
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 474 |
key = runtime.storage.build_key(
|
| 475 |
scene_id,
|
| 476 |
runtime.settings.models_dir,
|
|
@@ -575,7 +582,7 @@ def _save_scene_glb(
|
|
| 575 |
filter_by_frames=payload.get("frame_filter", "All"),
|
| 576 |
mask_black_bg=_as_bool(payload.get("mask_black_bg"), False),
|
| 577 |
mask_white_bg=_as_bool(payload.get("mask_white_bg"), False),
|
| 578 |
-
show_cam=_as_bool(payload.get("show_cam"),
|
| 579 |
mask_sky=_as_bool(payload.get("mask_sky"), False),
|
| 580 |
target_dir=str(temp_dir),
|
| 581 |
prediction_mode=payload.get("prediction_mode", "Predicted Pointmap"),
|
|
@@ -837,6 +844,30 @@ def _execute_job(job_type: str, payload: Mapping[str, Any], handler: JobHandler)
|
|
| 837 |
"scene_id": scene_id,
|
| 838 |
}
|
| 839 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 840 |
runtime.db.upsert_job(
|
| 841 |
job_id=job_id,
|
| 842 |
job_type=job_type,
|
|
@@ -862,6 +893,7 @@ def _execute_job(job_type: str, payload: Mapping[str, Any], handler: JobHandler)
|
|
| 862 |
with tempfile.TemporaryDirectory(prefix=f"stream3r_{job_id}_") as tmp_dir:
|
| 863 |
temp_path = Path(tmp_dir)
|
| 864 |
frame_records = _collect_frames(runtime, scene_id, payload, temp_path)
|
|
|
|
| 865 |
cache_path = temp_path / runtime.settings.session_cache_filename if streaming else None
|
| 866 |
|
| 867 |
tracker = ProgressTracker(runtime, job_meta)
|
|
@@ -874,6 +906,7 @@ def _execute_job(job_type: str, payload: Mapping[str, Any], handler: JobHandler)
|
|
| 874 |
progress_cb=tracker,
|
| 875 |
window_size=window_size if streaming and mode == "window" else None,
|
| 876 |
)
|
|
|
|
| 877 |
|
| 878 |
session_settings = _prepare_session_settings(
|
| 879 |
payload,
|
|
@@ -895,6 +928,7 @@ def _execute_job(job_type: str, payload: Mapping[str, Any], handler: JobHandler)
|
|
| 895 |
session_settings=session_settings,
|
| 896 |
temp_dir=temp_path,
|
| 897 |
)
|
|
|
|
| 898 |
|
| 899 |
except Exception as exc:
|
| 900 |
error_text = traceback.format_exc()
|
|
@@ -914,9 +948,17 @@ def _execute_job(job_type: str, payload: Mapping[str, Any], handler: JobHandler)
|
|
| 914 |
"error": str(exc),
|
| 915 |
},
|
| 916 |
)
|
| 917 |
-
logger.exception(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 918 |
raise
|
| 919 |
|
|
|
|
|
|
|
| 920 |
runtime.db.upsert_job(
|
| 921 |
job_id=job_id,
|
| 922 |
job_type=job_type,
|
|
|
|
| 15 |
from datetime import datetime, timezone
|
| 16 |
from pathlib import Path
|
| 17 |
from contextlib import nullcontext
|
| 18 |
+
from time import perf_counter
|
| 19 |
from typing import Any, Callable, Mapping
|
| 20 |
|
| 21 |
import numpy as np
|
|
|
|
| 472 |
) -> str | None:
|
| 473 |
if cache_path is None or not cache_path.exists():
|
| 474 |
return None
|
| 475 |
+
if not runtime.settings.upload_session_cache:
|
| 476 |
+
logger.debug(
|
| 477 |
+
"Skipping session cache upload for scene %s (disabled via settings)",
|
| 478 |
+
scene_id,
|
| 479 |
+
)
|
| 480 |
+
return None
|
| 481 |
key = runtime.storage.build_key(
|
| 482 |
scene_id,
|
| 483 |
runtime.settings.models_dir,
|
|
|
|
| 582 |
filter_by_frames=payload.get("frame_filter", "All"),
|
| 583 |
mask_black_bg=_as_bool(payload.get("mask_black_bg"), False),
|
| 584 |
mask_white_bg=_as_bool(payload.get("mask_white_bg"), False),
|
| 585 |
+
show_cam=_as_bool(payload.get("show_cam"), False),
|
| 586 |
mask_sky=_as_bool(payload.get("mask_sky"), False),
|
| 587 |
target_dir=str(temp_dir),
|
| 588 |
prediction_mode=payload.get("prediction_mode", "Predicted Pointmap"),
|
|
|
|
| 844 |
"scene_id": scene_id,
|
| 845 |
}
|
| 846 |
|
| 847 |
+
logger.info(
|
| 848 |
+
"Job %s (%s) started for scene %s (timeout=%s)",
|
| 849 |
+
job_id,
|
| 850 |
+
job_type,
|
| 851 |
+
scene_id,
|
| 852 |
+
applied_timeout or desired_timeout or "default",
|
| 853 |
+
)
|
| 854 |
+
|
| 855 |
+
start_time = perf_counter()
|
| 856 |
+
last_time = start_time
|
| 857 |
+
|
| 858 |
+
def log_progress(stage: str) -> None:
|
| 859 |
+
nonlocal last_time
|
| 860 |
+
now = perf_counter()
|
| 861 |
+
logger.info(
|
| 862 |
+
"Job %s (%s): %s [delta=%.2fs total=%.2fs]",
|
| 863 |
+
job_id,
|
| 864 |
+
job_type,
|
| 865 |
+
stage,
|
| 866 |
+
now - last_time,
|
| 867 |
+
now - start_time,
|
| 868 |
+
)
|
| 869 |
+
last_time = now
|
| 870 |
+
|
| 871 |
runtime.db.upsert_job(
|
| 872 |
job_id=job_id,
|
| 873 |
job_type=job_type,
|
|
|
|
| 893 |
with tempfile.TemporaryDirectory(prefix=f"stream3r_{job_id}_") as tmp_dir:
|
| 894 |
temp_path = Path(tmp_dir)
|
| 895 |
frame_records = _collect_frames(runtime, scene_id, payload, temp_path)
|
| 896 |
+
log_progress(f"collected frames ({len(frame_records)} items)")
|
| 897 |
cache_path = temp_path / runtime.settings.session_cache_filename if streaming else None
|
| 898 |
|
| 899 |
tracker = ProgressTracker(runtime, job_meta)
|
|
|
|
| 906 |
progress_cb=tracker,
|
| 907 |
window_size=window_size if streaming and mode == "window" else None,
|
| 908 |
)
|
| 909 |
+
log_progress(f"inference completed ({inference.total_frames} frames)")
|
| 910 |
|
| 911 |
session_settings = _prepare_session_settings(
|
| 912 |
payload,
|
|
|
|
| 928 |
session_settings=session_settings,
|
| 929 |
temp_dir=temp_path,
|
| 930 |
)
|
| 931 |
+
log_progress("artifact generation completed")
|
| 932 |
|
| 933 |
except Exception as exc:
|
| 934 |
error_text = traceback.format_exc()
|
|
|
|
| 948 |
"error": str(exc),
|
| 949 |
},
|
| 950 |
)
|
| 951 |
+
logger.exception(
|
| 952 |
+
"Job %s (%s) failed after %.2fs: %s",
|
| 953 |
+
job_id,
|
| 954 |
+
job_type,
|
| 955 |
+
perf_counter() - start_time,
|
| 956 |
+
exc,
|
| 957 |
+
)
|
| 958 |
raise
|
| 959 |
|
| 960 |
+
log_progress("job finished")
|
| 961 |
+
|
| 962 |
runtime.db.upsert_job(
|
| 963 |
job_id=job_id,
|
| 964 |
job_type=job_type,
|
tests/test_voxel_reduction.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import pytest
|
| 3 |
+
import trimesh
|
| 4 |
+
|
| 5 |
+
from stream3r.utils.visual_utils import (
|
| 6 |
+
density_filter_points,
|
| 7 |
+
o3d_outlier_filter,
|
| 8 |
+
predictions_to_glb,
|
| 9 |
+
voxel_reduce,
|
| 10 |
+
)
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def test_voxel_reduce_merges_points():
|
| 14 |
+
points = np.array(
|
| 15 |
+
[
|
| 16 |
+
[0.0, 0.0, 0.0],
|
| 17 |
+
[0.01, 0.0, 0.0],
|
| 18 |
+
[0.2, 0.0, 0.0],
|
| 19 |
+
],
|
| 20 |
+
dtype=np.float32,
|
| 21 |
+
)
|
| 22 |
+
colors = np.array(
|
| 23 |
+
[
|
| 24 |
+
[255, 0, 0],
|
| 25 |
+
[255, 0, 0],
|
| 26 |
+
[0, 255, 0],
|
| 27 |
+
],
|
| 28 |
+
dtype=np.uint8,
|
| 29 |
+
)
|
| 30 |
+
conf = np.array([1.0, 3.0, 1.0], dtype=np.float32)
|
| 31 |
+
|
| 32 |
+
reduced_points, reduced_colors, support = voxel_reduce(points, colors, conf, voxel_size=0.05)
|
| 33 |
+
|
| 34 |
+
assert reduced_points.shape[0] == 2
|
| 35 |
+
assert reduced_colors.shape[0] == 2
|
| 36 |
+
assert np.isclose(support.sum(), conf.sum(), atol=1e-3)
|
| 37 |
+
merged_idx = int(np.argmin(reduced_points[:, 0]))
|
| 38 |
+
assert pytest.approx(reduced_points[merged_idx, 0], rel=1e-3) == 0.0075
|
| 39 |
+
assert reduced_colors[merged_idx, 0] >= 250
|
| 40 |
+
assert reduced_colors[1 - merged_idx, 1] >= 250
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def test_o3d_outlier_filter_removes_outlier():
|
| 44 |
+
pytest.importorskip("open3d", reason="Open3D not installed")
|
| 45 |
+
|
| 46 |
+
cluster = np.zeros((20, 3), dtype=np.float32)
|
| 47 |
+
outlier = np.array([[1.0, 1.0, 1.0]], dtype=np.float32)
|
| 48 |
+
points = np.vstack([cluster, outlier])
|
| 49 |
+
colors = np.tile(np.array([[128, 128, 255]], dtype=np.uint8), (points.shape[0], 1))
|
| 50 |
+
|
| 51 |
+
filtered_points, _ = o3d_outlier_filter(
|
| 52 |
+
points,
|
| 53 |
+
colors,
|
| 54 |
+
voxel_size=0.05,
|
| 55 |
+
radius_mult=2.5,
|
| 56 |
+
nb_points=4,
|
| 57 |
+
nb_neighbors=8,
|
| 58 |
+
std_ratio=1.0,
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
assert filtered_points.shape[0] < points.shape[0]
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def test_predictions_to_glb_voxel_pipeline():
|
| 65 |
+
world_points = np.array(
|
| 66 |
+
[
|
| 67 |
+
[
|
| 68 |
+
[[0.0, 0.0, 0.0], [0.02, 0.0, 0.0]],
|
| 69 |
+
[[0.0, 0.02, 0.0], [0.02, 0.02, 0.0]],
|
| 70 |
+
]
|
| 71 |
+
],
|
| 72 |
+
dtype=np.float32,
|
| 73 |
+
)
|
| 74 |
+
predictions = {
|
| 75 |
+
"world_points": world_points,
|
| 76 |
+
"world_points_conf": np.ones((1, 2, 2), dtype=np.float32),
|
| 77 |
+
"world_points_from_depth": world_points,
|
| 78 |
+
"depth_conf": np.ones((1, 2, 2), dtype=np.float32),
|
| 79 |
+
"images": np.ones((1, 2, 2, 3), dtype=np.float32) * 0.5,
|
| 80 |
+
"extrinsic": np.array(
|
| 81 |
+
[
|
| 82 |
+
[
|
| 83 |
+
[1.0, 0.0, 0.0, 0.0],
|
| 84 |
+
[0.0, 1.0, 0.0, 0.0],
|
| 85 |
+
[0.0, 0.0, 1.0, 0.0],
|
| 86 |
+
]
|
| 87 |
+
],
|
| 88 |
+
dtype=np.float32,
|
| 89 |
+
),
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
scene = predictions_to_glb(
|
| 93 |
+
predictions,
|
| 94 |
+
conf_thres=0.0,
|
| 95 |
+
voxel_size=0.05,
|
| 96 |
+
o3d_denoise=False,
|
| 97 |
+
density_filter=False,
|
| 98 |
+
)
|
| 99 |
+
|
| 100 |
+
assert isinstance(scene, trimesh.Scene)
|
| 101 |
+
assert len(scene.geometry) >= 1
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def test_density_filter_points_removes_isolated_samples():
|
| 105 |
+
rng = np.random.default_rng(0)
|
| 106 |
+
cluster = rng.normal(scale=0.005, size=(50, 3)).astype(np.float32)
|
| 107 |
+
outliers = np.array([[0.4, 0.4, 0.4], [0.6, 0.6, 0.6]], dtype=np.float32)
|
| 108 |
+
points = np.vstack([cluster, outliers])
|
| 109 |
+
colors = np.tile(np.array([[200, 200, 200]], dtype=np.uint8), (points.shape[0], 1))
|
| 110 |
+
|
| 111 |
+
filtered_points, _ = density_filter_points(points, colors, radius=0.05, min_neighbors=5)
|
| 112 |
+
|
| 113 |
+
assert filtered_points.shape[0] < points.shape[0]
|
| 114 |
+
assert np.all(filtered_points.max(axis=0) < 0.2)
|