mirror of
https://github.com/wassname/scikit-image.git
synced 2026-07-01 17:45:54 +08:00
41ea3ba7fd
I saved a copy of the line profile plugin in a branch and then deleted the plugin from the main qtmpl-viewer branch. Unfortunately, I forgot that rebasing off the main branch would erase the plugin. This commit just adds the plugin back.
242 lines
8.2 KiB
Python
242 lines
8.2 KiB
Python
import numpy as np
|
|
import scipy.ndimage as ndi
|
|
from skimage.util.dtype import dtype_range
|
|
|
|
from .plotplugin import PlotPlugin
|
|
|
|
|
|
__all__ = ['LineProfile']
|
|
|
|
#TODO: Extract line tool and add it to a new `canvastools` subpackage.
|
|
|
|
class LineProfile(PlotPlugin):
|
|
"""Plugin to compute interpolated intensity under a scan line on an image.
|
|
|
|
See PlotPlugin and Plugin classes for additional details.
|
|
|
|
Parameters
|
|
----------
|
|
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, linewidth=1, epsilon=5, limits='image', **kwargs):
|
|
super(LineProfile, self).__init__(**kwargs)
|
|
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_image_event('key_press_event', self.on_key_press)
|
|
self.connect_image_event('button_press_event', self.on_mouse_press)
|
|
self.connect_image_event('button_release_event', self.on_mouse_release)
|
|
self.connect_image_event('motion_notify_event', self.on_move)
|
|
self.connect_image_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
|