From 23244319d9f2733f7571eaa103607c9fa639d3c9 Mon Sep 17 00:00:00 2001 From: mdevans Date: Tue, 12 Nov 2024 11:10:48 +0200 Subject: [PATCH] release-signing bump version RC6, contact+kaleidoscope sheet prototyping --- .github/workflows/build.yml | 2 +- animation.gif | Bin 0 -> 615 bytes src/__version__.py | 2 +- src/messages.pot | 950 ------------------ src/prototyping/contact_sheet_gif.py | 63 ++ .../remove_facial_features.py | 0 src/utils/storage.py | 11 +- src/view/contact_sheet.py | 97 +- 8 files changed, 163 insertions(+), 962 deletions(-) create mode 100644 animation.gif delete mode 100644 src/messages.pot create mode 100644 src/prototyping/contact_sheet_gif.py rename src/{controller => prototyping}/remove_facial_features.py (100%) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 565e76e..dfaae98 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -77,7 +77,7 @@ jobs: api-token: '${{ secrets.SIGNPATH_API_TOKEN }}' organization-id: '4e519f49-439f-43aa-8fc1-e1f19225e705' project-slug: 'anonymizer' - signing-policy-slug: 'test-signing' + signing-policy-slug: 'release-signing' github-artifact-id: '${{steps.upload-win-exe.outputs.artifact-id}}' wait-for-completion: true output-artifact-directory: 'src/dist/signed' diff --git a/animation.gif b/animation.gif new file mode 100644 index 0000000000000000000000000000000000000000..8d010af36abbdddebfb483f7150b1c7392f6b119 GIT binary patch literal 615 zcmZ?wbhEHbOkqf2Xk=jc|NlP&3@HBR_Hzvhc6JPKHPSO+W(0~W{$yb=0@6AlH6Sev z919s3Ib=LGEI8QAA*>a1V#C72?E=bPb38UKI@&E^oOS2K#>L0`6`Z?dJU1;lIawok zRm{mvOHWTXNIo^kbMvyZvn`5W-8s2=`T6+{&0MlxTUK0L>@is@_SBY@mzM`D_L}Rp zb=B3?5u3B_p4z(l`uc>!U9#TW*4*5jad}nj>1}IoZ!dU!YOeS8b$54He13KJ^!D}l y_ct(e%lYis@bGYluy)*;hyn+Ys~L?L7!j^+Y-nfz`2Y-trVn7Q2hl^p2i5?O6dMo# literal 0 HcmV?d00001 diff --git a/src/__version__.py b/src/__version__.py index a2ba587..6ae7fb0 100644 --- a/src/__version__.py +++ b/src/__version__.py @@ -1,3 +1,3 @@ # Major.Minor.Patch # As per (https://semver.org/spec/v2.0.0.html) -__version__ = "17.2.1-RC5" +__version__ = "17.2.1-RC6" diff --git a/src/messages.pot b/src/messages.pot deleted file mode 100644 index ab3671b..0000000 --- a/src/messages.pot +++ /dev/null @@ -1,950 +0,0 @@ -msgid "RSNA DICOM Anonymizer Version" -msgstr "" - -msgid "Error writing json config file: " -msgstr "" - -msgid "Configuration File Write Error" -msgstr "" - -msgid "New Project Settings" -msgstr "" - -msgid "New Project Error" -msgstr "" - -msgid "Storage Directory not set, please set a valid directory in project settings." -msgstr "" - -msgid "Confirm Overwrite" -msgstr "" - -msgid "The project directory already exists." -msgstr "" - -msgid "Do you want to delete the existing project and all its data?" -msgstr "" - -msgid "Error deleting existing project directory" -msgstr "" - -msgid "Fatal Internal Error, Project Controller not created" -msgstr "" - -msgid "Error creating Project Controller" -msgstr "" - -msgid "Select Anonymizer Storage Directory" -msgstr "" - -msgid "Corruption detected: Loaded model is not an instance of ProjectModel" -msgstr "" - -msgid "Open Project Error" -msgstr "" - -msgid "Error loading Project Model from data file" -msgstr "" - -msgid "Project File corrupted, missing version information." -msgstr "" - -msgid "Project language mismatch" -msgstr "" - -msgid "No Project file not found in" -msgstr "" - -msgid "Local DICOM Server Error" -msgstr "" - -msgid "Query Busy" -msgstr "" - -msgid "Query is busy, please wait for query to complete before closing project." -msgstr "" - -msgid "Export Busy" -msgstr "" - -msgid "Export is busy, please wait for export to complete before closing project." -msgstr "" - -msgid "Clone Project Error" -msgstr "" - -msgid "No project open to clone." -msgstr "" - -msgid "Select Directory for Cloned Project" -msgstr "" - -msgid "Cloned directory cannot be the same as the current project directory." -msgstr "" - -msgid "Edit Cloned Project Settings" -msgstr "" - -msgid "Select DICOM Files to Import & Anonymize" -msgstr "" - -msgid "Select DICOM Directory to Import & Anonymize" -msgstr "" - -msgid "Reading DICOMDIR Root directory" -msgstr "" - -msgid "Error reading DICOMDIR file" -msgstr "" - -msgid "Import Directory Error" -msgstr "" - -msgid "Reading filenames from" -msgstr "" - -msgid "No files found in" -msgstr "" - -msgid "filenames read from" -msgstr "" - -msgid "Do you want to initiate import?" -msgstr "" - -msgid "Import Directory" -msgstr "" - -msgid "Import Directory Cancelled" -msgstr "" - -msgid "Importing" -msgstr "" - -msgid "Files processed" -msgstr "" - -msgid "Query is busy, please wait for query to complete before changing settings." -msgstr "" - -msgid "Export is busy, please wait for export to complete before changing settings." -msgstr "" - -msgid "Project Settings" -msgstr "" - -msgid "New Project" -msgstr "" - -msgid "Open Project" -msgstr "" - -msgid "Open Recent" -msgstr "" - -msgid "Exit" -msgstr "" - -msgid "File" -msgstr "" - -msgid "Help" -msgstr "" - -msgid "Import Files" -msgstr "" - -msgid "Clone Project" -msgstr "" - -msgid "Close Project" -msgstr "" - -msgid "Project" -msgstr "" - -msgid "Settings" -msgstr "" - -msgid "Computed Radiography" -msgstr "" - -msgid "Digital X-Ray" -msgstr "" - -msgid "Intra-oral Radiography" -msgstr "" - -msgid "Mammography" -msgstr "" - -msgid "Computer Tomography" -msgstr "" - -msgid "Magnetic Resonance" -msgstr "" - -msgid "Ultrasound" -msgstr "" - -msgid "Positron Emission Tomography" -msgstr "" - -msgid "Nuclear Medicine" -msgstr "" - -msgid "Secondary Capture" -msgstr "" - -msgid "Structured Report" -msgstr "" - -msgid "Presentation State" -msgstr "" - -msgid "Encapsulated PDF" -msgstr "" - -msgid "Other" -msgstr "" - -msgid "Document" -msgstr "" - -msgid "Progress Dialog" -msgstr "" - -msgid "Please wait..." -msgstr "" - -msgid "Cancel" -msgstr "" - -msgid "Config file not found: " -msgstr "" - -msgid "Invalid_DICOM" -msgstr "" - -msgid "DICOM_Read_Error" -msgstr "" - -msgid "Missing_Attributes" -msgstr "" - -msgid "Invalid_Storage_Class" -msgstr "" - -msgid "Capture_PHI_Error" -msgstr "" - -msgid "Storage_Error" -msgstr "" - -msgid "Dataset missing required attributes" -msgstr "" - -msgid "DICOM C-STORE scp is already running on" -msgstr "" - -msgid "Failed to start DICOM C-STORE scp on" -msgstr "" - -msgid "Connection error to" -msgstr "" - -msgid "Connection timed out, was aborted, or received an invalid response" -msgstr "" - -msgid "Unexpected Authorisation Challenge" -msgstr "" - -msgid "Authentication Result & Access Token not in response" -msgstr "" - -msgid "IdToken not in Authentication Result" -msgstr "" - -msgid "AccessToken Token not in Authentication Result" -msgstr "" - -msgid "AWS Cognito IDP authorisation failed" -msgstr "" - -msgid "AWS Cognito Get User Attributes failed" -msgstr "" - -msgid "UserAttributes Token not in get_user response" -msgstr "" - -msgid "User Attribute 'sub' not in get_user response" -msgstr "" - -msgid "AWS Cognito-identity authorisation failed" -msgstr "" - -msgid "IdentityId Token not in response" -msgstr "" - -msgid "Unable to retrieve UID Hierarchy for reliable import operation via DICOM C-MOVE." -msgstr "" - -msgid "Server" -msgstr "" - -msgid "did not return the number of instances in a series." -msgstr "" - -msgid "Standard DICOM field (0020,1209) NumberOfSeriesRelatedInstances is missing in the query response." -msgstr "" - -msgid "Connection timed out or aborted moving study_uid" -msgstr "" - -msgid "IMAGE" -msgstr "" - -msgid "INSTANCE" -msgstr "" - -msgid "ANON-PatientID" -msgstr "" - -msgid "ANON-PatientName" -msgstr "" - -msgid "PHI-PatientName" -msgstr "" - -msgid "PHI-PatientID" -msgstr "" - -msgid "DateOffset" -msgstr "" - -msgid "PHI-StudyDate" -msgstr "" - -msgid "ANON-Accession" -msgstr "" - -msgid "PHI-Accession" -msgstr "" - -msgid "ANON-StudyInstanceUID" -msgstr "" - -msgid "PHI-StudyInstanceUID" -msgstr "" - -msgid "Number of Series" -msgstr "" - -msgid "Number of Instances" -msgstr "" - -msgid "MY_PROJECT" -msgstr "" - -msgid "Documents" -msgstr "" - -msgid "RSNA Anonymizer" -msgstr "" - -msgid "ANONYMIZER" -msgstr "" - -msgid "QUERY" -msgstr "" - -msgid "EXPORT" -msgstr "" - -msgid "private" -msgstr "" - -msgid "public" -msgstr "" - -msgid "phi_export" -msgstr "" - -msgid "quarantine" -msgstr "" - -msgid "Logging Levels" -msgstr "" - -msgid "Ok" -msgstr "" - -msgid "Warning" -msgstr "" - -msgid "Enabling debug mode in pydicom will cause PHI to be written to the log file." -msgstr "" - -msgid "Are you sure you want to enable pydicom debug mode?" -msgstr "" - -msgid "Transfer Syntax Name" -msgstr "" - -msgid "Transfer Syntax UID" -msgstr "" - -msgid "Select Transfer Syntaxes" -msgstr "" - -msgid "Select All" -msgstr "" - -msgid "Default" -msgstr "" - -msgid "DICOM Node" -msgstr "" - -msgid "No local IP addresses found." -msgstr "" - -msgid "Address" -msgstr "" - -msgid "Domain Name" -msgstr "" - -msgid "DNS Lookup" -msgstr "" - -msgid "IP Address" -msgstr "" - -msgid "Port" -msgstr "" - -msgid "AE Title" -msgstr "" - -msgid "Code" -msgstr "" - -msgid "Description" -msgstr "" - -msgid "Select Modalities" -msgstr "" - -msgid "Class Name" -msgstr "" - -msgid "Class UID" -msgstr "" - -msgid "Select Storage Classes" -msgstr "" - -msgid "From Modalities" -msgstr "" - -msgid "Network Timeouts in SECONDS" -msgstr "" - -msgid "TCP Connection" -msgstr "" - -msgid "DICOM Association Messages (ACSE)" -msgstr "" - -msgid "DICOM Service Element Messages (DIMSE)" -msgstr "" - -msgid "Network (Close Inactive Connection)" -msgstr "" - -msgid "AWS Cognito Credentials for Export to S3" -msgstr "" - -msgid "AWS Account ID" -msgstr "" - -msgid "Region Name" -msgstr "" - -msgid "Cognito Application Client ID" -msgstr "" - -msgid "Cognito User Pool ID" -msgstr "" - -msgid "Cognito Identity Pool ID" -msgstr "" - -msgid "S3 Bucket" -msgstr "" - -msgid "S3 Prefix" -msgstr "" - -msgid "Username" -msgstr "" - -msgid "Password" -msgstr "" - -msgid "Export to AWS" -msgstr "" - -msgid "Site ID" -msgstr "" - -msgid "Load JAVA Index File" -msgstr "" - -msgid "Project Name" -msgstr "" - -msgid "UID Root" -msgstr "" - -msgid "DICOM Servers" -msgstr "" - -msgid "Local Server" -msgstr "" - -msgid "Query Server" -msgstr "" - -msgid "Export Server" -msgstr "" - -msgid "AWS S3 Server" -msgstr "" - -msgid "AWS Cognito Credentials" -msgstr "" - -msgid "Network Timeouts" -msgstr "" - -msgid "Storage Directory" -msgstr "" - -msgid "Modalities" -msgstr "" - -msgid "Storage Classes" -msgstr "" - -msgid "Transfer Syntaxes" -msgstr "" - -msgid "Script File" -msgstr "" - -msgid "Set Logging Levels" -msgstr "" - -msgid "Create Project" -msgstr "" - -msgid "Update Project" -msgstr "" - -msgid "Select Storage Directory" -msgstr "" - -msgid "Anonymizer Script Files" -msgstr "" - -msgid "All Files" -msgstr "" - -msgid "Select Java Anonymizer Index File" -msgstr "" - -msgid "Excel Files" -msgstr "" - -msgid "Error reading Java Anonymizer Index File" -msgstr "" - -msgid "Load Java Anonymizer Index File Error" -msgstr "" - -msgid "No PHI data records found in:" -msgstr "" - -msgid "Java Index File Loaded" -msgstr "" - -msgid "Studies from Java Index loaded." -msgstr "" - -msgid "Site ID, UID Root will be inferred from the first PHI record." -msgstr "" - -msgid "Please enter your Project Name and configure all other settings below." -msgstr "" - -msgid "The Java Index data will be processed into the Python Anonymizer database when the project is created." -msgstr "" - -msgid "Importing Studies" -msgstr "" - -msgid "Importing Study" -msgstr "" - -msgid "Import from" -msgstr "" - -msgid "Retrieving Study Metadata at" -msgstr "" - -msgid "No studies to retrieve" -msgstr "" - -msgid "Close" -msgstr "" - -msgid "Error retrieving ANY Study Metadata" -msgstr "" - -msgid "Finished retrieving Study Metadata" -msgstr "" - -msgid "No instances to import" -msgstr "" - -msgid "at" -msgstr "" - -msgid "level" -msgstr "" - -msgid "of" -msgstr "" - -msgid "Images" -msgstr "" - -msgid "Import Finished" -msgstr "" - -msgid "Cancelling the move operation may not stop transfers from the remote server." -msgstr "" - -msgid "Are you sure you want to continue?" -msgstr "" - -msgid "Patient Name" -msgstr "" - -msgid "Anonymized ID" -msgstr "" - -msgid "Studies" -msgstr "" - -msgid "Series" -msgstr "" - -msgid "Date Time" -msgstr "" - -msgid "Images Sent" -msgstr "" - -msgid "Last Export Error" -msgstr "" - -msgid "Export" -msgstr "" - -msgid "Processing" -msgstr "" - -msgid "Patients" -msgstr "" - -msgid "Cancel Export" -msgstr "" - -msgid "Create Patient Lookup" -msgstr "" - -msgid "Refresh" -msgstr "" - -msgid "Clear Selection" -msgstr "" - -msgid "Error Creating PHI CSV File" -msgstr "" - -msgid "PHI CSV File Created" -msgstr "" - -msgid "PHI Lookup Data saved to" -msgstr "" - -msgid "Processed" -msgstr "" - -msgid "Export Error" -msgstr "" - -msgid "Failed to export" -msgstr "" - -msgid "patient(s)" -msgstr "" - -msgid "Connection Error" -msgstr "" - -msgid "Export Server Failed DICOM C-ECHO" -msgstr "" - -msgid "No patients selected for export." -msgstr "" - -msgid "Use SHIFT+Click and/or CMD/CTRL+Click to select multiple patients." -msgstr "" - -msgid "Cancel active export?" -msgstr "" - -msgid "Patient ID" -msgstr "" - -msgid "Date" -msgstr "" - -msgid "Study Description" -msgstr "" - -msgid "Accession No." -msgstr "" - -msgid "Imported" -msgstr "" - -msgid "Last Import Error" -msgstr "" - -msgid "StudyInstanceUID" -msgstr "" - -msgid "STUDY" -msgstr "" - -msgid "SERIES" -msgstr "" - -msgid "Query, Retrieve & Import Studies" -msgstr "" - -msgid "Accession No.(s)" -msgstr "" - -msgid "Study Date" -msgstr "" - -msgid "Modality" -msgstr "" - -msgid "Load Accession Numbers" -msgstr "" - -msgid "Show Imported Studies" -msgstr "" - -msgid "Query" -msgstr "" - -msgid "Cancel Query" -msgstr "" - -msgid "Found" -msgstr "" - -msgid "Studies Selected" -msgstr "" - -msgid "Move Level" -msgstr "" - -msgid "Import & Anonymize" -msgstr "" - -msgid "Select text or csv file with list of accession numbers to retrieve" -msgstr "" - -msgid "File Read Error" -msgstr "" - -msgid "Error reading Accession Number file" -msgstr "" - -msgid "Accession Numbers not found" -msgstr "" - -msgid "Accession Numbers not found were written to text file" -msgstr "" - -msgid "Query Server Failed DICOM C-ECHO" -msgstr "" - -msgid "Query via Accession Numbers" -msgstr "" - -msgid "Loaded" -msgstr "" - -msgid "Accession Numbers" -msgstr "" - -msgid "Proceed with Query?" -msgstr "" - -msgid "Query Criteria" -msgstr "" - -msgid "Enter at least one search criterion" -msgstr "" - -msgid " Studies" -msgstr "" - -msgid "Cancel active Query?" -msgstr "" - -msgid "Anonymizer Queue" -msgstr "" - -msgid "online" -msgstr "" - -msgid "Server Failed DICOM ECHO" -msgstr "" - -msgid "Check Project Settings" -msgstr "" - -msgid "Ensure the remote server is setup to allow the local server for echo and storage services." -msgstr "" - -msgid "offline" -msgstr "" - -msgid "Checking Query DICOM Server is online" -msgstr "" - -msgid "Waiting for AWS Authentication" -msgstr "" - -msgid "Checking Export DICOM Server is online" -msgstr "" - -msgid "AWS Authentication Failed" -msgstr "" - -msgid "Check Project Settings/AWS Cognito and ensure all parameters are correct." -msgstr "" - -msgid "AWS Authenticated" -msgstr "" - -msgid "Welcome" -msgstr "" - -msgid "The RSNA DICOM Anonymizer program is a free open-source tool for curating and de-identifying DICOM studies." -msgstr "" - -msgid "Easy to use, advanced DICOM expertise not required!" -msgstr "" - -msgid "Use it to ensure privacy by removing protected health information (PHI)." -msgstr "" - -msgid "Go to Help/Overview for a quick overview." -msgstr "" - -msgid "Go to Help/Project settings for instructions on how to configure the program." -msgstr "" - -msgid "Go to Help/Operation for instructions on how to use the program." -msgstr "" - -msgid "Select File/New Project to start." -msgstr "" - -msgid "Study Metadata" -msgstr "" - -msgid "Error" -msgstr "" - -msgid "Errors" -msgstr "" - -msgid "Study" -msgstr "" - -msgid "Anonymizer Workers Busy" -msgstr "" - -msgid "Anonymizer queues are not empty, please wait for workers to process files before closing project." -msgstr "" - -msgid "Project restart" -msgstr "" - -msgid "The settings change will take effect when the project is next opened." -msgstr "" - -msgid "Quarantined" -msgstr "" - -msgid "Missing Attributes" -msgstr "" - -msgid "Instance already stored" -msgstr "" - -msgid "Storage Class mismatch" -msgstr "" - -msgid "Contact sheet for" -msgstr "" - -msgid "selected patient(s)" -msgstr "" - -msgid "All incoming datasets, via network or file import, will also be written to the private subdirectory of storage folder." -msgstr "" - -msgid "Remove Pixel PHI" -msgstr "" - -msgid "Contact Sheet" -msgstr "" - -msgid "AccNos" -msgstr "" - -msgid "Quarantine" -msgstr "" - -msgid "Metadata Queue" -msgstr "" - -msgid "Pixel PHI Queue" -msgstr "" - -msgid "Use it to ensure privacy by removing protected identity & health information (PHI/PII) from both metadata and burnt into pixel data." -msgstr "" diff --git a/src/prototyping/contact_sheet_gif.py b/src/prototyping/contact_sheet_gif.py new file mode 100644 index 0000000..9e60401 --- /dev/null +++ b/src/prototyping/contact_sheet_gif.py @@ -0,0 +1,63 @@ +from tkinter import Tk, Canvas, PhotoImage +from PIL import Image, ImageSequence +import time + + +def create_gif(images, duration=500, loop=0): + """ + Creates an animated GIF from a list of PIL Image objects. + + Args: + images: A list of PIL Image objects. + duration: The duration of each frame in milliseconds. + loop: The number of times to loop the animation. 0 means infinite loop. + + Returns: + The created GIF image. + """ + + images[0].save("animation.gif", save_all=True, append_images=images[1:], duration=duration, loop=loop) + + return Image.open("animation.gif") + + +def main(): + # Create a list of PIL Image objects for the animation + images = [ + Image.new("RGB", (100, 100), "white"), + Image.new("RGB", (100, 100), "gray"), + Image.new("RGB", (100, 100), "black"), + ] + + # Create the animated GIF + gif_image = create_gif(images, duration=500) + + # Create the Tkinter window + root = Tk() + root.title("Animated GIF") + + # Create a canvas to display the GIF + canvas = Canvas(root, width=100, height=100) + canvas.pack() + + # Create a PhotoImage object to display the GIF + photo = PhotoImage(file="animation.gif") + canvas.create_image(0, 0, image=photo, anchor="nw") + + # Start the animation + def update(): + try: + frame = next(ImageSequence.Iterator(gif_image)) + photo.put(frame.getdata(), (0, 0)) + root.after(500, update) # Schedule next update + except StopIteration: + # Handle reaching the end of the animation + pass + + update() + + root.mainloop() + + +if __name__ == "__main__": + main() diff --git a/src/controller/remove_facial_features.py b/src/prototyping/remove_facial_features.py similarity index 100% rename from src/controller/remove_facial_features.py rename to src/prototyping/remove_facial_features.py diff --git a/src/utils/storage.py b/src/utils/storage.py index 660df3e..64d7d27 100644 --- a/src/utils/storage.py +++ b/src/utils/storage.py @@ -49,22 +49,19 @@ def count_studies_series_images(patient_path: str) -> tuple[int, int, int]: return study_count, series_count, image_count -def patient_dcm_files(patient_path: str) -> list[Path]: +def get_dcm_files(root_path: str) -> list[Path]: """ - Retrieves paths for each dicom file for a patient + Retrieves paths of each dicom file from a root path which could be at patient, study or series level Args: - patient_path (str): The path to the patient directory. + root_path (str): The root path to start the search for dicom files. Returns: List of Path objects """ return [ - Path(root) / file - for root, _, files in os.walk(patient_path) - for file in files - if file.endswith(DICOM_FILE_SUFFIX) + Path(root) / file for root, _, files in os.walk(root_path) for file in files if file.endswith(DICOM_FILE_SUFFIX) ] diff --git a/src/view/contact_sheet.py b/src/view/contact_sheet.py index 61e14f1..9dc2b59 100644 --- a/src/view/contact_sheet.py +++ b/src/view/contact_sheet.py @@ -6,8 +6,10 @@ from PIL import Image from cv2 import normalize, NORM_MINMAX, equalizeHist import customtkinter as ctk -from utils.storage import patient_dcm_files +from utils.storage import get_dcm_files from utils.translate import _ +from io import BytesIO +import tempfile logger = logging.getLogger(__name__) @@ -27,7 +29,7 @@ def __init__(self, patient_ids, base_dir): self._frame.pack(fill="both", expand=True) self._columns = max(800 // self.THUMBNAIL_SIZE[0], 1) logger.info(f"ContactSheet for Patients={len(patient_ids)}, Columns={self._columns}") - self._load_patient_image_frames(patient_ids) + self._load_kaleidoscopes(patient_ids) def _on_image_click(self, event, dcm_path, frame_number): logger.info(f"Image clicked: {dcm_path}, Frame: {frame_number}") @@ -38,7 +40,7 @@ def _load_patient_image_frames(self, patient_ids): # TODO: work out an efficient way to sort according to series.InstanceNumber # (perhaps add instance number to end or beginning of instance filename?) dcm_paths = sorted( - dcm_path for patient_id in patient_ids for dcm_path in patient_dcm_files(Path(self._base_dir / patient_id)) + dcm_path for patient_id in patient_ids for dcm_path in get_dcm_files(Path(self._base_dir / patient_id)) ) logger.info(f"load images frames from {len(dcm_paths)} dicom file(s)") @@ -86,3 +88,92 @@ def _load_patient_image_frames(self, patient_ids): "", # Left mouse button click event lambda event, path=dcm_path, frame_number=frame: self._on_image_click(event, path, frame_number), ) + + def _load_kaleidoscopes(self, patient_ids): + + total_thumbnails = 0 + + # Process patients sequentially & display each series belonging to patient as a kaleidoscope: + for patient_id in patient_ids: + + patient_path = Path(self._base_dir / patient_id) + + for study_path in patient_path.iterdir(): + if study_path.is_dir(): + for series_path in study_path.iterdir(): + if series_path.is_dir(): + dcm_paths = sorted(get_dcm_files(series_path)) + + logger.info(f"Processing series in {series_path} with {len(dcm_paths)} DICOM file(s)") + + all_pixels = [] + + for dcm_path in dcm_paths: + ds = pydicom.dcmread(dcm_path) + pixels = ds.pixel_array + pi = ds.get("PhotometricInterpretation", None) + if pi is None: + continue + + if pi in ["MONOCHROME1", "MONOCHROME2"]: + pixels = apply_voi_lut(apply_modality_lut(pixels, ds), ds) + normalize( + src=pixels, + dst=pixels, + alpha=0, + beta=255, + norm_type=NORM_MINMAX, + dtype=-1, + mask=None, + ) + pixels = pixels.astype(np.uint8) + if pi == "MONOCHROME1": + pixels = np.invert(pixels) + + no_of_frames = ds.get("NumberOfFrames", 1) + if no_of_frames == 1: + all_pixels.append(pixels) + else: + for frame in range(no_of_frames): + all_pixels.append(pixels[frame]) + + # Create low, mid, and high contrast images + if all_pixels: + combined_image = np.mean(all_pixels, axis=0).astype(np.uint8) + low_contrast = np.clip(combined_image * 0.5, 0, 255).astype(np.uint8) + mid_contrast = combined_image + high_contrast = np.clip(combined_image * 1.5, 0, 255).astype(np.uint8) + + # Create a rotating thumbnail + images = [low_contrast, mid_contrast, high_contrast] + thumbnails = [ + Image.fromarray(img) + .convert("RGB") + .resize(tuple(dim * 2 for dim in self.THUMBNAIL_SIZE), Image.Resampling.NEAREST) + for img in images + ] + + # Create and display CTkLabel that rotates images every 0.5 seconds + ctk_image = ctk.CTkImage(light_image=thumbnails[0], size=self.THUMBNAIL_SIZE) + label = ctk.CTkLabel(self._frame, image=ctk_image, text="") + row = int(total_thumbnails // self._columns) + col = int(total_thumbnails % self._columns) + label.grid(row=row, column=col, padx=1, pady=1, sticky="nsew") + total_thumbnails += 1 + + # Add rotation logic + def rotate_images(): + index = 0 + while True: + ctk_image.configure(light_image=thumbnails[index]) + self._frame.update_idletasks() + self._frame.after(500) # Wait for 0.5 seconds + index = (index + 1) % len(thumbnails) + + label.bind( + "", + lambda event, path=series_path, frame_number=0: self._on_image_click( + event, path, frame_number + ), + ) + self._frame.after(0, rotate_images)