├── .github └── FUNDING.yml ├── .gitignore ├── MANIFEST.in ├── README.md ├── dicompyler ├── __init__.py ├── baseplugins │ ├── 2dview.py │ ├── 2dview.xrc │ ├── __init__.py │ ├── anonymize.py │ ├── anonymize.xrc │ ├── dvh.py │ ├── dvh.xrc │ ├── quickopen.py │ ├── treeview.py │ └── treeview.xrc ├── credits.txt ├── dicomgui.py ├── dvhdata.py ├── guidvh.py ├── guiutil.py ├── license.txt ├── main.py ├── plugin.py ├── preferences.py ├── resources │ ├── accept.png │ ├── book.png │ ├── bricks.png │ ├── chart_bar.png │ ├── chart_bar_error.png │ ├── chart_curve.png │ ├── chart_curve_error.png │ ├── cog.png │ ├── contrast_high.png │ ├── dicomgui.xrc │ ├── dicompyler.icns │ ├── dicompyler.ico │ ├── dicompyler_icon11_16.png │ ├── dicompyler_logo.png │ ├── error.png │ ├── folder_image.png │ ├── folder_user.png │ ├── group.png │ ├── guiutil.xrc │ ├── magnifier_zoom_in.png │ ├── magnifier_zoom_out.png │ ├── main.xrc │ ├── pencil.png │ ├── pencil_error.png │ ├── plugin.png │ ├── plugin.xrc │ ├── plugin_disabled.png │ ├── preferences.xrc │ ├── table_multiple.png │ └── user.png ├── util.py └── wxmpl.py ├── dicompyler_app.py ├── distribute_setup.py ├── requirements.txt ├── scripts └── dicompyler └── setup.py /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | patreon: bastula 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include distribute_setup.py 2 | include dicompyler_app.py 3 | include dicompyler/*.txt 4 | include dicompyler/baseplugins/*.xrc 5 | include dicompyler/resources/* 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | dicompyler 2 | ============ 3 | 4 | **NOTE**: dicompyler has been archived and is no longer supported. The developers of dicompyler and DVH Analytics have teamed up to form [ONCurate](https://www.oncurate.com). 5 | 6 | dicompyler screenshot 7 | dicompyler is an extensible open source radiation therapy research platform based on the DICOM standard. It also functions as a cross-platform DICOM RT viewer. 8 | 9 | dicompyler is written in Python and is built on a number of technologies including: [pydicom](https://github.com/pydicom/pydicom), [wxPython](http://www.wxpython.org), [Pillow](http://python-pillow.org/), and [matplotlib](http://matplotlib.org) and runs on Windows, Mac OS X and Linux. 10 | 11 | dicompyler is released under a BSD [license](dicompyler/license.txt). 12 | 13 | Take a tour of dicompyler by checking out some [screenshots](https://github.com/bastula/dicompyler/wiki/Screenshots) or download a copy today. 14 | 15 | ![alt text](https://img.shields.io/pypi/v/dicompyler.svg "pypi version") ![alt text](https://img.shields.io/pypi/dm/dicompyler.svg "pypi version") 16 | --- 17 | 18 | Downloads: 19 | ---------- 20 | Downloads are available through Google Drive:
21 |
22 | Version 0.4.2: [Windows](https://bit.ly/dicompylerwindows) | [Mac](https://bit.ly/dicompylermac) | [Source](https://pypi.python.org/packages/source/d/dicompyler/dicompyler-0.4.2.tar.gz#md5=adbfa47b07f983f17fdba26a1442fce0) | [Test Data](https://bit.ly/dicompylertestdata) - Released July 15th, 2014 - [Release Notes](https://github.com/bastula/dicompyler/wiki/ReleaseNotes) 23 | 24 | Features: 25 | --------- 26 | * Import CT/MR/PET Images, DICOM RT structure set, RT dose and RT plan files 27 | * Extensible plugin system with included plugins: 28 | * 2D image viewer with dose and structure overlay 29 | * Dose volume histogram viewer with the ability to analyze DVH parameters 30 | * DICOM data tree viewer 31 | * Patient anonymizer 32 | * 3rd-party plugins can be found at [https://github.com/dicompyler/dicompyler-plugins](https://github.com/dicompyler/dicompyler-plugins) 33 | * Custom plugins can be written by following the [Plugin development guide](https://github.com/bastula/dicompyler/wiki/PluginDevelopmentGuide) 34 | 35 | For upcoming features, see the [project roadmap](https://github.com/bastula/dicompyler/wiki/Roadmap). 36 | 37 | System Requirements: 38 | -------------------- 39 | * Windows XP/Vista/7/8/10 40 | * Mac OS X 10.5 - 10.13 (Intel) - Must bypass [Gatekeeper](https://support.apple.com/en-us/HT202491) 41 | * Linux - via a package from [PyPI](https://pypi.python.org/pypi/dicompyler) or a [Debian package](https://packages.debian.org/sid/dicompyler) (courtesy of debian-med) 42 | 43 | If you are interested in building from source, please check out the [build instructions](https://github.com/bastula/dicompyler/wiki/BuildRequirements). 44 | 45 | Getting Started: 46 | ---------------- 47 | 48 | * How to run dicompyler: 49 | * If you have downloaded dicompyler as an application for Windows or Mac, please 50 | follow the normal process for running any other application on your system. 51 | 52 | * If you are running from a Python package, a script called "dicompyler" will now 53 | be present on your path, which you can run from your command line or terminal. 54 | 55 | * If you are running from a source checkout, there is a script in the main folder 56 | called "dicompyler_app.py" which can be executed via your Python interpreter. 57 | 58 | dicompyler will read properly formatted DICOM and DICOM-RT files. To get 59 | started, run dicompyler and click "Open Patient" to bring up a dialog box that 60 | will show the DICOM files in the last selected directory. You may click 61 | "Browse..." to navigate to other folders that contain DICOM data. 62 | 63 | In the current version of dicompyler, you can import any DICOM CT, PET, 64 | or MRI image series, DICOM RT structure set, RT dose and RT plan files. 65 | dicompyler will automatically highlight the most dependent item for the patient. 66 | All related items (up the tree) will be automatically imported as well. 67 | 68 | Alternatively, you can selectively import data. For example, If you only want 69 | to import CT images and an RT structure set just highlight the RT structure set. 70 | If you are importing an RT dose file and the corresponding plan does not 71 | contain a prescription dose, enter one in the box first. To import the data, 72 | click "Select" and dicompyler will process the information. 73 | 74 | Once the DICOM data has been loaded, the main window will show the patient and 75 | plan information. Additionally it will show a list of structures and isodoses 76 | that are associated with the plan. 77 | 78 | Getting Help: 79 | ------------- 80 | * As a starting point, please read the [FAQ](https://github.com/bastula/dicompyler/wiki/FAQ) as it answers the most commonly asked questions about dicompyler. 81 | * If you are unable to find the answer in the FAQ or in the [wiki](https://github.com/bastula/dicompyler/wiki), dicompyler has a [discussion forum](https://groups.google.com/group/dicompyler) hosted on Google Groups. 82 | 83 | Citing dicompyler: 84 | ------------------ 85 | * If you need to cite dicompyler as a reference in your publication, please use the following citation: 86 | * **A Panchal and R Keyes**. "SU-GG-T-260: dicompyler: An Open Source Radiation Therapy Research Platform with a Plugin Architecture" Med. Phys. 37, 3245, 2010 87 | * The reference in Medical Physics can be accessed via [http://dx.doi.org/10.1118/1.3468652](http://dx.doi.org/10.1118/1.3468652) 88 | -------------------------------------------------------------------------------- /dicompyler/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # __init__.py 4 | """Package initialization for dicompyler.""" 5 | # Copyright (c) 2009-2017 Aditya Panchal 6 | # This file is part of dicompyler, relased under a BSD license. 7 | # See the file license.txt included with this distribution, also 8 | # available at https://github.com/bastula/dicompyler/ 9 | 10 | __author__ = 'Aditya Panchal' 11 | __email__ = 'apanchal@bastula.org' 12 | __version__ = '0.5.0' 13 | __version_info__ = (0, 5, 0) 14 | 15 | 16 | from dicompyler.main import start 17 | 18 | if __name__ == '__main__': 19 | import dicompyler.main 20 | dicompyler.main.start() -------------------------------------------------------------------------------- /dicompyler/baseplugins/2dview.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 2dview.py 4 | """dicompyler plugin that displays images, structures and dose in 2D planes.""" 5 | # Copyright (c) 2009-2017 Aditya Panchal 6 | # This file is part of dicompyler, released under a BSD license. 7 | # See the file license.txt included with this distribution, also 8 | # available at https://github.com/bastula/dicompyler/ 9 | # 10 | 11 | import wx 12 | from wx.xrc import XmlResource, XRCCTRL, XRCID 13 | from wx.lib.pubsub import pub 14 | from matplotlib import _cntr as cntr 15 | from matplotlib import __version__ as mplversion 16 | import numpy as np 17 | from dicompyler import guiutil, util 18 | 19 | def pluginProperties(): 20 | """Properties of the plugin.""" 21 | 22 | props = {} 23 | props['name'] = '2D View' 24 | props['description'] = "Display image, structure and dose data in 2D" 25 | props['author'] = 'Aditya Panchal' 26 | props['version'] = "0.5.0" 27 | props['plugin_type'] = 'main' 28 | props['plugin_version'] = 1 29 | props['min_dicom'] = ['images'] 30 | props['recommended_dicom'] = ['images', 'rtss', 'rtdose'] 31 | 32 | return props 33 | 34 | def pluginLoader(parent): 35 | """Function to load the plugin.""" 36 | 37 | # Load the XRC file for our gui resources 38 | res = XmlResource(util.GetBasePluginsPath('2dview.xrc')) 39 | 40 | panel2DView = res.LoadPanel(parent, 'plugin2DView') 41 | panel2DView.Init(res) 42 | 43 | return panel2DView 44 | 45 | class plugin2DView(wx.Panel): 46 | """Plugin to display DICOM image, RT Structure, RT Dose in 2D.""" 47 | 48 | def __init__(self): 49 | wx.Panel.__init__(self) 50 | 51 | def Init(self, res): 52 | """Method called after the panel has been initialized.""" 53 | 54 | # Bind ui events to the proper methods 55 | self.Bind(wx.EVT_PAINT, self.OnPaint) 56 | self.Bind(wx.EVT_SIZE, self.OnSize) 57 | self.Bind(wx.EVT_WINDOW_DESTROY, self.OnDestroy) 58 | 59 | # Initialize variables 60 | self.images = [] 61 | self.structures = {} 62 | self.window = 0 63 | self.level = 0 64 | self.zoom = 1 65 | self.pan = [0, 0] 66 | self.bwidth = 0 67 | self.bheight = 0 68 | self.xpos = 0 69 | self.ypos = 0 70 | self.mousepos = wx.Point(-10000, -10000) 71 | self.mouse_in_window = False 72 | self.isodose_line_style = 'Solid' 73 | self.isodose_fill_opacity = 25 74 | self.structure_line_style = 'Solid' 75 | self.structure_fill_opacity = 50 76 | self.plugins = {} 77 | 78 | # Setup toolbar controls 79 | if guiutil.IsGtk(): 80 | drawingstyles = ['Solid', 'Transparent', 'Dot'] 81 | else: 82 | drawingstyles = ['Solid', 'Transparent', 'Dot', 'Dash', 'Dot Dash'] 83 | zoominbmp = wx.Bitmap(util.GetResourcePath('magnifier_zoom_in.png')) 84 | zoomoutbmp = wx.Bitmap(util.GetResourcePath('magnifier_zoom_out.png')) 85 | toolsbmp = wx.Bitmap(util.GetResourcePath('cog.png')) 86 | self.tools = [] 87 | self.tools.append({'label':"Zoom In", 'bmp':zoominbmp, 'shortHelp':"Zoom In", 'eventhandler':self.OnZoomIn}) 88 | self.tools.append({'label':"Zoom Out", 'bmp':zoomoutbmp, 'shortHelp':"Zoom Out", 'eventhandler':self.OnZoomOut}) 89 | self.tools.append({'label':"Tools", 'bmp':toolsbmp, 'shortHelp':"Tools", 'eventhandler':self.OnToolsMenu}) 90 | 91 | # Set up preferences 92 | self.preferences = [ 93 | {'Drawing Settings': 94 | [{'name':'Isodose Line Style', 95 | 'type':'choice', 96 | 'values':drawingstyles, 97 | 'default':'Solid', 98 | 'callback':'2dview.drawingprefs.isodose_line_style'}, 99 | {'name':'Isodose Fill Opacity', 100 | 'type':'range', 101 | 'values':[0, 100], 102 | 'default':25, 103 | 'units':'%', 104 | 'callback':'2dview.drawingprefs.isodose_fill_opacity'}, 105 | {'name':'Structure Line Style', 106 | 'type':'choice', 107 | 'values':drawingstyles, 108 | 'default':'Solid', 109 | 'callback':'2dview.drawingprefs.structure_line_style'}, 110 | {'name':'Structure Fill Opacity', 111 | 'type':'range', 112 | 'values':[0, 100], 113 | 'default':50, 114 | 'units':'%', 115 | 'callback':'2dview.drawingprefs.structure_fill_opacity'}] 116 | }] 117 | 118 | # Set up pubsub 119 | pub.subscribe(self.OnUpdatePatient, 'patient.updated.parsed_data') 120 | pub.subscribe(self.OnStructureCheck, 'structures.checked') 121 | pub.subscribe(self.OnIsodoseCheck, 'isodoses.checked') 122 | pub.subscribe(self.OnRefresh, '2dview.refresh') 123 | pub.subscribe(self.OnDrawingPrefsChange, '2dview.drawingprefs') 124 | pub.subscribe(self.OnPluginLoaded, 'plugin.loaded.2dview') 125 | pub.sendMessage('preferences.requested.values', msg='2dview.drawingprefs') 126 | 127 | def OnUpdatePatient(self, msg): 128 | """Update and load the patient data.""" 129 | 130 | self.z = 0 131 | self.structurepixlut = ([], []) 132 | self.dosepixlut = ([], []) 133 | if 'images' in msg: 134 | self.images = msg['images'] 135 | self.imagenum = 1 136 | # If more than one image, set first image to middle of the series 137 | if (len(self.images) > 1): 138 | self.imagenum = int(len(self.images)/2) 139 | image = self.images[self.imagenum-1] 140 | self.structurepixlut = image.GetPatientToPixelLUT() 141 | # Determine the default window and level of the series 142 | self.window, self.level = image.GetDefaultImageWindowLevel() 143 | # Dose display depends on whether we have images loaded or not 144 | self.isodoses = {} 145 | if ('dose' in msg and \ 146 | ("PixelData" in msg['dose'].ds)): 147 | self.dose = msg['dose'] 148 | self.dosedata = self.dose.GetDoseData() 149 | # First get the dose grid LUT 150 | doselut = self.dose.GetPatientToPixelLUT() 151 | # Then convert dose grid LUT into an image pixel LUT 152 | self.dosepixlut = self.GetDoseGridPixelData(self.structurepixlut, doselut) 153 | else: 154 | self.dose = [] 155 | if 'plan' in msg: 156 | self.rxdose = msg['plan']['rxdose'] 157 | else: 158 | self.rxdose = 0 159 | else: 160 | self.images = [] 161 | 162 | self.SetBackgroundColour(wx.Colour(0, 0, 0)) 163 | # Set the focus to this panel so we can capture key events 164 | self.SetFocus() 165 | self.OnFocus() #Workaround on Windows. ST 166 | self.Refresh() 167 | 168 | def OnFocus(self): 169 | """Bind to certain events when the plugin is focused.""" 170 | 171 | # Bind keyboard and mouse events 172 | self.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown) 173 | self.Bind(wx.EVT_MOUSEWHEEL, self.OnMouseWheel) 174 | self.Bind(wx.EVT_LEFT_DOWN, self.OnMouseDown) 175 | self.Bind(wx.EVT_LEFT_UP, self.OnMouseUp) 176 | self.Bind(wx.EVT_RIGHT_DOWN, self.OnMouseDown) 177 | self.Bind(wx.EVT_RIGHT_UP, self.OnMouseUp) 178 | self.Bind(wx.EVT_ENTER_WINDOW, self.OnMouseEnter) 179 | self.Bind(wx.EVT_LEAVE_WINDOW, self.OnMouseLeave) 180 | if guiutil.IsMSWindows(): 181 | pub.subscribe(self.OnKeyDown, 'main.key_down') 182 | pub.subscribe(self.OnMouseWheel, 'main.mousewheel') 183 | 184 | def OnUnfocus(self): 185 | """Unbind to certain events when the plugin is unfocused.""" 186 | 187 | # Unbind keyboard and mouse events 188 | self.Unbind(wx.EVT_KEY_DOWN) 189 | self.Unbind(wx.EVT_MOUSEWHEEL) 190 | self.Unbind(wx.EVT_LEFT_DOWN) 191 | self.Unbind(wx.EVT_LEFT_UP) 192 | self.Unbind(wx.EVT_RIGHT_DOWN) 193 | self.Unbind(wx.EVT_RIGHT_UP) 194 | self.Unbind(wx.EVT_MOTION) 195 | if guiutil.IsMSWindows(): 196 | pub.unsubscribe(self.OnKeyDown, 'main.key_down') 197 | pub.unsubscribe(self.OnMouseWheel, 'main.mousewheel') 198 | pub.unsubscribe(self.OnRefresh, '2dview.refresh') 199 | 200 | def OnDestroy(self, evt): 201 | """Unbind to all events before the plugin is destroyed.""" 202 | 203 | pub.unsubscribe(self.OnUpdatePatient, 'patient.updated.parsed_data') 204 | pub.unsubscribe(self.OnStructureCheck, 'structures.checked') 205 | pub.unsubscribe(self.OnIsodoseCheck, 'isodoses.checked') 206 | pub.unsubscribe(self.OnDrawingPrefsChange, '2dview.drawingprefs') 207 | pub.unsubscribe(self.OnPluginLoaded, 'plugin.loaded.2dview') 208 | # self.OnUnfocus() 209 | 210 | def OnStructureCheck(self, msg): 211 | """When the structure list changes, update the panel.""" 212 | 213 | self.structures = msg 214 | self.SetFocus() 215 | self.Refresh() 216 | 217 | def OnIsodoseCheck(self, msg): 218 | """When the isodose list changes, update the panel.""" 219 | 220 | self.isodoses = msg 221 | self.SetFocus() 222 | self.Refresh() 223 | 224 | def OnDrawingPrefsChange(self, topic, msg): 225 | """When the drawing preferences change, update the drawing styles.""" 226 | topic = topic.split('.') 227 | if (topic[1] == 'isodose_line_style'): 228 | self.isodose_line_style = msg 229 | elif (topic[1] == 'isodose_fill_opacity'): 230 | self.isodose_fill_opacity = msg 231 | elif (topic[1] == 'structure_line_style'): 232 | self.structure_line_style = msg 233 | elif (topic[1] == 'structure_fill_opacity'): 234 | self.structure_fill_opacity = msg 235 | self.Refresh() 236 | 237 | def OnPluginLoaded(self, msg): 238 | """When a 2D View-dependent plugin is loaded, initialize the plugin.""" 239 | 240 | name = msg.pluginProperties()['name'] 241 | self.plugins[name] = msg.plugin(self) 242 | 243 | def DrawStructure(self, structure, gc, position, prone, feetfirst): 244 | """Draw the given structure on the panel.""" 245 | 246 | # Create an indexing array of z positions of the structure data 247 | # to compare with the image z position 248 | if not "zarray" in structure: 249 | structure['zarray'] = np.array( 250 | list(structure['planes'].keys()), dtype=np.float32) 251 | structure['zkeys'] = structure['planes'].keys() 252 | 253 | # Return if there are no z positions in the structure data 254 | if not len(structure['zarray']): 255 | return 256 | 257 | # Determine the closest z plane to the given position 258 | zmin = np.amin(np.abs(structure['zarray'] - float(position))) 259 | index = np.argmin(np.abs(structure['zarray'] - float(position))) 260 | 261 | # Draw the structure only if the structure has contours 262 | # on the closest plane, within a threshold 263 | if (zmin < 0.5): 264 | # Set the color of the contour 265 | color = wx.Colour(structure['color'][0], structure['color'][1], 266 | structure['color'][2], int(self.structure_fill_opacity*255/100)) 267 | # Set fill (brush) color, transparent for external contour 268 | if (('type' in structure) and (structure['type'].lower() == 'external')): 269 | gc.SetBrush(wx.Brush(color, style=wx.TRANSPARENT)) 270 | else: 271 | gc.SetBrush(wx.Brush(color)) 272 | gc.SetPen(wx.Pen(tuple(structure['color']), 273 | style=self.GetLineDrawingStyle(self.structure_line_style))) 274 | # Create the path for the contour 275 | path = gc.CreatePath() 276 | for contour in structure['planes'][list(structure['zkeys'])[index]]: 277 | if (contour['type'] == u"CLOSED_PLANAR"): 278 | # Convert the structure data to pixel data 279 | pixeldata = self.GetContourPixelData( 280 | self.structurepixlut, contour['data'], prone, feetfirst) 281 | 282 | # Move the origin to the last point of the contour 283 | point = pixeldata[-1] 284 | path.MoveToPoint(point[0], point[1]) 285 | 286 | # Add each contour point to the path 287 | for point in pixeldata: 288 | path.AddLineToPoint(point[0], point[1]) 289 | # Close the subpath in preparation for the next contour 290 | path.CloseSubpath() 291 | # Draw the path 292 | gc.DrawPath(path) 293 | 294 | def DrawIsodose(self, isodose, gc, isodosegen): 295 | """Draw the given structure on the panel.""" 296 | 297 | # Calculate the isodose level according to rx dose and dose grid scaling 298 | level = isodose['data']['level'] * self.rxdose / (self.dosedata['dosegridscaling'] * 10000) 299 | contours = isodosegen.trace(level) 300 | # matplotlib 1.0.0 and above returns vertices and segments, but we only need vertices 301 | if (mplversion >= "1.0.0"): 302 | contours = contours[:len(contours)//2] 303 | if len(contours): 304 | 305 | # Set the color of the isodose line 306 | color = wx.Colour(isodose['color'][0], isodose['color'][1], 307 | isodose['color'][2], int(self.isodose_fill_opacity*255/100)) 308 | gc.SetBrush(wx.Brush(color)) 309 | gc.SetPen(wx.Pen(tuple(isodose['color']), 310 | style=self.GetLineDrawingStyle(self.isodose_line_style))) 311 | 312 | # Create the drawing path for the isodose line 313 | path = gc.CreatePath() 314 | # Draw each contour for the isodose line 315 | for c in contours: 316 | # Move the origin to the last point of the contour 317 | path.MoveToPoint( 318 | self.dosepixlut[0][int(c[-1][0])]+1, self.dosepixlut[1][int(c[-1][1])]+1) 319 | # Add a line to the rest of the points 320 | # Note: draw every other point since there are too many points 321 | for p in c[::2]: 322 | path.AddLineToPoint( 323 | self.dosepixlut[0][int(p[0])]+1, self.dosepixlut[1][int(p[1])]+1) 324 | # Close the subpath in preparation for the next contour 325 | path.CloseSubpath() 326 | # Draw the final isodose path 327 | gc.DrawPath(path) 328 | 329 | def GetLineDrawingStyle(self, style): 330 | """Convert the stored line drawing style into wxWidgets pen drawing format.""" 331 | 332 | styledict = {'Solid':wx.SOLID, 333 | 'Transparent':wx.TRANSPARENT, 334 | 'Dot':wx.DOT, 335 | 'Dash':wx.SHORT_DASH, 336 | 'Dot Dash':wx.DOT_DASH} 337 | return styledict[style] 338 | 339 | def GetContourPixelData(self, pixlut, contour, prone = False, feetfirst = False): 340 | """Convert structure data into pixel data using the patient to pixel LUT.""" 341 | 342 | pixeldata = [] 343 | # For each point in the structure data 344 | # look up the value in the LUT and find the corresponding pixel pair 345 | for p, point in enumerate(contour): 346 | for xv, xval in enumerate(pixlut[0]): 347 | if (xval > point[0] and not prone and not feetfirst): 348 | break 349 | elif (xval < point[0]): 350 | if feetfirst or prone: 351 | break 352 | for yv, yval in enumerate(pixlut[1]): 353 | if (yval > point[1] and not prone): 354 | break 355 | elif (yval < point[1] and prone): 356 | break 357 | pixeldata.append((xv, yv)) 358 | 359 | return pixeldata 360 | 361 | def GetDoseGridPixelData(self, pixlut, doselut): 362 | """Convert dosegrid data into pixel data using the dose to pixel LUT.""" 363 | 364 | dosedata = [] 365 | x = [] 366 | y = [] 367 | # Determine if the patient is prone or supine 368 | imdata = self.images[self.imagenum-1].GetImageData() 369 | prone = -1 if 'p' in imdata['patientposition'].lower() else 1 370 | feetfirst = -1 if 'ff' in imdata['patientposition'].lower() else 1 371 | # Get the pixel spacing 372 | spacing = imdata['pixelspacing'] 373 | 374 | # Transpose the dose grid LUT onto the image grid LUT 375 | x = (np.array(doselut[0]) - pixlut[0][0]) * prone * feetfirst / spacing[0] 376 | y = (np.array(doselut[1]) - pixlut[1][0]) * prone / spacing[1] 377 | return (x, y) 378 | 379 | def OnPaint(self, evt): 380 | """Update the panel when it needs to be refreshed.""" 381 | 382 | # Bind motion event when the panel has been painted to avoid a blank 383 | # image on Windows if a file is loaded too quickly before the plugin 384 | # is initialized 385 | self.Bind(wx.EVT_MOTION, self.OnMouseMotion) 386 | 387 | # Special case for Windows to account for flickering 388 | # if and only if images are loaded 389 | if (guiutil.IsMSWindows() and len(self.images)): 390 | dc = wx.BufferedPaintDC(self) 391 | self.SetBackgroundStyle(wx.BG_STYLE_CUSTOM) 392 | else: 393 | dc = wx.PaintDC(self) 394 | 395 | width, height = self.GetClientSize() 396 | try: 397 | gc = wx.GraphicsContext.Create(dc) 398 | except NotImplementedError: 399 | dc.DrawText("This build of wxPython does not support the " 400 | "wx.GraphicsContext family of classes.", 401 | 25, 25) 402 | return 403 | 404 | # If we have images loaded, process and show the image 405 | if len(self.images): 406 | # Save the original drawing state 407 | gc.PushState() 408 | # Scale the image by the zoom factor 409 | gc.Scale(self.zoom, self.zoom) 410 | 411 | # Redraw the background on Windows 412 | if guiutil.IsMSWindows(): 413 | gc.SetBrush(wx.Brush(wx.Colour(0, 0, 0))) 414 | gc.SetPen(wx.Pen(wx.Colour(0, 0, 0))) 415 | gc.DrawRectangle(0, 0, width, height) 416 | 417 | image = guiutil.convert_pil_to_wx( 418 | self.images[self.imagenum-1].GetImage(self.window, self.level)) 419 | bmp = wx.Bitmap(image) 420 | self.bwidth, self.bheight = image.GetSize() 421 | 422 | # Center the image 423 | transx = self.pan[0]+(width-self.bwidth*self.zoom)/(2*self.zoom) 424 | transy = self.pan[1]+(height-self.bheight*self.zoom)/(2*self.zoom) 425 | gc.Translate(transx, transy) 426 | gc.DrawBitmap(bmp, 0, 0, self.bwidth, self.bheight) 427 | gc.SetBrush(wx.Brush(wx.Colour(0, 0, 255, 30))) 428 | gc.SetPen(wx.Pen(wx.Colour(0, 0, 255, 30))) 429 | 430 | # Draw the structures if present 431 | imdata = self.images[self.imagenum-1].GetImageData() 432 | self.z = '%.2f' % imdata['position'][2] 433 | 434 | # Determine whether the patient is prone or supine 435 | if 'p' in imdata['patientposition'].lower(): 436 | prone = True 437 | else: 438 | prone = False 439 | # Determine whether the patient is feet first or head first 440 | if 'ff' in imdata['patientposition'].lower(): 441 | feetfirst = True 442 | else: 443 | feetfirst = False 444 | for id, structure in self.structures.items(): 445 | self.DrawStructure(structure, gc, self.z, prone, feetfirst) 446 | 447 | # Draw the isodoses if present 448 | if len(self.isodoses): 449 | grid = self.dose.GetDoseGrid(float(self.z)) 450 | if not (grid == []): 451 | x, y = np.meshgrid( 452 | np.arange(grid.shape[1]), np.arange(grid.shape[0])) 453 | # Instantiate the isodose generator for this slice 454 | isodosegen = cntr.Cntr(x, y, grid) 455 | for id, isodose in iter(sorted(self.isodoses.items())): 456 | self.DrawIsodose(isodose, gc, isodosegen) 457 | 458 | # Restore the translation and scaling 459 | gc.PopState() 460 | 461 | # Prepare the font for drawing the information text 462 | font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT) 463 | if guiutil.IsMac(): 464 | font.SetPointSize(10) 465 | gc.SetFont(font, wx.WHITE) 466 | 467 | # Draw the information text 468 | imtext = "Image: " + str(self.imagenum) + "/" + str(len(self.images)) 469 | te = gc.GetFullTextExtent(imtext) 470 | gc.DrawText(imtext, 10, 7) 471 | impos = "Position: " + str(self.z) + " mm" 472 | gc.DrawText(impos, 10, 7+te[1]*1.1) 473 | if ("%.3f" % self.zoom == "1.000"): 474 | zoom = "1" 475 | else: 476 | zoom = "%.3f" % self.zoom 477 | imzoom = "Zoom: " + zoom + ":1" 478 | gc.DrawText(imzoom, 10, height-17) 479 | imsize = "Image Size: " + str(self.bheight) + "x" + str(self.bwidth) + " px" 480 | gc.DrawText(imsize, 10, height-17-te[1]*1.1) 481 | imwinlevel = "W/L: " + str(self.window) + ' / ' + str(self.level) 482 | te = gc.GetFullTextExtent(imwinlevel) 483 | gc.DrawText(imwinlevel, width-te[0]-7, 7) 484 | impatpos = "Patient Position: " + imdata['patientposition'] 485 | te = gc.GetFullTextExtent(impatpos) 486 | gc.DrawText(impatpos, width-te[0]-7, height-17) 487 | 488 | # Send message with the current image number and various properties 489 | pub.sendMessage('2dview.updated.image', 490 | msg={'number':self.imagenum, # slice number 491 | 'z':self.z, # slice location 492 | 'window':self.window, # current window value 493 | 'level':self.level, # curent level value 494 | 'gc':gc, # wx.GraphicsContext 495 | 'scale':self.zoom, # current zoom level 496 | 'transx':transx, # current x translation 497 | 'transy':transy, # current y translation 498 | 'imdata':imdata, # image data dictionary 499 | 'patientpixlut':self.structurepixlut}) 500 | # pat to pixel coord LUT 501 | 502 | def OnSize(self, evt): 503 | """Refresh the view when the size of the panel changes.""" 504 | 505 | self.Refresh() 506 | evt.Skip() 507 | 508 | def OnRefresh(self, msg): 509 | """Refresh the view when it is requested by a plugin.""" 510 | 511 | self.Refresh() 512 | 513 | def OnUpdatePositionValues(self, evt=None): 514 | """Update the current position and value(s) of the mouse cursor.""" 515 | 516 | if (evt == None): 517 | pos = np.array(self.mousepos) 518 | else: 519 | pos = np.array(evt.GetPosition()) 520 | 521 | # On the Mac, the cursor position is shifted by 1 pixel to the left 522 | if guiutil.IsMac(): 523 | pos = pos - 1 524 | 525 | # Determine the coordinates with respect to the current zoom and pan 526 | w, h = self.GetClientSize() 527 | xpos = int(pos[0]/self.zoom-self.pan[0]-(w-self.bwidth*self.zoom)/ 528 | (2*self.zoom)) 529 | ypos = int(pos[1]/self.zoom-self.pan[1]-(h-self.bheight*self.zoom)/ 530 | (2*self.zoom)) 531 | 532 | # Save the coordinates so they can be used by the 2dview plugins 533 | self.xpos = xpos 534 | self.ypos = ypos 535 | 536 | # Set an empty text placeholder if the coordinates are not within range 537 | text = "" 538 | value = "" 539 | # Skip processing if images are not loaded 540 | if not len(self.images): 541 | pub.sendMessage('main.update_statusbar', msg={1:text, 2:value}) 542 | # Only display if the mouse coordinates are within the image size range 543 | if ((0 <= xpos < len(self.structurepixlut[0])) and 544 | (0 <= ypos < len(self.structurepixlut[1])) and self.mouse_in_window): 545 | text = "X: " + str('%.2f' % self.structurepixlut[0][xpos]) + \ 546 | " mm Y: " + str('%.2f' % self.structurepixlut[1][ypos]) + \ 547 | " mm / X: " + str(xpos) + \ 548 | " px Y:" + str(ypos) + " px" 549 | 550 | # Lookup the current image and find the value of the current pixel 551 | image = self.images[self.imagenum-1] 552 | # Rescale the slope and intercept of the image if present 553 | if ('RescaleIntercept' in image.ds and 554 | 'RescaleSlope' in image.ds): 555 | pixel_array = image.ds.pixel_array*image.ds.RescaleSlope + \ 556 | image.ds.RescaleIntercept 557 | else: 558 | pixel_array = image.ds.pixel_array 559 | value = "Value: " + str(pixel_array[ypos, xpos]) 560 | 561 | # Lookup the current dose plane and find the value of the current 562 | # pixel, if the dose has been loaded 563 | if not (self.dose == []): 564 | xdpos = np.argmin(np.fabs(np.array(self.dosepixlut[0]) - xpos)) 565 | ydpos = np.argmin(np.fabs(np.array(self.dosepixlut[1]) - ypos)) 566 | dosegrid = self.dose.GetDoseGrid(float(self.z)) 567 | if not (dosegrid == []): 568 | dose = dosegrid[ydpos, xdpos] * \ 569 | self.dosedata['dosegridscaling'] 570 | value = value + " / Dose: " + \ 571 | str('%.4g' % dose) + " Gy / " + \ 572 | str('%.4g' % float(dose*10000/self.rxdose)) + " %" 573 | # Send a message with the text to the 2nd and 3rd statusbar sections 574 | pub.sendMessage('main.update_statusbar', msg={1:text, 2:value}) 575 | 576 | def OnZoomIn(self, evt): 577 | """Zoom the view in.""" 578 | 579 | self.zoom = self.zoom * 1.1 580 | self.Refresh() 581 | 582 | def OnZoomOut(self, evt): 583 | """Zoom the view out.""" 584 | 585 | if (self.zoom > 1): 586 | self.zoom = self.zoom / 1.1 587 | self.Refresh() 588 | 589 | def OnKeyDown(self, evt): 590 | """Change the image when the user presses the appropriate keys.""" 591 | 592 | if len(self.images): 593 | keyname = evt.GetKeyCode() 594 | prevkey = [wx.WXK_UP, wx.WXK_PAGEUP] 595 | nextkey = [wx.WXK_DOWN, wx.WXK_PAGEDOWN] 596 | zoominkey = [43, 61, 388] # Keys: +, =, Numpad add 597 | zoomoutkey = [45, 95, 390] # Keys: -, _, Numpad subtract 598 | if (keyname in prevkey): 599 | if (self.imagenum > 1): 600 | self.imagenum -= 1 601 | self.Refresh() 602 | if (keyname in nextkey): 603 | if (self.imagenum < len(self.images)): 604 | self.imagenum += 1 605 | self.Refresh() 606 | if (keyname == wx.WXK_HOME): 607 | self.imagenum = 1 608 | self.Refresh() 609 | if (keyname == wx.WXK_END): 610 | self.imagenum = len(self.images) 611 | self.Refresh() 612 | if (keyname in zoominkey): 613 | self.OnZoomIn(None) 614 | if (keyname in zoomoutkey): 615 | self.OnZoomOut(None) 616 | 617 | def OnMouseWheel(self, evt): 618 | """Change the image when the user scrolls the mouse wheel.""" 619 | 620 | if len(self.images): 621 | delta = evt.GetWheelDelta() 622 | rot = evt.GetWheelRotation() 623 | rot = rot/delta 624 | if (rot >= 1): 625 | if (self.imagenum > 1): 626 | self.imagenum -= 1 627 | self.Refresh() 628 | if (rot <= -1): 629 | if (self.imagenum < len(self.images)): 630 | self.imagenum += 1 631 | self.Refresh() 632 | 633 | def OnMouseDown(self, evt): 634 | """Get the initial position of the mouse when dragging.""" 635 | 636 | self.mousepos = evt.GetPosition() 637 | # Publish the coordinates of the cursor position based 638 | # on the scaled image size range 639 | if ((0 <= self.xpos < len(self.structurepixlut[0])) and 640 | (0 <= self.ypos < len(self.structurepixlut[1])) and 641 | (self.mouse_in_window) and 642 | (evt.LeftDown())): 643 | pub.sendMessage('2dview.mousedown', 644 | msg={'x':self.xpos, 645 | 'y':self.ypos, 646 | 'xmm':self.structurepixlut[0][self.xpos], 647 | 'ymm':self.structurepixlut[1][self.ypos]}) 648 | 649 | def OnMouseUp(self, evt): 650 | """Reset the cursor when the mouse is released.""" 651 | 652 | self.SetCursor(wx.Cursor(wx.CURSOR_DEFAULT)) 653 | 654 | def OnMouseEnter(self, evt): 655 | """Set a flag when the cursor enters the window.""" 656 | 657 | self.mouse_in_window = True 658 | self.OnUpdatePositionValues(None) 659 | 660 | def OnMouseLeave(self, evt): 661 | """Set a flag when the cursor leaves the window.""" 662 | 663 | self.mouse_in_window = False 664 | self.OnUpdatePositionValues(None) 665 | 666 | def OnMouseMotion(self, evt): 667 | """Process mouse motion events and pass to the appropriate handler.""" 668 | 669 | if evt.LeftIsDown(): 670 | self.OnLeftIsDown(evt) 671 | self.SetCursor(wx.Cursor(wx.CURSOR_SIZING)) 672 | elif evt.RightIsDown(): 673 | self.OnRightIsDown(evt) 674 | # Custom cursors with > 2 colors only works on Windows currently 675 | if guiutil.IsMSWindows(): 676 | image = wx.Image(util.GetResourcePath('contrast_high.png')) 677 | self.SetCursor(wx.CursorFromImage(image)) 678 | # Update the positon and values of the mouse cursor 679 | self.mousepos = evt.GetPosition() 680 | self.OnUpdatePositionValues(evt) 681 | 682 | def OnLeftIsDown(self, evt): 683 | """Change the image pan when the left mouse button is dragged.""" 684 | 685 | delta = self.mousepos - evt.GetPosition() 686 | self.mousepos = evt.GetPosition() 687 | self.pan[0] -= (delta[0]/self.zoom) 688 | self.pan[1] -= (delta[1]/self.zoom) 689 | self.Refresh() 690 | 691 | def OnRightIsDown(self, evt): 692 | """Change the window/level when the right mouse button is dragged.""" 693 | 694 | delta = self.mousepos - evt.GetPosition() 695 | self.mousepos = evt.GetPosition() 696 | self.window -= delta[0] 697 | self.level -= delta[1] 698 | self.Refresh() 699 | 700 | def OnToolsMenu(self, evt): 701 | """Show a context menu for the loaded 2D View plugins when the 702 | 'Tools' toolbar item is selected.""" 703 | 704 | menu = wx.Menu() 705 | if len(self.plugins): 706 | for name, p in self.plugins.items(): 707 | id = wx.NewId() 708 | self.Bind(wx.EVT_MENU, p.pluginMenu, id=id) 709 | menu.Append(id, name) 710 | else: 711 | id = wx.NewId() 712 | menu.Append(id, "No tools found") 713 | menu.Enable(id, False) 714 | self.PopupMenu(menu) 715 | menu.Destroy() -------------------------------------------------------------------------------- /dicompyler/baseplugins/2dview.xrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /dicompyler/baseplugins/__init__.py: -------------------------------------------------------------------------------- 1 | # __init__.py -------------------------------------------------------------------------------- /dicompyler/baseplugins/anonymize.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # anonymize.py 4 | """dicompyler plugin that anonymizes DICOM / DICOM RT data.""" 5 | # Copyright (c) 2010-2017 Aditya Panchal 6 | # This file is part of dicompyler, released under a BSD license. 7 | # See the file license.txt included with this distribution, also 8 | # available at https://github.com/bastula/dicompyler/ 9 | # 10 | 11 | import wx 12 | from wx.xrc import XmlResource, XRCCTRL, XRCID 13 | from wx.lib.pubsub import pub 14 | import os, threading 15 | from dicompyler import guiutil, util 16 | 17 | def pluginProperties(): 18 | """Properties of the plugin.""" 19 | 20 | props = {} 21 | props['name'] = 'Anonymize' 22 | props['menuname'] = "as Anonymized DICOM" 23 | props['description'] = "Anonymizes DICOM / DICOM RT data" 24 | props['author'] = 'Aditya Panchal' 25 | props['version'] = "0.5.0" 26 | props['plugin_type'] = 'export' 27 | props['plugin_version'] = 1 28 | props['min_dicom'] = [] 29 | props['recommended_dicom'] = ['images', 'rtss', 'rtplan', 'rtdose'] 30 | 31 | return props 32 | 33 | class plugin: 34 | 35 | def __init__(self, parent): 36 | 37 | self.parent = parent 38 | 39 | # Set up pubsub 40 | pub.subscribe(self.OnUpdatePatient, 'patient.updated.raw_data') 41 | 42 | # Load the XRC file for our gui resources 43 | self.res = XmlResource(util.GetBasePluginsPath('anonymize.xrc')) 44 | 45 | def OnUpdatePatient(self, msg): 46 | """Update and load the patient data.""" 47 | 48 | self.data = msg 49 | 50 | def pluginMenu(self, evt): 51 | """Anonymize DICOM / DICOM RT data.""" 52 | 53 | dlgAnonymize = self.res.LoadDialog(self.parent, "AnonymizeDialog") 54 | dlgAnonymize.Init() 55 | 56 | if dlgAnonymize.ShowModal() == wx.ID_OK: 57 | path = dlgAnonymize.path 58 | name = str(dlgAnonymize.name) 59 | patientid = str(dlgAnonymize.patientid) 60 | privatetags = dlgAnonymize.privatetags 61 | 62 | # If the path doesn't exist, create it 63 | if not os.path.exists(path): 64 | os.mkdir(path) 65 | 66 | # Initialize the progress dialog 67 | dlgProgress = guiutil.get_progress_dialog( 68 | wx.GetApp().GetTopWindow(), 69 | "Anonymizing DICOM data...") 70 | # Initialize and start the anonymization thread 71 | self.t=threading.Thread(target=self.AnonymizeDataThread, 72 | args=(self.data, path, name, patientid, privatetags, 73 | dlgProgress.OnUpdateProgress)) 74 | self.t.start() 75 | # Show the progress dialog 76 | dlgProgress.ShowModal() 77 | dlgProgress.Destroy() 78 | 79 | else: 80 | pass 81 | dlgAnonymize.Destroy() 82 | return 83 | 84 | def AnonymizeDataThread(self, data, path, name, patientid, privatetags, 85 | progressFunc): 86 | """Anonmyize and save each DICOM / DICOM RT file.""" 87 | 88 | length = 0 89 | for key in ['rtss', 'rtplan', 'rtdose']: 90 | if key in data: 91 | length = length + 1 92 | if 'images' in data: 93 | length = length + len(data['images']) 94 | 95 | i = 1 96 | if 'rtss' in data: 97 | rtss = data['rtss'] 98 | wx.CallAfter(progressFunc, i, length, 99 | 'Anonymizing file ' + str(i) + ' of ' + str(length)) 100 | self.updateCommonElements(rtss, name, patientid, privatetags) 101 | self.updateElement(rtss, 'SeriesDescription', 'RT Structure Set') 102 | self.updateElement(rtss, 'StructureSetDate', '19010101') 103 | self.updateElement(rtss, 'StructureSetTime', '000000') 104 | if 'RTROIObservations' in rtss: 105 | for item in rtss.RTROIObservations: 106 | self.updateElement(item, 'ROIInterpreter', 'anonymous') 107 | rtss.save_as(os.path.join(path, 'rtss.dcm')) 108 | i = i + 1 109 | if 'rtplan' in data: 110 | rtplan = data['rtplan'] 111 | wx.CallAfter(progressFunc, i, length, 112 | 'Anonymizing file ' + str(i) + ' of ' + str(length)) 113 | self.updateCommonElements(rtplan, name, patientid, privatetags) 114 | self.updateElement(rtplan, 'SeriesDescription', 'RT Plan') 115 | self.updateElement(rtplan, 'RTPlanName', 'plan') 116 | self.updateElement(rtplan, 'RTPlanDate', '19010101') 117 | self.updateElement(rtplan, 'RTPlanTime', '000000') 118 | if 'ToleranceTables' in rtplan: 119 | for item in rtplan.ToleranceTables: 120 | self.updateElement(item, 'ToleranceTableLabel', 'tolerance') 121 | if 'Beams' in rtplan: 122 | for item in rtplan.Beams: 123 | self.updateElement(item, 'Manufacturer', 'manufacturer') 124 | self.updateElement(item, 'InstitutionName', 'institution') 125 | self.updateElement(item, 'InstitutionAddress', 'address') 126 | self.updateElement(item, 'InstitutionalDepartmentName', 'department') 127 | self.updateElement(item, 'ManufacturersModelName', 'model') 128 | self.updateElement(item, 'TreatmentMachineName', 'txmachine') 129 | if 'TreatmentMachines' in rtplan: 130 | for item in rtplan.TreatmentMachines: 131 | self.updateElement(item, 'Manufacturer', 'manufacturer') 132 | self.updateElement(item, 'InstitutionName', 'vendor') 133 | self.updateElement(item, 'InstitutionAddress', 'address') 134 | self.updateElement(item, 'InstitutionalDepartmentName', 'department') 135 | self.updateElement(item, 'ManufacturersModelName', 'model') 136 | self.updateElement(item, 'DeviceSerialNumber', '0') 137 | self.updateElement(item, 'TreatmentMachineName', 'txmachine') 138 | if 'Sources' in rtplan: 139 | for item in rtplan.Sources: 140 | self.updateElement(item, 'SourceManufacturer', 'manufacturer') 141 | self.updateElement(item, 'SourceIsotopeName', 'isotope') 142 | rtplan.save_as(os.path.join(path, 'rtplan.dcm')) 143 | i = i + 1 144 | if 'rtdose' in data: 145 | rtdose = data['rtdose'] 146 | wx.CallAfter(progressFunc, i, length, 147 | 'Anonymizing file ' + str(i) + ' of ' + str(length)) 148 | self.updateCommonElements(rtdose, name, patientid, privatetags) 149 | self.updateElement(rtdose, 'SeriesDescription', 'RT Dose') 150 | rtdose.save_as(os.path.join(path, 'rtdose.dcm')) 151 | i = i + 1 152 | if 'images' in data: 153 | images = data['images'] 154 | for n, image in enumerate(images): 155 | wx.CallAfter(progressFunc, i, length, 156 | 'Anonymizing file ' + str(i) + ' of ' + str(length)) 157 | self.updateCommonElements(image, name, patientid, privatetags) 158 | self.updateElement(image, 'SeriesDate', '19010101') 159 | self.updateElement(image, 'ContentDate', '19010101') 160 | self.updateElement(image, 'SeriesTime', '000000') 161 | self.updateElement(image, 'ContentTime', '000000') 162 | self.updateElement(image, 'InstitutionName', 'institution') 163 | self.updateElement(image, 'InstitutionAddress', 'address') 164 | self.updateElement(image, 'InstitutionalDepartmentName', 'department') 165 | modality = image.SOPClassUID.name.partition(' Image Storage')[0] 166 | image.save_as( 167 | os.path.join(path, modality.lower() + '.' + str(n) + '.dcm')) 168 | i = i + 1 169 | 170 | wx.CallAfter(progressFunc, length-1, length, 'Done') 171 | 172 | def updateElement(self, data, element, value): 173 | """Updates the element only if it exists in the original DICOM data.""" 174 | 175 | if element in data: 176 | data.update({element:value}) 177 | 178 | def updateCommonElements(self, data, name, patientid, privatetags): 179 | """Updates the element only if it exists in the original DICOM data.""" 180 | 181 | if len(name): 182 | self.updateElement(data, 'PatientsName', name) 183 | if len(patientid): 184 | self.updateElement(data, 'PatientID', patientid) 185 | if privatetags: 186 | data.remove_private_tags() 187 | self.updateElement(data, 'OtherPatientIDs', patientid) 188 | self.updateElement(data, 'OtherPatientNames', name) 189 | self.updateElement(data, 'InstanceCreationDate', '19010101') 190 | self.updateElement(data, 'InstanceCreationTime', '000000') 191 | self.updateElement(data, 'StudyDate', '19010101') 192 | self.updateElement(data, 'StudyTime', '000000') 193 | self.updateElement(data, 'AccessionNumber', '') 194 | self.updateElement(data, 'Manufacturer', 'manufacturer') 195 | self.updateElement(data, 'ReferringPhysiciansName', 'physician') 196 | self.updateElement(data, 'StationName', 'station') 197 | self.updateElement(data, 'NameofPhysiciansReadingStudy', 'physician') 198 | self.updateElement(data, 'OperatorsName', 'operator') 199 | self.updateElement(data, 'PhysiciansofRecord', 'physician') 200 | self.updateElement(data, 'ManufacturersModelName', 'model') 201 | self.updateElement(data, 'PatientsBirthDate', '') 202 | self.updateElement(data, 'PatientsSex', 'O') 203 | self.updateElement(data, 'PatientsAge', '000Y') 204 | self.updateElement(data, 'PatientsWeight', 0) 205 | self.updateElement(data, 'PatientsSize', 0) 206 | self.updateElement(data, 'PatientsAddress', 'address') 207 | self.updateElement(data, 'AdditionalPatientHistory', '') 208 | self.updateElement(data, 'EthnicGroup', 'ethnicity') 209 | self.updateElement(data, 'StudyID', '1') 210 | self.updateElement(data, 'DeviceSerialNumber', '0') 211 | self.updateElement(data, 'SoftwareVersions', '1.0') 212 | self.updateElement(data, 'ReviewDate', '19010101') 213 | self.updateElement(data, 'ReviewTime', '000000') 214 | self.updateElement(data, 'ReviewerName', 'anonymous') 215 | 216 | class AnonymizeDialog(wx.Dialog): 217 | """Dialog that shows the options to anonymize DICOM / DICOM RT data.""" 218 | 219 | def __init__(self): 220 | wx.Dialog.__init__(self) 221 | 222 | def Init(self): 223 | """Method called after the dialog has been initialized.""" 224 | 225 | # Set window icon 226 | if not guiutil.IsMac(): 227 | self.SetIcon(guiutil.get_icon()) 228 | 229 | # Initialize controls 230 | self.txtDICOMFolder = XRCCTRL(self, 'txtDICOMFolder') 231 | self.checkPatientName = XRCCTRL(self, 'checkPatientName') 232 | self.txtFirstName = XRCCTRL(self, 'txtFirstName') 233 | self.txtLastName = XRCCTRL(self, 'txtLastName') 234 | self.checkPatientID = XRCCTRL(self, 'checkPatientID') 235 | self.txtPatientID = XRCCTRL(self, 'txtPatientID') 236 | self.checkPrivateTags = XRCCTRL(self, 'checkPrivateTags') 237 | self.bmpError = XRCCTRL(self, 'bmpError') 238 | self.lblDescription = XRCCTRL(self, 'lblDescription') 239 | 240 | # Bind interface events to the proper methods 241 | wx.EVT_BUTTON(self, XRCID('btnFolderBrowse'), self.OnFolderBrowse) 242 | wx.EVT_CHECKBOX(self, XRCID('checkPatientName'), self.OnCheckPatientName) 243 | wx.EVT_CHECKBOX(self, XRCID('checkPatientID'), self.OnCheckPatientID) 244 | wx.EVT_BUTTON(self, wx.ID_OK, self.OnOK) 245 | 246 | # Set and bold the font of the description label 247 | if guiutil.IsMac(): 248 | font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT) 249 | font.SetWeight(wx.FONTWEIGHT_BOLD) 250 | self.lblDescription.SetFont(font) 251 | 252 | # Initialize the import location via pubsub 253 | pub.subscribe(self.OnImportPrefsChange, 'general.dicom.import_location') 254 | pub.sendMessage('preferences.requested.value', msg='general.dicom.import_location') 255 | 256 | # Pre-select the text on the text controls due to a Mac OS X bug 257 | self.txtFirstName.SetSelection(-1, -1) 258 | self.txtLastName.SetSelection(-1, -1) 259 | self.txtPatientID.SetSelection(-1, -1) 260 | 261 | # Load the error bitmap 262 | self.bmpError.SetBitmap(wx.Bitmap(util.GetResourcePath('error.png'))) 263 | 264 | # Initialize variables 265 | self.name = self.txtLastName.GetValue() + '^' + self.txtFirstName.GetValue() 266 | self.patientid = self.txtPatientID.GetValue() 267 | self.privatetags = True 268 | 269 | def OnImportPrefsChange(self, msg): 270 | """When the import preferences change, update the values.""" 271 | 272 | self.path = str(msg) 273 | self.txtDICOMFolder.SetValue(self.path) 274 | 275 | def OnFolderBrowse(self, evt): 276 | """Get the directory selected by the user.""" 277 | 278 | dlg = wx.DirDialog( 279 | self, defaultPath = self.path, 280 | message="Choose a folder to save the anonymized DICOM data...") 281 | 282 | if dlg.ShowModal() == wx.ID_OK: 283 | self.path = dlg.GetPath() 284 | self.txtDICOMFolder.SetValue(self.path) 285 | 286 | dlg.Destroy() 287 | 288 | def OnCheckPatientName(self, evt): 289 | """Enable or disable whether the patient's name is anonymized.""" 290 | 291 | self.txtFirstName.Enable(evt.IsChecked()) 292 | self.txtLastName.Enable(evt.IsChecked()) 293 | if not evt.IsChecked(): 294 | self.txtDICOMFolder.SetFocus() 295 | else: 296 | self.txtFirstName.SetFocus() 297 | self.txtFirstName.SetSelection(-1, -1) 298 | 299 | def OnCheckPatientID(self, evt): 300 | """Enable or disable whether the patient's ID is anonymized.""" 301 | 302 | self.txtPatientID.Enable(evt.IsChecked()) 303 | if not evt.IsChecked(): 304 | self.txtDICOMFolder.SetFocus() 305 | else: 306 | self.txtPatientID.SetFocus() 307 | self.txtPatientID.SetSelection(-1, -1) 308 | 309 | def OnOK(self, evt): 310 | """Return the options from the anonymize data dialog.""" 311 | 312 | # Patient name 313 | if self.checkPatientName.IsChecked(): 314 | self.name = self.txtLastName.GetValue() 315 | if len(self.txtFirstName.GetValue()): 316 | self.name = self.name + '^' + self.txtFirstName.GetValue() 317 | else: 318 | self.name = '' 319 | 320 | # Patient ID 321 | if self.checkPatientID.IsChecked(): 322 | self.patientid = self.txtPatientID.GetValue() 323 | else: 324 | self.patientid = '' 325 | 326 | # Private tags 327 | if self.checkPrivateTags.IsChecked(): 328 | self.privatetags = True 329 | else: 330 | self.privatetags = False 331 | 332 | self.EndModal(wx.ID_OK) -------------------------------------------------------------------------------- /dicompyler/baseplugins/anonymize.xrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | wxVERTICAL 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | wxALIGN_LEFT|wxALIGN_CENTRE_VERTICAL 16 | 17 | 18 | 5,0 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | wxALL|wxEXPAND|wxALIGN_CENTRE 27 | 28 | 29 | wxVERTICAL 30 | 31 | wxALL|wxEXPAND|wxALIGN_CENTRE 32 | 3 33 | 34 | 35 | 36 | 37 | 0,5 38 | 39 | 40 | 41 | 42 | 1 43 | 44 | 45 | 46 | 0,5 47 | 48 | 49 | 50 | 51 | 20,0 52 | 53 | 54 | 55 | 56 | 57 | 58 | wxALIGN_CENTRE 59 | 60 | 61 | 5,0 62 | 63 | 64 | 65 | 66 | wxALL|wxEXPAND|wxALIGN_CENTRE 67 | 68 | 69 | 7,0 70 | 71 | 72 | 73 | 74 | 75 | 76 | wxALIGN_CENTRE 77 | 78 | 79 | 5,0 80 | 81 | 82 | 83 | anonymous 84 | 85 | 86 | wxALL|wxEXPAND|wxALIGN_CENTRE 87 | 88 | wxHORIZONTAL 89 | 90 | 91 | wxALL|wxEXPAND|wxALIGN_CENTRE 92 | 93 | 94 | 0,5 95 | 96 | 97 | 98 | 99 | 1 100 | 101 | 102 | 103 | 0,5 104 | 105 | 106 | 107 | 108 | 20,0 109 | 110 | 111 | 112 | 113 | 114 | wxALIGN_CENTRE 115 | 116 | 117 | 5,0 118 | 119 | wxHORIZONTAL 120 | 121 | 122 | 123456 123 | 124 | wxALIGN_CENTRE 125 | 126 | 127 | wxALL|wxEXPAND|wxALIGN_CENTRE 128 | 129 | 130 | 0,5 131 | 132 | 133 | 134 | 135 | 1 136 | 137 | 138 | 139 | 0,15 140 | 141 | 142 | 143 | 144 | 145 | 16,16 146 | 147 | 148 | 5,0 149 | 150 | 151 | 152 | 153 | 154 | 155 | bold 156 | 0 157 | default 158 | ISO-8859-1 159 | 160 | 161 | wxALIGN_LEFT 162 | 163 | wxHORIZONTAL 164 | 165 | 166 | 167 | 0,5 168 | 169 | 170 | wxVERTICAL 171 | 172 | wxALL|wxEXPAND|wxALIGN_CENTRE 173 | 3 174 | 175 | 176 | 0,5 177 | 178 | 179 | 180 | 181 | 182 | 185 | 186 | 187 | 188 | 189 | 1 190 | 1 191 | 192 | 193 | 194 | wxALL|wxEXPAND|wxALIGN_CENTRE 195 | 3 196 | 197 | 198 | 0,5 199 | 200 | 201 | Anonymize DICOM Data 202 | 1 203 | 204 | 205 | -------------------------------------------------------------------------------- /dicompyler/baseplugins/dvh.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # dvh.py 4 | """dicompyler plugin that displays a dose volume histogram (DVH) 5 | with adjustable constraints via wxPython and matplotlib.""" 6 | # Copyright (c) 2009-2017 Aditya Panchal 7 | # This file is part of dicompyler, released under a BSD license. 8 | # See the file license.txt included with this distribution, also 9 | # available at https://github.com/bastula/dicompyler/ 10 | # 11 | # It is assumed that the reference (prescription) dose is in cGy. 12 | 13 | import wx 14 | from wx.xrc import XmlResource, XRCCTRL, XRCID 15 | from wx.lib.pubsub import pub 16 | from dicompyler import guiutil, util 17 | from dicompyler import guidvh 18 | import numpy as np 19 | 20 | def pluginProperties(): 21 | """Properties of the plugin.""" 22 | 23 | props = {} 24 | props['name'] = 'DVH' 25 | props['description'] = "Display and evaluate dose volume histogram (DVH) data" 26 | props['author'] = 'Aditya Panchal' 27 | props['version'] = "0.5.0" 28 | props['plugin_type'] = 'main' 29 | props['plugin_version'] = 1 30 | props['min_dicom'] = ['rtss', 'rtdose'] 31 | props['recommended_dicom'] = ['rtss', 'rtdose', 'rtplan'] 32 | 33 | return props 34 | 35 | def pluginLoader(parent): 36 | """Function to load the plugin.""" 37 | 38 | # Load the XRC file for our gui resources 39 | res = XmlResource(util.GetBasePluginsPath('dvh.xrc')) 40 | 41 | panelDVH = res.LoadPanel(parent, 'pluginDVH') 42 | panelDVH.Init(res) 43 | 44 | return panelDVH 45 | 46 | class pluginDVH(wx.Panel): 47 | """Plugin to display DVH data with adjustable constraints.""" 48 | 49 | def __init__(self): 50 | wx.Panel.__init__(self) 51 | 52 | def Init(self, res): 53 | """Method called after the panel has been initialized.""" 54 | 55 | self.guiDVH = guidvh.guiDVH(self) 56 | res.AttachUnknownControl('panelDVH', self.guiDVH.panelDVH, self) 57 | 58 | # Initialize the Constraint selector controls 59 | self.lblType = XRCCTRL(self, 'lblType') 60 | self.choiceConstraint = XRCCTRL(self, 'choiceConstraint') 61 | self.txtConstraint = XRCCTRL(self, 'txtConstraint') 62 | self.sliderConstraint = XRCCTRL(self, 'sliderConstraint') 63 | self.lblResultType = XRCCTRL(self, 'lblResultType') 64 | self.lblConstraintUnits = XRCCTRL(self, 'lblConstraintUnits') 65 | self.lblConstraintTypeUnits = XRCCTRL(self, 'lblConstraintTypeUnits') 66 | 67 | # Initialize the result labels 68 | self.lblConstraintType = XRCCTRL(self, 'lblConstraintType') 69 | self.lblResultDivider = XRCCTRL(self, 'lblResultDivider') 70 | self.lblConstraintPercent = XRCCTRL(self, 'lblConstraintPercent') 71 | 72 | # Modify the control and font size on Mac 73 | controls = [self.lblType, self.choiceConstraint, self.sliderConstraint, 74 | self.lblResultType, self.lblConstraintUnits, self.lblConstraintPercent, 75 | self.lblConstraintType, self.lblConstraintTypeUnits, self.lblResultDivider] 76 | # Add children of composite controls to modification list 77 | compositecontrols = [self.txtConstraint] 78 | for control in compositecontrols: 79 | for child in control.GetChildren(): 80 | controls.append(child) 81 | # Add the constraint static box to the modification list 82 | controls.append(self.lblType.GetContainingSizer().GetStaticBox()) 83 | 84 | if guiutil.IsMac(): 85 | for control in controls: 86 | control.SetWindowVariant(wx.WINDOW_VARIANT_SMALL) 87 | 88 | # Adjust the control size for the result value labels 89 | te = self.lblType.GetTextExtent('0') 90 | self.lblConstraintUnits.SetMinSize((te[0]*10, te[1])) 91 | self.lblConstraintPercent.SetMinSize((te[0]*6, te[1])) 92 | self.Layout() 93 | 94 | # Bind ui events to the proper methods 95 | self.Bind( 96 | wx.EVT_CHOICE, self.OnToggleConstraints, id=XRCID('choiceConstraint')) 97 | self.Bind( 98 | wx.EVT_SPINCTRL, self.OnChangeConstraint, id=XRCID('txtConstraint')) 99 | self.Bind( 100 | wx.EVT_COMMAND_SCROLL_THUMBTRACK, 101 | self.OnChangeConstraint, id=XRCID('sliderConstraint')) 102 | self.Bind( 103 | wx.EVT_COMMAND_SCROLL_CHANGED, 104 | self.OnChangeConstraint, id=XRCID('sliderConstraint')) 105 | self.Bind(wx.EVT_WINDOW_DESTROY, self.OnDestroy) 106 | 107 | # Initialize variables 108 | self.structures = {} # structures from initial DICOM data 109 | self.checkedstructures = {} # structures that need to be shown 110 | self.dvhs = {} # raw dvhs from initial DICOM data 111 | self.dvharray = {} # dict of dvh data processed from dvhdata 112 | self.dvhscaling = {} # dict of dvh scaling data 113 | self.plan = {} # used for rx dose 114 | self.structureid = 1 # used to indicate current constraint structure 115 | 116 | # Set up pubsub 117 | pub.subscribe(self.OnUpdatePatient, 'patient.updated.parsed_data') 118 | pub.subscribe(self.OnStructureCheck, 'structures.checked') 119 | pub.subscribe(self.OnStructureSelect, 'structure.selected') 120 | 121 | def OnUpdatePatient(self, msg): 122 | """Update and load the patient data.""" 123 | 124 | self.structures = msg['structures'] 125 | self.dvhs = msg['dvhs'] 126 | self.plan = msg['plan'] 127 | # show an empty plot when (re)loading a patient 128 | self.guiDVH.Replot() 129 | self.EnableConstraints(False) 130 | 131 | def OnDestroy(self, evt): 132 | """Unbind to all events before the plugin is destroyed.""" 133 | 134 | pub.unsubscribe(self.OnUpdatePatient, 'patient.updated.parsed_data') 135 | pub.unsubscribe(self.OnStructureCheck, 'structures.checked') 136 | pub.unsubscribe(self.OnStructureSelect, 'structure.selected') 137 | 138 | def OnStructureCheck(self, msg): 139 | """When a structure changes, update the interface and plot.""" 140 | 141 | # Make sure that the volume has been calculated for each structure 142 | # before setting it 143 | self.checkedstructures = msg 144 | for id, structure in self.checkedstructures.items(): 145 | if not 'volume' in self.structures[id]: 146 | self.structures[id]['volume'] = structure['volume'] 147 | 148 | # make sure that the dvh has been calculated for each structure 149 | # before setting it 150 | if id in self.dvhs: 151 | self.EnableConstraints(True) 152 | self.dvharray[id] = self.dvhs[id].relative_volume.counts 153 | # Create an instance of the dvh scaling data for guidvh 154 | self.dvhscaling[id] = 1 # self.dvhs[id]['scaling'] 155 | # 'Toggle' the choice box to refresh the dose data 156 | self.OnToggleConstraints(None) 157 | if not len(self.checkedstructures): 158 | self.EnableConstraints(False) 159 | # Make an empty plot on the DVH 160 | self.guiDVH.Replot() 161 | 162 | def OnStructureSelect(self, msg): 163 | """Load the constraints for the currently selected structure.""" 164 | 165 | if (msg['id'] == None): 166 | self.EnableConstraints(False) 167 | else: 168 | self.structureid = msg['id'] 169 | if self.structureid in self.dvhs: 170 | # Create an instance of the dvh scaling data for guidvh 171 | self.dvhscaling[self.structureid] = 1 # self.dvhs[self.structureid]['scaling'] 172 | # 'Toggle' the choice box to refresh the dose data 173 | self.OnToggleConstraints(None) 174 | else: 175 | self.EnableConstraints(False) 176 | self.guiDVH.Replot([self.dvharray], [self.dvhscaling], self.checkedstructures) 177 | 178 | def EnableConstraints(self, value): 179 | """Enable or disable the constraint selector.""" 180 | 181 | self.choiceConstraint.Enable(value) 182 | self.txtConstraint.Enable(value) 183 | self.sliderConstraint.Enable(value) 184 | if not value: 185 | self.lblConstraintUnits.SetLabel('- ') 186 | self.lblConstraintPercent.SetLabel('- ') 187 | self.txtConstraint.SetValue(0) 188 | self.sliderConstraint.SetValue(0) 189 | 190 | def OnToggleConstraints(self, evt): 191 | """Switch between different constraint modes.""" 192 | 193 | # Replot the remaining structures and disable the constraints 194 | # if a structure that has no DVH calculated is selected 195 | if not self.structureid in self.dvhs: 196 | self.guiDVH.Replot([self.dvharray], [self.dvhscaling], self.checkedstructures) 197 | self.EnableConstraints(False) 198 | return 199 | else: 200 | self.EnableConstraints(True) 201 | dvh = self.dvhs[self.structureid] 202 | 203 | # Check if the function was called via an event or not 204 | if not (evt == None): 205 | constrainttype = evt.GetInt() 206 | else: 207 | constrainttype = self.choiceConstraint.GetSelection() 208 | 209 | constraintrange = 0 210 | # Volume constraint 211 | if (constrainttype == 0): 212 | self.lblConstraintType.SetLabel(' Dose:') 213 | self.lblConstraintTypeUnits.SetLabel('% ') 214 | self.lblResultType.SetLabel('Volume:') 215 | constraintrange = dvh.relative_dose().max 216 | # Volume constraint in Gy 217 | elif (constrainttype == 1): 218 | self.lblConstraintType.SetLabel(' Dose:') 219 | self.lblConstraintTypeUnits.SetLabel('Gy ') 220 | self.lblResultType.SetLabel('Volume:') 221 | constraintrange = round(dvh.max) 222 | # Dose constraint 223 | elif (constrainttype == 2): 224 | self.lblConstraintType.SetLabel('Volume:') 225 | self.lblConstraintTypeUnits.SetLabel('% ') 226 | self.lblResultType.SetLabel(' Dose:') 227 | constraintrange = 100 228 | # Dose constraint in cc 229 | elif (constrainttype == 3): 230 | self.lblConstraintType.SetLabel('Volume:') 231 | self.lblConstraintTypeUnits.SetLabel('cm\u00B3') 232 | self.lblResultType.SetLabel(' Dose:') 233 | constraintrange = dvh.volume 234 | 235 | self.sliderConstraint.SetRange(0, constraintrange) 236 | self.sliderConstraint.SetValue(constraintrange) 237 | self.txtConstraint.SetRange(0, constraintrange) 238 | self.txtConstraint.SetValue(constraintrange) 239 | 240 | self.OnChangeConstraint(None) 241 | 242 | def OnChangeConstraint(self, evt): 243 | """Update the results when the constraint value changes.""" 244 | 245 | # Check if the function was called via an event or not 246 | if not (evt == None): 247 | slidervalue = evt.GetInt() 248 | else: 249 | slidervalue = self.sliderConstraint.GetValue() 250 | 251 | self.txtConstraint.SetValue(slidervalue) 252 | self.sliderConstraint.SetValue(slidervalue) 253 | id = self.structureid 254 | dvh = self.dvhs[self.structureid] 255 | 256 | constrainttype = self.choiceConstraint.GetSelection() 257 | # Volume constraint 258 | if (constrainttype == 0): 259 | absDose = dvh.rx_dose * slidervalue 260 | cc = dvh.volume_constraint(slidervalue) 261 | constraint = dvh.relative_volume.volume_constraint(slidervalue) 262 | 263 | self.lblConstraintUnits.SetLabel(str(cc)) 264 | self.lblConstraintPercent.SetLabel(str(constraint)) 265 | self.guiDVH.Replot([self.dvharray], [self.dvhscaling], 266 | self.checkedstructures, ([absDose], [constraint.value]), id) 267 | # Volume constraint in Gy 268 | elif (constrainttype == 1): 269 | absDose = slidervalue*100 270 | cc = dvh.volume_constraint(slidervalue, dvh.dose_units) 271 | constraint = dvh.relative_volume.volume_constraint( 272 | slidervalue, dvh.dose_units) 273 | 274 | self.lblConstraintUnits.SetLabel(str(cc)) 275 | self.lblConstraintPercent.SetLabel(str(constraint)) 276 | self.guiDVH.Replot([self.dvharray], [self.dvhscaling], 277 | self.checkedstructures, ([absDose], [constraint.value]), id) 278 | # Dose constraint 279 | elif (constrainttype == 2): 280 | dose = dvh.dose_constraint(slidervalue) 281 | relative_dose = dvh.relative_dose().dose_constraint(slidervalue) 282 | 283 | self.lblConstraintUnits.SetLabel(str(dose)) 284 | self.lblConstraintPercent.SetLabel(str(relative_dose)) 285 | self.guiDVH.Replot([self.dvharray], [self.dvhscaling], 286 | self.checkedstructures, 287 | ([dose.value * 100], [slidervalue]), id) 288 | # Dose constraint in cc 289 | elif (constrainttype == 3): 290 | volumepercent = slidervalue*100/self.structures[id]['volume'] 291 | dose = dvh.dose_constraint(slidervalue, dvh.volume_units) 292 | relative_dose = dvh.relative_dose().dose_constraint( 293 | slidervalue, dvh.volume_units) 294 | 295 | self.lblConstraintUnits.SetLabel(str(dose)) 296 | self.lblConstraintPercent.SetLabel(str(relative_dose)) 297 | self.guiDVH.Replot([self.dvharray], [self.dvhscaling], 298 | self.checkedstructures, 299 | ([dose.value * 100], [volumepercent]), id) 300 | -------------------------------------------------------------------------------- /dicompyler/baseplugins/dvh.xrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 20 | 21 | 22 | 23 | 24 | 25 | wxHORIZONTAL 26 | 27 | 28 | 29 | 30 | wxALIGN_RIGHT|wxALIGN_CENTRE_VERTICAL 31 | 32 | 33 | 3,0 34 | 35 | 36 | 37 | 38 | Volume (V___) 39 | Volume (V__Gy) 40 | Dose (D__) 41 | Dose (D__cc) 42 | 43 | 0 44 | 45 | wxALIGN_LEFT|wxALIGN_CENTRE_VERTICAL 46 | 47 | 48 | 5,0 49 | 50 | 51 | 52 | 53 | 54 | 55 | wxALIGN_RIGHT|wxALIGN_CENTRE_VERTICAL 56 | 57 | 58 | 3,0 59 | 60 | 61 | 62 | 100 63 | 1 64 | 65 | 66 | wxALIGN_CENTRE_VERTICAL 67 | 68 | 69 | 3,0 70 | 71 | 72 | 73 | 100 74 | 0 75 | 76 | 77 | wxALL|wxEXPAND|wxALIGN_CENTRE_VERTICAL 78 | 79 | 80 | 3,0 81 | 82 | 83 | 84 | 85 | 86 | wxALIGN_LEFT|wxALIGN_CENTRE_VERTICAL 87 | 88 | 89 | 5,0 90 | 91 | 92 | 93 | 94 | 95 | 96 | wxALIGN_CENTRE_VERTICAL 97 | 98 | 99 | 3,0 100 | 101 | 102 | 103 | 104 | 105 | 106 | wxALIGN_CENTRE_VERTICAL 107 | 108 | 109 | 3,0 110 | 111 | 112 | 113 | 114 | 115 | wxALIGN_CENTRE_VERTICAL 116 | 117 | 118 | 119 | 120 | 121 | 122 | wxALIGN_CENTRE_VERTICAL 123 | 124 | 125 | 10,0 126 | 127 | 128 | 129 | wxALL|wxEXPAND|wxALIGN_CENTRE 130 | 131 | wxHORIZONTAL 132 | 133 | 134 | wxALL|wxEXPAND|wxALIGN_CENTRE 135 | 136 | wxVERTICAL 137 | 138 | 139 | -------------------------------------------------------------------------------- /dicompyler/baseplugins/quickopen.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # quickopen.py 4 | """dicompyler plugin that allows quick import of DICOM data.""" 5 | # Copyright (c) 2012-2017 Aditya Panchal 6 | # This file is part of dicompyler, released under a BSD license. 7 | # See the file license.txt included with this distribution, also 8 | # available at https://github.com/bastula/dicompyler/ 9 | # 10 | 11 | import logging 12 | logger = logging.getLogger('dicompyler.quickimport') 13 | import wx 14 | from wx.lib.pubsub import pub 15 | from dicompylercore import dicomparser 16 | from dicompyler import util 17 | 18 | def pluginProperties(): 19 | """Properties of the plugin.""" 20 | 21 | props = {} 22 | props['name'] = 'DICOM Quick Import' 23 | props['menuname'] = "&DICOM File Quickly...\tCtrl-Shift-O" 24 | props['description'] = "Import DICOM data quickly" 25 | props['author'] = 'Aditya Panchal' 26 | props['version'] = "0.5.0" 27 | props['plugin_type'] = 'import' 28 | props['plugin_version'] = 1 29 | props['min_dicom'] = [] 30 | 31 | return props 32 | 33 | class plugin: 34 | 35 | def __init__(self, parent): 36 | 37 | # Initialize the import location via pubsub 38 | pub.subscribe(self.OnImportPrefsChange, 'general.dicom') 39 | pub.sendMessage('preferences.requested.values', msg='general.dicom') 40 | 41 | self.parent = parent 42 | 43 | # Setup toolbar controls 44 | openbmp = wx.Bitmap(util.GetResourcePath('folder_image.png')) 45 | self.tools = [{'label':"Open Quickly", 'bmp':openbmp, 46 | 'shortHelp':"Open DICOM File Quickly...", 47 | 'eventhandler':self.pluginMenu}] 48 | 49 | def OnImportPrefsChange(self, topic, msg): 50 | """When the import preferences change, update the values.""" 51 | topic = topic.split('.') 52 | if (topic[1] == 'import_location'): 53 | self.path = str(msg) 54 | elif (topic[1] == 'import_location_setting'): 55 | self.import_location_setting = msg 56 | 57 | def pluginMenu(self, evt): 58 | """Import DICOM data quickly.""" 59 | 60 | dlg = wx.FileDialog( 61 | self.parent, defaultDir = self.path, 62 | wildcard="All Files (*.*)|*.*|DICOM File (*.dcm)|*.dcm", 63 | message="Choose a DICOM File") 64 | 65 | patient = {} 66 | if dlg.ShowModal() == wx.ID_OK: 67 | filename = dlg.GetPath() 68 | # Try to parse the file if is a DICOM file 69 | try: 70 | logger.debug("Reading: %s", filename) 71 | dp = dicomparser.DicomParser(filename) 72 | # Otherwise show an error dialog 73 | except (AttributeError, EOFError, IOError, KeyError): 74 | logger.info("%s is not a valid DICOM file.", filename) 75 | dlg = wx.MessageDialog( 76 | self.parent, filename + " is not a valid DICOM file.", 77 | "Invalid DICOM File", wx.OK|wx.ICON_ERROR) 78 | dlg.ShowModal() 79 | # If this is really a DICOM file, place it in the appropriate bin 80 | else: 81 | if (('ImageOrientationPatient' in dp.ds) and not (dp.ds.Modality in ['RTDOSE'])): 82 | patient['images'] = [] 83 | patient['images'].append(dp.ds) 84 | elif (dp.ds.Modality in ['RTSTRUCT']): 85 | patient['rtss'] = dp.ds 86 | elif (dp.ds.Modality in ['RTPLAN']): 87 | patient['rtplan'] = dp.ds 88 | elif (dp.ds.Modality in ['RTDOSE']): 89 | patient['rtdose'] = dp.ds 90 | else: 91 | patient[dp.ds.Modality] = dp.ds 92 | # Since we have decided to use this location to import from, 93 | # update the location in the preferences for the next session 94 | # if the 'import_location_setting' is "Remember Last Used" 95 | if (self.import_location_setting == "Remember Last Used"): 96 | pub.sendMessage('preferences.updated.value', 97 | msg={'general.dicom.import_location':dlg.GetDirectory()}) 98 | pub.sendMessage('preferences.requested.values', msg='general.dicom') 99 | pub.sendMessage('patient.updated.raw_data', msg=patient) 100 | dlg.Destroy() 101 | return 102 | -------------------------------------------------------------------------------- /dicompyler/baseplugins/treeview.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # treeview.py 4 | """dicompyler plugin that displays a tree view of the DICOM data structure.""" 5 | # Copyright (c) 2010-2017 Aditya Panchal 6 | # This file is part of dicompyler, released under a BSD license. 7 | # See the file license.txt included with this distribution, also 8 | # available at https://github.com/bastula/dicompyler/ 9 | # 10 | 11 | import logging 12 | logger = logging.getLogger('dicompyler.treeview') 13 | import threading 14 | from six.moves import queue 15 | import wx 16 | from wx.xrc import XmlResource, XRCCTRL, XRCID 17 | from wx.lib.pubsub import pub 18 | from wx.dataview import TreeListCtrl as tlc 19 | from dicompyler import guiutil, util 20 | try: 21 | import pydicom 22 | except ImportError: 23 | import dicom as pydicom 24 | 25 | def pluginProperties(): 26 | """Properties of the plugin.""" 27 | 28 | props = {} 29 | props['name'] = 'DICOM Tree' 30 | props['description'] = "Display a tree view of the DICOM data stucture" 31 | props['author'] = 'Aditya Panchal' 32 | props['version'] = "0.5.0" 33 | props['plugin_type'] = 'main' 34 | props['plugin_version'] = 1 35 | props['min_dicom'] = [] 36 | props['recommended_dicom'] = ['rtss', 'rtdose', 'rtss', 'ct'] 37 | 38 | return props 39 | 40 | def pluginLoader(parent): 41 | """Function to load the plugin.""" 42 | 43 | # Load the XRC file for our gui resources 44 | res = XmlResource(util.GetBasePluginsPath('treeview.xrc')) 45 | 46 | panelTreeView = res.LoadPanel(parent, 'pluginTreeView') 47 | panelTreeView.Init(res) 48 | 49 | return panelTreeView 50 | 51 | class pluginTreeView(wx.Panel): 52 | """Plugin to display DICOM data in a tree view.""" 53 | 54 | def __init__(self): 55 | wx.Panel.__init__(self) 56 | 57 | def Init(self, res): 58 | """Method called after the panel has been initialized.""" 59 | 60 | # Initialize the panel controls 61 | self.choiceDICOM = XRCCTRL(self, 'choiceDICOM') 62 | self.tlcTreeView = DICOMTree(self) 63 | res.AttachUnknownControl('tlcTreeView', self.tlcTreeView, self) 64 | 65 | # Bind interface events to the proper methods 66 | self.Bind(wx.EVT_CHOICE, self.OnLoadTree, id=XRCID('choiceDICOM')) 67 | self.Bind(wx.EVT_WINDOW_DESTROY, self.OnDestroy) 68 | 69 | # Decrease the font size on Mac 70 | font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT) 71 | if guiutil.IsMac(): 72 | font.SetPointSize(10) 73 | self.tlcTreeView.SetFont(font) 74 | 75 | # Set up pubsub 76 | pub.subscribe(self.OnUpdatePatient, 'patient.updated.raw_data') 77 | 78 | def OnUpdatePatient(self, msg): 79 | """Update and load the patient data.""" 80 | 81 | self.choiceDICOM.Enable() 82 | self.choiceDICOM.Clear() 83 | self.choiceDICOM.Append("Select a DICOM dataset...") 84 | self.choiceDICOM.Select(0) 85 | self.tlcTreeView.DeleteAllItems() 86 | # Iterate through the message and enumerate the DICOM datasets 87 | for k, v in msg.items(): 88 | if isinstance(v, pydicom.dataset.FileDataset): 89 | i = self.choiceDICOM.Append(v.SOPClassUID.name.split(' Storage')[0]) 90 | self.choiceDICOM.SetClientData(i, v) 91 | # Add the images to the choicebox 92 | if (k == 'images'): 93 | for imgnum, image in enumerate(v): 94 | i = self.choiceDICOM.Append( 95 | image.SOPClassUID.name.split(' Storage')[0] + \ 96 | ' Slice ' + str(imgnum + 1)) 97 | self.choiceDICOM.SetClientData(i, image) 98 | pub.unsubscribe(self.OnUpdatePatient, 'patient.updated.raw_data') 99 | 100 | def OnDestroy(self, evt): 101 | """Unbind to all events before the plugin is destroyed.""" 102 | 103 | pub.unsubscribe(self.OnUpdatePatient, 'patient.updated.raw_data') 104 | 105 | def OnLoadTree(self, event): 106 | """Update and load the DICOM tree.""" 107 | 108 | choiceItem = event.GetInt() 109 | # Load the dataset chosen from the choice control 110 | if not (choiceItem == 0): 111 | dataset = self.choiceDICOM.GetClientData(choiceItem) 112 | else: 113 | return 114 | 115 | self.tlcTreeView.DeleteAllItems() 116 | self.root = self.tlcTreeView.AppendItem(self.tlcTreeView.GetRootItem(),dataset.SOPClassUID.name) 117 | self.tlcTreeView.Collapse(self.root) 118 | 119 | # Initialize the progress dialog 120 | dlgProgress = guiutil.get_progress_dialog( 121 | wx.GetApp().GetTopWindow(), 122 | "Loading DICOM data...") 123 | # Set up the queue so that the thread knows which item was added 124 | self.queue = queue.Queue() 125 | # Initialize and start the recursion thread 126 | self.t=threading.Thread(target=self.RecurseTreeThread, 127 | args=(dataset, self.root, self.AddItemTree, 128 | dlgProgress.OnUpdateProgress, len(dataset))) 129 | self.t.start() 130 | # Show the progress dialog 131 | dlgProgress.ShowModal() 132 | dlgProgress.Destroy() 133 | self.tlcTreeView.SetFocus() 134 | self.tlcTreeView.Expand(self.root) 135 | 136 | def RecurseTreeThread(self, ds, parent, addItemFunc, progressFunc, length): 137 | """Recursively process the DICOM tree.""" 138 | for i, data_element in enumerate(ds): 139 | # Check and update the progress of the recursion 140 | if (length > 0): 141 | wx.CallAfter(progressFunc, i, length, 'Processing DICOM data...') 142 | if (i == length-1): 143 | wx.CallAfter(progressFunc, i, len(ds), 'Done') 144 | # Add the data_element to the tree if not a sequence element 145 | if not (data_element.VR == 'SQ'): 146 | cs = ds.get('SpecificCharacterSet', "ISO_IR 6") 147 | wx.CallAfter(addItemFunc, data_element, parent, cs=cs) 148 | # Otherwise add the sequence element to the tree 149 | else: 150 | wx.CallAfter(addItemFunc, data_element, parent, needQueue=True) 151 | item = self.queue.get() 152 | # Enumerate for each child element of the sequence 153 | for i, ds in enumerate(data_element.value): 154 | sq_item_description = data_element.name.replace(" Sequence", "") 155 | sq_element_text = "%s %d" % (sq_item_description, i+1) 156 | # Add the child of the sequence to the tree 157 | wx.CallAfter(addItemFunc, data_element, item, sq_element_text, needQueue=True) 158 | sq = self.queue.get() 159 | self.RecurseTreeThread(ds, sq, addItemFunc, progressFunc, 0) 160 | 161 | def AddItemTree(self, data_element, parent, sq_element_text="", needQueue=False, cs=None): 162 | """Add a new item to the DICOM tree.""" 163 | 164 | # Set the item if it is a child of a sequence element 165 | if not (sq_element_text == ""): 166 | item = self.tlcTreeView.AppendItem(parent, text=sq_element_text) 167 | else: 168 | item = self.tlcTreeView.AppendItem(parent, text=data_element.name) 169 | # Set the value if not a sequence element 170 | if not (data_element.VR == 'SQ'): 171 | value = data_element.value 172 | # Account for Pixel data 173 | if (data_element.name == 'Pixel Data'): 174 | value = 'Array of ' + str(len(data_element.value)) + ' bytes' 175 | # Account for Unknown VRs 176 | elif ((data_element.VR == 'UN') and \ 177 | not (type(data_element.value) == str)): 178 | value = data_element.repval 179 | else: 180 | # Apply the DICOM character encoding to the data element 181 | if not isinstance(data_element.value, str): 182 | try: 183 | pydicom.charset.decode( 184 | pydicom.charset.decode(data_element, cs)) 185 | # Otherwise try decoding via ASCII encoding 186 | except: 187 | try: 188 | value = str(data_element.value) 189 | except: 190 | logger.info( 191 | "Could not decode character set for %s.", 192 | data_element.name) 193 | value = str( 194 | data_element.value, errors='replace') 195 | else: 196 | value = data_element.value 197 | self.tlcTreeView.SetItemText(item, 1, value) 198 | # Fill in the rest of the data_element properties 199 | self.tlcTreeView.SetItemText(item, 2, str(data_element.tag)) 200 | self.tlcTreeView.SetItemText(item, 3, str(data_element.VM)) 201 | self.tlcTreeView.SetItemText(item, 4, str(data_element.VR)) 202 | if (needQueue): 203 | self.queue.put(item) 204 | 205 | class DICOMTree(tlc): 206 | """DICOM tree view based on TreeListControl.""" 207 | 208 | def __init__(self, *args, **kwargs): 209 | super(DICOMTree, self).__init__(*args, **kwargs) 210 | self.AppendColumn('Name') 211 | self.AppendColumn('Value') 212 | self.AppendColumn('Tag') 213 | self.AppendColumn('VM') 214 | self.AppendColumn('VR') 215 | #self.SetMainColumn(0) 216 | self.SetColumnWidth(0, 200) 217 | self.SetColumnWidth(1, 200) 218 | self.SetColumnWidth(3, 50) 219 | self.SetColumnWidth(4, 50) 220 | -------------------------------------------------------------------------------- /dicompyler/baseplugins/treeview.xrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 5,5 9 | 10 | 11 | 12 | wxHORIZONTAL 13 | 14 | 15 | 16 | - 17 | 18 | 0 19 | 0 20 | 21 | wxALIGN_CENTRE 22 | 23 | 24 | 25 | wxVERTICAL 26 | 27 | 28 | wxALL|wxEXPAND|wxALIGN_CENTRE 29 | 1 30 | 31 | 32 | 5,5 33 | 34 | 35 | 36 | 37 | 38 | wxHORIZONTAL 39 | 40 | 41 | 42 | wxALL|wxEXPAND|wxALIGN_CENTRE|wxADJUST_MINSIZE 43 | 44 | 45 | 46 | wxALL|wxEXPAND|wxALIGN_CENTRE 47 | 48 | wxVERTICAL 49 | 50 | 51 | wxALL|wxEXPAND|wxALIGN_CENTRE 52 | 53 | wxVERTICAL 54 | 55 | 56 | -------------------------------------------------------------------------------- /dicompyler/credits.txt: -------------------------------------------------------------------------------- 1 | The dicompyler Team 2 | 3 | Lead Developer 4 | Aditya Panchal 5 | 6 | Contributors 7 | Roy Keyes 8 | 9 | Artists 10 | Roy Keyes 11 | famfamfam Silk 12 | -------------------------------------------------------------------------------- /dicompyler/dvhdata.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # dvhdata.py 4 | """Class and functions related to dose volume histogram (DVH) data.""" 5 | # Copyright (c) 2009-2017 Aditya Panchal 6 | # This file is part of dicompyler, released under a BSD license. 7 | # See the file license.txt included with this distribution, also 8 | # available at https://github.com/bastula/dicompyler/ 9 | # 10 | # It's assumed that the reference (prescription) dose is in cGy. 11 | 12 | import numpy as np 13 | from six import itervalues 14 | 15 | class DVH: 16 | """Processes the dose volume histogram from DICOM DVH data.""" 17 | 18 | def __init__(self, dvh): 19 | """Take a dvh numpy array and convert it to cGy.""" 20 | self.dvh = dvh['data'] * 100 / dvh['data'][0] 21 | self.scaling = dvh['scaling'] 22 | 23 | # Instruct numpy to print the full extent of the array 24 | np.set_printoptions(threshold=2147483647, suppress=True) 25 | 26 | def GetVolumeConstraint(self, dose): 27 | """ Return the volume (in percent) of the structure that receives at 28 | least a specific dose in cGy. i.e. V100, V150.""" 29 | return self.dvh[int(dose/self.scaling)] 30 | 31 | def GetVolumeConstraintCC(self, dose, volumecc): 32 | """ Return the volume (in cc) of the structure that receives at least a 33 | specific dose in cGy. i.e. V100, V150.""" 34 | 35 | volumepercent = self.GetVolumeConstraint(dose) 36 | 37 | return volumepercent * volumecc / 100 38 | 39 | def GetDoseConstraint(self, volume): 40 | """ Return the maximum dose (in cGy) that a specific volume (in percent) 41 | receives. i.e. D90, D20.""" 42 | 43 | return np.argmin(np.fabs(self.dvh - volume))*self.scaling 44 | 45 | def CalculateVolume(structure): 46 | """Calculates the volume for the given structure.""" 47 | 48 | sPlanes = structure['planes'] 49 | 50 | # Store the total volume of the structure 51 | sVolume = 0 52 | 53 | n = 0 54 | # Iterate over each plane in the structure 55 | for sPlane in itervalues(sPlanes): 56 | 57 | # Calculate the area for each contour in the current plane 58 | contours = [] 59 | largest = 0 60 | largestIndex = 0 61 | for c, contour in enumerate(sPlane): 62 | # Create arrays for the x,y coordinate pair for the triangulation 63 | x = [] 64 | y = [] 65 | for point in contour['data']: 66 | x.append(point[0]) 67 | y.append(point[1]) 68 | 69 | cArea = 0 70 | # Calculate the area based on the Surveyor's formula 71 | for i in range(0, len(x)-1): 72 | cArea = cArea + x[i]*y[i+1] - x[i+1]*y[i] 73 | cArea = abs(cArea / 2) 74 | contours.append({'area':cArea, 'data':contour['data']}) 75 | 76 | # Determine which contour is the largest 77 | if (cArea > largest): 78 | largest = cArea 79 | largestIndex = c 80 | 81 | # See if the rest of the contours are within the largest contour 82 | area = contours[largestIndex]['area'] 83 | for i, contour in enumerate(contours): 84 | # Skip if this is the largest contour 85 | if not (i == largestIndex): 86 | contour['inside'] = False 87 | for point in contour['data']: 88 | if PointInPolygon(point[0], point[1], contours[largestIndex]['data']): 89 | contour['inside'] = True 90 | # Assume if one point is inside, all will be inside 91 | break 92 | # If the contour is inside, subtract it from the total area 93 | if contour['inside']: 94 | area = area - contour['area'] 95 | # Otherwise it is outside, so add it to the total area 96 | else: 97 | area = area + contour['area'] 98 | 99 | # If the plane is the first or last slice 100 | # only add half of the volume, otherwise add the full slice thickness 101 | if ((n == 0) or (n == len(sPlanes)-1)): 102 | sVolume = float(sVolume) + float(area) * float(structure['thickness']) * 0.5 103 | else: 104 | sVolume = float(sVolume) + float(area) * float(structure['thickness']) 105 | # Increment the current plane number 106 | n = n + 1 107 | 108 | # Since DICOM uses millimeters, convert from mm^3 to cm^3 109 | volume = sVolume/1000 110 | 111 | return volume 112 | 113 | def PointInPolygon(x, y, poly): 114 | """Uses the Ray Casting method to determine whether a point is within 115 | the given polygon. 116 | Taken from: http://www.ariel.com.au/a/python-point-int-poly.html""" 117 | 118 | n = len(poly) 119 | inside = False 120 | p1x, p1y, p1z = poly[0] 121 | for i in range(n+1): 122 | p2x, p2y, p2z = poly[i % n] 123 | if y > min(p1y,p2y): 124 | if y <= max(p1y,p2y): 125 | if x <= max(p1x,p2x): 126 | if p1y != p2y: 127 | xinters = (y-p1y)*(p2x-p1x)/(p2y-p1y)+p1x 128 | if p1x == p2x or x <= xinters: 129 | inside = not inside 130 | p1x,p1y = p2x,p2y 131 | 132 | return inside 133 | -------------------------------------------------------------------------------- /dicompyler/guidvh.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # guidvh.py 4 | """Class that displays the dose volume histogram via wxPython and matplotlib.""" 5 | # Copyright (c) 2009-2017 Aditya Panchal 6 | # This file is part of dicompyler, released under a BSD license. 7 | # See the file license.txt included with this distribution, also 8 | # available at https://github.com/bastula/dicompyler/ 9 | # 10 | # It's assumed that the reference (prescription) dose is in cGy. 11 | 12 | from dicompyler import wxmpl 13 | import numpy as np 14 | 15 | class guiDVH: 16 | """Displays and updates the dose volume histogram using WxMpl.""" 17 | def __init__(self, parent): 18 | 19 | self.panelDVH = wxmpl.PlotPanel(parent, -1, 20 | size=(6, 4.50), dpi=68, crosshairs=False, 21 | autoscaleUnzoom=False) 22 | self.Replot() 23 | 24 | def Replot(self, dvhlist=None, scalinglist=None, structures=None, 25 | point=None, pointid=None, prefixes=None): 26 | """Redraws the plot.""" 27 | 28 | fig = self.panelDVH.get_figure() 29 | fig.set_edgecolor('white') 30 | 31 | # clear the axes and replot everything 32 | axes = fig.gca() 33 | axes.cla() 34 | maxlen = 1 35 | if not (dvhlist == None): 36 | # Enumerate each set of DVHs 37 | for d, dvhs in enumerate(dvhlist): 38 | # Plot the DVH from each set 39 | for id, dvh in dvhs.items(): 40 | if id in structures: 41 | # Convert the color array to MPL formatted color 42 | colorarray = np.array(structures[id]['color'], 43 | dtype=float) 44 | # Plot white as black so it is visible on the plot 45 | if np.size(np.nonzero(colorarray/255 - 1)): 46 | color = colorarray/255 47 | else: 48 | color = np.zeros(3) 49 | prefix = prefixes[d] if not (prefixes == None) else None 50 | linestyle = '-' if not (d % 2) else '--' 51 | maxlen = self.DrawDVH(dvh, structures[id], axes, color, 52 | maxlen, scalinglist[d], 53 | prefix, linestyle) 54 | if (point and (pointid == id)): 55 | self.DrawPoint(point, axes, color) 56 | axes.legend(fancybox=True, shadow=True) 57 | # set the axes parameters 58 | axes.grid(True) 59 | axes.set_xlim(0, maxlen) 60 | axes.set_ylim(0, 100) 61 | axes.set_xlabel('Dose (cGy)') 62 | axes.set_ylabel('Volume (%)') 63 | axes.set_title('DVH') 64 | 65 | # redraw the display 66 | self.panelDVH.draw() 67 | 68 | def DrawDVH(self, dvh, structure, axes, color, maxlen, 69 | scaling=None, prefix=None, linestyle='-'): 70 | """Draw the given structure on the plot.""" 71 | 72 | # Determine the maximum DVH length for the x axis limit 73 | if len(dvh) > maxlen: 74 | maxlen = len(dvh) 75 | # if the structure color is white, change it to black 76 | 77 | dose = np.arange(len(dvh)) 78 | if not (scaling == None): 79 | dose = dose * scaling[structure['id']] 80 | name = prefix + ' ' + structure['name'] if prefix else structure['name'] 81 | axes.plot(dose, dvh, 82 | label=name, 83 | color=color, 84 | linewidth=2, 85 | linestyle=linestyle) 86 | 87 | return maxlen 88 | 89 | def DrawPoint(self, point, axes, color): 90 | """Draw the point for the given structure on the plot.""" 91 | 92 | axes.plot(point[0], point[1], 'o', color=color) 93 | -------------------------------------------------------------------------------- /dicompyler/guiutil.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # guiutil.py 4 | """Several GUI utility functions that don't really belong anywhere.""" 5 | # Copyright (c) 2009-2017 Aditya Panchal 6 | # This file is part of dicompyler, released under a BSD license. 7 | # See the file license.txt included with this distribution, also 8 | # available at https://github.com/bastula/dicompyler/ 9 | 10 | from dicompyler import util 11 | import wx 12 | from wx.xrc import XmlResource, XRCCTRL, XRCID 13 | from wx.lib.pubsub import pub 14 | 15 | def IsMSWindows(): 16 | """Are we running on Windows? 17 | 18 | @rtype: Bool""" 19 | return wx.Platform=='__WXMSW__' 20 | 21 | def IsGtk(): 22 | """Are we running on GTK (Linux) 23 | 24 | @rtype: Bool""" 25 | return wx.Platform=='__WXGTK__' 26 | 27 | def IsMac(): 28 | """Are we running on Mac 29 | 30 | @rtype: Bool""" 31 | return wx.Platform=='__WXMAC__' 32 | 33 | def GetItemsList(wxCtrl): 34 | # Return the list of values stored in a wxCtrlWithItems 35 | list = [] 36 | if not (wxCtrl.IsEmpty()): 37 | for i in range(wxCtrl.GetCount()): 38 | list.append(wxCtrl.GetString(i)) 39 | return list 40 | 41 | def SetItemsList(wxCtrl, list = [], data = []): 42 | # Set the wxCtrlWithItems to the given list and store the data in the item 43 | wxCtrl.Clear() 44 | i = 0 45 | for item in list: 46 | wxCtrl.Append(item) 47 | # if no data has been given, no need to set the client data 48 | if not (data == []): 49 | wxCtrl.SetClientData(i, data[i]) 50 | i = i + 1 51 | if not (wxCtrl.IsEmpty()): 52 | wxCtrl.SetSelection(0) 53 | 54 | def get_data_dir(): 55 | """Returns the data location for the application.""" 56 | 57 | sp = wx.StandardPaths.Get() 58 | return wx.StandardPaths.GetUserLocalDataDir(sp) 59 | 60 | def get_icon(): 61 | """Returns the icon for the application.""" 62 | 63 | icon = None 64 | if IsMSWindows(): 65 | if util.main_is_frozen(): 66 | import sys 67 | exeName = sys.executable 68 | icon = wx.Icon(exeName, wx.BITMAP_TYPE_ICO) 69 | else: 70 | icon = wx.Icon(util.GetResourcePath('dicompyler.ico'), wx.BITMAP_TYPE_ICO) 71 | elif IsGtk(): 72 | icon = wx.Icon(util.GetResourcePath('dicompyler_icon11_16.png'), wx.BITMAP_TYPE_PNG) 73 | 74 | return icon 75 | 76 | def convert_pil_to_wx(pil, alpha=True): 77 | """ Convert a PIL Image into a wx.Image. 78 | Code taken from Dave Witten's imViewer-Simple.py in pydicom contrib.""" 79 | if alpha: 80 | image = wx.Image(pil.size[0], pil.size[1], clear=True) 81 | image.SetData(pil.convert("RGB").tobytes()) 82 | image.SetAlpha(pil.convert("RGBA").tobytes()[3::4]) 83 | else: 84 | image = wx.Image(pil.size[0], pil.size[1], clear=True) 85 | new_image = pil.convert('RGB') 86 | data = new_image.tostring() 87 | image.SetData(data) 88 | return image 89 | 90 | def get_progress_dialog(parent, title="Loading..."): 91 | """Function to load the progress dialog.""" 92 | 93 | # Load the XRC file for our gui resources 94 | res = XmlResource(util.GetResourcePath('guiutil.xrc')) 95 | 96 | dialogProgress = res.LoadDialog(parent, 'ProgressDialog') 97 | dialogProgress.Init(res, title) 98 | 99 | return dialogProgress 100 | 101 | def adjust_control(control): 102 | """Adjust the control and font size on the Mac.""" 103 | 104 | if IsMac(): 105 | font = control.GetFont() 106 | font.SetPointSize(11) 107 | control.SetWindowVariant(wx.WINDOW_VARIANT_SMALL) 108 | control.SetFont(font) 109 | 110 | class ProgressDialog(wx.Dialog): 111 | """Dialog to show progress for certain long-running events.""" 112 | 113 | def __init__(self): 114 | wx.Dialog.__init__(self) 115 | 116 | def Init(self, res, title=None): 117 | """Method called after the dialog has been initialized.""" 118 | 119 | # Initialize controls 120 | self.SetTitle(title) 121 | self.lblProgressLabel = XRCCTRL(self, 'lblProgressLabel') 122 | self.lblProgress = XRCCTRL(self, 'lblProgress') 123 | self.gaugeProgress = XRCCTRL(self, 'gaugeProgress') 124 | self.lblProgressPercent = XRCCTRL(self, 'lblProgressPercent') 125 | 126 | def OnUpdateProgress(self, num, length, message=''): 127 | """Update the process interface elements.""" 128 | 129 | if not length: 130 | percentDone = 0 131 | else: 132 | percentDone = int(100 * (num) / length) 133 | 134 | self.gaugeProgress.SetValue(percentDone) 135 | self.lblProgressPercent.SetLabel(str(percentDone)) 136 | self.lblProgress.SetLabel(message) 137 | 138 | # End the dialog since we are done with the import process 139 | if (message == 'Done'): 140 | self.EndModal(wx.ID_OK) 141 | 142 | class ColorCheckListBox(wx.ScrolledWindow): 143 | """Control similar to a wx.CheckListBox with additional color indication.""" 144 | 145 | def __init__(self, parent, pubsubname=''): 146 | wx.ScrolledWindow.__init__(self, parent, -1, style=wx.SUNKEN_BORDER) 147 | 148 | # Initialize variables 149 | self.pubsubname = pubsubname 150 | 151 | # Setup the layout for the frame 152 | self.grid = wx.BoxSizer(wx.VERTICAL) 153 | 154 | # Setup the panel background color and layout the controls 155 | self.SetBackgroundColour(wx.WHITE) 156 | self.SetSizer(self.grid) 157 | self.Layout() 158 | 159 | self.Clear() 160 | 161 | def Layout(self): 162 | self.SetScrollbars(20,20,50,50) 163 | super(ColorCheckListBox,self).Layout() 164 | 165 | def Append(self, item, data=None, color=None, refresh=True): 166 | """Add an item to the control.""" 167 | 168 | ccb = ColorCheckBox(self, item, data, color, self.pubsubname) 169 | self.items.append(ccb) 170 | self.grid.Add(ccb, 0, flag=wx.ALIGN_LEFT, border=4) 171 | self.grid.Add((0,3), 0) 172 | if refresh: 173 | self.Layout() 174 | 175 | def Clear(self): 176 | """Removes all items from the control.""" 177 | 178 | self.items = [] 179 | self.grid.Clear(True) 180 | self.grid.Add((0,3), 0) 181 | self.Layout() 182 | 183 | class ColorCheckBox(wx.Panel): 184 | """Control with a checkbox and a color indicator.""" 185 | 186 | def __init__(self, parent, item, data=None, color=None, pubsubname=''): 187 | wx.Panel.__init__(self, parent, -1) 188 | 189 | # Initialize variables 190 | self.item = item 191 | self.data = data 192 | self.pubsubname = pubsubname 193 | 194 | # Initialize the controls 195 | self.colorbox = ColorBox(self, color) 196 | self.checkbox = wx.CheckBox(self, -1, item) 197 | 198 | # Setup the layout for the frame 199 | grid = wx.BoxSizer(wx.HORIZONTAL) 200 | grid.Add((3,0), 0) 201 | grid.Add(self.colorbox, 0, flag=wx.ALIGN_CENTRE) 202 | grid.Add((5,0), 0) 203 | grid.Add(self.checkbox, 1, flag=wx.EXPAND|wx.ALL|wx.ALIGN_CENTRE) 204 | 205 | # Decrease the font size on Mac 206 | if IsMac(): 207 | font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT) 208 | font.SetPointSize(10) 209 | self.checkbox.SetWindowVariant(wx.WINDOW_VARIANT_SMALL) 210 | self.checkbox.SetFont(font) 211 | 212 | # Setup the panel background color and layout the controls 213 | self.SetBackgroundColour(wx.WHITE) 214 | self.SetSizer(grid) 215 | self.Layout() 216 | 217 | # Bind ui events to the proper methods 218 | self.Bind(wx.EVT_CHECKBOX, self.OnCheck) 219 | 220 | def OnCheck(self, evt): 221 | """Send a message via pubsub if the checkbox has been checked.""" 222 | 223 | message = {'item':self.item, 'data':self.data, 224 | 'color':self.colorbox.GetBackgroundColour()} 225 | if evt.IsChecked(): 226 | pub.sendMessage('colorcheckbox.checked.' + self.pubsubname, msg=message) 227 | else: 228 | pub.sendMessage('colorcheckbox.unchecked.' + self.pubsubname, msg=message) 229 | 230 | class ColorBox(wx.Window): 231 | """Control that shows and stores a color.""" 232 | 233 | def __init__(self, parent, color=[]): 234 | wx.Window.__init__(self, parent, -1) 235 | self.SetMinSize((16,16)) 236 | col = [] 237 | for val in color: 238 | col.append(int(val)) 239 | self.SetBackgroundColour(tuple(col)) 240 | 241 | # Bind ui events to the proper methods 242 | self.Bind(wx.EVT_SET_FOCUS, self.OnFocus) 243 | 244 | def OnFocus(self, evt): 245 | """Ignore the focus event via keyboard.""" 246 | 247 | self.Navigate() 248 | -------------------------------------------------------------------------------- /dicompyler/license.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009-2017 Aditya Panchal and dicompyler contributors 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are 7 | met: 8 | 9 | Redistributions of source code must retain the above copyright 10 | notice, this list of conditions and the following disclaimer. 11 | 12 | Redistributions in binary form must reproduce the above copyright 13 | notice, this list of conditions and the following disclaimer in the 14 | documentation and/or other materials provided with the 15 | distribution. 16 | 17 | The name of Aditya Panchal may not be used to endorse or promote 18 | products derived from this software without specific prior written 19 | permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 22 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 23 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 24 | PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER 25 | OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 26 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 27 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 28 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 29 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 30 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 31 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | 33 | -------------------------------------------------------------------------------- 34 | 35 | dicompyler uses the famfamfam Silk icon set created by Mark James found at: 36 | http://famfamfam.com/lab/icons/silk/ 37 | famfamfam Silk is licensed under the Creative Commons Attribution 2.5 License. 38 | 39 | -------------------------------------------------------------------------------- 40 | 41 | dicompyler uses the following libraries: 42 | 43 | pydicom 44 | Copyright (c) 2008-2017 Darcy Mason and pydicom contributors 45 | 46 | They are distributed under the MIT License as follows: 47 | 48 | Permission is hereby granted, free of charge, to any person obtaining a copy 49 | of this software and associated documentation files (the "Software"), to deal 50 | in the Software without restriction, including without limitation the rights 51 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 52 | copies of the Software, and to permit persons to whom the Software is 53 | furnished to do so, subject to the following conditions: 54 | 55 | The above copyright notice and this permission notice shall be included in 56 | all copies or substantial portions of the Software. 57 | 58 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 59 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 60 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 61 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 62 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 63 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 64 | THE SOFTWARE. 65 | 66 | -------------------------------------------------------------------------------- 67 | 68 | dicompyler uses The Python Imaging Library, which is distributed under the 69 | following license: 70 | 71 | The Python Imaging Library (PIL) is 72 | 73 | Copyright © 1997-2011 by Secret Labs AB 74 | Copyright © 1995-2011 by Fredrik Lundh 75 | 76 | Pillow is the friendly PIL fork. It is 77 | 78 | Copyright © 2010-2017 by Alex Clark and contributors 79 | 80 | Like PIL, Pillow is licensed under the open source PIL Software License: 81 | 82 | By obtaining, using, and/or copying this software and/or its associated documentation, you agree that you have read, understood, and will comply with the following terms and conditions: 83 | 84 | Permission to use, copy, modify, and distribute this software and its associated documentation for any purpose and without fee is hereby granted, provided that the above copyright notice appears in all copies, and that both that copyright notice and this permission notice appear in supporting documentation, and that the name of Secret Labs AB or the author not be used in advertising or publicity pertaining to distribution of the software without specific, written prior permission. 85 | 86 | SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 87 | 88 | -------------------------------------------------------------------------------- 89 | 90 | dicompyler uses matplotlib, which is distributed under the following license: 91 | 92 | License agreement for matplotlib versions 1.3.0 and later 93 | ========================================================= 94 | 95 | 1. This LICENSE AGREEMENT is between the Matplotlib Development Team 96 | ("MDT"), and the Individual or Organization ("Licensee") accessing and 97 | otherwise using matplotlib software in source or binary form and its 98 | associated documentation. 99 | 100 | 2. Subject to the terms and conditions of this License Agreement, MDT 101 | hereby grants Licensee a nonexclusive, royalty-free, world-wide license 102 | to reproduce, analyze, test, perform and/or display publicly, prepare 103 | derivative works, distribute, and otherwise use matplotlib 104 | alone or in any derivative version, provided, however, that MDT's 105 | License Agreement and MDT's notice of copyright, i.e., "Copyright (c) 106 | 2012- Matplotlib Development Team; All Rights Reserved" are retained in 107 | matplotlib alone or in any derivative version prepared by 108 | Licensee. 109 | 110 | 3. In the event Licensee prepares a derivative work that is based on or 111 | incorporates matplotlib or any part thereof, and wants to 112 | make the derivative work available to others as provided herein, then 113 | Licensee hereby agrees to include in any such work a brief summary of 114 | the changes made to matplotlib . 115 | 116 | 4. MDT is making matplotlib available to Licensee on an "AS 117 | IS" basis. MDT MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR 118 | IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, MDT MAKES NO AND 119 | DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS 120 | FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF MATPLOTLIB 121 | WILL NOT INFRINGE ANY THIRD PARTY RIGHTS. 122 | 123 | 5. MDT SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF MATPLOTLIB 124 | FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR 125 | LOSS AS A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING 126 | MATPLOTLIB , OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF 127 | THE POSSIBILITY THEREOF. 128 | 129 | 6. This License Agreement will automatically terminate upon a material 130 | breach of its terms and conditions. 131 | 132 | 7. Nothing in this License Agreement shall be deemed to create any 133 | relationship of agency, partnership, or joint venture between MDT and 134 | Licensee. This License Agreement does not grant permission to use MDT 135 | trademarks or trade name in a trademark sense to endorse or promote 136 | products or services of Licensee, or any third party. 137 | 138 | 8. By copying, installing or otherwise using matplotlib , 139 | Licensee agrees to be bound by the terms and conditions of this License 140 | Agreement. 141 | 142 | -------------------------------------------------------------------------------- 143 | 144 | dicompyler uses NumPy, which is distributed under the New BSD license: 145 | 146 | Copyright (c) 2005-2017, NumPy Developers. 147 | All rights reserved. 148 | 149 | Redistribution and use in source and binary forms, with or without 150 | modification, are permitted provided that the following conditions are 151 | met: 152 | 153 | * Redistributions of source code must retain the above copyright 154 | notice, this list of conditions and the following disclaimer. 155 | 156 | * Redistributions in binary form must reproduce the above 157 | copyright notice, this list of conditions and the following 158 | disclaimer in the documentation and/or other materials provided 159 | with the distribution. 160 | 161 | * Neither the name of the NumPy Developers nor the names of any 162 | contributors may be used to endorse or promote products derived 163 | from this software without specific prior written permission. 164 | 165 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 166 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 167 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 168 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 169 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 170 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 171 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 172 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 173 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 174 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 175 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 176 | 177 | -------------------------------------------------------------------------------- 178 | 179 | dicompyler uses Python, which is distributed under the following license: 180 | 181 | PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 182 | -------------------------------------------- 183 | 184 | 1. This LICENSE AGREEMENT is between the Python Software Foundation 185 | ("PSF"), and the Individual or Organization ("Licensee") accessing and 186 | otherwise using this software ("Python") in source or binary form and 187 | its associated documentation. 188 | 189 | 2. Subject to the terms and conditions of this License Agreement, PSF 190 | hereby grants Licensee a nonexclusive, royalty-free, world-wide 191 | license to reproduce, analyze, test, perform and/or display publicly, 192 | prepare derivative works, distribute, and otherwise use Python 193 | alone or in any derivative version, provided, however, that PSF's 194 | License Agreement and PSF's notice of copyright, i.e., "Copyright (c) 195 | 2001, 2002, 2003, 2004, 2005, 2006 Python Software Foundation; All Rights 196 | Reserved" are retained in Python alone or in any derivative version 197 | prepared by Licensee. 198 | 199 | 3. In the event Licensee prepares a derivative work that is based on 200 | or incorporates Python or any part thereof, and wants to make 201 | the derivative work available to others as provided herein, then 202 | Licensee hereby agrees to include in any such work a brief summary of 203 | the changes made to Python. 204 | 205 | 4. PSF is making Python available to Licensee on an "AS IS" 206 | basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR 207 | IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND 208 | DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS 209 | FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT 210 | INFRINGE ANY THIRD PARTY RIGHTS. 211 | 212 | 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON 213 | FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS 214 | A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, 215 | OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. 216 | 217 | 6. This License Agreement will automatically terminate upon a material 218 | breach of its terms and conditions. 219 | 220 | 7. Nothing in this License Agreement shall be deemed to create any 221 | relationship of agency, partnership, or joint venture between PSF and 222 | Licensee. This License Agreement does not grant permission to use PSF 223 | trademarks or trade name in a trademark sense to endorse or promote 224 | products or services of Licensee, or any third party. 225 | 226 | 8. By copying, installing or otherwise using Python, Licensee 227 | agrees to be bound by the terms and conditions of this License 228 | Agreement. 229 | 230 | -------------------------------------------------------------------------------- 231 | 232 | dicompyler uses WxMpl, which is distributed under the following license: 233 | 234 | Copyright 2005-2009 Illinois Institute of Technology 235 | 236 | Permission is hereby granted, free of charge, to any person obtaining 237 | a copy of this software and associated documentation files (the 238 | "Software"), to deal in the Software without restriction, including 239 | without limitation the rights to use, copy, modify, merge, publish, 240 | distribute, sublicense, and/or sell copies of the Software, and to 241 | permit persons to whom the Software is furnished to do so, subject to 242 | the following conditions: 243 | 244 | The above copyright notice and this permission notice shall be 245 | included in all copies or substantial portions of the Software. 246 | 247 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 248 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 249 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 250 | IN NO EVENT SHALL ILLINOIS INSTITUTE OF TECHNOLOGY BE LIABLE FOR ANY 251 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 252 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 253 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 254 | 255 | Except as contained in this notice, the name of Illinois Institute 256 | of Technology shall not be used in advertising or otherwise to promote 257 | the sale, use or other dealings in this Software without prior written 258 | authorization from Illinois Institute of Technology. 259 | 260 | -------------------------------------------------------------------------------- 261 | 262 | dicompyler uses wxPython, which is distributed under the following license: 263 | 264 | wxWindows Library Licence, Version 3.1 265 | ====================================== 266 | 267 | Copyright (c) 1998-2005 Julian Smart, Robert Roebling et al 268 | 269 | Everyone is permitted to copy and distribute verbatim copies 270 | of this licence document, but changing it is not allowed. 271 | 272 | WXWINDOWS LIBRARY LICENCE 273 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 274 | 275 | This library is free software; you can redistribute it and/or modify it 276 | under the terms of the GNU Library General Public Licence as published by 277 | the Free Software Foundation; either version 2 of the Licence, or (at 278 | your option) any later version. 279 | 280 | This library is distributed in the hope that it will be useful, but 281 | WITHOUT ANY WARRANTY; without even the implied warranty of 282 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library 283 | General Public Licence for more details. 284 | 285 | You should have received a copy of the GNU Library General Public Licence 286 | along with this software, usually in a file named COPYING.LIB. If not, 287 | write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, 288 | Boston, MA 02111-1307 USA. 289 | 290 | EXCEPTION NOTICE 291 | 292 | 1. As a special exception, the copyright holders of this library give 293 | permission for additional uses of the text contained in this release of 294 | the library as licenced under the wxWindows Library Licence, applying 295 | either version 3.1 of the Licence, or (at your option) any later version of 296 | the Licence as published by the copyright holders of version 297 | 3.1 of the Licence document. 298 | 299 | 2. The exception is that you may use, copy, link, modify and distribute 300 | under your own terms, binary object code versions of works based 301 | on the Library. 302 | 303 | 3. If you copy code from files distributed under the terms of the GNU 304 | General Public Licence or the GNU Library General Public Licence into a 305 | copy of this library, as this licence permits, the exception does not 306 | apply to the code that you add in this way. To avoid misleading anyone as 307 | to the status of such modified files, you must delete this exception 308 | notice from such code and/or adjust the licensing conditions notice 309 | accordingly. 310 | 311 | 4. If you write modifications of your own for this library, it is your 312 | choice whether to permit this exception to apply to your modifications. 313 | If you do not wish that, you must delete the exception notice from such 314 | code and/or adjust the licensing conditions notice accordingly. 315 | -------------------------------------------------------------------------------- /dicompyler/plugin.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # plugin.py 4 | """Plugin manager for dicompyler.""" 5 | # Copyright (c) 2010-2017 Aditya Panchal 6 | # This file is part of dicompyler, released under a BSD license. 7 | # See the file license.txt included with this distribution, also 8 | # available at https://github.com/bastula/dicompyler/ 9 | 10 | import logging 11 | logger = logging.getLogger('dicompyler.plugin') 12 | import imp, os 13 | import wx 14 | from wx.xrc import * 15 | from wx.lib.pubsub import pub 16 | from dicompyler import guiutil, util 17 | 18 | def import_plugins(userpath=None): 19 | """Find and import available plugins.""" 20 | 21 | # Get the base plugin path 22 | basepath = util.GetBasePluginsPath('') 23 | # Get the user plugin path if it has not been set 24 | if (userpath == None): 25 | datapath = guiutil.get_data_dir() 26 | userpath = os.path.join(datapath, 'plugins') 27 | # Get the list of possible plugins from both paths 28 | possibleplugins = [] 29 | for i in os.listdir(userpath): 30 | possibleplugins.append({'plugin': i, 'location': 'user'}) 31 | for i in os.listdir(basepath): 32 | possibleplugins.append({'plugin': i, 'location': 'base'}) 33 | 34 | modules = [] 35 | plugins = [] 36 | for p in possibleplugins: 37 | module = p['plugin'].split('.')[0] 38 | if module not in modules: 39 | if not ((module == "__init__") or (module == "")): 40 | # only try to import the module once 41 | modules.append(module) 42 | try: 43 | f, filename, description = \ 44 | imp.find_module(module, [userpath, basepath]) 45 | except ImportError: 46 | # Not able to find module so pass 47 | pass 48 | else: 49 | # Try to import the module if no exception occurred 50 | try: 51 | m = imp.load_module(module, f, filename, description) 52 | except ImportError: 53 | logger.exception("%s could not be loaded", module) 54 | else: 55 | plugins.append({'plugin': m, 56 | 'location': p['location']}) 57 | logger.debug("%s loaded", module) 58 | # If the module is a single file, close it 59 | if not (description[2] == imp.PKG_DIRECTORY): 60 | f.close() 61 | return plugins 62 | 63 | def PluginManager(parent, plugins, pluginsDisabled): 64 | """Prepare to show the plugin manager dialog.""" 65 | 66 | # Load the XRC file for our gui resources 67 | res = XmlResource(util.GetResourcePath('plugin.xrc')) 68 | 69 | dlgPluginManager = res.LoadDialog(parent, "PluginManagerDialog") 70 | dlgPluginManager.Init(plugins, pluginsDisabled) 71 | 72 | # Show the dialog 73 | dlgPluginManager.ShowModal() 74 | 75 | class PluginManagerDialog(wx.Dialog): 76 | """Manage the available plugins.""" 77 | 78 | def __init__(self): 79 | wx.Dialog.__init__(self) 80 | 81 | def Init(self, plugins, pluginsDisabled): 82 | """Method called after the panel has been initialized.""" 83 | 84 | # Set window icon 85 | if not guiutil.IsMac(): 86 | self.SetIcon(guiutil.get_icon()) 87 | 88 | # Initialize controls 89 | self.tcPlugins = XRCCTRL(self, 'tcPlugins') 90 | self.panelTreeView = XRCCTRL(self, 'panelTreeView') 91 | self.panelProperties = XRCCTRL(self, 'panelProperties') 92 | self.lblName = XRCCTRL(self, 'lblName') 93 | self.lblAuthor = XRCCTRL(self, 'lblAuthor') 94 | self.lblPluginType = XRCCTRL(self, 'lblPluginType') 95 | self.lblVersion = XRCCTRL(self, 'lblVersion') 96 | self.lblVersionNumber = XRCCTRL(self, 'lblVersionNumber') 97 | self.lblDescription = XRCCTRL(self, 'lblDescription') 98 | self.checkEnabled = XRCCTRL(self, 'checkEnabled') 99 | self.lblMessage = XRCCTRL(self, 'lblMessage') 100 | self.btnGetMorePlugins = XRCCTRL(self, 'btnGetMorePlugins') 101 | self.btnDeletePlugin = XRCCTRL(self, 'btnDeletePlugin') 102 | 103 | self.plugins = plugins 104 | self.pluginsDisabled = set(pluginsDisabled) 105 | 106 | # Bind interface events to the proper methods 107 | # wx.EVT_BUTTON(self, XRCID('btnDeletePlugin'), self.DeletePlugin) 108 | # wx.EVT_CHECKBOX(self, XRCID('checkEnabled'), self.OnEnablePlugin) 109 | self.Bind(wx.EVT_CHECKBOX, self.OnEnablePlugin, id=XRCID('checkEnabled')) 110 | # wx.EVT_TREE_ITEM_ACTIVATED(self, XRCID('tcPlugins'), self.OnEnablePlugin) 111 | self.Bind(wx.EVT_TREE_ITEM_ACTIVATED, self.OnEnablePlugin, id=XRCID('tcPlugins')) 112 | # wx.EVT_TREE_SEL_CHANGED(self, XRCID('tcPlugins'), self.OnSelectTreeItem) 113 | self.Bind(wx.EVT_TREE_SEL_CHANGED, self.OnSelectTreeItem, id=XRCID('tcPlugins')) 114 | # wx.EVT_TREE_SEL_CHANGING(self, XRCID('tcPlugins'), self.OnSelectRootItem) 115 | self.Bind(wx.EVT_TREE_SEL_CHANGING, self.OnSelectRootItem, id=XRCID('tcPlugins')) 116 | 117 | # Modify the control and font size as needed 118 | font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT) 119 | if guiutil.IsMac(): 120 | children = list(self.Children) + \ 121 | list(self.panelTreeView.Children) + \ 122 | list(self.panelProperties.Children) 123 | for control in children: 124 | control.SetFont(font) 125 | control.SetWindowVariant(wx.WINDOW_VARIANT_SMALL) 126 | XRCCTRL(self, 'wxID_OK').SetWindowVariant(wx.WINDOW_VARIANT_NORMAL) 127 | font.SetWeight(wx.FONTWEIGHT_BOLD) 128 | if guiutil.IsMSWindows(): 129 | self.tcPlugins.SetPosition((0, 3)) 130 | self.panelTreeView.SetWindowStyle(wx.STATIC_BORDER) 131 | if (guiutil.IsMac() or guiutil.IsGtk()): 132 | self.tcPlugins.SetPosition((-30, 0)) 133 | self.panelTreeView.SetWindowStyle(wx.SUNKEN_BORDER) 134 | self.lblName.SetFont(font) 135 | self.lblMessage.SetFont(font) 136 | 137 | self.Layout() 138 | self.InitPluginList() 139 | self.LoadPlugins() 140 | 141 | def InitPluginList(self): 142 | """Initialize the plugin list control.""" 143 | 144 | iSize = (16, 16) 145 | iList = wx.ImageList(iSize[0], iSize[1]) 146 | iList.Add( 147 | wx.Bitmap( 148 | util.GetResourcePath('bricks.png'), 149 | wx.BITMAP_TYPE_PNG)) 150 | iList.Add( 151 | wx.Bitmap( 152 | util.GetResourcePath('plugin.png'), 153 | wx.BITMAP_TYPE_PNG)) 154 | iList.Add( 155 | wx.Bitmap( 156 | util.GetResourcePath('plugin_disabled.png'), 157 | wx.BITMAP_TYPE_PNG)) 158 | self.tcPlugins.AssignImageList(iList) 159 | self.root = self.tcPlugins.AddRoot('Plugins') 160 | self.baseroot = self.tcPlugins.AppendItem( 161 | self.root, "Built-In Plugins", 0) 162 | self.userroot = self.tcPlugins.AppendItem( 163 | self.root, "User Plugins", 0) 164 | 165 | font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT) 166 | font.SetWeight(wx.FONTWEIGHT_BOLD) 167 | self.tcPlugins.SetItemFont(self.baseroot, font) 168 | self.tcPlugins.SetItemFont(self.userroot, font) 169 | 170 | def LoadPlugins(self): 171 | """Update and load the data for the plugin list control.""" 172 | 173 | # Set up the plugins for each plugin entry point of dicompyler 174 | for n, plugin in enumerate(self.plugins): 175 | # Skip plugin if it doesn't contain the required dictionary 176 | # or actually is a proper Python module 177 | p = plugin['plugin'] 178 | if not hasattr(p, 'pluginProperties'): 179 | continue 180 | props = p.pluginProperties() 181 | root = self.userroot 182 | if (plugin['location'] == 'base'): 183 | root = self.baseroot 184 | else: 185 | root = self.userroot 186 | i = self.tcPlugins.AppendItem(root, props['name'], 1) 187 | 188 | if (p.__name__ in self.pluginsDisabled): 189 | self.tcPlugins.SetItemImage(i, 2) 190 | self.tcPlugins.SetItemTextColour(i, wx.Colour(169, 169, 169)) 191 | 192 | self.tcPlugins.SetItemData(i, n) 193 | self.tcPlugins.SelectItem(i) 194 | self.tcPlugins.ExpandAll() 195 | self.Bind( 196 | wx.EVT_TREE_ITEM_COLLAPSING, 197 | self.OnExpandCollapseTree, 198 | id=XRCID('tcPlugins')) 199 | self.Bind( 200 | wx.EVT_TREE_ITEM_EXPANDING, 201 | self.OnExpandCollapseTree, 202 | id=XRCID('tcPlugins')) 203 | 204 | def OnSelectTreeItem(self, evt): 205 | """Update the interface when the selected item has changed.""" 206 | 207 | item = evt.GetItem() 208 | n = self.tcPlugins.GetItemData(item) 209 | if (n == None): 210 | self.panelProperties.Hide() 211 | return 212 | self.panelProperties.Show() 213 | plugin = self.plugins[n] 214 | p = plugin['plugin'] 215 | props = p.pluginProperties() 216 | self.lblName.SetLabel(props['name']) 217 | self.lblAuthor.SetLabel(props['author'].replace('&', '&&')) 218 | self.lblVersionNumber.SetLabel(str(props['version'])) 219 | ptype = props['plugin_type'] 220 | self.lblPluginType.SetLabel(ptype[0].capitalize() + ptype[1:]) 221 | self.lblDescription.SetLabel(props['description'].replace('&', '&&')) 222 | 223 | self.checkEnabled.SetValue(not (p.__name__ in self.pluginsDisabled)) 224 | 225 | self.Layout() 226 | self.panelProperties.Layout() 227 | 228 | def OnSelectRootItem(self, evt): 229 | """Block the root items from being selected.""" 230 | 231 | item = evt.GetItem() 232 | n = self.tcPlugins.GetItemData(item) 233 | if (n == None): 234 | evt.Veto() 235 | 236 | def OnExpandCollapseTree(self, evt): 237 | """Block the tree from expanding or collapsing.""" 238 | 239 | evt.Veto() 240 | 241 | def OnEnablePlugin(self, evt=None): 242 | """Publish the enabled/disabled state of the plugin.""" 243 | 244 | item = self.tcPlugins.GetSelection() 245 | n = self.tcPlugins.GetItemData(item) 246 | plugin = self.plugins[n] 247 | p = plugin['plugin'] 248 | 249 | # Set the checkbox to the appropriate state if the event 250 | # comes from the treeview 251 | if (evt.EventType == wx.EVT_TREE_ITEM_ACTIVATED.typeId): 252 | self.checkEnabled.SetValue(not self.checkEnabled.IsChecked()) 253 | 254 | if self.checkEnabled.IsChecked(): 255 | self.tcPlugins.SetItemImage(item, 1) 256 | self.tcPlugins.SetItemTextColour(item, wx.BLACK) 257 | self.pluginsDisabled.remove(p.__name__) 258 | logger.debug("%s enabled", p.__name__) 259 | else: 260 | self.tcPlugins.SetItemImage(item, 2) 261 | self.tcPlugins.SetItemTextColour(item, wx.Colour(169, 169, 169)) 262 | self.pluginsDisabled.add(p.__name__) 263 | logger.debug("%s disabled", p.__name__) 264 | 265 | pub.sendMessage('preferences.updated.value', 266 | msg={'general.plugins.disabled_list': list(self.pluginsDisabled)}) 267 | -------------------------------------------------------------------------------- /dicompyler/preferences.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # preferences.py 4 | """Preferences manager for dicompyler.""" 5 | # Copyright (c) 2011-2017 Aditya Panchal 6 | # This file is part of dicompyler, released under a BSD license. 7 | # See the file license.txt included with this distribution, also 8 | # available at https://github.com/bastula/dicompyler/ 9 | 10 | import os 11 | import wx 12 | from wx.xrc import * 13 | from wx.lib.pubsub import pub 14 | from dicompyler import guiutil, util 15 | 16 | try: 17 | # Only works on Python 2.6 and above 18 | import json 19 | except ImportError: 20 | # Otherwise try simplejson: http://github.com/simplejson/simplejson 21 | import simplejson as json 22 | 23 | class PreferencesManager(): 24 | """Class to access preferences and set up the preferences dialog.""" 25 | 26 | def __init__(self, parent, name = None, appname = "the application", 27 | filename='preferences.txt'): 28 | 29 | # Load the XRC file for our gui resources 30 | res = XmlResource(util.GetResourcePath('preferences.xrc')) 31 | self.dlgPreferences = res.LoadDialog(None, "PreferencesDialog") 32 | #self.dlgPreferences = PreferencesDialog(parent,name=name) 33 | self.dlgPreferences.Init(name, appname) 34 | 35 | # Setup internal pubsub methods 36 | pub.subscribe(self.SetPreferenceTemplate, 'preferences.updated.template') 37 | pub.subscribe(self.SavePreferenceValues, 'preferences.updated.values') 38 | 39 | # Setup user pubsub methods 40 | pub.subscribe(self.GetPreferenceValue, 'preferences.requested.value') 41 | pub.subscribe(self.GetPreferenceValues, 'preferences.requested.values') 42 | pub.subscribe(self.SetPreferenceValue, 'preferences.updated.value') 43 | 44 | # Initialize variables 45 | self.preftemplate = [] 46 | self.values = {} 47 | self.filename = os.path.join(guiutil.get_data_dir(), filename) 48 | self.LoadPreferenceValues() 49 | 50 | def __del__(self): 51 | 52 | # Destroy the dialog when the preferences manager object is deleted 53 | if self.dlgPreferences: 54 | 55 | self.dlgPreferences.Destroy() 56 | 57 | def Show(self): 58 | """Show the preferences dialog with the given preferences.""" 59 | 60 | # If the pref dialog has never been shown, load the values and show it 61 | if not self.dlgPreferences.IsShown(): 62 | self.dlgPreferences.LoadPreferences(self.preftemplate, self.values) 63 | self.dlgPreferences.Hide() 64 | # Otherwise, hide the dialog and redisplay it to bring it to the front 65 | else: 66 | self.dlgPreferences.Hide() 67 | self.dlgPreferences.Show() 68 | 69 | def SetPreferenceTemplate(self, msg): 70 | """Set the template that the preferences will be shown in the dialog.""" 71 | 72 | self.preftemplate = msg 73 | self.dlgPreferences.LoadPreferences(self.preftemplate, self.values) 74 | 75 | def LoadPreferenceValues(self): 76 | """Load the saved preference values from disk.""" 77 | 78 | if os.path.isfile(self.filename): 79 | with open(self.filename, mode='r') as f: 80 | try: 81 | self.values = json.load(f) 82 | except ValueError: 83 | self.values = {} 84 | else: 85 | self.values = {} 86 | 87 | def SavePreferenceValues(self, msg): 88 | """Save the preference values to disk after the dialog is closed.""" 89 | 90 | self.values = msg 91 | with open(self.filename, mode='w') as f: 92 | json.dump(self.values, f, sort_keys=True, indent=4) 93 | 94 | def GetPreferenceValue(self, msg): 95 | """Publish the requested value for a single preference setting.""" 96 | 97 | query = msg.split('.') 98 | v = self.values 99 | if query[0] in v: 100 | if query[1] in v[query[0]]: 101 | if query[2] in v[query[0]][query[1]]: 102 | pub.sendMessage(msg, topic=msg, msg=v[query[0]][query[1]][query[2]]) 103 | 104 | def GetPreferenceValues(self, msg): 105 | """Publish the requested values for preference setting group.""" 106 | 107 | query = msg.split('.') 108 | v = self.values 109 | if query[0] in v: 110 | if query[1] in v[query[0]]: 111 | for setting, value in list(v[query[0]][query[1]].items()): 112 | message = msg + '.' + setting 113 | pub.sendMessage(message, topic='.'.join(['general',setting]), msg=value) 114 | 115 | def SetPreferenceValue(self, msg): 116 | """Set the preference value for the given preference setting.""" 117 | 118 | #Using list() may break threading. 119 | #See https://blog.labix.org/2008/06/27/watch-out-for-listdictkeys-in-python-3 120 | SetValue(self.values, list(msg.keys())[0], list(msg.values())[0]) 121 | pub.sendMessage('preferences.updated.values', msg=self.values) 122 | pub.sendMessage(list(msg.keys())[0], topic=list(msg.keys())[0], msg=list(msg.values())[0]) 123 | 124 | ############################## Preferences Dialog ############################## 125 | 126 | class PreferencesDialog(wx.Dialog): 127 | """Dialog to display and change preferences.""" 128 | 129 | def __init__(self): 130 | wx.Dialog.__init__(self) 131 | 132 | def Init(self, name = None, appname = ""): 133 | """Method called after the panel has been initialized.""" 134 | 135 | # Hide the close button on Mac 136 | if guiutil.IsMac(): 137 | XRCCTRL(self, 'wxID_OK').Hide() 138 | # Set window icon 139 | else: 140 | self.SetIcon(guiutil.get_icon()) 141 | 142 | # Set the dialog title 143 | if name: 144 | self.SetTitle(name) 145 | 146 | # Initialize controls 147 | self.notebook = XRCCTRL(self, 'notebook') 148 | 149 | # Modify the control and font size on Mac 150 | for child in self.GetChildren(): 151 | guiutil.adjust_control(child) 152 | 153 | # Bind ui events to the proper methods 154 | self.Bind(wx.EVT_CLOSE, self.OnClose) 155 | self.Bind(wx.EVT_WINDOW_DESTROY, self.OnClose) 156 | self.Bind(wx.EVT_BUTTON, self.OnClose, id=wx.ID_OK) 157 | 158 | # Initialize variables 159 | self.preftemplate = [] 160 | self.values = {} 161 | self.appname = appname 162 | 163 | def LoadPreferences(self, preftemplate, values): 164 | """Update and load the data for the preferences notebook control.""" 165 | 166 | self.preftemplate = preftemplate 167 | self.values = values 168 | 169 | # Delete and reset all the previous preference panels 170 | self.notebook.DeleteAllPages() 171 | self.callbackdict = {} 172 | 173 | # Add each preference panel to the notebook 174 | for template in self.preftemplate: 175 | panel = self.CreatePreferencePanel(list(template.values())[0]) 176 | self.notebook.AddPage(panel, list(template.keys())[0]) 177 | 178 | def CreatePreferencePanel(self, prefpaneldata): 179 | """Create a preference panel for the given data.""" 180 | 181 | panel = wx.Panel(self.notebook, -1) 182 | border = wx.BoxSizer(wx.VERTICAL) 183 | show_restart = False 184 | 185 | for group in prefpaneldata: 186 | # Create a header for each group of settings 187 | bsizer = wx.BoxSizer(wx.VERTICAL) 188 | bsizer.Add((0,5)) 189 | hsizer = wx.BoxSizer(wx.HORIZONTAL) 190 | hsizer.Add((12, 0)) 191 | h = wx.StaticText(panel, -1, list(group.keys())[0]) 192 | font = h.GetFont() 193 | font.SetWeight(wx.FONTWEIGHT_BOLD) 194 | h.SetFont(font) 195 | hsizer.Add(h) 196 | bsizer.Add(hsizer) 197 | bsizer.Add((0,7)) 198 | # Create a FlexGridSizer to contain the group of settings 199 | fgsizer = wx.FlexGridSizer(len(list(group.values())[0]), 4, 10, 4) 200 | fgsizer.AddGrowableCol(2, 1) 201 | # Create controls for each setting 202 | for setting in list(group.values())[0]: 203 | fgsizer.Add((24, 0)) 204 | # Show the restart asterisk for this setting if required 205 | restart = str('*' if 'restart' in setting else '') 206 | if ('restart' in setting): 207 | if (setting['restart'] == True): 208 | show_restart = True 209 | t = wx.StaticText(panel, -1, setting['name']+restart+':', 210 | style=wx.ALIGN_RIGHT) 211 | fgsizer.Add(t, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALIGN_RIGHT) 212 | sizer = wx.BoxSizer(wx.HORIZONTAL) 213 | 214 | # Get the setting value 215 | value = GetValue(self.values, setting) 216 | # Save the setting value in case it hasn't been saved previously 217 | SetValue(self.values, setting['callback'], value) 218 | 219 | # If this is a choice setting 220 | if (setting['type'] == 'choice'): 221 | c = wx.Choice(panel, -1, choices=setting['values']) 222 | c.SetStringSelection(value) 223 | sizer.Add(c, 0, wx.ALIGN_CENTER) 224 | # Add control to the callback dict 225 | self.callbackdict[c] = setting['callback'] 226 | self.Bind(wx.EVT_CHOICE, self.OnUpdateChoice, c) 227 | # If this is a checkbox setting 228 | elif (setting['type'] == 'checkbox'): 229 | c = wx.CheckBox(panel, -1, setting['name']+restart) 230 | c.SetValue(value) 231 | sizer.Add(c, 0, wx.ALIGN_CENTER) 232 | # Remove the label preceding the checkbox 233 | t = c.GetPrevSibling() 234 | t.SetLabel('') 235 | # Adjust the sizer preceding the label 236 | fgsizer.GetItem(0).AssignSpacer((20,0)) 237 | # Add control to the callback dict 238 | self.callbackdict[c] = setting['callback'] 239 | self.Bind(wx.EVT_CHECKBOX, self.OnUpdateCheckbox, c) 240 | # If this is a range setting 241 | elif (setting['type'] == 'range'): 242 | s = wx.Slider(panel, -1, value, 243 | setting['values'][0], setting['values'][1], 244 | size=(120, -1), style=wx.SL_HORIZONTAL) 245 | sizer.Add(s, 0, wx.ALIGN_CENTER) 246 | t = wx.StaticText(panel, -1, str(value)) 247 | sizer.Add((3, 0)) 248 | sizer.Add(t, 0, wx.ALIGN_CENTER) 249 | sizer.Add((6, 0)) 250 | t = wx.StaticText(panel, -1, setting['units']) 251 | sizer.Add(t, 0, wx.ALIGN_CENTER) 252 | # Add control to the callback dict 253 | self.callbackdict[s] = setting['callback'] 254 | self.Bind(wx.EVT_COMMAND_SCROLL_THUMBTRACK, self.OnUpdateSlider, s) 255 | self.Bind(wx.EVT_COMMAND_SCROLL_CHANGED, self.OnUpdateSlider, s) 256 | # If this is a directory location setting 257 | elif (setting['type'] == 'directory'): 258 | # Check if the value is a valid directory, 259 | # otherwise set it to the default directory 260 | if not os.path.isdir(value): 261 | value = setting['default'] 262 | SetValue(self.values, setting['callback'], value) 263 | t = wx.TextCtrl(panel, -1, value, style=wx.TE_READONLY) 264 | sizer.Add(t, 1, wx.ALIGN_CENTER) 265 | sizer.Add((5, 0)) 266 | b = wx.Button(panel, -1, "Browse...") 267 | sizer.Add(b, 0, wx.ALIGN_CENTER) 268 | # Add control to the callback dict 269 | self.callbackdict[b] = setting['callback'] 270 | self.Bind(wx.EVT_BUTTON, self.OnUpdateDirectory, b) 271 | # Modify the control and font size on Mac 272 | for child in panel.GetChildren(): 273 | guiutil.adjust_control(child) 274 | fgsizer.Add(sizer, 1, wx.EXPAND|wx.ALL) 275 | fgsizer.Add((12, 0)) 276 | bsizer.Add(fgsizer, 0, wx.EXPAND|wx.ALL) 277 | border.Add(bsizer, 0, wx.EXPAND|wx.ALL, 2) 278 | border.Add((60, 20), 0, wx.EXPAND|wx.ALL) 279 | # Show the restart text for this group if required for >= 1 setting 280 | if show_restart: 281 | r = wx.StaticText(panel, -1, 282 | '* Restart ' + self.appname + \ 283 | ' for this setting to take effect.', 284 | style=wx.ALIGN_CENTER) 285 | font = r.GetFont() 286 | font.SetWeight(wx.FONTWEIGHT_BOLD) 287 | r.SetFont(font) 288 | border.Add((0,0), 1, wx.EXPAND|wx.ALL) 289 | rhsizer = wx.BoxSizer(wx.HORIZONTAL) 290 | rhsizer.Add((0,0), 1, wx.EXPAND|wx.ALL) 291 | rhsizer.Add(r) 292 | rhsizer.Add((0,0), 1, wx.EXPAND|wx.ALL) 293 | border.Add(rhsizer, 0, wx.EXPAND|wx.ALL) 294 | border.Add((0,5)) 295 | panel.SetSizer(border) 296 | 297 | return panel 298 | 299 | def OnUpdateChoice(self, evt): 300 | """Publish the updated choice when the value changes.""" 301 | 302 | c = evt.GetEventObject() 303 | pub.sendMessage(self.callbackdict[c], topic=self.callbackdict[c], msg=evt.GetString()) 304 | SetValue(self.values, self.callbackdict[c], evt.GetString()) 305 | 306 | def OnUpdateCheckbox(self, evt): 307 | """Publish the updated checkbox when the value changes.""" 308 | 309 | c = evt.GetEventObject() 310 | pub.sendMessage(self.callbackdict[c], topic=self.callbackdict[c], msg=evt.IsChecked()) 311 | SetValue(self.values, self.callbackdict[c], evt.IsChecked()) 312 | 313 | def OnUpdateSlider(self, evt): 314 | """Publish the updated number when the slider value changes.""" 315 | 316 | s = evt.GetEventObject() 317 | # Update the associated label with the new number 318 | t = self.FindWindowById(s.NextControlId(s.GetId())) 319 | t.SetLabel(str(s.GetValue())) 320 | pub.sendMessage(self.callbackdict[s], topic=self.callbackdict[s], msg=s.GetValue()) 321 | SetValue(self.values, self.callbackdict[s], s.GetValue()) 322 | 323 | def OnUpdateDirectory(self, evt): 324 | """Publish the updated directory when the value changes.""" 325 | 326 | b = evt.GetEventObject() 327 | # Get the the label associated with the browse button 328 | t = b.GetPrevSibling() 329 | dlg = wx.DirDialog(self, defaultPath = t.GetValue()) 330 | 331 | if dlg.ShowModal() == wx.ID_OK: 332 | # Update the associated label with the new directory 333 | d = str(dlg.GetPath()) 334 | t.SetValue(d) 335 | pub.sendMessage(self.callbackdict[b], topic=self.callbackdict[b], msg=d) 336 | SetValue(self.values, self.callbackdict[b], d) 337 | dlg.Destroy() 338 | 339 | def OnClose(self, evt): 340 | """Publish the updated preference values when closing the dialog.""" 341 | 342 | pub.sendMessage('preferences.updated.values', msg=self.values) 343 | if self: 344 | self.Hide() 345 | 346 | ############################ Get/Set Value Functions ########################### 347 | 348 | def GetValue(values, setting): 349 | """Get the saved setting value.""" 350 | 351 | # Look for the saved value and return it if it exists 352 | query = setting['callback'].split('.') 353 | value = setting['default'] 354 | if query[0] in values: 355 | if query[1] in values[query[0]]: 356 | if query[2] in values[query[0]][query[1]]: 357 | value = values[query[0]][query[1]][query[2]] 358 | # Otherwise return the default value 359 | return value 360 | 361 | def SetValue(values, setting, value): 362 | """Save the new setting value.""" 363 | 364 | # Look if a prior value exists and replace it 365 | query = setting.split('.') 366 | if query[0] in values: 367 | if query[1] in values[query[0]]: 368 | values[query[0]][query[1]][query[2]] = value 369 | else: 370 | values[query[0]].update({query[1]:{query[2]:value}}) 371 | else: 372 | values[query[0]] = {query[1]:{query[2]:value}} 373 | 374 | ############################### Test Preferences ############################### 375 | 376 | def main(): 377 | 378 | import tempfile, os 379 | import wx 380 | from wx.lib.pubsub import Publisher as pub 381 | 382 | app = wx.App(False) 383 | 384 | t = tempfile.NamedTemporaryFile(delete=False) 385 | sp = wx.StandardPaths.Get() 386 | 387 | # Create a frame as a parent for the preferences dialog 388 | frame = wx.Frame(None, wx.ID_ANY, "Preferences Test") 389 | frame.Centre() 390 | frame.Show(True) 391 | app.SetTopWindow(frame) 392 | 393 | filename = t.name 394 | frame.prefmgr = PreferencesManager(parent = frame, appname = 'preftest', 395 | filename=filename) 396 | 397 | # Set up the preferences template 398 | grp1template = [ 399 | {'Panel 1 Preference Group 1': 400 | [{'name':'Choice Setting', 401 | 'type':'choice', 402 | 'values':['Choice 1', 'Choice 2', 'Choice 3'], 403 | 'default':'Choice 2', 404 | 'callback':'panel1.prefgrp1.choice_setting'}, 405 | {'name':'Directory setting', 406 | 'type':'directory', 407 | 'default':str(sp.GetDocumentsDir()), 408 | 'callback':'panel1.prefgrp1.directory_setting'}] 409 | }, 410 | {'Panel 1 Preference Group 2': 411 | [{'name':'Range Setting', 412 | 'type':'range', 413 | 'values':[0, 100], 414 | 'default':50, 415 | 'units':'%', 416 | 'callback':'panel1.prefgrp2.range_setting'}] 417 | }] 418 | grp2template = [ 419 | {'Panel 2 Preference Group 1': 420 | [{'name':'Range Setting', 421 | 'type':'range', 422 | 'values':[0, 100], 423 | 'default':50, 424 | 'units':'%', 425 | 'callback':'panel2.prefgrp1.range_setting', 426 | 'restart':True}] 427 | }, 428 | {'Panel 2 Preference Group 2': 429 | [{'name':'Directory setting', 430 | 'type':'directory', 431 | 'default':str(sp.GetUserDataDir()), 432 | 'callback':'panel2.prefgrp2.directory_setting'}, 433 | {'name':'Choice Setting', 434 | 'type':'choice', 435 | 'values':['Choice 1', 'Choice 2', 'Choice 3'], 436 | 'default':'Choice 2', 437 | 'callback':'panel2.prefgrp2.choice_setting'}] 438 | }] 439 | preftemplate = [{'Panel 1':grp1template}, {'Panel 2':grp2template}] 440 | 441 | def print_template_value(msg): 442 | """Print the received template message.""" 443 | print(msg.topic, msg) 444 | 445 | # Subscribe the template value printer to each set of preferences 446 | pub.subscribe(print_template_value, 'panel1') 447 | pub.subscribe(print_template_value, 'panel2') 448 | 449 | # Notify the preferences manager that a pref template is available 450 | pub.sendMessage('preferences.updated.template', msg=preftemplate) 451 | 452 | frame.prefmgr.Show() 453 | app.MainLoop() 454 | 455 | # Print the results of the preferences 456 | with open(filename, mode='r') as f: 457 | for line in f: 458 | print(line) 459 | 460 | try: 461 | os.remove(filename) 462 | except WindowsError: 463 | print('\nCould not delete: '+filename+'. Please delete it manually.') 464 | 465 | if __name__ == '__main__': 466 | main() 467 | -------------------------------------------------------------------------------- /dicompyler/resources/accept.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bastula/dicompyler/ffba3a83dbbc56ee3b10ca43abcf959fca4c962f/dicompyler/resources/accept.png -------------------------------------------------------------------------------- /dicompyler/resources/book.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bastula/dicompyler/ffba3a83dbbc56ee3b10ca43abcf959fca4c962f/dicompyler/resources/book.png -------------------------------------------------------------------------------- /dicompyler/resources/bricks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bastula/dicompyler/ffba3a83dbbc56ee3b10ca43abcf959fca4c962f/dicompyler/resources/bricks.png -------------------------------------------------------------------------------- /dicompyler/resources/chart_bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bastula/dicompyler/ffba3a83dbbc56ee3b10ca43abcf959fca4c962f/dicompyler/resources/chart_bar.png -------------------------------------------------------------------------------- /dicompyler/resources/chart_bar_error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bastula/dicompyler/ffba3a83dbbc56ee3b10ca43abcf959fca4c962f/dicompyler/resources/chart_bar_error.png -------------------------------------------------------------------------------- /dicompyler/resources/chart_curve.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bastula/dicompyler/ffba3a83dbbc56ee3b10ca43abcf959fca4c962f/dicompyler/resources/chart_curve.png -------------------------------------------------------------------------------- /dicompyler/resources/chart_curve_error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bastula/dicompyler/ffba3a83dbbc56ee3b10ca43abcf959fca4c962f/dicompyler/resources/chart_curve_error.png -------------------------------------------------------------------------------- /dicompyler/resources/cog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bastula/dicompyler/ffba3a83dbbc56ee3b10ca43abcf959fca4c962f/dicompyler/resources/cog.png -------------------------------------------------------------------------------- /dicompyler/resources/contrast_high.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bastula/dicompyler/ffba3a83dbbc56ee3b10ca43abcf959fca4c962f/dicompyler/resources/contrast_high.png -------------------------------------------------------------------------------- /dicompyler/resources/dicomgui.xrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | wxVERTICAL 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | wxALIGN_LEFT|wxALIGN_CENTRE_VERTICAL 16 | 17 | 18 | 5,0 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | wxALL|wxEXPAND|wxALIGN_CENTRE 27 | 28 | 29 | wxVERTICAL 30 | 31 | 0,5 32 | 33 | 34 | 35 | 36 | 37 | 38 | 1 39 | 40 | 41 | 42 | 43 | 44 | wxALL|wxEXPAND|wxALIGN_CENTRE 45 | 3 46 | 47 | 48 | 49 | 50 | 51 | 52 | 2,0 53 | wxALL|wxEXPAND|wxALIGN_CENTRE 54 | 55 | 56 | 57 | accept.png 58 | 59 | wxALL|wxEXPAND|wxALIGN_CENTRE 60 | 16,16 61 | 62 | 63 | 2,0 64 | wxALL|wxEXPAND|wxALIGN_CENTRE 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | wxALIGN_LEFT 74 | 75 | 76 | 77 | 78 | 79 | 80 | wxALIGN_LEFT 81 | 82 | wxVERTICAL 83 | 84 | 85 | wxALL|wxEXPAND|wxALIGN_CENTRE 86 | 87 | wxHORIZONTAL 88 | 89 | wxALL|wxEXPAND|wxALIGN_CENTRE 90 | 91 | 92 | 0,5 93 | 94 | 95 | 96 | 455,300 97 | 98 | 99 | 100 | wxALL|wxEXPAND|wxALIGN_CENTRE 101 | 102 | 103 | 0,5 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | wxALIGN_CENTRE 113 | 114 | 115 | 116 | 117 | 118 | 119 | wxALIGN_CENTRE 120 | 121 | 122 | 5,0 123 | 124 | 125 | 126 | 100,15 127 | 100 128 | 75 129 | 130 | 131 | wxALIGN_CENTRE 132 | 133 | 134 | 5,0 135 | 136 | 137 | 138 | 139 | 140 | 141 | wxALIGN_CENTRE 142 | 143 | 144 | 145 | 146 | 147 | wxALIGN_CENTRE 148 | 149 | wxHORIZONTAL 150 | 151 | wxALL|wxEXPAND|wxALIGN_CENTRE 152 | 153 | 154 | 0,5 155 | 156 | 157 | 158 | 159 | 160 | error.png 161 | 162 | wxALIGN_CENTRE 163 | 164 | 165 | 5,0 166 | 167 | 168 | 169 | 170 | 171 | wxALIGN_CENTRE 172 | 173 | 174 | 5,0 175 | 176 | 177 | 178 | 179 | 80,22 180 | 1 181 | 1 182 | 99999 183 | 184 | wxALIGN_CENTRE 185 | 186 | 187 | 5,0 188 | 189 | 190 | 191 | 192 | 193 | wxALIGN_CENTRE 194 | 195 | 196 | 197 | 198 | 199 | wxALIGN_CENTRE 200 | 201 | wxHORIZONTAL 202 | 203 | wxALL|wxEXPAND|wxALIGN_CENTRE 204 | 205 | 206 | 0,9 207 | 208 | wxVERTICAL 209 | 210 | wxALL|wxEXPAND|wxALIGN_CENTRE 211 | 3 212 | 213 | 214 | 0,5 215 | 216 | 217 | 218 | 219 | 220 | 223 | 224 | 225 | 226 | 227 | 1 228 | 0 229 | 230 | 231 | 232 | wxALL|wxEXPAND|wxALIGN_CENTRE 233 | 3 234 | 235 | 236 | 0,5 237 | 238 | 239 | Open Patient 240 | 1 241 | 242 | 243 | -------------------------------------------------------------------------------- /dicompyler/resources/dicompyler.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bastula/dicompyler/ffba3a83dbbc56ee3b10ca43abcf959fca4c962f/dicompyler/resources/dicompyler.icns -------------------------------------------------------------------------------- /dicompyler/resources/dicompyler.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bastula/dicompyler/ffba3a83dbbc56ee3b10ca43abcf959fca4c962f/dicompyler/resources/dicompyler.ico -------------------------------------------------------------------------------- /dicompyler/resources/dicompyler_icon11_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bastula/dicompyler/ffba3a83dbbc56ee3b10ca43abcf959fca4c962f/dicompyler/resources/dicompyler_icon11_16.png -------------------------------------------------------------------------------- /dicompyler/resources/dicompyler_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bastula/dicompyler/ffba3a83dbbc56ee3b10ca43abcf959fca4c962f/dicompyler/resources/dicompyler_logo.png -------------------------------------------------------------------------------- /dicompyler/resources/error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bastula/dicompyler/ffba3a83dbbc56ee3b10ca43abcf959fca4c962f/dicompyler/resources/error.png -------------------------------------------------------------------------------- /dicompyler/resources/folder_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bastula/dicompyler/ffba3a83dbbc56ee3b10ca43abcf959fca4c962f/dicompyler/resources/folder_image.png -------------------------------------------------------------------------------- /dicompyler/resources/folder_user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bastula/dicompyler/ffba3a83dbbc56ee3b10ca43abcf959fca4c962f/dicompyler/resources/folder_user.png -------------------------------------------------------------------------------- /dicompyler/resources/group.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bastula/dicompyler/ffba3a83dbbc56ee3b10ca43abcf959fca4c962f/dicompyler/resources/group.png -------------------------------------------------------------------------------- /dicompyler/resources/guiutil.xrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | wxVERTICAL 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | wxALIGN_CENTRE 15 | 16 | 17 | 18 | 19 | 20 | 21 | wxALIGN_CENTRE 22 | 23 | wxHORIZONTAL 24 | 25 | wxALL|wxEXPAND|wxALIGN_CENTRE 26 | 27 | 28 | 5,0 29 | 30 | 31 | 32 | 150,15 33 | 100 34 | 0 35 | 36 | 37 | wxLEFT|wxRIGHT|wxEXPAND|wxALIGN_CENTRE 38 | 39 | 40 | 41 | 42 | 5,0 43 | 44 | 45 | 46 | 47 | 48 | 49 | wxALIGN_CENTRE 50 | 51 | 52 | 53 | 54 | 55 | wxALIGN_CENTRE 56 | 57 | wxHORIZONTAL 58 | 59 | 60 | 2 61 | 2 62 | 10 63 | 6 64 | 1 65 | 66 | 67 | wxALL|wxEXPAND|wxALIGN_CENTRE 68 | 10 69 | 70 | 71 | Loading... 72 | 1 73 | 74 | 75 | -------------------------------------------------------------------------------- /dicompyler/resources/magnifier_zoom_in.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bastula/dicompyler/ffba3a83dbbc56ee3b10ca43abcf959fca4c962f/dicompyler/resources/magnifier_zoom_in.png -------------------------------------------------------------------------------- /dicompyler/resources/magnifier_zoom_out.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bastula/dicompyler/ffba3a83dbbc56ee3b10ca43abcf959fca4c962f/dicompyler/resources/magnifier_zoom_out.png -------------------------------------------------------------------------------- /dicompyler/resources/main.xrc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bastula/dicompyler/ffba3a83dbbc56ee3b10ca43abcf959fca4c962f/dicompyler/resources/main.xrc -------------------------------------------------------------------------------- /dicompyler/resources/pencil.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bastula/dicompyler/ffba3a83dbbc56ee3b10ca43abcf959fca4c962f/dicompyler/resources/pencil.png -------------------------------------------------------------------------------- /dicompyler/resources/pencil_error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bastula/dicompyler/ffba3a83dbbc56ee3b10ca43abcf959fca4c962f/dicompyler/resources/pencil_error.png -------------------------------------------------------------------------------- /dicompyler/resources/plugin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bastula/dicompyler/ffba3a83dbbc56ee3b10ca43abcf959fca4c962f/dicompyler/resources/plugin.png -------------------------------------------------------------------------------- /dicompyler/resources/plugin.xrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 5,5 9 | 10 | 11 | 12 | 13 | 5,5 14 | 15 | 16 | 17 | 18 | #FFFFFF 19 | 20 | 21 | wxVERTICAL 22 | 23 | 24 | 250,250 25 | 26 | 27 | 28 | wxALL|wxEXPAND|wxALIGN_CENTRE 29 | 30 | 31 | 32 | wxALL|wxEXPAND|wxALIGN_CENTRE 33 | 34 | 35 | 10,0 36 | 37 | wxHORIZONTAL 38 | 39 | 40 | 41 | 42 | 0,3 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 4,0 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 2,0 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 5,0 69 | 70 | wxALL|wxEXPAND|wxALIGN_CENTRE 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 2,0 79 | 80 | 81 | 82 | 83 | 84 | 85 | wxHORIZONTAL 86 | 87 | wxALL|wxEXPAND|wxALIGN_CENTRE 88 | 89 | 90 | 0,7 91 | 92 | 93 | 94 | 95 | 10,0 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 3,0 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 5,0 112 | 113 | wxALL|wxEXPAND|wxALIGN_CENTRE 114 | 115 | 116 | 117 | 118 | 119 | 120 | wxHORIZONTAL 121 | 122 | wxALL|wxEXPAND|wxALIGN_CENTRE 123 | 124 | 125 | 0,5 126 | 127 | wxVERTICAL 128 | 129 | 130 | 131 | 132 | 133 | 10,0 134 | 135 | 136 | 137 | 138 | 139 | 140 | wxALL|wxEXPAND|wxALIGN_CENTRE 141 | 142 | wxHORIZONTAL 143 | 144 | 145 | wxALL|wxEXPAND|wxALIGN_CENTRE 146 | 147 | wxVERTICAL 148 | 149 | 150 | wxALL|wxEXPAND|wxALIGN_CENTRE 151 | 152 | 153 | 154 | 155 | wxALL|wxEXPAND|wxALIGN_CENTRE 156 | 157 | 158 | 5,5 159 | 160 | 161 | 162 | wxALL|wxEXPAND|wxALIGN_CENTRE 163 | 164 | 165 | 0,5 166 | 167 | wxVERTICAL 168 | 169 | 170 | wxALL|wxEXPAND|wxALIGN_CENTRE 171 | 3 172 | 173 | 174 | 175 | 176 | 0,5 177 | 178 | wxALL|wxEXPAND|wxALIGN_CENTRE 179 | 180 | 181 | 182 | 183 | 184 | 185 | wxALIGN_CENTRE 186 | 187 | 188 | 0,5 189 | 190 | wxALL|wxEXPAND|wxALIGN_CENTRE 191 | 192 | 193 | 194 | 195 | 196 | wxALL|wxEXPAND|wxALIGN_CENTRE 197 | 3 198 | 199 | 200 | 0,5 201 | wxALL|wxEXPAND|wxALIGN_CENTRE 202 | 3 203 | 204 | wxHORIZONTAL 205 | 206 | wxALL|wxEXPAND|wxALIGN_CENTRE 207 | 208 | wxVERTICAL 209 | 210 | 10,10 211 | wxALL|wxEXPAND|wxALIGN_CENTRE 212 | 213 | 214 | 600,400 215 | Plugin Manager 216 | 1 217 | 218 | 219 | -------------------------------------------------------------------------------- /dicompyler/resources/plugin_disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bastula/dicompyler/ffba3a83dbbc56ee3b10ca43abcf959fca4c962f/dicompyler/resources/plugin_disabled.png -------------------------------------------------------------------------------- /dicompyler/resources/preferences.xrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 550,350 8 | 9 | 10 | wxALL|wxEXPAND|wxALIGN_CENTRE 11 | 7 12 | 13 | wxVERTICAL 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | wxALL|wxEXPAND|wxALIGN_CENTRE 23 | 7 24 | 25 | 26 | Preferences 27 | 1 28 | 29 | 30 | -------------------------------------------------------------------------------- /dicompyler/resources/table_multiple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bastula/dicompyler/ffba3a83dbbc56ee3b10ca43abcf959fca4c962f/dicompyler/resources/table_multiple.png -------------------------------------------------------------------------------- /dicompyler/resources/user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bastula/dicompyler/ffba3a83dbbc56ee3b10ca43abcf959fca4c962f/dicompyler/resources/user.png -------------------------------------------------------------------------------- /dicompyler/util.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # util.py 4 | """Several utility functions that don't really belong anywhere.""" 5 | # Copyright (c) 2009-2017 Aditya Panchal 6 | # This file is part of dicompyler, released under a BSD license. 7 | # See the file license.txt included with this distribution, also 8 | # available at https://github.com/bastula/dicompyler/ 9 | 10 | from __future__ import with_statement 11 | import imp, os, sys 12 | import subprocess 13 | 14 | def platform(): 15 | if sys.platform.startswith('win'): 16 | return 'windows' 17 | elif sys.platform.startswith('darwin'): 18 | return 'mac' 19 | return 'linux' 20 | 21 | def GetResourcePath(resource): 22 | """Return the specified item from the resources folder.""" 23 | 24 | if main_is_frozen(): 25 | if (platform() == 'mac'): 26 | return os.path.join((os.path.join(get_main_dir(), '../Resources')), resource) 27 | return os.path.join((os.path.join(get_main_dir(), 'resources')), resource) 28 | 29 | def GetBasePluginsPath(resource): 30 | """Return the specified item from the base plugins folder.""" 31 | 32 | if main_is_frozen(): 33 | if (platform() == 'mac'): 34 | return os.path.join((os.path.join(get_main_dir(), '../PlugIns')), resource) 35 | return os.path.join((os.path.join(get_main_dir(), 'baseplugins')), resource) 36 | 37 | # from http://www.py2exe.org/index.cgi/HowToDetermineIfRunningFromExe 38 | def main_is_frozen(): 39 | return (hasattr(sys, "frozen") or # new py2exe 40 | hasattr(sys, "importers") # old py2exe 41 | or imp.is_frozen("__main__")) # tools/freeze 42 | 43 | def get_main_dir(): 44 | if main_is_frozen(): 45 | return os.path.dirname(sys.executable) 46 | return os.path.dirname(__file__) 47 | 48 | def get_text_resources(resource): 49 | """Return the resources that are located in the root folder of the 50 | distribution, except for the Mac py2app version, which is located 51 | in the Resources folder in the app bundle.""" 52 | 53 | if (main_is_frozen() and (platform() == 'mac')): 54 | resource = GetResourcePath(resource) 55 | else: 56 | resource = (os.path.join(get_main_dir(), resource)) 57 | 58 | return resource 59 | 60 | def open_path(path): 61 | """Open the specified path in the system default folder viewer.""" 62 | 63 | if sys.platform == 'darwin': 64 | subprocess.check_call(["open", path]) 65 | elif sys.platform == 'linux2': 66 | subprocess.check_call(["gnome-open", path]) 67 | elif sys.platform == 'win32': 68 | subprocess.Popen("explorer " + path) 69 | 70 | def get_credits(): 71 | """Read the credits file and return the data from it.""" 72 | 73 | developers = [] 74 | artists = [] 75 | with open(get_text_resources('credits.txt'), 'rU') as cf: 76 | credits = cf.readlines() 77 | for i, v in enumerate(credits): 78 | if (v == "Lead Developer\n"): 79 | developers.append(credits[i+1].strip()) 80 | if (v == "Developers\n"): 81 | for d in credits[i+1:len(credits)]: 82 | if (d.strip() == ""): 83 | break 84 | developers.append(d.strip()) 85 | if (v == "Artists\n"): 86 | for a in credits[i+1:len(credits)]: 87 | if (a.strip() == ""): 88 | break 89 | artists.append(a.strip()) 90 | return {'developers':developers, 'artists':artists} 91 | -------------------------------------------------------------------------------- /dicompyler_app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # dicompyler_app.py 4 | """Script to start dicompyler without installing from source.""" 5 | # Copyright (c) 2009-2017 Aditya Panchal 6 | # This file is part of dicompyler, released under a BSD license. 7 | # See the file license.txt included with this distribution, also 8 | # available at https://github.com/bastula/dicompyler/ 9 | 10 | import dicompyler.main 11 | 12 | dicompyler.main.start() 13 | -------------------------------------------------------------------------------- /distribute_setup.py: -------------------------------------------------------------------------------- 1 | #!python 2 | """Bootstrap distribute installation 3 | 4 | If you want to use setuptools in your package's setup.py, just include this 5 | file in the same directory with it, and add this to the top of your setup.py:: 6 | 7 | from distribute_setup import use_setuptools 8 | use_setuptools() 9 | 10 | If you want to require a specific version of setuptools, set a download 11 | mirror, or use an alternate download directory, you can do so by supplying 12 | the appropriate options to ``use_setuptools()``. 13 | 14 | This file can also be run as a script to install or upgrade setuptools. 15 | """ 16 | import os 17 | import sys 18 | import time 19 | import fnmatch 20 | import tempfile 21 | import tarfile 22 | from distutils import log 23 | 24 | try: 25 | from site import USER_SITE 26 | except ImportError: 27 | USER_SITE = None 28 | 29 | try: 30 | import subprocess 31 | 32 | def _python_cmd(*args): 33 | args = (sys.executable,) + args 34 | return subprocess.call(args) == 0 35 | 36 | except ImportError: 37 | # will be used for python 2.3 38 | def _python_cmd(*args): 39 | args = (sys.executable,) + args 40 | # quoting arguments if windows 41 | if sys.platform == 'win32': 42 | def quote(arg): 43 | if ' ' in arg: 44 | return '"%s"' % arg 45 | return arg 46 | args = [quote(arg) for arg in args] 47 | return os.spawnl(os.P_WAIT, sys.executable, *args) == 0 48 | 49 | DEFAULT_VERSION = "0.6.26" 50 | DEFAULT_URL = "http://pypi.python.org/packages/source/d/distribute/" 51 | SETUPTOOLS_FAKED_VERSION = "0.6c11" 52 | 53 | SETUPTOOLS_PKG_INFO = """\ 54 | Metadata-Version: 1.0 55 | Name: setuptools 56 | Version: %s 57 | Summary: xxxx 58 | Home-page: xxx 59 | Author: xxx 60 | Author-email: xxx 61 | License: xxx 62 | Description: xxx 63 | """ % SETUPTOOLS_FAKED_VERSION 64 | 65 | 66 | def _install(tarball, install_args=()): 67 | # extracting the tarball 68 | tmpdir = tempfile.mkdtemp() 69 | log.warn('Extracting in %s', tmpdir) 70 | old_wd = os.getcwd() 71 | try: 72 | os.chdir(tmpdir) 73 | tar = tarfile.open(tarball) 74 | _extractall(tar) 75 | tar.close() 76 | 77 | # going in the directory 78 | subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0]) 79 | os.chdir(subdir) 80 | log.warn('Now working in %s', subdir) 81 | 82 | # installing 83 | log.warn('Installing Distribute') 84 | if not _python_cmd('setup.py', 'install', *install_args): 85 | log.warn('Something went wrong during the installation.') 86 | log.warn('See the error message above.') 87 | finally: 88 | os.chdir(old_wd) 89 | 90 | 91 | def _build_egg(egg, tarball, to_dir): 92 | # extracting the tarball 93 | tmpdir = tempfile.mkdtemp() 94 | log.warn('Extracting in %s', tmpdir) 95 | old_wd = os.getcwd() 96 | try: 97 | os.chdir(tmpdir) 98 | tar = tarfile.open(tarball) 99 | _extractall(tar) 100 | tar.close() 101 | 102 | # going in the directory 103 | subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0]) 104 | os.chdir(subdir) 105 | log.warn('Now working in %s', subdir) 106 | 107 | # building an egg 108 | log.warn('Building a Distribute egg in %s', to_dir) 109 | _python_cmd('setup.py', '-q', 'bdist_egg', '--dist-dir', to_dir) 110 | 111 | finally: 112 | os.chdir(old_wd) 113 | # returning the result 114 | log.warn(egg) 115 | if not os.path.exists(egg): 116 | raise IOError('Could not build the egg.') 117 | 118 | 119 | def _do_download(version, download_base, to_dir, download_delay): 120 | egg = os.path.join(to_dir, 'distribute-%s-py%d.%d.egg' 121 | % (version, sys.version_info[0], sys.version_info[1])) 122 | if not os.path.exists(egg): 123 | tarball = download_setuptools(version, download_base, 124 | to_dir, download_delay) 125 | _build_egg(egg, tarball, to_dir) 126 | sys.path.insert(0, egg) 127 | import setuptools 128 | setuptools.bootstrap_install_from = egg 129 | 130 | 131 | def use_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, 132 | to_dir=os.curdir, download_delay=15, no_fake=True): 133 | # making sure we use the absolute path 134 | to_dir = os.path.abspath(to_dir) 135 | was_imported = 'pkg_resources' in sys.modules or \ 136 | 'setuptools' in sys.modules 137 | try: 138 | try: 139 | import pkg_resources 140 | if not hasattr(pkg_resources, '_distribute'): 141 | if not no_fake: 142 | _fake_setuptools() 143 | raise ImportError 144 | except ImportError: 145 | return _do_download(version, download_base, to_dir, download_delay) 146 | try: 147 | pkg_resources.require("distribute>="+version) 148 | return 149 | except pkg_resources.VersionConflict: 150 | e = sys.exc_info()[1] 151 | if was_imported: 152 | sys.stderr.write( 153 | "The required version of distribute (>=%s) is not available,\n" 154 | "and can't be installed while this script is running. Please\n" 155 | "install a more recent version first, using\n" 156 | "'easy_install -U distribute'." 157 | "\n\n(Currently using %r)\n" % (version, e.args[0])) 158 | sys.exit(2) 159 | else: 160 | del pkg_resources, sys.modules['pkg_resources'] # reload ok 161 | return _do_download(version, download_base, to_dir, 162 | download_delay) 163 | except pkg_resources.DistributionNotFound: 164 | return _do_download(version, download_base, to_dir, 165 | download_delay) 166 | finally: 167 | if not no_fake: 168 | _create_fake_setuptools_pkg_info(to_dir) 169 | 170 | def download_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, 171 | to_dir=os.curdir, delay=15): 172 | """Download distribute from a specified location and return its filename 173 | 174 | `version` should be a valid distribute version number that is available 175 | as an egg for download under the `download_base` URL (which should end 176 | with a '/'). `to_dir` is the directory where the egg will be downloaded. 177 | `delay` is the number of seconds to pause before an actual download 178 | attempt. 179 | """ 180 | # making sure we use the absolute path 181 | to_dir = os.path.abspath(to_dir) 182 | try: 183 | from urllib.request import urlopen 184 | except ImportError: 185 | from urllib2 import urlopen 186 | tgz_name = "distribute-%s.tar.gz" % version 187 | url = download_base + tgz_name 188 | saveto = os.path.join(to_dir, tgz_name) 189 | src = dst = None 190 | if not os.path.exists(saveto): # Avoid repeated downloads 191 | try: 192 | log.warn("Downloading %s", url) 193 | src = urlopen(url) 194 | # Read/write all in one block, so we don't create a corrupt file 195 | # if the download is interrupted. 196 | data = src.read() 197 | dst = open(saveto, "wb") 198 | dst.write(data) 199 | finally: 200 | if src: 201 | src.close() 202 | if dst: 203 | dst.close() 204 | return os.path.realpath(saveto) 205 | 206 | def _no_sandbox(function): 207 | def __no_sandbox(*args, **kw): 208 | try: 209 | from setuptools.sandbox import DirectorySandbox 210 | if not hasattr(DirectorySandbox, '_old'): 211 | def violation(*args): 212 | pass 213 | DirectorySandbox._old = DirectorySandbox._violation 214 | DirectorySandbox._violation = violation 215 | patched = True 216 | else: 217 | patched = False 218 | except ImportError: 219 | patched = False 220 | 221 | try: 222 | return function(*args, **kw) 223 | finally: 224 | if patched: 225 | DirectorySandbox._violation = DirectorySandbox._old 226 | del DirectorySandbox._old 227 | 228 | return __no_sandbox 229 | 230 | def _patch_file(path, content): 231 | """Will backup the file then patch it""" 232 | existing_content = open(path).read() 233 | if existing_content == content: 234 | # already patched 235 | log.warn('Already patched.') 236 | return False 237 | log.warn('Patching...') 238 | _rename_path(path) 239 | f = open(path, 'w') 240 | try: 241 | f.write(content) 242 | finally: 243 | f.close() 244 | return True 245 | 246 | _patch_file = _no_sandbox(_patch_file) 247 | 248 | def _same_content(path, content): 249 | return open(path).read() == content 250 | 251 | def _rename_path(path): 252 | new_name = path + '.OLD.%s' % time.time() 253 | log.warn('Renaming %s into %s', path, new_name) 254 | os.rename(path, new_name) 255 | return new_name 256 | 257 | def _remove_flat_installation(placeholder): 258 | if not os.path.isdir(placeholder): 259 | log.warn('Unkown installation at %s', placeholder) 260 | return False 261 | found = False 262 | for file in os.listdir(placeholder): 263 | if fnmatch.fnmatch(file, 'setuptools*.egg-info'): 264 | found = True 265 | break 266 | if not found: 267 | log.warn('Could not locate setuptools*.egg-info') 268 | return 269 | 270 | log.warn('Removing elements out of the way...') 271 | pkg_info = os.path.join(placeholder, file) 272 | if os.path.isdir(pkg_info): 273 | patched = _patch_egg_dir(pkg_info) 274 | else: 275 | patched = _patch_file(pkg_info, SETUPTOOLS_PKG_INFO) 276 | 277 | if not patched: 278 | log.warn('%s already patched.', pkg_info) 279 | return False 280 | # now let's move the files out of the way 281 | for element in ('setuptools', 'pkg_resources.py', 'site.py'): 282 | element = os.path.join(placeholder, element) 283 | if os.path.exists(element): 284 | _rename_path(element) 285 | else: 286 | log.warn('Could not find the %s element of the ' 287 | 'Setuptools distribution', element) 288 | return True 289 | 290 | _remove_flat_installation = _no_sandbox(_remove_flat_installation) 291 | 292 | def _after_install(dist): 293 | log.warn('After install bootstrap.') 294 | placeholder = dist.get_command_obj('install').install_purelib 295 | _create_fake_setuptools_pkg_info(placeholder) 296 | 297 | def _create_fake_setuptools_pkg_info(placeholder): 298 | if not placeholder or not os.path.exists(placeholder): 299 | log.warn('Could not find the install location') 300 | return 301 | pyver = '%s.%s' % (sys.version_info[0], sys.version_info[1]) 302 | setuptools_file = 'setuptools-%s-py%s.egg-info' % \ 303 | (SETUPTOOLS_FAKED_VERSION, pyver) 304 | pkg_info = os.path.join(placeholder, setuptools_file) 305 | if os.path.exists(pkg_info): 306 | log.warn('%s already exists', pkg_info) 307 | return 308 | 309 | log.warn('Creating %s', pkg_info) 310 | f = open(pkg_info, 'w') 311 | try: 312 | f.write(SETUPTOOLS_PKG_INFO) 313 | finally: 314 | f.close() 315 | 316 | pth_file = os.path.join(placeholder, 'setuptools.pth') 317 | log.warn('Creating %s', pth_file) 318 | f = open(pth_file, 'w') 319 | try: 320 | f.write(os.path.join(os.curdir, setuptools_file)) 321 | finally: 322 | f.close() 323 | 324 | _create_fake_setuptools_pkg_info = _no_sandbox(_create_fake_setuptools_pkg_info) 325 | 326 | def _patch_egg_dir(path): 327 | # let's check if it's already patched 328 | pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO') 329 | if os.path.exists(pkg_info): 330 | if _same_content(pkg_info, SETUPTOOLS_PKG_INFO): 331 | log.warn('%s already patched.', pkg_info) 332 | return False 333 | _rename_path(path) 334 | os.mkdir(path) 335 | os.mkdir(os.path.join(path, 'EGG-INFO')) 336 | pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO') 337 | f = open(pkg_info, 'w') 338 | try: 339 | f.write(SETUPTOOLS_PKG_INFO) 340 | finally: 341 | f.close() 342 | return True 343 | 344 | _patch_egg_dir = _no_sandbox(_patch_egg_dir) 345 | 346 | def _before_install(): 347 | log.warn('Before install bootstrap.') 348 | _fake_setuptools() 349 | 350 | 351 | def _under_prefix(location): 352 | if 'install' not in sys.argv: 353 | return True 354 | args = sys.argv[sys.argv.index('install')+1:] 355 | for index, arg in enumerate(args): 356 | for option in ('--root', '--prefix'): 357 | if arg.startswith('%s=' % option): 358 | top_dir = arg.split('root=')[-1] 359 | return location.startswith(top_dir) 360 | elif arg == option: 361 | if len(args) > index: 362 | top_dir = args[index+1] 363 | return location.startswith(top_dir) 364 | if arg == '--user' and USER_SITE is not None: 365 | return location.startswith(USER_SITE) 366 | return True 367 | 368 | 369 | def _fake_setuptools(): 370 | log.warn('Scanning installed packages') 371 | try: 372 | import pkg_resources 373 | except ImportError: 374 | # we're cool 375 | log.warn('Setuptools or Distribute does not seem to be installed.') 376 | return 377 | ws = pkg_resources.working_set 378 | try: 379 | setuptools_dist = ws.find(pkg_resources.Requirement.parse('setuptools', 380 | replacement=False)) 381 | except TypeError: 382 | # old distribute API 383 | setuptools_dist = ws.find(pkg_resources.Requirement.parse('setuptools')) 384 | 385 | if setuptools_dist is None: 386 | log.warn('No setuptools distribution found') 387 | return 388 | # detecting if it was already faked 389 | setuptools_location = setuptools_dist.location 390 | log.warn('Setuptools installation detected at %s', setuptools_location) 391 | 392 | # if --root or --preix was provided, and if 393 | # setuptools is not located in them, we don't patch it 394 | if not _under_prefix(setuptools_location): 395 | log.warn('Not patching, --root or --prefix is installing Distribute' 396 | ' in another location') 397 | return 398 | 399 | # let's see if its an egg 400 | if not setuptools_location.endswith('.egg'): 401 | log.warn('Non-egg installation') 402 | res = _remove_flat_installation(setuptools_location) 403 | if not res: 404 | return 405 | else: 406 | log.warn('Egg installation') 407 | pkg_info = os.path.join(setuptools_location, 'EGG-INFO', 'PKG-INFO') 408 | if (os.path.exists(pkg_info) and 409 | _same_content(pkg_info, SETUPTOOLS_PKG_INFO)): 410 | log.warn('Already patched.') 411 | return 412 | log.warn('Patching...') 413 | # let's create a fake egg replacing setuptools one 414 | res = _patch_egg_dir(setuptools_location) 415 | if not res: 416 | return 417 | log.warn('Patched done.') 418 | _relaunch() 419 | 420 | 421 | def _relaunch(): 422 | log.warn('Relaunching...') 423 | # we have to relaunch the process 424 | # pip marker to avoid a relaunch bug 425 | if sys.argv[:3] == ['-c', 'install', '--single-version-externally-managed']: 426 | sys.argv[0] = 'setup.py' 427 | args = [sys.executable] + sys.argv 428 | sys.exit(subprocess.call(args)) 429 | 430 | 431 | def _extractall(self, path=".", members=None): 432 | """Extract all members from the archive to the current working 433 | directory and set owner, modification time and permissions on 434 | directories afterwards. `path' specifies a different directory 435 | to extract to. `members' is optional and must be a subset of the 436 | list returned by getmembers(). 437 | """ 438 | import copy 439 | import operator 440 | from tarfile import ExtractError 441 | directories = [] 442 | 443 | if members is None: 444 | members = self 445 | 446 | for tarinfo in members: 447 | if tarinfo.isdir(): 448 | # Extract directories with a safe mode. 449 | directories.append(tarinfo) 450 | tarinfo = copy.copy(tarinfo) 451 | tarinfo.mode = 448 # decimal for oct 0700 452 | self.extract(tarinfo, path) 453 | 454 | # Reverse sort directories. 455 | if sys.version_info < (2, 4): 456 | def sorter(dir1, dir2): 457 | return cmp(dir1.name, dir2.name) 458 | directories.sort(sorter) 459 | directories.reverse() 460 | else: 461 | directories.sort(key=operator.attrgetter('name'), reverse=True) 462 | 463 | # Set correct owner, mtime and filemode on directories. 464 | for tarinfo in directories: 465 | dirpath = os.path.join(path, tarinfo.name) 466 | try: 467 | self.chown(tarinfo, dirpath) 468 | self.utime(tarinfo, dirpath) 469 | self.chmod(tarinfo, dirpath) 470 | except ExtractError: 471 | e = sys.exc_info()[1] 472 | if self.errorlevel > 1: 473 | raise 474 | else: 475 | self._dbg(1, "tarfile: %s" % e) 476 | 477 | def _build_install_args(argv): 478 | install_args = [] 479 | user_install = '--user' in argv 480 | if user_install and sys.version_info < (2,6): 481 | log.warn("--user requires Python 2.6 or later") 482 | raise SystemExit(1) 483 | if user_install: 484 | install_args.append('--user') 485 | return install_args 486 | 487 | def main(argv, version=DEFAULT_VERSION): 488 | """Install or upgrade setuptools and EasyInstall""" 489 | tarball = download_setuptools() 490 | _install(tarball, _build_install_args(argv)) 491 | 492 | 493 | if __name__ == '__main__': 494 | main(sys.argv[1:]) 495 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | dicompyler-core[image]>=0.5.2 2 | wxPython>=4.0.0b2 3 | matplotlib>=1.3,<2.2 4 | numpy>=1.13.1 5 | https://github.com/darcymason/pydicom/archive/master.zip 6 | -------------------------------------------------------------------------------- /scripts/dicompyler: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # dicompyler 4 | """Script to start dicompyler from source.""" 5 | # Copyright (c) 2009-2017 Aditya Panchal 6 | # This file is part of dicompyler, relased under a BSD license. 7 | # See the file license.txt included with this distribution, also 8 | # available at https://github.com/bastula/dicompyler/ 9 | 10 | import dicompyler.main 11 | 12 | dicompyler.main.start() 13 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # setup.py 4 | """Setup script for dicompyler.""" 5 | # Copyright (c) 2012-2017 Aditya Panchal 6 | # This file is part of dicompyler, relased under a BSD license. 7 | # See the file license.txt included with this distribution, also 8 | # available at https://github.com/bastula/dicompyler/ 9 | 10 | from setuptools import setup, find_packages 11 | 12 | requires = [ 13 | 'matplotlib>=1.3.0,<2.2', 14 | 'numpy>=1.2.1', 15 | 'pillow>=1.0', 16 | 'dicompyler-core>=0.5.2', 17 | 'pydicom>=0.9.9', 18 | 'wxPython>=4.0.0b2'] 19 | 20 | setup( 21 | name="dicompyler", 22 | version = "0.5.0", 23 | include_package_data = True, 24 | packages = find_packages(), 25 | package_data = {'dicompyler': 26 | ['*.txt', 'resources/*.png', 'resources/*.xrc', 'resources/*.ico', 27 | 'baseplugins/*.py', 'baseplugins/*.xrc']}, 28 | zip_safe = False, 29 | install_requires = requires, 30 | dependency_links = [ 31 | 'git+https://github.com/darcymason/pydicom.git#egg=pydicom-1.0.0'], 32 | entry_points={'console_scripts':['dicompyler = dicompyler.main:start']}, 33 | 34 | # metadata for upload to PyPI 35 | author = "Aditya Panchal", 36 | author_email = "apanchal@bastula.org", 37 | description = "Extensible radiation therapy research platform and " + \ 38 | "viewer for DICOM and DICOM RT.", 39 | license = "BSD License", 40 | keywords = "radiation therapy research python dicom dicom-rt", 41 | url = "https://github.com/bastula/dicompyler/", 42 | classifiers = [ 43 | "License :: OSI Approved :: BSD License", 44 | "Intended Audience :: Developers", 45 | "Intended Audience :: Healthcare Industry", 46 | "Intended Audience :: Science/Research", 47 | "Development Status :: 4 - Beta", 48 | "Programming Language :: Python :: 2", 49 | 'Programming Language :: Python :: 2.7', 50 | 'Programming Language :: Python :: 3', 51 | 'Programming Language :: Python :: 3.5', 52 | 'Programming Language :: Python :: 3.6' 53 | "Operating System :: OS Independent", 54 | "Topic :: Scientific/Engineering :: Medical Science Apps.", 55 | "Topic :: Scientific/Engineering :: Physics", 56 | "Topic :: Scientific/Engineering :: Visualization"], 57 | long_description = """ 58 | dicompyler 59 | ========== 60 | 61 | dicompyler is an extensible open source radiation therapy research 62 | platform based on the DICOM standard. It also functions as a 63 | cross-platform DICOM RT viewer. 64 | 65 | dicompyler runs on Windows, Mac and Linux systems and is available in 66 | source and binary versions. Since dicompyler is based on modular 67 | architecture, it is easy to extend it with 3rd party plugins. 68 | 69 | Visit the dicompyler _`home page`: 70 | https://github.com/bastula/dicompyler/ for how-to information and guides. 71 | 72 | Getting Help 73 | ============ 74 | 75 | To get help with dicompyler, visit the _`mailing list`: 76 | http://groups.google.com/group/dicompyler/ or follow us on _`twitter`: 77 | http://twitter.com/dicompyler 78 | 79 | Requirements 80 | ============ 81 | 82 | dicompyler requires the following packages to run from source: 83 | 84 | - Python 2.7 or 3.5 or higher 85 | - wxPython (Phoenix) 4.0.0b2 or higher 86 | - matplotlib 1.3.0 or higher 87 | - numpy 1.3.1 or higher 88 | - Pillow 1.0 or higher 89 | - dicompyler-core 0.5.2 or higher 90 | - pydicom 0.9.9 or higher""", 91 | ) --------------------------------------------------------------------------------