fucciphase 0.0.1__py3-none-any.whl

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.
fucciphase/__init__.py ADDED
@@ -0,0 +1,12 @@
1
+ """Cell cycle analysis plugin."""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ try:
6
+ __version__ = version("fucciphase")
7
+ except PackageNotFoundError:
8
+ __version__ = "uninstalled"
9
+ __all__ = ["__version__", "logistic", "process_dataframe", "process_trackmate"]
10
+
11
+ from .fucci_phase import process_dataframe, process_trackmate
12
+ from .sensor import logistic
@@ -0,0 +1,178 @@
1
+ from pathlib import Path
2
+ from typing import List, Optional, Union
3
+
4
+ import pandas as pd
5
+
6
+ from .io import read_trackmate_xml
7
+ from .phase import generate_cycle_phases
8
+ from .sensor import FUCCISensor
9
+ from .utils import normalize_channels, split_trackmate_tracks
10
+
11
+
12
+ def process_dataframe(
13
+ df: pd.DataFrame,
14
+ channels: List[str],
15
+ sensor: FUCCISensor,
16
+ thresholds: List[float],
17
+ use_moving_average: bool = True,
18
+ window_size: int = 7,
19
+ manual_min: Optional[List[float]] = None,
20
+ manual_max: Optional[List[float]] = None,
21
+ generate_unique_tracks: bool = False,
22
+ track_id_name: str = "TRACK_ID",
23
+ label_id_name: str = "name",
24
+ estimate_percentage: bool = True,
25
+ ) -> None:
26
+ """Process a pd.DataFrame by computing the cell cycle percentage from two FUCCI
27
+ cycle reporter channels in place.
28
+
29
+ The dataframe must contain ID and TRACK_ID features.
30
+
31
+ This function applies the following steps:
32
+ - if `use_moving_average` is True, apply a Savitzky-Golay filter to each track
33
+ and each channel
34
+ - if `manual_min` and `manual_max` are None, normalize the channels globally.
35
+ Otherwise, use them to normalize each channel.
36
+ - compute the cell cycle phases and their estimated percentage
37
+
38
+ Parameters
39
+ ----------
40
+ df : pandas.DataFrame
41
+ Dataframe
42
+ channels: List[str]
43
+ Names of channels holding FUCCI information
44
+ sensor : FUCCISensor
45
+ FUCCI sensor with phase specifics
46
+ thresholds: List[float]
47
+ Thresholds to separate phases
48
+ use_moving_average : bool, optional
49
+ Use moving average before normalization, by default True
50
+ window_size : int, optional
51
+ Window size of the moving average, by default 5
52
+ manual_min : Optional[List[float]], optional
53
+ Manually determined minimum for each channel, by default None
54
+ manual_max : Optional[List[float]], optional
55
+ Manually determined maximum for each channel, by default None
56
+ estimate_percentage: bool
57
+ Estimate cell cycle percentage
58
+ label_id_name: str
59
+ Give an indentifier for the spot name (needed for unique track ID generation)
60
+ generate_unique_tracks: bool
61
+ Assign unique track IDs to splitted tracks.
62
+ Requires usage of action in TrackMate.
63
+ track_id_name: str
64
+ Name of column with track IDs
65
+ """
66
+ if len(channels) != sensor.fluorophores:
67
+ raise ValueError(f"Need to provide {sensor.fluorophores} channel names.")
68
+
69
+ if generate_unique_tracks:
70
+ if "TRACK_ID" in df.columns:
71
+ split_trackmate_tracks(df, label_id_name=label_id_name)
72
+ # perform all operation on unique tracks
73
+ track_id_name = "UNIQUE_TRACK_ID"
74
+ else:
75
+ print("Warning: unique tracks can only be prepared for TrackMate files.")
76
+ print("The tracks have not been updated.")
77
+
78
+ # normalize the channels
79
+ normalize_channels(
80
+ df,
81
+ channels,
82
+ use_moving_average=use_moving_average,
83
+ moving_average_window=window_size,
84
+ manual_min=manual_min,
85
+ manual_max=manual_max,
86
+ track_id_name=track_id_name,
87
+ )
88
+
89
+ # compute the phases
90
+ generate_cycle_phases(
91
+ df,
92
+ sensor=sensor,
93
+ channels=channels,
94
+ thresholds=thresholds,
95
+ estimate_percentage=estimate_percentage,
96
+ )
97
+
98
+
99
+ def process_trackmate(
100
+ xml_path: Union[str, Path],
101
+ channels: List[str],
102
+ sensor: FUCCISensor,
103
+ thresholds: List[float],
104
+ use_moving_average: bool = True,
105
+ window_size: int = 7,
106
+ manual_min: Optional[List[float]] = None,
107
+ manual_max: Optional[List[float]] = None,
108
+ generate_unique_tracks: bool = False,
109
+ estimate_percentage: bool = True,
110
+ ) -> pd.DataFrame:
111
+ """Process a trackmate XML file, compute cell cycle percentage from two FUCCI cycle
112
+ reporter channels, save an updated copy of the XML and return the results in a
113
+ dataframe.
114
+
115
+ This function applies the following steps:
116
+ - load the XML file and generate a dataframe from the spots and tracks
117
+ - if `use_moving_average` is True, apply a Savitzky-Golay filter to each track
118
+ and each channel
119
+ - if `manual_min` and `manual_max` are None, normalize the channels globally.
120
+ Otherwise, use them to normalize each channel.
121
+ - compute the cell cycle percentage
122
+ - save an updated XML copy with the new features
123
+
124
+ Parameters
125
+ ----------
126
+ xml_path : Union[str, Path]
127
+ Path to the XML file
128
+ channels: List[str]
129
+ Names of channels holding FUCCI information
130
+ generate_unique_tracks: bool
131
+ Assign unique track IDs to splitted tracks.
132
+ Requires usage of action in TrackMate.
133
+ sensor : FUCCISensor
134
+ FUCCI sensor with phase specifics
135
+ thresholds: List[float]
136
+ Thresholds to separate phases
137
+ use_moving_average : bool, optional
138
+ Use moving average before normalization, by default True
139
+ window_size : int, optional
140
+ Window size of the moving average, by default 5
141
+ manual_min : Optional[List[float]], optional
142
+ Manually determined minimum for each channel, by default None
143
+ manual_max : Optional[List[float]], optional
144
+ Manually determined maximum for each channel, by default None
145
+ estimate_percentage: bool, optional
146
+ Estimate cell cycle percentage
147
+
148
+ Returns
149
+ -------
150
+ pd.DataFrame
151
+ Dataframe with the cell cycle percentage and the corresponding phases
152
+ """
153
+ # read the XML
154
+ df, tmxml = read_trackmate_xml(xml_path)
155
+
156
+ # process the dataframe
157
+ process_dataframe(
158
+ df,
159
+ channels,
160
+ sensor,
161
+ thresholds,
162
+ use_moving_average=use_moving_average,
163
+ window_size=window_size,
164
+ manual_min=manual_min,
165
+ manual_max=manual_max,
166
+ generate_unique_tracks=generate_unique_tracks,
167
+ estimate_percentage=estimate_percentage,
168
+ )
169
+
170
+ # update the XML
171
+ tmxml.update_features(df)
172
+
173
+ # export the xml
174
+ new_name = Path(xml_path).stem + "_processed.xml"
175
+ new_path = Path(xml_path).parent / new_name
176
+ tmxml.save_xml(new_path)
177
+
178
+ return df
fucciphase/io.py ADDED
@@ -0,0 +1,67 @@
1
+ from pathlib import Path
2
+ from typing import Tuple, Union
3
+
4
+ import pandas as pd
5
+
6
+ from .utils import TrackMateXML
7
+
8
+
9
+ def read_trackmate_xml(xml_path: Union[Path, str]) -> Tuple[pd.DataFrame, TrackMateXML]:
10
+ """Read a trackmate exported xml file.
11
+
12
+ Parameters
13
+ ----------
14
+ xml_path : Union[Path, str]
15
+ Path to the xml file.
16
+
17
+ Returns
18
+ -------
19
+ df : pandas.DataFrame
20
+ Dataframe containing the xml data.
21
+ trackmate : TrackMateXML
22
+ TrackMateXML object.
23
+ """
24
+ # read in the xml file
25
+ trackmate = TrackMateXML(xml_path)
26
+
27
+ # convert the spots to a dataframe
28
+ df = trackmate.to_pandas()
29
+ # sort by frame number to have increasing time
30
+ df.sort_values(by="FRAME")
31
+
32
+ return df, trackmate
33
+
34
+
35
+ def read_trackmate_csv(csv_path: Union[Path, str]) -> pd.DataFrame:
36
+ """Read a trackmate exported csv file.
37
+
38
+ The first three rows (excluding header) of the csv file are skipped as
39
+ they contain duplicate titles of columns and units (Trackmate specific).
40
+
41
+
42
+ Parameters
43
+ ----------
44
+ csv_path : str
45
+ Path to the csv file.
46
+
47
+ Returns
48
+ -------
49
+ df : pandas.DataFrame
50
+ Dataframe containing the csv data.
51
+
52
+ Raises
53
+ ------
54
+ ValueError
55
+ If the csv file does not contain at least two channels.
56
+ """
57
+ df = pd.read_csv(csv_path, encoding="unicode_escape", skiprows=[1, 2, 3])
58
+
59
+ # sanity check: trackmate must have at least two channels
60
+ if (
61
+ "MEAN_INTENSITY_CH1" not in df.columns
62
+ and "MEAN_INTENSITY_CH2" not in df.columns
63
+ ):
64
+ raise ValueError("Trackmate must have at least two channels.")
65
+
66
+ # return dataframe with converted types (object -> string)
67
+ return df.convert_dtypes()
@@ -0,0 +1,5 @@
1
+ """Napari convenience functions."""
2
+
3
+ from .tracks_to_napari import add_trackmate_data_to_viewer, pandas_df_to_napari_tracks
4
+
5
+ __all__ = ["add_trackmate_data_to_viewer", "pandas_df_to_napari_tracks"]
@@ -0,0 +1,117 @@
1
+ from typing import List, Optional
2
+
3
+ import numpy as np
4
+ import pandas as pd
5
+
6
+ HAS_NAPARI = True
7
+ try:
8
+ import napari
9
+ except ImportError:
10
+ HAS_NAPARI = False
11
+ pass
12
+
13
+
14
+ def add_trackmate_data_to_viewer(
15
+ df: pd.DataFrame,
16
+ viewer: napari.Viewer,
17
+ scale: tuple,
18
+ image_data: List[np.ndarray],
19
+ colormaps: List[str],
20
+ labels: Optional[np.ndarray],
21
+ cycle_percentage_id: Optional[str] = "CELL_CYCLE_PERC_POST",
22
+ dim: int = 2,
23
+ textkwargs: Optional[dict] = None,
24
+ label_id_name: Optional[str] = "MAX_INTENSITY_CH3",
25
+ track_id_name: Optional[str] = "TRACK_ID",
26
+ time_id_name: Optional[str] = "POSITION_T",
27
+ pos_x_id_name: Optional[str] = "POSITION_X",
28
+ pos_y_id_name: Optional[str] = "POSITION_Y",
29
+ crop_fov: Optional[List[float]] = None,
30
+ ) -> None:
31
+ """Overlay tracking result and video.
32
+
33
+ df: pd.DataFrame
34
+ TrackMate result processed by fucciphase
35
+ viewer: napari.Viewer
36
+ Viewer instance
37
+ scale: tuple
38
+ Pixel sizes as tuple of size dim
39
+ image_data: np.ndarray
40
+ List of image arrays
41
+ colormaps: List[str]
42
+ List of colormaps for each image channel
43
+ labels: Optional[np.ndarray]
44
+ Segmentation masks
45
+ textkwargs: dict
46
+ Dictionary to pass options to text in napari
47
+ crop_fov: List[List[float], List[float]]
48
+ Crop the masks and points
49
+ """
50
+ if textkwargs is None:
51
+ textkwargs = {}
52
+ if not HAS_NAPARI:
53
+ raise ImportError("Please install napari")
54
+ if dim != 2:
55
+ raise NotImplementedError("Workflow currently only implemented for 2D frames.")
56
+ # make sure it is sorted
57
+ napari_val_df = df.sort_values(time_id_name)
58
+ # extract points
59
+ points = napari_val_df[[time_id_name, pos_y_id_name, pos_x_id_name]].to_numpy()
60
+ # extract percentages at points
61
+ # TODO insert checks
62
+ percentage_values = napari_val_df[cycle_percentage_id].to_numpy()
63
+ if labels is not None:
64
+ new_labels = np.zeros(shape=labels.shape, dtype=labels.dtype)
65
+ # add labels to each frame
66
+ for i in range(round(df[time_id_name].max()) + 1):
67
+ subdf = df[np.isclose(df[time_id_name], i)]
68
+ label_ids = subdf[label_id_name]
69
+ track_ids = subdf[track_id_name]
70
+ for idx, label_id in enumerate(label_ids):
71
+ # add 1 because TRACK_ID 0 would be background
72
+ new_labels[i][np.isclose(labels[i], label_id)] = track_ids.iloc[idx] + 1
73
+ labels_layer = viewer.add_labels(new_labels, scale=scale)
74
+ labels_layer.contour = 10
75
+
76
+ for image, colormap in zip(image_data, colormaps):
77
+ viewer.add_image(image, blending="additive", colormap=colormap, scale=scale)
78
+ # TODO implement cropping, filter points in / outside range
79
+ # crop_fov =
80
+ viewer.add_points(
81
+ points,
82
+ features={"percentage": np.round(percentage_values, 1)},
83
+ text={"string": "{percentage}%", "color": "white", **textkwargs},
84
+ size=0.01,
85
+ )
86
+ viewer.scale_bar.visible = True
87
+ viewer.scale_bar.unit = "um"
88
+ return
89
+
90
+
91
+ def pandas_df_to_napari_tracks(
92
+ df: pd.DataFrame,
93
+ viewer: napari.Viewer,
94
+ unique_track_id_name: str,
95
+ frame_id_name: str,
96
+ position_x_name: str,
97
+ position_y_name: str,
98
+ feature_name: Optional[str] = None,
99
+ colormaps_dict: Optional[dict] = None,
100
+ ) -> None:
101
+ """Add tracks to Napari track layer.
102
+ Splitting and merging are not yet implemented.
103
+ """
104
+ # filter for valid tracks only
105
+ track_df = df[df[unique_track_id_name] > 0]
106
+ track_data = track_df[
107
+ [unique_track_id_name, frame_id_name, position_y_name, position_x_name]
108
+ ].to_numpy()
109
+ if track_df[feature_name].min() < 0 or track_df[feature_name].max() > 100.0:
110
+ raise ValueError(
111
+ "Make sure that the features are between 0 and 1, "
112
+ "otherwise the colormapping does not work well"
113
+ )
114
+ features = None
115
+ if feature_name is not None:
116
+ features = {feature_name: track_df[feature_name].to_numpy()}
117
+ viewer.add_tracks(track_data, features=features, colormaps_dict=colormaps_dict)