Пример #1
0
 def test_read(self):
     """Test the function"""
     with DicomFile(TEST_FILE, 'rb') as fp:
         assert not fp.parent.closed
         # Weird issue with Python 3.6 sometimes returning
         #   lowercase file path on Windows
         assert "ct_small.dcm" in fp.name.lower()
         assert fp.read(2) == b'\x49\x49'
Пример #2
0
def read_file_meta_info(filename):
    """Read and return the DICOM file meta information only.

    This function is meant to be used in user code, for quickly going through
    a series of files to find one which is referenced to a particular SOP,
    without having to read the entire files.
    """
    with DicomFile(filename, 'rb') as fp:
        read_preamble(fp, False)  # if no header, raise exception
        return _read_file_meta_info(fp)
Пример #3
0
def _get_bot(fp: DicomFile, number_of_frames: int) -> List[int]:
    """Tries to read the value of the Basic Offset Table (BOT) item and builds
    it in case it is empty.

    Parameters
    ----------
    fp: pydicom.filebase.DicomFile
        Pointer for DICOM PS3.10 file stream positioned at the first byte of
        the Pixel Data element
    number_of_frames: int
        Number of frames contained in the Pixel Data element

    Returns
    -------
    List[int]
        Offset of each Frame item in bytes from the first byte of the Pixel Data
        element following the BOT item

    Note
    ----
    Moves the pointer to the first byte of the open file following the BOT item
    (the first byte of the first Frame item).

    """
    logger.debug('read Basic Offset Table')
    basic_offset_table = _read_bot(fp)

    first_frame_offset = fp.tell()
    tag = TupleTag(fp.read_tag())
    if int(tag) != ItemTag:
        raise ValueError('Reading of Basic Offset Table failed')
    fp.seek(first_frame_offset, 0)

    # Basic Offset Table item must be present, but it may be empty
    if len(basic_offset_table) == 0:
        logger.debug('Basic Offset Table item is empty')
    if len(basic_offset_table) != number_of_frames:
        logger.debug('build Basic Offset Table item')
        basic_offset_table = _build_bot(fp, number_of_frames=number_of_frames)

    return basic_offset_table
Пример #4
0
def _read_bot(fp: DicomFile) -> List[int]:
    """Reads the Basic Offset Table (BOT) item of an encapsulated Pixel Data
    element.

    Parameters
    ----------
    fp: pydicom.filebase.DicomFile
        Pointer for DICOM PS3.10 file stream positioned at the first byte of
        the Pixel Data element

    Returns
    -------
    List[int]
        Offset of each Frame item in bytes from the first byte of the Pixel Data
        element following the BOT item

    Note
    ----
    Moves the pointer to the first byte of the open file following the BOT item
    (the first byte of the first Frame item).

    Raises
    ------
    IOError
        When file pointer is not positioned at first byte of Pixel Data element

    """
    tag = TupleTag(fp.read_tag())
    if int(tag) not in _PIXEL_DATA_TAGS:
        raise IOError(
            'Expected file pointer at first byte of Pixel Data element.')
    # Skip Pixel Data element header (tag, VR, length)
    pixel_data_element_value_offset = data_element_offset_to_value(
        fp.is_implicit_VR, 'OB')
    fp.seek(pixel_data_element_value_offset - 4, 1)
    is_empty, offsets = get_frame_offsets(fp)
    return offsets
Пример #5
0
def write_file(filename, dataset, write_like_original=True):
    """Store a FileDataset to the filename specified.

    Parameters
    ----------
    filename : str
        Name of file to save new DICOM file to.
    dataset : FileDataset
        Dataset holding the DICOM information; e.g. an object
        read with read_file().
    write_like_original : boolean
        If True (default), preserves the following information from
        the dataset:
        -preamble -- if no preamble in read file, than not used here
        -hasFileMeta -- if writer did not do file meta information,
            then don't write here either
        -seq.is_undefined_length -- if original had delimiters, write them now too,
            instead of the more sensible length characters
        - is_undefined_length_sequence_item -- for datasets that belong to a
            sequence, write the undefined length delimiters if that is
            what the original had.
        If False, produces a "nicer" DICOM file for other readers,
            where all lengths are explicit.

    See Also
    --------
    pydicom.dataset.FileDataset
        Dataset class with relevant attrs and information.
    pydicom.dataset.Dataset.save_as
        Write a DICOM file from a dataset that was read in with read_file().
        save_as wraps write_file.

    Notes
    -----
    Set dataset.preamble if you want something other than 128 0-bytes.
    If the dataset was read from an existing dicom file, then its preamble
    was stored at read time. It is up to the user to ensure the preamble is still
    correct for its purposes.

    If there is no Transfer Syntax tag in the dataset, then set
    dataset.is_implicit_VR and dataset.is_little_endian
    to determine the transfer syntax used to write the file.
    """

    # Decide whether to write DICOM preamble. Should always do so unless trying to mimic the original file read in
    preamble = getattr(dataset, "preamble", None)
    if not preamble and not write_like_original:
        preamble = b"\0" * 128
    file_meta = dataset.file_meta
    if file_meta is None:
        file_meta = Dataset()
    if 'TransferSyntaxUID' not in file_meta:
        if dataset.is_little_endian and dataset.is_implicit_VR:
            file_meta.add_new((2, 0x10), 'UI', ImplicitVRLittleEndian)
        elif dataset.is_little_endian and not dataset.is_implicit_VR:
            file_meta.add_new((2, 0x10), 'UI', ExplicitVRLittleEndian)
        elif not dataset.is_little_endian and not dataset.is_implicit_VR:
            file_meta.add_new((2, 0x10), 'UI', ExplicitVRBigEndian)
        else:
            raise NotImplementedError(
                "pydicom has not been verified for Big Endian with Implicit VR"
            )

    caller_owns_file = True
    # Open file if not already a file object
    if isinstance(filename, compat.string_types):
        fp = DicomFile(filename, 'wb')
        # caller provided a file name; we own the file handle
        caller_owns_file = False
    else:
        fp = DicomFileLike(filename)

    try:
        if preamble:
            fp.write(preamble)  # blank 128 byte preamble
            _write_file_meta_info(fp, file_meta)

        # Set file VR, endian. MUST BE AFTER writing META INFO (which changes to Explicit LittleEndian)
        fp.is_implicit_VR = dataset.is_implicit_VR
        fp.is_little_endian = dataset.is_little_endian

        write_dataset(fp, dataset)
    finally:
        if not caller_owns_file:
            fp.close()
Пример #6
0
def write_file(filename, dataset, write_like_original=True):
    """Store a FileDataset to the filename specified.

    Parameters
    ----------
    filename : str
        Name of file to save new DICOM file to.
    dataset : FileDataset
        Dataset holding the DICOM information; e.g. an object
        read with read_file().
    write_like_original : boolean
        If True (default), preserves the following information from
        the dataset:
        -preamble -- if no preamble in read file, than not used here
        -hasFileMeta -- if writer did not do file meta information,
            then don't write here either
        -seq.is_undefined_length -- if original had delimiters, write them now too,
            instead of the more sensible length characters
        - is_undefined_length_sequence_item -- for datasets that belong to a
            sequence, write the undefined length delimiters if that is
            what the original had.
        If False, produces a "nicer" DICOM file for other readers,
            where all lengths are explicit.

    See Also
    --------
    pydicom.dataset.FileDataset
        Dataset class with relevant attrs and information.
    pydicom.dataset.Dataset.save_as
        Write a DICOM file from a dataset that was read in with read_file().
        save_as wraps write_file.

    Notes
    -----
    Set dataset.preamble if you want something other than 128 0-bytes.
    If the dataset was read from an existing dicom file, then its preamble
    was stored at read time. It is up to the user to ensure the preamble is still
    correct for its purposes.

    If there is no Transfer Syntax tag in the dataset, then set
    dataset.is_implicit_VR and dataset.is_little_endian
    to determine the transfer syntax used to write the file.
    """

    # Decide whether to write DICOM preamble. Should always do so unless trying to mimic the original file read in
    preamble = getattr(dataset, "preamble", None)
    if not preamble and not write_like_original:
        preamble = b"\0" * 128
    file_meta = dataset.file_meta
    if file_meta is None:
        file_meta = Dataset()
    if 'TransferSyntaxUID' not in file_meta:
        if dataset.is_little_endian and dataset.is_implicit_VR:
            file_meta.add_new((2, 0x10), 'UI', ImplicitVRLittleEndian)
        elif dataset.is_little_endian and not dataset.is_implicit_VR:
            file_meta.add_new((2, 0x10), 'UI', ExplicitVRLittleEndian)
        elif not dataset.is_little_endian and not dataset.is_implicit_VR:
            file_meta.add_new((2, 0x10), 'UI', ExplicitVRBigEndian)
        else:
            raise NotImplementedError("pydicom has not been verified for Big Endian with Implicit VR")

    caller_owns_file = True
    # Open file if not already a file object
    if isinstance(filename, compat.string_types):
        fp = DicomFile(filename, 'wb')
        # caller provided a file name; we own the file handle
        caller_owns_file = False
    else:
        fp = DicomFileLike(filename)

    try:
        if preamble:
            fp.write(preamble)  # blank 128 byte preamble
            _write_file_meta_info(fp, file_meta)

        # Set file VR, endian. MUST BE AFTER writing META INFO (which changes to Explicit LittleEndian)
        fp.is_implicit_VR = dataset.is_implicit_VR
        fp.is_little_endian = dataset.is_little_endian

        write_dataset(fp, dataset)
    finally:
        if not caller_owns_file:
            fp.close()
Пример #7
0
def dcmwrite(filename, dataset, write_like_original=True):
    """Write `dataset` to the `filename` specified.

    If `write_like_original` is ``True`` then `dataset` will be written as is
    (after minimal validation checking) and may or may not contain all or parts
    of the File Meta Information (and hence may or may not be conformant with
    the DICOM File Format).

    If `write_like_original` is ``False``, `dataset` will be stored in the
    :dcm:`DICOM File Format <part10/chapter_7.html>`.  To do
    so requires that the ``Dataset.file_meta`` attribute
    exists and contains a :class:`Dataset` with the required (Type 1) *File
    Meta Information Group* elements. The byte stream of the `dataset` will be
    placed into the file after the DICOM *File Meta Information*.

    If `write_like_original` is ``True`` then the :class:`Dataset` will be
    written as is (after minimal validation checking) and may or may not
    contain all or parts of the *File Meta Information* (and hence may or
    may not be conformant with the DICOM File Format).

    **File Meta Information**

    The *File Meta Information* consists of a 128-byte preamble, followed by
    a 4 byte ``b'DICM'`` prefix, followed by the *File Meta Information Group*
    elements.

    **Preamble and Prefix**

    The ``dataset.preamble`` attribute shall be 128-bytes long or ``None`` and
    is available for use as defined by the Application Profile or specific
    implementations. If the preamble is not used by an Application Profile or
    specific implementation then all 128 bytes should be set to ``0x00``. The
    actual preamble written depends on `write_like_original` and
    ``dataset.preamble`` (see the table below).

    +------------------+------------------------------+
    |                  | write_like_original          |
    +------------------+-------------+----------------+
    | dataset.preamble | True        | False          |
    +==================+=============+================+
    | None             | no preamble | 128 0x00 bytes |
    +------------------+-------------+----------------+
    | 128 bytes        | dataset.preamble             |
    +------------------+------------------------------+

    The prefix shall be the bytestring ``b'DICM'`` and will be written if and
    only if the preamble is present.

    **File Meta Information Group Elements**

    The preamble and prefix are followed by a set of DICOM elements from the
    (0002,eeee) group. Some of these elements are required (Type 1) while
    others are optional (Type 3/1C). If `write_like_original` is ``True``
    then the *File Meta Information Group* elements are all optional. See
    :func:`~pydicom.filewriter.write_file_meta_info` for more information on
    which elements are required.

    The *File Meta Information Group* elements should be included within their
    own :class:`~pydicom.dataset.Dataset` in the ``dataset.file_meta``
    attribute.

    If (0002,0010) *Transfer Syntax UID* is included then the user must ensure
    its value is compatible with the values for the
    ``dataset.is_little_endian`` and ``dataset.is_implicit_VR`` attributes.
    For example, if ``is_little_endian`` and ``is_implicit_VR`` are both
    ``True`` then the Transfer Syntax UID must be 1.2.840.10008.1.2 *Implicit
    VR Little Endian*. See the DICOM Standard, Part 5,
    :dcm:`Section 10<part05/chapter_10.html>` for more information on Transfer
    Syntaxes.

    *Encoding*

    The preamble and prefix are encoding independent. The File Meta elements
    are encoded as *Explicit VR Little Endian* as required by the DICOM
    Standard.

    **Dataset**

    A DICOM Dataset representing a SOP Instance related to a DICOM Information
    Object Definition. It is up to the user to ensure the `dataset` conforms
    to the DICOM Standard.

    *Encoding*

    The `dataset` is encoded as specified by the ``dataset.is_little_endian``
    and ``dataset.is_implicit_VR`` attributes. It's up to the user to ensure
    these attributes are set correctly (as well as setting an appropriate
    value for ``dataset.file_meta.TransferSyntaxUID`` if present).

    Parameters
    ----------
    filename : str or file-like
        Name of file or the file-like to write the new DICOM file to.
    dataset : pydicom.dataset.FileDataset
        Dataset holding the DICOM information; e.g. an object read with
        :func:`~pydicom.filereader.dcmread`.
    write_like_original : bool, optional
        If ``True`` (default), preserves the following information from
        the Dataset (and may result in a non-conformant file):

        - preamble -- if the original file has no preamble then none will be
          written.
        - file_meta -- if the original file was missing any required *File
          Meta Information Group* elements then they will not be added or
          written.
          If (0002,0000) *File Meta Information Group Length* is present then
          it may have its value updated.
        - seq.is_undefined_length -- if original had delimiters, write them now
          too, instead of the more sensible length characters
        - is_undefined_length_sequence_item -- for datasets that belong to a
          sequence, write the undefined length delimiters if that is
          what the original had.

        If ``False``, produces a file conformant with the DICOM File Format,
        with explicit lengths for all elements.

    Raises
    ------
    AttributeError
        If either ``dataset.is_implicit_VR`` or ``dataset.is_little_endian``
        have not been set.
    ValueError
        If group 2 elements are in ``dataset`` rather than
        ``dataset.file_meta``, or if a preamble is given but is not 128 bytes
        long, or if Transfer Syntax is a compressed type and pixel data is not
        compressed.

    See Also
    --------
    pydicom.dataset.FileDataset
        Dataset class with relevant attributes and information.
    pydicom.dataset.Dataset.save_as
        Write a DICOM file from a dataset that was read in with ``dcmread()``.
        ``save_as()`` wraps ``dcmwrite()``.
    """

    # Ensure is_little_endian and is_implicit_VR are set
    if None in (dataset.is_little_endian, dataset.is_implicit_VR):
        has_tsyntax = False
        try:
            tsyntax = dataset.file_meta.TransferSyntaxUID
            if not tsyntax.is_private:
                dataset.is_little_endian = tsyntax.is_little_endian
                dataset.is_implicit_VR = tsyntax.is_implicit_VR
                has_tsyntax = True
        except AttributeError:
            pass

        if not has_tsyntax:
            raise AttributeError(
                "'{0}.is_little_endian' and '{0}.is_implicit_VR' must be "
                "set appropriately before saving.".format(
                    dataset.__class__.__name__))

    # Try and ensure that `is_undefined_length` is set correctly
    try:
        tsyntax = dataset.file_meta.TransferSyntaxUID
        if not tsyntax.is_private:
            dataset['PixelData'].is_undefined_length = tsyntax.is_compressed
    except (AttributeError, KeyError):
        pass

    # Check that dataset's group 0x0002 elements are only present in the
    #   `dataset.file_meta` Dataset - user may have added them to the wrong
    #   place
    if dataset.group_dataset(0x0002) != Dataset():
        raise ValueError("File Meta Information Group Elements (0002,eeee) "
                         "should be in their own Dataset object in the "
                         "'{0}.file_meta' "
                         "attribute.".format(dataset.__class__.__name__))

    # A preamble is required under the DICOM standard, however if
    #   `write_like_original` is True we treat it as optional
    preamble = getattr(dataset, 'preamble', None)
    if preamble and len(preamble) != 128:
        raise ValueError("'{0}.preamble' must be 128-bytes "
                         "long.".format(dataset.__class__.__name__))
    if not preamble and not write_like_original:
        # The default preamble is 128 0x00 bytes.
        preamble = b'\x00' * 128

    # File Meta Information is required under the DICOM standard, however if
    #   `write_like_original` is True we treat it as optional
    if not write_like_original:
        # the checks will be done in write_file_meta_info()
        dataset.fix_meta_info(enforce_standard=False)
    else:
        dataset.ensure_file_meta()

    # Check for decompression, give warnings if inconsistencies
    # If decompressed, then pixel_array is now used instead of PixelData
    if dataset.is_decompressed:
        xfer = dataset.file_meta.TransferSyntaxUID
        if xfer not in UncompressedPixelTransferSyntaxes:
            raise ValueError("file_meta transfer SyntaxUID is compressed type "
                             "but pixel data has been decompressed")

        # Force PixelData to the decompressed version
        dataset.PixelData = dataset.pixel_array.tobytes()

    caller_owns_file = True
    # Open file if not already a file object
    if isinstance(filename, compat.string_types):
        fp = DicomFile(filename, 'wb')
        # caller provided a file name; we own the file handle
        caller_owns_file = False
    else:
        fp = DicomFileLike(filename)

    # if we want to write with the same endianess and VR handling as
    # the read dataset we want to preserve raw data elements for
    # performance reasons (which is done by get_item);
    # otherwise we use the default converting item getter
    if dataset.is_original_encoding:
        get_item = Dataset.get_item
    else:
        get_item = Dataset.__getitem__

    try:
        # WRITE FILE META INFORMATION
        if preamble:
            # Write the 'DICM' prefix if and only if we write the preamble
            fp.write(preamble)
            fp.write(b'DICM')

        if dataset.file_meta:  # May be an empty Dataset
            # If we want to `write_like_original`, don't enforce_standard
            write_file_meta_info(fp,
                                 dataset.file_meta,
                                 enforce_standard=not write_like_original)

        # WRITE DATASET
        # The transfer syntax used to encode the dataset can't be changed
        #   within the dataset.
        # Write any Command Set elements now as elements must be in tag order
        #   Mixing Command Set with other elements is non-conformant so we
        #   require `write_like_original` to be True
        command_set = get_item(dataset, slice(0x00000000, 0x00010000))
        if command_set and write_like_original:
            fp.is_implicit_VR = True
            fp.is_little_endian = True
            write_dataset(fp, command_set)

        # Set file VR and endianness. MUST BE AFTER writing META INFO (which
        #   requires Explicit VR Little Endian) and COMMAND SET (which requires
        #   Implicit VR Little Endian)
        fp.is_implicit_VR = dataset.is_implicit_VR
        fp.is_little_endian = dataset.is_little_endian

        # Write non-Command Set elements now
        write_dataset(fp, get_item(dataset, slice(0x00010000, None)))
    finally:
        if not caller_owns_file:
            fp.close()
Пример #8
0
def dcmwrite(filename: Union[PathType, BinaryIO],
             dataset: Dataset,
             write_like_original: bool = True) -> None:
    """Write `dataset` to the `filename` specified.

    If `write_like_original` is ``True`` then the :class:`Dataset` will be
    written as is (after minimal validation checking) and may or may not
    contain all or parts of the *File Meta Information* (and hence may or
    may not be conformant with the DICOM File Format).

    If `write_like_original` is ``False``, `dataset` will be stored in the
    :dcm:`DICOM File Format <part10/chapter_7.html>`.  To do
    so requires that the ``Dataset.file_meta`` attribute
    exists and contains a :class:`Dataset` with the required (Type 1) *File
    Meta Information Group* elements. The byte stream of the `dataset` will be
    placed into the file after the DICOM *File Meta Information*.

    **File Meta Information**

    The *File Meta Information* consists of a 128-byte preamble, followed by
    a 4 byte ``b'DICM'`` prefix, followed by the *File Meta Information Group*
    elements.

    **Preamble and Prefix**

    The ``dataset.preamble`` attribute shall be 128-bytes long or ``None`` and
    is available for use as defined by the Application Profile or specific
    implementations. If the preamble is not used by an Application Profile or
    specific implementation then all 128 bytes should be set to ``0x00``. The
    actual preamble written depends on `write_like_original` and
    ``dataset.preamble`` (see the table below).

    +------------------+------------------------------+
    |                  | write_like_original          |
    +------------------+-------------+----------------+
    | dataset.preamble | True        | False          |
    +==================+=============+================+
    | None             | no preamble | 128 0x00 bytes |
    +------------------+-------------+----------------+
    | 128 bytes        | dataset.preamble             |
    +------------------+------------------------------+

    The prefix shall be the bytestring ``b'DICM'`` and will be written if and
    only if the preamble is present.

    **File Meta Information Group Elements**

    The preamble and prefix are followed by a set of DICOM elements from the
    (0002,eeee) group. Some of these elements are required (Type 1) while
    others are optional (Type 3/1C). If `write_like_original` is ``True``
    then the *File Meta Information Group* elements are all optional. See
    :func:`~pydicom.filewriter.write_file_meta_info` for more information on
    which elements are required.

    The *File Meta Information Group* elements should be included within their
    own :class:`~pydicom.dataset.Dataset` in the ``dataset.file_meta``
    attribute.

    If (0002,0010) *Transfer Syntax UID* is included then the user must ensure
    its value is compatible with the values for the
    ``dataset.is_little_endian`` and ``dataset.is_implicit_VR`` attributes.
    For example, if ``is_little_endian`` and ``is_implicit_VR`` are both
    ``True`` then the Transfer Syntax UID must be 1.2.840.10008.1.2 *Implicit
    VR Little Endian*. See the DICOM Standard, Part 5,
    :dcm:`Section 10<part05/chapter_10.html>` for more information on Transfer
    Syntaxes.

    *Encoding*

    The preamble and prefix are encoding independent. The File Meta elements
    are encoded as *Explicit VR Little Endian* as required by the DICOM
    Standard.

    **Dataset**

    A DICOM Dataset representing a SOP Instance related to a DICOM Information
    Object Definition. It is up to the user to ensure the `dataset` conforms
    to the DICOM Standard.

    *Encoding*

    The `dataset` is encoded as specified by the ``dataset.is_little_endian``
    and ``dataset.is_implicit_VR`` attributes. It's up to the user to ensure
    these attributes are set correctly (as well as setting an appropriate
    value for ``dataset.file_meta.TransferSyntaxUID`` if present).

    Parameters
    ----------
    filename : str or PathLike or file-like
        Name of file or the file-like to write the new DICOM file to.
    dataset : pydicom.dataset.FileDataset
        Dataset holding the DICOM information; e.g. an object read with
        :func:`~pydicom.filereader.dcmread`.
    write_like_original : bool, optional
        If ``True`` (default), preserves the following information from
        the Dataset (and may result in a non-conformant file):

        - preamble -- if the original file has no preamble then none will be
          written.
        - file_meta -- if the original file was missing any required *File
          Meta Information Group* elements then they will not be added or
          written.
          If (0002,0000) *File Meta Information Group Length* is present then
          it may have its value updated.
        - seq.is_undefined_length -- if original had delimiters, write them now
          too, instead of the more sensible length characters
        - is_undefined_length_sequence_item -- for datasets that belong to a
          sequence, write the undefined length delimiters if that is
          what the original had.

        If ``False``, produces a file conformant with the DICOM File Format,
        with explicit lengths for all elements.

    Raises
    ------
    AttributeError
        If either ``dataset.is_implicit_VR`` or ``dataset.is_little_endian``
        have not been set.
    ValueError
        If group 2 elements are in ``dataset`` rather than
        ``dataset.file_meta``, or if a preamble is given but is not 128 bytes
        long, or if Transfer Syntax is a compressed type and pixel data is not
        compressed.

    See Also
    --------
    pydicom.dataset.Dataset
        Dataset class with relevant attributes and information.
    pydicom.dataset.Dataset.save_as
        Write a DICOM file from a dataset that was read in with ``dcmread()``.
        ``save_as()`` wraps ``dcmwrite()``.
    """

    # Ensure is_little_endian and is_implicit_VR are set
    if None in (dataset.is_little_endian, dataset.is_implicit_VR):
        has_tsyntax = False
        try:
            tsyntax = dataset.file_meta.TransferSyntaxUID
            if not tsyntax.is_private:
                dataset.is_little_endian = tsyntax.is_little_endian
                dataset.is_implicit_VR = tsyntax.is_implicit_VR
                has_tsyntax = True
        except AttributeError:
            pass

        if not has_tsyntax:
            name = dataset.__class__.__name__
            raise AttributeError(
                f"'{name}.is_little_endian' and '{name}.is_implicit_VR' must "
                f"be set appropriately before saving")

    # Try and ensure that `is_undefined_length` is set correctly
    try:
        tsyntax = dataset.file_meta.TransferSyntaxUID
        if not tsyntax.is_private:
            dataset['PixelData'].is_undefined_length = tsyntax.is_compressed
    except (AttributeError, KeyError):
        pass

    # Check that dataset's group 0x0002 elements are only present in the
    #   `dataset.file_meta` Dataset - user may have added them to the wrong
    #   place
    if dataset.group_dataset(0x0002) != Dataset():
        raise ValueError(
            f"File Meta Information Group Elements (0002,eeee) should be in "
            f"their own Dataset object in the "
            f"'{dataset.__class__.__name__}.file_meta' attribute.")

    # A preamble is required under the DICOM standard, however if
    #   `write_like_original` is True we treat it as optional
    preamble = getattr(dataset, 'preamble', None)
    if preamble and len(preamble) != 128:
        raise ValueError(
            f"'{dataset.__class__.__name__}.preamble' must be 128-bytes long.")
    if not preamble and not write_like_original:
        # The default preamble is 128 0x00 bytes.
        preamble = b'\x00' * 128

    # File Meta Information is required under the DICOM standard, however if
    #   `write_like_original` is True we treat it as optional
    if not write_like_original:
        # the checks will be done in write_file_meta_info()
        dataset.fix_meta_info(enforce_standard=False)
    else:
        dataset.ensure_file_meta()

    # Check for decompression, give warnings if inconsistencies
    # If decompressed, then pixel_array is now used instead of PixelData
    if dataset.is_decompressed:
        if dataset.file_meta.TransferSyntaxUID.is_compressed:
            raise ValueError(
                f"The Transfer Syntax UID element in "
                f"'{dataset.__class__.__name__}.file_meta' is compressed "
                f"but the pixel data has been decompressed")

        # Force PixelData to the decompressed version
        dataset.PixelData = dataset.pixel_array.tobytes()

    caller_owns_file = True
    # Open file if not already a file object
    filename = path_from_pathlike(filename)
    if isinstance(filename, str):
        fp = DicomFile(filename, 'wb')
        # caller provided a file name; we own the file handle
        caller_owns_file = False
    else:
        try:
            fp = DicomFileLike(filename)
        except AttributeError:
            raise TypeError("dcmwrite: Expected a file path or a file-like, "
                            "but got " + type(filename).__name__)
    try:
        # WRITE FILE META INFORMATION
        if preamble:
            # Write the 'DICM' prefix if and only if we write the preamble
            fp.write(preamble)
            fp.write(b'DICM')

        tsyntax: Optional[UID] = None  # type: ignore[no-redef]
        if dataset.file_meta:  # May be an empty Dataset
            # If we want to `write_like_original`, don't enforce_standard
            write_file_meta_info(fp,
                                 dataset.file_meta,
                                 enforce_standard=not write_like_original)
            tsyntax = getattr(dataset.file_meta, "TransferSyntaxUID", None)

        if (tsyntax == DeflatedExplicitVRLittleEndian):
            # See PS3.5 section A.5
            # when writing, the entire dataset following
            #     the file metadata is prepared the normal way,
            #     then "deflate" compression applied.
            buffer = DicomBytesIO()
            _write_dataset(buffer, dataset, write_like_original)

            # Compress the encoded data and write to file
            compressor = zlib.compressobj(wbits=-zlib.MAX_WBITS)
            deflated = compressor.compress(
                buffer.parent.getvalue()  # type: ignore[union-attr]
            )
            deflated += compressor.flush()
            if len(deflated) % 2:
                deflated += b'\x00'

            fp.write(deflated)
        else:
            _write_dataset(fp, dataset, write_like_original)

    finally:
        if not caller_owns_file:
            fp.close()
Пример #9
0
def dcmwrite(filename, dataset, write_like_original=True):
    """Write `dataset` to the `filename` specified.

    If `write_like_original` is True then `dataset` will be written as is
    (after minimal validation checking) and may or may not contain all or parts
    of the File Meta Information (and hence may or may not be conformant with
    the DICOM File Format).
    If `write_like_original` is False, `dataset` will be stored in the DICOM
    File Format in accordance with DICOM Standard Part 10 Section 7. The byte
    stream of the `dataset` will be placed into the file after the DICOM File
    Meta Information.

    File Meta Information
    ---------------------
    The File Meta Information consists of a 128-byte preamble, followed by a 4
    byte DICOM prefix, followed by the File Meta Information Group elements.

    Preamble and Prefix
    ~~~~~~~~~~~~~~~~~~~
    The `dataset.preamble` attribute shall be 128-bytes long or None and is
    available for use as defined by the Application Profile or specific
    implementations. If the preamble is not used by an Application Profile or
    specific implementation then all 128 bytes should be set to 0x00. The
    actual preamble written depends on `write_like_original` and
    `dataset.preamble` (see the table below).

    +------------------+------------------------------+
    |                  | write_like_original          |
    +------------------+-------------+----------------+
    | dataset.preamble | True        | False          |
    +==================+=============+================+
    | None             | no preamble | 128 0x00 bytes |
    +------------------+------------------------------+
    | 128 bytes        | dataset.preamble             |
    +------------------+------------------------------+

    The prefix shall be the string 'DICM' and will be written if and only if
    the preamble is present.

    File Meta Information Group Elements
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    The preamble and prefix are followed by a set of DICOM Elements from the
    (0002,eeee) group. Some of these elements are required (Type 1) while
    others are optional (Type 3/1C). If `write_like_original` is True then the
    File Meta Information Group elements are all optional. See
    pydicom.filewriter.write_file_meta_info for more information on which
    elements are required.

    The File Meta Information Group elements should be included within their
    own Dataset in the `dataset.file_meta` attribute.

    If (0002,0010) 'Transfer Syntax UID' is included then the user must ensure
    it's value is compatible with the values for the `dataset.is_little_endian`
    and `dataset.is_implicit_VR` attributes. For example, if is_little_endian
    and is_implicit_VR are both True then the Transfer Syntax UID must be
    1.2.840.10008.1.2 'Implicit VR Little Endian'. See the DICOM standard
    Part 5 Section 10 for more information on Transfer Syntaxes.

    Encoding
    ~~~~~~~~
    The preamble and prefix are encoding independent. The File Meta Elements
    are encoded as Explicit VR Little Endian as required by the DICOM standard.

    Dataset
    -------
    A DICOM Dataset representing a SOP Instance related to a DICOM Information
    Object Definition. It is up to the user to ensure the `dataset` conforms
    to the DICOM standard.

    Encoding
    ~~~~~~~~
    The `dataset` is encoded as specified by the `dataset.is_little_endian`
    and `dataset.is_implicit_VR` attributes. It's up to the user to ensure
    these attributes are set correctly (as well as setting an appropriate value
    for `dataset.file_meta.TransferSyntaxUID` if present).

    Parameters
    ----------
    filename : str or file-like
        Name of file or the file-like to write the new DICOM file to.
    dataset : pydicom.dataset.FileDataset
        Dataset holding the DICOM information; e.g. an object read with
        pydicom.dcmread().
    write_like_original : bool
        If True (default), preserves the following information from
        the Dataset (and may result in a non-conformant file):
        - preamble -- if the original file has no preamble then none will be
            written.
        - file_meta -- if the original file was missing any required File Meta
            Information Group elements then they will not be added or written.
            If (0002,0000) 'File Meta Information Group Length' is present then
            it may have its value updated.
        - seq.is_undefined_length -- if original had delimiters, write them now
            too, instead of the more sensible length characters
        - is_undefined_length_sequence_item -- for datasets that belong to a
            sequence, write the undefined length delimiters if that is
            what the original had.
        If False, produces a file conformant with the DICOM File Format, with
        explicit lengths for all elements.

    See Also
    --------
    pydicom.dataset.FileDataset
        Dataset class with relevant attributes and information.
    pydicom.dataset.Dataset.save_as
        Write a DICOM file from a dataset that was read in with dcmread().
        save_as wraps dcmwrite.
    """
    # Check that dataset's group 0x0002 elements are only present in the
    #   `dataset.file_meta` Dataset - user may have added them to the wrong
    #   place
    if dataset.group_dataset(0x0002) != Dataset():
        raise ValueError("File Meta Information Group Elements (0002,eeee) "
                         "should be in their own Dataset object in the "
                         "'{0}.file_meta' "
                         "attribute.".format(dataset.__class__.__name__))

    # A preamble is required under the DICOM standard, however if
    #   `write_like_original` is True we treat it as optional
    preamble = getattr(dataset, 'preamble', None)
    if preamble and len(preamble) != 128:
        raise ValueError("'{0}.preamble' must be 128-bytes "
                         "long.".format(dataset.__class__.__name__))
    if not preamble and not write_like_original:
        # The default preamble is 128 0x00 bytes.
        preamble = b'\x00' * 128

    # File Meta Information is required under the DICOM standard, however if
    #   `write_like_original` is True we treat it as optional
    file_meta = getattr(dataset, 'file_meta', None)
    if not file_meta and not write_like_original:
        dataset.file_meta = Dataset()
        file_meta = dataset.file_meta

    # If enforcing the standard, correct the TransferSyntaxUID where possible,
    #   noting that the transfer syntax for is_implicit_VR = False and
    #   is_little_endian = True is ambiguous as it may be an encapsulated
    #   transfer syntax
    if not write_like_original:
        if dataset.is_little_endian and dataset.is_implicit_VR:
            file_meta.TransferSyntaxUID = ImplicitVRLittleEndian
        elif not dataset.is_little_endian and not dataset.is_implicit_VR:
            file_meta.TransferSyntaxUID = ExplicitVRBigEndian
        elif not dataset.is_little_endian and dataset.is_implicit_VR:
            raise NotImplementedError("Implicit VR Big Endian is not a"
                                      "supported Transfer Syntax.")

        if 'SOPClassUID' in dataset:
            file_meta.MediaStorageSOPClassUID = dataset.SOPClassUID
        if 'SOPInstanceUID' in dataset:
            file_meta.MediaStorageSOPInstanceUID = dataset.SOPInstanceUID

    caller_owns_file = True
    # Open file if not already a file object
    if isinstance(filename, compat.string_types):
        fp = DicomFile(filename, 'wb')
        # caller provided a file name; we own the file handle
        caller_owns_file = False
    else:
        fp = DicomFileLike(filename)

    try:
        # WRITE FILE META INFORMATION
        if preamble:
            # Write the 'DICM' prefix if and only if we write the preamble
            fp.write(preamble)
            fp.write(b'DICM')

        if file_meta is not None:  # May be an empty Dataset
            # If we want to `write_like_original`, don't enforce_standard
            write_file_meta_info(fp,
                                 file_meta,
                                 enforce_standard=not write_like_original)

        # WRITE DATASET
        # The transfer syntax used to encode the dataset can't be changed
        #   within the dataset.
        # Write any Command Set elements now as elements must be in tag order
        #   Mixing Command Set with other elements is non-conformant so we
        #   require `write_like_original` to be True
        command_set = dataset[0x00000000:0x00010000]
        if command_set and write_like_original:
            fp.is_implicit_VR = True
            fp.is_little_endian = True
            write_dataset(fp, command_set)

        # Set file VR and endianness. MUST BE AFTER writing META INFO (which
        #   requires Explicit VR Little Endian) and COMMAND SET (which requires
        #   Implicit VR Little Endian)
        fp.is_implicit_VR = dataset.is_implicit_VR
        fp.is_little_endian = dataset.is_little_endian

        # Write non-Command Set elements now
        write_dataset(fp, dataset[0x00010000:])
    finally:
        if not caller_owns_file:
            fp.close()
Пример #10
0
 def test_read(self):
     """Test the function"""
     with DicomFile(TEST_FILE, 'rb') as fp:
         assert not fp.parent.closed
         assert 'CT_small.dcm' in fp.name
         assert fp.read(2) == b'\x49\x49'
Пример #11
0
def dcmwrite(filename, dataset, write_like_original=True):
    """Write `dataset` to the `filename` specified.

    If `write_like_original` is True then `dataset` will be written as is
    (after minimal validation checking) and may or may not contain all or parts
    of the File Meta Information (and hence may or may not be conformant with
    the DICOM File Format).
    If `write_like_original` is False, `dataset` will be stored in the DICOM
    File Format in accordance with DICOM Standard Part 10 Section 7. The byte
    stream of the `dataset` will be placed into the file after the DICOM File
    Meta Information.

    File Meta Information
    ---------------------
    The File Meta Information consists of a 128-byte preamble, followed by a 4
    byte DICOM prefix, followed by the File Meta Information Group elements.

    Preamble and Prefix
    ~~~~~~~~~~~~~~~~~~~
    The `dataset.preamble` attribute shall be 128-bytes long or None and is
    available for use as defined by the Application Profile or specific
    implementations. If the preamble is not used by an Application Profile or
    specific implementation then all 128 bytes should be set to 0x00. The
    actual preamble written depends on `write_like_original` and
    `dataset.preamble` (see the table below).

    +------------------+------------------------------+
    |                  | write_like_original          |
    +------------------+-------------+----------------+
    | dataset.preamble | True        | False          |
    +==================+=============+================+
    | None             | no preamble | 128 0x00 bytes |
    +------------------+------------------------------+
    | 128 bytes        | dataset.preamble             |
    +------------------+------------------------------+

    The prefix shall be the string 'DICM' and will be written if and only if
    the preamble is present.

    File Meta Information Group Elements
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    The preamble and prefix are followed by a set of DICOM Elements from the
    (0002,eeee) group. Some of these elements are required (Type 1) while
    others are optional (Type 3/1C). If `write_like_original` is True then the
    File Meta Information Group elements are all optional. See
    pydicom.filewriter.write_file_meta_info for more information on which
    elements are required.

    The File Meta Information Group elements should be included within their
    own Dataset in the `dataset.file_meta` attribute.

    If (0002,0010) 'Transfer Syntax UID' is included then the user must ensure
    it's value is compatible with the values for the `dataset.is_little_endian`
    and `dataset.is_implicit_VR` attributes. For example, if is_little_endian
    and is_implicit_VR are both True then the Transfer Syntax UID must be
    1.2.840.10008.1.2 'Implicit VR Little Endian'. See the DICOM standard
    Part 5 Section 10 for more information on Transfer Syntaxes.

    Encoding
    ~~~~~~~~
    The preamble and prefix are encoding independent. The File Meta Elements
    are encoded as Explicit VR Little Endian as required by the DICOM standard.

    Dataset
    -------
    A DICOM Dataset representing a SOP Instance related to a DICOM Information
    Object Definition. It is up to the user to ensure the `dataset` conforms
    to the DICOM standard.

    Encoding
    ~~~~~~~~
    The `dataset` is encoded as specified by the `dataset.is_little_endian`
    and `dataset.is_implicit_VR` attributes. It's up to the user to ensure
    these attributes are set correctly (as well as setting an appropriate value
    for `dataset.file_meta.TransferSyntaxUID` if present).

    Parameters
    ----------
    filename : str or file-like
        Name of file or the file-like to write the new DICOM file to.
    dataset : pydicom.dataset.FileDataset
        Dataset holding the DICOM information; e.g. an object read with
        pydicom.dcmread().
    write_like_original : bool
        If True (default), preserves the following information from
        the Dataset (and may result in a non-conformant file):
        - preamble -- if the original file has no preamble then none will be
            written.
        - file_meta -- if the original file was missing any required File Meta
            Information Group elements then they will not be added or written.
            If (0002,0000) 'File Meta Information Group Length' is present then
            it may have its value updated.
        - seq.is_undefined_length -- if original had delimiters, write them now
            too, instead of the more sensible length characters
        - is_undefined_length_sequence_item -- for datasets that belong to a
            sequence, write the undefined length delimiters if that is
            what the original had.
        If False, produces a file conformant with the DICOM File Format, with
        explicit lengths for all elements.

    See Also
    --------
    pydicom.dataset.FileDataset
        Dataset class with relevant attributes and information.
    pydicom.dataset.Dataset.save_as
        Write a DICOM file from a dataset that was read in with dcmread().
        save_as wraps dcmwrite.
    """
    # Check that dataset's group 0x0002 elements are only present in the
    #   `dataset.file_meta` Dataset - user may have added them to the wrong
    #   place
    if dataset.group_dataset(0x0002) != Dataset():
        raise ValueError("File Meta Information Group Elements (0002,eeee) "
                         "should be in their own Dataset object in the "
                         "'{0}.file_meta' "
                         "attribute.".format(dataset.__class__.__name__))

    # A preamble is required under the DICOM standard, however if
    #   `write_like_original` is True we treat it as optional
    preamble = getattr(dataset, 'preamble', None)
    if preamble and len(preamble) != 128:
        raise ValueError("'{0}.preamble' must be 128-bytes "
                         "long.".format(dataset.__class__.__name__))
    if not preamble and not write_like_original:
        # The default preamble is 128 0x00 bytes.
        preamble = b'\x00' * 128

    # File Meta Information is required under the DICOM standard, however if
    #   `write_like_original` is True we treat it as optional
    file_meta = getattr(dataset, 'file_meta', None)
    if not file_meta and not write_like_original:
        dataset.file_meta = Dataset()
        file_meta = dataset.file_meta

    # If enforcing the standard, correct the TransferSyntaxUID where possible,
    #   noting that the transfer syntax for is_implicit_VR = False and
    #   is_little_endian = True is ambiguous as it may be an encapsulated
    #   transfer syntax
    if not write_like_original:
        if dataset.is_little_endian and dataset.is_implicit_VR:
            file_meta.TransferSyntaxUID = ImplicitVRLittleEndian
        elif not dataset.is_little_endian and not dataset.is_implicit_VR:
            file_meta.TransferSyntaxUID = ExplicitVRBigEndian
        elif not dataset.is_little_endian and dataset.is_implicit_VR:
            raise NotImplementedError("Implicit VR Big Endian is not a "
                                      "supported Transfer Syntax.")

        if 'SOPClassUID' in dataset:
            file_meta.MediaStorageSOPClassUID = dataset.SOPClassUID
        if 'SOPInstanceUID' in dataset:
            file_meta.MediaStorageSOPInstanceUID = dataset.SOPInstanceUID

    # Check for decompression, give warnings if inconsistencies
    # If decompressed, then pixel_array is now used instead of PixelData
    if dataset.is_decompressed:
        xfer = dataset.file_meta.TransferSyntaxUID
        if xfer not in UncompressedPixelTransferSyntaxes:
            raise ValueError("file_meta transfer SyntaxUID is compressed type "
                             "but pixel data has been decompressed")

        # Force PixelData to the decompressed version
        dataset.PixelData = dataset.pixel_array.tobytes()

    caller_owns_file = True
    # Open file if not already a file object
    if isinstance(filename, compat.string_types):
        fp = DicomFile(filename, 'wb')
        # caller provided a file name; we own the file handle
        caller_owns_file = False
    else:
        fp = DicomFileLike(filename)

    try:
        # WRITE FILE META INFORMATION
        if preamble:
            # Write the 'DICM' prefix if and only if we write the preamble
            fp.write(preamble)
            fp.write(b'DICM')

        if file_meta is not None:  # May be an empty Dataset
            # If we want to `write_like_original`, don't enforce_standard
            write_file_meta_info(fp, file_meta,
                                 enforce_standard=not write_like_original)

        # WRITE DATASET
        # The transfer syntax used to encode the dataset can't be changed
        #   within the dataset.
        # Write any Command Set elements now as elements must be in tag order
        #   Mixing Command Set with other elements is non-conformant so we
        #   require `write_like_original` to be True
        command_set = dataset[0x00000000:0x00010000]
        if command_set and write_like_original:
            fp.is_implicit_VR = True
            fp.is_little_endian = True
            write_dataset(fp, command_set)

        # Set file VR and endianness. MUST BE AFTER writing META INFO (which
        #   requires Explicit VR Little Endian) and COMMAND SET (which requires
        #   Implicit VR Little Endian)
        fp.is_implicit_VR = dataset.is_implicit_VR
        fp.is_little_endian = dataset.is_little_endian

        # Write non-Command Set elements now
        write_dataset(fp, dataset[0x00010000:])
    finally:
        if not caller_owns_file:
            fp.close()
Пример #12
0
 def test_read(self):
     """Test the function"""
     fp = DicomFile(TEST_FILE, 'rb')
     assert not fp.parent.closed
     assert 'CT_small.dcm' in fp.name
     assert fp.read(2) == b'\x49\x49'
Пример #13
0
    def open(self) -> None:
        """Opens file and reads metadata from it.

        Raises
        ------
        FileNotFoundError
            When file cannot be found
        OSError
            When file cannot be opened
        IOError
            When DICOM metadata cannot be read from file
        ValueError
            When DICOM dataset contained in file does not represent an image

        Note
        ----
        Builds a Basic Offset Table to speed up subsequent frame-level access.

        """
        logger.debug('read File Meta Information')
        file_meta = read_file_meta_info(self.filename)
        transfer_syntax_uid = UID(file_meta.TransferSyntaxUID)
        try:
            self._fp = DicomFile(str(self.filename), mode='rb')
            self._fp.is_little_endian = transfer_syntax_uid.is_little_endian
            self._fp.is_implicit_VR = transfer_syntax_uid.is_implicit_VR
        except FileNotFoundError:
            raise FileNotFoundError(f'File not found: "{self.filename}"')
        except Exception:
            raise OSError(
                f'Could not open file for reading: "{self.filename}"')
        logger.debug('read metadata elements')
        try:
            self._metadata = dcmread(self._fp, stop_before_pixels=True)
        except Exception as err:
            raise IOError(
                f'DICOM metadata cannot be read from file "{self.filename}": '
                f'"{err}"')
        self._pixel_data_offset = self._fp.tell()
        # Determine whether dataset contains a Pixel Data element
        try:
            tag = TupleTag(self._fp.read_tag())
        except EOFError:
            raise ValueError(
                'Dataset does not represent an image information entity.')
        if int(tag) not in _PIXEL_DATA_TAGS:
            raise ValueError(
                'Dataset does not represent an image information entity.')
        self._as_float = False
        if int(tag) in _FLOAT_PIXEL_DATA_TAGS:
            self._as_float = True

        # Reset the file pointer to the beginning of the Pixel Data element
        self._fp.seek(self._pixel_data_offset, 0)

        # Build the ICC Transformation object. This takes some time and should
        # be done only once to speedup subsequent color corrections.

        if self.metadata.SamplesPerPixel == 1:
            self._color_manager = None
        else:
            try:
                icc_profile = self.metadata.ICCProfile
            except AttributeError:
                try:
                    if len(self.metadata.OpticalPathSequence) > 1:
                        # This should not happen in case of a color image.
                        logger.warning(
                            'color image contains more than one optical path')
                    optical_path_item = self.metadata.OpticalPathSequence[0]
                    icc_profile = optical_path_item.ICCProfile
                except (IndexError, AttributeError):
                    raise AttributeError(
                        'No ICC Profile found in image metadata.')
            try:
                self._color_manager = ColorManager(icc_profile)
            except ValueError:
                logger.warning('could not read ICC Profile')
                self._color_manager = None

        logger.debug('build Basic Offset Table')
        transfer_syntax_uid = self.metadata.file_meta.TransferSyntaxUID
        if transfer_syntax_uid.is_encapsulated:
            try:
                self._basic_offset_table = _get_bot(
                    self._fp, number_of_frames=self.number_of_frames)
            except Exception as err:
                raise IOError(f'Failed to build Basic Offset Table: "{err}"')
            self._first_frame_offset = self._fp.tell()
        else:
            if self._fp.is_implicit_VR:
                header_offset = 4 + 4  # tag and length
            else:
                header_offset = 4 + 2 + 2 + 4  # tag, VR, reserved and length
            self._first_frame_offset = self._pixel_data_offset + header_offset
            n_pixels = self._pixels_per_frame
            bits_allocated = self.metadata.BitsAllocated
            if bits_allocated == 1:
                self._basic_offset_table = [
                    int(np.floor(i * n_pixels / 8))
                    for i in range(self.number_of_frames)
                ]
            else:
                self._basic_offset_table = [
                    i * self._bytes_per_frame_uncompressed
                    for i in range(self.number_of_frames)
                ]

        if len(self._basic_offset_table) != self.number_of_frames:
            raise ValueError(
                'Length of Basic Offset Table does not match Number of Frames.'
            )
Пример #14
0
class ImageFileReader(object):
    """Reader for DICOM datasets representing Image Information Entities.

    It provides efficient access to individual Frame items contained in the
    Pixel Data element without loading the entire element into memory.

    Attributes
    ----------
    filename: str
        Path to the DICOM Part10 file on disk

    Examples
    --------
    >>> from highdicom.io import ImageFileReader
    >>> with ImageFileReader('/path/to/file.dcm') as image:
    ...     print(image.metadata)
    ...     for i in range(image.number_of_frames):
    ...         frame = image.read_frame(i)
    ...         print(frame.shape)

    """
    def __init__(self, filename: str):
        """
        Parameters
        ----------
        filename: str
            Path to a DICOM Part10 file containing a dataset of an image
            SOP Instance

        """
        self.filename = filename

    def __enter__(self) -> 'ImageFileReader':
        self.open()
        return self

    def __exit__(self, except_type, except_value, except_trace) -> None:
        self._fp.close()
        if except_value:
            sys.stdout.write('Error while accessing file "{}":\n{}'.format(
                self.filename, str(except_value)))
            for tb in traceback.format_tb(except_trace):
                sys.stdout.write(tb)
            raise

    def open(self) -> None:
        """Opens file and reads metadata from it.

        Raises
        ------
        FileNotFoundError
            When file cannot be found
        OSError
            When file cannot be opened
        IOError
            When DICOM metadata cannot be read from file
        ValueError
            When DICOM dataset contained in file does not represent an image

        Note
        ----
        Builds a Basic Offset Table to speed up subsequent frame-level access.

        """
        logger.debug('read File Meta Information')
        file_meta = read_file_meta_info(self.filename)
        transfer_syntax_uid = UID(file_meta.TransferSyntaxUID)
        try:
            self._fp = DicomFile(str(self.filename), mode='rb')
            self._fp.is_little_endian = transfer_syntax_uid.is_little_endian
            self._fp.is_implicit_VR = transfer_syntax_uid.is_implicit_VR
        except FileNotFoundError:
            raise FileNotFoundError(f'File not found: "{self.filename}"')
        except Exception:
            raise OSError(
                f'Could not open file for reading: "{self.filename}"')
        logger.debug('read metadata elements')
        try:
            self._metadata = dcmread(self._fp, stop_before_pixels=True)
        except Exception as err:
            raise IOError(
                f'DICOM metadata cannot be read from file "{self.filename}": '
                f'"{err}"')
        self._pixel_data_offset = self._fp.tell()
        # Determine whether dataset contains a Pixel Data element
        try:
            tag = TupleTag(self._fp.read_tag())
        except EOFError:
            raise ValueError(
                'Dataset does not represent an image information entity.')
        if int(tag) not in _PIXEL_DATA_TAGS:
            raise ValueError(
                'Dataset does not represent an image information entity.')
        self._as_float = False
        if int(tag) in _FLOAT_PIXEL_DATA_TAGS:
            self._as_float = True

        # Reset the file pointer to the beginning of the Pixel Data element
        self._fp.seek(self._pixel_data_offset, 0)

        # Build the ICC Transformation object. This takes some time and should
        # be done only once to speedup subsequent color corrections.

        if self.metadata.SamplesPerPixel == 1:
            self._color_manager = None
        else:
            try:
                icc_profile = self.metadata.ICCProfile
            except AttributeError:
                try:
                    if len(self.metadata.OpticalPathSequence) > 1:
                        # This should not happen in case of a color image.
                        logger.warning(
                            'color image contains more than one optical path')
                    optical_path_item = self.metadata.OpticalPathSequence[0]
                    icc_profile = optical_path_item.ICCProfile
                except (IndexError, AttributeError):
                    raise AttributeError(
                        'No ICC Profile found in image metadata.')
            try:
                self._color_manager = ColorManager(icc_profile)
            except ValueError:
                logger.warning('could not read ICC Profile')
                self._color_manager = None

        logger.debug('build Basic Offset Table')
        transfer_syntax_uid = self.metadata.file_meta.TransferSyntaxUID
        if transfer_syntax_uid.is_encapsulated:
            try:
                self._basic_offset_table = _get_bot(
                    self._fp, number_of_frames=self.number_of_frames)
            except Exception as err:
                raise IOError(f'Failed to build Basic Offset Table: "{err}"')
            self._first_frame_offset = self._fp.tell()
        else:
            if self._fp.is_implicit_VR:
                header_offset = 4 + 4  # tag and length
            else:
                header_offset = 4 + 2 + 2 + 4  # tag, VR, reserved and length
            self._first_frame_offset = self._pixel_data_offset + header_offset
            n_pixels = self._pixels_per_frame
            bits_allocated = self.metadata.BitsAllocated
            if bits_allocated == 1:
                self._basic_offset_table = [
                    int(np.floor(i * n_pixels / 8))
                    for i in range(self.number_of_frames)
                ]
            else:
                self._basic_offset_table = [
                    i * self._bytes_per_frame_uncompressed
                    for i in range(self.number_of_frames)
                ]

        if len(self._basic_offset_table) != self.number_of_frames:
            raise ValueError(
                'Length of Basic Offset Table does not match Number of Frames.'
            )

    @property
    def metadata(self) -> Dataset:
        """pydicom.dataset.Dataset: Metadata"""
        try:
            return self._metadata
        except AttributeError:
            raise IOError('File has not been opened for reading.')

    @property
    def _pixels_per_frame(self) -> int:
        """int: Number of pixels per frame"""
        return int(
            np.prod([
                self.metadata.Rows, self.metadata.Columns,
                self.metadata.SamplesPerPixel
            ]))

    @property
    def _bytes_per_frame_uncompressed(self) -> int:
        """int: Number of bytes per frame when uncompressed"""
        n_pixels = self._pixels_per_frame
        bits_allocated = self.metadata.BitsAllocated
        if bits_allocated == 1:
            # Determine the nearest whole number of bytes needed to contain
            #   1-bit pixel data. e.g. 10 x 10 1-bit pixels is 100 bits, which
            #   are packed into 12.5 -> 13 bytes
            return n_pixels // 8 + (n_pixels % 8 > 0)
        else:
            return n_pixels * bits_allocated // 8

    def close(self) -> None:
        """Closes file."""
        self._fp.close()

    def read_frame_raw(self, index: int) -> bytes:
        """Reads the raw pixel data of an individual frame item.

        Parameters
        ----------
        index: int
            Zero-based frame index

        Returns
        -------
        bytes
            Pixel data of a given frame item encoded in the transfer syntax.

        Raises
        ------
        IOError
            When frame could not be read

        """
        if index > self.number_of_frames:
            raise ValueError('Frame index exceeds number of frames in image.')
        logger.debug(f'read frame #{index}')

        frame_offset = self._basic_offset_table[index]
        self._fp.seek(self._first_frame_offset + frame_offset, 0)
        if self.metadata.file_meta.TransferSyntaxUID.is_encapsulated:
            try:
                stop_at = self._basic_offset_table[index + 1] - frame_offset
            except IndexError:
                # For the last frame, there is no next offset available.
                stop_at = -1
            n = 0
            # A frame may consist of multiple items (fragments).
            fragments = []
            while True:
                tag = TupleTag(self._fp.read_tag())
                if n == stop_at or int(tag) == SequenceDelimiterTag:
                    break
                if int(tag) != ItemTag:
                    raise ValueError(f'Failed to read frame #{index}.')
                length = self._fp.read_UL()
                fragments.append(self._fp.read(length))
                n += 4 + 4 + length
            frame_data = b''.join(fragments)
        else:
            frame_data = self._fp.read(self._bytes_per_frame_uncompressed)

        if len(frame_data) == 0:
            raise IOError(f'Failed to read frame #{index}.')

        return frame_data

    def read_frame(self, index: int, correct_color: bool = True) -> np.ndarray:
        """Reads and decodes the pixel data of an individual frame item.

        Parameters
        ----------
        index: int
            Zero-based frame index
        correct_color: bool, optional
            Whether colors should be corrected by applying an ICC
            transformation. Will only be performed if metadata contain an
            ICC Profile.

        Returns
        -------
        numpy.ndarray
            Array of decoded pixels of the frame with shape (Rows x Columns)
            in case of a monochrome image or (Rows x Columns x SamplesPerPixel)
            in case of a color image.

        Raises
        ------
        IOError
            When frame could not be read

        """
        frame_data = self.read_frame_raw(index)

        logger.debug(f'decode frame #{index}')

        if self.metadata.BitsAllocated == 1:
            unpacked_frame = unpack_bits(frame_data)
            rows, columns = self.metadata.Rows, self.metadata.Columns
            n_pixels = self._pixels_per_frame
            pixel_offset = int(((index * n_pixels / 8) % 1) * 8)
            pixel_array = unpacked_frame[pixel_offset:pixel_offset + n_pixels]
            return pixel_array.reshape(rows, columns)

        frame_array = decode_frame(
            frame_data,
            rows=self.metadata.Rows,
            columns=self.metadata.Columns,
            samples_per_pixel=self.metadata.SamplesPerPixel,
            transfer_syntax_uid=self.metadata.file_meta.TransferSyntaxUID,
            bits_allocated=self.metadata.BitsAllocated,
            bits_stored=self.metadata.BitsStored,
            photometric_interpretation=self.metadata.PhotometricInterpretation,
            pixel_representation=self.metadata.PixelRepresentation,
            planar_configuration=getattr(self.metadata, 'PlanarConfiguration',
                                         None))

        # We don't use the color_correct_frame() function here, since we cache
        # the ICC transform on the reader instance for improved performance.
        if correct_color and self._color_manager is not None:
            logger.debug(f'correct color of frame #{index}')
            return self._color_manager.transform_frame(frame_array)

        return frame_array

    @property
    def number_of_frames(self) -> int:
        """int: Number of frames"""
        try:
            return int(self.metadata.NumberOfFrames)
        except AttributeError:
            return 1
Пример #15
0
def _build_bot(fp: DicomFile, number_of_frames: int) -> List[int]:
    """Builds a Basic Offset Table (BOT) item of an encapsulated Pixel Data
    element.

    Parameters
    ----------
    fp: pydicom.filebase.DicomFile
        Pointer for DICOM PS3.10 file stream positioned at the first byte of
        the Pixel Data element following the empty Basic Offset Table (BOT)
    number_of_frames: int
        Total number of frames in the dataset

    Returns
    -------
    List[int]
        Offset of each Frame item in bytes from the first byte of the Pixel Data
        element following the BOT item

    Note
    ----
    Moves the pointer back to the first byte of the Pixel Data element
    following the BOT item (the first byte of the first Frame item).

    Raises
    ------
    IOError
        When file pointer is not positioned at first byte of first Frame item
        after Basic Offset Table item or when parsing of Frame item headers
        fails
    ValueError
        When the number of offsets doesn't match the specified number of frames

    """
    initial_position = fp.tell()
    offset_values = []
    current_offset = 0
    i = 0
    while True:
        frame_position = fp.tell()
        tag = TupleTag(fp.read_tag())
        if int(tag) == SequenceDelimiterTag:
            break
        if int(tag) != ItemTag:
            fp.seek(initial_position, 0)
            raise IOError(
                'Building Basic Offset Table (BOT) failed. '
                f'Expected tag of Frame item #{i} at position {frame_position}.'
            )
        length = fp.read_UL()
        if length % 2:
            fp.seek(initial_position, 0)
            raise IOError('Building Basic Offset Table (BOT) failed. '
                          f'Length of Frame item #{i} is not a multiple of 2.')
        elif length == 0:
            fp.seek(initial_position, 0)
            raise IOError('Building Basic Offset Table (BOT) failed. '
                          f'Length of Frame item #{i} is zero.')

        first_two_bytes = fp.read(2, 1)
        if not fp.is_little_endian:
            first_two_bytes = first_two_bytes[::-1]

        # In case of fragmentation, we only want to get the offsets to the
        # first fragment of a given frame. We can identify those based on the
        # JPEG and JPEG 2000 markers that should be found at the beginning and
        # end of the compressed byte stream.
        if first_two_bytes in _START_MARKERS:
            current_offset = frame_position - initial_position
            offset_values.append(current_offset)

        i += 1
        fp.seek(length - 2, 1)  # minus the first two bytes

    if len(offset_values) != number_of_frames:
        raise ValueError(
            'Number of frame items does not match specified Number of Frames.')
    else:
        basic_offset_table = offset_values

    fp.seek(initial_position, 0)
    return basic_offset_table