Custom Imagery Loaders¶
VISTA natively loads imagery from its own HDF5 format (versions 1.5, 1.6, and 1.7). If your data lives in a different format — FITS, TIFF stacks, NetCDF, a proprietary binary, etc. — you can integrate it into VISTA in two ways:
Programmatic loading — parse your file in a Python script, construct
ImageryandSensorobjects, and launch VISTA viaVistaApp. This is the simplest approach and requires no changes to VISTA itself.GUI integration — add a menu action and a
DataLoaderThreadcode path so users can load your format directly from the VISTA GUI.
This guide covers both approaches in detail.
Prerequisites¶
Before writing a loader you need to understand how VISTA represents data internally:
Imagery (
vista.imagery.Imagery) — adataclassholding a 3Dfloat32image array(num_frames, height, width), frame numbers, optional timestamps, and a reference to aSensor.Sensor (
vista.sensors.Sensoror a subclass) — holds radiometric calibration data and provides geolocation methods. EveryImagerymust reference aSensor.loaded_frame_count — an
Imageryattribute that enables incremental loading. When set to an integer, onlyimages[0:loaded_frame_count]are treated as valid by the viewer.
See Custom Sensors for details on implementing custom sensors.
Approach 1: Programmatic Loading¶
The fastest way to load a custom format is to write a standalone script or function that:
Reads your file format into NumPy arrays.
Creates a
Sensor(or subclass).Creates an
Imageryobject.Passes them to
VistaApp.
Minimal Example¶
import numpy as np
from vista.app import VistaApp
from vista.imagery.imagery import Imagery
from vista.sensors.sensor import Sensor
def load_my_format(file_path: str) -> Imagery:
"""Load imagery from a custom file format.
Parameters
----------
file_path : str
Path to the file to load.
Returns
-------
Imagery
VISTA Imagery object.
"""
# --- Replace this with your own parsing logic ---
# For example, reading a FITS file:
# from astropy.io import fits
# hdu = fits.open(file_path)
# images = hdu[0].data.astype(np.float32)
images = np.fromfile(file_path, dtype=np.float32).reshape(-1, 256, 256)
# ------------------------------------------------
num_frames = images.shape[0]
frames = np.arange(num_frames)
# Create a sensor (use Sensor for minimal, or SampledSensor / custom for geolocation)
sensor = Sensor(name="My Sensor")
# Optionally create timestamps
start = np.datetime64("2024-01-01T00:00:00")
times = np.array([start + np.timedelta64(i * 100, "ms") for i in range(num_frames)])
imagery = Imagery(
name="My Imagery",
images=images,
frames=frames,
sensor=sensor,
times=times,
)
return imagery
if __name__ == "__main__":
imagery = load_my_format("data/my_file.bin")
app = VistaApp(imagery=imagery)
app.exec()
Key Points¶
imagesmust befloat32with shape(num_frames, height, width).framesis a 1Dintarray. Frame numbers do not need to start at zero or be contiguous.timesis optional. When provided, VISTA displays timestamps in the playback controls and enables time-based track loading.sensoris required. Use the baseSensorif you have no calibration or geolocation data.You can pass multiple imagery objects as a list:
VistaApp(imagery=[img1, img2]).You can pass detections, tracks, and sensors alongside imagery — see
VistaAppdocumentation invista/app.py.
Loading Multiple Sensors or Imagery¶
from vista.sensors import Sensor, SampledSensor
sensor_a = Sensor(name="Visible Camera")
sensor_b = Sensor(name="IR Camera")
imagery_vis = Imagery(name="Visible", images=vis_images, frames=frames, sensor=sensor_a)
imagery_ir = Imagery(name="IR", images=ir_images, frames=frames, sensor=sensor_b)
app = VistaApp(imagery=[imagery_vis, imagery_ir])
app.exec()
Each sensor appears as a separate entry in the Sensors panel, and imagery is grouped under its associated sensor.
Approach 2: GUI Integration¶
For a more integrated experience you can add a menu action so users load your format through File → Load My Format. This involves three steps:
Write a loader method on
DataLoaderThread.Add a menu action in
VistaMainWindow.create_menu_bar().Wire the action to a handler that creates and starts the loader thread.
Step 1: Add a Loader to DataLoaderThread¶
Open vista/widgets/core/data/data_loader.py and add a new data_type branch in the
run() method, along with a private loading method.
# In DataLoaderThread.run():
def run(self):
try:
if self.data_type == 'imagery':
self._load_imagery()
elif self.data_type == 'my_format':
self._load_my_format()
# ... existing branches ...
except Exception as e:
self.error_occurred.emit(f"Error loading {self.data_type}: {str(e)}")
def _load_my_format(self):
"""Load imagery from custom format with incremental loading."""
# 1. Open your file and read metadata (fast)
images_on_disk = ... # e.g. a memory-mapped array or file handle
num_frames, height, width = images_on_disk.shape
frames = np.arange(num_frames)
# 2. Create a sensor
sensor = Sensor(name="My Sensor")
# 3. Pre-allocate images (zeros are safe if loading is cancelled)
images = np.zeros((num_frames, height, width), dtype=np.float32)
# 4. Create Imagery with loaded_frame_count = 0
imagery = Imagery(
name=Path(self.file_path).stem,
images=images,
frames=frames,
sensor=sensor,
)
imagery.loaded_frame_count = 0
# 5. Load incrementally (see below)
self._load_my_format_incrementally(imagery, images_on_disk, sensor)
Step 2: Implement Incremental Loading¶
VISTA’s viewer can display imagery while frames are still loading. The protocol uses three signals:
Signal |
When to emit |
|---|---|
|
After the first block of frames is loaded. The viewer will add the imagery and display whatever frames are available. |
|
After each subsequent block. The viewer updates its frame range and progress bar. |
|
When all frames are loaded or loading was cancelled. The viewer removes the progress bar and enables algorithms. |
def _load_my_format_incrementally(self, imagery, images_on_disk, sensor):
"""Load frames in blocks, emitting signals so the UI stays responsive."""
num_frames = images_on_disk.shape[0]
block_size = max(10, num_frames // 100)
# Load first block
first_end = min(block_size, num_frames)
if self._cancelled:
self.imagery_load_complete.emit(imagery.uuid)
return
imagery.images[0:first_end] = images_on_disk[0:first_end]
imagery.loaded_frame_count = first_end
self.imagery_available.emit(imagery, sensor, num_frames)
# Load remaining blocks
for start in range(first_end, num_frames, block_size):
if self._cancelled:
self.imagery_load_complete.emit(imagery.uuid)
return
end = min(start + block_size, num_frames)
imagery.images[start:end] = images_on_disk[start:end]
imagery.loaded_frame_count = end
self.imagery_block_loaded.emit(imagery.uuid, end)
# Done
self.imagery_load_complete.emit(imagery.uuid)
Important
Always emit imagery_load_complete even when cancelled. The main thread uses this signal to
clean up loading state and re-enable algorithm actions.
Incremental Loading Protocol¶
The diagram below shows the full lifecycle of an incremental imagery load and how the background thread communicates with the main (GUI) thread:
DataLoaderThread (background) VistaMainWindow (main thread)
───────────────────────────── ─────────────────────────────
Parse file metadata
Pre-allocate images array
Set loaded_frame_count = 0
Read first block into images[0:N]
Set loaded_frame_count = N
── imagery_available ──────────────────► Add imagery + sensor to viewer
Show progress bar in imagery panel
Display first N frames
Read next block into images[N:M]
Set loaded_frame_count = M
── imagery_block_loaded ───────────────► Update progress bar
Extend playback frame range
...repeat for each block...
── imagery_load_complete ──────────────► Remove progress bar
Set loaded_frame_count = None
Re-enable algorithm actions
Thread Safety¶
VISTA relies on a simple contract for thread safety during incremental loading:
The background thread writes into
imagery.imagesand updatesloaded_frame_count.The main thread only reads
images[0:loaded_frame_count].
As long as your loader follows this pattern (write frames sequentially, update
loaded_frame_count after the write), no locks are needed.
Non-Incremental Loading¶
If your format does not benefit from incremental loading (e.g., all data must be read at once), you can skip the block-by-block approach:
def _load_my_format(self):
"""Load all frames at once."""
images = read_all_frames(self.file_path) # your reader
sensor = Sensor(name="My Sensor")
frames = np.arange(images.shape[0])
imagery = Imagery(
name=Path(self.file_path).stem,
images=images,
frames=frames,
sensor=sensor,
)
# loaded_frame_count defaults to None = fully loaded
# Emit all three signals in sequence
self.imagery_available.emit(imagery, sensor, len(frames))
self.imagery_load_complete.emit(imagery.uuid)
Complete Example: FITS Loader¶
Below is a complete example that adds FITS file loading to VISTA. This assumes astropy is
installed.
data_loader.py additions¶
from astropy.io import fits
# In DataLoaderThread.run(), add:
# elif self.data_type == 'fits':
# self._load_fits()
def _load_fits(self):
"""Load imagery from a FITS file with incremental loading."""
with fits.open(self.file_path, memmap=True) as hdu_list:
data = hdu_list[0].data # memory-mapped (num_frames, height, width)
if data.ndim == 2:
# Single frame — add a frame axis
data = data[np.newaxis, :, :]
num_frames, height, width = data.shape
frames = np.arange(num_frames)
sensor = Sensor(name=Path(self.file_path).stem)
# Pre-allocate
images = np.zeros((num_frames, height, width), dtype=np.float32)
imagery = Imagery(
name=Path(self.file_path).stem,
images=images,
frames=frames,
sensor=sensor,
)
imagery.loaded_frame_count = 0
# Incremental loading
block_size = max(10, num_frames // 100)
first_end = min(block_size, num_frames)
if self._cancelled:
self.imagery_load_complete.emit(imagery.uuid)
return
images[0:first_end] = data[0:first_end].astype(np.float32)
imagery.loaded_frame_count = first_end
self.imagery_available.emit(imagery, sensor, num_frames)
for start in range(first_end, num_frames, block_size):
if self._cancelled:
self.imagery_load_complete.emit(imagery.uuid)
return
end = min(start + block_size, num_frames)
images[start:end] = data[start:end].astype(np.float32)
imagery.loaded_frame_count = end
self.imagery_block_loaded.emit(imagery.uuid, end)
self.imagery_load_complete.emit(imagery.uuid)
main_window.py additions¶
# In create_menu_bar(), File menu section:
load_fits_action = QAction("Load FITS Imagery", self)
load_fits_action.triggered.connect(self.load_fits_file)
file_menu.addAction(load_fits_action)
# New method on VistaMainWindow:
def load_fits_file(self):
"""Load imagery from a FITS file."""
file_path, _ = QFileDialog.getOpenFileName(
self, "Open FITS File",
self.settings.value("last_fits_dir", ""),
"FITS Files (*.fits *.fit *.fts);;All Files (*)",
)
if not file_path:
return
self.settings.setValue("last_fits_dir", str(Path(file_path).parent))
self.loader_thread = DataLoaderThread(file_path, 'fits')
self.loader_thread.imagery_available.connect(self.on_imagery_available)
self.loader_thread.imagery_block_loaded.connect(self.on_imagery_block_loaded)
self.loader_thread.imagery_load_complete.connect(self.on_imagery_load_complete)
self.loader_thread.error_occurred.connect(self.on_loading_error)
self.loader_thread.finished.connect(self.on_loading_finished)
self.loader_thread.start()
Design Guidelines¶
Images must be
float32with shape(num_frames, height, width). Convert during loading if your format uses a different dtype.Always create a Sensor. Even if you have no calibration data,
Imageryrequires aSensorreference. Use the baseSensor(name="...")as a minimal placeholder.Pre-allocate with zeros. Initialize
np.zeros(shape, dtype=np.float32)so that if loading is cancelled partway through, unloaded frames are zeros rather than uninitialized memory.Check
self._cancelledfrequently. Check at least once per block to ensure the user can cancel long-running loads.Always emit
imagery_load_complete. The main thread relies on this signal to clean up progress bars and re-enable algorithm actions. Emit it even on cancellation or error.Frame numbers are arbitrary. They do not need to start at zero, be contiguous, or be sorted. Use whatever frame numbering is natural for your format.
Timestamps are optional but recommended. When present they enable time-based features like the timestamp display in playback controls and time-based track/detection CSV loading.
See Also¶
Custom Sensors — Implementing custom sensors with geolocation
Working with Imagery — HDF5 format specification and imagery user guide
Imagery Module — Imagery API reference
vista/widgets/core/data/data_loader.py— Reference implementation for HDF5 loading