Files
penracourses/helpers/images.py
T
Ross d891c7b9ee .
2025-01-06 11:14:34 +00:00

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