From cf402e322cada5d80a61779f3f781ef5c1059067 Mon Sep 17 00:00:00 2001 From: type-two Date: Sun, 19 Jul 2026 01:51:55 +1000 Subject: [PATCH] =?UTF-8?q?fix(stereo=5Fdepth):=20negative=20disparity=20w?= =?UTF-8?q?indow=20+=20percentile=20normalize=20=E2=80=94=20movie=20stereo?= =?UTF-8?q?=20works?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- server/operators/stereo_depth/run.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/server/operators/stereo_depth/run.py b/server/operators/stereo_depth/run.py index 2503665..a461d29 100644 --- a/server/operators/stereo_depth/run.py +++ b/server/operators/stereo_depth/run.py @@ -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]