515 lines
21 KiB
Python
515 lines
21 KiB
Python
|
import numpy as np
|
|||
|
|
|||
|
from scipy.interpolate import interp1d
|
|||
|
from scipy.constants import golden_ratio
|
|||
|
from ._warps import warp
|
|||
|
from ._radon_transform import sart_projection_update
|
|||
|
from .._shared.fft import fftmodule
|
|||
|
from .._shared.utils import deprecate_kwarg, convert_to_float
|
|||
|
from warnings import warn
|
|||
|
from functools import partial
|
|||
|
|
|||
|
if fftmodule is np.fft:
|
|||
|
# fallback from scipy.fft to scipy.fftpack instead of numpy.fft
|
|||
|
# (fftpack preserves single precision while numpy.fft does not)
|
|||
|
from scipy.fftpack import fft, ifft
|
|||
|
else:
|
|||
|
fft = fftmodule.fft
|
|||
|
ifft = fftmodule.ifft
|
|||
|
|
|||
|
|
|||
|
__all__ = ['radon', 'order_angles_golden_ratio', 'iradon', 'iradon_sart']
|
|||
|
|
|||
|
|
|||
|
def radon(image, theta=None, circle=True, *, preserve_range=None):
|
|||
|
"""
|
|||
|
Calculates the radon transform of an image given specified
|
|||
|
projection angles.
|
|||
|
|
|||
|
Parameters
|
|||
|
----------
|
|||
|
image : array_like
|
|||
|
Input image. The rotation axis will be located in the pixel with
|
|||
|
indices ``(image.shape[0] // 2, image.shape[1] // 2)``.
|
|||
|
theta : array_like, optional
|
|||
|
Projection angles (in degrees). If `None`, the value is set to
|
|||
|
np.arange(180).
|
|||
|
circle : boolean, optional
|
|||
|
Assume image is zero outside the inscribed circle, making the
|
|||
|
width of each projection (the first dimension of the sinogram)
|
|||
|
equal to ``min(image.shape)``.
|
|||
|
preserve_range : bool, optional
|
|||
|
Whether to keep the original range of values. Otherwise, the input
|
|||
|
image is converted according to the conventions of `img_as_float`.
|
|||
|
Also see https://scikit-image.org/docs/dev/user_guide/data_types.html
|
|||
|
|
|||
|
Returns
|
|||
|
-------
|
|||
|
radon_image : ndarray
|
|||
|
Radon transform (sinogram). The tomography rotation axis will lie
|
|||
|
at the pixel index ``radon_image.shape[0] // 2`` along the 0th
|
|||
|
dimension of ``radon_image``.
|
|||
|
|
|||
|
References
|
|||
|
----------
|
|||
|
.. [1] AC Kak, M Slaney, "Principles of Computerized Tomographic
|
|||
|
Imaging", IEEE Press 1988.
|
|||
|
.. [2] B.R. Ramesh, N. Srinivasa, K. Rajgopal, "An Algorithm for Computing
|
|||
|
the Discrete Radon Transform With Some Applications", Proceedings of
|
|||
|
the Fourth IEEE Region 10 International Conference, TENCON '89, 1989
|
|||
|
|
|||
|
Notes
|
|||
|
-----
|
|||
|
Based on code of Justin K. Romberg
|
|||
|
(https://www.clear.rice.edu/elec431/projects96/DSP/bpanalysis.html)
|
|||
|
|
|||
|
"""
|
|||
|
if image.ndim != 2:
|
|||
|
raise ValueError('The input image must be 2-D')
|
|||
|
if theta is None:
|
|||
|
theta = np.arange(180)
|
|||
|
|
|||
|
if preserve_range is None and np.issubdtype(image.dtype, np.integer):
|
|||
|
warn('Image dtype is not float. By default radon will assume '
|
|||
|
'you want to preserve the range of your image '
|
|||
|
'(preserve_range=True). In scikit-image 0.18 this behavior will '
|
|||
|
'change to preserve_range=False. To avoid this warning, '
|
|||
|
'explicitly specify the preserve_range parameter.',
|
|||
|
stacklevel=2)
|
|||
|
preserve_range = True
|
|||
|
|
|||
|
image = convert_to_float(image, preserve_range)
|
|||
|
|
|||
|
if circle:
|
|||
|
shape_min = min(image.shape)
|
|||
|
radius = shape_min // 2
|
|||
|
img_shape = np.array(image.shape)
|
|||
|
coords = np.array(np.ogrid[:image.shape[0], :image.shape[1]])
|
|||
|
dist = ((coords - img_shape // 2) ** 2).sum(0)
|
|||
|
outside_reconstruction_circle = dist > radius ** 2
|
|||
|
if np.any(image[outside_reconstruction_circle]):
|
|||
|
warn('Radon transform: image must be zero outside the '
|
|||
|
'reconstruction circle')
|
|||
|
# Crop image to make it square
|
|||
|
slices = tuple(slice(int(np.ceil(excess / 2)),
|
|||
|
int(np.ceil(excess / 2) + shape_min))
|
|||
|
if excess > 0 else slice(None)
|
|||
|
for excess in (img_shape - shape_min))
|
|||
|
padded_image = image[slices]
|
|||
|
else:
|
|||
|
diagonal = np.sqrt(2) * max(image.shape)
|
|||
|
pad = [int(np.ceil(diagonal - s)) for s in image.shape]
|
|||
|
new_center = [(s + p) // 2 for s, p in zip(image.shape, pad)]
|
|||
|
old_center = [s // 2 for s in image.shape]
|
|||
|
pad_before = [nc - oc for oc, nc in zip(old_center, new_center)]
|
|||
|
pad_width = [(pb, p - pb) for pb, p in zip(pad_before, pad)]
|
|||
|
padded_image = np.pad(image, pad_width, mode='constant',
|
|||
|
constant_values=0)
|
|||
|
|
|||
|
# padded_image is always square
|
|||
|
if padded_image.shape[0] != padded_image.shape[1]:
|
|||
|
raise ValueError('padded_image must be a square')
|
|||
|
center = padded_image.shape[0] // 2
|
|||
|
radon_image = np.zeros((padded_image.shape[0], len(theta)),
|
|||
|
dtype=image.dtype)
|
|||
|
|
|||
|
for i, angle in enumerate(np.deg2rad(theta)):
|
|||
|
cos_a, sin_a = np.cos(angle), np.sin(angle)
|
|||
|
R = np.array([[cos_a, sin_a, -center * (cos_a + sin_a - 1)],
|
|||
|
[-sin_a, cos_a, -center * (cos_a - sin_a - 1)],
|
|||
|
[0, 0, 1]])
|
|||
|
rotated = warp(padded_image, R, clip=False)
|
|||
|
radon_image[:, i] = rotated.sum(0)
|
|||
|
return radon_image
|
|||
|
|
|||
|
|
|||
|
def _sinogram_circle_to_square(sinogram):
|
|||
|
diagonal = int(np.ceil(np.sqrt(2) * sinogram.shape[0]))
|
|||
|
pad = diagonal - sinogram.shape[0]
|
|||
|
old_center = sinogram.shape[0] // 2
|
|||
|
new_center = diagonal // 2
|
|||
|
pad_before = new_center - old_center
|
|||
|
pad_width = ((pad_before, pad - pad_before), (0, 0))
|
|||
|
return np.pad(sinogram, pad_width, mode='constant', constant_values=0)
|
|||
|
|
|||
|
|
|||
|
def _get_fourier_filter(size, filter_name):
|
|||
|
"""Construct the Fourier filter.
|
|||
|
|
|||
|
This computation lessens artifacts and removes a small bias as
|
|||
|
explained in [1], Chap 3. Equation 61.
|
|||
|
|
|||
|
Parameters
|
|||
|
----------
|
|||
|
size: int
|
|||
|
filter size. Must be even.
|
|||
|
filter_name: str
|
|||
|
Filter used in frequency domain filtering. Filters available:
|
|||
|
ramp, shepp-logan, cosine, hamming, hann. Assign None to use
|
|||
|
no filter.
|
|||
|
|
|||
|
Returns
|
|||
|
-------
|
|||
|
fourier_filter: ndarray
|
|||
|
The computed Fourier filter.
|
|||
|
|
|||
|
References
|
|||
|
----------
|
|||
|
.. [1] AC Kak, M Slaney, "Principles of Computerized Tomographic
|
|||
|
Imaging", IEEE Press 1988.
|
|||
|
|
|||
|
"""
|
|||
|
n = np.concatenate((np.arange(1, size / 2 + 1, 2, dtype=np.int),
|
|||
|
np.arange(size / 2 - 1, 0, -2, dtype=np.int)))
|
|||
|
f = np.zeros(size)
|
|||
|
f[0] = 0.25
|
|||
|
f[1::2] = -1 / (np.pi * n) ** 2
|
|||
|
|
|||
|
# Computing the ramp filter from the fourier transform of its
|
|||
|
# frequency domain representation lessens artifacts and removes a
|
|||
|
# small bias as explained in [1], Chap 3. Equation 61
|
|||
|
fourier_filter = 2 * np.real(fft(f)) # ramp filter
|
|||
|
if filter_name == "ramp":
|
|||
|
pass
|
|||
|
elif filter_name == "shepp-logan":
|
|||
|
# Start from first element to avoid divide by zero
|
|||
|
omega = np.pi * fftmodule.fftfreq(size)[1:]
|
|||
|
fourier_filter[1:] *= np.sin(omega) / omega
|
|||
|
elif filter_name == "cosine":
|
|||
|
freq = np.linspace(0, np.pi, size, endpoint=False)
|
|||
|
cosine_filter = fftmodule.fftshift(np.sin(freq))
|
|||
|
fourier_filter *= cosine_filter
|
|||
|
elif filter_name == "hamming":
|
|||
|
fourier_filter *= fftmodule.fftshift(np.hamming(size))
|
|||
|
elif filter_name == "hann":
|
|||
|
fourier_filter *= fftmodule.fftshift(np.hanning(size))
|
|||
|
elif filter_name is None:
|
|||
|
fourier_filter[:] = 1
|
|||
|
|
|||
|
return fourier_filter[:, np.newaxis]
|
|||
|
|
|||
|
|
|||
|
@deprecate_kwarg(kwarg_mapping={'filter': 'filter_name'},
|
|||
|
removed_version="0.19")
|
|||
|
def iradon(radon_image, theta=None, output_size=None,
|
|||
|
filter_name="ramp", interpolation="linear", circle=True):
|
|||
|
"""Inverse radon transform.
|
|||
|
|
|||
|
Reconstruct an image from the radon transform, using the filtered
|
|||
|
back projection algorithm.
|
|||
|
|
|||
|
Parameters
|
|||
|
----------
|
|||
|
radon_image : array_like, dtype=float
|
|||
|
Image containing radon transform (sinogram). Each column of
|
|||
|
the image corresponds to a projection along a different
|
|||
|
angle. The tomography rotation axis should lie at the pixel
|
|||
|
index ``radon_image.shape[0] // 2`` along the 0th dimension of
|
|||
|
``radon_image``.
|
|||
|
theta : array_like, dtype=float, optional
|
|||
|
Reconstruction angles (in degrees). Default: m angles evenly spaced
|
|||
|
between 0 and 180 (if the shape of `radon_image` is (N, M)).
|
|||
|
output_size : int, optional
|
|||
|
Number of rows and columns in the reconstruction.
|
|||
|
filter_name : str, optional
|
|||
|
Filter used in frequency domain filtering. Ramp filter used by default.
|
|||
|
Filters available: ramp, shepp-logan, cosine, hamming, hann.
|
|||
|
Assign None to use no filter.
|
|||
|
interpolation : str, optional
|
|||
|
Interpolation method used in reconstruction. Methods available:
|
|||
|
'linear', 'nearest', and 'cubic' ('cubic' is slow).
|
|||
|
circle : boolean, optional
|
|||
|
Assume the reconstructed image is zero outside the inscribed circle.
|
|||
|
Also changes the default output_size to match the behaviour of
|
|||
|
``radon`` called with ``circle=True``.
|
|||
|
|
|||
|
Returns
|
|||
|
-------
|
|||
|
reconstructed : ndarray
|
|||
|
Reconstructed image. The rotation axis will be located in the pixel
|
|||
|
with indices
|
|||
|
``(reconstructed.shape[0] // 2, reconstructed.shape[1] // 2)``.
|
|||
|
|
|||
|
.. versionchanged :: 0.19
|
|||
|
In ``iradon``, ``filter`` argument is deprecated in favor of
|
|||
|
``filter_name``.
|
|||
|
|
|||
|
References
|
|||
|
----------
|
|||
|
.. [1] AC Kak, M Slaney, "Principles of Computerized Tomographic
|
|||
|
Imaging", IEEE Press 1988.
|
|||
|
.. [2] B.R. Ramesh, N. Srinivasa, K. Rajgopal, "An Algorithm for Computing
|
|||
|
the Discrete Radon Transform With Some Applications", Proceedings of
|
|||
|
the Fourth IEEE Region 10 International Conference, TENCON '89, 1989
|
|||
|
|
|||
|
Notes
|
|||
|
-----
|
|||
|
It applies the Fourier slice theorem to reconstruct an image by
|
|||
|
multiplying the frequency domain of the filter with the FFT of the
|
|||
|
projection data. This algorithm is called filtered back projection.
|
|||
|
|
|||
|
"""
|
|||
|
if radon_image.ndim != 2:
|
|||
|
raise ValueError('The input image must be 2-D')
|
|||
|
|
|||
|
if theta is None:
|
|||
|
theta = np.linspace(0, 180, radon_image.shape[1], endpoint=False)
|
|||
|
|
|||
|
angles_count = len(theta)
|
|||
|
if angles_count != radon_image.shape[1]:
|
|||
|
raise ValueError("The given ``theta`` does not match the number of "
|
|||
|
"projections in ``radon_image``.")
|
|||
|
|
|||
|
interpolation_types = ('linear', 'nearest', 'cubic')
|
|||
|
if interpolation not in interpolation_types:
|
|||
|
raise ValueError("Unknown interpolation: %s" % interpolation)
|
|||
|
|
|||
|
filter_types = ('ramp', 'shepp-logan', 'cosine', 'hamming', 'hann', None)
|
|||
|
if filter_name not in filter_types:
|
|||
|
raise ValueError("Unknown filter: %s" % filter_name)
|
|||
|
|
|||
|
img_shape = radon_image.shape[0]
|
|||
|
if output_size is None:
|
|||
|
# If output size not specified, estimate from input radon image
|
|||
|
if circle:
|
|||
|
output_size = img_shape
|
|||
|
else:
|
|||
|
output_size = int(np.floor(np.sqrt((img_shape) ** 2 / 2.0)))
|
|||
|
|
|||
|
if circle:
|
|||
|
radon_image = _sinogram_circle_to_square(radon_image)
|
|||
|
img_shape = radon_image.shape[0]
|
|||
|
|
|||
|
# Resize image to next power of two (but no less than 64) for
|
|||
|
# Fourier analysis; speeds up Fourier and lessens artifacts
|
|||
|
projection_size_padded = max(64, int(2 ** np.ceil(np.log2(2 * img_shape))))
|
|||
|
pad_width = ((0, projection_size_padded - img_shape), (0, 0))
|
|||
|
img = np.pad(radon_image, pad_width, mode='constant', constant_values=0)
|
|||
|
|
|||
|
# Apply filter in Fourier domain
|
|||
|
fourier_filter = _get_fourier_filter(projection_size_padded, filter_name)
|
|||
|
projection = fft(img, axis=0) * fourier_filter
|
|||
|
radon_filtered = np.real(ifft(projection, axis=0)[:img_shape, :])
|
|||
|
|
|||
|
# Reconstruct image by interpolation
|
|||
|
reconstructed = np.zeros((output_size, output_size))
|
|||
|
radius = output_size // 2
|
|||
|
xpr, ypr = np.mgrid[:output_size, :output_size] - radius
|
|||
|
x = np.arange(img_shape) - img_shape // 2
|
|||
|
|
|||
|
for col, angle in zip(radon_filtered.T, np.deg2rad(theta)):
|
|||
|
t = ypr * np.cos(angle) - xpr * np.sin(angle)
|
|||
|
if interpolation == 'linear':
|
|||
|
interpolant = partial(np.interp, xp=x, fp=col, left=0, right=0)
|
|||
|
else:
|
|||
|
interpolant = interp1d(x, col, kind=interpolation,
|
|||
|
bounds_error=False, fill_value=0)
|
|||
|
reconstructed += interpolant(t)
|
|||
|
|
|||
|
if circle:
|
|||
|
out_reconstruction_circle = (xpr ** 2 + ypr ** 2) > radius ** 2
|
|||
|
reconstructed[out_reconstruction_circle] = 0.
|
|||
|
|
|||
|
return reconstructed * np.pi / (2 * angles_count)
|
|||
|
|
|||
|
|
|||
|
def order_angles_golden_ratio(theta):
|
|||
|
"""Order angles to reduce the amount of correlated information in
|
|||
|
subsequent projections.
|
|||
|
|
|||
|
Parameters
|
|||
|
----------
|
|||
|
theta : 1D array of floats
|
|||
|
Projection angles in degrees. Duplicate angles are not allowed.
|
|||
|
|
|||
|
Returns
|
|||
|
-------
|
|||
|
indices_generator : generator yielding unsigned integers
|
|||
|
The returned generator yields indices into ``theta`` such that
|
|||
|
``theta[indices]`` gives the approximate golden ratio ordering
|
|||
|
of the projections. In total, ``len(theta)`` indices are yielded.
|
|||
|
All non-negative integers < ``len(theta)`` are yielded exactly once.
|
|||
|
|
|||
|
Notes
|
|||
|
-----
|
|||
|
The method used here is that of the golden ratio introduced
|
|||
|
by T. Kohler.
|
|||
|
|
|||
|
References
|
|||
|
----------
|
|||
|
.. [1] Kohler, T. "A projection access scheme for iterative
|
|||
|
reconstruction based on the golden section." Nuclear Science
|
|||
|
Symposium Conference Record, 2004 IEEE. Vol. 6. IEEE, 2004.
|
|||
|
.. [2] Winkelmann, Stefanie, et al. "An optimal radial profile order
|
|||
|
based on the Golden Ratio for time-resolved MRI."
|
|||
|
Medical Imaging, IEEE Transactions on 26.1 (2007): 68-76.
|
|||
|
|
|||
|
"""
|
|||
|
interval = 180
|
|||
|
|
|||
|
remaining_indices = list(np.argsort(theta)) # indices into theta
|
|||
|
# yield an arbitrary angle to start things off
|
|||
|
angle = theta[remaining_indices[0]]
|
|||
|
yield remaining_indices.pop(0)
|
|||
|
# determine subsequent angles using the golden ratio method
|
|||
|
angle_increment = interval / golden_ratio ** 2
|
|||
|
while remaining_indices:
|
|||
|
remaining_angles = theta[remaining_indices]
|
|||
|
angle = (angle + angle_increment) % interval
|
|||
|
index_above = np.searchsorted(remaining_angles, angle)
|
|||
|
index_below = index_above - 1
|
|||
|
index_above %= len(remaining_indices)
|
|||
|
|
|||
|
diff_below = abs(angle - remaining_angles[index_below])
|
|||
|
distance_below = min(diff_below % interval, diff_below % -interval)
|
|||
|
|
|||
|
diff_above = abs(angle - remaining_angles[index_above])
|
|||
|
distance_above = min(diff_above % interval, diff_above % -interval)
|
|||
|
|
|||
|
if distance_below < distance_above:
|
|||
|
yield remaining_indices.pop(index_below)
|
|||
|
else:
|
|||
|
yield remaining_indices.pop(index_above)
|
|||
|
|
|||
|
|
|||
|
def iradon_sart(radon_image, theta=None, image=None, projection_shifts=None,
|
|||
|
clip=None, relaxation=0.15, dtype=None):
|
|||
|
"""Inverse radon transform.
|
|||
|
|
|||
|
Reconstruct an image from the radon transform, using a single iteration of
|
|||
|
the Simultaneous Algebraic Reconstruction Technique (SART) algorithm.
|
|||
|
|
|||
|
Parameters
|
|||
|
----------
|
|||
|
radon_image : 2D array
|
|||
|
Image containing radon transform (sinogram). Each column of
|
|||
|
the image corresponds to a projection along a different angle. The
|
|||
|
tomography rotation axis should lie at the pixel index
|
|||
|
``radon_image.shape[0] // 2`` along the 0th dimension of
|
|||
|
``radon_image``.
|
|||
|
theta : 1D array, optional
|
|||
|
Reconstruction angles (in degrees). Default: m angles evenly spaced
|
|||
|
between 0 and 180 (if the shape of `radon_image` is (N, M)).
|
|||
|
image : 2D array, optional
|
|||
|
Image containing an initial reconstruction estimate. Shape of this
|
|||
|
array should be ``(radon_image.shape[0], radon_image.shape[0])``. The
|
|||
|
default is an array of zeros.
|
|||
|
projection_shifts : 1D array, optional
|
|||
|
Shift the projections contained in ``radon_image`` (the sinogram) by
|
|||
|
this many pixels before reconstructing the image. The i'th value
|
|||
|
defines the shift of the i'th column of ``radon_image``.
|
|||
|
clip : length-2 sequence of floats, optional
|
|||
|
Force all values in the reconstructed tomogram to lie in the range
|
|||
|
``[clip[0], clip[1]]``
|
|||
|
relaxation : float, optional
|
|||
|
Relaxation parameter for the update step. A higher value can
|
|||
|
improve the convergence rate, but one runs the risk of instabilities.
|
|||
|
Values close to or higher than 1 are not recommended.
|
|||
|
dtype : dtype, optional
|
|||
|
Output data type, must be floating point. By default, if input
|
|||
|
data type is not float, input is cast to double, otherwise
|
|||
|
dtype is set to input data type.
|
|||
|
|
|||
|
Returns
|
|||
|
-------
|
|||
|
reconstructed : ndarray
|
|||
|
Reconstructed image. The rotation axis will be located in the pixel
|
|||
|
with indices
|
|||
|
``(reconstructed.shape[0] // 2, reconstructed.shape[1] // 2)``.
|
|||
|
|
|||
|
Notes
|
|||
|
-----
|
|||
|
Algebraic Reconstruction Techniques are based on formulating the tomography
|
|||
|
reconstruction problem as a set of linear equations. Along each ray,
|
|||
|
the projected value is the sum of all the values of the cross section along
|
|||
|
the ray. A typical feature of SART (and a few other variants of algebraic
|
|||
|
techniques) is that it samples the cross section at equidistant points
|
|||
|
along the ray, using linear interpolation between the pixel values of the
|
|||
|
cross section. The resulting set of linear equations are then solved using
|
|||
|
a slightly modified Kaczmarz method.
|
|||
|
|
|||
|
When using SART, a single iteration is usually sufficient to obtain a good
|
|||
|
reconstruction. Further iterations will tend to enhance high-frequency
|
|||
|
information, but will also often increase the noise.
|
|||
|
|
|||
|
References
|
|||
|
----------
|
|||
|
.. [1] AC Kak, M Slaney, "Principles of Computerized Tomographic
|
|||
|
Imaging", IEEE Press 1988.
|
|||
|
.. [2] AH Andersen, AC Kak, "Simultaneous algebraic reconstruction
|
|||
|
technique (SART): a superior implementation of the ART algorithm",
|
|||
|
Ultrasonic Imaging 6 pp 81--94 (1984)
|
|||
|
.. [3] S Kaczmarz, "Angenäherte auflösung von systemen linearer
|
|||
|
gleichungen", Bulletin International de l’Academie Polonaise des
|
|||
|
Sciences et des Lettres 35 pp 355--357 (1937)
|
|||
|
.. [4] Kohler, T. "A projection access scheme for iterative
|
|||
|
reconstruction based on the golden section." Nuclear Science
|
|||
|
Symposium Conference Record, 2004 IEEE. Vol. 6. IEEE, 2004.
|
|||
|
.. [5] Kaczmarz' method, Wikipedia,
|
|||
|
https://en.wikipedia.org/wiki/Kaczmarz_method
|
|||
|
|
|||
|
"""
|
|||
|
if radon_image.ndim != 2:
|
|||
|
raise ValueError('radon_image must be two dimensional')
|
|||
|
|
|||
|
if dtype is None:
|
|||
|
if radon_image.dtype.char in 'fd':
|
|||
|
dtype = radon_image.dtype
|
|||
|
else:
|
|||
|
warn("Only floating point data type are valid for SART inverse "
|
|||
|
"radon transform. Input data is cast to float. To disable "
|
|||
|
"this warning, please cast image_radon to float.")
|
|||
|
dtype = np.dtype(float)
|
|||
|
elif np.dtype(dtype).char not in 'fd':
|
|||
|
raise ValueError("Only floating point data type are valid for inverse "
|
|||
|
"radon transform.")
|
|||
|
|
|||
|
dtype = np.dtype(dtype)
|
|||
|
radon_image = radon_image.astype(dtype, copy=False)
|
|||
|
|
|||
|
reconstructed_shape = (radon_image.shape[0], radon_image.shape[0])
|
|||
|
|
|||
|
if theta is None:
|
|||
|
theta = np.linspace(0, 180, radon_image.shape[1],
|
|||
|
endpoint=False, dtype=dtype)
|
|||
|
elif len(theta) != radon_image.shape[1]:
|
|||
|
raise ValueError('Shape of theta (%s) does not match the '
|
|||
|
'number of projections (%d)'
|
|||
|
% (len(theta), radon_image.shape[1]))
|
|||
|
else:
|
|||
|
theta = np.asarray(theta, dtype=dtype)
|
|||
|
|
|||
|
if image is None:
|
|||
|
image = np.zeros(reconstructed_shape, dtype=dtype)
|
|||
|
elif image.shape != reconstructed_shape:
|
|||
|
raise ValueError('Shape of image (%s) does not match first dimension '
|
|||
|
'of radon_image (%s)'
|
|||
|
% (image.shape, reconstructed_shape))
|
|||
|
elif image.dtype != dtype:
|
|||
|
warn("image dtype does not match output dtype: "
|
|||
|
"image is cast to {}".format(dtype))
|
|||
|
|
|||
|
image = np.asarray(image, dtype=dtype)
|
|||
|
|
|||
|
if projection_shifts is None:
|
|||
|
projection_shifts = np.zeros((radon_image.shape[1],), dtype=dtype)
|
|||
|
elif len(projection_shifts) != radon_image.shape[1]:
|
|||
|
raise ValueError('Shape of projection_shifts (%s) does not match the '
|
|||
|
'number of projections (%d)'
|
|||
|
% (len(projection_shifts), radon_image.shape[1]))
|
|||
|
else:
|
|||
|
projection_shifts = np.asarray(projection_shifts, dtype=dtype)
|
|||
|
if clip is not None:
|
|||
|
if len(clip) != 2:
|
|||
|
raise ValueError('clip must be a length-2 sequence')
|
|||
|
clip = np.asarray(clip, dtype=dtype)
|
|||
|
|
|||
|
for angle_index in order_angles_golden_ratio(theta):
|
|||
|
image_update = sart_projection_update(image, theta[angle_index],
|
|||
|
radon_image[:, angle_index],
|
|||
|
projection_shifts[angle_index])
|
|||
|
image += relaxation * image_update
|
|||
|
if clip is not None:
|
|||
|
image = np.clip(image, clip[0], clip[1])
|
|||
|
return image
|