IDS peak ICV v1.3.0
Loading...
Searching...
No Matches
point_cloud.py
Go to the documentation of this file.
1from __future__ import annotations
2
3import ctypes
4from enum import IntEnum
5from typing import Union, TypeAlias, Callable, Iterable, Any, cast, Type, TYPE_CHECKING
6
7import numpy as np
8import numpy.typing as npt
9
10from ids_peak_ipl import ids_peak_ipl
11
12from ids_peak_icv import lib_loader
13from ids_peak_icv.backend.datatypes import peak_icv_point_cloud_handle, peak_icv_point_xyz, peak_icv_point_xyzi
14from ids_peak_icv.backend.utils import execute_and_map_return_codes, create_array
15from ids_peak_icv.exceptions import NotPossibleException, NotSupportedException, MismatchException
18from ids_peak_icv.datatypes.image import Image, _check_and_get_icv_image
19from ids_peak_icv.datatypes.xyz_image import XYZImage
20
21if TYPE_CHECKING:
22 from ids_peak_icv.datatypes.undistorted_image import UndistortedImage
23 from ids_peak_icv.calibration.extrinsic_parameters import ExtrinsicParameters
24
25# Note: We loose type info here, as it is stored as a pointer to a python object
26XYZArray: TypeAlias = npt.NDArray[np.object_]
27XYZIArray: TypeAlias = npt.NDArray[np.object_]
28
29
30class PointCloudPointType(IntEnum):
31 """!
32 @brief
33 Enumeration representing different types of point cloud points.
34
35 Attributes:
36 XYZ: Represents a point with X, Y and Z coordinates.
37 XYZ_I8: Represents a point with X, Y, Z coordinates and 8-bit intensity.
38 XYZ_I10: Represents a point with X, Y, Z coordinates and 10-bit intensity.
39 XYZ_I12: Represents a point with X, Y, Z coordinates and 12-bit intensity.
40
41 @ingroup ids_peak_icv_python_types
42 @since ids_peak_icv 1.1
43 """
44 XYZ = 0
45 XYZ_I8 = 1
46 XYZ_I10 = 2
47 XYZ_I12 = 3
48
49
51 """!
52 @brief
53 A point cloud is a set of data points defined in a three-dimensional (3D) coordinate system.
54
55 Each point is represented by its X, Y, and Z coordinates,
56 specifying its exact position in 3D space. Optionally it can contain intensity metadata.
57 Point clouds are often generated from depth maps,
58 such as those obtained from a time-of-flight camera.
59 They serve as a detailed digital representation of the shape and size of physical objects,
60 enabling precise measurements and analysis.
61
62 @ingroup ids_peak_icv_python_types
63 @since ids_peak_icv 1.1
64 """
65
66 @classmethod
67 def create_from_xyz_image(cls, xyz_image: XYZImage,
68 intensity_image: Union[Image, ids_peak_ipl.Image, None] = None) -> PointCloud:
69 """!
70 @brief
71 Creates a point cloud from a xyz image and optional metadata.
72
73 @note The region of the metadata image is disregarded.
74
75 @param xyz_image: XYZ image providing the spatial information.
76 @param intensity_image: Image that holds the meta information (e.g. an intensity image).
77 @return Created point cloud instance
78
79 @exception MismatchException: If the xyz_image and the intensity image don't have the same size.
80
81 @since ids_peak_icv 1.1
82 """
83 return cls(_xyz_image=xyz_image, _intensity_image=intensity_image)
84
85 @classmethod
86 def create_from_undistorted_depth_map(cls, undistorted_depth_map: UndistortedImage,
87 intensity_image: Union[
88 Image, ids_peak_ipl.Image, None] = None) -> PointCloud:
89 """!
90 @brief
91 Creates a point cloud from an undistorted depth map and optional metadata.
92
93 @note The region of the metadata image is disregarded.
94
95 @param undistorted_depth_map: Undistorted image providing the radial depth information.
96 @param intensity_image: Image that holds the meta information (e.g. an intensity image).
97 @return Created point cloud instance
98
99 @exception MismatchException: If the undistorted_depth_map and the intensity image don't have the same size.
100
101 @since ids_peak_icv 1.1
102 """
103 return cls(_xyz_image=XYZImage.create_from_undistorted_image(undistorted_depth_map),
104 _intensity_image=intensity_image)
105
106 @classmethod
107 def create_from_points(cls, points: Union[XYZArray, XYZIArray],
108 point_type: PointCloudPointType) -> PointCloud:
109 """!
110 @brief
111 Creates a point cloud from points.
112
113 @param points: An array of PointXYZ or PointXYZI.
114 @param point_type: The type of point.
115
116 @return Created point cloud instance
117
118 @exception MismatchException: If Parameter points is not a numpy array of PointXYZ or PointXYZI objects
119 matching the point_type.
120
121 @since ids_peak_icv 1.1
122 """
124
125 c_point_type: Type[peak_icv_point_xyz] | Type[peak_icv_point_xyzi]
126 if point_type == PointCloudPointType.XYZ:
127 c_point_type = peak_icv_point_xyz
128 elif (point_type == PointCloudPointType.XYZ_I8 or
129 point_type == PointCloudPointType.XYZ_I10 or
130 point_type == PointCloudPointType.XYZ_I12):
131 c_point_type = peak_icv_point_xyzi
132 else:
133 raise NotSupportedException("The given point type {} is not supported".format(point_type.name))
134
135 size = len(points)
136 c_points = create_array(c_point_type, size)
137
138 for i, p in enumerate(points):
139 if isinstance(p, PointXYZI) and isinstance(points,
140 np.ndarray) and point_type != PointCloudPointType.XYZ:
141 c_points[i] = peak_icv_point_xyzi(p.x, p.y, p.z, p.intensity)
142 elif isinstance(p, PointXYZ) and isinstance(points,
143 np.ndarray) and point_type == PointCloudPointType.XYZ:
144 c_points[i] = peak_icv_point_xyz(p.x, p.y, p.z)
145 else:
146 raise MismatchException(
147 "Parameter points has to be a numpy array of PointXYZ or PointXYZI objects and "
148 "the point_type has to match it.")
149
150 execute_and_map_return_codes(lib_loader.dll().peak_icv_PointCloud_CreateFromPoints,
151 ctypes.pointer(handle), c_points, ctypes.c_size_t(size),
152 ctypes.c_int(point_type))
153
154 return cls(_handle=handle, _point_type=point_type)
155
156 @classmethod
157 def create_from_file(cls, file_path: str,
158 point_type: PointCloudPointType = PointCloudPointType.XYZ) -> PointCloud:
159 """!
160 @brief
161 Loads a point cloud from a file.
162
163 The chosen file type is determined by the given extension.
164
165 The following file formats are supported:
166 - [PLY]
167
168 @param file_path: The file is loaded from the file path, which should encompass both the directory path and the
169 filename.
170 @param point_type: Defines whether the point cloud is loaded with or without meta information and how the meta
171 information is interpreted.
172
173 @return The point cloud, which was loaded from the file.
174
175 @exception IOException: If the specified file path is invalid, you lack the necessary permissions, or the file
176 already exists.
177
178 @since ids_peak_icv 1.1
179 """
181
182 c_file_path = file_path.encode('utf-8')
183 execute_and_map_return_codes(lib_loader.dll().peak_icv_PointCloud_CreateFromFile,
184 ctypes.pointer(handle),
185 ctypes.c_int(point_type),
186 ctypes.c_char_p(c_file_path),
187 ctypes.c_size_t(len(c_file_path)))
188
189 return PointCloud(_handle=handle, _point_type=point_type)
190
191 def __init__(self, *args: Any, **kwargs: Any) -> None:
192 """!
193 @brief
194 In order to create a point cloud, please refer to the class methods.
195
196 @since ids_peak_icv 1.1
197 """
198
199 self._handle = None
200 self._sorted_points_cache_sorted_points_cache: np.ndarray | None = None
201 self._points: np.ndarray | None = None
202 self._point_type = None
203
204 if "_handle" in kwargs:
205 self._point_type = kwargs["_point_type"]
206 self._handle = kwargs["_handle"]
207 return
208 elif "_xyz_image" not in kwargs:
210 "Cannot initialize {} with __init__. Please use a classmethod.".format(self.__class__.__name__))
211
212 xyz_image = _check_and_get_icv_image(kwargs["_xyz_image"])
213 if "_intensity_image" in kwargs:
214 meta_data = _check_and_get_icv_image(kwargs["_intensity_image"])
215 else:
216 meta_data = None
217
218 xyz_image_handle = xyz_image._handle
219
221
222 try:
223 if meta_data is None:
224 execute_and_map_return_codes(lib_loader.dll().peak_icv_PointCloud_CreateFromXYZImage,
225 ctypes.pointer(self._handle), xyz_image_handle)
226 else:
227 image_handle = meta_data._handle
228 execute_and_map_return_codes(lib_loader.dll().peak_icv_PointCloud_CreateFromXYZImageAndIntensityImage,
229 ctypes.pointer(self._handle), xyz_image_handle, image_handle)
230 except Exception as e:
231 # Cause the __del__ method is called if a handle is already created, we have to set the handle back to None
232 # and reraise the exception.
233 self._handle = None
234 raise e
235
236 self._init_point_type()
237
238 def transform(self, matrix: npt.NDArray) -> PointCloud:
239 """!
240 @brief
241 Applies a transformation of a point cloud based on the given matrix.
242
243 @param matrix: The transformation matrix
244 @return A new transformed point cloud.
245
246 @exception NotPossibleException: If the dtype of numpy array isn't type float32 or the matrix size isn't 4x4.
247
248 @since ids_peak_icv 1.1
249 """
250 if matrix.dtype != np.float32:
252 "Matrix has wrong numpy array type. dtype has to be float32.")
253
254 if matrix.shape != (4, 4):
256 f"Matrix has wrong shape {matrix.shape}. Shape has to be (4,4).")
257
259 execute_and_map_return_codes(lib_loader.dll().peak_icv_PointCloud_Create,
260 ctypes.pointer(handle))
261 execute_and_map_return_codes(lib_loader.dll().peak_icv_PointCloud_Transform,
262 self._handle, matrix.ctypes.data_as(ctypes.POINTER(ctypes.c_float)),
263 ctypes.pointer(handle))
264
265 return PointCloud(_handle=handle, _point_type=self._point_type)
266
267 def transform_to_workspace(self, extrinsic_parameters: ExtrinsicParameters) -> PointCloud:
268 """!
269 @brief
270 Transforms a point cloud to the workspace coordinate system using the extrinsic parameters.
271
272 The point cloud is transformed from the camera coordinate system to the workspace coordinate system
273 using the inverse of the extrinsic parameters.
274
275 @param extrinsic_parameters: Extrinsic parameters of the workspace calibration.
276 @return A new transformed point cloud.
277
278 @since ids_peak_icv 1.1
279 """
280
282 execute_and_map_return_codes(lib_loader.dll().peak_icv_PointCloud_Create,
283 ctypes.pointer(handle))
284 execute_and_map_return_codes(lib_loader.dll().peak_icv_PointCloud_TransformToWorkspace,
285 self._handle, extrinsic_parameters._parameters,
286 ctypes.sizeof(extrinsic_parameters._parameters),
287 ctypes.pointer(handle))
288
289 return PointCloud(_handle=handle, _point_type=self._point_type)
290
291 @property
292 def points(self) -> Union[XYZArray, XYZIArray]:
293 """!
294 @brief
295 Get the points of the point cloud.
296
297 @return List of Point objects representing the point clouds points.
298
299 @since ids_peak_icv 1.1
300 """
301 if self._points is not None:
302 return cast(Union[XYZArray, XYZIArray], self._points)
303
304 if self._point_type == PointCloudPointType.XYZ:
305 self._init_points(peak_icv_point_xyz, PointXYZ,
306 lambda c_points: (PointXYZ(p.x, p.y, p.z) for p in c_points))
307 elif (self._point_type == PointCloudPointType.XYZ_I8 or
308 self._point_type == PointCloudPointType.XYZ_I10 or
309 self._point_type == PointCloudPointType.XYZ_I12):
310 self._init_points(peak_icv_point_xyzi, PointXYZI,
311 lambda c_points: (PointXYZI(p.x, p.y, p.z, p.i) for p in c_points))
312 else:
313 raise NotSupportedException("The given point type is invalid")
314
315 return cast(Union[XYZArray, XYZIArray], self._points)
316
317 def __eq__(self, other: object) -> bool:
318 """!
319 @brief
320 Check if two point clouds are equal.
321
322 @param other: Another point cloud object
323
324 @return True if point clouds are equal, False otherwise
325
326 @since ids_peak_icv 1.1
327 """
328 if isinstance(other, PointCloud):
329 if self._handle == other._handle:
330 return True
331
333 self._sorted_points_cache_sorted_points_cache = np.sort(self.pointspoints, axis=None)
334
335 if other._sorted_points_cache is None:
336 other._sorted_points_cache = np.sort(other.points, axis=None)
337
338 return np.array_equal(self._sorted_points_cache_sorted_points_cache, other._sorted_points_cache)
339
340 if isinstance(other, list) or isinstance(other, tuple):
341 if len(self.pointspoints) != len(other):
342 return False
343
346
347 other_points = [PointXYZ(p[0], p[1], p[2]) for p in other]
348 sorted_other_points = sorted(other_points)
349
350 for i in range(len(self._sorted_points_cache_sorted_points_cache)):
351 if self._sorted_points_cache_sorted_points_cache[i] != sorted_other_points[i]:
352 return False
353 return True
354
355 return False
356
357 def __ne__(self, other: object) -> bool:
358 """!
359 @brief
360 Check if two point clouds are not equal.
361
362 @param other: Another point cloud object
363
364 @return True if point clouds are not equal, False otherwise
365
366 @since ids_peak_icv 1.1
367 """
368 return not self.__eq__(other)
369
370 def __del__(self) -> None:
371 if self._handle is not None:
372 lib_loader.dll().peak_icv_PointCloud_Destroy(self._handle)
373
374 def __repr__(self) -> str:
375 """!
376 @brief
377 Get a string representation of the Point Cloud represented by points.
378
379 @return String representation of the Point Cloud
380
381 @since ids_peak_icv 1.1
382 """
383 return f"({', '.join(map(str, self.points))})"
384
385 def save(self, file_path: str) -> None:
386 """!
387 @brief
388 Saves point cloud to file.
389
390 The chosen file type is determined by the given extension.
391
392 The following file formats are supported:
393 - [PLY](only binary)
394
395 If no file extension is provided, .ply is automatically added.
396
397 @note If a point cloud does not contain any points it cannot be written to file.
398
399 @param file_path: The file is saved to the file_path, which should encompass both the directory path and the
400 filename.
401
402 @exception IOException: If the specified file path is invalid or lacks write permissions.
403 @exception NotSupportedException: If the specified file extension or point type is not supported or the point cloud is empty.
404
405 @since ids_peak_icv 1.1
406 """
407 c_file_path = file_path.encode('utf-8')
408 lib_loader.dll().peak_icv_PointCloud_SaveToFile(self._handle,
409 ctypes.c_char_p(c_file_path),
410 ctypes.c_size_t(len(c_file_path)))
411
412 def _init_points(self, c_type: Type[ctypes.Structure], python_type: Type,
413 func: Callable[[ctypes.Array], Iterable[Any]]) -> None:
414
415 c_num_points = ctypes.c_size_t()
416 execute_and_map_return_codes(lib_loader.dll().peak_icv_PointCloud_GetPoints_GetCount,
417 self._handle, ctypes.pointer(c_num_points))
418
419 if c_num_points.value == 0:
420 self._points = np.ndarray(0)
421 return
422
423 c_size_in_bytes_points = ctypes.c_size_t()
424 execute_and_map_return_codes(lib_loader.dll().peak_icv_PointCloud_GetPoints_GetSizeInBytes,
425 self._handle, ctypes.pointer(c_size_in_bytes_points))
426
427 c_points = create_array(c_type, c_num_points.value)
428 execute_and_map_return_codes(lib_loader.dll().peak_icv_PointCloud_GetPoints,
429 self._handle, c_points, c_size_in_bytes_points)
430 iterable = func(c_points)
431 # workaround to fill the array on python3.7 with numpy
432 self._points = np.empty(len(c_points), dtype=python_type)
433
434 for i, it in enumerate(iterable):
435 self._points[i] = it
436
437 def _init_point_type(self) -> None:
438 point_type = ctypes.c_int()
439 execute_and_map_return_codes(lib_loader.dll().peak_icv_PointCloud_GetType, self._handle,
440 ctypes.pointer(point_type))
441 self._point_type = PointCloudPointType(point_type.value)
A point cloud is a set of data points defined in a three-dimensional (3D) coordinate system.
PointCloud transform_to_workspace(self, ExtrinsicParameters extrinsic_parameters)
Transforms a point cloud to the workspace coordinate system using the extrinsic parameters.
None __init__(self, *Any args, **Any kwargs)
In order to create a point cloud, please refer to the class methods.
None save(self, str file_path)
Saves point cloud to file.
bool __eq__(self, object other)
Check if two point clouds are equal.
PointCloud create_from_xyz_image(cls, XYZImage xyz_image, Union[Image, ids_peak_ipl.Image, None] intensity_image=None)
Creates a point cloud from a xyz image and optional metadata.
PointCloud create_from_file(cls, str file_path, PointCloudPointType point_type=PointCloudPointType.XYZ)
Loads a point cloud from a file.
PointCloud transform(self, npt.NDArray matrix)
Applies a transformation of a point cloud based on the given matrix.
Union[XYZArray, XYZIArray] points(self)
Get the points of the point cloud.
None _init_points(self, Type[ctypes.Structure] c_type, Type python_type, Callable[[ctypes.Array], Iterable[Any]] func)
PointCloud create_from_points(cls, Union[XYZArray, XYZIArray] points, PointCloudPointType point_type)
Creates a point cloud from points.
str __repr__(self)
Get a string representation of the Point Cloud represented by points.
bool __ne__(self, object other)
Check if two point clouds are not equal.
PointCloud create_from_undistorted_depth_map(cls, UndistortedImage undistorted_depth_map, Union[Image, ids_peak_ipl.Image, None] intensity_image=None)
Creates a point cloud from an undistorted depth map and optional metadata.
Enumeration representing different types of point cloud points.
Occurs when input parameters do not match expected conditions (e.g., two input images have different ...
Definition exceptions.py:45
Occurs when an operation cannot be performed due to logical constraints (e.g., camera calibration wit...
Definition exceptions.py:72
Occurs when a requested operation is not implemented or supported (e.g., exporting to an unsupported ...
Definition exceptions.py:60