Last active
April 16, 2018 19:38
-
-
Save rdebroiz/03e5ed553035058d5b81ed3e29cb39e4 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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