From 189dd81e7f298d220203535cede4b783e291c547 Mon Sep 17 00:00:00 2001 From: rpauszek Date: Wed, 29 Jan 2025 16:47:49 +0100 Subject: [PATCH] kymo: allow calibrating upside down tether --- lumicks/pylake/kymo.py | 23 ++++++----- .../tests/test_greedy_algorithm.py | 39 +++++++++++++++++++ .../test_kymo_transforms.py | 12 ++++-- 3 files changed, 61 insertions(+), 13 deletions(-) diff --git a/lumicks/pylake/kymo.py b/lumicks/pylake/kymo.py index a5bce449c..72ce869f1 100644 --- a/lumicks/pylake/kymo.py +++ b/lumicks/pylake/kymo.py @@ -1016,12 +1016,11 @@ def calibrate_to_kbp(self, length_kbp, *, start=None, end=None): if (start is None) ^ (end is None): raise ValueError("Both start and end points of the tether must be supplied.") - if start is not None: - if end < start: - raise ValueError("end must be larger than start.") - kbp_per_pixel = length_kbp / (end - start) * self.pixelsize_um[0] - else: - kbp_per_pixel = length_kbp / self._num_pixels[0] + kbp_per_pixel = ( + length_kbp / (end - start) * self.pixelsize_um[0] + if start is not None + else length_kbp / self._num_pixels[0] + ) pixel_origin = start / self.pixelsize_um[0] if start is not None else 0.0 result = copy(self) @@ -1123,7 +1122,7 @@ def label(self): @dataclass(frozen=True) class PositionCalibration: unit: PositionUnit = PositionUnit.pixel - value: float = 1.0 + _value: float = 1.0 origin: float = 0.0 def __post_init__(self): @@ -1132,11 +1131,15 @@ def __post_init__(self): def from_pixels(self, pixels): """Convert coordinates from pixel values to calibrated values""" - return self.value * (np.array(pixels) - self.origin) + return self._value * (np.array(pixels) - self.origin) def to_pixels(self, calibrated): """Convert coordinates from calibrated values to pixel values""" - return np.array(calibrated) / self.value + self.origin + return np.array(calibrated) / self._value + self.origin + + @property + def value(self): + return np.abs(self._value) @property def unit_label(self): @@ -1146,7 +1149,7 @@ def downsample(self, factor): return ( self if self.unit == PositionUnit.pixel - else PositionCalibration(self.unit, self.value * factor) + else PositionCalibration(self.unit, self._value * factor) ) diff --git a/lumicks/pylake/kymotracker/tests/test_greedy_algorithm.py b/lumicks/pylake/kymotracker/tests/test_greedy_algorithm.py index 86515f415..2d50dcd70 100644 --- a/lumicks/pylake/kymotracker/tests/test_greedy_algorithm.py +++ b/lumicks/pylake/kymotracker/tests/test_greedy_algorithm.py @@ -171,3 +171,42 @@ def test_default_parameters(kymo_pixel_calibrations): with np.testing.assert_raises(AssertionError): for ref, track in zip(ref_tracks, tracks): np.testing.assert_allclose(ref.position, track.position) + + +@pytest.mark.parametrize("tether_len,start,end", [(10, 0.18, 0.62), (48.502, 0.03, 0.94)]) +def test_track_calibrated_flipped(tether_len, start, end): + """test that tracking a calibrated kymo with tether end < tether start yields the same + coordinates as tracking the flipped tether with end > start. + """ + + image = np.zeros((20, 25)) + + # ([time, position], ...) + ref_tracks = [ + ([1, 5], [2, 6], [3, 7]), + ([5, 15], [6, 15], [7, 14], [8, 15], [9, 16]), + ] + for coords in ref_tracks: + for c in coords: + image[*c[::-1]] = 10 + + kymo = _kymo_from_array(image, "g", line_time_seconds=0.1, pixel_size_um=0.050) + kymo_flipped = kymo.flip() + + # start, end = 0.18, 0.62 + len_um = kymo._calibration.from_pixels(kymo._num_pixels[0] - 1) + end_flipped = len_um - end + start_flipped = len_um - start + + # tether_len = 10 + kymo = kymo.calibrate_to_kbp(tether_len, start=start, end=end) + kymo_flipped = kymo_flipped.calibrate_to_kbp(tether_len, start=start_flipped, end=end_flipped) + + params = dict(pixel_threshold=5, window=3) + tracks = track_greedy(kymo, "green", track_width=3 * kymo._calibration.value, **params) + tracks_flipped = track_greedy( + kymo_flipped, "green", track_width=3 * kymo_flipped._calibration.value, **params + ) + + for track, track_flipped in zip(tracks, tracks_flipped): + np.testing.assert_allclose(track.position, track_flipped.position) diff --git a/lumicks/pylake/tests/test_imaging_confocal/test_kymo_transforms.py b/lumicks/pylake/tests/test_imaging_confocal/test_kymo_transforms.py index 1a6867170..5c879ebee 100644 --- a/lumicks/pylake/tests/test_imaging_confocal/test_kymo_transforms.py +++ b/lumicks/pylake/tests/test_imaging_confocal/test_kymo_transforms.py @@ -36,9 +36,6 @@ def test_calibrate_to_kbp(test_kymo): np.testing.assert_allclose(kymo_bp._calibration.value * n_pixels_tether, length_kbp) np.testing.assert_allclose(kymo_bp.pixelsize, length_kbp / n_pixels_tether) - with pytest.raises(ValueError, match="end must be larger than start."): - kymo.calibrate_to_kbp(length_kbp, start=end, end=start) - with pytest.raises(RuntimeError, match="kymo is already calibrated in base pairs."): kymo_bp.calibrate_to_kbp(10) @@ -163,3 +160,12 @@ def test_coordinate_transforms(): transformed = c.from_pixels(px_coord) np.testing.assert_allclose(kbp_coord, transformed) np.testing.assert_allclose(px_coord, c.to_pixels(transformed)) + + c_flipped = PositionCalibration("kbp", -0.42, origin=2.0) + kbp_coord = -kbp_coord + transformed = c_flipped.from_pixels(px_coord) + np.testing.assert_allclose(kbp_coord, transformed) + np.testing.assert_allclose(px_coord, c_flipped.to_pixels(transformed)) + + assert c._value == -c_flipped._value + assert c.value == c_flipped.value