rio-tiler 7.9.2__py3-none-any.whl → 8.0.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.
- rio_tiler/__init__.py +1 -1
- rio_tiler/colormap.py +7 -4
- rio_tiler/errors.py +8 -0
- rio_tiler/experimental/zarr.py +366 -0
- rio_tiler/expression.py +6 -6
- rio_tiler/io/base.py +20 -0
- rio_tiler/io/rasterio.py +1 -8
- rio_tiler/io/xarray.py +70 -58
- rio_tiler/models.py +223 -66
- rio_tiler/mosaic/backend.py +242 -0
- rio_tiler/mosaic/reader.py +6 -0
- rio_tiler/reader.py +31 -32
- rio_tiler/tasks.py +16 -0
- rio_tiler/utils.py +29 -1
- {rio_tiler-7.9.2.dist-info → rio_tiler-8.0.0.dist-info}/METADATA +12 -36
- {rio_tiler-7.9.2.dist-info → rio_tiler-8.0.0.dist-info}/RECORD +19 -17
- {rio_tiler-7.9.2.dist-info → rio_tiler-8.0.0.dist-info}/WHEEL +0 -0
- {rio_tiler-7.9.2.dist-info → rio_tiler-8.0.0.dist-info}/licenses/AUTHORS.txt +0 -0
- {rio_tiler-7.9.2.dist-info → rio_tiler-8.0.0.dist-info}/licenses/LICENSE +0 -0
rio_tiler/models.py
CHANGED
|
@@ -33,6 +33,7 @@ from rio_tiler.types import (
|
|
|
33
33
|
ColorMapType,
|
|
34
34
|
GDALColorMapType,
|
|
35
35
|
IntervalTuple,
|
|
36
|
+
NoData,
|
|
36
37
|
NumType,
|
|
37
38
|
RIOResampling,
|
|
38
39
|
WarpResampling,
|
|
@@ -155,10 +156,14 @@ class PointData:
|
|
|
155
156
|
|
|
156
157
|
array: numpy.ma.MaskedArray = attr.ib(converter=to_masked)
|
|
157
158
|
band_names: List[str] = attr.ib(kw_only=True)
|
|
159
|
+
band_descriptions: Optional[List[str]] = attr.ib(kw_only=True)
|
|
158
160
|
coordinates: Optional[Tuple[float, float]] = attr.ib(default=None, kw_only=True)
|
|
159
161
|
crs: Optional[CRS] = attr.ib(default=None, kw_only=True)
|
|
160
162
|
assets: Optional[List] = attr.ib(default=None, kw_only=True)
|
|
161
163
|
metadata: Optional[Dict] = attr.ib(factory=dict, kw_only=True)
|
|
164
|
+
nodata: Optional[NoData] = attr.ib(default=None, kw_only=True)
|
|
165
|
+
scales: Optional[List[NumType]] = attr.ib(kw_only=True)
|
|
166
|
+
offsets: Optional[List[NumType]] = attr.ib(kw_only=True)
|
|
162
167
|
pixel_location: Optional[Tuple[NumType, NumType]] = attr.ib(
|
|
163
168
|
default=None, kw_only=True
|
|
164
169
|
)
|
|
@@ -176,24 +181,36 @@ class PointData:
|
|
|
176
181
|
raise ValueError("Coordinates data has to be a 2d list")
|
|
177
182
|
|
|
178
183
|
@band_names.default
|
|
179
|
-
def
|
|
184
|
+
def _default_band_names(self):
|
|
180
185
|
return [f"b{ix + 1}" for ix in range(self.count)]
|
|
181
186
|
|
|
182
|
-
|
|
183
|
-
|
|
187
|
+
@band_descriptions.default
|
|
188
|
+
def _default_band_descriptions(self):
|
|
189
|
+
return ["" for ix in range(self.count)]
|
|
190
|
+
|
|
191
|
+
@scales.default
|
|
192
|
+
def _default_scales(self):
|
|
193
|
+
return [1.0] * self.count
|
|
194
|
+
|
|
195
|
+
@offsets.default
|
|
196
|
+
def _default_offsets(self):
|
|
197
|
+
return [0.0] * self.count
|
|
198
|
+
|
|
184
199
|
@property
|
|
185
200
|
def data(self) -> numpy.ndarray:
|
|
186
201
|
"""Return data part of the masked array."""
|
|
187
202
|
return self.array.data
|
|
188
203
|
|
|
204
|
+
@property
|
|
205
|
+
def _mask(self):
|
|
206
|
+
"""Return `inverted/merged` mask from data array."""
|
|
207
|
+
return numpy.array([numpy.logical_and.reduce(~self.array.mask)])
|
|
208
|
+
|
|
189
209
|
@property
|
|
190
210
|
def mask(self) -> numpy.ndarray:
|
|
191
211
|
"""Return Mask in form of rasterio dataset mask."""
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
)
|
|
195
|
-
|
|
196
|
-
###########################################################################
|
|
212
|
+
minv, maxv = dtype_ranges[str(self.array.dtype)]
|
|
213
|
+
return numpy.where(self._mask, maxv, minv).astype(self.array.dtype)
|
|
197
214
|
|
|
198
215
|
def __iter__(self):
|
|
199
216
|
"""Allow for variable expansion."""
|
|
@@ -236,6 +253,16 @@ class PointData:
|
|
|
236
253
|
itertools.chain.from_iterable([pt.band_names for pt in data if pt.band_names])
|
|
237
254
|
)
|
|
238
255
|
|
|
256
|
+
band_descriptions = list(
|
|
257
|
+
itertools.chain.from_iterable(
|
|
258
|
+
[pt.band_descriptions for pt in data if pt.band_descriptions]
|
|
259
|
+
)
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
scales = list(itertools.chain.from_iterable([pt.scales for pt in data]))
|
|
263
|
+
|
|
264
|
+
offsets = list(itertools.chain.from_iterable([pt.offsets for pt in data]))
|
|
265
|
+
|
|
239
266
|
metadata = dict(
|
|
240
267
|
itertools.chain.from_iterable(
|
|
241
268
|
[pt.metadata.items() for pt in data if pt.metadata]
|
|
@@ -246,6 +273,9 @@ class PointData:
|
|
|
246
273
|
arr,
|
|
247
274
|
assets=assets,
|
|
248
275
|
band_names=band_names,
|
|
276
|
+
band_descriptions=band_descriptions,
|
|
277
|
+
offsets=offsets,
|
|
278
|
+
scales=scales,
|
|
249
279
|
coordinates=data[0].coordinates,
|
|
250
280
|
crs=data[0].crs,
|
|
251
281
|
metadata=metadata,
|
|
@@ -259,6 +289,8 @@ class PointData:
|
|
|
259
289
|
# Using numexpr do not preserve mask info
|
|
260
290
|
data.mask = False
|
|
261
291
|
|
|
292
|
+
# TODO: Update band descriptions
|
|
293
|
+
|
|
262
294
|
return PointData(
|
|
263
295
|
data,
|
|
264
296
|
assets=self.assets,
|
|
@@ -297,7 +329,7 @@ class ImageData:
|
|
|
297
329
|
bounds (BoundingBox, optional): bounding box of the data.
|
|
298
330
|
crs (rasterio.crs.CRS, optional): Coordinates Reference System of the bounds.
|
|
299
331
|
metadata (dict, optional): Additional metadata. Defaults to `{}`.
|
|
300
|
-
band_names (list, optional): name of each band. Defaults to `["
|
|
332
|
+
band_names (list, optional): name of each band. Defaults to `["b1", "b2", "b3"]` for 3 bands image.
|
|
301
333
|
dataset_statistics (list, optional): dataset statistics `[(min, max), (min, max)]`
|
|
302
334
|
|
|
303
335
|
Note: `mask` should be considered as `PER_BAND` so shape should be similar as the data
|
|
@@ -311,18 +343,51 @@ class ImageData:
|
|
|
311
343
|
)
|
|
312
344
|
crs: Optional[CRS] = attr.ib(default=None, kw_only=True)
|
|
313
345
|
metadata: Optional[Dict] = attr.ib(factory=dict, kw_only=True)
|
|
346
|
+
nodata: Optional[NoData] = attr.ib(default=None, kw_only=True)
|
|
347
|
+
scales: Optional[List[NumType]] = attr.ib(kw_only=True)
|
|
348
|
+
offsets: Optional[List[NumType]] = attr.ib(kw_only=True)
|
|
314
349
|
band_names: Optional[List[str]] = attr.ib(kw_only=True)
|
|
350
|
+
band_descriptions: Optional[List[str]] = attr.ib(kw_only=True)
|
|
315
351
|
dataset_statistics: Optional[Sequence[Tuple[float, float]]] = attr.ib(
|
|
316
352
|
default=None, kw_only=True
|
|
317
353
|
)
|
|
318
354
|
cutline_mask: Optional[numpy.ndarray] = attr.ib(default=None)
|
|
355
|
+
alpha_mask: Optional[numpy.ndarray] = attr.ib(default=None)
|
|
319
356
|
|
|
320
357
|
@band_names.default
|
|
321
|
-
def
|
|
358
|
+
def _default_band_names(self):
|
|
322
359
|
return [f"b{ix + 1}" for ix in range(self.count)]
|
|
323
360
|
|
|
324
|
-
|
|
325
|
-
|
|
361
|
+
@band_descriptions.default
|
|
362
|
+
def _default_band_descriptions(self):
|
|
363
|
+
return ["" for ix in range(self.count)]
|
|
364
|
+
|
|
365
|
+
@scales.default
|
|
366
|
+
def _default_scales(self):
|
|
367
|
+
return [1.0] * self.count
|
|
368
|
+
|
|
369
|
+
@offsets.default
|
|
370
|
+
def _default_offsets(self):
|
|
371
|
+
return [0.0] * self.count
|
|
372
|
+
|
|
373
|
+
@alpha_mask.validator
|
|
374
|
+
def _check_alpha_mask(self, attribute, value):
|
|
375
|
+
"""Make sure alpha mask has valid shame and datatype."""
|
|
376
|
+
if value is not None:
|
|
377
|
+
if (
|
|
378
|
+
len(value.shape) != 2
|
|
379
|
+
or value.shape[0] != self.height
|
|
380
|
+
or value.shape[1] != self.width
|
|
381
|
+
):
|
|
382
|
+
raise ValueError(
|
|
383
|
+
f"Invalide shape {value.shape} for AlphaMask, should be of shape {self.height}x{self.width}"
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
if not value.dtype == self.array.dtype:
|
|
387
|
+
raise ValueError(
|
|
388
|
+
f"Invalide dtype {value.dtype} for AlphaMask, should be of {self.array.dtype}"
|
|
389
|
+
)
|
|
390
|
+
|
|
326
391
|
@property
|
|
327
392
|
def data(self) -> numpy.ndarray:
|
|
328
393
|
"""Return data part of the masked array."""
|
|
@@ -331,9 +396,17 @@ class ImageData:
|
|
|
331
396
|
@property
|
|
332
397
|
def mask(self) -> numpy.ndarray:
|
|
333
398
|
"""Return Mask in form of rasterio dataset mask."""
|
|
334
|
-
return
|
|
399
|
+
# NOTE: if available we return the alpha_mask
|
|
400
|
+
if self.alpha_mask is not None:
|
|
401
|
+
return self.alpha_mask
|
|
335
402
|
|
|
336
|
-
|
|
403
|
+
minv, maxv = dtype_ranges[str(self.array.dtype)]
|
|
404
|
+
return numpy.where(self._mask, maxv, minv).astype(self.array.dtype)
|
|
405
|
+
|
|
406
|
+
@property
|
|
407
|
+
def _mask(self):
|
|
408
|
+
"""Return `inverted/merged` mask from data array."""
|
|
409
|
+
return numpy.logical_or.reduce(~self.array.mask)
|
|
337
410
|
|
|
338
411
|
def __iter__(self):
|
|
339
412
|
"""Allow for variable expansion (``arr, mask = ImageData``)"""
|
|
@@ -386,7 +459,15 @@ class ImageData:
|
|
|
386
459
|
array,
|
|
387
460
|
crs=dataset.crs,
|
|
388
461
|
bounds=dataset.bounds,
|
|
462
|
+
band_names=[f"b{idx}" for idx in indexes],
|
|
463
|
+
band_descriptions=[
|
|
464
|
+
dataset.descriptions[ix - 1] or "" for idx in indexes
|
|
465
|
+
],
|
|
389
466
|
dataset_statistics=dataset_statistics,
|
|
467
|
+
nodata=dataset.nodata,
|
|
468
|
+
scales=list(dataset.scales),
|
|
469
|
+
offsets=list(dataset.offsets),
|
|
470
|
+
metadata=dataset.tags(),
|
|
390
471
|
)
|
|
391
472
|
|
|
392
473
|
@classmethod
|
|
@@ -439,6 +520,16 @@ class ImageData:
|
|
|
439
520
|
)
|
|
440
521
|
)
|
|
441
522
|
|
|
523
|
+
band_descriptions = list(
|
|
524
|
+
itertools.chain.from_iterable(
|
|
525
|
+
[img.band_descriptions for img in data if img.band_descriptions]
|
|
526
|
+
)
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
scales = list(itertools.chain.from_iterable([img.scales for img in data]))
|
|
530
|
+
|
|
531
|
+
offsets = list(itertools.chain.from_iterable([img.offsets for img in data]))
|
|
532
|
+
|
|
442
533
|
stats = list(
|
|
443
534
|
itertools.chain.from_iterable(
|
|
444
535
|
[img.dataset_statistics for img in data if img.dataset_statistics]
|
|
@@ -458,9 +549,12 @@ class ImageData:
|
|
|
458
549
|
crs=crs,
|
|
459
550
|
bounds=bounds,
|
|
460
551
|
band_names=band_names,
|
|
552
|
+
band_descriptions=band_descriptions,
|
|
461
553
|
dataset_statistics=dataset_statistics,
|
|
462
554
|
cutline_mask=cutline_mask,
|
|
463
555
|
metadata=metadata,
|
|
556
|
+
scales=scales,
|
|
557
|
+
offsets=offsets,
|
|
464
558
|
)
|
|
465
559
|
|
|
466
560
|
def data_as_image(self) -> numpy.ndarray:
|
|
@@ -503,21 +597,65 @@ class ImageData:
|
|
|
503
597
|
) -> Self:
|
|
504
598
|
"""Rescale data in place."""
|
|
505
599
|
self.array = rescale_image(
|
|
506
|
-
self.array
|
|
600
|
+
self.array,
|
|
507
601
|
in_range=in_range,
|
|
508
602
|
out_range=out_range,
|
|
509
603
|
out_dtype=out_dtype,
|
|
510
604
|
)
|
|
605
|
+
if self.alpha_mask is not None:
|
|
606
|
+
self.alpha_mask = linear_rescale(
|
|
607
|
+
self.alpha_mask,
|
|
608
|
+
in_range=dtype_ranges[str(self.alpha_mask.dtype)],
|
|
609
|
+
out_range=dtype_ranges[out_dtype],
|
|
610
|
+
).astype(out_dtype)
|
|
611
|
+
|
|
612
|
+
# reset scales/offsets
|
|
613
|
+
self.scales = [1.0] * self.count
|
|
614
|
+
self.offsets = [0.0] * self.count
|
|
615
|
+
|
|
616
|
+
return self
|
|
617
|
+
|
|
618
|
+
def apply_color_formula(self, color_formula: Optional[str]) -> Self:
|
|
619
|
+
"""Apply color-operations formula in place."""
|
|
620
|
+
out = self.array.data
|
|
621
|
+
out[out < 0] = 0
|
|
622
|
+
|
|
623
|
+
for ops in parse_operations(color_formula):
|
|
624
|
+
out = scale_dtype(ops(to_math_type(out)), numpy.uint8)
|
|
625
|
+
|
|
626
|
+
data = numpy.ma.MaskedArray(out)
|
|
627
|
+
data.mask = self.array.mask
|
|
628
|
+
self.array = data
|
|
629
|
+
|
|
630
|
+
# NOTE: we need to rescale the alpha mask if not in Uint8
|
|
631
|
+
if self.alpha_mask is not None and self.alpha_mask.dtype != "uint8":
|
|
632
|
+
self.alpha_mask = linear_rescale(
|
|
633
|
+
self.alpha_mask, in_range=dtype_ranges[str(self.alpha_mask.dtype)]
|
|
634
|
+
).astype("uint8")
|
|
635
|
+
|
|
636
|
+
# reset scales/offsets
|
|
637
|
+
self.scales = [1.0] * self.count
|
|
638
|
+
self.offsets = [0.0] * self.count
|
|
639
|
+
|
|
511
640
|
return self
|
|
512
641
|
|
|
513
642
|
def apply_colormap(self, colormap: ColorMapType) -> "ImageData":
|
|
514
643
|
"""Apply colormap to the image data."""
|
|
515
644
|
data, alpha = apply_cmap(self.array.data, colormap)
|
|
516
645
|
|
|
517
|
-
# Use Dataset Mask which is fine
|
|
518
|
-
# because in theory self.array should be a 1 band image
|
|
519
646
|
array = numpy.ma.MaskedArray(data)
|
|
520
|
-
|
|
647
|
+
|
|
648
|
+
# `inValid/Valid` values for colormaped datatype (e.g 0 for uint8)
|
|
649
|
+
invalid, valid = dtype_ranges[str(alpha.dtype)]
|
|
650
|
+
|
|
651
|
+
# Combine both dataset mask and alpha from colormap
|
|
652
|
+
array.mask = numpy.bitwise_or(alpha != valid, ~self._mask)
|
|
653
|
+
|
|
654
|
+
if self.alpha_mask is not None:
|
|
655
|
+
warnings.warn(
|
|
656
|
+
"Alpha Mask value ignored from the input ImageData object when applying colormap.",
|
|
657
|
+
UserWarning,
|
|
658
|
+
)
|
|
521
659
|
|
|
522
660
|
return ImageData(
|
|
523
661
|
array,
|
|
@@ -525,21 +663,10 @@ class ImageData:
|
|
|
525
663
|
crs=self.crs,
|
|
526
664
|
bounds=self.bounds,
|
|
527
665
|
metadata=self.metadata,
|
|
666
|
+
# NOTE: make sure masked part from initial data are also masked in alpha band
|
|
667
|
+
alpha_mask=numpy.where(~self._mask, invalid, alpha),
|
|
528
668
|
)
|
|
529
669
|
|
|
530
|
-
def apply_color_formula(self, color_formula: Optional[str]) -> Self:
|
|
531
|
-
"""Apply color-operations formula in place."""
|
|
532
|
-
out = self.array.data.copy()
|
|
533
|
-
out[out < 0] = 0
|
|
534
|
-
|
|
535
|
-
for ops in parse_operations(color_formula):
|
|
536
|
-
out = scale_dtype(ops(to_math_type(out)), numpy.uint8)
|
|
537
|
-
|
|
538
|
-
data = numpy.ma.MaskedArray(out)
|
|
539
|
-
data.mask = self.array.mask
|
|
540
|
-
self.array = data
|
|
541
|
-
return self
|
|
542
|
-
|
|
543
670
|
def apply_expression(self, expression: str) -> "ImageData":
|
|
544
671
|
"""Apply expression to the image data."""
|
|
545
672
|
blocks = get_expression_blocks(expression)
|
|
@@ -557,10 +684,12 @@ class ImageData:
|
|
|
557
684
|
)
|
|
558
685
|
)
|
|
559
686
|
|
|
560
|
-
data = apply_expression(blocks, self.band_names, self.array)
|
|
687
|
+
data = apply_expression(blocks, self.band_names, self.array.copy())
|
|
561
688
|
# NOTE: We use dataset mask when mixing bands
|
|
562
689
|
data.mask = numpy.logical_or.reduce(self.array.mask)
|
|
563
690
|
|
|
691
|
+
# TODO: update band descriptions
|
|
692
|
+
|
|
564
693
|
return ImageData(
|
|
565
694
|
data,
|
|
566
695
|
assets=self.assets,
|
|
@@ -569,6 +698,7 @@ class ImageData:
|
|
|
569
698
|
band_names=blocks,
|
|
570
699
|
metadata=self.metadata,
|
|
571
700
|
dataset_statistics=stats,
|
|
701
|
+
alpha_mask=self.alpha_mask,
|
|
572
702
|
)
|
|
573
703
|
|
|
574
704
|
def resize(
|
|
@@ -582,6 +712,9 @@ class ImageData:
|
|
|
582
712
|
mask = resize_array(self.array.mask * 1, height, width, resampling_method).astype(
|
|
583
713
|
"bool"
|
|
584
714
|
)
|
|
715
|
+
alpha_mask = self.alpha_mask
|
|
716
|
+
if alpha_mask is not None:
|
|
717
|
+
alpha_mask = resize_array(alpha_mask.copy(), height, width, resampling_method)
|
|
585
718
|
|
|
586
719
|
return ImageData(
|
|
587
720
|
numpy.ma.MaskedArray(data, mask=mask),
|
|
@@ -589,8 +722,13 @@ class ImageData:
|
|
|
589
722
|
crs=self.crs,
|
|
590
723
|
bounds=self.bounds,
|
|
591
724
|
band_names=self.band_names,
|
|
725
|
+
band_descriptions=self.band_descriptions,
|
|
726
|
+
nodata=self.nodata,
|
|
727
|
+
scales=self.scales,
|
|
728
|
+
offsets=self.offsets,
|
|
592
729
|
metadata=self.metadata,
|
|
593
730
|
dataset_statistics=self.dataset_statistics,
|
|
731
|
+
alpha_mask=alpha_mask,
|
|
594
732
|
)
|
|
595
733
|
|
|
596
734
|
def clip(self, bbox: BBox) -> "ImageData":
|
|
@@ -605,8 +743,15 @@ class ImageData:
|
|
|
605
743
|
crs=self.crs,
|
|
606
744
|
bounds=bbox,
|
|
607
745
|
band_names=self.band_names,
|
|
746
|
+
band_descriptions=self.band_descriptions,
|
|
747
|
+
nodata=self.nodata,
|
|
748
|
+
scales=self.scales,
|
|
749
|
+
offsets=self.offsets,
|
|
608
750
|
metadata=self.metadata,
|
|
609
751
|
dataset_statistics=self.dataset_statistics,
|
|
752
|
+
alpha_mask=self.alpha_mask[row_slice, col_slice].copy()
|
|
753
|
+
if self.alpha_mask is not None
|
|
754
|
+
else None,
|
|
610
755
|
)
|
|
611
756
|
|
|
612
757
|
def post_process(
|
|
@@ -633,25 +778,24 @@ class ImageData:
|
|
|
633
778
|
>>> img.post_process(color_formula="Gamma RGB 4.1")
|
|
634
779
|
|
|
635
780
|
"""
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
if in_range:
|
|
639
|
-
array = rescale_image(array, in_range, out_dtype=out_dtype, **kwargs)
|
|
640
|
-
|
|
641
|
-
if color_formula:
|
|
642
|
-
array[array < 0] = 0
|
|
643
|
-
for ops in parse_operations(color_formula):
|
|
644
|
-
array = scale_dtype(ops(to_math_type(array)), numpy.uint8)
|
|
645
|
-
array.mask = self.array.mask
|
|
646
|
-
|
|
647
|
-
return ImageData(
|
|
648
|
-
array,
|
|
781
|
+
img = ImageData(
|
|
782
|
+
self.array.copy(),
|
|
649
783
|
crs=self.crs,
|
|
650
784
|
bounds=self.bounds,
|
|
651
785
|
assets=self.assets,
|
|
652
786
|
metadata=self.metadata,
|
|
787
|
+
dataset_statistics=self.dataset_statistics,
|
|
788
|
+
alpha_mask=self.alpha_mask.copy() if self.alpha_mask is not None else None,
|
|
653
789
|
)
|
|
654
790
|
|
|
791
|
+
if in_range:
|
|
792
|
+
img.rescale(in_range, out_dtype=out_dtype, **kwargs)
|
|
793
|
+
|
|
794
|
+
if color_formula:
|
|
795
|
+
img.apply_color_formula(color_formula)
|
|
796
|
+
|
|
797
|
+
return img
|
|
798
|
+
|
|
655
799
|
def render(
|
|
656
800
|
self,
|
|
657
801
|
add_mask: bool = True,
|
|
@@ -680,39 +824,31 @@ class ImageData:
|
|
|
680
824
|
kwargs.update({"crs": self.crs})
|
|
681
825
|
|
|
682
826
|
array = self.array.copy()
|
|
827
|
+
mask = self.mask
|
|
683
828
|
|
|
684
829
|
datatype_range = self.dataset_statistics or (dtype_ranges[str(array.dtype)],)
|
|
685
830
|
|
|
686
831
|
if not colormap:
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
warnings.warn(
|
|
696
|
-
f"Invalid type: `{array.dtype}` for the `{img_format}` driver. Data will be rescaled using min/max type bounds or dataset_statistics.",
|
|
697
|
-
InvalidDatatypeWarning,
|
|
698
|
-
)
|
|
699
|
-
array = rescale_image(array, in_range=datatype_range)
|
|
700
|
-
|
|
701
|
-
elif img_format in ["JP2OPENJPEG"] and array.dtype not in [
|
|
702
|
-
"uint8",
|
|
703
|
-
"int16",
|
|
704
|
-
"uint16",
|
|
705
|
-
]:
|
|
832
|
+
format_dtypes = {
|
|
833
|
+
"PNG": ["uint8", "uint16"],
|
|
834
|
+
"JPEG": ["uint8"],
|
|
835
|
+
"WEBP": ["uint8"],
|
|
836
|
+
"JP2OPENJPEG": ["uint8", "int16", "uint16"],
|
|
837
|
+
}
|
|
838
|
+
valid_dtypes = format_dtypes.get(img_format, [])
|
|
839
|
+
if valid_dtypes and array.dtype not in valid_dtypes:
|
|
706
840
|
warnings.warn(
|
|
707
841
|
f"Invalid type: `{array.dtype}` for the `{img_format}` driver. Data will be rescaled using min/max type bounds or dataset_statistics.",
|
|
708
842
|
InvalidDatatypeWarning,
|
|
709
843
|
)
|
|
710
844
|
array = rescale_image(array, in_range=datatype_range)
|
|
845
|
+
if mask is not None:
|
|
846
|
+
mask = linear_rescale(mask, in_range=dtype_ranges[str(array.dtype)])
|
|
711
847
|
|
|
712
848
|
if add_mask:
|
|
713
849
|
return render(
|
|
714
850
|
array.data,
|
|
715
|
-
|
|
851
|
+
mask,
|
|
716
852
|
img_format=img_format,
|
|
717
853
|
colormap=colormap,
|
|
718
854
|
**kwargs,
|
|
@@ -728,7 +864,7 @@ class ImageData:
|
|
|
728
864
|
if "crs" not in kwargs and self.crs:
|
|
729
865
|
kwargs.update({"crs": self.crs})
|
|
730
866
|
|
|
731
|
-
write_nodata =
|
|
867
|
+
write_nodata = self.nodata is not None
|
|
732
868
|
count, height, width = self.array.shape
|
|
733
869
|
|
|
734
870
|
output_profile = {
|
|
@@ -736,6 +872,7 @@ class ImageData:
|
|
|
736
872
|
"count": count if write_nodata else count + 1,
|
|
737
873
|
"height": height,
|
|
738
874
|
"width": width,
|
|
875
|
+
"nodata": self.nodata,
|
|
739
876
|
}
|
|
740
877
|
output_profile.update(kwargs)
|
|
741
878
|
|
|
@@ -858,6 +995,21 @@ class ImageData:
|
|
|
858
995
|
resampling=Resampling[reproject_method],
|
|
859
996
|
)
|
|
860
997
|
|
|
998
|
+
alpha_mask = self.alpha_mask
|
|
999
|
+
if self.alpha_mask is not None:
|
|
1000
|
+
alpha_mask = numpy.ma.masked_array(
|
|
1001
|
+
numpy.zeros((h, w), dtype=self.alpha_mask.dtype),
|
|
1002
|
+
)
|
|
1003
|
+
alpha_mask, _ = reproject(
|
|
1004
|
+
self.alpha_mask,
|
|
1005
|
+
alpha_mask,
|
|
1006
|
+
src_transform=self.transform,
|
|
1007
|
+
src_crs=self.crs,
|
|
1008
|
+
dst_transform=dst_transform,
|
|
1009
|
+
dst_crs=dst_crs,
|
|
1010
|
+
resampling=Resampling[reproject_method],
|
|
1011
|
+
)
|
|
1012
|
+
|
|
861
1013
|
bounds = array_bounds(h, w, dst_transform)
|
|
862
1014
|
|
|
863
1015
|
return ImageData(
|
|
@@ -866,6 +1018,11 @@ class ImageData:
|
|
|
866
1018
|
crs=dst_crs,
|
|
867
1019
|
bounds=bounds,
|
|
868
1020
|
band_names=self.band_names,
|
|
1021
|
+
band_descriptions=self.band_descriptions,
|
|
1022
|
+
nodata=self.nodata,
|
|
1023
|
+
scales=self.scales,
|
|
1024
|
+
offsets=self.offsets,
|
|
869
1025
|
metadata=self.metadata,
|
|
870
1026
|
dataset_statistics=self.dataset_statistics,
|
|
1027
|
+
alpha_mask=alpha_mask,
|
|
871
1028
|
)
|