Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
012ab737d6 | ||
|
|
2f9aa76478 | ||
|
|
918c882abd | ||
|
|
79a1c2d6f3 | ||
|
|
d77af07189 | ||
|
|
6f8ed9d382 | ||
|
|
d85c2125d1 |
@@ -0,0 +1,34 @@
|
||||
name: Validate workflows & smoke tests
|
||||
|
||||
# Guards against node-schema drift silently breaking the shipped workflows:
|
||||
# validate_workflows.py compares every stored widgets_values against the
|
||||
# current INPUT_TYPES (see docs/workflows_review.md).
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
- name: Install CPU test dependencies
|
||||
run: |
|
||||
pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu
|
||||
pip install numpy pillow scipy tqdm
|
||||
- name: Workflow schema validation
|
||||
run: python notebooks/validate_workflows.py
|
||||
- name: Installer logic tests
|
||||
run: python notebooks/test_install_logic.py
|
||||
- name: 4D node smoke tests
|
||||
run: python notebooks/smoke_test_4d.py
|
||||
@@ -0,0 +1,2 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
@@ -180,6 +180,18 @@ def _uv_grid(height: int, width: int, device: torch.device) -> Tuple[torch.Tenso
|
||||
return u, v
|
||||
|
||||
|
||||
def _uv_in_fov(u: torch.Tensor, v: torch.Tensor, projection: str) -> torch.Tensor:
|
||||
"""True where normalized uv lies inside the projection's actual image region.
|
||||
|
||||
For FISHEYE the [-1,1] square contains the corners beyond the image circle
|
||||
(r > 1, i.e. view angles beyond fov/2); pixels there carry no scene content
|
||||
(black corners / garbage depth) and must not be lifted, warped or tracked.
|
||||
"""
|
||||
if projection == "FISHEYE":
|
||||
return (u * u + v * v) <= 1.0 + 1e-6
|
||||
return torch.ones_like(u, dtype=torch.bool)
|
||||
|
||||
|
||||
def _uv_to_dirs(u: torch.Tensor, v: torch.Tensor, projection: str, horizontal_fov: float) -> torch.Tensor:
|
||||
"""Unit ray directions [...,3] in camera frame for normalized uv in [-1,1].
|
||||
|
||||
@@ -222,7 +234,9 @@ def _project_xyz(
|
||||
def _projection_valid(
|
||||
u: torch.Tensor, v: torch.Tensor, Z: torch.Tensor, projection: str
|
||||
) -> torch.Tensor:
|
||||
"""In-image validity for projected points; pinhole additionally requires Z>0."""
|
||||
"""In-image validity for projected points; pinhole additionally requires Z>0,
|
||||
fisheye requires the point inside the image circle (r <= 1), not just the
|
||||
[-1,1] square — angles beyond fov/2 can otherwise land in the corners."""
|
||||
valid = (
|
||||
torch.isfinite(u)
|
||||
& torch.isfinite(v)
|
||||
@@ -233,6 +247,8 @@ def _projection_valid(
|
||||
)
|
||||
if projection == "PINHOLE":
|
||||
valid = valid & (Z > 1e-6)
|
||||
elif projection == "FISHEYE":
|
||||
valid = valid & ((u * u + v * v) <= 1.0 + 1e-6)
|
||||
return valid
|
||||
|
||||
|
||||
@@ -480,6 +496,7 @@ class MotionMaskFromDepth:
|
||||
|
||||
u, v = _uv_grid(H, W, target_device)
|
||||
dirs = _uv_to_dirs(u, v, input_projection, input_horizontal_fov) # [H,W,3]
|
||||
in_fov = _uv_in_fov(u, v, input_projection) # excludes fisheye corners
|
||||
gap = max(1, int(frame_gap))
|
||||
dynamic = torch.zeros((T, H, W), device=target_device)
|
||||
|
||||
@@ -490,7 +507,7 @@ class MotionMaskFromDepth:
|
||||
tr_t = poses[t, :3, 3]
|
||||
# world = (cam - t) @ R (inverse of cam = world @ R.T + t)
|
||||
world = (cam_pts - tr_t) @ R_t
|
||||
has_depth = depth_t > 1e-6
|
||||
has_depth = (depth_t > 1e-6) & in_fov
|
||||
|
||||
flagged = torch.zeros((H, W), dtype=torch.bool, device=target_device)
|
||||
for t2 in (t + gap, t - gap):
|
||||
@@ -693,7 +710,10 @@ class TracksToTrajectories:
|
||||
# world = (cam - t) @ R per frame.
|
||||
world = torch.bmm(cam - tr.unsqueeze(1), R)
|
||||
|
||||
in_bounds = (u >= -1.0) & (u <= 1.0) & (v >= -1.0) & (v <= 1.0)
|
||||
in_bounds = (
|
||||
(u >= -1.0) & (u <= 1.0) & (v >= -1.0) & (v <= 1.0)
|
||||
& _uv_in_fov(u, v, input_projection)
|
||||
)
|
||||
valid = vis & in_bounds & (d > 1e-6) & torch.isfinite(world).all(dim=-1)
|
||||
world = torch.where(valid.unsqueeze(-1), world, torch.zeros_like(world))
|
||||
|
||||
|
||||
@@ -527,7 +527,8 @@ def _extract_f_rest(data: Dict[str, np.ndarray]) -> Tuple[np.ndarray, int]:
|
||||
def _ensure_sharp_available() -> None:
|
||||
if not _SHARP_AVAILABLE:
|
||||
raise ModuleNotFoundError(
|
||||
f"ml-sharpt is unavailable. Ensure submodules/ml-sharpt is present and its dependencies are installed. "
|
||||
f"ml-sharpt is unavailable. Run this pack's install.py (ComfyUI-Manager does this "
|
||||
f"automatically) to fetch submodules/ml-sharpt and its dependencies (incl. gsplat). "
|
||||
f"Import error: {_SHARP_IMPORT_ERROR}"
|
||||
)
|
||||
|
||||
@@ -1302,6 +1303,12 @@ def render_gaussians(
|
||||
u, v, depth = _xyz_to_equirect(X, Y, Z, camera_horizontal_fov)
|
||||
|
||||
valid = (u >= -1.0) & (u <= 1.0) & (v >= -1.0) & (v <= 1.0)
|
||||
if camera_projection == "PINHOLE":
|
||||
# behind-camera points otherwise mirror-project into the frame
|
||||
valid = valid & (Z > 1e-6)
|
||||
elif camera_projection == "FISHEYE":
|
||||
# keep the image circle only: angles beyond fov/2 land in the corners
|
||||
valid = valid & ((u * u + v * v) <= 1.0 + 1e-6)
|
||||
if not valid.any():
|
||||
return _empty_render(output_width, output_height, dev)
|
||||
|
||||
|
||||
@@ -38,7 +38,17 @@ A collection of ComfyUI custom nodes to handle diverse camera projections (pinho
|
||||
|
||||
### Option A — ComfyUI Manager (recommended)
|
||||
|
||||
The node pack is published to the [ComfyUI Registry](https://registry.comfy.org) as **`camera-comfyui`** (publisher `alexk`). In ComfyUI, open **Manager → Custom Nodes Manager**, search for **camera-comfyUI**, and click **Install**, then restart ComfyUI. The registry package bundles the SHARP submodule and installs the base Python requirements automatically; optional CUDA-specific extras (`gsplat`, `vggt`) still follow the manual steps below.
|
||||
The node pack is published to the [ComfyUI Registry](https://registry.comfy.org) as **`camera-comfyui`** (publisher `alexk`). In ComfyUI, open **Manager → Custom Nodes Manager**, search for **camera-comfyUI**, and click **Install**, then restart ComfyUI.
|
||||
|
||||
Installation is fully automatic: ComfyUI-Manager installs `requirements.txt` and then runs this pack's `install.py`, which sets up everything the optional nodes need — no manual steps:
|
||||
|
||||
* **vggt** (`VideoPoseEstimator`) — pip-installed from GitHub over https (it is not on PyPI).
|
||||
* **SHARP** (`ImageToSplat`, `VideoToFusedSplats`, …) — the `submodules/ml-sharpt` checkout is bundled in the registry package (and fetched via `git submodule`/clone for git installs), and its Python deps come from `requirements.txt`.
|
||||
* **gsplat** — pip-installed; its CUDA kernels JIT-compile on first use.
|
||||
* **ComfyUI-Flux-Inpainting** (`OutpaintAnyProjection`, `SplatTrajectoryEnricher`) — cloned automatically into `custom_nodes/inpainting_flux` (skipped if you already have the pack under any of its usual folder names).
|
||||
* **Example inputs** — the sample image/trajectory files referenced by the bundled workflows are copied into ComfyUI's `input/` folder, so the templates run immediately.
|
||||
|
||||
Each step is optional and non-fatal: if one fails (e.g. no network), only the nodes that need it stay disabled — re-run `python install.py` inside the pack folder to retry.
|
||||
|
||||
> **Maintainers:** releases are automated — bumping `version` in `pyproject.toml` on `main` triggers `.github/workflows/publish_action.yml`, which publishes the new version to the registry (requires the `REGISTRY_ACCESS_TOKEN` repo secret).
|
||||
|
||||
@@ -56,35 +66,29 @@ The node pack is published to the [ComfyUI Registry](https://registry.comfy.org)
|
||||
sudo apt-get update && sudo apt-get install build-essential ffmpeg libsm6 libxext6 -y
|
||||
```
|
||||
|
||||
3. **Python Requirements**:
|
||||
3. **Python Requirements + optional dependencies** — one command sets up everything (base requirements, vggt, the SHARP submodule, gsplat, and the `inpainting_flux` sibling pack):
|
||||
|
||||
```bash
|
||||
pip install -r custom_nodes/camera-comfyUI/requirements.txt
|
||||
cd custom_nodes/camera-comfyUI && python install.py
|
||||
```
|
||||
|
||||
* *Optional:* `open3d` for GUI point cloud tools.
|
||||
This is the same script ComfyUI-Manager runs automatically; it is idempotent, and every optional step is non-fatal.
|
||||
|
||||
**Optional dependencies** (only needed for specific nodes):
|
||||
**What it covers** (for reference — no manual action needed):
|
||||
|
||||
* **gsplat** — CUDA-accelerated Gaussian splat rasterizer. Required by `SplatPolish` and used as the fast render backend for `RenderSplat` / `RenderSplats4D*`. Needs a CUDA GPU and a matching PyTorch build: `pip install gsplat`.
|
||||
* **vggt** — camera pose + depth estimation (`VideoPoseEstimator`). Install with `pip install vggt` (or `pip install git+https://github.com/facebookresearch/vggt.git`), or clone [facebookresearch/vggt](https://github.com/facebookresearch/vggt) as a sibling folder in your ComfyUI root. The `facebook/VGGT-1B` weights (~5 GB) download via `huggingface_hub` on first use.
|
||||
* **CoTracker3** — point tracking for `EstimateTracks`. No manual install: it is fetched automatically via `torch.hub` on first use.
|
||||
* **SHARP** — image→splat prediction (`ImageToSplat`, `FisheyeToGaussian`, `VideoToFusedSplats`, `SplatTrajectoryEnricher`). Ships as the existing git submodule at `submodules/ml-sharpt` ([apple/ml-sharp](https://github.com/apple/ml-sharp)) — run `git submodule update --init` after cloning.
|
||||
* **gsplat** — CUDA-accelerated Gaussian splat rasterizer. Required by `SplatPolish`, SHARP, and the fast render backend for `RenderSplat` / `RenderSplats4D*`. Kernels JIT-compile on first use (needs a CUDA GPU + matching PyTorch build).
|
||||
* **vggt** — camera pose + depth estimation (`VideoPoseEstimator`). Not on PyPI — installed with `pip install git+https://github.com/facebookresearch/vggt.git`. A sibling clone of [facebookresearch/vggt](https://github.com/facebookresearch/vggt) in your ComfyUI root also works. The `facebook/VGGT-1B` weights (~5 GB) download via `huggingface_hub` on first use.
|
||||
* **CoTracker3** — point tracking for `EstimateTracks`. Fetched automatically via `torch.hub` on first use.
|
||||
* **SHARP** — image→splat prediction (`ImageToSplat`, `FisheyeToGaussian`, `VideoToFusedSplats`, `SplatTrajectoryEnricher`). Lives as the git submodule at `submodules/ml-sharpt` ([apple/ml-sharp](https://github.com/apple/ml-sharp)); `install.py` initializes it for you.
|
||||
* **ComfyUI-Flux-Inpainting** — cloned into `custom_nodes/inpainting_flux` if missing. Any of the usual folder names (`inpainting_flux`, `ComfyUI-Flux-Inpainting`, `ComfyUI-Flux-Inpainting-main`) is detected — no renaming needed.
|
||||
|
||||
4. **Additional Nodes** (for certain workflows):
|
||||
4. **Additional Nodes** (only for some example workflows):
|
||||
|
||||
* Clone the following repositories directly into your `custom_nodes` folder:
|
||||
* [ComfyUI-Flux-Inpainting](https://github.com/rubi-du/ComfyUI-Flux-Inpainting)
|
||||
* [ComfyUI-Image-Filters](https://github.com/spacepxl/ComfyUI-Image-Filters)
|
||||
* **Important:** If the `ComfyUI-Flux-Inpainting` repository is cloned as `ComfyUI-Flux-Inpainting-main`, rename the folder to `inpainting_flux`:
|
||||
```bash
|
||||
mv custom_nodes/ComfyUI-Flux-Inpainting-main custom_nodes/inpainting_flux
|
||||
```
|
||||
* [ComfyUI-Image-Filters](https://github.com/spacepxl/ComfyUI-Image-Filters) — install via Manager or clone into `custom_nodes`.
|
||||
|
||||
5. **Flux Models** (Hugging Face):
|
||||
5. **Flux Models** (Hugging Face, only for gated models):
|
||||
|
||||
```bash
|
||||
pip install huggingface_hub
|
||||
huggingface-cli login
|
||||
```
|
||||
|
||||
@@ -205,6 +209,8 @@ Turn a monocular video into a navigable 4D (3D + time) Gaussian splat scene and
|
||||
4. **Tracked dynamic 4D Gaussians** — `EstimateTracks` (CoTracker3) tracks a dense point grid across the video; `TracksToTrajectories` lifts the tracks to world-space 3D using depth + poses; `SplitSplatsByMask` separates dynamic splats from the static background; `BuildSplats4D` binds the dynamic canonical splats to track control points via kNN blending, producing a `GSPLAT4D` scene.
|
||||
5. **Render a novel trajectory** — build any new camera path (e.g. `CameraInterpolationNode`, `TrajectoryCompose` to retarget relative to a source pose) and render with `RenderSplats4DVideo` (or single frames with `RenderSplats4DFrame`). Save/reload scenes with `SaveSplats4D` / `LoadSplats4D`.
|
||||
|
||||
**Static-camera fisheye variant** — for footage from a locked-off 180° fisheye camera, `workflows/fisheye_static_video_to_4d.json` skips pose estimation entirely (identity trajectory), uses the batched `FisheyeDepthEstimator` for per-frame radial depth and `FisheyeToGaussian` on frame 0 for the whole static world, then follows the same track → split → `BuildSplats4D` → render path (all 4D nodes accept the FISHEYE projection directly).
|
||||
|
||||
### Caveats
|
||||
|
||||
* **Z-depth vs ray depth**: depth estimators (including `VideoPoseEstimator`) output Z-depth; point-cloud and splat lifting nodes expect ray depth. Insert `ZDepthToRayDepthNode` where needed, or geometry will bow at wide FOVs.
|
||||
@@ -216,23 +222,31 @@ Turn a monocular video into a navigable 4D (3D + time) Gaussian splat scene and
|
||||
|
||||
## Workflows
|
||||
|
||||
A set of JSON workflows illustrating typical use cases. Each workflow lives in `workflows/` and can be loaded directly in ComfyUI.
|
||||
A set of JSON workflows illustrating typical use cases. Once the pack is installed they appear in ComfyUI under **Workflow → Browse Templates** (with thumbnails); the files live in `workflows/` and can also be loaded directly. Each workflow contains an embedded **“About this workflow”** note in the canvas explaining its stages, what to set, and what it needs — and references the bundled example inputs that `install.py` copies into your ComfyUI `input/` folder, so they run as-is on a fresh install.
|
||||
|
||||
| Workflow | Description |
|
||||
| -------------------------------------- | -------------------------------------------------------------- |
|
||||
| **demo\_camera\_workflow\.json** | Masked reprojection demo: pinhole → fisheye/equirect |
|
||||
| **outpainting\_fisheye.json** | Text‐guided fisheye outpainting (built‐in inpaint node) |
|
||||
| **outpainting\_fisheye\_flux.json** | Flux‐based outpainting with clear reprojection scheme |
|
||||
| **Outpaint\_node\_test.json** | Test harness for the universal outpaint node |
|
||||
| **Outpaint\_fisheye180.json** | 180° fisheye outpainting via `OutpaintAnyProjection` |
|
||||
| **Fisheye\_depth\_workflow\.json** | Fisheye → metric depth → point cloud → PLY export |
|
||||
| **Pointcloud.json** | Metric‐depth‐anything v2 → point cloud → camera view synthesis |
|
||||
| **pointcloud\_inpaint.json** | Inpaint + backproject to 3D for dynamic camera motion videos |
|
||||
| **Pointcloud\_walker.json** | GUI‐based camera control via Open3D |
|
||||
| **sbs180\_workflow.json** | Generate stereo (side-by-side) wide-angle/fisheye/equirectangular stereo pairs from a high-res input |
|
||||
| **video_camera.json** | Camera trajectory movement workflow using `wan-vace` for video inpainting. |
|
||||
| **video_to_4d_world\.json** | Video → 4D world: VGGT poses/depth → motion masking → fused static splats + polish → tracked dynamic 4D Gaussians → novel-trajectory render. |
|
||||
| **video_to_4d_walkable_world\.json** | Video → 4D WALKABLE world (test-friendly defaults): polished static splats enriched along a walk trajectory (`SplatTrajectoryEnricher`, Flux outpaint + SHARP) → 4D scene → walk-through render + `.ply`/`.npz` exports for free walking in external 3DGS viewers. |
|
||||
*Extras* below means dependencies beyond this pack and Depth-Anything V2 (which auto-downloads); `inpainting_flux` is installed automatically by `install.py`.
|
||||
|
||||
| Workflow | Description | Extras |
|
||||
| --- | --- | --- |
|
||||
| **demo\_camera\_workflow\.json** | Minimal demo: rotate the camera and reproject pinhole → equirectangular, with coverage mask | — |
|
||||
| **Outpaint\_node\_test.json** | One-patch smoke test of `OutpaintAnyProjection` | inpainting_flux |
|
||||
| **Outpaint\_fisheye180.json** | Pinhole 90° → full 180° fisheye via five chained `OutpaintAnyProjection` passes + composite/upscale | inpainting_flux |
|
||||
| **outpainting\_fisheye\_flux.json** | Manual version of the above: explicit Flux Inpainting + reprojection stages | inpainting_flux, RealESRGAN |
|
||||
| **fisheye\_to\_pointcloud.json** | Fisheye 180° → metric depth → point cloud (`.ply`/`.npy`) | — |
|
||||
| **PointCloud.json** | Single image → point cloud → cleaned novel-view render | Image-Filters (optional) |
|
||||
| **pointcloud\_walker.json** | Image → point cloud → camera fly-through WEBM | — |
|
||||
| **test\_pointcloud\_loading.json** | Reload a saved point cloud and orbit-render it | — |
|
||||
| **record\_trajectory.json** | Record a camera trajectory `.npy` for LoadTrajectory (two poses → SE(3) interpolation → SaveTrajectory) | — |
|
||||
| **pointcloud\_inpaint.json** | Enrich a cloud: Flux-inpaint disocclusions, lift them to 3D, merge, orbit render | inpainting_flux |
|
||||
| **PC\_enricher.json** | One-node version of the above: `PointcloudTrajectoryEnricher` along a saved trajectory | inpainting_flux |
|
||||
| **sbs180\_workflow.json** | Synthesize the second eye of a VR180 stereo pair from one fisheye view | inpainting_flux |
|
||||
| **video\_camera.json** | Re-shoot a video with a new camera move; WAN VACE regenerates disocclusions, Florence2 auto-captions | VHS, Florence2; WAN 2.1 VACE + Video-Depth-Anything models |
|
||||
| **wan\_vace\_ref\_to\_video.json** | Still fisheye image + recorded trajectory → WAN VACE camera-move video | WAN 2.1 VACE models; VHS (optional MP4 export) |
|
||||
| **video_to_4d_world\.json** | Video → 4D world: VGGT poses/depth → motion masking → fused static splats + polish → tracked dynamic 4D Gaussians → novel-trajectory render. | — |
|
||||
| **video_to_4d_walkable_world\.json** | Video → 4D WALKABLE world (test-friendly defaults): polished static splats enriched along a walk trajectory (`SplatTrajectoryEnricher`, Flux outpaint + SHARP) → 4D scene → walk-through render + `.ply`/`.npz` exports for free walking in external 3DGS viewers. | inpainting_flux |
|
||||
| **fisheye\_static\_video\_to\_4d.json** | Static-camera 180° fisheye video → 4D Gaussian scene → novel-path render. No pose estimation needed: identity trajectory, batched fisheye depth, frame-0 splats split into static world + dynamic canonical. | VHS |
|
||||
|
||||
Superseded reference graphs live in `workflows/legacy/` (kept out of the template browser): **outpainting\_fisheye.json** (SD-inpaint-checkpoint variant of the flux outpaint) and **Fisheye\_depth\_workflow\.json** (the multi-view depth fusion that `FisheyeDepthEstimator` now performs internally).
|
||||
|
||||
---
|
||||
|
||||
@@ -247,9 +261,9 @@ Basic reprojection pipeline: apply masks, rotate pinhole camera, outpaint fishey
|
||||
<img src="demo_images/Pinhole_camera_rotation.png" alt="Pinhole Rotation" width="45%" />
|
||||
</div>
|
||||
|
||||
### 2. `outpainting_fisheye.json`
|
||||
### 2. `legacy/outpainting_fisheye.json`
|
||||
|
||||
Simplest text‐guided fisheye outpainting built with the core inpaint node.
|
||||
Simplest text‐guided fisheye outpainting built with the core inpaint node (superseded by the Flux variant).
|
||||
|
||||
### 3. `outpainting_fisheye_flux.json`
|
||||
|
||||
@@ -265,9 +279,9 @@ Flux Inpainting ensures sharper results and explicit reprojection stages.
|
||||
|
||||
<img src="demo_images/Fisheye_outpainted_flux_dev.png" alt="Flux Dev" width="60%" />
|
||||
|
||||
### 5. `Fisheye_depth_workflow.json`
|
||||
### 5. `legacy/Fisheye_depth_workflow.json`
|
||||
|
||||
Convert fisheye images to metric depth and generate a PLY point cloud.
|
||||
Convert fisheye images to metric depth and generate a PLY point cloud — the manual multi-view graph that `FisheyeDepthEstimator` now performs in one node (see `fisheye_to_pointcloud.json`).
|
||||
|
||||
<img src="demo_images/Depthmap.png" alt="Fisheye Depth→PointCloud" width="60%" />
|
||||
|
||||
@@ -277,7 +291,7 @@ Convert fisheye images to metric depth and generate a PLY point cloud.
|
||||
|
||||
Quick test for the universal outpaint node in arbitrary views and camera movement
|
||||
|
||||
### 7. `Pointcloud.json`
|
||||
### 7. `PointCloud.json`
|
||||
|
||||
Depth→PointCloud pipeline with interactive camera movement and reprojection views.
|
||||
|
||||
@@ -297,9 +311,9 @@ Take a wide-angle (fisheye or equirectangular) high-resolution (e.g., 4096×4096
|
||||
|
||||
<img src="demo_images/equirect_stereo.gif" alt="Equirectangular Stereo Demo" width="80%" />
|
||||
|
||||
### 10. `Pointcloud_walker.json`
|
||||
### 10. `pointcloud_walker.json`
|
||||
|
||||
Interactive Open3D-based GUI for walking and setting camera trajectory inside pointcloud.
|
||||
Image → point cloud → camera fly-through rendered to WEBM (`CameraTrajectoryNode` + `CameraMotionNode`).
|
||||
|
||||
### 11. `video_camera.json`
|
||||
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
# Workflow review — July 2026
|
||||
|
||||
Scope: all 17 `workflows/*.json`. Tooling used (kept in `notebooks/`):
|
||||
|
||||
* `validate_workflows.py` — checks every stored `widgets_values` array against
|
||||
the *current* `INPUT_TYPES` of the node it targets (count + combo values).
|
||||
Run it whenever a node's inputs change.
|
||||
* `rework_workflows_2026_07.py` — the one-shot migration that produced the
|
||||
current state of the files (documented below, idempotent-ish).
|
||||
|
||||
## What was wrong (now fixed)
|
||||
|
||||
ComfyUI applies `widgets_values` **positionally**. When a node gains/loses/
|
||||
reorders widgets, old workflows load with silently shifted values — no error,
|
||||
just wrong settings. The validator found 28 such cases across 9 files:
|
||||
|
||||
| Node | Old → new widgets | Files affected | Migration applied |
|
||||
| --- | --- | --- | --- |
|
||||
| `DepthEstimatorNode` | 2 → 3 (`median_blur_kernel` added) | 5 files, 12 nodes | appended default `1` |
|
||||
| `FisheyeDepthEstimator` | 6 → 8 (`mode`, `median_blur_kernel` added) | 2 files | inserted `SOFTMERGE` (radius was already configured), appended `1` |
|
||||
| `PointCloudCleaner` | 2 → 4 (screen-space `width`/`height` added; units changed) | `PointCloud.json` | reset to defaults `[1024, 1024, 1.0, 3]` — old world-unit values were meaningless in the new schema (**retune on GPU box**) |
|
||||
| `CameraMotionNode` | 6 → 9 (`widen_mask`, `invert_mask`, `points_to_mask`) | 3 files | appended defaults `[0, false, false]` |
|
||||
| `CameraInterpolationNode` | 0 → 1 (`num_steps`) | 3 files | set `2` (keyframes only — `CameraMotionNode`/`VideoCameraMotionSequence` interpolate frames themselves) |
|
||||
| `PointcloudTrajectoryEnricher` | 20 → 16 (render/reproject-back options internalized) | `PC_enricher.json` | first 13 kept 1:1, new tail set to defaults |
|
||||
|
||||
Beyond widget drift:
|
||||
|
||||
* **`outpainting_fisheye.json` was structurally broken**: its seven
|
||||
`ReprojectImage` nodes stored patch rotations (±42°) in a pre-2025 widget
|
||||
slot that now lands on the `inverse` boolean (42 → truthy). Repaired by
|
||||
adding two `TransformToMatrix` nodes (±42°) wired to `transform_matrix`
|
||||
inputs and setting proper `inverse` flags — mirroring the structure of
|
||||
`outpainting_fisheye_flux.json`.
|
||||
* **`outpainting_fisheye_flux.json`** stored `45` in one `inverse` slot
|
||||
(truthy-by-accident); normalized all seven to real booleans.
|
||||
* **`wan_vace_ref_to_video.json`** pointed the UNETLoader at
|
||||
`wan2,1_vace14B_fp16.safetensors` (comma typo + missing underscore) — no such
|
||||
file can exist; fixed to `wan2.1_vace_14B_fp16.safetensors` (matches
|
||||
`install.sh`'s download name).
|
||||
* `Pointcloud walker.json` / `Test pointcloud_loading.json` renamed to
|
||||
`pointcloud_walker.json` / `test_pointcloud_loading.json` (spaces break
|
||||
shell ergonomics and URL linking).
|
||||
* README workflow table was stale (claimed `pointcloud_walker` was an "Open3D
|
||||
GUI", missed 5 workflows); rewritten with per-workflow extras columns.
|
||||
|
||||
Every workflow now carries an embedded **“About this workflow”** MarkdownNote
|
||||
(purpose, stages, what to set, required packs/models), meaningful group boxes,
|
||||
and titles on the nodes users are expected to edit.
|
||||
|
||||
## Improvements — applied 2026-07-16
|
||||
|
||||
Implemented by `notebooks/apply_improvements_2026_07.py` plus repo changes:
|
||||
|
||||
1. **Runnable defaults** ✅ — `example_inputs/` ships
|
||||
`camera_example_pinhole.jpg`, `camera_example_fisheye.jpg` and
|
||||
`ComfyUITrajectory_00001.npy`; `install.py` copies them into ComfyUI's
|
||||
`input/` dir (no overwrite), and every LoadImage/LoadTrajectory default
|
||||
points at them. Exception: `video_camera.json` still needs a user video
|
||||
(none bundled — a clip would bloat the archive).
|
||||
2. **Template browser** ✅ — `workflows/` is already an accepted template
|
||||
directory name (per docs.comfy.org, alongside `example_workflows`), so no
|
||||
rename was needed; added the missing same-name `.jpg` thumbnails (10
|
||||
workflows, generated 512 px from `demo_images/`) and removed the stray
|
||||
`workflows/__init__.py`. Legacy graphs moved to `workflows/legacy/`, which
|
||||
keeps them out of the browser.
|
||||
3. **`video_camera.json` dep trim** ✅ — removed the dead-end `BlurMaskFast`
|
||||
(blur radius was 0/0 — a no-op even if wired), the `easy mathInt` pad
|
||||
computation and the then-dangling `VHS_VideoInfo`; pad top/bottom now use
|
||||
the node's stored values (set to `(W−H)/2`, note explains); swapped
|
||||
KJNodes' `GetImageRangeFromBatch` for the built-in `ImageFromBatch`.
|
||||
Remaining pack deps: VideoHelperSuite + Florence2 (was five packs).
|
||||
4. **Fisheye-outpaint consolidation** ✅ — SD-checkpoint variant moved to
|
||||
`workflows/legacy/outpainting_fisheye.json`.
|
||||
5. **Depth consolidation** ✅ — `workflows/legacy/Fisheye_depth_workflow.json`.
|
||||
6. **Trajectory recorder** ✅ — new `workflows/record_trajectory.json`
|
||||
(TransformToMatrix ×2 → CameraInterpolationNode → SaveTrajectory), and the
|
||||
bundled example trajectory covers the zero-setup path.
|
||||
7. **CI guard** ✅ — `.github/workflows/validate.yml` runs the workflow
|
||||
validator, installer-logic tests and the 4D smoke suite on CPU torch for
|
||||
every PR and push to main.
|
||||
8. **Schema versioning** ✅ — every workflow now carries
|
||||
`extra.camera_comfyui_rev = 1`; bump on the next migration.
|
||||
9. Bonus: a bidirectional link-integrity sweep found and pruned 8 stale link
|
||||
references (pre-existing) plus one dangling `mask` input in
|
||||
`outpainting_fisheye_flux.json`.
|
||||
|
||||
## Still open
|
||||
|
||||
* **Retune migrated defaults on the GPU box.** `PointCloudCleaner` (in
|
||||
`PointCloud.json`) and the voxel-merge tail of `PointcloudTrajectoryEnricher`
|
||||
(in `PC_enricher.json`) were reset to schema defaults; verify visual quality
|
||||
and bake in good values.
|
||||
* **Load-and-queue pass in real ComfyUI.** All checks here are static; open
|
||||
each template once on the GPU box to confirm layout and execution.
|
||||
* **Bundle a small example video** for `video_camera.json` if archive size
|
||||
allows (or document a public sample clip URL in its note).
|
||||
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 196 KiB |
@@ -4,16 +4,52 @@ import numpy as np
|
||||
# reprojection helpers
|
||||
from .reprojection_nodes import Projection, ReprojectImage, TransformToMatrix
|
||||
|
||||
# Try importing FluxInpainting and capture any ImportError
|
||||
import importlib
|
||||
import sys, os, logging
|
||||
here = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
||||
if here not in sys.path:
|
||||
sys.path.append(here)
|
||||
|
||||
# Folder names the ComfyUI-Flux-Inpainting pack may live under in custom_nodes/
|
||||
# (install.py clones it as "inpainting_flux"; keep both lists in sync).
|
||||
_FLUX_PACK_CANDIDATES = (
|
||||
"inpainting_flux",
|
||||
"ComfyUI-Flux-Inpainting",
|
||||
"ComfyUI-Flux-Inpainting-main",
|
||||
"comfyui-flux-inpainting",
|
||||
)
|
||||
|
||||
|
||||
def _import_flux_inpainting():
|
||||
"""Import FluxNF4Inpainting from the flux inpainting pack regardless of the
|
||||
folder name it was installed under."""
|
||||
custom_nodes_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
comfy_root = os.path.dirname(custom_nodes_dir)
|
||||
if comfy_root not in sys.path:
|
||||
sys.path.append(comfy_root)
|
||||
|
||||
last_error = None
|
||||
for name in _FLUX_PACK_CANDIDATES:
|
||||
if not os.path.isfile(os.path.join(custom_nodes_dir, name, "nodes.py")):
|
||||
continue
|
||||
try:
|
||||
module = importlib.import_module(f"custom_nodes.{name}.nodes")
|
||||
return module.FluxNF4Inpainting
|
||||
except Exception as exc:
|
||||
last_error = exc
|
||||
if last_error is None:
|
||||
last_error = ModuleNotFoundError(
|
||||
"No flux inpainting pack found in custom_nodes (looked for "
|
||||
f"{', '.join(_FLUX_PACK_CANDIDATES)}). It is installed automatically "
|
||||
"by this pack's install.py (run by ComfyUI-Manager); to set it up "
|
||||
f"manually, clone {'https://github.com/rubi-du/ComfyUI-Flux-Inpainting'} "
|
||||
"into custom_nodes/inpainting_flux."
|
||||
)
|
||||
raise last_error
|
||||
|
||||
|
||||
# Try importing FluxInpainting and capture any error
|
||||
try:
|
||||
from custom_nodes.inpainting_flux.nodes import FluxNF4Inpainting as FluxInpainting
|
||||
FluxInpainting = _import_flux_inpainting()
|
||||
_flux_import_error = None
|
||||
except ImportError as e:
|
||||
except Exception as e:
|
||||
FluxInpainting = None
|
||||
_flux_import_error = e
|
||||
logging.error(f"[OutpaintAnyProjection] could not import FluxNF4Inpainting: {e}")
|
||||
@@ -71,7 +107,8 @@ class OutpaintAnyProjection:
|
||||
if _flux_import_error is not None:
|
||||
raise RuntimeError(
|
||||
f"FluxNF4Inpainting is not available: {_flux_import_error}\n"
|
||||
"Please install or fix your inpainting_flux package."
|
||||
"Run this pack's install.py (ComfyUI-Manager does this automatically) "
|
||||
"or install/fix custom_nodes/inpainting_flux."
|
||||
)
|
||||
|
||||
def normalize_mask(m: torch.Tensor):
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
"""Post-install setup for camera-comfyUI.
|
||||
|
||||
ComfyUI-Manager runs this script automatically after installing the node pack
|
||||
(both git and registry installs), right after `pip install -r requirements.txt`.
|
||||
Manual users can run it themselves: `python install.py` from this directory.
|
||||
|
||||
It makes the optional heavy dependencies work out of the box:
|
||||
* vggt — pip-installed from GitHub (not on PyPI); needed by VideoPoseEstimator.
|
||||
* SHARP — the submodules/ml-sharpt checkout; needed by ImageToSplat & co.
|
||||
* gsplat — SHARP's rasterizer backend (JIT-compiles CUDA kernels on first use).
|
||||
* inpainting_flux — the ComfyUI-Flux-Inpainting node pack, cloned as a sibling
|
||||
custom node; needed by OutpaintAnyProjection / SplatTrajectoryEnricher.
|
||||
|
||||
Every step is idempotent and non-fatal: the node pack degrades gracefully at
|
||||
runtime (see __init__.py), so a failed optional step only disables the nodes
|
||||
that need it. The script therefore always exits 0 and reports what it skipped.
|
||||
"""
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
NODE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
LOG_PREFIX = "[camera-comfyUI install]"
|
||||
|
||||
VGGT_GIT_URL = "https://github.com/facebookresearch/vggt.git"
|
||||
SHARP_GIT_URL = "https://github.com/apple/ml-sharp"
|
||||
FLUX_PACK_GIT_URL = "https://github.com/rubi-du/ComfyUI-Flux-Inpainting.git"
|
||||
# Folder names under custom_nodes/ that count as "the flux inpainting pack is
|
||||
# already installed" (must stay in sync with flux_fisheye_filling_nodes.py).
|
||||
FLUX_PACK_CANDIDATES = (
|
||||
"inpainting_flux",
|
||||
"ComfyUI-Flux-Inpainting",
|
||||
"ComfyUI-Flux-Inpainting-main",
|
||||
"comfyui-flux-inpainting",
|
||||
)
|
||||
|
||||
|
||||
def _log(message: str) -> None:
|
||||
print(f"{LOG_PREFIX} {message}", flush=True)
|
||||
|
||||
|
||||
def _run(cmd, cwd=None) -> None:
|
||||
_log("$ " + " ".join(cmd))
|
||||
subprocess.check_call(cmd, cwd=cwd)
|
||||
|
||||
|
||||
def _pip_install(*args: str) -> None:
|
||||
_run([sys.executable, "-m", "pip", "install", *args])
|
||||
|
||||
|
||||
def _git() -> str:
|
||||
git = shutil.which("git")
|
||||
if git is None:
|
||||
raise RuntimeError(
|
||||
"git executable not found on PATH; cannot fetch GitHub dependencies"
|
||||
)
|
||||
return git
|
||||
|
||||
|
||||
def _importable(module_name: str) -> bool:
|
||||
import importlib.util
|
||||
|
||||
try:
|
||||
return importlib.util.find_spec(module_name) is not None
|
||||
except (ImportError, ValueError):
|
||||
return False
|
||||
|
||||
|
||||
def ensure_base_requirements() -> None:
|
||||
"""Install requirements.txt if it clearly has not been installed yet.
|
||||
|
||||
ComfyUI-Manager installs it before running this script, so this only fires
|
||||
for manual `python install.py` users.
|
||||
"""
|
||||
if _importable("diffusers") and _importable("transformers"):
|
||||
_log("base requirements already satisfied")
|
||||
return
|
||||
_pip_install("-r", os.path.join(NODE_DIR, "requirements.txt"))
|
||||
|
||||
|
||||
def ensure_vggt() -> None:
|
||||
"""VGGT (VideoPoseEstimator). Not on PyPI — install from GitHub over https."""
|
||||
if _importable("vggt"):
|
||||
_log("vggt already installed")
|
||||
return
|
||||
_pip_install(f"vggt @ git+{VGGT_GIT_URL}")
|
||||
_log("vggt installed from GitHub")
|
||||
|
||||
|
||||
def ensure_sharp_checkout() -> None:
|
||||
"""Materialize the SHARP submodule (ImageToSplat / VideoToFusedSplats).
|
||||
|
||||
Registry archives already bundle it; git installs need `submodule update`
|
||||
(ComfyUI-Manager does not clone recursively). If this directory is not a
|
||||
git checkout at all, fall back to a direct clone.
|
||||
"""
|
||||
sharp_dir = os.path.join(NODE_DIR, "submodules", "ml-sharpt")
|
||||
sharp_src = os.path.join(sharp_dir, "src", "sharp")
|
||||
if os.path.isdir(sharp_src):
|
||||
_log("SHARP checkout already present")
|
||||
return
|
||||
|
||||
git = _git()
|
||||
if os.path.exists(os.path.join(NODE_DIR, ".git")):
|
||||
_run([git, "submodule", "update", "--init", "--recursive"], cwd=NODE_DIR)
|
||||
if os.path.isdir(sharp_src):
|
||||
_log("SHARP submodule initialized")
|
||||
return
|
||||
|
||||
if os.path.isdir(sharp_dir) and os.listdir(sharp_dir):
|
||||
raise RuntimeError(
|
||||
f"{sharp_dir} exists but does not contain src/sharp; "
|
||||
"remove it and re-run install.py"
|
||||
)
|
||||
_run([git, "clone", "--depth", "1", SHARP_GIT_URL, sharp_dir])
|
||||
_log("SHARP cloned from GitHub")
|
||||
|
||||
|
||||
def ensure_gsplat() -> None:
|
||||
"""gsplat backs SHARP's import chain and the fast splat render path.
|
||||
|
||||
pip install is lightweight (CUDA kernels JIT-compile on first use), but it
|
||||
can still fail on exotic setups — that only disables the SHARP/gsplat nodes.
|
||||
"""
|
||||
if _importable("gsplat"):
|
||||
_log("gsplat already installed")
|
||||
return
|
||||
_pip_install("gsplat")
|
||||
_log("gsplat installed")
|
||||
|
||||
|
||||
def ensure_flux_inpainting_pack() -> None:
|
||||
"""Clone ComfyUI-Flux-Inpainting next to this pack if no copy exists yet."""
|
||||
custom_nodes_dir = os.path.dirname(NODE_DIR)
|
||||
if os.path.basename(custom_nodes_dir).lower() != "custom_nodes":
|
||||
_log(
|
||||
"not installed under a ComfyUI custom_nodes directory; "
|
||||
"skipping ComfyUI-Flux-Inpainting setup"
|
||||
)
|
||||
return
|
||||
|
||||
for name in FLUX_PACK_CANDIDATES:
|
||||
if os.path.isdir(os.path.join(custom_nodes_dir, name)):
|
||||
_log(f"flux inpainting pack already present ({name})")
|
||||
return
|
||||
|
||||
git = _git()
|
||||
target = os.path.join(custom_nodes_dir, "inpainting_flux")
|
||||
_run([git, "clone", "--depth", "1", FLUX_PACK_GIT_URL, target])
|
||||
pack_requirements = os.path.join(target, "requirements.txt")
|
||||
if os.path.isfile(pack_requirements):
|
||||
_pip_install("-r", pack_requirements)
|
||||
_log("ComfyUI-Flux-Inpainting installed as custom_nodes/inpainting_flux")
|
||||
|
||||
|
||||
def ensure_example_inputs() -> None:
|
||||
"""Copy bundled example inputs into ComfyUI's input dir (no overwrite).
|
||||
|
||||
The shipped example workflows reference these files, so a fresh install
|
||||
can queue them immediately.
|
||||
"""
|
||||
src_dir = os.path.join(NODE_DIR, "example_inputs")
|
||||
if not os.path.isdir(src_dir):
|
||||
_log("no example_inputs directory; skipping")
|
||||
return
|
||||
custom_nodes_dir = os.path.dirname(NODE_DIR)
|
||||
if os.path.basename(custom_nodes_dir).lower() != "custom_nodes":
|
||||
_log("not installed under a ComfyUI custom_nodes directory; "
|
||||
"skipping example input setup")
|
||||
return
|
||||
input_dir = os.path.join(os.path.dirname(custom_nodes_dir), "input")
|
||||
os.makedirs(input_dir, exist_ok=True)
|
||||
copied = 0
|
||||
for name in os.listdir(src_dir):
|
||||
target = os.path.join(input_dir, name)
|
||||
if not os.path.exists(target):
|
||||
shutil.copy2(os.path.join(src_dir, name), target)
|
||||
copied += 1
|
||||
_log(f"example inputs ready ({copied} file(s) copied to {input_dir})")
|
||||
|
||||
|
||||
STEPS = (
|
||||
("base requirements", ensure_base_requirements),
|
||||
("vggt (VideoPoseEstimator)", ensure_vggt),
|
||||
("SHARP checkout (ImageToSplat)", ensure_sharp_checkout),
|
||||
("gsplat (splat rasterizer)", ensure_gsplat),
|
||||
("ComfyUI-Flux-Inpainting (OutpaintAnyProjection)", ensure_flux_inpainting_pack),
|
||||
("example workflow inputs", ensure_example_inputs),
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
failures = []
|
||||
for title, step in STEPS:
|
||||
_log(f"--- {title} ---")
|
||||
try:
|
||||
step()
|
||||
except Exception as exc: # keep going: each dependency is optional
|
||||
failures.append((title, exc))
|
||||
_log(f"WARNING: {title} failed: {exc}")
|
||||
|
||||
if failures:
|
||||
_log("finished with warnings - the affected optional nodes stay disabled:")
|
||||
for title, exc in failures:
|
||||
_log(f" * {title}: {exc}")
|
||||
_log("re-run `python install.py` after fixing the issue (network/git/pip)")
|
||||
else:
|
||||
_log("all optional dependencies are ready")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -29,6 +29,9 @@ install_camera_node() {
|
||||
git clone https://github.com/Alexankharin/camera-comfyUI.git \
|
||||
ComfyUI/custom_nodes/camera-comfyUI
|
||||
pip3 install -r ComfyUI/custom_nodes/camera-comfyUI/requirements.txt
|
||||
# install.py sets up vggt, the SHARP submodule, gsplat and the
|
||||
# inpainting_flux sibling pack (same script ComfyUI-Manager runs).
|
||||
( cd ComfyUI/custom_nodes/camera-comfyUI && python3 install.py )
|
||||
}
|
||||
|
||||
install_image_filters() {
|
||||
@@ -133,7 +136,6 @@ case "$MODE" in
|
||||
install_system_deps
|
||||
clone_and_install_comfyui
|
||||
install_camera_node
|
||||
clone_flux_inpainting
|
||||
install_image_filters
|
||||
install_comfyui_manager
|
||||
install_hf_hub
|
||||
@@ -145,7 +147,6 @@ case "$MODE" in
|
||||
install_system_deps
|
||||
clone_and_install_comfyui
|
||||
install_camera_node
|
||||
clone_flux_inpainting
|
||||
install_image_filters
|
||||
install_comfyui_manager
|
||||
install_hf_hub
|
||||
|
||||
@@ -0,0 +1,287 @@
|
||||
"""Second July-2026 workflow pass: apply the improvements proposed in
|
||||
docs/workflows_review.md.
|
||||
|
||||
1. Point every LoadImage at the bundled example inputs (example_inputs/ is
|
||||
copied into ComfyUI's input dir by install.py), so workflows run on a
|
||||
fresh install without hunting for files.
|
||||
2. video_camera.json dependency trim: drop the dead-end BlurMaskFast
|
||||
(Image-Filters), the easy-mathInt pad computation (Easy-Use) and the now
|
||||
dangling VHS_VideoInfo; swap KJNodes' GetImageRangeFromBatch for the
|
||||
built-in ImageFromBatch. Remaining pack deps: VHS + Florence2.
|
||||
3. Create workflows/record_trajectory.json (TransformToMatrix x2 ->
|
||||
CameraInterpolationNode -> SaveTrajectory) - the missing producer for the
|
||||
trajectory files PC_enricher / wan_vace_ref_to_video consume.
|
||||
4. Stamp `extra.camera_comfyui_rev = 1` in every workflow for future
|
||||
migrations.
|
||||
|
||||
Run: python notebooks/apply_improvements_2026_07.py
|
||||
"""
|
||||
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
|
||||
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
WF = os.path.join(REPO, "workflows")
|
||||
|
||||
PINHOLE = "camera_example_pinhole.jpg"
|
||||
FISHEYE = "camera_example_fisheye.jpg"
|
||||
|
||||
# workflow file -> example image for its LoadImage node(s)
|
||||
LOADIMAGE_DEFAULTS = {
|
||||
"demo_camera_workflow.json": PINHOLE,
|
||||
"Outpaint_node_test.json": PINHOLE,
|
||||
"Outpaint_fisheye180.json": PINHOLE,
|
||||
"outpainting_fisheye_flux.json": PINHOLE,
|
||||
"legacy/outpainting_fisheye.json": PINHOLE,
|
||||
"PointCloud.json": PINHOLE,
|
||||
"pointcloud_walker.json": PINHOLE,
|
||||
"pointcloud_inpaint.json": PINHOLE,
|
||||
"fisheye_to_pointcloud.json": FISHEYE,
|
||||
"legacy/Fisheye_depth_workflow.json": FISHEYE,
|
||||
"PC_enricher.json": FISHEYE,
|
||||
"sbs180_workflow.json": FISHEYE,
|
||||
"wan_vace_ref_to_video.json": FISHEYE,
|
||||
}
|
||||
|
||||
|
||||
def load(rel):
|
||||
with open(os.path.join(WF, rel), encoding="utf-8") as fh:
|
||||
return json.load(fh)
|
||||
|
||||
|
||||
def save(rel, data):
|
||||
with open(os.path.join(WF, rel), "w", encoding="utf-8", newline="\n") as fh:
|
||||
json.dump(data, fh, indent=2, ensure_ascii=False)
|
||||
fh.write("\n")
|
||||
|
||||
|
||||
def node(data, nid):
|
||||
for n in data["nodes"]:
|
||||
if n["id"] == nid:
|
||||
return n
|
||||
raise KeyError(nid)
|
||||
|
||||
|
||||
def set_example_inputs():
|
||||
for rel, image in LOADIMAGE_DEFAULTS.items():
|
||||
d = load(rel)
|
||||
changed = 0
|
||||
for n in d["nodes"]:
|
||||
if n["type"] == "LoadImage":
|
||||
n["widgets_values"][0] = image
|
||||
changed += 1
|
||||
assert changed, f"{rel}: no LoadImage found"
|
||||
save(rel, d)
|
||||
print(f"[ok] {rel}: {changed} LoadImage -> {image}")
|
||||
|
||||
|
||||
def remove_node(data, nid):
|
||||
"""Remove a node and every link touching it; re-index dst slots."""
|
||||
n = node(data, nid)
|
||||
dead = set()
|
||||
for inp in n.get("inputs") or []:
|
||||
if inp.get("link") is not None:
|
||||
dead.add(inp["link"])
|
||||
for out in n.get("outputs") or []:
|
||||
dead.update(out.get("links") or [])
|
||||
data["nodes"] = [x for x in data["nodes"] if x["id"] != nid]
|
||||
data["links"] = [l for l in data["links"] if l[0] not in dead]
|
||||
for x in data["nodes"]:
|
||||
for inp in x.get("inputs") or []:
|
||||
if inp.get("link") in dead:
|
||||
inp["link"] = None
|
||||
for out in x.get("outputs") or []:
|
||||
if out.get("links"):
|
||||
out["links"] = [l for l in out["links"] if l not in dead]
|
||||
|
||||
|
||||
def drop_input(data, nid, name):
|
||||
"""Remove a (widget-converted) input socket and fix slot indices."""
|
||||
n = node(data, nid)
|
||||
inputs = n.get("inputs") or []
|
||||
n["inputs"] = [i for i in inputs if i["name"] != name]
|
||||
index = {i["name"]: k for k, i in enumerate(n["inputs"])}
|
||||
for l in data["links"]:
|
||||
if l[3] == nid:
|
||||
# find by link id which input holds it
|
||||
for i in n["inputs"]:
|
||||
if i.get("link") == l[0]:
|
||||
l[4] = index[i["name"]]
|
||||
|
||||
|
||||
def bbox(data, ids, pad_top=60, pad=20):
|
||||
xs, ys, xe, ye = [], [], [], []
|
||||
for nid in ids:
|
||||
n = node(data, nid)
|
||||
p, s = n["pos"], n["size"]
|
||||
xs.append(p[0]); ys.append(p[1])
|
||||
xe.append(p[0] + s[0]); ye.append(p[1] + s[1])
|
||||
x, y = min(xs) - pad, min(ys) - pad_top
|
||||
return [x, y, max(xe) + pad - x, max(ye) + pad - y]
|
||||
|
||||
|
||||
def fix_video_camera(rel="video_camera.json"):
|
||||
d = load(rel)
|
||||
# 1. dead-end mask blur (only Image-Filters usage; radius was 0/0 anyway)
|
||||
remove_node(d, 60)
|
||||
# 2. pad computation chain (Easy-Use mathInt x2 + VHS_VideoInfo feeding it);
|
||||
# ImagePadForOutpaint falls back to its stored widgets (280/280)
|
||||
for nid in (27, 26, 23):
|
||||
remove_node(d, nid)
|
||||
for name in ("top", "bottom"):
|
||||
drop_input(d, 21, name)
|
||||
n21 = node(d, 21)
|
||||
n21["title"] = "Pad to square — set top/bottom to (W−H)/2"
|
||||
# 3. KJNodes GetImageRangeFromBatch -> built-in ImageFromBatch (frame 0)
|
||||
n63 = node(d, 63)
|
||||
n63["type"] = "ImageFromBatch"
|
||||
n63["properties"]["Node name for S&R"] = "ImageFromBatch"
|
||||
n63["inputs"][0]["name"] = "image"
|
||||
n63["widgets_values"] = [0, 1] # batch_index, length
|
||||
n63["title"] = "First frame (for captioning)"
|
||||
# regroup without the removed nodes
|
||||
groups = [
|
||||
("1. Load & pad video", [9, 21, 68]),
|
||||
("2. Metric video depth", [19, 12]),
|
||||
("3. Re-render with new camera", [16, 11, 10, 6]),
|
||||
("4. Masks & composite", [39, 41, 75, 54, 74]),
|
||||
("5. Auto-caption (Florence2)", [63, 62, 61]),
|
||||
("6. WAN VACE re-generation", [29, 30, 37, 32, 33, 34, 28, 31, 36, 35]),
|
||||
("7. Outputs", [4, 13, 14, 38]),
|
||||
]
|
||||
colors = ["#3f789e", "#a1309b", "#8A8", "#b58b2a", "#88A", "#b06634", "#535"]
|
||||
d["groups"] = [{
|
||||
"id": i + 1, "title": t, "bounding": bbox(d, ids),
|
||||
"color": colors[i % len(colors)], "font_size": 24, "flags": {},
|
||||
} for i, (t, ids) in enumerate(groups)]
|
||||
# refresh the note (dependency list changed)
|
||||
for n in d["nodes"]:
|
||||
if n["type"] == "MarkdownNote" and n.get("title") == "About this workflow":
|
||||
n["widgets_values"] = [(
|
||||
"# Re-shoot a video with a new camera move\n\n"
|
||||
"Re-renders an input video along a user-defined camera "
|
||||
"trajectory and uses WAN 2.1 VACE to regenerate what the new "
|
||||
"camera reveals:\n\n"
|
||||
"1. Video is padded square (ImagePadForOutpaint — set "
|
||||
"top/bottom to (width−height)/2 for your video) and "
|
||||
"depth-estimated per frame (Video-Depth-Anything metric).\n"
|
||||
"2. **VideoCameraMotionSequence** lifts each frame to a point "
|
||||
"cloud and re-renders it along the SE(3)-interpolated "
|
||||
"trajectory.\n"
|
||||
"3. Disocclusion masks + re-rendered frames become VACE "
|
||||
"control video/masks; **Florence2** auto-captions the clip as "
|
||||
"the prompt; WAN 2.1 VACE 14B fills the gaps.\n\n"
|
||||
"- **Set:** video path (VHS Load Video Path); the camera move "
|
||||
"(two TransformToMatrix poses); pad amounts; override the "
|
||||
"auto-caption in CLIPTextEncode if desired.\n"
|
||||
"- **Requires (packs):** VideoHelperSuite, ComfyUI-Florence2.\n"
|
||||
"- **Requires (models):** `metric_video_depth_anything_vitl"
|
||||
".pth` (install.sh `depth`), WAN 2.1 VACE 14B + umt5-xxl + "
|
||||
"WAN VAE (install.sh `vae`), Florence-2 (auto-download).\n"
|
||||
"- **Outputs:** re-rendered composite WEBM, depth WEBM, final "
|
||||
"VACE clip."
|
||||
)]
|
||||
save(rel, d)
|
||||
print(f"[ok] {rel}: removed BlurMaskFast/mathInt/VideoInfo, "
|
||||
f"ImageFromBatch swap, regrouped")
|
||||
|
||||
|
||||
def make_record_trajectory(rel="record_trajectory.json"):
|
||||
note = (
|
||||
"# Record a camera trajectory\n\n"
|
||||
"Produces the `.npy` trajectory file consumed by `PC_enricher.json` "
|
||||
"and `wan_vace_ref_to_video.json` (LoadTrajectory): two poses are "
|
||||
"SE(3)-interpolated into a smooth 20-step path and saved by "
|
||||
"**SaveTrajectory** to your ComfyUI **output** directory.\n\n"
|
||||
"- **Set:** the end pose (shift XYZ in scene units — metric if the "
|
||||
"cloud came from metric depth — plus theta = pitch, phi = yaw) and "
|
||||
"`num_steps`.\n"
|
||||
"- **Then:** move the saved file from `output/` to `input/` so "
|
||||
"LoadTrajectory can list it. A bundled example "
|
||||
"(`ComfyUITrajectory_00001.npy`) is already installed by install.py.\n"
|
||||
"- Chain more CameraInterpolationNode segments (or use "
|
||||
"CameraTrajectoryNode on a point cloud) for multi-keyframe paths."
|
||||
)
|
||||
d = {
|
||||
"id": "00000000-0000-0000-0000-000000000000",
|
||||
"revision": 0,
|
||||
"last_node_id": 5,
|
||||
"last_link_id": 3,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 1, "type": "TransformToMatrix", "title": "Start pose (identity)",
|
||||
"pos": [-500, 320], "size": [315, 154], "flags": {}, "order": 0,
|
||||
"mode": 0, "inputs": [],
|
||||
"outputs": [{"name": "transformation matrix", "type": "MAT_4X4",
|
||||
"links": [1], "slot_index": 0}],
|
||||
"properties": {"Node name for S&R": "TransformToMatrix"},
|
||||
"widgets_values": [0.0, 0.0, 0.0, 0.0, 0.0],
|
||||
},
|
||||
{
|
||||
"id": 2, "type": "TransformToMatrix", "title": "End pose (edit me)",
|
||||
"pos": [-500, 540], "size": [315, 154], "flags": {}, "order": 1,
|
||||
"mode": 0, "inputs": [],
|
||||
"outputs": [{"name": "transformation matrix", "type": "MAT_4X4",
|
||||
"links": [2], "slot_index": 0}],
|
||||
"properties": {"Node name for S&R": "TransformToMatrix"},
|
||||
"widgets_values": [0.0, 0.0, 0.3, 0.0, 30.0],
|
||||
},
|
||||
{
|
||||
"id": 3, "type": "CameraInterpolationNode",
|
||||
"title": "Interpolate 20 poses",
|
||||
"pos": [-120, 430], "size": [226, 78], "flags": {}, "order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{"name": "initial_matrix", "type": "MAT_4X4", "link": 1},
|
||||
{"name": "final_matrix", "type": "MAT_4X4", "link": 2},
|
||||
],
|
||||
"outputs": [{"name": "trajectory", "type": "TENSOR",
|
||||
"links": [3], "slot_index": 0}],
|
||||
"properties": {"Node name for S&R": "CameraInterpolationNode"},
|
||||
"widgets_values": [20],
|
||||
},
|
||||
{
|
||||
"id": 4, "type": "SaveTrajectory", "title": "Save to output/*.npy",
|
||||
"pos": [170, 430], "size": [315, 82], "flags": {}, "order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [{"name": "trajectory", "type": "TENSOR", "link": 3}],
|
||||
"outputs": [],
|
||||
"properties": {"Node name for S&R": "SaveTrajectory"},
|
||||
"widgets_values": ["ComfyUITrajectory"],
|
||||
},
|
||||
{
|
||||
"id": 5, "type": "MarkdownNote", "title": "About this workflow",
|
||||
"pos": [-1080, 320], "size": [520, 430], "flags": {}, "order": 4,
|
||||
"mode": 0, "inputs": [], "outputs": [], "properties": {},
|
||||
"widgets_values": [note], "color": "#432", "bgcolor": "#653",
|
||||
},
|
||||
],
|
||||
"links": [
|
||||
[1, 1, 0, 3, 0, "MAT_4X4"],
|
||||
[2, 2, 0, 3, 1, "MAT_4X4"],
|
||||
[3, 3, 0, 4, 0, "TENSOR"],
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {},
|
||||
"version": 0.4,
|
||||
}
|
||||
save(rel, d)
|
||||
print(f"[ok] created {rel}")
|
||||
|
||||
|
||||
def stamp_revision():
|
||||
for path in glob.glob(os.path.join(WF, "**", "*.json"), recursive=True):
|
||||
rel = os.path.relpath(path, WF)
|
||||
d = load(rel)
|
||||
d.setdefault("extra", {})["camera_comfyui_rev"] = 1
|
||||
save(rel, d)
|
||||
print("[ok] stamped extra.camera_comfyui_rev = 1")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
set_example_inputs()
|
||||
fix_video_camera()
|
||||
make_record_trajectory()
|
||||
stamp_revision()
|
||||
@@ -0,0 +1,294 @@
|
||||
"""Build workflows/fisheye_static_video_to_4d.json.
|
||||
|
||||
Static-fisheye-camera variant of video_to_4d_world.json: because the camera
|
||||
does not move, no VGGT pose estimation is needed — the trajectory is identity,
|
||||
per-frame depth comes from the batched FisheyeDepthEstimator, and the whole
|
||||
static background is a single FisheyeToGaussian prediction of frame 0 split by
|
||||
the motion mask (outside = static world, inside = dynamic canonical).
|
||||
|
||||
Node facts this graph relies on (verified against current INPUT_TYPES):
|
||||
- FisheyeDepthEstimator is batched: IMAGE [T,H,W,C] -> depthmap [T,H,W,1];
|
||||
GS4D nodes squeeze the trailing channel (GS4D_nodes.py::167).
|
||||
- MotionMaskFromDepth interpolates a [K,4,4] trajectory to T internally.
|
||||
- TracksToTrajectories' trajectory input is optional and defaults to identity
|
||||
("static camera" per its tooltip) — left unconnected on purpose.
|
||||
- SplitSplatsByMask returns (inside_splats, outside_splats); its optional
|
||||
camera_matrix defaults to identity, which is exactly the frame-0 camera here.
|
||||
|
||||
Run: python notebooks/build_fisheye_static_4d.py
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
OUT = os.path.join(REPO, "workflows", "fisheye_static_video_to_4d.json")
|
||||
|
||||
NODES = []
|
||||
LINKS = []
|
||||
_link_id = 0
|
||||
|
||||
|
||||
def node(nid, ntype, title, pos, size, widgets, inputs=(), outputs=(), mode=0,
|
||||
extra=None):
|
||||
n = {
|
||||
"id": nid, "type": ntype, "pos": list(pos), "size": list(size),
|
||||
"flags": {}, "order": len(NODES), "mode": mode,
|
||||
"inputs": [dict(i) for i in inputs],
|
||||
"outputs": [
|
||||
{"name": o[0], "type": o[1], "links": [], "slot_index": k}
|
||||
for k, o in enumerate(outputs)
|
||||
],
|
||||
"properties": {"Node name for S&R": ntype},
|
||||
"widgets_values": widgets,
|
||||
}
|
||||
if title:
|
||||
n["title"] = title
|
||||
if extra:
|
||||
n.update(extra)
|
||||
NODES.append(n)
|
||||
return n
|
||||
|
||||
|
||||
def link(src, src_slot, dst, dst_input, ltype):
|
||||
global _link_id
|
||||
_link_id += 1
|
||||
sn = next(n for n in NODES if n["id"] == src)
|
||||
dn = next(n for n in NODES if n["id"] == dst)
|
||||
slot = next(i for i, inp in enumerate(dn["inputs"]) if inp["name"] == dst_input)
|
||||
dn["inputs"][slot]["link"] = _link_id
|
||||
sn["outputs"][src_slot]["links"].append(_link_id)
|
||||
LINKS.append([_link_id, src, src_slot, dst, slot, ltype])
|
||||
|
||||
|
||||
def inp(name, ltype):
|
||||
return {"name": name, "type": ltype, "link": None}
|
||||
|
||||
|
||||
NOTE = """# Static fisheye video → 4D Gaussian video
|
||||
|
||||
Turns a video shot on a **static 180° fisheye camera** (locked-off shot,
|
||||
security cam, tripod) into a 4D (3D + time) Gaussian scene, then re-renders it
|
||||
from a *moving* novel camera.
|
||||
|
||||
A static camera needs **no pose estimation** (no VGGT, unlike
|
||||
`video_to_4d_world.json`): the trajectory is identity, and one frame already
|
||||
sees the entire static background.
|
||||
|
||||
**Stages**
|
||||
1. **FisheyeDepthEstimator** — per-frame metric *radial* depth on the whole
|
||||
fisheye batch (DISTANCE_AWARE multi-view merge).
|
||||
2. **MotionMaskFromDepth** (identity trajectory) — pixels whose depth changes
|
||||
over time are dynamic.
|
||||
3. **FisheyeToGaussian** on frame 0 → whole-scene splats;
|
||||
**SplitSplatsByMask** (FISHEYE 180°) separates the dynamic canonical
|
||||
(inside) from the static world (outside).
|
||||
4. **EstimateTracks** (CoTracker3) + **TracksToTrajectories** (FISHEYE 180°,
|
||||
identity poses — its default) lift 2D tracks to 3D control trajectories.
|
||||
5. **BuildSplats4D** binds the canonical splats to the tracks → 4D scene with
|
||||
the static background attached.
|
||||
6. **RenderSplats4DVideo** renders a novel orbit (pinhole 90°). A second,
|
||||
**muted** render replays the original static fisheye view for A/B
|
||||
comparison — unmute it (Ctrl+M) to use.
|
||||
|
||||
**Set:** the video path + `frame_load_cap` (≤ 64 recommended); the novel-path
|
||||
end pose; `threshold` in MotionMaskFromDepth (raise it if the mask flickers —
|
||||
per-frame depth is not temporally consistent).
|
||||
|
||||
**Requires:** SHARP + gsplat (auto via install.py), CoTracker3 (torch.hub,
|
||||
first-use download), Depth-Anything V2 (auto), VideoHelperSuite pack.
|
||||
|
||||
**Limitations:** the static world only knows what frame 0 saw — regions
|
||||
occluded by moving objects at t=0 are holes if the novel camera peeks behind
|
||||
them; strong depth flicker can leak static pixels into the dynamic set.
|
||||
|
||||
**Outputs:** novel-path WEBM, the 4D scene as `.npz` (SaveSplats4D — reload
|
||||
with LoadSplats4D), optional replay WEBM."""
|
||||
|
||||
DA = "Depth-Anything-V2-Metric-Indoor-Base-hf"
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
node(30, "MarkdownNote", "About this workflow", (-2000, 260), (560, 700), [NOTE],
|
||||
extra={"color": "#432", "bgcolor": "#653"})
|
||||
|
||||
node(1, "VHS_LoadVideoPath", "Load fisheye video (static camera, <= 64 frames)",
|
||||
(-1380, 300), (240, 262),
|
||||
{
|
||||
"video": "input/fisheye_video.mp4",
|
||||
"force_rate": 0, "custom_width": 0, "custom_height": 0,
|
||||
"frame_load_cap": 49, "skip_first_frames": 0, "select_every_nth": 1,
|
||||
"format": "AnimateDiff",
|
||||
"videopreview": {"hidden": False, "paused": False, "params": {
|
||||
"filename": "input/fisheye_video.mp4", "type": "path",
|
||||
"format": "video/mp4", "force_rate": 0, "custom_width": 0,
|
||||
"custom_height": 0, "frame_load_cap": 49,
|
||||
"skip_first_frames": 0, "select_every_nth": 1}},
|
||||
},
|
||||
inputs=[inp("meta_batch", "VHS_BatchManager"), inp("vae", "VAE")],
|
||||
outputs=[("IMAGE", "IMAGE"), ("frame_count", "INT"),
|
||||
("audio", "AUDIO"), ("video_info", "VHS_VIDEOINFO")])
|
||||
|
||||
node(2, "FisheyeDepthEstimator", "Per-frame radial depth (batched)",
|
||||
(-1060, 300), (315, 246),
|
||||
[DA, 1.0, 90.0, 518, 1024, "DISTANCE_AWARE", 25, 1],
|
||||
inputs=[inp("image", "IMAGE")],
|
||||
outputs=[("depthmap", "TENSOR"), ("mask", "MASK")])
|
||||
|
||||
node(3, "TransformToMatrix", "Static camera (identity pose)",
|
||||
(-1060, 640), (315, 154), [0.0, 0.0, 0.0, 0.0, 0.0],
|
||||
outputs=[("transformation matrix", "MAT_4X4")])
|
||||
|
||||
node(4, "CameraInterpolationNode", "Identity trajectory (interpolated to T)",
|
||||
(-680, 680), (226, 78), [2],
|
||||
inputs=[inp("initial_matrix", "MAT_4X4"), inp("final_matrix", "MAT_4X4")],
|
||||
outputs=[("trajectory", "TENSOR")])
|
||||
|
||||
node(5, "MotionMaskFromDepth", "Dynamic-pixel mask (1 = moving)",
|
||||
(-680, 300), (315, 202), ["FISHEYE", 180.0, 0.15, 4, 2, "auto"],
|
||||
inputs=[inp("depth_seq", "TENSOR"), inp("trajectory", "TENSOR")],
|
||||
outputs=[("motion_mask", "MASK")])
|
||||
|
||||
node(6, "ImageFromBatch", "Frame 0 (canonical view)",
|
||||
(-680, 560), (226, 82), [0, 1],
|
||||
inputs=[inp("image", "IMAGE")],
|
||||
outputs=[("IMAGE", "IMAGE")])
|
||||
|
||||
node(7, "FisheyeToGaussian", "SHARP frame 0 -> whole-scene splats",
|
||||
(-300, 460), (330, 290),
|
||||
[180.0, 0, 0, "<download default>", "auto", 90.0, 0, "smart", 0.01, 5.0],
|
||||
inputs=[inp("image", "IMAGE")],
|
||||
outputs=[("splats", "GSPLAT")])
|
||||
|
||||
node(8, "EstimateTracks", "CoTracker3 (downloads on first use)",
|
||||
(-300, 820), (300, 102), [20, "auto"],
|
||||
inputs=[inp("frames", "IMAGE")],
|
||||
outputs=[("tracks", "TENSOR"), ("visibility", "TENSOR")])
|
||||
|
||||
node(9, "SplitSplatsByMask", "Split: inside = dynamic, outside = static world",
|
||||
(100, 300), (315, 174), ["FISHEYE", 180.0, 0.5, "auto"],
|
||||
inputs=[inp("splats", "GSPLAT"), inp("mask", "MASK"),
|
||||
inp("camera_matrix", "MAT_4X4")],
|
||||
outputs=[("inside_splats", "GSPLAT"), ("outside_splats", "GSPLAT")])
|
||||
|
||||
node(10, "TracksToTrajectories", "Lift tracks to 3D (identity poses = default)",
|
||||
(100, 700), (315, 190), ["FISHEYE", 180.0, 0.5, "auto"],
|
||||
inputs=[inp("tracks", "TENSOR"), inp("visibility", "TENSOR"),
|
||||
inp("depth_seq", "TENSOR"), inp("trajectory", "TENSOR")],
|
||||
outputs=[("trajectories3d", "TENSOR"), ("track_valid", "TENSOR")])
|
||||
|
||||
node(11, "BuildSplats4D", "Bind canonical to tracks -> 4D scene",
|
||||
(500, 440), (315, 190), [0, 4, 0.0, "auto"],
|
||||
inputs=[inp("canonical", "GSPLAT"), inp("trajectories3d", "TENSOR"),
|
||||
inp("static", "GSPLAT"), inp("times", "TENSOR"),
|
||||
inp("track_valid", "TENSOR")],
|
||||
outputs=[("splats4d", "GSPLAT4D")])
|
||||
|
||||
node(12, "TransformToMatrix", "Novel path: start (original camera)",
|
||||
(500, 720), (315, 154), [0.0, 0.0, 0.0, 0.0, 0.0],
|
||||
outputs=[("transformation matrix", "MAT_4X4")])
|
||||
|
||||
node(13, "TransformToMatrix", "Novel path: end (small orbit — edit me)",
|
||||
(500, 920), (315, 154), [0.15, 0.0, 0.1, 0.0, -10.0],
|
||||
outputs=[("transformation matrix", "MAT_4X4")])
|
||||
|
||||
node(14, "CameraInterpolationNode", "Novel camera path",
|
||||
(880, 820), (226, 78), [2],
|
||||
inputs=[inp("initial_matrix", "MAT_4X4"), inp("final_matrix", "MAT_4X4")],
|
||||
outputs=[("trajectory", "TENSOR")])
|
||||
|
||||
node(15, "RenderSplats4DVideo", "Render 4D along novel path (pinhole 90°)",
|
||||
(900, 300), (315, 266), [49, 0.0, 1.0, "PINHOLE", 90.0, 768, 768, "auto", 0, "auto"],
|
||||
inputs=[inp("splats4d", "GSPLAT4D"), inp("trajectory", "TENSOR")],
|
||||
outputs=[("images", "IMAGE"), ("masks", "MASK"), ("disparity", "TENSOR")])
|
||||
|
||||
node(16, "RenderSplats4DVideo", "MUTED: replay original fisheye view (A/B check)",
|
||||
(900, 1060), (315, 266), [49, 0.0, 1.0, "FISHEYE", 180.0, 1024, 1024, "auto", 0, "auto"],
|
||||
inputs=[inp("splats4d", "GSPLAT4D"), inp("trajectory", "TENSOR")],
|
||||
outputs=[("images", "IMAGE"), ("masks", "MASK"), ("disparity", "TENSOR")],
|
||||
mode=2)
|
||||
|
||||
node(17, "SaveWEBM", None, (1300, 300), (315, 437),
|
||||
["4d_fisheye_novel", "vp9", 24, 32],
|
||||
inputs=[inp("images", "IMAGE")])
|
||||
|
||||
node(18, "SaveSplats4D", "Save 4D scene (.npz)", (1300, 800), (315, 106),
|
||||
["ComfyUISplat4D_fisheye", False],
|
||||
inputs=[inp("splats4d", "GSPLAT4D")])
|
||||
|
||||
node(19, "SaveWEBM", "MUTED: replay output", (1300, 1060), (315, 437),
|
||||
["4d_fisheye_replay", "vp9", 24, 32],
|
||||
inputs=[inp("images", "IMAGE")], mode=2)
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
link(1, 0, 2, "image", "IMAGE")
|
||||
link(1, 0, 6, "image", "IMAGE")
|
||||
link(1, 0, 8, "frames", "IMAGE")
|
||||
link(2, 0, 5, "depth_seq", "TENSOR")
|
||||
link(2, 0, 10, "depth_seq", "TENSOR")
|
||||
link(3, 0, 4, "initial_matrix", "MAT_4X4")
|
||||
link(3, 0, 4, "final_matrix", "MAT_4X4")
|
||||
link(4, 0, 5, "trajectory", "TENSOR")
|
||||
link(4, 0, 16, "trajectory", "TENSOR")
|
||||
link(5, 0, 9, "mask", "MASK")
|
||||
link(6, 0, 7, "image", "IMAGE")
|
||||
link(7, 0, 9, "splats", "GSPLAT")
|
||||
link(8, 0, 10, "tracks", "TENSOR")
|
||||
link(8, 1, 10, "visibility", "TENSOR")
|
||||
link(9, 0, 11, "canonical", "GSPLAT")
|
||||
link(9, 1, 11, "static", "GSPLAT")
|
||||
link(10, 0, 11, "trajectories3d", "TENSOR")
|
||||
link(10, 1, 11, "track_valid", "TENSOR")
|
||||
link(11, 0, 15, "splats4d", "GSPLAT4D")
|
||||
link(11, 0, 16, "splats4d", "GSPLAT4D")
|
||||
link(11, 0, 18, "splats4d", "GSPLAT4D")
|
||||
link(12, 0, 14, "initial_matrix", "MAT_4X4")
|
||||
link(13, 0, 14, "final_matrix", "MAT_4X4")
|
||||
link(14, 0, 15, "trajectory", "TENSOR")
|
||||
link(15, 0, 17, "images", "IMAGE")
|
||||
link(16, 0, 19, "images", "IMAGE")
|
||||
# NOTE: TracksToTrajectories.trajectory stays unconnected on purpose — its
|
||||
# default is identity poses, i.e. exactly the static camera.
|
||||
|
||||
|
||||
def bbox(ids, pad_top=60, pad=20):
|
||||
ns = [n for n in NODES if n["id"] in ids]
|
||||
x = min(n["pos"][0] for n in ns) - pad
|
||||
y = min(n["pos"][1] for n in ns) - pad_top
|
||||
x2 = max(n["pos"][0] + n["size"][0] for n in ns) + pad
|
||||
y2 = max(n["pos"][1] + n["size"][1] for n in ns) + pad
|
||||
return [x, y, x2 - x, y2 - y]
|
||||
|
||||
|
||||
GROUPS = [
|
||||
("1. Load fisheye video", [1]),
|
||||
("2. Per-frame fisheye depth", [2]),
|
||||
("3. Static camera trajectory", [3, 4]),
|
||||
("4. Motion mask", [5]),
|
||||
("5. Frame-0 splats & static/dynamic split", [6, 7, 9]),
|
||||
("6. Dynamic tracks -> 3D", [8, 10]),
|
||||
("7. 4D scene", [11, 18]),
|
||||
("8. Render novel path (+ muted replay)", [12, 13, 14, 15, 16, 17, 19]),
|
||||
]
|
||||
COLORS = ["#3f789e", "#a1309b", "#8A8", "#b58b2a", "#88A", "#b06634", "#535",
|
||||
"#3f789e"]
|
||||
|
||||
workflow = {
|
||||
"id": "00000000-0000-0000-0000-000000000000",
|
||||
"revision": 0,
|
||||
"last_node_id": 30,
|
||||
"last_link_id": _link_id,
|
||||
"nodes": NODES,
|
||||
"links": LINKS,
|
||||
"groups": [{
|
||||
"id": i + 1, "title": t, "bounding": bbox(ids),
|
||||
"color": COLORS[i % len(COLORS)], "font_size": 24, "flags": {},
|
||||
} for i, (t, ids) in enumerate(GROUPS)],
|
||||
"config": {},
|
||||
"extra": {"camera_comfyui_rev": 1},
|
||||
"version": 0.4,
|
||||
}
|
||||
|
||||
with open(OUT, "w", encoding="utf-8", newline="\n") as fh:
|
||||
json.dump(workflow, fh, indent=2, ensure_ascii=False)
|
||||
fh.write("\n")
|
||||
print(f"wrote {OUT}: {len(NODES)} nodes, {len(LINKS)} links")
|
||||
@@ -0,0 +1,699 @@
|
||||
"""One-shot July 2026 workflow rework: schema repairs + documentation.
|
||||
|
||||
For every workflows/*.json this script:
|
||||
1. migrates widgets_values stored with pre-2026 node schemas to the current
|
||||
widget lists (see notebooks/validate_workflows.py for the detector);
|
||||
2. fixes broken references (wan UNET filename typo, ReprojectImage rotation
|
||||
widgets that no longer exist -> explicit TransformToMatrix nodes);
|
||||
3. adds an embedded MarkdownNote explaining purpose/stages/requirements,
|
||||
meaningful group boxes and node titles;
|
||||
4. rewrites the JSON pretty-printed (indent 2).
|
||||
|
||||
Idempotent-ish: repairs are guarded by length/value checks, notes are only
|
||||
added if no MarkdownNote titled 'About this workflow' exists.
|
||||
|
||||
Run: python notebooks/rework_workflows_2026_07.py
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
WF = os.path.join(REPO, "workflows")
|
||||
|
||||
DA_MODEL = "Depth-Anything-V2-Metric-Indoor-Base-hf"
|
||||
|
||||
NOTE_TITLE = "About this workflow"
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Generic helpers
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
def load(name):
|
||||
with open(os.path.join(WF, name), encoding="utf-8") as fh:
|
||||
return json.load(fh)
|
||||
|
||||
|
||||
def save(name, data):
|
||||
# normalize pos/size dict-form (pre-2024 serialization) to arrays
|
||||
for n in data.get("nodes", []):
|
||||
for key in ("pos", "size"):
|
||||
v = n.get(key)
|
||||
if isinstance(v, dict):
|
||||
n[key] = [v[k] for k in sorted(v, key=lambda s: int(s))]
|
||||
with open(os.path.join(WF, name), "w", encoding="utf-8", newline="\n") as fh:
|
||||
json.dump(data, fh, indent=2, ensure_ascii=False)
|
||||
fh.write("\n")
|
||||
|
||||
|
||||
def node(data, nid):
|
||||
for n in data["nodes"]:
|
||||
if n["id"] == nid:
|
||||
return n
|
||||
raise KeyError(f"node {nid} not found")
|
||||
|
||||
|
||||
def xy(v):
|
||||
if isinstance(v, dict):
|
||||
return [float(v["0"]), float(v["1"])]
|
||||
return [float(v[0]), float(v[1])]
|
||||
|
||||
|
||||
def bbox(data, ids, pad_top=60, pad=20):
|
||||
xs, ys, xe, ye = [], [], [], []
|
||||
for nid in ids:
|
||||
n = node(data, nid)
|
||||
p, s = xy(n["pos"]), xy(n["size"])
|
||||
xs.append(p[0]); ys.append(p[1])
|
||||
xe.append(p[0] + s[0]); ye.append(p[1] + s[1])
|
||||
x, y = min(xs) - pad, min(ys) - pad_top
|
||||
return [x, y, max(xe) + pad - x, max(ye) + pad - y]
|
||||
|
||||
|
||||
GROUP_COLORS = ["#3f789e", "#a1309b", "#8A8", "#b58b2a", "#88A", "#b06634", "#535"]
|
||||
|
||||
|
||||
def set_groups(data, groups):
|
||||
"""groups: list of (title, node_ids). Replaces the groups list."""
|
||||
out = []
|
||||
for i, (title, ids) in enumerate(groups):
|
||||
out.append({
|
||||
"id": i + 1,
|
||||
"title": title,
|
||||
"bounding": bbox(data, ids),
|
||||
"color": GROUP_COLORS[i % len(GROUP_COLORS)],
|
||||
"font_size": 24,
|
||||
"flags": {},
|
||||
})
|
||||
data["groups"] = out
|
||||
|
||||
|
||||
def retitle_groups(data, titles):
|
||||
"""titles: list matching data['groups'] order."""
|
||||
assert len(titles) == len(data.get("groups", [])), "group count mismatch"
|
||||
for g, t in zip(data["groups"], titles):
|
||||
g["title"] = t
|
||||
|
||||
|
||||
def set_title(data, nid, title):
|
||||
node(data, nid)["title"] = title
|
||||
|
||||
|
||||
def next_node_id(data):
|
||||
data["last_node_id"] = int(data.get("last_node_id", 0)) + 1
|
||||
return data["last_node_id"]
|
||||
|
||||
|
||||
def next_link_id(data):
|
||||
data["last_link_id"] = int(data.get("last_link_id", 0)) + 1
|
||||
return data["last_link_id"]
|
||||
|
||||
|
||||
def add_note(data, markdown, pos=None, size=None):
|
||||
for n in data["nodes"]:
|
||||
if n["type"] in ("MarkdownNote", "Note") and n.get("title") == NOTE_TITLE:
|
||||
n["widgets_values"] = [markdown]
|
||||
return n["id"]
|
||||
if pos is None:
|
||||
xs = [xy(n["pos"])[0] for n in data["nodes"]]
|
||||
ys = [xy(n["pos"])[1] for n in data["nodes"]]
|
||||
pos = [min(xs) - 560, min(ys)]
|
||||
if size is None:
|
||||
lines = markdown.count("\n") + 1
|
||||
size = [520, max(220, min(700, 26 * lines + 80))]
|
||||
nid = next_node_id(data)
|
||||
data["nodes"].append({
|
||||
"id": nid,
|
||||
"type": "MarkdownNote",
|
||||
"title": NOTE_TITLE,
|
||||
"pos": pos,
|
||||
"size": size,
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"properties": {},
|
||||
"widgets_values": [markdown],
|
||||
"color": "#432",
|
||||
"bgcolor": "#653",
|
||||
})
|
||||
return nid
|
||||
|
||||
|
||||
def add_transform_node(data, pos, widgets):
|
||||
nid = next_node_id(data)
|
||||
data["nodes"].append({
|
||||
"id": nid,
|
||||
"type": "TransformToMatrix",
|
||||
"pos": pos,
|
||||
"size": [315, 154],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [{"name": "transformation matrix", "type": "MAT_4X4",
|
||||
"links": [], "slot_index": 0}],
|
||||
"properties": {"Node name for S&R": "TransformToMatrix"},
|
||||
"widgets_values": widgets,
|
||||
})
|
||||
return nid
|
||||
|
||||
|
||||
def link(data, src, dst, dst_input, ltype="MAT_4X4", src_slot=0):
|
||||
"""Connect src node output slot to dst node's named input (added if absent)."""
|
||||
dnode = node(data, dst)
|
||||
inputs = dnode.setdefault("inputs", [])
|
||||
slot = None
|
||||
for i, inp in enumerate(inputs):
|
||||
if inp["name"] == dst_input:
|
||||
slot = i
|
||||
break
|
||||
if slot is None:
|
||||
inputs.append({"name": dst_input, "type": ltype, "link": None})
|
||||
slot = len(inputs) - 1
|
||||
lid = next_link_id(data)
|
||||
inputs[slot]["link"] = lid
|
||||
snode = node(data, src)
|
||||
snode["outputs"][src_slot].setdefault("links", None)
|
||||
if snode["outputs"][src_slot]["links"] is None:
|
||||
snode["outputs"][src_slot]["links"] = []
|
||||
snode["outputs"][src_slot]["links"].append(lid)
|
||||
data["links"].append([lid, src, src_slot, dst, slot, ltype])
|
||||
return lid
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Global widget-schema repairs (2025 -> 2026 node schemas)
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
def repair_widgets(data):
|
||||
changed = []
|
||||
for n in data["nodes"]:
|
||||
t, w = n["type"], n.get("widgets_values")
|
||||
if not isinstance(w, list):
|
||||
continue
|
||||
if t == "DepthEstimatorNode" and len(w) == 2:
|
||||
n["widgets_values"] = w + [1] # median_blur_kernel
|
||||
elif t == "FisheyeDepthEstimator" and len(w) == 6:
|
||||
# old: [model, scale, pfov, pres, fres, softmerge_radius]
|
||||
n["widgets_values"] = w[:5] + ["SOFTMERGE", w[5], 1]
|
||||
elif t == "PointCloudCleaner" and len(w) == 2:
|
||||
# old (voxel_size, min_points) were world-unit; semantics changed
|
||||
n["widgets_values"] = [1024, 1024, 1.0, 3]
|
||||
elif t == "CameraMotionNode" and len(w) == 6:
|
||||
n["widgets_values"] = w + [0, False, False]
|
||||
elif t == "CameraInterpolationNode" and len(w) == 0:
|
||||
n["widgets_values"] = [2] # keyframes; renderers interpolate frames
|
||||
elif t == "PointcloudTrajectoryEnricher" and len(w) == 20:
|
||||
# old tail: reproject-back proj/fov/w/h + legacy backend/voxel params
|
||||
n["widgets_values"] = w[:13] + [0.07, 3, DA_MODEL]
|
||||
else:
|
||||
continue
|
||||
changed.append(f"{t}#{n['id']}")
|
||||
return changed
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Per-file specs
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
def do_demo_camera(name="demo_camera_workflow.json"):
|
||||
d = load(name)
|
||||
repair_widgets(d)
|
||||
set_title(d, 1, "Rotate camera (pitch 60°)")
|
||||
set_title(d, 2, "Pinhole 90° → Equirect 180°")
|
||||
set_title(d, 7, "Reprojected image")
|
||||
set_title(d, 4, "Coverage mask (white = hole)")
|
||||
add_note(d, (
|
||||
"# Camera reprojection demo\n\n"
|
||||
"Minimal example of the two core nodes: **TransformToMatrix** rotates the "
|
||||
"virtual camera (theta = 60° pitch) and **ReprojectImage** converts a 90° "
|
||||
"pinhole image into a 180° equirectangular view.\n\n"
|
||||
"Previews show the reprojected image and the coverage mask "
|
||||
"(white = pixels the source image cannot see).\n\n"
|
||||
"- **Set:** the image in LoadImage.\n"
|
||||
"- **Requires:** nothing beyond this pack (no models).\n"
|
||||
"- **Try:** switch `output_projection` to FISHEYE, or raise `feathering` "
|
||||
"to soften the mask edge."
|
||||
))
|
||||
save(name, d)
|
||||
|
||||
|
||||
def do_outpaint_node_test(name="Outpaint_node_test.json"):
|
||||
d = load(name)
|
||||
repair_widgets(d)
|
||||
set_title(d, 11, "Outpaint one patch (yaw +45°)")
|
||||
set_title(d, 3, "Result")
|
||||
set_title(d, 5, "Holes still to fill")
|
||||
add_note(d, (
|
||||
"# OutpaintAnyProjection smoke test\n\n"
|
||||
"Single-node sanity check: a 90° pinhole image is placed on a 180° fisheye "
|
||||
"canvas and one 90° pinhole patch at yaw +45° is Flux-inpainted "
|
||||
"(10 steps for speed).\n\n"
|
||||
"Outputs: the partially outpainted canvas and the *remaining holes* mask — "
|
||||
"chain more OutpaintAnyProjection nodes at other angles to fill it "
|
||||
"(see `Outpaint_fisheye180.json`).\n\n"
|
||||
"- **Set:** input image; prompt inside the node (empty = unconditional).\n"
|
||||
"- **Requires:** `custom_nodes/inpainting_flux` "
|
||||
"(installed automatically by this pack's install.py) — downloads "
|
||||
"FLUX.1-Fill NF4 weights on first run."
|
||||
))
|
||||
save(name, d)
|
||||
|
||||
|
||||
def do_fisheye_to_pointcloud(name="fisheye_to_pointcloud.json"):
|
||||
d = load(name)
|
||||
repair_widgets(d)
|
||||
set_title(d, 155, "Metric depth (fisheye-aware)")
|
||||
set_title(d, 160, "Depth → point cloud")
|
||||
set_title(d, 161, "Save .ply / .npy")
|
||||
set_groups(d, [
|
||||
("1. Fisheye metric depth", [154, 155, 156, 157, 158, 159]),
|
||||
("2. Unproject & save", [160, 161]),
|
||||
])
|
||||
add_note(d, (
|
||||
"# Fisheye 180° → point cloud\n\n"
|
||||
"Estimates metric depth directly on a 180° fisheye image — "
|
||||
"**FisheyeDepthEstimator** internally splits it into pinhole views, runs "
|
||||
"Depth-Anything V2 on each and merges the depths back (SOFTMERGE) — then "
|
||||
"unprojects image + depth to a 3D point cloud and saves it.\n\n"
|
||||
"Previews: colorized depth and the validity mask.\n\n"
|
||||
"- **Set:** fisheye input image (e.g. produced by "
|
||||
"`Outpaint_fisheye180.json`); filename in SavePointCloud.\n"
|
||||
"- **Requires:** Depth-Anything V2 (auto-downloads from HuggingFace).\n"
|
||||
"- **Next:** view the cloud with `test_pointcloud_loading.json` or "
|
||||
"synthesize a second eye with `sbs180_workflow.json`."
|
||||
))
|
||||
save(name, d)
|
||||
|
||||
|
||||
def do_pointcloud(name="PointCloud.json"):
|
||||
d = load(name)
|
||||
repair_widgets(d)
|
||||
set_title(d, 6, "Move camera (dolly −0.1, yaw 20°)")
|
||||
set_title(d, 31, "Render novel view")
|
||||
set_title(d, 47, "Drop stretched/occluded points")
|
||||
set_title(d, 43, "Novel-view depth → image")
|
||||
set_groups(d, [
|
||||
("1. Image → metric ray depth", [1, 38, 45, 42, 40]),
|
||||
("2. Lift to 3D & move camera", [18, 6, 9, 47]),
|
||||
("3. Novel view & previews", [31, 13, 12, 43, 44, 25, 26]),
|
||||
])
|
||||
add_note(d, (
|
||||
"# Single image → point cloud → novel view\n\n"
|
||||
"Lifts one pinhole image to a 3D point cloud via monocular metric depth, "
|
||||
"moves the camera, cleans occlusion artifacts and re-renders from the new "
|
||||
"viewpoint.\n\n"
|
||||
"Stages: DepthEstimator → ZDepthToRayDepth → DepthToPointCloud → "
|
||||
"TransformPointCloud (dolly −0.1, yaw 20°) → PointCloudCleaner → "
|
||||
"ProjectPointCloud. MedianFilterImage smooths the re-render "
|
||||
"(from ComfyUI-Image-Filters, optional).\n\n"
|
||||
"- **Set:** input image; the camera move in TransformToMatrix.\n"
|
||||
"- **Requires:** Depth-Anything V2 (auto-download); ComfyUI-Image-Filters "
|
||||
"only for the median-filter preview.\n"
|
||||
"- **Note:** PointCloudCleaner params were reset to defaults during the "
|
||||
"2026-07 schema migration — retune voxel_size / min_points_per_voxel if "
|
||||
"the render looks too sparse."
|
||||
))
|
||||
save(name, d)
|
||||
|
||||
|
||||
def do_pointcloud_walker(name="Pointcloud walker.json"):
|
||||
d = load(name)
|
||||
repair_widgets(d)
|
||||
set_title(d, 5, "Camera move (edit me)")
|
||||
set_title(d, 17, "Build trajectory")
|
||||
set_title(d, 14, "Render fly-through")
|
||||
set_groups(d, [
|
||||
("1. Image → point cloud", [1, 3, 20, 2]),
|
||||
("2. Trajectory", [5, 4, 17]),
|
||||
("3. Render & save", [14, 10]),
|
||||
])
|
||||
add_note(d, (
|
||||
"# Point cloud walker (fly-through video)\n\n"
|
||||
"Single image → metric depth → point cloud, then **CameraTrajectoryNode** "
|
||||
"derives a camera path and **CameraMotionNode** renders a fly-through "
|
||||
"saved as WEBM.\n\n"
|
||||
"- **Set:** input image; the move in TransformToMatrix "
|
||||
"(shift XYZ + theta/phi); frames-per-segment (`n_points`) in "
|
||||
"CameraMotionNode.\n"
|
||||
"- **Requires:** Depth-Anything V2 (auto-download).\n"
|
||||
"- **Note:** the pinhole FOV here is 60° — keep DepthToPointCloud and "
|
||||
"CameraMotionNode FOVs consistent with your input."
|
||||
))
|
||||
save(name, d)
|
||||
|
||||
|
||||
def do_test_pointcloud_loading(name="Test pointcloud_loading.json"):
|
||||
d = load(name)
|
||||
repair_widgets(d)
|
||||
set_title(d, 6, "Load saved cloud (.ply/.npy)")
|
||||
set_title(d, 1, "Start pose (identity)")
|
||||
set_title(d, 5, "End pose (dolly +0.1)")
|
||||
set_title(d, 7, "Render motion")
|
||||
add_note(d, (
|
||||
"# Load a saved point cloud & orbit\n\n"
|
||||
"Reloads a point cloud saved by SavePointCloud and renders a short camera "
|
||||
"move between two poses (identity → 0.1 forward) as a WEBM.\n\n"
|
||||
"- **Set:** the file in LoadPointCloud (dropdown lists the ComfyUI input "
|
||||
"dir — run `PointCloud.json` or `fisheye_to_pointcloud.json` first); the "
|
||||
"two TransformToMatrix poses.\n"
|
||||
"- **Requires:** nothing beyond this pack."
|
||||
))
|
||||
save(name, d)
|
||||
|
||||
|
||||
def do_pc_enricher(name="PC_enricher.json"):
|
||||
d = load(name)
|
||||
repair_widgets(d)
|
||||
set_title(d, 3, "Enrich cloud along trajectory (Flux outpaint)")
|
||||
set_title(d, 11, "Orbit preview of enriched cloud")
|
||||
set_groups(d, [
|
||||
("1. Fisheye → point cloud", [13, 14, 17, 16, 15]),
|
||||
("2. Enrich along trajectory", [18, 3]),
|
||||
("3. Save & preview", [4, 11, 12]),
|
||||
])
|
||||
add_note(d, (
|
||||
"# Point cloud enricher (outpaint along a trajectory)\n\n"
|
||||
"Fisheye image → metric depth → point cloud, then "
|
||||
"**PointcloudTrajectoryEnricher** walks the loaded camera trajectory: at "
|
||||
"each pose it renders the cloud, Flux-inpaints the disocclusion holes, "
|
||||
"re-estimates depth, aligns it and merges the new points into the cloud. "
|
||||
"The enriched cloud is saved and previewed as an orbit video.\n\n"
|
||||
"This is the one-node version of `pointcloud_inpaint.json`.\n\n"
|
||||
"- **Set:** input image; trajectory .npy (record one with "
|
||||
"SaveTrajectory); prompt inside the enricher.\n"
|
||||
"- **Requires:** inpainting_flux (auto-installed), Depth-Anything V2, "
|
||||
"FLUX.1-Fill NF4 weights.\n"
|
||||
"- **Note:** 2026-07 schema migration — the enricher's render/reproject "
|
||||
"options are now internal; voxel merge params were reset to defaults."
|
||||
))
|
||||
save(name, d)
|
||||
|
||||
|
||||
def do_fisheye_depth(name="Fisheye_depth_workflow.json"):
|
||||
d = load(name)
|
||||
repair_widgets(d)
|
||||
retitle_groups(d, [
|
||||
"View 1: center pinhole 90°",
|
||||
"View 2: yaw +45°",
|
||||
"View 3: yaw −45°",
|
||||
"View 4: pitch +45°",
|
||||
"View 5: pitch −45°",
|
||||
"View 6: full-fisheye fallback",
|
||||
])
|
||||
# fusion chain + export were never grouped
|
||||
d["groups"].append({
|
||||
"id": 7,
|
||||
"title": "Fuse views & export point cloud",
|
||||
"bounding": bbox(d, [95, 146, 147, 148, 149, 105, 151, 153, 154]),
|
||||
"color": "#b58b2a",
|
||||
"font_size": 24,
|
||||
"flags": {},
|
||||
})
|
||||
set_title(d, 149, "Final fuse (SRC keeps fused views)")
|
||||
set_title(d, 151, "Fused depth → point cloud")
|
||||
add_note(d, (
|
||||
"# Multi-view fisheye depth — manual reference\n\n"
|
||||
"Estimates consistent metric depth over a 180° fisheye image by "
|
||||
"reprojecting it into five pinhole views (center, yaw ±45°, pitch ±45°), "
|
||||
"running Depth-Anything V2 on each, reprojecting the depths back to the "
|
||||
"fisheye and progressively fusing them (CombineDepthsNode + "
|
||||
"DepthRenormalizer to align scales), plus a full-fisheye pass for the "
|
||||
"rim. The fused depth is unprojected and saved as a point cloud.\n\n"
|
||||
"⚠️ **This whole graph is now one node** — `FisheyeDepthEstimator` does "
|
||||
"the same split-and-merge internally (see `fisheye_to_pointcloud.json`). "
|
||||
"Kept as a transparent, tweakable reference implementation.\n\n"
|
||||
"- **Set:** fisheye input image; SavePointCloud filename.\n"
|
||||
"- **Requires:** Depth-Anything V2 (auto-download)."
|
||||
))
|
||||
save(name, d)
|
||||
|
||||
|
||||
def do_outpaint_fisheye180(name="Outpaint_fisheye180.json"):
|
||||
d = load(name)
|
||||
repair_widgets(d)
|
||||
retitle_groups(d, [
|
||||
"2. Chained patch outpaints",
|
||||
"1. Pinhole → fisheye canvas",
|
||||
])
|
||||
set_title(d, 2, "Outpaint yaw +45°")
|
||||
set_title(d, 3, "Outpaint yaw −45°")
|
||||
set_title(d, 4, "Outpaint pitch +45°")
|
||||
set_title(d, 5, "Outpaint pitch −45°")
|
||||
set_title(d, 9, "Full-frame pass (low-res rim fill)")
|
||||
set_title(d, 15, "Upscale rim fill to 4096")
|
||||
set_title(d, 12, "Composite sharp patches over rim")
|
||||
add_note(d, (
|
||||
"# Outpaint to a full 180° fisheye\n\n"
|
||||
"Places a 90° pinhole image onto a 180° fisheye canvas (ReprojectImage), "
|
||||
"then chains **five OutpaintAnyProjection passes** — yaw +45°, yaw −45°, "
|
||||
"pitch +45°, pitch −45°, and a low-res full-frame pass for the rim. Each "
|
||||
"pass consumes the previous pass's *remaining holes* mask. A PorterDuff "
|
||||
"composite keeps the sharp high-res patches on top of the upscaled rim "
|
||||
"fill.\n\n"
|
||||
"- **Set:** input image; prompts inside each outpaint node (optional).\n"
|
||||
"- **Requires:** inpainting_flux (auto-installed by install.py); "
|
||||
"FLUX.1-Fill NF4 weights download on first run (~12 GB VRAM).\n"
|
||||
"- **Output:** `Saved_fisheye` PNG — used as the input of "
|
||||
"`fisheye_to_pointcloud.json`, `sbs180_workflow.json` and "
|
||||
"`PC_enricher.json`."
|
||||
))
|
||||
save(name, d)
|
||||
|
||||
|
||||
def do_outpainting_fisheye_sd(name="outpainting_fisheye.json"):
|
||||
d = load(name)
|
||||
repair_widgets(d)
|
||||
# --- structural repair: pre-2025 ReprojectImage stored patch rotations as
|
||||
# widgets; the current node takes a MAT_4X4 transform_matrix input and an
|
||||
# inverse flag instead (mirrors outpainting_fisheye_flux.json).
|
||||
fixes = {
|
||||
42: ([90, 180, "PINHOLE", "FISHEYE", 4096, 4096, False, 0], None),
|
||||
40: ([180, 90, "FISHEYE", "PINHOLE", 1024, 1024, False, 0], "+42"),
|
||||
51: ([90, 180, "PINHOLE", "FISHEYE", 4096, 4096, True, 0], "+42"),
|
||||
59: ([180, 90, "FISHEYE", "PINHOLE", 1024, 1024, False, 0], "-42"),
|
||||
63: ([90, 180, "PINHOLE", "FISHEYE", 4096, 4096, True, 0], "-42"),
|
||||
67: ([180, 180, "FISHEYE", "FISHEYE", 1024, 1024, False, 0], None),
|
||||
72: ([180, 180, "FISHEYE", "FISHEYE", 4096, 4096, False, 0], None),
|
||||
}
|
||||
needs_repair = any(len(node(d, nid).get("widgets_values", [])) == 9
|
||||
for nid in fixes)
|
||||
if needs_repair:
|
||||
m_pos = add_transform_node(d, [20, 480], [0, 0, 0, 0, 42])
|
||||
m_neg = add_transform_node(d, [20, 1180], [0, 0, 0, 0, -42])
|
||||
set_title(d, m_pos, "Patch rotation +42°")
|
||||
set_title(d, m_neg, "Patch rotation −42°")
|
||||
for nid, (widgets, mat) in fixes.items():
|
||||
node(d, nid)["widgets_values"] = widgets
|
||||
if mat is not None:
|
||||
link(d, m_pos if mat == "+42" else m_neg, nid, "transform_matrix")
|
||||
retitle_groups(d, [
|
||||
"Patch 1: yaw +42° — extract & inpaint-encode",
|
||||
"Patch 2: yaw −42° — extract & inpaint-encode",
|
||||
])
|
||||
set_title(d, 42, "Pinhole 90° → fisheye canvas")
|
||||
set_title(d, 67, "Full-frame pass (low-res)")
|
||||
add_note(d, (
|
||||
"# Outpaint fisheye — SD-inpaint-checkpoint variant\n\n"
|
||||
"Same pipeline as `outpainting_fisheye_flux.json`, but the holes are "
|
||||
"filled with a classic SD inpainting checkpoint (VAEEncodeForInpaint + "
|
||||
"KSampler) instead of Flux: pinhole 90° → 180° fisheye canvas, two ±42° "
|
||||
"pinhole patches and a final full-frame pass are inpainted and "
|
||||
"composited; RealESRGAN upscales the result.\n\n"
|
||||
"- **Set:** input image; positive/negative prompts.\n"
|
||||
"- **Requires (models):** `512-inpainting-ema.safetensors`, "
|
||||
"`RealESRGAN_x4plus.pth`. No custom node packs.\n"
|
||||
"- **Repaired 2026-07:** patch rotations were stored in a pre-2025 "
|
||||
"ReprojectImage schema; they are now explicit TransformToMatrix (±42°) "
|
||||
"nodes + `inverse` flags. Prefer the Flux variant for quality."
|
||||
))
|
||||
save(name, d)
|
||||
|
||||
|
||||
def do_outpainting_fisheye_flux(name="outpainting_fisheye_flux.json"):
|
||||
d = load(name)
|
||||
repair_widgets(d)
|
||||
for nid, inverse in ((42, False), (40, False), (51, True), (80, False),
|
||||
(63, True), (67, False), (72, False)):
|
||||
w = node(d, nid)["widgets_values"]
|
||||
if len(w) == 8:
|
||||
w[6] = inverse # was stored as 0/45/true mixtures
|
||||
retitle_groups(d, [
|
||||
"Patch 1: yaw +42° — extract & Flux inpaint",
|
||||
"Patch 2: yaw −42° — extract & Flux inpaint",
|
||||
])
|
||||
set_title(d, 42, "Pinhole 90° → fisheye canvas")
|
||||
set_title(d, 75, "Patch rotation +42°")
|
||||
set_title(d, 76, "Patch rotation −42°")
|
||||
set_title(d, 67, "Full-frame pass (low-res)")
|
||||
set_title(d, 77, "Upscale full pass to 4096")
|
||||
add_note(d, (
|
||||
"# Outpaint fisheye — Flux variant\n\n"
|
||||
"Pinhole 90° image → 180° fisheye canvas; **Flux Inpainting** fills two "
|
||||
"90° pinhole patches (rotations ±42° from the TransformToMatrix nodes) "
|
||||
"and one low-res full-frame fisheye pass; PorterDuff composites + "
|
||||
"RealESRGAN upscale assemble the final 4096² fisheye "
|
||||
"(saved as `fluxfish`).\n\n"
|
||||
"This is the manual, step-visible version of what "
|
||||
"**OutpaintAnyProjection** does in one node — see "
|
||||
"`Outpaint_fisheye180.json`.\n\n"
|
||||
"- **Set:** input image; prompts in the three Flux Inpainting nodes.\n"
|
||||
"- **Requires:** inpainting_flux (auto-installed), FLUX.1-Fill NF4 "
|
||||
"weights (first-run download), `RealESRGAN_x4plus.pth`."
|
||||
))
|
||||
save(name, d)
|
||||
|
||||
|
||||
def do_sbs180(name="sbs180_workflow.json"):
|
||||
d = load(name)
|
||||
repair_widgets(d)
|
||||
retitle_groups(d, [
|
||||
"1. Fisheye metric depth",
|
||||
"2. Point cloud & eye-baseline shift",
|
||||
"3. Outpaint disocclusions & export equirect",
|
||||
"Previews",
|
||||
])
|
||||
set_title(d, 33, "Eye baseline (shiftX 0.1)")
|
||||
set_title(d, 37, "Right eye (equirect)")
|
||||
set_title(d, 44, "Left eye (equirect)")
|
||||
add_note(d, (
|
||||
"# SBS VR180: synthesize the second eye\n\n"
|
||||
"From one 180° fisheye view, synthesizes a stereo pair: fisheye metric "
|
||||
"depth → point cloud → clean → shift the camera by the eye baseline "
|
||||
"(TransformToMatrix shiftX = 0.1) → re-project to fisheye → four chained "
|
||||
"OutpaintAnyProjection passes fill the disocclusions → both eyes are "
|
||||
"exported as 180° equirectangular images.\n\n"
|
||||
"- **Set:** fisheye input (e.g. from `Outpaint_fisheye180.json`); the "
|
||||
"baseline (0.1 ≈ 6.5 cm when depth is metric); prompts optional.\n"
|
||||
"- **Requires:** inpainting_flux (auto-installed), Depth-Anything V2.\n"
|
||||
"- **Output:** `init_camera_equirect` + `shifted_camera_equirect` — "
|
||||
"combine side-by-side for a VR180 player."
|
||||
))
|
||||
save(name, d)
|
||||
|
||||
|
||||
def do_pointcloud_inpaint(name="pointcloud_inpaint.json"):
|
||||
d = load(name)
|
||||
repair_widgets(d)
|
||||
set_title(d, 6, "Camera shift (dolly −0.1)")
|
||||
set_title(d, 43, "Flux inpaint holes")
|
||||
set_title(d, 54, "Align new depth to cloud")
|
||||
set_title(d, 55, "Merge old + new points")
|
||||
set_groups(d, [
|
||||
("1. Image → point cloud", [1, 46, 18]),
|
||||
("2. Novel view & hole mask", [6, 9, 10, 45, 27, 35, 48, 47]),
|
||||
("3. Flux inpaint", [43, 26]),
|
||||
("4. Lift inpainted region & merge", [49, 54, 50, 55]),
|
||||
("5. Orbit render", [61, 67, 68, 69, 63]),
|
||||
])
|
||||
add_note(d, (
|
||||
"# Iterative point-cloud inpainting\n\n"
|
||||
"Enriches a single-image point cloud with generated content: move the "
|
||||
"camera back → render the cloud (holes appear) → grow + invert the "
|
||||
"coverage mask → **Flux-inpaint the holes** → re-estimate depth on the "
|
||||
"inpainted image → **DepthRenormalizer** aligns it to the original "
|
||||
"cloud's depth → lift the new pixels to 3D → **PointCloudUnion** merges "
|
||||
"everything → orbit render of the enriched scene.\n\n"
|
||||
"One-node alternative: PointcloudTrajectoryEnricher "
|
||||
"(`PC_enricher.json`).\n\n"
|
||||
"- **Set:** input image; camera shift; inpaint prompt.\n"
|
||||
"- **Requires:** inpainting_flux (auto-installed), Depth-Anything V2."
|
||||
))
|
||||
save(name, d)
|
||||
|
||||
|
||||
def do_video_camera(name="video_camera.json"):
|
||||
d = load(name)
|
||||
repair_widgets(d)
|
||||
set_groups(d, [
|
||||
("1. Load & pad video", [9, 23, 26, 27, 21, 68]),
|
||||
("2. Metric video depth", [19, 12]),
|
||||
("3. Re-render with new camera", [16, 11, 10, 6]),
|
||||
("4. Masks & composite", [39, 41, 60, 75, 54, 74]),
|
||||
("5. Auto-caption (Florence2)", [63, 62, 61]),
|
||||
("6. WAN VACE re-generation", [29, 30, 37, 32, 33, 34, 28, 31, 36, 35]),
|
||||
("7. Outputs", [4, 13, 14, 38]),
|
||||
])
|
||||
set_title(d, 11, "Final camera pose (edit me)")
|
||||
set_title(d, 6, "Re-render along trajectory")
|
||||
set_title(d, 28, "WAN VACE control")
|
||||
add_note(d, (
|
||||
"# Re-shoot a video with a new camera move\n\n"
|
||||
"Re-renders an input video along a user-defined camera trajectory and "
|
||||
"uses WAN 2.1 VACE to regenerate what the new camera reveals:\n\n"
|
||||
"1. Video is padded square and depth-estimated per frame "
|
||||
"(Video-Depth-Anything metric).\n"
|
||||
"2. **VideoCameraMotionSequence** lifts each frame to a point cloud and "
|
||||
"re-renders it along the SE(3)-interpolated trajectory.\n"
|
||||
"3. Disocclusion masks + the re-rendered frames become VACE "
|
||||
"control video/masks; **Florence2** auto-captions the clip as the "
|
||||
"prompt; WAN 2.1 VACE 14B fills the gaps.\n\n"
|
||||
"- **Set:** video path (VHS Load Video Path); the camera move "
|
||||
"(two TransformToMatrix poses); override the auto-caption in "
|
||||
"CLIPTextEncode if desired.\n"
|
||||
"- **Requires (packs):** VideoHelperSuite, ComfyUI-Florence2, KJNodes "
|
||||
"(GetImageRangeFromBatch), ComfyUI-Easy-Use (math), ComfyUI-Image-"
|
||||
"Filters (BlurMaskFast).\n"
|
||||
"- **Requires (models):** `metric_video_depth_anything_vitl.pth` "
|
||||
"(install.sh `depth`), WAN 2.1 VACE 14B + umt5-xxl + WAN VAE "
|
||||
"(install.sh `vae`), Florence-2 (auto-download).\n"
|
||||
"- **Outputs:** re-rendered composite WEBM, depth WEBM, final VACE clip."
|
||||
))
|
||||
save(name, d)
|
||||
|
||||
|
||||
def do_wan_vace_ref(name="wan_vace_ref_to_video.json"):
|
||||
d = load(name)
|
||||
repair_widgets(d)
|
||||
# broken model filename: comma + missing underscore
|
||||
n37 = node(d, 37)
|
||||
if n37["widgets_values"][0] == "wan2,1_vace14B_fp16.safetensors":
|
||||
n37["widgets_values"][0] = "wan2.1_vace_14B_fp16.safetensors"
|
||||
set_title(d, 70, "Render control frames + masks")
|
||||
set_title(d, 55, "WAN VACE control")
|
||||
set_groups(d, [
|
||||
("1. Fisheye image → point cloud", [52, 62, 83, 82, 81, 63, 84]),
|
||||
("2. Camera-motion control video", [77, 70, 78, 80]),
|
||||
("3. WAN VACE generation", [37, 54, 38, 6, 7, 39, 55, 3, 56, 8]),
|
||||
("4. Save", [60, 85, 58]),
|
||||
])
|
||||
add_note(d, (
|
||||
"# WAN VACE: still image + camera move → video\n\n"
|
||||
"Turns a single 180° fisheye still into a camera-move video: metric "
|
||||
"depth → point cloud → **CameraMotionNode** renders point-splat frames "
|
||||
"and masks along a loaded trajectory; these become the VACE control "
|
||||
"video/masks with the original image as the reference, and WAN 2.1 VACE "
|
||||
"14B synthesizes the final clip from your prompt.\n\n"
|
||||
"- **Set:** fisheye image; trajectory file (record one with "
|
||||
"SaveTrajectory); the positive prompt.\n"
|
||||
"- **Requires:** WAN 2.1 VACE models (install.sh `vae`), Depth-Anything "
|
||||
"V2, VideoHelperSuite (only for the h264/MP4 export — SaveWEBM works "
|
||||
"without it).\n"
|
||||
"- **Fixed 2026-07:** the UNET filename contained a typo "
|
||||
"(`wan2,1_vace14B` → `wan2.1_vace_14B_fp16.safetensors`)."
|
||||
))
|
||||
save(name, d)
|
||||
|
||||
|
||||
def main():
|
||||
for fn in (
|
||||
do_demo_camera, do_outpaint_node_test, do_fisheye_to_pointcloud,
|
||||
do_pointcloud, do_pointcloud_walker, do_test_pointcloud_loading,
|
||||
do_pc_enricher, do_fisheye_depth, do_outpaint_fisheye180,
|
||||
do_outpainting_fisheye_sd, do_outpainting_fisheye_flux, do_sbs180,
|
||||
do_pointcloud_inpaint, do_video_camera, do_wan_vace_ref,
|
||||
):
|
||||
fn()
|
||||
print(f"[ok] {fn.__name__}")
|
||||
# video_to_4d_world / video_to_4d_walkable_world already have notes+groups;
|
||||
# normalize formatting only.
|
||||
for name in ("video_to_4d_world.json", "video_to_4d_walkable_world.json"):
|
||||
save(name, load(name))
|
||||
print(f"[ok] reformat {name}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -18,6 +18,7 @@ with small synthetic data:
|
||||
8. align_depth_scale (world_nodes, contract C4) + DepthEdgeFilter
|
||||
9. FuseSplats weighted voxel fusion
|
||||
10. SphereSplatSeed pano -> splat sphere -> render round-trip
|
||||
11. Fisheye image-circle exclusion (motion mask / tracks / splat split)
|
||||
"""
|
||||
|
||||
import math
|
||||
@@ -445,6 +446,63 @@ def test_10_sphere_splat_seed():
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Runner
|
||||
# --------------------------------------------------------------------------- #
|
||||
def test_11_fisheye_circle_exclusion():
|
||||
"""Fisheye pixels/points outside the image circle (r > 1) must be ignored:
|
||||
corners never become 'dynamic', corner tracks never lift to 3D, and points
|
||||
at view angles beyond fov/2 are not matched against the mask."""
|
||||
T, H, W = 6, 32, 32
|
||||
fov = 180.0
|
||||
|
||||
# --- MotionMaskFromDepth: wildly flickering corners must stay static ----
|
||||
depth = torch.full((T, H, W), 5.0)
|
||||
depth[:, 14:18, 14:18] = torch.linspace(5.0, 2.0, T).view(T, 1, 1) # real motion
|
||||
u = torch.linspace(-1, 1, W).view(1, W).expand(H, W)
|
||||
v = torch.linspace(-1, 1, H).view(H, 1).expand(H, W)
|
||||
corners = (u * u + v * v) > 1.0 + 1e-6
|
||||
for t in range(T): # garbage depth flicker outside the circle
|
||||
depth[t][corners] = 1.0 if t % 2 == 0 else 10.0
|
||||
identity = torch.eye(4).unsqueeze(0).expand(T, 4, 4).contiguous()
|
||||
(dyn,) = GS4D_nodes.MotionMaskFromDepth().motion_mask(
|
||||
depth_seq=depth, trajectory=identity, input_projection="FISHEYE",
|
||||
input_horizontal_fov=fov, threshold=0.10, frame_gap=2, dilate=0,
|
||||
device="cpu",
|
||||
)
|
||||
assert dyn[:, corners].max().item() == 0.0, "fisheye corners were flagged dynamic"
|
||||
assert dyn[:, 14:18, 14:18].max().item() == 1.0, "real in-circle motion missed"
|
||||
|
||||
# --- TracksToTrajectories: corner track must be dropped -----------------
|
||||
tracks = torch.zeros(T, 2, 2)
|
||||
tracks[:, 0, 0] = W / 2.0 # center track
|
||||
tracks[:, 0, 1] = H / 2.0
|
||||
tracks[:, 1, 0] = 0.0 # corner track (u=v=-1, r=1.414)
|
||||
tracks[:, 1, 1] = 0.0
|
||||
visibility = torch.ones(T, 2)
|
||||
traj3d, track_ok = GS4D_nodes.TracksToTrajectories().tracks_to_trajectories(
|
||||
tracks=tracks, visibility=visibility, depth_seq=depth,
|
||||
input_projection="FISHEYE", input_horizontal_fov=fov,
|
||||
min_visible_frac=0.5, device="cpu",
|
||||
)
|
||||
assert bool(track_ok[0]), "in-circle track unexpectedly invalid"
|
||||
assert not bool(track_ok[1]), "out-of-circle corner track was lifted to 3D"
|
||||
|
||||
# --- SplitSplatsByMask: angle beyond fov/2 lands diagonally inside the
|
||||
# [-1,1] square (r=1.33, u=v~0.94) but must not be matched to the mask ---
|
||||
front = torch.tensor([[0.0, 0.0, 1.0]])
|
||||
theta = math.radians(120.0)
|
||||
behind_diag = torch.tensor([[
|
||||
math.sin(theta) * math.cos(math.radians(45.0)),
|
||||
math.sin(theta) * math.sin(math.radians(45.0)),
|
||||
math.cos(theta),
|
||||
]])
|
||||
splats = make_splats(torch.cat([front, behind_diag], dim=0))
|
||||
inside, outside = GS4D_nodes.SplitSplatsByMask().split_splats(
|
||||
splats=splats, mask=torch.ones(H, W), projection="FISHEYE",
|
||||
horizontal_fov=fov, threshold=0.5, device="cpu",
|
||||
)
|
||||
assert inside.xyz.shape[0] == 1, "expected only the in-fov splat inside"
|
||||
assert outside.xyz.shape[0] == 1, "beyond-fov splat must fall outside"
|
||||
|
||||
|
||||
TESTS = [
|
||||
test_01_interpolate_se3,
|
||||
test_02_render_gaussians_shapes_and_empty,
|
||||
@@ -456,6 +514,7 @@ TESTS = [
|
||||
test_08_align_depth_scale_and_depth_edge_filter,
|
||||
test_09_fuse_splats,
|
||||
test_10_sphere_splat_seed,
|
||||
test_11_fisheye_circle_exclusion,
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
"""Offline tests for install.py logic and the flux-pack import resolution in
|
||||
flux_fisheye_filling_nodes.py. Stubs pip/git so nothing is actually installed.
|
||||
|
||||
Run: python notebooks/test_install_logic.py
|
||||
"""
|
||||
|
||||
import importlib.util
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
import types
|
||||
|
||||
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
PASS = []
|
||||
|
||||
|
||||
def ok(name):
|
||||
PASS.append(name)
|
||||
print(f"[ ok ] {name}")
|
||||
|
||||
|
||||
def load_install_module():
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"camera_install", os.path.join(REPO, "install.py")
|
||||
)
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
return mod
|
||||
|
||||
|
||||
def stub_commands(mod):
|
||||
"""Replace subprocess-based helpers with recorders."""
|
||||
calls = []
|
||||
mod._run = lambda cmd, cwd=None: calls.append((tuple(cmd), cwd))
|
||||
mod._pip_install = lambda *args: calls.append((("pip",) + args, None))
|
||||
mod._git = lambda: "git"
|
||||
return calls
|
||||
|
||||
|
||||
def test_sharp_early_return():
|
||||
mod = load_install_module()
|
||||
calls = stub_commands(mod)
|
||||
# Build the "already materialized" state explicitly — the repo's own
|
||||
# submodule may not be checked out (e.g. CI clones without --recursive).
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
mod.NODE_DIR = os.path.join(tmp, "camera-comfyUI")
|
||||
os.makedirs(os.path.join(mod.NODE_DIR, "submodules", "ml-sharpt", "src", "sharp"))
|
||||
mod.ensure_sharp_checkout()
|
||||
assert calls == [], f"expected no commands, got {calls}"
|
||||
ok("sharp checkout present -> no git calls")
|
||||
|
||||
|
||||
def test_sharp_clone_fallback():
|
||||
mod = load_install_module()
|
||||
calls = stub_commands(mod)
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
mod.NODE_DIR = os.path.join(tmp, "custom_nodes", "camera-comfyUI")
|
||||
os.makedirs(mod.NODE_DIR)
|
||||
# No .git -> should go straight to direct clone
|
||||
mod.ensure_sharp_checkout()
|
||||
assert len(calls) == 1 and "clone" in calls[0][0], calls
|
||||
assert mod.SHARP_GIT_URL in calls[0][0], calls
|
||||
ok("sharp missing + no .git -> direct clone")
|
||||
|
||||
|
||||
def test_vggt_uses_https_git():
|
||||
mod = load_install_module()
|
||||
calls = stub_commands(mod)
|
||||
mod._importable = lambda name: False
|
||||
mod.ensure_vggt()
|
||||
assert calls == [(("pip", "vggt @ git+https://github.com/facebookresearch/vggt.git"), None)], calls
|
||||
ok("vggt -> pip install from git+https (no ssh)")
|
||||
|
||||
|
||||
def test_flux_pack_skip_outside_comfyui():
|
||||
mod = load_install_module()
|
||||
calls = stub_commands(mod)
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
mod.NODE_DIR = os.path.join(tmp, "somewhere", "camera-comfyUI")
|
||||
os.makedirs(mod.NODE_DIR)
|
||||
mod.ensure_flux_inpainting_pack()
|
||||
assert calls == [], calls
|
||||
ok("flux pack: skipped when parent is not custom_nodes")
|
||||
|
||||
|
||||
def test_flux_pack_detects_existing_and_clones_when_missing():
|
||||
mod = load_install_module()
|
||||
for existing in ("inpainting_flux", "ComfyUI-Flux-Inpainting", "ComfyUI-Flux-Inpainting-main"):
|
||||
calls = stub_commands(mod)
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
cn = os.path.join(tmp, "custom_nodes")
|
||||
mod.NODE_DIR = os.path.join(cn, "camera-comfyUI")
|
||||
os.makedirs(mod.NODE_DIR)
|
||||
os.makedirs(os.path.join(cn, existing))
|
||||
mod.ensure_flux_inpainting_pack()
|
||||
assert calls == [], f"{existing}: {calls}"
|
||||
ok("flux pack: all existing folder names detected, no clone")
|
||||
|
||||
calls = stub_commands(mod)
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
cn = os.path.join(tmp, "custom_nodes")
|
||||
mod.NODE_DIR = os.path.join(cn, "camera-comfyUI")
|
||||
os.makedirs(mod.NODE_DIR)
|
||||
mod.ensure_flux_inpainting_pack()
|
||||
assert len(calls) == 1 and "clone" in calls[0][0], calls
|
||||
assert calls[0][0][-1] == os.path.join(cn, "inpainting_flux"), calls
|
||||
ok("flux pack: missing -> cloned as custom_nodes/inpainting_flux")
|
||||
|
||||
|
||||
def test_example_inputs_copied():
|
||||
mod = load_install_module()
|
||||
stub_commands(mod)
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
cn = os.path.join(tmp, "ComfyUI", "custom_nodes")
|
||||
mod.NODE_DIR = os.path.join(cn, "camera-comfyUI")
|
||||
src = os.path.join(mod.NODE_DIR, "example_inputs")
|
||||
os.makedirs(src)
|
||||
with open(os.path.join(src, "example.jpg"), "w") as f:
|
||||
f.write("x")
|
||||
input_dir = os.path.join(tmp, "ComfyUI", "input")
|
||||
os.makedirs(input_dir)
|
||||
with open(os.path.join(input_dir, "existing.jpg"), "w") as f:
|
||||
f.write("keep me")
|
||||
mod.ensure_example_inputs()
|
||||
mod.ensure_example_inputs() # idempotent, no overwrite
|
||||
assert os.path.isfile(os.path.join(input_dir, "example.jpg"))
|
||||
assert open(os.path.join(input_dir, "existing.jpg")).read() == "keep me"
|
||||
ok("example inputs copied to ComfyUI input dir, no overwrite")
|
||||
|
||||
|
||||
def test_main_never_fails():
|
||||
mod = load_install_module()
|
||||
|
||||
def boom():
|
||||
raise RuntimeError("no network")
|
||||
|
||||
mod.STEPS = (("step-a", boom), ("step-b", boom))
|
||||
assert mod.main() == 0
|
||||
ok("main() returns 0 even when every step fails")
|
||||
|
||||
|
||||
def test_flux_import_resolution():
|
||||
"""Build a fake ComfyUI tree and check _import_flux_inpainting finds the
|
||||
pack under a dashed folder name and honors its relative imports."""
|
||||
if "PIL" not in sys.modules:
|
||||
try:
|
||||
import PIL # noqa: F401
|
||||
except ImportError:
|
||||
pil = types.ModuleType("PIL")
|
||||
pil.Image = types.SimpleNamespace()
|
||||
sys.modules["PIL"] = pil
|
||||
sys.modules["PIL.Image"] = types.ModuleType("PIL.Image")
|
||||
|
||||
tmp = tempfile.mkdtemp()
|
||||
try:
|
||||
cn = os.path.join(tmp, "ComfyUI", "custom_nodes")
|
||||
pack = os.path.join(cn, "campack")
|
||||
os.makedirs(pack)
|
||||
for fname in ("reprojection_nodes.py", "flux_fisheye_filling_nodes.py"):
|
||||
shutil.copy(os.path.join(REPO, fname), pack)
|
||||
open(os.path.join(pack, "__init__.py"), "w").close()
|
||||
|
||||
# Fake flux pack under a dashed (non-identifier) folder name with a
|
||||
# relative import, mirroring the real repo layout.
|
||||
flux = os.path.join(cn, "ComfyUI-Flux-Inpainting")
|
||||
os.makedirs(os.path.join(flux, "modules"))
|
||||
open(os.path.join(flux, "__init__.py"), "w").close()
|
||||
open(os.path.join(flux, "modules", "__init__.py"), "w").close()
|
||||
with open(os.path.join(flux, "modules", "load_util.py"), "w") as f:
|
||||
f.write("MARKER = 'loaded'\n")
|
||||
with open(os.path.join(flux, "nodes.py"), "w") as f:
|
||||
f.write(
|
||||
"from .modules.load_util import MARKER\n"
|
||||
"class FluxNF4Inpainting:\n"
|
||||
" marker = MARKER\n"
|
||||
)
|
||||
|
||||
sys.path.insert(0, os.path.dirname(pack))
|
||||
mod = importlib.import_module("campack.flux_fisheye_filling_nodes")
|
||||
assert mod._flux_import_error is None, mod._flux_import_error
|
||||
assert mod.FluxInpainting is not None
|
||||
assert mod.FluxInpainting.marker == "loaded"
|
||||
ok("flux import: dashed folder name resolved incl. relative imports")
|
||||
finally:
|
||||
shutil.rmtree(tmp, ignore_errors=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_sharp_early_return()
|
||||
test_sharp_clone_fallback()
|
||||
test_vggt_uses_https_git()
|
||||
test_flux_pack_skip_outside_comfyui()
|
||||
test_flux_pack_detects_existing_and_clones_when_missing()
|
||||
test_example_inputs_copied()
|
||||
test_main_never_fails()
|
||||
test_flux_import_resolution()
|
||||
print(f"\n{len(PASS)} install-logic checks passed")
|
||||
@@ -0,0 +1,133 @@
|
||||
"""Validate workflows/*.json against the current node definitions.
|
||||
|
||||
For every camera-comfyUI node used in a workflow, compares the stored
|
||||
widgets_values against the widget list derived from the node's INPUT_TYPES
|
||||
(required + optional, in order, counting only widget-type inputs). A length
|
||||
mismatch means the workflow predates a node-schema change and will load with
|
||||
silently shifted/défault values.
|
||||
|
||||
Run: python notebooks/validate_workflows.py
|
||||
Exit code 1 if any mismatch is found (missing node types are also reported).
|
||||
"""
|
||||
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import types
|
||||
|
||||
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
if REPO_ROOT not in sys.path:
|
||||
sys.path.insert(0, REPO_ROOT)
|
||||
|
||||
_TMP_DIR = tempfile.mkdtemp(prefix="wf_validate_")
|
||||
|
||||
|
||||
def _stub_get_save_image_path(filename_prefix, output_dir, *args, **kwargs):
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
return output_dir, filename_prefix, 0, "", filename_prefix
|
||||
|
||||
|
||||
_fp = types.ModuleType("folder_paths")
|
||||
_fp.get_input_directory = lambda: _TMP_DIR
|
||||
_fp.get_output_directory = lambda: _TMP_DIR
|
||||
_fp.get_temp_directory = lambda: _TMP_DIR
|
||||
_fp.get_save_image_path = _stub_get_save_image_path
|
||||
_fp.get_annotated_filepath = lambda name: os.path.join(_TMP_DIR, name)
|
||||
_fp.exists_annotated_filepath = lambda name: os.path.exists(os.path.join(_TMP_DIR, name))
|
||||
_fp.get_filename_list = lambda folder: []
|
||||
_fp.models_dir = _TMP_DIR
|
||||
sys.modules["folder_paths"] = _fp
|
||||
|
||||
# Light stubs for heavy deps that some modules import at module level but that
|
||||
# INPUT_TYPES itself does not need.
|
||||
for name in ("transformers", "diffusers"):
|
||||
if name not in sys.modules:
|
||||
try:
|
||||
__import__(name)
|
||||
except ImportError:
|
||||
stub = types.ModuleType(name)
|
||||
stub.pipeline = lambda *a, **k: None
|
||||
sys.modules[name] = stub
|
||||
|
||||
# Import the repo as a package so modules with relative imports load too.
|
||||
import importlib.util # noqa: E402
|
||||
|
||||
_spec = importlib.util.spec_from_file_location(
|
||||
"camcomfy",
|
||||
os.path.join(REPO_ROOT, "__init__.py"),
|
||||
submodule_search_locations=[REPO_ROOT],
|
||||
)
|
||||
_pkg = importlib.util.module_from_spec(_spec)
|
||||
sys.modules["camcomfy"] = _pkg
|
||||
_spec.loader.exec_module(_pkg)
|
||||
NODE_CLASS_MAPPINGS = dict(_pkg.NODE_CLASS_MAPPINGS)
|
||||
|
||||
WIDGET_TYPES = {"INT", "FLOAT", "STRING", "BOOLEAN"}
|
||||
|
||||
|
||||
def widget_specs(cls):
|
||||
"""Ordered (name, combo_options|None) for a node's widget inputs."""
|
||||
it = cls.INPUT_TYPES()
|
||||
specs = []
|
||||
for section in ("required", "optional"):
|
||||
for name, spec in it.get(section, {}).items():
|
||||
t = spec[0] if isinstance(spec, (tuple, list)) and spec else spec
|
||||
if isinstance(t, (list, tuple)): # combo box (either sequence type)
|
||||
specs.append((name, list(t)))
|
||||
elif isinstance(t, str) and t in WIDGET_TYPES:
|
||||
specs.append((name, None))
|
||||
# ComfyUI appends a control_after_generate widget after seeds
|
||||
if t == "INT" and name in ("seed", "noise_seed"):
|
||||
specs.append((f"{name}:control_after_generate", None))
|
||||
return specs
|
||||
|
||||
|
||||
def main() -> int:
|
||||
problems = 0
|
||||
for path in sorted(glob.glob(os.path.join(REPO_ROOT, "workflows", "**", "*.json"),
|
||||
recursive=True)):
|
||||
data = json.load(open(path, encoding="utf-8"))
|
||||
header_shown = False
|
||||
|
||||
def report(msg):
|
||||
nonlocal header_shown, problems
|
||||
if not header_shown:
|
||||
print(f"\n=== {os.path.basename(path)}")
|
||||
header_shown = True
|
||||
print(f" {msg}")
|
||||
problems += 1
|
||||
|
||||
for n in data.get("nodes", []):
|
||||
t = n["type"]
|
||||
if t not in NODE_CLASS_MAPPINGS:
|
||||
continue # builtin or third-party node
|
||||
specs = widget_specs(NODE_CLASS_MAPPINGS[t])
|
||||
got = n.get("widgets_values") or []
|
||||
if isinstance(got, dict):
|
||||
continue # API-style dict widgets (third-party save format)
|
||||
if len(got) != len(specs):
|
||||
report(
|
||||
f"N{n['id']} {t}: {len(got)} widget values, node now has "
|
||||
f"{len(specs)} widgets {[s[0] for s in specs]}; "
|
||||
f"stored={json.dumps(got)[:100]}"
|
||||
)
|
||||
continue
|
||||
for (name, options), value in zip(specs, got):
|
||||
# empty option lists are dynamic file dropdowns (input dir
|
||||
# listing) — not verifiable outside a real ComfyUI install
|
||||
if options and value not in options:
|
||||
report(
|
||||
f"N{n['id']} {t}: widget '{name}' has stale value "
|
||||
f"{value!r}, valid options are {options}"
|
||||
)
|
||||
if problems:
|
||||
print(f"\n{problems} mismatches found")
|
||||
return 1
|
||||
print("all workflow widget schemas match current node definitions")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -174,9 +174,10 @@ def _import_vggt() -> Tuple[Any, Any]:
|
||||
return VGGT, pose_encoding_to_extri_intri
|
||||
except ImportError as exc:
|
||||
raise ModuleNotFoundError(
|
||||
"VGGT is not installed. Install it with `pip install vggt` (or "
|
||||
"`pip install git+https://github.com/facebookresearch/vggt.git`), or clone "
|
||||
f"https://github.com/facebookresearch/vggt into {vggt_clone_path!r}. "
|
||||
"VGGT is not installed. Run this pack's install.py (ComfyUI-Manager does this "
|
||||
"automatically), or install it manually with "
|
||||
"`pip install git+https://github.com/facebookresearch/vggt.git` (it is not on PyPI), "
|
||||
f"or clone https://github.com/facebookresearch/vggt into {vggt_clone_path!r}. "
|
||||
"It also requires `huggingface_hub` to download the facebook/VGGT-1B weights."
|
||||
) from exc
|
||||
|
||||
|
||||
@@ -1,13 +1,23 @@
|
||||
[project]
|
||||
name = "camera-comfyui"
|
||||
description = "Custom ComfyUI nodes for camera projections (pinhole/fisheye/equirectangular), depth, point clouds, camera trajectories, and 3D/4D Gaussian splatting — including video-to-4D-world workflows."
|
||||
version = "1.0.0"
|
||||
version = "1.1.0"
|
||||
license = { file = "LICENSE" }
|
||||
# Keep in sync with requirements.txt. GitHub/CUDA-flavored extras (vggt,
|
||||
# gsplat) and the ComfyUI-Flux-Inpainting sibling pack are installed by
|
||||
# install.py, which ComfyUI-Manager runs automatically after install.
|
||||
dependencies = [
|
||||
"transformers==4.50.0",
|
||||
"diffusers==0.33.1",
|
||||
"open3d==0.19.0",
|
||||
"protobuf",
|
||||
"click",
|
||||
"timm",
|
||||
"plyfile",
|
||||
"pillow-heif",
|
||||
"matplotlib",
|
||||
"imageio",
|
||||
"imageio-ffmpeg",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
|
||||
@@ -2,9 +2,17 @@ transformers==4.50.0
|
||||
diffusers==0.33.1
|
||||
open3d==0.19.0
|
||||
protobuf
|
||||
# Runtime deps of the bundled SHARP submodule (submodules/ml-sharpt), so
|
||||
# ImageToSplat & co. work out of the box. torch/torchvision/scipy/tqdm come
|
||||
# with ComfyUI itself; gsplat and vggt are GitHub/CUDA-flavored and are
|
||||
# handled by install.py (run automatically by ComfyUI-Manager).
|
||||
click
|
||||
timm
|
||||
plyfile
|
||||
pillow-heif
|
||||
matplotlib
|
||||
imageio
|
||||
imageio-ffmpeg
|
||||
# open3d is optional, see pointcloud_nodes.py
|
||||
# Python version: 3.12.x (used by embedded python)
|
||||
# All versions pinned to match embedded environment
|
||||
# If using a different Python, adjust versions accordingly
|
||||
# For full reproducibility, consider using a virtual environment
|
||||
# and pip freeze > requirements.txt
|
||||
|
||||
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 27 KiB |
@@ -1 +1,289 @@
|
||||
{"id":"8c6e5ec1-4ff2-42d9-9408-fcd0a23d362a","revision":0,"last_node_id":11,"last_link_id":17,"nodes":[{"id":3,"type":"PreviewImage","pos":[-5.768195629119873,204.72561645507812],"size":[210,246],"flags":{},"order":2,"mode":0,"inputs":[{"localized_name":"images","name":"images","type":"IMAGE","link":16}],"outputs":[],"properties":{"Node name for S&R":"PreviewImage"},"widgets_values":[""]},{"id":4,"type":"MaskToImage","pos":[-30.901016235351562,532.864013671875],"size":[264.5999755859375,26],"flags":{},"order":3,"mode":0,"inputs":[{"localized_name":"mask","name":"mask","type":"MASK","link":17}],"outputs":[{"localized_name":"IMAGE","name":"IMAGE","type":"IMAGE","links":[4]}],"properties":{"Node name for S&R":"MaskToImage"},"widgets_values":[]},{"id":2,"type":"LoadImage","pos":[-843.689208984375,354.9311828613281],"size":[315,314],"flags":{},"order":0,"mode":0,"inputs":[],"outputs":[{"localized_name":"IMAGE","name":"IMAGE","type":"IMAGE","links":[15]},{"localized_name":"MASK","name":"MASK","type":"MASK","links":[]}],"properties":{"Node name for S&R":"LoadImage"},"widgets_values":["flux_dev_example.png","image",""]},{"id":5,"type":"PreviewImage","pos":[296.6977844238281,611.6702880859375],"size":[210,246],"flags":{},"order":4,"mode":0,"inputs":[{"localized_name":"images","name":"images","type":"IMAGE","link":4}],"outputs":[],"properties":{"Node name for S&R":"PreviewImage"},"widgets_values":[""]},{"id":11,"type":"OutpaintAnyProjection","pos":[-498.9301452636719,317.144287109375],"size":[400,492],"flags":{},"order":1,"mode":0,"inputs":[{"localized_name":"image","name":"image","type":"IMAGE","link":15},{"localized_name":"mask","name":"mask","shape":7,"type":"MASK","link":null}],"outputs":[{"localized_name":"final_image","name":"final_image","type":"IMAGE","links":[16]},{"localized_name":"needs_inpaint_mask","name":"needs_inpaint_mask","type":"MASK","links":[17]}],"properties":{"Node name for S&R":"OutpaintAnyProjection"},"widgets_values":["PINHOLE",90,"FISHEYE",180,4096,4096,"PINHOLE",90,1024,45,0,"",10,false,30,1,false]}],"links":[[4,4,0,5,0,"IMAGE"],[15,2,0,11,0,"IMAGE"],[16,11,0,3,0,"IMAGE"],[17,11,1,4,0,"MASK"]],"groups":[],"config":{},"extra":{"ds":{"scale":0.8390545288824094,"offset":[703.1897327574803,-308.43624510132975]}},"version":0.4}
|
||||
{
|
||||
"id": "8c6e5ec1-4ff2-42d9-9408-fcd0a23d362a",
|
||||
"revision": 0,
|
||||
"last_node_id": 12,
|
||||
"last_link_id": 17,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 3,
|
||||
"type": "PreviewImage",
|
||||
"pos": [
|
||||
-5.768195629119873,
|
||||
204.72561645507812
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
246
|
||||
],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "images",
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 16
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"Node name for S&R": "PreviewImage"
|
||||
},
|
||||
"widgets_values": [
|
||||
""
|
||||
],
|
||||
"title": "Result"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"type": "MaskToImage",
|
||||
"pos": [
|
||||
-30.901016235351562,
|
||||
532.864013671875
|
||||
],
|
||||
"size": [
|
||||
264.5999755859375,
|
||||
26
|
||||
],
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "mask",
|
||||
"name": "mask",
|
||||
"type": "MASK",
|
||||
"link": 17
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"localized_name": "IMAGE",
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
4
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "MaskToImage"
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "LoadImage",
|
||||
"pos": [
|
||||
-843.689208984375,
|
||||
354.9311828613281
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
314
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"localized_name": "IMAGE",
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
15
|
||||
]
|
||||
},
|
||||
{
|
||||
"localized_name": "MASK",
|
||||
"name": "MASK",
|
||||
"type": "MASK",
|
||||
"links": []
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "LoadImage"
|
||||
},
|
||||
"widgets_values": [
|
||||
"camera_example_pinhole.jpg",
|
||||
"image",
|
||||
""
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"type": "PreviewImage",
|
||||
"pos": [
|
||||
296.6977844238281,
|
||||
611.6702880859375
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
246
|
||||
],
|
||||
"flags": {},
|
||||
"order": 4,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "images",
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 4
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"Node name for S&R": "PreviewImage"
|
||||
},
|
||||
"widgets_values": [
|
||||
""
|
||||
],
|
||||
"title": "Holes still to fill"
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"type": "OutpaintAnyProjection",
|
||||
"pos": [
|
||||
-498.9301452636719,
|
||||
317.144287109375
|
||||
],
|
||||
"size": [
|
||||
400,
|
||||
492
|
||||
],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "image",
|
||||
"name": "image",
|
||||
"type": "IMAGE",
|
||||
"link": 15
|
||||
},
|
||||
{
|
||||
"localized_name": "mask",
|
||||
"name": "mask",
|
||||
"shape": 7,
|
||||
"type": "MASK",
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"localized_name": "final_image",
|
||||
"name": "final_image",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
16
|
||||
]
|
||||
},
|
||||
{
|
||||
"localized_name": "needs_inpaint_mask",
|
||||
"name": "needs_inpaint_mask",
|
||||
"type": "MASK",
|
||||
"links": [
|
||||
17
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "OutpaintAnyProjection"
|
||||
},
|
||||
"widgets_values": [
|
||||
"PINHOLE",
|
||||
90,
|
||||
"FISHEYE",
|
||||
180,
|
||||
4096,
|
||||
4096,
|
||||
"PINHOLE",
|
||||
90,
|
||||
1024,
|
||||
45,
|
||||
0,
|
||||
"",
|
||||
10,
|
||||
false,
|
||||
30,
|
||||
1,
|
||||
false
|
||||
],
|
||||
"title": "Outpaint one patch (yaw +45°)"
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"type": "MarkdownNote",
|
||||
"title": "About this workflow",
|
||||
"pos": [
|
||||
-1403.689208984375,
|
||||
204.72561645507812
|
||||
],
|
||||
"size": [
|
||||
520,
|
||||
288
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"properties": {},
|
||||
"widgets_values": [
|
||||
"# OutpaintAnyProjection smoke test\n\nSingle-node sanity check: a 90° pinhole image is placed on a 180° fisheye canvas and one 90° pinhole patch at yaw +45° is Flux-inpainted (10 steps for speed).\n\nOutputs: the partially outpainted canvas and the *remaining holes* mask — chain more OutpaintAnyProjection nodes at other angles to fill it (see `Outpaint_fisheye180.json`).\n\n- **Set:** input image; prompt inside the node (empty = unconditional).\n- **Requires:** `custom_nodes/inpainting_flux` (installed automatically by this pack's install.py) — downloads FLUX.1-Fill NF4 weights on first run."
|
||||
],
|
||||
"color": "#432",
|
||||
"bgcolor": "#653"
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
4,
|
||||
4,
|
||||
0,
|
||||
5,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
15,
|
||||
2,
|
||||
0,
|
||||
11,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
16,
|
||||
11,
|
||||
0,
|
||||
3,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
17,
|
||||
11,
|
||||
1,
|
||||
4,
|
||||
0,
|
||||
"MASK"
|
||||
]
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 0.8390545288824094,
|
||||
"offset": [
|
||||
703.1897327574803,
|
||||
-308.43624510132975
|
||||
]
|
||||
},
|
||||
"camera_comfyui_rev": 1
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "312a3a27-6189-4cd7-8c9f-5a62881c623e",
|
||||
"revision": 0,
|
||||
"last_node_id": 18,
|
||||
"last_node_id": 19,
|
||||
"last_link_id": 37,
|
||||
"nodes": [
|
||||
{
|
||||
@@ -38,7 +38,7 @@
|
||||
"Node name for S&R": "LoadImage"
|
||||
},
|
||||
"widgets_values": [
|
||||
"Fisheye_outpainted_flux_dev.png",
|
||||
"camera_example_fisheye.jpg",
|
||||
"image"
|
||||
]
|
||||
},
|
||||
@@ -213,7 +213,9 @@
|
||||
90,
|
||||
1024,
|
||||
4096,
|
||||
21
|
||||
"SOFTMERGE",
|
||||
21,
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -326,14 +328,11 @@
|
||||
10,
|
||||
7.5,
|
||||
5,
|
||||
"PINHOLE",
|
||||
90,
|
||||
1024,
|
||||
1024,
|
||||
"open",
|
||||
9,
|
||||
15
|
||||
]
|
||||
0.07,
|
||||
3,
|
||||
"Depth-Anything-V2-Metric-Indoor-Base-hf"
|
||||
],
|
||||
"title": "Enrich cloud along trajectory (Flux outpaint)"
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
@@ -411,8 +410,36 @@
|
||||
90,
|
||||
1024,
|
||||
1024,
|
||||
2
|
||||
]
|
||||
2,
|
||||
0,
|
||||
false,
|
||||
false
|
||||
],
|
||||
"title": "Orbit preview of enriched cloud"
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"type": "MarkdownNote",
|
||||
"title": "About this workflow",
|
||||
"pos": [
|
||||
982.239013671875,
|
||||
1182.49755859375
|
||||
],
|
||||
"size": [
|
||||
520,
|
||||
314
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"properties": {},
|
||||
"widgets_values": [
|
||||
"# Point cloud enricher (outpaint along a trajectory)\n\nFisheye image → metric depth → point cloud, then **PointcloudTrajectoryEnricher** walks the loaded camera trajectory: at each pose it renders the cloud, Flux-inpaints the disocclusion holes, re-estimates depth, aligns it and merges the new points into the cloud. The enriched cloud is saved and previewed as an orbit video.\n\nThis is the one-node version of `pointcloud_inpaint.json`.\n\n- **Set:** input image; trajectory .npy (record one with SaveTrajectory); prompt inside the enricher.\n- **Requires:** inpainting_flux (auto-installed), Depth-Anything V2, FLUX.1-Fill NF4 weights.\n- **Note:** 2026-07 schema migration — the enricher's render/reproject options are now internal; voxel merge params were reset to defaults."
|
||||
],
|
||||
"color": "#432",
|
||||
"bgcolor": "#653"
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
@@ -513,7 +540,47 @@
|
||||
"TENSOR"
|
||||
]
|
||||
],
|
||||
"groups": [],
|
||||
"groups": [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "1. Fisheye → point cloud",
|
||||
"bounding": [
|
||||
1522.239013671875,
|
||||
1122.49755859375,
|
||||
1332.4192962646484,
|
||||
589.1494140625
|
||||
],
|
||||
"color": "#3f789e",
|
||||
"font_size": 24,
|
||||
"flags": {}
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"title": "2. Enrich along trajectory",
|
||||
"bounding": [
|
||||
1964.2667236328125,
|
||||
1479.796630859375,
|
||||
1073.373291015625,
|
||||
614.0
|
||||
],
|
||||
"color": "#a1309b",
|
||||
"font_size": 24,
|
||||
"flags": {}
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"title": "3. Save & preview",
|
||||
"bounding": [
|
||||
2283.51123046875,
|
||||
1545.8380126953125,
|
||||
1130.08935546875,
|
||||
726.26318359375
|
||||
],
|
||||
"color": "#8A8",
|
||||
"font_size": 24,
|
||||
"flags": {}
|
||||
}
|
||||
],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
@@ -523,7 +590,8 @@
|
||||
-1433.3959538925521
|
||||
]
|
||||
},
|
||||
"frontendVersion": "1.19.9"
|
||||
"frontendVersion": "1.19.9",
|
||||
"camera_comfyui_rev": 1
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 65 KiB |
@@ -1,2 +0,0 @@
|
||||
# This file marks the workflows directory as a Python package.
|
||||
NODE_CLASS_MAPPINGS={}
|
||||
|
After Width: | Height: | Size: 38 KiB |
@@ -1 +1,331 @@
|
||||
{"id":"de69fe58-2f01-4527-92b1-681a151b1ce3","revision":0,"last_node_id":7,"last_link_id":8,"nodes":[{"id":3,"type":"LoadImage","pos":[-897.7078247070312,314.2335205078125],"size":[315,314],"flags":{},"order":0,"mode":0,"inputs":[],"outputs":[{"localized_name":"IMAGE","name":"IMAGE","type":"IMAGE","links":[2]},{"localized_name":"MASK","name":"MASK","type":"MASK","links":null}],"properties":{"Node name for S&R":"LoadImage"},"widgets_values":["example.png","image",""]},{"id":1,"type":"TransformToMatrix","pos":[-871.939697265625,694.5091552734375],"size":[315,154],"flags":{},"order":1,"mode":0,"inputs":[],"outputs":[{"localized_name":"MAT_4X4","name":"MAT_4X4","type":"MAT_4X4","links":[5]}],"properties":{"Node name for S&R":"TransformToMatrix"},"widgets_values":[0,0,0,60,0]},{"id":6,"type":"MaskToImage","pos":[-169.98599243164062,763.4010009765625],"size":[264.5999755859375,26],"flags":{},"order":4,"mode":0,"inputs":[{"localized_name":"mask","name":"mask","type":"MASK","link":6}],"outputs":[{"localized_name":"IMAGE","name":"IMAGE","type":"IMAGE","links":[7]}],"properties":{"Node name for S&R":"MaskToImage"}},{"id":4,"type":"PreviewImage","pos":[299.262451171875,609.4740600585938],"size":[210,246],"flags":{},"order":5,"mode":0,"inputs":[{"localized_name":"images","name":"images","type":"IMAGE","link":7}],"outputs":[],"properties":{"Node name for S&R":"PreviewImage"},"widgets_values":[""]},{"id":7,"type":"PreviewImage","pos":[-14.518107414245605,366.5712585449219],"size":[210,246],"flags":{},"order":3,"mode":0,"inputs":[{"localized_name":"images","name":"images","type":"IMAGE","link":8}],"outputs":[],"properties":{"Node name for S&R":"PreviewImage"},"widgets_values":[""]},{"id":2,"type":"ReprojectImage","pos":[-468.4571838378906,358.2191467285156],"size":[315,266],"flags":{},"order":2,"mode":0,"inputs":[{"localized_name":"image","name":"image","type":"IMAGE","link":2},{"localized_name":"mask","name":"mask","shape":7,"type":"MASK","link":null},{"localized_name":"transform_matrix","name":"transform_matrix","shape":7,"type":"MAT_4X4","link":5}],"outputs":[{"localized_name":"IMAGE","name":"IMAGE","type":"IMAGE","links":[8]},{"localized_name":"MASK","name":"MASK","type":"MASK","links":[6]}],"properties":{"Node name for S&R":"ReprojectImage"},"widgets_values":[90,180,"PINHOLE","EQUIRECTANGULAR",1024,1024,false,7]}],"links":[[2,3,0,2,0,"IMAGE"],[5,1,0,2,2,"MAT_4X4"],[6,2,1,6,0,"MASK"],[7,6,0,4,0,"IMAGE"],[8,2,0,7,0,"IMAGE"]],"groups":[],"config":{},"extra":{"ds":{"scale":0.8264462809917362,"offset":[806.6857406850668,-309.49182798364893]}},"version":0.4}
|
||||
{
|
||||
"id": "de69fe58-2f01-4527-92b1-681a151b1ce3",
|
||||
"revision": 0,
|
||||
"last_node_id": 8,
|
||||
"last_link_id": 8,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 3,
|
||||
"type": "LoadImage",
|
||||
"pos": [
|
||||
-897.7078247070312,
|
||||
314.2335205078125
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
314
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"localized_name": "IMAGE",
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
2
|
||||
]
|
||||
},
|
||||
{
|
||||
"localized_name": "MASK",
|
||||
"name": "MASK",
|
||||
"type": "MASK",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "LoadImage"
|
||||
},
|
||||
"widgets_values": [
|
||||
"camera_example_pinhole.jpg",
|
||||
"image",
|
||||
""
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"type": "TransformToMatrix",
|
||||
"pos": [
|
||||
-871.939697265625,
|
||||
694.5091552734375
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
154
|
||||
],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"localized_name": "MAT_4X4",
|
||||
"name": "MAT_4X4",
|
||||
"type": "MAT_4X4",
|
||||
"links": [
|
||||
5
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "TransformToMatrix"
|
||||
},
|
||||
"widgets_values": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
60,
|
||||
0
|
||||
],
|
||||
"title": "Rotate camera (pitch 60°)"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"type": "MaskToImage",
|
||||
"pos": [
|
||||
-169.98599243164062,
|
||||
763.4010009765625
|
||||
],
|
||||
"size": [
|
||||
264.5999755859375,
|
||||
26
|
||||
],
|
||||
"flags": {},
|
||||
"order": 4,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "mask",
|
||||
"name": "mask",
|
||||
"type": "MASK",
|
||||
"link": 6
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"localized_name": "IMAGE",
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
7
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "MaskToImage"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"type": "PreviewImage",
|
||||
"pos": [
|
||||
299.262451171875,
|
||||
609.4740600585938
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
246
|
||||
],
|
||||
"flags": {},
|
||||
"order": 5,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "images",
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 7
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"Node name for S&R": "PreviewImage"
|
||||
},
|
||||
"widgets_values": [
|
||||
""
|
||||
],
|
||||
"title": "Coverage mask (white = hole)"
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"type": "PreviewImage",
|
||||
"pos": [
|
||||
-14.518107414245605,
|
||||
366.5712585449219
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
246
|
||||
],
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "images",
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 8
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"Node name for S&R": "PreviewImage"
|
||||
},
|
||||
"widgets_values": [
|
||||
""
|
||||
],
|
||||
"title": "Reprojected image"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "ReprojectImage",
|
||||
"pos": [
|
||||
-468.4571838378906,
|
||||
358.2191467285156
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
266
|
||||
],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "image",
|
||||
"name": "image",
|
||||
"type": "IMAGE",
|
||||
"link": 2
|
||||
},
|
||||
{
|
||||
"localized_name": "mask",
|
||||
"name": "mask",
|
||||
"shape": 7,
|
||||
"type": "MASK",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"localized_name": "transform_matrix",
|
||||
"name": "transform_matrix",
|
||||
"shape": 7,
|
||||
"type": "MAT_4X4",
|
||||
"link": 5
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"localized_name": "IMAGE",
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
8
|
||||
]
|
||||
},
|
||||
{
|
||||
"localized_name": "MASK",
|
||||
"name": "MASK",
|
||||
"type": "MASK",
|
||||
"links": [
|
||||
6
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "ReprojectImage"
|
||||
},
|
||||
"widgets_values": [
|
||||
90,
|
||||
180,
|
||||
"PINHOLE",
|
||||
"EQUIRECTANGULAR",
|
||||
1024,
|
||||
1024,
|
||||
false,
|
||||
7
|
||||
],
|
||||
"title": "Pinhole 90° → Equirect 180°"
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"type": "MarkdownNote",
|
||||
"title": "About this workflow",
|
||||
"pos": [
|
||||
-1457.7078247070312,
|
||||
314.2335205078125
|
||||
],
|
||||
"size": [
|
||||
520,
|
||||
314
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"properties": {},
|
||||
"widgets_values": [
|
||||
"# Camera reprojection demo\n\nMinimal example of the two core nodes: **TransformToMatrix** rotates the virtual camera (theta = 60° pitch) and **ReprojectImage** converts a 90° pinhole image into a 180° equirectangular view.\n\nPreviews show the reprojected image and the coverage mask (white = pixels the source image cannot see).\n\n- **Set:** the image in LoadImage.\n- **Requires:** nothing beyond this pack (no models).\n- **Try:** switch `output_projection` to FISHEYE, or raise `feathering` to soften the mask edge."
|
||||
],
|
||||
"color": "#432",
|
||||
"bgcolor": "#653"
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
2,
|
||||
3,
|
||||
0,
|
||||
2,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
5,
|
||||
1,
|
||||
0,
|
||||
2,
|
||||
2,
|
||||
"MAT_4X4"
|
||||
],
|
||||
[
|
||||
6,
|
||||
2,
|
||||
1,
|
||||
6,
|
||||
0,
|
||||
"MASK"
|
||||
],
|
||||
[
|
||||
7,
|
||||
6,
|
||||
0,
|
||||
4,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
8,
|
||||
2,
|
||||
0,
|
||||
7,
|
||||
0,
|
||||
"IMAGE"
|
||||
]
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 0.8264462809917362,
|
||||
"offset": [
|
||||
806.6857406850668,
|
||||
-309.49182798364893
|
||||
]
|
||||
},
|
||||
"camera_comfyui_rev": 1
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 16 KiB |
@@ -1 +1,469 @@
|
||||
{"id":"8e48e6c6-735b-4131-9b4d-5847d2948c18","revision":0,"last_node_id":161,"last_link_id":330,"nodes":[{"id":156,"type":"DepthToImageNode","pos":[101.64265441894531,607.9051513671875],"size":[315,58],"flags":{},"order":2,"mode":0,"inputs":[{"localized_name":"depth","name":"depth","type":"TENSOR","link":323}],"outputs":[{"localized_name":"depth image","name":"depth image","type":"IMAGE","links":[322]}],"properties":{"Node name for S&R":"DepthToImageNode"},"widgets_values":[false]},{"id":157,"type":"PreviewImage","pos":[449.5372314453125,592.8480224609375],"size":[210,246],"flags":{},"order":5,"mode":0,"inputs":[{"localized_name":"images","name":"images","type":"IMAGE","link":322}],"outputs":[],"properties":{"Node name for S&R":"PreviewImage"},"widgets_values":[""]},{"id":159,"type":"PreviewImage","pos":[128.622802734375,835.9542236328125],"size":[210,246],"flags":{},"order":6,"mode":0,"inputs":[{"localized_name":"images","name":"images","type":"IMAGE","link":326}],"outputs":[],"properties":{"Node name for S&R":"PreviewImage"},"widgets_values":[""]},{"id":158,"type":"MaskToImage","pos":[105.04651641845703,715.2989501953125],"size":[264.5999755859375,26],"flags":{},"order":3,"mode":0,"inputs":[{"localized_name":"mask","name":"mask","type":"MASK","link":325}],"outputs":[{"localized_name":"IMAGE","name":"IMAGE","type":"IMAGE","links":[326]}],"properties":{"Node name for S&R":"MaskToImage"}},{"id":154,"type":"LoadImage","pos":[-601.5926513671875,610.8336181640625],"size":[315,314],"flags":{},"order":0,"mode":0,"inputs":[],"outputs":[{"localized_name":"IMAGE","name":"IMAGE","type":"IMAGE","links":[324,327]},{"localized_name":"MASK","name":"MASK","type":"MASK","links":null}],"properties":{"Node name for S&R":"LoadImage"},"widgets_values":["Saved_fisheye_00001_.png","image",""]},{"id":155,"type":"FisheyeDepthEstimator","pos":[-258.0684814453125,592.8478393554688],"size":[315,198],"flags":{},"order":1,"mode":0,"inputs":[{"localized_name":"image","name":"image","type":"IMAGE","link":324}],"outputs":[{"localized_name":"depthmap","name":"depthmap","type":"TENSOR","links":[323,329]},{"localized_name":"mask","name":"mask","type":"MASK","links":[325,328]}],"properties":{"Node name for S&R":"FisheyeDepthEstimator"},"widgets_values":["Depth-Anything-V2-Metric-Indoor-Base-hf",1,90,1024,4096,25]},{"id":160,"type":"DepthToPointCloud","pos":[398.363525390625,895.5885620117188],"size":[315,170],"flags":{},"order":4,"mode":0,"inputs":[{"localized_name":"image","name":"image","type":"IMAGE","link":327},{"localized_name":"depthmap","name":"depthmap","shape":7,"type":"TENSOR","link":329},{"localized_name":"mask","name":"mask","shape":7,"type":"MASK","link":328}],"outputs":[{"localized_name":"pointcloud","name":"pointcloud","type":"TENSOR","links":[330]}],"properties":{"Node name for S&R":"DepthToPointCloud"},"widgets_values":["FISHEYE",180,1,false]},{"id":161,"type":"SavePointCloud","pos":[785.9862670898438,881.0266723632812],"size":[315,82],"flags":{},"order":7,"mode":0,"inputs":[{"localized_name":"pointcloud","name":"pointcloud","type":"TENSOR","link":330}],"outputs":[],"properties":{"Node name for S&R":"SavePointCloud"},"widgets_values":["kitchen","npy"]}],"links":[[322,156,0,157,0,"IMAGE"],[323,155,0,156,0,"TENSOR"],[324,154,0,155,0,"IMAGE"],[325,155,1,158,0,"MASK"],[326,158,0,159,0,"IMAGE"],[327,154,0,160,0,"IMAGE"],[328,155,1,160,2,"MASK"],[329,155,0,160,1,"TENSOR"],[330,160,0,161,0,"TENSOR"]],"groups":[],"config":{},"extra":{"ds":{"scale":0.9229599817706441,"offset":[115.11465258583665,-662.5990021779805]},"frontendVersion":"1.18.9"},"version":0.4}
|
||||
{
|
||||
"id": "8e48e6c6-735b-4131-9b4d-5847d2948c18",
|
||||
"revision": 0,
|
||||
"last_node_id": 162,
|
||||
"last_link_id": 330,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 156,
|
||||
"type": "DepthToImageNode",
|
||||
"pos": [
|
||||
101.64265441894531,
|
||||
607.9051513671875
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
58
|
||||
],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "depth",
|
||||
"name": "depth",
|
||||
"type": "TENSOR",
|
||||
"link": 323
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"localized_name": "depth image",
|
||||
"name": "depth image",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
322
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "DepthToImageNode"
|
||||
},
|
||||
"widgets_values": [
|
||||
false
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 157,
|
||||
"type": "PreviewImage",
|
||||
"pos": [
|
||||
449.5372314453125,
|
||||
592.8480224609375
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
246
|
||||
],
|
||||
"flags": {},
|
||||
"order": 5,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "images",
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 322
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"Node name for S&R": "PreviewImage"
|
||||
},
|
||||
"widgets_values": [
|
||||
""
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 159,
|
||||
"type": "PreviewImage",
|
||||
"pos": [
|
||||
128.622802734375,
|
||||
835.9542236328125
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
246
|
||||
],
|
||||
"flags": {},
|
||||
"order": 6,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "images",
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 326
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"Node name for S&R": "PreviewImage"
|
||||
},
|
||||
"widgets_values": [
|
||||
""
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 158,
|
||||
"type": "MaskToImage",
|
||||
"pos": [
|
||||
105.04651641845703,
|
||||
715.2989501953125
|
||||
],
|
||||
"size": [
|
||||
264.5999755859375,
|
||||
26
|
||||
],
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "mask",
|
||||
"name": "mask",
|
||||
"type": "MASK",
|
||||
"link": 325
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"localized_name": "IMAGE",
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
326
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "MaskToImage"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 154,
|
||||
"type": "LoadImage",
|
||||
"pos": [
|
||||
-601.5926513671875,
|
||||
610.8336181640625
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
314
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"localized_name": "IMAGE",
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
324,
|
||||
327
|
||||
]
|
||||
},
|
||||
{
|
||||
"localized_name": "MASK",
|
||||
"name": "MASK",
|
||||
"type": "MASK",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "LoadImage"
|
||||
},
|
||||
"widgets_values": [
|
||||
"camera_example_fisheye.jpg",
|
||||
"image",
|
||||
""
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 155,
|
||||
"type": "FisheyeDepthEstimator",
|
||||
"pos": [
|
||||
-258.0684814453125,
|
||||
592.8478393554688
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
198
|
||||
],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "image",
|
||||
"name": "image",
|
||||
"type": "IMAGE",
|
||||
"link": 324
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"localized_name": "depthmap",
|
||||
"name": "depthmap",
|
||||
"type": "TENSOR",
|
||||
"links": [
|
||||
323,
|
||||
329
|
||||
]
|
||||
},
|
||||
{
|
||||
"localized_name": "mask",
|
||||
"name": "mask",
|
||||
"type": "MASK",
|
||||
"links": [
|
||||
325,
|
||||
328
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "FisheyeDepthEstimator"
|
||||
},
|
||||
"widgets_values": [
|
||||
"Depth-Anything-V2-Metric-Indoor-Base-hf",
|
||||
1,
|
||||
90,
|
||||
1024,
|
||||
4096,
|
||||
"SOFTMERGE",
|
||||
25,
|
||||
1
|
||||
],
|
||||
"title": "Metric depth (fisheye-aware)"
|
||||
},
|
||||
{
|
||||
"id": 160,
|
||||
"type": "DepthToPointCloud",
|
||||
"pos": [
|
||||
398.363525390625,
|
||||
895.5885620117188
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
170
|
||||
],
|
||||
"flags": {},
|
||||
"order": 4,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "image",
|
||||
"name": "image",
|
||||
"type": "IMAGE",
|
||||
"link": 327
|
||||
},
|
||||
{
|
||||
"localized_name": "depthmap",
|
||||
"name": "depthmap",
|
||||
"shape": 7,
|
||||
"type": "TENSOR",
|
||||
"link": 329
|
||||
},
|
||||
{
|
||||
"localized_name": "mask",
|
||||
"name": "mask",
|
||||
"shape": 7,
|
||||
"type": "MASK",
|
||||
"link": 328
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"localized_name": "pointcloud",
|
||||
"name": "pointcloud",
|
||||
"type": "TENSOR",
|
||||
"links": [
|
||||
330
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "DepthToPointCloud"
|
||||
},
|
||||
"widgets_values": [
|
||||
"FISHEYE",
|
||||
180,
|
||||
1,
|
||||
false
|
||||
],
|
||||
"title": "Depth → point cloud"
|
||||
},
|
||||
{
|
||||
"id": 161,
|
||||
"type": "SavePointCloud",
|
||||
"pos": [
|
||||
785.9862670898438,
|
||||
881.0266723632812
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
82
|
||||
],
|
||||
"flags": {},
|
||||
"order": 7,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "pointcloud",
|
||||
"name": "pointcloud",
|
||||
"type": "TENSOR",
|
||||
"link": 330
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"Node name for S&R": "SavePointCloud"
|
||||
},
|
||||
"widgets_values": [
|
||||
"kitchen",
|
||||
"npy"
|
||||
],
|
||||
"title": "Save .ply / .npy"
|
||||
},
|
||||
{
|
||||
"id": 162,
|
||||
"type": "MarkdownNote",
|
||||
"title": "About this workflow",
|
||||
"pos": [
|
||||
-1161.5926513671875,
|
||||
592.8478393554688
|
||||
],
|
||||
"size": [
|
||||
520,
|
||||
314
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"properties": {},
|
||||
"widgets_values": [
|
||||
"# Fisheye 180° → point cloud\n\nEstimates metric depth directly on a 180° fisheye image — **FisheyeDepthEstimator** internally splits it into pinhole views, runs Depth-Anything V2 on each and merges the depths back (SOFTMERGE) — then unprojects image + depth to a 3D point cloud and saves it.\n\nPreviews: colorized depth and the validity mask.\n\n- **Set:** fisheye input image (e.g. produced by `Outpaint_fisheye180.json`); filename in SavePointCloud.\n- **Requires:** Depth-Anything V2 (auto-downloads from HuggingFace).\n- **Next:** view the cloud with `test_pointcloud_loading.json` or synthesize a second eye with `sbs180_workflow.json`."
|
||||
],
|
||||
"color": "#432",
|
||||
"bgcolor": "#653"
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
322,
|
||||
156,
|
||||
0,
|
||||
157,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
323,
|
||||
155,
|
||||
0,
|
||||
156,
|
||||
0,
|
||||
"TENSOR"
|
||||
],
|
||||
[
|
||||
324,
|
||||
154,
|
||||
0,
|
||||
155,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
325,
|
||||
155,
|
||||
1,
|
||||
158,
|
||||
0,
|
||||
"MASK"
|
||||
],
|
||||
[
|
||||
326,
|
||||
158,
|
||||
0,
|
||||
159,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
327,
|
||||
154,
|
||||
0,
|
||||
160,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
328,
|
||||
155,
|
||||
1,
|
||||
160,
|
||||
2,
|
||||
"MASK"
|
||||
],
|
||||
[
|
||||
329,
|
||||
155,
|
||||
0,
|
||||
160,
|
||||
1,
|
||||
"TENSOR"
|
||||
],
|
||||
[
|
||||
330,
|
||||
160,
|
||||
0,
|
||||
161,
|
||||
0,
|
||||
"TENSOR"
|
||||
]
|
||||
],
|
||||
"groups": [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "1. Fisheye metric depth",
|
||||
"bounding": [
|
||||
-621.5926513671875,
|
||||
532.8478393554688,
|
||||
1301.1298828125,
|
||||
569.1063842773438
|
||||
],
|
||||
"color": "#3f789e",
|
||||
"font_size": 24,
|
||||
"flags": {}
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"title": "2. Unproject & save",
|
||||
"bounding": [
|
||||
378.363525390625,
|
||||
821.0266723632812,
|
||||
742.6227416992188,
|
||||
264.5618896484375
|
||||
],
|
||||
"color": "#a1309b",
|
||||
"font_size": 24,
|
||||
"flags": {}
|
||||
}
|
||||
],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 0.9229599817706441,
|
||||
"offset": [
|
||||
115.11465258583665,
|
||||
-662.5990021779805
|
||||
]
|
||||
},
|
||||
"frontendVersion": "1.18.9",
|
||||
"camera_comfyui_rev": 1
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 50 KiB |
@@ -0,0 +1,781 @@
|
||||
{
|
||||
"id": "016c13e5-c6a7-4b2e-adfc-18e9e969f21e",
|
||||
"revision": 0,
|
||||
"last_node_id": 21,
|
||||
"last_link_id": 32,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 10,
|
||||
"type": "SaveWEBM",
|
||||
"pos": [
|
||||
3352.103515625,
|
||||
1535.6951904296875
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
437
|
||||
],
|
||||
"flags": {},
|
||||
"order": 8,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "images",
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 24
|
||||
},
|
||||
{
|
||||
"localized_name": "filename_prefix",
|
||||
"name": "filename_prefix",
|
||||
"type": "STRING",
|
||||
"widget": {
|
||||
"name": "filename_prefix"
|
||||
},
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"localized_name": "codec",
|
||||
"name": "codec",
|
||||
"type": "COMBO",
|
||||
"widget": {
|
||||
"name": "codec"
|
||||
},
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"localized_name": "fps",
|
||||
"name": "fps",
|
||||
"type": "FLOAT",
|
||||
"widget": {
|
||||
"name": "fps"
|
||||
},
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"localized_name": "crf",
|
||||
"name": "crf",
|
||||
"type": "FLOAT",
|
||||
"widget": {
|
||||
"name": "crf"
|
||||
},
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {},
|
||||
"widgets_values": [
|
||||
"ComfyUI",
|
||||
"vp9",
|
||||
24,
|
||||
32
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"type": "TransformPointCloud",
|
||||
"pos": [
|
||||
2458.775390625,
|
||||
1405.860595703125
|
||||
],
|
||||
"size": [
|
||||
329.20001220703125,
|
||||
46
|
||||
],
|
||||
"flags": {},
|
||||
"order": 5,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "pointcloud",
|
||||
"name": "pointcloud",
|
||||
"type": "TENSOR",
|
||||
"link": 4
|
||||
},
|
||||
{
|
||||
"localized_name": "transform_matrix",
|
||||
"name": "transform_matrix",
|
||||
"type": "MAT_4X4",
|
||||
"link": 5
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"localized_name": "transformed pointcloud",
|
||||
"name": "transformed pointcloud",
|
||||
"type": "TENSOR",
|
||||
"links": [
|
||||
23
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "TransformPointCloud"
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"type": "TransformToMatrix",
|
||||
"pos": [
|
||||
2296.978759765625,
|
||||
1742.214599609375
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
154
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "shiftX",
|
||||
"name": "shiftX",
|
||||
"type": "FLOAT",
|
||||
"widget": {
|
||||
"name": "shiftX"
|
||||
},
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"localized_name": "shiftY",
|
||||
"name": "shiftY",
|
||||
"type": "FLOAT",
|
||||
"widget": {
|
||||
"name": "shiftY"
|
||||
},
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"localized_name": "shiftZ",
|
||||
"name": "shiftZ",
|
||||
"type": "FLOAT",
|
||||
"widget": {
|
||||
"name": "shiftZ"
|
||||
},
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"localized_name": "theta",
|
||||
"name": "theta",
|
||||
"type": "FLOAT",
|
||||
"widget": {
|
||||
"name": "theta"
|
||||
},
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"localized_name": "phi",
|
||||
"name": "phi",
|
||||
"type": "FLOAT",
|
||||
"widget": {
|
||||
"name": "phi"
|
||||
},
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"localized_name": "transformation matrix",
|
||||
"name": "transformation matrix",
|
||||
"type": "MAT_4X4",
|
||||
"links": [
|
||||
5,
|
||||
25
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "TransformToMatrix"
|
||||
},
|
||||
"widgets_values": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"title": "Camera move (edit me)"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "DepthToPointCloud",
|
||||
"pos": [
|
||||
2099.81640625,
|
||||
1385.9761962890625
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
170
|
||||
],
|
||||
"flags": {},
|
||||
"order": 4,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "image",
|
||||
"name": "image",
|
||||
"type": "IMAGE",
|
||||
"link": 1
|
||||
},
|
||||
{
|
||||
"localized_name": "depthmap",
|
||||
"name": "depthmap",
|
||||
"shape": 7,
|
||||
"type": "TENSOR",
|
||||
"link": 32
|
||||
},
|
||||
{
|
||||
"localized_name": "mask",
|
||||
"name": "mask",
|
||||
"shape": 7,
|
||||
"type": "MASK",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"localized_name": "input_projection",
|
||||
"name": "input_projection",
|
||||
"type": "COMBO",
|
||||
"widget": {
|
||||
"name": "input_projection"
|
||||
},
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"localized_name": "input_horizontal_fov",
|
||||
"name": "input_horizontal_fov",
|
||||
"type": "FLOAT",
|
||||
"widget": {
|
||||
"name": "input_horizontal_fov"
|
||||
},
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"localized_name": "depth_scale",
|
||||
"name": "depth_scale",
|
||||
"type": "FLOAT",
|
||||
"widget": {
|
||||
"name": "depth_scale"
|
||||
},
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"localized_name": "invert_depth",
|
||||
"name": "invert_depth",
|
||||
"type": "BOOLEAN",
|
||||
"widget": {
|
||||
"name": "invert_depth"
|
||||
},
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"localized_name": "pointcloud",
|
||||
"name": "pointcloud",
|
||||
"type": "TENSOR",
|
||||
"links": [
|
||||
4,
|
||||
26
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "DepthToPointCloud"
|
||||
},
|
||||
"widgets_values": [
|
||||
"PINHOLE",
|
||||
60,
|
||||
1,
|
||||
false
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"type": "CameraMotionNode",
|
||||
"pos": [
|
||||
2984.30224609375,
|
||||
1486.6483154296875
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
198
|
||||
],
|
||||
"flags": {},
|
||||
"order": 7,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "pointcloud",
|
||||
"name": "pointcloud",
|
||||
"type": "TENSOR",
|
||||
"link": 23
|
||||
},
|
||||
{
|
||||
"localized_name": "trajectory",
|
||||
"name": "trajectory",
|
||||
"type": "TENSOR",
|
||||
"link": 27
|
||||
},
|
||||
{
|
||||
"localized_name": "n_points",
|
||||
"name": "n_points",
|
||||
"type": "INT",
|
||||
"widget": {
|
||||
"name": "n_points"
|
||||
},
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"localized_name": "output_projection",
|
||||
"name": "output_projection",
|
||||
"type": "COMBO",
|
||||
"widget": {
|
||||
"name": "output_projection"
|
||||
},
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"localized_name": "output_horizontal_fov",
|
||||
"name": "output_horizontal_fov",
|
||||
"type": "FLOAT",
|
||||
"widget": {
|
||||
"name": "output_horizontal_fov"
|
||||
},
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"localized_name": "output_width",
|
||||
"name": "output_width",
|
||||
"type": "INT",
|
||||
"widget": {
|
||||
"name": "output_width"
|
||||
},
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"localized_name": "output_height",
|
||||
"name": "output_height",
|
||||
"type": "INT",
|
||||
"widget": {
|
||||
"name": "output_height"
|
||||
},
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"localized_name": "point_size",
|
||||
"name": "point_size",
|
||||
"type": "INT",
|
||||
"widget": {
|
||||
"name": "point_size"
|
||||
},
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"localized_name": "motion_frames",
|
||||
"name": "motion_frames",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
24
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CameraMotionNode"
|
||||
},
|
||||
"widgets_values": [
|
||||
10,
|
||||
"PINHOLE",
|
||||
60,
|
||||
1024,
|
||||
1024,
|
||||
2,
|
||||
0,
|
||||
false,
|
||||
false
|
||||
],
|
||||
"title": "Render fly-through"
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"type": "CameraTrajectoryNode",
|
||||
"pos": [
|
||||
2722.265380859375,
|
||||
1772.097412109375
|
||||
],
|
||||
"size": [
|
||||
317.4000244140625,
|
||||
46
|
||||
],
|
||||
"flags": {},
|
||||
"order": 6,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "pointcloud",
|
||||
"name": "pointcloud",
|
||||
"type": "TENSOR",
|
||||
"link": 26
|
||||
},
|
||||
{
|
||||
"localized_name": "initial_matrix",
|
||||
"name": "initial_matrix",
|
||||
"type": "MAT_4X4",
|
||||
"link": 25
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"localized_name": "trajectory",
|
||||
"name": "trajectory",
|
||||
"type": "TENSOR",
|
||||
"links": [
|
||||
27
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CameraTrajectoryNode"
|
||||
},
|
||||
"widgets_values": [],
|
||||
"title": "Build trajectory"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"type": "DepthEstimatorNode",
|
||||
"pos": [
|
||||
1570.1402587890625,
|
||||
1859.879638671875
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
82
|
||||
],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "image",
|
||||
"name": "image",
|
||||
"type": "IMAGE",
|
||||
"link": 3
|
||||
},
|
||||
{
|
||||
"localized_name": "model_name",
|
||||
"name": "model_name",
|
||||
"type": "STRING",
|
||||
"widget": {
|
||||
"name": "model_name"
|
||||
},
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"localized_name": "depth_scale",
|
||||
"name": "depth_scale",
|
||||
"type": "FLOAT",
|
||||
"widget": {
|
||||
"name": "depth_scale"
|
||||
},
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"localized_name": "depth tensor",
|
||||
"name": "depth tensor",
|
||||
"type": "TENSOR",
|
||||
"links": [
|
||||
31
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "DepthEstimatorNode"
|
||||
},
|
||||
"widgets_values": [
|
||||
"Depth-Anything-V2-Metric-Indoor-Base-hf",
|
||||
1.0000000000000002,
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"type": "ZDepthToRayDepthNode",
|
||||
"pos": [
|
||||
1969.0958251953125,
|
||||
1782.2877197265625
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
58
|
||||
],
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "depth",
|
||||
"name": "depth",
|
||||
"type": "TENSOR",
|
||||
"link": 31
|
||||
},
|
||||
{
|
||||
"localized_name": "fov",
|
||||
"name": "fov",
|
||||
"type": "FLOAT",
|
||||
"widget": {
|
||||
"name": "fov"
|
||||
},
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"localized_name": "ray depth",
|
||||
"name": "ray depth",
|
||||
"type": "TENSOR",
|
||||
"links": [
|
||||
32
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "ZDepthToRayDepthNode"
|
||||
},
|
||||
"widgets_values": [
|
||||
90
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"type": "LoadImage",
|
||||
"pos": [
|
||||
1575.78955078125,
|
||||
1419.3006591796875
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
314
|
||||
],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "image",
|
||||
"name": "image",
|
||||
"type": "COMBO",
|
||||
"widget": {
|
||||
"name": "image"
|
||||
},
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"localized_name": "choose file to upload",
|
||||
"name": "upload",
|
||||
"type": "IMAGEUPLOAD",
|
||||
"widget": {
|
||||
"name": "upload"
|
||||
},
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"localized_name": "IMAGE",
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
1,
|
||||
3
|
||||
]
|
||||
},
|
||||
{
|
||||
"localized_name": "MASK",
|
||||
"name": "MASK",
|
||||
"type": "MASK",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "LoadImage"
|
||||
},
|
||||
"widgets_values": [
|
||||
"camera_example_pinhole.jpg",
|
||||
"image"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 21,
|
||||
"type": "MarkdownNote",
|
||||
"title": "About this workflow",
|
||||
"pos": [
|
||||
1010.1402587890625,
|
||||
1385.9761962890625
|
||||
],
|
||||
"size": [
|
||||
520,
|
||||
262
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"properties": {},
|
||||
"widgets_values": [
|
||||
"# Point cloud walker (fly-through video)\n\nSingle image → metric depth → point cloud, then **CameraTrajectoryNode** derives a camera path and **CameraMotionNode** renders a fly-through saved as WEBM.\n\n- **Set:** input image; the move in TransformToMatrix (shift XYZ + theta/phi); frames-per-segment (`n_points`) in CameraMotionNode.\n- **Requires:** Depth-Anything V2 (auto-download).\n- **Note:** the pinhole FOV here is 60° — keep DepthToPointCloud and CameraMotionNode FOVs consistent with your input."
|
||||
],
|
||||
"color": "#432",
|
||||
"bgcolor": "#653"
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
2,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
3,
|
||||
1,
|
||||
0,
|
||||
3,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
4,
|
||||
2,
|
||||
0,
|
||||
4,
|
||||
0,
|
||||
"TENSOR"
|
||||
],
|
||||
[
|
||||
5,
|
||||
5,
|
||||
0,
|
||||
4,
|
||||
1,
|
||||
"MAT_4X4"
|
||||
],
|
||||
[
|
||||
23,
|
||||
4,
|
||||
0,
|
||||
14,
|
||||
0,
|
||||
"TENSOR"
|
||||
],
|
||||
[
|
||||
24,
|
||||
14,
|
||||
0,
|
||||
10,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
25,
|
||||
5,
|
||||
0,
|
||||
17,
|
||||
1,
|
||||
"MAT_4X4"
|
||||
],
|
||||
[
|
||||
26,
|
||||
2,
|
||||
0,
|
||||
17,
|
||||
0,
|
||||
"TENSOR"
|
||||
],
|
||||
[
|
||||
27,
|
||||
17,
|
||||
0,
|
||||
14,
|
||||
1,
|
||||
"TENSOR"
|
||||
],
|
||||
[
|
||||
31,
|
||||
3,
|
||||
0,
|
||||
20,
|
||||
0,
|
||||
"TENSOR"
|
||||
],
|
||||
[
|
||||
32,
|
||||
20,
|
||||
0,
|
||||
2,
|
||||
1,
|
||||
"TENSOR"
|
||||
]
|
||||
],
|
||||
"groups": [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "1. Image → point cloud",
|
||||
"bounding": [
|
||||
1550.1402587890625,
|
||||
1325.9761962890625,
|
||||
884.6761474609375,
|
||||
635.9034423828125
|
||||
],
|
||||
"color": "#3f789e",
|
||||
"font_size": 24,
|
||||
"flags": {}
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"title": "2. Trajectory",
|
||||
"bounding": [
|
||||
2276.978759765625,
|
||||
1345.860595703125,
|
||||
782.6866455078125,
|
||||
570.35400390625
|
||||
],
|
||||
"color": "#a1309b",
|
||||
"font_size": 24,
|
||||
"flags": {}
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"title": "3. Render & save",
|
||||
"bounding": [
|
||||
2964.30224609375,
|
||||
1426.6483154296875,
|
||||
722.80126953125,
|
||||
566.046875
|
||||
],
|
||||
"color": "#8A8",
|
||||
"font_size": 24,
|
||||
"flags": {}
|
||||
}
|
||||
],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 1.083470594338842,
|
||||
"offset": [
|
||||
-1328.4875281402603,
|
||||
-1380.0248413907814
|
||||
]
|
||||
},
|
||||
"frontendVersion": "1.18.9",
|
||||
"camera_comfyui_rev": 1
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000000",
|
||||
"revision": 0,
|
||||
"last_node_id": 5,
|
||||
"last_link_id": 3,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "TransformToMatrix",
|
||||
"title": "Start pose (identity)",
|
||||
"pos": [
|
||||
-500,
|
||||
320
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
154
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "transformation matrix",
|
||||
"type": "MAT_4X4",
|
||||
"links": [
|
||||
1
|
||||
],
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "TransformToMatrix"
|
||||
},
|
||||
"widgets_values": [
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "TransformToMatrix",
|
||||
"title": "End pose (edit me)",
|
||||
"pos": [
|
||||
-500,
|
||||
540
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
154
|
||||
],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "transformation matrix",
|
||||
"type": "MAT_4X4",
|
||||
"links": [
|
||||
2
|
||||
],
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "TransformToMatrix"
|
||||
},
|
||||
"widgets_values": [
|
||||
0.0,
|
||||
0.0,
|
||||
0.3,
|
||||
0.0,
|
||||
30.0
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"type": "CameraInterpolationNode",
|
||||
"title": "Interpolate 20 poses",
|
||||
"pos": [
|
||||
-120,
|
||||
430
|
||||
],
|
||||
"size": [
|
||||
226,
|
||||
78
|
||||
],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "initial_matrix",
|
||||
"type": "MAT_4X4",
|
||||
"link": 1
|
||||
},
|
||||
{
|
||||
"name": "final_matrix",
|
||||
"type": "MAT_4X4",
|
||||
"link": 2
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "trajectory",
|
||||
"type": "TENSOR",
|
||||
"links": [
|
||||
3
|
||||
],
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CameraInterpolationNode"
|
||||
},
|
||||
"widgets_values": [
|
||||
20
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"type": "SaveTrajectory",
|
||||
"title": "Save to output/*.npy",
|
||||
"pos": [
|
||||
170,
|
||||
430
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
82
|
||||
],
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "trajectory",
|
||||
"type": "TENSOR",
|
||||
"link": 3
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"Node name for S&R": "SaveTrajectory"
|
||||
},
|
||||
"widgets_values": [
|
||||
"ComfyUITrajectory"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"type": "MarkdownNote",
|
||||
"title": "About this workflow",
|
||||
"pos": [
|
||||
-1080,
|
||||
320
|
||||
],
|
||||
"size": [
|
||||
520,
|
||||
430
|
||||
],
|
||||
"flags": {},
|
||||
"order": 4,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"properties": {},
|
||||
"widgets_values": [
|
||||
"# Record a camera trajectory\n\nProduces the `.npy` trajectory file consumed by `PC_enricher.json` and `wan_vace_ref_to_video.json` (LoadTrajectory): two poses are SE(3)-interpolated into a smooth 20-step path and saved by **SaveTrajectory** to your ComfyUI **output** directory.\n\n- **Set:** the end pose (shift XYZ in scene units — metric if the cloud came from metric depth — plus theta = pitch, phi = yaw) and `num_steps`.\n- **Then:** move the saved file from `output/` to `input/` so LoadTrajectory can list it. A bundled example (`ComfyUITrajectory_00001.npy`) is already installed by install.py.\n- Chain more CameraInterpolationNode segments (or use CameraTrajectoryNode on a point cloud) for multi-keyframe paths."
|
||||
],
|
||||
"color": "#432",
|
||||
"bgcolor": "#653"
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
3,
|
||||
0,
|
||||
"MAT_4X4"
|
||||
],
|
||||
[
|
||||
2,
|
||||
2,
|
||||
0,
|
||||
3,
|
||||
1,
|
||||
"MAT_4X4"
|
||||
],
|
||||
[
|
||||
3,
|
||||
3,
|
||||
0,
|
||||
4,
|
||||
0,
|
||||
"TENSOR"
|
||||
]
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"camera_comfyui_rev": 1
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
|
After Width: | Height: | Size: 46 KiB |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "4e89c8a4-594b-4d15-a089-da088966adee",
|
||||
"revision": 0,
|
||||
"last_node_id": 44,
|
||||
"last_node_id": 45,
|
||||
"last_link_id": 87,
|
||||
"nodes": [
|
||||
{
|
||||
@@ -687,7 +687,8 @@
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
],
|
||||
"title": "Eye baseline (shiftX 0.1)"
|
||||
},
|
||||
{
|
||||
"id": 31,
|
||||
@@ -871,7 +872,8 @@
|
||||
"properties": {},
|
||||
"widgets_values": [
|
||||
"shifted_camera_equirect"
|
||||
]
|
||||
],
|
||||
"title": "Right eye (equirect)"
|
||||
},
|
||||
{
|
||||
"id": 27,
|
||||
@@ -908,7 +910,7 @@
|
||||
"Node name for S&R": "LoadImage"
|
||||
},
|
||||
"widgets_values": [
|
||||
"Fisheye_outpainted_flux_dev.png",
|
||||
"camera_example_fisheye.jpg",
|
||||
"image"
|
||||
]
|
||||
},
|
||||
@@ -998,7 +1000,32 @@
|
||||
"properties": {},
|
||||
"widgets_values": [
|
||||
"init_camera_equirect"
|
||||
]
|
||||
],
|
||||
"title": "Left eye (equirect)"
|
||||
},
|
||||
{
|
||||
"id": 45,
|
||||
"type": "MarkdownNote",
|
||||
"title": "About this workflow",
|
||||
"pos": [
|
||||
929.3206787109375,
|
||||
1152.5965576171875
|
||||
],
|
||||
"size": [
|
||||
520,
|
||||
262
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"properties": {},
|
||||
"widgets_values": [
|
||||
"# SBS VR180: synthesize the second eye\n\nFrom one 180° fisheye view, synthesizes a stereo pair: fisheye metric depth → point cloud → clean → shift the camera by the eye baseline (TransformToMatrix shiftX = 0.1) → re-project to fisheye → four chained OutpaintAnyProjection passes fill the disocclusions → both eyes are exported as 180° equirectangular images.\n\n- **Set:** fisheye input (e.g. from `Outpaint_fisheye180.json`); the baseline (0.1 ≈ 6.5 cm when depth is metric); prompts optional.\n- **Requires:** inpainting_flux (auto-installed), Depth-Anything V2.\n- **Output:** `init_camera_equirect` + `shifted_camera_equirect` — combine side-by-side for a VR180 player."
|
||||
],
|
||||
"color": "#432",
|
||||
"bgcolor": "#653"
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
@@ -1230,7 +1257,7 @@
|
||||
"groups": [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Metric depth estimation",
|
||||
"title": "1. Fisheye metric depth",
|
||||
"bounding": [
|
||||
1474.8399658203125,
|
||||
1625.7159423828125,
|
||||
@@ -1243,7 +1270,7 @@
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"title": "Pointcloud manipulation",
|
||||
"title": "2. Point cloud & eye-baseline shift",
|
||||
"bounding": [
|
||||
2231.516845703125,
|
||||
1746.5635986328125,
|
||||
@@ -1256,7 +1283,7 @@
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"title": "Outpaint and project to equirect",
|
||||
"title": "3. Outpaint disocclusions & export equirect",
|
||||
"bounding": [
|
||||
3721.45654296875,
|
||||
1723.2581787109375,
|
||||
@@ -1269,7 +1296,7 @@
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"title": "Visualisations",
|
||||
"title": "Previews",
|
||||
"bounding": [
|
||||
2852.595703125,
|
||||
1071.784912109375,
|
||||
@@ -1290,7 +1317,8 @@
|
||||
-1811.182955009025
|
||||
]
|
||||
},
|
||||
"frontendVersion": "1.20.4"
|
||||
"frontendVersion": "1.20.4",
|
||||
"camera_comfyui_rev": 1
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "dd56c0bf-7405-406e-924f-42b2feacb73f",
|
||||
"revision": 0,
|
||||
"last_node_id": 8,
|
||||
"last_node_id": 9,
|
||||
"last_link_id": 9,
|
||||
"nodes": [
|
||||
{
|
||||
@@ -87,7 +87,8 @@
|
||||
0,
|
||||
false,
|
||||
false
|
||||
]
|
||||
],
|
||||
"title": "Render motion"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
@@ -118,7 +119,8 @@
|
||||
},
|
||||
"widgets_values": [
|
||||
"ComfyUIPointCloud_00001.ply"
|
||||
]
|
||||
],
|
||||
"title": "Load saved cloud (.ply/.npy)"
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
@@ -153,7 +155,8 @@
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
],
|
||||
"title": "Start pose (identity)"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
@@ -188,7 +191,8 @@
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
],
|
||||
"title": "End pose (dolly +0.1)"
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
@@ -228,7 +232,33 @@
|
||||
"properties": {
|
||||
"Node name for S&R": "CameraInterpolationNode"
|
||||
},
|
||||
"widgets_values": []
|
||||
"widgets_values": [
|
||||
2
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"type": "MarkdownNote",
|
||||
"title": "About this workflow",
|
||||
"pos": [
|
||||
-1097.2319946289062,
|
||||
1294.427734375
|
||||
],
|
||||
"size": [
|
||||
520,
|
||||
236
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"properties": {},
|
||||
"widgets_values": [
|
||||
"# Load a saved point cloud & orbit\n\nReloads a point cloud saved by SavePointCloud and renders a short camera move between two poses (identity → 0.1 forward) as a WEBM.\n\n- **Set:** the file in LoadPointCloud (dropdown lists the ComfyUI input dir — run `PointCloud.json` or `fisheye_to_pointcloud.json` first); the two TransformToMatrix poses.\n- **Requires:** nothing beyond this pack."
|
||||
],
|
||||
"color": "#432",
|
||||
"bgcolor": "#653"
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
@@ -283,7 +313,8 @@
|
||||
-1186.3099424359816
|
||||
]
|
||||
},
|
||||
"frontendVersion": "1.21.7"
|
||||
"frontendVersion": "1.21.7",
|
||||
"camera_comfyui_rev": 1
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 28 KiB |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "6a6957a6-60ed-4027-9a58-b0b7a511139f",
|
||||
"revision": 0,
|
||||
"last_node_id": 75,
|
||||
"last_node_id": 76,
|
||||
"last_link_id": 151,
|
||||
"nodes": [
|
||||
{
|
||||
@@ -44,7 +44,9 @@
|
||||
"ver": "bc358f82634eb51b60207e1a8ddd5b29aaaaa2ae",
|
||||
"Node name for S&R": "CameraInterpolationNode"
|
||||
},
|
||||
"widgets_values": []
|
||||
"widgets_values": [
|
||||
2
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 30,
|
||||
@@ -205,142 +207,6 @@
|
||||
"default"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 26,
|
||||
"type": "easy mathInt",
|
||||
"pos": [
|
||||
-358.4687805175781,
|
||||
47.47260284423828
|
||||
],
|
||||
"size": [
|
||||
270,
|
||||
106
|
||||
],
|
||||
"flags": {},
|
||||
"order": 13,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "a",
|
||||
"type": "INT",
|
||||
"widget": {
|
||||
"name": "a"
|
||||
},
|
||||
"link": 28
|
||||
},
|
||||
{
|
||||
"name": "b",
|
||||
"type": "INT",
|
||||
"widget": {
|
||||
"name": "b"
|
||||
},
|
||||
"link": 29
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "INT",
|
||||
"type": "INT",
|
||||
"links": [
|
||||
30
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfyui-easy-use",
|
||||
"ver": "1.3.0",
|
||||
"Node name for S&R": "easy mathInt"
|
||||
},
|
||||
"widgets_values": [
|
||||
0,
|
||||
0,
|
||||
"subtract"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 23,
|
||||
"type": "VHS_VideoInfo",
|
||||
"pos": [
|
||||
-582.7188720703125,
|
||||
224.42620849609375
|
||||
],
|
||||
"size": [
|
||||
225.59765625,
|
||||
206
|
||||
],
|
||||
"flags": {},
|
||||
"order": 11,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "video_info",
|
||||
"type": "VHS_VIDEOINFO",
|
||||
"link": 25
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "source_fps🟨",
|
||||
"type": "FLOAT",
|
||||
"links": []
|
||||
},
|
||||
{
|
||||
"name": "source_frame_count🟨",
|
||||
"type": "INT",
|
||||
"links": []
|
||||
},
|
||||
{
|
||||
"name": "source_duration🟨",
|
||||
"type": "FLOAT",
|
||||
"links": null
|
||||
},
|
||||
{
|
||||
"name": "source_width🟨",
|
||||
"type": "INT",
|
||||
"links": [
|
||||
28
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "source_height🟨",
|
||||
"type": "INT",
|
||||
"links": [
|
||||
29
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "loaded_fps🟦",
|
||||
"type": "FLOAT",
|
||||
"links": null
|
||||
},
|
||||
{
|
||||
"name": "loaded_frame_count🟦",
|
||||
"type": "INT",
|
||||
"links": null
|
||||
},
|
||||
{
|
||||
"name": "loaded_duration🟦",
|
||||
"type": "FLOAT",
|
||||
"links": null
|
||||
},
|
||||
{
|
||||
"name": "loaded_width🟦",
|
||||
"type": "INT",
|
||||
"links": null
|
||||
},
|
||||
{
|
||||
"name": "loaded_height🟦",
|
||||
"type": "INT",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfyui-videohelpersuite",
|
||||
"ver": "a7ce59e381934733bfae03b1be029756d6ce936d",
|
||||
"Node name for S&R": "VHS_VideoInfo"
|
||||
},
|
||||
"widgets_values": {}
|
||||
},
|
||||
{
|
||||
"id": 28,
|
||||
"type": "WanVaceToVideo",
|
||||
@@ -432,7 +298,8 @@
|
||||
81,
|
||||
1,
|
||||
1
|
||||
]
|
||||
],
|
||||
"title": "WAN VACE control"
|
||||
},
|
||||
{
|
||||
"id": 35,
|
||||
@@ -566,7 +433,7 @@
|
||||
},
|
||||
{
|
||||
"id": 63,
|
||||
"type": "GetImageRangeFromBatch",
|
||||
"type": "ImageFromBatch",
|
||||
"pos": [
|
||||
583.0528564453125,
|
||||
-1016.3375854492188
|
||||
@@ -580,7 +447,7 @@
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"name": "image",
|
||||
"shape": 7,
|
||||
"type": "IMAGE",
|
||||
"link": 116
|
||||
@@ -609,12 +476,13 @@
|
||||
"properties": {
|
||||
"cnr_id": "comfyui-kjnodes",
|
||||
"ver": "d57154c3a808b8a3f232ed293eaa2d000867c884",
|
||||
"Node name for S&R": "GetImageRangeFromBatch"
|
||||
"Node name for S&R": "ImageFromBatch"
|
||||
},
|
||||
"widgets_values": [
|
||||
0,
|
||||
1
|
||||
]
|
||||
],
|
||||
"title": "First frame (for captioning)"
|
||||
},
|
||||
{
|
||||
"id": 62,
|
||||
@@ -755,51 +623,6 @@
|
||||
32
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 27,
|
||||
"type": "easy mathInt",
|
||||
"pos": [
|
||||
-29.064062118530273,
|
||||
56.160377502441406
|
||||
],
|
||||
"size": [
|
||||
270,
|
||||
106
|
||||
],
|
||||
"flags": {},
|
||||
"order": 15,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "a",
|
||||
"type": "INT",
|
||||
"widget": {
|
||||
"name": "a"
|
||||
},
|
||||
"link": 30
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "INT",
|
||||
"type": "INT",
|
||||
"links": [
|
||||
31,
|
||||
32
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfyui-easy-use",
|
||||
"ver": "1.3.0",
|
||||
"Node name for S&R": "easy mathInt"
|
||||
},
|
||||
"widgets_values": [
|
||||
0,
|
||||
2,
|
||||
"divide"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"type": "ImageScale",
|
||||
@@ -862,22 +685,6 @@
|
||||
"name": "image",
|
||||
"type": "IMAGE",
|
||||
"link": 22
|
||||
},
|
||||
{
|
||||
"name": "top",
|
||||
"type": "INT",
|
||||
"widget": {
|
||||
"name": "top"
|
||||
},
|
||||
"link": 31
|
||||
},
|
||||
{
|
||||
"name": "bottom",
|
||||
"type": "INT",
|
||||
"widget": {
|
||||
"name": "bottom"
|
||||
},
|
||||
"link": 32
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
@@ -907,7 +714,8 @@
|
||||
0,
|
||||
280,
|
||||
1
|
||||
]
|
||||
],
|
||||
"title": "Pad to square — set top/bottom to (W−H)/2"
|
||||
},
|
||||
{
|
||||
"id": 68,
|
||||
@@ -1018,7 +826,8 @@
|
||||
1.0000000000000002,
|
||||
0,
|
||||
-16.666717529296875
|
||||
]
|
||||
],
|
||||
"title": "Final camera pose (edit me)"
|
||||
},
|
||||
{
|
||||
"id": 61,
|
||||
@@ -1138,9 +947,7 @@
|
||||
{
|
||||
"name": "video_info",
|
||||
"type": "VHS_VIDEOINFO",
|
||||
"links": [
|
||||
25
|
||||
]
|
||||
"links": []
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
@@ -1221,45 +1028,6 @@
|
||||
60
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 60,
|
||||
"type": "BlurMaskFast",
|
||||
"pos": [
|
||||
1376.188720703125,
|
||||
442.5711669921875
|
||||
],
|
||||
"size": [
|
||||
270,
|
||||
82
|
||||
],
|
||||
"flags": {},
|
||||
"order": 25,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "masks",
|
||||
"type": "MASK",
|
||||
"link": 111
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "MASK",
|
||||
"type": "MASK",
|
||||
"links": []
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "ComfyUI-Image-Filters",
|
||||
"ver": "5abb3c2395739e0b94c2061bbc49bd1349de97c4",
|
||||
"Node name for S&R": "BlurMaskFast",
|
||||
"aux_id": "spacepxl/ComfyUI-Image-Filters"
|
||||
},
|
||||
"widgets_values": [
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"type": "SaveWEBM",
|
||||
@@ -1371,7 +1139,8 @@
|
||||
3,
|
||||
1,
|
||||
1
|
||||
]
|
||||
],
|
||||
"title": "Re-render along trajectory"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
@@ -1433,7 +1202,6 @@
|
||||
"name": "MASK",
|
||||
"type": "MASK",
|
||||
"links": [
|
||||
111,
|
||||
144,
|
||||
145,
|
||||
149
|
||||
@@ -1715,6 +1483,30 @@
|
||||
],
|
||||
"color": "#232",
|
||||
"bgcolor": "#353"
|
||||
},
|
||||
{
|
||||
"id": 76,
|
||||
"type": "MarkdownNote",
|
||||
"title": "About this workflow",
|
||||
"pos": [
|
||||
-1439.115966796875,
|
||||
-1016.3375854492188
|
||||
],
|
||||
"size": [
|
||||
520,
|
||||
392
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"properties": {},
|
||||
"widgets_values": [
|
||||
"# Re-shoot a video with a new camera move\n\nRe-renders an input video along a user-defined camera trajectory and uses WAN 2.1 VACE to regenerate what the new camera reveals:\n\n1. Video is padded square (ImagePadForOutpaint — set top/bottom to (width−height)/2 for your video) and depth-estimated per frame (Video-Depth-Anything metric).\n2. **VideoCameraMotionSequence** lifts each frame to a point cloud and re-renders it along the SE(3)-interpolated trajectory.\n3. Disocclusion masks + re-rendered frames become VACE control video/masks; **Florence2** auto-captions the clip as the prompt; WAN 2.1 VACE 14B fills the gaps.\n\n- **Set:** video path (VHS Load Video Path); the camera move (two TransformToMatrix poses); pad amounts; override the auto-caption in CLIPTextEncode if desired.\n- **Requires (packs):** VideoHelperSuite, ComfyUI-Florence2.\n- **Requires (models):** `metric_video_depth_anything_vitl.pth` (install.sh `depth`), WAN 2.1 VACE 14B + umt5-xxl + WAN VAE (install.sh `vae`), Florence-2 (auto-download).\n- **Outputs:** re-rendered composite WEBM, depth WEBM, final VACE clip."
|
||||
],
|
||||
"color": "#432",
|
||||
"bgcolor": "#653"
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
@@ -1798,54 +1590,6 @@
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
25,
|
||||
9,
|
||||
3,
|
||||
23,
|
||||
0,
|
||||
"VHS_VIDEOINFO"
|
||||
],
|
||||
[
|
||||
28,
|
||||
23,
|
||||
3,
|
||||
26,
|
||||
0,
|
||||
"INT"
|
||||
],
|
||||
[
|
||||
29,
|
||||
23,
|
||||
4,
|
||||
26,
|
||||
1,
|
||||
"INT"
|
||||
],
|
||||
[
|
||||
30,
|
||||
26,
|
||||
0,
|
||||
27,
|
||||
0,
|
||||
"INT"
|
||||
],
|
||||
[
|
||||
31,
|
||||
27,
|
||||
0,
|
||||
21,
|
||||
1,
|
||||
"INT"
|
||||
],
|
||||
[
|
||||
32,
|
||||
27,
|
||||
0,
|
||||
21,
|
||||
2,
|
||||
"INT"
|
||||
],
|
||||
[
|
||||
35,
|
||||
33,
|
||||
@@ -1974,14 +1718,6 @@
|
||||
1,
|
||||
"VAE"
|
||||
],
|
||||
[
|
||||
111,
|
||||
41,
|
||||
0,
|
||||
60,
|
||||
0,
|
||||
"MASK"
|
||||
],
|
||||
[
|
||||
113,
|
||||
63,
|
||||
@@ -2111,7 +1847,99 @@
|
||||
"IMAGE"
|
||||
]
|
||||
],
|
||||
"groups": [],
|
||||
"groups": [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "1. Load & pad video",
|
||||
"bounding": [
|
||||
-899.115966796875,
|
||||
135.00233459472656,
|
||||
1236.9286651611328,
|
||||
882.2797393798828
|
||||
],
|
||||
"color": "#3f789e",
|
||||
"font_size": 24,
|
||||
"flags": {}
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"title": "2. Metric video depth",
|
||||
"bounding": [
|
||||
-376.9869384765625,
|
||||
436.6513671875,
|
||||
645.8890285491943,
|
||||
255.26275634765625
|
||||
],
|
||||
"color": "#a1309b",
|
||||
"font_size": 24,
|
||||
"flags": {}
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"title": "3. Re-render with new camera",
|
||||
"bounding": [
|
||||
-310.3587646484375,
|
||||
426.8964538574219,
|
||||
978.3455200195312,
|
||||
817.5969543457031
|
||||
],
|
||||
"color": "#8A8",
|
||||
"font_size": 24,
|
||||
"flags": {}
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"title": "4. Masks & composite",
|
||||
"bounding": [
|
||||
701.5014038085938,
|
||||
437.3684387207031,
|
||||
313.2791748046875,
|
||||
562.5854187011719
|
||||
],
|
||||
"color": "#b58b2a",
|
||||
"font_size": 24,
|
||||
"flags": {}
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"title": "5. Auto-caption (Florence2)",
|
||||
"bounding": [
|
||||
402.0513000488281,
|
||||
-1076.3375854492188,
|
||||
915.5072937011719,
|
||||
674.8928833007812
|
||||
],
|
||||
"color": "#88A",
|
||||
"font_size": 24,
|
||||
"flags": {}
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"title": "6. WAN VACE re-generation",
|
||||
"bounding": [
|
||||
-252.87193298339844,
|
||||
-580.986328125,
|
||||
2209.4490814208984,
|
||||
931.6179580688477
|
||||
],
|
||||
"color": "#b06634",
|
||||
"font_size": 24,
|
||||
"flags": {}
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"title": "7. Outputs",
|
||||
"bounding": [
|
||||
782.4984130859375,
|
||||
-267.39794921875,
|
||||
1570.5733642578125,
|
||||
1506.2070922851562
|
||||
],
|
||||
"color": "#535",
|
||||
"font_size": 24,
|
||||
"flags": {}
|
||||
}
|
||||
],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
@@ -2125,7 +1953,8 @@
|
||||
"VHS_latentpreview": false,
|
||||
"VHS_latentpreviewrate": 0,
|
||||
"VHS_MetadataImage": true,
|
||||
"VHS_KeepIntermediate": true
|
||||
"VHS_KeepIntermediate": true,
|
||||
"camera_comfyui_rev": 1
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1605,7 +1605,8 @@
|
||||
450
|
||||
]
|
||||
},
|
||||
"frontendVersion": "1.23.4"
|
||||
"frontendVersion": "1.23.4",
|
||||
"camera_comfyui_rev": 1
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
|
||||
@@ -1509,7 +1509,8 @@
|
||||
450
|
||||
]
|
||||
},
|
||||
"frontendVersion": "1.23.4"
|
||||
"frontendVersion": "1.23.4",
|
||||
"camera_comfyui_rev": 1
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 41 KiB |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "0898f6a6-2814-4ccd-968a-a2405ee177e7",
|
||||
"revision": 0,
|
||||
"last_node_id": 85,
|
||||
"last_node_id": 86,
|
||||
"last_link_id": 152,
|
||||
"nodes": [
|
||||
{
|
||||
@@ -495,7 +495,8 @@
|
||||
81,
|
||||
1,
|
||||
1
|
||||
]
|
||||
],
|
||||
"title": "WAN VACE control"
|
||||
},
|
||||
{
|
||||
"id": 63,
|
||||
@@ -706,7 +707,7 @@
|
||||
"widget_ue_connectable": {}
|
||||
},
|
||||
"widgets_values": [
|
||||
"Fisheye_outpainted_flux_dev.png",
|
||||
"camera_example_fisheye.jpg",
|
||||
"image"
|
||||
]
|
||||
},
|
||||
@@ -780,7 +781,7 @@
|
||||
"widget_ue_connectable": {}
|
||||
},
|
||||
"widgets_values": [
|
||||
"wan2,1_vace14B_fp16.safetensors",
|
||||
"wan2.1_vace_14B_fp16.safetensors",
|
||||
"default"
|
||||
]
|
||||
},
|
||||
@@ -1051,7 +1052,8 @@
|
||||
0,
|
||||
true,
|
||||
false
|
||||
]
|
||||
],
|
||||
"title": "Render control frames + masks"
|
||||
},
|
||||
{
|
||||
"id": 85,
|
||||
@@ -1082,6 +1084,30 @@
|
||||
24,
|
||||
32
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 86,
|
||||
"type": "MarkdownNote",
|
||||
"title": "About this workflow",
|
||||
"pos": [
|
||||
-1085.614501953125,
|
||||
34.52389907836914
|
||||
],
|
||||
"size": [
|
||||
520,
|
||||
262
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"properties": {},
|
||||
"widgets_values": [
|
||||
"# WAN VACE: still image + camera move → video\n\nTurns a single 180° fisheye still into a camera-move video: metric depth → point cloud → **CameraMotionNode** renders point-splat frames and masks along a loaded trajectory; these become the VACE control video/masks with the original image as the reference, and WAN 2.1 VACE 14B synthesizes the final clip from your prompt.\n\n- **Set:** fisheye image; trajectory file (record one with SaveTrajectory); the positive prompt.\n- **Requires:** WAN 2.1 VACE models (install.sh `vae`), Depth-Anything V2, VideoHelperSuite (only for the h264/MP4 export — SaveWEBM works without it).\n- **Fixed 2026-07:** the UNET filename contained a typo (`wan2,1_vace14B` → `wan2.1_vace_14B_fp16.safetensors`)."
|
||||
],
|
||||
"color": "#432",
|
||||
"bgcolor": "#653"
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
@@ -1318,7 +1344,60 @@
|
||||
"IMAGE"
|
||||
]
|
||||
],
|
||||
"groups": [],
|
||||
"groups": [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "1. Fisheye image → point cloud",
|
||||
"bounding": [
|
||||
-545.614501953125,
|
||||
357.1524963378906,
|
||||
1213.47607421875,
|
||||
760.8647155761719
|
||||
],
|
||||
"color": "#3f789e",
|
||||
"font_size": 24,
|
||||
"flags": {}
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"title": "2. Camera-motion control video",
|
||||
"bounding": [
|
||||
-28.916632652282715,
|
||||
970.2586669921875,
|
||||
1234.2812566757202,
|
||||
456.984619140625
|
||||
],
|
||||
"color": "#a1309b",
|
||||
"font_size": 24,
|
||||
"flags": {}
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"title": "3. WAN VACE generation",
|
||||
"bounding": [
|
||||
-35.783010482788086,
|
||||
-25.47610092163086,
|
||||
1676.566946029663,
|
||||
968.7900657653809
|
||||
],
|
||||
"color": "#8A8",
|
||||
"font_size": 24,
|
||||
"flags": {}
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"title": "4. Save",
|
||||
"bounding": [
|
||||
1361.3507080078125,
|
||||
107.86778259277344,
|
||||
998.1239013671875,
|
||||
914.3678741455078
|
||||
],
|
||||
"color": "#b58b2a",
|
||||
"font_size": 24,
|
||||
"flags": {}
|
||||
}
|
||||
],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
@@ -1334,7 +1413,8 @@
|
||||
"VHS_latentpreview": false,
|
||||
"VHS_latentpreviewrate": 0,
|
||||
"VHS_MetadataImage": true,
|
||||
"VHS_KeepIntermediate": true
|
||||
"VHS_KeepIntermediate": true,
|
||||
"camera_comfyui_rev": 1
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +71,8 @@ def _load_outpaint_node_class():
|
||||
raise RuntimeError(
|
||||
"OutpaintAnyProjection could not be imported from flux_fisheye_filling_nodes. "
|
||||
"It requires the inpainting_flux custom node package (Flux NF4 inpainting, "
|
||||
"diffusers). Install/fix custom_nodes/inpainting_flux and its dependencies. "
|
||||
"diffusers), which this pack's install.py sets up automatically (ComfyUI-Manager "
|
||||
"runs it on install). Run install.py or fix custom_nodes/inpainting_flux. "
|
||||
f"Import error: {exc}"
|
||||
) from exc
|
||||
|
||||
|
||||