297 lines
11 KiB
Python
297 lines
11 KiB
Python
import base64
|
|
from collections import defaultdict
|
|
import hashlib
|
|
import mimetypes
|
|
|
|
from io import BytesIO
|
|
|
|
from PIL import Image, ImageFile
|
|
from blake3 import blake3
|
|
|
|
import pydicom
|
|
from pydicom.errors import InvalidDicomError
|
|
import sys
|
|
import glob
|
|
|
|
import itertools
|
|
|
|
try:
|
|
from .pydicom_PIL import get_PIL_image
|
|
except:
|
|
from pydicom_PIL import get_PIL_image
|
|
|
|
def image_as_base64(image_file):
|
|
"""
|
|
:param `image_file` for the complete path of image.
|
|
:param `format` is format for image, eg: `png` or `jpg`.
|
|
"""
|
|
# if not os.path.isfile(image_file):
|
|
# return None
|
|
|
|
try:
|
|
encoded_string = ""
|
|
# with open(image_file, 'rb') as img_f:
|
|
# encoded_string = base64.b64encode(img_f.read())
|
|
encoded_string = base64.b64encode(image_file.file.read())
|
|
mimetype, enc = mimetypes.guess_type(image_file.path)
|
|
# Treat unknown files as dicom
|
|
if None == mimetype:
|
|
return "data:application/dicom;base64,{}".format(encoded_string.decode("utf-8"))
|
|
if "dicom" in mimetype or "octet-stream" in mimetype:
|
|
return "data:{};base64,{}".format(mimetype, encoded_string.decode("utf-8"))
|
|
return "data:image/{};base64,{}".format(mimetype, encoded_string.decode("utf-8"))
|
|
except FileNotFoundError:
|
|
return "data:image/png;base64,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"
|
|
|
|
|
|
|
|
|
|
def pil_dicom_image(source, exif_orientation=True, **options):
|
|
"""
|
|
Try to open the source file directly using PIL, ignoring any errors.
|
|
exif_orientation
|
|
If EXIF orientation data is present, perform any required reorientation
|
|
before passing the data along the processing pipeline.
|
|
"""
|
|
# Use a BytesIO wrapper because if the source is an incomplete file like
|
|
# object, PIL may have problems with it. For example, some image types
|
|
# require tell and seek methods that are not present on all storage
|
|
# File objects.
|
|
#if not source:
|
|
# return
|
|
#source = BytesIO(source.read())
|
|
|
|
# open file with pydicom
|
|
ds = pydicom.dcmread(source)
|
|
|
|
# return the image file
|
|
return get_PIL_image(ds)
|
|
|
|
#with Image.open(source) as image:
|
|
# # Fully load the image now to catch any problems with the image contents.
|
|
# try:
|
|
# ImageFile.LOAD_TRUNCATED_IMAGES = True
|
|
# image.load()
|
|
# finally:
|
|
# ImageFile.LOAD_TRUNCATED_IMAGES = False
|
|
|
|
#if exif_orientation:
|
|
# image = utils.exif_orientation(image)
|
|
|
|
|
|
|
|
def show_PIL(dataset):
|
|
"""Display an image using the Python Imaging Library (PIL)"""
|
|
im = get_PIL_image(dataset)
|
|
im.show()
|
|
|
|
|
|
|
|
|
|
def get_dicom_order(path):
|
|
# load the DICOM files
|
|
files = []
|
|
|
|
for fname in glob.glob(path, recursive=False):
|
|
print("loading: {}".format(fname))
|
|
files.append((fname, pydicom.dcmread(fname)))
|
|
|
|
print("file count: {}".format(len(files)))
|
|
|
|
# skip files with no SliceLocation (eg scout views)
|
|
slices = []
|
|
map = {}
|
|
skipcount = 0
|
|
for fname, f in files:
|
|
print(dir(f))
|
|
print(fname)
|
|
if hasattr(f, 'SliceLocation'):
|
|
slices.append(f)
|
|
map[f.SliceLocation] = fname
|
|
else:
|
|
skipcount = skipcount + 1
|
|
|
|
print("skipped, no SliceLocation: {}".format(skipcount))
|
|
|
|
# ensure they are in the correct order
|
|
slices = sorted(slices, key=lambda s: s.SliceLocation)
|
|
|
|
#print(slices)
|
|
for i in slices:
|
|
print(map[i.SliceLocation])
|
|
|
|
#get_dicom_order("/home/ross/Downloads/DICOM HEAD/STD4/SER1/*.dcm")
|
|
|
|
def pretty_print_dicom(dataset, indent=0):
|
|
l = print_dicom(dataset, indent)
|
|
print(l)
|
|
return "".join(l)
|
|
|
|
|
|
def print_dicom(dataset, indent=0, include_tag_ids=True, join=True, join_text="\n<br/>"):
|
|
"""Go through all items in the dataset and print them with custom format
|
|
|
|
Modelled after Dataset._pretty_str()
|
|
"""
|
|
dont_print = ['Pixel Data', 'File Meta Information Version']
|
|
|
|
indent_string = "---" * indent
|
|
next_indent_string = "---" * (indent + 1)
|
|
|
|
l = []
|
|
|
|
for data_element in dataset:
|
|
if data_element.VR == "SQ": # a sequence
|
|
l.append(f"{data_element.tag}: {indent_string}{data_element.name}")
|
|
for sequence_item in data_element.value:
|
|
l_ = print_dicom(sequence_item, indent + 1, join=join, join_text=join_text)
|
|
if join:
|
|
l.append(l_)
|
|
else:
|
|
l.extend(l_)
|
|
|
|
l.append(next_indent_string + "---------")
|
|
else:
|
|
if data_element.name in dont_print:
|
|
l.append("""item not printed -- in the "don't print" list""")
|
|
else:
|
|
repr_value = repr(data_element.value)
|
|
if len(repr_value) > 50:
|
|
repr_value = repr_value[:50] + "..."
|
|
l.append(f"{data_element.tag}: {indent_string:s} {data_element.name:s} = {repr_value:s}")
|
|
if not join:
|
|
return l
|
|
|
|
return join_text.join(l)
|
|
|
|
def get_image_dicom_hash(img, dataset=None, hash_type="md5", direct_pixel_data=True) -> (str, bool):
|
|
if dataset is None:
|
|
with pydicom.dcmread(img) as ds:
|
|
dataset = ds
|
|
|
|
# TODO: improve?
|
|
match hash_type:
|
|
case "md5":
|
|
hasher = hashlib.md5()
|
|
case "blake3":
|
|
hasher = blake3()
|
|
case _:
|
|
raise NotImplemented
|
|
|
|
if direct_pixel_data:
|
|
try:
|
|
hasher.update(dataset.PixelData)
|
|
except AttributeError as e:
|
|
print("Error getting pixel data hash from dataset")
|
|
print(dataset)
|
|
raise InvalidDicomError("Error getting pixel data hash from dataset")
|
|
|
|
else:
|
|
first = True
|
|
for i in dataset.pixel_array.astype(str).flatten():
|
|
if first:
|
|
first = False
|
|
hasher.update(f"{i}".encode())
|
|
else:
|
|
hasher.update(f",{i}".encode())
|
|
|
|
hash = hasher.hexdigest()
|
|
return hash
|
|
|
|
def get_image_hash(img, dataset=None, hash_type="blake3", direct_pixel_data=True) -> (str, bool):
|
|
is_dicom = False
|
|
# Try and read the file as a dicom
|
|
hash = "1234"
|
|
try:
|
|
# and generate a hash from the pixel data
|
|
hash = get_image_dicom_hash(img, dataset=dataset, hash_type=hash_type, direct_pixel_data=direct_pixel_data)
|
|
is_dicom = True
|
|
# ----
|
|
|
|
# Random error catching is bad
|
|
except (pydicom.errors.InvalidDicomError, TypeError) as e:
|
|
print("dicom error", img)
|
|
print(e)
|
|
try: # This is horrible (but needed for current unit tests)
|
|
# (we use a temporary file that breaks here)
|
|
img.file.open()
|
|
match hash_type:
|
|
case "md5":
|
|
hash = hashlib.md5(img.read()).hexdigest()
|
|
case "blake3":
|
|
hash = blake3(img.read()).hexdigest()
|
|
except AttributeError:
|
|
print("BAD")
|
|
return hash, False
|
|
|
|
return hash, is_dicom
|
|
|
|
|
|
def compare_dicom_elements(element1: pydicom.DataElement, element2: pydicom.DataElement):
|
|
dont_print = ['Pixel Data', 'File Meta Information Version']
|
|
|
|
if element1.name in dont_print:
|
|
return {}
|
|
|
|
differences = {}
|
|
|
|
vr_exists = False
|
|
|
|
# Compare the element tags
|
|
try:
|
|
if element1.tag != element2.tag:
|
|
differences['tag'] = (element1.tag, element2.tag)
|
|
except AttributeError: # private elements won't/might not? have a tag
|
|
pass
|
|
|
|
# Compare the VR (Value Representation)
|
|
try:
|
|
if element1.VR != element2.VR:
|
|
vr_exists = True
|
|
differences['VR'] = (element1.VR, element2.VR)
|
|
|
|
except AttributeError:
|
|
pass
|
|
|
|
# Compare the value
|
|
if element1.value != element2.value:
|
|
differences['value'] = (element1.value, element2.value)
|
|
|
|
# Recursively compare the child elements
|
|
if vr_exists and element1.VR == 'SQ' and element2.VR == 'SQ':
|
|
for i, (item1, item2) in enumerate(zip(element1.value, element2.value)):
|
|
item_differences = compare_dicom_elements(item1, item2)
|
|
if item_differences:
|
|
differences[f'item{i+1}'] = item_differences
|
|
|
|
return differences
|
|
|
|
def compare_dicom_datasets(*datasets: pydicom.Dataset):
|
|
differences = defaultdict(list)
|
|
|
|
# Compare the dataset elements
|
|
for i, dataset in enumerate(datasets):
|
|
for element1 in dataset:
|
|
for j in range(i+1, len(datasets)):
|
|
dataset2 = datasets[j]
|
|
element2 = dataset2.get(element1.tag)
|
|
if element2 is None:
|
|
differences[f"{element1.tag} {element1.name}"].append(('missing', element1.value))
|
|
else:
|
|
element_differences = compare_dicom_elements(element1, element2)
|
|
if element_differences:
|
|
differences[f"{element1.tag} {element1.name}"].append(element_differences)
|
|
|
|
# Check for any extra elements in the datasets
|
|
for i, dataset in enumerate(datasets):
|
|
for element2 in dataset:
|
|
for j in range(i+1, len(datasets)):
|
|
dataset2 = datasets[j]
|
|
element1 = dataset2.get(element2.tag)
|
|
if element1 is None:
|
|
differences[f"{element1.tag} {element1.name}"].append(('extra', element2.value))
|
|
|
|
return differences
|
|
|
|
|