Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add function to correctly format a DS string #57

Closed
wants to merge 3 commits into from
Closed
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
8 changes: 8 additions & 0 deletions docs/package.rst
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,14 @@ highdicom.utils module
:undoc-members:
:show-inheritance:

highdicom.valuerep module
+++++++++++++++++++++++++

.. automodule:: highdicom.valuerep
:members:
:undoc-members:
:show-inheritance:


.. _highdicom-legacy-subpackage:

Expand Down
28 changes: 16 additions & 12 deletions src/highdicom/content.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
CoordinateSystemNames,
UniversalEntityIDTypeValues,
)
from highdicom.valuerep import get_ds_string
from highdicom.sr.coding import CodedConcept
from highdicom.sr.value_types import (
CodeContentItem,
Expand Down Expand Up @@ -103,10 +104,10 @@ def __init__(
"""
super().__init__()
item = Dataset()
item.PixelSpacing = list(pixel_spacing)
item.SliceThickness = slice_thickness
item.PixelSpacing = [get_ds_string(ps) for ps in pixel_spacing]
item.SliceThickness = get_ds_string(slice_thickness)
if spacing_between_slices is not None:
item.SpacingBetweenSlices = spacing_between_slices
item.SpacingBetweenSlices = get_ds_string(spacing_between_slices)
self.append(item)


Expand Down Expand Up @@ -145,9 +146,6 @@ def __init__(
super().__init__()
item = Dataset()

def ds(num: float) -> float:
return float(str(num)[:16])

coordinate_system = CoordinateSystemNames(coordinate_system)
if coordinate_system == CoordinateSystemNames.SLIDE:
if pixel_matrix_position is None:
Expand All @@ -157,13 +155,15 @@ def ds(num: float) -> float:
)
col_position, row_position = pixel_matrix_position
x, y, z = image_position
item.XOffsetInSlideCoordinateSystem = ds(x)
item.YOffsetInSlideCoordinateSystem = ds(y)
item.ZOffsetInSlideCoordinateSystem = ds(z)
item.XOffsetInSlideCoordinateSystem = get_ds_string(x)
item.YOffsetInSlideCoordinateSystem = get_ds_string(y)
item.ZOffsetInSlideCoordinateSystem = get_ds_string(z)
item.RowPositionInTotalImagePixelMatrix = row_position
item.ColumnPositionInTotalImagePixelMatrix = col_position
elif coordinate_system == CoordinateSystemNames.PATIENT:
item.ImagePositionPatient = list(image_position)
item.ImagePositionPatient = [
get_ds_string(p) for p in image_position
]
else:
raise ValueError(
f'Unknown coordinate system "{coordinate_system.value}".'
Expand Down Expand Up @@ -241,9 +241,13 @@ def __init__(
item = Dataset()
coordinate_system = CoordinateSystemNames(coordinate_system)
if coordinate_system == CoordinateSystemNames.SLIDE:
item.ImageOrientationSlide = list(image_orientation)
item.ImageOrientationSlide = [
get_ds_string(e) for e in image_orientation
]
elif coordinate_system == CoordinateSystemNames.PATIENT:
item.ImageOrientationPatient = list(image_orientation)
item.ImageOrientationPatient = [
get_ds_string(e) for e in image_orientation
]
else:
raise ValueError(
f'Unknown coordinate system "{coordinate_system.value}".'
Expand Down
5 changes: 3 additions & 2 deletions src/highdicom/sr/value_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from pydicom.uid import UID
from pydicom.valuerep import DA, TM, DT, PersonName

from highdicom.valuerep import get_ds_string
from highdicom.sr.coding import CodedConcept
from highdicom.sr.enum import (
GraphicTypeValues,
Expand Down Expand Up @@ -426,7 +427,7 @@ def __init__(
raise TypeError(
'Argument "value" must have type "int" or "float".'
)
measured_value_sequence_item.NumericValue = value
measured_value_sequence_item.NumericValue = get_ds_string(value)
if isinstance(value, float):
measured_value_sequence_item.FloatingPointValue = value
if not isinstance(unit, (CodedConcept, Code, )):
Expand Down Expand Up @@ -801,7 +802,7 @@ def __init__(
]
elif referenced_time_offsets is not None:
self.ReferencedTimeOffsets = [
float(v) for v in referenced_time_offsets
get_ds_string(float(v)) for v in referenced_time_offsets
]
elif referenced_date_time is not None:
self.ReferencedDateTime = [
Expand Down
68 changes: 68 additions & 0 deletions src/highdicom/valuerep.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
from math import log10, floor

from numpy import isfinite


def get_ds_string(f: float) -> str:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it make sense for this to be in pydicom instead?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pieper I agree, this would ideally go into pydicom. There is an option issue (see pydicom/pydicom#1264). @CPBridge have you considered creating a PR to update pydicom.valuerep.DS?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@CPBridge given that this has been merged into pydicom, can we remove this module?

Copy link
Collaborator Author

@CPBridge CPBridge Apr 7, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, there's no sense reviewing this PR in its current form

"""Get a string representation of a float suitable for DS value types

Returns a string representation of a floating point number that follows
the constraints that apply to decimal strings (DS) value representations.
These include that the string must be a maximum of 16 characters and
contain only digits, and '+', '-', '.' and 'e' characters.

Parameters
----------
f: float
Floating point number whose string representation is required

Returns
-------
str:
String representation of f, following the decimal string constraints

Raises
------
ValueError:
If the float is not representable as a decimal string (for example
because it has value ``nan`` or ``inf``)

"""
if not isfinite(f):
raise ValueError(
"Cannot encode non-finite floats as DICOM decimal strings. "
f"Got {f}"
)

fstr = str(f)
# In the simple case, the built-in python string representation
# will do
if len(fstr) <= 16:
return fstr

# Decide whether to use scientific notation
# (follow convention of python's standard float to string conversion)
logf = log10(abs(f))
use_scientific = logf < -4 or logf >= 13

# Characters needed for '-' at start
sign_chars = 1 if f < 0.0 else 0

if use_scientific:
# How many chars are taken by the exponent at the end.
# In principle, we could have number where the exponent
# needs three digits represent (bigger than this cannot be
# represented by floats)
if logf >= 100 or logf <= -100:
exp_chars = 5 # e.g. 'e-123'
else:
exp_chars = 4 # e.g. 'e+08'
remaining_chars = 14 - sign_chars - exp_chars
return f'%.{remaining_chars}e' % f
else:
if logf >= 1.0:
# chars remaining for digits after sign, digits left of '.' and '.'
remaining_chars = 14 - sign_chars - int(floor(logf))
else:
remaining_chars = 14 - sign_chars
return f'%.{remaining_chars}f' % f
36 changes: 36 additions & 0 deletions tests/test_valuerep.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import pytest
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@CPBridge I assume we no longer need these tests in highdicom, do we?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No this PR was originally written with the assumption that the functionality would be in highdicom and I haven't adapted it to at all yet for the fact that the functionality has moved to pydicom. A lot will need to change. I might just create a new PR from scratch, not sure which will end up being easier


from highdicom.valuerep import get_ds_string


@pytest.mark.parametrize(
"float_val,expected_str",
[
[1.0, "1.0"],
[0.0, "0.0"],
[-0.0, "-0.0"],
[0.123, "0.123"],
[-0.321, "-0.321"],
[0.00001, "1e-05"],
[3.14159265358979323846, '3.14159265358979'],
[-3.14159265358979323846, '-3.1415926535898'],
[5.3859401928763739403e-7, '5.3859401929e-07'],
[-5.3859401928763739403e-7, '-5.385940193e-07'],
[1.2342534378125532912998323e10, '12342534378.1255'],
[6.40708699858767842501238e13, '6.4070869986e+13'],
[1.7976931348623157e+308, '1.797693135e+308'],
]
)
def test_get_ds_string(float_val: float, expected_str: str):
returned_str = get_ds_string(float_val)
assert len(returned_str) <= 16
assert expected_str == returned_str


@pytest.mark.parametrize(
'float_val',
[float('nan'), float('inf'), float('-nan'), float('-inf')]
)
def test_get_ds_string_invalid(float_val: float):
with pytest.raises(ValueError):
get_ds_string(float_val)