File size: 11,045 Bytes
406ac25 cec4d26 13d40ab cec4d26 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 | ---
title: Physically Based Portrait Mode Engine
emoji: π
colorFrom: gray
colorTo: blue
sdk: gradio
sdk_version: 6.19.0
python_version: '3.13'
app_file: app.py
pinned: false
short_description: An improved portrait mode renderer for pictures.
---
# Physically-Based Portrait Mode Engine
A Gradio demo that simulates **shallow depth-of-field** (portrait mode) on ordinary photos. Instead of applying a flat Gaussian blur to the background, it estimates scene depth, builds a **circle-of-confusion (CoC)** map, and uses a trained neural renderer (**RendererNet**) to produce physically motivated defocus. In-focus regions are preserved by blending the render back onto the original image.
The app is designed to run as a [Hugging Face Space](https://huggingface.co/docs/hub/spaces) and is self-contained: model definitions and inference code live in a single `app.py` file.
## What it does
Given an input photograph and camera parameters (f-stop, focal length), the pipeline:
1. **Estimates relative depth** with [Depth Anything V2](https://huggingface.co/depth-anything/Depth-Anything-V2-Base-hf) via the Hugging Face `transformers` library.
2. **Builds a pseudo CoC map** by measuring how far each pixel's depth deviates from the depth at the **image center** (the assumed focus point). Pixels closer to the focus depth get a CoC near zero; pixels farther away get larger CoC values.
3. **Renders defocus with RendererNet**, a U-Net that takes the RGB image plus normalized f-stop, focal length, and CoC channels and outputs a blurred RGB image at the chosen aperture settings.
4. **Blends the NN render onto the original** using a smooth CoC-based weight so that in-focus areas (CoC below a threshold) remain untouched.
5. **Produces a non-NN baseline** for comparison: a flat Gaussian blur applied only where CoC exceeds a separate threshold.
Three outputs are shown:
| Output | Description |
|--------|-------------|
| **Rendered (NN)** | RendererNet defocus, blended with the original in in-focus regions |
| **Gaussian baseline** | Simple background blur for comparison against the learned renderer |
| **Pseudo CoC map** | Colorized circle-of-confusion visualization (inferno colormap) |
## Pipeline overview
```
Input image
β
βΌ
Resize (longest side β€ 1024 px)
β
βΌ
Depth Anything V2 βββΊ relative depth [0, 1]
β
βΌ
Pseudo CoC map (focus = image center, max 4 px)
β
ββββββββββββββββββββββββββββββββ
βΌ βΌ
RendererNet (512Γ512) Gaussian blur baseline
f-stop + focal length + CoC (CoC > threshold)
β β
βΌ β
CoC-weighted blend βββββββββββββββββ
β
βΌ
Final outputs (NN render, baseline, CoC map)
```
### Pseudo CoC
Because the demo has no interactive focus point, focus is fixed at the **center of the image** (`h // 2`, `w // 2`). The CoC at each pixel is proportional to the absolute difference between that pixel's relative depth and the depth at the focus point, scaled to a maximum of **4 px** (`COC_MAX_PX`).
### RendererNet input
RendererNet is a U-Net with **6 input channels** and **3 output channels** (RGB):
| Channel(s) | Content |
|------------|---------|
| 0β2 | RGB image, resized to 512Γ512 |
| 3 | f-stop map, normalized by `F_STOP_MAX` (22.0) |
| 4 | Focal length map, normalized by `FOCAL_LENGTH_MM_MAX` (200.0) |
| 5 | CoC map, clipped to [0, 25] px and normalized by `COC_PX_NORM` (25.0) |
The network output is resized back to the working resolution and blended with the original using a smoothstep weight derived from the CoC map and the **in-focus CoC threshold** slider.
### Blending
The blend weight uses a smoothstep on CoC values above the focus threshold, so transitions between sharp and blurred regions are gradual rather than hard-edged. Where CoC is near zero, the original pixels are kept; where CoC is large, the RendererNet output dominates.
## Models
### Depth Anything V2
- **Default checkpoint:** `depth-anything/Depth-Anything-V2-Base-hf`
- Loaded at startup via `AutoImageProcessor` and `AutoModelForDepthEstimation` from `transformers`.
- Returns relative (not metric) depth, normalized per image to [0, 1].
- Requires **torch** and **torchvision**.
Swap to a different variant via the `DEPTH_MODEL_ID` environment variable:
| Model ID | Trade-off |
|----------|-----------|
| `depth-anything/Depth-Anything-V2-Small-hf` | Faster, lower quality β good for CPU Spaces |
| `depth-anything/Depth-Anything-V2-Base-hf` | Default; balanced speed and quality |
| `depth-anything/Depth-Anything-V2-Large-hf` | Best quality, slowest |
### RendererNet
- A U-Net architecture (inlined in `app.py`) trained to render defocus given RGB + camera parameters + CoC.
- Weights are loaded from **`renderer/best_renderer.pth`** by default.
- Can alternatively be fetched from the Hugging Face Hub when `RENDERER_REPO_ID` is set.
## Using the demo
1. Upload an image (JPEG or PNG).
2. Adjust **f-stop** (0.95β22.0) β lower values produce stronger background blur.
3. Adjust **focal length** (4β200 mm) β longer focal lengths increase the shallow-DoF effect.
4. Tune **in-focus CoC threshold** β pixels with CoC below this value suppress the NN render and keep the original sharp.
5. Optionally expand **Gaussian baseline** to configure the comparison blur (CoC threshold and sigma).
6. Click **Render**.
If a `cache/` directory with example images exists, sample images appear in the Examples panel.
## Configuration
All settings below can be overridden with environment variables (useful for Hugging Face Space **Settings β Repository secrets / Variables** without editing code).
| Variable | Default | Description |
|----------|---------|-------------|
| `DEPTH_MODEL_ID` | `depth-anything/Depth-Anything-V2-Base-hf` | Hugging Face model ID for depth estimation |
| `RENDERER_LOCAL_PATH` | `renderer/best_renderer.pth` | Path to RendererNet weights on disk |
| `RENDERER_REPO_ID` | *(empty)* | Hugging Face repo to download weights from |
| `RENDERER_FILENAME` | `best_renderer.pth` | Filename within the Hub repo |
| `HF_TOKEN` | *(empty)* | Token for private Hub repos (only needed with `RENDERER_REPO_ID`) |
### Internal constants
These are fixed in code and must match RendererNet's training setup:
| Constant | Value | Purpose |
|----------|-------|---------|
| `F_STOP_MAX` | 22.0 | f-stop normalization divisor |
| `FOCAL_LENGTH_MM_MAX` | 200.0 | Focal length normalization divisor |
| `COC_PX_NORM` | 25.0 | CoC channel clip and normalize |
| `TARGET_SIZE` | 512 | RendererNet spatial resolution |
| `COC_MAX_PX` | 4.0 | Maximum pseudo CoC magnitude |
| `MAX_SIDE` | 1024 | Longest image side before inference |
## Local development
### Requirements
- Python 3.10+ (3.13 on Hugging Face Spaces)
- CUDA optional (falls back to CPU)
### Install and run
```bash
git clone https://github.com/<your-username>/Physically-Based-Portrait-Mode-Engine.git
cd Physically-Based-Portrait-Mode-Engine
pip install -r requirements.txt
# Ensure RendererNet weights are present
# (renderer/best_renderer.pth is included in the repo)
python app.py
```
Gradio will print a local URL (typically `http://127.0.0.1:7860`).
### Dependencies
```
gradio>=4.44.0
torch>=2.1.0
torchvision>=0.16.0
transformers>=4.45.0
huggingface_hub>=0.24.0
numpy>=1.26.0
pillow>=10.0.0
scikit-image>=0.22.0
matplotlib>=3.7.0
```
On first run, Depth Anything V2 weights are downloaded from the Hugging Face Hub (~370 MB for the Base model).
### Quantitative evaluation: edge artifacts
`quantitative-tests/benchmark_edge_artifacts.py` runs the same fixed f/1.2 pipeline as `prototype.py` on the four JPGs in `cache/` and compares **edge artifacts** against the original image for two compositing methods:
1. **Flat Gaussian background** β uniform blur pasted in wherever pseudo-CoC > 1 px (hard mask).
2. **CoC-weighted NN render** β RendererNet output blended back onto the original with a smoothstep weight (in-focus pixels stay untouched).
Metrics are computed in three regions where compositing artifacts show up most: **in-focus** pixels (CoC β€ 0.4), the **transition band** (0.4 < CoC β€ 1.0), and a **boundary ring** (Β±3 px around the Gaussian mask edge). Lower is better. Full per-image numbers are in `quantitative-tests/results/edge_artifact_benchmark.json`.
**Setup:** f/1.2, focal length 6.765 mm, Gaussian Ο = 12 px, images resized to max side 768 px, focus at image center.
| Metric (avg. across 4 images) | Gaussian vs original | NN render vs original | NN improvement |
|---|---:|---:|---:|
| Boundary ring mean abs diff | 0.066 | 0.007 | **9.3Γ** |
| Boundary ring mean grad excess | 0.044 | 0.005 | **8.2Γ** |
| Global mean grad excess | 0.025 | 0.009 | **2.8Γ** |
| In-focus mean grad excess | 0.0008 | 0.00009 | **8.9Γ** |
**Takeaways:**
- The hard Gaussian mask produces strong halos at the focus/defocus boundary β boundary-ring pixel error averages **~0.066** vs **~0.007** for the NN (roughly **6β21Γ** better per image).
- In-focus regions stay clean for both methods (mean abs diff β 0), but the NN leaks less spurious edge energy even there (**~9Γ** lower in-focus grad excess on average).
- The NNβs smoothstep blend removes most of the visible cut-line artifact; the flat Gaussian baseline is useful mainly as a simple comparison point, not as a production compositor.
Re-run the benchmark:
```bash
python quantitative-tests/benchmark_edge_artifacts.py
```
## Project structure
```
.
βββ app.py # Gradio app, RendererNet definition, full inference pipeline
βββ requirements.txt # Python dependencies
βββ renderer/
β βββ best_renderer.pth # Trained RendererNet weights
βββ cache/ # Optional example images for Gradio Examples
```
## Limitations
- **Fixed focus point:** Focus is always the image center; there is no click-to-focus or subject detection.
- **Relative depth only:** Depth Anything V2 outputs ordinal depth, not metric distances, so the CoC map is a heuristic rather than a physically exact optical simulation.
- **Resolution cap:** Images are downscaled so the longest side is at most 1024 px to keep CPU inference responsive.
- **RendererNet runs at 512Γ512:** Fine detail may be softened; output is upsampled to the working resolution.
- **CPU Spaces are slow:** First inference after startup can take tens of seconds; consider the Small depth model or a GPU Space for faster turnaround.
## Acknowledgments
- [Depth Anything V2](https://github.com/DepthAnything/Depth-Anything-V2) β monocular depth estimation ([Lihe Yang et al.](https://huggingface.co/depth-anything/Depth-Anything-V2-Base-hf))
- [Hugging Face Transformers](https://github.com/huggingface/transformers) β model loading and inference
- [Gradio](https://gradio.app/) β web UI
|