Procházet zdrojové kódy

Integration DICOMtools to core SlicerLabkey extension

Andrej Studen před 5 roky
rodič
revize
9ba121f857

+ 1 - 0
CMakeLists.txt

@@ -20,6 +20,7 @@ include(${Slicer_USE_FILE})
 # Extension modules
 add_subdirectory(labkeySlicerPythonExtension)
 add_subdirectory(utils)
+add_subdirectory(DICOMtools)
 ## NEXT_MODULE
 
 #-----------------------------------------------------------------------------

+ 33 - 0
DICOMtools/CMakeLists.txt

@@ -0,0 +1,33 @@
+#-----------------------------------------------------------------------------
+set(MODULE_NAME DICOMtools)
+
+#-----------------------------------------------------------------------------
+set(MODULE_PYTHON_SCRIPTS
+	loadDicom.py
+	importDicom.py
+	exportDicom.py
+	vtkInterface.py
+  )
+
+set(MODULE_PYTHON_RESOURCES
+  Resources/Icons/${MODULE_NAME}.png
+  )
+
+#-----------------------------------------------------------------------------
+slicerMacroBuildScriptedModule(
+  NAME ${MODULE_NAME}
+  SCRIPTS ${MODULE_PYTHON_SCRIPTS}
+  RESOURCES ${MODULE_PYTHON_RESOURCES}
+  WITH_GENERIC_TESTS
+  )
+
+#-----------------------------------------------------------------------------
+if(BUILD_TESTING)
+
+  # Register the unittest subclass in the main script as a ctest.
+  # Note that the test will also be available at runtime.
+  #slicer_add_python_unittest(SCRIPT ${MODULE_NAME}.py)
+
+  # Additional build-time testing
+  #add_subdirectory(Testing)
+endif()

+ 270 - 0
DICOMtools/Resources/Icons/DICOMtools.fig

@@ -0,0 +1,270 @@
+#FIG 3.2  Produced by xfig version 3.2.6a
+Landscape
+Center
+Inches
+Letter
+500.00
+Single
+-2
+1200 2
+0 32 #c8c8c8
+0 33 #ded7de
+0 34 #c1c1c1
+0 35 #6e6e6e
+0 36 #c1c1c1
+0 37 #444444
+0 38 #8d8e8d
+0 39 #8d8e8d
+0 40 #717171
+0 41 #adadad
+0 42 #333333
+0 43 #939295
+0 44 #747075
+0 45 #555555
+0 46 #b2b2b2
+0 47 #c1c1c1
+0 48 #c2c2c2
+0 49 #6d6d6d
+0 50 #454545
+0 51 #8d8d8d
+0 52 #8d8e8d
+0 53 #dc9d92
+0 54 #f0ebdf
+0 55 #c2c2c2
+0 56 #e1c7a7
+0 57 #e0e0e0
+0 58 #d1d1d1
+0 59 #ececec
+0 60 #d97a1a
+0 61 #f0e31a
+0 62 #877dc1
+0 63 #afa092
+0 64 #827cdc
+0 65 #d5d5d5
+0 66 #8b8ba4
+0 67 #4a4a4a
+0 68 #8b6b6b
+0 69 #5a5a5a
+0 70 #636363
+0 71 #8d8d8d
+0 72 #b69a73
+0 73 #4192fe
+0 74 #be703b
+0 75 #da7700
+0 76 #d9b700
+0 77 #006400
+0 78 #5a6b3b
+0 79 #d2d2d2
+0 80 #a9a9a9
+0 81 #8d8da3
+0 82 #f2b85d
+0 83 #88986b
+0 84 #646464
+0 85 #d5d5d5
+0 86 #8b8ba4
+0 87 #b6e5fe
+0 88 #85bfeb
+0 89 #bcbcbc
+0 90 #d29552
+0 91 #8d8d8d
+0 92 #97d1fd
+0 93 #8d8d8d
+0 94 #616161
+0 95 #adb1ad
+0 96 #fe9900
+0 97 #d5d5d5
+0 98 #8b8ba4
+0 99 #8b6b6b
+0 100 #8b9b6b
+0 101 #f66b00
+0 102 #5a6b39
+0 103 #8b9b6b
+0 104 #d5d5d5
+0 105 #8b8ba4
+0 106 #8b6b6b
+0 107 #8b9b6b
+0 108 #f66b00
+0 109 #8b9b7b
+0 110 #184a18
+0 111 #d5d5d5
+0 112 #8b8ba4
+0 113 #f6bc5a
+0 114 #8b9b6b
+0 115 #636b9b
+0 116 #8b6b6b
+0 117 #f6f6f6
+0 118 #dd0000
+0 119 #8b9b6b
+0 120 #d5d5d5
+0 121 #8b8ba4
+0 122 #f6bc5a
+0 123 #8b9b6b
+0 124 #d5d5d5
+0 125 #8b8ba4
+0 126 #f6bc5a
+0 127 #8b9b6b
+0 128 #636b9b
+0 129 #526b29
+0 130 #939393
+0 131 #006300
+0 132 #8b8ba4
+0 133 #8b8ba4
+0 134 #8b8ba4
+0 135 #00634a
+0 136 #7b834a
+0 137 #e6bc7b
+0 138 #8b9b7b
+0 139 #a4b4c5
+0 140 #6b6b93
+0 141 #836b6b
+0 142 #529b4a
+0 143 #d5e6e6
+0 144 #526363
+0 145 #186b4a
+0 146 #9ba4b4
+0 147 #fe9300
+0 148 #fe9300
+0 149 #8b6b6b
+0 150 #00634a
+0 151 #7b834a
+0 152 #63737b
+0 153 #e6bc7b
+0 154 #184a18
+0 155 #8b8ba4
+0 156 #f6bc5a
+0 157 #8b9b6b
+0 158 #d5d5d5
+0 159 #8b8ba4
+0 160 #8b6b6b
+0 161 #8b9b6b
+0 162 #d2d2d2
+0 163 #a9a9a9
+0 164 #8d8da3
+0 165 #f2b85d
+0 166 #88986b
+0 167 #d5d5d5
+0 168 #8b8ba4
+0 169 #d5d5d5
+0 170 #8b8ba4
+0 171 #8b6b6b
+0 172 #8b9b6b
+0 173 #d5d5d5
+0 174 #8b8ba4
+0 175 #8b6b6b
+0 176 #8b9b7b
+0 177 #000000
+0 178 #f63829
+0 179 #000000
+0 180 #fefe52
+0 181 #52794a
+0 182 #63995a
+0 183 #c56142
+0 184 #e66942
+0 185 #fe7952
+0 186 #dddddd
+0 187 #8b8ba4
+0 188 #f6bc5a
+0 189 #8b9b6b
+0 190 #636b9b
+0 191 #f6f6f6
+0 192 #d5d5d5
+0 193 #8b6b6b
+0 194 #d2d2d2
+0 195 #a9a9a9
+0 196 #8d8da3
+0 197 #f2b85d
+0 198 #88986b
+0 199 #d2d2d2
+0 200 #a9a9a9
+0 201 #8d8da3
+0 202 #f2b85d
+0 203 #88986b
+0 204 #d2d2d2
+0 205 #a9a9a9
+0 206 #8d8da3
+0 207 #f2b85d
+0 208 #88986b
+0 209 #d2d2d2
+0 210 #a9a9a9
+0 211 #8d8da3
+0 212 #f2b85d
+0 213 #88986b
+0 214 #d2d2d2
+0 215 #a9a9a9
+0 216 #8d8da3
+0 217 #f2b85d
+0 218 #88986b
+0 219 #d5d5d5
+0 220 #8b8ba4
+0 221 #d2d2d2
+0 222 #a9a9a9
+0 223 #8d8da3
+0 224 #f2b85d
+0 225 #88986b
+0 226 #d2d2d2
+0 227 #a9a9a9
+0 228 #8d8da3
+0 229 #f2b85d
+0 230 #88986b
+0 231 #d2d2d2
+0 232 #a9a9a9
+0 233 #8d8da3
+0 234 #f2b85d
+0 235 #88986b
+0 236 #d2d2d2
+0 237 #a9a9a9
+0 238 #8d8da3
+0 239 #f2b85d
+0 240 #88986b
+0 241 #d5d5d5
+0 242 #8b8ba4
+0 243 #f2edd2
+0 244 #f4ad5d
+0 245 #95cd98
+0 246 #a9a9a9
+0 247 #b4157d
+0 248 #ededed
+0 249 #838383
+0 250 #d5d5d5
+0 251 #8b8ba4
+0 252 #f6bc5a
+0 253 #8b9b6b
+0 254 #636b9b
+0 255 #7b7b7b
+0 256 #005a00
+0 257 #e67373
+0 258 #f6f6f6
+0 259 #dd0000
+0 260 #feca31
+0 261 #29794a
+0 262 #dd2821
+0 263 #2159c5
+0 264 #f7f7f7
+0 265 #ededed
+0 266 #e5e5e5
+0 267 #7b834a
+0 268 #d5d5d5
+0 269 #e6bc7b
+0 270 #8b9b7b
+0 271 #a4b4c5
+0 272 #6b6b93
+0 273 #836b6b
+0 274 #529b4a
+0 275 #d5e6e6
+0 276 #9ba4b4
+0 277 #21835a
+0 278 #8b8ba4
+0 279 #f6bc5a
+0 280 #8b9b6b
+0 281 #636b9b
+0 282 #d5d5d5
+0 283 #8b8ba4
+0 284 #f6bc5a
+0 285 #8b9b6b
+0 286 #8b6b6b
+2 2 0 0 0 7 50 -1 -1 0.000 0 0 -1 0 0 5
+	 4500 3000 7125 3000 7125 5625 4500 5625 4500 3000
+2 4 0 1 0 7 50 -1 -1 0.000 0 0 7 0 0 5
+	 6600 4800 6600 3600 5100 3600 5100 4800 6600 4800
+4 0 0 50 -1 4 16 0.0000 4 135 450 5400 4125 DICOM\001
+4 0 0 50 -1 4 14 0.0000 4 135 450 5550 4425 tools\001

binární
DICOMtools/Resources/Icons/DICOMtools.png


binární
DICOMtools/Resources/Screenshots/exportDICOM.png


+ 20 - 0
DICOMtools/Resources/Screenshots/importDICOM.fig

@@ -0,0 +1,20 @@
+#FIG 3.2  Produced by xfig version 3.2.6a
+Landscape
+Center
+Inches
+Letter
+100.00
+Single
+-2
+1200 2
+2 4 0 4 8 0 50 -1 -1 0.000 0 0 7 0 0 5
+	 13200 11250 6150 11250 6150 10950 13200 10950 13200 11250
+2 4 0 4 15 0 50 -1 -1 0.000 0 0 7 0 0 5
+	 13200 11625 6150 11625 6150 11325 13200 11325 13200 11625
+2 4 0 4 31 0 50 -1 -1 0.000 0 0 7 0 0 5
+	 13200 12000 6150 12000 6150 11700 13200 11700 13200 12000
+2 4 0 4 18 0 50 -1 -1 0.000 0 0 7 0 0 5
+	 13200 12375 6150 12375 6150 12075 13200 12075 13200 12375
+2 5 0 1 0 -1 55 -1 -1 0.000 0 0 -1 0 0 5
+	0 /data0/studen/data/blog/pelican/content/images/importDICOM.png
+	 4650 3825 17970 3825 17970 18870 4650 18870 4650 3825

binární
DICOMtools/Resources/Screenshots/importDICOM.png


+ 24 - 0
DICOMtools/Resources/Screenshots/loadDICOM.fig

@@ -0,0 +1,24 @@
+#FIG 3.2  Produced by xfig version 3.2.6a
+Landscape
+Center
+Inches
+Letter
+100.00
+Single
+-2
+1200 2
+2 4 0 4 8 0 50 -1 -1 0.000 0 0 7 0 0 5
+	 13200 11250 6150 11250 6150 10950 13200 10950 13200 11250
+2 4 0 4 15 0 50 -1 -1 0.000 0 0 7 0 0 5
+	 13200 11625 6150 11625 6150 11325 13200 11325 13200 11625
+2 5 0 1 0 -1 55 -1 -1 0.000 0 0 -1 0 0 5
+	0 /data0/studen/data/blog/pelican/content/images/loadDICOM.png
+	 4650 3825 17970 3825 17970 18870 4650 18870 4650 3825
+2 4 0 4 18 0 50 -1 -1 0.000 0 0 7 0 0 5
+	 13200 12675 6150 12675 6150 12375 13200 12375 13200 12675
+2 4 0 4 31 0 50 -1 -1 0.000 0 0 7 0 0 5
+	 13200 12300 6150 12300 6150 12000 13200 12000 13200 12300
+2 4 0 4 12 0 50 -1 -1 0.000 0 0 7 0 0 5
+	 13200 12000 6150 12000 6150 11700 13200 11700 13200 12000
+2 4 0 4 30 0 50 -1 -1 0.000 0 0 7 0 0 5
+	 13200 12975 6150 12975 6150 12675 13200 12675 13200 12975

binární
DICOMtools/Resources/Screenshots/loadDICOM.png


+ 389 - 0
DICOMtools/exportDicom.py

@@ -0,0 +1,389 @@
+import DICOMLib
+from slicer.ScriptedLoadableModule import *
+import slicerNetwork
+import qt,vtk,ctk,slicer
+import datetime
+import re
+import os
+import slicer.cli
+import json
+
+class exportDicom(slicer.ScriptedLoadableModule.ScriptedLoadableModule):
+  def __init__(self,parent):
+        slicer.ScriptedLoadableModule.ScriptedLoadableModule.__init__(self, parent)
+        self.className="exportDicom"
+        self.parent.title="exportDicom"
+        self.parent.categories = ["LabKeyDICOM"]
+        self.parent.dependencies = []
+        self.parent.contributors = ["Andrej Studen (University of Ljubljana)"] # replace with "Firstname Lastname (Organization)"
+        self.parent.helpText = """
+        This is an example of scripted loadable module bundled in an extension.
+        It adds hierarchy datat for reliable dicom export of a node
+        """
+        self.parent.helpText += self.getDefaultModuleDocumentationLink()
+        self.parent.acknowledgementText = """
+        This extension developed within Medical Physics research programe of ARRS
+        """ # replace with organization, grant and thanks.
+
+#
+# dataExplorerWidget
+
+
+
+class exportDicomWidget(ScriptedLoadableModuleWidget):
+  """Uses ScriptedLoadableModuleWidget base class, available at:
+  https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
+  """
+
+  def setup(self):
+      ScriptedLoadableModuleWidget.setup(self)
+      self.logic=exportDicomLogic(self)
+
+      try:
+           fhome=os.environ["HOME"]
+      except:
+              #in windows, the variable is called HOMEPATH
+           fhome=os.environ['HOMEDRIVE']+os.environ['HOMEPATH']
+
+      cfgPath=os.path.join(fhome,".labkey")
+      cfgNet=os.path.join(cfgPath,"Remote.json")
+
+      self.project='DICOM/Export'
+      
+      baseUUIDFile=os.path.join(cfgPath,"baseUUID.json")
+      baseUUIDCandidate='0.0.0.0.0'
+      try:
+          print("Opening {}".format(baseUUIDFile))
+          f=open(baseUUIDFile,'r')
+          print("Parsing {}".format(baseUUIDFile))
+          baseUUIDCfg=json.load(f)
+          print("Decoding {}".format(baseUUIDCfg))
+          baseUUIDCandidate=baseUUIDCfg['baseUUID']
+          print("baseUUID {}".format(baseUUIDCandidate))
+          self.project=baseUUIDCfg['project']
+          print("project {}".format(self.project))
+          
+      except IOError:
+          pass
+      except JSONDecodeError:
+          pass
+      except KeyError:
+          pass
+          
+      
+      self.Net=slicerNetwork.labkeyURIHandler()
+      self.Net.parseConfig(cfgNet)
+      self.Net.initRemote()
+
+      
+      datasetCollapsibleButton = ctk.ctkCollapsibleButton()
+      datasetCollapsibleButton.text = "Node data"
+      self.layout.addWidget(datasetCollapsibleButton)
+      # Layout within the dummy collapsible button
+      datasetFormLayout = qt.QFormLayout(datasetCollapsibleButton)
+
+      self.volumeNode=qt.QLineEdit("enter name of the volume Node")
+      datasetFormLayout.addRow("volumeNode:",self.volumeNode)
+      self.patientId=qt.QLineEdit("enter PatientID")
+      datasetFormLayout.addRow("PatientId:",self.patientId)
+      self.studyInstanceUid=qt.QLineEdit("enter study id")
+      datasetFormLayout.addRow("StudyInstanceUid:",self.studyInstanceUid)
+      self.studyDescription=qt.QLineEdit("enter study description")
+      datasetFormLayout.addRow("StudyDescription:",self.studyDescription)
+
+      self.addHierarchyButton=qt.QPushButton("Add hierarchy")
+      self.addHierarchyButton.clicked.connect(self.onAddHierarchyButtonClicked)
+      datasetFormLayout.addRow("Volume:",self.addHierarchyButton)
+      
+      self.baseUUIDText=qt.QLineEdit(baseUUIDCandidate)
+      datasetFormLayout.addRow("Base UUID:",self.baseUUIDText)
+      
+       
+
+      self.exportButton=qt.QPushButton("Export")
+      self.exportButton.clicked.connect(self.onExportButtonClicked)
+      datasetFormLayout.addRow("Export:",self.exportButton)
+
+
+
+  def onAddHierarchyButtonClicked(self):
+      metadata={'patientId':self.patientId.text,
+                'studyDescription':self.studyDescription.text,
+                'studyInstanceUid':self.studyInstanceUid.text,
+                'baseUUID':self.baseUUIDText.text}
+      node=slicer.util.getFirstNodeByName(self.volumeNode.text)
+      if node==None:
+          return
+      self.logic.addHierarchy(node,metadata)
+
+  def onExportButtonClicked(self):
+      metadata={'patientId':self.patientId.text,
+                'studyDescription':self.studyDescription.text,
+                'studyInstanceUid':self.studyInstanceUid.text,
+                'seriesInstanceUid':self.logic.generateSeriesUUID('volume',self.baseUUIDText.text),
+                'frameOfReferenceInstanceUid':self.logic.generateFrameOfReferenceUUID('volume',self.baseUUIDText.text)}
+      node=slicer.util.getFirstNodeByName(self.volumeNode.text)
+      if node==None:
+          return
+      self.logic.exportNodeAsDICOM(self.Net,self.project,node,metadata)
+
+
+
+class exportDicomLogic(slicer.ScriptedLoadableModule.ScriptedLoadableModuleLogic):
+  def __init__(self,parent):
+       slicer.ScriptedLoadableModule.ScriptedLoadableModuleLogic.__init__(self, parent)
+       self.labelUUID={'series':'1','study':'2','instance':'3','frameOfReference':4}
+       self.dataUUID={'volume':'1','segmentation':'2','transformation':'3'}
+
+       try:
+           fhome=os.environ["HOME"]
+       except:
+              #in windows, the variable is called HOMEPATH
+           fhome=os.environ['HOMEDRIVE']+os.environ['HOMEPATH']
+
+
+       self.basePath=os.path.join(fhome,'.dicom')
+
+       if not os.path.isdir(self.basePath):
+           os.mkdir(self.basePath)
+
+       self.itemLabel={
+            'patientName': slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientNameAttributeName(),
+            'modality': slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMSeriesModalityAttributeName(),
+            'patientId': slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientIDAttributeName(),
+            'patientSex': slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientSexAttributeName(),
+            'patientBirthDate': slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientBirthDateAttributeName(),
+            'patientComments': slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientCommentsAttributeName(),
+            'studyDescription': slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMStudyDescriptionAttributeName(),
+            'studyInstanceUid': slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMStudyInstanceUIDTagName(),
+            'studyDate': slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMStudyDateAttributeName(),
+            'studyId': slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMStudyIDTagName(),
+            'studyTime': slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMStudyTimeAttributeName()}
+
+
+
+
+
+
+
+  def generateStudyUUID(self,type,baseUUID):
+
+        x=datetime.datetime.now()
+        date=x.strftime("%Y%m%d")
+        studyFile=os.path.join(self.basePath,'studyCount'+date+'.txt')
+
+        try:
+            f=open(studyFile,"r")
+            id=int(f.readline())
+            id=id+1
+            f.close()
+        except:
+            id=0
+
+        studyId="{}.{}.{}.{}.{}".format(baseUUID,self.labelUUID['study'],
+                        self.dataUUID[type],date,id)
+
+        f=open(studyFile,"w")
+        f.write("{}".format(id))
+        f.close()
+        return studyId
+
+
+
+  def generateFrameOfReferenceUUID(self,type,baseUUID):
+
+        x=datetime.datetime.now()
+        date=x.strftime("%Y%m%d")
+        forFile=os.path.join(self.basePath,'frameCount'+date+'.txt')
+
+        try:
+            f=open(studyFile,"r")
+            id=int(f.readline())
+            id=id+1
+            f.close()
+        except:
+            id=0
+
+        forId="{}.{}.{}.{}.{}".format(baseUUID,self.labelUUID['frameOfReference'],
+                        self.dataUUID[type],date,id)
+
+        f=open(forFile,"w")
+        f.write("{}".format(id))
+        f.close()
+        return forId
+
+  def generateSeriesUUID(self,type,baseUUID):
+
+        x=datetime.datetime.now()
+        hour=x.strftime("%H")
+        hour=re.sub('^0','',hour)
+        ft=hour+x.strftime("%M%S")
+        seriesInstanceUid=baseUUID+'.'+self.labelUUID['series']+'.'
+        seriesInstanceUid+=self.dataUUID[type]+'.'+x.strftime("%Y%m%d")+'.'+ft
+        return seriesInstanceUid
+
+  def setAttribute(self,shn,itemId,label,metadata):
+        try:
+            shn.SetItemAttribute(itemId,self.itemLabel['modality'],metadata['modality'])
+        except KeyError:
+            pass
+
+  def addHierarchy(self,dataNode,metadata):
+        #convert dataNode to fully fledged DICOM object in hierarchy
+
+        #variation of addSeriesInSubjectHierarchy
+        shn=slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene)
+        sceneItemId=shn.GetSceneItemID()
+
+        #add the series for the data node
+        seriesItemId=shn.CreateItem(sceneItemId,dataNode)
+
+        x=datetime.datetime.now()
+        hour=x.strftime("%H")
+        hour=re.sub('^0','',hour)
+        ft=hour+x.strftime("%M%S")
+        seriesInstanceUid=metadata['baseUUID']+'.'+self.labelUUID['series']+'.'
+        seriesInstanceUid+=self.dataUUID['volume']+'.'+x.strftime("%Y%m%d")+'.'+ft
+        shn.SetItemUID(seriesItemId,slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMUIDName(),
+            seriesInstanceUid)
+
+        self.setAttribute(shn,seriesItemId,'modality',metadata)
+        self.setAttribute(shn,seriesItemId,'seriesNumber',metadata)
+
+        #add PatientId
+        try:
+            patientId=metadata['patientId']
+        except KeyError:
+            patientId='Unknown'
+
+        try:
+            studyInstanceUid=metadata['studyInstanceUid']
+        except KeyError:
+            studyInstanceUid=self.generateStudyUUID('volume',metadata['baseUUID'])
+
+        slicer.vtkSlicerSubjectHierarchyModuleLogic.InsertDicomSeriesInHierarchy(shn,patientId,
+            studyInstanceUid,seriesInstanceUid)
+
+        patientItemId=shn.GetItemByUID(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMUIDName(),patientId)
+
+        self.setAttribute(shn,patientItemId,'patientName',metadata)
+        self.setAttribute(shn,patientItemId,'patientId',metadata)
+        self.setAttribute(shn,patientItemId,'patientSex',metadata)
+        self.setAttribute(shn,patientItemId,'patientBirthDate',metadata)
+        self.setAttribute(shn,patientItemId,'patientComments',metadata)
+
+        patientItemName=patientId
+        shn.SetItemName(patientItemId,patientItemName)
+
+        studyItemId=shn.GetItemByUID(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMUIDName(),studyInstanceUid)
+
+        self.setAttribute(shn,studyItemId,'studyDescription',metadata)
+        self.setAttribute(shn,studyItemId,'studyInstanceUid',metadata)
+        self.setAttribute(shn,studyItemId,'studyId',metadata)
+        self.setAttribute(shn,studyItemId,'studyDate',metadata)
+        self.setAttribute(shn,studyItemId,'studyTime',metadata)
+
+        studyItemName=studyInstanceUid
+        shn.SetItemName(studyItemId,studyItemName)
+
+  def setTagValue(self,cliparameters,metadata,field):
+       try:
+           cliparameters[field]=metadata[field]
+       except KeyError:
+           pass
+
+  def setDirectoryValue(self,cliparameters,cliKey,metadata,key):
+       try:
+           cliparameters[cliKey]=metadata[key]
+       except KeyError:
+           pass
+
+
+
+  def generateDicom(self, volumeNode, metadata, fdir):
+
+       cliparameters={}
+       tagList=['patientName','patientComments','studyDate','studyTime','studyDescription',
+            'modality','manufacturer','model','seriesDescription',
+            'seriesNumber','seriesDate','seriesTime','contentDate','contentTime']
+
+       for tag in tagList:
+           self.setTagValue(cliparameters,metadata,tag)
+
+       valuePairs={'patientID':'patientId',
+                    'studyID':'studyId',
+                    'seriesInstanceUID':'seriesInstanceUid',
+                    'studyInstanceUID':'studyInstanceUid',
+                    'frameOfReferenceInstanceUID':'frameOfReferenceInstanceUid'}
+
+       for key in valuePairs:
+           self.setDirectoryValue(cliparameters,key,metadata,valuePairs[key])
+
+
+       cliparameters['dicomDirectory']=fdir
+       cliparameters['dicomPrefix']='IMG'
+       cliparameters['inputVolume']=volumeNode.GetID()
+
+       print("[CLI]SeriesInstanceUID: {}").format(cliparameters['seriesInstanceUID'])
+       print("[MeD]SeriesInstanceUID: {}").format(metadata['seriesInstanceUid'])
+
+       try:
+           dicomWrite=slicer.modules.createdicomseries
+       except AttributeError:
+           print("Missing dicom exporter")
+           return
+
+       cliNode=slicer.cli.run(dicomWrite,None,cliparameters,wait_for_completion=True)
+       status=cliNode.GetStatusString()
+       print("Status: {}").format(status)
+       if status.find("error")>-1:
+              print("Error: {}").format(cliNode.GetErrorText())
+              return False
+       return True
+
+
+  def exportNodeAsDICOM(self,net, project,volumeNode,metadata):
+
+
+       #buffed up exportable is now ready to be stored
+       fdir=net.GetLocalCacheDirectory()
+       #project=project.replace('/','\\')
+       fdir=os.path.join(fdir,project)
+       fdir=os.path.join(fdir,'%40files')
+       fdir=os.path.join(fdir,metadata['patientId'])
+       #fdirReg=os.path.join(fdir,'Registration')
+
+       if not os.path.isdir(fdir):
+           os.makedirs(fdir)
+       
+       relDir=net.GetRelativePathFromLocalPath(fdir)
+       remoteDir=net.GetLabkeyPathFromRelativePath(relDir)
+
+       if not net.isRemoteDir(remoteDir):
+            net.mkdir(remoteDir)
+
+              
+       dirName=volumeNode.GetName();
+       dirName=dirName.replace(" ","_")
+       fdir=os.path.join(fdir,dirName)
+
+       if not os.path.isdir(fdir):
+           os.mkdir(fdir)
+
+       relDir=net.GetRelativePathFromLocalPath(fdir)
+       remoteDir=net.GetLabkeyPathFromRelativePath(relDir)
+
+       if not net.isRemoteDir(remoteDir):
+            net.mkdir(remoteDir)
+
+       if not self.generateDicom(volumeNode,metadata,fdir):
+           return
+
+       for f in os.listdir(fdir):
+           localPath=os.path.join(fdir,f)
+           print("localPath: {}").format(localPath)
+           relativePath=net.GetRelativePathFromLocalPath(localPath)
+           print("relativePath: {}").format(relativePath)
+           remotePath=net.GetLabkeyPathFromRelativePath(relativePath)
+           print("remotePath: {}").format(relativePath)
+           net.copyLocalFileToRemote(localPath,remotePath)

binární
DICOMtools/exportDicom.pyc


+ 359 - 0
DICOMtools/importDicom.py

@@ -0,0 +1,359 @@
+import dicom
+import vtkInterface as vi
+import os
+import vtk, qt, ctk, slicer
+from slicer.ScriptedLoadableModule import *
+import slicerNetwork
+import json
+import loadDicom
+import DICOMLib
+
+class importDicom(slicer.ScriptedLoadableModule.ScriptedLoadableModule):
+    def __init__(self,parent):
+        slicer.ScriptedLoadableModule.ScriptedLoadableModule.__init__(self, parent)
+        self.className="importDICOM"
+        self.parent.title="importDICOM"
+        self.parent.categories = ["LabKeyDICOM"]
+        self.parent.dependencies = []
+        self.parent.contributors = ["Andrej Studen (UL/FMF)"] # replace with "Firstname Lastname (Organization)"
+        self.parent.helpText = """
+                utilities for parsing dicom entries
+                """
+        self.parent.acknowledgementText = """
+                Developed within the medical physics research programme of the Slovenian research agency.
+                """ # replace with organization, grant and thanks.
+
+class importDicomWidget(slicer.ScriptedLoadableModule.ScriptedLoadableModuleWidget):
+  """Uses ScriptedLoadableModuleWidget base class, available at:
+  https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
+  """
+
+  def setup(self):
+      
+    slicer.ScriptedLoadableModule.ScriptedLoadableModuleWidget.setup(self)
+    self.logic=importDicomLogic(self)
+    self.network=slicerNetwork.labkeyURIHandler()
+    
+    connectionCollapsibleButton = ctk.ctkCollapsibleButton()
+    connectionCollapsibleButton.text = "Connection"
+    self.layout.addWidget(connectionCollapsibleButton)
+
+    connectionFormLayout = qt.QFormLayout(connectionCollapsibleButton)
+    
+    self.loadConfigButton=qt.QPushButton("Load configuration")
+    self.loadConfigButton.toolTip="Load configuration"
+    self.loadConfigButton.connect('clicked(bool)',self.onLoadConfigButtonClicked)
+    connectionFormLayout.addRow("Connection:",self.loadConfigButton)
+
+    self.DICOMDirectory=qt.QLineEdit("Test/Temp/%40files/TEST/MLEM")
+    connectionFormLayout.addRow("LabKey directory:",self.DICOMDirectory)
+
+
+    #loadDICOMButton=qt.QPushButton("Load")
+    #loadDICOMButton.toolTip="Load DICOM"
+    #loadDICOMButton.clicked.connect(self.onLoadDICOMButtonClicked)
+    #connectionFormLayout.addRow("DICOM:",loadDICOMButton)
+
+    self.DICOMFilter=qt.QLineEdit('{"seriesNumber":"SeriesLabel"}')
+    connectionFormLayout.addRow("Filter(JSON):",self.DICOMFilter)
+
+    loadDICOMFilterButton=qt.QPushButton("Load with filter")
+    loadDICOMFilterButton.toolTip="Load DICOM with filter"
+    loadDICOMFilterButton.clicked.connect(self.onLoadDICOMFilterButtonClicked)
+    connectionFormLayout.addRow("DICOM:",loadDICOMFilterButton)
+
+  def onLoadConfigButtonClicked(self):
+       filename=qt.QFileDialog.getOpenFileName(None,'Open configuration file (JSON)',
+            os.path.join(os.path.expanduser('~'),'.labkey'), '*.json')
+       self.network.parseConfig(filename)
+       self.network.initRemote()
+       self.loadConfigButton.setText(os.path.basename(filename))
+  
+  def onLoadDICOMFilterButtonClicked(self):
+      filter=json.loads(self.DICOMFilter.text)
+      #print("Filter is {}".format(filter))
+      self.logic.loadVolumes(self.network,self.DICOMDirectory.text,filter)
+    
+  def onLoadDICOMButtonClicked(self):
+      self.logic.load(self.network,self.DICOMDirectory.text)
+       
+
+
+#equivalent of loadable + labkey interface
+class dicomSeries():
+    def __init__(self):
+        self.data = []
+        self.idx = []
+        self.z = []
+        self.pixel_size = [0,0,0]
+        self.lpsOrigin = [0,0,0]
+        self.lpsOrigin[2]=1e30
+        self.lpsOrientation=[0,0,0,0,0,0]
+        self.local=False
+
+
+    def getfile(self,net,relativePath):
+        if self.local:
+                return open(relativePath,'rb')
+        return net.readFileToBuffer(relativePath)
+
+    def addFile(self,f):
+        try:
+            self.files.append(f)
+        except:
+            self.files=[f]
+
+    def setLabel(self,label):
+        self.label=label
+
+    def getLabel(self):
+        try:
+            return self.label
+        except:
+            return None
+
+    def setMetadata(self,key,value):
+        try:
+            self.metadata[key]=value
+        except:
+            self.metadata={key:value}
+
+    def getMetadata(self):
+        try:
+            return self.metadata
+        except:
+            return {}
+
+            
+
+    def load(self,net):
+
+        for f in self.files:
+            print '{}:'.format(f)
+            fileBuffer=self.getfile(net,f)
+            self.loadFile(fileBuffer)
+
+        nz=len(self.idx)
+        sh=self.data[-1].shape
+        sh_list=list(sh)
+        sh_list.append(nz)
+        data_array=np.zeros(sh_list)
+
+        for k in range(0,nz):
+            kp=int(np.round((self.z[k]-self.center[2])/self.pixel_size[2]))
+            data_array[:,:,kp]=np.transpose(self.data[k])
+
+        try:
+            nodeName='Series'+self.label
+        except:
+            print('Could not set series label')
+            nodeName='UnknownSeries'
+
+        newNode=slicer.vtkMRMLScalarVolumeNode()
+        newNode.SetName(nodeName)
+
+        ijkToRAS = vtk.vtkMatrix4x4()
+        #think how to do this with image orientation
+
+        rasOrientation=[-self.lpsOrientation[i] if (i%3 < 2) else self.lpsOrientation[i]
+          for i in range(0,len(self.lpsOrientation))]
+        rasOrigin=[-self.lpsOrigin[i] if (i%3<2) else self.lpsOrigin[i] for i in range(0,len(self.lpsOrigin))]
+
+        for i in range(0,3):
+             for j in range(0,3):
+                 ijkToRAS.SetElement(i,j,self.pixel_size[i]*rasOrientation[3*j+i])
+
+             ijkToRAS.SetElement(i,3,rasOrigin[i])
+
+        newNode.SetIJKToRASMatrix(ijkToRAS)
+
+        v=vtk.vtkImageData()
+        v.GetPointData().SetScalars(
+            vtk.util.numpy_support.numpy_to_vtk(
+                np.ravel(self.data,order='F'),deep=True, array_type=vtk.VTK_FLOAT))
+        v.SetOrigin(0,0,0)
+        v.SetSpacing(1,1,1)
+        v.SetDimensions(self.data.shape)
+
+        newNode.SetAndObserveImageData(v)
+        slicer.mrmlScene.AddNode(newNode)
+        volume={'node':newNode,'metadata':self.metadata}
+        return volume
+
+    def loadFile(self,fileBuffer):
+
+        plan=dicom.read_file(fileBuffer)
+
+        self.data.append(plan.pixel_array)
+        self.idx.append(plan.InstanceNumber)
+        self.z.append(plan.ImagePositionPatient[2])
+
+
+        #pixelSize
+        pixel_size=[plan.PixelSpacing[0],plan.PixelSpacing[1],
+                    plan.SliceThickness]
+
+        for i in range(0,3):
+            if self.pixel_size[i] == 0:
+                self.pixel_size[i] = float(pixel_size[i])
+            if abs(self.pixel_size[i]-pixel_size[i]) > 1e-3:
+                print 'Pixel size mismatch {.2f}/{.2f}'.format(self.pixel_size[i],
+                        pixel_size[i])
+
+        #origin
+        for i in range(0,2):
+            if self.lpsOrigin[i] == 0:
+                self.lpsOrigin[i] = float(plan.ImagePositionPatient[i])
+            if abs(self.lpsOrigin[i]-plan.ImagePositionPatient[i]) > 1e-3:
+                print 'Image center mismatch {.2f}/{.2f}'.format(self.lpsOrigin[i],
+                    plan.ImagePositionPatient[i])
+        #not average, but minimum (!) why??
+
+        if plan.ImagePositionPatient[2]<self.lpsOrigin[2]:
+            self.lpsOrigin[2]=plan.ImagePositionPatient[2]
+
+
+        #orientation
+        for i in range(0,6):
+            if self.lpsOrientation[i] == 0:
+                self.lpsOrientation[i] = float(plan.ImageOrientationPatient[i])
+            if abs(self.lpsOrientation[i]-plan.ImageOrientationPatient[i]) > 1e-3:
+                print 'Image orientation mismatch {0:.2f}/{1:.2f}'.format(self.lpsOrientation[i],
+                        plan.ImageOrientationPatient[i])
+
+        return True
+
+class importDicomLogic(slicer.ScriptedLoadableModule.ScriptedLoadableModuleLogic):
+    def __init__(self,parent):
+        slicer.ScriptedLoadableModule.ScriptedLoadableModuleLogic.__init__(self, parent)
+        self.local=False
+        self.tag={
+        'studyInstanceUid':0x0020000d,
+        'seriesInstanceUid':0x0020000e,
+        'patientId':0x00100020,
+        'patientName':0x00100010,
+        'sequenceName':0x00180024,
+        'seriesNumber':0x00200011,
+        'percentPhaseFieldOfView':0x00180094,
+        'modality': 0x00080060,
+        'patientSex': 0x00100040,
+        'patientBirthDate': 0x00100030,
+        'patientComments': 0x00104000,
+        'studyDescription': 0x00081030,
+        'studyDate': 0x00080020,
+        'studyId': 0x00200010,
+        'studyTime': 0x00080030,
+        'frameOfReferenceInstanceUid':0x00200052}
+    
+
+
+    def setLocal(self, basePath):
+        self.local=True
+        self.basePath=basePath
+
+        
+    def loadVolumes(self,net,directory,filter):
+        #mimic examineForImport
+        seriesList=self.examineForImport(net,directory,filter)
+        print("Got {} series").format(len(seriesList))
+        volumes=[]
+
+        for s in seriesList:
+            try:
+                volumes.append(s.load(net))
+                #often fails, e.g. JPEGLossles
+            except:
+                loadable=DICOMLib.DICOMLoadable()
+                loadable.name='Series'+str(s.getLabel())
+                print("Loading for {} number of files (pre-load) {}").format(loadable.name,len(s.files))
+                loadable.files=[net.DownloadFileToCache(f) for f in s.files]
+                print("Loading for {} number of files (pre-sort) {}").format(loadable.name,len(loadable.files))
+
+                loadable.files,distances,loadable.warning=DICOMLib.DICOMUtils.getSortedImageFiles(loadable.files,1e-3)
+
+                print("Loading for {} number of files {}").format(loadable.name,len(loadable.files))
+                try:
+                    volumeNode=self.volumePlugin.load(loadable)
+                except:
+                    self.volumePlugin=slicer.modules.dicomPlugins['DICOMScalarVolumePlugin']()
+                    volumeNode=self.volumePlugin.load(loadable)
+                volume={'node':volumeNode,'metadata':s.getMetadata()}
+                volumes.append(volume)
+
+
+        return volumes
+
+
+    def listdir(self,net,relativeDirectory):
+        if self.local:
+            dirs=os.listdir(os.path.join(self.basePath,relativeDirectory))
+            return [os.path.join(relativeDirectory,dir) for dir in dirs]
+        return net.listRelativeDir(relativeDirectory)
+
+    def getfile(self,net,relativePath):
+        if self.local:
+            return open(os.path.join(self.basePath,relativePath),'rb')
+        return net.readFileToBuffer(relativePath)
+
+    def examineForImport(self,net,directory,filter):
+        #split by series
+        seriesList=[]
+
+        files=self.listdir(net,directory)
+        if len(files)==0:
+            print("No input found in {}".format(directory))
+            return seriesList
+
+
+        for f in files:
+            fileBuffer=self.getfile(net,f)
+
+            #validate
+            try:
+                plan = dicom.read_file(fileBuffer)
+            except:
+                print ("{}: Not a dicom file")
+                continue
+
+            #determine validity first
+            fileValid=True
+            for key in filter:
+                if filter[key]==None:
+                    continue
+                if filter[key]=='SeriesLabel':
+                    seriesTag=self.tag[key]
+                    continue
+
+                v=plan[self.tag[key]].value
+                if not v==filter[key]:
+                    print('Filter mismatch {}{:x}: {}/{}').format(key,self.tag[key],v,filter[key])
+                    fileValid=False
+
+            if not fileValid:
+                continue
+
+            #determine serieslabel second
+            seriesLabel=plan[seriesTag].value
+                
+            try:
+                if series.getLabel()==seriesLabel:
+                    series.addFile(f)
+                    continue
+            except NameError:
+                pass
+
+            #add new series
+            seriesList.append(dicomSeries())
+            series=seriesList[-1]
+            series.local=self.local
+
+            #set series parameters
+            series.addFile(f)
+            series.setLabel(seriesLabel)
+            for key in filter:
+                if not filter[key]==None:
+                    continue
+                v=plan[self.tag[key]].value
+                series.setMetadata(key,v)
+
+        return seriesList

binární
DICOMtools/importDicom.pyc


+ 458 - 0
DICOMtools/loadDicom.py

@@ -0,0 +1,458 @@
+import slicer
+import os
+import subprocess
+import re
+import slicerNetwork
+import ctk,qt
+import json
+
+dicomModify=os.getenv("HOME")
+if not dicomModify==None:
+    dicomModify+="/software/install/"
+    dicomModify+="dicomModify/bin/dicomModify"
+
+class loadDicom(slicer.ScriptedLoadableModule.ScriptedLoadableModule):
+    def __init__(self,parent):
+        slicer.ScriptedLoadableModule.ScriptedLoadableModule.__init__(self, parent)
+        self.className="loadDicom"
+        self.parent.title="loadDicom"
+        self.parent.categories = ["LabKeyDICOM"]
+        self.parent.dependencies = []
+        self.parent.contributors = ["Andrej Studen (UL/FMF)"] # replace with "Firstname Lastname (Organization)"
+        self.parent.helpText = """
+            utilities for parsing dicom entries
+            """
+        self.parent.acknowledgementText = """
+            Developed within the medical physics research programme of the Slovenian research agency.
+            """ # replace with organization, grant and thanks.
+
+class loadDicomWidget(slicer.ScriptedLoadableModule.ScriptedLoadableModuleWidget):
+  """Uses ScriptedLoadableModuleWidget base class, available at:
+  https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
+  """
+
+  def setup(self):
+    slicer.ScriptedLoadableModule.ScriptedLoadableModuleWidget.setup(self)
+    self.logic=loadDicomLogic(self)
+    self.network=slicerNetwork.labkeyURIHandler()
+    
+    connectionCollapsibleButton = ctk.ctkCollapsibleButton()
+    connectionCollapsibleButton.text = "Connection"
+    self.layout.addWidget(connectionCollapsibleButton)
+
+    connectionFormLayout = qt.QFormLayout(connectionCollapsibleButton)
+    
+    self.loadConfigButton=qt.QPushButton("Load configuration")
+    self.loadConfigButton.toolTip="Load configuration"
+    self.loadConfigButton.connect('clicked(bool)',self.onLoadConfigButtonClicked)
+    connectionFormLayout.addRow("Connection:",self.loadConfigButton)
+
+    self.DICOMDirectory=qt.QLineEdit("Test/Temp/%40files/TEST/MLEM")
+    connectionFormLayout.addRow("LabKey directory:",self.DICOMDirectory)
+
+
+    loadDICOMButton=qt.QPushButton("Load")
+    loadDICOMButton.toolTip="Load DICOM"
+    loadDICOMButton.clicked.connect(self.onLoadDICOMButtonClicked)
+    connectionFormLayout.addRow("DICOM:",loadDICOMButton)
+
+    self.DICOMFilter=qt.QLineEdit('{"seriesNumber":"SeriesLabel"}')
+    connectionFormLayout.addRow("Filter(JSON):",self.DICOMFilter)
+
+    loadDICOMFilterButton=qt.QPushButton("Load with filter")
+    loadDICOMFilterButton.toolTip="Load DICOM with filter"
+    loadDICOMFilterButton.clicked.connect(self.onLoadDICOMFilterButtonClicked)
+    connectionFormLayout.addRow("DICOM:",loadDICOMFilterButton)
+
+    loadDICOMSegmentationFilterButton=qt.QPushButton("Load segmentation with filter")
+    loadDICOMSegmentationFilterButton.toolTip="Load DICOM (RT contour) with filter"
+    loadDICOMSegmentationFilterButton.clicked.connect(self.onLoadDICOMSegmentationFilterButtonClicked)
+    connectionFormLayout.addRow("DICOM:",loadDICOMSegmentationFilterButton)
+
+
+  def onLoadConfigButtonClicked(self):
+       filename=qt.QFileDialog.getOpenFileName(None,'Open configuration file (JSON)',
+            os.path.join(os.path.expanduser('~'),'.labkey'), '*.json')
+       self.network.parseConfig(filename)
+       self.network.initRemote()
+       self.loadConfigButton.setText(os.path.basename(filename))
+  
+  def onLoadDICOMFilterButtonClicked(self):
+      filter=json.loads(self.DICOMFilter.text)
+      #print("Filter is {}".format(filter))
+      self.logic.loadVolumes(self.network,self.DICOMDirectory.text,filter)
+    
+  def onLoadDICOMSegmentationFilterButtonClicked(self):
+      filter=json.loads(self.DICOMFilter.text)
+      #print("Filter is {}".format(filter))
+      self.logic.loadSegmentations(self.network,self.DICOMDirectory.text,filter)
+  
+  def onLoadDICOMButtonClicked(self):
+      self.logic.load(self.network,self.DICOMDirectory.text)
+       
+
+class loadDicomLogic(slicer.ScriptedLoadableModule.ScriptedLoadableModuleLogic):
+    def __init__(self,parent):
+        slicer.ScriptedLoadableModule.ScriptedLoadableModuleLogic.__init__(self, parent)
+
+        self.tag={
+              'studyDate': {'tag':"0008,0020",'VR':'DA'},
+              'studyTime': {'tag':"0008,0030",'VR':'TM'},
+              'seriesTime': {'tag':"0008,0031",'VR':'TM'},
+              'modality': {'tag':"0008,0060",'VR':'CS'},
+              'presentationIntentType': {'tag':"0008,0068",'VR':'CS'},
+              'manufacturer': {'tag':"0008,0070",'VR':'LO'},
+              'institutionName': {'tag':"0008,0080",'VR':'LO'},
+              'studyDescription': {'tag':"0008,1030",'VR':'LO'},
+              'seriesDescription': {'tag':"0008,103e",'VR':'LO'},
+              'manufacturerModelName': {'tag':"0008,1090",'VR':'LO'},
+              'patientName': {'tag':"0010,0010",'VR':'PN'},
+              'patientId': {'tag':"0010,0020",'VR':'LO'},
+              'patientBirthDate': {'tag':"0010,0030",'VR':'DA'},
+              'patientSex': {'tag':"0010,0040",'VR':'CS'},
+              'patientAge': {'tag':"0010,1010",'VR':'AS'},
+              'patientComments': {'tag':"0010,4000",'VR':'LT'},
+              'sequenceName': {'tag':"0018,0024",'VR':'SH'},
+              'kVP': {'tag':"0018,0060",'VR':'DS'},
+              'percentPhaseFieldOfView': {'tag':"0018,0094",'VR':'DS'},
+              'xRayTubeCurrent': {'tag':"0018,1151",'VR':'IS'},
+              'exposure': {'tag':"0018,1152",'VR':'IS'},
+              'imagerPixelSpacing': {'tag':"0018,1164",'VR':'DS'},
+              'bodyPartThickness': {'tag':"0018,11a0",'VR':'DS'},
+              'compressionForce': {'tag':"0018,11a2",'VR':'DS'},
+              'viewPosition': {'tag':"0018,5101",'VR':'CS'},
+              'fieldOfViewHorizontalFlip': {'tag':"0018,7034",'VR':'CS'},
+              'studyInstanceUid': {'tag':"0020,000d",'VR':'UI'},
+              'seriesInstanceUid': {'tag':"0020,000e",'VR':'UI'},
+              'studyId': {'tag':"0020,0010",'VR':'SH'},
+              'seriesNumber': {'tag':"0020,0011",'VR':'IS'},
+              'instanceNumber': {'tag':"0020,0013",'VR':'IS'},
+              'frameOfReferenceInstanceUid': {'tag':"0020,0052",'seqTag':"3006,0010",'VR':'UI'},
+              'imageLaterality': {'tag':"0020,0062",'VR':'CS'},
+              'imagesInAcquisition': {'tag':"0020,1002",'VR':'IS'},
+              'photometricInterpretation': {'tag':"0028,0004",'VR':'CS'},
+              'reconstructionMethod': {'tag':"0054,1103",'VR':'LO'}
+        }
+        self.tagPyDicom={
+              'studyDate': 0x00080020,
+              'studyTime': 0x00080030,
+              'modality': 0x00080060,
+              'presentationIntentType': 0x00080068,
+              'manufacturer': 0x00080070,
+              'studyDescription': 0x00081030,
+              'seriesDescription': 0x0008103e,
+              'patientName': 0x00100010,
+              'patientId': 0x00100020,
+              'patientBirthDate': 0x00100030,
+              'patientSex': 0x00100040,
+              'patientComments': 0x00104000,
+              'sequenceName': 0x00180024,
+              'kVP': 0x00180060,
+              'percentPhaseFieldOfView': 0x00180094,
+              'xRayTubeCurrent': 0x00181151,
+              'exposure': 0x00181152,
+              'imagerPixelSpacing': 0x00181164,
+              'bodyPartThickness': 0x001811a0,
+              'compressionForce': 0x001811a2,
+              'viewPosition': 0x00185101,
+              'studyInstanceUid': 0x0020000d,
+              'seriesInstanceUid': 0x0020000e,
+              'studyId': 0x00200010,
+              'seriesNumber': 0x00200011,
+              'instanceNumber': 0x00200013,
+              'frameOfReferenceInstanceUid': 0x00200052
+              }
+      #new_dict_items={
+      #        0x001811a0: ('DS','BodyPartThickness','Body Part Thickness')
+      #}
+      #dicom.datadict.add_dict_entries(new_dict_items)
+        self.local=False
+
+    def setLocal(self,basePath):
+        self.local=True
+        self.basePath=basePath
+        
+
+    def getHex(self,key):
+        #convert string to hex key;
+        fv=key.split(",")
+        return int(fv[0],16)*0x10000+int(fv[1],16)
+
+    def load(self,sNet,dir,doRemove=True):
+        #load directory using DICOMLib tools
+
+        print("Loading dir {}").format(dir)
+        dicomFiles=self.listdir(sNet,dir)
+        
+        filelist=[]
+        for f in dicomFiles:
+                localPath=self.getfile(sNet,f)
+                f0=localPath
+                f1=f0+"1"
+                if not dicomModify==None:
+                    try:
+                        subprocess.call(dicomModify+" "+f0+" "+f1+" && mv "+f1+" "+f0+";", shell=True)
+                    except OSError:
+                        print("dicomModify failed")
+                filelist.append(localPath)
+
+        try:
+            loadables=self.volumePlugin.examineForImport([filelist])
+        except AttributeError:
+            self.volumePlugin=slicer.modules.dicomPlugins['DICOMScalarVolumePlugin']()
+            loadables=self.volumePlugin.examineForImport([filelist])
+
+
+        for loadable in loadables:
+            #check if it makes sense to load a particular loadable
+            
+            if loadable.name.find('imageOrientationPatient')>-1:
+                continue
+            filter={}
+            filter['seriesNumber']=None 
+            metadata={}
+            if not self.applyFilter(loadable,filter,metadata):
+                continue
+            volumeNode=self.volumePlugin.load(loadable)
+            if volumeNode != None:
+                vName='Series'+metadata['seriesNumber']
+                volumeNode.SetName(vName)
+
+        try:
+            loadableRTs=self.RTPlugin.examineForImport([filelist])
+        except:
+            self.RTPlugin=plugin=slicer.modules.dicomPlugins['DicomRtImportExportPlugin']()
+            loadableRTs=self.RTPlugin.examineForImport([filelist])
+
+        for loadable in loadableRTs:
+            segmentationNode=self.RTPlugin.load(loadable)
+           
+        if not doRemove:
+            return
+
+        for f in filelist:
+            os.remove(f)
+
+    def applyFilter(self,loadable,filter,nodeMetadata):
+        #apply filter to loadable.file[0]. Return true if file matches prescribed filter and
+        #false otherwise
+
+        #filter is a directory with keys equal to pre-specified values listed above
+        #if value associated to key equals None, that value gets set in nodeMetadata
+        #if value is set, a match is attempted and result reported in return value
+        #all filters should match for true output
+
+        filterOK=True
+        
+        for key in filter:
+            try:
+                fileValue=dicomValue(loadable.files[0],self.tag[key]['tag'],self.tag[key]['seqTag'])
+            except KeyError:
+                fileValue=dicomValue(loadable.files[0],self.tag[key]['tag'])
+
+
+            if filter[key]=="SeriesLabel":
+                nodeMetadata['seriesLabel']=fileValue
+                continue
+
+            if not filter[key]==None:
+                if not fileValue==filter[key]:
+                    print("File {} failed for tag {}: {}/{}").format(
+                        loadable.files[0],key,fileValue,filter[key])
+                    filterOK=False
+                    break
+
+            nodeMetadata[key]=fileValue
+
+
+        return filterOK
+
+    def listdir(self,sNet,dir):
+        #list remote directory
+        if self.local:
+            dir1=os.path.join(self.basePath,dir)
+            dirs=os.listdir(dir1)
+            return [os.path.join(dir1,f) for f in dirs]
+        return sNet.listRelativeDir(dir)
+
+    def getfile(self,sNet,file):
+        #get remote file
+        if self.local:
+            return file
+        return sNet.DownloadFileToCache(file)
+        
+
+    def loadVolumes(self,sNet,dir,filter,doRemove=True):
+    #returns all series from the directory, each as a separate node in a node list
+    #filter is a dictionary of speciifed dicom values, if filter(key)=None, that values
+    #get set, if it isn't, the file gets checked for a match
+
+        print("Loading dir {}").format(dir)
+        dicomFiles=self.listdir(sNet,dir)
+        #filelist=[os.path.join(dir,f) for f in os.listdir(dir)]
+        filelist=[]
+        for f in dicomFiles:
+            localPath=self.getfile(sNet,f)
+            f0=localPath
+            f1=f0+"1"
+            if not dicomModify==None:
+                try:
+                    subprocess.call(dicomModify+" "+f0+" "+f1+" && mv "+f1+" "+f0+";", shell=False)
+                except OSError:
+                    print("dicomModify failed")
+            filelist.append(localPath)
+
+        try:
+            loadables=self.volumePlugin.examineForImport([filelist])
+        except AttributeError:
+            self.volumePlugin=slicer.modules.dicomPlugins['DICOMScalarVolumePlugin']()
+            loadables=self.volumePlugin.examineForImport([filelist])
+
+
+        volumeNodes=[]
+        print("Number of loadables:{}").format(len(loadables))
+        for loadable in loadables:
+            #TODO check if it makes sense to load a particular loadable
+
+            print "Loading {} number of files: {}".format(loadable.name,len(loadable.files))
+            for f in loadable.files:
+                print "\t {}".format(f)
+            #perform checks
+            nodeMetadata={}
+            filterOK=self.applyFilter(loadable,filter,nodeMetadata)
+
+            if not filterOK:
+                #skip this loadable
+                continue
+
+            volumeNode=self.volumePlugin.load(loadable,"DCMTK")
+            if volumeNode != None:
+                vName='Series'+nodeMetadata['seriesLabel']
+                volumeNode.SetName(vName)
+                volume={'node':volumeNode,'metadata':nodeMetadata}
+                volumeNodes.append(volume)
+
+        if self.local:
+            return volumeNodes
+
+        if doRemove:
+            for f in filelist:
+                os.remove(f)
+
+        return volumeNodes
+
+    def loadSegmentations(self,net,dir,filter,doRemove=True):
+
+        print("Loading dir {}").format(dir)
+        dicomFiles=self.listdir(net,dir)
+        filelist=[self.getfile(net,f) for f in dicomFiles]
+        segmentationNodes=[]
+
+        try:
+            loadableRTs=self.RTPlugin.examineForImport([filelist])
+        except:
+            self.RTPlugin=plugin=slicer.modules.dicomPlugins['DicomRtImportExportPlugin']()
+            loadableRTs=self.RTPlugin.examineForImport([filelist])
+
+        for loadable in loadableRTs:
+
+            nodeMetadata={}
+            filterOK=self.applyFilter(loadable,filter,nodeMetadata)
+
+            if not filterOK:
+                continue
+
+            success=self.RTPlugin.load(loadable)
+
+            if not success:
+                print("Could not load RT structure set")
+                return
+
+            segNodes=slicer.util.getNodesByClass("vtkMRMLSegmentationNode")
+            segmentationNode=segNodes[0]
+            #assume we loaded the first node in list
+
+            if segmentationNode != None:
+                sName='Segmentation'+nodeMetadata['seriesLabel']
+                segmentationNode.SetName(sName)
+                segmentation={'node':segmentationNode,'metadata':nodeMetadata}
+                segmentationNodes.append(segmentation)
+
+        if self.local or not doRemove:
+            return segmentationNodes
+        
+        for f in filelist:
+            os.remove(f)
+
+        return segmentationNodes
+
+def isDicom(file):
+    #check if file is a dicom file
+    try:
+        f=open(file,'rb')
+    except IOError:
+        return False
+
+    f.read(128)
+    dt=f.read(4)
+    f.close()
+    return dt=='DICM'
+
+
+def dicomValue(file,tag,seqTag=None,shell=False):
+    #query dicom value of file using dcmdump (DCMTK routine)
+    debug=False
+    dcmdump=os.path.join(os.environ['SLICER_HOME'],"bin","dcmdump")
+    try:
+        out=subprocess.check_output([dcmdump,'+p','+P',tag,file],shell=shell)
+        if debug:
+            print("Tag {} Line '{}'").format(tag,out)
+        if len(out)==0:
+            return out
+
+        #parse output
+        longTag="^\({}\)".format(tag)
+        #combine sequence and actual tags to long tags
+        if not seqTag==None:
+            if debug:
+                print("Tag:{} seqTag:{}").format(tag,seqTag)
+            longTag="^\({}\).\({}\)".format(seqTag,tag)
+        #parse multi-match outputs which appear as several lines
+        lst=out.split('\n')
+        #pick the values
+        pattern=r'^.*\[(.*)\].*$'
+        #extract tag values
+        rpl=[re.sub(pattern,r'\1',f) for f in lst]
+        #logical whether the line can be matched to typical dcmdump output
+        mtchPattern=[re.match(pattern,f) for f in lst]
+        #find matching tags
+        mtchTag=[re.match(longTag,f) for f in lst]
+        
+        #weed out non-matching lines and lines not matching longTag
+        mtch=[None if x==None or y==None else x \
+                for x,y in zip(mtchTag,mtchPattern)]
+        #set values 
+        out=[x for y,x in zip(mtch,rpl) if not y==None]
+        if len(out)==0:
+            return ''
+        #return first match
+        out=out[0]
+        
+        if debug:
+            print("Tag {} Parsed value {}").format(tag,out)
+        #split output to lists if values are DICOM lists
+        if out.find('\\')>-1:
+            out=out.split('\\')
+
+        return out
+    except subprocess.CalledProcessError as e:
+        return None
+
+def clearNodes():
+    nodes=[]
+    nodes.extend(slicer.util.getNodesByClass("vtkMRMLScalarVolumeNode"))
+    nodes.extend(slicer.util.getNodesByClass("vtkMRMLScalarVolumeDisplayNode"))
+    nodes.extend(slicer.util.getNodesByClass("vtkMRMLSegmentationNode"))
+    nodes.extend(slicer.util.getNodesByClass("vtkMRMLSegmentationDisplayNode"))
+    for node in nodes:
+        slicer.mrmlScene.RemoveNode(node)

binární
DICOMtools/loadDicom.pyc


+ 110 - 0
DICOMtools/vtkInterface.py

@@ -0,0 +1,110 @@
+import vtk, qt, ctk, slicer
+import numpy as np
+import SimpleITK as sitk
+
+#set of routines to transform images from one form to another, most notably
+#numpy to vtk to itk and all possible combinations inbetween. Keep track of
+#orientation, origin and spacing between transforms
+
+class vtkInterface:
+  def __init__(self, parent):
+    parent.title = "vtk Interface"
+    parent.categories = ["LabKeyDICOM"]
+    parent.dependencies = []
+    parent.contributors = ["Andrej Studen (FMF/JSI)"] # replace with "Firstname Lastname (Org)"
+    parent.helpText = """
+    Convert native numpy data structures to vtk
+    """
+    parent.acknowledgementText = """
+    This module was developed within the frame of the ARRS sponsored medical
+    physics research programe to investigate quantitative measurements of cardiac
+    function using sestamibi-like tracers
+    """ # replace with organization, grant and thanks.
+    self.parent = parent
+
+
+def numpyToVTK(numpy_array, shape, data_type=vtk.VTK_FLOAT):
+    v=vtk.vtkImageData()
+    v.GetPointData().SetScalars(
+        vtk.util.numpy_support.numpy_to_vtk(
+            np.ravel(numpy_array,order='F'),deep=True, array_type=data_type))
+    v.SetOrigin(0,0,0)
+    v.SetSpacing(1,1,1)
+    v.SetDimensions(shape)
+    return v
+
+
+def completeOrientation(orientation):
+    o=orientation
+    o.append(o[1]*o[5]-o[2]*o[4])#0,3
+    o.append(o[2]*o[3]-o[0]*o[5])#1,4
+    o.append(o[0]*o[4]-o[1]*o[3])#2,5
+    return o
+
+
+def ITK2VTK(img):
+    #convert itk to vtk format.
+    #get the array
+    data=sitk.GetArrayFromImage(img)
+    #reverse the shape (don't ask, look at vtk manual if really curios)
+    shape=list(reversed(data.shape))
+    return numpyToVTK(data.ravel(),shape)
+
+def VTK2ITK(v):
+    #convert vtk image to sitk image
+    #convert to numpy first and then go to sitk
+    scalars=v.GetPointData().GetScalars()
+    shape=v.GetDimensions()
+    data=vtk.util.numpy_support.vtk_to_numpy(scalars)
+    #now convert to sitk (notice the little reversal of the shape)
+    return sitk.GetImageFromArray(np.reshape(data,list(reversed(shape))))
+
+def ITKfromNode(nodeName):
+    #use node as data source and generate an itk image
+    node=slicer.mrmlScene.GetFirstNodeByName(nodeName)
+    if node==None:
+        print "Node {0} not available".format(nodeName)
+        return
+
+    img=VTK2ITK(node.GetImageData())
+
+    img.SetOrigin(node.GetOrigin())
+    img.SetSpacing(node.GetSpacing())
+    m=vtk.vtkMatrix4x4()
+    node.GetIJKToRASDirectionMatrix(m)
+    orientation=[0]*9
+    for i in range(0,3):
+        for j in range (0,3):
+            orientation[3*j+i]=m.GetElement(i,j)
+    img.SetDirection(orientation)
+    return img
+
+
+
+def ITKtoNode(img,nodeName):
+    #display itk image and assign it a volume node
+    #useful for displaying outcomes of itk calculations
+    node=slicer.mrmlScene.GetFirstNodeByName(nodeName)
+    if node==None:
+        node=slicer.vtkMRMLScalarVolumeNode()
+        node.SetName(nodeName)
+        slicer.mrmlScene.AddNode(node)
+
+    node.SetAndObserveImageData(ITK2VTK(img))
+
+    #hairy - keep orientation, spacing and origin from node and pass it to itk
+    #for future reference
+    spacing=img.GetSpacing()
+    orientation=img.GetDirection()
+    origin=img.GetOrigin()
+
+    #we should get orientation, spacing and origin from somewhere
+    ijkToRAS = vtk.vtkMatrix4x4()
+    
+    for i in range(0,3):
+       for j in range(0,3):
+           ijkToRAS.SetElement(i,j,spacing[i]*orientation[3*j+i])
+
+       ijkToRAS.SetElement(i,3,origin[i])
+
+    node.SetIJKToRASMatrix(ijkToRAS)

binární
DICOMtools/vtkInterface.pyc


+ 46 - 5
README.md

@@ -1,3 +1,5 @@
+# Slicer-Labkey extension
+
 This package allows user to connect to a labkey server through a slicer interface.
 
 To use, add path to unpackaged labkeySlicerPythonAPI/labkeySlicerPythonExtension to
@@ -12,8 +14,47 @@ implements the communication with the server and forms HTTP Labkey API commands.
 More on setup can be found at:
 http://www-f9.ijs.si/~studen/blogs/posts/2017/May/31/slicerlabkey-integration/
 
-Enjoy!
-
-Andrej
-
-
+# DICOMtools
+
+A set of tools that provide DICOM loading from a remote, labkey controlled database. 
+Most significant difference from native Slicer DICOM libraries is that 
+no local database is used as the files are tracked by LabKey database, and that
+files do not neccesarily exist on local storage. 
+
+## Loading DICOM files
+In particular, functions like load(self,net,path) are implemented where 
+net referrs to the server setup (slicerNetwork instance of SlicerLabKeyPythonAPI) and 
+path is a relative path to a remote file. Two implementations were made, 
+- importDicom that uses pydicom. Use Connection button (blue frame) to specify connections, 
+add path on a specified LabKey site text box (cyan), provide filter as a JSON 
+object in a text file (gold) and a button to load the remote DICOM directory as a node (red).
+Multiple nodes will be created if multiple DICOM series are in the target directory.
+To select, use filter window and specify seriesUUID or similar tags. Minimal settings require a single
+field with the content `SeriesLabel` and a key which should be a valid DICOM tag name
+with the first letter in small case.
+![importDICOM](DICOMtools/Resources/Screenshots/importDICOM.png)
+
+- loadDicom that is based on DICOMLib and Slicer embedded DCMTK tools. Using SlicerRT, 
+the program is able to load contours from DICOM-RT files, started by pushing on the
+pink button. The green framed button loads mixed content of volumes and segmentations.
+![loadDICOM](DICOMtools/Resources/Screenshots/loadDICOM.png)
+
+
+## Uploading DICOM files
+In particular workflows, vtkMRMLVolumeNodes created through data manipulation must be 
+stored remotely in DICOM format for validation in other image software tools (like review by 
+clinicans at dedicated work-stations). `exportDicom` has a function exportNode, which
+uploads a named node to a DICOM directory in LabKey, using unique UUID generated
+from baseUUID that has to be provided by the user. Use services such as [FreeDICOM][] to
+generate your own unique UUID.
+
+![exportDICOM](DICOMtools/Resources/Screenshots/exportDICOM.png)
+
+
+The extension consists of four classes, mostly API with minimal GUI:
+- importDicom, a set of tools roughly based on DICOMLib. 
+- loadDicom, similar to above, but resorting to DCMTK tools wrapped by Slicer
+- exportDicom, utility to generate DICOM files with configurable content
+- vtkInterface, a numpy to vtk converter
+- 
+[FreeDICOM]: "https://www.medicalconnections.co.uk/FreeUID/"