Atlas Add Patch View (multi-angle) π©Ή
Atlas Add Patch View, angle by angle
- solve
- patch_image
- plate_ref
- primary_depth
- exclude_mask
- patch_mask
- primary_image
- crop
- solve
- report
Projection has a hard limit: a single photograph can only texture what the recovered camera actually saw. The moment you orbit past the recovered viewpoint, occluded and grazing areas go black. Atlas Add Patch View (multi-angle) π©Ή is how you patch those holes - it takes a novel view of the same scene, generated at a defined angle by an image model, and stitches it back into the scene as if it had been shot from that angle in the first place.
The workflow it slots into
This is the multi-angle patch chain from the pack's examples. You:
- Run your photo through
AtlasInput(or any solve) to get anATLAS_SOLVE. - Generate a new view of the scene with the Qwen-Image-Edit-2511 Multiple-Angles LoRA - commonly via the
ComfyUI-qwenmultiangle"Qwen Multiangle Camera" node. - Feed that generated image into this node as
patch_image, alongside the solve. - Chain one node per angle; the viewport layers them over the primary, filling what it couldn't see.
Mechanically, the node constructs a patch camera by orbiting the recovered camera around the scene pivot to the declared view. That's the clever part: the patch shares the primary's world frame by construction, so nothing needs to be lined up by hand. It derives the patch view's own relief geometry in that frame (Depth Anything by default, or MoGe), and appends it to the solve as a ProjectionSource.
The trap that trips everyone
The LoRA's angles are absolute, subject-relative - "right side view" means 90Β° around the subject's front, not relative to your source photo. So you must tell the node two things: the view the source photo represents (source_azimuth_view, source_elevation_view) and the view the patch was generated at (patch_azimuth_view, patch_elevation_view, patch_distance). The actual orbit applied is patch β source. For a straight-on source photo, leave source_azimuth_view at "front view" and the patch's named view maps directly. If the patch lands mirrored, flip_azimuth is the one-click fix.
The other dropdowns that matter:
geometry_source-reuse_scene(default) turns the patch into a pure texture projector onto copies of geometry already in the solve, exactly how a matte painter projects new paint from a second camera onto the same geo in Nuke. No depth model runs, no scale problem.own_depthis the older behavior for genuinely new terrain - it needs the[neural]extra.mask_unseen_only- embeds an "unseen areas" matte so the patch only paints where the primary's projection is actually invalid. Leave it on;unseen_dilate_px(default 16) hides hairline seams at the boundary.primary_depth- strongly recommended: the shared depth map of the source photo. It unlocks overlap-based scale registration and the true depth-shadow term in the unseen matte.camera_source-declared_orbitplaces the patch by the named-view difference.register_to_primarymeasures the patch camera (MoGe pointmap + SIFT matches β RANSAC) against the primary's metric depth, resolvingflip_azimuthautomatically and reporting inliers and residual. Needsprimary_depth.
If you're coming from AtlasBlockoutViewport's Extract Angle, wire its patch_prompt into patch_view_override (or patch_exact into exact_view_override) and the dropdowns become redundant - the measured orbit drives everything.
Install and gotchas
Same pack install as every Atlas node: cd ComfyUI/custom_nodes && git clone https://github.com/mikejamesvfx/atlas-camera.git, then restart. This node's depth-based modes need the [neural] extra - GeoCalib plus transformers - or it fails soft with an explanatory report. Watch the license: the default depth model is Depth Anything V2 Metric Outdoor, and its Large weights are CC BY-NC 4.0; pick a small/base variant (Apache 2.0) if commercial use matters. The node outputs the updated solve and a text report - the report is worth reading, it tells you which registration path actually ran.
Inputs (37)
| Name | Type | Default | Description |
|---|---|---|---|
| solve | ATLAS_SOLVE | β | |
| patch_image | IMAGE | β | |
| patch_azimuth_viewopt | COMBO | front-right quarter view | The LoRA azimuth the patch was generated at β MUST match the Qwen Multiangle Camera node. Absolute about the subject's front. |
| patch_elevation_viewopt | COMBO | eye-level shot | The LoRA elevation the patch was generated at (match the LoRA node). |
| patch_distanceopt | COMBO | medium shot | The LoRA distance the patch was generated at (match the LoRA node). |
| source_azimuth_viewopt | COMBO | front view | Which view your SOURCE photo already is, in the LoRA's absolute frame. Orbit applied = patch β source. Leave 'front view' for a straight-on source. |
| source_elevation_viewopt | COMBO | eye-level shot | Elevation of the SOURCE photo in the LoRA's frame. |
| flip_azimuthopt | BOOLEAN | false | Flip left/right if the patch lands on the wrong side (recovered-camera handedness) β a calibration convenience. |
| nameopt | STRING | patch | β |
| depth_modelopt | COMBO | depth-anything/Depth-Anything-V2-Metric-Outdoor-Large-hf | 10 options: depth-anything/Depth-Anything-V2-Metric-Outdoor-Large-hf, depth-anything/Depth-Anything-V2-Metric-Indoor-Large-hf, depth-anything/DA3METRIC-LARGE, depth-anything/DA3MONO-LARGE, depth-anything/DA3NESTED-GIANT-LARGE-1.1, Ruicheng/moge-2-vitl-normal, +4 |
| relief_gridopt | INT | 9616β4096 | Patch relief-mesh density (long-edge grid columns). |
| priorityopt | FLOAT | 10β100 | Blend priority among patches (higher wins). The primary photo is always highest; patches only fill where it can't see. |
| plate_refopt | ATLAS_PLATE_REF | Optional registered final plate reference. Browser still uses image_b64 preview; exporters use this for EXR/float-safe handoff. | |
| deviceopt | COMBO | auto | 4 options: auto, cuda, mps, cpu |
| patch_view_overrideopt | STRING | Optional: wire AtlasBlockoutViewport's patch_prompt output here (the '<sks> [azimuth] [elevation] [distance]' string from π Extract Angle) β when connected it OVERRIDES the three patch_* dropdowns above, so the extracted angle drives both the Qwen generation and this node identically with one wire. (A single STRING socket because ComfyUI's backend rejects STRING links into combo dropdowns.) Errors loudly if the string doesn't parse, rather than silently patching at the wrong angle. | |
| exact_view_overrideopt | STRING | Optional: wire AtlasBlockoutViewport's patch_exact output here ('azimuth_deg=.. elevation_deg=.. distance_scale=..' β π's RAW measured orbit, before named-view snapping β or AtlasCameraMovePreset's exact_view, which appends 'pivot=x,y,z' so its scene-depth pivot is reproduced instead of the default ground-ray pivot). When connected it WINS over patch_view_override AND the dropdowns, and flip_azimuth is ignored (the raw delta is already in orbit_camera's own convention). This is the render-conditioned patch loop's channel: a frame baked at the artist's real orbit (then repaired by AtlasRenderFix) must project back from the IDENTICAL pose β the 45Β° named-view grid would misregister it. Errors loudly if unparseable. | |
| mask_unseen_onlyopt | BOOLEAN | true | Embed an UNSEEN-AREAS matte on the patch (ProjectionSource.mask_b64): the patch only paints where the PRIMARY camera's projection is invalid at the patch view (behind-camera, out-of-frame, and β when primary_depth is wired β hidden behind nearer geometry, the true MPTK depth-shadow test). Everywhere the primary CAN see keeps the primary's real pixels; the AI patch fills only genuine gaps. Also rides into the Nuke/Maya exports as the patch plate's alpha. |
| unseen_dilate_pxopt | INT | 160β200 | Dilate the unseen matte so the patch slightly overlaps the primary's coverage edge (hides hairline seams at the boundary). |
| primary_depthopt | ATLAS_DEPTH_MAP | STRONGLY RECOMMENDED: the shared AtlasDepthMap of the SOURCE photo. Enables (1) overlap-based scale REGISTRATION β the patch mesh's metric scale is solved by matching its depth against the primary's in the mutually-visible region, so the patch actually sits in the primary's world instead of trusting an independent (and fragile, on AI-generated views) ground fit; and (2) the true depth-shadow term in the unseen matte. | |
| exclude_maskopt | MASK | Segmentation of the PATCH image's sky (run SAM3Segment on the generated novel view, prompt 'sky'). In reuse_scene mode it keeps the patch from painting sky onto scene geometry; in own_depth mode it REPLACES the internal sky heuristic during meshing (hallucinated near-depth sky otherwise triangulates into geometry bulging toward the camera). | |
| geometry_sourceopt | COMBO | reuse_scene | reuse_scene (recommended): the patch derives NO geometry of its own β it becomes a pure texture projector onto copies of the geometry already in the solve (sky dome, band meshes, derived proxies), exactly how a DMP artist projects new paint from a second camera onto the SAME geo in Nuke. The scale/registration problem dissolves: that geometry is in the primary's world by construction, and Qwen scene mismatch shows only as texture misalignment, never floating geometry. No depth model runs. own_depth: the previous behavior (Depth Anything on the patch + overlap registration) β for patches revealing genuinely NEW terrain no existing geometry covers. Auto-falls back to own_depth when the solve carries no geometry to reuse. |
| patch_maskopt | MASK | Optional REGION-OF-INTEREST matte in the patch image's own frame: the patch only ever paints INSIDE it (ANDed with the unseen matte, both sides sharing unseen_dilate_px of overlap). The occlusion-fill loop wires the FILLED hole pixels here so a repaired end frame contributes exactly its fills β not a second full-frame copy of the scene, and never the sentinel still marking holes that were NOT filled. | |
| camera_sourceopt | COMBO | declared_orbit | declared_orbit: place the patch by the named-view difference (the LoRA angle you asked for; flip_azimuth by eye). register_to_primary: MEASURE the patch camera β MoGe pointmap on the patch + SIFT matches to the primary photo -> RANSAC similarity (s,R,t) against the primary's metric depth. Needs primary_depth (metric) and the primary image. Resolves flip_azimuth automatically and reports inliers / residual / deviation from the declared orbit. Falls back to the declared orbit (with the numbers) when the gates fail. Generated pixels register TO the measured world; the primary never moves. The pose inherits primary_depth's SCALE (metric when that is; always consistent with the geometry the patch projects onto). |
| primary_imageopt | IMAGE | The SOURCE photo (same image the solve came from), for feature matching in register_to_primary. If not wired the node tries solve.image_path. | |
| registration_min_inliersopt | INT | 406β5000 | Reject the measured camera below this many RANSAC inliers. |
| registration_max_residual_mopt | FLOAT | 0.350.01β50 | RANSAC inlier threshold AND accept ceiling on the 3D RMS residual, in metres of the primary's world. |
| registration_max_deviation_degopt | FLOAT | 250β180 | Reject when the measured camera's forward axis deviates more than this from the closest declared orbit (flip or no flip). Catches a LoRA that ignored the requested angle. |
| auto_flip_azimuthopt | BOOLEAN | true | register_to_primary: accept whichever handedness (flip on/off) the measurement lands on and record it. Off = a closer FLIPPED pose counts as a disagreement with your flip_azimuth. |
| depth_edge_relopt | FLOAT | 0.500β1000000000 | Tear a cell whose depth jumps more than this RATIO across one grid step. On a novel view that is right -- the tear is a real silhouette and whatever is behind reveals through it. A patch generated to FILL a disocclusion is itself the thing behind, so there every torn cell re-opens the hole the patch existed to close: measured 2026-09-04, a fill came back 40% torn and left the interior of its own ROI still holed. Raise it for a backmost layer, but LOOSEN max_edge_factor rather than disabling it -- measured, 0 shipped 11 m triangles; the cost is a stretched triangle bridging a genuine cliff instead of a hole, which is the seam doctrine's own trade -- the smear belongs on the layers behind. |
| max_edge_factoropt | FLOAT | 12.00β1000 | Tear a triangle whose world edge exceeds this multiple of the expected local sample spacing -- the second silhouette test, which catches metres-long shards a depth RATIO just under threshold still produces. 0 disables it. Set to 0 alongside a raised depth_edge_rel to stop a fill patch tearing at all. |
| sky_heuristicopt | BOOLEAN | true | Drop pixels above the horizon whose depth is far or ROUGH, before meshing. Right for a novel view: monocular depth has no cue on sky and hallucinates noise there, which triangulates into geometry bulging at the camera. Wrong for a FILL patch, whose depth is an estimate over INVENTED content and so is rough by construction -- measured, it removed 35% of the faces inside the hole, in a layer with nothing behind it, so what it leaves is a hole rather than slightly wrong depth. An explicit exclude_mask still applies either way; this only governs the internal guess. |
| scale_max_rel_iqropt | FLOAT | 1.000β10 | Refuse a registered scale whose samples disagree more than this, measured as the quartile spread over the median (0 disables the gate). The fit is a MEDIAN, so it returns a confident number whatever the agreement behind it: on the sea-cliff castle two adjacent ground patches, same size and distance, fitted 0.645 and 0.273 -- the second put its ground 0.78 m in the air and painted 5% of its own hole. 1.0 is measured, not guessed (tests/test_patch_scale_occlusion_bias): on a synthetic of known truth the spread reaches 0.75 while the fit is still EXACT, and 1.2 at the first wrong one, so the only threshold that refuses no good fit and catches the first bad one is between. A refusal falls back to the ground fit and says so in scale_source and in the report. Note what this CANNOT see: past ~78% occluded samples the spread returns to 0.000 while the scale is 75% wrong, because a single population that has won outright agrees with itself. That case is the exclude mask's job (the fit is given the hole to drop) and min_samples', not this gate's. |
| scale_max_ground_disagreementopt | FLOAT | 0.000β100 | Refuse a registered scale that disagrees with the patch's OWN ground fit by more than this factor, either way (0 = never refuse). The two estimators share no evidence: the registration fits depth ratios against the primary's map, the ground fit lands the patch's own near-horizontal surfaces on Y=0. So they only agree when both are right, which is what makes the cross-check see failures dispersion cannot -- on the castle the broken ROI put its ground 0.78 m in the air with the camera at 1.6 m, and 1.6*(1-0.51)=0.78 recovers its fitted ratio exactly. Skipped when the patch shows no usable ground (the fit says so, and a patch that is all sky or all facade legitimately has none), so this gate abstains rather than guessing. A refusal falls back to that same ground fit and says so in scale_source. DEFAULT OFF, and that is a measurement: on the castle the disagreement did not separate outcomes -- the ROI painting 69% of its hole and the one painting 5% both read 2.15x -- and arming at 1.5 fixed the bad one (5% -> 63%, ground +0.783 m -> +0.002) while breaking the good one (69% -> 16%), for a net 10% MORE fillable hole. It is a real instrument and a poor automatic gate; read it before arming it. |
| scale_max_sibling_disagreementopt | FLOAT | 1.800β100 | Refuse a patch scale that disagrees with the MEDIAN of the patches already on this solve by more than this factor, and adopt that median instead (0 = never refuse). The other two gates ask a fit whether it agrees with itself or with one other estimator on the same patch, and a confidently-wrong fit passes both -- measured on the sea-cliff castle, the ROI that painted 5% of its hole had a TIGHTER spread than the two that painted 69%, and disagreed with its ground fit by exactly as much as a good one. Its siblings caught it: scales ran 0.830 / 0.611 / 0.381 / 0.645 / 0.273 and the failure is the lone 0.45x outlier. Requires the SAME depth model and a METRIC one -- a relative model normalises each crop on its own, so the numbers would not be comparable -- and at least scale_min_siblings of them, so the first patches on a solve are never judged against nothing. Unlike the other gates this one supplies a REPLACEMENT rather than a veto: the siblings are measurements of the same world. MEASURED at 1.8 on the castle, the only one of the three gates that helps: ROI 5 refused at 2.30x and given the median 0.628, painting 78% of its hole instead of 5% with its ground back at -0.10 m instead of +0.78, ROI 2 untouched at 69%, and 5,425 -> 5,285 px of fillable hole. Compare the ground gate, which fixed the same ROI and cost 10%. The pixel gain understates it -- the metric is read at the move's end frame, where a scale error is largely invisible, while a DCC export sees the floating slab. |
| scale_min_siblingsopt | INT | 32β32 | How many comparable sibling patches must already be on the solve before the sibling check will act. A median needs a population; below this the check abstains and only reports. Note the ORDER dependence this implies -- patches are added one at a time, so the first few are never judged and the last is judged against the most. |
| scale_refuse_never_behindopt | BOOLEAN | true | Refuse a patch scale under which NOT ONE pixel of the patch sits behind the primary's surface, and rebuild at the sibling median (or the ground fit) instead. This is a contradiction rather than a tuned threshold: a disocclusion hole is by definition a view of what lies BEHIND what the primary can see, so a fill for it that is entirely in FRONT of the primary cannot be at the right scale, and the unseen matte duly throws nearly all of it away. Measured on the sea-cliff castle: the ROI that painted 5% of its own hole was the only one of five with matte_depth_ratio_under_1_frac == 1.0, and the next-lowest painted 30%. Its value is COVER: the sibling check abstains until 3 comparable patches exist, so on that scene 3 of 5 patches were judged by nothing at all, the first one included. Costs one extra mesh + matte build on a patch that fires, and no depth re-run. |
| cropopt | ATLAS_CROP | AtlasCropROI's crop handle, when patch_image is that CROP rather than a full-frame novel view. A crop of the plate is the SAME lens with a SHIFTED principal point and a smaller raster (core.camera_crop.crop_intrinsics + scale_intrinsics), so without this the node builds a centred full-frame camera the crop was never shot through and the patch lands off by the crop origin. With it wired the patch camera is the crop camera, exactly β and register_to_primary stops guessing the focal from MoGe, because a crop of the primary's own photograph has a KNOWN camera (MoGe's prediction is kept as a cross-check in the metadata). An empty handle (an unused slot) is a no-op: the full-frame behaviour every saved graph already has. |
Outputs (2)
| Name | Type | Description |
|---|---|---|
| solve | ATLAS_SOLVE | β |
| report | STRING | β |