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
"): """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