camera-client 0.1.2__tar.gz → 0.2.1__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.1.2/camera_client.egg-info → camera_client-0.2.1}/PKG-INFO +92 -13
- {camera_client-0.1.2 → camera_client-0.2.1}/README.md +88 -10
- {camera_client-0.1.2 → camera_client-0.2.1}/camera_client/client.py +57 -178
- {camera_client-0.1.2 → camera_client-0.2.1}/camera_client/loading.py +15 -34
- {camera_client-0.1.2 → camera_client-0.2.1/camera_client.egg-info}/PKG-INFO +92 -13
- {camera_client-0.1.2 → camera_client-0.2.1}/camera_client.egg-info/SOURCES.txt +0 -1
- {camera_client-0.1.2 → camera_client-0.2.1}/camera_client.egg-info/requires.txt +1 -0
- {camera_client-0.1.2 → camera_client-0.2.1}/pyproject.toml +5 -4
- {camera_client-0.1.2 → camera_client-0.2.1}/requirements.txt +1 -0
- camera_client-0.1.2/camera_client/utils.py +0 -218
- {camera_client-0.1.2 → camera_client-0.2.1}/LICENSE +0 -0
- {camera_client-0.1.2 → camera_client-0.2.1}/MANIFEST.in +0 -0
- {camera_client-0.1.2 → camera_client-0.2.1}/camera_client/__init__.py +0 -0
- {camera_client-0.1.2 → camera_client-0.2.1}/camera_client.egg-info/dependency_links.txt +0 -0
- {camera_client-0.1.2 → camera_client-0.2.1}/camera_client.egg-info/entry_points.txt +0 -0
- {camera_client-0.1.2 → camera_client-0.2.1}/camera_client.egg-info/top_level.txt +0 -0
- {camera_client-0.1.2 → camera_client-0.2.1}/setup.cfg +0 -0
- {camera_client-0.1.2 → camera_client-0.2.1}/setup.py +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: camera-client
|
|
3
|
-
Version: 0.1
|
|
4
|
-
Summary: Python SDK for camera calibration projection
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: Python SDK for camera calibration and projection transformations - handle lens distortion, coordinate transformations, and 3D ray casting with symbolic expressions.
|
|
5
5
|
Author-email: Alexander Abramov <extremal.ru@gmail.com>
|
|
6
6
|
License: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/avabr/camera-client
|
|
8
8
|
Project-URL: Repository, https://github.com/avabr/camera-client
|
|
9
9
|
Project-URL: Issues, https://github.com/avabr/camera-client/issues
|
|
10
|
-
Keywords: camera,calibration,projection,3d,
|
|
10
|
+
Keywords: camera,calibration,projection,3d,ray-casting,computer-vision,lens-distortion,coordinate-transformation,sympy
|
|
11
11
|
Classifier: Development Status :: 3 - Alpha
|
|
12
12
|
Classifier: Intended Audience :: Developers
|
|
13
13
|
Classifier: Intended Audience :: Science/Research
|
|
@@ -25,6 +25,7 @@ Requires-Python: >=3.7
|
|
|
25
25
|
Description-Content-Type: text/markdown
|
|
26
26
|
License-File: LICENSE
|
|
27
27
|
Requires-Dist: numpy>=1.20.0
|
|
28
|
+
Requires-Dist: sympy>=1.10.0
|
|
28
29
|
Dynamic: license-file
|
|
29
30
|
|
|
30
31
|
# camera-client
|
|
@@ -37,6 +38,8 @@ Python SDK for camera calibration and projection transformations. Transform coor
|
|
|
37
38
|
- **Multiple coordinate systems** - Transform between source (distorted), corrected, and ground (3D world) coordinates
|
|
38
39
|
- **Lens distortion handling** - Correct for camera lens distortion using calibration lookup tables
|
|
39
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
|
+
- **Sympy-based transformations** - Fast compiled symbolic expressions for mathematical transformations
|
|
40
43
|
- **NumPy-based** - Fast array operations with minimal dependencies
|
|
41
44
|
|
|
42
45
|
## Installation
|
|
@@ -56,12 +59,13 @@ from camera_client import CameraProjection
|
|
|
56
59
|
# Load camera calibration data from NPZ archive
|
|
57
60
|
camera = CameraProjection.load("camera_calibration.npz")
|
|
58
61
|
|
|
59
|
-
# Transform
|
|
62
|
+
# Transform multiple points (vectorized operations)
|
|
63
|
+
# Note: All methods require (N, 2) or (N, 3) shaped arrays
|
|
60
64
|
source_points = np.array([
|
|
61
65
|
[100, 200],
|
|
62
66
|
[300, 400],
|
|
63
67
|
[500, 600]
|
|
64
|
-
])
|
|
68
|
+
]) # Shape: (3, 2)
|
|
65
69
|
|
|
66
70
|
# Remove lens distortion
|
|
67
71
|
corrected_points = camera.src_to_ctd(source_points)
|
|
@@ -158,6 +162,28 @@ corrected = camera.src_to_ctd(random_points)
|
|
|
158
162
|
ground = camera.src_to_gnd(random_points, h=0)
|
|
159
163
|
```
|
|
160
164
|
|
|
165
|
+
### Ray Casting (3D Reconstruction)
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
# Get 3D rays from image points (useful for ray tracing, 3D reconstruction)
|
|
169
|
+
src_points = np.array([[640, 480], [800, 600]])
|
|
170
|
+
|
|
171
|
+
# Get ray directions from source (distorted) coordinates
|
|
172
|
+
rays = camera.src_to_ray(src_points)
|
|
173
|
+
print(rays.shape) # (2, 3) - normalized direction vectors
|
|
174
|
+
|
|
175
|
+
# Or from corrected coordinates
|
|
176
|
+
ctd_points = camera.src_to_ctd(src_points)
|
|
177
|
+
rays = camera.ctd_to_ray(ctd_points)
|
|
178
|
+
|
|
179
|
+
# Get camera position in world space (ray origin)
|
|
180
|
+
key_point = camera.get_key_point()
|
|
181
|
+
print(key_point.shape) # (3,) - [x, y, z] camera position
|
|
182
|
+
|
|
183
|
+
# Ray equation: point_on_ray = key_point + t * ray_direction
|
|
184
|
+
# All rays are normalized to unit length
|
|
185
|
+
```
|
|
186
|
+
|
|
161
187
|
### Accessing Camera Properties
|
|
162
188
|
|
|
163
189
|
```python
|
|
@@ -186,10 +212,12 @@ Load camera calibration from NPZ file.
|
|
|
186
212
|
Transform from source (distorted) to corrected coordinates.
|
|
187
213
|
|
|
188
214
|
**Parameters:**
|
|
189
|
-
- `points` (np.ndarray): Shape (N, 2) array of [x, y] coordinates
|
|
215
|
+
- `points` (np.ndarray): Shape **(N, 2)** array of [x, y] coordinates
|
|
190
216
|
|
|
191
217
|
**Returns:**
|
|
192
|
-
- `np.ndarray`: Shape (N, 2) corrected coordinates
|
|
218
|
+
- `np.ndarray`: Shape **(N, 2)** corrected coordinates
|
|
219
|
+
|
|
220
|
+
**Note:** Input must be 2D array. For single point use `np.array([[x, y]])`
|
|
193
221
|
|
|
194
222
|
---
|
|
195
223
|
|
|
@@ -253,21 +281,67 @@ Transform from 3D ground coordinates to corrected coordinates.
|
|
|
253
281
|
**Returns:**
|
|
254
282
|
- `np.ndarray`: Shape (N, 2) corrected coordinates
|
|
255
283
|
|
|
284
|
+
---
|
|
285
|
+
|
|
286
|
+
### `src_to_ray(points)`
|
|
287
|
+
|
|
288
|
+
Generate 3D ray directions from source (distorted) image coordinates.
|
|
289
|
+
|
|
290
|
+
**Parameters:**
|
|
291
|
+
- `points` (np.ndarray): Shape (N, 2) array of [x, y] coordinates
|
|
292
|
+
|
|
293
|
+
**Returns:**
|
|
294
|
+
- `np.ndarray`: Shape (N, 3) normalized ray direction vectors
|
|
295
|
+
|
|
296
|
+
**Note:** All rays originate from the camera key-point (use `get_key_point()`)
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
### `ctd_to_ray(points)`
|
|
301
|
+
|
|
302
|
+
Generate 3D ray directions from corrected (undistorted) image coordinates.
|
|
303
|
+
|
|
304
|
+
**Parameters:**
|
|
305
|
+
- `points` (np.ndarray): Shape (N, 2) array of [x, y] coordinates
|
|
306
|
+
|
|
307
|
+
**Returns:**
|
|
308
|
+
- `np.ndarray`: Shape (N, 3) normalized ray direction vectors
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
312
|
+
### `get_key_point()`
|
|
313
|
+
|
|
314
|
+
Get the camera position (key-point) in world space.
|
|
315
|
+
|
|
316
|
+
**Returns:**
|
|
317
|
+
- `np.ndarray`: Shape (3,) array with [x, y, z] camera position
|
|
318
|
+
|
|
256
319
|
## Calibration File Format
|
|
257
320
|
|
|
258
321
|
The calibration file is a NumPy `.npz` archive containing:
|
|
259
322
|
|
|
260
|
-
|
|
261
|
-
- `
|
|
262
|
-
- `
|
|
263
|
-
- `
|
|
264
|
-
- `
|
|
265
|
-
- `
|
|
323
|
+
### Lookup Tables
|
|
324
|
+
- `src2ctd`: Source to corrected coordinate map (H x W x 2)
|
|
325
|
+
- `ctd2src`: Corrected to source coordinate map (H x W x 2)
|
|
326
|
+
- `map_scale_h`: Height scale values (H x W)
|
|
327
|
+
- `map_scale_w`: Width scale values (H x W)
|
|
328
|
+
- `map_scale_vang`: Vertical angle values (H x W)
|
|
329
|
+
|
|
330
|
+
### Symbolic Expressions (stored as strings, parsed with SymPy)
|
|
331
|
+
- `exp_im2gnd`: Image to ground coordinate transformation
|
|
332
|
+
- `exp_gnd2im`: Ground to image coordinate transformation
|
|
333
|
+
- `exp_key_point`: Camera key-point (position) in world space
|
|
334
|
+
- `exp_im2ray`: Image to ray direction transformation
|
|
335
|
+
|
|
336
|
+
### Metadata
|
|
337
|
+
- `im_width`, `im_height`: Image dimensions in pixels
|
|
338
|
+
- `plan_scale`: Scale factor for ground plane coordinates (pixels per meter)
|
|
266
339
|
|
|
267
340
|
## Requirements
|
|
268
341
|
|
|
269
342
|
- Python >= 3.7
|
|
270
343
|
- NumPy >= 1.20.0
|
|
344
|
+
- SymPy >= 1.10.0
|
|
271
345
|
|
|
272
346
|
## Links
|
|
273
347
|
|
|
@@ -282,3 +356,8 @@ MIT License - see [LICENSE](LICENSE) file for details.
|
|
|
282
356
|
## Author
|
|
283
357
|
|
|
284
358
|
Alexander Abramov ([extremal.ru@gmail.com](mailto:extremal.ru@gmail.com))
|
|
359
|
+
|
|
360
|
+
## Upload PyPi
|
|
361
|
+
|
|
362
|
+
rm dist/* && python -m build && python -m twine upload dist/*
|
|
363
|
+
|
|
@@ -8,6 +8,8 @@ Python SDK for camera calibration and projection transformations. Transform coor
|
|
|
8
8
|
- **Multiple coordinate systems** - Transform between source (distorted), corrected, and ground (3D world) coordinates
|
|
9
9
|
- **Lens distortion handling** - Correct for camera lens distortion using calibration lookup tables
|
|
10
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
|
+
- **Sympy-based transformations** - Fast compiled symbolic expressions for mathematical transformations
|
|
11
13
|
- **NumPy-based** - Fast array operations with minimal dependencies
|
|
12
14
|
|
|
13
15
|
## Installation
|
|
@@ -27,12 +29,13 @@ from camera_client import CameraProjection
|
|
|
27
29
|
# Load camera calibration data from NPZ archive
|
|
28
30
|
camera = CameraProjection.load("camera_calibration.npz")
|
|
29
31
|
|
|
30
|
-
# Transform
|
|
32
|
+
# Transform multiple points (vectorized operations)
|
|
33
|
+
# Note: All methods require (N, 2) or (N, 3) shaped arrays
|
|
31
34
|
source_points = np.array([
|
|
32
35
|
[100, 200],
|
|
33
36
|
[300, 400],
|
|
34
37
|
[500, 600]
|
|
35
|
-
])
|
|
38
|
+
]) # Shape: (3, 2)
|
|
36
39
|
|
|
37
40
|
# Remove lens distortion
|
|
38
41
|
corrected_points = camera.src_to_ctd(source_points)
|
|
@@ -129,6 +132,28 @@ corrected = camera.src_to_ctd(random_points)
|
|
|
129
132
|
ground = camera.src_to_gnd(random_points, h=0)
|
|
130
133
|
```
|
|
131
134
|
|
|
135
|
+
### Ray Casting (3D Reconstruction)
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
# Get 3D rays from image points (useful for ray tracing, 3D reconstruction)
|
|
139
|
+
src_points = np.array([[640, 480], [800, 600]])
|
|
140
|
+
|
|
141
|
+
# Get ray directions from source (distorted) coordinates
|
|
142
|
+
rays = camera.src_to_ray(src_points)
|
|
143
|
+
print(rays.shape) # (2, 3) - normalized direction vectors
|
|
144
|
+
|
|
145
|
+
# Or from corrected coordinates
|
|
146
|
+
ctd_points = camera.src_to_ctd(src_points)
|
|
147
|
+
rays = camera.ctd_to_ray(ctd_points)
|
|
148
|
+
|
|
149
|
+
# Get camera position in world space (ray origin)
|
|
150
|
+
key_point = camera.get_key_point()
|
|
151
|
+
print(key_point.shape) # (3,) - [x, y, z] camera position
|
|
152
|
+
|
|
153
|
+
# Ray equation: point_on_ray = key_point + t * ray_direction
|
|
154
|
+
# All rays are normalized to unit length
|
|
155
|
+
```
|
|
156
|
+
|
|
132
157
|
### Accessing Camera Properties
|
|
133
158
|
|
|
134
159
|
```python
|
|
@@ -157,10 +182,12 @@ Load camera calibration from NPZ file.
|
|
|
157
182
|
Transform from source (distorted) to corrected coordinates.
|
|
158
183
|
|
|
159
184
|
**Parameters:**
|
|
160
|
-
- `points` (np.ndarray): Shape (N, 2) array of [x, y] coordinates
|
|
185
|
+
- `points` (np.ndarray): Shape **(N, 2)** array of [x, y] coordinates
|
|
161
186
|
|
|
162
187
|
**Returns:**
|
|
163
|
-
- `np.ndarray`: Shape (N, 2) corrected coordinates
|
|
188
|
+
- `np.ndarray`: Shape **(N, 2)** corrected coordinates
|
|
189
|
+
|
|
190
|
+
**Note:** Input must be 2D array. For single point use `np.array([[x, y]])`
|
|
164
191
|
|
|
165
192
|
---
|
|
166
193
|
|
|
@@ -224,21 +251,67 @@ Transform from 3D ground coordinates to corrected coordinates.
|
|
|
224
251
|
**Returns:**
|
|
225
252
|
- `np.ndarray`: Shape (N, 2) corrected coordinates
|
|
226
253
|
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
### `src_to_ray(points)`
|
|
257
|
+
|
|
258
|
+
Generate 3D ray directions from source (distorted) image coordinates.
|
|
259
|
+
|
|
260
|
+
**Parameters:**
|
|
261
|
+
- `points` (np.ndarray): Shape (N, 2) array of [x, y] coordinates
|
|
262
|
+
|
|
263
|
+
**Returns:**
|
|
264
|
+
- `np.ndarray`: Shape (N, 3) normalized ray direction vectors
|
|
265
|
+
|
|
266
|
+
**Note:** All rays originate from the camera key-point (use `get_key_point()`)
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
### `ctd_to_ray(points)`
|
|
271
|
+
|
|
272
|
+
Generate 3D ray directions from corrected (undistorted) image coordinates.
|
|
273
|
+
|
|
274
|
+
**Parameters:**
|
|
275
|
+
- `points` (np.ndarray): Shape (N, 2) array of [x, y] coordinates
|
|
276
|
+
|
|
277
|
+
**Returns:**
|
|
278
|
+
- `np.ndarray`: Shape (N, 3) normalized ray direction vectors
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
### `get_key_point()`
|
|
283
|
+
|
|
284
|
+
Get the camera position (key-point) in world space.
|
|
285
|
+
|
|
286
|
+
**Returns:**
|
|
287
|
+
- `np.ndarray`: Shape (3,) array with [x, y, z] camera position
|
|
288
|
+
|
|
227
289
|
## Calibration File Format
|
|
228
290
|
|
|
229
291
|
The calibration file is a NumPy `.npz` archive containing:
|
|
230
292
|
|
|
231
|
-
|
|
232
|
-
- `
|
|
233
|
-
- `
|
|
234
|
-
- `
|
|
235
|
-
- `
|
|
236
|
-
- `
|
|
293
|
+
### Lookup Tables
|
|
294
|
+
- `src2ctd`: Source to corrected coordinate map (H x W x 2)
|
|
295
|
+
- `ctd2src`: Corrected to source coordinate map (H x W x 2)
|
|
296
|
+
- `map_scale_h`: Height scale values (H x W)
|
|
297
|
+
- `map_scale_w`: Width scale values (H x W)
|
|
298
|
+
- `map_scale_vang`: Vertical angle values (H x W)
|
|
299
|
+
|
|
300
|
+
### Symbolic Expressions (stored as strings, parsed with SymPy)
|
|
301
|
+
- `exp_im2gnd`: Image to ground coordinate transformation
|
|
302
|
+
- `exp_gnd2im`: Ground to image coordinate transformation
|
|
303
|
+
- `exp_key_point`: Camera key-point (position) in world space
|
|
304
|
+
- `exp_im2ray`: Image to ray direction transformation
|
|
305
|
+
|
|
306
|
+
### Metadata
|
|
307
|
+
- `im_width`, `im_height`: Image dimensions in pixels
|
|
308
|
+
- `plan_scale`: Scale factor for ground plane coordinates (pixels per meter)
|
|
237
309
|
|
|
238
310
|
## Requirements
|
|
239
311
|
|
|
240
312
|
- Python >= 3.7
|
|
241
313
|
- NumPy >= 1.20.0
|
|
314
|
+
- SymPy >= 1.10.0
|
|
242
315
|
|
|
243
316
|
## Links
|
|
244
317
|
|
|
@@ -253,3 +326,8 @@ MIT License - see [LICENSE](LICENSE) file for details.
|
|
|
253
326
|
## Author
|
|
254
327
|
|
|
255
328
|
Alexander Abramov ([extremal.ru@gmail.com](mailto:extremal.ru@gmail.com))
|
|
329
|
+
|
|
330
|
+
## Upload PyPi
|
|
331
|
+
|
|
332
|
+
rm dist/* && python -m build && python -m twine upload dist/*
|
|
333
|
+
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import numpy as np
|
|
2
|
-
|
|
2
|
+
import sympy as sp
|
|
3
3
|
from camera_client.loading import read_npz_file
|
|
4
4
|
|
|
5
5
|
|
|
@@ -16,20 +16,6 @@ class CameraProjection:
|
|
|
16
16
|
- gnd: Ground (3D world) coordinates
|
|
17
17
|
"""
|
|
18
18
|
|
|
19
|
-
# Allowed variable names for different transformation types
|
|
20
|
-
VARS_CTD_TO_GND = {
|
|
21
|
-
"x_im",
|
|
22
|
-
"y_im",
|
|
23
|
-
"proj_height",
|
|
24
|
-
"np",
|
|
25
|
-
"float64",
|
|
26
|
-
"asarray",
|
|
27
|
-
"dtype",
|
|
28
|
-
}
|
|
29
|
-
VARS_GND_TO_CTD = {"x_gnd", "y_gnd", "z_gnd", "np", "float64", "asarray", "dtype"}
|
|
30
|
-
VARS_CTD_TO_RAY = {"x_im", "y_im", "np", "float64", "asarray", "dtype"}
|
|
31
|
-
VARS_KEYPOINT = {"np", "float64", "asarray", "dtype"}
|
|
32
|
-
|
|
33
19
|
def __init__(self, cam_archive_data):
|
|
34
20
|
"""
|
|
35
21
|
Initialize the camera transformer with calibration data.
|
|
@@ -55,66 +41,25 @@ class CameraProjection:
|
|
|
55
41
|
self.im_size = self.src2ctd_points_map.shape[:2]
|
|
56
42
|
|
|
57
43
|
# Compile transformation expressions for ctd -> gnd
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
allowed_vars=self.VARS_CTD_TO_GND,
|
|
62
|
-
)
|
|
63
|
-
self._y_gnd_func = compile_safe_expression(
|
|
64
|
-
data["y_gnd"],
|
|
65
|
-
param_names=["x_im", "y_im", "proj_height"],
|
|
66
|
-
allowed_vars=self.VARS_CTD_TO_GND,
|
|
67
|
-
)
|
|
68
|
-
self._z_gnd_func = compile_safe_expression(
|
|
69
|
-
data["z_gnd"],
|
|
70
|
-
param_names=["x_im", "y_im", "proj_height"],
|
|
71
|
-
allowed_vars=self.VARS_CTD_TO_GND,
|
|
44
|
+
x_im, y_im, proj_height = sp.symbols("x_im y_im proj_height")
|
|
45
|
+
self._lambda_im2gnd = sp.lambdify(
|
|
46
|
+
(x_im, y_im, proj_height), sp.sympify(data["exp_im2gnd"]), "numpy"
|
|
72
47
|
)
|
|
73
48
|
|
|
74
49
|
# Compile transformation expressions for gnd -> ctd
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
allowed_vars=self.VARS_GND_TO_CTD,
|
|
79
|
-
)
|
|
80
|
-
self._y_im_func = compile_safe_expression(
|
|
81
|
-
data["y_im"],
|
|
82
|
-
param_names=["x_gnd", "y_gnd", "z_gnd"],
|
|
83
|
-
allowed_vars=self.VARS_GND_TO_CTD,
|
|
50
|
+
x_gnd, y_gnd, z_gnd = sp.symbols("x_gnd y_gnd z_gnd")
|
|
51
|
+
self._lambda_gnd2im = sp.lambdify(
|
|
52
|
+
(x_gnd, y_gnd, z_gnd), sp.sympify(data["exp_gnd2im"]), "numpy"
|
|
84
53
|
)
|
|
85
54
|
|
|
86
55
|
# Compile keypoint expressions (camera position in world space)
|
|
87
|
-
self.
|
|
88
|
-
data["
|
|
89
|
-
param_names=[],
|
|
90
|
-
allowed_vars=self.VARS_KEYPOINT,
|
|
91
|
-
)
|
|
92
|
-
self._y_key_func = compile_safe_expression(
|
|
93
|
-
data["y_key"],
|
|
94
|
-
param_names=[],
|
|
95
|
-
allowed_vars=self.VARS_KEYPOINT,
|
|
96
|
-
)
|
|
97
|
-
self._z_key_func = compile_safe_expression(
|
|
98
|
-
data["z_key"],
|
|
99
|
-
param_names=[],
|
|
100
|
-
allowed_vars=self.VARS_KEYPOINT,
|
|
56
|
+
self._lambda_key_point = sp.lambdify(
|
|
57
|
+
(), sp.sympify(data["exp_key_point"]), "numpy"
|
|
101
58
|
)
|
|
102
59
|
|
|
103
60
|
# Compile ray direction expressions for ctd -> ray
|
|
104
|
-
self.
|
|
105
|
-
data["
|
|
106
|
-
param_names=["x_im", "y_im"],
|
|
107
|
-
allowed_vars=self.VARS_CTD_TO_RAY,
|
|
108
|
-
)
|
|
109
|
-
self._y_ray_func = compile_safe_expression(
|
|
110
|
-
data["y_ray"],
|
|
111
|
-
param_names=["x_im", "y_im"],
|
|
112
|
-
allowed_vars=self.VARS_CTD_TO_RAY,
|
|
113
|
-
)
|
|
114
|
-
self._z_ray_func = compile_safe_expression(
|
|
115
|
-
data["z_ray"],
|
|
116
|
-
param_names=["x_im", "y_im"],
|
|
117
|
-
allowed_vars=self.VARS_CTD_TO_RAY,
|
|
61
|
+
self._lambda_im2ray = sp.lambdify(
|
|
62
|
+
(x_im, y_im), sp.sympify(data["exp_im2ray"]), "numpy"
|
|
118
63
|
)
|
|
119
64
|
|
|
120
65
|
def src_to_ctd(self, points):
|
|
@@ -219,64 +164,21 @@ class CameraProjection:
|
|
|
219
164
|
h: Scalar height or (N,) array of heights for each point
|
|
220
165
|
|
|
221
166
|
Returns:
|
|
222
|
-
(N, 3) array of ground points [[x1, y1, z1], ...]
|
|
167
|
+
(N, 3) array of ground points [[x1, y1, z1], ...]
|
|
223
168
|
"""
|
|
224
169
|
points = np.asarray(points, dtype=float)
|
|
225
170
|
if points.ndim != 2 or points.shape[1] != 2:
|
|
226
171
|
raise ValueError(f"Expected (N, 2) array, got shape {points.shape}")
|
|
227
172
|
|
|
228
|
-
N =
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
# Handle scalar or array height
|
|
232
|
-
if np.isscalar(h):
|
|
233
|
-
h_array = np.full(N, h, dtype=float)
|
|
234
|
-
else:
|
|
235
|
-
h_array = np.asarray(h, dtype=float)
|
|
236
|
-
if h_array.shape != (N,):
|
|
237
|
-
raise ValueError(
|
|
238
|
-
f"Height array must have shape ({N},), got {h_array.shape}"
|
|
239
|
-
)
|
|
173
|
+
N = points.shape[0]
|
|
174
|
+
x_im, y_im = points.T
|
|
175
|
+
hs = np.asarray(h, dtype=float)
|
|
240
176
|
|
|
241
|
-
#
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
if not valid.any():
|
|
245
|
-
return result
|
|
246
|
-
|
|
247
|
-
# Extract valid points
|
|
248
|
-
valid_points = points[valid]
|
|
249
|
-
valid_heights = h_array[valid]
|
|
177
|
+
# Call lambdified function: returns shape (3, 1, N)
|
|
178
|
+
# Reshape directly to (N, 3)
|
|
179
|
+
ps_gnd = np.asarray(self._lambda_im2gnd(x_im, y_im, hs)).reshape(3, N).T
|
|
250
180
|
|
|
251
|
-
|
|
252
|
-
# NumPy operations in expressions will work element-wise on arrays
|
|
253
|
-
x_im = valid_points[:, 0]
|
|
254
|
-
y_im = valid_points[:, 1]
|
|
255
|
-
proj_height = valid_heights
|
|
256
|
-
|
|
257
|
-
# Evaluate expressions (they should handle arrays automatically via NumPy)
|
|
258
|
-
gnd_x = self._x_gnd_func(x_im, y_im, proj_height)
|
|
259
|
-
gnd_y = self._y_gnd_func(x_im, y_im, proj_height)
|
|
260
|
-
gnd_z = self._z_gnd_func(x_im, y_im, proj_height)
|
|
261
|
-
|
|
262
|
-
# Stack results and assign to valid indices
|
|
263
|
-
result[valid] = np.column_stack([gnd_x, gnd_y, gnd_z])
|
|
264
|
-
|
|
265
|
-
return result
|
|
266
|
-
|
|
267
|
-
def src_to_gnd(self, points, h):
|
|
268
|
-
"""
|
|
269
|
-
Transform from source to ground coordinates.
|
|
270
|
-
|
|
271
|
-
Args:
|
|
272
|
-
points: (N, 2) array of source points
|
|
273
|
-
h: Scalar height or (N,) array of heights
|
|
274
|
-
|
|
275
|
-
Returns:
|
|
276
|
-
(N, 3) array of ground points
|
|
277
|
-
"""
|
|
278
|
-
ctd_points = self.src_to_ctd(points)
|
|
279
|
-
return self.ctd_to_gnd(ctd_points, h)
|
|
181
|
+
return ps_gnd
|
|
280
182
|
|
|
281
183
|
def gnd_to_ctd(self, points):
|
|
282
184
|
"""
|
|
@@ -286,37 +188,34 @@ class CameraProjection:
|
|
|
286
188
|
points: (N, 3) array of ground points [[x1, y1, z1], [x2, y2, z2], ...]
|
|
287
189
|
|
|
288
190
|
Returns:
|
|
289
|
-
(N, 2) array of image points
|
|
191
|
+
(N, 2) array of image points [[x1, y1], ...]
|
|
290
192
|
"""
|
|
291
193
|
points = np.asarray(points, dtype=float)
|
|
292
194
|
if points.ndim != 2 or points.shape[1] != 3:
|
|
293
195
|
raise ValueError(f"Expected (N, 3) array, got shape {points.shape}")
|
|
294
196
|
|
|
295
|
-
N =
|
|
296
|
-
|
|
197
|
+
N = points.shape[0]
|
|
198
|
+
x_gnd, y_gnd, z_gnd = points.T
|
|
297
199
|
|
|
298
|
-
#
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
if not valid.any():
|
|
302
|
-
return result
|
|
303
|
-
|
|
304
|
-
# Extract valid points
|
|
305
|
-
valid_points = points[valid]
|
|
200
|
+
# Call lambdified function: returns shape (2, 1, N)
|
|
201
|
+
# Reshape directly to (N, 2)
|
|
202
|
+
ps_im = np.asarray(self._lambda_gnd2im(x_gnd, y_gnd, z_gnd)).reshape(2, N).T
|
|
306
203
|
|
|
307
|
-
|
|
308
|
-
x_gnd = valid_points[:, 0]
|
|
309
|
-
y_gnd = valid_points[:, 1]
|
|
310
|
-
z_gnd = valid_points[:, 2]
|
|
204
|
+
return ps_im
|
|
311
205
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
206
|
+
def src_to_gnd(self, points, h):
|
|
207
|
+
"""
|
|
208
|
+
Transform from source to ground coordinates.
|
|
315
209
|
|
|
316
|
-
|
|
317
|
-
|
|
210
|
+
Args:
|
|
211
|
+
points: (N, 2) array of source points
|
|
212
|
+
h: Scalar height or (N,) array of heights
|
|
318
213
|
|
|
319
|
-
|
|
214
|
+
Returns:
|
|
215
|
+
(N, 3) array of ground points
|
|
216
|
+
"""
|
|
217
|
+
ctd_points = self.src_to_ctd(points)
|
|
218
|
+
return self.ctd_to_gnd(ctd_points, h)
|
|
320
219
|
|
|
321
220
|
def gnd_to_src(self, points):
|
|
322
221
|
"""
|
|
@@ -331,6 +230,16 @@ class CameraProjection:
|
|
|
331
230
|
ctd_points = self.gnd_to_ctd(points)
|
|
332
231
|
return self.ctd_to_src(ctd_points)
|
|
333
232
|
|
|
233
|
+
def get_key_point(self):
|
|
234
|
+
"""
|
|
235
|
+
Get the key-point of the camera in world space.
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
(3,) 3D coordinates of the key-point of the camera in world space
|
|
239
|
+
"""
|
|
240
|
+
key_point = self._lambda_key_point()
|
|
241
|
+
return np.array(key_point, dtype=np.float64).flatten()
|
|
242
|
+
|
|
334
243
|
def ctd_to_ray(self, points):
|
|
335
244
|
"""
|
|
336
245
|
Transform from corrected image coordinates to 3D rays in camera space.
|
|
@@ -338,9 +247,8 @@ class CameraProjection:
|
|
|
338
247
|
Args:
|
|
339
248
|
points: (N, 2) array of corrected image points [[x1, y1], [x2, y2], ...]
|
|
340
249
|
|
|
341
|
-
Returns
|
|
342
|
-
|
|
343
|
-
- (N, 3) array of ray directions in camera space, with [nan, nan, nan] for invalid
|
|
250
|
+
Returns:
|
|
251
|
+
(N, 3) array of ray directions in camera space, with [nan, nan, nan] for invalid
|
|
344
252
|
Ray directions are normalized to unit length.
|
|
345
253
|
The ray for a point is defined as the vector from key-point of the camera.
|
|
346
254
|
"""
|
|
@@ -348,47 +256,19 @@ class CameraProjection:
|
|
|
348
256
|
if points.ndim != 2 or points.shape[1] != 2:
|
|
349
257
|
raise ValueError(f"Expected (N, 2) array, got shape {points.shape}")
|
|
350
258
|
|
|
351
|
-
N =
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
# Calculate keypoint (camera position in world space)
|
|
355
|
-
# These are typically constants, so we call with no arguments
|
|
356
|
-
keypoint = np.array(
|
|
357
|
-
[self._x_key_func(), self._y_key_func(), self._z_key_func()], dtype=float
|
|
358
|
-
)
|
|
359
|
-
|
|
360
|
-
# Check for valid points (not NaN)
|
|
361
|
-
valid = ~np.isnan(points).any(axis=1)
|
|
362
|
-
|
|
363
|
-
if not valid.any():
|
|
364
|
-
return keypoint, ray_directions
|
|
365
|
-
|
|
366
|
-
# Extract valid corrected points
|
|
367
|
-
valid_points = points[valid]
|
|
259
|
+
N = points.shape[0]
|
|
260
|
+
x_im, y_im = points.T
|
|
368
261
|
|
|
369
|
-
#
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
# Evaluate ray direction expressions
|
|
374
|
-
ray_x = self._x_ray_func(x_im, y_im)
|
|
375
|
-
ray_y = self._y_ray_func(x_im, y_im)
|
|
376
|
-
ray_z = self._z_ray_func(x_im, y_im)
|
|
377
|
-
|
|
378
|
-
# Stack ray components and ensure float64 dtype
|
|
379
|
-
# (expressions with large integers can create object dtype arrays)
|
|
380
|
-
rays = np.column_stack([ray_x, ray_y, ray_z]).astype(np.float64)
|
|
262
|
+
# Call lambdified function: returns shape (3, 1, N)
|
|
263
|
+
# Reshape directly to (N, 3)
|
|
264
|
+
rays = np.asarray(self._lambda_im2ray(x_im, y_im)).reshape(3, N).T
|
|
381
265
|
|
|
382
266
|
# Normalize ray directions to unit length
|
|
383
267
|
ray_lengths = np.linalg.norm(rays, axis=1, keepdims=True)
|
|
384
|
-
# Avoid division by zero
|
|
385
268
|
ray_lengths = np.where(ray_lengths > 0, ray_lengths, 1.0)
|
|
386
269
|
rays_normalized = rays / ray_lengths
|
|
387
270
|
|
|
388
|
-
|
|
389
|
-
ray_directions[valid] = rays_normalized
|
|
390
|
-
|
|
391
|
-
return keypoint, ray_directions
|
|
271
|
+
return rays_normalized
|
|
392
272
|
|
|
393
273
|
def src_to_ray(self, points):
|
|
394
274
|
"""
|
|
@@ -397,9 +277,8 @@ class CameraProjection:
|
|
|
397
277
|
Args:
|
|
398
278
|
points: (N, 2) array of source points [[x1, y1], [x2, y2], ...]
|
|
399
279
|
|
|
400
|
-
Returns
|
|
401
|
-
|
|
402
|
-
- (N, 3) array of ray directions in camera space, with [nan, nan, nan] for invalid
|
|
280
|
+
Returns:
|
|
281
|
+
(N, 3) array of ray directions in camera space, with [nan, nan, nan] for invalid
|
|
403
282
|
Ray directions are normalized to unit length.
|
|
404
283
|
The ray for a point is defined as the vector from key-point of the camera.
|
|
405
284
|
"""
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import numpy as np
|
|
2
|
+
import sympy as sp
|
|
2
3
|
|
|
3
4
|
|
|
4
5
|
# Method 1: Read from a saved file
|
|
@@ -20,17 +21,10 @@ def read_npz_file(filename):
|
|
|
20
21
|
- map_scale_h (np.ndarray): Scalar map of height scale values. Shape: (height, width)
|
|
21
22
|
- map_scale_w (np.ndarray): Scalar map of width scale values. Shape: (height, width)
|
|
22
23
|
- map_scale_vang (np.ndarray): Scalar map of vertical angle values. Shape: (height, width)
|
|
23
|
-
-
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
- y_im (str): Symbolic expression for image Y coordinate transformation
|
|
28
|
-
- x_key (str): Symbolic expression for keypoint X coordinate
|
|
29
|
-
- y_key (str): Symbolic expression for keypoint Y coordinate
|
|
30
|
-
- z_key (str): Symbolic expression for keypoint Z coordinate
|
|
31
|
-
- x_ray (str): Symbolic expression for ray X direction
|
|
32
|
-
- y_ray (str): Symbolic expression for ray Y direction
|
|
33
|
-
- z_ray (str): Symbolic expression for ray Z direction
|
|
24
|
+
- exp_im2gnd (sp.Expr): Sympy expression for image to ground coordinate transformation
|
|
25
|
+
- exp_gnd2im (sp.Expr): Sympy expression for ground to image coordinate transformation
|
|
26
|
+
- exp_key_point (sp.Expr): Sympy expression for keypoint coordinates
|
|
27
|
+
- exp_im2ray (sp.Expr): Sympy expression for image to ray direction transformation
|
|
34
28
|
"""
|
|
35
29
|
data = np.load(filename)
|
|
36
30
|
|
|
@@ -45,18 +39,12 @@ def read_npz_file(filename):
|
|
|
45
39
|
map_scale_w = data["map_scale_w"]
|
|
46
40
|
map_scale_vang = data["map_scale_vang"]
|
|
47
41
|
|
|
48
|
-
# String values (convert from
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
x_key = str(data["x_key_exp"])
|
|
55
|
-
y_key = str(data["y_key_exp"])
|
|
56
|
-
z_key = str(data["z_key_exp"])
|
|
57
|
-
x_ray = str(data["x_ray_exp"])
|
|
58
|
-
y_ray = str(data["y_ray_exp"])
|
|
59
|
-
z_ray = str(data["z_ray_exp"])
|
|
42
|
+
# String values (convert from sympy srepr to sympy expressions)
|
|
43
|
+
|
|
44
|
+
exp_im2gnd = sp.sympify(str(data["exp_im2gnd"]))
|
|
45
|
+
exp_gnd2im = sp.sympify(str(data["exp_gnd2im"]))
|
|
46
|
+
exp_key_point = sp.sympify(str(data["exp_key_point"]))
|
|
47
|
+
exp_im2ray = sp.sympify(str(data["exp_im2ray"]))
|
|
60
48
|
|
|
61
49
|
# Don't forget to close the file
|
|
62
50
|
data.close()
|
|
@@ -70,15 +58,8 @@ def read_npz_file(filename):
|
|
|
70
58
|
"map_scale_h": map_scale_h,
|
|
71
59
|
"map_scale_w": map_scale_w,
|
|
72
60
|
"map_scale_vang": map_scale_vang,
|
|
73
|
-
"
|
|
74
|
-
"
|
|
75
|
-
"
|
|
76
|
-
"
|
|
77
|
-
"y_im": y_im,
|
|
78
|
-
"x_key": x_key,
|
|
79
|
-
"y_key": y_key,
|
|
80
|
-
"z_key": z_key,
|
|
81
|
-
"x_ray": x_ray,
|
|
82
|
-
"y_ray": y_ray,
|
|
83
|
-
"z_ray": z_ray,
|
|
61
|
+
"exp_im2gnd": exp_im2gnd,
|
|
62
|
+
"exp_gnd2im": exp_gnd2im,
|
|
63
|
+
"exp_key_point": exp_key_point,
|
|
64
|
+
"exp_im2ray": exp_im2ray,
|
|
84
65
|
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: camera-client
|
|
3
|
-
Version: 0.1
|
|
4
|
-
Summary: Python SDK for camera calibration projection
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: Python SDK for camera calibration and projection transformations - handle lens distortion, coordinate transformations, and 3D ray casting with symbolic expressions.
|
|
5
5
|
Author-email: Alexander Abramov <extremal.ru@gmail.com>
|
|
6
6
|
License: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/avabr/camera-client
|
|
8
8
|
Project-URL: Repository, https://github.com/avabr/camera-client
|
|
9
9
|
Project-URL: Issues, https://github.com/avabr/camera-client/issues
|
|
10
|
-
Keywords: camera,calibration,projection,3d,
|
|
10
|
+
Keywords: camera,calibration,projection,3d,ray-casting,computer-vision,lens-distortion,coordinate-transformation,sympy
|
|
11
11
|
Classifier: Development Status :: 3 - Alpha
|
|
12
12
|
Classifier: Intended Audience :: Developers
|
|
13
13
|
Classifier: Intended Audience :: Science/Research
|
|
@@ -25,6 +25,7 @@ Requires-Python: >=3.7
|
|
|
25
25
|
Description-Content-Type: text/markdown
|
|
26
26
|
License-File: LICENSE
|
|
27
27
|
Requires-Dist: numpy>=1.20.0
|
|
28
|
+
Requires-Dist: sympy>=1.10.0
|
|
28
29
|
Dynamic: license-file
|
|
29
30
|
|
|
30
31
|
# camera-client
|
|
@@ -37,6 +38,8 @@ Python SDK for camera calibration and projection transformations. Transform coor
|
|
|
37
38
|
- **Multiple coordinate systems** - Transform between source (distorted), corrected, and ground (3D world) coordinates
|
|
38
39
|
- **Lens distortion handling** - Correct for camera lens distortion using calibration lookup tables
|
|
39
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
|
+
- **Sympy-based transformations** - Fast compiled symbolic expressions for mathematical transformations
|
|
40
43
|
- **NumPy-based** - Fast array operations with minimal dependencies
|
|
41
44
|
|
|
42
45
|
## Installation
|
|
@@ -56,12 +59,13 @@ from camera_client import CameraProjection
|
|
|
56
59
|
# Load camera calibration data from NPZ archive
|
|
57
60
|
camera = CameraProjection.load("camera_calibration.npz")
|
|
58
61
|
|
|
59
|
-
# Transform
|
|
62
|
+
# Transform multiple points (vectorized operations)
|
|
63
|
+
# Note: All methods require (N, 2) or (N, 3) shaped arrays
|
|
60
64
|
source_points = np.array([
|
|
61
65
|
[100, 200],
|
|
62
66
|
[300, 400],
|
|
63
67
|
[500, 600]
|
|
64
|
-
])
|
|
68
|
+
]) # Shape: (3, 2)
|
|
65
69
|
|
|
66
70
|
# Remove lens distortion
|
|
67
71
|
corrected_points = camera.src_to_ctd(source_points)
|
|
@@ -158,6 +162,28 @@ corrected = camera.src_to_ctd(random_points)
|
|
|
158
162
|
ground = camera.src_to_gnd(random_points, h=0)
|
|
159
163
|
```
|
|
160
164
|
|
|
165
|
+
### Ray Casting (3D Reconstruction)
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
# Get 3D rays from image points (useful for ray tracing, 3D reconstruction)
|
|
169
|
+
src_points = np.array([[640, 480], [800, 600]])
|
|
170
|
+
|
|
171
|
+
# Get ray directions from source (distorted) coordinates
|
|
172
|
+
rays = camera.src_to_ray(src_points)
|
|
173
|
+
print(rays.shape) # (2, 3) - normalized direction vectors
|
|
174
|
+
|
|
175
|
+
# Or from corrected coordinates
|
|
176
|
+
ctd_points = camera.src_to_ctd(src_points)
|
|
177
|
+
rays = camera.ctd_to_ray(ctd_points)
|
|
178
|
+
|
|
179
|
+
# Get camera position in world space (ray origin)
|
|
180
|
+
key_point = camera.get_key_point()
|
|
181
|
+
print(key_point.shape) # (3,) - [x, y, z] camera position
|
|
182
|
+
|
|
183
|
+
# Ray equation: point_on_ray = key_point + t * ray_direction
|
|
184
|
+
# All rays are normalized to unit length
|
|
185
|
+
```
|
|
186
|
+
|
|
161
187
|
### Accessing Camera Properties
|
|
162
188
|
|
|
163
189
|
```python
|
|
@@ -186,10 +212,12 @@ Load camera calibration from NPZ file.
|
|
|
186
212
|
Transform from source (distorted) to corrected coordinates.
|
|
187
213
|
|
|
188
214
|
**Parameters:**
|
|
189
|
-
- `points` (np.ndarray): Shape (N, 2) array of [x, y] coordinates
|
|
215
|
+
- `points` (np.ndarray): Shape **(N, 2)** array of [x, y] coordinates
|
|
190
216
|
|
|
191
217
|
**Returns:**
|
|
192
|
-
- `np.ndarray`: Shape (N, 2) corrected coordinates
|
|
218
|
+
- `np.ndarray`: Shape **(N, 2)** corrected coordinates
|
|
219
|
+
|
|
220
|
+
**Note:** Input must be 2D array. For single point use `np.array([[x, y]])`
|
|
193
221
|
|
|
194
222
|
---
|
|
195
223
|
|
|
@@ -253,21 +281,67 @@ Transform from 3D ground coordinates to corrected coordinates.
|
|
|
253
281
|
**Returns:**
|
|
254
282
|
- `np.ndarray`: Shape (N, 2) corrected coordinates
|
|
255
283
|
|
|
284
|
+
---
|
|
285
|
+
|
|
286
|
+
### `src_to_ray(points)`
|
|
287
|
+
|
|
288
|
+
Generate 3D ray directions from source (distorted) image coordinates.
|
|
289
|
+
|
|
290
|
+
**Parameters:**
|
|
291
|
+
- `points` (np.ndarray): Shape (N, 2) array of [x, y] coordinates
|
|
292
|
+
|
|
293
|
+
**Returns:**
|
|
294
|
+
- `np.ndarray`: Shape (N, 3) normalized ray direction vectors
|
|
295
|
+
|
|
296
|
+
**Note:** All rays originate from the camera key-point (use `get_key_point()`)
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
### `ctd_to_ray(points)`
|
|
301
|
+
|
|
302
|
+
Generate 3D ray directions from corrected (undistorted) image coordinates.
|
|
303
|
+
|
|
304
|
+
**Parameters:**
|
|
305
|
+
- `points` (np.ndarray): Shape (N, 2) array of [x, y] coordinates
|
|
306
|
+
|
|
307
|
+
**Returns:**
|
|
308
|
+
- `np.ndarray`: Shape (N, 3) normalized ray direction vectors
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
312
|
+
### `get_key_point()`
|
|
313
|
+
|
|
314
|
+
Get the camera position (key-point) in world space.
|
|
315
|
+
|
|
316
|
+
**Returns:**
|
|
317
|
+
- `np.ndarray`: Shape (3,) array with [x, y, z] camera position
|
|
318
|
+
|
|
256
319
|
## Calibration File Format
|
|
257
320
|
|
|
258
321
|
The calibration file is a NumPy `.npz` archive containing:
|
|
259
322
|
|
|
260
|
-
|
|
261
|
-
- `
|
|
262
|
-
- `
|
|
263
|
-
- `
|
|
264
|
-
- `
|
|
265
|
-
- `
|
|
323
|
+
### Lookup Tables
|
|
324
|
+
- `src2ctd`: Source to corrected coordinate map (H x W x 2)
|
|
325
|
+
- `ctd2src`: Corrected to source coordinate map (H x W x 2)
|
|
326
|
+
- `map_scale_h`: Height scale values (H x W)
|
|
327
|
+
- `map_scale_w`: Width scale values (H x W)
|
|
328
|
+
- `map_scale_vang`: Vertical angle values (H x W)
|
|
329
|
+
|
|
330
|
+
### Symbolic Expressions (stored as strings, parsed with SymPy)
|
|
331
|
+
- `exp_im2gnd`: Image to ground coordinate transformation
|
|
332
|
+
- `exp_gnd2im`: Ground to image coordinate transformation
|
|
333
|
+
- `exp_key_point`: Camera key-point (position) in world space
|
|
334
|
+
- `exp_im2ray`: Image to ray direction transformation
|
|
335
|
+
|
|
336
|
+
### Metadata
|
|
337
|
+
- `im_width`, `im_height`: Image dimensions in pixels
|
|
338
|
+
- `plan_scale`: Scale factor for ground plane coordinates (pixels per meter)
|
|
266
339
|
|
|
267
340
|
## Requirements
|
|
268
341
|
|
|
269
342
|
- Python >= 3.7
|
|
270
343
|
- NumPy >= 1.20.0
|
|
344
|
+
- SymPy >= 1.10.0
|
|
271
345
|
|
|
272
346
|
## Links
|
|
273
347
|
|
|
@@ -282,3 +356,8 @@ MIT License - see [LICENSE](LICENSE) file for details.
|
|
|
282
356
|
## Author
|
|
283
357
|
|
|
284
358
|
Alexander Abramov ([extremal.ru@gmail.com](mailto:extremal.ru@gmail.com))
|
|
359
|
+
|
|
360
|
+
## Upload PyPi
|
|
361
|
+
|
|
362
|
+
rm dist/* && python -m build && python -m twine upload dist/*
|
|
363
|
+
|
|
@@ -4,15 +4,15 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "camera-client"
|
|
7
|
-
version = "0.1
|
|
8
|
-
description = "Python SDK for camera calibration projection
|
|
7
|
+
version = "0.2.1"
|
|
8
|
+
description = "Python SDK for camera calibration and projection transformations - handle lens distortion, coordinate transformations, and 3D ray casting with symbolic expressions."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.7"
|
|
11
11
|
license = {text = "MIT"}
|
|
12
12
|
authors = [
|
|
13
13
|
{name = "Alexander Abramov", email = "extremal.ru@gmail.com"},
|
|
14
14
|
]
|
|
15
|
-
keywords = ["camera", "calibration", "projection", "3d", "
|
|
15
|
+
keywords = ["camera", "calibration", "projection", "3d", "ray-casting", "computer-vision", "lens-distortion", "coordinate-transformation", "sympy"]
|
|
16
16
|
classifiers = [
|
|
17
17
|
"Development Status :: 3 - Alpha",
|
|
18
18
|
"Intended Audience :: Developers",
|
|
@@ -29,7 +29,8 @@ classifiers = [
|
|
|
29
29
|
"Topic :: Scientific/Engineering :: Mathematics",
|
|
30
30
|
]
|
|
31
31
|
dependencies = [
|
|
32
|
-
"numpy>=1.20.0"
|
|
32
|
+
"numpy>=1.20.0",
|
|
33
|
+
"sympy>=1.10.0"
|
|
33
34
|
]
|
|
34
35
|
|
|
35
36
|
[project.urls]
|
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
import numpy as np
|
|
2
|
-
import ast
|
|
3
|
-
import operator
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
# ============================================================================
|
|
7
|
-
# Expression Validation and Compilation Functions
|
|
8
|
-
# ============================================================================
|
|
9
|
-
|
|
10
|
-
# Allowed operations for expression validation
|
|
11
|
-
ALLOWED_OPERATORS = {
|
|
12
|
-
ast.Add: operator.add,
|
|
13
|
-
ast.Sub: operator.sub,
|
|
14
|
-
ast.Mult: operator.mul,
|
|
15
|
-
ast.Div: operator.truediv,
|
|
16
|
-
ast.Pow: operator.pow,
|
|
17
|
-
ast.USub: operator.neg,
|
|
18
|
-
ast.UAdd: operator.pos,
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
# Allowed functions
|
|
22
|
-
ALLOWED_FUNCTIONS = {
|
|
23
|
-
"sqrt": np.sqrt,
|
|
24
|
-
"sin": np.sin,
|
|
25
|
-
"cos": np.cos,
|
|
26
|
-
"tan": np.tan,
|
|
27
|
-
"arcsin": np.arcsin,
|
|
28
|
-
"arccos": np.arccos,
|
|
29
|
-
"arctan": np.arctan,
|
|
30
|
-
"arctan2": np.arctan2,
|
|
31
|
-
"abs": np.abs,
|
|
32
|
-
"exp": np.exp,
|
|
33
|
-
"log": np.log,
|
|
34
|
-
"log10": np.log10,
|
|
35
|
-
"pow": np.power,
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def normalize_expression(expr):
|
|
40
|
-
"""
|
|
41
|
-
Normalize expression by replacing common math functions with numpy equivalents.
|
|
42
|
-
|
|
43
|
-
Args:
|
|
44
|
-
expr: String expression to normalize
|
|
45
|
-
|
|
46
|
-
Returns:
|
|
47
|
-
Normalized expression string
|
|
48
|
-
"""
|
|
49
|
-
# Replace math functions with numpy equivalents
|
|
50
|
-
# For sqrt, we need special handling to avoid object dtype issues with large integers
|
|
51
|
-
# We replace sqrt(x) with np.sqrt(np.asarray(x, dtype=np.float64))
|
|
52
|
-
import re
|
|
53
|
-
|
|
54
|
-
# Find all sqrt(...) calls and wrap the argument
|
|
55
|
-
# Match sqrt followed by parentheses, handling nested parentheses
|
|
56
|
-
def replace_sqrt(match):
|
|
57
|
-
return f"np.sqrt(np.asarray({match.group(1)}, dtype=np.float64))"
|
|
58
|
-
|
|
59
|
-
# Use regex to find sqrt(...) with balanced parentheses
|
|
60
|
-
# This pattern matches sqrt( followed by content, handling nested parentheses
|
|
61
|
-
depth = 0
|
|
62
|
-
result = []
|
|
63
|
-
i = 0
|
|
64
|
-
while i < len(expr):
|
|
65
|
-
if expr[i:i+4] == 'sqrt':
|
|
66
|
-
# Found sqrt, now find the matching closing parenthesis
|
|
67
|
-
if i + 4 < len(expr) and expr[i+4] == '(':
|
|
68
|
-
start = i + 5 # Start of argument
|
|
69
|
-
depth = 1
|
|
70
|
-
j = start
|
|
71
|
-
while j < len(expr) and depth > 0:
|
|
72
|
-
if expr[j] == '(':
|
|
73
|
-
depth += 1
|
|
74
|
-
elif expr[j] == ')':
|
|
75
|
-
depth -= 1
|
|
76
|
-
j += 1
|
|
77
|
-
# j now points to one past the closing paren
|
|
78
|
-
arg = expr[start:j-1]
|
|
79
|
-
result.append(f"np.sqrt(np.asarray({arg}, dtype=np.float64))")
|
|
80
|
-
i = j
|
|
81
|
-
else:
|
|
82
|
-
result.append(expr[i])
|
|
83
|
-
i += 1
|
|
84
|
-
else:
|
|
85
|
-
result.append(expr[i])
|
|
86
|
-
i += 1
|
|
87
|
-
|
|
88
|
-
expr = ''.join(result)
|
|
89
|
-
|
|
90
|
-
expr = expr.replace("sin(", "np.sin(")
|
|
91
|
-
expr = expr.replace("cos(", "np.cos(")
|
|
92
|
-
expr = expr.replace("tan(", "np.tan(")
|
|
93
|
-
# Handle cases where np.np might occur
|
|
94
|
-
expr = expr.replace("np.np.", "np.")
|
|
95
|
-
return expr
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
def validate_expression(expr, allowed_vars):
|
|
99
|
-
"""
|
|
100
|
-
Validate that expression only contains safe operations.
|
|
101
|
-
|
|
102
|
-
Args:
|
|
103
|
-
expr: String expression to validate
|
|
104
|
-
allowed_vars: Set of allowed variable names
|
|
105
|
-
|
|
106
|
-
Raises:
|
|
107
|
-
ValueError: If expression contains dangerous operations
|
|
108
|
-
"""
|
|
109
|
-
try:
|
|
110
|
-
tree = ast.parse(expr, mode="eval")
|
|
111
|
-
except SyntaxError as e:
|
|
112
|
-
raise ValueError(f"Invalid expression syntax: {expr}") from e
|
|
113
|
-
|
|
114
|
-
# Walk through AST and check all nodes
|
|
115
|
-
for node in ast.walk(tree):
|
|
116
|
-
if isinstance(node, ast.Name):
|
|
117
|
-
# Check variable names
|
|
118
|
-
if node.id not in allowed_vars and node.id not in ALLOWED_FUNCTIONS:
|
|
119
|
-
raise ValueError(
|
|
120
|
-
f"Disallowed variable '{node.id}' in expression: {expr}"
|
|
121
|
-
)
|
|
122
|
-
|
|
123
|
-
elif isinstance(node, ast.Call):
|
|
124
|
-
# Check function calls
|
|
125
|
-
if isinstance(node.func, ast.Attribute):
|
|
126
|
-
# Handle np.sqrt, np.sin, etc.
|
|
127
|
-
if isinstance(node.func.value, ast.Name):
|
|
128
|
-
if node.func.value.id != "np":
|
|
129
|
-
raise ValueError(
|
|
130
|
-
f"Disallowed module '{node.func.value.id}' in expression: {expr}"
|
|
131
|
-
)
|
|
132
|
-
# np.function_name - allow numpy functions
|
|
133
|
-
else:
|
|
134
|
-
raise ValueError(f"Complex attribute access not allowed: {expr}")
|
|
135
|
-
|
|
136
|
-
elif isinstance(node.func, ast.Name):
|
|
137
|
-
if node.func.id not in ALLOWED_FUNCTIONS:
|
|
138
|
-
raise ValueError(
|
|
139
|
-
f"Disallowed function '{node.func.id}' in expression: {expr}"
|
|
140
|
-
)
|
|
141
|
-
else:
|
|
142
|
-
raise ValueError(f"Complex function call not allowed: {expr}")
|
|
143
|
-
|
|
144
|
-
elif isinstance(node, ast.BinOp):
|
|
145
|
-
# Check binary operations
|
|
146
|
-
if type(node.op) not in ALLOWED_OPERATORS:
|
|
147
|
-
raise ValueError(
|
|
148
|
-
f"Disallowed operator '{node.op.__class__.__name__}' in expression: {expr}"
|
|
149
|
-
)
|
|
150
|
-
|
|
151
|
-
elif isinstance(node, ast.UnaryOp):
|
|
152
|
-
# Check unary operations
|
|
153
|
-
if type(node.op) not in ALLOWED_OPERATORS:
|
|
154
|
-
raise ValueError(
|
|
155
|
-
f"Disallowed unary operator '{node.op.__class__.__name__}' in expression: {expr}"
|
|
156
|
-
)
|
|
157
|
-
|
|
158
|
-
elif isinstance(node, (ast.Attribute, ast.Import, ast.ImportFrom)):
|
|
159
|
-
# Block imports and most attribute access
|
|
160
|
-
if isinstance(node, ast.Attribute):
|
|
161
|
-
# Only allow np.function
|
|
162
|
-
if not (isinstance(node.value, ast.Name) and node.value.id == "np"):
|
|
163
|
-
raise ValueError(
|
|
164
|
-
f"Disallowed attribute access in expression: {expr}"
|
|
165
|
-
)
|
|
166
|
-
else:
|
|
167
|
-
raise ValueError(f"Import statements not allowed in expression: {expr}")
|
|
168
|
-
|
|
169
|
-
elif isinstance(node, (ast.Lambda, ast.FunctionDef, ast.ClassDef)):
|
|
170
|
-
raise ValueError(
|
|
171
|
-
f"Function/class definitions not allowed in expression: {expr}"
|
|
172
|
-
)
|
|
173
|
-
|
|
174
|
-
elif isinstance(node, (ast.ListComp, ast.DictComp, ast.GeneratorExp)):
|
|
175
|
-
raise ValueError(f"Comprehensions not allowed in expression: {expr}")
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
def compile_safe_expression(expr, param_names, allowed_vars=None):
|
|
179
|
-
"""
|
|
180
|
-
Validate and compile expression into a callable function.
|
|
181
|
-
|
|
182
|
-
Args:
|
|
183
|
-
expr: String expression to compile
|
|
184
|
-
param_names: List of parameter names expected in expression
|
|
185
|
-
allowed_vars: Set of allowed variable names (defaults to param_names + 'np')
|
|
186
|
-
|
|
187
|
-
Returns:
|
|
188
|
-
Compiled function that evaluates the expression
|
|
189
|
-
|
|
190
|
-
Raises:
|
|
191
|
-
ValueError: If expression validation fails
|
|
192
|
-
"""
|
|
193
|
-
# Default allowed vars to param_names plus 'np'
|
|
194
|
-
if allowed_vars is None:
|
|
195
|
-
allowed_vars = set(param_names) | {"np"}
|
|
196
|
-
|
|
197
|
-
# Normalize the expression
|
|
198
|
-
normalized_expr = normalize_expression(expr)
|
|
199
|
-
|
|
200
|
-
# Validate the expression
|
|
201
|
-
validate_expression(normalized_expr, allowed_vars)
|
|
202
|
-
|
|
203
|
-
# Compile the expression
|
|
204
|
-
code = compile(normalized_expr, "<string>", "eval")
|
|
205
|
-
|
|
206
|
-
# Create restricted namespace with only allowed functions
|
|
207
|
-
safe_namespace = {
|
|
208
|
-
"np": np,
|
|
209
|
-
"__builtins__": {}, # Remove all builtins
|
|
210
|
-
}
|
|
211
|
-
safe_namespace.update(ALLOWED_FUNCTIONS)
|
|
212
|
-
|
|
213
|
-
def compiled_func(*args):
|
|
214
|
-
# Create local namespace with parameter values
|
|
215
|
-
local_vars = dict(zip(param_names, args))
|
|
216
|
-
return eval(code, safe_namespace, local_vars)
|
|
217
|
-
|
|
218
|
-
return compiled_func
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|