|
| 1 | +# Stereo Dynamic Calibration |
| 2 | + |
| 3 | +This example demonstrates **runtime stereo camera calibration** with the `DynamicCalibration` node, plus a host-side controller/visualizer that overlays helpful UI (help panel, coverage bar, quality/recalibration modals, and a depth ROI HUD). It integrates with the [RemoteConnection](https://rvc4.docs.luxonis.com/software/depthai-components/tools/remote_connection/) service to visualize streams in real time. |
| 4 | + |
| 5 | +> Works in **peripheral mode**: the device performs calibration; the host sends commands and renders overlays. |
| 6 | +
|
| 7 | +## Features |
| 8 | + |
| 9 | +- **Interactive commands**: start/force recalibration, load images, run quality checks, apply/rollback calibrations, and **flash** (EEPROM) new/previous/factory calibration. |
| 10 | +- **Coverage bar**: centered, large progress bar while collecting frames (or briefly after `l`). |
| 11 | +- **Quality modal**: big 3-color bar (GOOD / COULD BE IMPROVED / NEEDS RECALIBRATION) with a pointer based on rotation change and a summary of depth-error deltas. |
| 12 | +- **Recalibration modal**: summary with Euler angle deltas and depth-error deltas; prompts to flash if there is a significant change. |
| 13 | +- **Depth HUD**: optional, shows depth/disp at a movable ROI (center + mean), with a small “tiny box” indicator. |
| 14 | +- **Always-on help panel** (toggleable). |
| 15 | + |
| 16 | +## Demo |
| 17 | + |
| 18 | +<p align="center"> |
| 19 | + <img src="media/dcl.gif" alt="demo" /> |
| 20 | +</p> |
| 21 | + |
| 22 | +## Requirements |
| 23 | + |
| 24 | +- A **Luxonis device** connected via USB/Ethernet. |
| 25 | +- Python **3.10+** (tested with 3.12). |
| 26 | +- Packages: |
| 27 | + - `depthai` |
| 28 | + - `depthai-nodes` |
| 29 | + - `opencv-python` |
| 30 | + - `numpy` |
| 31 | + |
| 32 | +Install via: |
| 33 | +```bash |
| 34 | +pip install -r requirements.txt |
| 35 | +``` |
| 36 | + |
| 37 | +## Run |
| 38 | + |
| 39 | +```bash |
| 40 | +python3 main.py |
| 41 | +# or |
| 42 | +python3 main.py --fps_limit 10 |
| 43 | +# or |
| 44 | +python3 main.py --device 18443010C1BA9D1200 |
| 45 | +``` |
| 46 | + |
| 47 | +When launched, the app starts a RemoteConnection server. Open the visualizer at: |
| 48 | +``` |
| 49 | +http://localhost:8082 |
| 50 | +``` |
| 51 | +Replace `localhost` with your host IP if viewing from another machine. |
| 52 | + |
| 53 | +## Wiring Notes (snippet) |
| 54 | + |
| 55 | +Make sure to: |
| 56 | +1) Link a preview stream for timestamped overlays (e.g., colormapped disparity/depth), |
| 57 | +2) Link a raw depth OR disparity stream to the controller (for the ROI HUD), |
| 58 | +3) Provide the **device** handle to enable flashing. |
| 59 | + |
| 60 | +```python |
| 61 | +visualizer = dai.RemoteConnection(httpPort=8082) |
| 62 | +device = pipeline.getDefaultDevice() |
| 63 | + |
| 64 | +stereo = pipeline.create(dai.node.StereoDepth) |
| 65 | +# ... set stereo configs, link left/right, etc. |
| 66 | + |
| 67 | +# Colormap for display |
| 68 | +depth_color = pipeline.create(ApplyColormap).build(stereo.disparity) # or stereo.depth |
| 69 | +depth_color.setColormap(cv2.COLORMAP_JET) |
| 70 | + |
| 71 | +# Dynamic calibration node |
| 72 | +dyn_calib = pipeline.create(dai.node.DynamicCalibration) |
| 73 | +left_out.link(dyn_calib.left) |
| 74 | +right_out.link(dyn_calib.right) |
| 75 | + |
| 76 | +# Controller |
| 77 | +dyn_ctrl = pipeline.create(DynamicCalibrationControler).build( |
| 78 | + preview=depth_color.out, # used for overlay timing |
| 79 | + depth=stereo.depth # or .disparity; call set_depth_units_is_mm(False) if disparity |
| 80 | +) |
| 81 | +dyn_ctrl.set_command_input(dyn_calib.inputControl.createInputQueue()) |
| 82 | +dyn_ctrl.set_quality_output(dyn_calib.qualityOutput.createOutputQueue()) |
| 83 | +dyn_ctrl.set_calibration_output(dyn_calib.calibrationOutput.createOutputQueue()) |
| 84 | +dyn_ctrl.set_coverage_output(dyn_calib.coverageOutput.createOutputQueue()) |
| 85 | +dyn_ctrl.set_device(device) # enables flashing p/k/f |
| 86 | +``` |
| 87 | + |
| 88 | +## Controls |
| 89 | + |
| 90 | +Use these keys while the app is running (focus the browser visualizer window): |
| 91 | + |
| 92 | +| Key | Action | |
| 93 | +| --- | ------ | |
| 94 | +| `q` | Quit the app | |
| 95 | +| `h` | Toggle help panel | |
| 96 | +| `g` | Toggle Depth HUD (ROI readout) | |
| 97 | +| `r` | Start recalibration | |
| 98 | +| `d` | **Force** recalibration | |
| 99 | +| `l` | Load image(s) for calibration (shows coverage bar for ~2s) | |
| 100 | +| `c` | Calibration quality check | |
| 101 | +| `v` | **Force** calibration quality check | |
| 102 | +| `n` | Apply **NEW** calibration (when available) | |
| 103 | +| `o` | Apply **PREVIOUS** calibration (rollback) | |
| 104 | +| `p` | **Flash NEW/current** calibration to EEPROM | |
| 105 | +| `k` | **Flash PREVIOUS** calibration to EEPROM | |
| 106 | +| `f` | **Flash FACTORY** calibration to EEPROM | |
| 107 | +| `w / a / s` | Move ROI up/left/down (Depth HUD).<br>**Note:** `d` is reserved for *Force recalibrate*. | |
| 108 | +| `z / x` | ROI size − / + | |
| 109 | + |
| 110 | +> **Status banners** appear in the **center** after critical actions (e.g., applying/ flashing calibration) and auto-hide after ~2s. |
| 111 | +> **Modals** (quality/recalibration) also appear centered and auto-hide after ~3.5s or on any key press. |
| 112 | +
|
| 113 | +## On‑screen UI Cheat Sheet |
| 114 | + |
| 115 | +- **Help panel** (top-left): quick reference of all keys (toggle with `h`). |
| 116 | +- **Coverage bar** (center): big progress bar while collecting frames; also shown briefly (≈2s) after pressing `l`. |
| 117 | +- **Quality modal** (center): three colored segments (green/yellow/red) with a **downward** pointer (`▼`) indicating rotation-change severity; optional line with depth-error deltas (@1m/2m/5m/10m). |
| 118 | +- **Recalibration modal** (center): “Recalibration complete”, significant-axis warning (if any), Euler angles, and depth-error deltas; suggests flashing if the change is significant. |
| 119 | +- **Depth HUD** (inline): shows depth/disp at the ROI center and mean within a tiny box; move with `w/a/s` (and resize with `z/x`). |
| 120 | + |
| 121 | +## Output (console) |
| 122 | + |
| 123 | +- **Coverage**: per-cell coverage and acquisition status when emitted by the device. |
| 124 | +- **Calibration results**: prints when a new calibration is produced and shows deltas: |
| 125 | + - Rotation delta `|| r_current - r_new ||` in degrees, |
| 126 | + - Mean Sampson error (new vs. current), |
| 127 | + - Theoretical **Depth Error Difference** at 1/2/5/10 meters. |
| 128 | +- **Quality checks**: same metrics as above without actually applying a new calibration. |
| 129 | + |
| 130 | +## Tips & Notes |
| 131 | + |
| 132 | +- To **flash** (EEPROM) from the UI you must pass the `device` into the controller (`dyn_ctrl.set_device(device)`). |
| 133 | +- If you link **disparity** instead of **depth** to the controller, call `dyn_ctrl.set_depth_units_is_mm(False)` so the HUD labels use “Disp” instead of meters. |
| 134 | +- The coverage percentage accepts either `[0..1]` or `[0..100]` from the device; the controller auto-detects and normalizes. |
| 135 | +- The **Collecting frames** bar hides automatically 2s after pressing `l`; during active recalibration (`r`/`d`) it stays up until calibration finishes. |
| 136 | + |
| 137 | +## Installation (dev quick start) |
| 138 | + |
| 139 | +```bash |
| 140 | +python3 -m venv .venv |
| 141 | +source .venv/bin/activate |
| 142 | +pip install -U pip |
| 143 | +pip install -r requirements.txt |
| 144 | +python3 main.py |
| 145 | +``` |
| 146 | + |
| 147 | +--- |
| 148 | + |
| 149 | +If you use this as a base for your own app, the heart of the UX is `utils/dynamic_controler.py` — it wires `DynamicCalibration` queues and renders all overlays via `ImgAnnotations` so you don’t need `cv2.imshow()`. |
0 commit comments