Source code for pyradise.fileio.series_info

import re
import warnings
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import List, Optional, Sequence, Tuple, Union

import numpy as np
import SimpleITK as sitk
from pydicom import Dataset
from pydicom.tag import Tag

from pyradise.data import (Annotator, Modality, Organ, str_to_annotator,
                           str_to_modality, str_to_organ)
from pyradise.utils import (is_dir_and_exists, is_file_and_exists,
                            load_dataset, load_dataset_tag)

__all__ = [
    "SeriesInfo",
    "FileSeriesInfo",
    "IntensityFileSeriesInfo",
    "SegmentationFileSeriesInfo",
    "DicomSeriesInfo",
    "DicomSeriesImageInfo",
    "DicomSeriesDoseInfo",
    "DicomSeriesRegistrationInfo",
    "DicomSeriesRTSSInfo",
    "ReferenceInfo",
    "RegistrationInfo",
    "RegistrationSequenceInfo",
]


[docs]class SeriesInfo(ABC): """An abstract base class for all :class:`SeriesInfo` classes. A :class:`SeriesInfo` class is used to retrieve and store content-specific information which is required by the :class:`~pyradise.fileio.loading.SubjectLoader` in order to correctly load the data and to construct a :class:`~pyradise.data.subject.Subject`. Depending on the type of data, the :class:`SeriesInfo` subclasses retrieve different information which is essential for loading and :class:`~pyradise.data.subject.Subject` construction. For example, the :class:`DicomSeriesImageInfo` retrieves and holds data about the :class:`~pyradise.data.modality.Modality` whereas the :class:`DicomSeriesRTSSInfo` manages information about the :class:`~pyradise.data.annotator.Annotator` and information about the referenced DICOM image series. Args: path (Union[str, Tuple[str, ...]]): The path or paths to the files to load. """ def __init__(self, path: Union[str, Tuple[str, ...]]) -> None: super().__init__() if isinstance(path, str): self.path = (path,) else: self.path = path self._check_paths(self.path) self.patient_name = "" self.patient_id = "" self._is_updated = False @staticmethod def _check_paths(paths: Union[str, Tuple[str, ...]], should_be_dir: bool = False) -> None: """Check if the provided paths are files/directories. Args: paths (Union[str, Tuple[str, ...]]): The paths for checking. should_be_dir (bool): If True the paths will be checked if they are directories. Returns: None """ if isinstance(paths, str): internal_path = (paths,) else: internal_path = paths for path in internal_path: if should_be_dir: is_dir_and_exists(path) else: is_file_and_exists(path)
[docs] def get_path(self) -> Tuple[str]: """Get the file paths assigned to the :class:`SeriesInfo` instance. Returns: Tuple[str]: The file paths assigned to the :class:`SeriesInfo` instance. """ return self.path
[docs] def get_patient_name(self) -> str: """Get the patient name. Returns: str: The patient name. """ return self.patient_name
[docs] def get_patient_id(self) -> str: """Get the patient ID. Returns: str: The patient ID. """ return self.patient_id
[docs] def is_updated(self) -> bool: """Check if is updated. Returns: bool: True if is updated, False otherwise. """ return self._is_updated
[docs] @abstractmethod def update(self) -> None: """Update the :class:`SeriesInfo` instance. Returns: None """ raise NotImplementedError()
[docs]class FileSeriesInfo(SeriesInfo): """An abstract base :class:`SeriesInfo` class for all discrete image files (e.g. NIFTI, NRRD, MHA, etc.). Important: For DICOM files use a :class:`DicomSeriesInfo` subclass instead. Args: path (str): The path to the discrete image file to load. subject_name (str): The name of the subject. """ def __init__(self, path: str, subject_name: str) -> None: super().__init__(path) # remove illegal characters in the subject name subject_name_ = subject_name.replace(" ", "_") pattern = r"""[^\da-zA-Z_-]+""" regex = re.compile(pattern) # to stay consistent the subject name is called patient name as for DICOM info self.patient_name = regex.sub(r"", subject_name_) self.patient_id = self.patient_name
[docs] @abstractmethod def update(self) -> None: """Update the :class:`FileSeriesInfo` instance. Returns: None """ raise NotImplementedError()
[docs]class IntensityFileSeriesInfo(FileSeriesInfo): """A :class:`FileSeriesInfo` class for intensity images. In addition to the information provided by the :class:`FileSeriesInfo` class, this class contains also a :class:`~pyradise.data.modality.Modality` instance. Args: path (str): The path to the discrete intensity image file to load. subject_name (str): The name of the subject. modality (Union[str, Modality]): The modality of the intensity image. """ def __init__(self, path: str, subject_name: str, modality: Union[Modality, str]) -> None: if not isinstance(path, str): raise TypeError(f"Expected a string for the path but got {type(path)} instead.") super().__init__(path, subject_name) self.modality: Modality = str_to_modality(modality) self.update()
[docs] def get_modality(self) -> Modality: """Get the :class:`~pyradise.data.modality.Modality`. Returns: Modality: The :class:`~pyradise.data.modality.Modality`. """ return self.modality
[docs] def set_modality(self, modality: Modality) -> None: """Set the :class:`~pyradise.data.modality.Modality`. Args: modality (Modality): The :class:`~pyradise.data.modality.Modality` to be set. Returns: None """ self.modality: Modality = modality
[docs] def update(self) -> None: """Update the :class:`IntensityFileSeriesInfo`. Returns: None """ self._is_updated = True
[docs]class SegmentationFileSeriesInfo(FileSeriesInfo): """A :class:`FileSeriesInfo` class for segmentation images. In addition to the information provided by the :class:`FileSeriesInfo` class, this class contains also an :class:`~pyradise.data.organ.Organ` instance and a :class:`~pyradise.data.annotator.Annotator` instance. Note: We assume that the segmentation image is a binary image with the foreground having the value 1 and the background being 0. If your images are different we recommend to separate the segmentation masks into separate files because in RT practice segmentations may overlap. Args: path (str): The path to the discrete segmentation image file to load. subject_name (str): The name of the subject. organ (Union[Organ, str]): The organ the segmentation is representing. annotator (Union[Annotator, str]): The annotator who created the segmentation. """ def __init__( self, path: str, subject_name: str, organ: Union[Organ, str], annotator: Union[Annotator, str] ) -> None: if not isinstance(path, str): raise TypeError(f"Expected a single path as a string but got {type(path)}.") super().__init__(path, subject_name) self.organ: Organ = str_to_organ(organ) self.annotator: Annotator = str_to_annotator(annotator) self.update()
[docs] def get_organ(self) -> Organ: """Get the :class:`~pyradise.data.organ.Organ`. Returns: Organ: The :class:`~pyradise.data.organ.Organ`. """ return self.organ
[docs] def set_organ(self, organ: Organ) -> None: """Set the :class:`~pyradise.data.organ.Organ`. Args: organ (Organ): The :class:`~pyradise.data.organ.Organ` to be set. Returns: None """ self.organ: Organ = organ
[docs] def get_annotator(self) -> Annotator: """Get the :class:`~pyradise.data.annotator.Annotator`. Returns: Annotator: The :class:`~pyradise.data.annotator.Annotator`. """ return self.annotator
[docs] def set_annotator(self, annotator: Annotator) -> None: """Set the :class:`~pyradise.data.annotator.Annotator`. Args: annotator (Annotator): The :class:`~pyradise.data.annotator.Annotator` to be set. Returns: None """ self.annotator: Annotator = annotator
[docs] def update(self) -> None: """Update the :class:`SegmentationFileSeriesInfo` instance. Returns: None """ self._is_updated = True
[docs]class DicomSeriesInfo(SeriesInfo): """An abstract base :class:`SeriesInfo` class for all DICOM data (i.e. DICOM image, DICOM registration, DICOM-RTSS). Important: For discrete image files use a :class:`FileSeriesInfo` subclass instead. Args: path (Union[str, Tuple[str, ...]]): The path or paths specifying DICOM files to load. """ def __init__( self, path: Union[str, Tuple[str, ...]], ) -> None: super().__init__(path) self.patient_id = "" self.patient_name = "" self.study_instance_uid = "" self.study_description = "" self.series_instance_uid = "" self.series_description = "" self.series_number = -1 self.sop_class_uid = "" self.dicom_modality = "" self.frame_of_reference_uid = "" self._get_dicom_base_info() self._is_updated = False # noinspection DuplicatedCode def _get_dicom_base_info(self, additional_tags: Optional[Sequence[Tag]] = None) -> Dataset: """Get the basic information from the initial DICOM file path. Args: additional_tags (Optional[Sequence[Tag]]): Additional tags to retrieve from the DICOM file. Returns: Dataset: The dataset loaded. """ tags = [ Tag(0x0010, 0x0020), # PatientID Tag(0x0010, 0x0010), # PatientName Tag(0x0020, 0x000D), # StudyInstanceUID Tag(0x0008, 0x1030), # StudyDescription Tag(0x0020, 0x000E), # SeriesInstanceUID Tag(0x0008, 0x103E), # SeriesDescription Tag(0x0020, 0x0011), # SeriesNumber Tag(0x0008, 0x0016), # SOPClassUID Tag(0x0008, 0x0060), # Modality Tag(0x0020, 0x0052), # FrameOfReferenceUID Tag(0x3006, 0x0002), ] # StructureSetLabel if additional_tags: tags.extend(additional_tags) dataset = load_dataset_tag(self.path[0], tags) self.patient_id = str(dataset.get("PatientID", "")) self.patient_name = str(dataset.get("PatientName", "")) self.study_instance_uid = str(dataset.get("StudyInstanceUID", "")) self.study_description = str(dataset.get("StudyDescription", "")) self.series_instance_uid = str(dataset.get("SeriesInstanceUID", "")) self.series_description = str(dataset.get("SeriesDescription", "Unnamed_Series")) self.series_number = str(dataset.get("SeriesNumber", 0) if dataset.get("SeriesNumber", 0) is not None else "") self.sop_class_uid = str(dataset.get("SOPClassUID", "")) self.dicom_modality = str(dataset.get("Modality", "")) self.frame_of_reference_uid = str(dataset.get("FrameOfReferenceUID", "")) self.structure_set_label = str(dataset.get("StructureSetLabel", "")) minimum_criterion = ( self.patient_id != "", self.patient_name != "", self.study_instance_uid != "", self.series_instance_uid != "", self.sop_class_uid != "", self.dicom_modality != "", ) if not all(minimum_criterion): raise ValueError(f"At least one necessary DICOM information is not provided for subject {self.patient_id}!") return dataset
[docs] @abstractmethod def update(self) -> None: """Update the :class:`DicomSeriesInfo` instance. Returns: None """ raise NotImplementedError()
[docs]class DicomSeriesImageInfo(DicomSeriesInfo): """A :class:`DicomSeriesInfo` class for DICOM images. In addition to the information provided by the :class:`DicomSeriesInfo` class, this class contains also a :class:`~pyradise.data.modality.Modality` instance. Args: paths (Tuple[str, ...]): The paths to the DICOM image files to load. """ def __init__(self, paths: Tuple[str, ...]) -> None: super().__init__(paths) self.modality = Modality.get_default()
[docs] def get_modality(self) -> Modality: """Get the :class:`~pyradise.data.modality.Modality` property. Returns: Modality: The :class:`~pyradise.data.modality.Modality` property. """ return self.modality
[docs] def set_modality(self, modality: Modality) -> None: """Set the :class:`~pyradise.data.modality.Modality`. Args: modality (Modality): The :class:`~pyradise.data.modality.Modality` to be assigned. Returns: None """ self.modality = modality
[docs] def update(self) -> None: """Update the :class:`DicomSeriesImageInfo` instance. Returns: None """ self._is_updated = True
[docs]class DicomSeriesDoseInfo(DicomSeriesImageInfo): """A :class:`DicomSeriesDoseInfo` class for DICOM Dose images. In addition to the information provided by the :class:`DicomSeriesImageInfo` class, this class contains a flag to indicate the image is a Dose volume. Args: paths (Tuple[str, ...]): The paths to the DICOM image files to load. """ def __init__(self, paths: Tuple[str, ...]) -> None: super().__init__(paths) scaling_tag = [Tag(0x3004,0x000E)] dataset = load_dataset_tag(self.path[0], scaling_tag) self.scaling_value = str(dataset.get("DoseGridScaling", 1.0)) self.is_dose_image = True
# noinspection PyUnresolvedReferences
[docs]@dataclass class ReferenceInfo: """A class storing one of multiple reference infos from a DICOM registration file. Warning: This class is intended for internal use only. Args: series_instance_uid (str): The SeriesInstanceUID. study_instance_uid (str): The StudyInstanceUID. is_same_study (bool): Indicates if the series is from the same study as the reference. """ series_instance_uid: str study_instance_uid: str is_same_study: bool
# noinspection PyUnresolvedReferences
[docs]@dataclass class RegistrationSequenceInfo: """A class storing one of multiple registration sequence infos from a DICOM registration file. Warning: This class is intended for internal use only. Args: frame_of_reference_uid (str): The FrameOfReferenceUID. transforms (Tuple[sitk.AffineTransform, ...]): The transforms. transform_parameters (Tuple[List, ...]): The transformation parameters. """ frame_of_reference_uid: str transforms: Tuple[sitk.AffineTransform, ...] transform_parameters: Tuple[List, ...]
# noinspection PyUnresolvedReferences
[docs]@dataclass class RegistrationInfo: """A class storing all necessary infos for applying a registration transformation to a DICOM image. Warning: This class is intended for internal use only. Args: registration_info (RegistrationSequenceInfo): The registration sequence info. reference_info (ReferenceInfo): The reference info. is_reference_image (bool): Indicates if the image is the reference image. """ registration_info: RegistrationSequenceInfo reference_info: ReferenceInfo is_reference_image: bool
[docs]class DicomSeriesRegistrationInfo(DicomSeriesInfo): """A :class:`DicomSeriesInfo` class for DICOM registrations. In addition to the information provided by the :class:`DicomSeriesInfo` class, this class contains transformation parameters and references to the pair of DICOM images associated with the registration. Args: path (str): The path to the DICOM registration file to load. image_infos (Tuple[DicomSeriesImageInfo, ...]): The :class:`DicomSeriesImageInfo` used. persistent_image_infos (bool): If True the class holds to the image_infos after updating, otherwise not (default: False). """ def __init__( self, path: str, image_infos: Tuple[DicomSeriesImageInfo, ...], persistent_image_infos: bool = False ) -> None: self.dataset = None super().__init__(path) self.image_infos = image_infos self.persistent_image_infos = persistent_image_infos self.transform: Optional[sitk.Transform] = None self.transform_parameters = tuple() self.referenced_series_instance_uid_transform = "" self.referenced_series_instance_uid_identity = "" # since the update is lightweight let's update this class upon instantiation self.update() def _get_dicom_base_info(self, additional_tags: Optional[Sequence[Tag]] = None) -> Dataset: """Get the basic information from the initial DICOM file path. Args: additional_tags (Optional[Sequence[Tag]]): Additional tags to retrieve from the DICOM file. Returns: Dataset: The dataset loaded. """ additional_tags_ = [ Tag(0x0008, 0x1115), # ReferencedSeriesSequence Tag(0x0008, 0x1200), ] # StudiesContainingOtherReferencedInstancesSequence if additional_tags: additional_tags_.extend(additional_tags) super()._get_dicom_base_info(additional_tags) self.dataset = load_dataset(self.path[0]) return self.dataset
[docs] @staticmethod def get_referenced_series_info(registration_dataset: Dataset) -> Tuple[ReferenceInfo, ...]: """Get the :class:`ReferenceInfo` entries from a dataset. Args: registration_dataset (Dataset): The registration dataset to extract the infos from. Returns: Tuple[ReferenceInfo, ...]: The :class:`ReferenceInfo` retrieved from the Dataset. """ referenced_series_instance_uids = [] referenced_series_sq = registration_dataset.get("ReferencedSeriesSequence", []) for item in referenced_series_sq: referenced_series_instance_uids.append( ReferenceInfo( str(item.get("SeriesInstanceUID", "")), str(registration_dataset.get("StudyInstanceUID", "")), True ) ) other_referenced_series_sq = registration_dataset.get("StudiesContainingOtherReferencedInstancesSequence", []) for item in other_referenced_series_sq: referenced_series_sq = item.get("ReferencedSeriesSequence", []) for referenced_series_item in referenced_series_sq: referenced_series_instance_uids.append( ReferenceInfo( str(referenced_series_item.get("SeriesInstanceUID", "")), str(item.get("StudyInstanceUID", "")), False, ) ) return tuple(referenced_series_instance_uids)
@staticmethod def _get_registration_sequence_info(registration_dataset: Dataset) -> Tuple[RegistrationSequenceInfo, ...]: """Get the :class:`RegistrationSequenceInfo` entries from a dataset. Args: registration_dataset (Dataset): The registration dataset to extract the information from. Returns: Tuple[RegistrationSequenceInfo, ...]: The :class:`RegistrationSequenceInfo` entries retrieved. """ registration_info = [] for item in registration_dataset.get("RegistrationSequence", []): frame_of_reference_uid = str(item.get("FrameOfReferenceUID", "")) transforms = [] transform_parameters = [] for matrix_reg_item in item.get("MatrixRegistrationSequence", []): for matrix_item in matrix_reg_item.get("MatrixSequence", []): transform_type = str(matrix_item.get("FrameOfReferenceTransformationMatrixType")) transform_matrix = matrix_item.get("FrameOfReferenceTransformationMatrix") if transform_type == "RIGID": transform_params = [float(param) for param in transform_matrix] transform_matrix = np.array(transform_params).reshape(4, 4) transform = sitk.AffineTransform(3) transform.SetMatrix(transform_matrix[:3, :3].flatten().tolist()) transform.SetTranslation(transform_matrix[:-1, 3:].flatten().tolist()) transforms.append(transform.GetInverse()) transform_parameters.append(transform_params) else: print( "Invalid transform type registered in subject " f'{registration_dataset.get("PatientID", "n/a")} ({transform_type})!' ) registration_info.append( RegistrationSequenceInfo(frame_of_reference_uid, tuple(transforms), tuple(transform_parameters)) ) return tuple(registration_info) @staticmethod def _get_unique_series_instance_uid_entries( infos: Union[Tuple[DicomSeriesImageInfo, ...], Tuple[Dataset, ...]] ) -> Union[Tuple[DicomSeriesImageInfo, ...], Tuple[Dataset, ...]]: """Get the unique SeriesInstanceUID entries from a list of :class:`DicomSeriesImageInfo` or datasets. Args: infos (Union[Tuple[DicomSeriesImageInfo, ...], Tuple[Dataset, ...]]): The infos to extract the unique entries from. Returns: Union[Tuple[DicomSeriesImageInfo, ...], Tuple[Dataset, ...]]: The unique entries. """ unique_infos = [] if isinstance(infos[0], DicomSeriesImageInfo): unique_instance_uids = list({info.series_instance_uid for info in infos}) else: unique_instance_uids = list({str(info.get("SeriesInstanceUID")) for info in infos}) for info in infos: if isinstance(info, DicomSeriesImageInfo): if info.series_instance_uid in unique_instance_uids: unique_infos.append(info) index = unique_instance_uids.index(info.series_instance_uid) unique_instance_uids.pop(index) else: if str(info.get("SeriesInstanceUID")) in unique_instance_uids: unique_infos.append(info) index = unique_instance_uids.index(str(info.get("SeriesInstanceUID"))) unique_instance_uids.pop(index) return tuple(unique_infos)
[docs] @staticmethod def get_registration_infos( registration_dataset: Dataset, image_infos: Union[Tuple[DicomSeriesImageInfo, ...], Tuple[Dataset, ...]] ) -> Tuple[RegistrationInfo, ...]: """Extract the :class:`RegistrationInfo` entries with the corresponding :class:`ReferenceInfo`. Args: registration_dataset (Dataset): The registration dataset to extract the registration infos from. image_infos (Union[Tuple[DicomSeriesImageInfo, ...], Tuple[Dataset, ...]]): The image infos or datasets to use for the combination. Returns: Tuple[RegistrationInfo, ...]: The combined :class:`RegistrationInfo`. """ registration_infos = DicomSeriesRegistrationInfo._get_registration_sequence_info(registration_dataset) reference_sequence_infos = DicomSeriesRegistrationInfo.get_referenced_series_info(registration_dataset) internal_image_infos = DicomSeriesRegistrationInfo._get_unique_series_instance_uid_entries(image_infos) identity_transform = (1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0) def is_ident(transform: Tuple[list]): for trans in transform: yield tuple(trans) == identity_transform combined_info = [] for image_info in internal_image_infos: if isinstance(image_info, Dataset): if not any( uid in str(image_info.get("SOPClassUID", "")) + "." for uid in ("1.2.840.10008.5.1.4.1.1.2.", "1.2.840.10008.5.1.4.1.1.4.") ): raise ValueError( f'The provided image info SOPClassUID ({str(image_info.get("SOPClassUID", ""))})' " is invalid!" ) image_series_instance_uid = image_info.get("SeriesInstanceUID") image_study_instance_uid = image_info.get("StudyInstanceUID") image_frame_of_reference_uid = image_info.get("FrameOfReferenceUID") else: image_series_instance_uid = image_info.series_instance_uid image_study_instance_uid = image_info.study_instance_uid image_frame_of_reference_uid = image_info.frame_of_reference_uid for reference_sequence_info in reference_sequence_infos: if not all( ( reference_sequence_info.series_instance_uid == image_series_instance_uid, reference_sequence_info.study_instance_uid == image_study_instance_uid, ) ): continue for registration_info in registration_infos: if registration_info.frame_of_reference_uid != image_frame_of_reference_uid: continue combined_info.append( RegistrationInfo( registration_info, reference_sequence_info, all(is_ident(registration_info.transform_parameters)), ) ) if len(combined_info) > 2: warnings.warn("There are more than two DICOM images assigned to a registration!") return tuple(combined_info)
[docs] def set_image_infos(self, image_infos: Tuple[DicomSeriesImageInfo, ...]) -> None: """Set the :class:`DicomSeriesImageInfo` entries. Args: image_infos (Tuple[DicomSeriesImageInfo, ...]): The :class:`DicomSeriesImageInfo` entries to set. Returns: None """ self.image_infos = image_infos self._is_updated = False
[docs] def get_image_infos(self) -> Tuple[DicomSeriesImageInfo, ...]: """Get the :class:`DicomSeriesImageInfo` entries. Returns: Tuple[DicomSeriesImageInfo, ...]: The :class:`DicomSeriesImageInfo` entries. """ return self.image_infos
[docs] def update(self) -> None: """Update the :class:`DicomSeriesRegistrationInfo`. Returns: None """ if len(self.path) != 1: raise ValueError("Only one registration file path is allowed, but multiple or none are provided!") if not self.image_infos: raise ValueError("No image infos are provided and thus no registration is possible!") if not self.dataset: self.dataset = load_dataset(self.path[0]) combined_info = self.get_registration_infos(self.dataset, self.image_infos) for info in combined_info: if info.is_reference_image: self.referenced_series_instance_uid_identity = info.reference_info.series_instance_uid else: self.referenced_series_instance_uid_transform = info.reference_info.series_instance_uid if len(info.registration_info.transforms) != 1: raise ValueError( f"The current implementation only supports registration files with " f"one transformation, but there are {len(info.registration_info.transforms)}!" ) self.transform = info.registration_info.transforms[0] self.transform_parameters = info.registration_info.transform_parameters[0] if not self.persistent_image_infos: self.image_infos = tuple() self._is_updated = True
[docs]class DicomSeriesRTSSInfo(DicomSeriesInfo): """A :class:`DicomSeriesInfo` class for DICOM-RTSS. In addition to the information provided by the :class:`DicomSeriesInfo` class, this class contains a :class:`~pyradise.data.annotator.Annotator` instance and a reference to the DICOM image associated with the DICOM-RTSS. Args: path (str): The path to the DICOM-RTSS file to load. """ def __init__(self, path: str) -> None: self.dataset: Optional[Dataset] = None self.annotator: Annotator = Annotator.get_default() self.referenced_instance_uid = "" self.roi_names = [] super().__init__(path) self.update() def _get_dicom_base_info(self, additional_tags: Optional[Sequence[Tag]] = None) -> Dataset: """Get the basic information from the initial DICOM file path. Args: additional_tags (Optional[Sequence[Tag]]): Additional tags to retrieve from the DICOM file. Returns: Dataset: The Dataset loaded. """ additional_tags_ = [ Tag(0x3006, 0x0002), # StructureSetLabel Tag(0x0008, 0x1070), # OperatorName Tag(0x3006, 0x0010), # ReferencedFrameOfReferenceSequence Tag(0x3006, 0x0080), # RTROIObservationsSequence Tag(0x3006, 0x0020), ] # StructureSetROISequence if additional_tags: additional_tags_.extend(additional_tags) self.dataset = super()._get_dicom_base_info(additional_tags_) self.annotator = self._get_annotator_from_dicom(self.dataset) self.referenced_instance_uid = self._get_referenced_series_instance_uid(self.dataset) self.roi_names = self._get_roi_names() self._is_updated = True return self.dataset @staticmethod def _get_annotator_from_dicom(dataset: Dataset) -> Annotator: """Get the :class:`~pyradise.data.annotator.Annotator` from the provided dataset. Args: dataset (Dataset): The dataset to retrieve the :class:`~pyradise.data.annotator.Annotator` from. Returns: Annotator: The annotator. """ operator_name = str(dataset.get("OperatorsName", "")) if operator_name: operator_name = operator_name.replace(" ", "_") pattern = r"""[^\da-zA-Z_-]+""" regex = re.compile(pattern) operator_name = regex.sub(r"", operator_name) search_pattern = r"""[A-Z]+""" abbreviation = "".join(re.findall(search_pattern, operator_name)) abbreviation = abbreviation if abbreviation else None return Annotator(operator_name, abbreviation) return Annotator.get_default()
[docs] def get_annotator(self) -> Annotator: """Get the :class:`~pyradise.data.annotator.Annotator`. Returns: Annotator: The annotator. """ return self.annotator
@staticmethod def _get_referenced_series_instance_uid(dataset: Dataset) -> str: """Get the referenced SeriesInstanceUID from the dataset. Args: dataset (Dataset): The dataset to retrieve the referenced SeriesInstanceUID from. Returns: str: The referenced SeriesInstanceUID """ referenced_series_instance_uids = [] ref_frame_of_ref_sq = dataset.get("ReferencedFrameOfReferenceSequence", []) for ref_frame_ref_item in ref_frame_of_ref_sq: rt_ref_study_sq = ref_frame_ref_item.get("RTReferencedStudySequence", []) for rt_ref_study_item in rt_ref_study_sq: rt_ref_series_sq = rt_ref_study_item.get("RTReferencedSeriesSequence", []) for rt_ref_series_item in rt_ref_series_sq: referenced_series_instance_uids.append(str(rt_ref_series_item.get("SeriesInstanceUID"))) if len(referenced_series_instance_uids) != 1: raise ValueError( f"There are multiple or no ({len(referenced_series_instance_uids)}) referenced " f"SeriesInstanceUIDs, but only one is allowed!" ) return referenced_series_instance_uids[0] def _get_roi_names(self) -> List[str]: """Get the ROI names from the dataset. Returns: List[str]: The ROI names. """ roi_names = [] roi_sq = self.dataset.get("StructureSetROISequence", []) for roi_item in roi_sq: roi_names.append(str(roi_item.get("ROIName"))) return roi_names # pylint: disable=unnecessary-pass
[docs] def update(self) -> None: """Update the :class:`DicomSeriesRTSSInfo`. Returns: None """ self._is_updated = True