Skip to content

Commit

Permalink
filter_tracks: add option to filter by length
Browse files Browse the repository at this point in the history
  • Loading branch information
JoepVanlier committed Nov 7, 2023
1 parent 495571b commit 27e8f61
Show file tree
Hide file tree
Showing 3 changed files with 45 additions and 3 deletions.
1 change: 1 addition & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
* Shortcuts `"r"`, `"g"`, and `"b"` can now be used for plotting single color channels in addition to `"red"`, `"green"`, and `"blue"`.
* Two-channel visualizations can be plotted using `"rg"`, `"gb"`, or `"rb"`.
* Added `duration` property to `KymoTrack` which returns the duration (in seconds) that the track was observed.
* Added option to filter tracks with `lk.filter_tracks(tracks, minimum_duration=duration_in_seconds)` by track duration (in seconds).

#### Improvements

Expand Down
17 changes: 14 additions & 3 deletions lumicks/pylake/kymotracker/kymotracker.py
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,7 @@ def track_lines(
return refine_tracks_centroid(kymotrack_group, track_width=line_width, bias_correction=True)


def filter_tracks(tracks, minimum_length):
def filter_tracks(tracks, minimum_length, *, minimum_duration=0):
"""Remove tracks shorter than a minimum number of time points from the list.
This can be used to enforce a minimum number of frames a spot has to be detected in order
Expand All @@ -379,8 +379,19 @@ def filter_tracks(tracks, minimum_length):
Detected tracks on a kymograph.
minimum_length : int
Minimum length for the track to be accepted.
minimum_duration : seconds
Minimum duration for a track to be accepted.
"""

def minimum_observable_time(track, min_length, min_duration):
line_time = track._kymo.line_time_seconds
minimum_length_based = (min_length - 1) * line_time

# When we filter with a minimum duration, we lose all durations up to the next
# full time step.
minimum_duration_based = np.ceil(min_duration / line_time) * line_time
return max(minimum_length_based, minimum_duration_based)

return KymoTrackGroup(
[
track._with_minimum_time(
Expand All @@ -389,11 +400,11 @@ def filter_tracks(tracks, minimum_length):
track._minimum_observable_duration
if track._minimum_observable_duration is not None
else 0,
(minimum_length - 1) * track._kymo.line_time_seconds,
minimum_observable_time(track, minimum_length, minimum_duration),
)
)
for track in tracks
if len(track) >= minimum_length
if len(track) >= minimum_length and track.duration >= minimum_duration
]
)

Expand Down
30 changes: 30 additions & 0 deletions lumicks/pylake/kymotracker/tests/test_refinement.py
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,36 @@ def test_filter_tracks(blank_kymo, blank_kymo_track_args):
)


@pytest.mark.parametrize(
"minimum_duration, ref_length, ref_minimum, ref_minimum_dt2",
[
[1.0, 5, 1.0, 2.0],
[1.1, 4, 2.0, 2.0], # Going over slightly bumps the minimum time to the next frame
[2.0, 4, 2.0, 2.0],
[2.1, 2, 3.0, 4.0], # Last line gets a bigger bump in minimum time once we filter
],
)
def test_filter_by_duration(
blank_kymo_track_args, minimum_duration, ref_length, ref_minimum, ref_minimum_dt2
):
k1 = KymoTrack([1, 2, 3], [1, 2, 3], *blank_kymo_track_args) # duration = 2
k2 = KymoTrack([2, 3], [1, 2], *blank_kymo_track_args) # duration = 1
k3 = KymoTrack([2, 3, 5], [1, 2, 5], *blank_kymo_track_args) # duration = 3

# Track with different line time. We use two line times because we want to test that the
# minimum observable time gets bumped correctly to the next full timestep.
kymo2 = _kymo_from_array(np.zeros((3, 3)), "r", line_time_seconds=2.0, pixel_size_um=50)
k4 = KymoTrack([2, 3], [1, 2, 5], kymo2, "red", 0) # duration = 2
k5 = KymoTrack([2, 6], [1, 2, 5], kymo2, "red", 0) # duration = 6

tracks = KymoTrackGroup([k1, k2, k3, k4, k5])

filtered = filter_tracks(tracks, 0, minimum_duration=minimum_duration)
assert len(filtered) == ref_length
np.testing.assert_allclose(filtered[0]._minimum_observable_duration, ref_minimum)
np.testing.assert_allclose(filtered[-1]._minimum_observable_duration, ref_minimum_dt2)


def test_empty_group():
"""Validate that the refinement methods don't fail when applied to an empty group"""
tracks = KymoTrackGroup([])
Expand Down

0 comments on commit 27e8f61

Please sign in to comment.