spacr.plaque

Well detection and physical scale for the plaque assay.

A plaque assay is counted from images that arrive in two shapes, and they need different handling:

  • one plaque field per image – segment it and count, which is what spacr.submodules.analyze_plaques() has always done;

  • several wells in one image – a whole plate, or a strip. Segmenting that directly counts every plaque in every well into one number and loses which well each came from, which is the entire experiment.

This module supplies the front half for the second case: find the wells, then hand each one to the segmenter separately.

WHY THE WELL IS MEASURED AND NOT JUST CROPPED. Plaque area in pixels is a property of the microscope, not of the biology. The same plaque imaged at two magnifications gives two areas, and a study that pools them is comparing optics. A well, by contrast, is a manufactured object of known physical size – a 6-well plate well is 34.8 mm whatever images it. So the well’s diameter in pixels is a ruler that is present in the image itself, and dividing by it turns every area into a physical one that can be pooled across microscopes, objectives and days.

That is why detect_wells() returns a diameter rather than only a box, and why scale_from_well() is the piece the analysis actually consumes.

Classes

PlaqueScale

Pixels-to-millimetres for one well, and what it was derived from.

Well

One detected well.

Functions

crop_well(→ numpy.ndarray)

The image inside one well.

detect_wells(→ List[Well])

Find the wells in one image.

plaque_flow_outputs(→ Dict[str, Optional[numpy.ndarray]])

The flow picture and cell probability out of a Cellpose result.

scale_from_well(→ Optional[PlaqueScale])

Pixels-per-millimetre from a detected well, or None.

segment_plaque_image(→ Any)

Segment one plaque image the way both Plaque mode's preview and run do.

Module Contents

class spacr.plaque.PlaqueScale[source]

Pixels-to-millimetres for one well, and what it was derived from.

Parameters:
  • px_per_mm – pixels per millimetre.

  • well_diameter_px – the measured diameter the scale came from.

  • well_diameter_mm – the physical diameter it was compared against.

  • source – how well_diameter_mm was decided – a plate format name, or "explicit".

area_mm2(area_px: float) → float[source]

Convert a pixel area to mm^2.

Parameters:

area_px – an area in pixels.

Returns:

the same area in square millimetres.

class spacr.plaque.Well[source]

One detected well.

Parameters:
  • x0 – left edge in pixels.

  • y0 – top edge in pixels.

  • x1 – right edge in pixels.

  • y1 – bottom edge in pixels.

  • confidence – the detector’s score for this box.

as_dict() → Dict[str, Any][source]

The box plus its derived measures, for a results table.

Returns:

The stored coordinates and confidence plus derived diameter_px and axis_ratio values.

property axis_ratio: float[source]

Shorter box side over longer, so 1.0 is square.

THE HONESTY CHECK ON THE RULER. A well is circular; a box much wider than it is tall means the detector clipped it at an image edge, or merged two wells, or found something that is not a well. Any of those makes diameter_px wrong, and since that diameter rescales every area in the well, a wrong one is worse than a missing one.

property diameter_px: float[source]

The well’s diameter in pixels, as the mean of the box sides.

A well is round, so a correct box is square and the two sides agree. The MEAN rather than either side alone is what makes a slightly loose box degrade gently instead of biasing one way – and the disagreement itself is reported by axis_ratio, so a box that is not square is visible rather than silently averaged into a plausible number.

property height: int[source]

Box height in pixels.

property width: int[source]

Box width in pixels.

spacr.plaque.crop_well(image: numpy.ndarray, well: Well, *, pad: int = 0) → numpy.ndarray[source]

The image inside one well.

Parameters:
  • image – the full field.

  • well – the box to cut out.

  • pad – extra pixels around the box, clipped to the image.

Returns:

the clipped image region selected by the padded box.

spacr.plaque.detect_wells(image: numpy.ndarray, weights: str, *, confidence: float = DEFAULT_CONFIDENCE, imgsz: int = 640, min_axis_ratio: float = 0.7) → List[Well][source]

Find the wells in one image.

Parameters:
  • image – the field, as an H x W x 3 array in RGB channel order – what cellpose.io.imread() returns for a colour image. It is converted to BGR here, because that is the order ultralytics reads an array in and therefore the order these detectors were trained in. A greyscale or otherwise non-three-channel array is passed through untouched, and so is a file path, which ultralytics decodes itself.

  • weights – path to the YOLO checkpoint.

  • confidence – drop detections scoring below this.

  • imgsz – inference size; 640 is what the shipped detector trained at.

  • min_axis_ratio – reject boxes less square than this. See Well.axis_ratio – a non-square box makes the diameter, and therefore every area in that well, wrong.

Returns:

the wells, ordered top-to-bottom then left-to-right, which is reading order and therefore the order a plate map is written in.

Raises:

ImportError – when ultralytics is not installed.

spacr.plaque.plaque_flow_outputs(output: Any) → Dict[str, numpy.ndarray | None][source]

The flow picture and cell probability out of a Cellpose result.

Cellpose’s eval returns (masks, flows, styles), and flows is a list whose first entry is the flow field already drawn as an RGB image (direction as hue, strength as brightness, the picture the Cellpose GUI shows) and whose third is the cell-probability map, in logits. Either can be a torch tensor on the device the model ran on, so both go through _host_array(). A result that has no flows – a stub, or a model that returned only masks – gives None for both.

Parameters:

output – what model.eval returned.

Returns:

{'flow_rgb': H x W x 3 uint8 or None, 'cellprob': H x W float32 or None}.

spacr.plaque.scale_from_well(well: Well, *, plate_format: str | None = None, well_diameter_mm: float | None = None) → PlaqueScale | None[source]

Pixels-per-millimetre from a detected well, or None.

Parameters:
  • well – the detected well to measure.

  • plate_format – a key of WELL_DIAMETERS_MM.

  • well_diameter_mm – the physical diameter, overriding plate_format.

Returns:

the scale, or None when neither argument says how big the well physically is.

Raises:

KeyError – if plate_format is not a known format.

RETURNS None RATHER THAN ASSUMING. Without a physical diameter there is no scale, and inventing one – a default plate format, say – would convert every area into confident millimetres that are wrong by whatever the real plate was. The caller keeps pixels and says so.

spacr.plaque.segment_plaque_image(model: Any, image: numpy.ndarray, settings: Dict[str, Any], *, return_flows: bool = False) → Any[source]

Segment one plaque image the way both Plaque mode’s preview and run do.

The image goes to Cellpose as it is, RGB or grey, and Cellpose normalises it. It is NOT sent through the run’s historical loader (_load_normalized_images_and_labels with background=200): on an 8-bit crop that loader saturated every pixel to 1.0 – measured on malnio__2.tif, min = max = 1.0 in every channel – so the run found no plaques while the preview, reading the image as it is, found 67. One function for both is what keeps them from disagreeing again.

Parameters:
  • model – a Cellpose model.

  • image – H x W or H x W x 3.

  • settings – diameter, flow_threshold and CP_prob.

  • return_flows – also hand back what the live preview’s Flows and Cell probability tabs show, from the same call.

Returns:

the label image; with return_flows, (labels, flows) where flows is plaque_flow_outputs().