Skip to content

Z-Stack Acquisition

This example demonstrates how to perform a Z-stack acquisition programmatically using the Inscoper API.

In the Inscoper API, a Z-stack is defined declaratively as a sequence of Status objects. Each Status represents one image/slice of the stack, mapping the active channel settings to a unique Z coordinate. When the sequence is run, the bridge automatically drives the focus axis to each coordinate and triggers the camera to acquire the corresponding image.

Prerequisites

This example builds on the device exploration pattern introduced in Explore Devices and the channel definition concepts from Define Channels. The device map is used to locate the focus controller and active optical components.

Overview

The Z-stack acquisition sequence relies on the following sub-device types:

Sub-device type Role
FOCUS Controls the focus axis (Z position) of the microscope

The workflow executes the following steps in order:

  1. Resolve the focus sub-device: Identify the focus controller SubDeviceId handle from the device map.
  2. Start the camera: Prepare the camera to stream images using the SOFTWARE_TRIGGER mode.
  3. Establish the reference focus: Load the desired channel and prompt the user to manually adjust the microscope focus on the sample.
  4. Define Z-stack slices: Generate a list of Status objects centered around the reference focus position, spanning a volume of 100 microns with a 10-micron step size. Each status carries the required channel states, focus coordinate, and the trigger tags.
  5. Execute acquisition sequence: Load and run the Z-stack sequence to capture all 11 slices sequentially.
  6. Retrieve and display images: Gather all captured images from the listener and display them side-by-side in a grid layout.

Note

This example uses a configuration named camera_and_microscope_and_fluo_source, a camera named MyCamera, and a blue LED source named MyFluoSource. Replace these values with the names defined in your own hardware configuration file.

Z-Stack Acquisition

# ── Step 1: Resolve the sub-device IDs for the focus
focus_sub_device_id = None              # Focus controller
for device_id, device in device_map.items():
    for sub_device_tag, sub_device in device.sub_device_map.items():
        if sub_device.inscoper_type == "FOCUS":
            # Focus controller: uniquely identified by the "FOCUS" type
            focus_sub_device_id = inscoper_api.SubDeviceId(device.name, sub_device.name)


# ── Step 2: Start the camera in the correct mode
start_cam_status = inscoper_api.Status()
start_cam_status.addTag("MyCamera")
start_cam_status.addTag("SOFTWARE_TRIGGER")

my_bridge.loadSequence(0, "StartCamera", [start_cam_status])
my_bridge.runSequence(0, False)

# ── Step 3: Set the blue channel
my_bridge.loadSequence(1, "DeviceUpdate", [blue_status])
my_bridge.runSequence(1, False)


# ── Step 4: Wait for the user to adjust the focus on the microscope
input("Please manually adjust the focus on the microscope. Once done, press Enter to continue...")


# ── Step 5: Get current Z position and define the Zstack 
current_z = my_bridge.getLongValue(focus_sub_device_id)
print(f"Current Z position: {current_z} nm")

zstack_status = []
step = 10000  # 10 micron in nanometers (Inscoper Z focus unit)
volume = 100000  # 100 microns in nanometers
half_volume = volume // 2

for z in range(current_z - half_volume, current_z + half_volume + 1, step):
    slice_status = inscoper_api.Status()
    for sub_device_id, value in blue_channel_description.items():
        slice_status.addParam(sub_device_id, value)
    slice_status.addParam(focus_sub_device_id, z)
    slice_status.addTag("MyCamera")
    slice_status.addTag("SOFTWARE_TRIGGER")

    zstack_status.append(slice_status)

# ── Step 6: Load and run the acquisition
my_bridge.loadSequence(2, "Acquisition", zstack_status)
my_bridge.runSequence(2, False)

# ── Step 7: Retrieve and display the 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"Slice {idx}")
        ax.axis("off")

    # Hide any unused subplots in the grid
    for idx in range(N, len(axes)):
        axes[idx].axis("off")

    plt.tight_layout()
    plt.show()
else:
    print("No images received to plot.")