Skip to content

Statistical Figures

compare_image_spectral_profiles_pairs(image_groups_dict, output_figure_path, title, xlabel, ylabel, line_width=1)

Plots paired spectral profiles for before-and-after image comparisons.

Parameters:

Name Type Description Default
image_groups_dict dict

Mapping of labels to image path pairs (before, after): {'Image A': [ '/image/before/a.tif', 'image/after/a.tif' ], 'Image B': [ '/image/before/b.tif', '/image/after/b.tif' ]}

required
output_figure_path str

Path to save the resulting comparison figure.

required
title str

Title of the plot.

required
xlabel str

X-axis label.

required
ylabel str

Y-axis label.

required
line_width float

Width of the spectral profiles lines. Default is 1.

1
Outputs

Saves a spectral comparison plot showing pre- and post-processing profiles.

Source code in spectralmatch/statistics.py
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
def compare_image_spectral_profiles_pairs(
    image_groups_dict: dict,
    output_figure_path: str,
    title: str,
    xlabel: str,
    ylabel: str,
    line_width: float = 1,
):
    """
    Plots paired spectral profiles for before-and-after image comparisons.

    Args:
        image_groups_dict (dict): Mapping of labels to image path pairs (before, after):
            {'Image A': [
                '/image/before/a.tif',
                'image/after/a.tif'
            ],
            'Image B': [
                '/image/before/b.tif',
                '/image/after/b.tif'
            ]}
        output_figure_path (str): Path to save the resulting comparison figure.
        title (str): Title of the plot.
        xlabel (str): X-axis label.
        ylabel (str): Y-axis label.
        line_width (float, optional): Width of the spectral profiles lines. Default is 1.

    Outputs:
        Saves a spectral comparison plot showing pre- and post-processing profiles.
    """

    os.makedirs(os.path.dirname(output_figure_path), exist_ok=True)
    plt.figure(figsize=(10, 6))
    colors = itertools.cycle(plt.cm.tab10.colors)

    for label, group in image_groups_dict.items():
        if len(group) == 2:
            image_path1, image_path2 = group
            color = next(colors)

            for i, image_path in enumerate([image_path1, image_path2]):
                with rasterio.open(image_path) as src:
                    img = src.read()
                    num_bands = img.shape[0]
                    img_reshaped = img.reshape(num_bands, -1)
                    nodata = src.nodata
                    if nodata is not None:
                        img_reshaped = np.where(
                            img_reshaped == nodata, np.nan, img_reshaped
                        )
                    mean_spectral = np.nanmean(img_reshaped, axis=1)
                    bands = np.arange(1, num_bands + 1)
                    linestyle = "dashed" if i == 0 else "solid"
                    plt.plot(
                        bands,
                        mean_spectral,
                        linestyle=linestyle,
                        color=color,
                        linewidth=line_width,
                        label=f"{label} - {'Before' if i == 0 else 'After'}",
                    )

    plt.xlabel(xlabel)
    plt.ylabel(ylabel)
    plt.title(title)
    plt.legend()
    plt.grid(True)
    plt.xticks(np.arange(1, num_bands + 1, 1))
    plt.legend(frameon=True, facecolor='white', edgecolor='black', framealpha=1)
    plt.savefig(output_figure_path, dpi=300)
    plt.close()
    print(f"Saved: {os.path.splitext(os.path.basename(output_figure_path))[0]}")

compare_spatial_spectral_difference_band_average(input_images, output_figure_path, title, diff_label, subtitle, scale=None)

Computes and visualizes the mean per-pixel spectral difference between two coregistered, equal-size images.

Parameters:

Name Type Description Default
input_images list

List of two image file paths [before, after].

required
output_figure_path str

Path to save the resulting difference image (PNG).

required
title str

Title for the plot.

required
diff_label str

Label for the colorbar.

required
subtitle str

Subtitle text shown below the image.

required
scale tuple

Tuple (vmin, vmax) to fix the color scale. Centered at 0.

None

Raises:

Type Description
ValueError

If the input list doesn't contain exactly two image paths, or shapes mismatch.

Source code in spectralmatch/statistics.py
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
def compare_spatial_spectral_difference_band_average(
    input_images: list,
    output_figure_path: str,
    title: str,
    diff_label: str,
    subtitle: str,
    scale: tuple = None,
):
    """
    Computes and visualizes the mean per-pixel spectral difference between two coregistered, equal-size images.

    Args:
        input_images (list): List of two image file paths [before, after].
        output_figure_path (str): Path to save the resulting difference image (PNG).
        title (str): Title for the plot.
        diff_label (str): Label for the colorbar.
        subtitle (str): Subtitle text shown below the image.
        scale (tuple, optional): Tuple (vmin, vmax) to fix the color scale. Centered at 0.

    Raises:
        ValueError: If the input list doesn't contain exactly two image paths, or shapes mismatch.
    """
    if len(input_images) != 2:
        raise ValueError("input_images must be a list of exactly two image paths.")

    path1, path2 = input_images

    with rasterio.open(path1) as src1, rasterio.open(path2) as src2:
        img1 = src1.read().astype("float32")
        img2 = src2.read().astype("float32")
        nodata = src1.nodata

        if img1.shape != img2.shape:
            raise ValueError("Images must have the same dimensions.")

        diff = img2 - img1

        if nodata is not None:
            mask = np.full(diff.shape[1:], True)
            for b in range(diff.shape[0]):
                mask &= (img1[b] != nodata) & (img2[b] != nodata)
            diff[:, ~mask] = np.nan

        with np.errstate(invalid="ignore"):
            mean_diff = np.full(diff.shape[1:], np.nan)
            valid_mask = ~np.all(np.isnan(diff), axis=0)
            mean_diff[valid_mask] = np.nanmean(diff[:, valid_mask], axis=0)

        fig, ax = plt.subplots(figsize=(10, 6), constrained_layout=True)

        vmin, vmax = scale if scale else (np.nanmin(mean_diff), np.nanmax(mean_diff))
        max_abs = max(abs(vmin), abs(vmax))
        im = ax.imshow(mean_diff, cmap="coolwarm", vmin=-max_abs, vmax=max_abs)

        cbar = fig.colorbar(im, ax=ax, fraction=0.046, pad=0.04)
        cbar.set_label(diff_label)

        ax.set_title(title, fontsize=14, pad=12)
        if subtitle:
            ax.text(
                0.5, -0.1, subtitle, fontsize=10, ha="center", transform=ax.transAxes
            )

        ax.axis("off")
        plt.savefig(output_figure_path, dpi=300, bbox_inches="tight")
        plt.close()

        print(f"Saved: {os.path.splitext(os.path.basename(output_figure_path))[0]}")