コード例 #1
0
    def test_16_segments_raises(self):
        """Test that trying to encode 16-segments raises exception."""
        arr = np.asarray([[[1, 2, 3, 4]]], dtype='uint32')
        assert (1, 1, 4) == arr.shape
        assert 4 == arr.dtype.itemsize

        msg = (
            r"Unable to encode as the DICOM standard only allows "
            r"a maximum of 15 segments in RLE encoded data"
        )
        with pytest.raises(ValueError, match=msg):
            rle_encode_frame(arr)
コード例 #2
0
    def test_encoding_multiple_frames_raises(self):
        """Test encoding multiple framed pixel data raises exception."""
        # Note: only works with multi-sample data
        ds = dcmread(EXPL_8_3_2F)
        assert ds.NumberOfFrames > 1
        kwargs = RLELosslessEncoder.kwargs_from_ds(ds)

        msg = (
            r"Unable to encode multiple frames at once, please encode one "
            r"frame at a time"
        )
        with pytest.raises(ValueError, match=msg):
            rle_encode_frame(ds.pixel_array)
コード例 #3
0
    def test_functional(self):
        """Test function works OK."""
        ds = dcmread(EXPL_16_3_1F)
        ref = ds.pixel_array
        assert ds.BitsAllocated == 16
        assert ds.SamplesPerPixel == 3
        assert ds.PixelRepresentation == 0

        encoded = rle_encode_frame(ref)
        decoded = _rle_decode_frame(
            encoded, ds.Rows, ds.Columns, ds.SamplesPerPixel, ds.BitsAllocated
        )
        ds.PlanarConfiguration = 1
        arr = np.frombuffer(decoded, '<u2')
        arr = reshape_pixel_array(ds, arr)

        assert np.array_equal(ref, arr)
コード例 #4
0
    def test_big_endian_arr(self):
        """Test using a big endian array works."""
        ds = dcmread(EXPL_16_3_1F)
        ref = ds.pixel_array
        assert ds.BitsAllocated == 16
        assert ds.SamplesPerPixel == 3
        assert ds.PixelRepresentation == 0

        arr = ref.newbyteorder('>')
        assert id(arr) != id(ref)
        assert arr.dtype == '>u2'
        encoded = rle_encode_frame(arr)
        decoded = _rle_decode_frame(
            encoded, ds.Rows, ds.Columns, ds.SamplesPerPixel, ds.BitsAllocated
        )
        ds.PlanarConfiguration = 1
        arr = np.frombuffer(decoded, '<u2')
        arr = reshape_pixel_array(ds, arr)

        assert np.array_equal(ref, arr)
コード例 #5
0
 def time_32_3(self):
     """Time encoding 32 bit 3 sample/pixel."""
     for ii in range(self.no_runs):
         rle_encode_frame(self.arr32_3)
コード例 #6
0
 def time_16_1(self):
     """Time encoding 16 bit 1 sample/pixel."""
     for ii in range(self.no_runs):
         rle_encode_frame(self.arr16_1)
コード例 #7
0
ファイル: frame.py プロジェクト: fedorov/highdicom
def encode_frame(
    array: np.ndarray,
    transfer_syntax_uid: str,
    bits_allocated: int,
    bits_stored: int,
    photometric_interpretation: Union[PhotometricInterpretationValues, str],
    pixel_representation: Union[PixelRepresentationValues, int] = 0,
    planar_configuration: Optional[Union[PlanarConfigurationValues,
                                         int]] = None
) -> bytes:
    """Encodes pixel data of an individual frame.

    Parameters
    ----------
    array: numpy.ndarray
        Pixel data in form of an array with dimensions
        (Rows x Columns x SamplesPerPixel) in case of a color image and
        (Rows x Columns) in case of a monochrome image
    transfer_syntax_uid: int
        Transfer Syntax UID
    bits_allocated: int
        Number of bits that need to be allocated per pixel sample
    bits_stored: int
        Number of bits that are required to store a pixel sample
    photometric_interpretation: int
        Photometric interpretation
    pixel_representation: int, optional
        Whether pixel samples are represented as unsigned integers or
        2's complements
    planar_configuration: int, optional
        Whether color samples are conded by pixel (`R1G1B1R2G2B2...`) or
        by plane (`R1R2...G1G2...B1B2...`).

    Returns
    -------
    bytes
        Pixel data (potentially compressed in case of encapsulated format
        encoding, depending on the transfer snytax)

    Raises
    ------
    ValueError
        When `transfer_syntax_uid` is not supported or when
        `planar_configuration` is missing in case of a color image frame.

    """
    rows = array.shape[0]
    cols = array.shape[1]
    if array.ndim > 2:
        if planar_configuration is None:
            raise ValueError(
                'Planar configuration needs to be specified for encoding of '
                'color image frames.')
        planar_configuration = PlanarConfigurationValues(
            planar_configuration).value
        samples_per_pixel = array.shape[2]
    else:
        samples_per_pixel = 1

    pixel_representation = PixelRepresentationValues(
        pixel_representation).value
    photometric_interpretation = PhotometricInterpretationValues(
        photometric_interpretation).value

    uncompressed_transfer_syntaxes = {
        ExplicitVRLittleEndian,
        ImplicitVRLittleEndian,
    }
    compressed_transfer_syntaxes = {
        JPEGBaseline,
        JPEG2000Lossless,
        RLELossless,
    }
    supported_transfer_syntaxes = uncompressed_transfer_syntaxes.union(
        compressed_transfer_syntaxes)
    if transfer_syntax_uid not in supported_transfer_syntaxes:
        raise ValueError(
            f'Transfer Syntax "{transfer_syntax_uid}" is not supported. '
            'Only the following are supported: "{}"'.format(
                '", "'.join(supported_transfer_syntaxes)))
    if transfer_syntax_uid in uncompressed_transfer_syntaxes:
        if bits_allocated == 1:
            if (rows * cols * samples_per_pixel) % 8 != 0:
                raise ValueError(
                    'Frame cannot be bit packed because its size is not a '
                    'multiple of 8.')
            return pack_bits(array.flatten())
        else:
            return array.flatten().tobytes()

    else:
        compression_lut = {
            JPEGBaseline: (
                'jpeg',
                {
                    'quality': 95
                },
            ),
            JPEG2000Lossless: (
                'jpeg2000',
                {
                    'tile_size': None,
                    'num_resolutions': 1,
                    'irreversible': False,
                },
            ),
        }

        if transfer_syntax_uid == JPEGBaseline:
            if samples_per_pixel == 1:
                if planar_configuration is not None:
                    raise ValueError(
                        'Planar configuration must be absent for encoding of '
                        'monochrome image frames with JPEG Baseline codec.')
                if photometric_interpretation not in ('MONOCHROME1',
                                                      'MONOCHROME2'):
                    raise ValueError(
                        'Photometric intpretation must be either "MONOCHROME1" '
                        'or "MONOCHROME2" for encoding of monochrome image '
                        'frames with JPEG Baseline codec.')
            elif samples_per_pixel == 3:
                if photometric_interpretation != 'YBR_FULL_422':
                    raise ValueError(
                        'Photometric intpretation must be "YBR_FULL_422" for '
                        'encoding of color image frames with '
                        'JPEG Baseline codec.')
                if planar_configuration != 0:
                    raise ValueError(
                        'Planar configuration must be 0 for encoding of '
                        'color image frames with JPEG Baseline codec.')
            else:
                raise ValueError(
                    'Samples per pixel must be 1 or 3 for '
                    'encoding of image frames with JPEG Baseline codec.')
            if bits_allocated != 8 or bits_stored != 8:
                raise ValueError(
                    'Bits allocated and bits stored must be 8 for '
                    'encoding of image frames with JPEG Baseline codec.')
            if pixel_representation != 0:
                raise ValueError(
                    'Pixel representation must be 0 for '
                    'encoding of image frames with JPEG Baseline codec.')

        if transfer_syntax_uid == JPEG2000Lossless:
            if samples_per_pixel == 1:
                if planar_configuration is not None:
                    raise ValueError(
                        'Planar configuration must be absent for encoding of '
                        'monochrome image frames with Lossless JPEG2000 codec.'
                    )
                if photometric_interpretation not in ('MONOCHROME1',
                                                      'MONOCHROME2'):
                    raise ValueError(
                        'Photometric intpretation must be either "MONOCHROME1" '
                        'or "MONOCHROME2" for encoding of monochrome image '
                        'frames with Lossless JPEG2000 codec.')
            elif samples_per_pixel == 3:
                if photometric_interpretation != 'YBR_FULL':
                    raise ValueError(
                        'Photometric interpretation must be "YBR_FULL" for '
                        'encoding of color image frames with '
                        'Lossless JPEG2000 codec.')
                if planar_configuration != 0:
                    raise ValueError(
                        'Planar configuration must be 0 for encoding of '
                        'color image frames with Lossless JPEG2000 codec.')
            else:
                raise ValueError(
                    'Samples per pixel must be 1 or 3 for '
                    'encoding of image frames with Lossless JPEG2000 codec.')
            if pixel_representation != 0:
                raise ValueError(
                    'Pixel representation must be 0 for '
                    'encoding of image frames with Lossless JPEG2000 codec.')

        if transfer_syntax_uid in compression_lut.keys():
            image_format, kwargs = compression_lut[transfer_syntax_uid]
            image = Image.fromarray(array)
            with BytesIO() as buf:
                image.save(buf, format=image_format, **kwargs)
                data = buf.getvalue()
        elif transfer_syntax_uid == RLELossless:
            data = rle_encode_frame(array)
        else:
            raise ValueError(
                f'Transfer Syntax "{transfer_syntax_uid}" is not supported.')
    return data
コード例 #8
0
    def __init__(
            self,
            pixel_array: np.ndarray,
            photometric_interpretation: Union[str,
                                              PhotometricInterpretationValues],
            bits_allocated: int,
            coordinate_system: Union[str, CoordinateSystemNames],
            study_instance_uid: str,
            series_instance_uid: str,
            series_number: int,
            sop_instance_uid: str,
            instance_number: int,
            manufacturer: str,
            patient_id: Optional[str] = None,
            patient_name: Optional[str] = None,
            patient_birth_date: Optional[str] = None,
            patient_sex: Optional[str] = None,
            accession_number: Optional[str] = None,
            study_id: str = None,
            study_date: Optional[Union[str, datetime.date]] = None,
            study_time: Optional[Union[str, datetime.time]] = None,
            referring_physician_name: Optional[str] = None,
            pixel_spacing: Optional[Tuple[int, int]] = None,
            laterality: Optional[Union[str, LateralityValues]] = None,
            patient_orientation: Optional[
                Union[Tuple[str, str], Tuple[PatientOrientationValuesBiped,
                                             PatientOrientationValuesBiped, ],
                      Tuple[PatientOrientationValuesQuadruped,
                            PatientOrientationValuesQuadruped, ]]] = None,
            anatomical_orientation_type: Optional[Union[
                str, AnatomicalOrientationTypeValues]] = None,
            container_identifier: Optional[str] = None,
            issuer_of_container_identifier: Optional[
                IssuerOfIdentifier] = None,
            specimen_descriptions: Optional[
                Sequence[SpecimenDescription]] = None,
            transfer_syntax_uid: str = ImplicitVRLittleEndian,
            **kwargs: Any):
        """

        Parameters
        ----------
        pixel_array: numpy.ndarray
            Array of unsigned integer pixel values representing a single-frame
            image; either a 2D grayscale image or a 3D color image
            (RGB color space)
        photometric_interpretation: Union[str, highdicom.enum.PhotometricInterpretationValues]
            Interpretation of pixel data; either ``"MONOCHROME1"`` or
            ``"MONOCHROME2"`` for 2D grayscale images or ``"RGB"`` or
            ``"YBR_FULL"`` for 3D color images
        bits_allocated: int
            Number of bits that should be allocated per pixel value
        coordinate_system: Union[str, highdicom.enum.CoordinateSystemNames]
            Subject (``"PATIENT"`` or ``"SLIDE"``) that was the target of
            imaging
        study_instance_uid: str
            Study Instance UID
        series_instance_uid: str
            Series Instance UID of the SC image series
        series_number: Union[int, None]
            Series Number of the SC image series
        sop_instance_uid: str
            SOP instance UID that should be assigned to the SC image instance
        instance_number: int
            Number that should be assigned to this SC image instance
        manufacturer: str
            Name of the manufacturer of the device that creates the SC image
            instance (in a research setting this is typically the same
            as `institution_name`)
        patient_id: str, optional
           ID of the patient (medical record number)
        patient_name: str, optional
           Name of the patient
        patient_birth_date: str, optional
           Patient's birth date
        patient_sex: str, optional
           Patient's sex
        study_id: str, optional
           ID of the study
        accession_number: str, optional
           Accession number of the study
        study_date: Union[str, datetime.date], optional
           Date of study creation
        study_time: Union[str, datetime.time], optional
           Time of study creation
        referring_physician_name: str, optional
            Name of the referring physician
        pixel_spacing: Tuple[int, int], optional
            Physical spacing in millimeter between pixels along the row and
            column dimension
        laterality: Union[str, highdicom.enum.LateralityValues], optional
            Laterality of the examined body part (required if
            `coordinate_system` is ``"PATIENT"``)
        patient_orientation:
                Union[Tuple[str, str], Tuple[highdicom.enum.PatientOrientationValuesBiped, highdicom.enum.PatientOrientationValuesBiped], Tuple[highdicom.enum.PatientOrientationValuesQuadruped, highdicom.enum.PatientOrientationValuesQuadruped]], optional
            Orientation of the patient along the row and column axes of the
            image (required if `coordinate_system` is ``"PATIENT"``)
        anatomical_orientation_type: Union[str, highdicom.enum.AnatomicalOrientationTypeValues], optional
            Type of anatomical orientation of patient relative to image (may be
            provide if `coordinate_system` is ``"PATIENT"`` and patient is
            an animal)
        container_identifier: str, optional
            Identifier of the container holding the specimen (required if
            `coordinate_system` is ``"SLIDE"``)
        issuer_of_container_identifier: highdicom.content.IssuerOfIdentifier, optional
            Issuer of `container_identifier`
        specimen_descriptions: Sequence[highdicom.content.SpecimenDescriptions], optional
            Description of each examined specimen (required if
            `coordinate_system` is ``"SLIDE"``)
        transfer_syntax_uid: str, optional
            UID of transfer syntax that should be used for encoding of
            data elements. The following lossless compressed transfer syntaxes
            are supported: RLE Lossless (``"1.2.840.10008.1.2.5"``).
        **kwargs: Any, optional
            Additional keyword arguments that will be passed to the constructor
            of `highdicom.base.SOPClass`

        """  # noqa
        supported_transfer_syntaxes = {
            ImplicitVRLittleEndian,
            ExplicitVRLittleEndian,
            RLELossless,
        }
        if transfer_syntax_uid not in supported_transfer_syntaxes:
            raise ValueError(
                f'Transfer syntax "{transfer_syntax_uid}" is not supported')

        super().__init__(study_instance_uid=study_instance_uid,
                         series_instance_uid=series_instance_uid,
                         series_number=series_number,
                         sop_instance_uid=sop_instance_uid,
                         sop_class_uid=SecondaryCaptureImageStorage,
                         instance_number=instance_number,
                         manufacturer=manufacturer,
                         modality='OT',
                         transfer_syntax_uid=transfer_syntax_uid,
                         patient_id=patient_id,
                         patient_name=patient_name,
                         patient_birth_date=patient_birth_date,
                         patient_sex=patient_sex,
                         accession_number=accession_number,
                         study_id=study_id,
                         study_date=study_date,
                         study_time=study_time,
                         referring_physician_name=referring_physician_name,
                         **kwargs)

        coordinate_system = CoordinateSystemNames(coordinate_system)
        if coordinate_system == CoordinateSystemNames.PATIENT:
            if laterality is None:
                raise TypeError('Laterality is required if coordinate system '
                                'is "PATIENT".')
            if patient_orientation is None:
                raise TypeError(
                    'Patient orientation is required if coordinate system '
                    'is "PATIENT".')

            # General Series
            laterality = LateralityValues(laterality)
            self.Laterality = laterality.value

            # General Image
            if anatomical_orientation_type is not None:
                anatomical_orientation_type = AnatomicalOrientationTypeValues(
                    anatomical_orientation_type)
                self.AnatomicalOrientationType = \
                    anatomical_orientation_type.value
            else:
                anatomical_orientation_type = \
                    AnatomicalOrientationTypeValues.BIPED

            row_orientation, col_orientation = patient_orientation
            if (anatomical_orientation_type ==
                    AnatomicalOrientationTypeValues.BIPED):
                patient_orientation = (
                    PatientOrientationValuesBiped(row_orientation).value,
                    PatientOrientationValuesBiped(col_orientation).value,
                )
            else:
                patient_orientation = (
                    PatientOrientationValuesQuadruped(row_orientation).value,
                    PatientOrientationValuesQuadruped(col_orientation).value,
                )
            self.PatientOrientation = list(patient_orientation)

        elif coordinate_system == CoordinateSystemNames.SLIDE:
            if container_identifier is None:
                raise TypeError(
                    'Container identifier is required if coordinate system '
                    'is "SLIDE".')
            if specimen_descriptions is None:
                raise TypeError(
                    'Specimen descriptions are required if coordinate system '
                    'is "SLIDE".')

            # Specimen
            self.ContainerIdentifier = container_identifier
            self.IssuerOfTheContainerIdentifierSequence: List[Dataset] = []
            if issuer_of_container_identifier is not None:
                self.IssuerOftheContainerIdentifierSequence.append(
                    issuer_of_container_identifier)
            container_type_item = CodedConcept(*codes.SCT.MicroscopeSlide)
            self.ContainerTypeCodeSequence = [container_type_item]
            self.SpecimenDescriptionSequence = specimen_descriptions

        # SC Equipment
        self.ConversionType = ConversionTypeValues.DI.value

        # SC Image
        now = datetime.datetime.now()
        self.DateOfSecondaryCapture = DA(now.date())
        self.TimeOfSecondaryCapture = TM(now.time())

        # Image Pixel
        self.ImageType = ['DERIVED', 'SECONDARY', 'OTHER']
        self.Rows = pixel_array.shape[0]
        self.Columns = pixel_array.shape[1]
        allowed_types = [np.bool, np.uint8, np.uint16]
        if not any(pixel_array.dtype == t for t in allowed_types):
            raise TypeError(
                'Pixel array must be of type np.bool, np.uint8 or np.uint16. '
                f'Found {pixel_array.dtype}.')
        wrong_bit_depth_assignment = (
            pixel_array.dtype == np.bool and bits_allocated != 1,
            pixel_array.dtype == np.uint8 and bits_allocated != 8,
            pixel_array.dtype == np.uint16 and bits_allocated not in (12, 16),
        )
        if any(wrong_bit_depth_assignment):
            raise ValueError('Pixel array has an unexpected bit depth.')
        if bits_allocated not in (1, 8, 12, 16):
            raise ValueError('Unexpected number of bits allocated.')
        if transfer_syntax_uid == RLELossless and bits_allocated % 8 != 0:
            raise ValueError(
                'When using run length encoding, bits allocated must be a '
                'multiple of 8')
        self.BitsAllocated = bits_allocated
        self.HighBit = self.BitsAllocated - 1
        self.BitsStored = self.BitsAllocated
        self.PixelRepresentation = 0
        photometric_interpretation = PhotometricInterpretationValues(
            photometric_interpretation)
        if pixel_array.ndim == 3:
            accepted_interpretations = {
                PhotometricInterpretationValues.RGB.value,
                PhotometricInterpretationValues.YBR_FULL.value,
                PhotometricInterpretationValues.YBR_FULL_422.value,
                PhotometricInterpretationValues.YBR_PARTIAL_420.value,
            }
            if photometric_interpretation.value not in accepted_interpretations:
                raise ValueError(
                    'Pixel array has an unexpected photometric interpretation.'
                )
            if pixel_array.shape[-1] != 3:
                raise ValueError(
                    'Pixel array has an unexpected number of color channels.')
            if bits_allocated != 8:
                raise ValueError('Color images must be 8-bit.')
            if pixel_array.dtype != np.uint8:
                raise TypeError(
                    'Pixel array must have 8-bit unsigned integer data type '
                    'in case of a color image.')
            self.PhotometricInterpretation = photometric_interpretation.value
            self.SamplesPerPixel = 3
            self.PlanarConfiguration = 0
        elif pixel_array.ndim == 2:
            accepted_interpretations = {
                PhotometricInterpretationValues.MONOCHROME1.value,
                PhotometricInterpretationValues.MONOCHROME2.value,
            }
            if photometric_interpretation.value not in accepted_interpretations:
                raise ValueError(
                    'Pixel array has an unexpected photometric interpretation.'
                )
            self.PhotometricInterpretation = photometric_interpretation.value
            self.SamplesPerPixel = 1
        else:
            raise ValueError(
                'Pixel array has an unexpected number of dimensions.')
        if pixel_spacing is not None:
            self.PixelSpacing = pixel_spacing

        # Pixel compression based on transfer syntax uid
        if self.file_meta.TransferSyntaxUID == RLELossless:
            self.PixelData = encapsulate([rle_encode_frame(pixel_array)])
        else:
            self.PixelData = pixel_array.tobytes()