|
40 | 40 | kymograph, |
41 | 41 | detection, |
42 | 42 | loi_detection, |
43 | | - # sarcomere_tracking |
| 43 | + sarcomere_tracking, |
44 | 44 | ) |
45 | 45 |
|
46 | 46 | class Structure(SarcAsM): |
@@ -1615,188 +1615,188 @@ def delete_lois(self): |
1615 | 1615 | except Exception as e: |
1616 | 1616 | logger.debug(f"Error deleting LOI directory: {e}. Continuing anyway.") |
1617 | 1617 |
|
1618 | | - # def track_sarcomere_vectors( |
1619 | | - # self, |
1620 | | - # frames: Union[str, int, List[int], np.ndarray] = 'all', |
1621 | | - # max_distance: float = 0.5, |
1622 | | - # memory: int = 5, |
1623 | | - # min_track_length: int = 10, |
1624 | | - # flow_blur_sigma: Optional[float] = None, |
1625 | | - # use_hungarian: bool = True, |
1626 | | - # length_weight: float = 2.0, |
1627 | | - # return_flow: bool = False, |
1628 | | - # progress_notifier: ProgressNotifier = ProgressNotifier.progress_notifier_tqdm() |
1629 | | - # ) -> Optional[List[np.ndarray]]: |
1630 | | - # """ |
1631 | | - # Track sarcomere vectors across frames using optical flow. |
1632 | | - |
1633 | | - # This method uses dense optical flow computed on Z-band and M-band masks |
1634 | | - # to predict vector motion between frames, then matches predicted positions |
1635 | | - # to detected vectors using greedy exclusive assignment. |
1636 | | - |
1637 | | - # Parameters |
1638 | | - # ---------- |
1639 | | - # frames : {'all', int, list, np.ndarray}, optional |
1640 | | - # Frames for tracking ('all' for all frames, int for a single frame, |
1641 | | - # list or ndarray for selected frames). Defaults to 'all'. |
1642 | | - # max_distance : float, optional |
1643 | | - # Maximum matching distance in µm. Vectors predicted to be further |
1644 | | - # than this from any detection are considered lost. Default 0.5 µm. |
1645 | | - # memory : int, optional |
1646 | | - # Number of frames to keep tracks "sleeping" before termination. |
1647 | | - # Sleeping tracks can be re-linked if a matching vector reappears. |
1648 | | - # Default 5 frames. |
1649 | | - # min_track_length : int, optional |
1650 | | - # Minimum track length to keep. Shorter tracks are filtered out |
1651 | | - # and their vectors marked as untracked (track_id = -1). Default 10. |
1652 | | - # flow_blur_sigma : float, optional |
1653 | | - # Gaussian blur sigma (in pixels) to apply to masks before optical |
1654 | | - # flow computation. Can help with noisy masks. Default None (no blur). |
1655 | | - # use_hungarian : bool, optional |
1656 | | - # If True, use Hungarian algorithm for optimal matching. This gives |
1657 | | - # better results when vectors are densely packed. Default True. |
1658 | | - # length_weight : float, optional |
1659 | | - # Weight for sarcomere length difference in matching cost when using |
1660 | | - # Hungarian algorithm. Higher values favor matching vectors with |
1661 | | - # similar sarcomere lengths. Default 2.0. |
1662 | | - # return_flow : bool, optional |
1663 | | - # If True, also return the computed optical flow fields. |
1664 | | - # Default False. |
1665 | | - # progress_notifier : ProgressNotifier, optional |
1666 | | - # Progress notifier for tracking progress. |
1667 | | - |
1668 | | - # Returns |
1669 | | - # ------- |
1670 | | - # flow_fields : list of np.ndarray, optional |
1671 | | - # Only returned if return_flow=True. List of flow fields, |
1672 | | - # each of shape (H, W, 2). |
1673 | | - |
1674 | | - # Notes |
1675 | | - # ----- |
1676 | | - # Results are stored in `self.data` with the following keys: |
1677 | | - |
1678 | | - # - ``'track_ids'``: list of (N_t,) arrays with track ID for each vector |
1679 | | - # per frame. Track ID of -1 indicates untracked vectors. |
1680 | | - # - ``'n_tracks'``: total number of unique trajectories. |
1681 | | - # - ``'track_lengths'``: (n_tracks,) array with number of frames each |
1682 | | - # track spans. |
1683 | | - # - ``'params.track_sarcomere_vectors.*'``: tracking parameters used. |
1684 | | - |
1685 | | - # Requires :meth:`analyze_sarcomere_vectors` to be run first. |
1686 | | - |
1687 | | - # Examples |
1688 | | - # -------- |
1689 | | - # >>> structure.analyze_sarcomere_vectors(frames='all') |
1690 | | - # >>> structure.track_sarcomere_vectors(max_distance=0.5, memory=5) |
1691 | | - # >>> track_ids = structure.data['track_ids'] |
1692 | | - # >>> # track_ids[frame][vector_idx] gives the track ID for that vector |
1693 | | - # """ |
1694 | | - # # Validate prerequisites |
1695 | | - # if 'pos_vectors_px' not in self.data: |
1696 | | - # raise ValueError( |
1697 | | - # "Sarcomere vectors not yet analyzed. Run analyze_sarcomere_vectors first." |
1698 | | - # ) |
1699 | | - |
1700 | | - # if not os.path.exists(self.file_zbands) or not os.path.exists(self.file_mbands): |
1701 | | - # raise FileNotFoundError( |
1702 | | - # "Z-band or M-band mask not found. Please run detect_sarcomeres first." |
1703 | | - # ) |
1704 | | - |
1705 | | - # # Determine frames to track |
1706 | | - # analyzed_frames = self.data.get('params.analyze_sarcomere_vectors.frames', []) |
1707 | | - # if isinstance(frames, str) and frames == 'all': |
1708 | | - # list_frames = list(range(self.metadata.n_stack)) |
1709 | | - # elif np.issubdtype(type(frames), np.integer): |
1710 | | - # list_frames = [frames] |
1711 | | - # elif isinstance(frames, (list, np.ndarray)): |
1712 | | - # list_frames = [int(f) for f in frames] |
1713 | | - # else: |
1714 | | - # raise ValueError('frames argument not valid') |
1715 | | - |
1716 | | - # # Validate frames have been analyzed |
1717 | | - # if not set(list_frames).issubset(set(analyzed_frames)): |
1718 | | - # missing = set(list_frames) - set(analyzed_frames) |
1719 | | - # raise ValueError( |
1720 | | - # f"Frames {missing} have not been analyzed. " |
1721 | | - # f"Run analyze_sarcomere_vectors first for these frames." |
1722 | | - # ) |
1723 | | - |
1724 | | - # logger.info(f"Starting sarcomere vector tracking for {len(list_frames)} frames...") |
1725 | | - |
1726 | | - # # Load masks |
1727 | | - # zbands_stack = tifffile.imread(self.file_zbands) |
1728 | | - # mbands_stack = tifffile.imread(self.file_mbands) |
1729 | | - |
1730 | | - # # Ensure 3D |
1731 | | - # if zbands_stack.ndim == 2: |
1732 | | - # zbands_stack = np.expand_dims(zbands_stack, axis=0) |
1733 | | - # if mbands_stack.ndim == 2: |
1734 | | - # mbands_stack = np.expand_dims(mbands_stack, axis=0) |
1735 | | - |
1736 | | - # # Select frames |
1737 | | - # zbands_stack = zbands_stack[list_frames] |
1738 | | - # mbands_stack = mbands_stack[list_frames] |
1739 | | - |
1740 | | - # # Prepare vector data for selected frames |
1741 | | - # positions_per_frame = [self.data['pos_vectors_px'][f] for f in list_frames] |
1742 | | - # lengths_per_frame = [self.data['sarcomere_length_vectors'][f] for f in list_frames] |
1743 | | - # orientations_per_frame = [self.data['sarcomere_orientation_vectors'][f] for f in list_frames] |
1744 | | - |
1745 | | - # # Run tracking |
1746 | | - # result = sarcomere_tracking.track_sarcomere_vectors( |
1747 | | - # zbands_stack=zbands_stack, |
1748 | | - # mbands_stack=mbands_stack, |
1749 | | - # positions_per_frame=positions_per_frame, |
1750 | | - # sarcomere_lengths_per_frame=lengths_per_frame, |
1751 | | - # orientations_per_frame=orientations_per_frame, |
1752 | | - # pixelsize=self.metadata.pixelsize, |
1753 | | - # max_distance=max_distance, |
1754 | | - # memory=memory, |
1755 | | - # min_track_length=min_track_length, |
1756 | | - # flow_blur_sigma=flow_blur_sigma, |
1757 | | - # use_hungarian=use_hungarian, |
1758 | | - # length_weight=length_weight, |
1759 | | - # return_flow=return_flow, |
1760 | | - # progress_notifier=progress_notifier |
1761 | | - # ) |
1762 | | - |
1763 | | - # if return_flow: |
1764 | | - # tracking_result, flow_fields = result |
1765 | | - # else: |
1766 | | - # tracking_result = result |
1767 | | - # flow_fields = None |
1768 | | - |
1769 | | - # # Map track IDs back to full frame indices |
1770 | | - # full_track_ids = [None] * self.metadata.n_stack |
1771 | | - # for i, frame_idx in enumerate(list_frames): |
1772 | | - # full_track_ids[frame_idx] = tracking_result.track_ids[i] |
1773 | | - |
1774 | | - # # Store results in data dict |
1775 | | - # tracking_dict = { |
1776 | | - # 'params.track_sarcomere_vectors.frames': list_frames, |
1777 | | - # 'params.track_sarcomere_vectors.max_distance': max_distance, |
1778 | | - # 'params.track_sarcomere_vectors.memory': memory, |
1779 | | - # 'params.track_sarcomere_vectors.min_track_length': min_track_length, |
1780 | | - # 'params.track_sarcomere_vectors.flow_blur_sigma': flow_blur_sigma, |
1781 | | - # 'params.track_sarcomere_vectors.use_hungarian': use_hungarian, |
1782 | | - # 'params.track_sarcomere_vectors.length_weight': length_weight, |
1783 | | - # 'track_ids': full_track_ids, |
1784 | | - # 'n_tracks': tracking_result.n_tracks, |
1785 | | - # 'track_lengths': tracking_result.track_lengths, |
1786 | | - # } |
1787 | | - # self.data.update(tracking_dict) |
1788 | | - |
1789 | | - # if self.auto_save: |
1790 | | - # self.store_structure_data() |
1791 | | - |
1792 | | - # logger.info( |
1793 | | - # f"Tracking complete: {tracking_result.n_tracks} tracks, " |
1794 | | - # f"median length {np.median(tracking_result.track_lengths) if len(tracking_result.track_lengths) > 0 else 0:.0f} frames" |
1795 | | - # ) |
1796 | | - |
1797 | | - # if return_flow: |
1798 | | - # return flow_fields |
1799 | | - # return None |
| 1618 | + def track_sarcomere_vectors( |
| 1619 | + self, |
| 1620 | + frames: Union[str, int, List[int], np.ndarray] = 'all', |
| 1621 | + max_distance: float = 0.5, |
| 1622 | + memory: int = 5, |
| 1623 | + min_track_length: int = 10, |
| 1624 | + flow_blur_sigma: Optional[float] = None, |
| 1625 | + use_hungarian: bool = True, |
| 1626 | + length_weight: float = 2.0, |
| 1627 | + return_flow: bool = False, |
| 1628 | + progress_notifier: ProgressNotifier = ProgressNotifier.progress_notifier_tqdm() |
| 1629 | + ) -> Optional[List[np.ndarray]]: |
| 1630 | + """ |
| 1631 | + Track sarcomere vectors across frames using optical flow. |
| 1632 | +
|
| 1633 | + This method uses dense optical flow computed on Z-band and M-band masks |
| 1634 | + to predict vector motion between frames, then matches predicted positions |
| 1635 | + to detected vectors using greedy exclusive assignment. |
| 1636 | +
|
| 1637 | + Parameters |
| 1638 | + ---------- |
| 1639 | + frames : {'all', int, list, np.ndarray}, optional |
| 1640 | + Frames for tracking ('all' for all frames, int for a single frame, |
| 1641 | + list or ndarray for selected frames). Defaults to 'all'. |
| 1642 | + max_distance : float, optional |
| 1643 | + Maximum matching distance in µm. Vectors predicted to be further |
| 1644 | + than this from any detection are considered lost. Default 0.5 µm. |
| 1645 | + memory : int, optional |
| 1646 | + Number of frames to keep tracks "sleeping" before termination. |
| 1647 | + Sleeping tracks can be re-linked if a matching vector reappears. |
| 1648 | + Default 5 frames. |
| 1649 | + min_track_length : int, optional |
| 1650 | + Minimum track length to keep. Shorter tracks are filtered out |
| 1651 | + and their vectors marked as untracked (track_id = -1). Default 10. |
| 1652 | + flow_blur_sigma : float, optional |
| 1653 | + Gaussian blur sigma (in pixels) to apply to masks before optical |
| 1654 | + flow computation. Can help with noisy masks. Default None (no blur). |
| 1655 | + use_hungarian : bool, optional |
| 1656 | + If True, use Hungarian algorithm for optimal matching. This gives |
| 1657 | + better results when vectors are densely packed. Default True. |
| 1658 | + length_weight : float, optional |
| 1659 | + Weight for sarcomere length difference in matching cost when using |
| 1660 | + Hungarian algorithm. Higher values favor matching vectors with |
| 1661 | + similar sarcomere lengths. Default 2.0. |
| 1662 | + return_flow : bool, optional |
| 1663 | + If True, also return the computed optical flow fields. |
| 1664 | + Default False. |
| 1665 | + progress_notifier : ProgressNotifier, optional |
| 1666 | + Progress notifier for tracking progress. |
| 1667 | +
|
| 1668 | + Returns |
| 1669 | + ------- |
| 1670 | + flow_fields : list of np.ndarray, optional |
| 1671 | + Only returned if return_flow=True. List of flow fields, |
| 1672 | + each of shape (H, W, 2). |
| 1673 | +
|
| 1674 | + Notes |
| 1675 | + ----- |
| 1676 | + Results are stored in `self.data` with the following keys: |
| 1677 | +
|
| 1678 | + - ``'track_ids'``: list of (N_t,) arrays with track ID for each vector |
| 1679 | + per frame. Track ID of -1 indicates untracked vectors. |
| 1680 | + - ``'n_tracks'``: total number of unique trajectories. |
| 1681 | + - ``'track_lengths'``: (n_tracks,) array with number of frames each |
| 1682 | + track spans. |
| 1683 | + - ``'params.track_sarcomere_vectors.*'``: tracking parameters used. |
| 1684 | +
|
| 1685 | + Requires :meth:`analyze_sarcomere_vectors` to be run first. |
| 1686 | +
|
| 1687 | + Examples |
| 1688 | + -------- |
| 1689 | + >>> structure.analyze_sarcomere_vectors(frames='all') |
| 1690 | + >>> structure.track_sarcomere_vectors(max_distance=0.5, memory=5) |
| 1691 | + >>> track_ids = structure.data['track_ids'] |
| 1692 | + >>> # track_ids[frame][vector_idx] gives the track ID for that vector |
| 1693 | + """ |
| 1694 | + # Validate prerequisites |
| 1695 | + if 'pos_vectors_px' not in self.data: |
| 1696 | + raise ValueError( |
| 1697 | + "Sarcomere vectors not yet analyzed. Run analyze_sarcomere_vectors first." |
| 1698 | + ) |
| 1699 | + |
| 1700 | + if not os.path.exists(self.file_zbands) or not os.path.exists(self.file_mbands): |
| 1701 | + raise FileNotFoundError( |
| 1702 | + "Z-band or M-band mask not found. Please run detect_sarcomeres first." |
| 1703 | + ) |
| 1704 | + |
| 1705 | + # Determine frames to track |
| 1706 | + analyzed_frames = self.data.get('params.analyze_sarcomere_vectors.frames', []) |
| 1707 | + if isinstance(frames, str) and frames == 'all': |
| 1708 | + list_frames = list(range(self.metadata.n_stack)) |
| 1709 | + elif np.issubdtype(type(frames), np.integer): |
| 1710 | + list_frames = [frames] |
| 1711 | + elif isinstance(frames, (list, np.ndarray)): |
| 1712 | + list_frames = [int(f) for f in frames] |
| 1713 | + else: |
| 1714 | + raise ValueError('frames argument not valid') |
| 1715 | + |
| 1716 | + # Validate frames have been analyzed |
| 1717 | + if not set(list_frames).issubset(set(analyzed_frames)): |
| 1718 | + missing = set(list_frames) - set(analyzed_frames) |
| 1719 | + raise ValueError( |
| 1720 | + f"Frames {missing} have not been analyzed. " |
| 1721 | + f"Run analyze_sarcomere_vectors first for these frames." |
| 1722 | + ) |
| 1723 | + |
| 1724 | + logger.info(f"Starting sarcomere vector tracking for {len(list_frames)} frames...") |
| 1725 | + |
| 1726 | + # Load masks |
| 1727 | + zbands_stack = tifffile.imread(self.file_zbands) |
| 1728 | + mbands_stack = tifffile.imread(self.file_mbands) |
| 1729 | + |
| 1730 | + # Ensure 3D |
| 1731 | + if zbands_stack.ndim == 2: |
| 1732 | + zbands_stack = np.expand_dims(zbands_stack, axis=0) |
| 1733 | + if mbands_stack.ndim == 2: |
| 1734 | + mbands_stack = np.expand_dims(mbands_stack, axis=0) |
| 1735 | + |
| 1736 | + # Select frames |
| 1737 | + zbands_stack = zbands_stack[list_frames] |
| 1738 | + mbands_stack = mbands_stack[list_frames] |
| 1739 | + |
| 1740 | + # Prepare vector data for selected frames |
| 1741 | + positions_per_frame = [self.data['pos_vectors_px'][f] for f in list_frames] |
| 1742 | + lengths_per_frame = [self.data['sarcomere_length_vectors'][f] for f in list_frames] |
| 1743 | + orientations_per_frame = [self.data['sarcomere_orientation_vectors'][f] for f in list_frames] |
| 1744 | + |
| 1745 | + # Run tracking |
| 1746 | + result = sarcomere_tracking.track_sarcomere_vectors( |
| 1747 | + zbands_stack=zbands_stack, |
| 1748 | + mbands_stack=mbands_stack, |
| 1749 | + positions_per_frame=positions_per_frame, |
| 1750 | + sarcomere_lengths_per_frame=lengths_per_frame, |
| 1751 | + orientations_per_frame=orientations_per_frame, |
| 1752 | + pixelsize=self.metadata.pixelsize, |
| 1753 | + max_distance=max_distance, |
| 1754 | + memory=memory, |
| 1755 | + min_track_length=min_track_length, |
| 1756 | + flow_blur_sigma=flow_blur_sigma, |
| 1757 | + use_hungarian=use_hungarian, |
| 1758 | + length_weight=length_weight, |
| 1759 | + return_flow=return_flow, |
| 1760 | + progress_notifier=progress_notifier |
| 1761 | + ) |
| 1762 | + |
| 1763 | + if return_flow: |
| 1764 | + tracking_result, flow_fields = result |
| 1765 | + else: |
| 1766 | + tracking_result = result |
| 1767 | + flow_fields = None |
| 1768 | + |
| 1769 | + # Map track IDs back to full frame indices |
| 1770 | + full_track_ids = [None] * self.metadata.n_stack |
| 1771 | + for i, frame_idx in enumerate(list_frames): |
| 1772 | + full_track_ids[frame_idx] = tracking_result.track_ids[i] |
| 1773 | + |
| 1774 | + # Store results in data dict |
| 1775 | + tracking_dict = { |
| 1776 | + 'params.track_sarcomere_vectors.frames': list_frames, |
| 1777 | + 'params.track_sarcomere_vectors.max_distance': max_distance, |
| 1778 | + 'params.track_sarcomere_vectors.memory': memory, |
| 1779 | + 'params.track_sarcomere_vectors.min_track_length': min_track_length, |
| 1780 | + 'params.track_sarcomere_vectors.flow_blur_sigma': flow_blur_sigma, |
| 1781 | + 'params.track_sarcomere_vectors.use_hungarian': use_hungarian, |
| 1782 | + 'params.track_sarcomere_vectors.length_weight': length_weight, |
| 1783 | + 'track_ids': full_track_ids, |
| 1784 | + 'n_tracks': tracking_result.n_tracks, |
| 1785 | + 'track_lengths': tracking_result.track_lengths, |
| 1786 | + } |
| 1787 | + self.data.update(tracking_dict) |
| 1788 | + |
| 1789 | + if self.auto_save: |
| 1790 | + self.store_structure_data() |
| 1791 | + |
| 1792 | + logger.info( |
| 1793 | + f"Tracking complete: {tracking_result.n_tracks} tracks, " |
| 1794 | + f"median length {np.median(tracking_result.track_lengths) if len(tracking_result.track_lengths) > 0 else 0:.0f} frames" |
| 1795 | + ) |
| 1796 | + |
| 1797 | + if return_flow: |
| 1798 | + return flow_fields |
| 1799 | + return None |
1800 | 1800 |
|
1801 | 1801 | def full_analysis_structure(self, frames='all'): |
1802 | 1802 | """ |
|
0 commit comments