Instructions to use a-ml/FaceDepth with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- DepthAnythingV2
How to use a-ml/FaceDepth with DepthAnythingV2:
# Install from https://github.com/DepthAnything/Depth-Anything-V2 # Load the model and infer depth from an image import cv2 import torch from depth_anything_v2.dpt import DepthAnythingV2 # instantiate the model model = DepthAnythingV2(encoder="<ENCODER>", features=<NUMBER_OF_FEATURES>, out_channels=<OUT_CHANNELS>) # load the weights filepath = hf_hub_download(repo_id="a-ml/FaceDepth", filename="depth_anything_v2_<ENCODER>.pth", repo_type="model") state_dict = torch.load(filepath, map_location="cpu") model.load_state_dict(state_dict).eval() raw_img = cv2.imread("your/image/path") depth = model.infer_image(raw_img) # HxW raw depth map in numpy - Notebooks
- Google Colab
- Kaggle
Add video conversion script
Browse files- video_depth.py +227 -0
video_depth.py
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
"""Turn any video into a FaceDepth depth-map video.
|
| 3 |
+
|
| 4 |
+
Decodes anything ffmpeg can read (mov, mp4, mkv, and the rest, up to 4K), runs every
|
| 5 |
+
frame through FaceDepth, and re-encodes. A progress bar reports frames per second and
|
| 6 |
+
ETA. Output is either the depth map alone or a side-by-side with the source on the left.
|
| 7 |
+
|
| 8 |
+
Frames stream through an ffmpeg pipe, so a long 4K clip never lands on disk as a frame
|
| 9 |
+
dump and memory stays flat.
|
| 10 |
+
|
| 11 |
+
Stability
|
| 12 |
+
---------
|
| 13 |
+
FaceDepth predicts each frame independently, which can make the colour mapping pulse
|
| 14 |
+
between frames. Two inference-time controls damp that:
|
| 15 |
+
|
| 16 |
+
--range-ema smooths the near/far normalisation range across frames. On by default.
|
| 17 |
+
Removes brightness pulsing with no ghosting. Leave it on.
|
| 18 |
+
--smooth-depth blends each depth frame with the previous one. Off by default.
|
| 19 |
+
Cuts residual per-pixel jitter, but ghosts behind fast motion.
|
| 20 |
+
0.3 is a reasonable starting point for handheld footage.
|
| 21 |
+
|
| 22 |
+
Setup
|
| 23 |
+
-----
|
| 24 |
+
pip install torch torchvision opencv-python numpy
|
| 25 |
+
git clone https://github.com/DepthAnything/Depth-Anything-V2 third_party/DepthAnythingV2
|
| 26 |
+
|
| 27 |
+
Download FaceDepth_step15792.pt from https://huggingface.co/a-ml/FaceDepth and pass it
|
| 28 |
+
with --ckpt. ffmpeg must be on PATH.
|
| 29 |
+
|
| 30 |
+
Examples
|
| 31 |
+
--------
|
| 32 |
+
python video_depth.py --input clip.mov --output depth.mp4
|
| 33 |
+
python video_depth.py --input clip.mkv --output sbs.mp4 --side-by-side --colormap turbo
|
| 34 |
+
python video_depth.py --input 4k.mp4 --output out.mp4 --smooth-depth 0.3 --bf16
|
| 35 |
+
|
| 36 |
+
Runs on Apple silicon (mps), CUDA, or CPU, picked automatically.
|
| 37 |
+
"""
|
| 38 |
+
import argparse, json, subprocess, sys, time
|
| 39 |
+
from pathlib import Path
|
| 40 |
+
|
| 41 |
+
import cv2
|
| 42 |
+
import numpy as np
|
| 43 |
+
import torch
|
| 44 |
+
|
| 45 |
+
ROOT = Path(__file__).resolve().parent
|
| 46 |
+
for cand in (ROOT / "third_party" / "DepthAnythingV2", ROOT.parent / "third_party" / "DepthAnythingV2"):
|
| 47 |
+
if cand.exists():
|
| 48 |
+
sys.path.insert(0, str(cand))
|
| 49 |
+
break
|
| 50 |
+
try:
|
| 51 |
+
from depth_anything_v2.dpt import DepthAnythingV2
|
| 52 |
+
except ImportError:
|
| 53 |
+
sys.exit("Could not import depth_anything_v2. Clone Depth-Anything-V2 into "
|
| 54 |
+
"third_party/DepthAnythingV2 (see the setup notes at the top of this file).")
|
| 55 |
+
|
| 56 |
+
MEAN = torch.tensor([0.485, 0.456, 0.406]).view(1, 3, 1, 1)
|
| 57 |
+
STD = torch.tensor([0.229, 0.224, 0.225]).view(1, 3, 1, 1)
|
| 58 |
+
COLORMAPS = {"inferno": cv2.COLORMAP_INFERNO, "magma": cv2.COLORMAP_MAGMA,
|
| 59 |
+
"turbo": cv2.COLORMAP_TURBO, "viridis": cv2.COLORMAP_VIRIDIS,
|
| 60 |
+
"plasma": cv2.COLORMAP_PLASMA, "bone": cv2.COLORMAP_BONE, "gray": None}
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def pick_device():
|
| 64 |
+
if torch.backends.mps.is_available():
|
| 65 |
+
return "mps"
|
| 66 |
+
if torch.cuda.is_available():
|
| 67 |
+
return "cuda"
|
| 68 |
+
return "cpu"
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def probe(path):
|
| 72 |
+
out = subprocess.run(
|
| 73 |
+
["ffprobe", "-v", "error", "-select_streams", "v:0",
|
| 74 |
+
"-show_entries", "stream=width,height,r_frame_rate,nb_frames,duration",
|
| 75 |
+
"-of", "json", path], capture_output=True, text=True, check=True).stdout
|
| 76 |
+
s = json.loads(out)["streams"][0]
|
| 77 |
+
w, h = int(s["width"]), int(s["height"])
|
| 78 |
+
num, den = s["r_frame_rate"].split("/")
|
| 79 |
+
fps = float(num) / float(den)
|
| 80 |
+
n = int(s["nb_frames"]) if s.get("nb_frames", "N/A") not in ("N/A", None) else \
|
| 81 |
+
int(round(float(s.get("duration", 0)) * fps)) or 0
|
| 82 |
+
return w, h, fps, n
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def read_exact(pipe, n):
|
| 86 |
+
buf = b""
|
| 87 |
+
while len(buf) < n:
|
| 88 |
+
chunk = pipe.read(n - len(buf))
|
| 89 |
+
if not chunk:
|
| 90 |
+
return None
|
| 91 |
+
buf += chunk
|
| 92 |
+
return buf
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def fit14(w, h, res):
|
| 96 |
+
"""Scale so the longer side is about `res`, with both sides multiples of 14."""
|
| 97 |
+
scale = res / max(w, h)
|
| 98 |
+
return (max(14, int(round(w * scale / 14)) * 14),
|
| 99 |
+
max(14, int(round(h * scale / 14)) * 14))
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def bar(i, n, t0):
|
| 103 |
+
frac = i / n if n else 0
|
| 104 |
+
filled = int(40 * frac)
|
| 105 |
+
el = time.time() - t0
|
| 106 |
+
fps = i / max(el, 1e-6)
|
| 107 |
+
eta = (n - i) / max(fps, 1e-6) if n else 0
|
| 108 |
+
print(f"\r[{'#' * filled}{'-' * (40 - filled)}] {i}/{n} {100 * frac:5.1f}% "
|
| 109 |
+
f"{fps:4.1f} fps ETA {eta:5.0f}s", end="", flush=True)
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def main():
|
| 113 |
+
ap = argparse.ArgumentParser()
|
| 114 |
+
ap.add_argument("--input", required=True)
|
| 115 |
+
ap.add_argument("--output", required=True)
|
| 116 |
+
ap.add_argument("--ckpt", default="FaceDepth_step15792.pt")
|
| 117 |
+
ap.add_argument("--res", type=int, default=910,
|
| 118 |
+
help="model longer-side resolution, rounded to a multiple of 14. "
|
| 119 |
+
"Higher is sharper and slower.")
|
| 120 |
+
ap.add_argument("--side-by-side", action="store_true", help="source left, depth right")
|
| 121 |
+
ap.add_argument("--colormap", choices=list(COLORMAPS), default="inferno")
|
| 122 |
+
ap.add_argument("--invert", action="store_true", help="flip so near reads dark")
|
| 123 |
+
ap.add_argument("--near-pct", type=float, default=2.0)
|
| 124 |
+
ap.add_argument("--far-pct", type=float, default=98.0)
|
| 125 |
+
ap.add_argument("--range-ema", type=float, default=0.85,
|
| 126 |
+
help="temporal smoothing of the near/far range, 0 disables")
|
| 127 |
+
ap.add_argument("--smooth-depth", type=float, default=0.0,
|
| 128 |
+
help="temporal smoothing of depth itself, 0 disables, ghosts on motion")
|
| 129 |
+
ap.add_argument("--bf16", action="store_true", help="roughly 2x faster, negligible quality cost")
|
| 130 |
+
ap.add_argument("--keep-audio", action="store_true", default=True)
|
| 131 |
+
ap.add_argument("--no-audio", dest="keep_audio", action="store_false")
|
| 132 |
+
ap.add_argument("--crf", type=int, default=16, help="x264 quality, lower is better")
|
| 133 |
+
args = ap.parse_args()
|
| 134 |
+
|
| 135 |
+
inp = str(Path(args.input).expanduser())
|
| 136 |
+
W, H, fps, N = probe(inp)
|
| 137 |
+
mw, mh = fit14(W, H, args.res)
|
| 138 |
+
outW, outH = (W * 2, H) if args.side_by_side else (W, H)
|
| 139 |
+
print(f"input {W}x{H} @ {fps:.3f}fps, {N or '?'} frames -> model {mw}x{mh} -> "
|
| 140 |
+
f"output {outW}x{outH} ({'side-by-side' if args.side_by_side else 'depth'})", flush=True)
|
| 141 |
+
|
| 142 |
+
dev = pick_device()
|
| 143 |
+
m = DepthAnythingV2(encoder="vitl", features=256, out_channels=[256, 512, 1024, 1024])
|
| 144 |
+
ck = torch.load(args.ckpt, map_location="cpu", weights_only=True)
|
| 145 |
+
m.load_state_dict(ck.get("ema_model") or ck.get("model") or ck)
|
| 146 |
+
m = m.to(dev).eval()
|
| 147 |
+
mean, std = MEAN.to(dev), STD.to(dev)
|
| 148 |
+
print(f"loaded {Path(args.ckpt).name} on {dev}", flush=True)
|
| 149 |
+
|
| 150 |
+
@torch.no_grad()
|
| 151 |
+
def infer(rgb):
|
| 152 |
+
x = torch.from_numpy(rgb).permute(2, 0, 1).unsqueeze(0).to(dev)
|
| 153 |
+
if args.bf16 and dev != "cpu":
|
| 154 |
+
with torch.autocast(dev, dtype=torch.bfloat16):
|
| 155 |
+
d = m((x - mean) / std)
|
| 156 |
+
else:
|
| 157 |
+
d = m((x - mean) / std)
|
| 158 |
+
return d.float().cpu().numpy()[0]
|
| 159 |
+
|
| 160 |
+
dec = subprocess.Popen(["ffmpeg", "-v", "error", "-i", inp, "-f", "rawvideo",
|
| 161 |
+
"-pix_fmt", "bgr24", "-"], stdout=subprocess.PIPE, bufsize=10 ** 8)
|
| 162 |
+
enc_cmd = ["ffmpeg", "-y", "-v", "error", "-f", "rawvideo", "-pix_fmt", "bgr24",
|
| 163 |
+
"-s", f"{outW}x{outH}", "-r", f"{fps}", "-i", "-"]
|
| 164 |
+
if args.keep_audio:
|
| 165 |
+
enc_cmd += ["-i", inp, "-map", "0:v:0", "-map", "1:a:0?", "-c:a", "aac", "-shortest"]
|
| 166 |
+
enc_cmd += ["-c:v", "libx264", "-crf", str(args.crf), "-pix_fmt", "yuv420p", args.output]
|
| 167 |
+
enc = subprocess.Popen(enc_cmd, stdin=subprocess.PIPE, bufsize=10 ** 8)
|
| 168 |
+
|
| 169 |
+
frame_bytes = W * H * 3
|
| 170 |
+
ema_lo = ema_hi = ema_depth = None
|
| 171 |
+
t0 = time.time()
|
| 172 |
+
i = 0
|
| 173 |
+
try:
|
| 174 |
+
while True:
|
| 175 |
+
raw = read_exact(dec.stdout, frame_bytes)
|
| 176 |
+
if raw is None:
|
| 177 |
+
break
|
| 178 |
+
frame = np.frombuffer(raw, np.uint8).reshape(H, W, 3)
|
| 179 |
+
small = cv2.resize(frame, (mw, mh), interpolation=cv2.INTER_AREA)
|
| 180 |
+
rgb = cv2.cvtColor(small, cv2.COLOR_BGR2RGB).astype(np.float32) / 255.0
|
| 181 |
+
disp = infer(rgb) # inverse depth, larger is nearer
|
| 182 |
+
|
| 183 |
+
if args.smooth_depth > 0:
|
| 184 |
+
ema_depth = disp if ema_depth is None else \
|
| 185 |
+
(1 - args.smooth_depth) * disp + args.smooth_depth * ema_depth
|
| 186 |
+
disp = ema_depth
|
| 187 |
+
|
| 188 |
+
lo = np.percentile(disp, args.near_pct)
|
| 189 |
+
hi = np.percentile(disp, args.far_pct)
|
| 190 |
+
a = args.range_ema
|
| 191 |
+
if a > 0:
|
| 192 |
+
if ema_lo is None:
|
| 193 |
+
ema_lo, ema_hi = lo, hi
|
| 194 |
+
else:
|
| 195 |
+
ema_lo = (1 - a) * lo + a * ema_lo
|
| 196 |
+
ema_hi = (1 - a) * hi + a * ema_hi
|
| 197 |
+
lo, hi = ema_lo, ema_hi
|
| 198 |
+
|
| 199 |
+
norm = np.clip((disp - lo) / (hi - lo + 1e-6), 0, 1)
|
| 200 |
+
if args.invert:
|
| 201 |
+
norm = 1 - norm
|
| 202 |
+
gray = (norm * 255).astype(np.uint8)
|
| 203 |
+
cmap = COLORMAPS[args.colormap]
|
| 204 |
+
dvis = cv2.applyColorMap(gray, cmap) if cmap is not None else \
|
| 205 |
+
cv2.cvtColor(gray, cv2.COLOR_GRAY2BGR)
|
| 206 |
+
dvis = cv2.resize(dvis, (W, H), interpolation=cv2.INTER_CUBIC)
|
| 207 |
+
|
| 208 |
+
out = np.hstack([frame, dvis]) if args.side_by_side else dvis
|
| 209 |
+
enc.stdin.write(out.tobytes())
|
| 210 |
+
|
| 211 |
+
i += 1
|
| 212 |
+
if i % 5 == 0 or i == N:
|
| 213 |
+
bar(i, N, t0)
|
| 214 |
+
finally:
|
| 215 |
+
print()
|
| 216 |
+
if dec.stdout:
|
| 217 |
+
dec.stdout.close()
|
| 218 |
+
dec.wait()
|
| 219 |
+
if enc.stdin:
|
| 220 |
+
enc.stdin.close()
|
| 221 |
+
enc.wait()
|
| 222 |
+
print(f"done: {i} frames -> {args.output} "
|
| 223 |
+
f"({i / max(time.time() - t0, 1e-6):.1f} fps avg)", flush=True)
|
| 224 |
+
|
| 225 |
+
|
| 226 |
+
if __name__ == "__main__":
|
| 227 |
+
main()
|