fucciphase 0.0.2__py3-none-any.whl → 0.0.4__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 +7 -1
- fucciphase/__main__.py +12 -0
- fucciphase/fucci_phase.py +123 -53
- fucciphase/io.py +18 -17
- fucciphase/main_cli.py +151 -34
- fucciphase/napari/tracks_to_napari.py +20 -21
- fucciphase/phase.py +350 -137
- fucciphase/plot.py +240 -88
- fucciphase/sensor.py +47 -33
- fucciphase/tracking_utilities.py +70 -9
- fucciphase/utils/__init__.py +14 -1
- fucciphase/utils/checks.py +2 -5
- fucciphase/utils/dtw.py +2 -4
- fucciphase/utils/normalize.py +46 -12
- fucciphase/utils/phase_fit.py +11 -7
- fucciphase/utils/simulator.py +1 -1
- fucciphase/utils/track_postprocessing.py +16 -11
- fucciphase/utils/trackmate.py +30 -13
- fucciphase-0.0.4.dist-info/METADATA +238 -0
- fucciphase-0.0.4.dist-info/RECORD +25 -0
- {fucciphase-0.0.2.dist-info → fucciphase-0.0.4.dist-info}/WHEEL +1 -1
- fucciphase-0.0.2.dist-info/METADATA +0 -137
- fucciphase-0.0.2.dist-info/RECORD +0 -24
- {fucciphase-0.0.2.dist-info → fucciphase-0.0.4.dist-info}/entry_points.txt +0 -0
- {fucciphase-0.0.2.dist-info → fucciphase-0.0.4.dist-info}/licenses/LICENSE +0 -0
fucciphase/__init__.py
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""
|
|
2
|
+
FUCCIphase: Analysis tools for cell-cycle estimation from FUCCI imaging.
|
|
3
|
+
|
|
4
|
+
This module exposes the main public API of the package, including the
|
|
5
|
+
core processing functions and the package version.
|
|
6
|
+
|
|
7
|
+
"""
|
|
2
8
|
|
|
3
9
|
from importlib.metadata import PackageNotFoundError, version
|
|
4
10
|
|
fucciphase/__main__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module entry point for ``python -m fucciphase``.
|
|
3
|
+
|
|
4
|
+
This thin wrapper forwards execution to :func:`fucciphase.main_cli.main_cli`,
|
|
5
|
+
which implements the command-line interface used by the ``fucciphase``
|
|
6
|
+
console script.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from .main_cli import main_cli
|
|
10
|
+
|
|
11
|
+
if __name__ == "__main__":
|
|
12
|
+
main_cli()
|
fucciphase/fucci_phase.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import logging
|
|
1
2
|
from pathlib import Path
|
|
2
|
-
from typing import List, Optional, Union
|
|
3
3
|
|
|
4
4
|
import pandas as pd
|
|
5
5
|
|
|
@@ -8,23 +8,33 @@ from .phase import generate_cycle_phases
|
|
|
8
8
|
from .sensor import FUCCISensor
|
|
9
9
|
from .utils import normalize_channels, split_trackmate_tracks
|
|
10
10
|
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
11
13
|
|
|
12
14
|
def process_dataframe(
|
|
13
15
|
df: pd.DataFrame,
|
|
14
|
-
channels:
|
|
16
|
+
channels: list[str],
|
|
15
17
|
sensor: FUCCISensor,
|
|
16
|
-
thresholds:
|
|
18
|
+
thresholds: list[float],
|
|
17
19
|
use_moving_average: bool = True,
|
|
18
20
|
window_size: int = 7,
|
|
19
|
-
manual_min:
|
|
20
|
-
manual_max:
|
|
21
|
+
manual_min: list[float] | None = None,
|
|
22
|
+
manual_max: list[float] | None = None,
|
|
21
23
|
generate_unique_tracks: bool = False,
|
|
22
24
|
track_id_name: str = "TRACK_ID",
|
|
23
25
|
label_id_name: str = "name",
|
|
24
26
|
estimate_percentage: bool = True,
|
|
25
27
|
) -> None:
|
|
26
|
-
"""
|
|
27
|
-
|
|
28
|
+
"""Apply the FUCCIphase analysis pipeline to an existing dataframe.
|
|
29
|
+
|
|
30
|
+
This function assumes that tracking and fluorescence information are
|
|
31
|
+
already available in a pandas DataFrame with the expected column
|
|
32
|
+
structure. It performs the same core steps as ``process_trackmate``,
|
|
33
|
+
but skips the TrackMate file I/O and starts directly from tabular data.
|
|
34
|
+
|
|
35
|
+
Use this when your tracking pipeline already provides a dataframe, or
|
|
36
|
+
when you have manually assembled the input table and still want to use
|
|
37
|
+
FUCCIphase for cell-cycle analysis and visualization.
|
|
28
38
|
|
|
29
39
|
The dataframe must contain ID and TRACK_ID features.
|
|
30
40
|
|
|
@@ -38,42 +48,82 @@ def process_dataframe(
|
|
|
38
48
|
Parameters
|
|
39
49
|
----------
|
|
40
50
|
df : pandas.DataFrame
|
|
41
|
-
|
|
51
|
+
Input dataframe containing tracking and intensity features.
|
|
52
|
+
|
|
42
53
|
channels: List[str]
|
|
43
|
-
|
|
54
|
+
Names of columns holding FUCCI fluorescence information.
|
|
55
|
+
|
|
44
56
|
sensor : FUCCISensor
|
|
45
|
-
FUCCI sensor with phase
|
|
57
|
+
FUCCI sensor with phase-specific parameters.
|
|
58
|
+
|
|
46
59
|
thresholds: List[float]
|
|
47
|
-
Thresholds to separate phases
|
|
60
|
+
Thresholds used to separate cell-cycle phases.
|
|
61
|
+
|
|
48
62
|
use_moving_average : bool, optional
|
|
49
|
-
|
|
63
|
+
If True, apply a moving average before normalization. Default is True.
|
|
64
|
+
|
|
50
65
|
window_size : int, optional
|
|
51
|
-
Window size of the moving average
|
|
66
|
+
Window size of the moving average. Default is 7.
|
|
67
|
+
|
|
52
68
|
manual_min : Optional[List[float]], optional
|
|
53
69
|
Manually determined minimum for each channel, by default None
|
|
70
|
+
|
|
54
71
|
manual_max : Optional[List[float]], optional
|
|
55
72
|
Manually determined maximum for each channel, by default None
|
|
56
|
-
|
|
57
|
-
Estimate cell cycle percentage
|
|
58
|
-
label_id_name: str
|
|
59
|
-
Give an indentifier for the spot name (needed for unique track ID generation)
|
|
73
|
+
|
|
60
74
|
generate_unique_tracks: bool
|
|
61
|
-
|
|
62
|
-
|
|
75
|
+
If True, assign unique track IDs to split tracks. This requires
|
|
76
|
+
using the appropriate action in TrackMate. Default is False.
|
|
77
|
+
|
|
63
78
|
track_id_name: str
|
|
64
|
-
Name of column
|
|
79
|
+
Name of the column containing track IDs. Default is ``"TRACK_ID"``.
|
|
80
|
+
|
|
81
|
+
label_id_name: str
|
|
82
|
+
Column name identifying the spot label / name (used for unique
|
|
83
|
+
track ID generation). Default is ``"name"``.
|
|
84
|
+
|
|
85
|
+
estimate_percentage: bool, optional
|
|
86
|
+
If True, estimate cell-cycle percentage along each track. Default is True.
|
|
87
|
+
|
|
88
|
+
Returns
|
|
89
|
+
-------
|
|
90
|
+
None
|
|
91
|
+
The input dataframe is modified in-place. No value is returned.
|
|
65
92
|
"""
|
|
93
|
+
# ensure that the number of provided channels matches the sensor definition
|
|
66
94
|
if len(channels) != sensor.fluorophores:
|
|
67
95
|
raise ValueError(f"Need to provide {sensor.fluorophores} channel names.")
|
|
68
96
|
|
|
97
|
+
# validate DataFrame is not empty
|
|
98
|
+
if df.empty:
|
|
99
|
+
raise ValueError("Input DataFrame is empty.")
|
|
100
|
+
|
|
101
|
+
# validate that required channel columns exist
|
|
102
|
+
missing_channels = [ch for ch in channels if ch not in df.columns]
|
|
103
|
+
if missing_channels:
|
|
104
|
+
raise ValueError(
|
|
105
|
+
f"Missing channel columns in DataFrame: {missing_channels}. "
|
|
106
|
+
f"Available columns: {list(df.columns)}"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# validate that FRAME column exists (required for processing)
|
|
110
|
+
if "FRAME" not in df.columns:
|
|
111
|
+
raise ValueError(
|
|
112
|
+
"Missing required 'FRAME' column in DataFrame. "
|
|
113
|
+
f"Available columns: {list(df.columns)}"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# optionally split TrackMate subtracks and re-label them as unique tracks
|
|
69
117
|
if generate_unique_tracks:
|
|
70
118
|
if "TRACK_ID" in df.columns:
|
|
71
119
|
split_trackmate_tracks(df, label_id_name=label_id_name)
|
|
72
120
|
# perform all operation on unique tracks
|
|
73
121
|
track_id_name = "UNIQUE_TRACK_ID"
|
|
74
122
|
else:
|
|
75
|
-
|
|
76
|
-
|
|
123
|
+
logger.warning(
|
|
124
|
+
"Unique tracks can only be prepared for TrackMate files. "
|
|
125
|
+
"The tracks have not been updated."
|
|
126
|
+
)
|
|
77
127
|
|
|
78
128
|
# normalize the channels
|
|
79
129
|
normalize_channels(
|
|
@@ -86,7 +136,7 @@ def process_dataframe(
|
|
|
86
136
|
track_id_name=track_id_name,
|
|
87
137
|
)
|
|
88
138
|
|
|
89
|
-
# compute the phases
|
|
139
|
+
# compute the phases (and, optionally, the cycle percentage)
|
|
90
140
|
generate_cycle_phases(
|
|
91
141
|
df,
|
|
92
142
|
sensor=sensor,
|
|
@@ -97,20 +147,29 @@ def process_dataframe(
|
|
|
97
147
|
|
|
98
148
|
|
|
99
149
|
def process_trackmate(
|
|
100
|
-
xml_path:
|
|
101
|
-
channels:
|
|
150
|
+
xml_path: str | Path,
|
|
151
|
+
channels: list[str],
|
|
102
152
|
sensor: FUCCISensor,
|
|
103
|
-
thresholds:
|
|
153
|
+
thresholds: list[float],
|
|
104
154
|
use_moving_average: bool = True,
|
|
105
155
|
window_size: int = 7,
|
|
106
|
-
manual_min:
|
|
107
|
-
manual_max:
|
|
156
|
+
manual_min: list[float] | None = None,
|
|
157
|
+
manual_max: list[float] | None = None,
|
|
108
158
|
generate_unique_tracks: bool = False,
|
|
109
159
|
estimate_percentage: bool = True,
|
|
160
|
+
output_dir: str | Path | None = None,
|
|
110
161
|
) -> pd.DataFrame:
|
|
111
|
-
"""
|
|
112
|
-
|
|
113
|
-
|
|
162
|
+
"""Run the full FUCCIphase pipeline on a TrackMate export.
|
|
163
|
+
|
|
164
|
+
This high-level helper takes tracking data exported from Fiji/TrackMate
|
|
165
|
+
(typically XML or CSV), converts it into a pandas DataFrame with the
|
|
166
|
+
expected fucciphase columns, applies basic quality checks and
|
|
167
|
+
preprocessing, and estimates cell-cycle phase information that can be
|
|
168
|
+
used for downstream analysis and plotting.
|
|
169
|
+
|
|
170
|
+
The returned table is intended to be the main entry point for
|
|
171
|
+
fucciphase workflows, and is compatible with the plotting and
|
|
172
|
+
visualization functions provided in this package.
|
|
114
173
|
|
|
115
174
|
This function applies the following steps:
|
|
116
175
|
- load the XML file and generate a dataframe from the spots and tracks
|
|
@@ -124,36 +183,40 @@ def process_trackmate(
|
|
|
124
183
|
Parameters
|
|
125
184
|
----------
|
|
126
185
|
xml_path : Union[str, Path]
|
|
127
|
-
Path to the XML file
|
|
128
|
-
channels: List[str]
|
|
129
|
-
Names of
|
|
130
|
-
generate_unique_tracks: bool
|
|
131
|
-
Assign unique track IDs to splitted tracks.
|
|
132
|
-
Requires usage of action in TrackMate.
|
|
186
|
+
Path to the TrackMate XML file.
|
|
187
|
+
channels : List[str]
|
|
188
|
+
Names of columns holding FUCCI fluorescence information.
|
|
133
189
|
sensor : FUCCISensor
|
|
134
|
-
FUCCI sensor with phase
|
|
135
|
-
thresholds: List[float]
|
|
136
|
-
Thresholds to separate phases
|
|
190
|
+
FUCCI sensor with phase-specific parameters.
|
|
191
|
+
thresholds : List[float]
|
|
192
|
+
Thresholds used to separate cell-cycle phases.
|
|
137
193
|
use_moving_average : bool, optional
|
|
138
|
-
|
|
194
|
+
If True, apply a moving average before normalization. Default is True.
|
|
139
195
|
window_size : int, optional
|
|
140
|
-
Window size of the moving average
|
|
196
|
+
Window size of the moving average. Default is 7.
|
|
141
197
|
manual_min : Optional[List[float]], optional
|
|
142
|
-
Manually determined minimum for each channel, by default None
|
|
198
|
+
Manually determined minimum for each channel, by default None.
|
|
143
199
|
manual_max : Optional[List[float]], optional
|
|
144
|
-
Manually determined maximum for each channel, by default None
|
|
145
|
-
|
|
146
|
-
|
|
200
|
+
Manually determined maximum for each channel, by default None.
|
|
201
|
+
generate_unique_tracks : bool, optional
|
|
202
|
+
If True, assign unique track IDs to split tracks. This requires
|
|
203
|
+
using the appropriate action in TrackMate. Default is False.
|
|
204
|
+
estimate_percentage : bool, optional
|
|
205
|
+
If True, estimate cell-cycle percentage along each track. Default is True.
|
|
206
|
+
output_dir : Optional[Union[str, Path]], optional
|
|
207
|
+
Optional directory where the updated XML should be written. If None,
|
|
208
|
+
the file is saved next to the input XML.
|
|
147
209
|
|
|
148
210
|
Returns
|
|
149
211
|
-------
|
|
150
|
-
|
|
151
|
-
Dataframe with the cell
|
|
212
|
+
pandas.DataFrame
|
|
213
|
+
Dataframe with the cell-cycle percentage and the corresponding phases.
|
|
214
|
+
|
|
152
215
|
"""
|
|
153
|
-
# read the XML
|
|
216
|
+
# read the XML and extract the dataframe and XML wrapper
|
|
154
217
|
df, tmxml = read_trackmate_xml(xml_path)
|
|
155
218
|
|
|
156
|
-
# process the dataframe
|
|
219
|
+
# process the dataframe in-place (and also get a reference to it)
|
|
157
220
|
process_dataframe(
|
|
158
221
|
df,
|
|
159
222
|
channels,
|
|
@@ -167,12 +230,19 @@ def process_trackmate(
|
|
|
167
230
|
estimate_percentage=estimate_percentage,
|
|
168
231
|
)
|
|
169
232
|
|
|
170
|
-
# update the XML
|
|
233
|
+
# update the XML with the new features
|
|
171
234
|
tmxml.update_features(df)
|
|
172
235
|
|
|
173
|
-
# export the
|
|
236
|
+
# export the updated XML next to the original file
|
|
174
237
|
new_name = Path(xml_path).stem + "_processed.xml"
|
|
175
|
-
|
|
238
|
+
|
|
239
|
+
if output_dir is not None:
|
|
240
|
+
output_dir = Path(output_dir)
|
|
241
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
242
|
+
new_path = output_dir / new_name
|
|
243
|
+
else:
|
|
244
|
+
new_path = Path(xml_path).parent / new_name
|
|
245
|
+
|
|
176
246
|
tmxml.save_xml(new_path)
|
|
177
247
|
|
|
178
248
|
return df
|
fucciphase/io.py
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
|
-
from typing import Tuple, Union
|
|
3
2
|
|
|
4
3
|
import pandas as pd
|
|
5
4
|
|
|
6
5
|
from .utils import TrackMateXML
|
|
7
6
|
|
|
8
7
|
|
|
9
|
-
def read_trackmate_xml(xml_path:
|
|
10
|
-
"""Read a
|
|
8
|
+
def read_trackmate_xml(xml_path: Path | str) -> tuple[pd.DataFrame, TrackMateXML]:
|
|
9
|
+
"""Read a TrackMate-exported XML file and return data and XML wrapper.
|
|
11
10
|
|
|
12
11
|
Parameters
|
|
13
12
|
----------
|
|
14
13
|
xml_path : Union[Path, str]
|
|
15
|
-
Path to the
|
|
14
|
+
Path to the XML file.
|
|
16
15
|
|
|
17
16
|
Returns
|
|
18
17
|
-------
|
|
19
18
|
df : pandas.DataFrame
|
|
20
|
-
Dataframe containing the
|
|
19
|
+
Dataframe containing the spot and track data, sorted by FRAME.
|
|
21
20
|
trackmate : TrackMateXML
|
|
22
|
-
TrackMateXML object
|
|
21
|
+
TrackMateXML object wrapping the original XML and allowing
|
|
22
|
+
feature updates / re-export.
|
|
23
23
|
"""
|
|
24
24
|
# read in the xml file
|
|
25
25
|
trackmate = TrackMateXML(xml_path)
|
|
@@ -27,40 +27,41 @@ def read_trackmate_xml(xml_path: Union[Path, str]) -> Tuple[pd.DataFrame, TrackM
|
|
|
27
27
|
# convert the spots to a dataframe
|
|
28
28
|
df = trackmate.to_pandas()
|
|
29
29
|
# sort by frame number to have increasing time
|
|
30
|
-
df.sort_values(by="FRAME")
|
|
30
|
+
df = df.sort_values(by="FRAME")
|
|
31
31
|
|
|
32
32
|
return df, trackmate
|
|
33
33
|
|
|
34
34
|
|
|
35
|
-
def read_trackmate_csv(csv_path:
|
|
36
|
-
"""Read a
|
|
35
|
+
def read_trackmate_csv(csv_path: Path | str) -> pd.DataFrame:
|
|
36
|
+
"""Read a TrackMate-exported CSV file.
|
|
37
37
|
|
|
38
38
|
The first three rows (excluding header) of the csv file are skipped as
|
|
39
39
|
they contain duplicate titles of columns and units (Trackmate specific).
|
|
40
40
|
|
|
41
|
+
The first three rows (excluding the header) of the CSV file are
|
|
42
|
+
skipped as they contain duplicate column titles and units
|
|
43
|
+
(TrackMate-specific).
|
|
41
44
|
|
|
42
45
|
Parameters
|
|
43
46
|
----------
|
|
44
|
-
csv_path : str
|
|
45
|
-
Path to the
|
|
47
|
+
csv_path : Union[Path, str]
|
|
48
|
+
Path to the CSV file.
|
|
46
49
|
|
|
47
50
|
Returns
|
|
48
51
|
-------
|
|
49
52
|
df : pandas.DataFrame
|
|
50
|
-
Dataframe containing the
|
|
53
|
+
Dataframe containing the CSV data with converted dtypes.
|
|
51
54
|
|
|
52
55
|
Raises
|
|
53
56
|
------
|
|
54
57
|
ValueError
|
|
55
|
-
If the
|
|
58
|
+
If the CSV file does not contain both MEAN_INTENSITY_CH1 and
|
|
59
|
+
MEAN_INTENSITY_CH2 columns.
|
|
56
60
|
"""
|
|
57
61
|
df = pd.read_csv(csv_path, encoding="unicode_escape", skiprows=[1, 2, 3])
|
|
58
62
|
|
|
59
63
|
# 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
|
+
if "MEAN_INTENSITY_CH1" not in df.columns or "MEAN_INTENSITY_CH2" not in df.columns:
|
|
64
65
|
raise ValueError("Trackmate must have at least two channels.")
|
|
65
66
|
|
|
66
67
|
# return dataframe with converted types (object -> string)
|
fucciphase/main_cli.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import argparse
|
|
2
2
|
import json
|
|
3
|
+
import logging
|
|
4
|
+
from pathlib import Path
|
|
3
5
|
|
|
4
6
|
import pandas as pd
|
|
5
7
|
|
|
@@ -8,19 +10,47 @@ from fucciphase.napari import add_trackmate_data_to_viewer
|
|
|
8
10
|
from fucciphase.phase import estimate_percentage_by_subsequence_alignment
|
|
9
11
|
from fucciphase.sensor import FUCCISASensor, get_fuccisa_default_sensor
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
import napari
|
|
13
|
-
except ImportError as err:
|
|
14
|
-
raise ImportError("Install napari.") from err
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
15
14
|
|
|
16
15
|
|
|
16
|
+
# ruff: noqa: C901
|
|
17
17
|
def main_cli() -> None:
|
|
18
|
-
"""Fucciphase CLI.
|
|
18
|
+
"""Fucciphase CLI: Command-line entry point for FUCCIphase.
|
|
19
|
+
|
|
20
|
+
This function is invoked by the ``fucciphase`` console script and
|
|
21
|
+
implements the standard command-line workflow:
|
|
22
|
+
|
|
23
|
+
1. Parse command-line arguments describing:
|
|
24
|
+
- a TrackMate tracking file (XML or CSV),
|
|
25
|
+
- a reference cell-cycle trace in CSV format,
|
|
26
|
+
- an optional FUCCI sensor JSON file,
|
|
27
|
+
- the acquisition timestep and channel names.
|
|
28
|
+
2. Load the reference data and rename its fluorescence columns to
|
|
29
|
+
match the user-specified channel names.
|
|
30
|
+
3. Load and preprocess the tracking data using either
|
|
31
|
+
:func:`process_trackmate` (for XML) or
|
|
32
|
+
:func:`process_dataframe` (for CSV).
|
|
33
|
+
4. Estimate cell-cycle percentages for each track by subsequence
|
|
34
|
+
alignment against the reference trace.
|
|
35
|
+
5. Write the processed table to ``<tracking_file>_processed.csv`` in
|
|
36
|
+
the same directory as the input file.
|
|
37
|
+
|
|
38
|
+
The function is designed to be used from the command line and does
|
|
39
|
+
not return a value. It will raise a ``ValueError`` if the tracking
|
|
40
|
+
file does not have an XML or CSV extension.
|
|
41
|
+
"""
|
|
42
|
+
logging.basicConfig(
|
|
43
|
+
level=logging.INFO,
|
|
44
|
+
format="%(levelname)s - %(name)s - %(message)s",
|
|
45
|
+
)
|
|
46
|
+
|
|
19
47
|
parser = argparse.ArgumentParser(
|
|
20
48
|
prog="fucciphase",
|
|
21
49
|
description="FUCCIphase tool to estimate cell cycle phases and percentages.",
|
|
22
50
|
epilog="Please report bugs and errors on GitHub.",
|
|
23
51
|
)
|
|
52
|
+
|
|
53
|
+
# -------------- 1. Parse command-line arguments --------------
|
|
24
54
|
parser.add_argument("tracking_file", type=str, help="TrackMate XML or CSV file")
|
|
25
55
|
parser.add_argument(
|
|
26
56
|
"-ref",
|
|
@@ -55,36 +85,78 @@ def main_cli() -> None:
|
|
|
55
85
|
)
|
|
56
86
|
parser.add_argument(
|
|
57
87
|
"--generate_unique_tracks",
|
|
58
|
-
|
|
88
|
+
action="store_true",
|
|
59
89
|
help="Split subtracks (TrackMate specific)",
|
|
60
|
-
default=False,
|
|
61
90
|
)
|
|
62
91
|
|
|
63
92
|
args = parser.parse_args()
|
|
93
|
+
# Decide where to store outputs (CSV and, for XML input, processed XML)
|
|
94
|
+
output_dir = Path("outputs")
|
|
95
|
+
output_dir.mkdir(exist_ok=True)
|
|
96
|
+
|
|
97
|
+
# ---------------- 2. Load and adapt the reference cell-cycle trace ----------------
|
|
98
|
+
try:
|
|
99
|
+
reference_df = pd.read_csv(args.reference_file)
|
|
100
|
+
except FileNotFoundError:
|
|
101
|
+
raise FileNotFoundError(
|
|
102
|
+
f"Reference file not found: {args.reference_file}"
|
|
103
|
+
) from None
|
|
104
|
+
except pd.errors.EmptyDataError:
|
|
105
|
+
raise ValueError(f"Reference file is empty: {args.reference_file}") from None
|
|
106
|
+
except pd.errors.ParserError as e:
|
|
107
|
+
raise ValueError(
|
|
108
|
+
f"Failed to parse reference file {args.reference_file}: {e}"
|
|
109
|
+
) from None
|
|
64
110
|
|
|
65
|
-
|
|
111
|
+
# The reference file is expected to contain 'cyan' and 'magenta' columns;
|
|
112
|
+
# they are renamed here to match the actual channel names in the data.
|
|
66
113
|
reference_df.rename(
|
|
67
114
|
columns={"cyan": args.cyan_channel, "magenta": args.magenta_channel},
|
|
68
115
|
inplace=True,
|
|
69
116
|
)
|
|
70
117
|
|
|
118
|
+
# ---------------- 3. Build the sensor model ----------------
|
|
71
119
|
if args.sensor_file is not None:
|
|
72
|
-
|
|
73
|
-
|
|
120
|
+
try:
|
|
121
|
+
with open(args.sensor_file) as fp:
|
|
122
|
+
sensor_properties = json.load(fp)
|
|
123
|
+
except FileNotFoundError:
|
|
124
|
+
raise FileNotFoundError(
|
|
125
|
+
f"Sensor file not found: {args.sensor_file}"
|
|
126
|
+
) from None
|
|
127
|
+
except json.JSONDecodeError as e:
|
|
128
|
+
raise ValueError(
|
|
129
|
+
f"Invalid JSON in sensor file {args.sensor_file}: {e}"
|
|
130
|
+
) from None
|
|
74
131
|
sensor = FUCCISASensor(**sensor_properties)
|
|
75
132
|
else:
|
|
76
133
|
sensor = get_fuccisa_default_sensor()
|
|
77
134
|
|
|
135
|
+
# ---------------- 4. Load and preprocess the tracking data ----------------
|
|
78
136
|
if args.tracking_file.endswith(".xml"):
|
|
137
|
+
# XML: let process_trackmate handle I/O and preprocessing
|
|
79
138
|
df = process_trackmate(
|
|
80
139
|
args.tracking_file,
|
|
81
140
|
channels=[args.cyan_channel, args.magenta_channel],
|
|
82
141
|
sensor=sensor,
|
|
83
142
|
thresholds=[0.1, 0.1],
|
|
84
143
|
generate_unique_tracks=args.generate_unique_tracks,
|
|
144
|
+
output_dir=output_dir,
|
|
85
145
|
)
|
|
86
146
|
elif args.tracking_file.endswith(".csv"):
|
|
87
|
-
|
|
147
|
+
# CSV: read the table and then run the processing pipeline on it
|
|
148
|
+
try:
|
|
149
|
+
df = pd.read_csv(args.tracking_file)
|
|
150
|
+
except FileNotFoundError:
|
|
151
|
+
raise FileNotFoundError(
|
|
152
|
+
f"Tracking file not found: {args.tracking_file}"
|
|
153
|
+
) from None
|
|
154
|
+
except pd.errors.EmptyDataError:
|
|
155
|
+
raise ValueError(f"Tracking file is empty: {args.tracking_file}") from None
|
|
156
|
+
except pd.errors.ParserError as e:
|
|
157
|
+
raise ValueError(
|
|
158
|
+
f"Failed to parse tracking file {args.tracking_file}: {e}"
|
|
159
|
+
) from None
|
|
88
160
|
process_dataframe(
|
|
89
161
|
df,
|
|
90
162
|
channels=[args.cyan_channel, args.magenta_channel],
|
|
@@ -95,6 +167,7 @@ def main_cli() -> None:
|
|
|
95
167
|
else:
|
|
96
168
|
raise ValueError("Tracking file must be an XML or CSV file.")
|
|
97
169
|
|
|
170
|
+
# ---------------- 5. Estimate cell-cycle percentages ----------------
|
|
98
171
|
track_id_name = "UNIQUE_TRACK_ID"
|
|
99
172
|
if not args.generate_unique_tracks:
|
|
100
173
|
track_id_name = "TRACK_ID"
|
|
@@ -104,13 +177,41 @@ def main_cli() -> None:
|
|
|
104
177
|
dt=args.timestep,
|
|
105
178
|
channels=[args.cyan_channel, args.magenta_channel],
|
|
106
179
|
reference_data=reference_df,
|
|
107
|
-
track_id_name=track_id_name
|
|
180
|
+
track_id_name=track_id_name,
|
|
108
181
|
)
|
|
109
|
-
|
|
182
|
+
# ---------------- 6. Save results ----------------
|
|
183
|
+
tracking_path = Path(args.tracking_file)
|
|
184
|
+
output_csv = output_dir / (tracking_path.stem + "_processed.csv")
|
|
185
|
+
df.to_csv(output_csv, index=False)
|
|
110
186
|
|
|
111
187
|
|
|
112
188
|
def main_visualization() -> None:
|
|
113
|
-
"""Fucciphase visualization.
|
|
189
|
+
"""Fucciphase visualization.
|
|
190
|
+
|
|
191
|
+
Launch a napari-based visualization of FUCCIphase results.
|
|
192
|
+
|
|
193
|
+
This command-line entry point loads a processed FUCCIphase CSV file
|
|
194
|
+
together with the corresponding OME-TIFF time-lapse movie and
|
|
195
|
+
segmentation masks, then opens an interactive napari viewer showing:
|
|
196
|
+
|
|
197
|
+
- cyan and magenta fluorescence channels,
|
|
198
|
+
- segmentation masks as a labels layer,
|
|
199
|
+
- tracks and cell-cycle information overlaid on the image.
|
|
200
|
+
|
|
201
|
+
The function is intended to be invoked via the ``fucciphase-napari``
|
|
202
|
+
console script and does not return a value.
|
|
203
|
+
|
|
204
|
+
"""
|
|
205
|
+
logging.basicConfig(
|
|
206
|
+
level=logging.INFO,
|
|
207
|
+
format="%(levelname)s - %(name)s - %(message)s",
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
try:
|
|
211
|
+
import napari
|
|
212
|
+
except ImportError as err:
|
|
213
|
+
raise ImportError("Install napari.") from err
|
|
214
|
+
|
|
114
215
|
parser = argparse.ArgumentParser(
|
|
115
216
|
prog="fucciphase-napari",
|
|
116
217
|
description="FUCCIphase napari script to launch visualization.",
|
|
@@ -142,44 +243,64 @@ def main_visualization() -> None:
|
|
|
142
243
|
required=True,
|
|
143
244
|
)
|
|
144
245
|
parser.add_argument(
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
246
|
+
"--pixel_size",
|
|
247
|
+
type=float,
|
|
248
|
+
help="Pixel size, only used if not in metadata",
|
|
249
|
+
default=None,
|
|
250
|
+
)
|
|
149
251
|
|
|
150
252
|
args = parser.parse_args()
|
|
151
253
|
|
|
254
|
+
# Decide where to store outputs (CSV and, for XML input, processed XML)
|
|
255
|
+
output_dir = Path("outputs")
|
|
256
|
+
output_dir.mkdir(exist_ok=True)
|
|
257
|
+
|
|
258
|
+
# Try to read the video using AICSImage; fall back to bioio if needed
|
|
152
259
|
AICSIMAGE = False
|
|
153
|
-
BIOIMAGE = False
|
|
154
260
|
try:
|
|
155
261
|
from aicsimageio import AICSImage
|
|
156
262
|
|
|
157
263
|
AICSIMAGE = True
|
|
158
|
-
except ImportError
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
if not BIOIMAGE:
|
|
264
|
+
except ImportError:
|
|
265
|
+
try:
|
|
266
|
+
import bioio_ome_tiff
|
|
267
|
+
from bioio import BioImage
|
|
268
|
+
except ImportError as err:
|
|
165
269
|
raise ImportError(
|
|
166
|
-
"Please install
|
|
270
|
+
"Please install aicsimageio or bioio to read videos. "
|
|
271
|
+
"Install with: pip install aicsimageio "
|
|
272
|
+
"or pip install bioio bioio-ome-tiff"
|
|
167
273
|
) from err
|
|
274
|
+
|
|
168
275
|
if AICSIMAGE:
|
|
169
276
|
image = AICSImage(args.video)
|
|
170
|
-
|
|
277
|
+
else:
|
|
171
278
|
image = BioImage(args.video, reader=bioio_ome_tiff.Reader)
|
|
279
|
+
|
|
280
|
+
# Determine spatial scale; fall back to unit scale or user-provided pixel size
|
|
172
281
|
scale = (image.physical_pixel_sizes.Y, image.physical_pixel_sizes.X)
|
|
173
282
|
if None in scale:
|
|
174
283
|
if args.pixel_size is not None:
|
|
175
284
|
scale = (args.pixel_size, args.pixel_size)
|
|
176
285
|
else:
|
|
177
|
-
|
|
286
|
+
logger.warning("No pixel sizes found in image metadata, using unit scale")
|
|
178
287
|
scale = (1.0, 1.0)
|
|
179
288
|
cyan = image.get_image_dask_data("TYX", C=args.cyan_channel)
|
|
180
289
|
magenta = image.get_image_dask_data("TYX", C=args.magenta_channel)
|
|
181
290
|
masks = image.get_image_dask_data("TYX", C=args.segmask_channel)
|
|
182
|
-
|
|
291
|
+
|
|
292
|
+
try:
|
|
293
|
+
track_df = pd.read_csv(args.fucciphase_file)
|
|
294
|
+
except FileNotFoundError:
|
|
295
|
+
raise FileNotFoundError(
|
|
296
|
+
f"FUCCIphase file not found: {args.fucciphase_file}"
|
|
297
|
+
) from None
|
|
298
|
+
except pd.errors.EmptyDataError:
|
|
299
|
+
raise ValueError(f"FUCCIphase file is empty: {args.fucciphase_file}") from None
|
|
300
|
+
except pd.errors.ParserError as e:
|
|
301
|
+
raise ValueError(
|
|
302
|
+
f"Failed to parse FUCCIphase file {args.fucciphase_file}: {e}"
|
|
303
|
+
) from None
|
|
183
304
|
|
|
184
305
|
viewer = napari.Viewer()
|
|
185
306
|
|
|
@@ -194,7 +315,3 @@ def main_visualization() -> None:
|
|
|
194
315
|
textkwargs={"size": 14},
|
|
195
316
|
)
|
|
196
317
|
napari.run()
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
if __name__ == "__main__":
|
|
200
|
-
main_cli()
|