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 CHANGED
@@ -1,4 +1,10 @@
1
- """Cell cycle analysis plugin."""
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: List[str],
16
+ channels: list[str],
15
17
  sensor: FUCCISensor,
16
- thresholds: List[float],
18
+ thresholds: list[float],
17
19
  use_moving_average: bool = True,
18
20
  window_size: int = 7,
19
- manual_min: Optional[List[float]] = None,
20
- manual_max: Optional[List[float]] = None,
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
- """Process a pd.DataFrame by computing the cell cycle percentage from two FUCCI
27
- cycle reporter channels in place.
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
- Dataframe
51
+ Input dataframe containing tracking and intensity features.
52
+
42
53
  channels: List[str]
43
- Names of channels holding FUCCI information
54
+ Names of columns holding FUCCI fluorescence information.
55
+
44
56
  sensor : FUCCISensor
45
- FUCCI sensor with phase specifics
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
- Use moving average before normalization, by default True
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, by default 5
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
- 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)
73
+
60
74
  generate_unique_tracks: bool
61
- Assign unique track IDs to splitted tracks.
62
- Requires usage of action in TrackMate.
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 with track IDs
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
- print("Warning: unique tracks can only be prepared for TrackMate files.")
76
- print("The tracks have not been updated.")
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: Union[str, Path],
101
- channels: List[str],
150
+ xml_path: str | Path,
151
+ channels: list[str],
102
152
  sensor: FUCCISensor,
103
- thresholds: List[float],
153
+ thresholds: list[float],
104
154
  use_moving_average: bool = True,
105
155
  window_size: int = 7,
106
- manual_min: Optional[List[float]] = None,
107
- manual_max: Optional[List[float]] = None,
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
- """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.
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 channels holding FUCCI information
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 specifics
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
- Use moving average before normalization, by default True
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, by default 5
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
- estimate_percentage: bool, optional
146
- Estimate cell cycle percentage
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
- pd.DataFrame
151
- Dataframe with the cell cycle percentage and the corresponding phases
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 xml
236
+ # export the updated XML next to the original file
174
237
  new_name = Path(xml_path).stem + "_processed.xml"
175
- new_path = Path(xml_path).parent / new_name
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: Union[Path, str]) -> Tuple[pd.DataFrame, TrackMateXML]:
10
- """Read a trackmate exported xml file.
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 xml file.
14
+ Path to the XML file.
16
15
 
17
16
  Returns
18
17
  -------
19
18
  df : pandas.DataFrame
20
- Dataframe containing the xml data.
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: Union[Path, str]) -> pd.DataFrame:
36
- """Read a trackmate exported csv file.
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 csv file.
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 csv data.
53
+ Dataframe containing the CSV data with converted dtypes.
51
54
 
52
55
  Raises
53
56
  ------
54
57
  ValueError
55
- If the csv file does not contain at least two channels.
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
- try:
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
- type=bool,
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
- reference_df = pd.read_csv(args.reference_file)
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
- with open(args.sensor_file) as fp:
73
- sensor_properties = json.load(fp)
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
- df = pd.read_csv(args.tracking_file)
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
- df.to_csv(args.tracking_file + "_processed.csv", index=False)
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
- "--pixel_size",
146
- type=float,
147
- help="Pixel size, only used if not in metadata",
148
- default=None)
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 as err:
159
- from bioio import BioImage
160
-
161
- BIOIMAGE = True
162
- import bioio_ome_tiff
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 AICSImage or bioio to read videos"
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
- elif BIOIMAGE:
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
- print("WARNING: No pixel sizes found, using unit scale")
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
- track_df = pd.read_csv(args.fucciphase_file)
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()