Annotations and ROIs
When you draw ROIs in HV Explorer and export them as a JSON file, the SDK can load those annotations directly and use them in processing pipelines — no need to recreate masks by hand. This is the bridge between exploratory GUI work and reproducible SDK scripts: the same regions you drew for inspection become the training data for classifiers, regressors, and PCA models.
Each annotation carries a descriptor (the shape and position of the ROI) and
a properties dict (your labels, concentration values, or other metadata). The
SDK turns a descriptor into a pixel selection with select_mask_from_descriptor(),
which works for rectangle, polygon, and ellipse ROIs.
For the conceptual overview and full helper function list, see Annotations and ROIs in the usage guide.
The downloadable .py files can be run without editing paths by setting
environment variables such as HSI_EXAMPLE_BASE_DIR and
HSI_EXAMPLE_ANNOTATIONS. See
Running Downloaded Scripts
for the full list of supported overrides.
Extract Spectra From an ROI
Use hs.annotations.open() to load the exported JSON file. Each annotation
contains an SDK descriptor that can be passed directly to
select_mask_from_descriptor(), so you do not need to recreate polygon,
rectangle, or ellipse masks manually.
def annotation_value(value):
return value[0] if value is not None else None
def load_annotations():
annotations_path = Path(ANNOTATIONS)
if not annotations_path.is_absolute():
annotations_path = BASE_DIR / annotations_path
if not annotations_path.exists():
raise SystemExit("Set HSI_EXAMPLE_ANNOTATIONS to an annotations JSON file.")
return hs.annotations.open(str(annotations_path))
def find_roi(ann_file, cube_name=CUBE, roi_type=ROI_TYPE):
for annot in ann_file.annotations:
image_path = Path(annot.image_path or cube_name).name
if image_path != Path(cube_name).name:
continue
if annotation_value(annot.properties.get("type")) == roi_type:
return annot
raise SystemExit(f"No ROI with type {roi_type!r} found for {cube_name!r}.")
reflectance = open_reflectance_cube()
ann_file = load_annotations()
roi = find_roi(ann_file)
selected = reflectance.select_mask_from_descriptor(roi.descriptor)
spectra = selected.to_numpy_with_interleave(hs.bip)[:, 0, :]
mean = spectra.mean(axis=0)
std = spectra.std(axis=0)
wavelengths = wavelengths_for_image(reflectance)
plt.plot(wavelengths, mean, label="mean")
plt.fill_between(wavelengths, mean - std, mean + std, alpha=0.25, label="+/- std")
plt.xlabel("Wavelength [nm]")
plt.ylabel("Reflectance")
plt.title(f"ROI mean spectrum: {roi.title}")
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()
The selected ROI spectra are exported as ordinary NumPy arrays with shape
(n_pixels, n_bands). That is the same shape used by the PCA, classification,
and regression examples when they train from annotated pixels.
Plot ROIs on an Image
Plot annotations on a preview image before training or interpreting results. This makes it easier to catch mismatched annotation files, wrong cube paths, or ROIs that do not cover the intended samples.
import matplotlib.patches as mpatches
def patch_top_center(patch):
"""Return (x_center, y_top) in data coordinates for any patch type.
In image coordinates y increases downward, so the visual top of a shape
is its minimum y value.
"""
if isinstance(patch, mpatches.Ellipse):
x = patch.center[0]
y = patch.center[1] - patch.height / 2
elif isinstance(patch, mpatches.Rectangle):
x = patch.get_x() + patch.get_width() / 2
y = patch.get_y()
else: # Polygon or any other patch — use path vertices (already in data coords)
verts = patch.get_path().vertices
x = (verts[:, 0].min() + verts[:, 0].max()) / 2
y = verts[:, 1].min()
return x, y
reflectance = open_reflectance_cube()
ann_file = load_annotations()
target_wavelengths = (650, 550, 460)
band_indices = [
band_index_for_wavelength(wv, reflectance)
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):
# select_bands() expects increasing band indices, so map each requested
# RGB wavelength back to its position in the sorted cube.
selected_channel = sorted_band_indices.index(band_index)
rgb[:, :, channel] = contrast_stretch(selected_cube[:, :, selected_channel])
fig, ax, patches = hs.annotations.plot_image_with_annotations(
rgb,
ann_file,
label=False,
show_axes=False,
)
for annot, patch in zip(ann_file.annotations, patches):
# plot_image_with_annotations() draws the ROI geometry. The returned
# matplotlib patches let us add our own labels without rebuilding the ROIs.
label = annotation_value(annot.properties.get("type")) or ""
x, y = patch_top_center(patch)
# va="bottom" places the text's baseline at y, so the text body
# extends toward smaller y values — visually above the ROI.
ax.text(
x, y,
label,
ha="center",
va="bottom",
color=annot.color,
fontsize=9,
fontweight="bold",
)
ax.set_title("ROIs on false RGB preview")
plt.show()