wolfhece 2.2.27__py3-none-any.whl → 2.2.29__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.
@@ -0,0 +1,1054 @@
1
+ import logging
2
+ import numpy as np
3
+ import numpy.ma as ma
4
+ from pathlib import Path
5
+ import matplotlib.pyplot as plt
6
+ from enum import Enum
7
+ from scipy.ndimage import label, sum_labels, find_objects
8
+ import pymupdf as pdf
9
+ import wx
10
+ from tqdm import tqdm
11
+ from matplotlib import use, get_backend
12
+
13
+ from .common import A4_rect, rect_cm, list_to_html, list_to_html_aligned, get_rect_from_text
14
+ from .common import inches2cm, pts2cm, cm2pts, cm2inches, DefaultLayoutA4, NamedTemporaryFile, pt2inches, TemporaryDirectory
15
+ from ..wolf_array import WolfArray, header_wolf, vector, zone, Zones, wolfvertex as wv, wolfpalette
16
+ from ..PyTranslate import _
17
+ from .pdf import PDFViewer
18
+
19
+ class ArrayDifferenceLayout(DefaultLayoutA4):
20
+ """
21
+ Layout for comparing two arrays in a report.
22
+
23
+ 1 cadre pour la zone traitée avec photo de fond ign + contour vectoriel
24
+ 1 cadre avec zoom plus large min 250m
25
+ 1 cadre avec matrice ref + contour vectoriel
26
+ 1 cadre avec matrice à comparer + contour vectoriel
27
+ 1 cadre avec différence
28
+ 1 cadre avec valeurs de synthèse
29
+
30
+ 1 cadre avec histogramme
31
+ 1 cadre avec histogramme des différences
32
+ """
33
+
34
+ def __init__(self, title:str, filename = '', ox = 0, oy = 0, tx = 0, ty = 0, parent=None, is2D=True, idx = '', plotted = True, mapviewer=None, need_for_wx = False, bbox = None, find_minmax = True, shared = False, colors = None):
35
+ super().__init__(title, filename, ox, oy, tx, ty, parent, is2D, idx, plotted, mapviewer, need_for_wx, bbox, find_minmax, shared, colors)
36
+
37
+ useful = self.useful_part
38
+
39
+ width = useful.xmax - useful.xmin
40
+ height = useful.ymax - useful.ymin
41
+
42
+ self._hitograms = self.add_element_repeated(_("Histogram"), width=width, height=2.5,
43
+ first_x=useful.xmin, first_y=useful.ymax,
44
+ count_x=1, count_y=-2, padding=0.5)
45
+
46
+ self._arrays = self.add_element_repeated(_("Arrays"), width= (width-self.padding) / 2, height=5.5,
47
+ first_x=useful.xmin, first_y=self._hitograms.ymin - self.padding,
48
+ count_x=2, count_y=-3, padding=0.5)
49
+
50
+ class CompareArraysLayout(DefaultLayoutA4):
51
+
52
+ def __init__(self, title:str, filename = '', ox = 0, oy = 0, tx = 0, ty = 0, parent=None, is2D=True, idx = '', plotted = True, mapviewer=None, need_for_wx = False, bbox = None, find_minmax = True, shared = False, colors = None):
53
+ super().__init__(title, filename, ox, oy, tx, ty, parent, is2D, idx, plotted, mapviewer, need_for_wx, bbox, find_minmax, shared, colors)
54
+
55
+ useful = self.useful_part
56
+
57
+ width = useful.xmax - useful.xmin
58
+ height = useful.ymax - useful.ymin
59
+
60
+ self._summary = self.add_element_repeated(_("Summary"), width=(width-self.padding) / 2, height=3, first_x=useful.xmin, first_y=useful.ymax-3, count_x=2, count_y=1)
61
+
62
+ self._arrays = self.add_element_repeated(_("Arrays"), width= (width-self.padding) / 2, height=9., count_x=2, count_y=1, first_x=useful.xmin, first_y=14)
63
+ self._diff_rect = self.add_element(_("Difference"), width= width, height=11.5, x=useful.xmin, y=useful.ymin)
64
+
65
+
66
+ class CompareArraysLayout2(DefaultLayoutA4):
67
+
68
+ def __init__(self, title:str, filename = '', ox = 0, oy = 0, tx = 0, ty = 0, parent=None, is2D=True, idx = '', plotted = True, mapviewer=None, need_for_wx = False, bbox = None, find_minmax = True, shared = False, colors = None):
69
+ super().__init__(title, filename, ox, oy, tx, ty, parent, is2D, idx, plotted, mapviewer, need_for_wx, bbox, find_minmax, shared, colors)
70
+
71
+ useful = self.useful_part
72
+
73
+ width = useful.xmax - useful.xmin
74
+ height = useful.ymax - useful.ymin
75
+
76
+ self._summary = self.add_element_repeated(_("Histogram"), width=(width-self.padding) / 2, height=6., first_x=useful.xmin, first_y=useful.ymax-6, count_x=2, count_y=1)
77
+
78
+ self._arrays = self.add_element_repeated(_("Arrays"), width= (width-self.padding) / 2, height=6., count_x=2, count_y=1, first_x=useful.xmin, first_y=14)
79
+ self._diff_rect = self.add_element(_("Position"), width= width, height=11.5, x=useful.xmin, y=useful.ymin)
80
+
81
+
82
+ class ArrayDifference():
83
+ """
84
+ Class to manage the difference between two WolfArray objects.
85
+ """
86
+
87
+ def __init__(self, reference:WolfArray, to_compare:WolfArray, index:int, label:np.ndarray):
88
+
89
+ self._dpi = 600
90
+ self.default_size_hitograms = (12, 6)
91
+ self.default_size_arrays = (10, 10)
92
+ self._fontsize = 6
93
+
94
+ self.reference = reference
95
+ self.to_compare = to_compare
96
+
97
+ self.reference.updatepalette()
98
+ self.to_compare.mypal = self.reference.mypal
99
+
100
+ self.index = index
101
+ self.label = label
102
+
103
+ self._background = 'IGN'
104
+
105
+ self._contour = None
106
+ self._external_border = None
107
+
108
+ @property
109
+ def contour(self) -> vector:
110
+ """ Get the contour of the difference part. """
111
+
112
+ if self._contour is not None and isinstance(self._contour, vector):
113
+ return self._contour
114
+
115
+ ret = self.reference.suxsuy_contour(abs=True)
116
+ ret = ret[2]
117
+
118
+ ret.myprop.color = (0, 0, 255)
119
+ ret.myprop.width = 2
120
+
121
+ return ret
122
+
123
+ @property
124
+ def external_border(self) -> vector:
125
+ """
126
+ Get the bounds of the difference part.
127
+ """
128
+ if self._external_border is not None and isinstance(self._external_border, vector):
129
+ return self._external_border
130
+
131
+ ret = vector(name=_("External border"))
132
+ (xmin, xmax), (ymin, ymax) = self.reference.get_bounds()
133
+ ret.add_vertex(wv(xmin, ymin))
134
+ ret.add_vertex(wv(xmax, ymin))
135
+ ret.add_vertex(wv(xmax, ymax))
136
+ ret.add_vertex(wv(xmin, ymax))
137
+ ret.force_to_close()
138
+
139
+ ret.myprop.color = (255, 0, 0)
140
+ ret.myprop.width = 3
141
+
142
+ return ret
143
+
144
+ def __str__(self):
145
+
146
+ assert self.reference.nbnotnull == self.to_compare.nbnotnull, "The number of non-null cells in both arrays must be the same."
147
+
148
+ ret = self.reference.__str__() + '\n'
149
+
150
+ ret += _("Index : ") + str(self.index) + '\n'
151
+ ret += _("Number of cells : ") + str(self.reference.nbnotnull) + '\n'
152
+
153
+ return ret
154
+
155
+ @property
156
+ def _summary_text(self):
157
+ """
158
+ Generate a summary text for the report.
159
+ """
160
+ diff = self.difference.array.compressed()
161
+ text = [
162
+ _("Index: ") + str(self.index),
163
+ _("Number of cells: ") + str(self.reference.nbnotnull),
164
+ _('Resolution: ') + f"{self.reference.dx} m x {self.reference.dy} m",
165
+ _('Extent: ') + f"({self.reference.origx}, {self.reference.origy})" + f" - ({self.reference.origx + self.reference.nbx * self.reference.dx}, {self.reference.origy + self.reference.nby * self.reference.dy})",
166
+ _('Width x Height: ') + f"{self.reference.nbx * self.reference.dx} m x {self.reference.nby * self.reference.dy} m",
167
+ _('Excavation: ') + f"{np.sum(diff[diff < 0.]) * self.reference.dx * self.reference.dy:.3f} m³",
168
+ _('Deposit/Backfill: ') + f"{np.sum(diff[diff > 0.]) * self.reference.dx * self.reference.dy:.3f} m³",
169
+ _('Net volume: ') + f"{np.sum(diff) * self.reference.dx * self.reference.dy:.3f} m³",
170
+ ]
171
+ return text
172
+
173
+ def set_palette_distribute(self, minval:float, maxval:float, step:int=0):
174
+ """
175
+ Set the palette for both arrays.
176
+ """
177
+ self.reference.mypal.distribute_values(minval, maxval, step)
178
+
179
+ def set_palette(self, values:list[float], colors:list[tuple[int, int, int]]):
180
+ """
181
+ Set the palette for both arrays based on specific values.
182
+ """
183
+ self.reference.mypal.set_values_colors(values, colors)
184
+
185
+ def plot_position(self, figax:tuple[plt.Figure, plt.Axes]=None) -> tuple[plt.Figure, plt.Axes]:
186
+ """
187
+ Plot the reference array.
188
+ """
189
+ if figax is None:
190
+ figax = plt.subplots()
191
+
192
+ fig, ax = figax
193
+
194
+ old_mask = self.reference.array.mask.copy()
195
+ self.reference.array.mask[:,:] = True
196
+
197
+ if self._background.upper() == 'IGN' or self._background.upper() == 'NGI':
198
+ self.reference.plot_matplotlib(figax=figax, figsize = self.default_size_arrays,
199
+ first_mask_data=False, with_legend=False,
200
+ update_palette= False,
201
+ IGN= True,
202
+ cat = 'orthoimage_coverage',
203
+ )
204
+
205
+ elif self._background.upper() == 'WALONMAP':
206
+ self.reference.plot_matplotlib(figax=figax, figsize = self.default_size_arrays,
207
+ first_mask_data=False, with_legend=False,
208
+ update_palette= False,
209
+ Walonmap= True,
210
+ cat = 'IMAGERIE/ORTHO_2022_ETE',
211
+ )
212
+ else:
213
+ self.reference.plot_matplotlib(figax=figax, figsize = self.default_size_arrays,
214
+ first_mask_data=False, with_legend=False,
215
+ update_palette= False,
216
+ Walonmap= False,
217
+ )
218
+
219
+
220
+ self.reference.array.mask[:,:] = old_mask
221
+
222
+ self.external_border.plot_matplotlib(ax=ax)
223
+ self.contour.plot_matplotlib(ax=ax)
224
+
225
+ return fig, ax
226
+
227
+ def plot_position_scaled(self, scale = 4, figax:tuple[plt.Figure, plt.Axes]=None) -> tuple[plt.Figure, plt.Axes]:
228
+ """
229
+ Plot the reference array.
230
+ """
231
+ if figax is None:
232
+ figax = plt.subplots()
233
+
234
+ fig, ax = figax
235
+
236
+ h = self.reference.get_header()
237
+ width = h.nbx * h.dx
238
+ height = h.nby * h.dy
239
+
240
+ h.origx += -width * scale / 2
241
+ h.origy += -height *scale / 2
242
+ h.nbx = 1
243
+ h.nby = 1
244
+ h.dx = width *(scale + 1)
245
+ h.dy = height *(scale + 1)
246
+
247
+ new = WolfArray(srcheader=h)
248
+ new.array.mask[:,:] = True
249
+
250
+ if self._background.upper() == 'IGN' or self._background.upper() == 'NGI':
251
+ new.plot_matplotlib(figax=figax, figsize = self.default_size_arrays,
252
+ first_mask_data=False, with_legend=False,
253
+ update_palette= False,
254
+ IGN= True,
255
+ cat = 'orthoimage_coverage')
256
+ elif self._background.upper() == 'WALONMAP':
257
+ new.plot_matplotlib(figax=figax, figsize = self.default_size_arrays,
258
+ first_mask_data=False, with_legend=False,
259
+ update_palette= False,
260
+ Walonmap= True,
261
+ cat = 'IMAGERIE/ORTHO_2022_ETE')
262
+ else:
263
+ new.plot_matplotlib(figax=figax, figsize = self.default_size_arrays,
264
+ first_mask_data=False, with_legend=False,
265
+ update_palette= False,
266
+ Walonmap= False)
267
+
268
+ self.external_border.plot_matplotlib(ax=ax)
269
+
270
+ return fig, ax
271
+
272
+ def plot_reference(self, figax:tuple[plt.Figure, plt.Axes]=None) -> tuple[plt.Figure, plt.Axes]:
273
+ """
274
+ Plot the reference array.
275
+ """
276
+ if figax is None:
277
+ figax = plt.subplots()
278
+
279
+ fig, ax = figax
280
+
281
+ self.reference.plot_matplotlib(figax=figax, figsize = self.default_size_arrays, first_mask_data=False, with_legend=True, update_palette= False)
282
+ return fig, ax
283
+
284
+ def plot_to_compare(self, figax:tuple[plt.Figure, plt.Axes]=None) -> tuple[plt.Figure, plt.Axes]:
285
+ """
286
+ Plot the array to compare.
287
+ """
288
+ if figax is None:
289
+ figax = plt.subplots()
290
+
291
+ fig, ax = figax
292
+
293
+ self.to_compare.plot_matplotlib(figax=figax, figsize = self.default_size_arrays, first_mask_data=False, with_legend=True, update_palette= False)
294
+ return fig, ax
295
+
296
+ @property
297
+ def difference(self) -> WolfArray:
298
+ """
299
+ Get the difference between the two arrays.
300
+ """
301
+ if not isinstance(self.reference, WolfArray) or not isinstance(self.to_compare, WolfArray):
302
+ raise TypeError("Both inputs must be instances of WolfArray")
303
+
304
+ return self.to_compare - self.reference
305
+
306
+ def plot_difference(self, figax:tuple[plt.Figure, plt.Axes]=None) -> tuple[plt.Figure, plt.Axes]:
307
+ """
308
+ Plot the array to compare.
309
+ """
310
+ if figax is None:
311
+ figax = plt.subplots()
312
+
313
+ fig, ax = figax
314
+
315
+ pal = wolfpalette()
316
+ pal.default_difference3()
317
+
318
+ diff = self.difference
319
+ diff.mypal = pal
320
+ diff.plot_matplotlib(figax=figax, figsize = self.default_size_arrays, first_mask_data=False, with_legend=True, update_palette= False)
321
+ return fig, ax
322
+
323
+ def _plot_histogram_reference(self, figax:tuple[plt.Figure, plt.Axes]=None, density = True, alpha = 0.5, **kwargs) -> tuple[plt.Figure, plt.Axes]:
324
+ """
325
+ Plot histogram of the reference array.
326
+ """
327
+ if figax is None:
328
+ figax = plt.subplots()
329
+
330
+ fig, ax = figax
331
+
332
+ data = self.reference.array.compressed()
333
+ ax.hist(data, bins = min(100, int(len(data)/4)), density=density, alpha = alpha, **kwargs)
334
+ # ax.set_xlabel("Value")
335
+ # ax.set_ylabel("Frequency")
336
+ return fig, ax
337
+
338
+ def _plot_histogram_to_compare(self, figax:tuple[plt.Figure, plt.Axes]=None, density = True, alpha = 0.5, **kwargs) -> tuple[plt.Figure, plt.Axes]:
339
+ """
340
+ Plot histogram of the array to compare.
341
+ """
342
+ if figax is None:
343
+ figax = plt.subplots()
344
+
345
+ fig, ax = figax
346
+
347
+ data = self.to_compare.array.compressed()
348
+ ax.hist(data, bins= min(100, int(len(data)/4)), density=density, alpha = alpha, **kwargs)
349
+ # ax.set_xlabel("Value")
350
+ # ax.set_ylabel("Frequency")
351
+ return fig, ax
352
+
353
+ def plot_histograms(self, figax:tuple[plt.Figure, plt.Axes]=None, density = True, alpha = 0.5, **kwargs) -> tuple[plt.Figure, plt.Axes]:
354
+ """
355
+ Plot histograms of both arrays.
356
+ """
357
+ if figax is None:
358
+ figax = plt.subplots(1, 1, figsize=self.default_size_hitograms)
359
+
360
+ fig, ax = figax
361
+
362
+ self._plot_histogram_reference((fig, ax), density = density, alpha=alpha, **kwargs)
363
+ self._plot_histogram_to_compare((fig, ax), density = density, alpha=alpha, **kwargs)
364
+
365
+ # set font size of the labels
366
+ ax.tick_params(axis='both', which='major', labelsize=6)
367
+ for label in ax.get_xticklabels():
368
+ label.set_fontsize(self._fontsize)
369
+ for label in ax.get_yticklabels():
370
+ label.set_fontsize(self._fontsize)
371
+ # and gfor the label title
372
+ ax.set_xlabel(ax.get_xlabel(), fontsize=self._fontsize)
373
+ ax.set_ylabel(ax.get_ylabel(), fontsize=self._fontsize)
374
+
375
+ fig.tight_layout()
376
+ return fig, ax
377
+
378
+ def plot_histograms_difference(self, figax:tuple[plt.Figure, plt.Axes]=None, density = True, alpha = 1.0, **kwargs) -> tuple[plt.Figure, plt.Axes]:
379
+ """
380
+ Plot histogram of the difference between the two arrays.
381
+ """
382
+ if figax is None:
383
+ figax = plt.subplots(figsize=self.default_size_hitograms)
384
+
385
+ fig, ax = figax
386
+
387
+ difference_data = self.difference.array.compressed()
388
+ ax.hist(difference_data, bins= min(100, int(len(difference_data)/4)), density=density, alpha=alpha, **kwargs)
389
+
390
+ # ax.set_xlabel("Value")
391
+ # ax.set_ylabel("Frequency")
392
+
393
+ # set font size of the labels
394
+ ax.tick_params(axis='both', which='major', labelsize=6)
395
+ for label in ax.get_xticklabels():
396
+ label.set_fontsize(self._fontsize)
397
+ for label in ax.get_yticklabels():
398
+ label.set_fontsize(self._fontsize)
399
+ # and gfor the label title
400
+ ax.set_xlabel(ax.get_xlabel(), fontsize=self._fontsize)
401
+ ax.set_ylabel(ax.get_ylabel(), fontsize=self._fontsize)
402
+
403
+ return fig, ax
404
+
405
+ def _complete_report(self, layout:ArrayDifferenceLayout):
406
+
407
+ """
408
+ Complete the report with the arrays and histograms.
409
+ """
410
+ useful = layout.useful_part
411
+
412
+ # Plot reference array
413
+ key_fig = [('Histogram_0-0', self.plot_histograms),
414
+ ('Histogram_0-1', self.plot_histograms_difference),
415
+ ('Arrays_0-0', self.plot_position),
416
+ ('Arrays_1-0', self.plot_position_scaled),
417
+ ('Arrays_0-1', self.plot_reference),
418
+ ('Arrays_1-1', self.plot_to_compare),
419
+ ('Arrays_0-2', self.plot_difference),]
420
+
421
+ keys = layout.keys
422
+ for key, fig_routine in key_fig:
423
+ if key in keys:
424
+
425
+ rect = layout._layout[key]
426
+
427
+ fig, ax = fig_routine()
428
+
429
+ # set size to fit the rectangle
430
+ fig.set_size_inches(pt2inches(rect.width), pt2inches(rect.height))
431
+
432
+ if 'Histogram' in key:
433
+ fig.tight_layout()
434
+
435
+ # convert canvas to PNG and insert it into the PDF
436
+ temp_file = NamedTemporaryFile(delete=False, suffix='.png')
437
+ fig.savefig(temp_file, format='png', bbox_inches='tight', dpi=self._dpi)
438
+ layout._page.insert_image(layout._layout[key], filename = temp_file.name)
439
+ # delete the temporary file
440
+ temp_file.delete = True
441
+ temp_file.close()
442
+
443
+ # Force to delete fig
444
+ plt.close(fig)
445
+ else:
446
+ logging.warning(f"Key {key} not found in layout. Skipping plot.")
447
+
448
+ key = 'Arrays_1-2'
449
+ if key in keys:
450
+ text, css = list_to_html(self._summary_text, font_size='8pt')
451
+ layout._page.insert_htmlbox(layout._layout[key], text,
452
+ css=css)
453
+
454
+ def create_report(self, output_file: str | Path = None) -> Path:
455
+ """ Create a page report for the array difference. """
456
+
457
+ from time import sleep
458
+ if output_file is None:
459
+ output_file = Path(f"array_difference_{self.index}.pdf")
460
+
461
+ if output_file.exists():
462
+ logging.warning(f"Output file {output_file} already exists. It will be overwritten.")
463
+
464
+ layout = ArrayDifferenceLayout(f"Differences - Index n°{self.index}")
465
+ layout.create_report()
466
+ self._complete_report(layout)
467
+ layout.save_report(output_file)
468
+ sleep(0.2) # Ensure the file is saved before returning
469
+
470
+ return output_file
471
+
472
+ class CompareArrays:
473
+
474
+ def __init__(self, reference: WolfArray | str | Path, to_compare: WolfArray | str | Path):
475
+
476
+ self._dpi = 600
477
+ self.default_size_arrays = (10, 10)
478
+ self._fontsize = 6
479
+
480
+ if isinstance(reference, (str, Path)):
481
+ reference = WolfArray(reference)
482
+ if isinstance(to_compare, (str, Path)):
483
+ to_compare = WolfArray(to_compare)
484
+
485
+ if not reference.is_like(to_compare):
486
+ raise ValueError("Arrays are not compatible for comparison")
487
+
488
+ self.array_reference:WolfArray
489
+ self.array_to_compare:WolfArray
490
+ self.array_reference = reference
491
+ self.array_to_compare = to_compare
492
+
493
+ self.labeled_array: np.ndarray = None
494
+ self.num_features: int = 0
495
+ self.nb_cells: list = []
496
+
497
+ self.difference_parts:dict[int, ArrayDifference] = {}
498
+
499
+ self._pdf_path = None
500
+
501
+ self._background = 'IGN'
502
+
503
+ @property
504
+ def difference(self) -> WolfArray:
505
+
506
+ if not isinstance(self.array_reference, WolfArray) or not isinstance(self.array_to_compare, WolfArray):
507
+ raise TypeError("Both inputs must be instances of WolfArray")
508
+
509
+ return self.array_to_compare - self.array_reference
510
+
511
+ def get_zones(self):
512
+ """
513
+ Get a Zones object containing the differences.
514
+ """
515
+
516
+ ret_zones = Zones()
517
+ exterior = zone(name=_("External border"))
518
+ contours = zone(name=_("Contours"))
519
+
520
+ ret_zones.add_zone(exterior, forceparent=True)
521
+ ret_zones.add_zone(contours, forceparent=True)
522
+
523
+ for diff in self.difference_parts.values():
524
+ exterior.add_vector(diff.external_border, forceparent=True)
525
+ contours.add_vector(diff.contour, forceparent=True)
526
+
527
+ return ret_zones
528
+
529
+ def plot_position(self, figax:tuple[plt.Figure, plt.Axes]=None) -> tuple[plt.Figure, plt.Axes]:
530
+ """
531
+ Plot the reference array with a background.
532
+ """
533
+ if figax is None:
534
+ figax = plt.subplots()
535
+
536
+ fig, ax = figax
537
+
538
+ h = self.array_reference.get_header()
539
+ width = h.nbx * h.dx
540
+ height = h.nby * h.dy
541
+ h.dx = width
542
+ h.dy = height
543
+ h.nbx = 1
544
+ h.nby = 1
545
+
546
+ new = WolfArray(srcheader=h)
547
+ new.array.mask[:,:] = True
548
+
549
+ if self._background.upper() == 'IGN' or self._background.upper() == 'NGI':
550
+
551
+ new.plot_matplotlib(figax=figax, figsize = self.default_size_arrays,
552
+ first_mask_data=False, with_legend=False,
553
+ update_palette= False,
554
+ IGN= True,
555
+ cat = 'orthoimage_coverage',
556
+ )
557
+ elif self._background.upper() == 'WALONMAP':
558
+ new.plot_matplotlib(figax=figax, figsize = self.default_size_arrays,
559
+ first_mask_data=False, with_legend=False,
560
+ update_palette= False,
561
+ Walonmap= True,
562
+ cat = 'IMAGERIE/ORTHO_2022_ETE',
563
+ )
564
+ else:
565
+ new.plot_matplotlib(figax=figax, figsize = self.default_size_arrays,
566
+ first_mask_data=False, with_legend=False,
567
+ update_palette= False,
568
+ Walonmap= False,
569
+ )
570
+ return fig, ax
571
+
572
+ def plot_cartoweb(self, figax:tuple[plt.Figure, plt.Axes]=None) -> tuple[plt.Figure, plt.Axes]:
573
+ """
574
+ Plot the reference array with a background.
575
+ """
576
+ if figax is None:
577
+ figax = plt.subplots()
578
+
579
+ fig, ax = figax
580
+
581
+ h = self.array_reference.get_header()
582
+ width = h.nbx * h.dx
583
+ height = h.nby * h.dy
584
+ h.dx = width
585
+ h.dy = height
586
+ h.nbx = 1
587
+ h.nby = 1
588
+
589
+ new = WolfArray(srcheader=h)
590
+ new.array.mask[:,:] = True
591
+
592
+ if self._background.upper() == 'IGN' or self._background.upper() == 'NGI':
593
+ new.plot_matplotlib(figax=figax, figsize = self.default_size_arrays,
594
+ first_mask_data=False, with_legend=False,
595
+ update_palette= False,
596
+ Cartoweb= True,
597
+ cat = 'overlay',
598
+ )
599
+ else:
600
+ new.plot_matplotlib(figax=figax, figsize = self.default_size_arrays,
601
+ first_mask_data=False, with_legend=False,
602
+ update_palette= False,
603
+ Cartoweb= False,
604
+ cat = 'overlay',
605
+ )
606
+ return fig, ax
607
+
608
+ def plot_topo_grey(self, figax:tuple[plt.Figure, plt.Axes]=None) -> tuple[plt.Figure, plt.Axes]:
609
+ """
610
+ Plot the reference array with a background.
611
+ """
612
+ if figax is None:
613
+ figax = plt.subplots()
614
+
615
+ fig, ax = figax
616
+
617
+ h = self.array_reference.get_header()
618
+ width = h.nbx * h.dx
619
+ height = h.nby * h.dy
620
+ h.dx = width
621
+ h.dy = height
622
+ h.nbx = 1
623
+ h.nby = 1
624
+
625
+ new = WolfArray(srcheader=h)
626
+ new.array.mask[:,:] = True
627
+
628
+ if self._background.upper() == 'IGN' or self._background.upper() == 'NGI':
629
+ new.plot_matplotlib(figax=figax, figsize = self.default_size_arrays,
630
+ first_mask_data=False, with_legend=False,
631
+ update_palette= False,
632
+ Cartoweb= True,
633
+ cat = 'topo_grey',
634
+ )
635
+ else:
636
+ new.plot_matplotlib(figax=figax, figsize = self.default_size_arrays,
637
+ first_mask_data=False, with_legend=False,
638
+ update_palette= False,
639
+ Cartoweb= False,
640
+ cat = 'topo_grey',
641
+ )
642
+ return fig, ax
643
+
644
+ def plot_reference(self, figax:tuple[plt.Figure, plt.Axes]=None) -> tuple[plt.Figure, plt.Axes]:
645
+ """
646
+ Plot the reference array.
647
+ """
648
+ if figax is None:
649
+ figax = plt.subplots()
650
+
651
+ fig, ax = figax
652
+
653
+ self.array_reference.plot_matplotlib(figax=figax, figsize = self.default_size_arrays, first_mask_data=False, with_legend=True, update_palette= False)
654
+
655
+ for diff in tqdm(self.difference_parts.values(), desc="Plotting external borders"):
656
+ diff.external_border.plot_matplotlib(ax=ax)
657
+
658
+ return fig, ax
659
+
660
+ def plot_to_compare(self, figax:tuple[plt.Figure, plt.Axes]=None) -> tuple[plt.Figure, plt.Axes]:
661
+ """
662
+ Plot the array to compare.
663
+ """
664
+ if figax is None:
665
+ figax = plt.subplots()
666
+
667
+ fig, ax = figax
668
+
669
+ self.array_to_compare.plot_matplotlib(figax=figax, figsize = self.default_size_arrays, first_mask_data=False, with_legend=True, update_palette= False)
670
+
671
+ for diff in tqdm(self.difference_parts.values(), desc="Plotting contours"):
672
+ diff.contour.plot_matplotlib(ax=ax)
673
+
674
+ return fig, ax
675
+
676
+ def plot_difference(self, figax:tuple[plt.Figure, plt.Axes]=None) -> tuple[plt.Figure, plt.Axes]:
677
+ """
678
+ Plot the difference between the two arrays.
679
+ """
680
+ if figax is None:
681
+ figax = plt.subplots()
682
+
683
+ fig, ax = figax
684
+
685
+ pal = wolfpalette()
686
+ pal.default_difference3()
687
+
688
+ diff = self.difference
689
+ diff.mypal = pal
690
+ diff.plot_matplotlib(figax=figax, figsize = self.default_size_arrays, first_mask_data=False, with_legend=True, update_palette= False)
691
+ return fig, ax
692
+
693
+ def localize_differences(self, threshold: float = 0.0,
694
+ ignored_patche_area:float = 1.) -> np.ndarray:
695
+ """ Localize the differences between the two arrays and label them.
696
+
697
+ :param threshold: The threshold value to consider a difference significant.
698
+ :param ignored_patche_area: The area of patches to ignore (in m²).
699
+ """
700
+
701
+ assert threshold >= 0, "Threshold must be a non-negative value."
702
+
703
+ labeled_array = self.difference.array.data.copy()
704
+ labeled_array[self.array_reference.array.mask] = 0
705
+
706
+ # apply threshold
707
+ labeled_array[np.abs(labeled_array) < threshold] = 0
708
+
709
+ self.labeled_array, self.num_features = label(labeled_array)
710
+
711
+ self.nb_cells = []
712
+
713
+ self.nb_cells = list(sum_labels(np.ones(self.labeled_array.shape, dtype=np.int32), self.labeled_array, range(1, self.num_features+1)))
714
+ self.nb_cells = [[self.nb_cells[j], j+1] for j in range(0, self.num_features)]
715
+
716
+ self.nb_cells.sort(key=lambda x: x[0], reverse=True)
717
+
718
+ # find features where nb_cells is lower than ignored_patche_area / (dx * dy)
719
+ ignored_patche_cells = int(ignored_patche_area / (self.array_reference.dx * self.array_reference.dy))
720
+ self.last_features = self.num_features
721
+ for idx, (nb_cell, idx_feature) in enumerate(self.nb_cells):
722
+ if nb_cell <= ignored_patche_cells:
723
+ self.last_features = idx
724
+ break
725
+
726
+ all_slices = find_objects(self.labeled_array)
727
+
728
+ logging.info(f"Total number of features found: {self.last_features}")
729
+
730
+ # find xmin, ymin, xmax, ymax for each feature
731
+ for idx_feature, slices in tqdm(zip(range(1, self.num_features+1), all_slices), desc="Processing features", unit="feature"):
732
+ mask = self.labeled_array[slices] == idx_feature
733
+ nb_in_patch = np.count_nonzero(mask)
734
+
735
+ if nb_in_patch <= ignored_patche_cells:
736
+ logging.debug(f"Feature {idx_feature} has too few cells ({np.count_nonzero(mask)}) and will be ignored.")
737
+ continue
738
+
739
+ imin, imax = slices[0].start, slices[0].stop - 1
740
+ jmin, jmax = slices[1].start, slices[1].stop - 1
741
+
742
+ imin = int(max(imin - 1, 0))
743
+ imax = int(min(imax + 1, self.labeled_array.shape[0] - 1))
744
+ jmin = int(max(jmin - 1, 0))
745
+ jmax = int(min(jmax + 1, self.labeled_array.shape[1] - 1))
746
+
747
+ ref_crop = self.array_reference.crop(imin, jmin, imax-imin+1, jmax-jmin+1)
748
+ to_compare_crop = self.array_to_compare.crop(imin, jmin, imax-imin+1, jmax-jmin+1)
749
+ label_crop = self.labeled_array[imin:imax+1, jmin:jmax+1].copy()
750
+
751
+
752
+ to_compare_crop.array.mask[:,:] = ref_crop.array.mask[:,:] = self.labeled_array[imin:imax+1, jmin:jmax+1] != idx_feature
753
+
754
+ ref_crop.set_nullvalue_in_mask()
755
+ to_compare_crop.set_nullvalue_in_mask()
756
+ label_crop[label_crop != idx_feature] = 0
757
+
758
+ ref_crop.nbnotnull = nb_in_patch
759
+ to_compare_crop.nbnotnull = nb_in_patch
760
+
761
+ self.difference_parts[idx_feature] = ArrayDifference(ref_crop, to_compare_crop, idx_feature, label_crop)
762
+
763
+ assert self.last_features == len(self.difference_parts), \
764
+ f"Last feature index {self.last_features} does not match the number of differences found"
765
+
766
+ self.num_features = self.last_features
767
+ logging.info(f"Number of features after filtering: {self.num_features}")
768
+
769
+ return self.labeled_array
770
+
771
+ @property
772
+ def summary_text(self) -> list[str]:
773
+ """
774
+ Generate a summary text for the report.
775
+ """
776
+
777
+ diff = self.difference.array.compressed()
778
+ text_left = [
779
+ _("Number of features: ") + str(self.num_features),
780
+ _('Resolution: ') + f"{self.array_reference.dx} m x {self.array_reference.dy} m",
781
+ _('Extent: ') + f"({self.array_reference.origx}, {self.array_reference.origy})" + f" - ({self.array_reference.origx + self.array_reference.nbx * self.array_reference.dx}, {self.array_reference.origy + self.array_reference.nby * self.array_reference.dy})",
782
+ _('Width x Height: ') + f"{self.array_reference.nbx * self.array_reference.dx} m x {self.array_reference.nby * self.array_reference.dy} m",
783
+ ]
784
+ text_right = [
785
+ _('Excavation: ') + f"{np.sum(diff[diff < 0.]) * self.array_reference.dx * self.array_reference.dy:.3f} m³",
786
+ _('Deposit/Backfill: ') + f"{np.sum(diff[diff > 0.]) * self.array_reference.dx * self.array_reference.dy:.3f} m³",
787
+ _('Net volume: ') + f"{np.sum(diff) * self.array_reference.dx * self.array_reference.dy:.3f} m³",
788
+ ]
789
+ return text_left, text_right
790
+
791
+ def _complete_report(self, layout:CompareArraysLayout):
792
+ """ Complete the report with the global summary and individual differences. """
793
+
794
+ key_fig = [('Arrays_0-0', self.plot_reference),
795
+ ('Arrays_1-0', self.plot_to_compare),
796
+ ('Difference', self.plot_difference),]
797
+
798
+ keys = layout.keys
799
+ for key, fig_routine in key_fig:
800
+ if key in keys:
801
+
802
+ rect = layout._layout[key]
803
+
804
+ fig, ax = fig_routine()
805
+
806
+ # set size to fit the rectangle
807
+ fig.set_size_inches(pt2inches(rect.width), pt2inches(rect.height))
808
+
809
+ # convert canvas to PNG and insert it into the PDF
810
+ temp_file = NamedTemporaryFile(delete=False, suffix='.png')
811
+ fig.savefig(temp_file, format='png', bbox_inches='tight', dpi=self._dpi)
812
+ layout._page.insert_image(layout._layout[key], filename=temp_file.name)
813
+ # delete the temporary file
814
+ temp_file.delete = True
815
+ temp_file.close()
816
+
817
+ # Force to delete fig
818
+ plt.close(fig)
819
+ else:
820
+ logging.warning(f"Key {key} not found in layout. Skipping plot.")
821
+
822
+ tleft, tright = self.summary_text
823
+
824
+ rect = layout._layout['Summary_0-0']
825
+ text_left, css_left = list_to_html(tleft, font_size='8pt')
826
+ layout._page.insert_htmlbox(rect, text_left, css=css_left)
827
+ rect = layout._layout['Summary_1-0']
828
+ text_right, css_right = list_to_html(tright, font_size='8pt')
829
+ layout._page.insert_htmlbox(rect, text_right, css=css_right)
830
+
831
+ def plot_histogram_features(self, figax:tuple[plt.Figure, plt.Axes]=None, density = True, alpha = 0.5, **kwargs) -> tuple[plt.Figure, plt.Axes]:
832
+ """
833
+ Plot histogram of the number of cells in each feature.
834
+ """
835
+ if figax is None:
836
+ figax = plt.subplots()
837
+
838
+ fig, ax = figax
839
+
840
+ surf = self.array_reference.dx * self.array_reference.dy
841
+
842
+ # Extract the number of cells for each feature
843
+ nb_cells = [item[0] * surf for item in self.nb_cells[:self.last_features]]
844
+
845
+ if len(nb_cells) > 0:
846
+ ax.hist(nb_cells, bins= min(100, int(len(nb_cells)/4)), density=density, alpha=alpha, **kwargs)
847
+
848
+ ax.set_title(_("Histogram of surface in each feature [m²]"))
849
+
850
+ # set font size of the labels
851
+ ax.tick_params(axis='both', which='major', labelsize=6)
852
+ for label in ax.get_xticklabels():
853
+ label.set_fontsize(self._fontsize)
854
+ for label in ax.get_yticklabels():
855
+ label.set_fontsize(self._fontsize)
856
+ # and gfor the label title
857
+ ax.set_xlabel(ax.get_xlabel(), fontsize=self._fontsize)
858
+ ax.set_ylabel(ax.get_ylabel(), fontsize=self._fontsize)
859
+
860
+ return fig, ax
861
+
862
+ def plot_histogram_features_difference(self, figax:tuple[plt.Figure, plt.Axes]=None, density = True, alpha = 1.0, **kwargs) -> tuple[plt.Figure, plt.Axes]:
863
+ """
864
+ Plot histogram of the volume in each feature for the difference.
865
+ """
866
+ if figax is None:
867
+ figax = plt.subplots()
868
+
869
+ fig, ax = figax
870
+
871
+ # # Calculate the difference between the two arrays
872
+ # diff = self.difference
873
+
874
+ volumes = []
875
+ for idx in tqdm(self.nb_cells[:self.last_features], desc="Calculating volumes"):
876
+ # Get the feature index
877
+ feature_index = idx[1]
878
+ part = self.difference_parts[feature_index]
879
+ # Create a mask for the feature
880
+ mask = part.label == feature_index
881
+ # Calculate the volume for this feature
882
+ volumes.append(np.ma.sum(part.difference.array[mask]) * self.array_reference.dx * self.array_reference.dy)
883
+
884
+ # Create a histogram of the differences
885
+ if len(volumes) > 0:
886
+ ax.hist(volumes, bins= min(100, int(len(volumes)/4)), density=density, alpha=alpha, **kwargs)
887
+
888
+ ax.set_title(_("Histogram of net volumes [m³]"))
889
+
890
+ # set font size of the labels
891
+ ax.tick_params(axis='both', which='major', labelsize=6)
892
+ for label in ax.get_xticklabels():
893
+ label.set_fontsize(self._fontsize)
894
+ for label in ax.get_yticklabels():
895
+ label.set_fontsize(self._fontsize)
896
+ # and gfor the label title
897
+ ax.set_xlabel(ax.get_xlabel(), fontsize=self._fontsize)
898
+ ax.set_ylabel(ax.get_ylabel(), fontsize=self._fontsize)
899
+
900
+ fig.tight_layout()
901
+
902
+ return fig, ax
903
+
904
+ def _complete_report2(self, layout:CompareArraysLayout):
905
+ """ Complete the report with the individual differences. """
906
+
907
+ key_fig = [('Histogram_0-0', self.plot_histogram_features),
908
+ ('Histogram_1-0', self.plot_histogram_features_difference),
909
+ ('Arrays_0-0', self.plot_position),
910
+ ('Arrays_1-0', self.plot_cartoweb),
911
+ ('Position', self.plot_topo_grey),
912
+ ]
913
+
914
+ keys = layout.keys
915
+ for key, fig_routine in key_fig:
916
+ if key in keys:
917
+
918
+ rect = layout._layout[key]
919
+
920
+ fig, ax = fig_routine()
921
+
922
+ # set size to fit the rectangle
923
+ fig.set_size_inches(pt2inches(rect.width), pt2inches(rect.height))
924
+ fig.tight_layout()
925
+
926
+ # convert canvas to PNG and insert it into the PDF
927
+ temp_file = NamedTemporaryFile(delete=False, suffix='.png')
928
+ fig.savefig(temp_file, format='png', bbox_inches='tight', dpi=self._dpi)
929
+ layout._page.insert_image(layout._layout[key], filename=temp_file.name)
930
+ # delete the temporary file
931
+ temp_file.delete = True
932
+ temp_file.close()
933
+
934
+ # Force to delete fig
935
+ plt.close(fig)
936
+ else:
937
+ logging.warning(f"Key {key} not found in layout. Skipping plot.")
938
+
939
+
940
+
941
+ def create_report(self, output_file: str | Path = None,
942
+ append_all_differences: bool = True,
943
+ nb_max_differences:int = -1) -> None:
944
+ """ Create a page report for the array comparison. """
945
+
946
+ if output_file is None:
947
+ output_file = Path(f"compare_arrays_report.pdf")
948
+
949
+ if output_file.exists():
950
+ logging.warning(f"Output file {output_file} already exists. It will be overwritten.")
951
+
952
+ layout = CompareArraysLayout("Comparison Report")
953
+ layout.create_report()
954
+ self._complete_report(layout)
955
+
956
+ if nb_max_differences < 0:
957
+ nb_max_differences = len(self.difference_parts)
958
+ elif nb_max_differences > len(self.difference_parts):
959
+ logging.warning(f"Requested {nb_max_differences} differences, but only {len(self.difference_parts)} are available. Using all available differences.")
960
+ elif nb_max_differences < len(self.difference_parts):
961
+ logging.info(f"Limiting to {nb_max_differences} differences.")
962
+
963
+ features_to_treat = [feature[1] for feature in self.nb_cells[:nb_max_differences]]
964
+
965
+ with TemporaryDirectory() as temp_dir:
966
+
967
+ layout2 = CompareArraysLayout2("Distribution of Differences")
968
+ layout2.create_report()
969
+ self._complete_report2(layout2)
970
+ layout2.save_report(Path(temp_dir) / "distribution_of_differences.pdf")
971
+ all_pdfs = [Path(temp_dir) / "distribution_of_differences.pdf"]
972
+
973
+ if append_all_differences:
974
+ # Add each difference report to the main layout
975
+ all_pdfs.extend([self.difference_parts[idx].create_report(Path(temp_dir) / f"array_difference_{idx}.pdf") for idx in tqdm(features_to_treat, desc="Creating individual difference reports")])
976
+
977
+ for pdf_file in tqdm(all_pdfs, desc="Compiling PDFs"):
978
+ layout._doc.insert_file(pdf_file)
979
+
980
+ # create a TOC
981
+ layout._doc.set_toc(layout._doc.get_toc())
982
+
983
+ layout.save_report(output_file)
984
+ self._pdf_path = output_file
985
+
986
+ @property
987
+ def pdf_path(self) -> Path:
988
+ """ Return the path to the generated PDF report. """
989
+ if hasattr(self, '_pdf_path'):
990
+ return self._pdf_path
991
+ else:
992
+ raise AttributeError("PDF path not set. Please create the report first.")
993
+
994
+ class CompareArrays_wx(PDFViewer):
995
+
996
+ def __init__(self, reference: WolfArray | str | Path,
997
+ to_compare: WolfArray | str | Path,
998
+ ignored_patche_area:float = 2.0,
999
+ nb_max_patches:int = 10,
1000
+ threshold: float = 0.01,
1001
+ dpi=200, **kwargs):
1002
+ """ Initialize the Simple Simulation GPU Report Viewer for comparison """
1003
+
1004
+ super(CompareArrays_wx, self).__init__(None, **kwargs)
1005
+
1006
+ use('agg')
1007
+
1008
+ if isinstance(reference, WolfArray) and isinstance(to_compare, WolfArray):
1009
+ if np.any(reference.array.mask != to_compare.array.mask):
1010
+ logging.warning("The masks of the two arrays are not identical. This may lead to unexpected results.")
1011
+ dlg = wx.MessageDialog(self,
1012
+ _("The masks of the two arrays are not identical.\nThis may lead to unexpected results.\n\nWe will use the reference mask for the comparison."),
1013
+ _("Warning"),
1014
+ wx.OK | wx.ICON_WARNING)
1015
+ dlg.ShowModal()
1016
+ dlg.Destroy()
1017
+ to_compare = WolfArray(mold=to_compare)
1018
+ to_compare.array.mask[:,:] = reference.array.mask[:,:]
1019
+
1020
+ self._report = CompareArrays(reference, to_compare)
1021
+
1022
+ self._report._dpi = dpi
1023
+ self._report.localize_differences(threshold=threshold,
1024
+ ignored_patche_area=ignored_patche_area)
1025
+ self._report.create_report(nb_max_differences=nb_max_patches)
1026
+
1027
+ # Load the PDF into the viewer
1028
+ if self._report.pdf_path is None:
1029
+ logging.error("No report created. Cannot load PDF.")
1030
+ return
1031
+
1032
+ self.load_pdf(self._report.pdf_path)
1033
+ self.viewer.SetZoom(-1) # Fit to width
1034
+
1035
+ # Set the title of the frame
1036
+ self.SetTitle("Simple Simulation GPU Comparison Report")
1037
+
1038
+ self.Bind(wx.EVT_CLOSE, self.on_close)
1039
+
1040
+ use('wxagg')
1041
+
1042
+ def on_close(self, event):
1043
+ """ Handle the close event to clean up resources """
1044
+ self.viewer.pdfdoc.pdfdoc.close()
1045
+ self.Destroy()
1046
+
1047
+ def get_zones(self) -> Zones:
1048
+ """
1049
+ Get the zones from the report.
1050
+ """
1051
+ ret = self._report.get_zones()
1052
+ ret.prep_listogl()
1053
+
1054
+ return ret