Skip to content

feat(stereo-camera): R2 — service-side rectifier (u_stereo_rectify), distorted fake, probe --rectified - #1747

Draft
dfattal wants to merge 15 commits into
feat/stereo-camera-r1from
feat/stereo-camera-r2
Draft

dfattal wants to merge 15 commits into
feat/stereo-camera-r1from
feat/stereo-camera-r2

Conversation

@dfattal

@dfattal dfattal commented Sep 26, 2026 •

Copy link
Copy Markdown
Collaborator

This PR is phase R2 of the stereo camera source (ADR-043, spec, roadmap). The service now rectifies any calibrated camera itself, in vendor-neutral C with no OpenCV dependency. Clients get RECTIFIED frames and rectified calibration. RAW frames are still refused to the browser.

Stacked on #1746 (feat/stereo-camera-r1); merge #1746 first. Rebased 2026-10-03 onto the post-lift R1 (lift #1781 is merged, v2.22.0): order is now R1 → R2 → L1 → B1; the ABI positions (camera slots after create_dp_d3d11_lift + get_platform_state, type values 1004999300–310) are in #1746's rebase comment. The Windows tester's f433d31fc (PRESENT_OWNER owner quota) survives as 0191f5481. The first real source is the Leia provider (L1): DisplayXR/displayxr-leia-plugin#273, which carries the on-hardware recipe.

What's in it

  • auxiliary/util/u_stereo_rectify.{h,c} has three layers, so a GPU path can replace only the last one:
    1. Geometry. u_stereo_rectify_compute re-implements OpenCV's stereoRectify(CALIB_ZERO_DISPARITY, alpha = 0). Each camera is rotated by half the relative rotation, the baseline is put on the x axis, focal = min fy, the principal point is the corner centroid, and the image is zoomed to the valid region. The zoom is then checked on every border pixel of the real maps and increased until no output pixel samples outside its raw image. No convergence shear is applied.
    2. Float maps. u_stereo_rectify_build_map is what a GPU backend uploads as an RG32F texture.
    3. CPU backend. A fixed-point bilinear LUT over the whole SBS image (6 bytes per sample). GRAY8 and BGRA8 use one LUT; NV12 uses a full-res Y LUT plus a half-res UV LUT.
    • Supported lens models: RADTAN5, RADTAN8 and KB4. Calibration taken at a different size than the frames is rescaled.
  • Service (ipc_server_stereo_camera.c):
    • A struct scam_rectifier per camera is built at create time from the plug-in's RAW calibration, whenever the camera is CALIBRATED and not NATIVELY_RECTIFIED. The WARN line reports f, principal point, baseline, zoom and build time.
    • It runs once per source frame, on the camera thread with the lock released, and only while a started stream wants RECTIFIED. RAW streams read the original frame. A RECTIFIED stream never receives unrectified pixels.
    • get_calibration(RECTIFIED) returns the same geometry: one pinhole for both eyes, model NONE, and rightFromLeft = identity plus (+B, 0, 0). horizontalFovDeg is the rectified value.
    • The browser (PRESENT_OWNER) is refused (INPUT_UNSUPPORTED) a camera the rectifier rejected. It no longer gets the RAW-flagged fallback that native clients get.
    • The INFO stats line adds rectify ms/frame.
    • It WARNs when a plug-in's calibration puts the right camera at −x (T.x > 0, i.e. the SBS halves look swapped). The pair still rectifies, but every disparity would come out negative.
  • Fake: SIM_DISPLAY_FAKE_STEREO_CAMERA_DISTORT=1 produces a raw pair with exact ground truth:
    • different per-eye intrinsics and RADTAN5 barrel lenses;
    • 0.7° relative pitch and 0.9° relative roll.
    • The rotation is split symmetrically (left Sᵀ, right S), so Bouguet recovers exactly the virtual parallel pair. After rectification, disparity is exactly f·B/Z.
  • CLI:
    • camera calib <id> --rectified prints P1/P2 and Z = f·B/d.
    • Every probe now reports row alignment: 2-D block matching over dx and dy on textured blocks, giving |dy| median / p90 / blocks > 1 px, and the dominant disparities converted to depths.
    • probe --rectified exits 5 if the frames come back RAW or the median |dy| is above 0.5 px.
  • Docs: the spec §4/§10/§11/§13, the roadmap ("R2 as built", phase table), the ADR status and the iface reference are updated.

Test evidence (macOS, M1 Pro)

tests_stereo_rectify (716 assertions) uses golden fixtures from tests/fixtures/gen_stereo_rectify_fixtures.py (OpenCV 4.10, run offline, header committed). There are 3 calibrations: the sim fake, an SR-like pair with a non-axial T, and a RADTAN8 pair calibrated at 2× the frame size.

Check Result
R1/R2 vs OpenCV equal to 1e-12
Principal point vs converged reference within 1e-4 px
Principal point vs OpenCV's own P1 within 0.75 px. OpenCV stops undistortPoints after 5 iterations; we iterate to convergence
Focal after zoom +0.05 to +0.13 % vs OpenCV (our border check only ever zooms further; OpenCV leaves 8 black pixels on the RADTAN8 case)
Maps vs initUndistortRectifyMap ≤ 3.0e-5 px
Black output pixels 0, at full and half resolution

End to end on the distorted fake:

Result
RAW pair median
RECTIFIED, worst block of 689 **
Background disparity ground truth 11.330 px, measured median 11.305, worst block error 0.059 px
Bar disparity ground truth 37.765 px, measured median 37.785, worst block error 0.063 px

NV12 and BGRA8 planes are bit-identical to GRAY8 where comparable. tests_stereo_camera (R1) and tests_ipc_proto still pass.

Live service + CLI (Debug service, SIM_DISPLAY_FAKE_STEREO_CAMERA=1 _DISTORT=1 DXR_STEREO_CAMERA_DEV_ALLOW=1):

stereo camera 1: rectifier ready (CPU) — f 453.18 px, principal point (320.43, 239.36), baseline 50.00 mm, valid-region zoom x1.1109, 614400/614400 valid taps, built in 38.1 ms
$ displayxr-cli camera calib 1 --rectified
  P1 = [453.181 0 320.426 0; 0 453.181 239.364 0; 0 0 1 0]
  P2 = [453.181 0 320.426 -22659.030; 0 453.181 239.364 0; 0 0 1 0]   (P2[0][3] = f * Tx, Tx = -50.000 mm)
$ displayxr-cli camera probe --rectified --frames 150
measured delivery rate: 29.99 Hz over 4.97 s
row alignment (RECTIFIED, 221 textured 32x32 blocks): |dy| median 0.016 px, p90 0.064 px, 9 block(s) > 1 px
  disparity mode 1: 11.30 px (165 blocks) -> Z = f*B/d = 2.004 m
  disparity mode 2: 37.81 px (48 blocks) -> Z = f*B/d = 0.599 m
--rectified: PASS — rows aligned
$ displayxr-cli camera probe --raw --format gray8
row alignment (RAW, 221 textured 32x32 blocks): |dy| median 2.589 px, p90 5.292 px, 183 block(s) > 1 px
  • The 9 outlier blocks sit on the bar's occlusion edges and on the frame counter, which is burned in raw space.
  • A RAW gray8 stream and a RECTIFIED bgra8 stream capped at 15 Hz ran concurrently from one source: 29.97 Hz and 15.00 Hz.
  • The undistorted (NATIVELY_RECTIFIED) fake is unchanged: no rectifier is built, disparities are 12.00 / 40.00 px, and |dy| median is 0.004 px.

MinGW-w64: build-mingw-check.sh aux_util drv_sim_display passes, with no warnings from the new files. It caught a -Wpedantic array-qualifier issue, which is fixed.

Performance (CPU budget)

Measured single-threaded on an M1 Pro at -O2, per 1280×480 frame:

Format Time
GRAY8 0.94 ms
NV12 1.36 ms
BGRA8 2.25 ms
  • A Debug service measures 3.0 ms for GRAY8.
  • For the Leia GRAY8 source at 30 Hz, that is about 3 % of one core.
  • Budget: ≤ 2 ms per frame on the camera thread. Past that, split the rows across threads with u_stereo_rectify_lut_apply_rows, or move to the GPU backend.
  • One-time setup (geometry + both LUTs) takes 9–18 ms. LUT memory is 4.6 MB per camera.
  • With the distorted fake, the service stats show about 28 ms mean latency. Most of that is the fake re-rendering its warped scene in Debug, not the rectifier.

Deviations from the design

  • The alpha = 0 zoom is stricter than OpenCV's (it is verified per border pixel), which costs at most +0.13 % focal on the fixtures.
  • The principal point uses a converged undistortPoints (OpenCV stops at 5 iterations).
  • A PRESENT_OWNER request for RECTIFIED on a camera that cannot be rectified is now refused. Before, it returned RAW-flagged frames; this tightens the "no raw frames to pages" decision.

Not tested

  • Windows MSVC, Linux and Android builds and runs (CI covers the builds).
  • Real hardware (that is L1's recipe).
  • The GPU backend (only the seam exists).
  • Runtime recalibration: calibrationGeneration stays 0, and the rectifier is built once per service lifetime.
  • KB4 has round-trip tests only, with no OpenCV golden, because OpenCV's plain stereoRectify does not model fisheye lenses.

🤖 Generated with Claude Code

@dfattal
dfattal force-pushed the feat/stereo-camera-r2 branch from f1a7e66 to 0fb5ca9 Compare September 26, 2026 18:16
@dfattal
dfattal force-pushed the feat/stereo-camera-r1 branch from 15167e5 to b9925bc Compare September 27, 2026 01:47
@dfattal
dfattal force-pushed the feat/stereo-camera-r2 branch 2 times, most recently from 8fce4c3 to 9c23856 Compare September 27, 2026 18:11
@dfattal

dfattal commented Sep 27, 2026

Copy link
Copy Markdown
Collaborator Author

Follow-ups from the first SR-hardware run. I rebased onto the updated R1 and force-pushed with lease; 8fce4c3c3 (M_PI) is kept.

  • Probe verdict (9c2385671): row alignment now uses u_stereo_camera_match_block, a zero-mean NCC matcher with coarse-to-fine search and parabolic sub-pixel refinement. It returns its search window and flags a peak that lands on either bound. The probe uses:
    • a baseline-aware disparity window and dy ±24 px;
    • an NCC ≥ 0.90 acceptance, with low-NCC and at-bound blocks counted and excluded (it warns when more than 20% hit a bound);
    • reporting of the signed dy median;
    • --rectified now also fails on a constant offset.
    • Overrides: --max-disparity, --max-dy, --min-ncc.
  • The distorted fake honours _BASELINE_MM.
  • Tests:
    • A 120 mm matcher test: bar 94.9 px, signed dy +1.68 recovered as +1.74, with gain/offset and a flat block. The old 64 px window now gets rejected instead of reported.
    • An end-to-end test at 120 mm: bar ground truth 90.64 px, measured 90.66 px, signed dy −0.0001.
    • [vshift] hypothesis tests for the +1.7 px field residual:
      • a per-eye cy error gives a constant dy ≈ −Δcy (spread < 0.13);
      • a transposed R gives a large dy that follows the roll (−13 px, spread 7), so it is not a clean offset;
      • the calibration-size rescale and L1's swap (R′ = Rᵀ, T′ = −RᵀT) are exact;
      • an identity calibration is a pure scale about cy (the border nudge zooms ×1.001).

Verified: macOS build, ctest -R stereo (all pass), MinGW, and the probe on the distorted fake at 50 and 120 mm (--rectified PASS; 120 mm Z = 2.002 / 0.600 m).

@dfattal

dfattal commented Sep 27, 2026

Copy link
Copy Markdown
Collaborator Author

+1.7 px vertical residual on the SR laptop: diagnosed. The runtime rectifier is correct; the calibration doesn't match the frames.

Data: David's upload from the SR laptop (device QALA2137AL0011): the raw SBS frame, the runtime-rectified frames, the ProgramData intrinsics.yml/extrinsics.yml, the calib dumps and the service log.

I rectified the same raw frame with OpenCV stereoRectify + initUndistortRectifyMap from the same ymls (left = camera 2, R' = Rᵀ, T' = −RᵀT, the plug-in's swap) and measured SIFT matches:

signed dy median (R−L) std
raw +19.09 px (≈ calib cy diff 19.65 ✓) 4.2
OpenCV rectified (alpha 0) +1.71 px 2.2
runtime rectified (same frame) +1.71 px 1.1
runtime rectified, face 0.6 m +1.68 px —

OpenCV's P1 also matches the runtime's to 0.5 px in f and 0.01 px in the principal point. So the rectifier reproduces OpenCV exactly, and OpenCV has the same residual: the ymls don't describe these frames perfectly.

The residual is not a pure offset: a fit gives dy ≈ +2.6 px at the centre with a −0.47 px / 100 px slope in y, which reads as a small pitch/vertical-scale mismatch, not a single cy error. (That kills my "one eye's cy" lead hypothesis.)

Most likely cause: the ProgramData ymls are a cache and not what SR uses at runtime. The SR Server log on the 8K box reads "Fetching Stereo Camera calibration data from FPC", i.e. the live calibration comes from the device itself.

Next:

  • ask the SR SDK for the LIVE stereo-camera calibration (an API, or which file is authoritative);
  • optionally, a runtime-side online vertical-alignment refinement (estimate a dy offset plus y-slope from matched features over a few seconds and fold it into the rectification). It's cheap and would also absorb drift.

Script: OpenCV 4.10, SIFT 4000, ratio 0.7.

🤖 Generated with Claude Code

@dfattal

dfattal commented Sep 27, 2026

Copy link
Copy Markdown
Collaborator Author

Online vertical-alignment refinement added (602ae1ae8..91cfc6930). It addresses the +1.7 px SR residual diagnosed above.

What it does

  • Model: dy = a + b·(y − cy) (right minus left, output px). On the SR data the x/roll term was 0.04 px / 100 px (≤ 0.15 px at the edge, below match noise), so it is left out.
  • A per-camera worker (off the frame path) measures rectified frames the camera thread hands it:
    • Shi-Tomasi patches on a 2× downscale, NCC over d × dy, then full-res NCC ±2 px with parabolic sub-pixel.
    • Gated on NCC ≥ 0.9, d > 0, a peak not on a window bound, and real vertical curvature.
    • Robust fit: median start, then Tukey IRLS.
  • When the correction moves by more than 0.2 px, the worker rebuilds geometry + LUTs with v_offset = a/f and v_slope = b folded in.
    • The fold is a symmetric per-eye vertical affine in normalized rectified coordinates, applied before the principal point and alpha = 0 crop. So P1/P2 still describe one pinhole with a pure +x baseline, and they match the corrected frames.
    • The camera thread swaps the new maps in between frames and bumps calibration_generation.
  • Duty cycle: every 250 ms for 5 s after an open or an update, then one 3-frame window every 30 s.
  • Cost: 2.7 ms per measurement at -O2 (< 0.4 ms/frame amortised at 30 Hz); 8 ms per rebuild.
  • Safety:
    • clamps |a| ≤ 6 px and |b| ≤ 2 px / 100 px;
    • needs ≥ 50 matches over ≥ 3 frames;
    • re-verified on every re-open;
    • kill switch DXR_STEREO_CAMERA_REFINE=0.
  • Logging: one WARN on the first application, INFO after that (never per frame).
  • camera probe prints the state, the applied a/b, the match count, and dy before → after.

Results (macOS)

  • tests_stereo_vrefine, all 3 stereo suites green:
    • Fake with the SR model injected (SIM_DISPLAY_FAKE_STEREO_CAMERA_DY=2.6, _DY_SLOPE=-0.47): converges in one rebuild, and the fit recovers b = −0.469.

    • Real SR frame, checked against an independent judge (63 OpenCV SIFT matches; fixture stereo_vrefine_sr_raw.jpg, 62 KB):

      signed dy median
      before +1.715 px
      after +0.075 px
    • Flat, noise and vertical-stripe frames never apply a correction, and the clamps hold.

  • Live service + probe on the fake (+2.6 px injected):
    WARN stereo camera 1: vertical-alignment refinement APPLIED — the calibration left the rows +2.51 px off (fit +2.49 px -0.469 px/100 px about the centre row, 438 matches); correction a +2.49 px, b -0.469 px/100 px, maps rebuilt in 38.9 ms
    $ displayxr-cli camera probe --rectified --frames 90
      dy (right - left) median +0.001 px SIGNED; |dy| median 0.017 px ...  Z = 2.004 m / 0.600 m
    vertical refinement: APPLIED — correction a +2.49 px, b -0.469 px/100 px; 1 update(s), 5 window(s), 447 matches ...
    vertical refinement: signed dy BEFORE (first window, uncorrected) +2.51 px -> latest window +0.01 px; this probe's last frame +0.00 px (254 blocks)
    --rectified: PASS — rows aligned
    
    • With DXR_STEREO_CAMERA_REFINE=0: signed dy +2.54 px and --rectified: FAIL, as expected.
  • MinGW aux_util drv_sim_display passes. MinGW has no ipc_server target, so the service file is covered by MSVC CI.

On-hardware check (Windows SR box)

  1. Build and push the runtime. Keep DXR_STEREO_CAMERA_DEV_ALLOW=1 in the service's environment.
  2. Run displayxr-cli camera probe --rectified --format gray8 --seconds 6. The service needs about 1 s to settle, and the scene needs some texture: a sparse ceiling gave ~23 matches per frame, so 3 frames are enough.
  3. Expect this service-log line: vertical-alignment refinement APPLIED — the calibration left the rows +1.6..1.8 px off (… N matches); correction a ≈ +1.7 px …
  4. In the probe output, expect:
    • vertical refinement: APPLIED … signed dy BEFORE (first window, uncorrected) ≈ +1.7 px -> latest window ≈ 0.0 px;
    • the probe's own dy (right - left) median within ±0.5 px;
    • --rectified: PASS.
  5. A/B: restart the service with DXR_STEREO_CAMERA_REFINE=0. The probe should then read about +1.7 px again.

Not done:

  • no GPU backend;
  • no roll term;
  • no reset path, because nothing changes the calibration at runtime yet (u_stereo_vrefine_reset is there for that);
  • no merges.

🤖 Generated with Claude Code

@dfattal

dfattal commented Sep 28, 2026

Copy link
Copy Markdown
Collaborator Author

Review of f433d31 (PRESENT_OWNER quota counts distinct owner executables), from mac/holocall: approve for this branch.

🤖 Generated with Claude Code

dfattal and others added 15 commits October 3, 2026 19:59
…emap, no OpenCV (R2)

Vendor-neutral rectifier for XR_DXR_stereo_camera (ADR-043 R2), in three
layers so a GPU path can replace only the last one:

- geometry: OpenCV's stereoRectify(CALIB_ZERO_DISPARITY, alpha = 0) in C
  (half rotations + baseline onto x, min-fy focal, corner-centroid principal
  point, 9x9 inner-rectangle zoom), plus a border check of the real maps that
  zooms further until no output pixel samples outside its raw image;
- float maps (what a GPU backend uploads as an RG32F texture);
- CPU backend: fixed-point bilinear taps over the whole SBS image, 1/2/4
  channels (GRAY8, NV12 Y + half-res UV, BGRA8).

RADTAN5 / RADTAN8 / KB4 lens models, calibration rescaled when it was taken
at a different size than the frames. Also u_stereo_camera_estimate_offset:
SSD block matching over dx AND dy, for row-alignment checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd truth (R2)

SIM_DISPLAY_FAKE_STEREO_CAMERA_DISTORT=1 renders the fake's scene through two
RAW cameras: different per-eye intrinsics, RADTAN5 barrel lenses, and each
camera turned by half of (pitch 0.35, yaw 0.25, roll 0.45 deg) in opposite
senses (~6 px vertical misalignment, 0.9 deg relative roll). The symmetric
split makes Bouguet recover exactly the virtual parallel pair the scene is
defined in, so the ground truth after rectification is exact: aligned rows and
disparity f_rect * B / Z. The camera drops NATIVELY_RECTIFIED and its
calibration slot returns the truth, so RECTIFIED output exercises the
service's rectifier. The scene is sampled continuously (bilinear value noise),
so resampling is honest.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the distorted fake

tests_stereo_rectify, with fixtures generated offline by
tests/fixtures/gen_stereo_rectify_fixtures.py (OpenCV 4.10) for three
calibrations (the sim fake, an SR-like pair with a non-axial baseline, a
RADTAN8 pair calibrated at 2x the frame size):

- R1/R2 equal OpenCV's to 1e-12; principal point equals the converged
  reference to 1e-4 px (OpenCV's own P1 within 0.75 px: it stops
  undistortPoints after 5 iterations); focal within 1 % (measured +0.05..0.13 %);
- maps equal initUndistortRectifyMap to 3e-5 px;
- alpha = 0 really leaves no black pixel (full and half resolution);
- end to end: raw |dy| median 2.9 px -> rectified worst block 0.061 px;
  disparity 11.33 / 37.77 px ground truth, worst block error 0.063 px;
- NV12 / BGRA8 planes agree with GRAY8; hidden [.perf] case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…fied calibration (R2)

A per-camera rectifier is built at manager create from the plug-in's RAW
calibration when it is CALIBRATED and not NATIVELY_RECTIFIED. It runs once per
source frame on the camera thread, outside the manager lock, and only while a
started stream wants RECTIFIED; RAW streams read the original frame. A
RECTIFIED stream never receives unrectified pixels (a frame produced before it
started is skipped).

get_calibration(RECTIFIED) returns the same geometry: one pinhole for both
eyes (zero disparity at infinity), no distortion, rightFromLeft = identity +
(+baseline, 0, 0). horizontalFovDeg is the rectified one. The browser
(PRESENT_OWNER) is refused a camera the rectifier rejected instead of getting
the RAW-flagged fallback native clients get. The seam is struct scam_rectifier
(backend-neutral geometry + an apply() backend: CPU LUT today).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lignment, --rectified gates it

Every probe now 2-D block-matches the last frame (textured 32x32 blocks) and
prints |dy| median / p90 / blocks above 1 px, plus the dominant disparities as
depths Z = f*B/d from the RECTIFIED calibration. probe --rectified asks for
RECTIFIED output and exits 5 when frames come back RAW or the median |dy|
exceeds 0.5 px. Against the distorted fake: median 0.016 px, depths 2.004 m
and 0.599 m (truth 2.0 / 0.6); --raw shows the 2.6 px it fixes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…measured numbers

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… camera at -x

A swapped SBS pair still rectifies (Bouguet does not care), but every
disparity comes out negative and rightFromLeft points the wrong way. The
L1 Leia provider decides eye order from the sign of T; this makes a wrong
decision visible in the service log instead of only in a probe.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MSVC only defines M_PI when _USE_MATH_DEFINES precedes the first
<cmath>, which Catch may already have pulled in. Use a local constant.
First MSVC build of R2; the test passes (716 assertions, 7 cases).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…, signed dy, bound hits flagged

First SR-hardware run: a face at 0.6 m on the 120 mm tracker is ~95 px of
disparity, past the probe's 0..64 search, and the SSD matcher's +-10 px dy
window clamped too — it reported "d = 64, |dy| = 10.000" as if measured.

- u_stereo_camera_match_block: zero-mean NCC (blind to the two sensors'
  gain/offset), coarse pass on every 2nd pixel + full-res hill-climb, parabolic
  sub-pixel on both axes, the searched window returned, and dx_at_edge /
  dy_at_edge when the integer peak sits ON a bound (a clamp, not a measurement).
- camera probe: disparity window f*B/0.4 m from the camera (R1 helper), dy
  +-24 px, accept NCC >= 0.90; prints accepted / low-NCC / at-bound counts,
  warns when > 20 % of blocks hit a bound, reports the SIGNED dy median, and
  --rectified now also fails on a constant offset (|signed median| > 0.5 px).
  --max-disparity / --max-dy / --min-ncc override.
- sim fake: the DISTORTED variant honours _BASELINE_MM too (truth + slot).
- tests: large-baseline matcher (bar 94.9 px, signed dy +1.68 recovered to
  0.07 px, gain/offset, flat refused, the old 64 px window is REJECTED not
  reported); END TO END at 120 mm (bar 90.64 px GT, measured 90.66); the
  [vshift] hypotheses for the field's +1.7 px residual (per-eye cy error =
  constant dy ~ -dcy, transposed R = large roll-shaped dy, calib-size rescale
  and the plug-in's half swap exact, identity = pure scale about cy).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…torted fake (R2)

SIM_DISPLAY_FAKE_STEREO_CAMERA_DY (px) and _DY_SLOPE (px per 100 px) shift the
right eye's rows on top of the DISTORTED pair without telling the reported
calibration, like a device whose stored calibration is slightly off (the Leia
SR laptop: +2.6 px, -0.47 px / 100 px). Drives the online refinement's tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…reo_vrefine)

On a Leia SR laptop the rectified rows stayed +1.71 px apart, and OpenCV's
stereoRectify reproduces that on the same frame: the stored calibration is off,
not the rectifier. u_stereo_vrefine measures the residual dy = a + b (y - cy)
on rectified frames (Shi-Tomasi patches, half-res NCC search, full-res NCC
refinement, gated on NCC, d > 0, unclamped peaks and vertical curvature), fits
it robustly (median + Tukey IRLS) and runs the controller: duty cycle, >= 50
matches over >= 3 frames, 0.2 px deadband, |a| <= 6 px, |b| <= 2 px/100 px.

u_stereo_rectify folds the correction in as v_offset = a/f, v_slope = b: a
symmetric per-eye vertical affine in normalized rectified coordinates applied
BEFORE the principal point and the alpha = 0 crop, so P1/P2 describe the
corrected frames. Adds u_stereo_rectify_rect_from_raw (the inverse map).

tests_stereo_vrefine: the fake with the SR model converges to +0.000 px in one
rebuild; degenerate frames never apply; clamps hold; and a REAL SR raw frame
(62 KB JPEG + calibration + 63 OpenCV SIFT matches as an independent judge):
signed dy median +1.715 px -> +0.075 px.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…reports it

A per-camera worker measures rectified frames the camera thread parks for it
(every 250 ms for 5 s after an open or an update, then every 30 s; 2.7 ms per
measurement at -O2, off the frame path), rebuilds geometry + LUTs when the
correction moves, and the camera thread swaps them in between frames and bumps
calibration_generation. WARN on first application, INFO after, re-verified on
re-open, DXR_STEREO_CAMERA_REFINE=0 kill switch. Stream stats carry the state,
applied a/b, match count and dy before/after; camera probe prints them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… |dy| median

On the SR laptop with a face at 0.6 m the refined rows were aligned (signed median
-0.10/-0.18 px) but --rectified FAILED: block-match noise alone put the unsigned
|dy| median at 1.03/1.10 px. Gate on |signed median| <= 0.5 px, MAD about it
<= 1.5 px and >= 20 blocks; print the gate numbers; keep |dy| stats informational.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tions

On the SR laptop the B1 browser's "3D Camera (DisplayXR)" never appeared:
[CAP] refusing client pid=... ('ChromiumStereoCamera'): class PRESENT_OWNER
quota 2/2 exhausted (#960). One browser already takes both slots (GPU
process weaves, browser process binds the window), so its video-capture
utility - the XR_DXR_stereo_camera client, which declares PRESENT_OWNER
by enabling XR_DXR_weave - was the third connection and was refused
(xrCreateInstance -10 in the capture service).

A connection from the same executable as an admitted PRESENT_OWNER is now
a sibling and takes no new slot; the quota is compared against DISTINCT
owner executables, so two different browsers still fill it. A peer whose
image path is unreadable is never a sibling (fails closed).

Verified on the SR laptop: all three Chromium processes admitted, the
page lists and opens "3D Camera (DisplayXR)" (sbs 1280x480), stream
RECTIFIED.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant