fix(stereo_depth): negative disparity window + percentile normalize — movie stereo works

Movie SBS converges on the screen plane, so real content sits at NEGATIVE
disparity; minDisparity=0 clipped everything behind convergence to black.
Window is now ±num_disp/2 with 2-98 percentile normalization and a validity
mask. Verified on Big Buck Bunny (half-OU) + One Night In Hell (half-SBS):
structured depth + colored clouds that render in Blender.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-07-19 01:51:55 +10:00
parent 95708ad220
commit cf402e322c

View File

@ -50,8 +50,9 @@ subprocess.run([FFMPEG, "-y", "-loglevel", "error", "-i", src,
frames = sorted(glob.glob(os.path.join(frames_dir, "*.png")))
print(f"[stereo] {len(frames)} frames, num_disp={num_disp} block={block}", flush=True)
sgbm = cv2.StereoSGBM_create(
minDisparity=0, numDisparities=num_disp, blockSize=block,
min_disp = -(num_disp // 2) # movie stereo converges on the screen plane — content
sgbm = cv2.StereoSGBM_create( # lives BOTH sides of it, so the window must go negative
minDisparity=min_disp, numDisparities=num_disp, blockSize=block,
P1=8 * 3 * block * block, P2=32 * 3 * block * block,
disp12MaxDiff=1, uniquenessRatio=10, speckleWindowSize=100, speckleRange=2,
mode=cv2.STEREO_SGBM_MODE_SGBM_3WAY)
@ -66,11 +67,16 @@ for i, f in enumerate(frames):
L, R = img[:, : w // 2], img[:, w // 2:]
gl, gr = cv2.cvtColor(L, cv2.COLOR_BGR2GRAY), cv2.cvtColor(R, cv2.COLOR_BGR2GRAY)
disp = sgbm.compute(gl, gr).astype(np.float32) / 16.0
disp[disp < 0] = 0
norm = (disp / max(1e-6, disp.max()) * 255).astype(np.uint8)
valid = disp > (min_disp - 0.5) # cv2 marks unmatched pixels min_disp-1
if valid.sum() > 100:
lo, hi = np.percentile(disp[valid], [2, 98])
else:
lo, hi = 0.0, 1.0
norm = np.zeros(disp.shape, np.uint8)
norm[valid] = (np.clip((disp[valid] - lo) / max(1e-6, hi - lo), 0, 1) * 255).astype(np.uint8)
cv2.imwrite(os.path.join(depth_dir, os.path.basename(f)), norm)
if f == mid: # colored point cloud of the middle frame
ys, xs = np.where(disp > 1)
ys, xs = np.where(valid & (norm > 2))
step = max(1, len(ys) // 400_000) # cap cloud size
ys, xs = ys[::step], xs[::step]
z = disp[ys, xs]