loadDicom.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528
  1. import slicer
  2. import os
  3. import subprocess
  4. import re
  5. import slicerNetwork
  6. import ctk,qt
  7. import json
  8. dicomModify=os.getenv("HOME")
  9. if not dicomModify==None:
  10. dicomModify+="/software/install/"
  11. dicomModify+="dicomModify/bin/dicomModify"
  12. class loadDicom(slicer.ScriptedLoadableModule.ScriptedLoadableModule):
  13. def __init__(self,parent):
  14. slicer.ScriptedLoadableModule.ScriptedLoadableModule.__init__(self, parent)
  15. self.className="loadDicom"
  16. self.parent.title="loadDicom"
  17. self.parent.categories = ["LabKey"]
  18. self.parent.dependencies = []
  19. self.parent.contributors = ["Andrej Studen (UL/FMF)"] # replace with "Firstname Lastname (Organization)"
  20. self.parent.helpText = """
  21. utilities for parsing dicom entries
  22. """
  23. self.parent.acknowledgementText = """
  24. Developed within the medical physics research programme of the Slovenian research agency.
  25. """ # replace with organization, grant and thanks.
  26. class loadDicomWidget(slicer.ScriptedLoadableModule.ScriptedLoadableModuleWidget):
  27. """Uses ScriptedLoadableModuleWidget base class, available at:
  28. https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
  29. """
  30. def setup(self):
  31. slicer.ScriptedLoadableModule.ScriptedLoadableModuleWidget.setup(self)
  32. self.logic=loadDicomLogic(self)
  33. self.network=slicerNetwork.labkeyURIHandler()
  34. connectionCollapsibleButton = ctk.ctkCollapsibleButton()
  35. connectionCollapsibleButton.text = "Connection"
  36. self.layout.addWidget(connectionCollapsibleButton)
  37. connectionFormLayout = qt.QFormLayout(connectionCollapsibleButton)
  38. self.loadConfigButton=qt.QPushButton("Load configuration")
  39. self.loadConfigButton.toolTip="Load configuration"
  40. self.loadConfigButton.connect('clicked(bool)',self.onLoadConfigButtonClicked)
  41. connectionFormLayout.addRow("Connection:",self.loadConfigButton)
  42. self.DICOMDirectory=qt.QLineEdit("Test/Temp/%40files/TEST/MLEM")
  43. connectionFormLayout.addRow("LabKey directory:",self.DICOMDirectory)
  44. loadDICOMButton=qt.QPushButton("Load")
  45. loadDICOMButton.toolTip="Load DICOM"
  46. loadDICOMButton.clicked.connect(self.onLoadDICOMButtonClicked)
  47. connectionFormLayout.addRow("DICOM:",loadDICOMButton)
  48. self.DICOMFilter=qt.QLineEdit('{"seriesNumber":"SeriesLabel"}')
  49. connectionFormLayout.addRow("Filter(JSON):",self.DICOMFilter)
  50. loadDICOMFilterButton=qt.QPushButton("Load with filter")
  51. loadDICOMFilterButton.toolTip="Load DICOM with filter"
  52. loadDICOMFilterButton.clicked.connect(self.onLoadDICOMFilterButtonClicked)
  53. connectionFormLayout.addRow("DICOM:",loadDICOMFilterButton)
  54. loadDICOMSegmentationFilterButton=qt.QPushButton("Load segmentation with filter")
  55. loadDICOMSegmentationFilterButton.toolTip="Load DICOM (RT contour) with filter"
  56. loadDICOMSegmentationFilterButton.clicked.connect(self.onLoadDICOMSegmentationFilterButtonClicked)
  57. connectionFormLayout.addRow("DICOM:",loadDICOMSegmentationFilterButton)
  58. def onLoadConfigButtonClicked(self):
  59. filename=qt.QFileDialog.getOpenFileName(None,'Open configuration file (JSON)',
  60. os.path.join(os.path.expanduser('~'),'.labkey'), '*.json')
  61. self.network.parseConfig(filename)
  62. self.network.initRemote()
  63. self.loadConfigButton.setText(os.path.basename(filename))
  64. def onLoadDICOMFilterButtonClicked(self):
  65. filter=json.loads(self.DICOMFilter.text)
  66. #print("Filter is {}".format(filter))
  67. self.logic.loadVolumes(self.network,self.DICOMDirectory.text,filter)
  68. def onLoadDICOMSegmentationFilterButtonClicked(self):
  69. filter=json.loads(self.DICOMFilter.text)
  70. #print("Filter is {}".format(filter))
  71. self.logic.loadSegmentations(self.network,self.DICOMDirectory.text,filter)
  72. def onLoadDICOMButtonClicked(self):
  73. self.logic.load(self.network,self.DICOMDirectory.text)
  74. class loadDicomLogic(slicer.ScriptedLoadableModule.ScriptedLoadableModuleLogic):
  75. def __init__(self,parent):
  76. slicer.ScriptedLoadableModule.\
  77. ScriptedLoadableModuleLogic.__init__(self, parent)
  78. self.tag={
  79. 'sopInstanceUid' : {'tag':"0008,0018",'VR':'UI'},
  80. 'studyDate': {'tag':"0008,0020",'VR':'DA'},
  81. 'studyTime': {'tag':"0008,0030",'VR':'TM'},
  82. 'seriesTime': {'tag':"0008,0031",'VR':'TM'},
  83. 'acquisitionTime':{'tag':"0008,0032",'VR':'TM'},
  84. 'modality': {'tag':"0008,0060",'VR':'CS'},
  85. 'presentationIntentType': {'tag':"0008,0068",'VR':'CS'},
  86. 'manufacturer': {'tag':"0008,0070",'VR':'LO'},
  87. 'institutionName': {'tag':"0008,0080",'VR':'LO'},
  88. 'studyDescription': {'tag':"0008,1030",'VR':'LO'},
  89. 'seriesDescription': {'tag':"0008,103e",'VR':'LO'},
  90. 'manufacturerModelName': {'tag':"0008,1090",'VR':'LO'},
  91. 'patientName': {'tag':"0010,0010",'VR':'PN'},
  92. 'patientId': {'tag':"0010,0020",'VR':'LO'},
  93. 'patientBirthDate': {'tag':"0010,0030",'VR':'DA'},
  94. 'patientSex': {'tag':"0010,0040",'VR':'CS'},
  95. 'patientAge': {'tag':"0010,1010",'VR':'AS'},
  96. 'patientComments': {'tag':"0010,4000",'VR':'LT'},
  97. 'sequenceName': {'tag':"0018,0024",'VR':'SH'},
  98. 'kVP': {'tag':"0018,0060",'VR':'DS'},
  99. 'percentPhaseFieldOfView': {'tag':"0018,0094",'VR':'DS'},
  100. 'xRayTubeCurrent': {'tag':"0018,1151",'VR':'IS'},
  101. 'exposure': {'tag':"0018,1152",'VR':'IS'},
  102. 'imagerPixelSpacing': {'tag':"0018,1164",'VR':'DS'},
  103. 'anodeTargetMaterial':{'tag':"0018,1191",'VR':'CS'},
  104. 'bodyPartThickness': {'tag':"0018,11a0",'VR':'DS'},
  105. 'compressionForce': {'tag':"0018,11a2",'VR':'DS'},
  106. 'viewPosition': {'tag':"0018,5101",'VR':'CS'},
  107. 'fieldOfViewHorizontalFlip': {'tag':"0018,7034",'VR':'CS'},
  108. 'filterMaterial':{'tag':"0018,7050",'VR':'CS'},
  109. 'studyInstanceUid': {'tag':"0020,000d",'VR':'UI'},
  110. 'seriesInstanceUid': {'tag':"0020,000e",'VR':'UI'},
  111. 'studyId': {'tag':"0020,0010",'VR':'SH'},
  112. 'seriesNumber': {'tag':"0020,0011",'VR':'IS'},
  113. 'instanceNumber': {'tag':"0020,0013",'VR':'IS'},
  114. 'frameOfReferenceInstanceUid': {'tag':"0020,0052",'seqTag':"3006,0010",'VR':'UI'},
  115. 'imageLaterality': {'tag':"0020,0062",'VR':'CS'},
  116. 'imagesInAcquisition': {'tag':"0020,1002",'VR':'IS'},
  117. 'photometricInterpretation': {'tag':"0028,0004",'VR':'CS'},
  118. 'organDose':{'tag':"0040,0316",'VR':'DS'},
  119. 'entranceDoseInmGy':{'tag':"0040,8302",'VR':'DS'},
  120. 'reconstructionMethod': {'tag':"0054,1103",'VR':'LO'}
  121. }
  122. self.tagPyDicom={
  123. 'studyDate': 0x00080020,
  124. 'studyTime': 0x00080030,
  125. 'modality': 0x00080060,
  126. 'presentationIntentType': 0x00080068,
  127. 'manufacturer': 0x00080070,
  128. 'studyDescription': 0x00081030,
  129. 'seriesDescription': 0x0008103e,
  130. 'patientName': 0x00100010,
  131. 'patientId': 0x00100020,
  132. 'patientBirthDate': 0x00100030,
  133. 'patientSex': 0x00100040,
  134. 'patientComments': 0x00104000,
  135. 'sequenceName': 0x00180024,
  136. 'kVP': 0x00180060,
  137. 'percentPhaseFieldOfView': 0x00180094,
  138. 'xRayTubeCurrent': 0x00181151,
  139. 'exposure': 0x00181152,
  140. 'imagerPixelSpacing': 0x00181164,
  141. 'bodyPartThickness': 0x001811a0,
  142. 'compressionForce': 0x001811a2,
  143. 'viewPosition': 0x00185101,
  144. 'studyInstanceUid': 0x0020000d,
  145. 'seriesInstanceUid': 0x0020000e,
  146. 'studyId': 0x00200010,
  147. 'seriesNumber': 0x00200011,
  148. 'instanceNumber': 0x00200013,
  149. 'frameOfReferenceInstanceUid': 0x00200052
  150. }
  151. self.local=False
  152. self.useDicomModify=False
  153. def enableDicomModify(self):
  154. self.useDicomModify=True
  155. def setLocal(self,basePath):
  156. self.local=True
  157. self.basePath=basePath
  158. def getHex(self,key):
  159. #convert string to hex key;
  160. fv=key.split(",")
  161. return int(fv[0],16)*0x10000+int(fv[1],16)
  162. def load(self,sNet,dir,doRemove=True):
  163. #load directory using DICOMLib tools
  164. print("Loading dir {}").format(dir)
  165. dicomFiles=self.listdir(sNet,dir)
  166. filelist=[]
  167. for f in dicomFiles:
  168. localPath=self.getfile(sNet,f)
  169. filelist.append(localPath)
  170. if not self.useDicomModify:
  171. continue
  172. if dicomModify==None:
  173. continue
  174. f0=localPath
  175. f1=f0+"1"
  176. try:
  177. subprocess.call(\
  178. dicomModify+" "+f0+" "+f1+" && mv "+f1+" "+f0+";",
  179. shell=True)
  180. except OSError:
  181. print("dicomModify failed")
  182. try:
  183. loadables=self.volumePlugin.examineForImport([filelist])
  184. except AttributeError:
  185. self.volumePlugin=slicer.modules.dicomPlugins['DICOMScalarVolumePlugin']()
  186. loadables=self.volumePlugin.examineForImport([filelist])
  187. for loadable in loadables:
  188. #check if it makes sense to load a particular loadable
  189. if loadable.name.find('imageOrientationPatient')>-1:
  190. continue
  191. filter={}
  192. filter['seriesNumber']=None
  193. metadata={}
  194. if not self.applyFilter(loadable,filter,metadata):
  195. continue
  196. volumeNode=self.volumePlugin.load(loadable)
  197. if volumeNode != None:
  198. vName='Series'+metadata['seriesNumber']
  199. volumeNode.SetName(vName)
  200. try:
  201. loadableRTs=self.RTPlugin.examineForImport([filelist])
  202. except:
  203. self.RTPlugin=plugin=slicer.modules.dicomPlugins['DicomRtImportExportPlugin']()
  204. loadableRTs=self.RTPlugin.examineForImport([filelist])
  205. for loadable in loadableRTs:
  206. segmentationNode=self.RTPlugin.load(loadable)
  207. if not doRemove:
  208. return
  209. for f in filelist:
  210. self.removeLocal(f)
  211. def applyFilter(self,loadable,filter,nodeMetadata):
  212. #apply filter to loadable.file[0]. Return true if file matches prescribed filter and
  213. #false otherwise
  214. #filter is a directory with keys equal to pre-specified values listed above
  215. #if value associated to key equals None, that value gets set in nodeMetadata
  216. #if value is set, a match is attempted and result reported in return value
  217. #all filters should match for true output
  218. return self.applyFilterFile(loadable.files[0],filter,nodeMetadata)
  219. def applyFilterFile(self,file,filter,nodeMetadata,shell=False,debug=False):
  220. #apply filter to file. Return true if file matches prescribed filter and
  221. #false otherwise
  222. #filter is a directory with keys equal to pre-specified values listed above
  223. #if value associated to key equals None, that value gets set in nodeMetadata
  224. #if value is set, a match is attempted and result reported in return value
  225. #all filters should match for true output
  226. filterOK=True
  227. for key in filter:
  228. try:
  229. fileValue=dicomValue(file,self.tag[key]['tag'],self.tag[key]['seqTag'],shell=shell)
  230. except KeyError:
  231. fileValue=dicomValue(file,self.tag[key]['tag'],shell=shell)
  232. if filter[key]=="SeriesLabel":
  233. nodeMetadata['seriesLabel']=fileValue
  234. continue
  235. if not filter[key]==None:
  236. if not fileValue==filter[key]:
  237. if debug:
  238. print("File {} failed for tag {}: {}/{}").format(\
  239. file,key,fileValue,filter[key])
  240. filterOK=False
  241. break
  242. nodeMetadata[key]=fileValue
  243. return filterOK
  244. def listdir(self,sNet,dir):
  245. #list remote directory
  246. if self.local:
  247. dir1=os.path.join(self.basePath,dir)
  248. dirs=os.listdir(dir1)
  249. return [os.path.join(dir1,f) for f in dirs]
  250. return sNet.listRelativeDir(dir)
  251. def getfile(self,sNet,file):
  252. #get remote file
  253. if self.local:
  254. return file
  255. return sNet.DownloadFileToCache(file)
  256. def removeLocal(self,localFile):
  257. if self.local:
  258. return
  259. os.remove(localFile)
  260. def loadVolumes(self,sNet,dir,filter,doRemove=True):
  261. #returns all series from the directory, each as a separate node in a node list
  262. #filter is a dictionary of speciifed dicom values,
  263. #if filter(key)=None, that value
  264. #get set, if it isn't, the file gets checked for a match
  265. print("Loading dir {}").format(dir)
  266. dicomFiles=self.listdir(sNet,dir)
  267. #filelist=[os.path.join(dir,f) for f in os.listdir(dir)]
  268. filelist=[]
  269. for f in dicomFiles:
  270. localPath=self.getfile(sNet,f)
  271. filelist.append(localPath)
  272. if not self.useDicomModify:
  273. continue
  274. if dicomModify==None:
  275. continue
  276. f0=localPath
  277. f1=f0+"1"
  278. try:
  279. cmd=dicomModify+" "+f0+" "+f1+" && mv "+f1+" "+f0+";"
  280. subprocess.call(cmd, shell=False)
  281. except OSError:
  282. print("dicomModify failed")
  283. try:
  284. loadables=self.volumePlugin.examineForImport([filelist])
  285. except AttributeError:
  286. self.volumePlugin=\
  287. slicer.modules.dicomPlugins['DICOMScalarVolumePlugin']()
  288. loadables=self.volumePlugin.examineForImport([filelist])
  289. volumeNodes=[]
  290. print("Number of loadables:{}").format(len(loadables))
  291. for loadable in loadables:
  292. #TODO check if it makes sense to load a particular loadable
  293. print "{}: Checking number of files: {}".format(\
  294. loadable.name,len(loadable.files))
  295. #perform checks
  296. fileMetadata={}
  297. loadable.files[:]=[f for f in loadable.files \
  298. if self.applyFilterFile(f,filter,fileMetadata)]
  299. if len(loadable.files)<1:
  300. #skip this loadable
  301. continue
  302. print "{}: Final number of files: {}".format(\
  303. loadable.name,len(loadable.files))
  304. nodeMetadata={}
  305. self.applyFilterFile(loadable.files[0],filter,nodeMetadata)
  306. volumeNode=self.volumePlugin.load(loadable,"DCMTK")
  307. if volumeNode != None:
  308. vName='Series'+nodeMetadata['seriesLabel']
  309. volumeNode.SetName(vName)
  310. volume={'node':volumeNode,'metadata':nodeMetadata}
  311. volumeNodes.append(volume)
  312. self.volumePlugin.loadableCache.clear()
  313. if self.local:
  314. return volumeNodes
  315. if doRemove:
  316. for f in filelist:
  317. self.removeLocal(f)
  318. return volumeNodes
  319. def loadSegmentations(self,net,dir,filter,doRemove=True):
  320. print("Loading dir {}").format(dir)
  321. dicomFiles=self.listdir(net,dir)
  322. filelist=[self.getfile(net,f) for f in dicomFiles]
  323. segmentationNodes=[]
  324. try:
  325. loadableRTs=self.RTPlugin.examineForImport([filelist])
  326. except:
  327. self.RTPlugin=plugin=slicer.modules.dicomPlugins['DicomRtImportExportPlugin']()
  328. loadableRTs=self.RTPlugin.examineForImport([filelist])
  329. for loadable in loadableRTs:
  330. nodeMetadata={}
  331. filterOK=self.applyFilter(loadable,filter,nodeMetadata)
  332. if not filterOK:
  333. continue
  334. success=self.RTPlugin.load(loadable)
  335. if not success:
  336. print("Could not load RT structure set")
  337. return
  338. segNodes=slicer.util.getNodesByClass("vtkMRMLSegmentationNode")
  339. segmentationNode=segNodes[0]
  340. #assume we loaded the first node in list
  341. if segmentationNode != None:
  342. sName='Segmentation'+nodeMetadata['seriesLabel']
  343. segmentationNode.SetName(sName)
  344. segmentation={'node':segmentationNode,'metadata':nodeMetadata}
  345. segmentationNodes.append(segmentation)
  346. if not doRemove:
  347. return segmentationNodes
  348. for f in filelist:
  349. self.removeLocal(f)
  350. return segmentationNodes
  351. def isDicom(file):
  352. #check if file is a dicom file
  353. try:
  354. f=open(file,'rb')
  355. except IOError:
  356. return False
  357. f.read(128)
  358. dt=f.read(4)
  359. f.close()
  360. return dt=='DICM'
  361. def dicomValue(fileName,tag,seqTag=None,shell=False):
  362. #query dicom value of file using dcmdump (DCMTK routine)
  363. #shell must be false for *nix
  364. if os.name=="posix":
  365. shell=False
  366. debug=False
  367. dcmdump=os.path.join(os.environ['SLICER_HOME'],"bin","dcmdump")
  368. try:
  369. if debug:
  370. print("Calling {}".format(dcmdump))
  371. out=subprocess.check_output([dcmdump,'+p','+P',tag,fileName],shell=shell)
  372. if debug:
  373. print("Got {}".format(out))
  374. except subprocess.CalledProcessError as e:
  375. return None
  376. if debug:
  377. print("Tag {} Line '{}'").format(tag,out)
  378. if len(out)==0:
  379. return out
  380. #parse multi-match outputs which appear as several lines
  381. lst=out.split('\n')
  382. return getTagValue(lst,tag,seqTag)
  383. def getTagValue(lst,tag,seqTag=None):
  384. #report tag value from a list lst of lines reported by dcmdump
  385. debug=False
  386. #parse output
  387. longTag="^\({}\)".format(tag)
  388. #combine sequence and actual tags to long tags
  389. if not seqTag==None:
  390. if debug:
  391. print("Tag:{} seqTag:{}").format(tag,seqTag)
  392. longTag="^\({}\).\({}\)".format(seqTag,tag)
  393. #pick the values
  394. pattern=r'^.*\[(.*)\].*$'
  395. #extract tag values
  396. rpl=[re.sub(pattern,r'\1',f) for f in lst]
  397. #logical whether the line can be matched to typical dcmdump output
  398. mtchPattern=[re.match(pattern,f) for f in lst]
  399. #find matching tags
  400. mtchTag=[re.match(longTag,f) for f in lst]
  401. #weed out non-matching lines and lines not matching longTag
  402. mtch=[None if x==None or y==None else x \
  403. for x,y in zip(mtchTag,mtchPattern)]
  404. #set values
  405. out=[x for y,x in zip(mtch,rpl) if not y==None]
  406. if len(out)==0:
  407. return ''
  408. #return first match
  409. out=out[0]
  410. if debug:
  411. print("Tag {} Parsed value {}").format(tag,out)
  412. #split output to lists if values are DICOM lists
  413. if out.find('\\')>-1:
  414. out=out.split('\\')
  415. return out
  416. def clearNodes():
  417. nodes=[]
  418. nodes.extend(slicer.util.getNodesByClass("vtkMRMLScalarVolumeNode"))
  419. nodes.extend(slicer.util.getNodesByClass("vtkMRMLScalarVolumeDisplayNode"))
  420. nodes.extend(slicer.util.getNodesByClass("vtkMRMLSegmentationNode"))
  421. nodes.extend(slicer.util.getNodesByClass("vtkMRMLSegmentationDisplayNode"))
  422. for node in nodes:
  423. slicer.mrmlScene.RemoveNode(node)