Nodes/Seb Nodes/Read 360 (Seb)
ComfyUI Node

Read 360 (Seb)

Read 360 (Seb) pulls a photo out of a panorama so you can generate it like a photo

By cyberhirsch·Created about a year ago·Updated 3 days ago· 4
Read 360 (Seb)
  • pano_image_override
  • view_image
  • mask
  • view_info
  • pano_ref
image
projection
yaw0
pitch0
fov90.00
view_width1024
view_height1024
mask_mode
margin_deg7.0
coverage
state_json
canvas_width0

Generating detail inside a 2:1 equirectangular panorama is fighting the projection. Near the poles the image is smeared across a few hundred pixels, the whole format is usually capped at 2048×1024 because you're generating the entire sphere at once, and anything you inpaint in that space comes out warped. The usual answer is to stop generating equirect and start generating views: cut a properly-projected flat window out of the sphere, run the model on it at full resolution, then put it back.

Read 360 is the first half of that. It gives you the flat window, a mask, and two handles you'll need to write the result back onto the sphere.

How it works

The mechanism has three parts, and each one is a decision worth understanding.

The sphere never travels down a wire. It lives in a module-level store keyed by the source filename (or by a hash of the tensor if you use the override input), so what goes through your graph is one framed view, not a 200-odd-megabyte 8K equirect. Four panoramas are kept at a time; a fifth evicts the oldest, committed layer and all. The store is float16 and deliberately unclamped, so values above 1.0 survive a write-back - there's a log-encoded HDR route left open there.

Extraction is a per-pixel inverse projection. For every output pixel, the node builds a ray, un-rotates it by yaw and pitch, and samples the sphere. Longitude wraps at ±180; latitude deliberately clamps instead of wrapping, because otherwise the top rows of your view would sample across to the bottom of the sphere.

margin_deg is the clever bit, and the reason this node exists. The extracted FOV is your framed fov plus twice the margin - so you get a ring of real context around the framing. That ring is visible to the model and gets generated over, but Write 360 never commits it. Which means the seam where the model's output starts going wrong lands in a ring you throw away. It's inpainting's padding trick, applied to a sphere.

The inputs that matter

image is a dropdown of files in ComfyUI's input folder with upload enabled - drop your equirect PNG in like you would for a normal Load Image. If the panorama arrives from the graph instead, skip it and use the optional pano_image_override.

Then the camera: yaw, pitch, fov and the view size. If you don't want to type them, the node face has a WebGL preview and an Open Pano Editor button - a vendored copy of Panorama-Stickers' interactive editor (MIT, © nomadoor) - and framing a shot there writes state_json, which overrides the yaw/pitch/fov widgets. Leave it empty and the widgets are used.

mask_mode decides what the MASK output means, white = generate, following the usual inpaint convention: core is the framed FOV with the margin left as context, full is everything extracted, editor is what you painted with the editor's Mask tool clipped to the core, and unpainted / painted key off the committed layer instead.

canvas_width is the one you'll come back to. It resamples the sphere on load (height = width/2) before the layer is allocated, so it decides how much committed detail can survive; 0 keeps the source size. Set it larger than your seed panorama, or you're blending into a resolution that can't hold the result - changing it later resamples existing committed work rather than dropping it.

coverage (360/180) tracks the editor's viewport coverage; the extraction math is driven by yaw/pitch/fov, so leave it at 360.

Outputs

view_image goes into your sampler (for masked work, InpaintModelConditioning or a VAE Encode + Set Latent Noise Mask, per inpainting.md). mask is the inpaint mask. view_info and pano_ref both go to Write 360 - that's the whole reason the pair exists.

There's no ui image output on purpose - the WebGL widget already shows the framing, and returning one made ComfyUI stack a second, differently-framed preview underneath it. Wire a Preview Image node if you want a static copy.

The density readout

The node face has a widget that reads like this:

8192x4096 sphere | 1706px source -> 1024px view | 0.60x downscale | 22.8 px/deg

That's how many real source pixels actually back this view. Above 1.05× it says UPSCALE, which means you're asking a generator to invent detail the panorama never had - widen the framing, lower the view resolution, or raise canvas_width first. It's a genuinely useful sanity check and I wish more nodes printed one.

Installing it

Part of Seb Nodes: Manager → search seb_nodes → install → restart fully. Manually:

cd ComfyUI/custom_nodes
git clone https://github.com/cyberhirsch/seb_nodes

No model downloads. The pack auto-installs its own Python deps (OpenCV and friends) on first import, so the first start takes a moment longer.

Where people get burned

You replaced the panorama file and nothing changed. The store is keyed by filename and survives across runs - that's what lets committed edits accumulate - so editing the PNG on disk under the same name is not re-read. Restart ComfyUI to drop the store, or use pano_image_override, which hashes the image content so a changed image gets a fresh entry.

editor mask mode with nothing painted. It logs a warning and falls back to core rather than failing. Paint something in the editor and close it (closing is what publishes the rasterised mask) and it'll start working.

Your committed work vanished. More than four panoramas in the store evicts the oldest. Export before you load the fifth.

CategorySeb/360

Inputs (13)

NameTypeDefaultDescription
imageCOMBO1 options: example.png
projectionCOMBO2 options: perspective, equirectangular
yawFLOAT0-180–180
pitchFLOAT0-90–90
fovFLOAT90.0010–160
view_widthINT1024
view_heightINT1024
mask_modeCOMBOWhite = generate. core: the framed FOV, the margin stays as context. full: everything extracted. editor: what you painted with the Pano Editor's Mask tool, clipped to the core. unpainted / painted: by the committed layer.
margin_degFLOAT7.00–45Extra FOV per side, extracted as inpainting context but NOT committed. The generation boundary lands here and is discarded.
coverageCOMBO2 options: 360, 180
state_jsonSTRINGWritten by the Pano Editor. Leave empty to use the yaw/pitch/fov widgets instead.
canvas_widthINT00–16384Resample the panorama to this width on load (height = width/2). 0 keeps the source size. Set it larger than the seed so committed detail has somewhere to live.
pano_image_overrideoptIMAGE

Outputs (4)

NameTypeDescription
view_imageIMAGE
maskMASK
view_infoPANO_VIEW_INFO
pano_refPANO_REF