Files
scikit-image/skimage/viewer/plugins/lineprofile.py
T
Tony S Yu f47312a3d1 API Change: Attach ImageViewer to Plugin after init.
Plugin is now added to the viewer using an inplace add on the viewer instead of on initialization of the plugin. This change means that operations requiring the viewer must be delayed until attach operation.
2012-07-23 00:12:21 -04:00

245 lines
8.3 KiB
Python

import numpy as np
import scipy.ndimage as ndi
from skimage.util.dtype import dtype_range
from .plotplugin import PlotPlugin
__all__ = ['LineProfile']
class LineProfile(PlotPlugin):
"""Plugin to compute interpolated intensity under a scan line on an image.
Parameters
----------
image_viewer : ImageViewer instance.
Window containing image used in measurement.
useblit : bool
If True, use blitting to speed up animation. Only available on some
backends. If None, set to True when using Agg backend, otherwise False.
linewidth : float
Line width for interpolation. Wider lines average over more pixels.
epsilon : float
Maximum pixel distance allowed when selecting end point of scan line.
limits : tuple or {None, 'image', 'dtype'}
(minimum, maximum) intensity limits for plotted profile. The following
special values are defined:
None : rescale based on min/max intensity along selected scan line.
'image' : fixed scale based on min/max intensity in image.
'dtype' : fixed scale based on min/max intensity of image dtype.
"""
name = 'Line Profile'
draws_on_image = True
def __init__(self, useblit=None, linewidth=1, epsilon=5, limits='image'):
super(LineProfile, self).__init__(height=200, width=600,
useblit=useblit)
self.linewidth = linewidth
self.epsilon = epsilon
self._active_pt = None
self._limit_type = limits
self.line_kwargs = dict(color='y', lw=linewidth, alpha=0.5, marker='s',
markersize=5, solid_capstyle='butt')
print self.help()
def attach(self, image_viewer):
super(LineProfile, self).attach(image_viewer)
image = image_viewer.original_image
if self._limit_type == 'image':
self.limits = (np.min(image), np.max(image))
elif self._limit_type == 'dtype':
self.self._limit_type = dtype_range[image.dtype.type]
elif self._limit_type is None or len(self._limit_type) == 2:
self.limits = self._limit_type
else:
raise ValueError("Unrecognized `limits`: %s" % self._limit_type)
if not self._limit_type is None:
self.ax.set_ylim(self.limits)
h, w = image.shape
self._init_end_pts = np.array([[w/3, h/2], [2*w/3, h/2]])
self.end_pts = self._init_end_pts.copy()
x, y = np.transpose(self.end_pts)
self.scan_line = image_viewer.ax.plot(x, y, **self.line_kwargs)[0]
self.artists.append(self.scan_line)
scan_data = profile_line(image, self.end_pts)
self.profile = self.ax.plot(scan_data, 'k-')[0]
self._autoscale_view()
self.connect_event('key_press_event', self.on_key_press)
self.connect_event('button_press_event', self.on_mouse_press)
self.connect_event('button_release_event', self.on_mouse_release)
self.connect_event('motion_notify_event', self.on_move)
self.connect_event('scroll_event', self.on_scroll)
self.image_viewer.redraw()
def help(self):
helpstr = ("Line profile tool",
"+ and - keys or mouse scroll changes width of scan line.",
"Select and drag ends of the scan line to adjust it.")
return '\n'.join(helpstr)
def get_profile(self):
"""Return intensity profile of the selected line.
Returns
-------
end_pts: (2, 2) array
The positions ((x1, y1), (x2, y2)) of the line ends.
profile: 1d array
Profile of intensity values.
"""
end_pts = self.scan_line.get_xydata()
profile = self.profile.get_ydata()
return end_pts, profile
def on_scroll(self, event):
if not event.inaxes: return
if event.button == 'up':
self._thicken_scan_line()
elif event.button == 'down':
self._shrink_scan_line()
def on_key_press(self, event):
if not event.inaxes: return
elif event.key == '+':
self._thicken_scan_line()
elif event.key == '-':
self._shrink_scan_line()
elif event.key == 'r':
self.reset()
def _thicken_scan_line(self):
self.linewidth += 1
self.line_changed(None, None)
def _shrink_scan_line(self):
if self.linewidth > 1:
self.linewidth -= 1
self.line_changed(None, None)
def _autoscale_view(self):
if self.limits is None:
self.ax.autoscale_view(tight=True)
else:
self.ax.autoscale_view(scaley=False, tight=True)
def get_pt_under_cursor(self, event):
"""Return index of the end point under cursor, if sufficiently close"""
xy = np.asarray(self.scan_line.get_xydata())
xyt = self.scan_line.get_transform().transform(xy)
xt, yt = xyt[:, 0], xyt[:, 1]
d = np.sqrt((xt - event.x)**2 + (yt - event.y)**2)
indseq = np.nonzero(np.equal(d, np.amin(d)))[0]
ind = indseq[0]
if d[ind] >= self.epsilon:
ind = None
return ind
def on_mouse_press(self, event):
if event.button != 1: return
if event.inaxes==None: return
self._active_pt = self.get_pt_under_cursor(event)
def on_mouse_release(self, event):
if event.button != 1: return
self._active_pt = None
def on_move(self, event):
if event.button != 1: return
if self._active_pt is None: return
if not self.image_viewer.ax.in_axes(event): return
x,y = event.xdata, event.ydata
self.line_changed(x, y)
def reset(self):
self.end_pts = self._init_end_pts.copy()
self.scan_line.set_data(np.transpose(self.end_pts))
self.line_changed(None, None)
def line_changed(self, x, y):
if x is not None:
self.end_pts[self._active_pt, :] = x, y
self.scan_line.set_data(np.transpose(self.end_pts))
self.scan_line.set_linewidth(self.linewidth)
scan = profile_line(self.image_viewer.original_image, self.end_pts,
linewidth=self.linewidth)
self.profile.set_xdata(np.arange(scan.shape[0]))
self.profile.set_ydata(scan)
self.ax.relim()
if self.useblit:
self.image_viewer.canvas.restore_region(self.img_background)
self.ax.draw_artist(self.scan_line)
self.ax.draw_artist(self.profile)
self.image_viewer.canvas.blit(self.image_viewer.ax.bbox)
self._autoscale_view()
self.image_viewer.redraw()
self.redraw()
def profile_line(img, end_pts, linewidth=1):
"""Return the intensity profile of an image measured along a scan line.
Parameters
----------
img : 2d array
The image.
end_pts: (2, 2) list
End points ((x1, y1), (x2, y2)) of scan line.
linewidth: int
Width of the scan, perpendicular to the line
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.
"""
point1, point2 = end_pts
x1, y1 = point1 = np.asarray(point1, dtype = float)
x2, y2 = point2 = np.asarray(point2, dtype = float)
dx, dy = point2 - point1
# Quick calculation if perfectly horizontal or vertical (remove?)
if x1 == x2:
pixels = img[min(y1, y2) : max(y1, y2)+1,
x1 - linewidth / 2 : x1 + linewidth / 2 + 1]
intensities = pixels.mean(axis = 1)
return intensities
elif y1 == y2:
pixels = img[y1 - linewidth / 2 : y1 + linewidth / 2 + 1,
min(x1, x2) : max(x1, x2)+1]
intensities = pixels.mean(axis = 0)
return intensities
theta = np.arctan2(dy,dx)
a = dy/dx
b = y1 - a * x1
length = np.hypot(dx, dy)
line_x = np.linspace(min(x1, x2), max(x1, x2), np.ceil(length))
line_y = line_x * a + b
y_width = abs(linewidth * np.cos(theta)/2)
perp_ys = np.array([np.linspace(yi - y_width,
yi + y_width, linewidth) for yi in line_y])
perp_xs = - a * perp_ys + (line_x + a * line_y)[:, np.newaxis]
perp_lines = np.array([perp_ys, perp_xs])
pixels = ndi.map_coordinates(img, perp_lines)
intensities = pixels.mean(axis=1)
return intensities