mapchete-eo 2025.8.2__py2.py3-none-any.whl → 2025.10.0__py2.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.
mapchete_eo/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "2025.8.2"
1
+ __version__ = "2025.10.0"
mapchete_eo/base.py CHANGED
@@ -5,6 +5,7 @@ from functools import cached_property
5
5
  from typing import Any, Callable, List, Optional, Type, Union
6
6
 
7
7
  import croniter
8
+ from mapchete import Bounds
8
9
  import numpy.ma as ma
9
10
  import xarray as xr
10
11
  from dateutil.tz import tzutc
@@ -436,7 +437,13 @@ class InputData(base.InputData):
436
437
  process_area = input_params["delimiters"]["effective_area"]
437
438
  if self.params.area:
438
439
  # read area parameter and intersect with effective area
439
- configured_area, configured_area_crs = guess_geometry(self.params.area)
440
+ configured_area, configured_area_crs = guess_geometry(
441
+ self.params.area,
442
+ bounds=Bounds.from_inp(
443
+ input_params.get("delimiters", {}).get("bounds"),
444
+ crs=getattr(input_params.get("pyramid"), "crs"),
445
+ ),
446
+ )
440
447
  return process_area.intersection(
441
448
  reproject_geometry(
442
449
  configured_area,
@@ -0,0 +1,579 @@
1
+ """
2
+
3
+ Original LICENSE:
4
+
5
+ MIT License
6
+
7
+ Copyright (c) 2016 Florian Roscheck
8
+
9
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
10
+ documentation files (the "Software"), to deal in the Software without restriction, including without limitation th
11
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
12
+ persons to whom the Software is furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
15
+ Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
18
+ INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
21
+ OR OTHER DEALINGS IN THE SOFTWARE.
22
+
23
+ """
24
+
25
+ import numpy as np
26
+
27
+
28
+ def _compose_alpha(fg: np.ndarray, bg: np.ndarray, opacity: float):
29
+ """Calculate alpha composition ratio between two images."""
30
+
31
+ comp_alpha = np.minimum(fg[:, :, 3], bg[:, :, 3]) * opacity
32
+ new_alpha = fg[:, :, 3] + (1.0 - fg[:, :, 3]) * comp_alpha
33
+ np.seterr(divide="ignore", invalid="ignore")
34
+ ratio = comp_alpha / new_alpha
35
+ ratio[np.isnan(ratio)] = 0.0
36
+ return ratio
37
+
38
+
39
+ def normal(fg: np.ndarray, bg: np.ndarray, opacity: float):
40
+ """Apply "normal" blending mode of a layer on an image.
41
+
42
+ See Also:
43
+ Find more information on `Wikipedia <https://en.wikipedia.org/wiki/Alpha_compositing#Description>`__.
44
+
45
+ Args:
46
+ fg(3-dimensional numpy array of floats (r/g/b/a) in range 0.0-255.0): Layer to be blended with image
47
+ bg(3-dimensional numpy array of floats (r/g/b/a) in range 0-255.0): Image to be blended upon
48
+ opacity(float): Desired opacity of layer for blending
49
+ disable_type_checks(bool): Whether type checks within the function should be disabled. Disabling the checks may
50
+ yield a slight performance improvement, but comes at the cost of user experience. If you are certain that
51
+ you are passing in the right arguments, you may set this argument to 'True'. Defaults to 'False'.
52
+
53
+ Returns:
54
+ 3-dimensional numpy array of floats (r/g/b/a) in range 0.0-255.0: Blended image
55
+ """
56
+
57
+ # Extract alpha-channels and apply opacity
58
+ fg_alp = np.expand_dims(fg[:, :, 3], 2) # alpha of b, prepared for broadcasting
59
+ bg_alp = (
60
+ np.expand_dims(bg[:, :, 3], 2) * opacity
61
+ ) # alpha of a, prepared for broadcasting
62
+
63
+ # Blend images
64
+ with np.errstate(divide="ignore", invalid="ignore"):
65
+ img_out = (bg[:, :, :3] * bg_alp + fg[:, :, :3] * fg_alp * (1 - bg_alp)) / (
66
+ bg_alp + fg_alp * (1 - bg_alp)
67
+ )
68
+ img_out[np.isnan(img_out)] = 0 # replace NaNs with 0
69
+
70
+ # Blend alpha
71
+ cout_alp = bg_alp + fg_alp * (1 - bg_alp)
72
+
73
+ # Combine image and alpha
74
+ img_out = np.dstack((img_out, cout_alp))
75
+
76
+ np.nan_to_num(img_out, copy=False)
77
+
78
+ return img_out
79
+
80
+
81
+ def soft_light(fg: np.ndarray, bg: np.ndarray, opacity: float):
82
+ """Apply soft light blending mode of a layer on an image.
83
+
84
+ See Also:
85
+ Find more information on
86
+ `Wikipedia <https://en.wikipedia.org/w/index.php?title=Blend_modes&oldid=747749280#Soft_Light>`__.
87
+
88
+ Args:
89
+ fg(3-dimensional numpy array of floats (r/g/b/a) in range 0-255.0): Image to be blended upon
90
+ bg(3-dimensional numpy array of floats (r/g/b/a) in range 0.0-255.0): Layer to be blended with image
91
+ opacity(float): Desired opacity of layer for blending
92
+ disable_type_checks(bool): Whether type checks within the function should be disabled. Disabling the checks may
93
+ yield a slight performance improvement, but comes at the cost of user experience. If you are certain that
94
+ you are passing in the right arguments, you may set this argument to 'True'. Defaults to 'False'.
95
+
96
+ Returns:
97
+ 3-dimensional numpy array of floats (r/g/b/a) in range 0.0-255.0: Blended image
98
+
99
+ """
100
+
101
+ ratio = _compose_alpha(fg, bg, opacity)
102
+
103
+ # The following code does this:
104
+ # multiply = fg[:, :, :3]*bg[:, :, :3]
105
+ # screen = 1.0 - (1.0-fg[:, :, :3])*(1.0-bg[:, :, :3])
106
+ # comp = (1.0 - fg[:, :, :3]) * multiply + fg[:, :, :3] * screen
107
+ # ratio_rs = np.reshape(np.repeat(ratio,3),comp.shape)
108
+ # img_out = comp*ratio_rs + fg[:, :, :3] * (1.0-ratio_rs)
109
+
110
+ comp = (1.0 - fg[:, :, :3]) * fg[:, :, :3] * bg[:, :, :3] + fg[:, :, :3] * (
111
+ 1.0 - (1.0 - fg[:, :, :3]) * (1.0 - bg[:, :, :3])
112
+ )
113
+
114
+ ratio_rs = np.reshape(
115
+ np.repeat(ratio, 3), [comp.shape[0], comp.shape[1], comp.shape[2]]
116
+ )
117
+ img_out = comp * ratio_rs + fg[:, :, :3] * (1.0 - ratio_rs)
118
+ img_out = np.nan_to_num(
119
+ np.dstack((img_out, fg[:, :, 3]))
120
+ ) # add alpha channel and replace nans
121
+ return img_out
122
+
123
+
124
+ def lighten_only(fg: np.ndarray, bg: np.ndarray, opacity: float):
125
+ """Apply lighten only blending mode of a layer on an image.
126
+
127
+ See Also:
128
+ Find more information on
129
+ `Wikipedia <https://en.wikipedia.org/w/index.php?title=Blend_modes&oldid=747749280#Lighten_Only>`__.
130
+
131
+ Args:
132
+ fg(3-dimensional numpy array of floats (r/g/b/a) in range 0-255.0): Image to be blended upon
133
+ bg(3-dimensional numpy array of floats (r/g/b/a) in range 0.0-255.0): Layer to be blended with image
134
+ opacity(float): Desired opacity of layer for blending
135
+ disable_type_checks(bool): Whether type checks within the function should be disabled. Disabling the checks may
136
+ yield a slight performance improvement, but comes at the cost of user experience. If you are certain that
137
+ you are passing in the right arguments, you may set this argument to 'True'. Defaults to 'False'.
138
+
139
+ Returns:
140
+ 3-dimensional numpy array of floats (r/g/b/a) in range 0.0-255.0: Blended image
141
+
142
+ """
143
+
144
+ ratio = _compose_alpha(fg, bg, opacity)
145
+
146
+ comp = np.maximum(fg[:, :, :3], bg[:, :, :3])
147
+
148
+ ratio_rs = np.reshape(
149
+ np.repeat(ratio, 3), [comp.shape[0], comp.shape[1], comp.shape[2]]
150
+ )
151
+ img_out = comp * ratio_rs + fg[:, :, :3] * (1.0 - ratio_rs)
152
+ img_out = np.nan_to_num(
153
+ np.dstack((img_out, fg[:, :, 3]))
154
+ ) # add alpha channel and replace nans
155
+ return img_out
156
+
157
+
158
+ def screen(fg: np.ndarray, bg: np.ndarray, opacity: float):
159
+ """Apply screen blending mode of a layer on an image.
160
+
161
+ See Also:
162
+ Find more information on
163
+ `Wikipedia <https://en.wikipedia.org/w/index.php?title=Blend_modes&oldid=747749280#Screen>`__.
164
+
165
+ Args:
166
+ fg(3-dimensional numpy array of floats (r/g/b/a) in range 0-255.0): Image to be blended upon
167
+ bg(3-dimensional numpy array of floats (r/g/b/a) in range 0.0-255.0): Layer to be blended with image
168
+ opacity(float): Desired opacity of layer for blending
169
+ disable_type_checks(bool): Whether type checks within the function should be disabled. Disabling the checks may
170
+ yield a slight performance improvement, but comes at the cost of user experience. If you are certain that
171
+ you are passing in the right arguments, you may set this argument to 'True'. Defaults to 'False'.
172
+
173
+ Returns:
174
+ 3-dimensional numpy array of floats (r/g/b/a) in range 0.0-255.0: Blended image
175
+
176
+ """
177
+
178
+ ratio = _compose_alpha(fg, bg, opacity)
179
+
180
+ comp = 1.0 - (1.0 - fg[:, :, :3]) * (1.0 - bg[:, :, :3])
181
+
182
+ ratio_rs = np.reshape(
183
+ np.repeat(ratio, 3), [comp.shape[0], comp.shape[1], comp.shape[2]]
184
+ )
185
+ img_out = comp * ratio_rs + fg[:, :, :3] * (1.0 - ratio_rs)
186
+ img_out = np.nan_to_num(
187
+ np.dstack((img_out, fg[:, :, 3]))
188
+ ) # add alpha channel and replace nans
189
+ return img_out
190
+
191
+
192
+ def dodge(fg: np.ndarray, bg: np.ndarray, opacity: float):
193
+ """Apply dodge blending mode of a layer on an image.
194
+
195
+ See Also:
196
+ Find more information on
197
+ `Wikipedia <https://en.wikipedia.org/w/index.php?title=Blend_modes&oldid=747749280#Dodge_and_burn>`__.
198
+
199
+ Args:
200
+ fg(3-dimensional numpy array of floats (r/g/b/a) in range 0-255.0): Image to be blended upon
201
+ bg(3-dimensional numpy array of floats (r/g/b/a) in range 0.0-255.0): Layer to be blended with image
202
+ opacity(float): Desired opacity of layer for blending
203
+ disable_type_checks(bool): Whether type checks within the function should be disabled. Disabling the checks may
204
+ yield a slight performance improvement, but comes at the cost of user experience. If you are certain that
205
+ you are passing in the right arguments, you may set this argument to 'True'. Defaults to 'False'.
206
+
207
+ Returns:
208
+ 3-dimensional numpy array of floats (r/g/b/a) in range 0.0-255.0: Blended image
209
+
210
+ """
211
+
212
+ ratio = _compose_alpha(fg, bg, opacity)
213
+
214
+ comp = np.minimum(fg[:, :, :3] / (1.0 - bg[:, :, :3]), 1.0)
215
+
216
+ ratio_rs = np.reshape(
217
+ np.repeat(ratio, 3), [comp.shape[0], comp.shape[1], comp.shape[2]]
218
+ )
219
+ img_out = comp * ratio_rs + fg[:, :, :3] * (1.0 - ratio_rs)
220
+ img_out = np.nan_to_num(
221
+ np.dstack((img_out, fg[:, :, 3]))
222
+ ) # add alpha channel and replace nans
223
+ return img_out
224
+
225
+
226
+ def addition(fg: np.ndarray, bg: np.ndarray, opacity: float):
227
+ """Apply addition blending mode of a layer on an image.
228
+
229
+ See Also:
230
+ Find more information on
231
+ `Wikipedia <https://en.wikipedia.org/w/index.php?title=Blend_modes&oldid=747749280#Addition>`__.
232
+
233
+ Args:
234
+ fg(3-dimensional numpy array of floats (r/g/b/a) in range 0-255.0): Image to be blended upon
235
+ bg(3-dimensional numpy array of floats (r/g/b/a) in range 0.0-255.0): Layer to be blended with image
236
+ opacity(float): Desired opacity of layer for blending
237
+ disable_type_checks(bool): Whether type checks within the function should be disabled. Disabling the checks may
238
+ yield a slight performance improvement, but comes at the cost of user experience. If you are certain that
239
+ you are passing in the right arguments, you may set this argument to 'True'. Defaults to 'False'.
240
+
241
+ Returns:
242
+ 3-dimensional numpy array of floats (r/g/b/a) in range 0.0-255.0: Blended image
243
+
244
+ """
245
+
246
+ ratio = _compose_alpha(fg, bg, opacity)
247
+
248
+ comp = fg[:, :, :3] + bg[:, :, :3]
249
+
250
+ ratio_rs = np.reshape(
251
+ np.repeat(ratio, 3), [comp.shape[0], comp.shape[1], comp.shape[2]]
252
+ )
253
+ img_out = np.clip(comp * ratio_rs + fg[:, :, :3] * (1.0 - ratio_rs), 0.0, 1.0)
254
+ img_out = np.nan_to_num(
255
+ np.dstack((img_out, fg[:, :, 3]))
256
+ ) # add alpha channel and replace nans
257
+ return img_out
258
+
259
+
260
+ def darken_only(fg: np.ndarray, bg: np.ndarray, opacity: float):
261
+ """Apply darken only blending mode of a layer on an image.
262
+
263
+ See Also:
264
+ Find more information on
265
+ `Wikipedia <https://en.wikipedia.org/w/index.php?title=Blend_modes&oldid=747749280#Darken_Only>`__.
266
+
267
+ Args:
268
+ fg(3-dimensional numpy array of floats (r/g/b/a) in range 0-255.0): Image to be blended upon
269
+ bg(3-dimensional numpy array of floats (r/g/b/a) in range 0.0-255.0): Layer to be blended with image
270
+ opacity(float): Desired opacity of layer for blending
271
+ disable_type_checks(bool): Whether type checks within the function should be disabled. Disabling the checks may
272
+ yield a slight performance improvement, but comes at the cost of user experience. If you are certain that
273
+ you are passing in the right arguments, you may set this argument to 'True'. Defaults to 'False'.
274
+
275
+ Returns:
276
+ 3-dimensional numpy array of floats (r/g/b/a) in range 0.0-255.0: Blended image
277
+
278
+ """
279
+
280
+ ratio = _compose_alpha(fg, bg, opacity)
281
+
282
+ comp = np.minimum(fg[:, :, :3], bg[:, :, :3])
283
+
284
+ ratio_rs = np.reshape(
285
+ np.repeat(ratio, 3), [comp.shape[0], comp.shape[1], comp.shape[2]]
286
+ )
287
+ img_out = comp * ratio_rs + fg[:, :, :3] * (1.0 - ratio_rs)
288
+ img_out = np.nan_to_num(
289
+ np.dstack((img_out, fg[:, :, 3]))
290
+ ) # add alpha channel and replace nans
291
+ return img_out
292
+
293
+
294
+ def multiply(fg: np.ndarray, bg: np.ndarray, opacity: float):
295
+ """Apply multiply blending mode of a layer on an image.
296
+
297
+ See Also:
298
+ Find more information on
299
+ `Wikipedia <https://en.wikipedia.org/w/index.php?title=Blend_modes&oldid=747749280#Multiply>`__.
300
+
301
+ Args:
302
+ fg(3-dimensional numpy array of floats (r/g/b/a) in range 0-255.0): Image to be blended upon
303
+ bg(3-dimensional numpy array of floats (r/g/b/a) in range 0.0-255.0): Layer to be blended with image
304
+ opacity(float): Desired opacity of layer for blending
305
+ disable_type_checks(bool): Whether type checks within the function should be disabled. Disabling the checks may
306
+ yield a slight performance improvement, but comes at the cost of user experience. If you are certain that
307
+ you are passing in the right arguments, you may set this argument to 'True'. Defaults to 'False'.
308
+
309
+ Returns:
310
+ 3-dimensional numpy array of floats (r/g/b/a) in range 0.0-255.0: Blended image
311
+
312
+ """
313
+
314
+ ratio = _compose_alpha(fg, bg, opacity)
315
+
316
+ comp = np.clip(bg[:, :, :3] * fg[:, :, :3], 0.0, 1.0)
317
+
318
+ ratio_rs = np.reshape(
319
+ np.repeat(ratio, 3), [comp.shape[0], comp.shape[1], comp.shape[2]]
320
+ )
321
+ img_out = comp * ratio_rs + fg[:, :, :3] * (1.0 - ratio_rs)
322
+ img_out = np.nan_to_num(
323
+ np.dstack((img_out, fg[:, :, 3]))
324
+ ) # add alpha channel and replace nans
325
+ return img_out
326
+
327
+
328
+ def hard_light(fg: np.ndarray, bg: np.ndarray, opacity: float):
329
+ """Apply hard light blending mode of a layer on an image.
330
+
331
+ See Also:
332
+ Find more information on
333
+ `Wikipedia <https://en.wikipedia.org/w/index.php?title=Blend_modes&oldid=747749280#Hard_Light>`__.
334
+
335
+ Args:
336
+ fg(3-dimensional numpy array of floats (r/g/b/a) in range 0-255.0): Image to be blended upon
337
+ bg(3-dimensional numpy array of floats (r/g/b/a) in range 0.0-255.0): Layer to be blended with image
338
+ opacity(float): Desired opacity of layer for blending
339
+ disable_type_checks(bool): Whether type checks within the function should be disabled. Disabling the checks may
340
+ yield a slight performance improvement, but comes at the cost of user experience. If you are certain that
341
+ you are passing in the right arguments, you may set this argument to 'True'. Defaults to 'False'.
342
+
343
+ Returns:
344
+ 3-dimensional numpy array of floats (r/g/b/a) in range 0.0-255.0: Blended image
345
+
346
+ """
347
+
348
+ ratio = _compose_alpha(fg, bg, opacity)
349
+
350
+ comp = np.greater(bg[:, :, :3], 0.5) * np.minimum(
351
+ 1.0 - ((1.0 - fg[:, :, :3]) * (1.0 - (bg[:, :, :3] - 0.5) * 2.0)),
352
+ 1.0,
353
+ ) + np.logical_not(np.greater(bg[:, :, :3], 0.5)) * np.minimum(
354
+ fg[:, :, :3] * (bg[:, :, :3] * 2.0), 1.0
355
+ )
356
+
357
+ ratio_rs = np.reshape(
358
+ np.repeat(ratio, 3), [comp.shape[0], comp.shape[1], comp.shape[2]]
359
+ )
360
+ img_out = comp * ratio_rs + fg[:, :, :3] * (1.0 - ratio_rs)
361
+ img_out = np.nan_to_num(
362
+ np.dstack((img_out, fg[:, :, 3]))
363
+ ) # add alpha channel and replace nans
364
+ return img_out
365
+
366
+
367
+ def difference(fg: np.ndarray, bg: np.ndarray, opacity: float):
368
+ """Apply difference blending mode of a layer on an image.
369
+
370
+ See Also:
371
+ Find more information on
372
+ `Wikipedia <https://en.wikipedia.org/w/index.php?title=Blend_modes&oldid=747749280#Difference>`__.
373
+
374
+ Args:
375
+ fg(3-dimensional numpy array of floats (r/g/b/a) in range 0-255.0): Image to be blended upon
376
+ bg(3-dimensional numpy array of floats (r/g/b/a) in range 0.0-255.0): Layer to be blended with image
377
+ opacity(float): Desired opacity of layer for blending
378
+ disable_type_checks(bool): Whether type checks within the function should be disabled. Disabling the checks may
379
+ yield a slight performance improvement, but comes at the cost of user experience. If you are certain that
380
+ you are passing in the right arguments, you may set this argument to 'True'. Defaults to 'False'.
381
+
382
+ Returns:
383
+ 3-dimensional numpy array of floats (r/g/b/a) in range 0.0-255.0: Blended image
384
+
385
+ """
386
+
387
+ ratio = _compose_alpha(fg, bg, opacity)
388
+
389
+ comp = fg[:, :, :3] - bg[:, :, :3]
390
+ comp[comp < 0.0] *= -1.0
391
+
392
+ ratio_rs = np.reshape(
393
+ np.repeat(ratio, 3), [comp.shape[0], comp.shape[1], comp.shape[2]]
394
+ )
395
+ img_out = comp * ratio_rs + fg[:, :, :3] * (1.0 - ratio_rs)
396
+ img_out = np.nan_to_num(
397
+ np.dstack((img_out, fg[:, :, 3]))
398
+ ) # add alpha channel and replace nans
399
+ return img_out
400
+
401
+
402
+ def subtract(fg: np.ndarray, bg: np.ndarray, opacity: float):
403
+ """Apply subtract blending mode of a layer on an image.
404
+
405
+ See Also:
406
+ Find more information on
407
+ `Wikipedia <https://en.wikipedia.org/w/index.php?title=Blend_modes&oldid=747749280#Subtract>`__.
408
+
409
+ Args:
410
+ fg(3-dimensional numpy array of floats (r/g/b/a) in range 0-255.0): Image to be blended upon
411
+ bg(3-dimensional numpy array of floats (r/g/b/a) in range 0.0-255.0): Layer to be blended with image
412
+ opacity(float): Desired opacity of layer for blending
413
+ disable_type_checks(bool): Whether type checks within the function should be disabled. Disabling the checks may
414
+ yield a slight performance improvement, but comes at the cost of user experience. If you are certain that
415
+ you are passing in the right arguments, you may set this argument to 'True'. Defaults to 'False'.
416
+
417
+ Returns:
418
+ 3-dimensional numpy array of floats (r/g/b/a) in range 0.0-255.0: Blended image
419
+
420
+ """
421
+
422
+ ratio = _compose_alpha(fg, bg, opacity)
423
+
424
+ comp = fg[:, :, :3] - bg[:, :, :3]
425
+
426
+ ratio_rs = np.reshape(
427
+ np.repeat(ratio, 3), [comp.shape[0], comp.shape[1], comp.shape[2]]
428
+ )
429
+ img_out = np.clip(comp * ratio_rs + fg[:, :, :3] * (1.0 - ratio_rs), 0.0, 1.0)
430
+ img_out = np.nan_to_num(
431
+ np.dstack((img_out, fg[:, :, 3]))
432
+ ) # add alpha channel and replace nans
433
+ return img_out
434
+
435
+
436
+ def grain_extract(fg: np.ndarray, bg: np.ndarray, opacity: float):
437
+ """Apply grain extract blending mode of a layer on an image.
438
+
439
+ See Also:
440
+ Find more information in the `GIMP Documentation <https://docs.gimp.org/en/gimp-concepts-layer-modes.html>`__.
441
+
442
+ Args:
443
+ fg(3-dimensional numpy array of floats (r/g/b/a) in range 0-255.0): Image to be blended upon
444
+ bg(3-dimensional numpy array of floats (r/g/b/a) in range 0.0-255.0): Layer to be blended with image
445
+ opacity(float): Desired opacity of layer for blending
446
+ disable_type_checks(bool): Whether type checks within the function should be disabled. Disabling the checks may
447
+ yield a slight performance improvement, but comes at the cost of user experience. If you are certain that
448
+ you are passing in the right arguments, you may set this argument to 'True'. Defaults to 'False'.
449
+
450
+ Returns:
451
+ 3-dimensional numpy array of floats (r/g/b/a) in range 0.0-255.0: Blended image
452
+
453
+ """
454
+
455
+ ratio = _compose_alpha(fg, bg, opacity)
456
+
457
+ comp = np.clip(fg[:, :, :3] - bg[:, :, :3] + 0.5, 0.0, 1.0)
458
+
459
+ ratio_rs = np.reshape(
460
+ np.repeat(ratio, 3), [comp.shape[0], comp.shape[1], comp.shape[2]]
461
+ )
462
+ img_out = comp * ratio_rs + fg[:, :, :3] * (1.0 - ratio_rs)
463
+ img_out = np.nan_to_num(
464
+ np.dstack((img_out, fg[:, :, 3]))
465
+ ) # add alpha channel and replace nans
466
+ return img_out
467
+
468
+
469
+ def grain_merge(fg: np.ndarray, bg: np.ndarray, opacity: float):
470
+ """Apply grain merge blending mode of a layer on an image.
471
+
472
+ See Also:
473
+ Find more information in the `GIMP Documentation <https://docs.gimp.org/en/gimp-concepts-layer-modes.html>`__.
474
+
475
+ Args:
476
+ fg(3-dimensional numpy array of floats (r/g/b/a) in range 0-255.0): Image to be blended upon
477
+ bg(3-dimensional numpy array of floats (r/g/b/a) in range 0.0-255.0): Layer to be blended with image
478
+ opacity(float): Desired opacity of layer for blending
479
+ disable_type_checks(bool): Whether type checks within the function should be disabled. Disabling the checks may
480
+ yield a slight performance improvement, but comes at the cost of user experience. If you are certain that
481
+ you are passing in the right arguments, you may set this argument to 'True'. Defaults to 'False'.
482
+
483
+ Returns:
484
+ 3-dimensional numpy array of floats (r/g/b/a) in range 0.0-255.0: Blended image
485
+
486
+ """
487
+
488
+ ratio = _compose_alpha(fg, bg, opacity)
489
+
490
+ comp = np.clip(fg[:, :, :3] + bg[:, :, :3] - 0.5, 0.0, 1.0)
491
+
492
+ ratio_rs = np.reshape(
493
+ np.repeat(ratio, 3), [comp.shape[0], comp.shape[1], comp.shape[2]]
494
+ )
495
+ img_out = comp * ratio_rs + fg[:, :, :3] * (1.0 - ratio_rs)
496
+ img_out = np.nan_to_num(
497
+ np.dstack((img_out, fg[:, :, 3]))
498
+ ) # add alpha channel and replace nans
499
+ return img_out
500
+
501
+
502
+ def divide(fg: np.ndarray, bg: np.ndarray, opacity: float):
503
+ """Apply divide blending mode of a layer on an image.
504
+
505
+ See Also:
506
+ Find more information on
507
+ `Wikipedia <https://en.wikipedia.org/w/index.php?title=Blend_modes&oldid=747749280#Divide>`__.
508
+
509
+ Args:
510
+ fg(3-dimensional numpy array of floats (r/g/b/a) in range 0-255.0): Image to be blended upon
511
+ bg(3-dimensional numpy array of floats (r/g/b/a) in range 0.0-255.0): Layer to be blended with image
512
+ opacity(float): Desired opacity of layer for blending
513
+ disable_type_checks(bool): Whether type checks within the function should be disabled. Disabling the checks may
514
+ yield a slight performance improvement, but comes at the cost of user experience. If you are certain that
515
+ you are passing in the right arguments, you may set this argument to 'True'. Defaults to 'False'.
516
+
517
+ Returns:
518
+ 3-dimensional numpy array of floats (r/g/b/a) in range 0.0-255.0: Blended image
519
+
520
+ """
521
+
522
+ ratio = _compose_alpha(fg, bg, opacity)
523
+
524
+ comp = np.minimum(
525
+ (256.0 / 255.0 * fg[:, :, :3]) / (1.0 / 255.0 + bg[:, :, :3]),
526
+ 1.0,
527
+ )
528
+
529
+ ratio_rs = np.reshape(
530
+ np.repeat(ratio, 3), [comp.shape[0], comp.shape[1], comp.shape[2]]
531
+ )
532
+ img_out = comp * ratio_rs + fg[:, :, :3] * (1.0 - ratio_rs)
533
+ img_out = np.nan_to_num(
534
+ np.dstack((img_out, fg[:, :, 3]))
535
+ ) # add alpha channel and replace nans
536
+ return img_out
537
+
538
+
539
+ def overlay(fg: np.ndarray, bg: np.ndarray, opacity: float):
540
+ """Apply overlay blending mode of a layer on an image.
541
+
542
+ Note:
543
+ The implementation of this method was changed in version 2.0.0. Previously, it would be identical to the
544
+ soft light blending mode. Now, it resembles the implementation on Wikipedia. You can still use the soft light
545
+ blending mode if you are looking for backwards compatibility.
546
+
547
+ See Also:
548
+ Find more information on
549
+ `Wikipedia <https://en.wikipedia.org/w/index.php?title=Blend_modes&oldid=868545948#Overlay>`__.
550
+
551
+ Args:
552
+ fg(3-dimensional numpy array of floats (r/g/b/a) in range 0-255.0): Image to be blended upon
553
+ bg(3-dimensional numpy array of floats (r/g/b/a) in range 0.0-255.0): Layer to be blended with image
554
+ opacity(float): Desired opacity of layer for blending
555
+ disable_type_checks(bool): Whether type checks within the function should be disabled. Disabling the checks may
556
+ yield a slight performance improvement, but comes at the cost of user experience. If you are certain that
557
+ you are passing in the right arguments, you may set this argument to 'True'. Defaults to 'False'.
558
+
559
+ Returns:
560
+ 3-dimensional numpy array of floats (r/g/b/a) in range 0.0-255.0: Blended image
561
+
562
+ """
563
+
564
+ ratio = _compose_alpha(fg, bg, opacity)
565
+
566
+ comp = np.less(fg[:, :, :3], 0.5) * (
567
+ 2 * fg[:, :, :3] * bg[:, :, :3]
568
+ ) + np.greater_equal(fg[:, :, :3], 0.5) * (
569
+ 1 - (2 * (1 - fg[:, :, :3]) * (1 - bg[:, :, :3]))
570
+ )
571
+
572
+ ratio_rs = np.reshape(
573
+ np.repeat(ratio, 3), [comp.shape[0], comp.shape[1], comp.shape[2]]
574
+ )
575
+ img_out = comp * ratio_rs + fg[:, :, :3] * (1.0 - ratio_rs)
576
+ img_out = np.nan_to_num(
577
+ np.dstack((img_out, fg[:, :, 3]))
578
+ ) # add alpha channel and replace nans
579
+ return img_out
@@ -8,7 +8,7 @@ import numpy.ma as ma
8
8
  from mapchete import Timer
9
9
  from rasterio.plot import reshape_as_image, reshape_as_raster
10
10
 
11
- from mapchete_eo.image_operations.blend_modes import blending_functions
11
+ from mapchete_eo.image_operations import blend_functions
12
12
 
13
13
 
14
14
  logger = logging.getLogger(__name__)
@@ -55,14 +55,16 @@ def to_rgba(arr: np.ndarray) -> np.ndarray:
55
55
  def _blend_base(
56
56
  bg: np.ndarray, fg: np.ndarray, opacity: float, operation: Callable
57
57
  ) -> ma.MaskedArray:
58
- # generate RGBA output and run compositing
58
+ # generate RGBA output and run compositing and normalize by dividing by 255
59
59
  out_arr = reshape_as_raster(
60
- operation(
61
- reshape_as_image(to_rgba(bg)),
62
- reshape_as_image(to_rgba(fg)),
63
- opacity,
64
- disable_type_checks=True,
65
- ).astype(np.uint8)
60
+ (
61
+ operation(
62
+ reshape_as_image(to_rgba(bg) / 255),
63
+ reshape_as_image(to_rgba(fg) / 255),
64
+ opacity,
65
+ )
66
+ * 255
67
+ ).astype(np.uint8, copy=False)
66
68
  )
67
69
  # generate mask from alpha band
68
70
  out_mask = np.where(out_arr[3] == 0, True, False)
@@ -70,63 +72,63 @@ def _blend_base(
70
72
 
71
73
 
72
74
  def normal(bg: np.ndarray, fg: np.ndarray, opacity: float = 1) -> ma.MaskedArray:
73
- return _blend_base(bg, fg, opacity, blending_functions.normal)
75
+ return _blend_base(bg, fg, opacity, blend_functions.normal)
74
76
 
75
77
 
76
78
  def soft_light(bg: np.ndarray, fg: np.ndarray, opacity: float = 1) -> ma.MaskedArray:
77
- return _blend_base(bg, fg, opacity, blending_functions.soft_light)
79
+ return _blend_base(bg, fg, opacity, blend_functions.soft_light)
78
80
 
79
81
 
80
82
  def lighten_only(bg: np.ndarray, fg: np.ndarray, opacity: float = 1) -> ma.MaskedArray:
81
- return _blend_base(bg, fg, opacity, blending_functions.lighten_only)
83
+ return _blend_base(bg, fg, opacity, blend_functions.lighten_only)
82
84
 
83
85
 
84
86
  def screen(bg: np.ndarray, fg: np.ndarray, opacity: float = 1) -> ma.MaskedArray:
85
- return _blend_base(bg, fg, opacity, blending_functions.screen)
87
+ return _blend_base(bg, fg, opacity, blend_functions.screen)
86
88
 
87
89
 
88
90
  def dodge(bg: np.ndarray, fg: np.ndarray, opacity: float = 1) -> ma.MaskedArray:
89
- return _blend_base(bg, fg, opacity, blending_functions.dodge)
91
+ return _blend_base(bg, fg, opacity, blend_functions.dodge)
90
92
 
91
93
 
92
94
  def addition(bg: np.ndarray, fg: np.ndarray, opacity: float = 1) -> ma.MaskedArray:
93
- return _blend_base(bg, fg, opacity, blending_functions.addition)
95
+ return _blend_base(bg, fg, opacity, blend_functions.addition)
94
96
 
95
97
 
96
98
  def darken_only(bg: np.ndarray, fg: np.ndarray, opacity: float = 1) -> ma.MaskedArray:
97
- return _blend_base(bg, fg, opacity, blending_functions.darken_only)
99
+ return _blend_base(bg, fg, opacity, blend_functions.darken_only)
98
100
 
99
101
 
100
102
  def multiply(bg: np.ndarray, fg: np.ndarray, opacity: float = 1) -> ma.MaskedArray:
101
- return _blend_base(bg, fg, opacity, blending_functions.multiply)
103
+ return _blend_base(bg, fg, opacity, blend_functions.multiply)
102
104
 
103
105
 
104
106
  def hard_light(bg: np.ndarray, fg: np.ndarray, opacity: float = 1) -> ma.MaskedArray:
105
- return _blend_base(bg, fg, opacity, blending_functions.hard_light)
107
+ return _blend_base(bg, fg, opacity, blend_functions.hard_light)
106
108
 
107
109
 
108
110
  def difference(bg: np.ndarray, fg: np.ndarray, opacity: float = 1) -> ma.MaskedArray:
109
- return _blend_base(bg, fg, opacity, blending_functions.difference)
111
+ return _blend_base(bg, fg, opacity, blend_functions.difference)
110
112
 
111
113
 
112
114
  def subtract(bg: np.ndarray, fg: np.ndarray, opacity: float = 1) -> ma.MaskedArray:
113
- return _blend_base(bg, fg, opacity, blending_functions.subtract)
115
+ return _blend_base(bg, fg, opacity, blend_functions.subtract)
114
116
 
115
117
 
116
118
  def grain_extract(bg: np.ndarray, fg: np.ndarray, opacity: float = 1) -> ma.MaskedArray:
117
- return _blend_base(bg, fg, opacity, blending_functions.grain_extract)
119
+ return _blend_base(bg, fg, opacity, blend_functions.grain_extract)
118
120
 
119
121
 
120
122
  def grain_merge(bg: np.ndarray, fg: np.ndarray, opacity: float = 1) -> ma.MaskedArray:
121
- return _blend_base(bg, fg, opacity, blending_functions.grain_merge)
123
+ return _blend_base(bg, fg, opacity, blend_functions.grain_merge)
122
124
 
123
125
 
124
126
  def divide(bg: np.ndarray, fg: np.ndarray, opacity: float = 1) -> ma.MaskedArray:
125
- return _blend_base(bg, fg, opacity, blending_functions.divide)
127
+ return _blend_base(bg, fg, opacity, blend_functions.divide)
126
128
 
127
129
 
128
130
  def overlay(bg: np.ndarray, fg: np.ndarray, opacity: float = 1) -> ma.MaskedArray:
129
- return _blend_base(bg, fg, opacity, blending_functions.overlay)
131
+ return _blend_base(bg, fg, opacity, blend_functions.overlay)
130
132
 
131
133
 
132
134
  METHODS = {
@@ -212,11 +214,14 @@ def fuzzy_alpha_mask(
212
214
  gradient_position=GradientPosition.outside,
213
215
  ) -> np.ndarray:
214
216
  """Return an RGBA array with a fuzzy alpha mask."""
215
- gradient_position = (
216
- GradientPosition[gradient_position]
217
- if isinstance(gradient_position, str)
218
- else gradient_position
219
- )
217
+ try:
218
+ gradient_position = (
219
+ GradientPosition[gradient_position]
220
+ if isinstance(gradient_position, str)
221
+ else gradient_position
222
+ )
223
+ except KeyError:
224
+ raise ValueError(f"unknown gradient_position: {gradient_position}")
220
225
 
221
226
  if arr.shape[0] != 3:
222
227
  raise TypeError("input array must have exactly three bands")
@@ -6,7 +6,7 @@ import numpy.ma as ma
6
6
  from mapchete import Timer
7
7
  from rasterio.features import rasterize, shapes
8
8
  from rasterio.fill import fillnodata as rio_fillnodata
9
- from scipy.ndimage.filters import convolve
9
+ from scipy.ndimage import convolve
10
10
  from shapely.geometry import shape
11
11
 
12
12
  logger = logging.getLogger(__name__)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mapchete-eo
3
- Version: 2025.8.2
3
+ Version: 2025.10.0
4
4
  Summary: mapchete EO data reader
5
5
  Project-URL: Homepage, https://gitlab.eox.at/maps/mapchete_eo
6
6
  Author-email: Joachim Ungar <joachim.ungar@eox.at>, Petr Sevcik <petr.sevcik@eox.at>
@@ -12,11 +12,12 @@ Classifier: License :: OSI Approved :: MIT License
12
12
  Classifier: Programming Language :: Python :: 3.10
13
13
  Classifier: Programming Language :: Python :: 3.11
14
14
  Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
15
16
  Classifier: Topic :: Scientific/Engineering :: GIS
16
17
  Requires-Dist: click
17
18
  Requires-Dist: croniter
18
19
  Requires-Dist: lxml
19
- Requires-Dist: mapchete[complete]>=2025.6.0
20
+ Requires-Dist: mapchete[complete]>=2025.10.0
20
21
  Requires-Dist: opencv-python-headless
21
22
  Requires-Dist: pillow
22
23
  Requires-Dist: pydantic
@@ -43,6 +44,9 @@ Earth Observation–specific driver extensions for `Mapchete <https://github.com
43
44
  .. image:: https://img.shields.io/pypi/v/mapchete-eo.svg
44
45
  :target: https://pypi.org/project/mapchete-eo/
45
46
 
47
+ .. image:: https://img.shields.io/conda/v/conda-forge/mapchete-eo
48
+ :target: https://anaconda.org/conda-forge/mapchete-eo
49
+
46
50
  .. image:: https://img.shields.io/pypi/l/mapchete-eo.svg
47
51
  :target: https://github.com/mapchete/mapchete-eo/blob/main/LICENSE
48
52
 
@@ -1,5 +1,5 @@
1
- mapchete_eo/__init__.py,sha256=proat4RKhxlrOiUazmDvmlHqo8wGe3Jrpdv-JeH4vvs,25
2
- mapchete_eo/base.py,sha256=-bwBYkKCthr7Q48Y10KbExw7i9sXY8lc0UGo-fe08WA,18907
1
+ mapchete_eo/__init__.py,sha256=HtoUGacYQ9lSC7EssjuZ7xIyD0DlTBcOEDAUU3Qep04,26
2
+ mapchete_eo/base.py,sha256=Ka1HDqKeeTZYgnhW2LFpgR1AaI9buXCrbNztXWkxU78,19164
3
3
  mapchete_eo/blacklist.txt,sha256=6KhBY0jNjXgRpKRvKJoOTswvNUoP56IrIcNeCYnd2qo,17471
4
4
  mapchete_eo/eostac.py,sha256=5K08Mr4wm-VOXTEvTB6DQHX7rbFLYw8mjW1wlpPltC0,574
5
5
  mapchete_eo/exceptions.py,sha256=ul7_9o6_SfJXQPsDOQqRBhh09xv2t4AwHl6YJ7yWN5s,2150
@@ -30,15 +30,14 @@ mapchete_eo/cli/s2_rgb.py,sha256=nzK5m4s0m2VOXgtit0uUOOhNJ14wquikWmbEQON5WJw,374
30
30
  mapchete_eo/cli/s2_verify.py,sha256=atfJA5luHZjDSqiM2udaufmnQ2B4uW2Tur_OgtYLa2g,4080
31
31
  mapchete_eo/cli/static_catalog.py,sha256=zq20fc0phjfjWMjSao-L6VVI-o2DPrMapvDmy6i30YQ,4291
32
32
  mapchete_eo/image_operations/__init__.py,sha256=c36Vs53mT5UQR9Iwd2Ln_vnT4K_Ni8g-K7J9Fl6UsJo,432
33
+ mapchete_eo/image_operations/blend_functions.py,sha256=YaKLOEapMbsUR-_mkBatXwPcmim8GKpIAVua_GOdaHs,24131
33
34
  mapchete_eo/image_operations/color_correction.py,sha256=vMXxcdyH3qOJDSKcXGU3FgXWIcgQxMy-QVe2fGQTbdk,4447
34
- mapchete_eo/image_operations/compositing.py,sha256=LhTx6K7TY7KwiAY7vBznbno5B7w5TEUvkRn1YFEA16U,8306
35
+ mapchete_eo/image_operations/compositing.py,sha256=K0bGCZqJuS39IBsjrRiJz5zowItI6RsiA-EBUVlbboo,8449
35
36
  mapchete_eo/image_operations/dtype_scale.py,sha256=_CCdTBkMLDkZdjV_PsVEkqLWT6jWuTJPeMzCcRwBOns,1243
36
- mapchete_eo/image_operations/fillnodata.py,sha256=IYCQkPlH-K6ri0G_-Xp8JJzvWz6HhBiA1DolWeGJVyc,4864
37
+ mapchete_eo/image_operations/fillnodata.py,sha256=Xdd-kacfo7VKqX7HRP0hqnnvV5OwNqnHLRTW64QhG24,4856
37
38
  mapchete_eo/image_operations/filters.py,sha256=sa_Igv0zMIANHLEALVi8mxohZMoSrdQc6XVjXDYwzy8,7201
38
39
  mapchete_eo/image_operations/linear_normalization.py,sha256=-eQX3WLCUYWUv-um3s1u33rgjAwCz84Ht8gcPsMWtJE,2468
39
40
  mapchete_eo/image_operations/sigmoidal.py,sha256=IKU8F89HhQJWGUVmIrnkuhqzn_ztlGrTf8xXZaVQWzU,3575
40
- mapchete_eo/image_operations/blend_modes/blending_functions.py,sha256=hv3oWFwzha0aAfG0X9CTKc1Y4rfIC6J4v_xVSGu4XAE,5955
41
- mapchete_eo/image_operations/blend_modes/type_checks.py,sha256=KVJyE1jLsZ4qDMlylWMh40X9POcJy_e5aZYhhqgXH88,3445
42
41
  mapchete_eo/io/__init__.py,sha256=1-1g4cESZZREvyumEUABZhDwgVuSxtxdqbNlLuVKlow,950
43
42
  mapchete_eo/io/assets.py,sha256=8OrqvrCzJOY0pExh_vKRL7Bz_RGk6M_LfayQqxHo8ag,17014
44
43
  mapchete_eo/io/items.py,sha256=FDDMRtFvNNL3Iwf0xiaSI8G3s-54vhqfYKo7xzCQq-w,5657
@@ -82,8 +81,8 @@ mapchete_eo/search/s2_mgrs.py,sha256=5LWl9c7WzFvXODr9f8m37cv-8yyf-LvnN0-TSSPcXKM
82
81
  mapchete_eo/search/stac_search.py,sha256=YnISHpdvMJfrY_wfM1dHOtHYqgsbXsYLMYrRdJlAZTo,9551
83
82
  mapchete_eo/search/stac_static.py,sha256=P1C2OC33UopV80xIvFeL-HbOuaxxe7qsPDdnkU8IXcA,8613
84
83
  mapchete_eo/search/utm_search.py,sha256=wKrG2KwqL2fjSrkDUTZuRayiVAR27BtWpX-PFab0Dng,9646
85
- mapchete_eo-2025.8.2.dist-info/METADATA,sha256=t5fNQs5buszhui2sjovYtLu6gaaI8yzELrN58w4IGxY,3061
86
- mapchete_eo-2025.8.2.dist-info/WHEEL,sha256=tkmg4JIqwd9H8mL30xA7crRmoStyCtGp0VWshokd1Jc,105
87
- mapchete_eo-2025.8.2.dist-info/entry_points.txt,sha256=ewk6R4FGdAclOnUpikhlPZGWI40MWeksVIIwu4jVebk,324
88
- mapchete_eo-2025.8.2.dist-info/licenses/LICENSE,sha256=TC5JwvBnFrUgsSQSCDFPc3cqlbth2N0q8MWrhY1EVd0,1089
89
- mapchete_eo-2025.8.2.dist-info/RECORD,,
84
+ mapchete_eo-2025.10.0.dist-info/METADATA,sha256=IUBh6j50RcAjOSWYsJ5UPME5ahbnaFAf9H0YtLK-1sM,3237
85
+ mapchete_eo-2025.10.0.dist-info/WHEEL,sha256=tkmg4JIqwd9H8mL30xA7crRmoStyCtGp0VWshokd1Jc,105
86
+ mapchete_eo-2025.10.0.dist-info/entry_points.txt,sha256=ewk6R4FGdAclOnUpikhlPZGWI40MWeksVIIwu4jVebk,324
87
+ mapchete_eo-2025.10.0.dist-info/licenses/LICENSE,sha256=TC5JwvBnFrUgsSQSCDFPc3cqlbth2N0q8MWrhY1EVd0,1089
88
+ mapchete_eo-2025.10.0.dist-info/RECORD,,
@@ -1,198 +0,0 @@
1
- """
2
-
3
- Original LICENSE:
4
-
5
- MIT License
6
-
7
- Copyright (c) 2016 Florian Roscheck
8
-
9
- Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
10
- documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
11
- rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
12
- persons to whom the Software is furnished to do so, subject to the following conditions:
13
-
14
- The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
15
- Software.
16
-
17
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
18
- INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19
- IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20
- WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
21
- OR OTHER DEALINGS IN THE SOFTWARE.
22
-
23
- Overview
24
- --------
25
-
26
- .. currentmodule:: blend_modes.blending_functions
27
-
28
- .. autosummary::
29
- :nosignatures:
30
-
31
- addition
32
- darken_only
33
- difference
34
- divide
35
- dodge
36
- grain_extract
37
- grain_merge
38
- hard_light
39
- lighten_only
40
- multiply
41
- normal
42
- overlay
43
- screen
44
- soft_light
45
- subtract
46
- """
47
-
48
- import numpy as np
49
- from typing import Callable
50
- from mapchete_eo.image_operations.blend_modes.type_checks import (
51
- assert_image_format,
52
- assert_opacity,
53
- )
54
-
55
-
56
- class BlendBase:
57
- def __init__(
58
- self,
59
- opacity=1.0,
60
- disable_type_checks=False,
61
- dtype=np.float16,
62
- fcn_name="BlendBase",
63
- ):
64
- self.opacity = opacity
65
- self.disable_type_checks = disable_type_checks
66
- self.dtype = dtype
67
- self.fcn_name = fcn_name
68
-
69
- def _prepare(self, src: np.ndarray, dst: np.ndarray):
70
- if not self.disable_type_checks:
71
- assert_image_format(src, fcn_name=self.fcn_name, arg_name="src")
72
- assert_image_format(dst, fcn_name=self.fcn_name, arg_name="dst")
73
- assert_opacity(self.opacity, fcn_name=self.fcn_name)
74
- if src.dtype != self.dtype:
75
- src = src.astype(self.dtype)
76
- if dst.dtype != self.dtype:
77
- dst = dst.astype(self.dtype)
78
- return src, dst
79
-
80
- def blend(self, src: np.ndarray, dst: np.ndarray, blend_func: Callable):
81
- src, dst = self._prepare(src, dst)
82
- blended = blend_func(src, dst)
83
- result = (blended * self.opacity) + (dst * (1 - self.opacity))
84
- return np.clip(result, 0, 1).astype(self.dtype)
85
-
86
-
87
- def make_blend_function(blend_func: Callable):
88
- # This function returns a wrapper that uses a shared BlendBase instance
89
- base = BlendBase()
90
-
91
- def func(
92
- src: np.ndarray,
93
- dst: np.ndarray,
94
- opacity: float = 1.0,
95
- disable_type_checks: bool = False,
96
- dtype: np.dtype = np.float16,
97
- ) -> np.ndarray:
98
- # If parameters differ from base, create new BlendBase (rare)
99
- if (
100
- opacity != base.opacity
101
- or disable_type_checks != base.disable_type_checks
102
- or dtype != base.dtype
103
- ):
104
- base_local = BlendBase(opacity, disable_type_checks, dtype)
105
- return base_local.blend(src, dst, blend_func)
106
- return base.blend(src, dst, blend_func)
107
-
108
- return func
109
-
110
-
111
- normal = make_blend_function(lambda s, d: s)
112
- multiply = make_blend_function(lambda s, d: s * d)
113
- screen = make_blend_function(lambda s, d: 1 - (1 - s) * (1 - d))
114
- darken_only = make_blend_function(lambda s, d: np.minimum(s, d))
115
- lighten_only = make_blend_function(lambda s, d: np.maximum(s, d))
116
- difference = make_blend_function(lambda s, d: np.abs(d - s))
117
- subtract = make_blend_function(lambda s, d: np.clip(d - s, 0, 1))
118
-
119
-
120
- def divide_blend(s: np.ndarray, d: np.ndarray) -> np.ndarray:
121
- with np.errstate(divide="ignore", invalid="ignore"):
122
- res = np.true_divide(d, s)
123
- res[~np.isfinite(res)] = 0
124
- return np.clip(res, 0, 1)
125
-
126
-
127
- divide = make_blend_function(divide_blend)
128
-
129
-
130
- def grain_extract_blend(s: np.ndarray, d: np.ndarray) -> np.ndarray:
131
- return np.clip(d - s + 0.5, 0, 1)
132
-
133
-
134
- grain_extract = make_blend_function(grain_extract_blend)
135
-
136
-
137
- def grain_merge_blend(s: np.ndarray, d: np.ndarray) -> np.ndarray:
138
- return np.clip(d + s - 0.5, 0, 1)
139
-
140
-
141
- grain_merge = make_blend_function(grain_merge_blend)
142
-
143
-
144
- def overlay_blend(s: np.ndarray, d: np.ndarray) -> np.ndarray:
145
- mask = d <= 0.5
146
- result = np.empty_like(d)
147
- result[mask] = 2 * s[mask] * d[mask]
148
- result[~mask] = 1 - 2 * (1 - s[~mask]) * (1 - d[~mask])
149
- return np.clip(result, 0, 1)
150
-
151
-
152
- overlay = make_blend_function(overlay_blend)
153
-
154
-
155
- def hard_light_blend(s: np.ndarray, d: np.ndarray) -> np.ndarray:
156
- mask = s <= 0.5
157
- result = np.empty_like(d)
158
- result[mask] = 2 * s[mask] * d[mask]
159
- result[~mask] = 1 - 2 * (1 - s[~mask]) * (1 - d[~mask])
160
- return np.clip(result, 0, 1)
161
-
162
-
163
- hard_light = make_blend_function(hard_light_blend)
164
-
165
-
166
- def soft_light_blend(s: np.ndarray, d: np.ndarray) -> np.ndarray:
167
- result = (1 - 2 * s) * d**2 + 2 * s * d
168
- return np.clip(result, 0, 1)
169
-
170
-
171
- soft_light = make_blend_function(soft_light_blend)
172
-
173
-
174
- def dodge_blend(s: np.ndarray, d: np.ndarray) -> np.ndarray:
175
- with np.errstate(divide="ignore", invalid="ignore"):
176
- res = np.true_divide(d, 1 - s)
177
- res[~np.isfinite(res)] = 1
178
- return np.clip(res, 0, 1)
179
-
180
-
181
- dodge = make_blend_function(dodge_blend)
182
-
183
-
184
- def burn_blend(s: np.ndarray, d: np.ndarray) -> np.ndarray:
185
- with np.errstate(divide="ignore", invalid="ignore"):
186
- res = 1 - np.true_divide(1 - d, s)
187
- res[~np.isfinite(res)] = 0
188
- return np.clip(res, 0, 1)
189
-
190
-
191
- burn = make_blend_function(burn_blend)
192
-
193
-
194
- def addition_blend(s: np.ndarray, d: np.ndarray) -> np.ndarray:
195
- return np.clip(s + d, 0, 1)
196
-
197
-
198
- addition = make_blend_function(addition_blend)
@@ -1,99 +0,0 @@
1
- """This module includes functions to check if variable types match expected formats."""
2
-
3
- import numpy as np
4
-
5
-
6
- def assert_image_format(image, fcn_name: str, arg_name: str, force_alpha: bool = True):
7
- """Assert if image arguments have the expected format.
8
-
9
- Checks:
10
- - Image is a numpy array
11
- - Array is of floating-point type
12
- - Array is 3D (height x width x channels)
13
- - Array has the correct number of layers (3 or 4)
14
-
15
- Args:
16
- image: The image to be checked.
17
- fcn_name (str): Calling function name for display in error messages.
18
- arg_name (str): Relevant argument name for display in error messages.
19
- force_alpha (bool): Whether the image must include an alpha layer.
20
-
21
- Raises:
22
- TypeError: If type or shape are incorrect.
23
- """
24
- if not isinstance(image, np.ndarray):
25
- raise TypeError(
26
- f"\n[Invalid Type]\n"
27
- f"Function: {fcn_name}\n"
28
- f"Argument: {arg_name}\n"
29
- f"Expected: numpy.ndarray\n"
30
- f"Got: {type(image).__name__}\n"
31
- f'Hint: Pass a numpy.ndarray for "{arg_name}".'
32
- )
33
-
34
- if image.dtype.kind != "f":
35
- raise TypeError(
36
- f"\n[Invalid Data Type]\n"
37
- f"Function: {fcn_name}\n"
38
- f"Argument: {arg_name}\n"
39
- f'Expected dtype kind: "f" (floating-point)\n'
40
- f'Got dtype kind: "{image.dtype.kind}"\n'
41
- f"Hint: Convert the array to float, e.g., image.astype(float)."
42
- )
43
-
44
- if len(image.shape) != 3:
45
- raise TypeError(
46
- f"\n[Invalid Dimensions]\n"
47
- f"Function: {fcn_name}\n"
48
- f"Argument: {arg_name}\n"
49
- f"Expected: 3D array (height x width x channels)\n"
50
- f"Got: {len(image.shape)}D array\n"
51
- f"Hint: Ensure the array has three dimensions."
52
- )
53
-
54
- if force_alpha and image.shape[2] != 4:
55
- raise TypeError(
56
- f"\n[Invalid Channel Count]\n"
57
- f"Function: {fcn_name}\n"
58
- f"Argument: {arg_name}\n"
59
- f"Expected: 4 layers (R, G, B, Alpha)\n"
60
- f"Got: {image.shape[2]} layers\n"
61
- f"Hint: Include all four channels if force_alpha=True."
62
- )
63
-
64
-
65
- def assert_opacity(opacity, fcn_name: str, arg_name: str = "opacity"):
66
- """Assert if opacity has the expected format.
67
-
68
- Checks:
69
- - Opacity is float or int
70
- - Opacity is within 0.0 <= x <= 1.0
71
-
72
- Args:
73
- opacity: The opacity value to be checked.
74
- fcn_name (str): Calling function name for display in error messages.
75
- arg_name (str): Argument name for display in error messages.
76
-
77
- Raises:
78
- TypeError: If type is not float or int.
79
- ValueError: If opacity is out of range.
80
- """
81
- if not isinstance(opacity, (float, int)):
82
- raise TypeError(
83
- f"\n[Invalid Type]\n"
84
- f"Function: {fcn_name}\n"
85
- f"Argument: {arg_name}\n"
86
- f"Expected: float (or int)\n"
87
- f"Got: {type(opacity).__name__}\n"
88
- f"Hint: Pass a float between 0.0 and 1.0."
89
- )
90
-
91
- if not 0.0 <= opacity <= 1.0:
92
- raise ValueError(
93
- f"\n[Out of Range]\n"
94
- f"Function: {fcn_name}\n"
95
- f"Argument: {arg_name}\n"
96
- f"Expected: value in range 0.0 <= x <= 1.0\n"
97
- f"Got: {opacity}\n"
98
- f"Hint: Clamp or normalize the value to the valid range."
99
- )