wolfhece 2.2.3__py3-none-any.whl → 2.2.5__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.
wolfhece/lifewatch.py CHANGED
@@ -1,6 +1,20 @@
1
1
  from enum import Enum
2
2
  from PIL import Image
3
+ import numpy as np
4
+ from typing import Literal
5
+ import matplotlib.pyplot as plt
6
+ from matplotlib.colors import LinearSegmentedColormap, Normalize, ListedColormap, BoundaryNorm
3
7
 
8
+
9
+ from .PyTranslate import _
10
+ from .PyWMS import getLifeWatch
11
+ from .wolf_array import WolfArray, header_wolf, WOLF_ARRAY_FULL_INTEGER8, WOLF_ARRAY_FULL_INTEGER, wolfpalette
12
+
13
+
14
+ YEARS = [2006, 2010, 2015, 2018, 2019, 2020, 2021, 2022]
15
+ PIXEL_SIZE = 2 # in meters
16
+ MAP_LW = 'LW_ecotopes_lc_hr_raster'
17
+ MAX_PIXELS = 2048 # Maximum number of pixels in one direction (x or y)
4
18
  class LifeWatch_Legend(Enum):
5
19
  """
6
20
  https://www.mdpi.com/2306-5729/8/1/13
@@ -21,68 +35,389 @@ class LifeWatch_Legend(Enum):
21
35
  Small broadleaved trees (<3 m) and shrubs 56 LCC-2_1_2 & LCH-3_1_2 1.75
22
36
 
23
37
  Color Table (RGB with 256 entries) from tiff file
24
- 10: 10,10,210,255
25
- 11: 254,254,254,255
26
- 15: 215,215,215,255
27
- 20: 20,20,20,255
28
- 21: 210,0,0,255
29
- 30: 230,230,130,255
30
- 35: 235,170,0,255
31
- 40: 240,40,240,255
32
- 45: 145,245,245,255
38
+ 10: 10,10,210
39
+ 15: 210,210,210
40
+ 20: 20,20,20
41
+ 21: 210,0,0
42
+ 30: 230,230,130
43
+ 35: 235,170,0
44
+ 40: 240,40,240
45
+ 45: 145,245,245
46
+ 48: 148,118,0
47
+ 50: 50,150,50
48
+ 51: 0,151,151
49
+ 55: 55,255,0
50
+ 56: 156,255,156
51
+
33
52
  46: 246,146,246,255
34
- 48: 148,112,0,255
35
- 50: 50,150,50,255
36
- 51: 0,151,151,255
37
- 55: 55,255,0,255
38
- 56: 156,255,156,255
53
+ 11: 254,254,254,255
54
+ """
55
+ NODATA_WHITE = (0, (255, 255, 255), 'Nodata', '') # Outside Belgium/Wallonia
56
+
57
+ WATER = (10, (10, 10, 210), _("Water"), 'LCC-3')
58
+ NATURAL_MATERIAL_SURFACES = (15, (210, 210, 210), _("Natural Material Surfaces with less than 10% vegetation"), 'LCC-1_2')
59
+ ARTIFICIALLY_SEALED_GROUND_SURFACE = (20, (20, 20, 20), _("Artificially sealed ground surface"), 'LCC-1_1_1_3')
60
+ BUILDING = (21, (210, 0, 0), _("Building, specific structures and facilities"), 'LCC-1_1_1_1 || LCC-1_1_1_2')
61
+ HERBACEOUS_ROTATION = (30, (230, 230, 130), _("Herbaceous in rotation during the year (e.g., crops)"), 'LCC-2_2')
62
+ GRASSLAND_INTENSIVE_MANAGEMENT = (35, (235, 170, 0), _("Grassland with intensive management"), 'LCC-2_2')
63
+ GRASSLAND_SCRUB_BIOLOGICAL_INTEREST = (40, (240, 40, 240), _("Grassland and scrub of biological interest"), 'LCC-2_2')
64
+ INUNDATED_GRASSLAND_SCRUB_BIOLOGICAL_INTEREST = (45, (145, 245, 245), _("Inundated grassland and scrub of biological interest"), 'LCC-2_2 & LCH-4_4_2')
65
+ VEGETATION_RECENTLY_DISTURBED_AREA = (48, (148, 118, 0), _("Vegetation of recently disturbed area (e.g., clear cut)"), 'LCC-2_2 & LCH-3_8')
66
+ CONIFEROUS_TREES = (50, (50, 150, 50), _("Coniferous trees (≥3 m)"), 'LCC-2_1_1 & LCH-3_1_1')
67
+ SMALL_CONIFEROUS_TREES = (51, (0, 151, 151), _("Small coniferous trees (<3 m)"), 'LCC-2_1_2 & LCH-3_1_1')
68
+ BROADLEAVED_TREES = (55, (55, 255, 0), _("Broadleaved trees (≥3 m)"), 'LCC-2_1_1 & LCH-3_1_2')
69
+ SMALL_BROADLEAVED_TREES_SHRUBS = (56, (156, 255, 156), _("Small broadleaved trees (<3 m) and shrubs"), 'LCC-2_1_2 & LCH-3_1_2')
70
+
71
+ # NODATA11 = (11, (254,254,254,255)) # Not used
72
+ # NODATA46 = (46, (246,146,246,255)) # Not used
73
+ NODATA_BLACK = (100, (0, 0, 0), 'Nodata', '') # Outside Belgium/Wallonia
74
+
75
+ @classmethod
76
+ def reference(cls) -> str:
77
+ """
78
+ Return the reference
79
+ """
80
+ return 'https://www.mdpi.com/2306-5729/8/1/13'
81
+
82
+ @classmethod
83
+ def colors(cls, rgba:bool = False) -> list[tuple[int, int, int] | tuple[int, int, int, int]]:
84
+ """
85
+ Return the color of the class as a tuple (R, G, B)
86
+ """
87
+ if rgba:
88
+ return [leg.value[1] + (255,) for leg in cls]
89
+ else:
90
+ return [leg.value[1] for leg in cls]
91
+
92
+ @classmethod
93
+ def codes(cls):
94
+ """
95
+ Return the code of the class as integer
96
+ """
97
+ return [leg.value[0] for leg in cls]
98
+
99
+ @classmethod
100
+ def plot_legend(cls, figax = None):
101
+ """
102
+ Return the color of the class as a tuple (R, G, B)
103
+ """
104
+
105
+ colors = cls.colors()
106
+ codes = cls.codes()
107
+ texts = cls.texts()
108
+
109
+ if figax is None:
110
+ fig, ax = plt.subplots(figsize=(1, 6))
111
+ else:
112
+ fig, ax = figax
113
+
114
+ for i, color in enumerate(colors):
115
+ ax.fill([0,1,1,0,0],[i,i,i+1,i+1,i], color=np.array(color)/255.0)
116
+ ax.text(1.05, i + 0.5, f"{codes[i]}: {texts[i]}", va='center', fontsize=12)
117
+ ax.axis('off')
118
+
119
+ return fig, ax
120
+
121
+ @classmethod
122
+ def cmap(cls) -> plt.cm:
123
+ """
124
+ Return the colormap of the class
125
+ """
126
+ colors = np.asarray(cls.colors()).astype(float)/255.
127
+ codes = np.asarray(cls.codes()).astype(float)
128
+
129
+ normval = codes/100.
130
+
131
+ normval[0] = 0.
132
+ normval[-1] = 1.
133
+ segmentdata = {"red": np.column_stack([normval, colors[:, 0], colors[:, 0]]),
134
+ "green": np.column_stack([normval, colors[:, 1], colors[:, 1]]),
135
+ "blue": np.column_stack([normval, colors[:, 2], colors[:, 2]]),
136
+ "alpha": np.column_stack([normval, np.ones(len(colors)) * 255., np.ones(len(colors)) * 255.])}
137
+
138
+ return LinearSegmentedColormap('LifeWatch', segmentdata, 256)
139
+
140
+ @classmethod
141
+ def norm(cls) -> BoundaryNorm:
142
+ """
143
+ Return the norm of the class
144
+ """
145
+ return Normalize(0, 100)
146
+
147
+ @classmethod
148
+ def texts(cls):
149
+ """
150
+ Return the text of the class as a string
151
+ """
152
+ return [leg.value[2] for leg in cls]
153
+
154
+ @classmethod
155
+ def EAGLE_codes(cls):
156
+ """
157
+ Return the EAGLE code of the class as string
158
+ """
159
+ return [leg.value[3] for leg in cls]
160
+
161
+ @classmethod
162
+ def colors2codes(cls, array: np.ndarray | Image.Image,
163
+ aswolf:bool = True) -> np.ndarray:
164
+ """
165
+ Convert the color of the class to the code of the class
166
+ :param array: numpy array or PIL image
167
+ """
168
+
169
+ if isinstance(array, Image.Image):
170
+ mode = array.mode
171
+ if mode == 'RGB':
172
+ array = np.array(array)
173
+ elif mode == 'RGBA':
174
+ array = np.array(array)[:, :, :3]
175
+ elif mode == 'P':
176
+ array = np.array(array.convert('RGB'))
177
+ else:
178
+ raise ValueError(f"Unsupported image mode: {mode}")
179
+
180
+ elif isinstance(array, np.ndarray):
181
+ if array.ndim == 3 and array.shape[2] == 4:
182
+ array = array[:, :, :3]
183
+ elif array.ndim == 2:
184
+ pass
185
+ else:
186
+ raise ValueError(f"Unsupported array shape: {array.shape}")
187
+
188
+ unique_colors = np.unique(array.reshape(-1, array.shape[2]), axis=0)
189
+
190
+ # check if the colors are in the legend
191
+ for color in unique_colors:
192
+ if not any(np.array_equal(color, leg.value[1]) for leg in cls):
193
+ raise ValueError(f"Color {color} not found in legend")
194
+
195
+ # convert the color to the code
196
+ color_to_code = {leg.value[1]: leg.value[0] for leg in cls}
197
+ code_array = np.zeros(array.shape[:2], dtype=np.uint8)
198
+ for color, code in color_to_code.items():
199
+ mask = np.all(array == color, axis=-1)
200
+ code_array[mask] = code
201
+
202
+ if aswolf:
203
+ return np.asfortranarray(np.fliplr(code_array.T))
204
+ else:
205
+ return code_array
206
+
207
+ @classmethod
208
+ def codes2colors(cls, array: np.ndarray, asimage:bool = False) -> np.ndarray | Image.Image:
209
+ """
210
+ Convert the code of the class to the color of the class
211
+ :param array: numpy array or PIL image
212
+ """
213
+
214
+ if isinstance(array, np.ndarray):
215
+ if array.ndim == 2:
216
+ pass
217
+ else:
218
+ raise ValueError(f"Unsupported array shape: {array.shape}")
219
+ else:
220
+ raise ValueError(f"Unsupported array type: {type(array)}")
221
+
222
+ # check if the codes are in the legend
223
+ for code in np.unique(array):
224
+ if code not in cls.codes():
225
+ raise ValueError(f"Code {code} not found in legend")
226
+
227
+ # convert the code to the color
228
+ code_to_color = {leg.value[0]: leg.value[1] for leg in cls}
229
+ color_array = np.zeros((*array.shape, 3), dtype=np.uint8)
230
+ for code, color in code_to_color.items():
231
+ mask = (array == code)
232
+ color_array[mask] = color
233
+
234
+ if asimage:
235
+ color_array = Image.fromarray(color_array, mode='RGB')
236
+ color_array = color_array.convert('RGBA')
237
+ color_array.putalpha(255)
238
+ return color_array
239
+ else:
240
+ return color_array
241
+
242
+ return color_array
243
+
244
+ @classmethod
245
+ def getwolfpalette(cls) -> wolfpalette:
246
+ """
247
+ Get the wolf palette for the class
248
+ """
249
+ palette = wolfpalette()
250
+
251
+ palette.set_values_colors(cls.codes(), cls.colors())
252
+ palette.automatic = False
253
+ palette.interval_cst = True
254
+
255
+ return palette
256
+
257
+ def get_LifeWatch_bounds(year:int,
258
+ xmin:float,
259
+ ymin:float,
260
+ xmax:float,
261
+ ymax:float,
262
+ format:Literal['WolfArray',
263
+ 'NUMPY',
264
+ 'RGB',
265
+ 'RGBA',
266
+ 'Palette'] = 'WolfArray',
267
+ force_size:bool= True,
268
+ ) -> WolfArray | np.ndarray | Image.Image:
269
+
270
+
271
+ if year not in YEARS:
272
+ raise ValueError(f"Year {year} not found in LifeWatch years")
273
+
274
+ dx = xmax - xmin
275
+ dy = ymax - ymin
276
+
277
+ if force_size:
278
+ w = dx / PIXEL_SIZE
279
+ h = dy / PIXEL_SIZE
280
+
281
+ if w > MAX_PIXELS or h > MAX_PIXELS:
282
+ raise ValueError(f"Map size is too large: {w}x{h} pixels (max. {MAX_PIXELS}x{MAX_PIXELS})")
283
+ else:
284
+
285
+ if dx > dy:
286
+ w = MAX_PIXELS
287
+ h = int(dy * w / dx)
288
+ else:
289
+ h = MAX_PIXELS
290
+ w = int(dx * h / dy)
291
+
292
+ # Get the map from the WMS server
293
+ mybytes = getLifeWatch(f'{MAP_LW}_{year}',
294
+ xmin, ymin, # Lower left corner
295
+ xmax, ymax, # Upper right corner
296
+ w=MAX_PIXELS, h=None, # Width and height of the image [pixels]
297
+ tofile=False, # Must be False to get bytes --> save the image to ".\Lifewatch.png" if True
298
+ format='image/png; mode=8bit')
299
+
300
+ # Check if the map is empty
301
+ if mybytes is None:
302
+ raise ValueError(f"Error getting LifeWatch map for year {year} -- Check you internet connection or the resolution of the map (max. 2048x2048 pixels or 2mx2m)")
303
+
304
+ image = Image.open(mybytes) # Convert bytes to Image
305
+
306
+ if format in ['RGB', 'RGBA', 'Palette']:
307
+ if format == 'RGB':
308
+ image = image.convert('RGB')
309
+ elif format == 'RGBA':
310
+ image = image.convert('RGBA')
311
+ elif format == 'Palette':
312
+ image = image.convert('P')
313
+ else:
314
+ raise ValueError(f"Unsupported format: {format}")
315
+
316
+ return image, (xmin, ymin, xmax, ymax)
317
+
318
+ elif format == 'NUMPY':
319
+ return LifeWatch_Legend.colors2codes(image, aswolf=False), (xmin, ymin, xmax, ymax)
320
+
321
+ elif format in ['WolfArray', 'WOLF']:
322
+ h = header_wolf()
323
+ h.set_origin(xmin, ymin)
324
+ h.shape = image.size[0], image.size[1]
325
+ h.set_resolution((xmax-xmin)/h.nbx, (ymax-ymin)/h.nby)
326
+ wolf = WolfArray(srcheader=h, whichtype=WOLF_ARRAY_FULL_INTEGER)
327
+ wolf.array[:,:] = LifeWatch_Legend.colors2codes(image, aswolf=True).astype(int)
328
+ wolf.mask_data(0)
329
+ wolf.mypal = LifeWatch_Legend.getwolfpalette()
330
+
331
+ return wolf, (xmin, ymin, xmax, ymax)
332
+ else:
333
+ raise ValueError(f"Unsupported format: {format}")
334
+
335
+ def get_LifeWatch_Wallonia(year: int,
336
+ format:Literal['WolfArray',
337
+ 'NUMPY',
338
+ 'RGB',
339
+ 'RGBA',
340
+ 'Palette'] = 'WolfArray') -> WolfArray | np.ndarray | Image.Image:
341
+ """
342
+ Get the Wallonia LifeWatch map for the given year
343
+ :param year: year of the map
344
+ :param asimage: if True, return the image as PIL image, else return numpy array
345
+ :return: numpy array or PIL image
39
346
  """
40
- WATER = (10, (10, 210, 255))
41
- NATURAL_MATERIAL_SURFACES = (15, (215, 215, 215, 255))
42
- ARTIFICIALLY_SEALED_GROUND_SURFACE = (20, (20, 20, 20, 255))
43
- BUILDING = (21, (210, 0, 0, 255))
44
- HERBACEOUS_ROTATION = (30, (230, 230, 130, 255))
45
- GRASSLAND_INTENSIVE_MANAGEMENT = (35, (235, 170, 0, 255))
46
- GRASSLAND_SCRUB_BIOLOGICAL_INTEREST = (40, (240, 40, 240, 255))
47
- INUNDATED_GRASSLAND_SCRUB_BIOLOGICAL_INTEREST = (45, (145, 245, 245, 255))
48
- VEGETATION_RECENTLY_DISTURBED_AREA = (48, (148, 112, 0, 255))
49
- CONIFEROUS_TREES = (50, (50, 150, 50, 255))
50
- SMALL_CONIFEROUS_TREES = (51, (0, 151, 151, 255))
51
- BROADLEAVED_TREES = (55, (55, 255, 0, 255))
52
- SMALL_BROADLEAVED_TREES_SHRUBS = (56, (156, 255, 156, 255))
53
-
54
- NODATA11 = (11, (254,254,254,255)) # Not used
55
- NODATA46 = (46, (246,146,246,255)) # Not used
56
- NODATA100 = (100, (0, 0, 0, 255)) # Outside Belgium/Wallonia
57
-
58
- if __name__ == "__main__":
59
- import numpy as np
60
- n = 4
61
-
62
- DIR = r'E:\MODREC-Vesdre\vesdre-data\LifeWatch'
63
-
64
- # Tif file is very large, so we need to use PIL to open it
65
- Image.MAX_IMAGE_PIXELS = 15885900000
66
- img = Image.open(DIR + r'\lifewatch_LC2018_vx19_2mLB08cog.tif',)
67
- img = np.asarray(img)
68
-
69
- ij11 = np.where(img == 11)
70
- ij46 = np.where(img == 46)
71
-
72
- print(ij11[0].shape) # must be 0
73
- print(ij11[1].shape) # must be 0
74
-
75
- img = img[::n,:-img.shape[1]//2:n]
76
- print(np.unique(img))
77
-
78
- img = Image.open(DIR +r'\lifewatch_LC2022_vx20_2mLB08cog.tif',)
79
- img = np.asarray(img)
80
-
81
- ij11 = np.where(img == 11) # must be 0
82
- ij46 = np.where(img == 46) # must be 0
83
-
84
- print(ij11[0].shape)
85
- print(ij11[1].shape)
86
-
87
- img = img[::n,:-img.shape[1]//2:n]
88
- print(np.unique(img))
347
+
348
+ # Whole Wallonia
349
+ xmin = 40_000
350
+ xmax = 300_000
351
+ ymin = 10_000
352
+ ymax = 175_000
353
+
354
+ return get_LifeWatch_bounds(year, xmin, ymin, xmax, ymax, format, force_size=False)
355
+
356
+ def get_LifeWatch_center_width_height(year: int,
357
+ x: float,
358
+ y: float,
359
+ width: float = 2000,
360
+ height: float = 2000,
361
+ format:Literal['WolfArray',
362
+ 'NUMPY',
363
+ 'RGB',
364
+ 'RGBA',
365
+ 'Palette'] = 'WolfArray') -> WolfArray | np.ndarray | Image.Image:
366
+ """
367
+ Get the LifeWatch map for the given year and center
368
+ :param year: year of the map
369
+ :param x: x coordinate of the center
370
+ :param y: y coordinate of the center
371
+ :param asimage: if True, return the image as PIL image, else return numpy array
372
+ :return: numpy array or PIL image
373
+ """
374
+
375
+ # compute bounds
376
+ xmin = x - width / 2
377
+ xmax = x + width / 2
378
+ ymin = y - height / 2
379
+ ymax = y + height / 2
380
+
381
+ return get_LifeWatch_bounds(year, xmin, ymin, xmax, ymax, format)
382
+
383
+ def count_pixels(array:np.ndarray | WolfArray) -> dict[int, int]:
384
+ """
385
+ Count the number of pixels for each code in the array
386
+ :param array: numpy array or WolfArray
387
+ :return: dictionary with the code as key and the number of pixels as value
388
+ """
389
+ if isinstance(array, WolfArray):
390
+ array = array.array[~array.array.mask]
391
+ elif isinstance(array, np.ndarray):
392
+ pass
393
+ else:
394
+ raise ValueError(f"Unsupported array type: {type(array)}")
395
+
396
+ unique_codes, counts = np.unique(array, return_counts=True)
397
+
398
+ for code in unique_codes:
399
+ if code not in LifeWatch_Legend.codes():
400
+ raise ValueError(f"Code {code} not found in legend")
401
+
402
+ return {int(code): int(count) for code, count in zip(unique_codes, counts)}
403
+
404
+ def get_areas(array:np.ndarray | WolfArray) -> dict[int, float]:
405
+ """
406
+ Get the areas of each code in the array
407
+ :param array: numpy array or WolfArray
408
+ :return: dictionary with the code as key and the area in m² as value
409
+ """
410
+ if isinstance(array, WolfArray):
411
+ array = array.array[~array.array.mask]
412
+ elif isinstance(array, np.ndarray):
413
+ pass
414
+ else:
415
+ raise ValueError(f"Unsupported array type: {type(array)}")
416
+
417
+ unique_codes, counts = np.unique(array, return_counts=True)
418
+
419
+ for code in unique_codes:
420
+ if code not in LifeWatch_Legend.codes():
421
+ raise ValueError(f"Code {code} not found in legend")
422
+
423
+ return {int(code): float(count) * PIXEL_SIZE**2 for code, count in zip(unique_codes, counts)}
@@ -0,0 +1,61 @@
1
+ 15
2
+ 0.0
3
+ 255
4
+ 255
5
+ 255
6
+ 10.0
7
+ 10
8
+ 10
9
+ 210
10
+ 15.0
11
+ 210
12
+ 210
13
+ 210
14
+ 20.0
15
+ 20
16
+ 20
17
+ 20
18
+ 21.0
19
+ 210
20
+ 0
21
+ 0
22
+ 30.0
23
+ 230
24
+ 230
25
+ 130
26
+ 35.0
27
+ 235
28
+ 170
29
+ 0
30
+ 40.0
31
+ 240
32
+ 40
33
+ 240
34
+ 45.0
35
+ 145
36
+ 245
37
+ 245
38
+ 48.0
39
+ 148
40
+ 118
41
+ 0
42
+ 50.0
43
+ 50
44
+ 150
45
+ 50
46
+ 51.0
47
+ 0
48
+ 151
49
+ 151
50
+ 55.0
51
+ 55
52
+ 255
53
+ 0
54
+ 56.0
55
+ 156
56
+ 255
57
+ 156
58
+ 100.0
59
+ 0
60
+ 0
61
+ 0