camera-client 0.2.9__tar.gz → 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.
- camera_client-0.3.0/PKG-INFO +308 -0
- camera_client-0.3.0/README.md +278 -0
- camera_client-0.3.0/camera_client/__init__.py +5 -0
- {camera_client-0.2.9 → camera_client-0.3.0}/camera_client/__main__.py +17 -12
- camera_client-0.3.0/camera_client/camera_network.py +211 -0
- {camera_client-0.2.9 → camera_client-0.3.0}/camera_client/client.py +26 -1
- camera_client-0.3.0/camera_client/error_model.py +221 -0
- {camera_client-0.2.9 → camera_client-0.3.0}/camera_client/loading.py +8 -0
- camera_client-0.3.0/camera_client/triangulation.py +109 -0
- camera_client-0.3.0/camera_client.egg-info/PKG-INFO +308 -0
- {camera_client-0.2.9 → camera_client-0.3.0}/camera_client.egg-info/SOURCES.txt +3 -0
- {camera_client-0.2.9 → camera_client-0.3.0}/pyproject.toml +1 -1
- camera_client-0.2.9/PKG-INFO +0 -446
- camera_client-0.2.9/README.md +0 -416
- camera_client-0.2.9/camera_client/__init__.py +0 -3
- camera_client-0.2.9/camera_client.egg-info/PKG-INFO +0 -446
- {camera_client-0.2.9 → camera_client-0.3.0}/LICENSE +0 -0
- {camera_client-0.2.9 → camera_client-0.3.0}/MANIFEST.in +0 -0
- {camera_client-0.2.9 → camera_client-0.3.0}/camera_client/script.py +0 -0
- {camera_client-0.2.9 → camera_client-0.3.0}/camera_client.egg-info/dependency_links.txt +0 -0
- {camera_client-0.2.9 → camera_client-0.3.0}/camera_client.egg-info/entry_points.txt +0 -0
- {camera_client-0.2.9 → camera_client-0.3.0}/camera_client.egg-info/requires.txt +0 -0
- {camera_client-0.2.9 → camera_client-0.3.0}/camera_client.egg-info/top_level.txt +0 -0
- {camera_client-0.2.9 → camera_client-0.3.0}/requirements.txt +0 -0
- {camera_client-0.2.9 → camera_client-0.3.0}/setup.cfg +0 -0
- {camera_client-0.2.9 → camera_client-0.3.0}/setup.py +0 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: camera-client
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Python SDK for camera calibration and projection transformations - handle lens distortion, coordinate transformations, and 3D ray casting with symbolic expressions.
|
|
5
|
+
Author-email: Alexander Abramov <extremal.ru@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/avabr/camera-client
|
|
8
|
+
Project-URL: Repository, https://github.com/avabr/camera-client
|
|
9
|
+
Project-URL: Issues, https://github.com/avabr/camera-client/issues
|
|
10
|
+
Keywords: camera,calibration,projection,3d,ray-casting,computer-vision,lens-distortion,coordinate-transformation,sympy
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
24
|
+
Requires-Python: >=3.7
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Requires-Dist: numpy>=1.20.0
|
|
28
|
+
Requires-Dist: sympy>=1.10.0
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
|
|
31
|
+
# camera-client
|
|
32
|
+
|
|
33
|
+
Python SDK for camera calibration, projection transformations, and multi-camera spatial uncertainty analysis. Transform coordinates between distorted image space, corrected image space, and real-world 3D coordinates using pre-computed calibration data. Quantify measurement uncertainty and fuse observations from multiple cameras.
|
|
34
|
+
|
|
35
|
+
## Features
|
|
36
|
+
|
|
37
|
+
- **Vectorized operations** - Process multiple points simultaneously for high performance
|
|
38
|
+
- **Multiple coordinate systems** - Transform between source (distorted), corrected, and ground (3D world) coordinates
|
|
39
|
+
- **Lens distortion handling** - Correct for camera lens distortion using calibration lookup tables
|
|
40
|
+
- **Ground plane projection** - Project image coordinates to 3D world coordinates and vice versa
|
|
41
|
+
- **Ray casting** - Generate 3D rays from image coordinates for ray tracing and 3D reconstruction
|
|
42
|
+
- **Calibration error models** - Quantify pixel-level uncertainty from distortion correction and geometric calibration
|
|
43
|
+
- **3D spatial covariance** - Propagate pixel uncertainty to full 3x3 covariance matrices in world space
|
|
44
|
+
- **Multi-camera fusion** - Combine measurements from multiple cameras via information fusion
|
|
45
|
+
- **Triangulation** - Recover 3D positions from multi-camera observations with consistency checking
|
|
46
|
+
- **Sympy-based transformations** - Fast compiled symbolic expressions for mathematical transformations
|
|
47
|
+
- **NumPy-based** - Fast array operations with minimal dependencies
|
|
48
|
+
|
|
49
|
+
## Installation
|
|
50
|
+
|
|
51
|
+
Install from PyPI:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pip install camera-client
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## CLI Usage
|
|
58
|
+
|
|
59
|
+
Download camera calibration archives from URL:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
# Download single archive
|
|
63
|
+
python -m camera_client get_camera_archive https://example.com/camera.npz
|
|
64
|
+
|
|
65
|
+
# Download from file with URLs (one per line, non-URL lines ignored)
|
|
66
|
+
python -m camera_client get_camera_archive -f urls.txt -o ./archives
|
|
67
|
+
|
|
68
|
+
# Download from JSON config (list of objects with "archive_url" or "camera_uuid" key)
|
|
69
|
+
python -m camera_client get_camera_archive -f config.json -o ./archives
|
|
70
|
+
|
|
71
|
+
# Download only specific camera from JSON config
|
|
72
|
+
python -m camera_client get_camera_archive -f config.json --camera_id=66 -o ./archives
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
JSON config entries may use `"archive_url"` (direct link) or `"camera_uuid"` (requires
|
|
76
|
+
`CAMERA_SERVICE_ENTRYPOINT` env variable — the URL is constructed as
|
|
77
|
+
`<CAMERA_SERVICE_ENTRYPOINT>/processing_api/projection_npz_archive/<camera_uuid>`).
|
|
78
|
+
|
|
79
|
+
## Quick Start (Projection)
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
import numpy as np
|
|
83
|
+
from camera_client import CameraProjection
|
|
84
|
+
|
|
85
|
+
# Load camera calibration data from NPZ archive
|
|
86
|
+
camera = CameraProjection.load("camera_calibration_archive.npz")
|
|
87
|
+
|
|
88
|
+
# All methods work with (N, 2) or (N, 3) shaped arrays
|
|
89
|
+
source_points = np.array([
|
|
90
|
+
[100, 200],
|
|
91
|
+
[300, 400],
|
|
92
|
+
[500, 600]
|
|
93
|
+
]) # Shape: (3, 2)
|
|
94
|
+
|
|
95
|
+
# ── Forward: image → world ──
|
|
96
|
+
|
|
97
|
+
# Project to ground plane: src → gnd (at height = 0)
|
|
98
|
+
ground_points = camera.src_to_gnd(source_points, h=0)
|
|
99
|
+
print(ground_points) # (N, 3) array with [x, y, z] coordinates
|
|
100
|
+
|
|
101
|
+
# Remove lens distortion only: src → ctd
|
|
102
|
+
ctd_points = camera.src_to_ctd(source_points)
|
|
103
|
+
|
|
104
|
+
# ── Reverse: world → image ──
|
|
105
|
+
|
|
106
|
+
# Project 3D points back to distorted image coordinates
|
|
107
|
+
world_points = np.array([[10.0, 5.0, 0.0], [15.0, 8.0, 1.5]])
|
|
108
|
+
src_points = camera.gnd_to_src(world_points) # gnd → src
|
|
109
|
+
ctd_points = camera.gnd_to_ctd(world_points) # gnd → ctd
|
|
110
|
+
|
|
111
|
+
# Corrected back to distorted
|
|
112
|
+
src_from_ctd = camera.ctd_to_src(ctd_points) # ctd → src
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Quick Start (Measurements)
|
|
116
|
+
|
|
117
|
+
Multi-camera uncertainty analysis: error models, covariance fusion, and triangulation.
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
import numpy as np
|
|
121
|
+
from camera_client import CameraProjection, CameraNetwork, triangulation
|
|
122
|
+
|
|
123
|
+
# Load cameras and create a network
|
|
124
|
+
cameras = [
|
|
125
|
+
CameraProjection.load("camera_1.npz"),
|
|
126
|
+
CameraProjection.load("camera_2.npz"),
|
|
127
|
+
CameraProjection.load("camera_3.npz"),
|
|
128
|
+
]
|
|
129
|
+
net = CameraNetwork(cameras)
|
|
130
|
+
# net.cameras — dict {camera_id: CameraProjection}
|
|
131
|
+
# net.covariances — dict {camera_id: CameraSpatialCovariance}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Spatial covariance
|
|
135
|
+
|
|
136
|
+
`get_covariance` computes 3x3 spatial covariance matrices for 3D points.
|
|
137
|
+
The matrix encodes how pixel-level uncertainty (distortion + geometric calibration + detection)
|
|
138
|
+
propagates into world-space uncertainty through the camera's ray geometry.
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
points = np.array([
|
|
142
|
+
[15.0, 5.0, 0.0],
|
|
143
|
+
[18.0, 6.0, 1.5],
|
|
144
|
+
])
|
|
145
|
+
|
|
146
|
+
# Per-camera covariance (no fusion, no visibility check)
|
|
147
|
+
covs_cam = net.get_covariance(points, camera_id=1177, detection_sigma=0.01)
|
|
148
|
+
# covs_cam[i] is a (3, 3) covariance matrix from camera 1177
|
|
149
|
+
|
|
150
|
+
# Fused covariance from all visible cameras (information fusion)
|
|
151
|
+
covs_fused = net.get_covariance(points, detection_sigma=0.01)
|
|
152
|
+
# covs_fused[i] is (3, 3) fused covariance, or None if not visible to any camera
|
|
153
|
+
|
|
154
|
+
for i, cov in enumerate(covs_fused):
|
|
155
|
+
if cov is not None:
|
|
156
|
+
stds = np.sqrt(np.linalg.eigvalsh(cov))
|
|
157
|
+
print(f"Point {i}: σ = {stds[0]:.3f}m, {stds[1]:.3f}m, {stds[2]:.3f}m")
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Triangulation
|
|
161
|
+
|
|
162
|
+
Recover a 3D point from pixel observations in multiple cameras.
|
|
163
|
+
Returns the fused position, a priori covariance (from error models),
|
|
164
|
+
and a posteriori covariance (inflated if cameras are inconsistent).
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
# Observations: {camera_id: src_point} — source (distorted) image coordinates
|
|
168
|
+
observations = {
|
|
169
|
+
1177: np.array([946.9, 853.1]),
|
|
170
|
+
1178: np.array([956.0, 765.0]),
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
result = net.triangulate(observations, detection_sigma=0.01)
|
|
174
|
+
p_fused, sigma_prior, sigma_posterior = result
|
|
175
|
+
|
|
176
|
+
print(f"Position: {p_fused}")
|
|
177
|
+
print(f"Consistent: {sigma_prior is sigma_posterior}") # True if within n_sigma
|
|
178
|
+
|
|
179
|
+
# Adjust consistency threshold (default n_sigma=3.0)
|
|
180
|
+
result = net.triangulate(observations, n_sigma=2.0)
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Mahalanobis distance
|
|
184
|
+
|
|
185
|
+
Check statistical consistency between two point estimates with their covariances.
|
|
186
|
+
|
|
187
|
+
```python
|
|
188
|
+
d2 = triangulation.mahalanobis_distance(p1, cov1, p2, cov2)
|
|
189
|
+
# d2 is squared Mahalanobis distance; compare to chi-squared thresholds
|
|
190
|
+
# e.g. chi2(3 dof, 99%) ≈ 11.34
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Coordinate Systems
|
|
194
|
+
|
|
195
|
+
This library handles transformations between three coordinate systems:
|
|
196
|
+
|
|
197
|
+
- **src** (Source): Distorted image coordinates from the camera
|
|
198
|
+
- **ctd** (Corrected): Undistorted image coordinates after lens correction
|
|
199
|
+
- **gnd** (Ground): Real-world 3D coordinates (x, y, z)
|
|
200
|
+
|
|
201
|
+
```
|
|
202
|
+
Distorted Undistorted World 3D
|
|
203
|
+
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
|
204
|
+
│ │ │ │ │ │
|
|
205
|
+
│ Source (src) │ <──> │ Corrected (ctd) │ <──> │ Ground (gnd) │
|
|
206
|
+
│ │ │ │ │ │
|
|
207
|
+
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
|
208
|
+
Lens distortion Lens correction 3D projection
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## API Reference
|
|
212
|
+
|
|
213
|
+
### `CameraProjection`
|
|
214
|
+
|
|
215
|
+
| Method | Input | Output | Description |
|
|
216
|
+
|--------|-------|--------|-------------|
|
|
217
|
+
| `load(archive_path)` | str | `CameraProjection` | Load calibration from .npz file |
|
|
218
|
+
| `src_to_ctd(points)` | (N, 2) | (N, 2) | Source → corrected (undistort) |
|
|
219
|
+
| `ctd_to_src(points)` | (N, 2) | (N, 2) | Corrected → source (redistort) |
|
|
220
|
+
| `src_to_gnd(points, h)` | (N, 2), scalar/array | (N, 3) | Source → 3D ground at height h |
|
|
221
|
+
| `gnd_to_src(points)` | (N, 3) | (N, 2) | 3D ground → source |
|
|
222
|
+
| `ctd_to_gnd(points, h)` | (N, 2), scalar/array | (N, 3) | Corrected → 3D ground at height h |
|
|
223
|
+
| `gnd_to_ctd(points)` | (N, 3) | (N, 2) | 3D ground → corrected |
|
|
224
|
+
| `src_to_ray(points)` | (N, 2) | (N, 3) | Source → normalized ray directions |
|
|
225
|
+
| `ctd_to_ray(points)` | (N, 2) | (N, 3) | Corrected → normalized ray directions |
|
|
226
|
+
| `ctd_to_ray_jacobian(x, y)` | scalar, scalar | (3, 2) | Ray direction Jacobian at CTD point |
|
|
227
|
+
| `get_key_point()` | — | (3,) | Camera position in world space |
|
|
228
|
+
| `get_ctd_points_context(points)` | (N, 2) | dict | Scale context (wscale, hscale, vangle) at CTD points |
|
|
229
|
+
| `get_src_points_context(points)` | (N, 2) | dict | Scale context at source points (converts to CTD internally) |
|
|
230
|
+
|
|
231
|
+
All point methods expect 2D arrays. For a single point: `np.array([[x, y]])`.
|
|
232
|
+
Out-of-bounds points return NaN. Height `h` can be a scalar or per-point (N,) array.
|
|
233
|
+
|
|
234
|
+
### `CameraNetwork`
|
|
235
|
+
|
|
236
|
+
| Method | Description |
|
|
237
|
+
|--------|-------------|
|
|
238
|
+
| `CameraNetwork(cameras)` | Create network from list of `CameraProjection` instances |
|
|
239
|
+
| `get_covariance(points, ...)` | Fused 3x3 covariance for (N, 3) points from all visible cameras |
|
|
240
|
+
| `get_covariance(points, camera_id=id, ...)` | Per-camera 3x3 covariance (no fusion, no visibility check) |
|
|
241
|
+
| `triangulate(observations, ...)` | 3D point + covariance from `{camera_id: src_point}` observations |
|
|
242
|
+
|
|
243
|
+
Common parameters: `detection_sigma` (float), `sigma_binding` (float), `use_efov` (bool), `n_sigma` (float).
|
|
244
|
+
|
|
245
|
+
### `triangulation` module
|
|
246
|
+
|
|
247
|
+
| Function | Description |
|
|
248
|
+
|----------|-------------|
|
|
249
|
+
| `fuse_covariances(covs)` | (Σ₁⁻¹ + ... + Σₙ⁻¹)⁻¹ |
|
|
250
|
+
| `information_fusion(points, covs)` | Fused point + covariance |
|
|
251
|
+
| `mahalanobis_distance(p1, cov1, p2, cov2)` | Squared Mahalanobis distance |
|
|
252
|
+
| `least_squares_intersection(rays)` | Closest point to N rays (SVD) |
|
|
253
|
+
| `closest_point_on_ray(origin, dir, point)` | Project point onto ray |
|
|
254
|
+
|
|
255
|
+
## Calibration File Format
|
|
256
|
+
|
|
257
|
+
The calibration file is a NumPy `.npz` archive containing:
|
|
258
|
+
|
|
259
|
+
### Lookup Tables
|
|
260
|
+
- `src2ctd`: Source to corrected coordinate map (H x W x 2)
|
|
261
|
+
- `ctd2src`: Corrected to source coordinate map (H x W x 2)
|
|
262
|
+
- `map_scale_h`: Height scale values (H x W)
|
|
263
|
+
- `map_scale_w`: Width scale values (H x W)
|
|
264
|
+
- `map_scale_vang`: Vertical angle values (H x W)
|
|
265
|
+
|
|
266
|
+
### Symbolic Expressions (stored as strings, parsed with SymPy)
|
|
267
|
+
- `exp_im2gnd`: Image to ground coordinate transformation
|
|
268
|
+
- `exp_gnd2im`: Ground to image coordinate transformation
|
|
269
|
+
- `exp_key_point`: Camera key-point (position) in world space
|
|
270
|
+
- `exp_im2ray`: Image to ray direction transformation
|
|
271
|
+
|
|
272
|
+
### Metadata
|
|
273
|
+
- `format_version`: Version string of the data format
|
|
274
|
+
- `camera_id`: Integer identifier for the camera
|
|
275
|
+
- `plan_url`: URL or path to the ground plan image
|
|
276
|
+
- `plan_scale`: Scale factor for ground plane coordinates (pixels per meter)
|
|
277
|
+
- `plan_width`: Width of the ground plan in pixels
|
|
278
|
+
- `plan_height`: Height of the ground plan in pixels
|
|
279
|
+
- `im_src_url`: URL or path to the source (distorted) camera image
|
|
280
|
+
- `im_ctd_url`: URL or path to the corrected (undistorted) camera image
|
|
281
|
+
- `im_width`: Width of the camera image in pixels
|
|
282
|
+
- `im_height`: Height of the camera image in pixels
|
|
283
|
+
- `ctd_geometry`: JSON object with geometry data in CTD coordinates (efov_polygon, counting_lines)
|
|
284
|
+
|
|
285
|
+
## Requirements
|
|
286
|
+
|
|
287
|
+
- Python >= 3.7
|
|
288
|
+
- NumPy >= 1.20.0
|
|
289
|
+
- SymPy >= 1.10.0
|
|
290
|
+
|
|
291
|
+
## Links
|
|
292
|
+
|
|
293
|
+
- **Repository**: [https://github.com/avabr/camera-client](https://github.com/avabr/camera-client)
|
|
294
|
+
- **Issues**: [https://github.com/avabr/camera-client/issues](https://github.com/avabr/camera-client/issues)
|
|
295
|
+
- **PyPI**: [https://pypi.org/project/camera-client/](https://pypi.org/project/camera-client/)
|
|
296
|
+
|
|
297
|
+
## License
|
|
298
|
+
|
|
299
|
+
MIT License - see [LICENSE](LICENSE) file for details.
|
|
300
|
+
|
|
301
|
+
## Author
|
|
302
|
+
|
|
303
|
+
Alexander V. Abramov ([avabr.me@gmail.com](mailto:avabr.me@gmail.com))
|
|
304
|
+
|
|
305
|
+
## Upload PyPi
|
|
306
|
+
|
|
307
|
+
rm dist/* && python -m build && python -m twine upload dist/*
|
|
308
|
+
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
# camera-client
|
|
2
|
+
|
|
3
|
+
Python SDK for camera calibration, projection transformations, and multi-camera spatial uncertainty analysis. Transform coordinates between distorted image space, corrected image space, and real-world 3D coordinates using pre-computed calibration data. Quantify measurement uncertainty and fuse observations from multiple cameras.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Vectorized operations** - Process multiple points simultaneously for high performance
|
|
8
|
+
- **Multiple coordinate systems** - Transform between source (distorted), corrected, and ground (3D world) coordinates
|
|
9
|
+
- **Lens distortion handling** - Correct for camera lens distortion using calibration lookup tables
|
|
10
|
+
- **Ground plane projection** - Project image coordinates to 3D world coordinates and vice versa
|
|
11
|
+
- **Ray casting** - Generate 3D rays from image coordinates for ray tracing and 3D reconstruction
|
|
12
|
+
- **Calibration error models** - Quantify pixel-level uncertainty from distortion correction and geometric calibration
|
|
13
|
+
- **3D spatial covariance** - Propagate pixel uncertainty to full 3x3 covariance matrices in world space
|
|
14
|
+
- **Multi-camera fusion** - Combine measurements from multiple cameras via information fusion
|
|
15
|
+
- **Triangulation** - Recover 3D positions from multi-camera observations with consistency checking
|
|
16
|
+
- **Sympy-based transformations** - Fast compiled symbolic expressions for mathematical transformations
|
|
17
|
+
- **NumPy-based** - Fast array operations with minimal dependencies
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
Install from PyPI:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install camera-client
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## CLI Usage
|
|
28
|
+
|
|
29
|
+
Download camera calibration archives from URL:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# Download single archive
|
|
33
|
+
python -m camera_client get_camera_archive https://example.com/camera.npz
|
|
34
|
+
|
|
35
|
+
# Download from file with URLs (one per line, non-URL lines ignored)
|
|
36
|
+
python -m camera_client get_camera_archive -f urls.txt -o ./archives
|
|
37
|
+
|
|
38
|
+
# Download from JSON config (list of objects with "archive_url" or "camera_uuid" key)
|
|
39
|
+
python -m camera_client get_camera_archive -f config.json -o ./archives
|
|
40
|
+
|
|
41
|
+
# Download only specific camera from JSON config
|
|
42
|
+
python -m camera_client get_camera_archive -f config.json --camera_id=66 -o ./archives
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
JSON config entries may use `"archive_url"` (direct link) or `"camera_uuid"` (requires
|
|
46
|
+
`CAMERA_SERVICE_ENTRYPOINT` env variable — the URL is constructed as
|
|
47
|
+
`<CAMERA_SERVICE_ENTRYPOINT>/processing_api/projection_npz_archive/<camera_uuid>`).
|
|
48
|
+
|
|
49
|
+
## Quick Start (Projection)
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
import numpy as np
|
|
53
|
+
from camera_client import CameraProjection
|
|
54
|
+
|
|
55
|
+
# Load camera calibration data from NPZ archive
|
|
56
|
+
camera = CameraProjection.load("camera_calibration_archive.npz")
|
|
57
|
+
|
|
58
|
+
# All methods work with (N, 2) or (N, 3) shaped arrays
|
|
59
|
+
source_points = np.array([
|
|
60
|
+
[100, 200],
|
|
61
|
+
[300, 400],
|
|
62
|
+
[500, 600]
|
|
63
|
+
]) # Shape: (3, 2)
|
|
64
|
+
|
|
65
|
+
# ── Forward: image → world ──
|
|
66
|
+
|
|
67
|
+
# Project to ground plane: src → gnd (at height = 0)
|
|
68
|
+
ground_points = camera.src_to_gnd(source_points, h=0)
|
|
69
|
+
print(ground_points) # (N, 3) array with [x, y, z] coordinates
|
|
70
|
+
|
|
71
|
+
# Remove lens distortion only: src → ctd
|
|
72
|
+
ctd_points = camera.src_to_ctd(source_points)
|
|
73
|
+
|
|
74
|
+
# ── Reverse: world → image ──
|
|
75
|
+
|
|
76
|
+
# Project 3D points back to distorted image coordinates
|
|
77
|
+
world_points = np.array([[10.0, 5.0, 0.0], [15.0, 8.0, 1.5]])
|
|
78
|
+
src_points = camera.gnd_to_src(world_points) # gnd → src
|
|
79
|
+
ctd_points = camera.gnd_to_ctd(world_points) # gnd → ctd
|
|
80
|
+
|
|
81
|
+
# Corrected back to distorted
|
|
82
|
+
src_from_ctd = camera.ctd_to_src(ctd_points) # ctd → src
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Quick Start (Measurements)
|
|
86
|
+
|
|
87
|
+
Multi-camera uncertainty analysis: error models, covariance fusion, and triangulation.
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
import numpy as np
|
|
91
|
+
from camera_client import CameraProjection, CameraNetwork, triangulation
|
|
92
|
+
|
|
93
|
+
# Load cameras and create a network
|
|
94
|
+
cameras = [
|
|
95
|
+
CameraProjection.load("camera_1.npz"),
|
|
96
|
+
CameraProjection.load("camera_2.npz"),
|
|
97
|
+
CameraProjection.load("camera_3.npz"),
|
|
98
|
+
]
|
|
99
|
+
net = CameraNetwork(cameras)
|
|
100
|
+
# net.cameras — dict {camera_id: CameraProjection}
|
|
101
|
+
# net.covariances — dict {camera_id: CameraSpatialCovariance}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Spatial covariance
|
|
105
|
+
|
|
106
|
+
`get_covariance` computes 3x3 spatial covariance matrices for 3D points.
|
|
107
|
+
The matrix encodes how pixel-level uncertainty (distortion + geometric calibration + detection)
|
|
108
|
+
propagates into world-space uncertainty through the camera's ray geometry.
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
points = np.array([
|
|
112
|
+
[15.0, 5.0, 0.0],
|
|
113
|
+
[18.0, 6.0, 1.5],
|
|
114
|
+
])
|
|
115
|
+
|
|
116
|
+
# Per-camera covariance (no fusion, no visibility check)
|
|
117
|
+
covs_cam = net.get_covariance(points, camera_id=1177, detection_sigma=0.01)
|
|
118
|
+
# covs_cam[i] is a (3, 3) covariance matrix from camera 1177
|
|
119
|
+
|
|
120
|
+
# Fused covariance from all visible cameras (information fusion)
|
|
121
|
+
covs_fused = net.get_covariance(points, detection_sigma=0.01)
|
|
122
|
+
# covs_fused[i] is (3, 3) fused covariance, or None if not visible to any camera
|
|
123
|
+
|
|
124
|
+
for i, cov in enumerate(covs_fused):
|
|
125
|
+
if cov is not None:
|
|
126
|
+
stds = np.sqrt(np.linalg.eigvalsh(cov))
|
|
127
|
+
print(f"Point {i}: σ = {stds[0]:.3f}m, {stds[1]:.3f}m, {stds[2]:.3f}m")
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Triangulation
|
|
131
|
+
|
|
132
|
+
Recover a 3D point from pixel observations in multiple cameras.
|
|
133
|
+
Returns the fused position, a priori covariance (from error models),
|
|
134
|
+
and a posteriori covariance (inflated if cameras are inconsistent).
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
# Observations: {camera_id: src_point} — source (distorted) image coordinates
|
|
138
|
+
observations = {
|
|
139
|
+
1177: np.array([946.9, 853.1]),
|
|
140
|
+
1178: np.array([956.0, 765.0]),
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
result = net.triangulate(observations, detection_sigma=0.01)
|
|
144
|
+
p_fused, sigma_prior, sigma_posterior = result
|
|
145
|
+
|
|
146
|
+
print(f"Position: {p_fused}")
|
|
147
|
+
print(f"Consistent: {sigma_prior is sigma_posterior}") # True if within n_sigma
|
|
148
|
+
|
|
149
|
+
# Adjust consistency threshold (default n_sigma=3.0)
|
|
150
|
+
result = net.triangulate(observations, n_sigma=2.0)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Mahalanobis distance
|
|
154
|
+
|
|
155
|
+
Check statistical consistency between two point estimates with their covariances.
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
d2 = triangulation.mahalanobis_distance(p1, cov1, p2, cov2)
|
|
159
|
+
# d2 is squared Mahalanobis distance; compare to chi-squared thresholds
|
|
160
|
+
# e.g. chi2(3 dof, 99%) ≈ 11.34
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Coordinate Systems
|
|
164
|
+
|
|
165
|
+
This library handles transformations between three coordinate systems:
|
|
166
|
+
|
|
167
|
+
- **src** (Source): Distorted image coordinates from the camera
|
|
168
|
+
- **ctd** (Corrected): Undistorted image coordinates after lens correction
|
|
169
|
+
- **gnd** (Ground): Real-world 3D coordinates (x, y, z)
|
|
170
|
+
|
|
171
|
+
```
|
|
172
|
+
Distorted Undistorted World 3D
|
|
173
|
+
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
|
174
|
+
│ │ │ │ │ │
|
|
175
|
+
│ Source (src) │ <──> │ Corrected (ctd) │ <──> │ Ground (gnd) │
|
|
176
|
+
│ │ │ │ │ │
|
|
177
|
+
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
|
178
|
+
Lens distortion Lens correction 3D projection
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## API Reference
|
|
182
|
+
|
|
183
|
+
### `CameraProjection`
|
|
184
|
+
|
|
185
|
+
| Method | Input | Output | Description |
|
|
186
|
+
|--------|-------|--------|-------------|
|
|
187
|
+
| `load(archive_path)` | str | `CameraProjection` | Load calibration from .npz file |
|
|
188
|
+
| `src_to_ctd(points)` | (N, 2) | (N, 2) | Source → corrected (undistort) |
|
|
189
|
+
| `ctd_to_src(points)` | (N, 2) | (N, 2) | Corrected → source (redistort) |
|
|
190
|
+
| `src_to_gnd(points, h)` | (N, 2), scalar/array | (N, 3) | Source → 3D ground at height h |
|
|
191
|
+
| `gnd_to_src(points)` | (N, 3) | (N, 2) | 3D ground → source |
|
|
192
|
+
| `ctd_to_gnd(points, h)` | (N, 2), scalar/array | (N, 3) | Corrected → 3D ground at height h |
|
|
193
|
+
| `gnd_to_ctd(points)` | (N, 3) | (N, 2) | 3D ground → corrected |
|
|
194
|
+
| `src_to_ray(points)` | (N, 2) | (N, 3) | Source → normalized ray directions |
|
|
195
|
+
| `ctd_to_ray(points)` | (N, 2) | (N, 3) | Corrected → normalized ray directions |
|
|
196
|
+
| `ctd_to_ray_jacobian(x, y)` | scalar, scalar | (3, 2) | Ray direction Jacobian at CTD point |
|
|
197
|
+
| `get_key_point()` | — | (3,) | Camera position in world space |
|
|
198
|
+
| `get_ctd_points_context(points)` | (N, 2) | dict | Scale context (wscale, hscale, vangle) at CTD points |
|
|
199
|
+
| `get_src_points_context(points)` | (N, 2) | dict | Scale context at source points (converts to CTD internally) |
|
|
200
|
+
|
|
201
|
+
All point methods expect 2D arrays. For a single point: `np.array([[x, y]])`.
|
|
202
|
+
Out-of-bounds points return NaN. Height `h` can be a scalar or per-point (N,) array.
|
|
203
|
+
|
|
204
|
+
### `CameraNetwork`
|
|
205
|
+
|
|
206
|
+
| Method | Description |
|
|
207
|
+
|--------|-------------|
|
|
208
|
+
| `CameraNetwork(cameras)` | Create network from list of `CameraProjection` instances |
|
|
209
|
+
| `get_covariance(points, ...)` | Fused 3x3 covariance for (N, 3) points from all visible cameras |
|
|
210
|
+
| `get_covariance(points, camera_id=id, ...)` | Per-camera 3x3 covariance (no fusion, no visibility check) |
|
|
211
|
+
| `triangulate(observations, ...)` | 3D point + covariance from `{camera_id: src_point}` observations |
|
|
212
|
+
|
|
213
|
+
Common parameters: `detection_sigma` (float), `sigma_binding` (float), `use_efov` (bool), `n_sigma` (float).
|
|
214
|
+
|
|
215
|
+
### `triangulation` module
|
|
216
|
+
|
|
217
|
+
| Function | Description |
|
|
218
|
+
|----------|-------------|
|
|
219
|
+
| `fuse_covariances(covs)` | (Σ₁⁻¹ + ... + Σₙ⁻¹)⁻¹ |
|
|
220
|
+
| `information_fusion(points, covs)` | Fused point + covariance |
|
|
221
|
+
| `mahalanobis_distance(p1, cov1, p2, cov2)` | Squared Mahalanobis distance |
|
|
222
|
+
| `least_squares_intersection(rays)` | Closest point to N rays (SVD) |
|
|
223
|
+
| `closest_point_on_ray(origin, dir, point)` | Project point onto ray |
|
|
224
|
+
|
|
225
|
+
## Calibration File Format
|
|
226
|
+
|
|
227
|
+
The calibration file is a NumPy `.npz` archive containing:
|
|
228
|
+
|
|
229
|
+
### Lookup Tables
|
|
230
|
+
- `src2ctd`: Source to corrected coordinate map (H x W x 2)
|
|
231
|
+
- `ctd2src`: Corrected to source coordinate map (H x W x 2)
|
|
232
|
+
- `map_scale_h`: Height scale values (H x W)
|
|
233
|
+
- `map_scale_w`: Width scale values (H x W)
|
|
234
|
+
- `map_scale_vang`: Vertical angle values (H x W)
|
|
235
|
+
|
|
236
|
+
### Symbolic Expressions (stored as strings, parsed with SymPy)
|
|
237
|
+
- `exp_im2gnd`: Image to ground coordinate transformation
|
|
238
|
+
- `exp_gnd2im`: Ground to image coordinate transformation
|
|
239
|
+
- `exp_key_point`: Camera key-point (position) in world space
|
|
240
|
+
- `exp_im2ray`: Image to ray direction transformation
|
|
241
|
+
|
|
242
|
+
### Metadata
|
|
243
|
+
- `format_version`: Version string of the data format
|
|
244
|
+
- `camera_id`: Integer identifier for the camera
|
|
245
|
+
- `plan_url`: URL or path to the ground plan image
|
|
246
|
+
- `plan_scale`: Scale factor for ground plane coordinates (pixels per meter)
|
|
247
|
+
- `plan_width`: Width of the ground plan in pixels
|
|
248
|
+
- `plan_height`: Height of the ground plan in pixels
|
|
249
|
+
- `im_src_url`: URL or path to the source (distorted) camera image
|
|
250
|
+
- `im_ctd_url`: URL or path to the corrected (undistorted) camera image
|
|
251
|
+
- `im_width`: Width of the camera image in pixels
|
|
252
|
+
- `im_height`: Height of the camera image in pixels
|
|
253
|
+
- `ctd_geometry`: JSON object with geometry data in CTD coordinates (efov_polygon, counting_lines)
|
|
254
|
+
|
|
255
|
+
## Requirements
|
|
256
|
+
|
|
257
|
+
- Python >= 3.7
|
|
258
|
+
- NumPy >= 1.20.0
|
|
259
|
+
- SymPy >= 1.10.0
|
|
260
|
+
|
|
261
|
+
## Links
|
|
262
|
+
|
|
263
|
+
- **Repository**: [https://github.com/avabr/camera-client](https://github.com/avabr/camera-client)
|
|
264
|
+
- **Issues**: [https://github.com/avabr/camera-client/issues](https://github.com/avabr/camera-client/issues)
|
|
265
|
+
- **PyPI**: [https://pypi.org/project/camera-client/](https://pypi.org/project/camera-client/)
|
|
266
|
+
|
|
267
|
+
## License
|
|
268
|
+
|
|
269
|
+
MIT License - see [LICENSE](LICENSE) file for details.
|
|
270
|
+
|
|
271
|
+
## Author
|
|
272
|
+
|
|
273
|
+
Alexander V. Abramov ([avabr.me@gmail.com](mailto:avabr.me@gmail.com))
|
|
274
|
+
|
|
275
|
+
## Upload PyPi
|
|
276
|
+
|
|
277
|
+
rm dist/* && python -m build && python -m twine upload dist/*
|
|
278
|
+
|
|
@@ -36,6 +36,8 @@ def download_archive(url: str, output_dir: str = ".", silent: bool = False) -> b
|
|
|
36
36
|
True if download succeeded, False otherwise
|
|
37
37
|
"""
|
|
38
38
|
try:
|
|
39
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
40
|
+
|
|
39
41
|
# Parse the URL to extract filename from path or Content-Disposition header
|
|
40
42
|
parsed_url = urlparse(url)
|
|
41
43
|
|
|
@@ -76,7 +78,7 @@ def download_archive(url: str, output_dir: str = ".", silent: bool = False) -> b
|
|
|
76
78
|
|
|
77
79
|
if not silent:
|
|
78
80
|
print(f"Successfully downloaded: {filename}")
|
|
79
|
-
print(f"File size: {output_path.stat().st_size}
|
|
81
|
+
print(f"File size: {output_path.stat().st_size / 1024 / 1024:.2f} MB")
|
|
80
82
|
|
|
81
83
|
return True
|
|
82
84
|
|
|
@@ -85,7 +87,7 @@ def download_archive(url: str, output_dir: str = ".", silent: bool = False) -> b
|
|
|
85
87
|
return False
|
|
86
88
|
|
|
87
89
|
|
|
88
|
-
def download_from_file(file_path: str, output_dir: str = ".",
|
|
90
|
+
def download_from_file(file_path: str, output_dir: str = ".", camera_ids: list = None) -> None:
|
|
89
91
|
"""
|
|
90
92
|
Download camera calibration archives from a .txt file with URLs or a .json config.
|
|
91
93
|
|
|
@@ -96,7 +98,7 @@ def download_from_file(file_path: str, output_dir: str = ".", camera_id: int = N
|
|
|
96
98
|
Args:
|
|
97
99
|
file_path: Path to .txt or .json file
|
|
98
100
|
output_dir: Directory to save the downloaded files (default: current directory)
|
|
99
|
-
|
|
101
|
+
camera_ids: If provided, only download archives for these camera_ids (JSON only)
|
|
100
102
|
"""
|
|
101
103
|
try:
|
|
102
104
|
if file_path.endswith('.json'):
|
|
@@ -106,8 +108,8 @@ def download_from_file(file_path: str, output_dir: str = ".", camera_id: int = N
|
|
|
106
108
|
if not isinstance(configs, list):
|
|
107
109
|
configs = [configs]
|
|
108
110
|
|
|
109
|
-
if
|
|
110
|
-
configs = [c for c in configs if c.get('camera_id')
|
|
111
|
+
if camera_ids is not None:
|
|
112
|
+
configs = [c for c in configs if c.get('camera_id') in camera_ids]
|
|
111
113
|
|
|
112
114
|
urls = []
|
|
113
115
|
for c in configs:
|
|
@@ -127,8 +129,8 @@ def download_from_file(file_path: str, output_dir: str = ".", camera_id: int = N
|
|
|
127
129
|
|
|
128
130
|
if not urls:
|
|
129
131
|
msg = f"No matching entries found in {file_path}"
|
|
130
|
-
if
|
|
131
|
-
msg += f" for camera_id={
|
|
132
|
+
if camera_ids is not None:
|
|
133
|
+
msg += f" for camera_id={','.join(str(x) for x in camera_ids)}"
|
|
132
134
|
print(msg, file=sys.stderr)
|
|
133
135
|
sys.exit(1)
|
|
134
136
|
else:
|
|
@@ -196,14 +198,14 @@ def main():
|
|
|
196
198
|
)
|
|
197
199
|
download_parser.add_argument(
|
|
198
200
|
'-o', '--output-dir',
|
|
199
|
-
default='
|
|
200
|
-
help='Output directory (default:
|
|
201
|
+
default='camera_archives/',
|
|
202
|
+
help='Output directory (default: camera_archives/)'
|
|
201
203
|
)
|
|
202
204
|
download_parser.add_argument(
|
|
203
205
|
'--camera_id',
|
|
204
|
-
type=
|
|
206
|
+
type=str,
|
|
205
207
|
default=None,
|
|
206
|
-
help='Filter by camera_id
|
|
208
|
+
help='Filter by camera_id, comma-separated for multiple (e.g. --camera_id=1156,1157)'
|
|
207
209
|
)
|
|
208
210
|
|
|
209
211
|
args = parser.parse_args()
|
|
@@ -220,7 +222,10 @@ def main():
|
|
|
220
222
|
|
|
221
223
|
# Process based on input type
|
|
222
224
|
if args.file:
|
|
223
|
-
|
|
225
|
+
camera_ids = None
|
|
226
|
+
if args.camera_id:
|
|
227
|
+
camera_ids = [int(x.strip()) for x in args.camera_id.split(',')]
|
|
228
|
+
download_from_file(args.file, args.output_dir, camera_ids=camera_ids)
|
|
224
229
|
else:
|
|
225
230
|
success = download_archive(args.url, args.output_dir)
|
|
226
231
|
sys.exit(0 if success else 1)
|