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.
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: camera-client
3
- Version: 0.1.2
4
- Summary: Python SDK for camera calibration projection data - transform between distorted, corrected, and world coordinates.
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,mathematics
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 single or multiple points (vectorized operations)
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
- - `src2ctd`: Lookup table for source to corrected transformation (H x W x 2)
261
- - `ctd2src`: Lookup table for corrected to source transformation (H x W x 2)
262
- - `x_gnd`, `y_gnd`, `z_gnd`: Expression strings for corrected to ground transformation
263
- - `x_im`, `y_im`: Expression strings for ground to corrected transformation
264
- - `im_width`, `im_height`: Image dimensions
265
- - `plan_scale`: Scale factor for ground plane
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 single or multiple points (vectorized operations)
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
- - `src2ctd`: Lookup table for source to corrected transformation (H x W x 2)
232
- - `ctd2src`: Lookup table for corrected to source transformation (H x W x 2)
233
- - `x_gnd`, `y_gnd`, `z_gnd`: Expression strings for corrected to ground transformation
234
- - `x_im`, `y_im`: Expression strings for ground to corrected transformation
235
- - `im_width`, `im_height`: Image dimensions
236
- - `plan_scale`: Scale factor for ground plane
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
- from camera_client.utils import compile_safe_expression
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
- self._x_gnd_func = compile_safe_expression(
59
- data["x_gnd"],
60
- param_names=["x_im", "y_im", "proj_height"],
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
- self._x_im_func = compile_safe_expression(
76
- data["x_im"],
77
- param_names=["x_gnd", "y_gnd", "z_gnd"],
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._x_key_func = compile_safe_expression(
88
- data["x_key"],
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._x_ray_func = compile_safe_expression(
105
- data["x_ray"],
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], ...], with [nan, nan, nan] for invalid
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 = len(points)
229
- result = np.full((N, 3), np.nan, dtype=float)
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
- # Check for NaN input points
242
- valid = ~np.isnan(points).any(axis=1)
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
- # Vectorized expression evaluation
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, with [nan, nan] for invalid
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 = len(points)
296
- result = np.full((N, 2), np.nan, dtype=float)
197
+ N = points.shape[0]
198
+ x_gnd, y_gnd, z_gnd = points.T
297
199
 
298
- # Check for NaN input points
299
- valid = ~np.isnan(points).any(axis=1)
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
- # Vectorized expression evaluation
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
- # Evaluate expressions
313
- x_im = self._x_im_func(x_gnd, y_gnd, z_gnd)
314
- y_im = self._y_im_func(x_gnd, y_gnd, z_gnd)
206
+ def src_to_gnd(self, points, h):
207
+ """
208
+ Transform from source to ground coordinates.
315
209
 
316
- # Stack results and assign to valid indices
317
- result[valid] = np.column_stack([x_im, y_im])
210
+ Args:
211
+ points: (N, 2) array of source points
212
+ h: Scalar height or (N,) array of heights
318
213
 
319
- return result
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 two argiments:
342
- - (3,) 3D coordinates of the key-point of the camera in world space
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 = len(points)
352
- ray_directions = np.full((N, 3), np.nan, dtype=float)
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
- # Vectorized ray direction evaluation
370
- x_im = valid_points[:, 0]
371
- y_im = valid_points[:, 1]
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
- # Assign normalized rays to valid indices
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 two argiments:
401
- - (3,) 3D coordinates of the key-point of the camera in world space
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
- - x_gnd (str): Symbolic expression for gndal X coordinate perspective transformation
24
- - y_gnd (str): Symbolic expression for gndal Y coordinate perspective transformation
25
- - z_gnd (str): Symbolic expression for gndal Z coordinate perspective transformation
26
- - x_im (str): Symbolic expression for image X coordinate transformation
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 numpy string to Python string)
49
- x_gnd = str(data["x_gnd_exp"])
50
- y_gnd = str(data["y_gnd_exp"])
51
- z_gnd = str(data["z_gnd_exp"])
52
- x_im = str(data["x_im_exp"])
53
- y_im = str(data["y_im_exp"])
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
- "x_gnd": x_gnd,
74
- "y_gnd": y_gnd,
75
- "z_gnd": z_gnd,
76
- "x_im": x_im,
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.2
4
- Summary: Python SDK for camera calibration projection data - transform between distorted, corrected, and world coordinates.
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,mathematics
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 single or multiple points (vectorized operations)
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
- - `src2ctd`: Lookup table for source to corrected transformation (H x W x 2)
261
- - `ctd2src`: Lookup table for corrected to source transformation (H x W x 2)
262
- - `x_gnd`, `y_gnd`, `z_gnd`: Expression strings for corrected to ground transformation
263
- - `x_im`, `y_im`: Expression strings for ground to corrected transformation
264
- - `im_width`, `im_height`: Image dimensions
265
- - `plan_scale`: Scale factor for ground plane
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
+
@@ -7,7 +7,6 @@ setup.py
7
7
  camera_client/__init__.py
8
8
  camera_client/client.py
9
9
  camera_client/loading.py
10
- camera_client/utils.py
11
10
  camera_client.egg-info/PKG-INFO
12
11
  camera_client.egg-info/SOURCES.txt
13
12
  camera_client.egg-info/dependency_links.txt
@@ -1 +1,2 @@
1
1
  numpy>=1.20.0
2
+ sympy>=1.10.0
@@ -4,15 +4,15 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "camera-client"
7
- version = "0.1.2"
8
- description = "Python SDK for camera calibration projection data - transform between distorted, corrected, and world coordinates."
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", "mathematics"]
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,3 +1,4 @@
1
1
  numpy
2
+ sympy
2
3
  matplotlib
3
4
  python-dotenv
@@ -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