wolfhece 2.2.27__py3-none-any.whl → 2.2.28__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/PyDraw.py +10 -2
- wolfhece/PyPalette.py +18 -0
- wolfhece/PyVertexvectors.py +1 -4
- wolfhece/apps/version.py +1 -1
- wolfhece/report/common.py +496 -0
- wolfhece/report/compare_arrays.py +844 -0
- wolfhece/report/simplesimgpu.py +1 -95
- wolfhece/report/tools.py +121 -16
- wolfhece/scenario/config_manager.py +22 -1
- wolfhece/wolf_array.py +49 -2
- {wolfhece-2.2.27.dist-info → wolfhece-2.2.28.dist-info}/METADATA +1 -1
- {wolfhece-2.2.27.dist-info → wolfhece-2.2.28.dist-info}/RECORD +15 -13
- {wolfhece-2.2.27.dist-info → wolfhece-2.2.28.dist-info}/WHEEL +0 -0
- {wolfhece-2.2.27.dist-info → wolfhece-2.2.28.dist-info}/entry_points.txt +0 -0
- {wolfhece-2.2.27.dist-info → wolfhece-2.2.28.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,844 @@
|
|
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
|
8
|
+
import pymupdf as pdf
|
9
|
+
|
10
|
+
from .common import A4_rect, rect_cm, list_to_html, list_to_html_aligned, get_rect_from_text
|
11
|
+
from .common import inches2cm, pts2cm, cm2pts, cm2inches, DefaultLayoutA4, NamedTemporaryFile, pt2inches, TemporaryDirectory
|
12
|
+
from ..wolf_array import WolfArray, header_wolf, vector, zone, Zones, wolfvertex as wv, wolfpalette
|
13
|
+
from ..PyTranslate import _
|
14
|
+
|
15
|
+
class ArrayDifferenceLayout(DefaultLayoutA4):
|
16
|
+
"""
|
17
|
+
Layout for comparing two arrays in a report.
|
18
|
+
|
19
|
+
1 cadre pour la zone traitée avec photo de fond ign + contour vectoriel
|
20
|
+
1 cadre avec zoom plus large min 250m
|
21
|
+
1 cadre avec matrice ref + contour vectoriel
|
22
|
+
1 cadre avec matrice à comparer + contour vectoriel
|
23
|
+
1 cadre avec différence
|
24
|
+
1 cadre avec valeurs de synthèse
|
25
|
+
|
26
|
+
1 cadre avec histogramme
|
27
|
+
1 cadre avec histogramme des différences
|
28
|
+
"""
|
29
|
+
|
30
|
+
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):
|
31
|
+
super().__init__(title, filename, ox, oy, tx, ty, parent, is2D, idx, plotted, mapviewer, need_for_wx, bbox, find_minmax, shared, colors)
|
32
|
+
|
33
|
+
useful = self.useful_part
|
34
|
+
|
35
|
+
width = useful.xmax - useful.xmin
|
36
|
+
height = useful.ymax - useful.ymin
|
37
|
+
|
38
|
+
self._hitograms = self.add_element_repeated(_("Histogram"), width=width, height=2.5,
|
39
|
+
first_x=useful.xmin, first_y=useful.ymax,
|
40
|
+
count_x=1, count_y=-2, padding=0.5)
|
41
|
+
|
42
|
+
self._arrays = self.add_element_repeated(_("Arrays"), width= (width-self.padding) / 2, height=5.5,
|
43
|
+
first_x=useful.xmin, first_y=self._hitograms.ymin - self.padding,
|
44
|
+
count_x=2, count_y=-3, padding=0.5)
|
45
|
+
|
46
|
+
class CompareArraysLayout(DefaultLayoutA4):
|
47
|
+
|
48
|
+
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):
|
49
|
+
super().__init__(title, filename, ox, oy, tx, ty, parent, is2D, idx, plotted, mapviewer, need_for_wx, bbox, find_minmax, shared, colors)
|
50
|
+
|
51
|
+
useful = self.useful_part
|
52
|
+
|
53
|
+
width = useful.xmax - useful.xmin
|
54
|
+
height = useful.ymax - useful.ymin
|
55
|
+
|
56
|
+
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)
|
57
|
+
|
58
|
+
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)
|
59
|
+
self._diff_rect = self.add_element(_("Difference"), width= width, height=11.5, x=useful.xmin, y=useful.ymin)
|
60
|
+
|
61
|
+
|
62
|
+
class CompareArraysLayout2(DefaultLayoutA4):
|
63
|
+
|
64
|
+
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):
|
65
|
+
super().__init__(title, filename, ox, oy, tx, ty, parent, is2D, idx, plotted, mapviewer, need_for_wx, bbox, find_minmax, shared, colors)
|
66
|
+
|
67
|
+
useful = self.useful_part
|
68
|
+
|
69
|
+
width = useful.xmax - useful.xmin
|
70
|
+
height = useful.ymax - useful.ymin
|
71
|
+
|
72
|
+
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)
|
73
|
+
|
74
|
+
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)
|
75
|
+
self._diff_rect = self.add_element(_("Position"), width= width, height=11.5, x=useful.xmin, y=useful.ymin)
|
76
|
+
|
77
|
+
|
78
|
+
class ArrayDifference():
|
79
|
+
"""
|
80
|
+
Class to manage the difference between two WolfArray objects.
|
81
|
+
"""
|
82
|
+
|
83
|
+
def __init__(self, reference:WolfArray, to_compare:WolfArray, index:int, label:np.ndarray):
|
84
|
+
|
85
|
+
self._dpi = 600
|
86
|
+
self.default_size_hitograms = (12, 6)
|
87
|
+
self.default_size_arrays = (10, 10)
|
88
|
+
self._fontsize = 6
|
89
|
+
|
90
|
+
self.reference = reference
|
91
|
+
self.to_compare = to_compare
|
92
|
+
|
93
|
+
self.reference.updatepalette()
|
94
|
+
self.to_compare.mypal = self.reference.mypal
|
95
|
+
|
96
|
+
self.index = index
|
97
|
+
self.label = label
|
98
|
+
|
99
|
+
self._background = 'IGN'
|
100
|
+
|
101
|
+
@property
|
102
|
+
def contour(self) -> vector:
|
103
|
+
ret = self.reference.suxsuy_contour(abs=True)
|
104
|
+
ret = ret[2]
|
105
|
+
|
106
|
+
ret.myprop.color = (0, 0, 255)
|
107
|
+
ret.myprop.width = 2
|
108
|
+
|
109
|
+
return ret
|
110
|
+
|
111
|
+
@property
|
112
|
+
def external_border(self) -> vector:
|
113
|
+
"""
|
114
|
+
Get the bounds of the reference array.
|
115
|
+
"""
|
116
|
+
ret = vector(name=_("External border"))
|
117
|
+
(xmin, xmax), (ymin, ymax) = self.reference.get_bounds()
|
118
|
+
ret.add_vertex(wv(xmin, ymin))
|
119
|
+
ret.add_vertex(wv(xmax, ymin))
|
120
|
+
ret.add_vertex(wv(xmax, ymax))
|
121
|
+
ret.add_vertex(wv(xmin, ymax))
|
122
|
+
ret.force_to_close()
|
123
|
+
|
124
|
+
ret.myprop.color = (255, 0, 0)
|
125
|
+
ret.myprop.width = 3
|
126
|
+
|
127
|
+
return ret
|
128
|
+
|
129
|
+
def __str__(self):
|
130
|
+
|
131
|
+
assert self.reference.nbnotnull == self.to_compare.nbnotnull, "The number of non-null cells in both arrays must be the same."
|
132
|
+
|
133
|
+
ret = self.reference.__str__() + '\n'
|
134
|
+
|
135
|
+
ret += _("Index : ") + str(self.index) + '\n'
|
136
|
+
ret += _("Number of cells : ") + str(self.reference.nbnotnull) + '\n'
|
137
|
+
|
138
|
+
return ret
|
139
|
+
|
140
|
+
@property
|
141
|
+
def _summary_text(self):
|
142
|
+
"""
|
143
|
+
Generate a summary text for the report.
|
144
|
+
"""
|
145
|
+
diff = self.difference.array.compressed()
|
146
|
+
text = [
|
147
|
+
_("Index: ") + str(self.index),
|
148
|
+
_("Number of cells: ") + str(self.reference.nbnotnull),
|
149
|
+
_('Resolution: ') + f"{self.reference.dx} m x {self.reference.dy} m",
|
150
|
+
_('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})",
|
151
|
+
_('Width x Height: ') + f"{self.reference.nbx * self.reference.dx} m x {self.reference.nby * self.reference.dy} m",
|
152
|
+
_('Excavation: ') + f"{np.sum(diff[diff < 0.]) * self.reference.dx * self.reference.dy:.3f} m³",
|
153
|
+
_('Deposit/Backfill: ') + f"{np.sum(diff[diff > 0.]) * self.reference.dx * self.reference.dy:.3f} m³",
|
154
|
+
_('Net volume: ') + f"{np.sum(diff) * self.reference.dx * self.reference.dy:.3f} m³",
|
155
|
+
]
|
156
|
+
return text
|
157
|
+
|
158
|
+
def set_palette_distribute(self, minval:float, maxval:float, step:int=0):
|
159
|
+
"""
|
160
|
+
Set the palette for both arrays.
|
161
|
+
"""
|
162
|
+
self.reference.mypal.distribute_values(minval, maxval, step)
|
163
|
+
|
164
|
+
def set_palette(self, values:list[float], colors:list[tuple[int, int, int]]):
|
165
|
+
"""
|
166
|
+
Set the palette for both arrays based on specific values.
|
167
|
+
"""
|
168
|
+
self.reference.mypal.set_values_colors(values, colors)
|
169
|
+
|
170
|
+
def plot_position(self, figax:tuple[plt.Figure, plt.Axes]=None) -> tuple[plt.Figure, plt.Axes]:
|
171
|
+
"""
|
172
|
+
Plot the reference array.
|
173
|
+
"""
|
174
|
+
if figax is None:
|
175
|
+
figax = plt.subplots()
|
176
|
+
|
177
|
+
fig, ax = figax
|
178
|
+
|
179
|
+
old_mask = self.reference.array.mask.copy()
|
180
|
+
self.reference.array.mask[:,:] = True
|
181
|
+
|
182
|
+
if self._background.upper() == 'IGN' or self._background.upper() == 'NGI':
|
183
|
+
self.reference.plot_matplotlib(figax=figax, figsize = self.default_size_arrays,
|
184
|
+
first_mask_data=False, with_legend=False,
|
185
|
+
update_palette= False,
|
186
|
+
IGN= True,
|
187
|
+
cat = 'orthoimage_coverage',
|
188
|
+
)
|
189
|
+
|
190
|
+
elif self._background.upper() == 'WALONMAP':
|
191
|
+
self.reference.plot_matplotlib(figax=figax, figsize = self.default_size_arrays,
|
192
|
+
first_mask_data=False, with_legend=False,
|
193
|
+
update_palette= False,
|
194
|
+
Walonmap= True,
|
195
|
+
cat = 'IMAGERIE/ORTHO_2022_ETE',
|
196
|
+
)
|
197
|
+
|
198
|
+
self.reference.array.mask[:,:] = old_mask
|
199
|
+
|
200
|
+
self.external_border.plot_matplotlib(ax=ax)
|
201
|
+
self.contour.plot_matplotlib(ax=ax)
|
202
|
+
|
203
|
+
return fig, ax
|
204
|
+
|
205
|
+
def plot_position_scaled(self, scale = 4, figax:tuple[plt.Figure, plt.Axes]=None) -> tuple[plt.Figure, plt.Axes]:
|
206
|
+
"""
|
207
|
+
Plot the reference array.
|
208
|
+
"""
|
209
|
+
if figax is None:
|
210
|
+
figax = plt.subplots()
|
211
|
+
|
212
|
+
fig, ax = figax
|
213
|
+
|
214
|
+
h = self.reference.get_header()
|
215
|
+
width = h.nbx * h.dx
|
216
|
+
height = h.nby * h.dy
|
217
|
+
|
218
|
+
h.origx += -width * scale / 2
|
219
|
+
h.origy += -height *scale / 2
|
220
|
+
h.nbx = 1
|
221
|
+
h.nby = 1
|
222
|
+
h.dx = width *(scale + 1)
|
223
|
+
h.dy = height *(scale + 1)
|
224
|
+
|
225
|
+
new = WolfArray(srcheader=h)
|
226
|
+
new.array.mask[:,:] = True
|
227
|
+
|
228
|
+
new.plot_matplotlib(figax=figax, figsize = self.default_size_arrays,
|
229
|
+
first_mask_data=False, with_legend=False,
|
230
|
+
update_palette= False,
|
231
|
+
Walonmap= True,
|
232
|
+
cat = 'IMAGERIE/ORTHO_2022_ETE')
|
233
|
+
|
234
|
+
self.external_border.plot_matplotlib(ax=ax)
|
235
|
+
|
236
|
+
return fig, ax
|
237
|
+
|
238
|
+
def plot_reference(self, figax:tuple[plt.Figure, plt.Axes]=None) -> tuple[plt.Figure, plt.Axes]:
|
239
|
+
"""
|
240
|
+
Plot the reference array.
|
241
|
+
"""
|
242
|
+
if figax is None:
|
243
|
+
figax = plt.subplots()
|
244
|
+
|
245
|
+
fig, ax = figax
|
246
|
+
|
247
|
+
self.reference.plot_matplotlib(figax=figax, figsize = self.default_size_arrays, first_mask_data=False, with_legend=True, update_palette= False)
|
248
|
+
return fig, ax
|
249
|
+
|
250
|
+
def plot_to_compare(self, figax:tuple[plt.Figure, plt.Axes]=None) -> tuple[plt.Figure, plt.Axes]:
|
251
|
+
"""
|
252
|
+
Plot the array to compare.
|
253
|
+
"""
|
254
|
+
if figax is None:
|
255
|
+
figax = plt.subplots()
|
256
|
+
|
257
|
+
fig, ax = figax
|
258
|
+
|
259
|
+
self.to_compare.plot_matplotlib(figax=figax, figsize = self.default_size_arrays, first_mask_data=False, with_legend=True, update_palette= False)
|
260
|
+
return fig, ax
|
261
|
+
|
262
|
+
@property
|
263
|
+
def difference(self) -> WolfArray:
|
264
|
+
"""
|
265
|
+
Get the difference between the two arrays.
|
266
|
+
"""
|
267
|
+
if not isinstance(self.reference, WolfArray) or not isinstance(self.to_compare, WolfArray):
|
268
|
+
raise TypeError("Both inputs must be instances of WolfArray")
|
269
|
+
|
270
|
+
return self.to_compare - self.reference
|
271
|
+
|
272
|
+
def plot_difference(self, figax:tuple[plt.Figure, plt.Axes]=None) -> tuple[plt.Figure, plt.Axes]:
|
273
|
+
"""
|
274
|
+
Plot the array to compare.
|
275
|
+
"""
|
276
|
+
if figax is None:
|
277
|
+
figax = plt.subplots()
|
278
|
+
|
279
|
+
fig, ax = figax
|
280
|
+
|
281
|
+
pal = wolfpalette()
|
282
|
+
pal.default_difference3()
|
283
|
+
|
284
|
+
diff = self.difference
|
285
|
+
diff.mypal = pal
|
286
|
+
diff.plot_matplotlib(figax=figax, figsize = self.default_size_arrays, first_mask_data=False, with_legend=True, update_palette= False)
|
287
|
+
return fig, ax
|
288
|
+
|
289
|
+
def _plot_histogram_reference(self, figax:tuple[plt.Figure, plt.Axes]=None, density = True, alpha = 0.5, **kwargs) -> tuple[plt.Figure, plt.Axes]:
|
290
|
+
"""
|
291
|
+
Plot histogram of the reference array.
|
292
|
+
"""
|
293
|
+
if figax is None:
|
294
|
+
figax = plt.subplots()
|
295
|
+
|
296
|
+
fig, ax = figax
|
297
|
+
|
298
|
+
ax.hist(self.reference.array.compressed(), density=density, alpha = alpha, **kwargs)
|
299
|
+
# ax.set_xlabel("Value")
|
300
|
+
# ax.set_ylabel("Frequency")
|
301
|
+
return fig, ax
|
302
|
+
|
303
|
+
def _plot_histogram_to_compare(self, figax:tuple[plt.Figure, plt.Axes]=None, density = True, alpha = 0.5, **kwargs) -> tuple[plt.Figure, plt.Axes]:
|
304
|
+
"""
|
305
|
+
Plot histogram of the array to compare.
|
306
|
+
"""
|
307
|
+
if figax is None:
|
308
|
+
figax = plt.subplots()
|
309
|
+
|
310
|
+
fig, ax = figax
|
311
|
+
|
312
|
+
ax.hist(self.to_compare.array.compressed(), density=density, alpha = alpha, **kwargs)
|
313
|
+
# ax.set_xlabel("Value")
|
314
|
+
# ax.set_ylabel("Frequency")
|
315
|
+
return fig, ax
|
316
|
+
|
317
|
+
def plot_histograms(self, figax:tuple[plt.Figure, plt.Axes]=None, density = True, alpha = 0.5, **kwargs) -> tuple[plt.Figure, plt.Axes]:
|
318
|
+
"""
|
319
|
+
Plot histograms of both arrays.
|
320
|
+
"""
|
321
|
+
if figax is None:
|
322
|
+
figax = plt.subplots(1, 1, figsize=self.default_size_hitograms)
|
323
|
+
|
324
|
+
fig, ax = figax
|
325
|
+
|
326
|
+
self._plot_histogram_reference((fig, ax), density = density, alpha=alpha, **kwargs)
|
327
|
+
self._plot_histogram_to_compare((fig, ax), density = density, alpha=alpha, **kwargs)
|
328
|
+
|
329
|
+
# set font size of the labels
|
330
|
+
ax.tick_params(axis='both', which='major', labelsize=6)
|
331
|
+
for label in ax.get_xticklabels():
|
332
|
+
label.set_fontsize(self._fontsize)
|
333
|
+
for label in ax.get_yticklabels():
|
334
|
+
label.set_fontsize(self._fontsize)
|
335
|
+
# and gfor the label title
|
336
|
+
ax.set_xlabel(ax.get_xlabel(), fontsize=self._fontsize)
|
337
|
+
ax.set_ylabel(ax.get_ylabel(), fontsize=self._fontsize)
|
338
|
+
|
339
|
+
fig.tight_layout()
|
340
|
+
return fig, ax
|
341
|
+
|
342
|
+
def plot_histograms_difference(self, figax:tuple[plt.Figure, plt.Axes]=None, density = True, alpha = 1.0, **kwargs) -> tuple[plt.Figure, plt.Axes]:
|
343
|
+
"""
|
344
|
+
Plot histogram of the difference between the two arrays.
|
345
|
+
"""
|
346
|
+
if figax is None:
|
347
|
+
figax = plt.subplots(figsize=self.default_size_hitograms)
|
348
|
+
|
349
|
+
fig, ax = figax
|
350
|
+
|
351
|
+
difference_data = self.difference.array.compressed()
|
352
|
+
ax.hist(difference_data, density=density, alpha=alpha, **kwargs)
|
353
|
+
|
354
|
+
# ax.set_xlabel("Value")
|
355
|
+
# ax.set_ylabel("Frequency")
|
356
|
+
|
357
|
+
# set font size of the labels
|
358
|
+
ax.tick_params(axis='both', which='major', labelsize=6)
|
359
|
+
for label in ax.get_xticklabels():
|
360
|
+
label.set_fontsize(self._fontsize)
|
361
|
+
for label in ax.get_yticklabels():
|
362
|
+
label.set_fontsize(self._fontsize)
|
363
|
+
# and gfor the label title
|
364
|
+
ax.set_xlabel(ax.get_xlabel(), fontsize=self._fontsize)
|
365
|
+
ax.set_ylabel(ax.get_ylabel(), fontsize=self._fontsize)
|
366
|
+
|
367
|
+
return fig, ax
|
368
|
+
|
369
|
+
def _complete_report(self, layout:ArrayDifferenceLayout):
|
370
|
+
|
371
|
+
"""
|
372
|
+
Complete the report with the arrays and histograms.
|
373
|
+
"""
|
374
|
+
useful = layout.useful_part
|
375
|
+
|
376
|
+
# Plot reference array
|
377
|
+
key_fig = [('Histogram_0-0', self.plot_histograms),
|
378
|
+
('Histogram_0-1', self.plot_histograms_difference),
|
379
|
+
('Arrays_0-0', self.plot_position),
|
380
|
+
('Arrays_1-0', self.plot_position_scaled),
|
381
|
+
('Arrays_0-1', self.plot_reference),
|
382
|
+
('Arrays_1-1', self.plot_to_compare),
|
383
|
+
('Arrays_0-2', self.plot_difference),]
|
384
|
+
|
385
|
+
keys = layout.keys
|
386
|
+
for key, fig_routine in key_fig:
|
387
|
+
if key in keys:
|
388
|
+
|
389
|
+
rect = layout._layout[key]
|
390
|
+
|
391
|
+
fig, ax = fig_routine()
|
392
|
+
|
393
|
+
# set size to fit the rectangle
|
394
|
+
fig.set_size_inches(pt2inches(rect.width), pt2inches(rect.height))
|
395
|
+
|
396
|
+
if 'Histogram' in key:
|
397
|
+
fig.tight_layout()
|
398
|
+
|
399
|
+
|
400
|
+
# convert canvas to PNG and insert it into the PDF
|
401
|
+
temp_file = NamedTemporaryFile(delete=False, suffix='.png')
|
402
|
+
fig.savefig(temp_file, format='png', bbox_inches='tight', dpi=self._dpi)
|
403
|
+
layout._page.insert_image(layout._layout[key], filename = temp_file.name)
|
404
|
+
# delete the temporary file
|
405
|
+
temp_file.delete = True
|
406
|
+
temp_file.close()
|
407
|
+
|
408
|
+
# Force to delete fig
|
409
|
+
plt.close(fig)
|
410
|
+
else:
|
411
|
+
logging.warning(f"Key {key} not found in layout. Skipping plot.")
|
412
|
+
|
413
|
+
key = 'Arrays_1-2'
|
414
|
+
if key in keys:
|
415
|
+
text, css = list_to_html(self._summary_text, font_size='8pt')
|
416
|
+
layout._page.insert_htmlbox(layout._layout[key], text,
|
417
|
+
css=css)
|
418
|
+
|
419
|
+
def create_report(self, output_file: str | Path = None) -> Path:
|
420
|
+
""" Create a page report for the array difference. """
|
421
|
+
|
422
|
+
if output_file is None:
|
423
|
+
output_file = Path(f"array_difference_{self.index}.pdf")
|
424
|
+
|
425
|
+
if output_file.exists():
|
426
|
+
logging.warning(f"Output file {output_file} already exists. It will be overwritten.")
|
427
|
+
|
428
|
+
layout = ArrayDifferenceLayout(f"Differences - Index n°{self.index}")
|
429
|
+
layout.create_report()
|
430
|
+
self._complete_report(layout)
|
431
|
+
layout.save_report(output_file)
|
432
|
+
|
433
|
+
return output_file
|
434
|
+
|
435
|
+
class CompareArrays:
|
436
|
+
|
437
|
+
def __init__(self, reference: WolfArray | str | Path, to_compare: WolfArray | str | Path):
|
438
|
+
|
439
|
+
self._dpi = 600
|
440
|
+
self.default_size_arrays = (10, 10)
|
441
|
+
self._fontsize = 6
|
442
|
+
|
443
|
+
if isinstance(reference, (str, Path)):
|
444
|
+
reference = WolfArray(reference)
|
445
|
+
if isinstance(to_compare, (str, Path)):
|
446
|
+
to_compare = WolfArray(to_compare)
|
447
|
+
|
448
|
+
if not reference.is_like(to_compare):
|
449
|
+
raise ValueError("Arrays are not compatible for comparison")
|
450
|
+
|
451
|
+
self.array_reference:WolfArray
|
452
|
+
self.array_to_compare:WolfArray
|
453
|
+
self.array_reference = reference
|
454
|
+
self.array_to_compare = to_compare
|
455
|
+
|
456
|
+
self.labeled_array: np.ndarray = None
|
457
|
+
self.num_features: int = 0
|
458
|
+
self.nb_cells: list = []
|
459
|
+
|
460
|
+
self.difference_parts:dict[int, ArrayDifference] = {}
|
461
|
+
|
462
|
+
@property
|
463
|
+
def difference(self) -> WolfArray:
|
464
|
+
|
465
|
+
if not isinstance(self.array_reference, WolfArray) or not isinstance(self.array_to_compare, WolfArray):
|
466
|
+
raise TypeError("Both inputs must be instances of WolfArray")
|
467
|
+
|
468
|
+
return self.array_to_compare - self.array_reference
|
469
|
+
|
470
|
+
def plot_position(self, figax:tuple[plt.Figure, plt.Axes]=None) -> tuple[plt.Figure, plt.Axes]:
|
471
|
+
"""
|
472
|
+
Plot the reference array with a background.
|
473
|
+
"""
|
474
|
+
if figax is None:
|
475
|
+
figax = plt.subplots()
|
476
|
+
|
477
|
+
fig, ax = figax
|
478
|
+
|
479
|
+
h = self.array_reference.get_header()
|
480
|
+
width = h.nbx * h.dx
|
481
|
+
height = h.nby * h.dy
|
482
|
+
h.dx = width
|
483
|
+
h.dy = height
|
484
|
+
h.nbx = 1
|
485
|
+
h.nby = 1
|
486
|
+
|
487
|
+
new = WolfArray(srcheader=h)
|
488
|
+
new.array.mask[:,:] = True
|
489
|
+
|
490
|
+
|
491
|
+
new.plot_matplotlib(figax=figax, figsize = self.default_size_arrays,
|
492
|
+
first_mask_data=False, with_legend=False,
|
493
|
+
update_palette= False,
|
494
|
+
IGN= True,
|
495
|
+
cat = 'orthoimage_coverage',
|
496
|
+
)
|
497
|
+
|
498
|
+
return fig, ax
|
499
|
+
|
500
|
+
def plot_cartoweb(self, figax:tuple[plt.Figure, plt.Axes]=None) -> tuple[plt.Figure, plt.Axes]:
|
501
|
+
"""
|
502
|
+
Plot the reference array with a background.
|
503
|
+
"""
|
504
|
+
if figax is None:
|
505
|
+
figax = plt.subplots()
|
506
|
+
|
507
|
+
fig, ax = figax
|
508
|
+
|
509
|
+
h = self.array_reference.get_header()
|
510
|
+
width = h.nbx * h.dx
|
511
|
+
height = h.nby * h.dy
|
512
|
+
h.dx = width
|
513
|
+
h.dy = height
|
514
|
+
h.nbx = 1
|
515
|
+
h.nby = 1
|
516
|
+
|
517
|
+
new = WolfArray(srcheader=h)
|
518
|
+
new.array.mask[:,:] = True
|
519
|
+
|
520
|
+
|
521
|
+
new.plot_matplotlib(figax=figax, figsize = self.default_size_arrays,
|
522
|
+
first_mask_data=False, with_legend=False,
|
523
|
+
update_palette= False,
|
524
|
+
Cartoweb= True,
|
525
|
+
cat = 'overlay',
|
526
|
+
)
|
527
|
+
|
528
|
+
return fig, ax
|
529
|
+
|
530
|
+
def plot_topo_grey(self, figax:tuple[plt.Figure, plt.Axes]=None) -> tuple[plt.Figure, plt.Axes]:
|
531
|
+
"""
|
532
|
+
Plot the reference array with a background.
|
533
|
+
"""
|
534
|
+
if figax is None:
|
535
|
+
figax = plt.subplots()
|
536
|
+
|
537
|
+
fig, ax = figax
|
538
|
+
|
539
|
+
h = self.array_reference.get_header()
|
540
|
+
width = h.nbx * h.dx
|
541
|
+
height = h.nby * h.dy
|
542
|
+
h.dx = width
|
543
|
+
h.dy = height
|
544
|
+
h.nbx = 1
|
545
|
+
h.nby = 1
|
546
|
+
|
547
|
+
new = WolfArray(srcheader=h)
|
548
|
+
new.array.mask[:,:] = True
|
549
|
+
|
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
|
+
Cartoweb= True,
|
555
|
+
cat = 'topo_grey',
|
556
|
+
)
|
557
|
+
|
558
|
+
return fig, ax
|
559
|
+
|
560
|
+
def plot_reference(self, figax:tuple[plt.Figure, plt.Axes]=None) -> tuple[plt.Figure, plt.Axes]:
|
561
|
+
"""
|
562
|
+
Plot the reference array.
|
563
|
+
"""
|
564
|
+
if figax is None:
|
565
|
+
figax = plt.subplots()
|
566
|
+
|
567
|
+
fig, ax = figax
|
568
|
+
|
569
|
+
self.array_reference.plot_matplotlib(figax=figax, figsize = self.default_size_arrays, first_mask_data=False, with_legend=True, update_palette= False)
|
570
|
+
|
571
|
+
for diff in self.difference_parts.values():
|
572
|
+
diff.external_border.plot_matplotlib(ax=ax)
|
573
|
+
|
574
|
+
return fig, ax
|
575
|
+
|
576
|
+
def plot_to_compare(self, figax:tuple[plt.Figure, plt.Axes]=None) -> tuple[plt.Figure, plt.Axes]:
|
577
|
+
"""
|
578
|
+
Plot the array to compare.
|
579
|
+
"""
|
580
|
+
if figax is None:
|
581
|
+
figax = plt.subplots()
|
582
|
+
|
583
|
+
fig, ax = figax
|
584
|
+
|
585
|
+
self.array_to_compare.plot_matplotlib(figax=figax, figsize = self.default_size_arrays, first_mask_data=False, with_legend=True, update_palette= False)
|
586
|
+
|
587
|
+
for diff in self.difference_parts.values():
|
588
|
+
diff.contour.plot_matplotlib(ax=ax)
|
589
|
+
|
590
|
+
return fig, ax
|
591
|
+
|
592
|
+
def plot_difference(self, figax:tuple[plt.Figure, plt.Axes]=None) -> tuple[plt.Figure, plt.Axes]:
|
593
|
+
"""
|
594
|
+
Plot the difference between the two arrays.
|
595
|
+
"""
|
596
|
+
if figax is None:
|
597
|
+
figax = plt.subplots()
|
598
|
+
|
599
|
+
fig, ax = figax
|
600
|
+
|
601
|
+
pal = wolfpalette()
|
602
|
+
pal.default_difference3()
|
603
|
+
|
604
|
+
diff = self.difference
|
605
|
+
diff.mypal = pal
|
606
|
+
diff.plot_matplotlib(figax=figax, figsize = self.default_size_arrays, first_mask_data=False, with_legend=True, update_palette= False)
|
607
|
+
return fig, ax
|
608
|
+
|
609
|
+
def localize_differences(self, threshold: float = 0.0) -> np.ndarray:
|
610
|
+
|
611
|
+
labeled_array = self.difference.array.data.copy()
|
612
|
+
labeled_array[self.array_reference.array.mask] = 0
|
613
|
+
|
614
|
+
self.labeled_array, self.num_features = label(labeled_array)
|
615
|
+
|
616
|
+
self.nb_cells = []
|
617
|
+
|
618
|
+
self.nb_cells = list(sum_labels(np.ones(self.labeled_array.shape, dtype=np.int32), self.labeled_array, range(1, self.num_features+1)))
|
619
|
+
self.nb_cells = [[self.nb_cells[j], j+1] for j in range(0, self.num_features)]
|
620
|
+
|
621
|
+
self.nb_cells.sort(key=lambda x: x[0], reverse=True)
|
622
|
+
|
623
|
+
# find xmin, ymin, xmax, ymax for each feature
|
624
|
+
for idx_feature in range(1, self.num_features + 1):
|
625
|
+
mask = self.labeled_array == idx_feature
|
626
|
+
i, j = np.where(mask)
|
627
|
+
|
628
|
+
imin, imax = i.min(), i.max()
|
629
|
+
jmin, jmax = j.min(), j.max()
|
630
|
+
|
631
|
+
imin = int(max(imin - 1, 0))
|
632
|
+
imax = int(min(imax + 1, self.labeled_array.shape[0] - 1))
|
633
|
+
jmin = int(max(jmin - 1, 0))
|
634
|
+
jmax = int(min(jmax + 1, self.labeled_array.shape[1] - 1))
|
635
|
+
|
636
|
+
ref_crop = self.array_reference.crop(imin, jmin, imax-imin+1, jmax-jmin+1)
|
637
|
+
to_compare_crop = self.array_to_compare.crop(imin, jmin, imax-imin+1, jmax-jmin+1)
|
638
|
+
|
639
|
+
ref_crop.array.mask[:,:] = ~mask[imin:imax+1, jmin:jmax+1]
|
640
|
+
ref_crop.set_nullvalue_in_mask()
|
641
|
+
|
642
|
+
to_compare_crop.array.mask[:,:] = ~mask[imin:imax+1, jmin:jmax+1]
|
643
|
+
to_compare_crop.set_nullvalue_in_mask()
|
644
|
+
|
645
|
+
ref_crop.count()
|
646
|
+
to_compare_crop.nbnotnull = ref_crop.nbnotnull
|
647
|
+
|
648
|
+
self.difference_parts[idx_feature] = ArrayDifference(ref_crop, to_compare_crop, idx_feature, self.labeled_array[imin:imax+1, jmin:jmax+1].copy())
|
649
|
+
|
650
|
+
return self.labeled_array
|
651
|
+
|
652
|
+
@property
|
653
|
+
def summary_text(self) -> list[str]:
|
654
|
+
"""
|
655
|
+
Generate a summary text for the report.
|
656
|
+
"""
|
657
|
+
|
658
|
+
diff = self.difference.array.compressed()
|
659
|
+
text_left = [
|
660
|
+
_("Number of features: ") + str(self.num_features),
|
661
|
+
_('Resolution: ') + f"{self.array_reference.dx} m x {self.array_reference.dy} m",
|
662
|
+
_('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})",
|
663
|
+
_('Width x Height: ') + f"{self.array_reference.nbx * self.array_reference.dx} m x {self.array_reference.nby * self.array_reference.dy} m",
|
664
|
+
]
|
665
|
+
text_right = [
|
666
|
+
_('Excavation: ') + f"{np.sum(diff[diff < 0.]) * self.array_reference.dx * self.array_reference.dy:.3f} m³",
|
667
|
+
_('Deposit/Backfill: ') + f"{np.sum(diff[diff > 0.]) * self.array_reference.dx * self.array_reference.dy:.3f} m³",
|
668
|
+
_('Net volume: ') + f"{np.sum(diff) * self.array_reference.dx * self.array_reference.dy:.3f} m³",
|
669
|
+
]
|
670
|
+
return text_left, text_right
|
671
|
+
|
672
|
+
def _complete_report(self, layout:CompareArraysLayout):
|
673
|
+
""" Complete the report with the global summary and individual differences. """
|
674
|
+
|
675
|
+
key_fig = [('Arrays_0-0', self.plot_reference),
|
676
|
+
('Arrays_1-0', self.plot_to_compare),
|
677
|
+
('Difference', self.plot_difference),]
|
678
|
+
|
679
|
+
keys = layout.keys
|
680
|
+
for key, fig_routine in key_fig:
|
681
|
+
if key in keys:
|
682
|
+
|
683
|
+
rect = layout._layout[key]
|
684
|
+
|
685
|
+
fig, ax = fig_routine()
|
686
|
+
|
687
|
+
# set size to fit the rectangle
|
688
|
+
fig.set_size_inches(pt2inches(rect.width), pt2inches(rect.height))
|
689
|
+
|
690
|
+
# convert canvas to PNG and insert it into the PDF
|
691
|
+
temp_file = NamedTemporaryFile(delete=False, suffix='.png')
|
692
|
+
fig.savefig(temp_file, format='png', bbox_inches='tight', dpi=self._dpi)
|
693
|
+
layout._page.insert_image(layout._layout[key], filename=temp_file.name)
|
694
|
+
# delete the temporary file
|
695
|
+
temp_file.delete = True
|
696
|
+
temp_file.close()
|
697
|
+
|
698
|
+
# Force to delete fig
|
699
|
+
plt.close(fig)
|
700
|
+
else:
|
701
|
+
logging.warning(f"Key {key} not found in layout. Skipping plot.")
|
702
|
+
|
703
|
+
tleft, tright = self.summary_text
|
704
|
+
|
705
|
+
rect = layout._layout['Summary_0-0']
|
706
|
+
text_left, css_left = list_to_html(tleft, font_size='8pt')
|
707
|
+
layout._page.insert_htmlbox(rect, text_left, css=css_left)
|
708
|
+
rect = layout._layout['Summary_1-0']
|
709
|
+
text_right, css_right = list_to_html(tright, font_size='8pt')
|
710
|
+
layout._page.insert_htmlbox(rect, text_right, css=css_right)
|
711
|
+
|
712
|
+
def plot_histogram_features(self, figax:tuple[plt.Figure, plt.Axes]=None, density = True, alpha = 0.5, **kwargs) -> tuple[plt.Figure, plt.Axes]:
|
713
|
+
"""
|
714
|
+
Plot histogram of the number of cells in each feature.
|
715
|
+
"""
|
716
|
+
if figax is None:
|
717
|
+
figax = plt.subplots()
|
718
|
+
|
719
|
+
fig, ax = figax
|
720
|
+
|
721
|
+
surf = self.array_reference.dx * self.array_reference.dy
|
722
|
+
|
723
|
+
# Extract the number of cells for each feature
|
724
|
+
nb_cells = [item[0] * surf for item in self.nb_cells]
|
725
|
+
|
726
|
+
ax.hist(nb_cells, density=density, alpha=alpha, **kwargs)
|
727
|
+
|
728
|
+
ax.set_title(_("Histogram of surface in each feature [m²]"))
|
729
|
+
|
730
|
+
# set font size of the labels
|
731
|
+
ax.tick_params(axis='both', which='major', labelsize=6)
|
732
|
+
for label in ax.get_xticklabels():
|
733
|
+
label.set_fontsize(self._fontsize)
|
734
|
+
for label in ax.get_yticklabels():
|
735
|
+
label.set_fontsize(self._fontsize)
|
736
|
+
# and gfor the label title
|
737
|
+
ax.set_xlabel(ax.get_xlabel(), fontsize=self._fontsize)
|
738
|
+
ax.set_ylabel(ax.get_ylabel(), fontsize=self._fontsize)
|
739
|
+
|
740
|
+
return fig, ax
|
741
|
+
|
742
|
+
def plot_histogram_features_difference(self, figax:tuple[plt.Figure, plt.Axes]=None, density = True, alpha = 1.0, **kwargs) -> tuple[plt.Figure, plt.Axes]:
|
743
|
+
"""
|
744
|
+
Plot histogram of the volume in each feature for the difference.
|
745
|
+
"""
|
746
|
+
if figax is None:
|
747
|
+
figax = plt.subplots()
|
748
|
+
|
749
|
+
fig, ax = figax
|
750
|
+
|
751
|
+
# Calculate the difference between the two arrays
|
752
|
+
diff = self.difference
|
753
|
+
|
754
|
+
volumes = np.bincount(self.labeled_array.ravel(), weights=diff.array.ravel()) * self.array_reference.dx * self.array_reference.dy
|
755
|
+
|
756
|
+
# Create a histogram of the differences
|
757
|
+
ax.hist(volumes, density=density, alpha=alpha, **kwargs)
|
758
|
+
|
759
|
+
ax.set_title(_("Histogram of net volumes [m³]"))
|
760
|
+
|
761
|
+
# set font size of the labels
|
762
|
+
ax.tick_params(axis='both', which='major', labelsize=6)
|
763
|
+
for label in ax.get_xticklabels():
|
764
|
+
label.set_fontsize(self._fontsize)
|
765
|
+
for label in ax.get_yticklabels():
|
766
|
+
label.set_fontsize(self._fontsize)
|
767
|
+
# and gfor the label title
|
768
|
+
ax.set_xlabel(ax.get_xlabel(), fontsize=self._fontsize)
|
769
|
+
ax.set_ylabel(ax.get_ylabel(), fontsize=self._fontsize)
|
770
|
+
|
771
|
+
fig.tight_layout()
|
772
|
+
|
773
|
+
return fig, ax
|
774
|
+
|
775
|
+
def _complete_report2(self, layout:CompareArraysLayout):
|
776
|
+
""" Complete the report with the individual differences. """
|
777
|
+
|
778
|
+
key_fig = [('Histogram_0-0', self.plot_histogram_features),
|
779
|
+
('Histogram_1-0', self.plot_histogram_features_difference),
|
780
|
+
('Arrays_0-0', self.plot_position),
|
781
|
+
('Arrays_1-0', self.plot_cartoweb),
|
782
|
+
('Position', self.plot_topo_grey),
|
783
|
+
]
|
784
|
+
|
785
|
+
keys = layout.keys
|
786
|
+
for key, fig_routine in key_fig:
|
787
|
+
if key in keys:
|
788
|
+
|
789
|
+
rect = layout._layout[key]
|
790
|
+
|
791
|
+
fig, ax = fig_routine()
|
792
|
+
|
793
|
+
# set size to fit the rectangle
|
794
|
+
fig.set_size_inches(pt2inches(rect.width), pt2inches(rect.height))
|
795
|
+
fig.tight_layout()
|
796
|
+
|
797
|
+
# convert canvas to PNG and insert it into the PDF
|
798
|
+
temp_file = NamedTemporaryFile(delete=False, suffix='.png')
|
799
|
+
fig.savefig(temp_file, format='png', bbox_inches='tight', dpi=self._dpi)
|
800
|
+
layout._page.insert_image(layout._layout[key], filename=temp_file.name)
|
801
|
+
# delete the temporary file
|
802
|
+
temp_file.delete = True
|
803
|
+
temp_file.close()
|
804
|
+
|
805
|
+
# Force to delete fig
|
806
|
+
plt.close(fig)
|
807
|
+
else:
|
808
|
+
logging.warning(f"Key {key} not found in layout. Skipping plot.")
|
809
|
+
|
810
|
+
|
811
|
+
|
812
|
+
def create_report(self, output_file: str | Path = None, append_all_differences: bool = True) -> None:
|
813
|
+
""" Create a page report for the array comparison. """
|
814
|
+
|
815
|
+
if output_file is None:
|
816
|
+
output_file = Path(f"compare_arrays_report.pdf")
|
817
|
+
|
818
|
+
if output_file.exists():
|
819
|
+
logging.warning(f"Output file {output_file} already exists. It will be overwritten.")
|
820
|
+
|
821
|
+
layout = CompareArraysLayout("Comparison Report")
|
822
|
+
layout.create_report()
|
823
|
+
self._complete_report(layout)
|
824
|
+
|
825
|
+
|
826
|
+
with TemporaryDirectory() as temp_dir:
|
827
|
+
|
828
|
+
layout2 = CompareArraysLayout2("Distribution of Differences")
|
829
|
+
layout2.create_report()
|
830
|
+
self._complete_report2(layout2)
|
831
|
+
layout2.save_report(Path(temp_dir) / "distribution_of_differences.pdf")
|
832
|
+
all_pdfs = [Path(temp_dir) / "distribution_of_differences.pdf"]
|
833
|
+
|
834
|
+
if append_all_differences:
|
835
|
+
# Add each difference report to the main layout
|
836
|
+
all_pdfs.extend([diff.create_report(Path(temp_dir) / f"array_difference_{idx}.pdf") for idx, diff in self.difference_parts.items()])
|
837
|
+
|
838
|
+
for pdf_file in all_pdfs:
|
839
|
+
layout._doc.insert_file(pdf_file)
|
840
|
+
|
841
|
+
# create a TOC
|
842
|
+
layout._doc.set_toc(layout._doc.get_toc())
|
843
|
+
|
844
|
+
layout.save_report(output_file)
|