ds-msp 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ds_msp-0.3.0/LICENSE +21 -0
- ds_msp-0.3.0/PKG-INFO +508 -0
- ds_msp-0.3.0/README.md +478 -0
- ds_msp-0.3.0/ds_msp/__init__.py +47 -0
- ds_msp-0.3.0/ds_msp/adapt/__init__.py +7 -0
- ds_msp-0.3.0/ds_msp/adapt/convert.py +93 -0
- ds_msp-0.3.0/ds_msp/adapt/evaluate.py +54 -0
- ds_msp-0.3.0/ds_msp/adapt/sampling.py +24 -0
- ds_msp-0.3.0/ds_msp/calib/__init__.py +19 -0
- ds_msp-0.3.0/ds_msp/calib/bundle.py +147 -0
- ds_msp-0.3.0/ds_msp/calib/detect.py +99 -0
- ds_msp-0.3.0/ds_msp/calib/targets.py +99 -0
- ds_msp-0.3.0/ds_msp/core/__init__.py +14 -0
- ds_msp-0.3.0/ds_msp/core/contracts.py +120 -0
- ds_msp-0.3.0/ds_msp/core/pinhole.py +30 -0
- ds_msp-0.3.0/ds_msp/cv.py +296 -0
- ds_msp-0.3.0/ds_msp/io/__init__.py +17 -0
- ds_msp-0.3.0/ds_msp/io/kalibr.py +131 -0
- ds_msp-0.3.0/ds_msp/ldc.py +193 -0
- ds_msp-0.3.0/ds_msp/model.py +460 -0
- ds_msp-0.3.0/ds_msp/models/__init__.py +27 -0
- ds_msp-0.3.0/ds_msp/models/double_sphere.py +115 -0
- ds_msp-0.3.0/ds_msp/models/ds_math.py +169 -0
- ds_msp-0.3.0/ds_msp/models/eucm.py +96 -0
- ds_msp-0.3.0/ds_msp/models/eucm_math.py +93 -0
- ds_msp-0.3.0/ds_msp/models/kb.py +105 -0
- ds_msp-0.3.0/ds_msp/models/kb_math.py +132 -0
- ds_msp-0.3.0/ds_msp/models/ocam.py +105 -0
- ds_msp-0.3.0/ds_msp/models/ocam_math.py +169 -0
- ds_msp-0.3.0/ds_msp/models/radtan.py +94 -0
- ds_msp-0.3.0/ds_msp/models/radtan_math.py +123 -0
- ds_msp-0.3.0/ds_msp/models/ucm.py +91 -0
- ds_msp-0.3.0/ds_msp/models/ucm_math.py +95 -0
- ds_msp-0.3.0/ds_msp/ops/__init__.py +6 -0
- ds_msp-0.3.0/ds_msp/ops/pose.py +50 -0
- ds_msp-0.3.0/ds_msp/ops/undistort.py +89 -0
- ds_msp-0.3.0/ds_msp/testing.py +204 -0
- ds_msp-0.3.0/ds_msp/utils.py +130 -0
- ds_msp-0.3.0/ds_msp.egg-info/PKG-INFO +508 -0
- ds_msp-0.3.0/ds_msp.egg-info/SOURCES.txt +45 -0
- ds_msp-0.3.0/ds_msp.egg-info/dependency_links.txt +1 -0
- ds_msp-0.3.0/ds_msp.egg-info/requires.txt +7 -0
- ds_msp-0.3.0/ds_msp.egg-info/top_level.txt +1 -0
- ds_msp-0.3.0/pyproject.toml +85 -0
- ds_msp-0.3.0/setup.cfg +4 -0
- ds_msp-0.3.0/tests/test_ds_camera_cv.py +117 -0
- ds_msp-0.3.0/tests/test_robustness_and_ldc.py +193 -0
ds_msp-0.3.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Black_and_White
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
ds_msp-0.3.0/PKG-INFO
ADDED
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ds_msp
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Double Sphere (and multi-model) fisheye camera library with model conversion
|
|
5
|
+
Author: Munna-Manoj
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Munna-Manoj/DS-MSP
|
|
8
|
+
Project-URL: Repository, https://github.com/Munna-Manoj/DS-MSP
|
|
9
|
+
Project-URL: Issues, https://github.com/Munna-Manoj/DS-MSP/issues
|
|
10
|
+
Keywords: fisheye,camera-model,calibration,double-sphere,computer-vision,slam,opencv,kannala-brandt
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering :: Image Recognition
|
|
19
|
+
Classifier: Operating System :: OS Independent
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: numpy
|
|
24
|
+
Requires-Dist: opencv-python
|
|
25
|
+
Requires-Dist: scipy
|
|
26
|
+
Requires-Dist: pyyaml
|
|
27
|
+
Provides-Extra: calib
|
|
28
|
+
Requires-Dist: aprilgrid; extra == "calib"
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
|
|
31
|
+
# DS-MSP — Double Sphere & Multi-Model Fisheye Camera Library
|
|
32
|
+
|
|
33
|
+
[](https://github.com/Munna-Manoj/DS-MSP/actions/workflows/ci.yml)
|
|
34
|
+
[](https://github.com/Munna-Manoj/DS-MSP)
|
|
35
|
+
[](LICENSE)
|
|
36
|
+

|
|
37
|
+
|
|
38
|
+
A clean, tested, **OpenCV-compatible** camera library for wide-FOV (fisheye) lenses — built around the
|
|
39
|
+
**Double Sphere** model (Usenko et al. 2018) and a uniform multi-model layer, with analytic Jacobians,
|
|
40
|
+
calibration, model conversion, and hardware export. It doubles as a **guided, runnable course** in
|
|
41
|
+
wide-FOV camera geometry.
|
|
42
|
+
|
|
43
|
+

|
|
44
|
+
|
|
45
|
+
> *A real fisheye frame (left) rectified to a pinhole view (right), sweeping the `balance` knob from
|
|
46
|
+
> widest-FOV to tightest-crop. The bent ceiling lines and curved checkerboard straighten out.*
|
|
47
|
+
|
|
48
|
+
> **Two ways in — pick yours:**
|
|
49
|
+
> - 🎓 **Learn the geometry** → start the runnable curriculum in **[`docs/learn/`](docs/learn/README.md)**.
|
|
50
|
+
> Each chapter prints a number you can verify; the **[🏆 capstone](docs/learn/capstone_calibrating_a_real_camera.md)**
|
|
51
|
+
> calibrates a real fisheye from TUM-VI footage and matches the *published* intrinsics to ~1 % (0.12 px median).
|
|
52
|
+
> - 🛠️ **Use the library** → jump to **[Installation](#installation)** and **[Quick start](#quick-start)**.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Table of contents
|
|
57
|
+
|
|
58
|
+
- [Why DS-MSP](#why-ds-msp)
|
|
59
|
+
- [Installation](#installation)
|
|
60
|
+
- [Quick start](#quick-start)
|
|
61
|
+
- [Repository map](#repository-map)
|
|
62
|
+
- [Learn: the guided curriculum](#learn-the-guided-curriculum)
|
|
63
|
+
- [Using the library](#using-the-library)
|
|
64
|
+
- [Create a camera](#create-a-camera)
|
|
65
|
+
- [Project & unproject (+ analytic Jacobian)](#project--unproject--analytic-jacobian)
|
|
66
|
+
- [Undistort images](#undistort-images)
|
|
67
|
+
- [Robust PnP](#robust-pnp)
|
|
68
|
+
- [Multi-model support & conversion](#multi-model-support--conversion)
|
|
69
|
+
- [Hardware LDC export (TI Jacinto)](#hardware-ldc-export-ti-jacinto)
|
|
70
|
+
- [Calibration](#calibration)
|
|
71
|
+
- [Deep dive: FOV, validity & undistortion](#deep-dive-fov-validity--undistortion)
|
|
72
|
+
- [Accuracy & verification](#accuracy--verification)
|
|
73
|
+
- [FAQ](#faq)
|
|
74
|
+
- [Roadmap](#roadmap)
|
|
75
|
+
- [Credits](#credits)
|
|
76
|
+
- [License](#license)
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Why DS-MSP
|
|
81
|
+
|
|
82
|
+
Fisheye lenses capture a very wide field of view — often **> 180°** — by deliberately bending straight
|
|
83
|
+
lines. The familiar **pinhole** model can't describe that, and worse, its `X/Z` projection blows up as
|
|
84
|
+
rays approach 90°. DS-MSP implements the models that *can*, and does it carefully:
|
|
85
|
+
|
|
86
|
+
| | What you get |
|
|
87
|
+
| :-- | :-- |
|
|
88
|
+
| **Correct wide-FOV geometry** | Double Sphere with the exact `z > -w₂·d₁` half-space validity test — handles the full **> 180° FOV**, not the naive `z > 0` check that silently clips it. |
|
|
89
|
+
| **One interface, many models** | UCM, EUCM, Kannala-Brandt (= OpenCV fisheye), RadTan (= OpenCV pinhole), OCamCalib, Double Sphere — all behind a single `CameraModel` contract. |
|
|
90
|
+
| **Analytic Jacobians** | Exact closed-form derivatives (no autodiff, no finite differences) → faster, more robust calibration. KB & RadTan match OpenCV to ~1e-13. |
|
|
91
|
+
| **Model conversion** | Translate a calibration between models **without images or recalibration** (sample → unproject → LM refit). |
|
|
92
|
+
| **Calibration** | Generic Levenberg–Marquardt bundle adjustment for *any* model, with a robust (Cauchy) loss option. |
|
|
93
|
+
| **Ecosystem fluency** | Read/write **Kalibr** camchain YAML; OpenCV-style drop-in API; **TI Jacinto** LDC hardware mesh export. |
|
|
94
|
+
| **Verified, CI-tested** | 171 tests + import-linter layer checks + mypy, green on Python 3.10–3.12. |
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Installation
|
|
99
|
+
|
|
100
|
+
Requires **Python ≥ 3.10**.
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
git clone https://github.com/Munna-Manoj/DS-MSP.git
|
|
104
|
+
cd DS-MSP
|
|
105
|
+
|
|
106
|
+
# core library (editable install)
|
|
107
|
+
pip install -e .
|
|
108
|
+
|
|
109
|
+
# …or with the AprilGrid detector used by the calibration capstone
|
|
110
|
+
pip install -e ".[calib]"
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Verify:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
python -c "import ds_msp; print('DS-MSP loaded:', ds_msp.__name__)"
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
> Prefer isolation? `python -m venv .venv && source .venv/bin/activate` (or `uv venv`) first.
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## Quick start
|
|
124
|
+
|
|
125
|
+
A camera model is just two maps — **project** (3D → 2D) and **unproject** (2D → 3D) — plus a handful of
|
|
126
|
+
intrinsics. They are exact inverses:
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
import numpy as np
|
|
130
|
+
from ds_msp import DoubleSphereCamera
|
|
131
|
+
|
|
132
|
+
# 6 intrinsics fully describe the lens (width/height are optional, only for image ops)
|
|
133
|
+
cam = DoubleSphereCamera(fx=711.57, fy=711.24, cx=949.18, cy=518.81, xi=0.183, alpha=0.809)
|
|
134
|
+
|
|
135
|
+
pts_3d = np.array([[0.0, 0.0, 1.0], [1.0, 1.0, 2.0]]) # camera-frame points (N, 3)
|
|
136
|
+
px, ok = cam.project(pts_3d) # -> (N, 2) pixels + (N,) validity mask
|
|
137
|
+
rays, ok = cam.unproject(px) # -> (N, 3) unit rays (inverse of project)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
**Want to see it on real data?** With the `[calib]` extra and the TUM-VI download
|
|
141
|
+
(`bash scripts/download_datasets.sh tumvi`), calibrate a real fisheye from scratch and match the
|
|
142
|
+
published reference:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
python examples/03_calibrate_tumvi_aprilgrid.py
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Repository map
|
|
151
|
+
|
|
152
|
+
| Path | Contents |
|
|
153
|
+
| :-- | :-- |
|
|
154
|
+
| [`ds_msp/`](ds_msp) | The library: `core/` contracts → pure math → `models/` → services (`ops/`, `adapt/`, `io/`, `calib/`), plus `cv.py` (OpenCV-style API) and `ldc.py` (hardware export). |
|
|
155
|
+
| [`examples/`](examples) | Five runnable demos on real TUM-VI data (`01`–`05`) — round-trip precision, the calibration capstone, robust-loss A/B, model equivalence. |
|
|
156
|
+
| [`docs/learn/`](docs/learn/README.md) | The guided curriculum (start here to learn). |
|
|
157
|
+
| [`docs/`](docs) | [`MULTI_MODEL.md`](docs/MULTI_MODEL.md) (multi-model + conversion guide), [`ROADMAP.md`](docs/ROADMAP.md), [`WRITING_GUIDE.md`](docs/WRITING_GUIDE.md) (docs style guide). |
|
|
158
|
+
| [`datasets/`](datasets/README.md) | Data guide: what to download, where it goes, how to start. |
|
|
159
|
+
| [`tests/`](tests) | 171 tests (contract suite, analytic-Jacobian gradient checks, calibration). |
|
|
160
|
+
|
|
161
|
+
The library is **strictly layered** (enforced in CI by import-linter): `core` depends on nothing, the
|
|
162
|
+
service layers depend only on the contract — not on concrete models or each other — and the pure-math
|
|
163
|
+
modules are NumPy-only.
|
|
164
|
+
|
|
165
|
+
```mermaid
|
|
166
|
+
graph TD
|
|
167
|
+
services["services: ops · adapt · calib · io<br/>(work on any model via the contract)"]
|
|
168
|
+
models["models: DoubleSphere · UCM · EUCM · KB · RadTan · OCam<br/>(value object + pure-NumPy *_math)"]
|
|
169
|
+
core["core: CameraModel contract · pinhole<br/>(dependency-free foundation)"]
|
|
170
|
+
services --> core
|
|
171
|
+
models -. implements .-> core
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
*(Full diagram and design guarantees in [`docs/MULTI_MODEL.md`](docs/MULTI_MODEL.md#6-architecture--design-guarantees).)*
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## Learn: the guided curriculum
|
|
179
|
+
|
|
180
|
+
If you want to *understand* wide-FOV geometry (not just call it), the **[`docs/learn/`](docs/learn/README.md)**
|
|
181
|
+
track teaches it on real public data — every chapter prints a number you can verify.
|
|
182
|
+
|
|
183
|
+
| # | Lesson | You'll be able to… |
|
|
184
|
+
| :-- | :-- | :-- |
|
|
185
|
+
| 1 | [Fisheye & camera models](docs/learn/01_fisheye_and_camera_models.md) | load a published calibration, prove project/unproject invert to ~1e-14 px, rectify a real frame |
|
|
186
|
+
| 2 | [The Double Sphere model](docs/learn/02_double_sphere_model.md) | derive DS from first principles and read it in code |
|
|
187
|
+
| 🏆 | [**Capstone: calibrate a real camera**](docs/learn/capstone_calibrating_a_real_camera.md) | detect AprilGrid corners, bundle-adjust, and **match TUM-VI's published intrinsics to ~1 %** |
|
|
188
|
+
| 🔬 | [Robust losses & evaluation](docs/learn/robust_losses_and_evaluation.md) | handle outliers without discarding data; why median/inlier RMS beat naive RMS |
|
|
189
|
+
| 🔬 | [Are two models the same camera?](docs/learn/are_two_models_the_same_camera.md) | prove DS `fx≈152` and KB `fx≈191` describe the same lens |
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## Using the library
|
|
194
|
+
|
|
195
|
+
> Full multi-model cookbook (every operation, on every model) lives in
|
|
196
|
+
> **[`docs/MULTI_MODEL.md`](docs/MULTI_MODEL.md)**. The essentials:
|
|
197
|
+
|
|
198
|
+
### Create a camera
|
|
199
|
+
|
|
200
|
+
The model needs **only the 6 intrinsics** for projection / unprojection / PnP. `width` and `height` are
|
|
201
|
+
optional — required *only* by image-level helpers (which raise a clear error if missing).
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
from ds_msp import DoubleSphereCamera
|
|
205
|
+
|
|
206
|
+
# (a) math-only — no meaningless image dimensions required
|
|
207
|
+
cam = DoubleSphereCamera(fx=711.57, fy=711.24, cx=949.18, cy=518.81, xi=0.183, alpha=0.809)
|
|
208
|
+
|
|
209
|
+
# (b) with dimensions, for image undistortion
|
|
210
|
+
cam = DoubleSphereCamera(711.57, 711.24, 949.18, 518.81, 0.183, 0.809, width=1920, height=1080)
|
|
211
|
+
|
|
212
|
+
# (c) from a calibration result
|
|
213
|
+
cam = DoubleSphereCamera.from_json("results/calibration_params.json")
|
|
214
|
+
|
|
215
|
+
K, D = cam.K, cam.D # 3×3 intrinsic matrix and [xi, alpha]
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
> `alpha` is validated to `[0, 1]` at construction; keep `xi` in `[-1, 1]` for the well-posed domain.
|
|
219
|
+
|
|
220
|
+
### Project & unproject (+ analytic Jacobian)
|
|
221
|
+
|
|
222
|
+
```python
|
|
223
|
+
import numpy as np
|
|
224
|
+
pts_3d = np.array([[0, 0, 1], [1, 1, 2]], dtype=np.float64)
|
|
225
|
+
|
|
226
|
+
px, valid = cam.project(pts_3d) # (N,2) pixels + validity (correct half-space test)
|
|
227
|
+
rays, valid = cam.unproject(px) # (N,3) unit rays + validity
|
|
228
|
+
|
|
229
|
+
# Hot loops: allocation-free standalone functions + exact derivatives
|
|
230
|
+
from ds_msp import ds_project
|
|
231
|
+
from ds_msp.model import ds_project_jacobian
|
|
232
|
+
|
|
233
|
+
u, v, valid = ds_project(pts_3d, cam.fx, cam.fy, cam.cx, cam.cy, cam.xi, cam.alpha)
|
|
234
|
+
# J_point = d(u,v)/d(x,y,z), J_intr = d(u,v)/d(fx,fy,cx,cy,xi,alpha)
|
|
235
|
+
u, v, J_point, J_intr, valid = ds_project_jacobian(
|
|
236
|
+
pts_3d, cam.fx, cam.fy, cam.cx, cam.cy, cam.xi, cam.alpha)
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Undistort images
|
|
240
|
+
|
|
241
|
+
Drop-in OpenCV-style API (`ds_msp.cv` mirrors `cv2.fisheye`):
|
|
242
|
+
|
|
243
|
+
```python
|
|
244
|
+
import cv2, ds_msp.cv as ds_cv
|
|
245
|
+
|
|
246
|
+
img = cv2.imread("assets/test_image.jpg")
|
|
247
|
+
K, D = cam.K, cam.D
|
|
248
|
+
|
|
249
|
+
# balance=0.0 -> widest FOV (more scene, black borders); balance=1.0 -> tightest crop
|
|
250
|
+
K_new = ds_cv.estimateNewCameraMatrixForUndistortRectify(K, D, (1920, 1080), balance=0.0)
|
|
251
|
+
img_undist = ds_cv.undistortImage(img, K, D, Knew=K_new)
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
The object API is equivalent: `img_undist, K_new = cam.undistort_image(img)`.
|
|
255
|
+
|
|
256
|
+
### Robust PnP
|
|
257
|
+
|
|
258
|
+
Standard PnP assumes a pinhole model and fails on raw fisheye points. The DS solver unprojects to rays
|
|
259
|
+
first, keeps the front-facing valid rays, then solves:
|
|
260
|
+
|
|
261
|
+
```python
|
|
262
|
+
success, rvec, tvec = cam.solve_pnp(points_3d, points_2d) # object API
|
|
263
|
+
success, rvec, tvec = ds_cv.solvePnP(points_3d, points_2d, cam.K, cam.D) # OpenCV-style
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### Multi-model support & conversion
|
|
267
|
+
|
|
268
|
+
Calibrate in one model and translate to another **without images or recalibration**:
|
|
269
|
+
|
|
270
|
+
```python
|
|
271
|
+
import json
|
|
272
|
+
from ds_msp import DoubleSphereModel, KannalaBrandtModel, convert, Undistorter, solve_pnp
|
|
273
|
+
|
|
274
|
+
ds = DoubleSphereModel.from_dict(json.load(open("results/calibration_params.json")))
|
|
275
|
+
|
|
276
|
+
kb, report = convert(ds, KannalaBrandtModel, width=1920, height=1080) # DS -> OpenCV fisheye
|
|
277
|
+
print(report["rms_px"]) # sub-pixel agreement across the image
|
|
278
|
+
|
|
279
|
+
# every feature works on any model — swapping models is a one-line change
|
|
280
|
+
solve_pnp(kb, object_points, image_points)
|
|
281
|
+
img_rect, K_new = Undistorter(kb, 1920, 1080).undistort_image(img)
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
Supported: **UCM, EUCM, Kannala-Brandt, RadTan, OCamCalib, Double Sphere** — all with analytic
|
|
285
|
+
Jacobians. You can also calibrate any model (`ds_msp.calib.calibrate`) and read/write **Kalibr YAML**
|
|
286
|
+
(`ds_msp.io`). Conversion design follows
|
|
287
|
+
[Fisheye-Calib-Adapter](https://github.com/eowjd0512/fisheye-calib-adapter) (see [Credits](#credits)).
|
|
288
|
+
|
|
289
|
+
### Hardware LDC export (TI Jacinto)
|
|
290
|
+
|
|
291
|
+
Generate a displacement-mesh LUT for the on-chip Lens Distortion Correction engine (J7 / TDA4):
|
|
292
|
+
|
|
293
|
+
```python
|
|
294
|
+
from ds_msp.ldc import TI_LDC_MeshGenerator
|
|
295
|
+
|
|
296
|
+
gen = TI_LDC_MeshGenerator(cam) # cam built with width/height
|
|
297
|
+
res = gen.generate_mesh_and_intrinsics(1920, 1080, downsample_factor=4, balance=0.5)
|
|
298
|
+
mesh_lut, K_new = res["mesh_lut"], res["K_new"] # int16 Q3 displacements + rectified intrinsics
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
> **Best practice:** use the LDC image for the picture, but undistort *keypoints* with the closed-form
|
|
302
|
+
> `cam.undistort_points(pts, K_new)` at the **same `balance`**. The mesh point-inverse is exact at the
|
|
303
|
+
> center but diverges toward the periphery; sharing `K_new` keeps the two consistent to ~0.1 px.
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
## Calibration
|
|
308
|
+
|
|
309
|
+
Two paths, depending on your data:
|
|
310
|
+
|
|
311
|
+
**1 — The modern, generic calibrator** (`ds_msp.calib`) works for *any* model and is what the
|
|
312
|
+
[capstone](docs/learn/capstone_calibrating_a_real_camera.md) uses on real TUM-VI AprilGrid footage:
|
|
313
|
+
|
|
314
|
+
```python
|
|
315
|
+
import glob
|
|
316
|
+
from ds_msp.calib import calibrate, AprilGridTarget, detect_aprilgrid
|
|
317
|
+
from ds_msp.models import KannalaBrandtModel
|
|
318
|
+
|
|
319
|
+
# 1. detect AprilGrid corners in your calibration frames
|
|
320
|
+
frames = sorted(glob.glob("datasets/tumvi/dataset-calib-cam1_512_16/mav0/cam0/data/*.png"))
|
|
321
|
+
detections = detect_aprilgrid(frames, family="t36h11")
|
|
322
|
+
|
|
323
|
+
# 2. turn tag detections into 3D<->2D correspondences (board geometry: 6x6, 88 mm, spacing 0.3)
|
|
324
|
+
target = AprilGridTarget(tag_rows=6, tag_cols=6, tag_size=0.088, tag_spacing=0.3)
|
|
325
|
+
X_world, keypoints, visibility = target.build_correspondences(detections)
|
|
326
|
+
|
|
327
|
+
# 3. bundle-adjust from a generic seed (analytic Jacobian + robust Cauchy loss)
|
|
328
|
+
seed = KannalaBrandtModel(fx=180, fy=180, cx=256, cy=256)
|
|
329
|
+
result = calibrate(seed, X_world, keypoints, visibility, loss="cauchy", f_scale=0.5)
|
|
330
|
+
print(result["rms_px"]) # -> ~0.18 px, matching TUM-VI's published calibration
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
See the full walk-through in the **[calibration capstone](docs/learn/capstone_calibrating_a_real_camera.md)**.
|
|
334
|
+
|
|
335
|
+
**2 — The bundled Double Sphere script** calibrates from COCO-style checkerboard annotations:
|
|
336
|
+
|
|
337
|
+
```bash
|
|
338
|
+
python calibrate.py # reads anns.json -> writes results/calibration_params.json
|
|
339
|
+
python validate.py # visual reprojection check -> results/visualizations/
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
On the bundled data this converges to `fx≈711.6, fy≈711.2, cx≈949.2, cy≈518.8, xi≈0.183, alpha≈0.809`
|
|
343
|
+
at **0.64 px** RMS.
|
|
344
|
+
|
|
345
|
+
> **Parameter domain (important).** The optimizer constrains distortion to the *well-posed* Double
|
|
346
|
+
> Sphere range `α ∈ [0, 1]`, `ξ ∈ [-1, 1]` (matching basalt/Kalibr). Outside it the model becomes
|
|
347
|
+
> non-injective (projection folds back, so unprojection can't invert it); real fisheye lenses sit
|
|
348
|
+
> roughly in `ξ ∈ [-0.2, 0.6]`.
|
|
349
|
+
|
|
350
|
+
---
|
|
351
|
+
|
|
352
|
+
## Deep dive: FOV, validity & undistortion
|
|
353
|
+
|
|
354
|
+
*A common question: "Why are pixels missing from my undistorted image, even when I try to keep the whole image?"*
|
|
355
|
+
|
|
356
|
+
### Undistortion modes
|
|
357
|
+
|
|
358
|
+
Verified on real data (`assets/test_image.jpg`, `assets/test_image_96.jpg`):
|
|
359
|
+
|
|
360
|
+
| Distorted | Undistorted (crop) | Undistorted (whole) | Undistorted (zoom) |
|
|
361
|
+
| :---: | :---: | :---: | :---: |
|
|
362
|
+
|  |  |  |  |
|
|
363
|
+
|  |  |  |  |
|
|
364
|
+
|
|
365
|
+
- **Crop (`balance=1.0`)** — keeps only center-valid pixels: no black borders, less FOV.
|
|
366
|
+
- **Whole (`balance=0.0`)** — keeps all pixels that map to the plane: full content, black borders.
|
|
367
|
+
- **Zoom (reduced focal)** — captures even more wide-angle content, shrinking the center.
|
|
368
|
+
|
|
369
|
+
### Projection validity — the correct condition
|
|
370
|
+
|
|
371
|
+
The Double Sphere projection `π(x)` is **not** valid for all 3D points. The exact projectability test
|
|
372
|
+
(Usenko et al. 2018, Eq. 43–45), implemented in `ds_project`, is the half-space condition:
|
|
373
|
+
|
|
374
|
+
$$z > -w_2\, d_1, \qquad d_1 = \sqrt{x^2 + y^2 + z^2}$$
|
|
375
|
+
|
|
376
|
+
with the two helper terms
|
|
377
|
+
|
|
378
|
+
$$
|
|
379
|
+
w_1 =
|
|
380
|
+
\begin{cases}
|
|
381
|
+
\dfrac{\alpha}{1-\alpha} & \text{if } \alpha \le 0.5 \\
|
|
382
|
+
\dfrac{1-\alpha}{\alpha} & \text{if } \alpha > 0.5
|
|
383
|
+
\end{cases}
|
|
384
|
+
$$
|
|
385
|
+
|
|
386
|
+
$$
|
|
387
|
+
w_2 = \frac{w_1 + \xi}{\sqrt{2\, w_1 \xi + \xi^2 + 1}}
|
|
388
|
+
$$
|
|
389
|
+
|
|
390
|
+
This admits points with **`z ≤ 0`** (rays beyond 90°), which is exactly why the model supports a
|
|
391
|
+
**> 180° FOV**. A naive `z > 0` test — a common implementation mistake — rejects those rays and
|
|
392
|
+
silently caps the FOV below 180°; this library does **not** make that mistake.
|
|
393
|
+
|
|
394
|
+
<details>
|
|
395
|
+
<summary><b>Forward / inverse equations (for reference)</b></summary>
|
|
396
|
+
|
|
397
|
+
$$d_2 = \sqrt{x^2 + y^2 + (\xi d_1 + z)^2}, \qquad
|
|
398
|
+
\begin{bmatrix} u \\ v \end{bmatrix} =
|
|
399
|
+
\begin{bmatrix} f_x\, x / \big(\alpha d_2 + (1-\alpha)(\xi d_1 + z)\big) + c_x \\
|
|
400
|
+
f_y\, y / \big(\alpha d_2 + (1-\alpha)(\xi d_1 + z)\big) + c_y \end{bmatrix}$$
|
|
401
|
+
|
|
402
|
+
Unprojection is closed-form; with $m_x=(u-c_x)/f_x$, $m_y=(v-c_y)/f_y$, $r^2=m_x^2+m_y^2$ it is valid
|
|
403
|
+
for all $r^2$ when $\alpha \le 0.5$, and for $r^2 \le 1/(2\alpha-1)$ when $\alpha > 0.5$.
|
|
404
|
+
|
|
405
|
+
**Valid parameter domain:** $\alpha \in [0, 1]$, $\xi \in [-1, 1]$.
|
|
406
|
+
|
|
407
|
+
</details>
|
|
408
|
+
|
|
409
|
+
### The FOV zones
|
|
410
|
+
|
|
411
|
+

|
|
412
|
+
|
|
413
|
+
- **Green (frontal, `θ < 90°`)** — safe for standard pinhole projection.
|
|
414
|
+
- **Yellow (side/back, `90° ≤ θ < θ_limit`)** — valid in DS, but impossible to project into a single
|
|
415
|
+
pinhole image (`Z ≤ 0`): a pinhole plane is infinite at 90°, so these pixels have nowhere to go.
|
|
416
|
+
- **Red (`θ ≥ θ_limit`)** — mathematically invalid in DS.
|
|
417
|
+
- **White stars** — real keypoints, all safely inside the valid regions.
|
|
418
|
+
|
|
419
|
+
This is why undistortion can't keep a full > 180° FOV: those wide-angle pixels are not lost to a bug,
|
|
420
|
+
they are geometrically un-pinhole-able. *(Reference: [projection-failed region analysis](https://jseobyun.tistory.com/457?category=1170976).)*
|
|
421
|
+
|
|
422
|
+
---
|
|
423
|
+
|
|
424
|
+
## Accuracy & verification
|
|
425
|
+
|
|
426
|
+
Correctness is asserted with **numbers**, not screenshots (`tests/`, `verify_real_samples.py`):
|
|
427
|
+
|
|
428
|
+
| Check | Result |
|
|
429
|
+
| :-- | :-- |
|
|
430
|
+
| Inverse projection `K⁻¹` (all undistortion modes) | mean error **< 0.00003 px** ✅ |
|
|
431
|
+
| 3D reconstruction of checkerboard corners | mean position error **1.168 mm**; recovered square **20.01 cm** (target 20.00) ✅ |
|
|
432
|
+
| PnP + reprojection RMS (real `test_image` / `_96`) | **0.43 px** / **0.85 px** |
|
|
433
|
+
| Undistort: object API vs `cv.py` wrapper | identical |
|
|
434
|
+
| KB / RadTan vs OpenCV | match to **~1e-13** |
|
|
435
|
+
|
|
436
|
+
**Conclusion:** the undistorted images are geometrically accurate pinhole projections suitable for
|
|
437
|
+
precise 3D vision. Reproduce locally with `bash verify_all.sh` or `pytest`; for the accuracy/speed
|
|
438
|
+
numbers above, run **[`python benchmarks/benchmark.py`](benchmarks/)** (e.g. KB vs `cv2.fisheye` to
|
|
439
|
+
~1e-13 px; analytic Jacobian ~28× faster per LM iteration than finite differences).
|
|
440
|
+
|
|
441
|
+
---
|
|
442
|
+
|
|
443
|
+
## FAQ
|
|
444
|
+
|
|
445
|
+
**My undistorted image has black borders?**
|
|
446
|
+
Normal for fisheye — a pinhole view can't capture the full > 180° FOV. Tune `balance` in
|
|
447
|
+
`estimateNewCameraMatrixForUndistortRectify` to trade border vs FOV.
|
|
448
|
+
|
|
449
|
+
**PnP fails or gives bad results?**
|
|
450
|
+
Use `cam.solve_pnp(...)` (or `ds_msp.cv.solvePnP`), not `cv2.solvePnP` — the latter assumes pinhole and
|
|
451
|
+
fails on raw fisheye points. You need ≥ 4 points that are in front of the camera after unprojection.
|
|
452
|
+
|
|
453
|
+
**What ranges are valid for `xi` and `alpha`?**
|
|
454
|
+
`alpha ∈ [0, 1]` (enforced at construction) and `xi ∈ [-1, 1]`. Beyond that the model is non-injective;
|
|
455
|
+
the calibrator constrains to this domain automatically. Real lenses sit in `xi ∈ [-0.2, 0.6]`.
|
|
456
|
+
|
|
457
|
+
**Do I have to pass `width` and `height`?**
|
|
458
|
+
No — only for image-level operations (undistortion maps / `compute_K_new`). Projection, unprojection,
|
|
459
|
+
Jacobians, and PnP need just the 6 intrinsics.
|
|
460
|
+
|
|
461
|
+
**How do I use this with ROS?**
|
|
462
|
+
Wrap `ds_msp.cv.undistortImage` in a node: subscribe to `image_raw`, undistort, publish `image_rect`.
|
|
463
|
+
|
|
464
|
+
---
|
|
465
|
+
|
|
466
|
+
## Roadmap
|
|
467
|
+
|
|
468
|
+
DS-MSP is actively growing from a camera library into a small perception toolkit (multi-camera &
|
|
469
|
+
camera-IMU calibration, visual odometry on public benchmarks, a C++/Ceres core, inference-only learned
|
|
470
|
+
3D). See **[`docs/ROADMAP.md`](docs/ROADMAP.md)** for the build order and design rules.
|
|
471
|
+
|
|
472
|
+
---
|
|
473
|
+
|
|
474
|
+
## Credits
|
|
475
|
+
|
|
476
|
+
This project builds on excellent open-source work and research.
|
|
477
|
+
|
|
478
|
+
**Model conversion (the multi-model adapter)**
|
|
479
|
+
- **Fisheye-Calib-Adapter** — Sangjun Lee, *"Fisheye-Calib-Adapter: An Easy Tool for Fisheye Camera
|
|
480
|
+
Model Conversion"*, arXiv:2407.12405 (2024) ·
|
|
481
|
+
[github.com/eowjd0512/fisheye-calib-adapter](https://github.com/eowjd0512/fisheye-calib-adapter).
|
|
482
|
+
Our conversion design (sample → unproject with the source → linear-seed → nonlinear refine on pixel
|
|
483
|
+
reprojection error, per-model analytic Jacobians) and the set of supported models follow this work.
|
|
484
|
+
|
|
485
|
+
**Camera models**
|
|
486
|
+
- **Double Sphere** — V. Usenko, N. Demmel, D. Cremers, *"The Double Sphere Camera Model"*, 3DV 2018,
|
|
487
|
+
arXiv:1807.08957. Reference: [basalt-headers](https://gitlab.com/VladyslavUsenko/basalt-headers)
|
|
488
|
+
(half-space validity condition & analytic Jacobians follow it).
|
|
489
|
+
- **Kannala-Brandt** (equidistant) — J. Kannala, S. Brandt, 2006; cross-checked vs OpenCV `cv2.fisheye`.
|
|
490
|
+
- **Radial-Tangential (Brown-Conrady)** — D. C. Brown, 1966; cross-checked vs OpenCV `cv2.projectPoints`.
|
|
491
|
+
- **OCamCalib** — D. Scaramuzza et al. · **EUCM** — Khomutenko, Garcia, Martinet, 2016 ·
|
|
492
|
+
**UCM** — Geyer & Daniilidis / Mei & Rives.
|
|
493
|
+
|
|
494
|
+
**Calibration ecosystem & tooling**
|
|
495
|
+
- **Kalibr** — P. Furgale et al., [github.com/ethz-asl/kalibr](https://github.com/ethz-asl/kalibr)
|
|
496
|
+
(DS & EUCM contributed by V. Usenko). We follow Kalibr's `camchain` YAML format for interop.
|
|
497
|
+
- **[dscamera](https://github.com/matsuren/dscamera)** — Python DS utilities.
|
|
498
|
+
- **[Double Sphere explanation](https://jseobyun.tistory.com/455)** &
|
|
499
|
+
**[projection-failed region](https://jseobyun.tistory.com/457?category=1170976)** — clear write-ups.
|
|
500
|
+
|
|
501
|
+
**This codebase**
|
|
502
|
+
- **Muhammadjon Boboev** — original Python Double Sphere intrinsics calibration this project grew from.
|
|
503
|
+
|
|
504
|
+
---
|
|
505
|
+
|
|
506
|
+
## License
|
|
507
|
+
|
|
508
|
+
[MIT](LICENSE).
|