Triggered Time-Series Acquisition¶
This example demonstrates how to perform a hardware-timed, externally-triggered time-series acquisition programmatically using the Inscoper API.
Unlike software-triggered acquisitions where the camera is explicitly commanded by software to capture each individual frame, a triggered time-series acquisition offloads sequence timing and camera synchronization entirely to the hardware level. The bridge utilizes a virtual Time Device and a hardware-synchronized trigger line to capture frames at highly precise, deterministic intervals (e.g., 1-second intervals), using a level-trigger pulse to control the camera exposure time.
Prerequisites¶
This example assumes you have an external trigger-capable camera and matching configuration templates loaded. The controller and recipe set must define timer operations.
To run this example successfully, make sure your physical setup has the camera trigger line correctly connected to the Inscoper Controller trigger output ports.
Hardware Timing Concepts¶
To achieve microsecond-precise, hardware-driven timing, the sequence status objects utilize specific tags and parameters:
- External Triggering (
EXTERNAL_PARAMStag): Starts and runs the camera in external level-trigger mode. In this mode, the camera's sensor exposure is driven directly by the hardware pulse width. - Level Trigger Parameter: Configured on the
MyCameraTriggersub-device (e.g.,"LevelTrigger"parameter). The parameter value specifies the duration of the hardware trigger pulse in microseconds (e.g.,20000µs for a 20 ms exposure). - Virtual Time Device: Created using
createTimeDevice(index, startRecipeId, waitRecipeId). It maps virtual timer registers inside the controller to drive hardware-timed wait intervals. - Wait Timer Parameter: Dictates the duration to pause before executing the next frame's recipe cycle (e.g.,
1000000µs for a 1.0s interval). - Reset Relative Timer (
ResetRelativeTimertag): Resets the relative time registers on the hardware controller at the beginning of each frame status, ensuring that wait intervals are measured relative to the start of each frame. - Sequence Boundaries (
SequenceStartandSequenceEndtags): Marks the start and end frames of the sequence. These tags allow the bridge to manage hardware timer activation and termination automatically.
Overview¶
The triggered time-series acquisition relies on the following key components:
| Component / Parameter | Role |
|---|---|
MyCamera |
The target camera device to receive trigger signals and capture frames. |
MyCameraTrigger |
Device responsible for generating level-trigger hardware pulses. |
EXTERNAL_PARAMS |
Status tag that prepares the camera to expect external hardware trigger pulses. |
| Virtual Time Device | Virtual device managing interval delays between successive frames. |
SequenceStart / SequenceEnd |
Marks the temporal boundaries of the time-series sequence for timer control. |
ResetRelativeTimer |
Instructs the hardware timer to reset relative to the start of each frame's execution cycle. |
The workflow executes the following steps in order:
- Initialize the bridge and listener: Setup the
Bridge, attach a customImageListenerdesigned to measure inter-frame intervals, and initialize the system with thetriggered_camera_and_microscope_and_fluo_sourceconfiguration. - Start the camera in external mode: Load and execute the
StartCamerasequence with theEXTERNAL_PARAMStag to put the camera in external level-trigger mode. - Create the Time Device: Setup a virtual Time Device mapping the
StartTimerRecipeIdandWaitTimerRecipeIdrecipes, and fetch the corresponding wait timer sub-device ID. - Define the time-series sequence: Construct a list of
Statusobjects, configuring the level trigger exposure, wait interval parameter, resetting tags, and boundary markers. - Execute the acquisition sequence: Load the sequence into the
Acquisitionrecipe index and run it synchronously. - Retrieve and plot images: Fetch the received frames from the custom listener and plot them in a grid layout to verify successful acquisition and timing.
Note
This example uses a configuration named triggered_camera_and_microscope_and_fluo_source, a camera named MyCamera, a camera trigger named MyCameraTrigger, and a virtual time device index 0. The trigger recipes StartTimerRecipeId and WaitTimerRecipeId are defined in the recipe.xml file. Replace these names with the ones defined in your hardware configuration and recipe setup.
Triggered Acquisition¶
class CustomImageListener(inscoper_api.ImageListener):
"""
A custom image listener that receives images from the bridge as they are acquired.
It tracks and prints the elapsed time between consecutive frames (Delta t)
and the total elapsed time since the acquisition started, which is crucial
for verifying the hardware timing accuracy in triggered workflows.
"""
def __init__(self):
super().__init__()
self._received_images = []
self._image_counter = 0
self._start_time = None
self._last_image_time = None
def handleNewImage(self, image):
current_time = time.time()
if self._start_time is None:
self._start_time = current_time
self._last_image_time = current_time
delta_t = 0.0
total_time = 0.0
else:
delta_t = current_time - self._last_image_time
total_time = current_time - self._start_time
self._last_image_time = current_time
print(f"Image {self._image_counter} received! (Delta t: {delta_t:.3f}s, Total time: {total_time:.3f}s)")
self._image_counter += 1
self._received_images.append(image)
def getReceivedImages(self):
return self._received_images
# ── Step 1: Initialize the API bridge and load hardware configuration
# Macro to all Inscoper configurations
CONFIG_PATH = os.environ['InscoperConfigsPath']
my_bridge = inscoper_api.Bridge()
my_listener = CustomImageListener()
my_bridge.addImageListener(my_listener)
# Read and load the specific hardware configuration designed for triggered acquisitions
config_name = "triggered_camera_and_microscope_and_fluo_source"
my_bridge.readConfigFile(os.sep.join([CONFIG_PATH, config_name]))
my_bridge.loadConfigFile(os.sep.join([CONFIG_PATH, config_name]))
# Initialize all loaded physical and virtual devices
my_bridge.initDevices()
# ── Step 2: Start the camera in external trigger mode
# To perform triggered acquisitions, the camera must be started with the 'EXTERNAL_PARAMS' tag.
# In this mode, the camera awaits level-trigger pulses to control its exposure time.
start_cam_status = inscoper_api.Status()
start_cam_status.addTag("MyCamera")
start_cam_status.addTag("EXTERNAL_PARAMS")
my_bridge.loadSequence(0, "StartCamera", [start_cam_status])
my_bridge.runSequence(0, False)
print("Camera started in external trigger mode.")
# ── Step 3: Create and configure a virtual Time Device
# Time series acquisitions require precise, hardware-timed intervals between frames.
# This is achieved by creating a virtual Time Device. The recipe.xml must define matching
# 'StartTimerRecipeId' and 'WaitTimerRecipeId' timer recipes to drive the hardware timer.
time_index = 0
my_bridge.createTimeDevice(time_index, "StartTimerRecipeId", "WaitTimerRecipeId")
# Retrieve the sub-device handle associated with the wait timer for the configured time index.
wait_timer_sub_device_id = my_bridge.getWaitTimerSubDeviceId(time_index)
# ── Step 4: Define the time-series sequence via frame Status objects
# We construct a list of Status states, where each Status defines the parameters
# and tags required for a single frame in the time-series acquisition.
time_series_status = []
number_of_frames = 10
for i in range(number_of_frames):
frame_status = inscoper_api.Status()
# Tag sequence boundaries to manage timer activation correctly
if i == 0:
frame_status.addTag("SequenceStart")
elif i == number_of_frames - 1:
frame_status.addTag("SequenceEnd")
# Set exposure time (LevelTrigger parameter) on the camera trigger sub-device.
# Unit: microseconds. Here, '20000' µs corresponds to a 20 ms exposure.
frame_status.addParam(inscoper_api.SubDeviceId("MyCameraTrigger", "LevelTrigger"), "20000")
# Set the wait interval between frames on the virtual wait timer sub-device.
# Unit: microseconds. Here, '1000000' µs corresponds to a 1.0 second interval.
frame_status.addParam(wait_timer_sub_device_id, "1000000")
# Tag to reset the hardware timer at the beginning of this frame's recipe cycle
frame_status.addTag("ResetRelativeTimer")
# Tag the specific camera device and indicate that it uses external level triggering
frame_status.addTag("MyCamera")
frame_status.addTag("EXTERNAL_PARAMS")
time_series_status.append(frame_status)
# ── Step 5: Load and run the acquisition sequence
# Load the time-series statuses into the "Acquisition" recipe index.
# The bridge handles the synchronous execution of this sequence, ensuring precise timing.
my_bridge.loadSequence(1, "Acquisition", time_series_status)
print("Executing acquisition sequence...")
t0 = time.time()
my_bridge.runSequence(1, False)
t1 = time.time()
print("Acquisition sequence executed successfully.")
print(f"Total hardware execution time: {t1 - t0:.3f} seconds.")
# ── Step 6: Retrieve and display the acquired images
received_images = my_listener.getReceivedImages()
if received_images:
N = len(received_images)
cols = int(np.ceil(np.sqrt(N)))
rows = int(np.ceil(N / cols))
fig, axes = plt.subplots(rows, cols, figsize=(cols * 3, rows * 3))
if N == 1:
axes = np.array([axes])
else:
axes = axes.flatten()
for idx, image in enumerate(received_images):
image_data = image.getImageData()
image_width = image.getWidth()
image_height = image.getHeight()
pixel_size_bytes = image.getPixelSize()
bit_depth = image.getBitDepth()
bytes_per_value = bit_depth // 8
values_per_pixel = pixel_size_bytes // bytes_per_value
img = np.reshape(image_data, (image_height, image_width, values_per_pixel))
ax = axes[idx]
ax.imshow(img[:, :, 0], cmap="gray")
ax.set_title(f"Frame {idx}")
ax.axis("off")
# Hide any unused subplots in the plotting grid
for idx in range(N, len(axes)):
axes[idx].axis("off")
plt.tight_layout()
plt.show()
else:
print("No images received to plot.")