Spaces:
Running on Zero
Running on Zero
File size: 6,343 Bytes
26b6d93 d70a36d 44b2388 d70a36d 26b6d93 d70a36d 44b2388 26b6d93 d70a36d 26b6d93 d70a36d 44b2388 d70a36d 26b6d93 | 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 | """Detail-injection relief pipeline (§5.2) -- the core value of DepthForge.
A raw monocular depth map only gives the smooth *form*. The carveable detail
(wrinkles, hair, fabric folds, stitching) comes from blending the photo's own
high-frequency luminance texture onto that form. This module ports the proven
Colab-spike math VERBATIM. Do not "improve" the constants or reorder operations.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Tuple
import numpy as np
from PIL import Image
from scipy.ndimage import gaussian_filter
from .config import RELIEF
@dataclass
class ReliefParams:
"""Parameters for :func:`build_heightmap`.
Mirrors the spike's ``params`` bag. ``invert`` orients the depth so near->high;
its correct default depends on the depth backend (DA3: True, V2: False).
"""
invert: bool = RELIEF.invert
detail_strength: float = RELIEF.detail_strength
detail_sigma: float = RELIEF.detail_sigma
fine_strength: float = RELIEF.fine_strength
contrast_gamma: float = RELIEF.contrast_gamma
mask_thresh: float = RELIEF.mask_thresh
bg_flatten: bool = RELIEF.bg_flatten
smooth: float = RELIEF.smooth
# Foreground depth expansion (the "Sculpt" fix): isolate the near subject
# and re-stretch depth *within* it so the subject's own form fills the
# relief, collapsing the far background into a shallow slab. Without this, a
# scene with a distant background crushes the subject to a flat plateau.
fg_expand: bool = True
fg_bg_level: float = 0.15 # background slab height (0..1); subject sits above
fg_local: float = 2.0 # depth-domain local-contrast (unsharp) on subject
def _otsu_threshold(x: np.ndarray) -> float:
"""Otsu threshold on values in ``[0, 1]`` (splits near subject from far bg)."""
hist, _ = np.histogram(x.ravel(), bins=256, range=(0.0, 1.0))
hist = hist.astype(np.float64)
total = hist.sum()
if total <= 0:
return 0.5
p = hist / total
centers = (np.arange(256) + 0.5) / 256.0
omega = np.cumsum(p)
mu = np.cumsum(p * centers)
mu_t = mu[-1]
denom = omega * (1.0 - omega)
denom[denom == 0] = 1e-12
sigma_b2 = (mu_t * omega - mu) ** 2 / denom
return float((np.argmax(sigma_b2) + 0.5) / 256.0)
def build_heightmap(
depth_raw: np.ndarray,
cleaned_rgb: Image.Image,
params: ReliefParams,
) -> Tuple[np.ndarray, np.ndarray]:
"""Blend detail texture onto the depth form to make a carveable relief.
Args:
depth_raw: Raw depth from a backend, float array ``[H, W]`` (any scale;
may contain NaN/inf). Resized to the cleaned image if needed.
cleaned_rgb: The cleaned source image (supplies high-frequency detail).
params: Relief parameters (see :class:`ReliefParams`).
Returns:
Tuple of ``(height_float, height_uint16)`` where ``height_float`` is in
``[0, 1]`` and ``height_uint16`` is the 16-bit heightmap for PNG export.
"""
invert = params.invert
detail_strength = params.detail_strength
detail_sigma = params.detail_sigma
fine_strength = params.fine_strength
contrast_gamma = params.contrast_gamma
mask_thresh = params.mask_thresh
bg_flatten = params.bg_flatten
smooth = params.smooth
fg_expand = getattr(params, "fg_expand", False)
fg_bg_level = getattr(params, "fg_bg_level", 0.15)
fg_local = getattr(params, "fg_local", 0.0)
# --- verbatim spike math -------------------------------------------------
rgb = np.asarray(cleaned_rgb.convert("RGB"), dtype=np.float32) / 255.0
H, W = rgb.shape[:2]
L = 0.299 * rgb[..., 0] + 0.587 * rgb[..., 1] + 0.114 * rgb[..., 2]
base = np.asarray(depth_raw, dtype=np.float32)
fin = base[np.isfinite(base)]
base = np.nan_to_num(
base,
nan=float(np.median(fin)),
posinf=float(fin.max()),
neginf=float(fin.min()),
)
if invert:
base = base.max() - base
if base.shape[:2] != (H, W):
base = np.asarray(
Image.fromarray(base, mode="F").resize((W, H), Image.BICUBIC),
np.float32,
)
# Robust global normalize to [0,1]. Wide percentiles keep the subject's own
# (small) depth range intact rather than clipping it flat.
blo, bhi = np.percentile(base, [1, 99])
d = np.clip((base - blo) / (bhi - blo + 1e-8), 0, 1)
if fg_expand:
# Split near subject (high depth) from far background via Otsu, then
# re-stretch depth *inside* the subject so its face/folds fill the
# relief. The far background is compressed into a shallow slab below.
t = _otsu_threshold(d)
fg = d >= t
frac = float(fg.mean())
if frac < 0.03 or frac > 0.9: # degenerate split -> percentile fallback
t = float(np.percentile(d, 70))
fg = d >= t
frac = float(fg.mean())
if frac >= 0.005:
flo, fhi = np.percentile(d[fg], [2, 98])
else:
flo, fhi = t, 1.0
subj = np.clip((d - flo) / (fhi - flo + 1e-8), 0, 1) # expanded subject
if fg_local > 0:
# Depth-domain unsharp: amplify the subject's OWN local relief
# (nose, eyes, folds) that global expansion leaves low-contrast.
sig = max(6.0, min(H, W) / 60.0)
subj = np.clip(subj + fg_local * (subj - gaussian_filter(subj, sigma=sig)), 0, 1)
soft = np.clip(gaussian_filter(fg.astype(np.float32), sigma=6), 0, 1)
bg = np.clip(d / (t + 1e-8), 0, 1) * fg_bg_level # shallow far slab
base = soft * (fg_bg_level + subj * (1.0 - fg_bg_level)) + (1.0 - soft) * bg
else:
base = d
mask = gaussian_filter((base > mask_thresh).astype(np.float32), sigma=4)
detail_coarse = L - gaussian_filter(L, sigma=detail_sigma)
detail_fine = L - gaussian_filter(L, sigma=max(1, detail_sigma / 3))
detail = (detail_strength * detail_coarse + fine_strength * detail_fine) * mask
h = base + detail
if bg_flatten:
h = h * mask
if smooth > 0:
h = gaussian_filter(h, sigma=smooth)
h = np.clip(h, 0, 1)
h = (h - h.min()) / (h.max() - h.min() + 1e-8)
h = np.power(h, contrast_gamma)
height16 = (np.clip(h, 0, 1) * 65535).astype(np.uint16)
return h, height16
|