Nodes/Atlas Camera/Atlas Fill Occluded πŸ•³οΈβ€πŸ©Ή
ComfyUI Node

Atlas Fill Occluded πŸ•³οΈβ€πŸ©Ή

Fill the holes a camera move reveals β€” and register each fill back as geometry

By mikejamesvfxΒ·Created 3 months agoΒ·Updated 13 days agoΒ· 1
Atlas Fill Occluded πŸ•³οΈβ€πŸ©Ή
  • solve
  • source_image
  • model
  • clip
  • vae
  • camera_path
  • primary_depth
  • exclude_mask
  • solve
  • image
  • report
β—„exact_view_overrideβ–Ί
β—„max_rois8β–Ί
β—„min_area_px1024β–Ί
β—„pad_frac0.10β–Ί
β—„snap64β–Ί
β—„hole_dilate_px4β–Ί
β—„max_gen_long_edge1280β–Ί
β—„promptReconstruct the masked region so it matches the surrounding lighting direction, materials, palette and camera perspective. The masked pixels are geometry the original camera never saw, revealed by a camera move -- continue the surrounding surfaces, do not invent new objects.β–Ί
β—„seed990112233β–Ί
β—„fill_modelβ–Ύβ–Ί
β—„flux_guidance30.0β–Ί
β—„steps20β–Ί
β—„cfg1.0β–Ί
β—„sampler_nameβ–Ύβ–Ί
β—„schedulernormalβ–Ί
β—„denoise1.00β–Ί
β—„relief_grid96β–Ί
β—„camera_sourceβ–Ύβ–Ί
β—„on_registration_failureβ–Ύβ–Ί
β—„registration_min_inliers40β–Ί
β—„registration_max_residual_m0.35β–Ί
β—„registration_max_deviation_deg25.0β–Ί
β—„min_gen_long_edge1024β–Ί
β—„scale_max_ground_disagreement0.00β–Ί
β—„scale_max_sibling_disagreement1.80β–Ί
β—„scale_refuse_never_behindtrueβ–Ί

The moment your camera moves off the recovered viewpoint, it reveals parts of the scene the photograph never saw - the far side of a column, the ground behind a foreground figure. Those are disocclusion holes, and they're the thing that makes a 2.5D projection read as a cheap card trick instead of a move. Most fill approaches answer the wrong question: they fill a hole in a render, which is only valid for the exact camera path it was generated against. Change the angle by a degree and the fill is worthless, and nothing ever comes back into the scene.

Atlas Fill Occluded πŸ•³οΈπŸ©Ή fills a hole in the scene. Each fill is registered back as a ProjectionSource - real geometry the solve owns - so it exports to your DCC and every later camera move reuses it instead of re-inventing it. That's the whole architectural difference, and it's why this node sits in Atlas/advanced and why the fix it does actually sticks.

How it works

For each move-revealed cluster of holes it runs a per-ROI loop: survey the hole regions β†’ crop the ROI at its own native raster β†’ fill it with an edit model at that size β†’ composite the fill back into the plate β†’ register the fill plus its crop camera back as geometry via AtlasAddPatchView. It fills at most max_rois clusters (default 8), largest first, with min_area_px (default 1024) as the floor. That budget is on purpose: leftover speckle at the rim of a tear is cheaper handled by a band layer than by a diffusion pass each. Residual hole after a defaults run is expected - it isn't evidence the node failed.

The fill model defaults to flux_fill, wired through InpaintModelConditioning so the noise mask confines the sampler to the hole. qwen_edit is the alternative (Qwen Image Edit with an inpainting LoRA is the author's measured pairing for registering fills). If you pick flux_fill, remember its guidance wants to be high - the default flux_guidance of 30 is there because Fill models are not ordinary FLUX. The node takes model, clip, vae sockets like any loader-based graph, and fill_model is not validated against what you wire in - matching them is on you.

What matters most

  • camera_path - the move whose disocclusion you're filling. Holes are surveyed at its final frame, whose holes are a superset of every earlier frame's. No path = the solved pose, which by definition has almost nothing to fill.
  • primary_depth - metric depth of the primary scene. The register_to_primary camera mode requires it; without it every ROI falls to the failure policy.
  • prompt - a sensible default ("continue the surrounding surfaces, do not invent new objects") that reads like it was written by someone who's seen a fill model wallpaper a hole with a beach. min_gen_long_edge exists for the same reason: below ~1024, small clusters reach FLUX at native size and answer with nonsense - the author measured two 256Γ—256 ROIs both answered with sand.

Outputs are the repaired image, the updated solve, and a report. Read the report's fillable hole numbers, not the raw peak hole figure - the raw number counts sky and off-plate pixels and will make a good run look like a failure.

Installing

Part of atlas-camera:

cd <ComfyUI>/custom_nodes
git clone https://github.com/mikejamesvfx/atlas-camera.git

Restart. The heavy lifting is model loading, which you already know how to do - you need FLUX Fill (or Qwen Image Edit) plus CLIP and VAE on the inputs. No non-commercial model is required by the pack itself.

Honest expectations

This is the most demanding node of the batch, and the author's own notes are refreshingly blunt about what "worked" means: at defaults on their test shot, fillable hole went from 4.56% to 1.28% of frame - 72% of the real disocclusion closed, zero pixels opened. That's the shape of success: not zero holes, but most of the real ones gone with none invented. And the default camera_source (declared_orbit) constructs the end pose exactly rather than measuring it, because a measured pose of a generated view was demonstrably worse than the constructed one. If you switch to register_to_primary, on_registration_failure decides what happens to ROIs the registration won't stand behind - skip for unattended loops, declared_orbit for artist-mode fallback. For a first run, leave the defaults alone and read the report.

CategoryAtlas/advanced

Inputs (34)

NameTypeDefaultDescription
solveATLAS_SOLVEβ€”
source_imageIMAGEβ€”
modelMODELThe edit model that fills each crop -- Qwen Image Edit 2509 with an inpainting LoRA is the measured pairing. It must be an EDIT model: a video model's fills cannot be registered.
clipCLIPβ€”
vaeVAEβ€”
camera_pathoptATLAS_CAMERA_PATHThe move whose disocclusion is being filled. Holes are surveyed at its FINAL frame, whose holes are a superset of every earlier frame's. No path = the solved pose, which by definition has almost nothing to fill.
primary_depthoptATLAS_DEPTH_MAPMetric depth of the primary. REQUIRED by register_to_primary -- without it every ROI falls to the failure policy.
exclude_maskoptMASKRegion carried by something other than geometry (a SkyDome, a matte). Subtracted from every survey mask, so it can only ever REMOVE candidate clusters.
exact_view_overrideoptSTRINGAtlasCameraMovePreset's exact_view. Carries the pivot= term, so the patch camera reproduces the move's END pose exactly instead of orbiting the ground ray.
max_roisoptINT81–64Ceiling on clusters filled, largest first. The long tail of speckle is not worth a generation each.
min_area_pxoptINT102464–1048576β€”
pad_fracoptFLOAT0.100–0.5β€”
snapoptINT648–128β€”
hole_dilate_pxoptINT40–64β€”
max_gen_long_edgeoptINT1280256–4096β€”
promptoptSTRINGReconstruct the masked region so it matches the surrounding lighting direction, materials, palette and camera perspective. The masked pixels are geometry the original camera never saw, revealed by a camera move -- continue the surrounding surfaces, do not invent new objects.β€”
seedoptINT9901122330–72057594037927940β€”
fill_modeloptCOMBOflux_fill (default) wires FLUX Fill through InpaintModelConditioning, whose noise_mask confines the sampler to the hole. qwen_edit wires Qwen Image Edit, whose mask is advisory -- measured, it returned the hole's neutral fill untouched even with 79% of the crop as real context. Wire the loaders to match: the model/clip/vae inputs are not validated against this choice.
flux_guidanceoptFLOAT30.00–100FLUX Fill's guidance (ignored by qwen_edit). The Fill models want a much higher value than ordinary FLUX.
stepsoptINT201–200β€”
cfgoptFLOAT1.00–30β€”
sampler_nameoptCOMBO6 options: euler, euler_ancestral, dpmpp_2m, dpmpp_2m_sde, ddim, uni_pc
scheduleroptCOMBOnormal6 options: beta57, normal, karras, simple, beta, sgm_uniform
denoiseoptFLOAT1.000–1β€”
relief_gridoptINT9616–512β€”
camera_sourceoptCOMBOdeclared_orbit (default) uses the camera Atlas CONSTRUCTED for this fill -- the move's end pose, exact, not a guess. register_to_primary MEASURES it instead, which is right for a novel-view generator whose camera is unknown and wrong here: measured 2026-09-03, the composited frame registers with 3583 inliers yet lands 10.1 deg from the pose it was demonstrably rendered at, while the plate at the primary pose -- a genuine 12 deg away -- measured 10.0 deg. The estimate does not track the difference, so a high inlier count is not proof of a correct pose.
on_registration_failureoptCOMBOAn unattended loop must not append geometry the pixels do not support. 'skip' drops the ROI and reports it; 'declared_orbit' restores AtlasAddPatchView's artist-mode fallback.
registration_min_inliersoptINT404–10000β€”
registration_max_residual_moptFLOAT0.350.01–10β€”
registration_max_deviation_degoptFLOAT25.01–180β€”
min_gen_long_edgeoptINT10240–4096Scale each crop UP to this long edge before the fill, and back DOWN to its native rect afterwards (0 = off). max_gen_long_edge only caps, so without a floor a small cluster reaches the model at its native size -- and an auto survey produces exactly those. Measured 2026-09-04 on the sea-cliff castle: two 256x256 ROIs, and FLUX Fill answered both with a sand beach. 1024 is FLUX's own training band; the cap still wins a conflict. Nothing downstream sees the working raster -- the fill is resampled to the crop rect before the composite and before the patch camera.
scale_max_ground_disagreementoptFLOAT0.000–100Refuse a patch scale that disagrees with that patch's own ground fit by more than this factor (0 = never refuse), falling back to the ground fit. Off by default because the threshold is not yet measured: on the sea-cliff castle the disagreement did NOT separate a good patch from a bad one -- the ROI that painted 69% of its hole and the one that painted 5% both disagreed by 2.15x -- so any threshold catching the bad one also refuses the good one. RUN AT 1.5 and scored: the bad ROI went 5% -> 63% painted and put its ground back on Y=0 (+0.783 m -> +0.002), and the good one went 69% -> 16% and floated to +0.800 -- net 5,425 -> 5,965 px of fillable hole, 10% WORSE, because the good ROI's hole is 14,111 px against the bad one's 5,123. Arm it only per-scene, having read the disagreements. See AtlasAddPatchView's own tooltip.
scale_max_sibling_disagreementoptFLOAT1.800–100Refuse a patch scale that disagrees with the median of this run's EARLIER patches by more than this factor, and adopt that median. The only gate that saw the castle failure: its sample spread was tighter than two good patches' and its ground disagreement equal to one, but its scale was 0.45x what its four siblings measured. Order matters -- patches are added one at a time, so early ROIs are judged against few or none. 0 = off; 1.8 is the measured castle value (ROI 4 read 1.06x and ROI 5 2.30x, so the gap is wide) and the only gate setting of the three that improved the outcome: 5,425 -> 5,285 px of fillable hole, against the ground gate's 5,965.
scale_refuse_never_behindoptBOOLEANtrueRefuse a patch scale under which not one pixel of the fill sits behind the primary's surface, and rebuild at the sibling median or the ground fit. A disocclusion hole is a view of what lies BEHIND what the primary can see, so a fill for one that is entirely in FRONT contradicts its own reason to exist -- a contradiction, not a tuned threshold, which is why it is a boolean. Measured on the sea-cliff castle: the ROI that painted 5% of its hole was the only one of five with every pixel in front, and the next-lowest painted 30%. See AtlasAddPatchView's tooltip.

Outputs (3)

NameTypeDescription
solveATLAS_SOLVEβ€”
imageIMAGEβ€”
reportSTRINGβ€”