Skip to content

Instantly share code, notes, and snippets.

@rdebroiz
Last active April 16, 2018 19:38
Show Gist options
  • Save rdebroiz/03e5ed553035058d5b81ed3e29cb39e4 to your computer and use it in GitHub Desktop.
Save rdebroiz/03e5ed553035058d5b81ed3e29cb39e4 to your computer and use it in GitHub Desktop.
import hashlib
import os
import random
import uuid
from collections import UserString
import re
import warnings
from pydicom._uid_dict import UID_dictionary
# Many thanks to the Medical Connections for offering free
# valid UIDs (http://www.medicalconnections.co.uk/FreeUID.html)
# Their service was used to obtain the following root UID for pydicom:
PYDICOM_ROOT_UID = '1.2.826.0.1.3680043.8.498.'
PYDICOM_IMPLEMENTATION_UID = PYDICOM_ROOT_UID + '1'
# Regexes for valid UIDs and valid UID prefixes
RE_VALID_UID = r'^(0|[1-9][0-9]*)(\.(0|[1-9][0-9]*))*$'
RE_VALID_UID_PREFIX = r'^(0|[1-9][0-9]*)(\.(0|[1-9][0-9]*))*\.$'
class UserStringUID(UserString):
"""Subclass python UserString to have human-friendly UIDs.
Example
-------
>>> from user_string_uid import UserStringUID
>>> uid = UserStringUID('1.2.840.10008.1.2.4.50')
>>> uid
'JPEG Baseline (Process 1)'
>>> uid.is_implicit_VR
False
>>> uid.is_little_endian
True
>>> uid.is_transfer_syntax
True
>>> uid.name
'JPEG Baseline (Process 1)'
"""
# Should be just removed
def __eq__(self, other):
"""Return True if `self` or `self.name` equals `other`."""
# TODO: v1.2 - The __ne__ override is deprecated
if isinstance(other, str) and other and '.' not in other:
msg = "The equality test for \"UID == '{0}'\" is deprecated and " \
"will be removed in pydicom v1.2. In the future use " \
"\"UID.name == '{0}'\"".format(other)
warnings.warn(msg, DeprecationWarning)
return self.name == other
return super().__eq__(other)
# Should be just removed
def __ne__(self, other):
"""Return True if `self` or `self.name` does not equal `other`."""
# TODO: v1.2 - The __ne__ override is deprecated
if isinstance(other, str) and other and '.' not in other:
msg = "The equality test for \"UID != '{0}'\" is deprecated and " \
"will be removed in pydicom v1.2. In the future use " \
"\"UID.name != '{0}'\"".format(other)
warnings.warn(msg, DeprecationWarning)
return self.name != other
return super().__ne__(other)
def __repr__(self):
return "UserStringUID({})".format(repr(self.data))
def __str__(self):
if self.name:
return self.name
else:
return super().__str__()
@property
def name(self):
"""Return the UID name from the UID dictionary."""
try:
return UID_dictionary[self.data][0]
except KeyError:
return ""
@property
def type(self):
"""Return the UID type from the UID dictionary."""
try:
return UID_dictionary[self.data][1]
except KeyError:
return ""
@property
def info(self):
"""Return the UID info from the UID dictionary."""
try:
return UID_dictionary[self.data][2]
except KeyError:
return ""
@property
def is_retired(self):
"""Return True if the UID is retired, False otherwise or if private."""
try:
return bool(UID_dictionary[self.data][3])
except KeyError:
return False
@property
def is_valid(self):
"""Return True if the UID is retired, False otherwise or if private."""
return len(self.data) <= 64 and re.match(RE_VALID_UID, self.data)
@property
def is_private(self):
"""Return True if the UID isn't an officially registered DICOM UID."""
return not self.data[:13] == '1.2.840.10008'
@property
def is_transfer_syntax(self):
"""Return True if a transfer syntax UID."""
if not self.is_private:
return self.type == "Transfer Syntax"
raise ValueError("Can't determine UID type for private UIDs.")
@property
def is_deflated(self):
"""Return True if a deflated transfer syntax UID."""
if self.is_transfer_syntax:
# Deflated Explicit VR Little Endian
return self == '1.2.840.10008.1.2.1.99'
# Explicit VR Little Endian
# Implicit VR Little Endian
# Explicit VR Big Endian
# All encapsulated transfer syntaxes
raise ValueError('UID is not a transfer syntax.')
@property
def is_little_endian(self):
"""Return True if a little endian transfer syntax UID."""
if self.is_transfer_syntax:
# Explicit VR Big Endian
return not self == '1.2.840.10008.1.2.2'
# Explicit VR Little Endian
# Implicit VR Little Endian
# Deflated Explicit VR Little Endian
# All encapsulated transfer syntaxes
raise ValueError('UID is not a transfer syntax.')
@property
def is_implicit_VR(self):
"""Return True if an implicit VR transfer syntax UID."""
if self.is_transfer_syntax:
# Implicit VR Little Endian
if self == '1.2.840.10008.1.2':
return True
# Explicit VR Little Endian
# Explicit VR Big Endian
# Deflated Explicit VR Little Endian
# All encapsulated transfer syntaxes
return False
raise ValueError('UID is not a transfer syntax.')
@property
def is_compressed(self):
"""Return True if a compressed transfer syntax UID."""
if self.is_transfer_syntax:
# Explicit VR Little Endian
# Implicit VR Little Endian
# Explicit VR Big Endian
# Deflated Explicit VR Little Endian
if self in ['1.2.840.10008.1.2', '1.2.840.10008.1.2.1',
'1.2.840.10008.1.2.2', '1.2.840.10008.1.2.1.99']:
return False
# All encapsulated transfer syntaxes
return True
raise ValueError('UID is not a transfer syntax.')
@property
def is_encapsulated(self):
"""Return True if an encasulated transfer syntax UID."""
return self.is_compressed
# Pre-defined Transfer Syntax UIDs (for convenience)
ExplicitVRLittleEndian = UserStringUID('1.2.840.10008.1.2.1')
ImplicitVRLittleEndian = UserStringUID('1.2.840.10008.1.2')
DeflatedExplicitVRLittleEndian = UserStringUID('1.2.840.10008.1.2.1.99')
ExplicitVRBigEndian = UserStringUID('1.2.840.10008.1.2.2')
JPEGBaseLineLossy8bit = UserStringUID('1.2.840.10008.1.2.4.50')
JPEGBaseLineLossy12bit = UserStringUID('1.2.840.10008.1.2.4.51')
JPEGLossless = UserStringUID('1.2.840.10008.1.2.4.70')
JPEGLSLossless = UserStringUID('1.2.840.10008.1.2.4.80')
JPEGLSLossy = UserStringUID('1.2.840.10008.1.2.4.81')
JPEG2000Lossless = UserStringUID('1.2.840.10008.1.2.4.90')
JPEG2000Lossy = UserStringUID('1.2.840.10008.1.2.4.91')
RLELossless = UserStringUID('1.2.840.10008.1.2.5')
UncompressedPixelTransferSyntaxes = [
ExplicitVRLittleEndian,
ImplicitVRLittleEndian,
DeflatedExplicitVRLittleEndian,
ExplicitVRBigEndian,
]
JPEGLSSupportedCompressedPixelTransferSyntaxes = [
JPEGLSLossless,
JPEGLSLossy,
]
PILSupportedCompressedPixelTransferSyntaxes = [
JPEGBaseLineLossy8bit,
JPEGLossless,
JPEGBaseLineLossy12bit,
JPEG2000Lossless,
JPEG2000Lossy,
]
JPEG2000CompressedPixelTransferSyntaxes = [
JPEG2000Lossless,
JPEG2000Lossy,
]
JPEGLossyCompressedPixelTransferSyntaxes = [
JPEGBaseLineLossy8bit,
JPEGBaseLineLossy12bit,
]
RLECompressedLosslessSyntaxes = [
RLELossless
]
def generate_uid(prefix=PYDICOM_ROOT_UID, entropy_srcs=None):
"""Return a 64 character UID which starts with `prefix`.
Parameters
----------
prefix : str or None
The UID prefix to use when creating the UID. Default is the pydicom
root UID '1.2.826.0.1.3680043.8.498.'. If None then a value of '2.25.'
will be used (as described on `David Clunie's website
<http://www.dclunie.com/medical-image-faq/html/part2.html#UID>`_).
entropy_srcs : list of str or None
If a list of str, the prefix will be appended with a SHA512 hash of the
list which means the result is deterministic and should make the
original data unrecoverable. If None random data will be used
(default).
Returns
-------
pydicom.uid.UID
A 64 character DICOM UID.
Raises
------
ValueError
If `prefix` is invalid or greater than 63 characters.
Example
-------
>>> from pydicom.uid import generate_uid
>>> generate_uid()
1.2.826.0.1.3680043.8.498.22463838056059845879389038257786771680
>>> generate_uid(prefix=None)
2.25.12586835699909622925962004639368649121731805922235633382942
>>> generate_uid(entropy_srcs=['lorem', 'ipsum'])
1.2.826.0.1.3680043.8.498.87507166259346337659265156363895084463
>>> generate_uid(entropy_srcs=['lorem', 'ipsum'])
1.2.826.0.1.3680043.8.498.87507166259346337659265156363895084463
"""
max_uid_len = 64
if prefix is None:
prefix = '2.25.'
if len(prefix) > max_uid_len - 1:
raise ValueError("The prefix must be less than 63 chars")
if not re.match(RE_VALID_UID_PREFIX, prefix):
raise ValueError("The prefix is not in a valid format")
avail_digits = max_uid_len - len(prefix)
if entropy_srcs is None:
entropy_srcs = [
str(uuid.uuid1()), # 128-bit from MAC/time/randomness
str(os.getpid()), # Current process ID
hex(random.getrandbits(64)) # 64 bits randomness
]
hash_val = hashlib.sha512(''.join(entropy_srcs).encode('utf-8'))
# Convert this to an int with the maximum available digits
dicom_uid = prefix + str(int(hash_val.hexdigest(), 16))[:avail_digits]
return UserStringUID(dicom_uid)
UserStringUID.__hash__ = UserString.__hash__
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment