Switch to unified view

a b/CLI/CancellousAnalysis/CancellousAnalysis.py
1
#!/usr/bin/env python-real
2
3
import sys
4
import os
5
from datetime import date
6
7
import numpy as np
8
9
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
10
from MusculoskeletalAnalysisCLITools import (
11
  crop,
12
  bWshape,
13
  densityMap,
14
  findSpheres,
15
  readImg,
16
  readMask,
17
  updateVertices,
18
  writeReport,
19
)
20
21
22
OUTPUT_FIELDS = [
23
    ("Date Analysis Performed", "The current date"),
24
    ("Input Volume", "Name of the input volume"),
25
    ("Total Volume (mm^3)", "The volume of the segmented area"),
26
    ("Bone Volume (mm^3)", "The volume of cancellous bone in the segmented area, calculated using marching cubes"),
27
    ("Bone Volume/Total Volume", "The fraction of the volume that is bone"),
28
    ("Mean Trabecular Thickness (mm)", "The mean thickess of the bone, measured using largest sphere thickness, in milimeters"),
29
    ("Trabecular Thickness Standard Deviation (mm)", "The standard deviation of the mean trabecular thickness"),
30
    ("Mean Trabecular Spacing (mm)", "The mean thickness of the non area not containing bone in milimeters, measured using the same method as bone thickness."),
31
    ("Trabecular Spacing Standard Deviation (mm)", "The standard deviation of the mean trabecular spacing"),
32
    ("Trabecular Number", "Approximated as inverse of trabecular spacing"),
33
    ("Structure Model Index", "A measurement of the trabecular shape. 0 is a plate, 3 is a rod, 4 is a sphere"),
34
    ("Connectivity Density", "A measurement of the number of connections per volume, based on the Euler characteristic of the bone after removing isolated components and holes"),
35
    ("Tissue Mineral Density(mgHA/cm^3)", "The mean density of the bone, measured in miligrams of hydroxyapatite per cubic centimeter"),
36
    ("Voxel Dimension (mm)", "The side length of one voxel, measured in milimeters"),
37
    ("Lower Threshold", "The lower threshold value for bone"),
38
    ("Upper Threshold", "The upper threshold value for bone"),
39
]
40
41
42
# Performs analysis on cancellous bone
43
# image: 3D image black and white of bone
44
# mask: 3D labelmap of bone area, including cavities
45
# threshold: The threshold for bone in the image. Any pixels with a higher value will be considered bone.
46
# voxSize: The physical side length of the voxels, in mm
47
# slope, intercept and scale: parameters for density conversion
48
# output: The name of the output directory
49
def main(inputImg, inputMask, lower, upper, voxSize, slope, intercept, name, output):
50
    from skimage import measure
51
52
    imgData = readImg(inputImg)
53
    (_, maskData) = readMask(inputMask)
54
    
55
    (maskData, imgData) = crop(maskData, imgData)
56
    if np.count_nonzero(maskData) == 0:
57
         raise Exception("Segmentation mask is empty.")
58
    trabecular = (imgData > lower) & (imgData <= upper)
59
    # Create mesh using marching squares and calculate its volume
60
    boneMesh = bWshape(imgData, lower)
61
    boneVolume=boneMesh.volume * voxSize**3
62
    # Find volume of entire mask by counting voxels
63
    totalVolume = np.count_nonzero(maskData) * voxSize**3
64
    bvtv = boneVolume/totalVolume
65
    print("""<filter-progress>{}</filter-progress>""".format(.20))
66
    sys.stdout.flush()
67
    background = np.bitwise_and(maskData, np.invert(trabecular))
68
    # Get the thickness map and calculate the thickness
69
    rads = findSpheres(trabecular)
70
    diams = rads * 2 * voxSize
71
    thickness = np.mean(diams)
72
    thicknessStd = np.std(diams)
73
    print("""<filter-progress>{}</filter-progress>""".format(.40))
74
    sys.stdout.flush()
75
    # Get the thickness map for the background
76
    rads = findSpheres(background)
77
    diams = rads * 2 * voxSize
78
    spacing = np.mean(diams)
79
    spacingStd = np.std(diams)
80
    trabecularNum = 1/spacing
81
    print("""<filter-progress>{}</filter-progress>""".format(.60))
82
    sys.stdout.flush()
83
    # SMI
84
    # Calculated by finding a 3d mesh, expanding the vertices by a small amount, and calculate a value based on the relative difference in surface area
85
    # Measures the characteristics of the shape; 0 for plate-like, 3 for rod-like, 4 for sphere-like
86
    # Surface area
87
    bS = measure.mesh_surface_area(boneMesh.vertices, boneMesh.faces)*(voxSize**2)
88
    # Arbitrary small value. Reducing size increases accuracy, but risks being rounded to zero
89
    dr = 0.000001
90
    # Creates a new mesh by moving each vertex a small distance in the direction of its vertex normal, then find the difference in surface area
91
    newVert=np.add(boneMesh.vertices, boneMesh.vertex_normals*dr)
92
    boneMesh = updateVertices(boneMesh, newVert)
93
    dS = (measure.mesh_surface_area(boneMesh.vertices, boneMesh.faces)*(voxSize**2))-bS
94
    # Convert to mm
95
    dr = dr*voxSize
96
    # Apply the SMI formula
97
    SMI=(6*boneVolume*(dS/dr)/(bS**2))
98
    print("""<filter-progress>{}</filter-progress>""".format(.80))
99
    sys.stdout.flush()
100
    # Calculate density
101
    density = densityMap(imgData, slope, intercept)
102
    tmd = np.mean(density[trabecular])
103
104
    # Connnectivity
105
    # Remove disconected holes and islands
106
    labelmap=measure.label(np.invert(trabecular), connectivity=1)
107
    largest=np.argmax(np.bincount(labelmap[np.nonzero(labelmap)]))
108
    trabecular=labelmap!=largest
109
    # Holes use face connectivity, switch to vertex connectivity for islands
110
    labelmap=measure.label(trabecular, connectivity=3)
111
    largest=np.argmax(np.bincount(labelmap[np.nonzero(labelmap)]))
112
    trabecular=labelmap==largest
113
114
    # Find the euler characteristic
115
    # roughly equal to 2-2*number of holes
116
    phi = measure.euler_number(trabecular, connectivity=3)
117
    # connD gives an approximate measure of holes/connections per volume
118
    connD = (1-phi)/totalVolume
119
120
121
122
123
    fPath = os.path.join(output, "cancellous.txt")
124
125
    header = [field[0] for field in OUTPUT_FIELDS]
126
127
    data = [
128
        date.today(),
129
        name,
130
        totalVolume,
131
        boneVolume,
132
        bvtv,
133
        thickness,
134
        thicknessStd,
135
        spacing,
136
        spacingStd,
137
        trabecularNum,
138
        SMI,
139
        connD,
140
        tmd,
141
        voxSize,
142
        lower,
143
        upper
144
    ]
145
146
    writeReport(fPath, header, data)
147
148
if __name__ == "__main__":
149
    if len(sys.argv) < 10:
150
        print(sys.argv)
151
        print(len(sys.argv))
152
        print("Usage: CancellousAnalysis <input> <mask> <lowerThreshold> <upperThreshold> <voxelSize> <slope> <intercept> <name> <output>")
153
        sys.exit(1)
154
155
    main(sys.argv[1], sys.argv[2], float(sys.argv[3]), float(sys.argv[4]), float(sys.argv[5]), float(sys.argv[6]), float(sys.argv[7]), str(sys.argv[8]), str(sys.argv[9]))