
Get the complete source, dependencies and setup instructions from the template repository.
Open the source on GitHub ↗Introduction
Depth Camera Acceptance Overview
An Intel RealSense D435 is an active stereo depth camera: two global-shutter infrared imagers 50 mm apart, an infrared projector that paints a pseudo-random dot pattern on the scene so that textureless surfaces can be matched, and an ASIC that computes disparity per pixel and converts it to depth with the factory calibration. Depth comes out as Z16 frames: unsigned 16-bit integers in the camera's depth unit, 1 mm by default, with zero where the matcher found nothing. Every camera is calibrated at Intel's factory and carries its own intrinsics and extrinsics in flash; what varies from unit to unit, and with every drop and every heat cycle in transit, is how well that calibration still describes the optics.
Stereo depth error is not constant. For a disparity error of δ pixels, a focal length fx and a baseline B, the depth error at distance z is Δz = z² · δ / (fx · B): it grows with the square of the distance and shrinks with the baseline. On a D435 at 848 by 480, fx is about 421 px and B is 50 mm, so 0.08 px of disparity noise is 0.9 mm of depth at 0.5 m and 15 mm at 2 m. Intel's own Depth Quality Tool reduces a frame of a flat target to four numbers: the fill rate (share of valid pixels in the region of interest), the Z-accuracy (distance of the fitted plane against a ground truth, in percent), the plane-fit RMS error (spatial noise, in mm and in sub-pixels of disparity) and the temporal noise (per-pixel standard deviation over a burst of frames). A robotics integrator receiving cameras by the hundred needs those four numbers per serial, on a controlled target, before the camera goes onto a robot whose grasping or navigation depends on them.
Test Purpose
The procedure produces, per camera:
- Identity: serial, product name, firmware version against an allow-list, USB link type, depth units, ASIC and projector temperatures, all read from the device
- Stream health: 3 s of depth frame timing at 30 fps, frames per second and worst gap validated, dropped frames counted
- Depth quality: at 0.5, 1.0, 1.5 and 2.0 m on a flat matte board, fill rate, Z-accuracy against a laser rangefinder, plane-fit RMS and temporal noise from 30 frames each, one residual map attached
- Projector: fill rate on the textureless board with the emitter off, then on, and the laser power read back
- Calibration: stereo baseline, depth-to-color offset and rotation, focal length, the full depth intrinsics kept as a JSON record

The four distances of one run of the mock: the Z-accuracy error sits at +0.4 % of distance (8 mm at 2 m), the plane-fit RMS climbs from 0.8 to 12.6 mm along the z² curve, the temporal noise from 0.4 to 5.2 mm, and all of it stays well under the dashed 2 % line that Intel quotes for the D435.
Defects this catches before the camera ships on a robot: a decalibrated stereo pair (Z-accuracy off, plane-fit RMS up at every distance), a dead or weak projector (fill rate collapses on the board with the emitter on, the emitter-off and emitter-on numbers converge), a camera stuck on USB 2 (the link type reads 2.1, the frame rate is a fraction of 30), unapproved firmware (the perception stack was validated against specific versions), a noisy imager (temporal noise up while the plane fit is fine), and a shifted color module (RGB offset or rotation outside the D435 geometry).
Beyond the camera content, the template shows five framework mechanics: the device under test written as a plug with its stream profile passed through config:, a station-scoped rail plug that stays connected across units while the camera changes, then: error: retry with a retry budget on the connect phase for the USB re-enumeration a hot-plugged camera goes through, multi-dimensional measurements over distance with distance-normalised aggregations validated in YAML, and attach.data of a residual map and a JSON summary on the run.
Equipment & Setup
To accept depth cameras at incoming inspection, the following are required:
- A flat target: a matte white board, 1200 by 800 mm, flat to better than 1 mm, no texture of its own so the projector check means something
- A target rail: a motorised linear axis carrying the board from 0.5 to 2 m, with a laser rangefinder reading the board distance as the reference
- A camera fixture: a nest that squares the camera to the rail, a USB 3.2 cable of known quality to the test PC
- The Devices Under Test: RealSense D435 cameras as received, stock firmware
- A TofuPilot Framework procedure to enumerate, stream, sweep, measure and record
- The TofuPilot Dashboard to keep every camera's four metrics per distance and trend them per firmware and per delivery batch

The station: the camera in its nest at one end, the board on the rail carriage, the rangefinder next to the camera looking at the same board, the instrument beside the rail. Enclosing the rail against daylight is not needed for a D435 (the projector and the matcher cope with ambient light), but a constant background helps the fill-rate numbers repeat.
Hardware Components
Depth Camera
The Intel RealSense D435 (and its D435i sibling with an IMU) is the reference: 848 by 480 depth at up to 90 fps, 50 mm baseline, 87° by 58° depth field of view, ideal range 0.3 to 3 m. The D455 doubles the baseline to 95 mm and pushes the ideal range to 6 m; the same procedure accepts it with wider limits on the baseline and the RGB offset. Both talk pyrealsense2 over USB 3.2 and expose the same descriptors, options and calibration.

Why the sweep stops at 2 m for a D435 and would go to 4 m for a D455: at the same 0.08 px of disparity noise the 95 mm baseline halves the depth error, and the run's four plane-fit RMS points sit on the D435 curve as they should.
Target Rail and Reference
A 2 m linear axis (an igus drylin or a Festo ELGA belt axis with a stepper, 0.1 mm repeatability) carries the board; a laser rangefinder (Leica DISTO or a Keyence IL-series sensor for a fixed installation) mounted beside the camera reads the board distance to 0.15 mm and is the ground truth the Z-accuracy is computed against. The rail plug is declared scope: station: the axis homes once when the station starts and stays connected across every camera of the shift.

The camera in its nest: two IR imagers 50 mm apart, the projector between them, the color module beside them. Each distance is 30 frames, one plane fit per frame, one row in the four multi-dimensional measurements.
Limits
Limits for a D435 at 848 by 480 with the projector at its default 150 mW, as the template ships them:
| Parameter | Limit | Why |
|---|---|---|
| Firmware version | in 5.16.0.1, 5.15.1.0 | the versions the robot's perception stack was validated against |
| USB type | 3.2 | on 2.1 the camera streams but at a fraction of the rate |
| Frame rate | 29.5 to 30.5 fps | a USB or bandwidth problem drops frames before anything else fails |
| Max frame gap | ≤ 40 ms | one skipped frame at 30 fps is a 66 ms gap |
| Fill rate | ≥ 99 % at every distance | a flat matte board under the projector should be fully filled |
| Z-accuracy | ≤ 2 % at every distance | Intel's datasheet figure for the D435 at 2 m |
| Plane-fit RMS | ≤ 0.15 px sub-pixel | distance-independent; typical cameras sit near 0.08 px |
| Temporal noise | ≤ 10 mm at 2 m | about 0.05 px of per-frame disparity noise |
| Fill rate, emitter off | ≤ 50 % | a textureless board must not match without the pattern |
| Fill rate, emitter on | ≥ 99 % | the projector restores the match |
| Stereo baseline | 49.5 to 50.5 mm | the D435 geometry as calibrated |
| RGB offset | 13.5 to 16 mm | the color module sits 15 mm from the left imager |
| Depth-color rotation | ≤ 0.5° | a shifted color module misaligns the point cloud colours |
| Focal length | 410 to 435 px | fx at 848 by 480, catches a wrong stream profile or a bad calibration |
Custom Firmware
None. The camera runs its stock Intel firmware; everything the procedure reads and sets is exposed by pyrealsense2: descriptors through camera_info, temperatures and the emitter through rs.option, calibration through the stream profiles. The only station-side requirement is librealsense with the kernel patches or the RSUSB backend, and a USB 3.2 port that actually enumerates at SuperSpeed.
Test Procedure
Overview
For each camera in the fixture, the procedure runs:
- Enumerate the camera, read serial, name, firmware, USB type, depth units and temperatures; retry once if the USB stack is still re-enumerating.
- Start the depth stream at 848 by 480, 30 fps, and time 3 s of frames.
- Move the board to 0.5, 1.0, 1.5 and 2.0 m; at each, read the rangefinder, capture 30 frames, fit a plane per frame, compute fill rate, Z-accuracy, plane-fit RMS and temporal noise.
- At 1 m, capture with the emitter off, then on, and compare the fill rates.
- Read the calibration back from the device and check baseline, RGB offset, rotation and focal length.
- Teardown: stop the stream, home the rail.
Why TofuPilot Framework?
TofuPilot Framework is a YAML + Python test framework built for hardware manufacturing. Instead of writing all your test logic, measurements, and limits inside Python code, you describe what the test does in a procedure.yaml file, and how in small Python phase files. The framework handles:
- Automatic Python environment management (via
uv) - Operator UI (no frontend code needed)
- Measurement validation and live charts
- Process isolation between phases and equipment plugs
Project Structure
The camera and the rail are mocks; the plane fits and the noise statistics are real numpy, run by the framework in a uv environment with numpy and pillow. Run it with tofupilot run . --no-tui --no-kiosk --json. You can find the full source on GitHub.
The Procedure File
This is the exact file the template ships, verified end to end with the CLI:
procedure.yaml · 259 lines
name: RealSense Depth Camera Acceptance Testversion: 0.1.0description: Accepts an Intel RealSense D435 depth camera on a target rail. Reads its descriptors, checks the stream timing, measures fill rate, Z-accuracy, plane-fit and temporal noise at four distances, proves the IR projector, and checks the factory extrinsics.unit: auto_identify: true serial_number: description: "Scan the camera label" placeholder: "213522070981" pattern: "^\\d{12}$" default_value: "213522070981" part_number: default_value: "RS-D435" metadata: firmware_version: description: "Firmware reported by the camera" usb_type: description: "USB link the camera enumerated on (3.2 or 2.1)"plugs: - name: RealSense Camera description: "The camera under test over pyrealsense2: descriptors, streams, options, calibration (mock)" python: plugs.realsense:RealSenseCamera key: camera config: width: 848 height: 480 fps: 30 - name: Target Rail description: Motorised rail carrying the flat target, with a laser rangefinder as distance reference (mock) python: plugs.rail:TargetRail key: rail scope: station config: port: "/dev/ttyUSB0"main: - name: Connect Camera key: connect_camera python: phases.connect_camera retry: limit: 3 delay: 2s then: error: retry measurements: - name: Camera Serial key: camera_serial validators: - {operator: matches, expected_value: "^\\d{12}$"} - name: Product Name key: product_name validators: - {operator: "==", expected_value: "Intel RealSense D435"} - name: Firmware Version key: firmware_version description: "Only firmware validated with the robot's perception stack is accepted" validators: - {operator: in, expected_value: ["5.16.0.1", "5.15.1.0"]} - name: USB Type key: usb_type description: "A camera on USB 2.1 streams at a fraction of the rate; the cable or the port is at fault" validators: - {operator: "==", expected_value: "3.2"} - name: Depth Units key: depth_units unit: µm validators: - {operator: "==", expected_value: 1000} - name: ASIC Temperature key: asic_temperature unit: °C validators: - {operator: ">=", expected_value: 20.0} - {operator: "<=", expected_value: 60.0} - name: Projector Temperature key: projector_temperature unit: °C validators: - {operator: ">=", expected_value: 20.0} - {operator: "<=", expected_value: 60.0} - name: Stream Check key: stream_check python: phases.stream_check depends_on: [connect_camera] measurements: - name: Frame Interval key: frame_interval title: Depth frame interval over 3 s at 30 fps x_axis: legend: Frame y_axis: - legend: Interval key: interval unit: ms aggregations: - type: fps validators: - {operator: ">=", expected_value: 29.5} - {operator: "<=", expected_value: 30.5} - type: max_gap_ms unit: ms validators: - {operator: "<=", expected_value: 40.0} - name: Frames Dropped key: frames_dropped validators: - {operator: "==", expected_value: 0} - name: Depth Quality Sweep key: depth_quality python: phases.depth_quality depends_on: [stream_check] timeout: 300s ui: components: - key: sweep_progress type: progress label: "Target distances" description: "0.5, 1.0, 1.5 and 2.0 m, 30 frames each" default_value: 0 max: 100 measurements: - name: Fill Rate key: fill_rate title: Valid depth pixels in the ROI per distance x_axis: legend: Distance unit: mm y_axis: - legend: Fill rate key: fill unit: "%" aggregations: - type: min_pct unit: "%" validators: - {operator: ">=", expected_value: 99.0} - name: Z-Accuracy key: z_accuracy title: Plane distance error against the rangefinder per distance x_axis: legend: Distance unit: mm y_axis: - legend: Error key: error unit: "%" aggregations: - type: max_abs_pct unit: "%" validators: - {operator: "<=", expected_value: 2.0} - name: Plane Fit RMS key: plane_fit_rms title: Plane-fit residual RMS per distance description: "Grows with the square of the distance; the aggregation converts it back to disparity pixels, which does not" x_axis: legend: Distance unit: mm y_axis: - legend: RMS key: rms unit: mm aggregations: - type: max_subpixel_px unit: px validators: - {operator: "<=", expected_value: 0.15} - name: Temporal Noise key: temporal_noise title: Per-pixel depth noise over 30 frames per distance x_axis: legend: Distance unit: mm y_axis: - legend: Noise key: noise unit: mm aggregations: - type: max_mm unit: mm validators: - {operator: "<=", expected_value: 10.0} - name: Projector Check key: projector_check python: phases.projector_check depends_on: [depth_quality] measurements: - name: Laser Power key: laser_power_mw unit: mW validators: - {operator: "==", expected_value: 150} - name: Fill Rate Emitter Off key: fill_rate_emitter_off unit: "%" description: "A matte white board has no texture, so without the projected pattern the matcher must lose most pixels" validators: - {operator: "<=", expected_value: 50.0} - name: Fill Rate Emitter On key: fill_rate_emitter_on unit: "%" validators: - {operator: ">=", expected_value: 99.0} - name: Emitter Gain key: emitter_gain_pct unit: "%" validators: - {operator: ">=", expected_value: 40.0} - name: Extrinsics Check key: extrinsics_check python: phases.extrinsics_check depends_on: [projector_check] measurements: - name: Stereo Baseline key: stereo_baseline_mm unit: mm validators: - {operator: ">=", expected_value: 49.5} - {operator: "<=", expected_value: 50.5} - name: RGB Offset key: rgb_offset_mm unit: mm description: "Depth to color translation, 15 mm on a D435 by construction" validators: - {operator: ">=", expected_value: 13.5} - {operator: "<=", expected_value: 16.0} - name: Depth-Color Rotation key: depth_color_rotation_deg unit: ° validators: - {operator: "<=", expected_value: 0.5} - name: Focal Length key: focal_length_px unit: px validators: - {operator: ">=", expected_value: 410.0} - {operator: "<=", expected_value: 435.0} - name: Depth Intrinsics key: depth_intrinsics description: "The full depth intrinsics as the device reports them, kept whole for the record"teardown: - name: Release Camera key: release_camera python: phases.release_camera measurements: - name: Stream Stopped key: stream_stopped validators: - {operator: "==", expected_value: true} - name: Rail Homed key: rail_homed validators: - {operator: "==", expected_value: true}Framework features to notice:
- The device under test is a plug:
plugs.realsense:RealSenseCamerawrapspyrealsense2, and the stream profile (width,height,fps) travels throughconfig:into its__init__, so the same plug accepts a D455 at 1280 by 720 by editing the YAML. - Two plug scopes on one bench: the rail is
scope: station, homed once and held across every camera of the shift; the camera keeps the default scope and is re-created for every unit, which is what a device under test needs. In a one-shottofupilot runa station plug behaves as execution scope, so the template runs unchanged. - Retry on error:
connect_cameracarriesretry: {limit: 3, delay: 2s}andthen: {error: retry}. A RealSense plugged in a moment ago failsquery_devices()while the USB stack re-enumerates; the first attempt errors, the second passes, and the run page shows both attempts. One thing learned on the way: the override is honoured onmainphases, not onsetupphases, where the engine declares the setup failed before it looks atthen, which is why the connect phase sits at the head ofmainwith everything depending on it. - Distance-normalised aggregations:
plane_fit_rmsis recorded in mm per distance, where it must grow with z², and validated through itsmax_subpixel_pxaggregation, which the phase computes as RMS · fx · B / z² and which does not depend on distance. The same pattern givesfill_rateitsmin_pct,z_accuracyitsmax_abs_pctandtemporal_noiseitsmax_mm, all four named by the phase and checked in YAML. - String validators for the descriptors:
firmware_versionwithinagainst the allow-list,usb_typewith==to"3.2",camera_serialwithmatcheson twelve digits; the descriptors also land inunit.metadata, so a run can be filtered by firmware later.
Connect Camera
The camera plug's open() enumerates the device and reads its descriptors; the phase records them, stamps firmware and USB type on the unit, and writes which rail and target the run used into run.metadata:
def connect_camera(measurements, unit, run, camera, log): """Setup: enumerate the camera, read its descriptors, stamp firmware and USB link on the unit.""" info = camera.open() log.info(f"{info['name']} {info['serial']}: firmware {info['firmware']}, USB {info['usb_type']}, ASIC {info['asic_temperature_c']:.1f} C") measurements.camera_serial = info["serial"] measurements.product_name = info["name"] measurements.firmware_version = info["firmware"] measurements.usb_type = info["usb_type"] measurements.depth_units = info["depth_units_um"] measurements.asic_temperature = info["asic_temperature_c"] measurements.projector_temperature = info["projector_temperature_c"] unit.metadata["firmware_version"] = info["firmware"] unit.metadata["usb_type"] = info["usb_type"] run.metadata["rail_id"] = "RAIL-02" run.metadata["target"] = "matte white board 1200x800"On the mock the first open() raises RuntimeError("No device connected"), the way librealsense does right after a hot-plug; the engine waits 2 s and runs the phase again, and attempt 2 records the descriptors.
Stream Check
Three seconds of depth frames at 30 fps, the interval between consecutive frames recorded as a multi-dimensional measurement, the frame rate and the worst gap as its aggregations:
TIMING_S = 3.0def stream_check(measurements, camera, log): """Start the depth stream and check it delivers every frame on time.""" camera.start() intervals = camera.frame_intervals(TIMING_S) nominal = 1000.0 / 30 dropped = sum(1 for dt in intervals if dt > 1.5 * nominal) fps = 1000.0 / (sum(intervals) / len(intervals)) log.info(f"{len(intervals)} frames in {TIMING_S:.0f} s: {fps:.2f} fps, max gap {max(intervals):.1f} ms, {dropped} dropped") measurements.frame_interval.x_axis = list(range(1, len(intervals) + 1)) measurements.frame_interval.y_axis.interval = intervals measurements.frame_interval.y_axis.interval.aggregations.fps = fps measurements.frame_interval.y_axis.interval.aggregations.max_gap_ms = max(intervals) measurements.frames_dropped = droppedOn the mock: 90 frames, 29.95 fps, 34.1 ms worst gap, none dropped. A camera that enumerated on USB 2.1 fails here twice, on the USB type in the previous phase and on the frame rate in this one.
Depth Quality Sweep
The heart of the procedure. For each distance, the rail moves, the rangefinder gives the reference, the camera captures 30 frames of the region of interest and saves them to a file; the phase deprojects every pixel with the depth intrinsics, fits a plane per frame in metric space (so a target that is not perfectly square to the camera still yields the distance along the optical axis), and reduces the burst to the four Depth Quality Tool metrics:
phases/depth_quality.py · 54 lines
DISTANCES_MM = [500, 1000, 1500, 2000]N_FRAMES = 30def depth_quality(measurements, attach, camera, rail, ui, log): """Walk the target through four distances and measure the four Depth Quality Tool metrics at each.""" intr = camera.depth_intrinsics() baseline = camera.extrinsics()["stereo_baseline_mm"] fill, z_err, rms, subpixel, temporal = [], [], [], [], [] residual_map = None for i, d in enumerate(DISTANCES_MM): rail.move_to(d) ref = rail.reference_mm() cap = camera.capture_depth(N_FRAMES, ref) frames = np.load(cap["path"]).astype(np.float64) * cap["depth_units_um"] / 1000.0 # mm valid = frames > 0 per_frame_rms, per_frame_dist = [], [] for z, ok in zip(frames, valid): x, y, z = deproject(z, cap["roi"], intr) dist, residual = fit_plane(x, y, z, ok) per_frame_dist.append(dist) per_frame_rms.append(float(np.sqrt(np.nanmean(residual**2)))) dist = float(np.mean(per_frame_dist)) always_valid = valid.all(axis=0) noise = float(frames[:, always_valid].std(axis=0).mean()) fill.append(100.0 * float(valid.mean())) z_err.append(100.0 * (dist - ref) / ref) rms.append(float(np.mean(per_frame_rms))) subpixel.append(rms[-1] * intr["fx"] * baseline / dist**2) temporal.append(noise) residual_map = residual # the last distance is where the residuals are largest log.info(f"{d} mm (ref {ref:.1f}): fill {fill[-1]:.2f} %, z error {z_err[-1]:+.2f} %, plane RMS {rms[-1]:.2f} mm = {subpixel[-1]:.3f} px, temporal {noise:.2f} mm") ui.sweep_progress = int(100 * (i + 1) / len(DISTANCES_MM)) measurements.fill_rate.x_axis = DISTANCES_MM measurements.fill_rate.y_axis.fill = fill measurements.fill_rate.y_axis.fill.aggregations.min_pct = min(fill) measurements.z_accuracy.x_axis = DISTANCES_MM measurements.z_accuracy.y_axis.error = z_err measurements.z_accuracy.y_axis.error.aggregations.max_abs_pct = max(abs(e) for e in z_err) measurements.plane_fit_rms.x_axis = DISTANCES_MM measurements.plane_fit_rms.y_axis.rms = rms measurements.plane_fit_rms.y_axis.rms.aggregations.max_subpixel_px = max(subpixel) measurements.temporal_noise.x_axis = DISTANCES_MM measurements.temporal_noise.y_axis.noise = temporal measurements.temporal_noise.y_axis.noise.aggregations.max_mm = max(temporal) attach.data(residual_png(residual_map, rms[-1]), f"plane_residuals_{DISTANCES_MM[-1]}mm.png") summary = {"distances_mm": DISTANCES_MM, "fill_rate_pct": fill, "z_accuracy_pct": z_err, "plane_fit_rms_mm": rms, "subpixel_rms_px": subpixel, "temporal_noise_mm": temporal} attach.data(json.dumps(summary, indent=2).encode(), "depth_quality.json")![]()
The two faces of the same measurement on the mock run: 0.84, 3.17, 7.12 and 12.65 mm of plane-fit RMS, on a k·z² curve with k = 3.16 mm per m², and 0.070, 0.066, 0.066 and 0.066 px once converted back to disparity. A limit in millimetres would have to be a different number at every distance; the sub-pixel figure is one limit for the whole sweep and compares directly across the D400 family.
The Z-accuracy reads +0.40 % at every distance, the signature of a stereo pair whose true baseline is 0.4 % shorter than the calibrated one. Fill rate is 99.8 % throughout, temporal noise 0.43 to 5.22 mm. The residual map attached to the run shows the 2 m plane fit pixel by pixel: blue closer than the plane, red farther, black where the matcher returned nothing.
Projector Check
A matte white board has no texture of its own, so a stereo matcher without the projected pattern loses most of it. The phase captures at 1 m with the emitter off, then on, and records both fill rates and their difference:
phases/projector_check.py · 20 lines
CHECK_MM = 1000N_FRAMES = 10def projector_check(measurements, camera, rail, log): """Prove the IR projector works: fill rate on a textureless board with the emitter off, then on.""" rail.move_to(CHECK_MM) ref = rail.reference_mm() camera.set_emitter(False) off = fill_rate(camera.capture_depth(N_FRAMES, ref)) camera.set_emitter(True) power = camera.laser_power_mw() on = fill_rate(camera.capture_depth(N_FRAMES, ref)) log.info(f"fill rate at {CHECK_MM} mm: {off:.1f} % emitter off, {on:.1f} % emitter on at {power:.0f} mW") measurements.laser_power_mw = power measurements.fill_rate_emitter_off = off measurements.fill_rate_emitter_on = on measurements.emitter_gain_pct = on - off
31.0 % with the emitter off, 99.8 % with it on, a gain of 69 points at 150 mW. A dead projector shows both numbers near 31 %; a weak one shows the emitter-on fill rate sliding under 99 % at 1 m long before it does at 0.5 m.
Extrinsics Check
The factory calibration is read back from the device and checked against the D435 geometry, with the full intrinsics kept as a JSON measurement:
def extrinsics_check(measurements, camera, log): """Read the factory calibration back from the device and check it against the D435 geometry.""" intr = camera.depth_intrinsics() extr = camera.extrinsics() rgb_offset = math.sqrt(sum(t * t for t in extr["depth_to_color_translation_mm"])) log.info(f"baseline {extr['stereo_baseline_mm']:.2f} mm, RGB offset {rgb_offset:.2f} mm, rotation {extr['depth_to_color_rotation_deg']:.2f} deg, fx {intr['fx']:.1f} px") measurements.stereo_baseline_mm = extr["stereo_baseline_mm"] measurements.rgb_offset_mm = rgb_offset measurements.depth_color_rotation_deg = extr["depth_to_color_rotation_deg"] measurements.focal_length_px = intr["fx"] measurements.depth_intrinsics = intrOn the mock: baseline 50.03 mm, RGB offset 14.72 mm, rotation 0.12°, fx 421.6 px. The extrinsics say what the camera believes about itself; the sweep before it says whether that belief still matches the optics.
Mock Plugs
The camera mock hides a true stereo geometry and answers capture_depth() with Z16 frames computed from it: the device reports a calibrated baseline of 50.03 mm while the true optical baseline is 49.83 mm (so every depth reads 0.4 % long), disparity carries a fixed spatial pattern of 0.06 px and a per-frame temporal noise of 0.028 px, the board is tilted 0.6° and 0.4° off square, 0.2 % of pixels are holes with the projector on and 69 % without it. Depth is quantised to the 1 mm depth unit. The phases never read these constants:
plugs/realsense.py · 29 lines
# The camera's real optics, unknown to the procedure.BASELINE_TRUE_MM = 49.83 # 0.4 % short of the calibrated value: depth reads longSPATIAL_PX = 0.060 # fixed disparity pattern (lens, sensor, ASIC)TEMPORAL_PX = 0.028 # per-frame disparity noiseTILT_DEG = (0.6, -0.4) # the target is never perfectly square to the cameraHOLE_RATE = 0.002 # invalid pixels with the projector onEMITTER_OFF_FILL = 0.31 # a matte white board has no texture of its own def capture_depth(self, n_frames, scene_mm) -> dict: """Capture `n_frames` Z16 frames of the ROI and save them to depth_capture.npy. `scene_mm` builds the mock scene: a flat matte board at that distance. A real plug ignores it, the board is whatever the rail put in front of the lens.""" n, d = int(n_frames), float(scene_mm) x0, y0, rw, rh = self._roi tx, ty = math.tan(math.radians(TILT_DEG[0])), math.tan(math.radians(TILT_DEG[1])) z_true = d / (1.0 - tx * self._nx - ty * self._ny) # plane through (0, 0, d), normal tilted disparity = FX * BASELINE_TRUE_MM / z_true gain = 1.0 if self._emitter else 3.0 # without the pattern the matcher is noisier hole_rate = HOLE_RATE if self._emitter else 1.0 - EMITTER_OFF_FILL frames = np.empty((n, rh, rw), dtype=np.uint16) for i in range(n): measured = disparity + gain * (self._pattern_px + self._rng.normal(0.0, TEMPORAL_PX, (rh, rw))) z_units = np.rint(FX * BASELINE_CAL_MM / measured * 1000.0 / DEPTH_UNITS_UM) z_units[self._rng.random((rh, rw)) < hole_rate] = 0 frames[i] = z_units.astype(np.uint16) np.save(CAPTURE_PATH, frames) return {"path": CAPTURE_PATH, "frames": n, "roi": list(self._roi), "depth_units_um": DEPTH_UNITS_UM}Plug calls cross a JSON boundary, and 30 frames of a 170 by 96 region are half a million values, so the frames go through a .npy file and the call returns its path and the ROI; the phase loads the array. On a real station, open() is rs.context().query_devices() plus get_info() for the descriptors, start() builds an rs.config from the width, height and fps the procedure passed through config: and starts an rs.pipeline, capture_depth() collects wait_for_frames().get_depth_frame() into the same array and ignores its second argument, set_emitter() and laser_power_mw() are rs.option.emitter_enabled and rs.option.laser_power, and the intrinsics and extrinsics come from the stream profiles. The rail plug drives the real axis and reads the rangefinder. The phases, the measurements and the limits do not change.
