Skip to content

Commit 8ea85a3

Browse files
author
pinter
committed
ENH: Separated dicom image sorter util into DICOMUtils
Also changed the function to use numpy instead of custom math utils git-svn-id: http://svn.slicer.org/Slicer4/trunk@26889 3bd1e089-480b-0410-8dfb-8563597acbee
1 parent ed8eadb commit 8ea85a3

File tree

2 files changed

+107
-123
lines changed

2 files changed

+107
-123
lines changed

Modules/Scripted/DICOMLib/DICOMUtils.py

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,3 +385,108 @@ def __enter__(self):
385385

386386
def __exit__(self, type, value, traceback):
387387
pass
388+
389+
#------------------------------------------------------------------------------
390+
# TODO: more consistency checks:
391+
# - is there gantry tilt?
392+
# - are the orientations the same for all slices?
393+
def getSortedImageFiles(filePaths, epsilon=0.01):
394+
""" Sort DICOM image files in increasing slice order (IS direction) corresponding to a series
395+
396+
Use the first file to get the ImageOrientationPatient for the
397+
series and calculate the scan direction (assumed to be perpendicular
398+
to the acquisition plane)
399+
400+
epsilon: Maximum difference in distance between slices to consider spacing uniform
401+
"""
402+
if len(filePaths) == 0:
403+
return [],[]
404+
405+
# Define DICOM tags used in this function
406+
tags = {}
407+
tags['position'] = "0020,0032"
408+
tags['orientation'] = "0020,0037"
409+
tags['numberOfFrames'] = "0028,0008"
410+
411+
warningText = ''
412+
if slicer.dicomDatabase.fileValue(filePaths[0], tags['numberOfFrames']) != "":
413+
warningText += "Multi-frame image. If slice orientation or spacing is non-uniform then the image may be displayed incorrectly. Use with caution.\n"
414+
415+
# Make sure first file contains valid geometry
416+
ref = {}
417+
for tag in [tags['position'], tags['orientation']]:
418+
value = slicer.dicomDatabase.fileValue(filePaths[0], tag)
419+
if not value or value == "":
420+
logging.error("Reference image does not contain geometry information in series " + str(seriesUID))
421+
return [],[]
422+
ref[tag] = value
423+
424+
# Determine out-of-plane direction for first slice
425+
import numpy as np
426+
sliceAxes = [float(zz) for zz in ref[tags['orientation']].split('\\')]
427+
x = np.array(sliceAxes[:3])
428+
y = np.array(sliceAxes[3:])
429+
scanAxis = np.cross(x,y)
430+
scanOrigin = np.array([float(zz) for zz in ref[tags['position']].split('\\')])
431+
432+
# For each file in series, calculate the distance along the scan axis, sort files by this
433+
sortList = []
434+
missingGeometry = False
435+
for file in filePaths:
436+
positionStr = slicer.dicomDatabase.fileValue(file,tags['position'])
437+
orientationStr = slicer.dicomDatabase.fileValue(file,tags['orientation'])
438+
if not positionStr or positionStr == "" or not orientationStr or orientationStr == "":
439+
missingGeometry = True
440+
break
441+
position = np.array([float(zz) for zz in positionStr.split('\\')])
442+
vec = position - scanOrigin
443+
dist = vec.dot(scanAxis)
444+
sortList.append((file, dist))
445+
446+
if missingGeometry:
447+
logging.error("One or more images is missing geometry information in series " + str(seriesUID))
448+
return [],[]
449+
450+
# Sort files names by distance from reference slice
451+
sortedFiles = sorted(sortList, key=lambda x: x[1])
452+
files = []
453+
distances = {}
454+
for file,dist in sortedFiles:
455+
files.append(file)
456+
distances[file] = dist
457+
458+
# Get acquisition geometry regularization setting value
459+
settings = qt.QSettings()
460+
acquisitionGeometryRegularizationEnabled = (settings.value("DICOM/ScalarVolume/AcquisitionGeometryRegularization", "default") == "transform")
461+
462+
# Confirm equal spacing between slices
463+
# - use variable 'epsilon' to determine the tolerance
464+
spaceWarnings = 0
465+
if len(files) > 1:
466+
file0 = files[0]
467+
file1 = files[1]
468+
dist0 = distances[file0]
469+
dist1 = distances[file1]
470+
spacing0 = dist1 - dist0
471+
n = 1
472+
for fileN in files[1:]:
473+
fileNminus1 = files[n-1]
474+
distN = distances[fileN]
475+
distNminus1 = distances[fileNminus1]
476+
spacingN = distN - distNminus1
477+
spaceError = spacingN - spacing0
478+
if abs(spaceError) > epsilon:
479+
spaceWarnings += 1
480+
warningText += "Images are not equally spaced (a difference of %g vs %g in spacings was detected)." % (spaceError, spacing0)
481+
if acquisitionGeometryRegularizationEnabled:
482+
warningText += " Slicer will apply a transform to this series trying to regularize the volume. Please use caution.\n"
483+
else:
484+
warningText += (" If loaded image appears distorted, enable 'Acquisition geometry regularization'"
485+
" in Application settins / DICOM / DICOMScalarVolumePlugin. Please use caution.\n")
486+
break
487+
n += 1
488+
489+
if spaceWarnings != 0:
490+
logging.warning("Geometric issues were found with %d of the series. Please use caution.\n" % spaceWarnings)
491+
492+
return files, distances, warningText

Modules/Scripted/DICOMPlugins/DICOMScalarVolumePlugin.py

Lines changed: 2 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import vtk, qt, ctk, slicer, vtkITK
44
from DICOMLib import DICOMPlugin
55
from DICOMLib import DICOMLoadable
6+
from DICOMLib import DICOMUtils
67
from DICOMLib import DICOMExportScalarVolume
78
import logging
89

@@ -160,11 +161,6 @@ def examineFiles(self,files):
160161
loadable.selected = True
161162
# add it to the list of loadables later, if pixel data is available in at least one file
162163

163-
# while looping through files, keep track of their
164-
# position and orientation for later use
165-
positions = {}
166-
orientations = {}
167-
168164
# make subseries volumes based on tag differences
169165
subseriesTags = [
170166
"seriesInstanceUID",
@@ -184,15 +180,6 @@ def examineFiles(self,files):
184180
subseriesFiles = {}
185181
subseriesValues = {}
186182
for file in loadable.files:
187-
188-
# save position and orientation
189-
positions[file] = slicer.dicomDatabase.fileValue(file,self.tags['position'])
190-
if positions[file] == "":
191-
positions[file] = None
192-
orientations[file] = slicer.dicomDatabase.fileValue(file,self.tags['orientation'])
193-
if orientations[file] == "":
194-
orientations[file] = None
195-
196183
# check for subseries values
197184
for tag in subseriesTags:
198185
value = slicer.dicomDatabase.fileValue(file,self.tags[tag])
@@ -250,100 +237,8 @@ def examineFiles(self,files):
250237
# now for each series and subseries, sort the images
251238
# by position and check for consistency
252239
#
253-
254-
# TODO: more consistency checks:
255-
# - is there gantry tilt?
256-
# - are the orientations the same for all slices?
257240
for loadable in loadables:
258-
#
259-
# use the first file to get the ImageOrientationPatient for the
260-
# series and calculate the scan direction (assumed to be perpendicular
261-
# to the acquisition plane)
262-
#
263-
value = slicer.dicomDatabase.fileValue(loadable.files[0], self.tags['numberOfFrames'])
264-
if value != "":
265-
loadable.warning += "Multi-frame image. If slice orientation or spacing is non-uniform then the image may be displayed incorrectly. Use with caution. "
266-
267-
validGeometry = True
268-
ref = {}
269-
for tag in [self.tags['position'], self.tags['orientation']]:
270-
value = slicer.dicomDatabase.fileValue(loadable.files[0], tag)
271-
if not value or value == "":
272-
loadable.warning += "Reference image in series does not contain geometry information. Please use caution. "
273-
validGeometry = False
274-
loadable.confidence = 0.2
275-
break
276-
ref[tag] = value
277-
278-
if not validGeometry:
279-
continue
280-
281-
# get the geometry of the scan
282-
# with respect to an arbitrary slice
283-
sliceAxes = [float(zz) for zz in ref[self.tags['orientation']].split('\\')]
284-
x = sliceAxes[:3]
285-
y = sliceAxes[3:]
286-
scanAxis = self.cross(x,y)
287-
scanOrigin = [float(zz) for zz in ref[self.tags['position']].split('\\')]
288-
289-
acquisitionGeometryRegularizationEnabled = self.acquisitionGeometryRegularizationEnabled()
290-
291-
#
292-
# for each file in series, calculate the distance along
293-
# the scan axis, sort files by this
294-
#
295-
sortList = []
296-
missingGeometry = False
297-
for file in loadable.files:
298-
if not positions[file]:
299-
missingGeometry = True
300-
break
301-
position = [float(zz) for zz in positions[file].split('\\')]
302-
vec = self.difference(position, scanOrigin)
303-
dist = self.dot(vec, scanAxis)
304-
sortList.append((file, dist))
305-
306-
if missingGeometry:
307-
loadable.warning += "One or more images is missing geometry information. "
308-
else:
309-
sortedFiles = sorted(sortList, key=lambda x: x[1])
310-
distances = {}
311-
loadable.files = []
312-
for file,dist in sortedFiles:
313-
loadable.files.append(file)
314-
distances[file] = dist
315-
316-
#
317-
# confirm equal spacing between slices
318-
# - use variable 'epsilon' to determine the tolerance
319-
#
320-
spaceWarnings = 0
321-
if len(loadable.files) > 1:
322-
file0 = loadable.files[0]
323-
file1 = loadable.files[1]
324-
dist0 = distances[file0]
325-
dist1 = distances[file1]
326-
spacing0 = dist1 - dist0
327-
n = 1
328-
for fileN in loadable.files[1:]:
329-
fileNminus1 = loadable.files[n-1]
330-
distN = distances[fileN]
331-
distNminus1 = distances[fileNminus1]
332-
spacingN = distN - distNminus1
333-
spaceError = spacingN - spacing0
334-
if abs(spaceError) > self.epsilon:
335-
spaceWarnings += 1
336-
loadable.warning += "Images are not equally spaced (a difference of %g vs %g in spacings was detected)." % (spaceError, spacing0)
337-
if acquisitionGeometryRegularizationEnabled:
338-
loadable.warning += " Slicer apply a transform to this series trying to regularize the volume. Please use caution. "
339-
else:
340-
loadable.warning += (" If loaded image appears distorted, enable 'Acquisition geometry regularization'"
341-
" in Application settins / DICOM / DICOMScalarVolumePlugin. Please use caution. ")
342-
break
343-
n += 1
344-
345-
if spaceWarnings != 0:
346-
logging.warning("Geometric issues were found with %d of the series. Please use caution." % spaceWarnings)
241+
loadable.files, distances, loadable.warning = DICOMUtils.getSortedImageFiles(loadable.files, self.epsilon)
347242

348243
return loadables
349244

@@ -363,22 +258,6 @@ def seriesSorter(self,x,y):
363258
cmp = xNumber - yNumber
364259
return cmp
365260

366-
#
367-
# math utilities for processing dicom volumes
368-
# TODO: there must be good replacements for these
369-
#
370-
def cross(self, x, y):
371-
return [x[1] * y[2] - x[2] * y[1],
372-
x[2] * y[0] - x[0] * y[2],
373-
x[0] * y[1] - x[1] * y[0]]
374-
375-
def difference(self, x, y):
376-
return [x[0] - y[0], x[1] - y[1], x[2] - y[2]]
377-
378-
def dot(self, x, y):
379-
return x[0] * y[0] + x[1] * y[1] + x[2] * y[2]
380-
381-
382261
#
383262
# different ways to load a set of dicom files:
384263
# - Logic: relies on the same loading mechanism used

0 commit comments

Comments
 (0)