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:
- Resolve the focus sub-device: Identify the focus controller
SubDeviceIdhandle from the device map. - Start the camera: Prepare the camera to stream images using the
SOFTWARE_TRIGGERmode. - Establish the reference focus: Load the desired channel and prompt the user to manually adjust the microscope focus on the sample.
- Define Z-stack slices: Generate a list of
Statusobjects 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. - Execute acquisition sequence: Load and run the Z-stack sequence to capture all 11 slices sequentially.
- 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.")