Slicer 5.13
Slicer is a multi-platform, free and open source software package for visualization and medical image computing
Loading...
Searching...
No Matches
qSlicerMultiVolumeExplorerCharts.py
Go to the documentation of this file.
1import math, logging
2from __main__ import vtk, slicer
3from qt import QSize
4from qSlicerMultiVolumeExplorerModuleHelper import qSlicerMultiVolumeExplorerModuleHelper as Helper
5
6
8
9 SIGNAL_INTENSITY_MODE = 0
10 FIXED_RANGE_INTENSITY_MODE = 1
11 PERCENTAGE_CHANGE_MODE = 2
12 MODES = [SIGNAL_INTENSITY_MODE, FIXED_RANGE_INTENSITY_MODE, PERCENTAGE_CHANGE_MODE]
13
14 @staticmethod
15 def getMultiVolumeLabels(volumeNode):
16 frameLabels = volumeNode.GetAttribute("MultiVolume.FrameLabels")
17 nFrames = volumeNode.GetNumberOfFrames()
18 mvLabels = [0]*nFrames
19 if frameLabels:
20 mvLabels = frameLabels.split(",")
21 if len(mvLabels) == nFrames:
22 for frameIndex in range(nFrames):
23 mvLabels[frameIndex] = float(mvLabels[frameIndex])
24 else:
25 for frameIndex in range(nFrames):
26 mvLabels[frameIndex] = float(frameIndex)
27 return mvLabels
28
29 @staticmethod
30 def getIJKIntFromIJKFloat(ijkFloat):
31 ijk = []
32 for element in ijkFloat:
33 try:
34 index = int(round(element)) # noqa: RUF046 (element may be a numpy float)
35 except ValueError:
36 index = 0
37 ijk.append(index)
38 return ijk
39
40 @staticmethod
41 def setExtractInput(extract, mvImage):
42 extract.SetInputData(mvImage)
43
44 @property
45 def chartView(self):
46 return self.__chartView
47
48 @property
49 def chartTable(self):
50 return self.__chartTable
51
52 @property
53 def chart(self):
54 return self.__chart
55
56 @property
57 def showXLogScale(self):
58 return self.__xLogScaleEnabled
59
60 @showXLogScale.setter
61 def showXLogScale(self, value):
62 assert type(value) is bool, "Only boolean values are allowed for this class member"
63 self.__xLogScaleEnabled = value
64 self.__chart.SetXAxisLogScale(value)
65
66 @property
67 def showYLogScale(self):
68 return self.__yLogScaleEnabled
69
70 @showYLogScale.setter
71 def showYLogScale(self, value):
72 assert type(value) is bool, "Only boolean values are allowed for this class member"
73 self.__yLogScaleEnabled = value
74 self.__chart.SetYAxisLogScale(value)
75
76 @property
77 def showLegend(self):
78 return self.__showLegend
79
80 @showLegend.setter
81 def showLegend(self, value):
82 assert type(value) is bool, "Only boolean values are allowed for this class member"
83 self.__showLegend = value
84 self.chart.SetLegendVisibility(value)
85
86 @property
88 return self.__fgMultiVolumeNode
89
90 @fgMultiVolumeNode.setter
91 def fgMultiVolumeNode(self, fgMultiVolumeNode):
92 self.__fgMultiVolumeNode = fgMultiVolumeNode
93
94 @property
96 return self.__bgMultiVolumeNode
97
98 @bgMultiVolumeNode.setter
99 def bgMultiVolumeNode(self, bgMultiVolumeNode):
100 logging.debug("MultiVolumeIntensityChartView: bgMultiVolumeNode changed")
101 self.__bgMultiVolumeNode = bgMultiVolumeNode
102
103 if not self.__bgMultiVolumeNode:
104 self.chartView.minimumSize = QSize(0,0)
105 return
106 else:
107 self.__chartView.minimumSize = QSize(200,240)
108 nFrames = self.__bgMultiVolumeNode.GetNumberOfFrames()
109
110 self.refreshArray(self.__bgxArray, nFrames, "x")
111 self.refreshArray(self.__bgyArray, nFrames, "1st multivolume")
112
113 if self.__chartTableNode:
114 slicer.mrmlScene.RemoveNode(self.__chartTableNode)
115 self.__chartTableNode = None
117 self.__chartTable = self.__chartTableNode.GetTable()
118 self.__chartTable.SetNumberOfRows(nFrames)
119
120 # get the range of intensities for the
121 mvi = self.__bgMultiVolumeNode.GetImageData()
122 self.__mvRange = [0,0]
123 for f in range(nFrames):
124 extract = vtk.vtkImageExtractComponents()
125 self.setExtractInput(extract, mvi)
126 extract.SetComponents(f)
127 extract.Update()
128
129 frame = extract.GetOutput()
130 frameRange = frame.GetScalarRange()
131 self.__mvRange[0] = min(self.__mvRange[0], frameRange[0])
132 self.__mvRange[1] = max(self.__mvRange[1], frameRange[1])
133
135
136 @property
140 @nFramesForBaselineCalculation.setter
144 @staticmethod
145 def refreshArray(array, nFrames, name):
146 array.SetNumberOfTuples(nFrames)
147 array.SetNumberOfComponents(1)
148 array.Allocate(nFrames)
149 array.SetName(name)
150
151 def __init__(self):
152 self.__chartView = slicer.qMRMLPlotView()
153 self.__chartView.setMRMLScene(slicer.mrmlScene)
154 self.__chartViewNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLPlotViewNode")
155 self.__chartView.setMRMLPlotViewNode(self.__chartViewNode)
156 self.__chart = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLPlotChartNode")
157 self.__chart.LegendVisibilityOff()
158 self.__chart.SetAxisTitleFontSize(15)
159 self.__chartViewNode.SetPlotChartNodeID(self.__chart.GetID())
160
161 self.__bgxArray = vtk.vtkFloatArray()
162 self.__bgyArray = vtk.vtkFloatArray()
163 self.__bgxArray.SetName("x")
164 self.__bgyArray.SetName("1st multivolume")
166 self.__chartTable = self.__chartTableNode.GetTable()
167 self.__fgChartTableNode = None
168
169 self.__bgPlot = None
170 self.__fgPlot = None
171
172 self.__bgMultiVolumeNode = None
173 self.__fgMultiVolumeNode = None
174
175 self.__mvLabels = []
176
177 self.__xLogScaleEnabled = False
178 self.__yLogScaleEnabled = False
179
180 self.__mvRange = [0,0]
182
183 self.__chartMode = None
185
187
188 self.__showLegend = False
189
190 def reset(self):
191 self.__mvLabels = []
192 self.clearPlots()
193
194 def clearPlots(self):
195 self.__chart.RemoveAllPlotSeriesNodeIDs()
196 if self.__bgPlot:
197 slicer.mrmlScene.RemoveNode(self.__bgPlot)
198 if self.__fgPlot:
199 slicer.mrmlScene.RemoveNode(self.__fgPlot)
200
201 def createNewVTKTableNode(self, xArray, yArray):
202 chartTableNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLTableNode", "bgChartTable")
203 chartTableNode.GetTable().AddColumn(xArray)
204 chartTableNode.GetTable().AddColumn(yArray)
205 return chartTableNode
206
207 def setMultiVolumeRange(self, minVal, maxVal):
208 assert maxVal > minVal and type(minVal) is int and type(maxVal) is int
209 self.__mvRange = [minVal, maxVal]
210
212 if self.__chartMode != self.SIGNAL_INTENSITY_MODE:
213 self.__chart.SetYAxisRangeAuto(True)
215
217 logging.debug("Fixed range for yAxis: min(%d), max(%d)" % (self.__mvRange[0], self.__mvRange[1]))
220 self.__chart.SetYAxisRangeAuto(False)
221 self.__chart.SetYAxisRange(self.__mvRange[0],self.__mvRange[1])
222
224 if self.__chartMode != self.PERCENTAGE_CHANGE_MODE:
225 self.__chart.SetYAxisRangeAuto(True)
227
228 def createChart(self, sliceWidget, xy, ignoreCurrentBackground=False):
229 if not sliceWidget and not xy:
230 return
231
232 sliceLogic = sliceWidget.sliceLogic()
233
234 bgLayer = sliceLogic.GetBackgroundLayer()
235
236 doIJKToRASTransformation = False
237 bgVolumeNode = bgLayer.GetVolumeNode()
238 if not bgVolumeNode or bgVolumeNode.GetID() != self.__bgMultiVolumeNode.GetID() or \
239 bgVolumeNode != self.__bgMultiVolumeNode:
240 if not ignoreCurrentBackground:
241 return
242 else:
243 doIJKToRASTransformation = True
244
245 xyz = sliceWidget.sliceView().convertDeviceToXYZ(xy)
246 xyToIJK = bgLayer.GetXYToIJKTransform()
247 ijkFloat = xyToIJK.TransformDoublePoint(xyz)
248 if doIJKToRASTransformation:
249 RAStoIJK = vtk.vtkMatrix4x4()
250 self.__bgMultiVolumeNode.GetRASToIJKMatrix(RAStoIJK)
251 ras = self.xyToRAS(sliceLogic, xy)
252 ijkFloat = RAStoIJK.MultiplyPoint([ras[0], ras[1], ras[2], 1])[:3]
253
254 bgijk = self.getIJKIntFromIJKFloat(ijkFloat)
255 bgImage = self.__bgMultiVolumeNode.GetImageData()
256
257 if not self.arePixelsWithinImageExtent(bgImage, bgijk):
258 self.clearPlots()
259 return
260
261 nComponents = self.__bgMultiVolumeNode.GetNumberOfFrames()
262
263 useFg = False
264 fgImage = None
265 fgijk = None
266 if self.__fgMultiVolumeNode:
267 fgijkFloat = xyToIJK.TransformDoublePoint(xyz)
268 fgijk = self.getIJKIntFromIJKFloat(fgijkFloat)
269
270 fgImage = self.__fgMultiVolumeNode.GetImageData()
271 if self.__fgChartTableNode:
272 slicer.mrmlScene.RemoveNode(self.__fgChartTableNode)
273 self.__fgChartTableNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLTableNode", "fgChartTable")
274 fgChartTable = self.__fgChartTableNode.GetTable()
275 if fgijk[0] == bgijk[0] and fgijk[1] == bgijk[1] and fgijk[2] == bgijk[2] and \
276 fgImage.GetNumberOfScalarComponents() == bgImage.GetNumberOfScalarComponents():
277 useFg = True
278
279 fgxArray = vtk.vtkFloatArray()
280 self.refreshArray(fgxArray, nComponents, "fg")
281
282 fgyArray = vtk.vtkFloatArray()
283 self.refreshArray(fgyArray, nComponents, "2nd multivolume")
284
285 # will crash if there is no name
286 fgChartTable.AddColumn(fgxArray)
287 fgChartTable.AddColumn(fgyArray)
288 fgChartTable.SetNumberOfRows(nComponents)
289
290 # get the vector of values at IJK
291 for c in range(nComponents):
292 val = bgImage.GetScalarComponentAsDouble(bgijk[0],bgijk[1],bgijk[2],c)
293 if math.isnan(val):
294 val = 0
295 self.__chartTable.SetValue(c, 0, self.__mvLabels[c])
296 self.__chartTable.SetValue(c, 1, val)
297 if useFg:
298 fgValue = fgImage.GetScalarComponentAsDouble(bgijk[0],bgijk[1],bgijk[2],c)
299 if math.isnan(fgValue):
300 fgValue = 0
301 fgChartTable.SetValue(c,0,self.__mvLabels[c])
302 fgChartTable.SetValue(c,1,fgValue)
303
304 self.baselineAverageSignal = 0
305 if self.__chartMode == self.PERCENTAGE_CHANGE_MODE:
307 if useFg:
309
310 self.clearPlots()
311 self.setAxesTitle()
312
313 self.__bgPlot = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLPlotSeriesNode", "1st multivolume")
314 self.__bgPlot.SetAndObserveTableNodeID(self.__chartTableNode.GetID())
315 self.__bgPlot.SetXColumnName("x")
316 self.__bgPlot.SetYColumnName("1st multivolume")
317 self.__bgPlot.SetPlotType(slicer.vtkMRMLPlotSeriesNode.PlotTypeScatter)
318 if useFg:
319 self.__bgPlot.SetLineStyle(slicer.vtkMRMLPlotSeriesNode.LineStyleNone)
320 else:
321 self.__bgPlot.SetMarkerStyle(slicer.vtkMRMLPlotSeriesNode.MarkerStyleNone)
322 self.__chart.AddAndObservePlotSeriesNodeID(self.__bgPlot.GetID())
323 # bgPlot.SetLabel("Primary multivolume ")
324
325 if useFg:
326 self.__fgPlot = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLPlotSeriesNode", "2nd multivolume")
327 self.__fgPlot.SetAndObserveTableNodeID(self.__fgChartTableNode.GetID())
328 self.__fgPlot.SetXColumnName("fg")
329 self.__fgPlot.SetYColumnName("2nd multivolume")
330 self.__fgPlot.SetPlotType(slicer.vtkMRMLPlotSeriesNode.PlotTypeScatter)
331 self.__fgPlot.SetColor(1,0,0)
332 self.__fgPlot.SetMarkerStyle(slicer.vtkMRMLPlotSeriesNode.MarkerStyleNone)
333 self.__chart.AddAndObservePlotSeriesNodeID(self.__fgPlot.GetID())
334 # bgPlot.SetLabel("Primary multivolume ")
335
336 def xyToRAS(self, sliceLogic, xyPoint):
337 sliceNode = sliceLogic.GetSliceNode()
338 rast = sliceNode.GetXYToRAS().MultiplyPoint(xyPoint + (0,1))
339 return rast[:3]
340
341 def computePercentageChangeWithRespectToBaseline(self, multiVolumeNode, chartTable, ijk):
342 self.baselineAverageSignal = 0
343 image = multiVolumeNode.GetImageData()
344 nComponents = multiVolumeNode.GetNumberOfFrames()
345 nBaselines = min(self.__nFramesForBaselineCalculation, nComponents)
346 for c in range(nBaselines):
347 val = image.GetScalarComponentAsDouble(ijk[0], ijk[1], ijk[2], c)
348 self.baselineAverageSignal += 0 if math.isnan(val) else val
349 self.baselineAverageSignal /= nBaselines
350 if self.baselineAverageSignal != 0:
351 for c in range(nComponents):
352 val = image.GetScalarComponentAsDouble(ijk[0], ijk[1], ijk[2], c)
353 if math.isnan(val):
354 val = 0
355 chartTable.SetValue(c, 1, int(val / self.baselineAverageSignal - 1) * 100.)
356
357 def arePixelsWithinImageExtent(self, image, ijk):
358 extent = image.GetExtent()
359 if not (extent[0] <= ijk[0] <= extent[1] and
360 extent[2] <= ijk[1] <= extent[3] and
361 extent[4] <= ijk[2] <= extent[5]):
362 # pixel outside the valid extent
363 logging.debug("Point %d,%d,%d is not in valid Extent P1(%d,%d,%d) and P2(%d,%d,%d) "
364 % (ijk[0], ijk[1], ijk[2], extent[0], extent[2], extent[4], extent[1], extent[3], extent[5]))
365 return False
366 return True
367
368 def setAxesTitle(self):
369 if self.__chartMode == self.PERCENTAGE_CHANGE_MODE and self.baselineAverageSignal != 0:
370 yTitle = "change relative to baseline, %"
371 else:
372 yTitle = "signal intensity"
373
374 tag = str(self.__bgMultiVolumeNode.GetAttribute("MultiVolume.FrameIdentifyingDICOMTagName"))
375 units = str(self.__bgMultiVolumeNode.GetAttribute("MultiVolume.FrameIdentifyingDICOMTagUnits"))
376 xTitle = tag + ", " + units
377
379 xTitle = "log of " + xTitle
381 yTitle = "log of " + yTitle
382
383 self.setYAxisTitle(yTitle)
384 self.setXAxisTitle(xTitle)
385
386 def setYAxisTitle(self, title):
387 self.__chart.SetYAxisTitle(title)
388
389 def setXAxisTitle(self, title):
390 self.__chart.SetXAxisTitle(title)
391
392
394
395 def __init__(self, labelNode, multiVolumeNode, multiVolumeLabels, baselineFrames, displayPercentageChange=False):
396 self.labelNode = labelNode
397 self.multiVolumeNode = multiVolumeNode
398 self.multiVolumeLabels = multiVolumeLabels
399 self.baselineFrames = baselineFrames
400 self.displayPercentageChange = displayPercentageChange
401
403 self.dataNodes = {}
404
406 # iterate over the label image and collect the IJK for each label element
407
408 if self.labelNode is None or self.multiVolumeNode is None:
409 return
410
413
416
417 Helper.setupChartNodeViewLayout()
418
419 chartNode = self.createChartNodeAndInsertData()
420
421 self.setAxesLabels(chartNode)
422 self.initiateChartViewNode(chartNode)
423
425 img = self.labelNode.GetImageData()
426 extent = img.GetExtent()
427 self.labeledVoxels = {}
428 for i in range(extent[1]+1):
429 for j in range(extent[3]+1):
430 for k in range(extent[5]+1):
431 labelValue = img.GetScalarComponentAsFloat(i, j, k, 0)
432 if labelValue:
433 if labelValue in self.labeledVoxels.keys():
434 self.labeledVoxels[labelValue].append([i, j, k])
435 else:
436 self.labeledVoxels[labelValue] = []
437 self.labeledVoxels[labelValue].append([i, j, k])
438
440 # calculate the mean in each frame for each label and add to the chart array
441 nComponents = self.multiVolumeNode.GetNumberOfFrames()
442 self.dataNodes = {}
443 for k in self.labeledVoxels.keys():
444 self.dataNodes[k] = slicer.mrmlScene.AddNode(slicer.vtkMRMLDoubleArrayNode())
445 self.dataNodes[k].GetArray().SetNumberOfTuples(nComponents)
446 mvImage = self.multiVolumeNode.GetImageData()
447 for c in range(nComponents):
448 for k in self.labeledVoxels.keys():
449 arr = self.dataNodes[k].GetArray()
450 mean = 0.
451 cnt = 0.
452 for v in self.labeledVoxels[k]:
453 val = mvImage.GetScalarComponentAsFloat(v[0], v[1], v[2], c)
454 if math.isnan(val):
455 val = 0
456 mean = mean + val
457 cnt += 1
458 arr.SetComponent(c, 0, self.multiVolumeLabels[c])
459 arr.SetComponent(c, 1, mean / cnt)
460 arr.SetComponent(c, 2, 0)
461
463 nComponents = self.multiVolumeNode.GetNumberOfFrames()
464 nBaselines = min(self.baselineFrames.value, nComponents)
465 for k in self.labeledVoxels.keys():
466 arr = self.dataNodes[k].GetArray()
467 baseline = 0
468 for bc in range(nBaselines):
469 baseline += arr.GetComponent(bc, 1)
470 baseline /= nBaselines
471 if baseline != 0:
472 for ic in range(nComponents):
473 intensity = arr.GetComponent(ic, 1)
474 percentChange = int(intensity / baseline - 1) * 100.
475 arr.SetComponent(ic, 1, percentChange)
476
478 # setup color node
479 colorNode = self.labelNode.GetDisplayNode().GetColorNode()
480 lut = colorNode.GetLookupTable()
481 # add initialized data nodes to the chart
482 chartNode = slicer.mrmlScene.AddNode(slicer.vtkMRMLChartNode())
483 chartNode.ClearArrays()
484 for k in self.labeledVoxels.keys():
485 k = int(k)
486 name = colorNode.GetColorName(k)
487 chartNode.AddArray(name, self.dataNodes[k].GetID())
488 rgb = lut.GetTableValue(int(k))
489
490 colorStr = Helper.RGBtoHex(rgb[0] * 255, rgb[1] * 255, rgb[2] * 255)
491 chartNode.SetProperty(name, "color", colorStr)
492 return chartNode
493
494 def setAxesLabels(self, chartNode):
495 tag = str(self.multiVolumeNode.GetAttribute("MultiVolume.FrameIdentifyingDICOMTagName"))
496 units = str(self.multiVolumeNode.GetAttribute("MultiVolume.FrameIdentifyingDICOMTagUnits"))
497 xTitle = tag + ", " + units
498 chartNode.SetProperty("default", "xAxisLabel", xTitle)
500 chartNode.SetProperty("default", "yAxisLabel", "change relative to baseline, %")
501 else:
502 chartNode.SetProperty("default", "yAxisLabel", "mean signal intensity")
503
504 def initiateChartViewNode(self, chartNode):
505 chartViewNodes = slicer.mrmlScene.GetNodesByClass("vtkMRMLChartViewNode")
506 chartViewNodes.SetReferenceCount(chartViewNodes.GetReferenceCount() - 1)
507 chartViewNodes.InitTraversal()
508 chartViewNode = chartViewNodes.GetNextItemAsObject()
509 chartViewNode.SetChartNodeID(chartNode.GetID())
__init__(self, labelNode, multiVolumeNode, multiVolumeLabels, baselineFrames, displayPercentageChange=False)
createChart(self, sliceWidget, xy, ignoreCurrentBackground=False)