〽️ Image Ops Defocus
Real bokeh from a depth map, with the aperture shape you asked for
- image
- depth
- shape_texture
- mask
- image
- mask
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) andfocus_range(default 0.05): the depth value that stays sharp, and how thick that band is. Smallfocus_range= razor-thin focal plane. Bumpinvert_depthif 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, orcustom(which reads the grayscaleshape_textureinput).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
depthisn't connected, ormax_blur_radiusis 0. Both cases short-circuit and return the source unchanged. - Everything is blurry. Your
focus_rangeis too tight for the depth map's precision, or the map's near/far convention is inverted. Flipinvert_depthand 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 bynum_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.
Inputs (14)
| Name | Type | Default | Description |
|---|---|---|---|
| bypass | BOOLEAN | false | — |
| focus_distance | FLOAT | 0.500–1 | Depth value (0=near, 1=far by default) that stays in focus. |
| focus_range | FLOAT | 0.050–0.99 | Depth thickness around focus_distance that stays sharp before blur ramps up. |
| max_blur_radius | INT | 240–128 | Bokeh disc radius in pixels at the depth extremes. |
| num_layers | INT | 82–32 | Depth slices blurred and interpolated between. Higher = smoother falloff, slower. |
| bokeh_shape | COMBO | circle | 4 options: circle, hexagon, octagon, custom |
| highlight_threshold | FLOAT | 0.900–2 | Brightness above which pixels bloom into visible bokeh discs when blurred. |
| highlight_boost | FLOAT | 1.50–8 | Strength of the bokeh highlight bloom. |
| invert_depth | BOOLEAN | false | — |
| invert_mask | BOOLEAN | false | — |
| imageopt | IMAGE,VIDEO | Images/Video input. | |
| depthopt | IMAGE,VIDEO | Depth map (grayscale). Its luma drives the per-pixel blur amount. | |
| shape_textureopt | IMAGE,VIDEO | Optional grayscale bokeh aperture shape, used when bokeh_shape is custom. | |
| maskopt | MASK | — |
Outputs (2)
| Name | Type | Description |
|---|---|---|
| image | IMAGE | — |
| mask | MASK | — |