diff --git a/Q3DC/CMakeLists.txt b/Q3DC/CMakeLists.txt
index 0106e4e..c30ff32 100644
--- a/Q3DC/CMakeLists.txt
+++ b/Q3DC/CMakeLists.txt
@@ -9,6 +9,7 @@ set(MODULE_PYTHON_SCRIPTS
set(MODULE_PYTHON_RESOURCES
Resources/Icons/${MODULE_NAME}.png
Resources/UI/${MODULE_NAME}.ui
+ Resources/Data/base_fiducial_legend.csv
)
#-----------------------------------------------------------------------------
diff --git a/Q3DC/Q3DC.py b/Q3DC/Q3DC.py
index 7397166..4ce4675 100644
--- a/Q3DC/Q3DC.py
+++ b/Q3DC/Q3DC.py
@@ -1,6 +1,9 @@
import vtk, qt, ctk, slicer
from slicer.ScriptedLoadableModule import *
+from slicer.util import NodeModify
import csv, os
+from collections import defaultdict
+from pathlib import Path
import json
import time
import math
@@ -90,6 +93,27 @@ def setup(self):
self.ui.inputLandmarksSelector.connect('currentNodeChanged(vtkMRMLNode*)', self.onLandmarksChanged)
self.ui.landmarkComboBox.connect('currentIndexChanged(QString)', self.UpdateInterface)
self.ui.surfaceDeplacementCheckBox.connect('stateChanged(int)', self.onSurfaceDeplacementStateChanged)
+
+ # --------------- anatomical legend --------------
+ self.suggested_landmarks = self.logic.load_suggested_landmarks()
+ self.anatomical_legend_space = self.ui.landmarkModifLayout
+ self.anatomical_radio_buttons_layout = qt.QHBoxLayout()
+ self.anatomical_legend_space.addLayout(self.anatomical_radio_buttons_layout)
+ self.init_anatomical_radio_buttons()
+
+ self.anatomical_legend = None
+ self.init_anatomical_legend()
+ self.anatomical_legend_view = slicer.qMRMLTableView()
+ self.anatomical_legend_view.setMRMLTableNode(self.anatomical_legend)
+ self.anatomical_legend_space.addWidget(self.anatomical_legend_view)
+ self.anatomical_legend_view.show()
+ self.anatomical_legend_view.setSelectionBehavior(
+ qt.QAbstractItemView.SelectRows
+ )
+ self.anatomical_legend_view.connect('selectionChanged()', self.on_legend_row_selected)
+
+ self.anatomical_radio_buttons[0].toggle()
+
# ----------------- Compute Mid Point -------------
self.ui.landmarkComboBox1.connect('currentIndexChanged(int)', self.UpdateInterface)
self.ui.landmarkComboBox2.connect('currentIndexChanged(int)', self.UpdateInterface)
@@ -313,6 +337,94 @@ def UpdateInterface(self):
self.ui.fidListComboBoxlineLB.currentNode())
self.logic.UpdateThreeDView(self.ui.landmarkComboBox.currentText)
+ def init_anatomical_legend(self):
+ if self.anatomical_legend is None:
+ for table_node in slicer.mrmlScene.GetNodesByClass('vtkMRMLTableNode'):
+ if table_node.GetAttribute('Q3DC.is_anatomical_legend') == 'True':
+ self.anatomical_legend = table_node
+ if self.anatomical_legend is None:
+ self.anatomical_legend = slicer.vtkMRMLTableNode()
+ self.anatomical_legend.SetSaveWithScene(False)
+ self.anatomical_legend.SetLocked(True)
+ slicer.mrmlScene.AddNode(self.anatomical_legend)
+ self.anatomical_legend.SetAttribute('Q3DC.is_anatomical_legend', 'True')
+
+ al = self.anatomical_legend
+ with NodeModify(al):
+ al.RemoveAllColumns()
+ al.AddColumn().SetName('Landmark')
+ al.AddColumn().SetName('Description')
+ al.SetUseColumnNameAsColumnHeader(True)
+
+ def init_anatomical_radio_buttons(self):
+ self.anatomical_radio_buttons = \
+ [qt.QRadioButton(region) for region in self.suggested_landmarks.keys()]
+ for i in range(self.anatomical_radio_buttons_layout.count()-1, -1, -1):
+ self.anatomical_radio_buttons_layout.itemAt[i].widget().setParent(None)
+ for radio_button in self.anatomical_radio_buttons:
+ self.anatomical_radio_buttons_layout.addWidget(radio_button)
+ radio_button.toggled.connect(
+ lambda state, _radio_button=radio_button:
+ self.on_anatomical_radio_button_toggled(state, _radio_button)
+ )
+
+ def on_anatomical_radio_button_toggled(self, state, radio_button):
+ if state:
+ self.init_anatomical_legend()
+ region = radio_button.text
+
+ al = self.anatomical_legend
+ with NodeModify(al):
+ for landmark, description in self.suggested_landmarks[region]:
+ new_row_index = al.AddEmptyRow()
+ al.SetCellText(new_row_index, 0, landmark)
+ al.SetCellText(new_row_index, 1, description)
+ self.anatomical_legend_view.resizeColumnsToContents()
+
+ def on_legend_row_selected(self):
+ # Calculate the index of the selected point.
+ fidList = self.logic.selectedFidList
+ if not fidList:
+ return
+ selectedFidReflID = self.logic.findIDFromLabel(
+ fidList,
+ self.ui.landmarkComboBox.currentText
+ )
+ if selectedFidReflID is None:
+ # code would run correctly if we continued but wouldn't do anything
+ return
+ fid_index = fidList.GetNthControlPointIndexByID(selectedFidReflID)
+ old_name = fidList.GetNthControlPointLabel(fid_index)
+
+ # Look in the legend for the info from the selected row.
+ selected_indices = self.anatomical_legend_view.selectedIndexes()
+ if len(selected_indices) != 2:
+ return
+ name_index, description_index = selected_indices
+ row_index = name_index.row()
+ name = self.anatomical_legend.GetCellText(row_index, 0)
+ description = self.anatomical_legend.GetCellText(row_index, 1)
+
+ # Refuse to create multiple fiducials with the same name.
+ for i in range(fidList.GetNumberOfControlPoints()):
+ if name == fidList.GetNthControlPointLabel(i):
+ return
+
+ # Set the name and description of the selected point.
+ fidList.SetNthControlPointLabel(fid_index, name)
+ fidList.SetNthControlPointDescription(fid_index, description)
+
+ # Update the landmark combo boxes to reflect the name change.
+ self.logic.updateLandmarkComboBox(fidList, self.ui.landmarkComboBox, False)
+ self.ui.landmarkComboBox.setCurrentText(name)
+ for box in (self.ui.landmarkComboBox1, self.ui.landmarkComboBox2):
+ new_selection = box.currentText
+ if new_selection == old_name:
+ new_selection = name
+ self.logic.updateLandmarkComboBox(fidList, box)
+ box.setCurrentText(new_selection)
+ self.UpdateInterface()
+
def onModelChanged(self):
print("-------Model Changed--------")
if self.logic.selectedModel:
@@ -541,6 +653,20 @@ def __init__(self, interface):
self.decimalPoint = chr(system.decimalPoint())
self.comboboxdict = dict()
+ @staticmethod
+ def load_suggested_landmarks():
+ suggested_landmarks = defaultdict(list)
+ suggestions_path = \
+ Path(__file__).parent / 'Resources' / 'Data' / 'base_fiducial_legend.csv'
+ with suggestions_path.open(newline='') as suggestions_file:
+ reader = csv.DictReader(suggestions_file)
+ for row in reader:
+ region = row['Region'].title()
+ landmark = row['Landmark']
+ name = row['Name']
+ suggested_landmarks[region].append((landmark, name))
+ return suggested_landmarks
+
def initComboboxdict(self):
self.comboboxdict[self.interface.landmarkComboBoxA] = None
self.comboboxdict[self.interface.landmarkComboBoxB] = None
@@ -1100,14 +1226,12 @@ def deleteLandmark(self, fidList, label):
if value is fidList:
key.removeItem(key.findText(label))
- def findIDFromLabel(self, fidList, landmarkLabel):
+ @staticmethod
+ def findIDFromLabel(fidList, landmarkLabel):
# find the ID of the markupsNode from the label of a landmark!
- landmarkDescription = self.decodeJSON(fidList.GetAttribute("landmarkDescription"))
- if not landmarkDescription:
- return None
- for ID, value in landmarkDescription.items():
- if value["landmarkLabel"] == landmarkLabel:
- return ID
+ for i in range(fidList.GetNumberOfFiducials()):
+ if landmarkLabel == fidList.GetNthFiducialLabel(i):
+ return fidList.GetNthMarkupID(i)
return None
def getClosestPointIndex(self, fidNode, inputPolyData, landmarkID):
@@ -1768,10 +1892,9 @@ def displayROI(self, inputModelNode, scalarName):
PolyData.Modified()
displayNode = inputModelNode.GetModelDisplayNode()
displayNode.SetScalarVisibility(False)
- disabledModify = displayNode.StartModify()
- displayNode.SetActiveScalarName(scalarName)
- displayNode.SetScalarVisibility(True)
- displayNode.EndModify(disabledModify)
+ with NodeModify(displayNode):
+ displayNode.SetActiveScalarName(scalarName)
+ displayNode.SetScalarVisibility(True)
def findROI(self, fidList):
hardenModel = slicer.app.mrmlScene().GetNodeByID(fidList.GetAttribute("hardenModelID"))
diff --git a/Q3DC/Resources/Data/base_fiducial_legend.csv b/Q3DC/Resources/Data/base_fiducial_legend.csv
new file mode 100644
index 0000000..0a21368
--- /dev/null
+++ b/Q3DC/Resources/Data/base_fiducial_legend.csv
@@ -0,0 +1,72 @@
+Region,Landmark,Name
+CRANIAL BASE,Ba,Basion
+CRANIAL BASE,S,Sella
+CRANIAL BASE,N,Nasion
+NASOMAXILLARY COMPLEX,A,A-point
+NASOMAXILLARY COMPLEX,ANS,Anterior Nasal Spine
+NASOMAXILLARY COMPLEX,PNS,Posterior Nasal Spine
+NASOMAXILLARY COMPLEX,OrR,Right Orbitale
+NASOMAXILLARY COMPLEX,OrL,Left Orbitale
+NASOMAXILLARY COMPLEX,ZygR,Right Zygomatic
+NASOMAXILLARY COMPLEX,ZygL,Left Zygomatic
+NASOMAXILLARY COMPLEX,NCR,Right Nasal Cavity
+NASOMAXILLARY COMPLEX,NCL,Left Nasal Cavity
+NASOMAXILLARY COMPLEX,PFR,Right Palatine Foramen
+NASOMAXILLARY COMPLEX,PFL,Left Palatine Foramen
+NASOMAXILLARY COMPLEX,IFr,Incisal Foramen
+NASOMAXILLARY COMPLEX,UR6,Right Maxillary molar cusp tip
+NASOMAXILLARY COMPLEX,UL6,Left Maxillary molar cusp tip
+NASOMAXILLARY COMPLEX,UR’6,Right Maxillary molar root apex
+NASOMAXILLARY COMPLEX,UL’6,Left Maxillary molar root apex
+NASOMAXILLARY COMPLEX,UR6AB,Buccal Alveolar Bone Level at first molar
+NASOMAXILLARY COMPLEX,UL6AB,Buccal Alveolar Bone Level at first molar
+NASOMAXILLARY COMPLEX,UR5,Right Maxillary 2nd premolar cusp tip
+NASOMAXILLARY COMPLEX,UL5,Left Maxillary 2nd premolar cusp tip
+NASOMAXILLARY COMPLEX,UR’5,Right Maxillary 2nd premolar root apex
+NASOMAXILLARY COMPLEX,UL’5,Left Maxillary 2nd premolar root apex
+NASOMAXILLARY COMPLEX,UR4,Right Maxillary 1st premolar cusp tip
+NASOMAXILLARY COMPLEX,UL4,Left Maxillary 1st premolar cusp tip
+NASOMAXILLARY COMPLEX,UR’4,Right Maxillary 1st premolar root apex
+NASOMAXILLARY COMPLEX,UL’4,Left Maxillary 1st premolar root apex
+NASOMAXILLARY COMPLEX,UR3,Right Maxillary 1st premolar cusp tip
+NASOMAXILLARY COMPLEX,UL3,Left Maxillary 1st premolar cusp tip
+NASOMAXILLARY COMPLEX,UR’3,Right Maxillary 1st premolar root apex
+NASOMAXILLARY COMPLEX,UL’3,Left Maxillary 1st premolar root apex
+NASOMAXILLARY COMPLEX,U1,Maxillary incisor incisal edge
+NASOMAXILLARY COMPLEX,U’1,Maxillary incisor root apex
+MANDIBLE,B,B-point
+MANDIBLE,Pog,Pogonion
+MANDIBLE,Me,Menton
+MANDIBLE,Gn,Gnathion
+MANDIBLE,RGo,Right Gonion
+MANDIBLE,LGo,Left Gonion
+MANDIBLE,RSGo,Right Superior Gonion
+MANDIBLE,RIGo,Right Inferior Gonion
+MANDIBLE,LSGo,Left Superior Gonion
+MANDIBLE,LIGo,Left Inferior Gonion
+MANDIBLE,RCo,Right Condylion
+MANDIBLE,LCo,Left Condylion
+MANDIBLE,RLCP,Right Lateral Condylar pole
+MANDIBLE,RMCP,Right Medial Condylar pole
+MANDIBLE,LLCP,Left Lateral Condylar pole
+MANDIBLE,LMCP,Left Medial Condylar pole
+MANDIBLE,LR6,Right Mandibular molar cusp tip
+MANDIBLE,LL6,Left Maxillary molar cusp tip
+MANDIBLE,LR’6,Right Maxillary molar root apex
+MANDIBLE,LL’6,Left Maxillary molar root apex
+MANDIBLE,LR6AB,Buccal Alveolar Bone Level at first molar
+MANDIBLE,LL6AB,Buccal Alveolar Bone Level at first molar
+MANDIBLE,LR5,Right Maxillary 2nd premolar cusp tip
+MANDIBLE,LL5,Left Maxillary 2nd premolar cusp tip
+MANDIBLE,LR’5,Right Maxillary 2nd premolar root apex
+MANDIBLE,LL’5,Left Maxillary 2nd premolar root apex
+MANDIBLE,LR4,Right Maxillary 1st premolar cusp tip
+MANDIBLE,LL4,Left Maxillary 1st premolar cusp tip
+MANDIBLE,LR’4,Right Maxillary 1st premolar root apex
+MANDIBLE,LL’4,Left Maxillary 1st premolar root apex
+MANDIBLE,LR3,Right Maxillary 1st premolar cusp tip
+MANDIBLE,LL3,Left Maxillary 1st premolar cusp tip
+MANDIBLE,LR’3,Right Maxillary 1st premolar root apex
+MANDIBLE,LL’3,Left Maxillary 1st premolar root apex
+MANDIBLE,L1,Maxillary incisor incisal edge
+MANDIBLE,L’1,Maxillary incisor root apex
diff --git a/Q3DC/Resources/UI/Q3DC.ui b/Q3DC/Resources/UI/Q3DC.ui
index cf71914..ac7d9bc 100644
--- a/Q3DC/Resources/UI/Q3DC.ui
+++ b/Q3DC/Resources/UI/Q3DC.ui
@@ -56,7 +56,7 @@
QFrame::StyledPanel
-
+
-
-
@@ -189,6 +189,13 @@
+ -
+
+
+ Landmark Legend
+
+
+