From c7523240923bb134649e669c34d2caaf088e6557 Mon Sep 17 00:00:00 2001 From: sophie22 Date: Thu, 17 Oct 2024 14:49:04 +0100 Subject: [PATCH 1/2] example files for MAP building with required fields and instructions --- project/app.py | 126 +++++ project/map/__init__.py | 0 project/map/app_config.json | 27 + project/map/operators/__init__.py | 0 project/map/operators/example_classifier.py | 117 +++++ project/map/operators/example_detector.py | 82 +++ project/map/operators/generate_pdf.py | 539 ++++++++++++++++++++ project/map/operators/images/cscLogo.png | Bin 0 -> 7091 bytes project/map/operators/images/gsttLogo.png | Bin 0 -> 7867 bytes 9 files changed, 891 insertions(+) create mode 100644 project/app.py create mode 100644 project/map/__init__.py create mode 100644 project/map/app_config.json create mode 100644 project/map/operators/__init__.py create mode 100644 project/map/operators/example_classifier.py create mode 100644 project/map/operators/example_detector.py create mode 100644 project/map/operators/generate_pdf.py create mode 100644 project/map/operators/images/cscLogo.png create mode 100644 project/map/operators/images/gsttLogo.png diff --git a/project/app.py b/project/app.py new file mode 100644 index 0000000..cb2a644 --- /dev/null +++ b/project/app.py @@ -0,0 +1,126 @@ +""" +This file contains the application class for MAP building. +""" + +import monai.deploy.core as md +import logging +from monai.deploy.core import Application +from monai.deploy.operators.dicom_utils import ModelInfo +from monai.deploy.operators import ( + DICOMEncapsulatedPDFWriterOperator, + DICOMSeriesSelectorOperator, +) +from monai.deploy.operators import DICOMDataLoaderOperator + +# TODO: Import project-specific operators from map.operators, eg + +from map.operators.example_detector import DetectorOperator +from map.operators.example_classifier import ClassifierOperator +from map.operators.generate_pdf import GeneratePDFOperator + +import os +import json +from pathlib import Path + +os.environ["root_dir"] = os.path.dirname(os.path.realpath(__file__)) + +# These files are expected to exist and correctly filled in +config = json.load(open(Path(__file__).resolve().parent / "map" / "app_config.json")) +requirements_file = Path(__file__).resolve().parent / "map" / "app_requirements.txt" + + +@md.resource( + cpu=config["resources"]["cpu"], + gpu=config["resources"]["gpu"], + memory=config["resources"]["memory"], +) +@md.env(pip_packages=requirements_file.as_posix()) +class myApplication(Application): + """Classifies the given image and returns the class name.""" + + def __init__(self, *args, **kwargs): + """Creates an application instance.""" + self._logger = logging.getLogger("{}.{}".format(__name__, type(self).__name__)) + super().__init__(*args, **kwargs) + + def run(self, *args, **kwargs): + # This method calls the base class to run. Can be omitted if simply calling through. + self._logger.debug(f"Begin {self.run.__name__}") + super().run(*args, **kwargs) + self._logger.debug(f"End {self.run.__name__}") + + def compose(self): + """Creates the app specific operators and chain them up in the processing DAG.""" + + self._logger.debug(f"Begin {self.compose.__name__}") + + app_directory = os.path.dirname(os.path.realpath(__file__)) + model_info = ModelInfo(creator="", name="", version="", uid="") + + # TODO: Define your models and optionally additional configs + # Model weights are the output of the training run saved as .pth + detector_model = app_directory + "/map/operators/models/detector_version3.pth" + mmdet_config = app_directory + "/detector/mmdetector.py" + classifier_model = ( + app_directory + "/map/operators/models/classifier_version2.pth" + ) + + # Build operators: + loader = DICOMDataLoaderOperator() + selector = DICOMSeriesSelectorOperator( + rules=json.dumps(config["input"]["selection_rules"]), all_matched=True + ) + + # TODO: Define project-sepcific operators, eg + example_detector = DetectorOperator( + detector_model=detector_model, mmdet_config=mmdet_config + ) + example_classifier = ClassifierOperator(classifier_model=classifier_model) + generate_pdf = GeneratePDFOperator(detector_model=detector_model) + + dicom_encapsulation = DICOMEncapsulatedPDFWriterOperator( + copy_tags=True, model_info=model_info + ) + + # Create flow - stringing together the operators, specifying inputs and outputs + # TODO: update flow as appropriate with project specific operators + self.add_flow(loader, selector, {"dicom_study_list": "dicom_study_list"}) + self.add_flow( + selector, + example_detector, + {"study_selected_series_list": "study_selected_series_list"}, + ) + + # EXAMPLE Classifier requires 2 inputs + self.add_flow( + selector, + example_classifier, + {"study_selected_series_list": "study_selected_series_list"}, + ) + self.add_flow( + example_detector, example_classifier, {"bbox_array": "bbox_array"} + ) + + # EXAMPLE PDF Generator requires 2 inputs + self.add_flow( + selector, + generate_pdf, + {"study_selected_series_list": "study_selected_series_list"}, + ) + self.add_flow( + example_classifier, generate_pdf, {"output_udict": "output_udict"} + ) + + # EXAMPLE dicom_encapsulation requires 2 inputs + self.add_flow( + selector, + dicom_encapsulation, + {"study_selected_series_list": "study_selected_series_list"}, + ) + self.add_flow(generate_pdf, dicom_encapsulation, {"pdf_file": "pdf_file"}) + + self._logger.debug(f"End {self.compose.__name__}") + + +if __name__ == "__main__": + myApplication(do_run=True) diff --git a/project/map/__init__.py b/project/map/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/project/map/app_config.json b/project/map/app_config.json new file mode 100644 index 0000000..cf731de --- /dev/null +++ b/project/map/app_config.json @@ -0,0 +1,27 @@ +{ + "info": { + "name": "myApp name", + "intended use": "Image classification.", + "application version": "0.0.1", + "detector algorithm version": "1", + "classifier algorithm version": "1", + "manufacturer": "Clinical Scientific Computing, Guy's and St Thomas' NHS Foundation Trust" + }, + "input": { + "selection_rules": { + "selections": [ + { + "name": "Example conditions", + "conditions": { + "Modality": "CR" + } + } + ] + } + }, + "resources": { + "cpu": 1, + "memory": "4Gi", + "gpu": 0 + } +} \ No newline at end of file diff --git a/project/map/operators/__init__.py b/project/map/operators/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/project/map/operators/example_classifier.py b/project/map/operators/example_classifier.py new file mode 100644 index 0000000..b533444 --- /dev/null +++ b/project/map/operators/example_classifier.py @@ -0,0 +1,117 @@ +"""This is an example operator to implement an image classifier""" + +import monai.deploy.core as md +import numpy as np +from monai.deploy.core.domain.dicom_series_selection import StudySelectedSeries +from typing import List, Optional +from collections import UserDict +from monai.deploy.core import ( + ExecutionContext, + InputContext, + IOType, + Operator, + OutputContext, + DataPath, +) + +from monai.data import DataLoader, Dataset +from monai.transforms import Compose +from transforms import load_and_norm, crop_and_output +import torch +import datetime + + +@md.input("study_selected_series_list", List[StudySelectedSeries], IOType.IN_MEMORY) +@md.input("bbox_array", np.ndarray, IOType.DISK) +@md.output("output_udict", UserDict, IOType.DISK) +@md.env(pip_packages=["monai"]) +class ClassifierOperator(Operator): + """Classifies the given image and returns the class name.""" + + def __init__(self, classifier_model: Optional[str] = "", *args, **kwargs): + super().__init__(*args, **kwargs) + self.classifier_model = classifier_model + + def compute( + self, + op_input: InputContext, + op_output: OutputContext, + context: ExecutionContext, + ): + + # get image from list + study_selected_series_list = op_input.get("study_selected_series_list") + + if not study_selected_series_list or len(study_selected_series_list) < 1: + raise ValueError("Missing expected input 'study_selected_series_list'") + + selected_series = study_selected_series_list[0].selected_series[0].series + dicom_list = [sop._sop.filename for sop in selected_series._sop_instances] + + transforms = Compose(load_and_norm() + crop_and_output()) + + bbox_array = op_input.get("bbox_array").tolist()[0] + + if bbox_array[-1] < 0.9: + # Update as appropriate to given project + output_dict = { + "result": "Indeterminate result", + "error_message": "Unable to identify the scaphoid bone", + "advice": "ScaphX is unable to provide a reliable assessment for this case. Please proceed with standard clinical evaluation, including X-ray interpretation and additional imaging as needed.", + "start_time": f"{begin}", + "end_time": f"{end}", + "bounding_box": bbox_array, + } + else: + dataset = Dataset( + data=[{"image": dicom_list, "label": -1, "roi": bbox_array}], + transform=transforms, + ) + + dataloader = DataLoader(dataset, batch_size=1, shuffle=False) + + model = torch.jit.load(self.classifier_model) + model.eval() + + begin = str(datetime.datetime.now()) + + with torch.inference_mode(): + for d in dataloader: + pred = model(d["image"].to("cpu")) + confidence = torch.nn.functional.softmax(pred, dim=1) + + result = confidence[0].detach().numpy()[0] + + end = str(datetime.datetime.now()) + + # Update as appropriate to specific project + if result >= 0.9: + output_dict = { + "result": "Scaphoid Fracture Detected", + "error_message": "None", + "advice": "Apply a below elbow POP backslab and refer the patient to fracture clinic as per scaphoid pathway.", + "start_time": f"{begin}", + "end_time": f"{end}", + "bounding_box": bbox_array, + } + elif result >= 0.9: # and result < 0.8: + output_dict = { + "result": "Indeterminate result", + "error_message": "Unable to classify image", + "advice": "ScaphX is unable to provide a reliable assessment for this case. Please proceed with standard clinical evaluation, including X-ray interpretation and additional imaging as needed.", + "start_time": f"{begin}", + "end_time": f"{end}", + "bounding_box": bbox_array, + } + else: + output_dict = { + "result": "No Scaphoid Fracture Detected", + "error_message": "None", + "advice": "Provide patient with Futura splint and refer the patient as per scaphoid pathway.", + "start_time": f"{begin}", + "end_time": f"{end}", + "bounding_box": bbox_array, + } + + output_udict = UserDict(output_dict) + op_output.set(output_udict, "output_udict") diff --git a/project/map/operators/example_detector.py b/project/map/operators/example_detector.py new file mode 100644 index 0000000..2384da0 --- /dev/null +++ b/project/map/operators/example_detector.py @@ -0,0 +1,82 @@ +"""This is an example operator to implement an image segmentator""" + +from typing import List, Optional +import numpy as np +import torch +import monai.deploy.core as md +from monai.deploy.core.domain.dicom_series_selection import StudySelectedSeries + +from monai.deploy.core import ( + ExecutionContext, + InputContext, + IOType, + Operator, + OutputContext, +) + +from monai.data import Dataset +from monai.transforms import Compose +from transforms import load_and_norm + +from torch.utils.data import DataLoader +from transforms.Detector import Detectord + + +@md.input("study_selected_series_list", List[StudySelectedSeries], IOType.IN_MEMORY) +@md.output("bbox_array", np.ndarray, IOType.DISK) +class DetectorOperator(Operator): + """Apply detector model on the selected image and return bbox coordinates.""" + + def __init__( + self, + detector_model: Optional[str] = "", + mmdet_config: Optional[str] = "", + *args, + **kwargs + ): + super().__init__(*args, **kwargs) + self.detector_model = detector_model + self.mmdet_config = mmdet_config + + def compute( + self, + op_input: InputContext, + op_output: OutputContext, + context: ExecutionContext, + ): + """ + Applies transforms to pixel data necessary for use of detector. + Set operator output to bbox coordinates + """ + + # get image from list + study_selected_series_list = op_input.get("study_selected_series_list") + + if not study_selected_series_list or len(study_selected_series_list) < 1: + raise ValueError("Missing expected input 'study_selected_series_list'") + + selected_series = study_selected_series_list[0].selected_series[0].series + dicom_list = [sop._sop.filename for sop in selected_series._sop_instances] + + transforms = Compose( + load_and_norm() + + [ + Detectord( + keys=["image"], + config_file=self.mmdet_config, + checkpoint_file=self.detector_model, + device=torch.device("cpu"), + ) + ] + ) + + dataset = Dataset( + data=[{"image": dicom_list, "label": -1}], + transform=transforms, + ) + + dataloader = DataLoader(dataset, batch_size=1, shuffle=False) + for d in dataloader: + bbox = d["roi"].numpy() + + op_output.set(bbox, "bbox_array") diff --git a/project/map/operators/generate_pdf.py b/project/map/operators/generate_pdf.py new file mode 100644 index 0000000..2f8fddd --- /dev/null +++ b/project/map/operators/generate_pdf.py @@ -0,0 +1,539 @@ +"""This is an example operator to implement PDF generator as a way to visualise segmentation and classification result overlaid on a DICOM image""" + +import os.path +import matplotlib.pyplot as plt +import matplotlib.patches as patches +import monai.deploy.core as md +from monai.deploy.core import ( + DataPath, + ExecutionContext, + InputContext, + IOType, + Operator, + OutputContext, +) +from monai.deploy.core.domain.dicom_series_selection import StudySelectedSeries + +import PIL as pil +from collections import UserDict +from typing import List +import reportlab.platypus as pl +from reportlab.platypus import Image +from reportlab.lib.pagesizes import A4 +from reportlab.lib.colors import toColor +from reportlab.platypus import Frame, Paragraph +from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle + + +from reportlab.lib.fonts import tt2ps +from reportlab.rl_config import canvas_basefontname as _baseFontName + +_baseFontNameB = tt2ps(_baseFontName, 1, 0) +_baseFontNameI = tt2ps(_baseFontName, 0, 1) +_baseFontNameBI = tt2ps(_baseFontName, 1, 1) + +import os +from reportlab.lib.pagesizes import A4 +from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet +from reportlab.pdfgen.canvas import Canvas +from reportlab.platypus import Frame, Image, Paragraph + + +@md.input("study_selected_series_list", List[StudySelectedSeries], IOType.IN_MEMORY) +@md.input("output_udict", UserDict, IOType.DISK) +@md.output("pdf_file", DataPath, IOType.DISK) +@md.env(pip_packages=["monai"]) +class GeneratePDFOperator(Operator): + """ + Creates a PDF of the results from the image classifier. + Results are input for the DICOM PDF Encapsulator + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def compute( + self, + op_input: InputContext, + op_output: OutputContext, + context: ExecutionContext, + ): + """ + Reads in original selected_series, and results from classifier, to generate a PDF report. + Sets operator output to path of generated dicom. + + """ + + # Read inputs + original_image = op_input.get("study_selected_series_list") + if not original_image or len(original_image) < 1: + raise ValueError("Missing input, list of 'StudySelectedSeries'.") + + # Extract selected image sop_instance + study_selected_series = original_image[0] + selected_series = study_selected_series.selected_series + sop_instance = ( + selected_series[0].series.get_sop_instances()[0].get_native_sop_instance() + ) + + # Read classifier results + result_dict = op_input.get("output_udict") + if not result_dict or len(result_dict) < 3: + raise ValueError("Missing input, Dict of results.") + + # Get output directory + output_folder = op_output.get_group_path() + + # Return pdf of results + pdf_path = self.generate_report_pdf(sop_instance, result_dict, output_folder) + + op_output.set(pdf_path, "pdf_file") + + # Generates the PDF + def generate_report_pdf(self, sop_instance, results_dict, output_folder): + + pdf_filename = "report_pdf.pdf" + + canv = Canvas(pdf_filename, pagesize=A4) + + # get dicom data from study + ds_meta = sop_instance + patient_name = str(ds_meta["PatientName"].value) + # patient_name = "John DOnt + dob = ds_meta["PatientBirthDate"].value + # dob = "01/01/2022" + pat_id = ds_meta["PatientID"].value + # pat_id = "1234567A" + # gender = ds_meta['PatientSex'].value + # consultant = ds_meta['ReferringPhysicianName'].value + study_description = ds_meta["StudyDescription"].value + # series_description = ds_meta['SeriesDescription'].value + # series_uid = ds_meta['SeriesInstanceUID'].value + accession_number = ds_meta["AccessionNumber"].value + xray_date = ds_meta["SeriesDate"].value + + if ds_meta["StudyTime"].value == "": + xray_time = "Time not available" + else: + xray_time = ds_meta["StudyTime"].value + + # protocol_name = ds_meta['ProtocolName'] + + # Get application data + result = results_dict["result"] + advice = results_dict["advice"] + error_message = results_dict["error_message"] + + b_box = results_dict["bounding_box"] + # list of [x1, y1, x2, y2, confidence] + + # reformat time + reversed_date = ( + xray_date[6:] + "/" + xray_date[4] + xray_date[5] + "/" + xray_date[:4] + ) + reversed_dob = dob[6:] + "/" + dob[4] + dob[5] + "/" + dob[:4] + xray_date_and_time = str(reversed_date) + " " + str(xray_time) + + # set variables for sizes of things + a4_height = A4[1] + a4_width = A4[0] + side_margins = 10 + banner_height = 60 + banner_frames_y = a4_height - banner_height + description_height = 25 + patient_name_frame_height = 120 + patient_dob_frame_height = 60 + patient_info_width = a4_width * 0.48 + study_frame_height = 60 + xray_frame_height = a4_height * 0.45 + xray_frame_width = (a4_width * 0.5) - side_margins + disclaimer_frame_height = a4_height / 8 + + # set text styles + styles = getSampleStyleSheet() + style_h1 = styles["Heading1"] + n_style = ParagraphStyle( + name="nameStyle", + parent=styles["Normal"], + fontName="Helvetica-Bold", + fontSize=30, + textColor="#bdd7ee", + wordWrap="LTR", + leading=24 * 1.2, + ) + pt_style = ParagraphStyle( + name="patientStyle", + parent=styles["Normal"], + fontName="Helvetica-Bold", + fontSize=24, + textColor="#bdd7ee", + leading=24, + ) + study_details_style = ParagraphStyle( + name="studyDetailsStyle", + parent=styles["Normal"], + fontName="Helvetica", + fontSize=20, + textColor="#bdd7ee", + leading=20, + ) + model_description_style = ParagraphStyle( + name="modelStyle", + parent=styles["Title"], + fontName="Helvetica", + fontSize=12, + textColor="#bdd7ee", + leading=0, + ) + disclaimer_style = ParagraphStyle( + name="disclaimerStyle", + parent=styles["Title"], + fontName="Helvetica-Bold", + fontSize=18, + textColor="#ffc000", + ) + misc_style = ParagraphStyle( + name="miscStyle", + parent=styles["Normal"], + fontName="Helvetica", + fontSize=12, + textColor="#bdd7ee", + # leading = 14, + spaceAfter=8, + ) + # changing the patientNameFrameHeight/text size based on name length + pt_name_length = len(patient_name) + + if pt_name_length <= 18: + patient_name_frame_height = 40 + patient_name = [Paragraph(patient_name, style=n_style)] + elif pt_name_length <= 37: + patient_name_frame_height = 80 + patient_name = [Paragraph(patient_name, style=n_style)] + elif pt_name_length <= 47: + patient_name_frame_height = 90 + patient_name = [Paragraph(patient_name, style=pt_style)] + else: + patient_name_frame_height = 100 + patient_name = [Paragraph(patient_name, style=pt_style)] + + # set banner frames (height of page 842 and width 595 in pixels) + banner_frame = ColorFrame( + 1, + banner_frames_y, + 198, + banner_height, + showBoundary=0, + background="white", + bottomPadding=1, + topPadding=1, + ) + + banner_frame_2 = ColorFrame( + 198, + banner_frames_y, + 200, + banner_height, + showBoundary=0, + background="white", + bottomPadding=0, + topPadding=10, + ) + + banner_frame_3 = ColorFrame( + 397, + banner_frames_y, + 198, + banner_height, + showBoundary=0, + background="white", + bottomPadding=1, + topPadding=1, + rightPadding=10, + ) + + description_frame = Frame( + side_margins, + (banner_frames_y - 25), + (a4_width - (side_margins * 2)), + description_height, + showBoundary=0, + ) + + # set results colorFrame + result_frame = ColorFrame( + side_margins, + a4_height / 4, + a4_width - (side_margins * 2), + a4_height / 6, + showBoundary=1, + background="#bdd7ee", + ) + + # set patient details and X-ray image Frames + patient_name_frame = Frame( + side_margins, + (banner_frames_y - (patient_name_frame_height + description_height)), + patient_info_width, + (patient_name_frame_height), + showBoundary=0, + topPadding=0, + leftPadding=0, + rightPadding=0, + bottomPadding=0, + ) + + patient_dob_frame = Frame( + side_margins, + ( + banner_frames_y + - ( + patient_name_frame_height + + description_height + + patient_dob_frame_height + ) + ), + patient_info_width, + patient_dob_frame_height, + showBoundary=0, + leftPadding=0, + ) + + patient_id_frame = Frame( + side_margins, + banner_frames_y + - ( + patient_dob_frame_height + + description_height + + (patient_dob_frame_height * 2) + ), + patient_info_width, + patient_dob_frame_height, + showBoundary=0, + leftPadding=0, + ) + + study_des_frame = Frame( + side_margins, + banner_frames_y + + (study_frame_height * 2) + - (xray_frame_height + description_height), + patient_info_width, + study_frame_height, + showBoundary=0, + leftPadding=0, + ) + + study_day_frame = Frame( + side_margins, + ( + banner_frames_y + + study_frame_height + - (xray_frame_height + description_height) + ), + patient_info_width, + study_frame_height, + showBoundary=0, + leftPadding=0, + ) + + study_id_frame = Frame( + side_margins, + (banner_frames_y - (xray_frame_height + description_height)), + patient_info_width, + study_frame_height, + showBoundary=0, + leftPadding=0, + ) + + xray_frame = Frame( + a4_width * 0.5, + (banner_frames_y - (xray_frame_height + description_height)), + (xray_frame_width), + xray_frame_height, + showBoundary=0, + rightPadding=0, + leftPadding=0, + topPadding=0, + bottomPadding=0, + ) + + # set disclaimer and misc frames + disclaimer_frame = Frame( + side_margins, + (a4_height / 4 - disclaimer_frame_height), + a4_width - (side_margins * 2), + disclaimer_frame_height, + showBoundary=0, + ) + + model_info_frame = Frame( + side_margins, 0, a4_width * 0.35, 100, showBoundary=0, leftPadding=0 + ) + + further_info_frame = Frame( + a4_width * 0.65, 0, a4_width * 0.35, 100, showBoundary=0, rightPadding=10 + ) + + # set background color + canv.setFillColor("#1f4e79") + canv.rect(0, 0, a4_width, a4_height, fill=1) + + # content + description = [ + Paragraph( + "ScaphX determines the presence of a Scaphoid fracture \ + on AP/PA views of the scaphoid", + style=model_description_style, + ) + ] + + csc_logo = [ + Image( + os.path.dirname(__file__) + "/images/cscLogo.png", width=100, height=40 + ) + ] + + gstt_logo = [ + Image( + os.path.dirname(__file__) + "/images/gsttLogo.png", + width=140, + height=58, + hAlign="RIGHT", + ) + ] + + patient_dob = [ + Paragraph("DOB:", style=study_details_style), + Paragraph(reversed_dob, style=study_details_style), + ] + + patient_id = [ + Paragraph("Patient ID:", style=study_details_style), + Paragraph(pat_id, style=study_details_style), + ] + + study_des = [ + Paragraph("Study Description:", style=study_details_style), + Paragraph(study_description, style=study_details_style), + ] + + study_day = [ + Paragraph("Study Date:", style=study_details_style), + Paragraph(xray_date_and_time, style=study_details_style), + ] + + study_id = [ + Paragraph("Accession Number:", style=study_details_style), + Paragraph(accession_number, style=study_details_style), + ] + + results = [ + Paragraph(f"Result: {result}", style=style_h1), + Paragraph(f"Action: {advice}", style=style_h1), + Paragraph(f"Error message: {error_message}", style=style_h1), + ] + + disclaimer = [ + Paragraph( + "These are preliminary results only. \ + Please await a finalised report. \ + For any diagnostic queries, \ + please discuss with an MSK Radiologist via the Radiology department.", + style=disclaimer_style, + ) + ] + + further_info = [ + Paragraph( + "For further information on how to use this tool \ + or to report a problem:", + style=misc_style, + ), + ] + + model_info = [ + Paragraph("ScaphX model version: v1.0.0", style=misc_style), + Paragraph("ScaphX app version: v1.0.0", style=misc_style), + ] + + xray = self.create_xray_jpeg( + sop_instance.pixel_array, b_box, xray_frame_width, xray_frame_height + ) + + # add Flowables to page + + banner_frame_2.addFromList(csc_logo, canv) + banner_frame_3.addFromList(gstt_logo, canv) + description_frame.addFromList(description, canv) + result_frame.addFromList(results, canv) + patient_name_frame.addFromList(patient_name, canv) + patient_dob_frame.addFromList(patient_dob, canv) + patient_id_frame.addFromList(patient_id, canv) + study_des_frame.addFromList(study_des, canv) + study_day_frame.addFromList(study_day, canv) + study_id_frame.addFromList(study_id, canv) + xray_frame.addFromList(xray, canv) + disclaimer_frame.addFromList(disclaimer, canv) + model_info_frame.addFromList(model_info, canv) + further_info_frame.addFromList(further_info, canv) + + # create page + + canv.showPage() + canv.save() + + return DataPath(os.path.join(os.getcwd(), pdf_filename)) + + def create_xray_jpeg(self, pixel_data, b_box, xray_frame_width, xray_frame_height): + """generate the jpeg of X-ray with bounding box overlay""" + + image = pil.Image.fromarray(pixel_data) + fig, ax = plt.subplots() + ax.imshow(image, cmap="gray") + rect = patches.Rectangle( + (b_box[0], b_box[1]), + b_box[2] - b_box[0], + b_box[3] - b_box[1], + linewidth=1, + edgecolor="r", + facecolor="none", + ) + ax.add_patch(rect) + plt.axis("off") + fig.savefig("dummy_im.png", bbox_inches="tight", pad_inches=0) + + return [ + pl.flowables.Image( + "dummy_im.png", width=xray_frame_width, height=xray_frame_height + ) + ] + + +class ColorFrame(Frame): + """Extends the reportlab Frame with a background color.""" + + def __init__(self, *args, **kwargs): + self.background = kwargs.pop("background") + super().__init__(*args, **kwargs) + + def draw_background(self, canv): + color = toColor(self.background) + canv.saveState() + canv.setFillColor(color) + canv.rect( + self._x1, + self._y1, + self._x2 - self._x1, + self._y2 - self._y1, + stroke=0, + fill=1, + ) + canv.restoreState() + + def addFromList(self, drawlist, canv): + if self.background: + self.draw_background(canv) + Frame.addFromList(self, drawlist, canv) + + # back to main script diff --git a/project/map/operators/images/cscLogo.png b/project/map/operators/images/cscLogo.png new file mode 100644 index 0000000000000000000000000000000000000000..a8a6e333a57977e819556e6f256938267db48b11 GIT binary patch literal 7091 zcmV;k8%*ShP)bEN#%D>HBG%_o2Z2$=dvrxBR`*{ExKz z%-sBXsQj6``@7Kmgs%J?d;YM=`e>g0pTPeTc>dvaE}8%U8stetK~#90?Okh|syeck z1R;QcpmGrrL`52U|Np;txFm=qT!pn~KWDukX7$W;Q%R*#w^UM_&0qY*U(|rw?&t-3 z$8oMX9OrZkB3IqO1x7=^4H)8Kxpr+W|5<4#_ED4H8MwsYTx z`cBp4@_*43|M&`$tDT(v#ppvdiXHR%ya(PZERk;G{WUXB-cehM$cSO+-qXJBb5SDG zm9>2NqhNovpQ*bByXBj)m*wNlz6M93YglS@Go9`3Yx+>%I#!apES5qw++M9DQdbo! z=oyXz_28PtVHYJRgmHHL)t4Sff<$o1zdJZaAxzBsH{|8;Y2HGi_X}`$K3VbNlYGPD zMTyI9M|tQMwOsm6k0l2Jfyi-akTCvb{gwslJUZdY_Qw4wJl@UQZWfi{Z(GH5{gJ=D z3(Wh1n$r+D@i6{#@ZmVq*IJE@K$-AY@OUz#RYW@hoXC%t7N{p*4I=HaOYWpU z6=L%>iMsyGrmv2+wSK#7$7I0s^S9c$s==f4q`#Ft%lpP$pLmIns~mnE(33f0Hf zG2TQl9nQYJ+By^-t>XXvT5rQkKHZb#d+rJdu!<2vp6v4-xvTww)eT(N)`{SbZUKA z0paW*)Jvt9r*Uf-{Sf7>zR#Is16q=^mG|D$^#fyQy7@SMKb;RPNhaG4=w-@g7uNep z2`&iqG62gRK-s4?eqFlUarK_~irLg~@aTa4=@9^AZR<&y`Qm)3tfPFv)v#7hLTZhg zZaxmv`(UnXhtntvw)e?_afB;wBPni9T)46YTM&VStj2F@-eo|ZKga-(5Z0VWdgEP? zPq*d}{e-0?d&bceCU`rIj6h0+W`hIpVNT#cMI9v#9SzSmYgfIBeSfR}s}@M(m$M?1 zdkl-jj618LVxj9GhIa0QgtBHnNesBEaWuo?M13lkLde7(9ni*$p zznLIc*kNb3RTPZ3n)dCvJD5z@ervPn9{{*s$IpH1EqMDz)+$I`NIvJ}{N$?*^AXvq zUx_p(Bh5_r2yf=VRQhhki*bNju;W}xQQFJt+ELkAUy5`EbcYRA7ZX$LpUXRH3BR9U zNoJICY{R$f?VvgQcOqRP-(Xl&gPePO)Whj(J>Z2{kgG7O+D3XF%btqQXMmFqmPiCXQ0)^Tk3Fq!(t0h^aLX@4xggF}YC}!SENPKi;rPlR$j(xn z&dG?=(!7QCs5u=jA<`8@P|31SJQNV>*ncPA8EP1aOD@=gZm_wx}QD0Si#l8N63I4uGsI$l)F zS5W=%96Q`V^0D7Y!=xtN$7z6!_(2~-z}@S~^`s6^?F604DAYsuX!DhGQbw5r6dre@ zFY`5?*B(g;3~*X3a)|9r3sj^1p=TUu-Qt4W_4L{nDcv-2M)>cq(hy^RkmU zc6o^M!RY|+og-_8=p9ZLzFd#L2lZ#uMEe(p5|Bh<8<;Gbd6li@CU9;@8c`k8bXGJ z+M`z)?3bfUg!)O(@SS3X$#{LW&Bu$YpanXmErJeXe#hf@Fol|*kQ$r}9eZFJotBfZ zXX2Y&-1CJ|5X|adhD%+S-E6ce^=^eC0@#@9v_bEvm(;MPE-zuwGF@<5y>ic0Zjn8A z?~%7~s0eDgEAjPe$6@&o!}Pv`JHeV#sigmiB44L6exx=KGi5{EAZG5P%Bq`y1J4;# z8~_xsTcj}vuS6X#40mEcrM5*Zx!SOW+0JB=uG?6fH+;y3O3(fepT^x9@-#?rTTz{uur(YHV@~Vx>Uw%EIo$xB}|@h z%9Xy6ND(KNwO#ZozYCx5b0hNxg0TFDcHL2>P>U3s$gM5aZmu<|05+z50FpyEcLzV; z0G@s-qpMJ>d?4;lxWK-K*r-yztVl9H>g9)V^E^g?@fm-sqs3CP$CQa!NKq3}>5w{@g5T=QhUrT}3ALM$b2G)LTB&GBRQCQim zD~~P6SXJNK!ZhK9fF(hn-I6uGFR_PNP{=bX*Ve|C$f>StEet|pq<8eehDzCjjld9YZt6zz| zp1=PBTE2~{0JMF^CK98#vldxpTu9%}@O+(DnS}D}7!~u;D5sjZ(%hFZaTnAdLZq## zYyA=TTre07H^$qtt*XJAxnT-&A<93+qEo7xc(&2kPXcrRW{$jko~xew@VcCUZ1g4Csn8Om3`S{lg-PoRIGLqlER`~(?EatiKVW>Q z97o}_U3)+efkynMnZQWG|7E9=lH>=yudv5{38aQHwMM)MuZ4L*DqP_t>;cp)u zOGIT&Ox3dy{s6vYYCCx-X4^l2ijDq$ue@W{7ON5V0h1MzFgPRK<^iZnQWn`z{(%Wg)Rn811R`c#4O;9 z>%-d)2=TYHMZz^UAT|f>d_CDvuPOJXhC%0e{sc-BU(_-15k3Z|HZh*2zpP|^Ky%nU zK*TCeFF@@Bt4wWul;?Sxg@6t5v0^)lAe%T^(ajfdHoWEh)Tbe*W#eXM;l=7em92|% zyM&7%s7;Kg=`9hjbwI&8#%b|@+>Fi^O(?SJ)cyc4tMeTUjcOq6WD}45tawESM8Qs7 zE3YSFVc#t4(X$Fr{{V1RyPUHc*LK`moA`i3;`@rhD`BJl}pS4`;=R%czTUX;;r^roX_-Zlm_>Wh?Crm677JZ{sRq(Ei{v=* zwpw2AUDVyKcqY<3|LrLzsI!kh0(>{Oh#|1E_uJutY?loqJ}?x*?3Z2Ue<_5iG;mSL zhC`Tk9)ic_aN*$i10*nSFEhf+oW8pMb_V{Xz`RlqyM2A`)(*^xHg{Ldp_1}0DpJM1 z{9{?#j5@hL08ojt9_Na(Qi$!ld?W{=u6fjHcO+4OvvI1Hxm^MKroE^Oq6vQh)G-IPM}^JvDoxQo8mJHMofTu4Uz{JHjz{qF zu6Zo6656$D28{j`BK*8<4J41yUfYPJ`OVxT1tT%{33(Rs!WTV`X^FP5ln< z*0ys?-fG#ccs4+2w~vrX23K2Cd#wWMHW6v8QJhT6*yCzfIM?+km-VhKz>H0sDC>>$ zU(+D%S~XFu{&%gIDY@O%Rx)CJ>3l^Ef(-&)nTcs1SGc~nD%yUlh{G1QRN44&QJ`v% zu{5hI?&5v5y{IBK;#tGP+Bbm~hTYO1q}FvC7XNw17jN2HhIrP?lsL12?P5E(_tqfi zwW`?G!LEH9k;qIHDDLoV+GqFmjU0WMa!?jjTwp}s55+zR9Pzr>{}8Bv6vv7MeEtFKKJ7T_;Xz)qLV=Rr*kFzg$q4-spi{}*)C(ND~_S@ zA96yddnTl$c8mW#V}(gwzXrr6omQ(*cwn>9PVSMboB2BbOZQb~0MZXtU_E%n|EhS8 zQg@H*GDS^X>)4BEFZywvx`1t5hm-#WP<7K%$ypei^&Z6sOZ3|(#NXRQt5xpd{!fhS zA!H{(W7)LzY7nUBSE{J@ghY~bBE_hieOrS_wM}}im5m6o2BBWg6l?XRa=yA^cjY%G zzd7*<`uz)|y7^KRc;g%;c~*daVXwYv1rL$zu5l-Z_#kqdbiU4H=EonN{{yj%d-(aL zp6W=w@KIfHDX$yVmy7X3Rj?iFH11HUfJb1BBi%dpFz7Q9L==azNRv(sNTd2ciW_}j zP|>IZd6J+!^5`cdzeb*bq2Cj#-+C&lD5N!F83ijT#>D$kD zYw6cq>({r0u(4UlyNZ;1TYH9OOWSb9-mJpNYpK-##uQXSnq z{9zrMZ#6O=%>(TB>PV$CqXLL%4ef5tf1R`s(8Sw|_43QNm%ueTr@(mT7r)pDwb3sc z?s$=VE8xwJ`ee?y4ikIA7B6C~`OMc)-xw&-evq#4jH0?pJ#A<}^M?v_1!mg^@(1T} z&yp3s>H9LC!sv%oeG(q2rL*B-3zmHaGLgnF=3Aq4@nBT)3>oXzX1?-?-rwk0H2 zL7C4Cqq)zas%Euvz;5kf*2`cWK(`JU+Nnz3qjOoD#;#1!=$O9rOS*-yH0FGL zP5PcmMqk3puTum}CQN*HA49MEHA;sTapoJ{z9IvENq9gRxLeGAR-D9h>cY8{+_DdXYD5=8i@2GtOndvMU`Lza<3r?a1J^ z5IIgmnak{Eg>@I6t(h?9osy+usgR<_>~z5nn+?hOo}|*q1%8!T+ApG!ZMkcxtuBJ- z(+grYHOR^QHpoCd&Ya+($W`PHq2@=o%t@Q7&^4GPm$i>7uwOibq~R#E*w2OEC#IX^ z&(lh=@4fN5eFysiZ4QlmD;>|^CAO!~TC9-WqZEC%wTHrSn_RD%VO&i6>fFztjmzXc zyL*OItJ`bGcH9xmxMLS0gGsOL!6UWeJ^dZvlbma9JGwQY`{)B**JQbKpC=3*9a+R? z-}U$CSm{-ma_lN%v99WTb*7#J;#YO7dG3jf*4WC4(R2xPzt>xLYJ*Ju#gz@?M)fiC z9d^4X^lTD`UH$JQv>%hNg;7Tbulx&S0}9>gIau#frYFFS?hdz6Vs?{DV0KR9NPMCq z?qEU$+~9pP__%hq*Zlc7&4F2Wufb&KBtr1IJ1*9#W@z?UbCsNHb$0=l&}nEKP)|Dh zTk_qfIrW~B&F-F)AbWM|5$Fep?mnh%|GbBcgesfu8=IwE@2M247(v)^+xm zEXUE)3P|~=o- zLhgq_SyT0Jn^>ri&qXj;ep!ArJefXT7o$bMIQ>RPT@i}zzi#N=Gp^_^-7_>)XJL0k zodDn6E&F&~OqZAJd-~8-O|GWyc@BDb{TY-U>z31Q-90nVwMH@oW3L#Cyzq| z0af@dTZr%2TW+mifjjnpCP zI#>$dImEQPf%P;rSg_?KaQPqz)4^8vBZ1zP4KNEgqu{@jjv*D`&V&f+tjps^lK4(V z*5I{!8PcSqfSh_dJIlLL(V)H%nRnU&W}}}5YVcYauumfata^f)b9PQWzJ2)i2ZiVN zyITbBw#oX18E$F=MrqUpFK@L3r!)fdYqx6yZwn}qM)E_*h7zL3>w6>8X`^WnQ6pkI z8eM^X)RD~?Po|Mlm(2;B&V~8%HlUBgmx2ur@06`#3|nU1^rJ2IgVYn%5Pbq9uzkBa zUQhTWurfAdAtIxZlUD4BHDH2u3@~tmC8lG+=Omjz_N2~wjek@s%hcS11=d#w& z%lH0j8h17s&|J@4NliXOfg%F6OXg>D7hOb6g^~6~i#3Xz^}Egprk6Qf5Ru*R>I8eJ zmy2RVfgQjpy zBSjcprjh~RBhx86bGogne3RAHqE8%$h%U{N3T2#IU(m`B`1#Q3@MTR=VgNne=A8E< zU#fp=fTg1dkDjuhOhDo2^c=(cL;D3$4>X~`9PnN zdDFL6%uCIROf7OiztD*}11|Ruf!+wo#M%bVnJz6; zY{oroXR=i%`T7CZqGE#ouqn{}HQD2+24V+pYr3H|rVNe(yyPT)+I~0`9^1s=_$sxD zpFZc&!QP*#%){q& z5y}UWy6wer**wW-?n!jF@W$KCs)h1uQ_bfbdqhPFVnGE4zVK-l`uFJ6!y0`*ly!^pEy_R zx=)X5cdu;&3di$r;#g)iUE6Y!-?O_qmp{kwadCMa60)H9x_)$ZeWz%vk{TY%`qTZ3 dzxc1<{{YurwV&OpBI7AehhT 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 Z|9=N0eT+*i6c_*i002ovPDHLkV1la Date: Thu, 17 Oct 2024 14:54:12 +0100 Subject: [PATCH 2/2] add MAP reqs file --- project/map/app_requirements.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 project/map/app_requirements.txt diff --git a/project/map/app_requirements.txt b/project/map/app_requirements.txt new file mode 100644 index 0000000..8c7ccd8 --- /dev/null +++ b/project/map/app_requirements.txt @@ -0,0 +1,3 @@ +monai-deploy-app-sdk==0.5.1 +csc-mlops==0.9.18 +highdicom==0.22.0 \ No newline at end of file