import json
import numpy as np
import cerr.contour.rasterseg as rs
from cerr import plan_container as pc
from cerr.radiomics import texture_filters
from cerr.radiomics.preprocess import preProcessForRadiomics
from cerr.utils.mask import computeBoundingBox
[docs]
def loadSettingsFromFile(settingsFile, scanNum=None, planC=None):
"""
Function to load filter parameters from user-input JSON.
Args:
settingsFile (string): Path to JSON file.
scanNum (int): [optional, default=None] Scan no. from which to extract additional
parameters like voxel size.
planC (plan_container.planC): [optional, default=None] pyCERR's plan container object.
Returns:
paramS (dict): Radiomics parameters parsed from JSON file.
filterTypes (list): Texture filters specified in JSON file.
"""
# Read settings
with open(settingsFile) as json_file:
paramS = json.load(json_file)
# Copy voxel dimensions and padding settings to filter parameter dictionary
filterTypes = list(paramS['imageType'].keys())
if scanNum is not None:
voxelSizemmV = planC.scan[scanNum].getScanSpacing() * 10
for n in range(len(filterTypes)):
paramS['imageType'][filterTypes[n]]['VoxelSize_mm'] = voxelSizemmV
if 'padding' in paramS['settings'].keys():
paramS['imageType'][filterTypes[n]]['padding'] = paramS['settings']['padding'][0]
return paramS, filterTypes
[docs]
def processImage(filterType, scan3M, mask3M, paramS):
"""
Function to process scan using selected filter and parameters
Args:
filterType (string): Name of supported filter.
scan3M (np.ndarray): 3D scan.
mask3M (np.ndarray(dtype=bool)): 3D binary mask.
paramS (dict): Parameters (read from JSON).
Returns:
outS(dict): Containing response maps for each filter type.
"""
filterType = filterType.strip().lower()
scan3M = scan3M.astype(float)
outS = dict()
if filterType == 'original':
# Do nothing
outS['original'] = scan3M
elif filterType == 'mean':
absFlag = False
kernelSize = np.array(paramS['KernelSize'])
if 'Absolute' in paramS.keys():
absFlag = paramS['Absolute'].lower() == 'yes'
mean3M = texture_filters.meanFilter(scan3M, kernelSize, absFlag)
outS['mean'] = mean3M
elif filterType == 'sobel':
mag3M, dir3M = texture_filters.sobelFilter(scan3M)
outS['SobelMag'] = mag3M
outS['SobelDir'] = dir3M
elif filterType == 'log':
sigmaV = paramS['Sigma_mm']
cutOffV = np.array(paramS['CutOff_mm'])
voxelSizeV = np.array(paramS['VoxelSize_mm'])
LoG3M = texture_filters.LoGFilter(scan3M, sigmaV, cutOffV, voxelSizeV)
outS['LoG'] = LoG3M
elif filterType in ['gabor', 'gabor3d']:
voxelSizV = np.array(paramS['VoxelSize_mm'])
sigma = paramS['Sigma_mm'] / voxelSizV[0]
wavelength = paramS['Wavlength_mm'] / voxelSizV[0]
thetaV = np.array(paramS['Orientation'])
gamma = paramS['SpatialAspectRatio']
radius = None
paddingV = None
if 'Radius_mm' in paramS.keys():
radius = np.array([paramS['Radius_mm'],paramS['Radius_mm']])
if 'Padding' in paramS.keys():
paddingV = paramS['Padding']['Size']
if filterType == 'gabor':
if 'OrientationAggregation' in paramS.keys():
aggS = {'OrientationAggregation': paramS['OrientationAggregation']}
outS, __ = texture_filters.gaborFilter(scan3M, sigma, wavelength, gamma, thetaV, aggS, radius, paddingV)
else:
outS, __ = texture_filters.gaborFilter(scan3M, sigma, wavelength, gamma, thetaV, radius, paddingV)
elif filterType == 'gabor3d':
aggS = {'PlaneAggregation': paramS['PlaneAggregation']}
if 'OrientationAggregation' in paramS.keys():
aggS['OrientationAggregation'] = paramS['OrientationAggregation']
outS, __ = texture_filters.gaborFilter3d(scan3M, sigma, wavelength, gamma, thetaV, aggS, radius, paddingV)
elif filterType in ['laws', 'rotationinvariantlaws']:
direction = paramS['Direction']
type = paramS['Type']
normFlag = 'false'
if 'Normalize' in paramS.keys():
normFlag = paramS['Normalize'].lower()=='yes'
if filterType == 'laws':
outS = texture_filters.lawsFilter(scan3M, direction, type, normFlag)
elif filterType == 'rotationinvariantlaws':
rotS = paramS['RotationInvariance']
out3M = texture_filters.rotationInvariantLawsFilter(scan3M, direction, type, normFlag, rotS)
outS[type] = out3M
elif filterType in ['lawsenergy', 'rotationinvariantlawsenergy']:
direction = paramS['Direction']
type = paramS['Type']
normFlag = 0
lawsPadSizeV = np.array([0, 0, 0])
energyKernelSizeV = paramS['EnergyKernelSize']
energyPadSizeV = paramS['EnergyPadSize']
energyPadMethod = paramS['EnergyPadMethod']
if 'Normalize' in paramS.keys():
normFlag = paramS['Normalize']
if 'Padding' in paramS.keys():
lawsPadFlag = paramS['Padding']['Flag']
lawsPadSizeV = paramS['Padding']['Size']
lawsPadMethod = paramS['Padding']['Method']
if filterType == 'lawsenergy':
outS = texture_filters.lawsEnergyFilter(scan3M, mask3M, direction, type, normFlag, lawsPadFlag, lawsPadSizeV, \
lawsPadMethod, energyKernelSizeV, energyPadSizeV, energyPadMethod)
elif filterType == 'rotationinvariantlawsenergy':
rotS = paramS['RotationInvariance']
out3M = texture_filters.rotationInvariantLawsEnergyFilter(scan3M, mask3M, direction, type, normFlag, \
lawsPadFlag, lawsPadSizeV, lawsPadMethod, \
energyKernelSizeV, energyPadSizeV, \
energyPadMethod, rotS)
outS[type + '_Energy'] = out3M
# elif filterType in ['wavelets', 'rotationinvariantwavelets']:
#
# waveType = paramS['Wavelets']
# direction = paramS['Direction']
# level = 1 # Default
# if 'level' in paramS.keys():
# level = paramS['Level']
# if 'Index' in paramS and paramS['Index'] is not None:
# waveType += str(paramS['Index'])
# if filterType == 'rotationInvariantWaveletFilter':
# outS = waveletFilter(scan3M, waveType, direction, level)
# # elif filterType == 'rotationInvariantWavelets':
else:
raise Exception('Unknown filter name ' + filterType)
return outS
[docs]
def generateTextureMapFromPlanC(planC, scanNum, strNum, configFilePath):
"""
Function to filter scan and import result to planC.
Args:
planC: pyCERR's plan container object.
scanNum: int for index of scan to be filtered.
strNum: int for index of ROI.
configFilePath: string for path to JSON config file with filter parameters.
Returns:
planC (plan_container.planC): pyCERR plan_container object with texture map as pseudo-scan.
"""
# Extract scan and mask
if isinstance(strNum, np.ndarray) and scanNum is not None:
mask3M = strNum
_, _, slicesV = np.where(mask3M)
uniqueSlicesV = np.unique(slicesV)
strName = 'ROI'
else:
scanNum = planC.structure[strNum].getStructureAssociatedScan(planC)
scan3M = planC.scan[scanNum].getScanArray()
origSizeV = scan3M.shape
mask3M = np.zeros(origSizeV, dtype=bool)
rasterSegM = planC.structure[strNum].rasterSegments
slcMask3M, uniqueSlicesV = rs.raster_to_mask(rasterSegM, scanNum, planC)
mask3M[:, :, uniqueSlicesV] = slcMask3M
strName = planC.structure[strNum].structureName
# Read config file
paramS, __ = loadSettingsFromFile(configFilePath)
# Apply preprocessing
procScan3M, procMask3M, morphmask3M, gridS, __, __ = preProcessForRadiomics(scanNum, strNum, paramS, planC)
minr, maxr, minc, maxc, mins, maxs, __ = computeBoundingBox(procMask3M)
# Extract settings to reverse preprocessing transformations
padFlag = False
padSizeV = [0,0,0]
padMethod = "none"
if 'padding' in paramS["settings"] and paramS["settings"]["padding"]["method"].lower()!='none':
padSizeV = paramS["settings"]["padding"]["size"]
padMethod = paramS["settings"]["padding"]["method"]
padFlag = True
# Apply filter(s)
filterTypes = list(paramS['imageType'].keys())
for filterType in filterTypes:
# Read filter parameters
filtParamS = paramS["imageType"][filterType]
if not isinstance(filtParamS,list):
filtParamS = [filtParamS]
for numPar in range(len(filtParamS)): # Loop over different settings for a filter
voxSizeV = gridS["PixelSpacingV"]
currFiltParamS = filtParamS[numPar]
currFiltParamS["VoxelSize_mm"] = voxSizeV * 10
currFiltParamS["Padding"] = {"Size":padSizeV,"Method": padMethod, "Flag": padFlag}
# Filter scan
outS = processImage(filterType, procScan3M, procMask3M, currFiltParamS)
fieldnames = list(outS.keys())
for nOut in range(len(fieldnames)):
filtScan3M = outS[fieldnames[nOut]]
texSizeV = filtScan3M.shape
# Remove padding
if currFiltParamS["Padding"]["Method"].lower()=='expand':
validPadSizeV = [
min(padSizeV[0], minr),
min(padSizeV[0], procMask3M.shape[0] - maxr),
min(padSizeV[1], minc),
min(padSizeV[1], procMask3M.shape[1] - maxc),
min(padSizeV[2], mins),
min(padSizeV[2], procMask3M.shape[2] - maxs)
]
else:
validPadSizeV = [padSizeV[0],padSizeV[0],padSizeV[1],padSizeV[1],\
padSizeV[2],padSizeV[2]]
filtScan3M = filtScan3M[validPadSizeV[0]:texSizeV[0] - validPadSizeV[1],
validPadSizeV[2]:texSizeV[1] - validPadSizeV[3],
validPadSizeV[4]:texSizeV[2] - validPadSizeV[5]]
filtMask3M = procMask3M[validPadSizeV[0]:texSizeV[0] - validPadSizeV[1],
validPadSizeV[2]:texSizeV[1] - validPadSizeV[3],
validPadSizeV[4]:texSizeV[2] - validPadSizeV[5]]
# Add filter response map to planC
xV = gridS['xValsV']
yV = gridS['yValsV']
zV = gridS['zValsV']
yV = yV[validPadSizeV[0]:texSizeV[0] - validPadSizeV[1]]
xV = xV[validPadSizeV[2]:texSizeV[1] - validPadSizeV[3]]
zV = zV[validPadSizeV[4]:texSizeV[2] - validPadSizeV[5]]
planC = pc.importScanArray(filtScan3M, xV, yV, zV, filterType, scanNum, planC)
#assocScanNum = len(planC.scan)-1
#assocStrName = 'processed_' + strName
#strNum = None
#planC = pc.import_structure_mask(filtMask3M.astype(int), assocScanNum, assocStrName, strNum, planC)
return planC