ctkit 0.1.0__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.
- ctkit/__init__.py +144 -0
- ctkit/api.py +358 -0
- ctkit/cli.py +754 -0
- ctkit/config.py +453 -0
- ctkit/constants.py +297 -0
- ctkit/dataset.py +1449 -0
- ctkit/datasets.py +216 -0
- ctkit/features.py +177 -0
- ctkit/image.py +1141 -0
- ctkit/io.py +384 -0
- ctkit/metadata.py +233 -0
- ctkit/py.typed +0 -0
- ctkit/qc.py +482 -0
- ctkit/segmentation.py +367 -0
- ctkit/tcia.py +427 -0
- ctkit/validation.py +187 -0
- ctkit-0.1.0.dist-info/METADATA +168 -0
- ctkit-0.1.0.dist-info/RECORD +22 -0
- ctkit-0.1.0.dist-info/WHEEL +5 -0
- ctkit-0.1.0.dist-info/entry_points.txt +2 -0
- ctkit-0.1.0.dist-info/licenses/LICENSE +24 -0
- ctkit-0.1.0.dist-info/top_level.txt +1 -0
ctkit/__init__.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""ctkit: reproducible CT image processing for AI and radiomics.
|
|
2
|
+
|
|
3
|
+
The package exists so that "how was this image processed?" has an exact,
|
|
4
|
+
runnable answer. A protocol is a :class:`ProcessingConfig`; applying it to one
|
|
5
|
+
scan is :class:`RadiologyImage`, and to a cohort is :class:`Dataset`.
|
|
6
|
+
|
|
7
|
+
Quick start — construct a cohort or a scan, then call the steps on it::
|
|
8
|
+
|
|
9
|
+
import ctkit
|
|
10
|
+
|
|
11
|
+
ctkit.download("tcga-kirc", "data/raw", limit=20)
|
|
12
|
+
|
|
13
|
+
data = ctkit.Dataset("data/raw")
|
|
14
|
+
data.filter(min_slices=25).process("tcga-kirc", out_dir="data/processed")
|
|
15
|
+
|
|
16
|
+
The same steps chain on one scan, modifying it in place::
|
|
17
|
+
|
|
18
|
+
scan = ctkit.RadiologyImage("case/imaging.nii.gz", mask="case/segmentation.nii.gz")
|
|
19
|
+
scan.orient().clip(-200, 300).resample((0.8, 0.8, 3.0)).save("processed/case.nii.gz")
|
|
20
|
+
|
|
21
|
+
Each step is also a function taking whatever you have — a path, an array, a
|
|
22
|
+
scan, or a cohort::
|
|
23
|
+
|
|
24
|
+
ctkit.clip("case/imaging.nii.gz", -200, 300)
|
|
25
|
+
|
|
26
|
+
and a field of a :class:`ProcessingConfig`, which is what makes a protocol
|
|
27
|
+
something you can print, save to YAML, and publish alongside a paper.
|
|
28
|
+
|
|
29
|
+
Nothing is written to disk until ``save()``; intermediate volumes only ever
|
|
30
|
+
exist in memory.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
from importlib import import_module
|
|
36
|
+
from typing import TYPE_CHECKING
|
|
37
|
+
|
|
38
|
+
__version__ = "0.1.0"
|
|
39
|
+
|
|
40
|
+
from .api import (
|
|
41
|
+
apply_mask,
|
|
42
|
+
check,
|
|
43
|
+
clip,
|
|
44
|
+
crop_to_content,
|
|
45
|
+
filter,
|
|
46
|
+
normalize,
|
|
47
|
+
orient,
|
|
48
|
+
process,
|
|
49
|
+
radiomics,
|
|
50
|
+
resample,
|
|
51
|
+
save,
|
|
52
|
+
segment,
|
|
53
|
+
select_slice,
|
|
54
|
+
standardize_size,
|
|
55
|
+
)
|
|
56
|
+
from .config import ProcessingConfig
|
|
57
|
+
from .constants import tcia_dataset_to_info
|
|
58
|
+
from .dataset import Dataset
|
|
59
|
+
from .datasets import EXTRA_CT_COLLECTIONS, get_dataset_info, list_datasets
|
|
60
|
+
from .image import RadiologyImage
|
|
61
|
+
from .io import dicom_to_nifti, load_image, save_image
|
|
62
|
+
from .metadata import annotate, categorize_phase, categorize_region, summarize
|
|
63
|
+
from .qc import QCCriteria, QCResult, check_series_metadata, check_volume
|
|
64
|
+
|
|
65
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
66
|
+
from . import features, segmentation
|
|
67
|
+
from .tcia import download, list_collections
|
|
68
|
+
|
|
69
|
+
__all__ = [
|
|
70
|
+
"__version__",
|
|
71
|
+
# pipeline steps, in the order they run
|
|
72
|
+
"filter",
|
|
73
|
+
"orient",
|
|
74
|
+
"segment",
|
|
75
|
+
"clip",
|
|
76
|
+
"resample",
|
|
77
|
+
"select_slice",
|
|
78
|
+
"apply_mask",
|
|
79
|
+
"crop_to_content",
|
|
80
|
+
"standardize_size",
|
|
81
|
+
"normalize",
|
|
82
|
+
# whole protocols and output
|
|
83
|
+
"process",
|
|
84
|
+
"save",
|
|
85
|
+
"radiomics",
|
|
86
|
+
# core
|
|
87
|
+
"RadiologyImage",
|
|
88
|
+
"Dataset",
|
|
89
|
+
"ProcessingConfig",
|
|
90
|
+
# quality control
|
|
91
|
+
"QCCriteria",
|
|
92
|
+
"QCResult",
|
|
93
|
+
"check",
|
|
94
|
+
"check_volume",
|
|
95
|
+
"check_series_metadata",
|
|
96
|
+
# data access
|
|
97
|
+
"download",
|
|
98
|
+
"download_supplementary",
|
|
99
|
+
"list_collections",
|
|
100
|
+
"list_datasets",
|
|
101
|
+
"get_dataset_info",
|
|
102
|
+
"EXTRA_CT_COLLECTIONS",
|
|
103
|
+
"tcia_dataset_to_info",
|
|
104
|
+
# io
|
|
105
|
+
"load_image",
|
|
106
|
+
"save_image",
|
|
107
|
+
"dicom_to_nifti",
|
|
108
|
+
# metadata
|
|
109
|
+
"categorize_region",
|
|
110
|
+
"categorize_phase",
|
|
111
|
+
"annotate",
|
|
112
|
+
"summarize",
|
|
113
|
+
# submodules
|
|
114
|
+
"features",
|
|
115
|
+
"segmentation",
|
|
116
|
+
"tcia",
|
|
117
|
+
"metadata",
|
|
118
|
+
]
|
|
119
|
+
|
|
120
|
+
#: Attributes served on first use, so that importing the package does not pull
|
|
121
|
+
#: in `requests`, `pyradiomics` or `matplotlib`.
|
|
122
|
+
_LAZY = {
|
|
123
|
+
"download": ("tcia", "download"),
|
|
124
|
+
"download_supplementary": ("tcia", "download_supplementary"),
|
|
125
|
+
"list_collections": ("tcia", "list_collections"),
|
|
126
|
+
"download_with_nbia_retriever": ("tcia", "download_with_nbia_retriever"),
|
|
127
|
+
"get_series": ("tcia", "get_series"),
|
|
128
|
+
"extract_features": ("features", "extract_features"),
|
|
129
|
+
"segment_organs": ("segmentation", "segment_organs"),
|
|
130
|
+
}
|
|
131
|
+
_LAZY_MODULES = {"features", "segmentation", "tcia", "io", "qc", "datasets", "metadata"}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def __getattr__(name: str):
|
|
135
|
+
if name in _LAZY:
|
|
136
|
+
module_name, attribute = _LAZY[name]
|
|
137
|
+
return getattr(import_module(f".{module_name}", __name__), attribute)
|
|
138
|
+
if name in _LAZY_MODULES:
|
|
139
|
+
return import_module(f".{name}", __name__)
|
|
140
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def __dir__() -> list:
|
|
144
|
+
return sorted(set(__all__) | set(globals()))
|
ctkit/api.py
ADDED
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
"""The functional API: ``import ctkit`` and call the steps directly.
|
|
2
|
+
|
|
3
|
+
Every pipeline step is a function here, so a protocol can be written without
|
|
4
|
+
constructing anything::
|
|
5
|
+
|
|
6
|
+
import ctkit
|
|
7
|
+
|
|
8
|
+
scan = ctkit.orient("case/imaging.nii.gz")
|
|
9
|
+
ctkit.clip(scan, -200, 300)
|
|
10
|
+
ctkit.resample(scan, (0.8, 0.8, 3.0))
|
|
11
|
+
ctkit.save(scan, "processed/case.nii.gz")
|
|
12
|
+
|
|
13
|
+
Each function takes whatever you have — a path, a NumPy array, a
|
|
14
|
+
:class:`~ctkit.image.RadiologyImage`, a :class:`~ctkit.dataset.Dataset`, or a
|
|
15
|
+
directory of scans — and returns the same kind of thing, so the same call works
|
|
16
|
+
on one scan or on a cohort::
|
|
17
|
+
|
|
18
|
+
ctkit.orient(image) # -> RadiologyImage
|
|
19
|
+
ctkit.orient("data/raw") # -> Dataset, every series reoriented
|
|
20
|
+
|
|
21
|
+
These are the methods, not a reimplementation of them: ``ctkit.clip(image)``
|
|
22
|
+
does exactly what ``image.clip()`` does, including modifying the image in
|
|
23
|
+
place and returning it. Construct a :class:`~ctkit.dataset.Dataset` or a
|
|
24
|
+
:class:`~ctkit.image.RadiologyImage` instead when you would rather chain::
|
|
25
|
+
|
|
26
|
+
ctkit.Dataset("data/raw").filter(min_slices=25).orient().clip(-200, 300)
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import os
|
|
32
|
+
from collections.abc import Mapping
|
|
33
|
+
from typing import Any, Optional, Sequence, Union
|
|
34
|
+
|
|
35
|
+
from .dataset import Dataset
|
|
36
|
+
from .image import RadiologyImage
|
|
37
|
+
from .io import ImageLike, is_dicom_directory
|
|
38
|
+
from .qc import QCCriteria, QCResult, resolve_criteria
|
|
39
|
+
from .qc import check as _check_metadata
|
|
40
|
+
from .validation import (
|
|
41
|
+
Integer,
|
|
42
|
+
Interpolator,
|
|
43
|
+
Labels,
|
|
44
|
+
NormalizationMethod,
|
|
45
|
+
Number,
|
|
46
|
+
OutputFormat,
|
|
47
|
+
PathArg,
|
|
48
|
+
QCLevel,
|
|
49
|
+
QCPreset,
|
|
50
|
+
SliceMode,
|
|
51
|
+
Spacing,
|
|
52
|
+
validated,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
__all__ = [
|
|
56
|
+
"filter",
|
|
57
|
+
"check",
|
|
58
|
+
"orient",
|
|
59
|
+
"segment",
|
|
60
|
+
"clip",
|
|
61
|
+
"resample",
|
|
62
|
+
"select_slice",
|
|
63
|
+
"apply_mask",
|
|
64
|
+
"crop_to_content",
|
|
65
|
+
"standardize_size",
|
|
66
|
+
"normalize",
|
|
67
|
+
"process",
|
|
68
|
+
"save",
|
|
69
|
+
"radiomics",
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
#: Anything a function here accepts: one image, a cohort, or a path to either.
|
|
73
|
+
Target = Union[RadiologyImage, Dataset, ImageLike]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# ----------------------------------------------------------------------
|
|
77
|
+
# input handling
|
|
78
|
+
# ----------------------------------------------------------------------
|
|
79
|
+
def _resolve(target: Target) -> Union[RadiologyImage, Dataset]:
|
|
80
|
+
"""One image or a cohort, whichever the input describes.
|
|
81
|
+
|
|
82
|
+
A path to a file is one image; a directory, a metadata table or a list is
|
|
83
|
+
a cohort. A directory of DICOM slices is one image, not a cohort of one
|
|
84
|
+
per file.
|
|
85
|
+
"""
|
|
86
|
+
if isinstance(target, (RadiologyImage, Dataset)):
|
|
87
|
+
return target
|
|
88
|
+
if isinstance(target, (list, tuple)):
|
|
89
|
+
return Dataset(target)
|
|
90
|
+
if isinstance(target, (str, os.PathLike)):
|
|
91
|
+
path = str(target)
|
|
92
|
+
if os.path.isdir(path) and not is_dicom_directory(path):
|
|
93
|
+
return Dataset(path)
|
|
94
|
+
if path.lower().endswith((".csv", ".tsv")) or any(c in path for c in "*?["):
|
|
95
|
+
return Dataset(path)
|
|
96
|
+
return RadiologyImage(path)
|
|
97
|
+
return RadiologyImage(target)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _is_metadata(target: Any) -> bool:
|
|
101
|
+
"""True for a DICOM header or a metadata row, which carry no pixel data."""
|
|
102
|
+
if isinstance(target, (RadiologyImage, Dataset, str, os.PathLike, list, tuple)):
|
|
103
|
+
return False
|
|
104
|
+
return isinstance(target, Mapping) or callable(getattr(target, "keys", None))
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _step(name: str, subject: Target, /, *args: Any, **kwargs: Any):
|
|
108
|
+
"""Run one step, on an image or over a cohort.
|
|
109
|
+
|
|
110
|
+
Both classes carry the step under the same name, so this dispatches to the
|
|
111
|
+
method rather than reimplementing it — which also means the arguments are
|
|
112
|
+
validated here, before a cohort is even discovered.
|
|
113
|
+
|
|
114
|
+
Positional-only, so a step's own arguments (``target=``, ``name=``) cannot
|
|
115
|
+
collide with these.
|
|
116
|
+
"""
|
|
117
|
+
return getattr(_resolve(subject), name)(*args, **kwargs)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# ----------------------------------------------------------------------
|
|
121
|
+
# quality control
|
|
122
|
+
# ----------------------------------------------------------------------
|
|
123
|
+
@validated
|
|
124
|
+
def filter(
|
|
125
|
+
images: Target,
|
|
126
|
+
criteria: Union[QCCriteria, QCPreset, None] = None,
|
|
127
|
+
level: QCLevel = "all",
|
|
128
|
+
progress: bool = True,
|
|
129
|
+
**thresholds: Any,
|
|
130
|
+
) -> Dataset:
|
|
131
|
+
"""Drop series that fail quality control, returning the ones that passed.
|
|
132
|
+
|
|
133
|
+
`criteria` is a :class:`QCCriteria`, or the name of a preset
|
|
134
|
+
(``"default"``, ``"radiomics"``, ``"permissive"``). Individual thresholds
|
|
135
|
+
can also be given as keywords: ``ctkit.filter(data, min_slices=25)``.
|
|
136
|
+
|
|
137
|
+
`level` is ``"metadata"`` (headers only, reads no pixels), ``"volume"``, or
|
|
138
|
+
``"all"``. The result carries ``.qc_report``, the pass/fail table for every
|
|
139
|
+
series, and ``.rejected``, the images that failed.
|
|
140
|
+
"""
|
|
141
|
+
return Dataset(images).filter(
|
|
142
|
+
criteria, level=level, progress=progress, **thresholds
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@validated
|
|
147
|
+
def check(
|
|
148
|
+
target: Any,
|
|
149
|
+
criteria: Union[QCCriteria, QCPreset, None] = None,
|
|
150
|
+
level: QCLevel = "all",
|
|
151
|
+
progress: bool = True,
|
|
152
|
+
**thresholds: Any,
|
|
153
|
+
) -> Union[QCResult, Any]:
|
|
154
|
+
"""Run quality control without dropping anything.
|
|
155
|
+
|
|
156
|
+
On one image this is a :class:`QCResult` (``.passed``, ``.reason``,
|
|
157
|
+
``.stats``); on a cohort it is the full pass/fail table as a DataFrame. A
|
|
158
|
+
DICOM header or metadata row can be passed directly, which checks it
|
|
159
|
+
without reading any pixels.
|
|
160
|
+
"""
|
|
161
|
+
if _is_metadata(target):
|
|
162
|
+
return _check_metadata(None, resolve_criteria(criteria, **thresholds), metadata=target)
|
|
163
|
+
|
|
164
|
+
resolved = _resolve(target)
|
|
165
|
+
if isinstance(resolved, Dataset):
|
|
166
|
+
return resolved.check(criteria, level=level, progress=progress, **thresholds)
|
|
167
|
+
return resolved.check(criteria, level=level, **thresholds)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# ----------------------------------------------------------------------
|
|
171
|
+
# pipeline steps
|
|
172
|
+
# ----------------------------------------------------------------------
|
|
173
|
+
@validated
|
|
174
|
+
def orient(image: Target, target: str = "RAS"):
|
|
175
|
+
"""Reorient to a canonical anatomical axis order. See :meth:`RadiologyImage.orient`."""
|
|
176
|
+
return _step("orient", image, target=target)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@validated
|
|
180
|
+
def segment(
|
|
181
|
+
image: Target,
|
|
182
|
+
organs: Optional[Sequence[str]] = None,
|
|
183
|
+
task: str = "total",
|
|
184
|
+
tumor_mask: Optional[ImageLike] = None,
|
|
185
|
+
restrict_organs_to_tumor: bool = True,
|
|
186
|
+
fast: bool = False,
|
|
187
|
+
remove_small_blobs: bool = True,
|
|
188
|
+
fill_holes: bool = True,
|
|
189
|
+
morphological_closing: bool = True,
|
|
190
|
+
device: Optional[str] = None,
|
|
191
|
+
output_dir: Optional[PathArg] = None,
|
|
192
|
+
replace: bool = False,
|
|
193
|
+
):
|
|
194
|
+
"""Segment organs with TotalSegmentator. See :meth:`RadiologyImage.segment`.
|
|
195
|
+
|
|
196
|
+
On a cohort, `output_dir` holds one subdirectory per series.
|
|
197
|
+
"""
|
|
198
|
+
# Dataset.segment rather than _step: on a cohort it is the one that gives
|
|
199
|
+
# each series its own output subdirectory.
|
|
200
|
+
resolved = _resolve(image)
|
|
201
|
+
return resolved.segment(
|
|
202
|
+
organs=organs,
|
|
203
|
+
task=task,
|
|
204
|
+
tumor_mask=tumor_mask,
|
|
205
|
+
restrict_organs_to_tumor=restrict_organs_to_tumor,
|
|
206
|
+
fast=fast,
|
|
207
|
+
remove_small_blobs=remove_small_blobs,
|
|
208
|
+
fill_holes=fill_holes,
|
|
209
|
+
morphological_closing=morphological_closing,
|
|
210
|
+
device=device,
|
|
211
|
+
output_dir=output_dir,
|
|
212
|
+
replace=replace,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@validated
|
|
217
|
+
def clip(
|
|
218
|
+
image: Target,
|
|
219
|
+
min_value: Optional[Number] = -200,
|
|
220
|
+
max_value: Optional[Number] = 300,
|
|
221
|
+
):
|
|
222
|
+
"""Clamp intensities to a window in HU. See :meth:`RadiologyImage.clip`."""
|
|
223
|
+
return _step("clip", image, min_value=min_value, max_value=max_value)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
@validated
|
|
227
|
+
def resample(
|
|
228
|
+
image: Target,
|
|
229
|
+
spacing: Spacing = (0.8, 0.8, 3.0),
|
|
230
|
+
interpolator: Interpolator = "linear",
|
|
231
|
+
):
|
|
232
|
+
"""Resample onto a fixed voxel size in mm. See :meth:`RadiologyImage.resample`."""
|
|
233
|
+
return _step("resample", image, spacing=spacing, interpolator=interpolator)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
@validated
|
|
237
|
+
def select_slice(
|
|
238
|
+
image: Target,
|
|
239
|
+
mode: SliceMode = "mask",
|
|
240
|
+
index: Optional[Integer] = None,
|
|
241
|
+
label: Optional[Labels] = None,
|
|
242
|
+
keepdims: bool = False,
|
|
243
|
+
):
|
|
244
|
+
"""Reduce a volume to one axial slice. See :meth:`RadiologyImage.select_slice`."""
|
|
245
|
+
return _step(
|
|
246
|
+
"select_slice", image, mode=mode, index=index, label=label, keepdims=keepdims
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
@validated
|
|
251
|
+
def apply_mask(
|
|
252
|
+
image: Target,
|
|
253
|
+
labels: Optional[Labels] = None,
|
|
254
|
+
crop: bool = True,
|
|
255
|
+
padding: Integer = 5,
|
|
256
|
+
fill_value: Optional[Number] = None,
|
|
257
|
+
):
|
|
258
|
+
"""Blank outside the ROI and crop to it. See :meth:`RadiologyImage.apply_mask`."""
|
|
259
|
+
return _step(
|
|
260
|
+
"apply_mask", image, labels=labels, crop=crop, padding=padding, fill_value=fill_value
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
@validated
|
|
265
|
+
def crop_to_content(image: Target, threshold: Optional[Number] = None, padding: Integer = 5):
|
|
266
|
+
"""Crop to the voxels above `threshold`. See :meth:`RadiologyImage.crop_to_content`."""
|
|
267
|
+
return _step("crop_to_content", image, threshold=threshold, padding=padding)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
@validated
|
|
271
|
+
def standardize_size(
|
|
272
|
+
image: Target,
|
|
273
|
+
x: Optional[Integer] = None,
|
|
274
|
+
y: Optional[Integer] = None,
|
|
275
|
+
z: Optional[Integer] = None,
|
|
276
|
+
fill_value: Optional[Number] = None,
|
|
277
|
+
mask_fill_value: Number = 0,
|
|
278
|
+
):
|
|
279
|
+
"""Center-crop or pad to a fixed shape. See :meth:`RadiologyImage.standardize_size`."""
|
|
280
|
+
return _step(
|
|
281
|
+
"standardize_size",
|
|
282
|
+
image,
|
|
283
|
+
x=x,
|
|
284
|
+
y=y,
|
|
285
|
+
z=z,
|
|
286
|
+
fill_value=fill_value,
|
|
287
|
+
mask_fill_value=mask_fill_value,
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
@validated
|
|
292
|
+
def normalize(
|
|
293
|
+
image: Target,
|
|
294
|
+
method: NormalizationMethod = "volume",
|
|
295
|
+
mean: Optional[Number] = None,
|
|
296
|
+
std: Optional[Number] = None,
|
|
297
|
+
within_mask: bool = False,
|
|
298
|
+
):
|
|
299
|
+
"""Z-score the intensities. See :meth:`RadiologyImage.normalize`.
|
|
300
|
+
|
|
301
|
+
Over a cohort, ``method="dataset"`` needs pooled statistics, which
|
|
302
|
+
:func:`process` resolves for you; here `mean` and `std` have to be given.
|
|
303
|
+
"""
|
|
304
|
+
return _step(
|
|
305
|
+
"normalize", image, method=method, mean=mean, std=std, within_mask=within_mask
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
# ----------------------------------------------------------------------
|
|
310
|
+
# whole protocols, output, features
|
|
311
|
+
# ----------------------------------------------------------------------
|
|
312
|
+
@validated
|
|
313
|
+
def process(
|
|
314
|
+
target: Target,
|
|
315
|
+
config: Optional[Any] = None,
|
|
316
|
+
**kwargs: Any,
|
|
317
|
+
):
|
|
318
|
+
"""Run a whole protocol, in the order the steps are listed.
|
|
319
|
+
|
|
320
|
+
`config` is a :class:`ProcessingConfig`, the name of a collection whose
|
|
321
|
+
curated protocol to use (``ctkit.process(data, "tcga-kirc")``), or a path
|
|
322
|
+
to a saved YAML protocol. Any field can be overridden with a keyword.
|
|
323
|
+
|
|
324
|
+
Over a cohort this is :meth:`Dataset.process`, so it also takes `out_dir`,
|
|
325
|
+
`workers`, `skip_existing` and the rest, and it resolves the steps that
|
|
326
|
+
need cohort-level statistics.
|
|
327
|
+
"""
|
|
328
|
+
return _resolve(target).process(config, **kwargs)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
@validated
|
|
332
|
+
def save(
|
|
333
|
+
target: Target,
|
|
334
|
+
path: PathArg,
|
|
335
|
+
output_format: OutputFormat = "nifti",
|
|
336
|
+
compress: bool = True,
|
|
337
|
+
**kwargs: Any,
|
|
338
|
+
):
|
|
339
|
+
"""Write to `path` — a file for one image, a directory for a cohort.
|
|
340
|
+
|
|
341
|
+
NIfTI (``.nii.gz``, or ``.nii`` with ``compress=False``) or a NumPy
|
|
342
|
+
``.npy`` array. Remaining keywords go to the method: `mask_path` and
|
|
343
|
+
`save_mask` for one image, `layout` and `progress` for a cohort.
|
|
344
|
+
"""
|
|
345
|
+
return _resolve(target).save(
|
|
346
|
+
path, output_format=output_format, compress=compress, **kwargs
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
@validated
|
|
351
|
+
def radiomics(
|
|
352
|
+
target: Target,
|
|
353
|
+
labels: Labels = (1, 2),
|
|
354
|
+
params: Optional[Union[str, dict]] = None,
|
|
355
|
+
**kwargs: Any,
|
|
356
|
+
):
|
|
357
|
+
"""Extract PyRadiomics features: a dict for one image, a DataFrame for a cohort."""
|
|
358
|
+
return _resolve(target).radiomics(labels=labels, params=params, **kwargs)
|