nrtk-albumentations 2.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.

Potentially problematic release.


This version of nrtk-albumentations might be problematic. Click here for more details.

Files changed (62) hide show
  1. albumentations/__init__.py +21 -0
  2. albumentations/augmentations/__init__.py +23 -0
  3. albumentations/augmentations/blur/__init__.py +0 -0
  4. albumentations/augmentations/blur/functional.py +438 -0
  5. albumentations/augmentations/blur/transforms.py +1633 -0
  6. albumentations/augmentations/crops/__init__.py +0 -0
  7. albumentations/augmentations/crops/functional.py +494 -0
  8. albumentations/augmentations/crops/transforms.py +3647 -0
  9. albumentations/augmentations/dropout/__init__.py +0 -0
  10. albumentations/augmentations/dropout/channel_dropout.py +134 -0
  11. albumentations/augmentations/dropout/coarse_dropout.py +567 -0
  12. albumentations/augmentations/dropout/functional.py +1017 -0
  13. albumentations/augmentations/dropout/grid_dropout.py +166 -0
  14. albumentations/augmentations/dropout/mask_dropout.py +274 -0
  15. albumentations/augmentations/dropout/transforms.py +461 -0
  16. albumentations/augmentations/dropout/xy_masking.py +186 -0
  17. albumentations/augmentations/geometric/__init__.py +0 -0
  18. albumentations/augmentations/geometric/distortion.py +1238 -0
  19. albumentations/augmentations/geometric/flip.py +752 -0
  20. albumentations/augmentations/geometric/functional.py +4151 -0
  21. albumentations/augmentations/geometric/pad.py +676 -0
  22. albumentations/augmentations/geometric/resize.py +956 -0
  23. albumentations/augmentations/geometric/rotate.py +864 -0
  24. albumentations/augmentations/geometric/transforms.py +1962 -0
  25. albumentations/augmentations/mixing/__init__.py +0 -0
  26. albumentations/augmentations/mixing/domain_adaptation.py +787 -0
  27. albumentations/augmentations/mixing/domain_adaptation_functional.py +453 -0
  28. albumentations/augmentations/mixing/functional.py +878 -0
  29. albumentations/augmentations/mixing/transforms.py +832 -0
  30. albumentations/augmentations/other/__init__.py +0 -0
  31. albumentations/augmentations/other/lambda_transform.py +180 -0
  32. albumentations/augmentations/other/type_transform.py +261 -0
  33. albumentations/augmentations/pixel/__init__.py +0 -0
  34. albumentations/augmentations/pixel/functional.py +4226 -0
  35. albumentations/augmentations/pixel/transforms.py +7556 -0
  36. albumentations/augmentations/spectrogram/__init__.py +0 -0
  37. albumentations/augmentations/spectrogram/transform.py +220 -0
  38. albumentations/augmentations/text/__init__.py +0 -0
  39. albumentations/augmentations/text/functional.py +272 -0
  40. albumentations/augmentations/text/transforms.py +299 -0
  41. albumentations/augmentations/transforms3d/__init__.py +0 -0
  42. albumentations/augmentations/transforms3d/functional.py +393 -0
  43. albumentations/augmentations/transforms3d/transforms.py +1422 -0
  44. albumentations/augmentations/utils.py +249 -0
  45. albumentations/core/__init__.py +0 -0
  46. albumentations/core/bbox_utils.py +920 -0
  47. albumentations/core/composition.py +1885 -0
  48. albumentations/core/hub_mixin.py +299 -0
  49. albumentations/core/keypoints_utils.py +521 -0
  50. albumentations/core/label_manager.py +339 -0
  51. albumentations/core/pydantic.py +239 -0
  52. albumentations/core/serialization.py +352 -0
  53. albumentations/core/transforms_interface.py +976 -0
  54. albumentations/core/type_definitions.py +127 -0
  55. albumentations/core/utils.py +605 -0
  56. albumentations/core/validation.py +129 -0
  57. albumentations/pytorch/__init__.py +1 -0
  58. albumentations/pytorch/transforms.py +189 -0
  59. nrtk_albumentations-2.1.0.dist-info/METADATA +196 -0
  60. nrtk_albumentations-2.1.0.dist-info/RECORD +62 -0
  61. nrtk_albumentations-2.1.0.dist-info/WHEEL +4 -0
  62. nrtk_albumentations-2.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,521 @@
1
+ """Module for handling keypoint operations during augmentation.
2
+
3
+ This module provides utilities for working with keypoints in various formats during
4
+ the augmentation process. It includes functions for converting between coordinate systems,
5
+ filtering keypoints based on visibility, validating keypoint data, and applying
6
+ transformations to keypoints. The module supports different keypoint formats including
7
+ xy, yx, and those with additional angle or size information.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import math
13
+ from collections.abc import Sequence
14
+ from typing import Any, Literal
15
+
16
+ import numpy as np
17
+
18
+ from albumentations.core.type_definitions import NUM_KEYPOINTS_COLUMNS_IN_ALBUMENTATIONS
19
+
20
+ from .utils import DataProcessor, Params, ShapeType
21
+
22
+ __all__ = [
23
+ "KeypointParams",
24
+ "KeypointsProcessor",
25
+ "angle_to_2pi_range",
26
+ "check_keypoints",
27
+ "convert_keypoints_from_albumentations",
28
+ "convert_keypoints_to_albumentations",
29
+ "filter_keypoints",
30
+ ]
31
+
32
+ keypoint_formats = {"xy", "yx", "xya", "xys", "xyas", "xysa", "xyz"}
33
+
34
+
35
+ def angle_to_2pi_range(angles: np.ndarray) -> np.ndarray:
36
+ """Convert angles to the range [0, 2π).
37
+
38
+ This function takes an array of angles and ensures they are all within
39
+ the range of 0 to 2π (exclusive) by applying modulo 2π.
40
+
41
+ Args:
42
+ angles (np.ndarray): Array of angle values in radians.
43
+
44
+ Returns:
45
+ np.ndarray: Array of the same shape as input with angles normalized to [0, 2π).
46
+
47
+ """
48
+ return np.mod(angles, 2 * np.pi)
49
+
50
+
51
+ class KeypointParams(Params):
52
+ """Parameters of keypoints
53
+
54
+ Args:
55
+ format (str): format of keypoints. Should be 'xy', 'yx', 'xya', 'xys', 'xyas', 'xysa', 'xyz'.
56
+
57
+ x - X coordinate,
58
+
59
+ y - Y coordinate
60
+
61
+ z - Z coordinate (for 3D keypoints)
62
+
63
+ s - Keypoint scale
64
+
65
+ a - Keypoint orientation in radians or degrees (depending on KeypointParams.angle_in_degrees)
66
+
67
+ label_fields (list[str]): list of fields that are joined with keypoints, e.g labels.
68
+ Should be same type as keypoints.
69
+ remove_invisible (bool): to remove invisible points after transform or not
70
+ angle_in_degrees (bool): angle in degrees or radians in 'xya', 'xyas', 'xysa' keypoints
71
+ check_each_transform (bool): if `True`, then keypoints will be checked after each dual transform.
72
+ Default: `True`
73
+
74
+ Note:
75
+ The internal Albumentations format is [x, y, z, angle, scale]. For 2D formats (xy, yx, xya, xys, xyas, xysa),
76
+ z coordinate is set to 0. For formats without angle or scale, these values are set to 0.
77
+
78
+ """
79
+
80
+ def __init__(
81
+ self,
82
+ format: str, # noqa: A002
83
+ label_fields: Sequence[str] | None = None,
84
+ remove_invisible: bool = True,
85
+ angle_in_degrees: bool = True,
86
+ check_each_transform: bool = True,
87
+ ):
88
+ super().__init__(format, label_fields)
89
+ self.remove_invisible = remove_invisible
90
+ self.angle_in_degrees = angle_in_degrees
91
+ self.check_each_transform = check_each_transform
92
+
93
+ def to_dict_private(self) -> dict[str, Any]:
94
+ """Get the private dictionary representation of keypoint parameters.
95
+
96
+ Returns:
97
+ dict[str, Any]: Dictionary containing the keypoint parameters.
98
+
99
+ """
100
+ data = super().to_dict_private()
101
+ data.update(
102
+ {
103
+ "remove_invisible": self.remove_invisible,
104
+ "angle_in_degrees": self.angle_in_degrees,
105
+ "check_each_transform": self.check_each_transform,
106
+ },
107
+ )
108
+ return data
109
+
110
+ @classmethod
111
+ def is_serializable(cls) -> bool:
112
+ """Check if the keypoint parameters are serializable.
113
+
114
+ Returns:
115
+ bool: Always returns True as KeypointParams is serializable.
116
+
117
+ """
118
+ return True
119
+
120
+ @classmethod
121
+ def get_class_fullname(cls) -> str:
122
+ """Get the full name of the class.
123
+
124
+ Returns:
125
+ str: The string "KeypointParams".
126
+
127
+ """
128
+ return "KeypointParams"
129
+
130
+ def __repr__(self) -> str:
131
+ return (
132
+ f"KeypointParams(format={self.format}, label_fields={self.label_fields},"
133
+ f" remove_invisible={self.remove_invisible}, angle_in_degrees={self.angle_in_degrees},"
134
+ f" check_each_transform={self.check_each_transform})"
135
+ )
136
+
137
+
138
+ class KeypointsProcessor(DataProcessor):
139
+ """Processor for keypoint data transformation.
140
+
141
+ This class handles the conversion, validation, and filtering of keypoints
142
+ during transformations. It ensures keypoints are correctly formatted and
143
+ processed according to the specified keypoint parameters.
144
+
145
+ Args:
146
+ params (KeypointParams): Parameters for keypoint processing.
147
+ additional_targets (dict[str, str] | None): Dictionary mapping additional target names to their types.
148
+
149
+ """
150
+
151
+ def __init__(self, params: KeypointParams, additional_targets: dict[str, str] | None = None):
152
+ super().__init__(params, additional_targets)
153
+
154
+ @property
155
+ def default_data_name(self) -> str:
156
+ """Get the default name for keypoint data.
157
+
158
+ Returns:
159
+ str: The string "keypoints".
160
+
161
+ """
162
+ return "keypoints"
163
+
164
+ def ensure_data_valid(self, data: dict[str, Any]) -> None:
165
+ """Ensure the provided data dictionary contains all required label fields.
166
+
167
+ Args:
168
+ data (dict[str, Any]): The data dictionary to validate.
169
+
170
+ Raises:
171
+ ValueError: If any label field specified in params is missing from the data.
172
+
173
+ """
174
+ if self.params.label_fields and not all(i in data for i in self.params.label_fields):
175
+ msg = "Your 'label_fields' are not valid - them must have same names as params in 'keypoint_params' dict"
176
+ raise ValueError(msg)
177
+
178
+ def filter(
179
+ self,
180
+ data: np.ndarray,
181
+ shape: ShapeType,
182
+ ) -> np.ndarray:
183
+ """Filter keypoints based on visibility within given shape.
184
+
185
+ Args:
186
+ data (np.ndarray): Keypoints in [x, y, z, angle, scale] format
187
+ shape (ShapeType): Shape to check against as {'height': height, 'width': width, 'depth': depth}
188
+
189
+ Returns:
190
+ np.ndarray: Filtered keypoints
191
+
192
+ """
193
+ self.params: KeypointParams
194
+ return filter_keypoints(data, shape, remove_invisible=self.params.remove_invisible)
195
+
196
+ def check(self, data: np.ndarray, shape: ShapeType) -> None:
197
+ """Check if keypoints are valid within the given shape.
198
+
199
+ Args:
200
+ data (np.ndarray): Keypoints to validate.
201
+ shape (ShapeType): Shape to check against.
202
+
203
+ """
204
+ check_keypoints(data, shape)
205
+
206
+ def convert_from_albumentations(
207
+ self,
208
+ data: np.ndarray,
209
+ shape: ShapeType,
210
+ ) -> np.ndarray:
211
+ """Convert keypoints from internal Albumentations format to the specified format.
212
+
213
+ Args:
214
+ data (np.ndarray): Keypoints in Albumentations format.
215
+ shape (ShapeType): Shape information for validation.
216
+
217
+ Returns:
218
+ np.ndarray: Converted keypoints in the target format.
219
+
220
+ """
221
+ if not data.size:
222
+ return data
223
+
224
+ params = self.params
225
+ return convert_keypoints_from_albumentations(
226
+ data,
227
+ params.format,
228
+ shape,
229
+ check_validity=params.remove_invisible,
230
+ angle_in_degrees=params.angle_in_degrees,
231
+ )
232
+
233
+ def convert_to_albumentations(
234
+ self,
235
+ data: np.ndarray,
236
+ shape: ShapeType,
237
+ ) -> np.ndarray:
238
+ """Convert keypoints from the specified format to internal Albumentations format.
239
+
240
+ Args:
241
+ data (np.ndarray): Keypoints in source format.
242
+ shape (ShapeType): Shape information for validation.
243
+
244
+ Returns:
245
+ np.ndarray: Converted keypoints in Albumentations format.
246
+
247
+ """
248
+ if not data.size:
249
+ return data
250
+ params = self.params
251
+ return convert_keypoints_to_albumentations(
252
+ data,
253
+ params.format,
254
+ shape,
255
+ check_validity=params.remove_invisible,
256
+ angle_in_degrees=params.angle_in_degrees,
257
+ )
258
+
259
+
260
+ def check_keypoints(keypoints: np.ndarray, shape: ShapeType) -> None:
261
+ """Check if keypoint coordinates are within valid ranges for the given shape.
262
+
263
+ This function validates that:
264
+ 1. All x-coordinates are within [0, width)
265
+ 2. All y-coordinates are within [0, height)
266
+ 3. If depth is provided in shape, z-coordinates are within [0, depth)
267
+ 4. Angles are within the range [0, 2π)
268
+
269
+ Args:
270
+ keypoints (np.ndarray): Array of keypoints with shape (N, 5+), where N is the number of keypoints.
271
+ - First 2 columns are always x, y
272
+ - Column 3 (if present) is z
273
+ - Column 4 (if present) is angle
274
+ - Column 5+ (if present) are additional attributes
275
+ shape (ShapeType): The shape of the image/volume:
276
+ - For 2D: {'height': int, 'width': int}
277
+ - For 3D: {'height': int, 'width': int, 'depth': int}
278
+
279
+ Raises:
280
+ ValueError: If any keypoint coordinate is outside the valid range, or if angles are invalid.
281
+ The error message will detail which keypoints are invalid and why.
282
+
283
+ Note:
284
+ - The function assumes that keypoint coordinates are in absolute pixel values, not normalized
285
+ - Angles are in radians
286
+ - Z-coordinates are only checked if 'depth' is present in shape
287
+
288
+ """
289
+ height, width = shape["height"], shape["width"]
290
+ has_depth = "depth" in shape
291
+
292
+ # Check x and y coordinates (always present)
293
+ x, y = keypoints[:, 0], keypoints[:, 1]
294
+ invalid_x = np.where((x < 0) | (x >= width))[0]
295
+ invalid_y = np.where((y < 0) | (y >= height))[0]
296
+
297
+ error_messages = []
298
+
299
+ # Handle x, y errors
300
+ for idx in sorted(set(invalid_x) | set(invalid_y)):
301
+ if idx in invalid_x:
302
+ error_messages.append(
303
+ f"Expected x for keypoint {keypoints[idx]} to be in range [0, {width}), got {x[idx]}",
304
+ )
305
+ if idx in invalid_y:
306
+ error_messages.append(
307
+ f"Expected y for keypoint {keypoints[idx]} to be in range [0, {height}), got {y[idx]}",
308
+ )
309
+
310
+ # Check z coordinates if depth is provided and keypoints have z
311
+ if has_depth and keypoints.shape[1] > 2:
312
+ z = keypoints[:, 2]
313
+ depth = shape["depth"]
314
+ invalid_z = np.where((z < 0) | (z >= depth))[0]
315
+ error_messages.extend(
316
+ f"Expected z for keypoint {keypoints[idx]} to be in range [0, {depth}), got {z[idx]}" for idx in invalid_z
317
+ )
318
+
319
+ # Check angles only if keypoints have angle column
320
+ if keypoints.shape[1] > 3:
321
+ angles = keypoints[:, 3]
322
+ invalid_angles = np.where((angles < 0) | (angles >= 2 * math.pi))[0]
323
+ error_messages.extend(
324
+ f"Expected angle for keypoint {keypoints[idx]} to be in range [0, 2π), got {angles[idx]}"
325
+ for idx in invalid_angles
326
+ )
327
+
328
+ if error_messages:
329
+ raise ValueError("\n".join(error_messages))
330
+
331
+
332
+ def filter_keypoints(
333
+ keypoints: np.ndarray,
334
+ shape: ShapeType,
335
+ remove_invisible: bool,
336
+ ) -> np.ndarray:
337
+ """Filter keypoints to remove those outside the boundaries.
338
+
339
+ Args:
340
+ keypoints (np.ndarray): A numpy array of shape (N, 5+) where N is the number of keypoints.
341
+ Each row represents a keypoint (x, y, z, angle, scale, ...).
342
+ shape (ShapeType): Shape to check against as {'height': height, 'width': width, 'depth': depth}.
343
+ remove_invisible (bool): If True, remove keypoints outside the boundaries.
344
+
345
+ Returns:
346
+ np.ndarray: Filtered keypoints.
347
+
348
+ """
349
+ if not remove_invisible:
350
+ return keypoints
351
+
352
+ if not keypoints.size:
353
+ return keypoints
354
+
355
+ height, width, depth = shape["height"], shape["width"], shape.get("depth", None)
356
+
357
+ # Create boolean mask for visible keypoints
358
+ x, y, z = keypoints[:, 0], keypoints[:, 1], keypoints[:, 2]
359
+ visible = (x >= 0) & (x < width) & (y >= 0) & (y < height)
360
+
361
+ if depth is not None:
362
+ visible &= (z >= 0) & (z < depth)
363
+
364
+ # Apply the mask to filter keypoints
365
+ return keypoints[visible]
366
+
367
+
368
+ def convert_keypoints_to_albumentations(
369
+ keypoints: np.ndarray,
370
+ source_format: Literal["xy", "yx", "xya", "xys", "xyas", "xysa", "xyz"],
371
+ shape: ShapeType,
372
+ check_validity: bool = False,
373
+ angle_in_degrees: bool = True,
374
+ ) -> np.ndarray:
375
+ """Convert keypoints from various formats to the Albumentations format.
376
+
377
+ This function takes keypoints in different formats and converts them to the standard
378
+ Albumentations format: [x, y, z, angle, scale]. For 2D formats, z is set to 0.
379
+ For formats without angle or scale, these values are set to 0.
380
+
381
+ Args:
382
+ keypoints (np.ndarray): Array of keypoints with shape (N, 2+), where N is the number of keypoints.
383
+ The number of columns depends on the source_format.
384
+ source_format (Literal["xy", "yx", "xya", "xys", "xyas", "xysa", "xyz"]): The format of the input keypoints.
385
+ - "xy": [x, y]
386
+ - "yx": [y, x]
387
+ - "xya": [x, y, angle]
388
+ - "xys": [x, y, scale]
389
+ - "xyas": [x, y, angle, scale]
390
+ - "xysa": [x, y, scale, angle]
391
+ - "xyz": [x, y, z]
392
+ shape (ShapeType): The shape of the image {'height': height, 'width': width, 'depth': depth}.
393
+ check_validity (bool, optional): If True, check if the converted keypoints are within the image boundaries.
394
+ Defaults to False.
395
+ angle_in_degrees (bool, optional): If True, convert input angles from degrees to radians.
396
+ Defaults to True.
397
+
398
+ Returns:
399
+ np.ndarray: Array of keypoints in Albumentations format [x, y, z, angle, scale] with shape (N, 5+).
400
+ Any additional columns from the input keypoints are preserved and appended after the
401
+ first 5 columns.
402
+
403
+ Raises:
404
+ ValueError: If the source_format is not one of the supported formats.
405
+
406
+ Note:
407
+ - For 2D formats (xy, yx, xya, xys, xyas, xysa), z coordinate is set to 0
408
+ - Angles are converted to the range [0, 2π) radians
409
+ - If the input keypoints have additional columns beyond what's specified in the source_format,
410
+ these columns are preserved in the output
411
+
412
+ """
413
+ if source_format not in keypoint_formats:
414
+ raise ValueError(f"Unknown source_format {source_format}. Supported formats are: {keypoint_formats}")
415
+
416
+ format_to_indices: dict[str, list[int | None]] = {
417
+ "xy": [0, 1, None, None, None],
418
+ "yx": [1, 0, None, None, None],
419
+ "xya": [0, 1, None, 2, None],
420
+ "xys": [0, 1, None, None, 2],
421
+ "xyas": [0, 1, None, 2, 3],
422
+ "xysa": [0, 1, None, 3, 2],
423
+ "xyz": [0, 1, 2, None, None],
424
+ }
425
+
426
+ indices: list[int | None] = format_to_indices[source_format]
427
+
428
+ processed_keypoints = np.zeros((keypoints.shape[0], NUM_KEYPOINTS_COLUMNS_IN_ALBUMENTATIONS), dtype=np.float32)
429
+
430
+ for i, idx in enumerate(indices):
431
+ if idx is not None:
432
+ processed_keypoints[:, i] = keypoints[:, idx]
433
+
434
+ if angle_in_degrees and indices[3] is not None: # angle is now at index 3
435
+ processed_keypoints[:, 3] = np.radians(processed_keypoints[:, 3])
436
+
437
+ processed_keypoints[:, 3] = angle_to_2pi_range(processed_keypoints[:, 3]) # angle is now at index 3
438
+
439
+ if keypoints.shape[1] > len(source_format):
440
+ processed_keypoints = np.column_stack((processed_keypoints, keypoints[:, len(source_format) :]))
441
+
442
+ if check_validity:
443
+ check_keypoints(processed_keypoints, shape)
444
+
445
+ return processed_keypoints
446
+
447
+
448
+ def convert_keypoints_from_albumentations(
449
+ keypoints: np.ndarray,
450
+ target_format: Literal["xy", "yx", "xya", "xys", "xyas", "xysa", "xyz"],
451
+ shape: ShapeType,
452
+ check_validity: bool = False,
453
+ angle_in_degrees: bool = True,
454
+ ) -> np.ndarray:
455
+ """Convert keypoints from Albumentations format to various other formats.
456
+
457
+ This function takes keypoints in the standard Albumentations format [x, y, z, angle, scale]
458
+ and converts them to the specified target format.
459
+
460
+ Args:
461
+ keypoints (np.ndarray): Array of keypoints in Albumentations format with shape (N, 5+),
462
+ where N is the number of keypoints. Each row represents a keypoint
463
+ [x, y, z, angle, scale, ...].
464
+ target_format (Literal["xy", "yx", "xya", "xys", "xyas", "xysa", "xyz"]): The desired output format.
465
+ - "xy": [x, y]
466
+ - "yx": [y, x]
467
+ - "xya": [x, y, angle]
468
+ - "xys": [x, y, scale]
469
+ - "xyas": [x, y, angle, scale]
470
+ - "xysa": [x, y, scale, angle]
471
+ - "xyz": [x, y, z]
472
+ shape (ShapeType): The shape of the image {'height': height, 'width': width, 'depth': depth}.
473
+ check_validity (bool, optional): If True, check if the keypoints are within the image boundaries.
474
+ Defaults to False.
475
+ angle_in_degrees (bool, optional): If True, convert output angles to degrees.
476
+ If False, angles remain in radians.
477
+ Defaults to True.
478
+
479
+ Returns:
480
+ np.ndarray: Array of keypoints in the specified target format with shape (N, 2+).
481
+ Any additional columns from the input keypoints beyond the first 5
482
+ are preserved and appended after the converted columns.
483
+
484
+ Raises:
485
+ ValueError: If the target_format is not one of the supported formats.
486
+
487
+ Note:
488
+ - Input angles are assumed to be in the range [0, 2π) radians
489
+ - If the input keypoints have additional columns beyond the first 5,
490
+ these columns are preserved in the output
491
+
492
+ """
493
+ if target_format not in keypoint_formats:
494
+ raise ValueError(f"Unknown target_format {target_format}. Supported formats are: {keypoint_formats}")
495
+
496
+ x, y, z, angle, scale = keypoints[:, 0], keypoints[:, 1], keypoints[:, 2], keypoints[:, 3], keypoints[:, 4]
497
+ angle = angle_to_2pi_range(angle)
498
+
499
+ if check_validity:
500
+ check_keypoints(np.column_stack((x, y, z, angle, scale)), shape)
501
+
502
+ if angle_in_degrees:
503
+ angle = np.degrees(angle)
504
+
505
+ format_to_columns = {
506
+ "xy": [x, y],
507
+ "yx": [y, x],
508
+ "xya": [x, y, angle],
509
+ "xys": [x, y, scale],
510
+ "xyas": [x, y, angle, scale],
511
+ "xysa": [x, y, scale, angle],
512
+ "xyz": [x, y, z],
513
+ }
514
+
515
+ result = np.column_stack(format_to_columns[target_format])
516
+
517
+ # Add any additional columns from the original keypoints
518
+ if keypoints.shape[1] > NUM_KEYPOINTS_COLUMNS_IN_ALBUMENTATIONS:
519
+ return np.column_stack((result, keypoints[:, NUM_KEYPOINTS_COLUMNS_IN_ALBUMENTATIONS:]))
520
+
521
+ return result