camera-client 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,3 @@
1
+ from camera_client.client import CameraProjection
2
+
3
+ __all__ = ["CameraProjection"]
@@ -0,0 +1,305 @@
1
+ import numpy as np
2
+ from camera_client.utils import compile_safe_expression
3
+ from camera_client.loading import read_npz_file
4
+
5
+
6
+ class CameraProjection:
7
+ """
8
+ Vectorized camera coordinate transformer for batch processing.
9
+
10
+ This class is optimized for processing multiple points at once.
11
+ All methods expect array inputs with shape (N, 2) or (N, 3).
12
+
13
+ Handles transformations between:
14
+ - src: Source (distorted) image coordinates
15
+ - ctd: Corrected (undistorted) image coordinates
16
+ - gnd: Ground (3D world) coordinates
17
+ """
18
+
19
+ # Allowed variable names for different transformation types
20
+ VARS_CTD_TO_GND = {"x_im", "y_im", "proj_height", "np"}
21
+ VARS_GND_TO_CTD = {"x_gnd", "y_gnd", "z_gnd", "np"}
22
+
23
+ def __init__(self, cam_archive_data):
24
+ """
25
+ Initialize the camera transformer with calibration data.
26
+
27
+ Args:
28
+ cam_archive_data: Dictionary containing:
29
+ - src2ctd: Source to corrected distortion map (H x W x 2)
30
+ - ctd2src: Corrected to source distortion map (H x W x 2)
31
+ - x_gnd, y_gnd, z_gnd: String expressions for ctd -> ground
32
+ - x_im, y_im: String expressions for ground -> ctd
33
+ """
34
+ data = cam_archive_data
35
+
36
+ self.plan_scale = data["plan_scale"]
37
+ self.im_width = data["im_width"]
38
+ self.im_height = data["im_height"]
39
+ self.im_wh_size = (self.im_width, self.im_height)
40
+
41
+ # Store lookup tables
42
+ self.src2ctd_points_map = data["src2ctd"]
43
+ self.ctd2src_points_map = data["ctd2src"]
44
+
45
+ self.im_size = self.src2ctd_points_map.shape[:2]
46
+
47
+ # Compile transformation expressions for ctd -> gnd
48
+ self._x_gnd_func = compile_safe_expression(
49
+ data["x_gnd"],
50
+ param_names=["x_im", "y_im", "proj_height"],
51
+ allowed_vars=self.VARS_CTD_TO_GND,
52
+ )
53
+ self._y_gnd_func = compile_safe_expression(
54
+ data["y_gnd"],
55
+ param_names=["x_im", "y_im", "proj_height"],
56
+ allowed_vars=self.VARS_CTD_TO_GND,
57
+ )
58
+ self._z_gnd_func = compile_safe_expression(
59
+ data["z_gnd"],
60
+ param_names=["x_im", "y_im", "proj_height"],
61
+ allowed_vars=self.VARS_CTD_TO_GND,
62
+ )
63
+
64
+ # Compile transformation expressions for gnd -> ctd
65
+ self._x_im_func = compile_safe_expression(
66
+ data["x_im"],
67
+ param_names=["x_gnd", "y_gnd", "z_gnd"],
68
+ allowed_vars=self.VARS_GND_TO_CTD,
69
+ )
70
+ self._y_im_func = compile_safe_expression(
71
+ data["y_im"],
72
+ param_names=["x_gnd", "y_gnd", "z_gnd"],
73
+ allowed_vars=self.VARS_GND_TO_CTD,
74
+ )
75
+
76
+ def src_to_ctd(self, points):
77
+ """
78
+ Transform from source (distorted) to corrected coordinates.
79
+
80
+ Args:
81
+ points: (N, 2) array of source points [[x1, y1], [x2, y2], ...]
82
+
83
+ Returns:
84
+ (N, 2) array of corrected points, with [nan, nan] for out-of-bounds
85
+ """
86
+ points = np.asarray(points, dtype=float)
87
+ if points.ndim != 2 or points.shape[1] != 2:
88
+ raise ValueError(f"Expected (N, 2) array, got shape {points.shape}")
89
+
90
+ N = len(points)
91
+ result = np.full((N, 2), np.nan, dtype=float)
92
+
93
+ # Check for NaN input points
94
+ valid_input = ~np.isnan(points).any(axis=1)
95
+
96
+ if not valid_input.any():
97
+ return result
98
+
99
+ # Round to integer coordinates
100
+ p_int = np.round(points[valid_input]).astype(int)
101
+
102
+ # Vectorized bounds checking
103
+ in_bounds = (
104
+ (p_int[:, 0] >= 0)
105
+ & (p_int[:, 0] < self.src2ctd_points_map.shape[1])
106
+ & (p_int[:, 1] >= 0)
107
+ & (p_int[:, 1] < self.src2ctd_points_map.shape[0])
108
+ )
109
+
110
+ # Create mask for points that are both valid input and in bounds
111
+ valid_indices = np.where(valid_input)[0]
112
+ final_valid_indices = valid_indices[in_bounds]
113
+ valid_p_int = p_int[in_bounds]
114
+
115
+ # Vectorized lookup using advanced indexing
116
+ # Note: lookup map is [y, x] indexed
117
+ result[final_valid_indices] = self.src2ctd_points_map[
118
+ valid_p_int[:, 1], valid_p_int[:, 0]
119
+ ]
120
+
121
+ return result
122
+
123
+ def ctd_to_src(self, points):
124
+ """
125
+ Transform from corrected to source (distorted) coordinates.
126
+
127
+ Args:
128
+ points: (N, 2) array of corrected points [[x1, y1], [x2, y2], ...]
129
+
130
+ Returns:
131
+ (N, 2) array of source points, with [nan, nan] for out-of-bounds
132
+ """
133
+ points = np.asarray(points, dtype=float)
134
+ if points.ndim != 2 or points.shape[1] != 2:
135
+ raise ValueError(f"Expected (N, 2) array, got shape {points.shape}")
136
+
137
+ N = len(points)
138
+ result = np.full((N, 2), np.nan, dtype=float)
139
+
140
+ # Check for NaN input points
141
+ valid_input = ~np.isnan(points).any(axis=1)
142
+
143
+ if not valid_input.any():
144
+ return result
145
+
146
+ # Round to integer coordinates
147
+ p_int = np.round(points[valid_input]).astype(int)
148
+
149
+ # Vectorized bounds checking
150
+ in_bounds = (
151
+ (p_int[:, 0] >= 0)
152
+ & (p_int[:, 0] < self.ctd2src_points_map.shape[1])
153
+ & (p_int[:, 1] >= 0)
154
+ & (p_int[:, 1] < self.ctd2src_points_map.shape[0])
155
+ )
156
+
157
+ # Create mask for points that are both valid input and in bounds
158
+ valid_indices = np.where(valid_input)[0]
159
+ final_valid_indices = valid_indices[in_bounds]
160
+ valid_p_int = p_int[in_bounds]
161
+
162
+ # Vectorized lookup using advanced indexing
163
+ result[final_valid_indices] = self.ctd2src_points_map[
164
+ valid_p_int[:, 1], valid_p_int[:, 0]
165
+ ]
166
+
167
+ return result
168
+
169
+ def ctd_to_gnd(self, points, h):
170
+ """
171
+ Transform from corrected image coordinates to ground (3D world) coordinates.
172
+
173
+ Args:
174
+ points: (N, 2) array of corrected points [[x1, y1], [x2, y2], ...]
175
+ h: Scalar height or (N,) array of heights for each point
176
+
177
+ Returns:
178
+ (N, 3) array of ground points [[x1, y1, z1], ...], with [nan, nan, nan] for invalid
179
+ """
180
+ points = np.asarray(points, dtype=float)
181
+ if points.ndim != 2 or points.shape[1] != 2:
182
+ raise ValueError(f"Expected (N, 2) array, got shape {points.shape}")
183
+
184
+ N = len(points)
185
+ result = np.full((N, 3), np.nan, dtype=float)
186
+
187
+ # Handle scalar or array height
188
+ if np.isscalar(h):
189
+ h_array = np.full(N, h, dtype=float)
190
+ else:
191
+ h_array = np.asarray(h, dtype=float)
192
+ if h_array.shape != (N,):
193
+ raise ValueError(
194
+ f"Height array must have shape ({N},), got {h_array.shape}"
195
+ )
196
+
197
+ # Check for NaN input points
198
+ valid = ~np.isnan(points).any(axis=1)
199
+
200
+ if not valid.any():
201
+ return result
202
+
203
+ # Extract valid points
204
+ valid_points = points[valid]
205
+ valid_heights = h_array[valid]
206
+
207
+ # Vectorized expression evaluation
208
+ # NumPy operations in expressions will work element-wise on arrays
209
+ x_im = valid_points[:, 0]
210
+ y_im = valid_points[:, 1]
211
+ proj_height = valid_heights
212
+
213
+ # Evaluate expressions (they should handle arrays automatically via NumPy)
214
+ gnd_x = self._x_gnd_func(x_im, y_im, proj_height)
215
+ gnd_y = self._y_gnd_func(x_im, y_im, proj_height)
216
+ gnd_z = self._z_gnd_func(x_im, y_im, proj_height)
217
+
218
+ # Stack results and assign to valid indices
219
+ result[valid] = np.column_stack([gnd_x, gnd_y, gnd_z])
220
+
221
+ return result
222
+
223
+ def src_to_gnd(self, points, h):
224
+ """
225
+ Transform from source to ground coordinates.
226
+
227
+ Args:
228
+ points: (N, 2) array of source points
229
+ h: Scalar height or (N,) array of heights
230
+
231
+ Returns:
232
+ (N, 3) array of ground points
233
+ """
234
+ ctd_points = self.src_to_ctd(points)
235
+ return self.ctd_to_gnd(ctd_points, h)
236
+
237
+ def gnd_to_ctd(self, points):
238
+ """
239
+ Transform from ground (3D world) to corrected image coordinates.
240
+
241
+ Args:
242
+ points: (N, 3) array of ground points [[x1, y1, z1], [x2, y2, z2], ...]
243
+
244
+ Returns:
245
+ (N, 2) array of image points, with [nan, nan] for invalid
246
+ """
247
+ points = np.asarray(points, dtype=float)
248
+ if points.ndim != 2 or points.shape[1] != 3:
249
+ raise ValueError(f"Expected (N, 3) array, got shape {points.shape}")
250
+
251
+ N = len(points)
252
+ result = np.full((N, 2), np.nan, dtype=float)
253
+
254
+ # Check for NaN input points
255
+ valid = ~np.isnan(points).any(axis=1)
256
+
257
+ if not valid.any():
258
+ return result
259
+
260
+ # Extract valid points
261
+ valid_points = points[valid]
262
+
263
+ # Vectorized expression evaluation
264
+ x_gnd = valid_points[:, 0]
265
+ y_gnd = valid_points[:, 1]
266
+ z_gnd = valid_points[:, 2]
267
+
268
+ # Evaluate expressions
269
+ x_im = self._x_im_func(x_gnd, y_gnd, z_gnd)
270
+ y_im = self._y_im_func(x_gnd, y_gnd, z_gnd)
271
+
272
+ # Stack results and assign to valid indices
273
+ result[valid] = np.column_stack([x_im, y_im])
274
+
275
+ return result
276
+
277
+ def gnd_to_src(self, points):
278
+ """
279
+ Transform from ground to source coordinates.
280
+
281
+ Args:
282
+ points: (N, 3) array of ground points
283
+
284
+ Returns:
285
+ (N, 2) array of source points
286
+ """
287
+ ctd_points = self.gnd_to_ctd(points)
288
+ return self.ctd_to_src(ctd_points)
289
+
290
+ @classmethod
291
+ def load(cls, archive_path):
292
+ """
293
+ Load camera transformer from NPZ archive file.
294
+
295
+ Args:
296
+ archive_path: Path to .npz calibration file
297
+
298
+ Returns:
299
+ CameraProjectionVectorized instance
300
+ """
301
+
302
+ # Convert to regular dict for easier access
303
+ cam_data = read_npz_file(archive_path)
304
+
305
+ return cls(cam_data)
@@ -0,0 +1,84 @@
1
+ import numpy as np
2
+
3
+
4
+ # Method 1: Read from a saved file
5
+ def read_npz_file(filename):
6
+ """Read camera projection data from saved .npz file.
7
+
8
+ Args:
9
+ filename (str): Path to the .npz file containing camera projection data
10
+
11
+ Returns:
12
+ dict: Dictionary containing the following keys:
13
+ - plan_scale (float): Scale factor for ground plane coordinates (pixels per meter)
14
+ - im_width (int): Width of the image in pixels
15
+ - im_height (int): Height of the image in pixels
16
+ - src2ctd (np.ndarray): Map of coordinates for undistorted (corrected) image.
17
+ Shape: (height, width, 2) where channel 0 is X, channel 1 is Y
18
+ - ctd2src (np.ndarray): Map of coordinates for distorted (raw) image based on undistorted.
19
+ Shape: (height, width, 2) where channel 0 is X, channel 1 is Y
20
+ - map_scale_h (np.ndarray): Scalar map of height scale values. Shape: (height, width)
21
+ - map_scale_w (np.ndarray): Scalar map of width scale values. Shape: (height, width)
22
+ - 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
34
+ """
35
+ data = np.load(filename)
36
+
37
+ plan_scale = data["plan_scale"]
38
+ im_width = data["im_width"]
39
+ im_height = data["im_height"]
40
+
41
+ # Access the arrays
42
+ src2ctd = data["src2ctd"]
43
+ ctd2src = data["ctd2src"]
44
+ map_scale_h = data["map_scale_h"]
45
+ map_scale_w = data["map_scale_w"]
46
+ map_scale_vang = data["map_scale_vang"]
47
+
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"])
60
+
61
+ # Don't forget to close the file
62
+ data.close()
63
+
64
+ return {
65
+ "plan_scale": plan_scale,
66
+ "im_width": im_width,
67
+ "im_height": im_height,
68
+ "src2ctd": src2ctd,
69
+ "ctd2src": ctd2src,
70
+ "map_scale_h": map_scale_h,
71
+ "map_scale_w": map_scale_w,
72
+ "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,
84
+ }
camera_client/utils.py ADDED
@@ -0,0 +1,178 @@
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
+ expr = expr.replace("sqrt", "np.sqrt")
50
+ expr = expr.replace("sin(", "np.sin(")
51
+ expr = expr.replace("cos(", "np.cos(")
52
+ expr = expr.replace("tan(", "np.tan(")
53
+ # Handle cases where np.np might occur
54
+ expr = expr.replace("np.np.", "np.")
55
+ return expr
56
+
57
+
58
+ def validate_expression(expr, allowed_vars):
59
+ """
60
+ Validate that expression only contains safe operations.
61
+
62
+ Args:
63
+ expr: String expression to validate
64
+ allowed_vars: Set of allowed variable names
65
+
66
+ Raises:
67
+ ValueError: If expression contains dangerous operations
68
+ """
69
+ try:
70
+ tree = ast.parse(expr, mode="eval")
71
+ except SyntaxError as e:
72
+ raise ValueError(f"Invalid expression syntax: {expr}") from e
73
+
74
+ # Walk through AST and check all nodes
75
+ for node in ast.walk(tree):
76
+ if isinstance(node, ast.Name):
77
+ # Check variable names
78
+ if node.id not in allowed_vars and node.id not in ALLOWED_FUNCTIONS:
79
+ raise ValueError(
80
+ f"Disallowed variable '{node.id}' in expression: {expr}"
81
+ )
82
+
83
+ elif isinstance(node, ast.Call):
84
+ # Check function calls
85
+ if isinstance(node.func, ast.Attribute):
86
+ # Handle np.sqrt, np.sin, etc.
87
+ if isinstance(node.func.value, ast.Name):
88
+ if node.func.value.id != "np":
89
+ raise ValueError(
90
+ f"Disallowed module '{node.func.value.id}' in expression: {expr}"
91
+ )
92
+ # np.function_name - allow numpy functions
93
+ else:
94
+ raise ValueError(f"Complex attribute access not allowed: {expr}")
95
+
96
+ elif isinstance(node.func, ast.Name):
97
+ if node.func.id not in ALLOWED_FUNCTIONS:
98
+ raise ValueError(
99
+ f"Disallowed function '{node.func.id}' in expression: {expr}"
100
+ )
101
+ else:
102
+ raise ValueError(f"Complex function call not allowed: {expr}")
103
+
104
+ elif isinstance(node, ast.BinOp):
105
+ # Check binary operations
106
+ if type(node.op) not in ALLOWED_OPERATORS:
107
+ raise ValueError(
108
+ f"Disallowed operator '{node.op.__class__.__name__}' in expression: {expr}"
109
+ )
110
+
111
+ elif isinstance(node, ast.UnaryOp):
112
+ # Check unary operations
113
+ if type(node.op) not in ALLOWED_OPERATORS:
114
+ raise ValueError(
115
+ f"Disallowed unary operator '{node.op.__class__.__name__}' in expression: {expr}"
116
+ )
117
+
118
+ elif isinstance(node, (ast.Attribute, ast.Import, ast.ImportFrom)):
119
+ # Block imports and most attribute access
120
+ if isinstance(node, ast.Attribute):
121
+ # Only allow np.function
122
+ if not (isinstance(node.value, ast.Name) and node.value.id == "np"):
123
+ raise ValueError(
124
+ f"Disallowed attribute access in expression: {expr}"
125
+ )
126
+ else:
127
+ raise ValueError(f"Import statements not allowed in expression: {expr}")
128
+
129
+ elif isinstance(node, (ast.Lambda, ast.FunctionDef, ast.ClassDef)):
130
+ raise ValueError(
131
+ f"Function/class definitions not allowed in expression: {expr}"
132
+ )
133
+
134
+ elif isinstance(node, (ast.ListComp, ast.DictComp, ast.GeneratorExp)):
135
+ raise ValueError(f"Comprehensions not allowed in expression: {expr}")
136
+
137
+
138
+ def compile_safe_expression(expr, param_names, allowed_vars=None):
139
+ """
140
+ Validate and compile expression into a callable function.
141
+
142
+ Args:
143
+ expr: String expression to compile
144
+ param_names: List of parameter names expected in expression
145
+ allowed_vars: Set of allowed variable names (defaults to param_names + 'np')
146
+
147
+ Returns:
148
+ Compiled function that evaluates the expression
149
+
150
+ Raises:
151
+ ValueError: If expression validation fails
152
+ """
153
+ # Default allowed vars to param_names plus 'np'
154
+ if allowed_vars is None:
155
+ allowed_vars = set(param_names) | {"np"}
156
+
157
+ # Normalize the expression
158
+ normalized_expr = normalize_expression(expr)
159
+
160
+ # Validate the expression
161
+ validate_expression(normalized_expr, allowed_vars)
162
+
163
+ # Compile the expression
164
+ code = compile(normalized_expr, "<string>", "eval")
165
+
166
+ # Create restricted namespace with only allowed functions
167
+ safe_namespace = {
168
+ "np": np,
169
+ "__builtins__": {}, # Remove all builtins
170
+ }
171
+ safe_namespace.update(ALLOWED_FUNCTIONS)
172
+
173
+ def compiled_func(*args):
174
+ # Create local namespace with parameter values
175
+ local_vars = dict(zip(param_names, args))
176
+ return eval(code, safe_namespace, local_vars)
177
+
178
+ return compiled_func
@@ -0,0 +1,284 @@
1
+ Metadata-Version: 2.4
2
+ Name: camera-client
3
+ Version: 0.1.0
4
+ Summary: Python SDK for camera calibration projection data - transform between distorted, corrected, and world coordinates.
5
+ Author-email: Alexander Abramov <extremal.ru@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/avabr/camera-client
8
+ Project-URL: Repository, https://github.com/avabr/camera-client
9
+ Project-URL: Issues, https://github.com/avabr/camera-client/issues
10
+ Keywords: camera,calibration,projection,3d,mathematics
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.7
17
+ Classifier: Programming Language :: Python :: 3.8
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
24
+ Requires-Python: >=3.7
25
+ Description-Content-Type: text/markdown
26
+ License-File: LICENSE
27
+ Requires-Dist: numpy>=1.20.0
28
+ Dynamic: license-file
29
+
30
+ # camera-client
31
+
32
+ Python SDK for camera calibration and projection transformations. Transform coordinates between distorted image space, corrected image space, and real-world 3D coordinates using pre-computed calibration data.
33
+
34
+ ## Features
35
+
36
+ - **Vectorized operations** - Process multiple points simultaneously for high performance
37
+ - **Multiple coordinate systems** - Transform between source (distorted), corrected, and ground (3D world) coordinates
38
+ - **Lens distortion handling** - Correct for camera lens distortion using calibration lookup tables
39
+ - **Ground plane projection** - Project image coordinates to 3D world coordinates and vice versa
40
+ - **NumPy-based** - Fast array operations with minimal dependencies
41
+
42
+ ## Installation
43
+
44
+ Install from PyPI:
45
+
46
+ ```bash
47
+ pip install camera-client
48
+ ```
49
+
50
+ ## Quick Start
51
+
52
+ ```python
53
+ import numpy as np
54
+ from camera_client import CameraProjection
55
+
56
+ # Load camera calibration data from NPZ archive
57
+ camera = CameraProjection.load("camera_calibration.npz")
58
+
59
+ # Transform single or multiple points (vectorized operations)
60
+ source_points = np.array([
61
+ [100, 200],
62
+ [300, 400],
63
+ [500, 600]
64
+ ])
65
+
66
+ # Remove lens distortion
67
+ corrected_points = camera.src_to_ctd(source_points)
68
+
69
+ # Project to ground plane (height = 0)
70
+ ground_points = camera.src_to_gnd(source_points, h=0)
71
+ print(ground_points) # Returns (N, 3) array with [x, y, z] coordinates
72
+ ```
73
+
74
+ ## Coordinate Systems
75
+
76
+ This library handles transformations between three coordinate systems:
77
+
78
+ - **src** (Source): Distorted image coordinates from the camera
79
+ - **ctd** (Corrected): Undistorted image coordinates after lens correction
80
+ - **gnd** (Ground): Real-world 3D coordinates (x, y, z)
81
+
82
+ ```
83
+ Distorted Undistorted World 3D
84
+ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
85
+ │ │ │ │ │ │
86
+ │ Source (src) │ <──> │ Corrected (ctd) │ <──> │ Ground (gnd) │
87
+ │ │ │ │ │ │
88
+ └─────────────────┘ └─────────────────┘ └─────────────────┘
89
+ Lens distortion Lens correction 3D projection
90
+ ```
91
+
92
+ ## Usage Examples
93
+
94
+ ### Basic Coordinate Transformations
95
+
96
+ ```python
97
+ from camera_client import CameraProjection
98
+ import numpy as np
99
+
100
+ # Load calibration data
101
+ camera = CameraProjection.load("camera_calibration.npz")
102
+
103
+ # Source (distorted) to Corrected (undistorted)
104
+ src_points = np.array([[640, 480], [1280, 720]])
105
+ ctd_points = camera.src_to_ctd(src_points)
106
+
107
+ # Corrected back to Source
108
+ src_points_back = camera.ctd_to_src(ctd_points)
109
+
110
+ # Check round-trip accuracy
111
+ error = np.linalg.norm(src_points - src_points_back, axis=1)
112
+ print(f"Round-trip error: {error}")
113
+ ```
114
+
115
+ ### 3D Ground Projection
116
+
117
+ ```python
118
+ # Project image points to ground plane
119
+ src_points = np.array([[640, 480], [800, 600]])
120
+
121
+ # Project to ground at height = 0 (ground level)
122
+ ground_points = camera.src_to_gnd(src_points, h=0)
123
+ print(ground_points) # Shape: (2, 3) with [x, y, z] coordinates
124
+
125
+ # Project to elevated plane (e.g., 1.5 meters above ground)
126
+ elevated_points = camera.src_to_gnd(src_points, h=1.5)
127
+
128
+ # Different height for each point
129
+ heights = np.array([0, 1.5])
130
+ mixed_points = camera.src_to_gnd(src_points, h=heights)
131
+ ```
132
+
133
+ ### Reverse Projection (3D to Image)
134
+
135
+ ```python
136
+ # Project 3D world coordinates back to image
137
+ world_points = np.array([
138
+ [10.0, 5.0, 0.0], # x, y, z in meters
139
+ [15.0, 8.0, 1.5]
140
+ ])
141
+
142
+ # Get corrected image coordinates
143
+ ctd_points = camera.gnd_to_ctd(world_points)
144
+
145
+ # Get source (distorted) image coordinates
146
+ src_points = camera.gnd_to_src(world_points)
147
+ ```
148
+
149
+ ### Batch Processing
150
+
151
+ ```python
152
+ # Process large batches of points efficiently
153
+ num_points = 10000
154
+ random_points = np.random.rand(num_points, 2) * [1920, 1080]
155
+
156
+ # Vectorized transformation (fast!)
157
+ corrected = camera.src_to_ctd(random_points)
158
+ ground = camera.src_to_gnd(random_points, h=0)
159
+ ```
160
+
161
+ ### Accessing Camera Properties
162
+
163
+ ```python
164
+ # Get camera dimensions
165
+ print(f"Image size: {camera.im_width} x {camera.im_height}")
166
+ print(f"Image WH: {camera.im_wh_size}")
167
+ print(f"Plan scale: {camera.plan_scale}")
168
+ ```
169
+
170
+ ## API Reference
171
+
172
+ ### `CameraProjection.load(archive_path)`
173
+
174
+ Load camera calibration from NPZ file.
175
+
176
+ **Parameters:**
177
+ - `archive_path` (str): Path to .npz calibration archive
178
+
179
+ **Returns:**
180
+ - `CameraProjection` instance
181
+
182
+ ---
183
+
184
+ ### `src_to_ctd(points)`
185
+
186
+ Transform from source (distorted) to corrected coordinates.
187
+
188
+ **Parameters:**
189
+ - `points` (np.ndarray): Shape (N, 2) array of [x, y] coordinates
190
+
191
+ **Returns:**
192
+ - `np.ndarray`: Shape (N, 2) corrected coordinates
193
+
194
+ ---
195
+
196
+ ### `ctd_to_src(points)`
197
+
198
+ Transform from corrected to source (distorted) coordinates.
199
+
200
+ **Parameters:**
201
+ - `points` (np.ndarray): Shape (N, 2) array of [x, y] coordinates
202
+
203
+ **Returns:**
204
+ - `np.ndarray`: Shape (N, 2) source coordinates
205
+
206
+ ---
207
+
208
+ ### `src_to_gnd(points, h)`
209
+
210
+ Transform from source coordinates to 3D ground coordinates.
211
+
212
+ **Parameters:**
213
+ - `points` (np.ndarray): Shape (N, 2) array of [x, y] coordinates
214
+ - `h` (float or np.ndarray): Height(s) above ground. Scalar or shape (N,) array
215
+
216
+ **Returns:**
217
+ - `np.ndarray`: Shape (N, 3) ground coordinates [x, y, z]
218
+
219
+ ---
220
+
221
+ ### `gnd_to_src(points)`
222
+
223
+ Transform from 3D ground coordinates to source coordinates.
224
+
225
+ **Parameters:**
226
+ - `points` (np.ndarray): Shape (N, 3) array of [x, y, z] coordinates
227
+
228
+ **Returns:**
229
+ - `np.ndarray`: Shape (N, 2) source coordinates
230
+
231
+ ---
232
+
233
+ ### `ctd_to_gnd(points, h)`
234
+
235
+ Transform from corrected coordinates to 3D ground coordinates.
236
+
237
+ **Parameters:**
238
+ - `points` (np.ndarray): Shape (N, 2) array of [x, y] coordinates
239
+ - `h` (float or np.ndarray): Height(s) above ground
240
+
241
+ **Returns:**
242
+ - `np.ndarray`: Shape (N, 3) ground coordinates
243
+
244
+ ---
245
+
246
+ ### `gnd_to_ctd(points)`
247
+
248
+ Transform from 3D ground coordinates to corrected coordinates.
249
+
250
+ **Parameters:**
251
+ - `points` (np.ndarray): Shape (N, 3) array of [x, y, z] coordinates
252
+
253
+ **Returns:**
254
+ - `np.ndarray`: Shape (N, 2) corrected coordinates
255
+
256
+ ## Calibration File Format
257
+
258
+ The calibration file is a NumPy `.npz` archive containing:
259
+
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
266
+
267
+ ## Requirements
268
+
269
+ - Python >= 3.7
270
+ - NumPy >= 1.20.0
271
+
272
+ ## Links
273
+
274
+ - **Repository**: [https://github.com/avabr/camera-client](https://github.com/avabr/camera-client)
275
+ - **Issues**: [https://github.com/avabr/camera-client/issues](https://github.com/avabr/camera-client/issues)
276
+ - **PyPI**: [https://pypi.org/project/camera-client/](https://pypi.org/project/camera-client/)
277
+
278
+ ## License
279
+
280
+ MIT License - see [LICENSE](LICENSE) file for details.
281
+
282
+ ## Author
283
+
284
+ Alexander Abramov ([extremal.ru@gmail.com](mailto:extremal.ru@gmail.com))
@@ -0,0 +1,10 @@
1
+ camera_client/__init__.py,sha256=XAIJyYru2yFfyPVH7bnS17g0PhZ3kOh6KpFHtIAedWc,82
2
+ camera_client/client.py,sha256=_K2nBARp9Y9tLR09ST_t_EBJK77Ys7ne1AaYJBYoOBI,9965
3
+ camera_client/loading.py,sha256=j90sW93vlry2C3jC3yVyvx9bWCKD6ztTwRRZ0_KXkqk,3477
4
+ camera_client/utils.py,sha256=QgZBygEKU5XGFFYn9p68rP08x68yXCXtELsz8iTkwAs,5933
5
+ camera_client-0.1.0.dist-info/licenses/LICENSE,sha256=1i4BjaOpClb6tkjlx0xls3RuF1q2AOeAG0uhG0y_RUU,1065
6
+ camera_client-0.1.0.dist-info/METADATA,sha256=cmV7PczFNE3E0uhQV3Zp6FuSqq2tBWepWTonNjVCQiQ,8511
7
+ camera_client-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
8
+ camera_client-0.1.0.dist-info/entry_points.txt,sha256=WR81o3UOxpPbLmVZXnncFFC9-GU1jbxNb0LSG3-2waA,60
9
+ camera_client-0.1.0.dist-info/top_level.txt,sha256=a1RgpRkQgXtlz1R9wYa4FF3b1B_r5Q9ic7TdoCuxlx4,14
10
+ camera_client-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ camera-client = camera_client.script:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alexandr
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ camera_client