Skip to content

Plotting Functions

The georeader.plot module provides matplotlib visualization functions for working with geospatial data, particularly with GeoTensor objects. These functions build on matplotlib to provide convenient ways to visualize raster data with appropriate geographic context.

plot.show

This function displays geospatial data on a matplotlib axis. It's a wrapper around matplotlib's imshow function that handles GeoData objects properly, respecting their coordinate systems.

Features: * Automatically handles the extent based on the data's bounds * Optional colorbar display * Optional scale bar showing geographic scale * Can display coordinates in lat/long format * Handles masking of no-data values

Example:

import matplotlib.pyplot as plt
from georeader import plot

# Display RGB data (3-band image)
rgb = (s2img[[3,2,1]] / 3_500).clip(0,1)
plot.show(rgb)

RGB visualization of satellite imagery

# With colorbar
greyimg = np.mean(rgb, axis=0)
plot.show(greyimg, add_colorbar_next_to=True)

Grayscale visualization with colorbar

plot.plot_segmentation_mask

This function visualizes discrete segmentation masks (like land cover classifications) with appropriate colors and legend.

Features: * Customizable color mapping for different classes * Optional legend with class names * Works with both numeric and categorical data

Example:

# Create a land/water/cloud mask
water = mndwi < 0
land_water_clouds = GeoTensor(np.ones(clouds.shape, dtype=np.uint8),
                              fill_value_default=0,
                              crs=clouds.crs,
                              transform=clouds.transform)

land_water_clouds[water] = 2
land_water_clouds[clouds] = 3
land_water_clouds[invalids] = 0

plot.plot_segmentation_mask(land_water_clouds, 
                           interpretation_array=["invalids","clear","water","cloud"], 
                           color_array=["#000000","#c66d43","#437cc6","#eeeeee"])

Land cover classification map showing water, cloud and land areas

plot.add_shape_to_plot

This function adds vector data (like points, lines, polygons) to an existing map.

Features: * Works with GeoDataFrame, individual geometries, or lists of geometries * Handles coordinate system transformations * Customizable styling options * Can plot polygon outlines only

Example:

from georeader import plot
from shapely.geometry import box

# Create a plot with raster data
ax = plot.show(rgb)
bbox = box(45.43, -19.53, 45.45, -19.58)

plot.add_shape_to_plot(bbox, ax=ax, polygon_no_fill=True, 
                       crs_plot=rgb.crs,
                       crs_shape="EPSG:4326",
                       kwargs_geopandas_plot={"color": "red"})
RGB with a square in red

API Reference

show(data, add_colorbar_next_to=False, add_scalebar=False, kwargs_scalebar=None, mask=False, bounds_in_latlng=True, **kwargs)

Wrapper around rasterio.plot.show for GeoData objects. It adds options to add a colorbar next to the plot and a scalebar showing the geographic scale.

Parameters:

Name Type Description Default
data GeoData

GeoData object to plot with imshow

required
add_colorbar_next_to bool

Defaults to False. Add a colorbar next to the plot

False
add_scalebar bool

Defaults to False. Add a scalebar to the plot

False
kwargs_scalebar Optional[dict]

Defaults to None. Keyword arguments for the scalebar.

None
See https

//github.com/ppinard/matplotlib-scalebar. (install with pip install matplotlib-scalebar)

required
mask Union[bool, array]

Defaults to False. Mask to apply to the data. If True, the fill_value_default of the GeoData is used.

False
bounds_in_latlng bool

Defaults to True. If True, the x and y ticks are shown in latlng.

True
**kwargs

Keyword arguments for imshow

{}

Returns:

Type Description
Axes

plt.Axes: image object

Source code in georeader/plot.py
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 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
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
def show(data:GeoData, add_colorbar_next_to:bool=False,
         add_scalebar:bool=False,
         kwargs_scalebar:Optional[dict]=None,
         mask:Union[bool,np.array]= False, bounds_in_latlng:bool=True,
           **kwargs) -> plt.Axes:
    """
    Wrapper around rasterio.plot.show for GeoData objects. It adds options to add a colorbar next to the plot
    and a scalebar showing the geographic scale.

    Args:
        data (GeoData): GeoData object to plot with imshow
        add_colorbar_next_to (bool, optional): Defaults to False. Add a colorbar next to the plot
        add_scalebar (bool, optional): Defaults to False. Add a scalebar to the plot
        kwargs_scalebar (Optional[dict], optional): Defaults to None. Keyword arguments for the scalebar. 
        See https://github.com/ppinard/matplotlib-scalebar. (install with pip install matplotlib-scalebar)
        mask (Union[bool,np.array], optional): Defaults to False. Mask to apply to the data. 
            If True, the fill_value_default of the GeoData is used.
        bounds_in_latlng (bool, optional): Defaults to True. If True, the x and y ticks are shown in latlng.
        **kwargs: Keyword arguments for imshow

    Returns:
        plt.Axes: image object
    """
    if "ax" in kwargs:
        ax = kwargs.pop("ax")
        if ax is None:
            ax = plt.gca()
    else:
        ax = plt.gca()

    if isinstance(mask, bool):
        if mask:
            mask = data.values == data.fill_value_default
            np_data = np.ma.masked_array(data.values, mask=mask)
        else:
            mask = None
            np_data = data.values
    else:
        np_data = np.ma.masked_array(data.values, mask=mask)

    if len(np_data.shape) == 3:
        if np_data.shape[0] == 1:
            np_data = np_data[0]
        else:
            np_data = np_data.transpose(1, 2, 0)

            if mask is not None:
                assert len(mask.shape) in (2, 3), f"mask must be 2D or 3D found shape: {mask.shape}"
                if len(mask.shape) == 3:
                    mask = np.any(mask, axis=0)

                # Convert np_data to RGBA using mask as alpha channel.
                np_data = np.concatenate([np_data, ~mask[..., None]], axis=-1)

    # https://matplotlib.org/stable/users/explain/artists/imshow_extent.html
    if data.transform.is_rectilinear:
        ul_x, ul_y = data.transform * (0, 0)
        lr_x, lr_y = data.transform * (data.shape[-1], data.shape[-2])
    else:
        # bounds takes the minimum and maximum of the 4 corners of the image
        xmin, ymin, xmax, ymax = data.bounds
        ul_x = xmin
        ul_y = ymax
        lr_x = xmax
        lr_y = ymin 
        warnings.warn("The transform is not rectilinear. The x and y ticks and the scale bar are not going to be correct."
                      " To discard this warning use: warnings.filterwarnings('ignore', message='The transform is not rectilinear.')")

    # kwargs['extent'] = (bounds.left, bounds.right, bounds.bottom, bounds.top)
    kwargs['extent'] = (ul_x, lr_x, lr_y, ul_y)


    title = None
    if "title" in kwargs:
        title = kwargs.pop("title")

    ax.imshow(np_data, **kwargs)

    if title is not None:
        ax.set_title(title)

    if add_colorbar_next_to:
        im = ax.images[-1]
        colorbar_next_to(im, ax)

    if add_scalebar:
        try:
             from matplotlib_scalebar.scalebar import ScaleBar
        except ImportError as e:
            raise ImportError("Install matplotlib-scalebar to use scalebar"
                              "pip install matplotlib-scalebar"
                              f"{e}")

        if kwargs_scalebar is None:
            kwargs_scalebar = {"dx":1}
        if "dx" not in kwargs_scalebar:
            kwargs_scalebar["dx"] = 1
        ax.add_artist(ScaleBar(**kwargs_scalebar))

    if bounds_in_latlng:
        from matplotlib.ticker import FuncFormatter

        @FuncFormatter
        def x_formatter(x, pos):
            # transform x,ymin to latlng
            longs, lats = rasterio.warp.transform(data.crs, "epsg:4326", [x], [lr_y])
            return f"{longs[0]:.2f}"


        @FuncFormatter
        def y_formatter(y, pos):
            # transform xmin,y to latlng
            longs, lats = rasterio.warp.transform(data.crs, "epsg:4326", [ul_x], [y])
            return f"{lats[0]:.2f}"

        ax.xaxis.set_major_formatter(x_formatter)
        ax.yaxis.set_major_formatter(y_formatter)


    return ax

plot_segmentation_mask(mask, color_array=None, interpretation_array=None, legend=True, ax=None, add_scalebar=False, kwargs_scalebar=None, min_val_mask=None, max_val_mask=None, bounds_in_latlng=True)

Plots a discrete segmentation mask with a legend.

Parameters:

Name Type Description Default
mask GeoData

(H, W) np.array with values from 0 to len(color_array)-1

required
color_array Optional[NDArray]

colors for values 0,...,len(color_array)-1 of mask

None
interpretation_array Optional[List[str]]

interpretation for classes 0, ..., len(color_array)-1

None
legend bool

plot the legend

True
ax Optional[Axes]

plt.Axes to plot

None
add_scalebar bool

Defaults to False. Add a scalebar to the plot

False
kwargs_scalebar Optional[dict]

Defaults to None. Keyword arguments for the scalebar.

None
See https

//github.com/ppinard/matplotlib-scalebar. (install with pip install matplotlib-scalebar)

required
bounds_in_latlng bool

Defaults to True. If True, the x and y ticks are shown in latlng.

True

Returns:

Type Description
Axes

plt.Axes

Source code in georeader/plot.py
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
def plot_segmentation_mask(mask:GeoData, color_array:Optional[NDArray]=None,
                           interpretation_array:Optional[List[str]]=None,
                           legend:bool=True, ax:Optional[plt.Axes]=None,
                           add_scalebar:bool=False,
                           kwargs_scalebar:Optional[dict]=None,
                           min_val_mask:Optional[int]=None,
                           max_val_mask:Optional[int]=None,
                           bounds_in_latlng:bool=True) -> plt.Axes:
    """
    Plots a discrete segmentation mask with a legend.

    Args:
        mask: (H, W) np.array with values from 0 to len(color_array)-1
        color_array: colors for values 0,...,len(color_array)-1 of mask
        interpretation_array: interpretation for classes 0, ..., len(color_array)-1
        legend: plot the legend
        ax: plt.Axes to plot
        add_scalebar (bool, optional): Defaults to False. Add a scalebar to the plot
        kwargs_scalebar (Optional[dict], optional): Defaults to None. Keyword arguments for the scalebar. 
        See https://github.com/ppinard/matplotlib-scalebar. (install with pip install matplotlib-scalebar)
        bounds_in_latlng (bool, optional): Defaults to True. If True, the x and y ticks are shown in latlng.

    Returns:
        plt.Axes

    """
    if color_array is None:
        if interpretation_array is not None:
            nlabels = len(interpretation_array)
        else:
            nlabels = len(np.unique(mask))
            min_val_mask = np.min(mask)
            max_val_mask = np.max(mask) + 1

        color_array = plt.cm.tab20.colors[:nlabels]

    cmap_categorical = colors.ListedColormap(color_array)
    color_array = np.array(color_array)
    if min_val_mask is None:
        min_val_mask = 0
    if max_val_mask is None:
        max_val_mask = color_array.shape[0]

    assert (max_val_mask - min_val_mask) == color_array.shape[0], f"max_val_mask - min_val_mask must be equal to the number of colors {max_val_mask} - {min_val_mask} != {color_array.shape[0]}"

    norm_categorical = colors.Normalize(vmin=min_val_mask -.5,
                                        vmax=max_val_mask - .5)


    if interpretation_array is not None:
        assert len(interpretation_array) == color_array.shape[
            0], f"Different numbers of colors and interpretation {len(interpretation_array)} {color_array.shape[0]}"


    ax = show(mask, ax=ax,
              cmap=cmap_categorical, norm=norm_categorical, 
              interpolation='nearest', add_scalebar=add_scalebar,
              kwargs_scalebar=kwargs_scalebar, bounds_in_latlng=bounds_in_latlng)

    if legend:
        if interpretation_array is None:
            interpretation_array = [str(i) for i in range(color_array.shape[0])]
        patches = []
        for c, interp in zip(color_array, interpretation_array):
            patches.append(mpatches.Patch(color=c, label=interp))

        ax.legend(handles=patches,
                  loc='upper right')

    return ax

add_shape_to_plot(shape, ax=None, crs_plot=None, crs_shape=None, polygon_no_fill=False, kwargs_geopandas_plot=None, title=None)

Adds a shape to a plot. It uses geopandas.plot.

Parameters:

Name Type Description Default
shape Union[GeoDataFrame, List[BaseGeometry], BaseGeometry]

geodata to plot

required
ax Optional[Axes]

Defaults to None. Axes to plot the shape

None
crs_plot Optional[Any]

Defaults to None. crs to plot the shape. If None, the crs of the shape is used.

None
crs_shape Optional[Any]

Defaults to None. crs of the shape. If None, the crs of the plot is used.

None
polygon_no_fill bool

If True, the polygons are plotted without fill.

False
kwargs_geopandas_plot Optional[Any]

Defaults to None. Keyword arguments for geopandas.plot

None
title Optional[str]

Defaults to None. Title of the plot.

None

Returns:

Type Description
Axes

plt.Axes:

Source code in georeader/plot.py
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
def add_shape_to_plot(shape:Union[gpd.GeoDataFrame, List[Geometry], Geometry], ax:Optional[plt.Axes]=None,
                      crs_plot:Optional[Any]=None,
                      crs_shape:Optional[Any]=None,
                      polygon_no_fill:bool=False,
                      kwargs_geopandas_plot:Optional[Any]=None,
                      title:Optional[str]=None) -> plt.Axes:
    """
    Adds a shape to a plot. It uses geopandas.plot.

    Args:
        shape (Union[gpd.GeoDataFrame, List[Geometry], Geometry]): geodata to plot
        ax (Optional[plt.Axes], optional): Defaults to None. Axes to plot the shape
        crs_plot (Optional[Any], optional): Defaults to None. crs to plot the shape. If None, the crs of the shape is used.
        crs_shape (Optional[Any], optional): Defaults to None. crs of the shape. If None, the crs of the plot is used.
        polygon_no_fill: If True, the polygons are plotted without fill.
        kwargs_geopandas_plot (Optional[Any], optional): Defaults to None. Keyword arguments for geopandas.plot
        title (Optional[str], optional): Defaults to None. Title of the plot.

    Returns:
        plt.Axes: 
    """
    if not isinstance(shape, gpd.GeoDataFrame):
        if isinstance(shape, Geometry):
            shape = [shape]
        shape = gpd.GeoDataFrame(geometry=shape,crs=crs_shape if crs_shape is not None else crs_plot)

    if crs_plot is not None:
        shape = shape.to_crs(crs_plot)

    # if color is not None:
    #     if not isinstance(color, str):
    #         assert len(color) == shape.shape[0], "The length of color array must be the same as the number of shapes"

    #     color = pd.Series(color, index=shape.index)

    if ax is None:
        ax = plt.gca()

    if kwargs_geopandas_plot is None:
        kwargs_geopandas_plot = {}

    if polygon_no_fill:
        shape.boundary.plot(ax=ax, **kwargs_geopandas_plot)
    else:
        shape.plot(ax=ax, **kwargs_geopandas_plot)

    if title is not None:
        ax.set_title(title)

    # if legend and color is not None:
    #     color_unique = color.unique()
    #     legend_elements = [Patch(facecolor=color_unique,  label=c) for c in color_unique]
    #     ax.legend(handles=legend_elements)

    return ax