TanagerSpec: Full walkthrough¶
tanagerspec is tailored to analyze Planet Tanager-1 hyperspectral products delivered as HDF5-EOS cubes. This notebook is a short end-to-end tour of the package and its TanagerSpec API (from tanagerspec import TanagerSpec): load data, preprocess, visualize, and run indices and classification workflows.
The package also ships utilities in tanagerspec.utils:
inspect_hdf(path)—from tanagerspec import inspect_hdf. Prints the HDF5-EOS tree so you can immediately see group/dataset paths to the spectral cube, wavelengths, masks, and metadata. Use this when you want to work outside theTanagerSpecclass (e.g. h5py, xarray, or a custom pipeline) but still need a fast map of the file layout for Tanager-1 products.scene_downloader— seetanagerspec.utils.scene_downloader.download_scene(url, dest_path, ...)streams large HDF5 files from a URL to disk in chunks (low memory) and returns apathlib.Pathyou can pass toTanagerSpec.from_file(path)or open yourself—useful for Open STAC–style scene URLs.index_catalog—from tanagerspec import IndexCatalog(moduletanagerspec.utils.index_catalog).IndexCatalogprints and searches the packagedtanager_indices.json(domains, per-index metadata,search,print_index, etc.) so you can choose index codes and understand formulas before callingTanagerSpec.analysis.calculate_index(...).
All these utilities you are going to explore through this tutorial
Tanager-1 product types¶
The loader recognizes two families of products:
- Ortho (orthorectified) — map-aligned imagery with georeferencing (
grid_info, CRS, transform). GeoTIFF export and map-accurate plots use this geometry. - Basic — no orthorectification / georeferencing in the same way as ortho products; the spectral cube and analysis paths work, but spatial reference for exporting or converting works but the output is not georeferenced or orthorectified.
Surface reflectance (SR) and radiance (RAD) variants are supported according to the product metadata inside the HDF5.
What you will Explore¶
This notebook is divided into sub-sections based on the APIs aims and potential. The table below summarizes all the sections and what you expect to learn within it.
| Stage | Section | Role |
|---|---|---|
| Data | Get scene | Optional: download ortho SR HDF5 from public URLs. |
| Structure | Inspect HDF5 | Optional: Explore HDF5-EOS Structure. |
| Load | TanagerSpec |
Open cube; info() summarizes geometry and bands. |
| Spectral edit | drop_bands |
Exclude wavelength intervals (noise, water vapor, etc.). |
| Radiometry | preprocess |
Mask nodata/cloud/cirrus; clip SR reflectance. |
| Denoise | denoise |
Optional PCA reconstruction to reduce noise. |
| Interop | convert_to |
GeoTIFF / ENVI BIL for GIS and ENVI (ortho: full geo context when available). |
| Viz | plot.* |
RGB, galleries, pixel hunter, spectra, correlation, animation. |
| Analysis | analysis.* |
Indices, custom index lab, PCA/MNF, clustering, SAM/RF/NN. |
Get a Tanager-1 scene (public HDF5)¶
This section shows how to download a public Tanager-1 scene. The example URLs below are orthorectified surface reflectance cubes (HDF5 / HDF-EOS style) from Open STAC.
You are not limited to ortho products: basic (non-orthorectified) surface reflectance scenes are also available from the catalog—use whichever product type fits your workflow.
download_scene(url, dest_path, …) saves the file to dest_path (including the filename). Files are large; allow enough time and disk space. If you already have a compatible .h5, skip this cell and set path_ag manually.
| Argument | Default | Meaning |
|---|---|---|
url |
— | Direct URL to the HDF5 scene file. |
dest_path |
— | Local destination path including the filename (e.g. "ortho_sr_ag_scene.h5"). |
chunk_size |
1 MB |
Download chunk size in bytes; reduce on low-memory or slow connections. |
overwrite |
False |
If True, re-download even if the file already exists on disk. |
from tanagerspec import download_scene
url_dif_land_covers = "https://storage.googleapis.com/open-cogs/planet-stac/tanager1-release2-core-imagery/ortho_sr_hdf5/20250510_112042_16_4001_ortho_sr_hdf5.h5"
# ag scene
url_ag = "https://storage.googleapis.com/open-cogs/planet-stac/tanager1-release2-core-imagery/ortho_sr_hdf5/20250501_143138_87_4001_ortho_sr_hdf5.h5"
path_ag = download_scene(
url_ag,
"ortho_sr_ag_scene.h5" # name of the file to save
)
Inspect the HDF5 container (HDF-EOS)¶
Optional. Before you load a large cube with TanagerSpec, you can peek at the HDF5-EOS layout: where surface_reflectance, wavelengths / FWHM / good bands and cloud / cirrus / nodata masks live. That's quick QA—you confirm paths and shapes without pulling the full array into memory.
Skip this section if the file is already validated or you're iterating on downstream code and don't need the tree printout.
API: from tanagerspec import inspect_hdf → inspect_hdf(path).
# from tanagerspec import inspect_hdf
# inspect_hdf(path_ag)
Load the scene: TanagerSpec¶
TanagerSpec.from_file(path) loads the spectral cube, metadata (product_type, wavelengths, good_wavelengths), and masks. Ortho products expose CRS and an affine transform through TanagerSpec.grid_info, which downstream steps (for example GeoTIFF export) use for correct geocoding. Basic products still load and analyze spectrally, but lack that orthorectified map geometry in the same way—treat map-based exports accordingly.
The next code cell sets logging.INFO so initialization and I/O messages are visible; use DEBUG for more verbose traces. Call TanagerSpec.info() to print a summary and a band-status figure (bad bands from metadata plus any TanagerSpec.drop_bands ranges).
import logging
log = logging.getLogger("tanagerspec")
log.setLevel(logging.INFO) # on
# log.setLevel(logging.DEBUG) # more detail
# log.disabled = True # fully mute this logger (rarely needed)
# Initialize the TanagerSpec object with the path to the HDF5 file
from tanagerspec import TanagerSpec
tanager_sr_ortho = TanagerSpec.from_file(path_ag)
Scene summary: info()¶
TanagerSpec.info() is a quick read-only overview of the loaded cube. It prints a compact text summary—product type (ortho vs basic), array shape (bands × rows × cols), band count, wavelength span, dtype, and whether CRS / affine transform are available (ortho scenes). It also reports preprocessing-related flags such as whether bands were dropped with TanagerSpec.drop_bands, and whether a denoise step was applied.
The figure encodes which wavelengths enter analysis and plots: metadata flags noisy regions (including water absorption), plus TanagerSpec.drop_bands; excluded regions are omitted automatically from downstream processing and figures.
Optional: TanagerSpec.info(save_png="band_status.png") saves that figure to a file; omit the argument to only display it in the notebook.
tanager_sr_ortho.info(
# save_png="band_status.png"
)
Drop spectral ranges: drop_bands¶
TanagerSpec.drop_bands([(λ_start, λ_end), …]) marks wavelengths in those intervals as invalid in good_wavelengths (they stay in the array but are excluded from algorithms that respect the mask). Typical uses: circum-water-vapor noisy regions, sensor roll-off at band edges, or intervals you know are unreliable for a given application. TanagerSpec.info() lists cumulative dropped ranges.
Make sure to run
TanagerSpec.info()again to track the available wavelengths
tanager_sr_ortho.drop_bands(
[
(400, 500), # example: short-visible / blue edge (tweak or remove as needed)
(900, 1000), # example: e.g. part of the NIR / transition region
]
)
After drop_bands: re-run info()¶
Call tanager_sr_ortho.info() again after drop_bands(...). The text summary now lists your Dropped Bands ranges, and the band-status figure updates: besides metadata-flagged noisy regions (e.g. water absorption), you should see your removed intervals highlighted as dropped. Only wavelengths that remain valid are used in downstream plots and analysis, treat the figure as the contract for what TanagerSpec will use for the rest of the notebook.
tanager_sr_ortho.info()
Preprocess: masks and SR clipping¶
TanagerSpec.preprocess(masking=…, clipping=…) applies scene quality masks (nodata, cloud, cirrus when masking=True) and, for surface reflectance products, can clip reflectance to a valid physical range when clipping=True. Run this when you want analysis/visualization to ignore contaminated pixels and avoid outliers from invalid reflectance.
preprocessed_cube = tanager_sr_ortho.preprocess(
masking = True, # mask nodata + cloud + cirrus
clipping = True # clip RS values to 0-1
)
Denoise (PCA reconstruction)¶
TanagerSpec.denoise(n_components=…) runs a PCA denoising step: the cube is projected to a low-dimensional spectral subspace and reconstructed—often suppressing random noise while retaining dominant spectral structure. Increase n_components if you see over-smoothing; decrease if noise remains.
Note: Calling TanagerSpec.denoise again is a no-op once is_denoised is True (see log/print warning).
#NOTE: It's Prefered to run it with plot section to see the effect of denoising
denoised_cube = tanager_sr_ortho.denoise(
n_components=3, # PCA components
)
Note: Preprocess + Denoise APIs updates the main hyperspectral cube in place (pixel values are modified directly). TanagerSpec does not keep a full second copy of the cube, by design this saves RAM compared to running two full cubes side by side. It's important to remember after run Denoise API that the rest of the notebook will use the denoised HS cube instead of the original noisy one.
Converters¶
Use convert_to.geotiff() and convert_to.envi_bil() to move from a single HDF5-EOS product—many internal datasets in one file—to multiple files on disk for GIS and ENVI-style workflows.
| Export | What you get |
|---|---|
geotiff() |
One multi-band GeoTIFF for the full surface_reflectance cube; with include_extras=True, each other exported layer becomes its own GeoTIFF alongside it. |
envi_bil() |
Same logical split: the main cube as .bil + .hdr, and separate BIL/HDR pairs per ancillary dataset when companions are exported. |
Georeferencing: CRS and the affine transform come from grid_info when the product provides them (ortho products usually do). Basic products are exported without a full CRS/transform in the GeoTIFF and ENVI. Users working with basic products are expected to know advanced methods to use latitude and longitude (or other supplied geometry) from the product and perform georeferencing or orthorectification.
ortho_sr_scene.h5 ← single container file
│
├── surface_reflectance (B × H × W) → ortho_sr_scene.tif
│ └─ all spectral bands in ONE stack
│
├── surface_reflectance_uncertainty → …_surface_reflectance_uncertainty.tif
├── aerosol_optical_depth → …_aerosol_optical_depth.tif
├── beta_cloud_mask → …_beta_cloud_mask.tif
├── beta_cirrus_mask → …_beta_cirrus_mask.tif
├── column_water_vapour → …_column_water_vapour.tif
├── nodata_pixels → …_nodata_pixels.tif
├── sensor_azimuth / sensor_zenith / … → one GeoTIFF per 2D layer
├── sun_azimuth / sun_zenith → …
├── time → …_time.tif
└── HDF-EOS struct / metadata → not emitted as GeoTIFF rasters (see API notes)
ENVI: same pattern — main cube = `*.bil` + `*.hdr`; each extra dataset = its own `*_… .bil` + `*.hdr`.
# Default layout next to the HDF5 (folder + main + companions)
tanager_sr_ortho.convert_to.geotiff(
# output_path="ortho_sr_ag_scene", # output path
include_extras=True # True: convert all assets, False: only the main HS cube
)
tanager_sr_ortho.convert_to.envi_bil(
# output_path="ortho_sr_ag_scene", # output path
include_extras=True # True: convert all assets, False: only the main HS cube
)
Export to xarray: convert_to.xarray()¶
TanagerSpec.convert_to.xarray() returns the current scene as an in-memory xarray.Dataset: spectral cube, wavelength coordinates, masks, companion 2D rasters, and grid info (CRS, transform) for ortho products — all in one labeled structure ready for further computation.
| Argument | Default | Meaning |
|---|---|---|
include_secondary_cubes |
False |
If True, also include other 3D HDF5 datasets whose shape matches the main cube (e.g. surface_reflectance_uncertainty). |
Returns: xarray.Dataset
ds = tanager_sr_ortho.convert_to.xarray(
# include_secondary_cubes=True # also load 3-D companions like surface_reflectance_uncertainty
)
ds
Plotting and exploration¶
The plot namespace turns the loaded cube into maps, spectral plots, and interactive tools (pixel picking, galleries, etc.).
Suggested order (typical notebook path)
- Quick look —
TanagerSpec.plot.rgb()withpreset="true_color"for a natural-colour overview. - Bands —
TanagerSpec.plot.bands_gallery()/bands_histograms()to inspect radiometry and noise per band. - Denoise (optional) — run
TanagerSpec.denoise()when you want a cleaner cube; re-runTanagerSpec.plot.rgb()afterward to see the difference. - Where and what — use
TanagerSpec.plot.hunt_pixels()to click locations and record pixel coordinates, then pass them intoTanagerSpec.plot.pixel_spectra/TanagerSpec.plot.roi_spectral_variabilityto inspect spectral signatures and variability. - Deeper — correlation views, animations, or analysis APIs once you care about specific materials or indices.
RGB before vs after denoise¶
To see how denoising improves quality and reduces noise in the hyperspectral cube, run the comparison below.
import matplotlib.pyplot as plt
# reinitialize the object
tanager_sr_ortho = TanagerSpec.from_file(path_ag)
preset = "true_color"
rgb_before = tanager_sr_ortho.plot.rgb(preset=preset, plot=False)
tanager_sr_ortho.denoise(n_components=3)
rgb_after = tanager_sr_ortho.plot.rgb(preset=preset, plot=False)
_ , axes = plt.subplots(1, 2, figsize=(14, 6), sharex=True, sharey=True)
axes[0].imshow(rgb_before)
axes[0].set_title(f"Before denoise — {preset}")
axes[0].axis("off")
axes[1].imshow(rgb_after)
axes[1].set_title("After denoise (PCA, same preset & stretch)")
axes[1].axis("off")
plt.tight_layout()
plt.show()
RGB composites: plot.rgb()¶
TanagerSpec.plot.rgb() builds a three-band colour composite from the cube: it picks bands by wavelength (nm) or from a named preset, applies a contrast stretch (default: percentile), optionally gamma, and returns a height × width × 3 array. With plot=True (default), it also renders the composite figure inline.
Parameters
| Argument | Role |
|---|---|
rgb_bands |
Optional (R_nm, G_nm, B_nm) in the same units as TanagerSpec.wavelengths. If omitted, bands come from preset. |
preset |
Named mix: "true_color" (approx. R/G/B), "false_color_nir" (NIR–Red–Green), "false_color_swir" (SWIR–NIR–Red), "false_color_urban" (SWIR2–SWIR1–Green). |
stretch |
"percentile", "gamma", "percentile_gamma", or "none". |
percentiles |
(low, high) for percentile stretch (default (2, 98)). |
gamma |
Gamma correction; None may auto-estimate where supported. |
target_brightness |
Used when gamma is estimated automatically. |
save_png |
If set, save the figure to that path. |
plot |
True to display; set False to only get the numpy RGB array (e.g. for custom layouts). |
# for ploting rgb true color don't use rgb_bands
rgb = tanager_sr_ortho.plot.rgb(
rgb_bands=(850.0, 660.0, 560.0), # optional for custom bands
# preset="red_edge_false_color", # choose one of the presets
save_png="rgb_true_color.png" # to save the image to a file
)
# for ploting rgb true color don't use rgb_bands
rgb = tanager_sr_ortho.plot.rgb(
rgb_bands=(850.0, 680.0, 560.0),
)
Band gallery and per-band histograms¶
bands_gallery(target_wvls=…) maps reflectance at requested central wavelengths (nm)—useful to compare visible vs red-edge vs NIR vs SWIR response. bands_histograms shows the distribution of reflectance for those bands over valid pixels (helps detect saturation, bimodal land cover mix, or residual clouds).
Band gallery: plot.bands_gallery()¶
TanagerSpec.plot.bands_gallery() draws a grid of single-band maps at chosen wavelengths (or an automatic spread across the spectrum). Each panel shows spatial reflectance for one band, with robust percentile scaling per band so bright/dark outliers do not dominate the display. Use it for quick QC: compare contrast, noise, and land-cover texture across bands before indices or classification.
Parameters
| Argument | Default | Meaning |
|---|---|---|
target_wvls |
None |
List/array of wavelengths (nm). The nearest valid band to each target is used. If None, bands are sampled evenly across the spectrum (up to max_bands). |
max_bands |
12 |
Upper limit on how many panels to show when wavelengths are auto-chosen. |
cmap |
"gray" |
Matplotlib colormap for each panel. |
robust_percentiles |
(2, 98) |
(low, high) percentiles for vmin/vmax per band (robust to outliers). |
mask |
None |
Optional 2D boolean array (True = valid pixel). None uses the cube's built-in nodata mask. |
save_png |
None |
If set, save the figure to this path. |
save_dpi |
500 |
Resolution (DPI) when save_png is set. |
Related: TanagerSpec.plot.bands_histograms() uses the same band selection but plots reflectance histograms instead of maps—good for comparing distributions band to band.
# Here is an example of band gallery combined with denoise
tanager_sr_ortho = TanagerSpec.from_file(path_ag)
targets = [400.0,430.0, 550.0, 680.0, # Visible (Blue, Green, Red)
850.0, 2600.0] # Red-Edge, NIR, SWIR1
print("Plotting band gallery before denoising...")
print("=" * 40)
tanager_sr_ortho.plot.bands_gallery(
target_wvls=targets,
# cmap="viridis", # Use a vibrant colormap
)
# after denoising
denoised_cube = tanager_sr_ortho.denoise(
n_components=3, # PCA components
)
print("Plotting band gallery after denoising...")
print("=" * 40)
tanager_sr_ortho.plot.bands_gallery(
target_wvls=targets,
# cmap="viridis",
)
Band histograms: plot.bands_histograms()¶
TanagerSpec.plot.bands_histograms() plots the reflectance distribution for the same set of bands as bands_gallery() but as histograms instead of spatial maps. Useful for spotting saturation, bimodal land-cover mixtures, or residual clouds.
| Argument | Default | Meaning |
|---|---|---|
target_wvls |
None |
List/array of wavelengths (nm); nearest valid band used. None auto-samples evenly (up to max_bands). |
max_bands |
12 |
Upper limit on number of histogram panels. |
bins |
500 |
Number of histogram bins. |
color |
"black" |
Bar fill colour. |
mask |
None |
Optional 2D boolean array (True = valid pixel). None uses the cube's built-in nodata mask. |
save_png |
None |
If set, save the figure to this path. |
save_dpi |
500 |
Resolution (DPI) when save_png is set. |
tanager_sr_ortho.plot.bands_histograms(
target_wvls=targets,
)
Analyze reflectance in one band: plot.analyze_reflectance_band()¶
TanagerSpec.plot.analyze_reflectance_band(...) focuses on one spectral band (the nearest band to your requested wavelength). It opens a two-panel figure: a grayscale map of reflectance for that band, and a histogram of pixel values with vertical lines at vmin and vmax. Use it to link land-cover patterns in space to the distribution of reflectance at a wavelength of interest (e.g. a SWIR absorption feature, red edge, or NIR plateau).
Parameters
| Argument | Meaning |
|---|---|
target_wavelength |
Target wavelength in nm (same units as TanagerSpec.wavelengths). The closest valid band index is used; the title shows the actual centre wavelength. |
vmin, vmax |
Optional color-scale limits for the map. If None, limits default to the 2nd and 98th percentiles of valid pixels (robust to outliers). Tighten or widen these to emphasize contrast without washing out real signal. |
save_png |
If set, save the figure to this path. |
# default: 2nd and 98th percentiles
tanager_sr_ortho.plot.analyze_reflectance_band(
950, # target_wavelength
# vmin=0.1, # vmin
# vmax=0.2, # vmax
)
# with vmin and vmax
tanager_sr_ortho.plot.analyze_reflectance_band(
950, # target_wavelength
vmin=0.1, # vmin
vmax=0.2, # vmax
)
Hunt pixels (interactive)¶
TanagerSpec.plot.hunt_pixels() opens an interactive UI to read row, column positions for pure-ish pixels or spectral comparisons. Use those coordinates in TanagerSpec.plot.pixel_spectra, TanagerSpec.plot.roi_spectral_variability, or TanagerSpec.build_spectral_library below.
tanager_sr_ortho.plot.hunt_pixels()
ROI spectral variability: plot.roi_spectral_variability()¶
TanagerSpec.plot.roi_spectral_variability(...) summarizes within-patch spectral spread around a chosen pixel. It cuts a square window ( window_size × window_size ) centred on coords, aggregates all spectra inside, and plots mean reflectance vs wavelength with a ±1 standard deviation band (shaded envelope). A zoomed RGB context map shows where the window sits. Use this to see intra-class variability (texture, mixed pixels, noise) for a material you label with target_name.
Parameters
| Argument | Default | Meaning |
|---|---|---|
target_name |
— | Legend / title label for the ROI (e.g. "Healthy crop", "Bare soil"). |
coords |
— | (row, col) — centre of the window in image (line, sample) indices, same convention as TanagerSpec.plot.hunt_pixels. |
window_size |
5 |
Side length in pixels of the square ROI (odd or even). |
color |
"tab:green" |
Colour for the mean curve and shading. |
preset |
"true_color" |
RGB background preset for the context panel (same names as TanagerSpec.plot.rgb()). |
mask |
None |
Optional 2D boolean array (True = valid pixel). None uses the cube's built-in nodata mask. |
save_png |
None |
Optional path to save the figure. |
tanager_sr_ortho.plot.roi_spectral_variability(
target_name="Vegetation",
coords=(370, 370), # row, col
window_size=7,
# preset="false_color_nir",
)
Pixel spectra: plot.pixel_spectra()¶
TanagerSpec.plot.pixel_spectra(...) overlays one spectrum per pixel on a reflectance vs wavelength plot, with a reference RGB map on the side showing where each target lies. Build targets from TanagerSpec.plot.hunt_pixels() (or any (row, col) pairs): { "label": (row, col), ... }. Optional colors assigns a Matplotlib colour per label; otherwise Tableau defaults are used.
Parameters
| Argument | Meaning |
|---|---|
targets |
dict[str, tuple[int, int]] — each key is a legend name, each value is (row, col) in image coordinates (line, sample). |
colors |
Optional dict[str, str] — map each target name to a colour (e.g. "Water": "tab:blue"). |
preset |
RGB background for the map: same presets as TanagerSpec.plot.rgb() ("true_color", "false_color_nir", …). |
mask |
Optional 2D boolean array (True = valid pixel). None (default) uses the cube's built-in nodata mask. |
save_png |
If set, save the figure to this path. |
Typical workflow
- Run
TanagerSpec.plot.hunt_pixels()and note(row, col)for materials of interest. - Pass them as
targetstoTanagerSpec.plot.pixel_spectrato compare signatures side by side.
# run hunt_pixels() first to get the targets
tanager_sr_ortho.plot.hunt_pixels()
targets = {
'Veg1': (403, 495), # row, col
'Veg2': (225, 486), # row, col
'Veg3': (400, 140) # row, col
}
# define colors are optional you can use the default colors
# colors = {
# 'Veg1': 'tab:green',
# 'Veg2': 'tab:red',
# 'Veg3': 'tab:blue'
# }
tanager_sr_ortho.plot.pixel_spectra(
targets=targets,
# colors=colors,
preset="false_color_nir",
)
Animate through wavelength (GIF)¶
TanagerSpec.plot.animate_bands(start_wvl, end_wvl, fps, filename, cmap) exports a GIF that steps through bands—intuitive for teaching spectral continuity and where clouds or noise appear. Increase fps slightly for faster playback; pick a cmap that separates low vs high reflectance for your surface.
Animate through wavelength: plot.animate_bands()¶
TanagerSpec.plot.animate_bands() writes an animated GIF that steps band by band through the cube over a chosen wavelength span. Each frame is a single-band map; the title shows the current wavelength (nm). Use it to see how spatial patterns and contrast evolve across the spectrum or monitoring the noise.
Parameters
| Argument | Default | Meaning |
|---|---|---|
start_wvl, end_wvl |
None |
Wavelength range (nm) to include. If both are None, all valid bands (after masking) in the cube are used. |
fps |
5 |
Frames per second for the GIF. |
filename |
"bands_timelapse.gif" |
Output path. Relative names are written to the current working directory; use a full path to control the folder. |
cmap |
"gray" |
Matplotlib colormap for each frame. |
dynamic_stretch |
True |
If True, vmin/vmax are recomputed per frame (each band auto-scaled). If False, one global stretch is computed from all frames in the range (fairer cross-band comparison, less "popping"). |
robust_percentiles |
(2, 98) |
Percentiles used for robust scaling (when stretching). |
mask |
None |
Optional 2D boolean array (True = valid pixel). None uses the cube's built-in nodata mask. |
Notes
- Output is a file (GIF), not an inline Jupyter widget—open the saved
filenameafter the cell runs. - Large band counts and high
fpsproduce bigger files and longer bake times.
tanager_sr_ortho.plot.animate_bands(
start_wvl=400, # start wavelength
end_wvl=450, # end wavelength
fps=5, # frames per second
filename="tanager_bands_animation3.gif", # output file name
cmap="gray", # colormap try to use viridis
# control the stretching of the bands
# dynamic_stretch=True, # dynamic stretch percintile 2 and 98
# robust_percentiles=(2, 98), # robust scaling percintile 2 and 98
)
Analysis: indices, catalog, and advanced methods¶
The analysis namespace (plus build_spectral_library on TanagerSpec) covers spectral indices, custom band math, dimensionality reduction, clustering, and supervised classification—all on the current cube state (respecting preprocess, drop_bands, denoise, etc.).
Band comparison
analysis.compare_bands(first_wavelength, second_wavelength, …)compares two specific wavelength slices with reflectance differences, normalized differences, summary metrics, and diagnostic plots.analysis.compare_band_range(preset=..., wavelength_range=..., …)compares every pair inside a wavelength interval so you can find redundant or high-contrast neighboring bands before building indices or ML features.
Indices & catalog
analysis.calculate_index(index_name, …)runs named indices whose definitions live in the packagedtanager_indices.json. UseIndexCatalog(print_domains,print_indices_by_domain,print_index,search) to choose codes and read formulas before computing. Tune the map withplot,cmap,robust_percentiles, and optionalmask_thresholdwhere supported.analysis.compare_layers(A, B)compares two index images with a scatter plot (A vs B per pixel) and overlaid histograms—useful for correlation and distribution checks, not map-vs-map panels.plot.bands_correlation()(in theplotnamespace) shows global band-to-band correlation—a quick view of redundancy before PCA/MNF or ML.
Custom & exploratory
analysis.index_creator_lab(…)— Your tool for creating your own index (normalized or custom function) or tuning the current ones for hyperspectral data.
Unsupervised
analysis.dim_reduction— PCA / ICA / MNF scores; optionalmethod_kwargsfor the backend. Returns a reduced cube; GeoTIFF export of that cube is not built into the method.analysis.clustering— optional DR then KMEANS or GMM; label map + optional GeoTIFF.
Supervised Classification
build_spectral_library— collect training spectra from (col, row) targets.analysis.classify_scene— Run a supervised classifier on the whole scene:SAM(class mean spectra),RF, orNN. TrainRF/NNwith thepandas.DataFramefrombuild_spectral_library; forSAM, pass per-class mean spectra derived from that library. Optionally write the result as single-band label GeoTIFF.
The cells below walk through these APIs in order, from quick band diagnostics through to classification.
Compare two bands: analysis.compare_bands()¶
TanagerSpec.analysis.compare_bands(...) compares two wavelength slices from the current cube. It reports mean differences, normalized differences, RMSE, and a linear relationship summary, then plots the two band maps, the difference maps, and a sampled scatter/histogram view. Use this when you want to test whether two candidate bands add distinct information before choosing them for an index or a classifier.
The example below compares two red-edge wavelengths. Adjust the wavelengths to match the region you are studying.
band_pair_report = tanager_sr_ortho.analysis.compare_bands(
first_wavelength=735,
second_wavelength=745,
plot=True,
# save_png="band_705_vs_740.png",
)
band_pair_report["comparison"]
Compare a band range: analysis.compare_band_range()¶
TanagerSpec.analysis.compare_band_range(...) runs the same idea as compare_bands() across every band pair inside a wavelength interval. You can use a named preset (e.g. "red_edge", "nir", "visible") or pass a custom wavelength_range=(start_nm, stop_nm). The returned summary ranks pairs by the selected metric, helping identify highly redundant bands or pairs with strong contrast.
Tip: call
tanager_sr_ortho.analysis.band_range_presetsto see all built-in preset names and their wavelength spans before calling this method.
# Inspect available preset wavelength ranges before calling compare_band_range
tanager_sr_ortho.analysis.band_range_presets
band_range_report = tanager_sr_ortho.analysis.compare_band_range(
preset="red_edge",
min_valid_pixels=10,
sort_by="mean_abs_normalized_difference",
ascending=False,
max_plot_pairs=8,
return_maps=False,
plot=True,
# save_png="red_edge_band_range_comparison.png",
)
band_range_report["summary"][:5]
Spectral indices: analysis.calculate_index()¶
TanagerSpec.analysis.calculate_index(...) computes a named spectral index on the current cube. It returns a 2D array. With plot=True, it also draws the index over a true-colour RGB background using the built-in plot helper.
Parameters
| Argument | Default | Meaning |
|---|---|---|
index_name |
— | Registered index code understood by the index engine (e.g. "NDVI"). Use IndexCatalog / tanager_indices.json to list valid names and formulas. |
plot |
True |
If True, show the index map (and histogram) over a reference RGB; set False to only get the numpy result. |
cmap |
'RdYlGn' |
Matplotlib colormap for the index layer in the figure. |
save_png |
None |
If set, save the figure to this path. |
mask_threshold |
None |
If set, any computed index value < mask_threshold is set to NaN in the output array and omitted from the map and histogram (together with ‑9999 / invalid pixels). Use for trimming dark or unreliable tails (index-dependent). |
rgb_preset |
"true_color" |
Name of the RGB underlay for the left panel when plot=True—same presets as TanagerSpec.plot.rgb() (e.g. "true_color", "false_color_nir", "false_color_swir", "false_color_urban"). |
robust_percentiles |
(2, 98) |
(low, high) percentiles for display scaling of the index map and histogram; None may fall back to min/max in the plotter. |
Returns
numpy.ndarray: shape (rows, cols) — the index image.
IMPORTANT to store the calculated index array into a variable for later comparison.
NDVI = tanager_sr_ortho.analysis.calculate_index(
"NDVI", # index name
plot=True, # plot the index
cmap='RdYlGn', # colormap
# save_png="NDVI_plot.png", # save the plot
# vmin=0, # min value
# vmax=1, # max value
# mask_threshold=0.3, # mask threshold: values below this will be masked
# rgb_preset = 'false_color_nir', # rgb preset
# robust_percentiles=(2, 98) # robust percentiles for calculated index
)
EVI = tanager_sr_ortho.analysis.calculate_index(
"EVI", # index name
plot=True, # plot the index
cmap='RdYlGn', # colormap
# save_png="NDVI_plot.png", # save the plot
# vmin=0, # min value
# vmax=1, # max value
# mask_threshold=0.3, # mask threshold: values below this will be masked
# rgb_preset = 'false_color_nir', # rgb preset
# robust_percentiles=(2, 98) # robust percentiles for calculated index
)
Index catalog: IndexCatalog()¶
IndexCatalog is a small console helper for exploring the packaged tanager_indices.json (spectral index codes, names, domains, formulas, band roles, references). It prints to the terminal; it does not return Python objects for you to loop over—use it to pick index_name strings for TanagerSpec.analysis.calculate_index(...).
from tanagerspec import IndexCatalog
# initalize the index catalog
cat = IndexCatalog()
# print all application_domain values in the file
cat.print_domains()
# print the indices by domain
cat.print_indices_by_domain("vegetation")
# print the details of the index
cat.print_index("EVI2")
# search for the index by query
cat.search(
query="evi", # query by words put anything you want
domain="vegetation", # optional filter by domain
limit=5 # optional limit the number of results
)
# Put the index name that you want to explore form the index catalog
EVI2 = tanager_sr_ortho.analysis.calculate_index("EVI2",
plot=True,
cmap='RdYlGn',
save_png="EVI_plot.png",
vmin=0,
vmax=1,
# mask_threshold=0.8
)
Validate index catalog: analysis.validate_indices()¶
TanagerSpec.analysis.validate_indices() runs every packaged spectral index against the current cube and reports which ones produce valid (finite) results. Use this after loading a new scene to quickly see which indices are computable given the cube's wavelength coverage.
| Argument | Default | Meaning |
|---|---|---|
index_names |
None |
Optional list of index codes to validate (e.g. ["NDVI", "EVI"]). None validates all packaged definitions. |
require_finite_pixels |
True |
If True, an index must produce at least one finite pixel to pass. |
print_report |
True |
Print a console validation report. |
Returns: dict — validation summary keyed by index name.
# Validate all packaged index definitions against the current cube
validation_report = tanager_sr_ortho.analysis.validate_indices(
# index_names=["NDVI", "EVI"], # optional: validate a subset only
print_report=True,
)
Compare two index maps: analysis.compare_layers()¶
TanagerSpec.analysis.compare_layers(...) takes two 2D index images (typically outputs of TanagerSpec.analysis.calculate_index with plot=False) and builds a two-panel figure: a scatter plot of index A vs index B per pixel, and overlaid density histograms of both index value distributions. Pixels where either array is non-finite are dropped from the comparison.
Parameters
| Argument | Meaning |
|---|---|
first_index, second_index |
numpy.ndarray — same spatial shape (flattened internally); e.g. NDVI vs NDRE cubes. |
index1_name, index2_name |
Labels for axes, titles, and legend (default "Index 1" / "Index 2"). |
save_png |
If set, save the figure to this path; None only displays it. |
tanager_sr_ortho.analysis.compare_layers(
NDVI,
EVI,
index1_name="NDVI",
index2_name="EVI",
save_png="comparison_plot.png"
)
tanager_sr_ortho.analysis.compare_layers(
EVI,
EVI2,
index1_name="EVI",
index2_name="EVI2",
save_png="comparison_plot.png"
)
Important:
compare_layersresults depend on how each index was computed. If you usedmask_thresholdincalculate_index, every value below that threshold was set toNaN. The scatter and histograms only use pixels where both indices are finite, so changingmask_thresholdon either index changes which pixels enter the comparison and shifts the joint distribution.
Band-to-band correlation: plot.bands_correlation()¶
TanagerSpec.plot.bands_correlation() builds a heatmap of Pearson correlation between every pair of spectral bands using valid pixels from the cube (nodata and non-finite values excluded). Bright off-diagonal blocks show spectral redundancy (neighbouring bands often correlate strongly); use it to see clustering, redundancy, and where bands carry independent information—helpful before PCA/MNF or feature selection.
| Argument | Default | Meaning |
|---|---|---|
mask |
None |
Optional 2D boolean array (True = valid pixel). None uses the cube's built-in nodata mask. |
save_png |
None |
If set, save the figure to this path. |
tanager_sr_ortho.plot.bands_correlation(
save_png="bands_correlation.png"
)
Custom index lab: analysis.index_creator_lab()¶
TanagerSpec.analysis.index_creator_lab(...) opens the Index Creator Lab—a playground for turning your physics intuition into a map. You pick two wavelength windows (band A and band B), place named training pixels on the scene, and optionally plug in your own math linking those bands. The lab averages reflectance inside each window, projects the formula across every pixel, and returns a full-scene index plus a rich dashboard: RGB context with your targets, the index map, its histogram, and mean spectra with the selected intervals highlighted. You are not limited to a normalized difference: any well-behaved function f(RA, RB) you define—ratios, residuals, weighted contrasts, or more expressive shapes—can be explored before you commit the idea elsewhere in the workflow.
Parameters
| Argument | Meaning |
|---|---|
target_dict |
dict[str, tuple[int, int]] — label → (row, col) for signature extraction (same style as TanagerSpec.plot.pixel_spectra). |
color_dict |
dict[str, str] — label → Matplotlib colour for each target on the map and spectra. |
band_x, band_y |
(name, min_nm, max_nm, unit) — e.g. ("NIR", 800, 900, "nm"). Defines the two spectral intervals averaged to RA and RB. |
index_name |
Title / colour-bar label for the custom index (default "Custom Index"). |
index_func |
Optional callable(RA, RB) -> index. If None, defaults to normalized difference: (RA - RB) / (RA + RB). |
avg_radius |
Half-width in pixels for spatial averaging around each (row, col) when building signatures (default 2). |
cmap |
Colormap for the index panel. |
robust_percentiles |
(low, high) percentiles for display scaling of the index map and histogram; None uses min/max. |
mask_threshold |
Values below this (in index space) can be masked to NaN (see lab implementation). |
mask_bad_bands |
Per-band good mask; None defaults to TanagerSpec.good_bands. |
Returns
dictwith keys:band_a_image,band_b_image— 2D spatial images for the averaged bandsindex_image— 2D custom indexband_a_mask,band_b_mask— validity masks used in the averaging
import numpy as np
# Define the each bands range
# Note: it callculate spectral average in the defined range
x = ("Red", 650, 680, "green")
y = ("NIR", 760, 900, "red")
# Define the targets to visualize the spectral signature
targets = {
'Veg1': (403, 495), # row, col
'Veg2': (225, 486), # row, col
'Veg3': (400, 140) # row, col
}
# Define the colors for the targets
colors = {
'Veg1': 'tab:green',
'Veg2': 'tab:red',
'Veg3': 'tab:blue'
}
# Optional: Define the custom index formula
# defualt is normalized difference
def evi2_index(RA, RB):
"""
Two-band Enhanced Vegetation Index (EVI2): Jiang et al.
RA = mean reflectance in the NIR window, RB = mean in the Red window.
g * (NIR - Red) / (NIR + 2.4 * Red + L)
"""
g, L = 2.5, 1.0
denom = RA + 2.4 * RB + L
with np.errstate(divide="ignore", invalid="ignore"):
out = g * (RA - RB) / denom
return np.where(np.isfinite(out), out, np.nan)
result = tanager_sr_ortho.analysis.index_creator_lab(
target_dict=targets,
color_dict=colors,
band_x=y,
band_y=x,
index_name="ND Index",
avg_radius=2, # change the default average radius
cmap="RdYlGn",
mask_threshold=0.01, # optional: define the mask threshold
index_func=evi2_index # optional: define the custom index formula
)
Dimensionality reduction: analysis.dim_reduction()¶
TanagerSpec.analysis.dim_reduction(...) compresses the spectral axis of the cube into n_components new "bands" (e.g. PCA, ICA, or MNF scores per pixel). Invalid pixels are excluded; the result is a 3D array with shape (rows, cols, n_components).
Parameters
| Argument | Default | Meaning |
|---|---|---|
method |
"PCA" |
Algorithm: "PCA", "ICA", or "MNF" (case-insensitive). |
n_components |
3 |
Number of components to retain. |
plot |
True |
If True, display reduced components with the built-in visualizer. |
present_rgb |
"true_color" |
RGB preset for the spatial context panel. |
scatter_color_by |
"index" |
What to colour the component scatter by: "index" colours each point by a named spectral index value; other values fall back to a spatial colour. |
scatter_index |
"NDVI" |
Index code used for scatter colouring when scatter_color_by="index". |
scatter_cmap |
"RdYlGn" |
Matplotlib colourmap applied to the scatter colour axis. |
save_png |
None |
Path to save the figure; False / None skips saving. |
save_dpi |
300 |
Resolution when save_png is set. |
Returns
numpy.ndarray— reduced cube(rows, cols, n_components).
Notes
- Export of the reduced cube to GeoTIFF/NetCDF is not implemented on this method; keep the array in memory or save it yourself if needed.
pca = tanager_sr_ortho.analysis.dim_reduction(
method="PCA", # PCA, ICA, MNF
n_components=2, # number of components
scatter_color_by="index", # color by index
scatter_index="EVI", # index to color by
present_rgb="true_color", # Try to change to false_color_nir
plot=True, # plot the reduced components
save_png=False, # save the plot
)
Unsupervised clustering: analysis.clustering()¶
TanagerSpec.analysis.clustering(...) assigns each valid pixel to a cluster ID (unsupervised). You may optionally run dimensionality reduction first (same engines as TanagerSpec.analysis.dim_reduction: PCA, ICA, MNF), then cluster in the reduced feature space; or set dr_method=None to cluster every spectral band at once. Results can be plotted over a true-colour RGB underlay and/or saved as a single-band label GeoTIFF.
Parameters
| Argument | Meaning |
|---|---|
dr_method |
None — cluster in full band space. "PCA", "ICA", or "MNF" — reduce to n_components first, then cluster. |
n_components |
Number of reduced dimensions when dr_method is set; use None when dr_method is None. |
clustering_method |
"KMEANS" or "GMM" (Gaussian mixture). |
n_clusters |
Number of clusters / mixture components. |
plot |
If True, show the cluster map beside the RGB context. |
save_geotiff |
If set, write a one-band integer label GeoTIFF to this path (uses TanagerSpec.grid_info when available; basic products may lack full CRS—see logs). |
save_png |
Optional path to save the figure. |
save_dpi |
DPI for save_png. |
**kwargs |
Passed to the clustering model (e.g. random_state, n_init, max_iter, GMM covariance_type, reg_covar, …). |
Returns
numpy.ndarray— 2D(rows, cols)of integer cluster labels; invalid pixels are‑9999(shown as gaps in the plot).
clusterd_array = tanager_sr_ortho.analysis.clustering(
dr_method= None, # PCA, MNF or None for full band space
n_components=2, # number of components
clustering_method="KMEANS", # GMM, KMEANS
n_clusters=8, # number of clusters
save_png="clustering_plot.png",
save_dpi=300,
# present_rgb="false_color_nir", # (true_color, false_color_nir)
save_geotiff="ortho_clustering.tif"
)
Supervised classification (SAM, Random Forest, Neural Network)¶
The supervised classification workflow is now organized as a two-step pipeline: build a spectral library from labeled pixels, then classify the full scene with SAM, Random Forest, or Neural Network. The library builder respects the current good-band mask from preprocessing and drop_bands(): masked bands are kept in the exported table but filled with a sentinel value so classifiers can drop those features consistently.
TanagerSpec.build_spectral_library— For each class name, pass one or more(col, row)locations fromTanagerSpec.plot.hunt_pixels.window_sizedefines a spatial neighborhood; valid spectra are collected, class mean/std spectra are computed, and masked bands are marked for later exclusion. Optionalexport_csvsaves the training table.TanagerSpec.analysis.classify_scene:method="SAM"— Spectral Angle Mapper compares each pixel vector to class mean spectra and ignores masked bands from the library.method="RF"— Random Forest trained ondf_training; wavelength-like feature columns are checked against the current cube before fitting.method="NN"— MLPClassifier on the same validated training table; tunenn_hidden_layer_sizes,nn_max_iter, and optionalnn_early_stopping.
Use save_geotiff to export label maps to GIS, and confidence_threshold with RF/NN when you want low-confidence pixels labeled as Unclassified.
Spectral library: build_spectral_library()¶
TanagerSpec.build_spectral_library(...) builds a supervised spectral library from named classes and pixel locations on the cube. For each class it cuts a square window around every listed coordinate, collects structurally valid spectra (no -9999 across bands), computes class mean and std, and returns both a dictionary and a pandas.DataFrame.
A key classification change is that the builder now receives the current good_bands mask automatically from TanagerSpec. Bands dropped by metadata or drop_bands() are not removed from the table; they are filled with masked_band_value (-9998 by default). This preserves the full wavelength column layout while giving SAM/RF/NN a clear marker for bands to ignore.
Parameters
| Argument | Default | Meaning |
|---|---|---|
targets |
— | dict: class name → one (col, row) tuple or a list of tuples for several training pixels per class. |
window_size |
5 |
Odd-sized spatial window (e.g. 5×5) around each point; spectra are collected over valid pixels in the window. |
plot |
True |
If True, show the mean ± std spectra and RGB context. |
export_csv |
None |
Path to save the DataFrame as CSV. |
save_png |
None |
Path to save the library figure. |
masked_band_value |
-9998 |
Sentinel written into masked-band columns so classifiers can exclude those bands. |
Returns
(library_dict, df_library)library_dict: each class →{"mean": 1D array, "std": 1D array}with masked mean bands marked bymasked_band_valuedf_library: training table with one row per extracted spectrum, per-wavelength columns, and aLabelcolumn
library_stats, df_training = tanager_sr_ortho.build_spectral_library(
# Feed it a LIST of coordinates you hunted
targets={
'veg1': [(369, 364)], # you can pass a list of coordinates for each class in same tube coordinates (y,x)
'veg2': (188, 287),# row, col
'building': (294,509),
'water': (518,451) # row, col
},
window_size=10, # Grabs 25 pixels around EVERY point!
export_csv="ml_spectral_training_set.csv"
)
Supervised classification: analysis.classify_scene()¶
TanagerSpec.analysis.classify_scene(...) labels every valid pixel in the scene with a class index using one of three methods. The classifier now treats masked spectral bands consistently across methods: masked_band_value columns from the spectral library are dropped before SAM/RF/NN classification, and the same retained-band mask is applied to the scene cube.
method |
Needs | Idea |
|---|---|---|
"SAM" |
library_means |
Spectral angle between each pixel spectrum and each class mean; masked bands are ignored and the smallest angle wins if below sam_threshold. |
"RF" |
df_training |
Random forest trained on rows from TanagerSpec.build_spectral_library after validating numeric wavelength columns against the current cube. |
"NN" |
df_training |
MLP neural network on the same validated, masked-band-aware training table. |
Parameters
| Argument | Role |
|---|---|
method |
"SAM", "RF", or "NN". |
library_means |
SAM only: class name → mean spectrum, or the full library_dict returned by TanagerSpec.build_spectral_library. |
df_training |
RF / NN only: pandas.DataFrame from TanagerSpec.build_spectral_library with a Label column and one column per wavelength. |
sam_threshold |
SAM: max spectral angle in radians to accept a match; larger angles become Unclassified. |
rf_estimators |
RF: number of trees. |
confidence_threshold |
RF / NN: optional probability cutoff from 0 to 1; low-confidence pixels become Unclassified. |
masked_band_value |
Sentinel used to identify library columns that should be ignored (-9998 by default). |
nn_hidden_layer_sizes |
NN: hidden layer sizes (default (100,)). |
nn_max_iter |
NN: max training iterations. |
nn_early_stopping |
NN: optional early stopping control; defaults to an automatic small-library-safe behavior. |
present_rgb |
RGB preset rendered behind the classification map (default "true_color"); same names as TanagerSpec.plot.rgb() presets. |
plot |
Show classification map over RGB. |
save_geotiff |
Optional path for a label GeoTIFF. |
save_png |
Optional path for the figure. |
Returns
numpy.ndarray— 2D per-pixel class labels, with unclassified/background pixels encoded as0or the nodata label used internally for invalid pixels.
## Spectral Angle Mapping (SAM) (SAM)
pure_means = {k: v['mean'] for k, v in library_stats.items()}
sam_prediction = tanager_sr_ortho.analysis.classify_scene(
method="SAM",
library_means=pure_means,
present_rgb="false_color_nir", # Optional: specify RGB preset for visualization (e.g., "true_color", "false_color_nir"
save_geotiff="ortho_sam_classification_sam.tif", # Optional: save the classification as a GeoTIFF,
)
rf_prediction = tanager_sr_ortho.analysis.classify_scene(
method="RF",
df_training=df_training, # Instantly passes the mass ML sample DataFrame flawlessly
present_rgb="false_color_nir", # Optional: specify RGB preset for visualization (e.g., "true_color", "false_color_nir"
save_geotiff="ortho_rf_classification_rf.tif", # Optional: save the
rf_estimators=100,
)
nn_predictions = tanager_sr_ortho.analysis.classify_scene(
method="NN",
df_training=df_training,
# Optional parameters for tuning the Neural Network:
nn_hidden_layer_sizes=(10,10), # e.g., Two hidden layers with 100 and 50 neurons
nn_max_iter=1000, # Max number of iterations for the solver
present_rgb="false_color_nir", # Optional: specify RGB preset for visualization (e.g., "true_color", "false_color_nir"
plot=True, # Set to True to display the classification map
save_png="nn_classification.png", # Optional: Save the plot directly
save_geotiff="nn_predictions.tif", # Optional: Save to a spatial GeoTIFF,
nn_early_stopping=False,
)