Skip to content

quick calibration for 4x and 20x objectives on cytation1 w/ 1288x964 pixel Blackfly BFLY-U3-13S2M sensor #590

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pylabrobot/plate_reading/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from .biotek_backend import Cytation5Backend, Cytation5ImagingConfig
from .biotek_backend import Cytation5Backend, CytationImagingConfig
from .clario_star_backend import CLARIOStarBackend
from .image_reader import ImageReader
from .imager import Imager
Expand Down
39 changes: 28 additions & 11 deletions pylabrobot/plate_reading/biotek_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,12 @@ async def _golden_ratio_search(
return (b + a) / 2


CytationModel = Literal["cytation1", "cytation5"]


@dataclass
class Cytation5ImagingConfig:
class CytationImagingConfig:
model: CytationModel = "cytation5"
camera_serial_number: Optional[str] = None
max_image_read_attempts: int = 8

Expand All @@ -87,6 +91,19 @@ class Cytation5ImagingConfig:
filters: Optional[List[Optional[ImagingMode]]] = None


_FOV: dict[str, dict[float, tuple[float, float]]] = {
"cytation1": {
4: (1288 / 596, 964 / 596),
20: (1288 / 3000, 964 / 3000),
Comment on lines +96 to +97
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is going on with these denominators?

for the cytation5, it's to convert um to mm

Copy link
Contributor Author

@ben-ray ben-ray Jun 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cytation5 sensor is 2448x2048 while the cytation1 is 1288x964. The scaling factor from pixel to micron will be identical for both axis, but why does the image_size conversion not take the sensor size into account? I would expect the X and Y size to be different but the scaling factor is identical in X and Y in the original Cytation5 implementation.

These numbers are my attempt to account for the significantly different x and y size of my sensor (my pixels are still perfectly square)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is not a scaling factor

Copy link
Contributor Author

@ben-ray ben-ray Jun 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then how do I determine these and why are they not different for X and Y on the cytation5?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i took the number from gen5.exe, and then did um -> mm conversion which seems to work

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

at least it corresponds with their firmware commands.

you can use a firmware command to check

},
"cytation5": {
4: (3474 / 1000, 3474 / 1000),
20: (694 / 1000, 694 / 1000),
40: (347 / 1000, 347 / 1000),
},
}


class Cytation5Backend(ImageReaderBackend):
"""Backend for biotek cytation 5 image reader.

Expand All @@ -98,16 +115,15 @@ def __init__(
self,
timeout: float = 20,
device_id: Optional[str] = None,
imaging_config: Optional[Cytation5ImagingConfig] = None,
imaging_config: Optional[CytationImagingConfig] = None,
) -> None:
super().__init__()
self.timeout = timeout

self.io = FTDI(device_id=device_id)

self.spinnaker_system: Optional["PySpin.SystemPtr"] = None
self.cam: Optional["PySpin.CameraPtr"] = None
self.imaging_config = imaging_config or Cytation5ImagingConfig()
self.imaging_config = imaging_config or CytationImagingConfig()
self._filters: Optional[List[Optional[ImagingMode]]] = self.imaging_config.filters
self._objectives: Optional[List[Optional[Objective]]] = self.imaging_config.objectives
self._version: Optional[str] = None
Expand Down Expand Up @@ -1173,16 +1189,17 @@ def image_size(magnification: float) -> Tuple[float, float]:
# "wide fov" is an option in gen5.exe, but in reality it takes the same pictures. So we just
# simply take the wide fov option.
# um to mm (plr unit)
if magnification == 4:
return (3474 / 1000, 3474 / 1000)
if magnification == 20:
return (694 / 1000, 694 / 1000)
if magnification == 40:
return (347 / 1000, 347 / 1000)
raise ValueError(f"Don't know image size for magnification {magnification}")
try:
size = _FOV[self.imaging_config.model][magnification]
except KeyError:
raise ValueError(
f"Don't know image size for model {self.imaging_config.model} and magnification {magnification}"
)
return size

if self._objective is None:
raise RuntimeError("Objective not set. Run set_objective() first.")

magnification = self._objective.magnification
img_width, img_height = image_size(magnification)

Expand Down