Skip to content

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_PARAMS tag): 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 MyCameraTrigger sub-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 (ResetRelativeTimer tag): 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 (SequenceStart and SequenceEnd tags): 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:

  1. Initialize the bridge and listener: Setup the Bridge, attach a custom ImageListener designed to measure inter-frame intervals, and initialize the system with the triggered_camera_and_microscope_and_fluo_source configuration.
  2. Start the camera in external mode: Load and execute the StartCamera sequence with the EXTERNAL_PARAMS tag to put the camera in external level-trigger mode.
  3. Create the Time Device: Setup a virtual Time Device mapping the StartTimerRecipeId and WaitTimerRecipeId recipes, and fetch the corresponding wait timer sub-device ID.
  4. Define the time-series sequence: Construct a list of Status objects, configuring the level trigger exposure, wait interval parameter, resetting tags, and boundary markers.
  5. Execute the acquisition sequence: Load the sequence into the Acquisition recipe index and run it synchronously.
  6. 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.")