yirgacheffe 1.7.9__py3-none-any.whl → 1.8.1__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 yirgacheffe might be problematic. Click here for more details.
- yirgacheffe/__init__.py +2 -0
- yirgacheffe/_backends/enumeration.py +20 -0
- yirgacheffe/_backends/mlx.py +4 -2
- yirgacheffe/_backends/numpy.py +4 -2
- yirgacheffe/_core.py +76 -96
- yirgacheffe/_operators.py +25 -28
- yirgacheffe/layers/area.py +5 -3
- yirgacheffe/layers/base.py +20 -27
- yirgacheffe/layers/constant.py +4 -2
- yirgacheffe/layers/group.py +11 -11
- yirgacheffe/layers/h3layer.py +4 -2
- yirgacheffe/layers/rasters.py +18 -18
- yirgacheffe/layers/rescaled.py +3 -3
- yirgacheffe/layers/vectors.py +65 -66
- yirgacheffe/rounding.py +4 -3
- yirgacheffe/window.py +52 -97
- {yirgacheffe-1.7.9.dist-info → yirgacheffe-1.8.1.dist-info}/METADATA +8 -4
- yirgacheffe-1.8.1.dist-info/RECORD +27 -0
- yirgacheffe-1.7.9.dist-info/RECORD +0 -27
- {yirgacheffe-1.7.9.dist-info → yirgacheffe-1.8.1.dist-info}/WHEEL +0 -0
- {yirgacheffe-1.7.9.dist-info → yirgacheffe-1.8.1.dist-info}/entry_points.txt +0 -0
- {yirgacheffe-1.7.9.dist-info → yirgacheffe-1.8.1.dist-info}/licenses/LICENSE +0 -0
- {yirgacheffe-1.7.9.dist-info → yirgacheffe-1.8.1.dist-info}/top_level.txt +0 -0
yirgacheffe/layers/group.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
import copy
|
|
3
3
|
from pathlib import Path
|
|
4
|
-
from typing import Any,
|
|
4
|
+
from typing import Any, Sequence
|
|
5
5
|
|
|
6
6
|
import numpy as np
|
|
7
7
|
from numpy import ma
|
|
@@ -22,8 +22,8 @@ class GroupLayer(YirgacheffeLayer):
|
|
|
22
22
|
@classmethod
|
|
23
23
|
def layer_from_directory(
|
|
24
24
|
cls,
|
|
25
|
-
directory_path:
|
|
26
|
-
name:
|
|
25
|
+
directory_path: Path | str,
|
|
26
|
+
name: str | None = None,
|
|
27
27
|
matching: str = "*.tif"
|
|
28
28
|
) -> GroupLayer:
|
|
29
29
|
if directory_path is None:
|
|
@@ -38,20 +38,20 @@ class GroupLayer(YirgacheffeLayer):
|
|
|
38
38
|
@classmethod
|
|
39
39
|
def layer_from_files(
|
|
40
40
|
cls,
|
|
41
|
-
filenames: Sequence[
|
|
42
|
-
name:
|
|
41
|
+
filenames: Sequence[Path | str],
|
|
42
|
+
name: str | None = None
|
|
43
43
|
) -> GroupLayer:
|
|
44
44
|
if filenames is None:
|
|
45
45
|
raise ValueError("filenames argument is None")
|
|
46
|
-
rasters:
|
|
46
|
+
rasters: list[YirgacheffeLayer] = [RasterLayer.layer_from_file(x) for x in filenames]
|
|
47
47
|
if len(rasters) < 1:
|
|
48
48
|
raise GroupLayerEmpty("No files found")
|
|
49
49
|
return cls(rasters, name)
|
|
50
50
|
|
|
51
51
|
def __init__(
|
|
52
52
|
self,
|
|
53
|
-
layers:
|
|
54
|
-
name:
|
|
53
|
+
layers: list[YirgacheffeLayer],
|
|
54
|
+
name: str | None = None
|
|
55
55
|
) -> None:
|
|
56
56
|
if not layers:
|
|
57
57
|
raise GroupLayerEmpty("Expected one or more layers")
|
|
@@ -255,7 +255,7 @@ class TiledGroupLayer(GroupLayer):
|
|
|
255
255
|
ysize
|
|
256
256
|
)
|
|
257
257
|
|
|
258
|
-
partials:
|
|
258
|
+
partials: list[TileData] = []
|
|
259
259
|
for layer in self.layers:
|
|
260
260
|
# Normally this is hidden with set_window_for_...
|
|
261
261
|
adjusted_layer_window = Window(
|
|
@@ -287,7 +287,7 @@ class TiledGroupLayer(GroupLayer):
|
|
|
287
287
|
# the "obvious" tile. In which case, we should reject the smaller section. If we have
|
|
288
288
|
# two tiles at the same offset and one is not a perfect subset of the other then the
|
|
289
289
|
# tile set we were given is not regularly shaped, and so we should give up.
|
|
290
|
-
combed_partials:
|
|
290
|
+
combed_partials: list[TileData] = []
|
|
291
291
|
previous_tile = None
|
|
292
292
|
for tile in sorted_partials:
|
|
293
293
|
if previous_tile is None:
|
|
@@ -312,7 +312,7 @@ class TiledGroupLayer(GroupLayer):
|
|
|
312
312
|
expected_next_x = 0
|
|
313
313
|
expected_next_y = 0
|
|
314
314
|
data = None
|
|
315
|
-
row_chunk:
|
|
315
|
+
row_chunk: np.ndarray | None = None
|
|
316
316
|
|
|
317
317
|
# Allow for reading off top
|
|
318
318
|
if combed_partials:
|
yirgacheffe/layers/h3layer.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
from math import ceil, floor
|
|
2
|
-
from typing import Any
|
|
4
|
+
from typing import Any
|
|
3
5
|
|
|
4
6
|
import h3
|
|
5
7
|
import numpy as np
|
|
@@ -75,7 +77,7 @@ class H3CellLayer(YirgacheffeLayer):
|
|
|
75
77
|
)
|
|
76
78
|
|
|
77
79
|
@property
|
|
78
|
-
def _raster_dimensions(self) ->
|
|
80
|
+
def _raster_dimensions(self) -> tuple[int, int]:
|
|
79
81
|
return (self._raster_xsize, self._raster_ysize)
|
|
80
82
|
|
|
81
83
|
@property
|
yirgacheffe/layers/rasters.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
import math
|
|
3
3
|
from pathlib import Path
|
|
4
|
-
from typing import Any
|
|
4
|
+
from typing import Any
|
|
5
5
|
|
|
6
6
|
import numpy as np
|
|
7
7
|
from osgeo import gdal
|
|
@@ -26,14 +26,14 @@ class RasterLayer(YirgacheffeLayer):
|
|
|
26
26
|
def empty_raster_layer(
|
|
27
27
|
area: Area,
|
|
28
28
|
scale: PixelScale,
|
|
29
|
-
datatype:
|
|
30
|
-
filename:
|
|
29
|
+
datatype: int | DataType,
|
|
30
|
+
filename: Path | str | None = None,
|
|
31
31
|
projection: str=WGS_84_PROJECTION,
|
|
32
|
-
name:
|
|
32
|
+
name: str | None = None,
|
|
33
33
|
compress: bool=True,
|
|
34
|
-
nodata:
|
|
35
|
-
nbits:
|
|
36
|
-
threads:
|
|
34
|
+
nodata: float | int | None = None,
|
|
35
|
+
nbits: int | None = None,
|
|
36
|
+
threads: int | None = None,
|
|
37
37
|
bands: int=1
|
|
38
38
|
) -> RasterLayer:
|
|
39
39
|
abs_xstep, abs_ystep = abs(scale.xstep), abs(scale.ystep)
|
|
@@ -88,13 +88,13 @@ class RasterLayer(YirgacheffeLayer):
|
|
|
88
88
|
@staticmethod
|
|
89
89
|
def empty_raster_layer_like(
|
|
90
90
|
layer: Any,
|
|
91
|
-
filename:
|
|
92
|
-
area:
|
|
93
|
-
datatype:
|
|
91
|
+
filename: Path | str | None = None,
|
|
92
|
+
area: Area | None = None,
|
|
93
|
+
datatype: int | DataType | None = None,
|
|
94
94
|
compress: bool=True,
|
|
95
|
-
nodata:
|
|
96
|
-
nbits:
|
|
97
|
-
threads:
|
|
95
|
+
nodata: float | int | None = None,
|
|
96
|
+
nbits: int | None = None,
|
|
97
|
+
threads: int | None = None,
|
|
98
98
|
bands: int=1
|
|
99
99
|
) -> RasterLayer:
|
|
100
100
|
if area is None:
|
|
@@ -165,7 +165,7 @@ class RasterLayer(YirgacheffeLayer):
|
|
|
165
165
|
cls,
|
|
166
166
|
source: RasterLayer,
|
|
167
167
|
new_pixel_scale: PixelScale,
|
|
168
|
-
filename:
|
|
168
|
+
filename: Path | str | None = None,
|
|
169
169
|
compress: bool=True,
|
|
170
170
|
algorithm: int=gdal.GRA_NearestNeighbour,
|
|
171
171
|
) -> RasterLayer:
|
|
@@ -220,7 +220,7 @@ class RasterLayer(YirgacheffeLayer):
|
|
|
220
220
|
@classmethod
|
|
221
221
|
def layer_from_file(
|
|
222
222
|
cls,
|
|
223
|
-
filename:
|
|
223
|
+
filename: Path | str,
|
|
224
224
|
band: int = 1,
|
|
225
225
|
ignore_nodata: bool = False,
|
|
226
226
|
) -> RasterLayer:
|
|
@@ -238,7 +238,7 @@ class RasterLayer(YirgacheffeLayer):
|
|
|
238
238
|
def __init__(
|
|
239
239
|
self,
|
|
240
240
|
dataset: gdal.Dataset,
|
|
241
|
-
name:
|
|
241
|
+
name: str | None = None,
|
|
242
242
|
band: int = 1,
|
|
243
243
|
ignore_nodata: bool = False,
|
|
244
244
|
) -> None:
|
|
@@ -273,7 +273,7 @@ class RasterLayer(YirgacheffeLayer):
|
|
|
273
273
|
self._ignore_nodata = ignore_nodata
|
|
274
274
|
|
|
275
275
|
@property
|
|
276
|
-
def _raster_dimensions(self) ->
|
|
276
|
+
def _raster_dimensions(self) -> tuple[int, int]:
|
|
277
277
|
return (self._raster_xsize, self._raster_ysize)
|
|
278
278
|
|
|
279
279
|
def close(self):
|
|
@@ -321,7 +321,7 @@ class RasterLayer(YirgacheffeLayer):
|
|
|
321
321
|
return DataType.of_gdal(self._dataset.GetRasterBand(1).DataType)
|
|
322
322
|
|
|
323
323
|
@property
|
|
324
|
-
def nodata(self) ->
|
|
324
|
+
def nodata(self) -> Any | None:
|
|
325
325
|
if self._dataset is None:
|
|
326
326
|
self._unpark()
|
|
327
327
|
assert self._dataset
|
yirgacheffe/layers/rescaled.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
from math import floor, ceil
|
|
3
3
|
from pathlib import Path
|
|
4
|
-
from typing import Any
|
|
4
|
+
from typing import Any
|
|
5
5
|
|
|
6
6
|
from skimage import transform
|
|
7
7
|
|
|
@@ -18,7 +18,7 @@ class RescaledRasterLayer(YirgacheffeLayer):
|
|
|
18
18
|
@classmethod
|
|
19
19
|
def layer_from_file(
|
|
20
20
|
cls,
|
|
21
|
-
filename:
|
|
21
|
+
filename: Path | str,
|
|
22
22
|
pixel_scale: PixelScale,
|
|
23
23
|
band: int = 1,
|
|
24
24
|
nearest_neighbour: bool = True,
|
|
@@ -35,7 +35,7 @@ class RescaledRasterLayer(YirgacheffeLayer):
|
|
|
35
35
|
src: RasterLayer,
|
|
36
36
|
target_projection: MapProjection,
|
|
37
37
|
nearest_neighbour: bool = True,
|
|
38
|
-
name:
|
|
38
|
+
name: str | None = None,
|
|
39
39
|
):
|
|
40
40
|
super().__init__(
|
|
41
41
|
src.area,
|
yirgacheffe/layers/vectors.py
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
from math import ceil, floor
|
|
3
3
|
from pathlib import Path
|
|
4
|
-
from typing import Any
|
|
5
|
-
from typing_extensions import NotRequired
|
|
4
|
+
from typing import Any
|
|
6
5
|
|
|
7
6
|
import deprecation
|
|
8
7
|
from osgeo import gdal, ogr
|
|
@@ -70,12 +69,12 @@ class RasteredVectorLayer(RasterLayer):
|
|
|
70
69
|
)
|
|
71
70
|
def layer_from_file( # type: ignore[override] # pylint: disable=W0221
|
|
72
71
|
cls,
|
|
73
|
-
filename:
|
|
74
|
-
where_filter:
|
|
72
|
+
filename: Path | str,
|
|
73
|
+
where_filter: str | None,
|
|
75
74
|
scale: PixelScale,
|
|
76
75
|
projection: str,
|
|
77
|
-
datatype:
|
|
78
|
-
burn_value:
|
|
76
|
+
datatype: int | DataType | None = None,
|
|
77
|
+
burn_value: int | float | str = 1,
|
|
79
78
|
) -> RasteredVectorLayer:
|
|
80
79
|
vectors = ogr.Open(filename)
|
|
81
80
|
if vectors is None:
|
|
@@ -111,14 +110,14 @@ class RasteredVectorLayer(RasterLayer):
|
|
|
111
110
|
self,
|
|
112
111
|
layer: ogr.Layer,
|
|
113
112
|
projection: MapProjection,
|
|
114
|
-
datatype:
|
|
115
|
-
burn_value:
|
|
113
|
+
datatype: int | DataType = DataType.Byte,
|
|
114
|
+
burn_value: int | float | str = 1,
|
|
116
115
|
):
|
|
117
116
|
if layer is None:
|
|
118
117
|
raise ValueError('No layer provided')
|
|
119
118
|
self.layer = layer
|
|
120
119
|
|
|
121
|
-
self._original:
|
|
120
|
+
self._original: Any | None = None
|
|
122
121
|
|
|
123
122
|
if isinstance(datatype, int):
|
|
124
123
|
datatype_arg = DataType.of_gdal(datatype)
|
|
@@ -180,11 +179,11 @@ class VectorLayer(YirgacheffeLayer):
|
|
|
180
179
|
@classmethod
|
|
181
180
|
def layer_from_file_like(
|
|
182
181
|
cls,
|
|
183
|
-
filename:
|
|
182
|
+
filename: Path | str,
|
|
184
183
|
other_layer: YirgacheffeLayer,
|
|
185
|
-
where_filter:
|
|
186
|
-
datatype:
|
|
187
|
-
burn_value:
|
|
184
|
+
where_filter: str | None = None,
|
|
185
|
+
datatype: int | DataType | None = None,
|
|
186
|
+
burn_value: int | float | str = 1,
|
|
188
187
|
) -> VectorLayer:
|
|
189
188
|
if other_layer is None:
|
|
190
189
|
raise ValueError("like layer can not be None")
|
|
@@ -223,13 +222,13 @@ class VectorLayer(YirgacheffeLayer):
|
|
|
223
222
|
@classmethod
|
|
224
223
|
def layer_from_file(
|
|
225
224
|
cls,
|
|
226
|
-
filename:
|
|
227
|
-
where_filter:
|
|
228
|
-
scale:
|
|
229
|
-
projection:
|
|
230
|
-
datatype:
|
|
231
|
-
burn_value:
|
|
232
|
-
anchor:
|
|
225
|
+
filename: Path | str,
|
|
226
|
+
where_filter: str | None,
|
|
227
|
+
scale: PixelScale | None,
|
|
228
|
+
projection: str | None,
|
|
229
|
+
datatype: int | DataType | None = None,
|
|
230
|
+
burn_value: int | float | str = 1,
|
|
231
|
+
anchor: tuple[float, float] = (0.0, 0.0)
|
|
233
232
|
) -> VectorLayer:
|
|
234
233
|
# In 2.0 we need to remove this and migrate to the MapProjection version
|
|
235
234
|
if (projection is None) ^ (scale is None):
|
|
@@ -250,12 +249,12 @@ class VectorLayer(YirgacheffeLayer):
|
|
|
250
249
|
@classmethod
|
|
251
250
|
def _future_layer_from_file(
|
|
252
251
|
cls,
|
|
253
|
-
filename:
|
|
254
|
-
where_filter:
|
|
255
|
-
projection:
|
|
256
|
-
datatype:
|
|
257
|
-
burn_value:
|
|
258
|
-
anchor:
|
|
252
|
+
filename: Path | str,
|
|
253
|
+
where_filter: str | None,
|
|
254
|
+
projection: MapProjection | None,
|
|
255
|
+
datatype: int | DataType | None = None,
|
|
256
|
+
burn_value: int | float | str = 1,
|
|
257
|
+
anchor: tuple[float, float] = (0.0, 0.0)
|
|
259
258
|
) -> VectorLayer:
|
|
260
259
|
try:
|
|
261
260
|
vectors = ogr.Open(filename)
|
|
@@ -295,11 +294,11 @@ class VectorLayer(YirgacheffeLayer):
|
|
|
295
294
|
def __init__(
|
|
296
295
|
self,
|
|
297
296
|
layer: ogr.Layer,
|
|
298
|
-
projection:
|
|
299
|
-
name:
|
|
300
|
-
datatype:
|
|
301
|
-
burn_value:
|
|
302
|
-
anchor:
|
|
297
|
+
projection: MapProjection | None,
|
|
298
|
+
name: str | None = None,
|
|
299
|
+
datatype: int | DataType = DataType.Byte,
|
|
300
|
+
burn_value: int | float | str = 1,
|
|
301
|
+
anchor: tuple[float, float] = (0.0, 0.0)
|
|
303
302
|
):
|
|
304
303
|
if layer is None:
|
|
305
304
|
raise ValueError('No layer provided')
|
|
@@ -316,9 +315,9 @@ class VectorLayer(YirgacheffeLayer):
|
|
|
316
315
|
self.burn_value = burn_value
|
|
317
316
|
|
|
318
317
|
self._original = None
|
|
319
|
-
self._dataset_path:
|
|
320
|
-
self._filter:
|
|
321
|
-
self._anchor:
|
|
318
|
+
self._dataset_path: Path | None = None
|
|
319
|
+
self._filter: str | None = None
|
|
320
|
+
self._anchor: tuple[float, float] = (0.0, 0.0)
|
|
322
321
|
|
|
323
322
|
# work out region for mask
|
|
324
323
|
envelopes = []
|
|
@@ -334,41 +333,41 @@ class VectorLayer(YirgacheffeLayer):
|
|
|
334
333
|
self._anchor = anchor
|
|
335
334
|
self._envelopes = envelopes
|
|
336
335
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
336
|
+
if projection is not None:
|
|
337
|
+
# Get the area, but scale it to the pixel resolution that we're using. Note that
|
|
338
|
+
# the pixel scale GDAL uses can have -ve values, but those will mess up the
|
|
339
|
+
# ceil/floor math, so we use absolute versions when trying to round.
|
|
340
|
+
abs_xstep, abs_ystep = abs(projection.xstep), abs(projection.ystep)
|
|
341
|
+
|
|
342
|
+
# Lacking any other reference, we will make the raster align with
|
|
343
|
+
# (0.0, 0.0), if sometimes we want to align with an existing raster, so if
|
|
344
|
+
# an anchor is specified, ensure we use that as our pixel space alignment
|
|
345
|
+
x_anchor = anchor[0]
|
|
346
|
+
y_anchor = anchor[1]
|
|
347
|
+
left_shift = x_anchor - abs_xstep
|
|
348
|
+
right_shift = x_anchor
|
|
349
|
+
top_shift = y_anchor
|
|
350
|
+
bottom_shift = y_anchor - abs_ystep
|
|
351
|
+
|
|
352
|
+
area = Area(
|
|
353
|
+
left=(floor((min(x[0] for x in envelopes) - left_shift) / abs_xstep) * abs_xstep) + left_shift,
|
|
354
|
+
top=(ceil((max(x[3] for x in envelopes) - top_shift) / abs_ystep) * abs_ystep) + top_shift,
|
|
355
|
+
right=(ceil((max(x[1] for x in envelopes) - right_shift) / abs_xstep) * abs_xstep) + right_shift,
|
|
356
|
+
bottom=(floor((min(x[2] for x in envelopes) - bottom_shift) / abs_ystep) * abs_ystep) + bottom_shift,
|
|
357
|
+
)
|
|
358
|
+
else:
|
|
360
359
|
# If we don't have a projection just go with the idealised area
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
360
|
+
area = Area(
|
|
361
|
+
left=floor(min(x[0] for x in envelopes)),
|
|
362
|
+
top=ceil(max(x[3] for x in envelopes)),
|
|
363
|
+
right=ceil(max(x[1] for x in envelopes)),
|
|
364
|
+
bottom=floor(min(x[2] for x in envelopes)),
|
|
365
|
+
)
|
|
367
366
|
|
|
368
367
|
super().__init__(area, projection)
|
|
369
368
|
|
|
370
369
|
|
|
371
|
-
def _get_operation_area(self, projection:
|
|
370
|
+
def _get_operation_area(self, projection: MapProjection | None = None) -> Area:
|
|
372
371
|
if self._projection is not None and projection is not None and self._projection != projection:
|
|
373
372
|
raise ValueError("Calculation projection does not match layer projection")
|
|
374
373
|
|
|
@@ -449,7 +448,7 @@ class VectorLayer(YirgacheffeLayer):
|
|
|
449
448
|
def _read_array_for_area(
|
|
450
449
|
self,
|
|
451
450
|
target_area: Area,
|
|
452
|
-
target_projection:
|
|
451
|
+
target_projection: MapProjection | None,
|
|
453
452
|
x: int,
|
|
454
453
|
y: int,
|
|
455
454
|
width: int,
|
|
@@ -496,7 +495,7 @@ class VectorLayer(YirgacheffeLayer):
|
|
|
496
495
|
return res
|
|
497
496
|
|
|
498
497
|
def _read_array_with_window(self, _x, _y, _width, _height, _window) -> Any:
|
|
499
|
-
|
|
498
|
+
raise NotImplementedError("VectorLayer does not support windowed reading")
|
|
500
499
|
|
|
501
500
|
def _read_array(self, x: int, y: int, width: int, height: int) -> Any:
|
|
502
501
|
return self._read_array_for_area(self.area, None, x, y, width, height)
|
yirgacheffe/rounding.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import math
|
|
2
4
|
import sys
|
|
3
|
-
from typing import List, Optional
|
|
4
5
|
|
|
5
6
|
from .window import PixelScale
|
|
6
7
|
|
|
@@ -39,10 +40,10 @@ def round_down_pixels(value: float, pixelscale: float) -> int:
|
|
|
39
40
|
else:
|
|
40
41
|
return math.floor(value)
|
|
41
42
|
|
|
42
|
-
def are_pixel_scales_equal_enough(pixel_scales:
|
|
43
|
+
def are_pixel_scales_equal_enough(pixel_scales: list[PixelScale | None]) -> bool:
|
|
43
44
|
# some layers (e.g., constant layers) have no scale, and always work, so filter
|
|
44
45
|
# them out first
|
|
45
|
-
cleaned_pixel_scales:
|
|
46
|
+
cleaned_pixel_scales: list[PixelScale] = [x for x in pixel_scales if x is not None]
|
|
46
47
|
|
|
47
48
|
try:
|
|
48
49
|
first = cleaned_pixel_scales[0]
|