File: dvh.py

package info (click to toggle)
dicompyler 0.4.2.0-2
  • links: PTS, VCS
  • area: main
  • in suites: buster
  • size: 1,896 kB
  • sloc: python: 5,332; makefile: 9
file content (312 lines) | stat: -rwxr-xr-x 13,805 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
#!/usr/bin/env python
# -*- coding: ISO-8859-1 -*-
# dvh.py
"""dicompyler plugin that displays a dose volume histogram (DVH)
    with adjustable constraints via wxPython and matplotlib."""
# Copyright (c) 2009-2012 Aditya Panchal
# This file is part of dicompyler, released under a BSD license.
#    See the file license.txt included with this distribution, also
#    available at http://code.google.com/p/dicompyler/
#
# It is assumed that the reference (prescription) dose is in cGy.

import wx
from wx.xrc import XmlResource, XRCCTRL, XRCID
from wx.lib.pubsub import setuparg1 #see https://wxpython.org/Phoenix/docs/html/wx.lib.pubsub.setuparg1.html
from wx.lib.pubsub import pub
from dicompyler import guiutil, util
from dicompyler import dvhdata, guidvh
from dicompyler import wxmpl
import numpy as np

def pluginProperties():
    """Properties of the plugin."""

    props = {}
    props['name'] = 'DVH'
    props['description'] = "Display and evaluate dose volume histogram (DVH) data"
    props['author'] = 'Aditya Panchal'
    props['version'] = "0.4.2"
    props['plugin_type'] = 'main'
    props['plugin_version'] = 1
    props['min_dicom'] = ['rtss', 'rtdose']
    props['recommended_dicom'] = ['rtss', 'rtdose', 'rtplan']

    return props

def pluginLoader(parent):
    """Function to load the plugin."""

    # Load the XRC file for our gui resources
    res = XmlResource(util.GetBasePluginsPath('dvh.xrc'))

    panelDVH = res.LoadPanel(parent, 'pluginDVH')
    panelDVH.Init(res)

    return panelDVH

class pluginDVH(wx.Panel):
    """Plugin to display DVH data with adjustable constraints."""

    def __init__(self):
        pre = wx.PrePanel()
        # the Create step is done by XRC.
        self.PostCreate(pre)

    def Init(self, res):
        """Method called after the panel has been initialized."""

        self.guiDVH = guidvh.guiDVH(self)
        res.AttachUnknownControl('panelDVH', self.guiDVH.panelDVH, self)

        # Initialize the Constraint selector controls
        self.lblType = XRCCTRL(self, 'lblType')
        self.choiceConstraint = XRCCTRL(self, 'choiceConstraint')
        self.txtConstraint = XRCCTRL(self, 'txtConstraint')
        self.sliderConstraint = XRCCTRL(self, 'sliderConstraint')
        self.lblResultType = XRCCTRL(self, 'lblResultType')
        self.lblConstraintUnits = XRCCTRL(self, 'lblConstraintUnits')
        self.lblConstraintTypeUnits = XRCCTRL(self, 'lblConstraintTypeUnits')

        # Initialize the result labels
        self.lblConstraintType = XRCCTRL(self, 'lblConstraintType')
        self.lblResultDivider = XRCCTRL(self, 'lblResultDivider')
        self.lblConstraintPercent = XRCCTRL(self, 'lblConstraintPercent')

        # Modify the control and font size on Mac
        controls = [self.lblType, self.choiceConstraint, self.sliderConstraint,
            self.lblResultType, self.lblConstraintUnits, self.lblConstraintPercent,
            self.lblConstraintType, self.lblConstraintTypeUnits, self.lblResultDivider]
        # Add children of composite controls to modification list
        compositecontrols = [self.txtConstraint]
        for control in compositecontrols:
            for child in control.GetChildren():
                controls.append(child)
        # Add the constraint static box to the modification list
        controls.append(self.lblType.GetContainingSizer().GetStaticBox())

        if guiutil.IsMac():
            font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT)
            font.SetPointSize(10)
            for control in controls:
                control.SetWindowVariant(wx.WINDOW_VARIANT_SMALL)
                control.SetFont(font)

        # Adjust the control size for the result value labels
        te = self.lblType.GetTextExtent('0')
        self.lblConstraintUnits.SetMinSize((te[0]*10, te[1]))
        self.lblConstraintPercent.SetMinSize((te[0]*6, te[1]))
        self.Layout()

        # Bind ui events to the proper methods
        wx.EVT_CHOICE(self, XRCID('choiceConstraint'), self.OnToggleConstraints)
        wx.EVT_SPINCTRL(self, XRCID('txtConstraint'), self.OnChangeConstraint)
        wx.EVT_COMMAND_SCROLL_THUMBTRACK(self, XRCID('sliderConstraint'), self.OnChangeConstraint)
        wx.EVT_COMMAND_SCROLL_CHANGED(self, XRCID('sliderConstraint'), self.OnChangeConstraint)
        self.Bind(wx.EVT_WINDOW_DESTROY, self.OnDestroy)

        # Initialize variables
        self.structures = {} # structures from initial DICOM data
        self.checkedstructures = {} # structures that need to be shown
        self.dvhs = {} # raw dvhs from initial DICOM data
        self.dvhdata = {} # dict of dvh constraint functions
        self.dvharray = {} # dict of dvh data processed from dvhdata
        self.dvhscaling = {} # dict of dvh scaling data
        self.plan = {} # used for rx dose
        self.structureid = 1 # used to indicate current constraint structure

        # Set up pubsub
        pub.subscribe(self.OnUpdatePatient, 'patient.updated.parsed_data')
        pub.subscribe(self.OnStructureCheck, 'structures.checked')
        pub.subscribe(self.OnStructureSelect, 'structure.selected')

    def OnUpdatePatient(self, msg):
        """Update and load the patient data."""

        self.structures = msg.data['structures']
        self.dvhs = msg.data['dvhs']
        self.plan = msg.data['plan']
        # show an empty plot when (re)loading a patient
        self.guiDVH.Replot()
        self.EnableConstraints(False)

    def OnDestroy(self, evt):
        """Unbind to all events before the plugin is destroyed."""

        pub.unsubscribe(self.OnUpdatePatient, 'patient.updated.parsed_data')
        pub.unsubscribe(self.OnStructureCheck, 'structures.checked')
        pub.unsubscribe(self.OnStructureSelect, 'structure.selected')

    def OnStructureCheck(self, msg):
        """When a structure changes, update the interface and plot."""

        # Make sure that the volume has been calculated for each structure
        # before setting it
        self.checkedstructures = msg.data
        for id, structure in self.checkedstructures.iteritems():
            if not self.structures[id].has_key('volume'):
                self.structures[id]['volume'] = structure['volume']

            # make sure that the dvh has been calculated for each structure
            # before setting it
            if self.dvhs.has_key(id):
                self.EnableConstraints(True)
                # Create an instance of the dvhdata class to can access its functions
                self.dvhdata[id] = dvhdata.DVH(self.dvhs[id])
                # Create an instance of the dvh arrays so that guidvh can plot it
                self.dvharray[id] = dvhdata.DVH(self.dvhs[id]).dvh
                # Create an instance of the dvh scaling data for guidvh
                self.dvhscaling[id] = self.dvhs[id]['scaling']
                # 'Toggle' the choice box to refresh the dose data
                self.OnToggleConstraints(None)
        if not len(self.checkedstructures):
            self.EnableConstraints(False)
            # Make an empty plot on the DVH
            self.guiDVH.Replot()

    def OnStructureSelect(self, msg):
        """Load the constraints for the currently selected structure."""

        if (msg.data['id'] == None):
            self.EnableConstraints(False)
        else:
            self.structureid = msg.data['id']
            if self.dvhs.has_key(self.structureid):
                # Create an instance of the dvhdata class to can access its functions
                self.dvhdata[self.structureid] = dvhdata.DVH(self.dvhs[self.structureid])
                # Create an instance of the dvh scaling data for guidvh
                self.dvhscaling[self.structureid] = self.dvhs[self.structureid]['scaling']
                # 'Toggle' the choice box to refresh the dose data
                self.OnToggleConstraints(None)
            else:
                self.EnableConstraints(False)
                self.guiDVH.Replot([self.dvharray], [self.dvhscaling], self.checkedstructures)

    def EnableConstraints(self, value):
        """Enable or disable the constraint selector."""

        self.choiceConstraint.Enable(value)
        self.txtConstraint.Enable(value)
        self.sliderConstraint.Enable(value)
        if not value:
            self.lblConstraintUnits.SetLabel('- ')
            self.lblConstraintPercent.SetLabel('- ')
            self.txtConstraint.SetValue(0)
            self.sliderConstraint.SetValue(0)

    def OnToggleConstraints(self, evt):
        """Switch between different constraint modes."""

        # Replot the remaining structures and disable the constraints
        # if a structure that has no DVH calculated is selected
        if not self.dvhs.has_key(self.structureid):
            self.guiDVH.Replot([self.dvharray], [self.dvhscaling], self.checkedstructures)
            self.EnableConstraints(False)
            return
        else:
            self.EnableConstraints(True)
            dvh = self.dvhs[self.structureid]

        # Check if the function was called via an event or not
        if not (evt == None):
            constrainttype = evt.GetInt()
        else:
            constrainttype = self.choiceConstraint.GetSelection()

        constraintrange = 0
        # Volume constraint
        if (constrainttype == 0):
            self.lblConstraintType.SetLabel('   Dose:')
            self.lblConstraintTypeUnits.SetLabel('%  ')
            self.lblResultType.SetLabel('Volume:')
            rxDose = float(self.plan['rxdose'])
            dvhdata = (len(dvh['data'])-1)*dvh['scaling']
            constraintrange = int(dvhdata*100/rxDose)
            # never go over the max dose as data does not exist
            if (constraintrange > int(dvh['max'])):
                constraintrange = int(dvh['max'])
        # Volume constraint in Gy
        elif (constrainttype == 1):
            self.lblConstraintType.SetLabel('   Dose:')
            self.lblConstraintTypeUnits.SetLabel('Gy ')
            self.lblResultType.SetLabel('Volume:')
            constraintrange = self.plan['rxdose']/100
            maxdose = int(dvh['max']*self.plan['rxdose']/10000)
            # never go over the max dose as data does not exist
            if (constraintrange*100 > maxdose):
                constraintrange = maxdose
        # Dose constraint
        elif (constrainttype == 2):
            self.lblConstraintType.SetLabel('Volume:')
            self.lblConstraintTypeUnits.SetLabel(u'%  ')
            self.lblResultType.SetLabel('   Dose:')
            constraintrange = 100
        # Dose constraint in cc
        elif (constrainttype == 3):
            self.lblConstraintType.SetLabel('Volume:')
            self.lblConstraintTypeUnits.SetLabel(u'cm')
            self.lblResultType.SetLabel('   Dose:')
            constraintrange = int(self.structures[self.structureid]['volume'])

        self.sliderConstraint.SetRange(0, constraintrange)
        self.sliderConstraint.SetValue(constraintrange)
        self.txtConstraint.SetRange(0, constraintrange)
        self.txtConstraint.SetValue(constraintrange)

        self.OnChangeConstraint(None)

    def OnChangeConstraint(self, evt):
        """Update the results when the constraint value changes."""

        # Check if the function was called via an event or not
        if not (evt == None):
            slidervalue = evt.GetInt()
        else:
            slidervalue = self.sliderConstraint.GetValue()

        self.txtConstraint.SetValue(slidervalue)
        self.sliderConstraint.SetValue(slidervalue)
        rxDose = self.plan['rxdose']
        id = self.structureid

        constrainttype = self.choiceConstraint.GetSelection()
        # Volume constraint
        if (constrainttype == 0):
            absDose = rxDose * slidervalue / 100
            volume = self.structures[id]['volume']
            cc = self.dvhdata[id].GetVolumeConstraintCC(absDose, volume)
            constraint = self.dvhdata[id].GetVolumeConstraint(absDose)

            self.lblConstraintUnits.SetLabel("%.1f" % cc + u' cm')
            self.lblConstraintPercent.SetLabel("%.1f" % constraint + " %")
            self.guiDVH.Replot([self.dvharray], [self.dvhscaling],
                self.checkedstructures, ([absDose], [constraint]), id)
        # Volume constraint in Gy
        elif (constrainttype == 1):
            absDose = slidervalue*100
            volume = self.structures[id]['volume']
            cc = self.dvhdata[id].GetVolumeConstraintCC(absDose, volume)
            constraint = self.dvhdata[id].GetVolumeConstraint(absDose)

            self.lblConstraintUnits.SetLabel("%.1f" % cc + u' cm')
            self.lblConstraintPercent.SetLabel("%.1f" % constraint + " %")
            self.guiDVH.Replot([self.dvharray], [self.dvhscaling],
                self.checkedstructures, ([absDose], [constraint]), id)
        # Dose constraint
        elif (constrainttype == 2):
            dose = self.dvhdata[id].GetDoseConstraint(slidervalue)

            self.lblConstraintUnits.SetLabel("%.1f" % dose + u' cGy')
            self.lblConstraintPercent.SetLabel("%.1f" % (dose*100/rxDose) + " %")
            self.guiDVH.Replot([self.dvharray], [self.dvhscaling],
                self.checkedstructures, ([dose], [slidervalue]), id)
        # Dose constraint in cc
        elif (constrainttype == 3):
            volumepercent = slidervalue*100/self.structures[id]['volume']

            dose = self.dvhdata[id].GetDoseConstraint(volumepercent)

            self.lblConstraintUnits.SetLabel("%.1f" % dose + u' cGy')
            self.lblConstraintPercent.SetLabel("%.1f" % (dose*100/rxDose) + " %")
            self.guiDVH.Replot([self.dvharray], [self.dvhscaling],
                self.checkedstructures, ([dose], [volumepercent]), id)