.. _mtf_topic: Modulation Transfer Function ============================ The **Modulation Transfer Function (MTF)** is a standard way of describing how well an imaging system preserves detail and contrast. It measures the ratio of image contrast to object contrast at different levels of detail, expressed in terms of spatial frequency. In simpler terms, MTF shows how clearly an imaging system can reproduce fine patterns: large features are usually transferred well, while very small details tend to blur and lose contrast. .. _peak-valley-mtf: Peak-Valley MTF --------------- The Peak-Valley MTF is used in CBCT and planar imaging metrics to describe high-contrast characteristics of the imaging system. An excellent introduction is `here `__. In pylinac, Peak-Valley MTF is calculated using equation 3 of the above reference, which is also the :ref:`Michelson ` contrast definition. .. math:: contrast = \frac{I_{max} - I_{min}}{I_{max} + I_{min}} Then, all the contrasts are normalized to the largest one, resulting in a normalized MTF or rMTF (relative). Pylinac only reports rMTF values. This is the first of two inputs. The other is the line pair spacing. The spacing is usually provided by the phantom manufacturer. The rMTF is the plotted against the line pair/mm values. Also from this data the MTF at a certain percentage (e.g. 50%) can be determined in units of lp/mm. However, it's important to know what :math:`I_{max}` and :math:`I_{min}` means here. For a line pair set, each bar and space-between is one contrast value. Thus, one contrast value is calculated for each bar/space combo. For phantoms with areas of the same spacing (e.g. the Leeds), all bars and spaces are the same and thus we can use an area-based ROI for the input to the contrast equation. .. _ESF_MTF: ESF-based MTF ------------- Logic ~~~~~ The Modulation Transfer Function (MTF) can be determined from an **edge spread function** (ESF) by first calculating the **line spread function** (LSF) from the ESF, then taking the Fourier transform of the LSF. The magnitude of the resulting complex function is the MTF. .. plot:: :include-source: False import matplotlib.pyplot as plt import numpy as np from scipy.ndimage import gaussian_filter1d from scipy.fft import fft, fftfreq n = 100 n_pad = 1024 x = np.arange(0,n) y = np.zeros(n) y[n//2:] = 1 esf = gaussian_filter1d(y, sigma=10) lsf = np.gradient(esf) lsf_padded = np.pad(lsf, n_pad, mode='constant', constant_values=0) mtf = np.abs(fft(lsf_padded)) mtf /= mtf[0] # Normalize freq = fftfreq(len(lsf_padded)) half = len(freq) // 2 freq = freq[:half] mtf = mtf[:half] fig, axs = plt.subplots(1, 3, constrained_layout=True, figsize=(6, 2.5)) ax = axs[0] ax.plot(x,esf) ax.set_title("ESF") ax.set_xlabel("Pixel #") ax.set_ylabel("Pixel count") ax.set_yticks([]) ax = axs[1] ax.plot(x,lsf) ax.set_title("LSF") ax.set_xlabel("Pixel #") ax.set_ylabel('$\mathit{dESF/dx}$') ax.set_yticks([]) ax = axs[2] ax.plot(freq,mtf) ax.set_title("MTF") ax.set_xlabel("Frequency [cycles/pixel]") ax.set_ylabel(r"$\mathcal{F}[LSF]$") ax.set_xlim(0,0.07) plt.show() Implementation ~~~~~~~~~~~~~~ In **pylinac**, the MTF is computed from a **list of profiles (edge spread functions)** for example, multiple profiles taken at different edge positions of a phantom. The profiles can have different lengths (by default they are padded to a common size). Since it is not guaranteed that all profiles are aligned, the MTF is calculated for each profile individually, and then the results are averaged to obtain a single MTF. It is assumed that the **sample spacing** (input parameter in mm) is the same for all profiles. * If ``sample_spacing`` is None (default) the frequency axis is expressed in cycles per sample, up to 0.5 (Nyquist frequency). * If ``sample_spacing`` is provided, the frequency axis is expressed in line pairs per mm, up to the Nyquist limit determined by the sample spacing. .. code-block:: # Default: frequency axis is expressd in cycles/sample EdgeSpreadFunctionMTF(...) # Same as above EdgeSpreadFunctionMTF(..., sample_spacing = None) # Sample_spacing in mm: frequency axis is expressd in line pairs/mm EdgeSpreadFunctionMTF(..., sample_spacing = 0.5) Each profile MTF is computed using the following sequence of operations: 1. **Differentiate ESF → LSF** Compute the **line spread function (LSF)** as the discrete derivative of the ESF, implemented with a central-difference method (``lsf = np.gradient(esf)``). 2. **Apply windowing** Multiply the LSF by a windowing function to taper the edges and reduce spectral leakage introduced by the finite measurement length. The default window is ``scipy.signal.windows.hann``. This can be modified using the ``windowing`` parameter. Input arguments to the windowing function can also be passed in the constructor through ``kwargs``. (see more windowing functions here: https://docs.scipy.org/doc/scipy/reference/signal.windows.html) .. code-block:: # Default window: Hann EdgeSpreadFunctionMTF(...) # No windowing EdgeSpreadFunctionMTF(..., windowing=None) # Custom window with default parameters EdgeSpreadFunctionMTF(..., windowing=scipy.signal.windows.tukey) # Custom window with custom parameters EdgeSpreadFunctionMTF(..., windowing=scipy.signal.windows.tukey, alpha=0.3) 3. **Zero-pad for frequency resolution** Pad the LSF to the target length (default: ``1024`` samples). Padding does not add information, but interpolates the frequency spectrum more finely, improving the resolution of the displayed MTF curve. This can be modified using the parameters ``padding_mode`` and ``num_samples``: .. code-block:: # no padding EdgeSpreadFunctionMTF(..., padding_mode="none") # array zero-padded to 2048 elements (must be larger than the largest array) EdgeSpreadFunctionMTF(..., padding_mode="fixed", num_samples=2048) # array padded to the next power of 2 or num_samples (i.e. len(largest_array) == 200 => num_samples=1024, len(largest_array) == 1025 => num_samples=2048) EdgeSpreadFunctionMTF(..., padding_mode="auto", num_samples=1024) 4. **Fourier transform** Compute the fast Fourier transform (FFT) of the windowed and padded LSF. 5. **Magnitude spectrum → MTF** Take the modulus (magnitude) of the FFT result to obtain the (unnormalized) MTF. 6. **Normalization** Normalize the MTF by dividing by the first value so that ``MTF(0) = 1``. Moments-based MTF ----------------- The MTF can also be calculated using the moments of the line pair spread function (LPSF). This algorithm is based on the work of Hander et al [1]_. Specifically, equation 8: .. math:: MTF = \frac{\sqrt{2 * (\sigma^{2} - \mu)}}{\mu} where :math:`\mu` is the mean pixel value of the ROI and :math:`\sigma` is the standard deviation of the ROI pixel values. .. [1] `Hander et al. `__ "Rapid objective measurement of gamma camera resolution using statistical moments" (1998). API Documentation ----------------- .. automodule:: pylinac.core.mtf