#!/usr/bin/env python
"""
geoutils.geoviewer provides a toolset for plotting raster and vector data
TO DO:
- change so that only needed band is loaded
- include some options from imviewer: https://github.com/dshean/imview/blob/master/imview/imviewer.py
"""
from __future__ import annotations
import argparse
import sys
from typing import Any
import matplotlib.pyplot as plt
import numpy as np
from geoutils.georaster import Raster
[docs]def getparser() -> argparse.Namespace:
# Set up description
parser = argparse.ArgumentParser(description="Visualisation tool for any image supported by GDAL.")
# Positional arguments
parser.add_argument("filename", type=str, help="str, path to the image")
# optional arguments
parser.add_argument(
"-cmap",
dest="cmap",
type=str,
default="default",
help="str, a matplotlib colormap string (default is from rcParams).",
)
parser.add_argument(
"-vmin",
dest="vmin",
type=str,
default=None,
help=(
"float, the minimum value for colorscale, or can be expressed as a "
"percentile e.g. 5%% (default is calculated min value)."
),
)
parser.add_argument(
"-vmax",
dest="vmax",
type=str,
default=None,
help=(
"float, the maximum value for colorscale, or can be expressed as a "
"percentile e.g. 95%% (default is calculated max value)."
),
)
parser.add_argument(
"-band",
dest="band",
type=int,
default=None,
help="int, which band to display (start at 0) for multiband images (Default is 0).",
)
parser.add_argument(
"-nocb",
dest="nocb",
help="If set, will not display a colorbar (Default is to display the colorbar).",
action="store_false",
)
parser.add_argument(
"-clabel", dest="clabel", type=str, default="", help="str, the label for the colorscale (Default is empty)."
)
parser.add_argument("-title", dest="title", type=str, default="", help="str, figure title (Default is empty).")
parser.add_argument(
"-figsize",
dest="figsize",
type=str,
default="default",
help=(
"str, figure size, must be a tuple of size 2, either written with quotes, "
"or two numbers separated by comma, no space (Default is from rcParams)."
),
)
parser.add_argument(
"-max_size",
dest="max_size",
type=int,
default=2000,
help="int, image size is limited to max_size**2 for faster reading/displaying (Default is 2000).",
)
parser.add_argument(
"-save",
dest="save",
type=str,
default="",
help="str, filename to the output filename to save to disk (Default is displayed on screen).",
)
parser.add_argument(
"-dpi",
dest="dpi",
type=str,
default="default",
help="int, dpi value to use when saving figure (Default is from rcParams).",
)
parser.add_argument(
"-nodata",
dest="nodata",
type=str,
default="default",
help="float, no data value (Default is read from file metadata).",
)
parser.add_argument(
"-noresampl",
dest="noresampl",
default=False,
action="store_true",
help="True or False, if False then allow dynamic image downscaling, if True, prevent it.",
)
args = parser.parse_args()
return args
[docs]def main() -> None:
# Parse arguments
args = getparser()
# Load image metadata #
img = Raster(args.filename, load_data=False)
xmin, xmax, ymin, ymax = img.bounds
# Resample if image is too large #
if ((img.width > args.max_size) or (img.height > args.max_size)) & (not args.noresampl):
dfact = max(int(img.width / args.max_size), int(img.height / args.max_size))
print(f"Image will be downsampled by a factor {dfact}.")
new_shape: tuple[Any, ...] | None = (img.count, int(img.height / dfact), int(img.width / dfact))
else:
new_shape = None
# Read image #
img.load(out_shape=new_shape)
# Set no data value
if args.nodata == "default":
nodata = img.nodata
else:
try:
nodata = float(args.nodata)
except ValueError:
raise ValueError("ERROR: nodata must be a float, currently set to %s" % args.nodata)
img.set_ndv(nodata)
# Set default parameters #
# vmin
if args.vmin is not None:
try:
vmin: float | None = float(args.vmin)
except ValueError: # Case is not a number
perc, _ = args.vmin.split("%")
try:
perc = float(perc)
vmin = np.percentile(img.data, perc)
except ValueError: # Case no % sign
raise ValueError("vmin must be a float or percentage, currently set to %s" % args.vmin)
else:
vmin = None
# vmax
if args.vmax is not None:
try:
vmax: float | None = float(args.vmax)
except ValueError: # Case is not a number
perc, _ = args.vmax.split("%")
try:
perc = float(perc)
vmax = np.percentile(img.data, perc)
except ValueError: # Case no % sign
raise ValueError("vmax must be a float or percentage, currently set to %s" % args.vmax)
else:
vmax = None
# color map
if args.cmap == "default":
cmap = plt.rcParams["image.cmap"]
elif args.cmap in plt.cm.datad.keys():
cmap = args.cmap
else:
print("ERROR: cmap set to %s, must be in:" % args.cmap)
for i, elem in enumerate(plt.cm.datad.keys(), 1):
print(str(elem), end="\n" if i % 10 == 0 else ", ")
sys.exit(1)
# Figsize
if args.figsize == "default":
figsize = plt.rcParams["figure.figsize"]
else:
try:
figsize = tuple(int(arg) for arg in args.figsize)
xfigsize, yfigsize = figsize
except Exception as exception:
print("ERROR: figsize must be a tuple of size 2, currently set to %s" % args.figsize)
sys.stderr.write(str(exception))
sys.exit(1)
# dpi
if args.dpi == "default":
dpi = plt.rcParams["figure.dpi"]
else:
try:
dpi = int(args.dpi)
except ValueError:
raise ValueError("ERROR: dpi must be an integer, currently set to %s" % args.dpi)
# Plot data #
fig = plt.figure(figsize=figsize)
ax = fig.add_subplot(111)
# plot
img.show(
ax=ax,
band=args.band,
cmap=cmap,
interpolation="nearest",
vmin=vmin,
vmax=vmax,
add_cb=args.nocb,
cb_title=args.clabel,
title=args.title,
)
plt.tight_layout()
# Save
if args.save != "":
plt.savefig(args.save, dpi=dpi)
print("Figure saved to file %s." % args.save)
else:
plt.show()
if __name__ == "__main__":
main()