forked from Project-MONAI/MONAI
-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathutils.py
More file actions
353 lines (280 loc) · 11.9 KB
/
Copy pathutils.py
File metadata and controls
353 lines (280 loc) · 11.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
# Copyright (c) MONAI Consortium
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
import os
import pickle
import sys
import warnings
from copy import deepcopy
from numbers import Number
from typing import Any, cast
import numpy as np
import torch
from monai.auto3dseg import Algo
from monai.bundle.config_parser import ConfigParser
from monai.bundle.utils import ID_SEP_KEY
from monai.data.meta_tensor import MetaTensor
from monai.transforms import CropForeground, ToCupy
from monai.utils import min_version, optional_import
__all__ = [
"get_foreground_image",
"get_foreground_label",
"get_label_ccp",
"concat_val_to_np",
"concat_multikeys_to_dict",
"datafold_read",
"verify_report_format",
"algo_to_pickle",
"algo_from_pickle",
]
measure_np, has_measure = optional_import("skimage.measure", "0.14.2", min_version)
cp, has_cp = optional_import("cupy")
def get_foreground_image(image: MetaTensor) -> np.ndarray:
"""
Get a foreground image by removing all-zero rectangles on the edges of the image
Note for the developer: update select_fn if the foreground is defined differently.
Args:
image: ndarray image to segment.
Returns:
ndarray of foreground image by removing all-zero edges.
Notes:
the size of the output is smaller than the input.
"""
copper = CropForeground(select_fn=lambda x: x > 0)
image_foreground = copper(image)
return cast(np.ndarray, image_foreground)
def get_foreground_label(image: MetaTensor, label: MetaTensor) -> MetaTensor:
"""
Get foreground image pixel values and mask out the non-labeled area.
Args
image: ndarray image to segment.
label: ndarray the image input and annotated with class IDs.
Returns:
1D array of foreground image with label > 0
"""
label_foreground = MetaTensor(image[label > 0])
return label_foreground
def get_label_ccp(mask_index: MetaTensor, use_gpu: bool = True) -> tuple[list[Any], int]:
"""
Find all connected components and their bounding shape. Backend can be cuPy/cuCIM or Numpy
depending on the hardware.
Args:
mask_index: a binary mask.
use_gpu: a switch to use GPU/CUDA or not. If GPU is unavailable, CPU will be used
regardless of this setting.
"""
cucim, has_cucim = optional_import("cucim")
shape_list = []
if mask_index.device.type == "cuda" and has_cp and has_cucim and use_gpu:
mask_cupy = ToCupy()(mask_index.short())
labeled = cucim.skimage.measure.label(mask_cupy)
vals = cp.unique(labeled[cp.nonzero(labeled)])
for ncomp in vals:
comp_idx = cp.argwhere(labeled == ncomp)
comp_idx_min = cp.min(comp_idx, axis=0).tolist()
comp_idx_max = cp.max(comp_idx, axis=0).tolist()
bbox_shape = [comp_idx_max[i] - comp_idx_min[i] + 1 for i in range(len(comp_idx_max))]
shape_list.append(bbox_shape)
ncomponents = len(vals)
del mask_cupy, labeled, vals, comp_idx, ncomp
cp.get_default_memory_pool().free_all_blocks()
elif has_measure:
labeled, ncomponents = measure_np.label(mask_index.data.cpu().numpy(), background=-1, return_num=True)
for ncomp in range(1, ncomponents + 1):
comp_idx = np.argwhere(labeled == ncomp)
comp_idx_min = np.min(comp_idx, axis=0).tolist()
comp_idx_max = np.max(comp_idx, axis=0).tolist()
bbox_shape = [comp_idx_max[i] - comp_idx_min[i] + 1 for i in range(len(comp_idx_max))]
shape_list.append(bbox_shape)
else:
raise RuntimeError("Cannot find one of the following required dependencies: {cuPy+cuCIM} or {scikit-image}")
return shape_list, ncomponents
def concat_val_to_np(
data_list: list[dict],
fixed_keys: list[str | int],
ragged: bool | None = False,
allow_missing: bool | None = False,
**kwargs: Any,
) -> np.ndarray:
"""
Get the nested value in a list of dictionary that shares the same structure.
Args:
data_list: a list of dictionary {key1: {key2: np.ndarray}}.
fixed_keys: a list of keys that records to path to the value in the dict elements.
ragged: if True, numbers can be in list of lists or ragged format so concat mode needs change.
allow_missing: if True, it will return a None if the value cannot be found.
Returns:
nd.array of concatenated array.
"""
np_list: list[np.ndarray | None] = []
for data in data_list:
parser = ConfigParser(data)
for i, key in enumerate(fixed_keys):
fixed_keys[i] = str(key)
val: Any
val = parser.get(ID_SEP_KEY.join(fixed_keys)) # type: ignore
if val is None:
if allow_missing:
np_list.append(None)
else:
raise AttributeError(f"{fixed_keys} is not nested in the dictionary")
elif isinstance(val, list):
np_list.append(np.array(val))
elif isinstance(val, (torch.Tensor, MetaTensor)):
np_list.append(val.cpu().numpy())
elif isinstance(val, np.ndarray):
np_list.append(val)
elif isinstance(val, Number):
np_list.append(np.array(val))
else:
raise NotImplementedError(f"{val.__class__} concat is not supported.")
if allow_missing:
np_list = [x for x in np_list if x is not None]
if len(np_list) == 0:
return np.array([0])
elif ragged:
return np.concatenate(np_list, **kwargs) # type: ignore
else:
return np.concatenate([np_list], **kwargs)
def concat_multikeys_to_dict(
data_list: list[dict], fixed_keys: list[str | int], keys: list[str], zero_insert: bool = True, **kwargs: Any
) -> dict[str, np.ndarray]:
"""
Get the nested value in a list of dictionary that shares the same structure iteratively on all keys.
It returns a dictionary with keys with the found values in nd.ndarray.
Args:
data_list: a list of dictionary {key1: {key2: np.ndarray}}.
fixed_keys: a list of keys that records to path to the value in the dict elements.
keys: a list of string keys that will be iterated to generate a dict output.
zero_insert: insert a zero in the list so that it can find the value in element 0 before getting the keys
flatten: if True, numbers are flattened before concat.
Returns:
a dict with keys - nd.array of concatenated array pair.
"""
ret_dict = {}
for key in keys:
addon: list[str | int] = [0, key] if zero_insert else [key]
val = concat_val_to_np(data_list, fixed_keys + addon, **kwargs)
ret_dict.update({key: val})
return ret_dict
def datafold_read(datalist: str | dict, basedir: str, fold: int = 0, key: str = "training") -> tuple[list, list]:
"""
Read a list of data dictionary `datalist`
Args:
datalist: the name of a JSON file listing the data, or a dictionary.
basedir: directory of image files.
fold: which fold to use (0..1 if in training set).
key: usually 'training' , but can try 'validation' or 'testing' to get the list data without labels (used in challenges).
Returns:
A tuple of two arrays (training, validation).
"""
if isinstance(datalist, str):
json_data = ConfigParser.load_config_file(datalist)
else:
json_data = datalist
dict_data = deepcopy(json_data[key])
for d in dict_data:
for k, _ in d.items():
if isinstance(d[k], list):
d[k] = [os.path.join(basedir, iv) for iv in d[k]]
elif isinstance(d[k], str):
d[k] = os.path.join(basedir, d[k]) if len(d[k]) > 0 else d[k]
tr = []
val = []
for d in dict_data:
if "fold" in d and d["fold"] == fold:
val.append(d)
else:
tr.append(d)
return tr, val
def verify_report_format(report: dict, report_format: dict) -> bool:
"""
Compares the report and the report_format that has only keys.
Args:
report: dict that has real values.
report_format: dict that only has keys and list-nested value.
"""
for k_fmt, v_fmt in report_format.items():
if k_fmt not in report:
return False
v = report[k_fmt]
if isinstance(v_fmt, list) and isinstance(v, list):
if len(v_fmt) != 1:
raise UserWarning("list length in report_format is not 1")
if len(v_fmt) > 0 and len(v) > 0:
return verify_report_format(v[0], v_fmt[0])
else:
return False
return True
def algo_to_pickle(algo: Algo, **algo_meta_data: Any) -> str:
"""
Export the Algo object to pickle file
Args:
algo: Algo-like object
algo_meta_data: additional keyword to save into the dictionary. It may include template_path
which is used to instantiate the class. It may also include model training info
such as acc/best_metrics
Returns:
filename of the pickled Algo object
"""
data = {"algo_bytes": pickle.dumps(algo)}
pkl_filename = os.path.join(algo.get_output_path(), "algo_object.pkl")
for k, v in algo_meta_data.items():
data.update({k: v})
data_bytes = pickle.dumps(data)
with open(pkl_filename, "wb") as f_pi:
f_pi.write(data_bytes)
return pkl_filename
def algo_from_pickle(pkl_filename: str, **kwargs: Any) -> Any:
"""
Import the Algo object from a pickle file
Args:
pkl_filename: name of the pickle file
algo_templates_dir: the algorithm script folder which is needed to instantiate the object.
If it is None, the function will use the internal ``'algo_templates_dir`` in the object
dict.
Returns:
algo: Algo-like object
Raises:
ValueError if the pkl_filename does not contain a dict, or the dict does not contain
``template_path`` or ``algo_bytes``
"""
with open(pkl_filename, "rb") as f_pi:
data_bytes = f_pi.read()
data = pickle.loads(data_bytes)
if not isinstance(data, dict):
raise ValueError(f"the data object is {data.__class__}. Dict is expected.")
if "algo_bytes" not in data:
raise ValueError(f"key [algo_bytes] not found in {data}. Unable to instantiate.")
algo_bytes = data.pop("algo_bytes")
algo_meta_data = {}
if "template_path" in kwargs: # add template_path to sys.path
template_path = kwargs["template_path"]
if template_path is None: # then load template_path from pickled data
if "template_path" not in data:
raise ValueError(f"key [template_path] not found in {data}")
template_path = data.pop("template_path")
if not os.path.isdir(template_path):
raise ValueError(f"Algorithm templates {template_path} is not a directory")
# Example of template path: "algorithm_templates/dints".
sys.path.insert(0, os.path.abspath(os.path.join(template_path, "..")))
algo_meta_data.update({"template_path": template_path})
algo = pickle.loads(algo_bytes)
pkl_dir = os.path.dirname(pkl_filename)
if pkl_dir != algo.get_output_path():
warnings.warn(
f"{algo.get_output_path()} does not contain {pkl_filename}."
f"Now override the Algo output_path with: {pkl_dir}"
)
algo.output_path = pkl_dir
for k, v in data.items():
algo_meta_data.update({k: v})
return algo, algo_meta_data