Introduction¶
The Hypervision SDK is built around the concept of declarative pipelines that are constructed by applying operations
on the qtec_hv_sdk.Image qtec_hv_sdk.Image
In general, we can group the kinds of qtec_hv_sdk.Image
- Inputs
Any type that either holds its own data or represents an interface to a data producer like a file or camera.
- Operations
Any type that is composed of other
qtec_hv_sdk.Imageelements and performs some operation or transformation of their data.
Important
In this documentation, instances of the qtec_hv_sdk.Image
Let us look at an example. We start by opening a file using the qtec_hv_sdk.open()
img: qtec_hv_sdk.Image = hs.open("<path-to-file>")
hv_image_t* img = hv_hsi_file_open("<path-to-file>");
The returned image object represents a file, but the data has not yet been loaded. Using the API, it is possible to fetch the data in a number of ways:
# All of these operations are blocking
array = img.to_numpy() # read the entire file into a NumPy array
plane = img.array_plane(200, hs.bands) # read a single 2d plane
img = img.resolve() # read the file into an in-memory buffer
# This operation does not block
stream = img.stream() # create a stream object
// All of these operations are blocking
// read the entire file into an array
hv_array_t* array;
hv_image_full(img, &array);
// read a single 2d plane
hv_array_t* plane;
hv_image_array_plane(img, 200, HV_AXIS_BANDS, &plane);
// read the file into an in-memory buffer
hv_image_t* img_array;
hv_image_resolve(img, &img_array);
// This operation does not block
// create a stream object
hv_stream_t* stream;
hv_image_stream(img, &stream);
Not that all of these are blocking, except the qtec_hv_sdk.Image.stream() qtec_hv_sdk.Stream
Note
The other operations use qtec_hv_sdk.Stream
Important
A running qtec_hv_sdk.Stream qtec_hv_sdk.Image.to_numpy()
Only use qtec_hv_sdk.Image.stream()
Warning
The qtec_hv_sdk.Stream
You can expect the type to be particularly useful for interfacing the SDK with your own custom processes.
A complete example¶
To better understand how pipelines work in the SDK, let’s take a look at a complete example of a calculation that is used in hyperspectral image analysis, reflectance calibration:
import qtec_hv_sdk as hs
# Open the input files
img = hs.open("image.pam")
white_img = hs.open("white_ref.pam")
dark_img = hs.open("dark_ref.pam")
# Convert the main image to float32
img = img.ensure_dtype(hs.float32)
# Pre-calculate the white reference
white_ref = white_img
.ensure_dtype(hs.float32)
.mean_axis(hs.lines)
.resolve()
# Pre-calculate the dark reference
dark_ref = dark_img
.ensure_dtype(hs.float32)
.mean_axis(hs.lines)
.resolve()
# Perform the calibration itself
calibrated = (img - dark_ref) / (white_ref - dark_ref)
# Write the output to disk
hs.write(calibrated, "calibrated.pam")
int err = 0;
// Open files and get HSImage handles
hv_hsi_file_t* f_img = NULL;
hv_hsi_file_t* f_white = NULL;
hv_hsi_file_t* f_dark = NULL;
err = hv_hsi_file_open("image.pam", &f_img);
if (err) return err;
err = hv_hsi_file_open("white_ref.pam", &f_white);
if (err) return err;
err = hv_hsi_file_open("dark_ref.pam", &f_dark);
if (err) return err;
hv_image_t* img = NULL;
hv_image_t* white_img = NULL;
hv_image_t* dark_img = NULL;
img = hv_hsi_file_to_image(&f_img);
white_img = hv_hsi_file_to_image(&f_white);
dark_img = hv_hsi_file_to_image(&f_dark);
// Create a float32_t version of the image
hv_image_t* img_f32 = NULL;
err = hv_image_as_dtype(img, HV_DTYPE_F32, &img_f32);
if (err) return err;
hv_image_free(&img);
// Create the white reference using the following steps:
// 1. Convert the data type to float32_t
hv_image_t* white_f32 = NULL;
err = hv_image_as_dtype(white_img, HV_DTYPE_F32, &white_f32);
if (err) return err;
// 2. Compute the mean image along the lines axis
hv_image_t* white_mean = NULL;
err = hv_image_mean_axis(white_f32, HV_AXIS_LINES, &white_mean);
if (err) return err;
// 3. Resolve the computation to an in-memory array
hv_image_t* white_ref = NULL;
err = hv_image_resolve(white_mean, &white_ref);
if (err) return err;
hv_image_free(&white_img);
hv_image_free(&white_f32);
hv_image_free(&white_mean);
// Create the dark reference using the same steps.
hv_image_t* dark_f32 = NULL;
err = hv_image_as_dtype(dark_img, HV_DTYPE_F32, &dark_f32);
if (err) return err;
hv_image_t* dark_mean = NULL;
err = hv_image_mean_axis(dark_f32, HV_AXIS_LINES, &dark_mean);
if (err) return err;
hv_image_t* dark_ref = NULL;
err = hv_image_resolve(dark_mean, &dark_ref);
if (err) return err;
hv_image_free(&dark_img);
hv_image_free(&dark_f32);
hv_image_free(&dark_mean);
// Calculate the reflectance using the formula (img - dark) / (white - dark)
// 1. Subtract the dark reference from the image
hv_image_t* img_minus_dark = NULL;
err = hv_image_sub(img_f32, dark_ref, &img_minus_dark);
if (err) return err;
// 2. Subtract the dark reference from the white reference
hv_image_t* white_minus_dark = NULL;
err = hv_image_sub(white_ref, dark_ref, &white_minus_dark);
if (err) return err;
// 3. Divide the two intermediary results
hv_image_t* calibrated = NULL;
err = hv_image_div(img_minus_dark, white_minus_dark, &calibrated);
if (err) return err;
// Write the result to disk (adjust path/format as needed)
err = hv_image_write(calibrated_buf, "calibrated.pam");
if (err) return err;
// Cleanup
hv_image_free(&img_f32);
hv_image_free(&white_ref);
hv_image_free(&dark_ref);
hv_image_free(&img_minus_dark);
hv_image_free(&white_minus_dark);
hv_image_free(&calibrated);
hv_image_free(&calibrated_buf);
The example combines multiple source images and operations to achieve a result. Much of the actual computation is
spent generating each 2d reference from a reference data cube. Note how qtec_hv_sdk.Image.resolve() white_ref or
dark_ref would run the computation again.
Knowing when to use qtec_hv_sdk.Image.resolve() qtec_hv_sdk.Image.mean_axis()
Tip
Use caching whenever you have to reuse the result of a computation, especially when that computation is expensive to run.