Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #865 from jni/profile-line
Move `profile_line()` out of viewer and refactor
- Loading branch information
Showing
4 changed files
with
228 additions
and
79 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
import numpy as np | ||
import scipy.ndimage as nd | ||
|
||
|
||
def profile_line(img, src, dst, linewidth=1, | ||
order=1, mode='constant', cval=0.0): | ||
"""Return the intensity profile of an image measured along a scan line. | ||
Parameters | ||
---------- | ||
img : numeric array, shape (M, N[, C]) | ||
The image, either grayscale (2D array) or multichannel | ||
(3D array, where the final axis contains the channel | ||
information). | ||
src : 2-tuple of numeric scalar (float or int) | ||
The start point of the scan line. | ||
dst : 2-tuple of numeric scalar (float or int) | ||
The end point of the scan line. | ||
linewidth : int, optional | ||
Width of the scan, perpendicular to the line | ||
order : int in {0, 1, 2, 3, 4, 5}, optional | ||
The order of the spline interpolation to compute image values at | ||
non-integer coordinates. 0 means nearest-neighbor interpolation. | ||
mode : string, one of {'constant', 'nearest', 'reflect', 'wrap'}, optional | ||
How to compute any values falling outside of the image. | ||
cval : float, optional | ||
If `mode` is 'constant', what constant value to use outside the image. | ||
Returns | ||
------- | ||
return_value : array | ||
The intensity profile along the scan line. The length of the profile | ||
is the ceil of the computed length of the scan line. | ||
Examples | ||
-------- | ||
>>> x = np.array([[1, 1, 1, 2, 2, 2]]) | ||
>>> img = np.vstack([np.zeros_like(x), x, x, x, np.zeros_like(x)]) | ||
>>> img | ||
array([[0, 0, 0, 0, 0, 0], | ||
[1, 1, 1, 2, 2, 2], | ||
[1, 1, 1, 2, 2, 2], | ||
[1, 1, 1, 2, 2, 2], | ||
[0, 0, 0, 0, 0, 0]]) | ||
>>> profile_line(img, (2, 1), (2, 4)) | ||
array([ 1., 1., 2., 2.]) | ||
Notes | ||
----- | ||
The destination point is included in the profile, in contrast to | ||
standard numpy indexing. | ||
""" | ||
src_row, src_col = src = np.asarray(src, dtype=float) | ||
dst_row, dst_col = dst = np.asarray(dst, dtype=float) | ||
d_row, d_col = dst - src | ||
theta = np.arctan2(d_row, d_col) | ||
|
||
length = np.ceil(np.hypot(d_row, d_col) + 1) | ||
# we add one above because we include the last point in the profile | ||
# (in contrast to standard numpy indexing) | ||
line_col = np.linspace(src_col, dst_col, length) | ||
line_row = np.linspace(src_row, dst_row, length) | ||
|
||
# we subtract 1 from linewidth to change from pixel-counting | ||
# (make this line 3 pixels wide) to point distances (the | ||
# distance between pixel centers) | ||
col_width = (linewidth - 1) * np.sin(-theta) / 2 | ||
row_width = (linewidth - 1) * np.cos(theta) / 2 | ||
perp_rows = np.array([np.linspace(row_i - row_width, row_i + row_width, | ||
linewidth) for row_i in line_row]) | ||
perp_cols = np.array([np.linspace(col_i - col_width, col_i + col_width, | ||
linewidth) for col_i in line_col]) | ||
perp_lines = np.array([perp_rows, perp_cols]) | ||
|
||
if img.ndim == 3: | ||
pixels = [nd.map_coordinates(img[..., i], perp_lines, | ||
order=order, mode=mode, cval=cval) | ||
for i in range(img.shape[2])] | ||
pixels = np.transpose(np.asarray(pixels), (1, 2, 0)) | ||
else: | ||
pixels = nd.map_coordinates(img, perp_lines, | ||
order=order, mode=mode, cval=cval) | ||
|
||
intensities = pixels.mean(axis=1) | ||
|
||
return intensities | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
from numpy.testing import assert_equal, assert_almost_equal | ||
import numpy as np | ||
|
||
from skimage.measure import profile_line | ||
|
||
image = np.arange(100).reshape((10, 10)).astype(np.float) | ||
|
||
def test_horizontal_rightward(): | ||
prof = profile_line(image, (0, 2), (0, 8), order=0) | ||
expected_prof = np.arange(2, 9) | ||
assert_equal(prof, expected_prof) | ||
|
||
|
||
def test_horizontal_leftward(): | ||
prof = profile_line(image, (0, 8), (0, 2), order=0) | ||
expected_prof = np.arange(8, 1, -1) | ||
assert_equal(prof, expected_prof) | ||
|
||
|
||
def test_vertical_downward(): | ||
prof = profile_line(image, (2, 5), (8, 5), order=0) | ||
expected_prof = np.arange(25, 95, 10) | ||
assert_equal(prof, expected_prof) | ||
|
||
|
||
def test_vertical_upward(): | ||
prof = profile_line(image, (8, 5), (2, 5), order=0) | ||
expected_prof = np.arange(85, 15, -10) | ||
assert_equal(prof, expected_prof) | ||
|
||
|
||
def test_45deg_right_downward(): | ||
prof = profile_line(image, (2, 2), (8, 8), order=0) | ||
expected_prof = np.array([22, 33, 33, 44, 55, 55, 66, 77, 77, 88]) | ||
# repeats are due to aliasing using nearest neighbor interpolation. | ||
# to see this, imagine a diagonal line with markers every unit of | ||
# length traversing a checkerboard pattern of squares also of unit | ||
# length. Because the line is diagonal, sometimes more than one | ||
# marker will fall on the same checkerboard box. | ||
assert_almost_equal(prof, expected_prof) | ||
|
||
|
||
def test_45deg_right_downward_interpolated(): | ||
prof = profile_line(image, (2, 2), (8, 8), order=1) | ||
expected_prof = np.linspace(22, 88, 10) | ||
assert_almost_equal(prof, expected_prof) | ||
|
||
|
||
def test_45deg_right_upward(): | ||
prof = profile_line(image, (8, 2), (2, 8), order=1) | ||
expected_prof = np.arange(82, 27, -6) | ||
assert_almost_equal(prof, expected_prof) | ||
|
||
|
||
def test_45deg_left_upward(): | ||
prof = profile_line(image, (8, 8), (2, 2), order=1) | ||
expected_prof = np.arange(88, 21, -22. / 3) | ||
assert_almost_equal(prof, expected_prof) | ||
|
||
|
||
def test_45deg_left_downward(): | ||
prof = profile_line(image, (2, 8), (8, 2), order=1) | ||
expected_prof = np.arange(28, 83, 6) | ||
assert_almost_equal(prof, expected_prof) | ||
|
||
|
||
def test_pythagorean_triangle_right_downward(): | ||
prof = profile_line(image, (1, 1), (7, 9), order=0) | ||
expected_prof = np.array([11, 22, 23, 33, 34, 45, 56, 57, 67, 68, 79]) | ||
assert_equal(prof, expected_prof) | ||
|
||
|
||
def test_pythagorean_triangle_right_downward_interpolated(): | ||
prof = profile_line(image, (1, 1), (7, 9), order=1) | ||
expected_prof = np.linspace(11, 79, 11) | ||
assert_almost_equal(prof, expected_prof) | ||
|
||
pyth_image = np.zeros((6, 7), np.float) | ||
line = ((1, 2, 2, 3, 3, 4), (1, 2, 3, 3, 4, 5)) | ||
below = ((2, 2, 3, 4, 4, 5), (0, 1, 2, 3, 4, 4)) | ||
above = ((0, 1, 1, 2, 3, 3), (2, 2, 3, 4, 5, 6)) | ||
pyth_image[line] = 1.8 | ||
pyth_image[below] = 0.6 | ||
pyth_image[above] = 0.6 | ||
|
||
|
||
def test_pythagorean_triangle_right_downward_linewidth(): | ||
prof = profile_line(pyth_image, (1, 1), (4, 5), linewidth=3, order=0) | ||
expected_prof = np.ones(6) | ||
assert_almost_equal(prof, expected_prof) | ||
|
||
|
||
def test_pythagorean_triangle_right_upward_linewidth(): | ||
prof = profile_line(pyth_image[::-1, :], (4, 1), (1, 5), | ||
linewidth=3, order=0) | ||
expected_prof = np.ones(6) | ||
assert_almost_equal(prof, expected_prof) | ||
|
||
|
||
def test_pythagorean_triangle_transpose_left_down_linewidth(): | ||
prof = profile_line(pyth_image.T[:, ::-1], (1, 4), (5, 1), | ||
linewidth=3, order=0) | ||
expected_prof = np.ones(6) | ||
assert_almost_equal(prof, expected_prof) | ||
|
||
|
||
if __name__ == "__main__": | ||
from numpy.testing import run_module_suite | ||
run_module_suite() | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters