Image Operations¶
The Hypervision SDK provides a number of built-in operations that work as building blocks for more complex behaviour.
Slicing¶
The hsi.HSImage.slice()
How slicing works
Slicing is special, because it propagates back through operations as far as possible, which means that it can limit the amount of data being operated on after the previous operations have been constructed. For example, slicing a pipeline that loads data from a file will limit the amount of data being loaded from disk.
This feature is used internally in the library to implement the functionality of methods like
hsi.HSImage.array_plane()
A few examples:
img[:, :, :] # The full image
img[:20, :20, :] # The first 20 lines and samples
img[:, :, ::4] # Take only every 4th band
# Demonstration of how lines/samples/bands map to the index dimensions
lines = slice(start=0, end=10)
samples = slice(end=None)
bands = slice(end=None, step=2)
img[lines, samples, bands]
// The slice descriptor uses BIP order: (lines, samples, bands)
hv_slice_desc_t desc_full = {
.lines = hv_slice_elem_new_range(0, -1, 1),
.samples = hv_slice_elem_new_range(0, -1, 1),
.bands = hv_slice_elem_new_range(0, -1, 1),
};
hv_hs_image_t *full = hv_hs_image_slice(img, desc_full);
// First 20 lines and samples, all bands
hv_slice_desc_t desc_roi = {
.lines = hv_slice_elem_new_range(0, 20, 1),
.samples = hv_slice_elem_new_range(0, 20, 1),
.bands = hv_slice_elem_new_range(0, -1, 1),
};
hv_hs_image_t *roi = hv_hs_image_slice(img, desc_roi);
// Take only every 4th band
hv_slice_desc_t desc_stride = {
.lines = hv_slice_elem_new_range(0, -1, 1),
.samples = hv_slice_elem_new_range(0, -1, 1),
.bands = hv_slice_elem_new_range(0, -1, 4),
};
hv_hs_image_t *sliced_img = hv_hs_image_slice(img, desc_stride);
Note for Python users
The slicing syntax is a bit more restrictive than NumPy. You always need exactly three arguments. The reason for
this is that a hsi.HSImage
The slice indices are always in BIP format (lines, samples, bands) as shown in the example above. This can cause
confusion when used together with the hsi.HSImage.to_numpy()
img = hsi.HSImage.from_numpy(np.ones((10, 10, 10)), interleave=hsi.bil)
# Note how the index order differs between the two types. This is because `img` is in BIL format but the index
# method always accepts BIP format.
assert img[:, :, :5].to_numpy().shape == (10, 5, 10)
Elementwise operations¶
You can use common arithmetic elementwise operations using the regular operators +, -, *, /. Currently, only
these four are supported. The operations work on both hsi.HSImage
# Subtract two bands
diff = img[:, :, 500] - img[:, :, 810]
# Scale image from Hypervision 1700 to (0,1) (image is assumed to be float)
scaled = img / 254.
// Create slices
hv_slice_desc_t b500 = {
.lines = hv_slice_elem_new_range(0, -1, 1),
.samples = hv_slice_elem_new_range(0, -1, 1),
.bands = hv_slice_elem_new_index(500),
};
hv_slice_desc_t b810 = {
.lines = hv_slice_elem_new_range(0, -1, 1),
.samples = hv_slice_elem_new_range(0, -1, 1),
.bands = hv_slice_elem_new_index(810),
};
// Slice image into two bands and subtract them
hv_hs_image_t *band500 = hv_hs_image_slice(img, b500);
hv_hs_image_t *band810 = hv_hs_image_slice(img, b810);
hv_hs_image_t *diff = hv_hs_image_sub(band500, band810);
// Scale the image to (0,1) (image is assumed to be float32_t)
hv_hs_image_t *scaled = hv_hs_image_div_scalar_left_f32(img, 254.0);
Important
The scalar operations do not automatically convert the type. Instead, the output type is always
determined by the hsi.HSImage
You can convert the data types using hsi.HSImage.ensure_dtype()
Reduction operations¶
The library supports a limited set of reduction operations for calculating the mean, standard deviation, variance, and sum. The operations always work on a single axis only. Because the images are always three-dimensional, the operations reduce the specified axis to length 1, similar to how the keepdims argument works in NumPy.
img.mean_axis(hsi.bands) # Mean spatial image
# Workaround for performing multiple reductions. The result is the mean spectrum.
img.mean_axis(hsi.lines).mean_axis(hsi.samples)
// Mean over bands (spatial mean image)
hv_hs_image_t *mean_spatial = hv_hs_image_mean_axis(img, HV_AXIS_BANDS);
// Chained reductions to get mean spectrum
hv_hs_image_t *mean_over_lines = hv_hs_image_mean_axis(mean_spatial, HV_AXIS_LINES);
hv_hs_image_t *mean_spectrum = hv_hs_image_mean_axis(mean_over_lines, HV_AXIS_SAMPLES);
Matrix multiplication¶
Matrix multiplication is essential for many different kinds of processes, including projections and certain
machine learning models. The Hypervision SDK provides a generic method hsi.HSImage.dot() hsi.Axis
Let us start by looking at how to use the hsi.HSImage.dot()
Matrix multiplication calculation details
Let us look at how the calculations are performed. Given a hyperspectral image \(H \in \mathbb{R}^{L\times S\times B}\) and a vector operand \(\mathbf{u} \in \mathbb{R}^{B}\), the dot product is defined as:
For the other two axes, let \(\mathbf{v} \in \mathbb{R}^{L}\) and \(\mathbf{w} \in \mathbb{R}^{S}\), then:
For vector-matrix multiplication, let \(U \in \mathbb{R}^{N\times B}\) be the operand. The operation is then defined as:
For the other two axes, let \(V \in \mathbb{R}^{N\times L}\) and \(W \in \mathbb{R}^{N\times S}\), then:
Note
The implementation uses matrix multiplication as much as possible for efficiency. For example, A vector-matrix multiply along the bands axis in an BIP image is implemented as a matrix-matrix multiplication performed on each image plane, with each plane consisting of bands as its rows and samples as its columns.
# Create a new vector with element 2 and 200 set to 0.5
operand = np.zeros(920)
operand[2] = 0.5
operand[200] = 0.5
# Calculate the dot product. The result is a mix between band 2 and 200.
res = img.dot(operand, hsi.Bands)
# Create a new matrix operand
operand = np.zeros((2, 920))
operand[0, 2] = 0.5
operand[0, 200] = 0.5
operand[1, 25] = 1
# Calculate the matrix-vector product. The result is an image with 2 output bands.
# The first band is equal to the dot-product above. The second band is simply band 25 from the input
# image.
res = img.dot(operand, hsi.bands)
// Create a new vector with element 2 and 200 set to 0.5
float32 operand_array[920];
operand_array[2] = 0.5;
operand_array[200] = 0.5;
slice_ref_float32_t operand_slice = {
.len = 920,
.ptr = &operand_array
};
slice_ref_size_t shape = {
.len = 1,
.ptr = (size_t[1]){920},
}
hv_array_t operand;
hv_array_from_f32(shape, operand_slice, &operand);
// Calculate the dot product. The result is a mix between band 2 and 200.
hv_hs_image_t* res = hv_hs_image_dot(img, operand, HV_AXIS_BANDS);
// Create a new matrix operand
float32 operand_array2[2*920];
operand_array2[2] = 0.5;
operand_array2[200] = 0.5;
operand_array2[1*920 + 25] = 0.5;
slice_ref_float32_t operand_slice2 = {
.len = 920*2,
.ptr = &operand_array2
};
slice_ref_size_t shape2 = {
.len = 2,
.ptr = (size_t[1]){2, 920},
}
hv_array_t operand2;
hv_array_from_f32(shape2, operand_slice2, &operand2);
// Calculate the matrix-vector product. The result is an image with 2 output bands.
// The first band is equal to the dot-product above. The second band is simply band 25 from the input
// image.
hv_hs_image_t* res2 = hv_hs_image_dot(img, operand2, HV_AXIS_BANDS);
Other operations¶
The SDK provides a number of useful operations that
# Apply mean-binning (beware that the dtype will not be changed automatically)
# Use `ensure_dtype()` first if the input type is not big enough to hold the binning results
condensed = img.binning(8, hsi.bands)
# Clip image to the provided range
clipped = img.clip(0, 1)
# Convert NaN values to the provided value
non_nan = img.nan_to_num(0)
# Type conversions
new_type = img.as_dtype(hsi.float32) # This one always does the convertion, even if the type is already the same
ensured_type = img.ensure_dtype(hsi.float32) # This one only converts if the types are different
// Apply mean-binning (beware that the dtype will not be changed automatically)
// Use `ensure_dtype()` first if the input type is not big enough to hold the binning results
hv_hs_image_t* condensed = hv_hs_image_binning(img, 8, HV_AXIS_BANDS);
// Clip image to the provided range
hv_hs_image_t* clipped = hv_hs_image_clip(img, 0.0f, 1.0f);
// Convert NaN values to the provided value
hv_hs_image_t* non_nan = hv_hs_image_nan_to_num(img, 0.0);
// Type conversions
// This one always does the convertion, even if the type is already the same
hv_hs_image_t* new_type = hv_hs_image_as_dtype(img, HV_DTYPE_F32);
// This one only converts if the types are different
hv_hs_image_t* ensured_type;
hv_hs_image_ensure_dtype(img, HV_DTYPE_F32, &ensured_type);