Source code for pymedphys._pinnacle.pinnacle

# Copyright (C) 2019 South Western Sydney Local Health District,
# University of New South Wales

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at

#     http://www.apache.org/licenses/LICENSE-2.0

# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# This work is derived from:
# https://github.com/AndrewWAlexander/Pinnacle-tar-DICOM
# which is released under the following license:

# Copyright (c) [2017] [Colleen Henschel, Andrew Alexander]

# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:

# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.

# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.


import logging
import os
import sys
import warnings

from .image import convert_image
from .pinn_yaml import pinn_to_dict
from .pinnacle_image import PinnacleImage
from .pinnacle_plan import PinnaclePlan
from .rtdose import convert_dose
from .rtplan import convert_plan
from .rtstruct import convert_struct


[docs]class PinnacleExport: """Handle Pinnacle data to allow export of DICOM objects This class is used to export the various DICOM objects from Pinnacle data found within the given path. Parameters ---------- path : str Path to raw Pinnacle data (directory containing 'Patient' file). logger : Logger Logger the tool will log to. """ def __init__(self, path, logger=None): # Show a warning to ensure that this module is used with caution warnings.warn( "The Pinnacle Export Module is intended for research purposes only. Parts " "of the DICOM conversion are hard-coded and completely ignore your data. " "This has proven useful for research applications but is not appropriate " "for clinical use.", category=UserWarning, stacklevel=2, ) self._logger = logger # Logger to use for all logging self._path = path # Path to the root directory of the dataset # (containing the Patient file) self._patient_info = None # The patient data read from self._plans = None # Pinnacle plans for this path self._images = None # Images found in image.info if not self._logger: self._logger = logging.getLogger(__name__) self._logger.setLevel(logging.DEBUG) if len(self._logger.handlers) == 0: ch = logging.StreamHandler(sys.stdout) formatter = logging.Formatter( "%(asctime)s - %(levelname)s - %(message)s" ) ch.setFormatter(formatter) ch.setLevel(logging.DEBUG) self._logger.addHandler(ch) @property def logger(self): """Gets the configured logger. Returns ------- logger : Logger Logger configured. """ return self._logger @property def patient_info(self): """Gets the patient info. Returns ------- patient_info : dict Dictionary object containing the data found in the 'Patient' file within the raw Pinnacle data. """ if not self._patient_info: path_patient = os.path.join(self._path, "Patient") self.logger.debug("Reading patient data from: %s", path_patient) self._patient_info = pinn_to_dict(path_patient) # Set the full patient name last_name = self._patient_info["LastName"] first_name = self._patient_info["FirstName"] middle_name = self._patient_info["MiddleName"] self._patient_info["FullName"] = f"{last_name}^{first_name}^{middle_name}^" # gets birthday string with numbers and dashes dobstr = self._patient_info["DateOfBirth"] if "-" in dobstr: dob_list = dobstr.split("-") elif "/" in dobstr: dob_list = dobstr.split("/") else: dob_list = dobstr.split(" ") dob = "" for num in dob_list: if len(num) == 1: num = f"0{num}" dob = f"{dob}{num}" self._patient_info["DOB"] = dob return self._patient_info @property def plans(self): """Get the plans available. Returns ------- plans : list List of PinnaclePlan's available within the Pinnacle raw data in the path configured. """ # Read patient info to populate patients plans if not self._plans: self._plans = [] for plan in self.patient_info["PlanList"]: path_plan = os.path.join(self._path, f"Plan_{plan['PlanID']}") self._plans.append(PinnaclePlan(self, path_plan, plan)) return self._plans @property def images(self): """Get the images available. Returns ------- images : list List of PinnacleImage's available within the Pinnacle raw data in the path configured. """ # Read patient info to populate patients images if not self._images: self._images = [] for image in self.patient_info["ImageSetList"]: pi = PinnacleImage(self, self._path, image) # Check that image info exists to ensure the image is really available if pi.image_info: self._images.append(pi) return self._images @staticmethod def export_struct(plan, export_path=".", skip_pattern="^$"): """Exports the RTSTRUCT DICOM modality. Parameters ---------- plan : PinnaclePlan The plan for which structures should be exported. export_path : str, optional The file system path where the DICOM object should be exported to. skip_pattern : str, optional A regular expression specifying which ROIs to skip For example: RadCalc|normal_tissue """ # Export Structures for plan convert_struct(plan, export_path, skip_pattern) @staticmethod def export_dose(plan, export_path="."): """Exports the RTDOSE DICOM modality. Parameters ---------- plan : PinnaclePlan The plan for which dose should be exported. export_path : str, optional The file system path where the DICOM object should be exported to. """ # Export dose for plan convert_dose(plan, export_path) @staticmethod def export_plan(plan, export_path="."): """Exports the RTPLAN DICOM modality. Parameters ---------- plan : PinnaclePlan The plan for which dose should be exported. export_path : str, optional The file system path where the DICOM object should be exported to. """ # Export rtplan for plan convert_plan(plan, export_path) def export_image(self, image=None, series_uid="", export_path="."): """Exports an image from the Pinnacle data. Parameters ---------- image : PinnacleImage, optional The image to export. series_uid : str, optional The SeriesInstanceUID of the image to export. export_path : str, optional The file system path where the DICOM objects should be exported to. """ if not image and not series_uid: self.logger.error("No image to export") for im in self.images: im_info = im.image_info[0] im_suid = im_info["SeriesUID"] if len(series_uid) > 0 and im_suid == series_uid: convert_image(im, export_path) break if image: convert_image(image, export_path) def log_images(self): """Outputs all images found in the Pinnacle data to the log.""" for i in self.images: image_header = i.image_header if not image_header: # If no ImageHeader is available then we can ignore this continue modality = "?" if "modality" in image_header.keys(): modality = image_header["modality"] series_uid = "?" if "series_UID" in image_header.keys(): series_uid = image_header["series_UID"] series_date_time = "?" if "SeriesDateTime" in image_header.keys(): series_date_time = image_header["SeriesDateTime"] self.logger.info("%s: %s %s", modality, series_uid, series_date_time) def log_plan_names(self): """Outputs all plans found in the Pinnacle data to the log.""" for p in self.plans: self.logger.info(p.plan_info["PlanName"]) def log_trial_names(self): """Outputs all trials found in the Pinnacle data to the log.""" for p in self.plans: self.logger.info("### %s ###", p.plan_info["PlanName"]) for t in p.trials: self.logger.info("- %s", t["Name"]) def log_trial_names_in_plan(self, plan): """Outputs all trials found within a plan to the log. Parameters ---------- plan : PinnaclePlan The plan for which to log the trials. """ self.logger.info("### %s ###", plan.plan_info["PlanName"]) self.logger.info(plan.path) for trial in plan.trials: self.logger.info("- %s", trial["Name"])