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/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
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 0000000..a8a6e33
Binary files /dev/null and b/project/map/operators/images/cscLogo.png differ
diff --git a/project/map/operators/images/gsttLogo.png b/project/map/operators/images/gsttLogo.png
new file mode 100644
index 0000000..7bf3b59
Binary files /dev/null and b/project/map/operators/images/gsttLogo.png differ