Advanced imaging and coordinates#
This notebook goes beyond the Celigo hello world: it inspects the installed imaging configuration, plans calibrated coordinates and galvo fields of view, works directly with CameraFrame, tunes exposure and autofocus, and inspects structured acquisition results.
The executable cells move the stage, Z axis, filter wheel, and galvos and switch illumination. Clear the motion envelope, seat the plate correctly, and keep the final cleanup cell available before continuing.
Configure the instrument#
CeligoConfig.from_install(install_dir) loads the complete, per-instrument configuration from an explicit installation root, ConfigFiles directory, or hardware-config path. Assign the plate after constructing Celigo; the plate is not a constructor setting.
from pathlib import Path
from pylabrobot.revvity import Capture, Celigo, CeligoConfig, ScanSpec
from pylabrobot.revvity.celigo import CoordinateSystems
from pylabrobot.revvity.celigo.navigation import galvo_field_of_view_offsets_mm
from pylabrobot.resources.corning.plates import cor_96_wellplate_360uL_Fb
config_root = Path("/path/to/Celigo/ConfigFiles")
lucam_sdk = Path("/path/to/liblucamapi.so")
usb_address = "3-2"
config = CeligoConfig.from_install(str(config_root))
plate = cor_96_wellplate_360uL_Fb(name="imaging_plate")
celigo = Celigo(
config=config,
usb_address=usb_address,
lucam_sdk=str(lucam_sdk),
)
celigo.set_plate(plate)
Connect and establish position references#
setup() initializes the controller and camera, configures the motors and galvos, then homes Z, X, Y, and the dichroic filter in that clearance-safe order.
await celigo.setup()
Inspect the installed channel recipes#
Channel configuration is magnification-specific. Each recipe supplies the logical filter, lighting output, default intensity, Z correction, and pixel-scale correction used by acquire() and scan planning.
channel_summary = {
name: {
"logical_filter": channel.logical_filter,
"lighting_output": channel.lighting_io_name,
"intensity_percent": channel.intensity_percent,
"z_offset_mm": channel.z_offset_to_brightfield_mm,
"pixel_scale": (
channel.mm_per_pixel_x_correction_to_brightfield,
channel.mm_per_pixel_y_correction_to_brightfield,
),
}
for name, channel in celigo.config.channels.items()
}
celigo.config.magnification, channel_summary
Plan calibrated coordinates without moving#
well_position_mm() converts a standard PyLabRobot well into calibrated stage millimeters. CoordinateSystems also converts between plate-relative sample millimeters, stage millimeters, and pixels. For image conversions, reference_point_mm is the field center in sample coordinates; the calibrated center pixel maps to that sample point and its corresponding stage position.
plate_coordinates = CoordinateSystems.from_config(
celigo.config.calibration,
celigo.config.hardware_defaults,
)
a1_stage_mm = celigo.well_position_mm("A1")
a1_sample_mm = plate_coordinates.stage_mm_to_sample_mm(*a1_stage_mm)
field_coordinates = CoordinateSystems.from_config(
celigo.config.calibration,
celigo.config.hardware_defaults,
reference_point_mm=a1_sample_mm,
)
center_pixel = (
celigo.config.calibration.image_width_pixels / 2,
celigo.config.calibration.image_height_pixels / 2,
)
center_sample_mm = field_coordinates.image_pixel_to_sample_mm(*center_pixel)
center_stage_mm = field_coordinates.image_pixel_to_stage_mm(*center_pixel)
{
"A1 stage mm": a1_stage_mm,
"A1 sample mm": a1_sample_mm,
"center pixel": center_pixel,
"center pixel sample mm": center_sample_mm,
"center pixel stage mm": center_stage_mm,
}
Preview the galvo FOV plan#
The navigation calibration defines a centered serpentine grid of sample-space offsets. Galvo.voltages_for_offset() combines one offset with the active magnification center, the logical-filter correction, and the calibrated inverse polynomial. These calculations do not move hardware.
brightfield_filter = celigo.config.channels["brightfield"].logical_filter
fov_offsets_mm = galvo_field_of_view_offsets_mm(
celigo.config.calibration,
celigo.config.navigation,
)
fov_plan = [
{
"offset_mm": offset_mm,
"logical_voltages": celigo.galvo.voltages_for_offset(
brightfield_filter,
offset_mm,
),
}
for offset_mm in fov_offsets_mm
]
fov_plan
Capture and analyze a CameraFrame#
capture_frame() captures at the current stage, Z, filter, galvo, and illumination state. The following cells establish that state explicitly. CameraFrame stores dependency-free monochrome bytes and exposes statistics, sharpness, PGM export, and optional NumPy conversion.
await celigo.move_to_well("A1", retract_z=True)
await celigo.select_channel("brightfield")
await celigo.z_axis.move_to(celigo.config.calibration.calibrated_z_position)
await celigo.galvo.home(logical_filter=brightfield_filter)
await celigo.set_camera_exposure_and_gain(
exposure_ms=1.0,
gain=1.0,
restart_camera_stream=True,
)
await celigo.set_illumination_enabled(True)
try:
frame = await celigo.capture_frame(flush_frames=2)
finally:
await celigo.turn_off_illumination()
frame.save_pgm("A1-brightfield-direct.pgm")
{
"shape": (frame.height, frame.width),
"bit_depth": frame.bit_depth,
"exposure_ms": frame.exposure_ms,
"gain": frame.gain,
"statistics": frame.statistics(),
"sharpness": frame.sharpness(sample_step=8),
}
NumPy is optional. When installed, to_numpy() returns a two-dimensional uint8 or uint16 view suitable for scientific image tooling.
try:
image = frame.to_numpy()
except ImportError as error:
print(error)
else:
print(image.shape, image.dtype)
Tune exposure directly#
auto_exposure() tests only the supplied positive candidates, from left to right. It chooses the first frame that is bright enough while keeping the saturated-pixel fraction below the requested limit. It does not move or select a channel, so establish the field first as above.
selected_exposure_ms, exposure_frame = await celigo.auto_exposure(
candidates_ms=(10.0, 5.0, 2.0, 1.0, 0.5),
saturation_fraction=0.01,
minimum_mean_fraction=0.03,
)
selected_exposure_ms, exposure_frame.statistics()
Inspect a direct autofocus result#
The high-level acquisition API accepts autofocus="image". Calling autofocus() directly additionally exposes the sampled Z ticks and scores. Its span and step arguments are controller-native encoder ticks; use the Z-axis conversion helpers when starting from millimeters. The scan restores the initial Z position on failure and rejects flat focus curves and boundary optima.
center_z_ticks = await celigo.z_axis.request_encoder_ticks()
focus = await celigo.autofocus(
center_z_ticks=center_z_ticks,
span_ticks=1500,
coarse_step_ticks=250,
fine_step_ticks=75,
)
focus.frame.save_pgm("A1-brightfield-focused-direct.pgm")
{
"z_ticks": focus.z_ticks,
"z_mm": focus.z_mm,
"verified_score": focus.score,
"samples": focus.scored_z_samples,
}
Inspect structured acquisition metadata#
AcquisitionResult records the requested well and channel, settled X/Y/Z millimeters, the final frame, optional FocusResult, and the hardware galvo voltages. Acquisition extinguishes illumination if any step fails or is cancelled.
result = await celigo.acquire(
"A1",
"brightfield",
exposure_ms=selected_exposure_ms,
gain=1.0,
autofocus="image",
galvo_offset_mm=fov_offsets_mm[0],
)
{
"label": result.label,
"channel": result.channel,
"stage_mm": (result.x_mm, result.y_mm),
"z_mm": result.z_mm,
"galvo_hardware_voltages": result.galvo_hardware_voltages,
"focus_score": None if result.focus is None else result.focus.score,
"frame_statistics": result.frame.statistics(),
}
Build and execute a multichannel scan#
ScanSpec.wells() converts well names to physical centers and stores every capture setting. plan() is offline; execute() accepts no scientific overrides and runs the inspected operations exactly. The coarse stage moves once per block.
scan_spec = ScanSpec.wells(
plate,
["A1", "A2"],
block_shape=(2, 3),
captures=[
Capture(channel="brightfield", exposure_ms=selected_exposure_ms, gain=1.0),
Capture(channel="green", exposure_ms=10.0, gain=1.0),
],
autofocus="image",
)
scan_plan = celigo.plan(scan_spec)
print(scan_plan)
scan_result = await celigo.execute(scan_plan)
[
(
item.planned.block.label,
item.planned.capture.channel,
item.actual_stage_mm,
item.actual_z_mm,
)
for item in scan_result.frames
]
Stop safely#
Run cleanup even after an exception. turn_off_illumination() attempts every configured lighting output even if one output fails; stop() also aborts controller work, clears safe outputs, closes the camera, and releases FTDI.
await celigo.turn_off_illumination()
await celigo.stop()