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.
Files changed (26) hide show
  1. camera_client-0.3.0/PKG-INFO +308 -0
  2. camera_client-0.3.0/README.md +278 -0
  3. camera_client-0.3.0/camera_client/__init__.py +5 -0
  4. {camera_client-0.2.9 → camera_client-0.3.0}/camera_client/__main__.py +17 -12
  5. camera_client-0.3.0/camera_client/camera_network.py +211 -0
  6. {camera_client-0.2.9 → camera_client-0.3.0}/camera_client/client.py +26 -1
  7. camera_client-0.3.0/camera_client/error_model.py +221 -0
  8. {camera_client-0.2.9 → camera_client-0.3.0}/camera_client/loading.py +8 -0
  9. camera_client-0.3.0/camera_client/triangulation.py +109 -0
  10. camera_client-0.3.0/camera_client.egg-info/PKG-INFO +308 -0
  11. {camera_client-0.2.9 → camera_client-0.3.0}/camera_client.egg-info/SOURCES.txt +3 -0
  12. {camera_client-0.2.9 → camera_client-0.3.0}/pyproject.toml +1 -1
  13. camera_client-0.2.9/PKG-INFO +0 -446
  14. camera_client-0.2.9/README.md +0 -416
  15. camera_client-0.2.9/camera_client/__init__.py +0 -3
  16. camera_client-0.2.9/camera_client.egg-info/PKG-INFO +0 -446
  17. {camera_client-0.2.9 → camera_client-0.3.0}/LICENSE +0 -0
  18. {camera_client-0.2.9 → camera_client-0.3.0}/MANIFEST.in +0 -0
  19. {camera_client-0.2.9 → camera_client-0.3.0}/camera_client/script.py +0 -0
  20. {camera_client-0.2.9 → camera_client-0.3.0}/camera_client.egg-info/dependency_links.txt +0 -0
  21. {camera_client-0.2.9 → camera_client-0.3.0}/camera_client.egg-info/entry_points.txt +0 -0
  22. {camera_client-0.2.9 → camera_client-0.3.0}/camera_client.egg-info/requires.txt +0 -0
  23. {camera_client-0.2.9 → camera_client-0.3.0}/camera_client.egg-info/top_level.txt +0 -0
  24. {camera_client-0.2.9 → camera_client-0.3.0}/requirements.txt +0 -0
  25. {camera_client-0.2.9 → camera_client-0.3.0}/setup.cfg +0 -0
  26. {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
+
@@ -0,0 +1,5 @@
1
+ from camera_client.client import CameraProjection
2
+ from camera_client.camera_network import CameraNetwork
3
+ from camera_client import triangulation
4
+
5
+ __all__ = ["CameraProjection", "CameraNetwork", "triangulation"]
@@ -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} bytes")
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 = ".", camera_id: int = None) -> None:
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
- camera_id: If provided, only download archives for this camera_id (JSON only)
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 camera_id is not None:
110
- configs = [c for c in configs if c.get('camera_id') == 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 camera_id is not None:
131
- msg += f" for camera_id={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: current directory)'
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=int,
206
+ type=str,
205
207
  default=None,
206
- help='Filter by camera_id (only used with JSON config files)'
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
- download_from_file(args.file, args.output_dir, camera_id=args.camera_id)
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)