from abc import (
    ABC,
    abstractmethod)
from typing import (
    Any,
    Tuple,
    Optional,
    Union,
    List,
    Dict)
from copy import deepcopy
from datetime import datetime
from dataclasses import dataclass
import warnings

import vtkmodules.vtkFiltersCore as vtk_fcore
import vtkmodules.vtkCommonDataModel as vtk_dm
import vtkmodules.vtkImagingGeneral as vtk_igen
import vtkmodules.vtkImagingCore as vtk_icore
import vtkmodules.vtkFiltersModeling as vtk_fmodel
import vtkmodules.vtkCommonCore as vtk_ccore
import itk
import numpy as np
import cv2 as cv
import SimpleITK as sitk
from pydicom import (
    Dataset,
    FileDataset,
    Sequence)
from pydicom.uid import (
    generate_uid,
    ImplicitVRLittleEndian,
    PYDICOM_IMPLEMENTATION_UID)
from pydicom.dataset import FileMetaDataset
from pydicom.tag import Tag

from pyradise.data import (
    Subject,
    IntensityImage,
    SegmentationImage,
    Organ,
    Modality,
    str_to_modality)
from pyradise.utils import (
    load_datasets,
    load_dataset,
    load_dataset_tag,
    chunkify,
    get_slice_position,
    get_slice_direction,
    get_spacing_between_slices,
    convert_to_itk_image)
from .series_info import (
    SeriesInfo,
    DicomSeriesImageInfo,
    DicomSeriesRegistrationInfo,
    DicomSeriesRTSSInfo,
    RegistrationInfo)

__all__ = ['Converter', 'DicomImageSeriesConverter', 'DicomRTSSSeriesConverter', 'SubjectToRTSSConverter',
           'RTSSToSegmentConverter', 'SegmentToRTSSConverter2D', 'SegmentToRTSSConverter3D', 'RTSSMetaData',
           'RTSSConverter2DConfiguration', 'RTSSConverter3DConfiguration']


ROI_GENERATION_ALGORITHMS = ['AUTOMATIC', 'SEMIAUTOMATIC', 'MANUAL']


COLOR_PALETTE = [[255, 0, 255],
                 [0, 235, 235],
                 [255, 255, 0],
                 [255, 0, 0],
                 [0, 132, 255],
                 [0, 240, 0],
                 [255, 175, 0],
                 [0, 208, 255],
                 [180, 255, 105],
                 [255, 20, 147],
                 [160, 32, 240],
                 [0, 255, 127],
                 [255, 114, 0],
                 [64, 224, 208],
                 [0, 178, 47],
                 [220, 20, 60],
                 [238, 130, 238],
                 [218, 165, 32],
                 [255, 140, 190],
                 [0, 0, 255],
                 [255, 225, 0]]


class Converter(ABC):
    """An abstract base class for all :class:`Converter` classes. Typically, the :class:`Converter` classes are used to
    convert DICOM data from and to other representations. For example, the :class:`DicomImageSeriesConverter` converts
    DICOM image series to :class:`~pyradise.data.image.IntensityImage` instances and applies the associated DICOM
    registration if provided.
    """

    @abstractmethod
    def convert(self) -> Any:
        """Convert the provided :class:`~pyradise.fileio.series_info.DicomSeriesInfo`.

        Returns:
            Any: The converted data.
        """
        raise NotImplementedError()


class RTSSToSegmentConverter(Converter):
    """A low-level DICOM-RTSS to SimpleITK image :class:`Converter` class converting the content of the DICOM-RTSS to
    one or multiple SimpleITK images. In contrast to the :class:`DicomRTSSSeriesConverter` this class generates a dict
    of binary :class:`SimpleITK.Image` instances and organ names instead of a tuple of
    :class:`~pyradise.data.image.SegmentationImage` s.

    Notes:
        Typically, this class is not used directly by the user but via the :class:`DicomRTSSSeriesConverter` which
        processes :class:`~pyradise.fileio.series_info.DicomSeriesInfo` entries directly.

        This class can be used with a DICOM registration which will be applied to the reference image and the structure
        set. However, the registration must reference the corresponding DICOM image, otherwise the registration will not
        be applied.

    Args:
        rtss_dataset (Union[str, Dataset]): The path to the DICOM-RTSS file or DICOM-RTSS
         :class:`~pydicom.dataset.Dataset`.
        image_datasets (Union[Tuple[str], Tuple[Dataset]]): The path to the DICOM image files or the DICOM image
         :class:`~pydicom.dataset.Dataset` entries which are referenced in the ``rtss_dataset``.
        registration_dataset (Union[str, Dataset, None]): The path to a DICOM registration file or a DICOM
         registration :class:`~pydicom.dataset.Dataset` entry which contains a reference to the DICOM image
         (default: None).
    """

    def __init__(self,
                 rtss_dataset: Union[str, Dataset],
                 image_datasets: Union[Tuple[str], Tuple[Dataset]],
                 registration_dataset: Union[str, Dataset, None] = None
                 ) -> None:
        super().__init__()

        # get the RTSS dataset and the referenced SeriesInstanceUID
        if isinstance(rtss_dataset, str):
            self.rtss_dataset: Dataset = load_dataset(rtss_dataset)
        else:
            self.rtss_dataset: Dataset = rtss_dataset
        ref_series_uid = self._get_ref_series_instance(self.rtss_dataset)

        # get the appropriate image datasets according to the RTSS
        if isinstance(image_datasets[0], str):
            self.rtss_image_datasets = self._load_ref_image_datasets(image_datasets, ref_series_uid)
        else:
            self.rtss_image_datasets = self._clean_image_datasets_for_rtss(image_datasets, ref_series_uid)

        # get the registration dataset and the appropriate images
        if not registration_dataset:
            self.reg_dataset: Optional[Dataset] = None
        elif isinstance(registration_dataset, str):
            self.reg_dataset: Optional[Dataset] = load_dataset(registration_dataset)
        elif isinstance(registration_dataset, Dataset):
            self.reg_dataset: Optional[Dataset] = registration_dataset
        self.reg_image_datasets = self._get_image_datasets_for_reg(image_datasets, self.reg_dataset)

        # validate the loaded data
        self._validate_rtss_dataset(self.rtss_dataset)
        self._validate_rtss_image_references(self.rtss_dataset, self.rtss_image_datasets)
        self.reg_dataset: Optional[Dataset] = self._validate_registration_dataset(self.reg_dataset, self.rtss_dataset)

    @staticmethod
    def _get_ref_series_instance(rtss_dataset: Dataset) -> str:
        """Get the referenced SeriesInstanceUID of the image series in the RTSS dataset.

        Args:
            rtss_dataset (Dataset): The RTSS dataset.

        Returns:
            str: The referenced SeriesInstanceUID of the image series.
        """
        ref_series_instance_uids = []
        for ref_frame_of_ref in rtss_dataset.get('ReferencedFrameOfReferenceSequence', []):
            for rt_ref_study in ref_frame_of_ref.get('RTReferencedStudySequence', []):
                for rt_ref_series in rt_ref_study.get('RTReferencedSeriesSequence', []):
                    si_uid = rt_ref_series.get('SeriesInstanceUID', None)
                    if si_uid is not None:
                        ref_series_instance_uids.append(str(si_uid))

        if len(ref_series_instance_uids) > 1:
            raise Exception(f'Multiple ({len(ref_series_instance_uids)}) referenced SeriesInstanceUIDs '
                            'have been retrieved from the RTSS but only one is allowed!')

        if not ref_series_instance_uids:
            raise Exception('No referenced SeriesInstanceUID could be retrieved!')

        return ref_series_instance_uids[0]

    @staticmethod
    def _load_ref_image_datasets(image_paths: Tuple[str],
                                 referenced_series_uid: str
                                 ) -> Tuple[Dataset]:
        """Load the appropriate image datasets which are referenced in the RTSS dataset.

        Args:
            image_paths (Tuple[str]): The paths to the image datasets.
            referenced_series_uid (str): The referenced SeriesInstanceUID.

        Returns:
            Tuple[Dataset]: The referenced image datasets.
        """
        # check all image file paths for the referenced SeriesInstanceUID
        ref_image_files = []
        for image_path in image_paths:
            image_dataset = load_dataset_tag(image_path, (Tag(0x0020, 0x000e),))
            if image_dataset.get('SeriesInstanceUID', '') == referenced_series_uid:
                ref_image_files.append(image_path)

        # load the appropriate image datasets
        ref_image_datasets = []
        for ref_image_file in ref_image_files:
            ref_image_datasets.append(load_dataset(ref_image_file))

        ref_image_datasets.sort(key=get_slice_position, reverse=False)

        return tuple(ref_image_datasets)

    @staticmethod
    def _clean_image_datasets_for_rtss(image_datasets: Tuple[Dataset],
                                       referenced_series_uid: str
                                       ) -> Tuple[Dataset]:
        """Clean the image datasets based on the referenced SeriesInstanceUID.

        Args:
            image_datasets (Tuple[Dataset]): The image Datasets which should be analyzed.
            referenced_series_uid (str): The referenced SeriesInstanceUID to identify the appropriate images.

        Returns:
            Tuple[Dataset]: The image datasets which are referenced by the RTSS Dataset.
        """
        selected = [image_dataset for image_dataset in image_datasets
                    if image_dataset.get('SeriesInstanceUID', None) == referenced_series_uid]

        selected.sort(key=get_slice_position, reverse=False)

        return tuple(selected)

    @staticmethod
    def _get_image_datasets_for_reg(image_datasets: Union[Tuple[Dataset, ...], Tuple[str, ...]],
                                    registration_dataset: Optional[Dataset]
                                    ) -> Optional[Tuple[Dataset, ...]]:
        """Get the image datasets for registration. This function outputs the datasets which are referenced within
         the registration dataset.

        Args:
            image_datasets (Union[Tuple[Dataset, ...], Tuple[str, ...]]): The image datasets to analyze.
            registration_dataset (Optional[Dataset]): The registration dataset containing the references.

        Returns:
            Optional[Tuple[Dataset, ...]]: The datasets being referenced within the registration.
        """
        if not registration_dataset:
            return None

        selected = []

        ref_image_series = DicomSeriesRegistrationInfo.get_referenced_series_info(registration_dataset)
        ref_series_uids = [ref_info.series_instance_uid for ref_info in ref_image_series]
        ref_study_uids = [ref_info.study_instance_uid for ref_info in ref_image_series]

        if isinstance(image_datasets[0], str):
            for path in image_datasets:
                dataset = load_dataset_tag(path, (Tag(0x0020, 0x000e), Tag(0x0020, 0x000d)))
                criteria = (str(dataset.get('SeriesInstanceUID', '')) in ref_series_uids,
                            str(dataset.get('StudyInstanceUID', '')) in ref_study_uids)
                if all(criteria):
                    selected.append(load_dataset(path))

        else:
            for image_dataset in image_datasets:
                criteria = (str(image_dataset.get('SeriesInstanceUID', '')) in ref_series_uids,
                            str(image_dataset.get('StudyInstanceUID', '')) in ref_study_uids)
                if all(criteria):
                    selected.append(image_dataset)

        return tuple(selected)

    @staticmethod
    def _validate_rtss_dataset(rtss_dataset: Dataset) -> None:
        """Validate if the RTSS dataset is containing the minimal data for conversion.

        Args:
            rtss_dataset (Dataset): The RTSS dataset to validate.

        Returns:
            None
        """
        criteria = (rtss_dataset.get('SOPClassUID', '') == '1.2.840.10008.5.1.4.1.1.481.3',
                    hasattr(rtss_dataset, 'ROIContourSequence'),
                    hasattr(rtss_dataset, 'StructureSetROISequence'),
                    hasattr(rtss_dataset, 'RTROIObservationsSequence'))

        if not all(criteria):
            raise Exception(f'The checked RTSS from subject {rtss_dataset.get("PatientID")} is invalid!')

    # pylint: disable=use-a-generator
    @staticmethod
    def _validate_registration_dataset(reg_dataset: Optional[Dataset],
                                       rtss_dataset: Dataset
                                       ) -> Optional[Dataset]:
        """Validate the registration dataset if it contains the minimally required data for conversion. If the provided
        information is insufficient this function will return None.

        Args:
            reg_dataset (Optional[Dataset]): The registration dataset to validate.
            rtss_dataset (Dataset): The RTSS dataset containing the references.

        Returns:
            Optional[Dataset]: The valid registration dataset or None.
        """

        if not reg_dataset:
            return None

        # search for references in the registration dataset
        reg_ref_instance_uids = []
        for ref_study_item in reg_dataset.get('StudiesContainingOtherReferencedInstancesSequence', []):
            for ref_series_item in ref_study_item.get('ReferencedSeriesSequence', []):
                ref_series_instance_uid = ref_series_item.get('SeriesInstanceUID', None)
                if ref_series_instance_uid:
                    reg_ref_instance_uids.append(ref_series_instance_uid)

        for ref_series_item in reg_dataset.get('ReferencedSeriesSequence', []):
            ref_series_uid = ref_series_item.get('SeriesInstanceUID', None)
            if ref_series_uid:
                reg_ref_instance_uids.append(ref_series_uid)

        # search for references in the rtss dataset
        rtss_ref_instance_uids = []
        for ref_frame_of_ref in rtss_dataset.get('ReferencedFrameOfReferenceSequence', []):
            for rt_ref_study in ref_frame_of_ref.get('RTReferencedStudySequence', []):
                for rt_ref_series in rt_ref_study.get('RTReferencedSeriesSequence', []):
                    ref_series_uid = rt_ref_series.get('SeriesInstanceUID', None)
                    if ref_series_uid:
                        rtss_ref_instance_uids.append(ref_series_uid)

        # check the criteria
        criteria = (reg_dataset.get('SOPClassUID', '') == '1.2.840.10008.5.1.4.1.1.66.1',
                    hasattr(reg_dataset, 'RegistrationSequence'),
                    hasattr(reg_dataset, 'ReferencedSeriesSequence'),
                    all([rt_reference in reg_ref_instance_uids for
                         rt_reference in rtss_ref_instance_uids]),
                    len(reg_ref_instance_uids) != 0)

        if not all(criteria):
            print(f'The checked registration from subject {rtss_dataset.get("PatientID", "n/a")} is invalid!')
            return None

        return reg_dataset

    @staticmethod
    def _validate_rtss_image_references(rtss_dataset: Dataset,
                                        image_datasets: Tuple[Dataset]
                                        ) -> None:
        """Validate if the ReferencedSOPInstanceUIDs of the RTSS dataset are contained in the image datasets.

        Args:
            rtss_dataset (Dataset): The RTSS dataset to validate.
            image_datasets (Tuple[Dataset]): The image datasets to be used for comparison.

        Returns:
            None
        """
        # get the ReferencedSOPInstanceUIDs from the RTSS dataset
        ref_sop_instance_uids = []
        for ref_frame_of_ref in rtss_dataset.get('ReferencedFrameOfReferenceSequence', []):
            for rt_ref_study in ref_frame_of_ref.get('RTReferencedStudySequence', []):
                for rt_ref_series in rt_ref_study.get('RTReferencedSeriesSequence', []):
                    for contour_image_entry in rt_ref_series.get('ContourImageSequence', []):
                        ref_sop_instance_uids.append(contour_image_entry.get('ReferencedSOPInstanceUID', ''))

        # get MediaStorageSOPInstanceUIDs from the image datasets
        image_sop_instance_uids = [entry.file_meta.get('MediaStorageSOPInstanceUID') for entry in image_datasets]

        # check if all ReferencedSOPInstanceUIDs are contained in the image datasets
        missing_sop_instance_uids = tuple(set(ref_sop_instance_uids) - set(image_sop_instance_uids))
        if missing_sop_instance_uids:
            raise ValueError('The following ReferencedSOPInstanceUIDs are missing in the image datasets: '
                             f'{missing_sop_instance_uids}')

    @staticmethod
    def _get_contour_sequence_by_roi_number(rtss_dataset: Dataset,
                                            roi_number: int
                                            ) -> Optional[Sequence]:
        """Get the ContourSequence by the ROINumber.

        Args:
            rtss_dataset (Dataset): The RT Structure Set dataset to retrieve the ContourSequence from.
            roi_number (int): The ROINumber for which the ContourSequence should be returned.

        Returns:
            Optional[Sequence]: The ContourSequence with the corresponding ROINumber if available, otherwise
             :class:`None`.
        """
        if rtss_dataset.get('ROIContourSequence', None) is None:
            return None

        for roi_contour_entry in rtss_dataset.get('ROIContourSequence', []):
            if str(roi_contour_entry.get('ReferencedROINumber', None)) == str(roi_number):
                if roi_contour_entry.get('ContourSequence', None) is None:
                    return None
                return roi_contour_entry.get('ContourSequence')

        raise Exception(f"Referenced ROI number '{roi_number}' not found")

    @staticmethod
    def _create_empty_series_mask(image_datasets: Tuple[Dataset, ...]) -> np.ndarray:
        """Create an empty numpy array with the shape according to the reference image.

        Returns:
            np.ndarray: The empty numpy array with appropriate shape.
        """
        rows = int(image_datasets[0].get('Rows'))
        columns = int(image_datasets[0].get('Columns'))
        num_slices = len(image_datasets)

        return np.zeros((columns, rows, num_slices)).astype(np.bool)

    @staticmethod
    def _create_empty_slice_mask(image_dataset: Dataset) -> np.ndarray:
        """Create an empty numpy array representing one slice of the output mask.

        Args:
            image_dataset (Dataset): The dataset providing the spatial information for the empty mask.

        Returns:
            np.ndarray: The empty slice numpy array.
        """
        columns = int(image_dataset.get('Columns'))
        rows = int(image_dataset.get('Rows'))
        return np.zeros((columns, rows)).astype(np.uint8)

    @staticmethod
    def _apply_transformation_to_3d_points(points: np.ndarray,
                                           transformation_matrix: np.ndarray
                                           ) -> np.ndarray:
        """Apply the provided transformation to multiple points in the 3D-space.

        Args:
            points (np.ndarray): The points to transform.
            transformation_matrix (np.ndarray): The transformation matrix.

        Notes:
            1. Augment each point with a '1' as the fourth coordinate for homogeneous coordinates.
            2. Multiply by a 4x4 transformation matrix
            3. Throw away the adaptation for homogeneous coordinates

        Returns:
            np.ndarray: The transformed points.
        """
        vec = np.concatenate((points, np.ones((points.shape[0], 1))), axis=1)
        return vec.dot(transformation_matrix.T)[:, :3]

    @staticmethod
    def _get_contour_fill_mask(series_slice: Dataset,
                               contour_coords: List[float],
                               transformation_matrix: np.ndarray
                               ) -> np.ndarray:
        """Get the mask with the filled contour.

        Args:
            series_slice (Dataset): The corresponding slice dataset.
            contour_coords (List[float]): The contour points.
            transformation_matrix (np.ndarray): The transformation matrix.

        Returns:
            np.ndarray: The mask with the filled contour.
        """
        reshaped_contour_data = np.reshape(contour_coords, [len(contour_coords) // 3, 3])
        translated_contour_data = RTSSToSegmentConverter._apply_transformation_to_3d_points(reshaped_contour_data,
                                                                                            transformation_matrix)
        polygon = [np.around([translated_contour_data[:, :2]]).astype(np.int32)]

        # Create mask for the region. Fill with 1 for ROI
        fill_mask = RTSSToSegmentConverter._create_empty_slice_mask(series_slice)
        cv.fillPoly(img=fill_mask, pts=polygon, color=1)
        return fill_mask

    @staticmethod
    def _get_patient_to_pixel_transformation_matrix(image_datasets: Tuple[Dataset, ...]) -> np.ndarray:
        """Get the patient to pixel transformation matrix from the first image dataset.

        Args:
            image_datasets (Tuple[Dataset, ...]): The datasets to retrieve the transformation matrix.

        Returns:
            np.ndarray: The transformation matrix.
        """
        offset = np.array(image_datasets[0].get('ImagePositionPatient'))
        row_spacing, column_spacing = image_datasets[0].get('PixelSpacing')
        slice_spacing = get_spacing_between_slices(image_datasets)
        row_direction, column_direction, slice_direction = get_slice_direction(image_datasets[0])

        linear = np.identity(3, dtype=np.float32)
        linear[0, :3] = column_direction / column_spacing
        linear[1, :3] = row_direction / row_spacing
        linear[2, :3] = slice_direction / slice_spacing

        mat = np.identity(4, dtype=np.float32)
        mat[:3, :3] = linear
        mat[:3, 3] = offset.dot(-linear.T)

        return mat

    @staticmethod
    def _get_slice_contour_data(image_dataset: Dataset,
                                contour_sequence: Sequence
                                ) -> Tuple[Any, ...]:
        """Get the contour data from the corresponding ContourSequence.

        Args:
            image_dataset (Dataset): The referenced image dataset.
            contour_sequence (Sequence): The ContourSequence.

        Returns:
            Tuple[Any, ...]: The retrieved ContourData.
        """
        slice_contour_data = []

        for contour in contour_sequence:
            for contour_image in contour.get('ContourImageSequence', []):
                if contour_image.get('ReferencedSOPInstanceUID', None) == image_dataset.get('SOPInstanceUID', '1'):
                    slice_contour_data.append(contour.get('ContourData', []))

        return tuple(slice_contour_data)

    @staticmethod
    def _get_slice_mask_from_slice_contour_data(image_dataset: Dataset,
                                                contour_data: Tuple[Any, ...],
                                                transformation_matrix: np.ndarray
                                                ) -> np.ndarray:
        """Get the slice mask from the ContourData.

        Args:
            image_dataset (Dataset): The referenced image dataset.
            contour_data (Tuple[Any, ...]): The contour data.
            transformation_matrix (np.ndarray): The transformation matrix.

        Returns:
            np.ndarray: The discrete slice mask.
        """
        slice_mask = RTSSToSegmentConverter._create_empty_slice_mask(image_dataset)

        for contour_coords in contour_data:
            fill_mask = RTSSToSegmentConverter._get_contour_fill_mask(image_dataset, contour_coords,
                                                                      transformation_matrix)
            slice_mask[fill_mask == 1] = np.invert(slice_mask[fill_mask == 1])

        return slice_mask

    @staticmethod
    def _create_mask_from_contour_sequence(image_datasets: Tuple[Dataset, ...],
                                           contour_sequence: Sequence
                                           ) -> np.ndarray:
        """Create the whole 3D mask from the ContourSequence.

        Args:
            image_datasets (Tuple[Dataset, ...]): The image datasets to be used for mask creation.
            contour_sequence (Sequence): The ContourSequence to be discretized.

        Returns:
            np.ndarray: The discrete segmentation mask.
        """
        mask = RTSSToSegmentConverter._create_empty_series_mask(image_datasets)
        transformation_matrix = RTSSToSegmentConverter._get_patient_to_pixel_transformation_matrix(image_datasets)

        for i, image_dataset in enumerate(image_datasets):
            slice_contour_data = RTSSToSegmentConverter._get_slice_contour_data(image_dataset, contour_sequence)
            if slice_contour_data:
                mask[:, :, i] = RTSSToSegmentConverter._get_slice_mask_from_slice_contour_data(image_dataset,
                                                                                               slice_contour_data,
                                                                                               transformation_matrix)

        return mask

    @staticmethod
    def _create_image_from_mask(image_datasets: Tuple[Dataset, ...],
                                mask: np.ndarray
                                ) -> sitk.Image:
        """Create an image from the numpy segmentation mask with appropriate orientation.

        Args:
            image_datasets (Tuple[Dataset, ...]): The image datasets used to create the image.
            mask (np.ndarray): The mask to be converted into a sitk.Image.

        Returns:
            sitk.Image: The image generated.
        """
        mask = np.swapaxes(mask, 0, -1)

        image = sitk.GetImageFromArray(mask.astype(np.uint8))
        image = sitk.Cast(image, sitk.sitkUInt8)

        image.SetOrigin(image_datasets[0].get('ImagePositionPatient'))

        row_spacing, column_spacing = image_datasets[0].get('PixelSpacing')
        slice_spacing = get_spacing_between_slices(image_datasets)
        image.SetSpacing((float(row_spacing), float(column_spacing), float(slice_spacing)))

        slice_direction = np.stack(get_slice_direction(image_datasets[0]), axis=0).T.flatten().tolist()
        image.SetDirection(slice_direction)

        return image

    @staticmethod
    def _get_transform_from_registration_info(registration_infos: Tuple[RegistrationInfo, ...]) -> sitk.Transform:
        """Get the transformation from the registration infos.

        Args:
            registration_infos (Tuple[RegistrationInfo]): Registration info entries which hold transformations and
             references.

        Returns:
            sitk.Transform: The transformation which is not an identity transformation.
        """
        assert len(registration_infos) <= 2, 'The number of registration infos must be at max two but is ' \
                                             f'{len(registration_infos)}!'

        transforms = []
        for registration_info in registration_infos:
            if not registration_info.is_reference_image:
                transforms = registration_info.registration_info.transforms

        if len(transforms) != 1:
            raise NotImplementedError('The use of multiple sequential transformations is currently not supported!')

        return transforms[0]

    @staticmethod
    def _transform_image_datasets(image_datasets: Tuple[Dataset, ...],
                                  transform: sitk.Transform
                                  ) -> Tuple[Dataset, ...]:
        """Apply the transformation to multiple image datasets.

        Args:
            image_datasets (Tuple[Dataset, ...]): The image datasets to be transformed.
            transform (sitk.Transform): The transformation to be applied.

        Returns:
            Tuple[Dataset, ...]: The transformed image datasets.
        """

        transformed_image_datasets = []

        for image_dataset in image_datasets:
            # transform the patient position
            image_position_patient = image_dataset.get('ImagePositionPatient')
            position_transformed = list(transform.GetInverse().TransformPoint(image_position_patient))
            image_dataset['ImagePositionPatient'].value = position_transformed

            # transform the image orientation
            image_orientation_patient = image_dataset.get('ImageOrientationPatient')
            vector_0 = np.array(image_orientation_patient[:3])
            vector_1 = np.array(image_orientation_patient[3:6])

            vector_0_transformed = transform.GetInverse().TransformVector(vector_0, (0, 0, 0))
            vector_1_transformed = transform.GetInverse().TransformVector(vector_1, (0, 0, 0))

            new_direction = list(vector_0_transformed + vector_1_transformed)
            image_dataset['ImageOrientationPatient'].value = new_direction

            transformed_image_datasets.append(image_dataset)

        return image_datasets

    @staticmethod
    def _transform_rtss_dataset(rtss_dataset: Dataset,
                                transform: sitk.Transform
                                ) -> Dataset:
        """Apply the transformation to the RTSS.

        Args:
            rtss_dataset (Dataset): The dataset to be transformed.
            transform (sitk.Transform): The transformation to apply.

        Returns:
            Dataset: The transformed dataset.
        """
        dataset = deepcopy(rtss_dataset)

        for roi_contour in dataset.get('ROIContourSequence', []):
            for contour_sequence_item in roi_contour.get('ContourSequence', []):
                contour_data = contour_sequence_item.get('ContourData')

                if len(contour_data) % 3 != 0:
                    raise ValueError('The number of contour points must be a multiple of three!')

                transformed_points = []

                for chunk in chunkify(contour_data, 3):
                    transformed_point = list(transform.GetInverse().TransformPoint(np.array(chunk)))
                    transformed_points.extend(transformed_point)

                contour_sequence_item["ContourData"].value = transformed_points

        return dataset

    def convert(self) -> Dict[str, sitk.Image]:
        """Convert a DICOM-RTSS :class:`~pydicom.dataset.Dataset` instance into a dict of binary
        :class:`SimpleITK.Image` instances including their associated ROINames as a key in the dict.

        Returns:
            Dict[str, sitk.Image]: The ROINames and the corresponding binary segmented :class:`SimpleITK.Image`
            instances.
        """
        converted_images = {}

        # apply the registration if available
        if self.reg_dataset:
            reg_infos = DicomSeriesRegistrationInfo.get_registration_infos(self.reg_dataset,
                                                                           self.reg_image_datasets)
            transform = self._get_transform_from_registration_info(reg_infos)
            image_datasets = self._transform_image_datasets(self.rtss_image_datasets, transform)
            rtss_dataset = self._transform_rtss_dataset(self.rtss_dataset, transform)

        else:
            image_datasets = self.rtss_image_datasets
            rtss_dataset = self.rtss_dataset

        # convert the contours to images
        for ss_roi in rtss_dataset.get('StructureSetROISequence', []):
            roi_number = int(ss_roi.get('ROINumber'))
            roi_name = str(ss_roi.get('ROIName', ''))

            contour_sequence = self._get_contour_sequence_by_roi_number(rtss_dataset, roi_number)

            if contour_sequence is None:
                continue

            mask = self._create_mask_from_contour_sequence(image_datasets, contour_sequence)
            image = self._create_image_from_mask(image_datasets, mask)
            converted_images.update({roi_name: image})

        return converted_images


@dataclass
class RTSSMetaData:
    """A class to define metadata of a new DICOM-RTSS dataset.

    Note:
        Some attributes can take ``None`` as a value. This means that the attribute will be copied from the reference
        DICOM image dataset.

    Note:
        For some attributes, the value must follow the value representation of the DICOM standard. For example, the
        ``PatientSex`` attribute must be either ``'M'``, ``'F'`` or ``'O'``. For more information, we refer to the
        `DICOM standard part 5 chapter 6.2 <https://dicom.nema.org/dicom/2013/output/chtml/part05/sect_6.2.html>`_.

    Args:
        patient_name (Optional[str]): The patient name.
        patient_id (Optional[str]): The patient ID.
        patient_birth_date (Optional[str]): The patient birth date (format: YYYYMMDD).
        patient_sex (Optional[str]): The patient sex (valid values: 'F' (female), 'M' (male), 'O' (other)).
        patient_weight (Optional[str]): The patient weight (unit: kilograms with decimals).
        patient_size (Optional[str]): The patient size (unit: meters with decimals).
        study_description (Optional[str]): The study description.
        series_description (Optional[str]): The series description.
        series_number (Optional[str]): The series number (default: '99').
        structure_set_label (str): The structure set label (default: 'Autogenerated').
        operators_name (str): The operator's name (default: 'NA').
        manufacturer (str): The manufacturer (default: 'University of Bern, Switzerland').
        manufacturer_model_name (str): The manufacturer model name (default: 'PyRaDiSe Package').
        institution_name (str): The institution name (default: 'Unknown Institution').
        referring_physician_name (str): The referring physician name (default: 'NA').
        approval_status (str): The approval status (valid values: 'APPROVED', 'UNAPPROVED', 'REJECTED',
         default: 'UNAPPROVED').
        roi_gen_algorithm (str): The ROI generation algorithm (valid values: 'AUTOMATIC', 'SEMIAUTOMATIC', 'MANUAL',
         default: 'AUTOMATIC').

    """

    patient_name: Optional[str] = None
    patient_id: Optional[str] = None
    patient_birth_date: Optional[str] = None
    patient_sex: Optional[str] = None
    patient_age: Optional[str] = None
    patient_weight: Optional[str] = None
    patient_size: Optional[str] = None
    study_description: Optional[str] = None
    series_description: Optional[str] = None
    series_number: str = '99'
    structure_set_label: str = 'Autogenerated'
    operators_name: str = 'NA'
    manufacturer: str = 'University of Bern, Switzerland'
    manufacturer_model_name: str = 'PyRaDiSe Package'
    institution_name: str = 'Unknown Institution'
    referring_physician_name: str = 'NA'
    approval_status: str = 'UNAPPROVED'
    roi_gen_algorithm: str = 'AUTOMATIC'


    def __post_init__(self) -> None:
        # validate entries
        criteria = (isinstance(self.patient_name, str) or self.patient_name is None,
                    isinstance(self.patient_id, str) or self.patient_id is None,
                    isinstance(self.patient_birth_date, str) or self.patient_birth_date is None,
                    isinstance(self.patient_sex, str) or self.patient_sex is None,
                    isinstance(self.patient_age, str) or self.patient_age is None,
                    isinstance(self.patient_weight, str) or self.patient_weight is None,
                    isinstance(self.patient_size, str) or self.patient_size is None,
                    isinstance(self.study_description, str) or self.study_description is None,
                    isinstance(self.series_description, str) or self.series_description is None,
                    isinstance(self.series_number, str),
                    isinstance(self.structure_set_label, str),
                    isinstance(self.operators_name, str),
                    isinstance(self.manufacturer, str),
                    isinstance(self.manufacturer_model_name, str),
                    isinstance(self.institution_name, str),
                    isinstance(self.referring_physician_name, str),
                    isinstance(self.approval_status, str) and self.approval_status in ('APPROVED',
                                                                                       'UNAPPROVED',
                                                                                       'REJECTED'),
                    isinstance(self.roi_gen_algorithm, str) and self.roi_gen_algorithm in ('AUTOMATIC',
                                                                                           'SEMIAUTOMATIC',
                                                                                           'MANUAL',
                                                                                           ))

        if not all(criteria):
            raise ValueError('The RTSS meta data is not valid! Please check the input values.')



class RTSSConverterConfiguration(ABC):
    """An abstract base class to parameterize a segment to DICOM-RTSS converter."""

    def __init__(self) -> None:
        super().__init__()
        self.general_params: Dict[str, Any] = {}
        self.image_specific_params: Dict[str, Dict[str, Any]] = {}

    @abstractmethod
    def set_general_params(self, **kwargs: Any) -> None:
        """Set the general parameters of the converter.

        Args:
            **kwargs: The parameters of the converter.

        Returns:
            None
        """
        raise NotImplementedError()

    def get_general_params(self,
                           name: Optional[str] = None
                           ) -> Optional[Union[Dict[str, Any], Any]]:
        """Get all or a specific general parameter for the converter.

        Args:
            name (Optional[str]): The name of the parameter to get. If ``None``, all parameters are returned
             (default: None).

        Returns:
            Optional[Union[Dict[str, Any], Any]]: All or the selected parameter.
        """
        if name is None:
            if self.general_params:
                return self.general_params
            return None

        return self.general_params.get(name, None)

    @abstractmethod
    def set_image_params(self,
                         **kwargs: Any
                         ) -> None:
        """Set the parameters of the converter for a specific image using an identifier.
        instance.

        Args:
            **kwargs: The parameters of the converter.

        Returns:
            None
        """
        raise NotImplementedError()

    def get_image_params(self,
                         image_identifier: str,
                         name: Optional[str] = None
                         ) -> Optional[Union[Dict[str, Any], Any]]:
        """Get all parameters for a specific image using its identifier.

        Args:
            image_identifier (str): The identifier of the image
            name (Optional[str]): The name of the parameter to get. If ``None``, all parameters are returned (default:
             None).

        Returns:
            Optional[Union[Dict[str, Any], Any]]: All or the selected parameter belonging to the specific image

        """
        if image_identifier not in self.image_specific_params:
            return None

        specific = self.image_specific_params[image_identifier]
        if name is None:
            if specific:
                return specific
            return None
        return specific.get(name, None)


class RTSSConverter2DConfiguration(RTSSConverterConfiguration):
    """A configuration class to parameterize a :class:`SegmentToRTSSConverter2D` instance.

    The configuration can be used to set the general and image specific conversion parameters of the converter.
    The general parameters are applied to all images except if image specific parameters are provided.
    used for the image.

    The parameters define the following:

    * ``smoothing``: Indicates if Gaussian smoothing is applied.

    * ``smoothing_sigma``: The variance of the discrete Gaussian smoothing kernel.

    * ``smoothing_kernel_size``: The size of the discrete Gaussian smoothing kernel.

    Args:
        smoothing (bool): Whether to smooth the contours or not (default: True).
        smoothing_sigma (float): The variance of the Gaussian smoothing (default: 1.0).
        smoothing_kernel_size (int): The size of the Gaussian smoothing kernel (default: 8).
    """

    def __init__(self,
                 smoothing: bool = True,
                 smoothing_sigma: float = 1.,
                 smoothing_kernel_size: int = 8,
                 ) -> None:
        super().__init__()

        self._validate_entries(smoothing, smoothing_sigma, smoothing_kernel_size)

        self.set_general_params(smoothing, smoothing_sigma, smoothing_kernel_size)


    @staticmethod
    def _validate_entries(smoothing: bool,
                          smoothing_sigma: float,
                          smoothing_kernel_size: int,
                          ) -> None:
        """Validate the entries of the configuration.

        Args:
            smoothing (bool): Whether to smooth the contours or not.
            smoothing_sigma (float): The variance of the Gaussian smoothing.
            smoothing_kernel_size (int): The size of the Gaussian smoothing kernel.

        Raises:
            ValueError: If the entries are not valid.

        Returns:
            None
        """
        criteria = (isinstance(smoothing, bool),
                    isinstance(smoothing_sigma, float) and smoothing_sigma > 0,
                    isinstance(smoothing_kernel_size, int) and smoothing_kernel_size > 0)

        if not all(criteria):
            raise ValueError('The RTSS converter configuration is not valid! Please check the input values.')

    def set_general_params(self,
                           smoothing: bool,
                           smoothing_sigma: float,
                           smoothing_kernel_size: int,
                           ) -> None:
        """Set the general parameters for all images except those that have specific parameters.

        Args:
            smoothing (bool): Whether to apply Gaussian smoothing.
            smoothing_sigma (float): The sigma of the Gaussian filter.
            smoothing_kernel_size (int): The kernel size of the Gaussian filter.

        Returns:
            None
        """
        self._validate_entries(smoothing, smoothing_sigma, smoothing_kernel_size)

        self.general_params['smoothing'] = smoothing
        self.general_params['smoothing_sigma'] = smoothing_sigma
        self.general_params['smoothing_kernel_size'] = smoothing_kernel_size

    def set_image_params(self,
                         image_identifier: str,
                         smoothing: bool,
                         smoothing_sigma: float,
                         smoothing_kernel_size: int
                         ) -> None:
        """Set the parameters of the converter for a specific image using the :class:`~pyradise.data.organ.Organ`
        instance.

        Args:
            image_identifier (str): The image to set the parameter(s) for.
            smoothing (bool): Whether to apply Gaussian smoothing to the image.
            smoothing_sigma (float): The sigma value for the Gaussian smoothing kernel.
            smoothing_kernel_size (int): The kernel size for the Gaussian smoothing kernel.

        Returns:
            None
        """
        self._validate_entries(smoothing, smoothing_sigma, smoothing_kernel_size)

        self.image_specific_params[image_identifier] = {'smoothing': smoothing,
                                                        'smoothing_sigma': smoothing_sigma,
                                                        'smoothing_kernel_size': smoothing_kernel_size}


class RTSSConverter3DConfiguration(RTSSConverterConfiguration):
    """A configuration class to parameterize a :class:`SegmentToRTSSConverter3D` instance.

    The configuration can be used to set the general and image specific conversion parameters of the converter.
    The general parameters are applied to all images except if image specific parameters are provided.
    used for the image.

    The parameters define the following:

    * ``image_smoothing``: Whether to apply Gaussian smoothing to the image before 3D model construction.

    * ``image_smoothing_sigma``: The standard deviation value for the Gaussian smoothing kernel.

    * ``image_smoothing_radius``: The radius of the Gaussian smoothing kernel.

    * ``image_smoothing_threshold``: The threshold value for the Gaussian smoothing. All segmentation masks that
      have less foreground voxels than this threshold are not smoothed to avoid deletion.

    * ``decimate_reduction``: The reduction factor for the decimation of the 3D model. This factor defines how much
      vertices are removed from the model during the decimation process (0 = none, 1: all).

    * ``decimate_threshold``: The threshold value for the decimation of the 3D model. All models that arise from a
      segmentation mask with less than the threshold number of foreground voxels are not decimated to avoid
      deletion.

    * ``model_smoothing_iterations``: The number of iterations for the smoothing of the 3D model (Sinc-Filter).
      Typically, 15 to 20 iterations are sufficient for smoothing.

    * ``model_smoothing_pass_band``: The pass band for the smoothing of the 3D model (Sinc-Filter). The closer this
      value is to zero (e.g., 0.001) the stronger the smoothing is. The higher the value (e.g., 0.4) the less the
      smoothing is.


    Args:
        image_smoothing (bool): Whether to smooth the image before 3D model construction or not (default: True).
        image_smoothing_sigma (float): The standard deviation of the Gaussian smoothing before 3D model construction
         (default: 2.).
        image_smoothing_radius (float): The radius of the Gaussian smoothing before 3D model construction (default: 1.).
        image_smoothing_threshold (float): The minimum number of foreground voxels that must be contained in the
         segmentation mask to trigger Gaussian smoothing (default: 500).
        decimate_reduction (float): The reduction factor for the 3D decimation. The decimation factor is valid
         between 0 and 1 and the lower it is the more smoothing is applied (default: 0.2).
        decimate_threshold (float): The minimum number of foreground voxels that must be contained in the
         segmentation mask to trigger 3D decimation (default: 500).
        model_smoothing_iterations (int): The number of 3D smoothing steps (typically 15 - 20 steps) (default: 15).
        model_smoothing_pass_band (bool): The strength of the 3D smoothing (0.001 - 0.1 = strong smoothing,
         0.5 - 1 = almost no smoothing) (default: 0.002).
    """

    def __init__(self,
                 image_smoothing: bool = True,
                 image_smoothing_sigma: float = 2.,
                 image_smoothing_radius: float = 1.,
                 image_smoothing_threshold: float = 500.,
                 decimate_reduction: float = 0.2,
                 decimate_threshold: float = 500.,
                 model_smoothing_iterations: int = 15,
                 model_smoothing_pass_band: float = 0.002
                 ) -> None:
        super().__init__()

        self.set_general_params(image_smoothing,
                                image_smoothing_sigma,
                                image_smoothing_radius,
                                image_smoothing_threshold,
                                decimate_reduction,
                                decimate_threshold,
                                model_smoothing_iterations,
                                model_smoothing_pass_band)

    @staticmethod
    def _validate_entries(image_smoothing: bool,
                          image_smoothing_sigma: float,
                          image_smoothing_radius: float,
                          image_smoothing_threshold: float,
                          decimate_reduction: float,
                          decimate_threshold: float,
                          model_smoothing_iterations: int,
                          model_smoothing_pass_band: float
                          ) -> None:
        """Validate the entries of the configuration.

        Args:
            image_smoothing (bool): Whether to smooth the image before 3D model construction or not.
            image_smoothing_sigma (float): The standard deviation of the Gaussian smoothing before 3D model
             construction.
            image_smoothing_radius (float): The radius of the Gaussian smoothing before 3D model construction.
            image_smoothing_threshold (float): The minimum number of foreground voxels that must be contained in the
             segmentation mask to trigger Gaussian smoothing.
            decimate_reduction (float): The reduction factor for the 3D decimation. The decimation factor is valid
             between 0 and 1 and the lower it is the more smoothing is applied.
            decimate_threshold (float): The minimum number of foreground voxels that must be contained in the
             segmentation mask to trigger 3D decimation.
            model_smoothing_iterations (int): The number of 3D smoothing steps (typically 15 - 20 steps).
            model_smoothing_pass_band (float): The strength of the 3D smoothing (0.001 - 0.1 = strong smoothing,
             0.5 - 1 = almost no smoothing).

        Raises:
            ValueError: If the entries are not valid.

        Returns:
            None
        """
        criteria = (isinstance(image_smoothing, bool),
                    isinstance(image_smoothing_sigma, float) and image_smoothing_sigma > 0,
                    isinstance(image_smoothing_radius, float) and image_smoothing_radius > 0,
                    isinstance(image_smoothing_threshold, (float, int)) and image_smoothing_threshold > 0,
                    isinstance(decimate_reduction, float) and 0 < decimate_reduction < 0.99,
                    isinstance(decimate_threshold, (float, int)) and decimate_threshold >= 0,
                    isinstance(model_smoothing_iterations, int) and model_smoothing_iterations >= 0,
                    isinstance(model_smoothing_pass_band, float))

        if not all(criteria):
            raise ValueError('The RTSS converter configuration is not valid! Please check the input values.')

    def set_general_params(self,
                           image_smoothing: bool,
                           image_smoothing_sigma: float,
                           image_smoothing_radius: float,
                           image_smoothing_threshold: float,
                           decimate_reduction: float,
                           decimate_threshold: float,
                           model_smoothing_iterations: int,
                           model_smoothing_pass_band: float
                           ) -> None:
        """Set the general parameters for all images except those that have specific parameters.

        Args:
            image_smoothing (bool): Whether to smooth the image before 3D model construction or not.
            image_smoothing_sigma (float): The standard deviation of the Gaussian smoothing before 3D model
             construction.
            image_smoothing_radius (float): The radius of the Gaussian smoothing before 3D model construction.
            image_smoothing_threshold (float): The minimum number of foreground voxels that must be contained in the
             segmentation mask to trigger Gaussian smoothing.
            decimate_reduction (float): The reduction factor for the 3D decimation. The decimation factor is valid
             between 0 and 1 and the lower it is the more smoothing is applied.
            decimate_threshold (float): The minimum number of foreground voxels that must be contained in the
             segmentation mask to trigger 3D decimation.
            model_smoothing_iterations (int): The number of 3D smoothing steps (typically 15 - 20 steps).
            model_smoothing_pass_band (bool): The strength of the 3D smoothing (0.001 - 0.1 = strong smoothing,
             0.5 - 1 = almost no smoothing).

        Returns:
            None
        """
        self._validate_entries(image_smoothing,
                               image_smoothing_sigma,
                               image_smoothing_radius,
                               image_smoothing_threshold,
                               decimate_reduction,
                               decimate_threshold,
                               model_smoothing_iterations,
                               model_smoothing_pass_band)

        self.general_params['image_smoothing'] = image_smoothing
        self.general_params['image_smoothing_sigma'] = image_smoothing_sigma
        self.general_params['image_smoothing_radius'] = image_smoothing_radius
        self.general_params['image_smoothing_threshold'] = image_smoothing_threshold
        self.general_params['decimate_reduction'] = decimate_reduction
        self.general_params['decimate_threshold'] = decimate_threshold
        self.general_params['model_smoothing_iterations'] = model_smoothing_iterations
        self.general_params['model_smoothing_pass_band'] = model_smoothing_pass_band

    def set_image_params(self,
                         image_identifier: str,
                         image_smoothing: bool,
                         image_smoothing_sigma: float,
                         image_smoothing_radius: float,
                         image_smoothing_threshold: float,
                         decimate_reduction: float,
                         decimate_threshold: float,
                         model_smoothing_iterations: int,
                         model_smoothing_pass_band: float
                         ) -> None:
        """Set the parameters of the converter for a specific image using an identifier.

        Args:
            image_identifier (str): The identifier that identifies the segmentation mask for which the parameters are
             set.
            image_smoothing (bool): Whether to smooth the image before 3D model construction or not.
            image_smoothing_sigma (float): The variance of the Gaussian smoothing before 3D model construction.
            image_smoothing_radius (float): The radius of the Gaussian smoothing before 3D model construction.
            image_smoothing_threshold (float): The minimum number of foreground voxels that must be contained in the
             segmentation mask to trigger Gaussian smoothing.
            decimate_reduction (float): The reduction factor for the 3D decimation. The decimation factor is valid
             between 0 and 1 and the lower it is the more smoothing is applied.
            decimate_threshold (float): The minimum number of foreground voxels that must be contained in the
             segmentation mask to trigger 3D decimation.
            model_smoothing_iterations (int): The number of 3D smoothing steps (typically 15 - 20 steps).
            model_smoothing_pass_band (bool): The strength of the 3D smoothing (0.001 - 0.1 = strong smoothing,
             0.5 - 1 = almost no smoothing).

        Returns:
            None
        """

        self._validate_entries(image_smoothing,
                               image_smoothing_sigma,
                               image_smoothing_radius,
                               image_smoothing_threshold,
                               decimate_reduction,
                               decimate_threshold,
                               model_smoothing_iterations,
                               model_smoothing_pass_band)

        self.image_specific_params[image_identifier] = {'image_smoothing': image_smoothing,
                                                        'image_smoothing_sigma': image_smoothing_sigma,
                                                        'image_smoothing_radius': image_smoothing_radius,
                                                        'image_smoothing_threshold': image_smoothing_threshold,
                                                        'decimate_reduction': decimate_reduction,
                                                        'decimate_threshold': decimate_threshold,
                                                        'model_smoothing_iterations': model_smoothing_iterations,
                                                        'model_smoothing_pass_band': model_smoothing_pass_band}


class SegmentToRTSSConverterBase(Converter):
    """A base class for low-level segmentation mask to DICOM-RTSS converters. This class is not intended to be used
    directly but rather as a base class for more specific conversion algorithm implementations.

    Args:
        label_images (Union[Tuple[str, ...], Tuple[sitk.Image, ...]]): The path to the images or a sequence of
         :class:`SimpleITK.Image` instances.
        ref_image_datasets (Union[Tuple[str, ...], Tuple[Dataset, ...]]): The referenced DICOM image
         :class:`~pydicom.dataset.Dataset` instances.
        roi_names (Union[Tuple[str, ...], Dict[int, str], None]): The label names which will be assigned to the ROIs.
        colors (Optional[Tuple[Tuple[int, int, int], ...]]): The colors which will be assigned to the ROIs.
        meta_data (RTSSMetaData): The configuration to specify certain DICOM attributes (default: RTSSMetaData()).
    """


    def __init__(self,
                 label_images: Union[Tuple[str, ...], Tuple[sitk.Image, ...]],
                 ref_image_datasets: Union[Tuple[str, ...], Tuple[Dataset, ...]],
                 roi_names: Union[Tuple[str, ...], Dict[int, str], None],
                 colors: Optional[Tuple[Tuple[int, int, int], ...]],
                 config: RTSSConverterConfiguration,
                 meta_data: RTSSMetaData = RTSSMetaData(),
                 ) -> None:
        super().__init__()

        # get or load the label images
        self.label_images: Tuple[sitk.Image, ...] = label_images if isinstance(label_images[0], sitk.Image) \
            else tuple([sitk.ReadImage(path, sitk.sitkUInt8) for path in label_images])

        # get or load the reference image datasets
        self.image_datasets: Tuple[Dataset, ...] = ref_image_datasets if isinstance(ref_image_datasets[0], Dataset) \
            else load_datasets(ref_image_datasets)

        # get the ROI names
        if isinstance(roi_names, dict):
            sorted_keys = sorted(roi_names.keys())
            self.roi_names = tuple([str(roi_names.get(key)) for key in sorted_keys])
        elif not roi_names:
            self.roi_names = tuple([f'Structure_{i}' for i in range(len(self.label_images))])
        else:
            self.roi_names = roi_names

        # get the colors
        if not colors:
            self.colors = COLOR_PALETTE
        else:
            self.colors = colors

        # check if the correct number of ROI names and colors are provided
        if len(self.roi_names) < len(self.label_images):
            raise ValueError('The number of ROI names must be equal or larger than the number of label images!')

        if len(self.colors) < len(self.label_images):
            raise ValueError('The number of colors must be equal or larger than the number of label images!')

        # get the meta data
        self.meta_data = meta_data

        # get the configuration
        self.config = config

    def _validate_label_images(self) -> None:
        """Validate the label images.

        Raises:
            ValueError: If the label images are not binary or the pixel type is not an integer.
        """
        # check if the label images are binary and that they have an integer pixel type
        for image in self.label_images:
            if np.unique(sitk.GetArrayFromImage(image)).size > 2:
                raise ValueError('The label images must be binary!')

            if 'float' in image.GetPixelIDTypeAsString():
                raise ValueError('The label images must have an integer pixel type!')


    def _generate_basic_rtss(self) -> FileDataset:
        """Generate the basic RTSS skeleton.

        Returns:
            FileDataset: The basic RT Structure Set.
        """
        # create the file meta for the dataset
        file_meta = FileMetaDataset()
        file_meta.FileMetaInformationGroupLength = 202
        file_meta.FileMetaInformationVersion = b'\x00\x01'
        file_meta.TransferSyntaxUID = ImplicitVRLittleEndian
        file_meta.MediaStorageSOPClassUID = '1.2.840.10008.5.1.4.1.1.481.3'
        file_meta.MediaStorageSOPInstanceUID = generate_uid()
        file_meta.ImplementationClassUID = PYDICOM_IMPLEMENTATION_UID

        # create the dataset
        rtss = FileDataset('rt_struct', {}, file_meta=file_meta, preamble=b"\0" * 128)

        # add the basic information
        now = datetime.now()
        rtss.SpecificCharacterSet = 'ISO_IR 100'
        rtss.InstanceCreationDate = now.strftime('%Y%m%d')
        rtss.InstanceCreationTime = now.strftime('%H%M%S.%f')
        rtss.StructureSetLabel = self.meta_data.structure_set_label
        rtss.StructureSetDate = now.strftime('%Y%m%d')
        rtss.StructureSetTime = now.strftime('%H%M%S.%f')
        rtss.Modality = 'RTSTRUCT'
        rtss.Manufacturer = self.meta_data.manufacturer
        rtss.ManufacturerModelName = self.meta_data.manufacturer_model_name
        rtss.InstitutionName = self.meta_data.institution_name
        rtss.OperatorsName = self.meta_data.operators_name
        rtss.ApprovalStatus = self.meta_data.approval_status

        rtss.is_little_endian = True
        rtss.is_implicit_VR = True

        # set values already defined in the file meta
        rtss.SOPClassUID = rtss.file_meta.MediaStorageSOPClassUID
        rtss.SOPInstanceUID = rtss.file_meta.MediaStorageSOPInstanceUID

        # add study and series information
        reference_dataset = self.image_datasets[0]
        rtss.StudyDate = reference_dataset.StudyDate
        rtss.SeriesDate = getattr(reference_dataset, 'SeriesDate', '')
        rtss.StudyTime = reference_dataset.StudyTime
        rtss.SeriesTime = getattr(reference_dataset, 'SeriesTime', '')

        if self.meta_data.study_description is None:
            rtss.StudyDescription = getattr(reference_dataset, 'StudyDescription', '')
        else:
            rtss.StudyDescription = self.meta_data.study_description

        if self.meta_data.series_description is None:
            rtss.SeriesDescription = getattr(reference_dataset, 'SeriesDescription', '')
        else:
            rtss.SeriesDescription = self.meta_data.series_description

        rtss.StudyInstanceUID = reference_dataset.StudyInstanceUID
        rtss.SeriesInstanceUID = generate_uid()
        rtss.StudyID = reference_dataset.StudyID
        rtss.SeriesNumber = self.meta_data.series_number
        rtss.ReferringPhysicianName = self.meta_data.referring_physician_name
        rtss.AccessionNumber = "0"

        # add the patient information
        if self.meta_data.patient_name is None:
            rtss.PatientName = getattr(reference_dataset, 'PatientName', '')
        else:
            rtss.PatientName = self.meta_data.patient_name

        if self.meta_data.patient_id is None:
            rtss.PatientID = getattr(reference_dataset, 'PatientID', '')
        else:
            rtss.PatientID = self.meta_data.patient_id

        if self.meta_data.patient_birth_date is None:
            rtss.PatientBirthDate = getattr(reference_dataset, 'PatientBirthDate', '')
        else:
            rtss.PatientBirthDate = self.meta_data.patient_birth_date

        if self.meta_data.patient_sex is None:
            rtss.PatientSex = getattr(reference_dataset, 'PatientSex', '')
        else:
            rtss.PatientSex = self.meta_data.patient_sex

        if self.meta_data.patient_age is None:
            rtss.PatientAge = getattr(reference_dataset, 'PatientAge', '')
        else:
            rtss.PatientAge = self.meta_data.patient_age

        if self.meta_data.patient_weight is None:
            rtss.PatientWeight = getattr(reference_dataset, 'PatientWeight', '')
        else:
            rtss.PatientWeight = self.meta_data.patient_weight

        if self.meta_data.patient_size is None:
            rtss.PatientSize = getattr(reference_dataset, 'PatientSize', '')
        else:
            rtss.PatientSize = self.meta_data.patient_size

        # construct the ContourImageSequence
        contour_image_sequence = Sequence()
        for image_dataset in self.image_datasets:
            contour_image_entry = Dataset()
            contour_image_entry.ReferencedSOPClassUID = image_dataset.file_meta.MediaStorageSOPClassUID
            contour_image_entry.ReferencedSOPInstanceUID = image_dataset.file_meta.MediaStorageSOPInstanceUID
            contour_image_sequence.append(contour_image_entry)

        # construct the RTReferencedSeriesSequence
        rt_referenced_series_sequence = Sequence()
        rt_referenced_series_entry = Dataset()
        rt_referenced_series_entry.SeriesInstanceUID = reference_dataset.SeriesInstanceUID
        rt_referenced_series_entry.ContourImageSequence = contour_image_sequence
        rt_referenced_series_sequence.append(rt_referenced_series_entry)

        # construct the RTReferencedStudySequence
        rt_referenced_study_sequence = Sequence()
        rt_referenced_study_entry = Dataset()
        rt_referenced_study_entry.ReferencedSOPClassUID = '1.2.840.10008.3.1.2.3.1'  # RT Structure Set Storage
        rt_referenced_study_entry.ReferencedSOPInstanceUID = reference_dataset.StudyInstanceUID
        rt_referenced_study_entry.RTReferencedSeriesSequence = rt_referenced_series_sequence
        rt_referenced_study_sequence.append(rt_referenced_study_entry)

        # construct the ReferencedFrameOfReferenceSequence
        rtss.ReferencedFrameOfReferenceSequence = Sequence()
        referenced_frame_of_ref_entry = Dataset()
        referenced_frame_of_ref_entry.FrameOfReferenceUID = reference_dataset.FrameOfReferenceUID
        referenced_frame_of_ref_entry.RTReferencedStudySequence = rt_referenced_study_sequence
        rtss.ReferencedFrameOfReferenceSequence.append(referenced_frame_of_ref_entry)

        # construct the ROIContourSequence, StructureSetROISequence and RTROIObservationsSequence
        rtss.ROIContourSequence = Sequence()
        rtss.StructureSetROISequence = Sequence()
        rtss.RTROIObservationsSequence = Sequence()

        return rtss


    @staticmethod
    def _append_rt_roi_observation(roi_number: int,
                                   rtss: Dataset
                                   ) -> None:
        """Create a RTROIObservationsSequence entry for the given ROI data.

        Args:
            roi_number (int): The ROI data to be used for creating the RTROIObservationsSequence entry.
            rtss (Dataset): The RTSS to be used for creating the RTROIObservationsSequence entry.

        Returns:
            None
        """
        # generate the RTROIObservationsSequence entry
        rt_roi_observation = Dataset()
        rt_roi_observation.ObservationNumber = roi_number
        rt_roi_observation.ReferencedROINumber = roi_number
        rt_roi_observation.ROIObservationDescription = 'Type:Soft,Range:*/*,Fill:0,Opacity:0.0,Thickness:1,' \
                                                       'LineThickness:2,read-only:false'
        rt_roi_observation.private_creators = 'University of Bern, Switzerland'
        rt_roi_observation.RTROIInterpretedType = ''
        rt_roi_observation.ROIInterpreter = ''

        # add the RTROIObservationsSequence entry to the RTSS
        rtss.RTROIObservationsSequence.append(rt_roi_observation)

    def _append_structure_set_roi_sequence_entry(self,
                                                 roi_name: str,
                                                 roi_number: int,
                                                 rtss: Dataset
                                                 ) -> None:
        """Append a StructureSetROISequence entry to the given RTSS.

        Args:
            roi_name (str): The name of the ROI.
            roi_number (int): The number of the ROI.
            rtss (Dataset): The RTSS to be used for creating the StructureSetROISequence entry.

        Returns:
            None
        """
        # generate the StructureSetROISequence entry
        structure_set_roi = Dataset()
        structure_set_roi.ROINumber = roi_number
        structure_set_roi.ReferencedFrameOfReferenceUID = self.image_datasets[0].get('FrameOfReferenceUID')
        structure_set_roi.ROIName = roi_name
        structure_set_roi.ROIDescription = ''
        structure_set_roi.ROIGenerationAlgorithm = self.meta_data.roi_gen_algorithm

        # add the StructureSetROISequence entry to the RTSS
        rtss.StructureSetROISequence.append(structure_set_roi)


    @abstractmethod
    def convert(self) -> Any:
        """Abstract method for starting the conversion procedure.

        Returns:
            Any: The converted data.
        """
        raise NotImplementedError()


class SegmentToRTSSConverter2D(SegmentToRTSSConverterBase):
    """A low-level 2D-based :class:`Converter` class for converting one or multiple
    :class:`~pyradise.data.image.SegmentationImage` instances to a DICOM-RTSS :class:`~pydicom.dataset.Dataset`.
    In contrast to the :class:`SegmentToRTSSConverter3D` class, this class generates the DICOM-RTSS contours using a
    two-dimensional approach. This reduces the computation time and leads to a more robust conversion procedure.
    However, this class has limitations such as the inability to smooth contours in all three dimensions. Furthermore,
    the resulting contours may appear to be artificially generated and not as smooth as the ones generated by the
    :class:`SegmentToRTSSConverter3D` class.

    Warning:
        The provided ``label_images`` must be binary, otherwise the conversion will fail.

    Note:
        Typically, this class is not used directly by the used but via the :class:`SubjectToRTSSConverter` which
        processes :class:`~pyradise.fileio.series_info.DicomSeriesInfo` entries and thus provides a more suitable
        interface.

    Note:
        This class can take a :class:`RTSSMetaData` instance as input to specify certain DICOM attributes of the
        output DICOM-RTSS. If no instance is provided, the default values will be used.

    Args:
        label_images (Union[Tuple[str, ...], Tuple[sitk.Image, ...]]): The path to the images or a sequence of
         :class:`SimpleITK.Image` instances.
        ref_image_datasets (Union[Tuple[str, ...], Tuple[Dataset, ...]]): The referenced DICOM image
         :class:`~pydicom.dataset.Dataset` instances.
        roi_names (Union[Tuple[str, ...], Dict[int, str], None]): The label names which will be assigned to the ROIs.
        colors (Optional[Tuple[Tuple[int, int, int], ...]]): The colors which will be assigned to the ROIs.
        meta_data (RTSSMetaData): The configuration to specify certain DICOM attributes (default: RTSSMetaData()).
        config (RTSSConverter2DConfiguration): The configuration to specify certain conversion parameters (default:
         RTSSConverter2DConfiguration()).
    """

    def __init__(self,
                 label_images: Union[Tuple[str, ...], Tuple[sitk.Image, ...]],
                 ref_image_datasets: Union[Tuple[str, ...], Tuple[Dataset, ...]],
                 roi_names: Union[Tuple[str, ...], Dict[int, str], None],
                 colors: Optional[Tuple[Tuple[int, int, int], ...]],
                 meta_data: RTSSMetaData = RTSSMetaData(),
                 config: RTSSConverter2DConfiguration = RTSSConverter2DConfiguration()
                 ) -> None:
        super().__init__(label_images, ref_image_datasets, roi_names, colors, config, meta_data)

        self.config = config
        self._validate_label_images()

    @staticmethod
    def _append_roi_contour(mask: np.ndarray,
                            image_datasets: Tuple[Dataset, ...],
                            rtss: Dataset,
                            roi_color: Tuple[int, int, int],
                            roi_number: int,
                            ) -> None:
        """Create a ROIContourSequence entry for the given ROI data.

        Args:
            mask (np.ndarray): The ROI mask to generate the contours from.
            image_datasets (Tuple[Dataset, ...]): The referenced image datasets.
            rtss (Dataset): The RTSS dataset.

        Returns:
            None
        """
        roi_contour = Dataset()
        roi_contour.ROIDisplayColor = roi_color
        roi_contour.ContourSequence = SegmentToRTSSConverter2D._create_contour_sequence(mask, image_datasets)
        roi_contour.ReferencedROINumber = str(roi_number)

        # add the ROIContourSequence entry to the RTSS
        rtss.ROIContourSequence.append(roi_contour)

    @staticmethod
    def _create_contour_sequence(mask: np.ndarray,
                                 image_datasets: Tuple[Dataset, ...]
                                 ) -> Sequence:
        """Create a ContourSequence for the given ROI data by iterating through each slice of the mask.
        For each connected segment within a slice, a ContourSequence entry is created.

        Args:
            mask (np.ndarray): The ROI mask for generating the contours.
            image_datasets (Tuple[Dataset, ...]): The referenced image datasets.

        Returns:
            Sequence: The created ContourSequence.
        """
        contour_sequence = Sequence()

        contours_coordinates = SegmentToRTSSConverter2D._get_contours_coordinates(mask, image_datasets)

        for series_slice, slice_contours in zip(image_datasets, contours_coordinates):
            for contour_data in slice_contours:
                if len(contour_data) <= 3:
                    continue
                contour_seq_entry = SegmentToRTSSConverter2D._create_contour_sequence_entry(series_slice, contour_data)
                contour_sequence.append(contour_seq_entry)

        return contour_sequence

    @staticmethod
    def _get_contours_coordinates(mask: np.ndarray,
                                  image_datasets: Tuple[Dataset, ...]
                                  ) -> List[List[List[float]]]:
        """Get the contour coordinates for each slice of the mask.

        Args:
            mask (np.ndarray): The ROI mask for generating the contours.
            image_datasets (Tuple[Dataset, ...]): The referenced image datasets.

        Returns:
            List[List[List[float]]]: The contour coordinates for each slice of the mask.

        """
        transform_matrix = SegmentToRTSSConverter2D._get_pixel_to_patient_transformation_matrix(image_datasets)

        series_contours = []
        for i in range(len(image_datasets)):
            mask_slice = mask[i, :, :]

            # Do not add ROI's for blank slices
            if np.sum(mask_slice) == 0:
                series_contours.append([])
                continue

            # Get contours from mask
            contours, _ = SegmentToRTSSConverter2D._find_mask_contours(mask_slice)

            if not contours:
                raise Exception('Unable to find contour in non empty mask, please check your mask formatting!')

            # Format for DICOM
            formatted_contours = []
            for contour in contours:
                # Add z index
                contour = np.concatenate((np.array(contour), np.full((len(contour), 1), i)), axis=1)

                transformed_contour = SegmentToRTSSConverter2D._apply_transformation_to_3d_points(contour,
                                                                                                  transform_matrix)
                dicom_formatted_contour = np.ravel(transformed_contour).tolist()
                formatted_contours.append(dicom_formatted_contour)

            series_contours.append(formatted_contours)

        return series_contours

    @staticmethod
    def _get_pixel_to_patient_transformation_matrix(image_datasets: Tuple[Dataset]) -> np.ndarray:
        """Get the pixel to patient transformation matrix according to the referenced image datasets.

        Notes:
            Description see: https://nipy.org/nibabel/dicom/dicom_orientation.html

        Args:
            image_datasets (Tuple[Dataset]): The image datasets with the necessary information to retrieve the pixel
             to patient transformation matrix.

        Returns:
            np.ndarray: The pixel to patient transformation matrix.
        """

        first_slice = image_datasets[0]

        offset = np.array(first_slice.ImagePositionPatient)
        row_spacing, column_spacing = first_slice.PixelSpacing
        slice_spacing = get_spacing_between_slices(image_datasets)
        row_direction, column_direction, slice_direction = get_slice_direction(first_slice)

        mat = np.identity(4, dtype=np.float32)
        mat[:3, 0] = row_direction * row_spacing
        mat[:3, 1] = column_direction * column_spacing
        mat[:3, 2] = slice_direction * slice_spacing
        mat[:3, 3] = offset

        return mat

    @staticmethod
    def _find_mask_contours(mask: np.ndarray,
                            approximate_contours: bool = True
                            ) -> Tuple[List[np.ndarray], List]:
        """Find the contours in the provided mask.

        Args:
            mask (np.ndarray): The mask to be used for finding the contours.
            approximate_contours (bool): Whether to approximate the contours (default: True).

        Returns:
            Tuple[List[np.ndarray], List]: The contours and the hierarchy.
        """
        method = cv.CHAIN_APPROX_SIMPLE if approximate_contours else cv.CHAIN_APPROX_NONE
        contours, hierarchy = cv.findContours(mask.astype(np.uint8), cv.RETR_TREE, method)
        contours = list(contours)

        for i, contour in enumerate(contours):
            contours[i] = [[pos[0][0], pos[0][1]] for pos in contour]
        hierarchy = hierarchy[0]

        return contours, hierarchy

    @staticmethod
    def _apply_transformation_to_3d_points(points: np.ndarray,
                                           transformation_matrix: np.ndarray
                                           ) -> np.ndarray:
        """Apply the given transformation matrix to the given 3D points.

        Notes:
            The transformation matrix is expected to be a 4x4 matrix (homogeneous coordinate transform).


        Args:
            points (np.ndarray): The points to be transformed.
            transformation_matrix (np.ndarray): The transformation matrix.

        Returns:
            np.ndarray: The transformed points.
        """
        vec = np.concatenate((points, np.ones((points.shape[0], 1))), axis=1)
        return vec.dot(transformation_matrix.T)[:, :3]

    @staticmethod
    def _create_contour_sequence_entry(series_slice: Dataset,
                                       contour_data: List[float]
                                       ) -> Dataset:
        """Create a contour sequence entry for the given slice.

        Args:
            series_slice (Dataset): The slice to be used for creating the contour sequence entry.
            contour_data (List[float]): The contour data to be used for creating the contour sequence entry.

        Returns:
            Dataset: The contour sequence entry.
        """
        # create the ContourImageSequence entry
        contour_image = Dataset()
        contour_image.ReferencedSOPClassUID = series_slice.file_meta.MediaStorageSOPClassUID
        contour_image.ReferencedSOPInstanceUID = series_slice.file_meta.MediaStorageSOPInstanceUID

        # construct the ContourImageSequence
        contour_image_sequence = Sequence()
        contour_image_sequence.append(contour_image)

        # construct the ContourSequence entry
        contour = Dataset()
        contour.ContourImageSequence = contour_image_sequence
        contour.ContourGeometricType = 'CLOSED_PLANAR'
        contour.NumberOfContourPoints = len(contour_data) // 3  # each contour point consists of an x, y, and z value
        contour.ContourData = contour_data

        return contour

    @staticmethod
    def _adjust_label_image_to_dicom(label_image: sitk.Image,
                                     image_datasets: Tuple[Dataset, ...]
                                     ) -> sitk.Image:
        """Adjust the given label image to the DICOM image.

        Args:
            label_image (sitk.Image): The label image to be adjusted.
            image_datasets (Tuple[Dataset, ...]): The DICOM image datasets.

        Returns:
            sitk.Image: The adjusted label image.
        """
        # construct a reference image from the DICOM datasets

        # compute the direction from the DICOM datasets
        vec_0 = image_datasets[0].ImageOrientationPatient[:3]
        vec_1 = image_datasets[0].ImageOrientationPatient[3:]
        vec_2 = np.cross(np.array(vec_0), np.array(vec_1))
        dicom_direction = np.stack((vec_0, vec_1, vec_2)).T.reshape(-1).tolist()

        # compute the origin from the DICOM datasets
        dicom_origin = image_datasets[0].ImagePositionPatient

        # compute the spacing from the DICOM datasets
        dicom_spacing = [float(image_datasets[0].PixelSpacing[0]),
                         float(image_datasets[0].PixelSpacing[1]),
                         get_spacing_between_slices(image_datasets)]

        # compute the size from the DICOM datasets
        # note: sizes must be in z, x, y order because numpy reverses axes
        dicom_size = (len(image_datasets),
                      image_datasets[0].Rows,
                      image_datasets[0].Columns)

        # construct the reference image via numpy
        reference_image_np = np.zeros(dicom_size)
        reference_image_sitk = sitk.GetImageFromArray(reference_image_np)
        reference_image_sitk.SetDirection(dicom_direction)
        reference_image_sitk.SetOrigin(dicom_origin)
        reference_image_sitk.SetSpacing(dicom_spacing)

        # orient the label image to LPS (DICOM standard)
        label_image_0 = sitk.DICOMOrient(label_image, 'LPS')

        # resample the oriented label image to the reference image
        label_image_1 = sitk.Resample(label_image_0,
                                      reference_image_sitk,
                                      sitk.Transform(),
                                      sitk.sitkNearestNeighbor, 0.0, sitk.sitkUInt8)

        return label_image_1


    @staticmethod
    def _smooth_label_image(label_image: sitk.Image,
                            kernel_size: int,
                            sigma: float
                            ) -> sitk.Image:
        """Apply Gaussian smoothing to a label image.

        Args:
            label_image (sitk.Image): The image to be smoothened.
            kernel_size (int): The maximum kernel size for the Gaussian kernel.
            sigma (float): The sigma for the Gaussian Kernel.

        Returns:
             sitk.Image: The smoothened image.
        """
        label_image_0 = sitk.Cast(label_image, sitk.sitkFloat32)

        num_dims = label_image.GetDimension()
        label_image_1 = sitk.DiscreteGaussian(label_image_0,
                                              variance=[sigma] * num_dims,
                                              maximumKernelWidth=kernel_size)
        label_image_2 = sitk.BinaryThreshold(label_image_1, 0.5, 1.0, 1, 0)

        return label_image_2

    def convert(self) -> Dataset:
        """Convert the provided :class:`SimpleITK.Image` instances to a DICOM-RTSS :class:`~pydicom.dataset.Dataset`
        instance using a two-dimensional reconstruction algorithm.

        Returns:
            Dataset: The generated DICOM-RTSS :class:`~pydicom.dataset.Dataset`.
        """
        # generate the basic RTSS dataset
        rtss = self._generate_basic_rtss()

        # convert and add the ROIs to the RTSS
        for idx, (label, name, color) in enumerate(zip(self.label_images, self.roi_names, self.colors)):
            # check if a specific parameterization must be used for this image
            if self.config.get_image_params(name) is not None:
                smooth = self.config.get_image_params(name, 'smoothing')
                kernel = self.config.get_image_params(name, 'smoothing_kernel_size')
                sigma = self.config.get_image_params(name, 'smoothing_sigma')
            else:
                config = self.config.get_general_params()
                smooth = config.get('smoothing')
                kernel = config.get('smoothing_kernel_size')
                sigma = config.get('smoothing_sigma')

            # adjust the label image to have the same properties as the DICOM datasets
            label_processed = self._adjust_label_image_to_dicom(label, self.image_datasets)

            # smooth the label image
            if smooth:
                label_processed = self._smooth_label_image(label_processed, kernel, sigma)

            # get the binary image data from the label image
            mask = sitk.GetArrayFromImage(label_processed)

            self._append_roi_contour(mask, self.image_datasets, rtss, color, idx + 1)
            self._append_structure_set_roi_sequence_entry(name, idx + 1, rtss)
            self._append_rt_roi_observation(idx + 1, rtss)

        return rtss


class SegmentToRTSSConverter3D(SegmentToRTSSConverterBase):
    """A low-level 3D-based :class:`Converter` class for converting one or multiple
    :class:`~pyradise.data.image.SegmentationImage` instances to a DICOM-RTSS :class:`~pydicom.dataset.Dataset`.
    In contrast to the :class:`SegmentToRTSSConverter2D` class, this class generates the DICOM-RTSS contours using a
    three-dimensional approach. This reduces spatial inconsistencies but comes at the cost of a longer computation time
    and higher memory consumption. Furthermore, this converter is less robust than its two-dimensional counterpart.
    However, the resulting contours are more accurate and appear more natural it the converter is applied with
    appropriate parameterization.

    Warning:
        The provided ``label_images`` must be binary, otherwise the conversion will fail.

    Note:
        Typically, this class is not used directly by the used but via the :class:`SubjectToRTSSConverter` which
        processes :class:`~pyradise.fileio.series_info.DicomSeriesInfo` entries and thus provides a more suitable
        interface.

    Note:
        This class can take a :class:`RTSSMetaData` instance as input to specify certain DICOM attributes of the
        output DICOM-RTSS. If no instance is provided, the default values will be used.

    Args:
        label_images (Union[Tuple[str, ...], Tuple[sitk.Image, ...]]): The path to the images or a sequence of
         :class:`SimpleITK.Image` instances.
        ref_image_datasets (Union[Tuple[str, ...], Tuple[Dataset, ...]]): The referenced DICOM image
         :class:`~pydicom.dataset.Dataset` instances.
        roi_names (Union[Tuple[str, ...], Dict[int, str], None]): The label names which will be assigned to the ROIs.
        colors (Optional[Tuple[Tuple[int, int, int], ...]]): The colors which will be assigned to the ROIs.
        meta_data (RTSSMetaData): The configuration to specify certain DICOM attributes (default: RTSSMetaData()).
        config (RTSSConverter3DConfiguration): The configuration to specify certain conversion parameters (default:
         RTSSConverter3DConfiguration()).
    """

    def __init__(self,
                 label_images: Union[Tuple[str, ...], Tuple[sitk.Image, ...]],
                 ref_image_datasets: Union[Tuple[str, ...], Tuple[Dataset, ...]],
                 roi_names: Union[Tuple[str, ...], Dict[int, str], None],
                 colors: Optional[Tuple[Tuple[int, int, int], ...]],
                 meta_data: RTSSMetaData = RTSSMetaData(),
                 config: RTSSConverter3DConfiguration = RTSSConverter3DConfiguration()
                 ):
        super().__init__(label_images, ref_image_datasets, roi_names, colors, config, meta_data)

        self.config = config
        self._validate_label_images()


    def preprocess_image(self, image_sitk: sitk.Image) -> sitk.Image:
        """Preprocess the provided :class:`SimpleITK.Image` instance such that the image has the same properties as
        the referenced DICOM image series.

        Args:
            image_sitk (sitk.Image): The :class:`SimpleITK.Image` instance to preprocess.

        Returns:
            sitk.Image: The preprocessed :class:`SimpleITK.Image` instance.
        """
        # orient the image to the LPS coordinate system (-> DICOM standard)
        image_sitk = sitk.DICOMOrient(image_sitk, 'LPS')

        # resample to match the DICOM image
        origin = self.image_datasets[0].ImagePositionPatient
        slice_spacing = get_spacing_between_slices(self.image_datasets)
        spacing = (float(self.image_datasets[0].PixelSpacing[0]),
                   float(self.image_datasets[0].PixelSpacing[1]),
                   slice_spacing)
        direction = np.array(get_slice_direction(self.image_datasets[0])).T.flatten()
        size = (self.image_datasets[0].Rows,
                self.image_datasets[0].Columns,
                len(self.image_datasets))

        resampler = sitk.ResampleImageFilter()
        resampler.SetOutputOrigin(origin)
        resampler.SetOutputSpacing(spacing)
        resampler.SetOutputDirection(direction)
        resampler.SetSize(size)
        resampler.SetTransform(sitk.Transform())
        resampler.SetOutputPixelType(sitk.sitkUInt8)
        resampler.SetInterpolator(sitk.sitkNearestNeighbor)
        resampler.SetDefaultPixelValue(0)
        image_sitk = resampler.Execute(image_sitk)

        # convert to binary
        image_sitk = sitk.BinaryThreshold(image_sitk, 1, 255, 255, 0)

        return image_sitk

    @staticmethod
    def _get_3d_model(sitk_image: sitk.Image,
                      image_smoothing: bool,
                      smooth_sigma: float,
                      smooth_radius: float,
                      smooth_threshold: float,
                      decimate_reduction: float,
                      decimate_threshold: float,
                      model_smooth_iter: int,
                      model_smooth_pass_band: float
                      ) -> vtk_dm.vtkPolyData:
        """Generate a 3D model of the provided :class:`SimpleITK.Image` instance.

        Args:
            sitk_image (sitk.Image): The :class:`SimpleITK.Image` instance to generate the 3D model for.
            image_smoothing (bool): Whether to smooth the image before generating the 3D model.
            smooth_sigma (float): The sigma value for the Gaussian smoothing.
            smooth_radius (float): The radius value for the Gaussian smoothing.
            smooth_threshold (float): The threshold value for the Gaussian smoothing.
            decimate_reduction (float): The reduction value for the decimation.
            decimate_threshold (float): The threshold value for the decimation.
            model_smooth_iter (int): The number of iterations for the model smoothing.
            model_smooth_pass_band (float): The pass band value for the model smoothing.

        Returns:
            vtk_dm.vtkPolyData: The 3D model of the provided :class:`SimpleITK.Image` instance.
        """
        # cast the image to vtkImageData
        itk_image = convert_to_itk_image(sitk_image)
        vtk_image = itk.vtk_image_from_image(itk_image)

        # set the direction matrix
        vtk_image.SetDirectionMatrix(sitk_image.GetDirection())

        # pad the image to avoid boundary effects
        extent = vtk_image.GetExtent()
        new_extent = (extent[0] - 5, extent[1] + 5, extent[2] - 5, extent[3] + 5, extent[4] - 5, extent[5] + 5)
        padder = vtk_icore.vtkImageConstantPad()
        padder.SetInputDataObject(0, vtk_image)
        padder.SetOutputWholeExtent(new_extent)
        padder.Update(0)
        vtk_image = padder.GetOutput()

        # apply gaussian smoothing
        foreground_amount = sitk.GetArrayFromImage(sitk_image).sum() / 255
        if foreground_amount > smooth_threshold and image_smoothing:
            gaussian = vtk_igen.vtkImageGaussianSmooth()
            gaussian.SetInputDataObject(0, vtk_image)
            gaussian.SetStandardDeviation(smooth_sigma)
            gaussian.SetRadiusFactor(smooth_radius)
            gaussian.Update(0)
            vtk_image = gaussian.GetOutputDataObject(0)

        # apply flying edges
        flying_edges = vtk_fcore.vtkFlyingEdges3D()
        flying_edges.SetInputDataObject(0, vtk_image)
        flying_edges.SetValue(0, 127.5)
        flying_edges.ComputeGradientsOff()
        flying_edges.ComputeNormalsOff()
        flying_edges.Update(0)
        model = flying_edges.GetOutputDataObject(0)

        # apply decimation
        if foreground_amount > decimate_threshold:
            decimate = vtk_fcore.vtkDecimatePro()
            decimate.SetInputConnection(0, flying_edges.GetOutputPort(0))
            decimate.SetTargetReduction(decimate_reduction)
            decimate.PreserveTopologyOn()
            decimate.SetMaximumError(1)
            decimate.SplittingOff()
            decimate.SetFeatureAngle(60.)
            decimate.Update(0)
            model = decimate.GetOutputDataObject(0)

        # smooth via the sinc filter
        if model_smooth_iter > 0:
            smoother = vtk_fcore.vtkWindowedSincPolyDataFilter()
            smoother.SetInputDataObject(0, model)
            smoother.SetNumberOfIterations(model_smooth_iter)
            smoother.BoundarySmoothingOff()
            smoother.FeatureEdgeSmoothingOff()
            smoother.SetFeatureAngle(60.0)
            smoother.SetPassBand(model_smooth_pass_band)
            smoother.NonManifoldSmoothingOn()
            smoother.NormalizeCoordinatesOff()
            smoother.Update(0)
            model = smoother.GetOutputDataObject(0)

        # get normals
        normals = vtk_fcore.vtkPolyDataNormals()
        normals.SetInputDataObject(0, model)
        normals.SetFeatureAngle(60.0)

        # strip the polydata
        stripper = vtk_fcore.vtkStripper()
        stripper.SetInputConnection(0, normals.GetOutputPort(0))
        stripper.JoinContiguousSegmentsOn()
        stripper.Update(0)
        stripped = stripper.GetOutput()

        return stripped

    def _get_2d_contours(self, polydata: vtk_dm.vtkPolyData) -> List[List[List[List[float]]]]:
        """Get the 2D contours of the provided :class:`vtk.vtkPolyData` instance that correspond with the referenced
        DICOM image series.

        Args:
            polydata (vtk.vtkPolyData): The :class:`vtk.vtkPolyData` instance to get the 2D contours for.

        Returns:
            List[List[List[List[float]]]]: The 2D contours of the provided :class:`vtk.vtkPolyData` instance that
            correspond with the referenced DICOM image series.
        """
        origin = self.image_datasets[0].ImagePositionPatient
        origin = [float(val) for val in origin]
        first_pos = self.image_datasets[0].ImagePositionPatient
        last_pos = self.image_datasets[-1].ImagePositionPatient
        length = np.abs(np.linalg.norm(np.array(last_pos) - np.array(first_pos)))
        slice_spacing = length / (len(self.image_datasets) - 1)
        normal = tuple(np.cross(np.array(self.image_datasets[0].get('ImageOrientationPatient')[0:3]),
                                np.array(self.image_datasets[0].get('ImageOrientationPatient')[3:6])))

        # create the initial cutting plane
        plane = vtk_dm.vtkPlane()
        plane.SetOrigin(*origin)
        plane.SetNormal(*normal)

        # create the cutter
        cutter = vtk_fcore.vtkCutter()
        cutter.SetInputDataObject(0, polydata)
        cutter.SetCutFunction(plane)
        cutter.GenerateTrianglesOn()
        cutter.GenerateValues(len(self.image_datasets), 0, length)

        # create the cleaner
        cleaner = vtk_fcore.vtkCleanPolyData()
        cleaner.SetInputConnection(0, cutter.GetOutputPort(0))
        cleaner.SetAbsoluteTolerance(0.01)
        cleaner.PointMergingOn()
        cleaner.Update(0)

        # get the polylines
        loop = vtk_fmodel.vtkContourLoopExtraction()
        loop.SetInputConnection(0, cleaner.GetOutputPort(0))
        loop.SetOutputModeToPolylines()
        loop.SetNormal(*normal)
        loop.Update(0)
        looped = loop.GetOutput()

        # get the polylines for each slice if there are any
        cells = looped.GetLines()
        points = looped.GetPoints()
        contours_points = []

        indices = vtk_ccore.vtkIdList()
        cell_indicator = cells.GetNextCell(indices)

        for slice_idx, dataset in enumerate(self.image_datasets):
            slice_plane = vtk_dm.vtkPlane()
            slice_plane.SetOrigin(*dataset.ImagePositionPatient)
            slice_plane.SetNormal(*normal)

            if cell_indicator:
                point = points.GetPoint(indices.GetId(0))
                distance = slice_plane.DistanceToPlane(point)
            else:
                distance = 2 * slice_spacing

            if distance <= slice_spacing and cell_indicator == 1:
                contour_points = []
                for i in range(indices.GetNumberOfIds()):
                    point = list(points.GetPoint(indices.GetId(i)))
                    contour_points.append(point)
                contours_points.append(contour_points)
                cell_indicator = cells.GetNextCell(indices)

            else:
                contours_points.append(None)

        return contours_points


    def _append_roi_contour_sequence_entry(self,
                                           contours: List[List[List[List[float]]]],
                                           color: Tuple[int, int, int],
                                           roi_number: int,
                                           rtss: Dataset
                                           ) -> None:
        """Append a ROIContourSequence entry to the given DICOM-RTSS.

        Args:
            contours (List[List[List[List[float]]]]): The 2D contours of the ROI.
            color (Tuple[int, int, int]): The color of the ROI.
            roi_number (int): The ROI number.
            rtss (Dataset): The DICOM-RTSS to append the ROIContourSequence entry to.

        Returns:
            None
        """
        roi_contour = Dataset()
        roi_contour.ROIDisplayColor = list(color)
        roi_contour.ReferencedROINumber = str(roi_number)

        # create the contour sequence
        contour_sequence = Sequence()
        for slice_dataset, slice_coords in zip(self.image_datasets, contours):
            if slice_coords is None:
                continue

            # create the contour image sequence
            contour_image = Dataset()
            contour_image.ReferencedSOPClassUID = str(slice_dataset.file_meta.MediaStorageSOPClassUID)
            contour_image.ReferencedSOPInstanceUID = str(slice_dataset.SOPInstanceUID)

            contour_image_sequence = Sequence()
            contour_image_sequence.append(contour_image)

            # append to the contour sequence
            contour = Dataset()
            contour.ContourImageSequence = contour_image_sequence
            contour.ContourGeometricType = 'CLOSED_PLANAR'
            contour.NumberOfContourPoints = len(slice_coords)
            contour.ContourData = [coord for point in slice_coords for coord in point]
            contour_sequence.append(contour)

        roi_contour.ContourSequence = contour_sequence

        # append to the ROIContourSequence to the RTSS
        rtss.ROIContourSequence.append(roi_contour)

    def convert(self) -> Dataset:
        """Convert the provided :class:`SimpleITK.Image` instances to a DICOM-RTSS :class:`~pydicom.dataset.Dataset`
        instance using a three-dimensional reconstruction algorithm.

        Returns:
            Dataset: The generated DICOM-RTSS :class:`~pydicom.dataset.Dataset`.
        """
        rtss = self._generate_basic_rtss()

        for idx, (label, name, color) in enumerate(zip(self.label_images, self.roi_names, self.colors)):

            # check if the image is empty
            label_image_np = sitk.GetArrayFromImage(label)
            if np.sum(label_image_np) == 0:
                warnings.warn(f'The label image {name} is empty and will be skipped from RTSS construction')
                continue

            # check if specific parameters are given for this ROI
            if self.config.get_image_params(name) is not None:
                image_params = self.config.get_image_params(name)
                image_smooth = image_params.get('image_smoothing')
                image_smooth_sigma = image_params.get('image_smoothing_sigma')
                image_smooth_radius = image_params.get('image_smoothing_radius')
                image_threshold = image_params.get('image_smoothing_threshold')
                decimate_reduction = image_params.get('decimate_reduction')
                decimate_threshold = image_params.get('decimate_threshold')
                model_smoothing_iter = image_params.get('model_smoothing_iterations')
                model_smoothing_pass_band = image_params.get('model_smoothing_pass_band')
            else:
                image_params = self.config.get_general_params()
                image_smooth = image_params.get('image_smoothing')
                image_smooth_sigma = image_params.get('image_smoothing_sigma')
                image_smooth_radius = image_params.get('image_smoothing_radius')
                image_threshold = image_params.get('image_smoothing_threshold')
                decimate_reduction = image_params.get('decimate_reduction')
                decimate_threshold = image_params.get('decimate_threshold')
                model_smoothing_iter = image_params.get('model_smoothing_iterations')
                model_smoothing_pass_band = image_params.get('model_smoothing_pass_band')


            # preprocess the image
            label = self.preprocess_image(label)

            # Get polydata
            polydata = self._get_3d_model(label,
                                          image_smooth,
                                          image_smooth_sigma,
                                          image_smooth_radius,
                                          image_threshold,
                                          decimate_reduction,
                                          decimate_threshold,
                                          model_smoothing_iter,
                                          model_smoothing_pass_band)

            # Get the contour data
            contours = self._get_2d_contours(polydata)

            # enhance the rtss with the data
            self._append_structure_set_roi_sequence_entry(name, idx + 1, rtss)
            self._append_roi_contour_sequence_entry(contours, color, idx + 1, rtss)
            self._append_rt_roi_observation(idx + 1, rtss)

        return rtss


class DicomImageSeriesConverter(Converter):
    """A :class:`Converter` class for converting DICOM image series to one or multiple
    :class:`~pyradise.data.image.IntensityImage` instances.

    Args:
        image_info (Tuple[DicomSeriesImageInfo, ...]): The :class:`~pyradise.fileio.series_info.DicomSeriesImageInfo`
         entries of the images to convert.
        registration_info (Tuple[DicomSeriesRegistrationInfo, ...]): The
         :class:`~pyradise.fileio.series_info.DicomSeriesRegistrationInfo` entries (default: tuple()).
    """

    def __init__(self,
                 image_info: Tuple[DicomSeriesImageInfo, ...],
                 registration_info: Tuple[DicomSeriesRegistrationInfo, ...] = tuple()
                 ) -> None:
        super().__init__()
        self.image_info = image_info
        self.reg_info = registration_info

    def _get_image_info_by_series_instance_uid(self,
                                               series_instance_uid: str
                                               ) -> Optional[DicomSeriesImageInfo]:
        """Get the :class:`DicomSeriesImageInfo` entries which match with the specified SeriesInstanceUID.

        Args:
            series_instance_uid (str): The SeriesInstanceUID which must be contained in the returned
             :class:`DicomSeriesImageInfo` entries.

        Returns:
            Optional[DicomSeriesImageInfo]: The :class:`DicomSeriesImageInfo` entries which contains the specified
             SeriesInstanceUID or None
        """
        if not self.image_info:
            return None

        selected = [info for info in self.image_info if info.series_instance_uid == series_instance_uid]

        if len(selected) > 1:
            raise ValueError(f'Multiple image infos detected with the same SeriesInstanceUID ({series_instance_uid})!')

        if not selected:
            return None

        return selected[0]

    def _get_registration_info(self,
                               image_info: DicomSeriesImageInfo,
                               ) -> Optional[DicomSeriesRegistrationInfo]:
        """Get the :class:`DicomSeriesRegistrationInfo` instance which belongs to the specified
        :class:`DicomSeriesImageInfo`, if available. If no :class:`DicomSeriesRegistrationInfo` is available
        :class:`None` is returned.

        Args:
            image_info (DicomSeriesImageInfo): The :class:`DicomSeriesImageInfo` for which the
             :class:`DicomSeriesRegistrationInfo` is requested.

        Returns:
            Optional[DicomSeriesRegistrationInfo]: The :class:`DicomSeriesRegistrationInfo` which belongs to the
             specified :class:`DicomSeriesImageInfo` or :class:`None`.
        """
        if not self.reg_info:
            return None

        selected = []
        for reg_info in self.reg_info:
            reg_info.update() if not reg_info.is_updated() else None

            if reg_info.referenced_series_instance_uid_transform == image_info.series_instance_uid:
                selected.append(reg_info)

        if len(selected) > 1:
            raise ValueError(f'Multiple registration infos detected with the same referenced '
                             f'SeriesInstanceUID ({image_info.series_instance_uid})!')

        if not selected:
            return None

        return selected[0]

    @staticmethod
    def _transform_image(image: sitk.Image,
                         transform: sitk.Transform,
                         is_intensity: bool
                         ) -> sitk.Image:
        """Transform an :class:`sitk.Image` according to the provided :class:`sitk.Transform`.

        Args:
            image (sitk.Image): The image to transform.
            transform (sitk.Transform): The transform to be applied to the image.
            is_intensity (bool): If True the image will be resampled using a B-Spline interpolation function,
             otherwise a nearest neighbour interpolation function will be used.

        Returns:
            sitk.Image: The transformed image.
        """
        # select the appropriate interpolation function
        if is_intensity:
            interpolator = sitk.sitkBSpline
        else:
            interpolator = sitk.sitkNearestNeighbor

        image_np = sitk.GetArrayFromImage(image)
        default_pixel_value = np.min(image_np).astype(np.float)

        # compute the new origin
        new_origin = transform.GetInverse().TransformPoint(image.GetOrigin())

        # compute the new direction
        new_direction_0 = transform.TransformVector(image.GetDirection()[:3], image.GetOrigin())
        new_direction_1 = transform.TransformVector(image.GetDirection()[3:6], image.GetOrigin())
        new_direction_2 = transform.TransformVector(image.GetDirection()[6:], image.GetOrigin())
        new_direction = new_direction_0 + new_direction_1 + new_direction_2

        new_direction_matrix = np.array(new_direction).reshape(3, 3)
        original_direction_matrix = np.array(image.GetDirection()).reshape(3, 3)
        new_direction_corr = np.dot(np.dot(new_direction_matrix, original_direction_matrix).transpose(),
                                    original_direction_matrix).transpose()

        # resample the image
        resampled_image = sitk.Resample(image,
                                        image.GetSize(),
                                        transform=transform,
                                        interpolator=interpolator,
                                        outputOrigin=new_origin,
                                        outputSpacing=image.GetSpacing(),
                                        outputDirection=tuple(new_direction_corr.flatten()),
                                        defaultPixelValue=default_pixel_value,
                                        outputPixelType=image.GetPixelIDValue())

        return resampled_image

    def convert(self) -> Tuple[IntensityImage, ...]:
        """Convert the provided :class:`~pyradise.fileio.series_info.DicomSeriesImageInfo` entries to one or multiple
        :class:`~pyradise.data.image.IntensityImage` instances.

        Returns:
            Tuple[IntensityImage, ...]: The converted :class:`~pyradise.data.image.IntensityImage` instances.
        """
        images = []

        for info in self.image_info:

            # read the image
            reader = sitk.ImageSeriesReader()
            reader.SetFileNames(info.path)
            image = reader.Execute()

            # get the registration info if available
            reg_info = self._get_registration_info(info)

            if not info.is_updated():
                info.update()

            # if no registration info is available, the image is added as is
            if reg_info is None:
                images.append(IntensityImage(image, info.modality))

            # else the image is transformed
            else:
                ref_series_instance_uid = reg_info.referenced_series_instance_uid_identity
                ref_image_info = self._get_image_info_by_series_instance_uid(ref_series_instance_uid)

                if ref_image_info is None:
                    raise ValueError(f'The reference image with SeriesInstanceUID {ref_series_instance_uid} '
                                     f'is missing for the registration!')

                image = self._transform_image(image, reg_info.transform, is_intensity=True)

                images.append(IntensityImage(image, info.modality))

        return tuple(images)


class DicomRTSSSeriesConverter(Converter):
    """A :class:`Converter` class for converting a DICOM-RTSS (i.e.
    :class:`~pyradise.fileio.series_info.DicomSeriesRTSSInfo`) to one or multiple
    :class:`~pyradise.data.image.SegmentationImage` instances.

    Notes:
        The user may provide all available :class:`~pyradise.fileio.series_info.DicomSeriesImageInfo` and
        :class:`~pyradise.fileio.series_info.DicomSeriesRegistrationInfo` entries to the corresponding ``image_infos``
        and ``registration_infos``, respectively. In this case the :class:`DicomRTSSSeriesConverter` will sort out
        unused entries.

    Args:
        rtss_infos (Union[DicomSeriesRTSSInfo, Tuple[DicomSeriesRTSSInfo, ...]]): The
         :class:`~pyradise.fileio.series_info.DicomSeriesRTSSInfo` instance holding the information to be converted.
        image_infos (Tuple[DicomSeriesImageInfo, ...]): The :class:`~pyradise.fileio.series_info.DicomSeriesImageInfo`
         entries which are referenced in the :class:`~pyradise.fileio.series_info.DicomSeriesRTSSInfo` instance.
        registration_infos (Optional[Tuple[DicomSeriesRegistrationInfo, ...]]): The
         :class:`~pyradise.fileio.series_info.DicomSeriesRegistrationInfo` entries referencing the DICOM image or
         DICOM-RTSS.
    """

    def __init__(self,
                 rtss_infos: Union[DicomSeriesRTSSInfo, Tuple[DicomSeriesRTSSInfo, ...]],
                 image_infos: Tuple[DicomSeriesImageInfo, ...],
                 registration_infos: Optional[Tuple[DicomSeriesRegistrationInfo, ...]]
                 ) -> None:
        super().__init__()

        if isinstance(rtss_infos, DicomSeriesRTSSInfo):
            self.rtss_infos = (rtss_infos,)
        else:
            self.rtss_infos = rtss_infos

        self.image_infos = image_infos
        self.reg_infos = registration_infos

    def _get_referenced_image_info(self, rtss_info: DicomSeriesRTSSInfo) -> Optional[DicomSeriesImageInfo]:
        """Get the :class:`DicomSeriesImageInfo` which is referenced by the provided :class:`DicomSeriesRTSSInfo`.

        Args:
            rtss_info (DicomSeriesRTSSInfo): The :class:`DicomSeriesRTSSInfo` containing the reference.

        Returns:
            Optional[DicomSeriesImageInfo]: The referenced :class:`DicomSeriesImageInfo` or :class:`None` if no
             reference is available.

        """
        if not self.image_infos:
            return None

        selected = [info for info in self.image_infos if info.series_instance_uid == rtss_info.referenced_instance_uid]

        if len(selected) > 1:
            raise ValueError(f'Multiple image infos detected with the same referenced '
                             f'SeriesInstanceUID ({rtss_info.referenced_instance_uid})!')

        if not selected:
            raise ValueError(f'The reference image with the SeriesInstanceUID '
                             f'{rtss_info.referenced_instance_uid} for the RTSS conversion is missing!')

        return selected[0]

    def _get_referenced_registration_info(self,
                                          rtss_info: DicomSeriesRTSSInfo,
                                          ) -> Optional[DicomSeriesRegistrationInfo]:
        """Get the :class:`DicomSeriesRegistrationInfo` which is referenced in the :class:`DicomSeriesRTSSInfo`.

        Args:
            rtss_info (DicomSeriesRTSSInfo): The :class:`DicomSeriesRTSSInfo` for which the referenced
             :class:`DicomSeriesRegistrationInfo` should be retrieved.

        Returns:
            Optional[DicomSeriesRegistrationInfo]: The :class:`DicomSeriesRegistrationInfo` instance referenced in the
             RTSS or None.
        """

        if not self.reg_infos:
            return None

        selected = []

        for registration_info in self.reg_infos:
            if registration_info.referenced_series_instance_uid_transform == rtss_info.referenced_instance_uid:
                selected.append(registration_info)

        if not selected:
            return None

        if len(selected) > 1:
            raise NotImplementedError('The number of referenced registrations is larger than one! '
                                      'The sequential application of registrations is not supported yet!')

        return selected[0]

    def convert(self) -> Tuple[SegmentationImage, ...]:
        """Convert the :class:`~pyradise.fileio.series_info.DicomSeriesRTSSInfo` instances into one or multiple
        :class:`~pyradise.data.image.SegmentationImage` instances.

        Returns:
            Tuple[SegmentationImage, ...]: The converted :class:`~pyradise.data.image.SegmentationImage` instances.
        """
        images = []

        for rtss_info in self.rtss_infos:

            ref_image_info = self._get_referenced_image_info(rtss_info)
            ref_reg_info = self._get_referenced_registration_info(rtss_info)

            if ref_reg_info:
                ref_reg_info.update() if not ref_reg_info.is_updated() else None
                reg_dataset = ref_reg_info.dataset
            else:
                reg_dataset = None

            dataset = load_dataset(rtss_info.get_path()[0])
            structures = RTSSToSegmentConverter(dataset, ref_image_info.path, reg_dataset).convert()

            for roi_name, segmentation_image in structures.items():
                images.append(SegmentationImage(segmentation_image, Organ(roi_name), rtss_info.get_annotator()))

        return tuple(images)


class SubjectToRTSSConverter(Converter):
    """A :class:`Converter` class for converting the :class:`~pyradise.data.image.SegmentationImage` instances of a
    :class:`~pyradise.data.subject.Subject` instance to a :class:`~pydicom.dataset.Dataset` instance.

    Note:
        This class is typically used at the end of a processing pipeline to output a DICOM-RTSS file containing the
        segmentation results of the pipeline.

    Note:
        This class can take a :class:`RTSSMetaData` instance as input to specify certain DICOM attributes of the
        output DICOM-RTSS. If no instance is provided, the default values will be used.

    Important:
        The ``config`` parameter defines the type of conversion algorithm. If specific conversion parameters are
        required for a certain :class:`~pyradise.data.image.SegmentationImage` instance, they must be provided in the
        appropriate :class:`RTSSConverterConfiguration` (i.e., :class:`RTSSConverter2DConfiguration` or
        :class:`RTSSConverter3DConfiguration`). Furthermore, the ``image_identifier`` parameter must be set to the
        organ name of the :class:`~pyradise.data.image.SegmentationImage` instance. Otherwise, segmentation masks with
        specific parameters can not be identified.

    Args:
        subject (Subject): The :class:`~pyradise.data.subject.Subject` instance to be converted to a DICOM-RTSS
         :class:`~pydicom.dataset.Dataset` instance.
        infos (Tuple[SeriesInfo, ...]): The :class:`~pyradise.fileio.series_info.DicomSeriesInfo` entries provided for
         the conversion (only :class:`~pyradise.fileio.series_info.DicomSeriesImageInfo` will be considered).
        reference_modality (Union[Modality, str]): The reference :class:`~pyradise.data.modality.Modality` of the
         images to be used for the conversion to DICOM-RTSS.
        config (Union[RTSSConverter2DConfiguration, RTSSConverter3DConfiguration]): The configuration for the conversion
         procedure. The type of conversion configuration determines also the conversion algorithm (2D or 3D) that is
         used.
        meta_data (RTSSMetaData): The configuration to specify certain DICOM attributes (default: RTSSMetaData()).
        config (Union[RTSS
    """

    def __init__(self,
                 subject: Subject,
                 infos: Tuple[SeriesInfo, ...],
                 reference_modality: Union[Modality, str],
                 config: Union[RTSSConverter2DConfiguration, RTSSConverter3DConfiguration],
                 meta_data: RTSSMetaData = RTSSMetaData(),
                 ) -> None:
        super().__init__()

        assert subject.segmentation_images, 'The subject must contain segmentation images!'
        self.subject = subject

        assert infos, 'There must be infos provided for the conversion!'

        reference_modality_: Modality = str_to_modality(reference_modality)
        image_infos = [entry for entry in infos if isinstance(entry, DicomSeriesImageInfo) and
                       entry.modality == reference_modality_]

        assert image_infos, 'There must be image infos in the provided infos!'

        assert len(image_infos) == 1, 'There are multiple image infos fitting the reference modality!'
        self.image_info = image_infos[0]
        self.ref_modality = reference_modality_

        # check if all segmentation images are binary
        for image in subject.segmentation_images:
            if not image.is_binary():
                raise ValueError(f'The segmentation image of organ {image.get_organ(True)} and '
                                 f'annotator {image.get_annotator(True)} is not binary or empty!')

        if not isinstance(config, (RTSSConverter2DConfiguration, RTSSConverter3DConfiguration)):
            raise ValueError(f'The config type {type(config)} is not supported!')

        self.config: Union[RTSSConverter2DConfiguration, RTSSConverter3DConfiguration] = config
        self.meta_data = meta_data

    def convert(self) -> Dataset:
        """Convert a :class:`~pyradise.data.subject.Subject` instance to a DICOM-RTSS :class:`~pydicom.dataset.Dataset`
        instance.

        Returns:
            Dataset: The DICOM-RTSS :class:`~pydicom.dataset.Dataset` instance generated from the provided
            :class:`~pyradise.data.subject.Subject` instance.
        """
        # get the image data and the label names
        sitk_images = []
        label_names = []
        for image in self.subject.segmentation_images:
            sitk_image = image.get_image_data()
            sitk_images.append(sitk_image)
            label_names.append(image.get_organ(as_str=True))

        # load the image datasets
        image_datasets = load_datasets(self.image_info.path)

        # convert the images to a rtss
        if isinstance(self.config, RTSSConverter2DConfiguration):
            rtss = SegmentToRTSSConverter2D(label_images=tuple(sitk_images),
                                            ref_image_datasets=image_datasets,
                                            roi_names=tuple(label_names),
                                            colors=None,
                                            meta_data=self.meta_data,
                                            config=self.config).convert()

        elif isinstance(self.config, RTSSConverter3DConfiguration):
            rtss = SegmentToRTSSConverter3D(label_images=tuple(sitk_images),
                                            ref_image_datasets=image_datasets,
                                            roi_names=tuple(label_names),
                                            colors=None,
                                            meta_data=self.meta_data,
                                            config=self.config).convert()

        else:
            raise ValueError(f'Invalid configuration type: {type(self.config)}!')

        return rtss


# def show_polydata(polydata: vtk_dm.vtkPolyData) -> None:
#     import vtkmodules.vtkInteractionStyle
#     import vtkmodules.vtkRenderingOpenGL2
#     from vtkmodules.vtkCommonColor import vtkNamedColors
#     from vtkmodules.vtkRenderingCore import (
#         vtkActor,
#         vtkPolyDataMapper,
#         vtkRenderWindow,
#         vtkRenderWindowInteractor,
#         vtkRenderer
#     )
#
#     # Visualize
#     colors = vtkNamedColors()
#
#     mapper = vtkPolyDataMapper()
#     mapper.SetInputData(polydata)
#     actor = vtkActor()
#     actor.SetMapper(mapper)
#     actor.GetProperty().SetLineWidth(4)
#     actor.GetProperty().SetColor(colors.GetColor3d("Peacock"))
#
#     renderer = vtkRenderer()
#     renderWindow = vtkRenderWindow()
#     renderWindow.SetWindowName("Line")
#     renderWindow.AddRenderer(renderer)
#     renderWindowInteractor = vtkRenderWindowInteractor()
#     renderWindowInteractor.SetRenderWindow(renderWindow)
#
#     renderer.SetBackground(*colors.GetColor3d("Silver"))
#     renderer.AddActor(actor)
#
#     renderWindow.Render()
#     renderWindowInteractor.Start()
