Module ctsimu.image_analysis.mtf
Calculate the MTF from a given ESF or LSF.
Expand source code
# -*- coding: UTF-8 -*-
"""Calculate the MTF from a given ESF or LSF."""
import math
import numpy
from scipy import optimize, fft
def MTF(positions, ESF=None, LSF=None, ESFsmoothingWidth=8):
""" Calculate the MTF for given edge spread function (ESF) or line spread function (LSF) and positions. """
if not(ESF is None):
# Smooth the ESF using a Gaussian-weighted 4th order polynomial fit:
nSamples = len(ESF)
smoothedESF = numpy.zeros(nSamples, dtype=numpy.float64)
w = ESFsmoothingWidth # number of samples for an interpolation fit, in each direction
for pos in range(nSamples):
# Fit to weighted sub-segment:
startIdx = int(max(0, pos-w))
stopIdx = int(min(pos+w+1, nSamples))
centralIdx = int(min(pos, startIdx+w))
fitPositions = positions[startIdx:stopIdx]
fitESF = ESF[startIdx:stopIdx]
minVal = numpy.amin(fitESF)
maxVal = numpy.amax(fitESF)
delta = maxVal-minVal
smoothedValue = 0
#if delta > 0.1: # skip if all elements are (roughly) equal
sigma = numpy.zeros(stopIdx-startIdx, dtype=numpy.float64)
for i in range(stopIdx-startIdx):
g = float(i - (centralIdx - startIdx))
sigma[i] = math.exp(math.pow(2.0*g/w, 2.0))
try:
popt, pcov = optimize.curve_fit(f=poly4, xdata=fitPositions, ydata=fitESF, p0=None, sigma=sigma)
smoothedValue = poly4(x=positions[pos], a=popt[0], b=popt[1], c=popt[2], d=popt[3], e=popt[4])
except Exception:
#print("Exception at pos. {}".format(pos-nSamples/2.0))
smoothedValue = numpy.mean(fitESF)
smoothedESF[pos] = smoothedValue
print("\rSmoothing ESF... {:.1f}%".format(100.0*pos/nSamples), end='')
print("\rSmoothing ESF... 100% ")
print("Calculating LSF and MTF...")
# Differentiate smoothed line to get line spread function (LSF)
diffStepSize = 2.0*(positions[1]-positions[0])
LSF = numpy.zeros(nSamples, dtype=numpy.float64)
for pos in range(1, nSamples-1):
LSF[pos] = (smoothedESF[pos+1] - smoothedESF[pos-1]) / diffStepSize
elif not(LSF is None):
nSamples = len(LSF)
smoothedESF = None
else:
raise Exception("MTF: either an ESF or an LSF must be passed.")
# Subtract background in long tails (10 px at the very left and right of the LSF):
background = (numpy.mean(LSF[0:10]) + numpy.mean(LSF[-10:])) / 2.0
if abs(background) > 1e-3:
LSF -= background
print("Subtracted LSF background level: {}".format(background))
# Normalize LSF maximum to 1:
LSFmax = numpy.amax(LSF)
if LSFmax != 0:
LSF /= LSFmax
# Apply a von Hann window before Fourier transform:
hann_window_width = nSamples
hann_window = numpy.hanning(hann_window_width)
LSF_windowed = LSF * hann_window
# Normalize smoothed LSF maximum to 1:
LSFsmoothed_max = numpy.amax(LSF_windowed)
if LSFsmoothed_max != 0:
LSF_windowed /= LSFsmoothed_max
# Pad zeros to nearest full power of 2 (or next-nearest)
power_of_two = math.floor(math.log(nSamples, 2)) + 1
if (2**power_of_two - nSamples) < 20:
# Pad at least 20 zeros or increase power of two:
power_of_two += 1
nMTFsamples = 2**power_of_two
LSF_for_MTF = numpy.zeros(nMTFsamples, dtype=LSF_windowed.dtype)
LSF_for_MTF[0:len(LSF_windowed)] = LSF_windowed
print("{} samples before zero-padding, {} samples after.".format(len(LSF_windowed), len(LSF_for_MTF)))
mtf = numpy.absolute(fft.fft(LSF_for_MTF))
# Normalize value at zero frequency to 1:
if mtf[0] != 0:
mtf = mtf / mtf[0]
stepsize = 1
if len(positions) > 1:
stepsize = positions[1] - positions[0]
# Calculate the Nyquist frequency and frequency position axis:
fnyq = 1.0 / (2.0*stepsize)
mtfpos = numpy.linspace(start=0, stop=2.0*fnyq, num=nMTFsamples, endpoint=False, dtype=numpy.float64)
return smoothedESF, LSF, LSF_windowed, mtf, mtfpos
def MTFfreq(MTFpos, MTF, modulation=0.2):
""" Return the frequency for the requested modulation height. """
mtf_freq = 0
for f in range(len(MTFpos)):
if MTF[f] < modulation:
if f > 0:
# Linear interpolation:
x0 = MTFpos[f-1]
x1 = MTFpos[f]
y0 = MTF[f-1]
y1 = MTF[f]
m = (y1-y0)/(x1-x0)
n = y0 - m*x0
mtf_freq = (modulation-n)/m
else:
mtf_freq = MTFpos[f]
break
return mtf_freq
Functions
def MTF(positions, ESF=None, LSF=None, ESFsmoothingWidth=8)
-
Calculate the MTF for given edge spread function (ESF) or line spread function (LSF) and positions.
Expand source code
def MTF(positions, ESF=None, LSF=None, ESFsmoothingWidth=8): """ Calculate the MTF for given edge spread function (ESF) or line spread function (LSF) and positions. """ if not(ESF is None): # Smooth the ESF using a Gaussian-weighted 4th order polynomial fit: nSamples = len(ESF) smoothedESF = numpy.zeros(nSamples, dtype=numpy.float64) w = ESFsmoothingWidth # number of samples for an interpolation fit, in each direction for pos in range(nSamples): # Fit to weighted sub-segment: startIdx = int(max(0, pos-w)) stopIdx = int(min(pos+w+1, nSamples)) centralIdx = int(min(pos, startIdx+w)) fitPositions = positions[startIdx:stopIdx] fitESF = ESF[startIdx:stopIdx] minVal = numpy.amin(fitESF) maxVal = numpy.amax(fitESF) delta = maxVal-minVal smoothedValue = 0 #if delta > 0.1: # skip if all elements are (roughly) equal sigma = numpy.zeros(stopIdx-startIdx, dtype=numpy.float64) for i in range(stopIdx-startIdx): g = float(i - (centralIdx - startIdx)) sigma[i] = math.exp(math.pow(2.0*g/w, 2.0)) try: popt, pcov = optimize.curve_fit(f=poly4, xdata=fitPositions, ydata=fitESF, p0=None, sigma=sigma) smoothedValue = poly4(x=positions[pos], a=popt[0], b=popt[1], c=popt[2], d=popt[3], e=popt[4]) except Exception: #print("Exception at pos. {}".format(pos-nSamples/2.0)) smoothedValue = numpy.mean(fitESF) smoothedESF[pos] = smoothedValue print("\rSmoothing ESF... {:.1f}%".format(100.0*pos/nSamples), end='') print("\rSmoothing ESF... 100% ") print("Calculating LSF and MTF...") # Differentiate smoothed line to get line spread function (LSF) diffStepSize = 2.0*(positions[1]-positions[0]) LSF = numpy.zeros(nSamples, dtype=numpy.float64) for pos in range(1, nSamples-1): LSF[pos] = (smoothedESF[pos+1] - smoothedESF[pos-1]) / diffStepSize elif not(LSF is None): nSamples = len(LSF) smoothedESF = None else: raise Exception("MTF: either an ESF or an LSF must be passed.") # Subtract background in long tails (10 px at the very left and right of the LSF): background = (numpy.mean(LSF[0:10]) + numpy.mean(LSF[-10:])) / 2.0 if abs(background) > 1e-3: LSF -= background print("Subtracted LSF background level: {}".format(background)) # Normalize LSF maximum to 1: LSFmax = numpy.amax(LSF) if LSFmax != 0: LSF /= LSFmax # Apply a von Hann window before Fourier transform: hann_window_width = nSamples hann_window = numpy.hanning(hann_window_width) LSF_windowed = LSF * hann_window # Normalize smoothed LSF maximum to 1: LSFsmoothed_max = numpy.amax(LSF_windowed) if LSFsmoothed_max != 0: LSF_windowed /= LSFsmoothed_max # Pad zeros to nearest full power of 2 (or next-nearest) power_of_two = math.floor(math.log(nSamples, 2)) + 1 if (2**power_of_two - nSamples) < 20: # Pad at least 20 zeros or increase power of two: power_of_two += 1 nMTFsamples = 2**power_of_two LSF_for_MTF = numpy.zeros(nMTFsamples, dtype=LSF_windowed.dtype) LSF_for_MTF[0:len(LSF_windowed)] = LSF_windowed print("{} samples before zero-padding, {} samples after.".format(len(LSF_windowed), len(LSF_for_MTF))) mtf = numpy.absolute(fft.fft(LSF_for_MTF)) # Normalize value at zero frequency to 1: if mtf[0] != 0: mtf = mtf / mtf[0] stepsize = 1 if len(positions) > 1: stepsize = positions[1] - positions[0] # Calculate the Nyquist frequency and frequency position axis: fnyq = 1.0 / (2.0*stepsize) mtfpos = numpy.linspace(start=0, stop=2.0*fnyq, num=nMTFsamples, endpoint=False, dtype=numpy.float64) return smoothedESF, LSF, LSF_windowed, mtf, mtfpos
def MTFfreq(MTFpos, MTF, modulation=0.2)
-
Return the frequency for the requested modulation height.
Expand source code
def MTFfreq(MTFpos, MTF, modulation=0.2): """ Return the frequency for the requested modulation height. """ mtf_freq = 0 for f in range(len(MTFpos)): if MTF[f] < modulation: if f > 0: # Linear interpolation: x0 = MTFpos[f-1] x1 = MTFpos[f] y0 = MTF[f-1] y1 = MTF[f] m = (y1-y0)/(x1-x0) n = y0 - m*x0 mtf_freq = (modulation-n)/m else: mtf_freq = MTFpos[f] break return mtf_freq