a-ml commited on
Commit
0805bfe
·
verified ·
1 Parent(s): e0cc87f

Add video conversion script

Browse files
Files changed (1) hide show
  1. 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()