Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -222,4 +222,8 @@ video/demos/*
# UMI specific ignores
datasets/*
checkpoints/*
demos/*
demos/*


workspace/*
*.png
3 changes: 3 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,9 @@ Outcome:
- Make artifact creation deterministic where possible.
- Make errors notebook-friendly and actionable.
- Fail fast on required dependencies. If a feature depends on a package that is required by this repo, import it normally and let missing dependencies fail at import time rather than adding deferred runtime guards. Reserve lazy imports or fallback guards for truly optional integrations only.
- Do not wrap calls that rely on required dependencies in local `ModuleNotFoundError` handling. Call them directly and let the required dependency failure surface naturally.
- In `src/opai/presentation/facade.py`, prefer comprehensive public-function implementations over underscore-prefixed helper members. Keep the facade logic visible in the public functions rather than hiding it behind private abstractions unless the file would otherwise become unworkable.
- Prefer comprehensive workflow implementations over tiny underscore-prefixed helpers across the repo, especially in application modules. Do not extract routine 2-5 line steps into private functions unless they carry real domain meaning, are reused meaningfully, or materially reduce complexity.
- Do not import types from the `typing` module. Python 3.10+ native annotations are the repo standard.
- Prefer built-in generics such as `list[str]`, `dict[str, object]`, `tuple[int, ...]`, and unions like `Path | None`.
- When a protocol-style annotation is needed, import it from `collections.abc` instead of `typing`.
Expand Down
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ run-isort:
run-formatter:
uv run --extra dev ruff format src tests

launch-jupyterlab:
uv run jupyter lab --notebook-dir=workspace --ip=0.0.0.0 --no-browser

clean:
rm -rf .pytest_cache .ruff_cache .venv build dist src/*.egg-info
find . -type d -name "__pycache__" -prune -exec rm -rf {} +
237 changes: 237 additions & 0 deletions docs/calibration-workflow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
# Calibration Workflow With `opai`

This guide documents the notebook-facing camera calibration workflow in `opai`.
The intended flow is:

1. Start an `opai` session.
2. Generate a ChArUco board and print it.
3. Record a calibration video outside `opai`.
4. Run calibration from the recorded video.
5. Optionally verify the saved intrinsics.

The notebook API lives at the package root, so the standard workflow should use `opai.*` directly.

## What `opai` Provides

`opai` currently supports these notebook-facing calibration calls:

```python
ctx = opai.init(name)
board = opai.generate_charuco_board(...)
opai.plot_video_frames(video_path, frame_sample_step=...)
result = opai.calibrate_with_video(...)
verification = opai.verify_calibrated_parameters(...)
```

`opai` does not currently record camera video for you. The video capture step must be done with your normal camera app, CLI tool, or another script, then passed back into `opai` by file path.

## 1. Start A Calibration Session

Every notebook workflow starts by creating the active context:

```python
import opai

ctx = opai.init("camera-calibration")
ctx.session_directory
```

This creates or resumes the session directory at:

```text
.opai_sessions/camera-calibration/
```

All calibration artifacts are written under that directory. If you call `opai.init(...)` again with the same name, `opai` resumes that existing session directory and reuses its manifest.

## 2. Generate A ChArUco Board

Generate the board from the notebook and keep the returned config object. That avoids retyping parameters later.

```python
board = opai.generate_charuco_board(
dictionary="DICT_5X5_100",
squares_x=11,
squares_y=8,
square_length=0.03,
marker_length=0.022,
image_width_px=2000,
image_height_px=1400,
margin_size_px=20,
)

board.image_path
board.config_path
board.config
```

This writes:

- `charuco_board.png`
- `charuco_config.json`

Both files are saved into the active session directory.

### Parameter Notes

- `dictionary` must be a valid OpenCV ArUco dictionary name such as `DICT_5X5_100`.
- `square_length` and `marker_length` must be positive.
- `marker_length` must be smaller than `square_length`.
- `square_length` and `marker_length` should use a real-world unit consistently. Meters are a reasonable default.

### Printing Guidance

- Print the generated board without rescaling it after export.
- Use a flat, rigid backing if possible.
- Measure the printed square size if print scaling is a concern.
- Use the same board for the full calibration run.

## 3. Record The Calibration Video

Record the calibration video outside `opai`, then save it somewhere accessible from the notebook, for example:

```python
video_path = "/path/to/calibration.mp4"
```

Recommended capture guidance:

- Use the same camera, lens, focus mode, and image resolution you plan to use later.
- Move the board through the center, edges, and corners of the image.
- Capture different distances and tilt angles.
- Avoid heavy motion blur and severe occlusion.
- Keep enough frames where the board is fully visible and sharp.

`opai.calibrate_with_video(...)` samples frames from the saved video, so one continuous handheld calibration clip is enough.

## 4. Preview Sampled Frames

Before calibrating, it can help to inspect the frames that `opai` will sample:

```python
opai.plot_video_frames(
video_path=video_path,
frame_sample_step=15,
)
```

Use this to check whether the chosen sampling step keeps enough diverse board poses. `frame_sample_step` must be greater than `0`.

## 5. Run Calibration From The Video

The safest pattern is to reuse the values from `board.config` so the calibration inputs stay consistent with the generated board:

```python
result = opai.calibrate_with_video(
video_path=video_path,
frame_sample_step=15,
row_count=board.config.squares_y,
col_count=board.config.squares_x,
square_length=board.config.square_length,
marker_length=board.config.marker_length,
dictionary=board.config.dictionary,
plot_result=True,
)

result
```

Important mapping:

- `row_count` corresponds to `squares_y`
- `col_count` corresponds to `squares_x`

This workflow:

- samples frames from the video
- detects ChArUco corners
- calibrates a fisheye camera model
- writes `calibration.json` into the session directory

If `plot_result=True`, `opai` also plots the detected ChArUco corners on the frames that were kept for calibration.

## 6. Optional Verification

After calibration, you can verify the saved intrinsics against a calibration video:

```python
verification = opai.verify_calibrated_parameters(
video_path=video_path,
n_check_imgs=10,
charuco_config_json="charuco_config.json",
intrinsics_json="calibration.json",
plot_result=True,
)

verification
```

For verification, relative JSON paths are resolved against the active session directory first. This makes the saved session artifacts convenient to reuse directly from notebook cells.

Verification writes:

- `calibration_verification.json`

## Session Artifacts

After a typical calibration workflow, the session directory will contain files like:

```text
.opai_sessions/camera-calibration/
├── session.json
├── charuco_board.png
├── charuco_config.json
├── calibration.json
└── calibration_verification.json
```

`calibration_verification.json` is only created if you run verification.

## Complete Notebook Example

```python
import opai

ctx = opai.init("camera-calibration")

board = opai.generate_charuco_board(
dictionary="DICT_5X5_100",
squares_x=11,
squares_y=8,
square_length=0.03,
marker_length=0.022,
)

video_path = "/path/to/calibration.mp4"

opai.plot_video_frames(video_path=video_path, frame_sample_step=15)

result = opai.calibrate_with_video(
video_path=video_path,
frame_sample_step=15,
row_count=board.config.squares_y,
col_count=board.config.squares_x,
square_length=board.config.square_length,
marker_length=board.config.marker_length,
dictionary=board.config.dictionary,
plot_result=True,
)

verification = opai.verify_calibrated_parameters(
video_path=video_path,
n_check_imgs=10,
charuco_config_json="charuco_config.json",
intrinsics_json="calibration.json",
plot_result=True,
)
```

## Common Failure Cases

- Calling calibration functions before `opai.init(...)`.
- Passing board dimensions that do not match the generated board.
- Using a different dictionary, `square_length`, or `marker_length` than the printed board.
- Sampling a video that contains too few clear ChArUco detections.
- Using verification video frames whose image size does not match the saved intrinsics.

If you already have frames in memory instead of a video file, you can call `opai.calibrate(...)` directly with a sequence of `numpy.ndarray` frames, but the board parameters still need to match the printed ChArUco board.
4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ authors = [
requires-python = ">=3.10"
dependencies = [
"jupyterlab>=4.5.6",
"opencv-contrib-python>=4.13.0.92",
"matplotlib>=3.10.7",
"numpy>=2.2.6",
"opencv-contrib-python-headless>=4.13.0.92",
"rich>=14.1.0",
]

Expand Down
8 changes: 8 additions & 0 deletions src/opai/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,27 @@
add_mapping,
browse_session,
calibrate,
calibrate_with_video,
generate_charuco_board,
get_context,
init,
list_sessions,
main,
plot_video_frames,
verify_calibrated_parameters,
)

__all__ = [
"add_demos",
"add_mapping",
"browse_session",
"calibrate",
"calibrate_with_video",
"generate_charuco_board",
"get_context",
"init",
"list_sessions",
"main",
"plot_video_frames",
"verify_calibrated_parameters",
]
16 changes: 14 additions & 2 deletions src/opai/application/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
from opai.application.calibration import calibrate
from opai.application.calibration import (
calibrate,
generate_charuco_board,
verify_calibrated_parameters,
)
from opai.application.session import (
add_demos,
add_mapping,
browse_session,
list_sessions,
)

__all__ = ["add_demos", "add_mapping", "browse_session", "calibrate", "list_sessions"]
__all__ = [
"add_demos",
"add_mapping",
"browse_session",
"calibrate",
"generate_charuco_board",
"list_sessions",
"verify_calibrated_parameters",
]
Loading
Loading