Skip to main content

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.

Lazy Loading

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.

Downloaded scripts

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}")

Download this script

  • lines are the spatial rows of the cube.
  • samples are the spatial columns.
  • bands are 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

Download this script

RAM usage

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=True limits the result to the valid reflectance range [0, 1].

Download this script

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}")

Download this script

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()

Download this script

tip

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()

Download this script

select_bands

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()

Download this script