# DynamicCalibration - technical implementation

DynamicCalibration is a self-calibration workflow built into DepthAI 3.0 that restores and maintains stereo accuracy when
temperature changes, physical shocks, or long-term drift degrade factory calibration.

On this page we layout the steps for technical implementation of the DynamicCalibration into your projects. If you want to know
more about general information about DynamicCalibration, visit [this
page](https://docs.luxonis.com/hardware/platform/depth/dynamic-calibration.md).

### Key capabilities

 * Restore depth performance—brings the disparity map back to optimal visual quality.
 * No targets required—operate in natural scenes; just move the camera to capture a varied view.
 * Rapid execution—typically completes in seconds.
 * Health monitoring—run diagnostics at any time without flashing new calibration.
 * Multi-sensor support—supports both classic stereo recalibration and 3-sensor / FFC workflows.

## Automatic calibration options

There are two ways to run automatic calibration on top of `DynamicCalibration`:

 * Use the [AutoCalibration Host
   Node](https://docs.luxonis.com/software-v3/depthai/depthai-components/host_nodes/auto_calibration.md) for explicit node-level
   control inside your pipeline.
 * Use `DEPTHAI_AUTOCALIBRATION` for deployment-time enablement without changing pipeline code.
 * Using direct setter of `Pipeline.setAutoCalibrationMode()` to enable automatic calibration without using the AutoCalibration
   Host Node.

> **Note**
> From `DepthAI 3.6`, automatic calibration is enabled by default in `ON_START` mode. Use `OFF` to disable it. If your pipeline already includes a `DynamicCalibration` node or an `AutoCalibration` node, `AutoCalibrationMode` is not applied.

## Usage of the Dynamic Calibration Library (DCL)

This section is a simple high-level representation of how to use the `DynamicCalibration` node in DepthAI for dynamic calibration
workflows.

Dynamic calibration takes as input a
[DynamicCalibrationControl](https://docs.luxonis.com/software-v3/depthai/depthai-components/messages/dynamic_calibration_control.md)
message with a command (eg. `ApplyCalibration`, `Calibrate`, ... see all available commands in the message definition). The node
returns an output in one of the four output queues:

 * `calibrationOutput` with message type
   [DynamicCalibrationResult](https://docs.luxonis.com/software-v3/depthai/depthai-components/messages/dynamic_calibration_result.md),
 * `qualityOutput` with message type
   [CalibrationQuality](https://docs.luxonis.com/software-v3/depthai/depthai-components/messages/calibration_quality.md)
 * `coverageOutput` with message type
   [CoverageData](https://docs.luxonis.com/software-v3/depthai/depthai-components/messages/coverage_data.md)
 * `metricsOutput` with message type
   [CalibrationMetrics](https://docs.luxonis.com/software-v3/depthai/depthai-components/messages/calibration_metrics.md)

depending on which input command was sent.

In the next paragraphs we are going to explore how to actually implement DCL into your code.

#### Initializing the DynamicCalibration Node

The `DynamicCalibration` node requires synchronized camera streams from the same device. The classic setup uses two cameras, while
multi-sensor workflows can also attach an RGB stream. Here is the basic stereo setup:

```python
import depthai as dai

# initialize the pipeline
pipeline = dai.Pipeline()

# Create camera nodes
cam_left = pipeline.create(dai.node.Camera).build(dai.CameraBoardSocket.CAM_B)
cam_right = pipeline.create(dai.node.Camera).build(dai.CameraBoardSocket.CAM_C)

# Request full resolution NV12 outputs
left_out = cam_left.requestFullResolutionOutput()
right_out = cam_right.requestFullResolutionOutput()

# Initialize the DynamicCalibration node
dyn_calib = pipeline.create(dai.node.DynamicCalibration)

# Link the cameras to the DynamicCalibration
left_out.link(dyn_calib.left)
right_out.link(dyn_calib.right)

device = pipeline.getDefaultDevice()
calibration = device.readCalibration()
device.setCalibration(calibration)

pipeline.start()
while pipeline.isRunning():
    ...
```

#### Sending Commands to the Node

Nodes in DepthAI communicate via input/output message queues. The DynamicCalibration node has several queues, but the most
important for control is the inputControl queue.

```python
# Initialize the command input queue
command_input = dyn_calib.inputControl.createInputQueue()
# Example command of sending a 
command_input.send(dai.DynamicCalibrationControl.startCalibration())
```

Available Commands

 * `StartCalibration()` - Starts the calibration process.
 * `StopCalibration()` - Stops the calibration process.
 * `Calibrate(force=False, keepCameraCenters=True)` - Computes a new calibration based on the loaded data. * force - no
   restriction on loaded data
    * keepCameraCenters - keep the current camera centers fixed. Set this to `False` for free-form / unknown-baseline calibration.
 * `CalibrationQuality(force=False)` - Legacy quality-check command. * force - no restriction on loaded data
 * `LoadImage()` - Load one image from the device.
 * `ComputeCalibrationMetrics(calibration)` - Compute calibration metrics as `dataConfidence` and `calibrationConfidence`.
 * `ApplyCalibration(calibration)` - Apply calibration to the device.
 * `SetPerformanceMode(performanceMode)` - Send performance mode which will be used.
 * `ResetData()` - Remove all previously loaded data.

#### Receiving Data from the Node

The node provides multiple output queues:

 * `coverageOutput` → coverage statistics
   ([CoverageData](https://docs.luxonis.com/software-v3/depthai/depthai-components/messages/coverage_data.md))
 * `calibrationOutput` → calibration results
   ([DynamicCalibrationResult](https://docs.luxonis.com/software-v3/depthai/depthai-components/messages/dynamic_calibration_result.md))
 * `qualityOutput` → legacy calibration quality output
   ([CalibrationQuality](https://docs.luxonis.com/software-v3/depthai/depthai-components/messages/calibration_quality.md))
 * `metricsOutput` → calibration statistics and data quality
   ([CalibrationMetrics](https://docs.luxonis.com/software-v3/depthai/depthai-components/messages/calibration_metrics.md))

```python
# queue for recieving new calibration
calibration_output = dyn_calib.calibrationOutput.createOutputQueue()
# queue for revieving the coverage
coverage_output = dyn_calib.coverageOutput.createOutputQueue()
# queue for checking the calibration quality 
quality_output = dyn_calib.qualityOutput.createOutputQueue()
# queue for checking the calibration metrics
metrics_output = dyn_calib.metricsOutput.createOutputQueue()
```

Check references of `DynamicCalibrationResult`, `CoverageData`, `CalibrationQuality` and `CalibrationMetrics` to see exact data
structures of the outputs.

For runtime calibration-health checks, prefer `ComputeCalibrationMetrics(...)` and `metricsOutput`.

> **Note**
> From `DepthAI 3.8.0`, `CalibrationQuality(...)` should be treated as deprecated. The node still accepts the command for compatibility, but current runtimes return an empty `CalibrationQuality` result on `qualityOutput` and log a deprecation warning.

Replace this API with:

 * Old: `CalibrationQuality()`
 * New: `calibrate()` or `startCalibration()`
 * Old: `dai.DynamicCalibrationControl.calibrationQuality()` or `dai.DynamicCalibrationControl.Commands.CalibrationQuality()`
 * New: `dai.DynamicCalibrationControl.calibrate()` or `dai.DynamicCalibrationControl.startCalibration()`
 * Read comparison data from: `DynamicCalibrationResult.calibrationData.calibrationDifference` on `calibrationOutput`

#### Reading Coverage Data

Coverage data is received via `coverageOutput` when an image is loaded either manually (with `LoadImage` command) or during
continuous calibration (after running `StartCalibration` command).

> **Note**
> Coverage is available as `coveragePerCell`, a map keyed by `CameraBoardSocket`. The older `coveragePerCellA` / `coveragePerCellB` fields remain only as deprecated compatibility aliases for the first two connected sensors and should be considered for removal in future versions.

Manual Image Load

```python
# Load a single image
command_input.send(dai.DynamicCalibrationControl.loadImage())

# Get coverage after loading
coverage = coverage_output.get()
print(f"Coverage = {coverage.meanCoverage}")
```

Continuous Collection During Calibration

```python
command_input.send(dai.DynamicCalibrationControl.startCalibration())

while pipeline.isRunning():
    # Blocking read
    coverage = coverage_output.get()
    print(f"Coverage = {coverage.meanCoverage}")

    # Non-blocking read
    coverage = coverage_output.tryGet()
    if coverage:
        print(f"Coverage = {coverage.meanCoverage}")
```

#### Reading Calibration Data

Calibration results can be obtained from:

 * `dai.DynamicCalibrationControl.startCalibration()` — starts collecting data and attempts calibration.
 * `dai.DynamicCalibrationControl.calibrate(force=False, keepCameraCenters=True)` — calibrates with existing loaded data (image
   must be loaded beforehand with `LoadImage` command as can be seen in the example below).

Calibration data will be returned as
[DynamicCalibrationResult](https://docs.luxonis.com/software-v3/depthai/depthai-components/messages/dynamic_calibration_result.md)
message type. When present, the payload is stored under `calibrationData`, which contains `newCalibration`, `currentCalibration`,
`calibrationDifference`, and `dataConfidence`.

`CalibrationQuality` and `DynamicCalibrationResult` can report `pairwiseRotationDifference`, keyed by socket pairs. This is the
recommended rotation-delta output to inspect, including for pairwise calibration, because it stays consistent with the newer
socket-based result model. Treat the older single `rotationChange` field as legacy compatibility output.

Manual Image Load

```python
# Load a single image
command_input.send(dai.DynamicCalibrationControl.loadImage())

# Send a command to calibrate
command_input.send(dai.DynamicCalibrationControl.calibrate(force=False))

# Get calibration after loading
calibration = calibration_output.get()
print(f"Calibration = {calibration.info}")
```

Continuous Collection

```python
# Starts collecting data and attempts calibration
command_input.send(dai.DynamicCalibrationControl.startCalibration())

while pipeline.isRunning():
    # Blocking read
    calibration = calibration_output.get()
    print(f"Calibration = {calibration.info}")

    # Non-blocking read
    calibration = calibration_output.tryGet()
    if calibration:
        print(f"Calibration = {calibration.info}")
```

#### Performance Modes

Set the performance mode with

```python
# Set performance mode
dynCalibInputControl.send(
    dai.DynamicCalibrationControl.setPerformanceMode(
        dai.DynamicCalibrationControl.PerformanceMode.OPTIMIZE_PERFORMANCE
    )
)
```

The performance mode sets the amount of data needed for the calibration.

```python
dai.DynamicCalibrationControl.PerformanceMode.OPTIMIZE_PERFORMANCE  # The most strict mode
dai.DynamicCalibrationControl.PerformanceMode.DEFAULT               # Less strict but mostly sufficient
dai.DynamicCalibrationControl.PerformanceMode.OPTIMIZE_SPEED        # Optimize speed over performance
dai.DynamicCalibrationControl.PerformanceMode.STATIC_SCENERY        # Not strict
dai.DynamicCalibrationControl.PerformanceMode.SKIP_CHECKS           # Skip all internal checks
dai.DynamicCalibrationControl.PerformanceMode.RELAXED_COVERAGE      # Allow lower coverage before calibration actions
```

#### Multi-sensor and FFC workflows

The current multi-sensor work extends DynamicCalibration beyond pairwise stereo recalibration:

 * Use `dynCalib.left` and `dynCalib.right` for the main stereo pair.
 * Optionally connect `dynCalib.rgb` for a 3-sensor workflow such as `CAM_A + CAM_B + CAM_C`.
 * Prefer `keepCameraCenters=True` when the baseline is already known and only orientation drift should be corrected.
 * Use `keepCameraCenters=False` for free-form recalibration where translations must also be solved, such as new FFC layouts
   without a reliable baseline.
 * Read coverage from `coveragePerCell` instead of assuming only two sensors are present.

## Examples

#### Dynamic Calibration Interactive Visualizer

With the following commands you can clone and run the calibration integration.

```bash
git clone https://github.com/luxonis/depthai-core.git
cd depthai-core/
python3 -m venv venv
source venv/bin/activate
python3 examples/python/install_requirements.py
python3 examples/python/DynamicCalibration/calibration_integration.py
```

#### Calibration Metrics Check

For application-level health checks and recalibration decisions, prefer the metrics flow over `CalibrationQuality`.

#### Python

Run this example by following the [README on
Github](https://github.com/luxonis/depthai-core/blob/main/examples/python/DynamicCalibration/README.md).

```python
import depthai as dai
import numpy as np
import time
import cv2

def print_metrics(metrics: dai.CalibrationQualityData) -> None:
    for socket_pair, rotation in metrics.pairwiseRotationDifference.items():
        print(f"Pairwise rotation difference {socket_pair} = {rotation} deg")
    print(f"Mean Sampson error achievable = {metrics.sampsonErrorNew:.3f} px")
    print(f"Mean Sampson error current    = {metrics.sampsonErrorCurrent:.3f} px")

# ---------- Pipeline definition ----------
with dai.Pipeline() as pipeline:
    # Create camera nodes
    monoLeft  = pipeline.create(dai.node.Camera).build(dai.CameraBoardSocket.CAM_B)
    monoRight = pipeline.create(dai.node.Camera).build(dai.CameraBoardSocket.CAM_C)

    # Request full resolution NV12 outputs
    monoLeftOut  = monoLeft.requestFullResolutionOutput()
    monoRightOut = monoRight.requestFullResolutionOutput()

    # Initialize the DynamicCalibration node
    dynCalib = pipeline.create(dai.node.DynamicCalibration)

    # Link the cameras to the DynamicCalibration
    monoLeftOut.link(dynCalib.left)
    monoRightOut.link(dynCalib.right)

    stereo = pipeline.create(dai.node.StereoDepth)
    monoLeftOut.link(stereo.left)
    monoRightOut.link(stereo.right)

    # Queues
    syncedLeftQueue  = stereo.syncedLeft.createOutputQueue()
    syncedRightQueue = stereo.syncedRight.createOutputQueue()
    disparityQueue = stereo.disparity.createOutputQueue()

    # Initialize the command output queues for coverage and calibration output
    dynCalibCoverageQueue = dynCalib.coverageOutput.createOutputQueue()
    dynCalibCalibrationQueue = dynCalib.calibrationOutput.createOutputQueue()

    # Initialize the command input queue
    dynCalibInputControl = dynCalib.inputControl.createInputQueue()

    device = pipeline.getDefaultDevice()
    device.setCalibration(device.getCalibration())

    # Setup the colormap for visualization
    colorMap = cv2.applyColorMap(np.arange(256, dtype=np.uint8), cv2.COLORMAP_JET)
    colorMap[0] = [0, 0, 0]  # to make zero-disparity pixels black
    maxDisparity = 1

    pipeline.start()
    time.sleep(1) # wait for auto exposure to settle

    while pipeline.isRunning():
        leftSynced  = syncedLeftQueue.get()
        rightSynced = syncedRightQueue.get()
        disparity = disparityQueue.get()

        cv2.imshow("left", leftSynced.getCvFrame())
        cv2.imshow("right", rightSynced.getCvFrame())

        # --- Disparity visualization ---
        npDisparity = disparity.getFrame()
        curMax = float(np.max(npDisparity))
        if curMax > 0:
            maxDisparity = max(maxDisparity, curMax)
        normalized = (npDisparity / (maxDisparity if maxDisparity > 0 else 1.0) * 255.0).astype(np.uint8)
        colorizedDisparity = cv2.applyColorMap(normalized, cv2.COLORMAP_JET)
        colorizedDisparity[normalized == 0] = (0, 0, 0)
        cv2.imshow("disparity", colorizedDisparity)

        # --- Load one frame into calibration & read coverage
        dynCalibInputControl.send(dai.DynamicCalibrationControl.loadImage())
        coverage = dynCalibCoverageQueue.get()
        if coverage is not None:
            print(f"2D Spatial Coverage = {coverage.meanCoverage} / 100 [%]")
            print(f"Data Acquired       = {coverage.dataAcquired} / 100 [%]")

        # --- Run one-shot calibration & read metrics from calibrationOutput
        dynCalibInputControl.send(dai.DynamicCalibrationControl.calibrate(False))
        dynCalibrationResult = dynCalibCalibrationQueue.get()
        if dynCalibrationResult is not None:
            print(f"Dynamic calibration status: {dynCalibrationResult.info}")
            if dynCalibrationResult.calibrationData:
                print("Successfully evaluated metrics from calibration output")
                print_metrics(dynCalibrationResult.calibrationData.calibrationDifference)
                dynCalibInputControl.send(dai.DynamicCalibrationControl.resetData())

        key = cv2.waitKey(1)
        if key == ord('q'):
            pipeline.stop()
            break
```

#### C++

Example from our
[Github](https://github.com/luxonis/depthai-core/blob/main/examples/cpp/DynamicCalibration/calibration_quality_dynamic.cpp):

```cpp
#include <chrono>
#include <iostream>
#include <opencv2/opencv.hpp>
#include <thread>

#include "depthai/depthai.hpp"

namespace {
void printMetrics(const dai::CalibrationQuality::Data& q) {
    for(const auto& [socketPair, rotation] : q.pairwiseRotationDifference) {
        std::cout << "Pairwise rotation difference " << static_cast<int>(socketPair.first) << " -> " << static_cast<int>(socketPair.second) << " = [";
        for(std::size_t i = 0; i < rotation.size(); ++i) {
            if(i > 0) std::cout << ", ";
            std::cout << rotation[i];
        }
        std::cout << "] deg" << std::endl;
    }
    std::cout << "Mean Sampson error achievable = " << q.sampsonErrorNew << " px" << std::endl;
    std::cout << "Mean Sampson error current    = " << q.sampsonErrorCurrent << " px" << std::endl;
}
}  // namespace

int main() {
    auto device = std::make_shared<dai::Device>();

    // ---------- Pipeline definition ----------
    dai::Pipeline pipeline(device);

    auto monoLeft = pipeline.create<dai::node::Camera>()->build(dai::CameraBoardSocket::CAM_B);
    auto monoRight = pipeline.create<dai::node::Camera>()->build(dai::CameraBoardSocket::CAM_C);

    auto* leftOut = monoLeft->requestFullResolutionOutput();
    auto* rightOut = monoRight->requestFullResolutionOutput();

    // Dynamic-calibration node
    auto dynCalib = pipeline.create<dai::node::DynamicCalibration>();
    leftOut->link(dynCalib->left);
    rightOut->link(dynCalib->right);

    auto stereo = pipeline.create<dai::node::StereoDepth>();
    leftOut->link(stereo->left);
    rightOut->link(stereo->right);

    // In-pipeline host queues
    auto leftSyncedQueue = stereo->syncedLeft.createOutputQueue();
    auto rightSyncedQueue = stereo->syncedRight.createOutputQueue();
    auto disparityQueue = stereo->disparity.createOutputQueue();

    auto dynCoverageOutQ = dynCalib->coverageOutput.createOutputQueue();
    auto dynCalibOutQ = dynCalib->calibrationOutput.createOutputQueue();
    auto dynCalibInputControl = dynCalib->inputControl.createInputQueue();

    device->setCalibration(device->getCalibration());

    pipeline.start();
    std::this_thread::sleep_for(std::chrono::seconds(1));  // wait for autoexposure to settle

    using DCC = dai::DynamicCalibrationControl;

    while(pipeline.isRunning()) {
        auto leftSynced = leftSyncedQueue->get<dai::ImgFrame>();
        auto rightSynced = rightSyncedQueue->get<dai::ImgFrame>();
        auto disparity = disparityQueue->get<dai::ImgFrame>();

        cv::imshow("left", leftSynced->getCvFrame());
        cv::imshow("right", rightSynced->getCvFrame());

        // --- Load one frame pair into the calibration pipeline
        dynCalibInputControl->send(DCC::loadImage());

        // Wait for coverage info
        auto coverageMsg = dynCoverageOutQ->get<dai::CoverageData>();
        if(coverageMsg) {
            std::cout << "2D Spatial Coverage = " << coverageMsg->meanCoverage << " / 100 [%]" << std::endl;
            std::cout << "Data Acquired       = " << coverageMsg->dataAcquired << " / 100 [%]" << std::endl;
        }

        // Run one-shot calibration and inspect the returned metrics.
        dynCalibInputControl->send(DCC::calibrate(false));

        // Wait for calibration result
        auto dynCalibrationResult = dynCalibOutQ->get<dai::DynamicCalibrationResult>();
        if(dynCalibrationResult) {
            std::cout << "Dynamic calibration status: " << dynCalibrationResult->info << std::endl;

            if(dynCalibrationResult->calibrationData) {
                std::cout << "Successfully evaluated metrics from calibration output." << std::endl;
                printMetrics(dynCalibrationResult->calibrationData->calibrationDifference);
                dynCalibInputControl->send(DCC::resetData());
            }
        } else {
            std::cout << "Dynamic calibration: no result received." << std::endl;
        }

        int key = cv::waitKey(1);
        if(key == 'q') break;
    }

    return 0;
}
```

#### Dynamic Calibration

#### Python

Run this example by following the [README on
Github](https://github.com/luxonis/depthai-core/blob/main/examples/python/DynamicCalibration/README.md).

```python
import depthai as dai
import numpy as np
import time
import cv2

# ---------- Pipeline definition ----------
with dai.Pipeline() as pipeline:
    # Cameras
    monoLeft  = pipeline.create(dai.node.Camera).build(dai.CameraBoardSocket.CAM_B)
    monoRight = pipeline.create(dai.node.Camera).build(dai.CameraBoardSocket.CAM_C)

    # Full-res NV12 outputs
    monoLeftOut  = monoLeft.requestFullResolutionOutput()
    monoRightOut = monoRight.requestFullResolutionOutput()

    # Initialize the DynamicCalibration node
    dynCalib = pipeline.create(dai.node.DynamicCalibration)

    # Link the cameras to the DynamicCalibration
    monoLeftOut.link(dynCalib.left)
    monoRightOut.link(dynCalib.right)

    # Stereo (for disparity + synced previews)
    stereo = pipeline.create(dai.node.StereoDepth)
    monoLeftOut.link(stereo.left)
    monoRightOut.link(stereo.right)

    # Output queues
    syncedLeftQueue  = stereo.syncedLeft.createOutputQueue()
    syncedRightQueue = stereo.syncedRight.createOutputQueue()
    disparityQueue   = stereo.disparity.createOutputQueue()

    # Initialize the command output queues for calibration and coverage
    dynCalibCalibrationQueue = dynCalib.calibrationOutput.createOutputQueue()
    dynCalibCoverageQueue    = dynCalib.coverageOutput.createOutputQueue()

    # Initialize the command input queue
    dynCalibInputControl = dynCalib.inputControl.createInputQueue()

    device = pipeline.getDefaultDevice()
    device.setCalibration(device.getCalibration())

    # Setup the colormap for visualization
    colorMap = cv2.applyColorMap(np.arange(256, dtype=np.uint8), cv2.COLORMAP_JET)
    colorMap[0] = [0, 0, 0]  # to make zero-disparity pixels black
    maxDisparity = 1.0

    pipeline.start()
    time.sleep(1) # wait for auto exposure to settle

    # Set performance mode
    dynCalibInputControl.send(
        dai.DynamicCalibrationControl.setPerformanceMode(
            dai.DynamicCalibrationControl.PerformanceMode.OPTIMIZE_PERFORMANCE
        )
    )

    # Start periodic calibration
    dynCalibInputControl.send(
        dai.DynamicCalibrationControl.startCalibration()
    )

    while pipeline.isRunning():
        leftSynced  = syncedLeftQueue.get()
        rightSynced = syncedRightQueue.get()
        disparity = disparityQueue.get()

        cv2.imshow("left", leftSynced.getCvFrame())
        cv2.imshow("right", rightSynced.getCvFrame())

        # --- Disparity visualization ---
        npDisparity = disparity.getFrame()
        curMax = float(np.max(npDisparity))
        if curMax > 0:
            maxDisparity = max(maxDisparity, curMax)

        # Normalize to [0,255] and colorize; keep zero-disparity as black
        denom = maxDisparity if maxDisparity > 0 else 1.0
        normalized = (npDisparity / denom * 255.0).astype(np.uint8)
        colorizedDisparity = cv2.applyColorMap(normalized, cv2.COLORMAP_JET)
        colorizedDisparity[normalized == 0] = (0, 0, 0)
        cv2.imshow("disparity", colorizedDisparity)

        # --- Coverage (non-blocking) ---
        coverage = dynCalibCoverageQueue.tryGet()
        if coverage is not None:
            print(f"2D Spatial Coverage = {coverage.meanCoverage} / 100 [%]")
            print(f"Data Acquired       = {coverage.dataAcquired} / 100 [%]")

        # --- Calibration result (non-blocking) ---
        dynCalibrationResult = dynCalibCalibrationQueue.tryGet()
        calibrationData = dynCalibrationResult.calibrationData if dynCalibrationResult is not None else None

        if dynCalibrationResult is not None:
            print(f"Dynamic calibration status: {dynCalibrationResult.info}")

        # --- Apply calibration if available, print quality deltas, then reset+continue ---
        if calibrationData:
            print("Successfully calibrated")
            # Apply to device
            dynCalibInputControl.send(
                dai.DynamicCalibrationControl.applyCalibration(calibrationData.newCalibration)
            )

            q = calibrationData.calibrationDifference
            if q.pairwiseRotationDifference:
                pairwiseRotation = next(iter(q.pairwiseRotationDifference.values()))
                rotDiff = float(np.sqrt(sum(axis * axis for axis in pairwiseRotation)))
                print(f"Rotation difference: || r_current - r_new || = {rotDiff:.2f} deg")
            print(f"Mean Sampson error achievable = {q.sampsonErrorNew:.3f} px")
            print(f"Mean Sampson error current    = {q.sampsonErrorCurrent:.3f} px")

            # Reset accumulators and continue periodic calibration
            dynCalibInputControl.send(
                dai.DynamicCalibrationControl.resetData()
            )
            dynCalibInputControl.send(
                dai.DynamicCalibrationControl.startCalibration()
            )

        key = cv2.waitKey(1)
        if key == ord('q'):
            pipeline.stop()
            break
```

#### C++

Example from our
[Github](https://github.com/luxonis/depthai-core/blob/main/examples/cpp/DynamicCalibration/calibration_dynamic.cpp):

```cpp
// examples/cpp/DynamicCalibration/calibrate.cpp
#include <algorithm>
#include <chrono>
#include <cmath>
#include <iomanip>
#include <iostream>
#include <opencv2/opencv.hpp>
#include <thread>

#include "depthai/depthai.hpp"

int main() {
    auto device = std::make_shared<dai::Device>();

    // ---------- Pipeline definition ----------
    dai::Pipeline pipeline(device);

    auto monoLeft = pipeline.create<dai::node::Camera>()->build(dai::CameraBoardSocket::CAM_B);
    auto monoRight = pipeline.create<dai::node::Camera>()->build(dai::CameraBoardSocket::CAM_C);

    auto* leftOut = monoLeft->requestFullResolutionOutput();
    auto* rightOut = monoRight->requestFullResolutionOutput();

    // Dynamic-calibration node
    auto dynCalib = pipeline.create<dai::node::DynamicCalibration>();
    leftOut->link(dynCalib->left);
    rightOut->link(dynCalib->right);

    auto stereo = pipeline.create<dai::node::StereoDepth>();
    leftOut->link(stereo->left);
    rightOut->link(stereo->right);

    // In-pipeline host queues
    auto leftSyncedQueue = stereo->syncedLeft.createOutputQueue();
    auto rightSyncedQueue = stereo->syncedRight.createOutputQueue();
    auto disparityQueue = stereo->disparity.createOutputQueue();

    auto dynCalibOutQ = dynCalib->calibrationOutput.createOutputQueue();
    auto dynCoverageOutQ = dynCalib->coverageOutput.createOutputQueue();

    auto dynCalibInputControl = dynCalib->inputControl.createInputQueue();

    device->setCalibration(device->getCalibration());

    pipeline.start();
    std::this_thread::sleep_for(std::chrono::seconds(1));  // wait for autoexposure to settle

    using DCC = dai::DynamicCalibrationControl;
    // Optionally set performance mode:
    dynCalibInputControl->send(DCC::setPerformanceMode(DCC::PerformanceMode::OPTIMIZE_PERFORMANCE));

    // Start calibration (optimize performance)
    dynCalibInputControl->send(DCC::startCalibration());

    double maxDisparity = 1.0;
    while(pipeline.isRunning()) {
        auto leftSynced = leftSyncedQueue->get<dai::ImgFrame>();
        auto rightSynced = rightSyncedQueue->get<dai::ImgFrame>();
        auto disparity = disparityQueue->get<dai::ImgFrame>();

        cv::imshow("left", leftSynced->getCvFrame());
        cv::imshow("right", rightSynced->getCvFrame());

        cv::Mat npDisparity = disparity->getFrame();

        double minVal = 0.0, curMax = 0.0;
        cv::minMaxLoc(npDisparity, &minVal, &curMax);
        maxDisparity = std::max(maxDisparity, curMax);

        // Normalize the disparity image to an 8-bit scale.
        cv::Mat normalized;
        npDisparity.convertTo(normalized, CV_8UC1, 255.0 / (maxDisparity > 0 ? maxDisparity : 1.0));

        cv::Mat colorizedDisparity;
        cv::applyColorMap(normalized, colorizedDisparity, cv::COLORMAP_JET);

        // Set pixels with zero disparity to black.
        colorizedDisparity.setTo(cv::Scalar(0, 0, 0), normalized == 0);

        cv::imshow("disparity", colorizedDisparity);

        // Coverage (non-blocking)
        if(auto coverageMsg = dynCoverageOutQ->tryGet<dai::CoverageData>()) {
            std::cout << "2D Spatial Coverage = " << coverageMsg->meanCoverage << "  / 100 [%]\n";
            std::cout << "Data Acquired       = " << coverageMsg->dataAcquired << "  / 100 [%]\n";
        }

        // Calibration result (non-blocking)
        if(auto dynCalibrationResult = dynCalibOutQ->tryGet<dai::DynamicCalibrationResult>()) {
            std::cout << "Dynamic calibration status: " << dynCalibrationResult->info << std::endl;

            if(dynCalibrationResult->calibrationData) {
                std::cout << "Successfully calibrated." << std::endl;

                // Apply the produced calibration
                const auto& newCalib = dynCalibrationResult->calibrationData->newCalibration;
                dynCalibInputControl->send(DCC::applyCalibration(newCalib));

                // Print quality deltas
                const auto& q = dynCalibrationResult->calibrationData->calibrationDifference;

                if(!q.pairwiseRotationDifference.empty()) {
                    const auto& pairwiseRotation = q.pairwiseRotationDifference.begin()->second;
                    float rotDiff = 0.0f;
                    for(const auto axis : pairwiseRotation) rotDiff += axis * axis;
                    rotDiff = std::sqrt(rotDiff);
                    std::cout << "Rotation difference: " << rotDiff << " deg\n";
                }
                std::cout << "Mean Sampson error achievable = " << q.sampsonErrorNew << " px\n";
                std::cout << "Mean Sampson error current    = " << q.sampsonErrorCurrent << " px\n";

                // Reset and start a new round if desired
                dynCalibInputControl->send(DCC::startCalibration());
            }
        }

        int key = cv::waitKey(1);
        if(key == 'q') break;
    }

    return 0;
}
```

#### Dynamic Calibration (3 sensors)

The feature branch adds a 3-sensor example that connects `CAM_A`, `CAM_B`, and `CAM_C` to the DynamicCalibration node and reads
socket-keyed outputs.

```python
import depthai as dai

with dai.Pipeline() as pipeline:
    rgb = pipeline.create(dai.node.Camera).build(dai.CameraBoardSocket.CAM_A)
    monoLeft = pipeline.create(dai.node.Camera).build(dai.CameraBoardSocket.CAM_B)
    monoRight = pipeline.create(dai.node.Camera).build(dai.CameraBoardSocket.CAM_C)

    rgbOut = rgb.requestOutput((1280, 800), fps=30)
    monoLeftOut = monoLeft.requestFullResolutionOutput()
    monoRightOut = monoRight.requestFullResolutionOutput()

    dynCalib = pipeline.create(dai.node.DynamicCalibration)
    rgbOut.link(dynCalib.rgb)
    monoLeftOut.link(dynCalib.left)
    monoRightOut.link(dynCalib.right)

    coverageQueue = dynCalib.coverageOutput.createOutputQueue()
    calibrationQueue = dynCalib.calibrationOutput.createOutputQueue()
    inputControl = dynCalib.inputControl.createInputQueue()

    inputControl.send(
        dai.DynamicCalibrationControl.setPerformanceMode(
            dai.DynamicCalibrationControl.PerformanceMode.OPTIMIZE_PERFORMANCE
        )
    )
    inputControl.send(
        dai.DynamicCalibrationControl.startCalibration(
            loadImagePeriod=0.5,
            calibrationPeriod=5.0,
            keepCameraCenters=True,
        )
    )

    coverage = coverageQueue.get()
    print(coverage.coveragePerCell.keys())

    result = calibrationQueue.get()
    if result.calibrationData is not None:
        print(result.calibrationData.calibrationDifference.pairwiseRotationDifference)
```

This workflow is especially relevant for FFC devices where calibration needs to remain consistent across more than one camera
pair.

> **Note**
> Dynamic Calibration will not fully restore the factory-specified absolute depth accuracy. Use factory tools for production-grade re-calibration.

For more information please follow the example's README.

## Scenery guidelines

Good calibration scenes make it easier for algorithms to detect, match, and track features. Recommended characteristics:

 * Include textured objects at multiple depths.
 * Avoid blank walls or featureless surfaces.
 * Slowly move the camera to cover the full FOV; avoid sudden motions.

| Recommendation | Original Image VS. Feature Coverage (in green) |
| --- | --- |
| ✅Ensure rich texture and visual detail - rich textures, edges, and objects evenly distributed across FOV create ideal
calibration conditions | |
| 🚫Avoid flat or featureless surfaces - lack of textured surfaces or visually distinct objects provide little usable features | |
| 🚫Avoid reflective and transparent surfaces - reflective and transparent surfaces create false 3D features |  |
| 🚫Avoid dark scenes - low contrast, shadows, and poorly lit scenes yield few detectable features |  |
| 🚫Avoid repetitive patterns - many patterned regions look too similar to tell apart |  |

## PerformanceMode tuning

| Mode | When to use |
| --- | --- |
| `DEFAULT` | Balanced accuracy vs. speed. |
| `STATIC_SCENERY` | Camera is fixed, scene stable. |
| `OPTIMIZE_SPEED` | Fastest calibration, reduced precision. |
| `OPTIMIZE_PERFORMANCE` | Maximum precision in feature-rich scenes. |
| `SKIP_CHECKS` | Automated pipelines, were internal check to guarantee scene quality are ignored. |

> **Note**
> For highest accuracy, combine OPTIMIZE_PERFORMANCE with a dynamic, well-featured environment.

By combining appropriate scenery with the correct PerformanceMode, users can significantly improve calibration reliability and
depth estimation quality.

## Limitations & notes

 * Supported devices — Dynamic Calibration is available for: * All stereo OAK Series 2 cameras (excluding FFC)
    * All stereo OAK Series 4 cameras
 * DepthAI version — requires DepthAI 3.0 or later.
 * Re-calibrated parameters — updates extrinsics only; intrinsics remain unchanged.
 * OS support — Available on Linux, MacOS and Windows.
 * Absolute depth spec — DCL improves relative depth perception; absolute accuracy may still differ slightly from the original
   factory spec.

## Troubleshooting

| Symptom | Possible cause | Fix |
| --- | --- | --- |
| High reprojection error | Incorrect model name or HFOV in board config | Verify board JSON and camera specs |
| Depth still incorrect after “successful” DCL | Left / right cameras swapped | Swap sockets or update board config and
recalibrate |
| `nullopt` quality report | Insufficient scene coverage | Move camera to capture richer textures |
| Runtime error: `"The calibration on the device is too old to perform DynamicCalibration, full re-calibration required!"` | The
device calibration is too outdated for dynamic recalibration to provide any benefit. | A newer device is needed |

## See also

 * [General Dynamic Calibration information page](https://docs.luxonis.com/hardware/platform/depth/dynamic-calibration.md)
 * [Manual stereo and ToF calibration guide](https://docs.luxonis.com/hardware/platform/depth/manual-calibration.md)
 * If you'd like automatic calibration operation, have a look at the [AutoCalibration documentation
   page](https://docs.luxonis.com/software-v3/depthai/depthai-components/host_nodes/auto_calibration.md).
