matplotlib

Source code for mainWindow

# Copyright (c) 2014, Varian Medical Systems, Inc. (VMS)
# All rights reserved.
#
# Veritas is an open source tool for TrueBeam Developer Mode provided by Varian Medical Systems, Palo Alto.
# It lets users generate XML beams without assuming any prior knowledge of the underlying XML-schema rules.
# This version is based on the schema for TrueBeam 2.0.
#
# Veritas is licensed under the Varian Open Source License.
# You may obtain a copy of the License at:
#
#      http://radiotherapyresearchtools.com/license/
#
# For questions, please send us an email at: TrueBeamDeveloper@varian.com
#
# Developer Mode is intended for non-clinical use only and is NOT cleared for use on humans.
# Created on: 11:11:18 AM, Jul 7, 2014
# Author: Pankaj Mishra
#
# Update 7/29/14 - Removed hardcoded paths and win32api dependence -CLW
# Update 7/28/14 - Added 'electron' option for energy type - JDeMarco
# Update 9/16/14 - Updated to generates XML beam for TrueBeam 2.0
#                - Window based api is removed to make this version agnostic to
#                - under lying OS
#                - Removed hard-coded output XML beam file name
# Update 10/7/14 - Included IEC to Varian scale conversion in dicom2xml converter
# Update 5/12/15 - Major Documentation update - Alan Chang
# Update 4/13/16 - Added new functionality 'Dicom Tree' - Nilesh Gorle


# *************************************
# This file is the entrypoint to the program.

# In general, any given class has the following structure:
#    1) Each class, in the root folder corresponds logically to
#       a single window in the main application
#    2) The visual layout and UI elements are specified by an import
#       of form ui_xxxxx where xxxx is the name of the class
#    3) The main dataflow is mediated by the cp_data variable, which
#       is used as a container to pass information between windows
#    4) The class itself handles the semantics of what UI elements
#       map to what actions and what data passes through to cp_data
#    5) Any instance attributes inside a class is used as bookkeeping
#       to keep track of data before it gets sent to cp_data

# The above will be reproduced in the preface document, and is included
# to be an inline reference to the structure.
# Up to date as of May, 12 2015.

# %%%

# The main work flow of mainwindow is the following:
# It starts out with an empty CPData instance in self.cp_data
# Which then gets populated, either as a result of a child
# window providing data, or via an import from a file.
# When the CPData is populated, it can then be consumed
#  via cp_data.create_xml() to provide the root of an xml tree (see self.plotXML and
# openBeamonHeader)


# Any non-responsive or buggy  UI elements will be here
# Any xml import or export errors will be in CPData
# Any problems in sub windows will be in the respective files
# *************************************


# These imports are either provided by Pyside for manipulating UI elements
# Or generated via QtDesigner + pyside-uic
# Look in ui_files for UI element names
try:
    from PySide.QtGui import (QMainWindow, QDialog, QIcon, QFileDialog, QMessageBox,
                              QApplication, QLabel, QPushButton, QTreeView, QFrame,
                              QStandardItemModel, QStandardItem, QAbstractItemView,
                              QStandardItem, QHeaderView, QSizePolicy, QTextBrowser,
                              QVBoxLayout,QWidget, QImage, QPixmap, QScrollArea)
    from PySide.QtCore import QCoreApplication, QRect, Qt

    # The following imports are for plotting
    import matplotlib
    matplotlib.use('Qt4Agg')
    matplotlib.rcParams['backend.qt4'] = 'PySide'
    from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
    from matplotlib.figure import Figure
except ImportError:
    #print('Pyside not getting imported')
    pass

from UI import ui_mainWindow, ui_licenseDlg


# Modules containing other sub windows
# Any dialog or windows generated from those subwindows
# are also defined here
import imaging, beamon

# Container that passes data to and from windows, also responsible
# for data integrity and import/export of xml
# Look here for extending data type or bugs in XML specification
from models.CPData import CPData

# Utility helper functions
import sys, os, subprocess, shutil
from xml.etree import ElementTree as ET
from utils import dicom2xml, plotXML, SetBeam20, dcm_qt_tree #setBeam

DEFAULT_XML_FILE = os.path.join('output','output.xml')

[docs]class MainWindow(object):#QMainWindow, ui_mainWindow.Ui_MainWindow): '''The main window class is the entrance point for both DICOM-RT based XML generation/modification as well as for the XML-from-scratch. ''' def __init__(self, parent=None): super(MainWindow, self).__init__(parent) self.setupUi(self) self.setWindowTitle("Veritas: TrueBeam Developer Mode") self.setWindowIcon(QIcon('truebeam2.jpg')) self._init_beam_on() self._init_imaging() self._init_text_editor_load() self._init_plot() self._init_menu() self.dirty = False self.XMLfile = DEFAULT_XML_FILE # Activates on pressing the BeamON button
[docs] def openBeamonHeader(self, cpdata=CPData()): '''Open the header part of XML generation. Header basically refers to the header as well as part of the control point 0. :param cpdata cpdata=None: build XMLbeam file from scratch cpdata=cpdata: process a previously generated XMLbeam file''' if cpdata is None: self.cpdata = CPData() self.cpGUI = beamon.beamonHeader(cpdata=self.cpdata) self.cpGUI.exec_() self.cpHeader = self.cpGUI.cpdata.cpHeader #======================================================================= # When the doneButton is pressed from the control point # exec_() generates int 1 as a 'signal'. After receiving the signal # the control point window 1 is closed and XML file is generated #======================================================================= if self.cpHeader is not None: # To exclude close button and 'cancel' scenario if(self.cpGUI.cpWindow.exec_() == 1): self.cpdata = self.cpGUI.cpWindow.get_data() # Prompt the user for choosing output file name qout = QFileDialog.getSaveFileName(self, "Select output XML file", dir=self.XMLfile, filter="XML Files (*.xml);;All Files (*.*)") self.XMLfile = qout[0] self.dirty = True # Set the flag for saving the XML file # MISLEADING NAME. SetBeam20.parse also writes to self.XMLfile! root = self.cpdata.create_xml() SetBeam20.parse(root, self.XMLfile, True) self.displayOutput(self.XMLfile)
# Activates on pressing Imaging Button
[docs] def openImagingWindow(self): ''' Opens the imaging window, sending it data about Whether inside treatment or outside treatment is wanted, Based on the dropdown menu below the Imaging button. ''' image_type = self.imagingTypeBox.currentText() self.imagingGui = imaging.cpImage(None, '', image_type) # Instantiate an imaging window self.imagingGui.openImaging() # Open the imaging window self.imagingGui.exec_() self.XMLfile = self.imagingGui.xml_file self.displayOutput(self.XMLfile)
# Activates on pressing Plot Axes button
[docs] def plotXML(self): ''' Plots the XML displayed, using the two dropdown boxes + potentially the mlc text. Majority of heavy lifting happens inside the plotXML.plot_fig function. ''' x_axis = self.xAxisBox.currentText() y_axis = self.yAxisBox.currentText() try: root = ET.parse(self.XMLfile) except TypeError: self._no_xml_defined() self.openXMLPlan() return mlc_x = None mlc_y = None if self.mlcLeafX.text(): mlc_x = int(self.mlcLeafX.text()) x_axis = x_axis[-1] # assumes dropdown text is of format MLC A or MLC B if self.mlcLeafY.text(): mlc_y = int(self.mlcLeafY.text()) y_axis = y_axis[-1] # assumes dropdown text is of format MLC A or MLC B # Plot if all the data is correctly initialized, otherwise throw an error dialog try: self.ax = plotXML.plot_fig(self.ax, root, x_axis, y_axis, mlc_x, mlc_y) self.canvas.draw() self.canvas.show() except plotXML.AxisError: self._invalid_axis_box() return except AttributeError: self._improper_x_axis() return
# Activates on pressing Open XML File button def openTextEditor(self): ''' Open the final xml file in a text editor. ''' #CW Note - Use a generic opener. fileName = self.XMLfile self.systemOpenFile(fileName) # Activates on pressing "Open" icon or doing File > Open Plan def openXMLPlan(self): ''' Parse an existing XMl plan and display the file in header and subsequent control point windows. This function can be launched if an xml file already exists or an xml file has been generated from a DICOM file using Dcm2Xml function ''' fileObj = QFileDialog.getOpenFileName(self, "Choose an .xml file", dir="input", filter="Text files (*.xml)") fileName = fileObj[0] if not os.path.isfile(fileName): QMessageBox.information(self, "XML file", '''No XML file selected''', QMessageBox.Ok) return self.XMLfile = fileName self.loadFile(fileName) self.displayOutput(fileName) # Activates on pressing Dcm2xml button
[docs] def dcm2xml(self): ''' Calls DICOM-RT to XML conversion function. A message box conveys the completion of XML file generation. Most logic occurs inside the dicom2xml function ''' # Prompt the user to select a file fileObj = QFileDialog.getOpenFileName(self, "Choose a DICOM-RT file", dir="input", filter="Text files (*.DCM; *.dcm)") fileName = fileObj[0] # Let the user select the location to save the file qout = QFileDialog.getSaveFileName(self,"Save output XML file",dir=self.XMLfile,filter="XML Files (*.xml);;All Files (*.*)") outfile = qout[0] # Cancel if nothing changes if outfile == '': QMessageBox.information(self, "DICOM conversion cancelled", 'DICOM conversion was cancelled', QMessageBox.Ok) return self.XMLfile = outfile # Inform the user that the XML file has been generated if os.path.isfile(fileName): x = dicom2xml.Dicom2Xml(fileName, self.XMLfile) x.dicomToDataset() x.extractControlPoints() # Message that a specific file has been generated QMessageBox.information(self, "DICOM to XML conversion", '''XML file has been generated''', QMessageBox.Ok) # Set the file generated bit to true self.dirty = True self.loadFile(outfile) self.displayOutput(outfile) else: # No DICOM file has been selected QMessageBox.information(self, "DICOM to XML conversion", '''No DICOM file selected''', QMessageBox.Ok)
[docs] def loadFile(self, fileName): ''' Calls beamonHeader to load an existing XML file in the Control point windows :param fileName: The file is read as an xml tree and then imported into the cpdata structure ''' tree = ET.parse(fileName) root = tree.getroot() self.cpdata.import_xml(root)
# Activates on Help > About or Ctrl+A def aboutVeritas(self): ''' Print one line description, research only message line and contact email in a message ''' QMessageBox.about(self,"About Veritas", """<b> Veritas v 1.0 </b> <p>Veritas is an open source tool for TrueBeam Developer Mode provided by Varian Medical Systems, Palo Alto. <p>It lets users generate XML beams without assuming any prior knowledge of the underlying XML-schema rules <p>This version is based on the schema for TrueBeam 1.5 and 1.6 <p>For questions, please send us an email at: TrueBeamDeveloper@varian.com <p> <b> Developer Mode is intended for non-clinical use only and is NOT cleared for use on humans </b> """) # Activates on Help > Documentation or Ctrl+D def viewManual(self): ''' Display the TrueBeam Developer Mode manual in acrobat reader ''' fileName = 'TrueBeamDeveloperModeManual_V_0.2.0_03_2012.pdf' self.systemOpenFile(fileName) # Activates on Help > License or Ctrl+L def viewLicense(self): ''' Display Varian open source (VOS) license in acrobat reader ''' fileName = 'Veritas_License.pdf' self.systemOpenFile(fileName) def fileSave(self): pass # Activates on File > Save and pressing the Save button def fileSaveAs(self): ''' Save copy of an already existing XML file ''' # No XML beam created if self.dirty is False: QMessageBox.information(self, "Save XML file", '''No output XML file exists''', QMessageBox.Ok) return # Let the user select the location to save the file qout = QFileDialog.getSaveFileName(self, "Save output XML file", dir = self.XMLfile, filter = "XML Files (*.xml);;All Files (*.*)") # Save the XML file # Copy2 save file content, its meta data as well as permissions try: shutil.copy2(self.XMLFile, qout[0]) # In case source or dest are same file except shutil.Error as e: print("Error: %s" % e) # In case source or dest file doesn't exist except IOError as e: print("Error: %s" % e) # What to do when main window closes def closeEvent(self, event): ''' Double check with the user that he/she wants to quit Veritas :param event: ''' reply = QMessageBox.question(self, "Veritas", "Are you sure you want to quit?", QMessageBox.Yes|QMessageBox.No) if reply == QMessageBox.Yes: event.accept() elif reply == QMessageBox.No: event.ignore() # --- __init__ private helpers def _init_beam_on(self): self.beamonButton.clicked.connect(self.openBeamonHeader) # Connect to the beam on functionality self.cpHeader = dict() # A set of attributes for data self.cpControlPoints = list() self.cpdata = CPData() def _init_imaging(self, ): self.imagingButton.clicked.connect(self.openImagingWindow) def _init_text_editor_load(self): """Open XML file button """ self.xmlTextEdit.clicked.connect(self.openTextEditor) def _init_plot(self): """Plot Axes button""" fig = Figure(figsize=(5.0,4.0), dpi=72, facecolor=(1,1,1), edgecolor=(1,1,1)) self.ax = fig.add_subplot(111) self.ax.hold(False) self.canvas = FigureCanvas(fig) self.vbox.addWidget(self.canvas) self.plotAxes.clicked.connect(self.plotXML) self.xAxisBox.currentIndexChanged.connect(self.display_mlc_x) self.display_mlc_x() self.yAxisBox.currentIndexChanged.connect(self.display_mlc_y) self.display_mlc_y() def display_mlc_x(self): displayed_text = self.xAxisBox.currentText() if displayed_text == 'Mlc A' or displayed_text == 'Mlc B': self.leafXLabel.show() self.mlcLeafX.show() else: self.leafXLabel.hide() self.mlcLeafX.hide() self.mlcLeafX.setText('') def display_mlc_y(self): displayed_text = self.yAxisBox.currentText() if displayed_text == 'Mlc A' or displayed_text == 'Mlc B': self.leafYLabel.show() self.mlcLeafY.show() else: self.leafYLabel.hide() self.mlcLeafY.hide() self.mlcLeafY.setText('') def dicomtree(self): dtree_viewer = DicomTree(self) dtree_viewer.dicomtree() def _init_menu(self): self.actionOpenPlan.triggered.connect(self.openXMLPlan) self.actionCreatePlan.triggered.connect(lambda: self.openBeamonHeader(cpdata=None)) self.actionDcm2xml.triggered.connect(self.dcm2xml) self.actionDicomTree.triggered.connect(self.dicomtree) self.actionSave.triggered.connect(self.fileSaveAs) self.actionSaveAs.triggered.connect(self.fileSaveAs) self.actionExit.triggered.connect(self.close) self.actionAbout.triggered.connect(self.aboutVeritas) self.actionDocuments.triggered.connect(self.viewManual) self.actionLicense.triggered.connect(self.viewLicense) # --- End __init__ private helpers # --- private helper that displays output xml def displayOutput(self, fname): ''' Read the XML file line-by-line and display it on the main window text-browser Note: '<' and '>' needs to be replaced by &lt and &gt to display the XML tags correctly This is important in case of 'bank B' which is otherwise mistaken as a 'bold' html tag. :param fname: ''' # Start with a clear browser display # Also, cleans up the previous window display self.textBrowser.clear() # Read in xml file xmlFile = open(fname, "r") outputStr = xmlFile.readlines() xmlFile.close() # Replace '<' with '&lt;' and '>' with '&gt;'. # This is done to avoid confusion. # e.g., <B> mlc xml tag can be interpreted as bold for line in outputStr: line = line.replace("<","&lt;") line = line.replace(">","&gt;") line = line + '\n' self.textBrowser.append('%s' % line) self._change_button_color() def _change_button_color(self): self.beamonButton.setStyleSheet("background-color: teal") # --- end displayOutput helper # --- plotXML private helpers def check_mlc_range(self, leaf_box): if leaf_box.isHidden(): return True try: mlc_no = int(leaf_box.text()) if 60 >= mlc_no >= 1: return True else: QMessageBox.warning(self, u"Invalid MLC value", u"MLC must be in interval [1,60]", QMessageBox.Ok, QMessageBox.NoButton) except ValueError: QMessageBox.warning(self, u"Invalid MLC value", u"MLC must be an integer", QMessageBox.Ok, QMessageBox.NoButton) finally: leaf_box.setText('') return False def _no_xml_defined(self): QMessageBox.warning(self, u"No XML to plot!", u"Please open or construct and XML file first.", QMessageBox.Ok, QMessageBox.NoButton) def _invalid_axis_box(self): msgBox = QMessageBox() msgBox.setWindowTitle('Veritas plotting error') msgBox.setText("Select a valid axis type.") msgBox.exec_() def _improper_x_axis(self): QMessageBox.warning(self, u"Missing x-axis value", u"XML file does not have x-axis properly defined!\n"\ "Select another axis or add more points to x-axis.", QMessageBox.Ok, QMessageBox.NoButton) # -- end plotXML private helpers @staticmethod def systemOpenFile(fileName): ''' Should hopefully be replaced by a 'real' cross-platform file-open at some point. Added by CLW on 6/29/14 ''' try: os.startfile(fileName) except: if sys.platform == "darwin": subprocess.Popen(["open", fileName]) else: subprocess.Popen(["xdg-open", fileName])
class LicenseAgreement(object):#QDialog, ui_licenseDlg.Ui_licenseDlg): ''' Show the license window before the application starts ''' def __init__(self, parent=None): super(LicenseAgreement, self).__init__(parent) self.setupUi(self) self.setWindowTitle("Veritas: License Agreement") self.setModal(True) self.acceptButton.clicked.connect(self.startVeritas) self.cancelButton.clicked.connect(self.cancelVeritas) def startVeritas(self): ''' User accepted the license agreement and is allowed to proceed ''' self.mainwindow = MainWindow() self.hide() # Hide is used as close calls the close event function self.mainwindow.show() def cancelVeritas(self): ''' user decides not to proceed ''' self.close() def closeEvent(self, event): ''' Double check with the user. Also, alerts user in case he/ she accidentally presses the close button :param event: ''' reply = QMessageBox.question(self, "Veritas", "Are you sure you want to quit?", QMessageBox.Yes|QMessageBox.No) if reply == QMessageBox.Yes: event.accept() elif reply == QMessageBox.No: event.ignore() class Dicom_Dialog(object): def setupUi(self, Dialog): from PySide.QtCore import QMetaObject, QRect Dialog.setObjectName("Dialog") Dialog.resize(985, 520) self.retranslateUi(Dialog) QMetaObject.connectSlotsByName(Dialog) def retranslateUi(self, Dialog): Dialog.setWindowTitle(QApplication.translate("Dialog", "Dicom Tree", None, QApplication.UnicodeUTF8))
[docs]class DicomTree(object): """ Dicom tree viewer window for uploading dicom file and display all it's attribute in tree structure """ def __init__(self, parent): self.parent = parent
[docs] def showTree(self, filepath, dialog, layout, view, model, patientBrowser, planBrowser): """ Show data in tree """ # populate data dicomtree = dcm_qt_tree.DicomTree(filepath, model) # Dicom Tree Data model = dicomtree.show_tree() # Patient Info Table patient_table = dicomtree.get_patient_table() patientBrowser.append(patient_table) patientBrowser.show() # Plan Info Table plan_table = dicomtree.get_plan_table() planBrowser.append(plan_table) planBrowser.show() view.setModel(model) view.show() # store pixel data pixel_array = dicomtree.pixel_array if pixel_array is not None: self.displayImage(dialog, layout, pixel_array) return view
def displayImage(self, dialog, layout, pixel_array): """ Display dicom 2D image """ if pixel_array is not None: from matplotlib import pyplot as plt image_path = 'dicom.png' #path to your image file plt.imshow(pixel_array, cmap=plt.gray()) plt.savefig(image_path) new_frame = QWidget() new_frame.setGeometry(QRect(30, 540, 921, 500)) label_Image = QLabel(new_frame) image_profile = QImage(image_path) #QImage object image_profile = image_profile.scaled(650,487, aspectRatioMode=Qt.KeepAspectRatio, transformMode=Qt.SmoothTransformation) # To scale image for example and keep its Aspect Ration label_Image.setPixmap(QPixmap.fromImage(image_profile)) layout.addWidget(new_frame) # the matplotlib canvas dialog.resize(985, 1000) def openFile(self, dialog): qfDialog = QFileDialog() filename = qfDialog.getOpenFileName(dialog, 'Open File', '.') filepath = str(filename[0]) return filepath def callTree(self, filepath, dialog, layout, view, model, patientBrowser, planBrowser): # Clear all values before uploading the file model.clear() model.setHorizontalHeaderLabels(['Name', 'Value']) patientBrowser.clear() planBrowser.clear() dialog.resize(985, 520) for i in reversed(range(layout.count())): layout.itemAt(i).widget().setParent(None) # Set Filename as a label in Dialog box title = filepath.rsplit("/")[-1] self.btnlabel.setText(QApplication.translate("Dialog", title, None, QApplication.UnicodeUTF8)) # Call Show Tree method self.showTree(filepath, dialog, layout, view, model, patientBrowser, planBrowser) def triggerEvent(self, dialog, layout, view, model, patientBrowser, planBrowser): filepath = self.openFile(dialog) self.callTree(filepath, dialog, layout, view, model, patientBrowser, planBrowser)
[docs] def dicomtree(self): """ Show dicom tree window """ dicomDialog = QDialog(self.parent) dicomUi = Dicom_Dialog() dicomUi.setupUi(dicomDialog) sizePolicy = QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(dicomDialog.sizePolicy().hasHeightForWidth()) dicomDialog.setSizePolicy(sizePolicy) qbtn = QPushButton('Upload DICOM File', dicomDialog) qbtn.resize(921, 51) qbtn.move(30, 20) self.btnlabel = QLabel(dicomDialog) self.btnlabel.setGeometry(QRect(30, 80, 921, 20)) self.btnlabel.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter) self.btnlabel.setObjectName("Upload file title") self.label1 = QLabel(dicomDialog) self.label1.setGeometry(QRect(30, 330, 100, 31)) self.label1.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) self.label1.setObjectName("Patient") self.label1.setText(QApplication.translate("Dialog", "Patient Info:", None, QApplication.UnicodeUTF8)) self.label2 = QLabel(dicomDialog) self.label2.setGeometry(QRect(500, 330, 100, 31)) self.label2.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) self.label2.setObjectName("Plan") self.label2.setText(QApplication.translate("Dialog", "Plan Info:", None, QApplication.UnicodeUTF8)) dicomDialog.setStyleSheet("QLabel { font-weight:bold; font-size: 15px; }") # init widgets view = QTreeView(dicomDialog) view.setSelectionBehavior(QAbstractItemView.SelectRows) model = QStandardItemModel() model.setHorizontalHeaderLabels(['Name', 'Value']) view.setModel(model) view.setUniformRowHeights(False) view.header().setResizeMode(QHeaderView.ResizeToContents) view.resize(921, 220) view.move(30, 110) view.show() patientBrowser = QTextBrowser(dicomDialog) patientBrowser.setGeometry(QRect(30, 360, 450, 80)) patientBrowser.show() planBrowser = QTextBrowser(dicomDialog) planBrowser.setGeometry(QRect(500, 360, 450, 80)) planBrowser.show() frame = QFrame(dicomDialog) frame.setGeometry(QRect(30, 450, 921, 650)) layout = QVBoxLayout(frame) frame.show() qbtn.clicked.connect(lambda: self.triggerEvent(dicomDialog, layout, view, model, patientBrowser, planBrowser)) dicomDialog.show()
if __name__ == "__main__": app = QApplication(sys.argv) form = LicenseAgreement() form.show() sys.exit(app.exec_())