update
This commit is contained in:
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
+13
@@ -0,0 +1,13 @@
|
||||
import numpy as np
|
||||
|
||||
from skimage.transform import frt2, ifrt2
|
||||
|
||||
|
||||
def test_frt():
|
||||
SIZE = 59 # must be prime to ensure that f inverse is unique
|
||||
|
||||
# Generate a test image
|
||||
L = np.tri(SIZE, dtype=np.int32) + np.tri(SIZE, dtype=np.int32)[::-1]
|
||||
f = frt2(L)
|
||||
fi = ifrt2(f)
|
||||
assert np.array_equal(L, fi)
|
||||
File diff suppressed because it is too large
Load Diff
+635
@@ -0,0 +1,635 @@
|
||||
import numpy as np
|
||||
import pytest
|
||||
from numpy.testing import assert_almost_equal, assert_equal
|
||||
|
||||
from skimage import data, transform
|
||||
from skimage._shared.testing import run_in_parallel
|
||||
from skimage.draw import circle_perimeter, ellipse_perimeter, line
|
||||
|
||||
|
||||
@run_in_parallel()
|
||||
def test_hough_line():
|
||||
# Generate a test image
|
||||
img = np.zeros((100, 150), dtype=int)
|
||||
rr, cc = line(60, 130, 80, 10)
|
||||
img[rr, cc] = 1
|
||||
|
||||
out, angles, d = transform.hough_line(img)
|
||||
|
||||
y, x = np.where(out == out.max())
|
||||
dist = d[y[0]]
|
||||
theta = angles[x[0]]
|
||||
|
||||
assert_almost_equal(dist, 80.0, 1)
|
||||
assert_almost_equal(theta, 1.41, 1)
|
||||
|
||||
|
||||
def test_hough_line_angles():
|
||||
img = np.zeros((10, 10))
|
||||
img[0, 0] = 1
|
||||
|
||||
out, angles, d = transform.hough_line(img, np.linspace(0, 360, 10))
|
||||
|
||||
assert_equal(len(angles), 10)
|
||||
|
||||
|
||||
def test_hough_line_bad_input():
|
||||
img = np.zeros(100)
|
||||
img[10] = 1
|
||||
|
||||
# Expected error, img must be 2D
|
||||
with pytest.raises(ValueError):
|
||||
transform.hough_line(img, np.linspace(0, 360, 10))
|
||||
|
||||
|
||||
def test_probabilistic_hough():
|
||||
# Generate a test image
|
||||
img = np.zeros((100, 100), dtype=int)
|
||||
for i in range(25, 75):
|
||||
img[100 - i, i] = 100
|
||||
img[i, i] = 100
|
||||
|
||||
# decrease default theta sampling because similar orientations may confuse
|
||||
# as mentioned in article of Galambos et al
|
||||
theta = np.linspace(0, np.pi, 45)
|
||||
lines = transform.probabilistic_hough_line(
|
||||
img, threshold=10, line_length=10, line_gap=1, theta=theta
|
||||
)
|
||||
# sort the lines according to the x-axis
|
||||
sorted_lines = []
|
||||
for ln in lines:
|
||||
ln = list(ln)
|
||||
ln.sort(key=lambda x: x[0])
|
||||
sorted_lines.append(ln)
|
||||
|
||||
assert [(25, 75), (74, 26)] in sorted_lines
|
||||
assert [(25, 25), (74, 74)] in sorted_lines
|
||||
|
||||
# Execute with default theta
|
||||
transform.probabilistic_hough_line(img, line_length=10, line_gap=3)
|
||||
|
||||
|
||||
def test_probabilistic_hough_seed():
|
||||
# Load image that is likely to give a randomly varying number of lines
|
||||
image = data.checkerboard()
|
||||
|
||||
# Use constant seed to ensure a deterministic output
|
||||
lines = transform.probabilistic_hough_line(
|
||||
image, threshold=50, line_length=50, line_gap=1, rng=41537233
|
||||
)
|
||||
assert len(lines) == 56
|
||||
|
||||
|
||||
def test_probabilistic_hough_bad_input():
|
||||
img = np.zeros(100)
|
||||
img[10] = 1
|
||||
|
||||
# Expected error, img must be 2D
|
||||
with pytest.raises(ValueError):
|
||||
transform.probabilistic_hough_line(img)
|
||||
|
||||
|
||||
def test_hough_line_peaks():
|
||||
img = np.zeros((100, 150), dtype=int)
|
||||
rr, cc = line(60, 130, 80, 10)
|
||||
img[rr, cc] = 1
|
||||
|
||||
out, angles, d = transform.hough_line(img)
|
||||
|
||||
out, theta, dist = transform.hough_line_peaks(out, angles, d)
|
||||
|
||||
assert_equal(len(dist), 1)
|
||||
assert_almost_equal(dist[0], 81.0, 1)
|
||||
assert_almost_equal(theta[0], 1.41, 1)
|
||||
|
||||
|
||||
def test_hough_line_peaks_ordered():
|
||||
# Regression test per PR #1421
|
||||
testim = np.zeros((256, 64), dtype=bool)
|
||||
|
||||
testim[50:100, 20] = True
|
||||
testim[20:225, 25] = True
|
||||
testim[15:35, 50] = True
|
||||
testim[1:-1, 58] = True
|
||||
|
||||
hough_space, angles, dists = transform.hough_line(testim)
|
||||
|
||||
hspace, _, _ = transform.hough_line_peaks(hough_space, angles, dists)
|
||||
assert hspace[0] > hspace[1]
|
||||
|
||||
|
||||
def test_hough_line_peaks_single_line():
|
||||
# Regression test for gh-6187, gh-4129
|
||||
|
||||
# create an empty test image
|
||||
img = np.zeros((100, 100), dtype=bool)
|
||||
# draw a horizontal line into our test image
|
||||
img[30, :] = 1
|
||||
|
||||
hough_space, angles, dist = transform.hough_line(img)
|
||||
|
||||
best_h_space, best_angles, best_dist = transform.hough_line_peaks(
|
||||
hough_space, angles, dist
|
||||
)
|
||||
assert len(best_angles) == 1
|
||||
assert len(best_dist) == 1
|
||||
expected_angle = -np.pi / 2
|
||||
expected_dist = -30
|
||||
assert abs(best_angles[0] - expected_angle) < 0.01
|
||||
assert abs(best_dist[0] - expected_dist) < 0.01
|
||||
|
||||
|
||||
def test_hough_line_peaks_dist():
|
||||
img = np.zeros((100, 100), dtype=bool)
|
||||
img[:, 30] = True
|
||||
img[:, 40] = True
|
||||
hspace, angles, dists = transform.hough_line(img)
|
||||
assert (
|
||||
len(transform.hough_line_peaks(hspace, angles, dists, min_distance=5)[0]) == 2
|
||||
)
|
||||
assert (
|
||||
len(transform.hough_line_peaks(hspace, angles, dists, min_distance=15)[0]) == 1
|
||||
)
|
||||
|
||||
|
||||
def test_hough_line_peaks_angle():
|
||||
check_hough_line_peaks_angle()
|
||||
|
||||
|
||||
def check_hough_line_peaks_angle():
|
||||
img = np.zeros((100, 100), dtype=bool)
|
||||
img[:, 0] = True
|
||||
img[0, :] = True
|
||||
|
||||
hspace, angles, dists = transform.hough_line(img)
|
||||
assert len(transform.hough_line_peaks(hspace, angles, dists, min_angle=45)[0]) == 2
|
||||
assert len(transform.hough_line_peaks(hspace, angles, dists, min_angle=90)[0]) == 1
|
||||
|
||||
theta = np.linspace(0, np.pi, 100)
|
||||
hspace, angles, dists = transform.hough_line(img, theta)
|
||||
assert len(transform.hough_line_peaks(hspace, angles, dists, min_angle=45)[0]) == 2
|
||||
assert len(transform.hough_line_peaks(hspace, angles, dists, min_angle=90)[0]) == 1
|
||||
|
||||
theta = np.linspace(np.pi / 3, 4.0 / 3 * np.pi, 100)
|
||||
hspace, angles, dists = transform.hough_line(img, theta)
|
||||
assert len(transform.hough_line_peaks(hspace, angles, dists, min_angle=45)[0]) == 2
|
||||
assert len(transform.hough_line_peaks(hspace, angles, dists, min_angle=90)[0]) == 1
|
||||
|
||||
|
||||
def test_hough_line_peaks_num():
|
||||
img = np.zeros((100, 100), dtype=bool)
|
||||
img[:, 30] = True
|
||||
img[:, 40] = True
|
||||
hspace, angles, dists = transform.hough_line(img)
|
||||
assert (
|
||||
len(
|
||||
transform.hough_line_peaks(
|
||||
hspace, angles, dists, min_distance=0, min_angle=0, num_peaks=1
|
||||
)[0]
|
||||
)
|
||||
== 1
|
||||
)
|
||||
|
||||
|
||||
def test_hough_line_peaks_zero_input():
|
||||
# Test to make sure empty input doesn't cause a failure
|
||||
img = np.zeros((100, 100), dtype='uint8')
|
||||
theta = np.linspace(0, np.pi, 100)
|
||||
hspace, angles, dists = transform.hough_line(img, theta)
|
||||
h, a, d = transform.hough_line_peaks(hspace, angles, dists)
|
||||
assert_equal(a, np.array([]))
|
||||
|
||||
|
||||
def test_hough_line_peaks_single_angle():
|
||||
# Regression test for gh-4814
|
||||
# This code snippet used to raise an IndexError
|
||||
img = np.random.random((100, 100))
|
||||
tested_angles = np.array([np.pi / 2])
|
||||
h, theta, d = transform.hough_line(img, theta=tested_angles)
|
||||
accum, angles, dists = transform.hough_line_peaks(h, theta, d, threshold=2)
|
||||
|
||||
|
||||
@run_in_parallel()
|
||||
def test_hough_circle():
|
||||
# Prepare picture
|
||||
img = np.zeros((120, 100), dtype=int)
|
||||
radius = 20
|
||||
x_0, y_0 = (99, 50)
|
||||
y, x = circle_perimeter(y_0, x_0, radius)
|
||||
img[x, y] = 1
|
||||
|
||||
out1 = transform.hough_circle(img, radius)
|
||||
out2 = transform.hough_circle(img, [radius])
|
||||
assert_equal(out1, out2)
|
||||
out = transform.hough_circle(img, np.array([radius], dtype=np.intp))
|
||||
assert_equal(out, out1)
|
||||
x, y = np.where(out[0] == out[0].max())
|
||||
assert_equal(x[0], x_0)
|
||||
assert_equal(y[0], y_0)
|
||||
|
||||
|
||||
def test_hough_circle_extended():
|
||||
# Prepare picture
|
||||
# The circle center is outside the image
|
||||
img = np.zeros((100, 100), dtype=int)
|
||||
radius = 20
|
||||
x_0, y_0 = (-5, 50)
|
||||
y, x = circle_perimeter(y_0, x_0, radius)
|
||||
img[x[np.where(x > 0)], y[np.where(x > 0)]] = 1
|
||||
|
||||
out = transform.hough_circle(
|
||||
img, np.array([radius], dtype=np.intp), full_output=True
|
||||
)
|
||||
|
||||
x, y = np.where(out[0] == out[0].max())
|
||||
# Offset for x_0, y_0
|
||||
assert_equal(x[0], x_0 + radius)
|
||||
assert_equal(y[0], y_0 + radius)
|
||||
|
||||
|
||||
def test_hough_circle_peaks():
|
||||
x_0, y_0, rad_0 = (99, 50, 20)
|
||||
img = np.zeros((120, 100), dtype=int)
|
||||
y, x = circle_perimeter(y_0, x_0, rad_0)
|
||||
img[x, y] = 1
|
||||
|
||||
x_1, y_1, rad_1 = (49, 60, 30)
|
||||
y, x = circle_perimeter(y_1, x_1, rad_1)
|
||||
img[x, y] = 1
|
||||
|
||||
radii = [rad_0, rad_1]
|
||||
hspaces = transform.hough_circle(img, radii)
|
||||
out = transform.hough_circle_peaks(
|
||||
hspaces,
|
||||
radii,
|
||||
min_xdistance=1,
|
||||
min_ydistance=1,
|
||||
threshold=None,
|
||||
num_peaks=np.inf,
|
||||
total_num_peaks=np.inf,
|
||||
)
|
||||
s = np.argsort(out[3]) # sort by radii
|
||||
assert_equal(out[1][s], np.array([y_0, y_1]))
|
||||
assert_equal(out[2][s], np.array([x_0, x_1]))
|
||||
assert_equal(out[3][s], np.array([rad_0, rad_1]))
|
||||
|
||||
|
||||
def test_hough_circle_peaks_total_peak():
|
||||
img = np.zeros((120, 100), dtype=int)
|
||||
|
||||
x_0, y_0, rad_0 = (99, 50, 20)
|
||||
y, x = circle_perimeter(y_0, x_0, rad_0)
|
||||
img[x, y] = 1
|
||||
|
||||
x_1, y_1, rad_1 = (49, 60, 30)
|
||||
y, x = circle_perimeter(y_1, x_1, rad_1)
|
||||
img[x, y] = 1
|
||||
|
||||
radii = [rad_0, rad_1]
|
||||
hspaces = transform.hough_circle(img, radii)
|
||||
out = transform.hough_circle_peaks(
|
||||
hspaces,
|
||||
radii,
|
||||
min_xdistance=1,
|
||||
min_ydistance=1,
|
||||
threshold=None,
|
||||
num_peaks=np.inf,
|
||||
total_num_peaks=1,
|
||||
)
|
||||
assert_equal(
|
||||
out[1][0],
|
||||
np.array(
|
||||
[
|
||||
y_1,
|
||||
]
|
||||
),
|
||||
)
|
||||
assert_equal(
|
||||
out[2][0],
|
||||
np.array(
|
||||
[
|
||||
x_1,
|
||||
]
|
||||
),
|
||||
)
|
||||
assert_equal(
|
||||
out[3][0],
|
||||
np.array(
|
||||
[
|
||||
rad_1,
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def test_hough_circle_peaks_min_distance():
|
||||
x_0, y_0, rad_0 = (50, 50, 20)
|
||||
img = np.zeros((120, 100), dtype=int)
|
||||
y, x = circle_perimeter(y_0, x_0, rad_0)
|
||||
img[x, y] = 1
|
||||
|
||||
x_1, y_1, rad_1 = (60, 60, 30)
|
||||
y, x = circle_perimeter(y_1, x_1, rad_1)
|
||||
# Add noise and create an imperfect circle to lower the peak in Hough space
|
||||
y[::2] += 1
|
||||
x[::2] += 1
|
||||
img[x, y] = 1
|
||||
|
||||
x_2, y_2, rad_2 = (70, 70, 20)
|
||||
y, x = circle_perimeter(y_2, x_2, rad_2)
|
||||
# Add noise and create an imperfect circle to lower the peak in Hough space
|
||||
y[::2] += 1
|
||||
x[::2] += 1
|
||||
img[x, y] = 1
|
||||
|
||||
radii = [rad_0, rad_1, rad_2]
|
||||
hspaces = transform.hough_circle(img, radii)
|
||||
out = transform.hough_circle_peaks(
|
||||
hspaces,
|
||||
radii,
|
||||
min_xdistance=15,
|
||||
min_ydistance=15,
|
||||
threshold=None,
|
||||
num_peaks=np.inf,
|
||||
total_num_peaks=np.inf,
|
||||
normalize=True,
|
||||
)
|
||||
|
||||
# The second circle is too close to the first one
|
||||
# and has a weaker peak in Hough space due to imperfectness.
|
||||
# Therefore it got removed.
|
||||
assert_equal(out[1], np.array([y_0, y_2]))
|
||||
assert_equal(out[2], np.array([x_0, x_2]))
|
||||
assert_equal(out[3], np.array([rad_0, rad_2]))
|
||||
|
||||
|
||||
def test_hough_circle_peaks_total_peak_and_min_distance():
|
||||
img = np.zeros((120, 120), dtype=int)
|
||||
cx = cy = [40, 50, 60, 70, 80]
|
||||
radii = range(20, 30, 2)
|
||||
for i in range(len(cx)):
|
||||
y, x = circle_perimeter(cy[i], cx[i], radii[i])
|
||||
img[x, y] = 1
|
||||
|
||||
hspaces = transform.hough_circle(img, radii)
|
||||
out = transform.hough_circle_peaks(
|
||||
hspaces,
|
||||
radii,
|
||||
min_xdistance=15,
|
||||
min_ydistance=15,
|
||||
threshold=None,
|
||||
num_peaks=np.inf,
|
||||
total_num_peaks=2,
|
||||
normalize=True,
|
||||
)
|
||||
|
||||
# 2nd (4th) circle is removed as it is close to 1st (3rd) oneself.
|
||||
# 5th is removed as total_num_peaks = 2
|
||||
assert_equal(out[1], np.array(cy[:4:2]))
|
||||
assert_equal(out[2], np.array(cx[:4:2]))
|
||||
assert_equal(out[3], np.array(radii[:4:2]))
|
||||
|
||||
|
||||
def test_hough_circle_peaks_normalize():
|
||||
x_0, y_0, rad_0 = (50, 50, 20)
|
||||
img = np.zeros((120, 100), dtype=int)
|
||||
y, x = circle_perimeter(y_0, x_0, rad_0)
|
||||
img[x, y] = 1
|
||||
|
||||
x_1, y_1, rad_1 = (60, 60, 30)
|
||||
y, x = circle_perimeter(y_1, x_1, rad_1)
|
||||
img[x, y] = 1
|
||||
|
||||
radii = [rad_0, rad_1]
|
||||
hspaces = transform.hough_circle(img, radii)
|
||||
out = transform.hough_circle_peaks(
|
||||
hspaces,
|
||||
radii,
|
||||
min_xdistance=15,
|
||||
min_ydistance=15,
|
||||
threshold=None,
|
||||
num_peaks=np.inf,
|
||||
total_num_peaks=np.inf,
|
||||
normalize=False,
|
||||
)
|
||||
|
||||
# Two perfect circles are close but the second one is bigger.
|
||||
# Therefore, it is picked due to its high peak.
|
||||
assert_equal(out[1], np.array([y_1]))
|
||||
assert_equal(out[2], np.array([x_1]))
|
||||
assert_equal(out[3], np.array([rad_1]))
|
||||
|
||||
|
||||
def test_hough_ellipse_zero_angle():
|
||||
img = np.zeros((25, 25), dtype=int)
|
||||
rx = 6
|
||||
ry = 8
|
||||
x0 = 12
|
||||
y0 = 15
|
||||
angle = 0
|
||||
rr, cc = ellipse_perimeter(y0, x0, ry, rx)
|
||||
img[rr, cc] = 1
|
||||
result = transform.hough_ellipse(img, threshold=9)
|
||||
best = result[-1]
|
||||
assert_equal(best[1], y0)
|
||||
assert_equal(best[2], x0)
|
||||
assert_almost_equal(best[3], ry, decimal=1)
|
||||
assert_almost_equal(best[4], rx, decimal=1)
|
||||
assert_equal(best[5], angle)
|
||||
# Check if I re-draw the ellipse, points are the same!
|
||||
# ie check API compatibility between hough_ellipse and ellipse_perimeter
|
||||
rr2, cc2 = ellipse_perimeter(
|
||||
y0, x0, int(best[3]), int(best[4]), orientation=best[5]
|
||||
)
|
||||
assert_equal(rr, rr2)
|
||||
assert_equal(cc, cc2)
|
||||
|
||||
|
||||
def test_hough_ellipse_non_zero_posangle1():
|
||||
# ry > rx, angle in [0:pi/2]
|
||||
img = np.zeros((30, 24), dtype=int)
|
||||
rx = 6
|
||||
ry = 12
|
||||
x0 = 10
|
||||
y0 = 15
|
||||
angle = np.pi / 1.35
|
||||
rr, cc = ellipse_perimeter(y0, x0, ry, rx, orientation=angle)
|
||||
img[rr, cc] = 1
|
||||
result = transform.hough_ellipse(img, threshold=15, accuracy=3)
|
||||
result.sort(order='accumulator')
|
||||
best = result[-1]
|
||||
assert_almost_equal(best[1] / 100.0, y0 / 100.0, decimal=1)
|
||||
assert_almost_equal(best[2] / 100.0, x0 / 100.0, decimal=1)
|
||||
assert_almost_equal(best[3] / 10.0, ry / 10.0, decimal=1)
|
||||
assert_almost_equal(best[4] / 100.0, rx / 100.0, decimal=1)
|
||||
assert_almost_equal(best[5], angle, decimal=1)
|
||||
# Check if I re-draw the ellipse, points are the same!
|
||||
# ie check API compatibility between hough_ellipse and ellipse_perimeter
|
||||
rr2, cc2 = ellipse_perimeter(
|
||||
y0, x0, int(best[3]), int(best[4]), orientation=best[5]
|
||||
)
|
||||
assert_equal(rr, rr2)
|
||||
assert_equal(cc, cc2)
|
||||
|
||||
|
||||
def test_hough_ellipse_non_zero_posangle2():
|
||||
# ry < rx, angle in [0:pi/2]
|
||||
img = np.zeros((30, 24), dtype=int)
|
||||
rx = 12
|
||||
ry = 6
|
||||
x0 = 10
|
||||
y0 = 15
|
||||
angle = np.pi / 1.35
|
||||
rr, cc = ellipse_perimeter(y0, x0, ry, rx, orientation=angle)
|
||||
img[rr, cc] = 1
|
||||
result = transform.hough_ellipse(img, threshold=15, accuracy=3)
|
||||
result.sort(order='accumulator')
|
||||
best = result[-1]
|
||||
assert_almost_equal(best[1] / 100.0, y0 / 100.0, decimal=1)
|
||||
assert_almost_equal(best[2] / 100.0, x0 / 100.0, decimal=1)
|
||||
assert_almost_equal(best[3] / 10.0, ry / 10.0, decimal=1)
|
||||
assert_almost_equal(best[4] / 100.0, rx / 100.0, decimal=1)
|
||||
assert_almost_equal(best[5], angle, decimal=1)
|
||||
# Check if I re-draw the ellipse, points are the same!
|
||||
# ie check API compatibility between hough_ellipse and ellipse_perimeter
|
||||
rr2, cc2 = ellipse_perimeter(
|
||||
y0, x0, int(best[3]), int(best[4]), orientation=best[5]
|
||||
)
|
||||
assert_equal(rr, rr2)
|
||||
assert_equal(cc, cc2)
|
||||
|
||||
|
||||
def test_hough_ellipse_non_zero_posangle3():
|
||||
# ry < rx, angle in [pi/2:pi]
|
||||
img = np.zeros((30, 24), dtype=int)
|
||||
rx = 12
|
||||
ry = 6
|
||||
x0 = 10
|
||||
y0 = 15
|
||||
angle = np.pi / 1.35 + np.pi / 2.0
|
||||
rr, cc = ellipse_perimeter(y0, x0, ry, rx, orientation=angle)
|
||||
img[rr, cc] = 1
|
||||
result = transform.hough_ellipse(img, threshold=15, accuracy=3)
|
||||
result.sort(order='accumulator')
|
||||
best = result[-1]
|
||||
# Check if I re-draw the ellipse, points are the same!
|
||||
# ie check API compatibility between hough_ellipse and ellipse_perimeter
|
||||
rr2, cc2 = ellipse_perimeter(
|
||||
y0, x0, int(best[3]), int(best[4]), orientation=best[5]
|
||||
)
|
||||
assert_equal(rr, rr2)
|
||||
assert_equal(cc, cc2)
|
||||
|
||||
|
||||
def test_hough_ellipse_non_zero_posangle4():
|
||||
# ry < rx, angle in [pi:3pi/4]
|
||||
img = np.zeros((30, 24), dtype=int)
|
||||
rx = 12
|
||||
ry = 6
|
||||
x0 = 10
|
||||
y0 = 15
|
||||
angle = np.pi / 1.35 + np.pi
|
||||
rr, cc = ellipse_perimeter(y0, x0, ry, rx, orientation=angle)
|
||||
img[rr, cc] = 1
|
||||
result = transform.hough_ellipse(img, threshold=15, accuracy=3)
|
||||
result.sort(order='accumulator')
|
||||
best = result[-1]
|
||||
# Check if I re-draw the ellipse, points are the same!
|
||||
# ie check API compatibility between hough_ellipse and ellipse_perimeter
|
||||
rr2, cc2 = ellipse_perimeter(
|
||||
y0, x0, int(best[3]), int(best[4]), orientation=best[5]
|
||||
)
|
||||
assert_equal(rr, rr2)
|
||||
assert_equal(cc, cc2)
|
||||
|
||||
|
||||
def test_hough_ellipse_non_zero_negangle1():
|
||||
# ry > rx, angle in [0:-pi/2]
|
||||
img = np.zeros((30, 24), dtype=int)
|
||||
rx = 6
|
||||
ry = 12
|
||||
x0 = 10
|
||||
y0 = 15
|
||||
angle = -np.pi / 1.35
|
||||
rr, cc = ellipse_perimeter(y0, x0, ry, rx, orientation=angle)
|
||||
img[rr, cc] = 1
|
||||
result = transform.hough_ellipse(img, threshold=15, accuracy=3)
|
||||
result.sort(order='accumulator')
|
||||
best = result[-1]
|
||||
# Check if I re-draw the ellipse, points are the same!
|
||||
# ie check API compatibility between hough_ellipse and ellipse_perimeter
|
||||
rr2, cc2 = ellipse_perimeter(
|
||||
y0, x0, int(best[3]), int(best[4]), orientation=best[5]
|
||||
)
|
||||
assert_equal(rr, rr2)
|
||||
assert_equal(cc, cc2)
|
||||
|
||||
|
||||
def test_hough_ellipse_non_zero_negangle2():
|
||||
# ry < rx, angle in [0:-pi/2]
|
||||
img = np.zeros((30, 24), dtype=int)
|
||||
rx = 12
|
||||
ry = 6
|
||||
x0 = 10
|
||||
y0 = 15
|
||||
angle = -np.pi / 1.35
|
||||
rr, cc = ellipse_perimeter(y0, x0, ry, rx, orientation=angle)
|
||||
img[rr, cc] = 1
|
||||
result = transform.hough_ellipse(img, threshold=15, accuracy=3)
|
||||
result.sort(order='accumulator')
|
||||
best = result[-1]
|
||||
# Check if I re-draw the ellipse, points are the same!
|
||||
# ie check API compatibility between hough_ellipse and ellipse_perimeter
|
||||
rr2, cc2 = ellipse_perimeter(
|
||||
y0, x0, int(best[3]), int(best[4]), orientation=best[5]
|
||||
)
|
||||
assert_equal(rr, rr2)
|
||||
assert_equal(cc, cc2)
|
||||
|
||||
|
||||
def test_hough_ellipse_non_zero_negangle3():
|
||||
# ry < rx, angle in [-pi/2:-pi]
|
||||
img = np.zeros((30, 24), dtype=int)
|
||||
rx = 12
|
||||
ry = 6
|
||||
x0 = 10
|
||||
y0 = 15
|
||||
angle = -np.pi / 1.35 - np.pi / 2.0
|
||||
rr, cc = ellipse_perimeter(y0, x0, ry, rx, orientation=angle)
|
||||
img[rr, cc] = 1
|
||||
result = transform.hough_ellipse(img, threshold=15, accuracy=3)
|
||||
result.sort(order='accumulator')
|
||||
best = result[-1]
|
||||
# Check if I re-draw the ellipse, points are the same!
|
||||
# ie check API compatibility between hough_ellipse and ellipse_perimeter
|
||||
rr2, cc2 = ellipse_perimeter(
|
||||
y0, x0, int(best[3]), int(best[4]), orientation=best[5]
|
||||
)
|
||||
assert_equal(rr, rr2)
|
||||
assert_equal(cc, cc2)
|
||||
|
||||
|
||||
def test_hough_ellipse_non_zero_negangle4():
|
||||
# ry < rx, angle in [-pi:-3pi/4]
|
||||
img = np.zeros((30, 24), dtype=int)
|
||||
rx = 12
|
||||
ry = 6
|
||||
x0 = 10
|
||||
y0 = 15
|
||||
angle = -np.pi / 1.35 - np.pi
|
||||
rr, cc = ellipse_perimeter(y0, x0, ry, rx, orientation=angle)
|
||||
img[rr, cc] = 1
|
||||
result = transform.hough_ellipse(img, threshold=15, accuracy=3)
|
||||
result.sort(order='accumulator')
|
||||
best = result[-1]
|
||||
# Check if I re-draw the ellipse, points are the same!
|
||||
# ie check API compatibility between hough_ellipse and ellipse_perimeter
|
||||
rr2, cc2 = ellipse_perimeter(
|
||||
y0, x0, int(best[3]), int(best[4]), orientation=best[5]
|
||||
)
|
||||
assert_equal(rr, rr2)
|
||||
assert_equal(cc, cc2)
|
||||
|
||||
|
||||
def test_hough_ellipse_all_black_img():
|
||||
assert transform.hough_ellipse(np.zeros((100, 100))).shape == (0, 6)
|
||||
@@ -0,0 +1,68 @@
|
||||
import numpy as np
|
||||
import pytest
|
||||
from numpy.testing import assert_allclose, assert_equal
|
||||
|
||||
from skimage.transform import integral_image, integrate
|
||||
|
||||
|
||||
np.random.seed(0)
|
||||
x = (np.random.rand(50, 50) * 255).astype(np.uint8)
|
||||
s = integral_image(x)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'dtype', [np.float16, np.float32, np.float64, np.uint8, np.int32]
|
||||
)
|
||||
@pytest.mark.parametrize('dtype_as_kwarg', [False, True])
|
||||
def test_integral_image_validity(dtype, dtype_as_kwarg):
|
||||
rstate = np.random.default_rng(1234)
|
||||
dtype_kwarg = dtype if dtype_as_kwarg else None
|
||||
y = (rstate.random((20, 20)) * 255).astype(dtype)
|
||||
out = integral_image(y, dtype=dtype_kwarg)
|
||||
if y.dtype.kind == 'f':
|
||||
if dtype_as_kwarg:
|
||||
assert out.dtype == dtype
|
||||
rtol = 1e-3 if dtype == np.float16 else 1e-7
|
||||
assert_allclose(out[-1, -1], y.sum(dtype=np.float64), rtol=rtol)
|
||||
else:
|
||||
assert out.dtype == np.float64
|
||||
assert_allclose(out[-1, -1], y.sum(dtype=np.float64))
|
||||
else:
|
||||
assert out.dtype.kind == y.dtype.kind
|
||||
if not (dtype_as_kwarg and dtype == np.uint8):
|
||||
# omit check for dtype=uint8 case as it will overflow
|
||||
assert_equal(out[-1, -1], y.sum())
|
||||
|
||||
|
||||
def test_integrate_basic():
|
||||
assert_equal(x[12:24, 10:20].sum(), integrate(s, (12, 10), (23, 19)))
|
||||
assert_equal(x[:20, :20].sum(), integrate(s, (0, 0), (19, 19)))
|
||||
assert_equal(x[:20, 10:20].sum(), integrate(s, (0, 10), (19, 19)))
|
||||
assert_equal(x[10:20, :20].sum(), integrate(s, (10, 0), (19, 19)))
|
||||
|
||||
|
||||
def test_integrate_single():
|
||||
assert_equal(x[0, 0], integrate(s, (0, 0), (0, 0)))
|
||||
assert_equal(x[10, 10], integrate(s, (10, 10), (10, 10)))
|
||||
|
||||
|
||||
def test_vectorized_integrate():
|
||||
r0 = np.array([12, 0, 0, 10, 0, 10, 30])
|
||||
c0 = np.array([10, 0, 10, 0, 0, 10, 31])
|
||||
r1 = np.array([23, 19, 19, 19, 0, 10, 49])
|
||||
c1 = np.array([19, 19, 19, 19, 0, 10, 49])
|
||||
|
||||
expected = np.array(
|
||||
[
|
||||
x[12:24, 10:20].sum(),
|
||||
x[:20, :20].sum(),
|
||||
x[:20, 10:20].sum(),
|
||||
x[10:20, :20].sum(),
|
||||
x[0, 0],
|
||||
x[10, 10],
|
||||
x[30:, 31:].sum(),
|
||||
]
|
||||
)
|
||||
start_pts = [(r0[i], c0[i]) for i in range(len(r0))]
|
||||
end_pts = [(r1[i], c1[i]) for i in range(len(r0))]
|
||||
assert_equal(expected, integrate(s, start_pts, end_pts))
|
||||
@@ -0,0 +1,210 @@
|
||||
import math
|
||||
import warnings
|
||||
|
||||
import pytest
|
||||
import numpy as np
|
||||
from numpy.testing import assert_almost_equal, assert_array_equal, assert_equal
|
||||
|
||||
from skimage import data
|
||||
from skimage._shared.utils import _supported_float_type
|
||||
from skimage.transform import pyramids
|
||||
|
||||
|
||||
image = data.astronaut()
|
||||
image_gray = image[..., 0]
|
||||
|
||||
|
||||
@pytest.mark.parametrize('channel_axis', [0, 1, -1])
|
||||
def test_pyramid_reduce_rgb(channel_axis):
|
||||
image = data.astronaut()
|
||||
rows, cols, dim = image.shape
|
||||
image = np.moveaxis(image, source=-1, destination=channel_axis)
|
||||
out_ = pyramids.pyramid_reduce(image, downscale=2, channel_axis=channel_axis)
|
||||
out = np.moveaxis(out_, channel_axis, -1)
|
||||
assert_array_equal(out.shape, (rows / 2, cols / 2, dim))
|
||||
|
||||
|
||||
def test_pyramid_reduce_gray():
|
||||
rows, cols = image_gray.shape
|
||||
out1 = pyramids.pyramid_reduce(image_gray, downscale=2, channel_axis=None)
|
||||
assert_array_equal(out1.shape, (rows / 2, cols / 2))
|
||||
assert_almost_equal(np.ptp(out1), 1.0, decimal=2)
|
||||
out2 = pyramids.pyramid_reduce(
|
||||
image_gray, downscale=2, channel_axis=None, preserve_range=True
|
||||
)
|
||||
assert_almost_equal(np.ptp(out2) / np.ptp(image_gray), 1.0, decimal=2)
|
||||
|
||||
|
||||
def test_pyramid_reduce_gray_defaults():
|
||||
rows, cols = image_gray.shape
|
||||
out1 = pyramids.pyramid_reduce(image_gray)
|
||||
assert_array_equal(out1.shape, (rows / 2, cols / 2))
|
||||
assert_almost_equal(np.ptp(out1), 1.0, decimal=2)
|
||||
out2 = pyramids.pyramid_reduce(image_gray, preserve_range=True)
|
||||
assert_almost_equal(np.ptp(out2) / np.ptp(image_gray), 1.0, decimal=2)
|
||||
|
||||
|
||||
def test_pyramid_reduce_nd():
|
||||
for ndim in [1, 2, 3, 4]:
|
||||
img = np.random.randn(*((8,) * ndim))
|
||||
out = pyramids.pyramid_reduce(img, downscale=2, channel_axis=None)
|
||||
expected_shape = np.asarray(img.shape) / 2
|
||||
assert_array_equal(out.shape, expected_shape)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('channel_axis', [0, 1, 2, -1, -2, -3])
|
||||
def test_pyramid_expand_rgb(channel_axis):
|
||||
image = data.astronaut()
|
||||
rows, cols, dim = image.shape
|
||||
image = np.moveaxis(image, source=-1, destination=channel_axis)
|
||||
out = pyramids.pyramid_expand(image, upscale=2, channel_axis=channel_axis)
|
||||
expected_shape = [rows * 2, cols * 2]
|
||||
expected_shape.insert(channel_axis % image.ndim, dim)
|
||||
assert_array_equal(out.shape, expected_shape)
|
||||
|
||||
|
||||
def test_pyramid_expand_gray():
|
||||
rows, cols = image_gray.shape
|
||||
out = pyramids.pyramid_expand(image_gray, upscale=2)
|
||||
assert_array_equal(out.shape, (rows * 2, cols * 2))
|
||||
|
||||
|
||||
def test_pyramid_expand_nd():
|
||||
for ndim in [1, 2, 3, 4]:
|
||||
img = np.random.randn(*((4,) * ndim))
|
||||
out = pyramids.pyramid_expand(img, upscale=2, channel_axis=None)
|
||||
expected_shape = np.asarray(img.shape) * 2
|
||||
assert_array_equal(out.shape, expected_shape)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('channel_axis', [0, 1, 2, -1, -2, -3])
|
||||
def test_build_gaussian_pyramid_rgb(channel_axis):
|
||||
image = data.astronaut()
|
||||
rows, cols, dim = image.shape
|
||||
image = np.moveaxis(image, source=-1, destination=channel_axis)
|
||||
pyramid = pyramids.pyramid_gaussian(image, downscale=2, channel_axis=channel_axis)
|
||||
for layer, out in enumerate(pyramid):
|
||||
layer_shape = [rows / 2**layer, cols / 2**layer]
|
||||
layer_shape.insert(channel_axis % image.ndim, dim)
|
||||
assert out.shape == tuple(layer_shape)
|
||||
|
||||
|
||||
def test_build_gaussian_pyramid_gray():
|
||||
rows, cols = image_gray.shape
|
||||
pyramid = pyramids.pyramid_gaussian(image_gray, downscale=2, channel_axis=None)
|
||||
for layer, out in enumerate(pyramid):
|
||||
layer_shape = (rows / 2**layer, cols / 2**layer)
|
||||
assert_array_equal(out.shape, layer_shape)
|
||||
|
||||
|
||||
def test_build_gaussian_pyramid_gray_defaults():
|
||||
rows, cols = image_gray.shape
|
||||
pyramid = pyramids.pyramid_gaussian(image_gray)
|
||||
for layer, out in enumerate(pyramid):
|
||||
layer_shape = (rows / 2**layer, cols / 2**layer)
|
||||
assert_array_equal(out.shape, layer_shape)
|
||||
|
||||
|
||||
def test_build_gaussian_pyramid_nd():
|
||||
for ndim in [1, 2, 3, 4]:
|
||||
img = np.random.randn(*((8,) * ndim))
|
||||
original_shape = np.asarray(img.shape)
|
||||
pyramid = pyramids.pyramid_gaussian(img, downscale=2, channel_axis=None)
|
||||
for layer, out in enumerate(pyramid):
|
||||
layer_shape = original_shape / 2**layer
|
||||
assert_array_equal(out.shape, layer_shape)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('channel_axis', [0, 1, 2, -1, -2, -3])
|
||||
def test_build_laplacian_pyramid_rgb(channel_axis):
|
||||
image = data.astronaut()
|
||||
rows, cols, dim = image.shape
|
||||
image = np.moveaxis(image, source=-1, destination=channel_axis)
|
||||
pyramid = pyramids.pyramid_laplacian(image, downscale=2, channel_axis=channel_axis)
|
||||
for layer, out in enumerate(pyramid):
|
||||
layer_shape = [rows / 2**layer, cols / 2**layer]
|
||||
layer_shape.insert(channel_axis % image.ndim, dim)
|
||||
assert out.shape == tuple(layer_shape)
|
||||
|
||||
|
||||
def test_build_laplacian_pyramid_defaults():
|
||||
rows, cols = image_gray.shape
|
||||
pyramid = pyramids.pyramid_laplacian(image_gray)
|
||||
for layer, out in enumerate(pyramid):
|
||||
layer_shape = (rows / 2**layer, cols / 2**layer)
|
||||
assert_array_equal(out.shape, layer_shape)
|
||||
|
||||
|
||||
def test_build_laplacian_pyramid_nd():
|
||||
for ndim in [1, 2, 3, 4]:
|
||||
img = np.random.randn(*(16,) * ndim)
|
||||
original_shape = np.asarray(img.shape)
|
||||
pyramid = pyramids.pyramid_laplacian(img, downscale=2, channel_axis=None)
|
||||
for layer, out in enumerate(pyramid):
|
||||
layer_shape = original_shape / 2**layer
|
||||
assert_array_equal(out.shape, layer_shape)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('channel_axis', [0, 1, 2, -1, -2, -3])
|
||||
def test_laplacian_pyramid_max_layers(channel_axis):
|
||||
for downscale in [2, 3, 5, 7]:
|
||||
if channel_axis is None:
|
||||
shape = (32, 8)
|
||||
shape_without_channels = shape
|
||||
else:
|
||||
shape_without_channels = (32, 8)
|
||||
ndim = len(shape_without_channels) + 1
|
||||
n_channels = 5
|
||||
shape = list(shape_without_channels)
|
||||
shape.insert(channel_axis % ndim, n_channels)
|
||||
shape = tuple(shape)
|
||||
img = np.ones(shape)
|
||||
pyramid = pyramids.pyramid_laplacian(
|
||||
img, downscale=downscale, channel_axis=channel_axis
|
||||
)
|
||||
max_layer = math.ceil(math.log(max(shape_without_channels), downscale))
|
||||
for layer, out in enumerate(pyramid):
|
||||
if channel_axis is None:
|
||||
out_shape_without_channels = out.shape
|
||||
else:
|
||||
assert out.shape[channel_axis] == n_channels
|
||||
out_shape_without_channels = list(out.shape)
|
||||
out_shape_without_channels.pop(channel_axis)
|
||||
out_shape_without_channels = tuple(out_shape_without_channels)
|
||||
|
||||
if layer < max_layer:
|
||||
# should not reach all axes as size 1 prior to final level
|
||||
assert max(out_shape_without_channels) > 1
|
||||
|
||||
# total number of images is max_layer + 1
|
||||
assert_equal(max_layer, layer)
|
||||
|
||||
# final layer should be size 1 on all axes
|
||||
assert out_shape_without_channels == (1, 1)
|
||||
|
||||
|
||||
def test_check_factor():
|
||||
with pytest.raises(ValueError):
|
||||
pyramids._check_factor(0.99)
|
||||
with pytest.raises(ValueError):
|
||||
pyramids._check_factor(-2)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('dtype', ['float16', 'float32', 'float64', 'uint8', 'int64'])
|
||||
@pytest.mark.parametrize(
|
||||
'pyramid_func', [pyramids.pyramid_gaussian, pyramids.pyramid_laplacian]
|
||||
)
|
||||
def test_pyramid_dtype_support(pyramid_func, dtype):
|
||||
with warnings.catch_warnings():
|
||||
# Ignore arch specific warning on arm64, armhf, ppc64el, riscv64, s390x
|
||||
# https://github.com/scikit-image/scikit-image/issues/7391
|
||||
warnings.filterwarnings(
|
||||
action="ignore",
|
||||
category=RuntimeWarning,
|
||||
message="invalid value encountered in cast",
|
||||
)
|
||||
img = np.random.randn(32, 8).astype(dtype)
|
||||
|
||||
pyramid = pyramid_func(img)
|
||||
float_dtype = _supported_float_type(dtype)
|
||||
assert np.all([im.dtype == float_dtype for im in pyramid])
|
||||
+533
@@ -0,0 +1,533 @@
|
||||
import itertools
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from skimage._shared._dependency_checks import has_mpl
|
||||
from skimage._shared._warnings import expected_warnings
|
||||
from skimage._shared.testing import run_in_parallel
|
||||
from skimage._shared.utils import _supported_float_type, convert_to_float
|
||||
from skimage.data import shepp_logan_phantom
|
||||
from skimage.transform import radon, iradon, iradon_sart, rescale
|
||||
|
||||
|
||||
PHANTOM = shepp_logan_phantom()[::2, ::2]
|
||||
PHANTOM = rescale(
|
||||
PHANTOM, 0.5, order=1, mode='constant', anti_aliasing=False, channel_axis=None
|
||||
)
|
||||
|
||||
|
||||
def _debug_plot(original, result, sinogram=None):
|
||||
from matplotlib import pyplot as plt
|
||||
|
||||
imkwargs = dict(cmap='gray', interpolation='nearest')
|
||||
if sinogram is None:
|
||||
plt.figure(figsize=(15, 6))
|
||||
sp = 130
|
||||
else:
|
||||
plt.figure(figsize=(11, 11))
|
||||
sp = 221
|
||||
plt.subplot(sp + 0)
|
||||
plt.imshow(sinogram, aspect='auto', **imkwargs)
|
||||
plt.subplot(sp + 1)
|
||||
plt.imshow(original, **imkwargs)
|
||||
plt.subplot(sp + 2)
|
||||
plt.imshow(result, vmin=original.min(), vmax=original.max(), **imkwargs)
|
||||
plt.subplot(sp + 3)
|
||||
plt.imshow(result - original, **imkwargs)
|
||||
plt.colorbar()
|
||||
plt.show()
|
||||
|
||||
|
||||
def _rescale_intensity(x):
|
||||
x = x.astype(float)
|
||||
x -= x.min()
|
||||
x /= x.max()
|
||||
return x
|
||||
|
||||
|
||||
def test_iradon_bias_circular_phantom():
|
||||
"""
|
||||
test that a uniform circular phantom has a small reconstruction bias
|
||||
"""
|
||||
pixels = 128
|
||||
xy = np.arange(-pixels / 2, pixels / 2) + 0.5
|
||||
x, y = np.meshgrid(xy, xy)
|
||||
image = x**2 + y**2 <= (pixels / 4) ** 2
|
||||
|
||||
theta = np.linspace(0.0, 180.0, max(image.shape), endpoint=False)
|
||||
sinogram = radon(image, theta=theta)
|
||||
|
||||
reconstruction_fbp = iradon(sinogram, theta=theta)
|
||||
error = reconstruction_fbp - image
|
||||
|
||||
tol = 5e-5
|
||||
roi_err = np.abs(np.mean(error))
|
||||
assert roi_err < tol
|
||||
|
||||
|
||||
def check_radon_center(shape, circle, dtype, preserve_range):
|
||||
# Create a test image with only a single non-zero pixel at the origin
|
||||
image = np.zeros(shape, dtype=dtype)
|
||||
image[(shape[0] // 2, shape[1] // 2)] = 1.0
|
||||
# Calculate the sinogram
|
||||
theta = np.linspace(0.0, 180.0, max(shape), endpoint=False)
|
||||
sinogram = radon(image, theta=theta, circle=circle, preserve_range=preserve_range)
|
||||
assert sinogram.dtype == _supported_float_type(sinogram.dtype)
|
||||
# The sinogram should be a straight, horizontal line
|
||||
sinogram_max = np.argmax(sinogram, axis=0)
|
||||
print(sinogram_max)
|
||||
assert np.std(sinogram_max) < 1e-6
|
||||
|
||||
|
||||
@pytest.mark.parametrize("shape", [(16, 16), (17, 17)])
|
||||
@pytest.mark.parametrize("circle", [False, True])
|
||||
@pytest.mark.parametrize("dtype", [np.float64, np.float32, np.float16, np.uint8, bool])
|
||||
@pytest.mark.parametrize("preserve_range", [False, True])
|
||||
def test_radon_center(shape, circle, dtype, preserve_range):
|
||||
check_radon_center(shape, circle, dtype, preserve_range)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("shape", [(32, 16), (33, 17)])
|
||||
@pytest.mark.parametrize("circle", [False])
|
||||
@pytest.mark.parametrize("dtype", [np.float64, np.float32, np.uint8, bool])
|
||||
@pytest.mark.parametrize("preserve_range", [False, True])
|
||||
def test_radon_center_rectangular(shape, circle, dtype, preserve_range):
|
||||
check_radon_center(shape, circle, dtype, preserve_range)
|
||||
|
||||
|
||||
def check_iradon_center(size, theta, circle):
|
||||
debug = False
|
||||
# Create a test sinogram corresponding to a single projection
|
||||
# with a single non-zero pixel at the rotation center
|
||||
if circle:
|
||||
sinogram = np.zeros((size, 1), dtype=float)
|
||||
sinogram[size // 2, 0] = 1.0
|
||||
else:
|
||||
diagonal = int(np.ceil(np.sqrt(2) * size))
|
||||
sinogram = np.zeros((diagonal, 1), dtype=float)
|
||||
sinogram[sinogram.shape[0] // 2, 0] = 1.0
|
||||
maxpoint = np.unravel_index(np.argmax(sinogram), sinogram.shape)
|
||||
print('shape of generated sinogram', sinogram.shape)
|
||||
print('maximum in generated sinogram', maxpoint)
|
||||
# Compare reconstructions for theta=angle and theta=angle + 180;
|
||||
# these should be exactly equal
|
||||
reconstruction = iradon(sinogram, theta=[theta], circle=circle)
|
||||
reconstruction_opposite = iradon(sinogram, theta=[theta + 180], circle=circle)
|
||||
print(
|
||||
'rms deviance:',
|
||||
np.sqrt(np.mean((reconstruction_opposite - reconstruction) ** 2)),
|
||||
)
|
||||
if debug and has_mpl:
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
imkwargs = dict(cmap='gray', interpolation='nearest')
|
||||
plt.figure()
|
||||
plt.subplot(221)
|
||||
plt.imshow(sinogram, **imkwargs)
|
||||
plt.subplot(222)
|
||||
plt.imshow(reconstruction_opposite - reconstruction, **imkwargs)
|
||||
plt.subplot(223)
|
||||
plt.imshow(reconstruction, **imkwargs)
|
||||
plt.subplot(224)
|
||||
plt.imshow(reconstruction_opposite, **imkwargs)
|
||||
plt.show()
|
||||
|
||||
assert np.allclose(reconstruction, reconstruction_opposite)
|
||||
|
||||
|
||||
sizes_for_test_iradon_center = [16, 17]
|
||||
thetas_for_test_iradon_center = [0, 90]
|
||||
circles_for_test_iradon_center = [False, True]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"size, theta, circle",
|
||||
itertools.product(
|
||||
sizes_for_test_iradon_center,
|
||||
thetas_for_test_iradon_center,
|
||||
circles_for_test_iradon_center,
|
||||
),
|
||||
)
|
||||
def test_iradon_center(size, theta, circle):
|
||||
check_iradon_center(size, theta, circle)
|
||||
|
||||
|
||||
def check_radon_iradon(interpolation_type, filter_type):
|
||||
debug = False
|
||||
image = PHANTOM
|
||||
reconstructed = iradon(
|
||||
radon(image, circle=False),
|
||||
filter_name=filter_type,
|
||||
interpolation=interpolation_type,
|
||||
circle=False,
|
||||
)
|
||||
delta = np.mean(np.abs(image - reconstructed))
|
||||
print('\n\tmean error:', delta)
|
||||
if debug and has_mpl:
|
||||
_debug_plot(image, reconstructed)
|
||||
if filter_type in ('ramp', 'shepp-logan'):
|
||||
if interpolation_type == 'nearest':
|
||||
allowed_delta = 0.03
|
||||
else:
|
||||
allowed_delta = 0.025
|
||||
else:
|
||||
allowed_delta = 0.05
|
||||
assert delta < allowed_delta
|
||||
|
||||
|
||||
filter_types = ["ramp", "shepp-logan", "cosine", "hamming", "hann"]
|
||||
interpolation_types = ['linear', 'nearest']
|
||||
radon_iradon_inputs = list(itertools.product(interpolation_types, filter_types))
|
||||
# cubic interpolation is slow; only run one test for it
|
||||
radon_iradon_inputs.append(('cubic', 'shepp-logan'))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("interpolation_type, filter_type", radon_iradon_inputs)
|
||||
def test_radon_iradon(interpolation_type, filter_type):
|
||||
check_radon_iradon(interpolation_type, filter_type)
|
||||
|
||||
|
||||
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
|
||||
theta = np.linspace(0, 180, nb_angles, endpoint=False)
|
||||
radon_image_200 = radon(image, theta=theta, circle=False)
|
||||
reconstructed = iradon(radon_image_200, circle=False)
|
||||
delta_200 = np.mean(
|
||||
abs(_rescale_intensity(image) - _rescale_intensity(reconstructed))
|
||||
)
|
||||
assert delta_200 < 0.03
|
||||
# Lower number of projections
|
||||
nb_angles = 80
|
||||
radon_image_80 = radon(image, theta=theta, circle=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, circle=False)
|
||||
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
|
||||
|
||||
|
||||
def check_radon_iradon_minimal(shape, slices):
|
||||
debug = False
|
||||
theta = np.arange(180)
|
||||
image = np.zeros(shape, dtype=float)
|
||||
image[slices] = 1.0
|
||||
sinogram = radon(image, theta, circle=False)
|
||||
reconstructed = iradon(sinogram, theta, circle=False)
|
||||
print('\n\tMaximum deviation:', np.max(np.abs(image - reconstructed)))
|
||||
if debug and has_mpl:
|
||||
_debug_plot(image, reconstructed, sinogram)
|
||||
if image.sum() == 1:
|
||||
assert np.unravel_index(
|
||||
np.argmax(reconstructed), image.shape
|
||||
) == np.unravel_index(np.argmax(image), image.shape)
|
||||
|
||||
|
||||
shapes = [(3, 3), (4, 4), (5, 5)]
|
||||
|
||||
|
||||
def generate_test_data_for_radon_iradon_minimal(shapes):
|
||||
def shape2coordinates(shape):
|
||||
c0, c1 = shape[0] // 2, shape[1] // 2
|
||||
coordinates = itertools.product((c0 - 1, c0, c0 + 1), (c1 - 1, c1, c1 + 1))
|
||||
return coordinates
|
||||
|
||||
def shape2shapeandcoordinates(shape):
|
||||
return itertools.product([shape], shape2coordinates(shape))
|
||||
|
||||
return itertools.chain.from_iterable(
|
||||
[shape2shapeandcoordinates(shape) for shape in shapes]
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"shape, coordinate", generate_test_data_for_radon_iradon_minimal(shapes)
|
||||
)
|
||||
def test_radon_iradon_minimal(shape, coordinate):
|
||||
check_radon_iradon_minimal(shape, coordinate)
|
||||
|
||||
|
||||
def test_reconstruct_with_wrong_angles():
|
||||
a = np.zeros((3, 3))
|
||||
p = radon(a, theta=[0, 1, 2], circle=False)
|
||||
iradon(p, theta=[0, 1, 2], circle=False)
|
||||
with pytest.raises(ValueError):
|
||||
iradon(p, theta=[0, 1, 2, 3])
|
||||
|
||||
|
||||
def _random_circle(shape):
|
||||
# Synthetic random data, zero outside reconstruction circle
|
||||
np.random.seed(98312871)
|
||||
image = np.random.rand(*shape)
|
||||
c0, c1 = np.ogrid[0 : shape[0], 0 : shape[1]]
|
||||
r = np.sqrt((c0 - shape[0] // 2) ** 2 + (c1 - shape[1] // 2) ** 2)
|
||||
radius = min(shape) // 2
|
||||
image[r > radius] = 0.0
|
||||
return image
|
||||
|
||||
|
||||
def test_radon_circle():
|
||||
a = np.ones((10, 10))
|
||||
with expected_warnings(['reconstruction circle']):
|
||||
radon(a, circle=True)
|
||||
|
||||
# Synthetic data, circular symmetry
|
||||
shape = (61, 79)
|
||||
c0, c1 = np.ogrid[0 : shape[0], 0 : shape[1]]
|
||||
r = np.sqrt((c0 - shape[0] // 2) ** 2 + (c1 - shape[1] // 2) ** 2)
|
||||
radius = min(shape) // 2
|
||||
image = np.clip(radius - r, 0, np.inf)
|
||||
image = _rescale_intensity(image)
|
||||
angles = np.linspace(0, 180, min(shape), endpoint=False)
|
||||
sinogram = radon(image, theta=angles, circle=True)
|
||||
assert np.all(sinogram.std(axis=1) < 1e-2)
|
||||
|
||||
# Synthetic data, random
|
||||
image = _random_circle(shape)
|
||||
sinogram = radon(image, theta=angles, circle=True)
|
||||
mass = sinogram.sum(axis=0)
|
||||
average_mass = mass.mean()
|
||||
relative_error = np.abs(mass - average_mass) / average_mass
|
||||
print(relative_error.max(), relative_error.mean())
|
||||
assert np.all(relative_error < 3.2e-3)
|
||||
|
||||
|
||||
def check_sinogram_circle_to_square(size):
|
||||
from skimage.transform.radon_transform import _sinogram_circle_to_square
|
||||
|
||||
image = _random_circle((size, size))
|
||||
theta = np.linspace(0.0, 180.0, size, False)
|
||||
sinogram_circle = radon(image, theta, circle=True)
|
||||
|
||||
def argmax_shape(a):
|
||||
return np.unravel_index(np.argmax(a), a.shape)
|
||||
|
||||
print('\n\targmax of circle:', argmax_shape(sinogram_circle))
|
||||
sinogram_square = radon(image, theta, circle=False)
|
||||
print('\targmax of square:', argmax_shape(sinogram_square))
|
||||
sinogram_circle_to_square = _sinogram_circle_to_square(sinogram_circle)
|
||||
print('\targmax of circle to square:', argmax_shape(sinogram_circle_to_square))
|
||||
error = abs(sinogram_square - sinogram_circle_to_square)
|
||||
print(np.mean(error), np.max(error))
|
||||
assert argmax_shape(sinogram_square) == argmax_shape(sinogram_circle_to_square)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("size", (50, 51))
|
||||
def test_sinogram_circle_to_square(size):
|
||||
check_sinogram_circle_to_square(size)
|
||||
|
||||
|
||||
def check_radon_iradon_circle(interpolation, shape, output_size):
|
||||
# Forward and inverse radon on synthetic data
|
||||
image = _random_circle(shape)
|
||||
radius = min(shape) // 2
|
||||
sinogram_rectangle = radon(image, circle=False)
|
||||
reconstruction_rectangle = iradon(
|
||||
sinogram_rectangle,
|
||||
output_size=output_size,
|
||||
interpolation=interpolation,
|
||||
circle=False,
|
||||
)
|
||||
sinogram_circle = radon(image, circle=True)
|
||||
reconstruction_circle = iradon(
|
||||
sinogram_circle,
|
||||
output_size=output_size,
|
||||
interpolation=interpolation,
|
||||
circle=True,
|
||||
)
|
||||
# Crop rectangular reconstruction to match circle=True reconstruction
|
||||
width = reconstruction_circle.shape[0]
|
||||
excess = int(np.ceil((reconstruction_rectangle.shape[0] - width) / 2))
|
||||
s = np.s_[excess : width + excess, excess : width + excess]
|
||||
reconstruction_rectangle = reconstruction_rectangle[s]
|
||||
# Find the reconstruction circle, set reconstruction to zero outside
|
||||
c0, c1 = np.ogrid[0:width, 0:width]
|
||||
r = np.sqrt((c0 - width // 2) ** 2 + (c1 - width // 2) ** 2)
|
||||
reconstruction_rectangle[r > radius] = 0.0
|
||||
print(reconstruction_circle.shape)
|
||||
print(reconstruction_rectangle.shape)
|
||||
np.allclose(reconstruction_rectangle, reconstruction_circle)
|
||||
|
||||
|
||||
# if adding more shapes to test data, you might want to look at commit d0f2bac3f
|
||||
shapes_radon_iradon_circle = ((61, 79),)
|
||||
interpolations = ('nearest', 'linear')
|
||||
output_sizes = (
|
||||
None,
|
||||
min(shapes_radon_iradon_circle[0]),
|
||||
max(shapes_radon_iradon_circle[0]),
|
||||
97,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"shape, interpolation, output_size",
|
||||
itertools.product(shapes_radon_iradon_circle, interpolations, output_sizes),
|
||||
)
|
||||
def test_radon_iradon_circle(shape, interpolation, output_size):
|
||||
check_radon_iradon_circle(interpolation, shape, output_size)
|
||||
|
||||
|
||||
def test_order_angles_golden_ratio():
|
||||
from skimage.transform.radon_transform import order_angles_golden_ratio
|
||||
|
||||
np.random.seed(1231)
|
||||
lengths = [1, 4, 10, 180]
|
||||
for l in lengths:
|
||||
theta_ordered = np.linspace(0, 180, l, endpoint=False)
|
||||
theta_random = np.random.uniform(0, 180, l)
|
||||
for theta in (theta_random, theta_ordered):
|
||||
indices = [x for x in order_angles_golden_ratio(theta)]
|
||||
# no duplicate indices allowed
|
||||
assert len(indices) == len(set(indices))
|
||||
|
||||
|
||||
@run_in_parallel()
|
||||
def test_iradon_sart():
|
||||
debug = False
|
||||
|
||||
image = rescale(
|
||||
PHANTOM, 0.8, mode='reflect', channel_axis=None, anti_aliasing=False
|
||||
)
|
||||
theta_ordered = np.linspace(0.0, 180.0, image.shape[0], endpoint=False)
|
||||
theta_missing_wedge = np.linspace(0.0, 150.0, image.shape[0], endpoint=True)
|
||||
for theta, error_factor in ((theta_ordered, 1.0), (theta_missing_wedge, 2.0)):
|
||||
sinogram = radon(image, theta, circle=True)
|
||||
reconstructed = iradon_sart(sinogram, theta)
|
||||
|
||||
if debug and has_mpl:
|
||||
from matplotlib import pyplot as plt
|
||||
|
||||
plt.figure()
|
||||
plt.subplot(221)
|
||||
plt.imshow(image, interpolation='nearest')
|
||||
plt.subplot(222)
|
||||
plt.imshow(sinogram, interpolation='nearest')
|
||||
plt.subplot(223)
|
||||
plt.imshow(reconstructed, interpolation='nearest')
|
||||
plt.subplot(224)
|
||||
plt.imshow(reconstructed - image, interpolation='nearest')
|
||||
plt.show()
|
||||
|
||||
delta = np.mean(np.abs(reconstructed - image))
|
||||
print('delta (1 iteration) =', delta)
|
||||
assert delta < 0.02 * error_factor
|
||||
reconstructed = iradon_sart(sinogram, theta, reconstructed)
|
||||
delta = np.mean(np.abs(reconstructed - image))
|
||||
print('delta (2 iterations) =', delta)
|
||||
assert delta < 0.014 * error_factor
|
||||
reconstructed = iradon_sart(sinogram, theta, clip=(0, 1))
|
||||
delta = np.mean(np.abs(reconstructed - image))
|
||||
print('delta (1 iteration, clip) =', delta)
|
||||
assert delta < 0.018 * error_factor
|
||||
|
||||
np.random.seed(1239867)
|
||||
shifts = np.random.uniform(-3, 3, sinogram.shape[1])
|
||||
x = np.arange(sinogram.shape[0])
|
||||
sinogram_shifted = np.vstack(
|
||||
[
|
||||
np.interp(x + shifts[i], x, sinogram[:, i])
|
||||
for i in range(sinogram.shape[1])
|
||||
]
|
||||
).T
|
||||
reconstructed = iradon_sart(sinogram_shifted, theta, projection_shifts=shifts)
|
||||
if debug and has_mpl:
|
||||
from matplotlib import pyplot as plt
|
||||
|
||||
plt.figure()
|
||||
plt.subplot(221)
|
||||
plt.imshow(image, interpolation='nearest')
|
||||
plt.subplot(222)
|
||||
plt.imshow(sinogram_shifted, interpolation='nearest')
|
||||
plt.subplot(223)
|
||||
plt.imshow(reconstructed, interpolation='nearest')
|
||||
plt.subplot(224)
|
||||
plt.imshow(reconstructed - image, interpolation='nearest')
|
||||
plt.show()
|
||||
|
||||
delta = np.mean(np.abs(reconstructed - image))
|
||||
print('delta (1 iteration, shifted sinogram) =', delta)
|
||||
assert delta < 0.022 * error_factor
|
||||
|
||||
|
||||
@pytest.mark.parametrize("preserve_range", [True, False])
|
||||
def test_iradon_dtype(preserve_range):
|
||||
sinogram = np.zeros((16, 1), dtype=int)
|
||||
sinogram[8, 0] = 1.0
|
||||
sinogram64 = sinogram.astype('float64')
|
||||
sinogram32 = sinogram.astype('float32')
|
||||
|
||||
assert iradon(sinogram, theta=[0], preserve_range=preserve_range).dtype == 'float64'
|
||||
assert (
|
||||
iradon(sinogram64, theta=[0], preserve_range=preserve_range).dtype
|
||||
== sinogram64.dtype
|
||||
)
|
||||
assert (
|
||||
iradon(sinogram32, theta=[0], preserve_range=preserve_range).dtype
|
||||
== sinogram32.dtype
|
||||
)
|
||||
|
||||
|
||||
def test_radon_dtype():
|
||||
img = convert_to_float(PHANTOM, False)
|
||||
img32 = img.astype(np.float32)
|
||||
|
||||
assert radon(img).dtype == img.dtype
|
||||
assert radon(img32).dtype == img32.dtype
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", [np.float32, np.float64])
|
||||
def test_iradon_sart_dtype(dtype):
|
||||
sinogram = np.zeros((16, 1), dtype=int)
|
||||
sinogram[8, 0] = 1.0
|
||||
sinogram64 = sinogram.astype('float64')
|
||||
sinogram32 = sinogram.astype('float32')
|
||||
|
||||
with expected_warnings(['Input data is cast to float']):
|
||||
assert iradon_sart(sinogram, theta=[0]).dtype == 'float64'
|
||||
|
||||
assert iradon_sart(sinogram64, theta=[0]).dtype == sinogram64.dtype
|
||||
assert iradon_sart(sinogram32, theta=[0]).dtype == sinogram32.dtype
|
||||
|
||||
assert iradon_sart(sinogram, theta=[0], dtype=dtype).dtype == dtype
|
||||
assert iradon_sart(sinogram32, theta=[0], dtype=dtype).dtype == dtype
|
||||
assert iradon_sart(sinogram64, theta=[0], dtype=dtype).dtype == dtype
|
||||
|
||||
|
||||
def test_iradon_sart_wrong_dtype():
|
||||
sinogram = np.zeros((16, 1))
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
iradon_sart(sinogram, dtype=int)
|
||||
|
||||
|
||||
def test_iradon_rampfilter_bias_circular_phantom():
|
||||
"""
|
||||
test that a uniform circular phantom has a small reconstruction bias using
|
||||
the ramp filter
|
||||
"""
|
||||
pixels = 128
|
||||
xy = np.arange(-pixels / 2, pixels / 2) + 0.5
|
||||
x, y = np.meshgrid(xy, xy)
|
||||
image = x**2 + y**2 <= (pixels / 4) ** 2
|
||||
|
||||
theta = np.linspace(0.0, 180.0, max(image.shape), endpoint=False)
|
||||
sinogram = radon(image, theta=theta)
|
||||
|
||||
reconstruction_fbp = iradon(sinogram, theta=theta)
|
||||
error = reconstruction_fbp - image
|
||||
|
||||
tol = 5e-5
|
||||
roi_err = np.abs(np.mean(error))
|
||||
assert roi_err < tol
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
import skimage as ski
|
||||
from skimage.transform import ThinPlateSplineTransform
|
||||
|
||||
SRC = np.array([[0, 0], [0, 5], [5, 5], [5, 0]])
|
||||
|
||||
DST = np.array([[5, 0], [0, 0], [0, 5], [5, 5]])
|
||||
|
||||
|
||||
class TestThinPlateSplineTransform:
|
||||
def test_call_before_estimation(self):
|
||||
tps = ThinPlateSplineTransform()
|
||||
assert tps.src is None
|
||||
with pytest.raises(ValueError, match="Transformation is undefined"):
|
||||
tps(SRC)
|
||||
|
||||
def test_call_invalid_coords_shape(self):
|
||||
tps = ThinPlateSplineTransform()
|
||||
tps.estimate(SRC, DST)
|
||||
coords = np.array([1, 2, 3])
|
||||
with pytest.raises(
|
||||
ValueError, match=r"Input `coords` must have shape \(N, 2\)"
|
||||
):
|
||||
tps(coords)
|
||||
|
||||
def test_call_on_SRC(self):
|
||||
tps = ThinPlateSplineTransform()
|
||||
tps.estimate(SRC, DST)
|
||||
result = tps(SRC)
|
||||
np.testing.assert_allclose(result, DST, atol=1e-15)
|
||||
|
||||
def test_tps_transform_inverse(self):
|
||||
tps = ThinPlateSplineTransform()
|
||||
tps.estimate(SRC, DST)
|
||||
with pytest.raises(NotImplementedError):
|
||||
tps.inverse()
|
||||
|
||||
def test_tps_estimation_faulty_input(self):
|
||||
src = np.array([[0, 0], [0, 5], [5, 5], [5, 0]])
|
||||
dst = np.array([[5, 0], [0, 0], [0, 5]])
|
||||
|
||||
tps = ThinPlateSplineTransform()
|
||||
assert tps.src is None
|
||||
|
||||
with pytest.raises(ValueError, match="Shape of `src` and `dst` didn't match"):
|
||||
tps.estimate(src, dst)
|
||||
|
||||
less_than_3pts = np.array([[0, 0], [0, 5]])
|
||||
with pytest.raises(ValueError, match="Need at least 3 points"):
|
||||
tps.estimate(less_than_3pts, dst)
|
||||
with pytest.raises(ValueError, match="Need at least 3 points"):
|
||||
tps.estimate(src, less_than_3pts)
|
||||
with pytest.raises(ValueError, match="Need at least 3 points"):
|
||||
tps.estimate(less_than_3pts, less_than_3pts)
|
||||
|
||||
not_2d = np.array([0, 1, 2, 3])
|
||||
with pytest.raises(ValueError, match=".*`src` must be a 2-dimensional array"):
|
||||
tps.estimate(not_2d, dst)
|
||||
with pytest.raises(ValueError, match=".*`dst` must be a 2-dimensional array"):
|
||||
tps.estimate(src, not_2d)
|
||||
|
||||
# When the estimation fails, the instance attributes remain unchanged
|
||||
assert tps.src is None
|
||||
|
||||
def test_rotate(self):
|
||||
image = ski.data.astronaut()
|
||||
desired = ski.transform.rotate(image, angle=90)
|
||||
|
||||
src = np.array([[0, 0], [0, 511], [511, 511], [511, 0]])
|
||||
dst = np.array([[511, 0], [0, 0], [0, 511], [511, 511]])
|
||||
tps = ThinPlateSplineTransform()
|
||||
tps.estimate(src, dst)
|
||||
result = ski.transform.warp(image, tps)
|
||||
|
||||
np.testing.assert_allclose(result, desired, atol=1e-13)
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user