Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
305 changes: 261 additions & 44 deletions src/highdicom/legacy/sop.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,18 +190,15 @@ def _read_ambiguous_vr(dataset: Dataset, kw: str) -> int:
val = getattr(dataset, kw)
if (
dataset.PixelRepresentation == 1 and
dataset[kw].VR == "US"
dataset[kw].VR == "US" and
# Value in this range indicates that correction is
# needed, since it is invalid for VR of 'SS'
val > 32767
):
# Incorrect VR is used

if val > 32767:
# Value in this range indicates that correction is
# needed, since it is invalid for VR of 'SS'

# Find the negative number that has the same 16-bit binary
# representation as the value found and assume this is what was
# intended in the original file
return val - 65536
# Incorrect VR is used - Find the negative number that has the same
# 16-bit binary representation as the value found and assume this is
# what was intended in the original file
return val - 65536

return val

Expand Down Expand Up @@ -342,6 +339,7 @@ def __init__(
use_extended_offset_table: bool = False,
require_volume: bool = False,
include_dimension_index: bool | None = None,
include_enhanced_groups: bool = True,
strict: bool = False,
skip_private_attributes: bool = False,
workers: int | Executor = 0,
Expand Down Expand Up @@ -399,6 +397,15 @@ def __init__(
passed with no index. If ``False``, no attempt will be made to sort
the datasets, they will be encoded in the order they are passed
without including a dimension index.
include_enhanced_groups: bool, optional
Include multi-frame functional groups that are not part of the
Legacy Enhanced IOD, but are part of the corresponding Enhanced
IOD, if information exists in the legacy files to include in them.
If ``False``, only those groups explicitly included in the Legacy
Converted IOD are created in the new file, and any attributes that
do not belong in them are placed into the
UnassignedSharedConvertedAttributesSequence or
UnassignedPerFrameConvertedAttributesSequence.
strict: bool, optional
Whether to use strict requirements on the new converted dataset. If
True and any attributes required to create a standard-compliant
Expand Down Expand Up @@ -483,7 +490,7 @@ def __init__(
"ImagesInAcquisition",
]
self._unused_keywords = {
kw for kw in self._keyword_shared_dict.keys()
kw for kw in self._keyword_shared_dict
if kw not in excluded_keywords
}

Expand Down Expand Up @@ -816,6 +823,181 @@ def __init__(
required=False,
)

if include_enhanced_groups:
if (
self._destination.SOPClassUID ==
LegacyConvertedEnhancedCTImageStorage
):
self._add_functional_group(
"CTAcquisitionDetailsSequence",
[],
optional_attributes=[
_AttributeConfig("DataCollectionDiameter"),
_AttributeConfig("GantryDetectorTilt"),
_AttributeConfig("TableHeight"),
_AttributeConfig("RotationDirection"),
_AttributeConfig("RevolutionTime"),
_AttributeConfig("SingleCollimationWidth"),
_AttributeConfig("TotalCollimationWidth"),
],
required=False,
)
self._add_functional_group(
"CTReconstructionSequence",
[],
optional_attributes=[
_AttributeConfig("ReconstructionDiameter"),
_AttributeConfig("ConvolutionKernel"),
],
required=False,
)
self._add_functional_group(
"CTPositionSequence",
[],
optional_attributes=[
_AttributeConfig("DataCollectionCenterPatient"),
_AttributeConfig("ReconstructionTargetCenterPatient"),
],
required=False,
)
self._add_functional_group(
"CTXRayDetailsSequence",
[],
optional_attributes=[
_AttributeConfig("KVP"),
_AttributeConfig("FilterType"),
_AttributeConfig("FocalSpots"),
_AttributeConfig("FilterMaterial"),
_AttributeConfig("CalciumScoringMassFactorPatient"),
_AttributeConfig("CalciumScoringMassFactorDevice"),
_AttributeConfig("EnergyWeightingFactor"),
],
required=False,
)
self._add_functional_group(
"CTExposureSequence",
[],
optional_attributes=[
_AttributeConfig(
"ExposureTimeInms",
src_kws=["ExposureTimeInms", "ExposureTime"]
),
_AttributeConfig(
"XRayTubeCurrentInmA",
src_kws=["XRayTubeCurrent", "XRayTubeCurrentInmA"],
),
_AttributeConfig("ExposureInmAs"),
_AttributeConfig("ImageAndFluoroscopyAreaDoseProduct"),
_AttributeConfig("WaterEquivalentDiameter"),
_AttributeConfig(
"WaterEquivalentDiameterCalculationMethodCodeSequence" # noqa: E501
),
_AttributeConfig("ExposureModulationType"),
_AttributeConfig("CTDIvol"),
_AttributeConfig("CTDIPhantomTypeCodeSequence"),
],
custom_logic_callback=(
self._ct_exposure_custom_logic
),
required=False,
)
self._add_functional_group(
"CTGeometrySequence",
[],
optional_attributes=[
_AttributeConfig("DistanceSourceToDetector")
],
required=False,
)
self._add_functional_group(
"CTTableDynamicsSequence",
[],
optional_attributes=[
_AttributeConfig("TableSpeed"),
_AttributeConfig("TableFeedPerRotation"),
_AttributeConfig("SpiralPitchFactor"),
],
required=False,
)

self._copy_existing_sequence_to_functional_groups(
"CTAdditionalXRaySourceSequence"
)
self._copy_existing_sequence_to_functional_groups(
"MultienergyCTCharacteristicsSequence"
)
self._copy_existing_sequence_to_functional_groups(
"MultienergyCTProcessingSequence"
)

if (
self._destination.SOPClassUID ==
LegacyConvertedEnhancedMRImageStorage
):
self._add_functional_group(
"MRFOVGeometrySequence",
[],
optional_attributes=[
_AttributeConfig("PercentSampling"),
_AttributeConfig("PercentPhaseFieldOfView"),
_AttributeConfig("InPlanePhaseEncodingDirection"),
],
required=False,
)
self._add_functional_group(
"MRAveragesSequence",
[],
optional_attributes=[_AttributeConfig("NumberOfAverages")],
required=False,
)
self._add_functional_group(
"MRTransmitCoilSequence",
[],
optional_attributes=[_AttributeConfig("TransmitCoilName")],
required=False,
)
self._add_functional_group(
"MRTimingAndRelatedParametersSequence",
[],
optional_attributes=[
_AttributeConfig("RepetitionTime"),
_AttributeConfig("EchoTrainLength"),
_AttributeConfig("FlipAngle"),
],
required=False,
)
self._add_functional_group(
"MRImagingModifierSequence",
[],
optional_attributes=[_AttributeConfig("PixelBandwidth")],
required=False,
)
self._add_functional_group(
"MRReceiveCoilSequence",
[],
optional_attributes=[_AttributeConfig("ReceiveCoilName")],
required=False,
)

if (
self._destination.SOPClassUID ==
LegacyConvertedEnhancedPETImageStorage
):
self._add_functional_group(
"PETReconstructionSequence",
[],
optional_attributes=[
_AttributeConfig("ReconstructionDiameter")
],
required=False,
)
self._add_functional_group(
"PETFrameAcquisitionSequence",
[],
optional_attributes=[_AttributeConfig("TableHeight")],
required=False,
)

# Functional grops where entire existing sequences are copied from the
# source image
self._copy_existing_sequence_to_functional_groups(
Expand Down Expand Up @@ -1814,7 +1996,7 @@ def _frame_anatomy_custom_logic(
"'FrameLaterality' will be left empty in the "
"new legacy converted enhanced dataset."
)
logging.warning(msg)
logger.warning(msg)

destination.FrameLaterality = src_laterality

Expand Down Expand Up @@ -1865,6 +2047,30 @@ def _frame_content_custom_logic(

destination.FrameAcquisitionDateTime = fa_dt

def _ct_exposure_custom_logic(
self,
source: Dataset,
destination: Dataset,
) -> None:
"""Custom logic for the CTExposureSequence.

This is needed to handle unit conversion for exposure.

Parameters
----------
source: pydicom.Dataset
Dataset to copy from.
destination: pydicom.Dataset
Dataset to copy to.

"""
if (
"ExposureInmAs" not in destination and
"ExposureInuAs" in source
):
destination.ExposureInmAs = source.ExposureInuAs / 1000.0
self._mark_keyword_used("ExposureInuAs")

def _add_series_description(self) -> None:
"""Add a default series description."""
suffix = " (enhanced conversion)"
Expand Down Expand Up @@ -2528,13 +2734,14 @@ def __init__(
instance_number: int,
*,
transfer_syntax_uid: str | None = None,
use_extended_offset_table: bool = False,
require_volume: bool = False,
strict: bool = False,
skip_private_attributes: bool = False,
include_dimension_index: bool | None = None,
include_enhanced_groups: bool = True,
contributing_equipment: Sequence[ContributingEquipment] | None = None,
use_extended_offset_table: bool = False,
workers: int | Executor = 0,
include_dimension_index: bool | None = None,
**kwargs: Any,
) -> None:
"""
Expand Down Expand Up @@ -2564,15 +2771,23 @@ def __init__(
are JPEG 2000 (``"1.2.840.10008.1.2.4.91"``) , JPEG 2000 Lossless
(``"1.2.840.10008.1.2.4.90"``), and JPEG-LS Lossless
(``"1.2.840.10008.1.2.4.80"``).
use_extended_offset_table: bool, optional
Include an extended offset table instead of a basic offset table
for encapsulated transfer syntaxes. Extended offset tables avoid
size limitations on basic offset tables, and separate the offset
table from the pixel data by placing it into metadata. However,
they may be less widely supported than basic offset tables. This
parameter is ignored if using a native (uncompressed) transfer
syntax. The default value may change in a future release.
include_dimension_index: bool | None = None, optional
require_volume: bool, optional
If True, raise an error if the legacy datasets do not represent a
set of parallel, regularly-spaced frames from the same series. This
is not a requirement for the conversion to be valid according to
the standard, but users may wish to additionally impose this
stricter requirement to ensure the resulting files are easier to
work with.
strict: bool, optional
Whether to use strict requirements on the new converted dataset. If
True and any attributes required to create a standard-compliant
Legacy Converted Enhanced instance are missing or inconsistent in
the legacy source files, an error will be raised. If False, a
warning will be raised in the same situation and the attribute will
be skipped in the new Legacy Converted Enhanced file.
skip_private_attributes: bool, optional
Whether to skip copying private attributes to the converted file.
include_dimension_index: bool | None, optional
Whether to include a dimension index within the
DimensionIndexSequence in the new converted dataset. A dimension
index describes how the frames are ordered within the instance.
Expand All @@ -2599,25 +2814,26 @@ def __init__(
passed with no index. If ``False``, no attempt will be made to sort
the datasets, they will be encoded in the order they are passed
without including a dimension index.
require_volume: bool, optional
If True, raise an error if the legacy datasets do not represent a
set of parallel, regularly-spaced frames from the same series. This
is not a requirement for the conversion to be valid according to
the standard, but users may wish to additionally impose this
stricter requirement to ensure the resulting files are easier to
work with.
strict: bool, optional
Whether to use strict requirements on the new converted dataset. If
True and any attributes required to create a standard-compliant
Legacy Converted Enhanced instance are missing or inconsistent in
the legacy source files, an error will be raised. If False, a
warning will be raised in the same situation and the attribute will
be skipped in the new Legacy Converted Enhanced file.
skip_private_attributes: bool, optional
Whether to skip copying private attributes to the converted file.
include_enhanced_groups: bool, optional
Include multi-frame functional groups that are not part of the
Legacy Enhanced IOD, but are part of the corresponding Enhanced
IOD, if information exists in the legacy files to include in them.
If ``False``, only those groups explicitly included in the Legacy
Converted IOD are created in the new file, and any attributes that
do not belong in them are placed into the
UnassignedSharedConvertedAttributesSequence or
UnassignedPerFrameConvertedAttributesSequence.
contributing_equipment: Sequence[highdicom.ContributingEquipment] | None, optional
Additional equipment that has contributed to the acquisition,
creation or modification of this instance.
use_extended_offset_table: bool, optional
Include an extended offset table instead of a basic offset table
for encapsulated transfer syntaxes. Extended offset tables avoid
size limitations on basic offset tables, and separate the offset
table from the pixel data by placing it into metadata. However,
they may be less widely supported than basic offset tables. This
parameter is ignored if using a native (uncompressed) transfer
syntax. The default value may change in a future release.
workers: int | concurrent.futures.Executor, optional
Number of worker processes to use for frame compression, if
compression or transcoding is needed. If 0, no workers are used and
Expand Down Expand Up @@ -2731,13 +2947,14 @@ def __init__(
_LegacyConversionRunner(
legacy_datasets,
destination=self,
workers=workers,
require_volume=require_volume,
transfer_syntax_uid=transfer_syntax_uid,
use_extended_offset_table=use_extended_offset_table,
include_dimension_index=include_dimension_index,
skip_private_attributes=skip_private_attributes,
include_enhanced_groups=include_enhanced_groups,
strict=strict,
require_volume=require_volume,
skip_private_attributes=skip_private_attributes,
use_extended_offset_table=use_extended_offset_table,
workers=workers,
)

self._build_luts()
Expand Down
Loading