Diff of /evaluation.py [000000] .. [8ff467]

Switch to side-by-side view

--- a
+++ b/evaluation.py
@@ -0,0 +1,192 @@
+# -*- coding: utf-8 -*-
+
+import difflib
+import numpy as np
+import os
+import SimpleITK as sitk
+import scipy.spatial
+
+# Set the path to the source data (e.g. the training data for self-testing)
+# and the output directory of that subject
+testDir        = 'evaluation' # For example: '/input/2'
+participantDir = 'evaluation' # For example: '/output/2'
+
+
+labels = {1: 'Cortical gray matter',
+          2: 'Basal ganglia',
+          3: 'White matter',
+          4: 'White matter lesions',
+          5: 'Cerebrospinal fluid in the extracerebral space',
+          6: 'Ventricles',
+          7: 'Cerebellum',
+          8: 'Brain stem',
+          # The two labels below are ignored:
+          #9: 'Infarction',
+          #10: 'Other',
+          }
+
+
+def do():
+    """Main function"""    
+    resultFilename = getResultFilename(participantDir)  
+        
+    testImage, resultImage = getImages(os.path.join(testDir, 'segm.nii.gz'), resultFilename)
+    
+    dsc = getDSC(testImage, resultImage)
+    h95 = getHausdorff(testImage, resultImage)
+    vs  = getVS(testImage, resultImage)
+    
+    print('Dice',                dsc,       '(higher is better, max=1)')
+    print('HD',                  h95, 'mm',  '(lower is better, min=0)')
+    print('VS',                   vs,       '(higher is better, max=1)')
+    
+    
+    
+def getResultFilename(participantDir):
+    """Find the filename of the result image.
+    
+    This should be result.nii.gz or result.nii. If these files are not present,
+    it tries to find the closest filename."""
+    files = os.listdir(participantDir)
+    
+    if not files:
+        raise Exception("No results in "+ participantDir)
+    
+    resultFilename = None
+    if 'result.nii.gz' in files:
+        resultFilename = os.path.join(participantDir, 'result.nii.gz')
+    elif 'result.nii' in files:
+        resultFilename = os.path.join(participantDir, 'result.nii')
+    else:
+        # Find the filename that is closest to 'result.nii.gz'
+        maxRatio = -1
+        for f in files:
+            currentRatio = difflib.SequenceMatcher(a = f, b = 'result.nii.gz').ratio()
+            
+            if currentRatio > maxRatio:
+                resultFilename = os.path.join(participantDir, f)
+                maxRatio = currentRatio
+                
+    return resultFilename
+    
+
+def getImages(testFilename, resultFilename):
+    """Return the test and result images, thresholded and pathology masked."""
+    testImage   = sitk.ReadImage(testFilename)
+    resultImage = sitk.ReadImage(resultFilename)
+    
+    # Check for equality
+    assert testImage.GetSize() == resultImage.GetSize()
+    
+    # Get meta data from the test-image, needed for some sitk methods that check this
+    resultImage.CopyInformation(testImage)    
+    
+    # Remove pathology from the test and result images, since we don't evaluate on that
+    pathologyImage = sitk.BinaryThreshold(testImage, 9, 11, 0, 1)  # pathology == 9 or 10
+    
+    maskedTestImage   = sitk.Mask(testImage,   pathologyImage)     # tissue    == 1 --  8
+    maskedResultImage = sitk.Mask(resultImage, pathologyImage)
+    
+    # Force integer
+    if not 'integer' in maskedResultImage.GetPixelIDTypeAsString():
+        maskedResultImage = sitk.Cast(maskedResultImage, sitk.sitkUInt8)
+            
+    return maskedTestImage, maskedResultImage
+    
+    
+def getDSC(testImage, resultImage):    
+    """Compute the Dice Similarity Coefficient."""        
+    dsc = dict()
+    for k in labels.keys():
+        testArray   = sitk.GetArrayFromImage(sitk.BinaryThreshold(  testImage, k, k, 1, 0)).flatten()
+        resultArray = sitk.GetArrayFromImage(sitk.BinaryThreshold(resultImage, k, k, 1, 0)).flatten()
+        
+        # similarity = 1.0 - dissimilarity
+        # scipy.spatial.distance.dice raises a ZeroDivisionError if both arrays contain only zeros.
+        try:
+            dsc[k] = 1.0 - scipy.spatial.distance.dice(testArray, resultArray)
+        except ZeroDivisionError:
+            dsc[k] = None
+    
+    return dsc
+
+        
+def getHausdorff(testImage, resultImage):
+    """Compute the 95% Hausdorff distance."""    
+    hd = dict()
+    for k in labels.keys():
+        lTestImage   = sitk.BinaryThreshold(  testImage, k, k, 1, 0)
+        lResultImage = sitk.BinaryThreshold(resultImage, k, k, 1, 0)
+        
+        # Hausdorff distance is only defined when something is detected
+        statistics = sitk.StatisticsImageFilter()
+        statistics.Execute(lTestImage)
+        lTestSum = statistics.GetSum()
+        statistics.Execute(lResultImage)
+        lResultSum = statistics.GetSum()
+        if lTestSum == 0 or lResultSum == 0:
+            hd[k] = None
+            continue
+                                
+        # Edge detection is done by ORIGINAL - ERODED, keeping the outer boundaries of lesions. Erosion is performed in 2D
+        eTestImage   = sitk.BinaryErode(lTestImage, (1,1,0))
+        eResultImage = sitk.BinaryErode(lResultImage, (1,1,0))
+        
+        hTestImage   = sitk.Subtract(lTestImage, eTestImage)
+        hResultImage = sitk.Subtract(lResultImage, eResultImage)    
+        
+        hTestArray   = sitk.GetArrayFromImage(hTestImage)
+        hResultArray = sitk.GetArrayFromImage(hResultImage)   
+            
+        # Convert voxel location to world coordinates. Use the coordinate system of the test image
+        # np.nonzero   = elements of the boundary in numpy order (zyx)
+        # np.flipud    = elements in xyz order
+        # np.transpose = create tuples (x,y,z)
+        # testImage.TransformIndexToPhysicalPoint converts (xyz) to world coordinates (in mm)
+        # (Simple)ITK does not accept all Numpy arrays; therefore we need to convert the coordinate tuples into a Python list before passing them to TransformIndexToPhysicalPoint().
+        testCoordinates   = [testImage.TransformIndexToPhysicalPoint(x.tolist()) for x in np.transpose( np.flipud( np.nonzero(hTestArray) ))]
+        resultCoordinates = [testImage.TransformIndexToPhysicalPoint(x.tolist()) for x in np.transpose( np.flipud( np.nonzero(hResultArray) ))]
+                
+        # Use a kd-tree for fast spatial search
+        def getDistancesFromAtoB(a, b):    
+            kdTree = scipy.spatial.KDTree(a, leafsize=100)
+            return kdTree.query(b, k=1, eps=0, p=2)[0]
+        
+        # Compute distances from test to result and vice versa. 
+        dTestToResult = getDistancesFromAtoB(testCoordinates, resultCoordinates)
+        dResultToTest = getDistancesFromAtoB(resultCoordinates, testCoordinates)
+        hd[k] = max(np.percentile(dTestToResult, 95), np.percentile(dResultToTest, 95))
+        
+    return hd
+
+
+def getVS(testImage, resultImage):   
+    """Volume similarity.
+    
+    VS = 1 - abs(A - B) / (A + B)
+    
+    A = ground truth in ML
+    B = participant segmentation in ML
+    """    
+    # Compute statistics of both images
+    testStatistics   = sitk.StatisticsImageFilter()
+    resultStatistics = sitk.StatisticsImageFilter()
+    
+    vs = dict()
+    for k in labels.keys():
+        testStatistics.Execute(sitk.BinaryThreshold(testImage, k, k, 1, 0))
+        resultStatistics.Execute(sitk.BinaryThreshold(resultImage, k, k, 1, 0))
+        
+        numerator = abs(testStatistics.GetSum() - resultStatistics.GetSum())
+        denominator = testStatistics.GetSum() + resultStatistics.GetSum()               
+        
+        if denominator > 0:        
+            vs[k] = 1 - float(numerator) / denominator
+        else:
+            vs[k] = None
+        
+    return vs
+    
+    
+if __name__ == "__main__":
+    do()