> ## Documentation Index
> Fetch the complete documentation index at: https://docs.generalrobotics.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Camera Calibration

> Extrinsic calibration of a fixed camera against a robot arm

## Overview

Camera calibration finds where a **fixed (external) camera** sits relative to a
robot's base — the `camera_wrt_base` transform that skills such as pick-and-place
read to turn what the camera sees into where the arm should move. It runs from
the `grid` CLI, drives the arm hand-guided, and records the result on the
robot's camera automatically.

<Note>
  This calibrates **fixed cameras only** — a camera mounted on a tripod or rig
  looking at the workspace, not one riding the end-effector. The arm moves the
  **target** through the camera's view; the camera stays put.
</Note>

## Step 1: Print the calibration target

The calibration uses a **12×9 radon checkerboard with 16.5 mm squares**. Generate
it with OpenCV's [`gen_pattern.py`](https://github.com/opencv/opencv/blob/master/doc/pattern_tools/gen_pattern.py):

```bash theme={null}
python gen_pattern.py -o radon_checkerboard_12x9_16.5mm.svg \
  --rows 12 --columns 9 --type radon_checkerboard -s 16.5 -m 4 6 4 7 5 6
```

Print it and attach it to a rigid flat board. Make sure:

* **Scale is exact** — print at **100%** (no "fit to page"), then measure a
  printed square; it must be 16.5 mm. Printer scaling silently shrinks the board
  and biases every result.
* There is a **white border** of at least one square-width on all sides.
* The surface is **non-reflective**, with no creases or unevenness.
* The mount is **rigid** (a 3D-printed mount is ideal; tape works). The board
  and camera must never wiggle during a run.

Attach the printed board to the arm's **end-effector**, and mount the camera at
a **fixed** location looking at the workspace. During the run you hand-guide the
arm to move the board through the camera's view.

## Step 2: Run the calibration

Calibration is offered at the end of `robot add`, from the `grid` CLI:

1. Run `robot add` and complete the setup for your robot.
2. At the final step the wizard flags any camera that has not been calibrated.
   Press **`c`** to calibrate now (or **enter** to skip and finish). If more than
   one camera is uncalibrated, pick the one to calibrate.

A window opens at the camera's own size: the camera feed with the detected
target on the **left**, a panel on the **right** with everything you need to
judge the run, and a key strip along the **bottom**. Its keys are in **that
window**, not the terminal:

| Key | Action |
| - | - |
| `SPACE` | Collect a sample |
| `C` | Calibrate and finish |
| `P` | Toggle the collected-point overlay |
| `F` | Toggle full-screen |
| `Q` | Cancel |

Hand-guide the arm to a variety of poses and collect a sample at each. **Hold
the arm still** at the instant you press space, so the pose and the frame match.
15–20 well-distributed samples generally suffice; more is better.

Amber always means *still needed*, never a fault. Red is reserved for the two
things that need you to act: a **consistency** check that failed, and a sample
the run **rejected or dropped**.

<img src="https://mintcdn.com/scaledfoundations/GThFLgpTYUHzqHIt/assets/images/camera_calibration_window.webp?fit=max&auto=format&n=GThFLgpTYUHzqHIt&q=85&s=9eb9b986f4915907f8689be9d661e0ce" alt="Calibration window: the camera view with the detected target, the coverage panel, and the next step" width="1681" height="720" data-path="assets/images/camera_calibration_window.webp" />

### The four meters

| Meter | What it measures | How to fill it |
| - | - | - |
| **Scale** | how much of the frame the board fills | move the board closer to and farther from the camera |
| **Roll** | the board's sideways tilt | tilt the board left and right |
| **Pitch** | the board's forward/back tilt | tilt the board towards and away from the camera |
| **Axes** | how much rotation you have collected **about the least-used axis** | see below |

Each meter is a track: white ticks are the samples you captured, the blue dot is
the pose you are holding now, and the amber band is the range still missing. A
meter gets a green dot once its samples span enough range with no large gap in
the middle, so a cluster of similar poses leaves it an empty ring however many
samples you take.

**Axes** decides whether the solve is well-posed at all. It needs the wrist to
turn about genuinely different axes between samples; if every move is a
variation on the same twist, the solve has nothing to pin the remaining
direction down. To fill it, between samples change **at least two of** these
three, by roughly 30–45° each — and move the board to a different part of the
frame as you go:

* tilt the board forward/back,
* tilt it sideways,
* **twist it in its own plane** (rotate it about the camera's viewing direction
  — this is the one most people skip).

### The live estimate

* **`Est. error`** is the reprojection error your current samples produce,
  re-solved after every capture and shown in px and mm. It is reported, not
  graded — see [Step 3](#step-3-read-the-result). It is
  badged *preliminary* until there are enough samples for it to mean anything.
  Pressing `C` re-solves from exactly these samples, so **the number you watched
  is the number that gets saved**.
* **`Consistency`** shows two checks that compare the data against itself and
  need no camera pose, so they are meaningful within the first few samples.
  **Rotation** red means the arm's end-effector orientation is reported in a
  different convention than assumed, or the board is not rigidly fixed to the
  gripper. **Scale** red means a wrong printed square size, wrong units, or
  wrong camera intrinsics. Neither improves with more samples — stop and fix the
  cause.
* **`Next step`** names the single most useful next action. Follow it and
  ignore everything else.

### Samples the run rejects for you

There is no undo key: bad samples are removed automatically, and every removal
is announced in the panel.

* **Arm was moving** when you pressed space — hold still and re-capture.
* **Corners were not crisp** (blur, or a mis-ordered detection) — steady the
  board and re-capture.
* **Inconsistent sample dropped**, once 8 or more samples exist — one sample
  disagreed with all the others. Re-capture at that pose if you want it back.

<Note>
  If the target isn't detected, confirm it is non-reflective and that the board is
  12×9 **squares**. OpenCV sometimes counts interior corners (one fewer per side).
</Note>

## Step 3: Read the result

Press `C` to solve. The **reprojection error** is the one you were already
watching on the `Est. error` tile — the solve runs over exactly the samples you
kept. The window closes and the terminal prints the final line only: the camera
name and the error, in pixels and in millimetres.

```
Calibrated scene_cam · 6.72 px (≈6.8 mm)
```

The millimetre figure is the pixel error carried out to the board's distance
(`error_px × distance ÷ focal length`). It is how far, per sample, the arm's
reported pose and the camera's view of the board disagree out in the world,
which is the number your task actually cares about.

**The tool does not judge it.** There is no good/bad threshold and no advice to
re-run, because only you know what your application tolerates — a few
millimetres is fine for one task and not another. What does flag a real problem
is the `Consistency` pills and the samples the run rejects for you; both catch
a wrong pose convention, scale or intrinsics without depending on the solve at
all. The CLI records the calibration on the robot's camera and pushes it to the
robot's config. Skills that need the extrinsic then read it over Nexus — no
copying by hand.

`C` is refused, with a notice and you left collecting, only when there is no
solution at all yet — too few samples, or a solve that degenerated. That is the
absence of a result, not a judgement on one.

<Tip>
  If the error stays high despite good coverage, check the arm's end-effector
  pose. A wrong rotation-representation conversion in a robot driver gives the
  end-effector frame the wrong orientation, which the solve cannot recover from.
  A red `Consistency` pill points at the same family of causes — **rot** at the
  pose convention, **scale** at the square size, units, or intrinsics — and says
  so within the first few samples instead of after a full run.
</Tip>


## Related topics

- [ROS2 Communication](/v2.2/simulation/isaac/comms.md)
- [Galaxea R1 Pro Mobile Manipulator](/v2.2/python-api/r1pro/galaxear1pro.md)
- [Ghost Robotics Vision60 + Ghost Arm](/v2.2/python-api/vision60-arm/vision60witharm.md)
- [Ghost Robotics Vision60](/v2.2/python-api/vision60/vision60.md)
- [USBCamera](/v2.2/python-api/cameras/usbcamera.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.