Skip to content

Commit 9bc4cd6

Browse files
author
staydelight
committed
Fixes #7557
Add a function to create a JSON file that maps input and output paths. Signed-off-by: staydelight <kevin295643815697236@gmail.com>
1 parent daf2e71 commit 9bc4cd6

2 files changed

Lines changed: 99 additions & 27 deletions

File tree

monai/data/image_reader.py

Lines changed: 72 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,19 @@
1111

1212
from __future__ import annotations
1313

14+
import json
15+
import logging
16+
import sys
1417
import glob
1518
import os
16-
import re
1719
import warnings
1820
from abc import ABC, abstractmethod
1921
from collections.abc import Callable, Iterable, Iterator, Sequence
2022
from dataclasses import dataclass
2123
from pathlib import Path
2224
from typing import TYPE_CHECKING, Any
2325

26+
from monai.apps.utils import get_logger
2427
import numpy as np
2528
from torch.utils.data._utils.collate import np_str_obj_array_pattern
2629

@@ -51,6 +54,16 @@
5154
pydicom, has_pydicom = optional_import("pydicom")
5255
nrrd, has_nrrd = optional_import("nrrd", allow_namespace_pkg=True)
5356

57+
DEFAULT_FMT = "%(asctime)s %(levelname)s %(filename)s:%(lineno)d - %(message)s"
58+
59+
logger = get_logger(module_name=__name__, fmt=DEFAULT_FMT)
60+
logger = logging.getLogger(__name__)
61+
handler = logging.StreamHandler(sys.stdout)
62+
handler.setFormatter(logging.Formatter('%(asctime)s %(levelname)s %(message)s'))
63+
logger.addHandler(handler)
64+
logger.setLevel(logging.DEBUG)
65+
66+
5467
__all__ = ["ImageReader", "ITKReader", "NibabelReader", "NumpyReader", "PILReader", "PydicomReader", "NrrdReader"]
5568

5669

@@ -98,8 +111,10 @@ def read(self, data: Sequence[PathLike] | PathLike, **kwargs) -> Sequence[Any] |
98111
kwargs: additional args for actual `read` API of 3rd party libs.
99112
100113
"""
114+
#self.update_json(input_file=data)
101115
raise NotImplementedError(f"Subclass {self.__class__.__name__} must implement this method.")
102116

117+
103118
@abstractmethod
104119
def get_data(self, img) -> tuple[np.ndarray, dict]:
105120
"""
@@ -147,6 +162,24 @@ def _stack_images(image_list: list, meta_dict: dict):
147162
meta_dict[MetaKeys.ORIGINAL_CHANNEL_DIM] = 0
148163
return np.stack(image_list, axis=0)
149164

165+
def update_json(input_file=None, output_file=None):
166+
record_path = "img-label.json"
167+
168+
if not os.path.exists(record_path) or os.stat(record_path).st_size == 0:
169+
with open(record_path, 'w') as f:
170+
json.dump([], f)
171+
172+
with open(record_path, 'r+') as f:
173+
records = json.load(f)
174+
if input_file:
175+
new_record = {"image": input_file, "label": []}
176+
records.append(new_record)
177+
elif output_file and records:
178+
records[-1]["label"].append(output_file)
179+
180+
f.seek(0)
181+
json.dump(records, f, indent=4)
182+
150183

151184
@require_pkg(pkg_name="itk")
152185
class ITKReader(ImageReader):
@@ -168,8 +201,8 @@ class ITKReader(ImageReader):
168201
series_name: the name of the DICOM series if there are multiple ones.
169202
used when loading DICOM series.
170203
reverse_indexing: whether to use a reversed spatial indexing convention for the returned data array.
171-
If ``False``, the spatial indexing convention is reversed to be compatible with ITK;
172-
otherwise, the spatial indexing follows the numpy convention. Default is ``False``.
204+
If ``False``, the spatial indexing follows the numpy convention;
205+
otherwise, the spatial indexing convention is reversed to be compatible with ITK. Default is ``False``.
173206
This option does not affect the metadata.
174207
series_meta: whether to load the metadata of the DICOM series (using the metadata from the first slice).
175208
This flag is checked only when loading DICOM series. Default is ``False``.
@@ -225,6 +258,7 @@ def read(self, data: Sequence[PathLike] | PathLike, **kwargs):
225258
img_ = []
226259

227260
filenames: Sequence[PathLike] = ensure_tuple(data)
261+
update_json(input_file=filenames)
228262
kwargs_ = self.kwargs.copy()
229263
kwargs_.update(kwargs)
230264
for name in filenames:
@@ -332,6 +366,25 @@ def _get_affine(self, img, lps_to_ras: bool = True):
332366
affine[:sr, -1] = origin[:sr]
333367
if lps_to_ras:
334368
affine = orientation_ras_lps(affine)
369+
logger.debug("lps is changed to ras")
370+
371+
# 使用 Logger 輸出信息
372+
373+
logger.info("\nOrigin[:sr]:")
374+
logger.info(", ".join(f"{x:.10f}" for x in origin[:sr]))
375+
376+
logger.info("\nDirection[:sr, :sr]:")
377+
for row in direction[:sr, :sr]:
378+
logger.info(", ".join(f"{x:.15f}" for x in row))
379+
380+
logger.info("\nSpacing[:sr]:")
381+
logger.info(", ".join(f"{x:.15f}" for x in spacing[:sr]))
382+
383+
384+
# affine = numpy.round(affine, decimals=5)
385+
386+
logger.debug(f"Affine matrix:\n{affine}")
387+
335388
return affine
336389

337390
def _get_spatial_shape(self, img):
@@ -404,12 +457,8 @@ class PydicomReader(ImageReader):
404457
label_dict: label of the dicom data. If provided, it will be used when loading segmentation data.
405458
Keys of the dict are the classes, and values are the corresponding class number. For example:
406459
for TCIA collection "C4KC-KiTS", it can be: {"Kidney": 0, "Renal Tumor": 1}.
407-
fname_regex: a regular expression to match the file names when the input is a folder.
408-
If provided, only the matched files will be included. For example, to include the file name
409-
"image_0001.dcm", the regular expression could be `".*image_(\\d+).dcm"`. Default to `""`.
410-
Set it to `None` to use `pydicom.misc.is_dicom` to match valid files.
411460
kwargs: additional args for `pydicom.dcmread` API. more details about available args:
412-
https://pydicom.github.io/pydicom/stable/reference/generated/pydicom.filereader.dcmread.html
461+
https://pydicom.github.io/pydicom/stable/reference/generated/pydicom.filereader.dcmread.html#pydicom.filereader.dcmread
413462
If the `get_data` function will be called
414463
(for example, when using this reader with `monai.transforms.LoadImage`), please ensure that the argument
415464
`stop_before_pixels` is `True`, and `specific_tags` covers all necessary tags, such as `PixelSpacing`,
@@ -423,7 +472,6 @@ def __init__(
423472
swap_ij: bool = True,
424473
prune_metadata: bool = True,
425474
label_dict: dict | None = None,
426-
fname_regex: str = "",
427475
**kwargs,
428476
):
429477
super().__init__()
@@ -433,7 +481,6 @@ def __init__(
433481
self.swap_ij = swap_ij
434482
self.prune_metadata = prune_metadata
435483
self.label_dict = label_dict
436-
self.fname_regex = fname_regex
437484

438485
def verify_suffix(self, filename: Sequence[PathLike] | PathLike) -> bool:
439486
"""
@@ -465,6 +512,7 @@ def read(self, data: Sequence[PathLike] | PathLike, **kwargs):
465512
img_ = []
466513

467514
filenames: Sequence[PathLike] = ensure_tuple(data)
515+
update_json(input_file=filenames)
468516
kwargs_ = self.kwargs.copy()
469517
kwargs_.update(kwargs)
470518

@@ -474,16 +522,9 @@ def read(self, data: Sequence[PathLike] | PathLike, **kwargs):
474522
name = f"{name}"
475523
if Path(name).is_dir():
476524
# read DICOM series
477-
if self.fname_regex is not None:
478-
series_slcs = [slc for slc in glob.glob(os.path.join(name, "*")) if re.match(self.fname_regex, slc)]
479-
else:
480-
series_slcs = [slc for slc in glob.glob(os.path.join(name, "*")) if pydicom.misc.is_dicom(slc)]
481-
slices = []
482-
for slc in series_slcs:
483-
try:
484-
slices.append(pydicom.dcmread(fp=slc, **kwargs_))
485-
except pydicom.errors.InvalidDicomError as e:
486-
warnings.warn(f"Failed to read {slc} with exception: \n{e}.", stacklevel=2)
525+
series_slcs = glob.glob(os.path.join(name, "*"))
526+
series_slcs = [slc for slc in series_slcs if "LICENSE" not in slc]
527+
slices = [pydicom.dcmread(fp=slc, **kwargs_) for slc in series_slcs]
487528
img_.append(slices if len(slices) > 1 else slices[0])
488529
if len(slices) > 1:
489530
self.has_series = True
@@ -913,9 +954,11 @@ def read(self, data: Sequence[PathLike] | PathLike, **kwargs):
913954
https://github.com/nipy/nibabel/blob/master/nibabel/loadsave.py
914955
915956
"""
957+
logger.info(f"Reading NIfTI data from: {data}")
916958
img_: list[Nifti1Image] = []
917959

918960
filenames: Sequence[PathLike] = ensure_tuple(data)
961+
update_json(input_file=filenames)
919962
kwargs_ = self.kwargs.copy()
920963
kwargs_.update(kwargs)
921964
for name in filenames:
@@ -1076,13 +1119,14 @@ def read(self, data: Sequence[PathLike] | PathLike, **kwargs):
10761119
img_: list[Nifti1Image] = []
10771120

10781121
filenames: Sequence[PathLike] = ensure_tuple(data)
1122+
update_json(input_file=filenames)
10791123
kwargs_ = self.kwargs.copy()
10801124
kwargs_.update(kwargs)
10811125
for name in filenames:
10821126
img = np.load(name, allow_pickle=True, **kwargs_)
10831127
if Path(name).name.endswith(".npz"):
10841128
# load expected items from NPZ file
1085-
npz_keys = list(img.keys()) if self.npz_keys is None else self.npz_keys
1129+
npz_keys = [f"arr_{i}" for i in range(len(img))] if self.npz_keys is None else self.npz_keys
10861130
for k in npz_keys:
10871131
img_.append(img[k])
10881132
else:
@@ -1173,6 +1217,7 @@ def read(self, data: Sequence[PathLike] | PathLike | np.ndarray, **kwargs):
11731217
img_: list[PILImage.Image] = []
11741218

11751219
filenames: Sequence[PathLike] = ensure_tuple(data)
1220+
update_json(input_file=filenames)
11761221
kwargs_ = self.kwargs.copy()
11771222
kwargs_.update(kwargs)
11781223
for name in filenames:
@@ -1297,10 +1342,11 @@ def read(self, data: Sequence[PathLike] | PathLike, **kwargs) -> Sequence[Any] |
12971342
"""
12981343
img_: list = []
12991344
filenames: Sequence[PathLike] = ensure_tuple(data)
1345+
update_json(input_file=filenames)
13001346
kwargs_ = self.kwargs.copy()
13011347
kwargs_.update(kwargs)
13021348
for name in filenames:
1303-
nrrd_image = NrrdImage(*nrrd.read(name, index_order=self.index_order, **kwargs_))
1349+
nrrd_image = NrrdImage(*nrrd.read(name, index_order=self.index_order, *kwargs_))
13041350
img_.append(nrrd_image)
13051351
return img_ if len(filenames) > 1 else img_[0]
13061352

@@ -1323,7 +1369,7 @@ def get_data(self, img: NrrdImage | list[NrrdImage]) -> tuple[np.ndarray, dict]:
13231369
header = dict(i.header)
13241370
if self.index_order == "C":
13251371
header = self._convert_f_to_c_order(header)
1326-
header[MetaKeys.ORIGINAL_AFFINE] = self._get_affine(header)
1372+
header[MetaKeys.ORIGINAL_AFFINE] = self._get_affine(i)
13271373

13281374
if self.affine_lps_to_ras:
13291375
header = self._switch_lps_ras(header)
@@ -1344,7 +1390,7 @@ def get_data(self, img: NrrdImage | list[NrrdImage]) -> tuple[np.ndarray, dict]:
13441390

13451391
return _stack_images(img_array, compatible_meta), compatible_meta
13461392

1347-
def _get_affine(self, header: dict) -> np.ndarray:
1393+
def _get_affine(self, img: NrrdImage) -> np.ndarray:
13481394
"""
13491395
Get the affine matrix of the image, it can be used to correct
13501396
spacing, orientation or execute spatial transforms.
@@ -1353,8 +1399,8 @@ def _get_affine(self, header: dict) -> np.ndarray:
13531399
img: A `NrrdImage` loaded from image file
13541400
13551401
"""
1356-
direction = header["space directions"]
1357-
origin = header["space origin"]
1402+
direction = img.header["space directions"]
1403+
origin = img.header["space origin"]
13581404

13591405
x, y = direction.shape
13601406
affine_diam = min(x, y) + 1

monai/data/image_writer.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
from typing import TYPE_CHECKING, Any, cast
1616

1717
import numpy as np
18+
import os
19+
import json
1820

1921
from monai.apps.utils import get_logger
2022
from monai.config import DtypeLike, NdarrayOrTensor, PathLike
@@ -196,6 +198,25 @@ def write(self, filename: PathLike, verbose: bool = True, **kwargs):
196198
if verbose:
197199
logger.info(f"writing: {filename}")
198200

201+
def update_json(self, input_file=None, output_file=None):
202+
record_path = "img-label.json"
203+
204+
if not os.path.exists(record_path) or os.stat(record_path).st_size == 0:
205+
with open(record_path, 'w') as f:
206+
json.dump([], f)
207+
208+
with open(record_path, 'r+') as f:
209+
records = json.load(f)
210+
if input_file:
211+
new_record = {"image": input_file, "label": []}
212+
records.append(new_record)
213+
elif output_file and records:
214+
records[-1]["label"].append(output_file)
215+
216+
f.seek(0)
217+
json.dump(records, f, indent=4)
218+
219+
199220
@classmethod
200221
def create_backend_obj(cls, data_array: NdarrayOrTensor, **kwargs) -> np.ndarray:
201222
"""
@@ -276,7 +297,7 @@ def resample_if_needed(
276297
# convert back at the end
277298
if isinstance(output_array, MetaTensor):
278299
output_array.applied_operations = []
279-
data_array, *_ = convert_data_type(output_array, output_type=orig_type)
300+
data_array, *_ = convert_data_type(output_array, output_type=orig_type) # type: ignore
280301
affine, *_ = convert_data_type(output_array.affine, output_type=orig_type) # type: ignore
281302
return data_array[0], affine
282303

@@ -462,7 +483,9 @@ def write(self, filename: PathLike, verbose: bool = False, **kwargs):
462483
463484
- https://github.com/InsightSoftwareConsortium/ITK/blob/v5.2.1/Wrapping/Generators/Python/itk/support/extras.py#L809
464485
"""
486+
logger.info(f"ITKWriter is processing the file: {filename}")
465487
super().write(filename, verbose=verbose)
488+
super().update_json(output_file=filename)
466489
self.data_obj = self.create_backend_obj(
467490
cast(NdarrayOrTensor, self.data_obj),
468491
channel_dim=self.channel_dim,
@@ -625,7 +648,9 @@ def write(self, filename: PathLike, verbose: bool = False, **obj_kwargs):
625648
626649
- https://nipy.org/nibabel/reference/nibabel.nifti1.html#nibabel.nifti1.save
627650
"""
651+
logger.info(f"NibabelWriter is processing the file: {filename}")
628652
super().write(filename, verbose=verbose)
653+
super().update_json(output_file=filename)
629654
self.data_obj = self.create_backend_obj(
630655
cast(NdarrayOrTensor, self.data_obj), affine=self.affine, dtype=self.output_dtype, **obj_kwargs
631656
)
@@ -771,6 +796,7 @@ def write(self, filename: PathLike, verbose: bool = False, **kwargs):
771796
- https://pillow.readthedocs.io/en/stable/reference/Image.html#PIL.Image.Image.save
772797
"""
773798
super().write(filename, verbose=verbose)
799+
super().update_json(output_file=filename)
774800
self.data_obj = self.create_backend_obj(
775801
data_array=self.data_obj,
776802
dtype=self.output_dtype,

0 commit comments

Comments
 (0)