Atlas Ghost Pixels π»
The only pixels a generative fill is allowed to invent
- solve
- source_image
- camera_path
- exclude_mask
- guide
- valid
- ghost
- invalid
- debug_overlay
- report
Move a camera through a 2.5D projection and something has to fill the gaps: behind a tree, past a wall, off the edge of the photograph. The single most useful question you can ask before aiming a diffusion fill at that hole is which holes are actually holes. Atlas Ghost Pixels π» answers it by classifying every pixel of a rendered move as VALID, GHOST, INVALID or OUT_OF_BOUNDS instead of reporting the split as prose.
Why the prose version failed
The node exists because of a specific bad number. The raw "peak hole" figure it replaces climbed 64.8% β 86.1% on an improving run, because it was counting sky as hole. Sky isn't occluded geometry - it's geometry that was never derived. Those are different pixels with different fixes, and lumping them together makes your metric move the wrong way.
So the classes mean something:
- GHOST - a target sample whose location becomes visible after the camera moves, but whose appearance was never observed because something opaque stood in front. This is the only class a generative filler should be aimed at.
- INVALID - geometry that was never derived (sky, a partial relief mesh). Nothing occluded it, so a fill there paints over an upstream gap. Fix the geometry, don't hallucinate past it.
- OUT_OF_BOUNDS - the camera swinging past the edge of the photograph. That's outpainting, a different job, and it must not consume your fill budget.
Mechanism, and the part that got found live
Same rasterizer, same raster, and the same holes as AtlasDisocclusionGuide. The invariant is that ghost | invalid | out_of_bounds equals that node's hole_mask exactly - this node reclassifies the hole, it does not find a different one. Classification lives in core/ghost_pixels.py and is numpy-only, which is why CI tests it for real rather than skipping it.
The sky test is the bit I'd have got wrong: it runs through the cameras at infinity, not pixel for pixel. Two views differing by a rotation don't see sky at the same pixels, so the straightforward subtraction left a crescent of skyline ranking as a genuine hole - found live on 2026-08-15 on an arc. Worth knowing if you ever wonder why a rule that sounds over-engineered is there.
Inputs and outputs
Required: solve, source_image.
Optional: camera_path (the move to classify - every sampled frame becomes one image in the output batch; without it you get the solved camera itself, which has almost no ghosts by definition, so that's the identity sanity check, not a result), resolution (1024 long edge, same numpy rasterizer cost warning: O(faces Γ pixels) and it renders EVERY path frame), hole_dilate_px (0 - raise it to match AtlasDisocclusionGuide so the two agree pixel for pixel, but note it only ever marks more as unseen, so it moves the reported fractions; leave at 0 when you're measuring), exclude_mask (sky on a matte or a SkyDome; excluded pixels classify LOW_CONFIDENCE and leave the fractions from both numerator and denominator - an artist declaration is intent, not evidence, so it can never be reported as a factual disocclusion), last_n (0 = every frame; the end of a move holds a superset of every earlier frame's holes, and batch indices become relative to that window), overlay_alpha (0.55; below 1.0 keeps the reprojected RGB readable underneath, which is what makes the overlay useful for judging whether the geometry is right).
Outputs: guide (IMAGE batch), valid, ghost, invalid (MASK batches), debug_overlay (IMAGE batch), report (JSON, not prose - that's the point).
Install
cd <ComfyUI>/custom_nodes
git clone https://github.com/mikejamesvfx/atlas-camera.git
Or ComfyUI Manager β atlas-camera (Registry). Restart. Core nodes are pure Python with no pip step. MIT. You'll want a solve with derived projection geometry upstream and, if you're classifying a move, a camera path.
Getting useful output
Wire ghost into whatever does the repair, and wire exclude_mask with your sky matte so the fractions mean something. Feed the report to a file and diff two runs rather than eyeballing a render.
Two honest limits. It applies the camera path's fov_deg channel, so a zoom renders as a zoom and correctly produces no ghosts - a focal change moves no eye and therefore reveals nothing. And it does not handle lens distortion, because Atlas solves a pinhole camera; if your plate is wide and distorted, straighten it first or the borders of your classification drift.
Inputs (8)
| Name | Type | Default | Description |
|---|---|---|---|
| solve | ATLAS_SOLVE | β | |
| source_image | IMAGE | β | |
| camera_pathopt | ATLAS_CAMERA_PATH | The move to classify. Every sampled frame becomes one image in the output batch. Without it you get the solved camera itself, which by definition has almost no ghosts β that is the identity sanity check, not a useful result. | |
| resolutionopt | INT | 1024256β4096 | Long edge. The pure-numpy rasterizer is O(faces x pixels) and renders EVERY path frame; keep it moderate for long moves on dense relief meshes. |
| hole_dilate_pxopt | INT | 00β64 | Grow the hole before classifying, matching AtlasDisocclusionGuide so the two agree pixel for pixel. NOTE this moves the reported fractions: dilation only ever marks MORE as unseen. Leave at 0 when you are measuring rather than conditioning. |
| exclude_maskopt | MASK | Region carried by something other than geometry β sky on a matte or a SkyDome. Excluded pixels classify as LOW_CONFIDENCE and leave the fractions from BOTH numerator and denominator. An artist declaration is intent, not evidence, so it can never be reported as a factual disocclusion. | |
| last_nopt | INT | 00β4096 | Classify only the move's LAST N frames (0 = every frame). The end of a move holds a superset of every earlier frame's holes, so rendering 100 frames to read 1 is waste. Batch indices become RELATIVE to the window. |
| overlay_alphaopt | FLOAT | 0.550β1 | Overlay tint strength. Below 1.0 keeps the reprojected RGB readable underneath, which is what makes the overlay useful for judging whether the geometry is right. |
Outputs (6)
| Name | Type | Description |
|---|---|---|
| guide | IMAGE | β |
| valid | MASK | β |
| ghost | MASK | β |
| invalid | MASK | β |
| debug_overlay | IMAGE | β |
| report | STRING | β |