Basics
This page covers the core starting point for any SDK workflow: opening a datacube, reading regions, calibrating to reflectance or absorbance, and visualizing spectra and images.
For spectral preprocessing, band selection, binning, saving, and simple analysis, see Preprocessing and Analysis. For working with HV Explorer annotations and ROIs, see Annotations and ROIs.
Refer to the HV SDK Usage Guide for conceptual details about lazy operations, interleave, calibration, and streaming. And for the complete API reference, see the official HV SDK documentation, which also contains several guides on how to work with the framework.
The SDK uses lazy processing. Opening a cube or slicing an Image describes
the work to do; data is read first when you export to NumPy, write a file,
display an image, or otherwise request actual values.
The downloadable .py files can be run without editing paths by setting
environment variables such as HSI_EXAMPLE_BASE_DIR. See
Running Downloaded Scripts
for the full list of supported overrides.
Open and Inspect a Datacube
Use hs.open() to open a datacube. This does not load the full cube into RAM.
The shape tells you the number of lines, samples, and spectral bands.
cube = hs.open(str(BASE_DIR / CUBE))
print(f"Shape: {cube.shape}")
print(f"Lines: {cube.shape.lines}")
print(f"Samples: {cube.shape.samples}")
print(f"Bands: {cube.shape.bands}")
print(f"Metadata: {cube.meta}")
linesare the spatial rows of the cube.samplesare the spatial columns.bandsare the spectral channels.- The image metadata (
cube.meta) stores information such as shape, dtype, interleave, byte order, and optional wavelength information.
Read Lines, Bands, and Regions
Datacubes can be accessed via slicing, similar to NumPy arrays.
The important difference is that SDK slicing always uses the logical order
(lines, samples, bands), independent of how the data is stored on disk.
Once data is exported to NumPy, the axis order depends on the interleave you
request. For example, hs.bip gives (lines, samples, bands) for 3D exports,
which is usually the most convenient layout for pixel-wise spectral work.
The most common access patterns are one scan line, one wavelength band, or one
small spatial region. Use SDK constants such as hs.lines, hs.bands, and
hs.bip rather than strings.
For a single band, array_plane() is equivalent to slicing and exporting the
band as BIP. array_plane() is usually easier to read when you want a complete
line or band plane.
cube = hs.open(str(BASE_DIR / CUBE))
line_index = 100
band_index = 164
x0, x1 = 300, 500
y0, y1 = 200, 350
line = cube.array_plane(line_index, hs.lines)
band = cube.array_plane(band_index, hs.bands)
band_from_slice = cube[:, :, band_index].to_numpy_with_interleave(hs.bip)[:, :, 0]
assert np.array_equal(band, band_from_slice)
region = cube[y0:y1, x0:x1, :].to_numpy_with_interleave(hs.bip)
print(f"Line shape: {line.shape}") # bands x samples
print(f"Band shape: {band.shape}") # lines x samples
print(f"Region shape: {region.shape}") # lines x samples x bands
Prefer slicing before exporting to NumPy in order to benefit from the SDK lazy loading.
Calibration
Raw HSI values depend on exposure, illumination, sensor response, and dark current. Calibration normalizes the capture using a dark reference and a white reference.
Reflectance Calibration
cube = hs.open(str(BASE_DIR / CUBE))
dark = hs.open(str(BASE_DIR / DARK_REF))
white = hs.open(str(BASE_DIR / WHITE_REF))
dark_ref = make_reference(dark)
white_ref = make_reference(white)
reflectance = reflectance_calibration(cube, white_ref, dark_ref, clip=True)
band_index = 164
refl_band = reflectance.array_plane(band_index, hs.bands)
print(f"Reflectance band min/max: {refl_band.min():.3f}, {refl_band.max():.3f}")
make_reference()collapses a reference cube into a reference image by averaging all lines and caching the result.reflectance_calibration()applies the dark and white correction.clip=Truelimits the result to the valid reflectance range[0, 1].
Absorbance Calibration
Absorbance is common in spectroscopy and chemometrics. It is calculated from
reflectance as -log10(reflectance). The small lower clip value avoids taking
the logarithm of zero.
def make_references():
dark = hs.open(str(BASE_DIR / DARK_REF))
white = hs.open(str(BASE_DIR / WHITE_REF))
return make_reference(dark), make_reference(white)
def open_reflectance_cube(cube_name=CUBE):
dark_ref, white_ref = make_references()
img = hs.open(str(BASE_DIR / cube_name))
return reflectance_calibration(img, white_ref, dark_ref, clip=True)
reflectance = open_reflectance_cube()
absorbance = reflectance.ufunc(lambda meta, plane: -np.log10(np.clip(plane, 1e-6, 1.0)))
band_index = 164
abs_band = absorbance.array_plane(band_index, hs.bands)
print(f"Absorbance band min/max: {abs_band.min():.3f}, {abs_band.max():.3f}")
Visualization
Single Band
Display one calibrated band with a color map.
def contrast_stretch(image, percentiles=(1, 99)):
low, high = np.percentile(image, percentiles)
return np.clip((image - low) / (high - low + 1e-8), 0, 1)
reflectance = open_reflectance_cube()
band_index = 164
band = reflectance.array_plane(band_index, hs.bands)
plt.imshow(contrast_stretch(band), cmap="viridis")
plt.title(f"Band nr: {band_index}")
plt.axis("off")
plt.show()
The contrast stretch is only for visualization. Keep calibrated reflectance or absorbance values for quantitative processing.
False RGB Preview
A false RGB image is a quick way to inspect a calibrated cube. Pick three wavelengths, find the nearest bands, stretch each band, and stack them into RGB.
This example keeps the manual band-to-wavelength calculation visible so you can
see how the mapping works. In normal SDK workflows, prefer wavelength metadata
from img.meta.spectral.wavelengths, as shown in the following examples.
reflectance = open_reflectance_cube()
target_wavelengths = (650, 550, 460)
band_indices = [
band_index_for_wavelength(wv, reflectance.shape.bands)
for wv in target_wavelengths
]
sorted_band_indices = sorted(band_indices)
selected_cube = reflectance.select_bands(sorted_band_indices).to_numpy_with_interleave(hs.bip)
rgb = np.empty((*selected_cube.shape[:2], 3), dtype=np.float32)
for channel, band_index in enumerate(band_indices):
selected_channel = sorted_band_indices.index(band_index)
rgb[:, :, channel] = contrast_stretch(selected_cube[:, :, selected_channel])
plt.imshow(rgb)
plt.title(f"False RGB bands: {band_indices}")
plt.axis("off")
plt.show()
The select_bands() call reads the three requested bands together, which is
more efficient than extracting each band as a separate plane. Pass the indices
in increasing order, then map the selected planes back to display order if
needed.
Plot One Pixel Spectrum
To inspect a spectrum, slice one pixel and export it as BIP so all bands for the pixel are contiguous.
This is often the first debugging step when calibration or preprocessing looks wrong.
reflectance = open_reflectance_cube()
x = 450
y = 320
spectrum = reflectance[y, x, :].to_numpy_with_interleave(hs.bip)[0, 0, :]
wavelengths = wavelengths_for_image(reflectance)
plt.plot(wavelengths, spectrum)
plt.xlabel("Wavelength [nm]")
plt.ylabel("Reflectance")
plt.title(f"Spectrum at x={x}, y={y}")
plt.grid(True, alpha=0.3)
plt.show()