Skip to content

Commit beca197

Browse files
authored
Merge pull request #157 from AustralianCancerDataNetwork/pydicer-class-improvements
Add functions to PyDicer class and add some docs
2 parents 9e40815 + e36a759 commit beca197

File tree

7 files changed

+153
-36
lines changed

7 files changed

+153
-36
lines changed

docs/index.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,12 @@
2020
:maxdepth: 5
2121
:hidden:
2222

23+
tool
2324
input
2425
config
2526
preprocess
2627
convert
2728
visualise
2829
dataset
2930
analyse
31+
nnunet

docs/nnunet.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#####################
2+
nnUNet
3+
#####################
4+
5+
.. autoclass:: pydicer.dataset.nnunet.NNUNetDataset
6+
:members:

docs/tool.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#####################
2+
PyDicer
3+
#####################
4+
5+
6+
.. autoclass:: pydicer.tool.PyDicer
7+
:members:
8+

pydicer/dataset/nnunet.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,13 @@ def __init__(
4747
image_modality: str = "CT",
4848
mapping_id=DEFAULT_MAPPING_ID,
4949
):
50-
"""_summary_
50+
"""Prepare a dataset to train models using nnUNet.
51+
52+
Ensure that nnUNet is installed in your Python environment.
53+
For details on nnUNet see: https://github.com/MIC-DKFZ/nnUNet
54+
55+
> Note: This class currently support nnUNet v1. Contributions welcome to add support for
56+
nnUNet v2.
5157
5258
Args:
5359
working_directory (Union[str, Path]): The PyDicer working directory
@@ -502,8 +508,6 @@ def prepare_dataset(self) -> Path:
502508
target_label_path = label_ts_path.joinpath(f"{pat_id}.nii.gz")
503509
sitk.WriteImage(pat_label_map, str(target_label_path))
504510

505-
506-
507511
# write JSON file
508512
dataset_dict = {
509513
"name": self.nnunet_name,

pydicer/dataset/preparation.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@
1414

1515

1616
class PrepareDataset:
17+
"""
18+
Class that provides functionality for prepartion of subsets of data.
19+
20+
Args:
21+
- working_directory (str|pathlib.Path, optional): Main working directory for pydicer.
22+
Defaults to ".".
23+
"""
24+
1725
def __init__(self, working_directory="."):
1826
self.working_directory = Path(working_directory)
1927

pydicer/tool.py

Lines changed: 77 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
from logging.handlers import RotatingFileHandler
44
from pathlib import Path
55

6+
import pandas as pd
7+
68
from pydicer.config import PyDicerConfig
79
from pydicer.constants import CONVERTED_DIR_NAME, PYDICER_DIR_NAME
810

@@ -14,10 +16,31 @@
1416
from pydicer.dataset.preparation import PrepareDataset
1517
from pydicer.analyse.data import AnalyseData
1618

19+
from pydicer.utils import read_converted_data, add_structure_name_mapping, copy_doc
20+
21+
from pydicer.generate.object import add_object, add_structure_object, add_dose_object
22+
from pydicer.generate.segmentation import (
23+
read_all_segmentation_logs,
24+
segment_image,
25+
segment_dataset,
26+
)
27+
1728
logger = logging.getLogger()
1829

1930

2031
class PyDicer:
32+
"""The PyDicer class provides easy access to all the key PyDicer functionality.
33+
34+
Args:
35+
working_directory (str|pathlib.Path, optional): Directory in which data is stored. Defaults
36+
to ".".
37+
38+
:ivar convert: Instance of :class:`~pydicer.convert.data.ConvertData`
39+
:ivar visualise: Instance of :class:`~pydicer.visualise.data.VisualiseData`
40+
:ivar dataset: Instance of :class:`~pydicer.dataset.preparation.PrepareDataset`
41+
:ivar analyse: Instance of :class:`~pydicer.analyse.data.AnalyseData`
42+
"""
43+
2144
def __init__(self, working_directory="."):
2245

2346
self.working_directory = Path(working_directory)
@@ -135,8 +158,8 @@ def preprocess(self, force=True):
135158
if len(self.dicom_directories) == 0:
136159
raise ValueError("No DICOM input locations set. Add one using the add_input function.")
137160

138-
pd = PreprocessData(self.working_directory)
139-
pd.preprocess(self.dicom_directories, force=force)
161+
preprocess_data = PreprocessData(self.working_directory)
162+
preprocess_data.preprocess(self.dicom_directories, force=force)
140163

141164
self.preprocessed_data = read_preprocessed_data(self.working_directory)
142165

@@ -161,33 +184,60 @@ def run_pipeline(self, patient=None, force=True):
161184
)
162185
self.analyse.compute_dvh(dataset_name=CONVERTED_DIR_NAME, patient=patient, force=force)
163186

164-
# Object generation (insert in dataset(s) or all data)
165-
def add_object_to_dataset(
166-
self,
167-
uid,
168-
patient_id,
169-
obj_type,
170-
modality,
171-
for_uid=None,
172-
referenced_sop_instance_uid=None,
173-
datasets=None,
174-
):
175-
"""_summary_
187+
@copy_doc(add_structure_name_mapping, remove_args=["working_directory"])
188+
def add_structure_name_mapping( # pylint: disable=missing-function-docstring
189+
self, *args, **kwargs
190+
) -> pd.DataFrame:
176191

177-
Args:
178-
uid (_type_): _description_
179-
patient_id (_type_): _description_
180-
obj_type (_type_): _description_
181-
modality (_type_): _description_
182-
for_uid (_type_, optional): _description_. Defaults to None.
183-
referenced_sop_instance_uid (_type_, optional): _description_. Defaults to None.
184-
datasets (_type_, optional): _description_. Defaults to None.
185-
"""
192+
return add_structure_name_mapping(
193+
*args, working_directory=self.working_directory, **kwargs
194+
)
195+
196+
@copy_doc(read_converted_data, remove_args=["working_directory"])
197+
def read_converted_data( # pylint: disable=missing-function-docstring
198+
self, *_, **kwargs
199+
) -> pd.DataFrame:
200+
201+
return read_converted_data(working_directory=self.working_directory, **kwargs)
202+
203+
@copy_doc(add_object, remove_args=["working_directory"])
204+
def add_object( # pylint: disable=missing-function-docstring
205+
self, *args, **kwargs
206+
) -> pd.DataFrame:
207+
208+
return add_object(self.working_directory, *args, **kwargs)
209+
210+
@copy_doc(add_structure_object, remove_args=["working_directory"])
211+
def add_structure_object( # pylint: disable=missing-function-docstring
212+
self, *args, **kwargs
213+
) -> pd.DataFrame:
214+
215+
return add_structure_object(self.working_directory, *args, **kwargs)
216+
217+
@copy_doc(add_dose_object, remove_args=["working_directory"])
218+
def add_dose_object( # pylint: disable=missing-function-docstring
219+
self, *args, **kwargs
220+
) -> pd.DataFrame:
221+
222+
return add_dose_object(self.working_directory, *args, **kwargs)
223+
224+
@copy_doc(read_all_segmentation_logs, remove_args=["working_directory"])
225+
def read_all_segmentation_logs( # pylint: disable=missing-function-docstring
226+
self, *args, **kwargs
227+
) -> pd.DataFrame:
228+
229+
return read_all_segmentation_logs(self.working_directory, *args, **kwargs)
186230

187-
# Check that object folder exists, if not provide instructions for adding
231+
@copy_doc(segment_image, remove_args=["working_directory"])
232+
def segment_image( # pylint: disable=missing-function-docstring
233+
self, *args, **kwargs
234+
) -> pd.DataFrame:
188235

189-
# Check that no object with uid already exists
236+
return segment_image(self.working_directory, *args, **kwargs)
190237

191-
# Check that references sop uid exists, only warning if not
238+
@copy_doc(segment_dataset, remove_args=["working_directory"])
239+
def segment_dataset( # pylint: disable=missing-function-docstring
240+
self, *args, **kwargs
241+
) -> pd.DataFrame:
192242

193-
# Once ready, add to converted.csv for each dataset specified
243+
return segment_dataset(self.working_directory, *args, **kwargs)

pydicer/utils.py

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -70,13 +70,15 @@ def determine_dcm_datetime(ds, require_time=False):
7070
return None
7171

7272

73-
def load_object_metadata(row, keep_tags=None, remove_tags=None):
73+
def load_object_metadata(row: pd.Series, keep_tags=None, remove_tags=None):
7474
"""Loads the object's metadata
7575
7676
Args:
7777
row (pd.Series): The row of the converted DataFrame for which to load the metadata
78-
keep_tags TODO
79-
remove_tag TODO
78+
keep_tags (str|list, optional): DICOM tag keywords keep when loading data. If set all other
79+
tags will be removed. Defaults to None.
80+
remove_tag (str|list, optional): DICOM tag keywords keep when loading data. If set all
81+
other tags will be kept. Defaults to None.
8082
8183
Returns:
8284
pydicom.Dataset: The dataset object containing the original DICOM metadata
@@ -462,9 +464,9 @@ def add_structure_name_mapping(
462464
this mapping belongs. Defaults to None.
463465
464466
Raises:
465-
SystemError: _description_
466-
ValueError: _description_
467-
ValueError: _description_
467+
SystemError: Ensure working_directory or structure_set is provided.
468+
ValueError: All keys in mapping dictionary must be of type `str`.
469+
ValueError: All values in mapping dictionary must be a list of `str` entries.
468470
"""
469471

470472
mapping_path_base = None
@@ -585,3 +587,40 @@ def fetch_converted_test_data(working_directory=None, dataset="HNSCC"):
585587
shutil.copytree(output_directory.joinpath(working_name), working_directory)
586588

587589
return working_directory
590+
591+
592+
def copy_doc(copy_func, remove_args=None):
593+
"""Copies the doc string of the given function to another.
594+
This function is intended to be used as a decorator.
595+
596+
Remove args listed in `remove_args` from the docstring.
597+
598+
This function was adapted from:
599+
https://stackoverflow.com/questions/68901049/copying-the-docstring-of-function-onto-another-function-by-name
600+
601+
.. code-block:: python3
602+
603+
def foo():
604+
'''This is a foo doc string'''
605+
...
606+
607+
@copy_doc(foo)
608+
def bar():
609+
...
610+
611+
"""
612+
613+
if remove_args is None:
614+
remove_args = []
615+
616+
def wrapped(func):
617+
func.__doc__ = copy_func.__doc__
618+
619+
for arg in remove_args:
620+
func.__doc__ = "\n".join(
621+
[line for line in func.__doc__.split("\n") if not line.strip().startswith(arg)]
622+
)
623+
624+
return func
625+
626+
return wrapped

0 commit comments

Comments
 (0)