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 +12 -0
- fucciphase/fucci_phase.py +178 -0
- fucciphase/io.py +67 -0
- fucciphase/napari/__init__.py +5 -0
- fucciphase/napari/tracks_to_napari.py +117 -0
- fucciphase/phase.py +501 -0
- fucciphase/plot.py +548 -0
- fucciphase/py.typed +5 -0
- fucciphase/sensor.py +454 -0
- fucciphase/tracking_utilities.py +81 -0
- fucciphase/utils/__init__.py +35 -0
- fucciphase/utils/checks.py +16 -0
- fucciphase/utils/dtw.py +59 -0
- fucciphase/utils/normalize.py +202 -0
- fucciphase/utils/phase_fit.py +47 -0
- fucciphase/utils/simulator.py +85 -0
- fucciphase/utils/track_postprocessing.py +454 -0
- fucciphase/utils/trackmate.py +295 -0
- fucciphase-0.0.1.dist-info/METADATA +137 -0
- fucciphase-0.0.1.dist-info/RECORD +22 -0
- fucciphase-0.0.1.dist-info/WHEEL +4 -0
- fucciphase-0.0.1.dist-info/licenses/LICENSE +29 -0
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,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)
|