├── .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 |
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 |  
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 |
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 |
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 |
8 | 3
9 | wxVERTICAL
10 |
11 |
12 |
13 | wxALL|wxEXPAND|wxALIGN_CENTRE|wxADJUST_MINSIZE
14 | 4
15 |
16 |
17 |
18 | wxALL|wxEXPAND|wxALIGN_CENTRE
19 |
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 |
221 |
222 |
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 | )
--------------------------------------------------------------------------------