chipstream 0.6.0a5__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.
- chipstream/__init__.py +20 -0
- chipstream/_version.py +16 -0
- chipstream/cli/__init__.py +1 -0
- chipstream/cli/__main__.py +4 -0
- chipstream/cli/_main.py +10 -0
- chipstream/cli/cli_common.py +95 -0
- chipstream/cli/cli_main.py +228 -0
- chipstream/cli/cli_proc.py +141 -0
- chipstream/cli/cli_valid.py +127 -0
- chipstream/gui/__init__.py +1 -0
- chipstream/gui/__main__.py +4 -0
- chipstream/gui/_main.py +47 -0
- chipstream/gui/dlg_model_props.py +31 -0
- chipstream/gui/dlg_model_props.ui +165 -0
- chipstream/gui/img/chipstream_icon.png +0 -0
- chipstream/gui/main_window.py +438 -0
- chipstream/gui/main_window.ui +634 -0
- chipstream/gui/manager.py +255 -0
- chipstream/gui/splash.py +16 -0
- chipstream/gui/table_progress.py +83 -0
- chipstream/path_cache.py +34 -0
- chipstream-0.6.0a5.dist-info/LICENSE +674 -0
- chipstream-0.6.0a5.dist-info/METADATA +115 -0
- chipstream-0.6.0a5.dist-info/RECORD +27 -0
- chipstream-0.6.0a5.dist-info/WHEEL +5 -0
- chipstream-0.6.0a5.dist-info/entry_points.txt +3 -0
- chipstream-0.6.0a5.dist-info/top_level.txt +1 -0
chipstream/__init__.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# flake8: noqa: F401
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
from ._version import __version__, __version_tuple__
|
|
5
|
+
|
|
6
|
+
# Disable multithreading for all major tools that could somehow
|
|
7
|
+
# parallelize our code. We don't want that, because we have linear
|
|
8
|
+
# code and rely on multiprocessing for parallelization. This has
|
|
9
|
+
# to be done before importing numpy or any other library affected.
|
|
10
|
+
# If someone uses chipstream in a script, they either have to import
|
|
11
|
+
# chipstream first, or set the environment variables manually.
|
|
12
|
+
# We use `setdefault` to honor any other variables set by the
|
|
13
|
+
# user for whatever reason.
|
|
14
|
+
os.environ.setdefault("MKL_NUM_THREADS", "1")
|
|
15
|
+
os.environ.setdefault("NUMBA_NUM_THREADS", "1")
|
|
16
|
+
os.environ.setdefault("NUMEXPR_NUM_THREADS", "1")
|
|
17
|
+
os.environ.setdefault("NUMPY_NUM_THREADS", "1")
|
|
18
|
+
os.environ.setdefault("OPENBLAS_NUM_THREADS", "1")
|
|
19
|
+
os.environ.setdefault("OMP_NUM_THREADS", "1")
|
|
20
|
+
os.environ.setdefault("VECLIB_MAXIMUM_THREADS", "1")
|
chipstream/_version.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# file generated by setuptools_scm
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
TYPE_CHECKING = False
|
|
4
|
+
if TYPE_CHECKING:
|
|
5
|
+
from typing import Tuple, Union
|
|
6
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
7
|
+
else:
|
|
8
|
+
VERSION_TUPLE = object
|
|
9
|
+
|
|
10
|
+
version: str
|
|
11
|
+
__version__: str
|
|
12
|
+
__version_tuple__: VERSION_TUPLE
|
|
13
|
+
version_tuple: VERSION_TUPLE
|
|
14
|
+
|
|
15
|
+
__version__ = version = '0.6.0a5'
|
|
16
|
+
__version_tuple__ = version_tuple = (0, 6, 0)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from ._main import main # noqa: F401
|
chipstream/cli/_main.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from dcnum.feat.feat_background import get_available_background_methods
|
|
4
|
+
from dcnum.feat import Gate, QueueEventExtractor # noqa: F401
|
|
5
|
+
from dcnum.meta.ppid import get_class_method_info
|
|
6
|
+
from dcnum.segm import get_available_segmenters
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
seg_methods = get_available_segmenters()
|
|
10
|
+
bg_methods = get_available_background_methods()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PrettyFormatter(logging.Formatter):
|
|
14
|
+
grey = "\x1b[38;20m"
|
|
15
|
+
yellow = "\x1b[33;20m"
|
|
16
|
+
red = "\x1b[31;20m"
|
|
17
|
+
bold_red = "\x1b[31;1m"
|
|
18
|
+
reset = "\x1b[0m"
|
|
19
|
+
format = "%(levelname)s %(name)s: %(message)s"
|
|
20
|
+
|
|
21
|
+
FORMATS = {
|
|
22
|
+
logging.DEBUG: grey + format + reset,
|
|
23
|
+
logging.INFO: grey + format + reset,
|
|
24
|
+
logging.WARNING: yellow + format + reset,
|
|
25
|
+
logging.ERROR: red + format + reset,
|
|
26
|
+
logging.CRITICAL: bold_red + format + reset
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
def format(self, record):
|
|
30
|
+
log_fmt = self.FORMATS.get(record.levelno)
|
|
31
|
+
formatter = logging.Formatter(log_fmt)
|
|
32
|
+
return formatter.format(record)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def format_help_class_method_info(info_list):
|
|
36
|
+
"""Format a list of `info` dictionaries from `get_class_method_info`"""
|
|
37
|
+
choices = ["\b"] # "\b" means paragraph is not formatted in help string
|
|
38
|
+
for info in info_list:
|
|
39
|
+
code = info["code"]
|
|
40
|
+
title = info["title"]
|
|
41
|
+
choices.append(f" - '{code}': {title}")
|
|
42
|
+
for mname in info.get("defaults", []):
|
|
43
|
+
# We are flattening through all methods, because the CLI
|
|
44
|
+
# makes them transparently available. There must not be
|
|
45
|
+
# any name clashes, the developer is responsible for
|
|
46
|
+
# that.
|
|
47
|
+
kwds = info["defaults"][mname]
|
|
48
|
+
if kwds:
|
|
49
|
+
for kw in kwds:
|
|
50
|
+
choices.append(f" - {kw}={kwds[kw]}")
|
|
51
|
+
return "\n".join(choices)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def get_choices_help_string(class_dict, static_kw_methods=None):
|
|
55
|
+
"""Return a chipstream help string for this class
|
|
56
|
+
|
|
57
|
+
Parameters
|
|
58
|
+
----------
|
|
59
|
+
class_dict: dictionary of objects
|
|
60
|
+
The dictionary holds the classes to inspect
|
|
61
|
+
static_kw_methods: list of callable
|
|
62
|
+
The methods to inspect; all kwargs-only keyword arguments
|
|
63
|
+
are extracted.
|
|
64
|
+
"""
|
|
65
|
+
info_list = []
|
|
66
|
+
for key in class_dict:
|
|
67
|
+
class_obj = class_dict[key]
|
|
68
|
+
info_list.append(get_class_method_info(class_obj, static_kw_methods))
|
|
69
|
+
return format_help_class_method_info(info_list)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def get_choices_help_string_segmenter(class_dict):
|
|
73
|
+
"""Return a chipstream help string for a segmenter class
|
|
74
|
+
|
|
75
|
+
This is a custom function, analogous to get_choices_help_string,
|
|
76
|
+
but for segmenters, which might or might not allow mask
|
|
77
|
+
post-processing (depending on whether `mask_postprocessing` is set).
|
|
78
|
+
|
|
79
|
+
Parameters
|
|
80
|
+
----------
|
|
81
|
+
class_dict: dictionary of objects
|
|
82
|
+
The dictionary holds the classes to inspect
|
|
83
|
+
"""
|
|
84
|
+
info_list = []
|
|
85
|
+
for key in class_dict:
|
|
86
|
+
class_obj = class_dict[key]
|
|
87
|
+
static_kw_methods = ["segment_algorithm"]
|
|
88
|
+
static_kw_defaults = {}
|
|
89
|
+
if class_obj.mask_postprocessing:
|
|
90
|
+
static_kw_methods.append("process_mask")
|
|
91
|
+
static_kw_defaults["process_mask"] = class_obj.mask_default_kwargs
|
|
92
|
+
info_list.append(get_class_method_info(class_obj,
|
|
93
|
+
static_kw_methods,
|
|
94
|
+
static_kw_defaults))
|
|
95
|
+
return format_help_class_method_info(info_list)
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import multiprocessing as mp
|
|
3
|
+
import pathlib
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
from .._version import version
|
|
9
|
+
|
|
10
|
+
from . import cli_common as cm
|
|
11
|
+
from .cli_proc import process_dataset
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@click.command(name="chipstream-cli",
|
|
15
|
+
help=f"""
|
|
16
|
+
Segmentation and feature extraction for deformability cytometry data.
|
|
17
|
+
|
|
18
|
+
Read the image data from an input file, perform segmentation, feature
|
|
19
|
+
extraction, and gating.
|
|
20
|
+
If ``PATH_OUT`` is not specified, then the data are written to a new file
|
|
21
|
+
with the suffix "_dcn.rtdc". In recursive mode (``-r``), a target
|
|
22
|
+
directory may be specified.
|
|
23
|
+
|
|
24
|
+
You can specify which segmenter and feature extractor should be used.
|
|
25
|
+
You can also specify additional keyword arguments for
|
|
26
|
+
segmentation (``-ks``), feature extraction (``-kf``), and gating (``-kg``).
|
|
27
|
+
|
|
28
|
+
Available segmenters with keyword arguments:
|
|
29
|
+
|
|
30
|
+
{cm.get_choices_help_string_segmenter(cm.seg_methods)}
|
|
31
|
+
|
|
32
|
+
Available feature extractors:
|
|
33
|
+
|
|
34
|
+
{cm.get_choices_help_string({"legacy": cm.QueueEventExtractor},
|
|
35
|
+
["get_events_from_masks"])}
|
|
36
|
+
|
|
37
|
+
Available gating options:
|
|
38
|
+
|
|
39
|
+
{cm.get_choices_help_string({"": cm.Gate}, ["__init__"])}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
Examples:
|
|
43
|
+
|
|
44
|
+
Process an .rtdc measurement, without computing the Haralick texture features::
|
|
45
|
+
|
|
46
|
+
chipstream-cli process -kf haralick=False M001_data.rtdc
|
|
47
|
+
|
|
48
|
+
Recursively analyze a directory containing .rtdc files::
|
|
49
|
+
|
|
50
|
+
chipstream-cli process --recursive directory_name
|
|
51
|
+
|
|
52
|
+
""")
|
|
53
|
+
@click.argument("path_in",
|
|
54
|
+
type=click.Path(exists=True,
|
|
55
|
+
dir_okay=True,
|
|
56
|
+
resolve_path=True,
|
|
57
|
+
path_type=pathlib.Path))
|
|
58
|
+
@click.argument("path_out",
|
|
59
|
+
required=False,
|
|
60
|
+
type=click.Path(dir_okay=True,
|
|
61
|
+
writable=True,
|
|
62
|
+
resolve_path=True,
|
|
63
|
+
path_type=pathlib.Path),
|
|
64
|
+
)
|
|
65
|
+
@click.option("-b", "--background-method",
|
|
66
|
+
type=click.Choice(sorted(cm.bg_methods.keys()),
|
|
67
|
+
case_sensitive=False),
|
|
68
|
+
default="sparsemed",
|
|
69
|
+
help="Background computation method to use.")
|
|
70
|
+
@click.option("-kb", "background_kwargs",
|
|
71
|
+
multiple=True,
|
|
72
|
+
help="Optional ``KEY=VALUE`` argument for the specified "
|
|
73
|
+
"background method",
|
|
74
|
+
metavar="KEY=VALUE",
|
|
75
|
+
)
|
|
76
|
+
@click.option("-s", "--segmentation-method",
|
|
77
|
+
type=click.Choice(sorted(cm.seg_methods.keys()),
|
|
78
|
+
case_sensitive=False),
|
|
79
|
+
default="thresh",
|
|
80
|
+
help="Segmentation method to use.")
|
|
81
|
+
@click.option("-ks", "segmentation_kwargs",
|
|
82
|
+
multiple=True,
|
|
83
|
+
help="Optional ``KEY=VALUE`` argument for the specified "
|
|
84
|
+
"segmenter.",
|
|
85
|
+
metavar="KEY=VALUE",
|
|
86
|
+
)
|
|
87
|
+
@click.option("-kf", "feature_kwargs",
|
|
88
|
+
multiple=True,
|
|
89
|
+
help="Optional ``KEY=VALUE`` argument for the specified "
|
|
90
|
+
"feature extractor.",
|
|
91
|
+
metavar="KEY=VALUE",
|
|
92
|
+
)
|
|
93
|
+
@click.option("-kg", "gate_kwargs",
|
|
94
|
+
multiple=True,
|
|
95
|
+
help="Optional ``KEY=VALUE`` argument for event gating.",
|
|
96
|
+
metavar="KEY=VALUE",
|
|
97
|
+
)
|
|
98
|
+
@click.option("-p", "--pixel-size", type=float, default=0,
|
|
99
|
+
help="Set/override the pixel size for feature "
|
|
100
|
+
"extraction [µm].")
|
|
101
|
+
@click.option("--limit-events", type=str, default="0",
|
|
102
|
+
help="Limit events of events to analyze. This can be either "
|
|
103
|
+
"a number (e.g. '5000') or a range (e.g. '5000-7000'). "
|
|
104
|
+
"You can also specify a step size (e.g. '5000-7000-2' for "
|
|
105
|
+
"every second event). The convention follows Python slices "
|
|
106
|
+
"with 'n' substituting for 'None'.")
|
|
107
|
+
@click.option("--drain-basins", type=str, is_flag=True,
|
|
108
|
+
help="Write all basin features from input to output file. This "
|
|
109
|
+
"option trades computation time and small file size for "
|
|
110
|
+
"an output file that contains all available features.")
|
|
111
|
+
@click.option("-r", "--recursive", is_flag=True,
|
|
112
|
+
help="Recurse into subdirectories.")
|
|
113
|
+
@click.option("--num-cpus",
|
|
114
|
+
type=click.IntRange(min=1, max=mp.cpu_count(), clamp=True),
|
|
115
|
+
help="Number of processes to create."
|
|
116
|
+
)
|
|
117
|
+
@click.option("--dry-run", is_flag=True,
|
|
118
|
+
help="Only print the pipeline identifiers and exit.")
|
|
119
|
+
@click.option("--verbose", is_flag=True,
|
|
120
|
+
help="Yield a more verbose output.")
|
|
121
|
+
@click.option("--debug", is_flag=True,
|
|
122
|
+
help="Run chipstream in debugging mode. This disables "
|
|
123
|
+
"multiprocessing and yields a more verbose output.")
|
|
124
|
+
@click.version_option(version)
|
|
125
|
+
def chipstream_cli(
|
|
126
|
+
path_in,
|
|
127
|
+
path_out=None,
|
|
128
|
+
background_method="sparsemed",
|
|
129
|
+
background_kwargs=None,
|
|
130
|
+
segmentation_method="thresh",
|
|
131
|
+
segmentation_kwargs=None,
|
|
132
|
+
feature_kwargs=None,
|
|
133
|
+
gate_kwargs=None,
|
|
134
|
+
pixel_size=0,
|
|
135
|
+
limit_events="0",
|
|
136
|
+
drain_basins=False,
|
|
137
|
+
recursive=False,
|
|
138
|
+
num_cpus=None,
|
|
139
|
+
dry_run=False,
|
|
140
|
+
verbose=False,
|
|
141
|
+
debug=False,
|
|
142
|
+
):
|
|
143
|
+
|
|
144
|
+
if debug:
|
|
145
|
+
click.secho("Running in debug mode (this will be slow)",
|
|
146
|
+
fg="yellow")
|
|
147
|
+
verbose = True
|
|
148
|
+
|
|
149
|
+
# Parse limit_frames to get the HDF5Data index_mapping
|
|
150
|
+
if limit_events == "0":
|
|
151
|
+
index_mapping = None
|
|
152
|
+
elif limit_events.count("-"):
|
|
153
|
+
vals = limit_events.split("-")
|
|
154
|
+
assert len(vals) in [2, 3], "slice definition must have length 2 or 3"
|
|
155
|
+
start = None if vals[0] == "n" else int(vals[0])
|
|
156
|
+
stop = None if vals[1] == "n" else int(vals[1])
|
|
157
|
+
if len(vals) == 3:
|
|
158
|
+
step = None if vals[2] == "n" else int(vals[2])
|
|
159
|
+
else:
|
|
160
|
+
step = None
|
|
161
|
+
index_mapping = slice(start, stop, step)
|
|
162
|
+
else:
|
|
163
|
+
index_mapping = int(limit_events)
|
|
164
|
+
|
|
165
|
+
# Tell the root logger to pretty-print logs
|
|
166
|
+
root_logger = logging.getLogger()
|
|
167
|
+
handler = logging.StreamHandler()
|
|
168
|
+
handler.setFormatter(cm.PrettyFormatter())
|
|
169
|
+
root_logger.addHandler(handler)
|
|
170
|
+
handler.setLevel(logging.DEBUG if verbose else logging.WARNING)
|
|
171
|
+
|
|
172
|
+
mp.freeze_support()
|
|
173
|
+
|
|
174
|
+
process_kwargs = dict(
|
|
175
|
+
background_method=background_method,
|
|
176
|
+
background_kwargs=background_kwargs,
|
|
177
|
+
segmentation_method=segmentation_method,
|
|
178
|
+
segmentation_kwargs=segmentation_kwargs,
|
|
179
|
+
feature_kwargs=feature_kwargs,
|
|
180
|
+
gate_kwargs=gate_kwargs,
|
|
181
|
+
pixel_size=pixel_size,
|
|
182
|
+
index_mapping=index_mapping,
|
|
183
|
+
# Below this line are arguments that do not define the pipeline ID
|
|
184
|
+
basin_strategy="drain" if drain_basins else "tap",
|
|
185
|
+
num_cpus=num_cpus or mp.cpu_count(),
|
|
186
|
+
dry_run=dry_run,
|
|
187
|
+
debug=debug,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
if recursive:
|
|
191
|
+
failed = 0 # keeps track of files that failed to process
|
|
192
|
+
for pi in sorted(path_in.rglob("*.rtdc")):
|
|
193
|
+
if pi.name.endswith("_dcn.rtdc"):
|
|
194
|
+
continue
|
|
195
|
+
if path_out is not None:
|
|
196
|
+
poi = path_out / pi.relative_to(path_in)
|
|
197
|
+
po = poi.with_name(poi.stem + "_dcn.rtdc")
|
|
198
|
+
po.parent.mkdir(parents=True, exist_ok=True)
|
|
199
|
+
else:
|
|
200
|
+
po = None
|
|
201
|
+
click.secho(f"\nProcessing {pi}")
|
|
202
|
+
failed += process_dataset(path_in=pi,
|
|
203
|
+
path_out=po,
|
|
204
|
+
**process_kwargs)
|
|
205
|
+
if dry_run:
|
|
206
|
+
click.secho("Stopping dry run after one iteration")
|
|
207
|
+
break
|
|
208
|
+
if failed:
|
|
209
|
+
click.secho(f"Could not process {failed} files", fg="red")
|
|
210
|
+
exit_code = bool(failed)
|
|
211
|
+
else:
|
|
212
|
+
if path_in.is_dir():
|
|
213
|
+
click.secho(f"PATH_IN must be a file, but '{path_in}' "
|
|
214
|
+
f"is a directory. Did you forget to specify "
|
|
215
|
+
f"the `--recursive` flag?", fg="red")
|
|
216
|
+
exit_code = 1
|
|
217
|
+
elif path_out and path_out.is_dir():
|
|
218
|
+
click.secho(f"PATH_OUT must be a path to a file, but "
|
|
219
|
+
f"'{path_out}' is a directory")
|
|
220
|
+
exit_code = 1
|
|
221
|
+
else:
|
|
222
|
+
# everything ok
|
|
223
|
+
exit_code = process_dataset(path_in=path_in,
|
|
224
|
+
path_out=path_out,
|
|
225
|
+
**process_kwargs)
|
|
226
|
+
if exit_code:
|
|
227
|
+
click.secho("Encountered problems during processing", fg="red")
|
|
228
|
+
sys.exit(exit_code)
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import pathlib
|
|
2
|
+
import time
|
|
3
|
+
from typing import List, Literal
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
import dcnum.logic
|
|
7
|
+
from dcnum.meta import ppid
|
|
8
|
+
import dcnum.read
|
|
9
|
+
import dcnum.segm
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
from . import cli_common as cm
|
|
13
|
+
from .cli_valid import (
|
|
14
|
+
validate_background_kwargs, validate_feature_kwargs, validate_gate_kwargs,
|
|
15
|
+
validate_pixel_size, validate_segmentation_kwargs
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def process_dataset(
|
|
20
|
+
path_in: pathlib.Path,
|
|
21
|
+
path_out: pathlib.Path,
|
|
22
|
+
background_method: str,
|
|
23
|
+
background_kwargs: List[str],
|
|
24
|
+
segmentation_method: str,
|
|
25
|
+
segmentation_kwargs: List[str],
|
|
26
|
+
feature_kwargs: List[str],
|
|
27
|
+
gate_kwargs: List[str],
|
|
28
|
+
pixel_size: float,
|
|
29
|
+
index_mapping: int | slice | None,
|
|
30
|
+
# Below this line are arguments that do not affect the pipeline ID
|
|
31
|
+
basin_strategy: Literal["drain", "tap"],
|
|
32
|
+
num_cpus: int,
|
|
33
|
+
dry_run: bool,
|
|
34
|
+
debug: bool,
|
|
35
|
+
):
|
|
36
|
+
# Make sure the pixel size makes sense
|
|
37
|
+
if pixel_size == 0:
|
|
38
|
+
pixel_size = validate_pixel_size(data_path=path_in)
|
|
39
|
+
|
|
40
|
+
if path_out is None:
|
|
41
|
+
path_out = path_in.with_name(path_in.stem + "_dcn.rtdc")
|
|
42
|
+
path_out.parent.mkdir(parents=True, exist_ok=True)
|
|
43
|
+
|
|
44
|
+
# data keyword arguments
|
|
45
|
+
data_kwargs = {"pixel_size": pixel_size,
|
|
46
|
+
"index_mapping": index_mapping}
|
|
47
|
+
with dcnum.read.HDF5Data(path_in, **data_kwargs) as data:
|
|
48
|
+
dat_id = data.get_ppid()
|
|
49
|
+
click.echo(f"Data ID:\t{dat_id}")
|
|
50
|
+
|
|
51
|
+
# background keyword arguments
|
|
52
|
+
bg_kwargs = validate_background_kwargs(background_method,
|
|
53
|
+
background_kwargs)
|
|
54
|
+
bg_cls = cm.bg_methods[background_method]
|
|
55
|
+
bg_id = bg_cls.get_ppid_from_ppkw(bg_kwargs)
|
|
56
|
+
click.echo(f"Background ID:\t{bg_id}")
|
|
57
|
+
|
|
58
|
+
# segmenter keyword arguments
|
|
59
|
+
seg_kwargs = validate_segmentation_kwargs(segmentation_method,
|
|
60
|
+
segmentation_kwargs)
|
|
61
|
+
seg_cls = cm.seg_methods[segmentation_method]
|
|
62
|
+
seg_id = seg_cls.get_ppid_from_ppkw(seg_kwargs)
|
|
63
|
+
click.echo(f"Segmenter ID:\t{seg_id}")
|
|
64
|
+
|
|
65
|
+
# feature keyword arguments
|
|
66
|
+
feat_kwargs = validate_feature_kwargs(feature_kwargs)
|
|
67
|
+
feat_cls = cm.QueueEventExtractor
|
|
68
|
+
feat_id = feat_cls.get_ppid_from_ppkw(feat_kwargs)
|
|
69
|
+
click.echo(f"Feature ID:\t{feat_id}")
|
|
70
|
+
|
|
71
|
+
# gate keyword arguments
|
|
72
|
+
gate_cls = cm.Gate
|
|
73
|
+
gate_kwargs = validate_gate_kwargs(gate_kwargs)
|
|
74
|
+
gate_id = gate_cls.get_ppid_from_ppkw(gate_kwargs)
|
|
75
|
+
click.echo(f"Gate ID:\t{gate_id}")
|
|
76
|
+
|
|
77
|
+
# compute pipeline hash
|
|
78
|
+
pph = ppid.compute_pipeline_hash(
|
|
79
|
+
gen_id=ppid.DCNUM_PPID_GENERATION,
|
|
80
|
+
dat_id=dat_id,
|
|
81
|
+
bg_id=bg_id,
|
|
82
|
+
seg_id=seg_id,
|
|
83
|
+
feat_id=feat_id,
|
|
84
|
+
gate_id=gate_id)
|
|
85
|
+
click.secho(f"Pipeline hash:\t{pph}")
|
|
86
|
+
|
|
87
|
+
if dry_run:
|
|
88
|
+
click.echo("Dry run complete")
|
|
89
|
+
return 0
|
|
90
|
+
|
|
91
|
+
job = dcnum.logic.DCNumPipelineJob(
|
|
92
|
+
path_in=path_in,
|
|
93
|
+
path_out=path_out,
|
|
94
|
+
data_code="hdf",
|
|
95
|
+
data_kwargs=data_kwargs,
|
|
96
|
+
background_code=bg_cls.get_ppid_code(),
|
|
97
|
+
background_kwargs=bg_kwargs,
|
|
98
|
+
segmenter_code=seg_cls.get_ppid_code(),
|
|
99
|
+
segmenter_kwargs=seg_kwargs,
|
|
100
|
+
feature_code=feat_cls.get_ppid_code(),
|
|
101
|
+
feature_kwargs=feat_kwargs,
|
|
102
|
+
gate_code=gate_cls.get_ppid_code(),
|
|
103
|
+
gate_kwargs=gate_kwargs,
|
|
104
|
+
basin_strategy=basin_strategy,
|
|
105
|
+
num_procs=num_cpus,
|
|
106
|
+
debug=debug,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
job.validate()
|
|
111
|
+
except dcnum.segm.SegmenterNotApplicableError as e:
|
|
112
|
+
click.secho(f"Segmenter '{segmentation_method}' cannot be applied "
|
|
113
|
+
f"to '{path_in}': {', '.join(e.reasons_list)}",
|
|
114
|
+
fg="red")
|
|
115
|
+
return 1
|
|
116
|
+
|
|
117
|
+
runner = dcnum.logic.DCNumJobRunner(job)
|
|
118
|
+
runner.start()
|
|
119
|
+
strlen = 0
|
|
120
|
+
prev_str = ""
|
|
121
|
+
while True:
|
|
122
|
+
status = runner.get_status()
|
|
123
|
+
progress = status["progress"]
|
|
124
|
+
state = status["state"]
|
|
125
|
+
print_str = f"Processing {progress:.0%} ({state})"
|
|
126
|
+
if print_str != prev_str: # don't clutter stdout
|
|
127
|
+
strlen = max(strlen, len(print_str))
|
|
128
|
+
print(print_str.ljust(strlen), end="\r", flush=True)
|
|
129
|
+
prev_str = print_str
|
|
130
|
+
if status["state"] in ["done", "error"]:
|
|
131
|
+
break
|
|
132
|
+
time.sleep(.3) # don't use 100% CPU
|
|
133
|
+
print("") # new line
|
|
134
|
+
|
|
135
|
+
if status["state"] == "error":
|
|
136
|
+
click.secho(runner.error_tb, fg="red")
|
|
137
|
+
runner.join(delete_temporary_files=False)
|
|
138
|
+
return 1 # "exit code" > 0 means error
|
|
139
|
+
else:
|
|
140
|
+
runner.join(delete_temporary_files=True)
|
|
141
|
+
return 0
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import warnings
|
|
3
|
+
|
|
4
|
+
from dcnum.meta import ppid
|
|
5
|
+
import dcnum.read
|
|
6
|
+
import h5py
|
|
7
|
+
|
|
8
|
+
from . import cli_common as cm
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def validate_background_kwargs(bg_method, args):
|
|
12
|
+
"""Parse background keyword arguments"""
|
|
13
|
+
# Get list of valid keyword arguments
|
|
14
|
+
bg_cls = cm.bg_methods[bg_method]
|
|
15
|
+
spec = inspect.getfullargspec(bg_cls.check_user_kwargs)
|
|
16
|
+
valid_kw = spec.kwonlyargs
|
|
17
|
+
annot = spec.annotations
|
|
18
|
+
# Convert the input args to key-value pairs
|
|
19
|
+
kwargs = {}
|
|
20
|
+
for key, value in [a.split("=") for a in args]:
|
|
21
|
+
if key not in valid_kw:
|
|
22
|
+
raise ValueError(f"Invalid keyword '{key}' for {bg_method}. "
|
|
23
|
+
+ f"Allowed keywords are {valid_kw}!")
|
|
24
|
+
# Convert to correct dtype (default to string)
|
|
25
|
+
kwargs[key] = ppid.convert_to_dtype(value, annot[key])
|
|
26
|
+
return kwargs
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def validate_feature_kwargs(args):
|
|
30
|
+
# Get list of valid keyword arguments
|
|
31
|
+
feat_cls = cm.QueueEventExtractor
|
|
32
|
+
feat_code = feat_cls.get_ppid_code()
|
|
33
|
+
# extract_approach
|
|
34
|
+
spec_appr = inspect.getfullargspec(feat_cls.get_events_from_masks)
|
|
35
|
+
valid_kw_appr = spec_appr.kwonlyargs
|
|
36
|
+
annot_appr = spec_appr.annotations
|
|
37
|
+
# Convert the input args to key-value pairs
|
|
38
|
+
kwargs = {}
|
|
39
|
+
for key, value in [a.split("=") for a in args]:
|
|
40
|
+
if key in valid_kw_appr:
|
|
41
|
+
kwargs[key] = ppid.convert_to_dtype(value, annot_appr[key])
|
|
42
|
+
else:
|
|
43
|
+
raise ValueError(
|
|
44
|
+
f"Invalid keyword '{key}' for '{feat_code}'. "
|
|
45
|
+
f"Allowed keywords are {valid_kw_appr}!")
|
|
46
|
+
return kwargs
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def validate_gate_kwargs(args):
|
|
50
|
+
spec = inspect.getfullargspec(cm.Gate.__init__)
|
|
51
|
+
valid_kw_appr = spec.kwonlyargs
|
|
52
|
+
annot_appr = spec.annotations
|
|
53
|
+
# Convert the input args to key-value pairs
|
|
54
|
+
kwargs = {}
|
|
55
|
+
for key, value in [a.split("=") for a in args]:
|
|
56
|
+
if key in valid_kw_appr:
|
|
57
|
+
kwargs[key] = ppid.convert_to_dtype(value, annot_appr[key])
|
|
58
|
+
else:
|
|
59
|
+
raise ValueError(
|
|
60
|
+
f"Invalid keyword '{key}' for gating. "
|
|
61
|
+
f"Allowed keywords are {valid_kw_appr} (and box filters)!")
|
|
62
|
+
return kwargs
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def validate_pixel_size(data_path):
|
|
66
|
+
with h5py.File(data_path) as h5:
|
|
67
|
+
did = h5.attrs.get("setup:identifier", "EMPTY")
|
|
68
|
+
pixel_size = h5.attrs.get("imaging:pixel size", 0)
|
|
69
|
+
if (did.startswith("RC-")
|
|
70
|
+
and (pixel_size < 0.255 or pixel_size > 0.275)):
|
|
71
|
+
hd = dcnum.read.HDF5Data(h5, pixel_size=0.260) # placeholder
|
|
72
|
+
warnings.warn(
|
|
73
|
+
f"Correcting for invalid pixel size in '{hd.path}'!")
|
|
74
|
+
# Set default pixel size for Rivercyte devices
|
|
75
|
+
# Check the logs for the device name used.
|
|
76
|
+
logdat = hd.logs.get("cytoshot-acquisition", [])
|
|
77
|
+
for line in logdat:
|
|
78
|
+
line = line.strip().lower()
|
|
79
|
+
if line.startswith("device name:"):
|
|
80
|
+
dev_name = line.split(":")[1].strip()
|
|
81
|
+
break
|
|
82
|
+
else:
|
|
83
|
+
# fall-back to old camera
|
|
84
|
+
dev_name = "naiad 1.0"
|
|
85
|
+
if dev_name == "naiad 1.0":
|
|
86
|
+
# Naiad v1.0 camera VLXT06MI
|
|
87
|
+
pixel_size = 0.2645
|
|
88
|
+
elif dev_name == "naiad 1.1":
|
|
89
|
+
# Naiad v1.1 camera VCXU213M
|
|
90
|
+
pixel_size = 0.2675
|
|
91
|
+
else:
|
|
92
|
+
warnings.warn(f"Unknown device name: '{dev_name}'; "
|
|
93
|
+
f"Not changing pixel size {pixel_size}")
|
|
94
|
+
return pixel_size
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def validate_segmentation_kwargs(seg_method, args):
|
|
98
|
+
"""Parse segmenter keyword arguments"""
|
|
99
|
+
# Get list of valid keyword arguments
|
|
100
|
+
seg_cls = cm.seg_methods[seg_method]
|
|
101
|
+
# segment_algorithm
|
|
102
|
+
spec_appr = inspect.getfullargspec(seg_cls.segment_algorithm)
|
|
103
|
+
valid_kw_appr = spec_appr.kwonlyargs
|
|
104
|
+
annot_appr = spec_appr.annotations
|
|
105
|
+
# process_mask
|
|
106
|
+
if seg_cls.mask_postprocessing:
|
|
107
|
+
spec_mask = inspect.getfullargspec(seg_cls.process_mask)
|
|
108
|
+
valid_kw_mask = spec_mask.kwonlyargs
|
|
109
|
+
annot_mask = spec_mask.annotations
|
|
110
|
+
else:
|
|
111
|
+
valid_kw_mask = []
|
|
112
|
+
annot_mask = {}
|
|
113
|
+
|
|
114
|
+
# Convert the input args to key-value pairs
|
|
115
|
+
kwargs = {}
|
|
116
|
+
kwargs_mask = {}
|
|
117
|
+
for key, value in [a.split("=") for a in args]:
|
|
118
|
+
if key in valid_kw_mask:
|
|
119
|
+
kwargs_mask[key] = ppid.convert_to_dtype(value, annot_mask[key])
|
|
120
|
+
elif key in valid_kw_appr:
|
|
121
|
+
kwargs[key] = ppid.convert_to_dtype(value, annot_appr[key])
|
|
122
|
+
else:
|
|
123
|
+
raise ValueError(
|
|
124
|
+
f"Invalid keyword '{key}' for {seg_method}. "
|
|
125
|
+
f"Allowed keywords are {valid_kw_appr + valid_kw_mask}!")
|
|
126
|
+
kwargs["kwargs_mask"] = kwargs_mask
|
|
127
|
+
return kwargs
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from ._main import main # noqa: F401
|