Files
penracourses/helpers/images.py
T
2025-07-29 09:20:58 +01:00

328 lines
12 KiB
Python

import base64
from collections import defaultdict
import hashlib
import mimetypes
from pydicom.uid import ExplicitVRLittleEndian
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
import numpy as np
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):
files.append((fname, pydicom.dcmread(fname)))
# skip files with no SliceLocation (eg scout views)
slices = []
map = {}
skipcount = 0
for fname, f in files:
if hasattr(f, 'SliceLocation'):
slices.append(f)
map[f.SliceLocation] = fname
else:
skipcount = skipcount + 1
# ensure they are in the correct order
slices = sorted(slices, key=lambda s: s.SliceLocation)
return slices
#get_dicom_order("/home/ross/Downloads/DICOM HEAD/STD4/SER1/*.dcm")
def pretty_print_dicom(dataset, indent=0):
l = print_dicom(dataset, indent)
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")
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
def combine_dicom_images_side_by_side(dicom_path1, dicom_path2):
"""
Combines two DICOM images side by side, using the metadata from the first image.
If the heights differ, pads the shorter image with zeros at the bottom.
Returns a new pydicom.Dataset with the combined image.
"""
ds1 = pydicom.dcmread(dicom_path1)
ds2 = pydicom.dcmread(dicom_path2)
arr1 = ds1.pixel_array
arr2 = ds2.pixel_array
h1, w1 = arr1.shape[:2]
h2, w2 = arr2.shape[:2]
# Pad arrays if heights differ
if h1 != h2:
max_h = max(h1, h2)
def pad(arr, target_h):
pad_height = target_h - arr.shape[0]
if pad_height > 0:
pad_shape = ((0, pad_height), (0, 0)) if arr.ndim == 2 else ((0, pad_height), (0, 0), (0, 0))
arr = np.pad(arr, pad_shape, mode='constant', constant_values=0)
return arr
arr1 = pad(arr1, max_h)
arr2 = pad(arr2, max_h)
combined = np.hstack((arr1, arr2))
ds1.PixelData = combined.tobytes()
ds1.Rows, ds1.Columns = combined.shape[:2]
if hasattr(ds1, 'PhotometricInterpretation'):
ds1.PhotometricInterpretation = ds1.PhotometricInterpretation
# Ensure output is uncompressed
ds1.file_meta.TransferSyntaxUID = ExplicitVRLittleEndian
return ds1