Merge branch 'stefan-radon'

This commit is contained in:
Emmanuelle Gouillart
2011-09-26 23:58:12 +02:00
8 changed files with 626 additions and 1 deletions
+116
View File
@@ -0,0 +1,116 @@
***************
Radon transform
***************
The radon transform is a technique widely used in tomography, where you
reconstruct an object from its different projections. A projection for example
the scattering data obtained as the output of a tomographic scan.
For more information:
http://en.wikipedia.org/wiki/Radon_transform
http://www.clear.rice.edu/elec431/projects96/DSP/bpanalysis.html
Forward transform
=================
First we load the Schepp-Logan phantom, a classic test image representing a
tomographic scan.
.. ipython::
In [1]: from scikits.image.io import imread
In [1]: from scikits.image import data_dir
In [2]: from scikits.image.transform import radon, iradon
In [3]: from scikits.image.color import rgb2gray
In [4]: import matplotlib.pyplot as plt
In [5]: import matplotlib.cm as cm
In [6]: image = rgb2gray(imread(data_dir + "/phantom.png"))
In [7]: plt.title("original image");
In [8]: plt.imshow(image, cmap=cm.Greys_r)
@savefig radon_original_image.png width=4in
In [9]: plt.show()
Let us illustrate the transform by looking at projections taken at specific
angles.
.. ipython::
In [10]: projections = radon(image, theta=[0, 45, 90])
In [11]: plt.plot(projections);
In [12]: plt.title("radon projections");
In [13]: plt.xlabel("projection axis");
In [14]: plt.ylabel("intensity");
@savefig radon_projection_plot1.png width=4in
In [15]: plt.show()
We are going to reconstruct an image from 180 of these projections (the
default).
.. ipython::
In [16]: projections = radon(image)
In [17]: plt.figure()
In [18]: plt.title("radon projections");
In [19]: plt.xlabel("projection axis");
In [20]: plt.ylabel("intensity");
In [21]: plt.plot(projections)
@savefig radon_projection_plot2.png width=4in
In [22]: plt.show()
We have now constructed various projections, line integrals of an image, at
specific angles. This image is called a sinogram.
.. ipython::
In [23]: plt.figure()
In [24]: plt.title("sinogram");
In [25]: plt.xlabel("projection axis");
In [26]: plt.ylabel("intensity");
In [27]: plt.imshow(projections)
@savefig radon_sinogram.png width=4in
In [28]: plt.show()
Inverse transform
=================
To reconstruct the image from this sinogram, we apply the inverse transform.
.. ipython::
In [29]: reconstruction = iradon(projections)
In [30]: plt.title("reconstructed image");
In [31]: plt.imshow(reconstruction, cmap=cm.Greys_r)
@savefig radon_reconstructed_image.png width=4in
In [32]: plt.show()
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

+2
View File
@@ -1,4 +1,6 @@
from .hough_transform import *
from .radon_transform import *
from .finite_radon_transform import *
from .project import *
from ._project import homography as fast_homography
from .integral import *
+209
View File
@@ -0,0 +1,209 @@
#cython: cdivison=True boundscheck=False
__all__ = ['homography']
cimport cython
cimport numpy as np
import numpy as np
import cython
from cython.operator import dereference
np.import_array()
cdef extern from "math.h":
double floor(double)
double fmod(double, double)
cdef double get_pixel(double *image, int rows, int cols,
int r, int c, char mode, double cval=0):
"""Get a pixel from the image, taking wrapping mode into consideration.
Parameters
----------
image : *double
Input image.
rows, cols : int
Dimensions of image.
r, c : int
Position at which to get the pixel.
mode : {'C', 'W', 'M'}
Wrapping mode. Constant, Wrap or Mirror.
cval : double
Constant value to use for mode constant.
"""
if mode == 'C':
if (r < 0) or (r > rows - 1) or (c < 0) or (c > cols - 1):
return cval
else:
return image[r * cols + c]
else:
return image[coord_map(rows, r, mode) * cols +
coord_map(cols, c, mode)]
cdef int coord_map(int dim, int coord, char mode):
"""
Wrap a coordinate, according to a given dimension and mode.
Parameters
----------
dim : int
Maximum coordinate.
coord : int
Coord provided by user. May be < 0 or > dim.
mode : {'W', 'M'}
Whether to wrap or mirror the coordinate if it
falls outside [0, dim).
"""
dim = dim - 1
if mode == 'M': # mirror
if (coord < 0):
# How many times times does the coordinate wrap?
if (<int>(-coord / dim) % 2 != 0):
return dim - <int>(-coord % dim)
else:
return <int>(-coord % dim)
elif (coord > dim):
if (<int>(coord / dim) % 2 != 0):
return <int>(dim - (coord % dim))
else:
return <int>(coord % dim)
elif mode == 'W': # wrap
if (coord < 0):
return <int>(dim - (-coord % dim))
elif (coord > dim):
return <int>(coord % dim)
return coord
cdef tf(double x, double y, double* H, double *x_, double *y_):
"""Apply a homography to a coordinate.
Parameters
----------
x, y : double
Input coordinate.
H : (3,3) *double
Transformation matrix.
x_, y_ : *double
Output coordinate.
"""
cdef double xx, yy, zz
xx = H[0] * x + H[1] * y + H[2]
yy = H[3] * x + H[4] * y + H[5]
zz = H[6] * x + H[7] * y + H[8]
xx = xx / zz
yy = yy / zz
x_[0] = xx
y_[0] = yy
@cython.boundscheck(False)
def homography(np.ndarray image, np.ndarray H, output_shape=None,
mode='constant', double cval=0):
"""
Projective transformation (homography).
Perform a projective transformation (homography) of a
floating point image, using bi-linear interpolation.
For each pixel, given its homogeneous coordinate :math:`\mathbf{x}
= [x, y, 1]^T`, its target position is calculated by multiplying
with the given matrix, :math:`H`, to give :math:`H \mathbf{x}`.
E.g., to rotate by theta degrees clockwise, the matrix should be
::
[[cos(theta) -sin(theta) 0]
[sin(theta) cos(theta) 0]
[0 0 1]]
or, to translate x by 10 and y by 20,
::
[[1 0 10]
[0 1 20]
[0 0 1 ]].
Parameters
----------
image : 2-D array
Input image.
H : array of shape ``(3, 3)``
Transformation matrix H that defines the homography.
output_shape : tuple (rows, cols)
Shape of the output image generated.
order : int
Order of splines used in interpolation.
mode : {'constant', 'mirror', 'wrap'}
How to handle values outside the image borders.
cval : string
Used in conjunction with mode 'C' (constant), the value
outside the image boundaries.
"""
cdef np.ndarray[dtype=np.double_t, ndim=2] img = \
np.asarray(image, dtype=np.double)
cdef np.ndarray[dtype=np.double_t, ndim=2] M = \
np.ascontiguousarray(np.linalg.inv(H))
if mode not in ('constant', 'wrap', 'mirror'):
raise ValueError("Invalid mode specified. Please use "
"`constant`, `wrap` or `mirror`.")
if mode == 'constant':
mode_c = ord('C')
elif mode == 'wrap':
mode_c = ord('W')
elif mode == 'mirror':
mode_c = ord('M')
cdef int out_r, out_c, columns, rows
if output_shape is None:
out_r = img.shape[0]
out_c = img.shape[1]
else:
out_r = output_shape[0]
out_c = output_shape[1]
rows = img.shape[0]
columns = img.shape[1]
cdef np.ndarray[dtype=np.double_t, ndim=2] out = \
np.zeros((out_r, out_c), dtype=np.double)
cdef int tfr, tfc, r_int, c_int
cdef double y0, y1, y2, y3
cdef double r, c, z, t, u
for tfr in range(out_r):
for tfc in range(out_c):
tf(tfc, tfr, <double*>M.data, &c, &r)
r_int = <int>floor(r)
c_int = <int>floor(c)
t = r - r_int
u = c - c_int
y0 = get_pixel(<double*>img.data, rows, columns,
r_int, c_int, mode_c)
y1 = get_pixel(<double*>img.data, rows, columns,
r_int + 1, c_int, mode_c)
y2 = get_pixel(<double*>img.data, rows, columns,
r_int + 1, c_int + 1, mode_c)
y3 = get_pixel(<double*>img.data, rows, columns,
r_int, c_int + 1, mode_c)
out[tfr, tfc] = \
(1 - t) * (1 - u) * y0 + \
t * (1 - u) * y1 + \
t * u * y2 + (1 - t) * u * y3;
return out
+191
View File
@@ -0,0 +1,191 @@
"""
radon.py - Radon and inverse radon transforms
Based on code of Justin K. Romberg
(http://www.clear.rice.edu/elec431/projects96/DSP/bpanalysis.html)
J. Gillam and Chris Griffin.
References:
-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.
-A. C. Kak, Malcolm Slaney, "Principles of Computerized Tomographic
Imaging", IEEE Press 1988.
"""
import numpy as np
from scipy.fftpack import fftshift, fft, ifft
from ._project import homography
def radon(image, theta=None):
"""
Calculates the radon transform of an image given specified
projection angles.
Parameters
----------
image : array_like, dtype=float
Input image.
theta : array_like, dtype=float, optional (default np.arange(180))
Projection angles (in degrees).
Returns
-------
output : ndarray
Radon transform (sinogram).
"""
if image.ndim != 2:
raise ValueError('The input image must be 2-D')
if theta == None:
theta = np.arange(180)
height, width = image.shape
diagonal = np.sqrt(height ** 2 + width ** 2)
heightpad = np.ceil(diagonal - height) + 2
widthpad = np.ceil(diagonal - width) + 2
padded_image = np.zeros((int(height + heightpad),
int(width + widthpad)))
y0, y1 = int(np.ceil(heightpad / 2)), \
int((np.ceil(heightpad / 2) + height))
x0, x1 = int((np.ceil(widthpad / 2))), \
int((np.ceil(widthpad / 2) + width))
padded_image[y0:y1, x0:x1] = image
out = np.zeros((max(padded_image.shape), len(theta)))
h, w = padded_image.shape
shift0 = np.array([[1, 0, -w/2.],
[0, 1, -h/2.],
[0, 0, 1]])
shift1 = np.array([[1, 0, w/2.],
[0, 1, h/2.],
[0, 0, 1]])
def build_rotation(theta):
T = -np.deg2rad(theta)
R = np.array([[np.cos(T), -np.sin(T), 0],
[np.sin(T), np.cos(T), 0],
[0, 0, 1]])
return shift1.dot(R).dot(shift0)
for i in range(len(theta)):
rotated = homography(padded_image,
build_rotation(-theta[i]))
out[:,i] = rotated.sum(0)[::-1]
return out
def iradon(radon_image, theta=None, output_size=None,
filter="ramp", interpolation="linear"):
"""
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.
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 nxm)
output_size : int
Number of rows and columns in the reconstruction.
filter : str, optional (default ramp)
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 (default linear)
Interpolation method used in reconstruction.
Methods available: nearest, linear.
Returns
-------
output : ndarray
Reconstructed image.
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 == None:
m, n = radon_image.shape
theta = np.linspace(0, 180, n, endpoint=False)
th = (np.pi / 180.0) * theta
# if output size not specified, estimate from input radon image
if not output_size:
output_size = 2 * np.floor(radon_image.shape[0] / (2 * np.sqrt(2)))
n = radon_image.shape[0]
img = radon_image.copy()
# resize image to next power of two for fourier analysis
# speeds up fourier and lessens artifacts
order = max(64, 2 ** np.ceil(np.log(2 * n) / np.log(2)))
# zero pad input image
img.resize((order, img.shape[1]))
# construct the fourier filter
freqs = np.zeros((order, 1))
f = fftshift(abs(np.mgrid[-1:1:2 / order])).reshape(-1, 1)
w = 2 * np.pi * f
# start from first element to avoid divide by zero
if filter == "ramp":
pass
elif filter == "shepp-logan":
f[1:] = f[1:] * np.sin(w[1:] / 2) / (w[1:] / 2)
elif filter == "cosine":
f[1:] = f[1:] * np.cos(w[1:] / 2)
elif filter == "hamming":
f[1:] = f[1:] * (0.54 + 0.46 * np.cos(w[1:]))
elif filter == "hann":
f[1:] = f[1:] * (1 + np.cos(w[1:])) / 2
elif filter == None:
f[1:] = 1
else:
raise ValueError("Unknown filter: %s" % filter)
filter_ft = np.tile(f, (1, len(theta)))
# apply filter in fourier domain
projection = fft(img, axis=0) * filter_ft
radon_filtered = np.real(ifft(projection, axis=0))
# resize filtered image back to original size
radon_filtered = radon_filtered[:radon_image.shape[0], :]
reconstructed = np.zeros((output_size, output_size))
mid_index = np.ceil(n/2);
x = output_size
y = output_size
[X, Y] = np.mgrid[0.0:x, 0.0:y]
xpr = X - (output_size + 1.0) / 2.0
ypr = Y - (output_size + 1.0) / 2.0
# reconstruct image by interpolation
if interpolation == "nearest":
for i in range(len(theta)):
k = np.round(mid_index + xpr * np.sin(th[i]) - ypr * np.cos(th[i]))
reconstructed += radon_filtered[
((((k > 0) & (k < n)) * k) - 1).astype(np.int), i]
elif interpolation == "linear":
for i in range(len(theta)):
t = xpr*np.sin(th[i]) - ypr*np.cos(th[i])
a = np.floor(t)
b = mid_index + a
b0 = ((((b + 1 > 0) & (b + 1 < n)) * (b + 1)) - 1).astype(np.int)
b1 = ((((b > 0) & (b < n)) * b) - 1).astype(np.int)
reconstructed += (t - a) * radon_filtered[b0, i] + \
(a - t + 1) * radon_filtered[b1, i]
else:
raise ValueError("Unknown interpolation: %s" % interpolation)
return reconstructed * np.pi / (2 * len(th))
+4
View File
@@ -15,10 +15,14 @@ def configuration(parent_package='', top_path=None):
config.add_data_dir('tests')
cython(['_hough_transform.pyx'], working_path=base_path)
cython(['_project.pyx'], working_path=base_path)
config.add_extension('_hough_transform', sources=['_hough_transform.c'],
include_dirs=[get_numpy_include_dirs()])
config.add_extension('_project', sources=['_project.c'],
include_dirs=[get_numpy_include_dirs()])
return config
if __name__ == '__main__':
+41 -1
View File
@@ -1,7 +1,9 @@
import numpy as np
from numpy.testing import assert_array_almost_equal
from scikits.image.transform.project import _stackcopy, homography
from scikits.image.transform.project import _stackcopy
from scikits.image.transform import homography, fast_homography
from scikits.image import data
def test_stackcopy():
layers = 4
@@ -19,3 +21,41 @@ def test_homography():
[0, 0, 1]])
x90 = homography(x, M, order=1)
assert_array_almost_equal(x90, np.rot90(x))
def test_fast_homography():
img = data.lena()
img = img[:, :100]
theta = np.deg2rad(30)
scale = 0.5
tx, ty = 50, 50
H = np.eye(3)
S = scale * np.sin(theta)
C = scale * np.cos(theta)
H[:2, :2] = [[C, -S], [S, C]]
H[:2, 2] = [tx, ty]
for mode in ('constant', 'mirror', 'wrap'):
print 'Transform mode:', mode
p0 = homography(img, H, mode=mode, order=1)
p1 = fast_homography(img, H, mode=mode)
p1 = np.round(p1)
## import matplotlib.pyplot as plt
## f, (ax0, ax1, ax2, ax3) = plt.subplots(1, 4)
## ax0.imshow(img)
## ax1.imshow(p0, cmap=plt.cm.gray)
## ax2.imshow(p1, cmap=plt.cm.gray)
## ax3.imshow(np.abs(p0 - p1), cmap=plt.cm.gray)
## plt.show()
d = np.mean(np.abs(p0 - p1))
assert d < 0.2
if __name__ == "__main__":
from numpy.testing import run_module_suite
run_module_suite()
@@ -0,0 +1,63 @@
import numpy as np
from numpy.testing import *
from scikits.image.transform import *
def rescale(x):
x = x.astype(float)
x -= x.min()
x /= x.max()
return x
def test_radon_iradon():
size = 100
image = np.tri(size) + np.tri(size)[::-1]
for filter_type in ["ramp", "shepp-logan", "cosine", "hamming", "hann"]:
reconstructed = iradon(radon(image), filter=filter_type)
image = rescale(image)
reconstructed = rescale(reconstructed)
delta = np.mean(np.abs(image - reconstructed))
## print delta
## import matplotlib.pyplot as plt
## f, (ax1, ax2) = plt.subplots(1, 2)
## ax1.imshow(image, cmap=plt.cm.gray)
## ax2.imshow(reconstructed, cmap=plt.cm.gray)
## plt.show()
assert delta < 0.05
reconstructed = iradon(radon(image), filter="ramp", interpolation="nearest")
delta = np.mean(abs(image - reconstructed))
assert delta < 0.05
def test_iradon_angles():
"""
Test with different number of projections
"""
size = 100
# Synthetic data
image = np.tri(size) + np.tri(size)[::-1]
# Large number of projections: a good quality is expected
nb_angles = 200
radon_image_200 = radon(image, theta=np.linspace(0, 180, nb_angles,
endpoint=False))
reconstructed = iradon(radon_image_200)
delta_200 = np.mean(abs(rescale(image) - rescale(reconstructed)))
assert delta_200 < 0.03
# Lower number of projections
nb_angles = 80
radon_image_80 = radon(image, theta=np.linspace(0, 180, nb_angles,
endpoint=False))
# Test whether the sum of all projections is approximately the same
s = radon_image_80.sum(axis=0)
assert np.allclose(s, s[0], rtol=0.01)
reconstructed = iradon(radon_image_80)
delta_80 = np.mean(abs(image/np.max(image) - reconstructed/np.max(reconstructed)))
# Loss of quality when the number of projections is reduced
assert delta_80 > delta_200
if __name__ == "__main__":
run_module_suite()