Nodes/ComfyUI-Majoor-ImageOps/〽️ Image Ops Defocus
ComfyUI Node

〽️ Image Ops Defocus

Real bokeh from a depth map, with the aperture shape you asked for

By MajoorWaldi·Created 9 months ago·Updated 24 days ago· 14
〽️ Image Ops Defocus
  • image
  • depth
  • shape_texture
  • mask
  • image
  • mask
◄bypassfalse►
◄focus_distance0.50►
◄focus_range0.05►
◄max_blur_radius24►
◄num_layers8►
◄bokeh_shapecircle►
◄highlight_threshold0.90►
◄highlight_boost1.5►
◄invert_depthfalse►
◄invert_maskfalse►

You cannot prompt your way to good bokeh. photorealism.md nails the reason: "AI bokeh is often too uniform or has wrong falloff patterns. Real lenses produce characteristic bokeh shapes (circles, cat-eyes, swirly) that AI rarely replicates correctly." ImageOps Defocus is the other route - feed it a depth map and it blurs by distance, convolving each depth slice with an actual aperture kernel, so highlights turn into hexagonal or octagonal discs instead of a mushy Gaussian smear.

This is the node in the pack that earns its keep for portraits and product shots: a sharp subject, a background that falls apart convincingly, and blown highlights that bloom into visible bokeh balls.

How it works

The mechanism in nodes/defocus.py is more interesting than a blur node has any right to be. Your depth map's luma becomes a per-pixel blur amount. The depth range is then cut into num_layers slices; each slice is convolved with a kernel built from the selected aperture shape (a polygon with 0, 6 or 8 sides - circle, hexagon, octagon - or a custom kernel derived from the luma of a connected shape_texture) and the results are interpolated per-pixel between layers so you don't get banding. Pixels inside the in-focus band pass through completely untouched, so they stay pixel-perfect sharp. On top of that, highlight_threshold and highlight_boost pull bright pixels into the blur so they bloom into proper bokeh discs.

One sharp gotcha the author is upfront about: the live canvas preview only approximates this. It renders a few Gaussian blur bands by depth, with no shaped kernel and no highlight bloom. The real hexagonal bokeh and the glowing highlights only appear once the backend runs - i.e. after you queue the prompt. Don't tune the look off the preview.

The knobs that matter

  • depth (input): the depth map. Without it connected the node is a no-op - that's the author's own note and it's the first thing to check.
  • focus_distance (default 0.5) and focus_range (default 0.05): the depth value that stays sharp, and how thick that band is. Small focus_range = razor-thin focal plane. Bump invert_depth if your map's convention is the other way round.
  • max_blur_radius (default 24, 0 → 128): bokeh disc radius in pixels at the depth extremes. The single biggest look knob.
  • bokeh_shape: circle, hexagon, octagon, or custom (which reads the grayscale shape_texture input).
  • num_layers (default 8, 2 → 32): depth slices interpolated between. Higher is smoother and slower.
  • highlight_threshold / highlight_boost: where a highlight starts blooming into a disc, and how hard.

Inputs are image (required in practice), depth, optional shape_texture and mask; outputs are image and mask.

Where the depth map comes from

Any depth estimator will do - depth-estimation.md has the field guide. The short version: Depth Anything v2 is the current default choice, and its maps are typically black-far/white-near, so check whether you need invert_depth. MiDaS gives you relative depth (which thing is closer than which) rather than metric distance, which is all this node needs. Anything grayscale works; the node only reads luma and resizes it to match the image.

Installing it

cd ComfyUI/custom_nodes
git clone https://github.com/MajoorWaldi/ComfyUI-Majoor-ImageOps

Restart ComfyUI and hard-refresh the browser (Ctrl+F5) - the live preview is frontend JS shipped with the pack, and a stale cache makes it look broken. Or install through ComfyUI Manager by searching ComfyUI-Majoor-ImageOps. The node needs ComfyUI 0.19.0 or newer (V3 schema). No models are downloaded by the pack; you supply the depth map from whatever depth node you already use.

Where people get burned

  • Nothing happens at all. Either depth isn't connected, or max_blur_radius is 0. Both cases short-circuit and return the source unchanged.
  • Everything is blurry. Your focus_range is too tight for the depth map's precision, or the map's near/far convention is inverted. Flip invert_depth and widen the range before you blame the node.
  • Banding in the falloff. Raise num_layers. It genuinely is a smoother/slower dial, and the node budgets memory by num_layers, so 32 on a long video batch is a good way to find your VRAM ceiling.
  • The preview doesn't match the render. Expected - see above. Judge it from a queued result with an ImageOps Preview node, not the live canvas.
Categoryimage/imageops

Inputs (14)

NameTypeDefaultDescription
bypassBOOLEANfalse—
focus_distanceFLOAT0.500–1Depth value (0=near, 1=far by default) that stays in focus.
focus_rangeFLOAT0.050–0.99Depth thickness around focus_distance that stays sharp before blur ramps up.
max_blur_radiusINT240–128Bokeh disc radius in pixels at the depth extremes.
num_layersINT82–32Depth slices blurred and interpolated between. Higher = smoother falloff, slower.
bokeh_shapeCOMBOcircle4 options: circle, hexagon, octagon, custom
highlight_thresholdFLOAT0.900–2Brightness above which pixels bloom into visible bokeh discs when blurred.
highlight_boostFLOAT1.50–8Strength of the bokeh highlight bloom.
invert_depthBOOLEANfalse—
invert_maskBOOLEANfalse—
imageoptIMAGE,VIDEOImages/Video input.
depthoptIMAGE,VIDEODepth map (grayscale). Its luma drives the per-pixel blur amount.
shape_textureoptIMAGE,VIDEOOptional grayscale bokeh aperture shape, used when bokeh_shape is custom.
maskoptMASK—

Outputs (2)

NameTypeDescription
imageIMAGE—
maskMASK—