--- a +++ b/CLI/CancellousAnalysis/CancellousAnalysis.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python-real + +import sys +import os +from datetime import date + +import numpy as np + +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +from MusculoskeletalAnalysisCLITools import ( + crop, + bWshape, + densityMap, + findSpheres, + readImg, + readMask, + updateVertices, + writeReport, +) + + +OUTPUT_FIELDS = [ + ("Date Analysis Performed", "The current date"), + ("Input Volume", "Name of the input volume"), + ("Total Volume (mm^3)", "The volume of the segmented area"), + ("Bone Volume (mm^3)", "The volume of cancellous bone in the segmented area, calculated using marching cubes"), + ("Bone Volume/Total Volume", "The fraction of the volume that is bone"), + ("Mean Trabecular Thickness (mm)", "The mean thickess of the bone, measured using largest sphere thickness, in milimeters"), + ("Trabecular Thickness Standard Deviation (mm)", "The standard deviation of the mean trabecular thickness"), + ("Mean Trabecular Spacing (mm)", "The mean thickness of the non area not containing bone in milimeters, measured using the same method as bone thickness."), + ("Trabecular Spacing Standard Deviation (mm)", "The standard deviation of the mean trabecular spacing"), + ("Trabecular Number", "Approximated as inverse of trabecular spacing"), + ("Structure Model Index", "A measurement of the trabecular shape. 0 is a plate, 3 is a rod, 4 is a sphere"), + ("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"), + ("Tissue Mineral Density(mgHA/cm^3)", "The mean density of the bone, measured in miligrams of hydroxyapatite per cubic centimeter"), + ("Voxel Dimension (mm)", "The side length of one voxel, measured in milimeters"), + ("Lower Threshold", "The lower threshold value for bone"), + ("Upper Threshold", "The upper threshold value for bone"), +] + + +# Performs analysis on cancellous bone +# image: 3D image black and white of bone +# mask: 3D labelmap of bone area, including cavities +# threshold: The threshold for bone in the image. Any pixels with a higher value will be considered bone. +# voxSize: The physical side length of the voxels, in mm +# slope, intercept and scale: parameters for density conversion +# output: The name of the output directory +def main(inputImg, inputMask, lower, upper, voxSize, slope, intercept, name, output): + from skimage import measure + + imgData = readImg(inputImg) + (_, maskData) = readMask(inputMask) + + (maskData, imgData) = crop(maskData, imgData) + if np.count_nonzero(maskData) == 0: + raise Exception("Segmentation mask is empty.") + trabecular = (imgData > lower) & (imgData <= upper) + # Create mesh using marching squares and calculate its volume + boneMesh = bWshape(imgData, lower) + boneVolume=boneMesh.volume * voxSize**3 + # Find volume of entire mask by counting voxels + totalVolume = np.count_nonzero(maskData) * voxSize**3 + bvtv = boneVolume/totalVolume + print("""<filter-progress>{}</filter-progress>""".format(.20)) + sys.stdout.flush() + background = np.bitwise_and(maskData, np.invert(trabecular)) + # Get the thickness map and calculate the thickness + rads = findSpheres(trabecular) + diams = rads * 2 * voxSize + thickness = np.mean(diams) + thicknessStd = np.std(diams) + print("""<filter-progress>{}</filter-progress>""".format(.40)) + sys.stdout.flush() + # Get the thickness map for the background + rads = findSpheres(background) + diams = rads * 2 * voxSize + spacing = np.mean(diams) + spacingStd = np.std(diams) + trabecularNum = 1/spacing + print("""<filter-progress>{}</filter-progress>""".format(.60)) + sys.stdout.flush() + # SMI + # 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 + # Measures the characteristics of the shape; 0 for plate-like, 3 for rod-like, 4 for sphere-like + # Surface area + bS = measure.mesh_surface_area(boneMesh.vertices, boneMesh.faces)*(voxSize**2) + # Arbitrary small value. Reducing size increases accuracy, but risks being rounded to zero + dr = 0.000001 + # 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 + newVert=np.add(boneMesh.vertices, boneMesh.vertex_normals*dr) + boneMesh = updateVertices(boneMesh, newVert) + dS = (measure.mesh_surface_area(boneMesh.vertices, boneMesh.faces)*(voxSize**2))-bS + # Convert to mm + dr = dr*voxSize + # Apply the SMI formula + SMI=(6*boneVolume*(dS/dr)/(bS**2)) + print("""<filter-progress>{}</filter-progress>""".format(.80)) + sys.stdout.flush() + # Calculate density + density = densityMap(imgData, slope, intercept) + tmd = np.mean(density[trabecular]) + + # Connnectivity + # Remove disconected holes and islands + labelmap=measure.label(np.invert(trabecular), connectivity=1) + largest=np.argmax(np.bincount(labelmap[np.nonzero(labelmap)])) + trabecular=labelmap!=largest + # Holes use face connectivity, switch to vertex connectivity for islands + labelmap=measure.label(trabecular, connectivity=3) + largest=np.argmax(np.bincount(labelmap[np.nonzero(labelmap)])) + trabecular=labelmap==largest + + # Find the euler characteristic + # roughly equal to 2-2*number of holes + phi = measure.euler_number(trabecular, connectivity=3) + # connD gives an approximate measure of holes/connections per volume + connD = (1-phi)/totalVolume + + + + + fPath = os.path.join(output, "cancellous.txt") + + header = [field[0] for field in OUTPUT_FIELDS] + + data = [ + date.today(), + name, + totalVolume, + boneVolume, + bvtv, + thickness, + thicknessStd, + spacing, + spacingStd, + trabecularNum, + SMI, + connD, + tmd, + voxSize, + lower, + upper + ] + + writeReport(fPath, header, data) + +if __name__ == "__main__": + if len(sys.argv) < 10: + print(sys.argv) + print(len(sys.argv)) + print("Usage: CancellousAnalysis <input> <mask> <lowerThreshold> <upperThreshold> <voxelSize> <slope> <intercept> <name> <output>") + sys.exit(1) + + 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]))