diff --git a/doc/source/_workflows/outputs_accuracy/report.html b/doc/source/_workflows/outputs_accuracy/report.html index 3d466569..de671c77 100644 --- a/doc/source/_workflows/outputs_accuracy/report.html +++ b/doc/source/_workflows/outputs_accuracy/report.html @@ -2,7 +2,7 @@ Qualify elevation results

Accuracy assessment report — xDEM

-

xDEM version: 0.2.3.dev26

Date: 16/06/2026 14:38:58

Computing time: 12.87 seconds

Elevation inputs

+

xDEM version: 0.2.3.dev26

Date: 16/06/2026 18:46:15

Computing time: 10.76 seconds

Elevation inputs

Image PNG

Masked elevation data

Image PNG @@ -16,7 +16,7 @@

Information about inputs

Preprocessed elevation data

-Image PNG +Image PNG

Coregistration user configuration

@@ -52,9 +52,9 @@

Statistics

Valid count113360711452211133607

Elevation differences

-Image PNG +Image PNG

Differences histogram

-Image PNG +Image PNG diff --git a/doc/source/_workflows/outputs_accuracy/report.pdf b/doc/source/_workflows/outputs_accuracy/report.pdf index 0b668dcf..1e8f12d9 100644 Binary files a/doc/source/_workflows/outputs_accuracy/report.pdf and b/doc/source/_workflows/outputs_accuracy/report.pdf differ diff --git a/doc/source/_workflows/outputs_topo/report.html b/doc/source/_workflows/outputs_topo/report.html index 94319e29..4e6eecdb 100644 --- a/doc/source/_workflows/outputs_topo/report.html +++ b/doc/source/_workflows/outputs_topo/report.html @@ -2,7 +2,7 @@ Topographic summary results

Topography summary report — xDEM

-

xDEM version: 0.2.3.dev26

Date: 16/06/2026 14:39:12

Computing time: 7.20 seconds

Elevation input

+

xDEM version: 0.2.3.dev26

Date: 16/06/2026 18:46:25

Computing time: 6.03 seconds

Elevation input

Image PNG

Masked elevation data

Image PNG diff --git a/doc/source/_workflows/outputs_topo/report.pdf b/doc/source/_workflows/outputs_topo/report.pdf index 5e352468..6086b81b 100644 Binary files a/doc/source/_workflows/outputs_topo/report.pdf and b/doc/source/_workflows/outputs_topo/report.pdf differ diff --git a/doc/source/cli_accuracy.md b/doc/source/cli_accuracy.md index 1c6f0242..01bb22e7 100644 --- a/doc/source/cli_accuracy.md +++ b/doc/source/cli_accuracy.md @@ -301,19 +301,16 @@ Tree of outputs for level 1: - root ├─ tables │ ├─ [aligned_elev_stats.csv] (if coregistration) - │ ├─ [diff_elev_before_coreg_stats.csv] (if coregistration) │ ├─ [diff_elev_after_coreg_stats.csv] (if coregistration) - │ ├─ reference_elev_stats.csv - │ └─ to_be_aligned_elev_stats.csv + │ ├─ [diff_elev_before_coreg_stats.csv] (if coregistration) + │ ├─ [diff_elev_without_coreg_stats.csv] (if no coregistration) - ├─ plots - │ ├─ inputs.png - │ ├─ [masked_elev_map.png] (if `path_to_mask` is given in input) - │ ├─ [preprocessed_to_be_aligned_elev_map.png or preprocessed_reference_elev_map.png] (if sampling_grid) - │ ├─ [diff_elev_before_coreg_map.png] (if coregistration) - │ ├─ [diff_elev_after_coreg_map.png] (if coregistration) - │ ├─ [diff_elev_before_after_hist.png] (if coregistration) + │ ├─ [diff_elev_diff_coreg_map.png] (if coregistration) │ ├─ [diff_elev_without_coreg_map.png] (if no coregistration) - │ └─ [elev_diff_histo.png] (if coregistration)) + │ ├─ [elev_diff_histo.png] (if coregistration)) +- │ ├─ inputs.png + │ ├─ [masked_elev_map.png] (if `path_to_mask` is given in input) + │ └─ [preprocessed_to_be_aligned_elev_map.png or preprocessed_reference_elev_map.png] (if sampling_grid) ├─ [rasters │ └─ [aligned_elev.tif] (if coregistration) ├─ report.html @@ -327,28 +324,28 @@ Tree of outputs for level 2: - root ├─ tables │ ├─ [aligned_elev_stats.csv] (if coregistration) - │ ├─ [diff_elev_before_coreg_stats.csv] (if coregistration) │ ├─ [diff_elev_after_coreg_stats.csv] (if coregistration) + │ ├─ [diff_elev_before_coreg_stats.csv] (if coregistration) + │ ├─ [diff_elev_without_coreg_stats.csv] (if no coregistration) │ ├─ reference_elev_stats.csv │ └─ to_be_aligned_elev_stats.csv ├─ plots - │ ├─ inputs.png - │ ├─ [masked_elev_map.png] (if `path_to_mask` is given in input) - │ ├─ [preprocessed_to_be_aligned_elev_map.png or preprocessed_reference_elev_map.png] (if sampling_grid) - │ ├─ [diff_elev_before_coreg_map.png] (if coregistration) │ ├─ [diff_elev_after_coreg_map.png] (if coregistration) + │ ├─ [diff_elev_before_coreg_map.png] (if coregistration) │ ├─ [diff_elev_coreg_tba_map.png] (if coregistration) - │ ├─ [diff_elev_before_after_hist.png] (if coregistration) │ ├─ [diff_elev_without_coreg_map.png] (if no coregistration) - │ └─ [elev_diff_histo.png] (if coregistration)) + │ ├─ [elev_diff_histo.png] (if coregistration)) +- │ ├─ inputs.png + │ ├─ [masked_elev_map.png] (if `path_to_mask` is given in input) + │ └─ [preprocessed_to_be_aligned_elev_map.png or preprocessed_reference_elev_map.png] (if sampling_grid) ├─ rasters - │ ├─ [reference_elev_reprojected.tif] (if grid resampling) - │ ├─ [to_be_aligned_elev_reprojected.tif] (if grid resampling) │ ├─ [aligned_elev.tif] (if coregistration) - │ ├─ [diff_elev_before_coreg_map.tif] (if coregistration) │ ├─ [diff_elev_after_coreg_map.tif] (if coregistration) + │ ├─ [diff_elev_before_coreg_map.tif] (if coregistration) │ ├─ [diff_elev_coreg_tba_map.tif] (if coregistration) - │ └─ [diff_elev_without_coreg_map.tif] (if no coregistration) + │ ├─ [diff_elev_without_coreg_map.tif] (if no coregistration) + │ ├─ [reference_elev_reprojected.tif] (if grid resampling) + │ └─ [to_be_aligned_elev_reprojected.tif] (if grid resampling) ├─ report.html ├─ [report.pdf] (if `generate_pdf` if `True`) └─ used_config.yaml diff --git a/tests/test_workflows/test_accuracy.py b/tests/test_workflows/test_accuracy.py index 60c44315..8b9cec51 100644 --- a/tests/test_workflows/test_accuracy.py +++ b/tests/test_workflows/test_accuracy.py @@ -125,7 +125,12 @@ def test_run(get_accuracy_inputs_test, tmp_path, level, generated_pdf): assert Path(tmp_path / "tables").joinpath("aligned_elev_stats.csv").exists() - assert Path(tmp_path / "plots").joinpath("diff_elev_diff_coreg_map.png").exists() + if level == 1: + assert Path(tmp_path / "plots").joinpath("diff_elev_diff_coreg_map.png").exists() + else: + assert Path(tmp_path / "plots").joinpath("diff_elev_before_coreg_map.png").exists() + assert Path(tmp_path / "plots").joinpath("diff_elev_after_coreg_map.png").exists() + assert Path(tmp_path / "plots").joinpath("elev_diff_histo.png").exists() assert Path(tmp_path / "plots").joinpath("masked_elev_map.png").exists() assert Path(tmp_path / "plots").joinpath("inputs.png").exists() diff --git a/xdem/workflows/accuracy.py b/xdem/workflows/accuracy.py index dedb1d32..3846cf7a 100644 --- a/xdem/workflows/accuracy.py +++ b/xdem/workflows/accuracy.py @@ -97,7 +97,6 @@ def _load_data(self) -> tuple[float, float]: title_dem_right="To-be-aligned elevation", vmin=vmin, vmax=vmax, - cbar_title=f"Elevation ({self.reference_elev.crs.linear_units})", ) if ref_mask is not None or tba_mask is not None: if ref_mask is not None: @@ -115,7 +114,6 @@ def _load_data(self) -> tuple[float, float]: title_dem_right="Masked terrain for to-be-aligned elevation", vmin=vmin, vmax=vmax, - cbar_title=f"Elevation ({self.reference_elev.crs.linear_units})", ) self.dico_to_show = [ @@ -214,7 +212,6 @@ def _prepare_datas(self, vmin: float, vmax: float) -> None: filename="preprocessed_to_be_aligned_elev_map", vmin=vmin, vmax=vmax, - cbar_title=f"Elevation ({self.to_be_aligned_elev.crs.linear_units})", ) else: self.reference_elev = self.reference_elev.crop(coord_intersection) @@ -224,7 +221,6 @@ def _prepare_datas(self, vmin: float, vmax: float) -> None: filename="preprocessed_reference_elev_map", vmin=vmin, vmax=vmax, - cbar_title=f"Elevation ({self.reference_elev.crs.linear_units})", ) if self.level > 1: @@ -342,32 +338,66 @@ def run(self) -> None: self.stats_after["median"] + 3 * self.stats_after["nmad"], ) - self.generate_plot( - dem=self.diff_before, - title="Difference between To-be-align and Reference elevation\n(before coregistration)", - filename="diff_elev_diff_coreg_map", - dem_right=self.diff_after, - title_dem_right="Difference between Aligned and Reference elevation\n(after coregistration)", - vmin=vmin_diff, - vmax=vmax_diff, - cmap="RdBu", - cbar_title=f"Elevation differences ({self.diff_before.crs.linear_units})", - ) + if self.level == 1: + self.generate_plot( + dem=self.diff_before, + title="Difference between To-be-aligned and Reference elevation\n(before coregistration)", + filename="diff_elev_diff_coreg_map", + dem_right=self.diff_after, + title_dem_right="Difference between Aligned and Reference elevation\n(after coregistration)", + vmin=vmin_diff, + vmax=vmax_diff, + cmap="RdBu", + ) + else: + self.generate_plot_with_profiles( + dem=self.diff_before, + title="Difference between To-be-aligned and Reference elevation\n(before coregistration)", + filename="diff_elev_before_coreg_map", + vmin=vmin_diff, + vmax=vmax_diff, + cmap="RdBu", + ) + + self.generate_plot_with_profiles( + dem=self.diff_after, + title="Difference between Aligned and Reference elevation\n(after coregistration)", + filename="diff_elev_after_coreg_map", + vmin=vmin_diff, + vmax=vmax_diff, + cmap="RdBu", + ) - if self.level > 1: self.diff_coreg_tba = aligned_elev.reproject(self.to_be_aligned_elev) - self.to_be_aligned_elev - self.generate_plot( + self.generate_plot_with_profiles( dem=self.diff_coreg_tba, - title="Difference between Aligned and To-be-align elevation\n(no coregistration)", + title="Difference between Aligned and To-be-aligned elevation\n(after coregistration)", filename="diff_elev_coreg_tba_map", cmap="RdBu", - cbar_title=f"Elevation differences ({self.diff_after.crs.linear_units})", ) else: self.diff = self.to_be_aligned_elev - self.reference_elev self.stats = self.diff.get_stats(stats_keys) vmin, vmax = -(self.stats["median"] + 3 * self.stats["nmad"]), self.stats["median"] + 3 * self.stats["nmad"] + if self.level == 1: + self.generate_plot( + self.diff, + title="Difference between To-be-aligned and Reference elevation", + filename="diff_elev_without_coreg_map", + vmin=vmin, + vmax=vmax, + cmap="RdBu", + ) + else: + self.generate_plot_with_profiles( + dem=self.diff, + title="Difference between To-be-aligned and Reference elevation", + filename="diff_elev_without_coreg_map", + vmin=vmin, + vmax=vmax, + cmap="RdBu", + ) self.generate_plot( self.diff, title="Difference between To-be-align and Reference elevation", @@ -494,6 +524,12 @@ def print_dict(title: str, dictionary: dict[str, Any]) -> str: div_html += "
\n" return div_html + def print_png(title: str, width: int = 100) -> str: + return ( + f"Image PNG\n" + ) + # Metadata: Inputs inputs_information = list_dict[0] html += print_dict(inputs_information[0], inputs_information[1]) @@ -501,19 +537,19 @@ def print_dict(title: str, dictionary: dict[str, Any]) -> str: # Plot preprocessed data if did if "sampling_grid" in self.config["inputs"] and self.config["inputs"]["sampling_grid"] is not None: if self.config["inputs"]["sampling_grid"] == "reference_elev": - preprocessed_data = "plots/preprocessed_to_be_aligned_elev_map.png" + preprocessed_data = "preprocessed_to_be_aligned_elev_map" else: - preprocessed_data = "plots/preprocessed_reference_elev_map.png" + preprocessed_data = "preprocessed_reference_elev_map" html += "

Preprocessed elevation data

\n" - html += "Image PNG\n" + html += print_png(preprocessed_data) # Metadata: Inputs for title, dictionary in list_dict[1:]: # type: ignore html += print_dict(title, dictionary) if self.compute_coreg and self.level > 1: - html += "Image PNG\n" + html += print_png("diff_elev_coreg_tba_map") # Statistics table: if self.df_stats is not None: @@ -530,16 +566,19 @@ def print_dict(title: str, dictionary: dict[str, Any]) -> str: # Coregistration: Add elevation difference plot and histograms before/after if self.compute_coreg: html += "

Elevation differences

\n" - html += "Image PNG\n" + if self.level == 1: + html += print_png("diff_elev_diff_coreg_map") + else: + html += print_png("diff_elev_before_coreg_map") + html += print_png("diff_elev_after_coreg_map") html += "

Differences histogram

\n" - html += "Image PNG\n" + html += print_png("elev_diff_histo") else: html += "

Elevation differences

\n" - html += ( - "Image PNG\n" - ) + html += print_png("diff_elev_without_coreg_map") + html += """ diff --git a/xdem/workflows/workflows.py b/xdem/workflows/workflows.py index c6832bce..f920299c 100644 --- a/xdem/workflows/workflows.py +++ b/xdem/workflows/workflows.py @@ -223,6 +223,9 @@ def generate_plot( cmap.set_bad(color="k", alpha=None) kwargs["cmap"] = cmap + # Add colormap + kwargs["cbar_title"] = f"Elevation differences ({dem.crs.linear_units})" + # Force figsize with the good ratio to prevent larger right axe if not filled fig, (ax1, ax2) = plt.subplots(1, 2, figsize=[6.4, 2.4]) @@ -240,6 +243,100 @@ def generate_plot( plt.savefig(self.outputs_folder / "plots" / f"{filename}.png", dpi=300, bbox_inches="tight") plt.close() + def generate_plot_with_profiles( + self, + dem: RasterType, + title: str, + filename: str, + **kwargs: Any, + ) -> None: + """ + Generate plot from a DEM with profiles. + + :param dem: Input digital elevation model + :param title: Title of dem plot + :param filename: Filename of figure + """ + + import_optional("matplotlib") + import matplotlib.pyplot as plt + from matplotlib.gridspec import GridSpec + + # Raster data + data = dem.data + ny, nx = data.shape + + # Initial min/max for mean profiles + profile_cols = data.mean(axis=0) + profile_cols_stats = [profile_cols.min(), profile_cols.max()] + profile_rows = data.mean(axis=1) + profile_rows_stats = [profile_rows.min(), profile_rows.max()] + + # Keep profiles with at least more than 50% of valid values + nb_valid_rows, nb_valid_cols = data.count(axis=1), data.count(axis=0) + min_valid_rows, min_valid_cols = data.shape[1] / 2.0, data.shape[0] / 2.0 + + # Update profiles values according to valid values + profile_rows = np.ma.masked_where(nb_valid_rows < min_valid_rows, data.mean(axis=1)) + profile_cols = np.ma.masked_where(nb_valid_cols < min_valid_cols, data.mean(axis=0)) + + # Force figsize with the same size as generate_plot function + size_font = 6 + plt.rc("font", size=size_font) + plt.rc("axes", titlesize=size_font) + plt.rc("axes", labelsize=size_font) + plt.rc("xtick", labelsize=size_font) + plt.rc("ytick", labelsize=size_font) + plt.rc("legend", fontsize=size_font) + plt.rc("figure", titlesize=size_font) + + gs = GridSpec(2, 3, width_ratios=[1.2, 4, 0.3], height_ratios=[4, 1.2]) # 1 for the colobar + + fig = plt.figure() + ax_left = fig.add_subplot(gs[0, 0]) + ax_map = fig.add_subplot(gs[0, 1]) + cax = fig.add_subplot(gs[0, 2]) + ax_bottom = fig.add_subplot(gs[1, 1]) + + # Apply default cmap if not given in inputs + if "cmap" in kwargs: + cmap = plt.get_cmap(name=kwargs["cmap"]) + else: + cmap = plt.get_cmap(name="terrain") + cmap.set_bad(color="k", alpha=None) + kwargs["cmap"] = cmap + + # Plot DEM with colorbar + im = ax_map.imshow(data, aspect="auto", **kwargs) + ax_map.text(0.5, 1.12, title, transform=ax_map.transAxes, ha="center", va="top") + fig.colorbar(im, cax=cax).set_label(f"Elevation differences ({dem.crs.linear_units})") + + # Lines profiles + y = np.arange(ny) + ax_left.plot(profile_rows, y, color="black") + ax_left.set_ylim(ax_map.get_ylim()) + ax_left.invert_xaxis() + ax_left.yaxis.tick_left() + ax_left.yaxis.set_label_position("left") + ax_left.set_xlabel( + f"Mean along lines ({dem.crs.linear_units})\n" + f"Min: {np.round(profile_rows_stats[0], 2)} / Max: {np.round(profile_rows_stats[1], 2)}" + ) + + # Columns profiles + x = np.arange(nx) + ax_bottom.plot(x, profile_cols, color="black") + ax_bottom.set_xlim(ax_map.get_xlim()) + ax_bottom.yaxis.tick_left() + ax_bottom.xaxis.set_label_position("bottom") + ax_bottom.set_xlabel( + f"Mean along columns ({dem.crs.linear_units})\n" + f"Min: {np.round(profile_cols_stats[0], 2)} / Max: {np.round(profile_cols_stats[1], 2)}" + ) + + plt.savefig(self.outputs_folder / "plots" / f"{filename}.png", dpi=300, bbox_inches="tight") + plt.close() + def floats_process( self, dict_with_floats: Dict[str, Any] | InputCoregDict | OutputCoregDict | Any ) -> Dict[str, Any]: # type: ignore