spacr.ops_register

Register two overlapping tiles by phase correlation, on whatever hardware.

WHY PHASE CORRELATION AND NOT FEATURE MATCHING. Both were run on the same pixels – well A1 cycle 1 of screenA/20200202_6W-LaC024A, 333 tiles of 10X SBS DAPI:

ORB + RANSAC 993 pairs scored, median score 0.0002, max 0.0985

26 of 333 tiles placed, 307 dropped

phase correlation the geometry instantly, peak/mean 23.6 to 53.1

against control pairs at 9.0 to 15.6

Nuclei at 10X are round blobs. They have no corners, so a corner detector finds essentially nothing and the matcher’s scores are noise. Phase correlation asks a different question – what single translation makes these two fields agree – and a field of blobs answers it very well. Feature matching still earns its keep in ONE place, and it is not this one: the phenotype-to-SBS step across magnifications, where scale and rotation are real.

THREE THINGS THAT MAKE THE ANSWER RIGHT, all of them learned the hard way and all of them measured:

  • THE SHIFT COMES BACK WRAPPED. An FFT knows nothing about which way is negative: a -213 px shift of a 1480 px tile arrives as +1267. Reading it unwrapped made the first prototype’s shifts nonsense while every individual registration was correct. unwrap() folds anything past half a tile into the negative half.

  • (0,0) IS THE NO-OVERLAP SIGNATURE, not a perfect alignment. Two tiles that do not touch have no translation that makes them agree, and the peak lands at the origin. It is REJECTED rather than believed, which is what the three control pairs in 372’s PART 11-B confirmed.

  • A PEAK IS ONLY A PEAK AGAINST ITS BACKGROUND. The absolute height means nothing; peak/mean does. True neighbours measured 23.6 to 53.1 and non-neighbours 9.0 to 15.6, so min_peak_ratio has real room between them.

THE BACKEND IS AN INDIRECTION OVER THE ARRAY MODULE, which is all the GPU question is here: this is an FFT, a multiply and an argmax. CuPy first where it exists, torch.fft second – torch is already a spaCR dependency through Cellpose, so it costs nothing to install and reaches CUDA, ROCm and Apple MPS through one path – and NumPy always.

THE CPU PATH IS NOT AN ERROR PATH. Both run the same fixture and must agree to within a pixel, which is what tests/test_the_ops_registration_agrees_on_every_backend.py asserts. A fallback nobody compares is a second implementation, not a fallback.

AND IT NEVER ASSUMES THE CARD IS FREE. A shared GPU is the common case – this one also runs structure prediction. spacr.accelerator is the seam that decides whether there is a usable device at all, gpu=False refuses one that exists, and a backend that raises at runtime falls through to the next rather than failing the well.

Classes

Registration

What one pair of tiles measured.

Functions

available_backends(→ Tuple[str, ...])

Which backends this machine can actually run, best first.

phase_correlate([tolerance])

Measure the translation between two overlapping fields.

register_edge(→ Registration)

Register two tiles that touch along one edge, using that edge only.

register_pairs(→ dict)

Register every adjacency a layout offers.

unwrap(→ int)

Fold a wrapped FFT shift to the representative nearest about.

Module Contents

class spacr.ops_register.Registration[source]

What one pair of tiles measured.

Variables:
  • dy – rows to move the second tile to land it on the first, unwrapped into [-height/2, height/2).

  • dx – the same across columns.

  • peak_ratio – the correlation peak against the mean of the surface. The evidence, not the shift.

  • accepted – whether this is a real overlap: a peak above the threshold that is not at the origin.

  • backend – which array module answered, so a run can say what it ran on rather than leaving it to be inferred.

property shift: Tuple[int, int][source]

(dy, dx), for a caller that wants the pair.

spacr.ops_register.available_backends(gpu: bool = True) → Tuple[str, ...][source]

Which backends this machine can actually run, best first.

Parameters:

gpu – False refuses the accelerated ones outright, which is what the gpu setting means – a card that exists and is busy with somebody else’s job is not a card this run may take.

Returns:

the usable names, always ending in "numpy".

spacr.ops_register.phase_correlate(first: numpy.ndarray, second: numpy.ndarray, *, expected: Tuple[int, int] = (0, 0), tolerance=None, backend: str | None = None, gpu: bool = True, min_peak_ratio: float = MIN_PEAK_RATIO) → Registration[source]

Measure the translation between two overlapping fields.

The result is the shift that lands second on first: rolling second by (dy, dx) brings it into first’s frame.

Takes and returns NumPy whatever ran, so no caller learns which backend answered – the name is on the result for the record, not for branching.

Parameters:
  • first – the reference field, 2-D.

  • second – the field to place against it, the same shape.

  • expected – roughly where the neighbour is, for the unwrap. The default of (0, 0) folds into the half-range, which is right for two whole tiles and WRONG for two overlap strips – see unwrap().

  • tolerance –

    how far from expected a shift may land and still be accepted, in pixels. WHEN IT IS GIVEN, IT IS THE TEST, and the peak ratio becomes a recorded number rather than the gate – see the note on MIN_PEAK_RATIO.

    A PAIR OF NUMBERS IS ACCEPTED AND IS USUALLY WHAT IS WANTED: (rows, columns). The two axes are not the same question – a true edge is within a pixel or two ALONG the raster and up to the stage’s skew ACROSS it – and one number for both either rejects the skew or admits a neighbour a whole tile away. See SKEW_PX.

  • backend – force one; None takes the best available.

  • gpu – False keeps it on the CPU even where a card exists.

  • min_peak_ratio – below this the pair is not a neighbour.

Returns:

the registration, accepted or not.

Raises:

ValueError – when the two tiles are not the same 2-D shape, which is a caller error rather than a failed registration and must not come back as a confident (0, 0).

spacr.ops_register.register_edge(first: numpy.ndarray, second: numpy.ndarray, axis: str, *, overlap_fraction: float = STRIP_FRACTION, expected_overlap: int | None = None, tolerance: int | None = None, skew: int | None = SKEW_PX, **kwargs) → Registration[source]

Register two tiles that touch along one edge, using that edge only.

CORRELATE THE BAND THAT CAN AGREE, NOT THE TILE. The raster overlaps its fields by about 14 % – 213 px of 1480 – so 86 % of a whole-tile correlation is signal that CANNOT match and can only add background to the surface the peak is judged against. Measured on a blob fixture at exactly that overlap:

whole tile      peak/mean 15.2, shift WRONG, rejected
30 % strip      peak/mean 29.2, shift exact, accepted
                control pair 14.0, correctly rejected

which is also the separation the real acquisition showed (23.6-53.1 for true neighbours, 9.0-15.6 for controls). The strip has to be WIDER than the overlap and not much wider: 40 % diluted it back to 15.4 and lost the shift again.

Parameters:
  • first – the tile the edge is measured against.

  • second – its neighbour, below it for “vertical” and to its right for “horizontal” – the order WellLayout.pairs() returns them in.

  • axis – “vertical” or “horizontal”.

  • overlap_fraction – how much of the tile the strip takes.

  • expected_overlap – the overlap in pixels, when the pitch is known. It sets the unwrap’s expectation, which is what stops a true shift larger than half the strip reading as a negative one.

  • tolerance – how far ALONG the edge the measured shift may land from the expected overlap and still be accepted. This is the acceptance the real well used; without it the peak ratio decides, and the peak ratio cannot decide – see MIN_PEAK_RATIO.

  • skew – how far ACROSS it may land. The stage’s skew is a real ~9 px on the measured plate and is not an error, so this is a separate number: one tolerance for both axes rejected 525 of 624 true adjacencies at 8 px. See SKEW_PX.

  • kwargs – passed to phase_correlate().

Returns:

the registration, in TILE coordinates rather than strip ones – the caller asked about two tiles.

Raises:

ValueError – on an unknown axis.

spacr.ops_register.register_pairs(tiles, pairs: Sequence[Tuple[int, int, str]], *, expected_overlap: int | None = None, tolerance: int | None = None, skew: int | None = SKEW_PX, gpu: bool = True, min_peak_ratio: float = MIN_PEAK_RATIO, **kwargs) → dict[source]

Register every adjacency a layout offers.

Parameters:
  • tiles – site -> 2-D array, or any callable taking a site. A CALLABLE IS THE POINT for a real well: 333 tiles of 1480 px is more than needs to be resident, and the caller decides what to keep.

  • pairs – what to register, from spacr.ops_layout.WellLayout.pairs().

  • expected_overlap – the raster’s overlap in pixels, when known.

  • tolerance – how far along the edge a shift may land.

  • skew – how far across it may – the stage’s own, measured.

  • gpu – passed through.

  • min_peak_ratio – passed through.

  • kwargs – passed to register_edge().

Returns:

{(a, b): Registration}, including the rejected ones – a pair that failed is a fact about the acquisition and belongs in ops_geometry, not in a debug log.

spacr.ops_register.unwrap(value: int, extent: int, about: int = 0) → int[source]

Fold a wrapped FFT shift to the representative nearest about.

A translation of -213 in a 1480 px tile comes back as +1267, because the transform is periodic and has no notion of direction. Reading it unwrapped made 372’s first prototype’s shifts nonsense while every individual registration was correct.

about IS NOT DECORATION AND THE DEFAULT IS THE DANGEROUS CASE. With about=0 this folds into [-extent/2, extent/2), which is right only when the true shift is smaller than half the axis. It is not, whenever the correlation runs on an overlap STRIP: a 222 px strip carrying a true shift of 116 folds to -106, a number that is both wrong and plausible. The caller knows roughly where the neighbour is – the raster pitch says so – so it passes that, and the representative nearest it is the answer.

Parameters:
  • value – the raw peak coordinate.

  • extent – the axis length.

  • about – the shift the caller expects, in the same units.

Returns:

the representative of value mod extent closest to about.