HV SDK
A high-performance SDK for hyperspectral imaging — unifying control of the Qtechnology Hypervision camera line with efficient data processing and file I/O. The SDK is built in Rust and provides Python and C bindings.
The library makes it easy to work with different HSI sources as it supports input and output of multiple datacube file formats (currently PAM, ENVI and TIFF) as well as live camera data. In this way it is easy to move from a proof of concept phase to a final solution, as well as being able to debug workflows using saved data.
The HV SDK is used as the backend for the HSI related functionality of the HV Explorer. This makes it easy to take a workflow from the HV Explorer and implement it as a standalone program using the HV SDK library.
The HV SDK library provides a generic interface for working with HSI files and live camera data (and simulating a camera from a file). Using lazy operations and streaming, it optimizes memory and cpu usage for defined operations.
The HSI Tools library is still under development: new features and improvements/bug fixes are constantly being added.
The alpha version has been released in March 2025 and live data capture support was added in September 2025.
Python >= 3.10
RAM: 8Gb (minimum recommended)
Installation
The HV Explorer and HV SDK are proprietary tools developed internally by qtec, and are therefore only free to use in conjuction with qtec's cameras and/or HSI data acquired using qtec's cameras.
- Python package:
pip install qtec-hv-sdk --upgrade --index-url https://gitlab.com/api/v4/projects/75709056/packages/pypi/simple
Or for a specific version:
pip install qtec-hv-sdk==$VERSION --index-url https://gitlab.com/api/v4/projects/75709056/packages/pypi/simple
Check the latest version number at the official HV SDK docs.
Tutorial
See Recipes from the official docs for more examples.
Also check the Examples section for more examples using the HV SDK.
Reading and writing datacubes
import hsi
img = hsi.open("path/to/file.hdr") # Expects ENVI file due to the extension
img.write("path/to/output.pam") # Writes PAM format
Changing the interleave type
See exporting datacubes under the HV Explorer tutorial for details on the relevance of using the correct interleave when writing datacubes.
import hsi
a = hsi.open("path/to/file.hdr").to_numpy_with_interleave(hsi.bil)
Reflectance calibration
import hsi
from hsi.preprocessing import make_reference, reflectance_calibration
img = hsi.open("path/to/file.hdr")
dark = hsi.open("path/to/dark_file.hdr")
# inline white reference
white_ref = make_reference(img[:100, :, :])
dark_ref = make_reference(dark)
reflectance = reflectance_calibration(img, white_ref, dark_ref)
# Note that the data is now of float 32 type
# Consider scaling and converting to 'uint8' to save memory
reflectance_uint8 = (255*reflectance).ensure_dtype(hsi.uint8)
Camera interface
import hsi
def current_datetime_filename():
return datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
N_IMGS = 10
filename = f"/tmp/_HSI_{current_datetime_filename()}"
SAVE_CUBE = True
# Desired settings
EXP = 1000
FPS = 100
# Horizontal crop
H_START = 200
H_END = 300
# Bands
V_START = 0
V_END = 920
# ETH_B interface
cam = hsi.HSCamera("10.100.10.100")
# Get information
print(f"{cam.get_config()=}")
print(f"{cam.get_settings()=}")
print(f"{cam.get_crop()=}")
print(f"{cam.get_exposure()=}")
print(f"{cam.get_framerate()=} {cam.get_framerate_list()=}")
#print(f"Binning: {cam.get_horizontal_binning()}x{cam.get_vertical_binning()}")
print(f"{cam.get_bands()=}")
#print(f"{cam.get_wavelengths()=}")
# Set parameters
print(f"{cam.set_exposure(EXP)=}")
print(f"{cam.set_framerate(FPS)=}")
#cam.set_horizontal_binning(1)
#cam.set_vertical_binning(1)
print(f"{cam.set_horizontal_crop((H_START, H_END))=}")
print(f"{cam.set_bands([(V_START, V_END)])=}")
# Multiple band intervals (up to 8 regions):
#print(f"{cam.set_bands([(V_START1, V_END1), (V_START2, V_END2)])=}")
######### Datacube Capture
# Create a stream object
img = cam.to_hs_image()
# Configure the datacube size (N_IMGS)
datacube = img[:N_IMGS, :, :]
# Write to file or convert to numpy:
# triggers the streaming start
if SAVE_CUBE:
#hsi.write(datacube, filename + ".pam")
hsi.write(datacube, filename + ".hdr")
#hsi.write(datacube, filename + ".tif")
else:
array = datacube.to_numpy()
# Also triggers the streaming start
#datacube.resolve()
######### Datacube Processing
...
See also the more complete example under the Quick Start section and be aware of data throughput limitations.
Simulated Camera
From version 0.7.3 the SDK also allows to simulate the output of a HSI camera by providing an input datacube.
# Create simulated camera with img as source
src_img = hsi.open(<filename>)
cam = hsi.HSCamera(src_img)
# Create a stream object
img = cam.to_hs_image()
# Configure the datacube size (N_IMGS)
datacube = img[:N_IMGS, :, :]
# Trigger the streaming start
datacube.resolve()
#array = datacube.to_numpy()
See https://docs.qtec.com/hv-sdk/guide/camera.html for more information.
Buteo interface
In its normal configuration, the Buteo has a host-PC which controls the whole system (screen, camera, lights and belt) in order to make it simple to use. However, more flexibility is desired in some situations. Therefore, from version 0.9.1, the SDK also implements functionality that allows the camera to control the belt and lights of the Buteo directly.
See https://docs.qtec.com/hv-sdk/guide/buteo.html for more information.
This functionality is still in testing stages and the interface may therefore change.
Note that in order to give the camera direct access to the belt and light controller (instead of having the host-PC controlling them) it is required to switch some cables around inside the the Buteo cabinet. Contact qtec for more information on the procedure if you wish to explore this option.
The process also requires providing the camera with a proper software image to boot from. See qtecOS Image for more information on how to create the required bootable media (CFAST/USB drive).
Note that the Buteo screen is not functional while the camera is in control of the belt and lights. All configuration and control needs to be done via the python script (using HV SDK) running on a external PC (or the camera itself via the terminal).
This process is also easily reversible if it is desired to go back to original Buteo functionality.
import hsi
from hsi import StageController
from datetime import datetime
def current_datetime_filename():
return datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
# Settings
BIT_MODE = 8
WIDTH = 1280
N_BANDS = 920
EXP_TIME = 3000 # max exposure ~10.000 us at 100fps
TARGET_DIR = "/tmp/"
filename = f"_HSI_{current_datetime_filename()}"
# Camera IP
buteo = StageController("10.100.10.100")
# Can only set fps OR velocity not both
# So we reccomend setting the max fps for the available 1Gbs connection
# The actual throughtput seen experimentally for the 1Gbs connection is 12-15% lower than the max theorical value
MAX_FPS = pow(2,30) / (WIDTH*N_BANDS*BIT_MODE) * 0.85
#print(f"{MAX_FPS=}")
# Set conveyor belt velocity in mm/s
#buteo.velocity = 20.0
# max fps for the 1Gbs connection ~100 (for image size of 1280px x 900bands)
buteo.framerate = MAX_FPS
# Set conveyor move distance in mm.
buteo.distance = 50.0
# Set oversampling (lines)
# An oversampling of 4x gives a correct aspect ratio visually
buteo.oversampling = 4.0
# Adjust Camera Settings
exp_time = buteo.hs_camera.set_exposure(EXP_TIME)
# Gain is not available at the moment so it is fixed at 1x
#gain = buteo.hs_camera.set_gain(0)
# Change crop top in config to match the one from the Buteo
config = buteo.hs_camera.get_config()
print(f"{config=}")
#cal = config.calibration
#cal.crop_top = 20 # must be multiples of 4
#config.calibration = cal
#config = buteo.hs_camera.set_config(config)
#print(f"{config=}")
# Adjust spacial cropping
#crop = buteo.hs_camera.get_crop()
#crop = buteo.hs_camera.set_horizontal_crop(0, crop.max_width)
#crop = buteo.hs_camera.set_horizontal_crop((H_START, H_END))
h_crop = buteo.hs_camera.set_horizontal_crop(0, WIDTH)
print(f"{h_crop=}")
# Adjust number of bands or band intervals
#bands = buteo.hs_camera.get_bands()
# Multiple band intervals (up to 8 regions):
#bands = buteo.hs_camera.set_bands([(V_START1, V_END1), (V_START2, V_END2)])
bands = buteo.hs_camera.set_bands([(0, N_BANDS)])
print(f"{bands=}")
# Convert to HSImage object
image = buteo.to_hs_image()
# Don't change settings after this point
# Start streaming plus belt/lights
image.resolve()
# Save to file: the SDK supports PAM, ENVI and TIFF
hsi.write(image, TARGET_DIR + filename + ".hdr")
#hsi.write(image, TARGET_DIR + filename + ".pam")
#hsi.write(image, TARGET_DIR + filename + ".tiff")
# Save the current settings to a txt file as well if desired
try:
with open(TARGET_DIR + filename + "_settings.txt", 'w') as f:
f.write("--- Camera Configuration ---\n")
f.write(f"{filename=}\n")
f.write(f"{config=}\n")
f.write(f"{bands=}\n")
f.write(f"{exp_time=}\n")
f.write(f"{h_crop=}\n")
f.write(f"{buteo.framerate=}\n")
f.write(f"{buteo.distance=}\n")
f.write(f"{buteo.oversampling=}\n")
f.write(f"{buteo.velocity=}\n")
print(f"Successfully saved settings to {filename}")
except Exception as e:
print(f"An error occurred: {e}")
The hsi.StageController communicates directly with a REST API interface
running on the camera at <camera_ip>:5001.
Information about the available endpoints (for controlling the belt and lights)
can be seen at via the live documentation at <camera_ip>:5001/docs.
PCA
import hsi
from hsi.ml import pca_helper
import numpy as np
from sklearn.decomposition import PCA
img = hsi.open("path/to/file").as_dtype(hsi.float32)
def gen_select(img, n_samples_per_line=10):
"""Sample randomly (with the same number of samples per line) from the image."""
# Assumes BIL (doesn't work for BSQ/BIP)
def select(plane):
sample = np.random.choice(np.arange(plane.shape[1]), size=n_samples_per_line)
sample = plane[:, sample]
return sample
return img.ufunc(select) # Any Python function can be passed here
# Convert interleave type
img = img.to_interleave(hsi.Interleave.BIL)
# Get subsample from image in memory-efficient manner
s_out = gen_select(img).to_numpy()
s_out = s_out.transpose((0, 2, 1))
s_out = s_out.reshape((-1, s_out.shape[2]))
# Fit model
model = PCA(n_components)
model.fit(s_out)
hs_model = pca_helper(model)
# Here, the prediction function is created.
out = hs_model(img)
# The calculation is only applied when requested, similar to other operations
result = out.to_numpy()
See also the more complete example under the Examples section.
Support
Report bugs by writing an email to: hv-sdk-support