diff --git a/skimage/measure/profile.py b/skimage/measure/profile.py index 98038e06..fef3fac8 100644 --- a/skimage/measure/profile.py +++ b/skimage/measure/profile.py @@ -50,6 +50,44 @@ def profile_line(img, src, dst, linewidth=1, The destination point is included in the profile, in contrast to standard numpy indexing. """ + perp_lines = _line_profile_coordinates(src, dst, linewidth=linewidth) + 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 + + +def _line_profile_coordinates(src, dst, linewidth=1): + """Return the coordinates of the profile of an image along a scan line. + + Parameters + ---------- + 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 + + Returns + ------- + coords : array, shape (2, N, C), float + The coordinates of the profile along the scan line. The length of the + profile is the ceil of the computed length of the scan line. + + Notes + ----- + This is a utility method meant to be used internally by skimage functions. + 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 @@ -70,18 +108,5 @@ def profile_line(img, src, dst, linewidth=1, 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 + return np.array([perp_rows, perp_cols]) diff --git a/skimage/viewer/plugins/lineprofile.py b/skimage/viewer/plugins/lineprofile.py index ea44981a..e7c04d87 100644 --- a/skimage/viewer/plugins/lineprofile.py +++ b/skimage/viewer/plugins/lineprofile.py @@ -1,3 +1,4 @@ +from __future__ import division import warnings import numpy as np @@ -147,12 +148,24 @@ class LineProfile(PlotPlugin): ------- line_image : (M, N) uint8 array, same shape as image An array of 0s with the scanned line set to 255. + If the linewidth of the line tool is greater than 1, + sets the values within the profiled polygon to 128. scan : (P,) or (P, 3) array of int or float The line scan values across the image. """ - (x1, y1), (x2, y2) = self.line_tool.end_points + end_points = self.line_tool.end_points line_image = np.zeros(self.image_viewer.original_image.shape[:2], np.uint8) + width = self.line_tool.linewidth + if width > 1: + rp, cp = measure.profile._line_profile_coordinates( + *end_points[:, ::-1], linewidth=width) + # the points are aliased, so create a polygon using the corners + yp = np.rint(rp[[0, 0, -1, -1],[0, -1, -1, 0]]).astype(int) + xp = np.rint(cp[[0, 0, -1, -1],[0, -1, -1, 0]]).astype(int) + rp, cp = draw.polygon(yp, xp, line_image.shape) + line_image[rp, cp] = 128 + (x1, y1), (x2, y2) = end_points.astype(int) rr, cc = draw.line(y1, x1, y2, x2) line_image[rr, cc] = 255 return line_image, self.scan_data diff --git a/skimage/viewer/tests/test_viewer.py b/skimage/viewer/tests/test_viewer.py new file mode 100644 index 00000000..6b0fd48f --- /dev/null +++ b/skimage/viewer/tests/test_viewer.py @@ -0,0 +1,44 @@ +import skimage +import skimage.data as data +from skimage.viewer import ImageViewer +from numpy.testing import assert_equal, assert_allclose + + +def setup_line_profile(image): + from skimage.viewer.plugins.lineprofile import LineProfile + viewer = ImageViewer(skimage.img_as_float(image)) + plugin = LineProfile() + viewer += plugin + return plugin + + +def test_line_profile(): + """ Test a line profile using an ndim=2 image""" + plugin = setup_line_profile(data.camera()) + line_image, scan_data = plugin.output() + for inp in [line_image.nonzero()[0].size, + line_image.sum() / line_image.max(), + scan_data.size]: + assert_equal(inp, 172) + assert_equal(line_image.shape, (512, 512)) + assert_allclose(scan_data.max(), 0.9139, rtol=1e-3) + assert_allclose(scan_data.mean(), 0.2828, rtol=1e-3) + + +def test_line_profile_rgb(): + """ Test a line profile using an ndim=3 image""" + plugin = setup_line_profile(data.chelsea()) + for i in range(6): + plugin.line_tool._thicken_scan_line() + line_image, scan_data = plugin.output() + assert_equal(line_image[line_image == 128].size, 755) + assert_equal(line_image[line_image == 255].size, 151) + assert_equal(line_image.shape, (300, 451)) + assert_equal(scan_data.shape, (152, 3)) + assert_allclose(scan_data.max(), 0.772, rtol=1e-3) + assert_allclose(scan_data.mean(), 0.4355, rtol=1e-3) + + +if __name__ == "__main__": + from numpy.testing import run_module_suite + run_module_suite()