maps4fs 1.8.0__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -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