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 + + +