Skip to content

Repository files navigation

lens-blur

Depth-aware DSLR lens bokeh for photos — real "portrait mode," with HDR — on your Mac.

lens-blur estimates per-pixel depth with Apple's ml-depth-pro, then renders a true camera-style background blur: an aperture-shaped bokeh kernel convolved in scene-linear light, with bright bokeh balls from out-of-focus highlights. Pick a lens (85mm portrait, anamorphic, soap-bubble, …), click to set focus, and export HDR — Ultra HDR JPEG, PQ HDR10 HEIC, or scene-linear TIFF.

Lens presets render highlights differently — round, heptagonal, oval, ring

The same out-of-focus highlights through four lens presets — bokeh-ball shape is the visible signature of a lens (subject stays sharp; synthetic test scene).

platform python depth HDR


✨ Highlights

  • Real lens bokeh, not a Gaussian smear — aperture-shaped kernel (disc / polygon / oval / ring), composited in linear light with occlusion-aware edges.
  • Lens presets — 85mm f/1.4, 50mm f/1.8, 35mm f/2, 135mm f/2, anamorphic, soap-bubble. Each defines a bokeh shape + character, not just a blur amount.
  • Click-to-focus — click any point to set the focal plane; everything nearer or farther blurs, with an in-focus depth slab so the subject stays sharp.
  • End-to-end HDR — recovers real headroom from gain-map (JPEG/HEIC) and 10-bit PQ/HLG sources, renders in scene-linear, and exports genuine HDR. Bright highlights bloom into glowing bokeh balls.
  • Drag-and-drop web UI with live, downscaled previews — plus a CLI and a bulk HEIC→HDR transcoder.

Requirements

  • macOS on Apple Silicon (uses the MPS GPU; falls back to CPU)
  • Homebrew Python 3.12 — brew install python@3.12
  • libultrahdr for HDR .jpg export — brew install libultrahdr
  • ~4 GB free disk (PyTorch + the 1.8 GB depth model)

Setup

git clone https://github.com/erango/lens-blur.git
cd lens-blur
./setup.sh        # venv + deps + clones ml-depth-pro + downloads model weights (~1.8 GB)

setup.sh doesn't commit the venv, the ml-depth-pro checkout, or the weights — they're git-ignored and recreated locally.

🖥️ Web UI

./blur-ui          # http://127.0.0.1:7860

Drag a photo onto the stage — on-device AI estimates depth, then the result reveals. Pick a lens from the visual bokeh-shape tiles, click the photo to focus, and grab the result from the export cards (the format matching your source is flagged Recommended). The everyday control (background blur) is up front; More controls expands falloff, sharpness, glow, HDR headroom, manual focus, and the depth-map view. Hover any ⓘ for help.

Control What it does
Lens Bokeh shape + character; sets sensible default blur
Click to focus Click the photo to focus there; near and far blur
Max blur Strongest blur radius at the farthest depth
Falloff How gradually blur ramps from the focal plane
Sharpness keep Bends the focus→blur curve
Bokeh glow Intensity of the bright bokeh balls (0 = off)
HDR headroom Highlight bloom for SDR inputs (disabled when real HDR is recovered)
Foreground hold Blurs the near side more gently than the background (1× = symmetric, like a real lens)
Focus width Depth of field — how thick the in-focus slab around the focal plane is
Show focus area Live red overlay of what's currently in focus
Refine Brush over spots the depth model got wrong: paint keep sharp or blur, with undo/redo

iPhone portrait-mode HEICs carry their own depth map — those load instantly and with cleaner subject edges, skipping the model entirely (a badge tells you when this happened).

The depth map is computed once per photo; the live preview is downscaled so sliders/lenses stay responsive (~½ s). Downloads render at full resolution: SDR JPEG, HDR .jpg, or HDR .heic.

Lens presets

Preset Aperture shape Character
85mm f/1.4 Portrait 9 blades, near-circular creamy, smooth bloom
50mm f/1.8 Nifty Fifty 7 blades → heptagonal neutral
35mm f/2 Street 8 blades, roundish subtle, keeps more in focus
135mm f/2 Tele 9 blades, circular big bokeh balls, strong bloom
Anamorphic oval (≈1.9:1) vertical-oval bokeh balls
Soap-bubble / Petzval ring-weighted bright-rimmed "nervous" bokeh

🌅 HDR

Bright bokeh balls need highlight intensity above white, which 8-bit SDR clips away. The renderer works in an unclamped scene-linear buffer; headroom is recovered from the source when present, else synthesized:

  • Gain map (Apple/ISO 21496-1) in a JPEG/MPO or HEIC — HDR = SDR · 2^(gain · log2(headroom))
  • 10-bit PQ / HLG HEIC — decoded via the ST 2084 / ARIB STD-B67 transfer functions
  • Synthetic — near-white pixels expanded into headroom for plain SDR inputs

For real-HDR inputs the in-focus subject keeps its headroom (genuinely HDR output), while the SDR base is composed separately at normal exposure — so the shareable JPEG matches the original and can't over-expose. Everything is color-managed to sRGB (P3 / BT.2020 converted on the way in).

Output formats

Format Description
SDR .jpg Tone-mapped (soft-knee) — looks right everywhere
HDR .jpg Ultra HDR gain-map JPEG — opens anywhere, HDR in Photos/iOS/Chrome. Best for sharing
HDR .heic 10-bit PQ / HDR10, tagged BT.2020 + ST 2084. Small
HDR .tif Scene-linear float32 — for editing in HDR-capable apps

⌨️ CLI

./blur-bg photo.jpg                          # fast Gaussian -> photo_blur.jpg
./blur-bg photo.jpg --lens portrait85        # DSLR bokeh
./blur-bg photo.heic --lens anamorphic --hdr-out          # + HDR .jpg sidecar
./blur-bg photo.jpg --lens tele135 --max-blur 50 --glow 1.5
Flag Default Meaning
--lens none portrait85, fifty18, street35, tele135, anamorphic, soapbubble (omit = fast Gaussian)
--max-blur lens / 30 max blur radius in px
--focus auto normalized focus depth 0 (near) .. 1 (far)
--falloff lens / 0.35 blur ramp width
--gamma 1.0 >1 keeps more sharp, <1 blurs more
--glow 1.0 bokeh-ball glow (0 = off)
--headroom 4.0 synthetic HDR headroom for SDR inputs
--hdr-out also write an HDR file
--hdr-format jpg jpg (Ultra HDR) · heic (PQ HDR10) · tif (scene-linear)
--save-depth also write <name>_depth.png

Accepts JPG, PNG, and HEIC. HEIC inputs keep their HDR.

🔁 Bulk HDR transcode

Convert many HEICs to Ultra HDR JPEGs — a pure re-mux (no blur, no model load, parallel):

./convert-hdr ~/Photos -o ~/out        # every .heic -> Ultra HDR .jpg
./convert-hdr a.heic b.heic -j 8 --overwrite

How it works

  1. ml-depth-pro produces a metric depth map from a single image.
  2. Depth → a per-pixel blur strength: distance from the focal plane (both sides), with a sharp in-focus slab and a feathered edge so the subject doesn't look cut out.
  3. Two-pass compositing in scene-linear light: an occlusion-aware gather for the background (sharp foreground never bleeds in) and a premultiplied-alpha scatter for the foreground (out-of-focus near objects spread over the subject). Locally-peaked highlights bloom into aperture-shaped bokeh balls.
  4. A soft-knee tone-map renders SDR; the HDR intent is kept for HDR export.

Files

blur_core.py depth, scene-linear HDR, and all rendering
server.py drag-and-drop Flask web UI (single file)
blur-bg · blur-bg.py CLI
convert-hdr · convert-hdr.py bulk HEIC → Ultra HDR JPEG
setup.sh one-shot environment setup

Limitations

  • Tuned for macOS / Apple Silicon.
  • Very fine edges (stray hairs) can show faint halos — inherent to depth-driven blur; lower the blur or glow to reduce.
  • Monocular depth is relative; unusual scenes may need a manual focus click.

License

MIT — see LICENSE. Fetched at setup (not included in this repo) and under their own licenses: Apple ml-depth-pro and Google libultrahdr.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages