maps4fs 1.8.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,624 @@
1
+ """This module contains the Config class for map settings and configuration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ from random import choice, randint, uniform
8
+ from typing import Generator
9
+ from xml.etree import ElementTree as ET
10
+
11
+ import cv2
12
+ import numpy as np
13
+ from tqdm import tqdm
14
+
15
+ from maps4fs.generator.component import Component
16
+ from maps4fs.generator.texture import Texture
17
+
18
+ DISPLACEMENT_LAYER_SIZE_FOR_BIG_MAPS = 32768
19
+ NODE_ID_STARTING_VALUE = 2000
20
+ SPLINES_NODE_ID_STARTING_VALUE = 5000
21
+ TREE_NODE_ID_STARTING_VALUE = 10000
22
+
23
+
24
+ # pylint: disable=R0903
25
+ class I3d(Component):
26
+ """Component for map i3d file settings and configuration.
27
+
28
+ Arguments:
29
+ game (Game): The game instance for which the map is generated.
30
+ coordinates (tuple[float, float]): The latitude and longitude of the center of the map.
31
+ map_size (int): The size of the map in pixels.
32
+ map_rotated_size (int): The size of the map in pixels after rotation.
33
+ rotation (int): The rotation angle of the map.
34
+ map_directory (str): The directory where the map files are stored.
35
+ logger (Any, optional): The logger to use. Must have at least three basic methods: debug,
36
+ info, warning. If not provided, default logging will be used.
37
+ """
38
+
39
+ _map_i3d_path: str | None = None
40
+
41
+ def preprocess(self) -> None:
42
+ """Gets the path to the map I3D file from the game instance and saves it to the instance
43
+ attribute. If the game does not support I3D files, the attribute is set to None."""
44
+ try:
45
+ self._map_i3d_path = self.game.i3d_file_path(self.map_directory)
46
+ self.logger.debug("Map I3D path: %s.", self._map_i3d_path)
47
+ except NotImplementedError:
48
+ self.logger.warning("I3D file processing is not implemented for this game.")
49
+ self._map_i3d_path = None
50
+
51
+ def process(self) -> None:
52
+ """Updates the map I3D file with the default settings."""
53
+ self._update_i3d_file()
54
+ self._add_fields()
55
+ if self.game.code == "FS25":
56
+ self._add_forests()
57
+ self._add_splines()
58
+
59
+ def _get_tree(self) -> ET.ElementTree | None:
60
+ """Returns the ElementTree instance of the map I3D file."""
61
+ if not self._map_i3d_path:
62
+ self.logger.debug("I3D is not obtained, skipping the update.")
63
+ return None
64
+ if not os.path.isfile(self._map_i3d_path):
65
+ self.logger.warning("I3D file not found: %s.", self._map_i3d_path)
66
+ return None
67
+
68
+ return ET.parse(self._map_i3d_path)
69
+
70
+ def _update_i3d_file(self) -> None:
71
+ """Updates the map I3D file with the default settings."""
72
+
73
+ tree = self._get_tree()
74
+ if tree is None:
75
+ return
76
+
77
+ self.logger.debug("Map I3D file loaded from: %s.", self._map_i3d_path)
78
+
79
+ root = tree.getroot()
80
+ for map_elem in root.iter("Scene"):
81
+ for terrain_elem in map_elem.iter("TerrainTransformGroup"):
82
+ if self.map.shared_settings.change_height_scale:
83
+ suggested_height_scale = self.map.shared_settings.height_scale_value
84
+ if suggested_height_scale is not None and suggested_height_scale > 255:
85
+ new_height_scale = int(
86
+ self.map.shared_settings.height_scale_value # type: ignore
87
+ )
88
+ terrain_elem.set("heightScale", str(new_height_scale))
89
+ self.logger.info(
90
+ "heightScale attribute set to %s in TerrainTransformGroup element.",
91
+ new_height_scale,
92
+ )
93
+
94
+ self.logger.debug("TerrainTransformGroup element updated in I3D file.")
95
+ sun_elem = map_elem.find(".//Light[@name='sun']")
96
+
97
+ if sun_elem is not None:
98
+ self.logger.debug("Sun element found in I3D file.")
99
+
100
+ distance = self.map_size // 2
101
+
102
+ sun_elem.set("lastShadowMapSplitBboxMin", f"-{distance},-128,-{distance}")
103
+ sun_elem.set("lastShadowMapSplitBboxMax", f"{distance},148,{distance}")
104
+
105
+ self.logger.debug(
106
+ "Sun BBOX updated with half of the map size: %s.",
107
+ distance,
108
+ )
109
+
110
+ if self.map_size > 4096:
111
+ displacement_layer = terrain_elem.find(".//DisplacementLayer") # pylint: disable=W0631
112
+
113
+ if displacement_layer is not None:
114
+ displacement_layer.set("size", str(DISPLACEMENT_LAYER_SIZE_FOR_BIG_MAPS))
115
+ self.logger.debug(
116
+ "Map size is greater than 4096, DisplacementLayer size set to %s.",
117
+ )
118
+
119
+ tree.write(self._map_i3d_path) # type: ignore
120
+ self.logger.debug("Map I3D file saved to: %s.", self._map_i3d_path)
121
+
122
+ def previews(self) -> list[str]:
123
+ """Returns a list of paths to the preview images (empty list).
124
+ The component does not generate any preview images so it returns an empty list.
125
+
126
+ Returns:
127
+ list[str]: An empty list.
128
+ """
129
+ return []
130
+
131
+ # pylint: disable=R0914, R0915
132
+ def _add_splines(self) -> None:
133
+ """Adds splines to the map I3D file."""
134
+ splines_i3d_path = os.path.join(self.map_directory, "map", "splines.i3d")
135
+ if not os.path.isfile(splines_i3d_path):
136
+ self.logger.warning("Splines I3D file not found: %s.", splines_i3d_path)
137
+ return
138
+
139
+ tree = ET.parse(splines_i3d_path)
140
+
141
+ textures_info_layer_path = self.get_infolayer_path("textures")
142
+ if not textures_info_layer_path:
143
+ return
144
+
145
+ with open(textures_info_layer_path, "r", encoding="utf-8") as textures_info_layer_file:
146
+ textures_info_layer = json.load(textures_info_layer_file)
147
+
148
+ roads_polylines: list[list[tuple[int, int]]] | None = textures_info_layer.get(
149
+ "roads_polylines"
150
+ )
151
+
152
+ if not roads_polylines:
153
+ self.logger.warning("Roads polylines data not found in textures info layer.")
154
+ return
155
+
156
+ self.logger.debug("Found %s roads polylines in textures info layer.", len(roads_polylines))
157
+ self.logger.debug("Starging to add roads polylines to the I3D file.")
158
+
159
+ root = tree.getroot()
160
+ # Find <Shapes> element in the I3D file.
161
+ shapes_node = root.find(".//Shapes")
162
+ # Find <Scene> element in the I3D file.
163
+ scene_node = root.find(".//Scene")
164
+
165
+ # Read the not resized DEM to obtain Z values for spline points.
166
+ background_component = self.map.get_component("Background")
167
+ if not background_component:
168
+ self.logger.warning("Background component not found.")
169
+ return
170
+
171
+ # pylint: disable=no-member
172
+ not_resized_dem = cv2.imread(
173
+ background_component.not_resized_path, cv2.IMREAD_UNCHANGED # type: ignore
174
+ )
175
+ self.logger.debug(
176
+ "Not resized DEM loaded from: %s. Shape: %s.",
177
+ background_component.not_resized_path, # type: ignore
178
+ not_resized_dem.shape,
179
+ )
180
+ dem_x_size, dem_y_size = not_resized_dem.shape
181
+
182
+ if shapes_node is not None and scene_node is not None:
183
+ node_id = SPLINES_NODE_ID_STARTING_VALUE
184
+ user_attributes_node = root.find(".//UserAttributes")
185
+ if user_attributes_node is None:
186
+ self.logger.warning("UserAttributes node not found in I3D file.")
187
+ return
188
+
189
+ for road_id, road in enumerate(roads_polylines, start=1):
190
+ # Add to scene node
191
+ # <Shape name="spline01_CSV" translation="0 0 0" nodeId="11" shapeId="11"/>
192
+
193
+ try:
194
+ fitted_road = self.fit_object_into_bounds(
195
+ linestring_points=road, angle=self.rotation
196
+ )
197
+ except ValueError as e:
198
+ self.logger.debug(
199
+ "Road %s could not be fitted into the map bounds with error: %s",
200
+ road_id,
201
+ e,
202
+ )
203
+ continue
204
+
205
+ self.logger.debug("Road %s has %s points.", road_id, len(fitted_road))
206
+ fitted_road = self.interpolate_points(
207
+ fitted_road, num_points=self.map.spline_settings.spline_density
208
+ )
209
+ self.logger.debug(
210
+ "Road %s has %s points after interpolation.", road_id, len(fitted_road)
211
+ )
212
+
213
+ spline_name = f"spline{road_id}"
214
+
215
+ shape_node = ET.Element("Shape")
216
+ shape_node.set("name", spline_name)
217
+ shape_node.set("translation", "0 0 0")
218
+ shape_node.set("nodeId", str(node_id))
219
+ shape_node.set("shapeId", str(node_id))
220
+
221
+ scene_node.append(shape_node)
222
+
223
+ road_ccs = [self.top_left_coordinates_to_center(point) for point in fitted_road]
224
+
225
+ # Add to shapes node
226
+ # <NurbsCurve name="spline01_CSV" shapeId="11" degree="3" form="open">
227
+
228
+ nurbs_curve_node = ET.Element("NurbsCurve")
229
+ nurbs_curve_node.set("name", spline_name)
230
+ nurbs_curve_node.set("shapeId", str(node_id))
231
+ nurbs_curve_node.set("degree", "3")
232
+ nurbs_curve_node.set("form", "open")
233
+
234
+ # Now for each point in the road add the following entry to nurbs_curve_node
235
+ # <cv c="-224.548401, 427.297546, -2047.570312" />
236
+ # The second coordinate (Z) will be 0 at the moment.
237
+
238
+ for point_ccs, point in zip(road_ccs, fitted_road):
239
+ cx, cy = point_ccs
240
+ x, y = point
241
+
242
+ x = int(x)
243
+ y = int(y)
244
+
245
+ x = max(0, min(x, dem_x_size - 1))
246
+ y = max(0, min(y, dem_y_size - 1))
247
+
248
+ z = not_resized_dem[y, x]
249
+ z *= self.get_z_scaling_factor() # type: ignore
250
+
251
+ cv_node = ET.Element("cv")
252
+ cv_node.set("c", f"{cx}, {z}, {cy}")
253
+
254
+ nurbs_curve_node.append(cv_node)
255
+
256
+ shapes_node.append(nurbs_curve_node)
257
+
258
+ # Add UserAttributes to the shape node.
259
+ # <UserAttribute nodeId="5000">
260
+ # <Attribute name="maxSpeedScale" type="integer" value="1"/>
261
+ # <Attribute name="speedLimit" type="integer" value="100"/>
262
+ # </UserAttribute>
263
+
264
+ user_attribute_node = ET.Element("UserAttribute")
265
+ user_attribute_node.set("nodeId", str(node_id))
266
+
267
+ attributes = [
268
+ ("maxSpeedScale", "integer", "1"),
269
+ ("speedLimit", "integer", "100"),
270
+ ]
271
+
272
+ for name, attr_type, value in attributes:
273
+ user_attribute_node.append(I3d.create_attribute_node(name, attr_type, value))
274
+
275
+ user_attributes_node.append(user_attribute_node) # type: ignore
276
+
277
+ node_id += 1
278
+
279
+ tree.write(splines_i3d_path) # type: ignore
280
+ self.logger.debug("Splines I3D file saved to: %s.", splines_i3d_path)
281
+
282
+ # pylint: disable=R0914, R0915
283
+ def _add_fields(self) -> None:
284
+ """Adds fields to the map I3D file."""
285
+ tree = self._get_tree()
286
+ if tree is None:
287
+ return
288
+
289
+ textures_info_layer_path = self.get_infolayer_path("textures")
290
+ if not textures_info_layer_path:
291
+ return
292
+
293
+ border = 0
294
+ textures_layer: Texture | None = self.map.get_component("Texture") # type: ignore
295
+ if textures_layer:
296
+ border = textures_layer.get_layer_by_usage("field").border # type: ignore
297
+
298
+ with open(textures_info_layer_path, "r", encoding="utf-8") as textures_info_layer_file:
299
+ textures_info_layer = json.load(textures_info_layer_file)
300
+
301
+ fields: list[list[tuple[int, int]]] | None = textures_info_layer.get("fields")
302
+ if not fields:
303
+ self.logger.warning("Fields data not found in textures info layer.")
304
+ return
305
+
306
+ self.logger.debug("Found %s fields in textures info layer.", len(fields))
307
+ self.logger.debug("Starging to add fields to the I3D file.")
308
+
309
+ root = tree.getroot()
310
+ gameplay_node = root.find(".//TransformGroup[@name='gameplay']")
311
+ if gameplay_node is not None:
312
+ fields_node = gameplay_node.find(".//TransformGroup[@name='fields']")
313
+ user_attributes_node = root.find(".//UserAttributes")
314
+
315
+ if fields_node is not None:
316
+ node_id = NODE_ID_STARTING_VALUE
317
+
318
+ # Not using enumerate because in case of the error, we do not increment
319
+ # the field_id. So as a result we do not have a gap in the field IDs.
320
+ field_id = 1
321
+
322
+ for field in tqdm(fields, desc="Adding fields", unit="field"):
323
+ try:
324
+ fitted_field = self.fit_object_into_bounds(
325
+ polygon_points=field, angle=self.rotation, border=border
326
+ )
327
+ except ValueError as e:
328
+ self.logger.debug(
329
+ "Field %s could not be fitted into the map bounds with error: %s",
330
+ field_id,
331
+ e,
332
+ )
333
+ continue
334
+
335
+ field_ccs = [
336
+ self.top_left_coordinates_to_center(point) for point in fitted_field
337
+ ]
338
+
339
+ try:
340
+ cx, cy = self.get_polygon_center(field_ccs)
341
+ except Exception as e: # pylint: disable=W0718
342
+ self.logger.debug(
343
+ "Field %s could not be fitted into the map bounds.", field_id
344
+ )
345
+ self.logger.debug("Error: %s", e)
346
+ continue
347
+
348
+ # Creating the main field node.
349
+ field_node = ET.Element("TransformGroup")
350
+ field_node.set("name", f"field{field_id}")
351
+ field_node.set("translation", f"{cx} 0 {cy}")
352
+ field_node.set("nodeId", str(node_id))
353
+
354
+ # Adding UserAttributes to the field node.
355
+ user_attribute_node = self.create_user_attribute_node(node_id)
356
+ user_attributes_node.append(user_attribute_node) # type: ignore
357
+
358
+ node_id += 1
359
+
360
+ # Creating the polygon points node, which contains the points of the field.
361
+ polygon_points_node = ET.Element("TransformGroup")
362
+ polygon_points_node.set("name", "polygonPoints")
363
+ polygon_points_node.set("nodeId", str(node_id))
364
+ node_id += 1
365
+
366
+ for point_id, point in enumerate(field_ccs, start=1):
367
+ rx, ry = self.absolute_to_relative(point, (cx, cy))
368
+
369
+ node_id += 1
370
+ point_node = ET.Element("TransformGroup")
371
+ point_node.set("name", f"point{point_id}")
372
+ point_node.set("translation", f"{rx} 0 {ry}")
373
+ point_node.set("nodeId", str(node_id))
374
+
375
+ polygon_points_node.append(point_node)
376
+
377
+ field_node.append(polygon_points_node)
378
+
379
+ # Adding the name indicator node to the field node.
380
+ name_indicator_node, node_id = self.get_name_indicator_node(node_id, field_id)
381
+ field_node.append(name_indicator_node)
382
+
383
+ # Adding the teleport indicator node to the field node.
384
+ teleport_indicator_node, node_id = self.get_teleport_indicator_node(node_id)
385
+ field_node.append(teleport_indicator_node)
386
+
387
+ # Adding the field node to the fields node.
388
+ fields_node.append(field_node)
389
+ self.logger.debug("Field %s added to the I3D file.", field_id)
390
+
391
+ node_id += 1
392
+ field_id += 1
393
+
394
+ tree.write(self._map_i3d_path) # type: ignore
395
+ self.logger.debug("Map I3D file saved to: %s.", self._map_i3d_path)
396
+
397
+ def get_name_indicator_node(self, node_id: int, field_id: int) -> tuple[ET.Element, int]:
398
+ """Creates a name indicator node with given node ID and field ID.
399
+
400
+ Arguments:
401
+ node_id (int): The node ID of the name indicator node.
402
+ field_id (int): The ID of the field.
403
+
404
+ Returns:
405
+ tuple[ET.Element, int]: The name indicator node and the updated node ID.
406
+ """
407
+ node_id += 1
408
+ name_indicator_node = ET.Element("TransformGroup")
409
+ name_indicator_node.set("name", "nameIndicator")
410
+ name_indicator_node.set("nodeId", str(node_id))
411
+
412
+ node_id += 1
413
+ note_node = ET.Element("Note")
414
+ note_node.set("name", "Note")
415
+ note_node.set("nodeId", str(node_id))
416
+ note_node.set("text", f"field{field_id}&#xA;0.00 ha")
417
+ note_node.set("color", "4278190080")
418
+ note_node.set("fixedSize", "true")
419
+
420
+ name_indicator_node.append(note_node)
421
+
422
+ return name_indicator_node, node_id
423
+
424
+ def get_teleport_indicator_node(self, node_id: int) -> tuple[ET.Element, int]:
425
+ """Creates a teleport indicator node with given node ID.
426
+
427
+ Arguments:
428
+ node_id (int): The node ID of the teleport indicator node.
429
+
430
+ Returns:
431
+ tuple[ET.Element, int]: The teleport indicator node and the updated node ID.
432
+ """
433
+ node_id += 1
434
+ teleport_indicator_node = ET.Element("TransformGroup")
435
+ teleport_indicator_node.set("name", "teleportIndicator")
436
+ teleport_indicator_node.set("nodeId", str(node_id))
437
+
438
+ return teleport_indicator_node, node_id
439
+
440
+ @staticmethod
441
+ def create_user_attribute_node(node_id: int) -> ET.Element:
442
+ """Creates an XML user attribute node with given node ID.
443
+
444
+ Arguments:
445
+ node_id (int): The node ID of the user attribute node.
446
+
447
+ Returns:
448
+ ET.Element: The created user attribute node.
449
+ """
450
+ user_attribute_node = ET.Element("UserAttribute")
451
+ user_attribute_node.set("nodeId", str(node_id))
452
+
453
+ attributes = [
454
+ ("angle", "integer", "0"),
455
+ ("missionAllowed", "boolean", "true"),
456
+ ("missionOnlyGrass", "boolean", "false"),
457
+ ("nameIndicatorIndex", "string", "1"),
458
+ ("polygonIndex", "string", "0"),
459
+ ("teleportIndicatorIndex", "string", "2"),
460
+ ]
461
+
462
+ for name, attr_type, value in attributes:
463
+ user_attribute_node.append(I3d.create_attribute_node(name, attr_type, value))
464
+
465
+ return user_attribute_node
466
+
467
+ @staticmethod
468
+ def create_attribute_node(name: str, attr_type: str, value: str) -> ET.Element:
469
+ """Creates an XML attribute node with given name, type, and value.
470
+
471
+ Arguments:
472
+ name (str): The name of the attribute.
473
+ attr_type (str): The type of the attribute.
474
+ value (str): The value of the attribute.
475
+
476
+ Returns:
477
+ ET.Element: The created attribute node.
478
+ """
479
+ attribute_node = ET.Element("Attribute")
480
+ attribute_node.set("name", name)
481
+ attribute_node.set("type", attr_type)
482
+ attribute_node.set("value", value)
483
+ return attribute_node
484
+
485
+ # pylint: disable=R0911
486
+ def _add_forests(self) -> None:
487
+ """Adds forests to the map I3D file."""
488
+ custom_schema = self.kwargs.get("tree_custom_schema")
489
+ if custom_schema:
490
+ tree_schema = custom_schema
491
+ else:
492
+ try:
493
+ tree_schema_path = self.game.tree_schema
494
+ except ValueError:
495
+ self.logger.warning("Tree schema path not set for the Game %s.", self.game.code)
496
+ return
497
+
498
+ if not os.path.isfile(tree_schema_path):
499
+ self.logger.warning("Tree schema file was not found: %s.", tree_schema_path)
500
+ return
501
+
502
+ try:
503
+ with open(tree_schema_path, "r", encoding="utf-8") as tree_schema_file:
504
+ tree_schema = json.load(tree_schema_file) # type: ignore
505
+ except json.JSONDecodeError as e:
506
+ self.logger.warning(
507
+ "Could not load tree schema from %s with error: %s", tree_schema_path, e
508
+ )
509
+ return
510
+
511
+ texture_component: Texture | None = self.map.get_component("Texture") # type: ignore
512
+ if not texture_component:
513
+ self.logger.warning("Texture component not found.")
514
+ return
515
+
516
+ forest_layer = texture_component.get_layer_by_usage("forest")
517
+
518
+ if not forest_layer:
519
+ self.logger.warning("Forest layer not found.")
520
+ return
521
+
522
+ weights_directory = self.game.weights_dir_path(self.map_directory)
523
+ forest_image_path = forest_layer.get_preview_or_path(weights_directory)
524
+
525
+ if not forest_image_path or not os.path.isfile(forest_image_path):
526
+ self.logger.warning("Forest image not found.")
527
+ return
528
+
529
+ tree = self._get_tree()
530
+ if tree is None:
531
+ return
532
+
533
+ # Find the <Scene> element in the I3D file.
534
+ root = tree.getroot()
535
+ scene_node = root.find(".//Scene")
536
+ if scene_node is None:
537
+ self.logger.warning("Scene element not found in I3D file.")
538
+ return
539
+
540
+ self.logger.debug("Scene element found in I3D file, starting to add forests.")
541
+
542
+ node_id = TREE_NODE_ID_STARTING_VALUE
543
+
544
+ # Create <TransformGroup name="trees" translation="0 400 0" nodeId="{node_id}"> element.
545
+ trees_node = ET.Element("TransformGroup")
546
+ trees_node.set("name", "trees")
547
+ trees_node.set("translation", "0 400 0")
548
+ trees_node.set("nodeId", str(node_id))
549
+ node_id += 1
550
+
551
+ # pylint: disable=no-member
552
+ forest_image = cv2.imread(forest_image_path, cv2.IMREAD_UNCHANGED)
553
+
554
+ tree_count = 0
555
+ for x, y in self.non_empty_pixels(forest_image, step=self.map.i3d_settings.forest_density):
556
+ xcs, ycs = self.top_left_coordinates_to_center((x, y))
557
+ node_id += 1
558
+
559
+ rotation = randint(-180, 180)
560
+ xcs, ycs = self.randomize_coordinates( # type: ignore
561
+ (xcs, ycs), self.map.i3d_settings.forest_density
562
+ )
563
+
564
+ random_tree = choice(tree_schema) # type: ignore
565
+ tree_name = random_tree["name"]
566
+ tree_id = random_tree["reference_id"]
567
+
568
+ reference_node = ET.Element("ReferenceNode")
569
+ reference_node.set("name", tree_name) # type: ignore
570
+ reference_node.set("translation", f"{xcs} 0 {ycs}")
571
+ reference_node.set("rotation", f"0 {rotation} 0")
572
+ reference_node.set("referenceId", str(tree_id))
573
+ reference_node.set("nodeId", str(node_id))
574
+
575
+ trees_node.append(reference_node)
576
+ tree_count += 1
577
+
578
+ scene_node.append(trees_node)
579
+ self.logger.debug("Added %s trees to the I3D file.", tree_count)
580
+
581
+ tree.write(self._map_i3d_path) # type: ignore
582
+ self.logger.debug("Map I3D file saved to: %s.", self._map_i3d_path)
583
+
584
+ @staticmethod
585
+ def randomize_coordinates(coordinates: tuple[int, int], density: int) -> tuple[float, float]:
586
+ """Randomizes the coordinates of the point with the given density.
587
+
588
+ Arguments:
589
+ coordinates (tuple[int, int]): The coordinates of the point.
590
+ density (int): The density of the randomization.
591
+
592
+ Returns:
593
+ tuple[float, float]: The randomized coordinates of the point.
594
+ """
595
+ MAXIMUM_RELATIVE_SHIFT = 0.2 # pylint: disable=C0103
596
+ shift_range = density * MAXIMUM_RELATIVE_SHIFT
597
+
598
+ x_shift = uniform(-shift_range, shift_range)
599
+ y_shift = uniform(-shift_range, shift_range)
600
+
601
+ x, y = coordinates
602
+ x += x_shift # type: ignore
603
+ y += y_shift # type: ignore
604
+
605
+ return x, y
606
+
607
+ @staticmethod
608
+ def non_empty_pixels(
609
+ image: np.ndarray, step: int = 1
610
+ ) -> Generator[tuple[int, int], None, None]:
611
+ """Receives numpy array, which represents single-channeled image of uint8 type.
612
+ Yield coordinates of non-empty pixels (pixels with value greater than 0).
613
+
614
+ Arguments:
615
+ image (np.ndarray): The image to get non-empty pixels from.
616
+ step (int, optional): The step to iterate through the image. Defaults to 1.
617
+
618
+ Yields:
619
+ tuple[int, int]: The coordinates of non-empty pixels.
620
+ """
621
+ for y, row in enumerate(image[::step]):
622
+ for x, value in enumerate(row[::step]):
623
+ if value > 0:
624
+ yield x * step, y * step