byotrack 1.2.0.dev1__tar.gz → 1.2.0.dev3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. {byotrack-1.2.0.dev1 → byotrack-1.2.0.dev3}/PKG-INFO +1 -1
  2. {byotrack-1.2.0.dev1 → byotrack-1.2.0.dev3}/byotrack/__init__.py +1 -1
  3. {byotrack-1.2.0.dev1 → byotrack-1.2.0.dev3}/byotrack/api/detector/detections.py +46 -0
  4. {byotrack-1.2.0.dev1 → byotrack-1.2.0.dev3}/byotrack/api/features_extractor.py +40 -1
  5. {byotrack-1.2.0.dev1 → byotrack-1.2.0.dev3}/byotrack/dataset/ctc.py +41 -4
  6. {byotrack-1.2.0.dev1 → byotrack-1.2.0.dev3}/byotrack/fiji/run.py +14 -3
  7. byotrack-1.2.0.dev3/byotrack/implementation/linker/frame_by_frame/base.py +794 -0
  8. {byotrack-1.2.0.dev1 → byotrack-1.2.0.dev3}/byotrack/implementation/linker/frame_by_frame/kalman_linker.py +24 -13
  9. {byotrack-1.2.0.dev1 → byotrack-1.2.0.dev3}/byotrack/implementation/linker/frame_by_frame/koft.py +32 -21
  10. {byotrack-1.2.0.dev1 → byotrack-1.2.0.dev3}/byotrack/implementation/linker/frame_by_frame/nearest_neighbor.py +24 -12
  11. {byotrack-1.2.0.dev1 → byotrack-1.2.0.dev3}/byotrack/implementation/linker/trackmate/trackmate.py +1 -2
  12. {byotrack-1.2.0.dev1 → byotrack-1.2.0.dev3}/byotrack/metrics/ctc.py +152 -14
  13. {byotrack-1.2.0.dev1 → byotrack-1.2.0.dev3}/byotrack.egg-info/PKG-INFO +1 -1
  14. byotrack-1.2.0.dev1/byotrack/implementation/linker/frame_by_frame/base.py +0 -486
  15. {byotrack-1.2.0.dev1 → byotrack-1.2.0.dev3}/LICENSE +0 -0
  16. {byotrack-1.2.0.dev1 → byotrack-1.2.0.dev3}/README.md +0 -0
  17. {byotrack-1.2.0.dev1 → byotrack-1.2.0.dev3}/byotrack/api/__init__.py +0 -0
  18. {byotrack-1.2.0.dev1 → byotrack-1.2.0.dev3}/byotrack/api/detector/__init__.py +0 -0
  19. {byotrack-1.2.0.dev1 → byotrack-1.2.0.dev3}/byotrack/api/detector/detector.py +0 -0
  20. {byotrack-1.2.0.dev1 → byotrack-1.2.0.dev3}/byotrack/api/linker.py +0 -0
  21. {byotrack-1.2.0.dev1 → byotrack-1.2.0.dev3}/byotrack/api/optical_flow/__init__.py +0 -0
  22. {byotrack-1.2.0.dev1 → byotrack-1.2.0.dev3}/byotrack/api/optical_flow/optical_flow.py +0 -0
  23. {byotrack-1.2.0.dev1 → byotrack-1.2.0.dev3}/byotrack/api/parameters.py +0 -0
  24. {byotrack-1.2.0.dev1 → byotrack-1.2.0.dev3}/byotrack/api/refiner.py +0 -0
  25. {byotrack-1.2.0.dev1 → byotrack-1.2.0.dev3}/byotrack/api/tracker.py +0 -0
  26. {byotrack-1.2.0.dev1 → byotrack-1.2.0.dev3}/byotrack/api/tracks.py +0 -0
  27. {byotrack-1.2.0.dev1 → byotrack-1.2.0.dev3}/byotrack/dataset/__init__.py +0 -0
  28. {byotrack-1.2.0.dev1 → byotrack-1.2.0.dev3}/byotrack/example_data.py +0 -0
  29. {byotrack-1.2.0.dev1 → byotrack-1.2.0.dev3}/byotrack/fiji/__init__.py +0 -0
  30. {byotrack-1.2.0.dev1 → byotrack-1.2.0.dev3}/byotrack/fiji/io.py +0 -0
  31. {byotrack-1.2.0.dev1 → byotrack-1.2.0.dev3}/byotrack/icy/__init__.py +0 -0
  32. {byotrack-1.2.0.dev1 → byotrack-1.2.0.dev3}/byotrack/icy/io.py +0 -0
  33. {byotrack-1.2.0.dev1 → byotrack-1.2.0.dev3}/byotrack/icy/run.py +0 -0
  34. {byotrack-1.2.0.dev1 → byotrack-1.2.0.dev3}/byotrack/implementation/__init__.py +0 -0
  35. {byotrack-1.2.0.dev1 → byotrack-1.2.0.dev3}/byotrack/implementation/detector/__init__.py +0 -0
  36. {byotrack-1.2.0.dev1 → byotrack-1.2.0.dev3}/byotrack/implementation/detector/stardist.py +0 -0
  37. {byotrack-1.2.0.dev1 → byotrack-1.2.0.dev3}/byotrack/implementation/detector/wavelet.py +0 -0
  38. {byotrack-1.2.0.dev1 → byotrack-1.2.0.dev3}/byotrack/implementation/linker/__init__.py +0 -0
  39. {byotrack-1.2.0.dev1 → byotrack-1.2.0.dev3}/byotrack/implementation/linker/frame_by_frame/__init__.py +0 -0
  40. {byotrack-1.2.0.dev1 → byotrack-1.2.0.dev3}/byotrack/implementation/linker/frame_by_frame/greedy_lap.py +0 -0
  41. {byotrack-1.2.0.dev1 → byotrack-1.2.0.dev3}/byotrack/implementation/linker/icy_emht/__init__.py +0 -0
  42. {byotrack-1.2.0.dev1 → byotrack-1.2.0.dev3}/byotrack/implementation/linker/icy_emht/emht_protocol.xml +0 -0
  43. {byotrack-1.2.0.dev1 → byotrack-1.2.0.dev3}/byotrack/implementation/linker/icy_emht/emht_protocol_with_full_specs.xml +0 -0
  44. {byotrack-1.2.0.dev1 → byotrack-1.2.0.dev3}/byotrack/implementation/linker/icy_emht/icy_emht.py +0 -0
  45. {byotrack-1.2.0.dev1 → byotrack-1.2.0.dev3}/byotrack/implementation/linker/trackmate/__init__.py +0 -0
  46. {byotrack-1.2.0.dev1 → byotrack-1.2.0.dev3}/byotrack/implementation/linker/trackmate/_trackmate.py +0 -0
  47. {byotrack-1.2.0.dev1 → byotrack-1.2.0.dev3}/byotrack/implementation/optical_flow/__init__.py +0 -0
  48. {byotrack-1.2.0.dev1 → byotrack-1.2.0.dev3}/byotrack/implementation/optical_flow/opencv.py +0 -0
  49. {byotrack-1.2.0.dev1 → byotrack-1.2.0.dev3}/byotrack/implementation/optical_flow/skimage.py +0 -0
  50. {byotrack-1.2.0.dev1 → byotrack-1.2.0.dev3}/byotrack/implementation/refiner/__init__.py +0 -0
  51. {byotrack-1.2.0.dev1 → byotrack-1.2.0.dev3}/byotrack/implementation/refiner/cleaner.py +0 -0
  52. {byotrack-1.2.0.dev1 → byotrack-1.2.0.dev3}/byotrack/implementation/refiner/interpolater.py +0 -0
  53. {byotrack-1.2.0.dev1 → byotrack-1.2.0.dev3}/byotrack/implementation/refiner/propagation.py +0 -0
  54. {byotrack-1.2.0.dev1 → byotrack-1.2.0.dev3}/byotrack/implementation/refiner/stitching/__init__.py +0 -0
  55. {byotrack-1.2.0.dev1 → byotrack-1.2.0.dev3}/byotrack/implementation/refiner/stitching/dist_stitcher.py +0 -0
  56. {byotrack-1.2.0.dev1 → byotrack-1.2.0.dev3}/byotrack/implementation/refiner/stitching/emc2.py +0 -0
  57. {byotrack-1.2.0.dev1 → byotrack-1.2.0.dev3}/byotrack/metrics/__init__.py +0 -0
  58. {byotrack-1.2.0.dev1 → byotrack-1.2.0.dev3}/byotrack/py.typed +0 -0
  59. {byotrack-1.2.0.dev1 → byotrack-1.2.0.dev3}/byotrack/utils.py +0 -0
  60. {byotrack-1.2.0.dev1 → byotrack-1.2.0.dev3}/byotrack/video/__init__.py +0 -0
  61. {byotrack-1.2.0.dev1 → byotrack-1.2.0.dev3}/byotrack/video/reader.py +0 -0
  62. {byotrack-1.2.0.dev1 → byotrack-1.2.0.dev3}/byotrack/video/transforms.py +0 -0
  63. {byotrack-1.2.0.dev1 → byotrack-1.2.0.dev3}/byotrack/video/video.py +0 -0
  64. {byotrack-1.2.0.dev1 → byotrack-1.2.0.dev3}/byotrack/visualize.py +0 -0
  65. {byotrack-1.2.0.dev1 → byotrack-1.2.0.dev3}/byotrack.egg-info/SOURCES.txt +0 -0
  66. {byotrack-1.2.0.dev1 → byotrack-1.2.0.dev3}/byotrack.egg-info/dependency_links.txt +0 -0
  67. {byotrack-1.2.0.dev1 → byotrack-1.2.0.dev3}/byotrack.egg-info/requires.txt +0 -0
  68. {byotrack-1.2.0.dev1 → byotrack-1.2.0.dev3}/byotrack.egg-info/top_level.txt +0 -0
  69. {byotrack-1.2.0.dev1 → byotrack-1.2.0.dev3}/pyproject.toml +0 -0
  70. {byotrack-1.2.0.dev1 → byotrack-1.2.0.dev3}/setup.cfg +0 -0
  71. {byotrack-1.2.0.dev1 → byotrack-1.2.0.dev3}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: byotrack
3
- Version: 1.2.0.dev1
3
+ Version: 1.2.0.dev3
4
4
  Summary: Biological particle tracking with Python
5
5
  Home-page: https://github.com/raphaelreme/byotrack
6
6
  Author: Raphael Reme
@@ -83,4 +83,4 @@ from byotrack.api.tracks import Track
83
83
  from byotrack.video import Video, VideoTransformConfig
84
84
 
85
85
 
86
- __version__ = "1.2.0.dev1"
86
+ __version__ = "1.2.0.dev3"
@@ -36,6 +36,32 @@ def _check_confidence(confidence: torch.Tensor) -> None:
36
36
  assert confidence.dtype is torch.float32
37
37
 
38
38
 
39
+ @numba.njit
40
+ def _compute_mass(segmentation: np.ndarray) -> np.ndarray:
41
+ """Extract the number of pixels of each object in the segmentation
42
+
43
+ Args:
44
+ segmentation (np.ndarray): Segmentation mask
45
+
46
+ Returns:
47
+ np.ndarray: Mass for each object
48
+ Shape (n,), dtype: int32
49
+
50
+ """
51
+ n = segmentation.max()
52
+ mass = np.zeros(n, dtype=np.int32)
53
+
54
+ # Ravel in 1D
55
+ segmentation = segmentation.reshape(-1)
56
+
57
+ for i in range(segmentation.shape[0]):
58
+ instance = segmentation[i] - 1
59
+ if instance != -1:
60
+ mass[instance] += 1
61
+
62
+ return mass
63
+
64
+
39
65
  @numba.njit(parallel=False)
40
66
  def _position_from_segmentation(segmentation: np.ndarray) -> np.ndarray:
41
67
  """Return the center (mean) of each instance in the segmentation"""
@@ -278,6 +304,8 @@ class Detections:
278
304
  Shape: ([D, ]H, W), dtype: int32
279
305
  confidence (torch.Tensor): Confidence for each instance
280
306
  Shape: (N,), dtype: float32
307
+ mass (torch.Tensor): Size of each object in pixel, inferred from the data.
308
+ Shape: (N,), dtype: int32
281
309
  use_median_position (bool): Use median instead of mean to compute positions from segmentation.
282
310
  Default: True (Usually more robust)
283
311
 
@@ -360,6 +388,15 @@ class Detections:
360
388
 
361
389
  return confidence
362
390
 
391
+ @property
392
+ def mass(self) -> torch.Tensor:
393
+ mass = self.data.get("mass", self._lazy_extrapolated_data.get("mass"))
394
+ if mass is None:
395
+ mass = self._extrapolate_mass()
396
+ self._lazy_extrapolated_data["confidence"] = mass
397
+
398
+ return mass
399
+
363
400
  @property
364
401
  def use_median_position(self) -> bool:
365
402
  return self._use_median_position
@@ -443,6 +480,15 @@ class Detections:
443
480
  """Extrapolate confidence"""
444
481
  return torch.ones(self.length)
445
482
 
483
+ def _extrapolate_mass(self) -> torch.Tensor:
484
+ if "segmentation" in self.data:
485
+ return torch.tensor(_compute_mass(self.data["segmentation"].numpy()), dtype=torch.int32)
486
+
487
+ if "bbox" in self.data:
488
+ return self.data["bbox"][:, self.dim :].prod(dim=-1)
489
+
490
+ return torch.ones(self.length, dtype=torch.int32)
491
+
446
492
  def __len__(self) -> int:
447
493
  return self.length
448
494
 
@@ -4,6 +4,7 @@ from abc import ABC, abstractmethod
4
4
  from typing import Iterable
5
5
 
6
6
  import numpy as np
7
+ import numba # type: ignore
7
8
  import torch
8
9
 
9
10
  import byotrack # pylint: disable=cyclic-import
@@ -56,4 +57,42 @@ class MultiFeaturesExtractor(FeaturesExtractor):
56
57
  return torch.cat(features, dim=-1)
57
58
 
58
59
 
59
- # TODO: Add some examples
60
+ class MassExtractor(FeaturesExtractor):
61
+ """Extract the mass of each detection (number of pixels)"""
62
+
63
+ def __call__(self, frame: np.ndarray, detections: byotrack.Detections):
64
+ return detections.mass
65
+
66
+
67
+ class IntensityExtractor(FeaturesExtractor):
68
+ """Extract the sum of intensities of each detection"""
69
+
70
+ def __call__(self, frame: np.ndarray, detections: byotrack.Detections):
71
+ torch.tensor(compute_intensity(detections.segmentation.numpy(), frame.sum(axis=-1)), dtype=torch.float32)
72
+
73
+
74
+ @numba.njit
75
+ def compute_intensity(segmentation: np.ndarray, frame: np.ndarray) -> np.ndarray:
76
+ """Extract the cumulated intensity of each detection
77
+
78
+ Args:
79
+ segmentation (np.ndarray): Segmentation mask
80
+ frame (np.ndarray): Video frame (should have the same number of pixels than segmentation)
81
+
82
+ Returns:
83
+ np.ndarray: Sum of intensity for each object
84
+
85
+ """
86
+ n = segmentation.max()
87
+ intensity = np.zeros(n, dtype=frame.dtype)
88
+
89
+ # Ravel in 1D
90
+ segmentation = segmentation.reshape(-1)
91
+ frame = frame.reshape(-1)
92
+
93
+ for i in range(segmentation.shape[0]):
94
+ instance = segmentation[i] - 1
95
+ if instance != -1:
96
+ intensity[instance] += frame[i]
97
+
98
+ return intensity
@@ -286,6 +286,8 @@ def _fast_disk_2d(
286
286
  positions: np.ndarray,
287
287
  identifiers: np.ndarray,
288
288
  radius: np.ndarray,
289
+ *,
290
+ overwrite=False,
289
291
  ):
290
292
  """Fast inplace drawing of disk in 2D
291
293
 
@@ -300,6 +302,8 @@ def _fast_disk_2d(
300
302
  Shape: (n,), dtype: uint16
301
303
  radius (np.ndarray): Radius of each disk
302
304
  Shape: (n, ), dtype: float32
305
+ overwrite (bool): Overwrite pixels that are already written (!=0)
306
+ Default: False
303
307
 
304
308
  """
305
309
  positions_round = np.round(positions).astype(np.int64)
@@ -315,6 +319,9 @@ def _fast_disk_2d(
315
319
  if not 0 <= i < segmentation.shape[0] or not 0 <= j < segmentation.shape[1]:
316
320
  continue
317
321
 
322
+ if not overwrite and segmentation[i, j] != 0 and best_dist[i, j] == np.inf:
323
+ continue
324
+
318
325
  delta = pos - positions[k]
319
326
  dist = delta @ delta
320
327
 
@@ -325,12 +332,15 @@ def _fast_disk_2d(
325
332
 
326
333
 
327
334
  @numba.njit(parallel=True)
328
- def _fast_disk_3d(
335
+ def _fast_disk_3d( # pylint: disable=too-many-locals
329
336
  segmentation: np.ndarray,
330
337
  bbox: np.ndarray,
331
338
  positions: np.ndarray,
332
339
  identifiers: np.ndarray,
333
340
  radius: np.ndarray,
341
+ *,
342
+ anisoptropy=1.0,
343
+ overwrite=False,
334
344
  ):
335
345
  """Fast inplace drawing of disk in 3D
336
346
 
@@ -345,6 +355,11 @@ def _fast_disk_3d(
345
355
  Shape: (n,), dtype: uint16
346
356
  radius (np.ndarray): Radius of each disk
347
357
  Shape: (n, ), dtype: float32
358
+ anisotropy (float): Relative size of a pixel along the depth dimension
359
+ versus height/width dimensions.
360
+ Default: 1.0
361
+ overwrite (bool): Overwrite pixels that are already written (!=0)
362
+ Default: False
348
363
 
349
364
  """
350
365
  positions_round = np.round(positions).astype(np.int64)
@@ -364,7 +379,11 @@ def _fast_disk_3d(
364
379
  ):
365
380
  continue
366
381
 
382
+ if not overwrite and segmentation[z, i, j] != 0 and best_dist[z, i, j] == np.inf:
383
+ continue
384
+
367
385
  delta = pos - positions[k]
386
+ delta[0] *= anisoptropy # Increase distance in Z by the anisotropy
368
387
  dist = delta @ delta
369
388
 
370
389
  if dist <= radius[k]:
@@ -378,6 +397,9 @@ def draw_disk(
378
397
  positions: np.ndarray,
379
398
  identifiers: np.ndarray,
380
399
  radius: np.ndarray,
400
+ *,
401
+ anisotropy=1.0,
402
+ overwrite=False,
381
403
  ):
382
404
  """Draw disks on the segmentation
383
405
 
@@ -390,21 +412,27 @@ def draw_disk(
390
412
  Shape: (n,), dtype: uint16
391
413
  radius (np.ndarray): Radius of each disk
392
414
  Shape: (n, ), dtype: float32)
415
+ anisotropy (float): Relative size of a pixel along the depth dimension
416
+ versus height/width dimensions.
417
+ Default: 1.0
418
+ overwrite (bool): Overwrite pixels that are already written (!=0)
419
+ Default: False
420
+
393
421
  """
394
422
  # Wrapped to redirect in 2D/3D and numba does not support np.indices in 3.8
395
423
  if segmentation.ndim == 3:
396
424
  thresh = round(radius.max())
397
425
  bbox: np.ndarray = np.indices((thresh * 2 + 1, thresh * 2 + 1, thresh * 2 + 1)).transpose(1, 2, 3, 0) - thresh
398
426
  bbox = bbox.reshape(-1, 3)
399
- _fast_disk_3d(segmentation, bbox, positions, identifiers, radius)
427
+ _fast_disk_3d(segmentation, bbox, positions, identifiers, radius, anisoptropy=anisotropy, overwrite=overwrite)
400
428
  else:
401
429
  thresh = int(round(radius.max()))
402
430
  bbox = np.indices((thresh * 2 + 1, thresh * 2 + 1)).transpose(1, 2, 0) - thresh
403
431
  bbox = bbox.reshape(-1, 2)
404
- _fast_disk_2d(segmentation, bbox, positions, identifiers, radius)
432
+ _fast_disk_2d(segmentation, bbox, positions, identifiers, radius, overwrite=overwrite)
405
433
 
406
434
 
407
- def save_tracks( # pylint: disable=too-many-branches,too-many-locals,too-many-statements
435
+ def save_tracks( # pylint: disable=too-many-branches,too-many-locals,too-many-statements,too-many-arguments
408
436
  path: Union[str, os.PathLike],
409
437
  tracks: Collection[byotrack.Track],
410
438
  detections_sequence: Sequence[byotrack.Detections] = (),
@@ -415,6 +443,8 @@ def save_tracks( # pylint: disable=too-many-branches,too-many-locals,too-many-s
415
443
  last=0,
416
444
  shape: Optional[Tuple[int, ...]] = None,
417
445
  n_digit=4,
446
+ anisotropy=1.0,
447
+ overwrite_detections=False,
418
448
  ):
419
449
  """Save tracks in the CTC format [10]
420
450
 
@@ -453,6 +483,11 @@ def save_tracks( # pylint: disable=too-many-branches,too-many-locals,too-many-s
453
483
  Default: None
454
484
  n_digit (int): Number of digit used to encode time in file names.
455
485
  Default: 4
486
+ anisotropy (float): Relative size of a pixel along the depth dimension
487
+ versus height/width dimensions.
488
+ Default: 1.0
489
+ overwrite_detections (bool): Overwrite the segmentation of objects with disk.
490
+ Default: False (Disk are only drawn on background)
456
491
 
457
492
  """
458
493
  path = pathlib.Path(path)
@@ -517,6 +552,8 @@ def save_tracks( # pylint: disable=too-many-branches,too-many-locals,too-many-s
517
552
  torch.stack(disk_positions).numpy(),
518
553
  np.array(disk_ids, dtype=np.uint16) + 1,
519
554
  np.full(len(disk_ids), default_radius, dtype=np.float32),
555
+ anisotropy=anisotropy,
556
+ overwrite=overwrite_detections,
520
557
  )
521
558
 
522
559
  # Safety checks because CTC is quite restrictive
@@ -10,16 +10,23 @@ class FijiRunner: # pylint: disable=too-few-public-methods
10
10
  Attributes:
11
11
  fiji_path (str | os.PathLike): Path to the fiji executable
12
12
  The executable can be found inside the installation folder of Fiji.
13
- Linux: Fiji.app/ImageJ-<os>.exe
13
+ Linux: Fiji.app/ImageJ-<os>
14
14
  Windows: Fiji.app/ImageJ-<os>.exe
15
15
  MacOs: Fiji.app/Contents/MacOs/ImageJ-<os>
16
+ capture_outputs (bool): Whether to PIPE stderr and stdout into Python
17
+ This will allow you to find the stdout/stderr inside `last_outputs`.
18
+ But outputs are captured, and you do not see them while the scripts run.
19
+ Default: False
20
+ last_outputs (subprocess.CompletedProcess): Outputs of the last subprocess.run
16
21
 
17
22
  """
18
23
 
19
24
  cmd = './{fiji} --ij2 --headless --console --run "{script}" "{args}"'
20
25
 
21
- def __init__(self, fiji_path: Union[str, os.PathLike]) -> None:
26
+ def __init__(self, fiji_path: Union[str, os.PathLike], capture_outputs=False) -> None:
22
27
  self.fiji_path = fiji_path
28
+ self.capture_outputs = capture_outputs
29
+ self.last_outputs: subprocess.CompletedProcess = subprocess.CompletedProcess("", 0)
23
30
 
24
31
  assert os.path.isfile(fiji_path), "Unable to found the given path"
25
32
 
@@ -42,4 +49,8 @@ class FijiRunner: # pylint: disable=too-few-public-methods
42
49
  cmd = cmd[2:] # Strip ./ on windows
43
50
 
44
51
  print("Calling Fiji with:", cmd)
45
- return subprocess.run(cmd, check=True, cwd=os.path.dirname(self.fiji_path), shell=True).returncode
52
+ self.last_outputs = subprocess.run(
53
+ cmd, check=True, cwd=os.path.dirname(self.fiji_path), shell=True, capture_output=self.capture_outputs
54
+ )
55
+
56
+ return self.last_outputs.returncode