From 41ea3ba7fd3af2ae95e424230eea946c4443663c Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sat, 28 Jul 2012 11:29:28 -0400 Subject: [PATCH] Add line profile plugin back in. 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. --- skimage/viewer/plugins/lineprofile.py | 241 +++++++++++++++++++++++++ skimage/viewer/plugins/plotplugin.py | 50 +++++ viewer_examples/plugins/lineprofile.py | 9 + 3 files changed, 300 insertions(+) create mode 100644 skimage/viewer/plugins/lineprofile.py create mode 100644 skimage/viewer/plugins/plotplugin.py create mode 100644 viewer_examples/plugins/lineprofile.py diff --git a/skimage/viewer/plugins/lineprofile.py b/skimage/viewer/plugins/lineprofile.py new file mode 100644 index 00000000..eaedb90c --- /dev/null +++ b/skimage/viewer/plugins/lineprofile.py @@ -0,0 +1,241 @@ +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 diff --git a/skimage/viewer/plugins/plotplugin.py b/skimage/viewer/plugins/plotplugin.py new file mode 100644 index 00000000..fa06088d --- /dev/null +++ b/skimage/viewer/plugins/plotplugin.py @@ -0,0 +1,50 @@ +import numpy as np +from PyQt4 import QtGui + +import matplotlib.pyplot as plt + +from ..utils import MatplotlibCanvas +from .base import Plugin + + +class PlotCanvas(MatplotlibCanvas): + """Canvas for displaying images. + + This canvas derives from Matplotlib, and has attributes `fig` and `ax`, + which point to Matplotlib figure and axes. + """ + def __init__(self, parent, height, width, **kwargs): + self.fig, self.ax = plt.subplots(figsize=(height, width), **kwargs) + super(PlotCanvas, self).__init__(parent, self.fig, **kwargs) + self.setMinimumHeight(150) + +class PlotPlugin(Plugin): + """Plugin for ImageViewer that contains a plot canvas. + + Base class for plugins that contain a Matplotlib plot canvas, which can, + for example, display an image histogram. + + See base Plugin class for additional details. + """ + + def attach(self, image_viewer): + super(PlotPlugin, self).attach(image_viewer) + # Add plot for displaying intensity profile. + self.add_plot() + + def redraw(self): + """Redraw plot.""" + self.canvas.draw_idle() + + def add_plot(self, height=4, width=4): + self.canvas = PlotCanvas(self, height, width) + self.fig = self.canvas.fig + #TODO: Converted color is slightly different than Qt background. + qpalette = QtGui.QPalette() + qcolor = qpalette.color(QtGui.QPalette.Window) + bgcolor = qcolor.toRgb().value() + if np.isscalar(bgcolor): + bgcolor = str(bgcolor / 255.) + self.fig.patch.set_facecolor(bgcolor) + self.ax = self.canvas.ax + self.layout.addWidget(self.canvas, self.row, 0) diff --git a/viewer_examples/plugins/lineprofile.py b/viewer_examples/plugins/lineprofile.py new file mode 100644 index 00000000..2f1b2cdc --- /dev/null +++ b/viewer_examples/plugins/lineprofile.py @@ -0,0 +1,9 @@ +from skimage import data +from skimage.viewer import ImageViewer +from skimage.viewer.plugins.lineprofile import LineProfile + + +image = data.camera() +viewer = ImageViewer(image) +viewer += LineProfile() +viewer.show()