Nodes/ComfyUI-JH-PixelPro/Depth: Lens Blur
ComfyUI Node

Depth: Lens Blur

Fake f/1.4 in Post, With Bokeh Balls Instead of Smudge

By jetthuangai·Created 6 months ago·Updated a day ago· 6
Depth: Lens Blur
  • image
  • depth
  • focus_mask
  • image
  • coc
◄focus_depth0.50►
◄depth_of_field0.10►
◄max_blur24►
◄bokehdisc►
◄layers8►
◄blur_foregroundtrue►
◄keep_model_loadedtrue►

Phone "portrait mode" does this badly and everyone can tell: a Gaussian smear behind the subject with a jagged cut-out edge. Depth: Lens Blur (JHPixelProDepthLensBlur) does it properly, using the same model of a real lens - the circle of confusion - and a disc kernel that produces actual bokeh discs instead of fog.

This is the node the depth-estimation stack exists for. Everything else in that doc is about geometry or conditioning; this one is about simulating an f-stop.

How it works

Per pixel, coc = clamp((|depth − focus| − dof/2) / (1 − dof/2), 0, 1) × max_blur. That's your blur radius map: zero at the focus plane, growing with distance from it.

The node renders layers blur images at radii from 0 to max_blur and blends each pixel between the two nearest radii, so transitions stay smooth instead of banding into steps. Each layer is a normalised convolution - blur(image·w) / blur(w) - where the weight w shrinks for pixels sharper than that layer. That's the trick that stops a sharp foreground from smearing into a blurred background. Radii above 8 px are blurred on a downscaled copy and upsampled, so max_blur 96 doesn't try to allocate a 193-pixel kernel.

The blur map gets a small closing (window around max_blur / 8) so you don't get a thin sharp line where depth crosses the focus plane between two blurred zones. Wide in-focus areas are left alone.

The inputs, translated into photography

image, then:

focus_depth (0–1, default 0.5) - the depth that stays sharp, 1 = nearest. Ignored when focus_mask is connected.

depth_of_field (0–1, default 0.1) - the width of the sharp zone. Small = f/1.4, large = f/16. 0.05–0.15 keeps a whole face sharp rather than just the eyes, which is what you want on a portrait.

max_blur (1–96, default 24) - blur radius in pixels at the farthest point from focus. Scale it to the image: 24 is moderate at 2K, 60+ suits 6K.

bokeh (disc / gaussian) - disc for flat lens-like bokeh balls, gaussian for a soft fade. Use disc. The gaussian is there for when the discs look too literal on a busy background.

layers (2–16, default 8) - blur steps blended together. More = smoother, slower.

blur_foreground (default on) - off means only things behind the focus plane blur.

Optional inputs: depth (1 = nearest; if you don't connect it, the node estimates internally with Depth Anything V2 small at 518 px and you need onnxruntime), focus_mask (a subject mask - the median depth inside it becomes the focus depth, so a Face Detect Lite face_mask is the obvious feed), and keep_model_loaded for the internal depth model.

Outputs: image, and coc - the blur-radius map as a mask. That second output is underrated: preview it to see exactly where the blur is landing.

Install

cd ComfyUI/custom_nodes
git clone https://github.com/jetthuangai/ComfyUI-JH-PixelPro.git
cd ComfyUI-JH-PixelPro
pip install -r requirements.txt   # the Python that runs ComfyUI
pip install onnxruntime-gpu       # only needed if you don't feed a depth map

ComfyUI Manager: search ComfyUI-JH-PixelPro → Install → restart → Add Node → ComfyUI-JH-PixelPro → depth. Note onnxruntime is not in requirements.txt (it's commented out there), so a connected depth input is the way to avoid the extra install entirely - Depth Anything V2 in the same pack will hand you one. Speed is the good news: about 35 ms for 2048² on CUDA, around 2 s on CPU at max_blur 24.

Where it goes wrong

  • Blur edges don't match the subject. That's the depth map, not the blur. Check it first; soften its edges with Mask Edge Smoother.
  • Video flicker. The built-in depth estimate is normalised per frame, so the focus plane drifts. Feed a fixed focus_depth or a steady depth map for clips.
  • Stiff cut-out at the subject border. Raise depth_of_field slightly so the sharp zone is wider than you think it should be - real lenses don't have pixel-sharp focus boundaries.
  • Clean blurred areas, crunchy sharp areas. Blur destroys noise, so the two halves stop matching. Add a touch of grain after the blur. This is the same reason grain is the cheapest anti-AI-look move there is.
  • Known limit: a blurred foreground doesn't spread over an in-focus background - no occlusion-aware bleed. Fine for portraits, occasionally visible with railings and branches.

Pack note: Apache-2.0, JH (@jetthuangai), v1.10.0 / 64 nodes, EN + VI docs, and essentially no Reddit footprint to compare notes against. Judge it on one thing - whether the bokeh reads as a lens or as a filter - and the disc bokeh with a focus_mask from the face detector is the combination that gets you there.

CategoryComfyUI-JH-PixelPro/depth

Inputs (10)

NameTypeDefaultDescription
imageIMAGE—
focus_depthFLOAT0.500–1Depth value that stays sharp (1 = nearest). Ignored when focus_mask is connected.
depth_of_fieldFLOAT0.100–1Width of the sharp zone around the focus depth. Small = shallow depth of field (f/1.4), large = deep (f/16).
max_blurINT241–96Blur radius in pixels at the farthest distance from focus.
bokehCOMBOdiscdisc = flat lens-like bokeh balls, gaussian = soft blur.
layersINT82–16Number of blur steps blended together. More = smoother transitions, slower.
blur_foregroundBOOLEANtrueOff: only things behind the focus plane are blurred.
depthoptMASKDepth map, 1 = nearest. Not connected: estimated internally with Depth Anything V2 small (needs onnxruntime).
focus_maskoptMASKSubject mask (e.g. a face mask). The median depth inside it becomes the focus depth.
keep_model_loadedoptBOOLEANtrueON: keep the Depth Anything model in VRAM for the next run (faster). OFF: free it right after this run (ComfyUI's Free memory button cannot see it).

Outputs (2)

NameTypeDescription
imageIMAGE—
cocMASK—