import logging
import numpy as np
import numpy.ma as ma
from pathlib import Path
import matplotlib.pyplot as plt
from enum import Enum
from scipy.ndimage import label, sum_labels, find_objects
import pymupdf as pdf
import wx
from tqdm import tqdm
from matplotlib import use, get_backend
from .common import A4_rect, rect_cm, list_to_html, list_to_html_aligned, get_rect_from_text
from .common import inches2cm, pts2cm, cm2pts, cm2inches, DefaultLayoutA4, NamedTemporaryFile, pt2inches, TemporaryDirectory
from ..wolf_array import WolfArray, header_wolf, vector, zone, Zones, wolfvertex as wv, wolfpalette
from ..PyTranslate import _
from .pdf import PDFViewer
[docs]
class ArrayDifferenceLayout(DefaultLayoutA4):
"""
Layout for comparing two arrays in a report.
1 cadre pour la zone traitée avec photo de fond ign + contour vectoriel
1 cadre avec zoom plus large min 250m
1 cadre avec matrice ref + contour vectoriel
1 cadre avec matrice à comparer + contour vectoriel
1 cadre avec différence
1 cadre avec valeurs de synthèse
1 cadre avec histogramme
1 cadre avec histogramme des différences
"""
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):
super().__init__(title, filename, ox, oy, tx, ty, parent, is2D, idx, plotted, mapviewer, need_for_wx, bbox, find_minmax, shared, colors)
useful = self.useful_part
width = useful.xmax - useful.xmin
height = useful.ymax - useful.ymin
[docs]
self._hitograms = self.add_element_repeated(_("Histogram"), width=width, height=2.5,
first_x=useful.xmin, first_y=useful.ymax,
count_x=1, count_y=-2, padding=0.5)
[docs]
self._arrays = self.add_element_repeated(_("Arrays"), width= (width-self.padding) / 2, height=5.5,
first_x=useful.xmin, first_y=self._hitograms.ymin - self.padding,
count_x=2, count_y=-3, padding=0.5)
[docs]
class CompareArraysLayout(DefaultLayoutA4):
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):
super().__init__(title, filename, ox, oy, tx, ty, parent, is2D, idx, plotted, mapviewer, need_for_wx, bbox, find_minmax, shared, colors)
useful = self.useful_part
width = useful.xmax - useful.xmin
height = useful.ymax - useful.ymin
[docs]
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)
[docs]
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)
[docs]
self._diff_rect = self.add_element(_("Difference"), width= width, height=11.5, x=useful.xmin, y=useful.ymin)
[docs]
class CompareArraysLayout2(DefaultLayoutA4):
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):
super().__init__(title, filename, ox, oy, tx, ty, parent, is2D, idx, plotted, mapviewer, need_for_wx, bbox, find_minmax, shared, colors)
useful = self.useful_part
width = useful.xmax - useful.xmin
height = useful.ymax - useful.ymin
[docs]
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)
[docs]
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)
[docs]
self._diff_rect = self.add_element(_("Position"), width= width, height=11.5, x=useful.xmin, y=useful.ymin)
[docs]
class ArrayDifference():
"""
Class to manage the difference between two WolfArray objects.
"""
def __init__(self, reference:WolfArray, to_compare:WolfArray, index:int, label:np.ndarray):
[docs]
self.default_size_hitograms = (12, 6)
[docs]
self.default_size_arrays = (10, 10)
[docs]
self.reference = reference
[docs]
self.to_compare = to_compare
self.reference.updatepalette()
self.to_compare.mypal = self.reference.mypal
[docs]
self._background = 'IGN'
[docs]
self._external_border = None
@property
[docs]
def contour(self) -> vector:
""" Get the contour of the difference part. """
if self._contour is not None and isinstance(self._contour, vector):
return self._contour
ret = self.reference.suxsuy_contour(abs=True)
ret = ret[2]
ret.myprop.color = (0, 0, 255)
ret.myprop.width = 2
return ret
@property
[docs]
def external_border(self) -> vector:
"""
Get the bounds of the difference part.
"""
if self._external_border is not None and isinstance(self._external_border, vector):
return self._external_border
ret = vector(name=_("External border"))
(xmin, xmax), (ymin, ymax) = self.reference.get_bounds()
ret.add_vertex(wv(xmin, ymin))
ret.add_vertex(wv(xmax, ymin))
ret.add_vertex(wv(xmax, ymax))
ret.add_vertex(wv(xmin, ymax))
ret.force_to_close()
ret.myprop.color = (255, 0, 0)
ret.myprop.width = 3
return ret
def __str__(self):
assert self.reference.nbnotnull == self.to_compare.nbnotnull, "The number of non-null cells in both arrays must be the same."
ret = self.reference.__str__() + '\n'
ret += _("Index : ") + str(self.index) + '\n'
ret += _("Number of cells : ") + str(self.reference.nbnotnull) + '\n'
return ret
@property
[docs]
def _summary_text(self):
"""
Generate a summary text for the report.
"""
diff = self.difference.array.compressed()
text = [
_("Index: ") + str(self.index),
_("Number of cells: ") + str(self.reference.nbnotnull),
_('Resolution: ') + f"{self.reference.dx} m x {self.reference.dy} m",
_('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})",
_('Width x Height: ') + f"{self.reference.nbx * self.reference.dx} m x {self.reference.nby * self.reference.dy} m",
_('Excavation: ') + f"{np.sum(diff[diff < 0.]) * self.reference.dx * self.reference.dy:.3f} m³",
_('Deposit/Backfill: ') + f"{np.sum(diff[diff > 0.]) * self.reference.dx * self.reference.dy:.3f} m³",
_('Net volume: ') + f"{np.sum(diff) * self.reference.dx * self.reference.dy:.3f} m³",
]
return text
[docs]
def set_palette_distribute(self, minval:float, maxval:float, step:int=0):
"""
Set the palette for both arrays.
"""
self.reference.mypal.distribute_values(minval, maxval, step)
[docs]
def set_palette(self, values:list[float], colors:list[tuple[int, int, int]]):
"""
Set the palette for both arrays based on specific values.
"""
self.reference.mypal.set_values_colors(values, colors)
[docs]
def plot_position(self, figax:tuple[plt.Figure, plt.Axes]=None) -> tuple[plt.Figure, plt.Axes]:
"""
Plot the reference array.
"""
if figax is None:
figax = plt.subplots()
fig, ax = figax
old_mask = self.reference.array.mask.copy()
self.reference.array.mask[:,:] = True
if self._background.upper() == 'IGN' or self._background.upper() == 'NGI':
self.reference.plot_matplotlib(figax=figax, figsize = self.default_size_arrays,
first_mask_data=False, with_legend=False,
update_palette= False,
IGN= True,
cat = 'orthoimage_coverage',
)
elif self._background.upper() == 'WALONMAP':
self.reference.plot_matplotlib(figax=figax, figsize = self.default_size_arrays,
first_mask_data=False, with_legend=False,
update_palette= False,
Walonmap= True,
cat = 'IMAGERIE/ORTHO_2022_ETE',
)
else:
self.reference.plot_matplotlib(figax=figax, figsize = self.default_size_arrays,
first_mask_data=False, with_legend=False,
update_palette= False,
Walonmap= False,
)
self.reference.array.mask[:,:] = old_mask
self.external_border.plot_matplotlib(ax=ax)
self.contour.plot_matplotlib(ax=ax)
return fig, ax
[docs]
def plot_position_scaled(self, scale = 4, figax:tuple[plt.Figure, plt.Axes]=None) -> tuple[plt.Figure, plt.Axes]:
"""
Plot the reference array.
"""
if figax is None:
figax = plt.subplots()
fig, ax = figax
h = self.reference.get_header()
width = h.nbx * h.dx
height = h.nby * h.dy
h.origx += -width * scale / 2
h.origy += -height *scale / 2
h.nbx = 1
h.nby = 1
h.dx = width *(scale + 1)
h.dy = height *(scale + 1)
new = WolfArray(srcheader=h)
new.array.mask[:,:] = True
if self._background.upper() == 'IGN' or self._background.upper() == 'NGI':
new.plot_matplotlib(figax=figax, figsize = self.default_size_arrays,
first_mask_data=False, with_legend=False,
update_palette= False,
IGN= True,
cat = 'orthoimage_coverage')
elif self._background.upper() == 'WALONMAP':
new.plot_matplotlib(figax=figax, figsize = self.default_size_arrays,
first_mask_data=False, with_legend=False,
update_palette= False,
Walonmap= True,
cat = 'IMAGERIE/ORTHO_2022_ETE')
else:
new.plot_matplotlib(figax=figax, figsize = self.default_size_arrays,
first_mask_data=False, with_legend=False,
update_palette= False,
Walonmap= False)
self.external_border.plot_matplotlib(ax=ax)
return fig, ax
[docs]
def plot_reference(self, figax:tuple[plt.Figure, plt.Axes]=None) -> tuple[plt.Figure, plt.Axes]:
"""
Plot the reference array.
"""
if figax is None:
figax = plt.subplots()
fig, ax = figax
self.reference.plot_matplotlib(figax=figax, figsize = self.default_size_arrays, first_mask_data=False, with_legend=True, update_palette= False)
return fig, ax
[docs]
def plot_to_compare(self, figax:tuple[plt.Figure, plt.Axes]=None) -> tuple[plt.Figure, plt.Axes]:
"""
Plot the array to compare.
"""
if figax is None:
figax = plt.subplots()
fig, ax = figax
self.to_compare.plot_matplotlib(figax=figax, figsize = self.default_size_arrays, first_mask_data=False, with_legend=True, update_palette= False)
return fig, ax
@property
[docs]
def difference(self) -> WolfArray:
"""
Get the difference between the two arrays.
"""
if not isinstance(self.reference, WolfArray) or not isinstance(self.to_compare, WolfArray):
raise TypeError("Both inputs must be instances of WolfArray")
return self.to_compare - self.reference
[docs]
def plot_difference(self, figax:tuple[plt.Figure, plt.Axes]=None) -> tuple[plt.Figure, plt.Axes]:
"""
Plot the array to compare.
"""
if figax is None:
figax = plt.subplots()
fig, ax = figax
pal = wolfpalette()
pal.default_difference3()
diff = self.difference
diff.mypal = pal
diff.plot_matplotlib(figax=figax, figsize = self.default_size_arrays, first_mask_data=False, with_legend=True, update_palette= False)
return fig, ax
[docs]
def _plot_histogram_reference(self, figax:tuple[plt.Figure, plt.Axes]=None, density = True, alpha = 0.5, **kwargs) -> tuple[plt.Figure, plt.Axes]:
"""
Plot histogram of the reference array.
"""
if figax is None:
figax = plt.subplots()
fig, ax = figax
data = self.reference.array.compressed()
ax.hist(data, bins = min(100, int(len(data)/4)), density=density, alpha = alpha, **kwargs)
# ax.set_xlabel("Value")
# ax.set_ylabel("Frequency")
return fig, ax
[docs]
def _plot_histogram_to_compare(self, figax:tuple[plt.Figure, plt.Axes]=None, density = True, alpha = 0.5, **kwargs) -> tuple[plt.Figure, plt.Axes]:
"""
Plot histogram of the array to compare.
"""
if figax is None:
figax = plt.subplots()
fig, ax = figax
data = self.to_compare.array.compressed()
ax.hist(data, bins= min(100, int(len(data)/4)), density=density, alpha = alpha, **kwargs)
# ax.set_xlabel("Value")
# ax.set_ylabel("Frequency")
return fig, ax
[docs]
def plot_histograms(self, figax:tuple[plt.Figure, plt.Axes]=None, density = True, alpha = 0.5, **kwargs) -> tuple[plt.Figure, plt.Axes]:
"""
Plot histograms of both arrays.
"""
if figax is None:
figax = plt.subplots(1, 1, figsize=self.default_size_hitograms)
fig, ax = figax
self._plot_histogram_reference((fig, ax), density = density, alpha=alpha, **kwargs)
self._plot_histogram_to_compare((fig, ax), density = density, alpha=alpha, **kwargs)
# set font size of the labels
ax.tick_params(axis='both', which='major', labelsize=6)
for label in ax.get_xticklabels():
label.set_fontsize(self._fontsize)
for label in ax.get_yticklabels():
label.set_fontsize(self._fontsize)
# and gfor the label title
ax.set_xlabel(ax.get_xlabel(), fontsize=self._fontsize)
ax.set_ylabel(ax.get_ylabel(), fontsize=self._fontsize)
fig.tight_layout()
return fig, ax
[docs]
def plot_histograms_difference(self, figax:tuple[plt.Figure, plt.Axes]=None, density = True, alpha = 1.0, **kwargs) -> tuple[plt.Figure, plt.Axes]:
"""
Plot histogram of the difference between the two arrays.
"""
if figax is None:
figax = plt.subplots(figsize=self.default_size_hitograms)
fig, ax = figax
difference_data = self.difference.array.compressed()
ax.hist(difference_data, bins= min(100, int(len(difference_data)/4)), density=density, alpha=alpha, **kwargs)
# ax.set_xlabel("Value")
# ax.set_ylabel("Frequency")
# set font size of the labels
ax.tick_params(axis='both', which='major', labelsize=6)
for label in ax.get_xticklabels():
label.set_fontsize(self._fontsize)
for label in ax.get_yticklabels():
label.set_fontsize(self._fontsize)
# and gfor the label title
ax.set_xlabel(ax.get_xlabel(), fontsize=self._fontsize)
ax.set_ylabel(ax.get_ylabel(), fontsize=self._fontsize)
return fig, ax
[docs]
def _complete_report(self, layout:ArrayDifferenceLayout):
"""
Complete the report with the arrays and histograms.
"""
useful = layout.useful_part
# Plot reference array
key_fig = [('Histogram_0-0', self.plot_histograms),
('Histogram_0-1', self.plot_histograms_difference),
('Arrays_0-0', self.plot_position),
('Arrays_1-0', self.plot_position_scaled),
('Arrays_0-1', self.plot_reference),
('Arrays_1-1', self.plot_to_compare),
('Arrays_0-2', self.plot_difference),]
keys = layout.keys
for key, fig_routine in key_fig:
if key in keys:
rect = layout._layout[key]
fig, ax = fig_routine()
# set size to fit the rectangle
fig.set_size_inches(pt2inches(rect.width), pt2inches(rect.height))
if 'Histogram' in key:
fig.tight_layout()
# convert canvas to PNG and insert it into the PDF
temp_file = NamedTemporaryFile(delete=False, suffix='.png')
fig.savefig(temp_file, format='png', bbox_inches='tight', dpi=self._dpi)
layout._page.insert_image(layout._layout[key], filename = temp_file.name)
# delete the temporary file
temp_file.delete = True
temp_file.close()
# Force to delete fig
plt.close(fig)
else:
logging.warning(f"Key {key} not found in layout. Skipping plot.")
key = 'Arrays_1-2'
if key in keys:
text, css = list_to_html(self._summary_text, font_size='8pt')
layout._page.insert_htmlbox(layout._layout[key], text,
css=css)
[docs]
def create_report(self, output_file: str | Path = None) -> Path:
""" Create a page report for the array difference. """
from time import sleep
if output_file is None:
output_file = Path(f"array_difference_{self.index}.pdf")
if output_file.exists():
logging.warning(f"Output file {output_file} already exists. It will be overwritten.")
layout = ArrayDifferenceLayout(f"Differences - Index n°{self.index}")
layout.create_report()
self._complete_report(layout)
layout.save_report(output_file)
sleep(0.2) # Ensure the file is saved before returning
return output_file
[docs]
class CompareArrays:
def __init__(self, reference: WolfArray | str | Path, to_compare: WolfArray | str | Path):
[docs]
self.default_size_arrays = (10, 10)
if isinstance(reference, (str, Path)):
reference = WolfArray(reference)
if isinstance(to_compare, (str, Path)):
to_compare = WolfArray(to_compare)
if not reference.is_like(to_compare):
raise ValueError("Arrays are not compatible for comparison")
[docs]
self.array_reference:WolfArray
[docs]
self.array_to_compare:WolfArray
self.array_reference = reference
self.array_to_compare = to_compare
[docs]
self.labeled_array: np.ndarray = None
[docs]
self.num_features: int = 0
[docs]
self.nb_cells: list = []
[docs]
self.difference_parts:dict[int, ArrayDifference] = {}
[docs]
self._background = 'IGN'
@property
[docs]
def difference(self) -> WolfArray:
if not isinstance(self.array_reference, WolfArray) or not isinstance(self.array_to_compare, WolfArray):
raise TypeError("Both inputs must be instances of WolfArray")
return self.array_to_compare - self.array_reference
[docs]
def get_zones(self):
"""
Get a Zones object containing the differences.
"""
ret_zones = Zones()
exterior = zone(name=_("External border"))
contours = zone(name=_("Contours"))
ret_zones.add_zone(exterior, forceparent=True)
ret_zones.add_zone(contours, forceparent=True)
for diff in self.difference_parts.values():
exterior.add_vector(diff.external_border, forceparent=True)
contours.add_vector(diff.contour, forceparent=True)
return ret_zones
[docs]
def plot_position(self, figax:tuple[plt.Figure, plt.Axes]=None) -> tuple[plt.Figure, plt.Axes]:
"""
Plot the reference array with a background.
"""
if figax is None:
figax = plt.subplots()
fig, ax = figax
h = self.array_reference.get_header()
width = h.nbx * h.dx
height = h.nby * h.dy
h.dx = width
h.dy = height
h.nbx = 1
h.nby = 1
new = WolfArray(srcheader=h)
new.array.mask[:,:] = True
if self._background.upper() == 'IGN' or self._background.upper() == 'NGI':
new.plot_matplotlib(figax=figax, figsize = self.default_size_arrays,
first_mask_data=False, with_legend=False,
update_palette= False,
IGN= True,
cat = 'orthoimage_coverage',
)
elif self._background.upper() == 'WALONMAP':
new.plot_matplotlib(figax=figax, figsize = self.default_size_arrays,
first_mask_data=False, with_legend=False,
update_palette= False,
Walonmap= True,
cat = 'IMAGERIE/ORTHO_2022_ETE',
)
else:
new.plot_matplotlib(figax=figax, figsize = self.default_size_arrays,
first_mask_data=False, with_legend=False,
update_palette= False,
Walonmap= False,
)
return fig, ax
[docs]
def plot_cartoweb(self, figax:tuple[plt.Figure, plt.Axes]=None) -> tuple[plt.Figure, plt.Axes]:
"""
Plot the reference array with a background.
"""
if figax is None:
figax = plt.subplots()
fig, ax = figax
h = self.array_reference.get_header()
width = h.nbx * h.dx
height = h.nby * h.dy
h.dx = width
h.dy = height
h.nbx = 1
h.nby = 1
new = WolfArray(srcheader=h)
new.array.mask[:,:] = True
if self._background.upper() == 'IGN' or self._background.upper() == 'NGI':
new.plot_matplotlib(figax=figax, figsize = self.default_size_arrays,
first_mask_data=False, with_legend=False,
update_palette= False,
Cartoweb= True,
cat = 'overlay',
)
else:
new.plot_matplotlib(figax=figax, figsize = self.default_size_arrays,
first_mask_data=False, with_legend=False,
update_palette= False,
Cartoweb= False,
cat = 'overlay',
)
return fig, ax
[docs]
def plot_topo_grey(self, figax:tuple[plt.Figure, plt.Axes]=None) -> tuple[plt.Figure, plt.Axes]:
"""
Plot the reference array with a background.
"""
if figax is None:
figax = plt.subplots()
fig, ax = figax
h = self.array_reference.get_header()
width = h.nbx * h.dx
height = h.nby * h.dy
h.dx = width
h.dy = height
h.nbx = 1
h.nby = 1
new = WolfArray(srcheader=h)
new.array.mask[:,:] = True
if self._background.upper() == 'IGN' or self._background.upper() == 'NGI':
new.plot_matplotlib(figax=figax, figsize = self.default_size_arrays,
first_mask_data=False, with_legend=False,
update_palette= False,
Cartoweb= True,
cat = 'topo_grey',
)
else:
new.plot_matplotlib(figax=figax, figsize = self.default_size_arrays,
first_mask_data=False, with_legend=False,
update_palette= False,
Cartoweb= False,
cat = 'topo_grey',
)
return fig, ax
[docs]
def plot_reference(self, figax:tuple[plt.Figure, plt.Axes]=None) -> tuple[plt.Figure, plt.Axes]:
"""
Plot the reference array.
"""
if figax is None:
figax = plt.subplots()
fig, ax = figax
self.array_reference.plot_matplotlib(figax=figax, figsize = self.default_size_arrays, first_mask_data=False, with_legend=True, update_palette= False)
for diff in tqdm(self.difference_parts.values(), desc="Plotting external borders"):
diff.external_border.plot_matplotlib(ax=ax)
return fig, ax
[docs]
def plot_to_compare(self, figax:tuple[plt.Figure, plt.Axes]=None) -> tuple[plt.Figure, plt.Axes]:
"""
Plot the array to compare.
"""
if figax is None:
figax = plt.subplots()
fig, ax = figax
self.array_to_compare.plot_matplotlib(figax=figax, figsize = self.default_size_arrays, first_mask_data=False, with_legend=True, update_palette= False)
for diff in tqdm(self.difference_parts.values(), desc="Plotting contours"):
diff.contour.plot_matplotlib(ax=ax)
return fig, ax
[docs]
def plot_difference(self, figax:tuple[plt.Figure, plt.Axes]=None) -> tuple[plt.Figure, plt.Axes]:
"""
Plot the difference between the two arrays.
"""
if figax is None:
figax = plt.subplots()
fig, ax = figax
pal = wolfpalette()
pal.default_difference3()
diff = self.difference
diff.mypal = pal
diff.plot_matplotlib(figax=figax, figsize = self.default_size_arrays, first_mask_data=False, with_legend=True, update_palette= False)
return fig, ax
[docs]
def localize_differences(self, threshold: float = 0.0,
ignored_patche_area:float = 1.) -> np.ndarray:
""" Localize the differences between the two arrays and label them.
:param threshold: The threshold value to consider a difference significant.
:param ignored_patche_area: The area of patches to ignore (in m²).
"""
assert threshold >= 0, "Threshold must be a non-negative value."
labeled_array = self.difference.array.data.copy()
labeled_array[self.array_reference.array.mask] = 0
# apply threshold
labeled_array[np.abs(labeled_array) < threshold] = 0
self.labeled_array, self.num_features = label(labeled_array)
self.nb_cells = []
self.nb_cells = list(sum_labels(np.ones(self.labeled_array.shape, dtype=np.int32), self.labeled_array, range(1, self.num_features+1)))
self.nb_cells = [[self.nb_cells[j], j+1] for j in range(0, self.num_features)]
self.nb_cells.sort(key=lambda x: x[0], reverse=True)
# find features where nb_cells is lower than ignored_patche_area / (dx * dy)
ignored_patche_cells = int(ignored_patche_area / (self.array_reference.dx * self.array_reference.dy))
self.last_features = self.num_features
for idx, (nb_cell, idx_feature) in enumerate(self.nb_cells):
if nb_cell <= ignored_patche_cells:
self.last_features = idx
break
all_slices = find_objects(self.labeled_array)
logging.info(f"Total number of features found: {self.last_features}")
# find xmin, ymin, xmax, ymax for each feature
for idx_feature, slices in tqdm(zip(range(1, self.num_features+1), all_slices), desc="Processing features", unit="feature"):
mask = self.labeled_array[slices] == idx_feature
nb_in_patch = np.count_nonzero(mask)
if nb_in_patch <= ignored_patche_cells:
logging.debug(f"Feature {idx_feature} has too few cells ({np.count_nonzero(mask)}) and will be ignored.")
continue
imin, imax = slices[0].start, slices[0].stop - 1
jmin, jmax = slices[1].start, slices[1].stop - 1
imin = int(max(imin - 1, 0))
imax = int(min(imax + 1, self.labeled_array.shape[0] - 1))
jmin = int(max(jmin - 1, 0))
jmax = int(min(jmax + 1, self.labeled_array.shape[1] - 1))
ref_crop = self.array_reference.crop(imin, jmin, imax-imin+1, jmax-jmin+1)
to_compare_crop = self.array_to_compare.crop(imin, jmin, imax-imin+1, jmax-jmin+1)
label_crop = self.labeled_array[imin:imax+1, jmin:jmax+1].copy()
to_compare_crop.array.mask[:,:] = ref_crop.array.mask[:,:] = self.labeled_array[imin:imax+1, jmin:jmax+1] != idx_feature
ref_crop.set_nullvalue_in_mask()
to_compare_crop.set_nullvalue_in_mask()
label_crop[label_crop != idx_feature] = 0
ref_crop.nbnotnull = nb_in_patch
to_compare_crop.nbnotnull = nb_in_patch
self.difference_parts[idx_feature] = ArrayDifference(ref_crop, to_compare_crop, idx_feature, label_crop)
assert self.last_features == len(self.difference_parts), \
f"Last feature index {self.last_features} does not match the number of differences found"
self.num_features = self.last_features
logging.info(f"Number of features after filtering: {self.num_features}")
return self.labeled_array
@property
[docs]
def summary_text(self) -> list[str]:
"""
Generate a summary text for the report.
"""
diff = self.difference.array.compressed()
text_left = [
_("Number of features: ") + str(self.num_features),
_('Resolution: ') + f"{self.array_reference.dx} m x {self.array_reference.dy} m",
_('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})",
_('Width x Height: ') + f"{self.array_reference.nbx * self.array_reference.dx} m x {self.array_reference.nby * self.array_reference.dy} m",
]
text_right = [
_('Excavation: ') + f"{np.sum(diff[diff < 0.]) * self.array_reference.dx * self.array_reference.dy:.3f} m³",
_('Deposit/Backfill: ') + f"{np.sum(diff[diff > 0.]) * self.array_reference.dx * self.array_reference.dy:.3f} m³",
_('Net volume: ') + f"{np.sum(diff) * self.array_reference.dx * self.array_reference.dy:.3f} m³",
]
return text_left, text_right
[docs]
def _complete_report(self, layout:CompareArraysLayout):
""" Complete the report with the global summary and individual differences. """
key_fig = [('Arrays_0-0', self.plot_reference),
('Arrays_1-0', self.plot_to_compare),
('Difference', self.plot_difference),]
keys = layout.keys
for key, fig_routine in key_fig:
if key in keys:
rect = layout._layout[key]
fig, ax = fig_routine()
# set size to fit the rectangle
fig.set_size_inches(pt2inches(rect.width), pt2inches(rect.height))
# convert canvas to PNG and insert it into the PDF
temp_file = NamedTemporaryFile(delete=False, suffix='.png')
fig.savefig(temp_file, format='png', bbox_inches='tight', dpi=self._dpi)
layout._page.insert_image(layout._layout[key], filename=temp_file.name)
# delete the temporary file
temp_file.delete = True
temp_file.close()
# Force to delete fig
plt.close(fig)
else:
logging.warning(f"Key {key} not found in layout. Skipping plot.")
tleft, tright = self.summary_text
rect = layout._layout['Summary_0-0']
text_left, css_left = list_to_html(tleft, font_size='8pt')
layout._page.insert_htmlbox(rect, text_left, css=css_left)
rect = layout._layout['Summary_1-0']
text_right, css_right = list_to_html(tright, font_size='8pt')
layout._page.insert_htmlbox(rect, text_right, css=css_right)
[docs]
def plot_histogram_features(self, figax:tuple[plt.Figure, plt.Axes]=None, density = True, alpha = 0.5, **kwargs) -> tuple[plt.Figure, plt.Axes]:
"""
Plot histogram of the number of cells in each feature.
"""
if figax is None:
figax = plt.subplots()
fig, ax = figax
surf = self.array_reference.dx * self.array_reference.dy
# Extract the number of cells for each feature
nb_cells = [item[0] * surf for item in self.nb_cells[:self.last_features]]
if len(nb_cells) > 0:
ax.hist(nb_cells, bins= min(100, int(len(nb_cells)/4)), density=density, alpha=alpha, **kwargs)
ax.set_title(_("Histogram of surface in each feature [m²]"))
# set font size of the labels
ax.tick_params(axis='both', which='major', labelsize=6)
for label in ax.get_xticklabels():
label.set_fontsize(self._fontsize)
for label in ax.get_yticklabels():
label.set_fontsize(self._fontsize)
# and gfor the label title
ax.set_xlabel(ax.get_xlabel(), fontsize=self._fontsize)
ax.set_ylabel(ax.get_ylabel(), fontsize=self._fontsize)
return fig, ax
[docs]
def plot_histogram_features_difference(self, figax:tuple[plt.Figure, plt.Axes]=None, density = True, alpha = 1.0, **kwargs) -> tuple[plt.Figure, plt.Axes]:
"""
Plot histogram of the volume in each feature for the difference.
"""
if figax is None:
figax = plt.subplots()
fig, ax = figax
# # Calculate the difference between the two arrays
# diff = self.difference
volumes = []
for idx in tqdm(self.nb_cells[:self.last_features], desc="Calculating volumes"):
# Get the feature index
feature_index = idx[1]
part = self.difference_parts[feature_index]
# Create a mask for the feature
mask = part.label == feature_index
# Calculate the volume for this feature
volumes.append(np.ma.sum(part.difference.array[mask]) * self.array_reference.dx * self.array_reference.dy)
# Create a histogram of the differences
if len(volumes) > 0:
ax.hist(volumes, bins= min(100, int(len(volumes)/4)), density=density, alpha=alpha, **kwargs)
ax.set_title(_("Histogram of net volumes [m³]"))
# set font size of the labels
ax.tick_params(axis='both', which='major', labelsize=6)
for label in ax.get_xticklabels():
label.set_fontsize(self._fontsize)
for label in ax.get_yticklabels():
label.set_fontsize(self._fontsize)
# and gfor the label title
ax.set_xlabel(ax.get_xlabel(), fontsize=self._fontsize)
ax.set_ylabel(ax.get_ylabel(), fontsize=self._fontsize)
fig.tight_layout()
return fig, ax
[docs]
def _complete_report2(self, layout:CompareArraysLayout):
""" Complete the report with the individual differences. """
key_fig = [('Histogram_0-0', self.plot_histogram_features),
('Histogram_1-0', self.plot_histogram_features_difference),
('Arrays_0-0', self.plot_position),
('Arrays_1-0', self.plot_cartoweb),
('Position', self.plot_topo_grey),
]
keys = layout.keys
for key, fig_routine in key_fig:
if key in keys:
rect = layout._layout[key]
fig, ax = fig_routine()
# set size to fit the rectangle
fig.set_size_inches(pt2inches(rect.width), pt2inches(rect.height))
fig.tight_layout()
# convert canvas to PNG and insert it into the PDF
temp_file = NamedTemporaryFile(delete=False, suffix='.png')
fig.savefig(temp_file, format='png', bbox_inches='tight', dpi=self._dpi)
layout._page.insert_image(layout._layout[key], filename=temp_file.name)
# delete the temporary file
temp_file.delete = True
temp_file.close()
# Force to delete fig
plt.close(fig)
else:
logging.warning(f"Key {key} not found in layout. Skipping plot.")
[docs]
def create_report(self, output_file: str | Path = None,
append_all_differences: bool = True,
nb_max_differences:int = -1) -> None:
""" Create a page report for the array comparison. """
if output_file is None:
output_file = Path(f"compare_arrays_report.pdf")
if output_file.exists():
logging.warning(f"Output file {output_file} already exists. It will be overwritten.")
layout = CompareArraysLayout("Comparison Report")
layout.create_report()
self._complete_report(layout)
if nb_max_differences < 0:
nb_max_differences = len(self.difference_parts)
elif nb_max_differences > len(self.difference_parts):
logging.warning(f"Requested {nb_max_differences} differences, but only {len(self.difference_parts)} are available. Using all available differences.")
elif nb_max_differences < len(self.difference_parts):
logging.info(f"Limiting to {nb_max_differences} differences.")
features_to_treat = [feature[1] for feature in self.nb_cells[:nb_max_differences]]
with TemporaryDirectory() as temp_dir:
layout2 = CompareArraysLayout2("Distribution of Differences")
layout2.create_report()
self._complete_report2(layout2)
layout2.save_report(Path(temp_dir) / "distribution_of_differences.pdf")
all_pdfs = [Path(temp_dir) / "distribution_of_differences.pdf"]
if append_all_differences:
# Add each difference report to the main layout
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")])
for pdf_file in tqdm(all_pdfs, desc="Compiling PDFs"):
layout._doc.insert_file(pdf_file)
# create a TOC
layout._doc.set_toc(layout._doc.get_toc())
layout.save_report(output_file)
self._pdf_path = output_file
@property
[docs]
def pdf_path(self) -> Path:
""" Return the path to the generated PDF report. """
if hasattr(self, '_pdf_path'):
return self._pdf_path
else:
raise AttributeError("PDF path not set. Please create the report first.")
[docs]
class CompareArrays_wx(PDFViewer):
def __init__(self, reference: WolfArray | str | Path,
to_compare: WolfArray | str | Path,
ignored_patche_area:float = 2.0,
nb_max_patches:int = 10,
threshold: float = 0.01,
dpi=200, **kwargs):
""" Initialize the Simple Simulation GPU Report Viewer for comparison """
super(CompareArrays_wx, self).__init__(None, **kwargs)
use('agg')
if isinstance(reference, WolfArray) and isinstance(to_compare, WolfArray):
if np.any(reference.array.mask != to_compare.array.mask):
logging.warning("The masks of the two arrays are not identical. This may lead to unexpected results.")
dlg = wx.MessageDialog(self,
_("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."),
_("Warning"),
wx.OK | wx.ICON_WARNING)
dlg.ShowModal()
dlg.Destroy()
to_compare = WolfArray(mold=to_compare)
to_compare.array.mask[:,:] = reference.array.mask[:,:]
[docs]
self._report = CompareArrays(reference, to_compare)
self._report._dpi = dpi
self._report.localize_differences(threshold=threshold,
ignored_patche_area=ignored_patche_area)
self._report.create_report(nb_max_differences=nb_max_patches)
# Load the PDF into the viewer
if self._report.pdf_path is None:
logging.error("No report created. Cannot load PDF.")
return
self.load_pdf(self._report.pdf_path)
self.viewer.SetZoom(-1) # Fit to width
# Set the title of the frame
self.SetTitle("Simple Simulation GPU Comparison Report")
self.Bind(wx.EVT_CLOSE, self.on_close)
use('wxagg')
[docs]
def on_close(self, event):
""" Handle the close event to clean up resources """
self.viewer.pdfdoc.pdfdoc.close()
self.Destroy()
[docs]
def get_zones(self) -> Zones:
"""
Get the zones from the report.
"""
ret = self._report.get_zones()
ret.prep_listogl()
return ret