From 124e38751ca15137b1d896e3ab0d9bc646e945f8 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Tue, 28 May 2013 23:34:52 -0500 Subject: [PATCH 001/736] Fix `RequiredAttrs` definition. The example in python's descriptor tutorial creates a singleton so multiple, instances share the same attribute. This update fixes the issue based on [1]. [1] http://stackoverflow.com/questions/8718052/where-does-a-python-descriptors-state-go --- skimage/viewer/utils/core.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/skimage/viewer/utils/core.py b/skimage/viewer/utils/core.py index 0dcd4176..7ea28fab 100644 --- a/skimage/viewer/utils/core.py +++ b/skimage/viewer/utils/core.py @@ -42,17 +42,22 @@ def start_qtapp(): class RequiredAttr(object): """A class attribute that must be set before use.""" - def __init__(self, msg): + instances = dict() + + def __init__(self, msg='Required attribute not set', init_val=None): + self.instances[self, None] = init_val self.msg = msg - self.val = None def __get__(self, obj, objtype): - if self.val is None: + value = self.instances[self, obj] + if value is None: + # Should raise an error but that causes issues with the buildbot. warnings.warn(self.msg) - return self.val + self.__set__(obj, self.init_val) + return value - def __set__(self, obj, val): - self.val = val + def __set__(self, obj, value): + self.instances[self, obj] = value class LinearColormap(LinearSegmentedColormap): From e7ca4b6138443c0f52bda5485c24ed0355e36bbf Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Tue, 28 May 2013 23:51:53 -0500 Subject: [PATCH 002/736] Fix parameter name in docstring --- skimage/viewer/widgets/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/viewer/widgets/core.py b/skimage/viewer/widgets/core.py index ad69bfcf..0000bebb 100644 --- a/skimage/viewer/widgets/core.py +++ b/skimage/viewer/widgets/core.py @@ -81,7 +81,7 @@ class Slider(BaseWidget): Range of slider values. value : float Default slider value. If None, use midpoint between `low` and `high`. - value : {'float' | 'int'} + value_type : {'float' | 'int'} Numeric type of slider value. ptype : {'arg' | 'kwarg' | 'plugin'} Parameter type. From bd860b7720560f9642cc95200b3158a4c70783c3 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Tue, 28 May 2013 23:52:10 -0500 Subject: [PATCH 003/736] Add infrastructure for conneting plugin output to a viewer. --- skimage/viewer/plugins/base.py | 21 +++++++++++++++++++-- skimage/viewer/viewers/core.py | 33 +++++++++++++++++++++++---------- 2 files changed, 42 insertions(+), 12 deletions(-) diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py index aa6b585a..ab18d257 100644 --- a/skimage/viewer/plugins/base.py +++ b/skimage/viewer/plugins/base.py @@ -1,9 +1,12 @@ """ Base class for Plugins that interact with ImageViewer. """ -from ..qt import QtGui -from ..qt.QtCore import Qt +from warnings import warn +import numpy as np + +from ..qt import QtGui +from ..qt.QtCore import Qt, pyqtSignal from ..utils import RequiredAttr, init_qtapp @@ -71,6 +74,10 @@ class Plugin(QtGui.QDialog): name = 'Plugin' image_viewer = RequiredAttr("%s is not attached to ImageViewer" % name) + # Signals used when viewers are linked to the Plugin output. + image_updated = pyqtSignal(np.ndarray) + _started = pyqtSignal() + def __init__(self, image_filter=None, height=0, width=400, useblit=True): init_qtapp() super(Plugin, self).__init__() @@ -79,6 +86,9 @@ class Plugin(QtGui.QDialog): # If subclass defines `image_filter` method ignore input. if not hasattr(self, 'image_filter'): self.image_filter = image_filter + elif image_filter is not None: + warn("If the Plugin class defines an `image_filter` method, " + "then the `image_filter` argument is ignored.") self.setWindowTitle(self.name) self.layout = QtGui.QGridLayout(self) @@ -155,7 +165,9 @@ class Plugin(QtGui.QDialog): kwargs = dict([(name, self._get_value(a)) for name, a in self.keyword_arguments.iteritems()]) filtered = self.image_filter(*arguments, **kwargs) + self.display_filtered_image(filtered) + self.image_updated.emit(filtered) def _get_value(self, param): # If param is a widget, return its `val` attribute. @@ -183,6 +195,11 @@ class Plugin(QtGui.QDialog): """ setattr(self, name, value) + def show(self, main_window=True): + """Show plugin.""" + super(Plugin, self).show() + self._started.emit() + def closeEvent(self, event): """On close disconnect all artists and events from ImageViewer. diff --git a/skimage/viewer/viewers/core.py b/skimage/viewer/viewers/core.py index 9d6261c6..209e6b42 100644 --- a/skimage/viewer/viewers/core.py +++ b/skimage/viewer/viewers/core.py @@ -11,6 +11,7 @@ import numpy as np from .. import utils from ..widgets import Slider from ..utils import dialogs +from ..plugins.base import Plugin __all__ = ['ImageViewer', 'CollectionViewer'] @@ -81,6 +82,13 @@ class ImageViewer(QtGui.QMainWindow): self.main_widget = QtGui.QWidget() self.setCentralWidget(self.main_widget) + if isinstance(image, Plugin): + plugin = image + image = plugin.filtered_image + plugin.image_updated.connect(self._new_original_image) + # When plugin is started, start + plugin._started.connect(self._show) + self.fig, self.ax = utils.figimage(image) self.canvas = self.fig.canvas self.canvas.setParent(self) @@ -88,9 +96,7 @@ class ImageViewer(QtGui.QMainWindow): self.ax.autoscale(enable=False) self._image_plot = self.ax.images[0] - - self.original_image = image - self.image = image.copy() + self._new_original_image(image) self.plugins = [] self.layout = QtGui.QVBoxLayout(self.main_widget) @@ -115,8 +121,11 @@ class ImageViewer(QtGui.QMainWindow): if filename is None: return image = io.imread(filename) + self._new_original_image(image) + + def _new_original_image(self, image): self.original_image = image # update saved image - self.image = image # update displayed image + self.image = image.copy() # update displayed image def save_to_file(self): """Save current image to file. @@ -160,16 +169,20 @@ class ImageViewer(QtGui.QMainWindow): p.move(w, y) y += p.geometry().height() - def show(self): - """Show ImageViewer and attached plugins. - - This behaves much like `matplotlib.pyplot.show` and `QWidget.show`. - """ + def _show(self): self.auto_layout() for p in self.plugins: p.show() super(ImageViewer, self).show() - utils.start_qtapp() + + def show(self, main_window=True): + """Show ImageViewer and attached plugins. + + This behaves much like `matplotlib.pyplot.show` and `QWidget.show`. + """ + self._show() + if main_window: + utils.start_qtapp() def redraw(self): self.canvas.draw_idle() From 177a1fdb394eee4a41a3667b0f138a1f2d8b59ca Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Tue, 28 May 2013 23:52:48 -0500 Subject: [PATCH 004/736] Add example of connected viewers/plugins --- .../plugins/probabilistic_hough.py | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 viewer_examples/plugins/probabilistic_hough.py diff --git a/viewer_examples/plugins/probabilistic_hough.py b/viewer_examples/plugins/probabilistic_hough.py new file mode 100644 index 00000000..027136f9 --- /dev/null +++ b/viewer_examples/plugins/probabilistic_hough.py @@ -0,0 +1,45 @@ +import numpy as np + +from skimage import data +from skimage import draw +from skimage.transform import probabilistic_hough_line + +from skimage.viewer import ImageViewer +from skimage.viewer.widgets import Slider +from skimage.viewer.plugins.overlayplugin import OverlayPlugin +from skimage.viewer.plugins.canny import CannyPlugin + + +def line_image(shape, lines): + image = np.zeros(shape, dtype=bool) + for end_points in lines: + # hough lines returns (x, y) points, draw.line wants (row, columns) + end_points = np.asarray(end_points)[:, ::-1] + image[draw.line(*np.ravel(end_points))] = 1 + return image + + +def hough_lines(image, *args, **kwargs): + # Set threshold to 0.5 since we're working with a binary image (from canny) + lines = probabilistic_hough_line(image, threshold=0.5, *args, **kwargs) + image = line_image(image.shape, lines) + return image + + +image = data.camera() +canny_viewer = ImageViewer(image) +canny_plugin = CannyPlugin() +canny_viewer += canny_plugin + +hough_plugin = OverlayPlugin(image_filter=hough_lines) + +hough_plugin += Slider('line length', 0, 100, update_on='release') +hough_plugin += Slider('line gap', 0, 20, update_on='release') + +# Passing a plugin to a viewer connects the output of the plugin to the viewer. +hough_viewer = ImageViewer(canny_plugin) +hough_viewer += hough_plugin + +# Show viewers displays both viewers since `hough_viewer` is connected to +# `canny_viewer` through `canny_plugin` +canny_viewer.show() From 07630a93e6216d63915d4d61c842d7fd2e0433a3 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Thu, 30 May 2013 22:21:21 -0500 Subject: [PATCH 005/736] Dock plugins to image viewer. --- skimage/viewer/plugins/base.py | 5 +- skimage/viewer/viewers/core.py | 57 +++++++++++++++++----- viewer_examples/plugins/color_histogram.py | 2 +- 3 files changed, 50 insertions(+), 14 deletions(-) diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py index ab18d257..d4873377 100644 --- a/skimage/viewer/plugins/base.py +++ b/skimage/viewer/plugins/base.py @@ -78,10 +78,13 @@ class Plugin(QtGui.QDialog): image_updated = pyqtSignal(np.ndarray) _started = pyqtSignal() - def __init__(self, image_filter=None, height=0, width=400, useblit=True): + def __init__(self, image_filter=None, height=0, width=400, useblit=True, + dock='bottom'): init_qtapp() super(Plugin, self).__init__() + self.dock = dock + self.image_viewer = None # If subclass defines `image_filter` method ignore input. if not hasattr(self, 'image_filter'): diff --git a/skimage/viewer/viewers/core.py b/skimage/viewer/viewers/core.py index 209e6b42..f49d4b1a 100644 --- a/skimage/viewer/viewers/core.py +++ b/skimage/viewer/viewers/core.py @@ -2,7 +2,7 @@ ImageViewer class for viewing and interacting with images. """ from ..qt import QtGui -from ..qt import QtCore +from ..qt.QtCore import Qt from skimage import io, img_as_float from skimage.util.dtype import dtype_range @@ -17,6 +17,12 @@ from ..plugins.base import Plugin __all__ = ['ImageViewer', 'CollectionViewer'] +dock_areas = {'top': Qt.TopDockWidgetArea, + 'bottom': Qt.BottomDockWidgetArea, + 'left': Qt.LeftDockWidgetArea, + 'right': Qt.RightDockWidgetArea} + + def mpl_image_to_rgba(mpl_image): """Return RGB image from the given matplotlib image object. @@ -67,16 +73,16 @@ class ImageViewer(QtGui.QMainWindow): #TODO: Add ImageViewer to skimage.io window manager - self.setAttribute(QtCore.Qt.WA_DeleteOnClose) + self.setAttribute(Qt.WA_DeleteOnClose) self.setWindowTitle("Image Viewer") self.file_menu = QtGui.QMenu('&File', self) self.file_menu.addAction('Open file', self.open_file, - QtCore.Qt.CTRL + QtCore.Qt.Key_O) + Qt.CTRL + Qt.Key_O) self.file_menu.addAction('Save to file', self.save_to_file, - QtCore.Qt.CTRL + QtCore.Qt.Key_S) + Qt.CTRL + Qt.Key_S) self.file_menu.addAction('Quit', self.close, - QtCore.Qt.CTRL + QtCore.Qt.Key_Q) + Qt.CTRL + Qt.Key_Q) self.menuBar().addMenu(self.file_menu) self.main_widget = QtGui.QWidget() @@ -113,8 +119,33 @@ class ImageViewer(QtGui.QMainWindow): def __add__(self, plugin): """Add plugin to ImageViewer""" plugin.attach(self) + if plugin.dock: + location = dock_areas[plugin.dock] + dock_location = Qt.DockWidgetArea(location) + dock = QtGui.QDockWidget() + dock.setWidget(plugin) + self.addDockWidget(dock_location, dock) + + horiz = (dock_areas['left'], dock_areas['right']) + dimension = 'width' if location in horiz else 'height' + self._add_widget_size(plugin, dimension=dimension) + return self + def _add_widget_size(self, widget, dimension='width'): + widget_size = widget.sizeHint() + viewer_size = self.frameGeometry() + + dx = dy = 0 + if dimension == 'width': + dx = widget_size.width() + elif dimension == 'height': + dy = widget_size.height() + + w = viewer_size.width() + h = viewer_size.height() + self.resize(w + dx, h + dy) + def open_file(self): """Open image file and display in viewer.""" filename = dialogs.open_file_dialog() @@ -160,14 +191,16 @@ class ImageViewer(QtGui.QMainWindow): def auto_layout(self): """Move viewer to top-left and align plugin on right edge of viewer.""" - size = self.geometry() + size = self.frameGeometry() self.move(0, 0) - w = size.width() - y = 0 - #TODO: Layout isn't quite correct for multiple plugins (overlaps). - for p in self.plugins: - p.move(w, y) - y += p.geometry().height() + # w = size.width() + # h = size.height() + # x = 0 + # y = h + # #TODO: Layout isn't quite correct for multiple plugins (overlaps). + # for p in self.plugins: + # p.move(x, y) + # y += p.frameGeometry().height() def _show(self): self.auto_layout() diff --git a/viewer_examples/plugins/color_histogram.py b/viewer_examples/plugins/color_histogram.py index 0b654b3e..6b091d69 100644 --- a/viewer_examples/plugins/color_histogram.py +++ b/viewer_examples/plugins/color_histogram.py @@ -5,5 +5,5 @@ from skimage import data image = data.load('color.png') viewer = ImageViewer(image) -viewer += ColorHistogram() +viewer += ColorHistogram(dock='right') viewer.show() From e373e13f03350ccce888d95a9b686cd651050f5a Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Thu, 30 May 2013 22:21:55 -0500 Subject: [PATCH 006/736] Fix sizing of PlotPlugin --- skimage/viewer/plugins/plotplugin.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/skimage/viewer/plugins/plotplugin.py b/skimage/viewer/plugins/plotplugin.py index c120756c..0ce5df73 100644 --- a/skimage/viewer/plugins/plotplugin.py +++ b/skimage/viewer/plugins/plotplugin.py @@ -17,6 +17,13 @@ class PlotPlugin(Plugin): See base Plugin class for additional details. """ + def __init__(self, image_filter=None, height=150, width=400, **kwargs): + super(PlotPlugin, self).__init__(image_filter=image_filter, + height=height, width=width, **kwargs) + + self._height = height + self._width = width + def attach(self, image_viewer): super(PlotPlugin, self).attach(image_viewer) # Add plot for displaying intensity profile. @@ -26,10 +33,12 @@ class PlotPlugin(Plugin): """Redraw plot.""" self.canvas.draw_idle() - def add_plot(self, height=4, width=4): - self.fig, self.ax = new_plot(figsize=(height, width)) + def add_plot(self): + self.fig, self.ax = new_plot() + self.fig.set_figwidth(self._width / float(self.fig.dpi)) + self.fig.set_figheight(self._height / float(self.fig.dpi)) + self.canvas = self.fig.canvas - self.canvas.setMinimumHeight(150) #TODO: Converted color is slightly different than Qt background. qpalette = QtGui.QPalette() qcolor = qpalette.color(QtGui.QPalette.Window) From 16b6411059fcb1c92b20db51c500ea698052d8da Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Thu, 30 May 2013 22:25:02 -0500 Subject: [PATCH 007/736] Display title in docked plugins --- skimage/viewer/viewers/core.py | 1 + viewer_examples/plugins/probabilistic_hough.py | 1 + 2 files changed, 2 insertions(+) diff --git a/skimage/viewer/viewers/core.py b/skimage/viewer/viewers/core.py index f49d4b1a..b4ad4c4d 100644 --- a/skimage/viewer/viewers/core.py +++ b/skimage/viewer/viewers/core.py @@ -124,6 +124,7 @@ class ImageViewer(QtGui.QMainWindow): dock_location = Qt.DockWidgetArea(location) dock = QtGui.QDockWidget() dock.setWidget(plugin) + dock.setWindowTitle(plugin.name) self.addDockWidget(dock_location, dock) horiz = (dock_areas['left'], dock_areas['right']) diff --git a/viewer_examples/plugins/probabilistic_hough.py b/viewer_examples/plugins/probabilistic_hough.py index 027136f9..5564ba9f 100644 --- a/viewer_examples/plugins/probabilistic_hough.py +++ b/viewer_examples/plugins/probabilistic_hough.py @@ -32,6 +32,7 @@ canny_plugin = CannyPlugin() canny_viewer += canny_plugin hough_plugin = OverlayPlugin(image_filter=hough_lines) +hough_plugin.name = 'Hough Lines' hough_plugin += Slider('line length', 0, 100, update_on='release') hough_plugin += Slider('line gap', 0, 20, update_on='release') From 55386ec785f960bc49778e5c45ca830398efe736 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Thu, 30 May 2013 22:37:36 -0500 Subject: [PATCH 008/736] Add smart window-layout for multi-viewer display --- skimage/viewer/plugins/base.py | 6 ++++-- skimage/viewer/viewers/core.py | 17 ++--------------- 2 files changed, 6 insertions(+), 17 deletions(-) diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py index d4873377..85d75b95 100644 --- a/skimage/viewer/plugins/base.py +++ b/skimage/viewer/plugins/base.py @@ -76,7 +76,7 @@ class Plugin(QtGui.QDialog): # Signals used when viewers are linked to the Plugin output. image_updated = pyqtSignal(np.ndarray) - _started = pyqtSignal() + _started = pyqtSignal(int) def __init__(self, image_filter=None, height=0, width=400, useblit=True, dock='bottom'): @@ -201,7 +201,9 @@ class Plugin(QtGui.QDialog): def show(self, main_window=True): """Show plugin.""" super(Plugin, self).show() - self._started.emit() + size = self.frameGeometry() + x_hint = size.x() + size.width() + self._started.emit(x_hint) def closeEvent(self, event): """On close disconnect all artists and events from ImageViewer. diff --git a/skimage/viewer/viewers/core.py b/skimage/viewer/viewers/core.py index b4ad4c4d..d60e5693 100644 --- a/skimage/viewer/viewers/core.py +++ b/skimage/viewer/viewers/core.py @@ -190,21 +190,8 @@ class ImageViewer(QtGui.QMainWindow): def closeEvent(self, event): self.close() - def auto_layout(self): - """Move viewer to top-left and align plugin on right edge of viewer.""" - size = self.frameGeometry() - self.move(0, 0) - # w = size.width() - # h = size.height() - # x = 0 - # y = h - # #TODO: Layout isn't quite correct for multiple plugins (overlaps). - # for p in self.plugins: - # p.move(x, y) - # y += p.frameGeometry().height() - - def _show(self): - self.auto_layout() + def _show(self, x=0): + self.move(x, 0) for p in self.plugins: p.show() super(ImageViewer, self).show() From 3e3ead7d9eb05c2fd713d83510cf08b46cf21f15 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Thu, 30 May 2013 23:14:30 -0500 Subject: [PATCH 009/736] Add mock pyqtSignal to try to get Travis to build --- skimage/viewer/qt/QtCore.py | 1 + 1 file changed, 1 insertion(+) diff --git a/skimage/viewer/qt/QtCore.py b/skimage/viewer/qt/QtCore.py index 897b8760..e0def041 100644 --- a/skimage/viewer/qt/QtCore.py +++ b/skimage/viewer/qt/QtCore.py @@ -7,3 +7,4 @@ elif qt_api == 'pyqt': else: # Mock objects Qt = None + pyqtSignal = None From 86c2c1a37c18e260bee8e399f67b997b53acedad Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Thu, 30 May 2013 23:38:26 -0500 Subject: [PATCH 010/736] Second attempt to get Travis to pass --- skimage/viewer/qt/QtCore.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/skimage/viewer/qt/QtCore.py b/skimage/viewer/qt/QtCore.py index e0def041..e0aa923a 100644 --- a/skimage/viewer/qt/QtCore.py +++ b/skimage/viewer/qt/QtCore.py @@ -7,4 +7,5 @@ elif qt_api == 'pyqt': else: # Mock objects Qt = None - pyqtSignal = None + def pyqtSignal(*args, **kwargs): + pass From 9393eac1b0a4808391de4a792cb68252fb50329d Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Fri, 31 May 2013 00:06:59 -0500 Subject: [PATCH 011/736] Third attempt at getting Travis to build --- skimage/viewer/qt/QtCore.py | 2 -- skimage/viewer/viewers/core.py | 16 ++++++++-------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/skimage/viewer/qt/QtCore.py b/skimage/viewer/qt/QtCore.py index e0aa923a..897b8760 100644 --- a/skimage/viewer/qt/QtCore.py +++ b/skimage/viewer/qt/QtCore.py @@ -7,5 +7,3 @@ elif qt_api == 'pyqt': else: # Mock objects Qt = None - def pyqtSignal(*args, **kwargs): - pass diff --git a/skimage/viewer/viewers/core.py b/skimage/viewer/viewers/core.py index d60e5693..91362f47 100644 --- a/skimage/viewer/viewers/core.py +++ b/skimage/viewer/viewers/core.py @@ -17,12 +17,6 @@ from ..plugins.base import Plugin __all__ = ['ImageViewer', 'CollectionViewer'] -dock_areas = {'top': Qt.TopDockWidgetArea, - 'bottom': Qt.BottomDockWidgetArea, - 'left': Qt.LeftDockWidgetArea, - 'right': Qt.RightDockWidgetArea} - - def mpl_image_to_rgba(mpl_image): """Return RGB image from the given matplotlib image object. @@ -66,6 +60,12 @@ class ImageViewer(QtGui.QMainWindow): >>> # viewer.show() """ + + dock_areas = {'top': Qt.TopDockWidgetArea, + 'bottom': Qt.BottomDockWidgetArea, + 'left': Qt.LeftDockWidgetArea, + 'right': Qt.RightDockWidgetArea} + def __init__(self, image): # Start main loop utils.init_qtapp() @@ -120,14 +120,14 @@ class ImageViewer(QtGui.QMainWindow): """Add plugin to ImageViewer""" plugin.attach(self) if plugin.dock: - location = dock_areas[plugin.dock] + location = self.dock_areas[plugin.dock] dock_location = Qt.DockWidgetArea(location) dock = QtGui.QDockWidget() dock.setWidget(plugin) dock.setWindowTitle(plugin.name) self.addDockWidget(dock_location, dock) - horiz = (dock_areas['left'], dock_areas['right']) + horiz = (self.dock_areas['left'], self.dock_areas['right']) dimension = 'width' if location in horiz else 'height' self._add_widget_size(plugin, dimension=dimension) From 87afe3e175f06cba74d27c2be300b382ebdb6d04 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Fri, 31 May 2013 00:14:25 -0500 Subject: [PATCH 012/736] Accidental deletion --- skimage/viewer/qt/QtCore.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/skimage/viewer/qt/QtCore.py b/skimage/viewer/qt/QtCore.py index 897b8760..e0aa923a 100644 --- a/skimage/viewer/qt/QtCore.py +++ b/skimage/viewer/qt/QtCore.py @@ -7,3 +7,5 @@ elif qt_api == 'pyqt': else: # Mock objects Qt = None + def pyqtSignal(*args, **kwargs): + pass From 484e5693b2f3e0bc8c238cd64afeaad17bfa6673 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Fri, 31 May 2013 08:15:26 -0500 Subject: [PATCH 013/736] Add attributes to Mock object to fix Travis build --- skimage/viewer/qt/QtCore.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/skimage/viewer/qt/QtCore.py b/skimage/viewer/qt/QtCore.py index e0aa923a..60c5566e 100644 --- a/skimage/viewer/qt/QtCore.py +++ b/skimage/viewer/qt/QtCore.py @@ -5,7 +5,12 @@ if qt_api == 'pyside': elif qt_api == 'pyqt': from PyQt4.QtCore import * else: - # Mock objects - Qt = None + # Mock objects for buildbot (which doesn't have Qt, but imports viewer). + class Qt(object): + TopDockWidgetArea = None + BottomDockWidgetArea = None + LeftDockWidgetArea = None + RightDockWidgetArea = None + def pyqtSignal(*args, **kwargs): pass From 2ca77c42bed2a059aea5e04c207c48c2568b4fc0 Mon Sep 17 00:00:00 2001 From: tonysyu Date: Wed, 5 Jun 2013 11:22:24 -0500 Subject: [PATCH 014/736] Fix PySide compatibility for signal defs --- skimage/viewer/plugins/base.py | 6 +++--- skimage/viewer/qt/QtCore.py | 8 +++++++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py index 85d75b95..f5fbf935 100644 --- a/skimage/viewer/plugins/base.py +++ b/skimage/viewer/plugins/base.py @@ -6,7 +6,7 @@ from warnings import warn import numpy as np from ..qt import QtGui -from ..qt.QtCore import Qt, pyqtSignal +from ..qt.QtCore import Qt, Signal from ..utils import RequiredAttr, init_qtapp @@ -75,8 +75,8 @@ class Plugin(QtGui.QDialog): image_viewer = RequiredAttr("%s is not attached to ImageViewer" % name) # Signals used when viewers are linked to the Plugin output. - image_updated = pyqtSignal(np.ndarray) - _started = pyqtSignal(int) + image_updated = Signal(np.ndarray) + _started = Signal(int) def __init__(self, image_filter=None, height=0, width=400, useblit=True, dock='bottom'): diff --git a/skimage/viewer/qt/QtCore.py b/skimage/viewer/qt/QtCore.py index 60c5566e..19b92a53 100644 --- a/skimage/viewer/qt/QtCore.py +++ b/skimage/viewer/qt/QtCore.py @@ -4,6 +4,9 @@ if qt_api == 'pyside': from PySide.QtCore import * elif qt_api == 'pyqt': from PyQt4.QtCore import * + # Use pyside names for signals and slots + Signal = pyqtSignal + Slot = pyqtSlot else: # Mock objects for buildbot (which doesn't have Qt, but imports viewer). class Qt(object): @@ -12,5 +15,8 @@ else: LeftDockWidgetArea = None RightDockWidgetArea = None - def pyqtSignal(*args, **kwargs): + def Signal(*args, **kwargs): + pass + + def Slot(*args, **kwargs): pass From cc2f1854b5d1c39ec6b899d902dcf8a762bd814f Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sat, 8 Jun 2013 19:06:23 -0500 Subject: [PATCH 015/736] Fix plugin interaction with CollectionViewer * Signal updates to original image when image changed in CollectionViewer. * Update plugin arguments for the filter. * Also fixes image updates when opening a new image from the file menu. --- skimage/viewer/plugins/base.py | 16 +++++++++++++--- skimage/viewer/viewers/core.py | 18 ++++++++++++------ 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py index f5fbf935..1d655d61 100644 --- a/skimage/viewer/plugins/base.py +++ b/skimage/viewer/plugins/base.py @@ -75,7 +75,7 @@ class Plugin(QtGui.QDialog): image_viewer = RequiredAttr("%s is not attached to ImageViewer" % name) # Signals used when viewers are linked to the Plugin output. - image_updated = Signal(np.ndarray) + image_changed = Signal(np.ndarray) _started = Signal(int) def __init__(self, image_filter=None, height=0, width=400, useblit=True, @@ -122,7 +122,7 @@ class Plugin(QtGui.QDialog): self.image_viewer = image_viewer self.image_viewer.plugins.append(self) #TODO: Always passing image as first argument may be bad assumption. - self.arguments.append(self.image_viewer.original_image) + self.arguments = [self.image_viewer.original_image] # Call filter so that filtered image matches widget values self.filter_image() @@ -170,12 +170,20 @@ class Plugin(QtGui.QDialog): filtered = self.image_filter(*arguments, **kwargs) self.display_filtered_image(filtered) - self.image_updated.emit(filtered) + self.image_changed.emit(filtered) def _get_value(self, param): # If param is a widget, return its `val` attribute. return param if not hasattr(param, 'val') else param.val + def _update_original_image(self, image): + """Update the original image argument passed to the filter function. + + This method is called by the viewer when the original image is updated. + """ + self.arguments[0] = image + self.filter_image() + @property def filtered_image(self): """Return filtered image.""" @@ -201,6 +209,8 @@ class Plugin(QtGui.QDialog): def show(self, main_window=True): """Show plugin.""" super(Plugin, self).show() + + # Emit signal with x-hint so new windows can be displayed w/o overlap. size = self.frameGeometry() x_hint = size.x() + size.width() self._started.emit(x_hint) diff --git a/skimage/viewer/viewers/core.py b/skimage/viewer/viewers/core.py index 91362f47..a054da13 100644 --- a/skimage/viewer/viewers/core.py +++ b/skimage/viewer/viewers/core.py @@ -2,7 +2,7 @@ ImageViewer class for viewing and interacting with images. """ from ..qt import QtGui -from ..qt.QtCore import Qt +from ..qt.QtCore import Qt, Signal from skimage import io, img_as_float from skimage.util.dtype import dtype_range @@ -66,6 +66,9 @@ class ImageViewer(QtGui.QMainWindow): 'left': Qt.LeftDockWidgetArea, 'right': Qt.RightDockWidgetArea} + # Signal that the original image has been changed + original_image_changed = Signal(np.ndarray) + def __init__(self, image): # Start main loop utils.init_qtapp() @@ -91,7 +94,7 @@ class ImageViewer(QtGui.QMainWindow): if isinstance(image, Plugin): plugin = image image = plugin.filtered_image - plugin.image_updated.connect(self._new_original_image) + plugin.image_changed.connect(self._update_original_image) # When plugin is started, start plugin._started.connect(self._show) @@ -102,7 +105,7 @@ class ImageViewer(QtGui.QMainWindow): self.ax.autoscale(enable=False) self._image_plot = self.ax.images[0] - self._new_original_image(image) + self._update_original_image(image) self.plugins = [] self.layout = QtGui.QVBoxLayout(self.main_widget) @@ -119,6 +122,8 @@ class ImageViewer(QtGui.QMainWindow): def __add__(self, plugin): """Add plugin to ImageViewer""" plugin.attach(self) + self.original_image_changed.connect(plugin._update_original_image) + if plugin.dock: location = self.dock_areas[plugin.dock] dock_location = Qt.DockWidgetArea(location) @@ -153,11 +158,12 @@ class ImageViewer(QtGui.QMainWindow): if filename is None: return image = io.imread(filename) - self._new_original_image(image) + self._update_original_image(image) - def _new_original_image(self, image): + def _update_original_image(self, image): self.original_image = image # update saved image self.image = image.copy() # update displayed image + self.original_image_changed.emit(image) def save_to_file(self): """Save current image to file. @@ -330,7 +336,7 @@ class CollectionViewer(ImageViewer): This method can be overridden or extended in subclasses and plugins to react to image changes. """ - self.image = image + self._update_original_image(image) def keyPressEvent(self, event): if type(event) == QtGui.QKeyEvent: From a6293fd84b1b393f5a2ed00f07131dc13371554b Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sat, 8 Jun 2013 19:11:33 -0500 Subject: [PATCH 016/736] Add example of connecting plugins to CollectionViewer --- viewer_examples/plugins/collection_plugin.py | 33 ++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 viewer_examples/plugins/collection_plugin.py diff --git a/viewer_examples/plugins/collection_plugin.py b/viewer_examples/plugins/collection_plugin.py new file mode 100644 index 00000000..f29b0132 --- /dev/null +++ b/viewer_examples/plugins/collection_plugin.py @@ -0,0 +1,33 @@ +""" +================= +Collection plugin +================= + +Demo of a CollectionViewer for viewing collections of images with the +`autolevel` rank filter connected as a plugin. + +""" +from skimage import data +from skimage.filter import rank +from skimage.morphology import disk + +from skimage.viewer import CollectionViewer +from skimage.viewer.widgets import Slider +from skimage.viewer.plugins.base import Plugin + + +# Wrap autolevel function to make the disk size a filter argument. +def autolevel(image, disk_size): + return rank.autolevel(image, disk(disk_size)) + + +img_collection = [data.camera(), data.coins(), data.text()] + +plugin = Plugin(image_filter=autolevel) +plugin += Slider('disk_size', 2, 8, value_type='int', update_on='release') +plugin.name = "Autolevel" + +viewer = CollectionViewer(img_collection) +viewer += plugin + +viewer.show() From afd1b1b835ff09e716643f5608103c61a3251104 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sat, 8 Jun 2013 19:24:50 -0500 Subject: [PATCH 017/736] Fix display of overlay plugin when original image is updated --- skimage/viewer/plugins/overlayplugin.py | 5 +++-- skimage/viewer/utils/core.py | 22 ++++++++++++++++++- skimage/viewer/viewers/core.py | 7 ++---- viewer_examples/plugins/collection_overlay.py | 21 ++++++++++++++++++ viewer_examples/plugins/collection_plugin.py | 6 ++--- 5 files changed, 50 insertions(+), 11 deletions(-) create mode 100644 viewer_examples/plugins/collection_overlay.py diff --git a/skimage/viewer/plugins/overlayplugin.py b/skimage/viewer/plugins/overlayplugin.py index 278f072c..25ea3b00 100644 --- a/skimage/viewer/plugins/overlayplugin.py +++ b/skimage/viewer/plugins/overlayplugin.py @@ -2,7 +2,7 @@ from warnings import warn from skimage.util.dtype import dtype_range from .base import Plugin -from ..utils import ClearColormap +from ..utils import ClearColormap, update_axes_image __all__ = ['OverlayPlugin'] @@ -66,7 +66,8 @@ class OverlayPlugin(Plugin): self._overlay_plot = ax.imshow(image, cmap=self.cmap, vmin=vmin, vmax=vmax) else: - self._overlay_plot.set_array(image) + update_axes_image(self._overlay_plot, image) + self.image_viewer.redraw() @property diff --git a/skimage/viewer/utils/core.py b/skimage/viewer/utils/core.py index 7ea28fab..826147eb 100644 --- a/skimage/viewer/utils/core.py +++ b/skimage/viewer/utils/core.py @@ -18,7 +18,8 @@ from ..qt import QtGui __all__ = ['init_qtapp', 'start_qtapp', 'RequiredAttr', 'figimage', - 'LinearColormap', 'ClearColormap', 'FigureCanvas', 'new_plot'] + 'LinearColormap', 'ClearColormap', 'FigureCanvas', 'new_plot', + 'update_axes_image'] QApp = None @@ -179,3 +180,22 @@ def figimage(image, scale=1, dpi=None, **kwargs): ax.set_axis_off() ax.imshow(image, **kwargs) return fig, ax + + +def update_axes_image(image_axes, image): + """Update the image displayed by an image plot. + + This sets the image plot's array and updates its shape appropriately + + Parameters + ---------- + image_axes : `matplotlib.image.AxesImage` + Image axes to update. + image : array + Image array. + """ + image_axes.set_array(image) + + # Adjust size if new image shape doesn't match the original + h, w = image.shape[:2] + image_axes.set_extent((0, w, h, 0)) diff --git a/skimage/viewer/viewers/core.py b/skimage/viewer/viewers/core.py index a054da13..874dc6ba 100644 --- a/skimage/viewer/viewers/core.py +++ b/skimage/viewer/viewers/core.py @@ -221,13 +221,10 @@ class ImageViewer(QtGui.QMainWindow): @image.setter def image(self, image): self._img = image - self._image_plot.set_array(image) + utils.update_axes_image(self._image_plot, image) - # Adjust size if new image shape doesn't match the original - h, w = image.shape[:2] - # update data coordinates (otherwise pixel coordinates are off) - self._image_plot.set_extent((0, w, h, 0)) # update display (otherwise image doesn't fill the canvas) + h, w = image.shape[:2] self.ax.set_xlim(0, w) self.ax.set_ylim(h, 0) diff --git a/viewer_examples/plugins/collection_overlay.py b/viewer_examples/plugins/collection_overlay.py new file mode 100644 index 00000000..4c7002f2 --- /dev/null +++ b/viewer_examples/plugins/collection_overlay.py @@ -0,0 +1,21 @@ +""" +============================================== +``CollectionViewer`` with an ``OverlayPlugin`` +============================================== + +Demo of a CollectionViewer for viewing collections of images with an +overlay plugin. + +""" +from skimage import data + +from skimage.viewer import CollectionViewer +from skimage.viewer.plugins.canny import CannyPlugin + + +img_collection = [data.camera(), data.coins(), data.text()] + +viewer = CollectionViewer(img_collection) +viewer += CannyPlugin() + +viewer.show() diff --git a/viewer_examples/plugins/collection_plugin.py b/viewer_examples/plugins/collection_plugin.py index f29b0132..80c7ff45 100644 --- a/viewer_examples/plugins/collection_plugin.py +++ b/viewer_examples/plugins/collection_plugin.py @@ -1,7 +1,7 @@ """ -================= -Collection plugin -================= +================================== +``CollectionViewer`` with a plugin +================================== Demo of a CollectionViewer for viewing collections of images with the `autolevel` rank filter connected as a plugin. From 0a8ff1b3641a5cb24ce5885c2b86c8eb667fa9ad Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sun, 9 Jun 2013 05:38:08 -0500 Subject: [PATCH 018/736] Fix docstring for CollectionViewer slider. --- skimage/viewer/viewers/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/viewer/viewers/core.py b/skimage/viewer/viewers/core.py index 874dc6ba..3a5f8ad3 100644 --- a/skimage/viewer/viewers/core.py +++ b/skimage/viewer/viewers/core.py @@ -285,7 +285,7 @@ class CollectionViewer(ImageViewer): ---------- image_collection : list of images List of images to be displayed. - update_on : {'on_slide' | 'on_release'} + update_on : {'move' | 'release'} Control whether image is updated on slide or release of the image slider. Using 'on_release' will give smoother behavior when displaying large images or when writing a plugin/subclass that requires heavy From d67f7008cf6c78a7edadfe44e3fee7ba58de6649 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Sun, 16 Jun 2013 01:54:55 +0800 Subject: [PATCH 019/736] First quick implementation of BRIEF Feature descriptor --- skimage/feature/_brief.py | 63 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 skimage/feature/_brief.py diff --git a/skimage/feature/_brief.py b/skimage/feature/_brief.py new file mode 100644 index 00000000..64a05373 --- /dev/null +++ b/skimage/feature/_brief.py @@ -0,0 +1,63 @@ +# TODO Normal sampling from image patch of size 49 x 49 +# TODO Tests, example, doc + +import numpy as np +from skimage.color import rgb2gray +from scipy.ndimage.filters import gaussian_filter + +KERNEL_SIZE = (9, 9) +PATCH_SIZE = (49, 49) + + +def _remove_border_keypoints(image, keypoints, dist): + + width = image.shape[0] + height = image.shape[1] + for i, j in keypoints: + if i > width - dist[0] or i < dist[0] or j < dist[1] or j > height - dist[0]: + keypoints.remove((i, j)) + return keypoints + + +def brief(image, keypoints, descriptor_size=32, mode='uniform'): + + if descriptor_size not in (16, 32, 64): + raise ValueError('Descriptor size should be either 16, 32 or 64 bytes') + + if np.squeeze(image).ndim == 3: + image = rgb2gray(image) + + keypoints = _remove_border_keypoints(image, keypoints, (PATCH_SIZE[0] / 2, PATCH_SIZE[1] / 2)) + + descriptor = np.zeros((len(keypoints), descriptor_size * 8), dtype=int) + + image = gaussian_filter(image, 2) + + if mode == 'uniform': + np.random.seed(1) + first = np.random.randint(-24, 25, (descriptor_size * 8, 2)) + np.random.seed(2) + second = np.random.randint(-24, 25, (descriptor_size * 8, 2)) + else: + #TODO mode='normal' + pass + + for i in range(len(keypoints)): + set_1 = first + keypoints[i] + set_2 = second + keypoints[i] + + for j in range(descriptor_size * 8): + if image[set_1[j, 0]][set_1[j, 1]] < image[set_2[j, 0]][set_2[j, 0]]: + descriptor[i][j] = 1 + else: + descriptor[i][j] = 0 + + return descriptor + +def hamming_distance(descriptor_1, descriptor_2): + + distance = np.zeros((len(descriptor_1), len(descriptor_2)), dtype=int) + for i in range(len(descriptor_1)): + for j in range(len(descriptor_2)): + distance[i, j] = sum(np.bitwise_xor(descriptor_1[i][:], descriptor_2[j][:])) + return distance / descriptor_1.shape[1] From a1df85d511ba906072f74f144e67dd5f71d1f543 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Tue, 18 Jun 2013 01:07:07 +0800 Subject: [PATCH 020/736] Allowing arbitrary descriptor sizes in BRIEF --- skimage/feature/_brief.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/skimage/feature/_brief.py b/skimage/feature/_brief.py index 64a05373..a02b5c49 100644 --- a/skimage/feature/_brief.py +++ b/skimage/feature/_brief.py @@ -21,9 +21,6 @@ def _remove_border_keypoints(image, keypoints, dist): def brief(image, keypoints, descriptor_size=32, mode='uniform'): - if descriptor_size not in (16, 32, 64): - raise ValueError('Descriptor size should be either 16, 32 or 64 bytes') - if np.squeeze(image).ndim == 3: image = rgb2gray(image) From 566006bdaf4e6aebf7fb5187c7ecacf4f53baa2f Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Tue, 18 Jun 2013 03:18:38 +0800 Subject: [PATCH 021/736] Removing hard-coding in BRIEF code --- skimage/feature/_brief.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skimage/feature/_brief.py b/skimage/feature/_brief.py index a02b5c49..46bd5e92 100644 --- a/skimage/feature/_brief.py +++ b/skimage/feature/_brief.py @@ -32,9 +32,9 @@ def brief(image, keypoints, descriptor_size=32, mode='uniform'): if mode == 'uniform': np.random.seed(1) - first = np.random.randint(-24, 25, (descriptor_size * 8, 2)) + first = np.random.randint(-PATCH_SIZE / 2, (PATCH_SIZE / 2) + 1, (descriptor_size * 8, 2)) np.random.seed(2) - second = np.random.randint(-24, 25, (descriptor_size * 8, 2)) + second = np.random.randint(-PATCH_SIZE / 2, (PATCH_SIZE / 2) + 1, (descriptor_size * 8, 2)) else: #TODO mode='normal' pass From 4e0cbf97feb81258663b5a8b59cb94f3afb1dff6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Tue, 18 Jun 2013 11:46:50 +0200 Subject: [PATCH 022/736] transform.radon: Add test to verify the projection center. This test is designed for issue gh-592. --- .../transform/tests/test_radon_transform.py | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/skimage/transform/tests/test_radon_transform.py b/skimage/transform/tests/test_radon_transform.py index 79cf80c5..8a85d58c 100644 --- a/skimage/transform/tests/test_radon_transform.py +++ b/skimage/transform/tests/test_radon_transform.py @@ -3,6 +3,7 @@ from __future__ import division import numpy as np from numpy.testing import * +from numpy.fft import ifftshift, ifftn import itertools from skimage.transform import * @@ -14,6 +15,30 @@ def rescale(x): return x +def check_radon_center(shape, circle): + # Determine the center of an array as defined by the fft + ft_image = np.abs(ifftshift(ifftn(np.ones(shape))))**2 + fft_center = np.unravel_index(np.argmax(ft_image), shape) + print('fft_center =', fft_center) + # Create a test image with only a single non-zero pixel at the origin + image = np.zeros(shape, dtype=np.float) + image[fft_center] = 1. + # Calculate the sinogram + theta = np.linspace(0., 180., max(shape), endpoint=False) + sinogram = radon(image, theta=theta, circle=circle) + # The sinogram should be a straight, horizontal line + sinogram_max = np.argmax(sinogram, axis=0) + print(sinogram_max) + assert np.std(sinogram_max) < 1e-6 + + +def test_radon_center(): + shapes = [(16, 16), (17, 17)] + circles = [False, True] + for shape, circle in itertools.product(shapes, circles): + yield check_radon_center, shape, circle + + def test_radon_iradon(): size = 100 debug = False From b8a20bcb59874f55ddcd6c3a8c9901e5de06a7a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Tue, 18 Jun 2013 12:03:31 +0200 Subject: [PATCH 023/736] transform.radon: Add testcases for rectangular input arrays. --- skimage/transform/tests/test_radon_transform.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/skimage/transform/tests/test_radon_transform.py b/skimage/transform/tests/test_radon_transform.py index 8a85d58c..44d3ac77 100644 --- a/skimage/transform/tests/test_radon_transform.py +++ b/skimage/transform/tests/test_radon_transform.py @@ -37,6 +37,9 @@ def test_radon_center(): circles = [False, True] for shape, circle in itertools.product(shapes, circles): yield check_radon_center, shape, circle + rectangular_shapes = [(32, 16), (33, 17)] + for shape in rectangular_shapes: + yield check_radon_center, shape, False def test_radon_iradon(): From 1d64eb59eb535d84dd19f52d2d33d110a4a35c1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Tue, 18 Jun 2013 13:36:21 +0200 Subject: [PATCH 024/736] transform.iradon: Add tests for center of projection. This is a test designed for resolving gh-592. --- .../transform/tests/test_radon_transform.py | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/skimage/transform/tests/test_radon_transform.py b/skimage/transform/tests/test_radon_transform.py index 44d3ac77..8e31c658 100644 --- a/skimage/transform/tests/test_radon_transform.py +++ b/skimage/transform/tests/test_radon_transform.py @@ -42,6 +42,52 @@ def test_radon_center(): yield check_radon_center, shape, False +def check_iradon_center(size, theta, circle): + debug = False + # Create a test sinogram corresponding to a single projection + # with a single non-zero pixel at the rotation center + if circle: + sinogram = np.zeros((size, 1), dtype=np.float) + sinogram[size // 2, 0] = 1. + else: + diagonal = int(np.ceil(np.sqrt(2) * size)) + sinogram = np.zeros((diagonal, 1), dtype=np.float) + sinogram[sinogram.shape[0] // 2, 0] = 1. + maxpoint = np.unravel_index(np.argmax(sinogram), sinogram.shape) + print('shape of generated sinogram', sinogram.shape) + print('maximum in generated sinogram', maxpoint) + # Compare reconstructions for theta=angle and theta=angle + 180; + # these should be exactly equal + reconstruction = iradon(sinogram, theta=[theta], circle=circle) + reconstruction_opposite = iradon(sinogram, theta=[theta + 180], + circle=circle) + print('rms deviance:', + np.sqrt(np.mean((reconstruction_opposite - reconstruction)**2))) + if debug: + import matplotlib.pyplot as plt + imkwargs = dict(cmap='gray', interpolation='nearest') + plt.figure() + plt.subplot(221) + plt.imshow(sinogram, **imkwargs) + plt.subplot(222) + plt.imshow(reconstruction_opposite - reconstruction, **imkwargs) + plt.subplot(223) + plt.imshow(reconstruction, **imkwargs) + plt.subplot(224) + plt.imshow(reconstruction_opposite, **imkwargs) + plt.show() + + assert np.allclose(reconstruction, reconstruction_opposite) + + +def test_iradon_center(): + sizes = [16, 17] + thetas = [0, 90] + circles = [False, True] + for size, theta, circle in itertools.product(sizes, thetas, circles): + yield check_iradon_center, size, theta, circle + + def test_radon_iradon(): size = 100 debug = False From 28de2f978abf5fa7f9a1c4d71548d36325f1da44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Tue, 18 Jun 2013 18:23:09 +0200 Subject: [PATCH 025/736] transform.radon: Remove unneccesary matrix inverse. --- skimage/transform/radon_transform.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/skimage/transform/radon_transform.py b/skimage/transform/radon_transform.py index 59e6c338..e3f46f30 100644 --- a/skimage/transform/radon_transform.py +++ b/skimage/transform/radon_transform.py @@ -98,17 +98,16 @@ def radon(image, theta=None, circle=False): [0, 0, 1]]) def build_rotation(theta): - T = -np.deg2rad(theta) + T = np.deg2rad(theta) - R = np.array([[np.cos(T), -np.sin(T), 0], - [np.sin(T), np.cos(T), 0], + R = np.array([[np.cos(T), np.sin(T), 0], + [-np.sin(T), np.cos(T), 0], [0, 0, 1]]) return shift1.dot(R).dot(shift0) for i in range(len(theta)): - rotated = _warp_fast(padded_image, - np.linalg.inv(build_rotation(-theta[i]))) + rotated = _warp_fast(padded_image, build_rotation(theta[i])) out[:, i] = rotated.sum(0)[::-1] From 380c916c9221b144686de878eb0822fd38d06525 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Tue, 18 Jun 2013 18:36:23 +0200 Subject: [PATCH 026/736] transform.radon: Use correct padding for rectangular images. --- skimage/transform/radon_transform.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/skimage/transform/radon_transform.py b/skimage/transform/radon_transform.py index e3f46f30..fffe4e6f 100644 --- a/skimage/transform/radon_transform.py +++ b/skimage/transform/radon_transform.py @@ -74,9 +74,9 @@ def radon(image, theta=None, circle=False): out = np.zeros((min(padded_image.shape), len(theta))) else: height, width = image.shape - diagonal = np.sqrt(height**2 + width**2) - heightpad = np.ceil(diagonal - height) - widthpad = np.ceil(diagonal - width) + diagonal = np.sqrt(2) * max(image.shape) + heightpad = int(np.ceil(diagonal - height)) + widthpad = int(np.ceil(diagonal - width)) padded_image = np.zeros((int(height + heightpad), int(width + widthpad))) y0 = int(np.ceil(heightpad / 2)) From a44f1d4ef92342027f2901aa77f70e86b98ad1ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Tue, 18 Jun 2013 18:49:24 +0200 Subject: [PATCH 027/736] transform.radon: Consistent definition of center of array. The center is now defined as shape[i] // 2. --- skimage/transform/radon_transform.py | 11 +++++------ skimage/transform/tests/test_radon_transform.py | 6 +----- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/skimage/transform/radon_transform.py b/skimage/transform/radon_transform.py index fffe4e6f..2733e243 100644 --- a/skimage/transform/radon_transform.py +++ b/skimage/transform/radon_transform.py @@ -79,11 +79,10 @@ def radon(image, theta=None, circle=False): widthpad = int(np.ceil(diagonal - width)) padded_image = np.zeros((int(height + heightpad), int(width + widthpad))) - y0 = int(np.ceil(heightpad / 2)) - y1 = int((np.ceil(heightpad / 2) + height)) - x0 = int((np.ceil(widthpad / 2))) - x1 = int((np.ceil(widthpad / 2) + width)) - + y0 = heightpad // 2 + y1 = y0 + height + x0 = widthpad // 2 + x1 = x0 + width padded_image[y0:y1, x0:x1] = image out = np.zeros((max(padded_image.shape), len(theta))) @@ -109,7 +108,7 @@ def radon(image, theta=None, circle=False): for i in range(len(theta)): rotated = _warp_fast(padded_image, build_rotation(theta[i])) - out[:, i] = rotated.sum(0)[::-1] + out[:, i] = rotated.sum(0) return out diff --git a/skimage/transform/tests/test_radon_transform.py b/skimage/transform/tests/test_radon_transform.py index 8e31c658..9a03b1f2 100644 --- a/skimage/transform/tests/test_radon_transform.py +++ b/skimage/transform/tests/test_radon_transform.py @@ -16,13 +16,9 @@ def rescale(x): def check_radon_center(shape, circle): - # Determine the center of an array as defined by the fft - ft_image = np.abs(ifftshift(ifftn(np.ones(shape))))**2 - fft_center = np.unravel_index(np.argmax(ft_image), shape) - print('fft_center =', fft_center) # Create a test image with only a single non-zero pixel at the origin image = np.zeros(shape, dtype=np.float) - image[fft_center] = 1. + image[(shape[0] // 2, shape[1] // 2)] = 1. # Calculate the sinogram theta = np.linspace(0., 180., max(shape), endpoint=False) sinogram = radon(image, theta=theta, circle=circle) From 364e82176f6aa40533b4f4116eb49dd0c658d3c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Tue, 18 Jun 2013 22:03:56 +0200 Subject: [PATCH 028/736] radon tests: Refactor test for circle reconstructions. --- .../transform/tests/test_radon_transform.py | 57 ++++++++++--------- 1 file changed, 29 insertions(+), 28 deletions(-) diff --git a/skimage/transform/tests/test_radon_transform.py b/skimage/transform/tests/test_radon_transform.py index 9a03b1f2..2f4884ea 100644 --- a/skimage/transform/tests/test_radon_transform.py +++ b/skimage/transform/tests/test_radon_transform.py @@ -214,40 +214,41 @@ def test_radon_circle(): assert np.all(relative_error < 3e-3) +def check_radon_iradon_circle(interpolation, shape, output_size): + # Forward and inverse radon on synthetic data + image = _random_circle(shape) + radius = min(shape) // 2 + sinogram_rectangle = radon(image, circle=False) + reconstruction_rectangle = iradon(sinogram_rectangle, + output_size=output_size, + interpolation=interpolation, + circle=False) + sinogram_circle = radon(image, circle=True) + reconstruction_circle = iradon(sinogram_circle, + output_size=output_size, + interpolation=interpolation, + circle=True) + # Crop rectangular reconstruction to match circle=True reconstruction + width = reconstruction_circle.shape[0] + excess = int(np.ceil((reconstruction_rectangle.shape[0] - width) / 2)) + s = np.s_[excess:width + excess, excess:width + excess] + reconstruction_rectangle = reconstruction_rectangle[s] + # Find the reconstruction circle, set reconstruction to zero outside + c0, c1 = np.ogrid[0:width, 0:width] + r = np.sqrt((c0 - width // 2)**2 + (c1 - width // 2)**2) + reconstruction_rectangle[r >= radius] = 0. + print(reconstruction_circle.shape) + print(reconstruction_rectangle.shape) + np.allclose(reconstruction_rectangle, reconstruction_circle) + + def test_radon_iradon_circle(): shape = (61, 79) - radius = min(shape) // 2 - image = _random_circle(shape) interpolations = ('nearest', 'linear') output_sizes = (None, min(shape), max(shape), 97) - for interpolation, output_size in itertools.product(interpolations, output_sizes): - print('interpolation =', interpolation) - print('output_size =', output_size) - # Forward and inverse radon on synthetic data - sinogram_rectangle = radon(image, circle=False) - reconstruction_rectangle = iradon(sinogram_rectangle, - output_size=output_size, - interpolation=interpolation, - circle=False) - sinogram_circle = radon(image, circle=True) - reconstruction_circle = iradon(sinogram_circle, - output_size=output_size, - interpolation=interpolation, - circle=True) - # Crop rectangular reconstruction to match circle=True reconstruction - width = reconstruction_circle.shape[0] - excess = int(np.ceil((reconstruction_rectangle.shape[0] - width) / 2)) - s = np.s_[excess:width + excess, excess:width + excess] - reconstruction_rectangle = reconstruction_rectangle[s] - # Find the reconstruction circle, set reconstruction to zero outside - c0, c1 = np.ogrid[0:width, 0:width] - r = np.sqrt((c0 - width // 2)**2 + (c1 - width // 2)**2) - reconstruction_rectangle[r >= radius] = 0. - print(reconstruction_circle.shape) - print(reconstruction_rectangle.shape) - np.allclose(reconstruction_rectangle, reconstruction_circle) + yield check_radon_iradon_circle, interpolation, shape, output_size if __name__ == "__main__": From 131cfc73edf64ec5e25ef946cb677625356e5658 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Tue, 18 Jun 2013 22:22:22 +0200 Subject: [PATCH 029/736] transform.iradon: Redefine slice and projection center. These changes should match those made to radon. --- skimage/transform/radon_transform.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/skimage/transform/radon_transform.py b/skimage/transform/radon_transform.py index 2733e243..4bd03fdb 100644 --- a/skimage/transform/radon_transform.py +++ b/skimage/transform/radon_transform.py @@ -179,7 +179,7 @@ def iradon(radon_image, theta=None, output_size=None, if circle: radon_size = int(np.ceil(np.sqrt(2) * radon_image.shape[0])) radon_image_padded = np.zeros((radon_size, radon_image.shape[1])) - radon_pad = (radon_size - radon_image.shape[0]) // 2 + radon_pad = int(np.floor((radon_size - radon_image.shape[0] - 1) / 2.)) radon_image_padded[radon_pad:radon_pad + radon_image.shape[0], :] \ = radon_image radon_image = radon_image_padded @@ -221,7 +221,7 @@ def iradon(radon_image, theta=None, output_size=None, # resize filtered image back to original size radon_filtered = radon_filtered[:radon_image.shape[0], :] reconstructed = np.zeros((output_size, output_size)) - mid_index = np.ceil(n / 2.0) + mid_index = n // 2 + 1 x = output_size y = output_size @@ -236,7 +236,7 @@ def iradon(radon_image, theta=None, output_size=None, # reconstruct image by interpolation if interpolation == "nearest": for i in range(len(theta)): - k = np.round(mid_index + xpr * np.sin(th[i]) - ypr * np.cos(th[i])) + k = np.round(mid_index + ypr * np.cos(th[i]) - xpr * np.sin(th[i])) backprojected = radon_filtered[ ((((k > 0) & (k < n)) * k) - 1).astype(np.int), i] if circle: @@ -245,7 +245,7 @@ def iradon(radon_image, theta=None, output_size=None, elif interpolation == "linear": for i in range(len(theta)): - t = xpr * np.sin(th[i]) - ypr * np.cos(th[i]) + t = ypr * np.cos(th[i]) - xpr * np.sin(th[i]) a = np.floor(t) b = mid_index + a b0 = ((((b + 1 > 0) & (b + 1 < n)) * (b + 1)) - 1).astype(np.int) From da423931b5f10bb11d6662834833cc5a7188607a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Tue, 18 Jun 2013 22:30:09 +0200 Subject: [PATCH 030/736] test_radon_transform: Add helper functions. --- .../transform/tests/test_radon_transform.py | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/skimage/transform/tests/test_radon_transform.py b/skimage/transform/tests/test_radon_transform.py index 2f4884ea..7884ef83 100644 --- a/skimage/transform/tests/test_radon_transform.py +++ b/skimage/transform/tests/test_radon_transform.py @@ -6,6 +6,35 @@ from numpy.testing import * from numpy.fft import ifftshift, ifftn import itertools from skimage.transform import * +from skimage.io import imread +from skimage import data_dir + + +__PHANTOM = imread(data_dir + "/phantom.png", as_grey=True)[::2, ::2] + +def _get_phantom(): + return __PHANTOM + + +def _debug_plot(original, result, sinogram=None): + from matplotlib import pyplot as plt + imkwargs = dict(cmap='gray', interpolation='nearest') + if sinogram is None: + plt.figure(figsize=(15, 6)) + sp = 130 + else: + plt.figure(figsize=(11, 11)) + sp = 221 + plt.subplot(sp + 0) + plt.imshow(sinogram, aspect='auto', **imkwargs) + plt.subplot(sp + 1) + plt.imshow(original, **imkwargs) + plt.subplot(sp + 2) + plt.imshow(result, vmin=original.min(), vmax=original.max(), **imkwargs) + plt.subplot(sp + 3) + plt.imshow(result - original, **imkwargs) + plt.colorbar() + plt.show() def rescale(x): From b61ff7513efd53d1fdc330b8b84ee75777a8b0cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Wed, 19 Jun 2013 00:12:14 +0200 Subject: [PATCH 031/736] transform.iradon: Refactoring for shorter functions. Will facilitate testing. --- skimage/transform/radon_transform.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/skimage/transform/radon_transform.py b/skimage/transform/radon_transform.py index 4bd03fdb..8ed11cc7 100644 --- a/skimage/transform/radon_transform.py +++ b/skimage/transform/radon_transform.py @@ -113,6 +113,14 @@ def radon(image, theta=None, circle=False): return out +def _sinogram_circle_to_square(sinogram): + size = int(np.ceil(np.sqrt(2) * sinogram.shape[0])) + sinogram_padded = np.zeros((size, sinogram.shape[1])) + pad = int(np.ceil((size - sinogram.shape[0] - 1) / 2)) + sinogram_padded[pad:pad + sinogram.shape[0], :] = sinogram + return sinogram_padded + + def iradon(radon_image, theta=None, output_size=None, filter="ramp", interpolation="linear", circle=False): """ @@ -177,12 +185,7 @@ def iradon(radon_image, theta=None, output_size=None, output_size = int(np.floor(np.sqrt((radon_image.shape[0])**2 / 2.0))) if circle: - radon_size = int(np.ceil(np.sqrt(2) * radon_image.shape[0])) - radon_image_padded = np.zeros((radon_size, radon_image.shape[1])) - radon_pad = int(np.floor((radon_size - radon_image.shape[0] - 1) / 2.)) - radon_image_padded[radon_pad:radon_pad + radon_image.shape[0], :] \ - = radon_image - radon_image = radon_image_padded + radon_image = _sinogram_circle_to_square(radon_image) n = radon_image.shape[0] From 1caafd44516714cf5c4fd21b920a345036c5bab8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Wed, 19 Jun 2013 00:13:20 +0200 Subject: [PATCH 032/736] test_radon_transform: Test sinogram conversions. --- skimage/transform/tests/test_radon_transform.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/skimage/transform/tests/test_radon_transform.py b/skimage/transform/tests/test_radon_transform.py index 7884ef83..5a7ca963 100644 --- a/skimage/transform/tests/test_radon_transform.py +++ b/skimage/transform/tests/test_radon_transform.py @@ -243,6 +243,23 @@ def test_radon_circle(): assert np.all(relative_error < 3e-3) +def check_sinogram_circle_to_square(size): + from skimage.transform.radon_transform import _sinogram_circle_to_square + image = _random_circle((size, size)) + theta = np.linspace(0., 180., size, False) + sinogram_circle = radon(image, theta, circle=True) + sinogram_square = radon(image, theta, circle=False) + sinogram_circle_to_square = _sinogram_circle_to_square(sinogram_circle) + error = abs(sinogram_square - sinogram_circle_to_square) + print(np.mean(error), np.max(error)) + assert np.allclose(sinogram_square, sinogram_circle_to_square) + + +def test_sinogram_circle_to_square(): + for size in (50, 51): + yield check_sinogram_circle_to_square, size + + def check_radon_iradon_circle(interpolation, shape, output_size): # Forward and inverse radon on synthetic data image = _random_circle(shape) From cf51de6b37b8ed535fd97a45afe102e5128d77b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Wed, 19 Jun 2013 00:17:58 +0200 Subject: [PATCH 033/736] test_radon_transform: Refactor and improve test_radon_iradon. Aside from refactoring, the Shepp-Logan phantom is now used as it is a more challenging test object. --- .../transform/tests/test_radon_transform.py | 48 +++++++++---------- 1 file changed, 22 insertions(+), 26 deletions(-) diff --git a/skimage/transform/tests/test_radon_transform.py b/skimage/transform/tests/test_radon_transform.py index 5a7ca963..bc938da8 100644 --- a/skimage/transform/tests/test_radon_transform.py +++ b/skimage/transform/tests/test_radon_transform.py @@ -113,35 +113,31 @@ def test_iradon_center(): yield check_iradon_center, size, theta, circle -def test_radon_iradon(): - size = 100 +def check_radon_iradon(interpolation_type, filter_type): debug = False - image = np.tri(size) + np.tri(size)[::-1] - for filter_type in ["ramp", "shepp-logan", "cosine", "hamming", "hann"]: - reconstructed = iradon(radon(image), filter=filter_type) + image = _get_phantom() + reconstructed = iradon(radon(image), filter=filter_type, + interpolation=interpolation_type) + delta = np.mean(np.abs(image - reconstructed)) + print('\n\tmean error:', delta) + if debug: + _debug_plot(image, reconstructed) + if filter_type == 'ramp': + if interpolation_type == 'linear': + allowed_delta = 0.02 + else: + allowed_delta = 0.03 + else: + allowed_delta = 0.05 + assert delta < allowed_delta - image = rescale(image) - reconstructed = rescale(reconstructed) - delta = np.mean(np.abs(image - reconstructed)) - if debug: - print(delta) - import matplotlib.pyplot as plt - f, (ax1, ax2) = plt.subplots(1, 2) - ax1.imshow(image, cmap=plt.cm.gray) - ax2.imshow(reconstructed, cmap=plt.cm.gray) - plt.show() - - assert delta < 0.05 - - reconstructed = iradon(radon(image), filter="ramp", - interpolation="nearest") - delta = np.mean(abs(image - reconstructed)) - assert delta < 0.05 - size = 20 - image = np.tri(size) + np.tri(size)[::-1] - reconstructed = iradon(radon(image), filter="ramp", - interpolation="nearest") +def test_radon_iradon(): + filter_types = ["ramp", "shepp-logan", "cosine", "hamming", "hann"] + interpolation_types = ["linear", "nearest"] + for interpolation_type, filter_type in \ + itertools.product(interpolation_types, filter_types): + yield check_radon_iradon, interpolation_type, filter_type def test_iradon_angles(): From e63a1fb341052019991b62aff0ea5b1cf414b4cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Wed, 19 Jun 2013 00:19:31 +0200 Subject: [PATCH 034/736] test_radon_transform: debug option for test_iradon_minimal. --- skimage/transform/tests/test_radon_transform.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/skimage/transform/tests/test_radon_transform.py b/skimage/transform/tests/test_radon_transform.py index bc938da8..1c580a4a 100644 --- a/skimage/transform/tests/test_radon_transform.py +++ b/skimage/transform/tests/test_radon_transform.py @@ -172,6 +172,7 @@ def test_radon_minimal(): """ Test for small images for various angles """ + debug = False thetas = [np.arange(180)] for theta in thetas: a = np.zeros((3, 3)) @@ -179,6 +180,8 @@ def test_radon_minimal(): p = radon(a, theta) reconstructed = iradon(p, theta) reconstructed /= np.max(reconstructed) + if debug: + _debug_plot(a, reconstructed) assert np.all(abs(a - reconstructed) < 0.4) b = np.zeros((4, 4)) @@ -186,6 +189,8 @@ def test_radon_minimal(): p = radon(b, theta) reconstructed = iradon(p, theta) reconstructed /= np.max(reconstructed) + if debug: + _debug_plot(b, reconstructed) assert np.all(abs(b - reconstructed) < 0.4) c = np.zeros((5, 5)) @@ -193,6 +198,8 @@ def test_radon_minimal(): p = radon(c, theta) reconstructed = iradon(p, theta) reconstructed /= np.max(reconstructed) + if debug: + _debug_plot(c, reconstructed) assert np.all(abs(c - reconstructed) < 0.4) From b8a6b4fa00249646212569ffbdb93134938e92f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Wed, 19 Jun 2013 00:23:55 +0200 Subject: [PATCH 035/736] radon_transform: Stylistic changes. --- skimage/transform/radon_transform.py | 29 ++++++++-------------------- 1 file changed, 8 insertions(+), 21 deletions(-) diff --git a/skimage/transform/radon_transform.py b/skimage/transform/radon_transform.py index 8ed11cc7..2cde2232 100644 --- a/skimage/transform/radon_transform.py +++ b/skimage/transform/radon_transform.py @@ -91,25 +91,20 @@ def radon(image, theta=None, circle=False): shift0 = np.array([[1, 0, -dw], [0, 1, -dh], [0, 0, 1]]) - shift1 = np.array([[1, 0, dw], [0, 1, dh], [0, 0, 1]]) def build_rotation(theta): T = np.deg2rad(theta) - R = np.array([[np.cos(T), np.sin(T), 0], [-np.sin(T), np.cos(T), 0], [0, 0, 1]]) - return shift1.dot(R).dot(shift0) for i in range(len(theta)): rotated = _warp_fast(padded_image, build_rotation(theta[i])) - out[:, i] = rotated.sum(0) - return out @@ -158,27 +153,23 @@ def iradon(radon_image, theta=None, output_size=None, Notes ----- - It applies the fourier slice theorem to reconstruct an image by + It applies the Fourier slice theorem to reconstruct an image by multiplying the frequency domain of the filter with the FFT of the projection data. This algorithm is called filtered back projection. """ if radon_image.ndim != 2: raise ValueError('The input image must be 2-D') - if theta is None: m, n = radon_image.shape theta = np.linspace(0, 180, n, endpoint=False) else: theta = np.asarray(theta) - if len(theta) != radon_image.shape[1]: raise ValueError("The given ``theta`` does not match the number of " "projections in ``radon_image``.") - - th = (np.pi / 180.0) * theta - # if output size not specified, estimate from input radon image if not output_size: + # If output size not specified, estimate from input radon image if circle: output_size = radon_image.shape[0] else: @@ -187,19 +178,19 @@ def iradon(radon_image, theta=None, output_size=None, if circle: radon_image = _sinogram_circle_to_square(radon_image) + th = (np.pi / 180.0) * theta n = radon_image.shape[0] - img = radon_image.copy() # resize image to next power of two for fourier analysis # speeds up fourier and lessens artifacts order = max(64., 2**np.ceil(np.log(2 * n) / np.log(2))) # zero pad input image img.resize((order, img.shape[1])) - # construct the fourier filter + # Construct the Fourier filter f = fftshift(abs(np.mgrid[-1:1:2 / order])).reshape(-1, 1) w = 2 * np.pi * f - # start from first element to avoid divide by zero + # Start from first element to avoid divide by zero if filter == "ramp": pass elif filter == "shepp-logan": @@ -214,14 +205,12 @@ def iradon(radon_image, theta=None, output_size=None, f[1:] = 1 else: raise ValueError("Unknown filter: %s" % filter) - filter_ft = np.tile(f, (1, len(theta))) - - # apply filter in fourier domain + # Apply filter in Fourier domain projection = fft(img, axis=0) * filter_ft radon_filtered = np.real(ifft(projection, axis=0)) - # resize filtered image back to original size + # Resize filtered image back to original size radon_filtered = radon_filtered[:radon_image.shape[0], :] reconstructed = np.zeros((output_size, output_size)) mid_index = n // 2 + 1 @@ -231,12 +220,11 @@ def iradon(radon_image, theta=None, output_size=None, [X, Y] = np.mgrid[0.0:x, 0.0:y] xpr = X - int(output_size) // 2 ypr = Y - int(output_size) // 2 - if circle: radius = (output_size - 1) // 2 reconstruction_circle = (xpr**2 + ypr**2) < radius**2 - # reconstruct image by interpolation + # Reconstruct image by interpolation if interpolation == "nearest": for i in range(len(theta)): k = np.round(mid_index + ypr * np.cos(th[i]) - xpr * np.sin(th[i])) @@ -245,7 +233,6 @@ def iradon(radon_image, theta=None, output_size=None, if circle: backprojected[~reconstruction_circle] = 0. reconstructed += backprojected - elif interpolation == "linear": for i in range(len(theta)): t = ypr * np.cos(th[i]) - xpr * np.sin(th[i]) From afaab4fea7a6bc766a06a8d9df296f3f6ec63e49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Wed, 19 Jun 2013 00:51:25 +0200 Subject: [PATCH 036/736] test_radon_transform: Refactor tests and make them stricter. --- .../transform/tests/test_radon_transform.py | 47 +++++++------------ 1 file changed, 17 insertions(+), 30 deletions(-) diff --git a/skimage/transform/tests/test_radon_transform.py b/skimage/transform/tests/test_radon_transform.py index 1c580a4a..d2e0a490 100644 --- a/skimage/transform/tests/test_radon_transform.py +++ b/skimage/transform/tests/test_radon_transform.py @@ -168,39 +168,26 @@ def test_iradon_angles(): assert delta_80 > delta_200 -def test_radon_minimal(): - """ - Test for small images for various angles - """ +def check_radon_iradon_minimal(shape, slices): debug = False - thetas = [np.arange(180)] - for theta in thetas: - a = np.zeros((3, 3)) - a[1, 1] = 1 - p = radon(a, theta) - reconstructed = iradon(p, theta) - reconstructed /= np.max(reconstructed) - if debug: - _debug_plot(a, reconstructed) - assert np.all(abs(a - reconstructed) < 0.4) + theta = np.arange(180) + image = np.zeros(shape, dtype=np.float) + image[slices] = 1. + sinogram = radon(image, theta) + reconstructed = iradon(sinogram, theta) + print('\n\tMaximum deviation:', np.max(np.abs(image - reconstructed))) + if debug: + _debug_plot(image, reconstructed) + assert np.allclose(image, reconstructed, rtol=1e-1, atol=1e-1) - b = np.zeros((4, 4)) - b[1:3, 1:3] = 1 - p = radon(b, theta) - reconstructed = iradon(p, theta) - reconstructed /= np.max(reconstructed) - if debug: - _debug_plot(b, reconstructed) - assert np.all(abs(b - reconstructed) < 0.4) - c = np.zeros((5, 5)) - c[1:3, 1:3] = 1 - p = radon(c, theta) - reconstructed = iradon(p, theta) - reconstructed /= np.max(reconstructed) - if debug: - _debug_plot(c, reconstructed) - assert np.all(abs(c - reconstructed) < 0.4) +def test_radon_iradon_minimal(): + # Each testcase is a (image shape, slice of image to set to one) tuple + testcases = [((3, 3), np.s_[1, 1]), + ((4, 4), np.s_[1:3, 1:3]), + ((5, 5), np.s_[1:3, 1:3])] + for shape, slices in testcases: + yield check_radon_iradon_minimal, shape, slices def test_reconstruct_with_wrong_angles(): From 934f1040ad67466c5ede195a01e656eb31bf5699 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Wed, 19 Jun 2013 11:31:41 +0200 Subject: [PATCH 037/736] test_radon_transform: Change test criterion for iradon_minimal. --- .../transform/tests/test_radon_transform.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/skimage/transform/tests/test_radon_transform.py b/skimage/transform/tests/test_radon_transform.py index d2e0a490..7f517801 100644 --- a/skimage/transform/tests/test_radon_transform.py +++ b/skimage/transform/tests/test_radon_transform.py @@ -177,17 +177,20 @@ def check_radon_iradon_minimal(shape, slices): reconstructed = iradon(sinogram, theta) print('\n\tMaximum deviation:', np.max(np.abs(image - reconstructed))) if debug: - _debug_plot(image, reconstructed) - assert np.allclose(image, reconstructed, rtol=1e-1, atol=1e-1) + _debug_plot(image, reconstructed, sinogram) + if image.sum() == 1: + assert (np.unravel_index(np.argmax(reconstructed), image.shape) + == np.unravel_index(np.argmax(image), image.shape)) def test_radon_iradon_minimal(): - # Each testcase is a (image shape, slice of image to set to one) tuple - testcases = [((3, 3), np.s_[1, 1]), - ((4, 4), np.s_[1:3, 1:3]), - ((5, 5), np.s_[1:3, 1:3])] - for shape, slices in testcases: - yield check_radon_iradon_minimal, shape, slices + shapes = [(3, 3), (4, 4), (5, 5)] + for shape in shapes: + c0, c1 = shape[0] // 2, shape[1] // 2 + coordinates = itertools.product((c0 - 1, c0, c0 + 1), + (c1 - 1, c1, c1 + 1)) + for coordinate in coordinates: + yield check_radon_iradon_minimal, shape, coordinate def test_reconstruct_with_wrong_angles(): From df0b060c696da4fe03322b40931833a3beabb5ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Wed, 19 Jun 2013 11:32:38 +0200 Subject: [PATCH 038/736] test_radon_transform: Change test criterion for sinogram_circle. --- skimage/transform/tests/test_radon_transform.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/skimage/transform/tests/test_radon_transform.py b/skimage/transform/tests/test_radon_transform.py index 7f517801..5eef1e1c 100644 --- a/skimage/transform/tests/test_radon_transform.py +++ b/skimage/transform/tests/test_radon_transform.py @@ -241,11 +241,17 @@ def check_sinogram_circle_to_square(size): image = _random_circle((size, size)) theta = np.linspace(0., 180., size, False) sinogram_circle = radon(image, theta, circle=True) + argmax_shape = lambda a: np.unravel_index(np.argmax(a), a.shape) + print('\n\targmax of circle:', argmax_shape(sinogram_circle)) sinogram_square = radon(image, theta, circle=False) + print('\targmax of square:', argmax_shape(sinogram_square)) sinogram_circle_to_square = _sinogram_circle_to_square(sinogram_circle) + print('\targmax of circle to square:', + argmax_shape(sinogram_circle_to_square)) error = abs(sinogram_square - sinogram_circle_to_square) print(np.mean(error), np.max(error)) - assert np.allclose(sinogram_square, sinogram_circle_to_square) + assert (argmax_shape(sinogram_square) + == argmax_shape(sinogram_circle_to_square)) def test_sinogram_circle_to_square(): From cca66a04ef50b877d219b1f719f6f16c3dfe3891 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Wed, 19 Jun 2013 12:25:39 +0200 Subject: [PATCH 039/736] transform.radon: Robust determination of center of projection. --- skimage/transform/radon_transform.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/skimage/transform/radon_transform.py b/skimage/transform/radon_transform.py index 2cde2232..6bd0f628 100644 --- a/skimage/transform/radon_transform.py +++ b/skimage/transform/radon_transform.py @@ -72,6 +72,8 @@ def radon(image, theta=None, circle=False): slices = tuple(slices) padded_image = image[slices] out = np.zeros((min(padded_image.shape), len(theta))) + dh = padded_image.shape[0] // 2 + dw = padded_image.shape[1] // 2 else: height, width = image.shape diagonal = np.sqrt(2) * max(image.shape) @@ -85,9 +87,9 @@ def radon(image, theta=None, circle=False): x1 = x0 + width padded_image[y0:y1, x0:x1] = image out = np.zeros((max(padded_image.shape), len(theta))) + dh = y0 + height // 2 + dw = x0 + width // 2 - h, w = padded_image.shape - dh, dw = h // 2, w // 2 shift0 = np.array([[1, 0, -dw], [0, 1, -dh], [0, 0, 1]]) @@ -111,7 +113,7 @@ def radon(image, theta=None, circle=False): def _sinogram_circle_to_square(sinogram): size = int(np.ceil(np.sqrt(2) * sinogram.shape[0])) sinogram_padded = np.zeros((size, sinogram.shape[1])) - pad = int(np.ceil((size - sinogram.shape[0] - 1) / 2)) + pad = (size - sinogram.shape[0]) // 2 sinogram_padded[pad:pad + sinogram.shape[0], :] = sinogram return sinogram_padded From 4b25c482452da3c04c5803e48f64bb8813d99174 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Wed, 19 Jun 2013 12:26:47 +0200 Subject: [PATCH 040/736] transform.iradon: Correct determination of center of projection. --- skimage/transform/radon_transform.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/skimage/transform/radon_transform.py b/skimage/transform/radon_transform.py index 6bd0f628..b114adad 100644 --- a/skimage/transform/radon_transform.py +++ b/skimage/transform/radon_transform.py @@ -215,7 +215,10 @@ def iradon(radon_image, theta=None, output_size=None, # Resize filtered image back to original size radon_filtered = radon_filtered[:radon_image.shape[0], :] reconstructed = np.zeros((output_size, output_size)) - mid_index = n // 2 + 1 + # Determine the center of the projections (= center of sinogram) + circle_size = int(np.floor(radon_image.shape[0] / np.sqrt(2))) + square_size = radon_image.shape[0] + mid_index = (square_size - circle_size) // 2 + circle_size // 2 + 1 x = output_size y = output_size From 186a238e4814fcb86bc8351f59a52e50f8a92583 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Wed, 19 Jun 2013 12:37:46 +0200 Subject: [PATCH 041/736] test_radon_transform: Clean up imports. --- skimage/transform/tests/test_radon_transform.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/skimage/transform/tests/test_radon_transform.py b/skimage/transform/tests/test_radon_transform.py index 5eef1e1c..c385c84c 100644 --- a/skimage/transform/tests/test_radon_transform.py +++ b/skimage/transform/tests/test_radon_transform.py @@ -2,10 +2,9 @@ from __future__ import print_function from __future__ import division import numpy as np -from numpy.testing import * -from numpy.fft import ifftshift, ifftn +from numpy.testing import assert_raises import itertools -from skimage.transform import * +from skimage.transform import radon, iradon from skimage.io import imread from skimage import data_dir From 6d2f082c1196aae972e85319a67fe9cc60537fce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Wed, 19 Jun 2013 12:38:50 +0200 Subject: [PATCH 042/736] test_radon_transform: Style fixes, PEP8. --- skimage/transform/tests/test_radon_transform.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/skimage/transform/tests/test_radon_transform.py b/skimage/transform/tests/test_radon_transform.py index c385c84c..f7127aa5 100644 --- a/skimage/transform/tests/test_radon_transform.py +++ b/skimage/transform/tests/test_radon_transform.py @@ -11,6 +11,7 @@ from skimage import data_dir __PHANTOM = imread(data_dir + "/phantom.png", as_grey=True)[::2, ::2] + def _get_phantom(): return __PHANTOM @@ -264,14 +265,14 @@ def check_radon_iradon_circle(interpolation, shape, output_size): radius = min(shape) // 2 sinogram_rectangle = radon(image, circle=False) reconstruction_rectangle = iradon(sinogram_rectangle, - output_size=output_size, - interpolation=interpolation, - circle=False) + output_size=output_size, + interpolation=interpolation, + circle=False) sinogram_circle = radon(image, circle=True) reconstruction_circle = iradon(sinogram_circle, - output_size=output_size, - interpolation=interpolation, - circle=True) + output_size=output_size, + interpolation=interpolation, + circle=True) # Crop rectangular reconstruction to match circle=True reconstruction width = reconstruction_circle.shape[0] excess = int(np.ceil((reconstruction_rectangle.shape[0] - width) / 2)) From c4299c46374995f1dbe31faa4b2c1b175a880829 Mon Sep 17 00:00:00 2001 From: tonysyu Date: Tue, 25 Jun 2013 14:37:01 -0500 Subject: [PATCH 043/736] Fix execution in IPython with qt backend. New QApplication and event-loop implementation stolen shamelessly from IPython. Strangely, running the viewer at the IPython prompt will open an orphan Matplotlib figure window, but running a script using `%run` does not. Only tested on PySide (not PyQt4). --- skimage/viewer/utils/core.py | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/skimage/viewer/utils/core.py b/skimage/viewer/utils/core.py index 826147eb..4ec3e1cf 100644 --- a/skimage/viewer/utils/core.py +++ b/skimage/viewer/utils/core.py @@ -22,22 +22,37 @@ __all__ = ['init_qtapp', 'start_qtapp', 'RequiredAttr', 'figimage', 'update_axes_image'] -QApp = None - - def init_qtapp(): """Initialize QAppliction. The QApplication needs to be initialized before creating any QWidgets """ - global QApp - if QApp is None: - QApp = QtGui.QApplication([]) + app = QtGui.QApplication.instance() + if app is None: + app = QtGui.QApplication([]) + return app -def start_qtapp(): +def is_event_loop_running(app=None): + """Return True if event loop is running.""" + if app is None: + app = init_qtapp() + if hasattr(app, '_in_event_loop'): + return app._in_event_loop + else: + return False + + +def start_qtapp(app=None): """Start Qt mainloop""" - QApp.exec_() + if app is None: + app = init_qtapp() + if not is_event_loop_running(app): + app._in_event_loop = True + app.exec_() + app._in_event_loop = False + else: + app._in_event_loop = True class RequiredAttr(object): From e305677de59872674b0b7bb45e81844e1910e2ea Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Tue, 25 Jun 2013 23:15:42 -0500 Subject: [PATCH 044/736] Fix PyQt4 segfault caused by PySide fix. I guess PySide saves the QApplication internally, while PyQt4 doesn't. Saving the QApplication as a global prevents it from getting garbage collected. Saving the QApplication as an instance variable in the ImageViewer also works, but that might prevent the ImageViewer from getting garbage collected in an interactive session. (weakref doesn't seem to work here.) --- skimage/viewer/utils/core.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/skimage/viewer/utils/core.py b/skimage/viewer/utils/core.py index 4ec3e1cf..3ac0eef3 100644 --- a/skimage/viewer/utils/core.py +++ b/skimage/viewer/utils/core.py @@ -22,15 +22,19 @@ __all__ = ['init_qtapp', 'start_qtapp', 'RequiredAttr', 'figimage', 'update_axes_image'] +global QApp + + def init_qtapp(): """Initialize QAppliction. The QApplication needs to be initialized before creating any QWidgets """ - app = QtGui.QApplication.instance() - if app is None: - app = QtGui.QApplication([]) - return app + global QApp + QApp = QtGui.QApplication.instance() + if QApp is None: + QApp = QtGui.QApplication([]) + return QApp def is_event_loop_running(app=None): From dae0156230807f265fe9b8773f1ae9b5fd3f7a1b Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Tue, 25 Jun 2013 23:20:12 -0500 Subject: [PATCH 045/736] Make histogram threshold adjustable --- skimage/viewer/plugins/color_histogram.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/skimage/viewer/plugins/color_histogram.py b/skimage/viewer/plugins/color_histogram.py index 166b2805..39a75004 100644 --- a/skimage/viewer/plugins/color_histogram.py +++ b/skimage/viewer/plugins/color_histogram.py @@ -10,8 +10,9 @@ from ..canvastools import RectangleTool class ColorHistogram(PlotPlugin): name = 'Color Histogram' - def __init__(self, **kwargs): + def __init__(self, max_pct=0.99, **kwargs): super(ColorHistogram, self).__init__(height=400, **kwargs) + self.max_pct = max_pct print(self.help()) @@ -30,7 +31,7 @@ class ColorHistogram(PlotPlugin): normed=True) # Clip bin heights that dominate a-b histogram - max_val = pct_total_area(hist, percentile=99) + max_val = pct_total_area(hist, percentile=self.max_pct) hist = exposure.rescale_intensity(hist, in_range=(0, max_val)) self.ax.imshow(hist, extent=ab_extents, cmap=plt.cm.gray) @@ -55,12 +56,12 @@ class ColorHistogram(PlotPlugin): self.image_viewer.image = color.lab2rgb(lab_masked) -def pct_total_area(image, percentile=80): +def pct_total_area(image, percentile=0.80): """Return threshold value based on percentage of total area. The specified percent of pixels less than the given intensity threshold. """ - idx = int((image.size - 1) * percentile / 100.0) + idx = int((image.size - 1) * percentile) sorted_pixels = np.sort(image.flat) return sorted_pixels[idx] From cae693cb699b46d0554f50cf051d09cb7201ce15 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Tue, 25 Jun 2013 23:24:06 -0500 Subject: [PATCH 046/736] Change QApp default to previous behavior. --- skimage/viewer/utils/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/viewer/utils/core.py b/skimage/viewer/utils/core.py index 3ac0eef3..0a053784 100644 --- a/skimage/viewer/utils/core.py +++ b/skimage/viewer/utils/core.py @@ -22,7 +22,7 @@ __all__ = ['init_qtapp', 'start_qtapp', 'RequiredAttr', 'figimage', 'update_axes_image'] -global QApp +QApp = None def init_qtapp(): From ed7c75d4c6d3e687bdf05ca38c044d29e4fd65d5 Mon Sep 17 00:00:00 2001 From: tonysyu Date: Wed, 26 Jun 2013 11:00:24 -0500 Subject: [PATCH 047/736] Raise ImageViewer to front on start. Currently only works for main window, does not work for linked viewers. --- skimage/viewer/plugins/base.py | 2 ++ skimage/viewer/viewers/core.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/skimage/viewer/plugins/base.py b/skimage/viewer/plugins/base.py index 1d655d61..c84245d2 100644 --- a/skimage/viewer/plugins/base.py +++ b/skimage/viewer/plugins/base.py @@ -209,6 +209,8 @@ class Plugin(QtGui.QDialog): def show(self, main_window=True): """Show plugin.""" super(Plugin, self).show() + self.activateWindow() + self.raise_() # Emit signal with x-hint so new windows can be displayed w/o overlap. size = self.frameGeometry() diff --git a/skimage/viewer/viewers/core.py b/skimage/viewer/viewers/core.py index 3a5f8ad3..c3ffeb1e 100644 --- a/skimage/viewer/viewers/core.py +++ b/skimage/viewer/viewers/core.py @@ -201,6 +201,8 @@ class ImageViewer(QtGui.QMainWindow): for p in self.plugins: p.show() super(ImageViewer, self).show() + self.activateWindow() + self.raise_() def show(self, main_window=True): """Show ImageViewer and attached plugins. From c826935d9e3cb7c31a80f5d0d763452016cbd732 Mon Sep 17 00:00:00 2001 From: tonysyu Date: Wed, 26 Jun 2013 11:23:12 -0500 Subject: [PATCH 048/736] Change default Slider update_on value to 'release' Image filtering is usually slow, so updating on move was usually a bad idea. --- skimage/viewer/widgets/core.py | 4 ++-- viewer_examples/plugins/canny_simple.py | 6 +++--- viewer_examples/plugins/collection_plugin.py | 2 +- viewer_examples/plugins/median_filter.py | 2 +- viewer_examples/plugins/probabilistic_hough.py | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/skimage/viewer/widgets/core.py b/skimage/viewer/widgets/core.py index 0000bebb..b9714d38 100644 --- a/skimage/viewer/widgets/core.py +++ b/skimage/viewer/widgets/core.py @@ -90,12 +90,12 @@ class Slider(BaseWidget): is typically set when the widget is added to a plugin. orientation : {'horizontal' | 'vertical'} Slider orientation. - update_on : {'move' | 'release'} + update_on : {'release' | 'move'} Control when callback function is called: on slider move or release. """ def __init__(self, name, low=0.0, high=1.0, value=None, value_type='float', ptype='kwarg', callback=None, max_edit_width=60, - orientation='horizontal', update_on='move'): + orientation='horizontal', update_on='release'): super(Slider, self).__init__(name, ptype, callback) if value is None: diff --git a/viewer_examples/plugins/canny_simple.py b/viewer_examples/plugins/canny_simple.py index 912401e1..c26ca08d 100644 --- a/viewer_examples/plugins/canny_simple.py +++ b/viewer_examples/plugins/canny_simple.py @@ -12,9 +12,9 @@ image = data.camera() # You can create a UI for a filter just by passing a filter function... plugin = OverlayPlugin(image_filter=canny) # ... and adding widgets to adjust parameter values. -plugin += Slider('sigma', 0, 5, update_on='release') -plugin += Slider('low threshold', 0, 255, update_on='release') -plugin += Slider('high threshold', 0, 255, update_on='release') +plugin += Slider('sigma', 0, 5) +plugin += Slider('low threshold', 0, 255) +plugin += Slider('high threshold', 0, 255) # ... and we can also add buttons to save the overlay: plugin += SaveButtons(name='Save overlay to:') diff --git a/viewer_examples/plugins/collection_plugin.py b/viewer_examples/plugins/collection_plugin.py index 80c7ff45..65ff1f21 100644 --- a/viewer_examples/plugins/collection_plugin.py +++ b/viewer_examples/plugins/collection_plugin.py @@ -24,7 +24,7 @@ def autolevel(image, disk_size): img_collection = [data.camera(), data.coins(), data.text()] plugin = Plugin(image_filter=autolevel) -plugin += Slider('disk_size', 2, 8, value_type='int', update_on='release') +plugin += Slider('disk_size', 2, 8, value_type='int') plugin.name = "Autolevel" viewer = CollectionViewer(img_collection) diff --git a/viewer_examples/plugins/median_filter.py b/viewer_examples/plugins/median_filter.py index a20ad8f3..36593c3d 100644 --- a/viewer_examples/plugins/median_filter.py +++ b/viewer_examples/plugins/median_filter.py @@ -10,7 +10,7 @@ image = data.coins() viewer = ImageViewer(image) plugin = Plugin(image_filter=median_filter) -plugin += Slider('radius', 2, 10, value_type='int', update_on='release') +plugin += Slider('radius', 2, 10, value_type='int') plugin += SaveButtons() plugin += OKCancelButtons() diff --git a/viewer_examples/plugins/probabilistic_hough.py b/viewer_examples/plugins/probabilistic_hough.py index 5564ba9f..98052f87 100644 --- a/viewer_examples/plugins/probabilistic_hough.py +++ b/viewer_examples/plugins/probabilistic_hough.py @@ -34,8 +34,8 @@ canny_viewer += canny_plugin hough_plugin = OverlayPlugin(image_filter=hough_lines) hough_plugin.name = 'Hough Lines' -hough_plugin += Slider('line length', 0, 100, update_on='release') -hough_plugin += Slider('line gap', 0, 20, update_on='release') +hough_plugin += Slider('line length', 0, 100) +hough_plugin += Slider('line gap', 0, 20) # Passing a plugin to a viewer connects the output of the plugin to the viewer. hough_viewer = ImageViewer(canny_plugin) From 45d960d64f765dcf1ace17fe6c5baf052c32656f Mon Sep 17 00:00:00 2001 From: Horea Christian Date: Thu, 27 Jun 2013 08:15:49 +0200 Subject: [PATCH 049/736] User-specified output ratio Added a keyword argument which enables the user to select other ratios for the output image rather than just 1:1. --- skimage/util/montage.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/skimage/util/montage.py b/skimage/util/montage.py index df3ad12e..4d7deb9d 100644 --- a/skimage/util/montage.py +++ b/skimage/util/montage.py @@ -1,12 +1,12 @@ __all__ = ['montage2d'] import numpy as np -from .. import exposure +#~ from .. import exposure EPSILON = 1e-6 -def montage2d(arr_in, fill='mean', rescale_intensity=False): +def montage2d(arr_in, fill='mean', rescale_intensity=False, output_shape=(0,0)): """Create a 2-dimensional 'montage' from a 3-dimensional input array representing an ensemble of equally shaped 2-dimensional images. @@ -38,6 +38,9 @@ def montage2d(arr_in, fill='mean', rescale_intensity=False): rescale_intensity: bool, optional Whether to rescale the intensity of each image to [0, 1]. + + output_shape: tuple, optional + The desired aspect ratio for the montage (default is square). Returns ------- @@ -80,19 +83,23 @@ def montage2d(arr_in, fill='mean', rescale_intensity=False): arr_in[i] = exposure.rescale_intensity(arr_in[i]) # -- determine alpha - alpha = int(np.ceil(np.sqrt(n_images))) + if output_shape == (0,0): + alpha_y = alpha_x = int(np.ceil(np.sqrt(n_images))) + else: + alpha_y = output_shape[0] + alpha_x = output_shape[1] # -- fill missing patches if fill == 'mean': fill = arr_in.mean() - n_missing = int((alpha**2.) - n_images) + n_missing = int((alpha_y*alpha_x) - n_images) missing = np.ones((n_missing, height, width), dtype=arr_in.dtype) * fill arr_out = np.vstack((arr_in, missing)) # -- reshape to 2d montage, step by step - arr_out = arr_out.reshape(alpha, alpha, height, width) + arr_out = arr_out.reshape(alpha_y, alpha_x, height, width) arr_out = arr_out.swapaxes(1, 2) - arr_out = arr_out.reshape(alpha * height, alpha * width) + arr_out = arr_out.reshape(alpha_y * height, alpha_x * width) return arr_out From 8367542a20b405a17acaeaf4cbdf911102616731 Mon Sep 17 00:00:00 2001 From: Horea Christian Date: Fri, 28 Jun 2013 01:25:13 +0200 Subject: [PATCH 050/736] Corrected indentation and spacing. --- skimage/util/montage.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/skimage/util/montage.py b/skimage/util/montage.py index 4d7deb9d..74f65741 100644 --- a/skimage/util/montage.py +++ b/skimage/util/montage.py @@ -1,12 +1,12 @@ __all__ = ['montage2d'] import numpy as np -#~ from .. import exposure +from .. import exposure EPSILON = 1e-6 -def montage2d(arr_in, fill='mean', rescale_intensity=False, output_shape=(0,0)): +def montage2d(arr_in, fill='mean', rescale_intensity=False, output_shape=(0, 0)): """Create a 2-dimensional 'montage' from a 3-dimensional input array representing an ensemble of equally shaped 2-dimensional images. @@ -33,11 +33,11 @@ def montage2d(arr_in, fill='mean', rescale_intensity=False, output_shape=(0,0)): of equal shape (i.e. [height, width]). fill: float or 'mean', optional - How to fill the 2-dimensional output array when sqrt(n_images) - is not an integer. If 'mean' is chosen, then fill = arr_in.mean(). + How to fill the 2-dimensional output array when sqrt(n_images) + is not an integer. If 'mean' is chosen, then fill = arr_in.mean(). rescale_intensity: bool, optional - Whether to rescale the intensity of each image to [0, 1]. + Whether to rescale the intensity of each image to [0, 1]. output_shape: tuple, optional The desired aspect ratio for the montage (default is square). @@ -83,7 +83,7 @@ def montage2d(arr_in, fill='mean', rescale_intensity=False, output_shape=(0,0)): arr_in[i] = exposure.rescale_intensity(arr_in[i]) # -- determine alpha - if output_shape == (0,0): + if output_shape == (0, 0): alpha_y = alpha_x = int(np.ceil(np.sqrt(n_images))) else: alpha_y = output_shape[0] From 03623a30280c06f1915391fee0235b14ba56b548 Mon Sep 17 00:00:00 2001 From: Horea Christian Date: Fri, 28 Jun 2013 05:28:15 +0200 Subject: [PATCH 051/736] Corrected indents (4space instead tab). --- skimage/util/montage.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/skimage/util/montage.py b/skimage/util/montage.py index 74f65741..bf6ab06c 100644 --- a/skimage/util/montage.py +++ b/skimage/util/montage.py @@ -33,14 +33,14 @@ def montage2d(arr_in, fill='mean', rescale_intensity=False, output_shape=(0, 0)) of equal shape (i.e. [height, width]). fill: float or 'mean', optional - How to fill the 2-dimensional output array when sqrt(n_images) - is not an integer. If 'mean' is chosen, then fill = arr_in.mean(). + How to fill the 2-dimensional output array when sqrt(n_images) + is not an integer. If 'mean' is chosen, then fill = arr_in.mean(). rescale_intensity: bool, optional - Whether to rescale the intensity of each image to [0, 1]. + Whether to rescale the intensity of each image to [0, 1]. output_shape: tuple, optional - The desired aspect ratio for the montage (default is square). + The desired aspect ratio for the montage (default is square). Returns ------- @@ -84,10 +84,10 @@ def montage2d(arr_in, fill='mean', rescale_intensity=False, output_shape=(0, 0)) # -- determine alpha if output_shape == (0, 0): - alpha_y = alpha_x = int(np.ceil(np.sqrt(n_images))) + alpha_y = alpha_x = int(np.ceil(np.sqrt(n_images))) else: - alpha_y = output_shape[0] - alpha_x = output_shape[1] + alpha_y = output_shape[0] + alpha_x = output_shape[1] # -- fill missing patches if fill == 'mean': From 9e415a096a9ae2d80111295a6a20db15b55bd4f5 Mon Sep 17 00:00:00 2001 From: Horea Christian Date: Fri, 28 Jun 2013 13:21:55 +0200 Subject: [PATCH 052/736] Corrected indents Again. --- skimage/util/montage.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/skimage/util/montage.py b/skimage/util/montage.py index bf6ab06c..b5410b82 100644 --- a/skimage/util/montage.py +++ b/skimage/util/montage.py @@ -33,14 +33,14 @@ def montage2d(arr_in, fill='mean', rescale_intensity=False, output_shape=(0, 0)) of equal shape (i.e. [height, width]). fill: float or 'mean', optional - How to fill the 2-dimensional output array when sqrt(n_images) - is not an integer. If 'mean' is chosen, then fill = arr_in.mean(). + How to fill the 2-dimensional output array when sqrt(n_images) + is not an integer. If 'mean' is chosen, then fill = arr_in.mean(). rescale_intensity: bool, optional - Whether to rescale the intensity of each image to [0, 1]. - + Whether to rescale the intensity of each image to [0, 1]. + output_shape: tuple, optional - The desired aspect ratio for the montage (default is square). + The desired aspect ratio for the montage (default is square). Returns ------- @@ -84,10 +84,10 @@ def montage2d(arr_in, fill='mean', rescale_intensity=False, output_shape=(0, 0)) # -- determine alpha if output_shape == (0, 0): - alpha_y = alpha_x = int(np.ceil(np.sqrt(n_images))) + alpha_y = alpha_x = int(np.ceil(np.sqrt(n_images))) else: - alpha_y = output_shape[0] - alpha_x = output_shape[1] + alpha_y = output_shape[0] + alpha_x = output_shape[1] # -- fill missing patches if fill == 'mean': From bba2b1b3fa42f85ca641ff7a39a17f29288c1286 Mon Sep 17 00:00:00 2001 From: Marianne Corvellec Date: Fri, 28 Jun 2013 12:35:50 -0400 Subject: [PATCH 053/736] Documented another easy way to build scikit-image for virtualenv users. --- DEPENDS.txt | 5 +++++ requirements.txt | 3 +++ 2 files changed, 8 insertions(+) create mode 100644 requirements.txt diff --git a/DEPENDS.txt b/DEPENDS.txt index b2858256..e9e9025a 100644 --- a/DEPENDS.txt +++ b/DEPENDS.txt @@ -7,6 +7,11 @@ Build Requirements `Matplotlib >= 1.0 `__ is needed to generate the examples in the documentation. +If you like to use pip in a virtualenv, you can do the following to build the +development version of scikit-image: +$ pip install -r requirements.txt +$ python setup.py install + Runtime requirements -------------------- * `SciPy >= 0.10 `__ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..f291f1a5 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +cython>=0.15 +matplotlib>=1.0 +numpy>=1.6 From 058681796266381d3fd11b8a9c95c81eca758071 Mon Sep 17 00:00:00 2001 From: Davin Potts Date: Fri, 28 Jun 2013 12:17:28 -0500 Subject: [PATCH 054/736] Modified test_rank.test_compare_autolevels and test_rank.test_compare_8bit_vs_16bit to correct for assumption that data.camera() would always be loaded as ubyte (or ints at least) when using matplotlib as backend (which caused data.camera() to load as float32). --- skimage/filter/rank/tests/test_rank.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/skimage/filter/rank/tests/test_rank.py b/skimage/filter/rank/tests/test_rank.py index 1cfb92fe..d7748d0f 100644 --- a/skimage/filter/rank/tests/test_rank.py +++ b/skimage/filter/rank/tests/test_rank.py @@ -1,7 +1,7 @@ import numpy as np from numpy.testing import run_module_suite, assert_array_equal, assert_raises -from skimage import data +from skimage import data, util from skimage.morphology import cmorph, disk from skimage.filter import rank @@ -162,7 +162,7 @@ def test_compare_autolevels(): # compare autolevel and percentile autolevel with p0=0.0 and p1=1.0 # should returns the same arrays - image = data.camera() + image = util.img_as_ubyte(data.camera()) selem = disk(20) loc_autolevel = rank.autolevel(image, selem=selem) @@ -190,7 +190,7 @@ def test_compare_8bit_vs_16bit(): # filters applied on 8-bit image ore 16-bit image (having only real 8-bit of # dynamic) should be identical - image8 = data.camera() + image8 = util.img_as_ubyte(data.camera()) image16 = image8.astype(np.uint16) assert_array_equal(image8, image16) From 9583072271ae5740f66b56dd392c525b2bb5bee8 Mon Sep 17 00:00:00 2001 From: Horea Christian Date: Fri, 28 Jun 2013 20:20:39 +0200 Subject: [PATCH 055/736] Added usage example. --- skimage/util/montage.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/skimage/util/montage.py b/skimage/util/montage.py index b5410b82..03e2808a 100644 --- a/skimage/util/montage.py +++ b/skimage/util/montage.py @@ -70,6 +70,13 @@ def montage2d(arr_in, fill='mean', rescale_intensity=False, output_shape=(0, 0)) [ 10. 11. 5.5 5.5]] >>> print(arr_in.mean()) 5.5 + >>> arr_out_nonsquare = montage2d(arr_in, output_shape = (3, 4)) + >>> print(arr_out_nonsquare) + [[ 0. 1. 4. 5. ] + [ 2. 3. 6. 7. ] + [ 8. 9. 10. 11. ]] + >>> print(arr_out_nonsquare.shape) + (3, 4) """ assert arr_in.ndim == 3 From 362d915399cc9c77d9f8223004d0a5cc1b9cf295 Mon Sep 17 00:00:00 2001 From: Adam Wisniewski Date: Fri, 28 Jun 2013 16:18:01 -0400 Subject: [PATCH 056/736] support for saving to file-like object in imsave --- skimage/io/_plugins/pil_plugin.py | 9 ++++++--- skimage/io/tests/test_io.py | 12 ++++++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/skimage/io/_plugins/pil_plugin.py b/skimage/io/_plugins/pil_plugin.py index 2247f3a1..3ce79c47 100644 --- a/skimage/io/_plugins/pil_plugin.py +++ b/skimage/io/_plugins/pil_plugin.py @@ -71,9 +71,8 @@ def imsave(fname, arr, format_str=None): values in [0, 255], whereas floating-point arrays must be in [0, 1]. format_str: str - Format to save as, this is required if using a file-like object; - this is optional if fname is a string and the format can be - derived from the extension. + Format to save as, this is defaulted to PNG if using a file-like + object; this will be derived from the extension if fname is a string Notes ----- @@ -104,6 +103,10 @@ def imsave(fname, arr, format_str=None): # Force all integers to bytes arr = arr.astype(np.uint8) + # default to PNG if file-like object + if not isinstance(fname, basestring) and format_str is None: + format_str = "PNG" + img = Image.fromstring(mode, (arr.shape[1], arr.shape[0]), arr.tostring()) img.save(fname, format=format_str) diff --git a/skimage/io/tests/test_io.py b/skimage/io/tests/test_io.py index 484784ee..5fa3e14a 100644 --- a/skimage/io/tests/test_io.py +++ b/skimage/io/tests/test_io.py @@ -2,6 +2,7 @@ import os from numpy.testing import * import numpy as np +from StringIO import StringIO import skimage.io as io from skimage import data_dir @@ -27,6 +28,17 @@ def test_imread_url(): image = io.imread(image_url) assert image.shape == (512, 512) +def test_imsave_filelike(): + image = np.array([[0, 0], [0, 0]], dtype=float) + s = StringIO() + + # save to file-like object + io.imsave(s, image) + + # read from file-like object + s.seek(0) + image = io.imread(s) + assert image.shape == (2, 2) if __name__ == "__main__": run_module_suite() From edee15054b415d82fba498ef76646779679e528b Mon Sep 17 00:00:00 2001 From: Marianne Corvellec Date: Fri, 28 Jun 2013 16:50:43 -0400 Subject: [PATCH 057/736] Edited after Stefan's comments. --- DEPENDS.txt | 6 ++---- requirements.txt | 2 +- setup.py | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/DEPENDS.txt b/DEPENDS.txt index e9e9025a..1f6fb735 100644 --- a/DEPENDS.txt +++ b/DEPENDS.txt @@ -2,15 +2,13 @@ Build Requirements ------------------ * `Python >= 2.5 `__ * `Numpy >= 1.6 `__ -* `Cython >= 0.15 `__ +* `Cython >= 0.17 `__ `Matplotlib >= 1.0 `__ is needed to generate the examples in the documentation. -If you like to use pip in a virtualenv, you can do the following to build the -development version of scikit-image: +You can use pip to automatically install the base dependencies as follows: $ pip install -r requirements.txt -$ python setup.py install Runtime requirements -------------------- diff --git a/requirements.txt b/requirements.txt index f291f1a5..4a4aaa53 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ -cython>=0.15 +cython>=0.17 matplotlib>=1.0 numpy>=1.6 diff --git a/setup.py b/setup.py index d62a0d86..93460509 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ VERSION = '0.9dev' PYTHON_VERSION = (2, 5) DEPENDENCIES = { 'numpy': (1, 6), - 'Cython': (0, 15), + 'Cython': (0, 17), } From c76daa97a9a1f3014345e6e1bd0fceae2822fc0a Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Fri, 28 Jun 2013 16:00:03 -0500 Subject: [PATCH 058/736] Add explanation to LBP example --- doc/examples/plot_local_binary_pattern.py | 176 +++++++++++++++++++--- 1 file changed, 156 insertions(+), 20 deletions(-) diff --git a/doc/examples/plot_local_binary_pattern.py b/doc/examples/plot_local_binary_pattern.py index 8c9cabae..98ea08d4 100644 --- a/doc/examples/plot_local_binary_pattern.py +++ b/doc/examples/plot_local_binary_pattern.py @@ -4,27 +4,158 @@ Local Binary Pattern for texture classification =============================================== In this example, we will see how to classify textures based on LBP (Local -Binary Pattern). The histogram of the LBP result is a good measure to classify -textures. For simplicity the histogram distributions are then tested against -each other using the Kullback-Leibler-Divergence. +Binary Pattern). LBP looks at points surrounding a central point and tests +whether the surrounding points are greater than or less than the central point +(i.e. gives a binary result). +Before trying out LBP on an image, it helps to look at a schematic of LBPs. +The below code is just used to plot the schematic. """ from __future__ import print_function - import numpy as np -import matplotlib import matplotlib.pyplot as plt + +METHOD = 'uniform' +plt.rcParams['font.size'] = 9 + + +def plot_circle(ax, center, radius, color): + circle = plt.Circle(center, radius, facecolor=color, edgecolor='0.5') + ax.add_patch(circle) + + +def plot_lbp_model(ax, binary_values): + """Draw the schematic for a local binary pattern.""" + # Geometry spec + theta = np.deg2rad(45) + R = 1 + r = 0.15 + w = 1.5 + gray = '0.5' + + # Draw the central pixel. + plot_circle(ax, (0, 0), radius=r, color=gray) + # Draw the surrounding pixels. + for i, facecolor in enumerate(binary_values): + x = R * np.cos(i * theta) + y = R * np.sin(i * theta) + plot_circle(ax, (x, y), radius=r, color=str(facecolor)) + + # Draw the pixel grid. + for x in np.linspace(-w, w, 4): + ax.axvline(x, color=gray) + ax.axhline(x, color=gray) + + # Tweak the layout. + ax.axis('image') + ax.axis('off') + size = w + 0.2 + ax.set_xlim(-size, size) + ax.set_ylim(-size, size) + + +fig, axes = plt.subplots(ncols=5, figsize=(7, 2)) + +titles = ['flat', 'flat', 'edge', 'corner', 'non-uniform'] + +binary_patterns = [np.zeros(8), + np.ones(8), + np.hstack([np.ones(4), np.zeros(4)]), + np.hstack([np.zeros(3), np.ones(5)]), + [1, 0, 0, 1, 1, 1, 0, 0]] + +for ax, values, name in zip(axes, binary_patterns, titles): + plot_lbp_model(ax, values) + ax.set_title(name) + +""" +.. image:: PLOT2RST.current_figure + +The figure above shows example results with black (or white) representing +pixels that are less (or more) intense than the central pixel. When surrounding +pixels are all black or all white, then that image region is flat (i.e. +featureless). Groups of continuous black or white pixels are considered +"uniform" patterns that can be interpreted as corners or edges. If pixels +switch back-and-forth between black and white pixels, the pattern is considered +"non-uniform". + +When using LBP to detect texture, you measure a collection of LBPs over an +image patch and look at the distribution of these LBPs. Lets apply LBP to +a brick texture. +""" + from skimage.transform import rotate from skimage.feature import local_binary_pattern from skimage import data - +from skimage.color import label2rgb # settings for LBP -METHOD = 'uniform' -P = 16 -R = 2 -matplotlib.rcParams['font.size'] = 9 +radius = 3 +n_points = 8 * radius + + +def overlay_labels(image, lbp, labels): + mask = np.logical_or.reduce([lbp == each for each in labels]) + return label2rgb(mask, image=image, bg_label=0, alpha=0.5) + + +def highlight_bars(bars, indexes): + for i in indexes: + bars[i].set_facecolor('r') + + +image = data.load('brick.png') +lbp = local_binary_pattern(image, n_points, radius, METHOD) + +def hist(ax, lbp): + n_bins = lbp.max() + 1 + return ax.hist(lbp.ravel(), normed=True, bins=n_bins, range=(0, n_bins), + facecolor='0.5') + +# plot histograms of LBP of textures +fig, (ax_img, ax_hist) = plt.subplots(nrows=2, ncols=3, figsize=(9, 6)) +plt.gray() + +titles = ('edge', 'flat', 'corner') +w = width = radius - 1 +edge_labels = range(n_points // 2 - w, n_points // 2 + w + 1) +flat_labels = range(0, w + 1) + range(n_points - w, n_points + 2) +i_14 = n_points // 4 # 1/4th of the histogram +i_34 = 3 * (n_points // 4) # 3/4th of the histogram +corner_labels = range(i_14 - w, i_14 + w + 1) + range(i_34 - w, i_34 + w + 1) + +label_sets = (edge_labels, flat_labels, corner_labels) + +for ax, labels in zip(ax_img, label_sets): + ax.imshow(overlay_labels(image, lbp, labels)) + +for ax, labels, name in zip(ax_hist, label_sets, titles): + counts, _, bars = hist(ax, lbp) + highlight_bars(bars, labels) + ax.set_ylim(ymax=np.max(counts[:-1])) + ax.set_xlim(xmax=n_points + 2) + ax.set_title(name) + +ax_hist[0].set_ylabel('Percentage') +for ax in ax_img: + ax.axis('off') + + +""" +.. image:: PLOT2RST.current_figure + +The above plot highlights flat, edge-like, and corner-like regions of the +image. + +The histogram of the LBP result is a good measure to classify textures. For +simplicity the histogram distributions are then tested against each other using +the Kullback-Leibler-Divergence. +""" + +# settings for LBP +radius = 2 +n_points = 8 * radius def kullback_leibler_divergence(p, q): @@ -37,11 +168,12 @@ def kullback_leibler_divergence(p, q): def match(refs, img): best_score = 10 best_name = None - lbp = local_binary_pattern(img, P, R, METHOD) - hist, _ = np.histogram(lbp, normed=True, bins=P + 2, range=(0, P + 2)) + lbp = local_binary_pattern(img, n_points, radius, METHOD) + n_bins = lbp.max() + 1 + hist, _ = np.histogram(lbp, normed=True, bins=n_bins, range=(0, n_bins)) for name, ref in refs.items(): - ref_hist, _ = np.histogram(ref, normed=True, bins=P + 2, - range=(0, P + 2)) + ref_hist, _ = np.histogram(ref, normed=True, bins=n_bins, + range=(0, n_bins)) score = kullback_leibler_divergence(hist, ref_hist) if score < best_score: best_score = score @@ -54,9 +186,9 @@ grass = data.load('grass.png') wall = data.load('rough-wall.png') refs = { - 'brick': local_binary_pattern(brick, P, R, METHOD), - 'grass': local_binary_pattern(grass, P, R, METHOD), - 'wall': local_binary_pattern(wall, P, R, METHOD) + 'brick': local_binary_pattern(brick, n_points, radius, METHOD), + 'grass': local_binary_pattern(grass, n_points, radius, METHOD), + 'wall': local_binary_pattern(wall, n_points, radius, METHOD) } # classify rotated textures @@ -75,16 +207,20 @@ plt.gray() ax1.imshow(brick) ax1.axis('off') -ax4.hist(refs['brick'].ravel(), normed=True, bins=P + 2, range=(0, P + 2)) +hist(ax, refs['brick']) ax4.set_ylabel('Percentage') ax2.imshow(grass) ax2.axis('off') -ax5.hist(refs['grass'].ravel(), normed=True, bins=P + 2, range=(0, P + 2)) +hist(ax5, refs['grass']) ax5.set_xlabel('Uniform LBP values') ax3.imshow(wall) ax3.axis('off') -ax6.hist(refs['wall'].ravel(), normed=True, bins=P + 2, range=(0, P + 2)) +hist(ax6, refs['wall']) + +""" +.. image:: PLOT2RST.current_figure +""" plt.show() From 397e7c1adea63e121f9129a7f44d5fbc1c9f75c2 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Fri, 28 Jun 2013 16:08:11 -0500 Subject: [PATCH 059/736] Clean up test. --- skimage/io/tests/test_io.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/skimage/io/tests/test_io.py b/skimage/io/tests/test_io.py index 5fa3e14a..6643e82e 100644 --- a/skimage/io/tests/test_io.py +++ b/skimage/io/tests/test_io.py @@ -28,8 +28,10 @@ def test_imread_url(): image = io.imread(image_url) assert image.shape == (512, 512) + def test_imsave_filelike(): - image = np.array([[0, 0], [0, 0]], dtype=float) + shape = (2, 2) + image = np.zeros(shape) s = StringIO() # save to file-like object @@ -37,8 +39,10 @@ def test_imsave_filelike(): # read from file-like object s.seek(0) - image = io.imread(s) - assert image.shape == (2, 2) + out = io.imread(s) + assert out.shape == shape + np.testing.assert_allclose(out, image) + if __name__ == "__main__": run_module_suite() From c18373f3ad7693b48ec2104ad67d38545a1e4a20 Mon Sep 17 00:00:00 2001 From: "Josh Warner (Mac)" Date: Fri, 28 Jun 2013 17:53:14 -0500 Subject: [PATCH 060/736] Use memoryviews in skimage.transform --- skimage/_shared/transform.pxd | 4 ++-- skimage/_shared/transform.pyx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/skimage/_shared/transform.pxd b/skimage/_shared/transform.pxd index ccb16ff3..4978e843 100644 --- a/skimage/_shared/transform.pxd +++ b/skimage/_shared/transform.pxd @@ -1,5 +1,5 @@ cimport numpy as cnp -cdef float integrate(cnp.ndarray[float, ndim=2, mode="c"] sat, - Py_ssize_t r0, Py_ssize_t c0, Py_ssize_t r1, Py_ssize_t c1) +cdef float integrate(float[:, ::1] sat, Py_ssize_t r0, Py_ssize_t c0, + Py_ssize_t r1, Py_ssize_t c1) diff --git a/skimage/_shared/transform.pyx b/skimage/_shared/transform.pyx index 8ce2ab67..9bdc6824 100644 --- a/skimage/_shared/transform.pyx +++ b/skimage/_shared/transform.pyx @@ -5,8 +5,8 @@ cimport numpy as cnp -cdef float integrate(cnp.ndarray[float, ndim=2, mode="c"] sat, - Py_ssize_t r0, Py_ssize_t c0, Py_ssize_t r1, Py_ssize_t c1): +cdef float integrate(float[:, ::1] sat, Py_ssize_t r0, Py_ssize_t c0, + Py_ssize_t r1, Py_ssize_t c1): """ Using a summed area table / integral image, calculate the sum over a given window. From 0baffad640ad9794ea2bf388862db5a72506a5dc Mon Sep 17 00:00:00 2001 From: Marianne Corvellec Date: Fri, 28 Jun 2013 19:10:55 -0400 Subject: [PATCH 061/736] Okay, I think I got the rst syntax this time... --- DEPENDS.txt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/DEPENDS.txt b/DEPENDS.txt index 1f6fb735..9c7302f1 100644 --- a/DEPENDS.txt +++ b/DEPENDS.txt @@ -7,8 +7,9 @@ Build Requirements `Matplotlib >= 1.0 `__ is needed to generate the examples in the documentation. -You can use pip to automatically install the base dependencies as follows: -$ pip install -r requirements.txt +You can use pip to automatically install the base dependencies as follows:: + + $ pip install -r requirements.txt Runtime requirements -------------------- From e931739502417be5ff95c788f6d0cf4a21f29c27 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Sat, 29 Jun 2013 12:09:56 +0800 Subject: [PATCH 062/736] Adding custom sampling seed, Normal sampling mode --- skimage/feature/_brief.py | 42 +++++++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/skimage/feature/_brief.py b/skimage/feature/_brief.py index 46bd5e92..40ad204b 100644 --- a/skimage/feature/_brief.py +++ b/skimage/feature/_brief.py @@ -5,45 +5,53 @@ import numpy as np from skimage.color import rgb2gray from scipy.ndimage.filters import gaussian_filter -KERNEL_SIZE = (9, 9) -PATCH_SIZE = (49, 49) - - def _remove_border_keypoints(image, keypoints, dist): width = image.shape[0] height = image.shape[1] - for i, j in keypoints: + + keypoints_list = keypoints.tolist() + + for i, j in keypoints_list: if i > width - dist[0] or i < dist[0] or j < dist[1] or j > height - dist[0]: - keypoints.remove((i, j)) + keypoints.remove([i, j]) + + keypoints = np.asarray(keypoints_list) return keypoints -def brief(image, keypoints, descriptor_size=32, mode='uniform'): +def brief(image, keypoints, descriptor_size=256, mode='normal', patch_size=49, sample_seed=1): if np.squeeze(image).ndim == 3: image = rgb2gray(image) - keypoints = _remove_border_keypoints(image, keypoints, (PATCH_SIZE[0] / 2, PATCH_SIZE[1] / 2)) + keypoints = np.round(keypoints) - descriptor = np.zeros((len(keypoints), descriptor_size * 8), dtype=int) + keypoints = _remove_border_keypoints(image, keypoints, (patch_size / 2, patch_size / 2)) + descriptor = np.zeros((len(keypoints), descriptor_size), dtype=int) + + # Gaussian Low pass filtering with variance 2 to alleviate noise sensitivity image = gaussian_filter(image, 2) - if mode == 'uniform': - np.random.seed(1) - first = np.random.randint(-PATCH_SIZE / 2, (PATCH_SIZE / 2) + 1, (descriptor_size * 8, 2)) - np.random.seed(2) - second = np.random.randint(-PATCH_SIZE / 2, (PATCH_SIZE / 2) + 1, (descriptor_size * 8, 2)) + # Sampling pairs of decision pixels in patch_size x patch_size window + if mode == 'normal': + np.random.seed(sample_seed) + samples = np.round((patch_size / 5) * np.random.randn(descriptor_size * 8)) + samples = samples[samples < (patch_size / 2)] + samples = samples[samples > - (patch_size - 1) / 2] + first = (samples[: descriptor_size * 2]).reshape(descriptor_size, 2) + second = (samples[descriptor_size * 2: descriptor_size * 4]).reshape(descriptor_size, 2) else: - #TODO mode='normal' - pass + np.random.seed(sample_seed) + samples = np.random.randint(-patch_size / 2, (patch_size / 2) + 1, (descriptor_size * 2, 2)) + first, second = np.split(samples, 2) for i in range(len(keypoints)): set_1 = first + keypoints[i] set_2 = second + keypoints[i] - for j in range(descriptor_size * 8): + for j in range(descriptor_size): if image[set_1[j, 0]][set_1[j, 1]] < image[set_2[j, 0]][set_2[j, 0]]: descriptor[i][j] = 1 else: From 2ef6f0ae7353b018bc15a2a34bdf7ae941b38271 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Fri, 28 Jun 2013 10:26:11 -0500 Subject: [PATCH 063/736] Remove 2to3. --- setup.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/setup.py b/setup.py index d62a0d86..22db8e75 100644 --- a/setup.py +++ b/setup.py @@ -30,10 +30,7 @@ import sys import re import setuptools from numpy.distutils.core import setup -try: - from distutils.command.build_py import build_py_2to3 as build_py -except ImportError: - from distutils.command.build_py import build_py +from distutils.command.build_py import build_py def configuration(parent_package='', top_path=None): From 70d21f23a2a4f0984d98559b54dc5e692b0cb9bc Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Fri, 28 Jun 2013 11:32:11 -0500 Subject: [PATCH 064/736] Add six compatibility module. --- skimage/_shared/six.py | 423 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 423 insertions(+) create mode 100644 skimage/_shared/six.py diff --git a/skimage/_shared/six.py b/skimage/_shared/six.py new file mode 100644 index 00000000..8a877b17 --- /dev/null +++ b/skimage/_shared/six.py @@ -0,0 +1,423 @@ +"""Utilities for writing code that runs on Python 2 and 3""" + +# Copyright (c) 2010-2013 Benjamin Peterson +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import operator +import sys +import types + +__author__ = "Benjamin Peterson " +__version__ = "1.3.0" + + +# Useful for very coarse version differentiation. +PY2 = sys.version_info[0] == 2 +PY3 = sys.version_info[0] == 3 + +if PY3: + string_types = str, + integer_types = int, + class_types = type, + text_type = str + binary_type = bytes + + MAXSIZE = sys.maxsize +else: + string_types = basestring, + integer_types = (int, long) + class_types = (type, types.ClassType) + text_type = unicode + binary_type = str + + if sys.platform.startswith("java"): + # Jython always uses 32 bits. + MAXSIZE = int((1 << 31) - 1) + else: + # It's possible to have sizeof(long) != sizeof(Py_ssize_t). + class X(object): + def __len__(self): + return 1 << 31 + try: + len(X()) + except OverflowError: + # 32-bit + MAXSIZE = int((1 << 31) - 1) + else: + # 64-bit + MAXSIZE = int((1 << 63) - 1) + del X + + +def _add_doc(func, doc): + """Add documentation to a function.""" + func.__doc__ = doc + + +def _import_module(name): + """Import module, returning the module after the last dot.""" + __import__(name) + return sys.modules[name] + + +class _LazyDescr(object): + + def __init__(self, name): + self.name = name + + def __get__(self, obj, tp): + result = self._resolve() + setattr(obj, self.name, result) + # This is a bit ugly, but it avoids running this again. + delattr(tp, self.name) + return result + + +class MovedModule(_LazyDescr): + + def __init__(self, name, old, new=None): + super(MovedModule, self).__init__(name) + if PY3: + if new is None: + new = name + self.mod = new + else: + self.mod = old + + def _resolve(self): + return _import_module(self.mod) + + +class MovedAttribute(_LazyDescr): + + def __init__(self, name, old_mod, new_mod, old_attr=None, new_attr=None): + super(MovedAttribute, self).__init__(name) + if PY3: + if new_mod is None: + new_mod = name + self.mod = new_mod + if new_attr is None: + if old_attr is None: + new_attr = name + else: + new_attr = old_attr + self.attr = new_attr + else: + self.mod = old_mod + if old_attr is None: + old_attr = name + self.attr = old_attr + + def _resolve(self): + module = _import_module(self.mod) + return getattr(module, self.attr) + + + +class _MovedItems(types.ModuleType): + """Lazy loading of moved objects""" + + +_moved_attributes = [ + MovedAttribute("cStringIO", "cStringIO", "io", "StringIO"), + MovedAttribute("filter", "itertools", "builtins", "ifilter", "filter"), + MovedAttribute("input", "__builtin__", "builtins", "raw_input", "input"), + MovedAttribute("map", "itertools", "builtins", "imap", "map"), + MovedAttribute("range", "__builtin__", "builtins", "xrange", "range"), + MovedAttribute("reload_module", "__builtin__", "imp", "reload"), + MovedAttribute("reduce", "__builtin__", "functools"), + MovedAttribute("StringIO", "StringIO", "io"), + MovedAttribute("xrange", "__builtin__", "builtins", "xrange", "range"), + MovedAttribute("zip", "itertools", "builtins", "izip", "zip"), + + MovedModule("builtins", "__builtin__"), + MovedModule("configparser", "ConfigParser"), + MovedModule("copyreg", "copy_reg"), + MovedModule("http_cookiejar", "cookielib", "http.cookiejar"), + MovedModule("http_cookies", "Cookie", "http.cookies"), + MovedModule("html_entities", "htmlentitydefs", "html.entities"), + MovedModule("html_parser", "HTMLParser", "html.parser"), + MovedModule("http_client", "httplib", "http.client"), + MovedModule("email_mime_multipart", "email.MIMEMultipart", "email.mime.multipart"), + MovedModule("email_mime_text", "email.MIMEText", "email.mime.text"), + MovedModule("email_mime_base", "email.MIMEBase", "email.mime.base"), + MovedModule("BaseHTTPServer", "BaseHTTPServer", "http.server"), + MovedModule("CGIHTTPServer", "CGIHTTPServer", "http.server"), + MovedModule("SimpleHTTPServer", "SimpleHTTPServer", "http.server"), + MovedModule("cPickle", "cPickle", "pickle"), + MovedModule("queue", "Queue"), + MovedModule("reprlib", "repr"), + MovedModule("socketserver", "SocketServer"), + MovedModule("tkinter", "Tkinter"), + MovedModule("tkinter_dialog", "Dialog", "tkinter.dialog"), + MovedModule("tkinter_filedialog", "FileDialog", "tkinter.filedialog"), + MovedModule("tkinter_scrolledtext", "ScrolledText", "tkinter.scrolledtext"), + MovedModule("tkinter_simpledialog", "SimpleDialog", "tkinter.simpledialog"), + MovedModule("tkinter_tix", "Tix", "tkinter.tix"), + MovedModule("tkinter_constants", "Tkconstants", "tkinter.constants"), + MovedModule("tkinter_dnd", "Tkdnd", "tkinter.dnd"), + MovedModule("tkinter_colorchooser", "tkColorChooser", + "tkinter.colorchooser"), + MovedModule("tkinter_commondialog", "tkCommonDialog", + "tkinter.commondialog"), + MovedModule("tkinter_tkfiledialog", "tkFileDialog", "tkinter.filedialog"), + MovedModule("tkinter_font", "tkFont", "tkinter.font"), + MovedModule("tkinter_messagebox", "tkMessageBox", "tkinter.messagebox"), + MovedModule("tkinter_tksimpledialog", "tkSimpleDialog", + "tkinter.simpledialog"), + MovedModule("urllib_robotparser", "robotparser", "urllib.robotparser"), + MovedModule("winreg", "_winreg"), +] +for attr in _moved_attributes: + setattr(_MovedItems, attr.name, attr) +del attr + +moves = sys.modules[__name__ + ".moves"] = _MovedItems("moves") + + +def add_move(move): + """Add an item to six.moves.""" + setattr(_MovedItems, move.name, move) + + +def remove_move(name): + """Remove item from six.moves.""" + try: + delattr(_MovedItems, name) + except AttributeError: + try: + del moves.__dict__[name] + except KeyError: + raise AttributeError("no such move, %r" % (name,)) + + +if PY3: + _meth_func = "__func__" + _meth_self = "__self__" + + _func_closure = "__closure__" + _func_code = "__code__" + _func_defaults = "__defaults__" + _func_globals = "__globals__" + + _iterkeys = "keys" + _itervalues = "values" + _iteritems = "items" + _iterlists = "lists" +else: + _meth_func = "im_func" + _meth_self = "im_self" + + _func_closure = "func_closure" + _func_code = "func_code" + _func_defaults = "func_defaults" + _func_globals = "func_globals" + + _iterkeys = "iterkeys" + _itervalues = "itervalues" + _iteritems = "iteritems" + _iterlists = "iterlists" + + +try: + advance_iterator = next +except NameError: + def advance_iterator(it): + return it.next() +next = advance_iterator + + +try: + callable = callable +except NameError: + def callable(obj): + return any("__call__" in klass.__dict__ for klass in type(obj).__mro__) + + +if PY3: + def get_unbound_function(unbound): + return unbound + + create_bound_method = types.MethodType + + Iterator = object +else: + def get_unbound_function(unbound): + return unbound.im_func + + def create_bound_method(func, obj): + return types.MethodType(func, obj, obj.__class__) + + class Iterator(object): + + def next(self): + return type(self).__next__(self) + + callable = callable +_add_doc(get_unbound_function, + """Get the function out of a possibly unbound function""") + + +get_method_function = operator.attrgetter(_meth_func) +get_method_self = operator.attrgetter(_meth_self) +get_function_closure = operator.attrgetter(_func_closure) +get_function_code = operator.attrgetter(_func_code) +get_function_defaults = operator.attrgetter(_func_defaults) +get_function_globals = operator.attrgetter(_func_globals) + + +def iterkeys(d, **kw): + """Return an iterator over the keys of a dictionary.""" + return iter(getattr(d, _iterkeys)(**kw)) + +def itervalues(d, **kw): + """Return an iterator over the values of a dictionary.""" + return iter(getattr(d, _itervalues)(**kw)) + +def iteritems(d, **kw): + """Return an iterator over the (key, value) pairs of a dictionary.""" + return iter(getattr(d, _iteritems)(**kw)) + +def iterlists(d, **kw): + """Return an iterator over the (key, [values]) pairs of a dictionary.""" + return iter(getattr(d, _iterlists)(**kw)) + + +if PY3: + def b(s): + return s.encode("latin-1") + def u(s): + return s + unichr = chr + if sys.version_info[1] <= 1: + def int2byte(i): + return bytes((i,)) + else: + # This is about 2x faster than the implementation above on 3.2+ + int2byte = operator.methodcaller("to_bytes", 1, "big") + byte2int = operator.itemgetter(0) + indexbytes = operator.getitem + iterbytes = iter + import io + StringIO = io.StringIO + BytesIO = io.BytesIO +else: + def b(s): + return s + def u(s): + return unicode(s, "unicode_escape") + unichr = unichr + int2byte = chr + def byte2int(bs): + return ord(bs[0]) + def indexbytes(buf, i): + return ord(buf[i]) + def iterbytes(buf): + return (ord(byte) for byte in buf) + import StringIO + StringIO = BytesIO = StringIO.StringIO +_add_doc(b, """Byte literal""") +_add_doc(u, """Text literal""") + + +if PY3: + import builtins + exec_ = getattr(builtins, "exec") + + + def reraise(tp, value, tb=None): + if value.__traceback__ is not tb: + raise value.with_traceback(tb) + raise value + + + print_ = getattr(builtins, "print") + del builtins + +else: + def exec_(_code_, _globs_=None, _locs_=None): + """Execute code in a namespace.""" + if _globs_ is None: + frame = sys._getframe(1) + _globs_ = frame.f_globals + if _locs_ is None: + _locs_ = frame.f_locals + del frame + elif _locs_ is None: + _locs_ = _globs_ + exec("""exec _code_ in _globs_, _locs_""") + + + exec_("""def reraise(tp, value, tb=None): + raise tp, value, tb +""") + + + def print_(*args, **kwargs): + """The new-style print function.""" + fp = kwargs.pop("file", sys.stdout) + if fp is None: + return + def write(data): + if not isinstance(data, basestring): + data = str(data) + fp.write(data) + want_unicode = False + sep = kwargs.pop("sep", None) + if sep is not None: + if isinstance(sep, unicode): + want_unicode = True + elif not isinstance(sep, str): + raise TypeError("sep must be None or a string") + end = kwargs.pop("end", None) + if end is not None: + if isinstance(end, unicode): + want_unicode = True + elif not isinstance(end, str): + raise TypeError("end must be None or a string") + if kwargs: + raise TypeError("invalid keyword arguments to print()") + if not want_unicode: + for arg in args: + if isinstance(arg, unicode): + want_unicode = True + break + if want_unicode: + newline = unicode("\n") + space = unicode(" ") + else: + newline = "\n" + space = " " + if sep is None: + sep = space + if end is None: + end = newline + for i, arg in enumerate(args): + if i: + write(sep) + write(arg) + write(end) + +_add_doc(reraise, """Reraise an exception.""") + + +def with_metaclass(meta, *bases): + """Create a base class with a metaclass.""" + return meta("NewBase", bases, {}) From 9a38a59a258e50b3076fbb05e4d6ee1df2a7fc06 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Fri, 28 Jun 2013 11:33:36 -0500 Subject: [PATCH 065/736] Remove is_str and replace by isinstance(..., six.string_types). --- skimage/_shared/utils.py | 13 +------------ skimage/color/colorlabel.py | 4 ++-- 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/skimage/_shared/utils.py b/skimage/_shared/utils.py index e1642102..aad9220d 100644 --- a/skimage/_shared/utils.py +++ b/skimage/_shared/utils.py @@ -2,18 +2,7 @@ import warnings import functools -__all__ = ['deprecated', 'is_str'] - - -try: - isinstance("", basestring) - def is_str(s): - """Return True if `s` is a string. Safe for Python 2 and 3.""" - return isinstance(s, basestring) -except NameError: - def is_str(s): - """Return True if `s` is a string. Safe for Python 2 and 3.""" - return isinstance(s, str) +__all__ = ['deprecated'] class deprecated(object): diff --git a/skimage/color/colorlabel.py b/skimage/color/colorlabel.py index f1c91d67..41974f38 100644 --- a/skimage/color/colorlabel.py +++ b/skimage/color/colorlabel.py @@ -4,7 +4,7 @@ import itertools import numpy as np from skimage import img_as_float -from skimage._shared.utils import is_str +from skimage._shared import six from .colorconv import rgb2gray, gray2rgb from . import rgb_colors @@ -30,7 +30,7 @@ def _rgb_vector(color): color : str or array Color name in `color_dict` or RGB float values between [0, 1]. """ - if is_str(color): + if isinstance(color, six.string_types): color = color_dict[color] # slice to handle RGBA colors return np.array(color[:3]).reshape(1, 3) From a1adfa8d549121950303629881bffcbbcf04961e Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Fri, 28 Jun 2013 11:37:08 -0500 Subject: [PATCH 066/736] Correctly check for string type. --- skimage/io/_io.py | 5 +++-- skimage/io/collection.py | 3 ++- skimage/io/tests/test_collection.py | 5 ++--- skimage/viewer/plugins/overlayplugin.py | 4 +++- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/skimage/io/_io.py b/skimage/io/_io.py index 7b21b2b8..63d3d7e4 100644 --- a/skimage/io/_io.py +++ b/skimage/io/_io.py @@ -15,6 +15,7 @@ import numpy as np from skimage.io._plugins import call as call_plugin from skimage.color import rgb2grey +from skimage._shared import six # Shared image queue @@ -25,7 +26,7 @@ URL_REGEX = re.compile(r'http://|https://|ftp://|file://|file:\\') def is_url(filename): """Return True if string is an http or ftp path.""" - return (isinstance(filename, basestring) and + return (isinstance(filename, six.string_types) and URL_REGEX.match(filename) is not None) @@ -220,7 +221,7 @@ def imshow(arr, plugin=None, **plugin_args): Passed to the given plugin. """ - if isinstance(arr, basestring): + if isinstance(arr, six.string_types): arr = call_plugin('imread', arr, plugin=plugin) return call_plugin('imshow', arr, plugin=plugin, **plugin_args) diff --git a/skimage/io/collection.py b/skimage/io/collection.py index 7c3f739b..96855dbd 100644 --- a/skimage/io/collection.py +++ b/skimage/io/collection.py @@ -10,6 +10,7 @@ from copy import copy import numpy as np from ._io import imread +from .._shared import six def concatenate_images(ic): @@ -296,7 +297,7 @@ class ImageCollection(object): """ def __init__(self, load_pattern, conserve_memory=True, load_func=None): """Load and manage a collection of images.""" - if isinstance(load_pattern, basestring): + if isinstance(load_pattern, six.string_types): load_pattern = load_pattern.split(':') self._files = [] for pattern in load_pattern: diff --git a/skimage/io/tests/test_collection.py b/skimage/io/tests/test_collection.py index f650daeb..25cd1152 100644 --- a/skimage/io/tests/test_collection.py +++ b/skimage/io/tests/test_collection.py @@ -12,6 +12,7 @@ from skimage import data_dir from skimage.io import ImageCollection, MultiImage from skimage.io.collection import alphanumeric_key from skimage.io import Image as ioImage +from skimage._shared import six try: @@ -21,8 +22,6 @@ except ImportError: else: PIL_available = True -if sys.version_info[0] > 2: - basestring = str class TestAlphanumericKey(): def setUp(self): @@ -129,7 +128,7 @@ class TestMultiImage(): @skipif(not PIL_available) def test_files_property(self): - assert isinstance(self.img.filename, basestring) + assert isinstance(self.img.filename, six.string_types) def set_filename(f): self.img.filename = f diff --git a/skimage/viewer/plugins/overlayplugin.py b/skimage/viewer/plugins/overlayplugin.py index 25ea3b00..dc5ca060 100644 --- a/skimage/viewer/plugins/overlayplugin.py +++ b/skimage/viewer/plugins/overlayplugin.py @@ -3,6 +3,7 @@ from warnings import warn from skimage.util.dtype import dtype_range from .base import Plugin from ..utils import ClearColormap, update_axes_image +from skimage._shared import six __all__ = ['OverlayPlugin'] @@ -77,7 +78,8 @@ class OverlayPlugin(Plugin): @color.setter def color(self, index): # Update colormap whenever color is changed. - if isinstance(index, basestring) and index not in self.color_names: + if isinstance(index, six.string_types) and \ + index not in self.color_names: raise ValueError("%s not defined in OverlayPlugin.colors" % index) else: name = self.color_names[index] From b04c3282bf1f718a6c93454a4bec66eb0a88b559 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Fri, 28 Jun 2013 14:56:01 -0500 Subject: [PATCH 067/736] Fix zip. --- skimage/color/colorlabel.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/skimage/color/colorlabel.py b/skimage/color/colorlabel.py index 41974f38..d6379037 100644 --- a/skimage/color/colorlabel.py +++ b/skimage/color/colorlabel.py @@ -5,6 +5,7 @@ import numpy as np from skimage import img_as_float from skimage._shared import six +from skimage._shared.six.moves import zip from .colorconv import rgb2gray, gray2rgb from . import rgb_colors @@ -88,7 +89,7 @@ def label2rgb(label, image=None, colors=None, alpha=0.3, bg_color = _rgb_vector(bg_color) color_cycle = itertools.chain(bg_color, color_cycle) - for c, i in itertools.izip(color_cycle, labels): + for c, i in zip(color_cycle, labels): mask = (label == i) colorized[mask] = c * alpha + colorized[mask] * (1 - alpha) From 84615478e43e992fdf8a0e44942e60122aa2cfab Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Fri, 28 Jun 2013 15:09:58 -0500 Subject: [PATCH 068/736] Fix function code. --- skimage/_shared/utils.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/skimage/_shared/utils.py b/skimage/_shared/utils.py index aad9220d..fb9376ab 100644 --- a/skimage/_shared/utils.py +++ b/skimage/_shared/utils.py @@ -1,6 +1,8 @@ import warnings import functools +from . import six + __all__ = ['deprecated'] @@ -35,10 +37,11 @@ class deprecated(object): @functools.wraps(func) def wrapped(*args, **kwargs): if self.behavior == 'warn': + func_code = six.get_function_code(func) warnings.warn_explicit(msg, category=DeprecationWarning, - filename=func.func_code.co_filename, - lineno=func.func_code.co_firstlineno + 1) + filename=func_code.co_filename, + lineno=func_code.co_firstlineno + 1) elif self.behavior == 'raise': raise DeprecationWarning(msg) return func(*args, **kwargs) From 504ad94dc69c4cd1231e3df8446cc33a84153b87 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Fri, 28 Jun 2013 15:16:46 -0500 Subject: [PATCH 069/736] Fix iterator. --- skimage/measure/find_contours.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skimage/measure/find_contours.py b/skimage/measure/find_contours.py index 68e471ac..298c09de 100755 --- a/skimage/measure/find_contours.py +++ b/skimage/measure/find_contours.py @@ -126,8 +126,8 @@ def find_contours(array, level, def _take_2(seq): iterator = iter(seq) while(True): - n1 = iterator.next() - n2 = iterator.next() + n1 = next(iterator) + n2 = next(iterator) yield (n1, n2) From eb66e343514b891ea34b5e5236ae6eb0f5cbca40 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Fri, 28 Jun 2013 16:19:49 -0500 Subject: [PATCH 070/736] Fix label casting for numpy 1.7. --- skimage/morphology/misc.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/skimage/morphology/misc.py b/skimage/morphology/misc.py index 820ce4a1..4e71b224 100644 --- a/skimage/morphology/misc.py +++ b/skimage/morphology/misc.py @@ -66,7 +66,8 @@ def remove_small_objects(ar, min_size=64, connectivity=1, in_place=False): if out.dtype == bool: selem = nd.generate_binary_structure(ar.ndim, connectivity) - ccs = nd.label(ar, selem)[0] + ccs = np.zeros_like(ar, dtype=np.int) + nd.label(ar, selem, output=ccs) else: ccs = out From 227b2097200e4bbb17da5686d0603bdfeb2a63bc Mon Sep 17 00:00:00 2001 From: Adam Wisniewski Date: Fri, 28 Jun 2013 18:09:27 -0400 Subject: [PATCH 071/736] fix arraypad --- skimage/util/arraypad.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/skimage/util/arraypad.py b/skimage/util/arraypad.py index 52a2e3e5..acc89b7b 100644 --- a/skimage/util/arraypad.py +++ b/skimage/util/arraypad.py @@ -4,8 +4,10 @@ of an n-dimensional array. """ from __future__ import division, absolute_import, print_function +from skimage._shared.six import integer_types import numpy as np + try: # Available on 2.x at base, Py3 requires this compatibility import. # Later versions of NumPy have this for 2.x as well. @@ -1036,11 +1038,11 @@ def _normalize_shape(narray, shape): fmt = "Unable to create correctly shaped tuple from %s" raise ValueError(fmt % (normshp,)) elif (isinstance(shape, (tuple, list)) - and isinstance(shape[0], (int, float, long)) + and isinstance(shape[0], integer_types + (float,)) and len(shape) == 1): normshp = ((shape[0], shape[0]), ) * shapelen elif (isinstance(shape, (tuple, list)) - and isinstance(shape[0], (int, float, long)) + and isinstance(shape[0], integer_types + (float,)) and len(shape) == 2): normshp = (shape, ) * shapelen if normshp is None: From 81519b7863f636a758ac0e00d8b1c2296e3c5696 Mon Sep 17 00:00:00 2001 From: Adam Wisniewski Date: Fri, 28 Jun 2013 18:14:46 -0400 Subject: [PATCH 072/736] adding main to run tests --- skimage/util/tests/test_montage.py | 3 +++ skimage/util/tests/test_shape.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/skimage/util/tests/test_montage.py b/skimage/util/tests/test_montage.py index 47e5426d..bc5f2176 100644 --- a/skimage/util/tests/test_montage.py +++ b/skimage/util/tests/test_montage.py @@ -79,3 +79,6 @@ def test_rescale_intensity(): def test_error_ndim(): arr_error = np.random.randn(1, 2, 3, 4) montage2d(arr_error) + +if __name__ == '__main__': + np.testing.run_module_suite() diff --git a/skimage/util/tests/test_shape.py b/skimage/util/tests/test_shape.py index 3b5f8d41..33e3b638 100644 --- a/skimage/util/tests/test_shape.py +++ b/skimage/util/tests/test_shape.py @@ -139,3 +139,6 @@ def test_view_as_windows_2D(): [9, 10, 11], [13, 14, 15], [17, 18, 19]]]])) + +if __name__ == '__main__': + np.testing.run_module_suite() From 9d1907a21178ff841a77da7098ade16af9db544f Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Fri, 28 Jun 2013 17:16:13 -0500 Subject: [PATCH 073/736] Fix im_class access in Python 3. --- skimage/_shared/utils.py | 8 ++++++++ skimage/transform/_geometric.py | 5 +++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/skimage/_shared/utils.py b/skimage/_shared/utils.py index fb9376ab..8ba67ec5 100644 --- a/skimage/_shared/utils.py +++ b/skimage/_shared/utils.py @@ -1,5 +1,6 @@ import warnings import functools +import sys from . import six @@ -54,3 +55,10 @@ class deprecated(object): wrapped.__doc__ = doc + '\n\n ' + wrapped.__doc__ return wrapped + + +def get_bound_method_class(m): + """Return the class for a bound method. + + """ + return m.im_class if sys.version < '3' else m.__self__.__class__ diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index 58109224..f4920f46 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -4,6 +4,7 @@ from scipy import ndimage, spatial from skimage.util import img_as_float from ._warps_cy import _warp_fast +from skimage._shared.utils import get_bound_method_class class GeometricTransform(object): """Perform geometric transformations on a set of coordinates. @@ -1009,8 +1010,8 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, if inverse_map in HOMOGRAPHY_TRANSFORMS: matrix = inverse_map._matrix elif hasattr(inverse_map, '__name__') \ - and inverse_map.__name__ == 'inverse' \ - and inverse_map.im_class in HOMOGRAPHY_TRANSFORMS: + and inverse_map.__name__ == 'inverse' \ + and get_bound_method_class(inverse_map) in HOMOGRAPHY_TRANSFORMS: matrix = np.linalg.inv(inverse_map.im_self._matrix) if matrix is not None: # transform all bands From 558298f426ed66ef5ca49d34a4faa85f0635b9a2 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Fri, 28 Jun 2013 17:18:55 -0500 Subject: [PATCH 074/736] Fix im_self access in Python 3. --- skimage/transform/_geometric.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index f4920f46..32857893 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -5,6 +5,8 @@ from skimage.util import img_as_float from ._warps_cy import _warp_fast from skimage._shared.utils import get_bound_method_class +from skimage._shared import six + class GeometricTransform(object): """Perform geometric transformations on a set of coordinates. @@ -1012,7 +1014,7 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, elif hasattr(inverse_map, '__name__') \ and inverse_map.__name__ == 'inverse' \ and get_bound_method_class(inverse_map) in HOMOGRAPHY_TRANSFORMS: - matrix = np.linalg.inv(inverse_map.im_self._matrix) + matrix = np.linalg.inv(six.get_method_self(inverse_map)._matrix) if matrix is not None: # transform all bands dims = [] From f0506f1293bc77544f5dd406c56981beeba8dcc9 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Fri, 28 Jun 2013 17:24:46 -0500 Subject: [PATCH 075/736] Fix imports under Python 3. --- skimage/viewer/canvastools/__init__.py | 6 +++--- skimage/viewer/canvastools/linetool.py | 2 +- skimage/viewer/canvastools/painttool.py | 2 +- skimage/viewer/utils/__init__.py | 2 +- skimage/viewer/widgets/__init__.py | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/skimage/viewer/canvastools/__init__.py b/skimage/viewer/canvastools/__init__.py index 22a2d205..f5b89e2c 100644 --- a/skimage/viewer/canvastools/__init__.py +++ b/skimage/viewer/canvastools/__init__.py @@ -1,3 +1,3 @@ -from linetool import LineTool, ThickLineTool -from recttool import RectangleTool -from painttool import PaintTool +from .linetool import LineTool, ThickLineTool +from .recttool import RectangleTool +from .painttool import PaintTool diff --git a/skimage/viewer/canvastools/linetool.py b/skimage/viewer/canvastools/linetool.py index 7a459401..0b1d3b0c 100644 --- a/skimage/viewer/canvastools/linetool.py +++ b/skimage/viewer/canvastools/linetool.py @@ -5,7 +5,7 @@ try: except ImportError: print("Could not import matplotlib -- skimage.viewer not available.") -from base import CanvasToolBase, ToolHandles +from .base import CanvasToolBase, ToolHandles __all__ = ['LineTool', 'ThickLineTool'] diff --git a/skimage/viewer/canvastools/painttool.py b/skimage/viewer/canvastools/painttool.py index 3fbab153..b5d67229 100644 --- a/skimage/viewer/canvastools/painttool.py +++ b/skimage/viewer/canvastools/painttool.py @@ -8,7 +8,7 @@ try: except ImportError: print("Could not import matplotlib -- skimage.viewer not available.") -from base import CanvasToolBase +from .base import CanvasToolBase __all__ = ['PaintTool'] diff --git a/skimage/viewer/utils/__init__.py b/skimage/viewer/utils/__init__.py index 5af24064..bb67a43f 100644 --- a/skimage/viewer/utils/__init__.py +++ b/skimage/viewer/utils/__init__.py @@ -1 +1 @@ -from core import * +from .core import * diff --git a/skimage/viewer/widgets/__init__.py b/skimage/viewer/widgets/__init__.py index 6552a313..efa9fe9d 100644 --- a/skimage/viewer/widgets/__init__.py +++ b/skimage/viewer/widgets/__init__.py @@ -1,2 +1,2 @@ -from core import * -from history import * +from .core import * +from .history import * From 0286d2d30fcc59045711f6200158864b98ea33fc Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Fri, 28 Jun 2013 17:47:21 -0500 Subject: [PATCH 076/736] Use int32 for labelling. --- skimage/morphology/misc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/morphology/misc.py b/skimage/morphology/misc.py index 4e71b224..88770371 100644 --- a/skimage/morphology/misc.py +++ b/skimage/morphology/misc.py @@ -66,7 +66,7 @@ def remove_small_objects(ar, min_size=64, connectivity=1, in_place=False): if out.dtype == bool: selem = nd.generate_binary_structure(ar.ndim, connectivity) - ccs = np.zeros_like(ar, dtype=np.int) + ccs = np.zeros_like(ar, dtype=np.int32) nd.label(ar, selem, output=ccs) else: ccs = out From 39e66c022903bf53c5d5ad6f1463b569569af849 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Sat, 29 Jun 2013 10:18:43 -0500 Subject: [PATCH 077/736] Add to public API. --- skimage/_shared/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/_shared/utils.py b/skimage/_shared/utils.py index 8ba67ec5..ea631716 100644 --- a/skimage/_shared/utils.py +++ b/skimage/_shared/utils.py @@ -5,7 +5,7 @@ import sys from . import six -__all__ = ['deprecated'] +__all__ = ['deprecated', 'get_bound_method_class'] class deprecated(object): From 25661719293b95413e3e13cfab78a1b2ad76e709 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Sat, 29 Jun 2013 10:40:49 -0500 Subject: [PATCH 078/736] Space code more readably in transform. --- skimage/transform/_geometric.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index 32857893..4153b5bd 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -1009,12 +1009,16 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, # use fast Cython version for specific interpolation orders if order in range(4) and not map_args: matrix = None + if inverse_map in HOMOGRAPHY_TRANSFORMS: matrix = inverse_map._matrix + elif hasattr(inverse_map, '__name__') \ and inverse_map.__name__ == 'inverse' \ and get_bound_method_class(inverse_map) in HOMOGRAPHY_TRANSFORMS: + matrix = np.linalg.inv(six.get_method_self(inverse_map)._matrix) + if matrix is not None: # transform all bands dims = [] From 6cef8727a0c06fcc97f6d18b0d11ca4733bb7d7d Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sat, 29 Jun 2013 10:56:03 -0500 Subject: [PATCH 079/736] Minor fixes - Use absolute imports so __main__ examples work - PEP8: 2 blank lines --- skimage/util/tests/test_montage.py | 1 + skimage/util/tests/test_shape.py | 1 + skimage/viewer/canvastools/linetool.py | 2 +- skimage/viewer/canvastools/painttool.py | 2 +- 4 files changed, 4 insertions(+), 2 deletions(-) diff --git a/skimage/util/tests/test_montage.py b/skimage/util/tests/test_montage.py index bc5f2176..a25a6ee4 100644 --- a/skimage/util/tests/test_montage.py +++ b/skimage/util/tests/test_montage.py @@ -80,5 +80,6 @@ def test_error_ndim(): arr_error = np.random.randn(1, 2, 3, 4) montage2d(arr_error) + if __name__ == '__main__': np.testing.run_module_suite() diff --git a/skimage/util/tests/test_shape.py b/skimage/util/tests/test_shape.py index 33e3b638..8c62a191 100644 --- a/skimage/util/tests/test_shape.py +++ b/skimage/util/tests/test_shape.py @@ -140,5 +140,6 @@ def test_view_as_windows_2D(): [13, 14, 15], [17, 18, 19]]]])) + if __name__ == '__main__': np.testing.run_module_suite() diff --git a/skimage/viewer/canvastools/linetool.py b/skimage/viewer/canvastools/linetool.py index 0b1d3b0c..c32d0ea7 100644 --- a/skimage/viewer/canvastools/linetool.py +++ b/skimage/viewer/canvastools/linetool.py @@ -5,7 +5,7 @@ try: except ImportError: print("Could not import matplotlib -- skimage.viewer not available.") -from .base import CanvasToolBase, ToolHandles +from skimage.viewer.canvastools.base import CanvasToolBase, ToolHandles __all__ = ['LineTool', 'ThickLineTool'] diff --git a/skimage/viewer/canvastools/painttool.py b/skimage/viewer/canvastools/painttool.py index b5d67229..0678c460 100644 --- a/skimage/viewer/canvastools/painttool.py +++ b/skimage/viewer/canvastools/painttool.py @@ -8,7 +8,7 @@ try: except ImportError: print("Could not import matplotlib -- skimage.viewer not available.") -from .base import CanvasToolBase +from skimage.viewer.canvastools.base import CanvasToolBase __all__ = ['PaintTool'] From f429b5abad906bf115e0e4ddc846c066204b28b5 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Sat, 29 Jun 2013 11:00:55 -0500 Subject: [PATCH 080/736] Ensure PIL tests run with PIL. --- skimage/io/tests/test_io.py | 18 +----------------- skimage/io/tests/test_pil.py | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/skimage/io/tests/test_io.py b/skimage/io/tests/test_io.py index 6643e82e..8049860a 100644 --- a/skimage/io/tests/test_io.py +++ b/skimage/io/tests/test_io.py @@ -1,8 +1,7 @@ import os -from numpy.testing import * +from numpy.testing import assert_array_equal, raises, run_module_suite import numpy as np -from StringIO import StringIO import skimage.io as io from skimage import data_dir @@ -29,20 +28,5 @@ def test_imread_url(): assert image.shape == (512, 512) -def test_imsave_filelike(): - shape = (2, 2) - image = np.zeros(shape) - s = StringIO() - - # save to file-like object - io.imsave(s, image) - - # read from file-like object - s.seek(0) - out = io.imread(s) - assert out.shape == shape - np.testing.assert_allclose(out, image) - - if __name__ == "__main__": run_module_suite() diff --git a/skimage/io/tests/test_pil.py b/skimage/io/tests/test_pil.py index 3e2f3dde..664bcf4d 100644 --- a/skimage/io/tests/test_pil.py +++ b/skimage/io/tests/test_pil.py @@ -7,6 +7,8 @@ from tempfile import NamedTemporaryFile from skimage import data_dir from skimage.io import imread, imsave, use_plugin, reset_plugins +from skimage._shared.six.moves import StringIO + try: from PIL import Image @@ -124,5 +126,22 @@ class TestSave: x = (x * 255).astype(dtype) yield self.roundtrip, dtype, x + +@skipif(not PIL_available) +def test_imsave_filelike(): + shape = (2, 2) + image = np.zeros(shape) + s = StringIO() + + # save to file-like object + imsave(s, image) + + # read from file-like object + s.seek(0) + out = imread(s) + assert out.shape == shape + assert_allclose(out, image) + + if __name__ == "__main__": run_module_suite() From 9ed26136e2e6db48c2efaf6cf66e67381b5e4589 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Sat, 29 Jun 2013 11:16:10 -0500 Subject: [PATCH 081/736] Do not run flakes on test files -- we have too many custom formatted array. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index a47dae84..b0e7a494 100644 --- a/.travis.yml +++ b/.travis.yml @@ -35,4 +35,4 @@ script: # Change back to repository root directory and run all doc examples - cd .. - for f in doc/examples/*.py; do $PYTHON "$f"; if [ $? -ne 0 ]; then exit 1; fi done - - flake8 --exit-zero skimage doc/examples viewer_examples + - flake8 --exit-zero --exclude=test_*,six.py skimage doc/examples viewer_examples From 58a4a43f104d40d6e12019e4c37a602f09b860d6 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Fri, 28 Jun 2013 18:06:10 -0500 Subject: [PATCH 082/736] Change rank.pyx to _rank.py - Cython was unnecessary in the main rank module - Don't shadow package name in the module - Add support for int8 images. --- skimage/filter/rank/__init__.py | 6 +++--- skimage/filter/rank/{rank.pyx => _rank.py} | 20 +++++++++++------- skimage/filter/rank/tests/test_rank.py | 24 ++++++++++++++++++++++ 3 files changed, 40 insertions(+), 10 deletions(-) rename skimage/filter/rank/{rank.pyx => _rank.py} (98%) diff --git a/skimage/filter/rank/__init__.py b/skimage/filter/rank/__init__.py index 906566b7..deceaade 100644 --- a/skimage/filter/rank/__init__.py +++ b/skimage/filter/rank/__init__.py @@ -1,6 +1,6 @@ -from .rank import (autolevel, bottomhat, equalize, gradient, maximum, mean, - meansubtraction, median, minimum, modal, morph_contr_enh, - pop, threshold, tophat, noise_filter, entropy, otsu) +from ._rank import (autolevel, bottomhat, equalize, gradient, maximum, mean, + meansubtraction, median, minimum, modal, morph_contr_enh, + pop, threshold, tophat, noise_filter, entropy, otsu) from .percentile_rank import (percentile_autolevel, percentile_gradient, percentile_mean, percentile_mean_subtraction, percentile_morph_contr_enh, percentile, diff --git a/skimage/filter/rank/rank.pyx b/skimage/filter/rank/_rank.py similarity index 98% rename from skimage/filter/rank/rank.pyx rename to skimage/filter/rank/_rank.py index 559f1c76..652732dd 100644 --- a/skimage/filter/rank/rank.pyx +++ b/skimage/filter/rank/_rank.py @@ -40,13 +40,10 @@ def _apply(func8, func16, image, selem, out, mask, shift_x, shift_y): if image is out: raise NotImplementedError("Cannot perform rank operation in place.") - if image.dtype == np.uint8: - if func8 is None: - raise TypeError("Not implemented for uint8 image.") - if out is None: - out = np.zeros(image.shape, dtype=np.uint8) - func8(image, selem, shift_x=shift_x, shift_y=shift_y, - mask=mask, out=out) + is_8bit = image.dtype in (np.uint8, np.int8) + + if func8 is not None and (is_8bit or func16 is None): + out = _apply8(func8, image, selem, out, mask, shift_x, shift_y) elif image.dtype == np.uint16: if func16 is None: raise TypeError("Not implemented for uint16 image.") @@ -63,6 +60,15 @@ def _apply(func8, func16, image, selem, out, mask, shift_x, shift_y): return out +def _apply8(func8, image, selem, out, mask, shift_x, shift_y): + if out is None: + out = np.zeros(image.shape, dtype=np.uint8) + image = img_as_ubyte(image) + func8(image, selem, shift_x=shift_x, shift_y=shift_y, + mask=mask, out=out) + return out + + def autolevel(image, selem, out=None, mask=None, shift_x=False, shift_y=False): """Autolevel image using local histogram. diff --git a/skimage/filter/rank/tests/test_rank.py b/skimage/filter/rank/tests/test_rank.py index d7748d0f..7a515a89 100644 --- a/skimage/filter/rank/tests/test_rank.py +++ b/skimage/filter/rank/tests/test_rank.py @@ -1,6 +1,7 @@ import numpy as np from numpy.testing import run_module_suite, assert_array_equal, assert_raises +from skimage import img_as_ubyte from skimage import data, util from skimage.morphology import cmorph, disk from skimage.filter import rank @@ -186,6 +187,29 @@ def test_compare_autolevels_16bit(): assert_array_equal(loc_autolevel, loc_perc_autolevel) +def test_compare_8bit_unsigned_vs_signed(): + # filters applied on 8-bit image ore 16-bit image (having only real 8-bit of + # dynamic) should be identical + + # Create signed int8 image that and convert it to uint8 + image = img_as_ubyte(data.camera()) + image[image > 127] = 0 + image_s = image.astype(np.int8) + image_u = img_as_ubyte(image_s) + + assert_array_equal(image_u, img_as_ubyte(image_s)) + + methods = ['autolevel', 'bottomhat', 'equalize', 'gradient', 'maximum', + 'mean', 'meansubtraction', 'median', 'minimum', 'modal', + 'morph_contr_enh', 'pop', 'threshold', 'tophat'] + + for method in methods: + func = getattr(rank, method) + out_u = func(image_u, disk(3)) + out_s = func(image_s, disk(3)) + assert_array_equal(out_u, out_s) + + def test_compare_8bit_vs_16bit(): # filters applied on 8-bit image ore 16-bit image (having only real 8-bit of # dynamic) should be identical From f92f057cbd3690efc3ad98579f435820e3752038 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sat, 29 Jun 2013 11:25:08 -0500 Subject: [PATCH 083/736] Add support for all data types. All dtypes larger than 8-bits are converted to uint16 and then bit-shifted to uint12. --- skimage/filter/rank/_rank.py | 12 +++++------- skimage/filter/rank/tests/test_rank.py | 20 +++++++++++++++++++- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/skimage/filter/rank/_rank.py b/skimage/filter/rank/_rank.py index 652732dd..ea58ad86 100644 --- a/skimage/filter/rank/_rank.py +++ b/skimage/filter/rank/_rank.py @@ -17,7 +17,7 @@ References """ import numpy as np -from skimage import img_as_ubyte +from skimage import img_as_ubyte, img_as_uint from skimage.filter.rank import _crank8, _crank16 from skimage.filter.rank.generic import find_bitdepth @@ -44,18 +44,16 @@ def _apply(func8, func16, image, selem, out, mask, shift_x, shift_y): if func8 is not None and (is_8bit or func16 is None): out = _apply8(func8, image, selem, out, mask, shift_x, shift_y) - elif image.dtype == np.uint16: - if func16 is None: - raise TypeError("Not implemented for uint16 image.") + else: + image = img_as_uint(image) if out is None: out = np.zeros(image.shape, dtype=np.uint16) bitdepth = find_bitdepth(image) if bitdepth > 11: - raise ValueError("Only uint16 <4096 image (12bit) supported.") + image = image >> 4 + bitdepth = find_bitdepth(image) func16(image, selem, shift_x=shift_x, shift_y=shift_y, mask=mask, bitdepth=bitdepth + 1, out=out) - else: - raise TypeError("Only uint8 and uint16 image supported.") return out diff --git a/skimage/filter/rank/tests/test_rank.py b/skimage/filter/rank/tests/test_rank.py index 7a515a89..d39386c6 100644 --- a/skimage/filter/rank/tests/test_rank.py +++ b/skimage/filter/rank/tests/test_rank.py @@ -1,7 +1,7 @@ import numpy as np from numpy.testing import run_module_suite, assert_array_equal, assert_raises -from skimage import img_as_ubyte +from skimage import img_as_ubyte, img_as_uint, img_as_float from skimage import data, util from skimage.morphology import cmorph, disk from skimage.filter import rank @@ -187,6 +187,24 @@ def test_compare_autolevels_16bit(): assert_array_equal(loc_autolevel, loc_perc_autolevel) +def test_compare_uint_vs_float(): + # filters applied on 8-bit image ore 16-bit image (having only real 8-bit of + # dynamic) should be identical + + # Create signed int8 image that and convert it to uint8 + image_uint = img_as_uint(data.camera()) + image_float = img_as_float(image_uint) + + methods = ['autolevel', 'bottomhat', 'equalize', 'gradient', 'threshold', + 'meansubtraction', 'morph_contr_enh', 'pop', 'tophat'] + + for method in methods: + func = getattr(rank, method) + out_u = func(image_uint, disk(3)) + out_f = func(image_float, disk(3)) + assert_array_equal(out_u, out_f) + + def test_compare_8bit_unsigned_vs_signed(): # filters applied on 8-bit image ore 16-bit image (having only real 8-bit of # dynamic) should be identical From d96411a7438cd90c7b1284665203643cd0bf06cf Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sat, 29 Jun 2013 12:40:54 -0500 Subject: [PATCH 084/736] Fix build script --- skimage/filter/setup.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/skimage/filter/setup.py b/skimage/filter/setup.py index b1d070fc..c70730f0 100644 --- a/skimage/filter/setup.py +++ b/skimage/filter/setup.py @@ -21,7 +21,6 @@ def configuration(parent_package='', top_path=None): cython(['rank/_crank16.pyx'], working_path=base_path) cython(['rank/_crank16_percentiles.pyx'], working_path=base_path) cython(['rank/_crank16_bilateral.pyx'], working_path=base_path) - cython(['rank/rank.pyx'], working_path=base_path) cython(['rank/percentile_rank.pyx'], working_path=base_path) cython(['rank/bilateral_rank.pyx'], working_path=base_path) @@ -46,9 +45,6 @@ def configuration(parent_package='', top_path=None): config.add_extension( 'rank._crank16_bilateral', sources=['rank/_crank16_bilateral.c'], include_dirs=[get_numpy_include_dirs()]) - config.add_extension( - 'rank.rank', sources=['rank/rank.c'], - include_dirs=[get_numpy_include_dirs()]) config.add_extension( 'rank.percentile_rank', sources=['rank/percentile_rank.c'], include_dirs=[get_numpy_include_dirs()]) From 9ece0b576c0d42435711a1a4c728d8c636c5695c Mon Sep 17 00:00:00 2001 From: "Josh Warner (Mac)" Date: Fri, 28 Jun 2013 16:55:33 -0500 Subject: [PATCH 085/736] FEAT: Automatically switch between RGB / grayscale lineprofile --- skimage/viewer/plugins/lineprofile.py | 92 ++++++++++++++++++++++----- 1 file changed, 75 insertions(+), 17 deletions(-) diff --git a/skimage/viewer/plugins/lineprofile.py b/skimage/viewer/plugins/lineprofile.py index c9ceedc5..8c01488f 100644 --- a/skimage/viewer/plugins/lineprofile.py +++ b/skimage/viewer/plugins/lineprofile.py @@ -60,7 +60,7 @@ class LineProfile(PlotPlugin): if not self._limit_type is None: self.ax.set_ylim(self.limits) - h, w = image.shape + h, w = image.shape[0:2] x = [w / 3, 2 * w / 3] y = [h / 2] * 2 @@ -71,7 +71,9 @@ class LineProfile(PlotPlugin): self.line_tool.end_points = np.transpose([x, y]) scan_data = profile_line(image, self.line_tool.end_points) - self.profile = self.ax.plot(scan_data, 'k-')[0] + + self.reset_axes(scan_data) + self._autoscale_view() def help(self): @@ -90,7 +92,7 @@ class LineProfile(PlotPlugin): profile: 1d array Profile of intensity values. """ - profile = self.profile.get_ydata() + profile = self.profile[0].get_ydata() return self.line_tool.end_points, profile def _autoscale_view(self): @@ -105,17 +107,36 @@ class LineProfile(PlotPlugin): scan = profile_line(self.image_viewer.original_image, end_points, linewidth=self.line_tool.linewidth) - self.profile.set_xdata(np.arange(scan.shape[0])) - self.profile.set_ydata(scan) + try: + if scan[1].shape != len(self.profile): + self.reset_axes(scan) + except: + self.reset_axes(scan) + + for i in range(len(scan[0])): + self.profile[i].set_xdata(np.arange(scan.shape[0])) + self.profile[i].set_ydata(scan[:, i]) self.ax.relim() if self.useblit: - self.ax.draw_artist(self.profile) + self.ax.draw_artist(self.profile[0]) self._autoscale_view() self.redraw() + def reset_axes(self, scan_data): + # Clear lines out + for line in self.ax.lines: + self.ax.lines = [] + + if scan_data.shape[1] == 1: + self.profile = self.ax.plot(scan_data, 'k-') + else: + self.profile = self.ax.plot(scan_data[:, 0], 'r-', + scan_data[:, 1], 'g-', + scan_data[:, 2], 'b-') + def profile_line(img, end_points, linewidth=1): """Return the intensity profile of an image measured along a scan line. @@ -140,17 +161,44 @@ def profile_line(img, end_points, linewidth=1): x2, y2 = point2 = np.asarray(point2, dtype=float) dx, dy = point2 - point1 - # Quick calculation if perfectly horizontal or vertical (remove?) + # Quick calculation if perfectly horizontal or vertical 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 + if img.ndim == 2: + pixels = img[min(y1, y2): max(y1, y2) + 1, + x1 - linewidth / 2: x1 + linewidth / 2 + 1] + return pixels.mean(axis=1)[:, np.newaxis] + else: + for i in range(3): + try: + temp = img[min(y1, y2): max(y1, y2) + 1, + x1 - linewidth / 2: x1 + linewidth / 2 + 1, i] + pixels = np.concatenate((pixels, temp[..., np.newaxis]), + axis=2) + del temp + except: + pixels = img[min(y1, y2): max(y1, y2) + 1, + x1 - linewidth / 2: x1 + linewidth / 2 + 1, + i][..., np.newaxis] + return pixels.mean(axis=1) + 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 + if img.ndim == 2: + pixels = img[y1 - linewidth / 2: y1 + linewidth / 2 + 1, + min(x1, x2): max(x1, x2) + 1] + return pixels.mean(axis=1)[..., np.newaxis] + else: + for i in range(3): + try: + temp = img[y1 - linewidth / 2: y1 + linewidth / 2 + 1, + min(x1, x2): max(x1, x2) + 1, i] + pixels = np.concatenate((pixels, temp[..., np.newaxis]), + axis=2) + del temp + except: + pixels = img[y1 - linewidth / 2: y1 + linewidth / 2 + 1, + min(x1, x2): max(x1, x2) + 1, + i][..., np.newaxis] + return pixels.mean(axis=0) theta = np.arctan2(dy, dx) a = dy / dx @@ -165,7 +213,17 @@ def profile_line(img, end_points, linewidth=1): 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) + if img.ndim == 3: + pixels = np.zeros((perp_lines.shape[1], y_width * 2 + 1, 3)) + for i in range(3): + pixels[..., i] = ndi.map_coordinates(img[..., i], perp_lines) + else: + pixels = ndi.map_coordinates(img, perp_lines) + pixels = pixels[..., np.newaxis] + intensities = pixels.mean(axis=1) - return intensities + if intensities.ndim == 1: + return intensities[..., np.newaxis] + else: + return intensities From 2b5930ad60c091bef5bb92683b73542b89ab5845 Mon Sep 17 00:00:00 2001 From: "Josh Warner (Mac)" Date: Fri, 28 Jun 2013 16:56:25 -0500 Subject: [PATCH 086/736] DOC: Add viewer example for RGB line profile --- viewer_examples/plugins/lineprofile_rgb.py | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 viewer_examples/plugins/lineprofile_rgb.py diff --git a/viewer_examples/plugins/lineprofile_rgb.py b/viewer_examples/plugins/lineprofile_rgb.py new file mode 100644 index 00000000..86b71d5d --- /dev/null +++ b/viewer_examples/plugins/lineprofile_rgb.py @@ -0,0 +1,9 @@ +from skimage import data +from skimage.viewer import ImageViewer +from skimage.viewer.plugins.lineprofile import LineProfile + + +image = data.chelsea() +viewer = ImageViewer(image) +viewer += LineProfile() +viewer.show() From e20aa7c3815da3a5a2b76722835f0589a34d259b Mon Sep 17 00:00:00 2001 From: "Josh Warner (Mac)" Date: Fri, 28 Jun 2013 17:40:05 -0500 Subject: [PATCH 087/736] FIX: refactor code, fix linewidth calculation --- skimage/viewer/plugins/lineprofile.py | 77 +++++++++++++-------------- 1 file changed, 38 insertions(+), 39 deletions(-) diff --git a/skimage/viewer/plugins/lineprofile.py b/skimage/viewer/plugins/lineprofile.py index 8c01488f..2ea20187 100644 --- a/skimage/viewer/plugins/lineprofile.py +++ b/skimage/viewer/plugins/lineprofile.py @@ -138,13 +138,29 @@ class LineProfile(PlotPlugin): scan_data[:, 2], 'b-') +def _calc_horiz(img, x1, x2, y1, y2, linewidth): + # Quick calculation if perfectly horizontal + pixels = img[min(y1, y2): max(y1, y2) + 1, + x1 - linewidth / 2: x1 + linewidth / 2 + 1] + intensities = pixels.mean(axis=1) + return intensities + + +def _calc_vert(img, x1, x2, y1, y2, linewidth): + # Quick calculation if perfectly vertical + pixels = img[y1 - linewidth / 2: y1 + linewidth / 2 + 1, + min(x1, x2): max(x1, x2) + 1] + intensities = pixels.mean(axis=0) + return intensities + + def profile_line(img, end_points, linewidth=1): """Return the intensity profile of an image measured along a scan line. Parameters ---------- - img : 2d array - The image. + img : 2d or 3d array + The image, in grayscale (2d) or RGB (3d) format. end_points: (2, 2) list End points ((x1, y1), (x2, y2)) of scan line. linewidth: int @@ -160,45 +176,28 @@ def profile_line(img, end_points, linewidth=1): x1, y1 = point1 = np.asarray(point1, dtype=float) x2, y2 = point2 = np.asarray(point2, dtype=float) dx, dy = point2 - point1 + channels = 1 + if img.ndim == 3: + channels = 3 # Quick calculation if perfectly horizontal or vertical if x1 == x2: - if img.ndim == 2: - pixels = img[min(y1, y2): max(y1, y2) + 1, - x1 - linewidth / 2: x1 + linewidth / 2 + 1] - return pixels.mean(axis=1)[:, np.newaxis] - else: - for i in range(3): - try: - temp = img[min(y1, y2): max(y1, y2) + 1, - x1 - linewidth / 2: x1 + linewidth / 2 + 1, i] - pixels = np.concatenate((pixels, temp[..., np.newaxis]), - axis=2) - del temp - except: - pixels = img[min(y1, y2): max(y1, y2) + 1, - x1 - linewidth / 2: x1 + linewidth / 2 + 1, - i][..., np.newaxis] - return pixels.mean(axis=1) + for i in range(channels): + try: + intensities = np.concatenate( + (intensities, + _calc_horiz(img, x1, x2, y1, y2, linewidth)), axis=1) + except: + intensities = _calc_horiz(img, x1, x2, y1, y2, linewidth) elif y1 == y2: - if img.ndim == 2: - pixels = img[y1 - linewidth / 2: y1 + linewidth / 2 + 1, - min(x1, x2): max(x1, x2) + 1] - return pixels.mean(axis=1)[..., np.newaxis] - else: - for i in range(3): - try: - temp = img[y1 - linewidth / 2: y1 + linewidth / 2 + 1, - min(x1, x2): max(x1, x2) + 1, i] - pixels = np.concatenate((pixels, temp[..., np.newaxis]), - axis=2) - del temp - except: - pixels = img[y1 - linewidth / 2: y1 + linewidth / 2 + 1, - min(x1, x2): max(x1, x2) + 1, - i][..., np.newaxis] - return pixels.mean(axis=0) + for i in range(channels): + try: + intensities = np.concatenate( + (intensities, + _calc_vert(img, x1, x2, y1, y2, linewidth)), axis=1) + except: + intensities = _calc_vert(img, x1, x2, y1, y2, linewidth) theta = np.arctan2(dy, dx) a = dy / dx @@ -214,9 +213,9 @@ def profile_line(img, end_points, linewidth=1): perp_lines = np.array([perp_ys, perp_xs]) if img.ndim == 3: - pixels = np.zeros((perp_lines.shape[1], y_width * 2 + 1, 3)) - for i in range(3): - pixels[..., i] = ndi.map_coordinates(img[..., i], perp_lines) + pixels = [ndi.map_coordinates(img[..., i], perp_lines) + for i in range(3)] + pixels = np.transpose(np.asarray(pixels), (1, 2, 0)) else: pixels = ndi.map_coordinates(img, perp_lines) pixels = pixels[..., np.newaxis] From e790fcc44eb3edfd1e9dce0257512c79f34148eb Mon Sep 17 00:00:00 2001 From: "Josh Warner (Mac)" Date: Fri, 28 Jun 2013 22:25:12 -0500 Subject: [PATCH 088/736] FIX: lineprofile no longer flips on left half and cardinals work --- skimage/viewer/plugins/lineprofile.py | 35 ++++++++++----------------- 1 file changed, 13 insertions(+), 22 deletions(-) diff --git a/skimage/viewer/plugins/lineprofile.py b/skimage/viewer/plugins/lineprofile.py index 2ea20187..28b2f4aa 100644 --- a/skimage/viewer/plugins/lineprofile.py +++ b/skimage/viewer/plugins/lineprofile.py @@ -138,20 +138,16 @@ class LineProfile(PlotPlugin): scan_data[:, 2], 'b-') -def _calc_horiz(img, x1, x2, y1, y2, linewidth): +def _calc_vert(img, x1, x2, y1, y2, linewidth): # Quick calculation if perfectly horizontal pixels = img[min(y1, y2): max(y1, y2) + 1, x1 - linewidth / 2: x1 + linewidth / 2 + 1] - intensities = pixels.mean(axis=1) - return intensities + # Reverse index if necessary + if y2 > y1: + pixels = pixels[::-1, :] -def _calc_vert(img, x1, x2, y1, y2, linewidth): - # Quick calculation if perfectly vertical - pixels = img[y1 - linewidth / 2: y1 + linewidth / 2 + 1, - min(x1, x2): max(x1, x2) + 1] - intensities = pixels.mean(axis=0) - return intensities + return pixels.mean(axis=1)[:, np.newaxis] def profile_line(img, end_points, linewidth=1): @@ -180,31 +176,26 @@ def profile_line(img, end_points, linewidth=1): if img.ndim == 3: channels = 3 - # Quick calculation if perfectly horizontal or vertical + # Quick calculation if perfectly vertical; shortcuts div0 error if x1 == x2: for i in range(channels): try: intensities = np.concatenate( (intensities, - _calc_horiz(img, x1, x2, y1, y2, linewidth)), axis=1) + _calc_vert(img[..., i], x1, x2, y1, y2, + linewidth)), axis=1) except: - intensities = _calc_horiz(img, x1, x2, y1, y2, linewidth) - - elif y1 == y2: - for i in range(channels): - try: - intensities = np.concatenate( - (intensities, - _calc_vert(img, x1, x2, y1, y2, linewidth)), axis=1) - except: - intensities = _calc_vert(img, x1, x2, y1, y2, linewidth) + intensities = _calc_vert(img[..., i], + x1, x2, y1, y2, + linewidth) + 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_x = np.linspace(x2, x1, 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, From b4e71ecd432ddfde43769121eb5a1610006f74a4 Mon Sep 17 00:00:00 2001 From: "Josh Warner (Mac)" Date: Sat, 29 Jun 2013 15:09:58 -0500 Subject: [PATCH 089/736] FIX: remove blit, fix 0-length error on grayscale images --- skimage/viewer/plugins/lineprofile.py | 29 ++++++++++----------------- 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/skimage/viewer/plugins/lineprofile.py b/skimage/viewer/plugins/lineprofile.py index 28b2f4aa..d15d22b1 100644 --- a/skimage/viewer/plugins/lineprofile.py +++ b/skimage/viewer/plugins/lineprofile.py @@ -82,18 +82,18 @@ class LineProfile(PlotPlugin): "Select and drag ends of the scan line to adjust it.") return '\n'.join(helpstr) - def get_profile(self): + def get_profiles(self): """Return intensity profile of the selected line. Returns ------- end_points: (2, 2) array The positions ((x1, y1), (x2, y2)) of the line ends. - profile: 1d array - Profile of intensity values. + profile: list of 1d arrays + Profile of intensity values. Length 1 (grayscale) or 3 (rgb). """ - profile = self.profile[0].get_ydata() - return self.line_tool.end_points, profile + profiles = [data.get_ydata() for data in self.profile] + return self.line_tool.end_points, profiles def _autoscale_view(self): if self.limits is None: @@ -119,9 +119,6 @@ class LineProfile(PlotPlugin): self.ax.relim() - if self.useblit: - self.ax.draw_artist(self.profile[0]) - self._autoscale_view() self.redraw() @@ -178,16 +175,12 @@ def profile_line(img, end_points, linewidth=1): # Quick calculation if perfectly vertical; shortcuts div0 error if x1 == x2: - for i in range(channels): - try: - intensities = np.concatenate( - (intensities, - _calc_vert(img[..., i], x1, x2, y1, y2, - linewidth)), axis=1) - except: - intensities = _calc_vert(img[..., i], - x1, x2, y1, y2, - linewidth) + if channels == 1: + img = img[:, :, np.newaxis] + + img = np.rollaxis(img, -1) + intensities = np.hstack([_calc_vert(im, x1, x2, y1, y2, linewidth) + for im in img]) return intensities theta = np.arctan2(dy, dx) From 19a3d335730b11085852e29bbe8d358b1cd8c158 Mon Sep 17 00:00:00 2001 From: "Josh Warner (Mac)" Date: Sat, 29 Jun 2013 15:18:53 -0500 Subject: [PATCH 090/736] FIX: only reset axes when gray <-> rgb, not every update --- skimage/viewer/plugins/lineprofile.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/skimage/viewer/plugins/lineprofile.py b/skimage/viewer/plugins/lineprofile.py index d15d22b1..af65f081 100644 --- a/skimage/viewer/plugins/lineprofile.py +++ b/skimage/viewer/plugins/lineprofile.py @@ -107,10 +107,7 @@ class LineProfile(PlotPlugin): scan = profile_line(self.image_viewer.original_image, end_points, linewidth=self.line_tool.linewidth) - try: - if scan[1].shape != len(self.profile): - self.reset_axes(scan) - except: + if scan[1].shape != len(self.profile): self.reset_axes(scan) for i in range(len(scan[0])): From 6f775400b5643fe4b015b11c65be8a331ee2d238 Mon Sep 17 00:00:00 2001 From: "Josh Warner (Mac)" Date: Sat, 29 Jun 2013 15:34:03 -0500 Subject: [PATCH 091/736] FIX: No longer reset for each update --- skimage/viewer/plugins/lineprofile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/viewer/plugins/lineprofile.py b/skimage/viewer/plugins/lineprofile.py index af65f081..0d555eb5 100644 --- a/skimage/viewer/plugins/lineprofile.py +++ b/skimage/viewer/plugins/lineprofile.py @@ -107,7 +107,7 @@ class LineProfile(PlotPlugin): scan = profile_line(self.image_viewer.original_image, end_points, linewidth=self.line_tool.linewidth) - if scan[1].shape != len(self.profile): + if scan.shape[1] != len(self.profile): self.reset_axes(scan) for i in range(len(scan[0])): From 94dc84f5b719c4114325a2e93f4ef8a476f76c38 Mon Sep 17 00:00:00 2001 From: Marianne Corvellec Date: Fri, 28 Jun 2013 18:34:50 -0400 Subject: [PATCH 092/736] Clarified that you can pass images of any type into Canny filter. If low and high thresholds are not specified by user, default to 10 and 20 % (respectively) of range of image's dtype. --- skimage/filter/_canny.py | 39 +++++++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/skimage/filter/_canny.py b/skimage/filter/_canny.py index 904919be..cd676eaf 100644 --- a/skimage/filter/_canny.py +++ b/skimage/filter/_canny.py @@ -1,4 +1,4 @@ -'''canny.py - Canny Edge detector +"""canny.py - Canny Edge detector Reference: Canny, J., A Computational Approach To Edge Detection, IEEE Trans. Pattern Analysis and Machine Intelligence, 8:679-714, 1986 @@ -9,13 +9,13 @@ Copyright (c) 2003-2009 Massachusetts Institute of Technology Copyright (c) 2009-2011 Broad Institute All rights reserved. Original author: Lee Kamentsky - -''' +""" import numpy as np import scipy.ndimage as ndi from scipy.ndimage import (gaussian_filter, generate_binary_structure, binary_erosion, label) +from skimage import dtype_limits def smooth_with_function_and_mask(image, function, mask): @@ -30,7 +30,7 @@ def smooth_with_function_and_mask(image, function, mask): A function that takes an image and returns a smoothed image mask : array - Mask with 1's for significant pixels, 0 for masked pixels + Mask with 1's for significant pixels, 0's for masked pixels. Notes ------ @@ -50,26 +50,27 @@ def smooth_with_function_and_mask(image, function, mask): return output_image -def canny(image, sigma=1., low_threshold=.1, high_threshold=.2, mask=None): - '''Edge filter an image using the Canny algorithm. +def canny(image, sigma=1., low_threshold=None, high_threshold=None, mask=None): + """Edge filter an image using the Canny algorithm. Parameters ----------- - image : array_like, dtype=float - The greyscale input image to detect edges on; should be normalized to - 0.0 to 1.0. + image : array_like, dtype=float or int + Greyscale input image to detect edges on; can be of any dtype. sigma : float - The standard deviation of the Gaussian filter + Standard deviation of the Gaussian filter. low_threshold : float - The lower bound for hysterisis thresholding (linking edges) + Lower bound for hysteresis thresholding (linking edges). + If none is provided, low_threshold is set to 10%. high_threshold : float - The upper bound for hysterisis thresholding (linking edges) + Upper bound for hysteresis thresholding (linking edges). + If none is provided, high_threshold is set to 20%. mask : array, dtype=bool, optional - An optional mask to limit the application of Canny to a certain area. + Mask to limit the application of Canny to a certain area. Returns ------- @@ -107,7 +108,7 @@ def canny(image, sigma=1., low_threshold=.1, high_threshold=.2, mask=None): Canny, J., A Computational Approach To Edge Detection, IEEE Trans. Pattern Analysis and Machine Intelligence, 8:679-714, 1986 - William Green' Canny tutorial + William Green's Canny tutorial http://dasl.mem.drexel.edu/alumni/bGreen/www.pages.drexel.edu/_weg22/can_tut.html Examples @@ -121,7 +122,7 @@ def canny(image, sigma=1., low_threshold=.1, high_threshold=.2, mask=None): >>> edges1 = filter.canny(im) >>> # Increase the smoothing for better results >>> edges2 = filter.canny(im, sigma=3) - ''' + """ # # The steps involved: @@ -154,7 +155,13 @@ def canny(image, sigma=1., low_threshold=.1, high_threshold=.2, mask=None): # if image.ndim != 2: - raise TypeError("The input 'image' must be a two dimensional array.") + raise TypeError("The input 'image' must be a two-dimensional array.") + + if low_threshold is None: + low_threshold = 0.1 * dtype_limits(image)[0] + + if high_threshold is None: + high_threshold = 0.2 * dtype_limits(image)[1] if mask is None: mask = np.ones(image.shape, dtype=bool) From 8f40e05aab3850bcba6ff3f92c4b532cef52fb2a Mon Sep 17 00:00:00 2001 From: Marianne Corvellec Date: Fri, 28 Jun 2013 18:51:33 -0400 Subject: [PATCH 093/736] Fixed relative threshold: you consider 10% and 20% of the max for range of corresponding dtype. --- skimage/filter/_canny.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/filter/_canny.py b/skimage/filter/_canny.py index cd676eaf..13731ac6 100644 --- a/skimage/filter/_canny.py +++ b/skimage/filter/_canny.py @@ -158,7 +158,7 @@ def canny(image, sigma=1., low_threshold=None, high_threshold=None, mask=None): raise TypeError("The input 'image' must be a two-dimensional array.") if low_threshold is None: - low_threshold = 0.1 * dtype_limits(image)[0] + low_threshold = 0.1 * dtype_limits(image)[1] if high_threshold is None: high_threshold = 0.2 * dtype_limits(image)[1] From ee7f710e554020f74d33d84f2090d1468de1c21a Mon Sep 17 00:00:00 2001 From: Marianne Corvellec Date: Fri, 28 Jun 2013 19:24:28 -0400 Subject: [PATCH 094/736] Improved style. --- skimage/filter/_canny.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/skimage/filter/_canny.py b/skimage/filter/_canny.py index 13731ac6..cbac5054 100644 --- a/skimage/filter/_canny.py +++ b/skimage/filter/_canny.py @@ -1,4 +1,5 @@ -"""canny.py - Canny Edge detector +""" +canny.py - Canny Edge detector Reference: Canny, J., A Computational Approach To Edge Detection, IEEE Trans. Pattern Analysis and Machine Intelligence, 8:679-714, 1986 @@ -24,11 +25,9 @@ def smooth_with_function_and_mask(image, function, mask): Parameters ---------- image : array - The image to smooth - + Image you want to smooth. function : callable - A function that takes an image and returns a smoothed image - + A function that does image smoothing. mask : array Mask with 1's for significant pixels, 0's for masked pixels. @@ -55,20 +54,16 @@ def canny(image, sigma=1., low_threshold=None, high_threshold=None, mask=None): Parameters ----------- - image : array_like, dtype=float or int + image : two-dimensional array Greyscale input image to detect edges on; can be of any dtype. - sigma : float Standard deviation of the Gaussian filter. - low_threshold : float Lower bound for hysteresis thresholding (linking edges). If none is provided, low_threshold is set to 10%. - high_threshold : float Upper bound for hysteresis thresholding (linking edges). If none is provided, high_threshold is set to 20%. - mask : array, dtype=bool, optional Mask to limit the application of Canny to a certain area. From 5b3081ccc6e2f03c3837679c3f332bb28032e085 Mon Sep 17 00:00:00 2001 From: Marianne Corvellec Date: Sat, 29 Jun 2013 18:29:55 -0400 Subject: [PATCH 095/736] Always follow the style guide :) --- skimage/filter/_canny.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/skimage/filter/_canny.py b/skimage/filter/_canny.py index cbac5054..1c5e5c19 100644 --- a/skimage/filter/_canny.py +++ b/skimage/filter/_canny.py @@ -25,11 +25,11 @@ def smooth_with_function_and_mask(image, function, mask): Parameters ---------- image : array - Image you want to smooth. + Image you want to smooth. function : callable - A function that does image smoothing. + A function that does image smoothing. mask : array - Mask with 1's for significant pixels, 0's for masked pixels. + Mask with 1's for significant pixels, 0's for masked pixels. Notes ------ @@ -55,22 +55,22 @@ def canny(image, sigma=1., low_threshold=None, high_threshold=None, mask=None): Parameters ----------- image : two-dimensional array - Greyscale input image to detect edges on; can be of any dtype. + Greyscale input image to detect edges on; can be of any dtype. sigma : float - Standard deviation of the Gaussian filter. + Standard deviation of the Gaussian filter. low_threshold : float - Lower bound for hysteresis thresholding (linking edges). - If none is provided, low_threshold is set to 10%. + Lower bound for hysteresis thresholding (linking edges). + If None, low_threshold is set to 10%. high_threshold : float - Upper bound for hysteresis thresholding (linking edges). - If none is provided, high_threshold is set to 20%. + Upper bound for hysteresis thresholding (linking edges). + If None, high_threshold is set to 20%. mask : array, dtype=bool, optional - Mask to limit the application of Canny to a certain area. + Mask to limit the application of Canny to a certain area. Returns ------- output : array (image) - The binary edge map. + The binary edge map. See also -------- @@ -112,7 +112,7 @@ def canny(image, sigma=1., low_threshold=None, high_threshold=None, mask=None): >>> # Generate noisy image of a square >>> im = np.zeros((256, 256)) >>> im[64:-64, 64:-64] = 1 - >>> im += 0.2*np.random.random(im.shape) + >>> im += 0.2 * np.random.random(im.shape) >>> # First trial with the Canny filter, with the default smoothing >>> edges1 = filter.canny(im) >>> # Increase the smoothing for better results From 75b3fcd4dde6b20765e707dbba2eee0a6f82225c Mon Sep 17 00:00:00 2001 From: Marianne Corvellec Date: Sat, 29 Jun 2013 18:34:41 -0400 Subject: [PATCH 096/736] Included Tony's edits. --- skimage/filter/_canny.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/skimage/filter/_canny.py b/skimage/filter/_canny.py index 1c5e5c19..185bfd40 100644 --- a/skimage/filter/_canny.py +++ b/skimage/filter/_canny.py @@ -54,22 +54,22 @@ def canny(image, sigma=1., low_threshold=None, high_threshold=None, mask=None): Parameters ----------- - image : two-dimensional array + image : 2D array Greyscale input image to detect edges on; can be of any dtype. sigma : float Standard deviation of the Gaussian filter. low_threshold : float Lower bound for hysteresis thresholding (linking edges). - If None, low_threshold is set to 10%. + If None, low_threshold is set to 10% of dtype's max. high_threshold : float Upper bound for hysteresis thresholding (linking edges). - If None, high_threshold is set to 20%. + If None, high_threshold is set to 20% of dtype's max. mask : array, dtype=bool, optional Mask to limit the application of Canny to a certain area. Returns ------- - output : array (image) + output : 2D array (image) The binary edge map. See also From 5f7fc5283094dd74e8385a8df7757241ed8dd017 Mon Sep 17 00:00:00 2001 From: "Josh Warner (Mac)" Date: Sat, 29 Jun 2013 18:00:26 -0500 Subject: [PATCH 097/736] FEAT: generator to add various types of random noise to images --- skimage/util/__init__.py | 4 +- skimage/util/noise.py | 119 ++++++++++++++++++++++++ skimage/util/tests/test_random_noise.py | 87 +++++++++++++++++ 3 files changed, 209 insertions(+), 1 deletion(-) create mode 100644 skimage/util/noise.py create mode 100644 skimage/util/tests/test_random_noise.py diff --git a/skimage/util/__init__.py b/skimage/util/__init__.py index 5433a14b..a4274484 100644 --- a/skimage/util/__init__.py +++ b/skimage/util/__init__.py @@ -1,6 +1,7 @@ from .dtype import (img_as_float, img_as_int, img_as_uint, img_as_ubyte, img_as_bool, dtype_limits) from .shape import view_as_blocks, view_as_windows +from .noise import random_noise import numpy ver = numpy.__version__.split('.') @@ -20,4 +21,5 @@ __all__ = ['img_as_float', 'dtype_limits', 'view_as_blocks', 'view_as_windows', - 'pad'] + 'pad', + 'random_noise'] diff --git a/skimage/util/noise.py b/skimage/util/noise.py new file mode 100644 index 00000000..bf1d412d --- /dev/null +++ b/skimage/util/noise.py @@ -0,0 +1,119 @@ +import numpy as np +from .dtype import img_as_float + + +__all__ = ['random_noise'] + + +def random_noise(image, mode='gaussian', seed=None, **kwargs): + """ + Function to add random noise of various types to a floating-point image. + + Parameters + ---------- + image : ndarray + Input image data. Will be converted to float. + mode : str + One of the following strings, selecting the type of noise to add: + + 'gaussian' Gaussian-distributed additive noise. + 'poisson' Poisson-distributed noise generated from the data. + 'salt' Replaces random pixels with 1. + 'pepper' Replaces random pixels with 0. + 's&p' Replaces random pixels with 0 or 1. + 'speckle' Multiplicative noise using out = image + n*image, where + n is uniform noise with specified mean & variance. + seed : int + If provided, this will set the random seed before generating noise. + m : float + Mean of random distribution. Used in 'gaussian' and 'speckle'. + v : float + Variance of random distribution. Used in 'gaussian' and 'speckle'. + Note: variance = (standard deviation) ** 2 + d : float + Proportion of image pixels to replace with noise on range [0, 1]. + Used in 'salt', 'pepper', and 'salt & pepper'. + p : float + Proportion of salt vs. pepper noise for 's&p' on range [0, 1]. + Higher values represent more salt. + + Returns + ------- + out : ndarray + Output floating-point image data on range [0, 1]. + + """ + mode = mode.lower() + image = img_as_float(image) + if seed is not None: + np.random.seed(seed=seed) + + allowedtypes = { + 'gaussian': 'gaussian_values', + 'poisson': '', + 'salt': 'sp_values', + 'pepper': 'sp_values', + 's&p': 's&p_values', + 'speckle': 'gaussian_values'} + + kwdefaults = { + 'm': 0., + 'v': 0.01, + 'd': 0.05, + 'p': 0.5} + + allowedkwargs = { + 'gaussian_values': ['m', 'v'], + 'sp_values': ['d'], + 's&p_values': ['d', 'p']} + + for key in kwargs: + if key not in allowedkwargs[allowedtypes[mode]]: + raise ValueError('%s keyword not in allowed keywords %s' % + (key, allowedkwargs[allowedtypes[mode]])) + + # Set kwarg defaults + for kw in allowedkwargs[allowedtypes[mode]]: + kwargs.setdefault(kw, kwdefaults[kw]) + + if mode == 'gaussian': + noise = np.random.normal(kwargs['m'], kwargs['v'] ** 0.5, image.shape) + out = np.clip(image + noise, 0., 1.) + + elif mode == 'poisson': + # Generating noise for each unique value in image. + out = np.zeros_like(image) + for val in np.unique(image): + # Generate mask for a unique value, replace w/values drawn from + # Poisson distribution about the unique value + mask = image == val + out[mask] = np.poisson(val, mask.sum()) + + elif mode == 'salt': + # Re-call function with mode='s&p' and p=1 (all salt noise) + out = random_noise(image, mode='s&p', seed=seed, d=kwargs['d'], p=1) + + elif mode == 'pepper': + # Re-call function with mode='s&p' and p=1 (all pepper noise) + out = random_noise(image, mode='s&p', seed=seed, d=kwargs['d'], p=0) + + elif mode == 's&p': + out = image.copy() + + # Salt mode + num_salt = np.ceil(kwargs['d'] * image.size * kwargs['p']) + coords = [np.random.randint(0, i - 1, num_salt) + for i in image.shape] + out[coords] = 1 + + # Pepper mode + num_pepper = np.ceil(kwargs['d'] * image.size * (1. - kwargs['p'])) + coords = [np.random.randint(0, i - 1, num_pepper) + for i in image.shape] + out[coords] = 0 + + elif mode == 'speckle': + noise = np.random.normal(kwargs['m'], kwargs['v'] ** 0.5, image.shape) + out = np.clip(image + image * noise, 0., 1.) + + return out diff --git a/skimage/util/tests/test_random_noise.py b/skimage/util/tests/test_random_noise.py new file mode 100644 index 00000000..98b78117 --- /dev/null +++ b/skimage/util/tests/test_random_noise.py @@ -0,0 +1,87 @@ +from numpy.testing import assert_array_equal, assert_allclose + +import numpy as np +from skimage.data import camera +from skimage.util import random_noise, img_as_float + + +def test_set_seed(): + seed = 42 + cam = camera() + test = random_noise(cam, seed=seed) + assert_array_equal(test, random_noise(cam, seed=seed)) + + +def test_salt(): + seed = 42 + cam = img_as_float(camera()) + cam_noisy = random_noise(cam, seed=seed, mode='salt', d=0.15) + saltmask = cam != cam_noisy + + # Ensure all changes are to 1.0 + assert_allclose(cam_noisy[saltmask], np.ones(saltmask.sum())) + + # Ensure approximately correct amount of noise was added + proportion = float(saltmask.sum()) / (cam.shape[0] * cam.shape[1]) + assert 0.11 < proportion <= 0.18 + + +def test_pepper(): + seed = 42 + cam = img_as_float(camera()) + cam_noisy = random_noise(cam, seed=seed, mode='pepper', d=0.15) + peppermask = cam != cam_noisy + + # Ensure all changes are to 1.0 + assert_allclose(cam_noisy[peppermask], np.zeros(peppermask.sum())) + + # Ensure approximately correct amount of noise was added + proportion = float(peppermask.sum()) / (cam.shape[0] * cam.shape[1]) + assert 0.11 < proportion <= 0.18 + + +def test_salt_and_pepper(): + seed = 42 + cam = img_as_float(camera()) + cam_noisy = random_noise(cam, seed=seed, mode='s&p', d=0.15, p=0.25) + saltmask = np.logical_and(cam != cam_noisy, cam_noisy == 1.) + peppermask = np.logical_and(cam != cam_noisy, cam_noisy == 0.) + + # Ensure all changes are to 0. or 1. + assert_allclose(cam_noisy[saltmask], np.ones(saltmask.sum())) + assert_allclose(cam_noisy[peppermask], np.zeros(peppermask.sum())) + + # Ensure approximately correct amount of noise was added + proportion = float( + saltmask.sum() + peppermask.sum()) / (cam.shape[0] * cam.shape[1]) + assert 0.11 < proportion <= 0.18 + + # Verify the relative amount of salt vs. pepper is close to expected + assert 0.18 < saltmask.sum() / float(peppermask.sum()) < 0.32 + + +def test_gaussian(): + seed = 42 + data = np.zeros((128, 128)) + 0.5 + data_gaussian = random_noise(data, seed=seed, v=0.01) + assert 0.008 < data_gaussian.var() < 0.012 + + data_gaussian = random_noise(data, seed=seed, m=0.3, v=0.015) + assert 0.28 < data_gaussian.mean() - 0.5 < 0.32 + assert 0.012 < data_gaussian.var() < 0.018 + + +def test_speckle(): + seed = 42 + data = np.zeros((128, 128)) + 0.1 + np.random.seed(seed=42) + noise = np.random.normal(0.1, 0.02 ** 0.5, (128, 128)) + expected = np.clip(data + data * noise, 0, 1) + + data_speckle = random_noise(data, mode='speckle', seed=seed, m=0.1, + v=0.02) + assert_allclose(expected, data_speckle) + + +if __name__ == '__main__': + np.testing.run_module_suite() From 95352a586cb5d3e853ed405e6febae4ebe4396f6 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Sun, 30 Jun 2013 14:43:55 +0800 Subject: [PATCH 098/736] Added docs and used numpy trickery to filter keypoints --- skimage/feature/_brief.py | 66 ++++++++++++++++++++++++++++----------- 1 file changed, 48 insertions(+), 18 deletions(-) diff --git a/skimage/feature/_brief.py b/skimage/feature/_brief.py index 40ad204b..22f62e71 100644 --- a/skimage/feature/_brief.py +++ b/skimage/feature/_brief.py @@ -4,32 +4,63 @@ import numpy as np from skimage.color import rgb2gray from scipy.ndimage.filters import gaussian_filter +from scipy.spatial.distance import hamming + def _remove_border_keypoints(image, keypoints, dist): width = image.shape[0] height = image.shape[1] - keypoints_list = keypoints.tolist() - - for i, j in keypoints_list: - if i > width - dist[0] or i < dist[0] or j < dist[1] or j > height - dist[0]: - keypoints.remove([i, j]) - - keypoints = np.asarray(keypoints_list) + keypoints = keypoints[(dist < keypoints[:, 0]) & (keypoints[:, 0] < width - dist) & + (dist < keypoints[:, 1]) & (keypoints[:, 1] < height - dist)] return keypoints def brief(image, keypoints, descriptor_size=256, mode='normal', patch_size=49, sample_seed=1): + """Extract BRIEF Descriptor about given keypoints for a given image. + + Parameters + ---------- + image : ndarray + Input image. + keypoints : (P, 2) ndarray + Array of keypoint locations. + descriptor_size : int + Size of BRIEF descriptor about each keypoint. Sizes 128, 256 and 512 + preferred by the authors. Default is 256. + mode : string + Probability distribution for sampling location of decision pixel-pairs + around keypoints. Default is 'normal' otherwise uniform. + patch_size : int + Length of the two dimensional square patch sampling region around + the keypoints. Default is 49. + sample_seed : int + Seed for sampling the decision pixel-pairs. Default is 1. + + Returns + ------- + descriptor : ndarray with dtype bool + 2D ndarray of dimensions (no_of_keypoints, descriptor_size) with value + at an index (i, j) either being True or False representing the outcome + of Intensity comparison about ith keypoint on jth decision pixel-pair. + + References + ---------- + .. [1] Michael Calonder, Vincent Lepetit, Christoph Strecha, and Pascal Fua + "BRIEF : Binary robust independent elementary features", + http://cvlabwww.epfl.ch/~lepetit/papers/calonder_eccv10.pdf + + """ if np.squeeze(image).ndim == 3: image = rgb2gray(image) - keypoints = np.round(keypoints) + # Removing keypoints that are (patch_size / 2) distance from the image border + keypoints = np.array(keypoints + 0.5, dtype=np.intp) + keypoints = _remove_border_keypoints(image, keypoints, patch_size / 2) - keypoints = _remove_border_keypoints(image, keypoints, (patch_size / 2, patch_size / 2)) - - descriptor = np.zeros((len(keypoints), descriptor_size), dtype=int) + descriptor = np.zeros((len(keypoints), descriptor_size), dtype=bool) # Gaussian Low pass filtering with variance 2 to alleviate noise sensitivity image = gaussian_filter(image, 2) @@ -38,8 +69,7 @@ def brief(image, keypoints, descriptor_size=256, mode='normal', patch_size=49, s if mode == 'normal': np.random.seed(sample_seed) samples = np.round((patch_size / 5) * np.random.randn(descriptor_size * 8)) - samples = samples[samples < (patch_size / 2)] - samples = samples[samples > - (patch_size - 1) / 2] + samples = samples[(samples < (patch_size / 2)) & (samples > - (patch_size - 1) / 2)] first = (samples[: descriptor_size * 2]).reshape(descriptor_size, 2) second = (samples[descriptor_size * 2: descriptor_size * 4]).reshape(descriptor_size, 2) else: @@ -47,22 +77,22 @@ def brief(image, keypoints, descriptor_size=256, mode='normal', patch_size=49, s samples = np.random.randint(-patch_size / 2, (patch_size / 2) + 1, (descriptor_size * 2, 2)) first, second = np.split(samples, 2) + # Intensity comparison tests for building the descriptor for i in range(len(keypoints)): set_1 = first + keypoints[i] set_2 = second + keypoints[i] for j in range(descriptor_size): if image[set_1[j, 0]][set_1[j, 1]] < image[set_2[j, 0]][set_2[j, 0]]: - descriptor[i][j] = 1 - else: - descriptor[i][j] = 0 + descriptor[i][j] = True return descriptor + def hamming_distance(descriptor_1, descriptor_2): distance = np.zeros((len(descriptor_1), len(descriptor_2)), dtype=int) for i in range(len(descriptor_1)): for j in range(len(descriptor_2)): - distance[i, j] = sum(np.bitwise_xor(descriptor_1[i][:], descriptor_2[j][:])) - return distance / descriptor_1.shape[1] + distance[i, j] = hamming(descriptor_1[i][:], descriptor_2[j][:]) + return distance From a746834e09af8dfb18ff332288591d4617a692e2 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Sun, 30 Jun 2013 21:04:36 +0800 Subject: [PATCH 099/736] Changed dtype of hamming dist matrix and added docs --- skimage/feature/_brief.py | 37 ++++++++++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/skimage/feature/_brief.py b/skimage/feature/_brief.py index 22f62e71..a5ce37b7 100644 --- a/skimage/feature/_brief.py +++ b/skimage/feature/_brief.py @@ -1,6 +1,3 @@ -# TODO Normal sampling from image patch of size 49 x 49 -# TODO Tests, example, doc - import numpy as np from skimage.color import rgb2gray from scipy.ndimage.filters import gaussian_filter @@ -20,7 +17,6 @@ def _remove_border_keypoints(image, keypoints, dist): def brief(image, keypoints, descriptor_size=256, mode='normal', patch_size=49, sample_seed=1): """Extract BRIEF Descriptor about given keypoints for a given image. - Parameters ---------- image : ndarray @@ -48,7 +44,7 @@ def brief(image, keypoints, descriptor_size=256, mode='normal', patch_size=49, s References ---------- - .. [1] Michael Calonder, Vincent Lepetit, Christoph Strecha, and Pascal Fua + .. [1] Michael Calonder, Vincent Lepetit, Christoph Strecha and Pascal Fua "BRIEF : Binary robust independent elementary features", http://cvlabwww.epfl.ch/~lepetit/papers/calonder_eccv10.pdf @@ -56,8 +52,9 @@ def brief(image, keypoints, descriptor_size=256, mode='normal', patch_size=49, s if np.squeeze(image).ndim == 3: image = rgb2gray(image) - # Removing keypoints that are (patch_size / 2) distance from the image border keypoints = np.array(keypoints + 0.5, dtype=np.intp) + + # Removing keypoints that are (patch_size / 2) distance from the image border keypoints = _remove_border_keypoints(image, keypoints, patch_size / 2) descriptor = np.zeros((len(keypoints), descriptor_size), dtype=bool) @@ -90,8 +87,34 @@ def brief(image, keypoints, descriptor_size=256, mode='normal', patch_size=49, s def hamming_distance(descriptor_1, descriptor_2): + """A dissimilarity measure used for matching keypoints in different images + using binary feature descriptors like BRIEF etc. - distance = np.zeros((len(descriptor_1), len(descriptor_2)), dtype=int) + Parameters + ---------- + descriptor_1 : ndarray with dtype bool + Binary feature descriptor for keypoints in the first image. + 2D ndarray of dimensions (no_of_keypoints_in_image_1, descriptor_size) + with value at an index (i, j) either being True or False representing + the outcome of Intensity comparison about ith keypoint on jth decision + pixel-pair. + descriptor_2 : ndarray with dtype bool + Binary feature descriptor for keypoints in the second image. + 2D ndarray of dimensions (no_of_keypoints_in_image_2, descriptor_size) + with value at an index (i, j) either being True or False representing + the outcome of Intensity comparison about ith keypoint on jth decision + pixel-pair. + + Returns + ------- + distance : ndarray + 2D ndarray of dimensions (no_of_rows_in_descripto_1, no_of_rows_in_descripto_2) + with value at an index (i, j) between the range [0, 1] representing the + extent of dissimilarity between ith keypoint of in first image and jth + keypoint in second image. + + """ + distance = np.zeros((len(descriptor_1), len(descriptor_2)), dtype=float) for i in range(len(descriptor_1)): for j in range(len(descriptor_2)): distance[i, j] = hamming(descriptor_1[i][:], descriptor_2[j][:]) From f3b827d21b6686839f2a117a3ed4dd57e2419969 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Sun, 30 Jun 2013 21:12:54 +0800 Subject: [PATCH 100/736] Fixing indentation --- skimage/feature/_brief.py | 108 +++++++++++++++++++------------------- 1 file changed, 54 insertions(+), 54 deletions(-) diff --git a/skimage/feature/_brief.py b/skimage/feature/_brief.py index a5ce37b7..b12d827b 100644 --- a/skimage/feature/_brief.py +++ b/skimage/feature/_brief.py @@ -6,16 +6,16 @@ from scipy.spatial.distance import hamming def _remove_border_keypoints(image, keypoints, dist): - width = image.shape[0] - height = image.shape[1] + width = image.shape[0] + height = image.shape[1] - keypoints = keypoints[(dist < keypoints[:, 0]) & (keypoints[:, 0] < width - dist) & - (dist < keypoints[:, 1]) & (keypoints[:, 1] < height - dist)] - return keypoints + keypoints = keypoints[(dist < keypoints[:, 0]) & (keypoints[:, 0] < width - dist) & + (dist < keypoints[:, 1]) & (keypoints[:, 1] < height - dist)] + return keypoints def brief(image, keypoints, descriptor_size=256, mode='normal', patch_size=49, sample_seed=1): - """Extract BRIEF Descriptor about given keypoints for a given image. + """Extract BRIEF Descriptor about given keypoints for a given image. Parameters ---------- @@ -49,73 +49,73 @@ def brief(image, keypoints, descriptor_size=256, mode='normal', patch_size=49, s http://cvlabwww.epfl.ch/~lepetit/papers/calonder_eccv10.pdf """ - if np.squeeze(image).ndim == 3: - image = rgb2gray(image) + if np.squeeze(image).ndim == 3: + image = rgb2gray(image) - keypoints = np.array(keypoints + 0.5, dtype=np.intp) + keypoints = np.array(keypoints + 0.5, dtype=np.intp) - # Removing keypoints that are (patch_size / 2) distance from the image border - keypoints = _remove_border_keypoints(image, keypoints, patch_size / 2) + # Removing keypoints that are (patch_size / 2) distance from the image border + keypoints = _remove_border_keypoints(image, keypoints, patch_size / 2) - descriptor = np.zeros((len(keypoints), descriptor_size), dtype=bool) + descriptor = np.zeros((len(keypoints), descriptor_size), dtype=bool) - # Gaussian Low pass filtering with variance 2 to alleviate noise sensitivity - image = gaussian_filter(image, 2) + # Gaussian Low pass filtering with variance 2 to alleviate noise sensitivity + image = gaussian_filter(image, 2) - # Sampling pairs of decision pixels in patch_size x patch_size window - if mode == 'normal': - np.random.seed(sample_seed) - samples = np.round((patch_size / 5) * np.random.randn(descriptor_size * 8)) - samples = samples[(samples < (patch_size / 2)) & (samples > - (patch_size - 1) / 2)] - first = (samples[: descriptor_size * 2]).reshape(descriptor_size, 2) - second = (samples[descriptor_size * 2: descriptor_size * 4]).reshape(descriptor_size, 2) - else: - np.random.seed(sample_seed) - samples = np.random.randint(-patch_size / 2, (patch_size / 2) + 1, (descriptor_size * 2, 2)) - first, second = np.split(samples, 2) + # Sampling pairs of decision pixels in patch_size x patch_size window + if mode == 'normal': + np.random.seed(sample_seed) + samples = np.round((patch_size / 5) * np.random.randn(descriptor_size * 8)) + samples = samples[(samples < (patch_size / 2)) & (samples > - (patch_size - 1) / 2)] + first = (samples[: descriptor_size * 2]).reshape(descriptor_size, 2) + second = (samples[descriptor_size * 2: descriptor_size * 4]).reshape(descriptor_size, 2) + else: + np.random.seed(sample_seed) + samples = np.random.randint(-patch_size / 2, (patch_size / 2) + 1, (descriptor_size * 2, 2)) + first, second = np.split(samples, 2) - # Intensity comparison tests for building the descriptor - for i in range(len(keypoints)): - set_1 = first + keypoints[i] - set_2 = second + keypoints[i] + # Intensity comparison tests for building the descriptor + for i in range(len(keypoints)): + set_1 = first + keypoints[i] + set_2 = second + keypoints[i] - for j in range(descriptor_size): - if image[set_1[j, 0]][set_1[j, 1]] < image[set_2[j, 0]][set_2[j, 0]]: - descriptor[i][j] = True + for j in range(descriptor_size): + if image[set_1[j, 0]][set_1[j, 1]] < image[set_2[j, 0]][set_2[j, 0]]: + descriptor[i][j] = True - return descriptor + return descriptor def hamming_distance(descriptor_1, descriptor_2): - """A dissimilarity measure used for matching keypoints in different images - using binary feature descriptors like BRIEF etc. + """A dissimilarity measure used for matching keypoints in different images + using binary feature descriptors like BRIEF etc. Parameters ---------- descriptor_1 : ndarray with dtype bool - Binary feature descriptor for keypoints in the first image. - 2D ndarray of dimensions (no_of_keypoints_in_image_1, descriptor_size) - with value at an index (i, j) either being True or False representing - the outcome of Intensity comparison about ith keypoint on jth decision - pixel-pair. + Binary feature descriptor for keypoints in the first image. + 2D ndarray of dimensions (no_of_keypoints_in_image_1, descriptor_size) + with value at an index (i, j) either being True or False representing + the outcome of Intensity comparison about ith keypoint on jth decision + pixel-pair. descriptor_2 : ndarray with dtype bool - Binary feature descriptor for keypoints in the second image. - 2D ndarray of dimensions (no_of_keypoints_in_image_2, descriptor_size) - with value at an index (i, j) either being True or False representing - the outcome of Intensity comparison about ith keypoint on jth decision - pixel-pair. + Binary feature descriptor for keypoints in the second image. + 2D ndarray of dimensions (no_of_keypoints_in_image_2, descriptor_size) + with value at an index (i, j) either being True or False representing + the outcome of Intensity comparison about ith keypoint on jth decision + pixel-pair. Returns ------- distance : ndarray - 2D ndarray of dimensions (no_of_rows_in_descripto_1, no_of_rows_in_descripto_2) - with value at an index (i, j) between the range [0, 1] representing the - extent of dissimilarity between ith keypoint of in first image and jth - keypoint in second image. + 2D ndarray of dimensions (no_of_rows_in_descripto_1, no_of_rows_in_descripto_2) + with value at an index (i, j) between the range [0, 1] representing the + extent of dissimilarity between ith keypoint of in first image and jth + keypoint in second image. """ - distance = np.zeros((len(descriptor_1), len(descriptor_2)), dtype=float) - for i in range(len(descriptor_1)): - for j in range(len(descriptor_2)): - distance[i, j] = hamming(descriptor_1[i][:], descriptor_2[j][:]) - return distance + distance = np.zeros((len(descriptor_1), len(descriptor_2)), dtype=float) + for i in range(len(descriptor_1)): + for j in range(len(descriptor_2)): + distance[i, j] = hamming(descriptor_1[i][:], descriptor_2[j][:]) + return distance From 42e07a8ef6b75343ddc8b6382ca1180bdcf2d87f Mon Sep 17 00:00:00 2001 From: tonysyu Date: Mon, 1 Jul 2013 20:33:30 -0500 Subject: [PATCH 101/736] Fix naming bug --- doc/examples/plot_local_binary_pattern.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/examples/plot_local_binary_pattern.py b/doc/examples/plot_local_binary_pattern.py index 98ea08d4..c97a792d 100644 --- a/doc/examples/plot_local_binary_pattern.py +++ b/doc/examples/plot_local_binary_pattern.py @@ -207,7 +207,7 @@ plt.gray() ax1.imshow(brick) ax1.axis('off') -hist(ax, refs['brick']) +hist(ax4, refs['brick']) ax4.set_ylabel('Percentage') ax2.imshow(grass) From 19f264bc8b7092fc6e3b8f86181fbf7679b24d52 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Mon, 11 Mar 2013 21:03:12 +1100 Subject: [PATCH 102/736] Make color functions used by SLIC 3D-aware --- skimage/color/colorconv.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/skimage/color/colorconv.py b/skimage/color/colorconv.py index 1ef5a6c9..6789dbbc 100644 --- a/skimage/color/colorconv.py +++ b/skimage/color/colorconv.py @@ -59,7 +59,7 @@ def is_rgb(image): Input image. """ - return (image.ndim == 3 and image.shape[2] in (3, 4)) + return (image.ndim in (3, 4) and image.shape[-1] in (3, 4)) @deprecated() @@ -72,7 +72,7 @@ def is_gray(image): Input image. """ - return np.squeeze(image).ndim == 2 + return image.ndim in (2, 3) and not is_rgb(image) def convert_colorspace(arr, fromspace, tospace): @@ -628,23 +628,24 @@ def gray2rgb(image): Parameters ---------- image : array_like - Input image of shape ``(M, N)``. + Input image of shape ``(M, N [, P])``. Returns ------- rgb : ndarray - RGB image of shape ``(M, N, 3)``. + RGB image of shape ``(M, N, [, P], 3)``. Raises ------ ValueError - If the input is not 2-dimensional. + If the input is not a 2- or 3-dimensional image. """ if np.squeeze(image).ndim == 3 and image.shape[2] in (3, 4): return image - elif image.ndim == 2 or np.squeeze(image).ndim == 2: - return np.dstack((image, image, image)) + elif is_gray(image): + image = image[..., np.newaxis] + return np.concatenate((image,)*3, axis=-1) else: raise ValueError("Input image expected to be RGB, RGBA or gray.") From 0132998ca111b4cd35620d05d27a82877636c02c Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Tue, 12 Mar 2013 00:28:49 +1100 Subject: [PATCH 103/736] Make more colorconv functions 3D aware --- skimage/color/colorconv.py | 36 +++++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/skimage/color/colorconv.py b/skimage/color/colorconv.py index 6789dbbc..be2f1519 100644 --- a/skimage/color/colorconv.py +++ b/skimage/color/colorconv.py @@ -129,8 +129,8 @@ def _prepare_colorarray(arr): """ arr = np.asanyarray(arr) - if arr.ndim != 3 or arr.shape[2] != 3: - msg = "the input array must be have a shape == (.,.,3))" + if arr.ndim not in [3, 4] or arr.shape[-1] != 3: + msg = "the input array must be have a shape == (.., ..,[ ..,] 3))" raise ValueError(msg) return dtype.img_as_float(arr) @@ -413,12 +413,12 @@ def _convert(matrix, arr): The converted array. """ arr = _prepare_colorarray(arr) - arr = np.swapaxes(arr, 0, 2) + arr = np.swapaxes(arr, 0, -1) oldshape = arr.shape arr = np.reshape(arr, (3, -1)) out = np.dot(matrix, arr) out.shape = oldshape - out = np.swapaxes(out, 2, 0) + out = np.swapaxes(out, -1, 0) return np.ascontiguousarray(out) @@ -473,17 +473,19 @@ def rgb2xyz(rgb): Parameters ---------- rgb : array_like - The image in RGB format, in a 3-D array of shape (.., .., 3). + The image in RGB format, in a 3- or 4-D array of shape + (.., ..,[ ..,] 3). Returns ------- out : ndarray - The image in XYZ format, in a 3-D array of shape (.., .., 3). + The image in XYZ format, in a 3- or 4-D array of shape + (.., ..,[ ..,] 3). Raises ------ ValueError - If `rgb` is not a 3-D array of shape (.., .., 3). + If `rgb` is not a 3- or 4-D array of shape (.., ..,[ ..,] 3). Notes ----- @@ -656,17 +658,19 @@ def xyz2lab(xyz): Parameters ---------- xyz : array_like - The image in XYZ format, in a 3-D array of shape (.., .., 3). + The image in XYZ format, in a 3- or 4-D array of shape + (.., ..,[ ..,] 3). Returns ------- out : ndarray - The image in CIE-LAB format, in a 3-D array of shape (.., .., 3). + The image in CIE-LAB format, in a 3- or 4-D array of shape + (.., ..,[ ..,] 3). Raises ------ ValueError - If `xyz` is not a 3-D array of shape (.., .., 3). + If `xyz` is not a 3-D array of shape (.., ..,[ ..,] 3). Notes ----- @@ -696,14 +700,14 @@ def xyz2lab(xyz): arr[mask] = np.power(arr[mask], 1. / 3.) arr[~mask] = 7.787 * arr[~mask] + 16. / 116. - x, y, z = arr[:, :, 0], arr[:, :, 1], arr[:, :, 2] + x, y, z = arr[..., 0], arr[..., 1], arr[..., 2] # Vector scaling L = (116. * y) - 16. a = 500.0 * (x - y) b = 200.0 * (y - z) - return np.dstack([L, a, b]) + return np.concatenate(map(lambda x: x[..., np.newaxis], [L, a, b]), -1) def lab2xyz(lab): @@ -760,17 +764,19 @@ def rgb2lab(rgb): Parameters ---------- rgb : array_like - The image in RGB format, in a 3-D array of shape (.., .., 3). + The image in RGB format, in a 3- or 4-D array of shape + (.., ..,[ ..,] 3). Returns ------- out : ndarray - The image in Lab format, in a 3-D array of shape (.., .., 3). + The image in Lab format, in a 3- or 4-D array of shape + (.., ..,[ ..,] 3). Raises ------ ValueError - If `rgb` is not a 3-D array of shape (.., .., 3). + If `rgb` is not a 3- or 4-D array of shape (.., ..,[ ..,] 3). Notes ----- From a2e32cc90c5271308ec32ee588cfeff5ae6fd445 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Thu, 14 Mar 2013 01:56:14 +1100 Subject: [PATCH 104/736] Add initial 3D modifications (not working) --- skimage/segmentation/_slic.pyx | 152 +++++++++++++++--------- skimage/segmentation/tests/test_slic.py | 2 +- 2 files changed, 98 insertions(+), 56 deletions(-) diff --git a/skimage/segmentation/_slic.pyx b/skimage/segmentation/_slic.pyx index 9a5374d6..35a55804 100644 --- a/skimage/segmentation/_slic.pyx +++ b/skimage/segmentation/_slic.pyx @@ -2,6 +2,7 @@ #cython: boundscheck=False #cython: nonecheck=False #cython: wraparound=False +import collections as coll import numpy as np from time import time from scipy import ndimage @@ -13,24 +14,28 @@ from ..color import rgb2lab, gray2rgb def slic(image, n_segments=100, ratio=10., max_iter=10, sigma=1, - convert2lab=True): + multichannel=True, convert2lab=True): """Segments image using k-means clustering in Color-(x,y) space. Parameters ---------- - image : (width, height [, 3]) ndarray - Input image. - n_segments : int, optional (default 100) + image : (width, height [, depth] [, 3]) ndarray + Input image, which can be 2D or 3D, and grayscale or multi-channel + (see `multichannel` parameter). + n_segments : int, optional (default: 100) The (approximate) number of labels in the segmented output image. - ratio: float, optional (default 10) + ratio: float, optional (default: 10) Balances color-space proximity and image-space proximity. Higher values give more weight to color-space. - max_iter : int, optional (default 10) + max_iter : int, optional (default: 10) Maximum number of iterations of k-means. - sigma : float, optional (default 1) + sigma : float, optional (default: 1) Width of Gaussian smoothing kernel for preprocessing. Zero means no smoothing. - convert2lab : bool, optional (default True) + multichannel : bool, optional (default: True) + Whether the last axis of the image is to be interpreted as multiple + channels. Only 3 channels are supported. + convert2lab : bool, optional (default: True) Whether the input should be converted to Lab colorspace prior to segmentation. For this purpose, the input is assumed to be RGB. Highly recommended. @@ -40,9 +45,19 @@ def slic(image, n_segments=100, ratio=10., max_iter=10, sigma=1, segment_mask : (width, height) ndarray Integer mask indicating segment labels. + Raises + ------ + ValueError + If: + - the image dimension is not 2 or 3 and `multichannel == False`, OR + - the image dimension is not 3 or 4 and `multichannel == True`, OR + - `multichannel == True` and the length of the last dimension of + the image is not 3. + Notes ----- - The image is smoothed using a Gaussian kernel prior to segmentation. + The image is optionally smoothed using a Gaussian kernel prior to + segmentation. References ---------- @@ -59,42 +74,64 @@ def slic(image, n_segments=100, ratio=10., max_iter=10, sigma=1, >>> # Increasing the ratio parameter yields more square regions >>> segments = slic(img, n_segments=100, ratio=20) """ - if image.ndim == 2: + if ((not multichannel and image.ndim not in [2, 3]) or + (multichannel and image.ndim not in [3, 4]) or + (multichannel and image.shape[-1] != 3)): + ValueError("Only 1- or 3-channel 2- or 3-D images are supported.") + if image.ndim in [2, 3] and not multichannel: image = gray2rgb(image) - if image.ndim != 3 or image.shape[2] != 3: - ValueError("Only 1- or 3-channel 2D images are supported.") - image = ndimage.gaussian_filter(img_as_float(image), [sigma, sigma, 0]) + if image.ndim == 3: + # See 2D RGB image as 3D RGB image with Z = 1 + image = image[np.newaxis, ...] + if not isinstance(sigma, coll.Iterable): + sigma = np.array([sigma, sigma, sigma, 0]) + if (sigma > 0).any(): + image = ndimage.gaussian_filter(img_as_float(image), sigma) if convert2lab: image = rgb2lab(image) # initialize on grid: - cdef Py_ssize_t height, width - height, width = image.shape[:2] + cdef Py_ssize_t depth, height, width + depth, height, width = image.shape[:3] # approximate grid size for desired n_segments - cdef Py_ssize_t step = int(np.ceil(np.sqrt(height * width / n_segments))) - grid_y, grid_x = np.mgrid[:height, :width] - means_y = grid_y[::step, ::step] - means_x = grid_x[::step, ::step] + cdef Py_ssize_t step = int(np.ceil( + (depth * height * width / n_segments) ** + (1.0/3))) + grid_z, grid_y, grid_x = np.mgrid[:depth, :height, :width] + means_z = grid_z[::step, ::step, ::step] + means_y = grid_y[::step, ::step, ::step] + means_x = grid_x[::step, ::step, ::step] - means_color = np.zeros((means_y.shape[0], means_y.shape[1], 3)) - cdef cnp.ndarray[dtype=cnp.float_t, ndim=2] means \ - = np.dstack([means_y, means_x, means_color]).reshape(-1, 5) + means_color = np.zeros(means_z.shape + (3,)) + cdef cnp.ndarray[dtype=cnp.float_t, ndim=2] means = \ + np.concatenate([ + means_z[..., np.newaxis], + means_y[..., np.newaxis], + means_x[..., np.newaxis], + means_color + ], axis=-1).reshape(-1, 6) cdef cnp.float_t* current_mean cdef cnp.float_t* mean_entry n_means = means.shape[0] # we do the scaling of ratio in the same way as in the SLIC paper # so the values have the same meaning ratio = (ratio / float(step)) ** 2 - cdef cnp.ndarray[dtype=cnp.float_t, ndim=3] image_yx \ - = np.dstack([grid_y, grid_x, image / ratio]).copy("C") - cdef Py_ssize_t i, k, x, y, x_min, x_max, y_min, y_max, changes + cdef cnp.ndarray[dtype=cnp.float_t, ndim=4] image_zyx \ + = np.concatenate([ + grid_y[..., np.newaxis], + grid_x[..., np.newaxis], + grid_z[..., np.newaxis], + image / ratio + ], axis=-1).copy("C") + cdef Py_ssize_t i, k, x, y, z, x_min, x_max, y_min, y_max, z_min, z_max, \ + changes cdef double dist_mean - cdef cnp.ndarray[dtype=cnp.intp_t, ndim=2] nearest_mean \ - = np.zeros((height, width), dtype=np.intp) - cdef cnp.ndarray[dtype=cnp.float_t, ndim=2] distance \ - = np.empty((height, width)) - cdef cnp.float_t* image_p = image_yx.data + cdef cnp.ndarray[dtype=cnp.intp_t, ndim=3] nearest_mean \ + = np.zeros((depth, height, width), dtype=np.intp) + cdef cnp.ndarray[dtype=cnp.float_t, ndim=3] distance \ + = np.empty((depth, height, width)) + cdef cnp.float_t* image_p = image_zyx.data cdef cnp.float_t* distance_p = distance.data cdef cnp.float_t* current_distance cdef cnp.float_t* current_pixel @@ -106,35 +143,40 @@ def slic(image, n_segments=100, ratio=10., max_iter=10, sigma=1, # assign pixels to means for k in range(n_means): # compute windows: - y_min = int(max(current_mean[0] - 2 * step, 0)) - y_max = int(min(current_mean[0] + 2 * step, height)) - x_min = int(max(current_mean[1] - 2 * step, 0)) - x_max = int(min(current_mean[1] + 2 * step, width)) - for y in range(y_min, y_max): - current_pixel = &image_p[5 * (y * width + x_min)] - current_distance = &distance_p[y * width + x_min] - for x in range(x_min, x_max): - mean_entry = current_mean - dist_mean = 0 - for c in range(5): - # you would think the compiler can optimize the squaring - # itself. mine can't (with O2) - tmp = current_pixel[0] - mean_entry[0] - dist_mean += tmp * tmp - current_pixel += 1 - mean_entry += 1 - # some precision issue here. Doesnt work if testing ">" - if current_distance[0] - dist_mean > 1e-10: - nearest_mean[y, x] = k - current_distance[0] = dist_mean - changes += 1 - current_distance += 1 - current_mean += 5 + z_min = int(max(current_mean[0] - 2 * step, 0)) + z_max = int(min(current_mean[0] + 2 * step, depth)) + y_min = int(max(current_mean[1] - 2 * step, 0)) + y_max = int(min(current_mean[1] + 2 * step, height)) + x_min = int(max(current_mean[2] - 2 * step, 0)) + x_max = int(min(current_mean[2] + 2 * step, width)) + for z in range(z_min, z_max): + for y in range(y_min, y_max): + current_pixel = \ + &image_p[5 * ((z * height + y) * width + x_min)] + current_distance = \ + &distance_p[(z * height + y) * width + x_min] + for x in range(x_min, x_max): + mean_entry = current_mean + dist_mean = 0 + for c in range(5): + # you would think the compiler can optimize the + # squaring itself. mine can't (with O2) + tmp = current_pixel[0] - mean_entry[0] + dist_mean += tmp * tmp + current_pixel += 1 + mean_entry += 1 + # some precision issue here. Doesnt work if testing ">" + if current_distance[0] - dist_mean > 1e-10: + nearest_mean[z, y, x] = k + current_distance[0] = dist_mean + changes += 1 + current_distance += 1 + current_mean += 6 if changes == 0: break # recompute means: means_list = [np.bincount(nearest_mean.ravel(), - image_yx[:, :, j].ravel()) for j in range(5)] + image_zyx[:, :, :, j].ravel()) for j in range(6)] in_mean = np.bincount(nearest_mean.ravel()) in_mean[in_mean == 0] = 1 means = (np.vstack(means_list) / in_mean).T.copy("C") diff --git a/skimage/segmentation/tests/test_slic.py b/skimage/segmentation/tests/test_slic.py index 89dee59b..b080e378 100644 --- a/skimage/segmentation/tests/test_slic.py +++ b/skimage/segmentation/tests/test_slic.py @@ -30,7 +30,7 @@ def test_gray(): img += 0.0033 * rnd.normal(size=img.shape) img[img > 1] = 1 img[img < 0] = 0 - seg = slic(img, sigma=0, n_segments=4, ratio=50.0) + seg = slic(img, sigma=0, n_segments=4, ratio=50.0, multichannel=False) assert_equal(len(np.unique(seg)), 4) assert_array_equal(seg[:10, :10], 0) From bb8cfea8c63822565987a623b8d66e7a405a8e6c Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Thu, 14 Mar 2013 18:13:49 +1100 Subject: [PATCH 105/736] Add function to calculate regularly-spaced grid in nD --- skimage/util/__init__.py | 2 ++ skimage/util/regular_grid.py | 44 ++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 skimage/util/regular_grid.py diff --git a/skimage/util/__init__.py b/skimage/util/__init__.py index a4274484..474b2966 100644 --- a/skimage/util/__init__.py +++ b/skimage/util/__init__.py @@ -11,6 +11,7 @@ if chk < 18: # Use internal version for numpy versions < 1.8.x else: from numpy import pad del numpy, ver, chk +from .regular_grid import regular_grid __all__ = ['img_as_float', @@ -23,3 +24,4 @@ __all__ = ['img_as_float', 'view_as_windows', 'pad', 'random_noise'] + 'regular_grid'] diff --git a/skimage/util/regular_grid.py b/skimage/util/regular_grid.py new file mode 100644 index 00000000..40980491 --- /dev/null +++ b/skimage/util/regular_grid.py @@ -0,0 +1,44 @@ +import numpy as np + +def regular_grid(ar_shape, n_points): + """Find `n_points` regularly spaced along `ar_shape`. + + The returned points (as slices) should be as close to cubically-spaced as + possible. + + Parameters + ---------- + ar_shape : array-like of ints + The shape of the space embedding the grid. `len(ar_shape)` is the + number of dimensions. + n_points : int + The (approximate) number of points to embed in the space. + + Returns + ------- + slices : list of slice objects + A slice along each dimension of `ar_shape`, such that the intersection + of all the slices give the coordinates of regularly spaced points. + """ + ar_shape = np.asanyarray(ar_shape) + ndim = len(ar_shape) + unsort_dim_idxs = np.argsort(np.argsort(ar_shape)) + sorted_dims = np.sort(ar_shape) + space_size = float(np.prod(ar_shape)) + if space_size <= n_points: + return [slice(None)] * ndim + stepsizes = (space_size / n_points) ** (1.0 / ndim) * np.ones(ndim) + if (sorted_dims < stepsizes).any(): + for dim in range(ndim): + stepsizes[dim] = sorted_dims[dim] + space_size = float(np.prod(sorted_dims[dim+1:])) + stepsizes[dim+1:] = ((space_size / n_points) ** + (1.0 / (ndim - dim - 1))) + if (sorted_dims >= stepsizes).all(): + break + starts = np.floor(stepsizes/2) + stepsizes = np.round(stepsizes) + slices = [slice(start, None, step) for + start, step in zip(starts, stepsizes)] + slices = [slices[i] for i in unsort_dim_idxs] + return slices From 620210025f5dd2d363eb6815776220ee56626d3e Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Thu, 14 Mar 2013 18:33:01 +1100 Subject: [PATCH 106/736] Modify SLIC to allow uneven step sizes --- skimage/segmentation/_slic.pyx | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/skimage/segmentation/_slic.pyx b/skimage/segmentation/_slic.pyx index 35a55804..3ffbb5f6 100644 --- a/skimage/segmentation/_slic.pyx +++ b/skimage/segmentation/_slic.pyx @@ -9,7 +9,7 @@ from scipy import ndimage cimport numpy as cnp -from ..util import img_as_float +from ..util import img_as_float, regular_grid from ..color import rgb2lab, gray2rgb @@ -94,13 +94,13 @@ def slic(image, n_segments=100, ratio=10., max_iter=10, sigma=1, cdef Py_ssize_t depth, height, width depth, height, width = image.shape[:3] # approximate grid size for desired n_segments - cdef Py_ssize_t step = int(np.ceil( - (depth * height * width / n_segments) ** - (1.0/3))) + cdef Py_ssize_t step_z, step_y, step_x grid_z, grid_y, grid_x = np.mgrid[:depth, :height, :width] - means_z = grid_z[::step, ::step, ::step] - means_y = grid_y[::step, ::step, ::step] - means_x = grid_x[::step, ::step, ::step] + slices = regular_grid(image.shape, n_segments) + step_z, step_y, step_x = [int(s.step) for s in slices] + means_z = grid_z[slices] + means_y = grid_y[slices] + means_x = grid_x[slices] means_color = np.zeros(means_z.shape + (3,)) cdef cnp.ndarray[dtype=cnp.float_t, ndim=2] means = \ @@ -115,7 +115,7 @@ def slic(image, n_segments=100, ratio=10., max_iter=10, sigma=1, n_means = means.shape[0] # we do the scaling of ratio in the same way as in the SLIC paper # so the values have the same meaning - ratio = (ratio / float(step)) ** 2 + ratio = (ratio / float(max((step_z, step_y, step_x)))) ** 2 cdef cnp.ndarray[dtype=cnp.float_t, ndim=4] image_zyx \ = np.concatenate([ grid_y[..., np.newaxis], @@ -143,12 +143,12 @@ def slic(image, n_segments=100, ratio=10., max_iter=10, sigma=1, # assign pixels to means for k in range(n_means): # compute windows: - z_min = int(max(current_mean[0] - 2 * step, 0)) - z_max = int(min(current_mean[0] + 2 * step, depth)) - y_min = int(max(current_mean[1] - 2 * step, 0)) - y_max = int(min(current_mean[1] + 2 * step, height)) - x_min = int(max(current_mean[2] - 2 * step, 0)) - x_max = int(min(current_mean[2] + 2 * step, width)) + z_min = int(max(current_mean[0] - 2 * step_z, 0)) + z_max = int(min(current_mean[0] + 2 * step_z, depth)) + y_min = int(max(current_mean[1] - 2 * step_y, 0)) + y_max = int(min(current_mean[1] + 2 * step_y, height)) + x_min = int(max(current_mean[2] - 2 * step_x, 0)) + x_max = int(min(current_mean[2] + 2 * step_x, width)) for z in range(z_min, z_max): for y in range(y_min, y_max): current_pixel = \ From 30636428db4e0749b612645cb0e46bfc6a9a3373 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Thu, 14 Mar 2013 18:52:30 +1100 Subject: [PATCH 107/736] Add more descriptive error message to _prepare_colorarray --- skimage/color/colorconv.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/skimage/color/colorconv.py b/skimage/color/colorconv.py index be2f1519..540c5a75 100644 --- a/skimage/color/colorconv.py +++ b/skimage/color/colorconv.py @@ -130,7 +130,8 @@ def _prepare_colorarray(arr): arr = np.asanyarray(arr) if arr.ndim not in [3, 4] or arr.shape[-1] != 3: - msg = "the input array must be have a shape == (.., ..,[ ..,] 3))" + msg = ("the input array must be have a shape == (.., ..,[ ..,] 3)), " + + "got (" + (", ".join(map(str, arr.shape))) + ")") raise ValueError(msg) return dtype.img_as_float(arr) From 6dc8e6300bb8e0c7aa11b67575605e5e1690912b Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Wed, 3 Apr 2013 17:39:56 +1100 Subject: [PATCH 108/736] Separate inner loop of SLIC computation --- skimage/segmentation/__init__.py | 2 +- skimage/segmentation/_slic.pyx | 115 +++--------------------- skimage/segmentation/slic.py | 114 +++++++++++++++++++++++ skimage/segmentation/tests/test_slic.py | 4 +- 4 files changed, 127 insertions(+), 108 deletions(-) create mode 100644 skimage/segmentation/slic.py diff --git a/skimage/segmentation/__init__.py b/skimage/segmentation/__init__.py index c3aa1afc..ae1bf074 100644 --- a/skimage/segmentation/__init__.py +++ b/skimage/segmentation/__init__.py @@ -1,6 +1,6 @@ from .random_walker_segmentation import random_walker from ._felzenszwalb import felzenszwalb -from ._slic import slic +from .slic import slic from ._quickshift import quickshift from .boundaries import find_boundaries, visualize_boundaries, mark_boundaries from ._clear_border import clear_border diff --git a/skimage/segmentation/_slic.pyx b/skimage/segmentation/_slic.pyx index 3ffbb5f6..20e0001a 100644 --- a/skimage/segmentation/_slic.pyx +++ b/skimage/segmentation/_slic.pyx @@ -13,124 +13,29 @@ from ..util import img_as_float, regular_grid from ..color import rgb2lab, gray2rgb -def slic(image, n_segments=100, ratio=10., max_iter=10, sigma=1, - multichannel=True, convert2lab=True): - """Segments image using k-means clustering in Color-(x,y) space. - - Parameters - ---------- - image : (width, height [, depth] [, 3]) ndarray - Input image, which can be 2D or 3D, and grayscale or multi-channel - (see `multichannel` parameter). - n_segments : int, optional (default: 100) - The (approximate) number of labels in the segmented output image. - ratio: float, optional (default: 10) - Balances color-space proximity and image-space proximity. - Higher values give more weight to color-space. - max_iter : int, optional (default: 10) - Maximum number of iterations of k-means. - sigma : float, optional (default: 1) - Width of Gaussian smoothing kernel for preprocessing. Zero means no - smoothing. - multichannel : bool, optional (default: True) - Whether the last axis of the image is to be interpreted as multiple - channels. Only 3 channels are supported. - convert2lab : bool, optional (default: True) - Whether the input should be converted to Lab colorspace prior to - segmentation. For this purpose, the input is assumed to be RGB. Highly - recommended. - - Returns - ------- - segment_mask : (width, height) ndarray - Integer mask indicating segment labels. - - Raises - ------ - ValueError - If: - - the image dimension is not 2 or 3 and `multichannel == False`, OR - - the image dimension is not 3 or 4 and `multichannel == True`, OR - - `multichannel == True` and the length of the last dimension of - the image is not 3. - - Notes - ----- - The image is optionally smoothed using a Gaussian kernel prior to - segmentation. - - References - ---------- - .. [1] Radhakrishna Achanta, Appu Shaji, Kevin Smith, Aurelien Lucchi, - Pascal Fua, and Sabine Süsstrunk, SLIC Superpixels Compared to - State-of-the-art Superpixel Methods, TPAMI, May 2012. - - Examples - -------- - >>> from skimage.segmentation import slic - >>> from skimage.data import lena - >>> img = lena() - >>> segments = slic(img, n_segments=100, ratio=10) - >>> # Increasing the ratio parameter yields more square regions - >>> segments = slic(img, n_segments=100, ratio=20) - """ - if ((not multichannel and image.ndim not in [2, 3]) or - (multichannel and image.ndim not in [3, 4]) or - (multichannel and image.shape[-1] != 3)): - ValueError("Only 1- or 3-channel 2- or 3-D images are supported.") - if image.ndim in [2, 3] and not multichannel: - image = gray2rgb(image) - if image.ndim == 3: - # See 2D RGB image as 3D RGB image with Z = 1 - image = image[np.newaxis, ...] - if not isinstance(sigma, coll.Iterable): - sigma = np.array([sigma, sigma, sigma, 0]) - if (sigma > 0).any(): - image = ndimage.gaussian_filter(img_as_float(image), sigma) - if convert2lab: - image = rgb2lab(image) +def _slic_cython(cnp.ndarray[dtype=cnp.float_t, ndim=4] image_zyx, + cnp.ndarray[dtype=cnp.intp_t, ndim=3] nearest_mean, + cnp.ndarray[dtype=cnp.float_t, ndim=3] distance, + cnp.ndarray[dtype=cnp.float_t, ndim=2] means, + float ratio, int max_iter, int n_segments): + """Helper function for SLIC segmentation.""" # initialize on grid: cdef Py_ssize_t depth, height, width - depth, height, width = image.shape[:3] + depth, height, width = image_zyx.shape[0], image_zyx[1], image_zyx[2] # approximate grid size for desired n_segments cdef Py_ssize_t step_z, step_y, step_x grid_z, grid_y, grid_x = np.mgrid[:depth, :height, :width] - slices = regular_grid(image.shape, n_segments) + slices = regular_grid((depth, height, width), n_segments) step_z, step_y, step_x = [int(s.step) for s in slices] - means_z = grid_z[slices] - means_y = grid_y[slices] - means_x = grid_x[slices] - means_color = np.zeros(means_z.shape + (3,)) - cdef cnp.ndarray[dtype=cnp.float_t, ndim=2] means = \ - np.concatenate([ - means_z[..., np.newaxis], - means_y[..., np.newaxis], - means_x[..., np.newaxis], - means_color - ], axis=-1).reshape(-1, 6) cdef cnp.float_t* current_mean cdef cnp.float_t* mean_entry n_means = means.shape[0] - # we do the scaling of ratio in the same way as in the SLIC paper - # so the values have the same meaning - ratio = (ratio / float(max((step_z, step_y, step_x)))) ** 2 - cdef cnp.ndarray[dtype=cnp.float_t, ndim=4] image_zyx \ - = np.concatenate([ - grid_y[..., np.newaxis], - grid_x[..., np.newaxis], - grid_z[..., np.newaxis], - image / ratio - ], axis=-1).copy("C") cdef Py_ssize_t i, k, x, y, z, x_min, x_max, y_min, y_max, z_min, z_max, \ changes cdef double dist_mean - cdef cnp.ndarray[dtype=cnp.intp_t, ndim=3] nearest_mean \ - = np.zeros((depth, height, width), dtype=np.intp) - cdef cnp.ndarray[dtype=cnp.float_t, ndim=3] distance \ - = np.empty((depth, height, width)) cdef cnp.float_t* image_p = image_zyx.data cdef cnp.float_t* distance_p = distance.data cdef cnp.float_t* current_distance @@ -152,13 +57,13 @@ def slic(image, n_segments=100, ratio=10., max_iter=10, sigma=1, for z in range(z_min, z_max): for y in range(y_min, y_max): current_pixel = \ - &image_p[5 * ((z * height + y) * width + x_min)] + &image_p[6 * ((z * height + y) * width + x_min)] current_distance = \ &distance_p[(z * height + y) * width + x_min] for x in range(x_min, x_max): mean_entry = current_mean dist_mean = 0 - for c in range(5): + for c in range(6): # you would think the compiler can optimize the # squaring itself. mine can't (with O2) tmp = current_pixel[0] - mean_entry[0] diff --git a/skimage/segmentation/slic.py b/skimage/segmentation/slic.py new file mode 100644 index 00000000..b5d28f11 --- /dev/null +++ b/skimage/segmentation/slic.py @@ -0,0 +1,114 @@ +import collections as coll +import numpy as np +from scipy import ndimage + +from ..util import img_as_float, regular_grid +from ..color import rgb2lab, gray2rgb +from ._slic import _slic_cython + + +def slic(image, n_segments=100, ratio=10., max_iter=10, sigma=1, + multichannel=True, convert2lab=True): + """Segments image using k-means clustering in Color-(x,y) space. + + Parameters + ---------- + image : (width, height [, depth] [, 3]) ndarray + Input image, which can be 2D or 3D, and grayscale or multi-channel + (see `multichannel` parameter). + n_segments : int, optional (default: 100) + The (approximate) number of labels in the segmented output image. + ratio: float, optional (default: 10) + Balances color-space proximity and image-space proximity. + Higher values give more weight to color-space. + max_iter : int, optional (default: 10) + Maximum number of iterations of k-means. + sigma : float, optional (default: 1) + Width of Gaussian smoothing kernel for preprocessing. Zero means no + smoothing. + multichannel : bool, optional (default: True) + Whether the last axis of the image is to be interpreted as multiple + channels. Only 3 channels are supported. + convert2lab : bool, optional (default: True) + Whether the input should be converted to Lab colorspace prior to + segmentation. For this purpose, the input is assumed to be RGB. Highly + recommended. + + Returns + ------- + segment_mask : (width, height) ndarray + Integer mask indicating segment labels. + + Raises + ------ + ValueError + If: + - the image dimension is not 2 or 3 and `multichannel == False`, OR + - the image dimension is not 3 or 4 and `multichannel == True`, OR + - `multichannel == True` and the length of the last dimension of + the image is not 3. + + Notes + ----- + The image is optionally smoothed using a Gaussian kernel prior to + segmentation. + + References + ---------- + .. [1] Radhakrishna Achanta, Appu Shaji, Kevin Smith, Aurelien Lucchi, + Pascal Fua, and Sabine Süsstrunk, SLIC Superpixels Compared to + State-of-the-art Superpixel Methods, TPAMI, May 2012. + + Examples + -------- + >>> from skimage.segmentation import slic + >>> from skimage.data import lena + >>> img = lena() + >>> segments = slic(img, n_segments=100, ratio=10) + >>> # Increasing the ratio parameter yields more square regions + >>> segments = slic(img, n_segments=100, ratio=20) + """ + if ((not multichannel and image.ndim not in [2, 3]) or + (multichannel and image.ndim not in [3, 4]) or + (multichannel and image.shape[-1] != 3)): + ValueError("Only 1- or 3-channel 2- or 3-D images are supported.") + if image.ndim in [2, 3] and not multichannel: + image = gray2rgb(image) + if image.ndim == 3: + # See 2D RGB image as 3D RGB image with Z = 1 + image = image[np.newaxis, ...] + if not isinstance(sigma, coll.Iterable): + sigma = np.array([sigma, sigma, sigma, 0]) + if (sigma > 0).any(): + image = ndimage.gaussian_filter(img_as_float(image), sigma) + if convert2lab: + image = rgb2lab(image) + + # initialize on grid: + depth, height, width = image.shape[:3] + # approximate grid size for desired n_segments + grid_z, grid_y, grid_x = np.mgrid[:depth, :height, :width] + slices = regular_grid(image.shape[:3], n_segments) + step_z, step_y, step_x = [int(s.step) for s in slices] + means_z = grid_z[slices] + means_y = grid_y[slices] + means_x = grid_x[slices] + + means_color = np.zeros(means_z.shape + (3,)) + means = np.concatenate([means_z[..., np.newaxis], means_y[..., np.newaxis], + means_x[..., np.newaxis], means_color + ], axis=-1).reshape(-1, 6) + # we do the scaling of ratio in the same way as in the SLIC paper + # so the values have the same meaning + ratio = (ratio / float(max((step_z, step_y, step_x)))) ** 2 + image_zyx = np.concatenate([grid_y[..., np.newaxis], + grid_x[..., np.newaxis], + grid_z[..., np.newaxis], + image / ratio], axis=-1).copy("C") + nearest_mean = np.zeros((depth, height, width), dtype=np.intp) + distance = np.empty((depth, height, width), dtype=np.float) + segment_map = _slic_cython(image_zyx, nearest_mean, distance, means, + ratio, max_iter, n_segments) + if segment_map.shape[0] == 1: + segment_map = segment_map[0] + return segment_map diff --git a/skimage/segmentation/tests/test_slic.py b/skimage/segmentation/tests/test_slic.py index b080e378..58fda099 100644 --- a/skimage/segmentation/tests/test_slic.py +++ b/skimage/segmentation/tests/test_slic.py @@ -12,7 +12,7 @@ def test_color(): img += 0.01 * rnd.normal(size=img.shape) img[img > 1] = 1 img[img < 0] = 0 - seg = slic(img, sigma=0, n_segments=4) + seg = slic(img, sigma=0, n_segments=4)[0] # we expect 4 segments assert_equal(len(np.unique(seg)), 4) @@ -30,7 +30,7 @@ def test_gray(): img += 0.0033 * rnd.normal(size=img.shape) img[img > 1] = 1 img[img < 0] = 0 - seg = slic(img, sigma=0, n_segments=4, ratio=50.0, multichannel=False) + seg = slic(img, sigma=0, n_segments=4, ratio=50.0, multichannel=False)[0] assert_equal(len(np.unique(seg)), 4) assert_array_equal(seg[:10, :10], 0) From 8e4ab32ed9c5ba48802aadb653ab4055c1826345 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Mon, 13 May 2013 17:07:42 +1000 Subject: [PATCH 109/736] Bug fixes: concatenation order and shape assignments --- skimage/segmentation/_slic.pyx | 3 ++- skimage/segmentation/slic.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/skimage/segmentation/_slic.pyx b/skimage/segmentation/_slic.pyx index 20e0001a..d668f084 100644 --- a/skimage/segmentation/_slic.pyx +++ b/skimage/segmentation/_slic.pyx @@ -22,7 +22,8 @@ def _slic_cython(cnp.ndarray[dtype=cnp.float_t, ndim=4] image_zyx, # initialize on grid: cdef Py_ssize_t depth, height, width - depth, height, width = image_zyx.shape[0], image_zyx[1], image_zyx[2] + shape = image_zyx.shape + depth, height, width = shape[0], shape[1], shape[2] # approximate grid size for desired n_segments cdef Py_ssize_t step_z, step_y, step_x grid_z, grid_y, grid_x = np.mgrid[:depth, :height, :width] diff --git a/skimage/segmentation/slic.py b/skimage/segmentation/slic.py index b5d28f11..9cf95f11 100644 --- a/skimage/segmentation/slic.py +++ b/skimage/segmentation/slic.py @@ -101,9 +101,9 @@ def slic(image, n_segments=100, ratio=10., max_iter=10, sigma=1, # we do the scaling of ratio in the same way as in the SLIC paper # so the values have the same meaning ratio = (ratio / float(max((step_z, step_y, step_x)))) ** 2 - image_zyx = np.concatenate([grid_y[..., np.newaxis], + image_zyx = np.concatenate([grid_z[..., np.newaxis], + grid_y[..., np.newaxis], grid_x[..., np.newaxis], - grid_z[..., np.newaxis], image / ratio], axis=-1).copy("C") nearest_mean = np.zeros((depth, height, width), dtype=np.intp) distance = np.empty((depth, height, width), dtype=np.float) From e9aa78b937b1d6dd18c895b346b2e6eb4a23cee0 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Tue, 14 May 2013 03:06:01 +1000 Subject: [PATCH 110/736] Bug fix: don't add singleton dimension to 3D gray images --- skimage/segmentation/slic.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/skimage/segmentation/slic.py b/skimage/segmentation/slic.py index 9cf95f11..d039d5ed 100644 --- a/skimage/segmentation/slic.py +++ b/skimage/segmentation/slic.py @@ -3,7 +3,7 @@ import numpy as np from scipy import ndimage from ..util import img_as_float, regular_grid -from ..color import rgb2lab, gray2rgb +from ..color import rgb2lab, gray2rgb, is_rgb from ._slic import _slic_cython @@ -72,9 +72,9 @@ def slic(image, n_segments=100, ratio=10., max_iter=10, sigma=1, (multichannel and image.ndim not in [3, 4]) or (multichannel and image.shape[-1] != 3)): ValueError("Only 1- or 3-channel 2- or 3-D images are supported.") - if image.ndim in [2, 3] and not multichannel: + if not multichannel: image = gray2rgb(image) - if image.ndim == 3: + if image.ndim == 3 and is_rgb(image): # See 2D RGB image as 3D RGB image with Z = 1 image = image[np.newaxis, ...] if not isinstance(sigma, coll.Iterable): From 57cc86d7c81565d8f51037ebc178e90bc8981052 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Tue, 14 May 2013 03:07:09 +1000 Subject: [PATCH 111/736] Bug fix: remove unnecessary __get__ in test_slic --- skimage/segmentation/tests/test_slic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skimage/segmentation/tests/test_slic.py b/skimage/segmentation/tests/test_slic.py index 58fda099..1f5f0c42 100644 --- a/skimage/segmentation/tests/test_slic.py +++ b/skimage/segmentation/tests/test_slic.py @@ -12,7 +12,7 @@ def test_color(): img += 0.01 * rnd.normal(size=img.shape) img[img > 1] = 1 img[img < 0] = 0 - seg = slic(img, sigma=0, n_segments=4)[0] + seg = slic(img, sigma=0, n_segments=4) # we expect 4 segments assert_equal(len(np.unique(seg)), 4) @@ -30,7 +30,7 @@ def test_gray(): img += 0.0033 * rnd.normal(size=img.shape) img[img > 1] = 1 img[img < 0] = 0 - seg = slic(img, sigma=0, n_segments=4, ratio=50.0, multichannel=False)[0] + seg = slic(img, sigma=0, n_segments=4, ratio=20.0, multichannel=False) assert_equal(len(np.unique(seg)), 4) assert_array_equal(seg[:10, :10], 0) From 215439b43c27271c95fc208bf683a19619c81b8d Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Tue, 14 May 2013 03:07:28 +1000 Subject: [PATCH 112/736] Add 3D slic tests (gray not working yet) --- skimage/segmentation/tests/test_slic.py | 49 ++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/skimage/segmentation/tests/test_slic.py b/skimage/segmentation/tests/test_slic.py index 1f5f0c42..9a277970 100644 --- a/skimage/segmentation/tests/test_slic.py +++ b/skimage/segmentation/tests/test_slic.py @@ -1,9 +1,10 @@ +import itertools as it import numpy as np from numpy.testing import assert_equal, assert_array_equal from skimage.segmentation import slic -def test_color(): +def test_color_2d(): rnd = np.random.RandomState(0) img = np.zeros((20, 21, 3)) img[:10, :10, 0] = 1 @@ -21,7 +22,8 @@ def test_color(): assert_array_equal(seg[:10, 10:], 1) assert_array_equal(seg[10:, 10:], 3) -def test_gray(): + +def test_gray_2d(): rnd = np.random.RandomState(0) img = np.zeros((20, 21)) img[:10, :10] = 0.33 @@ -38,6 +40,49 @@ def test_gray(): assert_array_equal(seg[:10, 10:], 1) assert_array_equal(seg[10:, 10:], 3) + +def test_color_3d(): + rnd = np.random.RandomState(0) + img = np.zeros((20, 21, 22, 3)) + slices = [] + for dim_size in img.shape[:-1]: + midpoint = dim_size // 2 + slices.append((slice(None, midpoint), slice(midpoint, None))) + slices = list(it.product(*slices)) + colors = list(it.product(*(([0, 1],) * 3))) + for s, c in zip(slices, colors): + img[s] = c + img += 0.01 * rnd.normal(size=img.shape) + img[img > 1] = 1 + img[img < 0] = 0 + seg = slic(img, sigma=0, n_segments=8) + + assert_equal(len(np.unique(seg)), 8) + for s, c in zip(slices, range(8)): + assert_array_equal(seg[s], c) + + +def test_gray_3d(): + rnd = np.random.RandomState(0) + img = np.zeros((20, 21, 22)) + slices = [] + for dim_size in img.shape[:-1]: + midpoint = dim_size // 2 + slices.append((slice(None, midpoint), slice(midpoint, None))) + slices = list(it.product(*slices)) + shades = np.arange(0, 1.000001, 1.0/7) + for s, sh in zip(slices, shades): + img[s] = sh + img += 0.001 * rnd.normal(size=img.shape) + img[img > 1] = 1 + img[img < 0] = 0 + seg = slic(img, sigma=0, n_segments=8, ratio=40.0, multichannel=False) + + assert_equal(len(np.unique(seg)), 8) + for s, c in zip(slices, range(8)): + assert_array_equal(seg[s], c) + + if __name__ == '__main__': from numpy import testing testing.run_module_suite() From 5c4d0218cea97cfbe761906e7fda38b2b7381330 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Tue, 14 May 2013 03:24:27 +1000 Subject: [PATCH 113/736] bug fix: 3d test working --- skimage/segmentation/tests/test_slic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skimage/segmentation/tests/test_slic.py b/skimage/segmentation/tests/test_slic.py index 9a277970..1018969a 100644 --- a/skimage/segmentation/tests/test_slic.py +++ b/skimage/segmentation/tests/test_slic.py @@ -66,7 +66,7 @@ def test_gray_3d(): rnd = np.random.RandomState(0) img = np.zeros((20, 21, 22)) slices = [] - for dim_size in img.shape[:-1]: + for dim_size in img.shape: midpoint = dim_size // 2 slices.append((slice(None, midpoint), slice(midpoint, None))) slices = list(it.product(*slices)) @@ -76,7 +76,7 @@ def test_gray_3d(): img += 0.001 * rnd.normal(size=img.shape) img[img > 1] = 1 img[img < 0] = 0 - seg = slic(img, sigma=0, n_segments=8, ratio=40.0, multichannel=False) + seg = slic(img, sigma=0, n_segments=8, ratio=20.0, multichannel=False) assert_equal(len(np.unique(seg)), 8) for s, c in zip(slices, range(8)): From 332ab0449be9bff38c2edece247a1f09ef5a28de Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Tue, 14 May 2013 13:15:29 +1000 Subject: [PATCH 114/736] Improve PEP8 and Python 3 compliance Thanks to @JDWarner and @ahojnnes for the input. --- skimage/util/regular_grid.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/skimage/util/regular_grid.py b/skimage/util/regular_grid.py index 40980491..ab1b12dc 100644 --- a/skimage/util/regular_grid.py +++ b/skimage/util/regular_grid.py @@ -1,5 +1,6 @@ import numpy as np + def regular_grid(ar_shape, n_points): """Find `n_points` regularly spaced along `ar_shape`. @@ -36,7 +37,7 @@ def regular_grid(ar_shape, n_points): (1.0 / (ndim - dim - 1))) if (sorted_dims >= stepsizes).all(): break - starts = np.floor(stepsizes/2) + starts = stepsizes // 2 stepsizes = np.round(stepsizes) slices = [slice(start, None, step) for start, step in zip(starts, stepsizes)] From 441f4029f67db8733789f1d2e7f12e3c306b5055 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Tue, 14 May 2013 17:57:15 +1000 Subject: [PATCH 115/736] PEP8 --- skimage/util/regular_grid.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skimage/util/regular_grid.py b/skimage/util/regular_grid.py index ab1b12dc..d1cb5428 100644 --- a/skimage/util/regular_grid.py +++ b/skimage/util/regular_grid.py @@ -34,12 +34,12 @@ def regular_grid(ar_shape, n_points): stepsizes[dim] = sorted_dims[dim] space_size = float(np.prod(sorted_dims[dim+1:])) stepsizes[dim+1:] = ((space_size / n_points) ** - (1.0 / (ndim - dim - 1))) + (1.0 / (ndim - dim - 1))) if (sorted_dims >= stepsizes).all(): break starts = stepsizes // 2 stepsizes = np.round(stepsizes) slices = [slice(start, None, step) for - start, step in zip(starts, stepsizes)] + start, step in zip(starts, stepsizes)] slices = [slices[i] for i in unsort_dim_idxs] return slices From 99f905bc547319d58a34c97911c7e27cadf9ac29 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Wed, 15 May 2013 17:18:35 +1000 Subject: [PATCH 116/736] Always convert image to float in [0, 1] --- skimage/segmentation/slic.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/skimage/segmentation/slic.py b/skimage/segmentation/slic.py index d039d5ed..49f51773 100644 --- a/skimage/segmentation/slic.py +++ b/skimage/segmentation/slic.py @@ -50,8 +50,10 @@ def slic(image, n_segments=100, ratio=10., max_iter=10, sigma=1, Notes ----- - The image is optionally smoothed using a Gaussian kernel prior to - segmentation. + If `sigma > 0` as is default, the image is smoothed using a Gaussian kernel + prior to segmentation. + + The image is rescaled to be in [0, 1] prior to processing. References ---------- @@ -72,6 +74,7 @@ def slic(image, n_segments=100, ratio=10., max_iter=10, sigma=1, (multichannel and image.ndim not in [3, 4]) or (multichannel and image.shape[-1] != 3)): ValueError("Only 1- or 3-channel 2- or 3-D images are supported.") + image = img_as_float(image) if not multichannel: image = gray2rgb(image) if image.ndim == 3 and is_rgb(image): @@ -80,7 +83,7 @@ def slic(image, n_segments=100, ratio=10., max_iter=10, sigma=1, if not isinstance(sigma, coll.Iterable): sigma = np.array([sigma, sigma, sigma, 0]) if (sigma > 0).any(): - image = ndimage.gaussian_filter(img_as_float(image), sigma) + image = ndimage.gaussian_filter(image, sigma) if convert2lab: image = rgb2lab(image) From e757b9ae06fff23615971c10e03ed52f2e87c83d Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Mon, 20 May 2013 14:46:11 +1000 Subject: [PATCH 117/736] Rename slic.py to avoid name conflicts --- skimage/segmentation/__init__.py | 2 +- skimage/segmentation/{slic.py => slic_superpixels.py} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename skimage/segmentation/{slic.py => slic_superpixels.py} (100%) diff --git a/skimage/segmentation/__init__.py b/skimage/segmentation/__init__.py index ae1bf074..107111bd 100644 --- a/skimage/segmentation/__init__.py +++ b/skimage/segmentation/__init__.py @@ -1,6 +1,6 @@ from .random_walker_segmentation import random_walker from ._felzenszwalb import felzenszwalb -from .slic import slic +from .slic_superpixels import slic from ._quickshift import quickshift from .boundaries import find_boundaries, visualize_boundaries, mark_boundaries from ._clear_border import clear_border diff --git a/skimage/segmentation/slic.py b/skimage/segmentation/slic_superpixels.py similarity index 100% rename from skimage/segmentation/slic.py rename to skimage/segmentation/slic_superpixels.py From e9c5f666aed6566c7eba2ff04291590161fe6995 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Mon, 20 May 2013 16:30:55 +1000 Subject: [PATCH 118/736] Use educated guesses for presence of color channels in SLIC --- skimage/segmentation/slic_superpixels.py | 28 +++++++++++++++++++----- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/skimage/segmentation/slic_superpixels.py b/skimage/segmentation/slic_superpixels.py index 49f51773..43639f16 100644 --- a/skimage/segmentation/slic_superpixels.py +++ b/skimage/segmentation/slic_superpixels.py @@ -1,14 +1,15 @@ import collections as coll import numpy as np from scipy import ndimage +import warnings from ..util import img_as_float, regular_grid -from ..color import rgb2lab, gray2rgb, is_rgb +from ..color import rgb2lab, gray2rgb, guess_spatial_dimensions from ._slic import _slic_cython def slic(image, n_segments=100, ratio=10., max_iter=10, sigma=1, - multichannel=True, convert2lab=True): + multichannel=None, convert2lab=True): """Segments image using k-means clustering in Color-(x,y) space. Parameters @@ -26,9 +27,11 @@ def slic(image, n_segments=100, ratio=10., max_iter=10, sigma=1, sigma : float, optional (default: 1) Width of Gaussian smoothing kernel for preprocessing. Zero means no smoothing. - multichannel : bool, optional (default: True) + multichannel : bool, optional (default: None) Whether the last axis of the image is to be interpreted as multiple - channels. Only 3 channels are supported. + channels. Only 3 channels are supported. If `None`, the function will + attempt to guess this, and raise a warning if ambiguous, when the + array has shape (M, N, 3). convert2lab : bool, optional (default: True) Whether the input should be converted to Lab colorspace prior to segmentation. For this purpose, the input is assumed to be RGB. Highly @@ -46,7 +49,7 @@ def slic(image, n_segments=100, ratio=10., max_iter=10, sigma=1, - the image dimension is not 2 or 3 and `multichannel == False`, OR - the image dimension is not 3 or 4 and `multichannel == True`, OR - `multichannel == True` and the length of the last dimension of - the image is not 3. + the image is not 3, OR Notes ----- @@ -55,6 +58,10 @@ def slic(image, n_segments=100, ratio=10., max_iter=10, sigma=1, The image is rescaled to be in [0, 1] prior to processing. + Images of shape (M, N, 3) are interpreted as 2D RGB images by default. To + interpret them as 3D with the last dimension having length 3, use + `multichannel=False`. + References ---------- .. [1] Radhakrishna Achanta, Appu Shaji, Kevin Smith, Aurelien Lucchi, @@ -70,6 +77,15 @@ def slic(image, n_segments=100, ratio=10., max_iter=10, sigma=1, >>> # Increasing the ratio parameter yields more square regions >>> segments = slic(img, n_segments=100, ratio=20) """ + spatial_dims = guess_spatial_dimensions(image) + if spatial_dims is None and multichannel is None: + msg = ("Images with dimensions (M, N, 3) are interpreted as 2D+RGB" + + " by default. Use `multichannel=False` to interpret as " + + " 3D image with last dimension of length 3.") + warnings.warn(RuntimeWarning(msg)) + multichannel = True + elif multichannel is None: + multichannel = (spatial_dims == image.ndim + 1) if ((not multichannel and image.ndim not in [2, 3]) or (multichannel and image.ndim not in [3, 4]) or (multichannel and image.shape[-1] != 3)): @@ -77,7 +93,7 @@ def slic(image, n_segments=100, ratio=10., max_iter=10, sigma=1, image = img_as_float(image) if not multichannel: image = gray2rgb(image) - if image.ndim == 3 and is_rgb(image): + elif image.ndim == 3: # See 2D RGB image as 3D RGB image with Z = 1 image = image[np.newaxis, ...] if not isinstance(sigma, coll.Iterable): From 1c116a9905beddc188c0bef2a25fc0bdbe42872b Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Mon, 20 May 2013 16:42:06 +1000 Subject: [PATCH 119/736] Add function to guesstimate number of spatial dimensions --- skimage/color/__init__.py | 2 ++ skimage/color/colorconv.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/skimage/color/__init__.py b/skimage/color/__init__.py index 9202bee9..19620cb1 100644 --- a/skimage/color/__init__.py +++ b/skimage/color/__init__.py @@ -1,4 +1,5 @@ from .colorconv import (convert_colorspace, + guess_spatial_dimensions, rgb2hsv, hsv2rgb, rgb2xyz, @@ -45,6 +46,7 @@ from .colorlabel import color_dict, label2rgb __all__ = ['convert_colorspace', + 'guess_spatial_dimensions', 'rgb2hsv', 'hsv2rgb', 'rgb2xyz', diff --git a/skimage/color/colorconv.py b/skimage/color/colorconv.py index 540c5a75..5225f7ea 100644 --- a/skimage/color/colorconv.py +++ b/skimage/color/colorconv.py @@ -49,6 +49,37 @@ from ..util import dtype from skimage._shared.utils import deprecated +def guess_spatial_dimensions(image): + """Make an educated guess about whether an image has a channels dimension. + + Parameters + ---------- + image : ndarray + The input image. + + Returns + ------- + spatial_dims : int or None + The number of spatial dimensions of `image`. If ambiguous, the value + is `None`. + + Raises + ------ + ValueError + If the image array has less than two or more than four dimensions. + """ + if image.ndim == 2: + return 2 + if image.ndim == 3 and image.shape[-1] != 3: + return 3 + if image.ndim == 3 and image.shape[-1] == 3: + return None + if image.ndim == 4 and image.shape[-1] == 3: + return 3 + else: + raise ValueError("Expected 2D, 3D, or 4D array, got %iD." % image.ndim) + + @deprecated() def is_rgb(image): """Test whether the image is RGB or RGBA. From f0d586a32f5b902c66b9c8907a5039fde9538597 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Mon, 20 May 2013 16:43:20 +1000 Subject: [PATCH 120/736] Revert change to is_rgb --- skimage/color/colorconv.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/color/colorconv.py b/skimage/color/colorconv.py index 5225f7ea..a73f5f19 100644 --- a/skimage/color/colorconv.py +++ b/skimage/color/colorconv.py @@ -90,7 +90,7 @@ def is_rgb(image): Input image. """ - return (image.ndim in (3, 4) and image.shape[-1] in (3, 4)) + return (image.ndim == 3 and image.shape[2] in (3, 4)) @deprecated() From 5a43e0306100fc8f5109000f6708a265853bf972 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Mon, 20 May 2013 16:55:07 +1000 Subject: [PATCH 121/736] Bug fix: add z dimension regardless of multichannel --- skimage/segmentation/slic_superpixels.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/segmentation/slic_superpixels.py b/skimage/segmentation/slic_superpixels.py index 43639f16..35e00a34 100644 --- a/skimage/segmentation/slic_superpixels.py +++ b/skimage/segmentation/slic_superpixels.py @@ -93,7 +93,7 @@ def slic(image, n_segments=100, ratio=10., max_iter=10, sigma=1, image = img_as_float(image) if not multichannel: image = gray2rgb(image) - elif image.ndim == 3: + if image.ndim == 3: # See 2D RGB image as 3D RGB image with Z = 1 image = image[np.newaxis, ...] if not isinstance(sigma, coll.Iterable): From 55fe6ce2d685e25cfc9e0894c517bccd6b7bf4fe Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Mon, 20 May 2013 17:05:59 +1000 Subject: [PATCH 122/736] Bug fix: spatial vs image dimension comparison reversed --- skimage/segmentation/slic_superpixels.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/segmentation/slic_superpixels.py b/skimage/segmentation/slic_superpixels.py index 35e00a34..1c9b8510 100644 --- a/skimage/segmentation/slic_superpixels.py +++ b/skimage/segmentation/slic_superpixels.py @@ -85,7 +85,7 @@ def slic(image, n_segments=100, ratio=10., max_iter=10, sigma=1, warnings.warn(RuntimeWarning(msg)) multichannel = True elif multichannel is None: - multichannel = (spatial_dims == image.ndim + 1) + multichannel = (spatial_dims + 1 == image.ndim) if ((not multichannel and image.ndim not in [2, 3]) or (multichannel and image.ndim not in [3, 4]) or (multichannel and image.shape[-1] != 3)): From 69fb3fb7ba12055a0378330b26a3186e6638c615 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Thu, 30 May 2013 13:37:59 +1000 Subject: [PATCH 123/736] Make means array contiguous for numpy <= 1.6.1 --- skimage/segmentation/slic_superpixels.py | 1 + 1 file changed, 1 insertion(+) diff --git a/skimage/segmentation/slic_superpixels.py b/skimage/segmentation/slic_superpixels.py index 1c9b8510..a4e949fd 100644 --- a/skimage/segmentation/slic_superpixels.py +++ b/skimage/segmentation/slic_superpixels.py @@ -117,6 +117,7 @@ def slic(image, n_segments=100, ratio=10., max_iter=10, sigma=1, means = np.concatenate([means_z[..., np.newaxis], means_y[..., np.newaxis], means_x[..., np.newaxis], means_color ], axis=-1).reshape(-1, 6) + means = np.ascontiguousarray(means) # we do the scaling of ratio in the same way as in the SLIC paper # so the values have the same meaning ratio = (ratio / float(max((step_z, step_y, step_x)))) ** 2 From 8c2011d4f92858279d995373a0d6d2a6aa26312e Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Tue, 4 Jun 2013 14:25:40 +1000 Subject: [PATCH 124/736] Add UTF8 coding declaration to slic_superpixels.py --- skimage/segmentation/slic_superpixels.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/skimage/segmentation/slic_superpixels.py b/skimage/segmentation/slic_superpixels.py index a4e949fd..2caf8ade 100644 --- a/skimage/segmentation/slic_superpixels.py +++ b/skimage/segmentation/slic_superpixels.py @@ -1,3 +1,5 @@ +# coding=utf-8 + import collections as coll import numpy as np from scipy import ndimage From cba821d5e3128e0abb8f8fadbeb395531d6390a3 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Wed, 12 Jun 2013 23:14:22 +1000 Subject: [PATCH 125/736] Suppress warning of ambiguous array dim in test --- skimage/segmentation/tests/test_slic.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/skimage/segmentation/tests/test_slic.py b/skimage/segmentation/tests/test_slic.py index 1018969a..d0539cd2 100644 --- a/skimage/segmentation/tests/test_slic.py +++ b/skimage/segmentation/tests/test_slic.py @@ -1,4 +1,5 @@ import itertools as it +import warnings import numpy as np from numpy.testing import assert_equal, assert_array_equal from skimage.segmentation import slic @@ -13,7 +14,9 @@ def test_color_2d(): img += 0.01 * rnd.normal(size=img.shape) img[img > 1] = 1 img[img < 0] = 0 - seg = slic(img, sigma=0, n_segments=4) + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + seg = slic(img, sigma=0, n_segments=4) # we expect 4 segments assert_equal(len(np.unique(seg)), 4) From b1b70631bdecbfa7240a22aaa202ad88953baf78 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Mon, 24 Jun 2013 22:44:50 -0400 Subject: [PATCH 126/736] Initial attempt at updating SLIC for memoryviews --- skimage/segmentation/_slic.pyx | 44 +++++++++++----------------------- 1 file changed, 14 insertions(+), 30 deletions(-) diff --git a/skimage/segmentation/_slic.pyx b/skimage/segmentation/_slic.pyx index d668f084..99d8d51b 100644 --- a/skimage/segmentation/_slic.pyx +++ b/skimage/segmentation/_slic.pyx @@ -13,10 +13,10 @@ from ..util import img_as_float, regular_grid from ..color import rgb2lab, gray2rgb -def _slic_cython(cnp.ndarray[dtype=cnp.float_t, ndim=4] image_zyx, - cnp.ndarray[dtype=cnp.intp_t, ndim=3] nearest_mean, - cnp.ndarray[dtype=cnp.float_t, ndim=3] distance, - cnp.ndarray[dtype=cnp.float_t, ndim=2] means, +def _slic_cython(double[:, :, :, ::1] image_zyx, + int[:, :, ::1] nearest_mean, + double[:, :, ::1] distance, + double[:, ::1] means, float ratio, int max_iter, int n_segments): """Helper function for SLIC segmentation.""" @@ -30,54 +30,38 @@ def _slic_cython(cnp.ndarray[dtype=cnp.float_t, ndim=4] image_zyx, slices = regular_grid((depth, height, width), n_segments) step_z, step_y, step_x = [int(s.step) for s in slices] - cdef cnp.float_t* current_mean - cdef cnp.float_t* mean_entry n_means = means.shape[0] cdef Py_ssize_t i, k, x, y, z, x_min, x_max, y_min, y_max, z_min, z_max, \ changes cdef double dist_mean - cdef cnp.float_t* image_p = image_zyx.data - cdef cnp.float_t* distance_p = distance.data - cdef cnp.float_t* current_distance - cdef cnp.float_t* current_pixel cdef double tmp for i in range(max_iter): distance.fill(np.inf) changes = 0 - current_mean = means.data # assign pixels to means for k in range(n_means): # compute windows: - z_min = int(max(current_mean[0] - 2 * step_z, 0)) - z_max = int(min(current_mean[0] + 2 * step_z, depth)) - y_min = int(max(current_mean[1] - 2 * step_y, 0)) - y_max = int(min(current_mean[1] + 2 * step_y, height)) - x_min = int(max(current_mean[2] - 2 * step_x, 0)) - x_max = int(min(current_mean[2] + 2 * step_x, width)) + z_min = int(max(means[k, 0] - 2 * step_z, 0)) + z_max = int(min(means[k, 0] + 2 * step_z, depth)) + y_min = int(max(means[k, 1] - 2 * step_y, 0)) + y_max = int(min(means[k, 1] + 2 * step_y, height)) + x_min = int(max(means[k, 2] - 2 * step_x, 0)) + x_max = int(min(means[k, 2] + 2 * step_x, width)) for z in range(z_min, z_max): for y in range(y_min, y_max): - current_pixel = \ - &image_p[6 * ((z * height + y) * width + x_min)] - current_distance = \ - &distance_p[(z * height + y) * width + x_min] for x in range(x_min, x_max): - mean_entry = current_mean dist_mean = 0 for c in range(6): # you would think the compiler can optimize the # squaring itself. mine can't (with O2) - tmp = current_pixel[0] - mean_entry[0] + tmp = image_zyx[z, y, x, c] - means[k, c] dist_mean += tmp * tmp - current_pixel += 1 - mean_entry += 1 # some precision issue here. Doesnt work if testing ">" - if current_distance[0] - dist_mean > 1e-10: + if distance[z, y, x] - dist_mean > 1e-10: nearest_mean[z, y, x] = k - current_distance[0] = dist_mean - changes += 1 - current_distance += 1 - current_mean += 6 + distance[z, y, x] = dist_mean + changes = 1 if changes == 0: break # recompute means: From 6e64515ea9060cea77a12cde7a0923ad56d4a206 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Mon, 24 Jun 2013 23:41:53 -0400 Subject: [PATCH 127/736] Fix build errors with memoryviews --- skimage/segmentation/_slic.pyx | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/skimage/segmentation/_slic.pyx b/skimage/segmentation/_slic.pyx index 99d8d51b..7f0fa8fb 100644 --- a/skimage/segmentation/_slic.pyx +++ b/skimage/segmentation/_slic.pyx @@ -14,7 +14,7 @@ from ..color import rgb2lab, gray2rgb def _slic_cython(double[:, :, :, ::1] image_zyx, - int[:, :, ::1] nearest_mean, + long[:, :, ::1] nearest_mean, double[:, :, ::1] distance, double[:, ::1] means, float ratio, int max_iter, int n_segments): @@ -22,8 +22,8 @@ def _slic_cython(double[:, :, :, ::1] image_zyx, # initialize on grid: cdef Py_ssize_t depth, height, width - shape = image_zyx.shape - depth, height, width = shape[0], shape[1], shape[2] + depth, height, width = (image_zyx.shape[0], image_zyx.shape[1], + image_zyx.shape[2]) # approximate grid size for desired n_segments cdef Py_ssize_t step_z, step_y, step_x grid_z, grid_y, grid_x = np.mgrid[:depth, :height, :width] @@ -36,8 +36,10 @@ def _slic_cython(double[:, :, :, ::1] image_zyx, cdef double dist_mean cdef double tmp + #cdef long[::1] nearest_mean_ravel + #cdef double[::1] image_zyx_ravel_j for i in range(max_iter): - distance.fill(np.inf) + distance[:, :, :] = np.inf changes = 0 # assign pixels to means for k in range(n_means): @@ -65,9 +67,13 @@ def _slic_cython(double[:, :, :, ::1] image_zyx, if changes == 0: break # recompute means: - means_list = [np.bincount(nearest_mean.ravel(), - image_zyx[:, :, :, j].ravel()) for j in range(6)] - in_mean = np.bincount(nearest_mean.ravel()) + nearest_mean_ravel = np.asarray(nearest_mean).ravel() + means_list = [] + for j in range(6): + image_zyx_ravel = np.asarray(image_zyx[:, :, :, j]).ravel() + means_list.append(np.bincount(nearest_mean_ravel, + image_zyx_ravel)) + in_mean = np.bincount(nearest_mean_ravel) in_mean[in_mean == 0] = 1 means = (np.vstack(means_list) / in_mean).T.copy("C") return nearest_mean From 524255c0a272c69d15d7703a498e9df147a2244b Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Mon, 1 Jul 2013 18:34:23 +0200 Subject: [PATCH 128/736] Improve _slic.pyx doc, bug fixes, debug print --- skimage/segmentation/_slic.pyx | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/skimage/segmentation/_slic.pyx b/skimage/segmentation/_slic.pyx index 7f0fa8fb..6621d4bf 100644 --- a/skimage/segmentation/_slic.pyx +++ b/skimage/segmentation/_slic.pyx @@ -18,7 +18,32 @@ def _slic_cython(double[:, :, :, ::1] image_zyx, double[:, :, ::1] distance, double[:, ::1] means, float ratio, int max_iter, int n_segments): - """Helper function for SLIC segmentation.""" + """Helper function for SLIC segmentation. + + Parameters + ---------- + image_zyx : 4D np.ndarray of double, shape (Z, Y, X, 6) + The image with embedded coordinates, that is, `image_zyx[i, j, k]` is + `array([i, j, k, r, g, b])` or `array([i, j, k, L, a, b])`, depending + on the colorspace. + nearest_mean : 3D np.ndarray of long, shape (Z, Y, X) + The (initially empty) label field. + distance : 3D np.ndarray of double, shape (Z, Y, X) + The (initially infinity) array of distances to the nearest centroid. + means : 2D np.ndarray of double, shape (n_segments, 6) + The centroids obtained by SLIC. + ratio : float + The ratio of xyz-space and colorspace in the clustering. + max_iter : int + The maximum number of k-means iterations. + n_segments : int + The approximate/desired number of segments. + + Returns + ------- + nearest_mean : 3D np.ndarray of long, shape (Z, Y, X) + The label field/superpixels found by SLIC. + """ # initialize on grid: cdef Py_ssize_t depth, height, width @@ -39,7 +64,6 @@ def _slic_cython(double[:, :, :, ::1] image_zyx, #cdef long[::1] nearest_mean_ravel #cdef double[::1] image_zyx_ravel_j for i in range(max_iter): - distance[:, :, :] = np.inf changes = 0 # assign pixels to means for k in range(n_means): @@ -70,10 +94,11 @@ def _slic_cython(double[:, :, :, ::1] image_zyx, nearest_mean_ravel = np.asarray(nearest_mean).ravel() means_list = [] for j in range(6): - image_zyx_ravel = np.asarray(image_zyx[:, :, :, j]).ravel() + image_zyx_ravel = np.ascontiguousarray(image_zyx[:, :, :, j]).ravel() means_list.append(np.bincount(nearest_mean_ravel, image_zyx_ravel)) in_mean = np.bincount(nearest_mean_ravel) in_mean[in_mean == 0] = 1 means = (np.vstack(means_list) / in_mean).T.copy("C") + print np.asarray(nearest_mean) return nearest_mean From a33861896784b1c19d9d89050639f913eba65f49 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Mon, 1 Jul 2013 21:31:06 +0200 Subject: [PATCH 129/736] Remove diagnostic print --- skimage/segmentation/_slic.pyx | 1 - 1 file changed, 1 deletion(-) diff --git a/skimage/segmentation/_slic.pyx b/skimage/segmentation/_slic.pyx index 6621d4bf..c7517dc3 100644 --- a/skimage/segmentation/_slic.pyx +++ b/skimage/segmentation/_slic.pyx @@ -100,5 +100,4 @@ def _slic_cython(double[:, :, :, ::1] image_zyx, in_mean = np.bincount(nearest_mean_ravel) in_mean[in_mean == 0] = 1 means = (np.vstack(means_list) / in_mean).T.copy("C") - print np.asarray(nearest_mean) return nearest_mean From 578876eff89e9e5eb35135e9aead648a8624244b Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Mon, 1 Jul 2013 22:06:03 +0200 Subject: [PATCH 130/736] Bug fix: correctly initialize distance in slic --- skimage/segmentation/_slic.pyx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/skimage/segmentation/_slic.pyx b/skimage/segmentation/_slic.pyx index c7517dc3..337be882 100644 --- a/skimage/segmentation/_slic.pyx +++ b/skimage/segmentation/_slic.pyx @@ -65,6 +65,10 @@ def _slic_cython(double[:, :, :, ::1] image_zyx, #cdef double[::1] image_zyx_ravel_j for i in range(max_iter): changes = 0 + for z in range(depth): + for y in range(height): + for x in range(width): + distance[z, y, x] = np.inf # assign pixels to means for k in range(n_means): # compute windows: @@ -100,4 +104,4 @@ def _slic_cython(double[:, :, :, ::1] image_zyx, in_mean = np.bincount(nearest_mean_ravel) in_mean[in_mean == 0] = 1 means = (np.vstack(means_list) / in_mean).T.copy("C") - return nearest_mean + return np.ascontiguousarray(nearest_mean) From cd197527fb39796520dd22e1f8df97b17a4810b3 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Tue, 2 Jul 2013 13:16:17 +0200 Subject: [PATCH 131/736] Speed up initialising 'distance' in SLIC --- skimage/segmentation/_slic.pyx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/skimage/segmentation/_slic.pyx b/skimage/segmentation/_slic.pyx index 337be882..654c5cff 100644 --- a/skimage/segmentation/_slic.pyx +++ b/skimage/segmentation/_slic.pyx @@ -51,7 +51,6 @@ def _slic_cython(double[:, :, :, ::1] image_zyx, image_zyx.shape[2]) # approximate grid size for desired n_segments cdef Py_ssize_t step_z, step_y, step_x - grid_z, grid_y, grid_x = np.mgrid[:depth, :height, :width] slices = regular_grid((depth, height, width), n_segments) step_z, step_y, step_x = [int(s.step) for s in slices] @@ -65,10 +64,7 @@ def _slic_cython(double[:, :, :, ::1] image_zyx, #cdef double[::1] image_zyx_ravel_j for i in range(max_iter): changes = 0 - for z in range(depth): - for y in range(height): - for x in range(width): - distance[z, y, x] = np.inf + distance[:, :, :] = np.inf # assign pixels to means for k in range(n_means): # compute windows: From 7f1b9ea4c6f9f5ac1c0bff2d4d73aa0dd2387759 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Tue, 2 Jul 2013 19:42:24 +0200 Subject: [PATCH 132/736] Fix automerge bug --- skimage/util/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/util/__init__.py b/skimage/util/__init__.py index 474b2966..b196af34 100644 --- a/skimage/util/__init__.py +++ b/skimage/util/__init__.py @@ -23,5 +23,5 @@ __all__ = ['img_as_float', 'view_as_blocks', 'view_as_windows', 'pad', - 'random_noise'] + 'random_noise', 'regular_grid'] From dab831b72b59498de1cd2e30755f41d009826f50 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Tue, 2 Jul 2013 20:13:09 +0200 Subject: [PATCH 133/736] Bug fix: map returns iterator in Py3k --- skimage/color/colorconv.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/color/colorconv.py b/skimage/color/colorconv.py index a73f5f19..d2b20316 100644 --- a/skimage/color/colorconv.py +++ b/skimage/color/colorconv.py @@ -739,7 +739,7 @@ def xyz2lab(xyz): a = 500.0 * (x - y) b = 200.0 * (y - z) - return np.concatenate(map(lambda x: x[..., np.newaxis], [L, a, b]), -1) + return np.concatenate([x[..., np.newaxis] for x in [L, a, b]], axis=-1) def lab2xyz(lab): From ac7880b640fc6601680551a344f9145b1a047bf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Tue, 2 Jul 2013 21:59:49 +0200 Subject: [PATCH 134/736] Outsource inner brief loop into cython file --- skimage/feature/__init__.py | 4 +- skimage/feature/_brief.py | 78 ++++++++++++++++++++++++------------- skimage/feature/setup.py | 3 ++ 3 files changed, 56 insertions(+), 29 deletions(-) diff --git a/skimage/feature/__init__.py b/skimage/feature/__init__.py index 09ac4540..3e9050c0 100644 --- a/skimage/feature/__init__.py +++ b/skimage/feature/__init__.py @@ -6,6 +6,7 @@ from .corner import (corner_kitchen_rosenfeld, corner_harris, corner_shi_tomasi, corner_foerstner, corner_subpix, corner_peaks) from .corner_cy import corner_moravec from .template import match_template +from ._brief import brief __all__ = ['daisy', @@ -21,4 +22,5 @@ __all__ = ['daisy', 'corner_subpix', 'corner_peaks', 'corner_moravec', - 'match_template'] + 'match_template', + 'brief'] diff --git a/skimage/feature/_brief.py b/skimage/feature/_brief.py index b12d827b..8cca5abc 100644 --- a/skimage/feature/_brief.py +++ b/skimage/feature/_brief.py @@ -1,20 +1,28 @@ import numpy as np -from skimage.color import rgb2gray from scipy.ndimage.filters import gaussian_filter from scipy.spatial.distance import hamming +from ..color import rgb2gray +from ..util import img_as_float + +from ._brief_cy import _brief_loop + def _remove_border_keypoints(image, keypoints, dist): width = image.shape[0] height = image.shape[1] - keypoints = keypoints[(dist < keypoints[:, 0]) & (keypoints[:, 0] < width - dist) & - (dist < keypoints[:, 1]) & (keypoints[:, 1] < height - dist)] + keypoints = keypoints[(dist < keypoints[:, 0]) + & (keypoints[:, 0] < width - dist) + & (dist < keypoints[:, 1]) + & (keypoints[:, 1] < height - dist)] + return keypoints -def brief(image, keypoints, descriptor_size=256, mode='normal', patch_size=49, sample_seed=1): +def brief(image, keypoints, descriptor_size=256, mode='normal', patch_size=49, + sample_seed=1): """Extract BRIEF Descriptor about given keypoints for a given image. Parameters @@ -49,41 +57,55 @@ def brief(image, keypoints, descriptor_size=256, mode='normal', patch_size=49, s http://cvlabwww.epfl.ch/~lepetit/papers/calonder_eccv10.pdf """ - if np.squeeze(image).ndim == 3: - image = rgb2gray(image) - keypoints = np.array(keypoints + 0.5, dtype=np.intp) + np.random.seed(sample_seed) - # Removing keypoints that are (patch_size / 2) distance from the image border + image = np.squeeze(image) + if image.ndim != 2: + raise ValueError("Only 2-D gray-scale images supported.") + + image = img_as_float(image) + + # Gaussian Low pass filtering with variance 2 to alleviate noise + # sensitivity + image = gaussian_filter(image, 2) + + image = np.ascontiguousarray(image) + + keypoints = np.array(keypoints + 0.5, dtype=np.intp, order='C') + + # Removing keypoints that are (patch_size / 2) distance from the image + # border keypoints = _remove_border_keypoints(image, keypoints, patch_size / 2) - descriptor = np.zeros((len(keypoints), descriptor_size), dtype=bool) - - # Gaussian Low pass filtering with variance 2 to alleviate noise sensitivity - image = gaussian_filter(image, 2) + descriptors = np.zeros((keypoints.shape[0], descriptor_size), + dtype=bool, order='C') # Sampling pairs of decision pixels in patch_size x patch_size window if mode == 'normal': - np.random.seed(sample_seed) - samples = np.round((patch_size / 5) * np.random.randn(descriptor_size * 8)) - samples = samples[(samples < (patch_size / 2)) & (samples > - (patch_size - 1) / 2)] - first = (samples[: descriptor_size * 2]).reshape(descriptor_size, 2) - second = (samples[descriptor_size * 2: descriptor_size * 4]).reshape(descriptor_size, 2) + + samples = (patch_size / 5) * np.random.randn(descriptor_size * 8) + samples = np.array(samples, dtype=np.int32) + samples = samples[(samples < (patch_size / 2)) + & (samples > - (patch_size - 1) / 2)] + + pos1 = samples[:descriptor_size * 2] + pos1 = pos1.reshape(descriptor_size, 2) + pos2 = samples[descriptor_size * 2:descriptor_size * 4] + pos2 = pos2.reshape(descriptor_size, 2) + else: - np.random.seed(sample_seed) - samples = np.random.randint(-patch_size / 2, (patch_size / 2) + 1, (descriptor_size * 2, 2)) - first, second = np.split(samples, 2) - # Intensity comparison tests for building the descriptor - for i in range(len(keypoints)): - set_1 = first + keypoints[i] - set_2 = second + keypoints[i] + samples = np.random.randint(-patch_size / 2, (patch_size / 2) + 1, + (descriptor_size * 2, 2)) + pos1, pos2 = np.split(samples, 2) - for j in range(descriptor_size): - if image[set_1[j, 0]][set_1[j, 1]] < image[set_2[j, 0]][set_2[j, 0]]: - descriptor[i][j] = True + pos1 = np.ascontiguousarray(pos1) + pos2 = np.ascontiguousarray(pos2) - return descriptor + _brief_loop(image, descriptors.view(np.uint8), keypoints, pos1, pos2) + + return descriptors def hamming_distance(descriptor_1, descriptor_2): diff --git a/skimage/feature/setup.py b/skimage/feature/setup.py index e769621d..f4c62957 100644 --- a/skimage/feature/setup.py +++ b/skimage/feature/setup.py @@ -13,11 +13,14 @@ def configuration(parent_package='', top_path=None): config.add_data_dir('tests') cython(['corner_cy.pyx'], working_path=base_path) + cython(['_brief_cy.pyx'], working_path=base_path) cython(['_texture.pyx'], working_path=base_path) cython(['_template.pyx'], working_path=base_path) config.add_extension('corner_cy', sources=['corner_cy.c'], include_dirs=[get_numpy_include_dirs()]) + config.add_extension('_brief_cy', sources=['_brief_cy.c'], + include_dirs=[get_numpy_include_dirs()]) config.add_extension('_texture', sources=['_texture.c'], include_dirs=[get_numpy_include_dirs(), '../_shared']) config.add_extension('_template', sources=['_template.c'], From df2233cc6ae9ee8dac800a204c656e5fa2efddc2 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Wed, 3 Jul 2013 00:49:36 +0200 Subject: [PATCH 135/736] Remove old commented-out code --- skimage/segmentation/_slic.pyx | 2 -- 1 file changed, 2 deletions(-) diff --git a/skimage/segmentation/_slic.pyx b/skimage/segmentation/_slic.pyx index 654c5cff..f6c6788c 100644 --- a/skimage/segmentation/_slic.pyx +++ b/skimage/segmentation/_slic.pyx @@ -60,8 +60,6 @@ def _slic_cython(double[:, :, :, ::1] image_zyx, cdef double dist_mean cdef double tmp - #cdef long[::1] nearest_mean_ravel - #cdef double[::1] image_zyx_ravel_j for i in range(max_iter): changes = 0 distance[:, :, :] = np.inf From bcacd06f9c64fe580b031bd780f1ad14cb296691 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Tue, 2 Jul 2013 21:59:49 +0200 Subject: [PATCH 136/736] Outsource inner brief loop into cython file --- skimage/feature/__init__.py | 4 +- skimage/feature/_brief.py | 78 ++++++++++++++++++++++------------- skimage/feature/_brief_cy.pyx | 24 +++++++++++ skimage/feature/setup.py | 3 ++ 4 files changed, 80 insertions(+), 29 deletions(-) create mode 100644 skimage/feature/_brief_cy.pyx diff --git a/skimage/feature/__init__.py b/skimage/feature/__init__.py index 09ac4540..3e9050c0 100644 --- a/skimage/feature/__init__.py +++ b/skimage/feature/__init__.py @@ -6,6 +6,7 @@ from .corner import (corner_kitchen_rosenfeld, corner_harris, corner_shi_tomasi, corner_foerstner, corner_subpix, corner_peaks) from .corner_cy import corner_moravec from .template import match_template +from ._brief import brief __all__ = ['daisy', @@ -21,4 +22,5 @@ __all__ = ['daisy', 'corner_subpix', 'corner_peaks', 'corner_moravec', - 'match_template'] + 'match_template', + 'brief'] diff --git a/skimage/feature/_brief.py b/skimage/feature/_brief.py index b12d827b..8cca5abc 100644 --- a/skimage/feature/_brief.py +++ b/skimage/feature/_brief.py @@ -1,20 +1,28 @@ import numpy as np -from skimage.color import rgb2gray from scipy.ndimage.filters import gaussian_filter from scipy.spatial.distance import hamming +from ..color import rgb2gray +from ..util import img_as_float + +from ._brief_cy import _brief_loop + def _remove_border_keypoints(image, keypoints, dist): width = image.shape[0] height = image.shape[1] - keypoints = keypoints[(dist < keypoints[:, 0]) & (keypoints[:, 0] < width - dist) & - (dist < keypoints[:, 1]) & (keypoints[:, 1] < height - dist)] + keypoints = keypoints[(dist < keypoints[:, 0]) + & (keypoints[:, 0] < width - dist) + & (dist < keypoints[:, 1]) + & (keypoints[:, 1] < height - dist)] + return keypoints -def brief(image, keypoints, descriptor_size=256, mode='normal', patch_size=49, sample_seed=1): +def brief(image, keypoints, descriptor_size=256, mode='normal', patch_size=49, + sample_seed=1): """Extract BRIEF Descriptor about given keypoints for a given image. Parameters @@ -49,41 +57,55 @@ def brief(image, keypoints, descriptor_size=256, mode='normal', patch_size=49, s http://cvlabwww.epfl.ch/~lepetit/papers/calonder_eccv10.pdf """ - if np.squeeze(image).ndim == 3: - image = rgb2gray(image) - keypoints = np.array(keypoints + 0.5, dtype=np.intp) + np.random.seed(sample_seed) - # Removing keypoints that are (patch_size / 2) distance from the image border + image = np.squeeze(image) + if image.ndim != 2: + raise ValueError("Only 2-D gray-scale images supported.") + + image = img_as_float(image) + + # Gaussian Low pass filtering with variance 2 to alleviate noise + # sensitivity + image = gaussian_filter(image, 2) + + image = np.ascontiguousarray(image) + + keypoints = np.array(keypoints + 0.5, dtype=np.intp, order='C') + + # Removing keypoints that are (patch_size / 2) distance from the image + # border keypoints = _remove_border_keypoints(image, keypoints, patch_size / 2) - descriptor = np.zeros((len(keypoints), descriptor_size), dtype=bool) - - # Gaussian Low pass filtering with variance 2 to alleviate noise sensitivity - image = gaussian_filter(image, 2) + descriptors = np.zeros((keypoints.shape[0], descriptor_size), + dtype=bool, order='C') # Sampling pairs of decision pixels in patch_size x patch_size window if mode == 'normal': - np.random.seed(sample_seed) - samples = np.round((patch_size / 5) * np.random.randn(descriptor_size * 8)) - samples = samples[(samples < (patch_size / 2)) & (samples > - (patch_size - 1) / 2)] - first = (samples[: descriptor_size * 2]).reshape(descriptor_size, 2) - second = (samples[descriptor_size * 2: descriptor_size * 4]).reshape(descriptor_size, 2) + + samples = (patch_size / 5) * np.random.randn(descriptor_size * 8) + samples = np.array(samples, dtype=np.int32) + samples = samples[(samples < (patch_size / 2)) + & (samples > - (patch_size - 1) / 2)] + + pos1 = samples[:descriptor_size * 2] + pos1 = pos1.reshape(descriptor_size, 2) + pos2 = samples[descriptor_size * 2:descriptor_size * 4] + pos2 = pos2.reshape(descriptor_size, 2) + else: - np.random.seed(sample_seed) - samples = np.random.randint(-patch_size / 2, (patch_size / 2) + 1, (descriptor_size * 2, 2)) - first, second = np.split(samples, 2) - # Intensity comparison tests for building the descriptor - for i in range(len(keypoints)): - set_1 = first + keypoints[i] - set_2 = second + keypoints[i] + samples = np.random.randint(-patch_size / 2, (patch_size / 2) + 1, + (descriptor_size * 2, 2)) + pos1, pos2 = np.split(samples, 2) - for j in range(descriptor_size): - if image[set_1[j, 0]][set_1[j, 1]] < image[set_2[j, 0]][set_2[j, 0]]: - descriptor[i][j] = True + pos1 = np.ascontiguousarray(pos1) + pos2 = np.ascontiguousarray(pos2) - return descriptor + _brief_loop(image, descriptors.view(np.uint8), keypoints, pos1, pos2) + + return descriptors def hamming_distance(descriptor_1, descriptor_2): diff --git a/skimage/feature/_brief_cy.pyx b/skimage/feature/_brief_cy.pyx new file mode 100644 index 00000000..43d54f7a --- /dev/null +++ b/skimage/feature/_brief_cy.pyx @@ -0,0 +1,24 @@ +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False + +cimport numpy as cnp + + +def _brief_loop(double[:, ::1] image, char[:, ::1] descriptors, + Py_ssize_t[:, ::1] keypoints, + int[:, ::1] pos0, int[:, ::1] pos1): + + cdef Py_ssize_t k, d, kr, kc, pr0, pr1, pc0, pc1 + + for p in range(pos0.shape[0]): + pr0 = pos0[p, 0] + pc0 = pos0[p, 1] + pr1 = pos1[p, 0] + pc1 = pos1[p, 1] + for k in range(keypoints.shape[0]): + kr = keypoints[k, 0] + kc = keypoints[k, 1] + if image[kr + pr0, kc + pc0] < image[kr + pr1, kc + pc1]: + descriptors[k, p] = True diff --git a/skimage/feature/setup.py b/skimage/feature/setup.py index e769621d..f4c62957 100644 --- a/skimage/feature/setup.py +++ b/skimage/feature/setup.py @@ -13,11 +13,14 @@ def configuration(parent_package='', top_path=None): config.add_data_dir('tests') cython(['corner_cy.pyx'], working_path=base_path) + cython(['_brief_cy.pyx'], working_path=base_path) cython(['_texture.pyx'], working_path=base_path) cython(['_template.pyx'], working_path=base_path) config.add_extension('corner_cy', sources=['corner_cy.c'], include_dirs=[get_numpy_include_dirs()]) + config.add_extension('_brief_cy', sources=['_brief_cy.c'], + include_dirs=[get_numpy_include_dirs()]) config.add_extension('_texture', sources=['_texture.c'], include_dirs=[get_numpy_include_dirs(), '../_shared']) config.add_extension('_template', sources=['_template.c'], From 3dcd24e6f36ae841a159ca66d600dbd23493471d Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Wed, 3 Jul 2013 23:19:43 +0800 Subject: [PATCH 137/736] Clearing docs and making feature.util --- skimage/feature/__init__.py | 5 ++- skimage/feature/_brief.py | 76 ++++++++++--------------------------- skimage/feature/util.py | 50 ++++++++++++++++++++++++ 3 files changed, 72 insertions(+), 59 deletions(-) create mode 100644 skimage/feature/util.py diff --git a/skimage/feature/__init__.py b/skimage/feature/__init__.py index 3e9050c0..1a755da7 100644 --- a/skimage/feature/__init__.py +++ b/skimage/feature/__init__.py @@ -7,7 +7,7 @@ from .corner import (corner_kitchen_rosenfeld, corner_harris, corner_shi_tomasi, from .corner_cy import corner_moravec from .template import match_template from ._brief import brief - +from .util import hamming_distance __all__ = ['daisy', 'hog', @@ -23,4 +23,5 @@ __all__ = ['daisy', 'corner_peaks', 'corner_moravec', 'match_template', - 'brief'] + 'brief', + 'hamming_distance'] diff --git a/skimage/feature/_brief.py b/skimage/feature/_brief.py index 8cca5abc..facb28c0 100644 --- a/skimage/feature/_brief.py +++ b/skimage/feature/_brief.py @@ -1,33 +1,20 @@ import numpy as np from scipy.ndimage.filters import gaussian_filter -from scipy.spatial.distance import hamming from ..color import rgb2gray from ..util import img_as_float +from .util import _remove_border_keypoints from ._brief_cy import _brief_loop -def _remove_border_keypoints(image, keypoints, dist): - - width = image.shape[0] - height = image.shape[1] - - keypoints = keypoints[(dist < keypoints[:, 0]) - & (keypoints[:, 0] < width - dist) - & (dist < keypoints[:, 1]) - & (keypoints[:, 1] < height - dist)] - - return keypoints - - def brief(image, keypoints, descriptor_size=256, mode='normal', patch_size=49, - sample_seed=1): + sample_seed=1, variance=2, return_keypoints=False): """Extract BRIEF Descriptor about given keypoints for a given image. Parameters ---------- - image : ndarray + image : 2D ndarray Input image. keypoints : (P, 2) ndarray Array of keypoint locations. @@ -42,13 +29,20 @@ def brief(image, keypoints, descriptor_size=256, mode='normal', patch_size=49, the keypoints. Default is 49. sample_seed : int Seed for sampling the decision pixel-pairs. Default is 1. + return_keypoints : bool + If True, return the Q keypoints (after filtering out the border + keypoints) about which the descriptors are extracted. Default is False. Returns ------- - descriptor : ndarray with dtype bool - 2D ndarray of dimensions (no_of_keypoints, descriptor_size) with value - at an index (i, j) either being True or False representing the outcome + descriptors : (Q, descriptor_size) ndarray of dtype bool + 2D ndarray of binary descriptors of size descriptor_size about Q + keypoints after filtering out border keypoints with value at an index + (i, j) either being True or False representing the outcome of Intensity comparison about ith keypoint on jth decision pixel-pair. + keypoints : (Q, 2) ndarray + Keypoints after removing out those that are near border. + Returned only if return_keypoints is True. References ---------- @@ -66,9 +60,9 @@ def brief(image, keypoints, descriptor_size=256, mode='normal', patch_size=49, image = img_as_float(image) - # Gaussian Low pass filtering with variance 2 to alleviate noise + # Gaussian Low pass filtering to alleviate noise # sensitivity - image = gaussian_filter(image, 2) + image = gaussian_filter(image, variance) image = np.ascontiguousarray(image) @@ -105,39 +99,7 @@ def brief(image, keypoints, descriptor_size=256, mode='normal', patch_size=49, _brief_loop(image, descriptors.view(np.uint8), keypoints, pos1, pos2) - return descriptors - - -def hamming_distance(descriptor_1, descriptor_2): - """A dissimilarity measure used for matching keypoints in different images - using binary feature descriptors like BRIEF etc. - - Parameters - ---------- - descriptor_1 : ndarray with dtype bool - Binary feature descriptor for keypoints in the first image. - 2D ndarray of dimensions (no_of_keypoints_in_image_1, descriptor_size) - with value at an index (i, j) either being True or False representing - the outcome of Intensity comparison about ith keypoint on jth decision - pixel-pair. - descriptor_2 : ndarray with dtype bool - Binary feature descriptor for keypoints in the second image. - 2D ndarray of dimensions (no_of_keypoints_in_image_2, descriptor_size) - with value at an index (i, j) either being True or False representing - the outcome of Intensity comparison about ith keypoint on jth decision - pixel-pair. - - Returns - ------- - distance : ndarray - 2D ndarray of dimensions (no_of_rows_in_descripto_1, no_of_rows_in_descripto_2) - with value at an index (i, j) between the range [0, 1] representing the - extent of dissimilarity between ith keypoint of in first image and jth - keypoint in second image. - - """ - distance = np.zeros((len(descriptor_1), len(descriptor_2)), dtype=float) - for i in range(len(descriptor_1)): - for j in range(len(descriptor_2)): - distance[i, j] = hamming(descriptor_1[i][:], descriptor_2[j][:]) - return distance + if return_keypoints: + return descriptors, keypoints + else: + return descriptors diff --git a/skimage/feature/util.py b/skimage/feature/util.py new file mode 100644 index 00000000..5747eb47 --- /dev/null +++ b/skimage/feature/util.py @@ -0,0 +1,50 @@ +import numpy as np +from scipy.spatial.distance import hamming + + +def _remove_border_keypoints(image, keypoints, dist): + """Removes keypoints that are within dist pixels from the image border. + """ + width = image.shape[0] + height = image.shape[1] + + keypoints = keypoints[(dist < keypoints[:, 0]) + & (keypoints[:, 0] < width - dist) + & (dist < keypoints[:, 1]) + & (keypoints[:, 1] < height - dist)] + + return keypoints + + +def hamming_distance(descriptors1, descriptors2): + """A dissimilarity measure used for matching keypoints in different images + using binary feature descriptors like BRIEF etc. + + Parameters + ---------- + descriptors1 : (P1, D) array of dtype bool + Binary feature descriptors for keypoints in the first image. + 2D ndarray with a binary descriptors of size D about P1 keypoints + with value at an index (i, j) either being True or False representing + the outcome of Intensity comparison about ith keypoint on jth decision + pixel-pair. + descriptors2 : (P2, D) array of dtype bool + Binary feature descriptors for keypoints in the second image. + 2D ndarray with a binary descriptors of size D about P2 keypoints + with value at an index (i, j) either being True or False representing + the outcome of Intensity comparison about ith keypoint on jth decision + pixel-pair. + + Returns + ------- + distance : (P1, P2) array of dtype float + 2D ndarray with value at an index (i, j) in the range [0, 1] + representing the extent of dissimilarity between ith keypoint of in + first image and jth keypoint in second image. + + """ + distance = np.zeros((descriptors1.shape[0], descriptors2.shape[0]), dtype=float) + for i in range(descriptors1.shape[0]): + for j in range(descriptors2.shape[0]): + distance[i, j] = hamming(descriptors1[i, :], descriptors2[j, :]) + return distance From 56e7ea23c50e67b3ebc3fbf048a4b12c4d396b03 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Wed, 3 Jul 2013 23:32:49 +0800 Subject: [PATCH 138/736] Removing unused import and fixing indentation issue --- skimage/feature/_brief.py | 1 - skimage/feature/util.py | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/skimage/feature/_brief.py b/skimage/feature/_brief.py index facb28c0..23dc63ab 100644 --- a/skimage/feature/_brief.py +++ b/skimage/feature/_brief.py @@ -1,7 +1,6 @@ import numpy as np from scipy.ndimage.filters import gaussian_filter -from ..color import rgb2gray from ..util import img_as_float from .util import _remove_border_keypoints diff --git a/skimage/feature/util.py b/skimage/feature/util.py index 5747eb47..b634305b 100644 --- a/skimage/feature/util.py +++ b/skimage/feature/util.py @@ -3,8 +3,7 @@ from scipy.spatial.distance import hamming def _remove_border_keypoints(image, keypoints, dist): - """Removes keypoints that are within dist pixels from the image border. - """ + """Removes keypoints that are within dist pixels from the image border.""" width = image.shape[0] height = image.shape[1] From 32f39eecbbc82380a0a0342fd56f28daf7e6ee29 Mon Sep 17 00:00:00 2001 From: "Josh Warner (Mac)" Date: Wed, 3 Jul 2013 23:54:26 -0500 Subject: [PATCH 139/736] ENH: Improve random_noise with better param names & docfixes --- skimage/util/noise.py | 56 +++++++++++++++---------- skimage/util/tests/test_random_noise.py | 22 ++++++---- 2 files changed, 47 insertions(+), 31 deletions(-) diff --git a/skimage/util/noise.py b/skimage/util/noise.py index bf1d412d..71b69aad 100644 --- a/skimage/util/noise.py +++ b/skimage/util/noise.py @@ -24,18 +24,20 @@ def random_noise(image, mode='gaussian', seed=None, **kwargs): 'speckle' Multiplicative noise using out = image + n*image, where n is uniform noise with specified mean & variance. seed : int - If provided, this will set the random seed before generating noise. - m : float + If provided, this will set the random seed before generating noise, + for valid pseudo-random comparisons. + mean : float Mean of random distribution. Used in 'gaussian' and 'speckle'. - v : float + Default : 0. + var : float Variance of random distribution. Used in 'gaussian' and 'speckle'. - Note: variance = (standard deviation) ** 2 - d : float + Note: variance = (standard deviation) ** 2. Default : 0.01 + prop_replace : float Proportion of image pixels to replace with noise on range [0, 1]. - Used in 'salt', 'pepper', and 'salt & pepper'. - p : float + Used in 'salt', 'pepper', and 'salt & pepper'. Default : 0.05 + prop_salt : float Proportion of salt vs. pepper noise for 's&p' on range [0, 1]. - Higher values represent more salt. + Higher values represent more salt. Default : 0.5 (equal amounts) Returns ------- @@ -57,15 +59,15 @@ def random_noise(image, mode='gaussian', seed=None, **kwargs): 'speckle': 'gaussian_values'} kwdefaults = { - 'm': 0., - 'v': 0.01, - 'd': 0.05, - 'p': 0.5} + 'mean': 0., + 'var': 0.01, + 'prop_replace': 0.05, + 'prop_salt': 0.5} allowedkwargs = { - 'gaussian_values': ['m', 'v'], - 'sp_values': ['d'], - 's&p_values': ['d', 'p']} + 'gaussian_values': ['mean', 'var'], + 'sp_values': ['prop_replace'], + 's&p_values': ['prop_replace', 'prop_salt']} for key in kwargs: if key not in allowedkwargs[allowedtypes[mode]]: @@ -77,7 +79,8 @@ def random_noise(image, mode='gaussian', seed=None, **kwargs): kwargs.setdefault(kw, kwdefaults[kw]) if mode == 'gaussian': - noise = np.random.normal(kwargs['m'], kwargs['v'] ** 0.5, image.shape) + noise = np.random.normal(kwargs['mean'], kwargs['var'] ** 0.5, + image.shape) out = np.clip(image + noise, 0., 1.) elif mode == 'poisson': @@ -91,29 +94,36 @@ def random_noise(image, mode='gaussian', seed=None, **kwargs): elif mode == 'salt': # Re-call function with mode='s&p' and p=1 (all salt noise) - out = random_noise(image, mode='s&p', seed=seed, d=kwargs['d'], p=1) + out = random_noise(image, mode='s&p', seed=seed, + prop_replace=kwargs['prop_replace'], prop_salt=1.) elif mode == 'pepper': # Re-call function with mode='s&p' and p=1 (all pepper noise) - out = random_noise(image, mode='s&p', seed=seed, d=kwargs['d'], p=0) + out = random_noise(image, mode='s&p', seed=seed, + prop_replace=kwargs['prop_replace'], prop_salt=0.) elif mode == 's&p': + # This mode makes no effort to avoid repeat sampling. Thus, the + # exact number of replaced pixels is only approximate. out = image.copy() # Salt mode - num_salt = np.ceil(kwargs['d'] * image.size * kwargs['p']) - coords = [np.random.randint(0, i - 1, num_salt) + num_salt = np.ceil( + kwargs['prop_replace'] * image.size * kwargs['prop_salt']) + coords = [np.random.randint(0, i - 1, int(num_salt)) for i in image.shape] out[coords] = 1 # Pepper mode - num_pepper = np.ceil(kwargs['d'] * image.size * (1. - kwargs['p'])) - coords = [np.random.randint(0, i - 1, num_pepper) + num_pepper = np.ceil( + kwargs['prop_replace'] * image.size * (1. - kwargs['prop_salt'])) + coords = [np.random.randint(0, i - 1, int(num_pepper)) for i in image.shape] out[coords] = 0 elif mode == 'speckle': - noise = np.random.normal(kwargs['m'], kwargs['v'] ** 0.5, image.shape) + noise = np.random.normal(kwargs['mean'], kwargs['var'] ** 0.5, + image.shape) out = np.clip(image + image * noise, 0., 1.) return out diff --git a/skimage/util/tests/test_random_noise.py b/skimage/util/tests/test_random_noise.py index 98b78117..6b6ba5c4 100644 --- a/skimage/util/tests/test_random_noise.py +++ b/skimage/util/tests/test_random_noise.py @@ -1,4 +1,4 @@ -from numpy.testing import assert_array_equal, assert_allclose +from numpy.testing import assert_array_equal, assert_allclose, assert_raises import numpy as np from skimage.data import camera @@ -15,7 +15,7 @@ def test_set_seed(): def test_salt(): seed = 42 cam = img_as_float(camera()) - cam_noisy = random_noise(cam, seed=seed, mode='salt', d=0.15) + cam_noisy = random_noise(cam, seed=seed, mode='salt', prop_replace=0.15) saltmask = cam != cam_noisy # Ensure all changes are to 1.0 @@ -29,7 +29,7 @@ def test_salt(): def test_pepper(): seed = 42 cam = img_as_float(camera()) - cam_noisy = random_noise(cam, seed=seed, mode='pepper', d=0.15) + cam_noisy = random_noise(cam, seed=seed, mode='pepper', prop_replace=0.15) peppermask = cam != cam_noisy # Ensure all changes are to 1.0 @@ -43,7 +43,8 @@ def test_pepper(): def test_salt_and_pepper(): seed = 42 cam = img_as_float(camera()) - cam_noisy = random_noise(cam, seed=seed, mode='s&p', d=0.15, p=0.25) + cam_noisy = random_noise(cam, seed=seed, mode='s&p', prop_replace=0.15, + prop_salt=0.25) saltmask = np.logical_and(cam != cam_noisy, cam_noisy == 1.) peppermask = np.logical_and(cam != cam_noisy, cam_noisy == 0.) @@ -63,10 +64,10 @@ def test_salt_and_pepper(): def test_gaussian(): seed = 42 data = np.zeros((128, 128)) + 0.5 - data_gaussian = random_noise(data, seed=seed, v=0.01) + data_gaussian = random_noise(data, seed=seed, var=0.01) assert 0.008 < data_gaussian.var() < 0.012 - data_gaussian = random_noise(data, seed=seed, m=0.3, v=0.015) + data_gaussian = random_noise(data, seed=seed, mean=0.3, var=0.015) assert 0.28 < data_gaussian.mean() - 0.5 < 0.32 assert 0.012 < data_gaussian.var() < 0.018 @@ -78,10 +79,15 @@ def test_speckle(): noise = np.random.normal(0.1, 0.02 ** 0.5, (128, 128)) expected = np.clip(data + data * noise, 0, 1) - data_speckle = random_noise(data, mode='speckle', seed=seed, m=0.1, - v=0.02) + data_speckle = random_noise(data, mode='speckle', seed=seed, mean=0.1, + var=0.02) assert_allclose(expected, data_speckle) +def test_bad_mode(): + data = np.zeros((64, 64)) + assert_raises(KeyError, random_noise, data, 'perlin') + + if __name__ == '__main__': np.testing.run_module_suite() From b551a49708caf0fd30e057d2c2aeaca4f87aeda0 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Sat, 20 Apr 2013 05:00:40 +0530 Subject: [PATCH 140/736] First implementation of Integer Up/Downsampling --- skimage/transform/rescale.py | 51 ++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 skimage/transform/rescale.py diff --git a/skimage/transform/rescale.py b/skimage/transform/rescale.py new file mode 100644 index 00000000..69e004a9 --- /dev/null +++ b/skimage/transform/rescale.py @@ -0,0 +1,51 @@ +# TODO : Doc, Tests, PEP8 check + +import numpy as np + +def downsample(image, factors, how = 'sum'): + + is0 = image.shape[0] + is1 = image.shape[1] + f0 = factors[0] + f1 = factors[1] + + if (f0 - int(f0) != 0) or (f1 - int(f1) != 0): + return "Use resample for non-integer downsampling" + cropped = image[: is0 - (is0 % f0), : is1 - (is1 % f1)] + out = np.zeros((cropped.shape[0] / f0, cropped.shape[1] / f1)) + + if how == 'sum': + for i in range(cropped.shape[0]): + for j in range(cropped.shape[1]): + out[int(i / f0)][int(j / f1)] += cropped[i][j] + return out + + if how == 'mean': + for i in range(cropped.shape[0]): + for j in range(cropped.shape[1]): + out[int(i / f0)][int(j / f1)] += cropped[i][j] / float(f0 * f1) + return out + + +def upsample(image, factors, how = 'divide'): + + is0 = image.shape[0] + is1 = image.shape[1] + f0 = factors[0] + f1 = factors[1] + + if (f0 - int(f0) != 0) or (f1 - int(f1) != 0): + return "Use resample for non-integer upsampling" + out = np.zeros((f0 * image.shape[0], f1 * image.shape[1])) + + if how == 'divide': + for i in range(out.shape[0]): + for j in range(out.shape[1]): + out[i][j] = (image[i / f0][j / f1]) / float(f0 * f1) + return out + + if how == 'uniform': + for i in range(out.shape[0]): + for j in range(out.shape[1]): + out[i][j] = (image[i / f0][j / f1]) + return out From 19f0be9fbd4fab690546f23cd7a893d62e34b5d8 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Sat, 20 Apr 2013 18:19:28 +0530 Subject: [PATCH 141/736] PEP8 corrections --- skimage/transform/rescale.py | 76 ++++++++++++++++++------------------ 1 file changed, 39 insertions(+), 37 deletions(-) diff --git a/skimage/transform/rescale.py b/skimage/transform/rescale.py index 69e004a9..4ec4ab99 100644 --- a/skimage/transform/rescale.py +++ b/skimage/transform/rescale.py @@ -2,50 +2,52 @@ import numpy as np -def downsample(image, factors, how = 'sum'): +def downsample(image, factors, method='sum'): - is0 = image.shape[0] - is1 = image.shape[1] - f0 = factors[0] - f1 = factors[1] + is0 = image.shape[0] + is1 = image.shape[1] + f0 = factors[0] + f1 = factors[1] - if (f0 - int(f0) != 0) or (f1 - int(f1) != 0): - return "Use resample for non-integer downsampling" - cropped = image[: is0 - (is0 % f0), : is1 - (is1 % f1)] - out = np.zeros((cropped.shape[0] / f0, cropped.shape[1] / f1)) + if (f0 - int(f0) != 0) or (f1 - int(f1) != 0): + print "Use resample for non-integer downsampling" + return + cropped = image[: is0 - (is0 % f0), : is1 - (is1 % f1)] + out = np.zeros((cropped.shape[0] / f0, cropped.shape[1] / f1)) - if how == 'sum': - for i in range(cropped.shape[0]): - for j in range(cropped.shape[1]): - out[int(i / f0)][int(j / f1)] += cropped[i][j] - return out + if method == 'sum': + for i in range(cropped.shape[0]): + for j in range(cropped.shape[1]): + out[int(i / f0)][int(j / f1)] += cropped[i][j] + return out - if how == 'mean': - for i in range(cropped.shape[0]): - for j in range(cropped.shape[1]): - out[int(i / f0)][int(j / f1)] += cropped[i][j] / float(f0 * f1) - return out + if method == 'mean': + for i in range(cropped.shape[0]): + for j in range(cropped.shape[1]): + out[int(i / f0)][int(j / f1)] += cropped[i][j] + return out / float(f0 * f1) -def upsample(image, factors, how = 'divide'): +def upsample(image, factors, method='divide'): - is0 = image.shape[0] - is1 = image.shape[1] - f0 = factors[0] - f1 = factors[1] + is0 = image.shape[0] + is1 = image.shape[1] + f0 = factors[0] + f1 = factors[1] - if (f0 - int(f0) != 0) or (f1 - int(f1) != 0): - return "Use resample for non-integer upsampling" - out = np.zeros((f0 * image.shape[0], f1 * image.shape[1])) + if (f0 - int(f0) != 0) or (f1 - int(f1) != 0): + print "Use resample for non-integer upsampling" + return + out = np.zeros((f0 * image.shape[0], f1 * image.shape[1])) - if how == 'divide': - for i in range(out.shape[0]): - for j in range(out.shape[1]): - out[i][j] = (image[i / f0][j / f1]) / float(f0 * f1) - return out + if method == 'divide': + for i in range(out.shape[0]): + for j in range(out.shape[1]): + out[i][j] = (image[i / f0][j / f1]) + return out / float(f0 * f1) - if how == 'uniform': - for i in range(out.shape[0]): - for j in range(out.shape[1]): - out[i][j] = (image[i / f0][j / f1]) - return out + if method == 'uniform': + for i in range(out.shape[0]): + for j in range(out.shape[1]): + out[i][j] = (image[i / f0][j / f1]) + return out From 8372b0b6aa5a48c38002cf73bdc32cdc9e7273a6 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Mon, 29 Apr 2013 23:24:07 +0530 Subject: [PATCH 142/736] Removing code repetition --- skimage/transform/rescale.py | 33 +++++++++++++-------------------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/skimage/transform/rescale.py b/skimage/transform/rescale.py index 4ec4ab99..b7164994 100644 --- a/skimage/transform/rescale.py +++ b/skimage/transform/rescale.py @@ -10,21 +10,17 @@ def downsample(image, factors, method='sum'): f1 = factors[1] if (f0 - int(f0) != 0) or (f1 - int(f1) != 0): - print "Use resample for non-integer downsampling" + print "Use resample() for non-integer downsampling" return - cropped = image[: is0 - (is0 % f0), : is1 - (is1 % f1)] + cropped = image[:is0 - (is0 % f0), :is1 - (is1 % f1)] out = np.zeros((cropped.shape[0] / f0, cropped.shape[1] / f1)) + for i in range(cropped.shape[0]): + for j in range(cropped.shape[1]): + out[int(i / f0)][int(j / f1)] += cropped[i][j] if method == 'sum': - for i in range(cropped.shape[0]): - for j in range(cropped.shape[1]): - out[int(i / f0)][int(j / f1)] += cropped[i][j] return out - - if method == 'mean': - for i in range(cropped.shape[0]): - for j in range(cropped.shape[1]): - out[int(i / f0)][int(j / f1)] += cropped[i][j] + else: return out / float(f0 * f1) @@ -36,18 +32,15 @@ def upsample(image, factors, method='divide'): f1 = factors[1] if (f0 - int(f0) != 0) or (f1 - int(f1) != 0): - print "Use resample for non-integer upsampling" + print "Use resample() for non-integer upsampling" return out = np.zeros((f0 * image.shape[0], f1 * image.shape[1])) - if method == 'divide': - for i in range(out.shape[0]): - for j in range(out.shape[1]): - out[i][j] = (image[i / f0][j / f1]) - return out / float(f0 * f1) - if method == 'uniform': - for i in range(out.shape[0]): - for j in range(out.shape[1]): - out[i][j] = (image[i / f0][j / f1]) + for i in range(out.shape[0]): + for j in range(out.shape[1]): + out[i][j] = (image[i / f0][j / f1]) + if method == 'divide': + return out / float(f0 * f1) + else: return out From b5804214b02a4f68a201263d955bf56e7f05fd02 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Tue, 30 Apr 2013 19:49:44 +0530 Subject: [PATCH 143/736] Minor refactoring --- skimage/transform/rescale.py | 37 +++++++++++++++--------------------- 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/skimage/transform/rescale.py b/skimage/transform/rescale.py index b7164994..e974bc8f 100644 --- a/skimage/transform/rescale.py +++ b/skimage/transform/rescale.py @@ -4,43 +4,36 @@ import numpy as np def downsample(image, factors, method='sum'): - is0 = image.shape[0] - is1 = image.shape[1] - f0 = factors[0] - f1 = factors[1] + is = image.shape + f = factors - if (f0 - int(f0) != 0) or (f1 - int(f1) != 0): - print "Use resample() for non-integer downsampling" - return - cropped = image[:is0 - (is0 % f0), :is1 - (is1 % f1)] - out = np.zeros((cropped.shape[0] / f0, cropped.shape[1] / f1)) + if (f[0] - int(f[0]) != 0) or (f[1] - int(f[1]) != 0): + raise ValueError('Use resample() for non-integer downsampling') + cropped = image[:is[0] - (is[0] % f[0]), :is[1] - (is[1] % f[1])] + out = np.zeros((cropped.shape[0] / f[0], cropped.shape[1] / f[1])) for i in range(cropped.shape[0]): for j in range(cropped.shape[1]): - out[int(i / f0)][int(j / f1)] += cropped[i][j] + out[int(i / f[0])][int(j / f[1])] += cropped[i][j] if method == 'sum': return out else: - return out / float(f0 * f1) + return out / float(f[0] * f[1]) def upsample(image, factors, method='divide'): - is0 = image.shape[0] - is1 = image.shape[1] - f0 = factors[0] - f1 = factors[1] - - if (f0 - int(f0) != 0) or (f1 - int(f1) != 0): - print "Use resample() for non-integer upsampling" - return - out = np.zeros((f0 * image.shape[0], f1 * image.shape[1])) + is = image.shape + f = factors + if (f[0] - int(f[0]) != 0) or (f[1] - int(f[1]) != 0): + raise ValueError('Use resample() for non-integer upsampling') + out = np.zeros((f[0] * image.shape[0], f[1] * image.shape[1])) for i in range(out.shape[0]): for j in range(out.shape[1]): - out[i][j] = (image[i / f0][j / f1]) + out[i][j] = (image[i / f[0]][j / f[1]]) if method == 'divide': - return out / float(f0 * f1) + return out / float(f[0] * f[1]) else: return out From bfc2aac3e5771bb669aa7abb5dc510e971783dd2 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Tue, 30 Apr 2013 21:40:16 +0530 Subject: [PATCH 144/736] Downsampling of nD arrays --- skimage/transform/rescale.py | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/skimage/transform/rescale.py b/skimage/transform/rescale.py index e974bc8f..c7d77ee7 100644 --- a/skimage/transform/rescale.py +++ b/skimage/transform/rescale.py @@ -1,29 +1,24 @@ # TODO : Doc, Tests, PEP8 check import numpy as np +from skimage.util.shape import view_as_blocks def downsample(image, factors, method='sum'): - is = image.shape - f = factors + # works only if image.shape is perfectly divisible by factors + out = view_as_blocks(image, factors) + block_shape = out.shape - if (f[0] - int(f[0]) != 0) or (f[1] - int(f[1]) != 0): - raise ValueError('Use resample() for non-integer downsampling') - cropped = image[:is[0] - (is[0] % f[0]), :is[1] - (is[1] % f[1])] - out = np.zeros((cropped.shape[0] / f[0], cropped.shape[1] / f[1])) - - for i in range(cropped.shape[0]): - for j in range(cropped.shape[1]): - out[int(i / f[0])][int(j / f[1])] += cropped[i][j] if method == 'sum': - return out + for i in range(len(block_shape)/2): + out = out.sum(-1) else: - return out / float(f[0] * f[1]) - + for i in range(len(block_shape)/2): + out = out.mean(-1) + return out def upsample(image, factors, method='divide'): - is = image.shape f = factors if (f[0] - int(f[0]) != 0) or (f[1] - int(f[1]) != 0): From 329cf37ca2e25679e1b8ceb1032e51a6874b2d8f Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Wed, 1 May 2013 11:59:02 +0530 Subject: [PATCH 145/736] Padding ndarray with zeros to support downsampling by any integer factor --- skimage/transform/rescale.py | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/skimage/transform/rescale.py b/skimage/transform/rescale.py index c7d77ee7..653bb222 100644 --- a/skimage/transform/rescale.py +++ b/skimage/transform/rescale.py @@ -3,9 +3,35 @@ import numpy as np from skimage.util.shape import view_as_blocks + +def _pad_asymmetric_zeros(arr, pad_amt, axis=-1): + """Pads `arr` by `pad_amt` along specified `axis`""" + if axis == -1: + axis = arr.ndim - 1 + + zeroshape = tuple([x if i != axis else pad_amt + for (i, x) in enumerate(arr.shape)]) + + return np.concatenate((arr, np.zeros(zeroshape, dtype=arr.dtype)), + axis=axis) + + def downsample(image, factors, method='sum'): - # works only if image.shape is perfectly divisible by factors + pad_size = [] + if len(factors) != image.ndim: + raise ValueError("'factors' must have the same length " + "as 'image.shape'") + else: + for i in range(len(factors)): + if image.shape[i] % factors[i] != 0: + pad_size.append(factors[i] - (image.shape[i] % factors[i])) + else: + pad_size.append(0) + + for i in range(len(pad_size)): + image = _pad_asymmetric_zeros(image, pad_size[i], i) + out = view_as_blocks(image, factors) block_shape = out.shape From 7282f561d43312174fdf6b2c82c514e8e87a93ed Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Mon, 6 May 2013 23:45:01 +0530 Subject: [PATCH 146/736] Added docs, tests for downsample() in skimage.transform._warps --- skimage/transform/__init__.py | 3 +- skimage/transform/_warps.py | 61 +++++++++++++++++++++++++++ skimage/transform/rescale.py | 60 -------------------------- skimage/transform/tests/test_warps.py | 29 ++++++++++++- skimage/util/shape.py | 12 ++++++ 5 files changed, 103 insertions(+), 62 deletions(-) delete mode 100644 skimage/transform/rescale.py diff --git a/skimage/transform/__init__.py b/skimage/transform/__init__.py index 859c7515..311801be 100644 --- a/skimage/transform/__init__.py +++ b/skimage/transform/__init__.py @@ -9,7 +9,7 @@ from ._geometric import (warp, warp_coords, estimate_transform, SimilarityTransform, AffineTransform, ProjectiveTransform, PolynomialTransform, PiecewiseAffineTransform) -from ._warps import swirl, resize, rotate, rescale +from ._warps import swirl, resize, rotate, rescale, downscale_local_means from .pyramids import (pyramid_reduce, pyramid_expand, pyramid_gaussian, pyramid_laplacian) @@ -39,6 +39,7 @@ __all__ = ['hough_circle', 'resize', 'rotate', 'rescale', + 'downscale_local_means', 'pyramid_reduce', 'pyramid_expand', 'pyramid_gaussian', diff --git a/skimage/transform/_warps.py b/skimage/transform/_warps.py index caf2baaf..513c8f01 100644 --- a/skimage/transform/_warps.py +++ b/skimage/transform/_warps.py @@ -1,6 +1,8 @@ import numpy as np from scipy import ndimage + from ._geometric import warp, SimilarityTransform, AffineTransform +from skimage.util.shape import view_as_blocks, _pad_asymmetric_zeros def resize(image, output_shape, order=1, mode='constant', cval=0.): @@ -283,3 +285,62 @@ def swirl(image, center=None, strength=1, radius=100, rotation=0, return warp(image, _swirl_mapping, map_args=warp_args, output_shape=output_shape, order=order, mode=mode, cval=cval) + + +def downsample(array, factors, mode='sum'): + """Performs downsampling with integer factors. + + Parameters + ---------- + array : ndarray + Input n-dimensional array. + factors: tuple + Tuple containing downsampling factor along each axis. + mode : string + Decides whether the downsampled element is the sum or mean + of its corresponding constituent elements in the input array. Default + is 'sum'. + + Returns + ------- + array : ndarray + Downsampled array with same number of dimensions as that of input + array. + + Example + ------- + >>> a = np.arange(15).reshape(3, 5) + >>> a + array([[ 0, 1, 2, 3, 4], + [ 5, 6, 7, 8, 9], + [10, 11, 12, 13, 14]]) + >>> downsample(a, (2,3)) + array([[21, 24], + [33, 27]]) + + """ + + pad_size = [] + if len(factors) != array.ndim: + raise ValueError("'factors' must have the same length " + "as 'array.shape'") + else: + for i in range(len(factors)): + if array.shape[i] % factors[i] != 0: + pad_size.append(factors[i] - (array.shape[i] % factors[i])) + else: + pad_size.append(0) + + for i in range(len(pad_size)): + array = _pad_asymmetric_zeros(array, pad_size[i], i) + + out = view_as_blocks(array, factors) + block_shape = out.shape + + if mode == 'sum': + for i in range(len(block_shape)/2): + out = out.sum(-1) + else: + for i in range(len(block_shape)/2): + out = out.mean(-1) + return out diff --git a/skimage/transform/rescale.py b/skimage/transform/rescale.py deleted file mode 100644 index 653bb222..00000000 --- a/skimage/transform/rescale.py +++ /dev/null @@ -1,60 +0,0 @@ -# TODO : Doc, Tests, PEP8 check - -import numpy as np -from skimage.util.shape import view_as_blocks - - -def _pad_asymmetric_zeros(arr, pad_amt, axis=-1): - """Pads `arr` by `pad_amt` along specified `axis`""" - if axis == -1: - axis = arr.ndim - 1 - - zeroshape = tuple([x if i != axis else pad_amt - for (i, x) in enumerate(arr.shape)]) - - return np.concatenate((arr, np.zeros(zeroshape, dtype=arr.dtype)), - axis=axis) - - -def downsample(image, factors, method='sum'): - - pad_size = [] - if len(factors) != image.ndim: - raise ValueError("'factors' must have the same length " - "as 'image.shape'") - else: - for i in range(len(factors)): - if image.shape[i] % factors[i] != 0: - pad_size.append(factors[i] - (image.shape[i] % factors[i])) - else: - pad_size.append(0) - - for i in range(len(pad_size)): - image = _pad_asymmetric_zeros(image, pad_size[i], i) - - out = view_as_blocks(image, factors) - block_shape = out.shape - - if method == 'sum': - for i in range(len(block_shape)/2): - out = out.sum(-1) - else: - for i in range(len(block_shape)/2): - out = out.mean(-1) - return out - -def upsample(image, factors, method='divide'): - - f = factors - - if (f[0] - int(f[0]) != 0) or (f[1] - int(f[1]) != 0): - raise ValueError('Use resample() for non-integer upsampling') - out = np.zeros((f[0] * image.shape[0], f[1] * image.shape[1])) - - for i in range(out.shape[0]): - for j in range(out.shape[1]): - out[i][j] = (image[i / f[0]][j / f[1]]) - if method == 'divide': - return out / float(f[0] * f[1]) - else: - return out diff --git a/skimage/transform/tests/test_warps.py b/skimage/transform/tests/test_warps.py index 93f87320..a2cd2b05 100644 --- a/skimage/transform/tests/test_warps.py +++ b/skimage/transform/tests/test_warps.py @@ -1,4 +1,4 @@ -from numpy.testing import assert_array_almost_equal, run_module_suite +from numpy.testing import assert_array_almost_equal, run_module_suite, assert_array_equal import numpy as np from scipy.ndimage import map_coordinates @@ -193,6 +193,33 @@ def test_warp_coords_example(): coords = warp_coords(tform, (30, 30, 3)) map_coordinates(image[:, :, 0], coords[:2]) +def test_downsample_sum(): + """Verifying downsampling of an array with expected result in sum mode""" + image1 = np.arange(4*6).reshape(4, 6) + out1 = tf.downsample(image1, (2, 3)) + expected1 = np.array([[ 24, 42], + [ 96, 114]]) + assert_array_equal(expected1, out1) + image2 = np.arange(5*8).reshape(5, 8) + out2 = tf.downsample(image2, (3, 3)) + expected2 = np.array([[ 81, 108, 87], + [174, 192, 138]]) + assert_array_equal(expected2, out2) + + +def test_downsample_mean(): + """Verifying downsampling of an array with expected result in mean mode""" + image1 = np.arange(4*6).reshape(4, 6) + out1 = tf.downsample(image1, (2, 3), 'mean') + expected1 = np.array([[ 4., 7.], + [ 16., 19.]]) + assert_array_equal(expected1, out1) + image2 = np.arange(5*8).reshape(5, 8) + out2 = tf.downsample(image2, (4, 5), 'mean') + expected2 = np.array([[ 14. , 10.8], + [ 8.5, 5.7]]) + assert_array_equal(expected2, out2) + if __name__ == "__main__": run_module_suite() diff --git a/skimage/util/shape.py b/skimage/util/shape.py index 0126d2e3..2763d3d4 100644 --- a/skimage/util/shape.py +++ b/skimage/util/shape.py @@ -230,3 +230,15 @@ def view_as_windows(arr_in, window_shape): arr_out = as_strided(arr_in, shape=new_shape, strides=new_strides) return arr_out + + +def _pad_asymmetric_zeros(arr, pad_amt, axis=-1): + """Pads `arr` with zeros by `pad_amt` along specified `axis`""" + if axis == -1: + axis = arr.ndim - 1 + + zeroshape = tuple([x if i != axis else pad_amt + for (i, x) in enumerate(arr.shape)]) + + return np.concatenate((arr, np.zeros(zeroshape, dtype=arr.dtype)), + axis=axis) From 2f817542b86ec3247d2728b6d829a813ba98619d Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Tue, 7 May 2013 00:40:03 +0530 Subject: [PATCH 147/736] Making code compatible with Python 3 --- skimage/transform/_warps.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skimage/transform/_warps.py b/skimage/transform/_warps.py index 513c8f01..2fcfd80d 100644 --- a/skimage/transform/_warps.py +++ b/skimage/transform/_warps.py @@ -338,9 +338,9 @@ def downsample(array, factors, mode='sum'): block_shape = out.shape if mode == 'sum': - for i in range(len(block_shape)/2): + for i in range(len(block_shape)//2): out = out.sum(-1) else: - for i in range(len(block_shape)/2): + for i in range(len(block_shape)//2): out = out.mean(-1) return out From c57c86519672f5c94c89f9c56126980b871d4649 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Mon, 27 May 2013 07:48:57 +0800 Subject: [PATCH 148/736] Add _sum_blocks --- skimage/transform/__init__.py | 1 + skimage/transform/_warps.py | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/skimage/transform/__init__.py b/skimage/transform/__init__.py index 311801be..dcd39c31 100644 --- a/skimage/transform/__init__.py +++ b/skimage/transform/__init__.py @@ -9,6 +9,7 @@ from ._geometric import (warp, warp_coords, estimate_transform, SimilarityTransform, AffineTransform, ProjectiveTransform, PolynomialTransform, PiecewiseAffineTransform) + from ._warps import swirl, resize, rotate, rescale, downscale_local_means from .pyramids import (pyramid_reduce, pyramid_expand, pyramid_gaussian, pyramid_laplacian) diff --git a/skimage/transform/_warps.py b/skimage/transform/_warps.py index 2fcfd80d..640a451d 100644 --- a/skimage/transform/_warps.py +++ b/skimage/transform/_warps.py @@ -287,7 +287,7 @@ def swirl(image, center=None, strength=1, radius=100, rotation=0, order=order, mode=mode, cval=cval) -def downsample(array, factors, mode='sum'): +def _downsample(array, factors, mode='sum'): """Performs downsampling with integer factors. Parameters @@ -338,9 +338,9 @@ def downsample(array, factors, mode='sum'): block_shape = out.shape if mode == 'sum': - for i in range(len(block_shape)//2): + for i in range(len(block_shape) // 2): out = out.sum(-1) else: - for i in range(len(block_shape)//2): + for i in range(len(block_shape) // 2): out = out.mean(-1) return out From 6f9d7c5d1af916f6d4d96d6e8debe891ccfdbe83 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Thu, 4 Jul 2013 16:56:37 +0800 Subject: [PATCH 149/736] Cleaning up downsampling for integer factors --- skimage/measure/__init__.py | 4 +- skimage/measure/_sum_blocks.py | 34 ++++++++++++++ skimage/measure/tests/test_sum_blocks.py | 16 +++++++ skimage/transform/__init__.py | 1 - skimage/transform/_warps.py | 56 +++++++++++++++++------- skimage/transform/tests/test_warps.py | 22 +++------- 6 files changed, 97 insertions(+), 36 deletions(-) create mode 100644 skimage/measure/_sum_blocks.py create mode 100644 skimage/measure/tests/test_sum_blocks.py diff --git a/skimage/measure/__init__.py b/skimage/measure/__init__.py index 89f707c1..a9fbae24 100755 --- a/skimage/measure/__init__.py +++ b/skimage/measure/__init__.py @@ -3,6 +3,7 @@ from ._regionprops import regionprops, perimeter from ._structural_similarity import structural_similarity from ._polygon import approximate_polygon, subdivide_polygon from .fit import LineModel, CircleModel, EllipseModel, ransac +from ._sum_blocks import sum_blocks __all__ = ['find_contours', @@ -14,4 +15,5 @@ __all__ = ['find_contours', 'LineModel', 'CircleModel', 'EllipseModel', - 'ransac'] + 'ransac', + 'sum_blocks'] diff --git a/skimage/measure/_sum_blocks.py b/skimage/measure/_sum_blocks.py new file mode 100644 index 00000000..e6a478e5 --- /dev/null +++ b/skimage/measure/_sum_blocks.py @@ -0,0 +1,34 @@ +from ..transform._warps import _downsample + + +def sum_blocks(array, factors): + """Sums the elements in blocks of integer factors and pads the original + array with zeroes if the dimensions are not perfectly divisible by factors. + + Parameters + ---------- + array : ndarray + Input n-dimensional array. + factors: tuple + Tuple containing integer values representing block length along each + axis. + + Returns + ------- + array : ndarray + Downsampled array with same number of dimensions as that of input + array. + + Example + ------- + >>> a = np.arange(15).reshape(3, 5) + >>> a + array([[ 0, 1, 2, 3, 4], + [ 5, 6, 7, 8, 9], + [10, 11, 12, 13, 14]]) + >>> sum_blocks(a, (2,3)) + array([[21, 24], + [33, 27]]) + + """ + return _downsample(array, factors) diff --git a/skimage/measure/tests/test_sum_blocks.py b/skimage/measure/tests/test_sum_blocks.py new file mode 100644 index 00000000..b4910495 --- /dev/null +++ b/skimage/measure/tests/test_sum_blocks.py @@ -0,0 +1,16 @@ +import numpy as np +from numpy.testing import assert_array_equal +from skimage.measure._sum_blocks import sum_blocks + +def test_downsample_sum_blocks(): + """Verifying downsampling of an array with expected result in sum mode""" + image1 = np.arange(4*6).reshape(4, 6) + out1 = sum_blocks(image1, (2, 3)) + expected1 = np.array([[ 24, 42], + [ 96, 114]]) + assert_array_equal(expected1, out1) + image2 = np.arange(5*8).reshape(5, 8) + out2 = sum_blocks(image2, (3, 3)) + expected2 = np.array([[ 81, 108, 87], + [174, 192, 138]]) + assert_array_equal(expected2, out2) diff --git a/skimage/transform/__init__.py b/skimage/transform/__init__.py index dcd39c31..311801be 100644 --- a/skimage/transform/__init__.py +++ b/skimage/transform/__init__.py @@ -9,7 +9,6 @@ from ._geometric import (warp, warp_coords, estimate_transform, SimilarityTransform, AffineTransform, ProjectiveTransform, PolynomialTransform, PiecewiseAffineTransform) - from ._warps import swirl, resize, rotate, rescale, downscale_local_means from .pyramids import (pyramid_reduce, pyramid_expand, pyramid_gaussian, pyramid_laplacian) diff --git a/skimage/transform/_warps.py b/skimage/transform/_warps.py index 640a451d..cca5d1d9 100644 --- a/skimage/transform/_warps.py +++ b/skimage/transform/_warps.py @@ -287,7 +287,7 @@ def swirl(image, center=None, strength=1, radius=100, rotation=0, order=order, mode=mode, cval=cval) -def _downsample(array, factors, mode='sum'): +def _downsample(array, factors, sum=True): """Performs downsampling with integer factors. Parameters @@ -296,10 +296,9 @@ def _downsample(array, factors, mode='sum'): Input n-dimensional array. factors: tuple Tuple containing downsampling factor along each axis. - mode : string - Decides whether the downsampled element is the sum or mean - of its corresponding constituent elements in the input array. Default - is 'sum'. + sum : bool + If True, downsampled element is the sum of its corresponding + constituent elements in the input array. Default is True. Returns ------- @@ -307,17 +306,6 @@ def _downsample(array, factors, mode='sum'): Downsampled array with same number of dimensions as that of input array. - Example - ------- - >>> a = np.arange(15).reshape(3, 5) - >>> a - array([[ 0, 1, 2, 3, 4], - [ 5, 6, 7, 8, 9], - [10, 11, 12, 13, 14]]) - >>> downsample(a, (2,3)) - array([[21, 24], - [33, 27]]) - """ pad_size = [] @@ -337,10 +325,44 @@ def _downsample(array, factors, mode='sum'): out = view_as_blocks(array, factors) block_shape = out.shape - if mode == 'sum': + if sum: for i in range(len(block_shape) // 2): out = out.sum(-1) else: for i in range(len(block_shape) // 2): out = out.mean(-1) return out + + +def downscale_local_means(array, factors): + """Downsamples the array in blocks of input integer factors after padding + the original array with zeroes if the dimensions are not perfectly + divisible by factors and replaces it with mean i.e. average value. + + Parameters + ---------- + array : ndarray + Input n-dimensional array. + factors: tuple + Tuple containing integer values representing block length along each + axis. + + Returns + ------- + array : ndarray + Downsampled array with same number of dimensions as that of input + array. + + Example + ------- + >>> a = np.arange(15).reshape(3, 5) + >>> a + array([[ 0, 1, 2, 3, 4], + [ 5, 6, 7, 8, 9], + [10, 11, 12, 13, 14]]) + >>> downscale_local_means(a, (2,3)) + array([[3.5, 4.], + [5.5, 4.5]]) + + """ + return _downsample(array, factors, False) diff --git a/skimage/transform/tests/test_warps.py b/skimage/transform/tests/test_warps.py index a2cd2b05..88ede530 100644 --- a/skimage/transform/tests/test_warps.py +++ b/skimage/transform/tests/test_warps.py @@ -5,7 +5,8 @@ from scipy.ndimage import map_coordinates from skimage.transform import (warp, warp_coords, rotate, resize, rescale, AffineTransform, ProjectiveTransform, - SimilarityTransform) + SimilarityTransform, + downscale_local_means) from skimage import transform as tf, data, img_as_float from skimage.color import rgb2gray @@ -193,29 +194,16 @@ def test_warp_coords_example(): coords = warp_coords(tform, (30, 30, 3)) map_coordinates(image[:, :, 0], coords[:2]) -def test_downsample_sum(): - """Verifying downsampling of an array with expected result in sum mode""" - image1 = np.arange(4*6).reshape(4, 6) - out1 = tf.downsample(image1, (2, 3)) - expected1 = np.array([[ 24, 42], - [ 96, 114]]) - assert_array_equal(expected1, out1) - image2 = np.arange(5*8).reshape(5, 8) - out2 = tf.downsample(image2, (3, 3)) - expected2 = np.array([[ 81, 108, 87], - [174, 192, 138]]) - assert_array_equal(expected2, out2) - -def test_downsample_mean(): +def test_downscale_local_means(): """Verifying downsampling of an array with expected result in mean mode""" image1 = np.arange(4*6).reshape(4, 6) - out1 = tf.downsample(image1, (2, 3), 'mean') + out1 = downscale_local_means(image1, (2, 3)) expected1 = np.array([[ 4., 7.], [ 16., 19.]]) assert_array_equal(expected1, out1) image2 = np.arange(5*8).reshape(5, 8) - out2 = tf.downsample(image2, (4, 5), 'mean') + out2 = downscale_local_means(image2, (4, 5)) expected2 = np.array([[ 14. , 10.8], [ 8.5, 5.7]]) assert_array_equal(expected2, out2) From 8ac4427d9b6996079dbb213c030c775e85df75a2 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Fri, 5 Jul 2013 00:21:11 +0800 Subject: [PATCH 150/736] Expanding the documentation in transform._warps --- skimage/measure/_sum_blocks.py | 6 ++++++ skimage/transform/_warps.py | 17 +++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/skimage/measure/_sum_blocks.py b/skimage/measure/_sum_blocks.py index e6a478e5..b349f3e6 100644 --- a/skimage/measure/_sum_blocks.py +++ b/skimage/measure/_sum_blocks.py @@ -5,6 +5,12 @@ def sum_blocks(array, factors): """Sums the elements in blocks of integer factors and pads the original array with zeroes if the dimensions are not perfectly divisible by factors. + This function is different from resize and rescale in transform._warps in + the sense that they use interpolation to upsample or downsample on a 2D + array, while this function performs only dawnsampling but on any + n-dimensional array and returns the sum of elements in a block of size + factors in the original array. + Parameters ---------- array : ndarray diff --git a/skimage/transform/_warps.py b/skimage/transform/_warps.py index cca5d1d9..487ed760 100644 --- a/skimage/transform/_warps.py +++ b/skimage/transform/_warps.py @@ -8,6 +8,12 @@ from skimage.util.shape import view_as_blocks, _pad_asymmetric_zeros def resize(image, output_shape, order=1, mode='constant', cval=0.): """Resize image to match a certain size. + Resize performs interpolation to upsample or downsample 2D arrays. For + downsampling any n-dimensional array by performing arithmetic sum or + arithmetic mean, see meassure._sum_blocks.sum_blocks and + transform._warps.downscale_local_means respectively. + + Parameters ---------- image : ndarray @@ -89,6 +95,11 @@ def resize(image, output_shape, order=1, mode='constant', cval=0.): def rescale(image, scale, order=1, mode='constant', cval=0.): """Scale image by a certain factor. + Rescale performs interpolation to upsample or downsample 2D arrays. For + downsampling any n-dimensional array by performing arithmetic sum or + arithmetic mean, see meassure._sum_blocks.sum_blocks and + transform._warps.downscale_local_means respectively. + Parameters ---------- image : ndarray @@ -339,6 +350,12 @@ def downscale_local_means(array, factors): the original array with zeroes if the dimensions are not perfectly divisible by factors and replaces it with mean i.e. average value. + This function is different from resize and rescale in the sense that they + use interpolation to upsample or downsample on a 2D array, while this + function performs only dawnsampling but on any n-dimensional array and + returns the arithmetic mean of elements in a block of size factors in the + original array. + Parameters ---------- array : ndarray From f79582ecb476e660c9fbcb846ac0deedb90f2313 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Fri, 5 Jul 2013 01:34:17 +0800 Subject: [PATCH 151/736] Attempt to solve circular import issue --- skimage/measure/_sum_blocks.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/skimage/measure/_sum_blocks.py b/skimage/measure/_sum_blocks.py index b349f3e6..bfba0089 100644 --- a/skimage/measure/_sum_blocks.py +++ b/skimage/measure/_sum_blocks.py @@ -1,9 +1,6 @@ -from ..transform._warps import _downsample - - def sum_blocks(array, factors): - """Sums the elements in blocks of integer factors and pads the original - array with zeroes if the dimensions are not perfectly divisible by factors. + """Sums the elements in blocks of integer factors and pads the original + array with zeroes if the dimensions are not perfectly divisible by factors. This function is different from resize and rescale in transform._warps in the sense that they use interpolation to upsample or downsample on a 2D @@ -37,4 +34,6 @@ def sum_blocks(array, factors): [33, 27]]) """ - return _downsample(array, factors) + return _downsample(array, factors) + +from ..transform._warps import _downsample From 70d180408f257bcbe08d2c95fd0729bc09644441 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Fri, 5 Jul 2013 10:46:45 +0800 Subject: [PATCH 152/736] Another attempt at solving circular import issue --- skimage/measure/_sum_blocks.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/skimage/measure/_sum_blocks.py b/skimage/measure/_sum_blocks.py index bfba0089..c65c121e 100644 --- a/skimage/measure/_sum_blocks.py +++ b/skimage/measure/_sum_blocks.py @@ -34,6 +34,5 @@ def sum_blocks(array, factors): [33, 27]]) """ + from ..transform._warps import _downsample return _downsample(array, factors) - -from ..transform._warps import _downsample From 971e7beec5a906c22f11469cb1afc0e2c2d322cc Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Fri, 5 Jul 2013 11:10:12 +0800 Subject: [PATCH 153/736] Correcting typos --- skimage/transform/_warps.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skimage/transform/_warps.py b/skimage/transform/_warps.py index 487ed760..c2727c74 100644 --- a/skimage/transform/_warps.py +++ b/skimage/transform/_warps.py @@ -10,7 +10,7 @@ def resize(image, output_shape, order=1, mode='constant', cval=0.): Resize performs interpolation to upsample or downsample 2D arrays. For downsampling any n-dimensional array by performing arithmetic sum or - arithmetic mean, see meassure._sum_blocks.sum_blocks and + arithmetic mean, see measure._sum_blocks.sum_blocks and transform._warps.downscale_local_means respectively. @@ -97,7 +97,7 @@ def rescale(image, scale, order=1, mode='constant', cval=0.): Rescale performs interpolation to upsample or downsample 2D arrays. For downsampling any n-dimensional array by performing arithmetic sum or - arithmetic mean, see meassure._sum_blocks.sum_blocks and + arithmetic mean, see measure._sum_blocks.sum_blocks and transform._warps.downscale_local_means respectively. Parameters From 9484afeed17531dbfb747776d05df6c662e5d366 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Sun, 2 Jun 2013 21:05:53 +0200 Subject: [PATCH 154/736] Add SART tomography reconstruction to radon_transform. --- skimage/transform/_radon_transform.pyx | 170 +++++++++++++++++++++++++ skimage/transform/radon_transform.py | 91 ++++++++++++- skimage/transform/setup.py | 5 + 3 files changed, 265 insertions(+), 1 deletion(-) create mode 100644 skimage/transform/_radon_transform.pyx diff --git a/skimage/transform/_radon_transform.pyx b/skimage/transform/_radon_transform.pyx new file mode 100644 index 00000000..b1bcc300 --- /dev/null +++ b/skimage/transform/_radon_transform.pyx @@ -0,0 +1,170 @@ +#cython: cdivision=True +#cython: boundscheck=True +#cython: nonecheck=True +#cython: wraparound=False +import numpy as np +from numpy import pi + +cimport numpy as cnp +cimport cython +from libc.math cimport cos, sin, floor, ceil, sqrt, abs + + +cpdef bilinear_ray_sum(cnp.ndarray[cnp.double_t, ndim=2] image, double theta, + double ray_position): + '''Compute the projection of an image along a ray. + + Parameters + ---------- + image : 2D array, dtype=float + Image to project. + :param theta: Angle of the projection. + :param ray_position: Position of the ray within the projection + + Returns + ------- + projected_value : float + Ray sum along the projection + norm_of_weights : + A measure of how long the ray's path through the reconstruction + circle was + ''' + theta = theta / 180. * pi + cdef double radius = image.shape[0] // 2 - 1 + cdef double projection_center = image.shape[0] // 2 - 1 + cdef double rotation_center = image.shape[0] // 2 + # (s, t) is the (x, y) system rotated by theta + cdef double t = ray_position - projection_center + # s0 is the half-length of the ray's path in the reconstruction circle + cdef double s0 + s0 = sqrt(radius**2 - t**2) if radius**2 >= t**2 else 0. + cdef Py_ssize_t Ns = 2 * int(ceil(2 * s0)) # number of steps along the ray + cdef double ray_sum = 0. + cdef double weight_norm = 0. + cdef double ds, dx, dy, x0, y0, x, y, di, dj, index_i, index_j + cdef Py_ssize_t k, i, j + if Ns > 0: + # step length between samples + ds = 2 * s0 / Ns + dx = ds * cos(theta) + dy = ds * sin(theta) + # point of entry of the ray into the reconstruction circle + x0 = -s0 * cos(theta) + t * sin(theta) + y0 = -s0 * sin(theta) - t * cos(theta) + for k in range(Ns+1): + x = x0 + k * dx + y = y0 + k * dy + index_i = x + rotation_center + index_j = y + rotation_center + i = floor(index_i) + j = floor(index_j) + di = index_i - floor(index_i) + dj = index_j - floor(index_j) + # Use linear interpolation between values + # Where values fall outside the array, assume zero + if i > 0 and j > 0: + ray_sum += (1. - di) * (1. - dj) * image[i, j] * ds + weight_norm += ((1 - di) * (1 - dj) * ds)**2 + if i > 0 and j < image.shape[1] - 1: + ray_sum += (1. - di) * dj * image[i, j+1] * ds + weight_norm += ((1 - di) * dj * ds)**2 + if i < image.shape[0] - 1 and j > 0: + ray_sum += di * (1 - dj) * image[i+1, j] * ds + weight_norm += (di * (1 - dj) * ds)**2 + if i < image.shape[0] - 1 and j < image.shape[1] - 1: + ray_sum += di * dj * image[i+1, j+1] * ds + weight_norm += (di * dj * ds)**2 + return ray_sum, weight_norm + + +cpdef bilinear_ray_update(cnp.ndarray[cnp.double_t, ndim=2] image, + cnp.ndarray[cnp.double_t, ndim=2] image_update, + double theta, double ray_position, double projected_value): + """Compute the update along a ray using bilinear interpolation. + + Parameters + ---------- + image : + Current reconstruction estimate + image_update : + Array of same shape as ``image``. Updates will be added to this array. + theta : + Angle of the projection + ray_position : + Position of the ray within the projection + projected_value : + Projected value (from the sinogram) + + Returns + ------- + deviation : + Deviation before updating the image + """ + cdef double ray_sum, weight_norm, deviation + ray_sum, weight_norm = bilinear_ray_sum(image, theta, ray_position) + if weight_norm > 0.: + deviation = -(ray_sum - projected_value) / weight_norm + else: + deviation = 0. + theta = theta / 180. * pi + cdef double radius = image.shape[0] // 2 - 1 + cdef double projection_center = image.shape[0] // 2 - 1 + cdef double rotation_center = image.shape[0] // 2 + # (s, t) is the (x, y) system rotated by theta + cdef double t = ray_position - projection_center + # s0 is the half-length of the ray's path in the reconstruction circle + cdef double s0 + s0 = sqrt(radius*radius - t*t) if radius**2 >= t**2 else 0. + cdef unsigned int Ns = 2 * int(ceil(2 * s0)) + cdef double hamming_beta = 0.46164 + + cdef double ds, dx, dy, x0, y0, x, y, di, dj, index_i, index_j + cdef double hamming_window + cdef unsigned int k, i, j + if Ns > 0: + # Step length between samples + ds = 2 * s0 / Ns + dx = ds * cos(theta) + dy = ds * sin(theta) + # Point of entry of the ray into the reconstruction circle + x0 = -s0 * cos(theta) + t * sin(theta) + y0 = -s0 * sin(theta) - t * cos(theta) + for k in range(Ns+1): + x = x0 + k * dx + y = y0 + k * dy + index_i = x + rotation_center + index_j = y + rotation_center + i = floor(index_i) + j = floor(index_j) + di = index_i - floor(index_i) + dj = index_j - floor(index_j) + hamming_window = ((1 - hamming_beta) + - hamming_beta * cos(2*pi*k / (Ns - 1))) + if i > 0 and j > 0: + image_update[i, j] += (deviation * (1. - di) * (1. - dj) + * ds * hamming_window) + if i > 0 and j < image.shape[1] - 1: + image_update[i, j+1] += (deviation * (1. - di) * dj + * ds * hamming_window) + if i < image.shape[0] - 1 and j > 0: + image_update[i+1, j] += (deviation * di * (1 - dj) + * ds * hamming_window) + if i < image.shape[0] - 1 and j < image.shape[1] - 1: + image_update[i+1, j+1] += (deviation * di * dj + * ds * hamming_window) + return deviation + + +def sart_projection_update(cnp.ndarray[cnp.double_t, ndim=2] image, \ + double theta, \ + cnp.ndarray[cnp.double_t, ndim=1] projection): + cdef cnp.ndarray[cnp.double_t, ndim=2] image_update = np.zeros_like(image) + cdef unsigned int ray_position + cdef Py_ssize_t i + for i in range(projection.shape[0]): + # TODO: + # ip may differ from i in the future (for alignment of projections) + ray_position = i + bilinear_ray_update(image, image_update, theta, ray_position, + projection[i]) + return image_update diff --git a/skimage/transform/radon_transform.py b/skimage/transform/radon_transform.py index b114adad..55bd1d59 100644 --- a/skimage/transform/radon_transform.py +++ b/skimage/transform/radon_transform.py @@ -16,8 +16,9 @@ from __future__ import division import numpy as np from scipy.fftpack import fftshift, fft, ifft from ._warps_cy import _warp_fast +from ._radon_transform import sart_projection_update -__all__ = ["radon", "iradon"] +__all__ = ["radon", "iradon", "iradon_sart"] def radon(image, theta=None, circle=False): @@ -254,3 +255,91 @@ def iradon(radon_image, theta=None, output_size=None, raise ValueError("Unknown interpolation: %s" % interpolation) return reconstructed * np.pi / (2 * len(th)) + + +def _sart_next_angle(remaining, used): + used = np.array(used) + used.shape = (-1, 1) + remaining = np.array(remaining) + remaining.shape = (1, -1) + time = np.arange(used.shape[0]) + 1 + time.shape = (-1, 1) + tau = 3. + difference = used - remaining + distance = np.minimum(abs(difference % 180), abs(difference % -180)) + #print distance + cost = np.exp(-distance * time / tau).sum(axis=0).squeeze() + next_angle_index = np.argmin(cost) + return remaining[0, next_angle_index] + + +def iradon_sart(radon_image, theta=None, image=None, + clip=None, relaxation=0.15): + """ + Inverse radon transform + + Reconstruct an image from the radon transform, using a single iteration of + the Simultaneous Algebraic Reconstruction Technique (SART) algorithm. + + Parameters + ---------- + radon_image : array_like, dtype=float + Image containing radon transform (sinogram). Each column of + the image corresponds to a projection along a different angle. + theta : array_like, dtype=float, optional + Reconstruction angles (in degrees). Default: m angles evenly spaced + between 0 and 180 (if the shape of `radon_image` is (N, M)). + image : array_like, dtype=float, optional + Image containing an initial reconstruction estimate. Shape of this + array should be ``(radon_image.shape[0], radon_image.shape[0])``. The + default is an array of zeros. + + Returns + ------- + output : ndarray + Reconstructed image. + + Notes + ----- + Algebraic Reconstruction Techniques are based on formulating the tomography + reconstruction problem as a set of linear equations. Along each ray, + the projected value is the sum of all the values of the cross section along + the ray. A typical feature of SART (and a few other variants of algebraic + techniques) is that it samples the cross section at equidistant points + along the ray, using linear interpolation between the pixel values of the + cross section. The resulting set of linear equations are then solved using + a slightly modified Kaczmarz method. + + When using SART, a single iteration is usually sufficient to obtain a good + reconstruction. Further iterations will tend to enhance high-frequency + information, but will also often increase the noise. + + References: + -A. C. Kak, Malcolm Slaney, "Principles of Computerized Tomographic + Imaging", IEEE Press 1988. + -AH Andersen, AC Kak, "Simultaneous algebraic reconstruction technique + (SART): a superior implementation of the ART algorithm", Ultrasonic + Imaging 6 pp 81--94 (1984) + """ + if theta is None: + theta = np.linspace(0, 180, radon_image.shape[1], endpoint=False) + angle_indices = {theta[i]: i for i in range(theta.shape[0])} + reconstructed_shape = (radon_image.shape[0], radon_image.shape[0]) + if image is None: + image = np.zeros(reconstructed_shape, dtype=np.float) + elif image.shape != reconstructed_shape: + raise ValueError('Shape of image (%s) does not match first dimension ' + 'of radon_image (%s)' + % (image.shape, reconstructed_shape)) + used_angles = [] + while angle_indices: + angle_index = angle_indices.pop(_sart_next_angle(angle_indices.keys(), + used_angles)) + image_update = sart_projection_update(image, theta[angle_index], + radon_image[:, angle_index]) + image += relaxation * image_update + if not clip is None: + image = clip(image, clip[0], clip[1]) + used_angles.append(theta[angle_index]) + + return image diff --git a/skimage/transform/setup.py b/skimage/transform/setup.py index b0093d87..22f31696 100644 --- a/skimage/transform/setup.py +++ b/skimage/transform/setup.py @@ -15,6 +15,7 @@ def configuration(parent_package='', top_path=None): cython(['_hough_transform.pyx'], working_path=base_path) cython(['_warps_cy.pyx'], working_path=base_path) + cython(['_radon_transform.pyx'], working_path=base_path) config.add_extension('_hough_transform', sources=['_hough_transform.c'], include_dirs=[get_numpy_include_dirs()]) @@ -22,6 +23,10 @@ def configuration(parent_package='', top_path=None): config.add_extension('_warps_cy', sources=['_warps_cy.c'], include_dirs=[get_numpy_include_dirs(), '../_shared']) + config.add_extension('_radon_transform', + sources=['_radon_transform.c'], + include_dirs=[get_numpy_include_dirs()]) + return config if __name__ == '__main__': From d4b33059cb15af905252da2edb01db5bf7177e1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Sun, 2 Jun 2013 21:07:58 +0200 Subject: [PATCH 155/736] Make iradon_sart visible in the transform package. --- skimage/transform/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/skimage/transform/__init__.py b/skimage/transform/__init__.py index f079ada4..2cbc7007 100644 --- a/skimage/transform/__init__.py +++ b/skimage/transform/__init__.py @@ -2,7 +2,7 @@ from ._hough_transform import (hough_circle, hough_ellipse, hough_line, probabilistic_hough_line) from .hough_transform import (hough, probabilistic_hough, hough_peaks, hough_line_peaks) -from .radon_transform import radon, iradon +from .radon_transform import radon, iradon, iradon_sart from .finite_radon_transform import frt2, ifrt2 from .integral import integral_image, integrate from ._geometric import (warp, warp_coords, estimate_transform, @@ -24,6 +24,7 @@ __all__ = ['hough_circle', 'hough_line_peaks', 'radon', 'iradon', + 'iradon_sart', 'frt2', 'ifrt2', 'integral_image', From 8e6468eef5df2bf3ef79f030ceb2e78a5946cc51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Sun, 9 Jun 2013 15:01:48 +0200 Subject: [PATCH 156/736] Add tests for transform.iradon_sart. --- .../transform/tests/test_radon_transform.py | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/skimage/transform/tests/test_radon_transform.py b/skimage/transform/tests/test_radon_transform.py index f7127aa5..fd0d47a3 100644 --- a/skimage/transform/tests/test_radon_transform.py +++ b/skimage/transform/tests/test_radon_transform.py @@ -296,6 +296,41 @@ def test_radon_iradon_circle(): yield check_radon_iradon_circle, interpolation, shape, output_size +def test_iradon_sart(): + from skimage.io import imread + from skimage import data_dir + from skimage.transform import rescale, radon, iradon_sart + + debug = False + + shepp_logan = imread(data_dir + "/phantom.png", as_grey=True) + image = rescale(shepp_logan, scale=0.4) + theta = np.linspace(0., 180., image.shape[0], endpoint=False) + sinogram = radon(image, theta, circle=True) + reconstructed = iradon_sart(sinogram, theta) + + if debug: + from matplotlib import pyplot as plt + plt.figure() + plt.subplot(221) + plt.imshow(image, interpolation='nearest') + plt.subplot(222) + plt.imshow(sinogram, interpolation='nearest') + plt.subplot(223) + plt.imshow(reconstructed, interpolation='nearest') + plt.subplot(224) + plt.imshow(reconstructed - image, interpolation='nearest') + plt.show() + + delta = np.mean(np.abs(reconstructed - image)) + print('delta (1 iteration) =', delta) + assert delta < 0.025 + reconstructed = iradon_sart(sinogram, theta, reconstructed) + delta = np.mean(np.abs(reconstructed - image)) + print('delta (2 iterations) =', delta) + assert delta < 0.015 + + if __name__ == "__main__": from numpy.testing import run_module_suite run_module_suite() From 1e1dd180d2252a448d1771d0f06dca8a1beea228 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Sun, 9 Jun 2013 15:32:25 +0200 Subject: [PATCH 157/736] Implement projection shifts. --- skimage/transform/_radon_transform.pyx | 9 ++++----- skimage/transform/radon_transform.py | 11 +++++++++-- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/skimage/transform/_radon_transform.pyx b/skimage/transform/_radon_transform.pyx index b1bcc300..e198c4a5 100644 --- a/skimage/transform/_radon_transform.pyx +++ b/skimage/transform/_radon_transform.pyx @@ -157,14 +157,13 @@ cpdef bilinear_ray_update(cnp.ndarray[cnp.double_t, ndim=2] image, def sart_projection_update(cnp.ndarray[cnp.double_t, ndim=2] image, \ double theta, \ - cnp.ndarray[cnp.double_t, ndim=1] projection): + cnp.ndarray[cnp.double_t, ndim=1] projection, + double projection_shift=0.): cdef cnp.ndarray[cnp.double_t, ndim=2] image_update = np.zeros_like(image) - cdef unsigned int ray_position + cdef double ray_position cdef Py_ssize_t i for i in range(projection.shape[0]): - # TODO: - # ip may differ from i in the future (for alignment of projections) - ray_position = i + ray_position = i + projection_shift bilinear_ray_update(image, image_update, theta, ray_position, projection[i]) return image_update diff --git a/skimage/transform/radon_transform.py b/skimage/transform/radon_transform.py index 55bd1d59..cc1b76c9 100644 --- a/skimage/transform/radon_transform.py +++ b/skimage/transform/radon_transform.py @@ -273,7 +273,7 @@ def _sart_next_angle(remaining, used): return remaining[0, next_angle_index] -def iradon_sart(radon_image, theta=None, image=None, +def iradon_sart(radon_image, theta=None, image=None, projection_shifts=None, clip=None, relaxation=0.15): """ Inverse radon transform @@ -293,6 +293,10 @@ def iradon_sart(radon_image, theta=None, image=None, Image containing an initial reconstruction estimate. Shape of this array should be ``(radon_image.shape[0], radon_image.shape[0])``. The default is an array of zeros. + projection_shifts : 1D array, dtype=float + Shift the projections contained in ``radon_image`` (the sinogram) by + this many pixels before reconstructing the image. The i'th value + defines the shift of the i'th column of ``radon_image``. Returns ------- @@ -327,6 +331,8 @@ def iradon_sart(radon_image, theta=None, image=None, reconstructed_shape = (radon_image.shape[0], radon_image.shape[0]) if image is None: image = np.zeros(reconstructed_shape, dtype=np.float) + if projection_shifts is None: + projection_shifts = np.zeros((radon_image.shape[1],), dtype=np.float) elif image.shape != reconstructed_shape: raise ValueError('Shape of image (%s) does not match first dimension ' 'of radon_image (%s)' @@ -336,7 +342,8 @@ def iradon_sart(radon_image, theta=None, image=None, angle_index = angle_indices.pop(_sart_next_angle(angle_indices.keys(), used_angles)) image_update = sart_projection_update(image, theta[angle_index], - radon_image[:, angle_index]) + radon_image[:, angle_index], + projection_shifts[angle_index]) image += relaxation * image_update if not clip is None: image = clip(image, clip[0], clip[1]) From d5f323eec037194111a0f2ef9788347a1093e9ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Sun, 9 Jun 2013 17:06:39 +0200 Subject: [PATCH 158/736] Tests for the projection_shifts functionality of iradon_sart. --- .../transform/tests/test_radon_transform.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/skimage/transform/tests/test_radon_transform.py b/skimage/transform/tests/test_radon_transform.py index fd0d47a3..650d6961 100644 --- a/skimage/transform/tests/test_radon_transform.py +++ b/skimage/transform/tests/test_radon_transform.py @@ -330,6 +330,29 @@ def test_iradon_sart(): print('delta (2 iterations) =', delta) assert delta < 0.015 + np.random.seed(1239867) + shifts = np.random.uniform(-3, 3, sinogram.shape[1]) + x = np.arange(sinogram.shape[0]) + sinogram_shifted = np.vstack(np.interp(x + shifts[i], x, sinogram[:, i]) + for i in range(sinogram.shape[1])).T + reconstructed = iradon_sart(sinogram_shifted, theta, + projection_shifts=shifts) + if debug: + from matplotlib import pyplot as plt + plt.figure() + plt.subplot(221) + plt.imshow(image, interpolation='nearest') + plt.subplot(222) + plt.imshow(sinogram_shifted, interpolation='nearest') + plt.subplot(223) + plt.imshow(reconstructed, interpolation='nearest') + plt.subplot(224) + plt.imshow(reconstructed - image, interpolation='nearest') + plt.show() + + delta = np.mean(np.abs(reconstructed - image)) + print('delta (1 iteration, shifted sinogram) =', delta) + assert delta < 0.025 if __name__ == "__main__": from numpy.testing import run_module_suite From b474804654cae9652931eac0b5f94fab4828936c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Sun, 9 Jun 2013 15:33:08 +0200 Subject: [PATCH 159/736] Fix docstrings for transform.iradon_sart with subroutines. --- skimage/transform/_radon_transform.pyx | 46 ++++++++++++++++++++------ skimage/transform/radon_transform.py | 13 ++++++-- 2 files changed, 46 insertions(+), 13 deletions(-) diff --git a/skimage/transform/_radon_transform.pyx b/skimage/transform/_radon_transform.pyx index e198c4a5..65cf1023 100644 --- a/skimage/transform/_radon_transform.pyx +++ b/skimage/transform/_radon_transform.pyx @@ -12,14 +12,17 @@ from libc.math cimport cos, sin, floor, ceil, sqrt, abs cpdef bilinear_ray_sum(cnp.ndarray[cnp.double_t, ndim=2] image, double theta, double ray_position): - '''Compute the projection of an image along a ray. + """ + Compute the projection of an image along a ray. Parameters ---------- image : 2D array, dtype=float Image to project. - :param theta: Angle of the projection. - :param ray_position: Position of the ray within the projection + theta : float + Angle of the projection + ray_position : float + Position of the ray within the projection Returns ------- @@ -28,7 +31,7 @@ cpdef bilinear_ray_sum(cnp.ndarray[cnp.double_t, ndim=2] image, double theta, norm_of_weights : A measure of how long the ray's path through the reconstruction circle was - ''' + """ theta = theta / 180. * pi cdef double radius = image.shape[0] // 2 - 1 cdef double projection_center = image.shape[0] // 2 - 1 @@ -80,19 +83,20 @@ cpdef bilinear_ray_sum(cnp.ndarray[cnp.double_t, ndim=2] image, double theta, cpdef bilinear_ray_update(cnp.ndarray[cnp.double_t, ndim=2] image, cnp.ndarray[cnp.double_t, ndim=2] image_update, double theta, double ray_position, double projected_value): - """Compute the update along a ray using bilinear interpolation. + """ + Compute the update along a ray using bilinear interpolation. Parameters ---------- - image : + image : 2D array, dtype=float Current reconstruction estimate - image_update : + image_update : 2D array, dtype=float Array of same shape as ``image``. Updates will be added to this array. - theta : + theta : float Angle of the projection - ray_position : + ray_position : float Position of the ray within the projection - projected_value : + projected_value : float Projected value (from the sinogram) Returns @@ -159,6 +163,28 @@ def sart_projection_update(cnp.ndarray[cnp.double_t, ndim=2] image, \ double theta, \ cnp.ndarray[cnp.double_t, ndim=1] projection, double projection_shift=0.): + """ + Compute update to a reconstruction estimate from a single projection + using bilinear interpolation. + + Parameters + ---------- + image : 2D array, dtype=float + Current reconstruction estimate + theta : float + Angle of the projection + projection : 1D array, dtype=float + Projected values, taken from the sinogram + projection_shift : float + Shift the position of the projection by this many pixels before + using it to compute an update to the reconstruction estimate + + Returns + ------- + image_update : 2D array, dtype=float + Array of same shape as ``image`` containing updates that should be + added to ``image`` to improve the reconstruction estimate + """ cdef cnp.ndarray[cnp.double_t, ndim=2] image_update = np.zeros_like(image) cdef double ray_position cdef Py_ssize_t i diff --git a/skimage/transform/radon_transform.py b/skimage/transform/radon_transform.py index cc1b76c9..042d37e7 100644 --- a/skimage/transform/radon_transform.py +++ b/skimage/transform/radon_transform.py @@ -283,13 +283,13 @@ def iradon_sart(radon_image, theta=None, image=None, projection_shifts=None, Parameters ---------- - radon_image : array_like, dtype=float + radon_image : 2D array, dtype=float Image containing radon transform (sinogram). Each column of the image corresponds to a projection along a different angle. - theta : array_like, dtype=float, optional + theta : 1D array, dtype=float, optional Reconstruction angles (in degrees). Default: m angles evenly spaced between 0 and 180 (if the shape of `radon_image` is (N, M)). - image : array_like, dtype=float, optional + image : 2D array, dtype=float, optional Image containing an initial reconstruction estimate. Shape of this array should be ``(radon_image.shape[0], radon_image.shape[0])``. The default is an array of zeros. @@ -297,6 +297,13 @@ def iradon_sart(radon_image, theta=None, image=None, projection_shifts=None, Shift the projections contained in ``radon_image`` (the sinogram) by this many pixels before reconstructing the image. The i'th value defines the shift of the i'th column of ``radon_image``. + clip : length-2 sequence of floats + Force all values in the reconstructed tomogram to lie in the range + ``[clip[0], clip[1]]`` + relaxation : float + Relaxation parameter for the update step. A higher value can + improve the convergence rate, but one runs the risk of instabilities. + Values close to or higher than 1 are not recommended. Returns ------- From 372f0127f97bc75abb9786e012b9f4495522cb80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Sun, 9 Jun 2013 18:21:16 +0200 Subject: [PATCH 160/736] transform.iradon_sart: Clean up code for ordering projections. --- skimage/transform/radon_transform.py | 46 +++++++++++++++------------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/skimage/transform/radon_transform.py b/skimage/transform/radon_transform.py index 042d37e7..445cd24c 100644 --- a/skimage/transform/radon_transform.py +++ b/skimage/transform/radon_transform.py @@ -257,20 +257,30 @@ def iradon(radon_image, theta=None, output_size=None, return reconstructed * np.pi / (2 * len(th)) -def _sart_next_angle(remaining, used): - used = np.array(used) - used.shape = (-1, 1) - remaining = np.array(remaining) - remaining.shape = (1, -1) - time = np.arange(used.shape[0]) + 1 - time.shape = (-1, 1) - tau = 3. - difference = used - remaining - distance = np.minimum(abs(difference % 180), abs(difference % -180)) - #print distance - cost = np.exp(-distance * time / tau).sum(axis=0).squeeze() - next_angle_index = np.argmin(cost) - return remaining[0, next_angle_index] +def _sart_order_angles(theta, tau=3.): + """ + Order angles to reduce the amount of correlated information + in subsequent projections, i.e. make sure subsequent angles + are as far away from each other mod 180 degrees as possible. + Indices into the ``theta`` array are yielded. + """ + used_indices = [0] + remaining_indices = range(1, len(theta)) + yield 0 + while remaining_indices: + used = np.array(theta[used_indices]) + used.shape = (-1, 1) + remaining = np.array(theta[remaining_indices]) + remaining.shape = (1, -1) + time = (np.arange(used.shape[0]) + 1)[::-1] + time.shape = (-1, 1) + difference = used - remaining + distance = np.minimum(abs(difference % 180), abs(difference % -180)) + cost = np.exp(-distance * time / tau).sum(axis=0).squeeze() + next_angle_remaining_index = np.argmin(cost) + next_angle_index = remaining_indices.pop(next_angle_remaining_index) + used_indices.append(next_angle_index) + yield next_angle_index def iradon_sart(radon_image, theta=None, image=None, projection_shifts=None, @@ -334,7 +344,6 @@ def iradon_sart(radon_image, theta=None, image=None, projection_shifts=None, """ if theta is None: theta = np.linspace(0, 180, radon_image.shape[1], endpoint=False) - angle_indices = {theta[i]: i for i in range(theta.shape[0])} reconstructed_shape = (radon_image.shape[0], radon_image.shape[0]) if image is None: image = np.zeros(reconstructed_shape, dtype=np.float) @@ -344,16 +353,11 @@ def iradon_sart(radon_image, theta=None, image=None, projection_shifts=None, raise ValueError('Shape of image (%s) does not match first dimension ' 'of radon_image (%s)' % (image.shape, reconstructed_shape)) - used_angles = [] - while angle_indices: - angle_index = angle_indices.pop(_sart_next_angle(angle_indices.keys(), - used_angles)) + for angle_index in _sart_order_angles(theta): image_update = sart_projection_update(image, theta[angle_index], radon_image[:, angle_index], projection_shifts[angle_index]) image += relaxation * image_update if not clip is None: image = clip(image, clip[0], clip[1]) - used_angles.append(theta[angle_index]) - return image From 496902145f88169f53ae40bec3f5ef5ffd6fe260 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Mon, 10 Jun 2013 09:38:28 +0200 Subject: [PATCH 161/736] iradon_sart: Add wikipedia reference to Kaczmarz method. --- skimage/transform/radon_transform.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/skimage/transform/radon_transform.py b/skimage/transform/radon_transform.py index 445cd24c..ebf06d35 100644 --- a/skimage/transform/radon_transform.py +++ b/skimage/transform/radon_transform.py @@ -341,6 +341,8 @@ def iradon_sart(radon_image, theta=None, image=None, projection_shifts=None, -AH Andersen, AC Kak, "Simultaneous algebraic reconstruction technique (SART): a superior implementation of the ART algorithm", Ultrasonic Imaging 6 pp 81--94 (1984) + -Kaczmarz' method, Wikipedia, + http://en.wikipedia.org/wiki/Kaczmarz_method """ if theta is None: theta = np.linspace(0, 180, radon_image.shape[1], endpoint=False) From 94eba1c9116eeb7cdf87447c98a8005141b4cd4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Mon, 10 Jun 2013 10:56:12 +0200 Subject: [PATCH 162/736] iradon_sart: Clarify how constants are chosen. --- skimage/transform/_radon_transform.pyx | 2 +- skimage/transform/radon_transform.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/skimage/transform/_radon_transform.pyx b/skimage/transform/_radon_transform.pyx index 65cf1023..321d9556 100644 --- a/skimage/transform/_radon_transform.pyx +++ b/skimage/transform/_radon_transform.pyx @@ -120,7 +120,7 @@ cpdef bilinear_ray_update(cnp.ndarray[cnp.double_t, ndim=2] image, cdef double s0 s0 = sqrt(radius*radius - t*t) if radius**2 >= t**2 else 0. cdef unsigned int Ns = 2 * int(ceil(2 * s0)) - cdef double hamming_beta = 0.46164 + cdef double hamming_beta = 0.46164 # beta for equiripple Hamming window cdef double ds, dx, dy, x0, y0, x, y, di, dj, index_i, index_j cdef double hamming_window diff --git a/skimage/transform/radon_transform.py b/skimage/transform/radon_transform.py index ebf06d35..bba73c3f 100644 --- a/skimage/transform/radon_transform.py +++ b/skimage/transform/radon_transform.py @@ -257,13 +257,14 @@ def iradon(radon_image, theta=None, output_size=None, return reconstructed * np.pi / (2 * len(th)) -def _sart_order_angles(theta, tau=3.): +def _sart_order_angles(theta): """ Order angles to reduce the amount of correlated information in subsequent projections, i.e. make sure subsequent angles are as far away from each other mod 180 degrees as possible. Indices into the ``theta`` array are yielded. """ + tau = 3. # time constant for correlations; 0.1 < tau < 100 works well used_indices = [0] remaining_indices = range(1, len(theta)) yield 0 From 1b620a0a1294cbffb5529f28e5974b35804ac5e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Mon, 10 Jun 2013 11:00:19 +0200 Subject: [PATCH 163/736] Tests for iradon_sart: Robust path handling. --- skimage/transform/tests/test_radon_transform.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/skimage/transform/tests/test_radon_transform.py b/skimage/transform/tests/test_radon_transform.py index 650d6961..5ad87877 100644 --- a/skimage/transform/tests/test_radon_transform.py +++ b/skimage/transform/tests/test_radon_transform.py @@ -1,15 +1,17 @@ -from __future__ import print_function -from __future__ import division +from __future__ import print_function, division import numpy as np from numpy.testing import assert_raises import itertools +import os.path + from skimage.transform import radon, iradon from skimage.io import imread from skimage import data_dir -__PHANTOM = imread(data_dir + "/phantom.png", as_grey=True)[::2, ::2] +__PHANTOM = imread(os.path.join(data_dir, "phantom.png"), + as_grey=True)[::2, ::2] def _get_phantom(): @@ -303,7 +305,7 @@ def test_iradon_sart(): debug = False - shepp_logan = imread(data_dir + "/phantom.png", as_grey=True) + shepp_logan = imread(os.path.join(data_dir, "phantom.png"), as_grey=True) image = rescale(shepp_logan, scale=0.4) theta = np.linspace(0., 180., image.shape[0], endpoint=False) sinogram = radon(image, theta, circle=True) From 59bdb24c92cc7b12d6c04087f51300c1c003b49e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Mon, 10 Jun 2013 11:25:23 +0200 Subject: [PATCH 164/736] iradon_sart: Clean up cython code to minimize python calls. --- skimage/transform/_radon_transform.pyx | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/skimage/transform/_radon_transform.pyx b/skimage/transform/_radon_transform.pyx index 321d9556..d17640ad 100644 --- a/skimage/transform/_radon_transform.pyx +++ b/skimage/transform/_radon_transform.pyx @@ -1,13 +1,12 @@ #cython: cdivision=True -#cython: boundscheck=True -#cython: nonecheck=True +#cython: boundscheck=False +#cython: nonecheck=False #cython: wraparound=False import numpy as np -from numpy import pi cimport numpy as cnp cimport cython -from libc.math cimport cos, sin, floor, ceil, sqrt, abs +from libc.math cimport cos, sin, floor, ceil, sqrt, abs, M_PI cpdef bilinear_ray_sum(cnp.ndarray[cnp.double_t, ndim=2] image, double theta, @@ -32,7 +31,7 @@ cpdef bilinear_ray_sum(cnp.ndarray[cnp.double_t, ndim=2] image, double theta, A measure of how long the ray's path through the reconstruction circle was """ - theta = theta / 180. * pi + theta = theta / 180. * M_PI cdef double radius = image.shape[0] // 2 - 1 cdef double projection_center = image.shape[0] // 2 - 1 cdef double rotation_center = image.shape[0] // 2 @@ -41,7 +40,7 @@ cpdef bilinear_ray_sum(cnp.ndarray[cnp.double_t, ndim=2] image, double theta, # s0 is the half-length of the ray's path in the reconstruction circle cdef double s0 s0 = sqrt(radius**2 - t**2) if radius**2 >= t**2 else 0. - cdef Py_ssize_t Ns = 2 * int(ceil(2 * s0)) # number of steps along the ray + cdef Py_ssize_t Ns = 2 * ( ceil(2 * s0)) # number of steps along the ray cdef double ray_sum = 0. cdef double weight_norm = 0. cdef double ds, dx, dy, x0, y0, x, y, di, dj, index_i, index_j @@ -110,7 +109,7 @@ cpdef bilinear_ray_update(cnp.ndarray[cnp.double_t, ndim=2] image, deviation = -(ray_sum - projected_value) / weight_norm else: deviation = 0. - theta = theta / 180. * pi + theta = theta / 180. * M_PI cdef double radius = image.shape[0] // 2 - 1 cdef double projection_center = image.shape[0] // 2 - 1 cdef double rotation_center = image.shape[0] // 2 @@ -119,12 +118,12 @@ cpdef bilinear_ray_update(cnp.ndarray[cnp.double_t, ndim=2] image, # s0 is the half-length of the ray's path in the reconstruction circle cdef double s0 s0 = sqrt(radius*radius - t*t) if radius**2 >= t**2 else 0. - cdef unsigned int Ns = 2 * int(ceil(2 * s0)) + cdef Py_ssize_t Ns = 2 * ( ceil(2 * s0)) cdef double hamming_beta = 0.46164 # beta for equiripple Hamming window cdef double ds, dx, dy, x0, y0, x, y, di, dj, index_i, index_j cdef double hamming_window - cdef unsigned int k, i, j + cdef Py_ssize_t k, i, j if Ns > 0: # Step length between samples ds = 2 * s0 / Ns @@ -143,7 +142,7 @@ cpdef bilinear_ray_update(cnp.ndarray[cnp.double_t, ndim=2] image, di = index_i - floor(index_i) dj = index_j - floor(index_j) hamming_window = ((1 - hamming_beta) - - hamming_beta * cos(2*pi*k / (Ns - 1))) + - hamming_beta * cos(2 * M_PI * k / (Ns - 1))) if i > 0 and j > 0: image_update[i, j] += (deviation * (1. - di) * (1. - dj) * ds * hamming_window) @@ -159,9 +158,10 @@ cpdef bilinear_ray_update(cnp.ndarray[cnp.double_t, ndim=2] image, return deviation -def sart_projection_update(cnp.ndarray[cnp.double_t, ndim=2] image, \ - double theta, \ - cnp.ndarray[cnp.double_t, ndim=1] projection, +@cython.boundscheck(True) +def sart_projection_update(cnp.ndarray[cnp.double_t, ndim=2] image not None, + double theta, + cnp.ndarray[cnp.double_t, ndim=1] projection not None, double projection_shift=0.): """ Compute update to a reconstruction estimate from a single projection From c977c5974dd6a1f31cbd402c5bb6c93627f028c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Mon, 10 Jun 2013 11:45:47 +0200 Subject: [PATCH 165/736] iradon_sart: Improve argument checking. --- skimage/transform/radon_transform.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/skimage/transform/radon_transform.py b/skimage/transform/radon_transform.py index bba73c3f..1ca56feb 100644 --- a/skimage/transform/radon_transform.py +++ b/skimage/transform/radon_transform.py @@ -345,17 +345,33 @@ def iradon_sart(radon_image, theta=None, image=None, projection_shifts=None, -Kaczmarz' method, Wikipedia, http://en.wikipedia.org/wiki/Kaczmarz_method """ + if radon_image.ndim != 2: + raise ValueError('radon_image must be two dimensional') + reconstructed_shape = (radon_image.shape[0], radon_image.shape[0]) if theta is None: theta = np.linspace(0, 180, radon_image.shape[1], endpoint=False) - reconstructed_shape = (radon_image.shape[0], radon_image.shape[0]) + elif theta.shape != (radon_image.shape[1],): + raise ValueError('Shape of theta (%s) does not match the ' + 'number of projections (%d)' + % (projection_shifts.shape, radon_image.shape[1])) if image is None: image = np.zeros(reconstructed_shape, dtype=np.float) - if projection_shifts is None: - projection_shifts = np.zeros((radon_image.shape[1],), dtype=np.float) elif image.shape != reconstructed_shape: raise ValueError('Shape of image (%s) does not match first dimension ' 'of radon_image (%s)' % (image.shape, reconstructed_shape)) + if projection_shifts is None: + projection_shifts = np.zeros((radon_image.shape[1],), dtype=np.float) + elif projection_shifts.shape != (radon_image.shape[1],): + raise ValueError('Shape of projection_shifts (%s) does not match the ' + 'number of projections (%d)' + % (projection_shifts.shape, radon_image.shape[1])) + if not clip is None: + if len(clip) != 2: + raise ValueError('clip must be a length-2 sequence') + clip = (float(clip[0]), float(clip[1])) + relaxation = float(relaxation) + for angle_index in _sart_order_angles(theta): image_update = sart_projection_update(image, theta[angle_index], radon_image[:, angle_index], From 91aeb658ddb7d6b9867fcd0e69cda4fb5380c71a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Mon, 10 Jun 2013 13:22:55 +0200 Subject: [PATCH 166/736] iradon_sart's cython exts: Use typed memoryviews, not ndarrays. --- skimage/transform/_radon_transform.pyx | 59 +++++++++++++------------- 1 file changed, 30 insertions(+), 29 deletions(-) diff --git a/skimage/transform/_radon_transform.pyx b/skimage/transform/_radon_transform.pyx index d17640ad..8622666a 100644 --- a/skimage/transform/_radon_transform.pyx +++ b/skimage/transform/_radon_transform.pyx @@ -9,8 +9,8 @@ cimport cython from libc.math cimport cos, sin, floor, ceil, sqrt, abs, M_PI -cpdef bilinear_ray_sum(cnp.ndarray[cnp.double_t, ndim=2] image, double theta, - double ray_position): +cpdef bilinear_ray_sum(cnp.double_t[:, :] image, cnp.double_t theta, + cnp.double_t ray_position): """ Compute the projection of an image along a ray. @@ -32,18 +32,18 @@ cpdef bilinear_ray_sum(cnp.ndarray[cnp.double_t, ndim=2] image, double theta, circle was """ theta = theta / 180. * M_PI - cdef double radius = image.shape[0] // 2 - 1 - cdef double projection_center = image.shape[0] // 2 - 1 - cdef double rotation_center = image.shape[0] // 2 + cdef cnp.double_t radius = image.shape[0] // 2 - 1 + cdef cnp.double_t projection_center = image.shape[0] // 2 - 1 + cdef cnp.double_t rotation_center = image.shape[0] // 2 # (s, t) is the (x, y) system rotated by theta - cdef double t = ray_position - projection_center + cdef cnp.double_t t = ray_position - projection_center # s0 is the half-length of the ray's path in the reconstruction circle - cdef double s0 + cdef cnp.double_t s0 s0 = sqrt(radius**2 - t**2) if radius**2 >= t**2 else 0. cdef Py_ssize_t Ns = 2 * ( ceil(2 * s0)) # number of steps along the ray - cdef double ray_sum = 0. - cdef double weight_norm = 0. - cdef double ds, dx, dy, x0, y0, x, y, di, dj, index_i, index_j + cdef cnp.double_t ray_sum = 0. + cdef cnp.double_t weight_norm = 0. + cdef cnp.double_t ds, dx, dy, x0, y0, x, y, di, dj, index_i, index_j cdef Py_ssize_t k, i, j if Ns > 0: # step length between samples @@ -79,9 +79,10 @@ cpdef bilinear_ray_sum(cnp.ndarray[cnp.double_t, ndim=2] image, double theta, return ray_sum, weight_norm -cpdef bilinear_ray_update(cnp.ndarray[cnp.double_t, ndim=2] image, - cnp.ndarray[cnp.double_t, ndim=2] image_update, - double theta, double ray_position, double projected_value): +cpdef bilinear_ray_update(cnp.double_t[:, :] image, + cnp.double_t[:, :] image_update, + cnp.double_t theta, cnp.double_t ray_position, + cnp.double_t projected_value): """ Compute the update along a ray using bilinear interpolation. @@ -103,26 +104,26 @@ cpdef bilinear_ray_update(cnp.ndarray[cnp.double_t, ndim=2] image, deviation : Deviation before updating the image """ - cdef double ray_sum, weight_norm, deviation + cdef cnp.double_t ray_sum, weight_norm, deviation ray_sum, weight_norm = bilinear_ray_sum(image, theta, ray_position) if weight_norm > 0.: deviation = -(ray_sum - projected_value) / weight_norm else: deviation = 0. theta = theta / 180. * M_PI - cdef double radius = image.shape[0] // 2 - 1 - cdef double projection_center = image.shape[0] // 2 - 1 - cdef double rotation_center = image.shape[0] // 2 + cdef cnp.double_t radius = image.shape[0] // 2 - 1 + cdef cnp.double_t projection_center = image.shape[0] // 2 - 1 + cdef cnp.double_t rotation_center = image.shape[0] // 2 # (s, t) is the (x, y) system rotated by theta - cdef double t = ray_position - projection_center + cdef cnp.double_t t = ray_position - projection_center # s0 is the half-length of the ray's path in the reconstruction circle - cdef double s0 + cdef cnp.double_t s0 s0 = sqrt(radius*radius - t*t) if radius**2 >= t**2 else 0. cdef Py_ssize_t Ns = 2 * ( ceil(2 * s0)) - cdef double hamming_beta = 0.46164 # beta for equiripple Hamming window + cdef cnp.double_t hamming_beta = 0.46164 # beta for equiripple Hamming window - cdef double ds, dx, dy, x0, y0, x, y, di, dj, index_i, index_j - cdef double hamming_window + cdef cnp.double_t ds, dx, dy, x0, y0, x, y, di, dj, index_i, index_j + cdef cnp.double_t hamming_window cdef Py_ssize_t k, i, j if Ns > 0: # Step length between samples @@ -159,10 +160,10 @@ cpdef bilinear_ray_update(cnp.ndarray[cnp.double_t, ndim=2] image, @cython.boundscheck(True) -def sart_projection_update(cnp.ndarray[cnp.double_t, ndim=2] image not None, - double theta, - cnp.ndarray[cnp.double_t, ndim=1] projection not None, - double projection_shift=0.): +def sart_projection_update(cnp.double_t[:, :] image not None, + cnp.double_t theta, + cnp.double_t[:] projection not None, + cnp.double_t projection_shift=0.): """ Compute update to a reconstruction estimate from a single projection using bilinear interpolation. @@ -185,11 +186,11 @@ def sart_projection_update(cnp.ndarray[cnp.double_t, ndim=2] image not None, Array of same shape as ``image`` containing updates that should be added to ``image`` to improve the reconstruction estimate """ - cdef cnp.ndarray[cnp.double_t, ndim=2] image_update = np.zeros_like(image) - cdef double ray_position + cdef cnp.double_t[:, :] image_update = np.zeros_like(image) + cdef cnp.double_t ray_position cdef Py_ssize_t i for i in range(projection.shape[0]): ray_position = i + projection_shift bilinear_ray_update(image, image_update, theta, ray_position, projection[i]) - return image_update + return np.asarray(image_update) From 5baaf785646cefbabd86bcb1d61e85f56fd8caa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Mon, 10 Jun 2013 22:13:43 +0200 Subject: [PATCH 167/736] iradon_sart: Reduce code duplication in interpolation. --- skimage/transform/_radon_transform.pyx | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/skimage/transform/_radon_transform.pyx b/skimage/transform/_radon_transform.pyx index 8622666a..bd2f356c 100644 --- a/skimage/transform/_radon_transform.pyx +++ b/skimage/transform/_radon_transform.pyx @@ -40,10 +40,12 @@ cpdef bilinear_ray_sum(cnp.double_t[:, :] image, cnp.double_t theta, # s0 is the half-length of the ray's path in the reconstruction circle cdef cnp.double_t s0 s0 = sqrt(radius**2 - t**2) if radius**2 >= t**2 else 0. - cdef Py_ssize_t Ns = 2 * ( ceil(2 * s0)) # number of steps along the ray + cdef Py_ssize_t Ns = 2 * ( ceil(2 * s0)) # number of steps + # along the ray cdef cnp.double_t ray_sum = 0. cdef cnp.double_t weight_norm = 0. - cdef cnp.double_t ds, dx, dy, x0, y0, x, y, di, dj, index_i, index_j + cdef cnp.double_t ds, dx, dy, x0, y0, x, y, di, dj, + cdef cnp.double_t index_i, index_j, weight cdef Py_ssize_t k, i, j if Ns > 0: # step length between samples @@ -65,17 +67,21 @@ cpdef bilinear_ray_sum(cnp.double_t[:, :] image, cnp.double_t theta, # Use linear interpolation between values # Where values fall outside the array, assume zero if i > 0 and j > 0: - ray_sum += (1. - di) * (1. - dj) * image[i, j] * ds - weight_norm += ((1 - di) * (1 - dj) * ds)**2 + weight = (1. - di) * (1. - dj) * ds + ray_sum += weight * image[i, j] + weight_norm += weight**2 if i > 0 and j < image.shape[1] - 1: - ray_sum += (1. - di) * dj * image[i, j+1] * ds - weight_norm += ((1 - di) * dj * ds)**2 + weight = (1. - di) * dj * ds + ray_sum += weight * image[i, j+1] + weight_norm += weight**2 if i < image.shape[0] - 1 and j > 0: - ray_sum += di * (1 - dj) * image[i+1, j] * ds - weight_norm += (di * (1 - dj) * ds)**2 + weight = di * (1 - dj) * ds + ray_sum += weight * image[i+1, j] + weight_norm += weight**2 if i < image.shape[0] - 1 and j < image.shape[1] - 1: - ray_sum += di * dj * image[i+1, j+1] * ds - weight_norm += (di * dj * ds)**2 + weight = di * dj * ds + ray_sum += weight * image[i+1, j+1] + weight_norm += weight**2 return ray_sum, weight_norm From 89f227c5ffbb7a5c405c1a81d8dd3e6009a4e70e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Sat, 15 Jun 2013 17:10:03 +0200 Subject: [PATCH 168/736] iradon_sart: Remove needless memoryview/ndarray conversion. image_update is not manipulated in sart_projection_update and the conversion to memoryview will happen when it is passed to the subroutines anyway. --- skimage/transform/_radon_transform.pyx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skimage/transform/_radon_transform.pyx b/skimage/transform/_radon_transform.pyx index bd2f356c..d6112a57 100644 --- a/skimage/transform/_radon_transform.pyx +++ b/skimage/transform/_radon_transform.pyx @@ -192,11 +192,11 @@ def sart_projection_update(cnp.double_t[:, :] image not None, Array of same shape as ``image`` containing updates that should be added to ``image`` to improve the reconstruction estimate """ - cdef cnp.double_t[:, :] image_update = np.zeros_like(image) + cdef cnp.ndarray[cnp.double_t, ndim=2] image_update = np.zeros_like(image) cdef cnp.double_t ray_position cdef Py_ssize_t i for i in range(projection.shape[0]): ray_position = i + projection_shift bilinear_ray_update(image, image_update, theta, ray_position, projection[i]) - return np.asarray(image_update) + return image_update From 8d4b1e671014693742f82bf7967fc85f248b5e10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Sun, 16 Jun 2013 09:37:45 +0200 Subject: [PATCH 169/736] iradon_sart: Add Kaczmarz reference and reformat citations. --- skimage/transform/radon_transform.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/skimage/transform/radon_transform.py b/skimage/transform/radon_transform.py index 1ca56feb..b69439a2 100644 --- a/skimage/transform/radon_transform.py +++ b/skimage/transform/radon_transform.py @@ -337,11 +337,14 @@ def iradon_sart(radon_image, theta=None, image=None, projection_shifts=None, information, but will also often increase the noise. References: - -A. C. Kak, Malcolm Slaney, "Principles of Computerized Tomographic + -AC Kak, M Slaney, "Principles of Computerized Tomographic Imaging", IEEE Press 1988. -AH Andersen, AC Kak, "Simultaneous algebraic reconstruction technique (SART): a superior implementation of the ART algorithm", Ultrasonic Imaging 6 pp 81--94 (1984) + -S Kaczmarz, "Angenäherte auflösung von systemen linearer + gleichungen", Bulletin International de l’Academie Polonaise des + Sciences et des Lettres 35 pp 355--357 (1937) -Kaczmarz' method, Wikipedia, http://en.wikipedia.org/wiki/Kaczmarz_method """ From dcd31c18826ba491ee4442601e3ea66696cbcba3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Sun, 16 Jun 2013 10:18:48 +0200 Subject: [PATCH 170/736] radon_transform: Declare encoding. --- skimage/transform/radon_transform.py | 1 + 1 file changed, 1 insertion(+) diff --git a/skimage/transform/radon_transform.py b/skimage/transform/radon_transform.py index b69439a2..32572f68 100644 --- a/skimage/transform/radon_transform.py +++ b/skimage/transform/radon_transform.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """ radon.py - Radon and inverse radon transforms From 2082b57e4171225caa40ab94990e8ae168f7fb49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Sun, 16 Jun 2013 14:24:15 +0200 Subject: [PATCH 171/736] iradon_sart: Order angles using a golden ratio approach. --- skimage/transform/radon_transform.py | 76 +++++++++++++++++++--------- 1 file changed, 53 insertions(+), 23 deletions(-) diff --git a/skimage/transform/radon_transform.py b/skimage/transform/radon_transform.py index 32572f68..82f6bd21 100644 --- a/skimage/transform/radon_transform.py +++ b/skimage/transform/radon_transform.py @@ -258,31 +258,58 @@ def iradon(radon_image, theta=None, output_size=None, return reconstructed * np.pi / (2 * len(th)) -def _sart_order_angles(theta): +def order_angles_golden_ratio(theta): """ Order angles to reduce the amount of correlated information - in subsequent projections, i.e. make sure subsequent angles - are as far away from each other mod 180 degrees as possible. - Indices into the ``theta`` array are yielded. + in subsequent projections. + + Parameters + ---------- + theta : 1D array of floats + Projection angles in degrees. Duplicate angles are not allowed. + + Returns + ------- + indices : 1D array of unsigned integers + Indices into ``theta`` such that ``theta[indices]`` gives the + approximate golden ratio ordering of the projections. + + Notes + ----- + The method used here is that of the golden ratio introduced + by T. Kohler. + + References: + -Kohler, T. "A projection access scheme for iterative + reconstruction based on the golden section." Nuclear Science + Symposium Conference Record, 2004 IEEE. Vol. 6. IEEE, 2004. + -Winkelmann, Stefanie, et al. "An optimal radial profile order + based on the Golden Ratio for time-resolved MRI." + Medical Imaging, IEEE Transactions on 26.1 (2007): 68-76. """ - tau = 3. # time constant for correlations; 0.1 < tau < 100 works well - used_indices = [0] - remaining_indices = range(1, len(theta)) - yield 0 - while remaining_indices: - used = np.array(theta[used_indices]) - used.shape = (-1, 1) - remaining = np.array(theta[remaining_indices]) - remaining.shape = (1, -1) - time = (np.arange(used.shape[0]) + 1)[::-1] - time.shape = (-1, 1) - difference = used - remaining - distance = np.minimum(abs(difference % 180), abs(difference % -180)) - cost = np.exp(-distance * time / tau).sum(axis=0).squeeze() - next_angle_remaining_index = np.argmin(cost) - next_angle_index = remaining_indices.pop(next_angle_remaining_index) - used_indices.append(next_angle_index) - yield next_angle_index + interval = 180 + def angle_distance(a, b): + difference = a - b + return min(abs(difference % interval), abs(difference % -interval)) + + remaining = list(np.argsort(theta)) # indices into theta + # yield an arbitrary angle to start things off + index = remaining.pop(0) + angle = theta[index] + yield index + # determine subsequent angles using the golden ration method + angle_increment = interval * (1 - (np.sqrt(5) - 1) / 2) + while remaining: + angle = (angle + angle_increment) % interval + insert_point = np.searchsorted(theta[remaining], angle) + index_below = insert_point - 1 + index_above = 0 if insert_point == len(remaining) else insert_point + distance_below = angle_distance(angle, theta[remaining[index_below]]) + distance_above = angle_distance(angle, theta[remaining[index_above]]) + if distance_below < distance_above: + yield remaining.pop(index_below) + else: + yield remaining.pop(index_above) def iradon_sart(radon_image, theta=None, image=None, projection_shifts=None, @@ -346,6 +373,9 @@ def iradon_sart(radon_image, theta=None, image=None, projection_shifts=None, -S Kaczmarz, "Angenäherte auflösung von systemen linearer gleichungen", Bulletin International de l’Academie Polonaise des Sciences et des Lettres 35 pp 355--357 (1937) + -Kohler, T. "A projection access scheme for iterative + reconstruction based on the golden section." Nuclear Science + Symposium Conference Record, 2004 IEEE. Vol. 6. IEEE, 2004. -Kaczmarz' method, Wikipedia, http://en.wikipedia.org/wiki/Kaczmarz_method """ @@ -376,7 +406,7 @@ def iradon_sart(radon_image, theta=None, image=None, projection_shifts=None, clip = (float(clip[0]), float(clip[1])) relaxation = float(relaxation) - for angle_index in _sart_order_angles(theta): + for angle_index in order_angles_golden_ratio(theta): image_update = sart_projection_update(image, theta[angle_index], radon_image[:, angle_index], projection_shifts[angle_index]) From ace07a0b18987e734511e8f865b6140525a0f584 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Sun, 16 Jun 2013 14:24:47 +0200 Subject: [PATCH 172/736] iradon_sart: Add test for order_angles_golden_ratio. --- skimage/transform/tests/test_radon_transform.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/skimage/transform/tests/test_radon_transform.py b/skimage/transform/tests/test_radon_transform.py index 5ad87877..e5df9b8d 100644 --- a/skimage/transform/tests/test_radon_transform.py +++ b/skimage/transform/tests/test_radon_transform.py @@ -298,6 +298,19 @@ def test_radon_iradon_circle(): yield check_radon_iradon_circle, interpolation, shape, output_size +def test_order_angles_golden_ratio(): + from skimage.transform.radon_transform import order_angles_golden_ratio + np.random.seed(1231) + lengths = [1, 4, 10, 180] + for l in lengths: + theta_ordered = np.linspace(0, 180, l, endpoint=False) + theta_random = np.random.uniform(0, 180, l) + for theta in (theta_random, theta_ordered): + indices = [x for x in order_angles_golden_ratio(theta)] + # no duplicate indices allowed + assert len(indices) == len(set(indices)) + + def test_iradon_sart(): from skimage.io import imread from skimage import data_dir From 116e1dd57140fe99c9ff7456a71c50a0595c3fc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Sun, 16 Jun 2013 14:25:23 +0200 Subject: [PATCH 173/736] iradon_sart: Also test accuracy with a missing wedge. --- .../transform/tests/test_radon_transform.py | 91 ++++++++++--------- 1 file changed, 47 insertions(+), 44 deletions(-) diff --git a/skimage/transform/tests/test_radon_transform.py b/skimage/transform/tests/test_radon_transform.py index e5df9b8d..ae25c970 100644 --- a/skimage/transform/tests/test_radon_transform.py +++ b/skimage/transform/tests/test_radon_transform.py @@ -320,54 +320,57 @@ def test_iradon_sart(): shepp_logan = imread(os.path.join(data_dir, "phantom.png"), as_grey=True) image = rescale(shepp_logan, scale=0.4) - theta = np.linspace(0., 180., image.shape[0], endpoint=False) - sinogram = radon(image, theta, circle=True) - reconstructed = iradon_sart(sinogram, theta) + theta_ordered = np.linspace(0., 180., image.shape[0], endpoint=False) + theta_missing_wedge = np.linspace(0., 150., image.shape[0], endpoint=True) + for theta, error_factor in ((theta_ordered, 1.), + (theta_missing_wedge, 2.)): + sinogram = radon(image, theta, circle=True) + reconstructed = iradon_sart(sinogram, theta) - if debug: - from matplotlib import pyplot as plt - plt.figure() - plt.subplot(221) - plt.imshow(image, interpolation='nearest') - plt.subplot(222) - plt.imshow(sinogram, interpolation='nearest') - plt.subplot(223) - plt.imshow(reconstructed, interpolation='nearest') - plt.subplot(224) - plt.imshow(reconstructed - image, interpolation='nearest') - plt.show() + if debug: + from matplotlib import pyplot as plt + plt.figure() + plt.subplot(221) + plt.imshow(image, interpolation='nearest') + plt.subplot(222) + plt.imshow(sinogram, interpolation='nearest') + plt.subplot(223) + plt.imshow(reconstructed, interpolation='nearest') + plt.subplot(224) + plt.imshow(reconstructed - image, interpolation='nearest') + plt.show() - delta = np.mean(np.abs(reconstructed - image)) - print('delta (1 iteration) =', delta) - assert delta < 0.025 - reconstructed = iradon_sart(sinogram, theta, reconstructed) - delta = np.mean(np.abs(reconstructed - image)) - print('delta (2 iterations) =', delta) - assert delta < 0.015 + delta = np.mean(np.abs(reconstructed - image)) + print('delta (1 iteration) =', delta) + assert delta < 0.016 * error_factor + reconstructed = iradon_sart(sinogram, theta, reconstructed) + delta = np.mean(np.abs(reconstructed - image)) + print('delta (2 iterations) =', delta) + assert delta < 0.013 * error_factor - np.random.seed(1239867) - shifts = np.random.uniform(-3, 3, sinogram.shape[1]) - x = np.arange(sinogram.shape[0]) - sinogram_shifted = np.vstack(np.interp(x + shifts[i], x, sinogram[:, i]) - for i in range(sinogram.shape[1])).T - reconstructed = iradon_sart(sinogram_shifted, theta, - projection_shifts=shifts) - if debug: - from matplotlib import pyplot as plt - plt.figure() - plt.subplot(221) - plt.imshow(image, interpolation='nearest') - plt.subplot(222) - plt.imshow(sinogram_shifted, interpolation='nearest') - plt.subplot(223) - plt.imshow(reconstructed, interpolation='nearest') - plt.subplot(224) - plt.imshow(reconstructed - image, interpolation='nearest') - plt.show() + np.random.seed(1239867) + shifts = np.random.uniform(-3, 3, sinogram.shape[1]) + x = np.arange(sinogram.shape[0]) + sinogram_shifted = np.vstack(np.interp(x + shifts[i], x, sinogram[:, i]) + for i in range(sinogram.shape[1])).T + reconstructed = iradon_sart(sinogram_shifted, theta, + projection_shifts=shifts) + if debug: + from matplotlib import pyplot as plt + plt.figure() + plt.subplot(221) + plt.imshow(image, interpolation='nearest') + plt.subplot(222) + plt.imshow(sinogram_shifted, interpolation='nearest') + plt.subplot(223) + plt.imshow(reconstructed, interpolation='nearest') + plt.subplot(224) + plt.imshow(reconstructed - image, interpolation='nearest') + plt.show() - delta = np.mean(np.abs(reconstructed - image)) - print('delta (1 iteration, shifted sinogram) =', delta) - assert delta < 0.025 + delta = np.mean(np.abs(reconstructed - image)) + print('delta (1 iteration, shifted sinogram) =', delta) + assert delta < 0.018 * error_factor if __name__ == "__main__": from numpy.testing import run_module_suite From 266b2022262a8cbcad9af6962030cc8fd4826c00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Sun, 16 Jun 2013 14:34:53 +0200 Subject: [PATCH 174/736] iradon_sart: Style fixes. --- skimage/transform/radon_transform.py | 1 + 1 file changed, 1 insertion(+) diff --git a/skimage/transform/radon_transform.py b/skimage/transform/radon_transform.py index 82f6bd21..0f26e343 100644 --- a/skimage/transform/radon_transform.py +++ b/skimage/transform/radon_transform.py @@ -288,6 +288,7 @@ def order_angles_golden_ratio(theta): Medical Imaging, IEEE Transactions on 26.1 (2007): 68-76. """ interval = 180 + def angle_distance(a, b): difference = a - b return min(abs(difference % interval), abs(difference % -interval)) From dde4288865f48beca0f81b7ee91a4ddec6c5a664 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Sun, 16 Jun 2013 14:37:31 +0200 Subject: [PATCH 175/736] test_radon_transform: Style fixes. --- skimage/transform/tests/test_radon_transform.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/skimage/transform/tests/test_radon_transform.py b/skimage/transform/tests/test_radon_transform.py index ae25c970..74d20a24 100644 --- a/skimage/transform/tests/test_radon_transform.py +++ b/skimage/transform/tests/test_radon_transform.py @@ -351,8 +351,9 @@ def test_iradon_sart(): np.random.seed(1239867) shifts = np.random.uniform(-3, 3, sinogram.shape[1]) x = np.arange(sinogram.shape[0]) - sinogram_shifted = np.vstack(np.interp(x + shifts[i], x, sinogram[:, i]) - for i in range(sinogram.shape[1])).T + sinogram_shifted = np.vstack(np.interp(x + shifts[i], x, + sinogram[:, i]) + for i in range(sinogram.shape[1])).T reconstructed = iradon_sart(sinogram_shifted, theta, projection_shifts=shifts) if debug: From a5df8d463085e31dbaf8fecdf11945df4112c599 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Sun, 16 Jun 2013 18:43:49 +0200 Subject: [PATCH 176/736] Rewrite Radon example; include SART. The old example had some flaws. The new example corrects these, expands on the topic and adds content relating to the newly implemented SART algorithm. --- doc/examples/plot_radon_transform.py | 177 ++++++++++++++++++++++----- 1 file changed, 147 insertions(+), 30 deletions(-) diff --git a/doc/examples/plot_radon_transform.py b/doc/examples/plot_radon_transform.py index b4b9721d..df3b1577 100644 --- a/doc/examples/plot_radon_transform.py +++ b/doc/examples/plot_radon_transform.py @@ -3,55 +3,172 @@ Radon transform =============== -The radon transform is a technique widely used in tomography to -reconstruct an object from different projections. A projection is, for -example, the scattering data obtained as the output of a tomographic -scan. +In computed tomography, the tomography reconstruction problem is to obtain +a tomographic slice image from a set of projections. A projection is formed +by drawing a set of parallel rays through the 2D object of interest, assigning +the integral of the object's contrast along each ray to a single pixel in the +projection. A single projection of a 2D object is one dimensional. To +enable computed tomography reconstruction of the object, several projections +must be acquired, each of them with the rays making a different angle with +the axes of the object. A collection of projections at several angles is +called a sinogram. + +The inverse Radon transform is used in computed tomography to reconstruct +a 2D image from can hence be used to reconstruct an object from the measured +projections (the sinogram). A practical, exact implementation of the inverse +Radon transform does not exist, but there are several good approximate +algorithms available. + +As the inverse Radon transform reconstructs the object from a set of +projections, the (forward) Radon transform can be used to simulate a +tomography experiment. For more information see: + - AC Kak, M Slaney, "Principles of Computerized Tomographic Imaging", + http://www.slaney.org/pct/pct-toc.html - http://en.wikipedia.org/wiki/Radon_transform - - http://www.clear.rice.edu/elec431/projects96/DSP/bpanalysis.html -This script performs the radon transform, and reconstructs the -input image based on the resulting sinogram. +This script performs the Radon transform to simulate a tomography experiment +and reconstructs the input image based on the resulting sinogram formed by +the simulation. Two methods for performing the inverse Radon transform +and reconstructing the original image will be used: The Filtered Back +Projection (FBP) and the Simultaneous Algebraic Reconstruction +Technique (SART). + +The forward transform +===================== + +As our original image, we will use the Shepp-Logan phantom. When calculating +the Radon transform, we need to decide how many projection angles we wish +to use. As a rule of thumb, the number of projections should be about the +same as the number of pixels there are across the object (to see why this +is so, consider how many unknown pixel values must be determined in the +reconstruction process and compare this to the number of measurements +provided by the projections), and we follow that rule here. Below is the +original image and its Radon transform, often known as its _sinogram_: """ +import numpy as np import matplotlib.pyplot as plt from skimage.io import imread from skimage import data_dir -from skimage.transform import radon, iradon, rescale - +from skimage.transform import radon, rescale image = imread(data_dir + "/phantom.png", as_grey=True) image = rescale(image, scale=0.4) -plt.figure(figsize=(8, 8.5)) +plt.figure(figsize=(8, 4.5)) -plt.subplot(221) +plt.subplot(121) plt.title("Original") plt.imshow(image, cmap=plt.cm.Greys_r) -plt.subplot(222) -projections = radon(image, theta=[0, 45, 90]) -plt.plot(projections) -plt.title("Projections at\n0, 45 and 90 degrees") -plt.xlabel("Projection axis") -plt.ylabel("Intensity") - -projections = radon(image) -plt.subplot(223) -plt.title("Radon transform\n(Sinogram)") -plt.xlabel("Projection angle (degrees)") -plt.ylabel("Projection axis") -plt.imshow(projections, aspect='auto') - -reconstruction = iradon(projections) -plt.subplot(224) -plt.title("Reconstruction\nfrom sinogram") -plt.imshow(reconstruction, cmap=plt.cm.Greys_r) +theta = np.linspace(0., 180., max(image.shape), endpoint=True) +sinogram = radon(image, theta=theta, circle=True) +plt.subplot(122) +plt.title("Radon transform\n(Sinogram)"); +plt.xlabel("Projection angle (deg)"); +plt.ylabel("Projection position (pixels)"); +plt.imshow(sinogram, cmap=plt.cm.Greys_r, + extent=(0, 180, 0, sinogram.shape[0]), aspect='auto') plt.subplots_adjust(hspace=0.4, wspace=0.5) -plt.show() + +""" +.. image:: PLOT2RST.current_figure + +Reconstruction with the Filtered Back Projection (FBP) +====================================================== + +The mathematical foundation of the filtered back projection is the Fourier +slice theorem (http://en.wikipedia.org/wiki/Projection-slice_theorem). It +uses Fourier transform of the projection and interpolation in Fourier space +to obtain the 2D Fourier transform of the image, which is then inverted to +form the reconstructed image. The filtered back projection is among the +fastest methods of performing the inverse Radon transform. The only tunable +parameter for the FBP is the filter, which is applied to the Fourier +transformed projections. It is needed to suppress high frequency noise in the +reconstruction. ``skimage`` provides a few different options for the filter. + +""" + +from skimage.transform import iradon + +reconstruction_fbp = iradon(sinogram, theta=theta, circle=True) + +imkwargs = dict(vmin=-0.2, vmax=0.2) +plt.figure(figsize=(8, 4.5)) +plt.subplot(121) +plt.title("Reconstruction\nFiltered back projection") +plt.imshow(reconstruction_fbp, cmap=plt.cm.Greys_r) +plt.subplot(122) +plt.title("Reconstruction error\nFiltered back projection") +plt.imshow(reconstruction_fbp - image, cmap=plt.cm.Greys_r, **imkwargs) + +""" +.. image:: PLOT2RST.current_figure + +Reconstruction with the Simultaneous Algebraic Reconstruction Technique +======================================================================= + +Algebraic reconstruction techniques for tomography are based on a +straightforward idea: For a pixelated image the value of a single ray in a +particular projection is simply a sum of all the pixels the ray passes through +on its way through the object. This is a way of expressing the forward Radon +transform. The inverse Radon transform can then be formulated as a (large) set +of linear equations. As each ray passes through a small fraction of the pixels +in the image, this set of equations is sparse, allowing iterative solvers for +sparse linear systems to tackle the system of equations. One iterative method +has been particularly popular, namely Kaczmarz' method, which has the property +that the solution will approach a least-squares solution of the equation set. + +The combination of the formulation of the reconstruction problem as a set +of linear equations and an iterative solver makes algebraic techniques +relatively flexible, hence some forms of prior knowledge can be incorporated +with relative ease. + +``skimage`` provides one of the more popular variations of the algebraic +reconstruction techniques: the Simultaneous Algebraic Reconstruction Technique +(SART). It uses Kaczmarz' method as the iterative solver. A good +reconstruction is normally obtained in a single iteration, making the method +computationally effective. Running one or more extra iterations will normally +The implementation in ``skimage`` allows prior +information of the form of a lower and upper threshold on the reconstructed +values to be supplied to the reconstruction. + +""" + +from skimage.transform import iradon_sart + +reconstruction_sart = iradon_sart(sinogram, theta=theta) + +plt.figure(figsize=(8, 8.5)) + +plt.subplot(221) +plt.title("Reconstruction\nSART") +plt.imshow(reconstruction_sart, cmap=plt.cm.Greys_r) +plt.subplot(222) +plt.title("Reconstruction error\nSART") +plt.imshow(reconstruction_sart - image, cmap=plt.cm.Greys_r, **imkwargs) + +# Run a second iteration of SART by supplying the reconstruction +# from the first iteration as an initial estimate +reconstruction_sart2 = iradon_sart(sinogram, theta=theta, + image=reconstruction_sart) + +d = reconstruction_sart - image +print(d.max(), d.min()) + +plt.subplot(223) +plt.title("Reconstruction\nSART, 2 iterations") +plt.imshow(reconstruction_sart2, cmap=plt.cm.Greys_r) +plt.subplot(224) +plt.title("Reconstruction error\nSART, 2 iterations") +plt.imshow(reconstruction_sart2 - image, cmap=plt.cm.Greys_r, **imkwargs) + +""" +.. image:: PLOT2RST.current_figure +""" From a4870242b63458ed9ee79db6a951007c94deebf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Mon, 24 Jun 2013 10:18:54 +0200 Subject: [PATCH 177/736] iradon_sart: Test clip functionality and add a test for it. --- skimage/transform/radon_transform.py | 2 +- skimage/transform/tests/test_radon_transform.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/skimage/transform/radon_transform.py b/skimage/transform/radon_transform.py index 0f26e343..ff3af596 100644 --- a/skimage/transform/radon_transform.py +++ b/skimage/transform/radon_transform.py @@ -413,5 +413,5 @@ def iradon_sart(radon_image, theta=None, image=None, projection_shifts=None, projection_shifts[angle_index]) image += relaxation * image_update if not clip is None: - image = clip(image, clip[0], clip[1]) + image = np.clip(image, clip[0], clip[1]) return image diff --git a/skimage/transform/tests/test_radon_transform.py b/skimage/transform/tests/test_radon_transform.py index 74d20a24..11fced56 100644 --- a/skimage/transform/tests/test_radon_transform.py +++ b/skimage/transform/tests/test_radon_transform.py @@ -347,6 +347,9 @@ def test_iradon_sart(): delta = np.mean(np.abs(reconstructed - image)) print('delta (2 iterations) =', delta) assert delta < 0.013 * error_factor + reconstructed = iradon_sart(sinogram, theta, clip=(0, 1)) + print('delta (1 iteration, clip) =', delta) + assert delta < 0.013 * error_factor np.random.seed(1239867) shifts = np.random.uniform(-3, 3, sinogram.shape[1]) From 9e11e453b64424182e9221f32425df12612177f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Mon, 24 Jun 2013 10:20:16 +0200 Subject: [PATCH 178/736] radon example: Display plots when run from command line. Call to matplotlib's show() was missing. --- doc/examples/plot_radon_transform.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/examples/plot_radon_transform.py b/doc/examples/plot_radon_transform.py index df3b1577..fb2d9dbd 100644 --- a/doc/examples/plot_radon_transform.py +++ b/doc/examples/plot_radon_transform.py @@ -76,6 +76,7 @@ plt.imshow(sinogram, cmap=plt.cm.Greys_r, extent=(0, 180, 0, sinogram.shape[0]), aspect='auto') plt.subplots_adjust(hspace=0.4, wspace=0.5) +plt.show() """ .. image:: PLOT2RST.current_figure @@ -107,6 +108,7 @@ plt.imshow(reconstruction_fbp, cmap=plt.cm.Greys_r) plt.subplot(122) plt.title("Reconstruction error\nFiltered back projection") plt.imshow(reconstruction_fbp - image, cmap=plt.cm.Greys_r, **imkwargs) +plt.show() """ .. image:: PLOT2RST.current_figure @@ -168,6 +170,7 @@ plt.imshow(reconstruction_sart2, cmap=plt.cm.Greys_r) plt.subplot(224) plt.title("Reconstruction error\nSART, 2 iterations") plt.imshow(reconstruction_sart2 - image, cmap=plt.cm.Greys_r, **imkwargs) +plt.show() """ .. image:: PLOT2RST.current_figure From e5dfcb0ab45dabdfc670b8badc7352857ffdc094 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Thu, 4 Jul 2013 15:23:51 +0200 Subject: [PATCH 179/736] Improve the text in and add references to the Radon example. Based on comments from Emmanuelle. --- doc/examples/plot_radon_transform.py | 75 +++++++++++++++++----------- 1 file changed, 45 insertions(+), 30 deletions(-) diff --git a/doc/examples/plot_radon_transform.py b/doc/examples/plot_radon_transform.py index fb2d9dbd..4a2c01a5 100644 --- a/doc/examples/plot_radon_transform.py +++ b/doc/examples/plot_radon_transform.py @@ -4,38 +4,36 @@ Radon transform =============== In computed tomography, the tomography reconstruction problem is to obtain -a tomographic slice image from a set of projections. A projection is formed +a tomographic slice image from a set of projections [1]_. A projection is formed by drawing a set of parallel rays through the 2D object of interest, assigning the integral of the object's contrast along each ray to a single pixel in the projection. A single projection of a 2D object is one dimensional. To enable computed tomography reconstruction of the object, several projections -must be acquired, each of them with the rays making a different angle with -the axes of the object. A collection of projections at several angles is -called a sinogram. +must be acquired, each of them corresponding to a different angle between the +rays with respect to the object. A collection of projections at several angles +is called a sinogram, which is a linear transform of the original image. The inverse Radon transform is used in computed tomography to reconstruct -a 2D image from can hence be used to reconstruct an object from the measured -projections (the sinogram). A practical, exact implementation of the inverse -Radon transform does not exist, but there are several good approximate -algorithms available. +a 2D image from the measured projections (the sinogram). A practical, exact +implementation of the inverse Radon transform does not exist, but there are +several good approximate algorithms available. As the inverse Radon transform reconstructs the object from a set of projections, the (forward) Radon transform can be used to simulate a tomography experiment. -For more information see: - - - AC Kak, M Slaney, "Principles of Computerized Tomographic Imaging", - http://www.slaney.org/pct/pct-toc.html - - http://en.wikipedia.org/wiki/Radon_transform - This script performs the Radon transform to simulate a tomography experiment and reconstructs the input image based on the resulting sinogram formed by the simulation. Two methods for performing the inverse Radon transform -and reconstructing the original image will be used: The Filtered Back +and reconstructing the original image are compared: The Filtered Back Projection (FBP) and the Simultaneous Algebraic Reconstruction Technique (SART). +.. seealso:: + + - AC Kak, M Slaney, "Principles of Computerized Tomographic Imaging", + http://www.slaney.org/pct/pct-toc.html + - http://en.wikipedia.org/wiki/Radon_transform The forward transform ===================== @@ -85,14 +83,14 @@ Reconstruction with the Filtered Back Projection (FBP) ====================================================== The mathematical foundation of the filtered back projection is the Fourier -slice theorem (http://en.wikipedia.org/wiki/Projection-slice_theorem). It -uses Fourier transform of the projection and interpolation in Fourier space -to obtain the 2D Fourier transform of the image, which is then inverted to -form the reconstructed image. The filtered back projection is among the -fastest methods of performing the inverse Radon transform. The only tunable -parameter for the FBP is the filter, which is applied to the Fourier -transformed projections. It is needed to suppress high frequency noise in the -reconstruction. ``skimage`` provides a few different options for the filter. +slice theorem [2]_. It uses Fourier transform of the projection and +interpolation in Fourier space to obtain the 2D Fourier transform of the image, +which is then inverted to form the reconstructed image. The filtered back +projection is among the fastest methods of performing the inverse Radon +transform. The only tunable parameter for the FBP is the filter, which is +applied to the Fourier transformed projections. It may be used to suppress +high frequency noise in the reconstruction. ``skimage`` provides a few +different options for the filter. """ @@ -117,15 +115,16 @@ Reconstruction with the Simultaneous Algebraic Reconstruction Technique ======================================================================= Algebraic reconstruction techniques for tomography are based on a -straightforward idea: For a pixelated image the value of a single ray in a +straightforward idea: for a pixelated image the value of a single ray in a particular projection is simply a sum of all the pixels the ray passes through on its way through the object. This is a way of expressing the forward Radon transform. The inverse Radon transform can then be formulated as a (large) set of linear equations. As each ray passes through a small fraction of the pixels in the image, this set of equations is sparse, allowing iterative solvers for sparse linear systems to tackle the system of equations. One iterative method -has been particularly popular, namely Kaczmarz' method, which has the property -that the solution will approach a least-squares solution of the equation set. +has been particularly popular, namely Kaczmarz' method [3]_, which has the +property that the solution will approach a least-squares solution of the +equation set. The combination of the formulation of the reconstruction problem as a set of linear equations and an iterative solver makes algebraic techniques @@ -134,12 +133,15 @@ with relative ease. ``skimage`` provides one of the more popular variations of the algebraic reconstruction techniques: the Simultaneous Algebraic Reconstruction Technique -(SART). It uses Kaczmarz' method as the iterative solver. A good +(SART) [1]_ [4]_. It uses Kaczmarz' method [3]_ as the iterative solver. A good reconstruction is normally obtained in a single iteration, making the method computationally effective. Running one or more extra iterations will normally -The implementation in ``skimage`` allows prior -information of the form of a lower and upper threshold on the reconstructed -values to be supplied to the reconstruction. +improve the reconstruction of sharp, high frequency features and reduce the +mean squared error at the expense of increased high frequency noise (the user +will need to decide on what number of iterations is best suited to the problem +at hand. The implementation in ``skimage`` allows prior information of the +form of a lower and upper threshold on the reconstructed values to be supplied +to the reconstruction. """ @@ -174,4 +176,17 @@ plt.show() """ .. image:: PLOT2RST.current_figure + + +.. [1] AC Kak, M Slaney, "Principles of Computerized Tomographic Imaging", + IEEE Press 1988. http://www.slaney.org/pct/pct-toc.html +.. [2] Wikipedia, Radon transform, + http://en.wikipedia.org/wiki/Radon_transform#Relationship_with_the_Fourier_transform +.. [3] S Kaczmarz, "Angenäherte auflösung von systemen linearer + gleichungen", Bulletin International de l’Academie Polonaise des + Sciences et des Lettres 35 pp 355--357 (1937) +.. [4] AH Andersen, AC Kak, "Simultaneous algebraic reconstruction technique + (SART): a superior implementation of the ART algorithm", Ultrasonic + Imaging 6 pp 81--94 (1984) + """ From 1d611878a6c3cbd56cbc55a7abf6220643d3459a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Thu, 4 Jul 2013 15:24:44 +0200 Subject: [PATCH 180/736] Radon example: Style fixes. --- doc/examples/plot_radon_transform.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/examples/plot_radon_transform.py b/doc/examples/plot_radon_transform.py index 4a2c01a5..4b5b8d8d 100644 --- a/doc/examples/plot_radon_transform.py +++ b/doc/examples/plot_radon_transform.py @@ -67,9 +67,9 @@ plt.imshow(image, cmap=plt.cm.Greys_r) theta = np.linspace(0., 180., max(image.shape), endpoint=True) sinogram = radon(image, theta=theta, circle=True) plt.subplot(122) -plt.title("Radon transform\n(Sinogram)"); -plt.xlabel("Projection angle (deg)"); -plt.ylabel("Projection position (pixels)"); +plt.title("Radon transform\n(Sinogram)") +plt.xlabel("Projection angle (deg)") +plt.ylabel("Projection position (pixels)") plt.imshow(sinogram, cmap=plt.cm.Greys_r, extent=(0, 180, 0, sinogram.shape[0]), aspect='auto') From 789c19923d7b60b471e0f7e240d8d2957c0adb10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Thu, 4 Jul 2013 15:29:53 +0200 Subject: [PATCH 181/736] Radon example: print RMS errors of the reconstructions. --- doc/examples/plot_radon_transform.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/doc/examples/plot_radon_transform.py b/doc/examples/plot_radon_transform.py index 4b5b8d8d..d457d5a6 100644 --- a/doc/examples/plot_radon_transform.py +++ b/doc/examples/plot_radon_transform.py @@ -48,6 +48,8 @@ provided by the projections), and we follow that rule here. Below is the original image and its Radon transform, often known as its _sinogram_: """ +from __future__ import print_function, division + import numpy as np import matplotlib.pyplot as plt @@ -97,6 +99,8 @@ different options for the filter. from skimage.transform import iradon reconstruction_fbp = iradon(sinogram, theta=theta, circle=True) +error = reconstruction_fbp - image +print('FBP rms reconstruction error: %.3g' % np.sqrt(np.mean(error**2))) imkwargs = dict(vmin=-0.2, vmax=0.2) plt.figure(figsize=(8, 4.5)) @@ -148,6 +152,9 @@ to the reconstruction. from skimage.transform import iradon_sart reconstruction_sart = iradon_sart(sinogram, theta=theta) +error = reconstruction_sart - image +print('SART (1 iteration) rms reconstruction error: %.3g' + % np.sqrt(np.mean(error**2))) plt.figure(figsize=(8, 8.5)) @@ -162,9 +169,9 @@ plt.imshow(reconstruction_sart - image, cmap=plt.cm.Greys_r, **imkwargs) # from the first iteration as an initial estimate reconstruction_sart2 = iradon_sart(sinogram, theta=theta, image=reconstruction_sart) - -d = reconstruction_sart - image -print(d.max(), d.min()) +error = reconstruction_sart2 - image +print('SART (2 iterations) rms reconstruction error: %.3g' + % np.sqrt(np.mean(error**2))) plt.subplot(223) plt.title("Reconstruction\nSART, 2 iterations") From e2a0f7fff810c08e71db632782392fd533a949f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Thu, 4 Jul 2013 16:39:35 +0200 Subject: [PATCH 182/736] Radon example: Declare UTF-8 encoding. This is needed for non-ASCII characters in the references to papers cited in the text. --- doc/examples/plot_radon_transform.py | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/examples/plot_radon_transform.py b/doc/examples/plot_radon_transform.py index d457d5a6..61175f8a 100644 --- a/doc/examples/plot_radon_transform.py +++ b/doc/examples/plot_radon_transform.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """ =============== Radon transform From 06857d3b92c65127b11853ab2bd41824d47693cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Thu, 4 Jul 2013 17:35:52 +0200 Subject: [PATCH 183/736] Radon tests: Check the correct error metric. --- skimage/transform/tests/test_radon_transform.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/skimage/transform/tests/test_radon_transform.py b/skimage/transform/tests/test_radon_transform.py index 11fced56..333c298d 100644 --- a/skimage/transform/tests/test_radon_transform.py +++ b/skimage/transform/tests/test_radon_transform.py @@ -348,8 +348,9 @@ def test_iradon_sart(): print('delta (2 iterations) =', delta) assert delta < 0.013 * error_factor reconstructed = iradon_sart(sinogram, theta, clip=(0, 1)) + delta = np.mean(np.abs(reconstructed - image)) print('delta (1 iteration, clip) =', delta) - assert delta < 0.013 * error_factor + assert delta < 0.015 * error_factor np.random.seed(1239867) shifts = np.random.uniform(-3, 3, sinogram.shape[1]) From f2790d658c0e6f94bc1a479c4eed162907bf5a05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Fri, 5 Jul 2013 13:02:03 +0200 Subject: [PATCH 184/736] iradon_sart: redefine projection center. This reflects the bugfixes related to issue gh-592 implemented in gh-596. --- skimage/transform/_radon_transform.pyx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skimage/transform/_radon_transform.pyx b/skimage/transform/_radon_transform.pyx index d6112a57..3f865a93 100644 --- a/skimage/transform/_radon_transform.pyx +++ b/skimage/transform/_radon_transform.pyx @@ -33,7 +33,7 @@ cpdef bilinear_ray_sum(cnp.double_t[:, :] image, cnp.double_t theta, """ theta = theta / 180. * M_PI cdef cnp.double_t radius = image.shape[0] // 2 - 1 - cdef cnp.double_t projection_center = image.shape[0] // 2 - 1 + cdef cnp.double_t projection_center = image.shape[0] // 2 cdef cnp.double_t rotation_center = image.shape[0] // 2 # (s, t) is the (x, y) system rotated by theta cdef cnp.double_t t = ray_position - projection_center @@ -118,7 +118,7 @@ cpdef bilinear_ray_update(cnp.double_t[:, :] image, deviation = 0. theta = theta / 180. * M_PI cdef cnp.double_t radius = image.shape[0] // 2 - 1 - cdef cnp.double_t projection_center = image.shape[0] // 2 - 1 + cdef cnp.double_t projection_center = image.shape[0] // 2 cdef cnp.double_t rotation_center = image.shape[0] // 2 # (s, t) is the (x, y) system rotated by theta cdef cnp.double_t t = ray_position - projection_center From af479414353d6655497e0e9a55dcfe23a766f2d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Fri, 5 Jul 2013 13:03:13 +0200 Subject: [PATCH 185/736] iradon_sart: fix comment spelling. --- skimage/transform/radon_transform.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/transform/radon_transform.py b/skimage/transform/radon_transform.py index ff3af596..944b4a6b 100644 --- a/skimage/transform/radon_transform.py +++ b/skimage/transform/radon_transform.py @@ -298,7 +298,7 @@ def order_angles_golden_ratio(theta): index = remaining.pop(0) angle = theta[index] yield index - # determine subsequent angles using the golden ration method + # determine subsequent angles using the golden ratio method angle_increment = interval * (1 - (np.sqrt(5) - 1) / 2) while remaining: angle = (angle + angle_increment) % interval From 3cdfeccc7a80884676a3f364e406c4d7a47d1e98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Fri, 5 Jul 2013 13:12:34 +0200 Subject: [PATCH 186/736] iradon_sart: flip signs to reflect changes to radon. The changes to radon were introduced in gh-596. --- skimage/transform/_radon_transform.pyx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/skimage/transform/_radon_transform.pyx b/skimage/transform/_radon_transform.pyx index 3f865a93..91b943b9 100644 --- a/skimage/transform/_radon_transform.pyx +++ b/skimage/transform/_radon_transform.pyx @@ -50,11 +50,11 @@ cpdef bilinear_ray_sum(cnp.double_t[:, :] image, cnp.double_t theta, if Ns > 0: # step length between samples ds = 2 * s0 / Ns - dx = ds * cos(theta) - dy = ds * sin(theta) + dx = -ds * cos(theta) + dy = -ds * sin(theta) # point of entry of the ray into the reconstruction circle - x0 = -s0 * cos(theta) + t * sin(theta) - y0 = -s0 * sin(theta) - t * cos(theta) + x0 = s0 * cos(theta) - t * sin(theta) + y0 = s0 * sin(theta) + t * cos(theta) for k in range(Ns+1): x = x0 + k * dx y = y0 + k * dy @@ -134,11 +134,11 @@ cpdef bilinear_ray_update(cnp.double_t[:, :] image, if Ns > 0: # Step length between samples ds = 2 * s0 / Ns - dx = ds * cos(theta) - dy = ds * sin(theta) + dx = -ds * cos(theta) + dy = -ds * sin(theta) # Point of entry of the ray into the reconstruction circle - x0 = -s0 * cos(theta) + t * sin(theta) - y0 = -s0 * sin(theta) - t * cos(theta) + x0 = s0 * cos(theta) - t * sin(theta) + y0 = s0 * sin(theta) + t * cos(theta) for k in range(Ns+1): x = x0 + k * dx y = y0 + k * dy From 553767122bd18dec9dd1b75b25a272279d32b09d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Fri, 5 Jul 2013 13:36:55 +0200 Subject: [PATCH 187/736] Add SART to contributors. --- CONTRIBUTORS.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index e03271b8..ac11457c 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -144,3 +144,4 @@ - Jostein Bø Fløystad Reconstruction circle mode for Radon transform + Simultaneous Algebraic Reconstruction Technique for inverse Radon transform From 794a4d7daebfdcb217fbf0a73b9d42e256ce45cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Fri, 5 Jul 2013 14:29:09 +0200 Subject: [PATCH 188/736] arraypad: allow padding with zero entries (i.e. no padding). --- skimage/util/arraypad.py | 2 +- skimage/util/tests/test_arraypad.py | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/skimage/util/arraypad.py b/skimage/util/arraypad.py index acc89b7b..93f66bb8 100644 --- a/skimage/util/arraypad.py +++ b/skimage/util/arraypad.py @@ -1085,7 +1085,7 @@ def _validate_lengths(narray, number_elements): normshp = _normalize_shape(narray, number_elements) for i in normshp: chk = [1 if x is None else x for x in i] - chk = [1 if x > 0 else -1 for x in chk] + chk = [1 if x >= 0 else -1 for x in chk] if (chk[0] < 0) or (chk[1] < 0): fmt = "%s cannot contain negative values." raise ValueError(fmt % (number_elements,)) diff --git a/skimage/util/tests/test_arraypad.py b/skimage/util/tests/test_arraypad.py index 2c0e5ba7..008c3516 100644 --- a/skimage/util/tests/test_arraypad.py +++ b/skimage/util/tests/test_arraypad.py @@ -518,5 +518,12 @@ def test_pad_one_axis_three_ways(): **kwargs) +def test_zero_pad_width(): + arr = np.arange(30) + arr = np.reshape(arr, (6, 5)) + for pad_width in (0, (0, 0), ((0, 0), (0, 0))): + assert np.all(arr == pad(arr, pad_width, mode='constant')) + + if __name__ == "__main__": run_module_suite() From 04b14b42c9c2bcf811ab0904ef7c0f34afb19d8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Fri, 5 Jul 2013 09:04:09 +0200 Subject: [PATCH 189/736] Refactor local block functions and add additional functionality. --- skimage/measure/__init__.py | 2 +- skimage/measure/_sum_blocks.py | 38 ---- skimage/measure/blocks.py | 210 ++++++++++++++++++ skimage/measure/tests/test_blocks.py | 21 ++ .../tests/test_structural_similarity.py | 1 + skimage/measure/tests/test_sum_blocks.py | 16 -- skimage/transform/__init__.py | 4 +- skimage/transform/_warps.py | 144 ++++-------- skimage/transform/tests/test_warps.py | 8 +- 9 files changed, 286 insertions(+), 158 deletions(-) delete mode 100644 skimage/measure/_sum_blocks.py create mode 100644 skimage/measure/blocks.py create mode 100644 skimage/measure/tests/test_blocks.py delete mode 100644 skimage/measure/tests/test_sum_blocks.py diff --git a/skimage/measure/__init__.py b/skimage/measure/__init__.py index a9fbae24..4c44dc2f 100755 --- a/skimage/measure/__init__.py +++ b/skimage/measure/__init__.py @@ -3,7 +3,7 @@ from ._regionprops import regionprops, perimeter from ._structural_similarity import structural_similarity from ._polygon import approximate_polygon, subdivide_polygon from .fit import LineModel, CircleModel, EllipseModel, ransac -from ._sum_blocks import sum_blocks +from .blocks import block_sum, block_median, block_mean, block_min, block_max __all__ = ['find_contours', diff --git a/skimage/measure/_sum_blocks.py b/skimage/measure/_sum_blocks.py deleted file mode 100644 index c65c121e..00000000 --- a/skimage/measure/_sum_blocks.py +++ /dev/null @@ -1,38 +0,0 @@ -def sum_blocks(array, factors): - """Sums the elements in blocks of integer factors and pads the original - array with zeroes if the dimensions are not perfectly divisible by factors. - - This function is different from resize and rescale in transform._warps in - the sense that they use interpolation to upsample or downsample on a 2D - array, while this function performs only dawnsampling but on any - n-dimensional array and returns the sum of elements in a block of size - factors in the original array. - - Parameters - ---------- - array : ndarray - Input n-dimensional array. - factors: tuple - Tuple containing integer values representing block length along each - axis. - - Returns - ------- - array : ndarray - Downsampled array with same number of dimensions as that of input - array. - - Example - ------- - >>> a = np.arange(15).reshape(3, 5) - >>> a - array([[ 0, 1, 2, 3, 4], - [ 5, 6, 7, 8, 9], - [10, 11, 12, 13, 14]]) - >>> sum_blocks(a, (2,3)) - array([[21, 24], - [33, 27]]) - - """ - from ..transform._warps import _downsample - return _downsample(array, factors) diff --git a/skimage/measure/blocks.py b/skimage/measure/blocks.py new file mode 100644 index 00000000..428ecf11 --- /dev/null +++ b/skimage/measure/blocks.py @@ -0,0 +1,210 @@ +import numpy as np +from ..util.shape import view_as_blocks, _pad_asymmetric_zeros + + +def _block_func(image, factors, func): + """Down-sample image by integer factors. + + Parameters + ---------- + image : ndarray + N-dimensional input image. + factors : array_like + Array containing down-sampling integer factor along each axis. + func : object + Function object which is used to calculate the return value for each + local block, e.g. `numpy.sum`. + + Returns + ------- + image : ndarray + Down-sampled image with same number of dimensions as input image. + + """ + + pad_size = [] + if len(factors) != image.ndim: + raise ValueError("`factors` must have the same length " + "as `image.shape`.") + + for i in range(len(factors)): + if image.shape[i] % factors[i] != 0: + pad_size.append(factors[i] - (image.shape[i] % factors[i])) + else: + pad_size.append(0) + + for i in range(len(pad_size)): + image = _pad_asymmetric_zeros(image, pad_size[i], i) + + out = view_as_blocks(image, factors) + block_shape = out.shape + + for i in range(len(block_shape) // 2): + out = func(out, axis=-1) + + return out + + +def block_sum(image, block_size): + """Sum elements in local blocks. + + The image is padded with zeros if it is not perfectly divisible by integer + factors. + + Parameters + ---------- + image : ndarray + N-dimensional input image. + block_size : array_like + Array containing down-sampling integer factor along each axis. + + Returns + ------- + image : ndarray + Down-sampled image with same number of dimensions as input image. + + Example + ------- + >>> a = np.arange(15).reshape(3, 5) + >>> a + image([[ 0, 1, 2, 3, 4], + [ 5, 6, 7, 8, 9], + [10, 11, 12, 13, 14]]) + >>> sum_blocks(a, (2, 3)) + image([[21, 24], + [33, 27]]) + + """ + return _block_func(image, block_size, np.sum) + + +def block_mean(image, block_size): + """Average elements in local blocks. + + The image is padded with zeros if it is not perfectly divisible by integer + factors. + + Parameters + ---------- + image : ndarray + N-dimensional input image. + block_size : array_like + Array containing down-sampling integer factor along each axis. + + Returns + ------- + image : ndarray + Down-sampled image with same number of dimensions as input image. + + Example + ------- + >>> a = np.arange(15).reshape(3, 5) + >>> a + image([[ 0, 1, 2, 3, 4], + [ 5, 6, 7, 8, 9], + [10, 11, 12, 13, 14]]) + >>> sum_blocks(a, (2, 3)) + image([[21, 24], + [33, 27]]) + + """ + return _block_func(image, block_size, np.mean) + + +def block_median(image, block_size): + """Median element in local blocks. + + The image is padded with zeros if it is not perfectly divisible by integer + factors. + + Parameters + ---------- + image : ndarray + N-dimensional input image. + block_size : array_like + Array containing down-sampling integer factor along each axis. + + Returns + ------- + image : ndarray + Down-sampled image with same number of dimensions as input image. + + Example + ------- + >>> a = np.arange(15).reshape(3, 5) + >>> a + image([[ 0, 1, 2, 3, 4], + [ 5, 6, 7, 8, 9], + [10, 11, 12, 13, 14]]) + >>> sum_blocks(a, (2, 3)) + image([[21, 24], + [33, 27]]) + + """ + return _block_func(image, block_size, np.median) + + +def block_min(image, block_size): + """Minimum element in local blocks. + + The image is padded with zeros if it is not perfectly divisible by integer + factors. + + Parameters + ---------- + image : ndarray + N-dimensional input image. + block_size : array_like + Array containing down-sampling integer factor along each axis. + + Returns + ------- + image : ndarray + Down-sampled image with same number of dimensions as input image. + + Example + ------- + >>> a = np.arange(15).reshape(3, 5) + >>> a + image([[ 0, 1, 2, 3, 4], + [ 5, 6, 7, 8, 9], + [10, 11, 12, 13, 14]]) + >>> sum_blocks(a, (2, 3)) + image([[21, 24], + [33, 27]]) + + """ + return _block_func(image, block_size, np.min) + + +def block_max(image, block_size): + """Maximum element in local blocks. + + The image is padded with zeros if it is not perfectly divisible by integer + factors. + + Parameters + ---------- + image : ndarray + N-dimensional input image. + block_size : array_like + Array containing down-sampling integer factor along each axis. + + Returns + ------- + image : ndarray + Down-sampled image with same number of dimensions as input image. + + Example + ------- + >>> a = np.arange(15).reshape(3, 5) + >>> a + image([[ 0, 1, 2, 3, 4], + [ 5, 6, 7, 8, 9], + [10, 11, 12, 13, 14]]) + >>> sum_blocks(a, (2, 3)) + image([[21, 24], + [33, 27]]) + + """ + return _block_func(image, block_size, np.max) diff --git a/skimage/measure/tests/test_blocks.py b/skimage/measure/tests/test_blocks.py new file mode 100644 index 00000000..77dc2b11 --- /dev/null +++ b/skimage/measure/tests/test_blocks.py @@ -0,0 +1,21 @@ +import numpy as np +from numpy.testing import assert_array_equal +from skimage.measure import block_sum + + +def test_block_sum(): + image1 = np.arange(4 * 6).reshape(4, 6) + out1 = block_sum(image1, (2, 3)) + expected1 = np.array([[ 24, 42], + [ 96, 114]]) + assert_array_equal(expected1, out1) + + image2 = np.arange(5 * 8).reshape(5, 8) + out2 = block_sum(image2, (3, 3)) + expected2 = np.array([[ 81, 108, 87], + [174, 192, 138]]) + assert_array_equal(expected2, out2) + + +if __name__ == "__main__": + np.testing.run_module_suite() diff --git a/skimage/measure/tests/test_structural_similarity.py b/skimage/measure/tests/test_structural_similarity.py index ec5ce7ef..e08f2c31 100644 --- a/skimage/measure/tests/test_structural_similarity.py +++ b/skimage/measure/tests/test_structural_similarity.py @@ -68,5 +68,6 @@ def test_invalid_input(): assert_raises(ValueError, ssim, X, X, win_size=8) + if __name__ == "__main__": np.testing.run_module_suite() diff --git a/skimage/measure/tests/test_sum_blocks.py b/skimage/measure/tests/test_sum_blocks.py deleted file mode 100644 index b4910495..00000000 --- a/skimage/measure/tests/test_sum_blocks.py +++ /dev/null @@ -1,16 +0,0 @@ -import numpy as np -from numpy.testing import assert_array_equal -from skimage.measure._sum_blocks import sum_blocks - -def test_downsample_sum_blocks(): - """Verifying downsampling of an array with expected result in sum mode""" - image1 = np.arange(4*6).reshape(4, 6) - out1 = sum_blocks(image1, (2, 3)) - expected1 = np.array([[ 24, 42], - [ 96, 114]]) - assert_array_equal(expected1, out1) - image2 = np.arange(5*8).reshape(5, 8) - out2 = sum_blocks(image2, (3, 3)) - expected2 = np.array([[ 81, 108, 87], - [174, 192, 138]]) - assert_array_equal(expected2, out2) diff --git a/skimage/transform/__init__.py b/skimage/transform/__init__.py index f079ada4..15513f94 100644 --- a/skimage/transform/__init__.py +++ b/skimage/transform/__init__.py @@ -9,7 +9,7 @@ from ._geometric import (warp, warp_coords, estimate_transform, SimilarityTransform, AffineTransform, ProjectiveTransform, PolynomialTransform, PiecewiseAffineTransform) -from ._warps import swirl, resize, rotate, rescale, downscale_local_means +from ._warps import swirl, resize, rotate, rescale, downscale_local_mean from .pyramids import (pyramid_reduce, pyramid_expand, pyramid_gaussian, pyramid_laplacian) @@ -40,7 +40,7 @@ __all__ = ['hough_circle', 'resize', 'rotate', 'rescale', - 'downscale_local_means', + 'downscale_local_mean', 'pyramid_reduce', 'pyramid_expand', 'pyramid_gaussian', diff --git a/skimage/transform/_warps.py b/skimage/transform/_warps.py index c2727c74..cefe61a3 100644 --- a/skimage/transform/_warps.py +++ b/skimage/transform/_warps.py @@ -2,17 +2,16 @@ import numpy as np from scipy import ndimage from ._geometric import warp, SimilarityTransform, AffineTransform -from skimage.util.shape import view_as_blocks, _pad_asymmetric_zeros +from ..measure.blocks import _block_func def resize(image, output_shape, order=1, mode='constant', cval=0.): """Resize image to match a certain size. - Resize performs interpolation to upsample or downsample 2D arrays. For - downsampling any n-dimensional array by performing arithmetic sum or - arithmetic mean, see measure._sum_blocks.sum_blocks and - transform._warps.downscale_local_means respectively. - + Performs interpolation to up-size or down-size images. For down-sampling + N-dimensional images by applying the arithmetic sum or mean, see + `skimage.measure.sum_blocks` and `skimage.transform.downscale_local_means`, + respectively. Parameters ---------- @@ -95,10 +94,10 @@ def resize(image, output_shape, order=1, mode='constant', cval=0.): def rescale(image, scale, order=1, mode='constant', cval=0.): """Scale image by a certain factor. - Rescale performs interpolation to upsample or downsample 2D arrays. For - downsampling any n-dimensional array by performing arithmetic sum or - arithmetic mean, see measure._sum_blocks.sum_blocks and - transform._warps.downscale_local_means respectively. + Performs interpolation to upscale or down-scale images. For down-sampling + N-dimensional images with integer factors by applying the arithmetic sum or + mean, see `skimage.measure.sum_blocks` and + `skimage.transform.downscale_local_means`, respectively. Parameters ---------- @@ -226,6 +225,44 @@ def rotate(image, angle, resize=False, order=1, mode='constant', cval=0.): mode=mode, cval=cval) +def downscale_local_mean(image, factors): + """Down-sample N-dimensional image by local averaging. + + The image is padded with zeros if it is not perfectly divisible by integer + factors. + + In contrast to the 2-D interpolation in `skimage.transform.resize` and + `skimage.transform.rescale` this function may be applied to N-dimensional + images and calculates the local mean of elements in each block of size + `factors` in the input image. + + Parameters + ---------- + image : ndarray + N-dimensional input image. + factors : array_like + Array containing down-sampling integer factor along each axis. + + Returns + ------- + image : ndarray + Down-sampled image with same number of dimensions as input image. + + Example + ------- + >>> a = np.arange(15).reshape(3, 5) + >>> a + array([[ 0, 1, 2, 3, 4], + [ 5, 6, 7, 8, 9], + [10, 11, 12, 13, 14]]) + >>> downscale_local_means(a, (2, 3)) + array([[3.5, 4.], + [5.5, 4.5]]) + + """ + return _block_func(image, factors, np.mean) + + def _swirl_mapping(xy, center, rotation, strength, radius): x, y = xy.T x0, y0 = center @@ -296,90 +333,3 @@ def swirl(image, center=None, strength=1, radius=100, rotation=0, return warp(image, _swirl_mapping, map_args=warp_args, output_shape=output_shape, order=order, mode=mode, cval=cval) - - -def _downsample(array, factors, sum=True): - """Performs downsampling with integer factors. - - Parameters - ---------- - array : ndarray - Input n-dimensional array. - factors: tuple - Tuple containing downsampling factor along each axis. - sum : bool - If True, downsampled element is the sum of its corresponding - constituent elements in the input array. Default is True. - - Returns - ------- - array : ndarray - Downsampled array with same number of dimensions as that of input - array. - - """ - - pad_size = [] - if len(factors) != array.ndim: - raise ValueError("'factors' must have the same length " - "as 'array.shape'") - else: - for i in range(len(factors)): - if array.shape[i] % factors[i] != 0: - pad_size.append(factors[i] - (array.shape[i] % factors[i])) - else: - pad_size.append(0) - - for i in range(len(pad_size)): - array = _pad_asymmetric_zeros(array, pad_size[i], i) - - out = view_as_blocks(array, factors) - block_shape = out.shape - - if sum: - for i in range(len(block_shape) // 2): - out = out.sum(-1) - else: - for i in range(len(block_shape) // 2): - out = out.mean(-1) - return out - - -def downscale_local_means(array, factors): - """Downsamples the array in blocks of input integer factors after padding - the original array with zeroes if the dimensions are not perfectly - divisible by factors and replaces it with mean i.e. average value. - - This function is different from resize and rescale in the sense that they - use interpolation to upsample or downsample on a 2D array, while this - function performs only dawnsampling but on any n-dimensional array and - returns the arithmetic mean of elements in a block of size factors in the - original array. - - Parameters - ---------- - array : ndarray - Input n-dimensional array. - factors: tuple - Tuple containing integer values representing block length along each - axis. - - Returns - ------- - array : ndarray - Downsampled array with same number of dimensions as that of input - array. - - Example - ------- - >>> a = np.arange(15).reshape(3, 5) - >>> a - array([[ 0, 1, 2, 3, 4], - [ 5, 6, 7, 8, 9], - [10, 11, 12, 13, 14]]) - >>> downscale_local_means(a, (2,3)) - array([[3.5, 4.], - [5.5, 4.5]]) - - """ - return _downsample(array, factors, False) diff --git a/skimage/transform/tests/test_warps.py b/skimage/transform/tests/test_warps.py index 88ede530..272be66e 100644 --- a/skimage/transform/tests/test_warps.py +++ b/skimage/transform/tests/test_warps.py @@ -6,7 +6,7 @@ from skimage.transform import (warp, warp_coords, rotate, resize, rescale, AffineTransform, ProjectiveTransform, SimilarityTransform, - downscale_local_means) + downscale_local_mean) from skimage import transform as tf, data, img_as_float from skimage.color import rgb2gray @@ -195,15 +195,15 @@ def test_warp_coords_example(): map_coordinates(image[:, :, 0], coords[:2]) -def test_downscale_local_means(): +def test_downscale_local_mean(): """Verifying downsampling of an array with expected result in mean mode""" image1 = np.arange(4*6).reshape(4, 6) - out1 = downscale_local_means(image1, (2, 3)) + out1 = downscale_local_mean(image1, (2, 3)) expected1 = np.array([[ 4., 7.], [ 16., 19.]]) assert_array_equal(expected1, out1) image2 = np.arange(5*8).reshape(5, 8) - out2 = downscale_local_means(image2, (4, 5)) + out2 = downscale_local_mean(image2, (4, 5)) expected2 = np.array([[ 14. , 10.8], [ 8.5, 5.7]]) assert_array_equal(expected2, out2) From f9546d0d1447c1eb3807cdcf72a66a222cb4472b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Fri, 5 Jul 2013 09:07:02 +0200 Subject: [PATCH 190/736] Fix import namespace --- skimage/measure/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/skimage/measure/__init__.py b/skimage/measure/__init__.py index 4c44dc2f..021a6edf 100755 --- a/skimage/measure/__init__.py +++ b/skimage/measure/__init__.py @@ -16,4 +16,8 @@ __all__ = ['find_contours', 'CircleModel', 'EllipseModel', 'ransac', - 'sum_blocks'] + 'block_sum', + 'block_mean', + 'block_median', + 'block_min', + 'block_max'] From 5579aa22985299ff7dd35de37eb6756993c9964f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Fri, 5 Jul 2013 09:13:50 +0200 Subject: [PATCH 191/736] Fix doc string examples --- skimage/measure/blocks.py | 32 +++++++++++++++----------------- skimage/transform/_warps.py | 2 +- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/skimage/measure/blocks.py b/skimage/measure/blocks.py index 428ecf11..0e45cde2 100644 --- a/skimage/measure/blocks.py +++ b/skimage/measure/blocks.py @@ -70,7 +70,7 @@ def block_sum(image, block_size): image([[ 0, 1, 2, 3, 4], [ 5, 6, 7, 8, 9], [10, 11, 12, 13, 14]]) - >>> sum_blocks(a, (2, 3)) + >>> block_sum(a, (2, 3)) image([[21, 24], [33, 27]]) @@ -103,9 +103,9 @@ def block_mean(image, block_size): image([[ 0, 1, 2, 3, 4], [ 5, 6, 7, 8, 9], [10, 11, 12, 13, 14]]) - >>> sum_blocks(a, (2, 3)) - image([[21, 24], - [33, 27]]) + >>> block_mean(a, (2, 3)) + array([[ 3.5, 4. ], + [ 5.5, 4.5]]) """ return _block_func(image, block_size, np.mean) @@ -131,14 +131,12 @@ def block_median(image, block_size): Example ------- - >>> a = np.arange(15).reshape(3, 5) + >>> a = np.array([[1, 5, 100], [0, 5, 1000]]) >>> a - image([[ 0, 1, 2, 3, 4], - [ 5, 6, 7, 8, 9], - [10, 11, 12, 13, 14]]) - >>> sum_blocks(a, (2, 3)) - image([[21, 24], - [33, 27]]) + array([[ 1, 5, 100], + [ 0, 5, 1000]]) + >>> block_median(a, (2, 3)) + array([[ 5.]]) """ return _block_func(image, block_size, np.median) @@ -169,9 +167,9 @@ def block_min(image, block_size): image([[ 0, 1, 2, 3, 4], [ 5, 6, 7, 8, 9], [10, 11, 12, 13, 14]]) - >>> sum_blocks(a, (2, 3)) - image([[21, 24], - [33, 27]]) + >>> block_min(a, (2, 2)) + array([[0, 2, 0], + [0, 0, 0]]) """ return _block_func(image, block_size, np.min) @@ -202,9 +200,9 @@ def block_max(image, block_size): image([[ 0, 1, 2, 3, 4], [ 5, 6, 7, 8, 9], [10, 11, 12, 13, 14]]) - >>> sum_blocks(a, (2, 3)) - image([[21, 24], - [33, 27]]) + >>> block_max(a, (2, 3)) + array([[ 7, 9], + [12, 14]]) """ return _block_func(image, block_size, np.max) diff --git a/skimage/transform/_warps.py b/skimage/transform/_warps.py index cefe61a3..c3d918ee 100644 --- a/skimage/transform/_warps.py +++ b/skimage/transform/_warps.py @@ -255,7 +255,7 @@ def downscale_local_mean(image, factors): array([[ 0, 1, 2, 3, 4], [ 5, 6, 7, 8, 9], [10, 11, 12, 13, 14]]) - >>> downscale_local_means(a, (2, 3)) + >>> downscale_local_mean(a, (2, 3)) array([[3.5, 4.], [5.5, 4.5]]) From c0a019eb12fdcbe28ebd5668247d2f366b26a7de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Fri, 5 Jul 2013 09:28:28 +0200 Subject: [PATCH 192/736] Update test cases for block functions --- skimage/measure/tests/test_blocks.py | 63 ++++++++++++++++++++++++++- skimage/transform/tests/test_warps.py | 6 +-- 2 files changed, 65 insertions(+), 4 deletions(-) diff --git a/skimage/measure/tests/test_blocks.py b/skimage/measure/tests/test_blocks.py index 77dc2b11..11894585 100644 --- a/skimage/measure/tests/test_blocks.py +++ b/skimage/measure/tests/test_blocks.py @@ -1,6 +1,7 @@ import numpy as np from numpy.testing import assert_array_equal -from skimage.measure import block_sum +from skimage.measure import (block_sum, block_mean, block_median, block_min, + block_max) def test_block_sum(): @@ -17,5 +18,65 @@ def test_block_sum(): assert_array_equal(expected2, out2) +def test_block_mean(): + image1 = np.arange(4 * 6).reshape(4, 6) + out1 = block_mean(image1, (2, 3)) + expected1 = np.array([[ 4., 7.], + [ 16., 19.]]) + assert_array_equal(expected1, out1) + + image2 = np.arange(5 * 8).reshape(5, 8) + out2 = block_mean(image2, (4, 5)) + expected2 = np.array([[14. , 10.8], + [ 8.5, 5.7]]) + assert_array_equal(expected2, out2) + + +def test_block_median(): + image1 = np.arange(4 * 6).reshape(4, 6) + out1 = block_median(image1, (2, 3)) + expected1 = np.array([[ 4., 7.], + [ 16., 19.]]) + assert_array_equal(expected1, out1) + + image2 = np.arange(5 * 8).reshape(5, 8) + out2 = block_median(image2, (4, 5)) + expected2 = np.array([[ 14., 17.], + [ 0., 0.]]) + assert_array_equal(expected2, out2) + + image3 = np.array([[1, 5, 5, 5], [5, 5, 5, 1000]]) + out3 = block_median(image3, (2, 4)) + assert_array_equal(5, out3) + + +def test_block_min(): + image1 = np.arange(4 * 6).reshape(4, 6) + out1 = block_min(image1, (2, 3)) + expected1 = np.array([[ 0, 3], + [12, 15]]) + assert_array_equal(expected1, out1) + + image2 = np.arange(5 * 8).reshape(5, 8) + out2 = block_min(image2, (4, 5)) + expected2 = np.array([[0, 0], + [0, 0]]) + assert_array_equal(expected2, out2) + + +def test_block_max(): + image1 = np.arange(4 * 6).reshape(4, 6) + out1 = block_max(image1, (2, 3)) + expected1 = np.array([[ 8, 11], + [20, 23]]) + assert_array_equal(expected1, out1) + + image2 = np.arange(5 * 8).reshape(5, 8) + out2 = block_max(image2, (4, 5)) + expected2 = np.array([[28, 31], + [36, 39]]) + assert_array_equal(expected2, out2) + + if __name__ == "__main__": np.testing.run_module_suite() diff --git a/skimage/transform/tests/test_warps.py b/skimage/transform/tests/test_warps.py index 272be66e..af2b95da 100644 --- a/skimage/transform/tests/test_warps.py +++ b/skimage/transform/tests/test_warps.py @@ -196,13 +196,13 @@ def test_warp_coords_example(): def test_downscale_local_mean(): - """Verifying downsampling of an array with expected result in mean mode""" - image1 = np.arange(4*6).reshape(4, 6) + image1 = np.arange(4 * 6).reshape(4, 6) out1 = downscale_local_mean(image1, (2, 3)) expected1 = np.array([[ 4., 7.], [ 16., 19.]]) assert_array_equal(expected1, out1) - image2 = np.arange(5*8).reshape(5, 8) + + image2 = np.arange(5 * 8).reshape(5, 8) out2 = downscale_local_mean(image2, (4, 5)) expected2 = np.array([[ 14. , 10.8], [ 8.5, 5.7]]) From 9d806eb413942bafa23e675ff912dd0a27764a9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Fri, 5 Jul 2013 09:32:06 +0200 Subject: [PATCH 193/736] Update doc string for new functionality --- skimage/measure/blocks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/measure/blocks.py b/skimage/measure/blocks.py index 0e45cde2..abeef10f 100644 --- a/skimage/measure/blocks.py +++ b/skimage/measure/blocks.py @@ -3,7 +3,7 @@ from ..util.shape import view_as_blocks, _pad_asymmetric_zeros def _block_func(image, factors, func): - """Down-sample image by integer factors. + """Down-sample image by applying function to local blocks. Parameters ---------- From 4f6b39dcd3fe85ee2453f3ba35fa84ad9ee68550 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Fri, 5 Jul 2013 14:19:10 +0200 Subject: [PATCH 194/736] Rename block_* functions to local_* --- skimage/measure/__init__.py | 12 +++---- skimage/measure/{blocks.py => local.py} | 22 ++++++------ .../tests/{test_blocks.py => test_local.py} | 36 +++++++++---------- skimage/transform/_warps.py | 4 +-- 4 files changed, 37 insertions(+), 37 deletions(-) rename skimage/measure/{blocks.py => local.py} (90%) rename skimage/measure/tests/{test_blocks.py => test_local.py} (73%) diff --git a/skimage/measure/__init__.py b/skimage/measure/__init__.py index 021a6edf..26eb179c 100755 --- a/skimage/measure/__init__.py +++ b/skimage/measure/__init__.py @@ -3,7 +3,7 @@ from ._regionprops import regionprops, perimeter from ._structural_similarity import structural_similarity from ._polygon import approximate_polygon, subdivide_polygon from .fit import LineModel, CircleModel, EllipseModel, ransac -from .blocks import block_sum, block_median, block_mean, block_min, block_max +from .local import local_sum, local_mean, local_median, local_min, local_max __all__ = ['find_contours', @@ -16,8 +16,8 @@ __all__ = ['find_contours', 'CircleModel', 'EllipseModel', 'ransac', - 'block_sum', - 'block_mean', - 'block_median', - 'block_min', - 'block_max'] + 'local_sum', + 'local_mean', + 'local_median', + 'local_min', + 'local_max'] diff --git a/skimage/measure/blocks.py b/skimage/measure/local.py similarity index 90% rename from skimage/measure/blocks.py rename to skimage/measure/local.py index abeef10f..3f628a53 100644 --- a/skimage/measure/blocks.py +++ b/skimage/measure/local.py @@ -2,7 +2,7 @@ import numpy as np from ..util.shape import view_as_blocks, _pad_asymmetric_zeros -def _block_func(image, factors, func): +def _local_func(image, factors, func): """Down-sample image by applying function to local blocks. Parameters @@ -45,7 +45,7 @@ def _block_func(image, factors, func): return out -def block_sum(image, block_size): +def local_sum(image, block_size): """Sum elements in local blocks. The image is padded with zeros if it is not perfectly divisible by integer @@ -75,10 +75,10 @@ def block_sum(image, block_size): [33, 27]]) """ - return _block_func(image, block_size, np.sum) + return _local_func(image, block_size, np.sum) -def block_mean(image, block_size): +def local_mean(image, block_size): """Average elements in local blocks. The image is padded with zeros if it is not perfectly divisible by integer @@ -108,10 +108,10 @@ def block_mean(image, block_size): [ 5.5, 4.5]]) """ - return _block_func(image, block_size, np.mean) + return _local_func(image, block_size, np.mean) -def block_median(image, block_size): +def local_median(image, block_size): """Median element in local blocks. The image is padded with zeros if it is not perfectly divisible by integer @@ -139,10 +139,10 @@ def block_median(image, block_size): array([[ 5.]]) """ - return _block_func(image, block_size, np.median) + return _local_func(image, block_size, np.median) -def block_min(image, block_size): +def local_min(image, block_size): """Minimum element in local blocks. The image is padded with zeros if it is not perfectly divisible by integer @@ -172,10 +172,10 @@ def block_min(image, block_size): [0, 0, 0]]) """ - return _block_func(image, block_size, np.min) + return _local_func(image, block_size, np.min) -def block_max(image, block_size): +def local_max(image, block_size): """Maximum element in local blocks. The image is padded with zeros if it is not perfectly divisible by integer @@ -205,4 +205,4 @@ def block_max(image, block_size): [12, 14]]) """ - return _block_func(image, block_size, np.max) + return _local_func(image, block_size, np.max) diff --git a/skimage/measure/tests/test_blocks.py b/skimage/measure/tests/test_local.py similarity index 73% rename from skimage/measure/tests/test_blocks.py rename to skimage/measure/tests/test_local.py index 11894585..fc01b7b9 100644 --- a/skimage/measure/tests/test_blocks.py +++ b/skimage/measure/tests/test_local.py @@ -1,78 +1,78 @@ import numpy as np from numpy.testing import assert_array_equal -from skimage.measure import (block_sum, block_mean, block_median, block_min, - block_max) +from skimage.measure import (local_sum, local_mean, local_median, local_min, + local_max) -def test_block_sum(): +def test_local_sum(): image1 = np.arange(4 * 6).reshape(4, 6) - out1 = block_sum(image1, (2, 3)) + out1 = local_sum(image1, (2, 3)) expected1 = np.array([[ 24, 42], [ 96, 114]]) assert_array_equal(expected1, out1) image2 = np.arange(5 * 8).reshape(5, 8) - out2 = block_sum(image2, (3, 3)) + out2 = local_sum(image2, (3, 3)) expected2 = np.array([[ 81, 108, 87], [174, 192, 138]]) assert_array_equal(expected2, out2) -def test_block_mean(): +def test_local_mean(): image1 = np.arange(4 * 6).reshape(4, 6) - out1 = block_mean(image1, (2, 3)) + out1 = local_mean(image1, (2, 3)) expected1 = np.array([[ 4., 7.], [ 16., 19.]]) assert_array_equal(expected1, out1) image2 = np.arange(5 * 8).reshape(5, 8) - out2 = block_mean(image2, (4, 5)) + out2 = local_mean(image2, (4, 5)) expected2 = np.array([[14. , 10.8], [ 8.5, 5.7]]) assert_array_equal(expected2, out2) -def test_block_median(): +def test_local_median(): image1 = np.arange(4 * 6).reshape(4, 6) - out1 = block_median(image1, (2, 3)) + out1 = local_median(image1, (2, 3)) expected1 = np.array([[ 4., 7.], [ 16., 19.]]) assert_array_equal(expected1, out1) image2 = np.arange(5 * 8).reshape(5, 8) - out2 = block_median(image2, (4, 5)) + out2 = local_median(image2, (4, 5)) expected2 = np.array([[ 14., 17.], [ 0., 0.]]) assert_array_equal(expected2, out2) image3 = np.array([[1, 5, 5, 5], [5, 5, 5, 1000]]) - out3 = block_median(image3, (2, 4)) + out3 = local_median(image3, (2, 4)) assert_array_equal(5, out3) -def test_block_min(): +def test_local_min(): image1 = np.arange(4 * 6).reshape(4, 6) - out1 = block_min(image1, (2, 3)) + out1 = local_min(image1, (2, 3)) expected1 = np.array([[ 0, 3], [12, 15]]) assert_array_equal(expected1, out1) image2 = np.arange(5 * 8).reshape(5, 8) - out2 = block_min(image2, (4, 5)) + out2 = local_min(image2, (4, 5)) expected2 = np.array([[0, 0], [0, 0]]) assert_array_equal(expected2, out2) -def test_block_max(): +def test_local_max(): image1 = np.arange(4 * 6).reshape(4, 6) - out1 = block_max(image1, (2, 3)) + out1 = local_max(image1, (2, 3)) expected1 = np.array([[ 8, 11], [20, 23]]) assert_array_equal(expected1, out1) image2 = np.arange(5 * 8).reshape(5, 8) - out2 = block_max(image2, (4, 5)) + out2 = local_max(image2, (4, 5)) expected2 = np.array([[28, 31], [36, 39]]) assert_array_equal(expected2, out2) diff --git a/skimage/transform/_warps.py b/skimage/transform/_warps.py index c3d918ee..6511182b 100644 --- a/skimage/transform/_warps.py +++ b/skimage/transform/_warps.py @@ -2,7 +2,7 @@ import numpy as np from scipy import ndimage from ._geometric import warp, SimilarityTransform, AffineTransform -from ..measure.blocks import _block_func +from ..measure.local import _local_func def resize(image, output_shape, order=1, mode='constant', cval=0.): @@ -260,7 +260,7 @@ def downscale_local_mean(image, factors): [5.5, 4.5]]) """ - return _block_func(image, factors, np.mean) + return _local_func(image, factors, np.mean) def _swirl_mapping(xy, center, rotation, strength, radius): From b7f72ff15fe20abb64a099ae99e7859a7acb1bbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Fri, 5 Jul 2013 14:36:25 +0200 Subject: [PATCH 195/736] Fix deprecated function name referneces in doc strings --- skimage/transform/_warps.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/skimage/transform/_warps.py b/skimage/transform/_warps.py index 6511182b..539e8b8d 100644 --- a/skimage/transform/_warps.py +++ b/skimage/transform/_warps.py @@ -10,7 +10,7 @@ def resize(image, output_shape, order=1, mode='constant', cval=0.): Performs interpolation to up-size or down-size images. For down-sampling N-dimensional images by applying the arithmetic sum or mean, see - `skimage.measure.sum_blocks` and `skimage.transform.downscale_local_means`, + `skimage.measure.local_sum` and `skimage.transform.downscale_local_mean`, respectively. Parameters @@ -96,8 +96,8 @@ def rescale(image, scale, order=1, mode='constant', cval=0.): Performs interpolation to upscale or down-scale images. For down-sampling N-dimensional images with integer factors by applying the arithmetic sum or - mean, see `skimage.measure.sum_blocks` and - `skimage.transform.downscale_local_means`, respectively. + mean, see `skimage.measure.local_sum` and + `skimage.transform.downscale_local_mean`, respectively. Parameters ---------- From 06aaf93e6389401e63d16f2742dff491def407d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Fri, 5 Jul 2013 17:34:19 +0200 Subject: [PATCH 196/736] Use pad function and add option to define cval --- skimage/measure/local.py | 88 ++++++++++++++++++++++--------------- skimage/transform/_warps.py | 16 ++++--- skimage/util/shape.py | 12 ----- 3 files changed, 63 insertions(+), 53 deletions(-) diff --git a/skimage/measure/local.py b/skimage/measure/local.py index 3f628a53..bcb587cf 100644 --- a/skimage/measure/local.py +++ b/skimage/measure/local.py @@ -1,19 +1,22 @@ import numpy as np -from ..util.shape import view_as_blocks, _pad_asymmetric_zeros +from skimage.util import view_as_blocks, pad -def _local_func(image, factors, func): +def _local_func(image, block_size, func, cval): """Down-sample image by applying function to local blocks. Parameters ---------- image : ndarray N-dimensional input image. - factors : array_like + block_size : array_like Array containing down-sampling integer factor along each axis. func : object Function object which is used to calculate the return value for each local block, e.g. `numpy.sum`. + cval : float, optional + Constant padding value if image is not perfectly divisible by the + block size. Returns ------- @@ -22,34 +25,34 @@ def _local_func(image, factors, func): """ - pad_size = [] - if len(factors) != image.ndim: - raise ValueError("`factors` must have the same length " + if len(block_size) != image.ndim: + raise ValueError("`block_size` must have the same length " "as `image.shape`.") - for i in range(len(factors)): - if image.shape[i] % factors[i] != 0: - pad_size.append(factors[i] - (image.shape[i] % factors[i])) + pad_width = [] + for i in range(len(block_size)): + if image.shape[i] % block_size[i] != 0: + after_width = block_size[i] - (image.shape[i] % block_size[i]) else: - pad_size.append(0) + after_width = 0 + pad_width.append((0, after_width)) - for i in range(len(pad_size)): - image = _pad_asymmetric_zeros(image, pad_size[i], i) + image = pad(image, pad_width=pad_width, mode='constant', + constant_values=cval) - out = view_as_blocks(image, factors) - block_shape = out.shape + out = view_as_blocks(image, block_size) - for i in range(len(block_shape) // 2): + for i in range(len(out.shape) // 2): out = func(out, axis=-1) return out -def local_sum(image, block_size): +def local_sum(image, block_size, cval=0): """Sum elements in local blocks. - The image is padded with zeros if it is not perfectly divisible by integer - factors. + The image is padded with zeros if it is not perfectly divisible by the + block size. Parameters ---------- @@ -57,6 +60,9 @@ def local_sum(image, block_size): N-dimensional input image. block_size : array_like Array containing down-sampling integer factor along each axis. + cval : float, optional + Constant padding value if image is not perfectly divisible by the + block size. Returns ------- @@ -75,14 +81,14 @@ def local_sum(image, block_size): [33, 27]]) """ - return _local_func(image, block_size, np.sum) + return _local_func(image, block_size, np.sum, cval) -def local_mean(image, block_size): +def local_mean(image, block_size, cval=0): """Average elements in local blocks. - The image is padded with zeros if it is not perfectly divisible by integer - factors. + The image is padded with zeros if it is not perfectly divisible by the + block size. Parameters ---------- @@ -90,6 +96,9 @@ def local_mean(image, block_size): N-dimensional input image. block_size : array_like Array containing down-sampling integer factor along each axis. + cval : float, optional + Constant padding value if image is not perfectly divisible by the + block size. Returns ------- @@ -108,14 +117,14 @@ def local_mean(image, block_size): [ 5.5, 4.5]]) """ - return _local_func(image, block_size, np.mean) + return _local_func(image, block_size, np.mean, cval) -def local_median(image, block_size): +def local_median(image, block_size, cval=0): """Median element in local blocks. - The image is padded with zeros if it is not perfectly divisible by integer - factors. + The image is padded with zeros if it is not perfectly divisible by the + block size. Parameters ---------- @@ -123,6 +132,9 @@ def local_median(image, block_size): N-dimensional input image. block_size : array_like Array containing down-sampling integer factor along each axis. + cval : float, optional + Constant padding value if image is not perfectly divisible by the + block size. Returns ------- @@ -139,14 +151,14 @@ def local_median(image, block_size): array([[ 5.]]) """ - return _local_func(image, block_size, np.median) + return _local_func(image, block_size, np.median, cval) -def local_min(image, block_size): +def local_min(image, block_size, cval=0): """Minimum element in local blocks. - The image is padded with zeros if it is not perfectly divisible by integer - factors. + The image is padded with zeros if it is not perfectly divisible by the + block size. Parameters ---------- @@ -154,6 +166,9 @@ def local_min(image, block_size): N-dimensional input image. block_size : array_like Array containing down-sampling integer factor along each axis. + cval : float, optional + Constant padding value if image is not perfectly divisible by the + block size. Returns ------- @@ -172,14 +187,14 @@ def local_min(image, block_size): [0, 0, 0]]) """ - return _local_func(image, block_size, np.min) + return _local_func(image, block_size, np.min, cval) -def local_max(image, block_size): +def local_max(image, block_size, cval=0): """Maximum element in local blocks. - The image is padded with zeros if it is not perfectly divisible by integer - factors. + The image is padded with zeros if it is not perfectly divisible by the + block size. Parameters ---------- @@ -187,6 +202,9 @@ def local_max(image, block_size): N-dimensional input image. block_size : array_like Array containing down-sampling integer factor along each axis. + cval : float, optional + Constant padding value if image is not perfectly divisible by the + block size. Returns ------- @@ -205,4 +223,4 @@ def local_max(image, block_size): [12, 14]]) """ - return _local_func(image, block_size, np.max) + return _local_func(image, block_size, np.max, cval) diff --git a/skimage/transform/_warps.py b/skimage/transform/_warps.py index 539e8b8d..e4463721 100644 --- a/skimage/transform/_warps.py +++ b/skimage/transform/_warps.py @@ -1,8 +1,9 @@ import numpy as np from scipy import ndimage -from ._geometric import warp, SimilarityTransform, AffineTransform -from ..measure.local import _local_func +from skimage.transform._geometric import (warp, SimilarityTransform, + AffineTransform) +from skimage.measure.local import _local_func def resize(image, output_shape, order=1, mode='constant', cval=0.): @@ -225,11 +226,11 @@ def rotate(image, angle, resize=False, order=1, mode='constant', cval=0.): mode=mode, cval=cval) -def downscale_local_mean(image, factors): +def downscale_local_mean(image, factors, cval=0): """Down-sample N-dimensional image by local averaging. - The image is padded with zeros if it is not perfectly divisible by integer - factors. + The image is padded with `cval` if it is not perfectly divisible by the + integer factors. In contrast to the 2-D interpolation in `skimage.transform.resize` and `skimage.transform.rescale` this function may be applied to N-dimensional @@ -242,6 +243,9 @@ def downscale_local_mean(image, factors): N-dimensional input image. factors : array_like Array containing down-sampling integer factor along each axis. + cval : float, optional + Constant padding value if image is not perfectly divisible by the + integer factors. Returns ------- @@ -260,7 +264,7 @@ def downscale_local_mean(image, factors): [5.5, 4.5]]) """ - return _local_func(image, factors, np.mean) + return _local_func(image, factors, np.mean, cval) def _swirl_mapping(xy, center, rotation, strength, radius): diff --git a/skimage/util/shape.py b/skimage/util/shape.py index 2763d3d4..0126d2e3 100644 --- a/skimage/util/shape.py +++ b/skimage/util/shape.py @@ -230,15 +230,3 @@ def view_as_windows(arr_in, window_shape): arr_out = as_strided(arr_in, shape=new_shape, strides=new_strides) return arr_out - - -def _pad_asymmetric_zeros(arr, pad_amt, axis=-1): - """Pads `arr` with zeros by `pad_amt` along specified `axis`""" - if axis == -1: - axis = arr.ndim - 1 - - zeroshape = tuple([x if i != axis else pad_amt - for (i, x) in enumerate(arr.shape)]) - - return np.concatenate((arr, np.zeros(zeroshape, dtype=arr.dtype)), - axis=axis) From 516d6efa5d65991e87a8b657d8d22cc005e35ab4 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sat, 6 Jul 2013 00:55:51 -0500 Subject: [PATCH 197/736] Fix Python 3 incompatibility (also, some wording tweaks) --- doc/examples/plot_local_binary_pattern.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/doc/examples/plot_local_binary_pattern.py b/doc/examples/plot_local_binary_pattern.py index c97a792d..6789b238 100644 --- a/doc/examples/plot_local_binary_pattern.py +++ b/doc/examples/plot_local_binary_pattern.py @@ -120,10 +120,11 @@ plt.gray() titles = ('edge', 'flat', 'corner') w = width = radius - 1 edge_labels = range(n_points // 2 - w, n_points // 2 + w + 1) -flat_labels = range(0, w + 1) + range(n_points - w, n_points + 2) +flat_labels = list(range(0, w + 1)) + list(range(n_points - w, n_points + 2)) i_14 = n_points // 4 # 1/4th of the histogram i_34 = 3 * (n_points // 4) # 3/4th of the histogram -corner_labels = range(i_14 - w, i_14 + w + 1) + range(i_34 - w, i_34 + w + 1) +corner_labels = (list(range(i_14 - w, i_14 + w + 1)) + + list(range(i_34 - w, i_34 + w + 1))) label_sets = (edge_labels, flat_labels, corner_labels) @@ -148,9 +149,9 @@ for ax in ax_img: The above plot highlights flat, edge-like, and corner-like regions of the image. -The histogram of the LBP result is a good measure to classify textures. For -simplicity the histogram distributions are then tested against each other using -the Kullback-Leibler-Divergence. +The histogram of the LBP result is a good measure to classify textures. Here, +we test the histogram distributions against each other using the +Kullback-Leibler-Divergence. """ # settings for LBP From 2dfeab83df44057cfd9115afe41c46d1879c4f52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Sat, 6 Jul 2013 13:49:01 +0200 Subject: [PATCH 198/736] PEP8 + import --- skimage/transform/tests/test_hough_transform.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/skimage/transform/tests/test_hough_transform.py b/skimage/transform/tests/test_hough_transform.py index 344dbea0..ae0ae81b 100644 --- a/skimage/transform/tests/test_hough_transform.py +++ b/skimage/transform/tests/test_hough_transform.py @@ -1,5 +1,7 @@ import numpy as np -from numpy.testing import * +from numpy.testing import (assert_almost_equal, + assert_equal, + ) import skimage.transform as tf from skimage.draw import line, circle_perimeter, ellipse_perimeter @@ -167,16 +169,17 @@ def test_hough_ellipse_non_zero_angle(): b = 9 x0 = 10 y0 = 10 - angle = np.pi/1.35 + angle = np.pi / 1.35 rr, cc = ellipse_perimeter(x0, x0, b, a, orientation=angle) img[rr, cc] = 1 result = tf.hough_ellipse(img, threshold=15, accuracy=3) - print(result) - assert_almost_equal(result[0][0]/100., x0/100., decimal=1) - assert_almost_equal(result[0][1]/100., y0/100., decimal=1) - assert_almost_equal(result[0][2]/100., b/100., decimal=1) - assert_almost_equal(result[0][3]/100., a/100., decimal=1) + assert_almost_equal(result[0][0] / 100., x0 / 100., decimal=1) + assert_almost_equal(result[0][1] / 100., y0 / 100., decimal=1) + assert_almost_equal(result[0][2] / 100., b / 100., decimal=1) + assert_almost_equal(result[0][3] / 100., a / 100., decimal=1) assert_almost_equal(result[0][4], angle, decimal=1) + if __name__ == "__main__": + from numpy.testing import run_module_suite run_module_suite() From 73a39c8e2e8de37fa08f8810590f44fec278a5f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Sat, 6 Jul 2013 14:02:16 +0200 Subject: [PATCH 199/736] activate travis for a fork --- CONTRIBUTING.txt | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CONTRIBUTING.txt b/CONTRIBUTING.txt index 38ec8e4b..6c5c8b58 100644 --- a/CONTRIBUTING.txt +++ b/CONTRIBUTING.txt @@ -154,6 +154,19 @@ detailing the test coverage:: skimage/filter/__init__ 1 1 100% ... +Activate Travis for your fork (optional) +---------------------------------------- + +Travis checks all unittests in the project to prevent breakage. + +Before sending a pull request, you may want to check that Travis successfully +passes all tests. To do so, you can follow step one and two in +`Travis documentation __ +(Step three is already done in scikit-image). + +Thus, as soon as you push your code to your fork, it will triger Travis, +and you will receive an email notification when the process is done. + Bugs ---- From 71c0c2ce5832ebc40249e0be6dd9161127642237 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Sat, 6 Jul 2013 20:11:44 +0800 Subject: [PATCH 200/736] Added an example in skimage.feature._brief --- skimage/feature/_brief.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/skimage/feature/_brief.py b/skimage/feature/_brief.py index 23dc63ab..1637ea5e 100644 --- a/skimage/feature/_brief.py +++ b/skimage/feature/_brief.py @@ -49,6 +49,19 @@ def brief(image, keypoints, descriptor_size=256, mode='normal', patch_size=49, "BRIEF : Binary robust independent elementary features", http://cvlabwww.epfl.ch/~lepetit/papers/calonder_eccv10.pdf + Examples + -------- + >>> from skimage.data import camera + >>> from skimage.feature._brief import * + >>> from skimage.feature.corner import * + >>> img = camera() + >>> keypoints = corner_peaks(corner_harris(img), min_distance=10) + >>> keypoints.shape + (556, 2) + >>> descriptors = brief(img, keypoints) + >>> descriptors.shape + (489, 256) + """ np.random.seed(sample_seed) From e74b9713351cb5eea80a2e8ed4e250583bd5a7e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Sat, 6 Jul 2013 14:30:17 +0200 Subject: [PATCH 201/736] erode & dilate -> private functions --- skimage/morphology/cmorph.pyx | 4 ++-- skimage/morphology/grey.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/skimage/morphology/cmorph.pyx b/skimage/morphology/cmorph.pyx index a09a39a3..05dba333 100644 --- a/skimage/morphology/cmorph.pyx +++ b/skimage/morphology/cmorph.pyx @@ -8,7 +8,7 @@ cimport numpy as np from libc.stdlib cimport malloc, free -def dilate(np.ndarray[np.uint8_t, ndim=2] image, +def _dilate(np.ndarray[np.uint8_t, ndim=2] image, np.ndarray[np.uint8_t, ndim=2] selem, np.ndarray[np.uint8_t, ndim=2] out=None, char shift_x=0, char shift_y=0): @@ -63,7 +63,7 @@ def dilate(np.ndarray[np.uint8_t, ndim=2] image, return out -def erode(np.ndarray[np.uint8_t, ndim=2] image, +def _erode(np.ndarray[np.uint8_t, ndim=2] image, np.ndarray[np.uint8_t, ndim=2] selem, np.ndarray[np.uint8_t, ndim=2] out=None, char shift_x=0, char shift_y=0): diff --git a/skimage/morphology/grey.py b/skimage/morphology/grey.py index 2cca69b6..233bca4a 100644 --- a/skimage/morphology/grey.py +++ b/skimage/morphology/grey.py @@ -58,7 +58,7 @@ def erosion(image, selem, out=None, shift_x=False, shift_y=False): raise NotImplementedError("In-place erosion not supported!") image = img_as_ubyte(image) selem = img_as_ubyte(selem) - return cmorph.erode(image, selem, out=out, + return cmorph._erode(image, selem, out=out, shift_x=shift_x, shift_y=shift_y) @@ -111,7 +111,7 @@ def dilation(image, selem, out=None, shift_x=False, shift_y=False): raise NotImplementedError("In-place dilation not supported!") image = img_as_ubyte(image) selem = img_as_ubyte(selem) - return cmorph.dilate(image, selem, out=out, + return cmorph._dilate(image, selem, out=out, shift_x=shift_x, shift_y=shift_y) From 50ac1e89af799de23d8c37e3f0b24a3dfad75f0b Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Sat, 6 Jul 2013 20:34:32 +0800 Subject: [PATCH 202/736] Fixing bug in _remove_border_keypoints --- skimage/feature/util.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/skimage/feature/util.py b/skimage/feature/util.py index b634305b..c77421d7 100644 --- a/skimage/feature/util.py +++ b/skimage/feature/util.py @@ -7,10 +7,10 @@ def _remove_border_keypoints(image, keypoints, dist): width = image.shape[0] height = image.shape[1] - keypoints = keypoints[(dist < keypoints[:, 0]) - & (keypoints[:, 0] < width - dist) - & (dist < keypoints[:, 1]) - & (keypoints[:, 1] < height - dist)] + keypoints = keypoints[(dist - 1 < keypoints[:, 0]) + & (keypoints[:, 0] < width - dist + 1) + & (dist - 1 < keypoints[:, 1]) + & (keypoints[:, 1] < height - dist + 1)] return keypoints From f316abbfbedcfe5292e0bf0d53ad5265aec03428 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Sat, 6 Jul 2013 14:34:55 +0200 Subject: [PATCH 203/736] DOC: add doctrings --- skimage/morphology/cmorph.pyx | 48 +++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/skimage/morphology/cmorph.pyx b/skimage/morphology/cmorph.pyx index 05dba333..2e0fa88a 100644 --- a/skimage/morphology/cmorph.pyx +++ b/skimage/morphology/cmorph.pyx @@ -12,7 +12,30 @@ def _dilate(np.ndarray[np.uint8_t, ndim=2] image, np.ndarray[np.uint8_t, ndim=2] selem, np.ndarray[np.uint8_t, ndim=2] out=None, char shift_x=0, char shift_y=0): + """Return greyscale morphological erosion of an image. + Morphological erosion sets a pixel at (i,j) to the minimum over all pixels + in the neighborhood centered at (i,j). Erosion shrinks bright regions and + enlarges dark regions. + + Parameters + ---------- + image : ndarray + Image array. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + The array to store the result of the morphology. If None is + passed, a new array will be allocated. + shift_x, shift_y : bool + shift structuring element about center point. This only affects + eccentric structuring elements (i.e. selem with even numbered sides). + + Returns + ------- + eroded : uint8 array + The result of the morphological erosion. + """ cdef Py_ssize_t rows = image.shape[0] cdef Py_ssize_t cols = image.shape[1] cdef Py_ssize_t srows = selem.shape[0] @@ -67,6 +90,31 @@ def _erode(np.ndarray[np.uint8_t, ndim=2] image, np.ndarray[np.uint8_t, ndim=2] selem, np.ndarray[np.uint8_t, ndim=2] out=None, char shift_x=0, char shift_y=0): + """Return greyscale morphological dilation of an image. + + Morphological dilation sets a pixel at (i,j) to the maximum over all pixels + in the neighborhood centered at (i,j). Dilation enlarges bright regions + and shrinks dark regions. + + Parameters + ---------- + + image : ndarray + Image array. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + The array to store the result of the morphology. If None, is + passed, a new array will be allocated. + shift_x, shift_y : bool + shift structuring element about center point. This only affects + eccentric structuring elements (i.e. selem with even numbered sides). + + Returns + ------- + dilated : uint8 array + The result of the morphological dilation. + """ cdef Py_ssize_t rows = image.shape[0] cdef Py_ssize_t cols = image.shape[1] From de9d2662b1e4e8a74bfead39b84fa3d6bba2ce2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Sat, 6 Jul 2013 14:45:54 +0200 Subject: [PATCH 204/736] MIN: fix pep8 --- skimage/morphology/cmorph.pyx | 12 ++++++------ skimage/morphology/grey.py | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/skimage/morphology/cmorph.pyx b/skimage/morphology/cmorph.pyx index 2e0fa88a..070c881b 100644 --- a/skimage/morphology/cmorph.pyx +++ b/skimage/morphology/cmorph.pyx @@ -9,9 +9,9 @@ from libc.stdlib cimport malloc, free def _dilate(np.ndarray[np.uint8_t, ndim=2] image, - np.ndarray[np.uint8_t, ndim=2] selem, - np.ndarray[np.uint8_t, ndim=2] out=None, - char shift_x=0, char shift_y=0): + np.ndarray[np.uint8_t, ndim=2] selem, + np.ndarray[np.uint8_t, ndim=2] out=None, + char shift_x=0, char shift_y=0): """Return greyscale morphological erosion of an image. Morphological erosion sets a pixel at (i,j) to the minimum over all pixels @@ -87,9 +87,9 @@ def _dilate(np.ndarray[np.uint8_t, ndim=2] image, def _erode(np.ndarray[np.uint8_t, ndim=2] image, - np.ndarray[np.uint8_t, ndim=2] selem, - np.ndarray[np.uint8_t, ndim=2] out=None, - char shift_x=0, char shift_y=0): + np.ndarray[np.uint8_t, ndim=2] selem, + np.ndarray[np.uint8_t, ndim=2] out=None, + char shift_x=0, char shift_y=0): """Return greyscale morphological dilation of an image. Morphological dilation sets a pixel at (i,j) to the maximum over all pixels diff --git a/skimage/morphology/grey.py b/skimage/morphology/grey.py index 233bca4a..ef7158b2 100644 --- a/skimage/morphology/grey.py +++ b/skimage/morphology/grey.py @@ -59,7 +59,7 @@ def erosion(image, selem, out=None, shift_x=False, shift_y=False): image = img_as_ubyte(image) selem = img_as_ubyte(selem) return cmorph._erode(image, selem, out=out, - shift_x=shift_x, shift_y=shift_y) + shift_x=shift_x, shift_y=shift_y) def dilation(image, selem, out=None, shift_x=False, shift_y=False): @@ -112,7 +112,7 @@ def dilation(image, selem, out=None, shift_x=False, shift_y=False): image = img_as_ubyte(image) selem = img_as_ubyte(selem) return cmorph._dilate(image, selem, out=out, - shift_x=shift_x, shift_y=shift_y) + shift_x=shift_x, shift_y=shift_y) def opening(image, selem, out=None): From f932db666e187eecad711edcda26ccc91c320cc1 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Sat, 6 Jul 2013 20:51:01 +0800 Subject: [PATCH 205/736] Added more detailed example in docstrings --- skimage/feature/_brief.py | 57 ++++++++++++++++++++++++++++++++------- 1 file changed, 48 insertions(+), 9 deletions(-) diff --git a/skimage/feature/_brief.py b/skimage/feature/_brief.py index 1637ea5e..e782e008 100644 --- a/skimage/feature/_brief.py +++ b/skimage/feature/_brief.py @@ -51,16 +51,55 @@ def brief(image, keypoints, descriptor_size=256, mode='normal', patch_size=49, Examples -------- - >>> from skimage.data import camera - >>> from skimage.feature._brief import * >>> from skimage.feature.corner import * - >>> img = camera() - >>> keypoints = corner_peaks(corner_harris(img), min_distance=10) - >>> keypoints.shape - (556, 2) - >>> descriptors = brief(img, keypoints) - >>> descriptors.shape - (489, 256) + >>> from skimage.feature import brief, hamming_distance + >>> square1 = np.zeros([10, 10]) + >>> square1[2:8, 2:8] = 1 + >>> square1 + array([[ 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], + [ 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], + [ 0., 0., 1., 1., 1., 1., 1., 1., 0., 0.], + [ 0., 0., 1., 1., 1., 1., 1., 1., 0., 0.], + [ 0., 0., 1., 1., 1., 1., 1., 1., 0., 0.], + [ 0., 0., 1., 1., 1., 1., 1., 1., 0., 0.], + [ 0., 0., 1., 1., 1., 1., 1., 1., 0., 0.], + [ 0., 0., 1., 1., 1., 1., 1., 1., 0., 0.], + [ 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], + [ 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]]) + >>> keypoints1 = corner_peaks(corner_harris(square1), min_distance=1) + >>> keypoints1 + array([[2, 2], + [2, 7], + [7, 2], + [7, 7]]) + >>> descriptors1 = brief(square1, keypoints1, patch_size = 5) + >>> square2 = np.zeros([12, 12]) + >>> square2[3:9, 3:9] = 1 + >>> square2 + array([[ 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], + [ 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], + [ 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], + [ 0., 0., 0., 1., 1., 1., 1., 1., 1., 0., 0., 0.], + [ 0., 0., 0., 1., 1., 1., 1., 1., 1., 0., 0., 0.], + [ 0., 0., 0., 1., 1., 1., 1., 1., 1., 0., 0., 0.], + [ 0., 0., 0., 1., 1., 1., 1., 1., 1., 0., 0., 0.], + [ 0., 0., 0., 1., 1., 1., 1., 1., 1., 0., 0., 0.], + [ 0., 0., 0., 1., 1., 1., 1., 1., 1., 0., 0., 0.], + [ 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], + [ 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], + [ 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]]) + >>> keypoints2 = corner_peaks(corner_harris(square2), min_distance=1) + >>> keypoints2 + array([[3, 3], + [3, 8], + [8, 3], + [8, 8]]) + >>> descriptors2 = brief(square2, keypoints1, patch_size = 5) + >>> hamming_distance(descriptors1, descriptors2) + array([[ 0.02734375, 0.2890625 , 0.32421875, 0.6171875 ], + [ 0.3359375 , 0.05078125, 0.6640625 , 0.37109375], + [ 0.359375 , 0.64453125, 0.03125 , 0.33203125], + [ 0.640625 , 0.40234375, 0.3828125 , 0.01953125]]) """ From cf3efba3c1b4b793fc19cf22360d00cb88c71ba5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Sat, 6 Jul 2013 16:12:09 +0200 Subject: [PATCH 206/736] use previous import --- skimage/transform/tests/test_hough_transform.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/skimage/transform/tests/test_hough_transform.py b/skimage/transform/tests/test_hough_transform.py index ae0ae81b..148f4d43 100644 --- a/skimage/transform/tests/test_hough_transform.py +++ b/skimage/transform/tests/test_hough_transform.py @@ -181,5 +181,4 @@ def test_hough_ellipse_non_zero_angle(): if __name__ == "__main__": - from numpy.testing import run_module_suite - run_module_suite() + np.testing.run_module_suite() From 237fb989b0a76e6c67aa03d97952ef237527589c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Fri, 5 Jul 2013 14:05:06 +0200 Subject: [PATCH 207/736] radon: Use util.pad for array padding. --- skimage/transform/radon_transform.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/skimage/transform/radon_transform.py b/skimage/transform/radon_transform.py index 944b4a6b..dcb13cae 100644 --- a/skimage/transform/radon_transform.py +++ b/skimage/transform/radon_transform.py @@ -18,6 +18,8 @@ import numpy as np from scipy.fftpack import fftshift, fft, ifft from ._warps_cy import _warp_fast from ._radon_transform import sart_projection_update +from .. import util + __all__ = ["radon", "iradon", "iradon_sart"] @@ -77,20 +79,14 @@ def radon(image, theta=None, circle=False): dh = padded_image.shape[0] // 2 dw = padded_image.shape[1] // 2 else: - height, width = image.shape diagonal = np.sqrt(2) * max(image.shape) - heightpad = int(np.ceil(diagonal - height)) - widthpad = int(np.ceil(diagonal - width)) - padded_image = np.zeros((int(height + heightpad), - int(width + widthpad))) - y0 = heightpad // 2 - y1 = y0 + height - x0 = widthpad // 2 - x1 = x0 + width - padded_image[y0:y1, x0:x1] = image + pad = [int(np.ceil(diagonal - s)) for s in image.shape] + pad_width = [(p // 2, p - p // 2) for p in pad] + padded_image = util.pad(image, pad_width, mode='constant', + constant_values=0) out = np.zeros((max(padded_image.shape), len(theta))) - dh = y0 + height // 2 - dw = x0 + width // 2 + dh = pad[0] // 2 + image.shape[0] // 2 + dw = pad[1] // 2 + image.shape[1] // 2 shift0 = np.array([[1, 0, -dw], [0, 1, -dh], From ac460a9777303df3d4df17a06c1ec70b53122f68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Fri, 5 Jul 2013 14:13:41 +0200 Subject: [PATCH 208/736] iradon: Use util.pad for array padding. This depends on util.pad accepting 0s in pad_width, which has a proposed solution in gh-634. --- skimage/transform/radon_transform.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/skimage/transform/radon_transform.py b/skimage/transform/radon_transform.py index dcb13cae..93979977 100644 --- a/skimage/transform/radon_transform.py +++ b/skimage/transform/radon_transform.py @@ -109,11 +109,10 @@ def radon(image, theta=None, circle=False): def _sinogram_circle_to_square(sinogram): - size = int(np.ceil(np.sqrt(2) * sinogram.shape[0])) - sinogram_padded = np.zeros((size, sinogram.shape[1])) - pad = (size - sinogram.shape[0]) // 2 - sinogram_padded[pad:pad + sinogram.shape[0], :] = sinogram - return sinogram_padded + diagonal = int(np.ceil(np.sqrt(2) * sinogram.shape[0])) + pad = diagonal - sinogram.shape[0] + pad_width = ((pad // 2, pad - pad // 2), (0, 0)) + return util.pad(sinogram, pad_width, mode='constant', constant_values=0) def iradon(radon_image, theta=None, output_size=None, From 5955e4e612b7efdd3eafc0f105855d13959fff5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Fri, 5 Jul 2013 15:27:39 +0200 Subject: [PATCH 209/736] iradon: Reduce code duplication. --- skimage/transform/radon_transform.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/skimage/transform/radon_transform.py b/skimage/transform/radon_transform.py index 93979977..71548324 100644 --- a/skimage/transform/radon_transform.py +++ b/skimage/transform/radon_transform.py @@ -232,8 +232,6 @@ def iradon(radon_image, theta=None, output_size=None, k = np.round(mid_index + ypr * np.cos(th[i]) - xpr * np.sin(th[i])) backprojected = radon_filtered[ ((((k > 0) & (k < n)) * k) - 1).astype(np.int), i] - if circle: - backprojected[~reconstruction_circle] = 0. reconstructed += backprojected elif interpolation == "linear": for i in range(len(theta)): @@ -244,11 +242,11 @@ def iradon(radon_image, theta=None, output_size=None, b1 = ((((b > 0) & (b < n)) * b) - 1).astype(np.int) backprojected = (t - a) * radon_filtered[b0, i] + \ (a - t + 1) * radon_filtered[b1, i] - if circle: - backprojected[~reconstruction_circle] = 0. reconstructed += backprojected else: raise ValueError("Unknown interpolation: %s" % interpolation) + if circle: + reconstructed[~reconstruction_circle] = 0. return reconstructed * np.pi / (2 * len(th)) From 2be327815ebd6e8a6694e016871d9bcafe7ee55c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Sat, 6 Jul 2013 17:28:49 +0200 Subject: [PATCH 210/736] radon: Use numpy.interp/scipy.interpolate. Scipy supports all interpolation kinds (nearest, linear) we need, while numpy supports only linear interpolation. The numpy interpolation is substantially faster, so this is used even though it complicates the code slightly. --- skimage/transform/radon_transform.py | 41 ++++++++++++++-------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/skimage/transform/radon_transform.py b/skimage/transform/radon_transform.py index 71548324..b99da511 100644 --- a/skimage/transform/radon_transform.py +++ b/skimage/transform/radon_transform.py @@ -16,6 +16,7 @@ References: from __future__ import division import numpy as np from scipy.fftpack import fftshift, fft, ifft +from scipy.interpolate import interp1d from ._warps_cy import _warp_fast from ._radon_transform import sart_projection_update from .. import util @@ -137,9 +138,10 @@ def iradon(radon_image, theta=None, output_size=None, Filter used in frequency domain filtering. Ramp filter used by default. Filters available: ramp, shepp-logan, cosine, hamming, hann Assign None to use no filter. - interpolation : str, optional (default linear) + interpolation : str, optional (default 'linear') Interpolation method used in reconstruction. - Methods available: nearest, linear. + Methods available are the same as for `scipy.interpolate.interp1d: + 'linear', 'nearest', 'zero', 'slinear', 'quadratic' and 'cubic'. circle : boolean, optional Assume the reconstructed image is zero outside the inscribed circle. Also changes the default output_size to match the behaviour of @@ -167,6 +169,10 @@ def iradon(radon_image, theta=None, output_size=None, if len(theta) != radon_image.shape[1]: raise ValueError("The given ``theta`` does not match the number of " "projections in ``radon_image``.") + interpolation_types = ('linear', 'nearest', 'zero', 'slinear', + 'quadratic', 'cubic') + if not interpolation in interpolation_types: + raise ValueError("Unknown interpolation: %s" % interpolation) if not output_size: # If output size not specified, estimate from input radon image if circle: @@ -215,7 +221,7 @@ def iradon(radon_image, theta=None, output_size=None, # Determine the center of the projections (= center of sinogram) circle_size = int(np.floor(radon_image.shape[0] / np.sqrt(2))) square_size = radon_image.shape[0] - mid_index = (square_size - circle_size) // 2 + circle_size // 2 + 1 + mid_index = (square_size - circle_size) // 2 + circle_size // 2 x = output_size y = output_size @@ -227,24 +233,17 @@ def iradon(radon_image, theta=None, output_size=None, reconstruction_circle = (xpr**2 + ypr**2) < radius**2 # Reconstruct image by interpolation - if interpolation == "nearest": - for i in range(len(theta)): - k = np.round(mid_index + ypr * np.cos(th[i]) - xpr * np.sin(th[i])) - backprojected = radon_filtered[ - ((((k > 0) & (k < n)) * k) - 1).astype(np.int), i] - reconstructed += backprojected - elif interpolation == "linear": - for i in range(len(theta)): - t = ypr * np.cos(th[i]) - xpr * np.sin(th[i]) - a = np.floor(t) - b = mid_index + a - b0 = ((((b + 1 > 0) & (b + 1 < n)) * (b + 1)) - 1).astype(np.int) - b1 = ((((b > 0) & (b < n)) * b) - 1).astype(np.int) - backprojected = (t - a) * radon_filtered[b0, i] + \ - (a - t + 1) * radon_filtered[b1, i] - reconstructed += backprojected - else: - raise ValueError("Unknown interpolation: %s" % interpolation) + for i in range(len(theta)): + t = ypr * np.cos(th[i]) - xpr * np.sin(th[i]) + x = np.arange(radon_filtered.shape[0]) - mid_index + if interpolation == 'linear': + backprojected = np.interp(t, x, radon_filtered[:, i], + left=0, right=0) + else: + interpolant = interp1d(x, radon_filtered[:, i], kind=interpolation, + bounds_error=False, fill_value=0) + backprojected = interpolant(t) + reconstructed += backprojected if circle: reconstructed[~reconstruction_circle] = 0. From b90ba783ef5bc483078a2522cd21b683a0761b69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Sat, 6 Jul 2013 18:27:08 +0200 Subject: [PATCH 211/736] iradon: Only allow interpolation methods working well. Of the interpolation methods provided by scipy.interpolate.interp1d, only cubic has been found to work well with the tests in skimage (other methods are either identical to linear or nearest, or they produce bad reconstructions). --- skimage/transform/radon_transform.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/skimage/transform/radon_transform.py b/skimage/transform/radon_transform.py index b99da511..2f2a18e7 100644 --- a/skimage/transform/radon_transform.py +++ b/skimage/transform/radon_transform.py @@ -139,9 +139,8 @@ def iradon(radon_image, theta=None, output_size=None, Filters available: ramp, shepp-logan, cosine, hamming, hann Assign None to use no filter. interpolation : str, optional (default 'linear') - Interpolation method used in reconstruction. - Methods available are the same as for `scipy.interpolate.interp1d: - 'linear', 'nearest', 'zero', 'slinear', 'quadratic' and 'cubic'. + Interpolation method used in reconstruction. Methods available: + 'linear', 'nearest', and 'cubic' ('cubic' is slow). circle : boolean, optional Assume the reconstructed image is zero outside the inscribed circle. Also changes the default output_size to match the behaviour of @@ -169,8 +168,7 @@ def iradon(radon_image, theta=None, output_size=None, if len(theta) != radon_image.shape[1]: raise ValueError("The given ``theta`` does not match the number of " "projections in ``radon_image``.") - interpolation_types = ('linear', 'nearest', 'zero', 'slinear', - 'quadratic', 'cubic') + interpolation_types = ('linear', 'nearest', 'cubic') if not interpolation in interpolation_types: raise ValueError("Unknown interpolation: %s" % interpolation) if not output_size: From 643a1bc1f58dc6015446d97a63977c5f4d5a4c2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Sat, 6 Jul 2013 18:29:05 +0200 Subject: [PATCH 212/736] iradon: Add test for cubic interpolation. --- skimage/transform/tests/test_radon_transform.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/skimage/transform/tests/test_radon_transform.py b/skimage/transform/tests/test_radon_transform.py index 333c298d..1d82b29a 100644 --- a/skimage/transform/tests/test_radon_transform.py +++ b/skimage/transform/tests/test_radon_transform.py @@ -124,11 +124,11 @@ def check_radon_iradon(interpolation_type, filter_type): print('\n\tmean error:', delta) if debug: _debug_plot(image, reconstructed) - if filter_type == 'ramp': - if interpolation_type == 'linear': - allowed_delta = 0.02 - else: + if filter_type in ('ramp', 'shepp-logan'): + if interpolation_type == 'nearest': allowed_delta = 0.03 + else: + allowed_delta = 0.02 else: allowed_delta = 0.05 assert delta < allowed_delta @@ -136,11 +136,12 @@ def check_radon_iradon(interpolation_type, filter_type): def test_radon_iradon(): filter_types = ["ramp", "shepp-logan", "cosine", "hamming", "hann"] - interpolation_types = ["linear", "nearest"] + interpolation_types = ['linear', 'nearest'] for interpolation_type, filter_type in \ itertools.product(interpolation_types, filter_types): yield check_radon_iradon, interpolation_type, filter_type - + # cubic interpolation is slow; only run one test for it + yield check_radon_iradon, 'cubic', 'shepp-logan' def test_iradon_angles(): """ From b3746b90902dbb47d5da2d8500973c6f00392ea9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Sat, 6 Jul 2013 18:29:31 +0200 Subject: [PATCH 213/736] iradon: Cleanup by locating related code in one place. --- skimage/transform/radon_transform.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/skimage/transform/radon_transform.py b/skimage/transform/radon_transform.py index 2f2a18e7..dfac0748 100644 --- a/skimage/transform/radon_transform.py +++ b/skimage/transform/radon_transform.py @@ -226,9 +226,6 @@ def iradon(radon_image, theta=None, output_size=None, [X, Y] = np.mgrid[0.0:x, 0.0:y] xpr = X - int(output_size) // 2 ypr = Y - int(output_size) // 2 - if circle: - radius = (output_size - 1) // 2 - reconstruction_circle = (xpr**2 + ypr**2) < radius**2 # Reconstruct image by interpolation for i in range(len(theta)): @@ -243,6 +240,8 @@ def iradon(radon_image, theta=None, output_size=None, backprojected = interpolant(t) reconstructed += backprojected if circle: + radius = (output_size - 1) // 2 + reconstruction_circle = (xpr**2 + ypr**2) < radius**2 reconstructed[~reconstruction_circle] = 0. return reconstructed * np.pi / (2 * len(th)) From bea50aa6082846a0673fca6c8d9f6d46668b78eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Sat, 6 Jul 2013 18:55:06 +0200 Subject: [PATCH 214/736] iradon: use util.pad for sinogram padding. --- skimage/transform/radon_transform.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/skimage/transform/radon_transform.py b/skimage/transform/radon_transform.py index dfac0748..6009c54a 100644 --- a/skimage/transform/radon_transform.py +++ b/skimage/transform/radon_transform.py @@ -182,16 +182,15 @@ def iradon(radon_image, theta=None, output_size=None, radon_image = _sinogram_circle_to_square(radon_image) th = (np.pi / 180.0) * theta - n = radon_image.shape[0] - img = radon_image.copy() - # resize image to next power of two for fourier analysis - # speeds up fourier and lessens artifacts - order = max(64., 2**np.ceil(np.log(2 * n) / np.log(2))) - # zero pad input image - img.resize((order, img.shape[1])) + # resize image to next power of two (but no less than 64) for + # Fourier analysis; speeds up Fourier and lessens artifacts + projection_size_padded = \ + max(64, int(2**np.ceil(np.log2(2 * radon_image.shape[0])))) + pad_width = ((0, projection_size_padded - radon_image.shape[0]), (0, 0)) + img = util.pad(radon_image, pad_width, mode='constant', constant_values=0) # Construct the Fourier filter - f = fftshift(abs(np.mgrid[-1:1:2 / order])).reshape(-1, 1) + f = fftshift(abs(np.mgrid[-1:1:2 / projection_size_padded])).reshape(-1, 1) w = 2 * np.pi * f # Start from first element to avoid divide by zero if filter == "ramp": From 4f0c4f0306f52e1dac84a3feaa5c1f7967b2c78f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Sun, 7 Jul 2013 00:38:06 +0200 Subject: [PATCH 215/736] add coffee picture from flickr --- skimage/data/__init__.py | 15 +++++++++++++++ skimage/data/coffee.png | Bin 0 -> 305945 bytes skimage/data/tests/test_data.py | 5 +++++ 3 files changed, 20 insertions(+) create mode 100644 skimage/data/coffee.png diff --git a/skimage/data/__init__.py b/skimage/data/__init__.py index 864e3638..826ae75a 100644 --- a/skimage/data/__init__.py +++ b/skimage/data/__init__.py @@ -184,3 +184,18 @@ def chelsea(): """ return load("chelsea.png") + + +def coffee(): + """Coffee cup. + + An example with several shapes (including an ellipse). The background + is composed of stripes. + + Notes + ----- + This image was downloaded on + `Flickr `__ + No copyright restrictions. + """ + return load("coffee.png") diff --git a/skimage/data/coffee.png b/skimage/data/coffee.png new file mode 100644 index 0000000000000000000000000000000000000000..d7f38a3048e2b6dc7b0adf17d775e24d11ba0aac GIT binary patch literal 305945 zcmV)QK(xP!P)004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00006 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

JiDMgw;|Kr?PGD&$ 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

ygzgw_B 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 Date: Sun, 7 Jul 2013 00:38:49 +0200 Subject: [PATCH 216/736] add example HT for ellipse --- doc/examples/plot_circular_hough_transform.py | 75 ++++++++++++++++++- 1 file changed, 71 insertions(+), 4 deletions(-) diff --git a/doc/examples/plot_circular_hough_transform.py b/doc/examples/plot_circular_hough_transform.py index fe4bea26..8267fc55 100755 --- a/doc/examples/plot_circular_hough_transform.py +++ b/doc/examples/plot_circular_hough_transform.py @@ -1,11 +1,14 @@ """ -======================== -Circular Hough Transform -======================== +======================================== +Circular and Elliptical Hough Transforms +======================================== The Hough transform in its simplest form is a `method to detect straight lines `__ -but it can also be used to detect circles. +but it can also be used to detect circles or ellipses. + +Circle detection +================ In the following example, the Hough transform is used to detect coin positions and match their edges. We provide a range of @@ -70,3 +73,67 @@ for idx in np.argsort(accums)[::-1][:5]: ax.imshow(image, cmap=plt.cm.gray) plt.show() + + +""" +Ellipse detection +================= + +In this second example, the aim is to detect the edge of the coffee cup. +Basically, this is a projection of a circle, i.e. an ellipse. +The problem to solve is much more difficult since five parameters have to be determined, +instead of three for circles. + + +Algorithm overview +------------------ + +The algorithm takes two different points belonging to the ellipse. It assumes that it is +the main axis. A loop on all the other points determines how much an ellipse passes to +them. + +A full description of the algorithm can be found in reference [1]. + + +References +---------- +.. [1] Xie, Yonghong, and Qiang Ji. "A new efficient ellipse detection + method." Pattern Recognition, 2002. Proceedings. 16th International + Conference on. Vol. 2. IEEE, 2002 +""" +import numpy as np +import matplotlib.pyplot as plt + +from skimage import data, filter, color +from skimage.transform import hough_ellipse +from skimage.draw import ellipse_perimeter + +# Load picture, convert to grayscale and detect edges +image_rgb = data.load('coffee.png')[100:240, 110:250] +image_gray = color.rgb2gray(image_rgb) +edges = filter.canny(image_gray, sigma=2.0, low_threshold=0.1, high_threshold=0.6) + +# Perform a Hough Transform +accum = hough_ellipse(edges, accuracy=7, threshold=93) +center_y = int(accum[0][1]) +center_x = int(accum[0][2]) +xradius = int(accum[0][3]) +yradius = int(accum[0][4]) +angle = accum[0][5] + +# Draw the ellipse on the original image +cx, cy = ellipse_perimeter(center_y, center_x, yradius, xradius, orientation=angle) +image_rgb[cy, cx] = (0, 0, 220) +# Draw the edge (white) and the resulting ellipse (red) +edges = color.gray2rgb(edges) +edges[cy, cx] = (250, 0, 0) + +fig = plt.subplots(figsize=(10, 6)) +plt.subplot(1,2,1) +plt.title('Original picture') +plt.imshow(image_rgb) +plt.subplot(1,2,2) +plt.title('Edge (white) and result (red)') +plt.imshow(edges) + +plt.show() From df607071a0b3685384bfb0aa09088bd2d9f209c6 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Sun, 7 Jul 2013 13:28:31 +0800 Subject: [PATCH 217/736] Adding match_keypoints_brief --- skimage/feature/__init__.py | 5 +-- skimage/feature/_brief.py | 67 +++++++++++++++++++++++++++++++++---- 2 files changed, 63 insertions(+), 9 deletions(-) diff --git a/skimage/feature/__init__.py b/skimage/feature/__init__.py index 1a755da7..8192e323 100644 --- a/skimage/feature/__init__.py +++ b/skimage/feature/__init__.py @@ -6,7 +6,7 @@ from .corner import (corner_kitchen_rosenfeld, corner_harris, corner_shi_tomasi, corner_foerstner, corner_subpix, corner_peaks) from .corner_cy import corner_moravec from .template import match_template -from ._brief import brief +from ._brief import brief, match_keypoints_brief from .util import hamming_distance __all__ = ['daisy', @@ -24,4 +24,5 @@ __all__ = ['daisy', 'corner_moravec', 'match_template', 'brief', - 'hamming_distance'] + 'hamming_distance', + 'match_keypoints_brief'] diff --git a/skimage/feature/_brief.py b/skimage/feature/_brief.py index e782e008..398559e8 100644 --- a/skimage/feature/_brief.py +++ b/skimage/feature/_brief.py @@ -2,7 +2,7 @@ import numpy as np from scipy.ndimage.filters import gaussian_filter from ..util import img_as_float -from .util import _remove_border_keypoints +from .util import _remove_border_keypoints, hamming_distance from ._brief_cy import _brief_loop @@ -53,6 +53,7 @@ def brief(image, keypoints, descriptor_size=256, mode='normal', patch_size=49, -------- >>> from skimage.feature.corner import * >>> from skimage.feature import brief, hamming_distance + >>> from skimage.feature._brief import * >>> square1 = np.zeros([10, 10]) >>> square1[2:8, 2:8] = 1 >>> square1 @@ -72,7 +73,12 @@ def brief(image, keypoints, descriptor_size=256, mode='normal', patch_size=49, [2, 7], [7, 2], [7, 7]]) - >>> descriptors1 = brief(square1, keypoints1, patch_size = 5) + >>> descriptors1, keypoints1 = brief(square1, keypoints1, patch_size = 5, return_keypoints=True) + >>> keypoints1 + array([[2, 2], + [2, 7], + [7, 2], + [7, 7]]) >>> square2 = np.zeros([12, 12]) >>> square2[3:9, 3:9] = 1 >>> square2 @@ -94,12 +100,27 @@ def brief(image, keypoints, descriptor_size=256, mode='normal', patch_size=49, [3, 8], [8, 3], [8, 8]]) - >>> descriptors2 = brief(square2, keypoints1, patch_size = 5) + >>> descriptors2, keypoints2 = brief(square2, keypoints2, patch_size = 5, return_keypoints=True) + >>> keypoints2 + array([[3, 3], + [3, 8], + [8, 3], + [8, 8]]) >>> hamming_distance(descriptors1, descriptors2) - array([[ 0.02734375, 0.2890625 , 0.32421875, 0.6171875 ], - [ 0.3359375 , 0.05078125, 0.6640625 , 0.37109375], - [ 0.359375 , 0.64453125, 0.03125 , 0.33203125], - [ 0.640625 , 0.40234375, 0.3828125 , 0.01953125]]) + array([[ 0.00390625, 0.33984375, 0.35546875, 0.63671875], + [ 0.3359375 , 0. , 0.65625 , 0.3515625 ], + [ 0.359375 , 0.65625 , 0. , 0.3515625 ], + [ 0.6328125 , 0.3515625 , 0.3515625 , 0. ]]) + >>> match_keypoints_brief(keypoints1, descriptors1, keypoints2, descriptors2) + array([[[ 2., 2.], + [ 2., 7.], + [ 7., 2.], + [ 7., 7.]], + + [[ 3., 3.], + [ 3., 8.], + [ 8., 3.], + [ 8., 8.]]]) """ @@ -154,3 +175,35 @@ def brief(image, keypoints, descriptor_size=256, mode='normal', patch_size=49, return descriptors, keypoints else: return descriptors + +def match_keypoints_brief(keypoints1, descriptors1, keypoints2, + descriptors2, threshold=0.15): + + if keypoints1.shape[0] != descriptors1.shape[0] or keypoints2.shape[0] != descriptors2.shape[0]: + raise ValueError("The number of keypoints and number of described \ + keypoints do not match. Make the optional parameter \ + return_keypoints True to get described keypoints.") + + if descriptors1.shape[1] != descriptors2.shape[1]: + raise ValueError("Descriptor sizes for matching keypoints in both \ + the images should be equal.") + + distance = hamming_distance(descriptors1, descriptors2) + + dist_matched_kp = np.amin(distance, axis=1) + index_matched_kp2 = distance.argmin(axis=1) + + temp = np.zeros((keypoints1.shape[0], 3)) + temp[:, 0] = range(keypoints1.shape[0]) + temp[:, 1] = index_matched_kp2 + temp[:, 2] = dist_matched_kp + temp = temp[temp[:, 2] < threshold] + + matched_kp1 = keypoints1[np.int16(temp[:, 0])] + matched_kp2 = keypoints2[np.int16(temp[:, 1])] + + matched_kp = np.zeros((2, matched_kp1.shape[0], 2)) + matched_kp[0, :, :] = matched_kp1 + matched_kp[1, :, :] = matched_kp2 + + return matched_kp From 7506357f8a58aa13fb3734fc6025be27ebe940ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Sun, 7 Jul 2013 10:13:51 +0200 Subject: [PATCH 218/736] add extra comments --- doc/examples/plot_circular_hough_transform.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/doc/examples/plot_circular_hough_transform.py b/doc/examples/plot_circular_hough_transform.py index 8267fc55..6888c513 100755 --- a/doc/examples/plot_circular_hough_transform.py +++ b/doc/examples/plot_circular_hough_transform.py @@ -6,6 +6,8 @@ Circular and Elliptical Hough Transforms The Hough transform in its simplest form is a `method to detect straight lines `__ but it can also be used to detect circles or ellipses. +The algorithm assumes that the edge is detected and it is rebust against +noise or missing points. Circle detection ================ @@ -79,9 +81,9 @@ plt.show() Ellipse detection ================= -In this second example, the aim is to detect the edge of the coffee cup. +In this second example, the aim is to detect the edge of a coffee cup. Basically, this is a projection of a circle, i.e. an ellipse. -The problem to solve is much more difficult since five parameters have to be determined, +The problem to solve is much more difficult bacause five parameters have to be determined, instead of three for circles. @@ -90,7 +92,7 @@ Algorithm overview The algorithm takes two different points belonging to the ellipse. It assumes that it is the main axis. A loop on all the other points determines how much an ellipse passes to -them. +them. A good match corresponds to high accumulator values. A full description of the algorithm can be found in reference [1]. @@ -114,7 +116,11 @@ image_gray = color.rgb2gray(image_rgb) edges = filter.canny(image_gray, sigma=2.0, low_threshold=0.1, high_threshold=0.6) # Perform a Hough Transform +# The accuracy corresponds to the bin size of a major axis. +# The value is chosen in order to get a single high accumulator. +# The threshold eliminates low accumulators accum = hough_ellipse(edges, accuracy=7, threshold=93) +# Estimated parameters for the ellipse center_y = int(accum[0][1]) center_x = int(accum[0][2]) xradius = int(accum[0][3]) From e0c1e0332881a1e64750a6924d0dceb8709252b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Sun, 7 Jul 2013 10:18:54 +0200 Subject: [PATCH 219/736] fix title --- doc/examples/plot_hough_transform.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/examples/plot_hough_transform.py b/doc/examples/plot_hough_transform.py index 985d97ec..e19c37d7 100644 --- a/doc/examples/plot_hough_transform.py +++ b/doc/examples/plot_hough_transform.py @@ -1,7 +1,7 @@ """ -=============== -Hough transform -=============== +============================= +Straight line Hough transform +============================= The Hough transform in its simplest form is a `method to detect straight lines `__. From 4eed83720a34d6ee055ee380f45b39eb59f22516 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Sun, 7 Jul 2013 10:19:19 +0200 Subject: [PATCH 220/736] rename files --- ...h_transform.py => plot_circular_elliptical_hough_transform.py} | 0 .../{plot_hough_transform.py => plot_line_hough_transform.py} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename doc/examples/{plot_circular_hough_transform.py => plot_circular_elliptical_hough_transform.py} (100%) rename doc/examples/{plot_hough_transform.py => plot_line_hough_transform.py} (100%) diff --git a/doc/examples/plot_circular_hough_transform.py b/doc/examples/plot_circular_elliptical_hough_transform.py similarity index 100% rename from doc/examples/plot_circular_hough_transform.py rename to doc/examples/plot_circular_elliptical_hough_transform.py diff --git a/doc/examples/plot_hough_transform.py b/doc/examples/plot_line_hough_transform.py similarity index 100% rename from doc/examples/plot_hough_transform.py rename to doc/examples/plot_line_hough_transform.py From c2193cbc34f07f34aec2894a028c3e3b55e0d72c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Sun, 7 Jul 2013 10:42:32 +0200 Subject: [PATCH 221/736] fix doctest --- skimage/transform/_hough_transform.pyx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/skimage/transform/_hough_transform.pyx b/skimage/transform/_hough_transform.pyx index d158a84d..a3aa2649 100644 --- a/skimage/transform/_hough_transform.pyx +++ b/skimage/transform/_hough_transform.pyx @@ -130,9 +130,9 @@ def hough_ellipse(cnp.ndarray img, int threshold=4, double accuracy=1, -------- >>> img = np.zeros((25, 25), dtype=int) >>> rr, cc = draw.ellipse_perimeter(10, 10, 6, 8) - >>> img[rr, cc] = 1 - >>> result = hough_ellipse(img, threshold=6) - [(10.0, 10.0, 8.0, 6.0474292058692187, 0.0, 8)] + >>> img[cc, rr] = 1 + >>> result = hough_ellipse(img, threshold=8) + [(10.0, 10.0, 8.0, 6.0, 0.0, 10)] Notes ----- From 7fefbf91030194a83f077c34ef0de0deefaa3aba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Sun, 7 Jul 2013 11:32:38 +0200 Subject: [PATCH 222/736] PEP8 --- ...lot_circular_elliptical_hough_transform.py | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/doc/examples/plot_circular_elliptical_hough_transform.py b/doc/examples/plot_circular_elliptical_hough_transform.py index 6888c513..29ed17b3 100755 --- a/doc/examples/plot_circular_elliptical_hough_transform.py +++ b/doc/examples/plot_circular_elliptical_hough_transform.py @@ -83,16 +83,16 @@ Ellipse detection In this second example, the aim is to detect the edge of a coffee cup. Basically, this is a projection of a circle, i.e. an ellipse. -The problem to solve is much more difficult bacause five parameters have to be determined, -instead of three for circles. +The problem to solve is much more difficult bacause five parameters have to be +determined, instead of three for circles. Algorithm overview ------------------ -The algorithm takes two different points belonging to the ellipse. It assumes that it is -the main axis. A loop on all the other points determines how much an ellipse passes to -them. A good match corresponds to high accumulator values. +The algorithm takes two different points belonging to the ellipse. It assumes +that it is the main axis. A loop on all the other points determines how much +an ellipse passes to them. A good match corresponds to high accumulator values. A full description of the algorithm can be found in reference [1]. @@ -103,7 +103,6 @@ References method." Pattern Recognition, 2002. Proceedings. 16th International Conference on. Vol. 2. IEEE, 2002 """ -import numpy as np import matplotlib.pyplot as plt from skimage import data, filter, color @@ -113,7 +112,8 @@ from skimage.draw import ellipse_perimeter # Load picture, convert to grayscale and detect edges image_rgb = data.load('coffee.png')[100:240, 110:250] image_gray = color.rgb2gray(image_rgb) -edges = filter.canny(image_gray, sigma=2.0, low_threshold=0.1, high_threshold=0.6) +edges = filter.canny(image_gray, sigma=2.0, + low_threshold=0.1, high_threshold=0.6) # Perform a Hough Transform # The accuracy corresponds to the bin size of a major axis. @@ -128,17 +128,18 @@ yradius = int(accum[0][4]) angle = accum[0][5] # Draw the ellipse on the original image -cx, cy = ellipse_perimeter(center_y, center_x, yradius, xradius, orientation=angle) +cx, cy = ellipse_perimeter(center_y, center_x, + yradius, xradius, orientation=angle) image_rgb[cy, cx] = (0, 0, 220) # Draw the edge (white) and the resulting ellipse (red) edges = color.gray2rgb(edges) edges[cy, cx] = (250, 0, 0) fig = plt.subplots(figsize=(10, 6)) -plt.subplot(1,2,1) +plt.subplot(1, 2, 1) plt.title('Original picture') plt.imshow(image_rgb) -plt.subplot(1,2,2) +plt.subplot(1, 2, 2) plt.title('Edge (white) and result (red)') plt.imshow(edges) From 7048a9c995e9704340110f1c69d22139da7f9c31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Sun, 7 Jul 2013 12:05:30 +0200 Subject: [PATCH 223/736] add original picture --- doc/examples/plot_convex_hull.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/doc/examples/plot_convex_hull.py b/doc/examples/plot_convex_hull.py index c5505295..02d08732 100644 --- a/doc/examples/plot_convex_hull.py +++ b/doc/examples/plot_convex_hull.py @@ -27,9 +27,17 @@ image = np.array( [0, 1, 0, 0, 0, 0, 0, 1, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=float) +original_image = np.copy(image) + chull = convex_hull_image(image) image[chull] += 1.7 image -= -1.7 +fig = plt.subplots(figsize=(10, 6)) +plt.subplot(1, 2, 1) +plt.title('Original picture') +plt.imshow(original_image, cmap=plt.cm.gray, interpolation='nearest') +plt.subplot(1, 2, 2) +plt.title('Transformed picture') plt.imshow(image, cmap=plt.cm.gray, interpolation='nearest') plt.show() From acdfd8bc71bc87027f4354c050848ca78590f041 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Sun, 7 Jul 2013 12:21:34 +0200 Subject: [PATCH 224/736] simplify modification image + comment --- doc/examples/plot_convex_hull.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/doc/examples/plot_convex_hull.py b/doc/examples/plot_convex_hull.py index 02d08732..31398e6d 100644 --- a/doc/examples/plot_convex_hull.py +++ b/doc/examples/plot_convex_hull.py @@ -30,8 +30,15 @@ image = np.array( original_image = np.copy(image) chull = convex_hull_image(image) -image[chull] += 1.7 -image -= -1.7 +image[chull] += 1 +# image is now: +#[[ 0. 0. 0. 0. 0. 0. 0. 0. 0.] +# [ 0. 0. 0. 0. 2. 0. 0. 0. 0.] +# [ 0. 0. 0. 2. 1. 2. 0. 0. 0.] +# [ 0. 0. 2. 1. 1. 1. 2. 0. 0.] +# [ 0. 2. 1. 1. 1. 1. 1. 2. 0.] +# [ 0. 0. 0. 0. 0. 0. 0. 0. 0.]] + fig = plt.subplots(figsize=(10, 6)) plt.subplot(1, 2, 1) From bcd2974216e09dc5abd21c55d531f58d145bcdbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Sun, 7 Jul 2013 19:15:25 +0200 Subject: [PATCH 225/736] fix Johannes' comments --- .../plot_circular_elliptical_hough_transform.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/doc/examples/plot_circular_elliptical_hough_transform.py b/doc/examples/plot_circular_elliptical_hough_transform.py index 29ed17b3..a110698e 100755 --- a/doc/examples/plot_circular_elliptical_hough_transform.py +++ b/doc/examples/plot_circular_elliptical_hough_transform.py @@ -6,7 +6,7 @@ Circular and Elliptical Hough Transforms The Hough transform in its simplest form is a `method to detect straight lines `__ but it can also be used to detect circles or ellipses. -The algorithm assumes that the edge is detected and it is rebust against +The algorithm assumes that the edge is detected and it is robust against noise or missing points. Circle detection @@ -83,7 +83,7 @@ Ellipse detection In this second example, the aim is to detect the edge of a coffee cup. Basically, this is a projection of a circle, i.e. an ellipse. -The problem to solve is much more difficult bacause five parameters have to be +The problem to solve is much more difficult because five parameters have to be determined, instead of three for circles. @@ -94,7 +94,7 @@ The algorithm takes two different points belonging to the ellipse. It assumes that it is the main axis. A loop on all the other points determines how much an ellipse passes to them. A good match corresponds to high accumulator values. -A full description of the algorithm can be found in reference [1]. +A full description of the algorithm can be found in reference [1]_. References @@ -121,11 +121,11 @@ edges = filter.canny(image_gray, sigma=2.0, # The threshold eliminates low accumulators accum = hough_ellipse(edges, accuracy=7, threshold=93) # Estimated parameters for the ellipse -center_y = int(accum[0][1]) -center_x = int(accum[0][2]) -xradius = int(accum[0][3]) -yradius = int(accum[0][4]) -angle = accum[0][5] +center_y = int(accum[0, 1]) +center_x = int(accum[0, 2]) +xradius = int(accum[0, 3]) +yradius = int(accum[0, 4]) +angle = accum[0, 5] # Draw the ellipse on the original image cx, cy = ellipse_perimeter(center_y, center_x, From 0e7e838cac3c753e75f934b49ec7157048a2d77d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Sun, 7 Jul 2013 19:17:16 +0200 Subject: [PATCH 226/736] use shorter URL --- skimage/data/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/data/__init__.py b/skimage/data/__init__.py index 826ae75a..65802c0b 100644 --- a/skimage/data/__init__.py +++ b/skimage/data/__init__.py @@ -195,7 +195,7 @@ def coffee(): Notes ----- This image was downloaded on - `Flickr `__ + `Flickr `__ No copyright restrictions. """ return load("coffee.png") From c47aa27cc5effde24b9b5e76b5f8603553bdb6bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Sun, 7 Jul 2013 19:24:17 +0200 Subject: [PATCH 227/736] typo --- CONTRIBUTING.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.txt b/CONTRIBUTING.txt index 6c5c8b58..afa20020 100644 --- a/CONTRIBUTING.txt +++ b/CONTRIBUTING.txt @@ -164,7 +164,7 @@ passes all tests. To do so, you can follow step one and two in `Travis documentation __ (Step three is already done in scikit-image). -Thus, as soon as you push your code to your fork, it will triger Travis, +Thus, as soon as you push your code to your fork, it will trigger Travis, and you will receive an email notification when the process is done. Bugs From 02acd3ce4230b9d87f767514fa3207ec1b78175f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Sun, 7 Jul 2013 19:37:30 +0200 Subject: [PATCH 228/736] revert, accum is a tuple of lists --- .../plot_circular_elliptical_hough_transform.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/examples/plot_circular_elliptical_hough_transform.py b/doc/examples/plot_circular_elliptical_hough_transform.py index a110698e..e537a4a1 100755 --- a/doc/examples/plot_circular_elliptical_hough_transform.py +++ b/doc/examples/plot_circular_elliptical_hough_transform.py @@ -121,11 +121,11 @@ edges = filter.canny(image_gray, sigma=2.0, # The threshold eliminates low accumulators accum = hough_ellipse(edges, accuracy=7, threshold=93) # Estimated parameters for the ellipse -center_y = int(accum[0, 1]) -center_x = int(accum[0, 2]) -xradius = int(accum[0, 3]) -yradius = int(accum[0, 4]) -angle = accum[0, 5] +center_y = int(accum[0][1]) +center_x = int(accum[0][2]) +xradius = int(accum[0][3]) +yradius = int(accum[0][4]) +angle = accum[0][5] # Draw the ellipse on the original image cx, cy = ellipse_perimeter(center_y, center_x, From 0988650fbee7ef54edcb1032117ce3c654b33fdb Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Mon, 8 Jul 2013 18:58:05 +0800 Subject: [PATCH 229/736] Adding docs for match_keypoints_brief --- skimage/feature/_brief.py | 145 ++++++++++++++++++++++---------------- 1 file changed, 85 insertions(+), 60 deletions(-) diff --git a/skimage/feature/_brief.py b/skimage/feature/_brief.py index 398559e8..f4c20bff 100644 --- a/skimage/feature/_brief.py +++ b/skimage/feature/_brief.py @@ -28,6 +28,9 @@ def brief(image, keypoints, descriptor_size=256, mode='normal', patch_size=49, the keypoints. Default is 49. sample_seed : int Seed for sampling the decision pixel-pairs. Default is 1. + variance : float + Variance of the Gaussian Low Pass filter applied on the image to + alleviate noise sensitivity. Default is 2. return_keypoints : bool If True, return the Q keypoints (after filtering out the border keypoints) about which the descriptors are extracted. Default is False. @@ -46,81 +49,76 @@ def brief(image, keypoints, descriptor_size=256, mode='normal', patch_size=49, References ---------- .. [1] Michael Calonder, Vincent Lepetit, Christoph Strecha and Pascal Fua - "BRIEF : Binary robust independent elementary features", - http://cvlabwww.epfl.ch/~lepetit/papers/calonder_eccv10.pdf + "BRIEF : Binary robust independent elementary features", + http://cvlabwww.epfl.ch/~lepetit/papers/calonder_eccv10.pdf Examples -------- >>> from skimage.feature.corner import * - >>> from skimage.feature import brief, hamming_distance + >>> from skimage.feature import hamming_distance >>> from skimage.feature._brief import * - >>> square1 = np.zeros([10, 10]) - >>> square1[2:8, 2:8] = 1 + >>> square1 = np.zeros([8, 8], dtype=np.int32) + >>> square1[2:6, 2:6] = 1 >>> square1 - array([[ 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], - [ 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], - [ 0., 0., 1., 1., 1., 1., 1., 1., 0., 0.], - [ 0., 0., 1., 1., 1., 1., 1., 1., 0., 0.], - [ 0., 0., 1., 1., 1., 1., 1., 1., 0., 0.], - [ 0., 0., 1., 1., 1., 1., 1., 1., 0., 0.], - [ 0., 0., 1., 1., 1., 1., 1., 1., 0., 0.], - [ 0., 0., 1., 1., 1., 1., 1., 1., 0., 0.], - [ 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], - [ 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]]) + array([[0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0]], dtype=int32) >>> keypoints1 = corner_peaks(corner_harris(square1), min_distance=1) >>> keypoints1 array([[2, 2], - [2, 7], - [7, 2], - [7, 7]]) + [2, 5], + [5, 2], + [5, 5]]) >>> descriptors1, keypoints1 = brief(square1, keypoints1, patch_size = 5, return_keypoints=True) >>> keypoints1 array([[2, 2], - [2, 7], - [7, 2], - [7, 7]]) - >>> square2 = np.zeros([12, 12]) - >>> square2[3:9, 3:9] = 1 + [2, 5], + [5, 2], + [5, 5]]) + >>> square2 = np.zeros([9, 9], dtype=np.int32) + >>> square2[2:7, 2:7] = 1 >>> square2 - array([[ 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], - [ 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], - [ 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], - [ 0., 0., 0., 1., 1., 1., 1., 1., 1., 0., 0., 0.], - [ 0., 0., 0., 1., 1., 1., 1., 1., 1., 0., 0., 0.], - [ 0., 0., 0., 1., 1., 1., 1., 1., 1., 0., 0., 0.], - [ 0., 0., 0., 1., 1., 1., 1., 1., 1., 0., 0., 0.], - [ 0., 0., 0., 1., 1., 1., 1., 1., 1., 0., 0., 0.], - [ 0., 0., 0., 1., 1., 1., 1., 1., 1., 0., 0., 0.], - [ 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], - [ 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], - [ 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]]) + array([[0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=int32) >>> keypoints2 = corner_peaks(corner_harris(square2), min_distance=1) >>> keypoints2 - array([[3, 3], - [3, 8], - [8, 3], - [8, 8]]) + array([[2, 2], + [2, 6], + [6, 2], + [6, 6]]) >>> descriptors2, keypoints2 = brief(square2, keypoints2, patch_size = 5, return_keypoints=True) >>> keypoints2 - array([[3, 3], - [3, 8], - [8, 3], - [8, 8]]) + array([[2, 2], + [2, 6], + [6, 2], + [6, 6]]) >>> hamming_distance(descriptors1, descriptors2) - array([[ 0.00390625, 0.33984375, 0.35546875, 0.63671875], - [ 0.3359375 , 0. , 0.65625 , 0.3515625 ], - [ 0.359375 , 0.65625 , 0. , 0.3515625 ], - [ 0.6328125 , 0.3515625 , 0.3515625 , 0. ]]) + array([[ 0.03125 , 0.3203125, 0.3671875, 0.6171875], + [ 0.3203125, 0.03125 , 0.640625 , 0.375 ], + [ 0.375 , 0.6328125, 0.0390625, 0.328125 ], + [ 0.625 , 0.3671875, 0.34375 , 0.0234375]]) >>> match_keypoints_brief(keypoints1, descriptors1, keypoints2, descriptors2) array([[[ 2., 2.], - [ 2., 7.], - [ 7., 2.], - [ 7., 7.]], + [ 2., 5.], + [ 5., 2.], + [ 5., 5.]], - [[ 3., 3.], - [ 3., 8.], - [ 8., 3.], - [ 8., 8.]]]) + [[ 2., 2.], + [ 2., 6.], + [ 6., 2.], + [ 6., 6.]]]) """ @@ -178,8 +176,30 @@ def brief(image, keypoints, descriptor_size=256, mode='normal', patch_size=49, def match_keypoints_brief(keypoints1, descriptors1, keypoints2, descriptors2, threshold=0.15): + """Match keypoints described using BRIEF descriptors. - if keypoints1.shape[0] != descriptors1.shape[0] or keypoints2.shape[0] != descriptors2.shape[0]: + Parameters + ---------- + keypoints1 : (M, 2) ndarray + M Keypoints from the first image described using feature._brief.brief + descriptors1 : (M, P) ndarray + BRIEF descriptors of size P about M keypoints in the first image. + keypoints2 : (N, 2) ndarray + N Keypoints from the second image described using feature._brief.brief + descriptors2 : (N, P) ndarray + BRIEF descriptors of size P about N keypoints in the second image. + threshold : float in range [0, 1] + Threshold for removing matched keypoint pairs with hamming distance + greater than it. Default is 0.15 + + Returns + ------- + match_keypoints_brief : (2, Q, 2) ndarray + Location of Q matched keypoint pairs from two images. + + """ + if keypoints1.shape[0] != descriptors1.shape[0] or \ + keypoints2.shape[0] != descriptors2.shape[0]: raise ValueError("The number of keypoints and number of described \ keypoints do not match. Make the optional parameter \ return_keypoints True to get described keypoints.") @@ -187,12 +207,16 @@ def match_keypoints_brief(keypoints1, descriptors1, keypoints2, if descriptors1.shape[1] != descriptors2.shape[1]: raise ValueError("Descriptor sizes for matching keypoints in both \ the images should be equal.") - + # Get hamming distances between keeypoints1 and keypoints2 distance = hamming_distance(descriptors1, descriptors2) + # For each keypoint in keypoints1, match it with the keypoint in keypoints2 + # that has minimum hamming distance dist_matched_kp = np.amin(distance, axis=1) index_matched_kp2 = distance.argmin(axis=1) + # Remove the matched pairs which have hamming distance greater than the + # threshold temp = np.zeros((keypoints1.shape[0], 3)) temp[:, 0] = range(keypoints1.shape[0]) temp[:, 1] = index_matched_kp2 @@ -202,8 +226,9 @@ def match_keypoints_brief(keypoints1, descriptors1, keypoints2, matched_kp1 = keypoints1[np.int16(temp[:, 0])] matched_kp2 = keypoints2[np.int16(temp[:, 1])] - matched_kp = np.zeros((2, matched_kp1.shape[0], 2)) - matched_kp[0, :, :] = matched_kp1 - matched_kp[1, :, :] = matched_kp2 + # Collecting matched keypoint pairs from their index pairs + matched_keypoint_pairs = np.zeros((2, matched_kp1.shape[0], 2)) + matched_keypoint_pairs[0, :, :] = matched_kp1 + matched_keypoint_pairs[1, :, :] = matched_kp2 - return matched_kp + return matched_keypoint_pairs From 1b4cf9f76d23058ce516d11cbae13feb3c267b76 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Mon, 8 Jul 2013 13:14:08 +0200 Subject: [PATCH 230/736] Fix Bento build. --- bento.info | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bento.info b/bento.info index 4cfa0173..b1366173 100644 --- a/bento.info +++ b/bento.info @@ -108,6 +108,9 @@ Library: Extension: skimage.morphology._skeletonize_cy Sources: skimage/morphology/_skeletonize_cy.pyx + Extension: skimage.transform._radon_transform + Sources: + skimage/transform/_radon_transform.pyx Extension: skimage.transform._warps_cy Sources: skimage/transform/_warps_cy.pyx @@ -132,9 +135,6 @@ Library: Extension: skimage.filter.rank._core8 Sources: skimage/filter/rank/_core8.pyx - Extension: skimage.filter.rank.rank - Sources: - skimage/filter/rank/rank.pyx Extension: skimage.filter.rank.bilateral_rank Sources: skimage/filter/rank/bilateral_rank.pyx From 48d78c09d94904047c87744a639b975646c11f24 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Mon, 8 Jul 2013 14:41:42 +0200 Subject: [PATCH 231/736] Improve doc for regular_grid.py --- skimage/util/regular_grid.py | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/skimage/util/regular_grid.py b/skimage/util/regular_grid.py index d1cb5428..e304be20 100644 --- a/skimage/util/regular_grid.py +++ b/skimage/util/regular_grid.py @@ -5,7 +5,10 @@ def regular_grid(ar_shape, n_points): """Find `n_points` regularly spaced along `ar_shape`. The returned points (as slices) should be as close to cubically-spaced as - possible. + possible. Essentially, the points are spaced by the Nth root of the input + array size, where N is the number of dimensions. However, if an array + dimension cannot fit a full step size, it is "discarded", and the + computation is done for only the remaining dimensions. Parameters ---------- @@ -20,6 +23,30 @@ def regular_grid(ar_shape, n_points): slices : list of slice objects A slice along each dimension of `ar_shape`, such that the intersection of all the slices give the coordinates of regularly spaced points. + + Examples + -------- + >>> ar = np.zeros((20, 40)) + >>> g = regular_grid(ar.shape, 8) + >>> g + [slice(5.0, None, 10.0), slice(5.0, None, 10.0)] + >>> ar[g] = 1 + >>> ar.sum() + 8.0 + >>> ar = np.zeros((20, 40)) + >>> g = regular_grid(ar.shape, 32) + >>> g + [slice(2.0, None, 5.0), slice(2.0, None, 5.0)] + >>> ar[g] = 1 + >>> ar.sum() + 32.0 + >>> ar = np.zeros((3, 20, 40)) + >>> g = regular_grid(ar.shape, 8) + >>> g + [slice(1.0, None, 3.0), slice(5.0, None, 10.0), slice(5.0, None, 10.0)] + >>> ar[g] = 1 + >>> ar.sum() + 8.0 """ ar_shape = np.asanyarray(ar_shape) ndim = len(ar_shape) From 18a06a0adda7ae1263b08ced360f5de8f5664487 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Mon, 8 Jul 2013 14:50:06 +0200 Subject: [PATCH 232/736] iradon_sart: Format docs correctly. --- skimage/transform/radon_transform.py | 40 ++++++++++++++-------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/skimage/transform/radon_transform.py b/skimage/transform/radon_transform.py index 6009c54a..d9152879 100644 --- a/skimage/transform/radon_transform.py +++ b/skimage/transform/radon_transform.py @@ -268,12 +268,12 @@ def order_angles_golden_ratio(theta): by T. Kohler. References: - -Kohler, T. "A projection access scheme for iterative - reconstruction based on the golden section." Nuclear Science - Symposium Conference Record, 2004 IEEE. Vol. 6. IEEE, 2004. - -Winkelmann, Stefanie, et al. "An optimal radial profile order - based on the Golden Ratio for time-resolved MRI." - Medical Imaging, IEEE Transactions on 26.1 (2007): 68-76. + - Kohler, T. "A projection access scheme for iterative + reconstruction based on the golden section." Nuclear Science + Symposium Conference Record, 2004 IEEE. Vol. 6. IEEE, 2004. + - Winkelmann, Stefanie, et al. "An optimal radial profile order + based on the Golden Ratio for time-resolved MRI." + Medical Imaging, IEEE Transactions on 26.1 (2007): 68-76. """ interval = 180 @@ -336,7 +336,7 @@ def iradon_sart(radon_image, theta=None, image=None, projection_shifts=None, Returns ------- output : ndarray - Reconstructed image. + Reconstructed image. Notes ----- @@ -354,19 +354,19 @@ def iradon_sart(radon_image, theta=None, image=None, projection_shifts=None, information, but will also often increase the noise. References: - -AC Kak, M Slaney, "Principles of Computerized Tomographic - Imaging", IEEE Press 1988. - -AH Andersen, AC Kak, "Simultaneous algebraic reconstruction technique - (SART): a superior implementation of the ART algorithm", Ultrasonic - Imaging 6 pp 81--94 (1984) - -S Kaczmarz, "Angenäherte auflösung von systemen linearer - gleichungen", Bulletin International de l’Academie Polonaise des - Sciences et des Lettres 35 pp 355--357 (1937) - -Kohler, T. "A projection access scheme for iterative - reconstruction based on the golden section." Nuclear Science - Symposium Conference Record, 2004 IEEE. Vol. 6. IEEE, 2004. - -Kaczmarz' method, Wikipedia, - http://en.wikipedia.org/wiki/Kaczmarz_method + - AC Kak, M Slaney, "Principles of Computerized Tomographic + Imaging", IEEE Press 1988. + - AH Andersen, AC Kak, "Simultaneous algebraic reconstruction technique + (SART): a superior implementation of the ART algorithm", Ultrasonic + Imaging 6 pp 81--94 (1984) + - S Kaczmarz, "Angenäherte auflösung von systemen linearer + gleichungen", Bulletin International de l’Academie Polonaise des + Sciences et des Lettres 35 pp 355--357 (1937) + - Kohler, T. "A projection access scheme for iterative + reconstruction based on the golden section." Nuclear Science + Symposium Conference Record, 2004 IEEE. Vol. 6. IEEE, 2004. + - Kaczmarz' method, Wikipedia, + http://en.wikipedia.org/wiki/Kaczmarz_method """ if radon_image.ndim != 2: raise ValueError('radon_image must be two dimensional') From 4e0440d13f22a613b7cdce53585ff0013eb552a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Mon, 8 Jul 2013 18:02:11 +0200 Subject: [PATCH 233/736] detailed procedure --- CONTRIBUTING.txt | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/CONTRIBUTING.txt b/CONTRIBUTING.txt index afa20020..20e6cff0 100644 --- a/CONTRIBUTING.txt +++ b/CONTRIBUTING.txt @@ -154,17 +154,25 @@ detailing the test coverage:: skimage/filter/__init__ 1 1 100% ... -Activate Travis for your fork (optional) +Activate Travis-ci for your fork (optional) ---------------------------------------- -Travis checks all unittests in the project to prevent breakage. +Travis-ci checks all unittests in the project to prevent breakage. -Before sending a pull request, you may want to check that Travis successfully -passes all tests. To do so, you can follow step one and two in -`Travis documentation __ +Before sending a pull request, you may want to check that Travis-ci +successfully passes all tests. To do so, + + * Go to `Travis-ci `__ and follow the Sign In link at the top:: + + * Go to your `profile page `__ + + * Switch on your scikit-image fork + +It corresponds to steps one and two in +`Travis-ci documentation `__ (Step three is already done in scikit-image). -Thus, as soon as you push your code to your fork, it will trigger Travis, +Thus, as soon as you push your code to your fork, it will trigger Travis-ci, and you will receive an email notification when the process is done. Bugs From 0156593fb63c676247d8bd7cc17a85faaf727bf3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Mon, 8 Jul 2013 20:39:53 +0200 Subject: [PATCH 234/736] Let travis-ci check bento.info --- .travis.yml | 2 ++ check_bento_build.py | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/.travis.yml b/.travis.yml index b0e7a494..f4f420cb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,6 +25,8 @@ install: - $PYTHON setup.py build - sudo $PYTHON setup.py install script: + # Check if setup.py's match bento.info + - $PYTHON check_bento_build.py # Change into an innocuous directory and find tests from installation - mkdir $HOME/.matplotlib - "echo 'backend : Agg' > $HOME/.matplotlib/matplotlibrc" diff --git a/check_bento_build.py b/check_bento_build.py index 1b069056..bf3c5771 100644 --- a/check_bento_build.py +++ b/check_bento_build.py @@ -3,6 +3,7 @@ Check that Cython extensions in setup.py files match those in bento.info. """ import os import re +import sys RE_CYTHON = re.compile("config.add_extension\(\s*['\"]([\S]+)['\"]") @@ -93,3 +94,6 @@ if __name__ == '__main__': cy_bento, cy_setup = remove_common_extensions(cy_bento, cy_setup) print_results(cy_bento, cy_setup) + + if cy_setup or cy_bento: + sys.exit(1) From 633dca7c9748686306f4b6a67c703846217985ba Mon Sep 17 00:00:00 2001 From: Horea Christian Date: Tue, 9 Jul 2013 00:28:57 +0200 Subject: [PATCH 235/736] Refined syntax. --- skimage/util/montage.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/skimage/util/montage.py b/skimage/util/montage.py index 03e2808a..ff7793d6 100644 --- a/skimage/util/montage.py +++ b/skimage/util/montage.py @@ -93,14 +93,13 @@ def montage2d(arr_in, fill='mean', rescale_intensity=False, output_shape=(0, 0)) if output_shape == (0, 0): alpha_y = alpha_x = int(np.ceil(np.sqrt(n_images))) else: - alpha_y = output_shape[0] - alpha_x = output_shape[1] + alpha_y, alpha_x = output_shape # -- fill missing patches if fill == 'mean': fill = arr_in.mean() - n_missing = int((alpha_y*alpha_x) - n_images) + n_missing = int((alpha_y * alpha_x) - n_images) missing = np.ones((n_missing, height, width), dtype=arr_in.dtype) * fill arr_out = np.vstack((arr_in, missing)) From 1cc42bd94377a72a595e5fd90b81549f0bba6b99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Tue, 9 Jul 2013 11:44:03 +0200 Subject: [PATCH 236/736] iradon_sart: Reformat references. --- skimage/transform/radon_transform.py | 44 +++++++++++++++------------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/skimage/transform/radon_transform.py b/skimage/transform/radon_transform.py index d9152879..23e9b1f6 100644 --- a/skimage/transform/radon_transform.py +++ b/skimage/transform/radon_transform.py @@ -267,13 +267,14 @@ def order_angles_golden_ratio(theta): The method used here is that of the golden ratio introduced by T. Kohler. - References: - - Kohler, T. "A projection access scheme for iterative - reconstruction based on the golden section." Nuclear Science - Symposium Conference Record, 2004 IEEE. Vol. 6. IEEE, 2004. - - Winkelmann, Stefanie, et al. "An optimal radial profile order - based on the Golden Ratio for time-resolved MRI." - Medical Imaging, IEEE Transactions on 26.1 (2007): 68-76. + References + ---------- + .. [1] Kohler, T. "A projection access scheme for iterative + reconstruction based on the golden section." Nuclear Science + Symposium Conference Record, 2004 IEEE. Vol. 6. IEEE, 2004. + .. [2] Winkelmann, Stefanie, et al. "An optimal radial profile order + based on the Golden Ratio for time-resolved MRI." + Medical Imaging, IEEE Transactions on 26.1 (2007): 68-76. """ interval = 180 @@ -353,20 +354,21 @@ def iradon_sart(radon_image, theta=None, image=None, projection_shifts=None, reconstruction. Further iterations will tend to enhance high-frequency information, but will also often increase the noise. - References: - - AC Kak, M Slaney, "Principles of Computerized Tomographic - Imaging", IEEE Press 1988. - - AH Andersen, AC Kak, "Simultaneous algebraic reconstruction technique - (SART): a superior implementation of the ART algorithm", Ultrasonic - Imaging 6 pp 81--94 (1984) - - S Kaczmarz, "Angenäherte auflösung von systemen linearer - gleichungen", Bulletin International de l’Academie Polonaise des - Sciences et des Lettres 35 pp 355--357 (1937) - - Kohler, T. "A projection access scheme for iterative - reconstruction based on the golden section." Nuclear Science - Symposium Conference Record, 2004 IEEE. Vol. 6. IEEE, 2004. - - Kaczmarz' method, Wikipedia, - http://en.wikipedia.org/wiki/Kaczmarz_method + References + ---------- + .. [1] AC Kak, M Slaney, "Principles of Computerized Tomographic + Imaging", IEEE Press 1988. + .. [2] AH Andersen, AC Kak, "Simultaneous algebraic reconstruction technique + (SART): a superior implementation of the ART algorithm", Ultrasonic + Imaging 6 pp 81--94 (1984) + .. [3] S Kaczmarz, "Angenäherte auflösung von systemen linearer + gleichungen", Bulletin International de l’Academie Polonaise des + Sciences et des Lettres 35 pp 355--357 (1937) + .. [4] Kohler, T. "A projection access scheme for iterative + reconstruction based on the golden section." Nuclear Science + Symposium Conference Record, 2004 IEEE. Vol. 6. IEEE, 2004. + .. [5] Kaczmarz' method, Wikipedia, + http://en.wikipedia.org/wiki/Kaczmarz_method """ if radon_image.ndim != 2: raise ValueError('radon_image must be two dimensional') From 4d307f531557a8648b201648ad205adc99be955c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Tue, 9 Jul 2013 11:44:28 +0200 Subject: [PATCH 237/736] iradon_sart: Fix docstring typo. --- skimage/transform/radon_transform.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/transform/radon_transform.py b/skimage/transform/radon_transform.py index 23e9b1f6..1ebd26c6 100644 --- a/skimage/transform/radon_transform.py +++ b/skimage/transform/radon_transform.py @@ -136,7 +136,7 @@ def iradon(radon_image, theta=None, output_size=None, Number of rows and columns in the reconstruction. filter : str, optional (default ramp) Filter used in frequency domain filtering. Ramp filter used by default. - Filters available: ramp, shepp-logan, cosine, hamming, hann + Filters available: ramp, shepp-logan, cosine, hamming, hann. Assign None to use no filter. interpolation : str, optional (default 'linear') Interpolation method used in reconstruction. Methods available: From b737dc97a6b569fad13273e6cb5970cafb6b7e64 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Tue, 9 Jul 2013 23:47:44 +0800 Subject: [PATCH 238/736] Making hamming_distance more generalized; improving docs --- skimage/feature/_brief.py | 19 +++++++++++-------- skimage/feature/util.py | 30 +++++++++++------------------- 2 files changed, 22 insertions(+), 27 deletions(-) diff --git a/skimage/feature/_brief.py b/skimage/feature/_brief.py index f4c20bff..7f3a9788 100644 --- a/skimage/feature/_brief.py +++ b/skimage/feature/_brief.py @@ -27,7 +27,11 @@ def brief(image, keypoints, descriptor_size=256, mode='normal', patch_size=49, Length of the two dimensional square patch sampling region around the keypoints. Default is 49. sample_seed : int - Seed for sampling the decision pixel-pairs. Default is 1. + Seed for sampling the decision pixel-pairs. From a square window with + length patch_size, pixel pairs are sampled using the `mode` parameter + to build the descriptors using intensity comparison. The value of + `sample_seed` should be the same for the images to be matched while + building the descriptors. Default is 1. variance : float Variance of the Gaussian Low Pass filter applied on the image to alleviate noise sensitivity. Default is 2. @@ -37,8 +41,8 @@ def brief(image, keypoints, descriptor_size=256, mode='normal', patch_size=49, Returns ------- - descriptors : (Q, descriptor_size) ndarray of dtype bool - 2D ndarray of binary descriptors of size descriptor_size about Q + descriptors : (Q, `descriptor_size`) ndarray of dtype bool + 2D ndarray of binary descriptors of size `descriptor_size` about Q keypoints after filtering out border keypoints with value at an index (i, j) either being True or False representing the outcome of Intensity comparison about ith keypoint on jth decision pixel-pair. @@ -136,14 +140,13 @@ def brief(image, keypoints, descriptor_size=256, mode='normal', patch_size=49, image = np.ascontiguousarray(image) - keypoints = np.array(keypoints + 0.5, dtype=np.intp, order='C') + keypoints = np.array(keypoints + 0.5, dtype=np.intp) - # Removing keypoints that are (patch_size / 2) distance from the image - # border + # Removing keypoints that are within (patch_size / 2) distance from the + # image border keypoints = _remove_border_keypoints(image, keypoints, patch_size / 2) - descriptors = np.zeros((keypoints.shape[0], descriptor_size), - dtype=bool, order='C') + descriptors = np.zeros((keypoints.shape[0], descriptor_size), dtype=bool) # Sampling pairs of decision pixels in patch_size x patch_size window if mode == 'normal': diff --git a/skimage/feature/util.py b/skimage/feature/util.py index c77421d7..67eb93ca 100644 --- a/skimage/feature/util.py +++ b/skimage/feature/util.py @@ -15,35 +15,27 @@ def _remove_border_keypoints(image, keypoints, dist): return keypoints -def hamming_distance(descriptors1, descriptors2): +def hamming_distance(array1, array2): """A dissimilarity measure used for matching keypoints in different images using binary feature descriptors like BRIEF etc. Parameters ---------- - descriptors1 : (P1, D) array of dtype bool - Binary feature descriptors for keypoints in the first image. - 2D ndarray with a binary descriptors of size D about P1 keypoints - with value at an index (i, j) either being True or False representing - the outcome of Intensity comparison about ith keypoint on jth decision - pixel-pair. - descriptors2 : (P2, D) array of dtype bool - Binary feature descriptors for keypoints in the second image. - 2D ndarray with a binary descriptors of size D about P2 keypoints - with value at an index (i, j) either being True or False representing - the outcome of Intensity comparison about ith keypoint on jth decision - pixel-pair. + array1 : (P1, D) array of dtype bool + P1 vectors of size D with boolean elements. + array2 : (P2, D) array of dtype bool + P2 vectors of size D with boolean elements. Returns ------- distance : (P1, P2) array of dtype float 2D ndarray with value at an index (i, j) in the range [0, 1] - representing the extent of dissimilarity between ith keypoint of in - first image and jth keypoint in second image. + representing the hamming distance between ith vector in + array1 and jth vector in array2. """ - distance = np.zeros((descriptors1.shape[0], descriptors2.shape[0]), dtype=float) - for i in range(descriptors1.shape[0]): - for j in range(descriptors2.shape[0]): - distance[i, j] = hamming(descriptors1[i, :], descriptors2[j, :]) + distance = np.zeros((array1.shape[0], array2.shape[0]), dtype=float) + for i in range(array1.shape[0]): + for j in range(array2.shape[0]): + distance[i, j] = hamming(array1[i, :], array2[j, :]) return distance From 89c9b6161d67204966b6946993d688ab7ce2ff7e Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Wed, 10 Jul 2013 12:45:22 +0200 Subject: [PATCH 239/736] Fix casting in ransac example. --- doc/examples/plot_ransac.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/examples/plot_ransac.py b/doc/examples/plot_ransac.py index 26fd78c0..2de76619 100644 --- a/doc/examples/plot_ransac.py +++ b/doc/examples/plot_ransac.py @@ -21,7 +21,7 @@ y = 0.2 * x + 20 data = np.column_stack([x, y]) # add faulty data -faulty = np.array(30 * [(180, -100)]) +faulty = np.array(30 * [(180., -100)]) faulty += 5 * np.random.normal(size=faulty.shape) data[:faulty.shape[0]] = faulty From 11a2883c50e65ecc09ecbe2613e3795d9d6df7e9 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Wed, 10 Jul 2013 22:41:21 +0800 Subject: [PATCH 240/736] Broadcasting in pairwise_hamming_distance; numpy optimization in match_keypoints_brief --- skimage/feature/__init__.py | 4 +- skimage/feature/_brief.py | 82 ++++++++++++++--------------------- skimage/feature/_brief_cy.pyx | 2 +- skimage/feature/util.py | 20 +++------ 4 files changed, 42 insertions(+), 66 deletions(-) diff --git a/skimage/feature/__init__.py b/skimage/feature/__init__.py index 8192e323..8df1dc10 100644 --- a/skimage/feature/__init__.py +++ b/skimage/feature/__init__.py @@ -7,7 +7,7 @@ from .corner import (corner_kitchen_rosenfeld, corner_harris, corner_shi_tomasi, from .corner_cy import corner_moravec from .template import match_template from ._brief import brief, match_keypoints_brief -from .util import hamming_distance +from .util import pairwise_hamming_distance __all__ = ['daisy', 'hog', @@ -24,5 +24,5 @@ __all__ = ['daisy', 'corner_moravec', 'match_template', 'brief', - 'hamming_distance', + 'pairwise_hamming_distance', 'match_keypoints_brief'] diff --git a/skimage/feature/_brief.py b/skimage/feature/_brief.py index 7f3a9788..4c580d28 100644 --- a/skimage/feature/_brief.py +++ b/skimage/feature/_brief.py @@ -2,13 +2,13 @@ import numpy as np from scipy.ndimage.filters import gaussian_filter from ..util import img_as_float -from .util import _remove_border_keypoints, hamming_distance +from .util import _remove_border_keypoints, pairwise_hamming_distance from ._brief_cy import _brief_loop def brief(image, keypoints, descriptor_size=256, mode='normal', patch_size=49, - sample_seed=1, variance=2, return_keypoints=False): + sample_seed=1, variance=2): """Extract BRIEF Descriptor about given keypoints for a given image. Parameters @@ -35,9 +35,6 @@ def brief(image, keypoints, descriptor_size=256, mode='normal', patch_size=49, variance : float Variance of the Gaussian Low Pass filter applied on the image to alleviate noise sensitivity. Default is 2. - return_keypoints : bool - If True, return the Q keypoints (after filtering out the border - keypoints) about which the descriptors are extracted. Default is False. Returns ------- @@ -59,7 +56,7 @@ def brief(image, keypoints, descriptor_size=256, mode='normal', patch_size=49, Examples -------- >>> from skimage.feature.corner import * - >>> from skimage.feature import hamming_distance + >>> from skimage.feature import pairwise_hamming_distance >>> from skimage.feature._brief import * >>> square1 = np.zeros([8, 8], dtype=np.int32) >>> square1[2:6, 2:6] = 1 @@ -78,7 +75,7 @@ def brief(image, keypoints, descriptor_size=256, mode='normal', patch_size=49, [2, 5], [5, 2], [5, 5]]) - >>> descriptors1, keypoints1 = brief(square1, keypoints1, patch_size = 5, return_keypoints=True) + >>> descriptors1, keypoints1 = brief(square1, keypoints1, patch_size = 5) >>> keypoints1 array([[2, 2], [2, 5], @@ -102,30 +99,29 @@ def brief(image, keypoints, descriptor_size=256, mode='normal', patch_size=49, [2, 6], [6, 2], [6, 6]]) - >>> descriptors2, keypoints2 = brief(square2, keypoints2, patch_size = 5, return_keypoints=True) + >>> descriptors2, keypoints2 = brief(square2, keypoints2, patch_size = 5) >>> keypoints2 array([[2, 2], [2, 6], [6, 2], [6, 6]]) - >>> hamming_distance(descriptors1, descriptors2) + >>> pairwise_hamming_distance(descriptors1, descriptors2) array([[ 0.03125 , 0.3203125, 0.3671875, 0.6171875], [ 0.3203125, 0.03125 , 0.640625 , 0.375 ], [ 0.375 , 0.6328125, 0.0390625, 0.328125 ], [ 0.625 , 0.3671875, 0.34375 , 0.0234375]]) >>> match_keypoints_brief(keypoints1, descriptors1, keypoints2, descriptors2) - array([[[ 2., 2.], - [ 2., 5.], - [ 5., 2.], - [ 5., 5.]], + array([[[2, 2], + [2, 5], + [5, 2], + [5, 5]], - [[ 2., 2.], - [ 2., 6.], - [ 6., 2.], - [ 6., 6.]]]) + [[2, 2], + [2, 6], + [6, 2], + [6, 6]]]) """ - np.random.seed(sample_seed) image = np.squeeze(image) @@ -140,13 +136,15 @@ def brief(image, keypoints, descriptor_size=256, mode='normal', patch_size=49, image = np.ascontiguousarray(image) - keypoints = np.array(keypoints + 0.5, dtype=np.intp) + keypoints = np.array(keypoints + 0.5, dtype=np.intp, order='C') # Removing keypoints that are within (patch_size / 2) distance from the # image border keypoints = _remove_border_keypoints(image, keypoints, patch_size / 2) + keypoints = np.ascontiguousarray(keypoints) - descriptors = np.zeros((keypoints.shape[0], descriptor_size), dtype=bool) + descriptors = np.zeros((keypoints.shape[0], descriptor_size), dtype=bool, + order='C') # Sampling pairs of decision pixels in patch_size x patch_size window if mode == 'normal': @@ -172,28 +170,27 @@ def brief(image, keypoints, descriptor_size=256, mode='normal', patch_size=49, _brief_loop(image, descriptors.view(np.uint8), keypoints, pos1, pos2) - if return_keypoints: - return descriptors, keypoints - else: - return descriptors + return descriptors, keypoints + def match_keypoints_brief(keypoints1, descriptors1, keypoints2, descriptors2, threshold=0.15): - """Match keypoints described using BRIEF descriptors. + """Match keypoints described using BRIEF descriptors in one image to + those in second image. Parameters ---------- keypoints1 : (M, 2) ndarray - M Keypoints from the first image described using feature._brief.brief + M Keypoints from the first image described using skimage.feature.brief descriptors1 : (M, P) ndarray BRIEF descriptors of size P about M keypoints in the first image. keypoints2 : (N, 2) ndarray - N Keypoints from the second image described using feature._brief.brief + N Keypoints from the second image described using skimage.feature.brief descriptors2 : (N, P) ndarray BRIEF descriptors of size P about N keypoints in the second image. threshold : float in range [0, 1] - Threshold for removing matched keypoint pairs with hamming distance - greater than it. Default is 0.15 + Maximum allowable hamming distance between descriptors of two keypoints + in separate images to be regarded as a match. Default is 0.15. Returns ------- @@ -210,28 +207,13 @@ def match_keypoints_brief(keypoints1, descriptors1, keypoints2, if descriptors1.shape[1] != descriptors2.shape[1]: raise ValueError("Descriptor sizes for matching keypoints in both \ the images should be equal.") + # Get hamming distances between keeypoints1 and keypoints2 - distance = hamming_distance(descriptors1, descriptors2) + distance = pairwise_hamming_distance(descriptors1, descriptors2) - # For each keypoint in keypoints1, match it with the keypoint in keypoints2 - # that has minimum hamming distance - dist_matched_kp = np.amin(distance, axis=1) - index_matched_kp2 = distance.argmin(axis=1) - - # Remove the matched pairs which have hamming distance greater than the - # threshold - temp = np.zeros((keypoints1.shape[0], 3)) - temp[:, 0] = range(keypoints1.shape[0]) - temp[:, 1] = index_matched_kp2 - temp[:, 2] = dist_matched_kp - temp = temp[temp[:, 2] < threshold] - - matched_kp1 = keypoints1[np.int16(temp[:, 0])] - matched_kp2 = keypoints2[np.int16(temp[:, 1])] - - # Collecting matched keypoint pairs from their index pairs - matched_keypoint_pairs = np.zeros((2, matched_kp1.shape[0], 2)) - matched_keypoint_pairs[0, :, :] = matched_kp1 - matched_keypoint_pairs[1, :, :] = matched_kp2 + temp = distance > threshold + row_check = ~ np.all(temp, axis = 1) + matched_keypoints2 = keypoints2[np.argmin(distance, axis=1)] + matched_keypoint_pairs = np.array([keypoints1[row_check], matched_keypoints2[row_check]]) return matched_keypoint_pairs diff --git a/skimage/feature/_brief_cy.pyx b/skimage/feature/_brief_cy.pyx index 43d54f7a..c53d85fc 100644 --- a/skimage/feature/_brief_cy.pyx +++ b/skimage/feature/_brief_cy.pyx @@ -21,4 +21,4 @@ def _brief_loop(double[:, ::1] image, char[:, ::1] descriptors, kr = keypoints[k, 0] kc = keypoints[k, 1] if image[kr + pr0, kc + pc0] < image[kr + pr1, kc + pc1]: - descriptors[k, p] = True + descriptors[k, p] = True diff --git a/skimage/feature/util.py b/skimage/feature/util.py index 67eb93ca..8b5dd632 100644 --- a/skimage/feature/util.py +++ b/skimage/feature/util.py @@ -1,6 +1,3 @@ -import numpy as np -from scipy.spatial.distance import hamming - def _remove_border_keypoints(image, keypoints, dist): """Removes keypoints that are within dist pixels from the image border.""" @@ -15,9 +12,9 @@ def _remove_border_keypoints(image, keypoints, dist): return keypoints -def hamming_distance(array1, array2): - """A dissimilarity measure used for matching keypoints in different images - using binary feature descriptors like BRIEF etc. +def pairwise_hamming_distance(array1, array2): + """Calculate hamming dissimilarity measure between two sets of + boolean vectors. Parameters ---------- @@ -29,13 +26,10 @@ def hamming_distance(array1, array2): Returns ------- distance : (P1, P2) array of dtype float - 2D ndarray with value at an index (i, j) in the range [0, 1] - representing the hamming distance between ith vector in - array1 and jth vector in array2. + 2D ndarray with value at an index (i, j) representing the hamming + distance in the range [0, 1] between ith vector in array1 and jth + vector in array2. """ - distance = np.zeros((array1.shape[0], array2.shape[0]), dtype=float) - for i in range(array1.shape[0]): - for j in range(array2.shape[0]): - distance[i, j] = hamming(array1[i, :], array2[j, :]) + distance = (array1[:,None] != array2[None]).mean(axis=2) return distance From e7f9e673ac8166037d3b054e066fe5f72441fc7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Wed, 10 Jul 2013 18:51:18 +0200 Subject: [PATCH 241/736] minor modifications --- CONTRIBUTING.txt | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/CONTRIBUTING.txt b/CONTRIBUTING.txt index 20e6cff0..331b4ed8 100644 --- a/CONTRIBUTING.txt +++ b/CONTRIBUTING.txt @@ -154,25 +154,24 @@ detailing the test coverage:: skimage/filter/__init__ 1 1 100% ... -Activate Travis-ci for your fork (optional) ----------------------------------------- +Activate Travis-CI for your fork (optional) +------------------------------------------- -Travis-ci checks all unittests in the project to prevent breakage. +Travis-CI checks all unittests in the project to prevent breakage. -Before sending a pull request, you may want to check that Travis-ci +Before sending a pull request, you may want to check that Travis-CI successfully passes all tests. To do so, - * Go to `Travis-ci `__ and follow the Sign In link at the top:: + * Go to `Travis-CI `__ and follow the Sign In link at the top - * Go to your `profile page `__ - - * Switch on your scikit-image fork + * Go to your `profile page `__ and switch on your + scikit-image fork It corresponds to steps one and two in -`Travis-ci documentation `__ +`Travis-CI documentation `__ (Step three is already done in scikit-image). -Thus, as soon as you push your code to your fork, it will trigger Travis-ci, +Thus, as soon as you push your code to your fork, it will trigger Travis-CI, and you will receive an email notification when the process is done. Bugs From c8d5f707793f724fe3e47d04f59d19e73e98d55f Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Thu, 11 Jul 2013 01:38:17 +0800 Subject: [PATCH 242/736] Added _brief_cy.pyx in bento.info --- bento.info | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bento.info b/bento.info index 4cfa0173..a710dd2f 100644 --- a/bento.info +++ b/bento.info @@ -150,6 +150,9 @@ Library: Extension: skimage.filter.rank._crank16_bilateral Sources: skimage/filter/rank/_crank16_bilateral.pyx + Extension: skimage.feature._brief_cy + Sources: + skimage/feature/_brief_cy.pyx Executable: skivi Module: skimage.scripts.skivi From 8eec4770f402282f0a6b34c40fe715a9f785787a Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Thu, 11 Jul 2013 01:48:07 +0800 Subject: [PATCH 243/736] Moving _brief_cy.pyx changes in bento.info to the features section --- bento.info | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bento.info b/bento.info index a710dd2f..36d0e891 100644 --- a/bento.info +++ b/bento.info @@ -90,6 +90,9 @@ Library: Extension: skimage.morphology._greyreconstruct Sources: skimage/morphology/_greyreconstruct.pyx + Extension: skimage.feature._brief_cy + Sources: + skimage/feature/_brief_cy.pyx Extension: skimage.feature.corner_cy Sources: skimage/feature/corner_cy.pyx @@ -150,9 +153,6 @@ Library: Extension: skimage.filter.rank._crank16_bilateral Sources: skimage/filter/rank/_crank16_bilateral.pyx - Extension: skimage.feature._brief_cy - Sources: - skimage/feature/_brief_cy.pyx Executable: skivi Module: skimage.scripts.skivi From 70aedc4556a5684708ae43e3f2d25a377e76c97d Mon Sep 17 00:00:00 2001 From: Horea Christian Date: Wed, 10 Jul 2013 22:18:09 +0200 Subject: [PATCH 244/736] Removed blank lines in between parameters. --- skimage/util/montage.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/skimage/util/montage.py b/skimage/util/montage.py index ff7793d6..b9d8defa 100644 --- a/skimage/util/montage.py +++ b/skimage/util/montage.py @@ -31,14 +31,11 @@ def montage2d(arr_in, fill='mean', rescale_intensity=False, output_shape=(0, 0)) arr_in: ndarray, shape=[n_images, height, width] 3-dimensional input array representing an ensemble of n_images of equal shape (i.e. [height, width]). - fill: float or 'mean', optional How to fill the 2-dimensional output array when sqrt(n_images) is not an integer. If 'mean' is chosen, then fill = arr_in.mean(). - rescale_intensity: bool, optional Whether to rescale the intensity of each image to [0, 1]. - output_shape: tuple, optional The desired aspect ratio for the montage (default is square). @@ -99,7 +96,7 @@ def montage2d(arr_in, fill='mean', rescale_intensity=False, output_shape=(0, 0)) if fill == 'mean': fill = arr_in.mean() - n_missing = int((alpha_y * alpha_x) - n_images) + n_missing = int((alpha_y * alpha_x) - n_images) missing = np.ones((n_missing, height, width), dtype=arr_in.dtype) * fill arr_out = np.vstack((arr_in, missing)) From 083c13d7bd37fbaba0a54d73129173ab65fcd7a9 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Fri, 12 Jul 2013 00:30:10 +0800 Subject: [PATCH 245/736] Adding tests for BRIEF and pairwise_hamming_distance --- skimage/feature/tests/test_brief.py | 70 +++++++++++++++++++++++++++++ skimage/feature/tests/test_util.py | 27 +++++++++++ 2 files changed, 97 insertions(+) create mode 100644 skimage/feature/tests/test_brief.py create mode 100644 skimage/feature/tests/test_util.py diff --git a/skimage/feature/tests/test_brief.py b/skimage/feature/tests/test_brief.py new file mode 100644 index 00000000..7718e97c --- /dev/null +++ b/skimage/feature/tests/test_brief.py @@ -0,0 +1,70 @@ +import numpy as np +from numpy.testing import assert_array_equal, assert_raises +from skimage import data +from skimage import transform as tf +from skimage.feature.corner import corner_peaks, corner_harris +from skimage.color import rgb2gray +from skimage.feature import brief, match_keypoints_brief + + +def test_brief_color_image_unsupported_error(): + """Brief descriptors can be evaluated on gray-scale images only.""" + img = np.zeros((20, 20, 3)) + keypoints = [[7, 5], [11, 13]] + assert_raises(ValueError, brief, img, keypoints) + + +def test_match_keypoints_brief_lena_translation(): + """Test matched keypoints between lena image and its translated version.""" + img = data.lena() + img = rgb2gray(img) + img.shape + tform = tf.SimilarityTransform(scale=1, rotation=0, translation=(15, 20)) + translated_img = tf.warp(img, tform) + + keypoints1 = corner_peaks(corner_harris(img), min_distance=5) + descriptors1, keypoints1 = brief(img, keypoints1, descriptor_size=512) + + keypoints2 = corner_peaks(corner_harris(translated_img), min_distance=5) + descriptors2, keypoints2 = brief(translated_img, keypoints2, + descriptor_size=512) + + matched_keypoints = match_keypoints_brief(keypoints1, descriptors1, + keypoints2, descriptors2, + threshold=0.10) + + assert_array_equal(matched_keypoints[0,::], matched_keypoints[1,::] + + [20, 15]) + + +def test_match_keypoints_brief_lena_rotation(): + """Verify matched keypoints result between lena image and its rotated version + with the expected keypoint pairs.""" + img = data.lena() + img = rgb2gray(img) + img.shape + tform = tf.SimilarityTransform(scale=1, rotation=0.10, translation=(0, 0)) + rotated_img = tf.warp(img, tform) + + keypoints1 = corner_peaks(corner_harris(img), min_distance=5) + descriptors1, keypoints1 = brief(img, keypoints1, descriptor_size=512) + + keypoints2 = corner_peaks(corner_harris(rotated_img), min_distance=5) + descriptors2, keypoints2 = brief(rotated_img, keypoints2, + descriptor_size=512) + + matched_keypoints = match_keypoints_brief(keypoints1, descriptors1, + keypoints2, descriptors2, + threshold=0.07) + expected = np.array([[[248, 147], + [263, 272], + [271, 120], + [414, 70], + [454, 176]], + + [[232, 171], + [234, 298], + [258, 146], + [405, 111], + [435, 221]]]) + assert_array_equal(matched_keypoints, expected) diff --git a/skimage/feature/tests/test_util.py b/skimage/feature/tests/test_util.py new file mode 100644 index 00000000..6a397e5b --- /dev/null +++ b/skimage/feature/tests/test_util.py @@ -0,0 +1,27 @@ +import numpy as np +from numpy.testing import assert_array_equal +from skimage.feature.util import pairwise_hamming_distance + +def test_pairwise_hamming_distance_range(): + """Values of all the pairwise hamming distances should be in the range + [0, 1]. + """ + a = np.random.random_sample((10, 50)) > 0.5 + b = np.random.random_sample((20, 50)) > 0.5 + dist = pairwise_hamming_distance(a, b) + assert np.all((0 <= dist) & (dist <= 1)) + +def test_pairwise_hamming_distance_value(): + """The result of pairwise_hamming_distance of two fixed sets of boolean + vectors should be same as expected. + """ + np.random.seed(10) + a = np.random.random_sample((4, 100)) > 0.5 + np.random.seed(20) + b = np.random.random_sample((3, 100)) > 0.5 + result = pairwise_hamming_distance(a, b) + expected = np.array([[ 0.5 , 0.49, 0.44], + [ 0.44, 0.53, 0.52], + [ 0.4 , 0.55, 0.5 ], + [ 0.47, 0.48, 0.57]]) + assert_array_equal(result, expected) From 09cfe0b929a0044bea40e1cf68a6bdacd0384dff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Fri, 12 Jul 2013 12:51:04 +0200 Subject: [PATCH 246/736] examples: Avoid python3-specific syntax. from __future__ import ... is ineffective when the example script is exec-ed by sphinx, resulting in a SyntaxError when running under python2. Fixes gh-649. --- doc/examples/plot_local_binary_pattern.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/doc/examples/plot_local_binary_pattern.py b/doc/examples/plot_local_binary_pattern.py index 6789b238..c0169cc1 100644 --- a/doc/examples/plot_local_binary_pattern.py +++ b/doc/examples/plot_local_binary_pattern.py @@ -194,12 +194,12 @@ refs = { # classify rotated textures print('Rotated images matched against references using LBP:') -print('original: brick, rotated: 30deg, match result: ', end='') -print(match(refs, rotate(brick, angle=30, resize=False))) -print('original: brick, rotated: 70deg, match result: ', end='') -print(match(refs, rotate(brick, angle=70, resize=False))) -print('original: grass, rotated: 145deg, match result: ', end='') -print(match(refs, rotate(grass, angle=145, resize=False))) +print('original: brick, rotated: 30deg, match result: ', + match(refs, rotate(brick, angle=30, resize=False))) +print('original: brick, rotated: 70deg, match result: ', + match(refs, rotate(brick, angle=70, resize=False))) +print('original: grass, rotated: 145deg, match result: ', + match(refs, rotate(grass, angle=145, resize=False))) # plot histograms of LBP of textures fig, ((ax1, ax2, ax3), (ax4, ax5, ax6)) = plt.subplots(nrows=2, ncols=3, From e80dd18d7e7dbdfeaad63b384724d5b0fa816e4e Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Fri, 12 Jul 2013 18:26:11 +0200 Subject: [PATCH 247/736] Add tests for 'regular_grid()' --- skimage/util/tests/test_regular_grid.py | 33 +++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 skimage/util/tests/test_regular_grid.py diff --git a/skimage/util/tests/test_regular_grid.py b/skimage/util/tests/test_regular_grid.py new file mode 100644 index 00000000..1216b29d --- /dev/null +++ b/skimage/util/tests/test_regular_grid.py @@ -0,0 +1,33 @@ +import numpy as np +from nose.tools import raises +from numpy.testing import assert_equal +from skimage.util.regular_grid import regular_grid + + +def test_regular_grid_2d_8(): + ar = np.zeros((20, 40)) + g = regular_grid(ar.shape, 8) + assert_equal(g, [slice(5.0, None, 10.0), slice(5.0, None, 10.0)]) + ar[g] = 1 + assert_equal(ar.sum(), 8) + + +def test_regular_grid_2d_32(): + ar = np.zeros((20, 40)) + g = regular_grid(ar.shape, 32) + assert_equal(g, [slice(2.0, None, 5.0), slice(2.0, None, 5.0)]) + ar[g] = 1 + assert_equal(ar.sum(), 32) + + +def test_regular_grid_3d_8(): + ar = np.zeros((3, 20, 40)) + g = regular_grid(ar.shape, 8) + assert_equal(g, [slice(1.0, None, 3.0), slice(5.0, None, 10.0), + slice(5.0, None, 10.0)]) + ar[g] = 1 + assert_equal(ar.sum(), 8) + + +if __name__ == '__main__': + np.testing.run_module_suite() From f0ecc4d00de22f7e05421cdd3eb3f525e7813eb6 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Fri, 12 Jul 2013 18:28:11 +0200 Subject: [PATCH 248/736] Rename regular_grid.py --- skimage/util/__init__.py | 2 +- skimage/util/{regular_grid.py => _regular_grid.py} | 0 skimage/util/tests/test_regular_grid.py | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename skimage/util/{regular_grid.py => _regular_grid.py} (100%) diff --git a/skimage/util/__init__.py b/skimage/util/__init__.py index b196af34..7afcf54a 100644 --- a/skimage/util/__init__.py +++ b/skimage/util/__init__.py @@ -11,7 +11,7 @@ if chk < 18: # Use internal version for numpy versions < 1.8.x else: from numpy import pad del numpy, ver, chk -from .regular_grid import regular_grid +from ._regular_grid import regular_grid __all__ = ['img_as_float', diff --git a/skimage/util/regular_grid.py b/skimage/util/_regular_grid.py similarity index 100% rename from skimage/util/regular_grid.py rename to skimage/util/_regular_grid.py diff --git a/skimage/util/tests/test_regular_grid.py b/skimage/util/tests/test_regular_grid.py index 1216b29d..2808d99c 100644 --- a/skimage/util/tests/test_regular_grid.py +++ b/skimage/util/tests/test_regular_grid.py @@ -1,7 +1,7 @@ import numpy as np from nose.tools import raises from numpy.testing import assert_equal -from skimage.util.regular_grid import regular_grid +from skimage.util import regular_grid def test_regular_grid_2d_8(): From 9a17db19da341e4250d1168660db13b8fdb2075e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 30 Jun 2013 09:31:37 +0200 Subject: [PATCH 249/736] Refactor rank filter package for consistent naming --- skimage/filter/rank/__init__.py | 16 +- skimage/filter/rank/_rank.py | 773 ----------------- .../rank/{bilateral_rank.pyx => bilateral.py} | 8 +- ...ank16_bilateral.pyx => bilateral16_cy.pyx} | 2 +- .../rank/{_core16.pxd => core16_cy.pxd} | 0 .../rank/{_core16.pyx => core16_cy.pyx} | 2 +- .../filter/rank/{_core8.pxd => core8_cy.pxd} | 0 .../filter/rank/{_core8.pyx => core8_cy.pyx} | 0 skimage/filter/rank/generic.py | 776 ++++++++++++++++++ .../rank/{_crank16.pyx => generic16_cy.pyx} | 2 +- .../rank/{_crank8.pyx => generic8_cy.pyx} | 2 +- .../{percentile_rank.pyx => percentile.py} | 26 +- ...16_percentiles.pyx => percentile16_cy.pyx} | 2 +- ...nk8_percentiles.pyx => percentile8_cy.pyx} | 2 +- skimage/filter/setup.py | 37 +- 15 files changed, 822 insertions(+), 826 deletions(-) delete mode 100644 skimage/filter/rank/_rank.py rename skimage/filter/rank/{bilateral_rank.pyx => bilateral.py} (96%) rename skimage/filter/rank/{_crank16_bilateral.pyx => bilateral16_cy.pyx} (98%) rename skimage/filter/rank/{_core16.pxd => core16_cy.pxd} (100%) rename skimage/filter/rank/{_core16.pyx => core16_cy.pyx} (99%) rename skimage/filter/rank/{_core8.pxd => core8_cy.pxd} (100%) rename skimage/filter/rank/{_core8.pyx => core8_cy.pyx} (100%) rename skimage/filter/rank/{_crank16.pyx => generic16_cy.pyx} (99%) rename skimage/filter/rank/{_crank8.pyx => generic8_cy.pyx} (99%) rename skimage/filter/rank/{percentile_rank.pyx => percentile.py} (94%) rename skimage/filter/rank/{_crank16_percentiles.pyx => percentile16_cy.pyx} (99%) rename skimage/filter/rank/{_crank8_percentiles.pyx => percentile8_cy.pyx} (99%) diff --git a/skimage/filter/rank/__init__.py b/skimage/filter/rank/__init__.py index deceaade..9ad816ce 100644 --- a/skimage/filter/rank/__init__.py +++ b/skimage/filter/rank/__init__.py @@ -1,11 +1,11 @@ -from ._rank import (autolevel, bottomhat, equalize, gradient, maximum, mean, - meansubtraction, median, minimum, modal, morph_contr_enh, - pop, threshold, tophat, noise_filter, entropy, otsu) -from .percentile_rank import (percentile_autolevel, percentile_gradient, - percentile_mean, percentile_mean_subtraction, - percentile_morph_contr_enh, percentile, - percentile_pop, percentile_threshold) -from .bilateral_rank import bilateral_mean, bilateral_pop +from .generic import (autolevel, bottomhat, equalize, gradient, maximum, mean, + meansubtraction, median, minimum, modal, morph_contr_enh, + pop, threshold, tophat, noise_filter, entropy, otsu) +from .percentile import (percentile_autolevel, percentile_gradient, + percentile_mean, percentile_mean_subtraction, + percentile_morph_contr_enh, percentile, + percentile_pop, percentile_threshold) +from .bilateral import bilateral_mean, bilateral_pop __all__ = ['autolevel', diff --git a/skimage/filter/rank/_rank.py b/skimage/filter/rank/_rank.py deleted file mode 100644 index ea58ad86..00000000 --- a/skimage/filter/rank/_rank.py +++ /dev/null @@ -1,773 +0,0 @@ -"""The local histogram is computed using a sliding window similar to the method -described in [1]_. - -Input image can be 8-bit or 16-bit with a value < 4096 (i.e. 12 bit), for 16-bit -input images, the number of histogram bins is determined from the maximum value -present in the image. - -Result image is 8 or 16-bit with respect to the input image. - -References ----------- - -.. [1] Huang, T. ,Yang, G. ; Tang, G.. "A fast two-dimensional - median filtering algorithm", IEEE Transactions on Acoustics, Speech and - Signal Processing, Feb 1979. Volume: 27 , Issue: 1, Page(s): 13 - 18. - -""" - -import numpy as np -from skimage import img_as_ubyte, img_as_uint -from skimage.filter.rank import _crank8, _crank16 -from skimage.filter.rank.generic import find_bitdepth - - -__all__ = ['autolevel', 'bottomhat', 'equalize', 'gradient', 'maximum', 'mean', - 'meansubtraction', 'median', 'minimum', 'modal', 'morph_contr_enh', - 'pop', 'threshold', 'tophat', 'noise_filter', 'entropy', 'otsu'] - - -def _apply(func8, func16, image, selem, out, mask, shift_x, shift_y): - selem = img_as_ubyte(selem > 0) - image = np.ascontiguousarray(image) - - if mask is None: - mask = np.ones(image.shape, dtype=np.uint8) - else: - mask = np.ascontiguousarray(mask) - mask = img_as_ubyte(mask) - - if image is out: - raise NotImplementedError("Cannot perform rank operation in place.") - - is_8bit = image.dtype in (np.uint8, np.int8) - - if func8 is not None and (is_8bit or func16 is None): - out = _apply8(func8, image, selem, out, mask, shift_x, shift_y) - else: - image = img_as_uint(image) - if out is None: - out = np.zeros(image.shape, dtype=np.uint16) - bitdepth = find_bitdepth(image) - if bitdepth > 11: - image = image >> 4 - bitdepth = find_bitdepth(image) - func16(image, selem, shift_x=shift_x, shift_y=shift_y, mask=mask, - bitdepth=bitdepth + 1, out=out) - - return out - - -def _apply8(func8, image, selem, out, mask, shift_x, shift_y): - if out is None: - out = np.zeros(image.shape, dtype=np.uint8) - image = img_as_ubyte(image) - func8(image, selem, shift_x=shift_x, shift_y=shift_y, - mask=mask, out=out) - return out - - -def autolevel(image, selem, out=None, mask=None, shift_x=False, shift_y=False): - """Autolevel image using local histogram. - - Parameters - ---------- - image : ndarray - Image array (uint8 array or uint16). If image is uint16, the algorithm - uses max. 12bit histogram, an exception will be raised if image has a - value > 4095. - selem : ndarray - The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray - If None, a new array will be allocated. - mask : ndarray (uint8) - Mask array that defines (>0) area of the image included in the local - neighborhood. If None, the complete image is used (default). - shift_x, shift_y : int - Offset added to the structuring element center point. Shift is bounded - to the structuring element sizes (center must be inside the given - structuring element). - - Returns - ------- - out : uint8 array or uint16 array (same as input image) - The result of the local autolevel. - - Examples - -------- - >>> from skimage import data - >>> from skimage.morphology import disk - >>> from skimage.filter.rank import autolevel - >>> # Load test image - >>> ima = data.camera() - >>> # Stretch image contrast locally - >>> auto = autolevel(ima, disk(20)) - - """ - - return _apply(_crank8.autolevel, _crank16.autolevel, image, selem, out=out, - mask=mask, shift_x=shift_x, shift_y=shift_y) - - -def bottomhat(image, selem, out=None, mask=None, shift_x=False, shift_y=False): - """Returns greyscale local bottomhat of an image. - - Parameters - ---------- - image : ndarray - Image array (uint8 array or uint16). If image is uint16, the algorithm - uses max. 12bit histogram, an exception will be raised if image has a - value > 4095. - selem : ndarray - The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray - If None, a new array will be allocated. - mask : ndarray (uint8) - Mask array that defines (>0) area of the image included in the local - neighborhood. If None, the complete image is used (default). - shift_x, shift_y : int - Offset added to the structuring element center point. Shift is bounded - to the structuring element sizes (center must be inside the given - structuring element). - - Returns - ------- - local bottomhat : uint8 array or uint16 array depending on input image - The result of the local bottomhat. - - """ - - return _apply(_crank8.bottomhat, _crank16.bottomhat, image, selem, out=out, - mask=mask, shift_x=shift_x, shift_y=shift_y) - - -def equalize(image, selem, out=None, mask=None, shift_x=False, shift_y=False): - """Equalize image using local histogram. - - Parameters - ---------- - image : ndarray - Image array (uint8 array or uint16). If image is uint16, the algorithm - uses max. 12bit histogram, an exception will be raised if image has a - value > 4095. - selem : ndarray - The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray - If None, a new array will be allocated. - mask : ndarray (uint8) - Mask array that defines (>0) area of the image included in the local - neighborhood. If None, the complete image is used (default). - shift_x, shift_y : int - Offset added to the structuring element center point. Shift is bounded - to the structuring element sizes (center must be inside the given - structuring element). - - Returns - ------- - out : uint8 array or uint16 array (same as input image) - The result of the local equalize. - - Examples - -------- - >>> from skimage import data - >>> from skimage.morphology import disk - >>> from skimage.filter.rank import equalize - >>> # Load test image - >>> ima = data.camera() - >>> # Local equalization - >>> equ = equalize(ima, disk(20)) - - """ - - return _apply(_crank8.equalize, _crank16.equalize, image, selem, out=out, - mask=mask, shift_x=shift_x, shift_y=shift_y) - - -def gradient(image, selem, out=None, mask=None, shift_x=False, shift_y=False): - """Return greyscale local gradient of an image (i.e. local maximum - local - minimum). - - - Parameters - ---------- - image : ndarray - Image array (uint8 array or uint16). If image is uint16, the algorithm - uses max. 12bit histogram, an exception will be raised if image has a - value > 4095. - selem : ndarray - The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray - If None, a new array will be allocated. - mask : ndarray (uint8) - Mask array that defines (>0) area of the image included in the local - neighborhood. If None, the complete image is used (default). - shift_x, shift_y : int - Offset added to the structuring element center point. Shift is bounded - to the structuring element sizes (center must be inside the given - structuring element). - - Returns - ------- - out : uint8 array or uint16 array (same as input image) - The local gradient. - - """ - - return _apply(_crank8.gradient, _crank16.gradient, image, selem, out=out, - mask=mask, shift_x=shift_x, shift_y=shift_y) - - -def maximum(image, selem, out=None, mask=None, shift_x=False, shift_y=False): - """Return greyscale local maximum of an image. - - - Parameters - ---------- - image : ndarray - Image array (uint8 array or uint16). If image is uint16, the algorithm - uses max. 12bit histogram, an exception will be raised if image has a - value > 4095. - selem : ndarray - The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray - If None, a new array will be allocated. - mask : ndarray (uint8) - Mask array that defines (>0) area of the image included in the local - neighborhood. If None, the complete image is used (default). - shift_x, shift_y : int - Offset added to the structuring element center point. Shift is bounded - to the structuring element sizes (center must be inside the given - structuring element). - - Returns - ------- - out : uint8 array or uint16 array (same as input image) - The local maximum. - - See also - -------- - skimage.morphology.dilation - - Note - ---- - * input image can be 8-bit or 16-bit with a value < 4096 (i.e. 12 bit) - * the lower algorithm complexity makes the rank.maximum() more efficient for - larger images and structuring elements - - """ - - return _apply(_crank8.maximum, _crank16.maximum, image, selem, out=out, - mask=mask, shift_x=shift_x, shift_y=shift_y) - - -def mean(image, selem, out=None, mask=None, shift_x=False, shift_y=False): - """Return greyscale local mean of an image. - - Parameters - ---------- - image : ndarray - Image array (uint8 array or uint16). If image is uint16, the algorithm - uses max. 12bit histogram, an exception will be raised if image has a - value > 4095. - selem : ndarray - The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray - If None, a new array will be allocated. - mask : ndarray (uint8) - Mask array that defines (>0) area of the image included in the local - neighborhood. If None, the complete image is used (default). - shift_x, shift_y : int - Offset added to the structuring element center point. Shift is bounded - to the structuring element sizes (center must be inside the given - structuring element). - - Returns - ------- - out : uint8 array or uint16 array (same as input image) - The local mean. - - Examples - -------- - >>> from skimage import data - >>> from skimage.morphology import disk - >>> from skimage.filter.rank import mean - >>> # Load test image - >>> ima = data.camera() - >>> # Local mean - >>> avg = mean(ima, disk(20)) - - """ - - return _apply(_crank8.mean, _crank16.mean, image, selem, out=out, - mask=mask, shift_x=shift_x, shift_y=shift_y) - - -def meansubtraction(image, selem, out=None, mask=None, shift_x=False, - shift_y=False): - """Return image subtracted from its local mean. - - Parameters - ---------- - image : ndarray - Image array (uint8 array or uint16). If image is uint16, the algorithm - uses max. 12bit histogram, an exception will be raised if image has a - value > 4095. - selem : ndarray - The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray - If None, a new array will be allocated. - mask : ndarray (uint8) - Mask array that defines (>0) area of the image included in the local - neighborhood. If None, the complete image is used (default). - shift_x, shift_y : int - Offset added to the structuring element center point. Shift is bounded - to the structuring element sizes (center must be inside the given - structuring element). - - Returns - ------- - out : uint8 array or uint16 array (same as input image) - The result of the local meansubtraction. - - """ - - return _apply(_crank8.meansubtraction, _crank16.meansubtraction, image, - selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) - - -def median(image, selem, out=None, mask=None, shift_x=False, shift_y=False): - """Return greyscale local median of an image. - - Parameters - ---------- - image : ndarray - Image array (uint8 array or uint16). If image is uint16, the algorithm - uses max. 12bit histogram, an exception will be raised if image has a - value > 4095. - selem : ndarray - The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray - If None, a new array will be allocated. - mask : ndarray (uint8) - Mask array that defines (>0) area of the image included in the local - neighborhood. If None, the complete image is used (default). - shift_x, shift_y : int - Offset added to the structuring element center point. Shift is bounded - to the structuring element sizes (center must be inside the given - structuring element). - - Returns - ------- - out : uint8 array or uint16 array (same as input image) - The local median. - - Examples - -------- - >>> from skimage import data - >>> from skimage.morphology import disk - >>> from skimage.filter.rank import median - >>> # Load test image - >>> ima = data.camera() - >>> # Local mean - >>> avg = median(ima, disk(20)) - - """ - - return _apply(_crank8.median, _crank16.median, image, selem, out=out, - mask=mask, shift_x=shift_x, shift_y=shift_y) - - -def minimum(image, selem, out=None, mask=None, shift_x=False, shift_y=False): - """Return greyscale local minimum of an image. - - Parameters - ---------- - image : ndarray - Image array (uint8 array or uint16). If image is uint16, the algorithm - uses max. 12bit histogram, an exception will be raised if image has a - value > 4095. - selem : ndarray - The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray - If None, a new array will be allocated. - mask : ndarray (uint8) - Mask array that defines (>0) area of the image included in the local - neighborhood. If None, the complete image is used (default). - shift_x, shift_y : int - Offset added to the structuring element center point. Shift is bounded - to the structuring element sizes (center must be inside the given - structuring element). - - Returns - ------- - out : uint8 array or uint16 array (same as input image) - The local minimum. - - See also - -------- - skimage.morphology.erosion - - Note - ---- - * input image can be 8-bit or 16-bit with a value < 4096 (i.e. 12 bit) - * the lower algorithm complexity makes the rank.minimum() more efficient - for larger images and structuring elements - - """ - - return _apply(_crank8.minimum, _crank16.minimum, image, selem, out=out, - mask=mask, shift_x=shift_x, shift_y=shift_y) - - -def modal(image, selem, out=None, mask=None, shift_x=False, shift_y=False): - """Return greyscale local mode of an image. - - Parameters - ---------- - image : ndarray - Image array (uint8 array or uint16). If image is uint16, the algorithm - uses max. 12bit histogram, an exception will be raised if image has a - value > 4095. - selem : ndarray - The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray - If None, a new array will be allocated. - mask : ndarray (uint8) - Mask array that defines (>0) area of the image included in the local - neighborhood. If None, the complete image is used (default). - shift_x, shift_y : int - Offset added to the structuring element center point. Shift is bounded - to the structuring element sizes (center must be inside the given - structuring element). - - Returns - ------- - out : uint8 array or uint16 array (same as input image) - The local modal. - - """ - - return _apply(_crank8.modal, _crank16.modal, image, selem, out=out, - mask=mask, shift_x=shift_x, shift_y=shift_y) - - -def morph_contr_enh(image, selem, out=None, mask=None, shift_x=False, - shift_y=False): - """Enhance an image replacing each pixel by the local maximum if pixel - greylevel is closest to maximimum than local minimum OR local minimum - otherwise. - - Parameters - ---------- - image : ndarray - Image array (uint8 array or uint16). If image is uint16, the algorithm - uses max. 12bit histogram, an exception will be raised if image has a - value > 4095. - selem : ndarray - The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray - If None, a new array will be allocated. - mask : ndarray (uint8) - Mask array that defines (>0) area of the image included in the local - neighborhood. If None, the complete image is used (default). - shift_x, shift_y : int - Offset added to the structuring element center point. Shift is bounded - to the structuring element sizes (center must be inside the given - structuring element). - - Returns - ------- - out : uint8 array or uint16 array (same as input image) - The result of the local morph_contr_enh. - - Examples - -------- - >>> from skimage import data - >>> from skimage.morphology import disk - >>> from skimage.filter.rank import morph_contr_enh - >>> # Load test image - >>> ima = data.camera() - >>> # Local mean - >>> avg = morph_contr_enh(ima, disk(20)) - - """ - - return _apply(_crank8.morph_contr_enh, _crank16.morph_contr_enh, image, - selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) - - -def pop(image, selem, out=None, mask=None, shift_x=False, shift_y=False): - """Return the number (population) of pixels actually inside the - neighborhood. - - Parameters - ---------- - image : ndarray - Image array (uint8 array or uint16). If image is uint16, the algorithm - uses max. 12bit histogram, an exception will be raised if image has a - value > 4095. - selem : ndarray - The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray - If None, a new array will be allocated. - mask : ndarray (uint8) - Mask array that defines (>0) area of the image included in the local - neighborhood. If None, the complete image is used (default). - shift_x, shift_y : int - Offset added to the structuring element center point. Shift is bounded - to the structuring element sizes (center must be inside the given - structuring element). - - Returns - ------- - out : uint8 array or uint16 array (same as input image) - The number of pixels belonging to the neighborhood. - - Examples - -------- - >>> # Local mean - >>> from skimage.morphology import square - >>> import skimage.filter.rank as rank - >>> ima = 255 * np.array([[0, 0, 0, 0, 0], - ... [0, 1, 1, 1, 0], - ... [0, 1, 1, 1, 0], - ... [0, 1, 1, 1, 0], - ... [0, 0, 0, 0, 0]], dtype=np.uint8) - >>> rank.pop(ima, square(3)) - array([[4, 6, 6, 6, 4], - [6, 9, 9, 9, 6], - [6, 9, 9, 9, 6], - [6, 9, 9, 9, 6], - [4, 6, 6, 6, 4]], dtype=uint8) - - """ - - return _apply(_crank8.pop, _crank16.pop, image, selem, out=out, - mask=mask, shift_x=shift_x, shift_y=shift_y) - - -def threshold(image, selem, out=None, mask=None, shift_x=False, shift_y=False): - """Return greyscale local threshold of an image. - - Parameters - ---------- - image : ndarray - Image array (uint8 array or uint16). If image is uint16, the algorithm - uses max. 12bit histogram, an exception will be raised if image has a - value > 4095. - selem : ndarray - The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray - If None, a new array will be allocated. - mask : ndarray (uint8) - Mask array that defines (>0) area of the image included in the local - neighborhood. If None, the complete image is used (default). - shift_x, shift_y : int - Offset added to the structuring element center point. Shift is bounded - to the structuring element sizes (center must be inside the given - structuring element). - - Returns - ------- - out : uint8 array or uint16 array (same as input image) - The result of the local threshold. - - Examples - -------- - >>> # Local threshold - >>> from skimage.morphology import square - >>> from skimage.filter.rank import threshold - >>> ima = 255 * np.array([[0, 0, 0, 0, 0], - ... [0, 1, 1, 1, 0], - ... [0, 1, 1, 1, 0], - ... [0, 1, 1, 1, 0], - ... [0, 0, 0, 0, 0]], dtype=np.uint8) - >>> threshold(ima, square(3)) - array([[0, 0, 0, 0, 0], - [0, 1, 1, 1, 0], - [0, 1, 0, 1, 0], - [0, 1, 1, 1, 0], - [0, 0, 0, 0, 0]], dtype=uint8) - - """ - - return _apply(_crank8.threshold, _crank16.threshold, image, selem, out=out, - mask=mask, shift_x=shift_x, shift_y=shift_y) - - -def tophat(image, selem, out=None, mask=None, shift_x=False, shift_y=False): - """Return greyscale local tophat of an image. - - Parameters - ---------- - image : ndarray - Image array (uint8 array or uint16). If image is uint16, the algorithm - uses max. 12bit histogram, an exception will be raised if image has a - value > 4095. - selem : ndarray - The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray - If None, a new array will be allocated. - mask : ndarray (uint8) - Mask array that defines (>0) area of the image included in the local - neighborhood. If None, the complete image is used (default). - shift_x, shift_y : int - Offset added to the structuring element center point. Shift is bounded - to the structuring element sizes (center must be inside the given - structuring element). - - Returns - ------- - out : uint8 array or uint16 array (same as input image) - The image tophat. - - """ - - return _apply(_crank8.tophat, _crank16.tophat, image, selem, out=out, - mask=mask, shift_x=shift_x, shift_y=shift_y) - - -def noise_filter(image, selem, out=None, mask=None, shift_x=False, - shift_y=False): - """Returns the noise feature as described in [Hashimoto12]_ - - Parameters - ---------- - image : ndarray - Image array (uint8 array or uint16). If image is uint16, the algorithm - uses max. 12bit histogram, an exception will be raised if image has a - value > 4095. - selem : ndarray - The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray - If None, a new array will be allocated. - mask : ndarray (uint8) - Mask array that defines (>0) area of the image included in the local - neighborhood. If None, the complete image is used (default). - shift_x, shift_y : int - Offset added to the structuring element center point. Shift is bounded - to the structuring element sizes (center must be inside the given - structuring element). - - References - ---------- - .. [Hashimoto12] N. Hashimoto et al. Referenceless image quality evaluation - for whole slide imaging. J Pathol Inform 2012;3:9. - - Returns - ------- - out : uint8 array or uint16 array (same as input image) - The image noise. - - """ - - # ensure that the central pixel in the structuring element is empty - centre_r = int(selem.shape[0] / 2) + shift_y - centre_c = int(selem.shape[1] / 2) + shift_x - # make a local copy - selem_cpy = selem.copy() - selem_cpy[centre_r, centre_c] = 0 - - return _apply(_crank8.noise_filter, None, image, selem_cpy, out=out, - mask=mask, shift_x=shift_x, shift_y=shift_y) - - -def entropy(image, selem, out=None, mask=None, shift_x=False, shift_y=False): - """Returns the entropy [1]_ computed locally. Entropy is computed - using base 2 logarithm i.e. the filter returns the minimum number of - bits needed to encode local greylevel distribution. - - Parameters - ---------- - image : ndarray - Image array (uint8 array or uint16). If image is uint16, the algorithm - uses max. 12bit histogram, an exception will be raised if image has a - value > 4095. - selem : ndarray - The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray - If None, a new array will be allocated. - mask : ndarray (uint8) - Mask array that defines (>0) area of the image included in the local - neighborhood. If None, the complete image is used (default). - shift_x, shift_y : int - Offset added to the structuring element center point. Shift is bounded - to the structuring element sizes (center must be inside the given - structuring element). - - Returns - ------- - out : uint8 array or uint16 array (same as input image) - entropy x10 (uint8 images) and entropy x1000 (uint16 images) - - References - ---------- - .. [1] http://en.wikipedia.org/wiki/Entropy_(information_theory) - - Examples - -------- - >>> # Local entropy - >>> from skimage import data - >>> from skimage.filter.rank import entropy - >>> from skimage.morphology import disk - >>> # defining a 8- and a 16-bit test images - >>> a8 = data.camera() - >>> a16 = data.camera().astype(np.uint16) * 4 - >>> # pixel values contain 10x the local entropy - >>> ent8 = entropy(a8, disk(5)) - >>> # pixel values contain 1000x the local entropy - >>> ent16 = entropy(a16, disk(5)) - - """ - - return _apply(_crank8.entropy, _crank16.entropy, image, selem, out=out, - mask=mask, shift_x=shift_x, shift_y=shift_y) - - -def otsu(image, selem, out=None, mask=None, shift_x=False, shift_y=False): - """Returns the Otsu's threshold value for each pixel. - - Parameters - ---------- - image : ndarray - Image array (uint8 array). - selem : ndarray - The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray - If None, a new array will be allocated. - mask : ndarray (uint8) - Mask array that defines (>0) area of the image included in the local - neighborhood. If None, the complete image is used (default). - shift_x, shift_y : int - Offset added to the structuring element center point. Shift is bounded - to the structuring element sizes (center must be inside the given - structuring element). - - Returns - ------- - out : uint8 array - Otsu's threshold values - - References - ---------- - .. [otsu] http://en.wikipedia.org/wiki/Otsu's_method - - Notes - ----- - * input image are 8-bit only - - Examples - -------- - >>> # Local entropy - >>> from skimage import data - >>> from skimage.filter.rank import otsu - >>> from skimage.morphology import disk - >>> # defining a 8-bit test images - >>> a8 = data.camera() - >>> loc_otsu = otsu(a8, disk(5)) - >>> thresh_image = a8 >= loc_otsu - - """ - - return _apply(_crank8.otsu, None, image, selem, out=out, - mask=mask, shift_x=shift_x, shift_y=shift_y) diff --git a/skimage/filter/rank/bilateral_rank.pyx b/skimage/filter/rank/bilateral.py similarity index 96% rename from skimage/filter/rank/bilateral_rank.pyx rename to skimage/filter/rank/bilateral.py index e2a1fcf3..a111dd7c 100644 --- a/skimage/filter/rank/bilateral_rank.pyx +++ b/skimage/filter/rank/bilateral.py @@ -28,8 +28,8 @@ References import numpy as np from skimage import img_as_ubyte -from skimage.filter.rank import _crank16_bilateral -from skimage.filter.rank.generic import find_bitdepth +from . import bilateral16_cy +from .generic import find_bitdepth __all__ = ['bilateral_mean', 'bilateral_pop'] @@ -130,7 +130,7 @@ def bilateral_mean(image, selem, out=None, mask=None, shift_x=False, >>> bilat_ima = bilateral_mean(ima, disk(20), s0=10,s1=10) """ - return _apply(None, _crank16_bilateral.mean, image, selem, out=out, + return _apply(None, _bilateral16_cy.mean, image, selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y, s0=s0, s1=s1) @@ -188,5 +188,5 @@ def bilateral_pop(image, selem, out=None, mask=None, shift_x=False, """ - return _apply(None, _crank16_bilateral.pop, image, selem, out=out, + return _apply(None, _bilateral16_cy.pop, image, selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y, s0=s0, s1=s1) diff --git a/skimage/filter/rank/_crank16_bilateral.pyx b/skimage/filter/rank/bilateral16_cy.pyx similarity index 98% rename from skimage/filter/rank/_crank16_bilateral.pyx rename to skimage/filter/rank/bilateral16_cy.pyx index e431e42b..d9f68f73 100644 --- a/skimage/filter/rank/_crank16_bilateral.pyx +++ b/skimage/filter/rank/bilateral16_cy.pyx @@ -4,7 +4,7 @@ #cython: wraparound=False cimport numpy as cnp -from skimage.filter.rank._core16 cimport _core16 +from .core16_cy cimport _core16 # ----------------------------------------------------------------- diff --git a/skimage/filter/rank/_core16.pxd b/skimage/filter/rank/core16_cy.pxd similarity index 100% rename from skimage/filter/rank/_core16.pxd rename to skimage/filter/rank/core16_cy.pxd diff --git a/skimage/filter/rank/_core16.pyx b/skimage/filter/rank/core16_cy.pyx similarity index 99% rename from skimage/filter/rank/_core16.pyx rename to skimage/filter/rank/core16_cy.pyx index 0c7a7a82..63bcdff1 100644 --- a/skimage/filter/rank/_core16.pyx +++ b/skimage/filter/rank/core16_cy.pyx @@ -7,7 +7,7 @@ import numpy as np cimport numpy as cnp from libc.stdlib cimport malloc, free -from _core8 cimport is_in_mask +from .core8_cy cimport is_in_mask cdef inline int int_max(int a, int b): diff --git a/skimage/filter/rank/_core8.pxd b/skimage/filter/rank/core8_cy.pxd similarity index 100% rename from skimage/filter/rank/_core8.pxd rename to skimage/filter/rank/core8_cy.pxd diff --git a/skimage/filter/rank/_core8.pyx b/skimage/filter/rank/core8_cy.pyx similarity index 100% rename from skimage/filter/rank/_core8.pyx rename to skimage/filter/rank/core8_cy.pyx diff --git a/skimage/filter/rank/generic.py b/skimage/filter/rank/generic.py index 94fc3130..124d4f25 100644 --- a/skimage/filter/rank/generic.py +++ b/skimage/filter/rank/generic.py @@ -1,3 +1,31 @@ +"""The local histogram is computed using a sliding window similar to the method +described in [1]_. + +Input image can be 8-bit or 16-bit with a value < 4096 (i.e. 12 bit), for 16-bit +input images, the number of histogram bins is determined from the maximum value +present in the image. + +Result image is 8 or 16-bit with respect to the input image. + +References +---------- + +.. [1] Huang, T. ,Yang, G. ; Tang, G.. "A fast two-dimensional + median filtering algorithm", IEEE Transactions on Acoustics, Speech and + Signal Processing, Feb 1979. Volume: 27 , Issue: 1, Page(s): 13 - 18. + +""" + +import numpy as np +from skimage import img_as_ubyte, img_as_uint +from . import generic8_cy, generic16_cy + + +__all__ = ['autolevel', 'bottomhat', 'equalize', 'gradient', 'maximum', 'mean', + 'meansubtraction', 'median', 'minimum', 'modal', 'morph_contr_enh', + 'pop', 'threshold', 'tophat', 'noise_filter', 'entropy', 'otsu'] + + import numpy as np @@ -9,3 +37,751 @@ def find_bitdepth(image): return int(np.log2(umax)) else: return 1 + + +def _apply(func8, func16, image, selem, out, mask, shift_x, shift_y): + selem = img_as_ubyte(selem > 0) + image = np.ascontiguousarray(image) + + if mask is None: + mask = np.ones(image.shape, dtype=np.uint8) + else: + mask = np.ascontiguousarray(mask) + mask = img_as_ubyte(mask) + + if image is out: + raise NotImplementedError("Cannot perform rank operation in place.") + + is_8bit = image.dtype in (np.uint8, np.int8) + + if func8 is not None and (is_8bit or func16 is None): + out = _apply8(func8, image, selem, out, mask, shift_x, shift_y) + else: + image = img_as_uint(image) + if out is None: + out = np.zeros(image.shape, dtype=np.uint16) + bitdepth = find_bitdepth(image) + if bitdepth > 11: + image = image >> 4 + bitdepth = find_bitdepth(image) + func16(image, selem, shift_x=shift_x, shift_y=shift_y, mask=mask, + bitdepth=bitdepth + 1, out=out) + + return out + + +def _apply8(func8, image, selem, out, mask, shift_x, shift_y): + if out is None: + out = np.zeros(image.shape, dtype=np.uint8) + image = img_as_ubyte(image) + func8(image, selem, shift_x=shift_x, shift_y=shift_y, + mask=mask, out=out) + return out + + +def autolevel(image, selem, out=None, mask=None, shift_x=False, shift_y=False): + """Autolevel image using local histogram. + + Parameters + ---------- + image : ndarray + Image array (uint8 array or uint16). If image is uint16, the algorithm + uses max. 12bit histogram, an exception will be raised if image has a + value > 4095. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + If None, a new array will be allocated. + mask : ndarray (uint8) + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + + Returns + ------- + out : uint8 array or uint16 array (same as input image) + The result of the local autolevel. + + Examples + -------- + >>> from skimage import data + >>> from skimage.morphology import disk + >>> from skimage.filter.rank import autolevel + >>> # Load test image + >>> ima = data.camera() + >>> # Stretch image contrast locally + >>> auto = autolevel(ima, disk(20)) + + """ + + return _apply(generic8_cy.autolevel, generic16_cy.autolevel, image, selem, + out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) + + +def bottomhat(image, selem, out=None, mask=None, shift_x=False, shift_y=False): + """Returns greyscale local bottomhat of an image. + + Parameters + ---------- + image : ndarray + Image array (uint8 array or uint16). If image is uint16, the algorithm + uses max. 12bit histogram, an exception will be raised if image has a + value > 4095. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + If None, a new array will be allocated. + mask : ndarray (uint8) + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + + Returns + ------- + local bottomhat : uint8 array or uint16 array depending on input image + The result of the local bottomhat. + + """ + + return _apply(generic8_cy.bottomhat, generic16_cy.bottomhat, image, selem, + out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) + + +def equalize(image, selem, out=None, mask=None, shift_x=False, shift_y=False): + """Equalize image using local histogram. + + Parameters + ---------- + image : ndarray + Image array (uint8 array or uint16). If image is uint16, the algorithm + uses max. 12bit histogram, an exception will be raised if image has a + value > 4095. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + If None, a new array will be allocated. + mask : ndarray (uint8) + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + + Returns + ------- + out : uint8 array or uint16 array (same as input image) + The result of the local equalize. + + Examples + -------- + >>> from skimage import data + >>> from skimage.morphology import disk + >>> from skimage.filter.rank import equalize + >>> # Load test image + >>> ima = data.camera() + >>> # Local equalization + >>> equ = equalize(ima, disk(20)) + + """ + + return _apply(generic8_cy.equalize, generic16_cy.equalize, image, selem, + out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) + + +def gradient(image, selem, out=None, mask=None, shift_x=False, shift_y=False): + """Return greyscale local gradient of an image (i.e. local maximum - local + minimum). + + + Parameters + ---------- + image : ndarray + Image array (uint8 array or uint16). If image is uint16, the algorithm + uses max. 12bit histogram, an exception will be raised if image has a + value > 4095. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + If None, a new array will be allocated. + mask : ndarray (uint8) + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + + Returns + ------- + out : uint8 array or uint16 array (same as input image) + The local gradient. + + """ + + return _apply(generic8_cy.gradient, generic16_cy.gradient, image, selem, + out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) + + +def maximum(image, selem, out=None, mask=None, shift_x=False, shift_y=False): + """Return greyscale local maximum of an image. + + + Parameters + ---------- + image : ndarray + Image array (uint8 array or uint16). If image is uint16, the algorithm + uses max. 12bit histogram, an exception will be raised if image has a + value > 4095. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + If None, a new array will be allocated. + mask : ndarray (uint8) + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + + Returns + ------- + out : uint8 array or uint16 array (same as input image) + The local maximum. + + See also + -------- + skimage.morphology.dilation + + Note + ---- + * input image can be 8-bit or 16-bit with a value < 4096 (i.e. 12 bit) + * the lower algorithm complexity makes the rank.maximum() more efficient for + larger images and structuring elements + + """ + + return _apply(generic8_cy.maximum, generic16_cy.maximum, image, selem, + out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) + + +def mean(image, selem, out=None, mask=None, shift_x=False, shift_y=False): + """Return greyscale local mean of an image. + + Parameters + ---------- + image : ndarray + Image array (uint8 array or uint16). If image is uint16, the algorithm + uses max. 12bit histogram, an exception will be raised if image has a + value > 4095. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + If None, a new array will be allocated. + mask : ndarray (uint8) + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + + Returns + ------- + out : uint8 array or uint16 array (same as input image) + The local mean. + + Examples + -------- + >>> from skimage import data + >>> from skimage.morphology import disk + >>> from skimage.filter.rank import mean + >>> # Load test image + >>> ima = data.camera() + >>> # Local mean + >>> avg = mean(ima, disk(20)) + + """ + + return _apply(generic8_cy.mean, generic16_cy.mean, image, selem, out=out, + mask=mask, shift_x=shift_x, shift_y=shift_y) + + +def meansubtraction(image, selem, out=None, mask=None, shift_x=False, + shift_y=False): + """Return image subtracted from its local mean. + + Parameters + ---------- + image : ndarray + Image array (uint8 array or uint16). If image is uint16, the algorithm + uses max. 12bit histogram, an exception will be raised if image has a + value > 4095. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + If None, a new array will be allocated. + mask : ndarray (uint8) + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + + Returns + ------- + out : uint8 array or uint16 array (same as input image) + The result of the local meansubtraction. + + """ + + return _apply(generic8_cy.meansubtraction, generic16_cy.meansubtraction, + image, selem, out=out, mask=mask, shift_x=shift_x, + shift_y=shift_y) + + +def median(image, selem, out=None, mask=None, shift_x=False, shift_y=False): + """Return greyscale local median of an image. + + Parameters + ---------- + image : ndarray + Image array (uint8 array or uint16). If image is uint16, the algorithm + uses max. 12bit histogram, an exception will be raised if image has a + value > 4095. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + If None, a new array will be allocated. + mask : ndarray (uint8) + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + + Returns + ------- + out : uint8 array or uint16 array (same as input image) + The local median. + + Examples + -------- + >>> from skimage import data + >>> from skimage.morphology import disk + >>> from skimage.filter.rank import median + >>> # Load test image + >>> ima = data.camera() + >>> # Local mean + >>> avg = median(ima, disk(20)) + + """ + + return _apply(generic8_cy.median, generic16_cy.median, image, selem, + out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) + + +def minimum(image, selem, out=None, mask=None, shift_x=False, shift_y=False): + """Return greyscale local minimum of an image. + + Parameters + ---------- + image : ndarray + Image array (uint8 array or uint16). If image is uint16, the algorithm + uses max. 12bit histogram, an exception will be raised if image has a + value > 4095. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + If None, a new array will be allocated. + mask : ndarray (uint8) + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + + Returns + ------- + out : uint8 array or uint16 array (same as input image) + The local minimum. + + See also + -------- + skimage.morphology.erosion + + Note + ---- + * input image can be 8-bit or 16-bit with a value < 4096 (i.e. 12 bit) + * the lower algorithm complexity makes the rank.minimum() more efficient + for larger images and structuring elements + + """ + + return _apply(generic8_cy.minimum, generic16_cy.minimum, image, selem, + out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) + + +def modal(image, selem, out=None, mask=None, shift_x=False, shift_y=False): + """Return greyscale local mode of an image. + + Parameters + ---------- + image : ndarray + Image array (uint8 array or uint16). If image is uint16, the algorithm + uses max. 12bit histogram, an exception will be raised if image has a + value > 4095. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + If None, a new array will be allocated. + mask : ndarray (uint8) + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + + Returns + ------- + out : uint8 array or uint16 array (same as input image) + The local modal. + + """ + + return _apply(generic8_cy.modal, generic16_cy.modal, image, selem, + out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) + + +def morph_contr_enh(image, selem, out=None, mask=None, shift_x=False, + shift_y=False): + """Enhance an image replacing each pixel by the local maximum if pixel + greylevel is closest to maximimum than local minimum OR local minimum + otherwise. + + Parameters + ---------- + image : ndarray + Image array (uint8 array or uint16). If image is uint16, the algorithm + uses max. 12bit histogram, an exception will be raised if image has a + value > 4095. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + If None, a new array will be allocated. + mask : ndarray (uint8) + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + + Returns + ------- + out : uint8 array or uint16 array (same as input image) + The result of the local morph_contr_enh. + + Examples + -------- + >>> from skimage import data + >>> from skimage.morphology import disk + >>> from skimage.filter.rank import morph_contr_enh + >>> # Load test image + >>> ima = data.camera() + >>> # Local mean + >>> avg = morph_contr_enh(ima, disk(20)) + + """ + + return _apply(generic8_cy.morph_contr_enh, generic16_cy.morph_contr_enh, + image, selem, out=out, mask=mask, shift_x=shift_x, + shift_y=shift_y) + + +def pop(image, selem, out=None, mask=None, shift_x=False, shift_y=False): + """Return the number (population) of pixels actually inside the + neighborhood. + + Parameters + ---------- + image : ndarray + Image array (uint8 array or uint16). If image is uint16, the algorithm + uses max. 12bit histogram, an exception will be raised if image has a + value > 4095. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + If None, a new array will be allocated. + mask : ndarray (uint8) + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + + Returns + ------- + out : uint8 array or uint16 array (same as input image) + The number of pixels belonging to the neighborhood. + + Examples + -------- + >>> # Local mean + >>> from skimage.morphology import square + >>> import skimage.filter.rank as rank + >>> ima = 255 * np.array([[0, 0, 0, 0, 0], + ... [0, 1, 1, 1, 0], + ... [0, 1, 1, 1, 0], + ... [0, 1, 1, 1, 0], + ... [0, 0, 0, 0, 0]], dtype=np.uint8) + >>> rank.pop(ima, square(3)) + array([[4, 6, 6, 6, 4], + [6, 9, 9, 9, 6], + [6, 9, 9, 9, 6], + [6, 9, 9, 9, 6], + [4, 6, 6, 6, 4]], dtype=uint8) + + """ + + return _apply(generic8_cy.pop, generic16_cy.pop, image, selem, out=out, + mask=mask, shift_x=shift_x, shift_y=shift_y) + + +def threshold(image, selem, out=None, mask=None, shift_x=False, shift_y=False): + """Return greyscale local threshold of an image. + + Parameters + ---------- + image : ndarray + Image array (uint8 array or uint16). If image is uint16, the algorithm + uses max. 12bit histogram, an exception will be raised if image has a + value > 4095. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + If None, a new array will be allocated. + mask : ndarray (uint8) + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + + Returns + ------- + out : uint8 array or uint16 array (same as input image) + The result of the local threshold. + + Examples + -------- + >>> # Local threshold + >>> from skimage.morphology import square + >>> from skimage.filter.rank import threshold + >>> ima = 255 * np.array([[0, 0, 0, 0, 0], + ... [0, 1, 1, 1, 0], + ... [0, 1, 1, 1, 0], + ... [0, 1, 1, 1, 0], + ... [0, 0, 0, 0, 0]], dtype=np.uint8) + >>> threshold(ima, square(3)) + array([[0, 0, 0, 0, 0], + [0, 1, 1, 1, 0], + [0, 1, 0, 1, 0], + [0, 1, 1, 1, 0], + [0, 0, 0, 0, 0]], dtype=uint8) + + """ + + return _apply(generic8_cy.threshold, generic16_cy.threshold, image, selem, + out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) + + +def tophat(image, selem, out=None, mask=None, shift_x=False, shift_y=False): + """Return greyscale local tophat of an image. + + Parameters + ---------- + image : ndarray + Image array (uint8 array or uint16). If image is uint16, the algorithm + uses max. 12bit histogram, an exception will be raised if image has a + value > 4095. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + If None, a new array will be allocated. + mask : ndarray (uint8) + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + + Returns + ------- + out : uint8 array or uint16 array (same as input image) + The image tophat. + + """ + + return _apply(generic8_cy.tophat, generic16_cy.tophat, image, selem, + out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) + + +def noise_filter(image, selem, out=None, mask=None, shift_x=False, + shift_y=False): + """Returns the noise feature as described in [Hashimoto12]_ + + Parameters + ---------- + image : ndarray + Image array (uint8 array or uint16). If image is uint16, the algorithm + uses max. 12bit histogram, an exception will be raised if image has a + value > 4095. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + If None, a new array will be allocated. + mask : ndarray (uint8) + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + + References + ---------- + .. [Hashimoto12] N. Hashimoto et al. Referenceless image quality evaluation + for whole slide imaging. J Pathol Inform 2012;3:9. + + Returns + ------- + out : uint8 array or uint16 array (same as input image) + The image noise. + + """ + + # ensure that the central pixel in the structuring element is empty + centre_r = int(selem.shape[0] / 2) + shift_y + centre_c = int(selem.shape[1] / 2) + shift_x + # make a local copy + selem_cpy = selem.copy() + selem_cpy[centre_r, centre_c] = 0 + + return _apply(generic8_cy.noise_filter, None, image, selem_cpy, out=out, + mask=mask, shift_x=shift_x, shift_y=shift_y) + + +def entropy(image, selem, out=None, mask=None, shift_x=False, shift_y=False): + """Returns the entropy [1]_ computed locally. Entropy is computed + using base 2 logarithm i.e. the filter returns the minimum number of + bits needed to encode local greylevel distribution. + + Parameters + ---------- + image : ndarray + Image array (uint8 array or uint16). If image is uint16, the algorithm + uses max. 12bit histogram, an exception will be raised if image has a + value > 4095. + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + If None, a new array will be allocated. + mask : ndarray (uint8) + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + + Returns + ------- + out : uint8 array or uint16 array (same as input image) + entropy x10 (uint8 images) and entropy x1000 (uint16 images) + + References + ---------- + .. [1] http://en.wikipedia.org/wiki/Entropy_(information_theory) + + Examples + -------- + >>> # Local entropy + >>> from skimage import data + >>> from skimage.filter.rank import entropy + >>> from skimage.morphology import disk + >>> # defining a 8- and a 16-bit test images + >>> a8 = data.camera() + >>> a16 = data.camera().astype(np.uint16) * 4 + >>> # pixel values contain 10x the local entropy + >>> ent8 = entropy(a8, disk(5)) + >>> # pixel values contain 1000x the local entropy + >>> ent16 = entropy(a16, disk(5)) + + """ + + return _apply(generic8_cy.entropy, generic16_cy.entropy, image, selem, + out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) + + +def otsu(image, selem, out=None, mask=None, shift_x=False, shift_y=False): + """Returns the Otsu's threshold value for each pixel. + + Parameters + ---------- + image : ndarray + Image array (uint8 array). + selem : ndarray + The neighborhood expressed as a 2-D array of 1's and 0's. + out : ndarray + If None, a new array will be allocated. + mask : ndarray (uint8) + Mask array that defines (>0) area of the image included in the local + neighborhood. If None, the complete image is used (default). + shift_x, shift_y : int + Offset added to the structuring element center point. Shift is bounded + to the structuring element sizes (center must be inside the given + structuring element). + + Returns + ------- + out : uint8 array + Otsu's threshold values + + References + ---------- + .. [otsu] http://en.wikipedia.org/wiki/Otsu's_method + + Notes + ----- + * input image are 8-bit only + + Examples + -------- + >>> # Local entropy + >>> from skimage import data + >>> from skimage.filter.rank import otsu + >>> from skimage.morphology import disk + >>> # defining a 8-bit test images + >>> a8 = data.camera() + >>> loc_otsu = otsu(a8, disk(5)) + >>> thresh_image = a8 >= loc_otsu + + """ + + return _apply(generic8_cy.otsu, None, image, selem, out=out, + mask=mask, shift_x=shift_x, shift_y=shift_y) diff --git a/skimage/filter/rank/_crank16.pyx b/skimage/filter/rank/generic16_cy.pyx similarity index 99% rename from skimage/filter/rank/_crank16.pyx rename to skimage/filter/rank/generic16_cy.pyx index e704afe0..eace0afa 100644 --- a/skimage/filter/rank/_crank16.pyx +++ b/skimage/filter/rank/generic16_cy.pyx @@ -5,7 +5,7 @@ cimport numpy as cnp from libc.math cimport log -from skimage.filter.rank._core16 cimport _core16 +from .core16_cy cimport _core16 # ----------------------------------------------------------------- diff --git a/skimage/filter/rank/_crank8.pyx b/skimage/filter/rank/generic8_cy.pyx similarity index 99% rename from skimage/filter/rank/_crank8.pyx rename to skimage/filter/rank/generic8_cy.pyx index da511790..1cccc21f 100644 --- a/skimage/filter/rank/_crank8.pyx +++ b/skimage/filter/rank/generic8_cy.pyx @@ -5,7 +5,7 @@ cimport numpy as cnp from libc.math cimport log -from skimage.filter.rank._core8 cimport _core8 +from .core8_cy cimport _core8 # ----------------------------------------------------------------- diff --git a/skimage/filter/rank/percentile_rank.pyx b/skimage/filter/rank/percentile.py similarity index 94% rename from skimage/filter/rank/percentile_rank.pyx rename to skimage/filter/rank/percentile.py index 704f53c2..25758115 100644 --- a/skimage/filter/rank/percentile_rank.pyx +++ b/skimage/filter/rank/percentile.py @@ -24,8 +24,8 @@ References import numpy as np from skimage import img_as_ubyte -from skimage.filter.rank.generic import find_bitdepth -from skimage.filter.rank import _crank16_percentiles, _crank8_percentiles +from . import percentile8_cy, percentile16_cy +from .generic import find_bitdepth __all__ = ['percentile_autolevel', 'percentile_gradient', @@ -106,7 +106,7 @@ def percentile_autolevel(image, selem, out=None, mask=None, shift_x=False, """ return _apply( - _crank8_percentiles.autolevel, _crank16_percentiles.autolevel, + percentile8_cy.autolevel, percentile16_cy.autolevel, image, selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y, p0=p0, p1=p1) @@ -146,7 +146,7 @@ def percentile_gradient(image, selem, out=None, mask=None, shift_x=False, """ - return _apply(_crank8_percentiles.gradient, _crank16_percentiles.gradient, + return _apply(percentile8_cy.gradient, percentile16_cy.gradient, image, selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y, p0=p0, p1=p1) @@ -186,7 +186,7 @@ def percentile_mean(image, selem, out=None, mask=None, shift_x=False, """ - return _apply(_crank8_percentiles.mean, _crank16_percentiles.mean, + return _apply(percentile8_cy.mean, percentile16_cy.mean, image, selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y, p0=p0, p1=p1) @@ -226,8 +226,8 @@ def percentile_mean_subtraction(image, selem, out=None, mask=None, """ - return _apply(_crank8_percentiles.mean_subtraction, - _crank16_percentiles.mean_subtraction, + return _apply(percentile8_cy.mean_subtraction, + percentile16_cy.mean_subtraction, image, selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y, p0=p0, p1=p1) @@ -268,8 +268,8 @@ def percentile_morph_contr_enh( """ - return _apply(_crank8_percentiles.morph_contr_enh, - _crank16_percentiles.morph_contr_enh, + return _apply(percentile8_cy.morph_contr_enh, + percentile16_cy.morph_contr_enh, image, selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y, p0=p0, p1=p1) @@ -308,8 +308,8 @@ def percentile(image, selem, out=None, mask=None, shift_x=False, shift_y=False, """ - return _apply(_crank8_percentiles.percentile, - _crank16_percentiles.percentile, + return _apply(percentile8_cy.percentile, + percentile16_cy.percentile, image, selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y, p0=p0, p1=0.) @@ -349,7 +349,7 @@ def percentile_pop(image, selem, out=None, mask=None, shift_x=False, """ - return _apply(_crank8_percentiles.pop, _crank16_percentiles.pop, + return _apply(percentile8_cy.pop, percentile16_cy.pop, image, selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y, p0=p0, p1=p1) @@ -391,6 +391,6 @@ def percentile_threshold(image, selem, out=None, mask=None, shift_x=False, """ return _apply( - _crank8_percentiles.threshold, _crank16_percentiles.threshold, + percentile8_cy.threshold, percentile16_cy.threshold, image, selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y, p0=p0, p1=0.) diff --git a/skimage/filter/rank/_crank16_percentiles.pyx b/skimage/filter/rank/percentile16_cy.pyx similarity index 99% rename from skimage/filter/rank/_crank16_percentiles.pyx rename to skimage/filter/rank/percentile16_cy.pyx index f4a4c9b2..368bec0e 100644 --- a/skimage/filter/rank/_crank16_percentiles.pyx +++ b/skimage/filter/rank/percentile16_cy.pyx @@ -4,7 +4,7 @@ #cython: wraparound=False cimport numpy as cnp -from skimage.filter.rank._core16 cimport _core16, int_min, int_max +from .core16_cy cimport _core16, int_min, int_max # ----------------------------------------------------------------- diff --git a/skimage/filter/rank/_crank8_percentiles.pyx b/skimage/filter/rank/percentile8_cy.pyx similarity index 99% rename from skimage/filter/rank/_crank8_percentiles.pyx rename to skimage/filter/rank/percentile8_cy.pyx index 8e5cee9c..7f514f89 100644 --- a/skimage/filter/rank/_crank8_percentiles.pyx +++ b/skimage/filter/rank/percentile8_cy.pyx @@ -4,7 +4,7 @@ #cython: wraparound=False cimport numpy as cnp -from skimage.filter.rank._core8 cimport _core8, uint8_max, uint8_min +from .core8_cy cimport _core8, uint8_max, uint8_min # ----------------------------------------------------------------- diff --git a/skimage/filter/setup.py b/skimage/filter/setup.py index c70730f0..35626114 100644 --- a/skimage/filter/setup.py +++ b/skimage/filter/setup.py @@ -14,46 +14,39 @@ def configuration(parent_package='', top_path=None): cython(['_ctmf.pyx'], working_path=base_path) cython(['_denoise_cy.pyx'], working_path=base_path) - cython(['rank/_core8.pyx'], working_path=base_path) - cython(['rank/_core16.pyx'], working_path=base_path) - cython(['rank/_crank8.pyx'], working_path=base_path) - cython(['rank/_crank8_percentiles.pyx'], working_path=base_path) - cython(['rank/_crank16.pyx'], working_path=base_path) - cython(['rank/_crank16_percentiles.pyx'], working_path=base_path) - cython(['rank/_crank16_bilateral.pyx'], working_path=base_path) - cython(['rank/percentile_rank.pyx'], working_path=base_path) - cython(['rank/bilateral_rank.pyx'], working_path=base_path) + cython(['rank/core8_cy.pyx'], working_path=base_path) + cython(['rank/core16_cy.pyx'], working_path=base_path) + cython(['rank/generic8_cy.pyx'], working_path=base_path) + cython(['rank/percentile8_cy.pyx'], working_path=base_path) + cython(['rank/generic16_cy.pyx'], working_path=base_path) + cython(['rank/percentile16_cy.pyx'], working_path=base_path) + cython(['rank/bilateral16_cy.pyx'], working_path=base_path) config.add_extension('_ctmf', sources=['_ctmf.c'], include_dirs=[get_numpy_include_dirs()]) config.add_extension('_denoise_cy', sources=['_denoise_cy.c'], include_dirs=[get_numpy_include_dirs(), '../_shared']) - config.add_extension('rank._core8', sources=['rank/_core8.c'], + config.add_extension('rank.core8_cy', sources=['rank/core8_cy.c'], include_dirs=[get_numpy_include_dirs()]) - config.add_extension('rank._core16', sources=['rank/_core16.c'], + config.add_extension('rank.core16_cy', sources=['rank/core16_cy.c'], include_dirs=[get_numpy_include_dirs()]) - config.add_extension('rank._crank8', sources=['rank/_crank8.c'], + config.add_extension('rank.generic8_cy', sources=['rank/generic8_cy.c'], include_dirs=[get_numpy_include_dirs()]) config.add_extension( - 'rank._crank8_percentiles', sources=['rank/_crank8_percentiles.c'], + 'rank.percentile8_cy', sources=['rank/percentile8_cy.c'], include_dirs=[get_numpy_include_dirs()]) - config.add_extension('rank._crank16', sources=['rank/_crank16.c'], + config.add_extension('rank.generic16_cy', sources=['rank/generic16_cy.c'], include_dirs=[get_numpy_include_dirs()]) config.add_extension( - 'rank._crank16_percentiles', sources=['rank/_crank16_percentiles.c'], + 'rank.percentile16_cy', sources=['rank/percentile16_cy.c'], include_dirs=[get_numpy_include_dirs()]) config.add_extension( - 'rank._crank16_bilateral', sources=['rank/_crank16_bilateral.c'], - include_dirs=[get_numpy_include_dirs()]) - config.add_extension( - 'rank.percentile_rank', sources=['rank/percentile_rank.c'], - include_dirs=[get_numpy_include_dirs()]) - config.add_extension( - 'rank.bilateral_rank', sources=['rank/bilateral_rank.c'], + 'rank.bilateral16_cy', sources=['rank/bilateral16_cy.c'], include_dirs=[get_numpy_include_dirs()]) return config + if __name__ == '__main__': from numpy.distutils.core import setup setup(maintainer='scikit-image Developers', From 2641df3497bc65d5d98c3365e8f3862b24499648 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 30 Jun 2013 10:41:04 +0200 Subject: [PATCH 250/736] Use typed memoryviews in rank filter package --- skimage/filter/rank/core16_cy.pxd | 12 +- skimage/filter/rank/core16_cy.pyx | 74 ++++++------ skimage/filter/rank/core8_cy.pxd | 10 +- skimage/filter/rank/core8_cy.pyx | 67 +++++------ skimage/filter/rank/generic16_cy.pyx | 126 ++++++++++----------- skimage/filter/rank/generic8_cy.pyx | 143 ++++++++++++------------ skimage/filter/rank/percentile16_cy.pyx | 72 ++++++------ skimage/filter/rank/percentile8_cy.pyx | 69 ++++++------ 8 files changed, 275 insertions(+), 298 deletions(-) diff --git a/skimage/filter/rank/core16_cy.pxd b/skimage/filter/rank/core16_cy.pxd index 5586aea1..f13f0fd1 100644 --- a/skimage/filter/rank/core16_cy.pxd +++ b/skimage/filter/rank/core16_cy.pxd @@ -4,17 +4,17 @@ cimport numpy as cnp ctypedef cnp.uint16_t dtype_t -cdef int int_max(int a, int b) -cdef int int_min(int a, int b) +cdef dtype_t uint16_max(dtype_t a, dtype_t b) +cdef dtype_t uint16_min(dtype_t a, dtype_t b) # 16-bit core kernel receives extra information about data bitdepth cdef void _core16(dtype_t kernel(Py_ssize_t *, float, dtype_t, Py_ssize_t, Py_ssize_t, Py_ssize_t, float, float, Py_ssize_t, Py_ssize_t), - cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[cnp.uint8_t, ndim=2] selem, - cnp.ndarray[cnp.uint8_t, ndim=2] mask, - cnp.ndarray[dtype_t, ndim=2] out, + dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t[:, ::1] out, char shift_x, char shift_y, Py_ssize_t bitdepth, float p0, float p1, Py_ssize_t s0, Py_ssize_t s1) except * diff --git a/skimage/filter/rank/core16_cy.pyx b/skimage/filter/rank/core16_cy.pyx index 63bcdff1..25592d39 100644 --- a/skimage/filter/rank/core16_cy.pyx +++ b/skimage/filter/rank/core16_cy.pyx @@ -10,33 +10,33 @@ from libc.stdlib cimport malloc, free from .core8_cy cimport is_in_mask -cdef inline int int_max(int a, int b): +cdef inline dtype_t uint16_max(dtype_t a, dtype_t b): return a if a >= b else b -cdef inline int int_min(int a, int b): +cdef inline dtype_t uint16_min(dtype_t a, dtype_t b): return a if a <= b else b -cdef inline void histogram_increment(Py_ssize_t * histo, float * pop, +cdef inline void histogram_increment(Py_ssize_t* histo, float* pop, dtype_t value): histo[value] += 1 pop[0] += 1 -cdef inline void histogram_decrement(Py_ssize_t * histo, float * pop, +cdef inline void histogram_decrement(Py_ssize_t* histo, float* pop, dtype_t value): histo[value] -= 1 pop[0] -= 1 -cdef void _core16(dtype_t kernel(Py_ssize_t *, float, dtype_t, +cdef void _core16(dtype_t kernel(Py_ssize_t*, float, dtype_t, Py_ssize_t, Py_ssize_t, Py_ssize_t, float, float, Py_ssize_t, Py_ssize_t), - cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[cnp.uint8_t, ndim=2] selem, - cnp.ndarray[cnp.uint8_t, ndim=2] mask, - cnp.ndarray[dtype_t, ndim=2] out, + dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t[:, ::1] out, char shift_x, char shift_y, Py_ssize_t bitdepth, float p0, float p1, Py_ssize_t s0, Py_ssize_t s1) except *: """Compute histogram for each pixel neighborhood, apply kernel function and @@ -65,12 +65,8 @@ cdef void _core16(dtype_t kernel(Py_ssize_t *, float, dtype_t, cdef Py_ssize_t maxbin = maxbin_list[bitdepth] cdef Py_ssize_t midbin = midbin_list[bitdepth] - assert (image < maxbin).all() - # define pointers to the data - cdef dtype_t * out_data = out.data - cdef dtype_t * image_data = image.data - cdef cnp.uint8_t * mask_data = mask.data + cdef char* mask_data = &mask[0, 0] # define local variable types cdef Py_ssize_t r, c, rr, cc, s, value, local_max, i, even_row @@ -84,19 +80,19 @@ cdef void _core16(dtype_t kernel(Py_ssize_t *, float, dtype_t, cdef Py_ssize_t num_se_n, num_se_s, num_se_e, num_se_w # the current local histogram distribution - cdef Py_ssize_t * histo = malloc(maxbin * sizeof(Py_ssize_t)) + cdef Py_ssize_t* histo = malloc(maxbin * sizeof(Py_ssize_t)) # these lists contain the relative pixel row and column for each of the 4 # attack borders east, west, north and south e.g. se_e_r lists the rows of # the east structuring element border - cdef Py_ssize_t * se_e_r = malloc(max_se * sizeof(Py_ssize_t)) - cdef Py_ssize_t * se_e_c = malloc(max_se * sizeof(Py_ssize_t)) - cdef Py_ssize_t * se_w_r = malloc(max_se * sizeof(Py_ssize_t)) - cdef Py_ssize_t * se_w_c = malloc(max_se * sizeof(Py_ssize_t)) - cdef Py_ssize_t * se_n_r = malloc(max_se * sizeof(Py_ssize_t)) - cdef Py_ssize_t * se_n_c = malloc(max_se * sizeof(Py_ssize_t)) - cdef Py_ssize_t * se_s_r = malloc(max_se * sizeof(Py_ssize_t)) - cdef Py_ssize_t * se_s_c = malloc(max_se * sizeof(Py_ssize_t)) + cdef Py_ssize_t* se_e_r = malloc(max_se * sizeof(Py_ssize_t)) + cdef Py_ssize_t* se_e_c = malloc(max_se * sizeof(Py_ssize_t)) + cdef Py_ssize_t* se_w_r = malloc(max_se * sizeof(Py_ssize_t)) + cdef Py_ssize_t* se_w_c = malloc(max_se * sizeof(Py_ssize_t)) + cdef Py_ssize_t* se_n_r = malloc(max_se * sizeof(Py_ssize_t)) + cdef Py_ssize_t* se_n_c = malloc(max_se * sizeof(Py_ssize_t)) + cdef Py_ssize_t* se_s_r = malloc(max_se * sizeof(Py_ssize_t)) + cdef Py_ssize_t* se_s_c = malloc(max_se * sizeof(Py_ssize_t)) # build attack and release borders # by using difference along axis @@ -145,12 +141,12 @@ cdef void _core16(dtype_t kernel(Py_ssize_t *, float, dtype_t, cc = c - centre_c if selem[r, c]: if is_in_mask(rows, cols, rr, cc, mask_data): - histogram_increment(histo, &pop, image_data[rr * cols + cc]) + histogram_increment(histo, &pop, image[rr, cc]) r = 0 c = 0 # kernel ------------------------------------------- - out_data[r * cols + c] = kernel(histo, pop, image_data[r * cols + c], + out[r, c] = kernel(histo, pop, image[r, c], bitdepth, maxbin, midbin, p0, p1, s0, s1) # kernel ------------------------------------------- @@ -163,17 +159,17 @@ cdef void _core16(dtype_t kernel(Py_ssize_t *, float, dtype_t, rr = r + se_e_r[s] cc = c + se_e_c[s] if is_in_mask(rows, cols, rr, cc, mask_data): - histogram_increment(histo, &pop, image_data[rr * cols + cc]) + histogram_increment(histo, &pop, image[rr, cc]) for s in range(num_se_w): rr = r + se_w_r[s] cc = c + se_w_c[s] - 1 if is_in_mask(rows, cols, rr, cc, mask_data): - histogram_decrement(histo, &pop, image_data[rr * cols + cc]) + histogram_decrement(histo, &pop, image[rr, cc]) # kernel ------------------------------------------- - out_data[r * cols + c] = kernel( - histo, pop, image_data[r * cols + c], + out[r, c] = kernel( + histo, pop, image[r, c], bitdepth, maxbin, midbin, p0, p1, s0, s1) # kernel ------------------------------------------- @@ -186,16 +182,16 @@ cdef void _core16(dtype_t kernel(Py_ssize_t *, float, dtype_t, rr = r + se_s_r[s] cc = c + se_s_c[s] if is_in_mask(rows, cols, rr, cc, mask_data): - histogram_increment(histo, &pop, image_data[rr * cols + cc]) + histogram_increment(histo, &pop, image[rr, cc]) for s in range(num_se_n): rr = r + se_n_r[s] - 1 cc = c + se_n_c[s] if is_in_mask(rows, cols, rr, cc, mask_data): - histogram_decrement(histo, &pop, image_data[rr * cols + cc]) + histogram_decrement(histo, &pop, image[rr, cc]) # kernel ------------------------------------------- - out_data[r * cols + c] = kernel(histo, pop, image_data[r * cols + c], + out[r, c] = kernel(histo, pop, image[r, c], bitdepth, maxbin, midbin, p0, p1, s0, s1) # kernel ------------------------------------------- @@ -205,17 +201,17 @@ cdef void _core16(dtype_t kernel(Py_ssize_t *, float, dtype_t, rr = r + se_w_r[s] cc = c + se_w_c[s] if is_in_mask(rows, cols, rr, cc, mask_data): - histogram_increment(histo, &pop, image_data[rr * cols + cc]) + histogram_increment(histo, &pop, image[rr, cc]) for s in range(num_se_e): rr = r + se_e_r[s] cc = c + se_e_c[s] + 1 if is_in_mask(rows, cols, rr, cc, mask_data): - histogram_decrement(histo, &pop, image_data[rr * cols + cc]) + histogram_decrement(histo, &pop, image[rr, cc]) # kernel ------------------------------------------- - out_data[r * cols + c] = kernel( - histo, pop, image_data[r * cols + c], + out[r, c] = kernel( + histo, pop, image[r, c], bitdepth, maxbin, midbin, p0, p1, s0, s1) # kernel ------------------------------------------- @@ -228,16 +224,16 @@ cdef void _core16(dtype_t kernel(Py_ssize_t *, float, dtype_t, rr = r + se_s_r[s] cc = c + se_s_c[s] if is_in_mask(rows, cols, rr, cc, mask_data): - histogram_increment(histo, &pop, image_data[rr * cols + cc]) + histogram_increment(histo, &pop, image[rr, cc]) for s in range(num_se_n): rr = r + se_n_r[s] - 1 cc = c + se_n_c[s] if is_in_mask(rows, cols, rr, cc, mask_data): - histogram_decrement(histo, &pop, image_data[rr * cols + cc]) + histogram_decrement(histo, &pop, image[rr, cc]) # kernel ------------------------------------------- - out_data[r * cols + c] = kernel(histo, pop, image_data[r * cols + c], + out[r, c] = kernel(histo, pop, image[r, c], bitdepth, maxbin, midbin, p0, p1, s0, s1) # kernel ------------------------------------------- diff --git a/skimage/filter/rank/core8_cy.pxd b/skimage/filter/rank/core8_cy.pxd index d3b6d8c2..8d7f9744 100644 --- a/skimage/filter/rank/core8_cy.pxd +++ b/skimage/filter/rank/core8_cy.pxd @@ -10,16 +10,16 @@ cdef dtype_t uint8_min(dtype_t a, dtype_t b) cdef dtype_t is_in_mask(Py_ssize_t rows, Py_ssize_t cols, Py_ssize_t r, Py_ssize_t c, - dtype_t * mask) + char* mask) # 8-bit core kernel receives extra information about data inferior and superior # percentiles cdef void _core8(dtype_t kernel(Py_ssize_t *, float, dtype_t, float, float, Py_ssize_t, Py_ssize_t), - cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[dtype_t, ndim=2] selem, - cnp.ndarray[dtype_t, ndim=2] mask, - cnp.ndarray[dtype_t, ndim=2] out, + dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t[:, ::1] out, char shift_x, char shift_y, float p0, float p1, Py_ssize_t s0, Py_ssize_t s1) except * diff --git a/skimage/filter/rank/core8_cy.pyx b/skimage/filter/rank/core8_cy.pyx index 79ae9bbf..b8d49a0f 100644 --- a/skimage/filter/rank/core8_cy.pyx +++ b/skimage/filter/rank/core8_cy.pyx @@ -31,7 +31,7 @@ cdef inline void histogram_decrement(Py_ssize_t * histo, float * pop, cdef inline dtype_t is_in_mask(Py_ssize_t rows, Py_ssize_t cols, Py_ssize_t r, Py_ssize_t c, - dtype_t * mask): + char* mask): """Check whether given coordinate is within image and mask is true.""" if r < 0 or r > rows - 1 or c < 0 or c > cols - 1: return 0 @@ -44,10 +44,10 @@ cdef inline dtype_t is_in_mask(Py_ssize_t rows, Py_ssize_t cols, cdef void _core8(dtype_t kernel(Py_ssize_t *, float, dtype_t, float, float, Py_ssize_t, Py_ssize_t), - cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[dtype_t, ndim=2] selem, - cnp.ndarray[dtype_t, ndim=2] mask, - cnp.ndarray[dtype_t, ndim=2] out, + dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t[:, ::1] out, char shift_x, char shift_y, float p0, float p1, Py_ssize_t s0, Py_ssize_t s1) except *: """Compute histogram for each pixel neighborhood, apply kernel function and @@ -68,11 +68,7 @@ cdef void _core8(dtype_t kernel(Py_ssize_t *, float, dtype_t, float, assert centre_r < srows assert centre_c < scols - # define pointers to the data - - cdef dtype_t * out_data = out.data - cdef dtype_t * image_data = image.data - cdef dtype_t * mask_data = mask.data + cdef char* mask_data = &mask[0, 0] # define local variable types cdef Py_ssize_t r, c, rr, cc, s, value, local_max, i, even_row @@ -87,19 +83,19 @@ cdef void _core8(dtype_t kernel(Py_ssize_t *, float, dtype_t, float, cdef Py_ssize_t num_se_n, num_se_s, num_se_e, num_se_w # the current local histogram distribution - cdef Py_ssize_t * histo = malloc(256 * sizeof(Py_ssize_t)) + cdef Py_ssize_t * histo = malloc(256 * sizeof(Py_ssize_t)) # these lists contain the relative pixel row and column for each of the 4 # attack borders east, west, north and south e.g. se_e_r lists the rows of # the east structuring element border - cdef Py_ssize_t * se_e_r = malloc(max_se * sizeof(Py_ssize_t)) - cdef Py_ssize_t * se_e_c = malloc(max_se * sizeof(Py_ssize_t)) - cdef Py_ssize_t * se_w_r = malloc(max_se * sizeof(Py_ssize_t)) - cdef Py_ssize_t * se_w_c = malloc(max_se * sizeof(Py_ssize_t)) - cdef Py_ssize_t * se_n_r = malloc(max_se * sizeof(Py_ssize_t)) - cdef Py_ssize_t * se_n_c = malloc(max_se * sizeof(Py_ssize_t)) - cdef Py_ssize_t * se_s_r = malloc(max_se * sizeof(Py_ssize_t)) - cdef Py_ssize_t * se_s_c = malloc(max_se * sizeof(Py_ssize_t)) + cdef Py_ssize_t * se_e_r = malloc(max_se * sizeof(Py_ssize_t)) + cdef Py_ssize_t * se_e_c = malloc(max_se * sizeof(Py_ssize_t)) + cdef Py_ssize_t * se_w_r = malloc(max_se * sizeof(Py_ssize_t)) + cdef Py_ssize_t * se_w_c = malloc(max_se * sizeof(Py_ssize_t)) + cdef Py_ssize_t * se_n_r = malloc(max_se * sizeof(Py_ssize_t)) + cdef Py_ssize_t * se_n_c = malloc(max_se * sizeof(Py_ssize_t)) + cdef Py_ssize_t * se_s_r = malloc(max_se * sizeof(Py_ssize_t)) + cdef Py_ssize_t * se_s_c = malloc(max_se * sizeof(Py_ssize_t)) # build attack and release borders # by using difference along axis @@ -149,12 +145,12 @@ cdef void _core8(dtype_t kernel(Py_ssize_t *, float, dtype_t, float, cc = c - centre_c if selem[r, c]: if is_in_mask(rows, cols, rr, cc, mask_data): - histogram_increment(histo, &pop, image_data[rr * cols + cc]) + histogram_increment(histo, &pop, image[rr, cc]) r = 0 c = 0 # kernel ------------------------------------------------------------------- - out_data[r * cols + c] = kernel(histo, pop, image_data[r * cols + c], + out[r, c] = kernel(histo, pop, image[r, c], p0, p1, s0, s1) # kernel ------------------------------------------------------------------- @@ -167,17 +163,17 @@ cdef void _core8(dtype_t kernel(Py_ssize_t *, float, dtype_t, float, rr = r + se_e_r[s] cc = c + se_e_c[s] if is_in_mask(rows, cols, rr, cc, mask_data): - histogram_increment(histo, &pop, image_data[rr * cols + cc]) + histogram_increment(histo, &pop, image[rr, cc]) for s in range(num_se_w): rr = r + se_w_r[s] cc = c + se_w_c[s] - 1 if is_in_mask(rows, cols, rr, cc, mask_data): - histogram_decrement(histo, &pop, image_data[rr * cols + cc]) + histogram_decrement(histo, &pop, image[rr, cc]) # kernel ----------------------------------------------------------- - out_data[r * cols + c] = \ - kernel(histo, pop, image_data[r * cols + c], p0, p1, s0, s1) + out[r, c] = \ + kernel(histo, pop, image[r, c], p0, p1, s0, s1) # kernel ----------------------------------------------------------- r += 1 # pass to the next row @@ -189,16 +185,16 @@ cdef void _core8(dtype_t kernel(Py_ssize_t *, float, dtype_t, float, rr = r + se_s_r[s] cc = c + se_s_c[s] if is_in_mask(rows, cols, rr, cc, mask_data): - histogram_increment(histo, &pop, image_data[rr * cols + cc]) + histogram_increment(histo, &pop, image[rr, cc]) for s in range(num_se_n): rr = r + se_n_r[s] - 1 cc = c + se_n_c[s] if is_in_mask(rows, cols, rr, cc, mask_data): - histogram_decrement(histo, &pop, image_data[rr * cols + cc]) + histogram_decrement(histo, &pop, image[rr, cc]) # kernel --------------------------------------------------------------- - out_data[r * cols + c] = kernel(histo, pop, image_data[r * cols + c], + out[r, c] = kernel(histo, pop, image[r, c], p0, p1, s0, s1) # kernel --------------------------------------------------------------- @@ -208,17 +204,17 @@ cdef void _core8(dtype_t kernel(Py_ssize_t *, float, dtype_t, float, rr = r + se_w_r[s] cc = c + se_w_c[s] if is_in_mask(rows, cols, rr, cc, mask_data): - histogram_increment(histo, &pop, image_data[rr * cols + cc]) + histogram_increment(histo, &pop, image[rr, cc]) for s in range(num_se_e): rr = r + se_e_r[s] cc = c + se_e_c[s] + 1 if is_in_mask(rows, cols, rr, cc, mask_data): - histogram_decrement(histo, &pop, image_data[rr * cols + cc]) + histogram_decrement(histo, &pop, image[rr, cc]) # kernel ----------------------------------------------------------- - out_data[r * cols + c] = kernel( - histo, pop, image_data[r * cols + c], p0, p1, s0, s1) + out[r, c] = kernel( + histo, pop, image[r, c], p0, p1, s0, s1) # kernel ----------------------------------------------------------- r += 1 # pass to the next row @@ -230,21 +226,20 @@ cdef void _core8(dtype_t kernel(Py_ssize_t *, float, dtype_t, float, rr = r + se_s_r[s] cc = c + se_s_c[s] if is_in_mask(rows, cols, rr, cc, mask_data): - histogram_increment(histo, &pop, image_data[rr * cols + cc]) + histogram_increment(histo, &pop, image[rr, cc]) for s in range(num_se_n): rr = r + se_n_r[s] - 1 cc = c + se_n_c[s] if is_in_mask(rows, cols, rr, cc, mask_data): - histogram_decrement(histo, &pop, image_data[rr * cols + cc]) + histogram_decrement(histo, &pop, image[rr, cc]) # kernel --------------------------------------------------------------- - out_data[r * cols + c] = kernel(histo, pop, image_data[r * cols + c], + out[r, c] = kernel(histo, pop, image[r, c], p0, p1, s0, s1) # kernel --------------------------------------------------------------- # release memory allocated by malloc - free(se_e_r) free(se_e_c) free(se_w_r) diff --git a/skimage/filter/rank/generic16_cy.pyx b/skimage/filter/rank/generic16_cy.pyx index eace0afa..7b2a8088 100644 --- a/skimage/filter/rank/generic16_cy.pyx +++ b/skimage/filter/rank/generic16_cy.pyx @@ -5,17 +5,13 @@ cimport numpy as cnp from libc.math cimport log -from .core16_cy cimport _core16 +from .core16_cy cimport dtype_t, _core16 # ----------------------------------------------------------------- # kernels uint16 take extra parameter for defining the bitdepth # ----------------------------------------------------------------- - -ctypedef cnp.uint16_t dtype_t - - cdef inline dtype_t kernel_autolevel(Py_ssize_t * histo, float pop, dtype_t g, Py_ssize_t bitdepth, Py_ssize_t maxbin, Py_ssize_t midbin, @@ -287,136 +283,136 @@ cdef inline dtype_t kernel_entropy(Py_ssize_t * histo, float pop, # ----------------------------------------------------------------- -def autolevel(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[cnp.uint8_t, ndim=2] selem, - cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, +def autolevel(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask=None, + dtype_t[:, ::1] out=None, char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): _core16(kernel_autolevel, image, selem, mask, out, shift_x, shift_y, bitdepth, 0, 0, 0, 0) -def bottomhat(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[cnp.uint8_t, ndim=2] selem, - cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, +def bottomhat(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask=None, + dtype_t[:, ::1] out=None, char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): _core16(kernel_bottomhat, image, selem, mask, out, shift_x, shift_y, bitdepth, 0, 0, 0, 0) -def equalize(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[cnp.uint8_t, ndim=2] selem, - cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, +def equalize(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask=None, + dtype_t[:, ::1] out=None, char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): _core16(kernel_equalize, image, selem, mask, out, shift_x, shift_y, bitdepth, 0, 0, 0, 0) -def gradient(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[cnp.uint8_t, ndim=2] selem, - cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, +def gradient(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask=None, + dtype_t[:, ::1] out=None, char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): _core16(kernel_gradient, image, selem, mask, out, shift_x, shift_y, bitdepth, 0, 0, 0, 0) -def maximum(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[cnp.uint8_t, ndim=2] selem, - cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, +def maximum(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask=None, + dtype_t[:, ::1] out=None, char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): _core16(kernel_maximum, image, selem, mask, out, shift_x, shift_y, bitdepth, 0, 0, 0, 0) -def mean(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[cnp.uint8_t, ndim=2] selem, - cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, +def mean(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask=None, + dtype_t[:, ::1] out=None, char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): _core16(kernel_mean, image, selem, mask, out, shift_x, shift_y, bitdepth, 0, 0, 0, 0) -def meansubtraction(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[cnp.uint8_t, ndim=2] selem, - cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, +def meansubtraction(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask=None, + dtype_t[:, ::1] out=None, char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): _core16(kernel_meansubtraction, image, selem, mask, out, shift_x, shift_y, bitdepth, 0, 0, 0, 0) -def median(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[cnp.uint8_t, ndim=2] selem, - cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, +def median(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask=None, + dtype_t[:, ::1] out=None, char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): _core16(kernel_median, image, selem, mask, out, shift_x, shift_y, bitdepth, 0, 0, 0, 0) -def minimum(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[cnp.uint8_t, ndim=2] selem, - cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, +def minimum(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask=None, + dtype_t[:, ::1] out=None, char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): _core16(kernel_minimum, image, selem, mask, out, shift_x, shift_y, bitdepth, 0, 0, 0, 0) -def morph_contr_enh(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[cnp.uint8_t, ndim=2] selem, - cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, +def morph_contr_enh(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask=None, + dtype_t[:, ::1] out=None, char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): _core16(kernel_morph_contr_enh, image, selem, mask, out, shift_x, shift_y, bitdepth, 0, 0, 0, 0) -def modal(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[cnp.uint8_t, ndim=2] selem, - cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, +def modal(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask=None, + dtype_t[:, ::1] out=None, char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): _core16(kernel_modal, image, selem, mask, out, shift_x, shift_y, bitdepth, 0, 0, 0, 0) -def pop(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[cnp.uint8_t, ndim=2] selem, - cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, +def pop(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask=None, + dtype_t[:, ::1] out=None, char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): _core16(kernel_pop, image, selem, mask, out, shift_x, shift_y, bitdepth, 0, 0, 0, 0) -def threshold(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[cnp.uint8_t, ndim=2] selem, - cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, +def threshold(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask=None, + dtype_t[:, ::1] out=None, char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): _core16(kernel_threshold, image, selem, mask, out, shift_x, shift_y, bitdepth, 0, 0, 0, 0) -def tophat(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[cnp.uint8_t, ndim=2] selem, - cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, +def tophat(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask=None, + dtype_t[:, ::1] out=None, char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): _core16(kernel_tophat, image, selem, mask, out, shift_x, shift_y, bitdepth, 0, 0, 0, 0) -def entropy(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[cnp.uint8_t, ndim=2] selem, - cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, +def entropy(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask=None, + dtype_t[:, ::1] out=None, char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): _core16(kernel_entropy, image, selem, mask, out, shift_x, shift_y, bitdepth, 0, 0, 0, 0) diff --git a/skimage/filter/rank/generic8_cy.pyx b/skimage/filter/rank/generic8_cy.pyx index 1cccc21f..62749041 100644 --- a/skimage/filter/rank/generic8_cy.pyx +++ b/skimage/filter/rank/generic8_cy.pyx @@ -5,7 +5,7 @@ cimport numpy as cnp from libc.math cimport log -from .core8_cy cimport _core8 +from .core8_cy cimport dtype_t, _core8 # ----------------------------------------------------------------- @@ -13,9 +13,6 @@ from .core8_cy cimport _core8 # ----------------------------------------------------------------- -ctypedef cnp.uint8_t dtype_t - - cdef inline dtype_t kernel_autolevel(Py_ssize_t * histo, float pop, dtype_t g, float p0, float p1, Py_ssize_t s0, Py_ssize_t s1): @@ -330,154 +327,154 @@ cdef inline dtype_t kernel_otsu(Py_ssize_t * histo, float pop, dtype_t g, # ----------------------------------------------------------------- -def autolevel(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[dtype_t, ndim=2] selem, - cnp.ndarray[dtype_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, +def autolevel(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask=None, + dtype_t[:, ::1] out=None, char shift_x=0, char shift_y=0): _core8(kernel_autolevel, image, selem, mask, out, shift_x, shift_y, 0, 0, 0, 0) -def bottomhat(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[dtype_t, ndim=2] selem, - cnp.ndarray[dtype_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, +def bottomhat(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask=None, + dtype_t[:, ::1] out=None, char shift_x=0, char shift_y=0): _core8(kernel_bottomhat, image, selem, mask, out, shift_x, shift_y, 0, 0, 0, 0) -def equalize(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[dtype_t, ndim=2] selem, - cnp.ndarray[dtype_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, +def equalize(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask=None, + dtype_t[:, ::1] out=None, char shift_x=0, char shift_y=0): _core8(kernel_equalize, image, selem, mask, out, shift_x, shift_y, 0, 0, 0, 0) -def gradient(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[dtype_t, ndim=2] selem, - cnp.ndarray[dtype_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, +def gradient(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask=None, + dtype_t[:, ::1] out=None, char shift_x=0, char shift_y=0): _core8(kernel_gradient, image, selem, mask, out, shift_x, shift_y, 0, 0, 0, 0) -def maximum(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[dtype_t, ndim=2] selem, - cnp.ndarray[dtype_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, +def maximum(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask=None, + dtype_t[:, ::1] out=None, char shift_x=0, char shift_y=0): _core8(kernel_maximum, image, selem, mask, out, shift_x, shift_y, 0, 0, 0, 0) -def mean(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[dtype_t, ndim=2] selem, - cnp.ndarray[dtype_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, +def mean(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask=None, + dtype_t[:, ::1] out=None, char shift_x=0, char shift_y=0): _core8(kernel_mean, image, selem, mask, out, shift_x, shift_y, 0, 0, 0, 0) -def meansubtraction(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[dtype_t, ndim=2] selem, - cnp.ndarray[dtype_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, - char shift_x=0, char shift_y=0): +def meansubtraction(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask=None, + dtype_t[:, ::1] out=None, + char shift_x=0, char shift_y=0): _core8(kernel_meansubtraction, image, selem, mask, out, shift_x, shift_y, 0, 0, 0, 0) -def median(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[dtype_t, ndim=2] selem, - cnp.ndarray[dtype_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, +def median(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask=None, + dtype_t[:, ::1] out=None, char shift_x=0, char shift_y=0): _core8(kernel_median, image, selem, mask, out, shift_x, shift_y, 0, 0, 0, 0) -def minimum(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[dtype_t, ndim=2] selem, - cnp.ndarray[dtype_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, +def minimum(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask=None, + dtype_t[:, ::1] out=None, char shift_x=0, char shift_y=0): _core8(kernel_minimum, image, selem, mask, out, shift_x, shift_y, 0, 0, 0, 0) -def morph_contr_enh(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[dtype_t, ndim=2] selem, - cnp.ndarray[dtype_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, +def morph_contr_enh(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask=None, + dtype_t[:, ::1] out=None, char shift_x=0, char shift_y=0): _core8(kernel_morph_contr_enh, image, selem, mask, out, shift_x, shift_y, 0, 0, 0, 0) -def modal(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[dtype_t, ndim=2] selem, - cnp.ndarray[dtype_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, +def modal(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask=None, + dtype_t[:, ::1] out=None, char shift_x=0, char shift_y=0): _core8(kernel_modal, image, selem, mask, out, shift_x, shift_y, 0, 0, 0, 0) -def pop(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[dtype_t, ndim=2] selem, - cnp.ndarray[dtype_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, +def pop(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask=None, + dtype_t[:, ::1] out=None, char shift_x=0, char shift_y=0): _core8(kernel_pop, image, selem, mask, out, shift_x, shift_y, 0, 0, 0, 0) -def threshold(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[dtype_t, ndim=2] selem, - cnp.ndarray[dtype_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, +def threshold(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask=None, + dtype_t[:, ::1] out=None, char shift_x=0, char shift_y=0): _core8(kernel_threshold, image, selem, mask, out, shift_x, shift_y, 0, 0, 0, 0) -def tophat(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[dtype_t, ndim=2] selem, - cnp.ndarray[dtype_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, +def tophat(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask=None, + dtype_t[:, ::1] out=None, char shift_x=0, char shift_y=0): _core8(kernel_tophat, image, selem, mask, out, shift_x, shift_y, 0, 0, 0, 0) -def noise_filter(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[dtype_t, ndim=2] selem, - cnp.ndarray[dtype_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, +def noise_filter(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask=None, + dtype_t[:, ::1] out=None, char shift_x=0, char shift_y=0): _core8(kernel_noise_filter, image, selem, mask, out, shift_x, shift_y, 0, 0, 0, 0) -def entropy(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[dtype_t, ndim=2] selem, - cnp.ndarray[dtype_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, +def entropy(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask=None, + dtype_t[:, ::1] out=None, char shift_x=0, char shift_y=0): _core8(kernel_entropy, image, selem, mask, out, shift_x, shift_y, 0, 0, 0, 0) -def otsu(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[dtype_t, ndim=2] selem, - cnp.ndarray[dtype_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, +def otsu(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask=None, + dtype_t[:, ::1] out=None, char shift_x=0, char shift_y=0): _core8(kernel_otsu, image, selem, mask, out, shift_x, shift_y, 0, 0, 0, 0) diff --git a/skimage/filter/rank/percentile16_cy.pyx b/skimage/filter/rank/percentile16_cy.pyx index 368bec0e..8680aae4 100644 --- a/skimage/filter/rank/percentile16_cy.pyx +++ b/skimage/filter/rank/percentile16_cy.pyx @@ -4,17 +4,13 @@ #cython: wraparound=False cimport numpy as cnp -from .core16_cy cimport _core16, int_min, int_max +from .core16_cy cimport dtype_t, _core16, uint16_min, uint16_max # ----------------------------------------------------------------- # kernels uint16 (SOFT version using percentiles) # ----------------------------------------------------------------- - -ctypedef cnp.uint16_t dtype_t - - cdef inline dtype_t kernel_autolevel(Py_ssize_t * histo, float pop, dtype_t g, Py_ssize_t bitdepth, Py_ssize_t maxbin, Py_ssize_t midbin, @@ -41,7 +37,7 @@ cdef inline dtype_t kernel_autolevel(Py_ssize_t * histo, float pop, delta = imax - imin if delta > 0: return (1.0 * (maxbin - 1) - * (int_min(int_max(imin, g), imax) + * (uint16_min(uint16_max(imin, g), imax) - imin) / delta) else: return (imax - imin) @@ -233,10 +229,10 @@ cdef inline dtype_t kernel_threshold(Py_ssize_t * histo, float pop, # ----------------------------------------------------------------- -def autolevel(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[cnp.uint8_t, ndim=2] selem, - cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, +def autolevel(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask=None, + dtype_t[:, ::1] out=None, char shift_x=0, char shift_y=0, int bitdepth=8, float p0=0., float p1=0.): """bottom hat @@ -245,10 +241,10 @@ def autolevel(cnp.ndarray[dtype_t, ndim=2] image, bitdepth, p0, p1, 0, 0) -def gradient(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[cnp.uint8_t, ndim=2] selem, - cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, +def gradient(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask=None, + dtype_t[:, ::1] out=None, char shift_x=0, char shift_y=0, int bitdepth=8, float p0=0., float p1=0.): """return p0,p1 percentile gradient @@ -257,10 +253,10 @@ def gradient(cnp.ndarray[dtype_t, ndim=2] image, bitdepth, p0, p1, 0, 0) -def mean(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[cnp.uint8_t, ndim=2] selem, - cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, +def mean(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask=None, + dtype_t[:, ::1] out=None, char shift_x=0, char shift_y=0, int bitdepth=8, float p0=0., float p1=0.): """return mean between [p0 and p1] percentiles @@ -269,10 +265,10 @@ def mean(cnp.ndarray[dtype_t, ndim=2] image, bitdepth, p0, p1, 0, 0) -def mean_subtraction(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[cnp.uint8_t, ndim=2] selem, - cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, +def mean_subtraction(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask=None, + dtype_t[:, ::1] out=None, char shift_x=0, char shift_y=0, int bitdepth=8, float p0=0., float p1=0.): """return original - mean between [p0 and p1] percentiles *.5 +127 @@ -282,10 +278,10 @@ def mean_subtraction(cnp.ndarray[dtype_t, ndim=2] image, bitdepth, p0, p1, 0, 0) -def morph_contr_enh(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[cnp.uint8_t, ndim=2] selem, - cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, +def morph_contr_enh(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask=None, + dtype_t[:, ::1] out=None, char shift_x=0, char shift_y=0, int bitdepth=8, float p0=0., float p1=0.): """reforce contrast using percentiles @@ -294,10 +290,10 @@ def morph_contr_enh(cnp.ndarray[dtype_t, ndim=2] image, bitdepth, p0, p1, 0, 0) -def percentile(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[cnp.uint8_t, ndim=2] selem, - cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, +def percentile(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask=None, + dtype_t[:, ::1] out=None, char shift_x=0, char shift_y=0, int bitdepth=8, float p0=0.): """return p0 percentile @@ -306,10 +302,10 @@ def percentile(cnp.ndarray[dtype_t, ndim=2] image, bitdepth, p0, .0, 0, 0) -def pop(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[cnp.uint8_t, ndim=2] selem, - cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, +def pop(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask=None, + dtype_t[:, ::1] out=None, char shift_x=0, char shift_y=0, int bitdepth=8, float p0=0., float p1=0.): """return nb of pixels between [p0 and p1] @@ -318,10 +314,10 @@ def pop(cnp.ndarray[dtype_t, ndim=2] image, bitdepth, p0, p1, 0, 0) -def threshold(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[cnp.uint8_t, ndim=2] selem, - cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, +def threshold(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask=None, + dtype_t[:, ::1] out=None, char shift_x=0, char shift_y=0, int bitdepth=8, float p0=0.): """return (maxbin-1) if g > percentile p0 diff --git a/skimage/filter/rank/percentile8_cy.pyx b/skimage/filter/rank/percentile8_cy.pyx index 7f514f89..6c37cf66 100644 --- a/skimage/filter/rank/percentile8_cy.pyx +++ b/skimage/filter/rank/percentile8_cy.pyx @@ -4,7 +4,7 @@ #cython: wraparound=False cimport numpy as cnp -from .core8_cy cimport _core8, uint8_max, uint8_min +from .core8_cy cimport dtype_t, _core8, uint8_max, uint8_min # ----------------------------------------------------------------- @@ -12,9 +12,6 @@ from .core8_cy cimport _core8, uint8_max, uint8_min # ----------------------------------------------------------------- -ctypedef cnp.uint8_t dtype_t - - cdef inline dtype_t kernel_autolevel(Py_ssize_t * histo, float pop, dtype_t g, float p0, float p1, Py_ssize_t s0, Py_ssize_t s1): @@ -206,10 +203,10 @@ cdef inline dtype_t kernel_threshold(Py_ssize_t * histo, float pop, # ----------------------------------------------------------------- -def autolevel(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[dtype_t, ndim=2] selem, - cnp.ndarray[dtype_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, +def autolevel(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask=None, + dtype_t[:, ::1] out=None, char shift_x=0, char shift_y=0, float p0=0., float p1=0.): """autolevel """ @@ -217,10 +214,10 @@ def autolevel(cnp.ndarray[dtype_t, ndim=2] image, 0, 0) -def gradient(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[dtype_t, ndim=2] selem, - cnp.ndarray[dtype_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, +def gradient(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask=None, + dtype_t[:, ::1] out=None, char shift_x=0, char shift_y=0, float p0=0., float p1=0.): """return p0,p1 percentile gradient """ @@ -228,10 +225,10 @@ def gradient(cnp.ndarray[dtype_t, ndim=2] image, 0, 0) -def mean(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[dtype_t, ndim=2] selem, - cnp.ndarray[dtype_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, +def mean(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask=None, + dtype_t[:, ::1] out=None, char shift_x=0, char shift_y=0, float p0=0., float p1=0.): """return mean between [p0 and p1] percentiles """ @@ -239,10 +236,10 @@ def mean(cnp.ndarray[dtype_t, ndim=2] image, 0, 0) -def mean_subtraction(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[dtype_t, ndim=2] selem, - cnp.ndarray[dtype_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, +def mean_subtraction(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask=None, + dtype_t[:, ::1] out=None, char shift_x=0, char shift_y=0, float p0=0., float p1=0.): """return original - mean between [p0 and p1] percentiles *.5 +127 """ @@ -250,10 +247,10 @@ def mean_subtraction(cnp.ndarray[dtype_t, ndim=2] image, p0, p1, 0, 0) -def morph_contr_enh(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[dtype_t, ndim=2] selem, - cnp.ndarray[dtype_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, +def morph_contr_enh(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask=None, + dtype_t[:, ::1] out=None, char shift_x=0, char shift_y=0, float p0=0., float p1=0.): """reforce contrast using percentiles """ @@ -261,10 +258,10 @@ def morph_contr_enh(cnp.ndarray[dtype_t, ndim=2] image, p0, p1, 0, 0) -def percentile(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[dtype_t, ndim=2] selem, - cnp.ndarray[dtype_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, +def percentile(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask=None, + dtype_t[:, ::1] out=None, char shift_x=0, char shift_y=0, float p0=0.): """return p0 percentile """ @@ -272,10 +269,10 @@ def percentile(cnp.ndarray[dtype_t, ndim=2] image, p0, 0., 0, 0) -def pop(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[dtype_t, ndim=2] selem, - cnp.ndarray[dtype_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, +def pop(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask=None, + dtype_t[:, ::1] out=None, char shift_x=0, char shift_y=0, float p0=0., float p1=0.): """return nb of pixels between [p0 and p1] """ @@ -283,10 +280,10 @@ def pop(cnp.ndarray[dtype_t, ndim=2] image, 0, 0) -def threshold(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[dtype_t, ndim=2] selem, - cnp.ndarray[dtype_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, +def threshold(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask=None, + dtype_t[:, ::1] out=None, char shift_x=0, char shift_y=0, float p0=0.): """return 255 if g > percentile p0 """ From d251fd88919013d3a0665ba93445c0c54eef21af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 30 Jun 2013 10:47:13 +0200 Subject: [PATCH 251/736] Improve code layout and styling --- skimage/filter/rank/bilateral16_cy.pyx | 4 +- skimage/filter/rank/core16_cy.pxd | 2 +- skimage/filter/rank/core16_cy.pyx | 21 ++++------ skimage/filter/rank/core8_cy.pxd | 2 +- skimage/filter/rank/core8_cy.pyx | 56 ++++++++++++------------- skimage/filter/rank/generic16_cy.pyx | 30 ++++++------- skimage/filter/rank/generic8_cy.pyx | 34 +++++++-------- skimage/filter/rank/percentile16_cy.pyx | 16 +++---- skimage/filter/rank/percentile8_cy.pyx | 16 +++---- 9 files changed, 87 insertions(+), 94 deletions(-) diff --git a/skimage/filter/rank/bilateral16_cy.pyx b/skimage/filter/rank/bilateral16_cy.pyx index d9f68f73..63a89453 100644 --- a/skimage/filter/rank/bilateral16_cy.pyx +++ b/skimage/filter/rank/bilateral16_cy.pyx @@ -15,7 +15,7 @@ from .core16_cy cimport _core16 ctypedef cnp.uint16_t dtype_t -cdef inline dtype_t kernel_mean(Py_ssize_t * histo, float pop, +cdef inline dtype_t kernel_mean(Py_ssize_t* histo, float pop, dtype_t g, Py_ssize_t bitdepth, Py_ssize_t maxbin, Py_ssize_t midbin, float p0, float p1, @@ -37,7 +37,7 @@ cdef inline dtype_t kernel_mean(Py_ssize_t * histo, float pop, return (0) -cdef inline dtype_t kernel_pop(Py_ssize_t * histo, float pop, +cdef inline dtype_t kernel_pop(Py_ssize_t* histo, float pop, dtype_t g, Py_ssize_t bitdepth, Py_ssize_t maxbin, Py_ssize_t midbin, float p0, float p1, diff --git a/skimage/filter/rank/core16_cy.pxd b/skimage/filter/rank/core16_cy.pxd index f13f0fd1..cebf0aab 100644 --- a/skimage/filter/rank/core16_cy.pxd +++ b/skimage/filter/rank/core16_cy.pxd @@ -9,7 +9,7 @@ cdef dtype_t uint16_min(dtype_t a, dtype_t b) # 16-bit core kernel receives extra information about data bitdepth -cdef void _core16(dtype_t kernel(Py_ssize_t *, float, dtype_t, +cdef void _core16(dtype_t kernel(Py_ssize_t*, float, dtype_t, Py_ssize_t, Py_ssize_t, Py_ssize_t, float, float, Py_ssize_t, Py_ssize_t), dtype_t[:, ::1] image, diff --git a/skimage/filter/rank/core16_cy.pyx b/skimage/filter/rank/core16_cy.pyx index 25592d39..02acc93d 100644 --- a/skimage/filter/rank/core16_cy.pyx +++ b/skimage/filter/rank/core16_cy.pyx @@ -146,8 +146,8 @@ cdef void _core16(dtype_t kernel(Py_ssize_t*, float, dtype_t, r = 0 c = 0 # kernel ------------------------------------------- - out[r, c] = kernel(histo, pop, image[r, c], - bitdepth, maxbin, midbin, p0, p1, s0, s1) + out[r, c] = kernel(histo, pop, image[r, c], bitdepth, maxbin, midbin, + p0, p1, s0, s1) # kernel ------------------------------------------- # main loop @@ -168,9 +168,8 @@ cdef void _core16(dtype_t kernel(Py_ssize_t*, float, dtype_t, histogram_decrement(histo, &pop, image[rr, cc]) # kernel ------------------------------------------- - out[r, c] = kernel( - histo, pop, image[r, c], - bitdepth, maxbin, midbin, p0, p1, s0, s1) + out[r, c] = kernel(histo, pop, image[r, c], bitdepth, maxbin, + midbin, p0, p1, s0, s1) # kernel ------------------------------------------- r += 1 # pass to the next row @@ -192,7 +191,7 @@ cdef void _core16(dtype_t kernel(Py_ssize_t*, float, dtype_t, # kernel ------------------------------------------- out[r, c] = kernel(histo, pop, image[r, c], - bitdepth, maxbin, midbin, p0, p1, s0, s1) + bitdepth, maxbin, midbin, p0, p1, s0, s1) # kernel ------------------------------------------- # ---> east to west @@ -210,9 +209,8 @@ cdef void _core16(dtype_t kernel(Py_ssize_t*, float, dtype_t, histogram_decrement(histo, &pop, image[rr, cc]) # kernel ------------------------------------------- - out[r, c] = kernel( - histo, pop, image[r, c], - bitdepth, maxbin, midbin, p0, p1, s0, s1) + out[r, c] = kernel(histo, pop, image[r, c], bitdepth, maxbin, + midbin, p0, p1, s0, s1) # kernel ------------------------------------------- r += 1 # pass to the next row @@ -233,12 +231,11 @@ cdef void _core16(dtype_t kernel(Py_ssize_t*, float, dtype_t, histogram_decrement(histo, &pop, image[rr, cc]) # kernel ------------------------------------------- - out[r, c] = kernel(histo, pop, image[r, c], - bitdepth, maxbin, midbin, p0, p1, s0, s1) + out[r, c] = kernel(histo, pop, image[r, c], bitdepth, maxbin, midbin, + p0, p1, s0, s1) # kernel ------------------------------------------- # release memory allocated by malloc - free(se_e_r) free(se_e_c) free(se_w_r) diff --git a/skimage/filter/rank/core8_cy.pxd b/skimage/filter/rank/core8_cy.pxd index 8d7f9744..c2b709d4 100644 --- a/skimage/filter/rank/core8_cy.pxd +++ b/skimage/filter/rank/core8_cy.pxd @@ -15,7 +15,7 @@ cdef dtype_t is_in_mask(Py_ssize_t rows, Py_ssize_t cols, # 8-bit core kernel receives extra information about data inferior and superior # percentiles -cdef void _core8(dtype_t kernel(Py_ssize_t *, float, dtype_t, float, +cdef void _core8(dtype_t kernel(Py_ssize_t*, float, dtype_t, float, float, Py_ssize_t, Py_ssize_t), dtype_t[:, ::1] image, char[:, ::1] selem, diff --git a/skimage/filter/rank/core8_cy.pyx b/skimage/filter/rank/core8_cy.pyx index b8d49a0f..357a1051 100644 --- a/skimage/filter/rank/core8_cy.pyx +++ b/skimage/filter/rank/core8_cy.pyx @@ -17,13 +17,13 @@ cdef inline dtype_t uint8_min(dtype_t a, dtype_t b): return a if a <= b else b -cdef inline void histogram_increment(Py_ssize_t * histo, float * pop, +cdef inline void histogram_increment(Py_ssize_t* histo, float* pop, dtype_t value): histo[value] += 1 pop[0] += 1 -cdef inline void histogram_decrement(Py_ssize_t * histo, float * pop, +cdef inline void histogram_decrement(Py_ssize_t* histo, float* pop, dtype_t value): histo[value] -= 1 pop[0] -= 1 @@ -42,7 +42,7 @@ cdef inline dtype_t is_in_mask(Py_ssize_t rows, Py_ssize_t cols, return 0 -cdef void _core8(dtype_t kernel(Py_ssize_t *, float, dtype_t, float, +cdef void _core8(dtype_t kernel(Py_ssize_t*, float, dtype_t, float, float, Py_ssize_t, Py_ssize_t), dtype_t[:, ::1] image, char[:, ::1] selem, @@ -83,19 +83,19 @@ cdef void _core8(dtype_t kernel(Py_ssize_t *, float, dtype_t, float, cdef Py_ssize_t num_se_n, num_se_s, num_se_e, num_se_w # the current local histogram distribution - cdef Py_ssize_t * histo = malloc(256 * sizeof(Py_ssize_t)) + cdef Py_ssize_t* histo = malloc(256 * sizeof(Py_ssize_t)) # these lists contain the relative pixel row and column for each of the 4 # attack borders east, west, north and south e.g. se_e_r lists the rows of # the east structuring element border - cdef Py_ssize_t * se_e_r = malloc(max_se * sizeof(Py_ssize_t)) - cdef Py_ssize_t * se_e_c = malloc(max_se * sizeof(Py_ssize_t)) - cdef Py_ssize_t * se_w_r = malloc(max_se * sizeof(Py_ssize_t)) - cdef Py_ssize_t * se_w_c = malloc(max_se * sizeof(Py_ssize_t)) - cdef Py_ssize_t * se_n_r = malloc(max_se * sizeof(Py_ssize_t)) - cdef Py_ssize_t * se_n_c = malloc(max_se * sizeof(Py_ssize_t)) - cdef Py_ssize_t * se_s_r = malloc(max_se * sizeof(Py_ssize_t)) - cdef Py_ssize_t * se_s_c = malloc(max_se * sizeof(Py_ssize_t)) + cdef Py_ssize_t* se_e_r = malloc(max_se * sizeof(Py_ssize_t)) + cdef Py_ssize_t* se_e_c = malloc(max_se * sizeof(Py_ssize_t)) + cdef Py_ssize_t* se_w_r = malloc(max_se * sizeof(Py_ssize_t)) + cdef Py_ssize_t* se_w_c = malloc(max_se * sizeof(Py_ssize_t)) + cdef Py_ssize_t* se_n_r = malloc(max_se * sizeof(Py_ssize_t)) + cdef Py_ssize_t* se_n_c = malloc(max_se * sizeof(Py_ssize_t)) + cdef Py_ssize_t* se_s_r = malloc(max_se * sizeof(Py_ssize_t)) + cdef Py_ssize_t* se_s_c = malloc(max_se * sizeof(Py_ssize_t)) # build attack and release borders # by using difference along axis @@ -149,10 +149,9 @@ cdef void _core8(dtype_t kernel(Py_ssize_t *, float, dtype_t, float, r = 0 c = 0 - # kernel ------------------------------------------------------------------- - out[r, c] = kernel(histo, pop, image[r, c], - p0, p1, s0, s1) - # kernel ------------------------------------------------------------------- + # kernel ------------------------------------------------------------------ + out[r, c] = kernel(histo, pop, image[r, c], p0, p1, s0, s1) + # kernel ------------------------------------------------------------------ # main loop r = 0 @@ -171,10 +170,9 @@ cdef void _core8(dtype_t kernel(Py_ssize_t *, float, dtype_t, float, if is_in_mask(rows, cols, rr, cc, mask_data): histogram_decrement(histo, &pop, image[rr, cc]) - # kernel ----------------------------------------------------------- - out[r, c] = \ - kernel(histo, pop, image[r, c], p0, p1, s0, s1) - # kernel ----------------------------------------------------------- + # kernel ---------------------------------------------------------- + out[r, c] = kernel(histo, pop, image[r, c], p0, p1, s0, s1) + # kernel ---------------------------------------------------------- r += 1 # pass to the next row if r >= rows: @@ -193,10 +191,9 @@ cdef void _core8(dtype_t kernel(Py_ssize_t *, float, dtype_t, float, if is_in_mask(rows, cols, rr, cc, mask_data): histogram_decrement(histo, &pop, image[rr, cc]) - # kernel --------------------------------------------------------------- - out[r, c] = kernel(histo, pop, image[r, c], - p0, p1, s0, s1) - # kernel --------------------------------------------------------------- + # kernel -------------------------------------------------------------- + out[r, c] = kernel(histo, pop, image[r, c], p0, p1, s0, s1) + # kernel -------------------------------------------------------------- # ---> east to west for c in range(cols - 2, -1, -1): @@ -212,10 +209,10 @@ cdef void _core8(dtype_t kernel(Py_ssize_t *, float, dtype_t, float, if is_in_mask(rows, cols, rr, cc, mask_data): histogram_decrement(histo, &pop, image[rr, cc]) - # kernel ----------------------------------------------------------- + # kernel ---------------------------------------------------------- out[r, c] = kernel( histo, pop, image[r, c], p0, p1, s0, s1) - # kernel ----------------------------------------------------------- + # kernel ---------------------------------------------------------- r += 1 # pass to the next row if r >= rows: @@ -234,10 +231,9 @@ cdef void _core8(dtype_t kernel(Py_ssize_t *, float, dtype_t, float, if is_in_mask(rows, cols, rr, cc, mask_data): histogram_decrement(histo, &pop, image[rr, cc]) - # kernel --------------------------------------------------------------- - out[r, c] = kernel(histo, pop, image[r, c], - p0, p1, s0, s1) - # kernel --------------------------------------------------------------- + # kernel -------------------------------------------------------------- + out[r, c] = kernel(histo, pop, image[r, c], p0, p1, s0, s1) + # kernel -------------------------------------------------------------- # release memory allocated by malloc free(se_e_r) diff --git a/skimage/filter/rank/generic16_cy.pyx b/skimage/filter/rank/generic16_cy.pyx index 7b2a8088..32021e78 100644 --- a/skimage/filter/rank/generic16_cy.pyx +++ b/skimage/filter/rank/generic16_cy.pyx @@ -12,7 +12,7 @@ from .core16_cy cimport dtype_t, _core16 # kernels uint16 take extra parameter for defining the bitdepth # ----------------------------------------------------------------- -cdef inline dtype_t kernel_autolevel(Py_ssize_t * histo, float pop, +cdef inline dtype_t kernel_autolevel(Py_ssize_t* histo, float pop, dtype_t g, Py_ssize_t bitdepth, Py_ssize_t maxbin, Py_ssize_t midbin, float p0, float p1, @@ -35,7 +35,7 @@ cdef inline dtype_t kernel_autolevel(Py_ssize_t * histo, float pop, return (imax - imin) -cdef inline dtype_t kernel_bottomhat(Py_ssize_t * histo, float pop, +cdef inline dtype_t kernel_bottomhat(Py_ssize_t* histo, float pop, dtype_t g, Py_ssize_t bitdepth, Py_ssize_t maxbin, Py_ssize_t midbin, float p0, float p1, @@ -51,7 +51,7 @@ cdef inline dtype_t kernel_bottomhat(Py_ssize_t * histo, float pop, else: return (0) -cdef inline dtype_t kernel_equalize(Py_ssize_t * histo, float pop, +cdef inline dtype_t kernel_equalize(Py_ssize_t* histo, float pop, dtype_t g, Py_ssize_t bitdepth, Py_ssize_t maxbin, Py_ssize_t midbin, float p0, float p1, @@ -70,7 +70,7 @@ cdef inline dtype_t kernel_equalize(Py_ssize_t * histo, float pop, return (0) -cdef inline dtype_t kernel_gradient(Py_ssize_t * histo, float pop, +cdef inline dtype_t kernel_gradient(Py_ssize_t* histo, float pop, dtype_t g, Py_ssize_t bitdepth, Py_ssize_t maxbin, Py_ssize_t midbin, float p0, float p1, @@ -91,7 +91,7 @@ cdef inline dtype_t kernel_gradient(Py_ssize_t * histo, float pop, return (0) -cdef inline dtype_t kernel_maximum(Py_ssize_t * histo, float pop, +cdef inline dtype_t kernel_maximum(Py_ssize_t* histo, float pop, dtype_t g, Py_ssize_t bitdepth, Py_ssize_t maxbin, Py_ssize_t midbin, float p0, float p1, @@ -106,7 +106,7 @@ cdef inline dtype_t kernel_maximum(Py_ssize_t * histo, float pop, return (0) -cdef inline dtype_t kernel_mean(Py_ssize_t * histo, float pop, +cdef inline dtype_t kernel_mean(Py_ssize_t* histo, float pop, dtype_t g, Py_ssize_t bitdepth, Py_ssize_t maxbin, Py_ssize_t midbin, float p0, float p1, @@ -122,7 +122,7 @@ cdef inline dtype_t kernel_mean(Py_ssize_t * histo, float pop, return (0) -cdef inline dtype_t kernel_meansubtraction(Py_ssize_t * histo, +cdef inline dtype_t kernel_meansubtraction(Py_ssize_t* histo, float pop, dtype_t g, Py_ssize_t bitdepth, @@ -141,7 +141,7 @@ cdef inline dtype_t kernel_meansubtraction(Py_ssize_t * histo, return (0) -cdef inline dtype_t kernel_median(Py_ssize_t * histo, float pop, +cdef inline dtype_t kernel_median(Py_ssize_t* histo, float pop, dtype_t g, Py_ssize_t bitdepth, Py_ssize_t maxbin, Py_ssize_t midbin, float p0, float p1, @@ -159,7 +159,7 @@ cdef inline dtype_t kernel_median(Py_ssize_t * histo, float pop, return (0) -cdef inline dtype_t kernel_minimum(Py_ssize_t * histo, float pop, +cdef inline dtype_t kernel_minimum(Py_ssize_t* histo, float pop, dtype_t g, Py_ssize_t bitdepth, Py_ssize_t maxbin, Py_ssize_t midbin, float p0, float p1, @@ -174,7 +174,7 @@ cdef inline dtype_t kernel_minimum(Py_ssize_t * histo, float pop, return (0) -cdef inline dtype_t kernel_modal(Py_ssize_t * histo, float pop, +cdef inline dtype_t kernel_modal(Py_ssize_t* histo, float pop, dtype_t g, Py_ssize_t bitdepth, Py_ssize_t maxbin, Py_ssize_t midbin, float p0, float p1, @@ -191,7 +191,7 @@ cdef inline dtype_t kernel_modal(Py_ssize_t * histo, float pop, return (0) -cdef inline dtype_t kernel_morph_contr_enh(Py_ssize_t * histo, +cdef inline dtype_t kernel_morph_contr_enh(Py_ssize_t* histo, float pop, dtype_t g, Py_ssize_t bitdepth, @@ -218,7 +218,7 @@ cdef inline dtype_t kernel_morph_contr_enh(Py_ssize_t * histo, return (0) -cdef inline dtype_t kernel_pop(Py_ssize_t * histo, float pop, +cdef inline dtype_t kernel_pop(Py_ssize_t* histo, float pop, dtype_t g, Py_ssize_t bitdepth, Py_ssize_t maxbin, Py_ssize_t midbin, float p0, float p1, @@ -226,7 +226,7 @@ cdef inline dtype_t kernel_pop(Py_ssize_t * histo, float pop, return (pop) -cdef inline dtype_t kernel_threshold(Py_ssize_t * histo, float pop, +cdef inline dtype_t kernel_threshold(Py_ssize_t* histo, float pop, dtype_t g, Py_ssize_t bitdepth, Py_ssize_t maxbin, Py_ssize_t midbin, float p0, float p1, @@ -242,7 +242,7 @@ cdef inline dtype_t kernel_threshold(Py_ssize_t * histo, float pop, return (0) -cdef inline dtype_t kernel_tophat(Py_ssize_t * histo, float pop, +cdef inline dtype_t kernel_tophat(Py_ssize_t* histo, float pop, dtype_t g, Py_ssize_t bitdepth, Py_ssize_t maxbin, Py_ssize_t midbin, float p0, float p1, @@ -258,7 +258,7 @@ cdef inline dtype_t kernel_tophat(Py_ssize_t * histo, float pop, else: return (0) -cdef inline dtype_t kernel_entropy(Py_ssize_t * histo, float pop, +cdef inline dtype_t kernel_entropy(Py_ssize_t* histo, float pop, dtype_t g, Py_ssize_t bitdepth, Py_ssize_t maxbin, Py_ssize_t midbin, float p0, float p1, diff --git a/skimage/filter/rank/generic8_cy.pyx b/skimage/filter/rank/generic8_cy.pyx index 62749041..93473a85 100644 --- a/skimage/filter/rank/generic8_cy.pyx +++ b/skimage/filter/rank/generic8_cy.pyx @@ -13,7 +13,7 @@ from .core8_cy cimport dtype_t, _core8 # ----------------------------------------------------------------- -cdef inline dtype_t kernel_autolevel(Py_ssize_t * histo, float pop, +cdef inline dtype_t kernel_autolevel(Py_ssize_t* histo, float pop, dtype_t g, float p0, float p1, Py_ssize_t s0, Py_ssize_t s1): @@ -37,7 +37,7 @@ cdef inline dtype_t kernel_autolevel(Py_ssize_t * histo, float pop, return (0) -cdef inline dtype_t kernel_bottomhat(Py_ssize_t * histo, float pop, +cdef inline dtype_t kernel_bottomhat(Py_ssize_t* histo, float pop, dtype_t g, float p0, float p1, Py_ssize_t s0, Py_ssize_t s1): @@ -53,7 +53,7 @@ cdef inline dtype_t kernel_bottomhat(Py_ssize_t * histo, float pop, return (0) -cdef inline dtype_t kernel_equalize(Py_ssize_t * histo, float pop, +cdef inline dtype_t kernel_equalize(Py_ssize_t* histo, float pop, dtype_t g, float p0, float p1, Py_ssize_t s0, Py_ssize_t s1): @@ -71,7 +71,7 @@ cdef inline dtype_t kernel_equalize(Py_ssize_t * histo, float pop, return (0) -cdef inline dtype_t kernel_gradient(Py_ssize_t * histo, float pop, +cdef inline dtype_t kernel_gradient(Py_ssize_t* histo, float pop, dtype_t g, float p0, float p1, Py_ssize_t s0, Py_ssize_t s1): @@ -91,7 +91,7 @@ cdef inline dtype_t kernel_gradient(Py_ssize_t * histo, float pop, return (0) -cdef inline dtype_t kernel_maximum(Py_ssize_t * histo, float pop, +cdef inline dtype_t kernel_maximum(Py_ssize_t* histo, float pop, dtype_t g, float p0, float p1, Py_ssize_t s0, Py_ssize_t s1): @@ -105,7 +105,7 @@ cdef inline dtype_t kernel_maximum(Py_ssize_t * histo, float pop, return (0) -cdef inline dtype_t kernel_mean(Py_ssize_t * histo, float pop, +cdef inline dtype_t kernel_mean(Py_ssize_t* histo, float pop, dtype_t g, float p0, float p1, Py_ssize_t s0, Py_ssize_t s1): @@ -120,7 +120,7 @@ cdef inline dtype_t kernel_mean(Py_ssize_t * histo, float pop, return (0) -cdef inline dtype_t kernel_meansubtraction(Py_ssize_t * histo, float pop, +cdef inline dtype_t kernel_meansubtraction(Py_ssize_t* histo, float pop, dtype_t g, float p0, float p1, Py_ssize_t s0, Py_ssize_t s1): @@ -135,7 +135,7 @@ cdef inline dtype_t kernel_meansubtraction(Py_ssize_t * histo, float pop, return (0) -cdef inline dtype_t kernel_median(Py_ssize_t * histo, float pop, +cdef inline dtype_t kernel_median(Py_ssize_t* histo, float pop, dtype_t g, float p0, float p1, Py_ssize_t s0, Py_ssize_t s1): @@ -152,7 +152,7 @@ cdef inline dtype_t kernel_median(Py_ssize_t * histo, float pop, return (0) -cdef inline dtype_t kernel_minimum(Py_ssize_t * histo, float pop, +cdef inline dtype_t kernel_minimum(Py_ssize_t* histo, float pop, dtype_t g, float p0, float p1, Py_ssize_t s0, Py_ssize_t s1): @@ -166,7 +166,7 @@ cdef inline dtype_t kernel_minimum(Py_ssize_t * histo, float pop, return (0) -cdef inline dtype_t kernel_modal(Py_ssize_t * histo, float pop, +cdef inline dtype_t kernel_modal(Py_ssize_t* histo, float pop, dtype_t g, float p0, float p1, Py_ssize_t s0, Py_ssize_t s1): @@ -182,7 +182,7 @@ cdef inline dtype_t kernel_modal(Py_ssize_t * histo, float pop, return (0) -cdef inline dtype_t kernel_morph_contr_enh(Py_ssize_t * histo, float pop, +cdef inline dtype_t kernel_morph_contr_enh(Py_ssize_t* histo, float pop, dtype_t g, float p0, float p1, Py_ssize_t s0, Py_ssize_t s1): @@ -205,14 +205,14 @@ cdef inline dtype_t kernel_morph_contr_enh(Py_ssize_t * histo, float pop, return (0) -cdef inline dtype_t kernel_pop(Py_ssize_t * histo, float pop, +cdef inline dtype_t kernel_pop(Py_ssize_t* histo, float pop, dtype_t g, float p0, float p1, Py_ssize_t s0, Py_ssize_t s1): return (pop) -cdef inline dtype_t kernel_threshold(Py_ssize_t * histo, float pop, +cdef inline dtype_t kernel_threshold(Py_ssize_t* histo, float pop, dtype_t g, float p0, float p1, Py_ssize_t s0, Py_ssize_t s1): @@ -227,7 +227,7 @@ cdef inline dtype_t kernel_threshold(Py_ssize_t * histo, float pop, return (0) -cdef inline dtype_t kernel_tophat(Py_ssize_t * histo, float pop, +cdef inline dtype_t kernel_tophat(Py_ssize_t* histo, float pop, dtype_t g, float p0, float p1, Py_ssize_t s0, Py_ssize_t s1): @@ -242,7 +242,7 @@ cdef inline dtype_t kernel_tophat(Py_ssize_t * histo, float pop, else: return (0) -cdef inline dtype_t kernel_noise_filter(Py_ssize_t * histo, float pop, +cdef inline dtype_t kernel_noise_filter(Py_ssize_t* histo, float pop, dtype_t g, float p0, float p1, Py_ssize_t s0, Py_ssize_t s1): @@ -266,7 +266,7 @@ cdef inline dtype_t kernel_noise_filter(Py_ssize_t * histo, float pop, return min_i -cdef inline dtype_t kernel_entropy(Py_ssize_t * histo, float pop, +cdef inline dtype_t kernel_entropy(Py_ssize_t* histo, float pop, dtype_t g, float p0, float p1, Py_ssize_t s0, Py_ssize_t s1): cdef Py_ssize_t i @@ -284,7 +284,7 @@ cdef inline dtype_t kernel_entropy(Py_ssize_t * histo, float pop, else: return (0) -cdef inline dtype_t kernel_otsu(Py_ssize_t * histo, float pop, dtype_t g, +cdef inline dtype_t kernel_otsu(Py_ssize_t* histo, float pop, dtype_t g, float p0, float p1, Py_ssize_t s0, Py_ssize_t s1): cdef Py_ssize_t i diff --git a/skimage/filter/rank/percentile16_cy.pyx b/skimage/filter/rank/percentile16_cy.pyx index 8680aae4..d99dc3c8 100644 --- a/skimage/filter/rank/percentile16_cy.pyx +++ b/skimage/filter/rank/percentile16_cy.pyx @@ -11,7 +11,7 @@ from .core16_cy cimport dtype_t, _core16, uint16_min, uint16_max # kernels uint16 (SOFT version using percentiles) # ----------------------------------------------------------------- -cdef inline dtype_t kernel_autolevel(Py_ssize_t * histo, float pop, +cdef inline dtype_t kernel_autolevel(Py_ssize_t* histo, float pop, dtype_t g, Py_ssize_t bitdepth, Py_ssize_t maxbin, Py_ssize_t midbin, float p0, float p1, @@ -45,7 +45,7 @@ cdef inline dtype_t kernel_autolevel(Py_ssize_t * histo, float pop, return (0) -cdef inline dtype_t kernel_gradient(Py_ssize_t * histo, float pop, +cdef inline dtype_t kernel_gradient(Py_ssize_t* histo, float pop, dtype_t g, Py_ssize_t bitdepth, Py_ssize_t maxbin, Py_ssize_t midbin, float p0, float p1, @@ -73,7 +73,7 @@ cdef inline dtype_t kernel_gradient(Py_ssize_t * histo, float pop, return (0) -cdef inline dtype_t kernel_mean(Py_ssize_t * histo, float pop, +cdef inline dtype_t kernel_mean(Py_ssize_t* histo, float pop, dtype_t g, Py_ssize_t bitdepth, Py_ssize_t maxbin, Py_ssize_t midbin, float p0, float p1, @@ -99,7 +99,7 @@ cdef inline dtype_t kernel_mean(Py_ssize_t * histo, float pop, return (0) -cdef inline dtype_t kernel_mean_subtraction(Py_ssize_t * histo, +cdef inline dtype_t kernel_mean_subtraction(Py_ssize_t* histo, float pop, dtype_t g, Py_ssize_t bitdepth, @@ -127,7 +127,7 @@ cdef inline dtype_t kernel_mean_subtraction(Py_ssize_t * histo, return (0) -cdef inline dtype_t kernel_morph_contr_enh(Py_ssize_t * histo, +cdef inline dtype_t kernel_morph_contr_enh(Py_ssize_t* histo, float pop, dtype_t g, Py_ssize_t bitdepth, @@ -164,7 +164,7 @@ cdef inline dtype_t kernel_morph_contr_enh(Py_ssize_t * histo, return (0) -cdef inline dtype_t kernel_percentile(Py_ssize_t * histo, float pop, +cdef inline dtype_t kernel_percentile(Py_ssize_t* histo, float pop, dtype_t g, Py_ssize_t bitdepth, Py_ssize_t maxbin, Py_ssize_t midbin, float p0, float p1, @@ -184,7 +184,7 @@ cdef inline dtype_t kernel_percentile(Py_ssize_t * histo, float pop, return (0) -cdef inline dtype_t kernel_pop(Py_ssize_t * histo, float pop, +cdef inline dtype_t kernel_pop(Py_ssize_t* histo, float pop, dtype_t g, Py_ssize_t bitdepth, Py_ssize_t maxbin, Py_ssize_t midbin, float p0, float p1, @@ -204,7 +204,7 @@ cdef inline dtype_t kernel_pop(Py_ssize_t * histo, float pop, return (0) -cdef inline dtype_t kernel_threshold(Py_ssize_t * histo, float pop, +cdef inline dtype_t kernel_threshold(Py_ssize_t* histo, float pop, dtype_t g, Py_ssize_t bitdepth, Py_ssize_t maxbin, Py_ssize_t midbin, float p0, float p1, diff --git a/skimage/filter/rank/percentile8_cy.pyx b/skimage/filter/rank/percentile8_cy.pyx index 6c37cf66..3ce63478 100644 --- a/skimage/filter/rank/percentile8_cy.pyx +++ b/skimage/filter/rank/percentile8_cy.pyx @@ -12,7 +12,7 @@ from .core8_cy cimport dtype_t, _core8, uint8_max, uint8_min # ----------------------------------------------------------------- -cdef inline dtype_t kernel_autolevel(Py_ssize_t * histo, float pop, +cdef inline dtype_t kernel_autolevel(Py_ssize_t* histo, float pop, dtype_t g, float p0, float p1, Py_ssize_t s0, Py_ssize_t s1): cdef int i, imin, imax, sum, delta @@ -44,7 +44,7 @@ cdef inline dtype_t kernel_autolevel(Py_ssize_t * histo, float pop, return (128) -cdef inline dtype_t kernel_gradient(Py_ssize_t * histo, float pop, +cdef inline dtype_t kernel_gradient(Py_ssize_t* histo, float pop, dtype_t g, float p0, float p1, Py_ssize_t s0, Py_ssize_t s1): cdef int i, imin, imax, sum, delta @@ -69,7 +69,7 @@ cdef inline dtype_t kernel_gradient(Py_ssize_t * histo, float pop, return (0) -cdef inline dtype_t kernel_mean(Py_ssize_t * histo, float pop, +cdef inline dtype_t kernel_mean(Py_ssize_t* histo, float pop, dtype_t g, float p0, float p1, Py_ssize_t s0, Py_ssize_t s1): cdef int i, sum, mean, n @@ -91,7 +91,7 @@ cdef inline dtype_t kernel_mean(Py_ssize_t * histo, float pop, return (0) -cdef inline dtype_t kernel_mean_subtraction(Py_ssize_t * histo, +cdef inline dtype_t kernel_mean_subtraction(Py_ssize_t* histo, float pop, dtype_t g, float p0, float p1, @@ -115,7 +115,7 @@ cdef inline dtype_t kernel_mean_subtraction(Py_ssize_t * histo, return (0) -cdef inline dtype_t kernel_morph_contr_enh(Py_ssize_t * histo, +cdef inline dtype_t kernel_morph_contr_enh(Py_ssize_t* histo, float pop, dtype_t g, float p0, float p1, Py_ssize_t s0, Py_ssize_t s1): @@ -147,7 +147,7 @@ cdef inline dtype_t kernel_morph_contr_enh(Py_ssize_t * histo, return (0) -cdef inline dtype_t kernel_percentile(Py_ssize_t * histo, float pop, +cdef inline dtype_t kernel_percentile(Py_ssize_t* histo, float pop, dtype_t g, float p0, float p1, Py_ssize_t s0, Py_ssize_t s1): cdef int i @@ -164,7 +164,7 @@ cdef inline dtype_t kernel_percentile(Py_ssize_t * histo, float pop, return (0) -cdef inline dtype_t kernel_pop(Py_ssize_t * histo, float pop, +cdef inline dtype_t kernel_pop(Py_ssize_t* histo, float pop, dtype_t g, float p0, float p1, Py_ssize_t s0, Py_ssize_t s1): cdef int i, sum, n @@ -181,7 +181,7 @@ cdef inline dtype_t kernel_pop(Py_ssize_t * histo, float pop, return (0) -cdef inline dtype_t kernel_threshold(Py_ssize_t * histo, float pop, +cdef inline dtype_t kernel_threshold(Py_ssize_t* histo, float pop, dtype_t g, float p0, float p1, Py_ssize_t s0, Py_ssize_t s1): cdef int i From ea8111d0e3ee414461888588a040a2eae3ba83f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 30 Jun 2013 10:52:56 +0200 Subject: [PATCH 252/736] Use typed memoryviews for diff array --- skimage/filter/rank/core16_cy.pyx | 8 ++++---- skimage/filter/rank/core8_cy.pyx | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/skimage/filter/rank/core16_cy.pyx b/skimage/filter/rank/core16_cy.pyx index 02acc93d..8fe72423 100644 --- a/skimage/filter/rank/core16_cy.pyx +++ b/skimage/filter/rank/core16_cy.pyx @@ -97,16 +97,16 @@ cdef void _core16(dtype_t kernel(Py_ssize_t*, float, dtype_t, # build attack and release borders # by using difference along axis t = np.hstack((selem, np.zeros((selem.shape[0], 1)))) - t_e = np.diff(t, axis=1) < 0 + cdef char[:, :] t_e = np.diff(t, axis=1) < 0 t = np.hstack((np.zeros((selem.shape[0], 1)), selem)) - t_w = np.diff(t, axis=1) > 0 + cdef char[:, :] t_w = np.diff(t, axis=1) > 0 t = np.vstack((selem, np.zeros((1, selem.shape[1])))) - t_s = np.diff(t, axis=0) < 0 + cdef char[:, :] t_s = np.diff(t, axis=0) < 0 t = np.vstack((np.zeros((1, selem.shape[1])), selem)) - t_n = np.diff(t, axis=0) > 0 + cdef char[:, :] t_n = np.diff(t, axis=0) > 0 num_se_n = num_se_s = num_se_e = num_se_w = 0 diff --git a/skimage/filter/rank/core8_cy.pyx b/skimage/filter/rank/core8_cy.pyx index 357a1051..0b6ac554 100644 --- a/skimage/filter/rank/core8_cy.pyx +++ b/skimage/filter/rank/core8_cy.pyx @@ -100,16 +100,16 @@ cdef void _core8(dtype_t kernel(Py_ssize_t*, float, dtype_t, float, # build attack and release borders # by using difference along axis t = np.hstack((selem, np.zeros((selem.shape[0], 1)))) - t_e = np.diff(t, axis=1) < 0 + cdef char[:, :] t_e = np.diff(t, axis=1) < 0 t = np.hstack((np.zeros((selem.shape[0], 1)), selem)) - t_w = np.diff(t, axis=1) > 0 + cdef char[:, :] t_w = np.diff(t, axis=1) > 0 t = np.vstack((selem, np.zeros((1, selem.shape[1])))) - t_s = np.diff(t, axis=0) < 0 + cdef char[:, :] t_s = np.diff(t, axis=0) < 0 t = np.vstack((np.zeros((1, selem.shape[1])), selem)) - t_n = np.diff(t, axis=0) > 0 + cdef char[:, :] t_n = np.diff(t, axis=0) > 0 num_se_n = num_se_s = num_se_e = num_se_w = 0 From cd7959dd0836c135a64045a34ca590e59206c37b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 30 Jun 2013 10:55:48 +0200 Subject: [PATCH 253/736] Fix styling of some comments --- skimage/filter/rank/core16_cy.pyx | 9 ++++----- skimage/filter/rank/core8_cy.pyx | 9 ++++----- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/skimage/filter/rank/core16_cy.pyx b/skimage/filter/rank/core16_cy.pyx index 8fe72423..04d829e2 100644 --- a/skimage/filter/rank/core16_cy.pyx +++ b/skimage/filter/rank/core16_cy.pyx @@ -94,8 +94,7 @@ cdef void _core16(dtype_t kernel(Py_ssize_t*, float, dtype_t, cdef Py_ssize_t* se_s_r = malloc(max_se * sizeof(Py_ssize_t)) cdef Py_ssize_t* se_s_c = malloc(max_se * sizeof(Py_ssize_t)) - # build attack and release borders - # by using difference along axis + # build attack and release borders by using difference along axis t = np.hstack((selem, np.zeros((selem.shape[0], 1)))) cdef char[:, :] t_e = np.diff(t, axis=1) < 0 @@ -172,11 +171,11 @@ cdef void _core16(dtype_t kernel(Py_ssize_t*, float, dtype_t, midbin, p0, p1, s0, s1) # kernel ------------------------------------------- - r += 1 # pass to the next row + r += 1 # pass to the next row if r >= rows: break - # ---> north to south + # ---> north to south for s in range(num_se_s): rr = r + se_s_r[s] cc = c + se_s_c[s] @@ -213,7 +212,7 @@ cdef void _core16(dtype_t kernel(Py_ssize_t*, float, dtype_t, midbin, p0, p1, s0, s1) # kernel ------------------------------------------- - r += 1 # pass to the next row + r += 1 # pass to the next row if r >= rows: break diff --git a/skimage/filter/rank/core8_cy.pyx b/skimage/filter/rank/core8_cy.pyx index 0b6ac554..ad7c7ac4 100644 --- a/skimage/filter/rank/core8_cy.pyx +++ b/skimage/filter/rank/core8_cy.pyx @@ -97,8 +97,7 @@ cdef void _core8(dtype_t kernel(Py_ssize_t*, float, dtype_t, float, cdef Py_ssize_t* se_s_r = malloc(max_se * sizeof(Py_ssize_t)) cdef Py_ssize_t* se_s_c = malloc(max_se * sizeof(Py_ssize_t)) - # build attack and release borders - # by using difference along axis + # build attack and release borders by using difference along axis t = np.hstack((selem, np.zeros((selem.shape[0], 1)))) cdef char[:, :] t_e = np.diff(t, axis=1) < 0 @@ -174,11 +173,11 @@ cdef void _core8(dtype_t kernel(Py_ssize_t*, float, dtype_t, float, out[r, c] = kernel(histo, pop, image[r, c], p0, p1, s0, s1) # kernel ---------------------------------------------------------- - r += 1 # pass to the next row + r += 1 # pass to the next row if r >= rows: break - # ---> north to south + # ---> north to south for s in range(num_se_s): rr = r + se_s_r[s] cc = c + se_s_c[s] @@ -214,7 +213,7 @@ cdef void _core8(dtype_t kernel(Py_ssize_t*, float, dtype_t, float, histo, pop, image[r, c], p0, p1, s0, s1) # kernel ---------------------------------------------------------- - r += 1 # pass to the next row + r += 1 # pass to the next row if r >= rows: break From aa973aeff227dd1d8b6b9ac43c31bf96cd956454 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 30 Jun 2013 10:59:34 +0200 Subject: [PATCH 254/736] Fix wrong module name --- skimage/filter/rank/bilateral.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skimage/filter/rank/bilateral.py b/skimage/filter/rank/bilateral.py index a111dd7c..4eafac19 100644 --- a/skimage/filter/rank/bilateral.py +++ b/skimage/filter/rank/bilateral.py @@ -130,7 +130,7 @@ def bilateral_mean(image, selem, out=None, mask=None, shift_x=False, >>> bilat_ima = bilateral_mean(ima, disk(20), s0=10,s1=10) """ - return _apply(None, _bilateral16_cy.mean, image, selem, out=out, + return _apply(None, bilateral16_cy.mean, image, selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y, s0=s0, s1=s1) @@ -188,5 +188,5 @@ def bilateral_pop(image, selem, out=None, mask=None, shift_x=False, """ - return _apply(None, _bilateral16_cy.pop, image, selem, out=out, + return _apply(None, bilateral16_cy.pop, image, selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y, s0=s0, s1=s1) From c9e81053f78fb326257d63e3c1442876adf0cfec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 30 Jun 2013 11:02:21 +0200 Subject: [PATCH 255/736] Capitalize parameter description in doc string --- skimage/filter/rank/bilateral.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skimage/filter/rank/bilateral.py b/skimage/filter/rank/bilateral.py index 4eafac19..13bfe37a 100644 --- a/skimage/filter/rank/bilateral.py +++ b/skimage/filter/rank/bilateral.py @@ -103,7 +103,7 @@ def bilateral_mean(image, selem, out=None, mask=None, shift_x=False, to the structuring element sizes (center must be inside the given structuring element). s0, s1 : int - define the [s0, s1] interval to be considered for computing the value. + Define the [s0, s1] interval to be considered for computing the value. Returns ------- @@ -157,7 +157,7 @@ def bilateral_pop(image, selem, out=None, mask=None, shift_x=False, to the structuring element sizes (center must be inside the given structuring element). s0, s1 : int - define the [s0, s1] interval to be considered for computing the value. + Define the [s0, s1] interval to be considered for computing the value. Returns ------- From ccd902fcb492ef1a0635346d49318f48b70d377b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 30 Jun 2013 11:14:55 +0200 Subject: [PATCH 256/736] Extend bitdepth from 12bit to full 16bit --- skimage/filter/rank/bilateral.py | 15 ++---- skimage/filter/rank/core16_cy.pyx | 6 +-- skimage/filter/rank/generic.py | 76 ++++++++----------------------- skimage/filter/rank/percentile.py | 39 ++++------------ 4 files changed, 38 insertions(+), 98 deletions(-) diff --git a/skimage/filter/rank/bilateral.py b/skimage/filter/rank/bilateral.py index 13bfe37a..26784df8 100644 --- a/skimage/filter/rank/bilateral.py +++ b/skimage/filter/rank/bilateral.py @@ -3,8 +3,7 @@ The local histogram is computed using a sliding window similar to the method described in [1]_. -Input image must be 16-bit with a value < 4096 (i.e. 12 bit), -the number of histogram bins is determined from the +Input image must be 16-bit, the number of histogram bins is determined from the maximum value present in the image. The pixel neighborhood is defined by: @@ -61,8 +60,6 @@ def _apply(func8, func16, image, selem, out, mask, shift_x, shift_y, s0, s1): if out is None: out = np.zeros(image.shape, dtype=np.uint16) bitdepth = find_bitdepth(image) - if bitdepth > 11: - raise ValueError("Only uint16 <4096 image (12bit) supported.") func16(image, selem, shift_x=shift_x, shift_y=shift_y, mask=mask, bitdepth=bitdepth + 1, out=out, s0=s0, s1=s1) else: @@ -88,9 +85,8 @@ def bilateral_mean(image, selem, out=None, mask=None, shift_x=False, Parameters ---------- - image : ndarray - Image array (uint16). As the algorithm uses max. 12bit histogram, - an exception will be raised if image has a value > 4095 + image : ndarray (uint16) + Input image. selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. out : ndarray @@ -142,9 +138,8 @@ def bilateral_pop(image, selem, out=None, mask=None, shift_x=False, Parameters ---------- - image : ndarray - Image array (uint16). As the algorithm uses max. 12bit histogram, - an exception will be raised if image has a value > 4095 + image : ndarray (uint16) + Input image. selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. out : ndarray diff --git a/skimage/filter/rank/core16_cy.pyx b/skimage/filter/rank/core16_cy.pyx index 04d829e2..2b01f246 100644 --- a/skimage/filter/rank/core16_cy.pyx +++ b/skimage/filter/rank/core16_cy.pyx @@ -56,10 +56,10 @@ cdef void _core16(dtype_t kernel(Py_ssize_t*, float, dtype_t, assert centre_c >= 0 assert centre_r < srows assert centre_c < scols - assert bitdepth in range(2, 13) - maxbin_list = [0, 0, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096] - midbin_list = [0, 0, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048] + maxbin_list = [0, 0, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, + 8192, 16384, 32768, 65536] + midbin_list = [m / 2 for m in maxbin_list] # set maxbin and midbin cdef Py_ssize_t maxbin = maxbin_list[bitdepth] diff --git a/skimage/filter/rank/generic.py b/skimage/filter/rank/generic.py index 124d4f25..bcfda1bc 100644 --- a/skimage/filter/rank/generic.py +++ b/skimage/filter/rank/generic.py @@ -1,11 +1,10 @@ """The local histogram is computed using a sliding window similar to the method described in [1]_. -Input image can be 8-bit or 16-bit with a value < 4096 (i.e. 12 bit), for 16-bit -input images, the number of histogram bins is determined from the maximum value -present in the image. +Input image can be 8-bit or 16-bit, for 16-bit input images, the number of +histogram bins is determined from the maximum value present in the image. -Result image is 8 or 16-bit with respect to the input image. +Result image is 8- or 16-bit with respect to the input image. References ---------- @@ -61,9 +60,6 @@ def _apply(func8, func16, image, selem, out, mask, shift_x, shift_y): if out is None: out = np.zeros(image.shape, dtype=np.uint16) bitdepth = find_bitdepth(image) - if bitdepth > 11: - image = image >> 4 - bitdepth = find_bitdepth(image) func16(image, selem, shift_x=shift_x, shift_y=shift_y, mask=mask, bitdepth=bitdepth + 1, out=out) @@ -85,9 +81,7 @@ def autolevel(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Parameters ---------- image : ndarray - Image array (uint8 array or uint16). If image is uint16, the algorithm - uses max. 12bit histogram, an exception will be raised if image has a - value > 4095. + Image array (uint8 array or uint16). selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. out : ndarray @@ -127,9 +121,7 @@ def bottomhat(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Parameters ---------- image : ndarray - Image array (uint8 array or uint16). If image is uint16, the algorithm - uses max. 12bit histogram, an exception will be raised if image has a - value > 4095. + Image array (uint8 array or uint16). selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. out : ndarray @@ -159,9 +151,7 @@ def equalize(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Parameters ---------- image : ndarray - Image array (uint8 array or uint16). If image is uint16, the algorithm - uses max. 12bit histogram, an exception will be raised if image has a - value > 4095. + Image array (uint8 array or uint16). selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. out : ndarray @@ -203,9 +193,7 @@ def gradient(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Parameters ---------- image : ndarray - Image array (uint8 array or uint16). If image is uint16, the algorithm - uses max. 12bit histogram, an exception will be raised if image has a - value > 4095. + Image array (uint8 array or uint16). selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. out : ndarray @@ -236,9 +224,7 @@ def maximum(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Parameters ---------- image : ndarray - Image array (uint8 array or uint16). If image is uint16, the algorithm - uses max. 12bit histogram, an exception will be raised if image has a - value > 4095. + Image array (uint8 array or uint16). selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. out : ndarray @@ -278,9 +264,7 @@ def mean(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Parameters ---------- image : ndarray - Image array (uint8 array or uint16). If image is uint16, the algorithm - uses max. 12bit histogram, an exception will be raised if image has a - value > 4095. + Image array (uint8 array or uint16). selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. out : ndarray @@ -321,9 +305,7 @@ def meansubtraction(image, selem, out=None, mask=None, shift_x=False, Parameters ---------- image : ndarray - Image array (uint8 array or uint16). If image is uint16, the algorithm - uses max. 12bit histogram, an exception will be raised if image has a - value > 4095. + Image array (uint8 array or uint16). selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. out : ndarray @@ -354,9 +336,7 @@ def median(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Parameters ---------- image : ndarray - Image array (uint8 array or uint16). If image is uint16, the algorithm - uses max. 12bit histogram, an exception will be raised if image has a - value > 4095. + Image array (uint8 array or uint16). selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. out : ndarray @@ -396,9 +376,7 @@ def minimum(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Parameters ---------- image : ndarray - Image array (uint8 array or uint16). If image is uint16, the algorithm - uses max. 12bit histogram, an exception will be raised if image has a - value > 4095. + Image array (uint8 array or uint16). selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. out : ndarray @@ -438,9 +416,7 @@ def modal(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Parameters ---------- image : ndarray - Image array (uint8 array or uint16). If image is uint16, the algorithm - uses max. 12bit histogram, an exception will be raised if image has a - value > 4095. + Image array (uint8 array or uint16). selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. out : ndarray @@ -473,9 +449,7 @@ def morph_contr_enh(image, selem, out=None, mask=None, shift_x=False, Parameters ---------- image : ndarray - Image array (uint8 array or uint16). If image is uint16, the algorithm - uses max. 12bit histogram, an exception will be raised if image has a - value > 4095. + Image array (uint8 array or uint16). selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. out : ndarray @@ -517,9 +491,7 @@ def pop(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Parameters ---------- image : ndarray - Image array (uint8 array or uint16). If image is uint16, the algorithm - uses max. 12bit histogram, an exception will be raised if image has a - value > 4095. + Image array (uint8 array or uint16). selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. out : ndarray @@ -566,9 +538,7 @@ def threshold(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Parameters ---------- image : ndarray - Image array (uint8 array or uint16). If image is uint16, the algorithm - uses max. 12bit histogram, an exception will be raised if image has a - value > 4095. + Image array (uint8 array or uint16). selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. out : ndarray @@ -615,9 +585,7 @@ def tophat(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Parameters ---------- image : ndarray - Image array (uint8 array or uint16). If image is uint16, the algorithm - uses max. 12bit histogram, an exception will be raised if image has a - value > 4095. + Image array (uint8 array or uint16). selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. out : ndarray @@ -648,9 +616,7 @@ def noise_filter(image, selem, out=None, mask=None, shift_x=False, Parameters ---------- image : ndarray - Image array (uint8 array or uint16). If image is uint16, the algorithm - uses max. 12bit histogram, an exception will be raised if image has a - value > 4095. + Image array (uint8 array or uint16). selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. out : ndarray @@ -694,9 +660,7 @@ def entropy(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Parameters ---------- image : ndarray - Image array (uint8 array or uint16). If image is uint16, the algorithm - uses max. 12bit histogram, an exception will be raised if image has a - value > 4095. + Image array (uint8 array or uint16). selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. out : ndarray @@ -712,7 +676,7 @@ def entropy(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Returns ------- out : uint8 array or uint16 array (same as input image) - entropy x10 (uint8 images) and entropy x1000 (uint16 images) + Entropy x10 (uint8 images) and entropy x1000 (uint16 images) References ---------- diff --git a/skimage/filter/rank/percentile.py b/skimage/filter/rank/percentile.py index 25758115..56c67983 100644 --- a/skimage/filter/rank/percentile.py +++ b/skimage/filter/rank/percentile.py @@ -7,9 +7,8 @@ not produce halos. The local histogram is computed using a sliding window similar to the method described in [1]_. -Input image can be 8-bit or 16-bit with a value < 4096 (i.e. 12 bit), for 16-bit -input images, the number of histogram bins is determined from the maximum value -present in the image. +Input image can be 8-bit or 16-bit, for 16-bit input images, the number of +histogram bins is determined from the maximum value present in the image. Result image is 8 or 16-bit with respect to the input image. @@ -60,8 +59,6 @@ def _apply(func8, func16, image, selem, out, mask, shift_x, shift_y, p0, p1): if out is None: out = np.zeros(image.shape, dtype=np.uint16) bitdepth = find_bitdepth(image) - if bitdepth > 11: - raise ValueError("Only uint16 <4096 image (12bit) supported.") func16(image, selem, shift_x=shift_x, shift_y=shift_y, mask=mask, bitdepth=bitdepth + 1, out=out, p0=p0, p1=p1) else: @@ -80,9 +77,7 @@ def percentile_autolevel(image, selem, out=None, mask=None, shift_x=False, Parameters ---------- image : ndarray - Image array (uint8 array or uint16). If image is uint16, as the - algorithm uses max. 12bit histogram, an exception will be raised if - image has a value > 4095. + Image array (uint8 array or uint16). selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. out : ndarray @@ -121,9 +116,7 @@ def percentile_gradient(image, selem, out=None, mask=None, shift_x=False, Parameters ---------- image : ndarray - Image array (uint8 array or uint16). If image is uint16, as the - algorithm uses max. 12bit histogram, an exception will be raised if - image has a value > 4095. + Image array (uint8 array or uint16). selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. out : ndarray @@ -161,9 +154,7 @@ def percentile_mean(image, selem, out=None, mask=None, shift_x=False, Parameters ---------- image : ndarray - Image array (uint8 array or uint16). If image is uint16, as the - algorithm uses max. 12bit histogram, an exception will be raised if - image has a value > 4095. + Image array (uint8 array or uint16). selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. out : ndarray @@ -201,9 +192,7 @@ def percentile_mean_subtraction(image, selem, out=None, mask=None, Parameters ---------- image : ndarray - Image array (uint8 array or uint16). If image is uint16, as the - algorithm uses max. 12bit histogram, an exception will be raised if - image has a value > 4095. + Image array (uint8 array or uint16). selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. out : ndarray @@ -243,9 +232,7 @@ def percentile_morph_contr_enh( Parameters ---------- image : ndarray - Image array (uint8 array or uint16). If image is uint16, as the - algorithm uses max. 12bit histogram, an exception will be raised if - image has a value > 4095. + Image array (uint8 array or uint16). selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. out : ndarray @@ -284,9 +271,7 @@ def percentile(image, selem, out=None, mask=None, shift_x=False, shift_y=False, Parameters ---------- image : ndarray - Image array (uint8 array or uint16). If image is uint16, as the - algorithm uses max. 12bit histogram, an exception will be raised if - image has a value > 4095. + Image array (uint8 array or uint16). selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. out : ndarray @@ -324,9 +309,7 @@ def percentile_pop(image, selem, out=None, mask=None, shift_x=False, Parameters ---------- image : ndarray - Image array (uint8 array or uint16). If image is uint16, as the - algorithm uses max. 12bit histogram, an exception will be raised if - image has a value > 4095. + Image array (uint8 array or uint16). selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. out : ndarray @@ -366,9 +349,7 @@ def percentile_threshold(image, selem, out=None, mask=None, shift_x=False, Parameters ---------- image : ndarray - Image array (uint8 array or uint16). If image is uint16, as the - algorithm uses max. 12bit histogram, an exception will be raised if - image has a value > 4095. + Image array (uint8 array or uint16). selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. out : ndarray From e9fcdc4a56c21ac0f1a668f6ac19e03e0853c0b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 30 Jun 2013 12:21:08 +0200 Subject: [PATCH 257/736] Fix attack-release border initialization --- skimage/filter/rank/core16_cy.pyx | 8 ++++---- skimage/filter/rank/core8_cy.pyx | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/skimage/filter/rank/core16_cy.pyx b/skimage/filter/rank/core16_cy.pyx index 2b01f246..5641d2f1 100644 --- a/skimage/filter/rank/core16_cy.pyx +++ b/skimage/filter/rank/core16_cy.pyx @@ -96,16 +96,16 @@ cdef void _core16(dtype_t kernel(Py_ssize_t*, float, dtype_t, # build attack and release borders by using difference along axis t = np.hstack((selem, np.zeros((selem.shape[0], 1)))) - cdef char[:, :] t_e = np.diff(t, axis=1) < 0 + cdef char[:, :] t_e = (np.diff(t, axis=1) < 0).view(np.uint8) t = np.hstack((np.zeros((selem.shape[0], 1)), selem)) - cdef char[:, :] t_w = np.diff(t, axis=1) > 0 + cdef char[:, :] t_w = (np.diff(t, axis=1) > 0).view(np.uint8) t = np.vstack((selem, np.zeros((1, selem.shape[1])))) - cdef char[:, :] t_s = np.diff(t, axis=0) < 0 + cdef char[:, :] t_s = (np.diff(t, axis=0) < 0).view(np.uint8) t = np.vstack((np.zeros((1, selem.shape[1])), selem)) - cdef char[:, :] t_n = np.diff(t, axis=0) > 0 + cdef char[:, :] t_n = (np.diff(t, axis=0) > 0).view(np.uint8) num_se_n = num_se_s = num_se_e = num_se_w = 0 diff --git a/skimage/filter/rank/core8_cy.pyx b/skimage/filter/rank/core8_cy.pyx index ad7c7ac4..e6c0a714 100644 --- a/skimage/filter/rank/core8_cy.pyx +++ b/skimage/filter/rank/core8_cy.pyx @@ -99,16 +99,16 @@ cdef void _core8(dtype_t kernel(Py_ssize_t*, float, dtype_t, float, # build attack and release borders by using difference along axis t = np.hstack((selem, np.zeros((selem.shape[0], 1)))) - cdef char[:, :] t_e = np.diff(t, axis=1) < 0 + cdef char[:, :] t_e = (np.diff(t, axis=1) < 0).view(np.uint8) t = np.hstack((np.zeros((selem.shape[0], 1)), selem)) - cdef char[:, :] t_w = np.diff(t, axis=1) > 0 + cdef char[:, :] t_w = (np.diff(t, axis=1) > 0).view(np.uint8) t = np.vstack((selem, np.zeros((1, selem.shape[1])))) - cdef char[:, :] t_s = np.diff(t, axis=0) < 0 + cdef char[:, :] t_s = (np.diff(t, axis=0) < 0).view(np.uint8) t = np.vstack((np.zeros((1, selem.shape[1])), selem)) - cdef char[:, :] t_n = np.diff(t, axis=0) > 0 + cdef char[:, :] t_n = (np.diff(t, axis=0) > 0).view(np.uint8) num_se_n = num_se_s = num_se_e = num_se_w = 0 From d1fb0137886af647d848c1397c7e2df949b3efe2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 30 Jun 2013 12:21:46 +0200 Subject: [PATCH 258/736] Fix test cases for full 16bit support --- skimage/filter/rank/tests/test_rank.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/skimage/filter/rank/tests/test_rank.py b/skimage/filter/rank/tests/test_rank.py index d39386c6..ab584776 100644 --- a/skimage/filter/rank/tests/test_rank.py +++ b/skimage/filter/rank/tests/test_rank.py @@ -130,17 +130,6 @@ def test_structuring_element8(): assert_array_equal(r, out) -def test_fail_on_bitdepth(): - # should fail because data bitdepth is too high for the function - - image = np.ones((100, 100), dtype=np.uint16) * 2 ** 12 - elem = np.ones((3, 3), dtype=np.uint8) - out = np.empty_like(image) - mask = np.ones(image.shape, dtype=np.uint8) - assert_raises(ValueError, rank.percentile_mean, image=image, - selem=elem, out=out, mask=mask, shift_x=0, shift_y=0) - - def test_pass_on_bitdepth(): # should pass because data bitdepth is not too high for the function @@ -192,7 +181,7 @@ def test_compare_uint_vs_float(): # dynamic) should be identical # Create signed int8 image that and convert it to uint8 - image_uint = img_as_uint(data.camera()) + image_uint = img_as_uint(data.camera()[:50, :50]) image_float = img_as_float(image_uint) methods = ['autolevel', 'bottomhat', 'equalize', 'gradient', 'threshold', @@ -438,5 +427,17 @@ def test_selem_dtypes(): assert_array_equal(image, out) +def test_16bit(): + image = np.zeros((21, 21), dtype=np.uint16) + selem = np.ones((3, 3), dtype=np.uint8) + + for bitdepth in range(17): + value = 2 ** bitdepth - 1 + image[10, 10] = value + assert rank.minimum(image, selem)[10, 10] == 0 + assert rank.maximum(image, selem)[10, 10] == value + assert rank.mean(image, selem)[10, 10] == value / selem.size + + if __name__ == "__main__": run_module_suite() From 7fa8e704952c506faadb21da2da5195f237cca3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 30 Jun 2013 12:41:42 +0200 Subject: [PATCH 259/736] Fix README for full 16bit support --- skimage/filter/rank/README.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/skimage/filter/rank/README.rst b/skimage/filter/rank/README.rst index cdf8205c..e5c5a9ad 100644 --- a/skimage/filter/rank/README.rst +++ b/skimage/filter/rank/README.rst @@ -23,10 +23,10 @@ size. This implementation gives better results for large structuring elements. The local histogram is updated at each pixel as the structuring element window moves by, i.e. only those pixels entering and leaving the structuring element update the local histogram. The histogram size is 8-bit (256 bins) for 8-bit -images and 2 to 12-bit (up to 4096 bins) for 16-bit images depending on the -maximum value of the image. Pixel values higher than 4095 raise a ValueError. +images and 2 to 16-bit for 16-bit images depending on the maximum value of the +image. -The filter is applied up to the image border, the neighboorhood used is adjusted -accordingly. The user may provide a mask image (same size as input image) where -non zero values are the part of the image participating in the histogram -computation. By default the entire image is filtered. +The filter is applied up to the image border, the neighboorhood used is +adjusted accordingly. The user may provide a mask image (same size as input +image) where non zero values are the part of the image participating in the +histogram computation. By default the entire image is filtered. From 984e542425a1dbb8086e892cb8af1a45a5698648 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 30 Jun 2013 12:42:11 +0200 Subject: [PATCH 260/736] Use missing memoryviews for bilateral kernels --- skimage/filter/rank/bilateral16_cy.pyx | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/skimage/filter/rank/bilateral16_cy.pyx b/skimage/filter/rank/bilateral16_cy.pyx index 63a89453..f5614157 100644 --- a/skimage/filter/rank/bilateral16_cy.pyx +++ b/skimage/filter/rank/bilateral16_cy.pyx @@ -4,7 +4,7 @@ #cython: wraparound=False cimport numpy as cnp -from .core16_cy cimport _core16 +from .core16_cy cimport dtype_t, _core16 # ----------------------------------------------------------------- @@ -12,9 +12,6 @@ from .core16_cy cimport _core16 # ----------------------------------------------------------------- -ctypedef cnp.uint16_t dtype_t - - cdef inline dtype_t kernel_mean(Py_ssize_t* histo, float pop, dtype_t g, Py_ssize_t bitdepth, Py_ssize_t maxbin, Py_ssize_t midbin, @@ -59,10 +56,10 @@ cdef inline dtype_t kernel_pop(Py_ssize_t* histo, float pop, # ----------------------------------------------------------------- -def mean(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[cnp.uint8_t, ndim=2] selem, - cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, +def mean(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask=None, + dtype_t[:, ::1] out=None, char shift_x=0, char shift_y=0, int bitdepth=8, int s0=1, int s1=1): """average greylevel (clipped on uint8) """ @@ -70,10 +67,10 @@ def mean(cnp.ndarray[dtype_t, ndim=2] image, bitdepth, 0., 0., s0, s1) -def pop(cnp.ndarray[dtype_t, ndim=2] image, - cnp.ndarray[cnp.uint8_t, ndim=2] selem, - cnp.ndarray[cnp.uint8_t, ndim=2] mask=None, - cnp.ndarray[dtype_t, ndim=2] out=None, +def pop(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask=None, + dtype_t[:, ::1] out=None, char shift_x=0, char shift_y=0, int bitdepth=8, int s0=1, int s1=1): """returns the number of actual pixels of the structuring element inside the mask From ce792d5e1a564a183baa17454cdbb9f744dc39b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 30 Jun 2013 12:57:05 +0200 Subject: [PATCH 261/736] Improve description of bilateral filter --- skimage/filter/rank/bilateral.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/skimage/filter/rank/bilateral.py b/skimage/filter/rank/bilateral.py index 26784df8..386c88bc 100644 --- a/skimage/filter/rank/bilateral.py +++ b/skimage/filter/rank/bilateral.py @@ -9,7 +9,7 @@ maximum value present in the image. The pixel neighborhood is defined by: * the given structuring element -* an interval [g-s0,g+s1] in greylevel around g the processed pixel greylevel +* an interval [g-s0, g+s1] in greylevel around g the processed pixel greylevel The kernel is flat (i.e. each pixel belonging to the neighborhood contributes equally). @@ -78,7 +78,7 @@ def bilateral_mean(image, selem, out=None, mask=None, shift_x=False, Spatial closeness is measured by considering only the local pixel neighborhood given by a structuring element (selem). - Radiometric similarity is defined by the greylevel interval [g-s0,g+s1] + Radiometric similarity is defined by the greylevel interval [g-s0, g+s1] where g is the current pixel greylevel. Only pixels belonging to the structuring element AND having a greylevel inside this interval are averaged. Return greyscale local bilateral_mean of an image. @@ -99,7 +99,8 @@ def bilateral_mean(image, selem, out=None, mask=None, shift_x=False, to the structuring element sizes (center must be inside the given structuring element). s0, s1 : int - Define the [s0, s1] interval to be considered for computing the value. + Define the [s0, s1] interval around the greyvalue of the center pixel + to be considered for computing the value. Returns ------- @@ -152,7 +153,8 @@ def bilateral_pop(image, selem, out=None, mask=None, shift_x=False, to the structuring element sizes (center must be inside the given structuring element). s0, s1 : int - Define the [s0, s1] interval to be considered for computing the value. + Define the [s0, s1] interval around the greyvalue of the center pixel + to be considered for computing the value. Returns ------- From f74c073da155c404792a906759f01a37046f7941 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 30 Jun 2013 12:57:29 +0200 Subject: [PATCH 262/736] Add test case for bilateral filters --- skimage/filter/rank/tests/test_rank.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/skimage/filter/rank/tests/test_rank.py b/skimage/filter/rank/tests/test_rank.py index ab584776..1efe2bff 100644 --- a/skimage/filter/rank/tests/test_rank.py +++ b/skimage/filter/rank/tests/test_rank.py @@ -356,13 +356,11 @@ def test_otsu(): # test the local Otsu segmentation on a synthetic image # (left to right ramp * sinus) - test = np.tile( - [128, 145, 103, 127, 165, 83, 127, 185, 63, 127, 205, 43, - 127, 225, 23, 127], - (16, 1)) + test = np.tile([128, 145, 103, 127, 165, 83, 127, 185, 63, 127, 205, 43, + 127, 225, 23, 127], + (16, 1)) test = test.astype(np.uint8) - res = np.tile([1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1], - (16, 1)) + res = np.tile([1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1], (16, 1)) selem = np.ones((6, 6), dtype=np.uint8) th = 1 * (test >= rank.otsu(test, selem)) assert_array_equal(th, res) @@ -439,5 +437,19 @@ def test_16bit(): assert rank.mean(image, selem)[10, 10] == value / selem.size +def test_bilateral(): + image = np.zeros((21, 21), dtype=np.uint16) + selem = np.ones((3, 3), dtype=np.uint8) + + image[10, 10] = 1000 + image[10, 11] = 1010 + image[10, 9] = 900 + + assert rank.bilateral_mean(image, selem, s0=1, s1=1)[10, 10] == 1000 + assert rank.bilateral_pop(image, selem, s0=1, s1=1)[10, 10] == 1 + assert rank.bilateral_mean(image, selem, s0=11, s1=11)[10, 10] == 1005 + assert rank.bilateral_pop(image, selem, s0=11, s1=11)[10, 10] == 2 + + if __name__ == "__main__": run_module_suite() From 663092aca0e5ad59aeb211bc27d266cc559772c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 30 Jun 2013 13:02:45 +0200 Subject: [PATCH 263/736] Improve indentation --- skimage/filter/rank/percentile.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/skimage/filter/rank/percentile.py b/skimage/filter/rank/percentile.py index 56c67983..f302074c 100644 --- a/skimage/filter/rank/percentile.py +++ b/skimage/filter/rank/percentile.py @@ -100,10 +100,9 @@ def percentile_autolevel(image, selem, out=None, mask=None, shift_x=False, """ - return _apply( - percentile8_cy.autolevel, percentile16_cy.autolevel, - image, selem, out=out, mask=mask, shift_x=shift_x, - shift_y=shift_y, p0=p0, p1=p1) + return _apply(percentile8_cy.autolevel, percentile16_cy.autolevel, + image, selem, out=out, mask=mask, shift_x=shift_x, + shift_y=shift_y, p0=p0, p1=p1) def percentile_gradient(image, selem, out=None, mask=None, shift_x=False, @@ -221,9 +220,8 @@ def percentile_mean_subtraction(image, selem, out=None, mask=None, shift_y=shift_y, p0=p0, p1=p1) -def percentile_morph_contr_enh( - image, selem, out=None, mask=None, shift_x=False, - shift_y=False, p0=.0, p1=1.): +def percentile_morph_contr_enh(image, selem, out=None, mask=None, + shift_x=False, shift_y=False, p0=.0, p1=1.): """Return greyscale local morph_contr_enh of an image. morph_contr_enh is computed on the given structuring element. Only levels @@ -371,7 +369,6 @@ def percentile_threshold(image, selem, out=None, mask=None, shift_x=False, """ - return _apply( - percentile8_cy.threshold, percentile16_cy.threshold, - image, selem, out=out, mask=mask, shift_x=shift_x, - shift_y=shift_y, p0=p0, p1=0.) + return _apply(percentile8_cy.threshold, percentile16_cy.threshold, + image, selem, out=out, mask=mask, shift_x=shift_x, + shift_y=shift_y, p0=p0, p1=0.) From 6c5eb12df8c040bfcbbe5065de88bcc826016c5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 30 Jun 2013 13:37:13 +0200 Subject: [PATCH 264/736] Cast to int for python 3 --- skimage/filter/rank/core16_cy.pyx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/filter/rank/core16_cy.pyx b/skimage/filter/rank/core16_cy.pyx index 5641d2f1..7de39bf8 100644 --- a/skimage/filter/rank/core16_cy.pyx +++ b/skimage/filter/rank/core16_cy.pyx @@ -59,7 +59,7 @@ cdef void _core16(dtype_t kernel(Py_ssize_t*, float, dtype_t, maxbin_list = [0, 0, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536] - midbin_list = [m / 2 for m in maxbin_list] + midbin_list = [int(m / 2) for m in maxbin_list] # set maxbin and midbin cdef Py_ssize_t maxbin = maxbin_list[bitdepth] From 68461dbf0016b5d479d459f987885c7632b4291b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Mon, 1 Jul 2013 18:52:19 +0200 Subject: [PATCH 265/736] Update bento.info for refactored rank package --- bento.info | 34 ++++++++++++++-------------------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/bento.info b/bento.info index b1366173..fe07dc5d 100644 --- a/bento.info +++ b/bento.info @@ -123,33 +123,27 @@ Library: Extension: skimage._shared.geometry Sources: skimage/_shared/geometry.pyx - Extension: skimage.filter.rank._core16 + Extension: skimage.filter.rank.bilateral16_cy Sources: - skimage/filter/rank/_core16.pyx - Extension: skimage.filter.rank._crank8 + skimage/filter/rank/bilateral16_cy.pyx + Extension: skimage.filter.rank.generic8_cy Sources: - skimage/filter/rank/_crank8.pyx - Extension: skimage.filter.rank._crank16 + skimage/filter/rank/generic8_cy.pyx + Extension: skimage.filter.rank.percentile16_cy Sources: - skimage/filter/rank/_crank16.pyx - Extension: skimage.filter.rank._core8 + skimage/filter/rank/percentile16_cy.pyx + Extension: skimage.filter.rank.core16_cy Sources: - skimage/filter/rank/_core8.pyx - Extension: skimage.filter.rank.bilateral_rank + skimage/filter/rank/core16_cy.pyx + Extension: skimage.filter.rank.core8_cy Sources: - skimage/filter/rank/bilateral_rank.pyx - Extension: skimage.filter.rank._crank16_percentiles + skimage/filter/rank/core8_cy.pyx + Extension: skimage.filter.rank.generic16_cy Sources: - skimage/filter/rank/_crank16_percentiles.pyx - Extension: skimage.filter.rank.percentile_rank + skimage/filter/rank/generic16_cy.pyx + Extension: skimage.filter.rank.percentile8_cy Sources: - skimage/filter/rank/percentile_rank.pyx - Extension: skimage.filter.rank._crank8_percentiles - Sources: - skimage/filter/rank/_crank8_percentiles.pyx - Extension: skimage.filter.rank._crank16_bilateral - Sources: - skimage/filter/rank/_crank16_bilateral.pyx + skimage/filter/rank/percentile8_cy.pyx Executable: skivi Module: skimage.scripts.skivi From 9a36e3b27082d41f07cbe7f88b89d7d51d2478e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Mon, 1 Jul 2013 19:19:20 +0200 Subject: [PATCH 266/736] Log warning when bitdepth > 10 --- skimage/filter/rank/bilateral.py | 6 ++++++ skimage/filter/rank/generic.py | 6 ++++++ skimage/filter/rank/percentile.py | 6 ++++++ 3 files changed, 18 insertions(+) diff --git a/skimage/filter/rank/bilateral.py b/skimage/filter/rank/bilateral.py index 386c88bc..e159d512 100644 --- a/skimage/filter/rank/bilateral.py +++ b/skimage/filter/rank/bilateral.py @@ -27,6 +27,9 @@ References import numpy as np from skimage import img_as_ubyte +from ... import get_log +log = get_log() + from . import bilateral16_cy from .generic import find_bitdepth @@ -60,6 +63,9 @@ def _apply(func8, func16, image, selem, out, mask, shift_x, shift_y, s0, s1): if out is None: out = np.zeros(image.shape, dtype=np.uint16) bitdepth = find_bitdepth(image) + if bitdepth > 10: + log.warn("Bitdepth of %d may result in bad rank filter " + "performance." % bitdepth) func16(image, selem, shift_x=shift_x, shift_y=shift_y, mask=mask, bitdepth=bitdepth + 1, out=out, s0=s0, s1=s1) else: diff --git a/skimage/filter/rank/generic.py b/skimage/filter/rank/generic.py index bcfda1bc..d9b3569b 100644 --- a/skimage/filter/rank/generic.py +++ b/skimage/filter/rank/generic.py @@ -17,6 +17,9 @@ References import numpy as np from skimage import img_as_ubyte, img_as_uint +from ... import get_log +log = get_log() + from . import generic8_cy, generic16_cy @@ -60,6 +63,9 @@ def _apply(func8, func16, image, selem, out, mask, shift_x, shift_y): if out is None: out = np.zeros(image.shape, dtype=np.uint16) bitdepth = find_bitdepth(image) + if bitdepth > 10: + log.warn("Bitdepth of %d may result in bad rank filter " + "performance." % bitdepth) func16(image, selem, shift_x=shift_x, shift_y=shift_y, mask=mask, bitdepth=bitdepth + 1, out=out) diff --git a/skimage/filter/rank/percentile.py b/skimage/filter/rank/percentile.py index f302074c..b75d994f 100644 --- a/skimage/filter/rank/percentile.py +++ b/skimage/filter/rank/percentile.py @@ -23,6 +23,9 @@ References import numpy as np from skimage import img_as_ubyte +from ... import get_log +log = get_log() + from . import percentile8_cy, percentile16_cy from .generic import find_bitdepth @@ -59,6 +62,9 @@ def _apply(func8, func16, image, selem, out, mask, shift_x, shift_y, p0, p1): if out is None: out = np.zeros(image.shape, dtype=np.uint16) bitdepth = find_bitdepth(image) + if bitdepth > 10: + log.warn("Bitdepth of %d may result in bad rank filter " + "performance." % bitdepth) func16(image, selem, shift_x=shift_x, shift_y=shift_y, mask=mask, bitdepth=bitdepth + 1, out=out, p0=p0, p1=p1) else: From 4de4053a9f1727232878c7dc5478d7916f9b5a30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Tue, 2 Jul 2013 00:16:28 +0200 Subject: [PATCH 267/736] Refactor rank filter package as combined implementation for 8- and 16-bit --- skimage/filter/rank/bilateral.py | 93 +-- skimage/filter/rank/bilateral16_cy.pyx | 79 --- skimage/filter/rank/bilateral_cy.pyx | 80 +++ skimage/filter/rank/core16_cy.pxd | 20 - skimage/filter/rank/core16_cy.pyx | 247 -------- skimage/filter/rank/core8_cy.pxd | 25 - skimage/filter/rank/core_cy.pxd | 23 + .../filter/rank/{core8_cy.pyx => core_cy.pyx} | 95 ++- skimage/filter/rank/generic.py | 272 ++++----- skimage/filter/rank/generic16_cy.pyx | 418 ------------- skimage/filter/rank/generic8_cy.pyx | 480 --------------- skimage/filter/rank/generic_cy.pyx | 576 ++++++++++++++++++ skimage/filter/rank/percentile.py | 156 ++--- skimage/filter/rank/percentile16_cy.pyx | 326 ---------- skimage/filter/rank/percentile8_cy.pyx | 291 --------- skimage/filter/rank/percentile_cy.pyx | 325 ++++++++++ skimage/filter/rank/tests/test_rank.py | 20 +- skimage/filter/setup.py | 26 +- 18 files changed, 1279 insertions(+), 2273 deletions(-) delete mode 100644 skimage/filter/rank/bilateral16_cy.pyx create mode 100644 skimage/filter/rank/bilateral_cy.pyx delete mode 100644 skimage/filter/rank/core16_cy.pxd delete mode 100644 skimage/filter/rank/core16_cy.pyx delete mode 100644 skimage/filter/rank/core8_cy.pxd create mode 100644 skimage/filter/rank/core_cy.pxd rename skimage/filter/rank/{core8_cy.pyx => core_cy.pyx} (74%) delete mode 100644 skimage/filter/rank/generic16_cy.pyx delete mode 100644 skimage/filter/rank/generic8_cy.pyx create mode 100644 skimage/filter/rank/generic_cy.pyx delete mode 100644 skimage/filter/rank/percentile16_cy.pyx delete mode 100644 skimage/filter/rank/percentile8_cy.pyx create mode 100644 skimage/filter/rank/percentile_cy.pyx diff --git a/skimage/filter/rank/bilateral.py b/skimage/filter/rank/bilateral.py index e159d512..834a24a8 100644 --- a/skimage/filter/rank/bilateral.py +++ b/skimage/filter/rank/bilateral.py @@ -3,9 +3,6 @@ The local histogram is computed using a sliding window similar to the method described in [1]_. -Input image must be 16-bit, the number of histogram bins is determined from the -maximum value present in the image. - The pixel neighborhood is defined by: * the given structuring element @@ -14,8 +11,6 @@ The pixel neighborhood is defined by: The kernel is flat (i.e. each pixel belonging to the neighborhood contributes equally). -Result image is 16-bit with respect to the input image. - References ---------- @@ -30,46 +25,19 @@ from skimage import img_as_ubyte from ... import get_log log = get_log() -from . import bilateral16_cy -from .generic import find_bitdepth +from . import bilateral_cy +from .generic import _handle_input __all__ = ['bilateral_mean', 'bilateral_pop'] -def _apply(func8, func16, image, selem, out, mask, shift_x, shift_y, s0, s1): - selem = img_as_ubyte(selem > 0) - image = np.ascontiguousarray(image) +def _apply(func, image, selem, out, mask, shift_x, shift_y, s0, s1): - if mask is None: - mask = np.ones(image.shape, dtype=np.uint8) - else: - mask = np.ascontiguousarray(mask) - mask = img_as_ubyte(mask) + image, selem, out, mask, max_bin = _handle_input(image, selem, out, mask) - if image is out: - raise NotImplementedError("Cannot perform rank operation in place.") - - if image.dtype == np.uint8: - if func8 is None: - raise TypeError("Not implemented for uint8 image.") - if out is None: - out = np.zeros(image.shape, dtype=np.uint8) - func8(image, selem, shift_x=shift_x, shift_y=shift_y, - mask=mask, out=out, s0=s0, s1=s1) - elif image.dtype == np.uint16: - if func16 is None: - raise TypeError("Not implemented for uint16 image.") - if out is None: - out = np.zeros(image.shape, dtype=np.uint16) - bitdepth = find_bitdepth(image) - if bitdepth > 10: - log.warn("Bitdepth of %d may result in bad rank filter " - "performance." % bitdepth) - func16(image, selem, shift_x=shift_x, shift_y=shift_y, mask=mask, - bitdepth=bitdepth + 1, out=out, s0=s0, s1=s1) - else: - raise TypeError("Only uint8 and uint16 image supported.") + func(image, selem, shift_x=shift_x, shift_y=shift_y, mask=mask, + out=out, max_bin=max_bin, s0=s0, s1=s1) return out @@ -91,16 +59,16 @@ def bilateral_mean(image, selem, out=None, mask=None, shift_x=False, Parameters ---------- - image : ndarray (uint16) - Input image. + image : ndarray (uint8, uint16) + Image array. selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray + out : ndarray (same dtype as input) If None, a new array will be allocated. - mask : ndarray (uint8) + mask : ndarray Mask array that defines (>0) area of the image included in the local neighborhood. If None, the complete image is used (default). - shift_x, shift_y : (int) + shift_x, shift_y : int Offset added to the structuring element center point. Shift is bounded to the structuring element sizes (center must be inside the given structuring element). @@ -110,17 +78,12 @@ def bilateral_mean(image, selem, out=None, mask=None, shift_x=False, Returns ------- - out : uint16 array + out : ndarray (same dtype as input) The result of the local bilateral mean. See also -------- - skimage.filter.denoise_bilateral() for a gaussian bilateral filter. - - Notes - ----- - - * input image are 16-bit only + skimage.filter.denoise_bilateral for a gaussian bilateral filter. Examples -------- @@ -131,9 +94,10 @@ def bilateral_mean(image, selem, out=None, mask=None, shift_x=False, >>> ima = data.camera().astype(np.uint16) >>> # bilateral filtering of cameraman image using a flat kernel >>> bilat_ima = bilateral_mean(ima, disk(20), s0=10,s1=10) + """ - return _apply(None, bilateral16_cy.mean, image, selem, out=out, + return _apply(bilateral_cy._mean, image, selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y, s0=s0, s1=s1) @@ -145,16 +109,16 @@ def bilateral_pop(image, selem, out=None, mask=None, shift_x=False, Parameters ---------- - image : ndarray (uint16) - Input image. + image : ndarray (uint8, uint16) + Image array. selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray + out : ndarray (same dtype as input) If None, a new array will be allocated. - mask : ndarray (uint8) + mask : ndarray Mask array that defines (>0) area of the image included in the local neighborhood. If None, the complete image is used (default). - shift_x, shift_y : (int) + shift_x, shift_y : int Offset added to the structuring element center point. Shift is bounded to the structuring element sizes (center must be inside the given structuring element). @@ -164,24 +128,19 @@ def bilateral_pop(image, selem, out=None, mask=None, shift_x=False, Returns ------- - out : uint16 array + out : ndarray (same dtype as input) the local number of pixels inside the bilateral neighborhood - Notes - ----- - - * input image are 16-bit only - Examples -------- >>> # Local mean >>> from skimage.morphology import square >>> import skimage.filter.rank as rank >>> ima16 = 255 * np.array([[0, 0, 0, 0, 0], - ... [0, 1, 1, 1, 0], - ... [0, 1, 1, 1, 0], - ... [0, 1, 1, 1, 0], - ... [0, 0, 0, 0, 0]], dtype=np.uint16) + ... [0, 1, 1, 1, 0], + ... [0, 1, 1, 1, 0], + ... [0, 1, 1, 1, 0], + ... [0, 0, 0, 0, 0]], dtype=np.uint16) >>> rank.bilateral_pop(ima16, square(3), s0=10,s1=10) array([[3, 4, 3, 4, 3], [4, 4, 6, 4, 4], @@ -191,5 +150,5 @@ def bilateral_pop(image, selem, out=None, mask=None, shift_x=False, """ - return _apply(None, bilateral16_cy.pop, image, selem, out=out, + return _apply(bilateral_cy._pop, image, selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y, s0=s0, s1=s1) diff --git a/skimage/filter/rank/bilateral16_cy.pyx b/skimage/filter/rank/bilateral16_cy.pyx deleted file mode 100644 index f5614157..00000000 --- a/skimage/filter/rank/bilateral16_cy.pyx +++ /dev/null @@ -1,79 +0,0 @@ -#cython: cdivision=True -#cython: boundscheck=False -#cython: nonecheck=False -#cython: wraparound=False - -cimport numpy as cnp -from .core16_cy cimport dtype_t, _core16 - - -# ----------------------------------------------------------------- -# kernels uint16 take extra parameter for defining the bitdepth -# ----------------------------------------------------------------- - - -cdef inline dtype_t kernel_mean(Py_ssize_t* histo, float pop, - dtype_t g, Py_ssize_t bitdepth, - Py_ssize_t maxbin, Py_ssize_t midbin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - - cdef int i, bilat_pop = 0 - cdef float mean = 0. - - if pop: - for i in range(maxbin): - if (g > (i - s0)) and (g < (i + s1)): - bilat_pop += histo[i] - mean += histo[i] * i - if bilat_pop: - return (mean / bilat_pop) - else: - return (0) - else: - return (0) - - -cdef inline dtype_t kernel_pop(Py_ssize_t* histo, float pop, - dtype_t g, Py_ssize_t bitdepth, - Py_ssize_t maxbin, Py_ssize_t midbin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - - cdef int i, bilat_pop = 0 - - if pop: - for i in range(maxbin): - if (g > (i - s0)) and (g < (i + s1)): - bilat_pop += histo[i] - return (bilat_pop) - else: - return (0) - - -# ----------------------------------------------------------------- -# python wrappers -# ----------------------------------------------------------------- - - -def mean(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask=None, - dtype_t[:, ::1] out=None, - char shift_x=0, char shift_y=0, int bitdepth=8, int s0=1, int s1=1): - """average greylevel (clipped on uint8) - """ - _core16(kernel_mean, image, selem, mask, out, shift_x, shift_y, - bitdepth, 0., 0., s0, s1) - - -def pop(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask=None, - dtype_t[:, ::1] out=None, - char shift_x=0, char shift_y=0, int bitdepth=8, int s0=1, int s1=1): - """returns the number of actual pixels of the structuring element inside - the mask - """ - _core16(kernel_pop, image, selem, mask, out, shift_x, shift_y, - bitdepth, .0, .0, s0, s1) diff --git a/skimage/filter/rank/bilateral_cy.pyx b/skimage/filter/rank/bilateral_cy.pyx new file mode 100644 index 00000000..d93fdb3c --- /dev/null +++ b/skimage/filter/rank/bilateral_cy.pyx @@ -0,0 +1,80 @@ +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False + +cimport numpy as cnp +from libc.math cimport log + +from .core_cy cimport uint8_t, uint16_t, dtype_t, _core + + +cdef inline dtype_t _kernel_mean(Py_ssize_t* histo, float pop, + dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i + cdef Py_ssize_t bilat_pop = 0 + cdef Py_ssize_t mean = 0 + + if pop: + for i in range(max_bin): + if (g > (i - s0)) and (g < (i + s1)): + bilat_pop += histo[i] + mean += histo[i] * i + if bilat_pop: + return (mean / bilat_pop) + else: + return (0) + else: + return (0) + + +cdef inline dtype_t _kernel_pop(Py_ssize_t* histo, float pop, + dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i + cdef Py_ssize_t bilat_pop = 0 + + if pop: + for i in range(max_bin): + if (g > (i - s0)) and (g < (i + s1)): + bilat_pop += histo[i] + return (bilat_pop) + else: + return (0) + + +def _mean(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t[:, ::1] out, + char shift_x, char shift_y, Py_ssize_t s0, Py_ssize_t s1, + Py_ssize_t max_bin): + + if dtype_t is uint8_t: + _core[uint8_t](_kernel_mean[uint8_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, s0, s1, max_bin) + elif dtype_t is uint16_t: + _core[uint16_t](_kernel_mean[uint16_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, s0, s1, max_bin) + + +def _pop(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t[:, ::1] out, + char shift_x, char shift_y, Py_ssize_t s0, Py_ssize_t s1, + Py_ssize_t max_bin): + + if dtype_t is uint8_t: + _core[uint8_t](_kernel_pop[uint8_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, s0, s1, max_bin) + elif dtype_t is uint16_t: + _core[uint16_t](_kernel_pop[uint16_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, s0, s1, max_bin) diff --git a/skimage/filter/rank/core16_cy.pxd b/skimage/filter/rank/core16_cy.pxd deleted file mode 100644 index cebf0aab..00000000 --- a/skimage/filter/rank/core16_cy.pxd +++ /dev/null @@ -1,20 +0,0 @@ -cimport numpy as cnp - - -ctypedef cnp.uint16_t dtype_t - - -cdef dtype_t uint16_max(dtype_t a, dtype_t b) -cdef dtype_t uint16_min(dtype_t a, dtype_t b) - - -# 16-bit core kernel receives extra information about data bitdepth -cdef void _core16(dtype_t kernel(Py_ssize_t*, float, dtype_t, - Py_ssize_t, Py_ssize_t, Py_ssize_t, float, - float, Py_ssize_t, Py_ssize_t), - dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask, - dtype_t[:, ::1] out, - char shift_x, char shift_y, Py_ssize_t bitdepth, - float p0, float p1, Py_ssize_t s0, Py_ssize_t s1) except * diff --git a/skimage/filter/rank/core16_cy.pyx b/skimage/filter/rank/core16_cy.pyx deleted file mode 100644 index 7de39bf8..00000000 --- a/skimage/filter/rank/core16_cy.pyx +++ /dev/null @@ -1,247 +0,0 @@ -#cython: cdivision=True -#cython: boundscheck=False -#cython: nonecheck=False -#cython: wraparound=False - -import numpy as np - -cimport numpy as cnp -from libc.stdlib cimport malloc, free -from .core8_cy cimport is_in_mask - - -cdef inline dtype_t uint16_max(dtype_t a, dtype_t b): - return a if a >= b else b - - -cdef inline dtype_t uint16_min(dtype_t a, dtype_t b): - return a if a <= b else b - - -cdef inline void histogram_increment(Py_ssize_t* histo, float* pop, - dtype_t value): - histo[value] += 1 - pop[0] += 1 - - -cdef inline void histogram_decrement(Py_ssize_t* histo, float* pop, - dtype_t value): - histo[value] -= 1 - pop[0] -= 1 - - -cdef void _core16(dtype_t kernel(Py_ssize_t*, float, dtype_t, - Py_ssize_t, Py_ssize_t, Py_ssize_t, float, - float, Py_ssize_t, Py_ssize_t), - dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask, - dtype_t[:, ::1] out, - char shift_x, char shift_y, Py_ssize_t bitdepth, - float p0, float p1, Py_ssize_t s0, Py_ssize_t s1) except *: - """Compute histogram for each pixel neighborhood, apply kernel function and - use kernel function return value for output image. - """ - - cdef Py_ssize_t rows = image.shape[0] - cdef Py_ssize_t cols = image.shape[1] - cdef Py_ssize_t srows = selem.shape[0] - cdef Py_ssize_t scols = selem.shape[1] - - cdef Py_ssize_t centre_r = int(selem.shape[0] / 2) + shift_y - cdef Py_ssize_t centre_c = int(selem.shape[1] / 2) + shift_x - - # check that structuring element center is inside the element bounding box - assert centre_r >= 0 - assert centre_c >= 0 - assert centre_r < srows - assert centre_c < scols - - maxbin_list = [0, 0, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, - 8192, 16384, 32768, 65536] - midbin_list = [int(m / 2) for m in maxbin_list] - - # set maxbin and midbin - cdef Py_ssize_t maxbin = maxbin_list[bitdepth] - cdef Py_ssize_t midbin = midbin_list[bitdepth] - - # define pointers to the data - cdef char* mask_data = &mask[0, 0] - - # define local variable types - cdef Py_ssize_t r, c, rr, cc, s, value, local_max, i, even_row - # number of pixels actually inside the neighborhood (float) - cdef float pop - - # allocate memory with malloc - cdef Py_ssize_t max_se = srows * scols - - # number of element in each attack border - cdef Py_ssize_t num_se_n, num_se_s, num_se_e, num_se_w - - # the current local histogram distribution - cdef Py_ssize_t* histo = malloc(maxbin * sizeof(Py_ssize_t)) - - # these lists contain the relative pixel row and column for each of the 4 - # attack borders east, west, north and south e.g. se_e_r lists the rows of - # the east structuring element border - cdef Py_ssize_t* se_e_r = malloc(max_se * sizeof(Py_ssize_t)) - cdef Py_ssize_t* se_e_c = malloc(max_se * sizeof(Py_ssize_t)) - cdef Py_ssize_t* se_w_r = malloc(max_se * sizeof(Py_ssize_t)) - cdef Py_ssize_t* se_w_c = malloc(max_se * sizeof(Py_ssize_t)) - cdef Py_ssize_t* se_n_r = malloc(max_se * sizeof(Py_ssize_t)) - cdef Py_ssize_t* se_n_c = malloc(max_se * sizeof(Py_ssize_t)) - cdef Py_ssize_t* se_s_r = malloc(max_se * sizeof(Py_ssize_t)) - cdef Py_ssize_t* se_s_c = malloc(max_se * sizeof(Py_ssize_t)) - - # build attack and release borders by using difference along axis - t = np.hstack((selem, np.zeros((selem.shape[0], 1)))) - cdef char[:, :] t_e = (np.diff(t, axis=1) < 0).view(np.uint8) - - t = np.hstack((np.zeros((selem.shape[0], 1)), selem)) - cdef char[:, :] t_w = (np.diff(t, axis=1) > 0).view(np.uint8) - - t = np.vstack((selem, np.zeros((1, selem.shape[1])))) - cdef char[:, :] t_s = (np.diff(t, axis=0) < 0).view(np.uint8) - - t = np.vstack((np.zeros((1, selem.shape[1])), selem)) - cdef char[:, :] t_n = (np.diff(t, axis=0) > 0).view(np.uint8) - - num_se_n = num_se_s = num_se_e = num_se_w = 0 - - for r in range(srows): - for c in range(scols): - if t_e[r, c]: - se_e_r[num_se_e] = r - centre_r - se_e_c[num_se_e] = c - centre_c - num_se_e += 1 - if t_w[r, c]: - se_w_r[num_se_w] = r - centre_r - se_w_c[num_se_w] = c - centre_c - num_se_w += 1 - if t_n[r, c]: - se_n_r[num_se_n] = r - centre_r - se_n_c[num_se_n] = c - centre_c - num_se_n += 1 - if t_s[r, c]: - se_s_r[num_se_s] = r - centre_r - se_s_c[num_se_s] = c - centre_c - num_se_s += 1 - - # initial population and histogram - for i in range(maxbin): - histo[i] = 0 - - pop = 0 - - for r in range(srows): - for c in range(scols): - rr = r - centre_r - cc = c - centre_c - if selem[r, c]: - if is_in_mask(rows, cols, rr, cc, mask_data): - histogram_increment(histo, &pop, image[rr, cc]) - - r = 0 - c = 0 - # kernel ------------------------------------------- - out[r, c] = kernel(histo, pop, image[r, c], bitdepth, maxbin, midbin, - p0, p1, s0, s1) - # kernel ------------------------------------------- - - # main loop - r = 0 - for even_row in range(0, rows, 2): - # ---> west to east - for c in range(1, cols): - for s in range(num_se_e): - rr = r + se_e_r[s] - cc = c + se_e_c[s] - if is_in_mask(rows, cols, rr, cc, mask_data): - histogram_increment(histo, &pop, image[rr, cc]) - - for s in range(num_se_w): - rr = r + se_w_r[s] - cc = c + se_w_c[s] - 1 - if is_in_mask(rows, cols, rr, cc, mask_data): - histogram_decrement(histo, &pop, image[rr, cc]) - - # kernel ------------------------------------------- - out[r, c] = kernel(histo, pop, image[r, c], bitdepth, maxbin, - midbin, p0, p1, s0, s1) - # kernel ------------------------------------------- - - r += 1 # pass to the next row - if r >= rows: - break - - # ---> north to south - for s in range(num_se_s): - rr = r + se_s_r[s] - cc = c + se_s_c[s] - if is_in_mask(rows, cols, rr, cc, mask_data): - histogram_increment(histo, &pop, image[rr, cc]) - - for s in range(num_se_n): - rr = r + se_n_r[s] - 1 - cc = c + se_n_c[s] - if is_in_mask(rows, cols, rr, cc, mask_data): - histogram_decrement(histo, &pop, image[rr, cc]) - - # kernel ------------------------------------------- - out[r, c] = kernel(histo, pop, image[r, c], - bitdepth, maxbin, midbin, p0, p1, s0, s1) - # kernel ------------------------------------------- - - # ---> east to west - for c in range(cols - 2, -1, -1): - for s in range(num_se_w): - rr = r + se_w_r[s] - cc = c + se_w_c[s] - if is_in_mask(rows, cols, rr, cc, mask_data): - histogram_increment(histo, &pop, image[rr, cc]) - - for s in range(num_se_e): - rr = r + se_e_r[s] - cc = c + se_e_c[s] + 1 - if is_in_mask(rows, cols, rr, cc, mask_data): - histogram_decrement(histo, &pop, image[rr, cc]) - - # kernel ------------------------------------------- - out[r, c] = kernel(histo, pop, image[r, c], bitdepth, maxbin, - midbin, p0, p1, s0, s1) - # kernel ------------------------------------------- - - r += 1 # pass to the next row - if r >= rows: - break - - # ---> north to south - for s in range(num_se_s): - rr = r + se_s_r[s] - cc = c + se_s_c[s] - if is_in_mask(rows, cols, rr, cc, mask_data): - histogram_increment(histo, &pop, image[rr, cc]) - - for s in range(num_se_n): - rr = r + se_n_r[s] - 1 - cc = c + se_n_c[s] - if is_in_mask(rows, cols, rr, cc, mask_data): - histogram_decrement(histo, &pop, image[rr, cc]) - - # kernel ------------------------------------------- - out[r, c] = kernel(histo, pop, image[r, c], bitdepth, maxbin, midbin, - p0, p1, s0, s1) - # kernel ------------------------------------------- - - # release memory allocated by malloc - free(se_e_r) - free(se_e_c) - free(se_w_r) - free(se_w_c) - free(se_n_r) - free(se_n_c) - free(se_s_r) - free(se_s_c) - - free(histo) diff --git a/skimage/filter/rank/core8_cy.pxd b/skimage/filter/rank/core8_cy.pxd deleted file mode 100644 index c2b709d4..00000000 --- a/skimage/filter/rank/core8_cy.pxd +++ /dev/null @@ -1,25 +0,0 @@ -cimport numpy as cnp - - -ctypedef cnp.uint8_t dtype_t - - -cdef dtype_t uint8_max(dtype_t a, dtype_t b) -cdef dtype_t uint8_min(dtype_t a, dtype_t b) - - -cdef dtype_t is_in_mask(Py_ssize_t rows, Py_ssize_t cols, - Py_ssize_t r, Py_ssize_t c, - char* mask) - - -# 8-bit core kernel receives extra information about data inferior and superior -# percentiles -cdef void _core8(dtype_t kernel(Py_ssize_t*, float, dtype_t, float, - float, Py_ssize_t, Py_ssize_t), - dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask, - dtype_t[:, ::1] out, - char shift_x, char shift_y, float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1) except * diff --git a/skimage/filter/rank/core_cy.pxd b/skimage/filter/rank/core_cy.pxd new file mode 100644 index 00000000..544368a0 --- /dev/null +++ b/skimage/filter/rank/core_cy.pxd @@ -0,0 +1,23 @@ +from numpy cimport uint8_t, uint16_t + + +ctypedef fused dtype_t: + uint8_t + uint16_t + + +cdef dtype_t _max(dtype_t a, dtype_t b) +cdef dtype_t _min(dtype_t a, dtype_t b) + + +cdef void _core(dtype_t kernel(Py_ssize_t*, float, dtype_t, + Py_ssize_t, Py_ssize_t, float, + float, Py_ssize_t, Py_ssize_t), + dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t[:, ::1] out, + char shift_x, char shift_y, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1, + Py_ssize_t max_bin) except * diff --git a/skimage/filter/rank/core8_cy.pyx b/skimage/filter/rank/core_cy.pyx similarity index 74% rename from skimage/filter/rank/core8_cy.pyx rename to skimage/filter/rank/core_cy.pyx index e6c0a714..898e1775 100644 --- a/skimage/filter/rank/core8_cy.pyx +++ b/skimage/filter/rank/core_cy.pyx @@ -9,11 +9,11 @@ cimport numpy as cnp from libc.stdlib cimport malloc, free -cdef inline dtype_t uint8_max(dtype_t a, dtype_t b): +cdef inline dtype_t _max(dtype_t a, dtype_t b): return a if a >= b else b -cdef inline dtype_t uint8_min(dtype_t a, dtype_t b): +cdef inline dtype_t _min(dtype_t a, dtype_t b): return a if a <= b else b @@ -29,9 +29,9 @@ cdef inline void histogram_decrement(Py_ssize_t* histo, float* pop, pop[0] -= 1 -cdef inline dtype_t is_in_mask(Py_ssize_t rows, Py_ssize_t cols, - Py_ssize_t r, Py_ssize_t c, - char* mask): +cdef inline char is_in_mask(Py_ssize_t rows, Py_ssize_t cols, + Py_ssize_t r, Py_ssize_t c, + char* mask): """Check whether given coordinate is within image and mask is true.""" if r < 0 or r > rows - 1 or c < 0 or c > cols - 1: return 0 @@ -42,14 +42,17 @@ cdef inline dtype_t is_in_mask(Py_ssize_t rows, Py_ssize_t cols, return 0 -cdef void _core8(dtype_t kernel(Py_ssize_t*, float, dtype_t, float, - float, Py_ssize_t, Py_ssize_t), - dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask, - dtype_t[:, ::1] out, - char shift_x, char shift_y, float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1) except *: +cdef void _core(dtype_t kernel(Py_ssize_t*, float, dtype_t, + Py_ssize_t, Py_ssize_t, float, + float, Py_ssize_t, Py_ssize_t), + dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t[:, ::1] out, + char shift_x, char shift_y, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1, + Py_ssize_t max_bin) except *: """Compute histogram for each pixel neighborhood, apply kernel function and use kernel function return value for output image. """ @@ -59,8 +62,8 @@ cdef void _core8(dtype_t kernel(Py_ssize_t*, float, dtype_t, float, cdef Py_ssize_t srows = selem.shape[0] cdef Py_ssize_t scols = selem.shape[1] - cdef Py_ssize_t centre_r = int(selem.shape[0] / 2) + shift_y - cdef Py_ssize_t centre_c = int(selem.shape[1] / 2) + shift_x + cdef Py_ssize_t centre_r = (selem.shape[0] / 2) + shift_y + cdef Py_ssize_t centre_c = (selem.shape[1] / 2) + shift_x # check that structuring element center is inside the element bounding box assert centre_r >= 0 @@ -68,26 +71,35 @@ cdef void _core8(dtype_t kernel(Py_ssize_t*, float, dtype_t, float, assert centre_r < srows assert centre_c < scols + # add 1 to ensure maximum value is included in histogram -> range(max_bin) + max_bin += 1 + + cdef Py_ssize_t mid_bin = max_bin / 2 + + # define pointers to the data cdef char* mask_data = &mask[0, 0] # define local variable types cdef Py_ssize_t r, c, rr, cc, s, value, local_max, i, even_row # number of pixels actually inside the neighborhood (float) - cdef float pop - - # allocate memory with malloc - cdef Py_ssize_t max_se = srows * scols - - # number of element in each attack border - cdef Py_ssize_t num_se_n, num_se_s, num_se_e, num_se_w + cdef float pop = 0 # the current local histogram distribution - cdef Py_ssize_t* histo = malloc(256 * sizeof(Py_ssize_t)) + cdef Py_ssize_t* histo = malloc(max_bin * sizeof(Py_ssize_t)) + for i in range(max_bin): + histo[i] = 0 # these lists contain the relative pixel row and column for each of the 4 # attack borders east, west, north and south e.g. se_e_r lists the rows of # the east structuring element border + + cdef Py_ssize_t max_se = srows * scols + + # number of element in each attack border + cdef Py_ssize_t num_se_n, num_se_s, num_se_e, num_se_w + num_se_n = num_se_s = num_se_e = num_se_w = 0 + cdef Py_ssize_t* se_e_r = malloc(max_se * sizeof(Py_ssize_t)) cdef Py_ssize_t* se_e_c = malloc(max_se * sizeof(Py_ssize_t)) cdef Py_ssize_t* se_w_r = malloc(max_se * sizeof(Py_ssize_t)) @@ -110,8 +122,6 @@ cdef void _core8(dtype_t kernel(Py_ssize_t*, float, dtype_t, float, t = np.vstack((np.zeros((1, selem.shape[1])), selem)) cdef char[:, :] t_n = (np.diff(t, axis=0) > 0).view(np.uint8) - num_se_n = num_se_s = num_se_e = num_se_w = 0 - for r in range(srows): for c in range(scols): if t_e[r, c]: @@ -131,13 +141,6 @@ cdef void _core8(dtype_t kernel(Py_ssize_t*, float, dtype_t, float, se_s_c[num_se_s] = c - centre_c num_se_s += 1 - # initial population and histogram (kernel is centered on the first row and - # column) - for i in range(256): - histo[i] = 0 - - pop = 0 - for r in range(srows): for c in range(scols): rr = r - centre_r @@ -148,13 +151,13 @@ cdef void _core8(dtype_t kernel(Py_ssize_t*, float, dtype_t, float, r = 0 c = 0 - # kernel ------------------------------------------------------------------ - out[r, c] = kernel(histo, pop, image[r, c], p0, p1, s0, s1) - # kernel ------------------------------------------------------------------ + out[r, c] = kernel(histo, pop, image[r, c], max_bin, mid_bin, + p0, p1, s0, s1) # main loop r = 0 for even_row in range(0, rows, 2): + # ---> west to east for c in range(1, cols): for s in range(num_se_e): @@ -169,9 +172,8 @@ cdef void _core8(dtype_t kernel(Py_ssize_t*, float, dtype_t, float, if is_in_mask(rows, cols, rr, cc, mask_data): histogram_decrement(histo, &pop, image[rr, cc]) - # kernel ---------------------------------------------------------- - out[r, c] = kernel(histo, pop, image[r, c], p0, p1, s0, s1) - # kernel ---------------------------------------------------------- + out[r, c] = kernel(histo, pop, image[r, c], max_bin, + mid_bin, p0, p1, s0, s1) r += 1 # pass to the next row if r >= rows: @@ -190,9 +192,8 @@ cdef void _core8(dtype_t kernel(Py_ssize_t*, float, dtype_t, float, if is_in_mask(rows, cols, rr, cc, mask_data): histogram_decrement(histo, &pop, image[rr, cc]) - # kernel -------------------------------------------------------------- - out[r, c] = kernel(histo, pop, image[r, c], p0, p1, s0, s1) - # kernel -------------------------------------------------------------- + out[r, c] = kernel(histo, pop, image[r, c], + max_bin, mid_bin, p0, p1, s0, s1) # ---> east to west for c in range(cols - 2, -1, -1): @@ -208,10 +209,8 @@ cdef void _core8(dtype_t kernel(Py_ssize_t*, float, dtype_t, float, if is_in_mask(rows, cols, rr, cc, mask_data): histogram_decrement(histo, &pop, image[rr, cc]) - # kernel ---------------------------------------------------------- - out[r, c] = kernel( - histo, pop, image[r, c], p0, p1, s0, s1) - # kernel ---------------------------------------------------------- + out[r, c] = kernel(histo, pop, image[r, c], max_bin, + mid_bin, p0, p1, s0, s1) r += 1 # pass to the next row if r >= rows: @@ -230,9 +229,8 @@ cdef void _core8(dtype_t kernel(Py_ssize_t*, float, dtype_t, float, if is_in_mask(rows, cols, rr, cc, mask_data): histogram_decrement(histo, &pop, image[rr, cc]) - # kernel -------------------------------------------------------------- - out[r, c] = kernel(histo, pop, image[r, c], p0, p1, s0, s1) - # kernel -------------------------------------------------------------- + out[r, c] = kernel(histo, pop, image[r, c], max_bin, mid_bin, + p0, p1, s0, s1) # release memory allocated by malloc free(se_e_r) @@ -243,5 +241,4 @@ cdef void _core8(dtype_t kernel(Py_ssize_t*, float, dtype_t, float, free(se_n_c) free(se_s_r) free(se_s_c) - free(histo) diff --git a/skimage/filter/rank/generic.py b/skimage/filter/rank/generic.py index d9b3569b..e09bfba5 100644 --- a/skimage/filter/rank/generic.py +++ b/skimage/filter/rank/generic.py @@ -20,7 +20,7 @@ from skimage import img_as_ubyte, img_as_uint from ... import get_log log = get_log() -from . import generic8_cy, generic16_cy +from . import generic_cy __all__ = ['autolevel', 'bottomhat', 'equalize', 'gradient', 'maximum', 'mean', @@ -28,56 +28,43 @@ __all__ = ['autolevel', 'bottomhat', 'equalize', 'gradient', 'maximum', 'mean', 'pop', 'threshold', 'tophat', 'noise_filter', 'entropy', 'otsu'] -import numpy as np +def _handle_input(image, selem, out, mask): + if image.dtype not in (np.uint8, np.uint16): + image = img_as_ubyte(image) -def find_bitdepth(image): - """returns the max bith depth of a uint16 image - """ - umax = np.max(image) - if umax > 2: - return int(np.log2(umax)) - else: - return 1 - - -def _apply(func8, func16, image, selem, out, mask, shift_x, shift_y): - selem = img_as_ubyte(selem > 0) + selem = np.ascontiguousarray(img_as_ubyte(selem > 0)) image = np.ascontiguousarray(image) if mask is None: mask = np.ones(image.shape, dtype=np.uint8) else: - mask = np.ascontiguousarray(mask) mask = img_as_ubyte(mask) + mask = np.ascontiguousarray(mask) + + if out is None: + out = np.empty_like(image, dtype=image.dtype) if image is out: raise NotImplementedError("Cannot perform rank operation in place.") is_8bit = image.dtype in (np.uint8, np.int8) - if func8 is not None and (is_8bit or func16 is None): - out = _apply8(func8, image, selem, out, mask, shift_x, shift_y) + if is_8bit: + max_bin = 255 else: - image = img_as_uint(image) - if out is None: - out = np.zeros(image.shape, dtype=np.uint16) - bitdepth = find_bitdepth(image) - if bitdepth > 10: - log.warn("Bitdepth of %d may result in bad rank filter " - "performance." % bitdepth) - func16(image, selem, shift_x=shift_x, shift_y=shift_y, mask=mask, - bitdepth=bitdepth + 1, out=out) + max_bin = max(4, image.max()) - return out + return image, selem, out, mask, max_bin -def _apply8(func8, image, selem, out, mask, shift_x, shift_y): - if out is None: - out = np.zeros(image.shape, dtype=np.uint8) - image = img_as_ubyte(image) - func8(image, selem, shift_x=shift_x, shift_y=shift_y, - mask=mask, out=out) +def _apply(func, image, selem, out, mask, shift_x, shift_y): + + image, selem, out, mask, max_bin = _handle_input(image, selem, out, mask) + + func(image, selem, shift_x=shift_x, shift_y=shift_y, mask=mask, + out=out, max_bin=max_bin) + return out @@ -86,13 +73,13 @@ def autolevel(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Parameters ---------- - image : ndarray - Image array (uint8 array or uint16). + image : ndarray (uint8, uint16) + Image array. selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray + out : ndarray (same dtype as input) If None, a new array will be allocated. - mask : ndarray (uint8) + mask : ndarray Mask array that defines (>0) area of the image included in the local neighborhood. If None, the complete image is used (default). shift_x, shift_y : int @@ -102,7 +89,7 @@ def autolevel(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Returns ------- - out : uint8 array or uint16 array (same as input image) + out : ndarray (same dtype as input image) The result of the local autolevel. Examples @@ -117,7 +104,7 @@ def autolevel(image, selem, out=None, mask=None, shift_x=False, shift_y=False): """ - return _apply(generic8_cy.autolevel, generic16_cy.autolevel, image, selem, + return _apply(generic_cy._autolevel, image, selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) @@ -126,13 +113,13 @@ def bottomhat(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Parameters ---------- - image : ndarray - Image array (uint8 array or uint16). + image : ndarray (uint8, uint16) + Image array. selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray + out : ndarray (same dtype as input) If None, a new array will be allocated. - mask : ndarray (uint8) + mask : ndarray Mask array that defines (>0) area of the image included in the local neighborhood. If None, the complete image is used (default). shift_x, shift_y : int @@ -142,12 +129,12 @@ def bottomhat(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Returns ------- - local bottomhat : uint8 array or uint16 array depending on input image + bottomhat : ndarray (same dtype as input image) The result of the local bottomhat. """ - return _apply(generic8_cy.bottomhat, generic16_cy.bottomhat, image, selem, + return _apply(generic_cy._bottomhat, image, selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) @@ -156,13 +143,13 @@ def equalize(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Parameters ---------- - image : ndarray - Image array (uint8 array or uint16). + image : ndarray (uint8, uint16) + Image array. selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray + out : ndarray (same dtype as input) If None, a new array will be allocated. - mask : ndarray (uint8) + mask : ndarray Mask array that defines (>0) area of the image included in the local neighborhood. If None, the complete image is used (default). shift_x, shift_y : int @@ -172,7 +159,7 @@ def equalize(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Returns ------- - out : uint8 array or uint16 array (same as input image) + out : ndarray (same dtype as input image) The result of the local equalize. Examples @@ -187,7 +174,7 @@ def equalize(image, selem, out=None, mask=None, shift_x=False, shift_y=False): """ - return _apply(generic8_cy.equalize, generic16_cy.equalize, image, selem, + return _apply(generic_cy._equalize, image, selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) @@ -198,13 +185,13 @@ def gradient(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Parameters ---------- - image : ndarray - Image array (uint8 array or uint16). + image : ndarray (uint8, uint16) + Image array. selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray + out : ndarray (same dtype as input) If None, a new array will be allocated. - mask : ndarray (uint8) + mask : ndarray Mask array that defines (>0) area of the image included in the local neighborhood. If None, the complete image is used (default). shift_x, shift_y : int @@ -214,12 +201,12 @@ def gradient(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Returns ------- - out : uint8 array or uint16 array (same as input image) + out : ndarray (same dtype as input image) The local gradient. """ - return _apply(generic8_cy.gradient, generic16_cy.gradient, image, selem, + return _apply(generic_cy._gradient, image, selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) @@ -229,13 +216,13 @@ def maximum(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Parameters ---------- - image : ndarray - Image array (uint8 array or uint16). + image : ndarray (uint8, uint16) + Image array. selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray + out : ndarray (same dtype as input) If None, a new array will be allocated. - mask : ndarray (uint8) + mask : ndarray Mask array that defines (>0) area of the image included in the local neighborhood. If None, the complete image is used (default). shift_x, shift_y : int @@ -245,7 +232,7 @@ def maximum(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Returns ------- - out : uint8 array or uint16 array (same as input image) + out : ndarray (same dtype as input image) The local maximum. See also @@ -254,13 +241,12 @@ def maximum(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Note ---- - * input image can be 8-bit or 16-bit with a value < 4096 (i.e. 12 bit) * the lower algorithm complexity makes the rank.maximum() more efficient for larger images and structuring elements """ - return _apply(generic8_cy.maximum, generic16_cy.maximum, image, selem, + return _apply(generic_cy._maximum, image, selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) @@ -269,13 +255,13 @@ def mean(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Parameters ---------- - image : ndarray - Image array (uint8 array or uint16). + image : ndarray (uint8, uint16) + Image array. selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray + out : ndarray (same dtype as input) If None, a new array will be allocated. - mask : ndarray (uint8) + mask : ndarray Mask array that defines (>0) area of the image included in the local neighborhood. If None, the complete image is used (default). shift_x, shift_y : int @@ -285,7 +271,7 @@ def mean(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Returns ------- - out : uint8 array or uint16 array (same as input image) + out : ndarray (same dtype as input image) The local mean. Examples @@ -300,7 +286,7 @@ def mean(image, selem, out=None, mask=None, shift_x=False, shift_y=False): """ - return _apply(generic8_cy.mean, generic16_cy.mean, image, selem, out=out, + return _apply(generic_cy._mean, image, selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) @@ -310,13 +296,13 @@ def meansubtraction(image, selem, out=None, mask=None, shift_x=False, Parameters ---------- - image : ndarray - Image array (uint8 array or uint16). + image : ndarray (uint8, uint16) + Image array. selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray + out : ndarray (same dtype as input) If None, a new array will be allocated. - mask : ndarray (uint8) + mask : ndarray Mask array that defines (>0) area of the image included in the local neighborhood. If None, the complete image is used (default). shift_x, shift_y : int @@ -326,14 +312,13 @@ def meansubtraction(image, selem, out=None, mask=None, shift_x=False, Returns ------- - out : uint8 array or uint16 array (same as input image) + out : ndarray (same dtype as input image) The result of the local meansubtraction. """ - return _apply(generic8_cy.meansubtraction, generic16_cy.meansubtraction, - image, selem, out=out, mask=mask, shift_x=shift_x, - shift_y=shift_y) + return _apply(generic_cy._meansubtraction, image, selem, + out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) def median(image, selem, out=None, mask=None, shift_x=False, shift_y=False): @@ -341,13 +326,13 @@ def median(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Parameters ---------- - image : ndarray - Image array (uint8 array or uint16). + image : ndarray (uint8, uint16) + Image array. selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray + out : ndarray (same dtype as input) If None, a new array will be allocated. - mask : ndarray (uint8) + mask : ndarray Mask array that defines (>0) area of the image included in the local neighborhood. If None, the complete image is used (default). shift_x, shift_y : int @@ -357,7 +342,7 @@ def median(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Returns ------- - out : uint8 array or uint16 array (same as input image) + out : ndarray (same dtype as input image) The local median. Examples @@ -372,7 +357,7 @@ def median(image, selem, out=None, mask=None, shift_x=False, shift_y=False): """ - return _apply(generic8_cy.median, generic16_cy.median, image, selem, + return _apply(generic_cy._median, image, selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) @@ -381,13 +366,13 @@ def minimum(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Parameters ---------- - image : ndarray - Image array (uint8 array or uint16). + image : ndarray (uint8, uint16) + Image array. selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray + out : ndarray (same dtype as input) If None, a new array will be allocated. - mask : ndarray (uint8) + mask : ndarray Mask array that defines (>0) area of the image included in the local neighborhood. If None, the complete image is used (default). shift_x, shift_y : int @@ -397,7 +382,7 @@ def minimum(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Returns ------- - out : uint8 array or uint16 array (same as input image) + out : ndarray (same dtype as input image) The local minimum. See also @@ -406,13 +391,12 @@ def minimum(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Note ---- - * input image can be 8-bit or 16-bit with a value < 4096 (i.e. 12 bit) * the lower algorithm complexity makes the rank.minimum() more efficient for larger images and structuring elements """ - return _apply(generic8_cy.minimum, generic16_cy.minimum, image, selem, + return _apply(generic_cy._minimum, image, selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) @@ -421,13 +405,13 @@ def modal(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Parameters ---------- - image : ndarray - Image array (uint8 array or uint16). + image : ndarray (uint8, uint16) + Image array. selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray + out : ndarray (same dtype as input) If None, a new array will be allocated. - mask : ndarray (uint8) + mask : ndarray Mask array that defines (>0) area of the image included in the local neighborhood. If None, the complete image is used (default). shift_x, shift_y : int @@ -437,12 +421,12 @@ def modal(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Returns ------- - out : uint8 array or uint16 array (same as input image) + out : ndarray (same dtype as input image) The local modal. """ - return _apply(generic8_cy.modal, generic16_cy.modal, image, selem, + return _apply(generic_cy._modal, image, selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) @@ -454,13 +438,13 @@ def morph_contr_enh(image, selem, out=None, mask=None, shift_x=False, Parameters ---------- - image : ndarray - Image array (uint8 array or uint16). + image : ndarray (uint8, uint16) + Image array. selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray + out : ndarray (same dtype as input) If None, a new array will be allocated. - mask : ndarray (uint8) + mask : ndarray Mask array that defines (>0) area of the image included in the local neighborhood. If None, the complete image is used (default). shift_x, shift_y : int @@ -470,7 +454,7 @@ def morph_contr_enh(image, selem, out=None, mask=None, shift_x=False, Returns ------- - out : uint8 array or uint16 array (same as input image) + out : ndarray (same dtype as input image) The result of the local morph_contr_enh. Examples @@ -485,9 +469,8 @@ def morph_contr_enh(image, selem, out=None, mask=None, shift_x=False, """ - return _apply(generic8_cy.morph_contr_enh, generic16_cy.morph_contr_enh, - image, selem, out=out, mask=mask, shift_x=shift_x, - shift_y=shift_y) + return _apply(generic_cy._morph_contr_enh, image, selem, + out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) def pop(image, selem, out=None, mask=None, shift_x=False, shift_y=False): @@ -496,13 +479,13 @@ def pop(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Parameters ---------- - image : ndarray - Image array (uint8 array or uint16). + image : ndarray (uint8, uint16) + Image array. selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray + out : ndarray (same dtype as input) If None, a new array will be allocated. - mask : ndarray (uint8) + mask : ndarray Mask array that defines (>0) area of the image included in the local neighborhood. If None, the complete image is used (default). shift_x, shift_y : int @@ -512,7 +495,7 @@ def pop(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Returns ------- - out : uint8 array or uint16 array (same as input image) + out : ndarray (same dtype as input image) The number of pixels belonging to the neighborhood. Examples @@ -534,7 +517,7 @@ def pop(image, selem, out=None, mask=None, shift_x=False, shift_y=False): """ - return _apply(generic8_cy.pop, generic16_cy.pop, image, selem, out=out, + return _apply(generic_cy._pop, image, selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) @@ -543,13 +526,13 @@ def threshold(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Parameters ---------- - image : ndarray - Image array (uint8 array or uint16). + image : ndarray (uint8, uint16) + Image array. selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray + out : ndarray (same dtype as input) If None, a new array will be allocated. - mask : ndarray (uint8) + mask : ndarray Mask array that defines (>0) area of the image included in the local neighborhood. If None, the complete image is used (default). shift_x, shift_y : int @@ -559,7 +542,7 @@ def threshold(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Returns ------- - out : uint8 array or uint16 array (same as input image) + out : ndarray (same dtype as input image) The result of the local threshold. Examples @@ -581,7 +564,7 @@ def threshold(image, selem, out=None, mask=None, shift_x=False, shift_y=False): """ - return _apply(generic8_cy.threshold, generic16_cy.threshold, image, selem, + return _apply(generic_cy._threshold, image, selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) @@ -590,13 +573,13 @@ def tophat(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Parameters ---------- - image : ndarray - Image array (uint8 array or uint16). + image : ndarray (uint8, uint16) + Image array. selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray + out : ndarray (same dtype as input) If None, a new array will be allocated. - mask : ndarray (uint8) + mask : ndarray Mask array that defines (>0) area of the image included in the local neighborhood. If None, the complete image is used (default). shift_x, shift_y : int @@ -606,12 +589,12 @@ def tophat(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Returns ------- - out : uint8 array or uint16 array (same as input image) + out : ndarray (same dtype as input image) The image tophat. """ - return _apply(generic8_cy.tophat, generic16_cy.tophat, image, selem, + return _apply(generic_cy._tophat, image, selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) @@ -621,13 +604,13 @@ def noise_filter(image, selem, out=None, mask=None, shift_x=False, Parameters ---------- - image : ndarray - Image array (uint8 array or uint16). + image : ndarray (uint8, uint16) + Image array. selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray + out : ndarray (same dtype as input) If None, a new array will be allocated. - mask : ndarray (uint8) + mask : ndarray Mask array that defines (>0) area of the image included in the local neighborhood. If None, the complete image is used (default). shift_x, shift_y : int @@ -642,7 +625,7 @@ def noise_filter(image, selem, out=None, mask=None, shift_x=False, Returns ------- - out : uint8 array or uint16 array (same as input image) + out : ndarray (same dtype as input image) The image noise. """ @@ -654,7 +637,7 @@ def noise_filter(image, selem, out=None, mask=None, shift_x=False, selem_cpy = selem.copy() selem_cpy[centre_r, centre_c] = 0 - return _apply(generic8_cy.noise_filter, None, image, selem_cpy, out=out, + return _apply(generic_cy._noise_filter, image, selem_cpy, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) @@ -665,13 +648,13 @@ def entropy(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Parameters ---------- - image : ndarray - Image array (uint8 array or uint16). + image : ndarray (uint8, uint16) + Image array. selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray + out : ndarray (same dtype as input) If None, a new array will be allocated. - mask : ndarray (uint8) + mask : ndarray Mask array that defines (>0) area of the image included in the local neighborhood. If None, the complete image is used (default). shift_x, shift_y : int @@ -681,8 +664,8 @@ def entropy(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Returns ------- - out : uint8 array or uint16 array (same as input image) - Entropy x10 (uint8 images) and entropy x1000 (uint16 images) + out : ndarray (same dtype as input image) + Entropy of image. References ---------- @@ -694,17 +677,12 @@ def entropy(image, selem, out=None, mask=None, shift_x=False, shift_y=False): >>> from skimage import data >>> from skimage.filter.rank import entropy >>> from skimage.morphology import disk - >>> # defining a 8- and a 16-bit test images >>> a8 = data.camera() - >>> a16 = data.camera().astype(np.uint16) * 4 - >>> # pixel values contain 10x the local entropy >>> ent8 = entropy(a8, disk(5)) - >>> # pixel values contain 1000x the local entropy - >>> ent16 = entropy(a16, disk(5)) """ - return _apply(generic8_cy.entropy, generic16_cy.entropy, image, selem, + return _apply(generic_cy._entropy, image, selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) @@ -719,7 +697,7 @@ def otsu(image, selem, out=None, mask=None, shift_x=False, shift_y=False): The neighborhood expressed as a 2-D array of 1's and 0's. out : ndarray If None, a new array will be allocated. - mask : ndarray (uint8) + mask : ndarray Mask array that defines (>0) area of the image included in the local neighborhood. If None, the complete image is used (default). shift_x, shift_y : int @@ -729,17 +707,13 @@ def otsu(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Returns ------- - out : uint8 array - Otsu's threshold values + out : ndarray (same dtype as input image) + Otsu's threshold values. References ---------- .. [otsu] http://en.wikipedia.org/wiki/Otsu's_method - Notes - ----- - * input image are 8-bit only - Examples -------- >>> # Local entropy @@ -753,5 +727,5 @@ def otsu(image, selem, out=None, mask=None, shift_x=False, shift_y=False): """ - return _apply(generic8_cy.otsu, None, image, selem, out=out, + return _apply(generic_cy._otsu, image, selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) diff --git a/skimage/filter/rank/generic16_cy.pyx b/skimage/filter/rank/generic16_cy.pyx deleted file mode 100644 index 32021e78..00000000 --- a/skimage/filter/rank/generic16_cy.pyx +++ /dev/null @@ -1,418 +0,0 @@ -#cython: cdivision=True -#cython: boundscheck=False -#cython: nonecheck=False -#cython: wraparound=False - -cimport numpy as cnp -from libc.math cimport log -from .core16_cy cimport dtype_t, _core16 - - -# ----------------------------------------------------------------- -# kernels uint16 take extra parameter for defining the bitdepth -# ----------------------------------------------------------------- - -cdef inline dtype_t kernel_autolevel(Py_ssize_t* histo, float pop, - dtype_t g, Py_ssize_t bitdepth, - Py_ssize_t maxbin, Py_ssize_t midbin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - cdef Py_ssize_t i, imin, imax, delta - - if pop: - for i in range(maxbin - 1, -1, -1): - if histo[i]: - imax = i - break - for i in range(maxbin): - if histo[i]: - imin = i - break - delta = imax - imin - if delta > 0: - return (1. * (maxbin - 1) * (g - imin) / delta) - else: - return (imax - imin) - - -cdef inline dtype_t kernel_bottomhat(Py_ssize_t* histo, float pop, - dtype_t g, Py_ssize_t bitdepth, - Py_ssize_t maxbin, Py_ssize_t midbin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - cdef Py_ssize_t i - - if pop: - for i in range(maxbin): - if histo[i]: - break - - return (g - i) - else: - return (0) - -cdef inline dtype_t kernel_equalize(Py_ssize_t* histo, float pop, - dtype_t g, Py_ssize_t bitdepth, - Py_ssize_t maxbin, Py_ssize_t midbin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - cdef Py_ssize_t i - cdef float sum = 0. - - if pop: - for i in range(maxbin): - sum += histo[i] - if i >= g: - break - - return (((maxbin - 1) * sum) / pop) - else: - return (0) - - -cdef inline dtype_t kernel_gradient(Py_ssize_t* histo, float pop, - dtype_t g, Py_ssize_t bitdepth, - Py_ssize_t maxbin, Py_ssize_t midbin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - cdef Py_ssize_t i, imin, imax - - if pop: - for i in range(maxbin - 1, -1, -1): - if histo[i]: - imax = i - break - for i in range(maxbin): - if histo[i]: - imin = i - break - return (imax - imin) - else: - return (0) - - -cdef inline dtype_t kernel_maximum(Py_ssize_t* histo, float pop, - dtype_t g, Py_ssize_t bitdepth, - Py_ssize_t maxbin, Py_ssize_t midbin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - cdef Py_ssize_t i - - if pop: - for i in range(maxbin - 1, -1, -1): - if histo[i]: - return (i) - - return (0) - - -cdef inline dtype_t kernel_mean(Py_ssize_t* histo, float pop, - dtype_t g, Py_ssize_t bitdepth, - Py_ssize_t maxbin, Py_ssize_t midbin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - cdef Py_ssize_t i - cdef float mean = 0. - - if pop: - for i in range(maxbin): - mean += histo[i] * i - return (mean / pop) - else: - return (0) - - -cdef inline dtype_t kernel_meansubtraction(Py_ssize_t* histo, - float pop, - dtype_t g, - Py_ssize_t bitdepth, - Py_ssize_t maxbin, - Py_ssize_t midbin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - cdef Py_ssize_t i - cdef float mean = 0. - - if pop: - for i in range(maxbin): - mean += histo[i] * i - return ((g - mean / pop) / 2. + (midbin - 1)) - else: - return (0) - - -cdef inline dtype_t kernel_median(Py_ssize_t* histo, float pop, - dtype_t g, Py_ssize_t bitdepth, - Py_ssize_t maxbin, Py_ssize_t midbin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - cdef Py_ssize_t i - cdef float sum = pop / 2.0 - - if pop: - for i in range(maxbin): - if histo[i]: - sum -= histo[i] - if sum < 0: - return (i) - else: - return (0) - - -cdef inline dtype_t kernel_minimum(Py_ssize_t* histo, float pop, - dtype_t g, Py_ssize_t bitdepth, - Py_ssize_t maxbin, Py_ssize_t midbin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - cdef Py_ssize_t i - - if pop: - for i in range(maxbin): - if histo[i]: - return (i) - else: - return (0) - - -cdef inline dtype_t kernel_modal(Py_ssize_t* histo, float pop, - dtype_t g, Py_ssize_t bitdepth, - Py_ssize_t maxbin, Py_ssize_t midbin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - cdef Py_ssize_t hmax = 0, imax = 0 - - if pop: - for i in range(maxbin): - if histo[i] > hmax: - hmax = histo[i] - imax = i - return (imax) - else: - return (0) - - -cdef inline dtype_t kernel_morph_contr_enh(Py_ssize_t* histo, - float pop, - dtype_t g, - Py_ssize_t bitdepth, - Py_ssize_t maxbin, - Py_ssize_t midbin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - cdef Py_ssize_t i, imin, imax - - if pop: - for i in range(maxbin - 1, -1, -1): - if histo[i]: - imax = i - break - for i in range(maxbin): - if histo[i]: - imin = i - break - if imax - g < g - imin: - return (imax) - else: - return (imin) - else: - return (0) - - -cdef inline dtype_t kernel_pop(Py_ssize_t* histo, float pop, - dtype_t g, Py_ssize_t bitdepth, - Py_ssize_t maxbin, Py_ssize_t midbin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - return (pop) - - -cdef inline dtype_t kernel_threshold(Py_ssize_t* histo, float pop, - dtype_t g, Py_ssize_t bitdepth, - Py_ssize_t maxbin, Py_ssize_t midbin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - cdef Py_ssize_t i - cdef float mean = 0. - - if pop: - for i in range(maxbin): - mean += histo[i] * i - return (g > (mean / pop)) - else: - return (0) - - -cdef inline dtype_t kernel_tophat(Py_ssize_t* histo, float pop, - dtype_t g, Py_ssize_t bitdepth, - Py_ssize_t maxbin, Py_ssize_t midbin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - cdef Py_ssize_t i - - if pop: - for i in range(maxbin - 1, -1, -1): - if histo[i]: - break - - return (i - g) - else: - return (0) - -cdef inline dtype_t kernel_entropy(Py_ssize_t* histo, float pop, - dtype_t g, Py_ssize_t bitdepth, - Py_ssize_t maxbin, Py_ssize_t midbin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - cdef Py_ssize_t i - cdef float e, p - - if pop: - e = 0. - - for i in range(maxbin): - p = histo[i] / pop - if p > 0: - e -= p * log(p) / 0.6931471805599453 - - return e * 1000 - else: - return (0) - -# ----------------------------------------------------------------- -# python wrappers -# ----------------------------------------------------------------- - - -def autolevel(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask=None, - dtype_t[:, ::1] out=None, - char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): - _core16(kernel_autolevel, image, selem, mask, out, shift_x, shift_y, - bitdepth, 0, 0, 0, 0) - - -def bottomhat(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask=None, - dtype_t[:, ::1] out=None, - char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): - _core16(kernel_bottomhat, image, selem, mask, out, shift_x, shift_y, - bitdepth, 0, 0, 0, 0) - - -def equalize(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask=None, - dtype_t[:, ::1] out=None, - char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): - _core16(kernel_equalize, image, selem, mask, out, shift_x, shift_y, - bitdepth, 0, 0, 0, 0) - - -def gradient(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask=None, - dtype_t[:, ::1] out=None, - char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): - _core16(kernel_gradient, image, selem, mask, out, shift_x, shift_y, - bitdepth, 0, 0, 0, 0) - - -def maximum(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask=None, - dtype_t[:, ::1] out=None, - char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): - _core16(kernel_maximum, image, selem, mask, out, shift_x, shift_y, - bitdepth, 0, 0, 0, 0) - - -def mean(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask=None, - dtype_t[:, ::1] out=None, - char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): - _core16(kernel_mean, image, selem, mask, out, shift_x, shift_y, - bitdepth, 0, 0, 0, 0) - - -def meansubtraction(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask=None, - dtype_t[:, ::1] out=None, - char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): - _core16(kernel_meansubtraction, image, selem, mask, out, shift_x, shift_y, - bitdepth, 0, 0, 0, 0) - - -def median(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask=None, - dtype_t[:, ::1] out=None, - char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): - _core16(kernel_median, image, selem, mask, out, shift_x, shift_y, - bitdepth, 0, 0, 0, 0) - - -def minimum(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask=None, - dtype_t[:, ::1] out=None, - char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): - _core16(kernel_minimum, image, selem, mask, out, shift_x, shift_y, - bitdepth, 0, 0, 0, 0) - - -def morph_contr_enh(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask=None, - dtype_t[:, ::1] out=None, - char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): - _core16(kernel_morph_contr_enh, image, selem, mask, out, shift_x, shift_y, - bitdepth, 0, 0, 0, 0) - - -def modal(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask=None, - dtype_t[:, ::1] out=None, - char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): - _core16(kernel_modal, image, selem, mask, out, shift_x, shift_y, - bitdepth, 0, 0, 0, 0) - - -def pop(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask=None, - dtype_t[:, ::1] out=None, - char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): - _core16(kernel_pop, image, selem, mask, out, shift_x, shift_y, - bitdepth, 0, 0, 0, 0) - - -def threshold(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask=None, - dtype_t[:, ::1] out=None, - char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): - _core16(kernel_threshold, image, selem, mask, out, shift_x, shift_y, - bitdepth, 0, 0, 0, 0) - - -def tophat(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask=None, - dtype_t[:, ::1] out=None, - char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): - _core16(kernel_tophat, image, selem, mask, out, shift_x, shift_y, - bitdepth, 0, 0, 0, 0) - - -def entropy(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask=None, - dtype_t[:, ::1] out=None, - char shift_x=0, char shift_y=0, Py_ssize_t bitdepth=8): - _core16(kernel_entropy, image, selem, mask, out, shift_x, shift_y, - bitdepth, 0, 0, 0, 0) diff --git a/skimage/filter/rank/generic8_cy.pyx b/skimage/filter/rank/generic8_cy.pyx deleted file mode 100644 index 93473a85..00000000 --- a/skimage/filter/rank/generic8_cy.pyx +++ /dev/null @@ -1,480 +0,0 @@ -#cython: cdivision=True -#cython: boundscheck=False -#cython: nonecheck=False -#cython: wraparound=False - -cimport numpy as cnp -from libc.math cimport log -from .core8_cy cimport dtype_t, _core8 - - -# ----------------------------------------------------------------- -# kernels uint8 -# ----------------------------------------------------------------- - - -cdef inline dtype_t kernel_autolevel(Py_ssize_t* histo, float pop, - dtype_t g, float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - - cdef Py_ssize_t i, imin, imax, delta - - if pop: - for i in range(255, -1, -1): - if histo[i]: - imax = i - break - for i in range(256): - if histo[i]: - imin = i - break - delta = imax - imin - if delta > 0: - return (255. * (g - imin) / delta) - else: - return (imax - imin) - else: - return (0) - - -cdef inline dtype_t kernel_bottomhat(Py_ssize_t* histo, float pop, - dtype_t g, float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - - cdef Py_ssize_t i - - if pop: - for i in range(256): - if histo[i]: - break - - return (g - i) - else: - return (0) - - -cdef inline dtype_t kernel_equalize(Py_ssize_t* histo, float pop, - dtype_t g, float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - - cdef Py_ssize_t i - cdef float sum = 0. - - if pop: - for i in range(256): - sum += histo[i] - if i >= g: - break - - return ((255 * sum) / pop) - else: - return (0) - - -cdef inline dtype_t kernel_gradient(Py_ssize_t* histo, float pop, - dtype_t g, float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - - cdef Py_ssize_t i, imin, imax - - if pop: - for i in range(255, -1, -1): - if histo[i]: - imax = i - break - for i in range(256): - if histo[i]: - imin = i - break - return (imax - imin) - else: - return (0) - - -cdef inline dtype_t kernel_maximum(Py_ssize_t* histo, float pop, - dtype_t g, float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - - cdef Py_ssize_t i - - if pop: - for i in range(255, -1, -1): - if histo[i]: - return (i) - else: - return (0) - - -cdef inline dtype_t kernel_mean(Py_ssize_t* histo, float pop, - dtype_t g, float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - - cdef Py_ssize_t i - cdef float mean = 0. - - if pop: - for i in range(256): - mean += histo[i] * i - return (mean / pop) - else: - return (0) - - -cdef inline dtype_t kernel_meansubtraction(Py_ssize_t* histo, float pop, - dtype_t g, float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - - cdef Py_ssize_t i - cdef float mean = 0. - - if pop: - for i in range(256): - mean += histo[i] * i - return ((g - mean / pop) / 2. + 127) - else: - return (0) - - -cdef inline dtype_t kernel_median(Py_ssize_t* histo, float pop, - dtype_t g, float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - - cdef Py_ssize_t i - cdef float sum = pop / 2.0 - - if pop: - for i in range(256): - if histo[i]: - sum -= histo[i] - if sum < 0: - return (i) - else: - return (0) - - -cdef inline dtype_t kernel_minimum(Py_ssize_t* histo, float pop, - dtype_t g, float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - - cdef Py_ssize_t i - - if pop: - for i in range(256): - if histo[i]: - return (i) - else: - return (0) - - -cdef inline dtype_t kernel_modal(Py_ssize_t* histo, float pop, - dtype_t g, float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - - cdef Py_ssize_t hmax = 0, imax = 0 - - if pop: - for i in range(256): - if histo[i] > hmax: - hmax = histo[i] - imax = i - return (imax) - else: - return (0) - - -cdef inline dtype_t kernel_morph_contr_enh(Py_ssize_t* histo, float pop, - dtype_t g, float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - - cdef Py_ssize_t i, imin, imax - - if pop: - for i in range(255, -1, -1): - if histo[i]: - imax = i - break - for i in range(256): - if histo[i]: - imin = i - break - if imax - g < g - imin: - return (imax) - else: - return (imin) - else: - return (0) - - -cdef inline dtype_t kernel_pop(Py_ssize_t* histo, float pop, - dtype_t g, float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - - return (pop) - - -cdef inline dtype_t kernel_threshold(Py_ssize_t* histo, float pop, - dtype_t g, float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - - cdef Py_ssize_t i - cdef float mean = 0. - - if pop: - for i in range(256): - mean += histo[i] * i - return (g > (mean / pop)) - else: - return (0) - - -cdef inline dtype_t kernel_tophat(Py_ssize_t* histo, float pop, - dtype_t g, float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - - cdef Py_ssize_t i - - if pop: - for i in range(255, -1, -1): - if histo[i]: - break - - return (i - g) - else: - return (0) - -cdef inline dtype_t kernel_noise_filter(Py_ssize_t* histo, float pop, - dtype_t g, float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - - cdef Py_ssize_t i - cdef Py_ssize_t min_i - - # early stop if at least one pixel of the neighborhood has the same g - if histo[g] > 0: - return 0 - - for i in range(g, -1, -1): - if histo[i]: - break - min_i = g - i - for i in range(g, 256): - if histo[i]: - break - if i - g < min_i: - return (i - g) - else: - return min_i - - -cdef inline dtype_t kernel_entropy(Py_ssize_t* histo, float pop, - dtype_t g, float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - cdef Py_ssize_t i - cdef float e, p - - if pop: - e = 0. - - for i in range(256): - p = histo[i] / pop - if p > 0: - e -= p * log(p) / 0.6931471805599453 - - return e * 10 - else: - return (0) - -cdef inline dtype_t kernel_otsu(Py_ssize_t* histo, float pop, dtype_t g, - float p0, float p1, Py_ssize_t s0, - Py_ssize_t s1): - cdef Py_ssize_t i - cdef Py_ssize_t max_i - cdef float P, mu1, mu2, q1, new_q1, sigma_b, max_sigma_b - cdef float mu = 0. - - # compute local mean - if pop: - for i in range(256): - mu += histo[i] * i - mu = (mu / pop) - else: - return (0) - - # maximizing the between class variance - max_i = 0 - q1 = histo[0] / pop - m1 = 0. - max_sigma_b = 0. - - for i in range(1, 256): - P = histo[i] / pop - new_q1 = q1 + P - if new_q1 > 0: - mu1 = (q1 * mu1 + i * P) / new_q1 - mu2 = (mu - new_q1 * mu1) / (1. - new_q1) - sigma_b = new_q1 * (1. - new_q1) * (mu1 - mu2) ** 2 - if sigma_b > max_sigma_b: - max_sigma_b = sigma_b - max_i = i - q1 = new_q1 - - return max_i - - -# ----------------------------------------------------------------- -# python wrappers -# used only internally -# ----------------------------------------------------------------- - - -def autolevel(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask=None, - dtype_t[:, ::1] out=None, - char shift_x=0, char shift_y=0): - _core8(kernel_autolevel, image, selem, mask, out, shift_x, shift_y, - 0, 0, 0, 0) - - -def bottomhat(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask=None, - dtype_t[:, ::1] out=None, - char shift_x=0, char shift_y=0): - _core8(kernel_bottomhat, image, selem, mask, out, shift_x, shift_y, - 0, 0, 0, 0) - - -def equalize(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask=None, - dtype_t[:, ::1] out=None, - char shift_x=0, char shift_y=0): - _core8(kernel_equalize, image, selem, mask, out, shift_x, shift_y, - 0, 0, 0, 0) - - -def gradient(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask=None, - dtype_t[:, ::1] out=None, - char shift_x=0, char shift_y=0): - _core8(kernel_gradient, image, selem, mask, out, shift_x, shift_y, - 0, 0, 0, 0) - - -def maximum(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask=None, - dtype_t[:, ::1] out=None, - char shift_x=0, char shift_y=0): - _core8(kernel_maximum, image, selem, mask, out, shift_x, shift_y, - 0, 0, 0, 0) - - -def mean(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask=None, - dtype_t[:, ::1] out=None, - char shift_x=0, char shift_y=0): - _core8(kernel_mean, image, selem, mask, out, shift_x, shift_y, - 0, 0, 0, 0) - - -def meansubtraction(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask=None, - dtype_t[:, ::1] out=None, - char shift_x=0, char shift_y=0): - _core8(kernel_meansubtraction, image, selem, mask, out, shift_x, shift_y, - 0, 0, 0, 0) - - -def median(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask=None, - dtype_t[:, ::1] out=None, - char shift_x=0, char shift_y=0): - _core8(kernel_median, image, selem, mask, out, shift_x, shift_y, - 0, 0, 0, 0) - - -def minimum(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask=None, - dtype_t[:, ::1] out=None, - char shift_x=0, char shift_y=0): - _core8(kernel_minimum, image, selem, mask, out, shift_x, shift_y, - 0, 0, 0, 0) - - -def morph_contr_enh(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask=None, - dtype_t[:, ::1] out=None, - char shift_x=0, char shift_y=0): - _core8(kernel_morph_contr_enh, image, selem, mask, out, shift_x, shift_y, - 0, 0, 0, 0) - - -def modal(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask=None, - dtype_t[:, ::1] out=None, - char shift_x=0, char shift_y=0): - _core8(kernel_modal, image, selem, mask, out, shift_x, shift_y, - 0, 0, 0, 0) - - -def pop(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask=None, - dtype_t[:, ::1] out=None, - char shift_x=0, char shift_y=0): - _core8(kernel_pop, image, selem, mask, out, shift_x, shift_y, - 0, 0, 0, 0) - - -def threshold(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask=None, - dtype_t[:, ::1] out=None, - char shift_x=0, char shift_y=0): - _core8(kernel_threshold, image, selem, mask, out, shift_x, shift_y, 0, 0, - 0, 0) - - -def tophat(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask=None, - dtype_t[:, ::1] out=None, - char shift_x=0, char shift_y=0): - _core8(kernel_tophat, image, selem, mask, out, shift_x, shift_y, - 0, 0, 0, 0) - - -def noise_filter(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask=None, - dtype_t[:, ::1] out=None, - char shift_x=0, char shift_y=0): - _core8(kernel_noise_filter, image, selem, mask, out, shift_x, shift_y, - 0, 0, 0, 0) - - -def entropy(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask=None, - dtype_t[:, ::1] out=None, - char shift_x=0, char shift_y=0): - _core8(kernel_entropy, image, selem, mask, out, shift_x, shift_y, - 0, 0, 0, 0) - - -def otsu(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask=None, - dtype_t[:, ::1] out=None, - char shift_x=0, char shift_y=0): - _core8(kernel_otsu, image, selem, mask, out, shift_x, shift_y, - 0, 0, 0, 0) diff --git a/skimage/filter/rank/generic_cy.pyx b/skimage/filter/rank/generic_cy.pyx new file mode 100644 index 00000000..0e972c30 --- /dev/null +++ b/skimage/filter/rank/generic_cy.pyx @@ -0,0 +1,576 @@ +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False + +cimport numpy as cnp +from libc.math cimport log + +from .core_cy cimport uint8_t, uint16_t, dtype_t, _core + + +cdef inline dtype_t _kernel_autolevel(Py_ssize_t* histo, float pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i, imin, imax, delta + + if pop: + for i in range(max_bin - 1, -1, -1): + if histo[i]: + imax = i + break + for i in range(max_bin): + if histo[i]: + imin = i + break + delta = imax - imin + if delta > 0: + return ((max_bin - 1) * (g - imin) / delta) + else: + return (imax - imin) + else: + return (0) + + +cdef inline dtype_t _kernel_bottomhat(Py_ssize_t* histo, float pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i + + if pop: + for i in range(max_bin): + if histo[i]: + break + + return (g - i) + else: + return (0) + + +cdef inline dtype_t _kernel_equalize(Py_ssize_t* histo, float pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i + cdef Py_ssize_t sum = 0 + + if pop: + for i in range(max_bin): + sum += histo[i] + if i >= g: + break + + return (((max_bin - 1) * sum) / pop) + else: + return (0) + + +cdef inline dtype_t _kernel_gradient(Py_ssize_t* histo, float pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i, imin, imax + + if pop: + for i in range(max_bin - 1, -1, -1): + if histo[i]: + imax = i + break + for i in range(max_bin): + if histo[i]: + imin = i + break + return (imax - imin) + else: + return (0) + + +cdef inline dtype_t _kernel_maximum(Py_ssize_t* histo, float pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i + + if pop: + for i in range(max_bin - 1, -1, -1): + if histo[i]: + return (i) + else: + return (0) + + +cdef inline dtype_t _kernel_mean(Py_ssize_t* histo, float pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i + cdef Py_ssize_t mean = 0 + + if pop: + for i in range(max_bin): + mean += histo[i] * i + return (mean / pop) + else: + return (0) + + +cdef inline dtype_t _kernel_meansubtraction(Py_ssize_t* histo, float pop, + dtype_t g, Py_ssize_t max_bin, + Py_ssize_t mid_bin, float p0, + float p1, Py_ssize_t s0, + Py_ssize_t s1): + + cdef Py_ssize_t i + cdef Py_ssize_t mean = 0 + + if pop: + for i in range(max_bin): + mean += histo[i] * i + return ((g - mean / pop) / 2. + 127) + else: + return (0) + + +cdef inline dtype_t _kernel_median(Py_ssize_t* histo, float pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i + cdef float sum = pop / 2.0 + + if pop: + for i in range(max_bin): + if histo[i]: + sum -= histo[i] + if sum < 0: + return (i) + else: + return (0) + + +cdef inline dtype_t _kernel_minimum(Py_ssize_t* histo, float pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i + + if pop: + for i in range(max_bin): + if histo[i]: + return (i) + else: + return (0) + + +cdef inline dtype_t _kernel_modal(Py_ssize_t* histo, float pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t hmax = 0, imax = 0 + + if pop: + for i in range(max_bin): + if histo[i] > hmax: + hmax = histo[i] + imax = i + return (imax) + else: + return (0) + + +cdef inline dtype_t _kernel_morph_contr_enh(Py_ssize_t* histo, float pop, + dtype_t g, Py_ssize_t max_bin, + Py_ssize_t mid_bin, float p0, + float p1, Py_ssize_t s0, + Py_ssize_t s1): + + cdef Py_ssize_t i, imin, imax + + if pop: + for i in range(max_bin - 1, -1, -1): + if histo[i]: + imax = i + break + for i in range(max_bin): + if histo[i]: + imin = i + break + if imax - g < g - imin: + return (imax) + else: + return (imin) + else: + return (0) + + +cdef inline dtype_t _kernel_pop(Py_ssize_t* histo, float pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + + return (pop) + + +cdef inline dtype_t _kernel_threshold(Py_ssize_t* histo, float pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i + cdef Py_ssize_t mean = 0 + + if pop: + for i in range(max_bin): + mean += histo[i] * i + return (g > (mean / pop)) + else: + return (0) + + +cdef inline dtype_t _kernel_tophat(Py_ssize_t* histo, float pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i + + if pop: + for i in range(max_bin - 1, -1, -1): + if histo[i]: + break + + return (i - g) + else: + return (0) + + +cdef inline dtype_t _kernel_noise_filter(Py_ssize_t* histo, float pop, + dtype_t g, Py_ssize_t max_bin, + Py_ssize_t mid_bin, float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i + cdef Py_ssize_t min_i + + # early stop if at least one pixel of the neighborhood has the same g + if histo[g] > 0: + return 0 + + for i in range(g, -1, -1): + if histo[i]: + break + min_i = g - i + for i in range(g, max_bin): + if histo[i]: + break + if i - g < min_i: + return (i - g) + else: + return min_i + + +cdef inline dtype_t _kernel_entropy(Py_ssize_t* histo, float pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + cdef Py_ssize_t i + cdef float e, p + + if pop: + e = 0. + + for i in range(max_bin): + p = histo[i] / pop + if p > 0: + e -= p * log(p) / 0.6931471805599453 + + return e + else: + return (0) + + +cdef inline dtype_t _kernel_otsu(Py_ssize_t* histo, float pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + cdef Py_ssize_t i + cdef Py_ssize_t max_i + cdef float P, mu1, mu2, q1, new_q1, sigma_b, max_sigma_b + cdef float mu = 0. + + # compute local mean + if pop: + for i in range(max_bin): + mu += histo[i] * i + mu = (mu / pop) + else: + return (0) + + # maximizing the between class variance + max_i = 0 + q1 = histo[0] / pop + m1 = 0. + max_sigma_b = 0. + + for i in range(1, max_bin): + P = histo[i] / pop + new_q1 = q1 + P + if new_q1 > 0: + mu1 = (q1 * mu1 + i * P) / new_q1 + mu2 = (mu - new_q1 * mu1) / (1. - new_q1) + sigma_b = new_q1 * (1. - new_q1) * (mu1 - mu2) ** 2 + if sigma_b > max_sigma_b: + max_sigma_b = sigma_b + max_i = i + q1 = new_q1 + + return max_i + + +def _autolevel(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t[:, ::1] out, + char shift_x, char shift_y, Py_ssize_t max_bin): + + if dtype_t is uint8_t: + _core[uint8_t](_kernel_autolevel[uint8_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) + elif dtype_t is uint16_t: + _core[uint16_t](_kernel_autolevel[uint16_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) + + +def _bottomhat(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t[:, ::1] out, + char shift_x, char shift_y, Py_ssize_t max_bin): + + if dtype_t is uint8_t: + _core[uint8_t](_kernel_bottomhat[uint8_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) + elif dtype_t is uint16_t: + _core[uint16_t](_kernel_bottomhat[uint16_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) + + +def _equalize(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t[:, ::1] out, + char shift_x, char shift_y, Py_ssize_t max_bin): + + if dtype_t is uint8_t: + _core[uint8_t](_kernel_equalize[uint8_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) + elif dtype_t is uint16_t: + _core[uint16_t](_kernel_equalize[uint16_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) + + +def _gradient(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t[:, ::1] out, + char shift_x, char shift_y, Py_ssize_t max_bin): + + if dtype_t is uint8_t: + _core[uint8_t](_kernel_gradient[uint8_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) + elif dtype_t is uint16_t: + _core[uint16_t](_kernel_gradient[uint16_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) + + +def _maximum(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t[:, ::1] out, + char shift_x, char shift_y, Py_ssize_t max_bin): + + if dtype_t is uint8_t: + _core[uint8_t](_kernel_maximum[uint8_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) + elif dtype_t is uint16_t: + _core[uint16_t](_kernel_maximum[uint16_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) + + +def _mean(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t[:, ::1] out, + char shift_x, char shift_y, Py_ssize_t max_bin): + + if dtype_t is uint8_t: + _core[uint8_t](_kernel_mean[uint8_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) + elif dtype_t is uint16_t: + _core[uint16_t](_kernel_mean[uint16_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) + + +def _meansubtraction(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t[:, ::1] out, + char shift_x, char shift_y, Py_ssize_t max_bin): + + if dtype_t is uint8_t: + _core[uint8_t](_kernel_meansubtraction[uint8_t], image, selem, mask, + out, shift_x, shift_y, 0, 0, 0, 0, max_bin) + elif dtype_t is uint16_t: + _core[uint16_t](_kernel_meansubtraction[uint16_t], image, selem, mask, + out, shift_x, shift_y, 0, 0, 0, 0, max_bin) + + +def _median(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t[:, ::1] out, + char shift_x, char shift_y, Py_ssize_t max_bin): + + if dtype_t is uint8_t: + _core[uint8_t](_kernel_median[uint8_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) + elif dtype_t is uint16_t: + _core[uint16_t](_kernel_median[uint16_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) + + +def _minimum(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t[:, ::1] out, + char shift_x, char shift_y, Py_ssize_t max_bin): + + if dtype_t is uint8_t: + _core[uint8_t](_kernel_minimum[uint8_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) + elif dtype_t is uint16_t: + _core[uint16_t](_kernel_minimum[uint16_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) + + +def _morph_contr_enh(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t[:, ::1] out, + char shift_x, char shift_y, Py_ssize_t max_bin): + + if dtype_t is uint8_t: + _core[uint8_t](_kernel_morph_contr_enh[uint8_t], image, selem, mask, + out, shift_x, shift_y, 0, 0, 0, 0, max_bin) + elif dtype_t is uint16_t: + _core[uint16_t](_kernel_morph_contr_enh[uint16_t], image, selem, mask, + out, shift_x, shift_y, 0, 0, 0, 0, max_bin) + + +def _modal(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t[:, ::1] out, + char shift_x, char shift_y, Py_ssize_t max_bin): + + if dtype_t is uint8_t: + _core[uint8_t](_kernel_modal[uint8_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) + elif dtype_t is uint16_t: + _core[uint16_t](_kernel_modal[uint16_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) + + +def _pop(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t[:, ::1] out, + char shift_x, char shift_y, Py_ssize_t max_bin): + + if dtype_t is uint8_t: + _core[uint8_t](_kernel_pop[uint8_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) + elif dtype_t is uint16_t: + _core[uint16_t](_kernel_pop[uint16_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) + + +def _threshold(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t[:, ::1] out, + char shift_x, char shift_y, Py_ssize_t max_bin): + + if dtype_t is uint8_t: + _core[uint8_t](_kernel_threshold[uint8_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) + elif dtype_t is uint16_t: + _core[uint16_t](_kernel_threshold[uint16_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) + + +def _tophat(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t[:, ::1] out, + char shift_x, char shift_y, Py_ssize_t max_bin): + + if dtype_t is uint8_t: + _core[uint8_t](_kernel_tophat[uint8_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) + elif dtype_t is uint16_t: + _core[uint16_t](_kernel_tophat[uint16_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) + + +def _noise_filter(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t[:, ::1] out, + char shift_x, char shift_y, Py_ssize_t max_bin): + + if dtype_t is uint8_t: + _core[uint8_t](_kernel_noise_filter[uint8_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) + elif dtype_t is uint16_t: + _core[uint16_t](_kernel_noise_filter[uint16_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) + + +def _entropy(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t[:, ::1] out, + char shift_x, char shift_y, Py_ssize_t max_bin): + + if dtype_t is uint8_t: + _core[uint8_t](_kernel_entropy[uint8_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) + elif dtype_t is uint16_t: + _core[uint16_t](_kernel_entropy[uint16_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) + + +def _otsu(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t[:, ::1] out, + char shift_x, char shift_y, Py_ssize_t max_bin): + + if dtype_t is uint8_t: + _core[uint8_t](_kernel_otsu[uint8_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) + elif dtype_t is uint16_t: + _core[uint16_t](_kernel_otsu[uint16_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) diff --git a/skimage/filter/rank/percentile.py b/skimage/filter/rank/percentile.py index b75d994f..3b0a04ba 100644 --- a/skimage/filter/rank/percentile.py +++ b/skimage/filter/rank/percentile.py @@ -26,8 +26,8 @@ from skimage import img_as_ubyte from ... import get_log log = get_log() -from . import percentile8_cy, percentile16_cy -from .generic import find_bitdepth +from . import percentile_cy +from .generic import _handle_input __all__ = ['percentile_autolevel', 'percentile_gradient', @@ -36,45 +36,18 @@ __all__ = ['percentile_autolevel', 'percentile_gradient', 'percentile_threshold'] -def _apply(func8, func16, image, selem, out, mask, shift_x, shift_y, p0, p1): - selem = img_as_ubyte(selem > 0) - image = np.ascontiguousarray(image) +def _apply(func, image, selem, out, mask, shift_x, shift_y, p0, p1): - if mask is None: - mask = np.ones(image.shape, dtype=np.uint8) - else: - mask = np.ascontiguousarray(mask) - mask = img_as_ubyte(mask) + image, selem, out, mask, max_bin = _handle_input(image, selem, out, mask) - if image is out: - raise NotImplementedError("Cannot perform rank operation in place.") - - if image.dtype == np.uint8: - if func8 is None: - raise TypeError("Not implemented for uint8 image.") - if out is None: - out = np.zeros(image.shape, dtype=np.uint8) - func8(image, selem, shift_x=shift_x, shift_y=shift_y, - mask=mask, out=out, p0=p0, p1=p1) - elif image.dtype == np.uint16: - if func16 is None: - raise TypeError("Not implemented for uint16 image.") - if out is None: - out = np.zeros(image.shape, dtype=np.uint16) - bitdepth = find_bitdepth(image) - if bitdepth > 10: - log.warn("Bitdepth of %d may result in bad rank filter " - "performance." % bitdepth) - func16(image, selem, shift_x=shift_x, shift_y=shift_y, mask=mask, - bitdepth=bitdepth + 1, out=out, p0=p0, p1=p1) - else: - raise TypeError("Only uint8 and uint16 image supported.") + func(image, selem, shift_x=shift_x, shift_y=shift_y, mask=mask, + out=out, max_bin=max_bin, p0=p0, p1=p1) return out def percentile_autolevel(image, selem, out=None, mask=None, shift_x=False, - shift_y=False, p0=.0, p1=1.): + shift_y=False, p0=0, p1=1): """Return greyscale local autolevel of an image. Autolevel is computed on the given structuring element. Only levels between @@ -82,13 +55,13 @@ def percentile_autolevel(image, selem, out=None, mask=None, shift_x=False, Parameters ---------- - image : ndarray - Image array (uint8 array or uint16). + image : ndarray (uint8, uint16) + Image array. selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray + out : ndarray (same dtype as input) If None, a new array will be allocated. - mask : ndarray (uint8) + mask : ndarray Mask array that defines (>0) area of the image included in the local neighborhood. If None, the complete image is used (default). shift_x, shift_y : int @@ -101,18 +74,18 @@ def percentile_autolevel(image, selem, out=None, mask=None, shift_x=False, Returns ------- - local autolevel : uint8 array or uint16 + local autolevel : ndarray (same dtype as input) The result of the local autolevel. """ - return _apply(percentile8_cy.autolevel, percentile16_cy.autolevel, + return _apply(percentile_cy._autolevel, image, selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y, p0=p0, p1=p1) def percentile_gradient(image, selem, out=None, mask=None, shift_x=False, - shift_y=False, p0=.0, p1=1.): + shift_y=False, p0=0, p1=1): """Return greyscale local percentile_gradient of an image. percentile_gradient is computed on the given structuring element. Only @@ -120,13 +93,13 @@ def percentile_gradient(image, selem, out=None, mask=None, shift_x=False, Parameters ---------- - image : ndarray - Image array (uint8 array or uint16). + image : ndarray (uint8, uint16) + Image array. selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray + out : ndarray (same dtype as input) If None, a new array will be allocated. - mask : ndarray (uint8) + mask : ndarray Mask array that defines (>0) area of the image included in the local neighborhood. If None, the complete image is used (default). shift_x, shift_y : int @@ -139,18 +112,18 @@ def percentile_gradient(image, selem, out=None, mask=None, shift_x=False, Returns ------- - local percentile_gradient : uint8 array or uint16 + local percentile_gradient : ndarray (same dtype as input) The result of the local percentile_gradient. """ - return _apply(percentile8_cy.gradient, percentile16_cy.gradient, + return _apply(percentile_cy._gradient, image, selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y, p0=p0, p1=p1) def percentile_mean(image, selem, out=None, mask=None, shift_x=False, - shift_y=False, p0=.0, p1=1.): + shift_y=False, p0=0, p1=1): """Return greyscale local mean of an image. Mean is computed on the given structuring element. Only levels between @@ -158,13 +131,13 @@ def percentile_mean(image, selem, out=None, mask=None, shift_x=False, Parameters ---------- - image : ndarray - Image array (uint8 array or uint16). + image : ndarray (uint8, uint16) + Image array. selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray + out : ndarray (same dtype as input) If None, a new array will be allocated. - mask : ndarray (uint8) + mask : ndarray Mask array that defines (>0) area of the image included in the local neighborhood. If None, the complete image is used (default). shift_x, shift_y : int @@ -177,18 +150,18 @@ def percentile_mean(image, selem, out=None, mask=None, shift_x=False, Returns ------- - local mean : uint8 array or uint16 + local mean : ndarray (same dtype as input) The result of the local mean. """ - return _apply(percentile8_cy.mean, percentile16_cy.mean, + return _apply(percentile_cy._mean, image, selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y, p0=p0, p1=p1) def percentile_mean_subtraction(image, selem, out=None, mask=None, - shift_x=False, shift_y=False, p0=.0, p1=1.): + shift_x=False, shift_y=False, p0=0, p1=1): """Return greyscale local mean_subtraction of an image. mean_subtraction is computed on the given structuring element. Only levels @@ -196,13 +169,13 @@ def percentile_mean_subtraction(image, selem, out=None, mask=None, Parameters ---------- - image : ndarray - Image array (uint8 array or uint16). + image : ndarray (uint8, uint16) + Image array. selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray + out : ndarray (same dtype as input) If None, a new array will be allocated. - mask : ndarray (uint8) + mask : ndarray Mask array that defines (>0) area of the image included in the local neighborhood. If None, the complete image is used (default). shift_x, shift_y : int @@ -215,19 +188,18 @@ def percentile_mean_subtraction(image, selem, out=None, mask=None, Returns ------- - local mean_subtraction : uint8 array or uint16 + local mean_subtraction : ndarray (same dtype as input) The result of the local mean_subtraction. """ - return _apply(percentile8_cy.mean_subtraction, - percentile16_cy.mean_subtraction, + return _apply(percentile_cy._mean_subtraction, image, selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y, p0=p0, p1=p1) def percentile_morph_contr_enh(image, selem, out=None, mask=None, - shift_x=False, shift_y=False, p0=.0, p1=1.): + shift_x=False, shift_y=False, p0=0, p1=1): """Return greyscale local morph_contr_enh of an image. morph_contr_enh is computed on the given structuring element. Only levels @@ -235,13 +207,13 @@ def percentile_morph_contr_enh(image, selem, out=None, mask=None, Parameters ---------- - image : ndarray - Image array (uint8 array or uint16). + image : ndarray (uint8, uint16) + Image array. selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray + out : ndarray (same dtype as input) If None, a new array will be allocated. - mask : ndarray (uint8) + mask : ndarray Mask array that defines (>0) area of the image included in the local neighborhood. If None, the complete image is used (default). shift_x, shift_y : int @@ -254,19 +226,18 @@ def percentile_morph_contr_enh(image, selem, out=None, mask=None, Returns ------- - local morph_contr_enh : uint8 array or uint16 + local morph_contr_enh : ndarray (same dtype as input) The result of the local morph_contr_enh. """ - return _apply(percentile8_cy.morph_contr_enh, - percentile16_cy.morph_contr_enh, + return _apply(percentile_cy._morph_contr_enh, image, selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y, p0=p0, p1=p1) def percentile(image, selem, out=None, mask=None, shift_x=False, shift_y=False, - p0=.0): + p0=0): """Return greyscale local percentile of an image. percentile is computed on the given structuring element. Returns the value @@ -274,13 +245,13 @@ def percentile(image, selem, out=None, mask=None, shift_x=False, shift_y=False, Parameters ---------- - image : ndarray - Image array (uint8 array or uint16). + image : ndarray (uint8, uint16) + Image array. selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray + out : ndarray (same dtype as input) If None, a new array will be allocated. - mask : ndarray (uint8) + mask : ndarray Mask array that defines (>0) area of the image included in the local neighborhood. If None, the complete image is used (default). shift_x, shift_y : int @@ -292,19 +263,18 @@ def percentile(image, selem, out=None, mask=None, shift_x=False, shift_y=False, Returns ------- - local percentile : uint8 array or uint16 + local percentile : ndarray (same dtype as input) The result of the local percentile. """ - return _apply(percentile8_cy.percentile, - percentile16_cy.percentile, + return _apply(percentile_cy._percentile, image, selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y, p0=p0, p1=0.) def percentile_pop(image, selem, out=None, mask=None, shift_x=False, - shift_y=False, p0=.0, p1=1.): + shift_y=False, p0=0, p1=1): """Return greyscale local pop of an image. pop is computed on the given structuring element. Only levels between @@ -312,13 +282,13 @@ def percentile_pop(image, selem, out=None, mask=None, shift_x=False, Parameters ---------- - image : ndarray - Image array (uint8 array or uint16). + image : ndarray (uint8, uint16) + Image array. selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray + out : ndarray (same dtype as input) If None, a new array will be allocated. - mask : ndarray (uint8) + mask : ndarray Mask array that defines (>0) area of the image included in the local neighborhood. If None, the complete image is used (default). shift_x, shift_y : int @@ -331,18 +301,18 @@ def percentile_pop(image, selem, out=None, mask=None, shift_x=False, Returns ------- - local pop : uint8 array or uint16 + local pop : ndarray (same dtype as input) The result of the local pop. """ - return _apply(percentile8_cy.pop, percentile16_cy.pop, + return _apply(percentile_cy._pop, image, selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y, p0=p0, p1=p1) def percentile_threshold(image, selem, out=None, mask=None, shift_x=False, - shift_y=False, p0=.0): + shift_y=False, p0=0): """Return greyscale local threshold of an image. threshold is computed on the given structuring element. Returns @@ -352,13 +322,13 @@ def percentile_threshold(image, selem, out=None, mask=None, shift_x=False, Parameters ---------- - image : ndarray - Image array (uint8 array or uint16). + image : ndarray (uint8, uint16) + Image array. selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray + out : ndarray (same dtype as input) If None, a new array will be allocated. - mask : ndarray (uint8) + mask : ndarray Mask array that defines (>0) area of the image included in the local neighborhood. If None, the complete image is used (default). shift_x, shift_y : int @@ -370,11 +340,11 @@ def percentile_threshold(image, selem, out=None, mask=None, shift_x=False, Returns ------- - local threshold : uint8 array or uint16 + local threshold : ndarray (same dtype as input) The result of the local threshold. """ - return _apply(percentile8_cy.threshold, percentile16_cy.threshold, + return _apply(percentile_cy._threshold, image, selem, out=out, mask=mask, shift_x=shift_x, - shift_y=shift_y, p0=p0, p1=0.) + shift_y=shift_y, p0=p0, p1=0) diff --git a/skimage/filter/rank/percentile16_cy.pyx b/skimage/filter/rank/percentile16_cy.pyx deleted file mode 100644 index d99dc3c8..00000000 --- a/skimage/filter/rank/percentile16_cy.pyx +++ /dev/null @@ -1,326 +0,0 @@ -#cython: cdivision=True -#cython: boundscheck=False -#cython: nonecheck=False -#cython: wraparound=False - -cimport numpy as cnp -from .core16_cy cimport dtype_t, _core16, uint16_min, uint16_max - - -# ----------------------------------------------------------------- -# kernels uint16 (SOFT version using percentiles) -# ----------------------------------------------------------------- - -cdef inline dtype_t kernel_autolevel(Py_ssize_t* histo, float pop, - dtype_t g, Py_ssize_t bitdepth, - Py_ssize_t maxbin, Py_ssize_t midbin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - - cdef int i, imin, imax, sum, delta - - if pop: - sum = 0 - p1 = 1.0 - p1 - for i in range(maxbin): - sum += histo[i] - if sum > p0 * pop: - imin = i - break - sum = 0 - for i in range(maxbin - 1, -1, -1): - sum += histo[i] - if sum > p1 * pop: - imax = i - break - - delta = imax - imin - if delta > 0: - return (1.0 * (maxbin - 1) - * (uint16_min(uint16_max(imin, g), imax) - - imin) / delta) - else: - return (imax - imin) - else: - return (0) - - -cdef inline dtype_t kernel_gradient(Py_ssize_t* histo, float pop, - dtype_t g, Py_ssize_t bitdepth, - Py_ssize_t maxbin, Py_ssize_t midbin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - - cdef int i, imin, imax, sum, delta - - if pop: - sum = 0 - p1 = 1.0 - p1 - for i in range(maxbin): - sum += histo[i] - if sum >= p0 * pop: - imin = i - break - sum = 0 - for i in range((maxbin - 1), -1, -1): - sum += histo[i] - if sum >= p1 * pop: - imax = i - break - - return (imax - imin) - else: - return (0) - - -cdef inline dtype_t kernel_mean(Py_ssize_t* histo, float pop, - dtype_t g, Py_ssize_t bitdepth, - Py_ssize_t maxbin, Py_ssize_t midbin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - - cdef int i, sum, mean, n - - if pop: - sum = 0 - mean = 0 - n = 0 - for i in range(maxbin): - sum += histo[i] - if (sum >= p0 * pop) and (sum <= p1 * pop): - n += histo[i] - mean += histo[i] * i - - if n > 0: - return (1.0 * mean / n) - else: - return (0) - else: - return (0) - - -cdef inline dtype_t kernel_mean_subtraction(Py_ssize_t* histo, - float pop, - dtype_t g, - Py_ssize_t bitdepth, - Py_ssize_t maxbin, - Py_ssize_t midbin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - - cdef int i, sum, mean, n - - if pop: - sum = 0 - mean = 0 - n = 0 - for i in range(maxbin): - sum += histo[i] - if (sum >= p0 * pop) and (sum <= p1 * pop): - n += histo[i] - mean += histo[i] * i - if n > 0: - return ((g - (mean / n)) * .5 + midbin) - else: - return (0) - else: - return (0) - - -cdef inline dtype_t kernel_morph_contr_enh(Py_ssize_t* histo, - float pop, - dtype_t g, - Py_ssize_t bitdepth, - Py_ssize_t maxbin, - Py_ssize_t midbin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - - cdef int i, imin, imax, sum, delta - - if pop: - sum = 0 - p1 = 1.0 - p1 - for i in range(maxbin): - sum += histo[i] - if sum > p0 * pop: - imin = i - break - sum = 0 - for i in range((maxbin - 1), -1, -1): - sum += histo[i] - if sum > p1 * pop: - imax = i - break - if g > imax: - return imax - if g < imin: - return imin - if imax - g < g - imin: - return imax - else: - return imin - else: - return (0) - - -cdef inline dtype_t kernel_percentile(Py_ssize_t* histo, float pop, - dtype_t g, Py_ssize_t bitdepth, - Py_ssize_t maxbin, Py_ssize_t midbin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - - cdef int i - cdef float sum = 0. - - if pop: - for i in range(maxbin): - sum += histo[i] - if sum >= p0 * pop: - break - - return (i) - else: - return (0) - - -cdef inline dtype_t kernel_pop(Py_ssize_t* histo, float pop, - dtype_t g, Py_ssize_t bitdepth, - Py_ssize_t maxbin, Py_ssize_t midbin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - - cdef int i, sum, n - - if pop: - sum = 0 - n = 0 - for i in range(maxbin): - sum += histo[i] - if (sum >= p0 * pop) and (sum <= p1 * pop): - n += histo[i] - return (n) - else: - return (0) - - -cdef inline dtype_t kernel_threshold(Py_ssize_t* histo, float pop, - dtype_t g, Py_ssize_t bitdepth, - Py_ssize_t maxbin, Py_ssize_t midbin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - - cdef int i - cdef float sum = 0. - - if pop: - for i in range(maxbin): - sum += histo[i] - if sum >= p0 * pop: - break - - return ((maxbin - 1) * (g >= i)) - else: - return (0) - - -# ----------------------------------------------------------------- -# python wrappers -# ----------------------------------------------------------------- - - -def autolevel(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask=None, - dtype_t[:, ::1] out=None, - char shift_x=0, char shift_y=0, int bitdepth=8, - float p0=0., float p1=0.): - """bottom hat - """ - _core16(kernel_autolevel, image, selem, mask, out, shift_x, shift_y, - bitdepth, p0, p1, 0, 0) - - -def gradient(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask=None, - dtype_t[:, ::1] out=None, - char shift_x=0, char shift_y=0, int bitdepth=8, - float p0=0., float p1=0.): - """return p0,p1 percentile gradient - """ - _core16(kernel_gradient, image, selem, mask, out, shift_x, shift_y, - bitdepth, p0, p1, 0, 0) - - -def mean(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask=None, - dtype_t[:, ::1] out=None, - char shift_x=0, char shift_y=0, int bitdepth=8, - float p0=0., float p1=0.): - """return mean between [p0 and p1] percentiles - """ - _core16(kernel_mean, image, selem, mask, out, shift_x, shift_y, - bitdepth, p0, p1, 0, 0) - - -def mean_subtraction(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask=None, - dtype_t[:, ::1] out=None, - char shift_x=0, char shift_y=0, int bitdepth=8, - float p0=0., float p1=0.): - """return original - mean between [p0 and p1] percentiles *.5 +127 - """ - _core16( - kernel_mean_subtraction, image, selem, mask, out, shift_x, shift_y, - bitdepth, p0, p1, 0, 0) - - -def morph_contr_enh(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask=None, - dtype_t[:, ::1] out=None, - char shift_x=0, char shift_y=0, int bitdepth=8, - float p0=0., float p1=0.): - """reforce contrast using percentiles - """ - _core16(kernel_morph_contr_enh, image, selem, mask, out, shift_x, shift_y, - bitdepth, p0, p1, 0, 0) - - -def percentile(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask=None, - dtype_t[:, ::1] out=None, - char shift_x=0, char shift_y=0, int bitdepth=8, - float p0=0.): - """return p0 percentile - """ - _core16(kernel_percentile, image, selem, mask, out, shift_x, shift_y, - bitdepth, p0, .0, 0, 0) - - -def pop(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask=None, - dtype_t[:, ::1] out=None, - char shift_x=0, char shift_y=0, int bitdepth=8, - float p0=0., float p1=0.): - """return nb of pixels between [p0 and p1] - """ - _core16(kernel_pop, image, selem, mask, out, shift_x, shift_y, - bitdepth, p0, p1, 0, 0) - - -def threshold(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask=None, - dtype_t[:, ::1] out=None, - char shift_x=0, char shift_y=0, int bitdepth=8, - float p0=0.): - """return (maxbin-1) if g > percentile p0 - """ - _core16(kernel_threshold, image, selem, mask, out, shift_x, shift_y, - bitdepth, p0, 0., 0, 0) diff --git a/skimage/filter/rank/percentile8_cy.pyx b/skimage/filter/rank/percentile8_cy.pyx deleted file mode 100644 index 3ce63478..00000000 --- a/skimage/filter/rank/percentile8_cy.pyx +++ /dev/null @@ -1,291 +0,0 @@ -#cython: cdivision=True -#cython: boundscheck=False -#cython: nonecheck=False -#cython: wraparound=False - -cimport numpy as cnp -from .core8_cy cimport dtype_t, _core8, uint8_max, uint8_min - - -# ----------------------------------------------------------------- -# kernels uint8 (SOFT version using percentiles) -# ----------------------------------------------------------------- - - -cdef inline dtype_t kernel_autolevel(Py_ssize_t* histo, float pop, - dtype_t g, float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - cdef int i, imin, imax, sum, delta - - if pop: - sum = 0 - p1 = 1.0 - p1 - imin = 0 - imax = 255 - - for i in range(256): - sum += histo[i] - if sum > (p0 * pop): - imin = i - break - sum = 0 - for i in range(255, -1, -1): - sum += histo[i] - if sum > (p1 * pop): - imax = i - break - delta = imax - imin - if delta > 0: - return (255 * (uint8_min(uint8_max(imin, g), imax) - - imin) / delta) - else: - return (imax - imin) - else: - return (128) - - -cdef inline dtype_t kernel_gradient(Py_ssize_t* histo, float pop, - dtype_t g, float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - cdef int i, imin, imax, sum, delta - - if pop: - sum = 0 - p1 = 1.0 - p1 - for i in range(256): - sum += histo[i] - if sum >= p0 * pop: - imin = i - break - sum = 0 - for i in range(255, -1, -1): - sum += histo[i] - if sum >= p1 * pop: - imax = i - break - - return (imax - imin) - else: - return (0) - - -cdef inline dtype_t kernel_mean(Py_ssize_t* histo, float pop, - dtype_t g, float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - cdef int i, sum, mean, n - - if pop: - sum = 0 - mean = 0 - n = 0 - for i in range(256): - sum += histo[i] - if (sum >= p0 * pop) and (sum <= p1 * pop): - n += histo[i] - mean += histo[i] * i - if n > 0: - return (1.0 * mean / n) - else: - return (0) - else: - return (0) - - -cdef inline dtype_t kernel_mean_subtraction(Py_ssize_t* histo, - float pop, - dtype_t g, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - cdef int i, sum, mean, n - - if pop: - sum = 0 - mean = 0 - n = 0 - for i in range(256): - sum += histo[i] - if (sum >= p0 * pop) and (sum <= p1 * pop): - n += histo[i] - mean += histo[i] * i - if n > 0: - return ((g - (mean / n)) * .5 + 127) - else: - return (0) - else: - return (0) - - -cdef inline dtype_t kernel_morph_contr_enh(Py_ssize_t* histo, - float pop, - dtype_t g, float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - cdef int i, imin, imax, sum, delta - - if pop: - sum = 0 - p1 = 1.0 - p1 - for i in range(256): - sum += histo[i] - if sum >= p0 * pop: - imin = i - break - sum = 0 - for i in range(255, -1, -1): - sum += histo[i] - if sum >= p1 * pop: - imax = i - break - if g > imax: - return imax - if g < imin: - return imin - if imax - g < g - imin: - return imax - else: - return imin - else: - return (0) - - -cdef inline dtype_t kernel_percentile(Py_ssize_t* histo, float pop, - dtype_t g, float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - cdef int i - cdef float sum = 0. - - if pop: - for i in range(256): - sum += histo[i] - if sum >= p0 * pop: - break - - return (i) - else: - return (0) - - -cdef inline dtype_t kernel_pop(Py_ssize_t* histo, float pop, - dtype_t g, float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - cdef int i, sum, n - - if pop: - sum = 0 - n = 0 - for i in range(256): - sum += histo[i] - if (sum >= p0 * pop) and (sum <= p1 * pop): - n += histo[i] - return (n) - else: - return (0) - - -cdef inline dtype_t kernel_threshold(Py_ssize_t* histo, float pop, - dtype_t g, float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - cdef int i - cdef float sum = 0. - - if pop: - for i in range(256): - sum += histo[i] - if sum >= p0 * pop: - break - - return (255 * (g >= i)) - else: - return (0) - - -# ----------------------------------------------------------------- -# python wrappers -# ----------------------------------------------------------------- - - -def autolevel(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask=None, - dtype_t[:, ::1] out=None, - char shift_x=0, char shift_y=0, float p0=0., float p1=0.): - """autolevel - """ - _core8(kernel_autolevel, image, selem, mask, out, shift_x, shift_y, p0, p1, - 0, 0) - - -def gradient(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask=None, - dtype_t[:, ::1] out=None, - char shift_x=0, char shift_y=0, float p0=0., float p1=0.): - """return p0,p1 percentile gradient - """ - _core8(kernel_gradient, image, selem, mask, out, shift_x, shift_y, p0, p1, - 0, 0) - - -def mean(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask=None, - dtype_t[:, ::1] out=None, - char shift_x=0, char shift_y=0, float p0=0., float p1=0.): - """return mean between [p0 and p1] percentiles - """ - _core8(kernel_mean, image, selem, mask, out, shift_x, shift_y, p0, p1, - 0, 0) - - -def mean_subtraction(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask=None, - dtype_t[:, ::1] out=None, - char shift_x=0, char shift_y=0, float p0=0., float p1=0.): - """return original - mean between [p0 and p1] percentiles *.5 +127 - """ - _core8(kernel_mean_subtraction, image, selem, mask, out, shift_x, shift_y, - p0, p1, 0, 0) - - -def morph_contr_enh(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask=None, - dtype_t[:, ::1] out=None, - char shift_x=0, char shift_y=0, float p0=0., float p1=0.): - """reforce contrast using percentiles - """ - _core8(kernel_morph_contr_enh, image, selem, mask, out, shift_x, shift_y, - p0, p1, 0, 0) - - -def percentile(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask=None, - dtype_t[:, ::1] out=None, - char shift_x=0, char shift_y=0, float p0=0.): - """return p0 percentile - """ - _core8(kernel_percentile, image, selem, mask, out, shift_x, shift_y, - p0, 0., 0, 0) - - -def pop(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask=None, - dtype_t[:, ::1] out=None, - char shift_x=0, char shift_y=0, float p0=0., float p1=0.): - """return nb of pixels between [p0 and p1] - """ - _core8(kernel_pop, image, selem, mask, out, shift_x, shift_y, p0, p1, - 0, 0) - - -def threshold(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask=None, - dtype_t[:, ::1] out=None, - char shift_x=0, char shift_y=0, float p0=0.): - """return 255 if g > percentile p0 - """ - _core8(kernel_threshold, image, selem, mask, out, shift_x, shift_y, p0, 0., - 0, 0) diff --git a/skimage/filter/rank/percentile_cy.pyx b/skimage/filter/rank/percentile_cy.pyx new file mode 100644 index 00000000..6df271fa --- /dev/null +++ b/skimage/filter/rank/percentile_cy.pyx @@ -0,0 +1,325 @@ +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False + +cimport numpy as cnp +from .core_cy cimport uint8_t, uint16_t, dtype_t, _core, _min, _max + + +cdef inline dtype_t _kernel_autolevel(Py_ssize_t* histo, float pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i, imin, imax, sum, delta + + if pop: + sum = 0 + p1 = 1.0 - p1 + for i in range(max_bin - 1): + sum += histo[i] + if sum > p0 * pop: + imin = i + break + sum = 0 + for i in range(max_bin - 1, -1, -1): + sum += histo[i] + if sum > p1 * pop: + imax = i + break + + delta = imax - imin + if delta > 0: + return ((max_bin - 1) * (_min(_max(imin, g), imax) + - imin) / delta) + else: + return (imax - imin) + else: + return (0) + + +cdef inline dtype_t _kernel_gradient(Py_ssize_t* histo, float pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i, imin, imax, sum, delta + + if pop: + sum = 0 + p1 = 1.0 - p1 + for i in range(max_bin): + sum += histo[i] + if sum >= p0 * pop: + imin = i + break + sum = 0 + for i in range((max_bin - 1), -1, -1): + sum += histo[i] + if sum >= p1 * pop: + imax = i + break + + return (imax - imin) + else: + return (0) + + +cdef inline dtype_t _kernel_mean(Py_ssize_t* histo, float pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i, sum, mean, n + + if pop: + sum = 0 + mean = 0 + n = 0 + for i in range(max_bin): + sum += histo[i] + if (sum >= p0 * pop) and (sum <= p1 * pop): + n += histo[i] + mean += histo[i] * i + + if n > 0: + return (mean / n) + else: + return (0) + else: + return (0) + + +cdef inline dtype_t _kernel_mean_subtraction(Py_ssize_t* histo, float pop, + dtype_t g, Py_ssize_t max_bin, + Py_ssize_t mid_bin, float p0, + float p1, Py_ssize_t s0, + Py_ssize_t s1): + + cdef Py_ssize_t i, sum, mean, n + + if pop: + sum = 0 + mean = 0 + n = 0 + for i in range(max_bin): + sum += histo[i] + if (sum >= p0 * pop) and (sum <= p1 * pop): + n += histo[i] + mean += histo[i] * i + if n > 0: + return ((g - (mean / n)) * .5 + mid_bin) + else: + return (0) + else: + return (0) + + +cdef inline dtype_t _kernel_morph_contr_enh(Py_ssize_t* histo, float pop, + dtype_t g, Py_ssize_t max_bin, + Py_ssize_t mid_bin, float p0, + float p1, Py_ssize_t s0, + Py_ssize_t s1): + + cdef Py_ssize_t i, imin, imax, sum, delta + + if pop: + sum = 0 + p1 = 1.0 - p1 + for i in range(max_bin): + sum += histo[i] + if sum > p0 * pop: + imin = i + break + sum = 0 + for i in range((max_bin - 1), -1, -1): + sum += histo[i] + if sum > p1 * pop: + imax = i + break + if g > imax: + return imax + if g < imin: + return imin + if imax - g < g - imin: + return imax + else: + return imin + else: + return (0) + + +cdef inline dtype_t _kernel_percentile(Py_ssize_t* histo, float pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i + cdef Py_ssize_t sum = 0 + + if pop: + for i in range(max_bin): + sum += histo[i] + if sum >= p0 * pop: + break + + return (i) + else: + return (0) + + +cdef inline dtype_t _kernel_pop(Py_ssize_t* histo, float pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i, sum, n + + if pop: + sum = 0 + n = 0 + for i in range(max_bin): + sum += histo[i] + if (sum >= p0 * pop) and (sum <= p1 * pop): + n += histo[i] + return (n) + else: + return (0) + + +cdef inline dtype_t _kernel_threshold(Py_ssize_t* histo, float pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef int i + cdef Py_ssize_t sum = 0 + + if pop: + for i in range(max_bin): + sum += histo[i] + if sum >= p0 * pop: + break + + return ((max_bin - 1) * (g >= i)) + else: + return (0) + + +def _autolevel(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t[:, ::1] out, + char shift_x, char shift_y, float p0, float p1, + Py_ssize_t max_bin): + + if dtype_t is uint8_t: + _core[uint8_t](_kernel_autolevel[uint8_t], image, selem, mask, out, + shift_x, shift_y, p0, p1, 0, 0, max_bin) + elif dtype_t is uint16_t: + _core[uint16_t](_kernel_autolevel[uint16_t], image, selem, mask, out, + shift_x, shift_y, p0, p1, 0, 0, max_bin) + + +def _gradient(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t[:, ::1] out, + char shift_x, char shift_y, float p0, float p1, + Py_ssize_t max_bin): + + if dtype_t is uint8_t: + _core[uint8_t](_kernel_gradient[uint8_t], image, selem, mask, out, + shift_x, shift_y, p0, p1, 0, 0, max_bin) + elif dtype_t is uint16_t: + _core[uint16_t](_kernel_gradient[uint16_t], image, selem, mask, out, + shift_x, shift_y, p0, p1, 0, 0, max_bin) + + +def _mean(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t[:, ::1] out, + char shift_x, char shift_y, float p0, float p1, + Py_ssize_t max_bin): + + if dtype_t is uint8_t: + _core[uint8_t](_kernel_mean[uint8_t], image, selem, mask, out, + shift_x, shift_y, p0, p1, 0, 0, max_bin) + elif dtype_t is uint16_t: + _core[uint16_t](_kernel_mean[uint16_t], image, selem, mask, out, + shift_x, shift_y, p0, p1, 0, 0, max_bin) + + +def _mean_subtraction(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t[:, ::1] out, + char shift_x, char shift_y, float p0, float p1, + Py_ssize_t max_bin): + + if dtype_t is uint8_t: + _core[uint8_t](_kernel_mean_subtraction[uint8_t], image, selem, mask, + out, shift_x, shift_y, p0, p1, 0, 0, max_bin) + elif dtype_t is uint16_t: + _core[uint16_t](_kernel_mean_subtraction[uint16_t], image, selem, mask, + out, shift_x, shift_y, p0, p1, 0, 0, max_bin) + + +def _morph_contr_enh(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t[:, ::1] out, + char shift_x, char shift_y, float p0, float p1, + Py_ssize_t max_bin): + + if dtype_t is uint8_t: + _core[uint8_t](_kernel_morph_contr_enh[uint8_t], image, selem, mask, + out, shift_x, shift_y, p0, p1, 0, 0, max_bin) + elif dtype_t is uint16_t: + _core[uint16_t](_kernel_morph_contr_enh[uint16_t], image, selem, mask, + out, shift_x, shift_y, p0, p1, 0, 0, max_bin) + + +def _percentile(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t[:, ::1] out, + char shift_x, char shift_y, float p0, Py_ssize_t max_bin): + + if dtype_t is uint8_t: + _core[uint8_t](_kernel_percentile[uint8_t], image, selem, mask, out, + shift_x, shift_y, p0, 1, 0, 0, max_bin) + elif dtype_t is uint16_t: + _core[uint16_t](_kernel_percentile[uint16_t], image, selem, mask, out, + shift_x, shift_y, p0, 1, 0, 0, max_bin) + + +def _pop(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t[:, ::1] out, + char shift_x, char shift_y, float p0, float p1, + Py_ssize_t max_bin): + + if dtype_t is uint8_t: + _core[uint8_t](_kernel_pop[uint8_t], image, selem, mask, out, + shift_x, shift_y, p0, p1, 0, 0, max_bin) + elif dtype_t is uint16_t: + _core[uint16_t](_kernel_pop[uint16_t], image, selem, mask, out, + shift_x, shift_y, p0, p1, 0, 0, max_bin) + + +def _threshold(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t[:, ::1] out, + char shift_x, char shift_y, float p0, Py_ssize_t max_bin): + + if dtype_t is uint8_t: + _core[uint8_t](_kernel_threshold[uint8_t], image, selem, mask, out, + shift_x, shift_y, p0, 1, 0, 0, max_bin) + elif dtype_t is uint16_t: + _core[uint16_t](_kernel_threshold[uint16_t], image, selem, mask, out, + shift_x, shift_y, p0, 1, 0, 0, max_bin) diff --git a/skimage/filter/rank/tests/test_rank.py b/skimage/filter/rank/tests/test_rank.py index 1efe2bff..f55d3522 100644 --- a/skimage/filter/rank/tests/test_rank.py +++ b/skimage/filter/rank/tests/test_rank.py @@ -176,12 +176,10 @@ def test_compare_autolevels_16bit(): assert_array_equal(loc_autolevel, loc_perc_autolevel) -def test_compare_uint_vs_float(): - # filters applied on 8-bit image ore 16-bit image (having only real 8-bit of - # dynamic) should be identical +def test_compare_ubyte_vs_float(): # Create signed int8 image that and convert it to uint8 - image_uint = img_as_uint(data.camera()[:50, :50]) + image_uint = img_as_ubyte(data.camera()[:50, :50]) image_float = img_as_float(image_uint) methods = ['autolevel', 'bottomhat', 'equalize', 'gradient', 'threshold', @@ -372,37 +370,37 @@ def test_entropy(): selem = np.ones((16, 16), dtype=np.uint8) # 1 bit per pixel data = np.tile(np.asarray([0, 1]), (100, 100)).astype(np.uint8) - assert(np.max(rank.entropy(data, selem)) == 10) + assert(np.max(rank.entropy(data, selem)) == 1) # 2 bit per pixel data = np.tile(np.asarray([[0, 1], [2, 3]]), (10, 10)).astype(np.uint8) - assert(np.max(rank.entropy(data, selem)) == 20) + assert(np.max(rank.entropy(data, selem)) == 2) # 3 bit per pixel data = np.tile( np.asarray([[0, 1, 2, 3], [4, 5, 6, 7]]), (10, 10)).astype(np.uint8) - assert(np.max(rank.entropy(data, selem)) == 30) + assert(np.max(rank.entropy(data, selem)) == 3) # 4 bit per pixel data = np.tile( np.reshape(np.arange(16), (4, 4)), (10, 10)).astype(np.uint8) - assert(np.max(rank.entropy(data, selem)) == 40) + assert(np.max(rank.entropy(data, selem)) == 4) # 6 bit per pixel data = np.tile( np.reshape(np.arange(64), (8, 8)), (10, 10)).astype(np.uint8) - assert(np.max(rank.entropy(data, selem)) == 60) + assert(np.max(rank.entropy(data, selem)) == 6) # 8-bit per pixel data = np.tile( np.reshape(np.arange(256), (16, 16)), (10, 10)).astype(np.uint8) - assert(np.max(rank.entropy(data, selem)) == 80) + assert(np.max(rank.entropy(data, selem)) == 8) # 12 bit per pixel selem = np.ones((64, 64), dtype=np.uint8) data = np.tile( np.reshape(np.arange(4096), (64, 64)), (2, 2)).astype(np.uint16) - assert(np.max(rank.entropy(data, selem)) == 12000) + assert(np.max(rank.entropy(data, selem)) == 12) def test_selem_dtypes(): diff --git a/skimage/filter/setup.py b/skimage/filter/setup.py index 35626114..33ad97df 100644 --- a/skimage/filter/setup.py +++ b/skimage/filter/setup.py @@ -14,34 +14,24 @@ def configuration(parent_package='', top_path=None): cython(['_ctmf.pyx'], working_path=base_path) cython(['_denoise_cy.pyx'], working_path=base_path) - cython(['rank/core8_cy.pyx'], working_path=base_path) - cython(['rank/core16_cy.pyx'], working_path=base_path) - cython(['rank/generic8_cy.pyx'], working_path=base_path) - cython(['rank/percentile8_cy.pyx'], working_path=base_path) - cython(['rank/generic16_cy.pyx'], working_path=base_path) - cython(['rank/percentile16_cy.pyx'], working_path=base_path) - cython(['rank/bilateral16_cy.pyx'], working_path=base_path) + cython(['rank/core_cy.pyx'], working_path=base_path) + cython(['rank/generic_cy.pyx'], working_path=base_path) + cython(['rank/percentile_cy.pyx'], working_path=base_path) + cython(['rank/bilateral_cy.pyx'], working_path=base_path) config.add_extension('_ctmf', sources=['_ctmf.c'], include_dirs=[get_numpy_include_dirs()]) config.add_extension('_denoise_cy', sources=['_denoise_cy.c'], include_dirs=[get_numpy_include_dirs(), '../_shared']) - config.add_extension('rank.core8_cy', sources=['rank/core8_cy.c'], + config.add_extension('rank.core_cy', sources=['rank/core_cy.c'], include_dirs=[get_numpy_include_dirs()]) - config.add_extension('rank.core16_cy', sources=['rank/core16_cy.c'], - include_dirs=[get_numpy_include_dirs()]) - config.add_extension('rank.generic8_cy', sources=['rank/generic8_cy.c'], + config.add_extension('rank.generic_cy', sources=['rank/generic_cy.c'], include_dirs=[get_numpy_include_dirs()]) config.add_extension( - 'rank.percentile8_cy', sources=['rank/percentile8_cy.c'], - include_dirs=[get_numpy_include_dirs()]) - config.add_extension('rank.generic16_cy', sources=['rank/generic16_cy.c'], + 'rank.percentile_cy', sources=['rank/percentile_cy.c'], include_dirs=[get_numpy_include_dirs()]) config.add_extension( - 'rank.percentile16_cy', sources=['rank/percentile16_cy.c'], - include_dirs=[get_numpy_include_dirs()]) - config.add_extension( - 'rank.bilateral16_cy', sources=['rank/bilateral16_cy.c'], + 'rank.bilateral_cy', sources=['rank/bilateral_cy.c'], include_dirs=[get_numpy_include_dirs()]) return config From 68914b8934414e7ed62b26e6d43de2c0d48bd500 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Tue, 2 Jul 2013 00:18:14 +0200 Subject: [PATCH 268/736] Update bento config --- bento.info | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/bento.info b/bento.info index fe07dc5d..d7ddcecc 100644 --- a/bento.info +++ b/bento.info @@ -123,27 +123,18 @@ Library: Extension: skimage._shared.geometry Sources: skimage/_shared/geometry.pyx - Extension: skimage.filter.rank.bilateral16_cy + Extension: skimage.filter.rank.generic_cy Sources: - skimage/filter/rank/bilateral16_cy.pyx - Extension: skimage.filter.rank.generic8_cy + skimage/filter/rank/generic_cy.pyx + Extension: skimage.filter.rank.percentile_cy Sources: - skimage/filter/rank/generic8_cy.pyx - Extension: skimage.filter.rank.percentile16_cy + skimage/filter/rank/percentile_cy.pyx + Extension: skimage.filter.rank.core_cy Sources: - skimage/filter/rank/percentile16_cy.pyx - Extension: skimage.filter.rank.core16_cy + skimage/filter/rank/core_cy.pyx + Extension: skimage.filter.rank.bilateral_cy Sources: - skimage/filter/rank/core16_cy.pyx - Extension: skimage.filter.rank.core8_cy - Sources: - skimage/filter/rank/core8_cy.pyx - Extension: skimage.filter.rank.generic16_cy - Sources: - skimage/filter/rank/generic16_cy.pyx - Extension: skimage.filter.rank.percentile8_cy - Sources: - skimage/filter/rank/percentile8_cy.pyx + skimage/filter/rank/bilateral_cy.pyx Executable: skivi Module: skimage.scripts.skivi From 34ad95d9175588db0c53095289c151c609eef0dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Tue, 2 Jul 2013 00:24:36 +0200 Subject: [PATCH 269/736] Improve entropy example --- doc/examples/plot_entropy.py | 34 +++++++++++----------------------- 1 file changed, 11 insertions(+), 23 deletions(-) diff --git a/doc/examples/plot_entropy.py b/doc/examples/plot_entropy.py index db230d8b..2c06c2bb 100644 --- a/doc/examples/plot_entropy.py +++ b/doc/examples/plot_entropy.py @@ -3,6 +3,9 @@ Entropy ======= +Image entropy is a quantity which is used to describe the amount of information +coded in an image. + """ import numpy as np import matplotlib.pyplot as plt @@ -13,33 +16,18 @@ from skimage.morphology import disk from skimage.util import img_as_ubyte -# defining a 8- and a 16-bit test images -a8 = img_as_ubyte(data.camera()) -a16 = a8.astype(np.uint16) * 4 +image = img_as_ubyte(data.camera()) -ent8 = entropy(a8, disk(5)) # pixel value contain 10x the local entropy -ent16 = entropy(a16, disk(5)) # pixel value contain 1000x the local entropy +plt.figure(figsize=(10, 4)) -# display results -plt.figure(figsize=(10, 10)) - -plt.subplot(2,2,1) -plt.imshow(a8, cmap=plt.cm.gray) -plt.xlabel('8-bit image') +plt.subplot(121) +plt.imshow(image, cmap=plt.cm.gray) +plt.title('Image') plt.colorbar() -plt.subplot(2,2,2) -plt.imshow(ent8, cmap=plt.cm.jet) -plt.xlabel('entropy*10') +plt.subplot(122) +plt.imshow(entropy(image, disk(5)), cmap=plt.cm.jet) +plt.title('Entropy') plt.colorbar() -plt.subplot(2,2,3) -plt.imshow(a16, cmap=plt.cm.gray) -plt.xlabel('16-bit image') -plt.colorbar() - -plt.subplot(2,2,4) -plt.imshow(ent16, cmap=plt.cm.jet) -plt.xlabel('entropy*1000') -plt.colorbar() plt.show() From 73492045b263c20f9b0c6f84055ef0af0b197085 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Tue, 2 Jul 2013 00:28:16 +0200 Subject: [PATCH 270/736] Improve mean example --- doc/examples/plot_rank_mean.py | 37 ++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/doc/examples/plot_rank_mean.py b/doc/examples/plot_rank_mean.py index e23beb18..013535cb 100644 --- a/doc/examples/plot_rank_mean.py +++ b/doc/examples/plot_rank_mean.py @@ -6,9 +6,9 @@ Mean filters This example compares the following mean filters of the rank filter package: * **local mean**: all pixels belonging to the structuring element to compute - average gray level + average gray level. * **percentile mean**: only use values between percentiles p0 and p1 - (here 10% and 90%) + (here 10% and 90%). * **bilateral mean**: only use pixels of the structuring element having a gray level situated inside g-s0 and g+s1 (here g-500 and g+500) @@ -23,23 +23,30 @@ import matplotlib.pyplot as plt from skimage import data from skimage.morphology import disk -import skimage.filter.rank as rank +from skimage.filter import rank -a16 = (data.coins()).astype(np.uint16) * 16 + +image = (data.coins()).astype(np.uint16) * 16 selem = disk(20) -f1 = rank.percentile_mean(a16, selem=selem, p0=.1, p1=.9) -f2 = rank.bilateral_mean(a16, selem=selem, s0=500, s1=500) -f3 = rank.mean(a16, selem=selem) +percentile_result = rank.percentile_mean(image, selem=selem, p0=.1, p1=.9) +bilateral_result = rank.bilateral_mean(image, selem=selem, s0=500, s1=500) +normal_result = rank.mean(image, selem=selem) -# display results -fig, axes = plt.subplots(nrows=3, figsize=(15, 10)) + +fig, axes = plt.subplots(nrows=3, figsize=(8, 10)) ax0, ax1, ax2 = axes -ax0.imshow(np.hstack((a16, f1))) -ax0.set_title('percentile mean') -ax1.imshow(np.hstack((a16, f2))) -ax1.set_title('bilateral mean') -ax2.imshow(np.hstack((a16, f3))) -ax2.set_title('local mean') +ax0.imshow(np.hstack((image, percentile_result))) +ax0.set_title('Percentile mean') +ax0.axis('off') + +ax1.imshow(np.hstack((image, bilateral_result))) +ax1.set_title('Bilateral mean') +ax1.axis('off') + +ax2.imshow(np.hstack((image, normal_result))) +ax2.set_title('Local mean') +ax2.axis('off') + plt.show() From d1e0949533ad30e2cd3e5afccbf59d835c1b0fe3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Tue, 2 Jul 2013 00:32:46 +0200 Subject: [PATCH 271/736] Update entropy example with improved matplotlib usage --- doc/examples/plot_entropy.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/doc/examples/plot_entropy.py b/doc/examples/plot_entropy.py index 2c06c2bb..ed5519bd 100644 --- a/doc/examples/plot_entropy.py +++ b/doc/examples/plot_entropy.py @@ -18,16 +18,17 @@ from skimage.util import img_as_ubyte image = img_as_ubyte(data.camera()) -plt.figure(figsize=(10, 4)) +fig, (ax0, ax1) = plt.subplots(ncols=2, figsize=(10, 4)) -plt.subplot(121) -plt.imshow(image, cmap=plt.cm.gray) -plt.title('Image') -plt.colorbar() -plt.subplot(122) -plt.imshow(entropy(image, disk(5)), cmap=plt.cm.jet) -plt.title('Entropy') -plt.colorbar() +img0 = ax0.imshow(image, cmap=plt.cm.gray) +ax0.set_title('Image') +ax0.axis('off') +plt.colorbar(img0, ax=ax0) + +img1 = ax1.imshow(entropy(image, disk(5)), cmap=plt.cm.jet) +ax1.set_title('Entropy') +ax1.axis('off') +plt.colorbar(img1, ax=ax1) plt.show() From d6fb12a493abdce2a68cba5e6ed61cde303f5443 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Tue, 2 Jul 2013 01:12:51 +0200 Subject: [PATCH 272/736] Improve long rank filter example --- .../applications/plot_rank_filters.py | 421 ++++++++++-------- doc/examples/plot_entropy.py | 1 - 2 files changed, 237 insertions(+), 185 deletions(-) diff --git a/doc/examples/applications/plot_rank_filters.py b/doc/examples/applications/plot_rank_filters.py index 31284226..ded335bd 100644 --- a/doc/examples/applications/plot_rank_filters.py +++ b/doc/examples/applications/plot_rank_filters.py @@ -3,11 +3,11 @@ Rank filters ============ -Rank filters are non-linear filters using the local greylevels ordering to +Rank filters are non-linear filters using the local gray-level ordering to compute the filtered value. This ensemble of filters share a common base: the -local grey-level histogram extraction computed on the neighborhood of a pixel -(defined by a 2D structuring element). If the filtered value is taken as the -middle value of the histogram, we get the classical median filter. +local gray-level histogram is computed on the neighborhood of a pixel (defined +by a 2-D structuring element). If the filtered value is taken as the middle +value of the histogram, we get the classical median filter. Rank filters can be used for several purposes such as: @@ -26,11 +26,9 @@ Rank filters can be used for several purposes such as: Some well known filters are specific cases of rank filters [1]_ e.g. morphological dilation, morphological erosion, median filters. -The different implementation availables in `skimage` are compared. - -In this example, we will see how to filter a greylevel image using some of the -linear and non-linear filters availables in skimage. We use the `camera` -image from `skimage.data`. +In this example, we will see how to filter a gray-level image using some of the +linear and non-linear filters available in skimage. We use the `camera` image +from `skimage.data` for all comparisons. .. [1] Pierre Soille, On morphological operators based on rank filters, Pattern Recognition 35 (2002) 527-535. @@ -42,16 +40,16 @@ import matplotlib.pyplot as plt from skimage import data -ima = data.camera() -hist = np.histogram(ima, bins=np.arange(0, 256)) +noisy_image = data.camera() +hist = np.histogram(noisy_image, bins=np.arange(0, 256)) plt.figure(figsize=(8, 3)) plt.subplot(1, 2, 1) -plt.imshow(ima, cmap=plt.cm.gray, interpolation='nearest') +plt.imshow(noisy_image, interpolation='nearest') plt.axis('off') plt.subplot(1, 2, 2) plt.plot(hist[1][:-1], hist[0], lw=2) -plt.title('histogram of grey values') +plt.title('Histogram of grey values') """ @@ -65,50 +63,56 @@ randomly set to 0. The **median** filter is applied to remove the noise. .. note:: - there are different implementations of median filter : + There are different implementations of median filter: `skimage.filter.median_filter` and `skimage.filter.rank.median` """ -noise = np.random.random(ima.shape) -nima = data.camera() -nima[noise > 0.99] = 255 -nima[noise < 0.01] = 0 - from skimage.filter.rank import median from skimage.morphology import disk -fig = plt.figure(figsize=[10, 7]) +noise = np.random.random(noisy_image.shape) +noisy_image = data.camera() +noisy_image[noise > 0.99] = 255 +noisy_image[noise < 0.01] = 0 + +fig = plt.figure(figsize=(10, 7)) -lo = median(nima, disk(1)) -hi = median(nima, disk(5)) -ext = median(nima, disk(20)) plt.subplot(2, 2, 1) -plt.imshow(nima, cmap=plt.cm.gray, vmin=0, vmax=255) -plt.xlabel('noised image') +plt.imshow(noisy_image, vmin=0, vmax=255) +plt.title('Noisy image') +plt.axis('off') + plt.subplot(2, 2, 2) -plt.imshow(lo, cmap=plt.cm.gray, vmin=0, vmax=255) -plt.xlabel('median $r=1$') +plt.imshow(median(noisy_image, disk(1)), vmin=0, vmax=255) +plt.title('Median $r=1$') +plt.axis('off') + plt.subplot(2, 2, 3) -plt.imshow(hi, cmap=plt.cm.gray, vmin=0, vmax=255) -plt.xlabel('median $r=5$') +plt.imshow(median(noisy_image, disk(5)), vmin=0, vmax=255) +plt.title('Median $r=5$') +plt.axis('off') + plt.subplot(2, 2, 4) -plt.imshow(ext, cmap=plt.cm.gray, vmin=0, vmax=255) -plt.xlabel('median $r=20$') +plt.imshow(median(noisy_image, disk(20)), vmin=0, vmax=255) +plt.title('Median $r=20$') +plt.axis('off') """ .. image:: PLOT2RST.current_figure -The added noise is efficiently removed, as the image defaults are small (1 pixel -wide), a small filter radius is sufficient. As the radius is increasing, objects -with a bigger size are filtered as well, such as the camera tripod. The median -filter is commonly used for noise removal because borders are preserved. +The added noise is efficiently removed, as the image defaults are small (1 +pixel wide), a small filter radius is sufficient. As the radius is increasing, +objects with bigger sizes are filtered as well, such as the camera tripod. The +median filter is often used for noise removal because borders are preserved and +e.g. salt and pepper noise typically does not distort the gray-level. Image smoothing ================ -The example hereunder shows how a local **mean** smoothes the camera man image. +The example hereunder shows how a local **mean** filter smooths the camera man +image. """ @@ -116,13 +120,17 @@ from skimage.filter.rank import mean fig = plt.figure(figsize=[10, 7]) -loc_mean = mean(nima, disk(10)) +loc_mean = mean(noisy_image, disk(10)) + plt.subplot(1, 2, 1) -plt.imshow(ima, cmap=plt.cm.gray, vmin=0, vmax=255) -plt.xlabel('original') +plt.imshow(noisy_image, vmin=0, vmax=255) +plt.title('Original') +plt.axis('off') + plt.subplot(1, 2, 2) -plt.imshow(loc_mean, cmap=plt.cm.gray, vmin=0, vmax=255) -plt.xlabel('local mean $r=10$') +plt.imshow(loc_mean, vmin=0, vmax=255) +plt.title('Local mean $r=10$') +plt.axis('off') """ @@ -130,35 +138,42 @@ plt.xlabel('local mean $r=10$') One may be interested in smoothing an image while preserving important borders (median filters already achieved this), here we use the **bilateral** filter -that restricts the local neighborhood to pixel having a greylevel similar to +that restricts the local neighborhood to pixel having a gray-level similar to the central one. .. note:: - a different implementation is available for color images in + A different implementation is available for color images in `skimage.filter.denoise_bilateral`. """ from skimage.filter.rank import bilateral_mean -ima = data.camera() +noisy_image = data.camera() selem = disk(10) -bilat = bilateral_mean(ima.astype(np.uint16), disk(20), s0=10, s1=10) +bilat = bilateral_mean(noisy_image.astype(np.uint16), disk(20), s0=10, s1=10) -# display results fig = plt.figure(figsize=[10, 7]) + plt.subplot(2, 2, 1) -plt.imshow(ima, cmap=plt.cm.gray) -plt.xlabel('original') +plt.imshow(noisy_image, cmap=plt.cm.gray) +plt.title('Original') +plt.axis('off') + plt.subplot(2, 2, 3) plt.imshow(bilat, cmap=plt.cm.gray) -plt.xlabel('bilateral mean') +plt.title('Bilateral mean') +plt.axis('off') + plt.subplot(2, 2, 2) -plt.imshow(ima[200:350, 350:450], cmap=plt.cm.gray) +plt.imshow(noisy_image[200:350, 350:450], cmap=plt.cm.gray) +plt.axis('off') + plt.subplot(2, 2, 4) plt.imshow(bilat[200:350, 350:450], cmap=plt.cm.gray) +plt.axis('off') """ @@ -175,7 +190,7 @@ We compare here how the global histogram equalization is applied locally. The equalized image [2]_ has a roughly linear cumulative distribution function for each pixel neighborhood. The local version [3]_ of the histogram -equalization emphasizes every local greylevel variations. +equalization emphasizes every local gray-level variations. .. [2] http://en.wikipedia.org/wiki/Histogram_equalization .. [3] http://en.wikipedia.org/wiki/Adaptive_histogram_equalization @@ -185,74 +200,86 @@ equalization emphasizes every local greylevel variations. from skimage import exposure from skimage.filter import rank -ima = data.camera() +noisy_image = data.camera() + # equalize globally and locally -glob = exposure.equalize(ima) * 255 -loc = rank.equalize(ima, disk(20)) +glob = exposure.equalize(noisy_image) * 255 +loc = rank.equalize(noisy_image, disk(20)) # extract histogram for each image -hist = np.histogram(ima, bins=np.arange(0, 256)) +hist = np.histogram(noisy_image, bins=np.arange(0, 256)) glob_hist = np.histogram(glob, bins=np.arange(0, 256)) loc_hist = np.histogram(loc, bins=np.arange(0, 256)) plt.figure(figsize=(10, 10)) + plt.subplot(321) -plt.imshow(ima, cmap=plt.cm.gray, interpolation='nearest') +plt.imshow(noisy_image, interpolation='nearest') plt.axis('off') + plt.subplot(322) plt.plot(hist[1][:-1], hist[0], lw=2) -plt.title('histogram of grey values') +plt.title('Histogram of gray values') + plt.subplot(323) -plt.imshow(glob, cmap=plt.cm.gray, interpolation='nearest') +plt.imshow(glob, interpolation='nearest') plt.axis('off') + plt.subplot(324) plt.plot(glob_hist[1][:-1], glob_hist[0], lw=2) -plt.title('histogram of grey values') +plt.title('Histogram of gray values') + plt.subplot(325) -plt.imshow(loc, cmap=plt.cm.gray, interpolation='nearest') +plt.imshow(loc, interpolation='nearest') plt.axis('off') + plt.subplot(326) plt.plot(loc_hist[1][:-1], loc_hist[0], lw=2) -plt.title('histogram of grey values') +plt.title('Histogram of gray values') """ .. image:: PLOT2RST.current_figure -another way to maximize the number of greylevels used for an image is to apply -a local autoleveling, i.e. here a pixel greylevel is proportionally remapped -between local minimum and local maximum. +Another way to maximize the number of gray-levels used for an image is to apply +a local auto-leveling, i.e. the gray-value of a pixel is proportionally +remapped between local minimum and local maximum. -The following example shows how local autolevel enhances the camara man picture. +The following example shows how local auto-level enhances the camara man +picture. """ from skimage.filter.rank import autolevel -ima = data.camera() +noisy_image = data.camera() selem = disk(10) -auto = autolevel(ima.astype(np.uint16), disk(20)) +auto = autolevel(noisy_image.astype(np.uint16), disk(20)) -# display results fig = plt.figure(figsize=[10, 7]) + plt.subplot(1, 2, 1) -plt.imshow(ima, cmap=plt.cm.gray) -plt.xlabel('original') +plt.imshow(noisy_image, cmap=plt.cm.gray) +plt.title('Original') +plt.axis('off') + plt.subplot(1, 2, 2) plt.imshow(auto, cmap=plt.cm.gray) -plt.xlabel('local autolevel') +plt.title('Local autolevel') +plt.axis('off') """ .. image:: PLOT2RST.current_figure -This filter is very sensitive to local outlayers, see the little white spot in -the sky left part. This is due to a local maximum which is very high comparing -to the rest of the neighborhood. One can moderate this using the percentile -version of the autolevel filter which uses given percentiles (one inferior, -one superior) in place of local minimum and maximum. The example below -illustrates how the percentile parameters influence the local autolevel result. +This filter is very sensitive to local outliers, see the little white spot in +the left part of the sky. This is due to a local maximum which is very high +comparing to the rest of the neighborhood. One can moderate this using the +percentile version of the auto-level filter which uses given percentiles (one +inferior, one superior) in place of local minimum and maximum. The example +below illustrates how the percentile parameters influence the local auto-level +result. """ @@ -272,14 +299,14 @@ ax0, ax1, ax2 = axes plt.gray() ax0.imshow(np.hstack((image, loc_autolevel))) -ax0.set_title('original / autolevel') +ax0.set_title('Original / auto-level') ax1.imshow( np.hstack((loc_perc_autolevel0, loc_perc_autolevel1)), vmin=0, vmax=255) -ax1.set_title('percentile autolevel 0%,1%') +ax1.set_title('Percentile auto-level 0%,1%') ax2.imshow( np.hstack((loc_perc_autolevel2, loc_perc_autolevel3)), vmin=0, vmax=255) -ax2.set_title('percentile autolevel 5% and 10%') +ax2.set_title('Percentile auto-level 5% and 10%') for ax in axes: ax.axis('off') @@ -289,29 +316,35 @@ for ax in axes: .. image:: PLOT2RST.current_figure The morphological contrast enhancement filter replaces the central pixel by the -local maximum if the original pixel value is closest to local maximum, otherwise -by the minimum local. +local maximum if the original pixel value is closest to local maximum, +otherwise by the minimum local. """ from skimage.filter.rank import morph_contr_enh -ima = data.camera() +noisy_image = data.camera() -enh = morph_contr_enh(ima, disk(5)) +enh = morph_contr_enh(noisy_image, disk(5)) -# display results fig = plt.figure(figsize=[10, 7]) plt.subplot(2, 2, 1) -plt.imshow(ima, cmap=plt.cm.gray) -plt.xlabel('original') +plt.imshow(noisy_image, cmap=plt.cm.gray) +plt.title('Original') +plt.axis('off') + plt.subplot(2, 2, 3) plt.imshow(enh, cmap=plt.cm.gray) -plt.xlabel('local morphlogical contrast enhancement') +plt.title('Local morphological contrast enhancement') +plt.axis('off') + plt.subplot(2, 2, 2) -plt.imshow(ima[200:350, 350:450], cmap=plt.cm.gray) +plt.imshow(noisy_image[200:350, 350:450], cmap=plt.cm.gray) +plt.axis('off') + plt.subplot(2, 2, 4) plt.imshow(enh[200:350, 350:450], cmap=plt.cm.gray) +plt.axis('off') """ @@ -324,22 +357,28 @@ percentile *p0* and *p1* instead of the local minimum and maximum. from skimage.filter.rank import percentile_morph_contr_enh -ima = data.camera() +noisy_image = data.camera() -penh = percentile_morph_contr_enh(ima, disk(5), p0=.1, p1=.9) +penh = percentile_morph_contr_enh(noisy_image, disk(5), p0=.1, p1=.9) -# display results fig = plt.figure(figsize=[10, 7]) plt.subplot(2, 2, 1) -plt.imshow(ima, cmap=plt.cm.gray) -plt.xlabel('original') +plt.imshow(noisy_image, cmap=plt.cm.gray) +plt.title('Original') +plt.axis('off') + plt.subplot(2, 2, 3) plt.imshow(penh, cmap=plt.cm.gray) -plt.xlabel('local percentile morphlogical\n contrast enhancement') +plt.title('Local percentile morphological\n contrast enhancement') +plt.axis('off') + plt.subplot(2, 2, 2) -plt.imshow(ima[200:350, 350:450], cmap=plt.cm.gray) +plt.imshow(noisy_image[200:350, 350:450], cmap=plt.cm.gray) +plt.axis('off') + plt.subplot(2, 2, 4) plt.imshow(penh[200:350, 350:450], cmap=plt.cm.gray) +plt.axis('off') """ @@ -348,18 +387,18 @@ plt.imshow(penh[200:350, 350:450], cmap=plt.cm.gray) Image threshold =============== -The Otsu's threshold [1]_ method can be applied locally using the local -greylevel distribution. In the example below, for each pixel, an "optimal" -threshold is determined by maximizing the variance between two classes of pixels -of the local neighborhood defined by a structuring element. +The Otsu threshold [1]_ method can be applied locally using the local gray- +level distribution. In the example below, for each pixel, an "optimal" +threshold is determined by maximizing the variance between two classes of +pixels of the local neighborhood defined by a structuring element. The example compares the local threshold with the global threshold `skimage.filter.threshold_otsu`. .. note:: - Local thresholding is much slower than global one. There exists a function - for global Otsu thresholding: `skimage.filter.threshold_otsu`. + Local is much slower than global thresholding. A function for global Otsu + thresholding can be found in : `skimage.filter.threshold_otsu`. .. [4] http://en.wikipedia.org/wiki/Otsu's_method @@ -382,27 +421,35 @@ t_glob_otsu = threshold_otsu(p8) glob_otsu = p8 >= t_glob_otsu plt.figure() + plt.subplot(2, 2, 1) plt.imshow(p8, cmap=plt.cm.gray) -plt.xlabel('original') +plt.title('Original') plt.colorbar() +plt.axis('off') + plt.subplot(2, 2, 2) plt.imshow(t_loc_otsu, cmap=plt.cm.gray) -plt.xlabel('local Otsu ($radius=%d$)' % radius) +plt.title('Local Otsu ($r=%d$)' % radius) plt.colorbar() +plt.axis('off') + plt.subplot(2, 2, 3) plt.imshow(p8 >= t_loc_otsu, cmap=plt.cm.gray) -plt.xlabel('original>=local Otsu' % t_glob_otsu) +plt.title('Original >= local Otsu' % t_glob_otsu) +plt.axis('off') + plt.subplot(2, 2, 4) plt.imshow(glob_otsu, cmap=plt.cm.gray) -plt.xlabel('global Otsu ($t=%d$)' % t_glob_otsu) +plt.title('Global Otsu ($t=%d$)' % t_glob_otsu) +plt.axis('off') """ .. image:: PLOT2RST.current_figure -The following example shows how local Otsu's threshold handles a global level -shift applied to a synthetic image . +The following example shows how local Otsu thresholding handles a global level +shift applied to a synthetic image. """ @@ -413,13 +460,18 @@ m = (np.tile(x, (n, 1)) * np.linspace(0.1, 1, n) * 128 + 128).astype(np.uint8) radius = 10 t = rank.otsu(m, disk(radius)) + plt.figure() + plt.subplot(1, 2, 1) plt.imshow(m) -plt.xlabel('original') +plt.title('Original') +plt.axis('off') + plt.subplot(1, 2, 2) plt.imshow(m >= t, interpolation='nearest') -plt.xlabel('local Otsu ($radius=%d$)' % radius) +plt.title('Local Otsu ($r=%d$)' % radius) +plt.axis('off') """ @@ -428,7 +480,7 @@ plt.xlabel('local Otsu ($radius=%d$)' % radius) Image morphology ================ -Local maximum and local minimum are the base operators for greylevel +Local maximum and local minimum are the base operators for gray-level morphology. .. note:: @@ -436,33 +488,41 @@ morphology. `skimage.dilate` and `skimage.erode` are equivalent filters (see below for comparison). -Here is an example of the classical morphological greylevel filters: opening, +Here is an example of the classical morphological gray-level filters: opening, closing and morphological gradient. """ from skimage.filter.rank import maximum, minimum, gradient -ima = data.camera() +noisy_image = data.camera() -closing = maximum(minimum(ima, disk(5)), disk(5)) -opening = minimum(maximum(ima, disk(5)), disk(5)) -grad = gradient(ima, disk(5)) +closing = maximum(minimum(noisy_image, disk(5)), disk(5)) +opening = minimum(maximum(noisy_image, disk(5)), disk(5)) +grad = gradient(noisy_image, disk(5)) # display results fig = plt.figure(figsize=[10, 7]) + plt.subplot(2, 2, 1) -plt.imshow(ima, cmap=plt.cm.gray) -plt.xlabel('original') +plt.imshow(noisy_image, cmap=plt.cm.gray) +plt.title('Original') +plt.axis('off') + plt.subplot(2, 2, 2) plt.imshow(closing, cmap=plt.cm.gray) -plt.xlabel('greylevel closing') +plt.title('Gray-level closing') +plt.axis('off') + plt.subplot(2, 2, 3) plt.imshow(opening, cmap=plt.cm.gray) -plt.xlabel('greylevel opening') +plt.title('Gray-level opening') +plt.axis('off') + plt.subplot(2, 2, 4) plt.imshow(grad, cmap=plt.cm.gray) -plt.xlabel('morphological gradient') +plt.title('Morphological gradient') +plt.axis('off') """ @@ -471,13 +531,14 @@ plt.xlabel('morphological gradient') Feature extraction =================== -Local histogram can be exploited to compute local entropy, which is related to +Local histograms can be exploited to compute local entropy, which is related to the local image complexity. Entropy is computed using base 2 logarithm i.e. the -filter returns the minimum number of bits needed to encode local greylevel +filter returns the minimum number of bits needed to encode local gray-level distribution. -`skimage.rank.entropy` returns local entropy on a given structuring element. -The following example shows this filter applied on 8- and 16- bit images. +`skimage.rank.entropy` returns the local entropy on a given structuring +element. The following example shows applies this filter on 8- and 16-bit +images. .. note:: @@ -492,47 +553,36 @@ from skimage.morphology import disk import numpy as np import matplotlib.pyplot as plt -# defining a 8- and a 16-bit test images -a8 = data.camera() -a16 = data.camera().astype(np.uint16) * 4 +image = data.camera() -ent8 = entropy(a8, disk(5)) # pixel value contain 10x the local entropy -ent16 = entropy(a16, disk(5)) # pixel value contain 1000x the local entropy +plt.figure(figsize=(10, 4)) -# display results -plt.figure(figsize=(10, 10)) - -plt.subplot(2, 2, 1) -plt.imshow(a8, cmap=plt.cm.gray) -plt.xlabel('8-bit image') +plt.subplot(1, 2, 1) +plt.imshow(image, cmap=plt.cm.gray) +plt.title('Image') plt.colorbar() +plt.axis('off') -plt.subplot(2, 2, 2) -plt.imshow(ent8, cmap=plt.cm.jet) -plt.xlabel('entropy*10') -plt.colorbar() - -plt.subplot(2, 2, 3) -plt.imshow(a16, cmap=plt.cm.gray) -plt.xlabel('16-bit image') -plt.colorbar() - -plt.subplot(2, 2, 4) -plt.imshow(ent16, cmap=plt.cm.jet) -plt.xlabel('entropy*1000') +plt.subplot(1, 2, 2) +plt.imshow(entropy(image, disk(5)), cmap=plt.cm.jet) +plt.title('Entropy') plt.colorbar() +plt.axis('off') """ .. image:: PLOT2RST.current_figure Implementation -================ +============== -The central part of the `skimage.rank` filters is build on a sliding window that -update local greylevel histogram. This approach limits the algorithm complexity -to O(n) where n is the number of image pixels. The complexity is also limited -with respect to the structuring element size. +The central part of the `skimage.rank` filters is build on a sliding window +that updates the local gray-level histogram. This approach limits the algorithm +complexity to O(n) where n is the number of image pixels. The complexity is +also limited with respect to the structuring element size. + +In the following we compare the performance of different implementations +available in `skimage`. """ @@ -583,10 +633,10 @@ def ndi_med(image, n): Comparison between -* `rank.maximum` -* `cmorph.dilate` +* `filter.rank.maximum` +* `morphology.dilate` -on increasing structuring element size +on increasing structuring element size: """ @@ -603,18 +653,18 @@ for r in e_range: rec = np.asarray(rec) plt.figure() -plt.title('increasing element size') -plt.ylabel('time (ms)') -plt.xlabel('element radius') +plt.title('Performance with respect to element size') +plt.ylabel('Time (ms)') +plt.title('Element radius') plt.plot(e_range, rec) -plt.legend(['crank.maximum', 'cmorph.dilate']) +plt.legend(['filter.rank.maximum', 'morphology.dilate']) """ -and increasing image size - .. image:: PLOT2RST.current_figure +and increasing image size: + """ r = 9 @@ -623,7 +673,7 @@ elem = disk(r + 1) rec = [] s_range = range(100, 1000, 100) for s in s_range: - a = (np.random.random((s, s)) * 256).astype('uint8') + a = (np.random.random((s, s)) * 256).astype(np.uint8) (rc, ms_rc) = cr_max(a, elem) (rcm, ms_rcm) = cm_dil(a, elem) rec.append((ms_rc, ms_rcm)) @@ -631,11 +681,11 @@ for s in s_range: rec = np.asarray(rec) plt.figure() -plt.title('increasing image size') -plt.ylabel('time (ms)') -plt.xlabel('image size') +plt.title('Performance with respect to image size') +plt.ylabel('Time (ms)') +plt.title('Image size') plt.plot(s_range, rec) -plt.legend(['crank.maximum', 'cmorph.dilate']) +plt.legend(['filter.rank.maximum', 'morphology.dilate']) """ @@ -644,11 +694,11 @@ plt.legend(['crank.maximum', 'cmorph.dilate']) Comparison between: -* `rank.median` -* `ctmf.median_filter` -* `ndimage.percentile` +* `filter.rank.median` +* `filter.median_filter` +* `scipy.ndimage.percentile` -on increasing structuring element size +on increasing structuring element size: """ @@ -666,27 +716,29 @@ for r in e_range: rec = np.asarray(rec) plt.figure() -plt.title('increasing element size') +plt.title('Performance with respect to element size') plt.plot(e_range, rec) -plt.legend(['rank.median', 'ctmf.median_filter', 'ndimage.percentile']) -plt.ylabel('time (ms)') -plt.xlabel('element radius') +plt.legend(['filter.rank.median', 'filter.median_filter', + 'scipy.ndimage.percentile']) +plt.ylabel('Time (ms)') +plt.title('Element radius') """ .. image:: PLOT2RST.current_figure -comparison of outcome of the three methods +Comparison of outcome of the three methods: """ plt.figure() plt.imshow(np.hstack((rc, rctmf, rndi))) -plt.xlabel('rank.median vs ctmf.median_filter vs ndimage.percentile') +plt.title('filter.rank.median vs filtermedian_filter vs scipy.ndimage.percentile') +plt.axis('off') """ .. image:: PLOT2RST.current_figure -and increasing image size +and increasing image size: """ @@ -696,7 +748,7 @@ elem = disk(r + 1) rec = [] s_range = [100, 200, 500, 1000] for s in s_range: - a = (np.random.random((s, s)) * 256).astype('uint8') + a = (np.random.random((s, s)) * 256).astype(np.uint8) (rc, ms_rc) = cr_med(a, elem) rctmf, ms_rctmf = ctmf_med(a, r) rndi, ms_ndi = ndi_med(a, r) @@ -705,11 +757,12 @@ for s in s_range: rec = np.asarray(rec) plt.figure() -plt.title('increasing image size') +plt.title('Performance with respect to image size') plt.plot(s_range, rec) -plt.legend(['rank.median', 'ctmf.median_filter', 'ndimage.percentile']) -plt.ylabel('time (ms)') -plt.xlabel('image size') +plt.legend(['filter.rank.median', 'filter.median_filter', + 'scipy.ndimage.percentile']) +plt.ylabel('Time (ms)') +plt.title('Image size') """ .. image:: PLOT2RST.current_figure diff --git a/doc/examples/plot_entropy.py b/doc/examples/plot_entropy.py index ed5519bd..f5001e31 100644 --- a/doc/examples/plot_entropy.py +++ b/doc/examples/plot_entropy.py @@ -20,7 +20,6 @@ image = img_as_ubyte(data.camera()) fig, (ax0, ax1) = plt.subplots(ncols=2, figsize=(10, 4)) - img0 = ax0.imshow(image, cmap=plt.cm.gray) ax0.set_title('Image') ax0.axis('off') From 43feb4bf7844722b4d317325325a5a4408501f31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Tue, 2 Jul 2013 01:17:12 +0200 Subject: [PATCH 273/736] Update bitdepth warning --- skimage/filter/rank/bilateral.py | 2 -- skimage/filter/rank/generic.py | 5 +++++ skimage/filter/rank/percentile.py | 3 --- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/skimage/filter/rank/bilateral.py b/skimage/filter/rank/bilateral.py index 834a24a8..6e11d6e5 100644 --- a/skimage/filter/rank/bilateral.py +++ b/skimage/filter/rank/bilateral.py @@ -22,8 +22,6 @@ References import numpy as np from skimage import img_as_ubyte -from ... import get_log -log = get_log() from . import bilateral_cy from .generic import _handle_input diff --git a/skimage/filter/rank/generic.py b/skimage/filter/rank/generic.py index e09bfba5..167d3aa0 100644 --- a/skimage/filter/rank/generic.py +++ b/skimage/filter/rank/generic.py @@ -55,6 +55,11 @@ def _handle_input(image, selem, out, mask): else: max_bin = max(4, image.max()) + bitdepth = int(np.log2(max_bin)) + if bitdepth > 10: + log.warn("Bitdepth of %d may result in bad rank filter " + "performance due to large number of bins." % bitdepth) + return image, selem, out, mask, max_bin diff --git a/skimage/filter/rank/percentile.py b/skimage/filter/rank/percentile.py index 3b0a04ba..10c4bb9e 100644 --- a/skimage/filter/rank/percentile.py +++ b/skimage/filter/rank/percentile.py @@ -22,9 +22,6 @@ References """ import numpy as np -from skimage import img_as_ubyte -from ... import get_log -log = get_log() from . import percentile_cy from .generic import _handle_input From 54c73fae066f7254f4bfef8b17b490772020b920 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 7 Jul 2013 18:20:00 +0200 Subject: [PATCH 274/736] Replace log warning with UserWarning --- skimage/filter/rank/generic.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/skimage/filter/rank/generic.py b/skimage/filter/rank/generic.py index 167d3aa0..44a4e05b 100644 --- a/skimage/filter/rank/generic.py +++ b/skimage/filter/rank/generic.py @@ -15,10 +15,9 @@ References """ +import warnings import numpy as np from skimage import img_as_ubyte, img_as_uint -from ... import get_log -log = get_log() from . import generic_cy @@ -57,8 +56,8 @@ def _handle_input(image, selem, out, mask): bitdepth = int(np.log2(max_bin)) if bitdepth > 10: - log.warn("Bitdepth of %d may result in bad rank filter " - "performance due to large number of bins." % bitdepth) + warnings.warn("Bitdepth of %d may result in bad rank filter " + "performance due to large number of bins." % bitdepth) return image, selem, out, mask, max_bin From 658201f8f674604432b726303ef2156de6abceea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 7 Jul 2013 18:29:42 +0200 Subject: [PATCH 275/736] Rename mean_subtraction, morph_contr_enh to subtract_mean and enhance_contrast --- skimage/filter/rank/__init__.py | 14 ++++---- skimage/filter/rank/generic.py | 18 +++++----- skimage/filter/rank/generic_cy.pyx | 48 +++++++++++++------------- skimage/filter/rank/percentile.py | 28 +++++++-------- skimage/filter/rank/percentile_cy.pyx | 42 +++++++++++----------- skimage/filter/rank/tests/test_rank.py | 10 +++--- 6 files changed, 80 insertions(+), 80 deletions(-) diff --git a/skimage/filter/rank/__init__.py b/skimage/filter/rank/__init__.py index 9ad816ce..023127e7 100644 --- a/skimage/filter/rank/__init__.py +++ b/skimage/filter/rank/__init__.py @@ -1,9 +1,9 @@ from .generic import (autolevel, bottomhat, equalize, gradient, maximum, mean, - meansubtraction, median, minimum, modal, morph_contr_enh, + subtract_mean, median, minimum, modal, enhance_contrast, pop, threshold, tophat, noise_filter, entropy, otsu) from .percentile import (percentile_autolevel, percentile_gradient, - percentile_mean, percentile_mean_subtraction, - percentile_morph_contr_enh, percentile, + percentile_mean, percentile_subtract_mean, + percentile_enhance_contrast, percentile, percentile_pop, percentile_threshold) from .bilateral import bilateral_mean, bilateral_pop @@ -14,11 +14,11 @@ __all__ = ['autolevel', 'gradient', 'maximum', 'mean', - 'meansubtraction', + 'subtract_mean', 'median', 'minimum', 'modal', - 'morph_contr_enh', + 'enhance_contrast', 'pop', 'threshold', 'tophat', @@ -28,8 +28,8 @@ __all__ = ['autolevel', 'percentile_autolevel', 'percentile_gradient', 'percentile_mean', - 'percentile_mean_subtraction', - 'percentile_morph_contr_enh', + 'percentile_subtract_mean', + 'percentile_enhance_contrast', 'percentile', 'percentile_pop', 'percentile_threshold', diff --git a/skimage/filter/rank/generic.py b/skimage/filter/rank/generic.py index 44a4e05b..f22dcc4c 100644 --- a/skimage/filter/rank/generic.py +++ b/skimage/filter/rank/generic.py @@ -23,7 +23,7 @@ from . import generic_cy __all__ = ['autolevel', 'bottomhat', 'equalize', 'gradient', 'maximum', 'mean', - 'meansubtraction', 'median', 'minimum', 'modal', 'morph_contr_enh', + 'subtract_mean', 'median', 'minimum', 'modal', 'enhance_contrast', 'pop', 'threshold', 'tophat', 'noise_filter', 'entropy', 'otsu'] @@ -294,7 +294,7 @@ def mean(image, selem, out=None, mask=None, shift_x=False, shift_y=False): mask=mask, shift_x=shift_x, shift_y=shift_y) -def meansubtraction(image, selem, out=None, mask=None, shift_x=False, +def subtract_mean(image, selem, out=None, mask=None, shift_x=False, shift_y=False): """Return image subtracted from its local mean. @@ -317,11 +317,11 @@ def meansubtraction(image, selem, out=None, mask=None, shift_x=False, Returns ------- out : ndarray (same dtype as input image) - The result of the local meansubtraction. + The result of the local mean subtraction. """ - return _apply(generic_cy._meansubtraction, image, selem, + return _apply(generic_cy._subtract_mean, image, selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) @@ -434,7 +434,7 @@ def modal(image, selem, out=None, mask=None, shift_x=False, shift_y=False): out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) -def morph_contr_enh(image, selem, out=None, mask=None, shift_x=False, +def enhance_contrast(image, selem, out=None, mask=None, shift_x=False, shift_y=False): """Enhance an image replacing each pixel by the local maximum if pixel greylevel is closest to maximimum than local minimum OR local minimum @@ -459,21 +459,21 @@ def morph_contr_enh(image, selem, out=None, mask=None, shift_x=False, Returns ------- out : ndarray (same dtype as input image) - The result of the local morph_contr_enh. + The result of the local enhance_contrast. Examples -------- >>> from skimage import data >>> from skimage.morphology import disk - >>> from skimage.filter.rank import morph_contr_enh + >>> from skimage.filter.rank import enhance_contrast >>> # Load test image >>> ima = data.camera() >>> # Local mean - >>> avg = morph_contr_enh(ima, disk(20)) + >>> avg = enhance_contrast(ima, disk(20)) """ - return _apply(generic_cy._morph_contr_enh, image, selem, + return _apply(generic_cy._enhance_contrast, image, selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) diff --git a/skimage/filter/rank/generic_cy.pyx b/skimage/filter/rank/generic_cy.pyx index 0e972c30..ffb52cc8 100644 --- a/skimage/filter/rank/generic_cy.pyx +++ b/skimage/filter/rank/generic_cy.pyx @@ -122,11 +122,11 @@ cdef inline dtype_t _kernel_mean(Py_ssize_t* histo, float pop, dtype_t g, return (0) -cdef inline dtype_t _kernel_meansubtraction(Py_ssize_t* histo, float pop, - dtype_t g, Py_ssize_t max_bin, - Py_ssize_t mid_bin, float p0, - float p1, Py_ssize_t s0, - Py_ssize_t s1): +cdef inline dtype_t _kernel_subtract_mean(Py_ssize_t* histo, float pop, + dtype_t g, Py_ssize_t max_bin, + Py_ssize_t mid_bin, float p0, + float p1, Py_ssize_t s0, + Py_ssize_t s1): cdef Py_ssize_t i cdef Py_ssize_t mean = 0 @@ -189,11 +189,11 @@ cdef inline dtype_t _kernel_modal(Py_ssize_t* histo, float pop, dtype_t g, return (0) -cdef inline dtype_t _kernel_morph_contr_enh(Py_ssize_t* histo, float pop, - dtype_t g, Py_ssize_t max_bin, - Py_ssize_t mid_bin, float p0, - float p1, Py_ssize_t s0, - Py_ssize_t s1): +cdef inline dtype_t _kernel_enhance_contrast(Py_ssize_t* histo, float pop, + dtype_t g, Py_ssize_t max_bin, + Py_ssize_t mid_bin, float p0, + float p1, Py_ssize_t s0, + Py_ssize_t s1): cdef Py_ssize_t i, imin, imax @@ -422,17 +422,17 @@ def _mean(dtype_t[:, ::1] image, shift_x, shift_y, 0, 0, 0, 0, max_bin) -def _meansubtraction(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask, - dtype_t[:, ::1] out, - char shift_x, char shift_y, Py_ssize_t max_bin): +def _subtract_mean(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t[:, ::1] out, + char shift_x, char shift_y, Py_ssize_t max_bin): if dtype_t is uint8_t: - _core[uint8_t](_kernel_meansubtraction[uint8_t], image, selem, mask, + _core[uint8_t](_kernel_subtract_mean[uint8_t], image, selem, mask, out, shift_x, shift_y, 0, 0, 0, 0, max_bin) elif dtype_t is uint16_t: - _core[uint16_t](_kernel_meansubtraction[uint16_t], image, selem, mask, + _core[uint16_t](_kernel_subtract_mean[uint16_t], image, selem, mask, out, shift_x, shift_y, 0, 0, 0, 0, max_bin) @@ -464,17 +464,17 @@ def _minimum(dtype_t[:, ::1] image, shift_x, shift_y, 0, 0, 0, 0, max_bin) -def _morph_contr_enh(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask, - dtype_t[:, ::1] out, - char shift_x, char shift_y, Py_ssize_t max_bin): +def _enhance_contrast(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t[:, ::1] out, + char shift_x, char shift_y, Py_ssize_t max_bin): if dtype_t is uint8_t: - _core[uint8_t](_kernel_morph_contr_enh[uint8_t], image, selem, mask, + _core[uint8_t](_kernel_enhance_contrast[uint8_t], image, selem, mask, out, shift_x, shift_y, 0, 0, 0, 0, max_bin) elif dtype_t is uint16_t: - _core[uint16_t](_kernel_morph_contr_enh[uint16_t], image, selem, mask, + _core[uint16_t](_kernel_enhance_contrast[uint16_t], image, selem, mask, out, shift_x, shift_y, 0, 0, 0, 0, max_bin) diff --git a/skimage/filter/rank/percentile.py b/skimage/filter/rank/percentile.py index 10c4bb9e..73929b0a 100644 --- a/skimage/filter/rank/percentile.py +++ b/skimage/filter/rank/percentile.py @@ -28,8 +28,8 @@ from .generic import _handle_input __all__ = ['percentile_autolevel', 'percentile_gradient', - 'percentile_mean', 'percentile_mean_subtraction', - 'percentile_morph_contr_enh', 'percentile', 'percentile_pop', + 'percentile_mean', 'percentile_subtract_mean', + 'percentile_enhance_contrast', 'percentile', 'percentile_pop', 'percentile_threshold'] @@ -157,11 +157,11 @@ def percentile_mean(image, selem, out=None, mask=None, shift_x=False, shift_y=shift_y, p0=p0, p1=p1) -def percentile_mean_subtraction(image, selem, out=None, mask=None, +def percentile_subtract_mean(image, selem, out=None, mask=None, shift_x=False, shift_y=False, p0=0, p1=1): - """Return greyscale local mean_subtraction of an image. + """Return greyscale local subtract_mean of an image. - mean_subtraction is computed on the given structuring element. Only levels + subtract_mean is computed on the given structuring element. Only levels between percentiles [p0, p1] are used. Parameters @@ -185,21 +185,21 @@ def percentile_mean_subtraction(image, selem, out=None, mask=None, Returns ------- - local mean_subtraction : ndarray (same dtype as input) - The result of the local mean_subtraction. + local subtract_mean : ndarray (same dtype as input) + The result of the local subtract_mean. """ - return _apply(percentile_cy._mean_subtraction, + return _apply(percentile_cy._subtract_mean, image, selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y, p0=p0, p1=p1) -def percentile_morph_contr_enh(image, selem, out=None, mask=None, +def percentile_enhance_contrast(image, selem, out=None, mask=None, shift_x=False, shift_y=False, p0=0, p1=1): - """Return greyscale local morph_contr_enh of an image. + """Return greyscale local enhance_contrast of an image. - morph_contr_enh is computed on the given structuring element. Only levels + enhance_contrast is computed on the given structuring element. Only levels between percentiles [p0, p1] are used. Parameters @@ -223,12 +223,12 @@ def percentile_morph_contr_enh(image, selem, out=None, mask=None, Returns ------- - local morph_contr_enh : ndarray (same dtype as input) - The result of the local morph_contr_enh. + local enhance_contrast : ndarray (same dtype as input) + The result of the local enhance_contrast. """ - return _apply(percentile_cy._morph_contr_enh, + return _apply(percentile_cy._enhance_contrast, image, selem, out=out, mask=mask, shift_x=shift_x, shift_y=shift_y, p0=p0, p1=p1) diff --git a/skimage/filter/rank/percentile_cy.pyx b/skimage/filter/rank/percentile_cy.pyx index 6df271fa..bb4419de 100644 --- a/skimage/filter/rank/percentile_cy.pyx +++ b/skimage/filter/rank/percentile_cy.pyx @@ -91,11 +91,11 @@ cdef inline dtype_t _kernel_mean(Py_ssize_t* histo, float pop, dtype_t g, return (0) -cdef inline dtype_t _kernel_mean_subtraction(Py_ssize_t* histo, float pop, - dtype_t g, Py_ssize_t max_bin, - Py_ssize_t mid_bin, float p0, - float p1, Py_ssize_t s0, - Py_ssize_t s1): +cdef inline dtype_t _kernel_subtract_mean(Py_ssize_t* histo, float pop, + dtype_t g, Py_ssize_t max_bin, + Py_ssize_t mid_bin, float p0, + float p1, Py_ssize_t s0, + Py_ssize_t s1): cdef Py_ssize_t i, sum, mean, n @@ -116,11 +116,11 @@ cdef inline dtype_t _kernel_mean_subtraction(Py_ssize_t* histo, float pop, return (0) -cdef inline dtype_t _kernel_morph_contr_enh(Py_ssize_t* histo, float pop, - dtype_t g, Py_ssize_t max_bin, - Py_ssize_t mid_bin, float p0, - float p1, Py_ssize_t s0, - Py_ssize_t s1): +cdef inline dtype_t _kernel_enhance_contrast(Py_ssize_t* histo, float pop, + dtype_t g, Py_ssize_t max_bin, + Py_ssize_t mid_bin, float p0, + float p1, Py_ssize_t s0, + Py_ssize_t s1): cdef Py_ssize_t i, imin, imax, sum, delta @@ -252,22 +252,22 @@ def _mean(dtype_t[:, ::1] image, shift_x, shift_y, p0, p1, 0, 0, max_bin) -def _mean_subtraction(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask, - dtype_t[:, ::1] out, - char shift_x, char shift_y, float p0, float p1, - Py_ssize_t max_bin): +def _subtract_mean(dtype_t[:, ::1] image, + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t[:, ::1] out, + char shift_x, char shift_y, float p0, float p1, + Py_ssize_t max_bin): if dtype_t is uint8_t: - _core[uint8_t](_kernel_mean_subtraction[uint8_t], image, selem, mask, + _core[uint8_t](_kernel_subtract_mean[uint8_t], image, selem, mask, out, shift_x, shift_y, p0, p1, 0, 0, max_bin) elif dtype_t is uint16_t: - _core[uint16_t](_kernel_mean_subtraction[uint16_t], image, selem, mask, + _core[uint16_t](_kernel_subtract_mean[uint16_t], image, selem, mask, out, shift_x, shift_y, p0, p1, 0, 0, max_bin) -def _morph_contr_enh(dtype_t[:, ::1] image, +def _enhance_contrast(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, dtype_t[:, ::1] out, @@ -275,10 +275,10 @@ def _morph_contr_enh(dtype_t[:, ::1] image, Py_ssize_t max_bin): if dtype_t is uint8_t: - _core[uint8_t](_kernel_morph_contr_enh[uint8_t], image, selem, mask, + _core[uint8_t](_kernel_enhance_contrast[uint8_t], image, selem, mask, out, shift_x, shift_y, p0, p1, 0, 0, max_bin) elif dtype_t is uint16_t: - _core[uint16_t](_kernel_morph_contr_enh[uint16_t], image, selem, mask, + _core[uint16_t](_kernel_enhance_contrast[uint16_t], image, selem, mask, out, shift_x, shift_y, p0, p1, 0, 0, max_bin) diff --git a/skimage/filter/rank/tests/test_rank.py b/skimage/filter/rank/tests/test_rank.py index f55d3522..166fe67d 100644 --- a/skimage/filter/rank/tests/test_rank.py +++ b/skimage/filter/rank/tests/test_rank.py @@ -183,7 +183,7 @@ def test_compare_ubyte_vs_float(): image_float = img_as_float(image_uint) methods = ['autolevel', 'bottomhat', 'equalize', 'gradient', 'threshold', - 'meansubtraction', 'morph_contr_enh', 'pop', 'tophat'] + 'subtract_mean', 'enhance_contrast', 'pop', 'tophat'] for method in methods: func = getattr(rank, method) @@ -205,8 +205,8 @@ def test_compare_8bit_unsigned_vs_signed(): assert_array_equal(image_u, img_as_ubyte(image_s)) methods = ['autolevel', 'bottomhat', 'equalize', 'gradient', 'maximum', - 'mean', 'meansubtraction', 'median', 'minimum', 'modal', - 'morph_contr_enh', 'pop', 'threshold', 'tophat'] + 'mean', 'subtract_mean', 'median', 'minimum', 'modal', + 'enhance_contrast', 'pop', 'threshold', 'tophat'] for method in methods: func = getattr(rank, method) @@ -224,8 +224,8 @@ def test_compare_8bit_vs_16bit(): assert_array_equal(image8, image16) methods = ['autolevel', 'bottomhat', 'equalize', 'gradient', 'maximum', - 'mean', 'meansubtraction', 'median', 'minimum', 'modal', - 'morph_contr_enh', 'pop', 'threshold', 'tophat'] + 'mean', 'subtract_mean', 'median', 'minimum', 'modal', + 'enhance_contrast', 'pop', 'threshold', 'tophat'] for method in methods: func = getattr(rank, method) From ed21622cafd60a744f287f339745d75dd9480595 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 7 Jul 2013 18:32:18 +0200 Subject: [PATCH 276/736] Use consistent description of output image --- skimage/filter/rank/bilateral.py | 8 ++++---- skimage/filter/rank/generic.py | 32 +++++++++++++++---------------- skimage/filter/rank/percentile.py | 32 +++++++++++++++---------------- 3 files changed, 36 insertions(+), 36 deletions(-) diff --git a/skimage/filter/rank/bilateral.py b/skimage/filter/rank/bilateral.py index 6e11d6e5..560bc68c 100644 --- a/skimage/filter/rank/bilateral.py +++ b/skimage/filter/rank/bilateral.py @@ -76,8 +76,8 @@ def bilateral_mean(image, selem, out=None, mask=None, shift_x=False, Returns ------- - out : ndarray (same dtype as input) - The result of the local bilateral mean. + out : ndarray (same dtype as input image) + Output image. See also -------- @@ -126,8 +126,8 @@ def bilateral_pop(image, selem, out=None, mask=None, shift_x=False, Returns ------- - out : ndarray (same dtype as input) - the local number of pixels inside the bilateral neighborhood + out : ndarray (same dtype as input image) + Output image. Examples -------- diff --git a/skimage/filter/rank/generic.py b/skimage/filter/rank/generic.py index f22dcc4c..595a55ee 100644 --- a/skimage/filter/rank/generic.py +++ b/skimage/filter/rank/generic.py @@ -94,7 +94,7 @@ def autolevel(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Returns ------- out : ndarray (same dtype as input image) - The result of the local autolevel. + Output image. Examples -------- @@ -164,7 +164,7 @@ def equalize(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Returns ------- out : ndarray (same dtype as input image) - The result of the local equalize. + Output image. Examples -------- @@ -206,7 +206,7 @@ def gradient(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Returns ------- out : ndarray (same dtype as input image) - The local gradient. + Output image. """ @@ -237,7 +237,7 @@ def maximum(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Returns ------- out : ndarray (same dtype as input image) - The local maximum. + Output image. See also -------- @@ -276,7 +276,7 @@ def mean(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Returns ------- out : ndarray (same dtype as input image) - The local mean. + Output image. Examples -------- @@ -317,7 +317,7 @@ def subtract_mean(image, selem, out=None, mask=None, shift_x=False, Returns ------- out : ndarray (same dtype as input image) - The result of the local mean subtraction. + Output image. """ @@ -347,7 +347,7 @@ def median(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Returns ------- out : ndarray (same dtype as input image) - The local median. + Output image. Examples -------- @@ -387,7 +387,7 @@ def minimum(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Returns ------- out : ndarray (same dtype as input image) - The local minimum. + Output image. See also -------- @@ -426,7 +426,7 @@ def modal(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Returns ------- out : ndarray (same dtype as input image) - The local modal. + Output image. """ @@ -457,7 +457,7 @@ def enhance_contrast(image, selem, out=None, mask=None, shift_x=False, structuring element). Returns - ------- + Output image. out : ndarray (same dtype as input image) The result of the local enhance_contrast. @@ -500,7 +500,7 @@ def pop(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Returns ------- out : ndarray (same dtype as input image) - The number of pixels belonging to the neighborhood. + Output image. Examples -------- @@ -547,7 +547,7 @@ def threshold(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Returns ------- out : ndarray (same dtype as input image) - The result of the local threshold. + Output image. Examples -------- @@ -594,7 +594,7 @@ def tophat(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Returns ------- out : ndarray (same dtype as input image) - The image tophat. + Output image. """ @@ -630,7 +630,7 @@ def noise_filter(image, selem, out=None, mask=None, shift_x=False, Returns ------- out : ndarray (same dtype as input image) - The image noise. + Output image. """ @@ -669,7 +669,7 @@ def entropy(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Returns ------- out : ndarray (same dtype as input image) - Entropy of image. + Output image. References ---------- @@ -712,7 +712,7 @@ def otsu(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Returns ------- out : ndarray (same dtype as input image) - Otsu's threshold values. + Output image. References ---------- diff --git a/skimage/filter/rank/percentile.py b/skimage/filter/rank/percentile.py index 73929b0a..b01d314d 100644 --- a/skimage/filter/rank/percentile.py +++ b/skimage/filter/rank/percentile.py @@ -71,8 +71,8 @@ def percentile_autolevel(image, selem, out=None, mask=None, shift_x=False, Returns ------- - local autolevel : ndarray (same dtype as input) - The result of the local autolevel. + out : ndarray (same dtype as input image) + Output image. """ @@ -109,8 +109,8 @@ def percentile_gradient(image, selem, out=None, mask=None, shift_x=False, Returns ------- - local percentile_gradient : ndarray (same dtype as input) - The result of the local percentile_gradient. + out : ndarray (same dtype as input image) + Output image. """ @@ -147,8 +147,8 @@ def percentile_mean(image, selem, out=None, mask=None, shift_x=False, Returns ------- - local mean : ndarray (same dtype as input) - The result of the local mean. + out : ndarray (same dtype as input image) + Output image. """ @@ -185,8 +185,8 @@ def percentile_subtract_mean(image, selem, out=None, mask=None, Returns ------- - local subtract_mean : ndarray (same dtype as input) - The result of the local subtract_mean. + out : ndarray (same dtype as input image) + Output image. """ @@ -223,8 +223,8 @@ def percentile_enhance_contrast(image, selem, out=None, mask=None, Returns ------- - local enhance_contrast : ndarray (same dtype as input) - The result of the local enhance_contrast. + out : ndarray (same dtype as input image) + Output image. """ @@ -260,8 +260,8 @@ def percentile(image, selem, out=None, mask=None, shift_x=False, shift_y=False, Returns ------- - local percentile : ndarray (same dtype as input) - The result of the local percentile. + out : ndarray (same dtype as input image) + Output image. """ @@ -298,8 +298,8 @@ def percentile_pop(image, selem, out=None, mask=None, shift_x=False, Returns ------- - local pop : ndarray (same dtype as input) - The result of the local pop. + out : ndarray (same dtype as input image) + Output image. """ @@ -335,8 +335,8 @@ def percentile_threshold(image, selem, out=None, mask=None, shift_x=False, p0 : float in [0, ..., 1] Set the percentile value. - Returns - ------- + out : ndarray (same dtype as input image) + Output image. local threshold : ndarray (same dtype as input) The result of the local threshold. From 6ee96054c9633a662432e52572788d4840c92013 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 7 Jul 2013 18:42:35 +0200 Subject: [PATCH 277/736] Append percentile, bilateral function name part --- skimage/filter/rank/__init__.py | 32 +++++++++++++------------- skimage/filter/rank/bilateral.py | 6 ++--- skimage/filter/rank/percentile.py | 32 +++++++++++++------------- skimage/filter/rank/tests/test_rank.py | 20 ++++++++-------- 4 files changed, 45 insertions(+), 45 deletions(-) diff --git a/skimage/filter/rank/__init__.py b/skimage/filter/rank/__init__.py index 023127e7..b25abb8c 100644 --- a/skimage/filter/rank/__init__.py +++ b/skimage/filter/rank/__init__.py @@ -1,37 +1,37 @@ from .generic import (autolevel, bottomhat, equalize, gradient, maximum, mean, subtract_mean, median, minimum, modal, enhance_contrast, pop, threshold, tophat, noise_filter, entropy, otsu) -from .percentile import (percentile_autolevel, percentile_gradient, - percentile_mean, percentile_subtract_mean, - percentile_enhance_contrast, percentile, - percentile_pop, percentile_threshold) -from .bilateral import bilateral_mean, bilateral_pop +from .percentile import (autolevel_percentile, gradient_percentile, + mean_percentile, subtract_mean_percentile, + enhance_contrast_percentile, percentile, + pop_percentile, threshold_percentile) +from .bilateral import mean_bilateral, pop_bilateral __all__ = ['autolevel', + 'autolevel_percentile', 'bottomhat', 'equalize', 'gradient', + 'gradient_percentile', 'maximum', 'mean', + 'mean_percentile', + 'mean_bilateral', 'subtract_mean', + 'subtract_mean_percentile', 'median', 'minimum', 'modal', 'enhance_contrast', + 'enhance_contrast_percentile', 'pop', + 'pop_percentile', + 'pop_bilateral', 'threshold', + 'threshold_percentile', 'tophat', 'noise_filter', 'entropy', - 'otsu', - 'percentile_autolevel', - 'percentile_gradient', - 'percentile_mean', - 'percentile_subtract_mean', - 'percentile_enhance_contrast', - 'percentile', - 'percentile_pop', - 'percentile_threshold', - 'bilateral_mean', - 'bilateral_pop'] + 'otsu' + 'percentile'] diff --git a/skimage/filter/rank/bilateral.py b/skimage/filter/rank/bilateral.py index 560bc68c..f0bc2b38 100644 --- a/skimage/filter/rank/bilateral.py +++ b/skimage/filter/rank/bilateral.py @@ -27,7 +27,7 @@ from . import bilateral_cy from .generic import _handle_input -__all__ = ['bilateral_mean', 'bilateral_pop'] +__all__ = ['mean_bilateral', 'pop_bilateral'] def _apply(func, image, selem, out, mask, shift_x, shift_y, s0, s1): @@ -40,7 +40,7 @@ def _apply(func, image, selem, out, mask, shift_x, shift_y, s0, s1): return out -def bilateral_mean(image, selem, out=None, mask=None, shift_x=False, +def mean_bilateral(image, selem, out=None, mask=None, shift_x=False, shift_y=False, s0=10, s1=10): """Apply a flat kernel bilateral filter. @@ -99,7 +99,7 @@ def bilateral_mean(image, selem, out=None, mask=None, shift_x=False, mask=mask, shift_x=shift_x, shift_y=shift_y, s0=s0, s1=s1) -def bilateral_pop(image, selem, out=None, mask=None, shift_x=False, +def pop_bilateral(image, selem, out=None, mask=None, shift_x=False, shift_y=False, s0=10, s1=10): """Return the number (population) of pixels actually inside the bilateral neighborhood, i.e. being inside the structuring element AND having a gray diff --git a/skimage/filter/rank/percentile.py b/skimage/filter/rank/percentile.py index b01d314d..e91e0d82 100644 --- a/skimage/filter/rank/percentile.py +++ b/skimage/filter/rank/percentile.py @@ -1,6 +1,6 @@ """Inferior and superior ranks, provided by the user, are passed to the kernel function to provide a softer version of the rank filters. E.g. -percentile_autolevel will stretch image levels between percentile [p0, p1] +``autolevel_percentile`` will stretch image levels between percentile [p0, p1] instead of using [min, max]. It means that isolated bright or dark pixels will not produce halos. @@ -27,10 +27,10 @@ from . import percentile_cy from .generic import _handle_input -__all__ = ['percentile_autolevel', 'percentile_gradient', - 'percentile_mean', 'percentile_subtract_mean', - 'percentile_enhance_contrast', 'percentile', 'percentile_pop', - 'percentile_threshold'] +__all__ = ['autolevel_percentile', 'gradient_percentile', + 'mean_percentile', 'subtract_mean_percentile', + 'enhance_contrast_percentile', 'percentile', 'pop_percentile', + 'threshold_percentile'] def _apply(func, image, selem, out, mask, shift_x, shift_y, p0, p1): @@ -43,7 +43,7 @@ def _apply(func, image, selem, out, mask, shift_x, shift_y, p0, p1): return out -def percentile_autolevel(image, selem, out=None, mask=None, shift_x=False, +def autolevel_percentile(image, selem, out=None, mask=None, shift_x=False, shift_y=False, p0=0, p1=1): """Return greyscale local autolevel of an image. @@ -81,11 +81,11 @@ def percentile_autolevel(image, selem, out=None, mask=None, shift_x=False, shift_y=shift_y, p0=p0, p1=p1) -def percentile_gradient(image, selem, out=None, mask=None, shift_x=False, +def gradient_percentile(image, selem, out=None, mask=None, shift_x=False, shift_y=False, p0=0, p1=1): - """Return greyscale local percentile_gradient of an image. + """Return greyscale local gradient of an image. - percentile_gradient is computed on the given structuring element. Only + gradient is computed on the given structuring element. Only levels between percentiles [p0, p1] are used. Parameters @@ -119,7 +119,7 @@ def percentile_gradient(image, selem, out=None, mask=None, shift_x=False, shift_y=shift_y, p0=p0, p1=p1) -def percentile_mean(image, selem, out=None, mask=None, shift_x=False, +def mean_percentile(image, selem, out=None, mask=None, shift_x=False, shift_y=False, p0=0, p1=1): """Return greyscale local mean of an image. @@ -157,8 +157,8 @@ def percentile_mean(image, selem, out=None, mask=None, shift_x=False, shift_y=shift_y, p0=p0, p1=p1) -def percentile_subtract_mean(image, selem, out=None, mask=None, - shift_x=False, shift_y=False, p0=0, p1=1): +def subtract_mean_percentile(image, selem, out=None, mask=None, + shift_x=False, shift_y=False, p0=0, p1=1): """Return greyscale local subtract_mean of an image. subtract_mean is computed on the given structuring element. Only levels @@ -195,8 +195,8 @@ def percentile_subtract_mean(image, selem, out=None, mask=None, shift_y=shift_y, p0=p0, p1=p1) -def percentile_enhance_contrast(image, selem, out=None, mask=None, - shift_x=False, shift_y=False, p0=0, p1=1): +def enhance_contrast_percentile(image, selem, out=None, mask=None, + shift_x=False, shift_y=False, p0=0, p1=1): """Return greyscale local enhance_contrast of an image. enhance_contrast is computed on the given structuring element. Only levels @@ -270,7 +270,7 @@ def percentile(image, selem, out=None, mask=None, shift_x=False, shift_y=False, shift_y=shift_y, p0=p0, p1=0.) -def percentile_pop(image, selem, out=None, mask=None, shift_x=False, +def pop_percentile(image, selem, out=None, mask=None, shift_x=False, shift_y=False, p0=0, p1=1): """Return greyscale local pop of an image. @@ -308,7 +308,7 @@ def percentile_pop(image, selem, out=None, mask=None, shift_x=False, shift_y=shift_y, p0=p0, p1=p1) -def percentile_threshold(image, selem, out=None, mask=None, shift_x=False, +def threshold_percentile(image, selem, out=None, mask=None, shift_x=False, shift_y=False, p0=0): """Return greyscale local threshold of an image. diff --git a/skimage/filter/rank/tests/test_rank.py b/skimage/filter/rank/tests/test_rank.py index 166fe67d..d2906f98 100644 --- a/skimage/filter/rank/tests/test_rank.py +++ b/skimage/filter/rank/tests/test_rank.py @@ -33,10 +33,10 @@ def test_random_sizes(): shift_x=+1, shift_y=+1) assert_array_equal(image16.shape, out16.shape) - rank.percentile_mean(image=image16, mask=mask, out=out16, + rank.mean_percentile(image=image16, mask=mask, out=out16, selem=elem, shift_x=0, shift_y=0, p0=.1, p1=.9) assert_array_equal(image16.shape, out16.shape) - rank.percentile_mean(image=image16, mask=mask, out=out16, + rank.mean_percentile(image=image16, mask=mask, out=out16, selem=elem, shift_x=+1, shift_y=+1, p0=.1, p1=.9) assert_array_equal(image16.shape, out16.shape) @@ -78,7 +78,7 @@ def test_bitdepth(): for i in range(5): image = np.ones((100, 100), dtype=np.uint16) * 255 * 2 ** i - r = rank.percentile_mean(image=image, selem=elem, mask=mask, + r = rank.mean_percentile(image=image, selem=elem, mask=mask, out=out, shift_x=0, shift_y=0, p0=.1, p1=.9) @@ -156,7 +156,7 @@ def test_compare_autolevels(): selem = disk(20) loc_autolevel = rank.autolevel(image, selem=selem) - loc_perc_autolevel = rank.percentile_autolevel(image, selem=selem, + loc_perc_autolevel = rank.autolevel_percentile(image, selem=selem, p0=.0, p1=1.) assert_array_equal(loc_autolevel, loc_perc_autolevel) @@ -170,7 +170,7 @@ def test_compare_autolevels_16bit(): selem = disk(20) loc_autolevel = rank.autolevel(image, selem=selem) - loc_perc_autolevel = rank.percentile_autolevel(image, selem=selem, + loc_perc_autolevel = rank.autolevel_percentile(image, selem=selem, p0=.0, p1=1.) assert_array_equal(loc_autolevel, loc_perc_autolevel) @@ -418,7 +418,7 @@ def test_selem_dtypes(): rank.mean(image=image, selem=elem, out=out, mask=mask, shift_x=0, shift_y=0) assert_array_equal(image, out) - rank.percentile_mean(image=image, selem=elem, out=out, mask=mask, + rank.mean_percentile(image=image, selem=elem, out=out, mask=mask, shift_x=0, shift_y=0) assert_array_equal(image, out) @@ -443,10 +443,10 @@ def test_bilateral(): image[10, 11] = 1010 image[10, 9] = 900 - assert rank.bilateral_mean(image, selem, s0=1, s1=1)[10, 10] == 1000 - assert rank.bilateral_pop(image, selem, s0=1, s1=1)[10, 10] == 1 - assert rank.bilateral_mean(image, selem, s0=11, s1=11)[10, 10] == 1005 - assert rank.bilateral_pop(image, selem, s0=11, s1=11)[10, 10] == 2 + assert rank.mean_bilateral(image, selem, s0=1, s1=1)[10, 10] == 1000 + assert rank.pop_bilateral(image, selem, s0=1, s1=1)[10, 10] == 1 + assert rank.mean_bilateral(image, selem, s0=11, s1=11)[10, 10] == 1005 + assert rank.pop_bilateral(image, selem, s0=11, s1=11)[10, 10] == 2 if __name__ == "__main__": From c2939d53080959d1544d9c8b1e224e0c20036877 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Mon, 8 Jul 2013 20:11:36 +0200 Subject: [PATCH 278/736] Add TODO.txt --- TODO.txt | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 TODO.txt diff --git a/TODO.txt b/TODO.txt new file mode 100644 index 00000000..2067d036 --- /dev/null +++ b/TODO.txt @@ -0,0 +1,14 @@ +Version 0.10 +------------ +* Remove deprecated functions: + - ``skimage.filter.rank.*`` +* Remove deprecated parameter ``epsilon`` of ``skimage.viewer.LineProfile`` + +Version 0.9 +----------- +* Remove deprecated functions + - ``skimage.filter.denoise_tv_chambolle`` + - ``skimage.morphology.is_local_maximum`` + - ``skimage.transform.hough`` + - ``skimage.transform.probabilistic_hough`` + - ``skimage.transform.hough_peaks`` From af752b97d3a91c0d3152a7539bf8aa2eda73e443 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Mon, 8 Jul 2013 20:13:20 +0200 Subject: [PATCH 279/736] Add note about TODO.txt to release instructions --- RELEASE.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/RELEASE.txt b/RELEASE.txt index 80bf4cab..d29143aa 100644 --- a/RELEASE.txt +++ b/RELEASE.txt @@ -1,6 +1,8 @@ How to make a new release of ``skimage`` ======================================== +- Check ``TODO.txt`` for any outstanding tasks. + - Update release notes. - To show a list contributors, run ``doc/release/contributors.sh ``, From e041c27f884895dfd230b9a55aa93f9df68ccf07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Mon, 8 Jul 2013 20:25:25 +0200 Subject: [PATCH 280/736] Deprecate old rank filter functions --- skimage/filter/rank/__init__.py | 35 ++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/skimage/filter/rank/__init__.py b/skimage/filter/rank/__init__.py index b25abb8c..cfd034f1 100644 --- a/skimage/filter/rank/__init__.py +++ b/skimage/filter/rank/__init__.py @@ -7,6 +7,29 @@ from .percentile import (autolevel_percentile, gradient_percentile, pop_percentile, threshold_percentile) from .bilateral import mean_bilateral, pop_bilateral +from skimage._shared.utils import deprecated + + +percentile_autolevel = deprecated('autolevel_percentile')(autolevel_percentile) + +percentile_gradient = deprecated('gradient_percentile')(gradient_percentile) + +percentile_mean = deprecated('mean_percentile')(mean_percentile) +bilateral_mean = deprecated('mean_bilateral')(mean_bilateral) + +meansubtraction = deprecated('subtract_mean')(subtract_mean) +percentile_mean_subtraction = deprecated('subtract_mean_percentile')\ + (subtract_mean_percentile) + +morph_contr_enh = deprecated('enhance_contrast')(enhance_contrast) +percentile_morph_contr_enh = deprecated('enhance_contrast_percentile')\ + (enhance_contrast_percentile) + +percentile_pop = deprecated('pop_percentile')(pop_percentile) +bilateral_pop = deprecated('pop_bilateral')(pop_bilateral) + +percentile_threshold = deprecated('threshold_percentile')(threshold_percentile) + __all__ = ['autolevel', 'autolevel_percentile', @@ -34,4 +57,14 @@ __all__ = ['autolevel', 'noise_filter', 'entropy', 'otsu' - 'percentile'] + 'percentile', + # Deprecated + 'percentile_autolevel', + 'percentile_gradient', + 'percentile_mean', + 'percentile_mean_subtraction', + 'percentile_morph_contr_enh', + 'percentile_pop', + 'percentile_threshold', + 'bilateral_mean', + 'bilateral_pop'] From b9b5efbdf8d769e83fefc0553f0b226aced2644f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Mon, 8 Jul 2013 20:34:40 +0200 Subject: [PATCH 281/736] Replace deprecated function calls in examples --- doc/examples/applications/plot_rank_filters.py | 18 +++++++++--------- doc/examples/plot_rank_mean.py | 4 ++-- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/doc/examples/applications/plot_rank_filters.py b/doc/examples/applications/plot_rank_filters.py index ded335bd..fb69cd40 100644 --- a/doc/examples/applications/plot_rank_filters.py +++ b/doc/examples/applications/plot_rank_filters.py @@ -283,16 +283,16 @@ result. """ -from skimage.filter.rank import percentile_autolevel +from skimage.filter.rank import autolevel_percentile image = data.camera() selem = disk(20) loc_autolevel = autolevel(image, selem=selem) -loc_perc_autolevel0 = percentile_autolevel(image, selem=selem, p0=.00, p1=1.0) -loc_perc_autolevel1 = percentile_autolevel(image, selem=selem, p0=.01, p1=.99) -loc_perc_autolevel2 = percentile_autolevel(image, selem=selem, p0=.05, p1=.95) -loc_perc_autolevel3 = percentile_autolevel(image, selem=selem, p0=.1, p1=.9) +loc_perc_autolevel0 = autolevel_percentile(image, selem=selem, p0=.00, p1=1.0) +loc_perc_autolevel1 = autolevel_percentile(image, selem=selem, p0=.01, p1=.99) +loc_perc_autolevel2 = autolevel_percentile(image, selem=selem, p0=.05, p1=.95) +loc_perc_autolevel3 = autolevel_percentile(image, selem=selem, p0=.1, p1=.9) fig, axes = plt.subplots(nrows=3, figsize=(7, 8)) ax0, ax1, ax2 = axes @@ -321,11 +321,11 @@ otherwise by the minimum local. """ -from skimage.filter.rank import morph_contr_enh +from skimage.filter.rank import enhance_contrast noisy_image = data.camera() -enh = morph_contr_enh(noisy_image, disk(5)) +enh = enhance_contrast(noisy_image, disk(5)) fig = plt.figure(figsize=[10, 7]) plt.subplot(2, 2, 1) @@ -355,11 +355,11 @@ percentile *p0* and *p1* instead of the local minimum and maximum. """ -from skimage.filter.rank import percentile_morph_contr_enh +from skimage.filter.rank import enhance_contrast_percentile noisy_image = data.camera() -penh = percentile_morph_contr_enh(noisy_image, disk(5), p0=.1, p1=.9) +penh = enhance_contrast_percentile(noisy_image, disk(5), p0=.1, p1=.9) fig = plt.figure(figsize=[10, 7]) plt.subplot(2, 2, 1) diff --git a/doc/examples/plot_rank_mean.py b/doc/examples/plot_rank_mean.py index 013535cb..6f16c440 100644 --- a/doc/examples/plot_rank_mean.py +++ b/doc/examples/plot_rank_mean.py @@ -29,8 +29,8 @@ from skimage.filter import rank image = (data.coins()).astype(np.uint16) * 16 selem = disk(20) -percentile_result = rank.percentile_mean(image, selem=selem, p0=.1, p1=.9) -bilateral_result = rank.bilateral_mean(image, selem=selem, s0=500, s1=500) +percentile_result = rank.mean_percentile(image, selem=selem, p0=.1, p1=.9) +bilateral_result = rank.mean_bilateral(image, selem=selem, s0=500, s1=500) normal_result = rank.mean(image, selem=selem) From 33a09c3b1a1727e8f63cc2fce926a460bd50c5c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Fri, 12 Jul 2013 22:58:15 +0200 Subject: [PATCH 282/736] Add option to return double output image for rank filters --- skimage/filter/rank/bilateral.py | 9 +- skimage/filter/rank/bilateral_cy.pyx | 58 ++-- skimage/filter/rank/core_cy.pxd | 15 +- skimage/filter/rank/core_cy.pyx | 28 +- skimage/filter/rank/generic.py | 21 +- skimage/filter/rank/generic_cy.pyx | 427 +++++++++++--------------- skimage/filter/rank/percentile.py | 9 +- skimage/filter/rank/percentile_cy.pyx | 250 +++++++-------- 8 files changed, 362 insertions(+), 455 deletions(-) diff --git a/skimage/filter/rank/bilateral.py b/skimage/filter/rank/bilateral.py index f0bc2b38..f1b10fec 100644 --- a/skimage/filter/rank/bilateral.py +++ b/skimage/filter/rank/bilateral.py @@ -11,6 +11,9 @@ The pixel neighborhood is defined by: The kernel is flat (i.e. each pixel belonging to the neighborhood contributes equally). +Result image is 8-/16-bit or double with respect to the input image and the +rank filter operation. + References ---------- @@ -30,9 +33,11 @@ from .generic import _handle_input __all__ = ['mean_bilateral', 'pop_bilateral'] -def _apply(func, image, selem, out, mask, shift_x, shift_y, s0, s1): +def _apply(func, image, selem, out, mask, shift_x, shift_y, s0, s1, + out_dtype=None): - image, selem, out, mask, max_bin = _handle_input(image, selem, out, mask) + image, selem, out, mask, max_bin = _handle_input(image, selem, out, mask, + out_dtype) func(image, selem, shift_x=shift_x, shift_y=shift_y, mask=mask, out=out, max_bin=max_bin, s0=s0, s1=s1) diff --git a/skimage/filter/rank/bilateral_cy.pyx b/skimage/filter/rank/bilateral_cy.pyx index d93fdb3c..b8e0d1e7 100644 --- a/skimage/filter/rank/bilateral_cy.pyx +++ b/skimage/filter/rank/bilateral_cy.pyx @@ -6,14 +6,13 @@ cimport numpy as cnp from libc.math cimport log -from .core_cy cimport uint8_t, uint16_t, dtype_t, _core +from .core_cy cimport dtype_t, dtype_t_out, _core -cdef inline dtype_t _kernel_mean(Py_ssize_t* histo, float pop, - dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): +cdef inline float _kernel_mean(Py_ssize_t* histo, float pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): cdef Py_ssize_t i cdef Py_ssize_t bilat_pop = 0 @@ -25,18 +24,17 @@ cdef inline dtype_t _kernel_mean(Py_ssize_t* histo, float pop, bilat_pop += histo[i] mean += histo[i] * i if bilat_pop: - return (mean / bilat_pop) + return mean / bilat_pop else: - return (0) + return 0 else: - return (0) + return 0 -cdef inline dtype_t _kernel_pop(Py_ssize_t* histo, float pop, - dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): +cdef inline float _kernel_pop(Py_ssize_t* histo, float pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): cdef Py_ssize_t i cdef Py_ssize_t bilat_pop = 0 @@ -45,36 +43,28 @@ cdef inline dtype_t _kernel_pop(Py_ssize_t* histo, float pop, for i in range(max_bin): if (g > (i - s0)) and (g < (i + s1)): bilat_pop += histo[i] - return (bilat_pop) + return bilat_pop else: - return (0) + return 0 def _mean(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, - dtype_t[:, ::1] out, + dtype_t_out[:, ::1] out, char shift_x, char shift_y, Py_ssize_t s0, Py_ssize_t s1, Py_ssize_t max_bin): - if dtype_t is uint8_t: - _core[uint8_t](_kernel_mean[uint8_t], image, selem, mask, out, - shift_x, shift_y, 0, 0, s0, s1, max_bin) - elif dtype_t is uint16_t: - _core[uint16_t](_kernel_mean[uint16_t], image, selem, mask, out, - shift_x, shift_y, 0, 0, s0, s1, max_bin) + _core(_kernel_mean[dtype_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, s0, s1, max_bin) def _pop(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask, - dtype_t[:, ::1] out, - char shift_x, char shift_y, Py_ssize_t s0, Py_ssize_t s1, - Py_ssize_t max_bin): + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, ::1] out, + char shift_x, char shift_y, Py_ssize_t s0, Py_ssize_t s1, + Py_ssize_t max_bin): - if dtype_t is uint8_t: - _core[uint8_t](_kernel_pop[uint8_t], image, selem, mask, out, - shift_x, shift_y, 0, 0, s0, s1, max_bin) - elif dtype_t is uint16_t: - _core[uint16_t](_kernel_pop[uint16_t], image, selem, mask, out, - shift_x, shift_y, 0, 0, s0, s1, max_bin) + _core(_kernel_pop[dtype_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, s0, s1, max_bin) diff --git a/skimage/filter/rank/core_cy.pxd b/skimage/filter/rank/core_cy.pxd index 544368a0..a1e44f98 100644 --- a/skimage/filter/rank/core_cy.pxd +++ b/skimage/filter/rank/core_cy.pxd @@ -1,22 +1,27 @@ -from numpy cimport uint8_t, uint16_t +from numpy cimport uint8_t, uint16_t, double_t ctypedef fused dtype_t: uint8_t uint16_t +ctypedef fused dtype_t_out: + uint8_t + uint16_t + double_t + cdef dtype_t _max(dtype_t a, dtype_t b) cdef dtype_t _min(dtype_t a, dtype_t b) -cdef void _core(dtype_t kernel(Py_ssize_t*, float, dtype_t, - Py_ssize_t, Py_ssize_t, float, - float, Py_ssize_t, Py_ssize_t), +cdef void _core(float kernel(Py_ssize_t*, float, dtype_t, + Py_ssize_t, Py_ssize_t, float, + float, Py_ssize_t, Py_ssize_t), dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, - dtype_t[:, ::1] out, + dtype_t_out[:, ::1] out, char shift_x, char shift_y, float p0, float p1, Py_ssize_t s0, Py_ssize_t s1, diff --git a/skimage/filter/rank/core_cy.pyx b/skimage/filter/rank/core_cy.pyx index 898e1775..854a3ce7 100644 --- a/skimage/filter/rank/core_cy.pyx +++ b/skimage/filter/rank/core_cy.pyx @@ -42,13 +42,13 @@ cdef inline char is_in_mask(Py_ssize_t rows, Py_ssize_t cols, return 0 -cdef void _core(dtype_t kernel(Py_ssize_t*, float, dtype_t, - Py_ssize_t, Py_ssize_t, float, - float, Py_ssize_t, Py_ssize_t), +cdef void _core(float kernel(Py_ssize_t*, float, dtype_t, + Py_ssize_t, Py_ssize_t, float, + float, Py_ssize_t, Py_ssize_t), dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, - dtype_t[:, ::1] out, + dtype_t_out[:, ::1] out, char shift_x, char shift_y, float p0, float p1, Py_ssize_t s0, Py_ssize_t s1, @@ -151,8 +151,8 @@ cdef void _core(dtype_t kernel(Py_ssize_t*, float, dtype_t, r = 0 c = 0 - out[r, c] = kernel(histo, pop, image[r, c], max_bin, mid_bin, - p0, p1, s0, s1) + out[r, c] = kernel(histo, pop, image[r, c], max_bin, mid_bin, + p0, p1, s0, s1) # main loop r = 0 @@ -172,8 +172,8 @@ cdef void _core(dtype_t kernel(Py_ssize_t*, float, dtype_t, if is_in_mask(rows, cols, rr, cc, mask_data): histogram_decrement(histo, &pop, image[rr, cc]) - out[r, c] = kernel(histo, pop, image[r, c], max_bin, - mid_bin, p0, p1, s0, s1) + out[r, c] = kernel(histo, pop, image[r, c], + max_bin, mid_bin, p0, p1, s0, s1) r += 1 # pass to the next row if r >= rows: @@ -192,8 +192,8 @@ cdef void _core(dtype_t kernel(Py_ssize_t*, float, dtype_t, if is_in_mask(rows, cols, rr, cc, mask_data): histogram_decrement(histo, &pop, image[rr, cc]) - out[r, c] = kernel(histo, pop, image[r, c], - max_bin, mid_bin, p0, p1, s0, s1) + out[r, c] = kernel(histo, pop, image[r, c], + max_bin, mid_bin, p0, p1, s0, s1) # ---> east to west for c in range(cols - 2, -1, -1): @@ -209,8 +209,8 @@ cdef void _core(dtype_t kernel(Py_ssize_t*, float, dtype_t, if is_in_mask(rows, cols, rr, cc, mask_data): histogram_decrement(histo, &pop, image[rr, cc]) - out[r, c] = kernel(histo, pop, image[r, c], max_bin, - mid_bin, p0, p1, s0, s1) + out[r, c] = kernel(histo, pop, image[r, c], + max_bin, mid_bin, p0, p1, s0, s1) r += 1 # pass to the next row if r >= rows: @@ -229,8 +229,8 @@ cdef void _core(dtype_t kernel(Py_ssize_t*, float, dtype_t, if is_in_mask(rows, cols, rr, cc, mask_data): histogram_decrement(histo, &pop, image[rr, cc]) - out[r, c] = kernel(histo, pop, image[r, c], max_bin, mid_bin, - p0, p1, s0, s1) + out[r, c] = kernel(histo, pop, image[r, c], + max_bin, mid_bin, p0, p1, s0, s1) # release memory allocated by malloc free(se_e_r) diff --git a/skimage/filter/rank/generic.py b/skimage/filter/rank/generic.py index 595a55ee..0bc8ca50 100644 --- a/skimage/filter/rank/generic.py +++ b/skimage/filter/rank/generic.py @@ -4,7 +4,8 @@ described in [1]_. Input image can be 8-bit or 16-bit, for 16-bit input images, the number of histogram bins is determined from the maximum value present in the image. -Result image is 8- or 16-bit with respect to the input image. +Result image is 8-/16-bit or double with respect to the input image and the +rank filter operation. References ---------- @@ -17,7 +18,7 @@ References import warnings import numpy as np -from skimage import img_as_ubyte, img_as_uint +from skimage import img_as_ubyte from . import generic_cy @@ -27,7 +28,7 @@ __all__ = ['autolevel', 'bottomhat', 'equalize', 'gradient', 'maximum', 'mean', 'pop', 'threshold', 'tophat', 'noise_filter', 'entropy', 'otsu'] -def _handle_input(image, selem, out, mask): +def _handle_input(image, selem, out, mask, out_dtype=None): if image.dtype not in (np.uint8, np.uint16): image = img_as_ubyte(image) @@ -42,7 +43,9 @@ def _handle_input(image, selem, out, mask): mask = np.ascontiguousarray(mask) if out is None: - out = np.empty_like(image, dtype=image.dtype) + if out_dtype is None: + out_dtype = image.dtype + out = np.empty_like(image, dtype=out_dtype) if image is out: raise NotImplementedError("Cannot perform rank operation in place.") @@ -62,9 +65,10 @@ def _handle_input(image, selem, out, mask): return image, selem, out, mask, max_bin -def _apply(func, image, selem, out, mask, shift_x, shift_y): +def _apply(func, image, selem, out, mask, shift_x, shift_y, out_dtype=None): - image, selem, out, mask, max_bin = _handle_input(image, selem, out, mask) + image, selem, out, mask, max_bin = _handle_input(image, selem, out, mask, + out_dtype) func(image, selem, shift_x=shift_x, shift_y=shift_y, mask=mask, out=out, max_bin=max_bin) @@ -668,7 +672,7 @@ def entropy(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Returns ------- - out : ndarray (same dtype as input image) + out : ndarray (double) Output image. References @@ -687,7 +691,8 @@ def entropy(image, selem, out=None, mask=None, shift_x=False, shift_y=False): """ return _apply(generic_cy._entropy, image, selem, - out=out, mask=mask, shift_x=shift_x, shift_y=shift_y) + out=out, mask=mask, shift_x=shift_x, shift_y=shift_y, + out_dtype=np.double) def otsu(image, selem, out=None, mask=None, shift_x=False, shift_y=False): diff --git a/skimage/filter/rank/generic_cy.pyx b/skimage/filter/rank/generic_cy.pyx index ffb52cc8..47cb99da 100644 --- a/skimage/filter/rank/generic_cy.pyx +++ b/skimage/filter/rank/generic_cy.pyx @@ -6,13 +6,13 @@ cimport numpy as cnp from libc.math cimport log -from .core_cy cimport uint8_t, uint16_t, dtype_t, _core +from .core_cy cimport dtype_t, dtype_t_out, _core -cdef inline dtype_t _kernel_autolevel(Py_ssize_t* histo, float pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): +cdef inline float _kernel_autolevel(Py_ssize_t* histo, float pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): cdef Py_ssize_t i, imin, imax, delta @@ -27,17 +27,17 @@ cdef inline dtype_t _kernel_autolevel(Py_ssize_t* histo, float pop, dtype_t g, break delta = imax - imin if delta > 0: - return ((max_bin - 1) * (g - imin) / delta) + return (max_bin - 1) * (g - imin) / delta else: - return (imax - imin) + return imax - imin else: - return (0) + return 0 -cdef inline dtype_t _kernel_bottomhat(Py_ssize_t* histo, float pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): +cdef inline float _kernel_bottomhat(Py_ssize_t* histo, float pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): cdef Py_ssize_t i @@ -45,16 +45,15 @@ cdef inline dtype_t _kernel_bottomhat(Py_ssize_t* histo, float pop, dtype_t g, for i in range(max_bin): if histo[i]: break - - return (g - i) + return g - i else: - return (0) + return 0 -cdef inline dtype_t _kernel_equalize(Py_ssize_t* histo, float pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): +cdef inline float _kernel_equalize(Py_ssize_t* histo, float pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): cdef Py_ssize_t i cdef Py_ssize_t sum = 0 @@ -64,16 +63,15 @@ cdef inline dtype_t _kernel_equalize(Py_ssize_t* histo, float pop, dtype_t g, sum += histo[i] if i >= g: break - - return (((max_bin - 1) * sum) / pop) + return ((max_bin - 1) * sum) / pop else: - return (0) + return 0 -cdef inline dtype_t _kernel_gradient(Py_ssize_t* histo, float pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): +cdef inline float _kernel_gradient(Py_ssize_t* histo, float pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): cdef Py_ssize_t i, imin, imax @@ -86,64 +84,65 @@ cdef inline dtype_t _kernel_gradient(Py_ssize_t* histo, float pop, dtype_t g, if histo[i]: imin = i break - return (imax - imin) + return imax - imin else: - return (0) + return 0 -cdef inline dtype_t _kernel_maximum(Py_ssize_t* histo, float pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): +cdef inline float _kernel_maximum(Py_ssize_t* histo, float pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): cdef Py_ssize_t i if pop: for i in range(max_bin - 1, -1, -1): if histo[i]: - return (i) + return i else: - return (0) + return 0 -cdef inline dtype_t _kernel_mean(Py_ssize_t* histo, float pop, dtype_t g, +cdef inline float _kernel_mean(Py_ssize_t* histo, float pop,dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): + + cdef Py_ssize_t i + cdef Py_ssize_t mean = 0 + + if pop: + for i in range(max_bin): + mean += histo[i] * i + return mean / pop + else: + return 0 + + +cdef inline float _kernel_subtract_mean(Py_ssize_t* histo, float pop, + dtype_t g, + Py_ssize_t max_bin, + Py_ssize_t mid_bin, float p0, + float p1, Py_ssize_t s0, + Py_ssize_t s1): + + cdef Py_ssize_t i + cdef Py_ssize_t mean = 0 + + if pop: + for i in range(max_bin): + mean += histo[i] * i + return (g - mean / pop) / 2. + 127 + else: + return 0 + + +cdef inline float _kernel_median(Py_ssize_t* histo, float pop, dtype_t g, Py_ssize_t max_bin, Py_ssize_t mid_bin, float p0, float p1, Py_ssize_t s0, Py_ssize_t s1): - cdef Py_ssize_t i - cdef Py_ssize_t mean = 0 - - if pop: - for i in range(max_bin): - mean += histo[i] * i - return (mean / pop) - else: - return (0) - - -cdef inline dtype_t _kernel_subtract_mean(Py_ssize_t* histo, float pop, - dtype_t g, Py_ssize_t max_bin, - Py_ssize_t mid_bin, float p0, - float p1, Py_ssize_t s0, - Py_ssize_t s1): - - cdef Py_ssize_t i - cdef Py_ssize_t mean = 0 - - if pop: - for i in range(max_bin): - mean += histo[i] * i - return ((g - mean / pop) / 2. + 127) - else: - return (0) - - -cdef inline dtype_t _kernel_median(Py_ssize_t* histo, float pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): - cdef Py_ssize_t i cdef float sum = pop / 2.0 @@ -152,30 +151,30 @@ cdef inline dtype_t _kernel_median(Py_ssize_t* histo, float pop, dtype_t g, if histo[i]: sum -= histo[i] if sum < 0: - return (i) + return i else: - return (0) + return 0 -cdef inline dtype_t _kernel_minimum(Py_ssize_t* histo, float pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): +cdef inline float _kernel_minimum(Py_ssize_t* histo, float pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): cdef Py_ssize_t i if pop: for i in range(max_bin): if histo[i]: - return (i) + return i else: - return (0) + return 0 -cdef inline dtype_t _kernel_modal(Py_ssize_t* histo, float pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): +cdef inline float _kernel_modal(Py_ssize_t* histo, float pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): cdef Py_ssize_t hmax = 0, imax = 0 @@ -184,16 +183,17 @@ cdef inline dtype_t _kernel_modal(Py_ssize_t* histo, float pop, dtype_t g, if histo[i] > hmax: hmax = histo[i] imax = i - return (imax) + return imax else: - return (0) + return 0 -cdef inline dtype_t _kernel_enhance_contrast(Py_ssize_t* histo, float pop, - dtype_t g, Py_ssize_t max_bin, - Py_ssize_t mid_bin, float p0, - float p1, Py_ssize_t s0, - Py_ssize_t s1): +cdef inline float _kernel_enhance_contrast(Py_ssize_t* histo, float pop, + dtype_t g, + Py_ssize_t max_bin, + Py_ssize_t mid_bin, float p0, + float p1, Py_ssize_t s0, + Py_ssize_t s1): cdef Py_ssize_t i, imin, imax @@ -207,25 +207,25 @@ cdef inline dtype_t _kernel_enhance_contrast(Py_ssize_t* histo, float pop, imin = i break if imax - g < g - imin: - return (imax) + return imax else: - return (imin) + return imin else: - return (0) + return 0 -cdef inline dtype_t _kernel_pop(Py_ssize_t* histo, float pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): +cdef inline float _kernel_pop(Py_ssize_t* histo, float pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): - return (pop) + return pop -cdef inline dtype_t _kernel_threshold(Py_ssize_t* histo, float pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): +cdef inline float _kernel_threshold(Py_ssize_t* histo, float pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): cdef Py_ssize_t i cdef Py_ssize_t mean = 0 @@ -233,15 +233,15 @@ cdef inline dtype_t _kernel_threshold(Py_ssize_t* histo, float pop, dtype_t g, if pop: for i in range(max_bin): mean += histo[i] * i - return (g > (mean / pop)) + return g > (mean / pop) else: - return (0) + return 0 -cdef inline dtype_t _kernel_tophat(Py_ssize_t* histo, float pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): +cdef inline float _kernel_tophat(Py_ssize_t* histo, float pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): cdef Py_ssize_t i @@ -249,23 +249,22 @@ cdef inline dtype_t _kernel_tophat(Py_ssize_t* histo, float pop, dtype_t g, for i in range(max_bin - 1, -1, -1): if histo[i]: break - - return (i - g) + return i - g else: - return (0) + return 0 -cdef inline dtype_t _kernel_noise_filter(Py_ssize_t* histo, float pop, - dtype_t g, Py_ssize_t max_bin, - Py_ssize_t mid_bin, float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): +cdef inline float _kernel_noise_filter(Py_ssize_t* histo, float pop, dtype_t g, + Py_ssize_t max_bin, + Py_ssize_t mid_bin, float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): cdef Py_ssize_t i cdef Py_ssize_t min_i # early stop if at least one pixel of the neighborhood has the same g if histo[g] > 0: - return 0 + return 0 for i in range(g, -1, -1): if histo[i]: @@ -275,35 +274,33 @@ cdef inline dtype_t _kernel_noise_filter(Py_ssize_t* histo, float pop, if histo[i]: break if i - g < min_i: - return (i - g) + return i - g else: - return min_i + return min_i -cdef inline dtype_t _kernel_entropy(Py_ssize_t* histo, float pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): +cdef inline float _kernel_entropy(Py_ssize_t* histo, float pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): cdef Py_ssize_t i cdef float e, p if pop: e = 0. - for i in range(max_bin): p = histo[i] / pop if p > 0: e -= p * log(p) / 0.6931471805599453 - - return e + return e else: - return (0) + return 0 -cdef inline dtype_t _kernel_otsu(Py_ssize_t* histo, float pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): +cdef inline float _kernel_otsu(Py_ssize_t* histo, float pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): cdef Py_ssize_t i cdef Py_ssize_t max_i cdef float P, mu1, mu2, q1, new_q1, sigma_b, max_sigma_b @@ -313,9 +310,9 @@ cdef inline dtype_t _kernel_otsu(Py_ssize_t* histo, float pop, dtype_t g, if pop: for i in range(max_bin): mu += histo[i] * i - mu = (mu / pop) + mu = mu / pop else: - return (0) + return 0 # maximizing the between class variance max_i = 0 @@ -335,242 +332,174 @@ cdef inline dtype_t _kernel_otsu(Py_ssize_t* histo, float pop, dtype_t g, max_i = i q1 = new_q1 - return max_i + return max_i def _autolevel(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, - dtype_t[:, ::1] out, + dtype_t_out[:, ::1] out, char shift_x, char shift_y, Py_ssize_t max_bin): - if dtype_t is uint8_t: - _core[uint8_t](_kernel_autolevel[uint8_t], image, selem, mask, out, - shift_x, shift_y, 0, 0, 0, 0, max_bin) - elif dtype_t is uint16_t: - _core[uint16_t](_kernel_autolevel[uint16_t], image, selem, mask, out, - shift_x, shift_y, 0, 0, 0, 0, max_bin) + _core(_kernel_autolevel[dtype_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) def _bottomhat(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, - dtype_t[:, ::1] out, + dtype_t_out[:, ::1] out, char shift_x, char shift_y, Py_ssize_t max_bin): - if dtype_t is uint8_t: - _core[uint8_t](_kernel_bottomhat[uint8_t], image, selem, mask, out, - shift_x, shift_y, 0, 0, 0, 0, max_bin) - elif dtype_t is uint16_t: - _core[uint16_t](_kernel_bottomhat[uint16_t], image, selem, mask, out, - shift_x, shift_y, 0, 0, 0, 0, max_bin) + _core(_kernel_bottomhat[dtype_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) def _equalize(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, - dtype_t[:, ::1] out, + dtype_t_out[:, ::1] out, char shift_x, char shift_y, Py_ssize_t max_bin): - if dtype_t is uint8_t: - _core[uint8_t](_kernel_equalize[uint8_t], image, selem, mask, out, - shift_x, shift_y, 0, 0, 0, 0, max_bin) - elif dtype_t is uint16_t: - _core[uint16_t](_kernel_equalize[uint16_t], image, selem, mask, out, - shift_x, shift_y, 0, 0, 0, 0, max_bin) + _core(_kernel_equalize[dtype_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) def _gradient(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, - dtype_t[:, ::1] out, + dtype_t_out[:, ::1] out, char shift_x, char shift_y, Py_ssize_t max_bin): - if dtype_t is uint8_t: - _core[uint8_t](_kernel_gradient[uint8_t], image, selem, mask, out, - shift_x, shift_y, 0, 0, 0, 0, max_bin) - elif dtype_t is uint16_t: - _core[uint16_t](_kernel_gradient[uint16_t], image, selem, mask, out, - shift_x, shift_y, 0, 0, 0, 0, max_bin) + _core(_kernel_gradient[dtype_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) def _maximum(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, - dtype_t[:, ::1] out, + dtype_t_out[:, ::1] out, char shift_x, char shift_y, Py_ssize_t max_bin): - if dtype_t is uint8_t: - _core[uint8_t](_kernel_maximum[uint8_t], image, selem, mask, out, - shift_x, shift_y, 0, 0, 0, 0, max_bin) - elif dtype_t is uint16_t: - _core[uint16_t](_kernel_maximum[uint16_t], image, selem, mask, out, - shift_x, shift_y, 0, 0, 0, 0, max_bin) + _core(_kernel_maximum[dtype_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) def _mean(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, - dtype_t[:, ::1] out, + dtype_t_out[:, ::1] out, char shift_x, char shift_y, Py_ssize_t max_bin): - if dtype_t is uint8_t: - _core[uint8_t](_kernel_mean[uint8_t], image, selem, mask, out, - shift_x, shift_y, 0, 0, 0, 0, max_bin) - elif dtype_t is uint16_t: - _core[uint16_t](_kernel_mean[uint16_t], image, selem, mask, out, - shift_x, shift_y, 0, 0, 0, 0, max_bin) + _core(_kernel_mean[dtype_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) def _subtract_mean(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, - dtype_t[:, ::1] out, + dtype_t_out[:, ::1] out, char shift_x, char shift_y, Py_ssize_t max_bin): - if dtype_t is uint8_t: - _core[uint8_t](_kernel_subtract_mean[uint8_t], image, selem, mask, - out, shift_x, shift_y, 0, 0, 0, 0, max_bin) - elif dtype_t is uint16_t: - _core[uint16_t](_kernel_subtract_mean[uint16_t], image, selem, mask, - out, shift_x, shift_y, 0, 0, 0, 0, max_bin) + _core(_kernel_subtract_mean[dtype_t], image, selem, mask, + out, shift_x, shift_y, 0, 0, 0, 0, max_bin) def _median(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, - dtype_t[:, ::1] out, + dtype_t_out[:, ::1] out, char shift_x, char shift_y, Py_ssize_t max_bin): - if dtype_t is uint8_t: - _core[uint8_t](_kernel_median[uint8_t], image, selem, mask, out, - shift_x, shift_y, 0, 0, 0, 0, max_bin) - elif dtype_t is uint16_t: - _core[uint16_t](_kernel_median[uint16_t], image, selem, mask, out, - shift_x, shift_y, 0, 0, 0, 0, max_bin) + _core(_kernel_median[dtype_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) def _minimum(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, - dtype_t[:, ::1] out, + dtype_t_out[:, ::1] out, char shift_x, char shift_y, Py_ssize_t max_bin): - if dtype_t is uint8_t: - _core[uint8_t](_kernel_minimum[uint8_t], image, selem, mask, out, - shift_x, shift_y, 0, 0, 0, 0, max_bin) - elif dtype_t is uint16_t: - _core[uint16_t](_kernel_minimum[uint16_t], image, selem, mask, out, - shift_x, shift_y, 0, 0, 0, 0, max_bin) + _core(_kernel_minimum[dtype_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) def _enhance_contrast(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, - dtype_t[:, ::1] out, + dtype_t_out[:, ::1] out, char shift_x, char shift_y, Py_ssize_t max_bin): - if dtype_t is uint8_t: - _core[uint8_t](_kernel_enhance_contrast[uint8_t], image, selem, mask, - out, shift_x, shift_y, 0, 0, 0, 0, max_bin) - elif dtype_t is uint16_t: - _core[uint16_t](_kernel_enhance_contrast[uint16_t], image, selem, mask, - out, shift_x, shift_y, 0, 0, 0, 0, max_bin) + _core(_kernel_enhance_contrast[dtype_t], image, selem, mask, + out, shift_x, shift_y, 0, 0, 0, 0, max_bin) def _modal(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, - dtype_t[:, ::1] out, + dtype_t_out[:, ::1] out, char shift_x, char shift_y, Py_ssize_t max_bin): - if dtype_t is uint8_t: - _core[uint8_t](_kernel_modal[uint8_t], image, selem, mask, out, - shift_x, shift_y, 0, 0, 0, 0, max_bin) - elif dtype_t is uint16_t: - _core[uint16_t](_kernel_modal[uint16_t], image, selem, mask, out, - shift_x, shift_y, 0, 0, 0, 0, max_bin) + _core(_kernel_modal[dtype_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) def _pop(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, - dtype_t[:, ::1] out, + dtype_t_out[:, ::1] out, char shift_x, char shift_y, Py_ssize_t max_bin): - if dtype_t is uint8_t: - _core[uint8_t](_kernel_pop[uint8_t], image, selem, mask, out, - shift_x, shift_y, 0, 0, 0, 0, max_bin) - elif dtype_t is uint16_t: - _core[uint16_t](_kernel_pop[uint16_t], image, selem, mask, out, - shift_x, shift_y, 0, 0, 0, 0, max_bin) + _core(_kernel_pop[dtype_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) def _threshold(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, - dtype_t[:, ::1] out, + dtype_t_out[:, ::1] out, char shift_x, char shift_y, Py_ssize_t max_bin): - if dtype_t is uint8_t: - _core[uint8_t](_kernel_threshold[uint8_t], image, selem, mask, out, - shift_x, shift_y, 0, 0, 0, 0, max_bin) - elif dtype_t is uint16_t: - _core[uint16_t](_kernel_threshold[uint16_t], image, selem, mask, out, - shift_x, shift_y, 0, 0, 0, 0, max_bin) + _core(_kernel_threshold[dtype_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) def _tophat(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, - dtype_t[:, ::1] out, + dtype_t_out[:, ::1] out, char shift_x, char shift_y, Py_ssize_t max_bin): - if dtype_t is uint8_t: - _core[uint8_t](_kernel_tophat[uint8_t], image, selem, mask, out, - shift_x, shift_y, 0, 0, 0, 0, max_bin) - elif dtype_t is uint16_t: - _core[uint16_t](_kernel_tophat[uint16_t], image, selem, mask, out, - shift_x, shift_y, 0, 0, 0, 0, max_bin) + _core(_kernel_tophat[dtype_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) def _noise_filter(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, - dtype_t[:, ::1] out, + dtype_t_out[:, ::1] out, char shift_x, char shift_y, Py_ssize_t max_bin): - if dtype_t is uint8_t: - _core[uint8_t](_kernel_noise_filter[uint8_t], image, selem, mask, out, - shift_x, shift_y, 0, 0, 0, 0, max_bin) - elif dtype_t is uint16_t: - _core[uint16_t](_kernel_noise_filter[uint16_t], image, selem, mask, out, - shift_x, shift_y, 0, 0, 0, 0, max_bin) + _core(_kernel_noise_filter[dtype_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) def _entropy(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, - dtype_t[:, ::1] out, + dtype_t_out[:, ::1] out, char shift_x, char shift_y, Py_ssize_t max_bin): - if dtype_t is uint8_t: - _core[uint8_t](_kernel_entropy[uint8_t], image, selem, mask, out, - shift_x, shift_y, 0, 0, 0, 0, max_bin) - elif dtype_t is uint16_t: - _core[uint16_t](_kernel_entropy[uint16_t], image, selem, mask, out, - shift_x, shift_y, 0, 0, 0, 0, max_bin) + _core(_kernel_entropy[dtype_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) def _otsu(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, - dtype_t[:, ::1] out, + dtype_t_out[:, ::1] out, char shift_x, char shift_y, Py_ssize_t max_bin): - if dtype_t is uint8_t: - _core[uint8_t](_kernel_otsu[uint8_t], image, selem, mask, out, - shift_x, shift_y, 0, 0, 0, 0, max_bin) - elif dtype_t is uint16_t: - _core[uint16_t](_kernel_otsu[uint16_t], image, selem, mask, out, - shift_x, shift_y, 0, 0, 0, 0, max_bin) + _core(_kernel_otsu[dtype_t], image, selem, mask, out, + shift_x, shift_y, 0, 0, 0, 0, max_bin) diff --git a/skimage/filter/rank/percentile.py b/skimage/filter/rank/percentile.py index e91e0d82..ff3b1559 100644 --- a/skimage/filter/rank/percentile.py +++ b/skimage/filter/rank/percentile.py @@ -10,7 +10,8 @@ described in [1]_. Input image can be 8-bit or 16-bit, for 16-bit input images, the number of histogram bins is determined from the maximum value present in the image. -Result image is 8 or 16-bit with respect to the input image. +Result image is 8-/16-bit or double with respect to the input image and the +rank filter operation. References ---------- @@ -33,9 +34,11 @@ __all__ = ['autolevel_percentile', 'gradient_percentile', 'threshold_percentile'] -def _apply(func, image, selem, out, mask, shift_x, shift_y, p0, p1): +def _apply(func, image, selem, out, mask, shift_x, shift_y, p0, p1, + out_dtype=None): - image, selem, out, mask, max_bin = _handle_input(image, selem, out, mask) + image, selem, out, mask, max_bin = _handle_input(image, selem, out, mask, + out_dtype) func(image, selem, shift_x=shift_x, shift_y=shift_y, mask=mask, out=out, max_bin=max_bin, p0=p0, p1=p1) diff --git a/skimage/filter/rank/percentile_cy.pyx b/skimage/filter/rank/percentile_cy.pyx index bb4419de..664985f9 100644 --- a/skimage/filter/rank/percentile_cy.pyx +++ b/skimage/filter/rank/percentile_cy.pyx @@ -4,13 +4,13 @@ #cython: wraparound=False cimport numpy as cnp -from .core_cy cimport uint8_t, uint16_t, dtype_t, _core, _min, _max +from .core_cy cimport dtype_t, dtype_t_out, _core, _min, _max -cdef inline dtype_t _kernel_autolevel(Py_ssize_t* histo, float pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): +cdef inline float _kernel_autolevel(Py_ssize_t* histo, float pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): cdef Py_ssize_t i, imin, imax, sum, delta @@ -31,18 +31,18 @@ cdef inline dtype_t _kernel_autolevel(Py_ssize_t* histo, float pop, dtype_t g, delta = imax - imin if delta > 0: - return ((max_bin - 1) * (_min(_max(imin, g), imax) - - imin) / delta) + return (max_bin - 1) * (_min(_max(imin, g), imax) + - imin) / delta else: - return (imax - imin) + return imax - imin else: - return (0) + return 0 -cdef inline dtype_t _kernel_gradient(Py_ssize_t* histo, float pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): +cdef inline float _kernel_gradient(Py_ssize_t* histo, float pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): cdef Py_ssize_t i, imin, imax, sum, delta @@ -61,15 +61,15 @@ cdef inline dtype_t _kernel_gradient(Py_ssize_t* histo, float pop, dtype_t g, imax = i break - return (imax - imin) + return imax - imin else: - return (0) + return 0 -cdef inline dtype_t _kernel_mean(Py_ssize_t* histo, float pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): +cdef inline float _kernel_mean(Py_ssize_t* histo, float pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): cdef Py_ssize_t i, sum, mean, n @@ -84,18 +84,19 @@ cdef inline dtype_t _kernel_mean(Py_ssize_t* histo, float pop, dtype_t g, mean += histo[i] * i if n > 0: - return (mean / n) + return mean / n else: - return (0) + return 0 else: - return (0) + return 0 -cdef inline dtype_t _kernel_subtract_mean(Py_ssize_t* histo, float pop, - dtype_t g, Py_ssize_t max_bin, - Py_ssize_t mid_bin, float p0, - float p1, Py_ssize_t s0, - Py_ssize_t s1): +cdef inline float _kernel_subtract_mean(Py_ssize_t* histo, float pop, + dtype_t g, + Py_ssize_t max_bin, + Py_ssize_t mid_bin, float p0, + float p1, Py_ssize_t s0, + Py_ssize_t s1): cdef Py_ssize_t i, sum, mean, n @@ -109,18 +110,19 @@ cdef inline dtype_t _kernel_subtract_mean(Py_ssize_t* histo, float pop, n += histo[i] mean += histo[i] * i if n > 0: - return ((g - (mean / n)) * .5 + mid_bin) + return (g - (mean / n)) * .5 + mid_bin else: - return (0) + return 0 else: - return (0) + return 0 -cdef inline dtype_t _kernel_enhance_contrast(Py_ssize_t* histo, float pop, - dtype_t g, Py_ssize_t max_bin, - Py_ssize_t mid_bin, float p0, - float p1, Py_ssize_t s0, - Py_ssize_t s1): +cdef inline float _kernel_enhance_contrast(Py_ssize_t* histo, float pop, + dtype_t g, + Py_ssize_t max_bin, + Py_ssize_t mid_bin, float p0, + float p1, Py_ssize_t s0, + Py_ssize_t s1): cdef Py_ssize_t i, imin, imax, sum, delta @@ -139,21 +141,21 @@ cdef inline dtype_t _kernel_enhance_contrast(Py_ssize_t* histo, float pop, imax = i break if g > imax: - return imax + return imax if g < imin: - return imin + return imin if imax - g < g - imin: - return imax + return imax else: - return imin + return imin else: - return (0) + return 0 -cdef inline dtype_t _kernel_percentile(Py_ssize_t* histo, float pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): +cdef inline float _kernel_percentile(Py_ssize_t* histo, float pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): cdef Py_ssize_t i cdef Py_ssize_t sum = 0 @@ -164,15 +166,15 @@ cdef inline dtype_t _kernel_percentile(Py_ssize_t* histo, float pop, dtype_t g, if sum >= p0 * pop: break - return (i) + return i else: - return (0) + return 0 -cdef inline dtype_t _kernel_pop(Py_ssize_t* histo, float pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): +cdef inline float _kernel_pop(Py_ssize_t* histo, float pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): cdef Py_ssize_t i, sum, n @@ -183,15 +185,15 @@ cdef inline dtype_t _kernel_pop(Py_ssize_t* histo, float pop, dtype_t g, sum += histo[i] if (sum >= p0 * pop) and (sum <= p1 * pop): n += histo[i] - return (n) + return n else: - return (0) + return 0 -cdef inline dtype_t _kernel_threshold(Py_ssize_t* histo, float pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): +cdef inline float _kernel_threshold(Py_ssize_t* histo, float pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + float p0, float p1, + Py_ssize_t s0, Py_ssize_t s1): cdef int i cdef Py_ssize_t sum = 0 @@ -202,124 +204,92 @@ cdef inline dtype_t _kernel_threshold(Py_ssize_t* histo, float pop, dtype_t g, if sum >= p0 * pop: break - return ((max_bin - 1) * (g >= i)) + return (max_bin - 1) * (g >= i) else: - return (0) + return 0 def _autolevel(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask, - dtype_t[:, ::1] out, - char shift_x, char shift_y, float p0, float p1, - Py_ssize_t max_bin): + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, ::1] out, + char shift_x, char shift_y, float p0, float p1, + Py_ssize_t max_bin): - if dtype_t is uint8_t: - _core[uint8_t](_kernel_autolevel[uint8_t], image, selem, mask, out, - shift_x, shift_y, p0, p1, 0, 0, max_bin) - elif dtype_t is uint16_t: - _core[uint16_t](_kernel_autolevel[uint16_t], image, selem, mask, out, - shift_x, shift_y, p0, p1, 0, 0, max_bin) + _core(_kernel_autolevel[dtype_t], image, selem, mask, out, + shift_x, shift_y, p0, p1, 0, 0, max_bin) def _gradient(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask, - dtype_t[:, ::1] out, - char shift_x, char shift_y, float p0, float p1, - Py_ssize_t max_bin): + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, ::1] out, + char shift_x, char shift_y, float p0, float p1, + Py_ssize_t max_bin): - if dtype_t is uint8_t: - _core[uint8_t](_kernel_gradient[uint8_t], image, selem, mask, out, - shift_x, shift_y, p0, p1, 0, 0, max_bin) - elif dtype_t is uint16_t: - _core[uint16_t](_kernel_gradient[uint16_t], image, selem, mask, out, - shift_x, shift_y, p0, p1, 0, 0, max_bin) + _core(_kernel_gradient[dtype_t], image, selem, mask, out, + shift_x, shift_y, p0, p1, 0, 0, max_bin) def _mean(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask, - dtype_t[:, ::1] out, - char shift_x, char shift_y, float p0, float p1, - Py_ssize_t max_bin): + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, ::1] out, + char shift_x, char shift_y, float p0, float p1, + Py_ssize_t max_bin): - if dtype_t is uint8_t: - _core[uint8_t](_kernel_mean[uint8_t], image, selem, mask, out, - shift_x, shift_y, p0, p1, 0, 0, max_bin) - elif dtype_t is uint16_t: - _core[uint16_t](_kernel_mean[uint16_t], image, selem, mask, out, - shift_x, shift_y, p0, p1, 0, 0, max_bin) + _core(_kernel_mean[dtype_t], image, selem, mask, out, + shift_x, shift_y, p0, p1, 0, 0, max_bin) def _subtract_mean(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, - dtype_t[:, ::1] out, + dtype_t_out[:, ::1] out, char shift_x, char shift_y, float p0, float p1, Py_ssize_t max_bin): - if dtype_t is uint8_t: - _core[uint8_t](_kernel_subtract_mean[uint8_t], image, selem, mask, - out, shift_x, shift_y, p0, p1, 0, 0, max_bin) - elif dtype_t is uint16_t: - _core[uint16_t](_kernel_subtract_mean[uint16_t], image, selem, mask, - out, shift_x, shift_y, p0, p1, 0, 0, max_bin) + _core(_kernel_subtract_mean[dtype_t], image, selem, mask, + out, shift_x, shift_y, p0, p1, 0, 0, max_bin) def _enhance_contrast(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask, - dtype_t[:, ::1] out, - char shift_x, char shift_y, float p0, float p1, - Py_ssize_t max_bin): + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, ::1] out, + char shift_x, char shift_y, float p0, float p1, + Py_ssize_t max_bin): - if dtype_t is uint8_t: - _core[uint8_t](_kernel_enhance_contrast[uint8_t], image, selem, mask, - out, shift_x, shift_y, p0, p1, 0, 0, max_bin) - elif dtype_t is uint16_t: - _core[uint16_t](_kernel_enhance_contrast[uint16_t], image, selem, mask, - out, shift_x, shift_y, p0, p1, 0, 0, max_bin) + _core(_kernel_enhance_contrast[dtype_t], image, selem, mask, + out, shift_x, shift_y, p0, p1, 0, 0, max_bin) def _percentile(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask, - dtype_t[:, ::1] out, - char shift_x, char shift_y, float p0, Py_ssize_t max_bin): + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, ::1] out, + char shift_x, char shift_y, float p0, Py_ssize_t max_bin): - if dtype_t is uint8_t: - _core[uint8_t](_kernel_percentile[uint8_t], image, selem, mask, out, - shift_x, shift_y, p0, 1, 0, 0, max_bin) - elif dtype_t is uint16_t: - _core[uint16_t](_kernel_percentile[uint16_t], image, selem, mask, out, - shift_x, shift_y, p0, 1, 0, 0, max_bin) + _core(_kernel_percentile[dtype_t], image, selem, mask, out, + shift_x, shift_y, p0, 1, 0, 0, max_bin) def _pop(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask, - dtype_t[:, ::1] out, - char shift_x, char shift_y, float p0, float p1, - Py_ssize_t max_bin): + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, ::1] out, + char shift_x, char shift_y, float p0, float p1, + Py_ssize_t max_bin): - if dtype_t is uint8_t: - _core[uint8_t](_kernel_pop[uint8_t], image, selem, mask, out, - shift_x, shift_y, p0, p1, 0, 0, max_bin) - elif dtype_t is uint16_t: - _core[uint16_t](_kernel_pop[uint16_t], image, selem, mask, out, - shift_x, shift_y, p0, p1, 0, 0, max_bin) + _core(_kernel_pop[dtype_t], image, selem, mask, out, + shift_x, shift_y, p0, p1, 0, 0, max_bin) def _threshold(dtype_t[:, ::1] image, - char[:, ::1] selem, - char[:, ::1] mask, - dtype_t[:, ::1] out, - char shift_x, char shift_y, float p0, Py_ssize_t max_bin): + char[:, ::1] selem, + char[:, ::1] mask, + dtype_t_out[:, ::1] out, + char shift_x, char shift_y, float p0, Py_ssize_t max_bin): - if dtype_t is uint8_t: - _core[uint8_t](_kernel_threshold[uint8_t], image, selem, mask, out, - shift_x, shift_y, p0, 1, 0, 0, max_bin) - elif dtype_t is uint16_t: - _core[uint16_t](_kernel_threshold[uint16_t], image, selem, mask, out, - shift_x, shift_y, p0, 1, 0, 0, max_bin) + _core(_kernel_threshold[dtype_t], image, selem, mask, out, + shift_x, shift_y, p0, 1, 0, 0, max_bin) From 8ea6d1deb0b64a47ab38bb8a6aeedacaab71107a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Fri, 12 Jul 2013 23:06:44 +0200 Subject: [PATCH 283/736] Replace all float dtypes with double --- skimage/filter/rank/bilateral_cy.pyx | 16 +-- skimage/filter/rank/core_cy.pxd | 8 +- skimage/filter/rank/core_cy.pyx | 16 +-- skimage/filter/rank/generic_cy.pyx | 155 +++++++++++++------------- skimage/filter/rank/percentile_cy.pyx | 90 +++++++-------- 5 files changed, 143 insertions(+), 142 deletions(-) diff --git a/skimage/filter/rank/bilateral_cy.pyx b/skimage/filter/rank/bilateral_cy.pyx index b8e0d1e7..de2b3e53 100644 --- a/skimage/filter/rank/bilateral_cy.pyx +++ b/skimage/filter/rank/bilateral_cy.pyx @@ -9,10 +9,10 @@ from libc.math cimport log from .core_cy cimport dtype_t, dtype_t_out, _core -cdef inline float _kernel_mean(Py_ssize_t* histo, float pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): +cdef inline double _kernel_mean(Py_ssize_t* histo, double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): cdef Py_ssize_t i cdef Py_ssize_t bilat_pop = 0 @@ -31,10 +31,10 @@ cdef inline float _kernel_mean(Py_ssize_t* histo, float pop, dtype_t g, return 0 -cdef inline float _kernel_pop(Py_ssize_t* histo, float pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): +cdef inline double _kernel_pop(Py_ssize_t* histo, double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): cdef Py_ssize_t i cdef Py_ssize_t bilat_pop = 0 diff --git a/skimage/filter/rank/core_cy.pxd b/skimage/filter/rank/core_cy.pxd index a1e44f98..2e97e50a 100644 --- a/skimage/filter/rank/core_cy.pxd +++ b/skimage/filter/rank/core_cy.pxd @@ -15,14 +15,14 @@ cdef dtype_t _max(dtype_t a, dtype_t b) cdef dtype_t _min(dtype_t a, dtype_t b) -cdef void _core(float kernel(Py_ssize_t*, float, dtype_t, - Py_ssize_t, Py_ssize_t, float, - float, Py_ssize_t, Py_ssize_t), +cdef void _core(double kernel(Py_ssize_t*, double, dtype_t, + Py_ssize_t, Py_ssize_t, double, + double, Py_ssize_t, Py_ssize_t), dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, dtype_t_out[:, ::1] out, char shift_x, char shift_y, - float p0, float p1, + double p0, double p1, Py_ssize_t s0, Py_ssize_t s1, Py_ssize_t max_bin) except * diff --git a/skimage/filter/rank/core_cy.pyx b/skimage/filter/rank/core_cy.pyx index 854a3ce7..02c2c8d0 100644 --- a/skimage/filter/rank/core_cy.pyx +++ b/skimage/filter/rank/core_cy.pyx @@ -17,13 +17,13 @@ cdef inline dtype_t _min(dtype_t a, dtype_t b): return a if a <= b else b -cdef inline void histogram_increment(Py_ssize_t* histo, float* pop, +cdef inline void histogram_increment(Py_ssize_t* histo, double* pop, dtype_t value): histo[value] += 1 pop[0] += 1 -cdef inline void histogram_decrement(Py_ssize_t* histo, float* pop, +cdef inline void histogram_decrement(Py_ssize_t* histo, double* pop, dtype_t value): histo[value] -= 1 pop[0] -= 1 @@ -42,15 +42,15 @@ cdef inline char is_in_mask(Py_ssize_t rows, Py_ssize_t cols, return 0 -cdef void _core(float kernel(Py_ssize_t*, float, dtype_t, - Py_ssize_t, Py_ssize_t, float, - float, Py_ssize_t, Py_ssize_t), +cdef void _core(double kernel(Py_ssize_t*, double, dtype_t, + Py_ssize_t, Py_ssize_t, double, + double, Py_ssize_t, Py_ssize_t), dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, dtype_t_out[:, ::1] out, char shift_x, char shift_y, - float p0, float p1, + double p0, double p1, Py_ssize_t s0, Py_ssize_t s1, Py_ssize_t max_bin) except *: """Compute histogram for each pixel neighborhood, apply kernel function and @@ -82,8 +82,8 @@ cdef void _core(float kernel(Py_ssize_t*, float, dtype_t, # define local variable types cdef Py_ssize_t r, c, rr, cc, s, value, local_max, i, even_row - # number of pixels actually inside the neighborhood (float) - cdef float pop = 0 + # number of pixels actually inside the neighborhood (double) + cdef double pop = 0 # the current local histogram distribution cdef Py_ssize_t* histo = malloc(max_bin * sizeof(Py_ssize_t)) diff --git a/skimage/filter/rank/generic_cy.pyx b/skimage/filter/rank/generic_cy.pyx index 47cb99da..a773f62c 100644 --- a/skimage/filter/rank/generic_cy.pyx +++ b/skimage/filter/rank/generic_cy.pyx @@ -9,10 +9,10 @@ from libc.math cimport log from .core_cy cimport dtype_t, dtype_t_out, _core -cdef inline float _kernel_autolevel(Py_ssize_t* histo, float pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): +cdef inline double _kernel_autolevel(Py_ssize_t* histo, double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): cdef Py_ssize_t i, imin, imax, delta @@ -27,17 +27,17 @@ cdef inline float _kernel_autolevel(Py_ssize_t* histo, float pop, dtype_t g, break delta = imax - imin if delta > 0: - return (max_bin - 1) * (g - imin) / delta + return (max_bin - 1) * (g - imin) / delta else: return imax - imin else: return 0 -cdef inline float _kernel_bottomhat(Py_ssize_t* histo, float pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): +cdef inline double _kernel_bottomhat(Py_ssize_t* histo, double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): cdef Py_ssize_t i @@ -50,10 +50,10 @@ cdef inline float _kernel_bottomhat(Py_ssize_t* histo, float pop, dtype_t g, return 0 -cdef inline float _kernel_equalize(Py_ssize_t* histo, float pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): +cdef inline double _kernel_equalize(Py_ssize_t* histo, double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): cdef Py_ssize_t i cdef Py_ssize_t sum = 0 @@ -68,10 +68,10 @@ cdef inline float _kernel_equalize(Py_ssize_t* histo, float pop, dtype_t g, return 0 -cdef inline float _kernel_gradient(Py_ssize_t* histo, float pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): +cdef inline double _kernel_gradient(Py_ssize_t* histo, double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): cdef Py_ssize_t i, imin, imax @@ -89,10 +89,10 @@ cdef inline float _kernel_gradient(Py_ssize_t* histo, float pop, dtype_t g, return 0 -cdef inline float _kernel_maximum(Py_ssize_t* histo, float pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): +cdef inline double _kernel_maximum(Py_ssize_t* histo, double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): cdef Py_ssize_t i @@ -104,10 +104,10 @@ cdef inline float _kernel_maximum(Py_ssize_t* histo, float pop, dtype_t g, return 0 -cdef inline float _kernel_mean(Py_ssize_t* histo, float pop,dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): +cdef inline double _kernel_mean(Py_ssize_t* histo, double pop,dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): cdef Py_ssize_t i cdef Py_ssize_t mean = 0 @@ -120,12 +120,12 @@ cdef inline float _kernel_mean(Py_ssize_t* histo, float pop,dtype_t g, return 0 -cdef inline float _kernel_subtract_mean(Py_ssize_t* histo, float pop, - dtype_t g, - Py_ssize_t max_bin, - Py_ssize_t mid_bin, float p0, - float p1, Py_ssize_t s0, - Py_ssize_t s1): +cdef inline double _kernel_subtract_mean(Py_ssize_t* histo, double pop, + dtype_t g, + Py_ssize_t max_bin, + Py_ssize_t mid_bin, double p0, + double p1, Py_ssize_t s0, + Py_ssize_t s1): cdef Py_ssize_t i cdef Py_ssize_t mean = 0 @@ -138,13 +138,13 @@ cdef inline float _kernel_subtract_mean(Py_ssize_t* histo, float pop, return 0 -cdef inline float _kernel_median(Py_ssize_t* histo, float pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): +cdef inline double _kernel_median(Py_ssize_t* histo, double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): cdef Py_ssize_t i - cdef float sum = pop / 2.0 + cdef double sum = pop / 2.0 if pop: for i in range(max_bin): @@ -156,10 +156,10 @@ cdef inline float _kernel_median(Py_ssize_t* histo, float pop, dtype_t g, return 0 -cdef inline float _kernel_minimum(Py_ssize_t* histo, float pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): +cdef inline double _kernel_minimum(Py_ssize_t* histo, double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): cdef Py_ssize_t i @@ -171,10 +171,10 @@ cdef inline float _kernel_minimum(Py_ssize_t* histo, float pop, dtype_t g, return 0 -cdef inline float _kernel_modal(Py_ssize_t* histo, float pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): +cdef inline double _kernel_modal(Py_ssize_t* histo, double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): cdef Py_ssize_t hmax = 0, imax = 0 @@ -188,12 +188,12 @@ cdef inline float _kernel_modal(Py_ssize_t* histo, float pop, dtype_t g, return 0 -cdef inline float _kernel_enhance_contrast(Py_ssize_t* histo, float pop, - dtype_t g, - Py_ssize_t max_bin, - Py_ssize_t mid_bin, float p0, - float p1, Py_ssize_t s0, - Py_ssize_t s1): +cdef inline double _kernel_enhance_contrast(Py_ssize_t* histo, double pop, + dtype_t g, + Py_ssize_t max_bin, + Py_ssize_t mid_bin, double p0, + double p1, Py_ssize_t s0, + Py_ssize_t s1): cdef Py_ssize_t i, imin, imax @@ -214,18 +214,18 @@ cdef inline float _kernel_enhance_contrast(Py_ssize_t* histo, float pop, return 0 -cdef inline float _kernel_pop(Py_ssize_t* histo, float pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): +cdef inline double _kernel_pop(Py_ssize_t* histo, double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): return pop -cdef inline float _kernel_threshold(Py_ssize_t* histo, float pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): +cdef inline double _kernel_threshold(Py_ssize_t* histo, double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): cdef Py_ssize_t i cdef Py_ssize_t mean = 0 @@ -238,10 +238,10 @@ cdef inline float _kernel_threshold(Py_ssize_t* histo, float pop, dtype_t g, return 0 -cdef inline float _kernel_tophat(Py_ssize_t* histo, float pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): +cdef inline double _kernel_tophat(Py_ssize_t* histo, double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): cdef Py_ssize_t i @@ -254,10 +254,11 @@ cdef inline float _kernel_tophat(Py_ssize_t* histo, float pop, dtype_t g, return 0 -cdef inline float _kernel_noise_filter(Py_ssize_t* histo, float pop, dtype_t g, - Py_ssize_t max_bin, - Py_ssize_t mid_bin, float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): +cdef inline double _kernel_noise_filter(Py_ssize_t* histo, double pop, + dtype_t g, Py_ssize_t max_bin, + Py_ssize_t mid_bin, double p0, + double p1, Py_ssize_t s0, + Py_ssize_t s1): cdef Py_ssize_t i cdef Py_ssize_t min_i @@ -279,12 +280,12 @@ cdef inline float _kernel_noise_filter(Py_ssize_t* histo, float pop, dtype_t g, return min_i -cdef inline float _kernel_entropy(Py_ssize_t* histo, float pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): +cdef inline double _kernel_entropy(Py_ssize_t* histo, double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): cdef Py_ssize_t i - cdef float e, p + cdef double e, p if pop: e = 0. @@ -297,14 +298,14 @@ cdef inline float _kernel_entropy(Py_ssize_t* histo, float pop, dtype_t g, return 0 -cdef inline float _kernel_otsu(Py_ssize_t* histo, float pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): +cdef inline double _kernel_otsu(Py_ssize_t* histo, double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): cdef Py_ssize_t i cdef Py_ssize_t max_i - cdef float P, mu1, mu2, q1, new_q1, sigma_b, max_sigma_b - cdef float mu = 0. + cdef double P, mu1, mu2, q1, new_q1, sigma_b, max_sigma_b + cdef double mu = 0. # compute local mean if pop: diff --git a/skimage/filter/rank/percentile_cy.pyx b/skimage/filter/rank/percentile_cy.pyx index 664985f9..31afd53a 100644 --- a/skimage/filter/rank/percentile_cy.pyx +++ b/skimage/filter/rank/percentile_cy.pyx @@ -7,10 +7,10 @@ cimport numpy as cnp from .core_cy cimport dtype_t, dtype_t_out, _core, _min, _max -cdef inline float _kernel_autolevel(Py_ssize_t* histo, float pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): +cdef inline double _kernel_autolevel(Py_ssize_t* histo, double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): cdef Py_ssize_t i, imin, imax, sum, delta @@ -31,7 +31,7 @@ cdef inline float _kernel_autolevel(Py_ssize_t* histo, float pop, dtype_t g, delta = imax - imin if delta > 0: - return (max_bin - 1) * (_min(_max(imin, g), imax) + return (max_bin - 1) * (_min(_max(imin, g), imax) - imin) / delta else: return imax - imin @@ -39,10 +39,10 @@ cdef inline float _kernel_autolevel(Py_ssize_t* histo, float pop, dtype_t g, return 0 -cdef inline float _kernel_gradient(Py_ssize_t* histo, float pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): +cdef inline double _kernel_gradient(Py_ssize_t* histo, double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): cdef Py_ssize_t i, imin, imax, sum, delta @@ -66,10 +66,10 @@ cdef inline float _kernel_gradient(Py_ssize_t* histo, float pop, dtype_t g, return 0 -cdef inline float _kernel_mean(Py_ssize_t* histo, float pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): +cdef inline double _kernel_mean(Py_ssize_t* histo, double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): cdef Py_ssize_t i, sum, mean, n @@ -91,12 +91,12 @@ cdef inline float _kernel_mean(Py_ssize_t* histo, float pop, dtype_t g, return 0 -cdef inline float _kernel_subtract_mean(Py_ssize_t* histo, float pop, - dtype_t g, - Py_ssize_t max_bin, - Py_ssize_t mid_bin, float p0, - float p1, Py_ssize_t s0, - Py_ssize_t s1): +cdef inline double _kernel_subtract_mean(Py_ssize_t* histo, double pop, + dtype_t g, + Py_ssize_t max_bin, + Py_ssize_t mid_bin, double p0, + double p1, Py_ssize_t s0, + Py_ssize_t s1): cdef Py_ssize_t i, sum, mean, n @@ -117,12 +117,12 @@ cdef inline float _kernel_subtract_mean(Py_ssize_t* histo, float pop, return 0 -cdef inline float _kernel_enhance_contrast(Py_ssize_t* histo, float pop, - dtype_t g, - Py_ssize_t max_bin, - Py_ssize_t mid_bin, float p0, - float p1, Py_ssize_t s0, - Py_ssize_t s1): +cdef inline double _kernel_enhance_contrast(Py_ssize_t* histo, double pop, + dtype_t g, + Py_ssize_t max_bin, + Py_ssize_t mid_bin, double p0, + double p1, Py_ssize_t s0, + Py_ssize_t s1): cdef Py_ssize_t i, imin, imax, sum, delta @@ -152,10 +152,10 @@ cdef inline float _kernel_enhance_contrast(Py_ssize_t* histo, float pop, return 0 -cdef inline float _kernel_percentile(Py_ssize_t* histo, float pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): +cdef inline double _kernel_percentile(Py_ssize_t* histo, double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): cdef Py_ssize_t i cdef Py_ssize_t sum = 0 @@ -171,10 +171,10 @@ cdef inline float _kernel_percentile(Py_ssize_t* histo, float pop, dtype_t g, return 0 -cdef inline float _kernel_pop(Py_ssize_t* histo, float pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): +cdef inline double _kernel_pop(Py_ssize_t* histo, double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): cdef Py_ssize_t i, sum, n @@ -190,10 +190,10 @@ cdef inline float _kernel_pop(Py_ssize_t* histo, float pop, dtype_t g, return 0 -cdef inline float _kernel_threshold(Py_ssize_t* histo, float pop, dtype_t g, - Py_ssize_t max_bin, Py_ssize_t mid_bin, - float p0, float p1, - Py_ssize_t s0, Py_ssize_t s1): +cdef inline double _kernel_threshold(Py_ssize_t* histo, double pop, dtype_t g, + Py_ssize_t max_bin, Py_ssize_t mid_bin, + double p0, double p1, + Py_ssize_t s0, Py_ssize_t s1): cdef int i cdef Py_ssize_t sum = 0 @@ -213,7 +213,7 @@ def _autolevel(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, dtype_t_out[:, ::1] out, - char shift_x, char shift_y, float p0, float p1, + char shift_x, char shift_y, double p0, double p1, Py_ssize_t max_bin): _core(_kernel_autolevel[dtype_t], image, selem, mask, out, @@ -224,7 +224,7 @@ def _gradient(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, dtype_t_out[:, ::1] out, - char shift_x, char shift_y, float p0, float p1, + char shift_x, char shift_y, double p0, double p1, Py_ssize_t max_bin): _core(_kernel_gradient[dtype_t], image, selem, mask, out, @@ -235,7 +235,7 @@ def _mean(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, dtype_t_out[:, ::1] out, - char shift_x, char shift_y, float p0, float p1, + char shift_x, char shift_y, double p0, double p1, Py_ssize_t max_bin): _core(_kernel_mean[dtype_t], image, selem, mask, out, @@ -246,7 +246,7 @@ def _subtract_mean(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, dtype_t_out[:, ::1] out, - char shift_x, char shift_y, float p0, float p1, + char shift_x, char shift_y, double p0, double p1, Py_ssize_t max_bin): _core(_kernel_subtract_mean[dtype_t], image, selem, mask, @@ -257,7 +257,7 @@ def _enhance_contrast(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, dtype_t_out[:, ::1] out, - char shift_x, char shift_y, float p0, float p1, + char shift_x, char shift_y, double p0, double p1, Py_ssize_t max_bin): _core(_kernel_enhance_contrast[dtype_t], image, selem, mask, @@ -268,7 +268,7 @@ def _percentile(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, dtype_t_out[:, ::1] out, - char shift_x, char shift_y, float p0, Py_ssize_t max_bin): + char shift_x, char shift_y, double p0, Py_ssize_t max_bin): _core(_kernel_percentile[dtype_t], image, selem, mask, out, shift_x, shift_y, p0, 1, 0, 0, max_bin) @@ -278,7 +278,7 @@ def _pop(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, dtype_t_out[:, ::1] out, - char shift_x, char shift_y, float p0, float p1, + char shift_x, char shift_y, double p0, double p1, Py_ssize_t max_bin): _core(_kernel_pop[dtype_t], image, selem, mask, out, @@ -289,7 +289,7 @@ def _threshold(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, dtype_t_out[:, ::1] out, - char shift_x, char shift_y, float p0, Py_ssize_t max_bin): + char shift_x, char shift_y, double p0, Py_ssize_t max_bin): _core(_kernel_threshold[dtype_t], image, selem, mask, out, shift_x, shift_y, p0, 1, 0, 0, max_bin) From bb4a8aff26abd6b080e372c5058e978886b9bad7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Fri, 12 Jul 2013 23:20:32 +0200 Subject: [PATCH 284/736] Add test for output dtype of entropy --- skimage/filter/rank/tests/test_rank.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/skimage/filter/rank/tests/test_rank.py b/skimage/filter/rank/tests/test_rank.py index d2906f98..c190fb57 100644 --- a/skimage/filter/rank/tests/test_rank.py +++ b/skimage/filter/rank/tests/test_rank.py @@ -163,8 +163,8 @@ def test_compare_autolevels(): def test_compare_autolevels_16bit(): - # compare autolevel(16-bit) and percentile autolevel(16-bit) with p0=0.0 and - # p1=1.0 should returns the same arrays + # compare autolevel(16-bit) and percentile autolevel(16-bit) with p0=0.0 + # and p1=1.0 should returns the same arrays image = data.camera().astype(np.uint16) * 4 @@ -193,8 +193,8 @@ def test_compare_ubyte_vs_float(): def test_compare_8bit_unsigned_vs_signed(): - # filters applied on 8-bit image ore 16-bit image (having only real 8-bit of - # dynamic) should be identical + # filters applied on 8-bit image ore 16-bit image (having only real 8-bit + # of dynamic) should be identical # Create signed int8 image that and convert it to uint8 image = img_as_ubyte(data.camera()) @@ -216,8 +216,8 @@ def test_compare_8bit_unsigned_vs_signed(): def test_compare_8bit_vs_16bit(): - # filters applied on 8-bit image ore 16-bit image (having only real 8-bit of - # dynamic) should be identical + # filters applied on 8-bit image ore 16-bit image (having only real 8-bit + # of dynamic) should be identical image8 = util.img_as_ubyte(data.camera()) image16 = image8.astype(np.uint16) @@ -327,7 +327,8 @@ def test_smallest_selem16(): def test_empty_selem(): - # check that min, max and mean returns zeros if structuring element is empty + # check that min, max and mean returns zeros if structuring element is + # empty image = np.zeros((5, 5), dtype=np.uint16) out = np.zeros_like(image) @@ -402,6 +403,10 @@ def test_entropy(): np.reshape(np.arange(4096), (64, 64)), (2, 2)).astype(np.uint16) assert(np.max(rank.entropy(data, selem)) == 12) + # make sure output is of dtype double + out = rank.entropy(data, np.ones((16, 16), dtype=np.uint8)) + assert out.dtype == np.double + def test_selem_dtypes(): From 6221ce6e3b86c8bb63837673df412d8d542a0f31 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Sat, 13 Jul 2013 01:27:17 +0200 Subject: [PATCH 285/736] Add test coverage for guess_spatial_dimensions() --- skimage/color/tests/test_colorconv.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/skimage/color/tests/test_colorconv.py b/skimage/color/tests/test_colorconv.py index 05ab6915..6b06718b 100644 --- a/skimage/color/tests/test_colorconv.py +++ b/skimage/color/tests/test_colorconv.py @@ -33,7 +33,8 @@ from skimage.color import (rgb2hsv, hsv2rgb, rgb2grey, gray2rgb, xyz2lab, lab2xyz, lab2rgb, rgb2lab, - is_rgb, is_gray + is_rgb, is_gray, + guess_spatial_dimensions ) from skimage import data_dir, data @@ -41,6 +42,19 @@ from skimage import data_dir, data import colorsys +def test_guess_spatial_dimensions(): + im1 = np.zeros((5, 5)) + im2 = np.zeros((5, 5, 5)) + im3 = np.zeros((5, 5, 3)) + im4 = np.zeros((5, 5, 5, 3)) + im5 = np.zeros((5,)) + assert_equal(guess_spatial_dimensions(im1), 2) + assert_equal(guess_spatial_dimensions(im2), 3) + assert_equal(guess_spatial_dimensions(im3), None) + assert_equal(guess_spatial_dimensions(im4), 3) + assert_raises(guess_spatial_dimensions(im5), ValueError) + + class TestColorconv(TestCase): img_rgb = imread(os.path.join(data_dir, 'color.png')) From 858f7411d722dc2ccf08f794e2b3bb0b2dcd35b0 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Sat, 13 Jul 2013 01:34:51 +0200 Subject: [PATCH 286/736] Complete test_regular_grid coverage --- skimage/util/tests/test_regular_grid.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/skimage/util/tests/test_regular_grid.py b/skimage/util/tests/test_regular_grid.py index 2808d99c..61736a76 100644 --- a/skimage/util/tests/test_regular_grid.py +++ b/skimage/util/tests/test_regular_grid.py @@ -1,9 +1,16 @@ import numpy as np -from nose.tools import raises from numpy.testing import assert_equal from skimage.util import regular_grid +def test_regular_grid_full(): + ar = np.zeros((2, 2)) + g = regular_grid(ar, 25) + assert_equal(g, [slice(None, None, None), slice(None, None, None)]) + ar[g] = 1 + assert_equal(ar.size, ar.sum()) + + def test_regular_grid_2d_8(): ar = np.zeros((20, 40)) g = regular_grid(ar.shape, 8) From 8736a6650462dbeca176c19364917ce89a870ae2 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Sat, 13 Jul 2013 09:49:38 +1000 Subject: [PATCH 287/736] Fix typo using assert_raises in test_colorconv.py --- skimage/color/tests/test_colorconv.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/color/tests/test_colorconv.py b/skimage/color/tests/test_colorconv.py index 6b06718b..4fdaa4c8 100644 --- a/skimage/color/tests/test_colorconv.py +++ b/skimage/color/tests/test_colorconv.py @@ -52,7 +52,7 @@ def test_guess_spatial_dimensions(): assert_equal(guess_spatial_dimensions(im2), 3) assert_equal(guess_spatial_dimensions(im3), None) assert_equal(guess_spatial_dimensions(im4), 3) - assert_raises(guess_spatial_dimensions(im5), ValueError) + assert_raises(ValueError, guess_spatial_dimensions, im5) class TestColorconv(TestCase): From e3cd0620a703d85e09882128b7d77f7d675c5b36 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Sat, 13 Jul 2013 10:02:12 +0800 Subject: [PATCH 288/736] Fixing bugs and making code compatible with Python 3 --- skimage/feature/_brief.py | 40 ++++++++++++++++------------- skimage/feature/tests/test_brief.py | 29 +++++++++++++-------- 2 files changed, 40 insertions(+), 29 deletions(-) diff --git a/skimage/feature/_brief.py b/skimage/feature/_brief.py index 4c580d28..d6f4e72e 100644 --- a/skimage/feature/_brief.py +++ b/skimage/feature/_brief.py @@ -55,9 +55,9 @@ def brief(image, keypoints, descriptor_size=256, mode='normal', patch_size=49, Examples -------- - >>> from skimage.feature.corner import * - >>> from skimage.feature import pairwise_hamming_distance - >>> from skimage.feature._brief import * + >>> import numpy as np + >>> from skimage.feature.corner import corner_peaks, corner_harris + >>> from skimage.feature import pairwise_hamming_distance, brief, match_keypoints_brief >>> square1 = np.zeros([8, 8], dtype=np.int32) >>> square1[2:6, 2:6] = 1 >>> square1 @@ -111,15 +111,17 @@ def brief(image, keypoints, descriptor_size=256, mode='normal', patch_size=49, [ 0.375 , 0.6328125, 0.0390625, 0.328125 ], [ 0.625 , 0.3671875, 0.34375 , 0.0234375]]) >>> match_keypoints_brief(keypoints1, descriptors1, keypoints2, descriptors2) - array([[[2, 2], - [2, 5], - [5, 2], - [5, 5]], + array([[[ 2., 2.], + [ 2., 2.]], - [[2, 2], - [2, 6], - [6, 2], - [6, 6]]]) + [[ 2., 5.], + [ 2., 6.]], + + [[ 5., 2.], + [ 6., 2.]], + + [[ 5., 5.], + [ 6., 6.]]]) """ np.random.seed(sample_seed) @@ -140,7 +142,7 @@ def brief(image, keypoints, descriptor_size=256, mode='normal', patch_size=49, # Removing keypoints that are within (patch_size / 2) distance from the # image border - keypoints = _remove_border_keypoints(image, keypoints, patch_size / 2) + keypoints = _remove_border_keypoints(image, keypoints, patch_size // 2) keypoints = np.ascontiguousarray(keypoints) descriptors = np.zeros((keypoints.shape[0], descriptor_size), dtype=bool, @@ -149,10 +151,10 @@ def brief(image, keypoints, descriptor_size=256, mode='normal', patch_size=49, # Sampling pairs of decision pixels in patch_size x patch_size window if mode == 'normal': - samples = (patch_size / 5) * np.random.randn(descriptor_size * 8) + samples = (patch_size / 5.0) * np.random.randn(descriptor_size * 8) samples = np.array(samples, dtype=np.int32) - samples = samples[(samples < (patch_size / 2)) - & (samples > - (patch_size - 1) / 2)] + samples = samples[(samples < (patch_size // 2)) + & (samples > - (patch_size - 2) // 2)] pos1 = samples[:descriptor_size * 2] pos1 = pos1.reshape(descriptor_size, 2) @@ -161,7 +163,7 @@ def brief(image, keypoints, descriptor_size=256, mode='normal', patch_size=49, else: - samples = np.random.randint(-patch_size / 2, (patch_size / 2) + 1, + samples = np.random.randint(-(patch_size - 2) // 2, (patch_size // 2) + 1, (descriptor_size * 2, 2)) pos1, pos2 = np.split(samples, 2) @@ -194,7 +196,7 @@ def match_keypoints_brief(keypoints1, descriptors1, keypoints2, Returns ------- - match_keypoints_brief : (2, Q, 2) ndarray + match_keypoints_brief : (Q, 2, 2) ndarray Location of Q matched keypoint pairs from two images. """ @@ -214,6 +216,8 @@ def match_keypoints_brief(keypoints1, descriptors1, keypoints2, temp = distance > threshold row_check = ~ np.all(temp, axis = 1) matched_keypoints2 = keypoints2[np.argmin(distance, axis=1)] - matched_keypoint_pairs = np.array([keypoints1[row_check], matched_keypoints2[row_check]]) + matched_keypoint_pairs = np.zeros((np.sum(row_check), 2, 2)) + matched_keypoint_pairs[:, 0, :] = keypoints1[row_check] + matched_keypoint_pairs[:, 1, :] = matched_keypoints2[row_check] return matched_keypoint_pairs diff --git a/skimage/feature/tests/test_brief.py b/skimage/feature/tests/test_brief.py index 7718e97c..b8d26544 100644 --- a/skimage/feature/tests/test_brief.py +++ b/skimage/feature/tests/test_brief.py @@ -33,7 +33,7 @@ def test_match_keypoints_brief_lena_translation(): keypoints2, descriptors2, threshold=0.10) - assert_array_equal(matched_keypoints[0,::], matched_keypoints[1,::] + + assert_array_equal(matched_keypoints[:, 0,:], matched_keypoints[:, 1,:] + [20, 15]) @@ -56,15 +56,22 @@ def test_match_keypoints_brief_lena_rotation(): matched_keypoints = match_keypoints_brief(keypoints1, descriptors1, keypoints2, descriptors2, threshold=0.07) - expected = np.array([[[248, 147], - [263, 272], - [271, 120], - [414, 70], - [454, 176]], + expected = np.array([[[ 263., 272.], + [ 234., 298.]], + + [[ 271., 120.], + [ 258., 146.]], + + [[ 323., 164.], + [ 305., 195.]], + + [[ 414., 70.], + [ 405., 111.]], + + [[ 435., 181.], + [ 415., 223.]], + + [[ 454., 176.], + [ 435., 221.]]]) - [[232, 171], - [234, 298], - [258, 146], - [405, 111], - [435, 221]]]) assert_array_equal(matched_keypoints, expected) From 71c70ccd7129fe0409a308c2d2f0b85044b7de0f Mon Sep 17 00:00:00 2001 From: Ferdinand Deger Date: Sat, 13 Jul 2013 09:52:09 +0200 Subject: [PATCH 289/736] Added isotropic TV denoising + comment --- skimage/filter/_denoise_cy.pyx | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/skimage/filter/_denoise_cy.pyx b/skimage/filter/_denoise_cy.pyx index 6fd95f03..6e803062 100644 --- a/skimage/filter/_denoise_cy.pyx +++ b/skimage/filter/_denoise_cy.pyx @@ -200,7 +200,7 @@ def denoise_bilateral(image, Py_ssize_t win_size=5, sigma_range=None, return np.squeeze(out) -def denoise_tv_bregman(image, double weight, int max_iter=100, double eps=1e-3): +def denoise_tv_bregman(image, double weight, int max_iter=100, double eps=1e-3, anisotropic=False): """Perform total-variation denoising using split-Bregman optimization. Total-variation denoising (also know as total-variation regularization) @@ -225,6 +225,8 @@ def denoise_tv_bregman(image, double weight, int max_iter=100, double eps=1e-3): max_iter: int, optional Maximal number of iterations used for the optimization. + anisotropic: boolean, optimal - switch between isotropic and anisotropic tv denoising + Returns ------- u : ndarray @@ -239,6 +241,7 @@ def denoise_tv_bregman(image, double weight, int max_iter=100, double eps=1e-3): .. [3] Pascal Getreuer, "Rudin–Osher–Fatemi Total Variation Denoising using Split Bregman" in Image Processing On Line on 2012–05–19, http://www.ipol.im/pub/art/2012/g-tvd/article_lr.pdf + [4] http://www.math.ucsb.edu/~cgarcia/UGProjects/BregmanAlgorithms_JacquelineBush.pdf """ @@ -325,9 +328,26 @@ def denoise_tv_bregman(image, double weight, int max_iter=100, double eps=1e-3): bxx = bx[r, c, k] byy = by[r, c, k] - s = sqrt((ux + bxx)**2 + (uy + byy)**2) - dxx = s * lam * (ux + bxx) / (s * lam + 1) - dyy = s * lam * (uy + byy) / (s * lam + 1) + # d_subproblem after reference [4] + if anisotropic: + s = ux + bxx + if s > 1/lam: + dxx = s - 1/lam + elif s < -1/lam: + dxx = s + 1/lam + else: + dxx = 0 + s = uy + byy + if s > 1/lam: + dyy = s - 1/lam + elif s < -1/lam: + dyy = s + 1/lam + else: + dyy = 0 + else: + s = sqrt((ux + bxx)**2 + (uy + byy)**2) + dxx = s * lam * (ux + bxx) / (s * lam + 1) + dyy = s * lam * (uy + byy) / (s * lam + 1) dx[r, c, k] = dxx dy[r, c, k] = dyy From 7809e020a199dc7e8e9a9f0651da81a943177264 Mon Sep 17 00:00:00 2001 From: Ferdinand Deger Date: Sat, 13 Jul 2013 12:38:27 +0200 Subject: [PATCH 290/736] minor changes in isortopic tv denoising - flag istropic in stead of anistropic => order of if clause - .. before red - whitespace --- skimage/filter/_denoise_cy.pyx | 40 ++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/skimage/filter/_denoise_cy.pyx b/skimage/filter/_denoise_cy.pyx index 6e803062..7f5f988e 100644 --- a/skimage/filter/_denoise_cy.pyx +++ b/skimage/filter/_denoise_cy.pyx @@ -200,7 +200,7 @@ def denoise_bilateral(image, Py_ssize_t win_size=5, sigma_range=None, return np.squeeze(out) -def denoise_tv_bregman(image, double weight, int max_iter=100, double eps=1e-3, anisotropic=False): +def denoise_tv_bregman(image, double weight, int max_iter=100, double eps=1e-3, isotropic=True): """Perform total-variation denoising using split-Bregman optimization. Total-variation denoising (also know as total-variation regularization) @@ -225,7 +225,8 @@ def denoise_tv_bregman(image, double weight, int max_iter=100, double eps=1e-3, max_iter: int, optional Maximal number of iterations used for the optimization. - anisotropic: boolean, optimal - switch between isotropic and anisotropic tv denoising + isotropic: boolean, optimal + Switch between isotropic and anisotropic tv denoising Returns ------- @@ -241,7 +242,7 @@ def denoise_tv_bregman(image, double weight, int max_iter=100, double eps=1e-3, .. [3] Pascal Getreuer, "Rudin–Osher–Fatemi Total Variation Denoising using Split Bregman" in Image Processing On Line on 2012–05–19, http://www.ipol.im/pub/art/2012/g-tvd/article_lr.pdf - [4] http://www.math.ucsb.edu/~cgarcia/UGProjects/BregmanAlgorithms_JacquelineBush.pdf + .. [4] http://www.math.ucsb.edu/~cgarcia/UGProjects/BregmanAlgorithms_JacquelineBush.pdf """ @@ -329,26 +330,27 @@ def denoise_tv_bregman(image, double weight, int max_iter=100, double eps=1e-3, byy = by[r, c, k] # d_subproblem after reference [4] - if anisotropic: - s = ux + bxx - if s > 1/lam: - dxx = s - 1/lam - elif s < -1/lam: - dxx = s + 1/lam - else: - dxx = 0 - s = uy + byy - if s > 1/lam: - dyy = s - 1/lam - elif s < -1/lam: - dyy = s + 1/lam - else: - dyy = 0 - else: + if isotropic: s = sqrt((ux + bxx)**2 + (uy + byy)**2) dxx = s * lam * (ux + bxx) / (s * lam + 1) dyy = s * lam * (uy + byy) / (s * lam + 1) + else: + s = ux + bxx + if s > 1 / lam: + dxx = s - 1/lam + elif s < -1 / lam: + dxx = s + 1 / lam + else: + dxx = 0 + s = uy + byy + if s > 1 / lam: + dyy = s - 1 / lam + elif s < -1 / lam: + dyy = s + 1 / lam + else: + dyy = 0 + dx[r, c, k] = dxx dy[r, c, k] = dyy From 49e8f28fe90aef9fb1dbdeb7ccf81f33df6249f4 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Sat, 13 Jul 2013 19:01:30 +0800 Subject: [PATCH 291/736] Final changes --- skimage/feature/_brief.py | 18 ++++++++-------- skimage/feature/tests/test_brief.py | 33 +++++++++++++++-------------- skimage/feature/util.py | 10 ++++----- 3 files changed, 31 insertions(+), 30 deletions(-) diff --git a/skimage/feature/_brief.py b/skimage/feature/_brief.py index d6f4e72e..4feb2698 100644 --- a/skimage/feature/_brief.py +++ b/skimage/feature/_brief.py @@ -111,17 +111,17 @@ def brief(image, keypoints, descriptor_size=256, mode='normal', patch_size=49, [ 0.375 , 0.6328125, 0.0390625, 0.328125 ], [ 0.625 , 0.3671875, 0.34375 , 0.0234375]]) >>> match_keypoints_brief(keypoints1, descriptors1, keypoints2, descriptors2) - array([[[ 2., 2.], - [ 2., 2.]], + array([[[ 2, 2], + [ 2, 2]], - [[ 2., 5.], - [ 2., 6.]], + [[ 2, 5], + [ 2, 6]], - [[ 5., 2.], - [ 6., 2.]], + [[ 5, 2], + [ 6, 2]], - [[ 5., 5.], - [ 6., 6.]]]) + [[ 5, 5], + [ 6, 6]]]) """ np.random.seed(sample_seed) @@ -216,7 +216,7 @@ def match_keypoints_brief(keypoints1, descriptors1, keypoints2, temp = distance > threshold row_check = ~ np.all(temp, axis = 1) matched_keypoints2 = keypoints2[np.argmin(distance, axis=1)] - matched_keypoint_pairs = np.zeros((np.sum(row_check), 2, 2)) + matched_keypoint_pairs = np.zeros((np.sum(row_check), 2, 2), dtype=np.intp) matched_keypoint_pairs[:, 0, :] = keypoints1[row_check] matched_keypoint_pairs[:, 1, :] = matched_keypoints2[row_check] diff --git a/skimage/feature/tests/test_brief.py b/skimage/feature/tests/test_brief.py index b8d26544..65c99af2 100644 --- a/skimage/feature/tests/test_brief.py +++ b/skimage/feature/tests/test_brief.py @@ -8,10 +8,10 @@ from skimage.feature import brief, match_keypoints_brief def test_brief_color_image_unsupported_error(): - """Brief descriptors can be evaluated on gray-scale images only.""" - img = np.zeros((20, 20, 3)) - keypoints = [[7, 5], [11, 13]] - assert_raises(ValueError, brief, img, keypoints) + """Brief descriptors can be evaluated on gray-scale images only.""" + img = np.zeros((20, 20, 3)) + keypoints = [[7, 5], [11, 13]] + assert_raises(ValueError, brief, img, keypoints) def test_match_keypoints_brief_lena_translation(): @@ -56,22 +56,23 @@ def test_match_keypoints_brief_lena_rotation(): matched_keypoints = match_keypoints_brief(keypoints1, descriptors1, keypoints2, descriptors2, threshold=0.07) - expected = np.array([[[ 263., 272.], - [ 234., 298.]], + expected = np.array([[[263, 272], + [234, 298]], - [[ 271., 120.], - [ 258., 146.]], + [[271, 120], + [258, 146]], - [[ 323., 164.], - [ 305., 195.]], + [[323, 164], + [305, 195]], - [[ 414., 70.], - [ 405., 111.]], + [[414, 70], + [405, 111]], - [[ 435., 181.], - [ 415., 223.]], + [[435, 181], + [415, 223]], + + [[454, 176], + [435, 221]]]) - [[ 454., 176.], - [ 435., 221.]]]) assert_array_equal(matched_keypoints, expected) diff --git a/skimage/feature/util.py b/skimage/feature/util.py index 8b5dd632..a7a3670f 100644 --- a/skimage/feature/util.py +++ b/skimage/feature/util.py @@ -14,14 +14,14 @@ def _remove_border_keypoints(image, keypoints, dist): def pairwise_hamming_distance(array1, array2): """Calculate hamming dissimilarity measure between two sets of - boolean vectors. + vectors. Parameters ---------- - array1 : (P1, D) array of dtype bool - P1 vectors of size D with boolean elements. - array2 : (P2, D) array of dtype bool - P2 vectors of size D with boolean elements. + array1 : (P1, D) array + P1 vectors of size D. + array2 : (P2, D) array + P2 vectors of size D. Returns ------- From 2bfaafd9e2bdc5470c1c38a1cf568f49d0f00077 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Sat, 13 Jul 2013 19:31:43 +0200 Subject: [PATCH 292/736] Radon transform: Redefine projection center for sinograms. This definition is chosen because it is simple to express in the documentation. No changes in accuracy are to be expected, but comparing sinograms and reconstructions before and after this commit will give different results in the cases where ``circle=False`` for ``radon`` or ``iradon``. --- skimage/transform/radon_transform.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/skimage/transform/radon_transform.py b/skimage/transform/radon_transform.py index 1ebd26c6..9fe1c6e1 100644 --- a/skimage/transform/radon_transform.py +++ b/skimage/transform/radon_transform.py @@ -82,12 +82,15 @@ def radon(image, theta=None, circle=False): else: diagonal = np.sqrt(2) * max(image.shape) pad = [int(np.ceil(diagonal - s)) for s in image.shape] - pad_width = [(p // 2, p - p // 2) for p in pad] + new_center = [(s + p) // 2 for s, p in zip(image.shape, pad)] + old_center = [s // 2 for s in image.shape] + pad_before = [nc - oc for oc, nc in zip(old_center, new_center)] + pad_width = [(pb, p - pb) for pb, p in zip(pad_before, pad)] padded_image = util.pad(image, pad_width, mode='constant', constant_values=0) out = np.zeros((max(padded_image.shape), len(theta))) - dh = pad[0] // 2 + image.shape[0] // 2 - dw = pad[1] // 2 + image.shape[1] // 2 + dh = padded_image.shape[0] // 2 + dw = padded_image.shape[1] // 2 shift0 = np.array([[1, 0, -dw], [0, 1, -dh], @@ -112,7 +115,10 @@ def radon(image, theta=None, circle=False): def _sinogram_circle_to_square(sinogram): diagonal = int(np.ceil(np.sqrt(2) * sinogram.shape[0])) pad = diagonal - sinogram.shape[0] - pad_width = ((pad // 2, pad - pad // 2), (0, 0)) + old_center = sinogram.shape[0] // 2 + new_center = diagonal // 2 + pad_before = new_center - old_center + pad_width = ((pad_before, pad - pad_before), (0, 0)) return util.pad(sinogram, pad_width, mode='constant', constant_values=0) @@ -216,9 +222,7 @@ def iradon(radon_image, theta=None, output_size=None, radon_filtered = radon_filtered[:radon_image.shape[0], :] reconstructed = np.zeros((output_size, output_size)) # Determine the center of the projections (= center of sinogram) - circle_size = int(np.floor(radon_image.shape[0] / np.sqrt(2))) - square_size = radon_image.shape[0] - mid_index = (square_size - circle_size) // 2 + circle_size // 2 + mid_index = radon_image.shape[0] // 2 x = output_size y = output_size From 47b6d0c5a625aae4fda3348944c6738e3331588f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Sat, 13 Jul 2013 19:34:22 +0200 Subject: [PATCH 293/736] Radon transform: Document rotation axis location. --- skimage/transform/radon_transform.py | 29 ++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/skimage/transform/radon_transform.py b/skimage/transform/radon_transform.py index 9fe1c6e1..37505bfa 100644 --- a/skimage/transform/radon_transform.py +++ b/skimage/transform/radon_transform.py @@ -33,7 +33,8 @@ def radon(image, theta=None, circle=False): Parameters ---------- image : array_like, dtype=float - Input image. + Input image. The rotation axis will be located in the pixel with + indices ``(image.shape[0] // 2, image.shape[1] // 2)``. theta : array_like, dtype=float, optional (default np.arange(180)) Projection angles (in degrees). circle : boolean, optional @@ -43,8 +44,10 @@ def radon(image, theta=None, circle=False): Returns ------- - output : ndarray - Radon transform (sinogram). + radon_image : ndarray + Radon transform (sinogram). The tomography rotation axis will lie + at the pixel index ``radon_image.shape[0] // 2`` along the 0th + dimension of ``radon_image``. Raises ------ @@ -134,7 +137,10 @@ def iradon(radon_image, theta=None, output_size=None, ---------- radon_image : array_like, dtype=float Image containing radon transform (sinogram). Each column of - the image corresponds to a projection along a different angle. + the image corresponds to a projection along a different angle. The + tomography rotation axis should lie at the pixel index + ``radon_image.shape[0] // 2`` along the 0th dimension of + ``radon_image``. theta : array_like, dtype=float, optional Reconstruction angles (in degrees). Default: m angles evenly spaced between 0 and 180 (if the shape of `radon_image` is (N, M)). @@ -155,7 +161,9 @@ def iradon(radon_image, theta=None, output_size=None, Returns ------- output : ndarray - Reconstructed image. + Reconstructed image. The rotation axis will be located in the pixel + with indices + ``(reconstructed.shape[0] // 2, reconstructed.shape[1] // 2)``. Notes ----- @@ -318,7 +326,10 @@ def iradon_sart(radon_image, theta=None, image=None, projection_shifts=None, ---------- radon_image : 2D array, dtype=float Image containing radon transform (sinogram). Each column of - the image corresponds to a projection along a different angle. + the image corresponds to a projection along a different angle. The + tomography rotation axis should lie at the pixel index + ``radon_image.shape[0] // 2`` along the 0th dimension of + ``radon_image``. theta : 1D array, dtype=float, optional Reconstruction angles (in degrees). Default: m angles evenly spaced between 0 and 180 (if the shape of `radon_image` is (N, M)). @@ -340,8 +351,10 @@ def iradon_sart(radon_image, theta=None, image=None, projection_shifts=None, Returns ------- - output : ndarray - Reconstructed image. + reconstructed : ndarray + Reconstructed image. The rotation axis will be located in the pixel + with indices + ``(reconstructed.shape[0] // 2, reconstructed.shape[1] // 2)``. Notes ----- From 41c6f6d740a5c8936d3319bcf13dd1f204d4fd01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Sat, 13 Jul 2013 19:47:38 +0200 Subject: [PATCH 294/736] radon: Reduce duplication; simplifications. --- skimage/transform/radon_transform.py | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/skimage/transform/radon_transform.py b/skimage/transform/radon_transform.py index 37505bfa..da01d1b4 100644 --- a/skimage/transform/radon_transform.py +++ b/skimage/transform/radon_transform.py @@ -68,6 +68,7 @@ def radon(image, theta=None, circle=False): if not np.all(reconstruction_circle | (image == 0)): raise ValueError('Image must be zero outside the reconstruction' ' circle') + # Crop image to make it square slices = [] for d in (0, 1): if image.shape[d] > min(image.shape): @@ -79,9 +80,6 @@ def radon(image, theta=None, circle=False): slices.append(slice(None)) slices = tuple(slices) padded_image = image[slices] - out = np.zeros((min(padded_image.shape), len(theta))) - dh = padded_image.shape[0] // 2 - dw = padded_image.shape[1] // 2 else: diagonal = np.sqrt(2) * max(image.shape) pad = [int(np.ceil(diagonal - s)) for s in image.shape] @@ -91,15 +89,16 @@ def radon(image, theta=None, circle=False): pad_width = [(pb, p - pb) for pb, p in zip(pad_before, pad)] padded_image = util.pad(image, pad_width, mode='constant', constant_values=0) - out = np.zeros((max(padded_image.shape), len(theta))) - dh = padded_image.shape[0] // 2 - dw = padded_image.shape[1] // 2 + # padded_image is always square + assert padded_image.shape[0] == padded_image.shape[1] + radon_image = np.zeros((padded_image.shape[0], len(theta))) + center = padded_image.shape[0] // 2 - shift0 = np.array([[1, 0, -dw], - [0, 1, -dh], + shift0 = np.array([[1, 0, -center], + [0, 1, -center], [0, 0, 1]]) - shift1 = np.array([[1, 0, dw], - [0, 1, dh], + shift1 = np.array([[1, 0, center], + [0, 1, center], [0, 0, 1]]) def build_rotation(theta): @@ -111,8 +110,8 @@ def radon(image, theta=None, circle=False): for i in range(len(theta)): rotated = _warp_fast(padded_image, build_rotation(theta[i])) - out[:, i] = rotated.sum(0) - return out + radon_image[:, i] = rotated.sum(0) + return radon_image def _sinogram_circle_to_square(sinogram): @@ -160,7 +159,7 @@ def iradon(radon_image, theta=None, output_size=None, Returns ------- - output : ndarray + reconstructed : ndarray Reconstructed image. The rotation axis will be located in the pixel with indices ``(reconstructed.shape[0] // 2, reconstructed.shape[1] // 2)``. From 288ee694834ee20c4b750085a40ef598fe807e2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Sat, 13 Jul 2013 19:50:20 +0200 Subject: [PATCH 295/736] radon: Correct docstring of order_angles_golden_ratio. --- skimage/transform/radon_transform.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/skimage/transform/radon_transform.py b/skimage/transform/radon_transform.py index da01d1b4..6ae6e1b9 100644 --- a/skimage/transform/radon_transform.py +++ b/skimage/transform/radon_transform.py @@ -269,9 +269,11 @@ def order_angles_golden_ratio(theta): Returns ------- - indices : 1D array of unsigned integers - Indices into ``theta`` such that ``theta[indices]`` gives the - approximate golden ratio ordering of the projections. + indices_generator : generator yielding unsigned integers + The returned generator yields indices into ``theta`` such that + ``theta[indices]`` gives the approximate golden ratio ordering + of the projections. In total, ``len(theta)`` indices are yielded. + All non-negative integers < ``len(theta)`` are yielded exactly once. Notes ----- From 462173a53adb487b1200ed9dcc2639d7cee32fc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Sat, 13 Jul 2013 20:06:59 +0200 Subject: [PATCH 296/736] Radon transform: Include boundary in reconstruction circle. A test criterion needed to be relaxed slightly to have tests still passing. This is ok, as the reconstruction circle is now larger, meaning larger errors should be expected. Moreover, the test in question uses random data, and changing the seed causes greater changes in accuracy than the amount the test criterion was relaxed by. --- skimage/transform/radon_transform.py | 10 ++++------ skimage/transform/tests/test_radon_transform.py | 6 +++--- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/skimage/transform/radon_transform.py b/skimage/transform/radon_transform.py index 6ae6e1b9..57935d43 100644 --- a/skimage/transform/radon_transform.py +++ b/skimage/transform/radon_transform.py @@ -64,7 +64,7 @@ def radon(image, theta=None, circle=False): radius = min(image.shape) // 2 c0, c1 = np.ogrid[0:image.shape[0], 0:image.shape[1]] reconstruction_circle = ((c0 - image.shape[0] // 2)**2 - + (c1 - image.shape[1] // 2)**2) < radius**2 + + (c1 - image.shape[1] // 2)**2) <= radius**2 if not np.all(reconstruction_circle | (image == 0)): raise ValueError('Image must be zero outside the reconstruction' ' circle') @@ -231,9 +231,7 @@ def iradon(radon_image, theta=None, output_size=None, # Determine the center of the projections (= center of sinogram) mid_index = radon_image.shape[0] // 2 - x = output_size - y = output_size - [X, Y] = np.mgrid[0.0:x, 0.0:y] + [X, Y] = np.mgrid[0:output_size, 0:output_size] xpr = X - int(output_size) // 2 ypr = Y - int(output_size) // 2 @@ -250,8 +248,8 @@ def iradon(radon_image, theta=None, output_size=None, backprojected = interpolant(t) reconstructed += backprojected if circle: - radius = (output_size - 1) // 2 - reconstruction_circle = (xpr**2 + ypr**2) < radius**2 + radius = output_size // 2 + reconstruction_circle = (xpr**2 + ypr**2) <= radius**2 reconstructed[~reconstruction_circle] = 0. return reconstructed * np.pi / (2 * len(th)) diff --git a/skimage/transform/tests/test_radon_transform.py b/skimage/transform/tests/test_radon_transform.py index 1d82b29a..32d8780c 100644 --- a/skimage/transform/tests/test_radon_transform.py +++ b/skimage/transform/tests/test_radon_transform.py @@ -210,7 +210,7 @@ def _random_circle(shape): c0, c1 = np.ogrid[0:shape[0], 0:shape[1]] r = np.sqrt((c0 - shape[0] // 2)**2 + (c1 - shape[1] // 2)**2) radius = min(shape) // 2 - image[r >= radius] = 0. + image[r > radius] = 0. return image @@ -236,7 +236,7 @@ def test_radon_circle(): average_mass = mass.mean() relative_error = np.abs(mass - average_mass) / average_mass print(relative_error.max(), relative_error.mean()) - assert np.all(relative_error < 3e-3) + assert np.all(relative_error < 3.2e-3) def check_sinogram_circle_to_square(size): @@ -284,7 +284,7 @@ def check_radon_iradon_circle(interpolation, shape, output_size): # Find the reconstruction circle, set reconstruction to zero outside c0, c1 = np.ogrid[0:width, 0:width] r = np.sqrt((c0 - width // 2)**2 + (c1 - width // 2)**2) - reconstruction_rectangle[r >= radius] = 0. + reconstruction_rectangle[r > radius] = 0. print(reconstruction_circle.shape) print(reconstruction_rectangle.shape) np.allclose(reconstruction_rectangle, reconstruction_circle) From 60444ee4d333c4079e489abbfd58b4064eb9d352 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Sat, 13 Jul 2013 20:11:49 +0200 Subject: [PATCH 297/736] Radon transform: PEP8 fixes. --- skimage/transform/radon_transform.py | 6 +++--- skimage/transform/tests/test_radon_transform.py | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/skimage/transform/radon_transform.py b/skimage/transform/radon_transform.py index 57935d43..db43090b 100644 --- a/skimage/transform/radon_transform.py +++ b/skimage/transform/radon_transform.py @@ -374,9 +374,9 @@ def iradon_sart(radon_image, theta=None, image=None, projection_shifts=None, ---------- .. [1] AC Kak, M Slaney, "Principles of Computerized Tomographic Imaging", IEEE Press 1988. - .. [2] AH Andersen, AC Kak, "Simultaneous algebraic reconstruction technique - (SART): a superior implementation of the ART algorithm", Ultrasonic - Imaging 6 pp 81--94 (1984) + .. [2] AH Andersen, AC Kak, "Simultaneous algebraic reconstruction + technique (SART): a superior implementation of the ART algorithm", + Ultrasonic Imaging 6 pp 81--94 (1984) .. [3] S Kaczmarz, "Angenäherte auflösung von systemen linearer gleichungen", Bulletin International de l’Academie Polonaise des Sciences et des Lettres 35 pp 355--357 (1937) diff --git a/skimage/transform/tests/test_radon_transform.py b/skimage/transform/tests/test_radon_transform.py index 32d8780c..6fbc3f62 100644 --- a/skimage/transform/tests/test_radon_transform.py +++ b/skimage/transform/tests/test_radon_transform.py @@ -143,6 +143,7 @@ def test_radon_iradon(): # cubic interpolation is slow; only run one test for it yield check_radon_iradon, 'cubic', 'shepp-logan' + def test_iradon_angles(): """ Test with different number of projections From cb49e1ce7d2f05ecf38be5d8de0459d7c28c8e81 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Sun, 14 Jul 2013 05:27:32 +0800 Subject: [PATCH 298/736] Replacing np.all by equivalent np.any --- skimage/feature/_brief.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/feature/_brief.py b/skimage/feature/_brief.py index 4feb2698..30c0934f 100644 --- a/skimage/feature/_brief.py +++ b/skimage/feature/_brief.py @@ -214,7 +214,7 @@ def match_keypoints_brief(keypoints1, descriptors1, keypoints2, distance = pairwise_hamming_distance(descriptors1, descriptors2) temp = distance > threshold - row_check = ~ np.all(temp, axis = 1) + row_check = np.any(~temp, axis = 1) matched_keypoints2 = keypoints2[np.argmin(distance, axis=1)] matched_keypoint_pairs = np.zeros((np.sum(row_check), 2, 2), dtype=np.intp) matched_keypoint_pairs[:, 0, :] = keypoints1[row_check] From 4549204507a44b648785272cbae43d5221e8a28b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Sun, 14 Jul 2013 17:39:48 +0200 Subject: [PATCH 299/736] iradon: Clean up filter code. --- skimage/transform/radon_transform.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/skimage/transform/radon_transform.py b/skimage/transform/radon_transform.py index db43090b..fc827c8b 100644 --- a/skimage/transform/radon_transform.py +++ b/skimage/transform/radon_transform.py @@ -15,7 +15,7 @@ References: """ from __future__ import division import numpy as np -from scipy.fftpack import fftshift, fft, ifft +from scipy.fftpack import fft, ifft, fftfreq from scipy.interpolate import interp1d from ._warps_cy import _warp_fast from ._radon_transform import sart_projection_update @@ -203,26 +203,26 @@ def iradon(radon_image, theta=None, output_size=None, img = util.pad(radon_image, pad_width, mode='constant', constant_values=0) # Construct the Fourier filter - f = fftshift(abs(np.mgrid[-1:1:2 / projection_size_padded])).reshape(-1, 1) - w = 2 * np.pi * f - # Start from first element to avoid divide by zero + f = fftfreq(projection_size_padded).reshape(-1, 1) # digital frequency + omega = 2 * np.pi * f # angular frequency + fourier_filter = 2 * np.abs(f) # ramp filter if filter == "ramp": pass elif filter == "shepp-logan": - f[1:] = f[1:] * np.sin(w[1:] / 2) / (w[1:] / 2) + # Start from first element to avoid divide by zero + fourier_filter[1:] = fourier_filter[1:] * np.sin(omega[1:]) / omega[1:] elif filter == "cosine": - f[1:] = f[1:] * np.cos(w[1:] / 2) + fourier_filter *= np.cos(omega) elif filter == "hamming": - f[1:] = f[1:] * (0.54 + 0.46 * np.cos(w[1:])) + fourier_filter *= (0.54 + 0.46 * np.cos(omega / 2)) elif filter == "hann": - f[1:] = f[1:] * (1 + np.cos(w[1:])) / 2 + fourier_filter *= (1 + np.cos(omega / 2)) / 2 elif filter is None: f[1:] = 1 else: raise ValueError("Unknown filter: %s" % filter) - filter_ft = np.tile(f, (1, len(theta))) # Apply filter in Fourier domain - projection = fft(img, axis=0) * filter_ft + projection = fft(img, axis=0) * fourier_filter radon_filtered = np.real(ifft(projection, axis=0)) # Resize filtered image back to original size From d5b72f91ab4c5db34aa29c2f44d730e2cd0f3e4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Sun, 14 Jul 2013 17:40:17 +0200 Subject: [PATCH 300/736] iradon: Do not suppress 0 frequency for filter=None. --- skimage/transform/radon_transform.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/transform/radon_transform.py b/skimage/transform/radon_transform.py index fc827c8b..101d09c6 100644 --- a/skimage/transform/radon_transform.py +++ b/skimage/transform/radon_transform.py @@ -218,7 +218,7 @@ def iradon(radon_image, theta=None, output_size=None, elif filter == "hann": fourier_filter *= (1 + np.cos(omega / 2)) / 2 elif filter is None: - f[1:] = 1 + fourier_filter[:] = 1 else: raise ValueError("Unknown filter: %s" % filter) # Apply filter in Fourier domain From 1c7947fad1e5608d47f1a46ff8bf6da3275c0226 Mon Sep 17 00:00:00 2001 From: Horea Christian Date: Sun, 14 Jul 2013 22:21:51 +0200 Subject: [PATCH 301/736] Renamed parameter --- skimage/util/montage.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/skimage/util/montage.py b/skimage/util/montage.py index b9d8defa..f75af3b7 100644 --- a/skimage/util/montage.py +++ b/skimage/util/montage.py @@ -6,7 +6,7 @@ from .. import exposure EPSILON = 1e-6 -def montage2d(arr_in, fill='mean', rescale_intensity=False, output_shape=(0, 0)): +def montage2d(arr_in, fill='mean', rescale_intensity=False, grid_shape=(0, 0)): """Create a 2-dimensional 'montage' from a 3-dimensional input array representing an ensemble of equally shaped 2-dimensional images. @@ -36,8 +36,8 @@ def montage2d(arr_in, fill='mean', rescale_intensity=False, output_shape=(0, 0)) is not an integer. If 'mean' is chosen, then fill = arr_in.mean(). rescale_intensity: bool, optional Whether to rescale the intensity of each image to [0, 1]. - output_shape: tuple, optional - The desired aspect ratio for the montage (default is square). + grid_shape: tuple, optional + The desired grid shape for the montage (the default aspect ratio is square). Returns ------- @@ -67,7 +67,7 @@ def montage2d(arr_in, fill='mean', rescale_intensity=False, output_shape=(0, 0)) [ 10. 11. 5.5 5.5]] >>> print(arr_in.mean()) 5.5 - >>> arr_out_nonsquare = montage2d(arr_in, output_shape = (3, 4)) + >>> arr_out_nonsquare = montage2d(arr_in, grid_shape = (3, 4)) >>> print(arr_out_nonsquare) [[ 0. 1. 4. 5. ] [ 2. 3. 6. 7. ] @@ -87,10 +87,10 @@ def montage2d(arr_in, fill='mean', rescale_intensity=False, output_shape=(0, 0)) arr_in[i] = exposure.rescale_intensity(arr_in[i]) # -- determine alpha - if output_shape == (0, 0): + if grid_shape == (0, 0): alpha_y = alpha_x = int(np.ceil(np.sqrt(n_images))) else: - alpha_y, alpha_x = output_shape + alpha_y, alpha_x = grid_shape # -- fill missing patches if fill == 'mean': From 983c6589afeac28d40b39abb6e0c4de143270f9f Mon Sep 17 00:00:00 2001 From: Ferdinand Deger Date: Mon, 15 Jul 2013 07:57:04 +0200 Subject: [PATCH 302/736] eleminate empty line --- skimage/filter/_denoise_cy.pyx | 1 - 1 file changed, 1 deletion(-) diff --git a/skimage/filter/_denoise_cy.pyx b/skimage/filter/_denoise_cy.pyx index 7f5f988e..b80b8e6b 100644 --- a/skimage/filter/_denoise_cy.pyx +++ b/skimage/filter/_denoise_cy.pyx @@ -224,7 +224,6 @@ def denoise_tv_bregman(image, double weight, int max_iter=100, double eps=1e-3, max_iter: int, optional Maximal number of iterations used for the optimization. - isotropic: boolean, optimal Switch between isotropic and anisotropic tv denoising From 7e9ae43ae9a2f3728ea42b4ccffae8a1d93592a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Mon, 15 Jul 2013 13:57:35 +0200 Subject: [PATCH 303/736] docs: Makefile respects variables from environment. --- doc/Makefile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/Makefile b/doc/Makefile index 311ce3ca..beadac7d 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -2,9 +2,9 @@ # # You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -PAPER = +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +PAPER ?= # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 From 8796262b0ba53516954d3b6978d81af18c226ca6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jostein=20B=C3=B8=20Fl=C3=B8ystad?= Date: Mon, 15 Jul 2013 14:01:10 +0200 Subject: [PATCH 304/736] doc: Allow specifying python binary. --- doc/Makefile | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/doc/Makefile b/doc/Makefile index beadac7d..a593aa56 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -2,6 +2,7 @@ # # You can set these variables from the command line. +PYTHON ?= python SPHINXOPTS ?= SPHINXBUILD ?= sphinx-build PAPER ?= @@ -36,14 +37,14 @@ clean: -find ./source/auto_examples/* -type f | grep -v blank | xargs rm -f api: @mkdir -p source/api - python tools/build_modref_templates.py + $(PYTHON) tools/build_modref_templates.py @echo "Build API docs...done." random_gallery: - @cd source && python random_gallery.py + @cd source && $(PYTHON) random_gallery.py coveragetable: - @cd source && python coverage_generator.py + @cd source && $(PYTHON) coverage_generator.py html: api coveragetable random_gallery $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(DEST)/html @@ -120,10 +121,10 @@ doctest: "results in build/doctest/output.txt." gh-pages: - python gh-pages.py + $(PYTHON) gh-pages.py gitwash: - python tools/gitwash/gitwash_dumper.py source scikit-image \ + $(PYTHON) tools/gitwash/gitwash_dumper.py source scikit-image \ --project-url=http://scikit-image.org \ --project-ml-url=http://groups.google.com/group/scikit-image \ --repo-name=scikit-image \ From 875e47abcc77da93ee83b701937fa6f8458e1c8a Mon Sep 17 00:00:00 2001 From: Chintak Sheth Date: Mon, 15 Jul 2013 18:50:06 +0530 Subject: [PATCH 305/736] Docstring for _dilate and _erode were interchanged --- skimage/morphology/cmorph.pyx | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/skimage/morphology/cmorph.pyx b/skimage/morphology/cmorph.pyx index 070c881b..c96039eb 100644 --- a/skimage/morphology/cmorph.pyx +++ b/skimage/morphology/cmorph.pyx @@ -12,20 +12,21 @@ def _dilate(np.ndarray[np.uint8_t, ndim=2] image, np.ndarray[np.uint8_t, ndim=2] selem, np.ndarray[np.uint8_t, ndim=2] out=None, char shift_x=0, char shift_y=0): - """Return greyscale morphological erosion of an image. + """Return greyscale morphological dilation of an image. - Morphological erosion sets a pixel at (i,j) to the minimum over all pixels - in the neighborhood centered at (i,j). Erosion shrinks bright regions and - enlarges dark regions. + Morphological dilation sets a pixel at (i,j) to the maximum over all pixels + in the neighborhood centered at (i,j). Dilation enlarges bright regions + and shrinks dark regions. Parameters ---------- + image : ndarray Image array. selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. out : ndarray - The array to store the result of the morphology. If None is + The array to store the result of the morphology. If None, is passed, a new array will be allocated. shift_x, shift_y : bool shift structuring element about center point. This only affects @@ -33,9 +34,10 @@ def _dilate(np.ndarray[np.uint8_t, ndim=2] image, Returns ------- - eroded : uint8 array - The result of the morphological erosion. + dilated : uint8 array + The result of the morphological dilation. """ + cdef Py_ssize_t rows = image.shape[0] cdef Py_ssize_t cols = image.shape[1] cdef Py_ssize_t srows = selem.shape[0] @@ -90,21 +92,20 @@ def _erode(np.ndarray[np.uint8_t, ndim=2] image, np.ndarray[np.uint8_t, ndim=2] selem, np.ndarray[np.uint8_t, ndim=2] out=None, char shift_x=0, char shift_y=0): - """Return greyscale morphological dilation of an image. + """Return greyscale morphological erosion of an image. - Morphological dilation sets a pixel at (i,j) to the maximum over all pixels - in the neighborhood centered at (i,j). Dilation enlarges bright regions - and shrinks dark regions. + Morphological erosion sets a pixel at (i,j) to the minimum over all pixels + in the neighborhood centered at (i,j). Erosion shrinks bright regions and + enlarges dark regions. Parameters ---------- - image : ndarray Image array. selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. out : ndarray - The array to store the result of the morphology. If None, is + The array to store the result of the morphology. If None is passed, a new array will be allocated. shift_x, shift_y : bool shift structuring element about center point. This only affects @@ -112,8 +113,8 @@ def _erode(np.ndarray[np.uint8_t, ndim=2] image, Returns ------- - dilated : uint8 array - The result of the morphological dilation. + eroded : uint8 array + The result of the morphological erosion. """ cdef Py_ssize_t rows = image.shape[0] From 486909c5f5cacf03c516c28ab9416660c395d0e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Mon, 15 Jul 2013 21:51:55 +0200 Subject: [PATCH 306/736] Rename and combine local_* functions to block_reduce --- skimage/measure/__init__.py | 8 +- skimage/measure/block.py | 49 ++++ skimage/measure/local.py | 226 ------------------ .../tests/{test_local.py => test_block.py} | 35 ++- 4 files changed, 68 insertions(+), 250 deletions(-) create mode 100644 skimage/measure/block.py delete mode 100644 skimage/measure/local.py rename skimage/measure/tests/{test_local.py => test_block.py} (70%) diff --git a/skimage/measure/__init__.py b/skimage/measure/__init__.py index 26eb179c..423aa9a7 100755 --- a/skimage/measure/__init__.py +++ b/skimage/measure/__init__.py @@ -3,7 +3,7 @@ from ._regionprops import regionprops, perimeter from ._structural_similarity import structural_similarity from ._polygon import approximate_polygon, subdivide_polygon from .fit import LineModel, CircleModel, EllipseModel, ransac -from .local import local_sum, local_mean, local_median, local_min, local_max +from .block import block_reduce __all__ = ['find_contours', @@ -16,8 +16,4 @@ __all__ = ['find_contours', 'CircleModel', 'EllipseModel', 'ransac', - 'local_sum', - 'local_mean', - 'local_median', - 'local_min', - 'local_max'] + 'block_reduce'] diff --git a/skimage/measure/block.py b/skimage/measure/block.py new file mode 100644 index 00000000..7bef1220 --- /dev/null +++ b/skimage/measure/block.py @@ -0,0 +1,49 @@ +import numpy as np +from skimage.util import view_as_blocks, pad + + +def block_reduce(image, block_size, func=np.sum, cval=0): + """Down-sample image by applying function to local blocks. + + Parameters + ---------- + image : ndarray + N-dimensional input image. + block_size : array_like + Array containing down-sampling integer factor along each axis. + func : callable + Function object which is used to calculate the return value for each + local block. This function must implement an ``axis`` parameter such as + ``numpy.sum`` or ``numpy.min``. + cval : float + Constant padding value if image is not perfectly divisible by the + block size. + + Returns + ------- + image : ndarray + Down-sampled image with same number of dimensions as input image. + + """ + + if len(block_size) != image.ndim: + raise ValueError("`block_size` must have the same length " + "as `image.shape`.") + + pad_width = [] + for i in range(len(block_size)): + if image.shape[i] % block_size[i] != 0: + after_width = block_size[i] - (image.shape[i] % block_size[i]) + else: + after_width = 0 + pad_width.append((0, after_width)) + + image = pad(image, pad_width=pad_width, mode='constant', + constant_values=cval) + + out = view_as_blocks(image, block_size) + + for i in range(len(out.shape) // 2): + out = func(out, axis=-1) + + return out diff --git a/skimage/measure/local.py b/skimage/measure/local.py deleted file mode 100644 index bcb587cf..00000000 --- a/skimage/measure/local.py +++ /dev/null @@ -1,226 +0,0 @@ -import numpy as np -from skimage.util import view_as_blocks, pad - - -def _local_func(image, block_size, func, cval): - """Down-sample image by applying function to local blocks. - - Parameters - ---------- - image : ndarray - N-dimensional input image. - block_size : array_like - Array containing down-sampling integer factor along each axis. - func : object - Function object which is used to calculate the return value for each - local block, e.g. `numpy.sum`. - cval : float, optional - Constant padding value if image is not perfectly divisible by the - block size. - - Returns - ------- - image : ndarray - Down-sampled image with same number of dimensions as input image. - - """ - - if len(block_size) != image.ndim: - raise ValueError("`block_size` must have the same length " - "as `image.shape`.") - - pad_width = [] - for i in range(len(block_size)): - if image.shape[i] % block_size[i] != 0: - after_width = block_size[i] - (image.shape[i] % block_size[i]) - else: - after_width = 0 - pad_width.append((0, after_width)) - - image = pad(image, pad_width=pad_width, mode='constant', - constant_values=cval) - - out = view_as_blocks(image, block_size) - - for i in range(len(out.shape) // 2): - out = func(out, axis=-1) - - return out - - -def local_sum(image, block_size, cval=0): - """Sum elements in local blocks. - - The image is padded with zeros if it is not perfectly divisible by the - block size. - - Parameters - ---------- - image : ndarray - N-dimensional input image. - block_size : array_like - Array containing down-sampling integer factor along each axis. - cval : float, optional - Constant padding value if image is not perfectly divisible by the - block size. - - Returns - ------- - image : ndarray - Down-sampled image with same number of dimensions as input image. - - Example - ------- - >>> a = np.arange(15).reshape(3, 5) - >>> a - image([[ 0, 1, 2, 3, 4], - [ 5, 6, 7, 8, 9], - [10, 11, 12, 13, 14]]) - >>> block_sum(a, (2, 3)) - image([[21, 24], - [33, 27]]) - - """ - return _local_func(image, block_size, np.sum, cval) - - -def local_mean(image, block_size, cval=0): - """Average elements in local blocks. - - The image is padded with zeros if it is not perfectly divisible by the - block size. - - Parameters - ---------- - image : ndarray - N-dimensional input image. - block_size : array_like - Array containing down-sampling integer factor along each axis. - cval : float, optional - Constant padding value if image is not perfectly divisible by the - block size. - - Returns - ------- - image : ndarray - Down-sampled image with same number of dimensions as input image. - - Example - ------- - >>> a = np.arange(15).reshape(3, 5) - >>> a - image([[ 0, 1, 2, 3, 4], - [ 5, 6, 7, 8, 9], - [10, 11, 12, 13, 14]]) - >>> block_mean(a, (2, 3)) - array([[ 3.5, 4. ], - [ 5.5, 4.5]]) - - """ - return _local_func(image, block_size, np.mean, cval) - - -def local_median(image, block_size, cval=0): - """Median element in local blocks. - - The image is padded with zeros if it is not perfectly divisible by the - block size. - - Parameters - ---------- - image : ndarray - N-dimensional input image. - block_size : array_like - Array containing down-sampling integer factor along each axis. - cval : float, optional - Constant padding value if image is not perfectly divisible by the - block size. - - Returns - ------- - image : ndarray - Down-sampled image with same number of dimensions as input image. - - Example - ------- - >>> a = np.array([[1, 5, 100], [0, 5, 1000]]) - >>> a - array([[ 1, 5, 100], - [ 0, 5, 1000]]) - >>> block_median(a, (2, 3)) - array([[ 5.]]) - - """ - return _local_func(image, block_size, np.median, cval) - - -def local_min(image, block_size, cval=0): - """Minimum element in local blocks. - - The image is padded with zeros if it is not perfectly divisible by the - block size. - - Parameters - ---------- - image : ndarray - N-dimensional input image. - block_size : array_like - Array containing down-sampling integer factor along each axis. - cval : float, optional - Constant padding value if image is not perfectly divisible by the - block size. - - Returns - ------- - image : ndarray - Down-sampled image with same number of dimensions as input image. - - Example - ------- - >>> a = np.arange(15).reshape(3, 5) - >>> a - image([[ 0, 1, 2, 3, 4], - [ 5, 6, 7, 8, 9], - [10, 11, 12, 13, 14]]) - >>> block_min(a, (2, 2)) - array([[0, 2, 0], - [0, 0, 0]]) - - """ - return _local_func(image, block_size, np.min, cval) - - -def local_max(image, block_size, cval=0): - """Maximum element in local blocks. - - The image is padded with zeros if it is not perfectly divisible by the - block size. - - Parameters - ---------- - image : ndarray - N-dimensional input image. - block_size : array_like - Array containing down-sampling integer factor along each axis. - cval : float, optional - Constant padding value if image is not perfectly divisible by the - block size. - - Returns - ------- - image : ndarray - Down-sampled image with same number of dimensions as input image. - - Example - ------- - >>> a = np.arange(15).reshape(3, 5) - >>> a - image([[ 0, 1, 2, 3, 4], - [ 5, 6, 7, 8, 9], - [10, 11, 12, 13, 14]]) - >>> block_max(a, (2, 3)) - array([[ 7, 9], - [12, 14]]) - - """ - return _local_func(image, block_size, np.max, cval) diff --git a/skimage/measure/tests/test_local.py b/skimage/measure/tests/test_block.py similarity index 70% rename from skimage/measure/tests/test_local.py rename to skimage/measure/tests/test_block.py index fc01b7b9..a8bc62a9 100644 --- a/skimage/measure/tests/test_local.py +++ b/skimage/measure/tests/test_block.py @@ -1,78 +1,77 @@ import numpy as np from numpy.testing import assert_array_equal -from skimage.measure import (local_sum, local_mean, local_median, local_min, - local_max) +from skimage.measure import block_reduce -def test_local_sum(): +def test_block_reduce_sum(): image1 = np.arange(4 * 6).reshape(4, 6) - out1 = local_sum(image1, (2, 3)) + out1 = block_reduce(image1, (2, 3)) expected1 = np.array([[ 24, 42], [ 96, 114]]) assert_array_equal(expected1, out1) image2 = np.arange(5 * 8).reshape(5, 8) - out2 = local_sum(image2, (3, 3)) + out2 = block_reduce(image2, (3, 3)) expected2 = np.array([[ 81, 108, 87], [174, 192, 138]]) assert_array_equal(expected2, out2) -def test_local_mean(): +def test_block_reduce_mean(): image1 = np.arange(4 * 6).reshape(4, 6) - out1 = local_mean(image1, (2, 3)) + out1 = block_reduce(image1, (2, 3), func=np.mean) expected1 = np.array([[ 4., 7.], [ 16., 19.]]) assert_array_equal(expected1, out1) image2 = np.arange(5 * 8).reshape(5, 8) - out2 = local_mean(image2, (4, 5)) + out2 = block_reduce(image2, (4, 5), func=np.mean) expected2 = np.array([[14. , 10.8], [ 8.5, 5.7]]) assert_array_equal(expected2, out2) -def test_local_median(): +def test_block_reduce_median(): image1 = np.arange(4 * 6).reshape(4, 6) - out1 = local_median(image1, (2, 3)) + out1 = block_reduce(image1, (2, 3), func=np.median) expected1 = np.array([[ 4., 7.], [ 16., 19.]]) assert_array_equal(expected1, out1) image2 = np.arange(5 * 8).reshape(5, 8) - out2 = local_median(image2, (4, 5)) + out2 = block_reduce(image2, (4, 5), func=np.median) expected2 = np.array([[ 14., 17.], [ 0., 0.]]) assert_array_equal(expected2, out2) image3 = np.array([[1, 5, 5, 5], [5, 5, 5, 1000]]) - out3 = local_median(image3, (2, 4)) + out3 = block_reduce(image3, (2, 4), func=np.median) assert_array_equal(5, out3) -def test_local_min(): +def test_block_reduce_min(): image1 = np.arange(4 * 6).reshape(4, 6) - out1 = local_min(image1, (2, 3)) + out1 = block_reduce(image1, (2, 3), func=np.min) expected1 = np.array([[ 0, 3], [12, 15]]) assert_array_equal(expected1, out1) image2 = np.arange(5 * 8).reshape(5, 8) - out2 = local_min(image2, (4, 5)) + out2 = block_reduce(image2, (4, 5), func=np.min) expected2 = np.array([[0, 0], [0, 0]]) assert_array_equal(expected2, out2) -def test_local_max(): +def test_block_reduce_max(): image1 = np.arange(4 * 6).reshape(4, 6) - out1 = local_max(image1, (2, 3)) + out1 = block_reduce(image1, (2, 3), func=np.max) expected1 = np.array([[ 8, 11], [20, 23]]) assert_array_equal(expected1, out1) image2 = np.arange(5 * 8).reshape(5, 8) - out2 = local_max(image2, (4, 5)) + out2 = block_reduce(image2, (4, 5), func=np.max) expected2 = np.array([[28, 31], [36, 39]]) assert_array_equal(expected2, out2) From 7efb010051dbf505d73eea6184348997ee0d1767 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Mon, 15 Jul 2013 22:00:05 +0200 Subject: [PATCH 307/736] Add short doc string example --- skimage/measure/block.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/skimage/measure/block.py b/skimage/measure/block.py index 7bef1220..fad5668c 100644 --- a/skimage/measure/block.py +++ b/skimage/measure/block.py @@ -24,6 +24,34 @@ def block_reduce(image, block_size, func=np.sum, cval=0): image : ndarray Down-sampled image with same number of dimensions as input image. + Examples + -------- + >>> from skimage.measure import block_reduce + >>> image = np.arange(3*3*4).reshape(3, 3, 4) + >>> image + array([[[ 0, 1, 2, 3], + [ 4, 5, 6, 7], + [ 8, 9, 10, 11]], + + [[12, 13, 14, 15], + [16, 17, 18, 19], + [20, 21, 22, 23]], + + [[24, 25, 26, 27], + [28, 29, 30, 31], + [32, 33, 34, 35]]]) + >>> block_reduce(image, block_size=(3, 3, 1), func=np.mean) + array([[[ 16., 17., 18., 19.]]]) + >>> block_reduce(image, block_size=(1, 3, 4), func=np.max) + array([[[11]], + + [[23]], + + [[35]]]) + >>> block_reduce(image, block_size=(3, 1, 4), func=np.max) + array([[[27], + [31], + [35]]]) """ if len(block_size) != image.ndim: From 2466df39e1a575626918395eb20a23301f2c754a Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Tue, 16 Jul 2013 20:16:23 +0530 Subject: [PATCH 308/736] Stylistic changes --- skimage/feature/__init__.py | 5 ++- skimage/feature/_brief.py | 25 ++++++----- skimage/feature/tests/test_brief.py | 68 ++++++++++++++--------------- skimage/feature/tests/test_util.py | 40 ++++++++--------- skimage/feature/util.py | 3 +- 5 files changed, 72 insertions(+), 69 deletions(-) diff --git a/skimage/feature/__init__.py b/skimage/feature/__init__.py index 8df1dc10..d0c51fb0 100644 --- a/skimage/feature/__init__.py +++ b/skimage/feature/__init__.py @@ -2,8 +2,9 @@ from ._daisy import daisy from ._hog import hog from .texture import greycomatrix, greycoprops, local_binary_pattern from .peak import peak_local_max -from .corner import (corner_kitchen_rosenfeld, corner_harris, corner_shi_tomasi, - corner_foerstner, corner_subpix, corner_peaks) +from .corner import (corner_kitchen_rosenfeld, corner_harris, + corner_shi_tomasi, corner_foerstner, corner_subpix, + corner_peaks) from .corner_cy import corner_moravec from .template import match_template from ._brief import brief, match_keypoints_brief diff --git a/skimage/feature/_brief.py b/skimage/feature/_brief.py index 30c0934f..22b8e75a 100644 --- a/skimage/feature/_brief.py +++ b/skimage/feature/_brief.py @@ -31,7 +31,7 @@ def brief(image, keypoints, descriptor_size=256, mode='normal', patch_size=49, length patch_size, pixel pairs are sampled using the `mode` parameter to build the descriptors using intensity comparison. The value of `sample_seed` should be the same for the images to be matched while - building the descriptors. Default is 1. + building the descriptors. Default is 1. variance : float Variance of the Gaussian Low Pass filter applied on the image to alleviate noise sensitivity. Default is 2. @@ -75,7 +75,7 @@ def brief(image, keypoints, descriptor_size=256, mode='normal', patch_size=49, [2, 5], [5, 2], [5, 5]]) - >>> descriptors1, keypoints1 = brief(square1, keypoints1, patch_size = 5) + >>> descriptors1, keypoints1 = brief(square1, keypoints1, patch_size=5) >>> keypoints1 array([[2, 2], [2, 5], @@ -99,7 +99,7 @@ def brief(image, keypoints, descriptor_size=256, mode='normal', patch_size=49, [2, 6], [6, 2], [6, 6]]) - >>> descriptors2, keypoints2 = brief(square2, keypoints2, patch_size = 5) + >>> descriptors2, keypoints2 = brief(square2, keypoints2, patch_size=5) >>> keypoints2 array([[2, 2], [2, 6], @@ -163,7 +163,8 @@ def brief(image, keypoints, descriptor_size=256, mode='normal', patch_size=49, else: - samples = np.random.randint(-(patch_size - 2) // 2, (patch_size // 2) + 1, + samples = np.random.randint(-(patch_size - 2) // 2, + (patch_size // 2) + 1, (descriptor_size * 2, 2)) pos1, pos2 = np.split(samples, 2) @@ -200,21 +201,21 @@ def match_keypoints_brief(keypoints1, descriptors1, keypoints2, Location of Q matched keypoint pairs from two images. """ - if keypoints1.shape[0] != descriptors1.shape[0] or \ - keypoints2.shape[0] != descriptors2.shape[0]: - raise ValueError("The number of keypoints and number of described \ - keypoints do not match. Make the optional parameter \ - return_keypoints True to get described keypoints.") + if (keypoints1.shape[0] != descriptors1.shape[0] + or keypoints2.shape[0] != descriptors2.shape[0]): + raise ValueError("The number of keypoints and number of described " + "keypoints do not match. Make the optional parameter " + "return_keypoints True to get described keypoints.") if descriptors1.shape[1] != descriptors2.shape[1]: - raise ValueError("Descriptor sizes for matching keypoints in both \ - the images should be equal.") + raise ValueError("Descriptor sizes for matching keypoints in both " + "the images should be equal.") # Get hamming distances between keeypoints1 and keypoints2 distance = pairwise_hamming_distance(descriptors1, descriptors2) temp = distance > threshold - row_check = np.any(~temp, axis = 1) + row_check = np.any(~temp, axis=1) matched_keypoints2 = keypoints2[np.argmin(distance, axis=1)] matched_keypoint_pairs = np.zeros((np.sum(row_check), 2, 2), dtype=np.intp) matched_keypoint_pairs[:, 0, :] = keypoints1[row_check] diff --git a/skimage/feature/tests/test_brief.py b/skimage/feature/tests/test_brief.py index 65c99af2..3d126770 100644 --- a/skimage/feature/tests/test_brief.py +++ b/skimage/feature/tests/test_brief.py @@ -15,48 +15,49 @@ def test_brief_color_image_unsupported_error(): def test_match_keypoints_brief_lena_translation(): - """Test matched keypoints between lena image and its translated version.""" - img = data.lena() - img = rgb2gray(img) - img.shape - tform = tf.SimilarityTransform(scale=1, rotation=0, translation=(15, 20)) - translated_img = tf.warp(img, tform) + """Test matched keypoints between lena image and its translated version.""" + img = data.lena() + img = rgb2gray(img) + img.shape + tform = tf.SimilarityTransform(scale=1, rotation=0, translation=(15, 20)) + translated_img = tf.warp(img, tform) - keypoints1 = corner_peaks(corner_harris(img), min_distance=5) - descriptors1, keypoints1 = brief(img, keypoints1, descriptor_size=512) + keypoints1 = corner_peaks(corner_harris(img), min_distance=5) + descriptors1, keypoints1 = brief(img, keypoints1, descriptor_size=512) - keypoints2 = corner_peaks(corner_harris(translated_img), min_distance=5) - descriptors2, keypoints2 = brief(translated_img, keypoints2, - descriptor_size=512) + keypoints2 = corner_peaks(corner_harris(translated_img), min_distance=5) + descriptors2, keypoints2 = brief(translated_img, keypoints2, + descriptor_size=512) - matched_keypoints = match_keypoints_brief(keypoints1, descriptors1, - keypoints2, descriptors2, - threshold=0.10) + matched_keypoints = match_keypoints_brief(keypoints1, descriptors1, + keypoints2, descriptors2, + threshold=0.10) - assert_array_equal(matched_keypoints[:, 0,:], matched_keypoints[:, 1,:] + - [20, 15]) + assert_array_equal(matched_keypoints[:, 0, :], matched_keypoints[:, 1, :] + + [20, 15]) def test_match_keypoints_brief_lena_rotation(): - """Verify matched keypoints result between lena image and its rotated version - with the expected keypoint pairs.""" - img = data.lena() - img = rgb2gray(img) - img.shape - tform = tf.SimilarityTransform(scale=1, rotation=0.10, translation=(0, 0)) - rotated_img = tf.warp(img, tform) + """Verify matched keypoints result between lena image and its rotated + version with the expected keypoint pairs.""" + img = data.lena() + img = rgb2gray(img) + img.shape + tform = tf.SimilarityTransform(scale=1, rotation=0.10, translation=(0, 0)) + rotated_img = tf.warp(img, tform) - keypoints1 = corner_peaks(corner_harris(img), min_distance=5) - descriptors1, keypoints1 = brief(img, keypoints1, descriptor_size=512) + keypoints1 = corner_peaks(corner_harris(img), min_distance=5) + descriptors1, keypoints1 = brief(img, keypoints1, descriptor_size=512) - keypoints2 = corner_peaks(corner_harris(rotated_img), min_distance=5) - descriptors2, keypoints2 = brief(rotated_img, keypoints2, - descriptor_size=512) + keypoints2 = corner_peaks(corner_harris(rotated_img), min_distance=5) + descriptors2, keypoints2 = brief(rotated_img, keypoints2, + descriptor_size=512) - matched_keypoints = match_keypoints_brief(keypoints1, descriptors1, - keypoints2, descriptors2, - threshold=0.07) - expected = np.array([[[263, 272], + matched_keypoints = match_keypoints_brief(keypoints1, descriptors1, + keypoints2, descriptors2, + threshold=0.07) + + expected = np.array([[[263, 272], [234, 298]], [[271, 120], @@ -74,5 +75,4 @@ def test_match_keypoints_brief_lena_rotation(): [[454, 176], [435, 221]]]) - - assert_array_equal(matched_keypoints, expected) + assert_array_equal(matched_keypoints, expected) diff --git a/skimage/feature/tests/test_util.py b/skimage/feature/tests/test_util.py index 6a397e5b..6e2215c5 100644 --- a/skimage/feature/tests/test_util.py +++ b/skimage/feature/tests/test_util.py @@ -2,26 +2,26 @@ import numpy as np from numpy.testing import assert_array_equal from skimage.feature.util import pairwise_hamming_distance + def test_pairwise_hamming_distance_range(): - """Values of all the pairwise hamming distances should be in the range - [0, 1]. - """ - a = np.random.random_sample((10, 50)) > 0.5 - b = np.random.random_sample((20, 50)) > 0.5 - dist = pairwise_hamming_distance(a, b) - assert np.all((0 <= dist) & (dist <= 1)) + """Values of all the pairwise hamming distances should be in the range + [0, 1].""" + a = np.random.random_sample((10, 50)) > 0.5 + b = np.random.random_sample((20, 50)) > 0.5 + dist = pairwise_hamming_distance(a, b) + assert np.all((0 <= dist) & (dist <= 1)) + def test_pairwise_hamming_distance_value(): - """The result of pairwise_hamming_distance of two fixed sets of boolean - vectors should be same as expected. - """ - np.random.seed(10) - a = np.random.random_sample((4, 100)) > 0.5 - np.random.seed(20) - b = np.random.random_sample((3, 100)) > 0.5 - result = pairwise_hamming_distance(a, b) - expected = np.array([[ 0.5 , 0.49, 0.44], - [ 0.44, 0.53, 0.52], - [ 0.4 , 0.55, 0.5 ], - [ 0.47, 0.48, 0.57]]) - assert_array_equal(result, expected) + """The result of pairwise_hamming_distance of two fixed sets of boolean + vectors should be same as expected.""" + np.random.seed(10) + a = np.random.random_sample((4, 100)) > 0.5 + np.random.seed(20) + b = np.random.random_sample((3, 100)) > 0.5 + result = pairwise_hamming_distance(a, b) + expected = np.array([[0.5 , 0.49, 0.44], + [0.44, 0.53, 0.52], + [0.4 , 0.55, 0.5 ], + [0.47, 0.48, 0.57]]) + assert_array_equal(result, expected) diff --git a/skimage/feature/util.py b/skimage/feature/util.py index a7a3670f..aec4dfc8 100644 --- a/skimage/feature/util.py +++ b/skimage/feature/util.py @@ -1,4 +1,5 @@ + def _remove_border_keypoints(image, keypoints, dist): """Removes keypoints that are within dist pixels from the image border.""" width = image.shape[0] @@ -31,5 +32,5 @@ def pairwise_hamming_distance(array1, array2): vector in array2. """ - distance = (array1[:,None] != array2[None]).mean(axis=2) + distance = (array1[:, None] != array2[None]).mean(axis=2) return distance From 431261e0952b39f9fadb8104a75d9b53cb2b1806 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Wed, 17 Jul 2013 15:54:58 +0530 Subject: [PATCH 309/736] Added if __name__ == __main__ in new test files --- skimage/feature/tests/test_brief.py | 5 +++++ skimage/feature/tests/test_util.py | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/skimage/feature/tests/test_brief.py b/skimage/feature/tests/test_brief.py index 3d126770..b78270f7 100644 --- a/skimage/feature/tests/test_brief.py +++ b/skimage/feature/tests/test_brief.py @@ -76,3 +76,8 @@ def test_match_keypoints_brief_lena_rotation(): [435, 221]]]) assert_array_equal(matched_keypoints, expected) + + +if __name__ == '__main__': + from numpy import testing + testing.run_module_suite() diff --git a/skimage/feature/tests/test_util.py b/skimage/feature/tests/test_util.py index 6e2215c5..6e25f51a 100644 --- a/skimage/feature/tests/test_util.py +++ b/skimage/feature/tests/test_util.py @@ -25,3 +25,8 @@ def test_pairwise_hamming_distance_value(): [0.4 , 0.55, 0.5 ], [0.47, 0.48, 0.57]]) assert_array_equal(result, expected) + + +if __name__ == '__main__': + from numpy import testing + testing.run_module_suite() From 90f3a5d6851afdc2c64f19379f50986d33d70968 Mon Sep 17 00:00:00 2001 From: Horea Christian Date: Thu, 18 Jul 2013 17:54:03 +0200 Subject: [PATCH 310/736] Syntax and default variable. From (0,0) to None. --- skimage/util/montage.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/skimage/util/montage.py b/skimage/util/montage.py index f75af3b7..cd87a078 100644 --- a/skimage/util/montage.py +++ b/skimage/util/montage.py @@ -6,7 +6,7 @@ from .. import exposure EPSILON = 1e-6 -def montage2d(arr_in, fill='mean', rescale_intensity=False, grid_shape=(0, 0)): +def montage2d(arr_in, fill='mean', rescale_intensity=False, grid_shape=None): """Create a 2-dimensional 'montage' from a 3-dimensional input array representing an ensemble of equally shaped 2-dimensional images. @@ -37,7 +37,7 @@ def montage2d(arr_in, fill='mean', rescale_intensity=False, grid_shape=(0, 0)): rescale_intensity: bool, optional Whether to rescale the intensity of each image to [0, 1]. grid_shape: tuple, optional - The desired grid shape for the montage (the default aspect ratio is square). + The desired grid shape for the montage (tiles_y, tiles_x). Tthe default aspect ratio is square. Returns ------- @@ -67,7 +67,7 @@ def montage2d(arr_in, fill='mean', rescale_intensity=False, grid_shape=(0, 0)): [ 10. 11. 5.5 5.5]] >>> print(arr_in.mean()) 5.5 - >>> arr_out_nonsquare = montage2d(arr_in, grid_shape = (3, 4)) + >>> arr_out_nonsquare = montage2d(arr_in, grid_shape=(3, 4)) >>> print(arr_out_nonsquare) [[ 0. 1. 4. 5. ] [ 2. 3. 6. 7. ] From 228bf8f7ba633dc39bf76e864206502098d3d3c1 Mon Sep 17 00:00:00 2001 From: Horea Christian Date: Fri, 19 Jul 2013 00:32:38 +0200 Subject: [PATCH 311/736] Adapted if statement to grid_shape=None --- skimage/util/montage.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/skimage/util/montage.py b/skimage/util/montage.py index cd87a078..bdafe6ed 100644 --- a/skimage/util/montage.py +++ b/skimage/util/montage.py @@ -87,10 +87,10 @@ def montage2d(arr_in, fill='mean', rescale_intensity=False, grid_shape=None): arr_in[i] = exposure.rescale_intensity(arr_in[i]) # -- determine alpha - if grid_shape == (0, 0): - alpha_y = alpha_x = int(np.ceil(np.sqrt(n_images))) - else: + if grid_shape: alpha_y, alpha_x = grid_shape + else: + alpha_y = alpha_x = int(np.ceil(np.sqrt(n_images))) # -- fill missing patches if fill == 'mean': From 4b901cdef011f613e9776826836870c5562311a0 Mon Sep 17 00:00:00 2001 From: sg Date: Thu, 18 Jul 2013 22:58:21 -0400 Subject: [PATCH 312/736] add bento to build docs --- doc/source/install.txt | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/doc/source/install.txt b/doc/source/install.txt index 1186479e..66c8dadc 100644 --- a/doc/source/install.txt +++ b/doc/source/install.txt @@ -59,4 +59,32 @@ in-place:: python setup.py build_ext -i +In addition you can install using +`bento`_. However Bento requires +you install it, as well as link to WAFLIB which is part of +`Waf`_. + +To install bento follow the instructions on the webpage, then link +to WAF by adding:: + +export WAFLIB= + +e.g. WAFLIB=/usr/local/src/waf + +then install scikit learn by going to the scikit-image directory +and typing:: + +bentomaker install + +or if you want to use multiple cores:: + +bentomaker configure +bentomaker build -j <# of cores> -i (if you want to build it inplace) +bentomaker install + +Depending on the permissions of your /usr/local/ dir you might need +to put a sudo before the ``bentomaker install`` so that +scikit-image can put ``skivi`` there, which is used for image +viewing by ``skimage.io.imshow``. + .. include:: ../../DEPENDS.txt From a973f3a4bc25c56198e1720ce0b526418dad6a59 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Fri, 19 Jul 2013 12:01:46 +0200 Subject: [PATCH 313/736] Add javascript for locating latest stable version. --- doc/source/_static/docversions.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/doc/source/_static/docversions.js b/doc/source/_static/docversions.js index fde9437b..ab333671 100644 --- a/doc/source/_static/docversions.js +++ b/doc/source/_static/docversions.js @@ -1,17 +1,21 @@ -function insert_version_links() { - var labels = ['dev', '0.8.0', '0.7.0', '0.6', '0.5', '0.4', '0.3']; +var versions = ['dev', '0.8.0', '0.7.0', '0.6', '0.5', '0.4', '0.3']; - for (i = 0; i < labels.length; i++){ +function insert_version_links() { + for (i = 0; i < versions.length; i++){ open_list = '

  • ' if (typeof(DOCUMENTATION_OPTIONS) !== 'undefined') { - if ((DOCUMENTATION_OPTIONS['VERSION'] == labels[i]) || + if ((DOCUMENTATION_OPTIONS['VERSION'] == versions[i]) || (DOCUMENTATION_OPTIONS['VERSION'].match(/dev$/) && (i == 0))) { open_list = '
  • ' } } document.write(open_list); document.write('skimage VERSION
  • \n' - .replace('VERSION', labels[i]) - .replace('URL', 'http://scikit-image.org/docs/' + labels[i])); + .replace('VERSION', versions[i]) + .replace('URL', 'http://scikit-image.org/docs/' + versions[i])); } } + +function stable_version() { + return versions[1]; +} From 90d28a3c7aa42dcdf130041f551f18d2dbd37780 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Fri, 19 Jul 2013 12:18:45 +0200 Subject: [PATCH 314/736] Highlight current doc version. --- doc/source/themes/scikit-image/static/css/custom.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/source/themes/scikit-image/static/css/custom.css b/doc/source/themes/scikit-image/static/css/custom.css index 18ee7a34..0a2d3136 100644 --- a/doc/source/themes/scikit-image/static/css/custom.css +++ b/doc/source/themes/scikit-image/static/css/custom.css @@ -79,6 +79,10 @@ dt { padding-left: 15px; } +#current { + font-weight: bold; +} + .headerlink { margin-left: 10px; color: #ddd; From 8e562af6087bb28ee803e894c5744258f52a37d7 Mon Sep 17 00:00:00 2001 From: sg Date: Fri, 19 Jul 2013 21:31:01 -0400 Subject: [PATCH 315/736] added revisions by @stefanv and @ahojnnes --- doc/source/install.txt | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/doc/source/install.txt b/doc/source/install.txt index 66c8dadc..4377aa56 100644 --- a/doc/source/install.txt +++ b/doc/source/install.txt @@ -60,31 +60,27 @@ in-place:: python setup.py build_ext -i In addition you can install using -`bento`_. However Bento requires -you install it, as well as link to WAFLIB which is part of -`Waf`_. +`bento`_. However the extension +requires you to not only install Bento, but also WAFLIB +which is part of `Waf`_. -To install bento follow the instructions on the webpage, then link -to WAF by adding:: +To install bento follow the +`instructions on the webpage`, +then make it aware of WAF by adding:: -export WAFLIB= + export WAFLIB= e.g. WAFLIB=/usr/local/src/waf -then install scikit learn by going to the scikit-image directory +to the appropriate config file such as .bashrc or .zshrc. +then install scikit-image by going to the scikit-image directory and typing:: -bentomaker install - -or if you want to use multiple cores:: - -bentomaker configure -bentomaker build -j <# of cores> -i (if you want to build it inplace) -bentomaker install + bentomaker configure + bentomaker build -j -i (if you want to build it inplace) + bentomaker install Depending on the permissions of your /usr/local/ dir you might need -to put a sudo before the ``bentomaker install`` so that -scikit-image can put ``skivi`` there, which is used for image -viewing by ``skimage.io.imshow``. +to run the above command as sudo .. include:: ../../DEPENDS.txt From f70c6a61f4188023a2ba1462a4b3d68a64e840bf Mon Sep 17 00:00:00 2001 From: sg Date: Fri, 19 Jul 2013 22:13:11 -0400 Subject: [PATCH 316/736] updated gaussian doc --- skimage/filter/lpi_filter.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/skimage/filter/lpi_filter.py b/skimage/filter/lpi_filter.py index b3755a8b..02bbb354 100644 --- a/skimage/filter/lpi_filter.py +++ b/skimage/filter/lpi_filter.py @@ -66,9 +66,9 @@ class LPIFilter2D(object): -------- Gaussian filter: - - >>> def filt_func(r, c): - ... return np.exp(-np.hypot(r, c)/1) + Use a 1-D gaussian in each direction without normalization coefficients: + >>> def filt_func(r, c, sigma = 1): + ... return np.exp(-np.hypot(r, c)/sigma) >>> filter = LPIFilter2D(filt_func) """ From fd02030e78bd954ead10f495ba1149e76ac7762c Mon Sep 17 00:00:00 2001 From: sg Date: Fri, 19 Jul 2013 23:08:42 -0400 Subject: [PATCH 317/736] added testing dependencies and standardized use of bullets --- DEPENDS.txt | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/DEPENDS.txt b/DEPENDS.txt index 9c7302f1..227283e2 100644 --- a/DEPENDS.txt +++ b/DEPENDS.txt @@ -35,10 +35,16 @@ Optional Requirements You can use this scikit with the basic requirements listed above, but some functionality is only available with the following installed: -`PyQt4 `__ +* `PyQt4 `__ The ``qt`` plugin that provides ``imshow(x, fancy=True)`` and `skivi`. -`FreeImage `__ +* `FreeImage `__ The ``freeimage`` plugin provides support for reading various types of image file formats, including multi-page TIFFs. +Testing requirements +-------------------- +* `Nose `__ + A Python Unit Testing Framework +* `Coverage.py `__ + A tool that generates a unit test code coverage report From 641d1b4dfd14d481ff5985a58cf57c7d7620ba69 Mon Sep 17 00:00:00 2001 From: sg Date: Sat, 20 Jul 2013 03:35:03 -0400 Subject: [PATCH 318/736] remove extra :'s --- skimage/filter/lpi_filter.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/skimage/filter/lpi_filter.py b/skimage/filter/lpi_filter.py index 02bbb354..5b5705f2 100644 --- a/skimage/filter/lpi_filter.py +++ b/skimage/filter/lpi_filter.py @@ -66,7 +66,8 @@ class LPIFilter2D(object): -------- Gaussian filter: - Use a 1-D gaussian in each direction without normalization coefficients: + Use a 1-D gaussian in each direction without normalization + coefficients. >>> def filt_func(r, c, sigma = 1): ... return np.exp(-np.hypot(r, c)/sigma) >>> filter = LPIFilter2D(filt_func) From 3296000451df22d7b7f8be901cf0e480bc5029ee Mon Sep 17 00:00:00 2001 From: sg Date: Sat, 20 Jul 2013 03:46:10 -0400 Subject: [PATCH 319/736] added ahojnnes revisions --- doc/source/install.txt | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/doc/source/install.txt b/doc/source/install.txt index 4377aa56..0fa382f3 100644 --- a/doc/source/install.txt +++ b/doc/source/install.txt @@ -70,10 +70,8 @@ then make it aware of WAF by adding:: export WAFLIB= -e.g. WAFLIB=/usr/local/src/waf - -to the appropriate config file such as .bashrc or .zshrc. -then install scikit-image by going to the scikit-image directory +to the appropriate config file such as ``.bashrc`` or ``.zshrc``. +Then install scikit-image by going to the latter's directory and typing:: bentomaker configure @@ -81,6 +79,6 @@ and typing:: bentomaker install Depending on the permissions of your /usr/local/ dir you might need -to run the above command as sudo +to run the above command as sudo. .. include:: ../../DEPENDS.txt From 2d33bf7622ca1f5ed5cebdfaf7f27919ffbf850d Mon Sep 17 00:00:00 2001 From: Horea Christian Date: Sun, 21 Jul 2013 03:55:16 +0200 Subject: [PATCH 320/736] Added test case for grid_shape parameter. --- skimage/util/tests/test_montage.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/skimage/util/tests/test_montage.py b/skimage/util/tests/test_montage.py index 47e5426d..9de3f3cd 100644 --- a/skimage/util/tests/test_montage.py +++ b/skimage/util/tests/test_montage.py @@ -51,6 +51,23 @@ def test_shape(): arr_out = montage2d(arr_in) assert_equal(arr_out.shape, (alpha * height, alpha * width)) + + +def test_grid_shape(): + n_images = 6 + height, width = 2, 2 + arr_in = np.arange(n_images * height * width, dtype=np.float32) + arr_in = arr_in.reshape(n_images, height, width) + arr_out = montage.montage2d(arr_in, grid_shape=(3,2)) + correct_arr_out = np.array( + [[ 0., 1., 4., 5.], + [ 2., 3., 6., 7.], + [ 8., 9., 12., 13.], + [ 10., 11., 14., 15.], + [ 16., 17., 20., 21.], + [ 18., 19., 22., 23.]] + ) + assert_array_equal(arr_out, correct_arr_out) def test_rescale_intensity(): From 35a65aeef34192b644c9a3afaa13060dee3be0ad Mon Sep 17 00:00:00 2001 From: Horea Christian Date: Sun, 21 Jul 2013 05:55:02 +0200 Subject: [PATCH 321/736] Resolved faulty montage2d import. --- skimage/util/tests/test_montage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/util/tests/test_montage.py b/skimage/util/tests/test_montage.py index 9de3f3cd..450ab0d2 100644 --- a/skimage/util/tests/test_montage.py +++ b/skimage/util/tests/test_montage.py @@ -58,7 +58,7 @@ def test_grid_shape(): height, width = 2, 2 arr_in = np.arange(n_images * height * width, dtype=np.float32) arr_in = arr_in.reshape(n_images, height, width) - arr_out = montage.montage2d(arr_in, grid_shape=(3,2)) + arr_out = montage2d(arr_in, grid_shape=(3,2)) correct_arr_out = np.array( [[ 0., 1., 4., 5.], [ 2., 3., 6., 7.], From e866696d5de02515d299f9315015839ee47af149 Mon Sep 17 00:00:00 2001 From: Matt Terry Date: Sun, 21 Jul 2013 16:33:18 -0700 Subject: [PATCH 322/736] add color distance functions, ciede2000 tests --- skimage/color/__init__.py | 10 +- skimage/color/colorconv.py | 94 +++++++++ skimage/color/delta_e.py | 215 ++++++++++++++++++++ skimage/color/tests/ciede2000_test_data.txt | 37 ++++ skimage/color/tests/test_ciede2000.py | 61 ++++++ 5 files changed, 416 insertions(+), 1 deletion(-) create mode 100644 skimage/color/delta_e.py create mode 100644 skimage/color/tests/ciede2000_test_data.txt create mode 100644 skimage/color/tests/test_ciede2000.py diff --git a/skimage/color/__init__.py b/skimage/color/__init__.py index 19620cb1..2d072808 100644 --- a/skimage/color/__init__.py +++ b/skimage/color/__init__.py @@ -44,6 +44,12 @@ from .colorconv import (convert_colorspace, from .colorlabel import color_dict, label2rgb +from .delta_e import (deltaE_cie76, + deltaE_ciede94, + deltaE_ciede2000, + deltaE_cmc, + ) + __all__ = ['convert_colorspace', 'guess_spatial_dimensions', @@ -89,4 +95,6 @@ __all__ = ['convert_colorspace', 'is_rgb', 'is_gray', 'color_dict', - 'label2rgb'] + 'label2rgb', + 'deltaE_ciede2000', + ] diff --git a/skimage/color/colorconv.py b/skimage/color/colorconv.py index d2b20316..36fbeb1a 100644 --- a/skimage/color/colorconv.py +++ b/skimage/color/colorconv.py @@ -26,10 +26,13 @@ Supported color spaces Derived from the RGB CIE color space. Chosen such that ``x == y == z == 1/3`` at the whitepoint, and all color matching functions are greater than zero everywhere. +* LAB CIE : Lightness, a, b +* LCH CIE : Lightness, Chroma, Hue :author: Nicolas Pinto (rgb2hsv) :author: Ralf Gommers (hsv2rgb) :author: Travis Oliphant (XYZ and RGB CIE functions) +:author: Matt Terry (lab2lch) :license: modified BSD @@ -1026,3 +1029,94 @@ def combine_stains(stains, conv_matrix): logrgb2 = np.dot(-np.reshape(stains, (-1, 3)), conv_matrix) rgb2 = np.exp(logrgb2) return rescale_intensity(np.reshape(rgb2 - 2, stains.shape), in_range=(-1, 1)) + + +def lab2lch(lab): + """CIE-LAB to LCH color space conversion. + TODO: something about cylindrical representation + + Parameters + ---------- + lab : array_like + The image in CIE-LAB format, in a 3-D array of shape (.., .., 3). + + Returns + ------- + out : ndarray + The image in LCH format, in a 3-D array of shape (.., .., 3). + + Raises + ------ + ValueError + If `rgb` is not a 3-D array of shape (.., .., 3). + + References + ---------- + + Notes + ----- + The Hue is expressed as an angle between (0, 2*pi) + + Examples + -------- + >>> from skimage import data + >>> from skimage.color import rgb2lab, lab2lch + >>> lena = data.lena() + >>> lena_lab = rgb2lab(lena) + >>> lena_lch = lab2lch(lena_lab) + """ + shape = lab.shape + if len(shape) != 3 or shape[2] != 3: + raise ValueError("Input image expected to be LAB") + + lab = _prepare_colorarray(lab) + lch = np.empty_like(lab) + + a, b = lab[:, :, 1], lab[:, :, 2] + lch[:, :, 0] = lab[:, :, 0] + lch[:, :, 1] = np.sqrt(a**2 + b**2) # C + + H = lch[:, :, 2] = np.arctan2(b, a) + H[H < 0] += 2*np.pi # (-pi, pi) -> (0, 2*pi) + return lch + + +def lch2lab(lch): + """CIE-LCH to CIE-LAB color space conversion. + TODO: something about cylindrical representation + + Parameters + ---------- + lab : array_like + The image in CIE-LCH format, in a 3-D array of shape (.., .., 3). + + Returns + ------- + out : ndarray + The image in LAB format, in a 3-D array of shape (.., .., 3). + + Raises + ------ + ValueError + If `rgb` is not a 3-D array of shape (.., .., 3). + + References + ---------- + + Examples + -------- + >>> from skimage import data + >>> from skimage.color import rgb2lab, lch2lab + >>> lena = data.lena() + >>> lena_lab = rgb2lab(lena) + >>> lena_lch = lab2lch(lena_lab) + >>> lena_lab2 = lch2lab(lena_lch) + """ + lch = _prepare_colorarray(lch) + lab = np.empty_like(lch) + + c, h = lch[:, :, 1], lch[:, :, 2] + lab[:, :, 0] = lch[:, :, 0] + lab[:, :, 1] = c*np.cos(h) + lab[:, :, 2] = c*np.sin(h) + return lab diff --git a/skimage/color/delta_e.py b/skimage/color/delta_e.py new file mode 100644 index 00000000..91072aff --- /dev/null +++ b/skimage/color/delta_e.py @@ -0,0 +1,215 @@ +""" +Functions for calculating the "distance" between colors. + +Implicit in these definitions of "distance" is the notion of "Just Noticible +Distance" (JND). This represents the distance between colors where a human can +percieve different colors. Humans are more sensitive to certain colors than +others, which different deltaE metrics correct for this with varying degrees of +sophistication. + +:author: Matt Terry + +:license: modified BSD + +Reference +--------- + +""" +from __future__ import division + +import numpy as np +from .colorconv import lab2lch + +DEG = np.pi/180 + + +def _unpack_last(x): + x = np.asarray(x) + shape = x.shape + return [x[..., i] for i in range(shape[-1])] + + +def _arctan2pi(b, a): + """np.arctan2 mapped to (0, 2*pi)""" + ans = np.arctan2(b, a) + ans += np.where(ans < 0, 2*np.pi, 0.) + assert ans.max() <= 2*np.pi + assert ans.min() >= 0. + return ans + + +def deltaE_cie76(lab1, lab2): + """ + "just noticible difference" ~ 2.3 + """ + l1, a1, b1 = _unpack_last(lab1) + l2, a2, b2 = _unpack_last(lab2) + return np.sqrt((l2-l1)**2 + (a2-a1)**2 + (b2-b1)**2) + + +def deltaE_ciede94(lab1, lab2, kC=1, kH=1, kL=1, k1=0.045, k2=0.015): + """ + kC, kH are weighting factors, usually unity (default) + + kL, k1, k2 depend on the application. Sample values are: + kL, k1, k2 = 1, 0.045, 0.015 (graphic arts, default) + kL, k1, k2 = 2, 0.048, 0.014 (textiles) + + Note: deltaE_ciede94 the defines the scales for the lightness, hue, and + chroma in terms of the first color. Consequently + deltaE_ciede94(lab1, lab2) != deltaE_ciede94(lab2, lab1) + """ + l1, a1, b1 = _unpack_last(lab1) + l2, a2, b2 = _unpack_last(lab2) + + dl = l1 - l2 + c1 = np.sqrt(a1**2 + b1**2) + c2 = np.sqrt(a2**2 + b2**2) + da = a1 - a2 + db = b1 - b2 + dc = c1 - c2 + dh_ab = np.sqrt(da**2 + db**2 + dc**2) + + SL = 1 + SC = 1 + k1*c1 + SH = 1 + k2*c1 + + ans = (dl/(kL*SL))**2 + (dc/(kC*SC))**2 + (dh_ab/(kH*SH))**2 + return np.sqrt(ans) + + +def deltaE_ciede2000(lab1, lab2, kL=1, kC=1, kH=1): + """ + Parameters + ---------- + lab1 : array_like + pass + lab2 : array_like + pass + kL : float (range), optional + pass + kC : float (range), optional + pass + kH : float (range), optional + pass + + Returns + ------- + deltaE : array_like + The distance between `lab1` and `lab2` + + Notes + ----- + CIEDE 2000 assumes parametric weighting factors for the luminance, chroma, + and hue (kL, kC, kH respectively). + kL = 1 # graphic arts + kL = 2 # textiles + + References + ---------- + http://www.ece.rochester.edu/~gsharma/ciede2000/ciede2000noteCRNA.pdf + """ + L1, a1, b1 = _unpack_last(lab1) + L2, a2, b2 = _unpack_last(lab2) + + c1 = np.sqrt(a1**2 + b1**2) + c2 = np.sqrt(a2**2 + b2**2) + cbar = 0.5*(c1 + c2) + c7 = cbar**7 + G = 0.5 * (1 - np.sqrt(c7/(c7 + 25**7))) + + dL_prime = L2 - L1 + Lbar = 0.5*(L1 + L2) + + a1_prime = a1 * (1 + G) + a2_prime = a2 * (1 + G) + + c1_prime = np.sqrt(a1_prime**2 + b1**2) + c2_prime = np.sqrt(a2_prime**2 + b2**2) + cbar_prime = 0.5*(c1_prime + c2_prime) + dC_prime = c2_prime - c1_prime + + h1_prime = _arctan2pi(b1, a1_prime) + h2_prime = _arctan2pi(b2, a2_prime) + + dh_prime = h2_prime - h1_prime + + cc = c1_prime * c2_prime + mask1 = cc == 0. + mask2 = (-mask1) * (dh_prime > np.pi) + mask3 = (-mask1) * (dh_prime < -np.pi) + dh_prime[mask1] = 0. + dh_prime[mask2] += 2*np.pi + dh_prime[mask3] -= 2*np.pi + + dH_prime = 2 * np.sqrt(cc) * np.sin(dh_prime/2) + + Hbar_prime = h1_prime + h2_prime + mask0 = np.logical_and(np.abs(h1_prime - h2_prime) > np.pi, cc != 0.) + mask1 = np.logical_and(mask0, Hbar_prime < 2*np.pi) + mask2 = np.logical_and(mask0, Hbar_prime >= 2*np.pi) + Hbar_prime[mask1] += 2*np.pi + Hbar_prime[mask2] -= 2*np.pi + Hbar_prime[cc == 0.] *= 2 + Hbar_prime *= 0.5 + + deg = np.pi/180. + T = (1 - + 0.17 * np.cos(Hbar_prime - 30*deg) + + 0.24 * np.cos(2*Hbar_prime) + + 0.32 * np.cos(3*Hbar_prime + 6*deg) - + 0.20 * np.cos(4*Hbar_prime - 63*deg) + ) + dTheta = 30*deg * np.exp(-((Hbar_prime/deg - 275)/25)**2) + c7 = cbar_prime**7 + Rc = 2 * np.sqrt(c7 / (c7 + 25**7)) + + term = (Lbar - 50)**2 + SL = 1 + 0.015*term/np.sqrt(20 + term) + SC = 1 + 0.045*cbar_prime + SH = 1 + 0.015*cbar_prime * T + + RT = -np.sin(2*dTheta) * Rc + + l_term = dL_prime / (kL * SL) + c_term = dC_prime / (kC * SC) + h_term = dH_prime / (kH * SH) + r_term = RT * c_term * h_term + + dE2 = l_term**2 + dE2 += c_term**2 + dE2 += h_term**2 + dE2 += r_term + return np.sqrt(dE2) + + +def deltaE_cmc(lab1, lab2): + """ + indistinguishable if < 1 + usual value for "different" is > 2 + + Note: deltaE_cmc the defines the scales for the lightness, hue, and chroma + in terms of the first color. Consequently + deltaE_cmc(lab1, lab2) != deltaE_cmc(lab2, lab1) + """ + l1, c1, h1 = _unpack_last(lab2lch(lab1)) + l2, c2, h2 = _unpack_last(lab2lch(lab2)) + + sl = np.where(l1 < 16, 0.511, 0.040975*l1 / (1 + 0.01765*l1)) + sc = 0.638 + 0.0638*c1 / (1 + 0.0131*c1) + + c1_4 = c1**4 + f = np.sqrt(c1_4 / (c1_4 + 1900)) + t = np.where(np.logical_and(h1 >= 2.862, h1 <= 6.021), + 0.56 * 0.2 * np.abs(np.cos(h1 + 2.93)), + 0.36 + 0.4 * np.abs(np.cos(h1 + 0.611)) + ) + sh = sc * (f*t + 1-f) + + l, c = 1, 1 + ans = ((l2 - l1)/(l * sl))**2 + ans += ((c2 - c1)/(c * sc))**2 + deg = np.pi/180. + ans += ((h2 - h1)*deg/sh)**2 # metric defines h in terms of degrees + + return np.sqrt(ans) diff --git a/skimage/color/tests/ciede2000_test_data.txt b/skimage/color/tests/ciede2000_test_data.txt new file mode 100644 index 00000000..28503ce5 --- /dev/null +++ b/skimage/color/tests/ciede2000_test_data.txt @@ -0,0 +1,37 @@ +# input, intermediate, and output values for CIEDE2000 dE function +# data taken from "The CIEDE2000 Color-Difference Formula: Implementation Notes, ..." http://www.ece.rochester.edu/~gsharma/ciede2000/ciede2000noteCRNA.pdf +# pair 1 L1 a1 b1 ap1 cp1 hp1 hbar1 G T SL SC SH RT dE 2 L2 a2 b2 ap2 cp2 hp2 +1 1 50.0000 2.6772 -79.7751 2.6774 79.8200 271.9222 270.9611 0.0001 0.6907 1.0000 4.6578 1.8421 -1.7042 2.0425 2 50.0000 0.0000 -82.7485 0.0000 82.7485 270.0000 +2 1 50.0000 3.1571 -77.2803 3.1573 77.3448 272.3395 271.1698 0.0001 0.6843 1.0000 4.6021 1.8216 -1.7070 2.8615 2 50.0000 0.0000 -82.7485 0.0000 82.7485 270.0000 +3 1 50.0000 2.8361 -74.0200 2.8363 74.0743 272.1944 271.0972 0.0001 0.6865 1.0000 4.5285 1.8074 -1.7060 3.4412 2 50.0000 0.0000 -82.7485 0.0000 82.7485 270.0000 +4 1 50.0000 -1.3802 -84.2814 -1.3803 84.2927 269.0618 269.5309 0.0001 0.7357 1.0000 4.7584 1.9217 -1.6809 1.0000 2 50.0000 0.0000 -82.7485 0.0000 82.7485 270.0000 +5 1 50.0000 -1.1848 -84.8006 -1.1849 84.8089 269.1995 269.5997 0.0001 0.7335 1.0000 4.7700 1.9218 -1.6822 1.0000 2 50.0000 0.0000 -82.7485 0.0000 82.7485 270.0000 +6 1 50.0000 -0.9009 -85.5211 -0.9009 85.5258 269.3964 269.6982 0.0001 0.7303 1.0000 4.7862 1.9217 -1.6840 1.0000 2 50.0000 0.0000 -82.7485 0.0000 82.7485 270.0000 +7 1 50.0000 0.0000 0.0000 0.0000 0.0000 0.0000 126.8697 0.5000 1.2200 1.0000 1.0562 1.0229 0.0000 2.3669 2 50.0000 -1.0000 2.0000 -1.5000 2.5000 126.8697 +8 1 50.0000 -1.0000 2.0000 -1.5000 2.5000 126.8697 126.8697 0.5000 1.2200 1.0000 1.0562 1.0229 0.0000 2.3669 2 50.0000 0.0000 0.0000 0.0000 0.0000 0.0000 +9 1 50.0000 2.4900 -0.0010 3.7346 3.7346 359.9847 269.9854 0.4998 0.7212 1.0000 1.1681 1.0404 -0.0022 7.1792 2 50.0000 -2.4900 0.0009 -3.7346 3.7346 179.9862 +10 1 50.0000 2.4900 -0.0010 3.7346 3.7346 359.9847 269.9847 0.4998 0.7212 1.0000 1.1681 1.0404 -0.0022 7.1792 2 50.0000 -2.4900 0.0010 -3.7346 3.7346 179.9847 +11 1 50.0000 2.4900 -0.0010 3.7346 3.7346 359.9847 89.9839 0.4998 0.6175 1.0000 1.1681 1.0346 0.0000 7.2195 2 50.0000 -2.4900 0.0011 -3.7346 3.7346 179.9831 +12 1 50.0000 2.4900 -0.0010 3.7346 3.7346 359.9847 89.9831 0.4998 0.6175 1.0000 1.1681 1.0346 0.0000 7.2195 2 50.0000 -2.4900 0.0012 -3.7346 3.7346 179.9816 +13 1 50.0000 -0.0010 2.4900 -0.0015 2.4900 90.0345 180.0328 0.4998 0.9779 1.0000 1.1121 1.0365 0.0000 4.8045 2 50.0000 0.0009 -2.4900 0.0013 2.4900 270.0311 +14 1 50.0000 -0.0010 2.4900 -0.0015 2.4900 90.0345 180.0345 0.4998 0.9779 1.0000 1.1121 1.0365 0.0000 4.8045 2 50.0000 0.0010 -2.4900 0.0015 2.4900 270.0345 +15 1 50.0000 -0.0010 2.4900 -0.0015 2.4900 90.0345 0.0362 0.4998 1.3197 1.0000 1.1121 1.0493 0.0000 4.7461 2 50.0000 0.0011 -2.4900 0.0016 2.4900 270.0380 +16 1 50.0000 2.5000 0.0000 3.7496 3.7496 0.0000 315.0000 0.4998 0.8454 1.0000 1.1406 1.0396 -0.0001 4.3065 2 50.0000 0.0000 -2.5000 0.0000 2.5000 270.0000 +17 1 50.0000 2.5000 0.0000 3.4569 3.4569 0.0000 346.2470 0.3827 1.4453 1.1608 1.9547 1.4599 -0.0003 27.1492 2 73.0000 25.0000 -18.0000 34.5687 38.9743 332.4939 +18 1 50.0000 2.5000 0.0000 3.4954 3.4954 0.0000 51.7766 0.3981 0.6447 1.0640 1.7498 1.1612 0.0000 22.8977 2 61.0000 -5.0000 29.0000 -6.9907 29.8307 103.5532 +19 1 50.0000 2.5000 0.0000 3.5514 3.5514 0.0000 272.2362 0.4206 0.6521 1.0251 1.9455 1.2055 -0.8219 31.9030 2 56.0000 -27.0000 -3.0000 -38.3556 38.4728 184.4723 +20 1 50.0000 2.5000 0.0000 3.5244 3.5244 0.0000 11.9548 0.4098 1.1031 1.0400 1.9120 1.3353 0.0000 19.4535 2 58.0000 24.0000 15.0000 33.8342 37.0102 23.9095 +21 1 50.0000 2.5000 0.0000 3.7494 3.7494 0.0000 3.5056 0.4997 1.2616 1.0000 1.1923 1.0808 0.0000 1.0000 2 50.0000 3.1736 0.5854 4.7596 4.7954 7.0113 +22 1 50.0000 2.5000 0.0000 3.7493 3.7493 0.0000 0.0000 0.4997 1.3202 1.0000 1.1956 1.0861 0.0000 1.0000 2 50.0000 3.2972 0.0000 4.9450 4.9450 0.0000 +23 1 50.0000 2.5000 0.0000 3.7497 3.7497 0.0000 5.8190 0.4999 1.2197 1.0000 1.1486 1.0604 0.0000 1.0000 2 50.0000 1.8634 0.5757 2.7949 2.8536 11.6380 +24 1 50.0000 2.5000 0.0000 3.7493 3.7493 0.0000 1.9603 0.4997 1.2883 1.0000 1.1946 1.0836 0.0000 1.0000 2 50.0000 3.2592 0.3350 4.8879 4.8994 3.9206 +25 1 60.2574 -34.0099 36.2677 -34.0678 49.7590 133.2085 132.0835 0.0017 1.3010 1.1427 3.2946 1.9951 0.0000 1.2644 2 60.4626 -34.1751 39.4387 -34.2333 52.2238 130.9584 +26 1 63.0109 -31.0961 -5.8663 -32.6194 33.1427 190.1951 188.8221 0.0490 0.9402 1.1831 2.4549 1.4560 0.0000 1.2630 2 62.8187 -29.7946 -4.0864 -31.2542 31.5202 187.4490 +27 1 61.2901 3.7196 -5.3901 5.5668 7.7487 315.9240 310.0313 0.4966 0.6952 1.1586 1.3092 1.0717 -0.0032 1.8731 2 61.4292 2.2480 -4.9620 3.3644 5.9950 304.1385 +28 1 35.0831 -44.1164 3.7933 -44.3939 44.5557 175.1161 176.4290 0.0063 1.0168 1.2148 2.9105 1.6476 0.0000 1.8645 2 35.0232 -40.0716 1.5901 -40.3237 40.3550 177.7418 +29 1 22.7233 20.0904 -46.6940 20.1424 50.8532 293.3339 291.3809 0.0026 0.3636 1.4014 3.1597 1.2617 -1.2537 2.0373 2 23.0331 14.9730 -42.5619 15.0118 45.1317 289.4279 +30 1 36.4612 47.8580 18.3852 47.9197 51.3256 20.9901 21.8781 0.0013 0.9239 1.1943 3.3888 1.7357 0.0000 1.4146 2 36.2715 50.5065 21.2231 50.5716 54.8444 22.7660 +31 1 90.8027 -2.0831 1.4410 -3.1245 3.4408 155.2410 167.1011 0.4999 1.1546 1.6110 1.1329 1.0511 0.0000 1.4441 2 91.1528 -1.6435 0.0447 -2.4651 2.4655 178.9612 +32 1 90.9257 -0.5406 -0.9208 -0.8109 1.2270 228.6315 218.4363 0.5000 1.3916 1.5930 1.0620 1.0288 0.0000 1.5381 2 88.6381 -0.8985 -0.7239 -1.3477 1.5298 208.2412 +33 1 6.7747 -0.2908 -2.4247 -0.4362 2.4636 259.8025 263.0049 0.4999 0.9556 1.6517 1.1057 1.0337 -0.0004 0.6377 2 5.8714 -0.0985 -2.2286 -0.1477 2.2335 266.2073 +34 1 2.0776 0.0795 -1.1350 0.1192 1.1412 275.9978 268.0910 0.5000 0.7826 1.7246 1.0383 1.0100 0.0000 0.9082 2 0.9033 -0.0636 -0.5514 -0.0954 0.5596 260.18421 diff --git a/skimage/color/tests/test_ciede2000.py b/skimage/color/tests/test_ciede2000.py new file mode 100644 index 00000000..5c00fb24 --- /dev/null +++ b/skimage/color/tests/test_ciede2000.py @@ -0,0 +1,61 @@ +"""Test for correctness of color distance functions + +Authors +------- +Matt Terry + +:license: modified BSD +""" +from os.path import abspath, dirname, join as pjoin + +import numpy as np +from numpy.testing import assert_array_almost_equal + +from skimage.color import deltaE_ciede2000 + + +def test_ciede2000_dE(): + dtype = [('pair', int), + ('1', int), + ('L1', float), + ('a1', float), + ('b1', float), + ('a1_prime', float), + ('C1_prime', float), + ('h1_prime', float), + ('hbar_prime', float), + ('G', float), + ('T', float), + ('SL', float), + ('SC', float), + ('SH', float), + ('RT', float), + ('dE', float), + ('2', int), + ('L2', float), + ('a2', float), + ('b2', float), + ('a2_prime', float), + ('C2_prime', float), + ('h2_prime', float), + ] + + # note: ciede_test_data.txt contains several intermediate quantities + path = pjoin(dirname(abspath(__file__)), 'ciede_test_data.txt') + data = np.loadtxt(path, dtype=dtype) + + N = len(data) + + lab1 = np.zeros((N, 3)) + lab1[:, 0] = data['L1'] + lab1[:, 1] = data['a1'] + lab1[:, 2] = data['b1'] + + lab2 = np.zeros((N, 3)) + lab2[:, 0] = data['L2'] + lab2[:, 1] = data['a2'] + lab2[:, 2] = data['b2'] + + dE2 = deltaE_ciede2000(lab1, lab2) + + assert_array_almost_equal(dE2, data['dE']) From c55ef2e63aa5a54b8cc106fc884d33c5a51828b8 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Mon, 22 Jul 2013 11:46:58 +0200 Subject: [PATCH 323/736] Shorten Bento installation instructions and fix broken links. --- doc/source/install.txt | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/doc/source/install.txt b/doc/source/install.txt index 0fa382f3..19121d21 100644 --- a/doc/source/install.txt +++ b/doc/source/install.txt @@ -59,26 +59,27 @@ in-place:: python setup.py build_ext -i -In addition you can install using -`bento`_. However the extension -requires you to not only install Bento, but also WAFLIB -which is part of `Waf`_. +Building with bento +------------------- -To install bento follow the -`instructions on the webpage`, -then make it aware of WAF by adding:: +``scikit-image`` can also be built using `bento +`__. Bento depends on `WAF +`__ for compilation. + +Follow the `Bento installation instructions +`__ and `download WAF +`__. + +Tell Bento where to find WAF by setting the ``WAFLIB`` environment variable:: export WAFLIB= -to the appropriate config file such as ``.bashrc`` or ``.zshrc``. -Then install scikit-image by going to the latter's directory -and typing:: +From the ``scikit-image`` source directory:: bentomaker configure - bentomaker build -j -i (if you want to build it inplace) - bentomaker install + bentomaker build -j # (add -i for in-place build) + bentomaker install # (when not builing in-place) -Depending on the permissions of your /usr/local/ dir you might need -to run the above command as sudo. +Depending on file permissions, the install commands may need to be run as sudo. .. include:: ../../DEPENDS.txt From aadd346964a192a4ced9b177e03d1a201e0f72c9 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Mon, 22 Jul 2013 20:19:24 +1000 Subject: [PATCH 324/736] Fix ratio scaling in SLIC --- skimage/segmentation/slic_superpixels.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skimage/segmentation/slic_superpixels.py b/skimage/segmentation/slic_superpixels.py index 2caf8ade..478093a1 100644 --- a/skimage/segmentation/slic_superpixels.py +++ b/skimage/segmentation/slic_superpixels.py @@ -122,11 +122,11 @@ def slic(image, n_segments=100, ratio=10., max_iter=10, sigma=1, means = np.ascontiguousarray(means) # we do the scaling of ratio in the same way as in the SLIC paper # so the values have the same meaning - ratio = (ratio / float(max((step_z, step_y, step_x)))) ** 2 + ratio = float(max((step_z, step_y, step_x))) / ratio image_zyx = np.concatenate([grid_z[..., np.newaxis], grid_y[..., np.newaxis], grid_x[..., np.newaxis], - image / ratio], axis=-1).copy("C") + image * ratio], axis=-1).copy("C") nearest_mean = np.zeros((depth, height, width), dtype=np.intp) distance = np.empty((depth, height, width), dtype=np.float) segment_map = _slic_cython(image_zyx, nearest_mean, distance, means, From 2a19db609666e3b845c002754ae31664454289c6 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Mon, 22 Jul 2013 12:37:39 +0200 Subject: [PATCH 325/736] Environment variable is called WAFDIR. --- doc/source/install.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/install.txt b/doc/source/install.txt index 19121d21..e95f2b20 100644 --- a/doc/source/install.txt +++ b/doc/source/install.txt @@ -72,7 +72,7 @@ Follow the `Bento installation instructions Tell Bento where to find WAF by setting the ``WAFLIB`` environment variable:: - export WAFLIB= + export WAFDIR= From the ``scikit-image`` source directory:: From 61ad58125093d3d32ddc157cb9eadbd5a5cb3de2 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Mon, 22 Jul 2013 13:02:53 +0200 Subject: [PATCH 326/736] Properly fix WAF environment directory. Tell users to download source (not binary). --- doc/source/install.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/source/install.txt b/doc/source/install.txt index e95f2b20..b78c1269 100644 --- a/doc/source/install.txt +++ b/doc/source/install.txt @@ -67,10 +67,10 @@ Building with bento `__ for compilation. Follow the `Bento installation instructions -`__ and `download WAF -`__. +`__ and `download the WAF +source `__. -Tell Bento where to find WAF by setting the ``WAFLIB`` environment variable:: +Tell Bento where to find WAF by setting the ``WAFDIR`` environment variable:: export WAFDIR= From c50c926eae7a563301933ad58efded50a47d4565 Mon Sep 17 00:00:00 2001 From: Matt Terry Date: Mon, 22 Jul 2013 08:56:57 -0700 Subject: [PATCH 327/736] docs --- skimage/color/delta_e.py | 112 +++++++++++++++++++++++++++++++-------- 1 file changed, 90 insertions(+), 22 deletions(-) diff --git a/skimage/color/delta_e.py b/skimage/color/delta_e.py index 91072aff..52b10e80 100644 --- a/skimage/color/delta_e.py +++ b/skimage/color/delta_e.py @@ -7,6 +7,11 @@ percieve different colors. Humans are more sensitive to certain colors than others, which different deltaE metrics correct for this with varying degrees of sophistication. +The literature often mentions 1 as the minimum distance for visual +differentiation, but more recent studies (Mahy 1994) peg JND at 2.3 + +The delta-E notation comes from the German word for "Sensation" (Empfindung). + :author: Matt Terry :license: modified BSD @@ -39,25 +44,68 @@ def _arctan2pi(b, a): def deltaE_cie76(lab1, lab2): - """ - "just noticible difference" ~ 2.3 + """Euclidian distance between two points in in Lab color space + + Parameters + ---------- + lab1 : array_like + reference color (Lab colorspace) + lab2 : array_like + comparision color (Lab colorspace) + + Returns + ------- + dE : array_like + distance between colors `lab1` and `lab2` """ l1, a1, b1 = _unpack_last(lab1) l2, a2, b2 = _unpack_last(lab2) return np.sqrt((l2-l1)**2 + (a2-a1)**2 + (b2-b1)**2) -def deltaE_ciede94(lab1, lab2, kC=1, kH=1, kL=1, k1=0.045, k2=0.015): - """ - kC, kH are weighting factors, usually unity (default) +def deltaE_ciede94(lab1, lab2, kH=1, kC=1, kL=1, k1=0.045, k2=0.015): + """Color difference according to CIEDE 94 standard - kL, k1, k2 depend on the application. Sample values are: - kL, k1, k2 = 1, 0.045, 0.015 (graphic arts, default) - kL, k1, k2 = 2, 0.048, 0.014 (textiles) + Accomodates perceptual non-uniformites through the use of application + specific scale factors (kH, kC, kL, k1, and k2). - Note: deltaE_ciede94 the defines the scales for the lightness, hue, and - chroma in terms of the first color. Consequently - deltaE_ciede94(lab1, lab2) != deltaE_ciede94(lab2, lab1) + Parameters + ---------- + lab1 : array_like + reference color (Lab colorspace) + lab2 : array_like + comparision color (Lab colorspace) + kH : float, optional + Hue scale + kC : float, optional + Chroma scale + kL : float, optional + Lightness scale + k1 : float, optional + first scale parameter + k2 : float, optional + second scale parameter + + Returns + ------- + dE : array_like + color difference between `lab1` and `lab2` + + Notes + ----- + deltaE_ciede94 is not symmetric with respect to lab1 and lab2. CIEDE94 + defines the scales for the lightness, hue, and chroma in terms of the first + color. Consequently, the first color should be regarded as the "reference" + color. + + kL, k1, k2 depend on the application and default to the values suggested + for graphic arts + + Parameter Graphic Arts Textiles + ---------- ------------- -------- + kL 1.000 2.000 + k1 0.045 0.048 + k2 0.015 0.014 """ l1, a1, b1 = _unpack_last(lab1) l2, a2, b2 = _unpack_last(lab2) @@ -79,13 +127,17 @@ def deltaE_ciede94(lab1, lab2, kC=1, kH=1, kL=1, k1=0.045, k2=0.015): def deltaE_ciede2000(lab1, lab2, kL=1, kC=1, kH=1): - """ + """Color difference as given by the CIEDE 2000 standard. + + CIEDE 2000 is a major revision of CIDE94. The perceptual calibaration is + largely based on experience with automotive paint on smooth surfaces. + Parameters ---------- lab1 : array_like - pass + reference color (Lab colorspace) lab2 : array_like - pass + comparision color (Lab colorspace) kL : float (range), optional pass kC : float (range), optional @@ -101,13 +153,12 @@ def deltaE_ciede2000(lab1, lab2, kL=1, kC=1, kH=1): Notes ----- CIEDE 2000 assumes parametric weighting factors for the luminance, chroma, - and hue (kL, kC, kH respectively). - kL = 1 # graphic arts - kL = 2 # textiles + and hue (kL, kC, kH respectively). These default to 1. References ---------- - http://www.ece.rochester.edu/~gsharma/ciede2000/ciede2000noteCRNA.pdf + http://en.wikipedia.org/wiki/Color_difference + http://www.ece.rochester.edu/~gsharma/ciede2000/ciede2000noteCRNA.pdf (doi:10.1364/AO.33.008069) """ L1, a1, b1 = _unpack_last(lab1) L2, a2, b2 = _unpack_last(lab2) @@ -184,11 +235,28 @@ def deltaE_ciede2000(lab1, lab2, kL=1, kC=1, kH=1): def deltaE_cmc(lab1, lab2): - """ - indistinguishable if < 1 - usual value for "different" is > 2 + """Color difference from the CMC l:c standard. - Note: deltaE_cmc the defines the scales for the lightness, hue, and chroma + This color difference developed by the Colour Measurement Committee of the + Socieity of Dyes and Colourists of Great Britian (CMC). It is intended for + use in the textile industry. Color differences less than 1.0 are + officially indistiguisable, but 2.0 is the usual threshold. + + Parameters + ---------- + lab1 : array_like + reference color (Lab colorspace) + lab2 : array_like + comparision color (Lab colorspace) + + Returns + ------- + dE : array_like + distance between colors `lab1` and `lab2` + + Notes + ----- + deltaE_cmc the defines the scales for the lightness, hue, and chroma in terms of the first color. Consequently deltaE_cmc(lab1, lab2) != deltaE_cmc(lab2, lab1) """ From a821ad518ced72210e3dd7c315e8b96488ae8719 Mon Sep 17 00:00:00 2001 From: Matt Terry Date: Mon, 22 Jul 2013 09:35:25 -0700 Subject: [PATCH 328/736] better cmc implementation --- skimage/color/delta_e.py | 51 +++++++++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 19 deletions(-) diff --git a/skimage/color/delta_e.py b/skimage/color/delta_e.py index 52b10e80..4534ef07 100644 --- a/skimage/color/delta_e.py +++ b/skimage/color/delta_e.py @@ -234,13 +234,17 @@ def deltaE_ciede2000(lab1, lab2, kL=1, kC=1, kH=1): return np.sqrt(dE2) -def deltaE_cmc(lab1, lab2): +def deltaE_cmc(lab1, lab2, kL=1, kC=1): """Color difference from the CMC l:c standard. This color difference developed by the Colour Measurement Committee of the Socieity of Dyes and Colourists of Great Britian (CMC). It is intended for - use in the textile industry. Color differences less than 1.0 are - officially indistiguisable, but 2.0 is the usual threshold. + use in the textile industry. + + The scale factors kL, kC set the weight given to differences in lightness + and chroma relative to differences in hue. The usual values are kL=2, kC=1 + for "acceptability" and kL=1, kC=1 for "imperceptability". Colors with + dE > 1 are "different" for the given scale factors. Parameters ---------- @@ -260,24 +264,33 @@ def deltaE_cmc(lab1, lab2): in terms of the first color. Consequently deltaE_cmc(lab1, lab2) != deltaE_cmc(lab2, lab1) """ - l1, c1, h1 = _unpack_last(lab2lch(lab1)) - l2, c2, h2 = _unpack_last(lab2lch(lab2)) + l1, a1, b1 = _unpack_last(lab1) + l2, a2, b2 = _unpack_last(lab2) - sl = np.where(l1 < 16, 0.511, 0.040975*l1 / (1 + 0.01765*l1)) - sc = 0.638 + 0.0638*c1 / (1 + 0.0131*c1) + c1 = np.sqrt(a1**2 + b1**2) + c2 = np.sqrt(a2**2 + b2**2) + dC = c1 - c2 - c1_4 = c1**4 - f = np.sqrt(c1_4 / (c1_4 + 1900)) - t = np.where(np.logical_and(h1 >= 2.862, h1 <= 6.021), - 0.56 * 0.2 * np.abs(np.cos(h1 + 2.93)), - 0.36 + 0.4 * np.abs(np.cos(h1 + 0.611)) + da = a1 - a2 + db = b1 - b2 + dH = np.sqrt(da**2 + db**2 - dC**2) + + dL = l1 - l2 + + h1 = _arctan2pi(b1, a1) + T = np.where(np.logical_and(h1 >= 164*DEG, h1 <= 345*DEG), + 0.56 + 0.2 * np.abs(np.cos(h1 + 168*DEG)), + 0.36 + 0.4 * np.abs(np.cos(h1 + 35*DEG)) ) - sh = sc * (f*t + 1-f) + c1_4 = c1**4 + F = np.sqrt(c1_4 / (c1_4 + 1900)) - l, c = 1, 1 - ans = ((l2 - l1)/(l * sl))**2 - ans += ((c2 - c1)/(c * sc))**2 - deg = np.pi/180. - ans += ((h2 - h1)*deg/sh)**2 # metric defines h in terms of degrees + SL = np.where(l1 < 16, 0.511, 0.040975*l1 / (1. + 0.01765*l1)) + SC = 0.638 + 0.0638 * c1 / (1. + 0.0131*c1) + SH = SC * (F*T + 1 - F) - return np.sqrt(ans) + dE2 = (dL / (kL*SL))**2 + dE2 += (dC/(kC*SC))**2 + dE2 += (dH/SH)**2 + + return np.sqrt(dE2) From f9ca27a979323862c032c61df4c6a3b9af8168fa Mon Sep 17 00:00:00 2001 From: Matt Terry Date: Mon, 22 Jul 2013 09:36:33 -0700 Subject: [PATCH 329/736] don't need lab2lch --- skimage/color/delta_e.py | 1 - 1 file changed, 1 deletion(-) diff --git a/skimage/color/delta_e.py b/skimage/color/delta_e.py index 4534ef07..7c5399f6 100644 --- a/skimage/color/delta_e.py +++ b/skimage/color/delta_e.py @@ -23,7 +23,6 @@ Reference from __future__ import division import numpy as np -from .colorconv import lab2lch DEG = np.pi/180 From 28f58a2c504e908815f41fde0a952408553e646a Mon Sep 17 00:00:00 2001 From: Matt Terry Date: Mon, 22 Jul 2013 15:31:27 -0700 Subject: [PATCH 330/736] add tests. cmc stops naning --- skimage/color/__init__.py | 3 + skimage/color/delta_e.py | 27 ++-- skimage/color/tests/ciede2000_test_data.txt | 1 + skimage/color/tests/test_ciede2000.py | 61 -------- skimage/color/tests/test_dE.py | 145 ++++++++++++++++++++ 5 files changed, 161 insertions(+), 76 deletions(-) delete mode 100644 skimage/color/tests/test_ciede2000.py create mode 100644 skimage/color/tests/test_dE.py diff --git a/skimage/color/__init__.py b/skimage/color/__init__.py index 2d072808..95ccb0c0 100644 --- a/skimage/color/__init__.py +++ b/skimage/color/__init__.py @@ -96,5 +96,8 @@ __all__ = ['convert_colorspace', 'is_gray', 'color_dict', 'label2rgb', + 'deltaE_cie76', + 'deltaE_ciede94', 'deltaE_ciede2000', + 'deltaE_cmc', ] diff --git a/skimage/color/delta_e.py b/skimage/color/delta_e.py index 7c5399f6..47bf6e1a 100644 --- a/skimage/color/delta_e.py +++ b/skimage/color/delta_e.py @@ -112,10 +112,8 @@ def deltaE_ciede94(lab1, lab2, kH=1, kC=1, kL=1, k1=0.045, k2=0.015): dl = l1 - l2 c1 = np.sqrt(a1**2 + b1**2) c2 = np.sqrt(a2**2 + b2**2) - da = a1 - a2 - db = b1 - b2 dc = c1 - c2 - dh_ab = np.sqrt(da**2 + db**2 + dc**2) + dh_ab = np.sqrt(deltaE_cie76(lab1, lab2)**2 - dl**2 - dc**2) SL = 1 SC = 1 + k1*c1 @@ -186,11 +184,11 @@ def deltaE_ciede2000(lab1, lab2, kL=1, kC=1, kH=1): cc = c1_prime * c2_prime mask1 = cc == 0. - mask2 = (-mask1) * (dh_prime > np.pi) - mask3 = (-mask1) * (dh_prime < -np.pi) - dh_prime[mask1] = 0. - dh_prime[mask2] += 2*np.pi - dh_prime[mask3] -= 2*np.pi + mask2 = np.logical_and(-mask1, dh_prime > np.pi) + mask3 = np.logical_and(-mask1, dh_prime < -np.pi) + dh_prime = np.where(mask1, 0., dh_prime) + dh_prime += np.where(mask2, 2*np.pi, 0) + dh_prime -= np.where(mask3, 2*np.pi, 0) dH_prime = 2 * np.sqrt(cc) * np.sin(dh_prime/2) @@ -198,9 +196,10 @@ def deltaE_ciede2000(lab1, lab2, kL=1, kC=1, kH=1): mask0 = np.logical_and(np.abs(h1_prime - h2_prime) > np.pi, cc != 0.) mask1 = np.logical_and(mask0, Hbar_prime < 2*np.pi) mask2 = np.logical_and(mask0, Hbar_prime >= 2*np.pi) - Hbar_prime[mask1] += 2*np.pi - Hbar_prime[mask2] -= 2*np.pi - Hbar_prime[cc == 0.] *= 2 + + Hbar_prime += np.where(mask1, 2*np.pi, 0) + Hbar_prime -= np.where(mask2, 2*np.pi, 0) + Hbar_prime *= np.where(cc == 0., 2, 1) Hbar_prime *= 0.5 deg = np.pi/180. @@ -269,10 +268,8 @@ def deltaE_cmc(lab1, lab2, kL=1, kC=1): c1 = np.sqrt(a1**2 + b1**2) c2 = np.sqrt(a2**2 + b2**2) dC = c1 - c2 - - da = a1 - a2 - db = b1 - b2 - dH = np.sqrt(da**2 + db**2 - dC**2) + dl = l1 - l2 + dH = np.sqrt(deltaE_cie76(lab1, lab2)**2 - dl**2 - dC**2) dL = l1 - l2 diff --git a/skimage/color/tests/ciede2000_test_data.txt b/skimage/color/tests/ciede2000_test_data.txt index 28503ce5..b7e3fd57 100644 --- a/skimage/color/tests/ciede2000_test_data.txt +++ b/skimage/color/tests/ciede2000_test_data.txt @@ -1,5 +1,6 @@ # input, intermediate, and output values for CIEDE2000 dE function # data taken from "The CIEDE2000 Color-Difference Formula: Implementation Notes, ..." http://www.ece.rochester.edu/~gsharma/ciede2000/ciede2000noteCRNA.pdf +# tab delimited data # pair 1 L1 a1 b1 ap1 cp1 hp1 hbar1 G T SL SC SH RT dE 2 L2 a2 b2 ap2 cp2 hp2 1 1 50.0000 2.6772 -79.7751 2.6774 79.8200 271.9222 270.9611 0.0001 0.6907 1.0000 4.6578 1.8421 -1.7042 2.0425 2 50.0000 0.0000 -82.7485 0.0000 82.7485 270.0000 2 1 50.0000 3.1571 -77.2803 3.1573 77.3448 272.3395 271.1698 0.0001 0.6843 1.0000 4.6021 1.8216 -1.7070 2.8615 2 50.0000 0.0000 -82.7485 0.0000 82.7485 270.0000 diff --git a/skimage/color/tests/test_ciede2000.py b/skimage/color/tests/test_ciede2000.py deleted file mode 100644 index 5c00fb24..00000000 --- a/skimage/color/tests/test_ciede2000.py +++ /dev/null @@ -1,61 +0,0 @@ -"""Test for correctness of color distance functions - -Authors -------- -Matt Terry - -:license: modified BSD -""" -from os.path import abspath, dirname, join as pjoin - -import numpy as np -from numpy.testing import assert_array_almost_equal - -from skimage.color import deltaE_ciede2000 - - -def test_ciede2000_dE(): - dtype = [('pair', int), - ('1', int), - ('L1', float), - ('a1', float), - ('b1', float), - ('a1_prime', float), - ('C1_prime', float), - ('h1_prime', float), - ('hbar_prime', float), - ('G', float), - ('T', float), - ('SL', float), - ('SC', float), - ('SH', float), - ('RT', float), - ('dE', float), - ('2', int), - ('L2', float), - ('a2', float), - ('b2', float), - ('a2_prime', float), - ('C2_prime', float), - ('h2_prime', float), - ] - - # note: ciede_test_data.txt contains several intermediate quantities - path = pjoin(dirname(abspath(__file__)), 'ciede_test_data.txt') - data = np.loadtxt(path, dtype=dtype) - - N = len(data) - - lab1 = np.zeros((N, 3)) - lab1[:, 0] = data['L1'] - lab1[:, 1] = data['a1'] - lab1[:, 2] = data['b1'] - - lab2 = np.zeros((N, 3)) - lab2[:, 0] = data['L2'] - lab2[:, 1] = data['a2'] - lab2[:, 2] = data['b2'] - - dE2 = deltaE_ciede2000(lab1, lab2) - - assert_array_almost_equal(dE2, data['dE']) diff --git a/skimage/color/tests/test_dE.py b/skimage/color/tests/test_dE.py new file mode 100644 index 00000000..04ad933b --- /dev/null +++ b/skimage/color/tests/test_dE.py @@ -0,0 +1,145 @@ +"""Test for correctness of color distance functions + +Authors +------- +Matt Terry + +:license: modified BSD +""" +from os.path import abspath, dirname, join as pjoin + +import numpy as np +from numpy.testing import assert_array_almost_equal + +from skimage.color import (deltaE_cie76, + deltaE_ciede94, + deltaE_ciede2000, + deltaE_cmc) + + +def test_ciede2000_dE(): + data = load_ciede2000_data() + N = len(data) + lab1 = np.zeros((N, 3)) + lab1[:, 0] = data['L1'] + lab1[:, 1] = data['a1'] + lab1[:, 2] = data['b1'] + + lab2 = np.zeros((N, 3)) + lab2[:, 0] = data['L2'] + lab2[:, 1] = data['a2'] + lab2[:, 2] = data['b2'] + + dE2 = deltaE_ciede2000(lab1, lab2) + + assert_array_almost_equal(dE2, data['dE']) + + +def load_ciede2000_data(): + dtype = [('pair', int), + ('1', int), + ('L1', float), + ('a1', float), + ('b1', float), + ('a1_prime', float), + ('C1_prime', float), + ('h1_prime', float), + ('hbar_prime', float), + ('G', float), + ('T', float), + ('SL', float), + ('SC', float), + ('SH', float), + ('RT', float), + ('dE', float), + ('2', int), + ('L2', float), + ('a2', float), + ('b2', float), + ('a2_prime', float), + ('C2_prime', float), + ('h2_prime', float), + ] + + # note: ciede_test_data.txt contains several intermediate quantities + path = pjoin(dirname(abspath(__file__)), 'ciede_test_data.txt') + return np.loadtxt(path, dtype=dtype) + + +def test_cie76(): + data = load_ciede2000_data() + N = len(data) + lab1 = np.zeros((N, 3)) + lab1[:, 0] = data['L1'] + lab1[:, 1] = data['a1'] + lab1[:, 2] = data['b1'] + + lab2 = np.zeros((N, 3)) + lab2[:, 0] = data['L2'] + lab2[:, 1] = data['a2'] + lab2[:, 2] = data['b2'] + + dE2 = deltaE_cie76(lab1, lab2) + oracle = np.array([ + 4.00106328, 6.31415011, 9.1776999, 2.06270077, 2.36957073, + 2.91529271, 2.23606798, 2.23606798, 4.98000036, 4.9800004, + 4.98000044, 4.98000049, 4.98000036, 4.9800004, 4.98000044, + 3.53553391, 36.86800781, 31.91002977, 30.25309901, 27.40894015, + 0.89242934, 0.7972, 0.8583065, 0.82982507, 3.1819238, + 2.21334297, 1.53890382, 4.60630929, 6.58467989, 3.88641412, + 1.50514845, 2.3237848, 0.94413208, 1.31910843 + ]) + assert_array_almost_equal(dE2, oracle) + + +def test_ciede94(): + data = load_ciede2000_data() + N = len(data) + lab1 = np.zeros((N, 3)) + lab1[:, 0] = data['L1'] + lab1[:, 1] = data['a1'] + lab1[:, 2] = data['b1'] + + lab2 = np.zeros((N, 3)) + lab2[:, 0] = data['L2'] + lab2[:, 1] = data['a2'] + lab2[:, 2] = data['b2'] + + dE2 = deltaE_ciede94(lab1, lab2) + oracle = np.array([ + 1.39503887, 1.93410055, 2.45433566, 0.68449187, 0.6695627, + 0.69194527, 2.23606798, 2.03163832, 4.80069441, 4.80069445, + 4.80069449, 4.80069453, 4.80069441, 4.80069445, 4.80069449, + 3.40774352, 34.6891632, 29.44137328, 27.91408781, 24.93766082, + 0.82213163, 0.71658427, 0.8048753, 0.75284394, 1.39099471, + 1.24808929, 1.29795787, 1.82045088, 2.55613309, 1.42491303, + 1.41945261, 2.3225685, 0.93853308, 1.30654464 + ]) + assert_array_almost_equal(dE2, oracle) + + +def test_cmc(): + data = load_ciede2000_data() + N = len(data) + lab1 = np.zeros((N, 3)) + lab1[:, 0] = data['L1'] + lab1[:, 1] = data['a1'] + lab1[:, 2] = data['b1'] + + lab2 = np.zeros((N, 3)) + lab2[:, 0] = data['L2'] + lab2[:, 1] = data['a2'] + lab2[:, 2] = data['b2'] + + dE2 = deltaE_cmc(lab1, lab2) + oracle = np.array([ + 1.73873611, 2.49660844, 3.30494501, 0.85735576, 0.88332927, + 0.97822692, 3.50480874, 2.87930032, 6.5783807, 6.57838075, + 6.5783808, 6.57838086, 6.67492321, 6.67492326, 6.67492331, + 4.66852997, 42.10875485, 39.45889064, 38.36005919, 33.93663807, + 1.14400168, 1.00600419, 1.11302547, 1.05335328, 1.42822951, + 1.2548143, 1.76838061, 2.02583367, 3.08695508, 1.74893533, + 1.90095165, 1.70258148, 1.80317207, 2.44934417 + ]) + + assert_array_almost_equal(dE2, oracle) From 65d655e3ecdd20636bc093ec83268e1178f4c95c Mon Sep 17 00:00:00 2001 From: Matt Terry Date: Mon, 22 Jul 2013 15:46:20 -0700 Subject: [PATCH 331/736] cleaner lch implementations --- skimage/color/colorconv.py | 31 +++++++++++-------------------- 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/skimage/color/colorconv.py b/skimage/color/colorconv.py index 36fbeb1a..b1e70571 100644 --- a/skimage/color/colorconv.py +++ b/skimage/color/colorconv.py @@ -1032,8 +1032,8 @@ def combine_stains(stains, conv_matrix): def lab2lch(lab): - """CIE-LAB to LCH color space conversion. - TODO: something about cylindrical representation + """CIE-LAB to CIE-LCH color space conversion. + LCH is the cylindrical representation of the LAB (cartesian) colorspace Parameters ---------- @@ -1065,25 +1065,19 @@ def lab2lch(lab): >>> lena_lab = rgb2lab(lena) >>> lena_lch = lab2lch(lena_lab) """ - shape = lab.shape - if len(shape) != 3 or shape[2] != 3: - raise ValueError("Input image expected to be LAB") + lch = _prepare_colorarray(lab).copy() - lab = _prepare_colorarray(lab) - lch = np.empty_like(lab) + a, b = lch[..., 1], lch[..., 2] + lch[..., 1], lch[..., 2] = np.sqrt(a**2 + b**2), np.arctan2(b, a) - a, b = lab[:, :, 1], lab[:, :, 2] - lch[:, :, 0] = lab[:, :, 0] - lch[:, :, 1] = np.sqrt(a**2 + b**2) # C - - H = lch[:, :, 2] = np.arctan2(b, a) + H = lch[..., 2] H[H < 0] += 2*np.pi # (-pi, pi) -> (0, 2*pi) return lch def lch2lab(lch): """CIE-LCH to CIE-LAB color space conversion. - TODO: something about cylindrical representation + LCH is the cylindrical representation of the LAB (cartesian) colorspace Parameters ---------- @@ -1112,11 +1106,8 @@ def lch2lab(lch): >>> lena_lch = lab2lch(lena_lab) >>> lena_lab2 = lch2lab(lena_lch) """ - lch = _prepare_colorarray(lch) - lab = np.empty_like(lch) + lch = _prepare_colorarray(lch).copy() - c, h = lch[:, :, 1], lch[:, :, 2] - lab[:, :, 0] = lch[:, :, 0] - lab[:, :, 1] = c*np.cos(h) - lab[:, :, 2] = c*np.sin(h) - return lab + c, h = lch[..., 1], lch[..., 2] + lch[..., 1], lch[..., 2] = c*np.cos(h), c*np.sin(h) + return lch From 4fd18d43003839f798037c1921497cc9a0c2984f Mon Sep 17 00:00:00 2001 From: Matt Terry Date: Mon, 22 Jul 2013 15:56:12 -0700 Subject: [PATCH 332/736] fix path --- skimage/color/tests/test_dE.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/color/tests/test_dE.py b/skimage/color/tests/test_dE.py index 04ad933b..52e12724 100644 --- a/skimage/color/tests/test_dE.py +++ b/skimage/color/tests/test_dE.py @@ -62,7 +62,7 @@ def load_ciede2000_data(): ] # note: ciede_test_data.txt contains several intermediate quantities - path = pjoin(dirname(abspath(__file__)), 'ciede_test_data.txt') + path = pjoin(dirname(abspath(__file__)), 'ciede2000_test_data.txt') return np.loadtxt(path, dtype=dtype) From b2b101cc184a7c055fde1d04dc79bfd540c0a8cd Mon Sep 17 00:00:00 2001 From: Matt Terry Date: Tue, 23 Jul 2013 09:13:38 -0700 Subject: [PATCH 333/736] set testing tolerance --- skimage/color/tests/test_dE.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/skimage/color/tests/test_dE.py b/skimage/color/tests/test_dE.py index 52e12724..230e6e83 100644 --- a/skimage/color/tests/test_dE.py +++ b/skimage/color/tests/test_dE.py @@ -9,7 +9,7 @@ Matt Terry from os.path import abspath, dirname, join as pjoin import numpy as np -from numpy.testing import assert_array_almost_equal +from numpy.testing import assert_allclose from skimage.color import (deltaE_cie76, deltaE_ciede94, @@ -32,7 +32,7 @@ def test_ciede2000_dE(): dE2 = deltaE_ciede2000(lab1, lab2) - assert_array_almost_equal(dE2, data['dE']) + assert_allclose(dE2, data['dE'], rtol=1.e-4) def load_ciede2000_data(): @@ -89,7 +89,7 @@ def test_cie76(): 2.21334297, 1.53890382, 4.60630929, 6.58467989, 3.88641412, 1.50514845, 2.3237848, 0.94413208, 1.31910843 ]) - assert_array_almost_equal(dE2, oracle) + assert_allclose(dE2, oracle, rtol=1.e-8) def test_ciede94(): @@ -115,7 +115,7 @@ def test_ciede94(): 1.24808929, 1.29795787, 1.82045088, 2.55613309, 1.42491303, 1.41945261, 2.3225685, 0.93853308, 1.30654464 ]) - assert_array_almost_equal(dE2, oracle) + assert_allclose(dE2, oracle, rtol=1.e-8) def test_cmc(): @@ -142,4 +142,4 @@ def test_cmc(): 1.90095165, 1.70258148, 1.80317207, 2.44934417 ]) - assert_array_almost_equal(dE2, oracle) + assert_allclose(dE2, oracle, rtol=1.e-8) From 01d8cb2f4d099d6cd1484c765f213a99fd000a2d Mon Sep 17 00:00:00 2001 From: Matt Terry Date: Tue, 23 Jul 2013 09:16:18 -0700 Subject: [PATCH 334/736] make naming consistent --- skimage/color/tests/{test_dE.py => test_delta_e.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename skimage/color/tests/{test_dE.py => test_delta_e.py} (100%) diff --git a/skimage/color/tests/test_dE.py b/skimage/color/tests/test_delta_e.py similarity index 100% rename from skimage/color/tests/test_dE.py rename to skimage/color/tests/test_delta_e.py From 1dc46c827ff15133b7dc4c70183d856ee63e7c09 Mon Sep 17 00:00:00 2001 From: Matt Terry Date: Tue, 23 Jul 2013 09:21:48 -0700 Subject: [PATCH 335/736] add references --- skimage/color/delta_e.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/skimage/color/delta_e.py b/skimage/color/delta_e.py index 47bf6e1a..3703254e 100644 --- a/skimage/color/delta_e.py +++ b/skimage/color/delta_e.py @@ -56,6 +56,10 @@ def deltaE_cie76(lab1, lab2): ------- dE : array_like distance between colors `lab1` and `lab2` + + References + ---------- + http://en.wikipedia.org/wiki/Color_difference """ l1, a1, b1 = _unpack_last(lab1) l2, a2, b2 = _unpack_last(lab2) @@ -105,6 +109,11 @@ def deltaE_ciede94(lab1, lab2, kH=1, kC=1, kL=1, k1=0.045, k2=0.015): kL 1.000 2.000 k1 0.045 0.048 k2 0.015 0.014 + + References + ---------- + http://en.wikipedia.org/wiki/Color_difference + http://www.brucelindbloom.com/index.html?Eqn_DeltaE_CIE94.html """ l1, a1, b1 = _unpack_last(lab1) l2, a2, b2 = _unpack_last(lab2) @@ -261,6 +270,11 @@ def deltaE_cmc(lab1, lab2, kL=1, kC=1): deltaE_cmc the defines the scales for the lightness, hue, and chroma in terms of the first color. Consequently deltaE_cmc(lab1, lab2) != deltaE_cmc(lab2, lab1) + + References + ---------- + http://en.wikipedia.org/wiki/Color_difference + http://www.brucelindbloom.com/index.html?Eqn_DeltaE_CIE94.html """ l1, a1, b1 = _unpack_last(lab1) l2, a2, b2 = _unpack_last(lab2) From 4b3c11d23e2b799d06b71dac29080bd12c5c44f1 Mon Sep 17 00:00:00 2001 From: Matt Terry Date: Tue, 23 Jul 2013 09:32:57 -0700 Subject: [PATCH 336/736] references --- skimage/color/delta_e.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/skimage/color/delta_e.py b/skimage/color/delta_e.py index 3703254e..9f8cd2e8 100644 --- a/skimage/color/delta_e.py +++ b/skimage/color/delta_e.py @@ -60,6 +60,7 @@ def deltaE_cie76(lab1, lab2): References ---------- http://en.wikipedia.org/wiki/Color_difference + A. R. Robertson, "The CIE 1976 color-difference formulae," Color Res. Appl. 2, 7-11 (1977). """ l1, a1, b1 = _unpack_last(lab1) l2, a2, b2 = _unpack_last(lab2) @@ -165,6 +166,7 @@ def deltaE_ciede2000(lab1, lab2, kL=1, kC=1, kH=1): ---------- http://en.wikipedia.org/wiki/Color_difference http://www.ece.rochester.edu/~gsharma/ciede2000/ciede2000noteCRNA.pdf (doi:10.1364/AO.33.008069) + M. Melgosa, J. Quesada, and E. Hita, "Uniformity of some recent color metrics tested with an accurate color-difference tolerance dataset," Appl. Opt. 33, 8069-8077 (1994). """ L1, a1, b1 = _unpack_last(lab1) L2, a2, b2 = _unpack_last(lab2) @@ -275,6 +277,7 @@ def deltaE_cmc(lab1, lab2, kL=1, kC=1): ---------- http://en.wikipedia.org/wiki/Color_difference http://www.brucelindbloom.com/index.html?Eqn_DeltaE_CIE94.html + F. J. J. Clarke, R. McDonald, and B. Rigg, "Modification to the JPC79 colour-difference formula," J. Soc. Dyers Colour. 100, 128-132 (1984). """ l1, a1, b1 = _unpack_last(lab1) l2, a2, b2 = _unpack_last(lab2) From f67a088cce1621f0171179d298930a4dcc7af875 Mon Sep 17 00:00:00 2001 From: Matt Terry Date: Tue, 23 Jul 2013 09:33:55 -0700 Subject: [PATCH 337/736] references --- skimage/color/delta_e.py | 1 + 1 file changed, 1 insertion(+) diff --git a/skimage/color/delta_e.py b/skimage/color/delta_e.py index 9f8cd2e8..70bbb99b 100644 --- a/skimage/color/delta_e.py +++ b/skimage/color/delta_e.py @@ -18,6 +18,7 @@ The delta-E notation comes from the German word for "Sensation" (Empfindung). Reference --------- +http://en.wikipedia.org/wiki/Color_difference """ from __future__ import division From 86f7c4a1d5aff9aa385539b3911936c0bc3796db Mon Sep 17 00:00:00 2001 From: Matt Terry Date: Tue, 23 Jul 2013 21:32:37 -0700 Subject: [PATCH 338/736] expose lab/lch conversion --- skimage/color/__init__.py | 4 ++++ skimage/color/tests/test_colorconv.py | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/skimage/color/__init__.py b/skimage/color/__init__.py index 95ccb0c0..1c61020d 100644 --- a/skimage/color/__init__.py +++ b/skimage/color/__init__.py @@ -15,6 +15,8 @@ from .colorconv import (convert_colorspace, rgb2lab, rgb2hed, hed2rgb, + lab2lch, + lch2lab, separate_stains, combine_stains, rgb_from_hed, @@ -68,6 +70,8 @@ __all__ = ['convert_colorspace', 'rgb2lab', 'rgb2hed', 'hed2rgb', + 'lab2lch', + 'lch2lab', 'separate_stains', 'combine_stains', 'rgb_from_hed', diff --git a/skimage/color/tests/test_colorconv.py b/skimage/color/tests/test_colorconv.py index 4fdaa4c8..a184f807 100644 --- a/skimage/color/tests/test_colorconv.py +++ b/skimage/color/tests/test_colorconv.py @@ -34,6 +34,7 @@ from skimage.color import (rgb2hsv, hsv2rgb, xyz2lab, lab2xyz, lab2rgb, rgb2lab, is_rgb, is_gray, + lab2lch, lch2lab, guess_spatial_dimensions ) @@ -249,6 +250,13 @@ class TestColorconv(TestCase): img_rgb = img_as_float(self.img_rgb) assert_array_almost_equal(lab2rgb(rgb2lab(img_rgb)), img_rgb) + def test_lab_lch_roundtrip(self): + rgb = img_as_float(self.img_rgb) + lab = rgb2lab(rgb) + lab2 = lch2lab(lab2lch(lab)) + assert_array_almost_equal(lab2, lab) + + def test_gray2rgb(): x = np.array([0, 0.5, 1]) assert_raises(ValueError, gray2rgb, x) From 499141cd2b9131285e52d49f4d8683c4ddcf85e8 Mon Sep 17 00:00:00 2001 From: Matt Terry Date: Tue, 23 Jul 2013 21:32:49 -0700 Subject: [PATCH 339/736] docs --- skimage/color/colorconv.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/skimage/color/colorconv.py b/skimage/color/colorconv.py index b1e70571..ae0c1262 100644 --- a/skimage/color/colorconv.py +++ b/skimage/color/colorconv.py @@ -27,7 +27,11 @@ Supported color spaces ``x == y == z == 1/3`` at the whitepoint, and all color matching functions are greater than zero everywhere. * LAB CIE : Lightness, a, b + Colorspace derived from XYZ CIE that is intented to be more + perceptually uniform * LCH CIE : Lightness, Chroma, Hue + Defined in terms of LAB CIE. C and H are the polar representation of + a and b. The polar angle C is defined to be on (0, 2*pi) :author: Nicolas Pinto (rgb2hsv) :author: Ralf Gommers (hsv2rgb) From af3ea5f81784cfbaed37d4a25f670751e426f2b4 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Thu, 25 Jul 2013 00:23:01 +1000 Subject: [PATCH 340/736] Remove unused 'ratio' argument from _slic_cython --- skimage/segmentation/_slic.pyx | 8 +++----- skimage/segmentation/slic_superpixels.py | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/skimage/segmentation/_slic.pyx b/skimage/segmentation/_slic.pyx index f6c6788c..055907e9 100644 --- a/skimage/segmentation/_slic.pyx +++ b/skimage/segmentation/_slic.pyx @@ -17,7 +17,7 @@ def _slic_cython(double[:, :, :, ::1] image_zyx, long[:, :, ::1] nearest_mean, double[:, :, ::1] distance, double[:, ::1] means, - float ratio, int max_iter, int n_segments): + int max_iter, int n_segments): """Helper function for SLIC segmentation. Parameters @@ -32,8 +32,6 @@ def _slic_cython(double[:, :, :, ::1] image_zyx, The (initially infinity) array of distances to the nearest centroid. means : 2D np.ndarray of double, shape (n_segments, 6) The centroids obtained by SLIC. - ratio : float - The ratio of xyz-space and colorspace in the clustering. max_iter : int The maximum number of k-means iterations. n_segments : int @@ -58,7 +56,6 @@ def _slic_cython(double[:, :, :, ::1] image_zyx, cdef Py_ssize_t i, k, x, y, z, x_min, x_max, y_min, y_max, z_min, z_max, \ changes cdef double dist_mean - cdef double tmp for i in range(max_iter): changes = 0 @@ -92,7 +89,8 @@ def _slic_cython(double[:, :, :, ::1] image_zyx, nearest_mean_ravel = np.asarray(nearest_mean).ravel() means_list = [] for j in range(6): - image_zyx_ravel = np.ascontiguousarray(image_zyx[:, :, :, j]).ravel() + image_zyx_ravel = ( + np.ascontiguousarray(image_zyx[:, :, :, j]).ravel()) means_list.append(np.bincount(nearest_mean_ravel, image_zyx_ravel)) in_mean = np.bincount(nearest_mean_ravel) diff --git a/skimage/segmentation/slic_superpixels.py b/skimage/segmentation/slic_superpixels.py index 478093a1..cef668ef 100644 --- a/skimage/segmentation/slic_superpixels.py +++ b/skimage/segmentation/slic_superpixels.py @@ -130,7 +130,7 @@ def slic(image, n_segments=100, ratio=10., max_iter=10, sigma=1, nearest_mean = np.zeros((depth, height, width), dtype=np.intp) distance = np.empty((depth, height, width), dtype=np.float) segment_map = _slic_cython(image_zyx, nearest_mean, distance, means, - ratio, max_iter, n_segments) + max_iter, n_segments) if segment_map.shape[0] == 1: segment_map = segment_map[0] return segment_map From 3e99079107291e956e387981578ace4f18e479cd Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Thu, 25 Jul 2013 00:24:04 +1000 Subject: [PATCH 341/736] Precision issues appear to have vanished --- skimage/segmentation/_slic.pyx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/skimage/segmentation/_slic.pyx b/skimage/segmentation/_slic.pyx index 055907e9..d4121721 100644 --- a/skimage/segmentation/_slic.pyx +++ b/skimage/segmentation/_slic.pyx @@ -78,8 +78,7 @@ def _slic_cython(double[:, :, :, ::1] image_zyx, # squaring itself. mine can't (with O2) tmp = image_zyx[z, y, x, c] - means[k, c] dist_mean += tmp * tmp - # some precision issue here. Doesnt work if testing ">" - if distance[z, y, x] - dist_mean > 1e-10: + if distance[z, y, x] > dist_mean: nearest_mean[z, y, x] = k distance[z, y, x] = dist_mean changes = 1 From 1cd918e5c06a03a5dce4dd94447cac24a248b025 Mon Sep 17 00:00:00 2001 From: Matt Terry Date: Wed, 24 Jul 2013 07:54:56 -0700 Subject: [PATCH 342/736] pep8 math operators --- skimage/color/colorconv.py | 6 +- skimage/color/delta_e.py | 114 ++++++++++++++++++------------------- 2 files changed, 60 insertions(+), 60 deletions(-) diff --git a/skimage/color/colorconv.py b/skimage/color/colorconv.py index ae0c1262..726240bd 100644 --- a/skimage/color/colorconv.py +++ b/skimage/color/colorconv.py @@ -1072,10 +1072,10 @@ def lab2lch(lab): lch = _prepare_colorarray(lab).copy() a, b = lch[..., 1], lch[..., 2] - lch[..., 1], lch[..., 2] = np.sqrt(a**2 + b**2), np.arctan2(b, a) + lch[..., 1], lch[..., 2] = np.sqrt(a ** 2 + b ** 2), np.arctan2(b, a) H = lch[..., 2] - H[H < 0] += 2*np.pi # (-pi, pi) -> (0, 2*pi) + H[H < 0] += 2 * np.pi # (-pi, pi) -> (0, 2*pi) return lch @@ -1113,5 +1113,5 @@ def lch2lab(lch): lch = _prepare_colorarray(lch).copy() c, h = lch[..., 1], lch[..., 2] - lch[..., 1], lch[..., 2] = c*np.cos(h), c*np.sin(h) + lch[..., 1], lch[..., 2] = c * np.cos(h), c * np.sin(h) return lch diff --git a/skimage/color/delta_e.py b/skimage/color/delta_e.py index 70bbb99b..d5d5bb81 100644 --- a/skimage/color/delta_e.py +++ b/skimage/color/delta_e.py @@ -25,7 +25,7 @@ from __future__ import division import numpy as np -DEG = np.pi/180 +DEG = np.pi / 180 def _unpack_last(x): @@ -35,11 +35,9 @@ def _unpack_last(x): def _arctan2pi(b, a): - """np.arctan2 mapped to (0, 2*pi)""" + """np.arctan2 mapped to (0, 2 * pi)""" ans = np.arctan2(b, a) - ans += np.where(ans < 0, 2*np.pi, 0.) - assert ans.max() <= 2*np.pi - assert ans.min() >= 0. + ans += np.where(ans < 0, 2 * np.pi, 0.) return ans @@ -65,7 +63,7 @@ def deltaE_cie76(lab1, lab2): """ l1, a1, b1 = _unpack_last(lab1) l2, a2, b2 = _unpack_last(lab2) - return np.sqrt((l2-l1)**2 + (a2-a1)**2 + (b2-b1)**2) + return np.sqrt((l2 - l1) ** 2 + (a2 - a1) ** 2 + (b2 - b1) ** 2) def deltaE_ciede94(lab1, lab2, kH=1, kC=1, kL=1, k1=0.045, k2=0.015): @@ -121,16 +119,19 @@ def deltaE_ciede94(lab1, lab2, kH=1, kC=1, kL=1, k1=0.045, k2=0.015): l2, a2, b2 = _unpack_last(lab2) dl = l1 - l2 - c1 = np.sqrt(a1**2 + b1**2) - c2 = np.sqrt(a2**2 + b2**2) + c1 = np.sqrt(a1 ** 2 + b1 ** 2) + c2 = np.sqrt(a2 ** 2 + b2 ** 2) dc = c1 - c2 - dh_ab = np.sqrt(deltaE_cie76(lab1, lab2)**2 - dl**2 - dc**2) + dh_ab = np.sqrt(deltaE_cie76(lab1, lab2) ** 2 - dl ** 2 - dc ** 2) SL = 1 - SC = 1 + k1*c1 - SH = 1 + k2*c1 + SC = 1 + k1 * c1 + SH = 1 + k2 * c1 - ans = (dl/(kL*SL))**2 + (dc/(kC*SC))**2 + (dh_ab/(kH*SH))**2 + ans = ((dl / (kL * SL)) ** 2 + + (dc / (kC * SC)) ** 2 + + (dh_ab / (kH * SH)) ** 2 + ) return np.sqrt(ans) @@ -172,21 +173,21 @@ def deltaE_ciede2000(lab1, lab2, kL=1, kC=1, kH=1): L1, a1, b1 = _unpack_last(lab1) L2, a2, b2 = _unpack_last(lab2) - c1 = np.sqrt(a1**2 + b1**2) - c2 = np.sqrt(a2**2 + b2**2) - cbar = 0.5*(c1 + c2) - c7 = cbar**7 - G = 0.5 * (1 - np.sqrt(c7/(c7 + 25**7))) + c1 = np.sqrt(a1 ** 2 + b1 ** 2) + c2 = np.sqrt(a2 ** 2 + b2 ** 2) + cbar = 0.5 * (c1 + c2) + c7 = cbar ** 7 + G = 0.5 * (1 - np.sqrt(c7 / (c7 + 25 ** 7))) dL_prime = L2 - L1 - Lbar = 0.5*(L1 + L2) + Lbar = 0.5 * (L1 + L2) a1_prime = a1 * (1 + G) a2_prime = a2 * (1 + G) - c1_prime = np.sqrt(a1_prime**2 + b1**2) - c2_prime = np.sqrt(a2_prime**2 + b2**2) - cbar_prime = 0.5*(c1_prime + c2_prime) + c1_prime = np.sqrt(a1_prime ** 2 + b1 ** 2) + c2_prime = np.sqrt(a2_prime ** 2 + b2 ** 2) + cbar_prime = 0.5 * (c1_prime + c2_prime) dC_prime = c2_prime - c1_prime h1_prime = _arctan2pi(b1, a1_prime) @@ -199,47 +200,46 @@ def deltaE_ciede2000(lab1, lab2, kL=1, kC=1, kH=1): mask2 = np.logical_and(-mask1, dh_prime > np.pi) mask3 = np.logical_and(-mask1, dh_prime < -np.pi) dh_prime = np.where(mask1, 0., dh_prime) - dh_prime += np.where(mask2, 2*np.pi, 0) - dh_prime -= np.where(mask3, 2*np.pi, 0) + dh_prime += np.where(mask2, 2 * np.pi, 0) + dh_prime -= np.where(mask3, 2 * np.pi, 0) - dH_prime = 2 * np.sqrt(cc) * np.sin(dh_prime/2) + dH_prime = 2 * np.sqrt(cc) * np.sin(dh_prime / 2) Hbar_prime = h1_prime + h2_prime mask0 = np.logical_and(np.abs(h1_prime - h2_prime) > np.pi, cc != 0.) - mask1 = np.logical_and(mask0, Hbar_prime < 2*np.pi) - mask2 = np.logical_and(mask0, Hbar_prime >= 2*np.pi) + mask1 = np.logical_and(mask0, Hbar_prime < 2 * np.pi) + mask2 = np.logical_and(mask0, Hbar_prime >= 2 * np.pi) - Hbar_prime += np.where(mask1, 2*np.pi, 0) - Hbar_prime -= np.where(mask2, 2*np.pi, 0) + Hbar_prime += np.where(mask1, 2 * np.pi, 0) + Hbar_prime -= np.where(mask2, 2 * np.pi, 0) Hbar_prime *= np.where(cc == 0., 2, 1) Hbar_prime *= 0.5 - deg = np.pi/180. T = (1 - - 0.17 * np.cos(Hbar_prime - 30*deg) + - 0.24 * np.cos(2*Hbar_prime) + - 0.32 * np.cos(3*Hbar_prime + 6*deg) - - 0.20 * np.cos(4*Hbar_prime - 63*deg) + 0.17 * np.cos(Hbar_prime - 30 * DEG) + + 0.24 * np.cos(2 * Hbar_prime) + + 0.32 * np.cos(3 * Hbar_prime + 6 * DEG) - + 0.20 * np.cos(4 * Hbar_prime - 63 * DEG) ) - dTheta = 30*deg * np.exp(-((Hbar_prime/deg - 275)/25)**2) - c7 = cbar_prime**7 - Rc = 2 * np.sqrt(c7 / (c7 + 25**7)) + dTheta = 30 * DEG * np.exp(-((Hbar_prime / DEG - 275) / 25) ** 2) + c7 = cbar_prime ** 7 + Rc = 2 * np.sqrt(c7 / (c7 + 25 ** 7)) - term = (Lbar - 50)**2 - SL = 1 + 0.015*term/np.sqrt(20 + term) - SC = 1 + 0.045*cbar_prime - SH = 1 + 0.015*cbar_prime * T + term = (Lbar - 50) ** 2 + SL = 1 + 0.015 * term / np.sqrt(20 + term) + SC = 1 + 0.045 * cbar_prime + SH = 1 + 0.015 * cbar_prime * T - RT = -np.sin(2*dTheta) * Rc + RT = -np.sin(2 * dTheta) * Rc l_term = dL_prime / (kL * SL) c_term = dC_prime / (kC * SC) h_term = dH_prime / (kH * SH) r_term = RT * c_term * h_term - dE2 = l_term**2 - dE2 += c_term**2 - dE2 += h_term**2 + dE2 = l_term ** 2 + dE2 += c_term ** 2 + dE2 += h_term ** 2 dE2 += r_term return np.sqrt(dE2) @@ -283,28 +283,28 @@ def deltaE_cmc(lab1, lab2, kL=1, kC=1): l1, a1, b1 = _unpack_last(lab1) l2, a2, b2 = _unpack_last(lab2) - c1 = np.sqrt(a1**2 + b1**2) - c2 = np.sqrt(a2**2 + b2**2) + c1 = np.sqrt(a1 ** 2 + b1 ** 2) + c2 = np.sqrt(a2 ** 2 + b2 ** 2) dC = c1 - c2 dl = l1 - l2 - dH = np.sqrt(deltaE_cie76(lab1, lab2)**2 - dl**2 - dC**2) + dH = np.sqrt(deltaE_cie76(lab1, lab2) ** 2 - dl ** 2 - dC ** 2) dL = l1 - l2 h1 = _arctan2pi(b1, a1) - T = np.where(np.logical_and(h1 >= 164*DEG, h1 <= 345*DEG), - 0.56 + 0.2 * np.abs(np.cos(h1 + 168*DEG)), - 0.36 + 0.4 * np.abs(np.cos(h1 + 35*DEG)) + T = np.where(np.logical_and(h1 >= 164 * DEG, h1 <= 345 * DEG), + 0.56 + 0.2 * np.abs(np.cos(h1 + 168 * DEG)), + 0.36 + 0.4 * np.abs(np.cos(h1 + 35 * DEG)) ) - c1_4 = c1**4 + c1_4 = c1 ** 4 F = np.sqrt(c1_4 / (c1_4 + 1900)) - SL = np.where(l1 < 16, 0.511, 0.040975*l1 / (1. + 0.01765*l1)) - SC = 0.638 + 0.0638 * c1 / (1. + 0.0131*c1) - SH = SC * (F*T + 1 - F) + SL = np.where(l1 < 16, 0.511, 0.040975 * l1 / (1. + 0.01765 * l1)) + SC = 0.638 + 0.0638 * c1 / (1. + 0.0131 * c1) + SH = SC * (F * T + 1 - F) - dE2 = (dL / (kL*SL))**2 - dE2 += (dC/(kC*SC))**2 - dE2 += (dH/SH)**2 + dE2 = (dL / (kL * SL)) ** 2 + dE2 += (dC / (kC * SC)) ** 2 + dE2 += (dH / SH) ** 2 return np.sqrt(dE2) From b566ba496b20dc7b60de19f9faa524aff560413f Mon Sep 17 00:00:00 2001 From: Matt Terry Date: Wed, 24 Jul 2013 08:07:14 -0700 Subject: [PATCH 343/736] use np.rollaxis --- skimage/color/delta_e.py | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/skimage/color/delta_e.py b/skimage/color/delta_e.py index d5d5bb81..1728464f 100644 --- a/skimage/color/delta_e.py +++ b/skimage/color/delta_e.py @@ -28,12 +28,6 @@ import numpy as np DEG = np.pi / 180 -def _unpack_last(x): - x = np.asarray(x) - shape = x.shape - return [x[..., i] for i in range(shape[-1])] - - def _arctan2pi(b, a): """np.arctan2 mapped to (0, 2 * pi)""" ans = np.arctan2(b, a) @@ -61,8 +55,8 @@ def deltaE_cie76(lab1, lab2): http://en.wikipedia.org/wiki/Color_difference A. R. Robertson, "The CIE 1976 color-difference formulae," Color Res. Appl. 2, 7-11 (1977). """ - l1, a1, b1 = _unpack_last(lab1) - l2, a2, b2 = _unpack_last(lab2) + l1, a1, b1 = np.rollaxis(lab1, -1)[:3] + l2, a2, b2 = np.rollaxis(lab2, -1)[:3] return np.sqrt((l2 - l1) ** 2 + (a2 - a1) ** 2 + (b2 - b1) ** 2) @@ -115,8 +109,8 @@ def deltaE_ciede94(lab1, lab2, kH=1, kC=1, kL=1, k1=0.045, k2=0.015): http://en.wikipedia.org/wiki/Color_difference http://www.brucelindbloom.com/index.html?Eqn_DeltaE_CIE94.html """ - l1, a1, b1 = _unpack_last(lab1) - l2, a2, b2 = _unpack_last(lab2) + l1, a1, b1 = np.rollaxis(lab1, -1)[:3] + l2, a2, b2 = np.rollaxis(lab2, -1)[:3] dl = l1 - l2 c1 = np.sqrt(a1 ** 2 + b1 ** 2) @@ -170,8 +164,8 @@ def deltaE_ciede2000(lab1, lab2, kL=1, kC=1, kH=1): http://www.ece.rochester.edu/~gsharma/ciede2000/ciede2000noteCRNA.pdf (doi:10.1364/AO.33.008069) M. Melgosa, J. Quesada, and E. Hita, "Uniformity of some recent color metrics tested with an accurate color-difference tolerance dataset," Appl. Opt. 33, 8069-8077 (1994). """ - L1, a1, b1 = _unpack_last(lab1) - L2, a2, b2 = _unpack_last(lab2) + L1, a1, b1 = np.rollaxis(lab1, -1)[:3] + L2, a2, b2 = np.rollaxis(lab2, -1)[:3] c1 = np.sqrt(a1 ** 2 + b1 ** 2) c2 = np.sqrt(a2 ** 2 + b2 ** 2) @@ -280,8 +274,8 @@ def deltaE_cmc(lab1, lab2, kL=1, kC=1): http://www.brucelindbloom.com/index.html?Eqn_DeltaE_CIE94.html F. J. J. Clarke, R. McDonald, and B. Rigg, "Modification to the JPC79 colour-difference formula," J. Soc. Dyers Colour. 100, 128-132 (1984). """ - l1, a1, b1 = _unpack_last(lab1) - l2, a2, b2 = _unpack_last(lab2) + l1, a1, b1 = np.rollaxis(lab1, -1)[:3] + l2, a2, b2 = np.rollaxis(lab2, -1)[:3] c1 = np.sqrt(a1 ** 2 + b1 ** 2) c2 = np.sqrt(a2 ** 2 + b2 ** 2) From 3326239962c1e3923efe391757b47416dfa9dae8 Mon Sep 17 00:00:00 2001 From: Matt Terry Date: Wed, 24 Jul 2013 08:20:21 -0700 Subject: [PATCH 344/736] reference formatting --- skimage/color/delta_e.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/skimage/color/delta_e.py b/skimage/color/delta_e.py index 1728464f..f6ff87a5 100644 --- a/skimage/color/delta_e.py +++ b/skimage/color/delta_e.py @@ -52,8 +52,9 @@ def deltaE_cie76(lab1, lab2): References ---------- - http://en.wikipedia.org/wiki/Color_difference - A. R. Robertson, "The CIE 1976 color-difference formulae," Color Res. Appl. 2, 7-11 (1977). + .. [1] http://en.wikipedia.org/wiki/Color_difference + .. [2] A. R. Robertson, "The CIE 1976 color-difference formulae," + Color Res. Appl. 2, 7-11 (1977). """ l1, a1, b1 = np.rollaxis(lab1, -1)[:3] l2, a2, b2 = np.rollaxis(lab2, -1)[:3] @@ -106,8 +107,8 @@ def deltaE_ciede94(lab1, lab2, kH=1, kC=1, kL=1, k1=0.045, k2=0.015): References ---------- - http://en.wikipedia.org/wiki/Color_difference - http://www.brucelindbloom.com/index.html?Eqn_DeltaE_CIE94.html + .. [1] http://en.wikipedia.org/wiki/Color_difference + .. [2] http://www.brucelindbloom.com/index.html?Eqn_DeltaE_CIE94.html """ l1, a1, b1 = np.rollaxis(lab1, -1)[:3] l2, a2, b2 = np.rollaxis(lab2, -1)[:3] @@ -160,9 +161,12 @@ def deltaE_ciede2000(lab1, lab2, kL=1, kC=1, kH=1): References ---------- - http://en.wikipedia.org/wiki/Color_difference - http://www.ece.rochester.edu/~gsharma/ciede2000/ciede2000noteCRNA.pdf (doi:10.1364/AO.33.008069) - M. Melgosa, J. Quesada, and E. Hita, "Uniformity of some recent color metrics tested with an accurate color-difference tolerance dataset," Appl. Opt. 33, 8069-8077 (1994). + .. [1] http://en.wikipedia.org/wiki/Color_difference + .. [2] http://www.ece.rochester.edu/~gsharma/ciede2000/ciede2000noteCRNA.pdf + (doi:10.1364/AO.33.008069) + .. [3] M. Melgosa, J. Quesada, and E. Hita, "Uniformity of some recent + color metrics tested with an accurate color-difference tolerance + dataset," Appl. Opt. 33, 8069-8077 (1994). """ L1, a1, b1 = np.rollaxis(lab1, -1)[:3] L2, a2, b2 = np.rollaxis(lab2, -1)[:3] @@ -270,9 +274,11 @@ def deltaE_cmc(lab1, lab2, kL=1, kC=1): References ---------- - http://en.wikipedia.org/wiki/Color_difference - http://www.brucelindbloom.com/index.html?Eqn_DeltaE_CIE94.html - F. J. J. Clarke, R. McDonald, and B. Rigg, "Modification to the JPC79 colour-difference formula," J. Soc. Dyers Colour. 100, 128-132 (1984). + .. [1] http://en.wikipedia.org/wiki/Color_difference + .. [2] http://www.brucelindbloom.com/index.html?Eqn_DeltaE_CIE94.html + .. [3] F. J. J. Clarke, R. McDonald, and B. Rigg, "Modification to the + JPC79 colour-difference formula," J. Soc. Dyers Colour. 100, 128-132 + (1984). """ l1, a1, b1 = np.rollaxis(lab1, -1)[:3] l2, a2, b2 = np.rollaxis(lab2, -1)[:3] From 109f500d4bae033419095db69f63d21800b36c99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Thu, 25 Jul 2013 22:15:23 +0200 Subject: [PATCH 345/736] Replace deprecated import of _local_func with block_reduce --- skimage/transform/_warps.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skimage/transform/_warps.py b/skimage/transform/_warps.py index e4463721..d8da73d5 100644 --- a/skimage/transform/_warps.py +++ b/skimage/transform/_warps.py @@ -3,7 +3,7 @@ from scipy import ndimage from skimage.transform._geometric import (warp, SimilarityTransform, AffineTransform) -from skimage.measure.local import _local_func +from skimage.measure import block_reduce def resize(image, output_shape, order=1, mode='constant', cval=0.): @@ -264,7 +264,7 @@ def downscale_local_mean(image, factors, cval=0): [5.5, 4.5]]) """ - return _local_func(image, factors, np.mean, cval) + return block_reduce(image, factors, np.mean, cval) def _swirl_mapping(xy, center, rotation, strength, radius): From ab4e95e4f38a55dd645a52cafc88e2946780caca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Thu, 25 Jul 2013 22:35:12 +0200 Subject: [PATCH 346/736] Fix typo in doc string --- skimage/filter/_denoise_cy.pyx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/skimage/filter/_denoise_cy.pyx b/skimage/filter/_denoise_cy.pyx index b80b8e6b..0089e7b4 100644 --- a/skimage/filter/_denoise_cy.pyx +++ b/skimage/filter/_denoise_cy.pyx @@ -98,7 +98,7 @@ def denoise_bilateral(image, Py_ssize_t win_size=5, sigma_range=None, """ image = np.atleast_3d(img_as_float(image)) - + # if image.max() is 0, then dist_scale can have an unverified value # and color_lut[(dist * dist_scale)] may cause a segmentation fault # so we verify we have a positive image and that the max is not 0.0. @@ -112,7 +112,7 @@ def denoise_bilateral(image, Py_ssize_t win_size=5, sigma_range=None, Py_ssize_t window_ext = (win_size - 1) / 2 double max_value - + cnp.ndarray[dtype=cnp.double_t, ndim=3, mode='c'] cimage cnp.ndarray[dtype=cnp.double_t, ndim=3, mode='c'] out @@ -136,12 +136,12 @@ def denoise_bilateral(image, Py_ssize_t win_size=5, sigma_range=None, csigma_range = sigma_range max_value = image.max() - + if max_value == 0.0: raise ValueError("The maximum value found in the image was 0.") cimage = np.ascontiguousarray(image) - + out = np.zeros((rows, cols, dims), dtype=np.double) image_data = cimage.data out_data = out.data @@ -224,8 +224,8 @@ def denoise_tv_bregman(image, double weight, int max_iter=100, double eps=1e-3, max_iter: int, optional Maximal number of iterations used for the optimization. - isotropic: boolean, optimal - Switch between isotropic and anisotropic tv denoising + isotropic: boolean, optional + Switch between isotropic and anisotropic TV denoising. Returns ------- @@ -241,7 +241,7 @@ def denoise_tv_bregman(image, double weight, int max_iter=100, double eps=1e-3, .. [3] Pascal Getreuer, "Rudin–Osher–Fatemi Total Variation Denoising using Split Bregman" in Image Processing On Line on 2012–05–19, http://www.ipol.im/pub/art/2012/g-tvd/article_lr.pdf - .. [4] http://www.math.ucsb.edu/~cgarcia/UGProjects/BregmanAlgorithms_JacquelineBush.pdf + .. [4] http://www.math.ucsb.edu/~cgarcia/UGProjects/BregmanAlgorithms_JacquelineBush.pdf """ From 79a88f81f59e15f47617bf91384fc51c8193af72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Thu, 25 Jul 2013 22:36:06 +0200 Subject: [PATCH 347/736] Wrap line --- skimage/filter/_denoise_cy.pyx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/skimage/filter/_denoise_cy.pyx b/skimage/filter/_denoise_cy.pyx index 0089e7b4..b4d9b1d3 100644 --- a/skimage/filter/_denoise_cy.pyx +++ b/skimage/filter/_denoise_cy.pyx @@ -200,7 +200,8 @@ def denoise_bilateral(image, Py_ssize_t win_size=5, sigma_range=None, return np.squeeze(out) -def denoise_tv_bregman(image, double weight, int max_iter=100, double eps=1e-3, isotropic=True): +def denoise_tv_bregman(image, double weight, int max_iter=100, double eps=1e-3, + isotropic=True): """Perform total-variation denoising using split-Bregman optimization. Total-variation denoising (also know as total-variation regularization) From 012f8b4ea112b6d08061044118995e5baaf642b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Thu, 25 Jul 2013 22:36:41 +0200 Subject: [PATCH 348/736] Add missing space in parameter defintion in doc string --- skimage/filter/_denoise_cy.pyx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skimage/filter/_denoise_cy.pyx b/skimage/filter/_denoise_cy.pyx index b4d9b1d3..3459171c 100644 --- a/skimage/filter/_denoise_cy.pyx +++ b/skimage/filter/_denoise_cy.pyx @@ -223,9 +223,9 @@ def denoise_tv_bregman(image, double weight, int max_iter=100, double eps=1e-3, SUM((u(n) - u(n-1))**2) < eps - max_iter: int, optional + max_iter : int, optional Maximal number of iterations used for the optimization. - isotropic: boolean, optional + isotropic : boolean, optional Switch between isotropic and anisotropic TV denoising. Returns From e848ba1e6159565539fa910b4c86445fe63872ba Mon Sep 17 00:00:00 2001 From: Matt Terry Date: Thu, 25 Jul 2013 15:17:11 -0700 Subject: [PATCH 349/736] copy editing docs --- skimage/color/colorconv.py | 2 ++ skimage/color/delta_e.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/skimage/color/colorconv.py b/skimage/color/colorconv.py index 726240bd..f4051c99 100644 --- a/skimage/color/colorconv.py +++ b/skimage/color/colorconv.py @@ -1037,6 +1037,7 @@ def combine_stains(stains, conv_matrix): def lab2lch(lab): """CIE-LAB to CIE-LCH color space conversion. + LCH is the cylindrical representation of the LAB (cartesian) colorspace Parameters @@ -1081,6 +1082,7 @@ def lab2lch(lab): def lch2lab(lch): """CIE-LCH to CIE-LAB color space conversion. + LCH is the cylindrical representation of the LAB (cartesian) colorspace Parameters diff --git a/skimage/color/delta_e.py b/skimage/color/delta_e.py index f6ff87a5..8029116b 100644 --- a/skimage/color/delta_e.py +++ b/skimage/color/delta_e.py @@ -36,7 +36,7 @@ def _arctan2pi(b, a): def deltaE_cie76(lab1, lab2): - """Euclidian distance between two points in in Lab color space + """Euclidian distance between two points in Lab color space Parameters ---------- From 547fa2fc6cde995fb680a77900c28e7efee6e7b8 Mon Sep 17 00:00:00 2001 From: Matt Terry Date: Thu, 25 Jul 2013 15:17:30 -0700 Subject: [PATCH 350/736] test for rgb2lch roundtrip --- skimage/color/tests/test_colorconv.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/skimage/color/tests/test_colorconv.py b/skimage/color/tests/test_colorconv.py index a184f807..f2e99095 100644 --- a/skimage/color/tests/test_colorconv.py +++ b/skimage/color/tests/test_colorconv.py @@ -256,6 +256,14 @@ class TestColorconv(TestCase): lab2 = lch2lab(lab2lch(lab)) assert_array_almost_equal(lab2, lab) + def test_rgb_lch_roundtrip(self): + rgb = img_as_float(self.img_rgb) + lab = rgb2lab(rgb) + lch = lab2lch(lab) + lab2 = lch2lab(lch) + rgb2 = lab2rgb(lab2) + assert_array_almost_equal(rgb, rgb2) + def test_gray2rgb(): x = np.array([0, 0.5, 1]) From 9db3ab19279f9068d7619a06c587d595fc15a06d Mon Sep 17 00:00:00 2001 From: Matt Terry Date: Thu, 25 Jul 2013 15:18:47 -0700 Subject: [PATCH 351/736] deg2rad > DEG --- skimage/color/delta_e.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/skimage/color/delta_e.py b/skimage/color/delta_e.py index 8029116b..cf77479e 100644 --- a/skimage/color/delta_e.py +++ b/skimage/color/delta_e.py @@ -25,8 +25,6 @@ from __future__ import division import numpy as np -DEG = np.pi / 180 - def _arctan2pi(b, a): """np.arctan2 mapped to (0, 2 * pi)""" @@ -114,8 +112,8 @@ def deltaE_ciede94(lab1, lab2, kH=1, kC=1, kL=1, k1=0.045, k2=0.015): l2, a2, b2 = np.rollaxis(lab2, -1)[:3] dl = l1 - l2 - c1 = np.sqrt(a1 ** 2 + b1 ** 2) - c2 = np.sqrt(a2 ** 2 + b2 ** 2) + c1 = np.hypot(a1, b1) + c2 = np.hypot(a2, b2) dc = c1 - c2 dh_ab = np.sqrt(deltaE_cie76(lab1, lab2) ** 2 - dl ** 2 - dc ** 2) @@ -171,8 +169,8 @@ def deltaE_ciede2000(lab1, lab2, kL=1, kC=1, kH=1): L1, a1, b1 = np.rollaxis(lab1, -1)[:3] L2, a2, b2 = np.rollaxis(lab2, -1)[:3] - c1 = np.sqrt(a1 ** 2 + b1 ** 2) - c2 = np.sqrt(a2 ** 2 + b2 ** 2) + c1 = np.hypot(a1, b1) + c2 = np.hypot(a2, b2) cbar = 0.5 * (c1 + c2) c7 = cbar ** 7 G = 0.5 * (1 - np.sqrt(c7 / (c7 + 25 ** 7))) @@ -183,8 +181,8 @@ def deltaE_ciede2000(lab1, lab2, kL=1, kC=1, kH=1): a1_prime = a1 * (1 + G) a2_prime = a2 * (1 + G) - c1_prime = np.sqrt(a1_prime ** 2 + b1 ** 2) - c2_prime = np.sqrt(a2_prime ** 2 + b2 ** 2) + c1_prime = np.hypot(a1_prime, b1) + c2_prime = np.hypot(a2_prime, b2) cbar_prime = 0.5 * (c1_prime + c2_prime) dC_prime = c2_prime - c1_prime @@ -214,12 +212,14 @@ def deltaE_ciede2000(lab1, lab2, kL=1, kC=1, kH=1): Hbar_prime *= 0.5 T = (1 - - 0.17 * np.cos(Hbar_prime - 30 * DEG) + + 0.17 * np.cos(Hbar_prime - np.deg2rad(30)) + 0.24 * np.cos(2 * Hbar_prime) + - 0.32 * np.cos(3 * Hbar_prime + 6 * DEG) - - 0.20 * np.cos(4 * Hbar_prime - 63 * DEG) + 0.32 * np.cos(3 * Hbar_prime + np.deg2rad(6)) - + 0.20 * np.cos(4 * Hbar_prime - np.deg2rad(63)) ) - dTheta = 30 * DEG * np.exp(-((Hbar_prime / DEG - 275) / 25) ** 2) + dTheta = (np.deg2rad(30) * + np.exp(-((np.rad2deg(Hbar_prime) - 275) / 25) ** 2) + ) c7 = cbar_prime ** 7 Rc = 2 * np.sqrt(c7 / (c7 + 25 ** 7)) @@ -283,8 +283,8 @@ def deltaE_cmc(lab1, lab2, kL=1, kC=1): l1, a1, b1 = np.rollaxis(lab1, -1)[:3] l2, a2, b2 = np.rollaxis(lab2, -1)[:3] - c1 = np.sqrt(a1 ** 2 + b1 ** 2) - c2 = np.sqrt(a2 ** 2 + b2 ** 2) + c1 = np.hypot(a1, b1) + c2 = np.hypot(a2, b2) dC = c1 - c2 dl = l1 - l2 dH = np.sqrt(deltaE_cie76(lab1, lab2) ** 2 - dl ** 2 - dC ** 2) @@ -292,9 +292,9 @@ def deltaE_cmc(lab1, lab2, kL=1, kC=1): dL = l1 - l2 h1 = _arctan2pi(b1, a1) - T = np.where(np.logical_and(h1 >= 164 * DEG, h1 <= 345 * DEG), - 0.56 + 0.2 * np.abs(np.cos(h1 + 168 * DEG)), - 0.36 + 0.4 * np.abs(np.cos(h1 + 35 * DEG)) + T = np.where(np.logical_and(np.rad2deg(h1) >= 164, np.rad2deg(h1) <= 345), + 0.56 + 0.2 * np.abs(np.cos(h1 + np.deg2rad(168))), + 0.36 + 0.4 * np.abs(np.cos(h1 + np.deg2rad(35))) ) c1_4 = c1 ** 4 F = np.sqrt(c1_4 / (c1_4 + 1900)) From 42a2671a02c4bf18128ab430b4a302171ce39692 Mon Sep 17 00:00:00 2001 From: Matt Terry Date: Thu, 25 Jul 2013 15:22:43 -0700 Subject: [PATCH 352/736] skimage attribution convention --- CONTRIBUTORS.txt | 3 +++ skimage/color/delta_e.py | 4 ---- skimage/color/tests/test_delta_e.py | 9 +-------- 3 files changed, 4 insertions(+), 12 deletions(-) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index ac11457c..b304646b 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -145,3 +145,6 @@ - Jostein Bø Fløystad Reconstruction circle mode for Radon transform Simultaneous Algebraic Reconstruction Technique for inverse Radon transform + +- Matt Terry + Color difference functions diff --git a/skimage/color/delta_e.py b/skimage/color/delta_e.py index cf77479e..50f6e16c 100644 --- a/skimage/color/delta_e.py +++ b/skimage/color/delta_e.py @@ -12,10 +12,6 @@ differentiation, but more recent studies (Mahy 1994) peg JND at 2.3 The delta-E notation comes from the German word for "Sensation" (Empfindung). -:author: Matt Terry - -:license: modified BSD - Reference --------- http://en.wikipedia.org/wiki/Color_difference diff --git a/skimage/color/tests/test_delta_e.py b/skimage/color/tests/test_delta_e.py index 230e6e83..1bfcc4d0 100644 --- a/skimage/color/tests/test_delta_e.py +++ b/skimage/color/tests/test_delta_e.py @@ -1,11 +1,4 @@ -"""Test for correctness of color distance functions - -Authors -------- -Matt Terry - -:license: modified BSD -""" +"""Test for correctness of color distance functions""" from os.path import abspath, dirname, join as pjoin import numpy as np From c36d1cf248f5729445e458450b157cdfb501ac2e Mon Sep 17 00:00:00 2001 From: Matt Terry Date: Thu, 25 Jul 2013 15:28:29 -0700 Subject: [PATCH 353/736] run_module_suite --- skimage/color/tests/test_delta_e.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/skimage/color/tests/test_delta_e.py b/skimage/color/tests/test_delta_e.py index 1bfcc4d0..4535a311 100644 --- a/skimage/color/tests/test_delta_e.py +++ b/skimage/color/tests/test_delta_e.py @@ -136,3 +136,8 @@ def test_cmc(): ]) assert_allclose(dE2, oracle, rtol=1.e-8) + + +if __name__ == "__main__": + from numpy.testing import run_module_suite + run_module_suite() From a0f8905b8c3e6a5305eccf0b96b257c396cdad10 Mon Sep 17 00:00:00 2001 From: Matt Terry Date: Thu, 25 Jul 2013 15:32:52 -0700 Subject: [PATCH 354/736] docs for deltaE_ciede2000 length scales --- skimage/color/delta_e.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/skimage/color/delta_e.py b/skimage/color/delta_e.py index 50f6e16c..ee303e16 100644 --- a/skimage/color/delta_e.py +++ b/skimage/color/delta_e.py @@ -137,11 +137,12 @@ def deltaE_ciede2000(lab1, lab2, kL=1, kC=1, kH=1): lab2 : array_like comparision color (Lab colorspace) kL : float (range), optional - pass + luminance scale factor, 1 for "acceptably close"; 2 for "impercievable" + see deltaE_cmc kC : float (range), optional - pass + chroma scale factor, usually 1 kH : float (range), optional - pass + hue scale factor, usually 1 Returns ------- From 0b0cb6a1693556ac364409d099dd0294d57f6119 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Sun, 28 Jul 2013 17:21:25 +0530 Subject: [PATCH 355/736] Adding Octagon structural element --- skimage/morphology/__init__.py | 4 +- skimage/morphology/selem.py | 105 +++++++++++++++++++++++---------- 2 files changed, 78 insertions(+), 31 deletions(-) diff --git a/skimage/morphology/__init__.py b/skimage/morphology/__init__.py index c8fafe6f..38357f02 100644 --- a/skimage/morphology/__init__.py +++ b/skimage/morphology/__init__.py @@ -4,7 +4,8 @@ from .grey import (erosion, dilation, opening, closing, white_tophat, black_tophat, greyscale_erode, greyscale_dilate, greyscale_open, greyscale_close, greyscale_white_top_hat, greyscale_black_top_hat) -from .selem import square, rectangle, diamond, disk, cube, octahedron, ball +from .selem import (square, rectangle, diamond, disk, cube, octahedron, ball, + octagon) from .ccomp import label from .watershed import watershed, is_local_maximum from ._skeletonize import skeletonize, medial_axis @@ -36,6 +37,7 @@ __all__ = ['binary_erosion', 'cube', 'octahedron', 'ball', + 'octagon', 'label', 'watershed', 'is_local_maximum', diff --git a/skimage/morphology/selem.py b/skimage/morphology/selem.py index 2cccae22..f2cba44b 100644 --- a/skimage/morphology/selem.py +++ b/skimage/morphology/selem.py @@ -4,6 +4,7 @@ """ import numpy as np +from . import convex_hull_image def square(width, dtype=np.uint8): @@ -15,18 +16,18 @@ def square(width, dtype=np.uint8): Parameters ---------- width : int - The width and height of the square + The width and height of the square Other Parameters ---------------- dtype : data-type - The data type of the structuring element. + The data type of the structuring element. Returns ------- selem : ndarray - A structuring element consisting only of ones, i.e. every - pixel belongs to the neighborhood. + A structuring element consisting only of ones, i.e. every + pixel belongs to the neighborhood. """ return np.ones((width, width), dtype=dtype) @@ -41,21 +42,20 @@ def rectangle(width, height, dtype=np.uint8): Parameters ---------- width : int - The width of the rectangle - + The width of the rectangle height : int - The height of the rectangle + The height of the rectangle Other Parameters ---------------- dtype : data-type - The data type of the structuring element. + The data type of the structuring element. Returns ------- selem : ndarray - A structuring element consisting only of ones, i.e. every - pixel belongs to the neighborhood. + A structuring element consisting only of ones, i.e. every + pixel belongs to the neighborhood. """ return np.ones((width, height), dtype=dtype) @@ -71,17 +71,19 @@ def diamond(radius, dtype=np.uint8): Parameters ---------- radius : int - The radius of the diamond-shaped structuring element. + The radius of the diamond-shaped structuring element. + Other Parameters + ---------------- dtype : data-type - The data type of the structuring element. + The data type of the structuring element. Returns ------- selem : ndarray - The structuring element where elements of the neighborhood - are 1 and 0 otherwise. + The structuring element where elements of the neighborhood + are 1 and 0 otherwise. """ half = radius (I, J) = np.meshgrid(range(0, radius * 2 + 1), range(0, radius * 2 + 1)) @@ -98,16 +100,18 @@ def disk(radius, dtype=np.uint8): Parameters ---------- radius : int - The radius of the disk-shaped structuring element. + The radius of the disk-shaped structuring element. + Other Parameters + ---------------- dtype : data-type - The data type of the structuring element. + The data type of the structuring element. Returns ------- selem : ndarray - The structuring element where elements of the neighborhood - are 1 and 0 otherwise. + The structuring element where elements of the neighborhood + are 1 and 0 otherwise. """ L = np.linspace(-radius, radius, 2 * radius + 1) (X, Y) = np.meshgrid(L, L) @@ -125,18 +129,18 @@ def cube(width, dtype=np.uint8): Parameters ---------- width : int - The width, height and depth of the cube + The width, height and depth of the cube Other Parameters ---------------- dtype : data-type - The data type of the structuring element. + The data type of the structuring element. Returns ------- selem : ndarray - A structuring element consisting only of ones, i.e. every - pixel belongs to the neighborhood. + A structuring element consisting only of ones, i.e. every + pixel belongs to the neighborhood. """ return np.ones((width, width, width), dtype=dtype) @@ -153,17 +157,19 @@ def octahedron(radius, dtype=np.uint8): Parameters ---------- radius : int - The radius of the octahedron-shaped structuring element. + The radius of the octahedron-shaped structuring element. + Other Parameters + ---------------- dtype : data-type - The data type of the structuring element. + The data type of the structuring element. Returns ------- selem : ndarray - The structuring element where elements of the neighborhood - are 1 and 0 otherwise. + The structuring element where elements of the neighborhood + are 1 and 0 otherwise. """ # note that in contrast to diamond(), this method allows non-integer radii n = 2 * radius + 1 @@ -184,16 +190,18 @@ def ball(radius, dtype=np.uint8): Parameters ---------- radius : int - The radius of the ball-shaped structuring element. + The radius of the ball-shaped structuring element. + Other Parameters + ---------------- dtype : data-type - The data type of the structuring element. + The data type of the structuring element. Returns ------- selem : ndarray - The structuring element where elements of the neighborhood - are 1 and 0 otherwise. + The structuring element where elements of the neighborhood + are 1 and 0 otherwise. """ n = 2 * radius + 1 Z, Y, X = np.mgrid[ -radius:radius:n*1j, @@ -201,3 +209,40 @@ def ball(radius, dtype=np.uint8): -radius:radius:n*1j] s = X**2 + Y**2 + Z**2 return np.array(s <= radius * radius, dtype=dtype) + + +def octagon(m, n, dtype=np.uint8): + """ + Generates a octagon shaped structuring element with a given size of + horizontal and vertical sides and a given height of slanted sides. + The slanted sides are 45 or 135 degrees to the horizontal axis. + + Parameters + ---------- + m : int + The size of the horizontal and vertical sides. + n : int + The vertical height of the slanted sides. + + Other Parameters + ---------------- + dtype : data-type + The data type of the structuring element. + + Returns + ------- + selem : ndarray + The structuring element where elements of the neighborhood + are 1 and 0 otherwise. + """ + selem = np.zeros((m + 2*n, m + 2*n)) + selem[0, n] = 1 + selem[n, 0] = 1 + selem[0, m + n -1] = 1 + selem[m + n - 1, 0] = 1 + selem[-1, n] = 1 + selem[n, -1] = 1 + selem[-1, m + n - 1] = 1 + selem[m + n - 1, -1] = 1 + selem = convex_hull_image(selem).astype(dtype) + return selem From f4332ce488f16897a4d9db5a84dcbb7bcbdb4b4a Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Sun, 28 Jul 2013 18:41:19 +0530 Subject: [PATCH 356/736] Removing circular import --- skimage/morphology/selem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/morphology/selem.py b/skimage/morphology/selem.py index f2cba44b..8d907d88 100644 --- a/skimage/morphology/selem.py +++ b/skimage/morphology/selem.py @@ -4,7 +4,6 @@ """ import numpy as np -from . import convex_hull_image def square(width, dtype=np.uint8): @@ -235,6 +234,7 @@ def octagon(m, n, dtype=np.uint8): The structuring element where elements of the neighborhood are 1 and 0 otherwise. """ + from . import convex_hull_image selem = np.zeros((m + 2*n, m + 2*n)) selem[0, n] = 1 selem[n, 0] = 1 From 878c562a4528b1515c62a1e58800dff2a411757d Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Mon, 29 Jul 2013 00:28:38 +1000 Subject: [PATCH 357/736] Replace 'ratio' kwarg with 'compactness' in SLIC --- skimage/segmentation/slic_superpixels.py | 19 +++++++++++++------ skimage/segmentation/tests/test_slic.py | 5 +++-- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/skimage/segmentation/slic_superpixels.py b/skimage/segmentation/slic_superpixels.py index cef668ef..b905f199 100644 --- a/skimage/segmentation/slic_superpixels.py +++ b/skimage/segmentation/slic_superpixels.py @@ -10,8 +10,8 @@ from ..color import rgb2lab, gray2rgb, guess_spatial_dimensions from ._slic import _slic_cython -def slic(image, n_segments=100, ratio=10., max_iter=10, sigma=1, - multichannel=None, convert2lab=True): +def slic(image, n_segments=100, compactness=10., max_iter=10, sigma=1, + multichannel=None, convert2lab=True, ratio=None): """Segments image using k-means clustering in Color-(x,y) space. Parameters @@ -21,9 +21,10 @@ def slic(image, n_segments=100, ratio=10., max_iter=10, sigma=1, (see `multichannel` parameter). n_segments : int, optional (default: 100) The (approximate) number of labels in the segmented output image. - ratio: float, optional (default: 10) - Balances color-space proximity and image-space proximity. - Higher values give more weight to color-space. + compactness: float, optional (default: 10) + Balances color-space proximity and image-space proximity. Higher + values give more weight to image-space. As `compactness` tends to + infinity, superpixel shapes become square/cubic. max_iter : int, optional (default: 10) Maximum number of iterations of k-means. sigma : float, optional (default: 1) @@ -38,6 +39,8 @@ def slic(image, n_segments=100, ratio=10., max_iter=10, sigma=1, Whether the input should be converted to Lab colorspace prior to segmentation. For this purpose, the input is assumed to be RGB. Highly recommended. + ratio : float, optional + Synonym for `compactness`. This keyword is deprecated. Returns ------- @@ -79,6 +82,10 @@ def slic(image, n_segments=100, ratio=10., max_iter=10, sigma=1, >>> # Increasing the ratio parameter yields more square regions >>> segments = slic(img, n_segments=100, ratio=20) """ + if ratio is not None: + msg = 'Keyword `ratio` is deprecated. Use `compactness` instead.' + warnings.warn(msg) + compactness = ratio spatial_dims = guess_spatial_dimensions(image) if spatial_dims is None and multichannel is None: msg = ("Images with dimensions (M, N, 3) are interpreted as 2D+RGB" + @@ -122,7 +129,7 @@ def slic(image, n_segments=100, ratio=10., max_iter=10, sigma=1, means = np.ascontiguousarray(means) # we do the scaling of ratio in the same way as in the SLIC paper # so the values have the same meaning - ratio = float(max((step_z, step_y, step_x))) / ratio + ratio = float(max((step_z, step_y, step_x))) / compactness image_zyx = np.concatenate([grid_z[..., np.newaxis], grid_y[..., np.newaxis], grid_x[..., np.newaxis], diff --git a/skimage/segmentation/tests/test_slic.py b/skimage/segmentation/tests/test_slic.py index d0539cd2..8da5bcb9 100644 --- a/skimage/segmentation/tests/test_slic.py +++ b/skimage/segmentation/tests/test_slic.py @@ -16,7 +16,7 @@ def test_color_2d(): img[img < 0] = 0 with warnings.catch_warnings(): warnings.simplefilter("ignore") - seg = slic(img, sigma=0, n_segments=4) + seg = slic(img, n_segments=4, sigma=0) # we expect 4 segments assert_equal(len(np.unique(seg)), 4) @@ -35,7 +35,8 @@ def test_gray_2d(): img += 0.0033 * rnd.normal(size=img.shape) img[img > 1] = 1 img[img < 0] = 0 - seg = slic(img, sigma=0, n_segments=4, ratio=20.0, multichannel=False) + seg = slic(img, sigma=0, n_segments=4, compactness=20.0, + multichannel=False) assert_equal(len(np.unique(seg)), 4) assert_array_equal(seg[:10, :10], 0) From 72833f1729d451bbfb911ca17fe08c489f6ebe6f Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Mon, 29 Jul 2013 00:30:51 +1000 Subject: [PATCH 358/736] Do not use 'ratio' in test_slic.py either --- skimage/segmentation/tests/test_slic.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/skimage/segmentation/tests/test_slic.py b/skimage/segmentation/tests/test_slic.py index 8da5bcb9..9e6a39e2 100644 --- a/skimage/segmentation/tests/test_slic.py +++ b/skimage/segmentation/tests/test_slic.py @@ -80,7 +80,8 @@ def test_gray_3d(): img += 0.001 * rnd.normal(size=img.shape) img[img > 1] = 1 img[img < 0] = 0 - seg = slic(img, sigma=0, n_segments=8, ratio=20.0, multichannel=False) + seg = slic(img, sigma=0, n_segments=8, compactness=20.0, + multichannel=False) assert_equal(len(np.unique(seg)), 8) for s, c in zip(slices, range(8)): From 8a062617d0669cc421b1ec272c4873e6d476a81c Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Tue, 30 Jul 2013 03:22:45 +0530 Subject: [PATCH 359/736] Correcting grammatical typo --- skimage/morphology/selem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/morphology/selem.py b/skimage/morphology/selem.py index 8d907d88..d403a27a 100644 --- a/skimage/morphology/selem.py +++ b/skimage/morphology/selem.py @@ -212,7 +212,7 @@ def ball(radius, dtype=np.uint8): def octagon(m, n, dtype=np.uint8): """ - Generates a octagon shaped structuring element with a given size of + Generates an octagon shaped structuring element with a given size of horizontal and vertical sides and a given height of slanted sides. The slanted sides are 45 or 135 degrees to the horizontal axis. From 93819c74bbe5698b43ac633fe363afeb0b3f06b3 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Tue, 30 Jul 2013 03:28:58 +0530 Subject: [PATCH 360/736] Making the doc more explicit --- skimage/morphology/selem.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/skimage/morphology/selem.py b/skimage/morphology/selem.py index d403a27a..2c41e393 100644 --- a/skimage/morphology/selem.py +++ b/skimage/morphology/selem.py @@ -213,15 +213,16 @@ def ball(radius, dtype=np.uint8): def octagon(m, n, dtype=np.uint8): """ Generates an octagon shaped structuring element with a given size of - horizontal and vertical sides and a given height of slanted sides. - The slanted sides are 45 or 135 degrees to the horizontal axis. + horizontal and vertical sides and a given height or width of slanted + sides. The slanted sides are 45 or 135 degrees to the horizontal axis + and hence the widths and heights are equal. Parameters ---------- m : int The size of the horizontal and vertical sides. n : int - The vertical height of the slanted sides. + The height or width of the slanted sides. Other Parameters ---------------- From 364ebd7418c035f355873a077d15d36ec08d6ff4 Mon Sep 17 00:00:00 2001 From: Matt Terry Date: Tue, 30 Jul 2013 10:50:43 -0700 Subject: [PATCH 361/736] copy editing --- skimage/color/colorconv.py | 6 +++--- skimage/color/delta_e.py | 30 +++++++++++++++--------------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/skimage/color/colorconv.py b/skimage/color/colorconv.py index f4051c99..a09e8bdc 100644 --- a/skimage/color/colorconv.py +++ b/skimage/color/colorconv.py @@ -27,7 +27,7 @@ Supported color spaces ``x == y == z == 1/3`` at the whitepoint, and all color matching functions are greater than zero everywhere. * LAB CIE : Lightness, a, b - Colorspace derived from XYZ CIE that is intented to be more + Colorspace derived from XYZ CIE that is intended to be more perceptually uniform * LCH CIE : Lightness, Chroma, Hue Defined in terms of LAB CIE. C and H are the polar representation of @@ -1038,7 +1038,7 @@ def combine_stains(stains, conv_matrix): def lab2lch(lab): """CIE-LAB to CIE-LCH color space conversion. - LCH is the cylindrical representation of the LAB (cartesian) colorspace + LCH is the cylindrical representation of the LAB (Cartesian) colorspace Parameters ---------- @@ -1083,7 +1083,7 @@ def lab2lch(lab): def lch2lab(lch): """CIE-LCH to CIE-LAB color space conversion. - LCH is the cylindrical representation of the LAB (cartesian) colorspace + LCH is the cylindrical representation of the LAB (Cartesian) colorspace Parameters ---------- diff --git a/skimage/color/delta_e.py b/skimage/color/delta_e.py index ee303e16..f7cc637c 100644 --- a/skimage/color/delta_e.py +++ b/skimage/color/delta_e.py @@ -1,10 +1,10 @@ """ Functions for calculating the "distance" between colors. -Implicit in these definitions of "distance" is the notion of "Just Noticible +Implicit in these definitions of "distance" is the notion of "Just Noticeable Distance" (JND). This represents the distance between colors where a human can -percieve different colors. Humans are more sensitive to certain colors than -others, which different deltaE metrics correct for this with varying degrees of +perceive different colors. Humans are more sensitive to certain colors than +others, which different deltaE metrics correct for with varying degrees of sophistication. The literature often mentions 1 as the minimum distance for visual @@ -30,14 +30,14 @@ def _arctan2pi(b, a): def deltaE_cie76(lab1, lab2): - """Euclidian distance between two points in Lab color space + """Euclidean distance between two points in Lab color space Parameters ---------- lab1 : array_like reference color (Lab colorspace) lab2 : array_like - comparision color (Lab colorspace) + comparison color (Lab colorspace) Returns ------- @@ -58,7 +58,7 @@ def deltaE_cie76(lab1, lab2): def deltaE_ciede94(lab1, lab2, kH=1, kC=1, kL=1, k1=0.045, k2=0.015): """Color difference according to CIEDE 94 standard - Accomodates perceptual non-uniformites through the use of application + Accommodates perceptual non-uniformities through the use of application specific scale factors (kH, kC, kL, k1, and k2). Parameters @@ -66,7 +66,7 @@ def deltaE_ciede94(lab1, lab2, kH=1, kC=1, kL=1, k1=0.045, k2=0.015): lab1 : array_like reference color (Lab colorspace) lab2 : array_like - comparision color (Lab colorspace) + comparison color (Lab colorspace) kH : float, optional Hue scale kC : float, optional @@ -127,7 +127,7 @@ def deltaE_ciede94(lab1, lab2, kH=1, kC=1, kL=1, k1=0.045, k2=0.015): def deltaE_ciede2000(lab1, lab2, kL=1, kC=1, kH=1): """Color difference as given by the CIEDE 2000 standard. - CIEDE 2000 is a major revision of CIDE94. The perceptual calibaration is + CIEDE 2000 is a major revision of CIDE94. The perceptual calibration is largely based on experience with automotive paint on smooth surfaces. Parameters @@ -135,9 +135,9 @@ def deltaE_ciede2000(lab1, lab2, kL=1, kC=1, kH=1): lab1 : array_like reference color (Lab colorspace) lab2 : array_like - comparision color (Lab colorspace) + comparison color (Lab colorspace) kL : float (range), optional - luminance scale factor, 1 for "acceptably close"; 2 for "impercievable" + luminance scale factor, 1 for "acceptably close"; 2 for "imperceivable" see deltaE_cmc kC : float (range), optional chroma scale factor, usually 1 @@ -242,13 +242,13 @@ def deltaE_ciede2000(lab1, lab2, kL=1, kC=1, kH=1): def deltaE_cmc(lab1, lab2, kL=1, kC=1): """Color difference from the CMC l:c standard. - This color difference developed by the Colour Measurement Committee of the - Socieity of Dyes and Colourists of Great Britian (CMC). It is intended for - use in the textile industry. + This color difference was developed by the Colour Measurement Committee + (CMC) of the Society of Dyers and Colourists (United Kingdom).0 It is + intended for use in the textile industry. The scale factors kL, kC set the weight given to differences in lightness and chroma relative to differences in hue. The usual values are kL=2, kC=1 - for "acceptability" and kL=1, kC=1 for "imperceptability". Colors with + for "acceptability" and kL=1, kC=1 for "imperceptibility". Colors with dE > 1 are "different" for the given scale factors. Parameters @@ -256,7 +256,7 @@ def deltaE_cmc(lab1, lab2, kL=1, kC=1): lab1 : array_like reference color (Lab colorspace) lab2 : array_like - comparision color (Lab colorspace) + comparison color (Lab colorspace) Returns ------- From 6585efb7634111911bebf55158e6f6f2b39b4c3e Mon Sep 17 00:00:00 2001 From: Matt Terry Date: Tue, 30 Jul 2013 14:20:55 -0700 Subject: [PATCH 362/736] lab2lch/lch2lab for N-D images (w/tests) --- skimage/color/colorconv.py | 25 ++++++++++++++++++++----- skimage/color/tests/test_colorconv.py | 22 ++++++++++++++++++++++ 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/skimage/color/colorconv.py b/skimage/color/colorconv.py index a09e8bdc..75f4eba2 100644 --- a/skimage/color/colorconv.py +++ b/skimage/color/colorconv.py @@ -1043,7 +1043,9 @@ def lab2lch(lab): Parameters ---------- lab : array_like - The image in CIE-LAB format, in a 3-D array of shape (.., .., 3). + The N-D image in CIE-LAB format. The last (`N+1`th) dimension must have + at least 3 elements, corresponding to the `L`, `a`, and `b` color + channels. Subsequent elements are copied. Returns ------- @@ -1070,13 +1072,13 @@ def lab2lch(lab): >>> lena_lab = rgb2lab(lena) >>> lena_lch = lab2lch(lena_lab) """ - lch = _prepare_colorarray(lab).copy() + lch = _prepare_lab_array(lab) a, b = lch[..., 1], lch[..., 2] lch[..., 1], lch[..., 2] = np.sqrt(a ** 2 + b ** 2), np.arctan2(b, a) H = lch[..., 2] - H[H < 0] += 2 * np.pi # (-pi, pi) -> (0, 2*pi) + H += np.where(H < 0, 2*np.pi, 0) # (-pi, pi) -> (0, 2*pi) return lch @@ -1088,7 +1090,9 @@ def lch2lab(lch): Parameters ---------- lab : array_like - The image in CIE-LCH format, in a 3-D array of shape (.., .., 3). + The N-D image in CIE-LCH format. The last (`N+1`th) dimension must have + at least 3 elements, corresponding to the `L`, `a`, and `b` color + channels. Subsequent elements are copied. Returns ------- @@ -1112,8 +1116,19 @@ def lch2lab(lch): >>> lena_lch = lab2lch(lena_lab) >>> lena_lab2 = lch2lab(lena_lch) """ - lch = _prepare_colorarray(lch).copy() + lch = _prepare_lab_array(lch) c, h = lch[..., 1], lch[..., 2] lch[..., 1], lch[..., 2] = c * np.cos(h), c * np.sin(h) return lch + + +def _prepare_lab_array(arr): + """Ensure input for lab2lch, lch2lab are well-posed. + + Arrays must be in floating point and have at least 3 elements in + last dimension. Return a new array. + """ + shape = arr.shape + assert shape[-1] >= 3 + return dtype.img_as_float(arr, force_copy=True) diff --git a/skimage/color/tests/test_colorconv.py b/skimage/color/tests/test_colorconv.py index f2e99095..fbec9ba6 100644 --- a/skimage/color/tests/test_colorconv.py +++ b/skimage/color/tests/test_colorconv.py @@ -264,6 +264,28 @@ class TestColorconv(TestCase): rgb2 = lab2rgb(lab2) assert_array_almost_equal(rgb, rgb2) + def test_lab_lch_0d(self): + lab0 = self._get_lab0() + lch0 = lab2lch(lab0) + lch2 = lab2lch(lab0[None, None, :]) + assert_array_almost_equal(lch0, lch2[0, 0, :]) + + def test_lab_lch_1d(self): + lab0 = self._get_lab0() + lch0 = lab2lch(lab0) + lch1 = lab2lch(lab0[None, :]) + assert_array_almost_equal(lch0, lch1[0, :]) + + def test_lab_lch_3d(self): + lab0 = self._get_lab0() + lch0 = lab2lch(lab0) + lch3 = lab2lch(lab0[None, None, None, :]) + assert_array_almost_equal(lch0, lch3[0, 0, 0, :]) + + def _get_lab0(self): + rgb = img_as_float(self.img_rgb[:1, :1, :]) + return rgb2lab(rgb)[0, 0, :] + def test_gray2rgb(): x = np.array([0, 0.5, 1]) From a8403d26a18531e44d3a83a4bc1e4ba9d37e7274 Mon Sep 17 00:00:00 2001 From: Matt Terry Date: Wed, 31 Jul 2013 06:40:24 -0700 Subject: [PATCH 363/736] last round of copy-edits --- skimage/color/delta_e.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skimage/color/delta_e.py b/skimage/color/delta_e.py index f7cc637c..2c9d2b19 100644 --- a/skimage/color/delta_e.py +++ b/skimage/color/delta_e.py @@ -137,7 +137,7 @@ def deltaE_ciede2000(lab1, lab2, kL=1, kC=1, kH=1): lab2 : array_like comparison color (Lab colorspace) kL : float (range), optional - luminance scale factor, 1 for "acceptably close"; 2 for "imperceivable" + luminance scale factor, 1 for "acceptably close"; 2 for "imperceptible" see deltaE_cmc kC : float (range), optional chroma scale factor, usually 1 @@ -243,7 +243,7 @@ def deltaE_cmc(lab1, lab2, kL=1, kC=1): """Color difference from the CMC l:c standard. This color difference was developed by the Colour Measurement Committee - (CMC) of the Society of Dyers and Colourists (United Kingdom).0 It is + (CMC) of the Society of Dyers and Colourists (United Kingdom). It is intended for use in the textile industry. The scale factors kL, kC set the weight given to differences in lightness From 5c5c67027effce176c2ad4ceb8ee75be461964bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Wed, 31 Jul 2013 19:12:44 +0200 Subject: [PATCH 364/736] Add missing p1 parameter to percentile and threshold functions --- skimage/filter/rank/percentile_cy.pyx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/skimage/filter/rank/percentile_cy.pyx b/skimage/filter/rank/percentile_cy.pyx index 31afd53a..ffa5e1b0 100644 --- a/skimage/filter/rank/percentile_cy.pyx +++ b/skimage/filter/rank/percentile_cy.pyx @@ -268,7 +268,8 @@ def _percentile(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, dtype_t_out[:, ::1] out, - char shift_x, char shift_y, double p0, Py_ssize_t max_bin): + char shift_x, char shift_y, double p0, double p1, + Py_ssize_t max_bin): _core(_kernel_percentile[dtype_t], image, selem, mask, out, shift_x, shift_y, p0, 1, 0, 0, max_bin) @@ -289,7 +290,8 @@ def _threshold(dtype_t[:, ::1] image, char[:, ::1] selem, char[:, ::1] mask, dtype_t_out[:, ::1] out, - char shift_x, char shift_y, double p0, Py_ssize_t max_bin): + char shift_x, char shift_y, double p0, double p1, + Py_ssize_t max_bin): _core(_kernel_threshold[dtype_t], image, selem, mask, out, shift_x, shift_y, p0, 1, 0, 0, max_bin) From 391e9761571ebd486e2ff7784b0a64a964580ebe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Wed, 31 Jul 2013 19:24:18 +0200 Subject: [PATCH 365/736] Add specific branch for the percentile p0 = 0 case --- skimage/filter/rank/percentile_cy.pyx | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/skimage/filter/rank/percentile_cy.pyx b/skimage/filter/rank/percentile_cy.pyx index ffa5e1b0..8fad59c5 100644 --- a/skimage/filter/rank/percentile_cy.pyx +++ b/skimage/filter/rank/percentile_cy.pyx @@ -161,11 +161,15 @@ cdef inline double _kernel_percentile(Py_ssize_t* histo, double pop, dtype_t g, cdef Py_ssize_t sum = 0 if pop: - for i in range(max_bin): - sum += histo[i] - if sum >= p0 * pop: - break - + if p0 == 0: # make sure p0 == 0 returns the minimum filter + for i in range(max_bin): + if histo[i]: + break + else: + for i in range(max_bin): + sum += histo[i] + if sum >= p0 * pop: + break return i else: return 0 From 9f68e7141262dc1fcd54911642d08705773c9b89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Wed, 31 Jul 2013 19:27:52 +0200 Subject: [PATCH 366/736] Fix accidential variable naming confusion --- skimage/filter/rank/generic_cy.pyx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/filter/rank/generic_cy.pyx b/skimage/filter/rank/generic_cy.pyx index a773f62c..44c51d55 100644 --- a/skimage/filter/rank/generic_cy.pyx +++ b/skimage/filter/rank/generic_cy.pyx @@ -318,7 +318,7 @@ cdef inline double _kernel_otsu(Py_ssize_t* histo, double pop, dtype_t g, # maximizing the between class variance max_i = 0 q1 = histo[0] / pop - m1 = 0. + mu1 = 0. max_sigma_b = 0. for i in range(1, max_bin): From 7be96b4a013c3a8920c5a44e4be5e505aa4869f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Wed, 31 Jul 2013 19:45:55 +0200 Subject: [PATCH 367/736] Integrate new test cases from @odebeir and fix old function names of dilate and erode --- skimage/filter/rank/tests/test_rank.py | 49 ++++++++++++++++++++++++-- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/skimage/filter/rank/tests/test_rank.py b/skimage/filter/rank/tests/test_rank.py index c190fb57..1d573b81 100644 --- a/skimage/filter/rank/tests/test_rank.py +++ b/skimage/filter/rank/tests/test_rank.py @@ -51,7 +51,7 @@ def test_compare_with_cmorph_dilate(): for r in range(1, 20, 1): elem = np.ones((r, r), dtype=np.uint8) rank.maximum(image=image, selem=elem, out=out, mask=mask) - cm = cmorph.dilate(image=image, selem=elem) + cm = cmorph._dilate(image=image, selem=elem) assert_array_equal(out, cm) @@ -65,7 +65,7 @@ def test_compare_with_cmorph_erode(): for r in range(1, 20, 1): elem = np.ones((r, r), dtype=np.uint8) rank.minimum(image=image, selem=elem, out=out, mask=mask) - cm = cmorph.erode(image=image, selem=elem) + cm = cmorph._erode(image=image, selem=elem) assert_array_equal(out, cm) @@ -454,5 +454,50 @@ def test_bilateral(): assert rank.pop_bilateral(image, selem, s0=11, s1=11)[10, 10] == 2 +def test_percentile_min(): + # check that percentile p0 = 0 is identical to local min + img = data.camera() + img16 = img.astype(np.uint16) + selem = disk(15) + # check for 8bit + img_p0 = rank.percentile(img, selem=selem, p0=0) + img_min = rank.minimum(img, selem=selem) + assert_array_equal(img_p0, img_min) + # check for 16bit + img_p0 = rank.percentile(img16, selem=selem, p0=0) + img_min = rank.minimum(img16, selem=selem) + assert_array_equal(img_p0, img_min) + + +def test_percentile_max(): + # check that percentile p0 = 1 is identical to local max + img = data.camera() + img16 = img.astype(np.uint16) + selem = disk(15) + # check for 8bit + img_p0 = rank.percentile(img, selem=selem, p0=1.) + img_max = rank.maximum(img, selem=selem) + assert_array_equal(img_p0, img_max) + # check for 16bit + img_p0 = rank.percentile(img16, selem=selem, p0=1.) + img_max = rank.maximum(img16, selem=selem) + assert_array_equal(img_p0, img_max) + + +def test_percentile_median(): + # check that percentile p0 = 0.5 is identical to local median + img = data.camera() + img16 = img.astype(np.uint16) + selem = disk(15) + # check for 8bit + img_p0 = rank.percentile(img, selem=selem, p0=.5) + img_max = rank.median(img, selem=selem) + assert_array_equal(img_p0, img_max) + # check for 16bit + img_p0 = rank.percentile(img16, selem=selem, p0=.5) + img_max = rank.median(img16, selem=selem) + assert_array_equal(img_p0, img_max) + + if __name__ == "__main__": run_module_suite() From f9f405a8ba9e82f23f620843115035e0214ea1a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Wed, 31 Jul 2013 19:50:05 +0200 Subject: [PATCH 368/736] Fix invalid previous fix for percentile edge case --- skimage/filter/rank/percentile_cy.pyx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/skimage/filter/rank/percentile_cy.pyx b/skimage/filter/rank/percentile_cy.pyx index 8fad59c5..5989cdca 100644 --- a/skimage/filter/rank/percentile_cy.pyx +++ b/skimage/filter/rank/percentile_cy.pyx @@ -161,14 +161,14 @@ cdef inline double _kernel_percentile(Py_ssize_t* histo, double pop, dtype_t g, cdef Py_ssize_t sum = 0 if pop: - if p0 == 0: # make sure p0 == 0 returns the minimum filter - for i in range(max_bin): + if p0 == 1: # make sure p0 = 1 returns the maximum filter + for i in range(max_bin - 1, -1, -1): if histo[i]: break else: for i in range(max_bin): sum += histo[i] - if sum >= p0 * pop: + if sum > p0 * pop: break return i else: From d9196be3c3ba3158e1960541cd059c9861e294fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Thu, 1 Aug 2013 08:45:11 +0200 Subject: [PATCH 369/736] Fix loop ranges of rank filter kernel functions --- skimage/filter/rank/generic_cy.pyx | 4 ++-- skimage/filter/rank/percentile_cy.pyx | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/skimage/filter/rank/generic_cy.pyx b/skimage/filter/rank/generic_cy.pyx index 44c51d55..9815d6ea 100644 --- a/skimage/filter/rank/generic_cy.pyx +++ b/skimage/filter/rank/generic_cy.pyx @@ -29,7 +29,7 @@ cdef inline double _kernel_autolevel(Py_ssize_t* histo, double pop, dtype_t g, if delta > 0: return (max_bin - 1) * (g - imin) / delta else: - return imax - imin + return 0 else: return 0 @@ -321,7 +321,7 @@ cdef inline double _kernel_otsu(Py_ssize_t* histo, double pop, dtype_t g, mu1 = 0. max_sigma_b = 0. - for i in range(1, max_bin): + for i in range(max_bin): P = histo[i] / pop new_q1 = q1 + P if new_q1 > 0: diff --git a/skimage/filter/rank/percentile_cy.pyx b/skimage/filter/rank/percentile_cy.pyx index 5989cdca..e951a76e 100644 --- a/skimage/filter/rank/percentile_cy.pyx +++ b/skimage/filter/rank/percentile_cy.pyx @@ -17,7 +17,7 @@ cdef inline double _kernel_autolevel(Py_ssize_t* histo, double pop, dtype_t g, if pop: sum = 0 p1 = 1.0 - p1 - for i in range(max_bin - 1): + for i in range(max_bin): sum += histo[i] if sum > p0 * pop: imin = i @@ -55,7 +55,7 @@ cdef inline double _kernel_gradient(Py_ssize_t* histo, double pop, dtype_t g, imin = i break sum = 0 - for i in range((max_bin - 1), -1, -1): + for i in range(max_bin - 1, -1, -1): sum += histo[i] if sum >= p1 * pop: imax = i @@ -135,7 +135,7 @@ cdef inline double _kernel_enhance_contrast(Py_ssize_t* histo, double pop, imin = i break sum = 0 - for i in range((max_bin - 1), -1, -1): + for i in range(max_bin - 1, -1, -1): sum += histo[i] if sum > p1 * pop: imax = i From d883b456667b5c6893c28149f33cce7851ab8d07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Thu, 1 Aug 2013 11:12:55 +0200 Subject: [PATCH 370/736] Fix Otsu rank filter kernel loop range --- skimage/filter/rank/generic_cy.pyx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/filter/rank/generic_cy.pyx b/skimage/filter/rank/generic_cy.pyx index 9815d6ea..dcf6e361 100644 --- a/skimage/filter/rank/generic_cy.pyx +++ b/skimage/filter/rank/generic_cy.pyx @@ -321,7 +321,7 @@ cdef inline double _kernel_otsu(Py_ssize_t* histo, double pop, dtype_t g, mu1 = 0. max_sigma_b = 0. - for i in range(max_bin): + for i in range(1, max_bin): P = histo[i] / pop new_q1 = q1 + P if new_q1 > 0: From 97e1174e5ff5726326985f1db82a14caa010a25a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 30 Jun 2013 13:51:45 +0200 Subject: [PATCH 371/736] Let travis also run long example scripts --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index f4f420cb..843e0d6c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -37,4 +37,5 @@ script: # Change back to repository root directory and run all doc examples - cd .. - for f in doc/examples/*.py; do $PYTHON "$f"; if [ $? -ne 0 ]; then exit 1; fi done + - for f in doc/examples/applications/*.py; do $PYTHON "$f"; if [ $? -ne 0 ]; then exit 1; fi done - flake8 --exit-zero --exclude=test_*,six.py skimage doc/examples viewer_examples From 3338c423d3087cc8397e55274ca002cf5d143d1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 30 Jun 2013 13:53:22 +0200 Subject: [PATCH 372/736] Add short comment for travis flake8 --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 843e0d6c..6b2dc4c6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -38,4 +38,5 @@ script: - cd .. - for f in doc/examples/*.py; do $PYTHON "$f"; if [ $? -ne 0 ]; then exit 1; fi done - for f in doc/examples/applications/*.py; do $PYTHON "$f"; if [ $? -ne 0 ]; then exit 1; fi done + # Run pep8 and flake tests - flake8 --exit-zero --exclude=test_*,six.py skimage doc/examples viewer_examples From 37dfd294f2c8f57a57f1dcf9b40acbf6283226bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Thu, 1 Aug 2013 19:17:48 +0200 Subject: [PATCH 373/736] Fix image data type issues in rank filter example --- doc/examples/applications/plot_rank_filters.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/doc/examples/applications/plot_rank_filters.py b/doc/examples/applications/plot_rank_filters.py index fb69cd40..ef333e32 100644 --- a/doc/examples/applications/plot_rank_filters.py +++ b/doc/examples/applications/plot_rank_filters.py @@ -38,9 +38,10 @@ from `skimage.data` for all comparisons. import numpy as np import matplotlib.pyplot as plt +from skimage import img_as_ubyte from skimage import data -noisy_image = data.camera() +noisy_image = img_as_ubyte(data.camera()) hist = np.histogram(noisy_image, bins=np.arange(0, 256)) plt.figure(figsize=(8, 3)) @@ -72,7 +73,7 @@ from skimage.filter.rank import median from skimage.morphology import disk noise = np.random.random(noisy_image.shape) -noisy_image = data.camera() +noisy_image = img_as_ubyte(data.camera()) noisy_image[noise > 0.99] = 255 noisy_image[noise < 0.01] = 0 @@ -150,7 +151,7 @@ the central one. from skimage.filter.rank import bilateral_mean -noisy_image = data.camera() +noisy_image = img_as_ubyte(data.camera()) selem = disk(10) bilat = bilateral_mean(noisy_image.astype(np.uint16), disk(20), s0=10, s1=10) @@ -200,7 +201,7 @@ equalization emphasizes every local gray-level variations. from skimage import exposure from skimage.filter import rank -noisy_image = data.camera() +noisy_image = img_as_ubyte(data.camera()) # equalize globally and locally glob = exposure.equalize(noisy_image) * 255 @@ -252,7 +253,7 @@ picture. from skimage.filter.rank import autolevel -noisy_image = data.camera() +noisy_image = img_as_ubyte(data.camera()) selem = disk(10) auto = autolevel(noisy_image.astype(np.uint16), disk(20)) @@ -323,7 +324,7 @@ otherwise by the minimum local. from skimage.filter.rank import enhance_contrast -noisy_image = data.camera() +noisy_image = img_as_ubyte(data.camera()) enh = enhance_contrast(noisy_image, disk(5)) @@ -357,7 +358,7 @@ percentile *p0* and *p1* instead of the local minimum and maximum. from skimage.filter.rank import enhance_contrast_percentile -noisy_image = data.camera() +noisy_image = img_as_ubyte(data.camera()) penh = enhance_contrast_percentile(noisy_image, disk(5), p0=.1, p1=.9) @@ -495,7 +496,7 @@ closing and morphological gradient. from skimage.filter.rank import maximum, minimum, gradient -noisy_image = data.camera() +noisy_image = img_as_ubyte(data.camera()) closing = maximum(minimum(noisy_image, disk(5)), disk(5)) opening = minimum(maximum(noisy_image, disk(5)), disk(5)) From 89dbb4dccb93af5ebbfb669474b06d5bc14731be Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Mon, 5 Aug 2013 09:26:36 +1000 Subject: [PATCH 374/736] Add unique_rows function --- skimage/util/unique.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 skimage/util/unique.py diff --git a/skimage/util/unique.py b/skimage/util/unique.py new file mode 100644 index 00000000..a09fadd9 --- /dev/null +++ b/skimage/util/unique.py @@ -0,0 +1,36 @@ +import numpy as np + + +def unique_rows(ar): + """Remove repeated rows from a 2D array. + + Parameters + ---------- + ar : 2D np.ndarray + The input array. + + Returns + ------- + ar_out : 2D np.ndarray + A copy of the input array with repeated rows removed. + + Raises + ------ + ValueError : if `ar` is not two-dimensional. + + Examples + -------- + >>> ar = np.array([[1, 0, 1], + [0, 1, 0], + [1, 0, 1]], np.uint8) + >>> aru = unique_rows(ar) + array([[0, 1, 0], + [1, 0, 1]], dtype=uint8) + """ + if ar.ndim != 2: + raise ValueError("unique_rows() only makes sense for 2D arrays, " + "got %dd" % ar.ndim) + ar_row_view = ar.view('|S%d' % (ar.itemsize * ar.shape[1])) + _, unique_row_indices = np.unique(ar_row_view, return_index=True) + ar_out = ar[unique_row_indices] + return ar_out From 11f9a5bdbd601b600c08816806de3813b1a10aca Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Mon, 5 Aug 2013 09:35:50 +1000 Subject: [PATCH 375/736] Add test for unique_rows --- skimage/util/tests/test_unique_rows.py | 32 ++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 skimage/util/tests/test_unique_rows.py diff --git a/skimage/util/tests/test_unique_rows.py b/skimage/util/tests/test_unique_rows.py new file mode 100644 index 00000000..cfbaaf05 --- /dev/null +++ b/skimage/util/tests/test_unique_rows.py @@ -0,0 +1,32 @@ +import numpy as np +from numpy.testing import assert_equal, assert_raises +from skimage.util import unique_rows + + +def test_uint8_array(): + ar = np.array([[1, 0, 1], [0, 1, 0], [1, 0, 1]], np.uint8) + ar_out = unique_rows(ar) + desired_ar_out = np.array([[0, 1, 0], [1, 0, 1]], np.uint8) + assert_equal(ar_out, desired_ar_out) + + +def test_float_array(): + ar = np.array([[1.1, 0.0, 1.1], [0.0, 1.1, 0.0], [1.1, 0.0, 1.1]], + np.float) + ar_out = unique_rows(ar) + desired_ar_out = np.array([[0.0, 1.1, 0.0], [1.1, 0.0, 1.1]], np.float) + assert_equal(ar_out, desired_ar_out) + + +def test_1d_array(): + ar = np.array([1, 0, 1, 1], np.uint8) + assert_raises(ValueError, unique_rows, ar) + + +def test_3d_array(): + ar = np.arange(8).reshape((2, 2, 2)) + assert_raises(ValueError, unique_rows, ar) + + +if __name__ == '__main__': + np.testing.run_module_suite() From 640c49608def953f377c5f25f810056de360c3cc Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Mon, 5 Aug 2013 09:40:10 +1000 Subject: [PATCH 376/736] Add unique_rows to util/__init__.py --- skimage/util/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/skimage/util/__init__.py b/skimage/util/__init__.py index 7afcf54a..9cd2bc50 100644 --- a/skimage/util/__init__.py +++ b/skimage/util/__init__.py @@ -12,6 +12,7 @@ else: from numpy import pad del numpy, ver, chk from ._regular_grid import regular_grid +from .unique import unique_rows __all__ = ['img_as_float', @@ -24,4 +25,5 @@ __all__ = ['img_as_float', 'view_as_windows', 'pad', 'random_noise', - 'regular_grid'] + 'regular_grid', + 'unique_rows'] From a6ec1741dcbe3119e0ab8d45dd24ddc1e409cfa1 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Mon, 5 Aug 2013 09:42:23 +1000 Subject: [PATCH 377/736] Remove repeated coordinates before computing convex hull --- skimage/morphology/convex_hull.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/skimage/morphology/convex_hull.py b/skimage/morphology/convex_hull.py index 28ff204b..b045d7f3 100644 --- a/skimage/morphology/convex_hull.py +++ b/skimage/morphology/convex_hull.py @@ -4,6 +4,7 @@ import numpy as np from ._pnpoly import grid_points_inside_poly from ._convex_hull import possible_hull from skimage.morphology import label +from skimage.util import unique_rows def convex_hull_image(image): @@ -35,6 +36,9 @@ def convex_hull_image(image): # limits the number of coordinates to examine for the virtual # hull. coords = possible_hull(image.astype(np.uint8)) + # repeated coordinates can *sometimes* cause problems in + # scipy.spatial.Delaunay, so we remove them. + coords = unique_rows(coords) N = len(coords) # Add a vertex for the middle of each pixel edge From b03fa9247859c678d451429d5a42dd9181536573 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Mon, 5 Aug 2013 10:09:37 +1000 Subject: [PATCH 378/736] Unique rows after getting the pixel corners --- skimage/morphology/convex_hull.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/skimage/morphology/convex_hull.py b/skimage/morphology/convex_hull.py index b045d7f3..565b10ab 100644 --- a/skimage/morphology/convex_hull.py +++ b/skimage/morphology/convex_hull.py @@ -38,7 +38,6 @@ def convex_hull_image(image): coords = possible_hull(image.astype(np.uint8)) # repeated coordinates can *sometimes* cause problems in # scipy.spatial.Delaunay, so we remove them. - coords = unique_rows(coords) N = len(coords) # Add a vertex for the middle of each pixel edge @@ -47,7 +46,7 @@ def convex_hull_image(image): (-0.5, 0.5, 0, 0))): coords_corners[i * N:(i + 1) * N] = coords + [x_offset, y_offset] - coords = coords_corners + coords = unique_rows(coords_corners) try: from scipy.spatial import Delaunay From e9c9c2ca072533a474d5ab9e5fee3a8046317560 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Mon, 5 Aug 2013 10:20:48 +1000 Subject: [PATCH 379/736] Move code comment to appropriate line --- skimage/morphology/convex_hull.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skimage/morphology/convex_hull.py b/skimage/morphology/convex_hull.py index 565b10ab..3d592dca 100644 --- a/skimage/morphology/convex_hull.py +++ b/skimage/morphology/convex_hull.py @@ -36,8 +36,6 @@ def convex_hull_image(image): # limits the number of coordinates to examine for the virtual # hull. coords = possible_hull(image.astype(np.uint8)) - # repeated coordinates can *sometimes* cause problems in - # scipy.spatial.Delaunay, so we remove them. N = len(coords) # Add a vertex for the middle of each pixel edge @@ -46,6 +44,8 @@ def convex_hull_image(image): (-0.5, 0.5, 0, 0))): coords_corners[i * N:(i + 1) * N] = coords + [x_offset, y_offset] + # repeated coordinates can *sometimes* cause problems in + # scipy.spatial.Delaunay, so we remove them. coords = unique_rows(coords_corners) try: From c3582f21f8fafb1677910c73f6fd3cc7ef647d29 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Mon, 5 Aug 2013 10:39:42 +1000 Subject: [PATCH 380/736] Add (previously) pathological qhull test case --- skimage/morphology/tests/test_convex_hull.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/skimage/morphology/tests/test_convex_hull.py b/skimage/morphology/tests/test_convex_hull.py index 075762ed..ee3b6bfa 100644 --- a/skimage/morphology/tests/test_convex_hull.py +++ b/skimage/morphology/tests/test_convex_hull.py @@ -32,6 +32,19 @@ def test_basic(): assert_array_equal(convex_hull_image(image), expected) +@skipif(not scipy_spatial) +def test_pathological_qhull_example(): + image = np.array( + [[0, 0, 0, 0, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1], + [1, 1, 1, 0, 0, 0, 0]], dtype=bool) + expected = np.array( + [[0, 0, 0, 1, 1, 1, 0], + [0, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 0, 0, 0]], dtype=bool) + assert_array_equal(convex_hull_image(image), expected) + + @skipif(not scipy_spatial) def test_possible_hull(): image = np.array( From ac6f28225313ae2105d027864b5eb1fce23bd425 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Mon, 5 Aug 2013 10:04:58 +0530 Subject: [PATCH 381/736] Added star structural element --- skimage/morphology/__init__.py | 2 +- skimage/morphology/selem.py | 48 ++++++++++++++++++++++++++++++++-- 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/skimage/morphology/__init__.py b/skimage/morphology/__init__.py index 38357f02..f01fc3ed 100644 --- a/skimage/morphology/__init__.py +++ b/skimage/morphology/__init__.py @@ -5,7 +5,7 @@ from .grey import (erosion, dilation, opening, closing, white_tophat, greyscale_open, greyscale_close, greyscale_white_top_hat, greyscale_black_top_hat) from .selem import (square, rectangle, diamond, disk, cube, octahedron, ball, - octagon) + octagon, star) from .ccomp import label from .watershed import watershed, is_local_maximum from ._skeletonize import skeletonize, medial_axis diff --git a/skimage/morphology/selem.py b/skimage/morphology/selem.py index 2c41e393..9d466e84 100644 --- a/skimage/morphology/selem.py +++ b/skimage/morphology/selem.py @@ -232,8 +232,9 @@ def octagon(m, n, dtype=np.uint8): Returns ------- selem : ndarray - The structuring element where elements of the neighborhood - are 1 and 0 otherwise. + The structuring element where elements of the neighborhood + are 1 and 0 otherwise. + """ from . import convex_hull_image selem = np.zeros((m + 2*n, m + 2*n)) @@ -247,3 +248,46 @@ def octagon(m, n, dtype=np.uint8): selem[m + n - 1, -1] = 1 selem = convex_hull_image(selem).astype(dtype) return selem + + +def star(a, dtype=np.uint8): + """ + Generates a star shaped structuring element that is an overlap of square + of size `2*a + 1` with its 45 degree rotated version. The slanted sides + are 45 or 135 degrees to the horizontal axis. + + Parameters + ---------- + a : int + Parameter deciding the size of the star structural element. The side + of the square array returned is `2*a + 1 + 2*floor(a / 2)`. + + Other Parameters + ---------------- + dtype : data-type + The data type of the structuring element. + + Returns + ------- + selem : ndarray + The structuring element where elements of the neighborhood + are 1 and 0 otherwise. + + """ + from . import convex_hull_image + if a == 1: + bfilter = np.zeros((3, 3)) + bfilter[:] = 1 + return bfilter + m = 2 * a + 1 + n = a / 2 + selem_square = np.zeros((m + 2 * n, m + 2 * n), dtype=np.uint8) + selem_square[n: m + n, n: m + n] = 1 + selem_triangle = np.zeros((m + 2 * n, m + 2 * n), dtype=np.uint8) + selem_triangle[(m + 2 * n - 1) / 2, 0] = 1 + selem_triangle[(m + 1) / 2, n - 1] = 1 + selem_triangle[(m + 4 * n - 3) / 2, n - 1] = 1 + selem_triangle = convex_hull_image(selem_triangle).astype(int) + selem_triangle += (selem_triangle[:, ::-1] + selem_triangle.T + + selem_triangle.T[::-1, :]) + return selem_square + selem_triangle From 9d7a90395ba406fbb2dd1cffd2b4427d57946e57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Mon, 5 Aug 2013 16:45:59 +0200 Subject: [PATCH 382/736] DOC: changes for Marianne's coffee cup --- ...lot_circular_elliptical_hough_transform.py | 19 +++++++++--------- skimage/data/__init__.py | 8 +++----- skimage/data/coffee.png | Bin 305945 -> 466706 bytes 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/doc/examples/plot_circular_elliptical_hough_transform.py b/doc/examples/plot_circular_elliptical_hough_transform.py index e537a4a1..bd036e03 100755 --- a/doc/examples/plot_circular_elliptical_hough_transform.py +++ b/doc/examples/plot_circular_elliptical_hough_transform.py @@ -110,27 +110,28 @@ from skimage.transform import hough_ellipse from skimage.draw import ellipse_perimeter # Load picture, convert to grayscale and detect edges -image_rgb = data.load('coffee.png')[100:240, 110:250] +image_rgb = data.load('coffee.png')[0:220, 100:450] image_gray = color.rgb2gray(image_rgb) edges = filter.canny(image_gray, sigma=2.0, - low_threshold=0.1, high_threshold=0.6) + low_threshold=0.55, high_threshold=0.8) # Perform a Hough Transform # The accuracy corresponds to the bin size of a major axis. # The value is chosen in order to get a single high accumulator. # The threshold eliminates low accumulators -accum = hough_ellipse(edges, accuracy=7, threshold=93) +accum = hough_ellipse(edges, accuracy=10, threshold=170, min_size=50) +accum.sort(key=lambda x:x[5]) # Estimated parameters for the ellipse -center_y = int(accum[0][1]) -center_x = int(accum[0][2]) -xradius = int(accum[0][3]) -yradius = int(accum[0][4]) -angle = accum[0][5] +center_y = int(accum[-1][0]) +center_x = int(accum[-1][1]) +xradius = int(accum[-1][2]) +yradius = int(accum[-1][3]) +angle = np.pi - accum[-1][4] # Draw the ellipse on the original image cx, cy = ellipse_perimeter(center_y, center_x, yradius, xradius, orientation=angle) -image_rgb[cy, cx] = (0, 0, 220) +image_rgb[cy, cx] = (0, 0, 1) # Draw the edge (white) and the resulting ellipse (red) edges = color.gray2rgb(edges) edges[cy, cx] = (250, 0, 0) diff --git a/skimage/data/__init__.py b/skimage/data/__init__.py index 65802c0b..3791089b 100644 --- a/skimage/data/__init__.py +++ b/skimage/data/__init__.py @@ -189,13 +189,11 @@ def chelsea(): def coffee(): """Coffee cup. - An example with several shapes (including an ellipse). The background - is composed of stripes. + An example with several shapes (including an ellipse). Notes ----- - This image was downloaded on - `Flickr `__ - No copyright restrictions. + No copyright restrictions. CC0 by the photographer. + """ return load("coffee.png") diff --git a/skimage/data/coffee.png b/skimage/data/coffee.png index d7f38a3048e2b6dc7b0adf17d775e24d11ba0aac..f8350bf7e6e22734667f8c86fd230a741370fbcb 100644 GIT binary patch literal 466706 zcmV(=K-s^EP)@^ zZndBQG=T2GG)WDIG>*;COsnaymt^ycNSQPvQDd|g$?gUkOI2pZ9v%_;zVkc_=$=HO zDl;R({rP+M-R0bK@2g%fCxbzCyRRjLHUirIxQ+~r2-|$rr z>Z-o*Mn88QjOyXF@<-lzy>QeJh`PkKOs%t&q-wS_Va+%A^^~*gj*FjzTZC&S+ z+&pjQ7hWcVs2vl3ywT5w#ILy+zsYMaqb%e9_!M0Bd_8fEdN3@%bPSgZ-|);l{Li1% z>y)9tJWhH4%LN^XkHkYTKAy~19qrjG`?$Wrn_syvW6s1nGklUE@V07rWeUsVu>#!6 z9~{!zEBBM*T-3bv+A-za#YywMSEieLkDT|x+iCMqpWn3ga!^ew7V$J24`1IbS(g3o zbUvQXr|YRZ^Vw!tx5G;_Z0hPX8jiRd-;U~0*In9iHEJ%$ZZMgQ+hKjaboIEod9z}% zFBjf$J{->@K64$Mj)&{HyIiV=w|DFJAD$kMlhu6rFdtWZY_QpHpMKozxbK;f491gj zeLf$$W5>WR!|uXNS9LSEnT^M7grerxqi)bJ%&r=o>$+*Gb~0+K_OL&0H{J8@a5;Be zq`IC5r_1$n9A3xcNi}Y27H50vp0?NhaBzn(ch{%walJnt_DoZ^9FJzBaS@B_uB^Hf+;-8j_$Suye0GYfk>6b|O*0t|JBD{U9hn{1sv0rp-Qmm}jz^caX{%Ayb;JGn z#M`^>c;$0Q)b%nNx*>-(W2~A{&97L!h99qWcRnL^LxgmAMk0|%7rq%@U6^VxV!$<5 z7+gC3Vnm2o*G0&NLnPzmvv){ugGl=83uE948Ivvk= zPsimh*F$&N?N9sto=Ia6&2*e=R;NLSz_ZVJtmZlzaJ9kU)a?gXcG75=7m$eak=@Vi z^Mr1;OEql9?TEQ$8hlwnWdeI8Ft{?aDn6vQ8u6LS}XbGgqC*}sIoojvSvhll)&kT8ZL`0RxlHllFsE+=H$ zg*^}JE04jOxnI;Ss%q#sc#MYY^KHY$E<6n{w8P<){kT8|232#~Js&pf;nYG zS-T)?Jh|FuI^cth==fY$)9dAg%DiAesxxcF&Tl5Ob3HsBj(hendk>Mn#tKwt7LVCS zK!y!6$THSd%Lm!J45U?HmGBEIhQuHq0|tO<88*l^e_sb@7TE1nBcNAiS=d(F z3XImbw=3EpCRy9UBMr-uLpTcS7k?8>?Hi&G|B_rwGaE#00gYSv{Sgq|Vp+ zc-l9^0n(0GZnqmoT%YRhbok~Ue|H_W)ok3fb+)~iTxz1;+^YyrU-mkZZ=j|4&JiMHSqxQ5v zp*fhe`HZcA9_seT!S;lj9NbOZxqDRncv7DR)Arg9>X!XHst)=N7i`((v^@+5`vcY% zOiB})v-HYn`+W3H5d<#C`+=KcH8x7dq(4++;qZF76r>SHQH>j9_Iv1s&QEbL&dS;J}dcrX|GgyXqbj}g0!2tiJc!DTAb{mQTF=U+? z4p!-M=~@(r@`f0oFx7J?B%QE)T-n6WaY($`kE^%G<7uWP@ zanh>c7|l3rlwOAI__1)Y0BZ6x57O{l*kXpmL-I+Mnvo!rhvNaYj%UKCrp<(v#@=C! zPnW~tRPUIgi{D`&VdsG1y)Za6u3?L?{Vec@3 z6%LspglZMxWMhRol`wpM;dfo7F zjsc$x*(5j&*pjh={6A3Bf`Pn$zlbW2do8kW6I;MW`XO}wKTU6qxNTy= z49O79$^B!4`0*l0;m@Ma{%s|AWrHDIdX(iWqu~`Mn_;Lja$_D0LDm)h_(eFvzS1Ww zE`?!NF8I@1`pIWz$|HLK|BR=L1GZJOkf-SRW^&!6%=uUah9A#PM`E z46gTY?r_Yvd!A`<*2Q-7;a21D&gS)`sm@2k5Hu2&9>q(fie$39>}t#-=5su0*~Tm^ z)(fE+5#Bl#ryVMdT}OnczQx}?v@H(7g!ddVNX!AsK34KV%-f7B6u_xIq4M#WiJzMh z12Rmwb{I;gRv(Mk&{Bab3h7R==jiUi<8ukN@0bUDcki9z5D*|O;bOulEY<+WvJ3^-;;88I86j(hbkqK{c zF$Uf60z#&pPRz$#SH~moJF+nlp|jBfW)t5c$d0i!%dXeYcWNGQ$ezZPO4J;6r$s^M zw(L>t4bGo?_^M2zubC8FEtK}5guKj*A$)gYm~4uws}J?Dc0#9EFpVxM4DF4b8J(Ca zVxp)9))yPA4B!rF;gMLz%4}blkPAB-uVqM3HJq^hnF%9Ntb)OCjQgNtOX6Q)L5O0j z8dN|}o~`0G;sY%8OGM$^F%^+56dS&p@_gMR2S$LH?c;{gG_Cdp1c61-<3kS^`3lKk zhKxluk^`&~4>CZBVDXMj+77>x&=$i6nsQ`?h>?fm4q*^!Ab1cViCJ;_43ve;dv7>z%Cz2I@{u&^X72A`g{y2e>D9>4%4_9Axaj*+TA1=u5KEO#nGppyhR}`8;$}?csY^`hfL<(RdC9*c z9c66A^~(U861zfY^ zuU_4Ly&S!12g^%08xVoG+Cgsk4w*l#7AQW06=dmj0?=bJF$Qdg!K9teXY8oa^3}~u z8>g_Ne46QEasR3wvn0dq>4dkgzktF#9SLM`8wDc|r>D(s*tG3zGD7N(#}N_apx*6v z2r_DyXCZ9DwMB~)8@URD>Zl)kowe=4Kv5JF2e@h`XjoK3tUH$+5?W$eHoL=aj|5-F z=i_yYreMDeKp`iS8F3zy%g^k?&Hki`ywuy{b;pEWFAaB3oE+Sk#{#(n@Ewj9%Mo|P ztetT_6d^nbz|Gs6*_U5E+|7n)g%et(8Q$LB8-a1<-AT-rop&C9e==9oxsYuK5O>%! z2DUm8D%y%Qbs+~=@K2n-b_%%Wr*br00nfeMu$PYC-lH-Z4Y?dQ>14GZ~#uF z6^`%a#wLjKA&FekyxE#YvG`t04px_2T@S%S+$_x1@i^d`hAji&X(mpsOT4HF(0^b; zFO$)9IvLJ@k()7(0yd1pcEAxZz*jw^;TZy6#pY1)0%i&I3v^6?ip@gdy3YiPnT{a> zSx^;iV(~9W1Yn@5ya!EPvJgWd3p6Wc-@jEGPYvGTh76quCf)=lRPQA!64 zGVA2;Stp`GV|6@%MxavT%8VzVW`}V#qHf-PIpO29V2yG7&s24w(W;lvPB38t#1h6LH9Fxd3K1%T3>kIf1=3ua^Z25%$D0{-B-=Bb<= zyd)m8!p(dM4VYb)jtlrjY*)}ZWdvPsXzR=ZCHE$U^%ltl=H8b$MvpuOO1OkfgkA}c zq7~%6aUM%X$m{xT&@M4ebd*r6iWz51F;`|Cj?T67WZ0w5)^kv$GD?1GnMCG=EswqD zr2zOwOU2lrHuSiO2e_VSfuCiqhhvbdqT&Ni`a++d`)C9HYb#i+PB9)@G_k!R-Mwjh z(cLje8EkM=?M3-)P(Wd3Ev0*;JXJ2JX*r7xY9Ej=ZDhHVf0d9}Wr4mcQJRYsgHf#S zPeIj-QSQxOOsn>|gtoELv7Nr|Uus9a__7}PT?tfQz7lQ_BvS1nm>P-{;I7WNN}AFh=?j4 zGe*qfe(c)r!tQm=*^TbB4#SQkjEK!wlhtxGn@vCvm-9)Z>({YW58KoH{%-l^CNAn_ zz1^W)*lwBB;r?_6b)VIBcL4uHFN4CGiJ9UFoWbK{+D-|YP*n$_CX^a3 zum~2@G$mt4oLCPG8$cf0sfd+egXb@Y}}+GMXAu>CJP;p%(Sg$!_Qt#?`C7h za5!HlH&c|Zdv>b_pawhET!#lFX-f7e#&}2g!klUJ2!Tm=fz|=ono@$zMh^~N4ETHGpQWn8jhMOJP+7FI<)oB2A{%rUQHW5;|=t0{JBJVAI_Bj)@lydM3I zb#oI5jU0#t!Nf+@WHxLG5ScEXPwkLQk|}mpMXMaIjCm(Rr}i8Hwu;DUb{KJxO>7=} zhd`g`0h`Lm_#K&MeiBaA6C>8pkeCovlll?%5f4tr%e-uqobad$5>u@XKx&JN!2nwQ z9Wxx}T$<*L5h~uj>e|}~G9`^5l>isbslIk&plybm08IzGL}cdev7-31BqidvF&{^) zQitJiVkHtp%u2qA#Vs)@0k1wcLnbZFSAbAM6o!HlF3MG9teP^JHgvMq-Z&vKquCmyRP5%k&aR;h2~2^B^zpE8i+kdVtyOeTWvHHLu2- z7yi!Aj>6$aD;0wj_KI6B0bX>5;!AWYi;9*TqUplmk%#wpR`q!EWh zXq%~nD*O(9B6*H$bG{Ja@j1*5p$JJQ0va7srs#aemYJeDh(-aiB2=dnff_cZhVsDs zrZNSvMH5I8`}01#1#?J3;(~hUCMXYKHgqN7+U;$#Y6c5zSUY^)oJi77rzrXJens7)Z^ znW&nCdG#zYZ~Vu)u#GWOB50%n8`SO*g*dFHAMlp0@wfRfoF21aBF${I4%SfL5{Atqq3H^?DGGyrWH=tAf+(ZnkB>AYcIHZ)VXm1NNZem(|GZ}kvo=*;0~-4F9t|{07#E~DKf+~gqmW#(VJw= zNyVYBv9Q=AJ@vTx>KuZ9bQ`FtyG+)Q5e(BTt|Ry^9>#K!Mgx7s!ue6vacuE$FvjF1 zRx-mZgqNHa6M)tyi^l}+h@~ztl-LazWg<3y&GZqPYDExpbB?|f58=qn9@!T}kyn9q zOdyXSE`XpqLM&AX zo}33Wm`FHY1yB=<9?oJxPv%FZ|FuptOD*Vbx;tN_PDnT45EsPJ9che@r(l%QV|&rX z>iA+vdRtNmBSn3ei^W_;zsJHPa?_P`|4K0HUrjXQdz}zXLZHWZC9x5?mo}&;T9#jG z^s)rW{lDyA|fN`7mex0w0zP8|hwMutRu zl02h`fa}`_pn6ID+mmhOSz~uKhUIzUA^y?i{3)Qyj!JM`qQBTDSE={OGfc0H%!=6- z*dpw03f0~08zwB;ECuN;X>eO|sOGnK&2)km+#a_>h!0n?bbACCr~@N4X@&$zyYunO zU;kyhtiS)K-ZxC1T{p}>l!LKII4JQ?O-y+<}jbQzL->Q zE^R6~eRiFqy+QHsuba&lIc2BMK#GQg>^v90_dM4WFNP+v8B)&-JpqGI-)5=s#TN zI2i+-O=U(luw;~aA`xYu#XFNY6jdbc&R)d8kA{a&Zm0KL73>OvFG51XUgFZK1+{``V#uFBSTb5zBv=w?hHAHgGDD~byToaf%BE*5hY5&iEG?61 zA|*Q9U-rfhqEqN6T{09p**)gWWMnv;2)kF7K#1poLC7!TXRC~&B@#-ygNB$A7`zGK zxGD1|YqL8{>w4B8W+pQ9WC=zE7tQCJ<^}+Ffao3kFH<{QkC+cyj24$b#srg6jYJ{0 z=x`R8Fpz~}6T(jtL&qCH@=yQ`!o}PW@50Pk|K#>gL{%jUhvVUC3TmM>NA3 zQ3>Pm86|4^h&)_2CjJ5Mb;|LCFz%QK_hEofX`0YC>PdBl(G^uio=ll!D*@U!QeF>x zassIE08%A3kP8@ejW{xNIUXL7(rw18iyk9wfJcR9G$zAUQ7{ah2jrEx010M&Gs8^& z3izLBEUJX5a>ZFLp*}*^)_6#(ipWXEG*41|c4m;>s#B>v6PKSlNySDR;n6^c1I9P4 zXPOqcIe4rg1wVls;Ep6sqbfs3V&w&ReMm7(gGU|^Bj;X0Dv&O=8uty(PfAs8sE$B} zjvix))BnmB&Vu9g|CnRw(QJP!0CY9%~Kq#YdCXvq4XwnqXU(sOMYir}Pn$ zCa_01gKd1Yxr&eqp=WQF_hbVnshhjUycI+-#3P?MdAnjpnGZBlZ|Qpj`4iMx9xR4f zjZ!p4pw2Qgv2|`Ioi^=!tdq!ZAy zjCFN4d3d!X8HB&Pg6z(w$J}Bq@t=s)i18M(LjkrXR9&P@0>a+oVZY?f_2BS)c)wnM z{_C&5`udlb^^<9&@o>97zu%m;$J1;+#I!ujW+G(}A1+rYA^7~F`cu>y6i~S_Jo&Ym z;Tt1>8bL|y@}*749WR>=Nx>)P0Q7S_Jc5Oy3BRNz1na!O1d%R_>&*xbhrbx;u!DR#-#lVvPVtx%{ObbC(@z+p^dFX(1<_axH!Ix2xcpXsJP&RJ6 zc&`>QP##P&p%l?RFn665oTd+PeNBOipvD4n51bP8T&!7 z{5_Lq9RdT9KJ4iExNH=)Y*J`W6!(+CB34+)tGd_dtb;k{JTz)IMkl13V9cfhqw|<& zwU{#2TY^9rl5|)Y;Ehb2PZDdAED3l<0aN2Vi8)Ah7OM|+pN)&Q^tW;k6Adf&;gY&9 z3ehQF-t!w?w?!17i06YFe^&G<-QCZ<)>tDDfyJiixei?h$X>v!N9TN&oPVx9ZC2u(7JmHYP=G4tkS zd=4FlBv2EL;?CEDx69_w7tN;q`7zQFrPwK zktY56(^p9Q-gL!jQq|kTetxsU03El7F_0s)gKaUoanHS^M}7l zZ5eTD!gYu~w~OhiszG`1S_L^^2Vg7?GJ{4d3|Xoyo!!IRSM9@$G$dsiu(+Y@HM8Z6 ziSBkof?WeRLK7%GqyU@Y-FPnZfq40_Ctzi{2-L7(QU6BWWMS|q1C3JmQSDGo6mC#r z1;(VJcB_wxuJAeWh^abgCa6SNhQ#Y|XIWW{!jZ6`nar685mXF5ArD+4tD1tZ0BeS3 z!Zy(WAXC;d>d)%xlG*}SG6`}yf;plEg$;1F2%8{Hq{dfR%o?`@1A*8>t;fGEKBBq= zSWg@dCY9t20t|H%hJv{>#mQ6w?8cg~UB)C}Qb~}V#TBoN>|8<8!v+8;8%lPtrdWt+ zOVNmQSwupF^s)oUuA$T#bq-Fjh|z@r4_GH5N6>4Kd@@xjRuPV4=8^_hAus|Nv4K>u z=SBjTb2}me<^yBYU3Y?FtPu%|8)v|>05=$()0NsyXg*pVR4uqJP;Nk8dA7(#LKodc zTu4(^F&L;RCf5=p=P}N)3xPr5p;XqT!X!i~7F~!sl>&*BNC^kED_UNabsAZY5KM}f zutDf6_{o_S37aRy(qXZ*K&?n?E+QttP%O%lnPcfe?2;~PVr;b^eqA#eP~Uuzg?ms<1ZB<2nNA}^&O8|?Cp^W{3j^SVR5)as6BY~U zO!QQvU93d5X2@8P*VNY^qmY;Em%FE!1Yhmr!IB~?;ZJN~DOxa)O5Utw9K;b!ausV4 zG&|`vB*s`Z6b$Ff@=ad*nCWbk>VL!JPKGNSN&4CtC0>XTWJ5nCe6mTXc0c&iTI=r8m@-`KF@Xnur(Lmk& zCdqIk#1sL=$8y9dZC$!Z4q$oNul|%oX0g%|RnUO2OWsWeZ<@iw7{cNaEWikZ&0!(- z5sji#FCmYti4ID4xFD;j8VUnmy}g;;jV?P38>L|y7+4_i{RU|F?Yh{~i=0eJ1x8^yZZ!Q<(#gwp9`+*kP zgKoLAA@Ep41T_XiJl--0BUN=`Hu45#+mEG{NMG5IUwr<+V?#o`na`-ht*8|Mt}?~f z?beqcAHmWn_0>%^=Zxz_<_?{l?K2%uK;I7XABER=K;xeyz7Fy~lY4GZAP{@=iyM$z z?Cc4OJqgIkh}f5{vVGENtY{-Z#sNM^cUj#o2w)lH<1;l9#IcwnhAPR1b&^GI`L5*k{xoD;E8MDXo5r2^?63mDBXi64g256;@-?{;)B}e$BbuuP2^C0rI=OI>-G@33ZUt)MRjFhP+hGd)1g zfk~SwK1g@Q-wNZ2*m2CIOuZ}pWQWnSy0Y1yHqLOPiM-9h#!crhu$hT;Ml5&(8B;=|e|#Fr<@qkOV)*%23s% zFm=W3@%Sb_G(p@ecM0hULxIOYVx96iQlYYqRJ0OX@SF4qtRi)h5iAi=Lv5L?VICS4 zAu!xu{eE5BT>*6(=hy_ENQHhDE6bJDNQLB6jCvxnD0-b#Yd07UdPZsKjNp|SkQb2a z>jiwqMPh1N{0pw&bm4MY)zF8zSYmN$K8~TmSeY0#RbthSKM3!l?WF{ntkVjBr?W;f zyMkjd%|>#VwFY4a2%#cYZHjY5HL1S^cUB<^m^fkx5XN8e{2aC+EYE`Q3`R=`$3RcY zyf}d=ZJi_{ekcsu6(2mt<&UAr7cJkb_hJ-za|zmtHH^~M8ux!(XjOTB)&uCx2{*Jx zRgkE6Mg3q5L7d;L$j326Y?GpkwM|)GOq5uBC@svN=Io`s?`M`No^Q~rK#pS184zxQ zgx|oBi8Fn?RIVEoM#~aq^upZRz05$+>K9iaPRt93$nXV^$ZG}mOw9Q*E%`n(=I@h7 z^x=yoF7NZvghCRPx&eK&8?87lY^}G8(PxutSr2aEW>Nd136a}IEVA!9d^!;S1FAw^ zjWxo}9lVOe`7y^i`x2N}% z>sdw)xh5nSKy9dZuTK?!|AuXN4X#n5}I8bhWOFXLrxHZC@4(@(FR3k zbw)B0P5_}GfZp){oCCoG#;KAp(s&X%k`QAu#{3?#hINO!;Z%;~JybE6wJ1Dev2+{~ z9A*(FtgfYf=n~2|Ls1b~Le0YX#j6l+#BT8mvv24K{dsIsyhz-pGf-p-{o;Qmd8px} z(MfVkgdHgh9HqKX&^)CbjQBKDUo82sdF`Fh496=I0g-a9OpsV%d^#_=& z6YPIx8L7qjFfK`Z3izA0J31kgj8&p6C!L`ew7IdZ=J%;rQqc>vpg_xvezM zlGuqe4M42#7M+8RuoN&^s~CFXOY7vqx77c)BaCWCoX*e7(CzUcb&hSV5C9QqQ7E}5 z=m(gi>}!JGRCd~cMFKe6ow}pzK}Ka#hw+trMyiU*!aVm&sr@z+DRsuyFlHUF)`+nb zO*4ifqdYl$X*pP08iDlK<={|-MHq$0Mpi~}BZNKBl1{Epl({xt0iy=R2Asre$;v7Q z9?`dCM7vB(FUT(bV}?Vplp&@@AW@Ja9+VZ#pr}xYuR7Id+@8e822>V$5}CV`2@2U* zc-8wF88I&qMv9#636}Y4Dj8;z!!S>2p?-WDGSsk1g&C^Dbwed_l3v(mjj^FlMT3!_ ztYBGk3o}`GJj0jRD?v=Yirvb0i4?It60BSVjrS8^}Xc_ zz+&{~_LQ^Let5{l$FJN=GH$WZ<@XvdzKxIFr&#m*#C*z=HxK|+5f$&Dr{&}#{o4Y* zs~-yxS}2}ZFp=RGi{{;YFz>FkAsSsz6LP@-t)V-isgS}$`1E>jjQyy4IVG*tTw$i% zyt-N4W6X~G&1tsoLFWFSyX&;pNp!Hv7sO?w?aeb|2Z zakKjD_1l}NR02AS)ZO#b=CHX^I5(e9;DQjR14TbWGPQEZ7EY>RLe)vNn5_hP;=;&i z!25#A=Ht;i1jO^oxSqBImzOb!(x%%#N%O&GLzEQJ$p+e<59=*4T}308z(9nlkT!PU z9E%au4MJiFa+2&-JLnY9BxNvOtqvz{-e9?`?r!F2Sb(|1WrD3Cj5J$hQxXd-&@h|M zW=?mviy3?if{WuB%8Uhc6X>$cG$A;hbH;HZ2?PhT`2zYrm@X0U+_*=66nWVSs~^9u|3pygMHOEdY8RH_Z56 zyc{E1(O`&GK+N|zi<+8sL0G=IS@DD5|6DjqZG$Bm#RXB~&XPG*GQ97>U!yi~2>t4g_?m z-<3Bc@s&DWXt31UAWc?_sP0rb@fs6JMn^K|$?7D!4C@H*Pk@558^L61lNyQfi7}Nx zxKuoXVGcrqqhOg-iDAq*W(BO^fhI@#*=Shmp9rJ`W@xC|G1L$KRB=68Ndf}SA{MD& z)KkS1s}G`<{)Cj;>5y@1XzUCS0@8?Jkv5A(--k|%BW`yi7Stl!7d+BhQZ~?V1gJV> zSr$?y+k55l1*@wFozT#%HS*>np-SYJG{mtF0m6bN=7X=$X zgNC4lerU zkK_0=L-e}A!lF19m>X0NdeZ1p^MgMwGGYwmTvNtm71?ZZ{p9U!m@0$qo-H?=&_N4I z1*pne0*;W`f%SpEoxWl+!=VMxpNHgOLP>H1{>;}lBlwNXw zixk;&hN+*ccN#lWi4y~h%vXTEj0mg);V*qGeZckIDU!joAh?HUc zZHd4qWgv)b&~(MlRo502djxz6Yd}KHbTpHQFPtg};cc1gu`w}gn&Lh3z0MhapVgslQoIsgqGNVjb!*G z(r7rUsbPbroV_^GqC#vMr3u*wKj7OjdIvT)I-R6GVPKSljY_SDXKe*^(f@&2FExD> z2#j3~3RG+XTKa0HzmU2Fk-@ekPmTD>5H$(Nw24BZJ&h_g;liyJGld~yY?cVIsL^Oh zi!E@`%-kF4so#3z$j#g!Nn}RKLjsA3H>+VgNo`f_xJyX6n*PT$54~ zy{M9JE3uF6hBhx1y3#bHSEVLX?WLi6F)`8pDeFnl9n^-!OOLp4Q+gZfE5e67jUUk) zk-2KpwXX||88h$}=*Y0Gv@G>2Q7p!7xmO9*Q^e_y%z?;40y%G@U5z1CWJG#PPhADs z9g~muOq)Lr9Lq$N$dYxp(yAE9l0wex? z3fC?`hf?u@NMiS~7UqQ!rI(UnyY%|#-RHl02cX51t+$&GAJ(*KdG+Nls^f0`w3e<8 zG}>K0tRGkRP)uro!ZnF1OxfuAa5DjQ+CCjVJU-6fzWwC3J#Rj&Ru3Qe===9i^%}*; zJORCf-&qNb&VouwD{KH;R4|=T#$!x1Dw9cPN1{a7BN!FZX)Gf#B-@?Z0=j1diK1iR zz&t-oqy)jcSrVmD!$+iK2?Ta11!rUEE<4+nKHV-xt1;E66RIEZBl(@+14>+qiV%rx zdz3YM_EKYD9`2{FUoVKJXv1b|!jcS<{HA6*@U;j%UInTT@b1;C*G+w(>Vt8miz9KT zDu;hDb#dXXZwBi%BiNzMZLKZOb2P+Qpql_c=W}r*@{e$6*y5|jqMb}FP@@x?3s30Efalmi96SV`W7fdI?d% zLg#KMEjuD*=*iPL?Pz6+F!x;Eo}+L>j{tq=F-_8d9wCX?MrLTEF!zbg+ESEemQEUq za^hxpDny;r=-+yI-v}cfu-BRG4(7=H64K>2C2kNO4As0CPGZ*5+l9X@=D_CjW`Yle zSt#01sp;iMT6L6bYTu~xGq4XKzywrEpw&yNTH`M3V1)9LLh>i$8O@lUpA>EyL)lI} z#97*~q3u)VVM>jsz+GYqcy|m2z6Z1(s4(dmXRHxkdLG;wgE-c(knliboY7!oEWG62 zXv?q)#jM0cct=ppKvOnqG1e4+(-Si*sF^1gO)lJmckN@M?if?esIjmlY|$9)rx`k1 z_U9AGt-ZO;=IW2xrcO<)*PhsfA|RYRViF8E5sQ>kRlXVBQd(!>hr5s}o|dMp4jk7< zFy4NIC@%_D#}h={B8o&-p^I7)lM*NV4Kg=Oru5eWUZ+-4A3$SfG6G&A{V~qBZdFqX9P%{%Me!z z%Z=R~K4b|(eSeA&i?xbhlswc3cLvY{jp#Y|E*P`zc8YzAh3$=dQ1|3w%CL0_ zUow(fYw=wjjPg=O{9^iEek_7fg64b#F>soYrua3vZQh)}W6DCz;VbS)2?V{u?PBm( zZ{I!)&vPmt4Aam7XOxJjnng6=ao~o!W@J)=+gsAk-a!bO`|0PO-oewv+uj3OJsmqv zpZM~Z-TLXctDETzEDvRfnR)x}wXx5K&G&m5D6{$G)oQulJU@PbvN(i=`T4_}?bG)^ z{Pz2&@zY8EA8$V#!NDxg5G^d32Q(K7oI*yl2YqTR`Jx+@{UPM8dYk~MY1A}|8T_!S zcT^m%ck+x*DhMqu(Bf)0%_mv_-Kv$k-PUMJKLZ0cG?QYL&fVB(oN8Iq~wBtw% zl%-{ogr~SOB*q}x*s~FPOz{-ccKrcaxs5kP+13tXgLr~#SIq5ad4jb?N+ zZ3g@251V=qkSE|{7=bk+w4p_gvB_aSnvLlUr#8j^$6*D)NzqYUsmq+0hIVKK$w&J~ z+whExpe;5ZA;~07jlIQ+;P6_CBj#uF5?q>yQYgYYE8TM&A>#8`L4(+9?I7bag|uxG zgCZ4+Fp@6Cd0;)DPC$RPybVd?LL|~`Sw}TZ5#&8}kb-#U)R6>Xltsb_jF%8Mb&mPKR2!S=N`^!jAGfR-(bW`f z7>FJS*yA(`$v=h^pdBFIYQ-Ew=_95;(GM>H`B3N1%sg23`rWO()9%EMex=92wYaEsso>nxucL=&D z#mwZctHtQ>(a<^YJy{z7a1w#~kzyWLXdyEdRdSig6&hoY0J6Io3>(eFs(sMw|??@#dw3q|piv!j~i#hK8LEp<* z@BZaGOxAR@SS~PV)#H!9hg3?mbl6fG54Z71z)6=V#=Rz%3ocS}1kGxSjU%^4jG~Wx zZqTMyjeF#Un}CW&iUF9x=k;d2{lHlg&}$yv-2L*;zyAK)Z%Ac4J$}D?ygX85hvuoP z)!l4Dle22D=?2gH_XKg18dKLC>p9d!>KviZa2|vO$r8AaDhhDLL1&r?`r4?T-_FrT zocSZcLw6HZIWadH1&$AGH!jQOua=FVg-?fPnx+jeGpMbyJB)isu@PWN0h0QHvq9Hj z0sz~)1tyXojrwH|Lal*FiJUU;loZ;Oz%&vD5ik(shju9De5FFF*a3oQJWcpiC*mQ1og6D6H_D1~O8V^;2QPkrUy`FIR0AssK z#~cN=uA6y1T9AuQ(#4=Haf#}Hhf8s|7DC;?N|KCM*^^ZgaI?h1EV4ni8sF|M#-?JI zdf8|gDXb_cc7)+s@ykW8EOQK(o?n_Q%+e&x!c(J6Q$I2on)rj*hS^5(P;`$+q!Dvl z_hxYm!bAjWU+Snev|<{~N@QXbw#vbG2pWe%rX>@rqZ$^i47hDVNQD+^lrEx;ysbW! z;=y02nH|9ozfPB2E+R1jx z9{Hk4H_CEK(Viqxtl}rGKte1`NYgNZYs1eZ>Pn(3Soa?Rp?%y`epxEvULv0aW#RQC zIc%(?C5#ox`lMzqbxSdfxrxRubrD9%0U%3{)8wjSV|trdKuojPr5RMdjUm>tDoHo< zrSZ0cP6Wk`fv`ioFXKN&aiWG5aL>K7^ zwovX{zG5uvo(I!%^X_3q?6KXNQ@4y6?Lb3YfO{*u09Ve?@<09j-MBhFeg7VgFnVdbAs@yR zjJ|unB^MMPAOHX$07*naRP8=rqO(Su{qw`kd^N648&EG=Iqz=i+gZ2!_2&I5xb z$^Yqmpe!wo2dLarEWh3CXehE++Mez8{VhPx?akux+YhJhaBF)BLmE23!P(dkMq7Hk zaGn!80R2yP4ZTL9)MF+{A?}9oOE@}_LP@fBv9dF1sDz8d(3V3i;cbwk$M2vDOa03~Q>wKf6p5x2pV2 zwix9B&|=f|1fPKfX+sH$Mf>D!EtE3(alpSg4;J%sKw*smX7oPnBm1Sf2 zYg`U1%4mm)WJB5t1f~c?G5kWO{wR}GG%oFQtp+DB*4dP@Dj73`1grWy)egNN4y)!^ zs}0iYJvs*^RhkwEL%E|(b76LkCanf0aTgzjx&}iso1qzT%ReIWaXY7%vsqK=2!=HF zALZ4eODwEG9P$yZAUYUMEaQ=r1S)DfFiR&>$>Htr8$bkraczGhFv|mq(59_uf;kmq zIl%!}saSxmO4Eaccm~TFn$g^%QYp_l+{kFj=3)k>MlPn2gmX#J+f3Yw9hcj-JyxsgzXcdJKW9W~Pjmc4%irwnr=_zi5*G>l>?ES3JlC(pj8NPu@`JYrhsE_u z`Z~dgKB7)OUMR?jWxMe!!hK7I04xQb{Sa6irzEF2vD-81z`6yEZZ5i=}iX1C$2$ zQ{!s@a|!OsteA51$}Ussvt(#&MZACUUdcQHeXfL_YOyAT+Y^5zL`p58(q;(gm1QFo z)2mTSgPTw%9hmtO^CEU$5ng|Js*eF&SVu4E;1UOiA{8Sl>rs1J9wkXoS=6Rc^$81p zQtB3Qs*Ansrzk!QPiqBC>8_F4D7ollLtpG`EUXOKgoZE!UazMA=HcePIxR2f8$4oa z$T^0zW6h7RU)><9j~||GAYn&G+URPEXD}Aci_3byJ{?En87?~MHydRV zFAcVzeE9|tjazWL&tAQ{Y#-l$w`IamQp@H1ygvR&WkOT^)qnpVzW?TT93(*r#gD)L zt+*#mum&46I%_tcQNd;fn9c7ImG&~J=4NWz>r#?~emZTbFQX~ijzemJtY}i3jfpmRr1S-NDisv1mn@BB2kj|6OhKCwKK4W@umf^{y}*a_6aJ^rnV{{{i{le z_ig>YI>gFCf(*Tk)O!j?C->4fJxO_JdacoB&}1}#aIPWOv?O*+;_0ROKo-z&OW7_l zQmOvd?M3T?E~E(Fpwz#)66b2vgr}--sBcCvYL28hfvt^DfLOo*O6QX!Zl(T9`l_=d zNIi##pEx1S2i%9HNuI=FIi}R|vP7lgU9;kib)KXzr`3Wmvk~6d95s+-4p^jH2fHUt zj_guj0;Dv+(Rk@~7`i6dO^BS=&37tM9@b&WjUt-<{}R3|Z3WlmeRQ{n2%vGz$-`g+ z7P9BOz*r~fGlq?}(hKr$51pb4F7~*t;z~%vcCQF$jSo6Y%71DM;!YX1-Vv zL0&6aO6VmDZ=9G+VBa;S#BK3I0$6LrtYAtA7}p|($ecoOR9!XCT`#*`(4z#n$#K}B z#`Atl(T+)bmN&ibV>^{|7snvFH|=5O2o1g9mx+`$Z6z`j=QJox|M-x77#5i+&1+KM zdC~x_Muf%K_h@YoeLAO+Vb?(C%zMgNEgi#imtq^a5n996Sq6C5U%!3$SJP&BJrGOG z>H!Y5+=dI#8LlF(ZbeTQbKFw%bn(%uJ&R{rzx(p@*=VqTe1^{e%D3B|C`jON)7|mf zE}MV-%P#>RDANPxo6%e1{dZ5#n`W{6^wU?{AHIKj!aQDH{n^(o^7+H}bApjk_2YNn zJ(C2(Q^n8agrIS|n2%|;wuSM+E(da=8--~DI0Y~X2})bp<7`^asN=u}14b->{X;S5 zKxXSTM~Pw2T)Q#aa=)%8uO`dc`}g08mUPtytHQSBV0ZY1xCML9v`f=`v}EFX`9KFu z>cSq(X3lKZ7_otZ&+1@n=NybALkm6+a%eV1c0tlKThA6m|KQk~iNV1~lfu$B!lQfS zq`sXj(EuBY>K&n+Pb?WKX*#(9>-zA-1dh(l&CRQ~)tgt{?d#_D_2&-{v)h~L4d>ni zZ_elpO~;tHaAX;@MsCZ{W0EX#7^QR=dtmQxl6%`7LGoe!y#H~%{_*?E)6?<$Z+Acb za6CM6wDFjh+~8uQD2e8vn0nfHykfPtF(BcbUIxU;vpd4(DATu(ao;E;rlH&%z?mXeoSUg?x4MFLi ziovo+3kSSe1kx=52@nT6I3mSVgR7z0?v`c@ZLpwjHm0LP3gjIiQ))~)THJZ2f-Vhp zCdt%t;5j9aiJV+Faa9t>kbbTDv(^U%kFf~m7d3B#X<7lQnmuSoE}6_(Ze)_zE+~NK zf`1&tVFQ?)vmMnVHxf(B-GeT9@>AF%iW%xv6kINdI9kXiIjSH8PLH{^Btw_ilWov?4A=fp6IT-a>$>3&gIA?xn@0r z70brT(OD`qN1cNQ%5|iZm55I@J2HW*uM7vuM?<3otF{@F!)0n(4pR1+Ezs0Y_E@p1 zS@5zEx(R}0HSu?cRQQp;=Z9qCYYFmKIF=eZWg}pPKZQW#_ku`43=(ze*f8cW5gfgSKk;5Z zW$39jDsZQ^P1#aX%b3D9itl6AyO=Apg(*6WIW#`gk`+scrGF zyt|+2Gg=V=Yka$16WyKP{qhT(@zH@^q1)-3Pd|P4IqlWnfBOx|K(;6eQ@c=~fD736X8Jia;Sj3X0#x6F8R>(D+M&_69-5fUb=8Jb2 zy8VYA(4Mxu7iSw1zG6P5JtjM4Me?B9+aP9Or;T&95#oboX6M8(?DcVS z^d3HW{nZ!upMAA>^{{&Lc7FfK?1p2dVEnM}V8ldxBK1NkafuYoP#RxWKLB{2)h>=- zk-1)ER!UkDdpwb-ds=OEqvP}A{_$!3{+rDo{;>Y`_m98*=iT}Dm-Tm}&AJ(_Ek-A@ zBM}lQ6(vABD$KPsv+o`MTS=4fkd!V zMTO~k3w4FV0Vot@waxtFhj7>>yAP_7g@P$-vrlb}2S&egJ^IA()(IrNrYEf8sBCeP zP%ejtrWVc`eN~~Q%E*&61e4{znn2ZCl?FpNBD*-y4o&U??OF1Gjl>Nz!-~IZ zmr!n6+%XAla^ua6$T~EOYK>pcWR^Rr-=h`_8Lxa+tL0ee@a|$r12YO%23t}E(J)Mu)oM|bu=K6&bU~YuKV8GG3%$1O6!lD*yU;`x& z-xkAFn_D!Z$p9D;I(80iZcy@O>OrTPZ(@z?a+kE2QIJho0Z`MwJ?}WyeHBA5eE?Ai5_7C^VB*uW6luhnM&ZD}+rF2> zUG{BJ>mPsoXd06eD>9hvZQPqf8F>bK78q)&6!am}fio(h7!8wwK|6ggJ2l1_1@$*7 z3fvmISdhhd8!0~R1I(gz1YUJuina4Md2`UvD5hd@^G$#Egl!-Hiq0y@Kr@7SEkj9G zGK`gv&~8SQyCi*{FfdVQAGsD|D0t*-IRMp4M$(6j`hw*O1ws(#Df}9H`sw4mU5@_x z&FyE%E48tkEq1}eVGd-WXC^fEoa;%jMf!Y-k6y46PHH~whSmPfn-#wA`iJ+^yCtMU z3PQ=%DO4skVu=<8(#|_`VqQz<8(cp$ z%(R&ikAAxy9;(Us;obeOzg~R##r>C`zy8IW`OU42@)l15^@od%z60$kCxXeXi!51| zkO2QB`JLXO@zj);pd4z;9zTeK7L2^eq#%%cyHFR*9#X6~8n1pe{j9p{o~S&K6>MJa zy#Da`!#{uX{vUty^e?};eE8k|{kPZd1I-O+4nm2P-Y4f0@DFGC9QSU1%BI=&=4dsK zIvtjAi4&_ZtXzcS16$KB3PC=odr8*4w7w(>tg#8>22YA}m$aa6A45{H5(~m$D20uS zr8ADB%$fum{*yhw;^=tHfHX#657=HDtu89aQa~aCCG`X*TJenp-gAZ`)LBx%kYfj| z6AgD5DOe^_<*p^J!Qq!x8w%-Anr6~g_%tVvoY%sc<+ILPik2G&y8sC}CWeh{DV*`L z(Glfn@tZYpcD0V{%m3t-lq~@MOkHS;R*aa|v09hnuS`0~ zKoHYy=Vc;fxfk3IDWFW<8J(tQGspq$mUu1_D&;^oLm&rZoUOqM#oy*YI``rk1tMU~ z>NIQ}#jXy~<2CG|7WtLK5&&B2bAkrxJSfmGmqg!V{5_olO<=1_8@?O)CQ>pVXNye< zp8@!7D{V1uJjR3op`R*eVOfWPqWux63T@RGXF$P~#RcTn6T~5)B$aV-L3?nz(HNoi zYS3IeenNTZ33=d>*>|TJga#Dt`0OIlI;7X_Kl`UB{SpJ&a^=zq8~KzmnSoqN{f(TI zWMw`YPGj)c!9TVfUa?@2ObDv=#qJh^Y0@`e5#q#WF^mBl!9Ue4G!C%CaEwlYYL$f8 zLV=EQ!D6=i=#43qsiU)&a8ao?`CSSm#F-4H11lF-AVv2{k){MW^hjkt+2Qg?eWd7c z6U_Frl)INbP*^^A!!IQ0OKz(`rutK$SP#qOAvpGF&n;)J*z|o@$E`&+cmW@U1LDoI}_za*q zzgyie=ldPFX(wOvlh=nIza_1Rly6VZ2*%S7n>`#&c#L@Y>>>(~!78_;_M~c4L;&VL z;y}KZL+3eecewMM><1VZ-FY#psS-om(OPVAJ0+yx0XG}E z$(-1w+vg9o1x3x}&~_qAeO00jo}Dwsf#4SNsTGP;sEOK^ z|L_mTKm2CYy$2L0b4Q2mGq8B#2up@c0_FgE#zB$9WGa5i!BQ!~)zE2~&{H4`#2*Qx zaBc|y>F3D8X%c;|9GPWR45*ZeeG*`H{I^_#Y#7R_^kQH(%nKcDNTA!!iI^-1ZrYRx z7Cr7UKpgbA-*?nKv)stnoTM(8LpAj%5!g?j*u_e5AQtB~!zz^Pkh6J-RMCwoW zjx=QjHq?{y*u-KL_HK<{unU19MIuwk8_-u_`oP4JqG7O|}Sj@hjWWc_uD7h6?g6P6A9@nV>nrFh*P4%huI4|cGSbHz;GZ{5W5$WQF z+qTe zU<+t*)X=gn9B?_ZO~TSNT{(+GkR8#Y;gH1I68`Y)sv}bPYd1rqLZieCF8DW@&W;F2gjLL)V3XbZrv%qG83mrKM(ChSUPjL@znur7s9Hjk) zD-;7+p8BVUdI?oc31rliZ2jA}53gztPC74}ZVVhj10(nxVk&CmlHx~dZ%JNddvgvD zh&M+L-Q939^N?%@QYSAT-oMI&HWPHo8?0X4-ar)rpsYB_Z+Los#8|S8_$^zB+Pfdm z`(J$a6{YNq@w_>~ggI@<&#<3KUV9knbT*o!m|Q7{<@E+%DNXU2m&s&0Ti6(cmE~Ys zj%l!*$u@6znv{Z#XXA!U2|(@>Ck<8Ao0Yw@p~WLGuiq|~H&YfvnBEI91fx@wd!Cnvdvf^z56f?n8EQ;a>*f!rKc83Srxn-R?~ z=JJ~0`g__3v7+b5?@+qT!F)kys5OqV9*y-%CN3z!5_pV%-}N; z^Z4duVt4y6B4p#TAmv=Tdf2mOWKPO_A#AXY(7N?8y}fBZWD9S*qcAZR8M+Wq zk^~kppC*fqX*i$5R-jOw>9p!m7=~f#n&#;=<_AGvvGLTwfV;TF))C@}$;x5%&_ZgD zbVo6@$443Njps}if0w0tZnfUIwugSCmmAt5w0TY+0+y(qz$Fd z_(aL_R4Fb@tz2LQn$k@`hypw$(Hh<+zxT*dVP7e}fJ~AtS<0L~1h81f!p_6!j0?0aiToq+ng_KELON`i^CHD4aqPYoHpoP} z><5#FKMZLLDvDt-2~l;k^}e)pknfDgQ>Ks^lJIi_z8kDkbGf z%u?j4>Aip}bvGIPk{}O;K-IM-A(+$?$Pjrp;JZEB*R#7gYog(BJ%4L(Ct(aRn{!N0 zb^Y*R+Y$uLtHYswq74}RT*&Uw9rW(*_P*h0BpNeMFYWrf4^Yo3t6LJl9lIYMH>3ID z?&fZ=+fvF$O5?QQXusobV-6yW%4)u7X<)|SpJ-I%na(CXY{YS~ zoKZt1kmniG_swmZnMJfF*zOUmMU|)GTLY-ETZVBBs#f zO*%|_27o_UJD(gwP@q!c6NR0PQ8qzAWNjvWH+y$;|L(8;ilT+Xht2!{@1Oqp@BaRG z|M~B#^*_z)-7sbTI6~|gx%em)z+N}1_7RIfn&2%v+Lwy zo0;NmhgHf@LDo(vLprg8ervP*GBWgBo{O1jY8WV-CoClF2j!!@mHHV;@T1XkMN0q= zj%Ql&{EXB{0q_wBo+K<$@K%2Wjg*7|n(K*(OhLLcN^~SmNVYd8Em@S7%_+ugDI#|E zLMMs#l0rivx?n+=Kw>4SG$}Z+h9WgoHfc5QejPWo=tC|V%r;NwDF&We!lxmQm6}DP zdrz1pozL+m1Il%9GOUU82wHnuk~n2+XhDp{2n+<*dqQBt zW*=j($mM(BhcWh0Z3rI6_7Pa-NI0ntG{RbEZQ6a&`i3HPK1KP_Dike2P}fR%(Zeb$QQH1w-6W_*dldUoT$Pvhq2dZD)+yGxpTkh5%b!@{-h0>XmPdFo zgncOa@&<=w`LBx?22?Op-6t!YG92}p`miJ*^N){~#fDr8fWiv^!9Hx1t0aFSxrrZZN&Go7>gAr47yg!=o(xOw4Ga;Pzj=1UppRw z58GmrQwrS{AR^}LF}APO{mX3B>JaDFJ4miz)xQ?vr`~Uo(Km1?++u^tWW!XN? z=2l{2tN@F-h-qHStpMH9FeUkAQmI%$GJLqmFmdv{QUlVzUuFcg<%IGP9wH~u=Gw!m zaK>pSKy;(~VnHF@$q7McwEy-hm6d|)j&=%UcX91zOKRiC!UXNHW zz06YQE8m{365Dj)vGChGg}yAVnQ?_%(Wnj$Yx_*GzhkvOu8l|O=0>6$7Yuu&N)h_= z3L22K)WH5Q&**O%6(;VfuEq+YcbS~fw+IR)Bx!`C5#TJz?>GKX8SrOFH+67hhHtrH zaaYAD%cLRMc)D?Zm^(aPd(@@@6o*SQS5}K2!C>?V7BrNnE2e3Y)0RFDVt1a`I58N& zDD#^XRU;cIxsH&^EwMj>XRy#<9l|6-Or@m(g5<%(W!|$?f8%sB&@>OVA*rWi>7yI` z!*R21gsL>Y9qoY_C;)+FSLiWB$Mc%-3(9T*fgDgg3TYrb!KnTc1(}-Hh2sr+aIok2 zDZS=#Lu`^D1ujPqrqhqnc#c^Su=QaK)+s;pbkC&Epts?@c+gUK1dI;cewxE|VQX8J z3#x9m*79b-w3L?&@ZC|gg-=B4g}}Z3jafkE`YMehmN;{ux9CY6M~USu^acd(7!% zF+~)J%O2mqhtbEyRD;4LS4!XEqS&X&uBHtDJM05WBvONH_)PIQ2LkLUNUyH6^(5DX z{KIUY-YwpK_6gv_)At05cI}*2?T4rRns1dN9Xbzzh0Ur2BUFkcEi1 zKxT)OXR$=urz8kH%80zLL4PUX(m3R3Kb|<*Kl}kvIM3fp2x@O;`CK70dUhWn9>|}g zvr&tOF2YwzDyaxhH50x7Cau`>geg{e^Jdww=2M+1f=XN3B1-g40w86< zX>Wn4r;tXU8*bsL9nVMOc^nk@M>u7aB$gyEG+>kbX&Pe9-*v=BWlkIfj9 zFABo@<9!00QrM)@VLn#3!`sCT&23DhVcpHj+4cZEFr*#dov3eG_3|kO^(Ohr9ka9ijp5OoWpa1DU{x6Sz|Gy5_|1ugp z#(@Po%eiJCnuG;n_2ND8EUA28aV-0xxgY)V;J1W+PYYPGGwKwULqIxbP~;e)!FWbx zDg7TQa^a|FH2#T0KX8PwBcu#zVvtrOoUde#8RfXR13`cqW5==%nxBCHxt|H485v6s z1=H2U9OZB%D<(aIFwMy<%LxBbU2k|U_^Wd#sSeXzr~{y@xSJHzcnV*b8n#)Jl4y<_ zYGK$WYIJn9r`f52$a2Lvcnq$|9Yo!x*!Zf{jc^n?0SR1}q(+%BE8WdFhRlZtg)$*{ zVwX4)j0ZexDnbc4u`YNrq%5FeA9)fki@U%Zk~W;WsaQW!T%wV57*A`2(pp3{$9Qd( zg6>Fpg+UsDF+7^o%rkC*?(!d+OWZprA@;`sY7%UUrf@6#?Cb`5vA&Q;%yX}l7U&M) zAu&Re#Bqa2;}(Tz(F17`Ym-cu5`NNe(Cmq(z#~Y@nTh~jVFr1Oi8d0Vi{ZR{a)Bj9 z%{Z6bJw1fM?R^+Kwd&c{Fr{cb3_pqvh{WZf%!DmYAjb$WWiGz}Hzh&XdKzxP_!O*x z4{c8Z%PB$Dt&vBt$t|V7ftxLzi9GnqIx(Ia1$rQGoX(bG4wIWviAx&$)JKFrF)e#L zSK^`MKGKsYFrdb;IJU-Aru4KyY22PN3U+iTnOYsyGER+zJ}>3fG>RYToj=Dgds=Qd zdL<(m(^u@H&86~`Bh55H584Ke6WA4$CBTDa!0iP`1owvjxDy{a>&tm zzM@Eh_AB>yH*nQX>ywyYTvl7NUD|GV{Oq&WwA-fp$em@N44uvAN^3H1d;%%ie%9>iLycJP2qR+u9=?Didh z#PDk|6>na+bXWj-g-MH6^V;li_LaZ=i=X`M?~bl7Xq<=wzr_PgN-~s$TE^|M6@KG# zR!;wblfZ5f>gLD3G*2&dZdZUygQ63vECin0e#^JUpg# z$t0`KtpEl7r96i@wi_oTI`)xk*vv%M4D1zXI~3#;0nW4fv9BM`FsttkyXj8G70%P1 z2YxJw>gLzk$B)M19^0aPkkuh zZ|sTk445{ab1-a8MqS1)X+1KsnJ6%27-uZm*CB}K)`C-#NZ)pY1WhWlfCk1UsRW9} z1{hRSBz+A4jJQzks{kn>laQ)%HS(`Xs5s#?td=ag$N^j0NN0$-iY}FO#JG$_YMZ=P zhZ-jjghpQ2%lILxXx^rX7P|&;4u%jRGU+@FeMA`ehNVhDo0OM%diHmV5Qg-hC+AaU zur25?CSH}32r!f>(=mqH;L;F;V_4c~2>fS)mB|&xt)cmehMHZ36RW6`^B+vKy-b{I zS`cD~{Q}Nq(>Veyi&jAv^F?QwiLR8MgV<{Ccc4qfIQ2l*8o4N2DMa>+USszGB;}E` z)$k>~=M_|uMQ)v>75RXeTt1+hUY(IJ&|9t7OQFFJvayMGd0N~j?my@Q^*`G6;TZP7 z5}IU$(npPCDf7WgLVMhZ#t`LpxpZZs=4;OqD-pewHO$+|uLe`EM72&M8o5#8?fh!*?7w4T+ggEVYoRD;0*@QvOiBYG(xylwly*{00;?eR0JXEu=nQRC5;YMf{-0n&NIg3 zxz=gzfM~N4dZ2MlDVf9cV)Eq4DYs-fe_*`C>acanF0PWaZ?%O!pR@=Qk~yZD3heG- z#GFAeIz>PHbu>1R5q;?Pn}D*$cdgo}H*i*~6AfK$B&*z{`Mkcp{Pf#D|BKJQ`lar8 zM1KH*YT)0pu>335f$!2ntB|rXUU>OBQLRnO;k1;M7>h)+GlC^}5#dLpU2KHys?r=6mx}`@W73Bz$W-zV~yHO-bfZ?C_1;=v)op^YZ4i zfB#2+^uPWO>+k*h>3E&_AGoFHMS`o@nn#R60T@@P)+(bG85jbBrPwP9t%PL(&^9@Tz?pi2V(&TEp;gIiJh&_sh8qd zk#xtAeF?#t$2&~To$igqWn*CxopFc+*J=fhr%y?;)@7iwwK+%n3CLMB&cg!BG?(%N z0%C?BmS<}Il8v2+g#Dy?D(}(Is|x|7i<@>X@d?vVhMA#NLPH;@1uzvwWj&~m>Qd(8 z8NX%QqIN;}kpwG?k(|~>6oU8z>%{z}Jyk_s8Mv@(Mm9-LQlSEoOirPsK2*^rTs$%1zb%nkqnri@UDL{fEXOyX=^H&@JE6%O=? zaAT!Ku@t44ny;%NZtPDJEcvu1M%b`{{fFd_cClyT@`C=$X-10XZVcC{$ z&%%@m=1@GqV@6ntV~=hj_)odN#aS4@c^h0_l6j-LE#Q*YkWv@S=B*MgSA(mjqJUN( zWcHmzvp|p`6)9Pe_B>aZ0bN$CJgSo4Ss!>L_*q(hG;}5Y@`(qzD2_PS5VxFQzHAvI zt7M9mnrU%_B?O3QmUx<>bKy?NI2NyVm`Qj;82ih1?c?d-?69HcN7WXcge|QEFs`qO2%!zcy$CQBc!o+&)%Skxh|UoNNI4DqeMS@!7S@M7n(C0u3fy-f_y)rSongG+ z?DdTBeK5w|k`?Zk_s5fAtW6!aF#t6#O9Pl!-7V9M9#C*+gJx` zEzqgdYapNPa4!g=yAw#Pv{g96i+wKwEYc#IflkNOf}KP!6&4G1v1iYo3epk6FL3nf-(pi+ zwxL{L<2S~y!4-A8AAI?vpZkwreDXoNH_=T=tU zK8#16su%tbCZ(D<;CZ5rVKisyl!&L4()_37>k0ucShyB}Z^|GPHD=2?tTNApTRE-J z7*fb)DP=xO1u_P(4m`bixbVjXB&usT$ur3Ywzz}3y~kd1TfZXdnpVRJN8$cp4( zxs=wx#iDG01UZ7Gl0ZCXoRl*xsWQ=P4=~_F+){;-KvD6-^$TG*QSf6qv1FByHLNvJ zv;tSgLYO&bNQ5n$@p;j@vub9Hpm|bQK5SAqu8EhwE~^Jf6uixucD~vePmmONjp`Uc zF$qaH9s)a7!yEx+g4XH=$Sf@!bYT!M2;*_T-7MtWyp%#X#VrNZbPML-!oA)A01jR) z0fj(BVF7$Yh%3&uQZm>+@t_orwFVDWq+7I0L41QY&94}UEq9hZcqw4nhKhKV+llTf z%E};%xRLz141(GNuw=E?qa_v!jH+1d9GG~B+&w`Ipy)EU2BDGWEL3~~!0Jf`x z{rInK{zP3?!C~b6l#e0+=FKz!mMbH|h9KZzOLvr`C5XWBQEE5#2Otc`?f{Fm*`Ca9 zZjbA?q;10?m2awe&D8?l9Ywx7286e$w6fF?Enu>*zK;;Z7m%3A|_23>kb9f%rFd4bs*&+1+GdMvVQTI@3!VFW_$ z4zfBDwcj|m_}XxJylU|a>(Csq3QUSe%vXM0E0T;lk?!<+z4dZI+Z;g)9BrNq44MZdoEeMPX-$U1<$S)pLj_AcexfjtG8l!^tH@@R^kuVR}-Ns#XtVTkM+kFa{oksc8w+%mxf7s%OCBylD|R|4Ze~0*u!a5kxHlZKA(-;GoiC58wGuEx6n&U98BH(YHwt8^;?gar zjHUSg6-Dbf~4O+5t!@iLqNi9XS!D;0~U4RzG2r-puSx*<11%Of}1d za0yqJW$EZbUG&3BGLRjFEcfsW9J8Uo1hI7ZuB1RVg9g;YuY=7}>9JprvZ9laDM~0l zYFYpdmgl9Wi?cHgr8vXP#tPEK!KJS#ie?Ir{Wld)5TMq&W2WJ@@I=fCP+{!!a6UF{ z0$)2q&XL7_IsK4&zsgF)^jp>ww?kd32KjcEqA@(8!6p9y=~*3Xi~Gf4Ik#UPSH{Mt z+n5mp^JU|>Zj%p8CM+pt8DcHaZ&1-a*fkc30XC>hVsmsu%+=%}&{CXc5c`*L`QFS> z#T)<~M;JTL{gFNFEeZR`OSj5fK}c~uB@Oh!rwo=Xu!4$Qe}WDv6dH?EMZP>#eT5)> z+K0r-g3;^U(3;tblc>R{#i3s5XX?p65zC#UU`PcKGa8Z!EC zr7kFua<_x}DO4Mt1@3qhn65E-)D1P^@Nzx>0beF>E#8ENDPvd^{P( zrVbCM)%fnu{q)UW{=wzh*?fIxu)hW_GwNqg;i6OE?!k=ac~1wuIw#C9!UaGr!jh=1 zIdue7(c z>3=~$jm(w#cgkC#@{3XEpbGRE8yq>ntnI{Pl@CtZ=cyXGNqyJ6E9oHJAo}udH#0)h zJ`dwzJ!I%Baiqp1rAMMLoKwJnHo^<~00_s{5NDC_)Q>V`tNed1j(fRX)Ga06Q~GnB zebe|lA7TXH?pYlE!@v2rfB3)r#_>nL->WakPkMmS9#3H}Qhioc1|74Ed1iEZv8)*0 zL>RP#6?{8m0U)$i8;{!*zDdqlZvJ+)#XU@z_t<4l6Tv#SeLF(wb;QFSTB1`lh+a?* zZ~r@22PZ}XOA7J0C_htcX(Ii_O9=bSIrA$9_HkEO7tR2N6$v?4VE`4wdGyE-IQgMb8?bnt3uMj5Iw3C2 z`X>GrSH|2I6WDgb`t?97qxm#{g2P-cZVQII8F-M+(cMowa(Zm+e={h_ZoRDVmy2!wxvHgYxkJ1 z6QMrtgX;k-+VirZBV(AdKObApy*@9|a5yal$pe$CWq*Y~lN(VyhTAt-Q%R`=D6;g* z<)6(->guf7R)0in$t(Ah#7jf-&F0jC3F zfXq{={`zd$d-Bzv`H5fnh3U)ZL;(ubkl136bqRrOQgtiqQx_m{bUncPn_acgVI$-~4q)+{ImwC`~G zobd&oW)~glo%3tTGMDbOzvpGXn9J`>5TH^@D%-b8KKW$_{hdL5b9G=8^5M=7^+Is> z+-L>gABU4K{DXh=^1I*p?05g~-}#MyKED6`HpCZ9res~Nm)OXm26W1Ui*l~qraSbB zHv9Y8_Kwo=;e`BfjCJa1Ch3g<52H}5Mnqp5N&hP<4kQ8418IM0w}xqylOCe3frbG# zyjY<_cpRa;a+)tC90kLaK_y8uGiY`d42k}F;#GkxCChYH6uDbEZuMpC2ua8aY-ZqV z+AjCa7{VEihRmv#<^*^!M2oB!lO(pyknNEfL1a86r-g73xZQA_aUFoiM)H=q*xt-p zmYwfpsjx;%bDe<;_&bC$idc?ZcTkFAs%%xuiW$`?-GrwMKq+JeJJe*A%Ay26QcQb$ zhy(1D6HN(w$ZIm05nmTG05g-A&4&^u=%}cf1M!%`^6aQG@^Yo=S*5}R%E7UueT?Fr zM86c-gSR>}1qMM-5iEhtCGIjT3^ykLhPFLjeI&THx0?0^KDElrnHa+HO6nO>xA2oZ z7W))|H_2bs(6Za#v>&m#P?LJQ-EhN{C0?}O^}v%+kWo5+F0lx2o`$aEmb2Y@hQb2B z>kL52SO%U7VFmPA2`yYh6TzwFZjMAGMs>!Sz@D~qalhi))jirjbSINT`xI3GSq@O) z5)6_NqBUAPj{m0*6Kl^-Exnluhy86P$UkOp-tLH}6qgYs)T~fh-p|cblJ1feCdG6d zNqNiXU!k%1GAT)Zu9%gmf8y+rmQwqvc$=PKToe^D;utg2%&aYqJ62>OFdMdu(Nj*C z?jI{Lg$$Yi%EFBor6abPERR>K!v`hZD{Rz~8hc*(*sMU|QIEoN!>oVndh`j+#i&8n znk)F?hwZS{8&ef%92Sg5x?2M6k48$!uvD4p@ZS4RH>>%p*Q*xVFh}~^Y0_FjuOhT3 z8?x-4E9YE^CEwiKw!38YuJ-kQJDHBK67#o=p0!YokhM?`{SObDCG5;J`e*YiGzQr? zBe@@#QMg$7Lx-hxsV%EhJf>-*-&_zva+gpLr9#6Af^wj4)9;KLZ6J-i)poX8lc6|U zk1P|>kXDz*zSC*%erccla4@?6?Dp+Ft@w@JWQsGy=gl{>%ggia>(5#g8j+F_cXDVq z_y~|^hr)O0=k(6fkJan-6s<>6QZO^6_mk%BV%>T1li&H3e{lKo{l;KO?X`5VCaiXp zLISnQ1wO74+Awi}WI0QM?8C!75u&TS3b@$U+#OtUx_C6A4rIvybnKRsQ*?UhzbSX& zY-<_k>P*=-p|l43Ic-uU5K2^@Trld`Mn(QxxewNsT|H^R4!N%39GxsZwkhw4s*8gD zDrzL}GqTQ_)TDd(%%*X_sambARkVxnJh^1l&3JiA?!ar@0Ln|e!%t7k8i#gJ;YoLV z(jKrU$}UZ+n7d|D>oYy<-h>2dam#(j|KVe-$UzZV{nfwwo!|c-|M~H+|KDyK&Lj`c zzzV2Pcn%5&9&mZQqh3Q8hnDwAeZZtjWPfZhJVCo?b$qb59+?G+3^hbX*&BphT&na- zKF5<0Ot#EoH-zAG+O#d_Si}YY4C04Dq8uiON76-8nR=8sMmxTF8(t#XTqkM|s&ArM zf?yT2F$zw08gPA1bcc-La{&UO2Fjo#VM!Ya(+-XL7>5a{__BrqZ{&c?EZ2sQ%`Q(c#U{ITSvS46G9kiu9p-j|HJ2$?=Mk=ni( zGSu&~L>%-2V(HMp0Bm}Cf^ZvzIG-j^d5=|16^6ir(WD4aaUCZ9u9-GGP$C+oSA{*NbOgqF4?fP^?mk53>HrdNSat~0C{4`xhwb2W48nRs=I15uMrTThtme!68Bu);O8Y@HvcZH~gls-I z>-_KsKcF{^<+-3o7dlr1@kwd`*2L*qha=D%0@~Pa2gPUM0ftAAJvGh4pVEH`?G0?Oe_hqPIsyOGV_a*EA_#oi>`yhu5zW=IqW2kU{&SZ6oV=eSRyi! z@EDZfDHaHc-{e1}z~BL**ucMVfI&|A0FyK|HJtpW6f{3%8gZgB|6~&i*keC&x){FP zCHHAEAP460VeE-+4PQu940alrqhKDMINyMmQp?)j)sJf}RP|%MgK;)l((3jv|JCpO z;cx!;z4aeooI8JuZe{!GfSneb*Ke0A8cZC^S34baAb;_>+}gLZC2Nwp*x491DN{wL zf{Uz+N3>5SmXf?{;YeHeOZCM{2zYIJeI24?*~{VW!X%b_%&|ENXG>K7-B*dnlApXEKK+4*`dy$Aw6Q$QCx=c~%+9O9)8sKkf|wXSckcP!J(+ zi);{OZ`5qH2~m10M*qZNEW(573rKJTwU>>lMyugpvM16J01@yRcDf2P4tYq8(&v?m zvJ1mt5f;FD!D11GEP|R%%D|k0yl6$YMR0X%4joA-OpM5iS3}COr~FvE5le5h<$p73 zBQVv6(6muDM}E5Xh>(vaQ4A3eC zf*dsF_O!`)IlmEgK5THJEii^&ztg1=kdGa64HO*LtNFuXNmtJbBfGj|08Y0%falf4 zI{^j%f-~+(A|HF5?Xf@zhN7dcMue2wGK4*F_2GC#TLhEOx3q20m70j2 zwLI)JM}+IJTxZ7{_%2`xG=!}-SmTes^YL`pzWIyKKD%Aln%m1~=U9)~8~QI=7^vlu zHi?a`Tzd#A+valjiYW8gAi}y;72E$n?DT>}j038rBo>%;&Xef@3Io7UfMK&ao-chYPfjRyTl+l6mAof?F4g2j)ohO++kfvL; znH#;)7yjNqFIcw)z$i{SW0KwqAswah9Q_tcd- zrGo2t1WBi^9BZIyAwqxx-)%n1ESeqr2p7ZN@Om>6&}o5IM$5B_Znq&C=SD}M;j9|y zTrWjaf>E#o2Tp+BY}jVF7n4M(syGpJVz~#B9Wg<4S*KNpT}JPnR!%1ai(2Mt)+k9| zxGmPgQ#Th+=6AHF=olaj%;SL?QTYz z**R`uY0$Avc%{0!11SX*Sn)R2*#im|L^B(V1ml}%iWm8fguGSiQvx%?#yB8%xR_5I zln6|Cb%Aw0uhS&GDp&gW6*73-Ui?b@Ut094?{!Skt2co4hGu|q%v_bN$s&&L3p^bz zONnN^`(_F;G2B_tUoEg?ZiQ4FF!S@zs;;NF&kPV&c+cmkg%pqS&EQ%U-dirhxD{kZ zV9nbDyQHySeswzj`ekoSX1`q3ng77Cel^w~;`#azVxWbU^ z*r0~O#BR_+f%PQLDj7vP7ai?v^9@EqZC~yQ1H|b_rAHre$i`?QJCudGCWkw~;#tFU zfvrQxI_e%B@wYzAR}b^Wj^Pbvh{w(4#o*wwq^d7ihOg^J&WJht9;Po8}B z@BA0PK7BT&zLiy02y$6JiOch-vqZC$T$F&}GRS#24Dd5zN@JQmk7XeX#eD8gohzzY41ID4suWv!SmDJOs(a`HA|~|a0=(8-DVTB z8m)14T0^K3@k~?+d9>UDVt1neJ98XfseqfE)lk8R{-bCk6M|c6r5=Y7DQy!w=^%6s zrTQZhis47W88=Zy3}i+6_4o*ew}8I_pfJlCxGdZb2_P7KJrj40-R$GG7=q{4l*1r1 zGif)boT9~c2?HdcIYG9ORaq^18c!9N143yWnN(#KLF1BGCJjF3XIvp1S`t6_<7>lD zdk$G6ne(Aina}|Wt1MWqLN*IV-Lf9A4%O3?Lr}#;4hUyz^b>1Mf0JI8PywA5DVMHZ z0WBrY3-N=y>3V>GPQ=L^X6xEeF?o8-83}6Eu)NA)Kt&n&0F_$(@jUt{pAhhHM_Bco zF7S;ZkX>T7+Jw;~+DbAUDeq*;JQc;Q&5CiBt`H*%wd+LY68CXc_V#kOOi)8WOLJu4 z#`wd`2J~DHpb=&MrgNa>X*)546IL?YwinheyLW@mqQf%XY+kAC7C(sOe?zP_-z`wE zn^Sc%hCBhH0-f!(5N+Ugwg&!jgCXj_?6mHnG459PYvG7`N?_bIh28uBcs#0G0|8mM>O-PjUzj%*WOP~QHFIMHzkoy zuf4oo&vxi%GK*nHN{4uYqvxrgbdGCg2O%dmK}NR8#6DU@;m*(k!PcREcJZVS@$P=M z!V0vQrqCa`ZZiBCFq18`qS0%=A0f%z>l!{2+T$Gzl$@B$M2c36p-!Vyrb2Xw6sv41 z+@tYvyk~mc3Ovv`y91=o%xrhF#eT6_&=R=1o8!X1@smHHlnAe5sy)~i5tO^ZVFsI8 zor^n+X#r%LL4MgaW;525B9rVwC92py_JJrFz?xw^P`9TnPA1*Z@59prfQ00Hvp@aD zfBGvQedlY9&VZ{{1u;B;!fz78G}$nIE*FlIM2UuzXHRyapB=y zrYr_pvMlPbKLyleQP@*eP1Uw5OaYMuK2i^oPMXarAYwcjN9bQzmNaZgT+ljJutCuE zvH&w;RKfqpm@n(GjBv`jl|7a{QmWp1ef|yzpB?AN5cQLzx$rCnS_ZUESnR)tc9Lpb zOLKei6VsO;k*O~K@(&uvH);Qoc2Gp-EF5w?!r=m_CbKmW!H{*v$L_=&sZbyuaev~u z`t}VppX`ei783%7P`rQpr$6}L|J(oO{Fmkt#o5^R%FHCD9Z zhvYVZmW=KdhIna14|$1YY!f-)Es3i(*E^2ZDsdaF59ei&zhr=py3ratOAl5+C3g_T zGuD|LA{9j(6Q+2#qZ~~HNH*1_BO6kw7BlKys=23BCY>blyb*^WIxB)Xi{uGS>tyUO zMF!;@lGdmcQDPzSMl7o)o%d4Z0>EvSHCJXPD+${j@PGBd4@$+y{0AWt8CYtS7vnDZYIP#2U7%$e&Q(HBccL zv_ZdZ8&or#Rm4gc=%CdYsWuih9Ril4fmJi29khl}o(QT9RgJylg1~(TqO_7?^t=}A z7MI|5fELKr5$q3i;v7gklFU&5NB*l!S|RT^mz(7cD=8epsB!Hn<*8<_XDK+2v?Wqw zBO&*9J3#;sa|K68>1@(lv?5c9Gkvxqo5ThWYxq>6Q=1(ff5n83P8R@{9@aHNTZ_BJ z?6wWx(B7f}G0lMa-MukqMs={>;VC%tnRaP~mKu%4-7Dh_r2A|t{2Pui1-#Nn=jzu3 z8rjB^g>6+Dnm@kv&|1wcStfC7^)QR zq5~1QC$vp_rUH=`jMWbjV)3lPch?l@(0;rtLq!TffC>QC6q$ghBv~wr^d$BRox-N= zvIP^oNKWJf(g?=jgD*|`PZ}prt^Pky94D27T~4gFULbs@Js$O%lhYPlPqS|7ddP+H zU;#1h^lPE0X_(5|Mrd(79F2dt zZ?Ygnd5yj!(qR#hPsn=7JRFRyYtfV~A(t3uV!%MNuFX>96AOuFikNY5i+4*X>oB88 z$}WlaP}vlQNTK=&UHljp68=gk41sx5Gb}9+B2DGJfe*gaO2&nr$NEe)1qLy96ecRtO-t{ay`7>b)~w6@h00{Rg=;|F;Wu>;I#A^8sqwAAPT^NXx9bk)toz?-%5HknrC9h4gDi9@= zyj0Khp3I8E3vz*WDv%J3^;jh*aAbTEWm@}lVx3t(s>M(RG%}P~=d)oK$s9G?W4&+-t@CLE11RI& z?Ln7j!&XLF7Ti`3S@gYm6X)q{D_b(~Ba(Ot*P`H~qLe)VG-;5*o(!S`*F>ie3XGKs z)&S*^-Nuelr85Bx9FQ8bk7}+FFVV0+&&eZ?phu|r-*kyTug4`IrjI z&C3&%$J<3|LLFipn<@;t)Ievhn8HcdfEA{njDp8+V!}#Sb%JLDvvfvYbPiuV?VU9! zy&ovz^TE!rX2K8bJ26C-nD z+gW#j0wkh0$IhgC`Q(B3&4oC=)7)B_L`)73l|)pCsM0#bu;R7sWLSJutLZ zlabx)+DMOgEZDoKthmCEP=qwiVM;|OH2`5lgR6MLvKXsm(E?Rcu`J*!qAhI|9Lt13~O0#;|CD=yUu$7B6i#K|(10OHLIgWf_q-Gq28c?v2wBE@4TWeCo8 zeU)DP@ECP@BF`4r^vz}>lat=mCWEAgh4w?1d7$Tp+M%}6yJmh25jT9**Nf8=q}}DTzBW!Y(o4LIVt!1|>=0_9RYUinq6{GUT(JX*ViPxLDg^qGUQ{Y8 z(PyCw?`U!pObQVhb;zAFQoTr`BPL2bk5!*#-;K=jR|i$U!&X1G%_bq zI0Z#{PivG0=UUqynUaPWPXIk&(}a)2?ZZUF7z@ZCKPSxwT1jj((6UolL=oGZ2lYKI ztXKq&l(do~k z4q3dnUqVoq+rwknA_?m75Y8FWPwAVX7VrQ7AOJ~3K~zu*1gcmY+-eB7ww=i0 z>EpUx_o`^M>E3jPFM#<$A$4~a8T(klk?io)1l+ye&}zosJ3ptxsBn@@$WCBe;^gd| z=wfH}<>W&Fjd94ndk2U5uhMrFhg5vA%cpX4F(1XrSpgL#DDJcrAER+oYMT0Lg#|qk z;{uP&rjK!nL{I{DonOURNT}<2C1y{Y=ApbLzQFS|@F^}gakwr2{G8^uj|Tm3oHm}J zTGrT|iYx6w*>OyQ52|eV?#Z}&Htmhz`m^>m1O#YQ3P&p%3D*sJRhamJIWZ17Io>lz zqS+Y10%cOfn>(sHL}G|El2=|ny?*xI`1Jggse?ayGrPUtJf+>~?>LM9jVWO3Wheli;O#!PYQf`QaGC$?~L@ELE%ckCGKchZNPO`G59;mwQ! z<%04u)~eHQe(>S54_{pW=#Rhm`t=Mp`D(UaZr5i|F5AP=%tqGU#fL9h?inAo9bhaL ztML0^1zGJ-%!XS8t(nmWwdVBs>5C7qiOGQGy#~zR100`v?fEx<_P_kEo__p=+TeM6 zcqtWY1KrulIc!y3PxNEW4Sghh;s5BF)uOrXq`{E_C;=CZG}@xD9+YUQ2_h$f%s&J0 z&<3Y;O59H%fdwvIAUvS?s0qP=$Le`%r`)Q%ruG#0Fa@ z_ap^PAWqLSZ6Fz=r%X$zvt|p~Z@w(;ytXSO@#U_%&xkRA`Q|EL0|S=fp>8JtFM_!d zuvMju=)?g#>^V^T{P~M-e*4hB`s?p~f7IQNCTPa6@|~;9UDiWDEAIB}hZo7iel;8} zVp;$tP*RB#F(Wn_MKYfC+}=ZZ95)+ufxS&K3Z}Vde%> z7E8&l0k5cK>=08wIcwpe7ldlA{?5VKVz#^_faE#KBJ8$HG;j)NWUMcO_ z6=wU6c&nx}t2elx%OVZkHq0kbvJ9kzi&Z!Zgc)QeuhwnHLz3Hv*c>xuLjUHr;kbw> zWaogy*skbn?8A`RlHku8cdEbe738l)Ra&HlLJH(Bb?V$j83_Uufw(t#tf#?7DuXC3 z^s*jQ9OvrH!C(=i(6L^c?9?FKwWnwF1uEU+TWCvz%f#AiQa3gIO*{}^RA6T#KyWY{ zVWC0T28km?@uBn&B&+_MClVCl|CjT{5>x<+rsH34q_da}% z_qv-cU%kD-Kb?)nNGY&oESzDt?8MqrhB-TfI(0&`J^lJ|*K+0iq+If0?_OJjvf6D;p8RIS1 zszZ7(f{Y6QmII^_%A}&!1e5@qBcW zfK{?dp&{IE``3T#m%jU-|HJ;}IXp%H6eM!%+xt+L3VD*m*SIFGBMp4o!Q=`ih4?yY zFIYMsfp3!UEmhF8W69uY8nNl(w+h5yGa?6|(TFT6ncPs zEXeAs#L!tV3Ga-PO3QkQ^{VJNOD-O+60Hq}U*f*BJ|koD=ARRP9sJX1@OUfuR<YQlDr69!zg4b0ojrR3% z>3A^?!-F36TTI=+3ga#it+0Ji2L^p#3ZwSINF=pHVo;1QGYv53crLmSlFO=FK?5%p zJ7rIlMNcBgPpw+vAEmq`eTC0z9vG5E{17KkPDa9oT7?>Pg5!j+Xiy-%EjM)~3=2j| z7VItOX;Fc*dX;5j%SqLcZB^UOKgZ1*pB3;pMn18oFlA^6gzs%E=$DsBh^+`lo-8UD!GO&p;(icSL?egYhH4;*g%xD!gt6(?^d)AyI zhi#eL?Wu4n=c|&bv9*JoFj?!x9GX0KzuRpiy;VlW5}td{dTI+d+JjUmQfF~DDfel$ z-a{KeSYaWCPTk(7&2b+^jHGr(EAF4qoluDqib={>`6jnmUz49@Ysc-n7*h5#W=Wo> zx6wluS61fVs^Rj^2;_Mba}!sZEt6d8x2DFLgwPdQC67C*K(L-s_$8n!7=5KY}ghv}JK{ z!EIB!28AyuAoMKIw?r!voGXk0%ub^m`CddMFxCi^?4>1=sUtN%Jw?3&#g?VhPEVHi zG+i~j)6w%UUcGq!gb4TcZuZ${Ow4(hbOy`YdGzKBL6+AwCezNOhnguU=5a={hW!8l zX6j%Mx!U@8G?<)?uu=exfUUGApbUe%rTizuiJA?vdvpUX)_3zggM3#rDww;|VejRa zF9$e6p|Rg_hOMd=G$*Go z*ylrljUjyT;q%j8i|br1!DM7YtycBB`tzUsC;#;qzVj1ErZWGwL{2;|-B46IO1BaJ zN*sf?V`Zwv66S)cITFXyrb|qKnA3sz8r*eCL~A&$E97(5RE&HQ*bS_Z<$r-5O9G~U zDzQ{yksw$I+JYZheJctzne6zxZ5(?&+AW*+nO62^j z0vfaMh|EQu;$cEJP0U{HDWkZu!-8}qQ*?)AaToU)FYC=pF;aV$3Pt2ef)=}~;{_!` zhaqBF9>9FRY(D?A_4H}Gcd@^l!F+)+!o?>7ND^H7X6f14FHv{&;|QpYp#4DAUz-Ce zcRJD76}~LQV#P*-t4}{&AI|^ecmAlhV=_*AKyoPVo7C33C0vx7MjC}3wa>F`LDO6c zJk+htyfaW0(&Py@Sw*$0K)5->!LQE zc_z|I!ST&F^+;q<7G%&;F(j5UZPTGTYX(!0g?TX%fGdUPW;?oFu_eG$TFfbl>S~nP zc6!0w-;ybDic#%;l&sOW<5sEaL0dK*190-eOZYS}*8z~mSxt0~lkO{Hls$KlafnYQ zY`|2okhr|Z;mc()+q=Vdq!vovudZtH3TJ3&_65axYM}8Ng>=bEYNE;s(;Y|1%xR}x zbU;SI7Lb@}p*=~k1L3aG88zv@EPf?&t|fBJe)X^gWm3ThR7rY>U7k9^;)rsmytIV4 z#sCquD8H(_U=pm7WqF?qsuR;1Z^&o4vldzr9(&RVH(_|aK+I_$yX!l^+>L7EZr4aEme9Vlix8H^~&w5XaO>p>xc?EZ3_GZ4C(L~4yCaC4j$=SF+9#I*GKLaag zHKU<*LAL>GdIhu#>(zlxHs+CjL5R&X!P?^C*uR{7;Y-iJQy67O6w57V$1djm{gaQs z`%nMr`TOtJ`V)jMt(7DweKhq697{e1-QQF%CZ7FcBIvYNfM`>Hs6D;O@cgQAxZllw zv|HV6xS`Ddg+;AVA~e7?(#2td;9_$uXGCr_U13P!0E%rW-YEj0B}yxcy?7vPcV1sY zy~l-J+*npz>UP-=rhwG}ODWqmt$isZm;BZXOZ}j%aO2a*ja4#4-*NW}r__2{$njy3eHN!4Hl8SxUQqABr-R_~lwBbv5FCZ8 zMb9O$3zcv{`%DMpY?UJrWfruT56baL*!V@>a~brDWl0xD;xc zN?e)=W=OTyWWud_f)zn_ISH=+QPfECzL#}DD~N?hJVe5~=s$Q(4(cO?l`o3B2lC6f z5`%uhNX%fMP9DAEl(2$~tttXb>R}UA0$`P(LG00`sG$}kHuxt0%gAATx;8DCTq&-* zmVIr)H-uvpthEJ2-ErVky1rUa*>hwW-l8^w(KhKDBe_cLCl%&lrFdS7)A&UK>0mf= z=!k%3H?eY14gl8KFaZGD3Ix~32HE$9jf6pi6n~F=9GO$Rw%R?0S>{W*R->F~HbuLg zV5bc7^T6JdI6}8n6=2>k&_BkBK-R5|}sI5|;Qa%xp>+z@qJPruD zkssCqg(1xbMR9MzLXmZfe#9Q$k_3|a;p}<}wFQbVyiIxrW;A|6w;8lwe)ROoi)+@} z&CQ!vukOfT=*y$>^+4!Nvb9|zHP50Pj@u~uvyw>WaJ)3I1BdD<*qb!Mk6UAA-a5Y) z3OUmK?d_ohk_l%g6^Ytta>~#gR>AFjwV3ZVvz4!sg`?T8*b^SH=Adcwtdi?UkYa(iLP(GQv zK{9r`9!|ghv)}raf84*itfP1t;2&+wlHz2M_?+{YjL3a<*153WcDVbumYfg-fdHRD zhd$Z+@2}qc)rRIF1ix|pMC`O_<9IP<>EI3&ojfT72~PAe-*N@=$r)ObkS*4*imA#* zaCx}=SRdup<;(E2oNRfKRzRniS3WjKr2dy(k_A?QoQd?}!!#g|(OXJ}g>Wwar9_ok zs`1l7NW?3uR8<~dBxnM-Jbrevy4bRc!kyg~pwB{L*{7Lcwp+ek-~Bam?NnIPXq4?0 zA@6eU!zoj4ATcK9cZsKx^_vj}J|cA!`l>gBqw};XwHE?!ax>ecX;(>ZDyBoM9ntAI<1i7USigDTl?0;Cu`PotPcq+$l>&@I?Yb{)$B_;- zA_9=;$IZ!HDiO}4j27wbm(~?|v&o4HU&kIqawRfy$Nu_3oVke*k1y)oawa^2d>*SH zneSMnAH0+1)|waYDAn3%q+0?+c)&V*8i4`<*oI;9DQPzSENU~WUF9m&9T`k;kPN!u zW2Fcc6oi}g0MEd287tmQj~Y{X2}%gRt%VcSH3xLgG>~!kQh{F5^5CdBbQ29nxv}2c zI0!Wq()}lQ+21xBlY!l)C0i^(aEW(=&%W~_kCwpKRJ*5`iO0A2T}U!+pd6$6t=pio zK>3`igF@`6WQ3KItK5~oFs;TV)X)p&W#xc{iKj(!+xiS)yA;nFWL2qWx!EatYqerk zN+zjqETa8sjF!8W85p+Gi%A_%My=Y19lT)u8ACYh`(YCbDD$}nLROwD;3`ie`2TD7@AJADq&j5EB&i8|jgDB5Jxjvr`K?ZK$GQ0c0uu1f?2czEQ zv%&NCug=a+Av(T&bNl8;cQ_bAHHDTQ7~9)AFx_;|2)EW51AG~Twy|zUU2ug=KVXat z58>?Q>4Z*tCjEl{)wU=s(5Jz~9I;vp4-{HP=TDxz_kOcGVg)a6ZXkWvnBvW#-PV3Y z3qC6al@LZ?QgPg_QGcJDP03T09zC8Q^Y-TL0>uW{s+dC*7TGT(y>u^TeeUcK*&pVc zJu(2?m+KeLwy?iY#8PmPVom+^Zt%h1`{hr6{_opXLbE(<##qm)G-r5HkcWa1fQj)m zWShrp@C_y#jFf&9hD)&@V3#d6_di_Uym1aX?$;m~VGe@!xWH6L%p-#NXnVE3>3}J# zI#E|CuDx5pW+4dR?7^3v$*`uW)} zNgVYIAxM~n$ZoJYn9R=GWS>!K&P@_39n8T#bIJFhh#6?5hoS+HDU-DRMRO}I-V_4l z|LT)ZYs0J8fAZ(;13ERxZYtzuD`rH_U9`EeN+=V!kU}^v+?I8AlPaB7aU|5*QcY!& zb}`7-Ln2Z-Rw3Hcs*ZZCw-C!OSQ#kk8LA$um>(TEMu#?C~oNNgMO^PtlkBc;nyL+(;r4pAEe1WhqoXNH`m zPZ2_o>SC%NOaIA9qN72z4qAsT60R^s&f!kW*d!sPF~dTGUN8=ftA}luOR>skj-=a6 zzmL+QDGQfQGAn{UTox4hEh~MzgBeKk>4}AsMkb$1GolJ0qg69nIg&=VrC>>|g6+y>HT>M13Pq=+u73a-a zk&W{rT-Mf&9r$4&z{1mr+x4G9iDE!OR>UbK%Vbw`%~iAcJ3uss;BS7#a8((gfoi!~ zDT){dyTsyce1-vK109vulwdcmE8eWMbjAc`i&yv2eIF*=ySEh2Y`AQyDqquc*0=kv z9KuiHw^FnHp?j``w^HP~Hux-+skGsbg?bccQ1eN~NPJKF**_Kfq zpe&6odMn7OkN^#P^ba{6lMW%lROgU&onc+?FjrRbCjztvLmP;V-uvK!BJBKTv6%DN zsqd01?FMJVCr>8VPtM8a=JVCtSF;=7P&>Aci~Ci-Ka@5_4Xkl8oYW@WCTx?{Vo$yW zAcWXIEw7B(W08P83{R0~sXHSL@p}Y6Pa1$etEEGXWYq#-cDtwNmu>Rb?PBrvv+eAD zJ=+4|(w}>NPH2CG=?8V0OJhq9r-Sp$Gnfd};+#sjbjTdaFRM8|k>y1z%-3(jv(j*gmU--k?!%{EI;;rg)CAN!ibh{_fD$C7FNwLiJly`*|5PV+$rF||` zoGe<6i-D@ebIvnz#lgyWamFFUE@yWXucXk^IYEH2;@wi3e##a}Ym=AQZ=iizn z70lG^^f!LGVL{43fit5eUg{h2X3dzxb<2IoJ*xi1A4aN@8jteOnMfDMO?FcsG6&pZ zUZ&IeG_$)-Up$|@_w^tA@n5!&tIRQnBqCfV&(L(FO=yERIne4Huu5OQr}TDcH?8?$ z2Js%IAq@Tn>*&!mO$VwIeIQ2VDYCTn2z;euo>}S;QaX}S^RZ`NuJVJis;-!Qw{>#~#B;tKDwn$AmVo*cfcrBbG zEO)EixOr+EB=|wjl4H5XZ4WXhpd-a>N;+dks^ z%t#m_f&~ZkpNSBB{5Ciii=A!>Y!lHgNDq_;Ch`nv)5bG9T-UtcIVDC~ zb<#)qPhi>Z5QDlPR$+IAaATOj@C`N=i_co7d!qt=Dn}Sw5#N~dT2+>NhY3BptJr|D zcM}o&(970|#+3IUnQXy(9^IZYR8@$X%~}qd7lfV??^!ay66j_}8-P4^-=`?zUy^8k z%rghw%AO7O>VJiA;8TnfPYB6g0-(>16!7C-`$hY3M)S#G`|Nz$ zX~46nkte}HVU7xgd61Jpyi{wq_7m+JcZ>VU`G5-UaL}Hf_s0WAB!L`)gwI!ZqYw);OuJI961TQZ8)5!>Ib3Wg`{?Q#(J#d(VJ@WF)qWX8F(tLU`M9rTu zg`iVQN*C~_LADjx>T^qJ=bN7Pxl<36om*XXR=@~$Dh+{=7+W@vW4NQW2d;gArzM5D zM=N=EH(T|Z^@}GXrT_!&F3==}066MiJ-Zm6o{A(Hpo7ZfucG{e*b$opVF<9?TE zerY87 zEU0iq<+Z8>`oZ-}vnNuj}`(bfXyut7=Nx6$p}&qG}fj|A7vm z(4@bt;m9nG*r8p4<{*la{H$jtAQb}jTXRY6EFBs}SxOEX@g-}4-hU^;#^oztZwX9J`hx_3h|A zVD|B~J}=>4tF&BD8iB~boycqK;Qi;tWJ~;~n*yW3&&KfO5y!`6VJqhBGLf>rHO>^3 zbQPNdQcF{T&q%9qpn{O4n3c@c7)jJQTCd^l2V=05NF6ZP3N=AfX1fpz+MH=ZjHT{S->6|Pa4q63yAGqaV#3zKs|mrxMBXSfc4518oy zY9eYq>w8H$5p5S8mhP%7p``v9v}d`S0KW{<>4;(6WJ)LR7(T}auB?#aff&}czK z{aI9dvpcJ_@AITkcP~sNJ}D-YD71=)_2r2ubfU%O#61@GRQS0e8D$^GL;1jDC?sq% z2Io9IxNr9D@q*?tszPG|RV z+N^)jsedx+Ue^xOesiP!4%`3$AOJ~3K~&f{(0V{dJ%bMdzZu%I`oB0neLQVhoFlbv z0AE0$ztNA@@yX=~f%EBEZ!~I9rKhdsezjfDoPONCc*&@V_U)@z$~>_x=ezsmmWX6} zIYxsNZuxA!e|xvSzrBa*4>$XP-iBl&I`;i;Flpk8(FvW+U_aI%wJf&8t*%Sx!iauP zCcRO=0|Lt!TxiQUK7vJimPHC-$+X7Ihi|bVSx5&yX0yD-kwS7|qy={e5O>SuXQErx zi8kj?&z?LzL(QG>Tv-0)a(e@@&_-6#9SRDAR(nKW&T&bd@#5+9@_dMYozpG}4Zm3* zoK2u&!Jh@BJsppZ59^!T{r>tJKaF1zsLWXl~hC+RbH?c zm9KG%go$Lr`arB%z&;h{YSv<+VDp|8KtHdvNd)!zBcqSGstl%81K#Q`tOJ-Qk#MNHnMYmO< zPbZ^GB3Ed3ja{gi^V=#v@J@s2VCO1HnYgUmkj zUW;h?8WT6MmE8C(a7c(`KElGX3lky^Cy=23fl14{-Lup$~(Mrn{xwh3Fa zgA=26s6N90VM%+v$~E44dwJ>nHcjNTCBQPcQ{fn8xdrp^{^B9{%`F9ZTE^GpbT%v* zr&<`5vzu{VraHnj=^qWfFsq(5Wk6ZFN_djU4rGXgFh`b8jID0K^%i@nYmC{H^^;?G zJ=*Rxm!u^vB^i%SH#*Q)Iv-176Tmu9oY5T&gyb~*%VMfwAM&9*1!nCK;1FLJR+gXJ z$uBd;z7YEVoYigL{Bp6peanQo?P^iqZMu?4$&TP1-lOKBz@Awld!ZZJYeK*%aRV#_ zv)5DF)Y0l&b(Vj?HREx$7ObKaH3PP=+{ftQCJA%xX5SVXXIYvFYAY_xy9CifclMU< z&>I%%3hVK1J@-}iw{Lg*S4BQsE#<-l|M8cVD|G{SV`G>S4j0x;d?9~@UtT@hys4b6 zumQg=_1eb(KjE?GV6ixsCPTQrQ1`awKu^Ltu)RXp?Z!vl+9y*68$XO_1!w~O5okR? zF|j8IQHRZDJfS^8*GbR`Q&&Zc&T-IV>TwUQ)|5#Q9R|&>m~=!mcXvMrE_wOh6B@`F ziRD!B?H-*@+SuV#O)mRSE>Cfd_lp(Ni(cQ%SORoxvAR-lvp2uhIb2MKgKjHE-vQr_ zj3J!eLpBpNZ};lwXQygsTT3BORdrXLKqRdQifJ}(G@<3NXV6i*nXMhlgFc+}`VmVIO^t=1>eW|#{xVM!SutD{jZQ0bAV+$*tbxnQPVrQ93VD})6aUoFX) z)C!aKaVbtY_Gs}?TurNW{>2ZDwf@`h|3wq@k=RAaX+j_Dtb8z7Vd?1NL}~Hd(;fli zk70m9>luC*GsR&=)c^*aK_G622_va94yM9nR$=!y!XQecr5C_v26ILEBC-W!B#OcE zB$eLb6k(N#gebeOY7$)zTo#~@9Bl-nRnJz(&N?d2Qa|9ETTLqEb`QH}(?uV7pF#yz zFjd^!z)EMMAEw=-B}o#9)Ct`H{P=#To_4Jx7pLpxAV@zTMTK7uY%g5NfmTjs;a$M2 zARumXVgsH5o@e81s$dwF0Hv9Tohn^m9ggrKTBojELDux>XONWQrHR&D4Jj^{KqyQt zhkyz2S*}A+#+XRCxME!`KFg>SWM?<)ankRLmn>T^D=MdNt&(?zaTCG%LRoX9F!dGc ziijph5XoXOmk;=?78cUyc54z>Az%RD<7C~tyY=FpWFL<{I>Ix8{Y1umYr~BqrbNW4--r?m)O$H9c@q7)|_;BwN!Q((Gx zOn>7YtGjy^Yq!%`->q(L7YD{scDqlWou7}!*n+qB^zfm?xTZp)Rsm0m#h0Zouu}na%v+az4s3^ z>)+0olz&gVt<8M<>h^YWikRmX4L+#jM23X#G)aB!Yag7AJ1{)wNcV3Jx3{ymx3{Cw zS#LPT;h=S6edf4zxKqFW;>8#kf~mP^bEC{ZJUwOY535)V0HLQA3U#QT)Zr;P`12U z&QeI+q8_TeG*0j#Qc5kuArBGc1H2EW4wwjMWynKnrStH?O9XaKMV4BbA^k!>3yHI|=j+zYKz1U}fWSxe z&Carp&9@5oBv>^flQ~8Eic;!^gl258iZ~V2Hk9O{nk&M2f@1q7!*y?Y02pTo5$M@S zk#cc7IP#Z6(9b5Xh9jw}+(;@}s=s#ESRdf|c)NAC)(hMx!8;@VVfTj8iTWm>`#_^5 zA#>*?YBGQjIeh%eF5+&cqEXyd@ph@|2o9I3zVCbIsPg-0+wqNiD-D>jAG+0`qAvDC z*2OsaU%_<>ks-EIpeZ?C$$o*A9gFnN+vdiXjhdh}QIdRIU#7LuK2{!@8);sD(L8+E zYkqv*{lZzdby!(Vb}V}v0W;{__&ato4S$r(*sin10v0cGtwz(%^n8d~s4<%|=C##q zK?jxbkIlBCXPepf=H`AhVW>QVlUBf?>IDs(=T~&jweFV>Z|;_JM6_4yUZah-n<-8W z{KA^ton4JCrh__jbcOwDwA;^Vjl;6pM%?NRYFC#RG%dgYKw5a)V9YepFjY*ngO6sX z?artV-J8(zjs;0ULs0y?1n8Fm7T<^$|h+hw-@i?3$K7 zx(x4cXD5x9U;Fm2{@U>DijFG^iv^`AX;R!!0U<5P2-yWDa}9+ZYK#Os5u3*}HBdB~ z<*f)V7?TmrW~4;W?;<9#<4VjXT6!g7RhZw_Q7GGP-{LyrLh{kc{}L)igP{N(S;mRG z@-P2aY-v1|Z%KOf`0|o|xmHVOQ5;7uC;31XrO6#Az9#2pLiJc9FJ7&Xgi4(;+{LW^ zc-s;mln?P!a&BxP)i-j!@9v)0SF%oF+!Wi>mLb%nk&>F?)VgeEUtyuacZZi3D6|wo z%hn5`nsSgEy=*<8Ps*Vb zr!TMj7tj9e_x`ALpwuT6De!6C2=pGkn8OhD7AB~+8P`W$NRVF%uCR+i=Y<3ivrX%G z$d9?eVtM%BJ!LyplqCnLs@zG74Obv3L&H?CT3Zd&yTZFVVFb&`7+yJ_tuo5XAL;`)^L>vvw}1 z9UN+CBfEnF@UBi|#~*9OTrkqmAe@;w1;&vON4c_On<@(Pxxl`wBzZP0pjxKhjT1N{ zV@G-|RWPI|yoavEcum>zH$mD=#|GDsLdxH zRi}#VEJ9gIN!-03tOvBgvY%o%+y~hY1$WGUff5rdrseMOWl7+4Ik5wRIeLOvh@45; zPQhFJ<4w%ZOQ=vhS3aP+|49QYL8(VUJpEdL_fphJ0j7A*@}Kmqg2CnvGJVICK)2%j zj^vD$*>ZPt3f<%GMf>=bLH&!9)`tkRoIo2P3UWl~ZlPd16Evx1Twa}Bj61VCwCtM9 zRN^L2ulkc=*8)0@2w{uO9=M-^7_logLFD%UQNbRm;9or%)BAJ#W`U?8gFXj8K?`-g_)~|KEZ110AH2rG7TDYvd+!&YBGUwGn;`Y&7Zw|^5o+5?W?!1 z=5S>WjY0dp7f;}}aQqovIrLZ0H_$=}LaFef)3%_!W3yp&bjFTS1cl`~MCf-;ou6n; ziBo68@~}=%dl$nY{jD@7-r)tGf8(eB(XS8BE{J$DXd%H%o>2Ad3aX8HHQ~qpjQk-VAmwCK!HSy;K@^mPAv;P~%KVdzUdDtLc+%iAfVn&`;Fq$UZHCJxt#HNa z6jCFzyP#@tFH5dKrM{9VI50z8UIlrUg_jI6^LV|Sl=`YD6EbT_yh1w5=a$nG;E9jV+Z+*-8a>yUF*n}h;Hl{|e7F8{ zVL@g57~{{fW8BonAG!GQa?d$ovgo1!X(E<&_oJU_O<%05HFeG# z{p{R-2cWhYcM9n^d)?@{l>0|duO=_P@JIjG?{`sEW=@_XG#S|UK)ep45@Uw}1zKFM zSomVGf?Sr@GsIVpAHtMiCsczBr+5Ik6wVySu-u&R5*rAeabHH>>zlCp31vhHCE}y~ z?TC5r9nJ4pSEl$7Kgl2jc+Ic~M|YT^;#kSS(h1E-4g2583(!iThUZ`i$c$9I9r&vH ziMc3VP~a3fVut}|!h;Gg%u0hH!fnOb!{dPg1jie*t_+#%P$a|c`f+5Qeg1Y4B9=rK zE`SRUloBJUqy9zVTX>|F4T{wyn}%$IZ*cAsi7YaT+-?VI%74Mun-=p#^?n(#QDAwV zi>k#wmz1=H+yY|D^Fggfrc9WfLISA_%8rQ=Lgfq+8D?um@t~mVE>im+;s~79=aM2^ zQTq%r8lD@eUdml5O%+O660y`aL{s%iekcgg5={>}IyZCF7f0h!m$@`#*7or8F?c&F zt4MzP;R@Io9FckxL&TMW2e5*-D?3iaNu;fb0=jv*x)vK!bU$tW5o^uaK!u$`{tJse z5+#qoM|NB3G%t?M-5gk>aNL6SxL1pKM$u=<($C1p1hol-i?JwCW=L`+5zGq`@ERPH zr;;NS;JbiXl}ptec;kw$f-E$?)0r)Cw;xcY{uUG8hgA91CjY4QxjwLHBEhm_l)5Z;U`zPY$CF zqG+}2>k-o4&`W8JfZpr?9P06CsCR1g%1qA(gF%N)wFM4U_=Z4|582G;O9mDKz_O)# z1KJoKm>Ed*aoBF3_B!*~5~((J=0{)sVAwpqdi9$06|&FS#prT~IzASmIadRNB6Z`A zPLJBjXo6xmL*wR{ACw$fQpguQyBWYy#iT|E;FdWk#>CI^|eCF+&#AQY~Ag> z_syUFm0#^lC)`sav;tR_oZb~AKgs>$^VQfVDYVg;53()@U0o3w%Rn*!@#XZ-?in-Y zmdMmL1w})0R6I;?80+uz7xl3l-5FH@f#`p(9D%FESUkHT!$%}7cy3Af^wT1Ko>Rb4 zw`-M(#XoqoL}=AQ)`4ZAmi1o3>y#*B?@P{DoaMV0`9sZy#x}E%5*B1%RBJay$8wNl ziwP)Q=!MbxF7*uZ>jjyS6&F@cph&fG9^TrJ_e8h!%_*Sz!gnLsAZFsIVzfuQxsf7B2p}I zk-`D>b2#FXS8MwoHasn8#9%lr=M3+iNQ^=n0G-8~tVCbYBHw}WVPb@nLId8BUrheB z5Jvl;ZkKmyX&ssYyy*x=@=YbI;c57Qq!vfHQ@Q{UA{Z+@{ZHcvIlW6+u z!IzyC4yS4k$izIsG;I^I3u1~gc*S%Lf)+R;JtcuipxL~aJ<2oPI?*t_=s?;oT`X_*)>)DqT0w6qEk7oVg@uU zKNg8U8jWnR%6^n15DDvOqC#ZuC?m z-24a}9j`mJ?_3PuA2i498ht2d42*0Npp9@R;ObFeqD_Djpu+0}h)kMo)%NJ>o}VEG zMwnueyXaXqP)Zy0ZfB5_Iz+`B>Z`V)H`r~1T?Y>;F!i73Ea<}nh0C}~(Uw`)5az5W9Qv2))Cplz2 zZfMm~z0J|j^4Jay;84QJ^=v-iu2O~DL6&JeWklEhgsu-rGk_0_)r}=O3`e6US5qK$ zCPFXiFQ??@gDgrahYL`?9Grutqux}3Xy`qIecb+pjDD> zu-vWh4@i_!n^o#VUFDefIHi&Zs(Pu`Me1G>%~O^ymcHNzm6vfDYvrXM+oqdHt*nYz z*!Z99sNxdJntZo9%PK7?T3kz6V7wqDJEm&ZX@#Je)Y&^ye=&Kl;;?cw^7W+0;haQbS8hO5Q^L_wJ5^jDR)lE-*z z<}a>*Z4GUEKtc_;TJLnw0-s{coq0(vl%U!QR`TTnb1{cnEo<%-ppGL7A+cl-lFFXg z0Wes#yrI`RYXSkIuu;@-0V))IWe}$6Q*@wdz%o;lV!~zzW%b7}^dojlQA4BYoF@9e z=}pA(I2;39Lw#V_mJb>z=U154C;@mo|)&G}<`AYQVt& zzXlj66RH4@O#1@$r?ypAcA7~2QlwsJ)6|Ud5gG8{^DI&_L3xAW1sAXenZ?W##J$Fq zrl})ah5^ip5xK|CSuO!-pj3g304FK5Wv&PbdRlalD8hPkPg!sZ@Z?#_aLI5ifgt<* zGNujCJSsfuEGNShEjAJImWX~k5z3Pf#+xB!mbpo;lWci4iqxb?8xb9sZRytI^O`;5 z>OI5H-3B^io+Yz-N?;n-r76gQbN89B>W8zS^!5das$RxFqIzHUe0Hl>Ut;aD!F*-8 z;Pfs2xRt4nQ)QWYvBa2eLN#-|dlg1h6?&5n@~%n%wXz6?&t)iU39d`Zt&M0}!Y~)< z)a&o}YCpkP+rwr6(T#wyjV=rmH9PQ<3~FFsE2CE%jA7Z@k=i$<+><;_`yFr!$;!lT zX|e;)e?>(Cg+i%{?$n^kl2K@nD-qZ#Fd0?PWWI6A5s0C3n zbn+%6yBdvQw>cR#fJRq$547t+ia|3Cst(mR*7~sD84Q?ih<<11Zq4`!>fW6N^>b$4 z;#A2TPmaT3XCxfDzqy;gyOAbgy z5Q2_VlaPiZ+uo>9Q)0*HR23ua;52(CgYcz%QYQH&Abdm)t6ycg67yEwv$3Qa-h#2a zBV|f^QdzqN(af5y9JqCk{4&ef^unwziu0~E@vAJnSd2iU6$<*8f|V1Al#H9f7X(eJ zYT%z_T*X8ys+cq(3Tws`JIc&wgNvh%17F^ep$>vYu zn-=K8Bg&4C-^xu(y|lPQBL=Qu$8_APpDfk6sM5q(CXdVwp`iwBxkU*_Us$h=_(|ea zjg!@{{lnmVdiwqse((o>JZ#*PRLKT~A8geQy&6f`;q+xMLyva=Y$QWjFjn|&{^Pg6 z|50b#@*#jC+%z;{FrVaPLYNBws$UTbDPNTS6oN1rkPQ?8_|)W z+P0_ihrgk+5&EW$k6J=*J7=4LK-@(u&GITm8}QZ48Z-joLop3&xLmApVMZvUGIh}0 z1m|U;!+J(sDq;_Su1C`0X2%jrI0l72jW35&YQLj762~)jaS>Q0V{b8PNT<999~8=U z`Yp!l(4{s>W)pCfWK4i>*9vQqvL~O0`Y2CHFep zD^Y+rU|A8STLu9c-%tu(O4bDAC?G?IC9nc4mZ_r8%0pZu_=%k|-b>e-hmkZ(W~n%^ z8giZ9WIFA3{y(bT0_^T;^guQVk}YvCqbQTi)Y3u& zNok>t{ugbv)Jn#!v=OzCNoJxbQIyDTHqdA^fC8#=jKRa_2#*N;e&2mj0276(%t-g= z&+ohU4Bz?AcP21@^wdU4op1q6iH#wHd9bdL|MtpULIFH)Pg%uursiDpiwmO5%FTPA zz?bo(*$;`^*dP4IlsA|P$}&t)OrHASs61IY(emDMg88pFcyg>#lee3vRDeoLdklwH z&|IW0$S*^&bcn?vhD{SMRl0!6QBU;3eMB0q0(oQyq}8K}$6Ym7I9O|pUHL`5-9M#& zIHml;S@5&df5C}4Y1cpP-F{350PzU39Vz?Q=8bKG6p8~t3?q*z!6s-taK&iAY=_36 zUq9Mo23ZY}bL}FCmlM#Cx4;r?hmR+R2cw(WVme=U$CJIoF}3lFcQoJMw)zO7HSiI1 zwNG~U)~i`&@x2WI03ZNKL_t&+0lZcnQD_*yEE!xfT1Hd(8 zc1L|mq%SWaZ^M)s@wF^_H+Lv54m(ZWy8^RBk&1mcdhhdp@E`Y1_Ui3DdFXlqZGOrA zV_p*usR545>Iwqy7R{1Q9JkA)hOdr=iWg#lfZOluc#v(k&%3t8CZo{t|TYkDYVA z*ghOC`89f9O6>U+SMq>xRA<6C-`KOfAjwJbt7@p0*Zhk2OAzt!sqx&pV!xnS&%YVar#!VBGKtIALr%rk>pWS_3(#lSfKZJ6#G%ic6p@DXy2rjIM)qe<@JfPSdcX4+eAnXcw>t)*U3C7L2myuz z68xv~Y3Gn8{^fh`?ya7Hzu)z?W73>D10)I4%e16*QDRG zANg+{H$65L)0NL+8IzFe*!ViOkH-SI6@~FVf8+r=I&Z_MtHI>3uU%l}nbPf70_+?} zzgOZaETtOsU_TNvmNQw>;ld8cr5--J_&h%(7ArA>_0Zf$ zi8*5eio^EP6sIVOEymSunxd!WZKZMI{^sLh>zjk-e&fE+s%@4S_kI)5ADezh(*(?A z`XP4r20nRU$M!ugkSuCEuJ1x5p-#1I;J@yAJLA2BJA?=+^0k{sr)bG^F5a9iuCMpV zjCT9Yolg~^!-!8Jci_nE z73+fjZK3t&hZ=d~bm=b<&>#*gr1j_?k^|eVFGd9QH^m`ia0xrf(`ZJ_eO6-eai+Ou z+!n_r1#F4m7TEJwWstTgNaEiL>P!W+@;C8P3A(itYDamFGV8#`R-5@WO@2+%or2%x zWZdi8l1jSt zH#sRCrdD%3M?b+fRg{=5CkawWnAN1_Ct1^Of5#sMT}fPG!s7>1ca9{%G)EtN6 zCeR^rwhYD^q!{gqak-2^5=n5xv-b+Z>=-1|QJ%V|hzDo#of$h7LYHh<&{HpKJ&u)> zH_RLw=dHPQ?A53)pmh&wY%0pVyZ7wUQI)$SGc_tIj+!*p#5nThNy8Ic00$VyQ2S&- z)>9={-CcH_LOV1i>@euD$BqXsw;oyITU{WpAm?U=7b{PtX_=%bQKPABrmTqnJHgb1 zPHGEP*s36&MsCJV_EcWa8L0{yuD0BRud8n5Q0wkv;0k_JLd=31B_2;&Y%ueXvgH^j zT1*V%l1bgk6)G?3xhsoU1h6Y}mRII9`buqJ34jyVlzWX~Ncde$fGs2Gru1YpNB;I( zqy7iIoqZ%geTLiJQD>i7O1h$3lwCmRhkFzFkLTwL%CP(`yKlJH-UsgltcSXP2gzkN zI^G*hhMUV76QFlb_fJnI*YnNGSMPYphmQ|>{nqMoeR(k>H6IUqBwzFCicwgMXzEZG z0QOwMdQ^wLSmW`y_59f(Ta;uBAe7xjT_gnu`V7(TZqla@&O?CcUqX;;wBSn4mXsLh zY)!ToUTJSUgw%F*wLHIIhU#{BUwgd2=R}yDyUC;7A*`9%f)e4?YK@NX;iH4m2s!p9 zg%4_Ob7+`rV$b_2&5ZYb(A!D3K5@df6X%(>IGoVzF}U05yq(Sv+8B;SM5(r%JUKcf z>)fLEjRX!($|taGPd@&efA8qCUuz6T&K);Eufm(k4Y(7F_O#JvTufTO4HC6p1cOjk zxv18Ru0diA@MS1A%W=udFV%iY}EmPSl+!sHL9hE*-JtB;| zf(=^sc3Nq+l}!QaZv|3G^pz%QFP!#7EmZQ+*o}m0W$UIqnHa6=cSxDUxVi+{)qhHW zm>@C9q?{2;M+wdgR+QV#aKR*q<#kmwk=`S{NfJXGfZ{`?6Y=4LN&uRuwYt6H?xXGJqEJgQLrYCT;sXT;frcF!3PL&dpexC&GiVlgUn>QLharD(SC&|IHt zAaSALQBAi<3$N;l`UYaOv_rB+_JP>Y?~FrJ$vZpcEjF4W7RqQ*lCe9#14Xl`5Ev7B z9UNGL@;s-aK`8>*iMd473u(NKBSBt(K-L@uYMhL#Mq?n=*P>R{mx z`-RL(k}Z0ixTw@fBPDi!%ekOoeS2}=ED{abF0q;ja(H2eb)qNwQ#0P6Q9d2ha^#p- zb6P)eu5^Dn8$uzW6;fDfs{}P=(U2q1(cA4nWYGt41N!z91AeKV8 zst!>}+Di6VS;SN~l0_y&@Bo%em4lZf_sgH>xo4&ImkQgK&&pd%yz8%m_{1V8ej)0t z_FY)7u*vS%pY+<_7}wtGcZbAlhNL^F=RoeJfbF4Ghzk|n9!vlF&2;~0blAI}U0x3M zm>b_)Uog{iiw@a*b2B;JM>l5r?);|Fd;H>X*s5Qg&Cf38&?Zk#4~Y<0Q!>UiI6LIc ztMe%gHdr*Retk0ObNp8e(yiorsyuB_VmsXJGvZ-|WQ=tawvGMBJmAg%*s?{+#R*cF z8_&}j-8H(vM#z1x*U0Uwrgt!`4_m#}a(+pN;&evq7HzlvzD(KXuy=C2XXJUib9r?= zU2JJ8+&kGvLs&BQmZNxmc?thyb@J$FGlK#O2)cQ^#~^3=oR+vH##@ZXgGO&uYxiHB zy#W;&P$z8d3`WBbKYWi&7czsiS)g?%iR0NOhkxmBJp0Yho1@)^+JMRb9v*9XR>gAW z6zIAHNvudD_mUh)+K1btYaR*_98sX!IeB54B$QT*4|1EPsu0D>)IZ`tqRJ4DBM6=& zUUqQVjmdU0bv7GE5^{y!a}+i7ZoG`b$$gusRa-HsTk;W~QF1L05EX@ZC0jni0VTTE zZyFT_8VQQwrJ zn4b(kMc;Md@7zFmc>etic0#8g0oHs@RT$Jh+*NQ#l=vijx14JZMq z_Y@@1zp7$fZ^FRdPIv}SD(k{sb$4#oiA5lnn*Z_Q(eb$5J^1p6f80h$R?aQfN!Fth zEtxGJ)=;X;HkE@&b3|}eoN1d%xlg9__83G!Ne}v#wy-T(l7QD8$A0r(cIO_^7a z4dn9TZ9u*f3D`K1P3xDjAe*KOBgM?T1fR)Ziqu67nCw`k^hb0+>gvWb(euoj>r$=c z5i5X?iHks(9rsElxZ*bq*nv<)qYNMyl^Ln9-R{6IQQJ_bDdj+i-U|x)xio>YXIK?e z`)-@OUbxjz02Gu)7E*64q}wpizldDg3bRi|*F}D92m*Ca7$JOU)GOv7XwSnFXLCbW zKF9xL%V)7FU*&nI1Q37e1B-+F`m}#-`-+RI4rhfam4ahFDqbj=Mz4@)*MvR{ zb*>nMAQ$u)mjJEQo8yUe11SZRFtGB1555uO1%=GFCr;J68b*6eQ|->Kwa`)=tOx#A ztK^Yuv<8zn*qL^CYhm8tj-_ZIk08#Wa5P+lRNG@+%9%=ytsJ&rJzx1!Cb)^n`34vl z(Z6t8Lg%(TUudqmPuK)rn<+1u;@x3 zZZp;CA5jpR=n9&D`0APiQ1PO&vFNG*qa^IXR<@6VcpUtw2);ZY=`Eu3D)@Z z7|G=~Kl*XE4PDxzSLV^CDYJZrNHPoWBzpMRDTz^5Ug{L=i_pggFYZxrQj`?K+H3%g z^hZv0;7SCO*lM)}F_>ViW;v81Q~#tFB4aN#@j~fwY=(Sy6QwU0|O&6m*@q^N9hX9{I}CaGDZVW3K4 z_w6jitw?x>?H2lJhyS3Pu3fEP!F!GXi@jKoPVECSSrSPGmTf+16apSJ<=2cZj5%g}Ac& zqoCFXT+R_gQ_W+aK**sbnnTL5j7eS1WUwSq~CKp z=#&~^_}rCSOX>c>OO-6JM4G?)tGu9$f=rlN0?Bd~zP2Zc7e!nWyFO%ul|wUY4js#r znwv;uYPxNb=ebjv$FEyup(S+{8L%9Y`P3?jfI-}UyxabbgVEE`-C>tOFABIL?*dr3 zzzg0rYjnP^md8hv-Ci5-vZU%!Yg}GVfu*0E?lpGU>S$UBi@J7uzwQQ{qAWcE=j zt=I8W#5%aaV#Rm@Alm)0i5D$N_^>%sAew!i)qK6ZU~Vsc7>>Q&G69Ar z#^$DZbIthp4?lZO>_gd;e&lzrFD|C5xAXO?)jfXyNxx5D4(^<$4|;MILXB&h@X#K1 zmJ=PSeY<@eUBk+b55_x8UqfiMS*QF*%>>FSq@w5VKYn~TLgj&`Up^g15Ahrg{&1i! z?*^a#-M>FMea1`)sqJzoDhvE8JA^GEC_&g}E?BC@gr(Xc(o|}!R~Q)Ovlnvx=qQKx1e3*?aRVMCi}x-*L!7ByTFJgV)wWWp;UZLk3E|i)KZxq8$i# z3A?^>T-_EH$zTYyflN!6{X{XI8W<>!I?v2`^7W=S+%dLU31-6@2gGMz85xMv5qoOG zoG{H91bju?4cNvW3Cqht+f8Au6(;|cUdnfB(+`n(L$<@w=eUJbUGEJUFVW*B$xGqL zqJYS^z*0M$5Z8aVLxsTbFOo^NNGQZj;>bT3iV&o;YqE)O0&$DvkJ4Rb5v5k0+JWCf zQ7s~U%tA5Aj_7;<09jZ5NDb23T-xv{=oTJSLPDKP_KbxnvhBYI*3iwHO0vCq;-W@Pzu20uK z{il*PT*EwcYJ@o^fnfqN1x4Y)Ukf|sa1>Cqa*DdPD(HPUex9+|oX|agnN1c*)vG4V zCdw&cM#56Rq^0wWDU0DKM>oP-pxhPzw4l5JW2!@&1FK^f^*?Og{m#kYMeqLcu)a$I z%^WM<(TShh6 zgSfvtL@RzVw~3y|3I>o_Ew%2jhi;q7XuZaYsqyrV(=pTSOwnkWgNQ;^-y->Qyf;CW zcFM3M_-3mcsL<{D?YPI#BvtzM#v@c1-oBZi&sG;VH{+9|gQFvW79uVt`0~qX@*FuO z3!SXSQJGL9o#B&Vqb(=!XpZ*L_E^{I=vb5SIc~Ui+Z%R1d~rHxqH2hz+R{popGNkq zGmxJ0c6R^Czw<|rKK>YWNbQr4!tL-Y$xmV-%|rrG4=*jjhNboHK!ksJ5fM@JYt#kc z>eK~+gjiDib0u?@?cr{bY^s_J`{V9ztuU9*KG9n?MENkI?%3=HXfUByT!bzx6H$za zjpfTmPDa=;xE6sb@P>s<$q8k%TYReWN-2h>3n|kSVu76%Mtu4F3Pxm=QN}6QX2}tA zONO-GV$wKIh&W>0uQyh9b#;E?y{agt#gr`hZ^Bd!9e~Mj- zZgI}BYtPGpF;z)+oT0)HH3ZXnt(x7W1$3()oPPN7XJ6i)eWkz4OeX40sTR|CXObjh zD!hmIAXP*RrUjjlcASoPd-f0G-Gcgof!g%SG(sk`6CM&`OrH$G@^4y zRSSew@iZzF3fAC%)P4gGq-m} z3ZZg|T~%Tn-c$P8nJDjeNQ}Vh@_lG3L#bd*E#~$P z_;`S@M>Or=fwIB4`<BC7XA5@L8IOCDqwd}#a-r>Vj-ZuItDv}vc9hSIW$di(o_^v%7U zE!n;!Sd_HgF;=PDJUC&PyVjDeWf$#w3v2>LIUKegpNAo}l*Rye=OODgNy8 z>g;S?>-29r?Zd}M!x4no8Z8ma?Hx3o1qC}J`#CAL{a<9KCavI&blF3Nw#TP34$|HvrOp?G5`h8Rc1t7R~~`D9*NAUr=K zuFtsyg&JCNq^~2tP{&Ll3hO{P);86`orvEw(!(mp&G|N~AyGjnZD^VnKSmD|mF;r- zW^xZ$OT4ToPF#<)1@eL%M`B~BdWxKa*v8hW z;yDy1?kT&I7@$*uyJFRZLxFY}kaBD9AX7rwdBlR*4-uIWQPVHWr8hLNa@2vShLV%j zgBExRDQ1GlTL2rR(cs4b#*$0not-*Q3=(N?>vm1&DO!=bZ<6vU`6lnPhHwTPIIil* zzAdiNX}{RZGI)Z33Td>}nyF`;fF;iy>&D;R*x)-qNpNPyWTgn6Qhd_pi&#bKknEj=a?P+UFx zO|v6L>6TVWz!kvpp&5tKH5rtCdTOO{pSilN0!bGew85K5C^0i;_cEzPV7#h2IN^zhZsERpP zKDTmx0iddrS;0V}RLb%>z2%EW6WN2CcEi$j8S7Obu5wIc2&#@uxUwJjJKq|$ z51Q9c_Rs~mqjINse4}|f?!$$m48^b{#tRs^#pHJZ;&f8&yFaOQZ`w- zNW$Iy?#cMQPo5qOn(tm-yneY5b!&F{il$u4CF6}ilBwRUSG2X>?;TG#OBpMJ{0e1Q z6@lp*+}~?HJvpEPx0nG|n&hO5QidK|hQefTpa%O5^U0!O4~9d$Y-1i#6I7J@okK)C zJ52^Fk%=z2G`@7X=+^JmChfPuB^cs%b+M$6{*K`iL^Jzz`i%i2AO)aPI-Oo&A86mD za~OFRBxg={RcT}P6KHD}GVRG2p;SIiD(UQkjwZ*JcJ4p^)(20Y9&ML1lxytd0#1?u z03ZNKL_t&(TpLUgJy32~T%pc=*MIcMH~!AwYwsU3YCwW^bi85^gJQaCO1C)w6q>#L z38rhNdH}4K*NaOStar@h@dM`AxQq@@-P+bE*)$$Y{|PjwHC9?blzpRIbs8QumAK+A8!Zt}N%04Tcf=H4E{Y)vdxWinc`q;R+2WMugnQED* zKd@D}Jjw9lVblDa)Xv;3gI$VE~rUe24TjAr>s?ybDI z(!9uWl?UTp`SY?US;JK9_(V5`E0>kk(Yhl_UdZkBaBg+f+E|I!FnPzON3=dX`0|hc z4e2A96W1nM$D#vL5RVZalhu_fB;Ekes>yZ?Jvjy?btxwfePASulO>v$N-^B_WU?ti zsV+=Z-JG&WkG>5ENNe&;uoyJc!nbG+pwvYzTRvy$a7QT)&Z*7mGHEytYm!8Y{L};5 z%yEJGLXppERHB?Z)kK0?WtngQJv8ZzS=lvK6)YAqid|_N5vkhF=DxnUZBi1Tix-on zaG^Gl);jy&)sIeG0!S!xk~_8i!^0xBq_t#j6d*5`&3I}H@D|l9;d}lZBv;5z5^YGj zK?r>WTulfenTAD9>>|D^J%HBmN}OJ{tAt z(yezI!`%_sGi1oYqzmo;%{%nYwtEM?25osHNf0JifPy=t(S!j%twH1IM~@EnCX9#p z>CfKYK+$UBJG+33XX_gjFisDLEa9Ads@BF4jdpu97lII|1?5Ci<}`uJ*Z20Ijxyz* z33?E+N-@^?UIaRDG)H@OOc7e)0B9hmlNd*HSMLxLQ`V#~$-z4sk9pXaE48q-o-|-w zZ-xrrG)ALQWN8r^zJa?**Wqk)Jvn&-REkf9Oum@iTwNl_f*`+134MmdKF<#(RP7lV zuhs^Q^LrDrS_EV+W*5t=84Z{0|K4!)={H}{EwY+juE0)#L@CZ2?Y20oS69qpt#uBc ze)l^@dPPV7eN)I_}Ib}t#(M6FUT*_N{FhcS}n<(ohi6pUN5N1TB%Su zpV`^EP26<2Cp=*}5O%(s)>xKb1u}wMXC9Mb6WK0u0Q91Tx&mK}$)-*|^{}Tl&+p`) zLC?XoK~bPMp#lYEj?$-S>JgHv-BePtu`@IqsU>G;vFVC(Zn65NcY*%+gWOTGx2mA` zkV5@RQ6Sb|xsgaPC5N&xO^Tj(U-`qNhy`vehq!pGg0_2>3)q|J!nI8m#{)q?psh$Z zcL{#0c(HP*zDW99JZ%}u8E2nwl253PXZf!m;E9cG^6y1-^L%4s%4;g$7T`8WZ&Fm# zHUF-9vO|Ed-5JjdPnow|c1t-HZ=)E16V1|fJ! z?>wmvI zacoq*sf^TtQv2kPEmUj(%b{_yoi-r4@J-3p?B!sadPd=GQta3nwt*G}d!vS1`XJlH zCb+JR5Se{EB!nD#zM9m3Lyv|8Ya?5qj3(F4M=d4Kte}xnkj38IOm1hp{v8OJz36zR zLY$1kTfqW|oJvq>AxFw)TEi_I@W2H0IipS%=6V68RL*gDlcSE@3XrQsUJ*-^2`J1f ze@CYI5>vtuo$>ClKb(M?!aTw?rb9+;OL2b7*MtaCb24ke`}VdSWOz^bz?ygl3R~Cz zx}EqYG|p8dYj>T9cUm6(l1ZaHBSJGq*k;2w8|uDxkxA4bs!l`-G)jkK4qYfgRe1Vv z9_1*-brpYB{kwv5a5M*BT2A#cqyx{WD`;VTze?;){i=V%J`R3jJ-X7&`?&flN zKV2^m9*v$K_bBRJ2;@fd&a(uUx51rK{{5!k7!IF*>*V);uQP#g z8u4i3m|~&J#4aABYK-}XBQh3smq}TJ6^(&fI!ld$Ed&{|-}E;FVpPf&`ePl217O^^ z;b}Vp08geHrb#0hLI5uHXxIx<XB8kaRNrgXPj<`1!}oT*eB zgESe>(>LkBDLsb}AMcAWc5*ZeA>~X2{IFBX@2v|-K;Wy3a22&07#QP{7 zGYkXwMv~0}==ge@86CXD%W11;bMY6l7&cI@a4xbq8nwgIr8`@ED_9Q7^RTsT2c$Pc5Jy!SCS4`>L|Yg$FW zrq6HfkT%*XB(}JLK^pB2Rh5(6o`x8Hx{L*HPy{tSgd~VKBu(xV zaFjMMAzDt;!N-}WW@+k;5Tyxa-pL}EO_@yd13Ln|7`k88V-sl(m<3Yl6(@$fU?is%WSB|7_v-C9VNLeW=w}w>1CurIVFJdw|SAp z|6)Lj$NV}YeAxWerSy>xrm?(T2A~{-6#k3ya4rxPW=qYLK?!K%^>b&uG{P0PFD9Mu z4qK<48V$O`&JON~RR#KGQ=?N3HcSBKu<2&&;(RlsZ~gl2-33|P7LcC>wxCc~OOvO6 zaI}}|!}j?{kN5XGtMltW|Kb;H_UETZJDXdi)nL<*$ef%G9?^N<) R^L{lr`o|->%({k9H@A`^@0IpcQesVZIEo#_7}jXHO3RbevlV zVBpb#K-xlhQRXTwB0Q3pmlwCa@BT;sX!K~m-X2@1tBQiLycNP`>X+}FmOBUZSlm341*m8cmxXjp-6a`5u2`LSv zoeRZ3X4(4VnxoQ__gLeO14#L)I0g@lBovpcN|yzB#Bh|bm7oya#Xu&%%Qemsz$=umUsA}FL~^I`)wy%Cy>hg(!ev>D ztE&=_VkgpQZ{aO>;2DkmOaPP{DOJ&`=UzaEAL1JrRY^IZ14covzPx41AS{yR>Us`piXm9v)J(%?MH|u%rVR8CAXlo)=C%QlW4+v6Bh@Vsnikd5Vh*jqf@DY)JPqM8 zIF|=TrNePz(uWL30R|l2TR92APZBkQm9V5GeaaJ3vfHi$9A-qW^M+vBQhznHx&i`& zqNk$@jvtEtR4~=3*JRH_H;crmxGlcgMiG9Gq$CChaEuWi$b=}asN4lSUQ!^awrU}f z^$K2)Uv#_a5OD+h6_t_!RFjX(|7>eC$aen~@_3@#5`&qvk|n4h9_XV~4G2L1i^%}( z!}3eJO;s%tiQ-Y+=?F_o_w`vS=Su9>A0`#cl9U(Zl@Ds7{-l6o55MB-a$c)I&`ZU~ z`i!c4v*JU&F{(iOEEU&KG z_voDxETXAaBW*?Og64{57wXMwK{PZR?>?qVSBDe|Ttga2z{PGrG?+{p{PFz{kDndx zbM&Yea<8-}G&0t^? zY6aUh;|B4AbR`1Rgv@7bDrym1ToN08h-~>>nM&vFCBJdEq$>BYTZ=I-g~KE+A}ZN7 z+?BDX4S9t<>7}D{7Z^gEByTLDGgZ`;?Hz(M5pgc#$+Al=FuhHL5xpS$@9|*AiWR^r z-}<5Jt1Mt%lS7(xG(l>CbBZ4=hcM%d%ip;`0kbXAhtBWS1CQnR4~r8hr+Pl&^`x4c zFD0=fXjOG+V}5e36~-XdNCY208*}Nbd5+>v4dM(Sl@-AqCOHdsT~kQv^~Z02^n+gG z+A#uV(PZV?2LfFDWzb$fWx>fCgPRJ@n1=3T2_>y2obr!+^FH=Vz-WYHY*3j1W21+8dV5UY~`g-KtI+-xxS!%F|s>o1D=IUm)#}^V0U}3tpRi8;+8TOFD zxyHKcF*jd#7e$m*;k9VSN$1N)7PhfC3A;-JUa~`IC0ixR?KlA71rSziAiaJTUyb`n z5)}u?;ULP#vmx29RI>w_Qk$Dy!;eGWgc>nKP`{iRYM>e$kgWWtr6Jh6+Yh;!6!zSX z8A`~3Q^MP>8GM-tV-7=*o@_^rKj?5WUi(jy?<#vIx~|TV5ofFeA_gY>mMABE$)m;V z{L&W|0hjFLhoe=3=7(42G5CtWHs!4k&aW89a$;j{j4lf{WU?AVVk@0}D*p1`(;em~ z__C_#=?=I?Z7wy@N6QI|v`nUe_ zr~lyJ8%!n)DK5}KvF61K`kT^zWQ0^Q9!^=5s~mYq|1hD+nr6W*>=?L7#Jko7?Vb|9 zq4r+XIFn}j_UJER45I&OS)|eGYH!^wQXoC6=53;%g+Yqx38HA!6!+{);~LY=7%EDn z*fOfZ@i=LuGpegnpK1eZ0zw>p*qlbkj=L+{wPaEGk_jsFsu4s z#=AaXvO#xZAGEF(LAFLB%(W*Zg-ELOt3uE9_Acv2_SfH=Z06g|n=g8XNd^mcmGwLQ zlgE!g{roR3F5YaGJ3y2D(EthnRj1uC-e$NrMx{v&34!c1d(&(5=uzYNli7T}x_Cn- zY6AmakO{gZc3p&(6k3ykLY_fwg06df0@FIp(~(qD?~_kgJ(MJRK_>-vV83}7Nm^;3k_k#a*tH+vhL0A;H)!M7fZnFK5HTT4_L_j-IYKgG zaH6Eg!kwA770kb^T~Gj)thob20G%4DbdEXL9 zoI3Qe47(_fO$QNUpEGS>q~Y}`T5y#p+$P@0n0slm&XW?Nw#F4kh#ot`J$%2X-U+IF zdxx9a)&f6^tqmGP>xb{H(yxdO##fg%#9Pfa>%E~=Ms*$uLM&`BwZE0=)m%S%9# zqevxr=lXl6`@eNC>fGG)Rk(AA36j}#>LO=0Br3dF63+~==xqieT{@Mkc6)xlM0ROB z9uCow=`;ZT`{UN>;RqcWnqek~!>1oVLB8Sj&tJZKJ*C-oci4fdayh?U@Pym<$6fky zX)xS*`({cm`0m~M(QbECyMJ>w73S{T^~M8)en!Lg#6Ady;%)(LpkqfqLp_CL?9E-z z=gxe_|D_qBt%&M^1(8l%Nf_5?*mx&Hc+GZ_#ojz1iF0j8pW2q4@~*XR{IR>!#P_X12S-RuSOz(#rLSPfJPdttK;P@NJH6%+r?SA{@bpOeB{`zY0+(0pYnh=~Fu-j(83XwWq$X2n=6<_6er85cjf9@Z)-Y$*p3a z%<+`?g-<8Lyum^dD?8Db3gay*Yw2NNH=4d-a?R{4lDgyA3eA+EL1k=Tq@anDM`4O8 zk8KSe2hv%gMw=QN_dP0(NK|)tSuaUNLxD{%uF4vYkn>eyo03CiYj`we?y)GT;Rh`+ zgp8i7dgaMpcs%LK2vEV@urdHvBxw?d)QP3(Os)!Fht(iKENnony1$J$qTki3cL!Lj zNzq4ClO2zM2Xz|12M{H|nBc8`kgXJuhH%qrMVV%5Zb+C?pnM*yMGYTru{XX%hi6M? zX^An^GP)%!wWx0&Qs!mCuv3>VKAn2@($$$>A%Dojkl8!|8 zu2@b5U6weZ3JFWRnTWN-e${#5cSYf#`iZv5ujxq_UCMdzjRnH_<;f2+MdB=XAY>uW z6>nk w_p`E1gY6uRCFAcFR3!^CslqnM2@FTI(^r-y@X>uPa7zu7!K9T0Lk z9*+9W@ublkw|X*8uBMkVF6RI&cf+0D@@m0gipBLEW1|L*yXg#ta7Zw8`roT7wZrHo zBys>{KuA{xnT`7ookkoyT4pB43va{;y*KAs1iNk$gGtY6H>^Ft@1Cf_RUdWB&l zKT6A7l>Ex}j=!p~CVnRuz;=&RVG0Gz!LSdG`oSv{74gy~nLQx05lQl(ho_YQPh zj%-pjg))k}^oTf?;{pkKAsE>t_j7| zNyrEfA2%XxMKHyH$O^9MR46s6KxaarcNG|t4@Fc1A=WrMYgPh|J1u%bRT<}(ZTFl< zQa5+jJpx|rScjM^hmFb%yjqa#fE*3LZ6tbtuzVw@3hup0o(OB=Jmr(3E)$(J100_^{h3&FAbS1j-FxTzJP*pxB%S$7pbKE8af4%ZiE9Hi9~52xHMDC@TAP8tq!t zX*)v%Z*hcuo^WLvDh5s!@0Z*1@K?M2nqhr*=!$e`t%6}2PyyIy_!>$`);#0}DrZgwY7kC7+18ID^YJe};&D^S1r zX!RDZ>Ed(WOskmLBNwnbo8kg>z%VDhi&`fWGtKoKES?1IbJ^Y{cx99ltd8a zYB`R@pt35Y4|H5i4h}&}kkFi-U9D&qq&s3~NT0*PK1X>m2Lpz*;U(zepj&}93MzRw zJ!VZHY}34Za(svs=M_S|jYeP3Kd$$fLv{@k3dMv@`Pp=i#(kS zXr;HkyZYgeRzG`tJEdbw2m>Z$?tevZU^4)Rr_c69r7c+<1PEOfSuJv$9v+v)Q?& z@)Oq{xYMMH&k92?ej)1Zx>?U4cWbRt$7>44sj#+{Tq{1X5L*lr^!@4906lXow=?q? zor49hBRimB3OUs+?A^< zu$e@<%zGh`f`D2WdPyY{K2$+#qTCWIgwv65whAvQ_295Ts)yj$)#*D4D2mTH)1~WC ze+V;#+cBEbVqjRo&!``%u*LG=3vbM~Te~~kMXh}>I@vw{>6fp5^0PD4d|Q*d$+*_- z5xB8oSP}{WBv*)7a6e>aOniWcMFctAJMIq;IGJ0NY?-Xo?p<&Ay3@h1z5D6+jz9hW zpa1j!1tu7s?P`L9Ve)|nmo_}6=c3dQd0JKnbv3l2vr)oWN*AYDD{1fDO-4Ei=fe1Lz^`RL1ROz= zC4{C3su&adTw@@6Ts?ps4L+9gEREF8ZsdnfiV)=RQ^^d8V`TrZVFA04wUm((CYqyT z*+O_xLdY1K)IRj>R>7(O(iRd1+$&jtuqa*NYU{_fvHE|m3DWCq>wSc*ObXm%g{y3+hj@p)j)PM4N@ z_M<=;nr(|)Mxx-7#!UaS9sRBeZ;C@tZpxQ=dwz9=fGNl%)xT{GV$0KK2VlUn*^~+N z$WSbAZo0dJ>qg6RAVTdlHjw8nDIzwYiqZ^2aN64&o*YkTa#$F6Id+>f{q*VkY+mlU zc~fieY+<*sXHOsReenJ>w%YX?*##*tJF~0BpZ|RR)32HvAP*;TI}-->5h0)Z9zFl= z)4%`kpZ?D0&E81aV8g3wVte+67S^=MCu!pZXV7YaJ~8>6bX7^HVdAp^Zk2;=Zwabl~Kcn;9Q(gKLd`-x=dlFmZM=>=0ru z?iN2E9qsp@ee14y^zx7YcRU+#6#*$FHYyZ;d&OJebKA(qb+GSD)ovgwmYUbqYBfh) z5wr<(f;J}tQeQ0H{<`ULfUo`|_L$FHhuRx@fgLHMS|OkF1XvURA;U(RmNX3JKu z`Q2|lQ!#3X{oPtDSHvmwIMP9blcV;&ppbY?f8QOklSOPE>>Wh8#{Q^>f1H!cL5I7h znW{b78_V*?s!GhdLo$Qx^}~;z9PW)F`8l1+?+fi{=?q{E)5(iA}B!b#8x>nHn4Bk z45&(VkRaWiZ^UhVp(HV0EiTa=9v*>?i209lsLNJNa#mT~xV^-V_5;Nk2e;76i2um; zixn>F&xFyTj|5LCP(-Mj#X%Jp=$YmdtF$KHE~vkrw`57l?Q}~q?Qw z8%gA;y6AiiISuafoGeRb-f;K@xre-~M1`6ChAz4t8kPJF=7)<@rm1d(04q%+MSd&M zK%k)9W^qOSJU5iAw>Y?r>CHN`p+sk5*Fw*(PGZis?@kWicXkE7LtjgdKG0G5sW?kK(6*Jx1C>;Ceq zSG$i7y2CxLJA{Qjo{-sN=wisAv4|i7#jyae)zGMCiP{j=Iur8?GL)MeKA66x=IvMg zXMc77qmO_7C;y_mc!P52z+_Kr>6+S`>WXn^M7@sBwZDOpHMOfbV%ok&Iv$D#_@+(UE71i=pg6|U}Nkd*(t-~d??RUA}u^Z2q$h*q_}O= zZ?-{?oL24BGrTO##N;;=GMQ!QIA@NI*voB=VkK5XT!Bd3`Lq-uee@u_+q(_7MNXWE z&%GoMRGBAlCM^aJQfi?Hd;COV?6k`!Xqn%IS!N9{$3>A4RYnO?eK|h?O_nO{F|{Ag z`5bhr8i*2L5mj*H4Iy!4joxm1wA&sc7*f9>hb|K&pbnFd17jO#%jvYe*JM#ZIT2x? z8X*O@S*cxGgrqAY#cz9ETB^F^&bS}^BKfl3Ql4l}A%F~T)Ho#^&Ec>p5VgUK-sNAQ zg+XYd$u(-nt@}TG@#y4s15<@IxZ_=Ap6O0_@!8|k)8X`DiY}kCz z&_f|G1C<98gyea)*z|kto2%t?vwZx)drUz)e?9Fp?uh1h8^4giHpN3J6lj#h$`K9A z1)MO}1qKRF1qRGwJ_QtUL(vJua&~Kb`vch1b22+|Iwo);8zDIgSO8UBVAdY|na%d{ zVnHDRv9xWoM?83e+S$!LY);Dg*a*Uac9D;}AC6`VNHc3v%oaB>-o@#v zP~*65$K~!G4_|!qIT#N78XFrHtHodZ;_Az@@#!ZY|K>M#);Di|@+Bn(8mMUPKnffk zm;DzXe)zZl)@Oh4H<;hCGu~^AUo?-N%JR@ieI4|O=U3)0TSNmG4G7ORr9p~;rh^Nn zevr#j9W(%xNSQ^8a}WxN?3sB$usj`^K%A{k$yaoPr2v^?y!yf+%amPI!E~|U6*wOf zio37C7sWYL$xcxGQspd8&_Bde#*37mLIX87d`1Cam(m(l_I$`{Ng1^gaTkIgPyYJq9 zL1LGJWDT}w(P$tCpv|H`JRI~<)9-hkttqQTO62W^@*%;i@khN&A&~YR{Ll5(tK)C~ zyG(mJJ^I$`KmDhB^)*B6Sr_H+(kV*#rs!K6Sg%I<#mzB2bP8W>ehsWiJl$t%9YcfI zhX`W8f4;#bFoDmEqhUShDKQ^*kYPeGxt=^5WI|991L?@pen2dIG2ve z&!Z-cWiE0_5JZJT@xJX$^iA4^6epPv;{Vy2;-Vs{A;KitGtq_iC8Y!FI-;*GEKg2P(pDVmQ575?=Nu?y$`!~55`CE z6#kkoR~%K&Z~R&kLO&!1DnVl`OpaV=0-m8ND%R2FT;}~nf5~(((ewjKlj?bhg6Tj7siD|p8^xM`*N|^JsdyU z>ns-Y(cz=%*~OQy=V&n0Hu7S1b#axpMT6k= z5(IS#qC+*g)@zQ(TXRG#TboxQ>_#-0ZnnwkNmtxoAND|Ej>WY|W zT%hR!W`bE?IBgmSS68OqH;mr3b5Q1+vvRD&msG@|WhJ*u2a$&;#~52f$mhV^^cT{T ztRc5r5J)eC&tmlRi6N#uxU2kCig$5}G4aY2b19BSI61OCOD?MI)+-lwNPQo z@)o-CNyx1gzbi8>{Mej@xWf2K9c6}shq0Q$!M-9F)avK+5)hVZPXR$HvQP>hC0I?8 zXH0DHn+b&^>g;%<2a`X!sX3Hp8>)$MoisNV>Pk7c`j$AvM8P?urQGkD6bDw$YWY`L ziZF>1cbDYSUnLp|fjwDW%B3YhiEGRGuOf}|;&ejgxp2)yEk1)p4u-Aw#0i(cZ3I3?;LCZ&mC@7$ z=CoQaRkrEe^@kK9yW~OiJHcpM8&2ORq0|T)x+oU`WrmAzLEGHyAjkAM{DN4RkiJK! zg~Oa^X~*q=z_0^>Nn(D@$U+FQvrs-OUilVp#=F6H$?eKoW?rWqrwlcUYja?6iRL^- zj^=YUuwE*Cur0WtUc{`qsoug@iL4E>;j? zHz=M_x27EeCZw&l-R}H~N}tq{QJ1ln-Gh@MY}j|NUP|Di#e&i7D~h&G;_B_~c6yY! zuX)G$<&u6>g)2Hep!Qwc_R#aJ?T-7IAY`E!ZqELAf`9}NY3KoRzA|12)wIEA0>?!K zctl^WrhwMuG-G;muNI4|+4W%1o^%`&da+oa&tY%QsJD|Wa{mC_zy941e*4=`Z$UoW zBW4C(%`Xnet)t1Xw!S;NT3@2KjmFI88ZB593D4f1Q^2F>NTreIJvus=t*;jI_3T2e z8WLT6J3+t-tW(8w_nU}U%os0)1TjW#=rHS{NrcOpZ&@I^xwby zXaCQepPsLmo;+Ze&d2Y6=Rf?T4?q9B-@Vzq`QgP6{^YAywP)Y^E6vFfk=osQ0rSN# zo#9-?cIFu-%s2WqmgK?0)w|nV8WP_is(-y*U7`M_!YxC6ZLO235ijBa)-+*PsGQMqFZ{>4 zVbU>9T zHJ=oZUO6~^fW@g?W&qM+Ao81%WO~bSwy_c(?c|Il$1X)#-C21}!d!nRVxKfUT-hq$ zWijRWN$4w6;$ms4TlGO#D0E;ae4%5|L`Bg6Mhd^i{I2_JMC#{fZ?Kn8?=w?@vY(R( z+Pwh>B&$cO?wTwY3f*e8Wd0z1eiV(^fbisSS&R7_PB0IqJ*JbXhA|r&{5I#O8=-dJp)4%99ju&Dt;CeYp zIxc;va-giJ+8#IHK20F3v`HeP|Jm+jMjvQqV zF@58Ri#kz)Qklh{vtToPpB!Gi+KFS0bDGFGI zV4iLwWMF|pCBaYu3qApMZ&U%9CC5BTAo?P5LQ|X(~>NPX&xEC>1WC6kKkwGW@1eEI6l@uLaP1cA^o%?iDH7|X*6!-!hw#9y3E zC&!0Zuh2xp55hs9Hhnw9%WXDOMB-N~_1OnqmNDe^_2cn?^RhbzVQ1O4gl^4k9cYjS zjM?Rsdo@fWCC{u997ki&9PLkd^tfmm(CLw2hDGhZhwAQZw#2*c54$u-P)VO%U!Ttw zug(?(h;x+CHrG#{?|=XI|5A55e|>pD2j5q(U(;&z$%lvdFq-+#FRm^w&TltcrrlHD zBno(ac|I78h`Uy`+u1@9`c3mIv?YdL%j~bKa?q}cp@bmADACFt0$v^!+-u(P8 z7QeXo*}Lmc|I`1b!4ONjvWWOp#_(J)AO+B+AZKny9hn<00J?<-g+kP=V;C|dvMcn| z;E_2ARK|zxMJb928yS9^ipuEX?G@)m^J&9?yyqgl5Jr0>yMjoh%Kt5O4!|E*7oDVm} zOlG8~QSbzEn?u4-T8u+VR_A9wxML53g%AI>B4%2vaY@5S@Ra6q<FARiEPLFs6>p%gf+mKwN%RZ$Cy1e&_D0)jvFa@!606-+u=1$)3?BBj`kTE!Bli zpSxMi&o+xI5k=aD&3?$|=(R=n9$PYJk{W9Ht^Nc!;_?Uo^vUP{WhjS4ptQ6STdCPf z4^`Sobp13N)059Z=H9vkrN7~dN1_cPC`@#WxeQ@*QXY%i*>=PRcPG=bd68rl4Moa( z+a}N`ioXu9t<`k0l%G_p0SeFt|)jEA#Sk=ctdBpHZo z+-{Cm%d72TiksCURElcwXO&hoWORSE(*JJ~hcVEpvEDbFUp$%022S9G8?VDAxl9e3 zbpxsfG$k%sUXv9^$`k^tTbFOzgsPKt67S8>f?A8d{`&H2>bJ_nU}v_%G_$ZM4NP{K zHYjM1A)N~s6K3${j!7{f5n~RDrbOt(H+xc&ua;oAN;IPw2z*h{_Ya*jL**^kzoUU$xJ(`Wxm#;~0zz=V)uU2#u zQITqPh6lSeQ`D}`mo&5hE>B;fQB6l3jDD62guREcm@b&=aLXUziMr7dowpgs2#qQ= z9WXq6HO*=_{l3gH8WKv+SYiQQN*F~7N6n&Lr$277wo{&$0EgbTR}2DPE_Ns2s2C+Q z5h(c9-<&VMdP%nK;dj6JVm7_Fys5A5Mz3Ffh2HNcA3j4S_mU*_Vm`lA zFYff|5&rn>Y`(g$4Udj#O`X3|R2w=#AJz@kg}H`x*lw?Zif-@ebsr+^&(~}>PEJp7 zUyHLh&FT8)Z2tVO{LMf7{Ja11fBYZ*?ByG_$85IkOm;^Hz1#i0Z~ful*?abAbN=)7 zkN(BmU;JeH)0gwJ_5J#;`|0oOJ%3L6rPMNTsSrtHs zc9MJ}*CT(?4hm$_RFFyjl*1w5hY6R4nlxLps7on1N07AQd*o`m&GSA3I`ckbs$_wV zdJzPQtV-#c3O<{hwL<8{VbwiL1^y68K(L?@|SlaeT?fN>~XuB1u`$K3}E` zw3UVAj$vM4eBsj*^`fGVohIC72V+96F>4g9J0N+^FISf&z;(Wc>+H;ipu1^7E@5t| z+?R~ij|}kP9i#~9O=^V-=g^?h-x@q5Q`Hg(!zO3Ut_WSI;tH50@=|P;F{F!vMW^LK z6=gn){ykQvd^80!-1)l3jZ3mX_Y+9?nnQbkb98t2<=)AQX8#B0U;MZ^s?#^oqo9+x zs(~bp1)U6NMJ-e{ zt>a}IYWtvm3eS-;zZ#qlIHc*?h(#IWt+g4oOXy9K8|T2-$&2g_v*uXf0TB*e z57i6<1b844cO-hq8bi#9r-}QqO6Hg)g@`P&l3TIU2x-b7F>7URc86Rzb3;$5*_HF; z!68IdM9_okXTh>@6o>d``qNV$7FXyr&2wl?fL2Z-n6h63H4UcTbgca|0tf#n&DITW zqG=zbD2%;+9P?0Se9X>jf^ctv#_l7jhJIU%2zVjMvvNM|e#(y4KCol?OMRJs0lXN6 z9;O__AX?903&R95ydnTGeZ=ajL#kVYO>6tV3?f4)_LXrIqdV}e^q8o;=o}@_W)7^M z1u>Vmp@IW?MA#9a-Znie3$mt_=i^kcYnqv>Iz_Sv}g zv|Ib=c(+?~B-o2jp0KrFe(?@6Grf28i?T- zK)$~e)CnALJdj*CIjESiExU@(-cI&*8C6EftEZ)ruf;PgLd_zR& z2kZia_1ew>bC{^#Dx+|V3#1_U_Uw|v9vsf&!`^p)SLX zn%;mRXKxpm6hgK&h^cH-;>9^Wwm%w=hSS+}`Sy&C5wrj(9=qu%*ty#nD6wHdPf#>P zXUEpT9i&ZnmX}l`TUYF`_OtJNqqn{IpZ~l6c74Hs!kS~6C%wD9(ThL)dq>~+;BN7H z{*!-p{^x&o^~I~j%UQ3#hh*E$ruE``zejzrMrqYf<vk7K zf`?M5kNQ2!;4Gj4RuD!z{CpA7sUAl^ray`qcVwH>D3j-TI<@&};tjhdL7_UrndIl5 z52GD9A^|_Y>%%(euCYNz#}dsEoytrTKcR${#oOxQFd-SwW~LO8e(hMK@{N@v#;twG zj6L)YGh2w;N#d36KkKliv{^FIk|DE-7#Qg^*K5fOG>J<|)SXDX<+#f*6gg6JJUO29 z6sb7NrW{LtY7CJ8#q2KGA$Hg`_mGRXnHb!ogdz&fj}^Pn)oCG8mhxWHs+p%1A77pP z1WqP5GJ<)Ya@SFCCw0blk-%z1;l4N_PsfpZLsDCyFx$RZ%FTJL{TL0E+6_aG?mqnH zU%q^OzS+Jd2sKf+cU!8-80ZnbP>jKL)oKqQY!a0y^sDv6YmRQIPcvwGG3SN7(b4%Y zUR`f4HnVxhSwb*!*Fuu%zM*A@vK^c*zPUr>$?p_IbvDd3TI+CB8n>*m-Fo(M?{J_P z3B`BR8<x7oB_&u3=fraR|xKlH?qy~_9xoR}@nRrn&dSJ3e#`0BnB`zW;u zV8XQ20p|u3c)Es>0MH!$|E5@N$zr zTqmU@_G}dxV#gLwy6>@?j)JtI#~46N7$#7*Hz`~`*JFcTo*}hE(D0oZUn%|7esl8N z8w-=_-Dp)XKDK12gwvx>gB+QQxww3Dn_NgqS$X4cmK?Q%+I35bqnlNlD{TZpNeD@T zJVco}+>wVo&B7HxYQCDmP65ROQ$74jx)3i|I1j#3iZ2VNaDc5*?0puW$q$YG-S(Tq z?z3U@*G|S4Z(p|$N5Ao%57uYXS1-XV8`mv#^mc~ZI`d;U-JSUvQe;i$w{mFuh|KmI zFFt*KJD<_oI_@?u&KJX@!TV1SmS>k|(+$SJ$t_!`S$BBB>0$#sKG~fp$iBI~SY6Pe z{_OdvQM*2RG~PvPX1>6YA-j%g+H4k7;OQJ{cNyU{p?w`8u+kzIyn-`BGcWD@L#q>; z&3t|{Lv|FAhhdM(IrJ`Sx(DNCPucbD#l_;?#riGdDyWJxxa)57-e(8j`;AW!i#eO$ zPFHs@dr(mN;N$>&XLhx^ym)&tn^JFl|8NXG3~gtLa_MB3eSUGy)W8)X5)}DkzWUccrlXPGI!;7y)Vmw? zPCx(Ni{Jb02C}2S_`fgy^iQsS_GWpxhO~cw(_C+79G2$j(eTB4^)74$N27`78wv{R z#jljn^DIo5RDtz$guzIO*Yta;gwi!grO=MqjL(+3>_v27**|%E@hhf;y4ym{63Ho< zo|`_x9Xb?Mhf12FkhAApyf3XdDevVvifM*I8N@GcsNh{5K+k3Dd-iA$>i}Cu2aIr zaAb@=Z>7xGM54Ah2Pa^xkO`-cTMYCj&}!O1Z%ftw;gi6XY9uj}z(OI!FYcSzdVo0S zmq@!=_-S-0YlYWelGB)4^V!V`6NdFO9WJqZ9KJI{ya9e=T_u45_u;IDDoZpL3fSgg z$ZBkwi{mp9xJvF=_C4n=8I#czh+cRB}H|sJbkQ*+bEaq(n>_xjbn*I z$sHP`wfo8YpY9(0o402dI1*5;9WXIGnW*Gi-`(7SLqmoUYv^r|wgJzReB9jZZI-6T zTXcKt_smoQe6_EjMdvb&5Sbv4y@xzWsaf=U2V{u^3B1zoPJ~;oiPp7$ojT?Mmqh}O zPKsUNFggR$#SNgC0*kV2D5fm2)F489Ik8~)8hik-HU+*L11;8BQDVXrTNKzTj#Q*o zJ>@Q;6wBV=jkZM~!{pMW`vpW+kl#m1(vvw+fm5Uw^Z!g|NRiddP(#eYp(%?p7}VD6 zaKp5?eZn`#IDiErI_e%EZ34fIHj{dRrq}LrtA8ysTQtjgF{&Yoee>glLOf7MiI!%3 zPX}F+T%IkM_B!?J6W5q4C*mTqU~|5MC}B`+%&6yK=G^_3CQ*pEj>uJ8AzI@G(c5`r zYoNms0n*fl6xsi1;56=Ah^~4B&|{_p8ah#s2J45dUIX5TK}iQ;y3Krgv%azw1;~VN ztFwlg<+D+DPL3ABs7WHLBu~SXs99V^*jk5OXs{lr@|$9x{I~i>)#R_Z!vH$O7HaP~ z;?n4o0|x~2-f-u%-FY%-p7eIkFV0_l`~8#s?$z7#i&qf7uJ6YC%bUf)Xz*m*SzpZ0 zSL?%mYevlDw1g(p{towg*B5i<&f`#CEmxm@_Q9xeJAL^MA~RDZ5Voevm2vH_U*Q*5 z-O1qb!Gw|V@6M)plkvg0-$5yK1?YM(X)Uj)5ICYJU@-}-%=ZhC_) zKln7wN2tukin?)9J4%dDxh{8)A$qoqb2!0~oJr~dHlQulip&j-`vS%CD|!IfX`x33 zc}TTcyC8*0qD1_a>063v$&%s*MK9yd{LRC*w1$V{tSG7RZJrlZ3OR@w6PhB7-Y+FF zK^8$UT>+ca+Dmc8LLFkQfenom6^|lJHu{e97b5bV#9rZc=4Q++Qb{Ny-+YGo?s-AH zO~DX(a|tfen~)gUkKkp6gHYmw(r6e40?#}mIT69uZ=&c>{5>H)76b#xG#<{|=8k;m z+8JN1A>QC_Icv~|K>Ub1FOwRJ!z1m1A3;00ygUni`lJ+-1f~kglaJ5ok7$HDBwr*j zX_m$C#9K5z7D9K`zJ{bL$h2TZR$ag_WX!OS8X%uI^IpN)BS+^c3#9ByNb~i2#IMs@ z=h#t{kd{#g{Pfr6|L2bp#nw!rNz)$I8f!Z1d(GX=;)jFL9?>0Z0_vuvATrnm!cqL19{*pG&6M!jRvYu4Vs{go;Wo` zwd8Yn7R6^gB(|qx`r$R))o4D#QkdUG(jw6n>!c=(%?u99nXWl$A-^9FSt;_I!ZGkHQQxNbXv9YO#5K^K^D)Rny93U|%duepNqraY>uh zDk#YzrA=PUHp!=K_gjtspQ$(Nk~7QB^J1RIoD)b4RY1+vY<81clt_w_Wpim!y6`{t zg)h8p+LYWRC02D;6$(Qlhsem7=OO((`((9+!a@O=krChbopbi^?svcYRjqPXeK=%X zgVRa&zy9sFRIgWq<=}F-*i{P6Zhtv?dE99hw!_~2aERhGlu&hDTkUGMQXGvZoBFO> zua2&I+v4umzk0qIO>f4ux)uOkiuoBK5&g8cH>2yx^6dPmQ7zEddeui4snR%vJvy&3 z(VlUG;2cAy9W57V-|cV{oXs^|N#_^G3|4_iLI7ycye490{iJC#;3dInH=eR)55$U%E~{_^72 zPhLzXtMMd19Skood+>qO_}&yg4p#`oI7dE{MNkbXC3I~Lj*lP&Auh9~I!Xi_vrVdn zhqDg(T@KD~u|dIPH~sN#W-h`9X?C$(LZgMeV+=)aFnhC8d-cEk=hM%gPT&9I+rR(a z^lGpguL!HwQ~Xfb_MpJmwfCjU@o#=pZghZJq;kjh2nq#zj$8p7M7y_0S1=k)1S8Lj z+zQKu7j)v1^Cc{wIO!G>&UYW3LHHTc4iigp7=yaU;3)tc9Yx5Vsdg2?RYKm9Cn1HQ zw4zQD=RcgV+c4!GkvthGBfJaA&D(vHj2Ky&o@VJEqR3qWUqpL3x znlAv1fdi+KPwamvYR*v9b$Ql(!-OvKk%9c`S3ChBMw2Ko$#Xu!ypM$GU`2>*W$9n? z>O)XC&DMy1Y2t<$#2Oi40Il&*A~E$QqAx-sMWSOv;V{t(P7?hxGul##hty%g+Tt1k z@GEf*W^x{E<9$>QNFr&~^6>tGky^?N2$~qs8NM-_DISB3e-;6vq?a7FQA#R|B#RK& zY-L4UHR0e{hUQ59cf7l1(H8SiVlw+QWAUjAonnB!Ef6sL0m+c;hE0Y(T*`C{L}2b3 zrw1n|r$k$s{01Ust<&19>+{8+R%aIB$znNxg@HmMpIt81*@Np1;`Pud)NI6VR_kX+ zXD@cy`ffG_lO=T4l-WdUW)6MDS}$v^tSI#}oS;K@1-G*M3e?M(=40*^@(DZ_VrPd} zr2beSRw*G7`8c46QYCdh3wtao?@xwHUKO%HQSx>Yg_?6BmQno1fAA6H3!;07Qt8&n zwlXBVB$k|f>C}J%xeac>CI~%86z}Q4^HkA~;b_mOWNg8NOfLC02#uv%KUq^xCJc6L znF7a;&yMg+EsYJG8q_;U4eSLJY>%WXm!ostIWiIT)Im?_&Jvs@B9j{9u49rgOX&xB zeAo)Cwt;Klkep{di0BSzXPjcPaC|<~lLPFfGKJVW^;sTG^kW(n6OQnq$Y7E2Z&ssf zsl_SQvoh$OeoBH`#Yn++Z2`q0fsdgQB=KO&>^3_lvZ!0yRz@dWb+s5!P^ZbQ**>u7 ziBuaHH((=$_9Zf6YlKQ7BnttE*zKO@DB}bZ+6XspDHb@2G66%yQ8##qx?nqG%y!@^7Iue6ooAn%(R09>UZeHA$zzqL^U+bW!kC`-!-;_m zczEiam^#(Qq{A-}Af>2-p?NBhj7m!ii}FbY+GG9vnif zf~5Aam<&{-#Nck2i?pR03c+Wyo}RwAU_|iZ@_N!6fjT3GgIoi3enUu}UvXEiH;LKRPChx^&E8%9Z~y!F!_9KQh$ny%LLY)hB3IB$I8>f821cW!ntjqCkd}y?j%@0e zNr+_%MiUsIUkLJ45Dh(9f*7@Bue zgq`AYA&MAAHc&;s!!{2TC1zv?5E>*d4`p=0s>6%573vo~(Tf_ZN;lz`vpvi_^?A{J z;2j0`l`m`0i3PC(=4P5~Gt>1NMDbKg2+*2yv4O$70Wko`M_^T%?qung*wai8K|CuO^K;#^*-q z)Q4tn6J7L=O5|Y#QXUcyCF-1dJ12pF%Tf5pq+14+jl5sRjfz=^mUpyK>wtM?;$yOZ zL5$afT>0do*33@FG)NsDzqq;i1I4-BZdzwJXtl!~P_-*JC_j#(!;UW2WuwOQ6ncu- z#y*f|&t6&a;WPcX)qZ+Sr=R&NQ6-2u)5p9*7aB?n$x%#Y;U?u`RB}p4JHNp9QZvcB z_92;r zv!huB`<88$GbH6R$&3I{d9b(yLLVuTMWnZloD^HJ%C>=9Px@*?3?#c{=32s!%Z{OiiQpQV$~mKo&dW!niEo+QKWv{I87oH zMZzMa#;Dk1#&O2V3Yd5Tbl$R;1>09-heUteqT87kHe$8-;S%oC#z8K0$y^22i`p(x zodZU~7I>x$yS7R zWz#gUo^Cyq>tf#~cNYiDlO6H0KW)emhcAWZQ6-Om#jE1&2#BqxjHX&}Mffi_mDjcWf>2Vh* z7p0hPyJ{hDK6~-S^OJVv`h9PvQ7BJW8wQf0R8J;nO{SDNKkm~0jT{6)O{Y^M&_ERC z`B|;qZY~gdnXU0v!@-!FLoN_w+iK`R(PJtN2@Jk z?#H9~V!ok!$?3b99XKjuMlg8jtzW($y}yLcL*>6TT~1%WKK<)|`*pkgaE)qke|9&* z(Qm%}>^b#$6yk=%+0FZpli@^6p7*L(O`o=;qX02cmm`m+=A+LZ^Cha4bnn8!AuT2O zgOpWmmYzR9Z)LXQKfKuus8kmbOM@*w-#j?>kX=(NQ*JnT_44cg^q(8$)!>KUUH{kb z#y=0{w;{J3c?@og4m2ScLRXC<6Sa#^{^oDbe)Dyq-sU7Hj&<1FBubL@hWp`{COVuS zm5i`w8?aDlzM!!aHPvh!gwEqkmZPA2kanDf;AZ3@lB^R~A984X5(3+>fS{ ziP;~PUZ7Qr6D7ykfB{zef_Slo3eKuIP3+w;`8pBtK;2awd+#BxAZ3J@c8lS}4>+jB z+)x{{p-@vJgq>nOgM>dLQV(Pn6UTIhmG?VbP={8ez7%Y@X=`baa-G&LX)p>?g zlNS@xz>$$Wp2dU`#%EavinooEsRvG10QMOR^Z22n#BmVgk9gj`d9*5GQzGruhdt8F zz4Fwdtm90waH+#D@M=kK@j1R{+)Xk9Q3uj9OWE~lKUPH|&rZzoc%;d;#&SHyPvOVI zLPk?SSdHi`lx>*SH@v8A44U1rNIc1y)&$SdYS|K;VtjSU6-tze^ptf)F@x7ru4gmG zBPO$4GcyAJ7*GY(P@S0qE7{_7*w6Ogm3LDG&)29?X}?`?`_lMiy%fnIbP%patu_BI zSR1N-;&rjBNOUm4MuDbEwzEp|=QQyV76xs7V>G~Kw#h=)!53vJyp?RGAUlfy+V3t! zTGb5qM}(8gmBFcj0Bm;09qO>$6;bFVMBoBR63o|ZOQe2bgJwjT_zaS&r1LD4qrL-l zLOpk2jSALyou>SD+DRT^?dARj%>`xE; zrKbs)s7-Hw#V_;++-#Vq5ZbVEBeD_fyu^y|5c+-Y+j@IgfiMsf5!}hiQflKc`|G1p z=HEQ;o=AULIGPLY&U!($tI9M@GQY{-_TB8}o;w>-1f;4#>dFeVo5fafKj@7K&AZ(? zDx1KPbZhpm``K#s_2(z3w*362*BdSJRr>EYP@U+ZqbG>*C+%kG$w6y28_hGh=3&hz zTPVCd>z<#~*Q52_#{msRXmX-@hD;l35!Awu&d41LBN`W0I~*e$P?XvYK!^j*T4p!V zj*lG7hENQhHd_zDtv60bDy|WV81<*`-uJKjlqb=J%FH(FFaGS}H=jQPBz=1~?$Ha8 zE1q_lP1u;BMlnM*do$>diFH`1_CcFA-$@J)qi+e#CWb;qe{7K%6L3J;3M?^QkIiE7 zv}&e-?E->46r|eoVVD?AF}pUuD3y=C`BRkj-NaGEOEJ(d{x4a>_(PJt;0VjjX+%Ly z`(ROpCVJ&#Vv~!f1fM0sO;3VJq4q!3If6rys)mv6g?$gG(>yPDpy7k9jMI?u=sRQ9 znK~Tt;%Ls5j`_%di2{yvw6tTnR?G2^vnIr9ImzGzA~31fP3tCr3m|TNgTNWOj0v#RRu--0+qpTS61Jb2fRH_&e)?y5-u?9T`08;f@Q?kxSZ@?<70uF&4aUV%lxozsnmoq!-3`E~S^#K8lL-~73v6W|5eXtP_f?(1 zz$eAvYcQe~b20cqTT7D2e$ zeQf-l@cmQ(Nvww8DMYlGj)!D9S5j77GGP2w-401?>pwZHe0$hv7HNx7C2$SD|2FV~!tT4U3Lu z4Vrkg%H|J`>5*973?@FU40DZum5D0ys7$*PqGe_Rvt_=ZjqZ7i|ar8?PttS{O~V7QM$yv!u9*)`SEU^xxSq&96JFE3t`k!M|%jpCdrGjwAl*L+q`Lx5l2?(vTT;@-H`?@fnefay|Jq)QR zl#x|qT$`?Ezec@D{Xgb*woneBy=46U|NcK1`zzzs)$~XPEl?lBgl$nt2#+B&41&gK zsysU{KRG0hmL~I<^Ct<)loLdwbacYR%B%Bh))7iOmesJg?LKxB_LTmKe2)4a)(Dj*u<=xBA|wAj`VmR;Ezc1plOVO1m3ia^;QI1F~koXS)Q zCDLew58yG)SF^o=)s1%m6b^Q|wv$VWiU0s007*naR7dd8xd+4t!ceFka?8%W!qTAP z6KOw*o(yQwd@Bk+Y{NA_5{Ye~uD)wRjeP|SHqmu06PS39Q=NTZZgWv<`lCd zkqZEj<%_vLICU61+(LG$#u9J>Nn zU(Sl@1`&6)(2O2%8jF3T1w{-2FhR)Zk+5V` zWnonw5XqzkP#}}+4Qm;(WptDU`%Fl~%{jg};!%nEa&|&~G=;D7ZVCT!`#iUK4%vM`O+9{v z!zBuA`P+cnkF2%2bO?-wwZ+|Sc&}`|45_!tY;g%n+BoXmOo9x0m()1M=|ErbFrX}= zToLh`y~TmbNMH8GExmK`#3`zbX0mk}&cKY@e0@*|KaCby2ShQMQ;vzo*y};Z+Y0ReSX@4@Yd+G7+*){!r@W#xLt)L0{f@ZsF6&q z=j)G`_eiO=yM=>x{h(1B@Gej{vYV%;&GW+p-e|g@GRlx^c+wf>6CF0Hy{rD&VIFc|Ni!x1_{!uY-KoGAnWqm-@ZO9XYW7u`uDTRoQWBY^P|JXV0J%RUfttn z1rh-qc?cW{x{1VFp$0QWcV9SpdeAy-JE(e&?pc;Rc(a`xb&4+!noPB2_F(O_`Sq92 zr|;k1|L#L>>O^|>DMMgrwIrmgHkzn2RuKpgU$D=8k9y9GYttBMEb(^H;$R1qC214p zU};CN520dL?EdDrr~mrb)LnqiRi+P{7a2;*29nidFQ^ow{|nsGoJ4U^)YOJ(suA?q zG@*hNM_Gon#y=)jrxKLRn}i2Gf*64c22$vUCAlf9gcR!7hcYWt7^vYfMHL;iXyK;S zo7-uW0#b`Qfu!317s-lPm#Rj=ic|-VuO}JAaFq5H?dNC{*qyDmp+nMRqO3V_5KxhR z4#+B{w!R(eIburT!A$Z}T5DjVI|(c}B4&RiX{q?sgjL0H|44HU=rVO{*y`t@i@OXM zG69VIvSOKP^2VzPgfR|?3M)WU=%V=((Ftj`9r$I)_S=aS2iGVz9!8W^?N`yrl!Svy*VAv(655-INI^I`9b=`W22I?nawc0TF% z8g1mLNY_w7LB(l9B4NrngE6n3!)66Gv#sVElegbzXZLJ5>?|;w>XcSyaW}w#YxW<_ zab$x*x|dcYUd1+4C=HK5%?U9u;|zQ3-B8YmVD_Jc7t)}3y^;ZhK}c-SjL3P!?V%c( zW!M7VolG#rOqz4a>A^B{Nx2^mO*G!bQ4_|u7GMZ!Bc55S`k;Yha(QBh^mmZTy8?iv z+VMC_k2nL$+ zBS`PBH}-VIxzlx9i1YI7Oovm?$5^J5^Q%oLpAU&OYzQBGIAq;;Q@KH3K#zAyv zbCF^k9@Rk5)t*|tYB&Df)1%6A?3Dr20kRSNhO1`CBI9MG^keEF#1=Bojyt`(n?|Mj zv|T#xlI_=AM{kzd@ zSIoS6b#{DIA5Rt^Zr8oh9K;iVlf09Th4(*R-rejb3$l=0tdkb=zffiJjzJC(!ksBfaboWX?7AMj+!BWf5_2~ zc7+5XCe7u*kRz1~sRO^GDbs5E;VBCbd60Of)^wQ4K#mLgC95;OOw1k8hB%lgW%6xt z#Xi;GGbPkC;^R~}^t;mHqo5f6*q`uuNcMMoNrFmst`K+&q}Q~aBs&T?KvOEGg|Cli zn$Xlo3^oDa6nJ>SX5vk4SNAuUi1;BK1GtAPt<O5aemNPVEc63`Yg-N|~Q!>>`%#}HGm14~a;DL9LaXA!OTpAHw@{pPtyG3H@EXGbm zdyKy|RI>X+A8g!{VuLs-=Kb~$IG!FnAz^2h7WoL#PQD&;KW-1fCR)yem*s!{Uzibq zHjRk4eLN4XGin5OdmHox57SMKp^YsWxJz7vvuF95_&0qYkR9-50B+9O*wI4HaASqC z?XSRdk@2V7*6uQlWqZ{cF7>qYJqh-3Mu^_OSfRPtMP`^N0C}SaUX;H|r(g zB?lFi?rtXl761xNzN6{w?6`VzSRLLDacTq#pcm)lLzN68-6@8iy*S;k*Ki@#6x(O- z`mCD=`9^TY3#-;8s2AQQn?E58GC|QjC82F%^#ouqwxkwZoFrnw!(IWfQE2#F`H0O zq}%Y#4)zhZv?DCBKLp)#hgCEaDgy+?%ODyU&l4#U zH_#1>XKmG*7f)b>hXi%_oaA;Q77F9`m{cTKEWT{@%Y2T&mCClGWi6^HJVq?5g$ZtE zAn3e@$hoE-rC$qqfjuTNsxi4rzHYK`YPz^2EfJXz$W(YtZz>2IVNWqh2$Cg-$Hj_) z1TbmECayow#Sn=IXCoanEQ&ZS^&!(ZrFv{SFwz$XGj#9+$e4CZ>jJbw24Th)1(wzp zt0_yzi-)$-fE5{*abZ4t=Fh@!Q-UG z04`Q(y(~h@q>mz%CNWIB0a%V}k=!6(NC>%mAi&x`1eftyp^aCmpIv1B@gLY8Gv{kA zs54coEu1$7UWI@{WwjcU=`jC`w(dfug~x2Q4zQ0mxBo4>ToLZtT9ThgGip(kLH6e+>4x->`p~0IG<)-7#>k;e5 zXW{ddGKt6tp>JpkZs2H&7kjm;=i^~@qRblVNP=`uIJV^-?Q;4k>~xaSy9Eg~rIhCO z$n6&TWXmnX7SdxN(b^$_>GAwsF{9!J|i2(pFQWcuO2$4NwyR*FaM-seE#vTlfdY zH|9$9X0g$Qh&n;Qk`_p#JEp=tFnBL^q^fWVmivD6Be>P_U3vo|kbW%NXl{bs_5aTf z)N1LBBRzCydN_hE<6o_+9v3(N0TSbU$hT=?#IV#`l}@b*laL#pj>dQ(dw~Gf^P4~Y z^1M-e0G6anRsEv<{IkzrlpfYMAMcPWt2L{rEZw09N)ymBe?M@X0>CL4@p8RCI%sr{ zTf^bx_dnf&SGH;hxH3&?c{kYJ+z%NR)}p&Fm!a={a6ccdGtGAMpixD66}|w|`;Je$ zcs{%!IoUp262RDQDs|XId4$f&^xki?gTb7tBUNS^1#8_VdiKo4-ES9XPfy^Py#48# zT41NrrupP<0xtS+dQf_CjQaK#nDu@{NiIj{%VfEmF0*|osurxu^X0BtDJj#0+*_Vu z@lQDbGy~#(?|QSzk{o#k>M9Pi9-lEdqLOR0P~YD_`|8D$!`9^cAFh7)rZ5jmNUlI? z%{)-b42^E1)GBvgyvWtI4Em~;F=$f>RbV!9&YT`>Bp$q{sFPXhCtD@C4dDqE+iy`N zqk3F?_1Uw(`PcPMi}~Oh!N9`wm5=aO@`YiZv{{J@%7G_>qrOPo&HOCr&J3ME-4$1C zjIE^)FX7?QjE3r>JvZ_xf~XQ{T^kRCV5HJFUNI<768!6RKW;)CG6 zYIJ?@a)QF?TJMy4`| z!c8y&dS)A2f(~lCx8*l_5JQ%Lw}anC+!kO>92S!>b0Xp+NU*{!K@%lamIY!DyWQ;8 z@VRa}p3R;l<$+w{S$HKMzGA2;-1N+zo~w4JD{^fs6!jZt0jgI=Lx{V<>_`v-+d=C> z%y^O!+7ZxoFPn2a)Iid49L269D3`3-Q%I80|tRp!LnOa@CZdO3lDc{~b^ z#)_%Z6BH?rKI5TFCK~5%#1*I-L8GxZ9Pl}gbX-b z!4}#O(>Yiwo6Zb)@Y*af8r+@eEemET-oF3L#kyQbNbGqIV$&sQl?8iFx zE;q)Y6^q?gEj2$AI-nieIV|2Y4{(L(Ne%YTrCIAd?Oa@(tw+7< zzyIg;&APIy(vXYw=DlfZ|peJ{ep=lU-dt%e)N`kT2tuG*3(5L9Z`XN z)q{O!-Hq;v{zu2$D8Btjtkj~Uwt-|i(`Q+cOGY8~nL>RY?4)dHT`QW?jlad%EMNq0 zJFw7Thy*km$%KwSS{-cS@hjHE^j2If;1OxR zV)~G$Q6iRtkPeif`o z8E+P{kgtTgfp)~|KT>fDfU85pdkU7~k=N*LkS$>xoR`aUM>Ex*pxu|8mu?Nf0V+y0 z^vr7(FQNM^LCzIOw*{VyQnQSgL2-}46sq9xbsHW16e5(+L{vs4c*aT*X?>aUL-BP? zrfD=Nu1^5YPS&AX;WmO|tfQrSK-A2t#sTEEsUVSEr5Vg?+p5P|sy*xg340XNMi+X|c=?%N+^! za=mOE&>Nn=IPb7QuRabK`M_qgut;vzAO}9G$(J_;Qu5ao5 zYnCd^e7-n6>|&!!OuJpI3c05*51w?IB#bapr~#lHesp#S(na}^s|Dk?7oPfWW=pdJ z=b(GkZgrcNZ|)`&Tg{=BBArXC-72@~9v>k7>TRv~#5?Auz+P<~9KvuWktMw8wkyq2 zv3J+&&!(rxr@X`cbWb~Cn@J+1=Vdh-P<@#(0*zxA4YRBoW#3x6dGhQz^LROHOiAld zG2|FXSiN(9c`}qdRXQZS}1R?fVN(@O=Bkc>5pgG`yaeiYowcQ3OB{3N?9`+je z&LeNxA6E<8F}5VUsSnXS5`(sxY-Upqp;0Ly3C#nzKqG7d{AC2_aoG_~2l&A^@qaO8 zUS>kX-{B2JJOn5erHdL|1O>WW?pJRY_hs+75x)=EN{{$gR7c|7gVkfU<3WmwkaTw) z6iNq*bg)kVRDJS+4u_wM=89-|P@ygAppklclOgnI#Wj8?^u6OO8*n6Fn3gj2XawTu zr}=kD5Q)QSW7(S7lQvyJF%Kn@WE&E$nQWf-7Ue@+jT$AqQ(D|WGUH`l@*`dy?SeY* zw7_YxMYkO5&NB>6a?w1gva$iqI<<4U6B)c-jhBo6$XDy`KZl~vxv|032ect9FTyIM zT99ASZRX492Cpzz6LfpUjd&JTt4`-&Il8Y+uk!4J)n>{GoX|x`J^rD>%(X&QV1X8- zU~J-Vl&7ztSD`oq6)K|)y0`Vlu1KU6*2#i?7)e=lt5TARI;%mA$yg*E(OsrVA})k& z7lb6`A`%yxKiI^m;J7Mj8eM#wGi0IX*zd>~aJvoXM2f(S|1WtM4i4 z$y|Ye9#iB4MUWlCytYKWcIFgB71Aaaosv2y&V4F~ALxY%(EFgZgMm{m;%^bYP)L05miM5_^B5CButQ=Pc!chL zgET??AbAnt#niB2d5{N7C&|@J)z7r2Jz)tSh%*?p+JII38tMqos`)=Zth}t|TabYt zHe^%?xV$<)LV5+d2UpEw=h-hdyH7tmIjEE_-wcNRaj{rmGylE9M$Mme8~yv?zEXVg z;rX;mv^n~g@RgCi^d^uzlxeK*HXYA?S$eEt-^_5FN$ z(5=(q4vbDc@ak%?$=8`}fyxZv-fTLhL~#D(n0(>mhnq_K_^^BO@%sLLzU_~PpFKZg z&e^WmVCWJB3DgYd`}z6t5xl0!bV*)SJ85^GJn7$E0ljB8^Un~%Dd*vlIFx9!oj&Yd zeg3-JJwV8YekP+2+Sjm^h3$*aj=%cmQ}o2|e)z-PfBoqJH%CTin!O{+C46X*2%@fi z(8$5g9F8mH>Uuijh+5KA@6Xi^;b`}r%I5+udLswOXd&YZY5r}D6x07%;e9!YmOAdNP-q;kAc@3x|!;9qq?rOQ4Ph`Y9OOUm(#gQBaOXeeZY(e6o#4tQ?KM^i# zR^j1-Mqxw{LBby+2A9x4Hnn5vofGIAt|mA~kqO0952vT4jE9`6c8}*euvY6HrWbx( z;H5xwbcJpgmV|g8f_m~%nSSPaDd|nAUqH#OWLn-wR#kG%@k{U^bsL+u>kT8ijvI*t zKFU{7Rg%y_#1@ick#W1evG6HGe?(#}Cfy;>5*S=Z<;JC7O zpyG;I27c>ebA^I(!Uq=fq<#qJd84TszUC$z79XU37D?Wt1=&Yw9g6jahK!jE2c`!} zO_Tg+G!8{ZBKy=GVBBe3<5P1iy8<~pSqeMsHLKl9$?#C(l<=k)x(X#w$K4VR+M)yJ z%lL_LiI87GMCrjGxGXRUV>F*y5JpG4RA`iu9xxI0zMZ>hh!fK;q{O=Xv6c7>V=p6q zax_W=juBV8j{b;T5@q@nz6m=arb{dlf(w!WR3lJ0RVoobL#;-k0AN3=^3vgHFcs?- zI&$otG=2*?9uCLH3Y80<80Z3ma-+61R;#&cBTwf{kjCv2BFVtNt|Y4Pc3`TIf#U~5 zOw{V5r3rbqpoU>44SAzIEZ~XA0wZpfe`F;^!$*RpTO~kh6!%{>DlZEUhs9kR*>7@v z+W6YJlkQ=Uq-wQp*6JfJWxGYH07sjht>0XYNmideJ-@%Xt5izor``ME{ctjGoz^d2 zyx^garpqzo*R032VE_Oi07*naRI^(^jL~S+s1*-L&@;1dUZ25}8Q;$s*j_zs(o<2% zu6rL*B3(D0JVmi3U*7im{q1y7C$k1tB5+ZZcT=b#99`tIZg1}U%YEteS*_kY`}EWK zWHP$HeS7)-^Z>axCri#}^ZP!DW(!H)dKH4tOr~|d06TOunO^mKr{~X!=H6f3^~THT zaC~umJe{xSY~no>gZ38L`~CL3)f^2bAMR%LcCE!w+>js~|s{k*&!ZbtGR$QYrt zQJw-x&!RSP-o;#j`A#;pNx|n(n3`sKMD)cOjIE5EB9cOR&tNKSq*=ZvD$*DJbgi}d z;mx+oV)wk%Y6Axup+8#C;0Xty7Vhv-NK>PhN_TpT1dv)1h>Qxd=*GswsOs6S5zS`- zDTT=ltm~JYDM3N*x8%kg$7(=qJS}B86fq)?F%pXmCvp^71@WPYpr{qL=%%fY_F4l~ z+n|CYyGB}?-A?O@5|`+0noBB!YtR|=0)8@jJi_$_d>af~_jsSCt(5=#fbQv zrfZ15i_v6uKd3dB?Wv{X$Z}@6EV@VVAHen41UN*hNM#=CXIq`(VGxrHJX$MolrIemt;J zJH{dUvP#L{tE;4zgYL}|l@b=fs_bbR>rz$=@Q3Sqhp@H|NVUZ&MAr}55 zbNRok%z9B$!_8671^OZ@jQP401Dg=+L=gad0wEHH5do(zqz|vw)m_`_V zPFKKe_U2u$T+jUJ*PoAW$HUR0d(?gR;kwc)KmGLd;^o=>{RnEws@Og_I{NOv{9l|7 zs&0pk(x>N#xScxu+TCsd1iHzbw5sh^?e-_!R=#@rd_eM^&jRg^ulpp6rDla;9My6G z5@xRYa8PqquWt}utJE(}cnpohgP;EU?=}>WZD3`T7KGsZd^{b@mbrHOprk|@lOb@T z9KC8jEAwMLKRrHS^3lis`1;nChl8WTk)4^Ie2hJhtX;~t%XtDTI4p-3r$?tx=G^7& zUC~*ppT~`p)8DY_sIuHeoiTv(Vd%_KX`?VExsx_ zQ<8A_BR!6EB@%&+eYr6LvVHI^_K zuxfUJ>^)4;naBcVAaP5f4(Lpf%uF(LD+ z6kBt`#!)#yJyGPH9kbEZIjfRNv+V zJ{AQ|h+;57?_!bH43$;66ZJSXtP^$DLtzGI$xsMaVowCI7AOfGly(CWg+k&*!M0;c z8k(E?U9(XKP{nMMBC@-AGss1$*HowAnn`kIb)t{RYp1EC81-0#6Gi0+7gKS7&9IA^ zP2#nzE7)+z*4c?=*D`EbE2AY3quIAd+e$s~V6jO6K~}gOzbB8(Gh(t>dyGf};bHG- zsuZ20aLCY%7>j`kpd&DY{96?kDO}pH4}#L_ObFzl-T=cEQ7({d9(A2fjqO1CG+5uN zP4bZ7;wH%%He-}H1B?$mBREX9%%}&WRzPOk*yq0PG@g}LpmK+>jQ6GSX#Dj2s8cB{ zW^>kwkOEyJM8#&A%=6Rw%cJv;AFk+&JMLBv4q6|6eoNOWO>b~1500D9UbLTl@&pCz zEz(Knhq=k%?f3tMC)lay&)Sq4F^`jHpT8JRCzJct_wTP;XNOOYPk=RUkV3CDF8c$N z-_IMB>|{y`F5y6Im{z`-CBWho3$#Rchb;;SalmR_p26!)}I*HnWl_ zr1n0J=G%R}+l6057(eaZNye<^QGx6Aub)3Xqq`m#c-P-dW*bH`wU4_z`wCJmR@qfv zps8^=8jL92l`1C}9US(}+qcdgo6S#KO>Eq2+hfwKp+TlfrHv^xR~Abi<|tkcdt>V7RCq- ztPck;To*7eHYiA3F-p~g1478(IXc2c+EF{?bdYi?knwlx6dF&SIE2m3Dpx2#4{?DY zNrd|dLdq|xAs=Pp7aB~q!zUglsJym705I&wA4}&R5w}?C&-tSZOjWA@gVGmMr-uPC zZzjB%B1|P(LSn|~%~HJ@=)@TTuSAL&R}#QQ@K4lgoKJ_$RV92Di~}Svq@^9g;!H40 zNj?p1Nlb?Ce?B@t-$1OfcLtgT0+G}0WIZ3vEn-o$L&PqQaDl0h*`fn=ykG)eABj_M z92ui&GtV*jm)|a#!NiH5T_WsHh`XMxRrz9pfND9WSWG-8CUvqYEa;17Fm#=Dr#2~{ zml>J+VUfx*FcyG+sV=n@tTGsKSWW-Vd)XHlMeZ<0h7+-JF_o217b_OSdQ@f8kwsc& zni*3%3<@ILp}8KHAwv$Mosc8;3hILRgi5n5MrQ#xq*#7=m$0>w!0Yi+Q8`^l00|*o zr;CWx#>gX8l}tHP*03NI)L}Vgn!gfqPyi&gW>v`{Oh?QLqp8E4vuA#2Cje}?b(Q(l%&0Ee<%jp%m3rdB)Mr%Zz>%=^35oH7@8;9L%ILst=mlr)rQ z>3Cm+OrRfaSh7gqh#zA<1{X}t(l`S;aPYAsG6*a~99z@x9hhO5pj%lq?}&&%^X(Fl z9A!w@qCkw70X1=&eek0ACGXUYI1xtx`Mc>Wm+;sW+fUI1`(Z;$V$>g?q{0#S~Icu5jFns?&-9! zkwta)`6uOvZ@QI>YB9fFp_+#58RG0`$1P-KZ$}HbDAb1Fqk;cbn$=eqCyYS)_^t<$ z<-FTrT*LL%kh3{w{$a85$;jc3~ty4$TbW*o)__lvyX)!mRs zGvDS&_%c0eiMRKwbr*%27Mz|ER_OKTukU{U6Ak-~PPa-k-xMiMMCa(!XrRYB8TLPa z{R}ymcQ^CFXf^I5SCcF_&5ep8kCUy=&m$vsm~!HIqB=ckFzaFMJbO>wDJZ%iS?j1U~wZ( zw_F1e+7Oi%WilU(Hs)Ye2ofdor)fs2IC+(vG7PU?M7^)X@`!H<2>Dr@!$9hEjru}? ze)q36_UV`-3PFC@>(y%L8AZPqCmEPg>P5JAx`U^yaqpVF4O?=aVT$v#+-yQ6BquXv zQJ`DNDV`xkYsbG}zn}$&R2!x=2#4mtek6xJI&njh%h^dWtjasZK4BD8p!C@DR<`y- zW{8NJpVgiKpMpm5Kx)*Z)+@DJ6&Kc^GAE>s_4 zq@2|k=zzN-h>b=trku$ahMNrxRSPaa9`UuKc@h?Do~xs8+A&3K98WqVh5w&t++(l1LfFUY0$e~ z^>6AlhBGCb9tlGVgqgt1qR3Z#DAf_7sq-sXU_>Wkd*jK;CFP zAR+_rQ`Sms#po=xcfc}J$%1hRGaCbdI)qpP2&Z9_uQZFZso&c+nhUM9z(&GKn@!t$ zz$x%bHO@K2@dT)GUGSM8M3}1j1H#?9BT!!Q<}IL;#iDLQ?{RTzR}<_GN6tGjYzzNJ;jF{~;Jk1NoUg!id1t1v zx+`VST41$j5qQL4GJ*c#SWT@NcksHN|FTtjPRBd*y((2miI6Xw8B@0N1ge7Z#i7QznokRrw1MM zvx}`lhQ|3uxpea41QDSAEluxRaMQ1U^VN%&=hGW%eWU*UqSiexuX`uu%7>q>hbWos zDf0=)PUmwb%QM~M@@9sV0pwrg5D>|}z3cCrT^5W^I|4hfD?09`%PSPynn#C~;%+hN z$+9cty0!XZI)voYJw01IRPPv*bT_B79>wtGeqU$f$B;Kp8RXOgQO15heR%ln)vIc~ zc;CN;y~5+GciPkCEyyQ{Z>L^`cf$!o1^s3)Iy^ovHP9Bgr~ZE2s-nMl@f1b2?EHR_ zy}#VP?HBfLZ5UqkNI+)FmTMf!cC#+?XD~u}ZZ}ftt6IPg* z*09K7*3zy*i^OgH=wNL6!3%P(}_m2aikBm+xVGQhDFzob?A%;aHxI}wALofr#+kjS8EE~LsEL3*41O$?<#9WWw zU^$eyglrZ>*_l$exdpAh;m#)xYtDwOa?RzQn3l|!M)!@eXPxd4|`ymz(OYQc0w~F}sKr_+%Visl)4T1D-DVpRdnKexU7)Gx zJInwq;}Z}nz8~~v*Y66``$}#{k68jZDSH6%k-->uLk(yUFcoYWCa<9O5yb_uMPkjz|4ce>79xin)Iff03ylFDhR<1Qu$(KS5;TS;w z#rXYUj7Ec}dUPHUd@_)u-J8Z${5yo&RDn>GO^`e~bmBk3+Wd~*58}uQov56NQSOfRXJX^`N zge*~u2_*y{Pk2FZ&SGcEKO<=-=jR9m+);Y9do%t{QohBv1%kB2xj2|$>Ltkwi_p9p8F zO1AF%tpr-tdO7YWgUwrZF*M|p+4ZyFa=A^3T6`a(Xnly1M;1y6P|H_=nA? zliA;2jTSzC2-SpqKnlog+8aW1I*F47l5+L<*;Ck<)6o=$rrT-&y&QK@%%jhc84Ee+X24S5*@asD z<>#NG_At8ZL55*C#>>xN328bxw6vmBnb|)*Jt7cYU5Wj$lQU8cU|*1ZN}h`9vC-Huxz+ucet)*2v|@s>sGOyN2JCEYNsw6B z6C@m-b*uM5fDacKL2Oi~9zj%L@bvZJHuw5J{7vo2Y3c0SOzlj2NJtnEWn_WLKRsHp zXkbeE7m^zvHZu@W>+Xv=1B_%UC$ae`Lb|i{9!A2!5W`g{R3BAxGpl&>I7e}%mIEWQ zj+7%zZ7NAdNiSW7r7$7qGQl+gS(@Xx1mM%5r1)+lH6ztaoker#lgta z)F$_9+gs$N_EJPopLHcUC_yU?jMUfrfmuusT$8z@LwWD<_0MJIyA>J735^Jo?K1!x zcWJe1WyGat@4oNMXaaWfEgw$LoFreSdqQ8Qfw#f1EymBT)+@%;aW}F<0qgYrD*l*a zI9<~c7W@md=V*&C;0Q*OKjNm09#NZoyk4tu!dTB>LntolkSOm;JH?2*hv(J&=e5d3qkzYN;zg*05_kblR5kzNv!ml` z@y&PdukV&*WNG9M_+FvhpuKNAA0C~y&(7LA2J9{RGo+EK*{{BR@y);bGLM}f3`ZY^ ze|XdX{(AEA#nZ*OSF4nvhIURG2d77~1$DK-ZEyHc+W*O4e8Z6WYg*{pORo==_XGfOKq>Loo z0=j)wle(RF}zQRg_?pyc|>6o<>8|66sa)R#B-QWLvB4+ zM&O!0gGchQ{w>@aACvODAdsl|PRJg~#A7u~ZJS6RCM+7Q6_f`j1Gd^9UTLh5v-u{-PikgF9|x5`;%dr86399J)CAXz738Og_+eIH-T-h-Le2iA z>O@HB3C0%SYy!oNHxwj@4PxITd>>QTm_Fg>n!52q8Mom7CDTha?&Il+bVvzfECzaR z5c-3m(ZxGG^pZTps=>B0zUpqbfp=1}bpCTO5G?8l0TrfPRe51V?Ghv+W zwU}%+hJ>u)lv$?WFt!@H|D?*~6$PJ7GEix)?yoicq4Pd+(m zb(m^&`~G%tb<;n2dieRTKHD!=!y8bp%~fxhWve$~4h*G|uxCtXlYbh_)!YPF7lcYbttiCGG0Oeq8ME(Qc0arLI8Fi0`eIdA0$GL09Tk51(_aANc01S z_@s^})moyQ7s`QvV~TzbceyYB_U|euhiKq(zrmw_L@AR8Of(j`7i=I~S@BG22RiPH z%$JOCH9>1z#v_=aX|DDpK?}hqjN~A?8n`}3K{!p2uLPQ-^YE1sQK$Jnk;{b0pTo+9 znY1w}l8anZXTkx`&c@Tr4};#&7V37J(~XG8#IqLi^%cUNm4;8!l-d?Cl^3~n?Px^I z*Z{a!xnj)dd8E>Y>+t!=S2S}rIMSjjl zIW{jC4x!Hx7@$Tbh8jf#?`t24pW0u8Dsy^{c3@u=a;#(_AK}>B$nkuq*edB)w3qpI zrl*kzhSYKp4jx^bpJDE3Zp787yy9$3-2rw{H4Kr%h$-Y+Kl1qf3cDDeRs>qe&hB>e zyNAhb|K@G&9AD$tHxQ?-^xT+1Nde42h8=QF;rXLDj9H?>7}!v3NZfvu2610ofLzeEKpa?b%yC%&C7F(=zvIt0wp>o#ClMfqdH=8s=iEW zmFLxEj%t=k<-~5BsL&-2wv4+mYBDaNqZq?1Ds^FIFQV$BAnDFr6!V`n3(xDBQ##~h zh9f$eWKm=~jF4rh!u-R}-=mF!PG6B^ktY8c_+*W4_FS%a+duu}^r%%CeVou_Rch4_ zp0r+^cAvj~wiw@SM(cM!UejXVY963B{^_U3t-`uf&UcQEU=w}3yuH1jU)|h(@vE0l zU!E;S!_{cnXQnL7m%^6u?xWu1ZpxU0{cyfJIX+~8PFvmk-W9F1bKLIX-kee=41b$R`C~YxGyw^LnwtOh`ti!G@jAW^Lw3@zma2m*$eK6(-*oXS-GDu-;($(o|hE!`E`T zcG%u9n~)8M$xIp}u3D5%;;^w9*`Kl>BN`9m!`Q>61I*&AN;Qc}Mc^Iut}4w1J8s1p zkP>GBTM^nKE;y!D5G_SW|3AQ4q94(rKqjHK3x{7DpC!O_gmJV^*xXC$EIJ^pNyk81 z|8CZo_E(CagH7-_1W9X~SRI0278VjPrBeXYSmkq7&SdoVsR@i#P{43ct#`TX(-h6G zMXlaGes-{1EN;I0aoWcl?CZ3HHjtlB0s_Ksp-qM9R4~d1N$gI01f1#UVwM9a=~QHp z9E$`^o|&i%XM5>iEm_0G6QEk}qO~8_iY{ zGUl*A6fBnNgmgG!tOhV0B`K0T{&WP6`UX+A^+`!6vH42hB_L*>0|wUIKeBkP3W^~l zayt*q!wfn>v7m-exAbI{+jA^hw$j23?S&o`B$PEkkd#D%G+8=GyCtH{G}(6B=jGq> z3`kM*WWki@M<`VikOj0f8&&i;_>u-rD;k^=gzlz`g#WRw9Bdsn6je`MAb5X#D_$8q zk?0+aV2MJG87>e8PA2#%Wt9~wJ-M`nhg1sM97;TY%RyW{LD()S{o*!1e6 za@9UELFA?L{==l-rW8-3MnH#3Q9?Ar)n=Gf<{-}$=>xSBK3nG!T45X73=bL*$+5s1 zGSJu-%b$UUt0is97JO#TWaQNpu}s@!l0iBNDX}bB6rl|dSCbv?Ib}>-lK2hShQfX3 zPonCepnw6?^fdp9g7Js|zj-4C;2By(c?KyMY0~tHAYe&*004+x<+kY|F^24<$ET(CKC1_wLm&IT4AD{*H>z$ZWm?=1yv@WWi7MuesDZB~svHuR(ylAm=14@tIb3%yHkyB>uY^j1; z-^!pU3+`nL4|BEr5z2f4PdGPO2rDF^NE{haiS;6iFm6(8fTnrqGF$p0EIyZ#Offq@ zU=5{MU4_u2vvY^lBZzeR6{aAa2s4aLE4RNW6+UZL;fTWYpO8Z3vlnLvoB86$52(K6 zs>RZ1G`ZQVFJ2uyIji??#vqoJcKPX18=-fM&^Leib*q^D>7U-*3{>3w;`!6pU%UdY zT#ovqn|}Xp%s}1KgT_Do)4Oth^%QTF&sW=rG|b&zU0>ZSuJ0#Q-A<0XliNF<^6l05 z<6!ve#qlAS2xgAfI zgY~v{)H&)Ls;)j8(Y}B|Z#QdTPwzhT$o+r`uzB@PcJ{I(9-AB#4Utcv(0io>p4Eex4W zI(VMgGvym_Z>drdRh)qLP2HqR7{y!jXHVL6n?S69E zn*~W>mIh?B@ni>z_K6^oEQjE*lo}4H0SaN%e#f*iC;A4+$Ht?`Pq`OYjt0UKVT&}5 zNluE7E1RpM+rmOkSF2jJZ5J`2G!^La92f4z;7u)0u2Bx=`e^v3c4fJSLO0QXF*0El z(=k!evrjF`Yis*Tmq_NC9>65u-K?_B#Y179Z#J^U<{rTyAB0q&pzRtLCAQz=oVfrf zQ|XQ+6}XF~7e&|R>bPwEGRv^ixtdJEwe4Z?#U`@1|NNdT8 z9@p3?+>Q(b^i;{_=OfBNT{_jBl!L(V+3QK*5yfWUU8>gv@{~nQw%Bi_GRK6;Cp3Lx zbZaE$OeZYn2u(_(IdM()_RZV+=!e6zWAumB;54bT+(4U?9Y`2lb9=Ix7%R-Se;m63 z6+~Yl=T(lb-ET3DT{%)5hqDjNI=E*t4I>HL?ZtQ_=?)2`u-RU(85B2Qpc4TBH!XO_ zGo=kw=>-#=IV=)42_tkunKbmQIaUb23CYMGp~Zi}t`)K0Q%>Qz0h7)4 z(Wl{hg;r`pDLp2ba8H>y0j{5?>4uClejrJcElB4H^w@#wJKJd})WOE4 zVNxk?a%1k7S?O2+;4}wR@ywkcZ0r)PlBDju8yNU{4ssS_c-6$`8l6PJ@#Y683eZ(bp(-LLGK;_V^u(`$9n@-;SX)gD&T0Y3_PS|Vn8DrdOtbxS2Uhdy9 z@TgV;m*FHaBKGLTQ$$-B?tp1Ixahn(>)zcBa;@?g&v2QOpMLBQ*Zb~K_2pNeTzv7I zBRaXeyGN>Xwmp9Sj2R8z|F<6qkiPxmq+Y2s54+eIL_TNB!cSLs)pqUps5XHrI-+X5 z8&5`Gzc``Q<@R>;;chv_oj>ebwAI&<;K-6$KCIM8$T{xhp>cFXYeKE6VsfEenGTnC zWBRwsCr_WCF9TqWob7KnVV3oo;fdHeQi2-O2}c47C`pZ>|| z>ERDQ{h@Vojuq%n=8f6`EfyT>pWj>u@qPc*pZ%tLRC)8y|1e*UF+g0yYRgC9mZwMX zcZ94V+#+eo471&{PLphcehA1@ow+(kbCE?}tV%S~i&y3^iOX0ZlTz~X>k ztjPt%K^>O|`weetDID^5iHT2|Bs%{kHHZ42+;t5_jA4s5870<)K9jC`c&+%wPB+f9 zs*Jxh{SrM3QbmO7dV;{ag4N(YiZ@XGf`{wfQqjn|Tk%zB;l60=Hk$Bq5#~kz^mP1q$4=3(q zHG{5Grx1v;tjKy5lX+nB0dsHS%=@k|{JbEiK2h2cz{WPL5eKkI2Gg;Ugv|xSXdHGJ zu1Uvbd{ zPCRXd*g|%#nr&b6Loh9Q zI8ZmuFJe5FAyl-dL|;Jc!wiiLV)6{ENB@kJ0>jSeR@`u=S+0yS6@D?DdQs|3rypDO ze7k+r`{_-7Iss^2jR#Pp@tUDSK<|X>2xC~;3sAa1ePz%(r;c1akU;YZjvvLLkkp9; zlzhn-W6SMsfcWTMC6P&^41#E}4e=Pl%2XX7R0Snu;}P)$QP<~Tvu`xC$PgyjlXcCO zmB^$9_+v-!mWDuPqS^_8U+~m<7VI=72n$lJ$iK-Axwa#I99W1eUQ#SUlY=m2zf&}o zz5|jyS{)^Vx$;qGh`E*|ZHo4Avgti{$=NaqniZF@48!IDt|0$a`@jhtSS)hTQrQY6 z&LsLbBxmWpgC*n@$39Lal_&s40Kua${kMvTrkxwuRw9mM30@#FYDP`up*Hu4fjr;^ zFpl*YmXrp0V%tf78KMZq`hjL76l63e!X)^2GNKSHP$m#oJ-fv|Mhs-gTd}NBCD2Ek z1&1sS>9w>!9AU0f*t{;6&d?*zu3>GEi%8GO7pcFX1B;%wLoL@nJH&+CT=u5ReYC4LfA!hR-n$!c9_s%H1ipBFibehKzJEQKfL{jr zV_m5>$HQ^CR1ZTh3zunv#7@lh&25KSlsfji>15xZ(Ct#HHqpXZO^1UeVhZD7yU{sl zw+1~%HZ5r)N3HtPi>Ei2A3scq#ww#(|LCOqum0*c>)HJFc9^NuKi=HUhLdWwJ*UoW zpp@IpR;5n+^Iv_AHdg=kdOfDq57fPa`pm<4#vHR6Gwx~9spQ+l5S})0`TBG+=15fP zwZl^R_8-4*WvlE%zy_=(MvJX%t>}K3l>>$R!YZLI1d5EJ;-s+R0V%lSP~;MOu^-fk z+axdXj$tdSX%JOIleg6B8mU`2ov>m?M{H)202^s!aBbDjtAYRoQl@>f1`&1VKYGEa z>*%%1(HI>y@F0A~xB%%I4lNT}NwkYVZ_Qi$eD;rLEvS+Xa)0UcrTN{r)nmuVKPg?63zxw;%}g- zD65(MXLU?UqBid z(Ezcduw8FM5y?H&NV-B?A$EjiDaSM>KNmz$W1TdYS%!!eDBXlB!l%Zv@=vG}o9U36 zCi9>J>qFi!K)>MshQkXAX9}yLQs{d}hfQD;!rwRv%fJPq6Pce5F1K~9Q3 z&JaS0YHm*GF7gkeAJQPnIWbQ|EXQIL)PMJR5+ikrgv=9F{X|qS-nJ|7NaXtQu;42w zRhr@uo+0(WDAc(gR5Yywii-1p^sH2R!PPKY4$>&-6oXCK_6Q6_O_{t_Lbz$VwMX?` z{RBA!;*6-Bi3o9`b1+|kirs|hd;=qhio=##k*+zn2e!N8yJW5rHYSWX(*Hc+9h4DC zvwB5@%>r?cHg&bF^y)H#3D7yAre!gw%3D`JZcoyiw6P?$GI!oXf_rnR0fG6fQU-B} z(9s9A30k;NPS`!58(;?av!EzglE8;F2%bljG`LgErh1dwP?ZaKoy{zt*K@yWmQRZi zS2JxO=ft6@G0Tg+_`uMImq%SXxJ{_R$cy_%84@|r)Z*1APYJTN#q5hO&O7D8=w>oo z?F$SVYGq%2`DwjbL&koK5X^P2b8^a)y!rX^$M3I8?St;c5z5iD5+Wycb9wjn4#L*v zsKUU_%H8b{zD4hT)a}sYS0m~i_LsvI;x8w#?0~i>@-~`*#cH)kUInic-tyrAF=F=W0~4PZ-;`^eRG+^%=ko5Ud~QoO zxA(vP%P;@kzx_9(`@4_5yTN4IXWIR6Oc9lt3V59+erdNo=pHm=0x5;?YI z|K?^folgeSIy32WgkZT2xZN=q@)wxp4mZL*SQwdPm<_TMww4NnkFXo(l@21?Ww!*|McwWA1dCsD(b zNl%kKUF}gzH zqgTTp-rW7m&FBuvI|f|tm^yd=p+CGEJ$wBOwPXq)_>lM*xF+VBPFscRKniVP8(^oz zpO5K|2Ni0mHxIFe)MHj~9P%7NIrRhH3pt|+{ z5%p#0K*Lk1W3L`+#zm| zz_+%AWlP917~9xF;{|N@7<*~4S_=bACB@@7iciB95;J}>x|VWjBcu^jRo2sN3AOj@)F%Om#_#YT6z z%jSRc5@+NMcgbo!pk%Sb;Lt>ZOfFu$RA?s@9{YO)9kr4YuYS%HjC<;hj#HU{3Lc-h z(IIzBa0!lt?B(FK*fDS)sPriciK%d5#QJoO6$KMEG+zt4l0|rXw6^}f1R?Cc7W32R zUttE5UfJZ<^M&l-7z^5NwjL-N%C=c%?ZndHB-<~uk)w?Cd{dr^QjZCgp1X|uvlUxiwqxT!P-4U zaEBd&R&c@M6Rbt+xCf`)r79xo&GgPBq(~oMqX&RGhnlk&l1**qDMtw%mZm-jb5a`D z9-6Czc6_XZS>o;s^#>+(f(+l`sdQW&k}XG%j=Sf19u*5kFBQ2)-ydO2r-{iVSs7St zShO=Hb>Vg56=rBEYt0u`(4iJmO=@`2AtM3HafZXvn5(e_A>1TF1c{w$ENY75AM!~5 z)syF;iY1c|1?*1cQn!}H6dgUdEFxnc(Iw5?lv9F)bR-y^N{ z`s*9(+4=S3&!070<8))Y7x9T5a6xx0*v;1uMmOK7I>k$HiG^FuZQV+pMO3@Y%z=d)p_6$LH<#`NfEV zBw!2wQy4OxVg||1t0G&x(}YMiUC~4#R4d~q1L`~PO{TpOzy#NScYTevoKvu}y4LHo zXf)K^xy}d71?V+BGi|X;X{~9M@<*S7w5W>c84=|yaSFbku?Ml#3I!$)TL%UvT)-r=S%)LHIx- z_2L04Vvt^3o_4o(DKlTOGG17SMM z&w`HvHcQzK(*tTMdTJFaVY=aN1X@+*DuxaLrtqdQ^s99-w0V=z)Efn41VB1;;9@Ip zF4t|uTH7Ba1u5_V1C=y@K*+^@1Wrub(MQfFgQeM!4mOi1`NrdKstL;%%cY-#n|_z2 zPEr%e<+NnaRYFY!5wDlN`cfSuAoeH9Qb z2WbxM<)p|RRe2TwgY)me)k7eFjOgdMx~GpJ9P#Y@<>NZpU}qaFKx}nsO7ES)G3fmD zPyX)T-e5qIMr&UvZZ>$nW}P;96yX!8{Z25)0;=*<5DI1Dl2L)Z=kgbU&*^VquRXkwa(IYhl>>)W^!DgX0Y*N7^uE5 zL?e8QVm|FfDM|e!yKa4G{APSS;G^=ItjUAf6M9=#h0{EeS`>5v%{W;hn2YvVjCm8{ z&a;xtPE6}kh5DphP*Z>emfa?rE}rbbrdbygRAVjt!nh}zMIX|z{|F1H7)n!>%ELvx zD^xT~|EXO($(J!W;R_qJ>}o#Ky0~ntX1CVsB%J2ikjjmjW)&L?z9copR;Jb=kjndUOb8adYox`?U45FP;yPLSb)K%I@#p zKr@Lc&iQHgpxIrmTGou|*f9S!>h_d#A! z4p6~S6BSsrbUnuJY|hfD#Z_&!MwxzbhD9|)+jrn@1qYoXQyr6I8>T`Z+)x${qKDUtb+$lr-QR-uhnJFSFNEH0bLr5*@4!`mpTJF zp5a=pu(rNZtd)6~+m{gZ2lMWbn?#vH=}<2hdV?{`lFFsr2q8b0u3Eagzp-?A(){wE zNHn)C;CW*-2#kirZ-FV!tkdCM7pi#ZU4gL0Lt~sx_ONlTsyDXth2`e+Cxde~I0f)% zEb(uHAjc;t*H)wsha=2{)tWIZfhrRkbNo_?AAqWv$_Zqqlg&T;<}6@N&NI@u2*uR#@k>*>9Rk2Jf8^UBZM5AS+o@L zl|f>o0AE0$zYNq4C&Gd!%@e1k1O@~yY36-EyW@bt03r**%i{beD#OrIIL&w4Ef0^G zFANgmAp-wIgzoi9k+~5|h--V5n9LEa>0z=Z*6?NMAgHSN;kqHmIp-=+C&p-<7b{pS zB4iJqf%F-x%mj1R89cZQ@~=&SVjD)g1Uxeq`3T0PWgO~%YiOKADsEZ&-`hv~hgW`@ zX&=lYE@-1sLKWxQ8g!U)1+}=~y>s>I)j9|sB)NNUZR2l!cHAT1jB|^JA3wbLU?*S7 zPkNmRjK1NZLA$oKdH(w7i_c$Vs|^i(s`dR_I|vKleOw;3@TXqM74cE)y3KuQZFlwl z{Y_BF;c(U(0yHifdplhAqvq){{7D2}vjtdJ1C|cFqC%eCH&kwPkzCjMYL(}LoZaPj z3cA;yH{0Ew4vE`qE2tA%7fo!#n}go_pM4*-#@XR(w;8r)t!e+^NAKR<+v|5PS{MEH zaCV)}w=TN7>#T|AaTzZGkl{h9lN}p7=iB>x>$h%jbz7`lVyfBcvjC}DfaDd57oj;1 zZ;hp(V`$Ks_4VrBW_^M=+|Q1(vftD2{9qpP6aa-)3h*1f@2ms~_HyDq682JxhybSz zCLC-Y8rs?Mkd`YPuEpk^WxG|}&dj1!?SHg`Nt|~Rna@FlNp<3K`78mQ4mzI?gvWEm zj+0Kh`0$rLD>iDeY9|DIaR?(<27qEmSTzw!IUU2$53tMk1wa8w50wc1G8`U*Tg;G> ze7}hYvA?w+V!!;Q*J9j?m8**;+_a+kvElj84vQ<`Zb4 z5dMm>Q&-}V@D?A3ppeXJ9l(U{+fZWj2WTmL#DPpcw|oSDkf}FNKtD6}RwA*}{m=oV z7uh?eI9g=MA%+;44nZ1q>gJClH(0*CtoslGPE_e--{5|t=%M^c}Hw zvp=Ig4G@@j9>>pwkl+Cc&>B5a;*F3rngQfS&&8Dv#hkD+yY84CDD#L!7_V;!Y=IKM zWWcTFc*ax&fve&9akk%YJiNc$d72u24d*UhT)R%MKKb3>Y*cb@{n`ICTiR}Y`^8o7 zf?lw?z3cF+-S!1Y5}5iTj%KMhC8$9XjevF>|dm(xfoLv;7{t#!Szl>&I znBEjysL{%q{RL4G*2f~lq|#DW$?s?`NIk*K!j6_&#IFv`pVLCMlCrjJPetR69^5AB zNQqs5gH3x`Q7mbUd;uTL=R3S3teKoQ2cOFiVd#tF?78!@m6HtW2H zD+)C`+JolN>E~a(%&n|IWvZ{O?mxKKIejr7%#L215n^ho%(IuLI3?b>zgI3WzF+E{ zT#Od!_GJ&xjPi}${Pt=EaoeOlYGWq_)i&Q?y2E8!O5wytPYgvxGf6HNN)IRs&PX$z z_HZ~_>UaC~Qg(N4?8P2_kQs3yG3Hxc-HQY zFOD#)G4rf9R`EfFqRG<-_g!8w7qPIk`_|oTrF7P6Go)k>FngV;cU=y$G{v0YDt86o zR>1o%RbfXkBmK4Q8=ISz;p^9(Z;moES!&!Th(e%81rTNyxsQiTJu!Bol?;|#@H^f( zYygJH!3w19dV(c4J5!tzQ7w8vO>64mc{cJEf{t$jKGL89ZJ+5?0}&&m6jDA+gpjD{ zjkb{TVWvxeQz<(}q!EZrf*XiL{T#$Ja@uc3OBY;+Wok(juNmxf#QG(6a^|6m#rOKe zMBU35J1*fnf*}&Kl4LSKyo>W{YzWa<>}GL>U_I`>CYXEk(R4H^UuTe$7Skmop@&)l zU=(wPNM?XM*Wg-&h0$=rypP>Fz(cOWOr<*Q!OBX52RxrCl2F{KhcOxHie+|vhVTyP zoGJs>gYLcYG^gn<_cQ?d%-E`xlP8f65DSzuSMN#ZC4KRo&2AdA$8|G2EQMQ+1 zDMRETgGF1Ez}SXvJy?lHd_F~BT3Cx)I`JT|#wIv8m>@-TD!gOuW3t^o+e+EjrK;K? zR(NVj2M|E!ZqqrcBNk|t!eHXzcUhRpn+fVH!vw&)q$dp1Y9}&WvumvXW>?3{joxx$ z%N=XLo*%qu?ESOYr2A@psEmJy-`6? zPhKow2Ct-%EGmGcLJF+V>lE#j5(}=hoZg6e27aLcqi4XizRT#O!(4z*htXoUce#~a zo^;Nx=gn00yGxnP(do;te)p@p+lA}Qc;lD;?R38K{O|px)bS~p!OHFyyv6p(X?pqc zDu<>S3t|p5MHs(LSH^GHmdVm&nu|$-xUje!<&Lc5nUWYMuq={gmvUfllBO+Q0!Fdq z2#_GO=&|;aoF`Sre|7C1wQ!8)I7z|jlA-Bmq?h7iW_1=~dozo2)7SdIU>8m_z_^H@ z&nrO(b;CT0g(9s35pOh~*UL}?;Vgn#xkx7f(_8Rtz18nE4nrN59D1c9bdd;j! zAjkSLj}vUSGaa6WWXp~7@**JV_-uwD@74Hf+DWC$=}Hw)4C6Gep*^MQ-k9e3){@Dl zw1@dMYdiDWt-9My*}jDsL{v#i*4EKUXn$jv&~FlT+UTTFI%r3pXBq^>)G)3d+))%8JxXb9?y?ooS?cO z(>I6nrE0y=$ka<$rRAm0$@siCxn}gUx}Mpnm)15)nAlvL4KD^zUf8EHnQ|yx9ht6_ zSzR$3b=XL(xdc$;^s3#P4Hnnk>2j+N9pHLrt8{N|h2%>JmHKtOnut2y*ip(#S01Vg)SWj#>& zJfo3b1vq>WQF2)ZisVJne9&wfFM`4d)Nug29MTT>naCe08%IE5hHgQ@=*#L&YKn{z zE|&?@OS)@yFQO%9!Z9zRvCh!seoh@$J0(hPM!p(Q42Ri!a;}1q=yZqHUsjM zGJI+gaE}I*2fJry!=asy84M7iMpC%8JW;g~vDjoRZ z;63r6BuKHQI5EhKxDV@{TDU2Nq7cMd#M1haniY~1CJ_E_eudjeNDld4SH(Gli*3Pn zoS|+_5^)?_-o5QWbdWKCeuFb4qCDE0ydEU!>*C7B(%QqT!bWB^$29Tj&kpx1cb72Y zDle^m_lI=s&;HK;GCRHCyBfQ&Ihv5sV}zX`9w;HP71^_`;L5rE!y{&QXoF1JL!`~R zK#(s!+Ze}AzNk>7{nP0&&|{!lvz5(rEDe(C3KbM_4T*13uT-Gpm5ekb<01RwfH4)~ zQzoP6)+NBFn>8EehCsjNr>Lf|Y$22RNb<-ja^OK+AVEUqc?WMl-c#Fx-8hd_&X zBCupqF*TAZ#fp`@>olh?Au|jfE?iGBB;!n%(hcW(&_5)3@$FNb=D5V_HSmAA>MxfF zq}A&}jl5>fMcd9pWW>U25bXy(>41+E3DgS6cH~9``*oUHE{8%>OdB3IcfurU<1D6< z3J@DvK6MUflT=T%a}f=ZHfeNq5J+WSo%N6;F)_Tew{~&a>(BEg*N)crk ze_%pF2VKg}Km7iOT-Gk8(u-;9^kO=&<}R*QIGt{0VifeeBw_=YS5ZS$1GqfmZ1z5<_k4JbuxOSby0% zF1xeUm$imUDtW&ts&@kr&t_@udT@pqPiVq&%!Y@Pr3HxN6rOPTJWc{6f;(tP-gf*7 zs)Z56nPM}qyfk7?a=&2qXBD=+Wk(4j=8Ie&+cMKR9iGWbK_ZnG3flf%! z$H|J1BGaE7=8EnmDPs(#j0J9bDF1;;lAOWCCHtaQCT~Tz6Rb!#aDaH$iwt1iN0PEUAEaxW>Fio;g5*7LhvWdO zk_#HCG7b@Qp#smOdvu+bNo`V(s1}m{O?qwic{p6O9tyT+oYe$JjSCyFMD&!_Gm%C@ zN2nS-Ve4>#CF$QHtEqexIo=qx!Q_o z%wxThFqkxr;_2msaeH*@VZLKUDx(+ppRI9yS_SSSsRnq~LAb9&sOi|vwl zGbPJNrf#*ut)2CGyTy8mql-aiqw@a4yRW}`+#4+V^Tm0;_wdoJjqMtzp*u4{yFdC-d1Eb;uhMWN%Sg2dNs4(nJEXOGx)|gL2?i~P{G*WK;kbi`A;eGfW%CXkv&X z+)lgMgbHJA7=hx-?X#+gNkybZT+nIvAuBMRq|CLlac3G&7poL`wQLs8qD(nQS#5n9 zwv*&;7!@JO3>G-+C&PvYxYQ2bdw{-Bi2&~q>m^=?@ls9;Ak!EWhbUZ1_!4f)-xiI7dQJ2{2d&u4#w@8rYDPx?Qr-H&78lEHzde+~p@r@C+he15v zA-9A;I}qD1|EYT)6^7#g1+sCT=Z)etyHZ^)G|*QSwr*B;@0@=1N3-)|ri&Xlb`T#djrzI+rn2-1T#tMa z${3+LLs}7KPiPXsjX`S%9R)1JQ|3yhFqH5rdUbuQbWrlqp=b~J4PCD9oM#W%JJByn zei1E^-pZ6Rc1e3-ofBwYjh`d=mVp)_g1SjFYkGJKBZ)0qjsbK6>9Es=f{b0TwHj1G zSq%^sLw6IA0{Gh#P)LTXjF;Vr=_9B(O2LRhN8OJja2*41@U$RiOs7$qF(sJDe3qP& z1;0z0y2z}fdhU@o!p-6C8q^4vF{F}pS4#%{3XF|lW&uqgV_lLqMp-VrTv# z=L>BiMU`(glP-bTqzfp0m96ju@{7`hzVK=a;K#N*DmltKKNW9Ynn28KgXty8fV?E= z*Y?UJPQSle&sKNyjT?F)Bf7AC%;1vD4_C3xRQ1zGcMe{X>?W@|W1eiG|T4Uha{797 zHoxq5+e5g&R8j0>hrzQ_Ll-dyMTN3c+gPu!RL>4hUmdhMa3Cm|@GaxWq|&RkQnirn zogPhlvy<`UgYP}cEe40rUt(E>Z_QFQ^T&SiBbtYIK6?M@mtS5YduaCntO~gbTo33f zbbxX=;Yi|Lxm5$UFvPy6#1_ z#CA~H4s;vi^QTW6*Hw`e>|w9988MrHr=?huQW&(^8?4j39i0!GDkiISXdS_B-~gBy z0|^nyj!~NDIH}cx3&X?7p)n3l^%RoeAinBrd8kZAe0zv@HD@59f)8g z%_m*l_hVq_!yF)10!;bUQPJzcr#M6Ir8@6GICc6lK)EF$Tu~%g$7AO+?yn$|87_ll z&8Ky^F2G=MJu=gyY%AXI~E*P2&kG?*%sEl+POWKmCAv(v%5+;m$&A>F} zh%mR9F5$Q2T`-4>{X#dT2f)N*+ z!btSf28{35d?R=a5_EYfB)2u*3^zb>XD++3y~DoT>2%Uq-7GdL7)a2_0d}69pYc=J zi3zcmDJ-FjUi9MSO94@LeB1a-9Syd;O@P>`erGh9<|Kh`CReucmPK@L077X{p1S8CyTsE7}l2k>g>WxRN?{M~IUI z6l}5#9SNy0&v#G)2SHQjIq6AhqoKl-Y1a1&g|`eNHoekAK z>Zw_8-n_VY-GtiRK=a3KSzLpa)*2C>05fJJAO^*S_9f6d3Y zwl~s?{`u>ZKJtZB>fibkpMLx0(_RlqGX1*Oz46waNB8z-^Kq?MeDc$u;4O0e{8*2h6a&~)XuhlsMi>m=x0CktMVN9v?QYO2h}MAnd)tS7<6YX|+09L`KkpoY<=hyF^LCLxh7- z;0f*%QKS=6Q~NPvrB%JBEj%mzx1>P~!U%w>CP4W-#wGwsZlSO`HK7TA(8J332#x}4 z5JXq}u9)39XjbfmDGXO&+=1>Sj&4eG>r-%RqVA1wcRe41v_uZI<8;oaWu>Dhf0|QD z2YyB>w5#eIrD#U6>ysCYcO^E+9`mia!XgslKoc^p{z$7ND9t+#zCK|wmu|%dqiFP$ z3rVcyTge~*u5{NdI3gRt0;PL5zH;V*D#G#=o)xy!(JU59nOdpFZh_Xxxv@YGGs)OE z>_f0)FzGlf2s_a3%ETEdsr=K`SM)O=3;_=4<{XVZ2rQZ{`Je_qt^KuN8=KRy1uB2}$j0Vf!=ZTq03ZNKL_t&!D0cwR z&bDQPGMkSb(YBpZn8;Urkt-rSK|K4~%qq!q4F>lp)&je}J-V-;b2By`V->NKDwP|| zSh6h5NKL6$TnLMGC&o!!8dO9C0xNoD8P>s2)v@o9yR3rFx8a2rP|*BAgDR);+Q>$a z9vGB4(ktL&M+e~Y5JjF&Yzaji21ejHC79t%L%8y#(i7e*(Tum_rnvAk1?3h|b^+%^ z&2yAG%7iA27%bSrbkza7h?j3{lA7t{u6RWSi>>VpUJ_S9U!*KRj!2e<;NVgc;W%{5 zDbOrSZ6?lC{UW?Nw~;t?Qg;VM5p{8EG4JpiEIsEC`IF=3aJsg)aqF!u&~^z4oxzIep1mqUmORt95eL0(S?n9`(NS$-}|L#qn7a_@}nH z+DXmpYxS!^t9x)nTf)s^K^W!;AWXPc=c}pJjT*72$maT$_5MZo#jB&!(=Ll-i)$9R{0#IeyM|=MObQy&V($=5mR>Dg#|gc%MdF}q!S@qOQgM4`v^!@w3>46lUTusYM5DlfS^)w zJq?x-+G1oGIaGipW^iwsOnU$qAn|=7nh{F^ES}CT&k!Y^KWk&KoMj(%?18pTu!r#q ziQuRd=ec2TyAw*mD{BWZ-6HFeruZko3S(G?P=Vr9Gu6fU@nsB6ecDyJ=%__p&Z0mq zFzPeR$da!lD+%QBdtD0k>YWeY%d(fGTE?h@&8=<^I4S}jen9Utj;f^n|9_mUAnq{}3WCDm07loG(r zVXECRwVKH?nkYpCvHl{+r3sdyS?e35)lqi57B9q=pwNIMLKz3R@$3)2$-Ah4tAd3E zYbhsB|It6cJsCEB@z)8tKyrmU4?g&h{_8*bU;onV?AcX%zWMOobY^Ao>Swm^>7oT& zpe>G`Y{-N>sUh3}nW#_5+`;j3=}J5aK_If=>TjfYTjA8d!VDv}STZ z67S01j0>bs1ozzqLhcpu>gr1dN(sb6GJy|40R+F)0)tN8QKG*OJ6vd2X`{ZMR{{)( zdGM2?H6avE7eHbm(sfM=XVs~JK7WpvV_#0(X&aRUWA!d4A}KyK^jvB8L7kH^4)$Qv z?Cb=O4h-Z2-SzDAGu4rhirrI4yXiQ>8p2T})EM6U6ta=C_wY=$uO|Oe8u)J(_R_kB zP`l6Q)w!AFQk7Z`uQ7;iDMtPjlt80U#&!!CmJY5KQ@3x_2mL-8?|TpTuDio$2dB;9 z;>J6ByEoQW5%674&z_&0o?KwSeec1oYNdYsvUv@)J(Y*=a`(~A-Ss-9<=M+;uwhRx zPFJ=z8f6_2Y$OK#ai&rF=-u~@o<5(lm#L6L`QANiX2y$BI>j1Au1KjM9EE2dS`}Ci zlip}^wMzEFQKo->62;N-6?H(POprpcCz&L@)Tq_ZbqOcQEmqc6%f&3KH4tG`8!NL{ z7r8E#7apP1U3%qEvQUU)} z-rD}vfA>%S{lEMd`(2*)f}c7T=+HH>UsY-(7lA?+>Y3ZsX!7uDwg?X1Xv)12yp0)i zLDm{wZQt6v_0dNh#B!y~4zFsyJe~H2$FJJWBg9bWN3T}5_cdI2_Pp00pxPk;!Tas^ z`pc=$i4fei$G`QtckUX)lPOd0(QMSp9UcGlYZ!a10lO%^B7JfL$lZ;N&BpdlsXAR- z+5GI^`IDgNpZ?mflA|W0-oO6G|6qLDSl7uo|d?>>*Z)hX)OjwDhwK^u6K= zxR}HV+yCIbOR@?`5;BP$aJa%JGC7lOoNtdQFdNG_xgESNwx^hYgyO}oOEMxA7hoV+ zS^8hBo=yAQ!{_HCC z_Vv?mu5@uM^1QL$C`9kXf8JG2+!L4%e3S`NIOkEYIui}rb`Uvj{iO25gkSEPaAz`_ zfb9TAqL;e#T};qmLTYf3kW|8gUyB8cU;hQA4;M;opP#MwY_E?+1pW{cCW5F5dCBQy z$kZ`vJbs#1q9ROYY_WDF@RC?;CnfACgb2S1ZZlbs4CB-7I{`YCA!R-fCs033GRan7 z6^Du&%$h(i^@4=x9W%R={KzvD-z1ud6s{8Va2Q`Gv3cYjoX4sy4NhG!nV?mYStk4x zu1kzpT(2ATgCE~uH76HiZ5U4I0Yfkr)&CqGY0Sj&)yWk~*9hTNSL!v#n95 zeslHS?d^GYaQNhSwz_)f!@d2xTR3q~y2DqG50KBV?9{e5w=uLkJ!$d?;Q0mb?A`CY zb!T@I^y!-~kDtA`U<<HuP_5#%QpI~5A>iR@i|vHiqXBgli>2Xc;1Xi*_6&pV z=1DVODec|bK0iFVy3V(!^OH{Jt#=;a_xbY4ivgmbRKC|8jN6xxRNYXct24l8(E~s| zalPE1PNWHEvwgHxR})nvoUHJ&saD{$y628p(qqHxT`NmP{$n8tdGUaQfW36;>uB2OO_2`HDf(;`|J`Aj#w{vRJ5k z;xEHYko`CzLC3P9)?SaJ1y)bL1ig3?DZoc~9d$qb&P zGChd{;366m2WV$@b^ZdNpUCa8%Yr(t0ARepsufFw)~H@*2|JfUZkH=)T%sGrF124U zLmdEcN5YpYU0~=j|K?x+4w`-j>V7cpqJIjjPVeDRn!wEEN6+GPiRySqrKm=DRa3mY zy1#Yrdmrt8`2P02{rXCU%rU$;Wx{;=_{-yOzJB$^H=VucXf#I55YCyB(IGm(N+l_xG35}2p27IyWCSQ*_S|xLZD;T9qcQbGb)67&@chNu z>!-Nt)z`2v%H>MMN8kU!_Svs2#&l7vsW}~QgUQx&`gDW z2LBm2v?YwED+Zs&T+Wz7$2cU>_FE+d5E~_uF8OE>*CqJ{GIK$);1q}oGGlF9-RF4|Ni&yKus%oSyZ2M4AGE| zX8n`1T&h)R)R*{Nh|yLz>pR=WUw%0|YhI6L{gcEE-S>%)a5%mQ?m4U?0YE|57DvTNKISHULL9w%61!CMJIyDveP&?z`!OzQ8K>*!W0&AV^`E5(61b7 z_#|;{>FoGvQARi|G;KO zW@{3BV&;we?gAHR;UC*Tduwox7zK;VzDlQ~GY`UZ zYolH(oxMCbzhKaoI&EF-ytVb=qrKGl>g@IFgX7lo^Y-?7?K_WdUA%a4*}6Jyj+niD z`oV+wp#QovDYO3wt$n>d>$P*~X&L?rqYy0;*aNA7La~|z&NquxoyLAZIH-5nDZz6gZZaF&Y_PXwh^sdNjuTSZTxY8=3-0O=^NErhz&p9s z^)-f|^cVy>x{2NG^{YuA!ZuB!Bg3V|;kPH1EU6YJ0^ej?|u47=*5I$znUH+DbSegEO4(>wm=@$utt zS+D?2bvm0A*lbCKJH9TbGcd%_bdNP7cRz)e9I*?)%t0@d&rr4}v!M7TH#IibM~I>0 z;nCsl&{&jt5zH$dMb85m{*mxx@giWB;YJYzLDzO&dh7|biaJsDaVItBW}Tq)$K z$m+E)a>|>!nhd2TGBAGp2S0%?n5&aZMf&x%)MSTulpb9yUjFze-2KfTk_;Y@45|-5 zeeh@hvnPM^FLz$FmeRky^YO3I^<2IBnr>p61yvSX3B{ybTmhO|6p3YL{7u zrXJ9-s*SpJYk!&4JL5|+%f+3nkg&R$6p#rcL-3o9{g^po#>)wnYAk~nErgkpGY=0` zU-^*pap^#S-Cr2fnK(B(i)ynQYy3#>)>3{p!h(wuEeu>lh8~Z2=8^}v&|naf9o(ql zJ&jzcF9t8J43d*WE|>Df2M5v*J&rzcfhIfdAT+@V;Lp-a1qKe>Sl$9%EoFoT7lP|D zOkU1)hmU*dB#7!T@emF@klTz*n0UKO(y#Ki~bA4)6D z_Bn~Nkj)PUXx_6!wjkxR#q@l5(ZWq~DOKBA$H`?}DDE>;JUBch5boXHIQZsgONcHD zRaV+|JC|#W{sbWDAQyZOGKTJGkY_6mmPN=#hv^f1K~tG>cD@TbyjnavJnvq1>ZQfK zy#@(4Uo5R}vybVdgiW`#hHqZXZ+Ec@UPTLSs#@s~6isx772L5GJRVzF;xbkh!Z)~se7 z#>1Yip&!@(T3bP$PuRyY4*fF}-@%xORbb(KW<5n&sdzVV9D?fE!Al@Owti;kXj+5k z<}MVA+JM|bLZLT~iIBC?!O6C&6*aL$)wSLPoJe(L^ohCfWno~lz8AO zkVuf-^ys$+=Ur{};m;#;#vn}_DjsixJ8-n4O_3fV2r)qcI{0&cfb*)HMmH8@h*{{8 z4dD?r(RVuhV-gbvF3HM9_jsUrz|j2R_kWnGEU@^gtgf{VUd&#Pj=p`CD^%!?1)6~G zS>i`5Ft51Wn)jxowEQ1izYwJa0JIpEijoI_L&G10nx-J~8YsG$&oz6^CKa3?H7|u3 zKU=UQ1vV*IM<)B?4}TkM?ZhTc+sZAQ4$mR8lc%qVEDl%!#~c7q%VFPUIqb}8CNCi1_xDZTmg2{LzIDx1G)Lq`c35k9isYSA7{^* zbW_nn+Rp~+?k$jt<=#CO_YHUMJUBag(eEw))<6Dd6`VA~@e(o*(Nr3mnFoi0#o#o1 z@Z--x47PssXO=qEuN=P%ZVJrrdxj&hA!Ux1O zO;vQHC!lxA^*Q9OAmPy5M@*C0LM_2V9BnFsH>nQ7puxd4>N!XPKj%p*(-=9}Rtu|o zLJMFC^MQU`yR*!0fIq^0fKhaYr|Fte8qpZETDeW`y1cr2H9=>>8!9X&9rp9_Hsxu1 z*lanL2JP%}heArMRZozm^q;)7{PxFt5B3_vi_Y=uR%?F!;M2FOOI=7-BNh-Iv@VB( zhY#=d&dv{xTHQ(JtU0=9V`7y0(XV}2nG-$^PL2nM&|(JNz0KA1WN~zGaoQXoH`}-0 z-G6((-a2Y_2FuIEJc2v93Dar+=p-}F9N~GbUWx{GcIVMfW2I29FO~7*$(6X%$0wt4 zD%0%`t1w7M({2aa4Ism8wN{~^?l*gr&Y)DtGl?s#tZzTK#~}LP^^2X|?ZHKJMwloT zq1O$1*ndwqv4+DSl73t*I2%p553~BtI;M8S0Ma>~2a5dl8(Vko?^DH|932#wW;gMa zD;AK&)iyWQ>Q&rIhvD~7t>h{db|8$yT612l*2~$<$?t!j@6m5$1|wopc7VP|`?uU; z+y{vbxLr{*zw|A!=rUI8e2|pK09kr53y53f24Rj|72LewS2<=AS&Hoh0UX3(7A*C< zFsMfXB@(V@S3G_(aZoTypF|eXQZlA`KGDG$mDvzLW$#K~yR0_9Gh(xIpqKDCzHP!*Htf~{YuPJkHXt3`Je2bd_ogI*`_mS}d^+&#us zN}gvz+tBr1HJaAh{O!CamZx$8xj+wo>HF82$=ce=*~_QxZ=Q_0qy9+~!v-!Od4Y;L zkRX?SLz@TYWYY-fHs)0nQo7QlW`!tP*<7vAu&6U8h%ygUp;{&;NM;0Jne1qAb^b5^=2yS**9)Wec-VjNtN-QYtIxUA z7`8PU+hs^L`?r=V+#r;19km`zbYUojM21^TI2f+8FHs7HfQp#)z>W8po;vt!|gNb}F!RupjL!=yH zINK$&+1b_C|KvB4HEy7^HfKhuNi0 zrGd`6PJAxkzMs$3THpS3ae2n>qn&p@?O}b@lV;^UNp&;;I;`m_TA6|5gN(Cb)ZOx~ zeh44T0OA$IyPjHl2Q*84%;?6#NGLY?vkWd7oS zJ4@`W7O_wkKIyxlJDTBGR;7?5PK1a8;RK>M>#)*32>42rkZlY=!*rK-VxOoVQ~e^1ff}Ar-_qb1|ys4c1XYOfC* z*ME8R`0?pU`|7OOSrK0?boKkqSJO(m zcmPd?m6F5J)=u^2eyvt5mM~{2lndo*uQhmm2KhRVnL5!N)3ScMb1@v}>+4kvbo!&t zMMtmE76WQ=VSTScdE|?+@`IrA`3;L)lyqS%{dr+? zZFO_I*FGl#lU)f4K%);od~bWJet39*$wv{qLJHLa#BKZ5Ev{qpVgSrc2&)%4*hM#; z=uMffuB>oZI?rD(PWmKp)__sLsiu^~m|fdkn?ZY_>C$IQ@og-56|0FH5%`1dSsp=! z;Xdvj+9dsi1U>#Es&9IwI5*Z;3i85VDviFJAiMEkKVPoUNd*BQV3HU`#-uQDx60yMpC}~|zuO{c71*DTyE6(K z7)w#+M-WWjc+8^!Si(Zr3RDnxXj137!iePL9QtFq*0}29TpHiX6KbZ^=s3b|h>Yd1 zTN&^kTw56PfuMs6C(S9Ft*@04lIGpa8Z<~El=&>;u8#6ci|w~=)HX|fYPYkCvnStP z512u+Q)GZkFKG`-pi`<`s-%O1&mFeJ6l!=ExET+3VEZnrP6DDWkP{IJHzXlBw&p(N zosQu&!hj9$H}H-~hlP|IZ)Ba|#~`26Q~_9;6gOetqNf+m+Oujv__@X8n}7CyWNW)~ zgck>&*H>2oUJK>Q>h9Lo-TRI0J%ShJ^sYp9KOMyo%_xxzzBn`%jpztZ)>YwFowQEG1Cz7?jXCsE-$lzL`uw!9w3nS&^$yFMy_oVn8*RPsGm?p(iS| z*nyx(KfW4|&Yynu{I`C*_5OpU+U?I(51L5;03ZNKL_t)4_YYtF-j}yAaDq1OW^=1c z4v)vi4DF&&#{|<%^X04jf(r{#H>)LPcW`hrZygoug|&wt_dBiZ5K9^yp@eod zjMnRi1ckMr8^JjQurZ_jvJBu;Q6j@)bMqZ{Q(>sAa?cR}a!32rPyvll?(U|91UI!SnoDV?2<>XOh@UFNW z0@ozQSZ^90oIy^Bo{fJ&j7Pzsl+1;Y4%oG!0qV3rljTa$lm^ob^}j3O0*7XhY&GRJ zM)9xMmV=@KLJuqlkPje7uZ-EqO#_wzp$XIs+tbBQ2XH#vIR8IZ=p{dAjMJe|d6?JmpE{?? ztyNqL0DVA$zv&v6Osr7Tx$>~z?VdbK|Ap`Fy!z(p>w|H2bN$x64Jz>FMgQ!4{Q9i* z!T0tHNDL7GqV6R3TrDb%)O(-Yxmhn=zC1lRLM?xdcJA)Y^-;SIhmPoUdUTSlra$@g z0j~aEeyRMh*jUXsN-MQW|KN4Q5Rt}VhHzc1WZu5Fv9*hNh@SPe+BzTd^rF)mXXh^3 zS+H24b$)5}vB<34*uAl{-hO?2eu89wMk!67OW!>lPY=(|NP&5lH1=fDvDL%!dpcSx zm+Y%u>zJuFYNOHYq&scgyuDT0>F9PaBh57O?|u08 zd@@FQZ+no>;t#+~j?EhQ`x#Ntvf^iS(Hgxxt)S~<{NS!lM=UJa-ne_CT&eWV&MXtd zHpwOA;fQUfh$b)cL~!wp{z+VM4&ia3a@V*onH7c)mEY^u1x_+=Jy_$h?Q9X;(^Sni}j3^L$Qj&mYZnA!qn989b9YO}ERc?RqF3QeyrW6mArG0X0-n52E zxXa;npsrAFVnH>Y;px?=H|_zXW9H;*i&q6u77b;L(~J|oQSeFMTDZtFusVMFVtZ@5 zdvwfg-Q=e?tG;5(&VahzSw+s(d}Jb4Ir9&&-1zR+D2_>y|J~)?dzQ$*NU`>I{#a3 z#mI$>Cp7xT=Ekz$bKpCW@Rw*Gk};Kf3n!@pUQ{)_h{&>4nQ<;gyLN91EG68=Vv4Ac zq7O_cP*H-!v2uxgG$;XSBzZ0hK|3I~mn8Uk5UH&6jN=lT!gP#lfk`HWEY)K&pJOYb z3n*fm7_FzAYm<3UbHg=nKIy!Aa{PNgdH5@z4VPE`$^ZEq<5$Nw%b5mME2RL1!nGPo zaRZJWp`6Dg9#n>JgVSE>>b%e>q#L`b+V+rz z?vi7i#uf3JgFkUiwNESY>Oo%y;vr24vhzN0%3i)2Nj{RD3>we%8gqYbgBc-G6?0*& zW@O6*LB`FOoK-F=2GafGE_M-=T7e_}u zq0XgZxx%ic5i4`u6?avuyZg`$=EG6*%O_Pm1v6l`1a=oP7mIhuprgz2**O&Qq{CyP z?2yev%&C@C`DQ5~(>qt>g;FZeM}hcIv(Y!nY<#2qi@=cp@i8N`s0-8suq&okQI8sl z48GRhrDC=O!MHNxjRm4lm`T*g$&V%_%cl2SFu+Vh9PN$!55MDeoSTM;s1KGnSk$Y+wNie3+9Rk1@(aFqc-%g2 zPw*rf!u8Mynq`ejEu%pe!!aWg?I1UCe{kM$M#XnhH1oPFZJ`7tqg#ALy5;%Jm+eY~ zbewuo(q?Ka1uXC?rAp`I_28U^T(c7X#-u<(zM3elJ>4ctnGESC3o%xU1z=Bto5&{_ z(NRyVS9jo=fPH}xVS<&@$Y0>s?nNNWy*m07GOf`BO_85hU6qQVUhn|p2npm!>*R@T-xZ{06#Z1#GW zlffnF+~BYEFjNoO8)33$CX0EDrzrRgvKh(1JZXV81v@?#f(HuDCFX+a*Xp(9xBLXr zVR#jU<|D3o#r^U(@TT|FFfU*qGeWa$$moJPY85D(H&MX{${E?#g3HH3HwpK3u)!r~ zG*sB4{rb6 z9VARN+cjyAe7H~m{rtgP>Yq2uwN!Cy9S31HK5sty^y2m7@vCQ}=F!Tn+xdNdhMxs=JVgdt=LZaD%5OlwnuJ`ehkeYd|0-Ak^Uu#Z#YAY|gLX-#5g zLODcVE4!GSz~!2ZN4=3E!kCmXO3>9RGNl!@I$BF5RW4MO0^p+D&grAiw?_x3@aV)- zq)uA@aH9%I2Me!0BTJbn|Tf#+(d`E>T zPl_!xJ|jX)M6DvUAzR`=X`>7aicyr^t~g^V>0S-A*I7(WU7#U_miSQ$(>@wB^tpW{ zpb(GXybk-r*2QAnOaJ>HZme%t*xChsz1N$ZwVT_w8Xv#A-FZcRV9hLYTF|q}yC2-y zuIGlQm(6CoKe$Fvzp>pY0q9@lj$a)!(A|D(=ixiM%kAFD;l<@RI|Vpq{Q~AL&3?rh z%~G@3LV#1Lm3OwQjcT68w4BFJ4uVAK@R%RIgln#N7-o~*-q}U}s$49tH`efm7+efb zntlAP2UDCzQA|?>0$x>Ca<{g(2bWDw9cT_Ak$P48aXbU@Td{oxv&wdRgTmTYd8OVy zd(B>M&?FaiEzRreD?A}R=0oG&&PHuDpKo_ZkXY}1aCdLBe1ws}44!RfZDXxmNDnD? z+)Dz)S|?8phEp2W_LJwi0SyCM8G0fy2##d7$bd3?)of4NLpD-ygTzTZQPJ^fS`^M_ zVbYk8=Li#Sw+>NPl$0@Ti9*xv%5xT)+bE@@<+{d-njmG2i{T<4GgQ%rD27=6AfPZB zrv4CbO13v9{4gDx?A8<&ablg?%on(b<0>$oUUj;|w?Fz$X?rDKz~xp9{tYCRRQyS6 zNl?YaC_)`uc@t+NvHuz70$j=t0FJpl7SklXK)`E$b@E`v1W&MIY1BRX@;QVlE+8*L z#mTx8%3H696ZH5rRBj1I!xtzyaHOE0Pw zd?T0ZD=Uj31s$tGiUcwmKqoO4#u3rN6x0gajA8#Hq()H>7mf*AGN_`QD39bnD#DOFTQ#Cd;e_GKCRz*ppifCs9LP8uC3p^#TXCUK$d7a3?J%; zJ+#d7$cCMs_nNQplg4(!Fy0C@j+CnjLjWLBD3iw0i)magXK>KXF&TmorPpXQoyNN` zGmG>Z2{=U4ssVr{7UR*;&pz)zdA9YvkN)5fpZxAW_?(-rC+<7muIL508hfm%H!YPp#aBOU4>b#;qb%Q5tYC z5;`TCX^39?L1EClGt`l`+LlvdDbClRAK*cuC#^_t4&Jv4IsORM z5AUs<{AhQc;gjuEaspt|DugkD3r;S+;1U0ix|uU z!RP1RS?(vm6=%^{Mu#U8Rm47oI(P&!3S;z%tthu!K`O|UmyA=7x&wb-V0>%D9Qjnr zsPBg>BRp%VWKs%mY*G2FxQF!FZ0+5d=4qd9{V@{QdhkIcH?{eCcM9Xg6 zPH#TAEK+{`SD)>qnT!wThv#%L?H~T~`?ngE*WVuCsd91NghQPz%^kAdcBfOG-ptbE&dp6GcKGX_BTHcUD$hR`Wr8Wk%y}c^_(F-P(X3kDzIUhFVtG=x z)gGL8h9D!GTWhN~w}3d?mwlE#e{^>*jg(?^mESIX_{sY$M?5`c7K-I#7E3t-_H4v( zm{~46UTTUq$H;fCN3TvZ7lTR}3-9F-?9rIrmeIp=(7Cv@Hc^a=*W2eM<*YF$;UZfO zCMho{c2PSEO%e3N-7yLSFax+d zHbe~1vCS3ovF^d2DAzy(21ZN#BrFcNgjcn#t?fs53&jTK$shLrj|pRb8Occ{8WRPc zKNxOmjQx=;+nSF$K|Bn2~`virHxKB61wTdna zN));jlWSvr6SxNq|KzJL*Kh8pS8wQn;4E;qbNlvSKCG=Y2Ay*r06BbwS?o(}QN`$@ zwbj2{_{$NXn~JHsbvuhs8O@nVOJG)kpr+A^3d1m+gC7IfU}Py@4TL~M^bI(gV4enj zH5Fl;DRSUc(Q{+1kdOrb%~^U=?f9dECX~Y~i+Q9$I|6Z3JN~RF#RB04FbVhs zc|eM6^WtCs-t^VU?l1k||Nf2NxqSS5r;@2>moSNnfSzFWF^y6)!tFM($L!in%f*Lk z)_V24wA3r^>?{=(}i=c*Cp+;Rm~aB1lP;eaPZs zb-R1{{CJia>IUJBIS_V=sh2iOTA_leGgLq%;xBlfjI|{=$(>}7Xd6xt$*0vE8~)Sy z(VKoe-Y9guAkIe-M7jA*LW$DKgvW__4I7vA(v{|#$Po1bSwJf_4Z9`R0?;gyudr}- zWisx;al^@#!q*e-ZHk>wZ!qt`6wIq(g5$;GJr^iOVe{n6RmQ5!v!LJw0q}9qjT5-`-K?z z>ZDm(uim}AclrGE<;%|TWp7$Y|LULmEZuFs`10`8#o(G{b-4^Ye3tfYH1eCZD!bfS z`%o;yl7W(t-QHW--Cw67uT~p}hnLO4V!T8*!ytaK7<5Lx33B-Lx8B~|EMLC(S++Nw zwuk7~*EcsZSVF~9%6-3eYn6ZtCz$)>d;X*n&)tULW?_ zY%Ro-ceq_G++aV3AKXW!$O0(_p5=P2TrSSo zi6uF(HO{K}S^W?CwjP@kmWlS1YW3>5;rAfJ3z8VkIbkUyM9r^T% zE=9knvd7vsLTdDahM17KswcMAi}F_?o)wVDe_?c5)JwcKVUJk3N0MNFCC8M^5seo8VIeQ&kGjKsH6hRpBoZK$2g;e9!;hsh zMKgSs3Y{TM4<-^KIJ_H!h8ldTR`qbw-i1aYb9wm`p~_Ow>B|`|8HMK!F-RB`gEE!0 zwUrtaQHOV;`-;o@0*G{zDz0rLb=UUd!S(X@e{=Zqo6-5Rwe8#K$||MT&h7n`&24;m zN8=0DE>o^hvGeh~MKL{<|LA+unvFM?$rp?@%J=AxOgFR#cG#X0&#foS$*eVk7OWmU zJWM9=%om7A(Y;{rrw-3cDiPH>kGI6Y&Y3K$ zNm=*H1Dns*0P!5n>N}x47v!64i=^jYPHY3HQ!#;^xLPHaGHVfQ+k1j+Ga!5Yv^)e{X#itUX0<{Q!cp5Qly z=viK_)kH)X7aRh8Ap#~*7N99mz{M^wE~ess+e#|pD>^3V{oS+ct3j;7pv9EEcTz@f`cDt+w08>q^ujDVk zd7c_f2VJ*M6bq#>3I`V~qs>`{rYT(`Ynm1w$4d#K7xFyi?_k)gH8346l1E zvcL!&D>Qdh-QHcf|G~S9%+)J0@cHFk*5}sp=jWZ}a^*We{O;b~_W8l9vx_!Eo0au- z&~r{9D|3M**0y#xwzo&U=Ecc#M2!2}jizl_VV@0F0!AB^Eo+iEJUb^s%-%_fWn@fysTbPYK>5IfPr@P}dH zZHjLyAT`BD+yC)UkQnF1JUmg~2QdTB8BP6(;F^~tJUU^(5H)zE43}O^T7QgUomSbD z8`k}zs6Fi4qRSyMP}^H%a#tzzLvbIzV9su zfYSDSOlig0qVaJl16VioG}#kmYhKvAq#sh!k_RN1S}NkZ1h&=cjQh>id-rlHTTV%+ zeO6XW^~T`zu;{jMEMMG2fW&~9NPrHQloJB^1wIW?7GPXPl{eNAT2y1YE%}M#lS>hf z_vO^f4@ylSgOvC~2#XsVGwKB0hzcP_)&VF4>R=nnb#_Zhh!_&VqWV>w)l5e*x+9(! zD{0Dhq!i%W#ahqs8XEf7@DwLd*?nxgUfG!HgBHiOS7$C z_>kFztp3WkpLRt)w9)g(FEt5trC&Rc3MSZV*<{Q4s(2%5;C+vF1 zJHufHv?Xk}Sr|9$(=RT~hZNS3v)uZfE~fwE@73Rb>)r_esN+s+mYFv;UYs1Q@6>+y z=tk@Li*9q=A1-=5Trs9MZ*0t*&&#} zf^3N~5&MY$Kce32$JR7E@7qll?TDFS`7l?TIZUcZ7c5 z8;<%zq*UnyW6cf@n$5#qx*eatIn69?A8nP;JL!$bg+}eYkKf-|o1MKmy}s&^2|zC_ z6v~WnAM`u9VtM!Am?4N)ub&RO9R$<1HtVN9{w#I1sBJbub9oGRH63v5VEqu9q6vJd zJed1bNi8jAh@}}<@lJ!gsi8FJmf)T=n@8>#BX9UQ?UCtZ!bdQrkWxtv)Y$?VQ>YL= zv4RxUxH#cP;-HMyy*SvQHj~*}wXnH!1aUS`oeUoU!E-qBjv~l|42{$YT^}HI%=;jM zdGhU#D*Ia-1)3;gJhZuNg!f5C7=4Heg@lqIL8}jIBLs3c(*?xQagVNi-B(h=R&du6 zX;gOt4qDB7&!10QQxX6cKsvasz0Q>#k7@3qFyK@jf>zRJ(<9G)5Z;0O$M|dR6sT~4 zSRlbeQNkx)P=+UtkDZn&(T@~dO!*i18|g6Sb}%$?n6{Jz-@|NF$HN+ni<)0zrlo8I zRt-j_FEkP{N{x(<$FSvJT0$h{GC2}h(|j-7hG>>gnM`=l(CKd+%@s^im%$R6>fSDp znyHb@CuUx!Z+anA8x$SmeRCHKj`cq8L7M;t-z36UA`W&t9=!SaAFoDNwZjM4n_@9n zY;5ko{obHAUXFUiyB2mW*hz9UYLb5*WSlT~ILLs-)EtPxdkNMVuigA0JVB@L=<+?%+p%@YzXueKUhzGVg@@5UW&3 zg<^KJteIoPg_O%%AEW&=dj8eCeNoz6ZodE5 zFJHVYrx=!|kvDZGev2YOhKrS&001BWNklgJG!s!VNryt#$fQ~E zji3sNtB4I1TS2jQ0|vVTR9JXgde8$>d@*5@V_Fhgs%VV0=X5?1?NsgB*g_e788xP- zrtP{Y-HE48W~VkmNez9Oz`sC+To!j=r~oKTn5TKIy5WF$HEFO_d}{I{S+XR3^_L?Q zW<8avaSF{{JC-Oo!*D$CPIR@UfIjY$pXl z@(WJi+=&l2SdYEOj^l@HFERY!6_R((nZyLcD@e!I^Ya(&3A$gjSP+qzBXd*c!5pst`R{%3?8_Hum?3W6;XM`Cj~jr;lh&*A z#TZt|`kU6c+{o=!i?5%aA+FM$-)=p+|L!|S{jXlUdD%W~-OTev9)}WBf80SuAwN%# z2GeS-6g>l}S|xpgd}9NR3^L*TS6^H}24$c;Bih&k#aw1IM*fB2(ra|JpX_h-Uq9ou z~&{tM@*9H&-oQcLtYd=Ue6M@phe%7h+v?uX*&~XwYf58K-nT$W+U_Xj|cu z2R*b+n}_!vzWY9V?eg{0`D|F<+C6&o&gj+G{ZCJ`>GEPu=Qo8!jySD0=tQrVYmCvD zD4ZGE*9MCb5#81JBgfaYM;;~{*?dhC5lorHFUGczQVQh5^vKZfOK@_-&freKst3OF z6M~f&9CYjFp~w-@xTQ%kRnjK^ejQUz{w6sKZcbS@fIq(QS(9P~T<8gsXo zw1)$$DDa!v00+iBcXgx3*1Dac1^7{wH&;EUaz97|9VXacU3?@P0)kr+B@up3wx2U0_X z1`-fjGJqz2)!5S}WJnveWpHz{HkGfV2hOCmRSHd63iX@@LegHBTz-6 zGQh1GUa<+Oq!@rlh6q7GRVGk;NPcN5nZ*j-bx?dD2i-A)67qoviLDATdQrp`ad;^< zGHDWpKI3~e1&!|JWN7B^HmJe(rV>s4m(KeNWyA%X$fT?jI{{Lx9C*x-lq}Y}Sf{UYl1^sqN3k{o?v$%`rx08;wb} z=HR7>D-%nJm)@$r6w?8S0)Ml1(}oFO*~%ZDRA{|vz3M(YpG`BT=RLCJ;drsPQ@!nt z`{UVVXNElXyWf7WxxPGo_PTXBeRee{pB&1Yr8y>jTU}+YUcMy27|GMx~4?`;^rgI#CQh`RVKHd7;jwXntVY1EhJHE&$)h<1z1cy9hS>l3|4R4)@_qz3zT zXfIde9p*8iM*@*%m8#UYpIu&)J`mocTLUhPYU)J8eZy3Z4P^gS7jU{g$=##(9(N`k z8XRvIo$tQ;fT0<*0y0m0=VS-@@_wf`8Qyk>v+C9!sUVd`tPB;!$KUzR*5>x$oMy>a zbn!P&-a2{zqtv|n;&*;OyRn;qooFXMpg0)u?F#zt8^A^`nGR{aSwLgNW z4q~;o01i>vM};;!?w;>F{1BejeAKr8^7=X^ZyW0|YK&aESgw!g#dI>t>fv`j&Q+^` zAbRA;2;+~ZM-!jGquRcclt*Q7^UMNkV1_r#&hyut zc+wQZJO($34rdf#wJI6a8+r7mVnL%enk3{<#Z9^#-gcZv!W9^PeGT{vY=g--r3#TB zv=k;`gpM&`FL|5wNi@ZH8}b*B7E=kz#ae=-LjaSMXG~GZ^N&y)=V*K9zj;bg^=k={Zkvj52325&lZO?26EB} zxtu{NLy2j7`6hP^#za!#Awj1iYb*LWNH5>A~(8vwWVl1QeNXtGZ1l$)!5@X9}iLUG37(^cWq3-8+6n8e(4Z-g~ zR~-_r(w>Q80&7LZ>Moy*C{I`Crc^LJfYv+7nYfPOT{3x(OH-|+1d=OO*U@s@V4w|K z59E*qPq;SzZZZkjTu`2vXgYMQo7w2<)6ahKd%yeegZF;>55LZghWmvyh&#r|{s0$< zCsoVFxk^6loi7iDn%he9ByUhF*!1xJ8oMoWOX6tFdI z3FOuZ`8xsv!@M{E0v8dW3olbVF&`HK_xur8X7fa-xM>5Fgcar{;ATP(0>Bo?Kk$t( zHmaR*e?-(_{b*q_zlU1MbTuYJ*1T(e2EJAUufMip)M_;0)@y3JA4RMR6!j ztO8w*Hb)9EYUta~%IVPO!PVDR62xUTC`x71l`4QX^JvkoTg|&v&3c_H&=A<3Q`n=l zGZ{OoJrLobTK7023^NZmu*B*@5^Gx1bZ#MEk zjI(9-i>b>2y84fA1J zcBgf5aHf z#s&i%E6!0sics_~c{E1Ho}w^J9(|J(*$bv#@z+Skl0S*%5IzhcqB}>(-nOmV!yS$D z!MF4HPnk}%m||(iqpR`to15Nf-W!3H1O#DQnSFm0@6nSZF>uYtI_jFI2eJoy$4n&T|RWNbKxS0Vp$Oo zD#nV=KndYu1jA2j6uSw23iLMUni`?t;7VuLP{B63rCZeg>%qmVa%DSRZ_LH{K;&=0?71{gyF!6a^zSHkQ#=CfTrD16@_g z9U0lb&DFYWx1BFO);n6?5}-n8)K?sX%-zHCnw8tLV}zW(R8AM0AX(OOrSJj|2&*Nb zSMnKwwmS?hP7h(!blD%g`sB}_|Kz9pPoDgT-}{Tk`m&Kpl>o=BocTh62c`E*FoXoz zOHxAf&?vN@P?50XIw`Hm)XDXb#cT)^H23gZda3Aa)-oyNV= zl_5Wcg|SHKuY0pL3rd;dF6L9(NirypXtY2(G)gwxnpTozIOYkA#fOy72{Ssv^!+7~ar z{Iu5|ZKQJboh^=H_u}R7+1c>x7BxD?o{jr37gbeph~jcGCoY)C0d_{Un`MUT zF@<4ro4{C&^-UM`gn_vwg(hgkw9PW_J!<>RNg=Mp8`}s>d7Cv8I0X*H*W}AA@{Hy} zCvduyAsZptR(HsqQ!}*m7x1R(ODID+nqkHG6>C&B7|EORGOz|#N zypxF9Q_9s9Ar+-`8lNbwf@YDbgnN^^gJN5>aV4aBFKkvDva!+}ctPM-t}fxXeNmVI zB7qppO3BIqBob9w7AmB&GaO%Fgkx?Rv{czG$m<|F54UTo+S_V1x_Ei@`H%U@((xlu zBj7M3vuMD;zRVY>Z&+p_jW(i*eHFNC&?|zi;8NKSCese=%hn4!U+)~EnrCXTnPFMn zWcS*{X<+6v`3(aJuM}CB!F~j1fhY(&N6kybIYAaQrhFugFXdXc*u>Ry&pusge{G{g z3>RpvjhxZLA)qIss>roFXWY5^@^^oGbozRIyZGlnezBd)Y!zWXsCB}tG^X-yY_ z{`t=#rcNQ?Q(B0&SXPb^rMeUFcLKMh@XyL4n}?KOIzLS@iq`+8?#MPR7qUgR1Gh@g zc;u9LGG|G~5J53yUxgkeRH8%BAcAKb+hO83f<`V_vWWCPA>`(AdH2wgLR&!qILgW* z6j9kv1D4ZTjwvnD!C<+LfJQ-Hd09y_BFR%|{z32Im;C4u&~c0=_SH<@r;8RE9}tHs z4D0)jGPfGuG&tr^?j`JC5Hw{A5Hd*;T#AEIgcS_K)5#&B#e_HsefOloBn(_!inUBy zp92AR!!gOJ?oT=PTSN0hu`{V!ke=hp3X_dEy zU$U4aTUNkcDc%n68|Zpt5hZ46J#zsGyaQ6dA zGMZ0nV-cmsIGE{p(L?-YI3G?BK{V_{I6tak(2~$|nl2~ko13RCZ>Qby$#*{})EoF2 z$=S*w*!y7!?*#+jmq4iQL$ujqwe5ZsSDOvyquy*d3d|(bEG#wIB{4j^pa@A!yBv%3 zCThXl9*}}QJXj*EU=%^~R{Ceg{&a%SQ$+^^wvt_4C9)DG#Z%gF4(*xDmZ(#!7o2q0 z1?LI93>J-8C0Ns(wq&QZ2OOe^umhW9%@C347i2uJRAfs;>yXRysAv)&5t38UMnk_MA=fPvu^+b4r0Ssi%T-Z{XvH;XXjqQ<=3W=78W1>&mZUx6Y z3DZd-wuxC`xj=_Zhly|amb=STC#q5jXw6q}y3t;u3x|}!Y8PwGa<(y9T%%ioopO|B zbCdg@_gcXTRYWF~cNR`-kFeZM5>21~VZ&Ecgkdm=IG6rR74ocPpG+=Z_IC5@D7897J(Lh4gCIec0bkW z4|evqcMkS1nAJ8MQPOkZJ280@N3Ex-#f{_pjY_3Zt^rSH-<)>3tMoO4m3zPlrccz~ zh`<;bNRhCd%FNmw4*oBF@BQwp*S*fD-@ie#hBJ!d*>JW5-Pmo^N~OiUqa6l8_2z4X z6iQ&ox=nb=yibk}MyyoHk0v*(QVp64Lfc!_3gt7*`_aAX`}g-3fc-550Psj_Pu_Vr zZC&($L%Ym}xIH>N=AaE*tx=~-P2jFa)-dOVZR&C7@_R zX^#K4Zjo?D!^#A3V`rhg2I2Fx#X%I1TpuBKP{)%xcKnG0?x@eGSE#}uc4}%UdWISq zTOK-v5S*jLD3W6Z?4(9p0tzZXG@P*o>veO}Zgn4h>zxYFHVT%S<|K^gpCPem&72bw z;<@1X-mMyo8d3YCCJQL$<8dgTp}N9x#m}KmG@4wT-7xrA6cCT*Bo!VUjMYAPmO|M(7~d&)L|SGX`Ohhyh{fa$s+~hfH*+YCro*cJW;x>FE~q)s7TX-*{KOP z1$0*sUD&V@^B1E=$(n<)D8?0TBM1~+1S8QopAB1ZpzIc4qDv}OZtkXYwFN@b$oV)} zK(0{Wnhr?_1l|vms%KB61z8h4)dPItLBbSJiX@(0>lSjpmD3T`@)jaadeD44XEYk- zL@hk_vy&-(B>A*1hcRI!p_ZkJ{ayWQjVw?Env?W9$E2``+yhq$(N2SW20Aci z_www=Kl$X}{Oj$5gJ-YLhuz!V!dfLJc^N#!y=Y#8VwOO@{4?O%1iv*|;sEQiy*C-X z`tCb`W_2^(n6+QjcQWbv;To-B$8Vp1`qR|K+4%a4o$q`<(|FkV#gBk{8AV}85S@zI zBB*v5@s9}{?Bxw1-`L4m4or51M$_{8a<7u5s7+uNQ4L*zC`gb>S}yjqMzK|S7qU9! z4nkhvLV}Xz$;oC*t4z%ag0MmZ+6*!7#WF{!pg>T>Z0)i>GO(z`jDQLx$2LGd;~hx3|6P@NpA06r`2j-4T)K%qap3w zz{{w95}cG#ubvLS^YOcl;@VeVzF=(8WO9o*XrGhJ$nWKCvkE(OSt{Lb*2>IvT5ME- zJ?9V$H`Xdp2Gtltm7T#GnK6wWOtJ4?RSAWBgG)39f8z)5?bqv__Eo<(TIEXB-TM7Q z+5|5yNMq^S$RZij@P{kM9rpAY+&omLl+?ZM;s=!WTByyl-J{6jU@ z`}t{Svhnx-r+@xC|LcFd?5v{^&iqFt>Mbt8$)Rk`tF`j0XJh9{_6@Z^1@S@#CQwax zw5}z@rGiQjN`EM&;xi(IRs<64+jgJe)F<5(aaN-QcvmQq`Da3vn4Kh>j4D-pC1iii zJ`fB*3Z&gwE2j4qRb|J_mVFipF#7IJ2$*BIea%v08FLpmkuIlmg*In%oD!Fo1k& zkP2`IXc0M0RE^2i*(T~?(pl)vB1bO3?#Yy!Yq@H45b}=Yd@?*VxLQKR5x`+VRq(;# zlY)SA%!l2VKm6&_KlA@)JZ1XSK2SN_bYVUDnGu4bcoH}3vKvPlR)Y0~sD-Xw zcg}4nXl?OIS1t6NAjPO{ExPB8!+NUHNH>qFn}?^L{Av2?>gMKR_pkoV$*7pR_}cyi zUzdG2bjU5&^ZiAt4K@{hfG!rBfCzpt+sUr)6lrw@tTL2foe3d>XrmIJTPf5D*qfI3 zkXeYd$Oce^D{M6e-62H|b`reug`>(G9>YXvfYwS4`mwaln03uIvmmO>hNMjjp|<~v z|I+Kkn^b+3@8&rXCNjpgrb;3Ji6XX}Q+wv*CS{J2-cst&knfy!$y2Fo!4AZo(EJ`L zpPd-D#Y!Hs=te2pV01Doguc+LOmCD_s+><@+|UR}a1O9+-g3KSXUFJ^0*tdLl)}k@ z?P&LrG!Q?W001BWNkl^TaG2705NVkT5dhycR z_I`0N7{k$AE55lH5Ue!lD_GuM;NN?t`jEO`v=?mgFz1ns#(i__v6RyH)pS3 zTn|8E>YMxbc8Y~`w>=kaLW`{o8wmaxz$Vywy-Mzy%KmTMl>W+J(MvXDMm5cM! zRvSqHetQcg{L@eVVm-V0(LeiduKvaU(*Aldxhd8+OXclG0YMgu-bCR<6*?;G3?g6& zzdRyrf$s=I0VsqKA}dI8yC?&KHJ~klAx@lH5~m8Q#&3&~$EFP98OQk!K~3DIcuut8 zd#q`@HXT%hCYsK)0YL%qLR$ZX;I0GnAgRFSF9J(UMeqPsK&rp(L$^}6qQ7%884n(P z{9d8f0BwkED8Ee46;wWloz2|QjQblQs zb2!4!A%hL_>DH7QppzvQCVML88Mev5dg%hW83=sm_g!w>@swyJ+e~Z zXR;Tq-FqdAh)GMkEJVN~oMy-Y*jnJN86@NOeU%hXL&HEY$c#jUh7tmsZeBlrr0@d2 zhnO|RAi(5YV{-kPqbtiXil;IE0%Cy@Ubgv%;SsKOv13hwndMR>qK;~(x{I{s1SPcY zOM7uo;A&L+R}m)KEx+O6#NKna*!aPsc}&>+r1ofw7V@>tOsNiyhnooe->5G@R9DnW zYWR~#&DAr)iei{&t*g&}=XbB3zg({uo#3rC^%hA{d&}Ziq2gONQ8sC?s|t+X*a@8l%@)aCF{9HvP~4`u*;kR%e#IoG#v6Toy~WAHTELditD1>1wpPYK^w` zcgVO)x%K{a7wL@6dbX~lhX)b4xMFVO!K3YJ*+t;(e)ar(lHZz8HJ+hueLI951pu2$ z1FLcgD;4s|@{}s&a$Ku74j;b-@$B;X*Vo+v4!X9p1wPXo(GWT0Q#Ko0_=xMLPctp5 z4)fgQS8u+)03t`V0((@Vmk-d787oThnF`%e?V`2EBu*sMIt1U3TDX4C+%OQD@Pz@^ zIGHFRy_i92H1C>b;z9{35RrKDD|dA0|G#D8uI{g>Y!z{BXI5yk5{7y3*|esCM)CLP z)7L<8L<#@+GAJop7$_t1zY6qSNWjy@u-iX4d00Q*&z00JinJ+lq9Uuoku&?e17`!e z`sNAw%W^gCvk~d;RF(wHU^Kga_5y_2Uy5hqHmoGT8@?M5?N`~9L=`uqnWnXbBt7yz zs&1KyMU9ADB6uryKH<&qe4gY8Ac&aEBD6=+!8tmdHR}o?vQM!_+iudyp+M3FE5Igp z;wUHDJ*Qe5R?^}`^e3v=7S&d%>v{kRela^xN98KwlV2vDBcshP z5b70>whk+O)G_epn!aTh#J~*0I=Os91e$B^(^f&oqKsQEKWVojAPX(@U@~)KQNI>n zCEzfp#Xfjz( zU0e*eP*x*KcI=;Ar^o=4RM2zBQzQ*Y21g-!Y3Le@V2Bs{Su1Ab6AWXeoa0AtGsDYv zF59c@AKw<6+xOo)`}v{u`YUL4b2bsjePX{i*Ygd<|#$VLGG8N*5vhYz7bZZH5sp-`_i>YQ29$K`ZX-P&w4 zo7d03V(bOO*$c(O_Te7s{BS{={H#*hY*vdS231`4XK${nj403LTjzZWoft1ls0@=V z=PPr(`x^Xyc0*7DjO`*fy8|)pkx7o3^qD{|Bi4X@c?d`DlQDDce@}-jxo@B~!G(DW21g(U0jc~|DbS6!nk|4+dCPYC~$q3>Q!i{c= z@~P+w3J4chrD7?R-x8{cW5)bd{fnr*wi}d3Q9ZRki$!{S8AueNebnYmx`OV>t`)CS zvJ2;9c#Qkku2cP3(12taZ$U{Rg2;6;(ps1CVPl@*|;<1!t~{ zo74_Q0_6G!Ol3+RjDr}S9>R=CnzQ5Skgr)z3C_-njv zY&t)~{=#i1QId`jMmW+KwnbYr30G2q0{C2x{L(*4Q$d3*poAE))S_cL`a?nr7nGIb4v7cwYDP~==ZndY2b>~)CSPd)?|}GbsRVm)m4^%_6bTQY^bD%o z%^HrAdt#H)c6bVLvWMEkaFpnmQPO z%$_^}Fcy^Hf%uIfXzVV>#u@RX(*NMCy=a9HUYyc#wB0O5FI0Z}ph|;%xl-6WsH2iw+uj}!@{dvz`frf2nSqK;dRRgmVha6! zr?8lBXV&@IVmjR&-k?p>BhBL|rBl^PW!N7Q3uC6Ml~O&k+OCv1=B@s?ho@X3I7(18 zsg$9W%qV-AXCn z2d+is==3T(Tmi<8;d>F8Ky#%zvEdR3!YYpM`h35^&dWS^m zHMB+EO2St43by$LG!qp>*4geylroC->TP-o-vY7mVYmJ8y?2T`TMh)!)LBQ5!EuK5 zai{(>8L*xpc%rh2bSG?+NpFEr&18~Ad*0sidXlp! z$k1k(n`$xQ@R)@w+KRWcN|+;~BjKb9WKw&>7j3Awf`s{qnR!9{kM!E~H@t<^jf4Yf z>IQ~qkB=;jNOg=D9CK*fogT~#5l|5Y3gFP?bVf&wZy<(odsKNwCH>LFwGJf((P65H z4Wr%zNuElz?ofa2G}max3)>wZCD{^Shy#s~t&Nk8QnD;32>{LXVB^I^Bt>Nesgywp zWg(){Qi&AM8OwgQxuJg{{z&J*B#K4ARg2BYFyVu#r|OeqVh@4yWI3xJJ;+oVSV*fr zz>@gKVMm8e!(wIw=ALblQH-$xAgp6#!y(BPaHT0)=!ZB}KqUZ0$wg<$@v=qhWFaUo ztQz@a>=8YsyCpdc39b1~kq(kVfeL=1xs@(8QyFkka&%WNJghD-{u!Iiot?};pxq3^ zH=VRDzWTjC`SO#W6dUE<}u1e}s|)R;pWnaF{BhJ@RCYwR`&V=JKpuo^JofKkmJHwYqvWFO{ck zfBlV8 z%AVKQds5CB`Ahm`;fHL?xQMB=40ts$oprz=1Eh6H6_-ao6nQfSFj*a!)Pws8(KMgi zT#ueXIuH?GBuYi81D4@hmaGmipK}uPqbLeGMT#G^?VZm;OjR{e5E>#y&Uc~QEM=No zx24*k-KA*_O8~;*GC5L5qe^suNx;b5O!{Q1j-eX2Nil)7Acm`;yH3Y6HV0MgOtD(a zRd(Q}FfG900pH)vZr0L2c>m<|7oT5U&Rgvv?icdJWPStvsT2B$N$TXgaS(=&Qa{1=6kGUL!wj)ndNZ;Gu&^x>gs#PJBRlf#y7dR00ZAYJjCWQr-Gbzb=qCL=~VJ1N+*;8=^P~3 zE!60*%QCNGb9)apQn_2O*yMUq=QBHUN2)fTSOX(W`>hjC=f<$LB|?ASlLjG9oh5y0 zT7nxP2X&V?YQ66~U7Qu8>8M~ObwM|r)$b_XnIe$)xRvw=(fyu;?2&t!`zY3sYXjUGU<8wP^7i8mRLwKx7zxT{=z3HHE<%0L=7jhWyFVlZ zqK{4+cspE8hlB2QemTGT>Lu+cmH~jc(fo-jY0yD9Msg2LIGtsag73tcf-6Ad0K*{T z8?wHkq?7#x=Qd{5MW+fbFc5lw02z%~0rv z86VS1QyYuPzzAH_%_PoL*ELV7i2*)2ut&2InW9vym`Fk%!QUZEPz0h(0{KWLioX*) z0v}LvgNZZIm}8-|hb&ZHxLCY0dTv&?omXF_my=YEB6)p0xZFEBwO zcQTD#LMVEq^$eCI;&(j)L2 zv%-<3s&@xwLJl_oXBMg>AqfhgwTQ!uphRt+e_?-?BQGG+6E~PC?;bVpeOTT9*3B|M zx_ZelKHy!#0UjdqcMBSP1>{zoCK<)U$epD6aR&hq16XSzlSD_lSlws(-h7HA^=CuqM^$0njJFvXJ*Q^(Er&pcHV)p*`9)Ixo(b=+E)T-DT5gYOLr= zSIia2&7sZsR)A8)BBV_d!qTi3y%V-`LWPoF>e)Wy#6+Ftg%`9mr|)IoD&Ul)CgJCJA=svb zz;o~*FUPDjo@rpssxND+Z&TjyuZZwRi5*fTiGC5-wASs!*6j2DI zycFzk2htMQ+6s*e_V<)56LX<7D-m65&xt%F* z!WSh4u{9;q^1Gr`aG{cXMcoDhFFg}1k=w9u z+q|@x-|8v3tD7ariE9`BF}A5&@D3R7?I?B&JJ+vC7x;gp)57f*IL_+k@#j)zFxDrVhmA_4gMa;)( zRGh3!TbdMWA{oTBqL-8u#T{K@TSXxA;I}FcTJ1~PQq^aG$YVPoA4<3sSuvFv2t!b` zCekhmauwUF=BssXb~@=rzyzErTeY~;+-N+>mUi*at<%p^Q@)FLW(}(bNkp%xOS21A zApok?g%dg?VWVo!qFE}eTYZWO{d_u#bP%=VRQkU<&R({0K-lquY4BoMqX36{&#CoD zv8~ap_9sr6BwcGX$_%rw?d@zH9dPB-tKl_{5b@CQE#v*@QWN=Itd07;dg^w^YCF`2 zWwBaH7gom)j~BDi)8`i*6kAsE6yAI5q(AHueW;6dvxNDIJLkkfy+69wJl>=qe=?Y+ zx)bHhiiPa);U2p&F}0s^RK^naSDuU*4*6R-z=}KonMil&c=%s6+%Y; z(j#$ojzUCf@9;32U5(oP{&ffIoU1nvpL_sEq<{Tl+G`;IKAqpbyd3%>=Zj{uy58+9 z-gGTrZKU{?{6D#fDT=m}c8ASlD_WK*5V}2ka15y>WLp@qi7h7IhV{ZQS?Hs&Ljz}# zNuoihjJj#!L?+{tzDWlYz~-(}BAgV!MH2oezw(z+w$ZYA1^YPho59l*cZUUo)w2mK zJZ%D&nr(z7Ra6N#K$(jel;J$e?IA!gpWc7x(P%*DB`Wbo0k5Pd+p zmpci~kmXDeCu8u$&RCL^<%ilIny;PCm!I7ZMk@10BYJSq%(!%6PP+{R&FnL^xnJkF zam!inJA|S{c>-3GyCLl*!X#GC!LZ49rCoUT?08G=;LcOqP8MvJFak=SXl6w$gYn_p z3QhwM&>aoQE{0Hm_SB-o1bZc$3rZnbBa|~5^Q99CT3Y7|uv%WDa?(_2Ex5o(P|x0X z6`tC_n#v2Sr+AxYiUY!{x9>IvAt)N)1KNt`AQOJkZQv*a0&_%?bHO4FQ5AVDR892# z$=1T&BPQ&tGv9_EglaIz*&Ov`C1-dS$XyvIm@yDCXG;K>iqtTUPOF*CGwdFS$+|R8 zWQGZDj5}O0#+p8&2n*a^`rnA#SUu7)12Q2HSH^HlnKT*-%XAL7uzJEkjw3P&T8dGi z{0O2(I|brfjQ6Zn3eEj=ahGJmWK_#5pkSzxbm+U?*i#&!5T2FV1I{xWw_knnyTAX% zkA7OM7r~0IJ5xt5F@BP3+bs~ypy5NkTsMkU)$(?Dw zF3Zf^g^(_K46xKiw=N43G)-00$t7(_aL|`=>1t|&kua%5&$tk~E+oPjz~i69N?;pi zf}H`AVyI#=Fq_v)xc?oY5~L>+YmN`^OhI19ui3A~?u~JVLR2$19z1djg%tU?Rh%~2 z6@$3xQL;-RqADp-N|(wivkO<+NnB2r0CXN-9;MB>3`%3hC_>=;RYNrmRnUXbaDy(q zQ}{deSLI&HSL^Y+hOzgJ#C9Daqon) zEQ(|*!e=so!PJ<<+Kf?Z8A3ojAeS5sN6cJ;^5dJvEc{@*IG(I1-vwkx-Yi%)p&E+$ z&t}k}p3G=7yFEPIs@972t*xC$M_VUvkGdVkf1Y>8-QJ|%YBBQ<@p_t@_#&u91uE2R z&VA3wpBh!>rZsBStIO`oH=X%1O}(vNuk3D?&)!_Y;pV<@_}g1eq(tC$_C~|E-af&@ zoL#|Uxdq<@@@~;!LZPw0PuoA|+$&KK z{Qv+U07*naRKfAK?!<`pK!*~=BRj`K8Z>ruR8j8!B(#^P`6Ve${4q$3Y2kl~l-lzZ zVv;ylFA+_+QA~=0a8jWZFJ=EENoo{F0x~gwm80QL#bE*XRouzy{q5QoAKSj3o)ueSD$!n5NirETv zrX53yC9rLY>B=Rf9Ng5XI;>{^Ok^$SzRkYai8&{vL-7gY=DBjk?gm77RHD5|b8o_d zl}-pC%DRF`=q7?k8F54qjs$jW#EY095{~GH#mP{Gv5(lO5l*qhJd0-`oSG!q09i^Y z+9MNM!@5249?x2|8QjGw%0zj>tlbb$2MB=ELqXISNkmTC5#NFA;CER(0xxxN$RK4n zietNM(ukk}fT|J012Z^tfOr{D#b+%rlq_Z%&_2k!40%Z{zw0XA4yfzCQ>kFico1w@ zeeaM-G*rFd1ZGRkNuQY&J|#QMCO_^jGX7jXZy$H^2vLQVa~~Yq$d@<)z6P;$l4w(XB35D^vKi+Ag1e6?~2my6wNx71Rx9u|S ziLY}C6a$UZ6mka!nwB99_e^D?lgUKfjuCX!db9?eZyoL4)|*?$PhR}u&(l}!Y5Q#Z z+aJ`AzTN-g&ltRzYt(7F>J&nf zs~iJOoN%?~dkhCxR3`qnHwJoE^zyL5z*2y21g+52S0Fq(ofEYojrv#AW=;N3|$gjg; zVHT{m5wac#ira=8B=`%}4q3865jKUx*+Xcd^Gw)1TY`XkGaYi-1#UP5X6$grUiUhb z`W$X(Z!bHnFlSPqbKbugU3|@uK2XAF|Fuaok;rqxc|>6{V%AV!p|eg^W9pA_Q>?#* zLS^$=c4H2n(Pd}vrGIlfm8+FD?=iz`PL&z$UZ4|t<`FMP9~=amGf>+sHn+-~JI&_F zezj75`4>NXeKELb4X#_Q-bHUjZ*Dq^5=f`jBMsTf)78V$tanDU`flyvgWdjh>#{ZN zz&eoym^wJ9gZiAFU!lT7TRN7B6{Y-(TW!zCN6X#L;JQDBs)LFFr@v9Hji-}tyOX*d zAKu$~c>jnl;mHg$kehAX+<$ab-dF=09Wf4XFe#U7`}@0?(qaF4zL+<+_cph;``z>2 z#TD(;>-mk7U;2$wtpsak+3E~i-Fd2bJ>cGF&FW^pRCxIC9{n1<=NFm&6xsvs%8IK* z8G2Q-m1Y?vkuof(yRt!|ku)utbw#Zg*{<~a|KJ~%9vm!Y%m!u;#C-M;kAeB+bS}-Q z0_<$cgjsM$gkb||h_BkF;ebC7mDN-ni~T%UArARw@<7UM;hr11VdlnG_9MTJV^ zAQqYQN+;=Q)U}j@vY?eXNYZN#^ey;9uS?xq4p=y3Bvmk!mf$R@f_EXvl*v>hq?72N zj=t8F)rZ|08YezLK*B+Y4{B&IMqC8{>2oO37?l#ev51!NEXWd<0RkeYQ;H>cpCY%a zsg0l_%W{+{YCodc=Yk%lBnQR2(KQ^^Jm)B3F%7z{;(>87&(?c2@nP|TNt00XbP&4aP4d{>0A}- zbwe2pG`vL4jI-I?VnJi&vV5p!J$ktDek>}co8fBaSLT4xlZUJnfY9xDnJtyp3!CY3 zgGNKDpo<>iAiaugX-N>4GBW^K5Q3uHH6#ThlZyT{yGX4{i+bp`G(~n)v5-YN;v==a z!yTk-m`O$NP!JkJZz#a|98vb>@kV8rNd;s}Q8~5eGXg4k5(aSJk>OMwW}yM0bhZ^na+ge%_x@wP!dJSe01z<8;A|C``j*;#72~01DG1BqD^(0jGwJX zcOG5gbmH6WWVJ~asZV4G0?c5_4Xk2wfmD|jaJNl~9=A}90o#zn9NC#F5pt+VD{Y)n ze*<{jfzqmR22y1d9nBl=SC6agN)3-n)j$Q{n0iv@d}eZ`wQP;7YX*Ts7+L#f^v1(+ z|I(qY6AL8pq-KMVT_I57H6im?q^C}(n|Ti&1dcB9|6^t`(8!Jim?<13DLxU6zt+WN z|MCrD-L`nlx#uW{c$6~H&j>+ON*((F+e|pXC5b8)yRG@K5)(*~d6ZqN0p zzcF#bkxT2vd~OQmhwg;gvR=$@S4+I3)7C|`Ui!{Q58r$HD4*Tv0gt*f)8%I7?c;l+ ze)r<)+TOO+>frdWS}XU@uP!f7*$(#~zQZAGU%h5}6I-diw{!IP{R!WG^K5wj3N^s) zWHnkWE5$;+T-a)Yq!zBOu22K)fAzYt#t`6`jbK%eTKDbNR%1Ts&PG%|z`^lv5(*MZ z75KdYBgy;yN9Fq`9BQInHqf}&UyQ~IS_iNhnVAT_8C*EMNDAF{;gtL$G^Lc>Rrr>hEL2fkB6 z9DZDYV6{QT_TI^HzujY~q_c-s>zAjm9=`KVev7g7oE%YwjmDCZp-@F8h8TDC1hnwXHq9J~Pe2&p^ z029*Lc_9kM0pjY6+e*b$^J zZr{r{_SZFqRu=s~Ct@W`(FDYI;}XgTPgO=>3RU0E%j>Vd{^Nh~FRx!-)v6_;oAc`- zmW1YJI*B=Bf-+A032M|gjF|YhHx!K=pbVx}Cf2&jI32NVJUXEqq*aZ8g+MK;-_eC{ z_oweAbCVx;Pb<|q^LHvoAGTgRy*YccYMpOBx!?To@1A}3XX~@}Vy!@i0z}P2&j>JR zfTV4k{ty*`+>n^Qq^~ZH{!JzgQYyACEXjVrQ9xbB{6%LG1ZLLFpEUdHH|4OcX(b{b#k(M4I+oy*NA;DpB# zB_X_74*`9b%5D-W)_`!Jq85_nh8YQS1^Y9`6VoS+K#;HS|30+AE7@33)fI)|bdoBA zII;z26M6##EA1JS6+9!LBkrPeZ!%vW4#qgu0D}qoGvh&LK>}okNRsCm)YxV2#-OBW z1EuZQDwVbe5{?4;;dV>22qv`pLChR@n0>S12~Er`H>Cf`+s)SHu-BbZqnsdaMjt2! zA)P7~Ha6=uM5Q0RdmrUAni5~VIvd?KF3HtqHy5YZmsef#Bn|?+l-t>)cDrg8ZVwN3 zSggThy2}(pT>I>{MYYM6?J?*maX!CG3{$Ujw&CgDe){@)7PZhyIgi&t0~j?`6ySKi z`zQOmnC;v zPfa_Qx7=-DG^&spcM=g!H!bucb%sc~EP25gW_G5`? zI*I(pjT*JKfVIN)1+U$GouHtaNJ*!6@KpSe9>(ZVDKF9*At9nQ4*bsc&UiW)4`>F& zwSq;FAEfpk9c+|x3K&Y@jW_{5)=J&8AxI#`XQ`9Hd7jphtn5>)A z0fcTe;CRGqJm4Dz%Xd*^Qf@}ET_-UK+i^rgdqRlM`3ynk#~SFTERBRfUDEj-&>Yn` z{z5;KM;*!%Qy0LW7!9iER|J6Kf2w(T#ziH=QU)nEt^g@?O3BeEVFsK?DybDWP{#O! zmr53h4)lbt$-GmcXq*w_lnZ_IN&}4Js63khhSn< zQU%>{_GsnAs|~BggeE&x_`D1f2a;|aD2OOIM&D86L90|4fOP|a`$kg{q{6jAUTtcR z7pEw;Tw$}Bsa8?hMb)U!I9~=PL&ZY}jv-};o-;tze(vaIi>`B)g#v97$(dh(E&_i9 zx(?<6!WX8LxWaTa=%F$9*8SjXv-=|;(g5Uh?D#@$v$XRdTi#4%X=er;c27la90jHb z3ZweW4bXgII{_I3?qS-0{pv?Q`r&W?0sPZOnM0C4zaCTzG;I6M5qV9TT162eVvBtl zWr;vil=bm6+D;kUI9zUop2<#5TJ58Vk$f-uM5CO~1x?wbhx2v>wl;1al-HVj^?UE1 z{`sG-qg~bctnusrIU{j~FTTP!Gg%4>fur0VM`e8L1Ozk#u)++PvR+P#oV3ZtamG?l zWQQ6pM#(OE{%Lq%Kx3qdVfD?%v<0YkqnTpkB1w>3OA(O3jmUcMbVUTSKz{ti^7UPGi0A3;U<=$w$5Tg(o85pFGcj=atCap zB*mRZn}@>Np{_yn!NFOk|K>I^PIgTBV4)m1^IV{c(;&Fl*sL^)co)j*-LuYcndQ~L zd2{jV^y=y7uLfi2e-+{A@q~1`f~0Rgy}iFRMN;i%w%Mp4rRt$&fb9)v`Py~^{E}>g z0WgBkPAr_vTZ1WZMuEI{BR3pz@2l<*VQm(I(W&{t5asRBhu?a5w7)g%jF_EEn1JW2 zZ`Yoj?2mfw=?!E~=GV_Rk=3a+FfG>?h=UJyj_z$X%B}0`$%r3GJ$U==TCD;Vw0(6p zZk@p~f`xEC98sT-CeQxX&KBN{*$n+r_oKJpd-F$s#)ys^5SVB`bASl%X*Fpys!B9~ zUo8mV(gGA106J5s*#spqSE-K1ZBpP$tx4>?&ixMt*21Ye12=`%wns8FssgX_4_^;y z_HcXg^=Lfyub^#Ob1G809ZMzw-YH5HEkDT-jv^Aa6X_g(KdIn(3H{)m#F{7kFBx>n zhx&C{0wjhR{|Ps#v_=SN?66S?ii}(vbscrc{I;6ToDhQGq(@Di z7bsi7Dbji<--f%wTjk@;Aqo|O-H~wz& zaWs)fElQ6YRVcv)K3&?CmO^lY7{4g9j$D5}_JNnkP~|8ekoFMi`kb?=Ars*ldUaSJ zs+h*?k?u$fB3MC0%KBv#gq*WOmlg+q!eryy)MUf}wQ-@@lyr`J!74Xx1wpfP2-8l;@q3ql$qz0ooqt|%Ni zC8=V$ymOq}Jc#Lo0(7bq+y!UJqTU5qf|?1X$ao%o(aGri^ou|IaVXFPb&u%*1^?K*vd;jR-+3RfY1?U??C)jcL z(hxE-%B6^#%~nV)#cxy^%DZs9DH3~n{+PCFh=w=B%twVJ>%c$tbkqVA@x>?dy6vAu zZct2wzE<*iu9lFMvAQZ>;tt6}UBLJqF$fER911M6HE(Q&girX{;taON%2-nll)O~% zF=lmQhZKK`x5GYC?OtnVKP^=Nn~g+|9T`n7CrvDE~cUg311!J$`qSGih`GLjdc#9db&;2Z~&W0|7STm9B`A1I^9EG`sPmJ3=%8yjoQ20e?p!LVQ5 zZXEAzj`}pRFKD}%-Ym+wbfZ-0^@iig43k3p0sK}hF9R=FPgpRNRYa^eGH9}K=UWx~ z$53@SxWD~N-+Ro!?LlXBHKMWJ_j&YSYp+u2aQ$VPEbgn!7@y*-c z{v}k@UcPuS?2mGl(*4J8!_OKpByWVW=xwD|Cqd%ZsXwP7l&5bh)y5lGvy2DIq<`b@ z{KLhwpAVj%=F^qwgck5%IIgXz4xT(b8Ju5%idZFbjm>S?rwI$i+EqSVtT)));H%?d zchTuURmX=g;S;`TKrTsZfiY+_HOHXsB$Wz-ZWpUe&IDymm{u`LB2Hk0r5qT+_9PEW zlu;t?Q!FuNm-zK)R=0>}7hQr>#!V(kXZVX`R7Eg}BVfi6Pfc66$MocOzPVE=)oP<2 zslQDlY&ZeT$!Pb{Lv#%?B!wZ(90e+??pC1gFke2$qC$et#p7x9X)r@~8(eGByVUzy zd$m0cT#_FK)aS2+CBhWe-j!nEoXhfZ9qc;~ajbxN6I@Qi?&3tDGv0qh0lV zvFK6)2u6?M2q01v9hUgF@Nz%z}#8g!!X%ax&VBFHC zwZ@Qq3-zrS+HAU#43_e9ky{I{ZZKOS2(jkuEBj8XzO6qL5ny|yB8i_6!Sw1Q`lrbam zlGbWy71uJO1~%r(fNS;5^4`gMc^_tAkZZEhY^K)?cq5rj77<;-W-=B9sUxzyVjle` z|L-3nj$bWi>-oZIyAOo8P5Y?~Z?}DvGG(VFjXcqrX`MIlJ$W&ntk?-b9^nIhSicrL zV)4WfBV_RbWTQ?I<5rTVjXMkR1v^F783+68KUl%C+uXYP>gP`GJ^N(ySAMf}@^S0W ze}ul$3c(#~)x25uxdf9)l<7wwtKmbq!KxBl*>(leAREY6$}0!bcdMYW8^B$Nvv^}r z3quau-ntAWL-rf75O&`-R%cAkXPi^inZM1NB0FXK$uoef|@s_BOT}kKcK3F`4#zEt;{i8Omyv&eb)tZNxE!%nw?m#I-_hdCMueD`~JL5$Gqc2tbzv6P^lK_mZTM1C{nSQ+L{FQ!7UA$36G+s z)aZnRr0#zQzb1OKJdYuLgkXea8waf=*r}Lj0;}mx67{AiA6mvT7a;<(MJdM3;qeI# z5`8#a-tNu;VGw#q6S8n8Ps52#wa+hhws&iXHKh5ZDMun2^h9=*Udm_Fk_avOsL93; zWdQ`eZQxr07R+u34AS&h=?#=)w!Q?ML0Mb-V={U-(A+AZt?e{CNl1xxR?jeCBg9$e z5>F!OCH!T{0v%P#z}|0o+aexH|J(j%7~se;{Z)_=9OmsTT<9g7bsi-|@{+D{ts@|h z^%y=n+Sq*)mU_4m4yd4}yKna-1uJ1)a9)6eDLvX zfev+%=tD`|nagCDC2bEyC10X3O~{mJ%y~;t45D-wq)gNc?SGQMWX%hh8Eq;{q-F*7 zu%OC(I{}h~HEeRZQP4drD9an3@%qu6K%XUtgdr#q6_x48NpC9Jh&%D`BqiiM&{PvG zqKWhjBzQ>@37W@iQ7+R;p##(*F0Wh=#uX@ zR1!k3DAKaBnrj3fKA@sdnQeamx7ugpwX4tMctZFCU=)ZW)2Q(Rkp09T z!I?!mj2S|=wr63oRs4;qrQz|Y86>o#P=pUTBnJDjQqozCZn)jFGj8+`h6l_| zY&he_V%bFfC=cd*?x=8 z9~6aDde9%kt6NT{3=7U+7Bq=T<`&FEs)rJM;ObzwNLl$62i(Wh#C47g=wNp?rE|`Q zRZxD7;d_uqBaXE@NZCEjE5?EPIckEgD-es&9tZ6oJ!s4)OCIp>u(7k(C{}a#?(GB0 zJwLrzWDpTwbNkO3=V&(OjT-~x~1*I7}uz!CB+5ym;s!XB%@2VCv*5< zB!GRd&1ulOs5dw1=tR3C!x;r`?GN@uEeS3f|~z?CUY0KnXvi)ib{)9 zB~dBRHi1syIPQfg*CkkFRO#X<8P1CZ$!lOkg;2?z`wZJ;+GGMk$&+oYhyVZ}07*naRGDQ- zXO2}%B8d<#Qn|L z85=ZBC@?8b(Xw^N8t#%rg@ir~j+Wi7U2c}p!41r?GKeSaiqsr_& zldn*1*?Z^!dO!fvbIY{>T8Ndzn=u58Fpfb47KP|yp|rCYwy(bY;!pq8AGEFqwMucb zl!w#MVKf_4b7BTzqBiEqj4|{{Avtkce%-Jwj;%KxQ}3fZA!qWNeH&m(M55v7_~}sh zjbCuytalf~2MsQ1ZP-ZVMi-sx_F8`LB)h%OG>p;dVEOFN>hHZ({rGS8e*8zXerKan z181P&E?odVqu-7O`Kgn{NJk*z&B?|XI!4SR89hj=41w|$MpJ22UxTI1YG8>1_TWTQ z#ZpQSKZ;CBX5#v7S>kC?FP>+}rq|RvQr6M$8 z`B-rYPtz3VOLXO-kpfFI?3k1e77uzfbX@x0l{q!_7Ji7ia_kUbAnT*-UfB#%uaYW_ z15sgizQ{8(wH(%h`HFGr^rJ+Rcb-#!78{QSkmXt}D? zs_3_|QXhZsTN5srS$^=#?>~kkbN=S(*O&c1*`w0~9zM87cssexPB{9DWxiZGJi14J z!iz6H?Owm^^d|5A(yuoUc2Kr#oxd*Bx1N0Tz5K?CIZE`El7JP<4dhmfTYIx%n>W96 zxL@BpI)3jvuP!bc^~&MV!K<%6`~0UrWe=xMdyTwRS*n#7INk&Cd0FmZDkZZus(PV% zqicZyAJkFn>Ua2L6#W*dNsrPXu|^iH0S==1I;(Rhz7_t>^BG#V&c$;eU!)YF*PJAT zarVNRXpfTQPX$F}ElqjC#{{tFPimD+RNNFvm>x(}f}lodonRrxLvfN5IM+npPar6x z=MLX5OC6>ZcY-wK^!S}a#OAKA2GOAkwUOS_0q%G6;SsV?4rI3d*%;bIunjJexeia| zlEtSc6bb(cQcn0!Aqp{v7PcGsg)_~dCB!)$)T z&9I3fI_rc&=iA&Jf;$IwBAleIAiji>MhuUU^N;63i48+9T}6tJ*7vT8l-N&pamZvR z=}08gJe@jy*q10C+|8dQNPJK1aKA zoM~>8q`QkSA2KlC2-C=Ncx0;hrMz zvV@mVJ7Wt4P1GTIY9bQ{-OLBm>x-}d_(%UAQE%ELXL_CK)tY-|W$i1f3OgD=qZ_>d z-Oc7Ek|H&tG^_2AEj#Q8g<~8Jhrh`F4gTsM;0S-QV>~f4iWz%`k|>grxObDipf~J5 zq4qT^_sm*-UFWUlv=}pJbXDfNJkN8^xzByy=fl7KfX^ypP@2i^RGFN1FoFPE;e@R? ze;t*Pz)8|Mvd=(W6+Q_OZsYNJdx6&3MT&lU?7`pZ?tT5iVvP?pXO#Nr)3y<1(^^|1 z3TL@9srKIH$kbrpxho?V?ryyQgPt0R#b;CB`NQ44QqS6NtU2lSz=~#AL$gUohE}e!e*_`)FqgT^6Eb0#K}Z;s(YcHGL&-|=5Z=80lv4>o;*{+*(<$F}8-Vm7n{w&(nNWDq`JFDq@J1{+uVFQIW>Cq(gGyzr58IJ zd+?11xyw>1H#awl#hPC0Fn^mJOt?y=GDJQ^I!p%cO^?3u_1|M;_s)wa8_Q2hV{=pI z&Nr%CJDY3T4A7My8amD6g)@G*zsYNvo4;Bd9onXDeDKlu#3etR` z8k;fEcDP=?H&Pmky^+arR1iv40w>WQUBD_D@d4RMmlSdgW*2=#9M53BYR({S8i>F( zdB*q%I$R(uXLJeCZugh_ETmzv0i4BakL7 z0MBxg^Z-4BH*9-ok3{^lspE+g)afKJ8sGxz!Kv2&dVI(jAeJtlz`i3EFyGlq z2-r=Pgf1s`BLX9HFWdQtA+33mH1_ahlh-5FO+vpcn*dNjmYmENvJ*=;Y^lYQ1t+7Y zPIVG#WYFp?wwzhWHVI78dX+!N_4@1s923TMA}--m%*X%W0p!tXiQ)WU?pV`)AXSY6 z`Cf^3$-4`sgtNz^sOJ-zrDmCIUeER8O{G-Ph-%uOc5#G*OlZRR{Q-NS{V7;*2+43< zBDBzQ2;h%wp0hB9&kgh)b?b2G+5fHT9;hs7n&|;K78Do`Q&w}Rc3ds2OS6a={G;}j zSEFVT?8yP8^e@}kdI1sT&8Tg_C@%{Nhi9Eu=N_!CM>hcgNwj5z^~v9zcR1L~@7n29 z#Hgsk6f2Ihe7-b$DK~McCp+e-3L}@3fs<-5{$SDN0wM$&OX1Vg{q3F{*895ktw)c3 z{qO%~ZFR4d&lE|+j!yPk3`#j3g7tt5#pfN)712cw5If!$DF8fJA9(TwEdFqh@Wrrv zLrB7=@W)L+lI>}FWqO%xqG<64d>sjs;gg&qpl!F>VGGS!eRy%EH$T%|Tc#D$zx`4g<_Ib&H+xRMrS`~EPyeQC8KfyGm|e4-Vo)E6}S!ziC{x#8UG2f zpe+C~A50lYteu4&J~KRGDP8seSOc~s<&1Ngk&H&mfcNNtKwK%{zX5(yy@gB=_i}u+ zW8xDispG%wbBBV)Z5U2KL=Nywh4C@XDVd^~B7t?=@#l0SV|RkfzXiGd7wx< zQrL*-I3~mmN>HH9G|->RGGM9Ig;Cxj@g)kT1B5e&iK*T0slx+?2{Ud+L)@Vv17b><$_X+d^I%LUYBw$d=2Yc+W?L&Ia^Q*)hmb8BU3eQjlI z@xraw-`rb%{QT1oDtmjg7jNXsBWUNl^_r^6-V}KFV0MUk+}jWD0a|7JSYIO3w(IKJqCS&VGMTv(KlTO=v)o_Fl#73{c(Agwq zL-gT+DuT~oWMDDS6C>XiMR9w{aq%a`N6p}q5-^&LW;A?+lZR}nMA(ZCkLx~;grqJE zV9IzHG0z|!1I+{B3E7-~j`=uA=F1wg)?^Ra3rWhyfQ+m~jiF&YV_b|V)yIJXU&iA) zm^$EZ<D-^`7yE3Zx>2he1KO zr@&1}X8JgG*c3;iJ2;_?amWjfoA+reDiGqfy+@cdS#fHFNxFe2awy75`@R;fGTOOn z`2Rt1;&K3KonG%`)Kj_|Sl1rIu-4Pe10kF+gI@3aqo7JEl2+L61jH{PV}8KV$?-;2 zfEaS(P3XOVC>wrHu3-5|3eUVcd`*dw8XFibqP#bIi4m%ic||{)cB3R!gSUFSXyL#> zJk+z5CC3Hk@H*6?O4ewU6Z|g6T6t!^Fn4*N2q>C!#YVOfQP4JLGTzJg?%kPG#_c1? zt}`a1y7uDHuRi$TZ$3b`JBjfu0cCK$f1F`=sF_1b0@bfFKeKc|+5XAO8)Cvs2Mvt+LLEPhj7Tc8SjX}5|wLnF5^ zTV8nU*}WfS_m8UEPshLdX5(PwVEMOwOdQXYj}98ikO;~!Dit0%GnAZh7S8!mmj(dM z6DPy7*=ecGMf0SAuZc%K5z-FMwFuIYTnAs*iC~EqOY&MA1e}|J2?t6OAdqN~=#Jia z%CsPtP~8P1)T!{M($uYa)GW5pSO{1P5ox)`ND+g!8Ona(gdDhT^pJ%Z$@ZQ2K)|BR zV_t+~(zreeU#KvH{}lOF_#4pcK4=qXRYp3>nBX)>$poh2-~}#K#~FFhoVWVRh7O09 z$Vt`$7yx*{l7O!mJmIj`2r0UV#OedsF-m(*^MxGrWe7y%2jJK26V&I}8t}Y06r=vA%$paluOTz!);@%j0! z-Svg3k<2Ny(rjidb>}OuPn7e^OV6IK?zRtfpTlv2(SwZqJ;a^w7(b;-zC1E@ZjsHj zx4p5kzC2JGf9pHnFZ6aEefHtr#(H0-c;(f%NU`dbO}a9M4q9ln=GMk;Wpkyzv60Od zcqQwb_^)*fWAktQ;eXa`?>_nbb4&q-N6HhElikWjcdeEmq|+(Ej@Gd@U{aos@|jF? zbtleqPD$ej>p;86#0XpFWDE*LNGuoB5ehEX&{n$wq8(&pisALq`#H;jZ*X{if(%Or zssGs1ihU`S!%!AynT71{s?;RY<^)b>q%ZTYw6_~ONO~Ma6a`20(PNgPA3^U;K;KO* z6w0Whm=QyYfZx1>_oDM7dBr}w(3+BTJV+zpvdtLyz(7t3I6eUL2qsxV>5OEVbJ-Uu zy&FB8O;Cw`0ysrXD){{tlS2~|f+>GoSZ-}^w1dXw-JagzX5CJ$)r~>euIP}}2 zQ{po4bmpw&x2P*H{+D8E;R2P6(?l^}qMWifEdCSlV;J?B)7a5N)Y8$yUskT7>@c1_ zKB0+Xw(Qa{w-9D?^kK|`QX`^8nV_XF00JtFC9?;Yj&e}uDdlJ%d7TnDT{c9P;^c5_?(f`YLZz%zVi^m%E-IzVhOyT-(w1B;IWZ%da(c*S4?Ks81| z)>T+G@bjd04#Xz51OXkD<}f`-3lF1f_&EJc z_w7%?^|C?*Oku&Z4PB3(r94k4<`U&VU`>3H+4nRL$c9bD`6W(wP7OLZmJvi<{E~Xp zGD#V5m?@KO52lL9DpN5|1Mnn=tUJg&kc&+2q>tLs#&9ZVgdXDk|J#56?Ts()R~x*R z;V#43G3?y#Z0)vDC=kYa54+dSje&;Z1qv^Vk{p;6CMr>JzY1W{lP{)jE{xZ=wsbm- zF8WBR&_1SE+-DeIv5*_hWtNGoy?rz17fCY#EicT^A0F1xuFot?Uw-L4k7oJl*2-#y zo&r!VSn6;AKzXnRDBEqJADp>#r93-N*$3k#sXiA%O7^tR|r@-g-3XS#3_y zOe}_j@NT>dxW@BE`m)^>u#xzC5B%6yCUlWqCT)q}*03g=a;h*OsEA1eFOF0UPJs!- z04z>j00~QegQ{3i5c*xEUUkUF#fd`yEPbDc861HmL6#r}Hwf~!C~ZI!{6d2fS{G)R z6wic}&5gYn?#zW917@u*JsF>y92y^@=iZ|uhMyFrpc=q9j@Z;nkCS^jOLimNnN_C> zA*)w^gy~F#ACuMKc*Ln>=Z{n`DZdpR^4~2mcR;=vGw6g;-Ri94SuBfM`&iP{i*T*-lt3jzbD#|9u|pb74vKG zjOP7mJ`!SU4=>5adEsA=9wLOx(NzGq85$@0GpFrPToOz{+fbODLLF=LRH z%bU2w!wDxL(PMrhohmI{%1&MC&5e>cM@h|lj5uT8%M2+tSc3hrZw#(tzmxz6t;))? zFaGwI_kR8{F3aOX=@LYtcB^&L+vs$QyoUZB-35YR6>C6=h@46h0mamjDn;MHNaN*O z5(*A#Kw-QHtz7^ouHc|bCKEtCXvYB%kKRii)ebWy)PQjnM|6mPmruTk+8OH{+P1jqn&&>)@$g4loqu>;iY zm|ZCx0tl7!6sGn7p4lugPeAqI*Xkr^42PTHnGA9Fn5+@cxuC6SSP>|*iwHp(dkr~< zdH7KvHmP~di(AOA-g~1GI6-w3~ zysr^C%A1O2I*6$G<-t+Qmke!9$3><*`f3m)v*_FHE?qWGVu#0@#|irA0_NB$*0=-S z*lUR%!?nN}@=s@uw)Q(Qw`h+hT&r?WX&%*y!1ear*w8`=bzpsNZl+e<$I^ww9Ai#K zXn<)SrcN)+jh6?G)|OYs%FKmmF!jBZ>3_Pph1fpZ-#1k*v8GVSd z>C9ig!Zh#YkKbRre}8v(_r~jY8SU96IqbIN_7=)efT+zJl-KRzwLi0XX=i71YjuT_ z;MP0e;Hlu;z4YMWUZsBR?XSOj_p58mpC4^+cb2w``9d4PKUoH%lfz>UBV1c&=4QIp zYATz9Z0Z*9w6pU;Q^b%ck;UnKX z6M2OyLpFqei0m`~H0?2MJq8OZP(#l~zucok~=+L)CIvIL3xuxfRS!4TGzUW?%~`osZO>qudWTrLLk5w?=coB4+%0=8DNl?Y60Lv zv>G)S2$6ZeVOVUeceJvn2N!|f!_qtPqZDpXgn!^Cwxt^%tx0ER7OxP$~u%VXB@ zM-y{WRe6NT*a43i|6lbg(v+Bu%V>oFr33F9D9>KZ&0ZN8n$?%U-eE$E4nl|7U?RV9 zhk*|#JtUryL`Ecz`l)q@n#*tZ9E7N*d6QkH?fARV&mT~MYP>`42i)NbQL}fa%(d*mQI{7VIYs}AC&Of zJ#I0L$Ma1)B+dVeX(!_l%p)~=;RHQN{6>ohtw_en5Y~syFs%cmysyo zlu}zP6o2k--ffV|%;|OLR$YQYFA)+{R`&`WjA6_U8;uUqP$$L%$1A}4;}39GnWL)JL6gx=O(JVmA$%oNHIOYd#X0NJMH$^^bB)uD|_3Q zX67oJ%LoTY&rRI<%A2R{+VZ2N{i?IO9KhD!J3KbZzOeg#bkJ@$3Z>%tE3cxnq$Bv` z^DoeHyzfYHl;U%GI$t_2IjFBO2ZI3D%`Q7^g<5ZpR; z5Y2fAD&Z|eEf`HfJ=&$f1niikivbtGcRJ`sOiG&RZoSaQ%LL))_y}bWQ6t4r^?)oR zT19dk%B9F{BDWUhi-4;ak{Pa$jFg>#`_HP!h^C^cMg%u+WUNUTl5isXKbpkS+^wd( zq=?&*e{%`6ifF|s%n7n|oC$IvI*r!U#o5!rZsvG!b8Ckj&pqmj8epK+VXZ!We!f3P z&Ok#;PFL^=NEMMlkfKJtYw#A9(ZGWuFQgO^FjKV3J+250*jV77;&@J-#Dh7+DQnok zhp>;irZSP@Y$F226N;)Vz|p|0!|uyHLWWDd6HQ9a^t7?W)H-Go$cF(caat&#oR7>x zmPe*bwGx=KPOg6lDV~K_kToEy7gYeJmUTF`q75u>xSd0il&T}CfN_zc0x@j-@!BTgshRmoL= z9T(3;wweh#%rOMMy8XdO#0R{EKb%x%jw92G0Q&UlR*M<#BT2z3+H19~ho0^)@>*gA zIx?E`i&K@7EXr=l4+e6}($JtwXZ^D7;F_s%VkBM|JY$@R3$u%s;R(fwbNTsegQMr* zHHIn<-YTbV(xrX}M!mY%W&u>K+x>$lY{e%dp4-0~a!J zJ|-a!B-Xd1^pMrdxePq&zjg6g)-WNa+e?CpSsa6ya&{Oe#P!?L7T#i+L9XFYh!jd~ zjA^-)XU+nB7*Ok>n-9Tz50$CgUe-H>ZkJ&}%sfcobdNfHe)Y9c${&g(orrN_qfaRV zyT~TTz^Nlow5@+ljKd4p{$4(bMXI*mG+H^PzH$7xO+iPY1^(vi>vH%2;QjMgCVJaV zS`}NH^{xG*oqCIsW}bn#M@;g}kvh~XmC>OhF=DOWMkk!lXUapVY%0~>Z)$$S!)M$? zp-`zb>!_9n2c{-R7(+)DAIaC?er12SkewbLU4H&#aHx3uo!4%@bbj~g!{sHW`J#6; zFc*glBSS-21AwWvnk{C;%r9IS8J%o5YfDeQSXo|w>CV^Z=B8Kff3p1i>3+R-^P7M0 z#@D{G`S5*8J$iYzc=`t!i%2SoTlZi#Jv%>l&~seh+U(S;rG*PO-?;nq;YZINJ!x)N z%9G4BqIRn4pK*10V)`rUd6RixEvy@MdJPNudTn znqL$=^6p4ZBXLf0Ta%zj+1<<`N`?XTQ9IBrzdt5Wom=w`&icyEIiAs*8*CjPPRQsyFW5tDehUc6c&aHJEG7QLtt$&-V^di&>pTcJ zp6ebO+`j1wcaK|>hq*oE;IHFr&WB4A6?YJh5WzNMP~Uc3-P>YQ3-8OPAWAC!ILqd3 zw;*T4G}()n#20X|k3-PjSD~bzlZYu=>5|z6*=c#UIDa8Cd9g1wq8W=`GuAO4bW)Tg zJtaWFaj&*sF=c|@Raa@)L9-5E^xMz>?%)3<6T6GKG{gZ6*&OB2*Rx$`KFk301=1WH zYsEIKlmQOn`6ag%*_996J!lpenXBaNKqexWjGQ9SeIIq8v|NP`%avnUpY+}1b(-(G z?4q?FD3O6PRvH;swu|LRrZsf+`apKHdGFrV(|ePD^k3&^uf6!;4~i$u-wA#eiIy#i z2+!WR8NgtcLKmNIa9!CZVW;T-^ia{x(Mp?pAtA?qf=vsZ#pVp8P4jaUB*U=NZL$$- zV*0K*W1!aX?EyPn4*VG^Wn>@;ECjR4wv(+#9n*3PBQyp9e5nT2t@dRYR$ zCJ|^5W2gBd{t3qcC94w7xlR&UJ)g1pG=#Qzzw{&f|MdFs{(gJ;MPb7;Xi<39v}NFgb~!xN*=Oo|%068($eJ4x(LHf3aQLYwGcsE6kjmVn{_oVLI%z zv$@RTr3-i=Zf);uZ?5tD?*85%rhATGJpAml2M_x*Bj5d#KN-%Se)ji&Ro&X_bWTf? zV;mgY)#?Eb-v_72b+6vIwYu`6x`WE7@%A76DZ-40AOCXg$p#j$*KXbznK{>L?yjsp zsc+V27w5@#n$Ot$#3x`a(RLO_j)Q%uk5$3i#3+5>$jrhG8;KKvxak@zK!;^MniK4$CQcKrDC! zx{agPzVnTPp4w^caCLp_^whdG$pgUgwx6v`OiyMol1({lJMJfrI6Gh3pqyQd#|4`+ zNu8q~=rbDxXzLMzHjz_E1VE86XJvrXLX1Ix(Szl0h&ZCI9 zyc*uSD5#Aw-(WOMC8nE3xbC9QCrV6(nov408SvL5+X?znusMX{x!F)0N-9OhZ-Ei5 zoZyNu-o)b!JS~wqnuj@{!_IC#6K0Y?TV~<>2f7?eS2=5ezyhx_2a4=U@;;%%IRMrh z_OoMyP&cG|I6a+uRw^!D&rM$L&yUjGHNYoiQCsQ(Aqd3yE2>lMXF5`mz94RVyr5Qn zWADM!U;f2kJzLso^_(t@4dv6PD9HJKGH8aQl+yjB@GP^ZYZD9B5Bp-n-p>QXpwq#^V(1l#7;@&244%3#wnUsaef{w?Z;P3&T1eg;CJr-T5 zqvmqy;+6cw^otMfpYE(?CTqo){%D|Qq4nrr_4Yxnvc`iwVp62Pwxj!aR5%U9RCDGl z9VAAtEO_cbjAN4iyLb{1JienO?}3C(I4M#h0FWSy$pfWr3@WBn3iMLHhe)JO)4A+mzJTj&hu!HI3bHm1 zOG4B^CY^&

    `_EH8`PC2p@zcn@7v2rcS})VJ=$`abdsHsys%Rq4q}iLc5AQI^KRl z^a@=*$%MkFV?00Ma{?tY;7ZZOA77oQ>^62PEyC7I^CPA7DW(J48gwn3|NQZTrJ?bux4-{SuzdTRpt!un*x8FWZUIFszyC?W zsbRgXI%FxiluiOFWJ;yfaJjsW!!kMO5(Yw`D_M~P!(l#gKC{wL4_$JY~zfFP`QLZu{!D#Dl>(DsxQdJS3$aZQXO zxG)BZ#M~r;R5TiudBy&T<4HI)xji2V3QY0>A>qMJj}gba&w7 z#_gATyRAnLo;iZbL&eS|zY9-n^U3p>`T6uP_AoRUWIcrVAX02h2%)qj$gy8478w8d z2Mb*414d3TPC+|0vMkyXVO0vqNOUt!8xdmYpbBHqLIgqzboC>R3$X=3G?V$`5EJ~` ziK=1ED~wES0gM?3fn8AQvJMmEWvcipeKt1sLRxPJ%;)X|OdCJeCX|fYomwX+MxZ_w z(PVHekt>B05o2-9qAY{JA8NB02?kro;Y|(-CqT996~=A@HepJT%3#<;;LGF9n?mJ_ zkpv__$CrAcM&h_JXTa!C1ESX9_QGEoPSX)C56D~rWZ~b+;}P672!{WQAeXO`-X0$( ze>{qEQeZV|+M`E`pqA94G){e?VKWGP04m*d)8Urppqa^z(+*x){-EtA>xr2zK(bkpF6z~hX z4ec6cGd*qIM*~e77+a>gUVZqjV^#$b623bbOTN2d=?J0U!k z~2YG~s7lFwHV3 zslFTph)1;il5T950^3QY9QbkEvNV5hvb4Uw&UC<6&re_hM-g4wuI@FDrzR#UjrPuN zZ6K3scUn{BVFn}WP=0iZ0KJqQ9L^&X?C=iHT^YZ1^Ze5N$4|DK6Bl0U^d9c*tTQN# zTLx9!m0NFM0s}*#$~u`_ zyb7&!eRXwjXY=OUU%hl@qeeCLl}yZg?{lixi0_@m9Wdajs0H@~>_ z{K?T?t@V7R*jH#_7a$#gU_c7o2XPQ`F%>60DtTEH(Zt#q_>dS>B#3CV3GCq{+&a(R zaQNj|QHB`jS;VB^?3$z4sAtGWlN|b#7tO)m%t{l4Q+pq35sgszeT!3jH`Rscf!*`7c}` zb6_$`gV)une78E5l2swnL1a9Ca!e`{eDQK2i^GiB;62t50uxRan8O zLUD&~MxGtYArv`SK^rML!9#_;$df*IIjMik8mg;(ndD>ruri)rYVB2#GD$BxGK@ z>+49k2*f01YJy{6n%DxqA_^s8*x3~kMhrC~50Ep$t}*sWN*3h~h--E2X&V_-oVCM1 zARe{Y{O_N4=*8V&8NDR7HHnu+yG0){0rh{BWfJvT1ERF%C@$#T_YoUMvRFDEDd|=Hc?UQ$W)SCuo7Fu_U zsT0-jEgZm2X({BK+Qr7g1r6^QSp04N3K|t$Tu1E2Hc&6GL7)tdPmpo&1NIk*z_y#a(T!QdQ>nAyBI>YQYXO!! z;M{Gl87|JZV5gG*$O`&3ZO><|J&$s-vV}bkj@zth3XI5HTmY&&h!WVN2IoE~62^Wf zcmy$0v@kt%@m!7;SnH@gR4U@i;@0EG%e~`M3pZch-h9@n>=$|myDi1WQy0c=ytGJvmLhDY;#eezi37RZ z_{0#b&RXN322r!y8Xp>+m|MWP<;DGvAhV3kpMU+%yK4_VdGhh6)klfQg^e+gsRi#HcW3L~3a>$HOVD(%4<^l@@wn{o7{!@%m8m08S} zKnUDhCiM2)L*5Yhrzc3r;e-5=ggQqudqfx>qGe$29osFsZALfq$V9FX5LRreaFq6w zEEj_>8oy(@7IPH*{lGS3*F_x{fjDZBq}e41WY7{OWJgCVG(0Bt$B=GNN2(3t-{v7o^h@syDO45S>L zog{hGWjH7kl2DwiKynCk!N!rH%?$eNC`y8K%uFKIFb)O_W=U?)<$DhsJmqLAMtw!h9hU3y0JXOO^z#f7sKRPN3V@yefU3C^c%Pg;#I2O1zBpthlyEwkI+3AM zRfdi*TD5_xMp+bMXa>}?POyW}vwnIicr@``!#RUPBAm5Zbf`PHCwex?4%}YZ=mu?s4Z}peYp&AS;z8Kx*Mo0o+v=b7d zDDTr%DPX8irB+N$N}*@JegFNx_~B>wR~kphbHjz=}Rk^-4W;`26w7+6SZG|6g)fUw-ue{@)`f zdmZc%xY#rkVCj6}%t8bb^rMjTb1hLK5maWW){zlqzGg2U&_s~P{E){=w~+rNq!VKZ z)A`ueoXswC2Qk2+ayP4!;Z8ugU9MygnB1p--VAUJ8evBXuH-9tg(n#X zGHXW!3YU{7;*>$XQ%PXOrAb5eg@D9IC5KD_fTEXjC4~DY*p>K@7#hgj26Xr!KiDgD z+K4&%8F)XivAG~FCLh*wGQ5oCv3UZYm!ub((3Bp;Hn0o!}!{CIV%f-&U8XujR36^rGepw`MCyQTu=SHVeeeiUvuyAQ0 zpI!Rw<35g~!+}brh0*qn+m{yS^0l?S^{py1cv0^1Vi58b@+c7zhMrVv6*klqR^MY& zhpqjm4<6t!K0PxvR4zXM_=AHxR5G*Yzy6Q^%bnGS zpMQ7{`!r78bC)hp&76CF|K8&VA1^M@k(_w?;FHInf3~w(nZJDfwRhfHefr7j{rg9) zgYrmu;o3{-RDOAFnR9DyYP|C3X^xIVNnuz0l-Md?>Trz$ zx=#hJV;LWSROqXsca&7xNw^BkG5N@QzsTz`t@gxA8kz3?*xnHZ5?PYo%t6NTqrh^dd|3H}7lkBF&QMSh z@PfE*_49s$C?Aieesp~Mop+kMYuiifZ5iO=Ft!3j<@jU&>Wk%kDpeXC>d)oqy(P|X z)@SxmVUY4aDP#$L7CPUgnsS(ghrgg%rYBuM{H!R7pCIp3_Zrf@II|-yV3$yQ#5|0k z%_Mvo0;r-uIq$50ARY3OpxQ>RAioaroV^}ZV-6Y?bK&4~W;chc0xPda2VvqwqtT>* z6G$fPfe+v>b>u`)N(-HN9Q7%bD2Md$0D_{w?E>*kBUY7ruweF_cb+4J)B`0Q{70Zx{>%7+~} z0xh&-F-0$IYhVyt!H9tIKsa9`3UX{gVHm#ldhWvQf#Jp8^l(TLLV)4IGf{&U5yw0a zT=N>&GjD`vP4b}wG=sdW8;?JG_~Z9~`jcPODVYXNuPuzBr>kvO!aA=H3c`Npu*@6_ zsoaP)lK!UK;|zJ_$%stERB8?FBOoRdG2#VSNe50Mml!L=$weNNSR{VNzd4X&fIeQ;dTpSmsdi=miJ`%*vam#J%WThcFdC5=(jhjeLtB zM|>mZG1#2GA{_*6yiu=5Y8*OBeuv{9zLJ@0Ob%yZo2wBh+2gG?(;75yNevtU?ywN( zp7|jTxOIe+WE9)dY-9V-U@54|(G7w?A-tuY%qB=?#HTKdTo4Ey_2 zyea{-r6oJEDRi;2;o0M+BOVIeJQYrFW{5$YFnr>;aL16cg(i%S(g6#e_F{2ts62HJ z-A1F;pj}NJsG_W=N2pZfGmXDpKRC0MLt>a=eQTm4qGthQqQfWu*`29Yl~KzhT;2MM zooCzilfmLZzAs#StF`vnh4U}J`}Ofc`t$ex>dCzio88pR%U^l(Ywv#W z!~g5W{Uu=Z+{pONS8wmFKmX$WUo$7_<||*xX9^F#_-uc-lN~O<^7`wT0NnfK&-XXB zSmjga=0|3Rw_hyZfA#|OVx&CW+1zcd?r3vzN&@Qsmy_7vKT^yck`DZ4r6|y*T;?=<)}#MFw5eYKa4dopxb^Gq^Hl@pxi|S}t}b!Ax`5r~%FQm2F3? zvaE=}8TLY70yzdBX9UI1+x&>o8gT0oeB~es+sB9i>Cnt^0#>yT=NRZC?j$oz*X0-ca zIhz^CkCzxV3T)vqY@!=Gob4B?qX2v`84uqOzngDiSwo|+{Oaq^o~CjNuBk4Z(om1A z&HkK^LX|XE=BPjEg|_}}PlcR6>Sb6V-iEA_vw=aT4TQa5Q2=a$RGOFYS|xefO+$D@ zP958V$-3}m845uAjyOz20SprH3$}0QL(G9irKFjNc&blN;5QzrrG+qKNHU^H5y8G z_Kz|ZKU|KU<1Z{BAXsA8>f=)^?{sn&PdHtLXc*H1d6-hPQaSv-6S|6hmN@enaL->x z%PvY@|KPEt)2`(ci#GV^&W?~7OYt40fel8o2oB04PIVc$dZgh~-#`4)o#MqinXzj< z>7h`RCf-12Ra$Ul60UD3#C)?-$iT1U@^Mn4cG_HB`QYOp{l$+qm{8g|ym@{C>2RmQ zTo+ir5cp549VsLw#yK$D6qGN5YC^(EVl9C@@#u_5rtDqKLnKn+b8WZ%%O+r$y_f(* zv``KxN8jI-Jeh1mymj(b{79RDHhQ#zq3l67VU##Lp(4oxu$z2<#1%DnNY@gn^M<*Ycc$_Mm?6l0T{0W? zMZYy-SbBnroRfWBuhIZn33U$zda(6tHu1aaQ1 zegn<{YI6bHOqusxTwn*o>DBy6qktReJ8qp;3IvVbpHYaDrS$+Q5^71;SPv_tI>N|h%58CUW z|Mn+;x3N(<$xQ=?=F{C@{NUeljsWt_Uc9<+ZhYm7&${iS;fV{EuHAU@@Z;4NE7fkb zP%PYd;|(&L&wl&sz0Eyd|K+PMQS9v2_S^gQkzzhKkUtr0o$gjociU>Bk50Qz^)fRX zDi+93u{KVn({eD*#$A$a*<($F4HC)XU{?JQ#IxT;KJ^Kal!hkBV)4sXqB(d>M3Su@ z4MDxZf}9rO@HmXmdS;Q0MpkOfvttpK!5lwyw2dP67%bP~EBrMUU(oRrVAWzNijvUp z_4Kw|&B%izdK)LBHxS)PY9fz6rD2vX2Eq{hMDs+8gFy3emG7%`s&9SoJH5xvC!ajx zSz*|&+9TxZ0XX4Yf3o%b#n|w8wv>SY+J7HMK5uIN!1fz7g-(%E;#Vt|wrD5R&M45p}o z^=dK92dLeW zN5j|OC|rCaHGa7_GjckRb(n^4Fbt!y^P!}$T z0b)kNCw6+)=!_)Bbkj;9DXv4%{ktAwB9pKP6z8@#=2Qq%N0(WEhS9|j;&wC_0~n0} z!z&WIA8b~($1hHv^pxt)9}n(q4Zr((|MZpWi)zo~UmVk>6?>GRYXP4jm37FP8#$&9 zMWWz!9iB3?N7pZ;xCZiQ&MA2l-5pqy$Xq^JgM4A>F7;uQ_fVaJW~*l0na1&`O=A-kS_AbBJfD3)|bw3cWq zJ7S*i|O1UFR9PEQU& zo{;h|Fi;#DW)eNyuC}+$A-uk|^YqcP$x@LORPA(!M$)gpMIi{`tGd3nS8box+DE-v zx`!h}RA5xgXxeM_e6cvUcn-(a&9!HZ-3o8!tv~w57cMM(^s^tXu5OIXEWGjU?_aud z{^@Ui^1(-+c8lk(zWbfmzV_9F`trli?mc_9OGEmtZ~lH?xBBSe!)moUvv@I+8@T`3 zhv>I>yjNbjdF!=TnXI(=e6?P0End2~IDc+;bE{gdlhRC#PFW!I50p!(%9E!l?9nAi zhtF^@B+B^|H4)D@CUyBfl3>OPOk^q$>8ZJ0Nx`ueyR_!>WYkSGnKqEL-vWcN^m1U6 z;UrMfn5mF7r;=VkM5G9uv2_DA4b&@IC{btz=9yfHo%^7hdKkrKHjxe1gJy#PyD0-m zh@+;-3QqZZiPUvlcFnh3WA@WkID0-i|niS z)8wMd2VMA;_`_!c9gY{V5`?PT)2)(});!`;IcHqGGdl zlf&J-O7miiZ(!%ZaqSvnv1JbJcv>X~Hjvs(-=r;zJBdJ$`i*3iF0UXNZYSp@NL~nn z3q?tx4%UdO3{wK6EWmLDHP>aFp;Q%tB${8bA3TTsYa*O;cRT_u>g|_jheod6&CS0) zIC1SXUF?A%qi)1$1JPzYvtW(ljb}omC;Hs#RVV~i$syrvS9UfYJpS-M{QRfC{;UN} z_4we;n~-F3o9lbSSOLx6lQd)6prQP63#Nq^3o3WUGO- z$U^p<_BPx$Nl(}0HQBcoz@l(Is|4+Ig-8W(ZN#2(+q$*SX4!`_(x0L@_qjzvOI-oM z#&HUT-pM=P+p8r*g5PFTVd>wJ4w;(7fWZ8oJGMysDgC>RO>d}Ey$FP8zim#B$3ayOAK%$ zh?ASe{+4^x!kdHm4zN)u4CF>NO@ivI=07XpCJ|&R9AaVX)y|fPB98>ML1-et>R3de zg6@@t6g6<#ZaJdk@C5tQ!G3NO)IHb62Ncdowa)aFDs%S`?lHQ(-3;=1XhTTX$gVMa z>!aO{4}%MVhpdcEy(XXr05z5jss4XDm#rYc>q+f*yR&oU*;1~)Q|AVG`I8el%t9-T z`p$M6Yr;ahuTwkRZ?rk@ws)!@D#N4J;Zb&| zFf%#9v#QCHfo*>>K0P-*JG)N@Z+o+`-yT1I<+X2o=cu*w!OwoYyIYyRe(Tnqw-2f> ze)_||JSonOUAQrG`N~MSu=)HqD^DIieY8AW9J#P~esA;n^3n<>a4*00mE%tR!6%AO>+7w*Q;*UIcG_L7C+S(D()7JT&MJuLZ; zVNn^JY?%mg?21oEk>P*#jEHR*Bob55GXgrVNT`ZZo?Omepli!@?$#xaB$Z?6yi9p* zgEPlNcs*V}r6FeoFaFS}fet^A2oN}`NJs+l)Bo`zLl$Or=hkyW^48RxCk5M=-9|RY zA;Y6jI+K^q^))*iPhWrxB-wy56NJt)!}Fa9(L2wVOGCv>kr_k!(}#?j$LPe@9tgR% zwYX@5qDPK^zKH8ry1P*)g(*`M@z7TFIkXh` za(Ggx{gCf4Xq1G5Z5>i&Un{YSBQ3~Gp0Pkt_#Af{R&+d*XupK?gSMIsdaEl>UkRKD z7S&Q%2*T5FNJTP3nD`p&PWC+RZDjolSwMXwWr0c0$I*7SIcQXKJ|h&HBr6L~c8}Kt zZ4aQ27iW7_wxRdPNge~fm4VvygN~?{rQ0>YgiFg=Ib`b*{95~2*GQ^Vyzol@+|7Z} zOZ0Vm`-^?4e6(*9Z3j;v@k8*^6QDcTF&x^6=nptW0rL*)`+HBHz5jzB|KcaV+2=JJ z9l!R{#QBR8;M3dBH|%%z_8`JwL>mXraC$Hgv!CZ^f?^FQf5IZ)lGozhbK76>p2C1L z9zqoE3DZJ`nisF_OswTECl*CmiY+Q~q5RWz9=qRN;dV`Y@@SY*;|RKj5+vH*kuvx; zT*}bqt9!-aY_n27+}q3TZkNCHC;1DPSKhlfxcOeHFxtbIH0aiUHnW7DOYsWp1ag<9|DF>A5*k1~(wxVY(kF{!-*!;JmLx z+@swU#0V2IzlRzZl|I+R$i)tZ6@faTIMt7YKoyE>Fz0aL@I%C1!wx$tSR$?BMQ71N z_vGN_aj=<1!uz>YxOGSgfuHF_(qKAmw5glXiew0IoqCOs&OMV|Sq2OcjWHbHK_Sod z37XVV+Sx0QT#l!~x_PUV+G`v(PTOC*bF-(q-^9Y<2)1_`4OG_xmtTK#aq;}Kdmn!BWM%&5UC^)n z>h4JTuTz+VBN-W^d&ah(TRSnC46b~n{lWnr>`{^>P8JeJO@3V<5e^eb zC91{1RQw@Z`q3(n8Fl{m_~}sG_u*`dN| zv$SO6qoTk`LkXDh7O0cA~;TJWu0NR&+ zAAEmWTk*jXd$>PUQWkMgGcYke!Vnb_oMe=-t(iEZqh4l65Z6qfAiM}tWG;L!7YAqD zxqbFjOc8Y9l$GwIcJqeYsXg-~Eg`qHRdY5JFi`7AW{6HkTYb(VGm@|tB@^NK?BLSe zKq!lYDSDY_IfcasdMs1sMEVz#f4u&XDwyS{<@Y&-M~>KW(!ueEoXbrC+BrF3LO39* zU>x38JVatJ856h=Wp{#AtuK`i9;JOrIZ1B$RE4nP+-r8STG#jxkL zCdg_$!v_tbwZP+wdy%)pUCH|jlL4zI?dp1u^W(Ymx6{)%`-p<6LVtgu7ePJA1(m3E zs%s~JyTCsz&n%zhn}wSi{mkRT=0S63=lPQdzxd71{=Z*sbqvqb+1}URn8kMS(Pz)- z%7iJBwdi{Lh{Ly-mf4u`r3PCn7{s8Y(PfC>e70Qz?F<;xzlrS=glbo7LWoUbSMp4f zbXE&E*lR8pJAi}wOKNtc2RvZx_ArJj_JbDS02;>KP+7s7b-+uIXurqjY zcyH+LpAMXi?f>%s(R8ttQ?qn@Y^2b3gXan>mP%SWCJRDGX^!%kY^1nQ??xmp4DHg2 zfxn4MvOL5LnW&%?AeW_E;9+7DDUXBsWL7CPO>A?esC4)jcc=SIo|935SxVmdgI9O)%B5WDuvT$AB4#lx;R}~89d%m!J6jZUg~IUdJ8xDtH`iah zU^r5$ICAT2UmqRHefoU9}G0>n`_S>_B8j`9)7WOe~mfmZ+-RaBSgWKWjtOl zT)#CxckaO_@2{>_>5g1__4eD}{_gWHe);&*`>^&($m}OZSC-c5IO3!R78mF1``b=b zI_!*$PwwvSuY~*{ggemr7VW@NnoQ-Lw>OIKYxQ7T>Cgz;hd~rhaPx!W2Ot6Wg z6%off2!dxWKgbJLhNd%6Aq(sto6?{b;&n?f!%M>Uq_7AkUFWF6l3LRWvoD81*aV?IaHR?tP7tJgz(& zOy1;LN68vpljw?jEP%7`ZtcAM?wf4My^XC#vm1p-g54$kNmj7w9WzKbpDmS3MO36J zPGn75$HoY3>(zuHmyA;LVMM<(ft=0~jlk0wcHRqdj>dw6K=TYS3=o&uwvu1s(aIa~ zq4*X+GI`FhT4iBG)kYJMo7I?5qXS!a5qx}6N?;wSVd==Pa@;4Tzg~(>2rGXRa-W`< z7%~RcQKl@Qk;W)q4}Wk(Tu3w&EJt)Vt^~^z{Fs;p6^I)H9JZ)o!*Jq&DM9#ls%LO5 zKm}nTa3}aS%EYA{)GT2Z+PsGUA+KJ{2!vL+rou)#UFSySQ!DVbW?b!?WDf&=K2Py1+m1Y=AfYAVmfERCn~OpDO^q*lQ^i!q#%T|OS<^?RhE}t z{OaD1{`Fri)!JI$o*cbM zf>n4X5{;!EFF#cFaSq0ti7sixsidP3$!JvYvB8B@h4YhgGX6=>-9RAYZ6$p1*enUi z5{0#xf}aw01g0>7K(jBg)hgz*tw!zikecwl`S1UW^yr1p|MFjtrw<^!#)3Yh)EVkb zhdN=U24o^)6CuH3Ef{dlN)F8qYo0Ypy5cJKRB|FOa;9VnlMeRn15OpQ!q+?HF zY`jUp8M%PvrcgoqGW7#otH46Ixy8PdAK*-hG;xzEECuAi4W3f2+E+Apvol0&Gw}x{ z4mSXqX-uP3f6+YjX>Uf>*=9Pj$vbelBB`v)2DXYxm@2Ine^R+gGdA;M%g zZFQIpzD7_K?Sd1J2mAm0yVv?!oyK;(UhCO!wg3_3-yWXelzr*y+*oRGd37DV`m#u9(&=2aRs|L( z`j?A&t3KCyIF`jmGCAaxJ0+PTfMJzD2d1~l*5aR-`4Y!bY?5SavCfKxGARsy6X}@T zUys`0mquP0eZmk&_~)2AVU``+RJ#=CG*J`Ws>x#%M$s2Z zE-WcL-KlOJV+|h|AvTl`QGqBPP66 z%!ksKrUMZmTso!C8Jc}$G|~o0Z=)QwePG6*g^JEhLeZXUf$;k%2v-5@=T=Xu!&|ISdM*D;boaISTFK1 za?MYjHrv;%H&nTsf_Yv#jlWK5or3@r-N|y&MijT3v$$**$)mkJoNh9|M&pFQ(7k8W_9YS7gdyL80bg z&M|*F`*ZBZG8b;Jw;p055I0#67RnUex*8rFHMVz_9zFlXdw>0}fB3l4fqw*=bo)Z- z`t8ewk@AcCtCfvCd7%P>1K2xPPmW5sjPxBdN{7a5bP&Qn-HHHWPk1mz;+TZGXyr#j z5*e+}HAehcnMbH2z}UwQ8(n4ffxMgAnmrl-qpK0q(9V7(l*DEu!MF8MTf(rw^TQ?E zTwnGe~%;0Y-&arS8P&%Go-&%PX*! zdgDPy2^c#5-mIQG%GoH|X$Y}abuA~IJ8eVO^R@JJ015alb6EMzig3@k9CR@w!&5%X z+;UGG5{c-favFSpvY#$(t%>JyYq(UV%A{1!=ku4YP1Kg*YPB<^JkfJ7m%ViDGRlYD z-Fg?0y7zQ;sx(~8?(HMr=siwlPl4ew{nJyU_*^mCo*jyoB$duwx^-vf{CsWg>8HQ? zRp&JI=DXi&?rnc||C9ZLzWnIK-EV#O)myhe`TM`V_wgr{W@BPvdSP~sToQ%^fL67# z^VYY&cH{Ep2Os@vdu=ygDqVT`rLFb#Lwcg^dU_MN-U-6y~O@qgT2*~#Yf7cO3A zEp6^r@oSr(#(k|pG=(NSK0VKi-QC<}Zt%-@U&(Z8tDiiEJIWg&r{P4wbwxk_lMbnb zV?-DY7Cj-6hcg({Q<_xCepR909>(^D)-V=Zcc`=p$l;guGA$*d%q$LM0rOZ}wCeFq}p*Vdm;oUg4NR{S(m;Y@(1DI1f2D#;Ud@8SHAN7 ze=wLn=mH|Kj^a!=9iY{xA^Jv%yM5`)G;e?4Z*XtgWH7nOFG@SlK&=H9l`jncPMk*1U zD0efcg9Un!;MEv&K_<_%R0N%Lab(bTohltY61Fe=9`#f35mR7ISnbx6A@9~FF_fG# z+>uN~GW2lAPIOZK@2VhoHj&ZR1vT*z);SWRxCM?x5&OU~)r#0G^@&o*4Q3E&n5GCX z#{nDQcNm~rBO0#ijfP{J2??--4!fyA^2MgvA5}kfE!fzcV0YRe($3n{+pjX-)T`4^OJM=fp7l) zwW-C~?e(4KpD!OZv5TYrw0E%C6jLh?_T@Qbo$9Di##9I$TY&fxtCA2H*`5-3`K02r z0bKg8$u9EFBfDegxrU@ed3oW79(k(igaI`Iy3^PVmm;)+F>z}NQI2gI?3P|myd>9% zIBZM#O!+%-jMlBO^=PpF!tUySnEl@WP#(Rp^dJ8B>B~0{4jN#9A=@QmIXm|~TLf;M zfz?Y%M?carOsq}u9UWd|yO@u_QPJgiFiT(zdx^urOe&w^b?`6rl`+gS4G9#+#+^5S z_Yf;829kiZgSu!I!C%@8wci?MX=2t%n>~&TAr3MZ2Q!Lv<^((a*|7 zjh*eS8gLjAj~kb#7|=q4~7fOrRWC%pygI$V&>6dsXVlJ^LBdR`0;x` zd;eFzUbytq!s59{pMShlZKI)ETwHwZ&Yh#`+WSBL;r&NTOp1E#)z^W$*VeZg9N%d* zN5=B+zVmka^zgG^{cLBuHgjQ-$@QD-+a$_RWG}t(_N}*G!%chj$r7c`#OTOyxkzRD z;>8MV?~51DZSL-X9}SI^$EN2R`=YRg@$u`g+-~l#KmFMU$F-yKP>Eed_QB8#ZZw~T zzhHb(3*pdXPYO%y?4woy03ZNKL_t*aXfZ^>68NOEJUsyoJDxlOPBJ(WnP7k|v8~J$ z#ie3DL=7qbK)yD}n<-{;4pUVay%H2ODcus}B^q-H9vk&tQ97^hD1ld(T%OdF*qGeFfkj{s>~zhY|Nk0)V+Vw z)2_8i_^#c(+3oD?t?YnSCM`uXzO$Qm?6)vs?*NqL<)@De4E!wSEk0s8T6DzNcr+C# zwkQX~%-GbAWG)@&nAu;P;5I%z($pR>t)j%_Wt|CWk3m0YThlURx7)|Gk-#;NlB^dYckeD#X{7~M4$-Ai-e=p2#iox2KLROs5FT6G| zd%bV;%1L$vgft!_DFV=RED;x4!ZKrJnPXYB8Dgjv(7ZtI$M3jvSlixx{_y!PfBiRq z@snqp3~W`>QtCbV>YMYI?p`PRdH!g9?de*4E@%!wYTruzu$1a6gZ&X4e7=z}*ygkk znShx|+al67*S5X(h>p4?B+$0bx~_@{mdt;|jw>u(!fHnSS)8sxvWjpP);tmRf=wD9 z;VR-Bhzv1u5=YVox*W2-$cuS1LFop%Jm^Q8oi5er8$EfJnM`G0dFR1j{LAyRLr^lp zYntGMOei?$vCPD>6@rTT>oL!yqi~ImNvI|p+%d#xd*19T@iNy^b59SBQ zCKd(?vwQ1Hua$pNrxcZ$=lUPjekV68tb<9n(r>Ep2 zOx|Kt6DY5FXMfKjkZc^7r0h%uV(77mu(R;eMf_fn|AV^4$V5RJRyzeDHBTH1gn6F2 zi47sH4k~K#!e%9nqUx8vUF*`*}->onen3DPADsA+cv5)`u2bGNq&ECn$ z>D=gez1`X0*&mykEDvS2Ha1G7iOJa+GTGhD%|^Rb96QHw;H9SzTN`VuA1@UKb5PFk z5TUxwl45}0Ydz;W1d+!g$XOPl5t6*8SW4N8r&c; zj64QohsxRZZ|GXm-zd}B>^p2S;54YwGEH;{A^G?CKdaTEj!Zbv2EAz6MR-i01b1>$ z07l}?#&FLixo}@!acGR(Q6rR?Fd&qmX3S1xK(C=+)K1g#!X9IUjhF^tF^E*tn$?O) zIi6{5^Z96cYS490a3@XM{^~0;l*MZ9IO)-pdYq$_IBQ zX$LCvh^){W&KXVe;CW;bt`a_lADG0o*t3k>$qx6xph;zWL0t$2R*(eLc!GVW9TtzP z+d9~!#u~t=FAMDyj0Mc+MIOR#a>fwQs^b@6PaZbK7=3Y3A%;Jfu%zZJ zzR4ict30to>V?MH68*bq_R4qgQJZoa>bS(#{vMznw*oEM<}yyBMobQ6l=}|T&@b2{ zJ2I6%L`p6AF?jX(+&B)NmAm&z2h0f=KAa}X#( zD6Ikmpv<+73g6Ter9Uf+?_G_xYXRh~@ z&-D+M1qD5I{9m#%BQzyRoxn-L5j7mz&bD6Q)W=2du(7+p{Osw^Kl4dD=}rXf^hk&V05=C zWALa5Sb;nxG*m*&&VwQgipKx&#hK#l%j*w+UF;d%-28a{pZ!nUt9`Yf{~zhG>Eqsv zMtX)cK@&6U@YZ+QAf*YttqzE~&y*L*Phd92{o!43#>7i4|3}0hmnA+R>_4qM*!DJb)+csR@Tfj2`Z!urrO&td^If`y3D z8NUPX)$Bx*eGaTbX@mwpH%?S^74)ZA+?a;qp070>8j_x%l2SL*mgXCA0M@BA3ilv( z$LrVTHW*b-xuRU;`H(o!KjAr~8EZu%`R$23Gm_w8SN1CtBSTpI?KKYy<>J^_dG5j# z7JIu}yN_1(U~;AN{TCKy)5o2?JqV=a0;12HTO;G6{iz%~mzZCx)w1Q{D_{A&d?rgH z@6%5{8Jk&n`Rm_EW&6MQ@IyHF)AO@8-gt9i@%;UJzkG3jd26rs*0;a;%4^pjJoun> z+&4ZpTQ29`ca<-f`>rv|4Mf zSF;oc64sOtYj!?xnm6U}a1;CtBqWLqd4{A_Ry+Y_27`i3kpBy4N|He$Hi#559B7?Q zFOn_XBEengogdUhhK5f6cy8{icfRqb|K#+jR(ZPVY$3f@2J9)BwD`U_TWzcbMNOtd zI3)5q!7#&7B3k|C!v5#v;1TV#G>K2()>kDk(tFANXqC>`Vmr!=tIPFkueth%Cw#qV*aT2aLDB9+lICeSW21IU6!jQRT* zypk!Orl}6|{8u<~p%;V|Mp2lE;-$2xtK|>m!v0q&NDeBK^G)Okx;z{M8_B< zAH9~d(U1DAgqlL=ixSXkkr*al-Dk+BA0bZAxHo_{n=Mk=9b@FQLjPym^HAG4Tp9Wg zNxw{s7@sS=pruzblsBqxxDugMgu}*90n);L^VQ=bz@aV+O<6prKmDI@W~U-iRw@mb z58yd^3>|bJkndw;$)QYD$HB&=3;qt)yPdEjc#Ki4P`OgP=%lRYoc%?MazTEc65j{- z5Y7q)d2sfV^iX#0R`1M9{gc=F^5=SZ3%Vym;EScD_lbf(da~B2DOx#&L^NAY-M+oBaQ$+p-M9I8Y31=6-DE&snGgN_>n)x_ zf2sc@Pi8;ZgOr|NE(y>z91230kDD=RTLeOB5DpHE7Z<5sL^89qBt8bfVyifQETC1c zTtBs-&qVV&O2wq180|X>9*NnJT=GF8@&Wwto#vFfi1F-A9X%a-$Tm1Mw|h#YLKHIT zG;^BDM}@^#_y7KHFwz-d+AGbxIACH9Va$J*1g`H&xMFjWxQr-6iV;av9Qyc(>uc(9 zlA?NYbet6z3)-P(^v0bX519qA{1FdO#RTtzMh*#3j8JA@ILrj_K*vFq67^<_Vhs+A zx7DMU;dZ0lXtbrBTJOae!lVA$_Re9g#*x7!CZB#X@C5H6d_Ni9VpCYw;Xpc%;RBv* znpLsPLd0bl+8*FplnbP=3Xe2pP z^@5Q4Doo*lD=bXR?`6{fAX6=}Oh}D&7G!w z5U6XT)1WN}(R6Hj;m+58fBV^|_dj}Xb9wLX_y6B8{bBeIOBV$aIgzp1M!mW`;jlVJtwfZx$@Ze4EMpr*Kv2?#dQv@uiSq95 z;leHtUjk&A0V$VL>@lS9I5Sn>A*YZfx}p-DxaGW^_z4I9Q3i`#P{V#LHadi+3Q}u$ z)%PjtslXH)g(Om6mGs+^CMS|B5J*a;7kAK6aiS*qu(o~C7#txQ4g@SyuG2z2IGk!1 z^ZWa0wvX1;aU!iOOc$l_EFFi!EE)aUCgVP$-3bOrm=mWWy&XG}$(gzmy280q{tpgH@U%-am>nyf5DC_QJ-;eIdHmL=|weRN@+!hc}cO&k>+YnCi&IMI2Ik zw&_67*{S9z)!#H5NK2`TUapDOFONT=)pal#LZPK)m*t))7j05a3>J!OS_{}UB4P=p zpRC<$~qtX`qFi2bcdXb25;9!0LZY27;!RBkhA+AM&wF}iTwz>exC~Ns z&6Lm)WWxm=MoD(S*14#$K1>aJrx3|hIJk@%z0`PY@^JN-D&ZEGk8znGI0EU#_ z2Gzh+nTE*fduz*y$7I}+0;;$R z$)Hu``&q%H6H3Y@XF~lUH&hW#@CeEwl>T7Ksz}Uf^wwZ39QI9)^bsRskvLOY`cCwA zMZ8Db*}Y<;KppM(oIgE26c3;^qNI~aA^XgmnfHE;0sAO5IQ*?IiQZ=T(`%Ur6fuf8>T_RPlPTesi;9d>SgBV!jYUS3|B%kAw~ zizTXt(Sbhlod=H|uCMJSl3;J+M|(TVn4S7Oss7~IOIOk8KD+Y;4g*7j!xt`|+27e- zTHQR#6(^>rQv->OXLGwmyLhCWLF9kPhwM%!nP|PfIA1u(lXCPAB?m@EHt#%X<=PWh z&sH;sjWSC7I{M;pS2!B<%B9{MVuXGOMU;fI z=_IJaYtZo+{w7Y*{CNK;N#NZ<;p|(bWn}`H+>mv3R^ISRY!@q9bZ#@;QYV0zadQtd zK0@ClPa1bwqZWu8iaPRmaQG4{nuO?^FJ*pjJ2l)(+SaZ%@kY?=2!EHlTKTi!63eE! z6((~f4q)+9-97AzM?Fxmtn^^G;|WUv-Y$1qMMXF{$6JU}&|nRRkVcyctMW`6&LFo> zoD*eq!qN9ed?o7=opRLmQw zZAw5MLert)3GsY4|#aFe#N)B^I-FbV~ScJuMnXU{YFph~@*L z;HL`Ppj1U|Lve&8NgeL@HY=PjcASdLGXLE@qUcasYj0WlX7hp*kO;GSCUhS!4&p!WERdLrLlHmFWGF(d^vYi*jQcp z>dr?$`{2n&n!Cns;Q>e8cr<#ar+WHlMnRtsH;&exEFt1!d#LvD__7sOuG#AL`MSsl zY}8;F##9xB1WjsMkwQ?*%+$3Kc*)Aq5KaxdHe5s45T%xqD`PWI`!N2tFB&4uk`J}V!p_}I))SFCGwWhdJV&;ZGqb$((f?Cl&J6mnHF@<2-)j*N{B zf%DU+hc331&jnL`*I#+f-zd$0`SI@d-l^x`c=;RO+MB!m^yZgGJLzO<_{N)mHZe7E z|HEHA{_-Bixic59^bd4Dyz^!5Fi){Pd~$4bY;pPpWrU&h)d*)wqF(lr{(HW%jkjLFmxSv^1uP{GLP36}Tv z#wL_RFzg$fp5Y)q+}qB~5q^gL5JtpuK}pCK|c}3HmGC!1n}r*1SqyM+(YqWq-;w&QcS7 z5P7~RMDm%HiqZ;ThomSyQ@$x=k}r}3Dk3`r5lvjEw^kDct3hcmk4y&olE12f&73VI z&rxGK`H+?h+gb*h$^>L1pyLMFkH!eFIVB2LKfY!d+2~3YqiCtrDbF6}jg*Az*w>*V z4XE*3#!4zXQCclL-RLWlTPeO>?GQDk9C)n1Q!Pv4X~G42iH0UIZqvX$V}QTpwSA~6Kp`O_(9tppd? z#c`*We>9@iqr=SV;_8?8K6>Yqr<-W;ELC^AqFzsTz}FjhpS?I9Nu_F~`qHD#z0Cty zO`ed-aF2U62NNur3?u2~VlMLuTeH?BD563}@s>5}1U1E{lO3pgMoET4;MzCn15gRr zKMG(uUZ>7pGJTC@KL!^ILR8a;W5TFaBXbCg{PtEB_zke3%on_D7_6WJ?G$BVW^boN zs*?5ix*LV9W;-0r9)!Jx{@4G-(MLZEqQm9G&pi`ZBR9p zso52Ijruo?eR8UKYcr%QC)R`V!Ws(;r%ux+BEdzTswbuK9!=(a8@kO9c)p0=S=Z2r zFOuR44GL##wY}dP4nqer0AO@_WbEW{;V8SbxKZE^wLGb0d}=re+q9o9m2}Jsq>6SY zhlU0VWoCA?3xynBEz?(TT)Xk&*5l6~-uiv56FvXp>#>mg@fYtcEvz!ncJ}I(i3=Co z)$HvL-q~E)P7MuSyYU=_VjxM|1YUDKx~A_4D%`wtF^wceqj=dNChggyCuacz5* zCw=zf<(_!>$-U1vR@T@8qa&kaJa=y2CEe-o>j{S1wD|Aj( zjtfH)9Yjv4rW7Q|fq=yY2&gwn7KBbhZ-_3JME5EtquV{CcW zN}!6a?%^o^Sj#t%GO1p6N+=^IEsC3BNF|bt>sdzSdS!ocEf5Nw`_?}wWLH}G9g};> z#hdu7R%_i%S_YluT} z{Nx=WeaV;s0em11d>~Jx6&sF@U+@l|^(1H9u~GE82zrV6hWT;kNDrYfTgPkDzX%3kX%Q0J%$P$&KwjxGX4Z)6FU_<5TwXx|{6 zvZbOH6nR3Byv!-Y1PleJr(DjovL}E1*V{L5_C(sHdd?q7f>dJ}0ZwWBJ#t+&pi`A0fo*t;~ggKSEt zg7{K$U*c7#thQH}DNiI41$V}F4MjkgJUAiUL+WHumqV-CKrya#oCHwLb@Xj?Q5WjA z6N|JDyZlf4O1EIvv zQ!^thS;9lM}j`a zJJ9vOu|?A)I&Frmp)#XpWeJ8uamf-muHT*x4NVU8jwicf$(^;mwcQMTJAt5g`t$$- zm};%Pn=4c$Z*axp-NPrwdJ`eUGC91|GTC4}diAw$Bt!0dAH1_NzmyuD1{y7+{+xf5 z$!5EIhp)c*&55(Kn@?{)zIijBtDV1gZERvab#zqz=$y^lbwdFs^krK{Im<=ov{_j37qJdwQe-1Wo5-QB(X$unnqvBw6?WST{* zk?b9cM8mWsk-trxIZZ{o^z5O#)=4A(866rtF}%NXl*tqz0lV=kT-x);Ly@5WV12i| zN(^BD03ZNKL_t)6VXs6=m=uT&LqNrr5LPH(gP;-|qYWt3oPfxK*Ti;{lUd`noyo@= z@YsvlSPSV!ourWsAv<=R!9ph$MeKlAB-KDy<+8?!IU!7q&Dz6u;3=bIO8`FFuTUgv z-7q@K4T4hiv>Fgax>z(}a1J*i(n!ORpcx#Fgpb3kgNjH9;R|BNK$D&{t9F%I$wn4R zfz9^|X<{;69GpyPm3_Uv0+iDZEHVH8|L_zd{?P{j5zn)%NeD=X- zAN>7i%b5xnZFwK=FlZ7wF<*PMCvf)aOls;BMj41j4|ghyoF)a*2+IjeI|%oF;22iV=AnJNadPvaEpbeFSE)}&h>_j zm~O>sF=$c|>0{eBSmU7b9bVpiGMy8`>DpH`0AoO$zZHaGGXm8QN~HNil_>{TR=ukd zj>Xjw-1L^(`GMzVOX;AexB%7d4MnTq+9LlXa#JBq|AWh8l2+sma>UiGxAam8lDGoq z`)cvZ5k{$;_Y&F;(aDRH;RnFGeslVTjhJqP9j2h|m$x;we_EfUl8&JB&ty z6&b|94%aQUb3jz`2sD14SfpMZ$}*NFl2m@l>Q5n9@VjmrGbmVK@+egY8k9;E@r({m zcc)xMyVnV!sq}dFx3+@*7URYkCBdeR_}sAw&QAPem(NFZv^AtU8XcBuFUW6>ECDiD z8#=|GAE(nAkA|jgo*%H5xjU&)1Pe3^3I^$r_Tv)Fe>S2~U#zDu7)@2Gty;0DTALFv zm*(6=I4W;t@RNV}{R=ZQlch{)X=!Ue$HwxFkN1uCL{0J9I;=2zG?4HGaE6~cJ;OYw zt)1Omnla^pfyvoxue?#*p8N8>U+-sHGZ$|pQaxB{FE2gC0AlvJuj3ucw3#P2-&=UN z=#C^_|BJus>Fs%V^VZVxb|jHJIWf}Pmr7$Sf3!!(%Bh)I>d1}7m73e%-Io}jm>HRx z&Kzt!xO<=DB-t}`;qvn<3-=DQ`QB7dfDz_LX~evkBm|&{BzCWG+RR>l(e152x^+_q zAYRv{8!zy<4mP(A_jVciG&(Z%=(EqlHBTrUsHBUzQUwG$)tzLGv3x^_TpV{LiKAS> zFq&o-qmDPR(^9@^`zqVedX$*oFmU}L>v=^tougX$3O9~)&3FTyaOx(u@f2FREJGfk zhKd>uuVPDSopuq7+_G(s!nn=#!usZSXxIj$> zGc}e9Con@0g(FioE0dLeMRlhR3qd0gDV;tF{d_tSjVem9_9^&G$7A_4-^B6l^f)O1 zt8z7WH+8Ic+0UV;Ee(VDdloec$$ERe=w*7`z|FA(&m^@PygfeW9ZZ*#(0BW?ob~#cW!2| zMUt*S0@i}Mgk{3Juq`<DdD)tC7p!g3T)+A&$W(55+f&{v8SuX4+gkXygJaA z6sM5K#BElCP8JF*64X#s(2n_jkV5IUYes?y8L_dfAQ@rlBPx!I0wEW(*$%>|=nQL; zwHO%7XQLifX%7XV0w1eFjhSiTgCv=c@G;2`@b0GFN(TRY8;7l0A)A>rC=gFr%8n5H zzfHll0-EBFRUCpYGT@EGg4ABE82;2~cyQJ;c-oVi@+8Jwp?;SyO!Z*}tIlJ)P0GGh zNo(<2L%(^>Y7J_&m~o6|<HBxriVYjdAxM&E1+R

    uJK1;k@kB@J|kJ)Uf%wZ_CKpBo`h7c4h$4H`%r#fuh4nWEiH zr7gxU@c%?~g*{b%DoDs2WV>TYkfNXl>ZvevSaFk-8k;1+m7H|UUC1G+%$h(^&q?L1 zaMYx0tpLmK4EM93PPxWUsh7ewqr!oTLLN@dOX-tVARYZLnxvw!zNpZd^O*~>Q4rEGBAW} zEF+La7vpt>>85Gj9WV!S7XsbI@ptTxh>x(~OQdL zG~06Wg(?6iWhVE%W-eX_*ILIc1Y1JLYtURYG(5!m zrfjMfp~L-E+r0|40uoiwam}*15!5K=YG-@@{Dq6@-OaVz58Kri198pcT@4TBk-<-v zT}(lfG)GmS4mtx5?XDw)fq|Ev#L7rL9S1;$029tg3hD211-o70ZqPP&IE)~Q21LSv zIuM`jjqjI1oKt<+@rfRZRLf!|Ci-wYPRZ!7P@K3ny+e1{?V@XCZ)EsFaOjkO@VvWk#+w*-NBWu0ZN5L26F7=v!iOF5$G?meR~4lQB0h*z zjjOV7h}@APqg2Trq*vy*9zFW%*I#`8`P?qrNAoaa-SN8g!NdZdu&+JXAD@_kq3PRS z&#XLIE@d!WrBGEH0F>2csh)2mG{#xYm!L(=%ghFNsAKY*z@~u{((v1wwDL@P6N@U& zFX7=1?qT1-?dE?>vZOz4lj;*n1!HHfenxIVMyFelbif_^aYa-8&H8awQQA7t%Cqma zL`0XNdfCZ4ZG4*rVUT>KdPqWe@ZM_kaDC(K%U9NKEthv51j3;z;GfL%h*ruYeOP?Z zhFhcTQ~n}*4EY3EvqA;?0i#0f%EXz-VTf(3SuTi)=v_d)p~qq9EXfI%B~x;KQ5{;R zIbjY>GmkKWQ-$3=<#Rkdj(G4@ogTmjG(TueDY6DMCPX#Tae}-#P7s;o`~u}DOkSHp zEIQB?%a7xc(X(>xlKdRrn&r*WqFNMUP~ux(5HV^v)}`C5Z?2K8tKLJ9LR|FunJwHZ zGa8ml(kVvSBP|(Ws!|Z;3KFX}B~56ol^DTU%ik_P ze4NQ2U3}y1>tBE6!6(03xc}_vpvbVgshN|s(`@go);s=@>Dj)%EH@3FU&P=7}AR4_rsV=~8CP1Q5O2or- zWGkISSf>*ZY}^%$#Hj1#k0@?QX*V^81{+C@Z5?9*qh*DlZLO`N(L72^b!&NKiU1=2 z)E=%fO#YgtX>u5|KfZFV%XL~Qbtq_HjMT^g*Hg=v8QY?7b1uu`x>60*K`L9Mg)vbL z!z{T~)pBiTeyO-~fW?F1kU~Uc0Btr)=pZ{yblx0B>O-(Y+38s09`XGOhj!fAA!Wir z=QuQ=69%4e2ws|Whm+nIzZ3Ms@`EPkklT`#3Wo2I{>s@^0H%X6+h#E<9}-b{NUDJ2 za8`$$jSJ=sq68)oYJiNjJaV;aKc~J{pOaE0i&z%IDTk#`ks#i57dqDp1syVmvEhKM zQ4eIUjugd~3xja5Jz@cHqL~OwK(_!$WfQ8ugyF`9PS?}9WCYEp<_RDV;K*Zn3dCZE z2V#slV?3E8vUU{NqSQgwVUlOxbaec(w{P0pOBFck4fnXc&`}VylG{5B>L05>O9!3I zLzRi4l5)hghFZaScB5z$t28S)NWsIU<*lzCefXa~emJ*%$f+f32ABek;7SDi(LgKM z^2a0Yk+H6!$tgbJ+T2QdGs_*b+LKYv9prMWMTr`_yqy^Aj4VLZ#s^L401jHHP79E_ z+eJkPST8YrH4NCknIy-8qkh&} z)l4s^$L+Yt=t}fLyoRNl$MrM!NQAMIs!3I;u3ViWF2IgIaRAW*P>m`fN@S@=Ij}0GWc~)sG9Z zRp+L0RhoT-ZIA?6_@D`dLc5J!S0oZcWJ+aaR>NxHHlZFSHK6H};i)W}cSd9)iLqGB+v*@&!k$xuOdS<4iK zRdpW)kYZ5F@{12HGEyBGGEovnYS1*l+GTmg>5a+7JI(vOn_WFaFK^!-ZniGc`MvNG1+;4-2^?ntTU`1{(F; z;r3oSTj=gePMta(iFZBt@{6PW9Ao6EFa}3Y?jG!+!H9O_)i<`Yx|B{Ir3OZTG50q1 z8O&QPS5BRoB_1%~FngGbM7sxu1{jFC#v~12==`z(%oYQq8KgBkP{Pv(oU;v&q1)ELsJQ#lWYWoc}Og* zfh9VUBHk z?&|uqJ|8nRGrbg5l$@zFHASP5c#i&LC)yntJmW(YnmX-CPIkBNs=x^OxWqwv zS^`2X@>%v|36!c^9cMNz#FVQFelD^`^G*Y6qK7TFm)lz3Us#&|=&N^s`Psr=HY2x6 zscTJM=bcRF<`og%3|q`ph~~ChEkXKy2cJMnp)r$t#jn5i_LxggVhyqR2%0 z)B$xOTmgtGY9c>YmmY&4Ts2gq&3ac50X%&OF#Aj-bp?DZM|1#qg=OjFo^G@4us zudjPRxhN8@g-f3kZgsOI4>OQed^<5xqy1zWrTuOe_tH|}uLjV|Vl86|t{xNK0LGH= z7wih-(#%4a&QtKFa%MgQvm8KSxx3JZ4Aijnmy`&0hgGamYFrR$FOidT2t#U96-u(h zF{UUkqZtlj=! zBW^-Up}u#R!7mF)f<-?(dFt%*H}Z$O+beT>+j~t{=*92+c~9JZ=l#E1SX^rRBH#K) zf0G*Me){`&@7=#QcIv{pmtX52Ke7Gjqes90^vR>;vDquH{@}-z?DA*-`JL^>Y`80R z>f&kMetmHTr441}^o8@9n!B}~K0NA44W7Pyq1h@gKX?QK)H^uP*Ap+7ONU2?bYk|8 zoFr9VdGN654PAfj+vV);?$%cRDD8>(E?>BoJJ?uTUMf~<;DM>35rDbf?d@nPb@k%x z*21G_j~2W8Qm0N&W3+a#wcW|K=!oIwL7MV-y`dm2GS$Na8)1sWP}%7MlK-ot0WiZ* z9-vuNQ&%-GaZn7>*&r)-3sRJZcBq>eO%Xx^vL%Bw7LAdIW|ropq7q<9)}&Xp)XcyY z2d=Rqz`~^wlS;Im#xwrKQJiY`J4~Md5{~Au4o$t5b;arbRBTxNA|zHOe2DE3Qv_W- zy}eDGC*2r*<)jJL@OB%sErF@Fu|_UPkfLgW8D^|Ab}@#y_6EZ9?8JAXx-LMK8@pUp zhdhQ*EAp>nv!`0#s;AmP#>D!y^uaU>TCwUqu$8Q)6j=rxVZy-o3N1Qe5AW zK??AQ8WnWln!3Tb5_mpLGIc#6{plRk63RI2AO#UmNBqCwEUV{52y(AhkJr4EfJKdZ1>@>!nv{;5@U^O<8RZ~zEMG<0Z z44m0|sbSM5PVkm`Br+Fv5_6JknLi0LMN@!^L~WYhcr>7%Ev*Fj4?ASlbVfj_^3eFa zRdz3@fL4bxaZ4k#J`|xfMS&emhnZCLR>SJh;9acHvHHx!BzkbTo6=s=3HEqXqk;Z& zzQGwvKyPBgofzmuQh1sR!nH{U)+4J>?yPf5z^xE8sfj!dgdylJ5+_K93@OObiI$?= zsO2jAJGqVJjR%i!{o><~KEA&tH!TYULL;=4c|vsf6G8a#kyP}#H?ItzKc6`&?5%BM zDc4}tX)Jj|6G?obm2?SjnXrovLT}vPj_A1UMC4kl#ctcdsCzEgso^9yGYZTh1%Xp$U?u0%*gSZjJd6X3@Ii2nxEpB^3y_}({#|5R?KbdAoZz7$V-6$(D z1pt`PS-_8yVBIUlN?n_Dox<)Ti7p1J!%?)njc7a=Kl|e0ozLXv;dhlBUJi$B)%$F2 z(}=21z&?-MZ3D7tP{r0keJy4~QCq1pFMu3}{f=z}!P47+GAyH=CN(3Yq*BFNh%4k^ z!W)xgNNOVi>4aFE;;nJWBzRiAcrU>QQr}^m&DQ2{0A+>-+VgixS;r$r`Wy5RrUcN9V_q!zlBCuZnz~Ae zakS?vG6j;_8)LZVxz@ z?l?{x(r@bDuH*>sg}zqJmi$~rbBw+J^LJ;NQVgwAYG4qUl<%7+TK}TU0FGM@ltOrvbHjp8tN_Y z?X|LsL}E4M1tEDnXk3IbkI>3idVP6RR}q96Em*=A@e~W1(XUL(Un2^0u#J7KFc_lU*uG5keFQWs+i` zjSyCvh1mae(vXeoH7tE+Gdtx19syP0{&ouwyi%?R5n1lk`Ui)ydxt9IAdszx2z(z{ zhLVUa5l?iP@tpW`&I*4pK-$Vnn0q6~m{1NKV5UX4Q4?fM@D}8%b}RfItcOOYCR}Z* zQ>p}K7`bp6)esInWDN{P$|~MSm|z^5VZD;#<(68_RWSYN~vCq3d0k*6STfEhQ_}^a%^`HrGi-lG-+ugX@!tzEUCR zOrW)z&+Jm=B}t-;yMI%xh6dSSNu#qBX#rWPG&v}x%(>V;yjFyvlComdk*!`v%T>yHB_iCzu)mhsrd0b9FG& zA*a}NAU>z`QED^vc!3QEq$fXlZT!L|oV+6uXiuLV>>ldQXDgfYYxP{ESYp{oCP^s9SmB`F*scg# zq-z>O8QPtZVg$Sb`ALjK8B-bxD|A{JEavInSXE}UI(Lo-pxXNQ5*#Fj8cP;;=ALG# zwTORN@;FK;J{)&jg`fEz@ZlXFbUk+y)|i`x#3sTC&4yb>vBxa6jTGgumvxZpOZx^B zfyw1dsu4MYNj@hkFQ3^<_7X_KrJZcMUGXP-P%}4cS$4Htf-M^tw2PyPrb{wVf{uGS z3ZTzqB?QPyys~wLlMeZo#*NVCM|I9kqejvj`}{NwJKlbbDWUPruGFSsD|wPig-A=F zMUbg`Xv5lw_TdN(fl!HUWEQM1AQSwe9fbhfSQG8}c`!1yyqnh~xuP6)fqhLjfnGr{ zJ}*u?IK8}915n&Y!h~u-DS%pxPD$rfNotyeWI)_HGOKJM&x|gzO{wYyslxKd859pC zs3r|`H|PzOecJd9q(!VsF^1}xq6-CYxmu%a4z2(*vI~$8PQApv=Ub_V8^xS@LFm*( zK0$;;B=s*|9@|eBFqxv66{PEPu;XA+i_m6Jfg=Kr??Y|fr9eD8GfB5pf_g9y8yHZ2n`F4tACv-lqrg^wn!sCMP|6bd=6@r;?YhKVQx6-~RYRN6GFT?S9_1YB{M5bH_h2!!5zEc;^V;p|C zy?l_~o4ItoS;}qB&F9kxCr^wO_O~&wIeF$%JQ!O1>anMeut}n$3c^|33$!x`001BW zNklkY)&HOh02K0R61x_XK&6shDB=^+0L@d3xojguxjRgTIWeciH4XDiB^ zg|NfzH37HH*)nOSq>;`yi}$nSOIy|2%Vhcl`RqvZjnV)-bT}A|x;I{5b5cz~sos1x zfYJEIWvPDB&Zi9s5P*D;?}B$`DPXuxQ&Au;`%1Xg@==CKCp3^qFepq!7GV9SrptvR zc6%TmYgF;ACW^F!(Fn(Jr9>#?{BxuzW*>$Fh0SVkDpBRa9NN@_^4fQFl7RKv8nlp^ z{U2LSaGmj=n!7Dn(b!G{rR2w~gWNO)%1H@D?e^Z<=Hc9&r`)8I$8>Xmhxm{fG$?5R zru{_<5v3e1yn~$vH~kihqUw-5*v1^HNmCu+ePwxNzRf*g*Jcc$Xh%c(VVAAIn^z4d&x$T(Ib;H*zUON9h2t}ZjNSy*i6_J_CGwxO;#Kpp>sv^EH+l z;9bbw#;R~RTgjo{c6+)ok@ve$QSju{CuvrbCYg3@dZZM#U85E^nm3$fWaXZ4Wb3E3 z4QoM6`_%N(ri&3`AfC-+!$w)ycUpny2&s6-Kg=mp!UYH@p4BF4tG+RjjtK;m$r(kb zS2Be-LJb2Z#PX_M6iyRjK3_v(R1~J1&N$XbJMsQA>BR+Cqv%QWv=Ar;SsPrQ%~p_g zphQn%&5060R448d7Ha$;D6x+qY{RB-nWajl%EWI` z=G-H81OboF8C2>8UdkYfs!Xi7B+@VboBj|YrzU+GfGu*8%-i9w&DcX?8KpsH&}(M? z^+FzdxniXWUBK`_8Ek^a|M}$+I?9;00c$e-{Pl@br}GEfYme_8F6{xkUjD{+dXj;A zH$Oi(I-I!v@}K_IPul6lo4@|$&eqoW%;g(zeW#k)y!Fe!JJ{H-R9e@+@$DDB^_~5N zuYUi|JI|hMM0@+5f8hlN2W~As&g^ebo;i2s{H3Ga&AqK%PS?T7vllL3*;#u0^wAtG zHWR0(W~L^VpFP;w*=G$54E3jXcXrp;J<;gM$OM1GZWMFviOI23QxkL(uB>ll4vvz& z@zK*~IsMn>A28P9^3|7i7Va-Rco^;p^~IB08_VHXa_aPzLUw<1ZYj5!j>vqRvqIQH zwbe=V#=V@{6(fWwKzn;Ny4)JMH1S;02_2s-atZVuW=;p7P4=m8aL`FS! z0&Ngwse8;ufy-o4#fS``k27v-6SP{!Ou5)5l);>UV)!irn>N*=gIU%57i7nXD?@~o zwd#7TlgX(D$rea|+Q1qB<*bKDBI<)WipGNBpex!HOGI{lVl4lScLb$=BHXvKt!=6Ow_QU=o6+ns9Y| z>p14zWS}lhyIh*ibX|{uT$;MeqL)}k4FJ}0C|gl8P30D`YvBouv*KU<3G8V=P1^zL z!afh)H1U)>Ht37?VGG-#d<3fFZ`Z!3qN#dF)?o~lAjIS2Ktd$mmwhhB8RVlTEmol` zoXu2{0;d!pSv)powV_eHI>&TE>W2++kSB&&cWU{LfL zPf;+MN*#l&_aonCbN&{MNMs{ZdaUBE}D3{Ur@`bs~z(z z7Sh+vx<;Rb&z9gb1IcB&msfAGksg+#Z znJ!|LuWCUvqfr%Tyda2)^p-L_lPNzhuBRz6nng@)KZ{t}uF}^!oEKegLA63z&B+(Y zr^2x)+e%O&ZkdL0NIw~9M5Zrgm!Fc*XduG&(StKbe$8_x&KXnyLF6ZXNuZ)pFe>#aKutQ!t7hRd1?$_3`c9g@ui+bn3+9pZw|fdV9O(zkL7B?>;G1Jm;=FH+y>W*}_vSut=g_ z{^k#1tR8;xX=X1=x97y!OVekjZ~yl1H#T--se!Xsu1yT|fBNn_yZeQaaV*tR%a0d} z^~%(ROH8p?SzON3E8J%YFgqP0!A1}-H5k|&1C7dWvGAWM@CGTVD5 ziI(rXL7>Xz1ccfdVQm09A&#+4!@_iID~J38~daXJR>ZTJxW>*2UIOUMnqc zm}XWPQjpN0_#%mge(CAygAgV&74x<7FTSit;bKz^M-!hd}b!eq3f3e;)=GZ6OogR8n z+D(+!1$u|Pje@IDiq-R-M$Vl`dIHn#c2WK@^|Evu@+maI21`TM(sFaMA>+U$I-u>r zQj=<6xOH8dwAkt6RTnT&ssJFVGXs#H7`-FV%pY zRv@0Kc)I&f&`s})#eLykyjbZ?Xb0m6>Ns@09)z-rN|bSmYF0!!VbCFcO^$1!(X450 z$`?-tyBu2>*6DAPI@EruHV@9NX1P_}iI#n}{k>j^||EZ@vpPd+V_-ia($-K0 z9<0tg`=vNSkZ|%v^SV47A%oIVcPkz6~ zz1laZ61MxJMnkg_62GTknsCeu)8WhaP*52Tq`|@3>{?IyZA;Mc$k+5DITo_cQ4A7y z1f;6{PY)XTiK`tSokp8j$J{fY^CY{AXge*LX04$02Z5F&Yrt2&*pD^c<zwsxYTKcQ^e!9DU)H5(TJv-Y>@BZ|?pZonW?6a{2nt$}9(GFaB z@m17x5AWVXA<>;=HvJ6kkh!Bedk0Ur*{fgg>5o7BzAK@t+c(qb#HZhWwkdo zT0G1*LW~9OD%Lu=?2bDWf8*_cl3RYLqMji&LYW)#xoZcqhewHoKs0ax>mrhlMzoe5 zuGKE)!^LJ2-Y;pCZ47NV>%!Lmh>ufAW4d0_HI6)@i1mhAo3&w~mWK*?VVlt!rP-P~ zQ&_u#6tv+ufzb_1I4=54o7GTR+H`;^#FMM&6FKM11T|5z;76H5YO)~BRe_qTLMIt< zMZoVZg2((wIJ~CD)29QL;pS87OjRh5!cPE@QKW!+Fg+w#@$Tr*IIfB% zEH1-7A2wb}Dix-Q}(AZHvSY4yg2d{bxh?b1h>A_nD@1UMQ}25Ssro;@fZ>_1*y zUAa4d`}6yo=`z{|C?G`ys~{XOT#31+Mb&LEfz(rppzF%>CkJOHiuqi%043T0-74e| zW1#vWE93ZT2Ksu|H}}v+a05uCr^Q&zEh zZ*{plz=a|CrlJuOVdRDoQ^%{~xHwiy_XpXwleDm9Woshe`D2D&ShdWjjFMLMg|=Va z4eBv+Fdh-_s{iN$FOVV)4PGVB1amRh7Does|>KjaptkC^p+q zHWnOH0g6s4Ir$4K;i961U2ZiL^|kFgbvkCKka?w_2pv26J*;4mK#$VZp`lcF%=6`M-z`<^LxU5iW+yi{7M?wNI(Fg0>#x0fu=a3kWfjd>??nI1 z=_&YwV*U`8X5#F{+2^j#fAz_ePwvIK`%Vm&dlHXP5Rk7?61ji;WiY;s8A8tU{HyGl2k@(m6WbY>h6YfHDFKqCf{p` zxym|`)!I**$fI;#2nqk_IFSj2I1ZEMQ>Y?R+eLi4QECD?O;cs8jCDRKpqVmN+G-6z zk=ozsl`#}ndJ7{0ExR<>$A&MPI#v1x8HECf5L5V`0&l$z7^_7R6rbG$QAnslOAt2eNbeQYE?yoB!`-I z2cl=^qll%^db z6mPl1OhiF#=BgJuzL?Bq0&(J^vTu8QdRTV;wOxb?rlxiAnB`2-F0mOL#!}U)Rg#2E zgVUM_bMez)Tgl#uqmXzYVzNS?Gr!xeijTeyDuZTKuvD|81DFm@H9qm3MH+FyOgU6= zzCvGDs}&`>(cz)7h_zM~85|;W>I%O%T!GB@$EXCQ-c&&FYtEpYXk(OpWT+%NvU-s` zoW~qxNIieD;{fUqK{j;I`l?gYFz6 zR`@69q?l>p4%DXHBC|-544x%lYFG2(^A3(OI3?a&eDKAimAy;`*@G-2BuRHv7+TO; z4?4c6E4b*0*&lI^pSwKPH!)PBpk}@x&z$qH+$xnSqJK3AQErpLk+ZD_du%*w?4kch z&Dw(UG`?CztA@f$W+h_Sd^QZ{9-IcLPJ5$2U$m0@_MQ@@|7Ce0kSGUO2!>p%WsFU!VTpia>#VW zTwx+JUNypIWqWLv1)-yKk@i|ge#R4~FGxL0Sm&UN(n8gaK=(rQ4@khpne0Wlu0)9x z0Bl#*zFyiw2z4N_pCk|FQIBu8IogwdXF}cxmb0XP@4`ceI_sQ0dA`HzudXzWCtR z`v;lsfu2h*z0Q5!Sa@={mxlA6x%Rx@RlEPmuQL04Oj*8k=?sdnrIoc>%YFLX1-wn> zo;^J*X1@88pCkf}r=R|^f)YDh9KU@1M1TM4+~e)-jcB50{OqO4$;u zl2FYYn-DmetV-vse7B9^QTzkw1fqp<dFP7B{&qt;13DbL%!0s1DJ~h@~F66pNKkH1dlI$F1mzv_}M&kq#Wi9VZ z7Q6;_!S4vW5zN3sWPEe~GLYNb|f3T zwgR;|#0wrIEaC=4j(7WaUoJm-I(y@}{@LlS;gN8vHxjRh2e0}@Pt;fLf_(DL4Td_^ zp*;ka0=GaQ7^7jW@{&53c*76y7IHUZYE^HQq=6&`ND^YLn#%8?743l4foji!>Kwy1 z+kIy91Ki4KPP+q>EvwuM-rX@uG#aB!iA`oo=C3s*-WAa)@1j@LC>wwVb9__2>j^~# z9w@~%PZ?`JQ|iXg#aez!rKo0oc|!C_saF66joiNils`aHQhyk-iQrUGTvM8q|J&3w zrnS*FqZ3hH{!$Lf@c$3e7~x;)_7S*5SF&g;n|UJM7CpQ*L*wsLt(Ee%{88~Jli%Cj zeX?}t^9O6|nWF4}_;{B0iD$$gKte<=-KK1+9greD2%9UT-uhqu^EaczDZGEnnXIq` zH;`I8cTmvNcDF+0`}9YP)H3VC7FpNM3EqG($_<{7B&i z*ypVVnYQGvlxiTgx{cf!^gB|7VU&uDokA5qc;O>96{qQ~N#@F!OsaFq!o|YlT~{gz z3?l(RK%{dU?UIny5SE|lO+79ed5GuL?o@9$J^&m@L24*S3sA*(pDD>r|G>;C+Sh@1 zAAI)y&i3B(-~Y!mS8gnQ{BIwA^eN*RU;OruCoi1YUA%wy_TAO>y|JOw=e~A>y7m44 z@c%HLsZ?&JQiHP>&sEF$-~Qqq*r3^KFI{};#q7?)<~-Jc`#qDVUwZZHJIjw2A3Vxs za$}>zu|RNreXl5KV)*3AiDDtMu=MQ2#p~aE?QaU{wR>NDRy``0J>fI2z7=ql?tJ-i zCY$RW9i6#wJrZbs{PTb34JBWD`zOn{e|NNd7*2I1{h_Vp^>BCi?DRPx%*FYq;i0it zzVoBp=Ho}7yj#c=2L}dQ>0%QYOw^w;M;!<}R~`s)*ti@YUZaghI1ni@&<-mWVdB<} zY?#04sTJx#*c`VAv5BG*VSwu@uWbIA{D1WgHl)I-YfMLD`65OPYOzs<=uDU8fK}w< z^mG>s2+73ETg7SWHq`?9apNyU<{M`z^&tC9W-fpcXtC^-vCHMiiT0+vWNV2y>mBgk z<-yjUg&Ai4OQ}%o4f)es2SFxohW(hi@a`Dzv9p-q!N*WnL=97E=zS_HcPWiWIf=86 zUW#Bm+0_$jy7tT4X=od+rdAb>L7uMUR|XFv3`yP)JQ9T!0-+3^3 z_1cLGrz6v|ld}WO-Ni;ZtrITL;Mp>{m$2LMF#Co9p_*&&_7M>^?9fz<#{Y=q;b`Dg zz*a(Rr%1j|m1>X2rA8=;K{@TXQq3NcB2nm<^f2}UQBxpd4kHrEPGKvCjxjuVTX|)< z${hOAl>-Rynu@ND#h$5#NLo!eWtU|jfK9OE2$YJ?TD!&a+o&c$&a*}~i)JP` zQg);nxI!T+okrQ+hQcX1DTdlDybNoUgi+X`-qv~pbUE?`HClx#kfo^YVY(3Rp+GVc zoLc#)n%OHJ9Oid+w-?u*-JM@#T6Z0BI&wj!6egY*NMUw?Th1yR2=euDsdg01=s zubzsH_Ot0Lc?=$fQZlbSyIY`Zf-qsD0ih>a;hCwq4}P1#ViHWk#c&wlV2LvwAjQr% zLkUC%Ti=AE6w@CtZkqI>|57+NnUTq0%;Q8t5*>&(P0)s5Qzw|I1vWg6{7ngu?kMS( z*)N)Z3tWaY8g?TUASI5|zP6nwP=^<>^`NLJJDX!Yj14z~m^lR#YxBYV$4jFH ztD~kdJ?w@1^*S!bk7?$H{XSufFic>fM{Gk2hke zWHcB^@9&>DGu<~hurfE7t(K;dqrLq6>c_uXxpzC8E1x)jZR|v9{^l)jCuz5YeS|qO z180kA>hyj-Ut<1iT8$yzRe>jGW8iLpY#x{)TgG*+o)ON2ZX6P z$$;Quapb~wY=WLhZ7~J13e%yox0}zr`{CB3rC0vP8!-O+_daOUi-8!GaF18pBL+t5 zqOVwH|0NC*3>c}aT){Zg+a<%a+T2l$C@6}nhPj@Q>9V5+^DRM<6PRsje0(3s_Z4S<-!p z+5{Uo^Dh)xO;2|0fUkvM0tCk_pUBRFfiLrDWilze09IAvQ8rjDyuRWE-vvubWd!nx znJvxgVCAS*sm6iTOZbBjS{N8jBOoZ8H&mzq^N?>!(oWAg*M_gBjmFaK7pg=yySyQI zQ5D-`2w?+SBjw{nLE*m@GtTqPN{VZhBjpuvJm>WVV( z4qGB?P5|^T;c&uRF}}jnz%!V}P?~-v{R~kus=dcX(HW^JnV2M@>cHY=h!$mG3Kk0&M4 zJaPTTxvST+dkdd`e5+7y`=gO7FJ2oS8(Lmi$YNSk%Tq3#x%krXSnuM^4|mqKql4o& zzVY3jvB5{5{{4e5?)LW#yz=dDMxudxAN*o_bB`r8IeCga=l zaA^_!fk>-W@iDO^K+;$DwGj<6up$;4OjWYENO#m9mH)a5J+H4=I*Nq5JB%Jo7km0g zwr;=Q;HkM7@*y_UW&>$(xXpVYVFeCoP-RjFA+9NAYJ(?6^M~nH1&9x_njs(2WM8$M zslwWtdK}CLs( zgQ-}5PXHV^n)FAy0^ul(3IaRoSZKb6JMXGxFdb+I`>B;V(44yJzx=Wy%g|YgYpi&S z@fh%L1s=rX&<;AMq_f;eKWWv1IkTCiHRnI+3;C0Itg8=!6f^?xs8Hd|K~z~_q0Y2H z&c~JSf&+b9nR9#x6l@U2nQ&BbP8H(u8EzACZLp$D(exa3^sAHrPT#d?)#I>oKp zBP?(%$wycDoeg`I3_(67a4Ota5rvgTIa9-eA;bMlr#H6u=awJcda`+_)k6m|c%3Pw z<>6+U6+;tkDT`MUF{3J0)tf}2yg%FkX^o$kIf!qXT&Vm6`)^a1Me*6TSmr}A0LQ;#>Rw1>Jj<0+J=$z{eo z6fAT~Zldlui-%@fCiM{ic{~EiAO^5dQY|8hkUcB-JC$~3b1jkRIo#joQ`WL27t$xJ zw#uzQjLcMWFOXrP10R(=9}jkDCKs{m@)qGU8K%8*=#)+Vs4MJOI%H8F_0;)se%!0;d~TP&jK$ZqiZ>c0)7_q0#L+iD*;lg z6g32hP`!*koKFrkq|U-{(%mdK8reiFNWF)f46|dcR}(0Q%h5!ZfmtlrmXYIrceRMgj+~n)qRl~4esqsKKDjFw>9_v zzh7J54Mw|O`iI{SceU1^-C3Sn$d&3Nr_Mb0>Z>&5t<68#T;8w-DbEYv{a&Dzzx)0# zvgyJ&!+GEO*20(XzWeV!DHSSXr_O%;2Y*@0Y^*$7dbYSReqv&5WNdqLwOneZMkmIm zPb}QM;|a!)-kcm8-``tbK`vc#^-WG(|Joak!uIWtJ}K=S&;>Ej9oSxZvbnb(jmN+L zgCD2Y=WpG6z}bKK`n7|tz3!pW*|Yuo2RqC6=ZDT-`rc3eyYkxoTR;05ql5ttp8Mvv zX%{)%T=3=#ZjeG7C$4rL+0dzJ&)_WM;sgHd-Vu|J1Yjv*=^SFim|8{JWaCaG#8*|P zJ^?LyD;dpB+X1;G0?|4%)D**YVWcVnwX0O{v1y$XPug^CK1YqMohFFoIESUbO>UEx z=9|N>TFulX#E)`bf#=f}Sg7VX|LkML#hSnBYDBxcYH2XTAP1i@Ul5th#*?~-*M|=c`P=#Z!#ZQ&APP{s*Sx!% zyAu~@Yqf>asu*9gI@CRklL=x!fr=En7kK%00O5lmEyk#coY!XnqvbB`?QF3oM;|7^P&S&A=H*%f({Dr`}#ATTUETBRm?Kx)~=L zB>rE8UUO^(U=UDgla-7nl%7v+-~=LB+A*en#pTENMlx)$yWYX%-230Vo7(e08mfCq*fHU7nvdj)LO@z?H8x) za}IYkfT(2GJu?$96-9<^YpZKfNfT>hB{Y@Ok4{q8t*?pjW>L}ve%z+ZL#E##5Ujx* z3oaIo%5f?%Q^U@^u9{5cOoL?&myUI>gdomT5Tzqz7&5Um?{6qFgdsFCPyjJ+2eqQf z-PFs()tB+oJ38o|=w}^9YW^y=N|BIs0ZeNo-!$(Bv|>OiaC0H(<)ESlTcXr*SBkY* zOf%bD_=710Kd4ScTut?>)GBQpN6_(bAQL$Kk+>Sdl+AH7+sPD)^&n;-9zfIZN3`V-^t{fGa+Fs1f4IUAHqL3T7tB47xTAFEoZ_8T{sp`x8h1WDoq=6lsRHn58R?m(5h7s+7y&$^M!-sq_KwmK zGKZX6WdO)29JnexSJxL-f`bz`e(?6j7r$M7a6g-?OkDcfnU}7%^M#FP50>uFm0jM; zuf8!lHn{%e_Wt1!#tyDrwrgVM#aCW=_SO5Dts@ef7r*x-ccc9A|NE~X4|)gtzwx8* z_7C>YfA!JggU8&0>#w{uJUsUJ^LIB6GSr-Yck#)+yJ$5AMout1Z*g&cV`~pTffwI= z>*Dh_*6)9Yiwz6r>WkOTT)Mosz5dl#w`V4&E}ouRzV+Gi>gtK7haGrie}LHW^MMUcv(V9Y^xfc$q{Byn2HNW=f=0F zlUK7_=`06-fle=z{{?!IU3~*U26RX>Cx}Z3Mj|ZOT<$>aYuTa;EL@Yw&`hZ3`+^@?;N@s{zj?NGalU8 zLK;F45TJy)7BCymw&00i9Gp#f<9RgDTP?bVn%P~fdk?n?vF=bT2@V{qw(>j#_Lp@< zYm&Q@m@DuL6;x}jgIh{bt7KhnyXenMA8!@4F|VYxwK68prFv`gt?$0RoPAPiv`dA9 z^udwu)>k8wCq`yY^-Q3q>G5}U;V>A8Cv`Fhnf5@pr34O4Wb4!pJNaOXyb}PEW<+X{ zKLkamFtzHyep$V0M=J>|LraVhv^>aSGv~i#CTcrT*Ah`)U2%epRd_l~k<2_DF^9dP zFaQO9LqV;?lxwTid75XU16TiMnZLmc()mcXBkn{$WUu7ZlqS|Ns!tp;&mFW3P*?g@ z>kCv=q{tdY4)`k8rnc~65>#p5vcWRGen zR|$=7sM-lc3q&w=y8BT}a*;uDROu!aZfR{u)2?4_GHyAl24DGXbCt+Z0T38X)Rx*C z6pT+pD+X|g@LoA0nA9AGA5H{`9FAke6fUq)p?~=QXX-tpA*}j z14xh{a*maA6|lei{Lv$~T0IN}pz3|!{}b+W?|sM&bzejhDzya6U(5j#C9^$gKK9rn zR3@bW1?t>YCRHl8Rh>oeUGKKq$3c&nL@-+#AI_q4zT5Bpcwu&> zd*}Xxr_bh7Q+F?Zu((=0_T+QNUwC%**4uCXmv1dDW!k!WjvYNSKQVIW!}Hlfb#ULl zQYLlx;qcb&TaKJMoz0}CMjudmwsv>F_Tr}*JiYt3|6zV?A=cV?^z^YrXIExn;r_kr zXHJ{}&%OR3q#p0llcx&#>QZLq*bATD*4c95&0l3p^%s8sUq+%$4?g(G%)JS}x7OW1 z;0cDVoPQhAdZamu_iSrRyM`zV6%-S&3Y#k>cm%#!v<0?1prWtnSCo~;&DRKyy zm2(6-UvWpY8JM}PPN`Z?ZNJmAXW%Nt=5o0lW@+FCXl(_>49p;kcgosOTL;fwG~R8( zJf?K)Dl1W6RZ%Ks3dl=DkdJpTse&*a4aG~m!hko#BD~!i_L)%WeDKGx`_f1f--T$b zCDxh5`7xIQs7U8bN6bf^d^mCX)6dpY*KXfXd#4GD#&3SOY5`z)qCK{`G#7jdZ0&j0 z`LC_v3|xe@*4~UL6;#y)xVL7R zfEG~Uf~~RkN@X>Z#u&mYpidvv!mcHIGkK(I7Vc(6kM&a;0`geM(b0|m9P7%m%;ucW zkOG%`+&@nLZi}OtG?DN~5&}6<<0u6x^f?$$x^P*;Xrno`PZe=!j>W9y>{@9xjT7(6 z)Xe0_%;dwF<>ewZHR=%X0;b2xpXLI>2UoWzgIrCr)lWKyY2|12xqLRDf08-;E6THd z2is1+_$cu^y|RLFWMmN;}}I(=Iw z+2qUS^Q9WrFu@264ocG^%hs`s36~M=GEVc>ld(u*+YTgc#nmNnM_^UV$p|XZ;6^+V zELVN0OkT4zg!Jov_?GaxfjVMlE-G3zO?h*OMeYK;aVgU{bv>F$2)mMsUL3GMb424h zdrYIyYCxPcT!L&ztA_v0Mj@Zc7mJ|=Mm1W(mR(KBS_18XofA|RUY6m=|5~faX;7ty zsN*)lZ*U)S)JDIjLK(!8aGaGz#h`ytRjj=p5cb5HUdLryp5Dy;a3nADMv*@TV{t+{ zcA{EyDD8(da>Q&1q){iF=*pMQW=(BWr4x4pmP*5&ut7M7Z0 zJ+FQB%gHT?Yd`wt^!tGM`b! zf5TC*(X=9&oBUa*6H0;>So;TbOpFAeSZC}I?mn%i=1KAsQGaMBja#Q>3X=XfyHnXR z;Sf>A>eU~Mx8RS$P>`}N7)N$ikz*1m4Yz{q5-KmOae1oo7=j*xGkd*Q=xJ$dN+Ew< zY(gU&4K*SE(dm?LB@%5iShZM*wI++%Tot3y0Fsi-MxJyKU=7IX>e_~uTuU`uFIE@s z&vor=@0eLxUapE73KxZbmJeI;1E~rSBkL1`@$pmq;A3dlgyy88yLgJZa%Xo}v0P?5 z3IhxIbiv1wbdyIk7{maeH8MA~YJSM-{9J}`!A_Njo+P)TQ?Xg7l6e;!H>Z(WOC#Hd z#O+uDyqW=TWu=f>8VcUJ+tt(Gv$waszdzKSjJ0-cInv|p+`c|N(p;RW`AL47vIyDs z=2UW4Y!wlZBQ9cr1QQ-S@Fj8+M}nH(@c>PbXR&xwS!SuqBsBK+jn`qnw$}*x{G_>b ziPgQ*fJXPvCr!=fZ|z0)=hp0PCP#a^$WFQ{qMH}YI~^+=t;UuAnI3V@Hb0vStop(1 z*F=Z~`>NNA>+GJv2QQy3q?H0Qi%W|Sr>BM|re;@jl=WENThg$ON$*%Y@60v9T3soH z471oI2^mdRG7ah56yO>#8BnDhJ=*ik>o2ihVea08a;At<((sk0N+y?HN%INNSx1S0 zkx-0B^!X=idmbDA)?bbe&$By-?~Sg3Sjik%?(=GcW#g$+r!AoRnOt8JBPk1-GC+<> zym1Os4wc##TN&o2mNdYc4cSInW2bT##TMn4(eCRk?@2PhN{;lOCc2kX2XG=p=F0-o=lumjB>9T-0N@aPl!xmB2 zXkM9}WS+?k9z&EyIj_&>0dhnkRfHU>8cZ}bc`i{1LSTynaS`hQd9-S`8JWD+X_A(! zo^XUQvFz9aZQD{yY5J-ZHlypMEwQ#(OcG)2l{FL5PtY4?_be4rT-yY7l!8>qLHwl5 zgoxS)8&DvA7@!0T5J3s5rH16zWKt{S%K*-B&^se>?a?ObGJTL12Ubr7gsZ_O8ipk$ zoUE{aAi9+q;vmtjMg2vn1tOD)FDQj z!l{*~m8TpKM-+duD3&!8^8`SmqY7&-SD0-tTy@OdfS_wZo=rg(F|qF(MX$zRbhK69 z)i-}ZIr#V&erMpwv2tea-bX*37#d$n<wm!J$k;>}i+DhTx_-rKH zLgy5Z#%3NoXy3Z+Ycy;#{A4uBpBLt=+NoMpPe7Q z|IzvPy0`W18rX5~?oA5BZM*tH;l%vRbST+&XkhRBoeMXvUODmd7yG-r?w$Yf{Da%< z?ASSYs8KFYjX$iH!MixdrPj`5Yghlo-K)jLoVJJH{)ZVe7f2%k6_&OUNmgpq65`Nt zD`0ZF1!L=WZiRpdnxJ`y!Ydb_~k}v|E@D#H`R`lcxmHUWH zwrk$dPO0s*ipzR^TTsBswBlXIf+=JfDM%?Q%fvNNk-b+I$(dqJ33f6 zBFq$(-qPIK>?{j5VKS5pX#{>UI)fRe-HOzgYa6Q(f#^uNn0+^@4Bn}lDkzpH-l*@u zgrkX2dmI`tcC=Mw4$RS@ib2=ZMl@$H$AJxw+`RkBAAgy}s~6tBTdrX*qb8ZyRMy^- zKtLt3(maT~wk$!>#L@uA*m7)IWu}omUAAVFrSZwBbh=t-cyaqDEEv;6Y#YtkuvFz>vj6}f07*naR38C_G76}4XY|f)in;m8 zthaTYyFNUwxR4f^Gt|$X?R)vRUT5X{{SPlTDn%whTEc|&4vxxsoVR>cD7Cbl!HvYa zKNoBISKoT8+T_ozt}IVaN721VCPPXSp^X8K*r--~3YnB63Q(zrrZ!d=(#I|x5paQX z%&Jw@k{C1sPd7KZm|%p|r$CBli~MpmGqYY@V6nvkOI~&_45rpOH3qo)NG5bz%wVf# zY%HQbb*~l&SQV`?2@E$@wdNoLk$cQHLS6%mSan?Kud9!{3>`0v^ot;&oEVyRDci^K@AX%6|;?E zV=|K5f%Rayw#u+95=ex@LCD+eeJEAxC>AIbVQH43aWyqs;}M)lgCPcsTwD6I5bK(n zBr0o^iv`=p(SYH?79(3BgZ|8iK2MTjuLSR&FnR;qvE7SarlZ3Hd#`mU@hH&P^$T<_L-@f zVB>W&3*D&vD@Hm47fSKC3XKhsO*NFN7nn(}uLsNmA66l!gD23|OoHtA3XP4kU-<3* zM-HUm#azEQH@#4gwEWKh`e)NOE?)cD2ZhC~5_xmzx!1l_EzV#6*^gE$<+Go9W801$ z_dfi^Xu;l=mf-*fzxzQIaud0}Q|rC8nQ?nq>E#e_e(wlceY z-{8R`hZm;qO)o6xi&cNJ_0unXYHj-APk(YbUnmn!@M+t*YxmV3e|z}e@SgoAP@@bF zU**or0uim5#fsz9DL<+}ld|55dRZWvmRge`fsN>F z)J2P@A4= z!}+l9KzV6-`SOMAhNlHH*p0&vPP$mDj>nUJoC<_;^cc56V)7?Z3= zAtxN*K2C|6d!04w2(=DXG1>{unHZ!MdoNBz~2y5lM+g2U@J-F=&t6) zF}-+=+~9~IV}0heq4iU_&=8y7gtvT2w;f;oC*3! zCJu~in=#9Prb(_SR|~9;;B4Uqx}MFll7o0I%Dt~S8Vdj#$#XNuPWIVi3FTfT83{8o zEZ~b!CT&A8qj!^#3b`RyFPTgz1hEw)%7cwVH>C!ZiCl0wt#ES!S{y+I02;!0O$G%T z5J8@Rtzh;=~jivMnX>w=skZV5*b`{jw(ru z7FoPzk3V0|^8m;ri`An*r5r_+c3im7MpaaIl_9D;Rb(RvV1k9hk~Y?s+?fzdrJK1^ zRHY>0(WZRTdMsHgNae&Ah^i_E*+v5#)%W5b|9Ldfc=+!3r*GeyT&Xzr{%Wuz5-3>K-TYCEW|h0!&{Dxzg~Ur%ajITC5jL)OCO!5dzfnkA%0+v06ow~XEx!gozZ zIV$y8$;b@ zRSHy?+pC9zxWv&a!*~;`lloH(G3Y<>P%Iw&$)A0l^Xl@(gz{C70f11Ari`Qv{B-7!5oF?{tZb3waUbe3B5Ed8F^KHHFIBx*V& z(KSuizzuSlNg5|_fOu<|y5q9iqoJl;>5mprbS2w;CZ4NpuMq4buR}-Ca@<#yEqp07 zeMoR-4zC{D;2Onx2Z^ilQq7>oTNjK1m(!-@%|d=0^68KZWlBTy`1V?%X9a{3rJhH9 zV@t+p5Hvu6zsMQj#xml{mn-3DC?;hc^MiuiqeSm2HP?e`Zg_6|SnDA!jDb8xViBSf z?*|qkmSHsp-jtI1z(IHzyur3)lo=#!m|U4@oKIQ`K?*`aqfp1pt{6QYnAchsUko8% z_-D#rZ=;Oe47>t^UIYrMQkjfs%*R27`zU z#8N}8ZdKO-^&L(vCg2FYn#s*vifXaOf-2Hoy1;JrB7uVE6TCQr=ePm_(`n4)n25r@ z;d7wK(pnB>ykLk`900q_?rfSOFH@L$Trr*>mP??VL@O=5G_j?z9*3aJxu$)_^^sC*S?(`$T^n6 z2ZE2j_*{V%)S1=!g@r=R_rmL6S9bm*2Yi{`(zU_awVo zR~9B|i~IKOjfTCmqhpD#?qpl{^2F5G)ct)Yo?wURy$kPFi3V zYJhSbj{ggl5sM;*E%#wi{awBOpLseaNQifVr=IvxPKXe3|89H`r$=mMsvTTFzv z6qi?mfp}(NF%pfW78h}Ls5Avwv_%%tVGaheOUuE59b0?4?p~TE*s_CCb`iAWNIM*r z+!su*tui1?ucc+63Q1X06HGgNg_Tk|J<`8#U~Y1jP84<)p`hGUAS=G}y?=P|pMQS; zne^)7c(u>g<^?fgOi|f z(`kW+_^Fo&oYT{7EIdb-QJhmVMK7fP=o(78AW@-I>UyxnJ+ct9wz$UT4R~s%F`yg8 zCAdmfYi;idCj6yBE)82I3mjamV~}=tl9Sfao{YD}5m74uXiY znLR*ojKS_`C&ZUv`oLxc&c12J3{7+$zl^ih_JL)X)PdiM!=VR7mqNd^DWc;mnkZF# z^-T?i(@(lg&)~#c^MZ?ONt^q7QlR|{-o*S59~q($sCkJK$9~*eK3~peOX+fUX>oOa zX<>RXyINqy8!SHgLn<CUO=cAiv>ve0zv zFY30{aJ{L#-oGRA)GMb#Es^=j#kILbjTDtr#f;WxLuOj6Wzv;wrot#E41^gB<(IPm z^8fjVoG7MEV`KMMrq-BQ2w&q7Bo|7RCc3uND>^1Tt~^daVFX9hqU#@%w?NsDKwEEQ zlZ33Al^4sB!C8H&QgNNF`>il!y9xF^^dBwk6<0++%?P6UR!08R$rUK-7KmwPXgnIr zr8aw1c4G`Fl7~udH#KyKw*+?%>*i4vTEc_ROpl_5QCt8yw|sws&9!{RhTUTLm|(6c z3FVRcTc&lwms17|#eAeyvR~Q5 zk{utrU|Gn@T`iXjLH1Cft|Rq&*e4^t1B?x{fZ=GArEDZk5)-Kji+-;kQKK;+w2M)N zf;{@va0%|B+pgkwjb<>47i^+b)VU^VBDjG-pl7<752XDUlw9Hn>IPuB7i8i}u?8K6 zJI6F6;>S)#+h8EWBItsYiu67Lg(i&IZ!-^=bk? zk4-GnBH;&gP?#T9Mj{kYKaOKmrDUg;k3$+JG>Q8|!{-~las9!~pnpP8Uk4STx?%H$bnc2v@08Q>(~zcb`1R<-U9ULaeL%%+oK<4PVI= znvOpE`bU5J*VxEdK`!Y7JxU;?k_oU=>yT=mXgn$&omlY-&h=%2w1w=;t9d)Y zSOoso!q`x1Zk{6!SgC`pCIlb{*#@mHulD9L%mAnZwIqxfM8>3KfK!!BwmiHyv2*vH zxv5E6k>HJ5#dzDBx<5X7cW(Qs!w2RTubdwTW-{vy;|=mpi9HE2B}Xr0NR(wMN8*!I3=nODjR#zKwBwfQQM$E?_MK7^{SnL!W{oVe1YWp2=> zi!W@^Cbz;0J^_SuCFF4;7jDGYhp6qq>%*v4L3{&WxytiJUNE^qflb$H1>r5JeZ@ zD-T7zT22?L=^V{{W-)_Q9fn?}gfIe*0O1NyNlynbxKX^sf94;hJd<#$cQtbwp7Bkm zRe9>#D=pzTwyO|MlhA`y0Gpv8p$nADCC(KGyAGe-iK=vEF;fJPH|2|Ft*B_Y+1egn zE2Il+b14u@ITv|6UFH0d|N9%EJ-bxK*BhD9iv`@?VGAmkNj55i3B(P8w3{txR%q*0 zZEPykM)^vWgdSS0%n%nwpo@KhY#St;r%(|o#)*LCX(G(hJf1caB;T$PibN_5Y~WpJ zG-zrUV-M&~+5dCV>DhRcsD%|&-L?*6$QT5qud=D711B6bsxIF+u+n*2bkLJ#R;eQS z^JOH1P%ON`27{gii>Q!cXw?~o;(8$&3$bm2d&u3U0wj%cv&0&~CI*IRlaIg9x3f8C zOm+bBe61oX_J(ANR;k8uTZ<&91qk(IK^$Qs34HhRn6C)WrTX6hGe`2vRA{|`d#NdQ zNa+-(kQ24ToVlb{7jrmel&Y1&z|KMZ@~RL@QA8-_$-Ei|nMysEPb6xL8G&}n)bAue z-AWle!v{iR#*{=5zFe^a@P!#z3t^BVRLgsz#PEZeLbPw~(5`t^La;qKC@?=6Nou`^ zR;XjhAf`%mR+@W>;NKNz}7 zOxk<&k=`BKhp%6t%G+`9;M&6Uy@%uBj?M!Ij?O$B&Mhy(cSu*p(3{x0-RsSkmNJEk>?$~N2+WlXP0YXp4yYK8#qqY@oc=gjz1&A7 zsE`|i9QJe?LV=J7;--tXc42eJy-SmvXQp}f)v7%O;Aky@NDlOWO1_LH!#Fhp$CD~5 z5&RgEEk>0RGrpY4R10|=iKU{=*GpM;4clH$ojD9fN&8ywip6^XMR{~1n@-b3Y8NBE zh>YI@;VLv0Kun;ts?5gqqX~s<`Qq8u>2Bl!HSqQD1!gBEjj$r?30hUiU@%v&eE+9! z{M$d-^VqS4+06KO-k9gsVrU9NYtDKLZNz|n6d|R{i9_^di>b!8q{Nmn0Y?J7lJNu6 zOFhl;jfjvB&O8H9**I(EhAz9Us<4W}E|_`~XSijRhmxy)Z7g<8bnPeDkloNO1$r%1 z<&yAiG1UMPXjg$ep}ki&n#(j(40*b0WJ92@QtxhpczJStb+k-D3XYW zqp?`BB@l~xLQHbvFtPD5q@NImjzE{uDBIV>p`l5|trozC+*qe2l^?4e_37^pGtlD-u548YR$Q$>wx<;PL0>V{Oo_syQWdaI<Rw zs^w}-4Br#1MuX4<%DNE)XVY0Q&q9OCRRihnXf)>}ygF_Sm9}H-OtyKmlm%6V-N1{IPgd;5}X;mO)Yxr@y9kcvX|cZS$F@Tw)P0#p{Z1M z@A1>?werN!%|d1&7K*g@GJ5nb&n}SCs@aq`5P@7K*VhwhMV8p#*4Xtz00;qA=XzEz@#K%g+U>vGFqaN42-nMLe7=EWsRW4 zhDFAxa#!BmD#g_f&;r4|f?nH1sl+o{T|fVW^G|*4g`>}|t$p`{RF+F%J3&Po^U$-> z)hfwS@zM?W@pFRm46lfPI2{Th4q<=*7aDMXwpgDG-)6BIzEg^qGvg0d5l?zoaC^04M%XR4aHhQY{aEZ1(=Dowfdu|_9KGc87z9E z%;=?m!u_0YsadIcq3IgXzL-%qrLwj!a%)NgX|+gA7&C7UwkNY+z#>!<*~|*umr9|Q z=ZvK@xeD$9$Ul`S#P(c=up+9S{EX!7VmO=A8kkbk^+~Qc&?mEBxqxn+w4QeN3LA4< zL+UgEwiLxUV#W<65vpB;>x>y<`Q(X#Eq(EPG2cXCZR1O&4Gb?Sv;DFoQI;!L7baJ7 znHpU*?pShJE|q@whreoyVB)2pT9{tBd+DP3Icq;djH0Mebe)v*Y#Eu!bWKDmah`J> zR2S|(g+7ga7OP6NuukJm0gdK>W{Az^=jVSpmDJfrjnNm2V_9NAF0DF0NM}}7Gx~9= zSM-lu*+?8*NkXi7)7T{wBw<5hDom!cmI5L2o?+PnQFLH%6o!If<7kt<#L*i${dZf=Y=9&eIn>I8iZJ!PLOU^ zMXIo=AZrF_b>g8Af~s7RDnZ*7>gc%h#ipwEJ^)%U_%G821ZzCBI?Mphg1hBG8;OLX zLGHMR*&9zKY2EfYV5WnYRDHQvh{_!sa*mMTJfbcZ%+of2SpjEzn)4YZ{So9+AMo;p@ZQp2nH4mV_U<$6os@( z@(oJ`8*lZ^3|%eGO|LF4kg?8u_Dh5N2S$GVi=mOBt-Yte@GqY8HJ66of2Xomq&YkJ z%!}px(&aaQSt*fu!@KtE4z+guKN^Z6L@O540|$#^W!4Zt>yA>6tl15uF`9t=(JUCl=Ewx_y9@mbS#slV_rdBar!>}aK-%3u)S90}Ywzah-7;k}VfI(qqZW?OV zM%3Td-j&5ql^|NGM{bH0egFU<07*naRPcpC$}Wd^58EXG40w0^xYX)uBpRH&cm>47 zX8XLu3K@}683+tNj$&F?MudMPuVDDB&#izYz|BGVL(OFmt9W=)aRJU%as@7hpeSGS zz4xyieCo`OVPje}RZ>|6=@2TcI1Gkc= zDU0GbD~wM0O&t^$idtSv?>OBW%ZBI}<6Q{|-)sy31yGfOUQ0SrFayFa{i2>SXzKLE zG{kfO_()+(0FOZELIn{zHGSBxrSR z-9}?pW39i||5|pj4GcoItu3{F%Fs5gkh0IMRo0ehcoo?LMA(#@)$Vo=qYw0%fk>1| zGoQqmF1%xUK>8DU4!yTpsXjNen$587Qn$*e)9aDmc*kSUyGn)BF+1^K@&UjD=3os@ zV34&1GSyVc$Us5&NyJd86eN$37D1|94Jb@E0-qn$Pz$j155OZiSb(Gri#2O>b&yN- z#$jQt3Un>yD*lp56|o)w+pdtDp-oRHBMEowSv5;8wtUo< zDf36Nq0#IE0I3SEQ#p|EuSKxy++A*!f(B@xwVQ+`kO($5R&rQ_dLU{QN?D#zV5({i zl8XEU0KjGqzvq3K}TbTg$$J% zABuGlBw-Jd`BE`c;`$@Cmn|$Ykh+~b*@WklmQG_~6OS>=WO8Yr7vT!iOZ@MVL!f^z zVxq^FY`&6?u_i}s4YktZ&9OH^Ytz~OgPk`k zq-p?XK$ySrNQk!#SjHj65{R%vYNiAM$EFmgu%u)K0j06Q;*LWI#LB~ui2!5xaJ1Do zH*_bLEqWt~7k~Q;r0vT;{+p?()Ug-7IB;lhEx$Z=?W6I#4-)+cAA9ntdl%nconB;p zXe`(gi-)tR?C9u4^6|j~`yo56rdFfvZO5K@IlVZ&wy>66URqjAo%-|_dwaWv@O&A5 zSV-3nojf`)*groxx;(QG*}CK06y`?>sKNj z+g>{R*qtly0%~)pw(Q!67ig(ePtDEMDmk`!cJ%Mucl^}i^nG?Mb@psahCFZn&HuaO z#M%A(j!fTve|dhPcmJ7zXyn?Zci1H!(sBtAcR4)VV2uN5Sx^Qz!lNQLzQ5r}TMOzs z+hz&^qm^dD63yyNg{Nrl8nSD;hZ&k#CE3xl1>-JQs}v-jD{PpWLSM#=2(MT|qqZ)n zU{vL);kgm&`EuCEk$9w1%;9Y<@^?6#M`x;iTNuf95V-5rf+r9vW^ysQK-z#(MSIIo z!sJ;V9jqXg^LX03yCKcyG8t^}lpoeBL3$Hdglo%q0?RCilM`bY#{rda!@`yfZcKVC zDYiK@il0KSOn1i|OrV`y!+nK90RWJ-dzw-K`ZIh8`~UG9-~6qA^T)f+?8EnU_|6hp z-w4u*r$+GS8fht*DrL)e%oySt#mk#m{kd=|mDs8Z<<3J{2&hQyNdZnrP{Y)YU!qYY zw<_64B_;&VU3b;1o9vOzuErz?277_ax{I&FrMhEt%BAWy)Ct99b#%MDhm5!_p^o{#rRh7p6CXbN-mi*_>WaRVGk0A>F|s z89S*)vFwlfpiLn3Mdxbkt7W#S*n)5l)h<{%r2(68LtTxj#aMF%Gj5nPoY4YH>epFR zNN+46r-qkxPsERmZyhTx(hGnQO%f1~9x|#`Z1by4p@^-qg*#4}48#t&AYmW6A#BLj zIfqdnqEwO%yS3B{fJSjYNqbpy-?l2&xQ$Dgn$d{Tjz|YfQ%`)APCw($_%&4u?^4+QA!{}K=popU)R>X zd!PLL>!p>4H{X6cQ}I6e+Lsau|KjlFhgUyZo}b@w;@N$t&MZ$3r{{U#^WAL;@X6Kr zS)k{(?E`0?c!Bzbp>E&dV@Dr3eE0m%W@cArC&n4eKJoh3qrt`p|M+)PBeNuhljk1m z+|o5Zbc;ckq$riTfKB?!K)|k#DCc@v)XDkd4Tk#QD?9w?$sD@*@>lM^pzlOV_!f z%5{!zplBd+4HRttXb8F#8Iug@i$}w)NhV96cED_Yu!3TdJtLJ|8ogj?ElbT+#(W;2 zgrHmZU+_Heg+q1@^!zov-8QG`xO%fCd*u{A;JoivjoIjQQF zd9sCYMNNrgRX}J+j_bTzpQ3p`Bxip(nqAB!I$IOlyC^D4Y`RPpDfiSl=_6`RE>9#u zTV_3{liy6kr`eb$W}5y9WU?$`D+M&cLbN2@Sch1Rx(K7L11($7hq!K0A3)B-@WAAPv|8HShWxZt!%Q4E-M)WkxG6M#6Pql2w;+au_ZQ`r= z=kB)Nm7`edvC<#fILIj-rD;I?i{;_QIz2sq&#F!kEv_#XmS{Y9XkuB zrv7UwRA1$uaNf4#>MGBx>p@M46TGHY&CTs$Uw5bf)h|80{or<4vp3c$_RWQJ(?^Nh zEz?I`T?l&2F4EN;3~jt5)xq*muU9L_p5A-pYrj_y4S=`H9W__{;9LK3{MIm29G>H- z$KM*WM@P*bn0k04BWyjcPz+`HUNEV~BF)wZVeX zU{fBp0VNLo0VI0HrBKz(9zK_6ZlHU~TuIwz5E@eyzz>&ofOhSICHuX+$FLr9(qr9M z$QHygDY9=;JH9A{3;8l@k%Yq7W(%$cBStN!R!yb~SIH{CGT=CM4u*dMVztXghn-nc zxljQ23_xWn$V5UoTEj!5%TeOHFf5IuvecSM=wt;X7J#B;K{^lCscBSGt%A5F5=oUj zC?JJbfjCgJ${Um0EDTTK#pC_I{@ovJZHwRe=$)ZUA4mH3KL6D>)-$YlxV$(sQ}ZMq zfBm1ebVU~KULCr4kpq-~DOxPg&&~no^d2}eu%rL(t&1Ds_G6Df?W?3d`tc8E=h8E? z3nyRv)aj?6o4a@Q`uX?ijkoXIb@cQTeCF}Hw^I4)@n>JE<>qHbhMOXZXFm6Z!t(sc z?T@z|JhOY>p3A@Z?)c=&p|ej{(o?e|!-;r%AV{`vF5-xf1JKs3IO|bO>^k#UvaM_Q z!g(r}qvu|l8@sc%wmSI8V+#+jrk3Y|iI%~mC+9})jgQ_x{@Jhh9~ijv^Pe@9F=8(v zDOYeb%r3_D)9OK8TcL97y#&G9I{-bQG&9YZRRU2H7g^2%0j_ufa%N*0n=RlC$ggoV(}LW1kYNJl4^JeR13NGy{s?0{ zec_0#PFT!h(EK<)X5zQ@A5aC6YmW07d2BJ^MT7+}V+)q|eOr&_^G3LikW@BUM z#+|m#?(O@w``I2d3(r@yH?wIJJwv2KIzI#L45`y&$4tWf=L8lJ44B+IfXl&F%Fiko z)hx3a+l;;3rU!n$lo4joqo6Hl)#9AH;x76)^1D8SyAh5wZ~Ri1at$Q2+FvlIS>?HQ z!*ZLYf7kgr*jvBVB~m|NdDoKBZwyu}ma1aX-)?e;?V(W*r5}X$5Xsz1I!6TpB|+c8 zve{<3vQ%V=Nw?1iYz~+-6C(FG$;rVzG4^9D7wSEl(7`T+eJ<;U-1oJv!BV{*(oduY z_E-8z3dr__nyTteJ0C&24r+%CuGY680MhJ$QN$Q=dz(93{C(TQhffc7?CcN6+81YM zjVPhfuo1=|6lexn--io`<{n`s~c) zwV(cQ=HWa(mr%9vLQLSo0!+f7OpvHs#9>gnJx79Ih?kCvY;dtQL|aDNyatxvjK*m% zHj5g8a@GTzqQfkL9lWW7s7vXFi<_!d>ww+!Z;Tdo#r7F_Cz}21tK?t0$%9s^GZ@u9 zTAQ7vvtJOoRiqy2ORa1%`#%aqtdS7&0Atz$MaVQXRE>}4y~8rbJ1zy;lh=cnE-vc=;a81@XSSw1j~|lK+)=BLJRVzH!DwECHf}dEBHk=56jMVlpdu0u zj7!i6qJt8JVgqkWU}xSgw?N`bYG3$Yx-eqRI3p;&l8?*~Vf^E|fe$!>|zx$o1KYr(};o*l*eeF;89@)uUZS2lazL4E_ z^3`3ZPqERIEI2hb*`Da&ji++iay}OhcMKfdUtU`nd3e8N;NZEZpB(<^XCMFi!(72T zvz+!yOHm(+Z+j{y2`X1i9HhF&}**|#X+~c$NFHep<*t-At;J(3| zAO313l|S|DONG^$haX?fRyQ6wb9QBJobnm*s&)+vpn-) z}~a;24pWttO<`KK4-R!-r*7IImq;i=C6a5l{*Lo>KeI~(>7QX1DN%2MCo z!vJs7tC_7kcXSVI^)!_hCuX_C0!8#hOF1rtjsH_jkX6jVD)V4nHrc}wRWVMXuWL`W6ye+P;BC?o~Duk3KHAWI2 zl@DfJu^Kt+Pizf^LpJSS`$$|l`zk#)((M_+?VV=6|XbMRTT*hc*_297VRVJuePo;bXY%i#ltcOzmYEXu(eNC-*VU2F8+~ z`bb>~p&Q1+ZQyZZ51FVdn@!;-t)XZ>?!rs(AfBkMC+AZfR71b*VcJZv0?Y-Q>#!Ia zx>>kdFrBPbW9;${2MJ@G?NYI%t7l3hwyVMaBx1NnPYo$dG>C^btERSdM+!C^ zQGr%~+(TsGWm@W@+EgZ@6!;9n) z|MJb$>_Vi;14}KFDY0X9$0Miuckfyp8yTNnI`#7Bd&2AQfA5=xRC#SJ3t{%N|HJz|mve)LWM>WYhW1ZT-BbiR%{$?2o?k#cFEl=A|ngP-~7IyM67W z<)y{0{#}E6_e_o4!&nT1=hkF5R6|%b(Uwk{<8&%LaOh|v8M^Vo`Fu9B@Awn+!mH`E z!;d~)Ddnf{4{dmZ-P?8+i@Bw#@mSxk15dozR88G{>qnEfZ|4?MY+@C8jBSqUiCNl3 z-f3nlFG0;82@`Gu@mP3Z5ChLDWUXqBjuew1W6JOx$TKS*kKs3{l_(YVm7DBA{T-7J z8@Mafm~Ss&nyea?H!aM%Q@}sWJfy0&A&}xDTxY%oO(7?S1O_x;sUkFn`))oGY==s= zR&uF2EaJ74tc_?=QOw|qRFZ>|b|D$T(K4$UI$Lyff!|y%AOuC&3rFT#SXhaK;&h9U zP^9M-Yg*}{C9 z;hdj$WedHveb5ZPHLp{zq-Z1DQok_x)T(>?e=}fl4Y6Kn0rHc7lt3)qy7cgy&dj=D zOD}HKkglULmU533G7h1&jAbh-m3Gus*n$B)29}XsHnm31BBD!gRyJ|P_0uFH-F$b- zcS2>AONGJOUJ{2`>dhFqsv1YDW^5d)KewTlg*%d57=rY-mfnA>=8RugVgAK1I)=ode;k|}q$@7O3zg?o-c4SWCl zfBp6k-;v7@6_ID7HR#0%k~d&Ts8Wj%Q}NFXba9^mbWx$0fmRjI6nB@c5ffl;|5zJu zch?RlH%UWdt3M3?q`+c!Vnh~OXQ|>ayuj7K z`b$fhu48qh5dpM@t1JHq%~w;VZ_XfGiA6QGi751JE7=jiAwVkN!>Os(F^BcGc2H=b zA!Xu6cEM4UsKGK^z>FY8vxm;lwhuwK1Eya0=JU9Q!q*K1BEcG4uy{KS0t?>M$U_*= zT5C%NrAgGLzID7JG^=OvQd1L(3$xhui3>ybuJpn;1}zQnmX!5voLnmDVYIr=YC5xs zMB^pVxm}5A4immM2F>lRb24ZS@ibN@m-hBl{nHZ1~doxs{a@FTL2e zrTv5Ve&vt1?mu`uw=#t*jfBb}Z+q7kT=5v%`U0eSUmzOp*}ZpdX=#3Rq^-Me+m1bB z!&ic_*1>%TCWo#qPdvaitF^6Ta`Zm>#l4Te)PL|qE;T>=?k@|?(MO+oe)zrjy`ChJ zP#!O4F}|iit-0FS)k-ZOGj*>w(%0X1_(-IyD-`!ui>s{3O3f^5kDD>`WjCu$Cp7yO zoHM~UW3!1yKm$R=>+WD24NF(94KiqPm}mq zt4JC0(c=eOTA2hCR~8tU(}Jl$D&RsTaRvnu&mnj|??lz8K|nkp>JF_~wvcH-4WgJH zKjd-#SVH#$b>MccD8=Saopv_;JfDik&Ak^gw0(b}E_BhfYx;*s93F(@|t6ssDlX%uTXpm{;Mb?go*wEwl} zWZi)k?yj-2Kd~(6E}G>x>*DQCT|P6iup7$R*;j$FX-;e3$q-4)rl!tzGf1}B$kl)c zhF=JbQAx7-9bCe;E>ns!0nS#mm?p&WSlr-KC5^NYN{Y&*F0JcY+K;n0D}2G;hH>+4 z?~jZ8o1bE{E-+6on`x^(+JxuyYHpu{uGL|?+T854$!t8q)Mz2jOr*Ih=^H$-W$($o ztvwwA12&iXTB~a)qb?LB8Fic~7S_wkgVH(I*RvTYg+xMmN`lTYX59YR@tKvCQZZL7 ztVVqGNbkYv2Uk7InRkBrRzl+Z<^)s*y_mKJuBLV_7?7x~yo5}Zp7Nft(?!I!T}zvd zn&mewVpO-|rj|;L>O8F4)2p3zfGAy-#iU47m*{#pfghV5l};UKN^)Ic8)n%5@TtU< z5l2^JLA6QNg&S>0cg*2NdPcRC%(Avi3)Vr26CkT2WHHK;TAu(g+MEN3_2DYtcrF>q#fqU3Mw2*ZXxWvp<_)%y=7Q%^+V01>KO9-*+L&}b|wlN^D z!CGR0AadV&}2L;%SGavVj`|S|NhTD zd*l4Oi%Y3mbL8A>pYQ4Kp1OHuerjrIYQ8-Zfy9Qe0&H^EiBtPeA6}Upx_$L>th4XM zFTVcp%9~^NCxOMpIQ_~W^lfdu@UtI`-5!PVx_j@zw(hN~^K%PJi`#bWL>DF=e;r{m59i?zVn}v#oR@MU*gA7czve+286l`j)as z7Jr9Fi4zNe5RL(K5#Ran>@BRY&`U)u5u7mt+H_{u>nkhiBPWjfU1>BQR8faCk_NxiC zVJ4-VZO75UU3Mol^3?p#B}33~?FVql!z^a(uaq+^h;F7v7p%Z>#YA8F6Rg*Y5*iwQ~&Cx*p$Nl)x=p2Tdwmo>XmT> zZD?rdpnU`f;_8Vs%Be;lt^!vXV1p}rSY7|vGHaWs!i!R zaH=IO9pw9S*5Yl^(AMtxVl|t|5X;)TdYK?+BH1eqD@T+enC4T zq7?K|*;%R?wnyA1^+r`wU*X5jM1(a{wX2TOFi?5OE~BoTbR#@5Dn_Zgv_Hz6ezhe* zG_Jx?Cpg4Y^uE@)O&LXo-_= z!tql=RgFmuk_my)tyIAZDADo|O1xq;*oq+nY&d=jS%`Ozr5^l7$RttLNF(dwgJ`|I z562hD0LlpASY;~r2DUrpOPEV%B&CZ+w6>vE4VrX?zLyf#04n$+&2j!V0INXiI!;*; z@;Ng&Xpd3mBy^A@rXZy@iy-he1DytSfShU`8?0)gQRNS)ap(%L1lf>;wxxwXG+d{F zO$U@H*ObfNs;SZ5(gM!JDoQ90^dK5|_z1$ZAE8pAK#&48fVcoU#UoCPGTefD$73kW zg&>6NfgujQKVmoreDRi8YfB5t5o&zKfT36rnnIzvj=2oyR#5+X70G`*8W$<8R!h)V zm@{y$g3t-{gs|KMV6RJGEifJ98bh=PQ}#ZWtS&C(~LqMW=1D66!`7z z$!W#{X022|^U5pjJuMSAF5SF-=lBbs-~Y(|5C8F7rA*Vx^5WhTC(pg|ne_CnUw!Y} z#Z0}UH8HSvkVfR*#}{*j!j8ec>E*c+X5H<{L#NIxPLDnqx*h7=e(2P>*|DMF+qd^T za*hwVGCLDsd2zMc+O{>EXqlfH5&T`}T4i>ee!8cl^Y%w?hmt*~p8wp;!&^&p^M_yg zVl6#Ca^pG*!of$*FsgcRZ>aa!qX$kunq3(E@bA8H>6hmZKKI6f{Rcn%*|(>M$FlR; zcq}2#H8Gph&izRENVtL|v=KKEXl-R#Omk~TI2x$rNrc(u@#zM+#&l+MRU(z4AvL|N zv7~JdtjH(JVxb?_4(jZ%TujBBs#sTpZ#E8}5Z(lkm@vaSB=#vGcEH)$6N<+*g(y1}IVL?wHz?6biecSv4ZS|#WUBJcKllTlCamcZc?yJKB#oJ#b;geyy(peImV zloIrq*-eyFJq1aF8(z@%i$1)G@;pdF@K`U;HHOh2|Hd_^le(suZ(zGKGhMu6CEHE$JF(2MjgS_g%2bCIMy=;a50A9?UC%TC_(`t zYiiVoc-N5pb?%8>EM4Y3F7 zt`8fdLgX~!6t2$NH$dButFm(@rGkPQioLc;V7Wd1V8ovY`LT7Pkmou=ot4F$m$2P| zNTE^8M+2UgIC6k6ihe-awj^C}6JTMX;$`6xDu2!mn-`Havso$uv;aZKJndUzG4FE& zZ0z=95;Z?NwmLU8|KI^}v9Esz3fZ+BHeA6^{lOn*=EugbU746mz46cg_2TT!i4Q*} z^JlV!r`~vD=f1%kZ~gGW?R#y>EuFnRCti4IVSHrd*4>W2o!bZYER5Vj)^_m8XLldi zKXm2S=~Qvo;nO|adar%>PAXSC49|07s9IR#VvzH;^zQ?{%chs8EHbG~e9QJDk3F56 zy8H2k_j-07+`eP`#b18AIox&jGhbdA`t`(>OOc(sk3IVQwfEngoSZoOxv%%`+BrM) z(apDioXdIoj-RckSAY4Vzbj;l2M_McEX)VWXvw8|aSt?BAFAJo@AMQPj*UjBcTeA$ zlV~TZ*`-=BRZV3ZxlLDp%@Ebpj!_)YTr zr_A`tuUxD62~XdJ!c^O`8owbt|R8 zcHhO`vVw?ZTm9BljC#Zray4;OJmuy;%M{u4*mGSo?9!B7{E>-gQ7waK-RzfR|a5)R0V zk0TC*@p{dZ)3!Y-vl#iv6i_nYhCOF}mrV!lUD^>)%+-W%bCjh=gfXj;$Mi~jx%r)` zPPK~2T5XLu_02?Nw93dz1(LCTxL#tVn;td0$mZ|R6pqA+AM!RZY!pC4^@?(X6($C1 zn^Zz~)N!B8FJ6tO#R*jvRFb5(KqP@58hR^kUzw%S7_xLmBb`a=+(KP*ifAgd`++zHs!6AWCw$rgm%rdyUn zIT;IKp@!dr*=-qC6B&%@UKs`q_nA!^6bF3u!1lgMsmWK#w6;X!2GqO2tG)dvVR2-Hlw}Z7me<*7WTjdhv3VdlzBesd0lSbBs|Ly14<0vNETYB9HJ^ zuCr83tE3R&^3653ROiBgfrry+#8pw={P^anv&R_ZM3QYh)v2W=c0F0+C7OXo)2?hc z$eI4cs!rxXCMs95^zwmCT`T47+jf2Mvo|>&B<7gkn_wNEs{?8{Aj+gj zjh%kJIBw>>AigcaXzF<0uIhlWVi#O4VKRjQ5n+s=p@SX;t9q=OGGtsiPW}%XZPqiLjBA5F7d5l5OGTPd&*0$CVd$Y-wjY>Sh=S$+Afjo?{ze1%bUSfF49947A z5W~LBGHAoQj7K=LXNtUfExov~QAxMO$^WMM^)*E!G3l29D1yOEJ&@Bf)Ml;|F2O)t zyd4yoz|1tem{1n0SfDn0|KeY~vb>aUZH)ll0=(i?$!4fbDR=1DQ=J2yD`O*=e(l&d z2ndo%rI!}h()HTs{-;0RvSZ8LH^2Mv=1^@Tv~~B+%k_F#GUc*dHmDvwgb;=_y8T-vE%V zWYWbZ&tqTu{Y1ob=baz2Quv9_e|_xAd-v`?c2Gb_+p9Hp`L*UsY5Bppr=V?ks>)qUV9W&vk2nUQoOD@I zPBW(x#)7#_d$rG9*oaC}?LHKKXNT4KsTjbf=p#%IAmy+5{YZq6wu7Q#o=h5&}t02+$7Cg;M9v%X&d@%Vb&!)_a!#n zbHBH5t29jX*=$FYxm^pWq~$j2(pK(#9l(q>=-3{}+| z4~WrDttr2jEoO?jRCPu94Q-8( zZEI&fS%jf!f(Fl4cj%2tp)WbCUZ*jkECbD-T!+Qo8c_#iny=pnf*8-j>BkL`^x%P@ zNsY(;WVvqg9vTcoDmzb}T3TB2C8$)97K=7y6aOk?H{NBjx?aenIMg7_0L~zPnBKLv zCZ+B}M8Re;`Ce^^Q0wu4w2880TTu`g;%ck~AYO=1vIFG`L3v~MCK(N~DmY;Xsy5!b zsa%1bN+QtWji5Er%s6TED7nccG>hPw)cyHFC6_M=@rVJD#aWFm8G2A8&ePgd ztWyMYsn#R7P{EO9PE+-?q6+uo2A7k$KhF;A7yMVDm_o&&A)uk;t?*3LYAuNt9#$SQ zX40`pI8!d79@UEA&+l(3r_W^n>(rArMb?5AHqq?90{F$+!RJFAMpm{=Ek<$Rv7gKXh_%|L$8K zy_HH;51%}NG0pW0my$jG{kwL|j@~M+vI#cI?Ht&(`}U=a)8muLMEms2!?vCKo_+1> zvkxxbx$x_royQNIc=Y}6|D}J+;M1>v^}+ccJh*hB`_S35U-;_ejkm79_ujG3zOnP< zQyBdW|N8CJQhw*LQ% zvl)^DUHAnZp_)&Vek&twk_sI*Wh)6~CZ<>iiIVViOAm>bg1PGE~ zkVJ_~?yeQQYk5^=YyGEdy>{7EF1uXoEtj>sinO#OMTw+DLLfk73}%4Iv3sVwr}ORZ zn{)Q>IbV-<2m(FbeeeCg?|t7Bp7WgNXo8*Tj7SaNthCyT`c(Brod%*1{w2C(53Kdl z)M7ds*|UEaq1^Dq2qfn0$~p&A?Q2%(+9Z)$)lzgA*=qgI(l0!*sO*3r^i@I;!mZZo zaGjN|0+*3_ZL|5Q(jsq zWY!sPOoE|?Bj2$e&COtioXpAMH_~{-* zt(9`%Op7fP5~v^|NLZ3$00Sy%*mw!2HD0?<=hrG&D%)G1zWBQG{Tiq%#W-M3ij<;F zOO?VgfH=`#kYHB%_2<{H{4VG06O$_!p-(pFZI)H>;9 z#Ju65p<;F|G%?!u0T|Nm*20rgR+JIw`iMNxOyMdkh-Ol^cl8qS1blC@DU@B11H#dU znKFbDsc;?)630P^2zPEVC08F%zgLesTus`BetHbL-d+Q=lOR2_k%u8g*v`|?#q))9 z{Ncj%f{Z%SgkA?_SGy$-4}tzf1Rfur*Vm{aRqEjCyh61n6WF2!OBhZpqO+?Tk>(<2 zQig_}G(b}Ek4#-WyD&Rbt5+ZX(l<}P_|&yu{^jLge&7kj;zJ|5Pd|L{ z^!+m*|KwLc{>j$EkMBNosG3_PCp!7`3wW9fAHNHO@V-+ItuD;3tmStexj#7&Tb-MQ z2*bvR4veIe>CfJM8q7FcKZzlFlzK276IWWD`Irg0(;BNe?6ev1PtHy-L?L`QXd`RM!uF zTv+b|jV@)Hd74dB#9_ZX`t|ZCPDRJeo2t@^=T+vhD!7(K2h)a`$y^#^Q#McWrnuQA zR3Y%q3Io258wFVUio>GeNF|aJ*yd%W>>4qTrq2iBOvZ9LpIBh{%)G0{D|dhI0hU@B0`{l@>0NHnO_-Kp-Eh?CcLVdf`=jXX@($3 zi;L{ir5=Sx=Q{|VT7Iy&92G$v)6;B1pq6^bp4#nf0-l?+K_@dYq?F6M2bnrq;HKMicrgYW z7$?#aP@`K|f1pFf&`rr1DHnGhqJzsSz>+6+G8fBU%>Cgs)X0xGi}jW(+~5nvVSVPE z_7hS6P&$-MgrF*iV%~j64oF4KB!(Oy43qQGV@7RF&?D7v<1=f}rPU{;OTs8CgBlfgT33luKyHw{a`W-4 z+RXL?L2-ZrJtY$jwLu@79TB=AY3?}e8jE&}&nx`d7DVW#MDiuow#k>VV@orh;V*ie z%F#`4$r-iFcjWDy_E1n<)f6Pys?1d}6>ty8&}Pu2lV~(o&<+|76*=0%W4rg&Dh){3ad@04U$^@ju!{8-A5@?UY}g zVF7$We5pOA24azK4Xc(cJKv<~(9OfQc+&{J#==vyjp4HKHOQVpd$7Ww{4wYmd8NeJ zJKC$#r~_ywqN1*-&J0*_@embEtdlSaj(t_bG-#8+YBC&EOBR5^(S_MjRXx;6NP-c} z4nw|?>>~SQxy#LdrB(w@Lhp?;MAsVkD@l0>x*#7&1z{rp@Y`Rcr*&p_hT-7x$!#Z2 zox1YYUr*28z5kgng(IGs8#k)8Ql-&-{#)Onqv+Eg{@L7(St^|)Fh?JG0dnj|Km6lc zm!=>2!q*zr<&DMJMnCxEYj2==_W7AJyH7l}|M1bq&i?IRtmP{& ze)kV*Ytw)6zyFUDN1u7=d%xc)WiG$@lf_$i)1%|zNFXzNr(7yWw~Rdeg%_~Sfztp0 zAOJ~3K~%GgGne0cXMH&zjipC-?4n$>eCrOOr-slYSftzb_ClV)bgEr0vA)EF-C!iO z_sQ^m+k3@KyIkm3tJymcVO^ zQuXDqnHiDEHbR==HCDTmYyxiqey6G|To^~s0!YOoe}YivWr5`mNM37*|!OC&`?grA_>?-*49qNn5>gO2y&PdoO%Ud$lqs@DNrD*^*6o!cF%$AxoeJ@ubD!RN@S&42RD-*o z;mHADn6-s925$-;RpCnvRmI3MvSxquoD`(1hU)I7)8XWRAF6Z+i3=kU!XIt4-So36 z^OW@`NklBjVNg;^wAzj1S$G-_hh1I*q@_#2kr@aaBu4~QpR0a3TU5)Ct!KgH*^Iho zl2?kENl<0*EVwXK)AA|3tC3ms7f^xCUr=L&5kVdA>aw!F5+z@F8YinX`%J$WEk0BQ? zSNE%`s}a+5&2A>mN+HqdTT!%SNT$v{gCsP@U8aCe2Vn^ew4-_%3Sm7ciBvbT{53s1 zRw?G#5wLHNq&4|c-2r0}>)}EwVgrHj@Nlwq|Qmw=?A)H2{ z=J2Q$aqxbm5WWzf)1(l>bJm~_?LZtc)9tk|UEHVj>NA>!EZG{<#s(enpV(~aq zvH>^=Y@T3*q7ndiK#0H7IY`Sj|A5$=1p}z8%Lesg!O(D=!k$7S?L~rDqs6I+6pmC7 z&uZ4GI!iN6Mv~=mm1tmx+NxG(qI2jrtsji$dM>YP9N6;u@BXX8+=W{oy|=zz z3I${1yZ0Y>_<@a^AO8INf9wh+AA9lTrTJ-u&_;IbdEn{iFTDMun^U)+c>ULr!~6Ij zep;yYjy>>rqnc+PO>SYTABvrN?TzB{6yl|5s?(=2G_m8pM;=Z~pc7fAIS6{x4%=1M5@ge)U)1FBS^X zp)J^`TCUh=clJH-=#G6yfnZnf+(ihHk$J`SC|CGjq87ad# zO}En-0SXI8P527Q9iqAF6N>eua}EDnk)no#NO#C;iS%NbqC~4vMvsaWZ_s^GKnMnv z>K0>JDFkn-K$*qU>_eS6my1Ffv0@l&#QP}k7iUoOM0Fp{9?U7U_6ri z>Q(yvbDvy&?BNFpEAfA$6CC>DK;eu(%RxvBv@a)WaYJW<`^xnn$`~@@YEu{E7~#+z5sb# z{yx6kz;%XWHesv?YPPmbKfu}0s9G#sJ;upX?Zyq%nNP8v1s#R<8Bfq&YnZL#X)5D< zlneGEvkGwlB!V!W_@-PVs*OEKL4}!sOCMCTWy3q*_vwjaOmIaM)vywSgIi#%m&#~o zE6BG#4SJ|a35Y(WZNVudNHCPYEsYz=b=IlFW6T8gfG6`GIv!Arqz=gD)3bCYfYNz| z^J<`$K>0%F4B*rk$d}7Hge{%}I7BDv>!TPG02?j9e)P}=8zxlR?V1v zdNMq`W9!kQ_pL21QmYG$j6V3}lef;imoGx%NR95^bNuwN>+k$ELNnETg?)7RiKn*i zKYa1+AD=&e?c@tzI(Gcn^wrNh_4e@oBL`1CaO=aL&u0pczWj!(y7KnVelW7@zJn)D zH%htE+Hx_wyz}H!`%gc@yu-U!Zn8^Dxx((Fr^a{gn7Mi$1@u!dz7|f07c=W8Ui;3L zvDD=s{n3q!H%@-#H@8lXfBd8Wbm8pPCtm)IYGLm7xzEqN|Mro`pL_7Br%D?uYbb#G z0wZO%CQ&PtWhPI4v&t;aG7fK#oS;^9mN-O}O#AxV}prt%n0R_cLaaKd| zEl++ceqgMZn{_n`wNkOQzFJyBZd8SOb%L6ia(tdyHwk9yYiAgRgTk0OGbC&CuN*qn z7oCPelci9-U2-t&eVa66NEfg52dGZ7V>?V)u9d24re%|`G3oFnw+vALw+X=aLVpJ~ zvjG>5$-I|Vm~mAT4wJ0o(GJthZSb^^cT3~UFw5|=HNkGEY4d$G6bn^15jutcN*+zo z3}+7orUS!P;{&iiP|qOXDM^Mis3e8!BONsIry7rxsmX`PJC|FX^JlM|K7NdjCMxWs z+qQbz<=kqKvuPlr9gocp3A0;DlD5S12`?O7+D;wG95&aZ!!0D*QyM zs24@^w_T@%uaMt#43rAU;cdc>&dND;G-HE0K3Ge|U)5mH?`=uJfETS>%HGaUA6946 zPuVZ3I%e-obm;^%D)ocaApo#m zb;287>kQLYYHVat71Sa9%RM@VTFrh|>{p}+%~Bm4Y;{eRfGD6DWTZS+G*%)a&{bhD zv@apIrlM&JppB+CS1rb1UW+hzwm^j<#7Y;8J!_Xu{el7XM7*lfIa5nvOCpY6Zv@8E zBy^-WQrM@c3q_^x-zc{#M9I)2!%-!*QhPzHF@&%S{4ivuDow_;Q5S#%-hvC4ih}wD zsIDP5EKE{tr*@qteAq6`JG3q&&R&hqGcsU>xX{A+FjIv=c-@N>fZhSOVO49;qVA0+#KIv$$>{hd;)qBCvG;%t@ME}1HCP*V#d2d-)gP;}6U4VRoEEBy zEhKE*{lR!-Fy@OP4+C@1<11H)p1Xxw%f@J^aRZkpF+S+395WWBQ5B|xaecP{o_DMFgHgV|a z&`{#?2fwT}{mFsU*1bEoZ5y9L9kkRgW^&=!(8({nPA%rbTR+Y<126yce=)W-b@SX= zFAX4gl|4JJUwE(94LiC2*_`0TB}U1y-6 z$Vk2MU30dRysaJpRU$+bMZ~eue^g^j; zUi)UhlD&EEla6K zqR^OCUmre|ER~%*88gSI=`>nLyd6L%PPasG(=|$T+i{$F*ose?!ptmIaX{6&t2mc^ z&-5<*DpX*$p9deTLGaMGYDf@4;t(B0jqIEuD(F^*SFGv$(YNh-bn>ljUL8RM`;0a9 zqz2P1Vjikv%FvAdrqoM=Z?MinOl|Iu8DXdFr%Mw4N!o|#1HD%|to+!QNG40Q8l*58 zCCxA7oCxV{X@ZmL0mS(7f2wq<^FlOF8OuJP`jyMq4jVSn9y>-4ol#^7%3xIp--; z04D-ZCllB&%gG$5RBpt88~xoVyc&XSeoNVVJdCU+xT~fa^G(OS-HSOKu8HZ2I;0+Z z6HB=qkl8krEs3RkX8sKFl!G+KXW!YRz1SPu2D+C5Zl;^Yqj0v6vNH34GRRhSa&+2d z?gbpnDO%R1C>z#^IdI{Gf24IZq|P_K)G+v2L@G{{zqk^_r0~$z&f`ZsCcba7(Ln85 zvncpAz{;vp98^I-Vo8puq2N~(YC}Ka;!&@rz+b^N!GUQsxkQ+O^b)l_-bxcs1MRKF z@LIa-xbiMRBnXH)LfyQC5CZyYse-m+2{n^0t-8$y-GGuy@&DXcP^icrDD7G?Y;i1pRdj@?R)Bt)33aI{oS8@@b_<@di6ISc=7r9iyvKn z??YDmE5GvxYq!r`fB%D}wc4YvfAjPs58XWbtBu8#^`%8IuS3alxwtsHgi>g?QI7^= zquWO)&u-k#@aWKB2X<7TOvePpC)H^&ybQKVjO_ch_(Q|)M&4b|G;@VkcD0r*iG?fQ z!dYU)Y#g=bAc(SUzE2!qwK!QKXr9tsktDE%z36N??e_MEHy|dGEfx_&r(5wm0U&`C zIu5i|)#&|!ISQ_BdTwg(7A9jS)@&8=tOM|E<{!Eug`KKp$vWHa);S_QVpB5*PHZ4a z$;r!61(Jw1+HQ2uS__97=T++_9kk;tH8RNysNF$?*g(@Nc~DWP(3`%qnqP_K)ataE z0c(6KP%0%#&9VTa&_GviO4`|6O`+1cb?x?n1N+dq_aZbnnqo*=VWWWVpP441TS6}# zvc6(xE9Y2=ik3kND!&x=kIe~j3!5qgiXhx>3La=cfg0)aRS^+OelvR$)++Pqz~fp5g$=eT z#RPN>BTNiP2AwYf9*UTK#JlUjWMVi)kENgv4WV>K zqbLNha#ycjYGpH}xrO!Fx%Fa^z8DUS*hgw<5CI?hypKJd@3%KPoou6@Z?`MG-g>ri z^~Q~o%RPPlZlLWH?Yy2uda+hk8Gv3hV=~ZCHbgHM6VaNDNU#~~MwX_hX<-})F^)*p zIO;|!%dvk<4@uPoLrui9(zDjnwo^ccVQueLKD5n4znT=K&8iAyXZxB!jghLw^HN)d zGD4$6>*>;%h#xY!Y*Sxznl8jH7X?P55!4hUrUUq^=t(WJ@|mK#7{)5x!?>+>;rx--(Z6K*JHG~#jJ8=7;4DU$_*vsaN$X@EmyXk0lK`SBWX?Ix=1Z7CD z(1;=ABTQrj5}0HN3qb*hW+X*JF3ptAk${mfwy;!08*MOZxGl08DBVz9B&8CxCjud9ertjTi5B;YF(E0YRZY-^ROhKclRWnbXDK{uFbPu*VRklJ zuwH1FNivku1Nh`Fml?N1R@;OuVJ%RxN=-B6F-yRnoDz&O(l`o>$enw=nkFJT<`zAj zdYH0hqk%FY##nQ%l&f|Dj(Isj0(mM!8JX4q2uHUM+txjjd>Up@Q5!raEzqxaqa#n(5kyq%ey4UO&H`|MZ9 z6ADX9$t~NqOpLGH`t;*pzP<0hr-nz8>>6-r@9@_9pM7QF#+jv?H|rbK+q1L#PCpn6 z(MNbam#;ke!k1^JuHK%y{^UzvlrY=5PfH#DLtp)^p`pl`AOG3Zt=U)p=^uKU>(}1- z>4gu^9e?K496vOE5s+0`}aqO#+iCi%ufD9zUk*>I<_ zc;#llLpc}hUSrcO9s*3!!{vI*X@n@-?wMF-o+_O*7rl_+G<37%DZvMQTXrFV`x zCkd>*k3u7D{d(!O%;`uNB|9Fy#iY?(*|T(#^XXou)K z@-zl48^);cn_DsXPFlc?emC3buGV^af;-_w;2=FTpO{)hPd#ZskqFpWK$skq4iBH) zcH)7D=WgHP*#xi=VyLkSYN8UmPr;0GGH0b!byJZ}iH9?+&PmlQo0P$GvNYvZ_UO!$ zTD)L^j*|EwulJt?1Ax-2fJ@e`eEb+idA1!S0s*T+NwnyR70B;pKl^TlKSL?Xx+sliw>6=reh zCYH0ok}7!-YNx3;-9S0t+*mH$nqHb+%;EyEmMpAP%0qNNBr624 zL=rWQzyR9lMU&Lf}5d(3hk7}@Uk7vCQ~zm^JLlBq%JB$ z7?E2l^JIY9{3u$Oke_qBR6CFclo1y@s)re(fj1(vF)gY9;EL3E6tjvlZGB8SK3WQC zIB3O!v`Dra5`UoF*>2)^l=fI@Vsk%;XP`ZVH89eT!f0Zaa3UIrC1lP~PZc!D%LV0P zr@iBYTyZdzM5G$!F+N7@REs!4eQl~On*Ew5L(|~O>LNygJcbms!$5fm!CZkaNO~x8 z3ojv=CmVxXi%N@TQFP#8Rx4EkCaR&hr3xzliYlx&OQn{a71>hS=|d4JdyZf$Pghfa zqfxZe+Ys*|TZ97iu$N0!iMfQ;Y>6r1$um%r^+L{uw2cR=r!8@23pl`>x?bwGb@fO! zk%LQwn6m4J(Zh8%n~YJ?JXmfK3`5G`9&`pIeoVBjGN>lzD0J>faneOrsvaUP5A8jC z`qeKb6M;J){(R=z?Q*#`dHC3UPd(3R%PmY73(Y5f{kKCY&&Kt)GpmKk6VE3n$Fo=8 z&#teJ?%JPSn$O+M%w0Kq^5G}S+4*XrN;|{8hrc*DIgy#YRL$mi@`b+Z>;Lk|ht-HqwbKYI7=u_F(B{grQBzw~Zt zksRWfLNbxs=z|le7o4X>jVkEx`@=5wio=z_6P1_eWzr%kPbdz^P(|xD zovJLdVC)&OgN+lRx(El(Mk$$=Ke>WwIV!$4IZAy~#2G;-P84aOKP@uaT5P0E_I#$(O(t(T+iys*?yY zlC&b&~huu%3ypsPY}p`HRikib}NwEx4Oy?OG`mKUCXAUPD? zdHPf!9KC#Q?)FkyrpMSBHhx}DO;C%8qVgYBsMK)UZ^YwhS~XSOb<`b2>;~&C@wV1_dXMM{^#dc@;|u zIV);U5ue>j_AR;Oe))W7Jf88xWlH~xX|_C9uGLh8G=S9*I=#Q+P7QN2uA==h1l7q5 zH5<#(lF@g?In`Af6v#D@n>p#Ik{FV5473Y!puQGlN~{%dN8zNAenCpa977Et$3j#w zNFx?qgZjRZ2Jir41cz)&fLv#ER((CQp3Ub$YtZX42WMT9*c%fy<>|v(<_qmco}cS@zQ z>&IJ*^iicWVA+yk9>mgC>vQV1I&`q%RjzY#VB0LF;beIi`GM8obO4>Ytx0#9oPeXX zZADr9thGAG5SqJHerc#FFJU4NOh|DWZGtveE%*ndO*31xW}FYDr95tZt#_177BFW6 z+Jvc)L`wlKNH(p4mJQ#qo($pcDqIBv43sZs*D=Pd24Mp3$m@-@Ds|E!JM5lXtDM@h zbv;vMJ}Hdad@%>ANl^?Rz!kTe)wq{wym1K7_Pr zI0h_4lB;SGQ-v9P7!J96VH7kvjS59UdNV?3gQ#TLlujE>@LtAucK}^E2TFWfK%e4` zOK%zZPR(SJoUkMvA`HH4Aova-N^?iGfo=*C@Scz-TrLv0;fxjJ5sbhY#Ww>A9#ZidZ3GM(r|*F z_wwB(&ft!R9y;~p-GnJcn$&(Tjmd~fI?sezI$;;Jnev9rqtQar^9>GuJL2KJ^H@;>_DW8{9IvbML;Z=iaMVJF$sFPrUHl zjdy=`=j!DnPrepSMz5av@W$B-kiMUM@yknhr$IRBc-sHWV@Hl3xpnrf^}DmpVtr)i z?p;St%uU~!zdeh<@A$Ue0Z)Bk|LJDAcKy*cJDSE)u^jQ9YP0=J1btF3P(MyfqpXM@QnRc$o4R z+8eBlAwOpaTLKJbQe{{<@TOf$-zzHB-e$ECjfA->G>(Z`MGbX0i3)_x3@gxG4gbJX- z?prb@FQZY!8Ue^9_{vOa0h5q4hG(NzPwhIj@&5HPr7vt%r!%>A1dprs`^zE3>q1LOKv1lvK2uAC;H0f|fgvOC%ox`zU6W!-pO<;*7_#_|=fHS6m zh9d&mq>(L}Wh@d31QL?a;}Eqw8!M~iD2!I;r%n3wyG-HfFeSlB(k*h(V|VHdirG{{ z;IHahliT7_fkOn2K*nO4bW;?F&A`IKP?&IHXE2qPEKQM>F2f)kkpt|IYPye=GgP4l z?Sq%56S!IWQ}(Y}+zND59#_mKr5-B{+LdXQb*R|GY6brc5xa@A3$uHE;YP9?{!*Tl2QDhZ~R9%X| zTfsoBOyLmA-XZkSN8xYOxfrYW8kC}_EbvzfW0K%+b*im01-LNs+_gH?7or&44Qvs= zM;*iAa8wbHht)%G2J(iIHXU-2%!!3m)I>R<*(<}G;*p7L6^J5NEFt_52}Mi3mZ&ro zEp^)EVzXJRXdye^n)tCT#AMtdf1CZ$~2~_MsvdWtRnEU;RdE z$JV=N->no2$({Ede)8E$k!4|&NMg(JFSUy^ndxh>^q9ZbeD9|}nmlkn-ALS6G&Yc3 zTsVK}%6$(#zIfxJ&l}Aw&erpVemJpp+qR4E{oVZZ^vOq`E@oC2=59~yK0yE1;@oW< z&e;BA+xPGP;0OPqn5&+A=9Tq@scUCHyE8T034~6adi4C+_k*4&l3NGitd5Lcc$W^s zTd-;Oo_b=}(UUhWpI^LvTY7eAsx}Lg2aa`W*}GGVWV=H{>BvxM*Pi`zpI%@l0x6(& zpVBrj3hjwxu%%hM?9f;-n?CYie^sWovfRopxf;dd{9>onYxJ03s2wZArL4$8UaLts zZ)5}tmJ#%|(Kws{~PIPKp&`$~dWp>_J_?YuJ_iP@)PRd* z6AcVByizM+bveU1t@x?(JVA-8;d1@;-@KJsUH8$<PbK3i&cbnc1v-mXZkUyGl3OCjnHY ziuum`o$S;d=%58TL17xALU2i#b2KIShd@mIqjLR5O|gqsj%;FN1{||Vs53DcPK+~Q z*#=Xfi$^y`j=~5}#vH#uV~ytwxaeww0M+UelMW3Bd--0VSsP47Tg_U8YmWJch6i=z ztd!2N!FCIQ+fE4vH0H43pc# zgg(jK(dOUkVR%JY@CPM_-|xghDj`w2Devj{X~GoQSt!ijuUBdj&3}`IBgg@}h1yKM zT;Yn8R!fQ9q^O*EUVRt6?YIt_){#3v`Bh3~OwmR81z59J$9lCHZO*(AdJsj>kVFoV zMkws7(s)`Xj0Z;*VvLDEWk!((g-_N07N4&{9v0( zaITLsK#1Cu*Bj=8l~)*Jx{xoz>j~qK)MPC68#AiGLN?0;p;*|Rh=wYR%mAP5`Ec0~ zSw&moKm|S3R#n&-SRcVcSXQ)9E_=WGrPE*f-tV*uGw0s?JGvMH{>ae&llOo9yW~KN z=il~)2gmMvd}P;d)a9cilSpqfsdwY{nc16HCJx-+t*za-`1$=WeIpX-W)|-xVuR=3 z`PqA)eDcgoU%mC|`z%5wyIw2hd!gvA9S6?6`(|cgVb9*fGgG%xTSp&w=Gn#DH*3Xm zFd7}-eXxqd z*(gA+I`zg^yloHqrSmiM&_ZEJ#L|Ngf9Y#D?B)5pD(ZVW+m1bOFkf9Q(lCQbcl5F~qE8(LQi0ek z;G1bSgJylZnkEWA%}EN)x;jTlT~>_MWCXmR8JR^j3h1ayS(-XZEGl-CjZI+#aQcL_ z0CAAiBg@Z(VzJ1uxz1z>r3E$_ku4!Xc6nVLdMSWg7oWji>On^6fdPUp-dc50`J>HX z`ksn0RX3#?7%y}zcq52apjPlqj6A8%U^U^Hh?}8w%6-8X4fa&iA(thGFFyih#wig> zc}}*)Br-W&BIsE&U&%VvLw--OSh;fH4%t9zbYR=AEgag?23}8wO@XxRzi8}QmoBNI zR!YnVmQ-DSn{6ak>QhiHjRK?FNbrq`WFBM&yCtIBt{d7j(up^(E!9Au)v#-XOM@t# z+|sP0HignX#W<8p)zC{2&ERC%Yn*@h0jz2q-Cv+9Q4>)?(2pn}p zS>s)8(xep`j56tthz?UMR8rMDZ;*j-IH$?MKxMfKX=gARgu*99mae-}stR3qD4NzM z%jes)6fA*UjoKp=MjH772du+pd}pntH`YRabPX*C_E-^SLA`45Z*5TQ+1#7>sreJ%E%SMC9ROO-#y343%X%po1f4 zibrHt#wO>H_qP?dhJHSfswCb|j3*wc>B`wF8)#QBv`tS}B7y1Va%x z#uX&ho0|B`L!_adHb!tbh64gl(6v!+wrI+L;kyNG2f`rL6VaUWVFHMzpm_S29voyU zK!Oq|DIjeX+(ffcNya2b%K$07FG_7xjcKtUyL8$&X?(|)52lC_Q`tg|NxbBU@EbH} zC4#aKh#J`e-7m~}#f>uDydCzqkT?}nq;>K8ci#Woxm&jbiIGq&aq!tMh10Qx3mTuUo0)Xr<`2wjO;n9Z#Np`==W-8pBk2#14s{{dsv*c*+X$U{^Um;Yw<~wEef^z{+m|*z`grx+JgezQ zN|=O#sQ?;%*J@c9X7(TpVMAUl&a+TazvyIy<~+=Xo1BU=IWec@N@eX-4ABo!_%Kly z3umcf(pJM!L4v`MRb&VyUWfMg23s5A1V)p)0&kbZTVl$ZZrjrq*4l%1jt8A7V z)=OcA)M7>405IYW%mj;vrNBl%_|&vVo`BI83tY7*i9xfhRHI1m*a&JL6u(hpxl~Kmh+O8Bk^|bs8Ey~L zo+y5KF*h^6UT$bODzD*?vXeCIki+YuK6v97 zQ@!lM4!67bCGtU8{O z^96ddAj%j^V&Q0vnzkf>wlVkF%2b;S=rKeB2@+|=xtsh^|qr0W$1E3zHP4x13q0m(qt1k1utm{|{WMc#1~2N4ZOx+(xlBT+^=pqbYo3R6x9?T$6_g=)nHBpD(*l10v}qWVq~AIV0h zV=(G-C1J9}0_kK7kG`DGcPf-}0)Dt&c!g-J39wr(q6UZOqfM}kzK$Vs09J^>ak#>s zb~qdarqxs_^@ON2J3!`}Ku?F^!o+g{q67{BT8}iY6`4p_sm7S<3uBM`mA!@8qa5A;Nwn{R? zS$*`CFGu6B-n6rcL5tS(L)ag+pIfup6_#JUb>#q<*&`#usLSP@y&#sPNSC2pv{wP2q+meqkZ+CT~zK*HBtn zSgS}`al_~vgzc1~rM!d{QQ38Yk3;>r!WL*yD+sJ+f=(nM2Xg04K~$G87&g-dZXb z45wD+BJpqozQo^iF^1G#SJ$K9S)@>C>s7JApk1lK65wjEyG?X7W`^|Vh&eH2fL7q2 zZK`qV3zn}F6so3g4k6qVq8JKw6utzxDppWm8jZ#P1NbA_?KBN4K=@2Ev{%aN4hQJui$*<(SRjt-w4G28O&IS4{bAUa(A7g^egxTQvrtg>k_S*S!MKpH%IgVs zlxCyTbD`u&W>V4tFcAQ;V8#O+6gR3flrrYlR@Rx-Bh-MZU4t^A2R$mmjRuT64?rH+ zxUW_)wJNlOU*LW94Afp7EYlXyN2X8(I!Py+P9dktX$fGfWcgPxkPHX6PK?xxg~3#+ z+o|y+Xkr@}7+74-@H`@}T5YLJmm6?wD#DaB2oXFbB0_{<1&@miYI&B9fb}V95{0#% zQB~t}bZz&xn!Jmn@oSl@>A+aT!j)NkXx>a>(${-F!FknhYV4V>US+ZEhn{8C$@*aoCYcO^r1rt{oRbSx(ue?h?L- zq-||fhz3>)8KL)%!xEp^PWA?{6N?4aO;2ElWK5@>B`az`;o%HPfS4CC&yK!8<#Uwj zd=VtGc~UfN(@V#zcw?%<1lT0S1KiJhDxViKbio-+yZ`N*;xv3$7q6*mOv{;n%Z;5lQk*Y zCE?1_?To~lpSggpLV+>#s7Nt`gj5k-MoN7-zZ>}&+Q9uJzKj=S&(m4&1TaYGfTps; z6}R=>Xs(h;CSu;9;aH(WH5Di&lB2jvc*Ml|pqH7uR9|qHvMqw>i4`TeNP8JjGZYOm z4kEGe)?@eYJbp4W^>J=xrI4==Y(Ki|=<#0h%HqWzRPJ86G+j7)|AU@rFEW@~zVJ@I z;+fpPCmwd)x%N?EWxd2G<*sLTp~^_4?BcE1@WjD=`xxMG=fdU8;@sAqV+WpobTzYr zxc#=tiQ5;?Rfcc*t`40 z@p5MA+K1;5%o*M~xOM-5%;Ngh&(1U}%(`ZRT4e8`6AZHYg-thjNt)M%zWBnF}lvKu3jSgl$z^+okm5n}3$vBQH2Pblf} z14X6Tpv)NR)Qe0GAq7`a4IiK|(}Nk)CT~lrq1~fm7c^YZScJxYw2@{CKNTvXQbys) z8VfCoR1+6jpmcwN%Dn`Kc+o?uV&Np^&Cy^7h={|=i*bi_vk{k)r_l*DSP}}KECQZP z;i|Q=+F%3+80E!^hUK(j?DC-q!pT9}R3;6mG=q<2(*c!()X*n|2x2Oj7&j(Lefn}J z;t807qUjxr3#vPn+tX`x=LS((-Y5skT?lN1$0r+NKEi&50fS!hxizPg!|RF(D|Wy4 z-i@1A7f(H~^U(1(n)(Fok1d8s4|H9% zMx&w$OycE;up}^&utd7yGMvz3H@h+zMN2DHkHFs@5u02CTQw!I2N%xGG*jc;DxKxN zh+$W9m28P);-L(ts0Zh*t}c4F@sBpFP7^3)eW}2*J9vN^qGwJ-^L{L#(eyeVC`32q zh$u@bpHrS9fj6^}qS}ZyCkXw<@JF-DBNy4bf(iA5C=DqIJ4Z9K;qXg%w&Qx2`PI5>X2Y6s9TMFeJbp znl?389|!_VVfbR8W@^vIZ-My1^6vga2eSNQgvuHC<7naH4fE_W6|se z2hs^+AbA+FB2dB57p1DeYLw+1PKqWHhxKQ ztJE8%RzpN7C1GlECyXqr4T;#FFh-5>;QYje#5m-RpsL;=v)2Ij*@IzCB{RQmCkyO= zbs#4XwCQ%>osjwt#%mQ!s3u)7*sjKuW<)n=?PoWtyAdJvFvN};I%n{3FhXv`cL=1s zVRtJ?6`L(YEmENdn9FE1 z(sG9jOEep(GkM1a542AQVky zRx>`AFTVfLCnCwf+kf)!)-#1#-}A`xuTJh6EiF&is+r9E@^EsbT)G=dPL5873-eP` zH*fl4Bd(i`axO-wlL3U-{0rwhRwk`SjEA z?R!|Y8=sv68;lMPj8E>Ey?QoVDj#_0>5(m4vJ2D8Gqb6IbReC^eIRVW?%93+>5+-; z>oZe}w=TEJ)#07HC-?53zdKI~oQ|iH;rfBy$)Rogib4N3Wn57)KXkNAE#K5w zEKwADj0KiuHA)#Loz^0ZwKT9DF*cJlreP<;Y!&3wv3hyzR zsYSpdi#qgVruyz^O6qLMO;)m(t3R<8k=CAX_JN~YZxtr+a8E0p7bg+=8cd#K)6d{;yFW>0?;lKIsyh%?Z+x`B3 z`XdlAYYrlP_5sqd)+A{)HC#D8jz;L|YP^*7fVjZwu!OWS8ux;w_RzB^(MD4wajck_ zL3D&syJ{$yH);bGPOU813WJ2zwPr*f=fTT=m?|bx0N6715vnx=0*=y0{hT~cTB;a! zYF&J3G+=xxXr|J2rR}O(S$ZYuYq1(dqSc}kfUA3y#WEaRsU663gPEy-LC6Tv_bK;L zu2nfl7KdvQ*=LwHwhpkC33ED_pSlzbxiFCrO%xDLD>kTO#6^#^Zv(l4#P<5+z`jUq zHJ49%1x>?tu(7moC{)y9${Ya-QQUI9Mb^4!d;}5%vwFAES`mwA7y=n(C^~<4adI$v zvr_B!D%56B?=I2gg?)q&6!Zba8Q(%r&U~+c#7&BXjczs}S|Q%Xqp^fdfhI|$T)R{| zJ(xKZ(1@qbQB@S@j0t4j)cJ1sq?{-}*MZif#a*JWYx?ytvE3**Bl%^y zvsyGZxn-&*I+S3$40$hONvo+rsEBdHg<311@Y5wC6;nB|FcFeUsstt)RiS!<1d!da zAQ3oo@ucTVq0CE#7^k8(UyVK^n1uz(VB2b!yX6h= zh}Z_ek5rhR8VWg(H}O?I!cEO3b;njW){*L@ZV`qu5s6ZUKzNO^o`_E>UTU}m=K;ZS z9!IND3CAOFVc;hx!qI*tTFB>YZUJRTDd@0k6<^Cx-kz7xp&Xb=y3Y{rx%e`i!c)s5d_4#qH%AdXycN6<<+%n$+!F9wxVmrTMrOX z&;RiKW501&Osqh{7fv#_&>wR*b5O{X2ccF%SnWiVaw-HA@;?GvEUd@V5c1m~Z&MAn zyv+=x3IYlPsR7BtX*QNNwlWB~)ao!bL*xltkwLDG$ud&f)`%z#f|Aat&DPUlQK0Sg z?2E3T4tNPc8Nt&^mrjAlDl10NFd!J?8jYoLg}nHTRv+VNG{wP@-G_Q5+KZ}uz^H;o zG}0@b~x2=EDz;())(%oG}d>eKxQb~b-dwtXJzUv?w4we#R$}n;z=1$-iZ3;beRxMH1;6sop>oLHN8xM**KvEixKHdGQWrFW0 zgXF$MWl#?U*9;8C1#A8RE=XxCc8+SRLLI5NTeARLid!-vJP}!~htu*vXYPh>LAoS( z8wq0)Q)3yE>A_hV9y%>XiBcmZduwpIOt9PLP{4_b?nk!YgFa?Cl+PQ=vcjy3WK$X9bbm;6BEk)yaY_}8T zm~xXbot&OmhoL6yQB?quaV&AM^_FUw2yN<>qKv11E8b!0l#a7bo4|^WhG?~hq8MW8 zfcvTrXh?OduGS_umOVh?EVRJ1c~#di^-=9+tA{H<)$l^o-x14SzpQ#BJ6w1Gr(Y93 zlxccA)oclIGP6TmH*@EO0pQ@U>%`skAX2ZVD-lo}FkGV{JrM@7)4f;Dm*A2lQ%OEe zBaG+;gaC>JS%)k$hG(qjbMXMFbJETsLRwsMl;E`%2J5QSdT91vE(86A= zp3hJ2+wtXZ|K{D>7r}XFZ%&Pl?*uEGo1K5*t8Y}x>vt|+4emJn?CW2@@#YU_FI^8r z!^7K#*!8vC2H{$Cc*}uP50y67i}SPj#RUxYwu6URvdf=dZqzI8=G9<4ed>uvkz!uE zd7aTPNnfPt_a1)wxsl1qE1$o6cX}=!N=_WvTPtsT^3EsN@6jzovhm?+vshu(4RVla z$1cEx$gO5Pt`N7&>8h-s@wk3R$Br=~v}&bCLW$vqCu_mE#=06%;B?~APfbW9j79yQ zN(IDBkl2B?mwv-25mwq(@2zuU1r1pbq9cJQf(gHb<)`u^vH++>s}UdE0;I}`p^B8v zBl?hF6a)k>VlDwM5&~8uqp5x>Jrk6qnO~a`yuw4&UWkI_&i10sE=dIvqn5~OqVW!(3mKK$)ax&m$t~sNqP2h@81^&eKsI5{ZC#pma z?VYlrtUY!kuyk?$X6cT1TI6)2WEegn{l?0DAkXzYgT${?1_|>=QlS?feds^_`Hu!hx89wZg~ky`5cxsM7jDw%L&YJLv55%xcsty? z#gk4uCuqxNu}FkzLHUeIS2u-%Q4Xov6%>_9B8-Sf4@Pkzs|Hvt+Kh351_{Ss0M)~RQM zvFa9FxSOQfDj6~swynWn3Ls#tNRb23634?7Dos*iy-D3ENW17}7FUYeU$0big{mFq zNHu{GYQ^F}IvNfKS}l45ROQfxlIT>*xRbie*8zF;#s>d$v@nhawSAhbGewT}y<*#Y%|@Q&l1bh;P1TDm73ihK@w^4Tv6%2J`NH z1XH0Xi(F&2u;7Cx+kl80Gf}QqVgYX|7US{H%+8|zO7hxmbiJ=0-Er#4m)Z?7vD?r! zgTD6Q(9lLU2ek|m?r16n)x`8{y_w}DO=ZYrD-EW?7SPbG6l?Q~YlyWmLW(6!CDQAQ zbN4-d`q?kNeDm{P%}z~6(pw*Y?HlfPb!qO_vv2(R%?ocY%r75#{%ia9?>hUpe>y*P zD>0NB7@Gv}TUl8Ngu~-IcEr;IrHxEweL1@{8y}jaDe&&}EZreE%9uAWuythXo*nZu zcj#_ljA*A)X$HE_e*K>UAWdC5^U>eE9SOybo<4Qy!llLK_4MG#wuzy3D_?696}js9 zN=o%v5iK=Uuay;!4y^!be$kkSHtgwMrA4;f(#qbpKwwwcmkxR(=A0CN66cGWAV8a? z2DifliMTE{hmvw2B{1`Dtwxt_*yRQM76C{3IG3f(?F`pZ5~qrjA2`pt`eaPG0ZrJ@ zus8yHcr>QtqL4@9NZ1UL7L$q92wy(Ul5M9+L(rliQzsZOBCCB{R2 zeEiftCJA`j_3c{+n2*erqz9Pe8=AX0XDYg?DT**<4Xl>;Yd{ggBqQ6mN0U@yK)v9s zLlrdvLQ}!D@+vGN>n+2`c3KDb?b@?@&;0d^Q7^*HwV{EuJsF;WIx)qgcN+fmFjc}A zJ5*^SSQ!UOUdpfu-IeOBq8iJ)D@Igjk$L)D9IU2nHDo7n3GO4 z%fB@8tlllsThSINhGdinMcELXSEa&ko2h<$lcgudl*YHu2Vx9}02j>KtHcSn&Mx$4 z+z6$bURxKF#UAk^2cr^D7p@ArF%XTxIO$H}GRX5((@~rXDXdTL2jwe*fKs``sA0M$ ztV31NCfH08$+Skl3P9|3MUx3Ybq^ES*saW*qB3g`Kx|+j5={Y+iW(tFZmj_{8C)5M zicSjf(xvrvuRkJc22umiwg2t!|HksI^Gj3Hv8{Wa{POFQ6Qhf>vk0}}Vo+1%tkOv$ zKiFwk8K#sgsr#~39osTYJeytKaH9z0ro3qb<@%*x`)A*aM}rqX`Q+~1r9+RuviH6t z@Bipe3i-;h$DaoEJaqbn#Fib+;_^p7{)^0NZeqt?vQKjG{Q62Hp4fTlFifCAW;wTT zYvuNIY-F1+5TCw$39Y(jwUCO21|}y{V`B?5^BvKVTD?-V9_G1 z&rg!1@ZiN;acT$DFjVBTzewrFh#T@WhRKMRrJn?t`PUp9-%_p?;0{KpT*Hh;Iw+nn zL1%Po8B*UzGuIg7rWdN?XHBY`bvqeZr9AqpRh;;tqxb@ErOs>?Xjbw?8Z2REUjH~F z21zP=%xkuu2#vkxB|gn>G*hHwu*v;4SkyRn>Z}lq;OwpOV`1EIya zjM_l->6%3dObBEv$!guBio~&zGztA#ZjuYsqA)Vr%IVOEq4HB}qwhL z!79Hu2W-q)(RVea0p~9~)F8uk@IZz8P3O#YNXh%_V7rm?+y@)(~JOS=HRgioFSRQB$ik#W*r)YQ=-?QsKG9e z`ZeH!5C$+5CL2d44o6AkCfd@O`=Pl?6?O}?5rYdfC`yHBb>`~%0wZAY71W3FxpIg5 z3I^1pKpO^b%MVbbjturLupvaP-qYz4dBy!Ik;x zd9TyPPVtnFiYAjP90w9?ZF+V^LxTp^Z8I?it>}EE#zIDEzYui_bT)|i1LJWt$gbI@ zlQNB5ACa%$kNgeKmZ%O*49Qih-l%GAzF6T%GpdLX6str+?ES`(!OZf?juVfae)*-$ z>Q#<*S*MHTDGwdPz0;R-w{eT3kuA+9@5vPCAh+6*ji--}=>W zefjd4kFK7-;EAMP{MJ8l*RntV)Bk&HbVq9Y&awSR2L{q_{nek{y>>0PIA1Ea_Z>S) zGXX*gxs9cfZTk+NI#tQ8R#ukl*$nzT!`t>(Yt_}2d3u8ojY_3c6MK)Kt2lRihWb^% zUGeoh(QSKCtGRmdgZxsalFvW#>~kEqGw;8@u~OK%XE+=T@7uqBZh0vXOrQJclUhB{ zDisEN5idCa(4aWd6smi}TZhRGRdA!4O8=?*=&qNJ3+<^Z2kmYwEnm68JU;Cp>tS-p z5bDX(00)^Z5xZ5$GX)XRL{tcKKvYGnG{zc~XsL;E0>?*)Y+>yYJ|8`*Y$eI{s&MZQ zCdRh}kaus>9yFZGtWle+lq}KsZ^?5VW=dqpQ_p<9EN~eM+IF%N;M#=vE6OcD~=*;;3MQ!NtWyu1o@dYQ3YNp zpvq&8FEr@Q5?^zL@+ji+So$LCvf0wCVBb_4b+sf8iB9iO#J@A_Nk<5%x)f8461e$? zE0J8iSx%WQ{;hTu-J#l>)X!%e7Mh5aBS=k!44ofv_N+WsI3hWG=1M&lvDVn{@uon@EG2D{vf_5g<%*-2$o%T>R zr&l_;q7wuNhEi{#^Kz#MZU5t)H4#Ai9BQ>}wd zSTL#xIYdaEc+Cw6&@&<|t)KNoSlDiNA z+Y~+A_$WRPNo(~Z+U!ed4FfO9<0@_ELROol%-sB~&Qown)NA%Me=uLDA;M zmGWA!J#8pGDUlBtAi>&!N6Gr>DJlAHD|sLw4^*lRVlOFd)9nvU6c#_-sZvDuLZG7a zjS?W3H%h4_Yt%U@;q%sTd3effM<`r87u8v;=?c0mSsAD%a4sB)OfFlmw2&kV(eq9g z=Avc^Z=3)km?;#|&IRO@8L}CWX!z0k%nA$)q{4}~OkS(WT^1@8C{>I?(kzz4-SnCPHe3dm*`~2 zy|*aeLLhX7$OR=?$?(2h^~AQ(ILdL|W-=Ntm)4(u<=KZHJ$~`b2kT2~>Fo!ed*jzH zzWX;9KK$s5zxBKH;-7l?D+^cN|A#;NcRf#Zcx<>*$i`FK>7YV}j;B1n|M0#;hwt9H zR9efdtt`co$$_n7OA8sy4`c8!-@#CHe8--J<(bNQu35^tJB>stHh%ad8b5cZZ`ZPg zO0)fiZ+$PfHhbf_}110t@^x3?Sk1=+cPd1 zv!gi)iWY4%8LQ6*J17E`Sc8L?IF?ct#7rYWJ+H z|LjsZDH^V%-e3#gq2%EGtj$hTWNVy|36rN(H-hpKbM!)s)Qzsx$Vu|-N=+mp?< z|2TbNVpE!jaDYDjUf;Xd?>-iBkA-Qm7wf@914QRAsVAPB-b%jF9=$xF-I)1z)5_B= zsOV@McL_q11%k=N2rBe_U8xFK4q;XX%F)QN2PbnW5lE~uoj2gqD8tl6%~kTM8!;Q~ zcti&gwSEKT_Eh27C1XatgfBZ3?r3=O9f66$X;xRtgo z_dqf>HXNJSKFp*D`l~<(5KPW&u)dK8P>`CDqN0!ixfTd|-9uv|wN``LRY{}n08%ufu2L%x4-X1to6u5r9WEifl3RTG=~pkG{gllS@&Kz!ea2Mk9GwC&Hq|oeI2>_?gd8f} z7n{rULfL^;`v*GX-t2qbLa8Nr5 zws*8=$rhEHiao1|OJeWX4smA3>wmHdDyXaKU@K$=3yG{+CaMl9Vpbg+?!@36F}y{j z^~ZK@Z_5OUvxNm0TeTm^qL?3#($kLP!gJ(H^>{2kl%$H|&gEgfL4S)8*OJ5HLcl7q zrQt-(AByG?W^0zBILv4)vw_Yq{hKcK1Hm<;D3BHg>QtJ@(--(DkV!3JI3rm_hHr?q zs|-)GBQ<-Hvtom*bk=a*I6QQ{srE~K1~;XrA&wQpF#vvX^~}wt{tn>Ad>`XIH?>g_ z8bdyYtMM|@rwZaKXo0Aeo29F*-iYC%J+5lUlPgss0cO-wCi?%GdhcM%((AhK-gk3O zFNe;#d%CBmr+XrRnL(IA1_6)+X%-2R5($wM<&vwUD(ersEL&B!MA<6al3b(!i=s#x z1PFl0g9*T3QqOeG{kmVjoNvzg`nUFZ4QV)NPQP&Px#xV}xA$Iq?X{$o7NXFqRU)2H zwp?g7`LS>;8iG<|xeOc#)JJsq#h67+#^aGBvTsDmLKzc1Mm&58!a4AtQe@oO-IZdQ zbO4ZE=Qv=mXGpJBZ+O1;um9Qo^RK@8%8$PAkNPZ)umOK#>E2=;6uCgd zwK(unhkCNv60N6EcW<*=-T%muCL_!N*Nb2HT))?R?ML5E4^PF1M>s^@d-aEqg^oV? zY+z(`|A9T<`9J@sn-_09^|{|V{><6^$BrU(xOnfDoQwNIC!T$FWH|c%Prh5vl{Qx9 zckSH^DfiC32kA5n?It@onoP$tBO9BWwq8|Tj2}EYb@7m1(y$V)fh ze&z1@tN0bClVMK$@%@MM`HkyWZm%q^l*>%=`o~67h&6cXtq|IGvWypnL^3Iwj^Uth zGFnOT)~-|!eC8`4=$`rpcyzG4wR-77zX&W2kfXX!dq?wB`hs;ctaV{!kvdeuymZ*I zbSxRQgWF00gNlr|rgN7y4^`5^5Vg-F$OT#jd>-(13)Modgvt-|G@MW1gbZMT3-B%s z7K*?jdMDVI$u}EI!jv%Vg;euc3M}q~gZt@7)et>AD)0(chy7I@Dg$z)Cnno4W!D

    `4k2kIRpgD(VYZ-=d;^~v>>T~D_)L973IP> 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

    @~4q=?pg&hgEFO*+gV&PP_)mKvSz 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

    4MjK}iv~v88i#Cbow4{5&+R7T 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 AZ~y=R literal 305945 zcmV)QK(xP!P)004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00006 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

    JiDMgw;|Kr?PGD&$ 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

    ygzgw_B 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 Date: Mon, 5 Aug 2013 12:18:32 -0700 Subject: [PATCH 383/736] code cleanup --- skimage/color/colorconv.py | 15 +++- skimage/color/delta_e.py | 170 +++++++++++++++++-------------------- 2 files changed, 89 insertions(+), 96 deletions(-) diff --git a/skimage/color/colorconv.py b/skimage/color/colorconv.py index 75f4eba2..b4b9b4c6 100644 --- a/skimage/color/colorconv.py +++ b/skimage/color/colorconv.py @@ -1075,13 +1075,20 @@ def lab2lch(lab): lch = _prepare_lab_array(lab) a, b = lch[..., 1], lch[..., 2] - lch[..., 1], lch[..., 2] = np.sqrt(a ** 2 + b ** 2), np.arctan2(b, a) - - H = lch[..., 2] - H += np.where(H < 0, 2*np.pi, 0) # (-pi, pi) -> (0, 2*pi) + lch[..., 1], lch[..., 2] = _cart2polar_2pi(a, b) return lch +def _cart2polar_2pi(x, y): + """convert cartesian coordiantes to polar (uses non-standard theta range!) + + NON-STANDARD RANGE! Maps to (0, 2*pi) rather than usual (-pi, +pi) + """ + r, t = np.hypot(x, y), np.arctan2(y, x) + t += np.where(t < 0., 2 * np.pi, 0) + return r, t + + def lch2lab(lch): """CIE-LCH to CIE-LAB color space conversion. diff --git a/skimage/color/delta_e.py b/skimage/color/delta_e.py index 2c9d2b19..602e8b66 100644 --- a/skimage/color/delta_e.py +++ b/skimage/color/delta_e.py @@ -21,12 +21,7 @@ from __future__ import division import numpy as np - -def _arctan2pi(b, a): - """np.arctan2 mapped to (0, 2 * pi)""" - ans = np.arctan2(b, a) - ans += np.where(ans < 0, 2 * np.pi, 0.) - return ans +from skimage.color.colorconv import lab2lch, _cart2polar_2pi def deltaE_cie76(lab1, lab2): @@ -50,9 +45,9 @@ def deltaE_cie76(lab1, lab2): .. [2] A. R. Robertson, "The CIE 1976 color-difference formulae," Color Res. Appl. 2, 7-11 (1977). """ - l1, a1, b1 = np.rollaxis(lab1, -1)[:3] - l2, a2, b2 = np.rollaxis(lab2, -1)[:3] - return np.sqrt((l2 - l1) ** 2 + (a2 - a1) ** 2 + (b2 - b1) ** 2) + L1, a1, b1 = np.rollaxis(lab1, -1)[:3] + L2, a2, b2 = np.rollaxis(lab2, -1)[:3] + return np.sqrt((L2 - L1) ** 2 + (a2 - a1) ** 2 + (b2 - b1) ** 2) def deltaE_ciede94(lab1, lab2, kH=1, kC=1, kL=1, k1=0.045, k2=0.015): @@ -104,22 +99,20 @@ def deltaE_ciede94(lab1, lab2, kH=1, kC=1, kL=1, k1=0.045, k2=0.015): .. [1] http://en.wikipedia.org/wiki/Color_difference .. [2] http://www.brucelindbloom.com/index.html?Eqn_DeltaE_CIE94.html """ - l1, a1, b1 = np.rollaxis(lab1, -1)[:3] - l2, a2, b2 = np.rollaxis(lab2, -1)[:3] + L1, C1, h1 = np.rollaxis(lab2lch(lab1), -1)[:3] + L2, C2, h2 = np.rollaxis(lab2lch(lab2), -1)[:3] - dl = l1 - l2 - c1 = np.hypot(a1, b1) - c2 = np.hypot(a2, b2) - dc = c1 - c2 - dh_ab = np.sqrt(deltaE_cie76(lab1, lab2) ** 2 - dl ** 2 - dc ** 2) + dL = L1 - L2 + dC = C1 - C2 + dH_ab = np.sqrt(deltaE_cie76(lab1, lab2) ** 2 - dL ** 2 - dC ** 2) SL = 1 - SC = 1 + k1 * c1 - SH = 1 + k2 * c1 + SC = 1 + k1 * C1 + SH = 1 + k2 * C1 - ans = ((dl / (kL * SL)) ** 2 + - (dc / (kC * SC)) ** 2 + - (dh_ab / (kH * SH)) ** 2 + ans = ((dL / (kL * SL)) ** 2 + + (dC / (kC * SC)) ** 2 + + (dH_ab / (kH * SH)) ** 2 ) return np.sqrt(ans) @@ -166,76 +159,74 @@ def deltaE_ciede2000(lab1, lab2, kL=1, kC=1, kH=1): L1, a1, b1 = np.rollaxis(lab1, -1)[:3] L2, a2, b2 = np.rollaxis(lab2, -1)[:3] - c1 = np.hypot(a1, b1) - c2 = np.hypot(a2, b2) - cbar = 0.5 * (c1 + c2) - c7 = cbar ** 7 + # distort `a` based on average chroma + # then convert to lch coordines from distorted `a` + # all subsequence calculations are in the new coordiantes + # (often denoted "prime" in the literature) + Cbar = 0.5 * (np.hypot(a1, b1) + np.hypot(a2, b2)) + c7 = Cbar ** 7 G = 0.5 * (1 - np.sqrt(c7 / (c7 + 25 ** 7))) + scale = 1 + G + C1, h1 = _cart2polar_2pi(a1 * scale, b1) + C2, h2 = _cart2polar_2pi(a2 * scale, b2) + # recall that c, h are polar coordiantes. c==r, h==theta - dL_prime = L2 - L1 + # cide2000 has four terms to delta_e: + # 1) Luminance term + # 2) Hue term + # 3) Chroma term + # 4) hue Rotation term + + # luminance term Lbar = 0.5 * (L1 + L2) + tmp = (Lbar - 50) ** 2 + SL = 1 + 0.015 * tmp / np.sqrt(20 + tmp) + L_term = (L2 - L1) / (kL * SL) - a1_prime = a1 * (1 + G) - a2_prime = a2 * (1 + G) + # chroma term + Cbar = 0.5 * (C1 + C2) # new coordiantes + SC = 1 + 0.045 * Cbar + C_term = (C2 - C1) / (kC * SC) - c1_prime = np.hypot(a1_prime, b1) - c2_prime = np.hypot(a2_prime, b2) - cbar_prime = 0.5 * (c1_prime + c2_prime) - dC_prime = c2_prime - c1_prime + # hue term + h_diff = h2 - h1 + h_sum = h1 + h2 + CC = C1 * C2 - h1_prime = _arctan2pi(b1, a1_prime) - h2_prime = _arctan2pi(b2, a2_prime) + dH = h_diff.copy() + dH[h_diff > np.pi] -= 2 * np.pi + dH[h_diff < -np.pi] += 2 * np.pi + dH[CC == 0.] = 0. # if r == 0, dtheta == 0 + dH_term = 2 * np.sqrt(CC) * np.sin(dH / 2) - dh_prime = h2_prime - h1_prime - - cc = c1_prime * c2_prime - mask1 = cc == 0. - mask2 = np.logical_and(-mask1, dh_prime > np.pi) - mask3 = np.logical_and(-mask1, dh_prime < -np.pi) - dh_prime = np.where(mask1, 0., dh_prime) - dh_prime += np.where(mask2, 2 * np.pi, 0) - dh_prime -= np.where(mask3, 2 * np.pi, 0) - - dH_prime = 2 * np.sqrt(cc) * np.sin(dh_prime / 2) - - Hbar_prime = h1_prime + h2_prime - mask0 = np.logical_and(np.abs(h1_prime - h2_prime) > np.pi, cc != 0.) - mask1 = np.logical_and(mask0, Hbar_prime < 2 * np.pi) - mask2 = np.logical_and(mask0, Hbar_prime >= 2 * np.pi) - - Hbar_prime += np.where(mask1, 2 * np.pi, 0) - Hbar_prime -= np.where(mask2, 2 * np.pi, 0) - Hbar_prime *= np.where(cc == 0., 2, 1) - Hbar_prime *= 0.5 + Hbar = h_sum.copy() + mask = np.logical_and(CC != 0., np.abs(h_diff) > np.pi) + Hbar[mask * (h_sum < 2 * np.pi)] += 2 * np.pi + Hbar[mask * (h_sum >= 2 * np.pi)] -= 2 * np.pi + Hbar[CC == 0.] *= 2 + Hbar *= 0.5 T = (1 - - 0.17 * np.cos(Hbar_prime - np.deg2rad(30)) + - 0.24 * np.cos(2 * Hbar_prime) + - 0.32 * np.cos(3 * Hbar_prime + np.deg2rad(6)) - - 0.20 * np.cos(4 * Hbar_prime - np.deg2rad(63)) + 0.17 * np.cos(Hbar - np.deg2rad(30)) + + 0.24 * np.cos(2 * Hbar) + + 0.32 * np.cos(3 * Hbar + np.deg2rad(6)) - + 0.20 * np.cos(4 * Hbar - np.deg2rad(63)) ) - dTheta = (np.deg2rad(30) * - np.exp(-((np.rad2deg(Hbar_prime) - 275) / 25) ** 2) - ) - c7 = cbar_prime ** 7 + SH = 1 + 0.015 * Cbar * T + + H_term = dH_term / (kH * SH) + + # hue rotation + c7 = Cbar ** 7 Rc = 2 * np.sqrt(c7 / (c7 + 25 ** 7)) + dtheta = np.deg2rad(30) * np.exp(-((np.rad2deg(Hbar) - 275) / 25) ** 2) + R_term = -np.sin(2 * dtheta) * Rc * C_term * H_term - term = (Lbar - 50) ** 2 - SL = 1 + 0.015 * term / np.sqrt(20 + term) - SC = 1 + 0.045 * cbar_prime - SH = 1 + 0.015 * cbar_prime * T - - RT = -np.sin(2 * dTheta) * Rc - - l_term = dL_prime / (kL * SL) - c_term = dC_prime / (kC * SC) - h_term = dH_prime / (kH * SH) - r_term = RT * c_term * h_term - - dE2 = l_term ** 2 - dE2 += c_term ** 2 - dE2 += h_term ** 2 - dE2 += r_term + # put it all together + dE2 = L_term ** 2 + dE2 += C_term ** 2 + dE2 += H_term ** 2 + dE2 += R_term return np.sqrt(dE2) @@ -277,27 +268,22 @@ def deltaE_cmc(lab1, lab2, kL=1, kC=1): JPC79 colour-difference formula," J. Soc. Dyers Colour. 100, 128-132 (1984). """ - l1, a1, b1 = np.rollaxis(lab1, -1)[:3] - l2, a2, b2 = np.rollaxis(lab2, -1)[:3] + L1, C1, h1 = np.rollaxis(lab2lch(lab1), -1)[:3] + L2, C2, h2 = np.rollaxis(lab2lch(lab2), -1)[:3] - c1 = np.hypot(a1, b1) - c2 = np.hypot(a2, b2) - dC = c1 - c2 - dl = l1 - l2 - dH = np.sqrt(deltaE_cie76(lab1, lab2) ** 2 - dl ** 2 - dC ** 2) + dC = C1 - C2 + dL = L1 - L2 + dH = np.sqrt(deltaE_cie76(lab1, lab2) ** 2 - dL ** 2 - dC ** 2) - dL = l1 - l2 - - h1 = _arctan2pi(b1, a1) T = np.where(np.logical_and(np.rad2deg(h1) >= 164, np.rad2deg(h1) <= 345), 0.56 + 0.2 * np.abs(np.cos(h1 + np.deg2rad(168))), 0.36 + 0.4 * np.abs(np.cos(h1 + np.deg2rad(35))) ) - c1_4 = c1 ** 4 + c1_4 = C1 ** 4 F = np.sqrt(c1_4 / (c1_4 + 1900)) - SL = np.where(l1 < 16, 0.511, 0.040975 * l1 / (1. + 0.01765 * l1)) - SC = 0.638 + 0.0638 * c1 / (1. + 0.0131 * c1) + SL = np.where(L1 < 16, 0.511, 0.040975 * L1 / (1. + 0.01765 * L1)) + SC = 0.638 + 0.0638 * C1 / (1. + 0.0131 * C1) SH = SC * (F * T + 1 - F) dE2 = (dL / (kL * SL)) ** 2 From efe51c92a9f95ffc6ba8ede300abca9036b93d1b Mon Sep 17 00:00:00 2001 From: Matt Terry Date: Mon, 5 Aug 2013 12:43:42 -0700 Subject: [PATCH 384/736] tuples are valid inputs --- skimage/color/colorconv.py | 1 + skimage/color/delta_e.py | 18 +++++++++++++++++- skimage/color/tests/test_delta_e.py | 24 ++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/skimage/color/colorconv.py b/skimage/color/colorconv.py index b4b9b4c6..dde30269 100644 --- a/skimage/color/colorconv.py +++ b/skimage/color/colorconv.py @@ -1136,6 +1136,7 @@ def _prepare_lab_array(arr): Arrays must be in floating point and have at least 3 elements in last dimension. Return a new array. """ + arr = np.asarray(arr) shape = arr.shape assert shape[-1] >= 3 return dtype.img_as_float(arr, force_copy=True) diff --git a/skimage/color/delta_e.py b/skimage/color/delta_e.py index 602e8b66..e3f29a34 100644 --- a/skimage/color/delta_e.py +++ b/skimage/color/delta_e.py @@ -45,6 +45,8 @@ def deltaE_cie76(lab1, lab2): .. [2] A. R. Robertson, "The CIE 1976 color-difference formulae," Color Res. Appl. 2, 7-11 (1977). """ + lab1 = np.asarray(lab1) + lab2 = np.asarray(lab2) L1, a1, b1 = np.rollaxis(lab1, -1)[:3] L2, a2, b2 = np.rollaxis(lab2, -1)[:3] return np.sqrt((L2 - L1) ** 2 + (a2 - a1) ** 2 + (b2 - b1) ** 2) @@ -99,6 +101,8 @@ def deltaE_ciede94(lab1, lab2, kH=1, kC=1, kL=1, k1=0.045, k2=0.015): .. [1] http://en.wikipedia.org/wiki/Color_difference .. [2] http://www.brucelindbloom.com/index.html?Eqn_DeltaE_CIE94.html """ + lab1 = np.asarray(lab1) + lab2 = np.asarray(lab2) L1, C1, h1 = np.rollaxis(lab2lch(lab1), -1)[:3] L2, C2, h2 = np.rollaxis(lab2lch(lab2), -1)[:3] @@ -156,6 +160,15 @@ def deltaE_ciede2000(lab1, lab2, kL=1, kC=1, kH=1): color metrics tested with an accurate color-difference tolerance dataset," Appl. Opt. 33, 8069-8077 (1994). """ + lab1 = np.asarray(lab1) + lab2 = np.asarray(lab2) + unroll = False + if lab1.ndim == 1 and lab2.ndim == 1: + unroll = True + if lab1.ndim == 1: + lab1 = lab1[None, :] + if lab2.ndim == 1: + lab2 = lab2[None, :] L1, a1, b1 = np.rollaxis(lab1, -1)[:3] L2, a2, b2 = np.rollaxis(lab2, -1)[:3] @@ -227,7 +240,10 @@ def deltaE_ciede2000(lab1, lab2, kL=1, kC=1, kH=1): dE2 += C_term ** 2 dE2 += H_term ** 2 dE2 += R_term - return np.sqrt(dE2) + ans = np.sqrt(dE2) + if unroll: + ans = ans[0] + return ans def deltaE_cmc(lab1, lab2, kL=1, kC=1): diff --git a/skimage/color/tests/test_delta_e.py b/skimage/color/tests/test_delta_e.py index 4535a311..52799c9d 100644 --- a/skimage/color/tests/test_delta_e.py +++ b/skimage/color/tests/test_delta_e.py @@ -138,6 +138,30 @@ def test_cmc(): assert_allclose(dE2, oracle, rtol=1.e-8) +def test_single_color_cie76(): + lab1 = (0.5, 0.5, 0.5) + lab2 = (0.4, 0.4, 0.4) + deltaE_cie76(lab1, lab2) + + +def test_single_color_cidede94(): + lab1 = (0.5, 0.5, 0.5) + lab2 = (0.4, 0.4, 0.4) + deltaE_ciede94(lab1, lab2) + + +def test_single_color_ciede2000(): + lab1 = (0.5, 0.5, 0.5) + lab2 = (0.4, 0.4, 0.4) + deltaE_ciede2000(lab1, lab2) + + +def test_single_color_cmc(): + lab1 = (0.5, 0.5, 0.5) + lab2 = (0.4, 0.4, 0.4) + deltaE_cmc(lab1, lab2) + + if __name__ == "__main__": from numpy.testing import run_module_suite run_module_suite() From e59a75bb4a7a4bfd8228481b139986bec7fd8bfd Mon Sep 17 00:00:00 2001 From: Matt Terry Date: Mon, 5 Aug 2013 14:20:20 -0700 Subject: [PATCH 385/736] cleanup --- skimage/color/delta_e.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/skimage/color/delta_e.py b/skimage/color/delta_e.py index e3f29a34..fa8a5701 100644 --- a/skimage/color/delta_e.py +++ b/skimage/color/delta_e.py @@ -103,22 +103,21 @@ def deltaE_ciede94(lab1, lab2, kH=1, kC=1, kL=1, k1=0.045, k2=0.015): """ lab1 = np.asarray(lab1) lab2 = np.asarray(lab2) - L1, C1, h1 = np.rollaxis(lab2lch(lab1), -1)[:3] - L2, C2, h2 = np.rollaxis(lab2lch(lab2), -1)[:3] + L1, C1 = np.rollaxis(lab2lch(lab1), -1)[:2] + L2, C2 = np.rollaxis(lab2lch(lab2), -1)[:2] dL = L1 - L2 dC = C1 - C2 - dH_ab = np.sqrt(deltaE_cie76(lab1, lab2) ** 2 - dL ** 2 - dC ** 2) + dH = np.sqrt(deltaE_cie76(lab1, lab2) ** 2 - dL ** 2 - dC ** 2) SL = 1 SC = 1 + k1 * C1 SH = 1 + k2 * C1 - ans = ((dL / (kL * SL)) ** 2 + - (dC / (kC * SC)) ** 2 + - (dH_ab / (kH * SH)) ** 2 - ) - return np.sqrt(ans) + dE2 = (dL / (kL * SL)) ** 2 + dE2 += (dC / (kC * SC)) ** 2 + dE2 += (dH / (kH * SH)) ** 2 + return np.sqrt(dE2) def deltaE_ciede2000(lab1, lab2, kL=1, kC=1, kH=1): @@ -305,5 +304,4 @@ def deltaE_cmc(lab1, lab2, kL=1, kC=1): dE2 = (dL / (kL * SL)) ** 2 dE2 += (dC / (kC * SC)) ** 2 dE2 += (dH / SH) ** 2 - return np.sqrt(dE2) From 060a4dd2733b2701ec4eb29cf34c469889c4ea98 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Tue, 6 Aug 2013 03:02:58 +0530 Subject: [PATCH 386/736] Minor doc changes --- skimage/morphology/selem.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/skimage/morphology/selem.py b/skimage/morphology/selem.py index 9d466e84..8b63a798 100644 --- a/skimage/morphology/selem.py +++ b/skimage/morphology/selem.py @@ -252,9 +252,9 @@ def octagon(m, n, dtype=np.uint8): def star(a, dtype=np.uint8): """ - Generates a star shaped structuring element that is an overlap of square - of size `2*a + 1` with its 45 degree rotated version. The slanted sides - are 45 or 135 degrees to the horizontal axis. + Generates a star shaped structuring element that has 8 vertices and is an + overlap of square of size `2*a + 1` with its 45 degree rotated version. + The slanted sides are 45 or 135 degrees to the horizontal axis. Parameters ---------- From 67165363bfd3f8aaeed67a609137cd4bae91486e Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Tue, 6 Aug 2013 03:38:52 +0530 Subject: [PATCH 387/736] Test for Octagon --- skimage/morphology/tests/test_selem.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/skimage/morphology/tests/test_selem.py b/skimage/morphology/tests/test_selem.py index 9a8ca895..945a9ede 100644 --- a/skimage/morphology/tests/test_selem.py +++ b/skimage/morphology/tests/test_selem.py @@ -66,6 +66,21 @@ class TestSElem(): def test_selem_octahedron(self): self.strel_worker_3d("diamond-matlab-output.npz", selem.octahedron) + def test_selem_octagon(self): + expected_mask = array([[0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0]], dtype=uint8) + actual_mask = selem.octagon(5, 3) + assert_equal(expected_mask, actual_mask) + if __name__ == '__main__': np.testing.run_module_suite() From 55d7eaa87d07d32b46ba7f2b34255bd9896bfaab Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Tue, 6 Aug 2013 03:48:06 +0530 Subject: [PATCH 388/736] Improving the implementation of star --- skimage/morphology/selem.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/skimage/morphology/selem.py b/skimage/morphology/selem.py index 8b63a798..f85b3a95 100644 --- a/skimage/morphology/selem.py +++ b/skimage/morphology/selem.py @@ -276,18 +276,17 @@ def star(a, dtype=np.uint8): """ from . import convex_hull_image if a == 1: - bfilter = np.zeros((3, 3)) + bfilter = np.zeros((3, 3), dtype) bfilter[:] = 1 return bfilter m = 2 * a + 1 n = a / 2 - selem_square = np.zeros((m + 2 * n, m + 2 * n), dtype=np.uint8) + selem_square = np.zeros((m + 2 * n, m + 2 * n)) selem_square[n: m + n, n: m + n] = 1 - selem_triangle = np.zeros((m + 2 * n, m + 2 * n), dtype=np.uint8) - selem_triangle[(m + 2 * n - 1) / 2, 0] = 1 - selem_triangle[(m + 1) / 2, n - 1] = 1 - selem_triangle[(m + 4 * n - 3) / 2, n - 1] = 1 - selem_triangle = convex_hull_image(selem_triangle).astype(int) - selem_triangle += (selem_triangle[:, ::-1] + selem_triangle.T + - selem_triangle.T[::-1, :]) - return selem_square + selem_triangle + c = (m + 2 * n - 1) / 2 + selem_rotated = np.zeros((m + 2 * n, m + 2 * n)) + selem_rotated[0, c] = selem_rotated[-1, c] = selem_rotated[c, 0] = selem_rotated[c, -1] = 1 + selem_rotated = convex_hull_image(selem_rotated).astype(int) + selem = selem_square + selem_rotated + selem[selem > 0] = 1 + return selem.astype(dtype) From 8569d73d571a1685937f4464033fbffe92c5132d Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Tue, 6 Aug 2013 04:13:27 +0530 Subject: [PATCH 389/736] Adding namespace --- skimage/morphology/tests/test_selem.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/skimage/morphology/tests/test_selem.py b/skimage/morphology/tests/test_selem.py index 945a9ede..311f5a2e 100644 --- a/skimage/morphology/tests/test_selem.py +++ b/skimage/morphology/tests/test_selem.py @@ -81,6 +81,23 @@ class TestSElem(): actual_mask = selem.octagon(5, 3) assert_equal(expected_mask, actual_mask) + def test_selem_star(self): + expected_mask = array([[0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0]], dtype=uint8) + actual_mask = selem.star(4) + assert_equal(expected_mask, actual_mask) + if __name__ == '__main__': np.testing.run_module_suite() From 6455d9b6e9b6226c4bb35bbc2addf69aa2bee5a4 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Tue, 6 Aug 2013 04:14:40 +0530 Subject: [PATCH 390/736] Adding test for star --- skimage/morphology/tests/test_selem.py | 48 +++++++++++++------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/skimage/morphology/tests/test_selem.py b/skimage/morphology/tests/test_selem.py index 311f5a2e..a49cd026 100644 --- a/skimage/morphology/tests/test_selem.py +++ b/skimage/morphology/tests/test_selem.py @@ -67,34 +67,34 @@ class TestSElem(): self.strel_worker_3d("diamond-matlab-output.npz", selem.octahedron) def test_selem_octagon(self): - expected_mask = array([[0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0], - [0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0], - [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], - [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], - [0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0], - [0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0]], dtype=uint8) + expected_mask = np.array([[0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0]], dtype=np.uint8) actual_mask = selem.octagon(5, 3) assert_equal(expected_mask, actual_mask) def test_selem_star(self): - expected_mask = array([[0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0], - [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], - [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], - [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], - [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], - [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], - [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], - [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], - [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], - [0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0]], dtype=uint8) + expected_mask = np.array([[0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0]], dtype=np.uint8) actual_mask = selem.star(4) assert_equal(expected_mask, actual_mask) From d50af3f3bcca1f845072eab12421692345cc8008 Mon Sep 17 00:00:00 2001 From: Matt Terry Date: Mon, 5 Aug 2013 18:02:28 -0700 Subject: [PATCH 391/736] unneeded asarray --- skimage/color/delta_e.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/skimage/color/delta_e.py b/skimage/color/delta_e.py index fa8a5701..1a84bb60 100644 --- a/skimage/color/delta_e.py +++ b/skimage/color/delta_e.py @@ -101,8 +101,6 @@ def deltaE_ciede94(lab1, lab2, kH=1, kC=1, kL=1, k1=0.045, k2=0.015): .. [1] http://en.wikipedia.org/wiki/Color_difference .. [2] http://www.brucelindbloom.com/index.html?Eqn_DeltaE_CIE94.html """ - lab1 = np.asarray(lab1) - lab2 = np.asarray(lab2) L1, C1 = np.rollaxis(lab2lch(lab1), -1)[:2] L2, C2 = np.rollaxis(lab2lch(lab2), -1)[:2] From 7fda9000c700ec5509fc3d5a3b61c89b74e51d9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Tue, 6 Aug 2013 10:51:50 +0200 Subject: [PATCH 392/736] Add cached_property decorator --- skimage/_shared/utils.py | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/skimage/_shared/utils.py b/skimage/_shared/utils.py index ea631716..ae82bfbc 100644 --- a/skimage/_shared/utils.py +++ b/skimage/_shared/utils.py @@ -5,7 +5,7 @@ import sys from . import six -__all__ = ['deprecated', 'get_bound_method_class'] +__all__ = ['deprecated', 'cached_property', 'get_bound_method_class'] class deprecated(object): @@ -57,6 +57,38 @@ class deprecated(object): return wrapped +class cached_property(object): + """Decorator to use a function as a cached property. + + The function is only called the first time and each successive call returns + the cached result of the first call. + + class Foo(object): + + @cached_property + def foo(self): + return "Cached" + + Adapted from . + + """ + + def __init__(self, func, name=None, doc=None): + self.__name__ = name or func.__name__ + self.__module__ = func.__module__ + self.__doc__ = doc or func.__doc__ + self.func = func + + def __get__(self, obj, type=None): + if obj is None: + return self + value = obj.__dict__.get(self.__name__, _missing) + if value is _missing: + value = self.func(obj) + obj.__dict__[self.__name__] = value + return value + + def get_bound_method_class(m): """Return the class for a bound method. From 918332c4c6fc6a0d4090002c1ba158f231a3ab3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Tue, 6 Aug 2013 14:27:37 +0200 Subject: [PATCH 393/736] Refactor regionprops --- doc/examples/plot_regionprops.py | 24 +- skimage/measure/_regionprops.py | 773 ++++++++++++---------- skimage/measure/tests/test_regionprops.py | 109 ++- 3 files changed, 486 insertions(+), 420 deletions(-) diff --git a/doc/examples/plot_regionprops.py b/doc/examples/plot_regionprops.py index f96b3b8c..f675d11c 100644 --- a/doc/examples/plot_regionprops.py +++ b/doc/examples/plot_regionprops.py @@ -24,29 +24,23 @@ image[rr,cc] = 1 image = rotate(image, angle=15, order=0) label_img = label(image) -props = regionprops(label_img, [ - 'BoundingBox', - 'Centroid', - 'Orientation', - 'MajorAxisLength', - 'MinorAxisLength' -]) +regions = regionprops(label_img) plt.imshow(image) -for prop in props: - x0 = prop['Centroid'][1] - y0 = prop['Centroid'][0] - x1 = x0 + math.cos(prop['Orientation']) * 0.5 * prop['MajorAxisLength'] - y1 = y0 - math.sin(prop['Orientation']) * 0.5 * prop['MajorAxisLength'] - x2 = x0 - math.sin(prop['Orientation']) * 0.5 * prop['MinorAxisLength'] - y2 = y0 - math.cos(prop['Orientation']) * 0.5 * prop['MinorAxisLength'] +for props in regions: + y0, x0 = props.centroid + orientation = props.orientation + x1 = x0 + math.cos(orientation) * 0.5 * props.major_axis_length + y1 = y0 - math.sin(orientation) * 0.5 * props.major_axis_length + x2 = x0 - math.sin(orientation) * 0.5 * props.minor_axis_length + y2 = y0 - math.cos(orientation) * 0.5 * props.minor_axis_length plt.plot((x0, x1), (y0, y1), '-r', linewidth=2.5) plt.plot((x0, x2), (y0, y2), '-r', linewidth=2.5) plt.plot(x0, y0, '.g', markersize=15) - minr, minc, maxr, maxc = prop['BoundingBox'] + minr, minc, maxr, maxc = props.bbox bx = (minc, maxc, maxc, minc, minc) by = (minr, minr, maxr, maxr, minr) plt.plot(bx, by, '-b', linewidth=2.5) diff --git a/skimage/measure/_regionprops.py b/skimage/measure/_regionprops.py index 72d1e2f4..810a44a0 100644 --- a/skimage/measure/_regionprops.py +++ b/skimage/measure/_regionprops.py @@ -1,4 +1,5 @@ # coding: utf-8 +import warnings from math import sqrt, atan2, pi as PI import numpy as np from scipy import ndimage @@ -14,47 +15,308 @@ STREL_4 = np.array([[0, 1, 0], [1, 1, 1], [0, 1, 0]]) STREL_8 = np.ones((3, 3), 'int8') -PROPS = ( - 'Area', - 'BoundingBox', - 'CentralMoments', - 'Centroid', - 'ConvexArea', +PROPS = { + 'Area': 'area', + 'BoundingBox': 'bbox', + 'CentralMoments': 'central_moments', + 'Centroid': 'centroid', + 'ConvexArea': 'convex_area', # 'ConvexHull', - 'ConvexImage', - 'Coordinates', - 'Eccentricity', - 'EquivDiameter', - 'EulerNumber', - 'Extent', + 'ConvexImage': 'convex_image', + 'Coordinates': 'coords', + 'Eccentricity': 'eccentricity', + 'EquivDiameter': 'equivalent_diameter', + 'EulerNumber': 'euler_number', + 'Extent': 'extent', # 'Extrema', - 'FilledArea', - 'FilledImage', - 'HuMoments', - 'Image', - 'MajorAxisLength', - 'MaxIntensity', - 'MeanIntensity', - 'MinIntensity', - 'MinorAxisLength', - 'Moments', - 'NormalizedMoments', - 'Orientation', - 'Perimeter', + 'FilledArea': 'filled_area', + 'FilledImage': 'filled_image', + 'HuMoments': 'hu_moments', + 'Image': 'image', + 'MajorAxisLength': 'major_axis_length', + 'MaxIntensity': 'max_intensity', + 'MeanIntensity': 'mean_intensity', + 'MinIntensity': 'min_intensity', + 'MinorAxisLength': 'minor_axis_length', + 'Moments': 'moments', + 'NormalizedMoments': 'normalized_moments', + 'Orientation': 'orientation', + 'Perimeter': 'perimeter', # 'PixelIdxList', # 'PixelList', - 'Solidity', + 'Solidity': 'solidity', # 'SubarrayIdx' - 'WeightedCentralMoments', - 'WeightedCentroid', - 'WeightedHuMoments', - 'WeightedMoments', - 'WeightedNormalizedMoments' -) + 'WeightedCentralMoments': 'weighted_central_moments', + 'WeightedCentroid': 'weighted_centroid', + 'WeightedHuMoments': 'weighted_hu_moments', + 'WeightedMoments': 'weighted_moments', + 'WeightedNormalizedMoments': 'weighted_normalized_moments' +} -def regionprops(label_image, properties=['Area', 'Centroid'], - intensity_image=None): +class cached_property(object): + """Decorator to use a function as a cached property. + + The function is only called the first time and each successive call returns + the cached result of the first call. + + class Foo(object): + + @cached_property + def foo(self): + return "Cached" + + class Foo(object): + + def __init__(self): + self.cache_active = False + + @cached_property + def foo(self): + return "Not cached" + + Adapted from . + + """ + + def __init__(self, func, name=None, doc=None): + self.__name__ = name or func.__name__ + self.__module__ = func.__module__ + self.__doc__ = doc or func.__doc__ + self.func = func + + def __get__(self, obj, type=None): + if obj is None: + return self + + # call every time, if cache is not active + if not obj.__dict__.get('cache_active', True): + return self.func(obj) + + # try to retrieve from cache or call and store result in cache + try: + value = obj.__dict__[self.__name__] + except KeyError: + value = self.func(obj) + obj.__dict__[self.__name__] = value + return value + + +class _RegionProperties(object): + + def __init__(self, slice, label, label_image, intensity_image, + cache_active): + self._slice = slice + self.label = label + self._label_image = label_image + self._intensity_image = intensity_image + self.cache_active = cache_active + + @cached_property + def area(self): + """Number of pixels of region.""" + + return self.moments[0, 0] + + @cached_property + def bbox(self): + """Bounding box `(min_row, min_col, max_row, max_col)`""" + + return (self._slice[0].start, self._slice[1].start, + self._slice[0].stop, self._slice[1].stop) + + @cached_property + def centroid(self): + row, col = self.local_centroid + return row + self._slice[0].start, col + self._slice[1].start + + @cached_property + def central_moments(self): + row, col = self.local_centroid + return _moments.central_moments(self._image_double, row, col, 3) + + @cached_property + def convex_area(self): + return np.sum(self.convex_image) + + @cached_property + def convex_image(self): + return convex_hull_image(self.image) + + @cached_property + def coords(self): + rr, cc = np.nonzero(self.image) + return np.vstack((rr + self._slice[0].start, + cc + self._slice[1].start)).T + + @cached_property + def eccentricity(self): + l1, l2 = self.inertia_tensor_eigvals + if l1 == 0: + return 0 + return sqrt(1 - l2 / l1) + + @cached_property + def equivalent_diameter(self): + return sqrt(4 * self.moments[0, 0] / PI) + + @cached_property + def euler_number(self): + euler_array = self.filled_image != self.image + _, num = ndimage.label(euler_array, STREL_8) + return -num + + @cached_property + def extent(self): + rows, cols = self.image.shape + return self.moments[0, 0] / (rows * cols) + + @cached_property + def filled_area(self): + return np.sum(self.filled_image) + + @cached_property + def filled_image(self): + return ndimage.binary_fill_holes(self.image, STREL_8) + + @cached_property + def hu_moments(self): + return _moments.hu_moments(self.normalized_moments) + + @cached_property + def image(self): + return self._label_image[self._slice] == self.label + + @cached_property + def _image_double(self): + return self.image.astype(np.double) + + @cached_property + def inertia_tensor(self): + mu = self.central_moments + a = mu[2, 0] / mu[0, 0] + b = -mu[1, 1] / mu[0, 0] + c = mu[0, 2] / mu[0, 0] + return np.array([[a, b], [b, c]]) + + @cached_property + def inertia_tensor_eigvals(self): + a, b, b, c = self.inertia_tensor.flat + # eigen values of inertia tensor + l1 = (a + c) / 2 + sqrt(4 * b ** 2 + (a - c) ** 2) / 2 + l2 = (a + c) / 2 - sqrt(4 * b ** 2 + (a - c) ** 2) / 2 + return l1, l2 + + @cached_property + def intensity_image(self): + if self._intensity_image is None: + raise AttributeError('No intensity image specified.') + return self._intensity_image[self._slice] * self.image + + @cached_property + def _intensity_image_double(self): + return self.intensity_image.astype(np.double) + + @cached_property + def moments(self): + return _moments.central_moments(self._image_double, 0, 0, 3) + + @cached_property + def local_centroid(self): + m = self.moments + row = m[0, 1] / m[0, 0] + col = m[1, 0] / m[0, 0] + return row, col + + @cached_property + def max_intensity(self): + return np.max(self.intensity_image[self.image]) + + @cached_property + def mean_intensity(self): + return np.mean(self.intensity_image[self.image]) + + @cached_property + def min_intensity(self): + return np.min(self.intensity_image[self.image]) + + @cached_property + def major_axis_length(self): + l1, _ = self.inertia_tensor_eigvals + return 4 * sqrt(l1) + + @cached_property + def minor_axis_length(self): + _, l2 = self.inertia_tensor_eigvals + return 4 * sqrt(l2) + + @cached_property + def normalized_moments(self): + return _moments.normalized_moments(self.central_moments, 3) + + @cached_property + def orientation(self): + a, b, b, c = self.inertia_tensor.flat + b = -b + if a - c == 0: + if b > 0: + return -PI / 4. + else: + return PI / 4. + else: + return - 0.5 * atan2(2 * b, (a - c)) + + @cached_property + def perimeter(self): + return perimeter(self.image, 4) + + @cached_property + def solidity(self): + return self.moments[0, 0] / np.sum(self.convex_image) + + @cached_property + def weighted_central_moments(self): + row, col = self.weighted_local_centroid + return _moments.central_moments(self._intensity_image_double, + row, col, 3) + + @cached_property + def weighted_centroid(self): + row, col = self.weighted_local_centroid + return row + self._slice[0].start, col + self._slice[1].start + + @cached_property + def weighted_local_centroid(self): + m = self.weighted_moments + row = m[0, 1] / m[0, 0] + col = m[1, 0] / m[0, 0] + return row, col + + @cached_property + def weighted_hu_moments(self): + return _moments.hu_moments(self.weighted_normalized_moments) + + @cached_property + def weighted_moments(self): + return _moments.central_moments(self._intensity_image_double, 0, 0, 3) + + @cached_property + def weighted_normalized_moments(self): + return _moments.normalized_moments(self.weighted_central_moments, 3) + + def __getitem__(self, key): + value = getattr(self, key, None) + if value is not None: + return value + else: # backwards compatability + warnings.warn('Usage of deprecated property name.', + category=DeprecationWarning) + return getattr(self, PROPS[key]) + + + +def regionprops(label_image, properties=None, + intensity_image=None, cache=True): """Measure properties of labelled image regions. Parameters @@ -62,150 +324,128 @@ def regionprops(label_image, properties=['Area', 'Centroid'], label_image : (N, M) ndarray Labelled input image. properties : {'all', list} - Shape measurements to be determined for each labelled image region. - Default is `['Area', 'Centroid']`. The following properties can be - determined: + **Deprecated parameter** - * Area : int - Number of pixels of region. - - * BoundingBox : tuple - Bounding box `(min_row, min_col, max_row, max_col)` - - * CentralMoments : (3, 3) ndarray - Central moments (translation invariant) up to 3rd order. - - mu_ji = sum{ array(x, y) * (x - x_c)^j * (y - y_c)^i } - - where the sum is over the `x`, `y` coordinates of the region, - and `x_c` and `y_c` are the coordinates of the region's centroid. - - * Centroid : array - Centroid coordinate tuple `(row, col)`. - - * ConvexArea : int - Number of pixels of convex hull image. - - * ConvexImage : (H, J) ndarray - Binary convex hull image which has the same size as bounding box. - - * Coordinates : (N, 2) ndarray - Coordinate list `(row, col)` of the region. - - * Eccentricity : float - Eccentricity of the ellipse that has the same second-moments as the - region. The eccentricity is the ratio of the distance between its - minor and major axis length. The value is between 0 and 1. - - * EquivDiameter : float - The diameter of a circle with the same area as the region. - - * EulerNumber : int - Euler number of region. Computed as number of objects (= 1) - subtracted by number of holes (8-connectivity). - - * Extent : float - Ratio of pixels in the region to pixels in the total bounding box. - Computed as `Area / (rows*cols)` - - * FilledArea : int - Number of pixels of filled region. - - * FilledImage : (H, J) ndarray - Binary region image with filled holes which has the same size as - bounding box. - - * HuMoments : tuple - Hu moments (translation, scale and rotation invariant). - - * Image : (H, J) ndarray - Sliced binary region image which has the same size as bounding box. - - * MajorAxisLength : float - The length of the major axis of the ellipse that has the same - normalized second central moments as the region. - - * MaxIntensity: float - Value with the greatest intensity in the region. - - * MeanIntensity: float - Value with the mean intensity in the region. - - * MinIntensity: float - Value with the least intensity in the region. - - * MinorAxisLength : float - The length of the minor axis of the ellipse that has the same - normalized second central moments as the region. - - * Moments : (3, 3) ndarray - Spatial moments up to 3rd order. - - m_ji = sum{ array(x, y) * x^j * y^i } - - where the sum is over the `x`, `y` coordinates of the region. - - * NormalizedMoments : (3, 3) ndarray - Normalized moments (translation and scale invariant) up to 3rd - order. - - nu_ji = mu_ji / m_00^[(i+j)/2 + 1] - - where `m_00` is the zeroth spatial moment. - - * Orientation : float - Angle between the X-axis and the major axis of the ellipse that has - the same second-moments as the region. Ranging from `-pi/2` to - `pi/2` in counter-clockwise direction. - - * Perimeter : float - Perimeter of object which approximates the contour as a line - through the centers of border pixels using a 4-connectivity. - - * Solidity : float - Ratio of pixels in the region to pixels of the convex hull image. - - * WeightedCentralMoments : (3, 3) ndarray - Central moments (translation invariant) of intensity image up to - 3rd order. - - wmu_ji = sum{ array(x, y) * (x - x_c)^j * (y - y_c)^i } - - where the sum is over the `x`, `y` coordinates of the region, - and `x_c` and `y_c` are the coordinates of the region's centroid. - - * WeightedCentroid : array - Centroid coordinate tuple `(row, col)` weighted with intensity - image. - - * WeightedHuMoments : tuple - Hu moments (translation, scale and rotation invariant) of intensity - image. - - * WeightedMoments : (3, 3) ndarray - Spatial moments of intensity image up to 3rd order. - - wm_ji = sum{ array(x, y) * x^j * y^i } - - where the sum is over the `x`, `y` coordinates of the region. - - * WeightedNormalizedMoments : (3, 3) ndarray - Normalized moments (translation and scale invariant) of intensity - image up to 3rd order. - - wnu_ji = wmu_ji / wm_00^[(i+j)/2 + 1] - - where `wm_00` is the zeroth spatial moment (intensity-weighted - area). + This parameter is not needed any more since all properties are + determined dynamically. intensity_image : (N, M) ndarray, optional Intensity image with same size as labelled image. Default is None. + cache : bool, optional + Determine whether to cache calculated properties. The computation is + much faster for cached properties, whereas the memory consumption + increases. Returns ------- - properties : list of dicts - List containing a property dict for each region. The property dicts - contain all the specified properties plus a 'Label' field. + properties : list + List containing a properties for each region. The properties of each + region can be accessed as attributes and keys. + + Notes + ----- + The following properties can be accessed as attributes or keys: + + area : int + Number of pixels of region. + bbox : tuple + Bounding box `(min_row, min_col, max_row, max_col)` + central_moments : (3, 3) ndarray + Central moments (translation invariant) up to 3rd order:: + + mu_ji = sum{ array(x, y) * (x - x_c)^j * (y - y_c)^i } + + where the sum is over the `x`, `y` coordinates of the region, + and `x_c` and `y_c` are the coordinates of the region's centroid. + centroid : array + Centroid coordinate tuple `(row, col)`. + convex_area : int + Number of pixels of convex hull image. + convex_image : (H, J) ndarray + Binary convex hull image which has the same size as bounding box. + coords : (N, 2) ndarray + Coordinate list `(row, col)` of the region. + eccentricity : float + Eccentricity of the ellipse that has the same second-moments as the + region. The eccentricity is the ratio of the distance between its + minor and major axis length. The value is between 0 and 1. + equivalent_diameter : float + The diameter of a circle with the same area as the region. + euler_number : int + Euler number of region. Computed as number of objects (= 1) + subtracted by number of holes (8-connectivity). + extent : float + Ratio of pixels in the region to pixels in the total bounding box. + Computed as `Area / (rows*cols)` + filled_area : int + Number of pixels of filled region. + filled_image : (H, J) ndarray + Binary region image with filled holes which has the same size as + bounding box. + hu_moments : tuple + Hu moments (translation, scale and rotation invariant). + image : (H, J) ndarray + Sliced binary region image which has the same size as bounding box. + major_axis_length : float + The length of the major axis of the ellipse that has the same + normalized second central moments as the region. + min_intensity : float + Value with the greatest intensity in the region. + mean_intensity : float + Value with the mean intensity in the region. + min_intensity : float + Value with the least intensity in the region. + minor_axis_length : float + The length of the minor axis of the ellipse that has the same + normalized second central moments as the region. + moments : (3, 3) ndarray + Spatial moments up to 3rd order:: + + m_ji = sum{ array(x, y) * x^j * y^i } + + where the sum is over the `x`, `y` coordinates of the region. + normalized_moments : (3, 3) ndarray + Normalized moments (translation and scale invariant) up to 3rd order:: + + nu_ji = mu_ji / m_00^[(i+j)/2 + 1] + + where `m_00` is the zeroth spatial moment. + orientation : float + Angle between the X-axis and the major axis of the ellipse that has + the same second-moments as the region. Ranging from `-pi/2` to + `pi/2` in counter-clockwise direction. + perimeter : float + Perimeter of object which approximates the contour as a line + through the centers of border pixels using a 4-connectivity. + solidity : float + Ratio of pixels in the region to pixels of the convex hull image. + weighted_central_moments : (3, 3) ndarray + Central moments (translation invariant) of intensity image up to + 3rd order:: + + wmu_ji = sum{ array(x, y) * (x - x_c)^j * (y - y_c)^i } + + where the sum is over the `x`, `y` coordinates of the region, + and `x_c` and `y_c` are the coordinates of the region's centroid. + weighted_centroid : array + Centroid coordinate tuple `(row, col)` weighted with intensity + image. + weighted_hu_moments : tuple + Hu moments (translation, scale and rotation invariant) of intensity + image. + weighted_moments : (3, 3) ndarray + Spatial moments of intensity image up to 3rd order:: + + wm_ji = sum{ array(x, y) * x^j * y^i } + + where the sum is over the `x`, `y` coordinates of the region. + weighted_normalized_moments : (3, 3) ndarray + Normalized moments (translation and scale invariant) of intensity + image up to 3rd order:: + + wnu_ji = wmu_ji / wm_00^[(i+j)/2 + 1] + + where `wm_00` is the zeroth spatial moment (intensity-weighted area). References ---------- @@ -225,194 +465,29 @@ def regionprops(label_image, properties=['Area', 'Centroid'], >>> img = coins() > 110 >>> label_img = label(img) >>> props = regionprops(label_img) - >>> props[0]['Centroid'] # centroid of first labelled object + >>> props[0].centroid # centroid of first labelled object + >>> props[0]['centroid'] # centroid of first labelled object """ if not np.issubdtype(label_image.dtype, 'int'): - raise TypeError('labelled image must be of integer dtype') + raise TypeError('Labelled image must be of integer dtype.') - # determine all properties if nothing specified - if properties == 'all': - properties = PROPS + if properties is not None: + warnings.warn('The ``properties`` argument is deprecated and is ' + 'not needed any more as properties are ' + 'determined dynamically.', + category=DeprecationWarning) - props = [] + regions = [] objects = ndimage.find_objects(label_image) for i, sl in enumerate(objects): label = i + 1 - # create property dict for current label - obj_props = {} - props.append(obj_props) + props = _RegionProperties(sl, label, label_image, + intensity_image, cache) + regions.append(props) - obj_props['Label'] = label - - array = (label_image[sl] == label).astype('double') - - # upper left corner of object bbox - r0 = sl[0].start - c0 = sl[1].start - - m = _moments.central_moments(array, 0, 0, 3) - # centroid - cr = m[0, 1] / m[0, 0] - cc = m[1, 0] / m[0, 0] - mu = _moments.central_moments(array, cr, cc, 3) - - # elements of the inertia tensor [a b; b c] - a = mu[2, 0] / mu[0, 0] - b = mu[1, 1] / mu[0, 0] - c = mu[0, 2] / mu[0, 0] - # eigen values of inertia tensor - l1 = (a + c) / 2 + sqrt(4 * b ** 2 + (a - c) ** 2) / 2 - l2 = (a + c) / 2 - sqrt(4 * b ** 2 + (a - c) ** 2) / 2 - - # cached results which are used by several properties - _filled_image = None - _convex_image = None - _nu = None - - if 'Area' in properties: - obj_props['Area'] = m[0, 0] - - if 'BoundingBox' in properties: - obj_props['BoundingBox'] = (r0, c0, sl[0].stop, sl[1].stop) - - if 'Centroid' in properties: - obj_props['Centroid'] = cr + r0, cc + c0 - - if 'CentralMoments' in properties: - obj_props['CentralMoments'] = mu - - if 'ConvexArea' in properties: - if _convex_image is None: - _convex_image = convex_hull_image(array) - obj_props['ConvexArea'] = np.sum(_convex_image) - - if 'ConvexImage' in properties: - if _convex_image is None: - _convex_image = convex_hull_image(array) - obj_props['ConvexImage'] = _convex_image - - if 'Coordinates' in properties: - rr, cc = np.nonzero(array) - obj_props['Coordinates'] = np.vstack((rr + r0, cc + c0)).T - - if 'Eccentricity' in properties: - if l1 == 0: - obj_props['Eccentricity'] = 0 - else: - obj_props['Eccentricity'] = sqrt(1 - l2 / l1) - - if 'EquivDiameter' in properties: - obj_props['EquivDiameter'] = sqrt(4 * m[0, 0] / PI) - - if 'EulerNumber' in properties: - if _filled_image is None: - _filled_image = ndimage.binary_fill_holes(array, STREL_8) - euler_array = _filled_image != array - _, num = ndimage.label(euler_array, STREL_8) - obj_props['EulerNumber'] = - num - - if 'Extent' in properties: - obj_props['Extent'] = m[0, 0] / (array.shape[0] * array.shape[1]) - - if 'HuMoments' in properties: - if _nu is None: - _nu = _moments.normalized_moments(mu, 3) - obj_props['HuMoments'] = _moments.hu_moments(_nu) - - if 'Image' in properties: - obj_props['Image'] = array - - if 'FilledArea' in properties: - if _filled_image is None: - _filled_image = ndimage.binary_fill_holes(array, STREL_8) - obj_props['FilledArea'] = np.sum(_filled_image) - - if 'FilledImage' in properties: - if _filled_image is None: - _filled_image = ndimage.binary_fill_holes(array, STREL_8) - obj_props['FilledImage'] = _filled_image - - if 'MajorAxisLength' in properties: - obj_props['MajorAxisLength'] = 4 * sqrt(l1) - - if 'MinorAxisLength' in properties: - obj_props['MinorAxisLength'] = 4 * sqrt(l2) - - if 'Moments' in properties: - obj_props['Moments'] = m - - if 'NormalizedMoments' in properties: - if _nu is None: - _nu = _moments.normalized_moments(mu, 3) - obj_props['NormalizedMoments'] = _nu - - if 'Orientation' in properties: - if a - c == 0: - if b > 0: - obj_props['Orientation'] = -PI / 4. - else: - obj_props['Orientation'] = PI / 4. - else: - obj_props['Orientation'] = - 0.5 * atan2(2 * b, (a - c)) - - if 'Perimeter' in properties: - obj_props['Perimeter'] = perimeter(array, 4) - - if 'Solidity' in properties: - if _convex_image is None: - _convex_image = convex_hull_image(array) - obj_props['Solidity'] = m[0, 0] / np.sum(_convex_image) - - if intensity_image is not None: - weighted_array = array * intensity_image[sl] - - wm = _moments.central_moments(weighted_array, 0, 0, 3) - # weighted centroid - wcr = wm[0, 1] / wm[0, 0] - wcc = wm[1, 0] / wm[0, 0] - wmu = _moments.central_moments(weighted_array, wcr, wcc, 3) - - # cached results which are used by several properties - _wnu = None - _vals = None - - if 'MaxIntensity' in properties: - if _vals is None: - _vals = weighted_array[array.astype('bool')] - obj_props['MaxIntensity'] = np.max(_vals) - - if 'MeanIntensity' in properties: - if _vals is None: - _vals = weighted_array[array.astype('bool')] - obj_props['MeanIntensity'] = np.mean(_vals) - - if 'MinIntensity' in properties: - if _vals is None: - _vals = weighted_array[array.astype('bool')] - obj_props['MinIntensity'] = np.min(_vals) - - if 'WeightedCentralMoments' in properties: - obj_props['WeightedCentralMoments'] = wmu - - if 'WeightedCentroid' in properties: - obj_props['WeightedCentroid'] = wcr + r0, wcc + c0 - - if 'WeightedHuMoments' in properties: - if _wnu is None: - _wnu = _moments.normalized_moments(wmu, 3) - obj_props['WeightedHuMoments'] = _moments.hu_moments(_wnu) - - if 'WeightedMoments' in properties: - obj_props['WeightedMoments'] = wm - - if 'WeightedNormalizedMoments' in properties: - if _wnu is None: - _wnu = _moments.normalized_moments(wmu, 3) - obj_props['WeightedNormalizedMoments'] = _wnu - - return props + return regions def perimeter(image, neighbourhood=4): diff --git a/skimage/measure/tests/test_regionprops.py b/skimage/measure/tests/test_regionprops.py index c1396670..c515a3fd 100644 --- a/skimage/measure/tests/test_regionprops.py +++ b/skimage/measure/tests/test_regionprops.py @@ -22,33 +22,33 @@ INTENSITY_SAMPLE = SAMPLE.copy() INTENSITY_SAMPLE[1, 9:11] = 2 +def test_all_props(): + regions = regionprops(SAMPLE, 'all', INTENSITY_SAMPLE)[0] + for prop in PROPS: + regions[prop] + + def test_unsupported_dtype(): assert_raises(TypeError, regionprops, np.zeros((10, 10), dtype=np.double)) -def test_all_props(): - props = regionprops(SAMPLE, 'all', INTENSITY_SAMPLE)[0] - for prop in PROPS: - assert prop in props - - def test_area(): - area = regionprops(SAMPLE, ['Area'])[0]['Area'] + area = regionprops(SAMPLE)[0].area assert area == np.sum(SAMPLE) def test_bbox(): - bbox = regionprops(SAMPLE, ['BoundingBox'])[0]['BoundingBox'] + bbox = regionprops(SAMPLE)[0].bbox assert_array_almost_equal(bbox, (0, 0, SAMPLE.shape[0], SAMPLE.shape[1])) SAMPLE_mod = SAMPLE.copy() SAMPLE_mod[:, -1] = 0 - bbox = regionprops(SAMPLE_mod, ['BoundingBox'])[0]['BoundingBox'] + bbox = regionprops(SAMPLE_mod)[0].bbox assert_array_almost_equal(bbox, (0, 0, SAMPLE.shape[0], SAMPLE.shape[1]-1)) def test_central_moments(): - mu = regionprops(SAMPLE, ['CentralMoments'])[0]['CentralMoments'] + mu = regionprops(SAMPLE)[0].central_moments # determined with OpenCV assert_almost_equal(mu[0,2], 436.00000000000045) # different from OpenCV results, bug in OpenCV @@ -61,19 +61,19 @@ def test_central_moments(): def test_centroid(): - centroid = regionprops(SAMPLE, ['Centroid'])[0]['Centroid'] + centroid = regionprops(SAMPLE)[0].centroid # determined with MATLAB assert_array_almost_equal(centroid, (5.66666666666666, 9.444444444444444)) def test_convex_area(): - area = regionprops(SAMPLE, ['ConvexArea'])[0]['ConvexArea'] + area = regionprops(SAMPLE)[0].convex_area # determined with MATLAB assert area == 124 def test_convex_image(): - img = regionprops(SAMPLE, ['ConvexImage'])[0]['ConvexImage'] + img = regionprops(SAMPLE)[0].convex_image # determined with MATLAB ref = np.array( [[0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0], @@ -94,43 +94,43 @@ def test_coordinates(): sample = np.zeros((10, 10), dtype=np.int8) coords = np.array([[3, 2], [3, 3], [3, 4]]) sample[coords[:, 0], coords[:, 1]] = 1 - prop_coords = regionprops(sample, ['Coordinates'])[0]['Coordinates'] + prop_coords = regionprops(sample)[0].coords assert_array_equal(prop_coords, coords) def test_eccentricity(): - eps = regionprops(SAMPLE, ['Eccentricity'])[0]['Eccentricity'] + eps = regionprops(SAMPLE)[0].eccentricity assert_almost_equal(eps, 0.814629313427) img = np.zeros((5, 5), dtype=np.int) img[2, 2] = 1 - eps = regionprops(img, ['Eccentricity'])[0]['Eccentricity'] + eps = regionprops(img)[0].eccentricity assert_almost_equal(eps, 0) def test_equiv_diameter(): - diameter = regionprops(SAMPLE, ['EquivDiameter'])[0]['EquivDiameter'] + diameter = regionprops(SAMPLE)[0].equivalent_diameter # determined with MATLAB assert_almost_equal(diameter, 9.57461472963) def test_euler_number(): - en = regionprops(SAMPLE, ['EulerNumber'])[0]['EulerNumber'] + en = regionprops(SAMPLE)[0].euler_number assert en == 0 SAMPLE_mod = SAMPLE.copy() SAMPLE_mod[7, -3] = 0 - en = regionprops(SAMPLE_mod, ['EulerNumber'])[0]['EulerNumber'] + en = regionprops(SAMPLE_mod)[0].euler_number assert en == -1 def test_extent(): - extent = regionprops(SAMPLE, ['Extent'])[0]['Extent'] + extent = regionprops(SAMPLE)[0].extent assert_almost_equal(extent, 0.4) def test_hu_moments(): - hu = regionprops(SAMPLE, ['HuMoments'])[0]['HuMoments'] + hu = regionprops(SAMPLE)[0].hu_moments ref = np.array([ 3.27117627e-01, 2.63869194e-02, @@ -145,59 +145,59 @@ def test_hu_moments(): def test_image(): - img = regionprops(SAMPLE, ['Image'])[0]['Image'] + img = regionprops(SAMPLE)[0].image assert_array_equal(img, SAMPLE) def test_filled_area(): - area = regionprops(SAMPLE, ['FilledArea'])[0]['FilledArea'] + area = regionprops(SAMPLE)[0].filled_area assert area == np.sum(SAMPLE) SAMPLE_mod = SAMPLE.copy() SAMPLE_mod[7, -3] = 0 - area = regionprops(SAMPLE_mod, ['FilledArea'])[0]['FilledArea'] + area = regionprops(SAMPLE_mod)[0].filled_area assert area == np.sum(SAMPLE) def test_filled_image(): - img = regionprops(SAMPLE, ['FilledImage'])[0]['FilledImage'] + img = regionprops(SAMPLE)[0].filled_image assert_array_equal(img, SAMPLE) def test_major_axis_length(): - length = regionprops(SAMPLE, ['MajorAxisLength'])[0]['MajorAxisLength'] + length = regionprops(SAMPLE)[0].major_axis_length # MATLAB has different interpretation of ellipse than found in literature, # here implemented as found in literature assert_almost_equal(length, 16.7924234999) def test_max_intensity(): - intensity = regionprops(SAMPLE, ['MaxIntensity'], INTENSITY_SAMPLE - )[0]['MaxIntensity'] + intensity = regionprops(SAMPLE, intensity_image=INTENSITY_SAMPLE + )[0].max_intensity assert_almost_equal(intensity, 2) def test_mean_intensity(): - intensity = regionprops(SAMPLE, ['MeanIntensity'], INTENSITY_SAMPLE - )[0]['MeanIntensity'] + intensity = regionprops(SAMPLE, intensity_image=INTENSITY_SAMPLE + )[0].mean_intensity assert_almost_equal(intensity, 1.02777777777777) def test_min_intensity(): - intensity = regionprops(SAMPLE, ['MinIntensity'], INTENSITY_SAMPLE - )[0]['MinIntensity'] + intensity = regionprops(SAMPLE, intensity_image=INTENSITY_SAMPLE + )[0].min_intensity assert_almost_equal(intensity, 1) def test_minor_axis_length(): - length = regionprops(SAMPLE, ['MinorAxisLength'])[0]['MinorAxisLength'] + length = regionprops(SAMPLE)[0].minor_axis_length # MATLAB has different interpretation of ellipse than found in literature, # here implemented as found in literature assert_almost_equal(length, 9.739302807263) def test_moments(): - m = regionprops(SAMPLE, ['Moments'])[0]['Moments'] + m = regionprops(SAMPLE)[0].moments # determined with OpenCV assert_almost_equal(m[0,0], 72.0) assert_almost_equal(m[0,1], 408.0) @@ -212,7 +212,7 @@ def test_moments(): def test_normalized_moments(): - nu = regionprops(SAMPLE, ['NormalizedMoments'])[0]['NormalizedMoments'] + nu = regionprops(SAMPLE)[0].normalized_moments # determined with OpenCV assert_almost_equal(nu[0,2], 0.08410493827160502) assert_almost_equal(nu[1,1], -0.016846707818929982) @@ -223,29 +223,26 @@ def test_normalized_moments(): def test_orientation(): - orientation = regionprops(SAMPLE, ['Orientation'])[0]['Orientation'] + orientation = regionprops(SAMPLE)[0].orientation # determined with MATLAB assert_almost_equal(orientation, 0.10446844651921) # test correct quadrant determination - orientation2 = regionprops(SAMPLE.T, ['Orientation'])[0]['Orientation'] + orientation2 = regionprops(SAMPLE.T)[0].orientation assert_almost_equal(orientation2, math.pi / 2 - orientation) # test diagonal regions diag = np.eye(10, dtype=int) - orientation_diag = regionprops(diag, ['Orientation'])[0]['Orientation'] + orientation_diag = regionprops(diag)[0].orientation assert_almost_equal(orientation_diag, -math.pi / 4) - orientation_diag = regionprops(np.flipud(diag), ['Orientation'] - )[0]['Orientation'] + orientation_diag = regionprops(np.flipud(diag))[0].orientation assert_almost_equal(orientation_diag, math.pi / 4) - orientation_diag = regionprops(np.fliplr(diag), ['Orientation'] - )[0]['Orientation'] + orientation_diag = regionprops(np.fliplr(diag))[0].orientation assert_almost_equal(orientation_diag, math.pi / 4) - orientation_diag = regionprops(np.fliplr(np.flipud(diag)), ['Orientation'] - )[0]['Orientation'] + orientation_diag = regionprops(np.fliplr(np.flipud(diag)))[0].orientation assert_almost_equal(orientation_diag, -math.pi / 4) def test_perimeter(): - per = regionprops(SAMPLE, ['Perimeter'])[0]['Perimeter'] + per = regionprops(SAMPLE)[0].perimeter assert_almost_equal(per, 55.2487373415) per = perimeter(SAMPLE.astype('double'), neighbourhood=8) @@ -253,14 +250,14 @@ def test_perimeter(): def test_solidity(): - solidity = regionprops(SAMPLE, ['Solidity'])[0]['Solidity'] + solidity = regionprops(SAMPLE)[0].solidity # determined with MATLAB assert_almost_equal(solidity, 0.580645161290323) def test_weighted_central_moments(): - wmu = regionprops(SAMPLE, ['WeightedCentralMoments'], INTENSITY_SAMPLE - )[0]['WeightedCentralMoments'] + wmu = regionprops(SAMPLE, intensity_image=INTENSITY_SAMPLE + )[0].weighted_central_moments ref = np.array( [[ 7.4000000000e+01, -2.1316282073e-13, 4.7837837838e+02, -7.5943608473e+02], @@ -276,14 +273,14 @@ def test_weighted_central_moments(): def test_weighted_centroid(): - centroid = regionprops(SAMPLE, ['WeightedCentroid'], INTENSITY_SAMPLE - )[0]['WeightedCentroid'] + centroid = regionprops(SAMPLE, intensity_image=INTENSITY_SAMPLE + )[0].weighted_centroid assert_array_almost_equal(centroid, (5.540540540540, 9.445945945945)) def test_weighted_hu_moments(): - whu = regionprops(SAMPLE, ['WeightedHuMoments'], INTENSITY_SAMPLE - )[0]['WeightedHuMoments'] + whu = regionprops(SAMPLE, intensity_image=INTENSITY_SAMPLE + )[0].weighted_hu_moments ref = np.array([ 3.1750587329e-01, 2.1417517159e-02, @@ -297,8 +294,8 @@ def test_weighted_hu_moments(): def test_weighted_moments(): - wm = regionprops(SAMPLE, ['WeightedMoments'], INTENSITY_SAMPLE - )[0]['WeightedMoments'] + wm = regionprops(SAMPLE, intensity_image=INTENSITY_SAMPLE + )[0].weighted_moments ref = np.array( [[ 7.4000000000e+01, 4.1000000000e+02, 2.7500000000e+03, 1.9778000000e+04], @@ -313,8 +310,8 @@ def test_weighted_moments(): def test_weighted_normalized_moments(): - wnu = regionprops(SAMPLE, ['WeightedNormalizedMoments'], INTENSITY_SAMPLE - )[0]['WeightedNormalizedMoments'] + wnu = regionprops(SAMPLE, intensity_image=INTENSITY_SAMPLE + )[0].weighted_normalized_moments ref = np.array( [[ np.nan, np.nan, 0.0873590903, -0.0161217406], [ np.nan, -0.0160405109, -0.0031421072, -0.0031376984], From d94c25efde905581306a3cf230ce7639819124d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Tue, 6 Aug 2013 14:28:28 +0200 Subject: [PATCH 394/736] Remove cached_property class from shared utils --- skimage/_shared/utils.py | 32 -------------------------------- 1 file changed, 32 deletions(-) diff --git a/skimage/_shared/utils.py b/skimage/_shared/utils.py index ae82bfbc..5e2f7785 100644 --- a/skimage/_shared/utils.py +++ b/skimage/_shared/utils.py @@ -57,38 +57,6 @@ class deprecated(object): return wrapped -class cached_property(object): - """Decorator to use a function as a cached property. - - The function is only called the first time and each successive call returns - the cached result of the first call. - - class Foo(object): - - @cached_property - def foo(self): - return "Cached" - - Adapted from . - - """ - - def __init__(self, func, name=None, doc=None): - self.__name__ = name or func.__name__ - self.__module__ = func.__module__ - self.__doc__ = doc or func.__doc__ - self.func = func - - def __get__(self, obj, type=None): - if obj is None: - return self - value = obj.__dict__.get(self.__name__, _missing) - if value is _missing: - value = self.func(obj) - obj.__dict__[self.__name__] = value - return value - - def get_bound_method_class(m): """Return the class for a bound method. From 9f8e904b6671723b3fbbf2748737cc1a09f3916d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Tue, 6 Aug 2013 14:30:51 +0200 Subject: [PATCH 395/736] Add note about backwards compatibility to TODO.txt --- TODO.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/TODO.txt b/TODO.txt index 2067d036..7ad40875 100644 --- a/TODO.txt +++ b/TODO.txt @@ -3,6 +3,7 @@ Version 0.10 * Remove deprecated functions: - ``skimage.filter.rank.*`` * Remove deprecated parameter ``epsilon`` of ``skimage.viewer.LineProfile`` +* Remove backwards-compatability of ``skimage.measure.regionprops`` Version 0.9 ----------- From 01871c153d0425ed4af5fc5e6bc6192c86b2459c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Tue, 6 Aug 2013 14:34:36 +0200 Subject: [PATCH 396/736] Remove legacy doc strings --- skimage/measure/_regionprops.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/skimage/measure/_regionprops.py b/skimage/measure/_regionprops.py index 810a44a0..85c11416 100644 --- a/skimage/measure/_regionprops.py +++ b/skimage/measure/_regionprops.py @@ -114,14 +114,10 @@ class _RegionProperties(object): @cached_property def area(self): - """Number of pixels of region.""" - return self.moments[0, 0] @cached_property def bbox(self): - """Bounding box `(min_row, min_col, max_row, max_col)`""" - return (self._slice[0].start, self._slice[1].start, self._slice[0].stop, self._slice[1].stop) From 92e70547fa7d2cb7c12d66bff3fbe28db00dc766 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Tue, 6 Aug 2013 14:39:38 +0200 Subject: [PATCH 397/736] Use asbolute import for Cython lib --- skimage/measure/_regionprops.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/measure/_regionprops.py b/skimage/measure/_regionprops.py index 85c11416..90d4a25c 100644 --- a/skimage/measure/_regionprops.py +++ b/skimage/measure/_regionprops.py @@ -5,7 +5,7 @@ import numpy as np from scipy import ndimage from skimage.morphology import convex_hull_image -from . import _moments +from skimage.measure import _moments __all__ = ['regionprops'] From 909d4cb5f6ac2d051091c43c849475f1d1ae4aa1 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Tue, 6 Aug 2013 22:45:21 +1000 Subject: [PATCH 398/736] Add test to unique_rows for discontiguous arrays --- skimage/util/tests/test_unique_rows.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/skimage/util/tests/test_unique_rows.py b/skimage/util/tests/test_unique_rows.py index cfbaaf05..ad033b69 100644 --- a/skimage/util/tests/test_unique_rows.py +++ b/skimage/util/tests/test_unique_rows.py @@ -3,6 +3,14 @@ from numpy.testing import assert_equal, assert_raises from skimage.util import unique_rows +def test_discontiguous_array(): + ar = np.array([[1, 0, 1], [0, 1, 0], [1, 0, 1]], np.uint8) + ar = ar[::2] + ar_out = unique_rows(ar) + desired_ar_out = np.array([[1, 0, 1]], np.uint8) + assert_equal(ar_out, desired_ar_out) + + def test_uint8_array(): ar = np.array([[1, 0, 1], [0, 1, 0], [1, 0, 1]], np.uint8) ar_out = unique_rows(ar) From b40ef3ad7d461a9728522191f35ab3a08c608f0b Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Tue, 6 Aug 2013 22:45:52 +1000 Subject: [PATCH 399/736] Ensure array input to unique_rows is contiguous --- skimage/util/unique.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/skimage/util/unique.py b/skimage/util/unique.py index a09fadd9..6034d9a4 100644 --- a/skimage/util/unique.py +++ b/skimage/util/unique.py @@ -30,6 +30,11 @@ def unique_rows(ar): if ar.ndim != 2: raise ValueError("unique_rows() only makes sense for 2D arrays, " "got %dd" % ar.ndim) + # the view in the next line only works if the array is C-contiguous + ar = np.ascontiguousarray(ar) + # np.unique() finds identical items in a raveled array. To make it + # see each row as a single item, we create a view of each row as a + # byte string of length itemsize times number of columns in `ar` ar_row_view = ar.view('|S%d' % (ar.itemsize * ar.shape[1])) _, unique_row_indices = np.unique(ar_row_view, return_index=True) ar_out = ar[unique_row_indices] From a84388ccdba185db88f259cbfd0364f061105b12 Mon Sep 17 00:00:00 2001 From: Matt Terry Date: Tue, 6 Aug 2013 06:38:50 -0700 Subject: [PATCH 400/736] doc cleanup --- skimage/color/colorconv.py | 23 +++++++++-------------- skimage/color/delta_e.py | 31 ++++++++++++++++--------------- 2 files changed, 25 insertions(+), 29 deletions(-) diff --git a/skimage/color/colorconv.py b/skimage/color/colorconv.py index dde30269..5d41a063 100644 --- a/skimage/color/colorconv.py +++ b/skimage/color/colorconv.py @@ -1044,21 +1044,18 @@ def lab2lch(lab): ---------- lab : array_like The N-D image in CIE-LAB format. The last (`N+1`th) dimension must have - at least 3 elements, corresponding to the `L`, `a`, and `b` color + at least 3 elements, corresponding to the ``L``, ``a``, and ``b`` color channels. Subsequent elements are copied. Returns ------- out : ndarray - The image in LCH format, in a 3-D array of shape (.., .., 3). + The image in LCH format, in a N-D array with same shape as input `lab`. Raises ------ ValueError - If `rgb` is not a 3-D array of shape (.., .., 3). - - References - ---------- + If `lch` does not have at least 3 color channels (i.e. l, a, b). Notes ----- @@ -1096,23 +1093,20 @@ def lch2lab(lch): Parameters ---------- - lab : array_like + lch : array_like The N-D image in CIE-LCH format. The last (`N+1`th) dimension must have - at least 3 elements, corresponding to the `L`, `a`, and `b` color + at least 3 elements, corresponding to the ``L``, ``a``, and ``b`` color channels. Subsequent elements are copied. Returns ------- out : ndarray - The image in LAB format, in a 3-D array of shape (.., .., 3). + The image in LAB format, with same shape as input `lch`. Raises ------ ValueError - If `rgb` is not a 3-D array of shape (.., .., 3). - - References - ---------- + If `lch` does not have at least 3 color channels (i.e. l, c, h). Examples -------- @@ -1138,5 +1132,6 @@ def _prepare_lab_array(arr): """ arr = np.asarray(arr) shape = arr.shape - assert shape[-1] >= 3 + if shape[-1] < 3: + raise ValueError('Input array has less than 3 color channels') return dtype.img_as_float(arr, force_copy=True) diff --git a/skimage/color/delta_e.py b/skimage/color/delta_e.py index 1a84bb60..5ad6b3be 100644 --- a/skimage/color/delta_e.py +++ b/skimage/color/delta_e.py @@ -56,7 +56,7 @@ def deltaE_ciede94(lab1, lab2, kH=1, kC=1, kL=1, k1=0.045, k2=0.015): """Color difference according to CIEDE 94 standard Accommodates perceptual non-uniformities through the use of application - specific scale factors (kH, kC, kL, k1, and k2). + specific scale factors (`kH`, `kC`, `kL`, `k1`, and `k2`). Parameters ---------- @@ -87,14 +87,14 @@ def deltaE_ciede94(lab1, lab2, kH=1, kC=1, kL=1, k1=0.045, k2=0.015): color. Consequently, the first color should be regarded as the "reference" color. - kL, k1, k2 depend on the application and default to the values suggested - for graphic arts + `kL`, `k1`, `k2` depend on the application and default to the values + suggested for graphic arts Parameter Graphic Arts Textiles ---------- ------------- -------- - kL 1.000 2.000 - k1 0.045 0.048 - k2 0.015 0.014 + `kL` 1.000 2.000 + `k1` 0.045 0.048 + `k2` 0.015 0.014 References ---------- @@ -131,7 +131,7 @@ def deltaE_ciede2000(lab1, lab2, kL=1, kC=1, kH=1): lab2 : array_like comparison color (Lab colorspace) kL : float (range), optional - luminance scale factor, 1 for "acceptably close"; 2 for "imperceptible" + lightness scale factor, 1 for "acceptably close"; 2 for "imperceptible" see deltaE_cmc kC : float (range), optional chroma scale factor, usually 1 @@ -145,8 +145,8 @@ def deltaE_ciede2000(lab1, lab2, kL=1, kC=1, kH=1): Notes ----- - CIEDE 2000 assumes parametric weighting factors for the luminance, chroma, - and hue (kL, kC, kH respectively). These default to 1. + CIEDE 2000 assumes parametric weighting factors for the lightness, chroma, + and hue (`kL`, `kC`, `kH` respectively). These default to 1. References ---------- @@ -187,7 +187,7 @@ def deltaE_ciede2000(lab1, lab2, kL=1, kC=1, kH=1): # 3) Chroma term # 4) hue Rotation term - # luminance term + # lightness term Lbar = 0.5 * (L1 + L2) tmp = (Lbar - 50) ** 2 SL = 1 + 0.015 * tmp / np.sqrt(20 + tmp) @@ -250,10 +250,11 @@ def deltaE_cmc(lab1, lab2, kL=1, kC=1): (CMC) of the Society of Dyers and Colourists (United Kingdom). It is intended for use in the textile industry. - The scale factors kL, kC set the weight given to differences in lightness - and chroma relative to differences in hue. The usual values are kL=2, kC=1 - for "acceptability" and kL=1, kC=1 for "imperceptibility". Colors with - dE > 1 are "different" for the given scale factors. + The scale factors `kL`, `kC` set the weight given to differences in + lightness and chroma relative to differences in hue. The usual values are + ``kL=2``, ``kC=1`` for "acceptability" and ``kL=1``, ``kC=1`` for + "imperceptibility". Colors with ``dE > 1`` are "different" for the given + scale factors. Parameters ---------- @@ -271,7 +272,7 @@ def deltaE_cmc(lab1, lab2, kL=1, kC=1): ----- deltaE_cmc the defines the scales for the lightness, hue, and chroma in terms of the first color. Consequently - deltaE_cmc(lab1, lab2) != deltaE_cmc(lab2, lab1) + ``deltaE_cmc(lab1, lab2) != deltaE_cmc(lab2, lab1)`` References ---------- From 6e8dcd3ddd150b8eb26b054f3b6279fc85728b44 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Wed, 7 Aug 2013 12:12:23 +0530 Subject: [PATCH 401/736] Adding tests for smallest selem --- skimage/morphology/tests/test_selem.py | 66 +++++++++++++++----------- 1 file changed, 38 insertions(+), 28 deletions(-) diff --git a/skimage/morphology/tests/test_selem.py b/skimage/morphology/tests/test_selem.py index a49cd026..7895fa58 100644 --- a/skimage/morphology/tests/test_selem.py +++ b/skimage/morphology/tests/test_selem.py @@ -67,36 +67,46 @@ class TestSElem(): self.strel_worker_3d("diamond-matlab-output.npz", selem.octahedron) def test_selem_octagon(self): - expected_mask = np.array([[0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0], - [0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0], - [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], - [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], - [0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0], - [0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0]], dtype=np.uint8) - actual_mask = selem.octagon(5, 3) - assert_equal(expected_mask, actual_mask) + expected_mask1 = np.array([[0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0]], dtype=np.uint8) + actual_mask1 = selem.octagon(5, 3) + expected_mask2 = np.array([[0, 1, 0], + [1, 1, 1], + [0, 1, 0]], dtype=np.uint8) + actual_mask2 = selem.octagon(1, 1) + assert_equal(expected_mask1, actual_mask1) + assert_equal(expected_mask2, actual_mask2) def test_selem_star(self): - expected_mask = np.array([[0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0], - [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], - [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], - [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], - [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], - [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], - [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], - [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], - [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], - [0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0]], dtype=np.uint8) - actual_mask = selem.star(4) - assert_equal(expected_mask, actual_mask) + expected_mask1 = np.array([[0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0]], dtype=np.uint8) + actual_mask1 = selem.star(4) + expected_mask2 = np.array([[1, 1, 1], + [1, 1, 1], + [1, 1, 1]], dtype=np.uint8) + actual_mask2 = selem.star(1) + assert_equal(expected_mask1, actual_mask1) + assert_equal(expected_mask2, actual_mask2) if __name__ == '__main__': From 3b6e799d76dd14bace58b7501c51b8c297e67612 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Wed, 7 Aug 2013 19:38:41 +0200 Subject: [PATCH 402/736] Remove unused cached_property from _shared.utils namespace --- skimage/_shared/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/_shared/utils.py b/skimage/_shared/utils.py index 5e2f7785..ea631716 100644 --- a/skimage/_shared/utils.py +++ b/skimage/_shared/utils.py @@ -5,7 +5,7 @@ import sys from . import six -__all__ = ['deprecated', 'cached_property', 'get_bound_method_class'] +__all__ = ['deprecated', 'get_bound_method_class'] class deprecated(object): From fea0c3f56c7e49ceb4ac98ee91fb576b588aedfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Wed, 7 Aug 2013 20:04:19 +0200 Subject: [PATCH 403/736] Improve fromatting of regionprops doc string --- skimage/measure/_regionprops.py | 64 +++++++++++++++++---------------- 1 file changed, 34 insertions(+), 30 deletions(-) diff --git a/skimage/measure/_regionprops.py b/skimage/measure/_regionprops.py index 90d4a25c..0a3d5a67 100644 --- a/skimage/measure/_regionprops.py +++ b/skimage/measure/_regionprops.py @@ -342,80 +342,84 @@ def regionprops(label_image, properties=None, ----- The following properties can be accessed as attributes or keys: - area : int + **area** : int Number of pixels of region. - bbox : tuple + **bbox** : tuple Bounding box `(min_row, min_col, max_row, max_col)` - central_moments : (3, 3) ndarray + **central_moments** : (3, 3) ndarray Central moments (translation invariant) up to 3rd order:: mu_ji = sum{ array(x, y) * (x - x_c)^j * (y - y_c)^i } where the sum is over the `x`, `y` coordinates of the region, and `x_c` and `y_c` are the coordinates of the region's centroid. - centroid : array + **centroid** : array Centroid coordinate tuple `(row, col)`. - convex_area : int + **convex_area** : int Number of pixels of convex hull image. - convex_image : (H, J) ndarray + **convex_image** : (H, J) ndarray Binary convex hull image which has the same size as bounding box. - coords : (N, 2) ndarray + **coords** : (N, 2) ndarray Coordinate list `(row, col)` of the region. - eccentricity : float + **eccentricity** : float Eccentricity of the ellipse that has the same second-moments as the region. The eccentricity is the ratio of the distance between its minor and major axis length. The value is between 0 and 1. - equivalent_diameter : float + **equivalent_diameter** : float The diameter of a circle with the same area as the region. - euler_number : int + **euler_number** : int Euler number of region. Computed as number of objects (= 1) subtracted by number of holes (8-connectivity). - extent : float + **extent** : float Ratio of pixels in the region to pixels in the total bounding box. Computed as `Area / (rows*cols)` - filled_area : int + **filled_area** : int Number of pixels of filled region. - filled_image : (H, J) ndarray + **filled_image** : (H, J) ndarray Binary region image with filled holes which has the same size as bounding box. - hu_moments : tuple + **hu_moments** : tuple Hu moments (translation, scale and rotation invariant). - image : (H, J) ndarray + **image** : (H, J) ndarray Sliced binary region image which has the same size as bounding box. - major_axis_length : float + **inertia_tensor** : (2, 2) ndarray + Inertia tensor of the region for the rotation around its masss. + **inertia_tensor_eigvals** : tuple + The two eigen values of the inertia tensor in decreasing order. + **major_axis_length** : float The length of the major axis of the ellipse that has the same normalized second central moments as the region. - min_intensity : float + **min_intensity** : float Value with the greatest intensity in the region. - mean_intensity : float + **mean_intensity** : float Value with the mean intensity in the region. - min_intensity : float + **min_intensity** : float Value with the least intensity in the region. - minor_axis_length : float + **minor_axis_length** : float The length of the minor axis of the ellipse that has the same normalized second central moments as the region. - moments : (3, 3) ndarray + **moments** : (3, 3) ndarray Spatial moments up to 3rd order:: m_ji = sum{ array(x, y) * x^j * y^i } where the sum is over the `x`, `y` coordinates of the region. - normalized_moments : (3, 3) ndarray + **normalized_moments** : (3, 3) ndarray Normalized moments (translation and scale invariant) up to 3rd order:: nu_ji = mu_ji / m_00^[(i+j)/2 + 1] where `m_00` is the zeroth spatial moment. - orientation : float + **orientation** : float Angle between the X-axis and the major axis of the ellipse that has the same second-moments as the region. Ranging from `-pi/2` to `pi/2` in counter-clockwise direction. - perimeter : float + **perimeter** : float Perimeter of object which approximates the contour as a line through the centers of border pixels using a 4-connectivity. - solidity : float + **solidity** : float Ratio of pixels in the region to pixels of the convex hull image. - weighted_central_moments : (3, 3) ndarray + **weighted_central_moments** : (3, 3) ndarray Central moments (translation invariant) of intensity image up to 3rd order:: @@ -423,19 +427,19 @@ def regionprops(label_image, properties=None, where the sum is over the `x`, `y` coordinates of the region, and `x_c` and `y_c` are the coordinates of the region's centroid. - weighted_centroid : array + **weighted_centroid** : array Centroid coordinate tuple `(row, col)` weighted with intensity image. - weighted_hu_moments : tuple + **weighted_hu_moments** : tuple Hu moments (translation, scale and rotation invariant) of intensity image. - weighted_moments : (3, 3) ndarray + **weighted_moments** : (3, 3) ndarray Spatial moments of intensity image up to 3rd order:: wm_ji = sum{ array(x, y) * x^j * y^i } where the sum is over the `x`, `y` coordinates of the region. - weighted_normalized_moments : (3, 3) ndarray + **weighted_normalized_moments** : (3, 3) ndarray Normalized moments (translation and scale invariant) of intensity image up to 3rd order:: From 4f8d4ce3f227f7f176f42111b60cd17f60335dd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Wed, 7 Aug 2013 20:06:33 +0200 Subject: [PATCH 404/736] Hide cached_property and caching flag --- skimage/measure/_regionprops.py | 88 ++++++++++++++++----------------- 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/skimage/measure/_regionprops.py b/skimage/measure/_regionprops.py index 0a3d5a67..bfe8139c 100644 --- a/skimage/measure/_regionprops.py +++ b/skimage/measure/_regionprops.py @@ -54,7 +54,7 @@ PROPS = { } -class cached_property(object): +class _cached_property(object): """Decorator to use a function as a cached property. The function is only called the first time and each successive call returns @@ -62,16 +62,16 @@ class cached_property(object): class Foo(object): - @cached_property + @_cached_property def foo(self): return "Cached" class Foo(object): def __init__(self): - self.cache_active = False + self._cache_active = False - @cached_property + @_cached_property def foo(self): return "Not cached" @@ -90,7 +90,7 @@ class cached_property(object): return self # call every time, if cache is not active - if not obj.__dict__.get('cache_active', True): + if not obj.__dict__.get('_cache_active', True): return self.func(obj) # try to retrieve from cache or call and store result in cache @@ -106,88 +106,88 @@ class _RegionProperties(object): def __init__(self, slice, label, label_image, intensity_image, cache_active): - self._slice = slice self.label = label + self._slice = slice self._label_image = label_image self._intensity_image = intensity_image - self.cache_active = cache_active + self._cache_active = cache_active - @cached_property + @_cached_property def area(self): return self.moments[0, 0] - @cached_property + @_cached_property def bbox(self): return (self._slice[0].start, self._slice[1].start, self._slice[0].stop, self._slice[1].stop) - @cached_property + @_cached_property def centroid(self): row, col = self.local_centroid return row + self._slice[0].start, col + self._slice[1].start - @cached_property + @_cached_property def central_moments(self): row, col = self.local_centroid return _moments.central_moments(self._image_double, row, col, 3) - @cached_property + @_cached_property def convex_area(self): return np.sum(self.convex_image) - @cached_property + @_cached_property def convex_image(self): return convex_hull_image(self.image) - @cached_property + @_cached_property def coords(self): rr, cc = np.nonzero(self.image) return np.vstack((rr + self._slice[0].start, cc + self._slice[1].start)).T - @cached_property + @_cached_property def eccentricity(self): l1, l2 = self.inertia_tensor_eigvals if l1 == 0: return 0 return sqrt(1 - l2 / l1) - @cached_property + @_cached_property def equivalent_diameter(self): return sqrt(4 * self.moments[0, 0] / PI) - @cached_property + @_cached_property def euler_number(self): euler_array = self.filled_image != self.image _, num = ndimage.label(euler_array, STREL_8) return -num - @cached_property + @_cached_property def extent(self): rows, cols = self.image.shape return self.moments[0, 0] / (rows * cols) - @cached_property + @_cached_property def filled_area(self): return np.sum(self.filled_image) - @cached_property + @_cached_property def filled_image(self): return ndimage.binary_fill_holes(self.image, STREL_8) - @cached_property + @_cached_property def hu_moments(self): return _moments.hu_moments(self.normalized_moments) - @cached_property + @_cached_property def image(self): return self._label_image[self._slice] == self.label - @cached_property + @_cached_property def _image_double(self): return self.image.astype(np.double) - @cached_property + @_cached_property def inertia_tensor(self): mu = self.central_moments a = mu[2, 0] / mu[0, 0] @@ -195,7 +195,7 @@ class _RegionProperties(object): c = mu[0, 2] / mu[0, 0] return np.array([[a, b], [b, c]]) - @cached_property + @_cached_property def inertia_tensor_eigvals(self): a, b, b, c = self.inertia_tensor.flat # eigen values of inertia tensor @@ -203,54 +203,54 @@ class _RegionProperties(object): l2 = (a + c) / 2 - sqrt(4 * b ** 2 + (a - c) ** 2) / 2 return l1, l2 - @cached_property + @_cached_property def intensity_image(self): if self._intensity_image is None: raise AttributeError('No intensity image specified.') return self._intensity_image[self._slice] * self.image - @cached_property + @_cached_property def _intensity_image_double(self): return self.intensity_image.astype(np.double) - @cached_property + @_cached_property def moments(self): return _moments.central_moments(self._image_double, 0, 0, 3) - @cached_property + @_cached_property def local_centroid(self): m = self.moments row = m[0, 1] / m[0, 0] col = m[1, 0] / m[0, 0] return row, col - @cached_property + @_cached_property def max_intensity(self): return np.max(self.intensity_image[self.image]) - @cached_property + @_cached_property def mean_intensity(self): return np.mean(self.intensity_image[self.image]) - @cached_property + @_cached_property def min_intensity(self): return np.min(self.intensity_image[self.image]) - @cached_property + @_cached_property def major_axis_length(self): l1, _ = self.inertia_tensor_eigvals return 4 * sqrt(l1) - @cached_property + @_cached_property def minor_axis_length(self): _, l2 = self.inertia_tensor_eigvals return 4 * sqrt(l2) - @cached_property + @_cached_property def normalized_moments(self): return _moments.normalized_moments(self.central_moments, 3) - @cached_property + @_cached_property def orientation(self): a, b, b, c = self.inertia_tensor.flat b = -b @@ -262,41 +262,41 @@ class _RegionProperties(object): else: return - 0.5 * atan2(2 * b, (a - c)) - @cached_property + @_cached_property def perimeter(self): return perimeter(self.image, 4) - @cached_property + @_cached_property def solidity(self): return self.moments[0, 0] / np.sum(self.convex_image) - @cached_property + @_cached_property def weighted_central_moments(self): row, col = self.weighted_local_centroid return _moments.central_moments(self._intensity_image_double, row, col, 3) - @cached_property + @_cached_property def weighted_centroid(self): row, col = self.weighted_local_centroid return row + self._slice[0].start, col + self._slice[1].start - @cached_property + @_cached_property def weighted_local_centroid(self): m = self.weighted_moments row = m[0, 1] / m[0, 0] col = m[1, 0] / m[0, 0] return row, col - @cached_property + @_cached_property def weighted_hu_moments(self): return _moments.hu_moments(self.weighted_normalized_moments) - @cached_property + @_cached_property def weighted_moments(self): return _moments.central_moments(self._intensity_image_double, 0, 0, 3) - @cached_property + @_cached_property def weighted_normalized_moments(self): return _moments.normalized_moments(self.weighted_central_moments, 3) From 2086e44977cae8e8be46bf031f07a3a26b63ca69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Thu, 8 Aug 2013 09:01:06 +0200 Subject: [PATCH 405/736] Fix radon example script --- doc/examples/plot_radon_transform.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/doc/examples/plot_radon_transform.py b/doc/examples/plot_radon_transform.py index 61175f8a..0dab82e6 100644 --- a/doc/examples/plot_radon_transform.py +++ b/doc/examples/plot_radon_transform.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ =============== Radon transform @@ -190,8 +189,8 @@ plt.show() IEEE Press 1988. http://www.slaney.org/pct/pct-toc.html .. [2] Wikipedia, Radon transform, http://en.wikipedia.org/wiki/Radon_transform#Relationship_with_the_Fourier_transform -.. [3] S Kaczmarz, "Angenäherte auflösung von systemen linearer - gleichungen", Bulletin International de l’Academie Polonaise des +.. [3] S Kaczmarz, "Angenaeherte Aufloesung von Systemen linearer + Gleichungen", Bulletin International de l'Academie Polonaise des Sciences et des Lettres 35 pp 355--357 (1937) .. [4] AH Andersen, AC Kak, "Simultaneous algebraic reconstruction technique (SART): a superior implementation of the ART algorithm", Ultrasonic From fe02c18f3d5df6676225870a0df4314f7eafe2a9 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Fri, 9 Aug 2013 18:43:24 +1000 Subject: [PATCH 406/736] Use Py_ssize_t instead of int in _slic_cython --- skimage/segmentation/_slic.pyx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/skimage/segmentation/_slic.pyx b/skimage/segmentation/_slic.pyx index d4121721..d47cf8ff 100644 --- a/skimage/segmentation/_slic.pyx +++ b/skimage/segmentation/_slic.pyx @@ -14,10 +14,10 @@ from ..color import rgb2lab, gray2rgb def _slic_cython(double[:, :, :, ::1] image_zyx, - long[:, :, ::1] nearest_mean, + Py_ssize_t[:, :, ::1] nearest_mean, double[:, :, ::1] distance, double[:, ::1] means, - int max_iter, int n_segments): + Py_ssize_t max_iter, Py_ssize_t n_segments): """Helper function for SLIC segmentation. Parameters @@ -26,7 +26,7 @@ def _slic_cython(double[:, :, :, ::1] image_zyx, The image with embedded coordinates, that is, `image_zyx[i, j, k]` is `array([i, j, k, r, g, b])` or `array([i, j, k, L, a, b])`, depending on the colorspace. - nearest_mean : 3D np.ndarray of long, shape (Z, Y, X) + nearest_mean : 3D np.ndarray of int, shape (Z, Y, X) The (initially empty) label field. distance : 3D np.ndarray of double, shape (Z, Y, X) The (initially infinity) array of distances to the nearest centroid. From 4edf2278692a6f0fcbb9cd7846422cebb06fed34 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Sat, 10 Aug 2013 11:17:18 +1000 Subject: [PATCH 407/736] Change 'long' to 'int' in Cython docs --- skimage/segmentation/_slic.pyx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/segmentation/_slic.pyx b/skimage/segmentation/_slic.pyx index d47cf8ff..bfa6cd1a 100644 --- a/skimage/segmentation/_slic.pyx +++ b/skimage/segmentation/_slic.pyx @@ -39,7 +39,7 @@ def _slic_cython(double[:, :, :, ::1] image_zyx, Returns ------- - nearest_mean : 3D np.ndarray of long, shape (Z, Y, X) + nearest_mean : 3D np.ndarray of int, shape (Z, Y, X) The label field/superpixels found by SLIC. """ From 8b2f52da50ff14ebbd11ef27c40c201dbdd3b876 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Sat, 10 Aug 2013 13:57:24 +0200 Subject: [PATCH 408/736] DOC: protect last character in URL --- skimage/filter/rank/generic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/filter/rank/generic.py b/skimage/filter/rank/generic.py index 0bc8ca50..247701d5 100644 --- a/skimage/filter/rank/generic.py +++ b/skimage/filter/rank/generic.py @@ -677,7 +677,7 @@ def entropy(image, selem, out=None, mask=None, shift_x=False, shift_y=False): References ---------- - .. [1] http://en.wikipedia.org/wiki/Entropy_(information_theory) + .. [1] http://en.wikipedia.org/wiki/Entropy_(information_theory)> Examples -------- From 111a992140fa8b1104277a8c27c5c8cb5c1afa14 Mon Sep 17 00:00:00 2001 From: Matt Terry Date: Mon, 12 Aug 2013 09:57:13 -0700 Subject: [PATCH 409/736] fix table formatting --- skimage/color/delta_e.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/skimage/color/delta_e.py b/skimage/color/delta_e.py index 5ad6b3be..bf9e316e 100644 --- a/skimage/color/delta_e.py +++ b/skimage/color/delta_e.py @@ -90,11 +90,13 @@ def deltaE_ciede94(lab1, lab2, kH=1, kC=1, kL=1, k1=0.045, k2=0.015): `kL`, `k1`, `k2` depend on the application and default to the values suggested for graphic arts - Parameter Graphic Arts Textiles - ---------- ------------- -------- + ========== ============== ========== + Parameter Graphic Arts Textiles + ========== ============== ========== `kL` 1.000 2.000 `k1` 0.045 0.048 `k2` 0.015 0.014 + ========== ============== ========== References ---------- From f019ed1fed0ef50a972f093751d1308f7e9a90bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Tue, 13 Aug 2013 14:59:24 +0200 Subject: [PATCH 410/736] Fix missing label property description and backwards compatibility --- skimage/measure/_regionprops.py | 3 +++ skimage/measure/tests/test_regionprops.py | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/skimage/measure/_regionprops.py b/skimage/measure/_regionprops.py index bfe8139c..e3d02bf3 100644 --- a/skimage/measure/_regionprops.py +++ b/skimage/measure/_regionprops.py @@ -33,6 +33,7 @@ PROPS = { 'FilledImage': 'filled_image', 'HuMoments': 'hu_moments', 'Image': 'image', + 'Label': 'label', 'MajorAxisLength': 'major_axis_length', 'MaxIntensity': 'max_intensity', 'MeanIntensity': 'mean_intensity', @@ -386,6 +387,8 @@ def regionprops(label_image, properties=None, Inertia tensor of the region for the rotation around its masss. **inertia_tensor_eigvals** : tuple The two eigen values of the inertia tensor in decreasing order. + **label** : int + The label in the labelled input image. **major_axis_length** : float The length of the major axis of the ellipse that has the same normalized second central moments as the region. diff --git a/skimage/measure/tests/test_regionprops.py b/skimage/measure/tests/test_regionprops.py index c515a3fd..29da0456 100644 --- a/skimage/measure/tests/test_regionprops.py +++ b/skimage/measure/tests/test_regionprops.py @@ -149,6 +149,11 @@ def test_image(): assert_array_equal(img, SAMPLE) +def test_label(): + label = regionprops(SAMPLE)[0].label + assert_array_equal(label, 1) + + def test_filled_area(): area = regionprops(SAMPLE)[0].filled_area assert area == np.sum(SAMPLE) From 2b43dab5469b24097da0c7f86206197b5389a204 Mon Sep 17 00:00:00 2001 From: Marianne Corvellec Date: Tue, 13 Aug 2013 20:25:00 -0400 Subject: [PATCH 411/736] Added elements to the coffee picture docstring. --- skimage/data/__init__.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/skimage/data/__init__.py b/skimage/data/__init__.py index 3791089b..c3d9d5d8 100644 --- a/skimage/data/__init__.py +++ b/skimage/data/__init__.py @@ -23,7 +23,8 @@ __all__ = ['load', 'horse', 'clock', 'immunohistochemistry', - 'chelsea'] + 'chelsea', + 'coffee'] def load(f): @@ -189,11 +190,14 @@ def chelsea(): def coffee(): """Coffee cup. - An example with several shapes (including an ellipse). + This photograph is courtesy of Pikolo Espresso Bar. + It shows several shapes (including an ellipse). It may be used to + illustrate ellipse detection (using the Hough transform). Notes ----- - No copyright restrictions. CC0 by the photographer. + No copyright restrictions. CC0 by the photographer (Rachel Michetti). """ return load("coffee.png") + From 77165de55fcdd3b3de35e0606ac60e353bd5445d Mon Sep 17 00:00:00 2001 From: Kirill Shklovsky Date: Wed, 14 Aug 2013 20:46:52 -0400 Subject: [PATCH 412/736] Changed the 180 constant in bin calculation to avoid rounding errors The code 180 / orientations performs integral division in python < 3, leading to the calculations ignoring some orientations near the 180 maximum --- skimage/feature/_hog.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skimage/feature/_hog.py b/skimage/feature/_hog.py index a2ece2f0..431a5986 100644 --- a/skimage/feature/_hog.py +++ b/skimage/feature/_hog.py @@ -117,9 +117,9 @@ def hog(image, orientations=9, pixels_per_cell=(8, 8), #create new integral image for this orientation # isolate orientations in this range - temp_ori = np.where(orientation < 180 / orientations * (i + 1), + temp_ori = np.where(orientation < 180.0 / orientations * (i + 1), orientation, -1) - temp_ori = np.where(orientation >= 180 / orientations * i, + temp_ori = np.where(orientation >= 180.0 / orientations * i, temp_ori, -1) # select magnitudes for those orientations cond2 = temp_ori > -1 From 781bbaf5bceb19100835e1fd52441d71e2be9ee0 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Mon, 22 Jul 2013 22:17:14 +0530 Subject: [PATCH 413/736] Added partial implementation of censure for mode=DoB --- skimage/feature/censure.py | 47 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 skimage/feature/censure.py diff --git a/skimage/feature/censure.py b/skimage/feature/censure.py new file mode 100644 index 00000000..402aa4e6 --- /dev/null +++ b/skimage/feature/censure.py @@ -0,0 +1,47 @@ +import numpy as np +from scipy.ndimage.filters import maximum_filter, minimum_filter +from skimage.transform import integral_image +import time + +def _get_filtered_image(image, n, mode='DoB'): + if mode == 'DoB': + inner_wt = (1.0 / (2*n + 1)**2) + outer_wt = (1.0 / (12*n**2 + 4*n)) + integral_img = integral_image(image) + filtered_image = np.zeros(image.shape) + # TODO : Outsource to Cython + start = time.time() + for i in range(2 * n, image.shape[0] - 2 * n): + for j in range(2 * n, image.shape[1] - 2 * n): + inner = integral_img[i + n, j + n] + integral_img[i - n, j - n] - integral_img[i + n, j - n] - integral_img[i - n, j + n] + outer = integral_img[i + 2 * n, j + 2 * n] + integral_img[i - 2 * n, j - 2 * n] - integral_img[i + 2 * n, j - 2 * n] - integral_img[i - 2 * n, j + 2 * n] + filtered_image[i, j] = outer_wt * outer - (inner_wt + outer_wt) * inner + print time.time() - start + return filtered_image + + +def censure_keypoints(image, mode='DoB', threshold=0.1): + # TODO : Decide mode for convolve function + # TODO : Decide number of scales. Image-size dependent? + image = np.squeeze(image) + if image.ndim != 2: + raise ValueError("Only 2-D gray-scale images supported.") + # Generating all the scales + scale1 = _get_filtered_image(image, 1, mode) + scale2 = _get_filtered_image(image, 2, mode) + scale3 = _get_filtered_image(image, 3, mode) + scale4 = _get_filtered_image(image, 4, mode) + scale5 = _get_filtered_image(image, 5, mode) + scale6 = _get_filtered_image(image, 6, mode) + scale7 = _get_filtered_image(image, 7, mode) + # Stacking all the scales in the 3rd dimension + scales = np.dstack((scale1, scale2, scale3, scale4, scale5, scale6, scale7)) + # Suppressing points that are neither minima or maxima in their 3 x 3 x 3 + # neighbourhood to zero + minimas = (minimum_filter(scales, (3, 3, 3)) == scales).astype(int) * scales + maximas = (maximum_filter(scales, (3, 3, 3)) == scales).astype(int) * scales + # Suppressing minimas and maximas weaker than threshold + minimas[np.abs(minimas) < threshold] = 0 + maximas[np.abs(maximas) < threshold] = 0 + response = maximas + np.abs(minimas) + return response From c40ab969afe1d40876f0342adc1a1d0c458e5f52 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Thu, 25 Jul 2013 00:03:50 +0530 Subject: [PATCH 414/736] Line Suppression of the response --- skimage/feature/__init__.py | 4 +++- skimage/feature/censure.py | 22 ++++++++++++++++++---- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/skimage/feature/__init__.py b/skimage/feature/__init__.py index d0c51fb0..53c21d31 100644 --- a/skimage/feature/__init__.py +++ b/skimage/feature/__init__.py @@ -9,6 +9,7 @@ from .corner_cy import corner_moravec from .template import match_template from ._brief import brief, match_keypoints_brief from .util import pairwise_hamming_distance +from .censure import censure_keypoints __all__ = ['daisy', 'hog', @@ -26,4 +27,5 @@ __all__ = ['daisy', 'match_template', 'brief', 'pairwise_hamming_distance', - 'match_keypoints_brief'] + 'match_keypoints_brief', + 'censure_keypoints'] diff --git a/skimage/feature/censure.py b/skimage/feature/censure.py index 402aa4e6..e4a862d5 100644 --- a/skimage/feature/censure.py +++ b/skimage/feature/censure.py @@ -1,6 +1,7 @@ import numpy as np from scipy.ndimage.filters import maximum_filter, minimum_filter from skimage.transform import integral_image +from skimage.feature import _compute_auto_correlation import time def _get_filtered_image(image, n, mode='DoB'): @@ -13,15 +14,23 @@ def _get_filtered_image(image, n, mode='DoB'): start = time.time() for i in range(2 * n, image.shape[0] - 2 * n): for j in range(2 * n, image.shape[1] - 2 * n): - inner = integral_img[i + n, j + n] + integral_img[i - n, j - n] - integral_img[i + n, j - n] - integral_img[i - n, j + n] - outer = integral_img[i + 2 * n, j + 2 * n] + integral_img[i - 2 * n, j - 2 * n] - integral_img[i + 2 * n, j - 2 * n] - integral_img[i - 2 * n, j + 2 * n] + inner = integral_img[i + n, j + n] + integral_img[i - n - 1, j - n - 1] - integral_img[i + n, j - n - 1] - integral_img[i - n - 1, j + n] + outer = integral_img[i + 2 * n, j + 2 * n] + integral_img[i - 2 * n - 1, j - 2 * n - 1] - integral_img[i + 2 * n, j - 2 * n - 1] - integral_img[i - 2 * n - 1, j + 2 * n] filtered_image[i, j] = outer_wt * outer - (inner_wt + outer_wt) * inner print time.time() - start return filtered_image -def censure_keypoints(image, mode='DoB', threshold=0.1): - # TODO : Decide mode for convolve function +def _suppress_line(response, sigma): + Axx, Axy, Ayy = _compute_auto_correlation(response, sigma) + detA = Axx * Ayy - Axy**2 + traceA = Axx + Ayy + # ratio of principal curvatures + rpc = traceA / detA + rpc[rpc > 10] = 0 + return rpc + +def censure_keypoints(image, mode='DoB', threshold=0.03): # TODO : Decide number of scales. Image-size dependent? image = np.squeeze(image) if image.ndim != 2: @@ -44,4 +53,9 @@ def censure_keypoints(image, mode='DoB', threshold=0.1): minimas[np.abs(minimas) < threshold] = 0 maximas[np.abs(maximas) < threshold] = 0 response = maximas + np.abs(minimas) + response[:, :, 1] = _suppress_line(response[:, :, 1], 1.33) + response[:, :, 2] = _suppress_line(response[:, :, 2], 1.33) + response[:, :, 3] = _suppress_line(response[:, :, 3], 1.33) + response[:, :, 4] = _suppress_line(response[:, :, 4], 1.33) + response[:, :, 5] = _suppress_line(response[:, :, 5], 1.33) return response From c599b342361e7c4d8a01a624a3f342205afa2976 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Thu, 25 Jul 2013 21:06:17 +0530 Subject: [PATCH 415/736] Correcting import for a private function --- skimage/feature/censure.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/skimage/feature/censure.py b/skimage/feature/censure.py index e4a862d5..f7981c98 100644 --- a/skimage/feature/censure.py +++ b/skimage/feature/censure.py @@ -1,9 +1,10 @@ import numpy as np from scipy.ndimage.filters import maximum_filter, minimum_filter from skimage.transform import integral_image -from skimage.feature import _compute_auto_correlation +from skimage.feature.corner import _compute_auto_correlation import time + def _get_filtered_image(image, n, mode='DoB'): if mode == 'DoB': inner_wt = (1.0 / (2*n + 1)**2) @@ -21,14 +22,15 @@ def _get_filtered_image(image, n, mode='DoB'): return filtered_image -def _suppress_line(response, sigma): +def _suppress_line(response, sigma, rpc_threshold): Axx, Axy, Ayy = _compute_auto_correlation(response, sigma) detA = Axx * Ayy - Axy**2 traceA = Axx + Ayy # ratio of principal curvatures - rpc = traceA / detA - rpc[rpc > 10] = 0 - return rpc + rpc = traceA / (detA + 0.001) + response[rpc > rpc_threshold] = 0 + return response + def censure_keypoints(image, mode='DoB', threshold=0.03): # TODO : Decide number of scales. Image-size dependent? @@ -53,9 +55,9 @@ def censure_keypoints(image, mode='DoB', threshold=0.03): minimas[np.abs(minimas) < threshold] = 0 maximas[np.abs(maximas) < threshold] = 0 response = maximas + np.abs(minimas) - response[:, :, 1] = _suppress_line(response[:, :, 1], 1.33) - response[:, :, 2] = _suppress_line(response[:, :, 2], 1.33) - response[:, :, 3] = _suppress_line(response[:, :, 3], 1.33) - response[:, :, 4] = _suppress_line(response[:, :, 4], 1.33) - response[:, :, 5] = _suppress_line(response[:, :, 5], 1.33) + response[:, :, 1] = _suppress_line(response[:, :, 1], 1.33, 10) + response[:, :, 2] = _suppress_line(response[:, :, 2], 1.33, 10) + response[:, :, 3] = _suppress_line(response[:, :, 3], 1.33, 10) + response[:, :, 4] = _suppress_line(response[:, :, 4], 1.33, 10) + response[:, :, 5] = _suppress_line(response[:, :, 5], 1.33, 10) return response From 97990fbea562bfc441b6b7ae0862f04198982d52 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Thu, 25 Jul 2013 21:28:48 +0530 Subject: [PATCH 416/736] Adding TODO's --- skimage/feature/censure.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/skimage/feature/censure.py b/skimage/feature/censure.py index f7981c98..2aa2057e 100644 --- a/skimage/feature/censure.py +++ b/skimage/feature/censure.py @@ -6,6 +6,7 @@ import time def _get_filtered_image(image, n, mode='DoB'): + # TODO : Implement the STAR and Octagon mode if mode == 'DoB': inner_wt = (1.0 / (2*n + 1)**2) outer_wt = (1.0 / (12*n**2 + 4*n)) @@ -32,7 +33,7 @@ def _suppress_line(response, sigma, rpc_threshold): return response -def censure_keypoints(image, mode='DoB', threshold=0.03): +def censure_keypoints(image, mode='DoB', threshold=0.03, rpc_threshold=10): # TODO : Decide number of scales. Image-size dependent? image = np.squeeze(image) if image.ndim != 2: @@ -55,9 +56,12 @@ def censure_keypoints(image, mode='DoB', threshold=0.03): minimas[np.abs(minimas) < threshold] = 0 maximas[np.abs(maximas) < threshold] = 0 response = maximas + np.abs(minimas) - response[:, :, 1] = _suppress_line(response[:, :, 1], 1.33, 10) - response[:, :, 2] = _suppress_line(response[:, :, 2], 1.33, 10) - response[:, :, 3] = _suppress_line(response[:, :, 3], 1.33, 10) - response[:, :, 4] = _suppress_line(response[:, :, 4], 1.33, 10) - response[:, :, 5] = _suppress_line(response[:, :, 5], 1.33, 10) + # TODO : Decide the rpc_threshold and sigma for all the scales. The paper only discusses + # values for scale2 i.e. response[:, :, 1] + response[:, :, 1] = _suppress_line(response[:, :, 1], 1.33, rpc_threshold) + response[:, :, 2] = _suppress_line(response[:, :, 2], 1.33, rpc_threshold) + response[:, :, 3] = _suppress_line(response[:, :, 3], 1.33, rpc_threshold) + response[:, :, 4] = _suppress_line(response[:, :, 4], 1.33, rpc_threshold) + response[:, :, 5] = _suppress_line(response[:, :, 5], 1.33, rpc_threshold) + # TODO : Return key-points from all the scales? return response From 3d9534b8448dae075e0717ffbba23a2bcf9ae4fc Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Sat, 27 Jul 2013 01:58:36 +0530 Subject: [PATCH 417/736] Added mode Octagon using np.convolve --- skimage/feature/censure.py | 66 +++++++++++++++++++++++++++++++++----- 1 file changed, 58 insertions(+), 8 deletions(-) diff --git a/skimage/feature/censure.py b/skimage/feature/censure.py index 2aa2057e..6a707970 100644 --- a/skimage/feature/censure.py +++ b/skimage/feature/censure.py @@ -1,10 +1,12 @@ import numpy as np -from scipy.ndimage.filters import maximum_filter, minimum_filter +from scipy.ndimage.filters import maximum_filter, minimum_filter, convolve from skimage.transform import integral_image from skimage.feature.corner import _compute_auto_correlation +from skimage.morphology import convex_hull_image import time +""" def _get_filtered_image(image, n, mode='DoB'): # TODO : Implement the STAR and Octagon mode if mode == 'DoB': @@ -21,6 +23,52 @@ def _get_filtered_image(image, n, mode='DoB'): filtered_image[i, j] = outer_wt * outer - (inner_wt + outer_wt) * inner print time.time() - start return filtered_image + elif mode == 'Octagon': + outer_shape = [(5, 2), (5, 3), (7, 3), (9, 4), (9, 7), (13, 7), (15, 10)] + inner_shape = [(3, 0), (3, 1), (3, 2), (5, 2), (5, 3), (5, 4), (5, 5)] +""" + + +def _oct(m, n): + f = np.zeros((m + 2*n, m + 2*n)) + f[0, n] = 1 + f[n, 0] = 1 + f[0, m + n -1] = 1 + f[m + n - 1, 0] = 1 + f[-1, n] = 1 + f[n, -1] = 1 + f[-1, m + n - 1] = 1 + f[m + n - 1, -1] = 1 + return convex_hull_image(f).astype(int) + + +def _octagon_filter(mo, no, mi, ni): + outer = (mo + 2 * no)**2 - 2 * no * (no + 1) + inner = (mi + 2 * ni)**2 - 2 * ni * (ni + 1) + outer_wt = 1.0 / (outer - inner) + inner_wt = 1.0 / inner + c = ((mo + 2 * no) - (mi + 2 * ni)) / 2 + outer_oct = _oct(mo, no) + inner_oct = np.zeros((mo + 2 * no, mo + 2 * no)) + inner_oct[c:-c, c:-c] = _oct(mi, ni) + bfilter = outer_wt * outer_oct - (outer_wt + inner_wt) * inner_oct + return bfilter + + +def _filter_using_convolve(image, n, mode='DoB'): + + if mode == 'DoB': + inner_wt = (1.0 / (2*n + 1)**2) + outer_wt = (1.0 / (12*n**2 + 4*n)) + dob_filter = np.zeros((4 * n + 1, 4 * n + 1)) + dob_filter[:] = outer_wt + dob_filter[n : 3 * n + 1, n : 3 * n + 1] = - inner_wt + return convolve(image, dob_filter) + + elif mode == 'Octagon': + outer_shape = [(5, 2), (5, 3), (7, 3), (9, 4), (9, 7), (13, 7), (15, 10)] + inner_shape = [(3, 0), (3, 1), (3, 2), (5, 2), (5, 3), (5, 4), (5, 5)] + return convolve(image, _octagon_filter(outer_shape[n - 1][0], outer_shape[n - 1][1], inner_shape[n - 1][0], inner_shape[n - 1][1])) def _suppress_line(response, sigma, rpc_threshold): @@ -39,13 +87,15 @@ def censure_keypoints(image, mode='DoB', threshold=0.03, rpc_threshold=10): if image.ndim != 2: raise ValueError("Only 2-D gray-scale images supported.") # Generating all the scales - scale1 = _get_filtered_image(image, 1, mode) - scale2 = _get_filtered_image(image, 2, mode) - scale3 = _get_filtered_image(image, 3, mode) - scale4 = _get_filtered_image(image, 4, mode) - scale5 = _get_filtered_image(image, 5, mode) - scale6 = _get_filtered_image(image, 6, mode) - scale7 = _get_filtered_image(image, 7, mode) + start = time.time() + scale1 = _filter_using_convolve(image, 1, mode) + scale2 = _filter_using_convolve(image, 2, mode) + scale3 = _filter_using_convolve(image, 3, mode) + scale4 = _filter_using_convolve(image, 4, mode) + scale5 = _filter_using_convolve(image, 5, mode) + scale6 = _filter_using_convolve(image, 6, mode) + scale7 = _filter_using_convolve(image, 7, mode) + print time.time - start # Stacking all the scales in the 3rd dimension scales = np.dstack((scale1, scale2, scale3, scale4, scale5, scale6, scale7)) # Suppressing points that are neither minima or maxima in their 3 x 3 x 3 From 1484b9a72ad5ca9fb83f2a2a03ff26d527ae8521 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Sat, 27 Jul 2013 02:07:19 +0530 Subject: [PATCH 418/736] Correcting typo --- skimage/feature/censure.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/feature/censure.py b/skimage/feature/censure.py index 6a707970..f682735c 100644 --- a/skimage/feature/censure.py +++ b/skimage/feature/censure.py @@ -95,7 +95,7 @@ def censure_keypoints(image, mode='DoB', threshold=0.03, rpc_threshold=10): scale5 = _filter_using_convolve(image, 5, mode) scale6 = _filter_using_convolve(image, 6, mode) scale7 = _filter_using_convolve(image, 7, mode) - print time.time - start + print time.time() - start # Stacking all the scales in the 3rd dimension scales = np.dstack((scale1, scale2, scale3, scale4, scale5, scale6, scale7)) # Suppressing points that are neither minima or maxima in their 3 x 3 x 3 From 1bbeeb45d6b29e756c3e7442d9506f918d47a2c5 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Sat, 27 Jul 2013 16:34:28 +0530 Subject: [PATCH 419/736] For loop for mode=DoB in Cython --- bento.info | 3 +++ skimage/feature/censure.py | 41 ++++++++++++++++------------------ skimage/feature/censure_cy.pyx | 21 +++++++++++++++++ skimage/feature/setup.py | 3 +++ 4 files changed, 46 insertions(+), 22 deletions(-) create mode 100644 skimage/feature/censure_cy.pyx diff --git a/bento.info b/bento.info index 648445d6..acdb155d 100644 --- a/bento.info +++ b/bento.info @@ -90,6 +90,9 @@ Library: Extension: skimage.morphology._greyreconstruct Sources: skimage/morphology/_greyreconstruct.pyx + Extension: skimage.feature.censure_cy + Sources: + skimage/feature/censure_cy.pyx Extension: skimage.feature._brief_cy Sources: skimage/feature/_brief_cy.pyx diff --git a/skimage/feature/censure.py b/skimage/feature/censure.py index f682735c..b6a1d31f 100644 --- a/skimage/feature/censure.py +++ b/skimage/feature/censure.py @@ -1,32 +1,27 @@ import numpy as np from scipy.ndimage.filters import maximum_filter, minimum_filter, convolve -from skimage.transform import integral_image -from skimage.feature.corner import _compute_auto_correlation -from skimage.morphology import convex_hull_image + +from ..transform import integral_image +from ..feature.corner import _compute_auto_correlation +from ..morphology import convex_hull_image +from ..util import img_as_float + +from .censure_cy import _censure_dob_loop import time -""" def _get_filtered_image(image, n, mode='DoB'): # TODO : Implement the STAR and Octagon mode if mode == 'DoB': - inner_wt = (1.0 / (2*n + 1)**2) - outer_wt = (1.0 / (12*n**2 + 4*n)) + inner_wt = (1.0 / (2 * n + 1)**2) + outer_wt = (1.0 / (12 * n**2 + 4 * n)) integral_img = integral_image(image) filtered_image = np.zeros(image.shape) # TODO : Outsource to Cython start = time.time() - for i in range(2 * n, image.shape[0] - 2 * n): - for j in range(2 * n, image.shape[1] - 2 * n): - inner = integral_img[i + n, j + n] + integral_img[i - n - 1, j - n - 1] - integral_img[i + n, j - n - 1] - integral_img[i - n - 1, j + n] - outer = integral_img[i + 2 * n, j + 2 * n] + integral_img[i - 2 * n - 1, j - 2 * n - 1] - integral_img[i + 2 * n, j - 2 * n - 1] - integral_img[i - 2 * n - 1, j + 2 * n] - filtered_image[i, j] = outer_wt * outer - (inner_wt + outer_wt) * inner + _censure_dob_loop(image, n, integral_img, filtered_image, inner_wt, outer_wt) print time.time() - start return filtered_image - elif mode == 'Octagon': - outer_shape = [(5, 2), (5, 3), (7, 3), (9, 4), (9, 7), (13, 7), (15, 10)] - inner_shape = [(3, 0), (3, 1), (3, 2), (5, 2), (5, 3), (5, 4), (5, 5)] -""" def _oct(m, n): @@ -86,15 +81,17 @@ def censure_keypoints(image, mode='DoB', threshold=0.03, rpc_threshold=10): image = np.squeeze(image) if image.ndim != 2: raise ValueError("Only 2-D gray-scale images supported.") + image = img_as_float(image) # Generating all the scales + image = np.ascontiguousarray(image) start = time.time() - scale1 = _filter_using_convolve(image, 1, mode) - scale2 = _filter_using_convolve(image, 2, mode) - scale3 = _filter_using_convolve(image, 3, mode) - scale4 = _filter_using_convolve(image, 4, mode) - scale5 = _filter_using_convolve(image, 5, mode) - scale6 = _filter_using_convolve(image, 6, mode) - scale7 = _filter_using_convolve(image, 7, mode) + scale1 = _get_filtered_image(image, 1, mode) + scale2 = _get_filtered_image(image, 2, mode) + scale3 = _get_filtered_image(image, 3, mode) + scale4 = _get_filtered_image(image, 4, mode) + scale5 = _get_filtered_image(image, 5, mode) + scale6 = _get_filtered_image(image, 6, mode) + scale7 = _get_filtered_image(image, 7, mode) print time.time() - start # Stacking all the scales in the 3rd dimension scales = np.dstack((scale1, scale2, scale3, scale4, scale5, scale6, scale7)) diff --git a/skimage/feature/censure_cy.pyx b/skimage/feature/censure_cy.pyx new file mode 100644 index 00000000..b28bc5e1 --- /dev/null +++ b/skimage/feature/censure_cy.pyx @@ -0,0 +1,21 @@ +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False + +cimport numpy as cnp + + +def _censure_dob_loop(double[:, ::1] image, cnp.int16_t n, + double[:, ::1] integral_img, + double[:, ::1] filtered_image, + cnp.float_t inner_wt, cnp.float_t outer_wt): + + cdef Py_ssize_t i, j + cdef double inner, outer + + for i in range(2 * n, image.shape[0] - 2 * n): + for j in range(2 * n, image.shape[1] - 2 * n): + inner = integral_img[i + n, j + n] + integral_img[i - n - 1, j - n - 1] - integral_img[i + n, j - n - 1] - integral_img[i - n - 1, j + n] + outer = integral_img[i + 2 * n, j + 2 * n] + integral_img[i - 2 * n - 1, j - 2 * n - 1] - integral_img[i + 2 * n, j - 2 * n - 1] - integral_img[i - 2 * n - 1, j + 2 * n] + filtered_image[i, j] = outer_wt * outer - (inner_wt + outer_wt) * inner diff --git a/skimage/feature/setup.py b/skimage/feature/setup.py index f4c62957..7df64c32 100644 --- a/skimage/feature/setup.py +++ b/skimage/feature/setup.py @@ -13,12 +13,15 @@ def configuration(parent_package='', top_path=None): config.add_data_dir('tests') cython(['corner_cy.pyx'], working_path=base_path) + cython(['censure_cy.pyx'], working_path=base_path) cython(['_brief_cy.pyx'], working_path=base_path) cython(['_texture.pyx'], working_path=base_path) cython(['_template.pyx'], working_path=base_path) config.add_extension('corner_cy', sources=['corner_cy.c'], include_dirs=[get_numpy_include_dirs()]) + config.add_extension('censure_cy', sources=['censure_cy.c'], + include_dirs=[get_numpy_include_dirs()]) config.add_extension('_brief_cy', sources=['_brief_cy.c'], include_dirs=[get_numpy_include_dirs()]) config.add_extension('_texture', sources=['_texture.c'], From a0266c3834e7d5c9d635f19249166d3e758222b5 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Sun, 28 Jul 2013 00:55:41 +0530 Subject: [PATCH 420/736] Commenting out _filter_using_convolve --- skimage/feature/censure.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/skimage/feature/censure.py b/skimage/feature/censure.py index b6a1d31f..cacf8e2f 100644 --- a/skimage/feature/censure.py +++ b/skimage/feature/censure.py @@ -50,6 +50,7 @@ def _octagon_filter(mo, no, mi, ni): return bfilter +""" def _filter_using_convolve(image, n, mode='DoB'): if mode == 'DoB': @@ -64,7 +65,7 @@ def _filter_using_convolve(image, n, mode='DoB'): outer_shape = [(5, 2), (5, 3), (7, 3), (9, 4), (9, 7), (13, 7), (15, 10)] inner_shape = [(3, 0), (3, 1), (3, 2), (5, 2), (5, 3), (5, 4), (5, 5)] return convolve(image, _octagon_filter(outer_shape[n - 1][0], outer_shape[n - 1][1], inner_shape[n - 1][0], inner_shape[n - 1][1])) - +""" def _suppress_line(response, sigma, rpc_threshold): Axx, Axy, Ayy = _compute_auto_correlation(response, sigma) From ab729a00cac21ace8a118dc045c115778490e299 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Sun, 28 Jul 2013 13:10:44 +0530 Subject: [PATCH 421/736] Removing time() statements and correcting types in censure_cy --- skimage/feature/censure.py | 12 +++++------- skimage/feature/censure_cy.pyx | 2 +- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/skimage/feature/censure.py b/skimage/feature/censure.py index cacf8e2f..ca361cf7 100644 --- a/skimage/feature/censure.py +++ b/skimage/feature/censure.py @@ -7,7 +7,6 @@ from ..morphology import convex_hull_image from ..util import img_as_float from .censure_cy import _censure_dob_loop -import time def _get_filtered_image(image, n, mode='DoB'): @@ -17,10 +16,8 @@ def _get_filtered_image(image, n, mode='DoB'): outer_wt = (1.0 / (12 * n**2 + 4 * n)) integral_img = integral_image(image) filtered_image = np.zeros(image.shape) - # TODO : Outsource to Cython - start = time.time() _censure_dob_loop(image, n, integral_img, filtered_image, inner_wt, outer_wt) - print time.time() - start + return filtered_image @@ -83,9 +80,10 @@ def censure_keypoints(image, mode='DoB', threshold=0.03, rpc_threshold=10): if image.ndim != 2: raise ValueError("Only 2-D gray-scale images supported.") image = img_as_float(image) - # Generating all the scales + image = np.ascontiguousarray(image) - start = time.time() + + # Generating all the scales scale1 = _get_filtered_image(image, 1, mode) scale2 = _get_filtered_image(image, 2, mode) scale3 = _get_filtered_image(image, 3, mode) @@ -93,7 +91,7 @@ def censure_keypoints(image, mode='DoB', threshold=0.03, rpc_threshold=10): scale5 = _get_filtered_image(image, 5, mode) scale6 = _get_filtered_image(image, 6, mode) scale7 = _get_filtered_image(image, 7, mode) - print time.time() - start + # Stacking all the scales in the 3rd dimension scales = np.dstack((scale1, scale2, scale3, scale4, scale5, scale6, scale7)) # Suppressing points that are neither minima or maxima in their 3 x 3 x 3 diff --git a/skimage/feature/censure_cy.pyx b/skimage/feature/censure_cy.pyx index b28bc5e1..d5e8bf02 100644 --- a/skimage/feature/censure_cy.pyx +++ b/skimage/feature/censure_cy.pyx @@ -9,7 +9,7 @@ cimport numpy as cnp def _censure_dob_loop(double[:, ::1] image, cnp.int16_t n, double[:, ::1] integral_img, double[:, ::1] filtered_image, - cnp.float_t inner_wt, cnp.float_t outer_wt): + double inner_wt, double outer_wt): cdef Py_ssize_t i, j cdef double inner, outer From 92864fdfb0501ab0ebaf22b6939c8bc5ea8e5f8c Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Sun, 28 Jul 2013 13:30:04 +0530 Subject: [PATCH 422/736] Removing hard-coding in censure_keypoints() --- skimage/feature/censure.py | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/skimage/feature/censure.py b/skimage/feature/censure.py index ca361cf7..89114a07 100644 --- a/skimage/feature/censure.py +++ b/skimage/feature/censure.py @@ -74,7 +74,7 @@ def _suppress_line(response, sigma, rpc_threshold): return response -def censure_keypoints(image, mode='DoB', threshold=0.03, rpc_threshold=10): +def censure_keypoints(image, mode='DoB', no_of_scales=7, threshold=0.03, rpc_threshold=10): # TODO : Decide number of scales. Image-size dependent? image = np.squeeze(image) if image.ndim != 2: @@ -84,16 +84,10 @@ def censure_keypoints(image, mode='DoB', threshold=0.03, rpc_threshold=10): image = np.ascontiguousarray(image) # Generating all the scales - scale1 = _get_filtered_image(image, 1, mode) - scale2 = _get_filtered_image(image, 2, mode) - scale3 = _get_filtered_image(image, 3, mode) - scale4 = _get_filtered_image(image, 4, mode) - scale5 = _get_filtered_image(image, 5, mode) - scale6 = _get_filtered_image(image, 6, mode) - scale7 = _get_filtered_image(image, 7, mode) + scales = np.zeros((image.shape[0], image.shape[1], no_of_scales)) + for i in xrange(no_of_scales): + scales[:, :, i] = _get_filtered_image(image, i + 1, mode) - # Stacking all the scales in the 3rd dimension - scales = np.dstack((scale1, scale2, scale3, scale4, scale5, scale6, scale7)) # Suppressing points that are neither minima or maxima in their 3 x 3 x 3 # neighbourhood to zero minimas = (minimum_filter(scales, (3, 3, 3)) == scales).astype(int) * scales @@ -102,12 +96,9 @@ def censure_keypoints(image, mode='DoB', threshold=0.03, rpc_threshold=10): minimas[np.abs(minimas) < threshold] = 0 maximas[np.abs(maximas) < threshold] = 0 response = maximas + np.abs(minimas) - # TODO : Decide the rpc_threshold and sigma for all the scales. The paper only discusses - # values for scale2 i.e. response[:, :, 1] - response[:, :, 1] = _suppress_line(response[:, :, 1], 1.33, rpc_threshold) - response[:, :, 2] = _suppress_line(response[:, :, 2], 1.33, rpc_threshold) - response[:, :, 3] = _suppress_line(response[:, :, 3], 1.33, rpc_threshold) - response[:, :, 4] = _suppress_line(response[:, :, 4], 1.33, rpc_threshold) - response[:, :, 5] = _suppress_line(response[:, :, 5], 1.33, rpc_threshold) + + for i in xrange(1, no_of_scales - 1): + response[:, :, i] = _suppress_line(response[:, :, i], (1 + i / 3.0), rpc_threshold) + # TODO : Return key-points from all the scales? return response From 1e4ae4609b1b9a98983c59bebf433e94bcb5f3ed Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Sun, 28 Jul 2013 13:43:33 +0530 Subject: [PATCH 423/736] Returning keypoints with the scale info --- skimage/feature/censure.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/skimage/feature/censure.py b/skimage/feature/censure.py index 89114a07..cd34e010 100644 --- a/skimage/feature/censure.py +++ b/skimage/feature/censure.py @@ -100,5 +100,6 @@ def censure_keypoints(image, mode='DoB', no_of_scales=7, threshold=0.03, rpc_thr for i in xrange(1, no_of_scales - 1): response[:, :, i] = _suppress_line(response[:, :, i], (1 + i / 3.0), rpc_threshold) - # TODO : Return key-points from all the scales? - return response + # Returning keypoints with its scale + keypoints = np.transpose(np.nonzero(response[:, :, 1:no_of_scales])) + [0, 0, 1] + return keypoints From 25f89eeaa74cf7d87daec27ecab271a312a907d0 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Sun, 28 Jul 2013 13:52:07 +0530 Subject: [PATCH 424/736] Correcting the expression of response in censure_keypoints() --- skimage/feature/censure.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/feature/censure.py b/skimage/feature/censure.py index cd34e010..a38edf04 100644 --- a/skimage/feature/censure.py +++ b/skimage/feature/censure.py @@ -95,7 +95,7 @@ def censure_keypoints(image, mode='DoB', no_of_scales=7, threshold=0.03, rpc_thr # Suppressing minimas and maximas weaker than threshold minimas[np.abs(minimas) < threshold] = 0 maximas[np.abs(maximas) < threshold] = 0 - response = maximas + np.abs(minimas) + response = maximas + minimas for i in xrange(1, no_of_scales - 1): response[:, :, i] = _suppress_line(response[:, :, i], (1 + i / 3.0), rpc_threshold) From fdbdbfe108813b6978e7f5319d029f4d1be80a65 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Sun, 28 Jul 2013 14:13:09 +0530 Subject: [PATCH 425/736] Correcting the definition of Ratio of Principal Curvatures --- skimage/feature/censure.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/feature/censure.py b/skimage/feature/censure.py index a38edf04..c5d81373 100644 --- a/skimage/feature/censure.py +++ b/skimage/feature/censure.py @@ -69,7 +69,7 @@ def _suppress_line(response, sigma, rpc_threshold): detA = Axx * Ayy - Axy**2 traceA = Axx + Ayy # ratio of principal curvatures - rpc = traceA / (detA + 0.001) + rpc = traceA**2 / (detA + 0.001) response[rpc > rpc_threshold] = 0 return response From abb7bb65d426a7cdb4723c81dc7f0208a209e05f Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Mon, 29 Jul 2013 09:20:56 +0530 Subject: [PATCH 426/736] Adding _slanted_integral_image. Removed _oct and _filter_using_convolve --- skimage/feature/censure.py | 139 +++++++++++++++++++++++++------------ 1 file changed, 93 insertions(+), 46 deletions(-) diff --git a/skimage/feature/censure.py b/skimage/feature/censure.py index c5d81373..0b8d2b1b 100644 --- a/skimage/feature/censure.py +++ b/skimage/feature/censure.py @@ -1,12 +1,12 @@ import numpy as np -from scipy.ndimage.filters import maximum_filter, minimum_filter, convolve +from scipy.ndimage.filters import maximum_filter, minimum_filter from ..transform import integral_image from ..feature.corner import _compute_auto_correlation from ..morphology import convex_hull_image from ..util import img_as_float -from .censure_cy import _censure_dob_loop +from .censure_cy import _censure_dob_loop, _slanted_integral_image def _get_filtered_image(image, n, mode='DoB'): @@ -17,52 +17,99 @@ def _get_filtered_image(image, n, mode='DoB'): integral_img = integral_image(image) filtered_image = np.zeros(image.shape) _censure_dob_loop(image, n, integral_img, filtered_image, inner_wt, outer_wt) - return filtered_image - - -def _oct(m, n): - f = np.zeros((m + 2*n, m + 2*n)) - f[0, n] = 1 - f[n, 0] = 1 - f[0, m + n -1] = 1 - f[m + n - 1, 0] = 1 - f[-1, n] = 1 - f[n, -1] = 1 - f[-1, m + n - 1] = 1 - f[m + n - 1, -1] = 1 - return convex_hull_image(f).astype(int) - - -def _octagon_filter(mo, no, mi, ni): - outer = (mo + 2 * no)**2 - 2 * no * (no + 1) - inner = (mi + 2 * ni)**2 - 2 * ni * (ni + 1) - outer_wt = 1.0 / (outer - inner) - inner_wt = 1.0 / inner - c = ((mo + 2 * no) - (mi + 2 * ni)) / 2 - outer_oct = _oct(mo, no) - inner_oct = np.zeros((mo + 2 * no, mo + 2 * no)) - inner_oct[c:-c, c:-c] = _oct(mi, ni) - bfilter = outer_wt * outer_oct - (outer_wt + inner_wt) * inner_oct - return bfilter - - -""" -def _filter_using_convolve(image, n, mode='DoB'): - - if mode == 'DoB': - inner_wt = (1.0 / (2*n + 1)**2) - outer_wt = (1.0 / (12*n**2 + 4*n)) - dob_filter = np.zeros((4 * n + 1, 4 * n + 1)) - dob_filter[:] = outer_wt - dob_filter[n : 3 * n + 1, n : 3 * n + 1] = - inner_wt - return convolve(image, dob_filter) - elif mode == 'Octagon': outer_shape = [(5, 2), (5, 3), (7, 3), (9, 4), (9, 7), (13, 7), (15, 10)] inner_shape = [(3, 0), (3, 1), (3, 2), (5, 2), (5, 3), (5, 4), (5, 5)] - return convolve(image, _octagon_filter(outer_shape[n - 1][0], outer_shape[n - 1][1], inner_shape[n - 1][0], inner_shape[n - 1][1])) -""" + # Take these out of the loop. No need to compute again for different scales. + integral_img = integral_image(image) + integral_img1 = _slanted_integral_image_modes(image, 1) + integral_img2 = _slanted_integral_image_modes(image, 2) + integral_img3 = _slanted_integral_image_modes(image, 3) + integral_img4 = _slanted_integral_image_modes(image, 4) + filtered_image = np.zeros(image.shape) + mo = outer_shape[n - 1][0] + no = outer_shape[n - 1][1] + mi = inner_shape[n - 1][0] + ni = inner_shape[n - 1][1] + outer_pixels = (mo + 2 * no)**2 - 2 * no * (no + 1) + inner_pixels = (mi + 2 * ni)**2 - 2 * ni * (ni + 1) + outer_wt = 1.0 / (outer_pixels - inner_pixels) + inner_wt = 1.0 / inner_pixels + o_m = (mo - 1) / 2 + i_m = (mi - 1) / 2 + o_set = o_m + no + i_set = i_m + ni + # Outsource to Cython + for i in range(o_set + 1, image.shape[0] - o_set - 1): + for j in range(o_set + 1, image.shape[1] - o_set - 1): + outer = integral_img1[i + o_set, j + o_m] - integral_img1[i + o_m, j + o_set] - integral_img[i + o_set, j - o_m] + integral_img[i + o_m, j - o_m] + outer += integral_img[i + (mo - 3) / 2, j + (mo - 3) / 2] - integral_img[i - o_m, j + (mo - 3) / 2] - integral_img[i + (mo - 3) / 2, j - o_m] + integral_img[i - o_m, j - o_m] + outer += integral_img4[i + o_m, j - o_set] - integral_img4[i + o_set, j - o_m] - integral_img[i - o_m, j - (mo + 1) / 2] + integral_img[i - o_m, j - o_set - 1] + outer += integral_img2[i - o_set, j - o_m] - integral_img2[i - o_m, j - o_set] - integral_img[i - (mo + 1) / 2, -1] - integral_img[i - o_set - 1, j + (mo - 3) / 2] + integral_img[i - (mo + 1) / 2, j + (mo - 3) / 2] + integral_img[i - o_set - 1, -1] + outer += integral_img3[i - o_m, j + o_set] - integral_img3[i - o_set, j + o_m] - integral_img[-1, j + o_set + 1] - integral_img[i + (mo - 3) / 2, j + o_m] + integral_img[-1, j + o_m] + integral_img[i + (mo - 3) / 2, j + o_set + 1] + + inner = integral_img1[i + i_set, j + i_m] - integral_img1[i + i_m, j + i_set] - integral_img[i + i_set, j - i_m] + integral_img[i + i_m, j - i_m] + inner += integral_img[i + (mi - 3) / 2, j + (mi - 3) / 2] - integral_img[i - i_m, j + (mi - 3) / 2] - integral_img[i + (mi - 3) / 2, j - i_m] + integral_img[i - i_m, j - i_m] + inner += integral_img4[i + i_m, j - i_set] - integral_img4[i + i_set, j - i_m] - integral_img[i - i_m, j - (mi + 1) / 2] + integral_img[i - i_m, j - i_set - 1] + inner += integral_img2[i - i_set, j - i_m] - integral_img2[i - i_m, j - i_set] - integral_img[i - (mi + 1) / 2, -1] - integral_img[i - i_set - 1, j + (mi - 3) / 2] + integral_img[i - (mi + 1) / 2, j + (mi - 3) / 2] + integral_img[i - i_set - 1, -1] + inner += integral_img3[i - i_m, j + i_set] - integral_img3[i - i_set, j + i_m] - integral_img[-1, j + i_set + 1] - integral_img[i + (mi - 3) / 2, j + i_m] + integral_img[-1, j + i_m] + integral_img[i + (mi - 3) / 2, j + i_set + 1] + + filtered_image[i, j] = outer_wt * outer - (outer_wt + inner_wt) * inner + return filtered_image +def _censure_octagon_loop(): + + +# Outsource to Cython + +def _slanted_integral_image(image): + flipped_lr = np.fliplr(image) + left_sum = np.zeros(image.shape[0]) + for i in range(image.shape[1] - image.shape[0], image.shape[1]): + left_sum[image.shape[1] - 1 - i] = np.sum(flipped_lr.diagonal(i)) + left_sum = left_sum.cumsum(0) + right_sum = np.sum(image, 1).cumsum(0) + image[:, 0] = left_sum + image[:, -1] = right_sum + integral_img = np.zeros((image.shape[0] + 1, image.shape[1])) + integral_img[1:, :] = image + for i in range(1, integral_img.shape[0]): + for j in range(1, integral_img.shape[1] - 1): + integral_img[i, j] += integral_img[i, j - 1] + integral_img[i - 1, j + 1] - integral_img[i - 1, j] + return integral_img[1:, :integral_img.shape[1]] + + +def _slanted_integral_image_modes(img, mode=1): + if mode == 1: + image = np.copy(img) + mode1 = _slanted_integral_image(image) + return mode1 + + elif mode == 2: + image = np.copy(img) + image = np.fliplr(image) + image = np.flipud(image) + mode2 = _slanted_integral_image(image) + mode2 = np.fliplr(mode2) + mode2 = np.flipud(mode2) + return mode2 + + elif mode == 3: + image = np.copy(img) + image = np.flipud(image) + image = image.T + mode3 = _slanted_integral_image(image) + mode3 = np.flipud(mode3.T) + return mode3 + + else: + image = np.copy(img) + image = np.fliplr(image) + image = image.T + mode4 = _slanted_integral_image(image) + mode4 = np.fliplr(mode4.T) + return mode4 + def _suppress_line(response, sigma, rpc_threshold): Axx, Axy, Ayy = _compute_auto_correlation(response, sigma) @@ -85,7 +132,7 @@ def censure_keypoints(image, mode='DoB', no_of_scales=7, threshold=0.03, rpc_thr # Generating all the scales scales = np.zeros((image.shape[0], image.shape[1], no_of_scales)) - for i in xrange(no_of_scales): + for i in range(no_of_scales): scales[:, :, i] = _get_filtered_image(image, i + 1, mode) # Suppressing points that are neither minima or maxima in their 3 x 3 x 3 @@ -97,7 +144,7 @@ def censure_keypoints(image, mode='DoB', no_of_scales=7, threshold=0.03, rpc_thr maximas[np.abs(maximas) < threshold] = 0 response = maximas + minimas - for i in xrange(1, no_of_scales - 1): + for i in range(1, no_of_scales - 1): response[:, :, i] = _suppress_line(response[:, :, i], (1 + i / 3.0), rpc_threshold) # Returning keypoints with its scale From 474c7382ddcf42c070f5c6663fa9168df07f31a1 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Mon, 29 Jul 2013 09:24:38 +0530 Subject: [PATCH 427/736] Removing unused stuff --- skimage/feature/censure.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/skimage/feature/censure.py b/skimage/feature/censure.py index 0b8d2b1b..c30da272 100644 --- a/skimage/feature/censure.py +++ b/skimage/feature/censure.py @@ -3,7 +3,6 @@ from scipy.ndimage.filters import maximum_filter, minimum_filter from ..transform import integral_image from ..feature.corner import _compute_auto_correlation -from ..morphology import convex_hull_image from ..util import img_as_float from .censure_cy import _censure_dob_loop, _slanted_integral_image @@ -57,11 +56,9 @@ def _get_filtered_image(image, n, mode='DoB'): filtered_image[i, j] = outer_wt * outer - (outer_wt + inner_wt) * inner return filtered_image -def _censure_octagon_loop(): # Outsource to Cython - def _slanted_integral_image(image): flipped_lr = np.fliplr(image) left_sum = np.zeros(image.shape[0]) @@ -82,14 +79,14 @@ def _slanted_integral_image(image): def _slanted_integral_image_modes(img, mode=1): if mode == 1: image = np.copy(img) - mode1 = _slanted_integral_image(image) + mode1 = _slanted_integral_image(image, 1) return mode1 elif mode == 2: image = np.copy(img) image = np.fliplr(image) image = np.flipud(image) - mode2 = _slanted_integral_image(image) + mode2 = _slanted_integral_image(image, 2) mode2 = np.fliplr(mode2) mode2 = np.flipud(mode2) return mode2 @@ -98,7 +95,7 @@ def _slanted_integral_image_modes(img, mode=1): image = np.copy(img) image = np.flipud(image) image = image.T - mode3 = _slanted_integral_image(image) + mode3 = _slanted_integral_image(image, 3) mode3 = np.flipud(mode3.T) return mode3 @@ -106,7 +103,7 @@ def _slanted_integral_image_modes(img, mode=1): image = np.copy(img) image = np.fliplr(image) image = image.T - mode4 = _slanted_integral_image(image) + mode4 = _slanted_integral_image(image, 4) mode4 = np.fliplr(mode4.T) return mode4 From d15741c58c031ffb2171c96a70e46de606bfd5b1 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Mon, 29 Jul 2013 15:11:04 +0530 Subject: [PATCH 428/736] Cleaning up the non-Cython version for mode=Octagon --- skimage/feature/censure.py | 95 ++++++++++++++++++++------------------ 1 file changed, 51 insertions(+), 44 deletions(-) diff --git a/skimage/feature/censure.py b/skimage/feature/censure.py index c30da272..6e1b87d1 100644 --- a/skimage/feature/censure.py +++ b/skimage/feature/censure.py @@ -5,57 +5,64 @@ from ..transform import integral_image from ..feature.corner import _compute_auto_correlation from ..util import img_as_float -from .censure_cy import _censure_dob_loop, _slanted_integral_image +from .censure_cy import _censure_dob_loop -def _get_filtered_image(image, n, mode='DoB'): +def _get_filtered_image(image, no_of_scales, mode): # TODO : Implement the STAR and Octagon mode if mode == 'DoB': - inner_wt = (1.0 / (2 * n + 1)**2) - outer_wt = (1.0 / (12 * n**2 + 4 * n)) - integral_img = integral_image(image) - filtered_image = np.zeros(image.shape) - _censure_dob_loop(image, n, integral_img, filtered_image, inner_wt, outer_wt) - return filtered_image + scales = np.zeros((image.shape[0], image.shape[1], no_of_scales)) + for i in range(no_of_scales): + n = i + 1 + inner_wt = (1.0 / (2 * n + 1)**2) + outer_wt = (1.0 / (12 * n**2 + 4 * n)) + integral_img = integral_image(image) + filtered_image = np.zeros(image.shape) + _censure_dob_loop(image, n, integral_img, filtered_image, inner_wt, outer_wt) + scales[:, :, i] = filtered_image + return scales elif mode == 'Octagon': outer_shape = [(5, 2), (5, 3), (7, 3), (9, 4), (9, 7), (13, 7), (15, 10)] inner_shape = [(3, 0), (3, 1), (3, 2), (5, 2), (5, 3), (5, 4), (5, 5)] - # Take these out of the loop. No need to compute again for different scales. + scales = np.zeros((image.shape[0], image.shape[1], no_of_scales)) integral_img = integral_image(image) integral_img1 = _slanted_integral_image_modes(image, 1) integral_img2 = _slanted_integral_image_modes(image, 2) integral_img3 = _slanted_integral_image_modes(image, 3) integral_img4 = _slanted_integral_image_modes(image, 4) - filtered_image = np.zeros(image.shape) - mo = outer_shape[n - 1][0] - no = outer_shape[n - 1][1] - mi = inner_shape[n - 1][0] - ni = inner_shape[n - 1][1] - outer_pixels = (mo + 2 * no)**2 - 2 * no * (no + 1) - inner_pixels = (mi + 2 * ni)**2 - 2 * ni * (ni + 1) - outer_wt = 1.0 / (outer_pixels - inner_pixels) - inner_wt = 1.0 / inner_pixels - o_m = (mo - 1) / 2 - i_m = (mi - 1) / 2 - o_set = o_m + no - i_set = i_m + ni - # Outsource to Cython - for i in range(o_set + 1, image.shape[0] - o_set - 1): - for j in range(o_set + 1, image.shape[1] - o_set - 1): - outer = integral_img1[i + o_set, j + o_m] - integral_img1[i + o_m, j + o_set] - integral_img[i + o_set, j - o_m] + integral_img[i + o_m, j - o_m] - outer += integral_img[i + (mo - 3) / 2, j + (mo - 3) / 2] - integral_img[i - o_m, j + (mo - 3) / 2] - integral_img[i + (mo - 3) / 2, j - o_m] + integral_img[i - o_m, j - o_m] - outer += integral_img4[i + o_m, j - o_set] - integral_img4[i + o_set, j - o_m] - integral_img[i - o_m, j - (mo + 1) / 2] + integral_img[i - o_m, j - o_set - 1] - outer += integral_img2[i - o_set, j - o_m] - integral_img2[i - o_m, j - o_set] - integral_img[i - (mo + 1) / 2, -1] - integral_img[i - o_set - 1, j + (mo - 3) / 2] + integral_img[i - (mo + 1) / 2, j + (mo - 3) / 2] + integral_img[i - o_set - 1, -1] - outer += integral_img3[i - o_m, j + o_set] - integral_img3[i - o_set, j + o_m] - integral_img[-1, j + o_set + 1] - integral_img[i + (mo - 3) / 2, j + o_m] + integral_img[-1, j + o_m] + integral_img[i + (mo - 3) / 2, j + o_set + 1] + for k in range(no_of_scales): + n = k + 1 + filtered_image = np.zeros(image.shape) + mo = outer_shape[n - 1][0] + no = outer_shape[n - 1][1] + mi = inner_shape[n - 1][0] + ni = inner_shape[n - 1][1] + outer_pixels = (mo + 2 * no)**2 - 2 * no * (no + 1) + inner_pixels = (mi + 2 * ni)**2 - 2 * ni * (ni + 1) + outer_wt = 1.0 / (outer_pixels - inner_pixels) + inner_wt = 1.0 / inner_pixels + o_m = (mo - 1) / 2 + i_m = (mi - 1) / 2 + o_set = o_m + no + i_set = i_m + ni + # Outsource to Cython + for i in range(o_set + 1, image.shape[0] - o_set - 1): + for j in range(o_set + 1, image.shape[1] - o_set - 1): + outer = integral_img1[i + o_set, j + o_m] - integral_img1[i + o_m, j + o_set] - integral_img[i + o_set, j - o_m] + integral_img[i + o_m, j - o_m] + outer += integral_img[i + (mo - 3) / 2, j + (mo - 3) / 2] - integral_img[i - o_m, j + (mo - 3) / 2] - integral_img[i + (mo - 3) / 2, j - o_m] + integral_img[i - o_m, j - o_m] + outer += integral_img4[i + o_m, j - o_set] - integral_img4[i + o_set, j - o_m] - integral_img[i - o_m, j - (mo + 1) / 2] + integral_img[i - o_m, j - o_set - 1] + outer += integral_img2[i - o_set, j - o_m] - integral_img2[i - o_m, j - o_set] - integral_img[i - (mo + 1) / 2, -1] - integral_img[i - o_set - 1, j + (mo - 3) / 2] + integral_img[i - (mo + 1) / 2, j + (mo - 3) / 2] + integral_img[i - o_set - 1, -1] + outer += integral_img3[i - o_m, j + o_set] - integral_img3[i - o_set, j + o_m] - integral_img[-1, j + o_set + 1] - integral_img[i + (mo - 3) / 2, j + o_m] + integral_img[-1, j + o_m] + integral_img[i + (mo - 3) / 2, j + o_set + 1] - inner = integral_img1[i + i_set, j + i_m] - integral_img1[i + i_m, j + i_set] - integral_img[i + i_set, j - i_m] + integral_img[i + i_m, j - i_m] - inner += integral_img[i + (mi - 3) / 2, j + (mi - 3) / 2] - integral_img[i - i_m, j + (mi - 3) / 2] - integral_img[i + (mi - 3) / 2, j - i_m] + integral_img[i - i_m, j - i_m] - inner += integral_img4[i + i_m, j - i_set] - integral_img4[i + i_set, j - i_m] - integral_img[i - i_m, j - (mi + 1) / 2] + integral_img[i - i_m, j - i_set - 1] - inner += integral_img2[i - i_set, j - i_m] - integral_img2[i - i_m, j - i_set] - integral_img[i - (mi + 1) / 2, -1] - integral_img[i - i_set - 1, j + (mi - 3) / 2] + integral_img[i - (mi + 1) / 2, j + (mi - 3) / 2] + integral_img[i - i_set - 1, -1] - inner += integral_img3[i - i_m, j + i_set] - integral_img3[i - i_set, j + i_m] - integral_img[-1, j + i_set + 1] - integral_img[i + (mi - 3) / 2, j + i_m] + integral_img[-1, j + i_m] + integral_img[i + (mi - 3) / 2, j + i_set + 1] + inner = integral_img1[i + i_set, j + i_m] - integral_img1[i + i_m, j + i_set] - integral_img[i + i_set, j - i_m] + integral_img[i + i_m, j - i_m] + inner += integral_img[i + (mi - 3) / 2, j + (mi - 3) / 2] - integral_img[i - i_m, j + (mi - 3) / 2] - integral_img[i + (mi - 3) / 2, j - i_m] + integral_img[i - i_m, j - i_m] + inner += integral_img4[i + i_m, j - i_set] - integral_img4[i + i_set, j - i_m] - integral_img[i - i_m, j - (mi + 1) / 2] + integral_img[i - i_m, j - i_set - 1] + inner += integral_img2[i - i_set, j - i_m] - integral_img2[i - i_m, j - i_set] - integral_img[i - (mi + 1) / 2, -1] - integral_img[i - i_set - 1, j + (mi - 3) / 2] + integral_img[i - (mi + 1) / 2, j + (mi - 3) / 2] + integral_img[i - i_set - 1, -1] + inner += integral_img3[i - i_m, j + i_set] - integral_img3[i - i_set, j + i_m] - integral_img[-1, j + i_set + 1] - integral_img[i + (mi - 3) / 2, j + i_m] + integral_img[-1, j + i_m] + integral_img[i + (mi - 3) / 2, j + i_set + 1] - filtered_image[i, j] = outer_wt * outer - (outer_wt + inner_wt) * inner - return filtered_image + filtered_image[i, j] = outer_wt * outer - (outer_wt + inner_wt) * inner + scales[:, :, k] = filtered_image + return scales # Outsource to Cython @@ -79,14 +86,14 @@ def _slanted_integral_image(image): def _slanted_integral_image_modes(img, mode=1): if mode == 1: image = np.copy(img) - mode1 = _slanted_integral_image(image, 1) + mode1 = _slanted_integral_image(image) return mode1 elif mode == 2: image = np.copy(img) image = np.fliplr(image) image = np.flipud(image) - mode2 = _slanted_integral_image(image, 2) + mode2 = _slanted_integral_image(image) mode2 = np.fliplr(mode2) mode2 = np.flipud(mode2) return mode2 @@ -95,7 +102,7 @@ def _slanted_integral_image_modes(img, mode=1): image = np.copy(img) image = np.flipud(image) image = image.T - mode3 = _slanted_integral_image(image, 3) + mode3 = _slanted_integral_image(image) mode3 = np.flipud(mode3.T) return mode3 @@ -103,7 +110,7 @@ def _slanted_integral_image_modes(img, mode=1): image = np.copy(img) image = np.fliplr(image) image = image.T - mode4 = _slanted_integral_image(image, 4) + mode4 = _slanted_integral_image(image) mode4 = np.fliplr(mode4.T) return mode4 @@ -118,7 +125,7 @@ def _suppress_line(response, sigma, rpc_threshold): return response -def censure_keypoints(image, mode='DoB', no_of_scales=7, threshold=0.03, rpc_threshold=10): +def censure_keypoints(image, no_of_scales=7, mode='DoB', threshold=0.03, rpc_threshold=10): # TODO : Decide number of scales. Image-size dependent? image = np.squeeze(image) if image.ndim != 2: @@ -129,13 +136,13 @@ def censure_keypoints(image, mode='DoB', no_of_scales=7, threshold=0.03, rpc_thr # Generating all the scales scales = np.zeros((image.shape[0], image.shape[1], no_of_scales)) - for i in range(no_of_scales): - scales[:, :, i] = _get_filtered_image(image, i + 1, mode) + scales = _get_filtered_image(image, no_of_scales, mode) # Suppressing points that are neither minima or maxima in their 3 x 3 x 3 # neighbourhood to zero minimas = (minimum_filter(scales, (3, 3, 3)) == scales).astype(int) * scales maximas = (maximum_filter(scales, (3, 3, 3)) == scales).astype(int) * scales + # Suppressing minimas and maximas weaker than threshold minimas[np.abs(minimas) < threshold] = 0 maximas[np.abs(maximas) < threshold] = 0 From 7c32dc0c15ad6a85c09e68a6babed1d386984042 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Mon, 29 Jul 2013 17:16:15 +0530 Subject: [PATCH 429/736] Cythonizing the for loops --- skimage/feature/censure.py | 52 ++++++----------------------- skimage/feature/censure_cy.pyx | 60 ++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 42 deletions(-) diff --git a/skimage/feature/censure.py b/skimage/feature/censure.py index 6e1b87d1..47269a15 100644 --- a/skimage/feature/censure.py +++ b/skimage/feature/censure.py @@ -5,11 +5,11 @@ from ..transform import integral_image from ..feature.corner import _compute_auto_correlation from ..util import img_as_float -from .censure_cy import _censure_dob_loop - +from .censure_cy import _censure_dob_loop, _slanted_integral_image, _censure_octagon_loop +from time import time def _get_filtered_image(image, no_of_scales, mode): - # TODO : Implement the STAR and Octagon mode + # TODO : Implement the STAR mode if mode == 'DoB': scales = np.zeros((image.shape[0], image.shape[1], no_of_scales)) for i in range(no_of_scales): @@ -22,6 +22,7 @@ def _get_filtered_image(image, no_of_scales, mode): scales[:, :, i] = filtered_image return scales elif mode == 'Octagon': + # TODO : Decide the shapes of Octagon filters for scales > 7 outer_shape = [(5, 2), (5, 3), (7, 3), (9, 4), (9, 7), (13, 7), (15, 10)] inner_shape = [(3, 0), (3, 1), (3, 2), (5, 2), (5, 3), (5, 4), (5, 5)] scales = np.zeros((image.shape[0], image.shape[1], no_of_scales)) @@ -41,48 +42,13 @@ def _get_filtered_image(image, no_of_scales, mode): inner_pixels = (mi + 2 * ni)**2 - 2 * ni * (ni + 1) outer_wt = 1.0 / (outer_pixels - inner_pixels) inner_wt = 1.0 / inner_pixels - o_m = (mo - 1) / 2 - i_m = (mi - 1) / 2 - o_set = o_m + no - i_set = i_m + ni - # Outsource to Cython - for i in range(o_set + 1, image.shape[0] - o_set - 1): - for j in range(o_set + 1, image.shape[1] - o_set - 1): - outer = integral_img1[i + o_set, j + o_m] - integral_img1[i + o_m, j + o_set] - integral_img[i + o_set, j - o_m] + integral_img[i + o_m, j - o_m] - outer += integral_img[i + (mo - 3) / 2, j + (mo - 3) / 2] - integral_img[i - o_m, j + (mo - 3) / 2] - integral_img[i + (mo - 3) / 2, j - o_m] + integral_img[i - o_m, j - o_m] - outer += integral_img4[i + o_m, j - o_set] - integral_img4[i + o_set, j - o_m] - integral_img[i - o_m, j - (mo + 1) / 2] + integral_img[i - o_m, j - o_set - 1] - outer += integral_img2[i - o_set, j - o_m] - integral_img2[i - o_m, j - o_set] - integral_img[i - (mo + 1) / 2, -1] - integral_img[i - o_set - 1, j + (mo - 3) / 2] + integral_img[i - (mo + 1) / 2, j + (mo - 3) / 2] + integral_img[i - o_set - 1, -1] - outer += integral_img3[i - o_m, j + o_set] - integral_img3[i - o_set, j + o_m] - integral_img[-1, j + o_set + 1] - integral_img[i + (mo - 3) / 2, j + o_m] + integral_img[-1, j + o_m] + integral_img[i + (mo - 3) / 2, j + o_set + 1] - inner = integral_img1[i + i_set, j + i_m] - integral_img1[i + i_m, j + i_set] - integral_img[i + i_set, j - i_m] + integral_img[i + i_m, j - i_m] - inner += integral_img[i + (mi - 3) / 2, j + (mi - 3) / 2] - integral_img[i - i_m, j + (mi - 3) / 2] - integral_img[i + (mi - 3) / 2, j - i_m] + integral_img[i - i_m, j - i_m] - inner += integral_img4[i + i_m, j - i_set] - integral_img4[i + i_set, j - i_m] - integral_img[i - i_m, j - (mi + 1) / 2] + integral_img[i - i_m, j - i_set - 1] - inner += integral_img2[i - i_set, j - i_m] - integral_img2[i - i_m, j - i_set] - integral_img[i - (mi + 1) / 2, -1] - integral_img[i - i_set - 1, j + (mi - 3) / 2] + integral_img[i - (mi + 1) / 2, j + (mi - 3) / 2] + integral_img[i - i_set - 1, -1] - inner += integral_img3[i - i_m, j + i_set] - integral_img3[i - i_set, j + i_m] - integral_img[-1, j + i_set + 1] - integral_img[i + (mi - 3) / 2, j + i_m] + integral_img[-1, j + i_m] + integral_img[i + (mi - 3) / 2, j + i_set + 1] + _censure_octagon_loop(image, integral_img, integral_img1, integral_img2, integral_img3, integral_img4, filtered_image, outer_wt, inner_wt, mo, no, mi, ni) - filtered_image[i, j] = outer_wt * outer - (outer_wt + inner_wt) * inner scales[:, :, k] = filtered_image return scales -# Outsource to Cython -def _slanted_integral_image(image): - flipped_lr = np.fliplr(image) - left_sum = np.zeros(image.shape[0]) - for i in range(image.shape[1] - image.shape[0], image.shape[1]): - left_sum[image.shape[1] - 1 - i] = np.sum(flipped_lr.diagonal(i)) - left_sum = left_sum.cumsum(0) - right_sum = np.sum(image, 1).cumsum(0) - image[:, 0] = left_sum - image[:, -1] = right_sum - integral_img = np.zeros((image.shape[0] + 1, image.shape[1])) - integral_img[1:, :] = image - for i in range(1, integral_img.shape[0]): - for j in range(1, integral_img.shape[1] - 1): - integral_img[i, j] += integral_img[i, j - 1] + integral_img[i - 1, j + 1] - integral_img[i - 1, j] - return integral_img[1:, :integral_img.shape[1]] - - def _slanted_integral_image_modes(img, mode=1): if mode == 1: image = np.copy(img) @@ -135,22 +101,24 @@ def censure_keypoints(image, no_of_scales=7, mode='DoB', threshold=0.03, rpc_thr image = np.ascontiguousarray(image) # Generating all the scales + start = time() scales = np.zeros((image.shape[0], image.shape[1], no_of_scales)) scales = _get_filtered_image(image, no_of_scales, mode) + print time() - start # Suppressing points that are neither minima or maxima in their 3 x 3 x 3 # neighbourhood to zero minimas = (minimum_filter(scales, (3, 3, 3)) == scales).astype(int) * scales maximas = (maximum_filter(scales, (3, 3, 3)) == scales).astype(int) * scales - + print time() - start # Suppressing minimas and maximas weaker than threshold minimas[np.abs(minimas) < threshold] = 0 maximas[np.abs(maximas) < threshold] = 0 response = maximas + minimas - + print time() - start for i in range(1, no_of_scales - 1): response[:, :, i] = _suppress_line(response[:, :, i], (1 + i / 3.0), rpc_threshold) - + print time() - start # Returning keypoints with its scale keypoints = np.transpose(np.nonzero(response[:, :, 1:no_of_scales])) + [0, 0, 1] return keypoints diff --git a/skimage/feature/censure_cy.pyx b/skimage/feature/censure_cy.pyx index d5e8bf02..104d3348 100644 --- a/skimage/feature/censure_cy.pyx +++ b/skimage/feature/censure_cy.pyx @@ -4,6 +4,7 @@ #cython: wraparound=False cimport numpy as cnp +import numpy as np def _censure_dob_loop(double[:, ::1] image, cnp.int16_t n, @@ -19,3 +20,62 @@ def _censure_dob_loop(double[:, ::1] image, cnp.int16_t n, inner = integral_img[i + n, j + n] + integral_img[i - n - 1, j - n - 1] - integral_img[i + n, j - n - 1] - integral_img[i - n - 1, j + n] outer = integral_img[i + 2 * n, j + 2 * n] + integral_img[i - 2 * n - 1, j - 2 * n - 1] - integral_img[i + 2 * n, j - 2 * n - 1] - integral_img[i - 2 * n - 1, j + 2 * n] filtered_image[i, j] = outer_wt * outer - (inner_wt + outer_wt) * inner + + +def _slanted_integral_image(double[:, :] image): + + cdef Py_ssize_t i, j + cdef double[:, :] flipped_lr = np.fliplr(image) + cdef double[:] left_sum = np.zeros(image.shape[0], dtype=np.float) + cdef double[:] right_sum + cdef double[:, :] integral_img = np.zeros((image.shape[0] + 1, image.shape[1]), dtype=np.float) + + flipped_lr = np.fliplr(image) + for i in range(image.shape[1] - image.shape[0], image.shape[1]): + left_sum[image.shape[1] - 1 - i] = np.sum(flipped_lr.diagonal(i)) + left_sum = left_sum.cumsum(0) + + right_sum = np.sum(image, 1).cumsum(0) + + image[:, 0] = left_sum + image[:, -1] = right_sum + + integral_img[1:, :] = image + + for i in range(1, integral_img.shape[0]): + for j in range(1, integral_img.shape[1] - 1): + integral_img[i, j] += integral_img[i, j - 1] + integral_img[i - 1, j + 1] - integral_img[i - 1, j] + return integral_img[1:, :integral_img.shape[1]] + + +def _censure_octagon_loop(double[:, ::1] image, double[:, ::1] integral_img, + double[:, ::1] integral_img1, + double[:, ::1] integral_img2, + double[:, ::1] integral_img3, + double[:, ::1] integral_img4, + double[:, ::1] filtered_image, + double outer_wt, double inner_wt, + int mo, int no, int mi, int ni): + + cdef Py_ssize_t i, j, o_m, i_m, o_set, i_set + + o_m = (mo - 1) / 2 + i_m = (mi - 1) / 2 + o_set = o_m + no + i_set = i_m + ni + + for i in range(o_set + 1, image.shape[0] - o_set - 1): + for j in range(o_set + 1, image.shape[1] - o_set - 1): + outer = integral_img1[i + o_set, j + o_m] - integral_img1[i + o_m, j + o_set] - integral_img[i + o_set, j - o_m] + integral_img[i + o_m, j - o_m] + outer += integral_img[i + o_m - 1, j + o_m - 1] - integral_img[i - o_m, j + o_m - 1] - integral_img[i + o_m - 1, j - o_m] + integral_img[i - o_m, j - o_m] + outer += integral_img4[i + o_m, j - o_set] - integral_img4[i + o_set, j - o_m] - integral_img[i - o_m, j - o_m + 1] + integral_img[i - o_m, j - o_set - 1] + outer += integral_img2[i - o_set, j - o_m] - integral_img2[i - o_m, j - o_set] - integral_img[i - o_m + 1, -1] - integral_img[i - o_set - 1, j + o_m - 1] + integral_img[i - o_m + 1, j + o_m - 1] + integral_img[i - o_set - 1, -1] + outer += integral_img3[i - o_m, j + o_set] - integral_img3[i - o_set, j + o_m] - integral_img[-1, j + o_set + 1] - integral_img[i + o_m - 1, j + o_m] + integral_img[-1, j + o_m] + integral_img[i + o_m - 1, j + o_set + 1] + + inner = integral_img1[i + i_set, j + i_m] - integral_img1[i + i_m, j + i_set] - integral_img[i + i_set, j - i_m] + integral_img[i + i_m, j - i_m] + inner += integral_img[i + i_m - 1, j + i_m - 1] - integral_img[i - i_m, j + i_m - 1] - integral_img[i + i_m - 1, j - i_m] + integral_img[i - i_m, j - i_m] + inner += integral_img4[i + i_m, j - i_set] - integral_img4[i + i_set, j - i_m] - integral_img[i - i_m, j - i_m + 1] + integral_img[i - i_m, j - i_set - 1] + inner += integral_img2[i - i_set, j - i_m] - integral_img2[i - i_m, j - i_set] - integral_img[i - i_m + 1, -1] - integral_img[i - i_set - 1, j + i_m - 1] + integral_img[i - i_m + 1, j + i_m - 1] + integral_img[i - i_set - 1, -1] + inner += integral_img3[i - i_m, j + i_set] - integral_img3[i - i_set, j + i_m] - integral_img[-1, j + i_set + 1] - integral_img[i + i_m - 1, j + i_m] + integral_img[-1, j + i_m] + integral_img[i + i_m - 1, j + i_set + 1] + + filtered_image[i, j] = outer_wt * outer - (outer_wt + inner_wt) * inner From 5ecbb57e829b51e1aa8cf1de68be67f8de712420 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Mon, 29 Jul 2013 22:57:13 +0530 Subject: [PATCH 430/736] Adding the docs for censure_keypoints --- skimage/feature/censure.py | 45 +++++++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/skimage/feature/censure.py b/skimage/feature/censure.py index 47269a15..618a29b9 100644 --- a/skimage/feature/censure.py +++ b/skimage/feature/censure.py @@ -92,7 +92,50 @@ def _suppress_line(response, sigma, rpc_threshold): def censure_keypoints(image, no_of_scales=7, mode='DoB', threshold=0.03, rpc_threshold=10): - # TODO : Decide number of scales. Image-size dependent? + """ + Extracts Censure keypoints along with the corresponding scale using + either Difference of Boxes, Octagon or STAR bilevel filter. + + Parameters + ---------- + image : 2D ndarray + Input image. + + no_of_scales : positive integer + Number of scales to extract keypoints from. Default is 7. + + mode : 'DoB' + Type of bilevel filter used to get the scales of input image. Possible + values are 'DoB', 'Octagon' and 'STAR'. Default is 'DoB'. + + threshold : + Threshold value used to suppress maximas and minimas with a weak + magnitude response obtained after Non-Maximal Suppression. Default + is 0.03. + + rpc_threshold : + Threshold for rejecting interest points which have ratio of principal + curvatures greater than this value. Default is 10. + + Returns + ------- + keypoints : (N, 3) array + Location of extracted keypoints along with the corresponding scale. + + References + ---------- + .. [1] Motilal Agrawal, Kurt Konolige and Morten Rufus Blas + "CenSurE: Center Surround Extremas for Realtime Feature + Detection and Matching", + http://link.springer.com/content/pdf/10.1007%2F978-3-540-88693-8_8.pdf + + .. [2] Adam Schmidt, Marek Kraft, Michal Fularz and Zuzanna Domagala + "Comparative Assessment of Point Feature Detectors and + Descriptors in the Context of Robot Navigation" + http://www.jamris.org/01_2013/saveas.php?QUEST=JAMRIS_No01_2013_P_11-20.pdf + + """ + image = np.squeeze(image) if image.ndim != 2: raise ValueError("Only 2-D gray-scale images supported.") From 6243d4bc0944a034345c63f84376cf29e0c4aac4 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Mon, 29 Jul 2013 22:58:30 +0530 Subject: [PATCH 431/736] Trying to resolve Cython TypeErrors --- skimage/feature/censure_cy.pyx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/skimage/feature/censure_cy.pyx b/skimage/feature/censure_cy.pyx index 104d3348..4491363d 100644 --- a/skimage/feature/censure_cy.pyx +++ b/skimage/feature/censure_cy.pyx @@ -22,23 +22,23 @@ def _censure_dob_loop(double[:, ::1] image, cnp.int16_t n, filtered_image[i, j] = outer_wt * outer - (inner_wt + outer_wt) * inner -def _slanted_integral_image(double[:, :] image): +def _slanted_integral_image(double[:, ::1] image): cdef Py_ssize_t i, j - cdef double[:, :] flipped_lr = np.fliplr(image) cdef double[:] left_sum = np.zeros(image.shape[0], dtype=np.float) - cdef double[:] right_sum cdef double[:, :] integral_img = np.zeros((image.shape[0] + 1, image.shape[1]), dtype=np.float) - flipped_lr = np.fliplr(image) + flipped_lr = np.asarray(image[:, ::-1]) for i in range(image.shape[1] - image.shape[0], image.shape[1]): left_sum[image.shape[1] - 1 - i] = np.sum(flipped_lr.diagonal(i)) - left_sum = left_sum.cumsum(0) + left_sum_np = np.asarray(left_sum) + #image = np.asarray(image) + left_sum_np = left_sum_np.cumsum(0) + #right_sum_np = np.asarray() + right_sum_np = np.sum(image, 1).cumsum(0) - right_sum = np.sum(image, 1).cumsum(0) - - image[:, 0] = left_sum - image[:, -1] = right_sum + image[:, 0] = left_sum_np + image[:, -1] = right_sum_np integral_img[1:, :] = image From 4a19b60721f3be1fb2d35ac638e1deab8598eb05 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Tue, 30 Jul 2013 02:32:24 +0530 Subject: [PATCH 432/736] Trying to Debug the SegFault --- skimage/feature/censure.py | 27 ++++++++++++++++----------- skimage/feature/censure_cy.pyx | 26 +++++++++++++++++--------- 2 files changed, 33 insertions(+), 20 deletions(-) diff --git a/skimage/feature/censure.py b/skimage/feature/censure.py index 618a29b9..f7d75680 100644 --- a/skimage/feature/censure.py +++ b/skimage/feature/censure.py @@ -6,7 +6,7 @@ from ..feature.corner import _compute_auto_correlation from ..util import img_as_float from .censure_cy import _censure_dob_loop, _slanted_integral_image, _censure_octagon_loop -from time import time + def _get_filtered_image(image, no_of_scales, mode): # TODO : Implement the STAR mode @@ -52,14 +52,17 @@ def _get_filtered_image(image, no_of_scales, mode): def _slanted_integral_image_modes(img, mode=1): if mode == 1: image = np.copy(img) - mode1 = _slanted_integral_image(image) - return mode1 + mode1 = np.zeros((image.shape[0] + 1, image.shape[1])) + _slanted_integral_image(image, mode1) + return mode1[1:, :mode1.shape[1]] elif mode == 2: image = np.copy(img) image = np.fliplr(image) image = np.flipud(image) - mode2 = _slanted_integral_image(image) + mode2 = np.zeros((image.shape[0] + 1, image.shape[1])) + _slanted_integral_image(image, mode2) + mode2 = mode2[1:, :mode2.shape[1]] mode2 = np.fliplr(mode2) mode2 = np.flipud(mode2) return mode2 @@ -68,7 +71,9 @@ def _slanted_integral_image_modes(img, mode=1): image = np.copy(img) image = np.flipud(image) image = image.T - mode3 = _slanted_integral_image(image) + mode3 = np.zeros((image.shape[0] + 1, image.shape[1])) + _slanted_integral_image(image, mode3) + mode3 = mode3[1:, :mode3.shape[1]] mode3 = np.flipud(mode3.T) return mode3 @@ -76,7 +81,9 @@ def _slanted_integral_image_modes(img, mode=1): image = np.copy(img) image = np.fliplr(image) image = image.T - mode4 = _slanted_integral_image(image) + mode4 = np.zeros((image.shape[0] + 1, image.shape[1])) + _slanted_integral_image(image, mode4) + mode4 = mode4[1:, :mode4.shape[1]] mode4 = np.fliplr(mode4.T) return mode4 @@ -144,24 +151,22 @@ def censure_keypoints(image, no_of_scales=7, mode='DoB', threshold=0.03, rpc_thr image = np.ascontiguousarray(image) # Generating all the scales - start = time() scales = np.zeros((image.shape[0], image.shape[1], no_of_scales)) scales = _get_filtered_image(image, no_of_scales, mode) - print time() - start # Suppressing points that are neither minima or maxima in their 3 x 3 x 3 # neighbourhood to zero minimas = (minimum_filter(scales, (3, 3, 3)) == scales).astype(int) * scales maximas = (maximum_filter(scales, (3, 3, 3)) == scales).astype(int) * scales - print time() - start + # Suppressing minimas and maximas weaker than threshold minimas[np.abs(minimas) < threshold] = 0 maximas[np.abs(maximas) < threshold] = 0 response = maximas + minimas - print time() - start + for i in range(1, no_of_scales - 1): response[:, :, i] = _suppress_line(response[:, :, i], (1 + i / 3.0), rpc_threshold) - print time() - start + # Returning keypoints with its scale keypoints = np.transpose(np.nonzero(response[:, :, 1:no_of_scales])) + [0, 0, 1] return keypoints diff --git a/skimage/feature/censure_cy.pyx b/skimage/feature/censure_cy.pyx index 4491363d..9783d201 100644 --- a/skimage/feature/censure_cy.pyx +++ b/skimage/feature/censure_cy.pyx @@ -22,30 +22,37 @@ def _censure_dob_loop(double[:, ::1] image, cnp.int16_t n, filtered_image[i, j] = outer_wt * outer - (inner_wt + outer_wt) * inner -def _slanted_integral_image(double[:, ::1] image): +def _slanted_integral_image(double[:, ::1] image, + double[:, ::1] integral_img): cdef Py_ssize_t i, j cdef double[:] left_sum = np.zeros(image.shape[0], dtype=np.float) - cdef double[:, :] integral_img = np.zeros((image.shape[0] + 1, image.shape[1]), dtype=np.float) flipped_lr = np.asarray(image[:, ::-1]) for i in range(image.shape[1] - image.shape[0], image.shape[1]): left_sum[image.shape[1] - 1 - i] = np.sum(flipped_lr.diagonal(i)) left_sum_np = np.asarray(left_sum) - #image = np.asarray(image) + left_sum_np = left_sum_np.cumsum(0) - #right_sum_np = np.asarray() + right_sum_np = np.sum(image, 1).cumsum(0) - image[:, 0] = left_sum_np - image[:, -1] = right_sum_np + print '1' + for i in range(image.shape[0]): + image[i, 0] = left_sum_np[i] + image[i, -1] = right_sum_np[i] - integral_img[1:, :] = image + print '2' + for i in range(1, integral_img.shape[0]): + for j in range(integral_img.shape[1]): + integral_img[i, j] = image[i - 1, j] + print '3' for i in range(1, integral_img.shape[0]): for j in range(1, integral_img.shape[1] - 1): integral_img[i, j] += integral_img[i, j - 1] + integral_img[i - 1, j + 1] - integral_img[i - 1, j] - return integral_img[1:, :integral_img.shape[1]] + print '4' + def _censure_octagon_loop(double[:, ::1] image, double[:, ::1] integral_img, @@ -63,7 +70,7 @@ def _censure_octagon_loop(double[:, ::1] image, double[:, ::1] integral_img, i_m = (mi - 1) / 2 o_set = o_m + no i_set = i_m + ni - + print '5' for i in range(o_set + 1, image.shape[0] - o_set - 1): for j in range(o_set + 1, image.shape[1] - o_set - 1): outer = integral_img1[i + o_set, j + o_m] - integral_img1[i + o_m, j + o_set] - integral_img[i + o_set, j - o_m] + integral_img[i + o_m, j - o_m] @@ -79,3 +86,4 @@ def _censure_octagon_loop(double[:, ::1] image, double[:, ::1] integral_img, inner += integral_img3[i - i_m, j + i_set] - integral_img3[i - i_set, j + i_m] - integral_img[-1, j + i_set + 1] - integral_img[i + i_m - 1, j + i_m] + integral_img[-1, j + i_m] + integral_img[i + i_m - 1, j + i_set + 1] filtered_image[i, j] = outer_wt * outer - (outer_wt + inner_wt) * inner + print '6' From f75fbb986be6bb7ef5a56064d39555e4ce4caaff Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Tue, 30 Jul 2013 02:54:50 +0530 Subject: [PATCH 433/736] Trying to Debug SegFault : 2 --- skimage/feature/censure.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/skimage/feature/censure.py b/skimage/feature/censure.py index f7d75680..86bafc4b 100644 --- a/skimage/feature/censure.py +++ b/skimage/feature/censure.py @@ -28,9 +28,13 @@ def _get_filtered_image(image, no_of_scales, mode): scales = np.zeros((image.shape[0], image.shape[1], no_of_scales)) integral_img = integral_image(image) integral_img1 = _slanted_integral_image_modes(image, 1) + print '8' integral_img2 = _slanted_integral_image_modes(image, 2) + print '9' integral_img3 = _slanted_integral_image_modes(image, 3) + print '10' integral_img4 = _slanted_integral_image_modes(image, 4) + print '11' for k in range(no_of_scales): n = k + 1 filtered_image = np.zeros(image.shape) @@ -54,7 +58,8 @@ def _slanted_integral_image_modes(img, mode=1): image = np.copy(img) mode1 = np.zeros((image.shape[0] + 1, image.shape[1])) _slanted_integral_image(image, mode1) - return mode1[1:, :mode1.shape[1]] + print '7' + return mode1[1:, :] elif mode == 2: image = np.copy(img) @@ -62,7 +67,8 @@ def _slanted_integral_image_modes(img, mode=1): image = np.flipud(image) mode2 = np.zeros((image.shape[0] + 1, image.shape[1])) _slanted_integral_image(image, mode2) - mode2 = mode2[1:, :mode2.shape[1]] + print '7' + mode2 = mode2[1:, :] mode2 = np.fliplr(mode2) mode2 = np.flipud(mode2) return mode2 @@ -73,7 +79,8 @@ def _slanted_integral_image_modes(img, mode=1): image = image.T mode3 = np.zeros((image.shape[0] + 1, image.shape[1])) _slanted_integral_image(image, mode3) - mode3 = mode3[1:, :mode3.shape[1]] + print '7' + mode3 = mode3[1:, :] mode3 = np.flipud(mode3.T) return mode3 @@ -83,7 +90,8 @@ def _slanted_integral_image_modes(img, mode=1): image = image.T mode4 = np.zeros((image.shape[0] + 1, image.shape[1])) _slanted_integral_image(image, mode4) - mode4 = mode4[1:, :mode4.shape[1]] + print '7' + mode4 = mode4[1:, :] mode4 = np.fliplr(mode4.T) return mode4 From 345c1690cca2213398e262e280132becce6e7873 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Tue, 30 Jul 2013 22:14:23 +0530 Subject: [PATCH 434/736] Trying to debug the Segfault-3 : Making the arrays C-contiguous --- skimage/feature/censure.py | 16 ++++++++-------- skimage/feature/censure_cy.pyx | 6 +++--- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/skimage/feature/censure.py b/skimage/feature/censure.py index 86bafc4b..36f57836 100644 --- a/skimage/feature/censure.py +++ b/skimage/feature/censure.py @@ -55,17 +55,17 @@ def _get_filtered_image(image, no_of_scales, mode): def _slanted_integral_image_modes(img, mode=1): if mode == 1: - image = np.copy(img) - mode1 = np.zeros((image.shape[0] + 1, image.shape[1])) + image = np.copy(img, order='C') + mode1 = np.zeros((image.shape[0] + 1, image.shape[1]), order='C') _slanted_integral_image(image, mode1) print '7' return mode1[1:, :] elif mode == 2: - image = np.copy(img) + image = np.copy(img, order='C') image = np.fliplr(image) image = np.flipud(image) - mode2 = np.zeros((image.shape[0] + 1, image.shape[1])) + mode2 = np.zeros((image.shape[0] + 1, image.shape[1]), order='C') _slanted_integral_image(image, mode2) print '7' mode2 = mode2[1:, :] @@ -74,10 +74,10 @@ def _slanted_integral_image_modes(img, mode=1): return mode2 elif mode == 3: - image = np.copy(img) + image = np.copy(img, order='C') image = np.flipud(image) image = image.T - mode3 = np.zeros((image.shape[0] + 1, image.shape[1])) + mode3 = np.zeros((image.shape[0] + 1, image.shape[1]), order='C') _slanted_integral_image(image, mode3) print '7' mode3 = mode3[1:, :] @@ -85,10 +85,10 @@ def _slanted_integral_image_modes(img, mode=1): return mode3 else: - image = np.copy(img) + image = np.copy(img, order='C') image = np.fliplr(image) image = image.T - mode4 = np.zeros((image.shape[0] + 1, image.shape[1])) + mode4 = np.zeros((image.shape[0] + 1, image.shape[1]), order='C') _slanted_integral_image(image, mode4) print '7' mode4 = mode4[1:, :] diff --git a/skimage/feature/censure_cy.pyx b/skimage/feature/censure_cy.pyx index 9783d201..d8cc80b5 100644 --- a/skimage/feature/censure_cy.pyx +++ b/skimage/feature/censure_cy.pyx @@ -1,7 +1,7 @@ #cython: cdivision=True -#cython: boundscheck=False -#cython: nonecheck=False -#cython: wraparound=False +#cython: boundscheck=True +#cython: nonecheck=True +#cython: wraparound=True cimport numpy as cnp import numpy as np From efba964dde645619aaacf7e8efcbe4044dda44c5 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Wed, 31 Jul 2013 05:34:03 +0530 Subject: [PATCH 435/736] Fixing many bugs; Working Censure for mode=Octagon --- skimage/feature/censure.py | 16 +++++++--------- skimage/feature/censure_cy.pyx | 24 ++++++++++-------------- 2 files changed, 17 insertions(+), 23 deletions(-) diff --git a/skimage/feature/censure.py b/skimage/feature/censure.py index 36f57836..9997ad50 100644 --- a/skimage/feature/censure.py +++ b/skimage/feature/censure.py @@ -28,13 +28,12 @@ def _get_filtered_image(image, no_of_scales, mode): scales = np.zeros((image.shape[0], image.shape[1], no_of_scales)) integral_img = integral_image(image) integral_img1 = _slanted_integral_image_modes(image, 1) - print '8' integral_img2 = _slanted_integral_image_modes(image, 2) - print '9' + integral_img2 = np.ascontiguousarray(integral_img2) integral_img3 = _slanted_integral_image_modes(image, 3) - print '10' + integral_img3 = np.ascontiguousarray(integral_img3) integral_img4 = _slanted_integral_image_modes(image, 4) - print '11' + integral_img4 = np.ascontiguousarray(integral_img4) for k in range(no_of_scales): n = k + 1 filtered_image = np.zeros(image.shape) @@ -58,16 +57,15 @@ def _slanted_integral_image_modes(img, mode=1): image = np.copy(img, order='C') mode1 = np.zeros((image.shape[0] + 1, image.shape[1]), order='C') _slanted_integral_image(image, mode1) - print '7' return mode1[1:, :] elif mode == 2: image = np.copy(img, order='C') image = np.fliplr(image) image = np.flipud(image) + image = np.ascontiguousarray(image) mode2 = np.zeros((image.shape[0] + 1, image.shape[1]), order='C') _slanted_integral_image(image, mode2) - print '7' mode2 = mode2[1:, :] mode2 = np.fliplr(mode2) mode2 = np.flipud(mode2) @@ -77,9 +75,9 @@ def _slanted_integral_image_modes(img, mode=1): image = np.copy(img, order='C') image = np.flipud(image) image = image.T + image = np.ascontiguousarray(image) mode3 = np.zeros((image.shape[0] + 1, image.shape[1]), order='C') _slanted_integral_image(image, mode3) - print '7' mode3 = mode3[1:, :] mode3 = np.flipud(mode3.T) return mode3 @@ -88,9 +86,9 @@ def _slanted_integral_image_modes(img, mode=1): image = np.copy(img, order='C') image = np.fliplr(image) image = image.T + image = np.ascontiguousarray(image) mode4 = np.zeros((image.shape[0] + 1, image.shape[1]), order='C') _slanted_integral_image(image, mode4) - print '7' mode4 = mode4[1:, :] mode4 = np.fliplr(mode4.T) return mode4 @@ -176,5 +174,5 @@ def censure_keypoints(image, no_of_scales=7, mode='DoB', threshold=0.03, rpc_thr response[:, :, i] = _suppress_line(response[:, :, i], (1 + i / 3.0), rpc_threshold) # Returning keypoints with its scale - keypoints = np.transpose(np.nonzero(response[:, :, 1:no_of_scales])) + [0, 0, 1] + keypoints = np.transpose(np.nonzero(response[:, :, 1:no_of_scales - 1])) + [0, 0, 2] return keypoints diff --git a/skimage/feature/censure_cy.pyx b/skimage/feature/censure_cy.pyx index d8cc80b5..29c91378 100644 --- a/skimage/feature/censure_cy.pyx +++ b/skimage/feature/censure_cy.pyx @@ -37,21 +37,18 @@ def _slanted_integral_image(double[:, ::1] image, right_sum_np = np.sum(image, 1).cumsum(0) - print '1' for i in range(image.shape[0]): image[i, 0] = left_sum_np[i] image[i, -1] = right_sum_np[i] - print '2' for i in range(1, integral_img.shape[0]): for j in range(integral_img.shape[1]): integral_img[i, j] = image[i - 1, j] - print '3' for i in range(1, integral_img.shape[0]): for j in range(1, integral_img.shape[1] - 1): integral_img[i, j] += integral_img[i, j - 1] + integral_img[i - 1, j + 1] - integral_img[i - 1, j] - print '4' + @@ -70,20 +67,19 @@ def _censure_octagon_loop(double[:, ::1] image, double[:, ::1] integral_img, i_m = (mi - 1) / 2 o_set = o_m + no i_set = i_m + ni - print '5' + for i in range(o_set + 1, image.shape[0] - o_set - 1): for j in range(o_set + 1, image.shape[1] - o_set - 1): - outer = integral_img1[i + o_set, j + o_m] - integral_img1[i + o_m, j + o_set] - integral_img[i + o_set, j - o_m] + integral_img[i + o_m, j - o_m] + outer = integral_img1[i + o_set, j + o_m] - integral_img1[i + o_m - 1, j + o_set + 1] - integral_img[i + o_set, j - o_m] + integral_img[i + o_m - 1, j - o_m] outer += integral_img[i + o_m - 1, j + o_m - 1] - integral_img[i - o_m, j + o_m - 1] - integral_img[i + o_m - 1, j - o_m] + integral_img[i - o_m, j - o_m] - outer += integral_img4[i + o_m, j - o_set] - integral_img4[i + o_set, j - o_m] - integral_img[i - o_m, j - o_m + 1] + integral_img[i - o_m, j - o_set - 1] - outer += integral_img2[i - o_set, j - o_m] - integral_img2[i - o_m, j - o_set] - integral_img[i - o_m + 1, -1] - integral_img[i - o_set - 1, j + o_m - 1] + integral_img[i - o_m + 1, j + o_m - 1] + integral_img[i - o_set - 1, -1] - outer += integral_img3[i - o_m, j + o_set] - integral_img3[i - o_set, j + o_m] - integral_img[-1, j + o_set + 1] - integral_img[i + o_m - 1, j + o_m] + integral_img[-1, j + o_m] + integral_img[i + o_m - 1, j + o_set + 1] + outer += integral_img4[i + o_m, j - o_set] - integral_img4[i + o_set + 1, j - o_m + 1] - integral_img[i - o_m, j - o_m] + integral_img[i - o_m, j - o_set - 1] + outer += integral_img2[i - o_set, j - o_m] - integral_img2[i - o_m + 1, j - o_set - 1] - integral_img[i - o_m, -1] - integral_img[i - o_set - 1, j + o_m - 1] + integral_img[i - o_m, j + o_m - 1] + integral_img[i - o_set - 1, -1] + outer += integral_img3[i - o_m, j + o_set] - integral_img3[i - o_set - 1, j + o_m - 1] - integral_img[-1, j + o_set] - integral_img[i + o_m - 1, j + o_m - 1] + integral_img[-1, j + o_m - 1] + integral_img[i + o_m - 1, j + o_set] - inner = integral_img1[i + i_set, j + i_m] - integral_img1[i + i_m, j + i_set] - integral_img[i + i_set, j - i_m] + integral_img[i + i_m, j - i_m] + inner = integral_img1[i + i_set, j + i_m] - integral_img1[i + i_m - 1, j + i_set + 1] - integral_img[i + i_set, j - i_m] + integral_img[i + i_m - 1, j - i_m] inner += integral_img[i + i_m - 1, j + i_m - 1] - integral_img[i - i_m, j + i_m - 1] - integral_img[i + i_m - 1, j - i_m] + integral_img[i - i_m, j - i_m] - inner += integral_img4[i + i_m, j - i_set] - integral_img4[i + i_set, j - i_m] - integral_img[i - i_m, j - i_m + 1] + integral_img[i - i_m, j - i_set - 1] - inner += integral_img2[i - i_set, j - i_m] - integral_img2[i - i_m, j - i_set] - integral_img[i - i_m + 1, -1] - integral_img[i - i_set - 1, j + i_m - 1] + integral_img[i - i_m + 1, j + i_m - 1] + integral_img[i - i_set - 1, -1] - inner += integral_img3[i - i_m, j + i_set] - integral_img3[i - i_set, j + i_m] - integral_img[-1, j + i_set + 1] - integral_img[i + i_m - 1, j + i_m] + integral_img[-1, j + i_m] + integral_img[i + i_m - 1, j + i_set + 1] + inner += integral_img4[i + i_m, j - i_set] - integral_img4[i + i_set + 1, j - i_m + 1] - integral_img[i - i_m, j - i_m] + integral_img[i - i_m, j - i_set - 1] + inner += integral_img2[i - i_set, j - i_m] - integral_img2[i - i_m + 1, j - i_set - 1] - integral_img[i - i_m, -1] - integral_img[i - i_set - 1, j + i_m - 1] + integral_img[i - i_m, j + i_m - 1] + integral_img[i - i_set - 1, -1] + inner += integral_img3[i - i_m, j + i_set] - integral_img3[i - i_set - 1, j + i_m - 1] - integral_img[-1, j + i_set] - integral_img[i + i_m - 1, j + i_m - 1] + integral_img[-1, j + i_m - 1] + integral_img[i + i_m - 1, j + i_set] filtered_image[i, j] = outer_wt * outer - (outer_wt + inner_wt) * inner - print '6' From 50a18383df275d3f131b275baccd8e8f63c58fac Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Wed, 31 Jul 2013 12:40:49 +0530 Subject: [PATCH 436/736] Adding doc for the different modes of _slanted_integral_inage_modes --- skimage/feature/censure.py | 42 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/skimage/feature/censure.py b/skimage/feature/censure.py index 9997ad50..cea2b4c3 100644 --- a/skimage/feature/censure.py +++ b/skimage/feature/censure.py @@ -54,12 +54,34 @@ def _get_filtered_image(image, no_of_scales, mode): def _slanted_integral_image_modes(img, mode=1): if mode == 1: + """ + The following figures describe area that is summed up to calculate + the value at point @ in slanted integral image. + _________________ + |********/ | + |*******/ | + |******/ | + |-----@ | + | | + | | + |_________________| + """ image = np.copy(img, order='C') mode1 = np.zeros((image.shape[0] + 1, image.shape[1]), order='C') _slanted_integral_image(image, mode1) return mode1[1:, :] elif mode == 2: + """ + _________________ + | | + | | + | | + | @_____| + | /******| + | /*******| + |________/________| + """ image = np.copy(img, order='C') image = np.fliplr(image) image = np.flipud(image) @@ -72,6 +94,16 @@ def _slanted_integral_image_modes(img, mode=1): return mode2 elif mode == 3: + """ + _________________ + | | + |\\ | + |*\\ | + |**\\ | + |***@ | + |***| | + |___|_____________| + """ image = np.copy(img, order='C') image = np.flipud(image) image = image.T @@ -83,6 +115,16 @@ def _slanted_integral_image_modes(img, mode=1): return mode3 else: + """ + ________________ + | |****| + | |****| + | @****| + | \\**| + | \\*| + | \\| + |________________| + """ image = np.copy(img, order='C') image = np.fliplr(image) image = image.T From 8c38b5c031e9ff765e594dc9fd12f9b2f82b3120 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Wed, 31 Jul 2013 12:50:52 +0530 Subject: [PATCH 437/736] Correcting the docs and making them explicit --- skimage/feature/censure.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/skimage/feature/censure.py b/skimage/feature/censure.py index cea2b4c3..9a77dfcd 100644 --- a/skimage/feature/censure.py +++ b/skimage/feature/censure.py @@ -157,20 +157,20 @@ def censure_keypoints(image, no_of_scales=7, mode='DoB', threshold=0.03, rpc_thr Input image. no_of_scales : positive integer - Number of scales to extract keypoints from. Default is 7. + Number of scales to extract keypoints from. The keypoints will be + extracted from all the scales except the first and the last. - mode : 'DoB' + mode : {'DoB', 'Octagon', 'STAR'} Type of bilevel filter used to get the scales of input image. Possible - values are 'DoB', 'Octagon' and 'STAR'. Default is 'DoB'. + values are 'DoB', 'Octagon' and 'STAR'. - threshold : + threshold : float Threshold value used to suppress maximas and minimas with a weak - magnitude response obtained after Non-Maximal Suppression. Default - is 0.03. + magnitude response obtained after Non-Maximal Suppression. - rpc_threshold : + rpc_threshold : float Threshold for rejecting interest points which have ratio of principal - curvatures greater than this value. Default is 10. + curvatures greater than this value. Returns ------- From 3b9ee0094d59f2310d66dec79eadcdc973e9bfb3 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Wed, 31 Jul 2013 12:55:04 +0530 Subject: [PATCH 438/736] Commenting the sigma passed to _suppress_line --- skimage/feature/censure.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/skimage/feature/censure.py b/skimage/feature/censure.py index 9a77dfcd..01316ba9 100644 --- a/skimage/feature/censure.py +++ b/skimage/feature/censure.py @@ -213,6 +213,9 @@ def censure_keypoints(image, no_of_scales=7, mode='DoB', threshold=0.03, rpc_thr response = maximas + minimas for i in range(1, no_of_scales - 1): + # sigma = (window_size - 1) / 6.0 + # window_size = 7 + 2 * i + # Hence sigma = 1 + i / 3.0 response[:, :, i] = _suppress_line(response[:, :, i], (1 + i / 3.0), rpc_threshold) # Returning keypoints with its scale From 7df7f3a1f0c969ec9099d44277d6f0133a85a57a Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Wed, 31 Jul 2013 12:56:07 +0530 Subject: [PATCH 439/736] Removing unnecessary data-type conversion --- skimage/feature/censure.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skimage/feature/censure.py b/skimage/feature/censure.py index 01316ba9..efb0df81 100644 --- a/skimage/feature/censure.py +++ b/skimage/feature/censure.py @@ -204,8 +204,8 @@ def censure_keypoints(image, no_of_scales=7, mode='DoB', threshold=0.03, rpc_thr # Suppressing points that are neither minima or maxima in their 3 x 3 x 3 # neighbourhood to zero - minimas = (minimum_filter(scales, (3, 3, 3)) == scales).astype(int) * scales - maximas = (maximum_filter(scales, (3, 3, 3)) == scales).astype(int) * scales + minimas = (minimum_filter(scales, (3, 3, 3)) == scales) * scales + maximas = (maximum_filter(scales, (3, 3, 3)) == scales) * scales # Suppressing minimas and maximas weaker than threshold minimas[np.abs(minimas) < threshold] = 0 From 6c1424732b9384f518736575117843101be77d6c Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Wed, 31 Jul 2013 12:58:07 +0530 Subject: [PATCH 440/736] Removing unnecessary array pre-allocation --- skimage/feature/censure.py | 1 - 1 file changed, 1 deletion(-) diff --git a/skimage/feature/censure.py b/skimage/feature/censure.py index efb0df81..0b99b529 100644 --- a/skimage/feature/censure.py +++ b/skimage/feature/censure.py @@ -199,7 +199,6 @@ def censure_keypoints(image, no_of_scales=7, mode='DoB', threshold=0.03, rpc_thr image = np.ascontiguousarray(image) # Generating all the scales - scales = np.zeros((image.shape[0], image.shape[1], no_of_scales)) scales = _get_filtered_image(image, no_of_scales, mode) # Suppressing points that are neither minima or maxima in their 3 x 3 x 3 From ec7ea0007c97f602fed00e5661e63f6293abea01 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Wed, 31 Jul 2013 12:59:35 +0530 Subject: [PATCH 441/736] Changing no_of_scales to n_scales --- skimage/feature/censure.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/skimage/feature/censure.py b/skimage/feature/censure.py index 0b99b529..b63c5207 100644 --- a/skimage/feature/censure.py +++ b/skimage/feature/censure.py @@ -8,11 +8,11 @@ from ..util import img_as_float from .censure_cy import _censure_dob_loop, _slanted_integral_image, _censure_octagon_loop -def _get_filtered_image(image, no_of_scales, mode): +def _get_filtered_image(image, n_scales, mode): # TODO : Implement the STAR mode if mode == 'DoB': - scales = np.zeros((image.shape[0], image.shape[1], no_of_scales)) - for i in range(no_of_scales): + scales = np.zeros((image.shape[0], image.shape[1], n_scales)) + for i in range(n_scales): n = i + 1 inner_wt = (1.0 / (2 * n + 1)**2) outer_wt = (1.0 / (12 * n**2 + 4 * n)) @@ -25,7 +25,7 @@ def _get_filtered_image(image, no_of_scales, mode): # TODO : Decide the shapes of Octagon filters for scales > 7 outer_shape = [(5, 2), (5, 3), (7, 3), (9, 4), (9, 7), (13, 7), (15, 10)] inner_shape = [(3, 0), (3, 1), (3, 2), (5, 2), (5, 3), (5, 4), (5, 5)] - scales = np.zeros((image.shape[0], image.shape[1], no_of_scales)) + scales = np.zeros((image.shape[0], image.shape[1], n_scales)) integral_img = integral_image(image) integral_img1 = _slanted_integral_image_modes(image, 1) integral_img2 = _slanted_integral_image_modes(image, 2) @@ -34,7 +34,7 @@ def _get_filtered_image(image, no_of_scales, mode): integral_img3 = np.ascontiguousarray(integral_img3) integral_img4 = _slanted_integral_image_modes(image, 4) integral_img4 = np.ascontiguousarray(integral_img4) - for k in range(no_of_scales): + for k in range(n_scales): n = k + 1 filtered_image = np.zeros(image.shape) mo = outer_shape[n - 1][0] @@ -146,7 +146,7 @@ def _suppress_line(response, sigma, rpc_threshold): return response -def censure_keypoints(image, no_of_scales=7, mode='DoB', threshold=0.03, rpc_threshold=10): +def censure_keypoints(image, n_scales=7, mode='DoB', threshold=0.03, rpc_threshold=10): """ Extracts Censure keypoints along with the corresponding scale using either Difference of Boxes, Octagon or STAR bilevel filter. @@ -156,7 +156,7 @@ def censure_keypoints(image, no_of_scales=7, mode='DoB', threshold=0.03, rpc_thr image : 2D ndarray Input image. - no_of_scales : positive integer + n_scales : positive integer Number of scales to extract keypoints from. The keypoints will be extracted from all the scales except the first and the last. @@ -199,7 +199,7 @@ def censure_keypoints(image, no_of_scales=7, mode='DoB', threshold=0.03, rpc_thr image = np.ascontiguousarray(image) # Generating all the scales - scales = _get_filtered_image(image, no_of_scales, mode) + scales = _get_filtered_image(image, n_scales, mode) # Suppressing points that are neither minima or maxima in their 3 x 3 x 3 # neighbourhood to zero @@ -211,12 +211,12 @@ def censure_keypoints(image, no_of_scales=7, mode='DoB', threshold=0.03, rpc_thr maximas[np.abs(maximas) < threshold] = 0 response = maximas + minimas - for i in range(1, no_of_scales - 1): + for i in range(1, n_scales - 1): # sigma = (window_size - 1) / 6.0 # window_size = 7 + 2 * i # Hence sigma = 1 + i / 3.0 response[:, :, i] = _suppress_line(response[:, :, i], (1 + i / 3.0), rpc_threshold) # Returning keypoints with its scale - keypoints = np.transpose(np.nonzero(response[:, :, 1:no_of_scales - 1])) + [0, 0, 2] + keypoints = np.transpose(np.nonzero(response[:, :, 1:n_scales - 1])) + [0, 0, 2] return keypoints From 555da870fe82bb0714d4effda7c6d200879bcfac Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Wed, 31 Jul 2013 13:18:04 +0530 Subject: [PATCH 442/736] Renaming *_wt to *_weight; correcting the object type of n in _censure_dob_loop --- skimage/feature/censure.py | 15 +++++++++------ skimage/feature/censure_cy.pyx | 10 +++++----- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/skimage/feature/censure.py b/skimage/feature/censure.py index b63c5207..1175235f 100644 --- a/skimage/feature/censure.py +++ b/skimage/feature/censure.py @@ -14,11 +14,14 @@ def _get_filtered_image(image, n_scales, mode): scales = np.zeros((image.shape[0], image.shape[1], n_scales)) for i in range(n_scales): n = i + 1 - inner_wt = (1.0 / (2 * n + 1)**2) - outer_wt = (1.0 / (12 * n**2 + 4 * n)) + # Constant multipliers for the outer region and the inner region + # of the bilevel filters with the constraint of keeping the DC bias + # 0. + inner_weight = (1.0 / (2 * n + 1)**2) + outer_weight = (1.0 / (12 * n**2 + 4 * n)) integral_img = integral_image(image) filtered_image = np.zeros(image.shape) - _censure_dob_loop(image, n, integral_img, filtered_image, inner_wt, outer_wt) + _censure_dob_loop(image, n, integral_img, filtered_image, inner_weight, outer_weight) scales[:, :, i] = filtered_image return scales elif mode == 'Octagon': @@ -43,10 +46,10 @@ def _get_filtered_image(image, n_scales, mode): ni = inner_shape[n - 1][1] outer_pixels = (mo + 2 * no)**2 - 2 * no * (no + 1) inner_pixels = (mi + 2 * ni)**2 - 2 * ni * (ni + 1) - outer_wt = 1.0 / (outer_pixels - inner_pixels) - inner_wt = 1.0 / inner_pixels + outer_weight = 1.0 / (outer_pixels - inner_pixels) + inner_weight = 1.0 / inner_pixels - _censure_octagon_loop(image, integral_img, integral_img1, integral_img2, integral_img3, integral_img4, filtered_image, outer_wt, inner_wt, mo, no, mi, ni) + _censure_octagon_loop(image, integral_img, integral_img1, integral_img2, integral_img3, integral_img4, filtered_image, outer_weight, inner_weight, mo, no, mi, ni) scales[:, :, k] = filtered_image return scales diff --git a/skimage/feature/censure_cy.pyx b/skimage/feature/censure_cy.pyx index 29c91378..75f58278 100644 --- a/skimage/feature/censure_cy.pyx +++ b/skimage/feature/censure_cy.pyx @@ -7,10 +7,10 @@ cimport numpy as cnp import numpy as np -def _censure_dob_loop(double[:, ::1] image, cnp.int16_t n, +def _censure_dob_loop(double[:, ::1] image, Py_ssize_t n, double[:, ::1] integral_img, double[:, ::1] filtered_image, - double inner_wt, double outer_wt): + double inner_weight, double outer_weight): cdef Py_ssize_t i, j cdef double inner, outer @@ -19,7 +19,7 @@ def _censure_dob_loop(double[:, ::1] image, cnp.int16_t n, for j in range(2 * n, image.shape[1] - 2 * n): inner = integral_img[i + n, j + n] + integral_img[i - n - 1, j - n - 1] - integral_img[i + n, j - n - 1] - integral_img[i - n - 1, j + n] outer = integral_img[i + 2 * n, j + 2 * n] + integral_img[i - 2 * n - 1, j - 2 * n - 1] - integral_img[i + 2 * n, j - 2 * n - 1] - integral_img[i - 2 * n - 1, j + 2 * n] - filtered_image[i, j] = outer_wt * outer - (inner_wt + outer_wt) * inner + filtered_image[i, j] = outer_weight * outer - (inner_weight + outer_weight) * inner def _slanted_integral_image(double[:, ::1] image, @@ -58,7 +58,7 @@ def _censure_octagon_loop(double[:, ::1] image, double[:, ::1] integral_img, double[:, ::1] integral_img3, double[:, ::1] integral_img4, double[:, ::1] filtered_image, - double outer_wt, double inner_wt, + double outer_weight, double inner_weight, int mo, int no, int mi, int ni): cdef Py_ssize_t i, j, o_m, i_m, o_set, i_set @@ -82,4 +82,4 @@ def _censure_octagon_loop(double[:, ::1] image, double[:, ::1] integral_img, inner += integral_img2[i - i_set, j - i_m] - integral_img2[i - i_m + 1, j - i_set - 1] - integral_img[i - i_m, -1] - integral_img[i - i_set - 1, j + i_m - 1] + integral_img[i - i_m, j + i_m - 1] + integral_img[i - i_set - 1, -1] inner += integral_img3[i - i_m, j + i_set] - integral_img3[i - i_set - 1, j + i_m - 1] - integral_img[-1, j + i_set] - integral_img[i + i_m - 1, j + i_m - 1] + integral_img[-1, j + i_m - 1] + integral_img[i + i_m - 1, j + i_set] - filtered_image[i, j] = outer_wt * outer - (outer_wt + inner_wt) * inner + filtered_image[i, j] = outer_weight * outer - (outer_weight + inner_weight) * inner From 4d9baceb8a389bd9d47245455d61482cc3eecb44 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Wed, 31 Jul 2013 17:05:25 +0530 Subject: [PATCH 443/736] Documenting the code; Removing ascontiguousarray statements --- skimage/feature/censure.py | 26 +++++++++++++++------ skimage/feature/censure_cy.pyx | 41 +++++++++++++++++++++++++--------- 2 files changed, 50 insertions(+), 17 deletions(-) diff --git a/skimage/feature/censure.py b/skimage/feature/censure.py index 1175235f..c6ab7f38 100644 --- a/skimage/feature/censure.py +++ b/skimage/feature/censure.py @@ -24,6 +24,7 @@ def _get_filtered_image(image, n_scales, mode): _censure_dob_loop(image, n, integral_img, filtered_image, inner_weight, outer_weight) scales[:, :, i] = filtered_image return scales + elif mode == 'Octagon': # TODO : Decide the shapes of Octagon filters for scales > 7 outer_shape = [(5, 2), (5, 3), (7, 3), (9, 4), (9, 7), (13, 7), (15, 10)] @@ -32,11 +33,9 @@ def _get_filtered_image(image, n_scales, mode): integral_img = integral_image(image) integral_img1 = _slanted_integral_image_modes(image, 1) integral_img2 = _slanted_integral_image_modes(image, 2) - integral_img2 = np.ascontiguousarray(integral_img2) integral_img3 = _slanted_integral_image_modes(image, 3) - integral_img3 = np.ascontiguousarray(integral_img3) integral_img4 = _slanted_integral_image_modes(image, 4) - integral_img4 = np.ascontiguousarray(integral_img4) + for k in range(n_scales): n = k + 1 filtered_image = np.zeros(image.shape) @@ -59,7 +58,11 @@ def _slanted_integral_image_modes(img, mode=1): if mode == 1: """ The following figures describe area that is summed up to calculate - the value at point @ in slanted integral image. + the value at point @ in slanted integral image. The subtended at @ is + 135 degrees. + + censure_cy._slanted_integral_image performs the mode1 + _slanted_integral_image _________________ |********/ | |*******/ | @@ -70,12 +73,17 @@ def _slanted_integral_image_modes(img, mode=1): |_________________| """ image = np.copy(img, order='C') + mode1 = np.zeros((image.shape[0] + 1, image.shape[1]), order='C') _slanted_integral_image(image, mode1) return mode1[1:, :] elif mode == 2: """ + For mode2, the image can be first flipped left-right and then up-down. + Then we can use censure_cy._slanted_integral_image and the returned + result can be flipped left-right and then up-down to get the following + mode. _________________ | | | | @@ -88,9 +96,10 @@ def _slanted_integral_image_modes(img, mode=1): image = np.copy(img, order='C') image = np.fliplr(image) image = np.flipud(image) - image = np.ascontiguousarray(image) + mode2 = np.zeros((image.shape[0] + 1, image.shape[1]), order='C') _slanted_integral_image(image, mode2) + mode2 = mode2[1:, :] mode2 = np.fliplr(mode2) mode2 = np.flipud(mode2) @@ -110,9 +119,10 @@ def _slanted_integral_image_modes(img, mode=1): image = np.copy(img, order='C') image = np.flipud(image) image = image.T - image = np.ascontiguousarray(image) + mode3 = np.zeros((image.shape[0] + 1, image.shape[1]), order='C') _slanted_integral_image(image, mode3) + mode3 = mode3[1:, :] mode3 = np.flipud(mode3.T) return mode3 @@ -131,9 +141,10 @@ def _slanted_integral_image_modes(img, mode=1): image = np.copy(img, order='C') image = np.fliplr(image) image = image.T - image = np.ascontiguousarray(image) + mode4 = np.zeros((image.shape[0] + 1, image.shape[1]), order='C') _slanted_integral_image(image, mode4) + mode4 = mode4[1:, :] mode4 = np.fliplr(mode4.T) return mode4 @@ -143,6 +154,7 @@ def _suppress_line(response, sigma, rpc_threshold): Axx, Axy, Ayy = _compute_auto_correlation(response, sigma) detA = Axx * Ayy - Axy**2 traceA = Axx + Ayy + # ratio of principal curvatures rpc = traceA**2 / (detA + 0.001) response[rpc > rpc_threshold] = 0 diff --git a/skimage/feature/censure_cy.pyx b/skimage/feature/censure_cy.pyx index 75f58278..b65b0d96 100644 --- a/skimage/feature/censure_cy.pyx +++ b/skimage/feature/censure_cy.pyx @@ -22,8 +22,8 @@ def _censure_dob_loop(double[:, ::1] image, Py_ssize_t n, filtered_image[i, j] = outer_weight * outer - (inner_weight + outer_weight) * inner -def _slanted_integral_image(double[:, ::1] image, - double[:, ::1] integral_img): +def _slanted_integral_image(double[:, :] image, + double[:, :] integral_img): cdef Py_ssize_t i, j cdef double[:] left_sum = np.zeros(image.shape[0], dtype=np.float) @@ -33,8 +33,10 @@ def _slanted_integral_image(double[:, ::1] image, left_sum[image.shape[1] - 1 - i] = np.sum(flipped_lr.diagonal(i)) left_sum_np = np.asarray(left_sum) + # Initializing the leftmost column of the slanted integral image left_sum_np = left_sum_np.cumsum(0) + # Initializing the rightmost column of the slanted integral image right_sum_np = np.sum(image, 1).cumsum(0) for i in range(image.shape[0]): @@ -50,32 +52,51 @@ def _slanted_integral_image(double[:, ::1] image, integral_img[i, j] += integral_img[i, j - 1] + integral_img[i - 1, j + 1] - integral_img[i - 1, j] - - -def _censure_octagon_loop(double[:, ::1] image, double[:, ::1] integral_img, - double[:, ::1] integral_img1, - double[:, ::1] integral_img2, - double[:, ::1] integral_img3, - double[:, ::1] integral_img4, - double[:, ::1] filtered_image, +def _censure_octagon_loop(double[:, :] image, double[:, :] integral_img, + double[:, :] integral_img1, + double[:, :] integral_img2, + double[:, :] integral_img3, + double[:, :] integral_img4, + double[:, :] filtered_image, double outer_weight, double inner_weight, int mo, int no, int mi, int ni): cdef Py_ssize_t i, j, o_m, i_m, o_set, i_set + """ + For a (5, 2) octagon, i.e. mo = 5 and no = 2, + + |---o_set---| + [0, 0, 1, 1, 1, 1, 1, 0, 0] + [0, 1, 1, 1, 1, 1, 1, 1, 0] + [1, 1, 1, 1, 1, 1, 1, 1, 1] + [1, 1, 1, 1, 1, 1, 1, 1, 1] + [1, 1, 1, 1, 1, 1, 1, 1, 1] + [1, 1, 1, 1, 1, 1, 1, 1, 1] + [1, 1, 1, 1, 1, 1, 1, 1, 1] + [0, 1, 1, 1, 1, 1, 1, 1, 0] + [0, 0, 1, 1, 1, 1, 1, 0, 0] + |-o_m-| + """ o_m = (mo - 1) / 2 i_m = (mi - 1) / 2 + + # o_set and i_set are the distances of the center of the octagon + # from the horizontal or vertical sides of the octagon, + # for outer and inner octagon respectively o_set = o_m + no i_set = i_m + ni for i in range(o_set + 1, image.shape[0] - o_set - 1): for j in range(o_set + 1, image.shape[1] - o_set - 1): + # Calculating the sum of pixels in the outer octagon outer = integral_img1[i + o_set, j + o_m] - integral_img1[i + o_m - 1, j + o_set + 1] - integral_img[i + o_set, j - o_m] + integral_img[i + o_m - 1, j - o_m] outer += integral_img[i + o_m - 1, j + o_m - 1] - integral_img[i - o_m, j + o_m - 1] - integral_img[i + o_m - 1, j - o_m] + integral_img[i - o_m, j - o_m] outer += integral_img4[i + o_m, j - o_set] - integral_img4[i + o_set + 1, j - o_m + 1] - integral_img[i - o_m, j - o_m] + integral_img[i - o_m, j - o_set - 1] outer += integral_img2[i - o_set, j - o_m] - integral_img2[i - o_m + 1, j - o_set - 1] - integral_img[i - o_m, -1] - integral_img[i - o_set - 1, j + o_m - 1] + integral_img[i - o_m, j + o_m - 1] + integral_img[i - o_set - 1, -1] outer += integral_img3[i - o_m, j + o_set] - integral_img3[i - o_set - 1, j + o_m - 1] - integral_img[-1, j + o_set] - integral_img[i + o_m - 1, j + o_m - 1] + integral_img[-1, j + o_m - 1] + integral_img[i + o_m - 1, j + o_set] + # Calculating the sum of pixels in the inner octagon inner = integral_img1[i + i_set, j + i_m] - integral_img1[i + i_m - 1, j + i_set + 1] - integral_img[i + i_set, j - i_m] + integral_img[i + i_m - 1, j - i_m] inner += integral_img[i + i_m - 1, j + i_m - 1] - integral_img[i - i_m, j + i_m - 1] - integral_img[i + i_m - 1, j - i_m] + integral_img[i - i_m, j - i_m] inner += integral_img4[i + i_m, j - i_set] - integral_img4[i + i_set + 1, j - i_m + 1] - integral_img[i - i_m, j - i_m] + integral_img[i - i_m, j - i_set - 1] From 7478f2c79656d61eb1b6d886334b1c861bd57b5c Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Wed, 31 Jul 2013 21:40:25 +0530 Subject: [PATCH 444/736] Correcting indentation --- skimage/feature/censure_cy.pyx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/skimage/feature/censure_cy.pyx b/skimage/feature/censure_cy.pyx index b65b0d96..92d5a142 100644 --- a/skimage/feature/censure_cy.pyx +++ b/skimage/feature/censure_cy.pyx @@ -8,9 +8,9 @@ import numpy as np def _censure_dob_loop(double[:, ::1] image, Py_ssize_t n, - double[:, ::1] integral_img, - double[:, ::1] filtered_image, - double inner_weight, double outer_weight): + double[:, ::1] integral_img, + double[:, ::1] filtered_image, + double inner_weight, double outer_weight): cdef Py_ssize_t i, j cdef double inner, outer From 3318886ff30e2c2a564e5eb277061ad6da9c157c Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Wed, 31 Jul 2013 22:40:12 +0530 Subject: [PATCH 445/736] Using convolve in _get_filtered_image for mode=Octagon --- skimage/feature/censure.py | 56 ++++++++++++++++++++++++++++++++++---- 1 file changed, 51 insertions(+), 5 deletions(-) diff --git a/skimage/feature/censure.py b/skimage/feature/censure.py index c6ab7f38..7f2ae816 100644 --- a/skimage/feature/censure.py +++ b/skimage/feature/censure.py @@ -1,17 +1,18 @@ import numpy as np -from scipy.ndimage.filters import maximum_filter, minimum_filter +from scipy.ndimage.filters import maximum_filter, minimum_filter, convolve from ..transform import integral_image from ..feature.corner import _compute_auto_correlation from ..util import img_as_float +from ..morphology import convex_hull_image from .censure_cy import _censure_dob_loop, _slanted_integral_image, _censure_octagon_loop def _get_filtered_image(image, n_scales, mode): # TODO : Implement the STAR mode + scales = np.zeros((image.shape[0], image.shape[1], n_scales), dtype=np.double) if mode == 'DoB': - scales = np.zeros((image.shape[0], image.shape[1], n_scales)) for i in range(n_scales): n = i + 1 # Constant multipliers for the outer region and the inner region @@ -23,13 +24,15 @@ def _get_filtered_image(image, n_scales, mode): filtered_image = np.zeros(image.shape) _censure_dob_loop(image, n, integral_img, filtered_image, inner_weight, outer_weight) scales[:, :, i] = filtered_image - return scales + elif mode == 'Octagon': # TODO : Decide the shapes of Octagon filters for scales > 7 outer_shape = [(5, 2), (5, 3), (7, 3), (9, 4), (9, 7), (13, 7), (15, 10)] inner_shape = [(3, 0), (3, 1), (3, 2), (5, 2), (5, 3), (5, 4), (5, 5)] - scales = np.zeros((image.shape[0], image.shape[1], n_scales)) + for i in range(n_scales): + scales[:, :, i] = convolve(image, _octagon_filter(outer_shape[i][0], outer_shape[i][1], inner_shape[i][0], inner_shape[i][1])) + """ integral_img = integral_image(image) integral_img1 = _slanted_integral_image_modes(image, 1) integral_img2 = _slanted_integral_image_modes(image, 2) @@ -51,7 +54,50 @@ def _get_filtered_image(image, n_scales, mode): _censure_octagon_loop(image, integral_img, integral_img1, integral_img2, integral_img3, integral_img4, filtered_image, outer_weight, inner_weight, mo, no, mi, ni) scales[:, :, k] = filtered_image - return scales + """ + return scales + + +def _oct(m, n): + f = np.zeros((m + 2*n, m + 2*n)) + f[0, n] = 1 + f[n, 0] = 1 + f[0, m + n -1] = 1 + f[m + n - 1, 0] = 1 + f[-1, n] = 1 + f[n, -1] = 1 + f[-1, m + n - 1] = 1 + f[m + n - 1, -1] = 1 + return convex_hull_image(f).astype(int) + + +def _octagon_filter(mo, no, mi, ni): + outer = (mo + 2 * no)**2 - 2 * no * (no + 1) + inner = (mi + 2 * ni)**2 - 2 * ni * (ni + 1) + outer_wt = 1.0 / (outer - inner) + inner_wt = 1.0 / inner + c = ((mo + 2 * no) - (mi + 2 * ni)) / 2 + outer_oct = _oct(mo, no) + inner_oct = np.zeros((mo + 2 * no, mo + 2 * no)) + inner_oct[c:-c, c:-c] = _oct(mi, ni) + bfilter = outer_wt * outer_oct - (outer_wt + inner_wt) * inner_oct + return bfilter + + +def _filter_using_convolve(image, n, mode='DoB'): + + if mode == 'DoB': + inner_wt = (1.0 / (2*n + 1)**2) + outer_wt = (1.0 / (12*n**2 + 4*n)) + dob_filter = np.zeros((4 * n + 1, 4 * n + 1)) + dob_filter[:] = outer_wt + dob_filter[n : 3 * n + 1, n : 3 * n + 1] = - inner_wt + return convolve(image, dob_filter) + + elif mode == 'Octagon': + outer_shape = [(5, 2), (5, 3), (7, 3), (9, 4), (9, 7), (13, 7), (15, 10)] + inner_shape = [(3, 0), (3, 1), (3, 2), (5, 2), (5, 3), (5, 4), (5, 5)] + return convolve(image, _octagon_filter(outer_shape[n - 1][0], outer_shape[n - 1][1], inner_shape[n - 1][0], inner_shape[n - 1][1])) def _slanted_integral_image_modes(img, mode=1): From 344374e290ee820d698e67dd68a66e0179b6f973 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Wed, 31 Jul 2013 23:15:45 +0530 Subject: [PATCH 446/736] Removing slanted_integral_image Cython functions for mode=Octagon --- skimage/feature/censure_cy.pyx | 84 ---------------------------------- 1 file changed, 84 deletions(-) diff --git a/skimage/feature/censure_cy.pyx b/skimage/feature/censure_cy.pyx index 92d5a142..2d71f0dd 100644 --- a/skimage/feature/censure_cy.pyx +++ b/skimage/feature/censure_cy.pyx @@ -20,87 +20,3 @@ def _censure_dob_loop(double[:, ::1] image, Py_ssize_t n, inner = integral_img[i + n, j + n] + integral_img[i - n - 1, j - n - 1] - integral_img[i + n, j - n - 1] - integral_img[i - n - 1, j + n] outer = integral_img[i + 2 * n, j + 2 * n] + integral_img[i - 2 * n - 1, j - 2 * n - 1] - integral_img[i + 2 * n, j - 2 * n - 1] - integral_img[i - 2 * n - 1, j + 2 * n] filtered_image[i, j] = outer_weight * outer - (inner_weight + outer_weight) * inner - - -def _slanted_integral_image(double[:, :] image, - double[:, :] integral_img): - - cdef Py_ssize_t i, j - cdef double[:] left_sum = np.zeros(image.shape[0], dtype=np.float) - - flipped_lr = np.asarray(image[:, ::-1]) - for i in range(image.shape[1] - image.shape[0], image.shape[1]): - left_sum[image.shape[1] - 1 - i] = np.sum(flipped_lr.diagonal(i)) - left_sum_np = np.asarray(left_sum) - - # Initializing the leftmost column of the slanted integral image - left_sum_np = left_sum_np.cumsum(0) - - # Initializing the rightmost column of the slanted integral image - right_sum_np = np.sum(image, 1).cumsum(0) - - for i in range(image.shape[0]): - image[i, 0] = left_sum_np[i] - image[i, -1] = right_sum_np[i] - - for i in range(1, integral_img.shape[0]): - for j in range(integral_img.shape[1]): - integral_img[i, j] = image[i - 1, j] - - for i in range(1, integral_img.shape[0]): - for j in range(1, integral_img.shape[1] - 1): - integral_img[i, j] += integral_img[i, j - 1] + integral_img[i - 1, j + 1] - integral_img[i - 1, j] - - -def _censure_octagon_loop(double[:, :] image, double[:, :] integral_img, - double[:, :] integral_img1, - double[:, :] integral_img2, - double[:, :] integral_img3, - double[:, :] integral_img4, - double[:, :] filtered_image, - double outer_weight, double inner_weight, - int mo, int no, int mi, int ni): - - cdef Py_ssize_t i, j, o_m, i_m, o_set, i_set - - """ - For a (5, 2) octagon, i.e. mo = 5 and no = 2, - - |---o_set---| - [0, 0, 1, 1, 1, 1, 1, 0, 0] - [0, 1, 1, 1, 1, 1, 1, 1, 0] - [1, 1, 1, 1, 1, 1, 1, 1, 1] - [1, 1, 1, 1, 1, 1, 1, 1, 1] - [1, 1, 1, 1, 1, 1, 1, 1, 1] - [1, 1, 1, 1, 1, 1, 1, 1, 1] - [1, 1, 1, 1, 1, 1, 1, 1, 1] - [0, 1, 1, 1, 1, 1, 1, 1, 0] - [0, 0, 1, 1, 1, 1, 1, 0, 0] - |-o_m-| - """ - o_m = (mo - 1) / 2 - i_m = (mi - 1) / 2 - - # o_set and i_set are the distances of the center of the octagon - # from the horizontal or vertical sides of the octagon, - # for outer and inner octagon respectively - o_set = o_m + no - i_set = i_m + ni - - for i in range(o_set + 1, image.shape[0] - o_set - 1): - for j in range(o_set + 1, image.shape[1] - o_set - 1): - # Calculating the sum of pixels in the outer octagon - outer = integral_img1[i + o_set, j + o_m] - integral_img1[i + o_m - 1, j + o_set + 1] - integral_img[i + o_set, j - o_m] + integral_img[i + o_m - 1, j - o_m] - outer += integral_img[i + o_m - 1, j + o_m - 1] - integral_img[i - o_m, j + o_m - 1] - integral_img[i + o_m - 1, j - o_m] + integral_img[i - o_m, j - o_m] - outer += integral_img4[i + o_m, j - o_set] - integral_img4[i + o_set + 1, j - o_m + 1] - integral_img[i - o_m, j - o_m] + integral_img[i - o_m, j - o_set - 1] - outer += integral_img2[i - o_set, j - o_m] - integral_img2[i - o_m + 1, j - o_set - 1] - integral_img[i - o_m, -1] - integral_img[i - o_set - 1, j + o_m - 1] + integral_img[i - o_m, j + o_m - 1] + integral_img[i - o_set - 1, -1] - outer += integral_img3[i - o_m, j + o_set] - integral_img3[i - o_set - 1, j + o_m - 1] - integral_img[-1, j + o_set] - integral_img[i + o_m - 1, j + o_m - 1] + integral_img[-1, j + o_m - 1] + integral_img[i + o_m - 1, j + o_set] - - # Calculating the sum of pixels in the inner octagon - inner = integral_img1[i + i_set, j + i_m] - integral_img1[i + i_m - 1, j + i_set + 1] - integral_img[i + i_set, j - i_m] + integral_img[i + i_m - 1, j - i_m] - inner += integral_img[i + i_m - 1, j + i_m - 1] - integral_img[i - i_m, j + i_m - 1] - integral_img[i + i_m - 1, j - i_m] + integral_img[i - i_m, j - i_m] - inner += integral_img4[i + i_m, j - i_set] - integral_img4[i + i_set + 1, j - i_m + 1] - integral_img[i - i_m, j - i_m] + integral_img[i - i_m, j - i_set - 1] - inner += integral_img2[i - i_set, j - i_m] - integral_img2[i - i_m + 1, j - i_set - 1] - integral_img[i - i_m, -1] - integral_img[i - i_set - 1, j + i_m - 1] + integral_img[i - i_m, j + i_m - 1] + integral_img[i - i_set - 1, -1] - inner += integral_img3[i - i_m, j + i_set] - integral_img3[i - i_set - 1, j + i_m - 1] - integral_img[-1, j + i_set] - integral_img[i + i_m - 1, j + i_m - 1] + integral_img[-1, j + i_m - 1] + integral_img[i + i_m - 1, j + i_set] - - filtered_image[i, j] = outer_weight * outer - (outer_weight + inner_weight) * inner From 5ee71cba9b0bd97fa7ee15c76936f1f0a2d3c49c Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Wed, 31 Jul 2013 23:16:47 +0530 Subject: [PATCH 447/736] Reverting back to non-debugging mode in Cython --- skimage/feature/censure_cy.pyx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/skimage/feature/censure_cy.pyx b/skimage/feature/censure_cy.pyx index 2d71f0dd..1c007609 100644 --- a/skimage/feature/censure_cy.pyx +++ b/skimage/feature/censure_cy.pyx @@ -1,7 +1,7 @@ #cython: cdivision=True -#cython: boundscheck=True -#cython: nonecheck=True -#cython: wraparound=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False cimport numpy as cnp import numpy as np From 599ce8f7bd606d7b884c81b926c63b865b722391 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Wed, 31 Jul 2013 23:20:59 +0530 Subject: [PATCH 448/736] Remove all the slanted integral image functions and the vivid graphics from censure.py --- skimage/feature/censure.py | 138 +------------------------------------ 1 file changed, 2 insertions(+), 136 deletions(-) diff --git a/skimage/feature/censure.py b/skimage/feature/censure.py index 7f2ae816..9acbf7cd 100644 --- a/skimage/feature/censure.py +++ b/skimage/feature/censure.py @@ -6,7 +6,7 @@ from ..feature.corner import _compute_auto_correlation from ..util import img_as_float from ..morphology import convex_hull_image -from .censure_cy import _censure_dob_loop, _slanted_integral_image, _censure_octagon_loop +from .censure_cy import _censure_dob_loop def _get_filtered_image(image, n_scales, mode): @@ -25,39 +25,17 @@ def _get_filtered_image(image, n_scales, mode): _censure_dob_loop(image, n, integral_img, filtered_image, inner_weight, outer_weight) scales[:, :, i] = filtered_image - elif mode == 'Octagon': # TODO : Decide the shapes of Octagon filters for scales > 7 outer_shape = [(5, 2), (5, 3), (7, 3), (9, 4), (9, 7), (13, 7), (15, 10)] inner_shape = [(3, 0), (3, 1), (3, 2), (5, 2), (5, 3), (5, 4), (5, 5)] for i in range(n_scales): scales[:, :, i] = convolve(image, _octagon_filter(outer_shape[i][0], outer_shape[i][1], inner_shape[i][0], inner_shape[i][1])) - """ - integral_img = integral_image(image) - integral_img1 = _slanted_integral_image_modes(image, 1) - integral_img2 = _slanted_integral_image_modes(image, 2) - integral_img3 = _slanted_integral_image_modes(image, 3) - integral_img4 = _slanted_integral_image_modes(image, 4) - for k in range(n_scales): - n = k + 1 - filtered_image = np.zeros(image.shape) - mo = outer_shape[n - 1][0] - no = outer_shape[n - 1][1] - mi = inner_shape[n - 1][0] - ni = inner_shape[n - 1][1] - outer_pixels = (mo + 2 * no)**2 - 2 * no * (no + 1) - inner_pixels = (mi + 2 * ni)**2 - 2 * ni * (ni + 1) - outer_weight = 1.0 / (outer_pixels - inner_pixels) - inner_weight = 1.0 / inner_pixels - - _censure_octagon_loop(image, integral_img, integral_img1, integral_img2, integral_img3, integral_img4, filtered_image, outer_weight, inner_weight, mo, no, mi, ni) - - scales[:, :, k] = filtered_image - """ return scales +# TODO : Import from selem after getting #669 merged. def _oct(m, n): f = np.zeros((m + 2*n, m + 2*n)) f[0, n] = 1 @@ -84,118 +62,6 @@ def _octagon_filter(mo, no, mi, ni): return bfilter -def _filter_using_convolve(image, n, mode='DoB'): - - if mode == 'DoB': - inner_wt = (1.0 / (2*n + 1)**2) - outer_wt = (1.0 / (12*n**2 + 4*n)) - dob_filter = np.zeros((4 * n + 1, 4 * n + 1)) - dob_filter[:] = outer_wt - dob_filter[n : 3 * n + 1, n : 3 * n + 1] = - inner_wt - return convolve(image, dob_filter) - - elif mode == 'Octagon': - outer_shape = [(5, 2), (5, 3), (7, 3), (9, 4), (9, 7), (13, 7), (15, 10)] - inner_shape = [(3, 0), (3, 1), (3, 2), (5, 2), (5, 3), (5, 4), (5, 5)] - return convolve(image, _octagon_filter(outer_shape[n - 1][0], outer_shape[n - 1][1], inner_shape[n - 1][0], inner_shape[n - 1][1])) - - -def _slanted_integral_image_modes(img, mode=1): - if mode == 1: - """ - The following figures describe area that is summed up to calculate - the value at point @ in slanted integral image. The subtended at @ is - 135 degrees. - - censure_cy._slanted_integral_image performs the mode1 - _slanted_integral_image - _________________ - |********/ | - |*******/ | - |******/ | - |-----@ | - | | - | | - |_________________| - """ - image = np.copy(img, order='C') - - mode1 = np.zeros((image.shape[0] + 1, image.shape[1]), order='C') - _slanted_integral_image(image, mode1) - return mode1[1:, :] - - elif mode == 2: - """ - For mode2, the image can be first flipped left-right and then up-down. - Then we can use censure_cy._slanted_integral_image and the returned - result can be flipped left-right and then up-down to get the following - mode. - _________________ - | | - | | - | | - | @_____| - | /******| - | /*******| - |________/________| - """ - image = np.copy(img, order='C') - image = np.fliplr(image) - image = np.flipud(image) - - mode2 = np.zeros((image.shape[0] + 1, image.shape[1]), order='C') - _slanted_integral_image(image, mode2) - - mode2 = mode2[1:, :] - mode2 = np.fliplr(mode2) - mode2 = np.flipud(mode2) - return mode2 - - elif mode == 3: - """ - _________________ - | | - |\\ | - |*\\ | - |**\\ | - |***@ | - |***| | - |___|_____________| - """ - image = np.copy(img, order='C') - image = np.flipud(image) - image = image.T - - mode3 = np.zeros((image.shape[0] + 1, image.shape[1]), order='C') - _slanted_integral_image(image, mode3) - - mode3 = mode3[1:, :] - mode3 = np.flipud(mode3.T) - return mode3 - - else: - """ - ________________ - | |****| - | |****| - | @****| - | \\**| - | \\*| - | \\| - |________________| - """ - image = np.copy(img, order='C') - image = np.fliplr(image) - image = image.T - - mode4 = np.zeros((image.shape[0] + 1, image.shape[1]), order='C') - _slanted_integral_image(image, mode4) - - mode4 = mode4[1:, :] - mode4 = np.fliplr(mode4.T) - return mode4 - - def _suppress_line(response, sigma, rpc_threshold): Axx, Axy, Ayy = _compute_auto_correlation(response, sigma) detA = Axx * Ayy - Axy**2 From 6472dd10766d36a6e54ba492c9b6441c45504400 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Wed, 31 Jul 2013 23:40:23 +0530 Subject: [PATCH 449/736] Wrapping the long lines --- skimage/feature/censure.py | 33 ++++++++++++++++++++++++--------- skimage/feature/censure_cy.pyx | 15 ++++++++++++--- 2 files changed, 36 insertions(+), 12 deletions(-) diff --git a/skimage/feature/censure.py b/skimage/feature/censure.py index 9acbf7cd..8b8e1ae4 100644 --- a/skimage/feature/censure.py +++ b/skimage/feature/censure.py @@ -11,26 +11,38 @@ from .censure_cy import _censure_dob_loop def _get_filtered_image(image, n_scales, mode): # TODO : Implement the STAR mode - scales = np.zeros((image.shape[0], image.shape[1], n_scales), dtype=np.double) + scales = np.zeros((image.shape[0], image.shape[1], n_scales), + dtype=np.double) + if mode == 'DoB': for i in range(n_scales): n = i + 1 + # Constant multipliers for the outer region and the inner region - # of the bilevel filters with the constraint of keeping the DC bias - # 0. + # of the bilevel filters with the constraint of keeping the + # DC bias 0. inner_weight = (1.0 / (2 * n + 1)**2) outer_weight = (1.0 / (12 * n**2 + 4 * n)) + integral_img = integral_image(image) + filtered_image = np.zeros(image.shape) - _censure_dob_loop(image, n, integral_img, filtered_image, inner_weight, outer_weight) + _censure_dob_loop(image, n, integral_img, filtered_image, + inner_weight, outer_weight) + scales[:, :, i] = filtered_image elif mode == 'Octagon': # TODO : Decide the shapes of Octagon filters for scales > 7 - outer_shape = [(5, 2), (5, 3), (7, 3), (9, 4), (9, 7), (13, 7), (15, 10)] + outer_shape = [(5, 2), (5, 3), (7, 3), (9, 4), (9, 7), (13, 7), + (15, 10)] inner_shape = [(3, 0), (3, 1), (3, 2), (5, 2), (5, 3), (5, 4), (5, 5)] + for i in range(n_scales): - scales[:, :, i] = convolve(image, _octagon_filter(outer_shape[i][0], outer_shape[i][1], inner_shape[i][0], inner_shape[i][1])) + scales[:, :, i] = convolve(image, + _octagon_filter(outer_shape[i][0], + outer_shape[i][1], inner_shape[i][0], + inner_shape[i][1])) return scales @@ -73,7 +85,8 @@ def _suppress_line(response, sigma, rpc_threshold): return response -def censure_keypoints(image, n_scales=7, mode='DoB', threshold=0.03, rpc_threshold=10): +def censure_keypoints(image, n_scales=7, mode='DoB', threshold=0.03, + rpc_threshold=10): """ Extracts Censure keypoints along with the corresponding scale using either Difference of Boxes, Octagon or STAR bilevel filter. @@ -142,8 +155,10 @@ def censure_keypoints(image, n_scales=7, mode='DoB', threshold=0.03, rpc_thresho # sigma = (window_size - 1) / 6.0 # window_size = 7 + 2 * i # Hence sigma = 1 + i / 3.0 - response[:, :, i] = _suppress_line(response[:, :, i], (1 + i / 3.0), rpc_threshold) + response[:, :, i] = _suppress_line(response[:, :, i], (1 + i / 3.0), + rpc_threshold) # Returning keypoints with its scale - keypoints = np.transpose(np.nonzero(response[:, :, 1:n_scales - 1])) + [0, 0, 2] + keypoints = (np.transpose(np.nonzero(response[:, :, 1:n_scales - 1])) + + [0, 0, 2]) return keypoints diff --git a/skimage/feature/censure_cy.pyx b/skimage/feature/censure_cy.pyx index 1c007609..93c7c142 100644 --- a/skimage/feature/censure_cy.pyx +++ b/skimage/feature/censure_cy.pyx @@ -17,6 +17,15 @@ def _censure_dob_loop(double[:, ::1] image, Py_ssize_t n, for i in range(2 * n, image.shape[0] - 2 * n): for j in range(2 * n, image.shape[1] - 2 * n): - inner = integral_img[i + n, j + n] + integral_img[i - n - 1, j - n - 1] - integral_img[i + n, j - n - 1] - integral_img[i - n - 1, j + n] - outer = integral_img[i + 2 * n, j + 2 * n] + integral_img[i - 2 * n - 1, j - 2 * n - 1] - integral_img[i + 2 * n, j - 2 * n - 1] - integral_img[i - 2 * n - 1, j + 2 * n] - filtered_image[i, j] = outer_weight * outer - (inner_weight + outer_weight) * inner + inner = (integral_img[i + n, j + n] + + integral_img[i - n - 1, j - n - 1] + - integral_img[i + n, j - n - 1] + - integral_img[i - n - 1, j + n]) + + outer = (integral_img[i + 2 * n, j + 2 * n] + + integral_img[i - 2 * n - 1, j - 2 * n - 1] + - integral_img[i + 2 * n, j - 2 * n - 1] + - integral_img[i - 2 * n - 1, j + 2 * n]) + + filtered_image[i, j] = (outer_weight * outer + - (inner_weight + outer_weight) * inner) From 361652cec209e8efb975af26b77bf87cbc893dda Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Thu, 1 Aug 2013 01:11:26 +0530 Subject: [PATCH 450/736] Added a NOTE explaining the preference of convolve over slanted integral image --- skimage/feature/censure.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/skimage/feature/censure.py b/skimage/feature/censure.py index 8b8e1ae4..d0ac94a9 100644 --- a/skimage/feature/censure.py +++ b/skimage/feature/censure.py @@ -32,12 +32,18 @@ def _get_filtered_image(image, n_scales, mode): scales[:, :, i] = filtered_image + # NOTE : For the Octagon shaped filter, we implemented and evaluated the + # slanted integral image based image filtering but the performance was + # more or less equal to image filtering using + # scipy.ndimage.filters.convolve(). Hence we have decided to use the + # later for a much cleaner implementation. elif mode == 'Octagon': # TODO : Decide the shapes of Octagon filters for scales > 7 outer_shape = [(5, 2), (5, 3), (7, 3), (9, 4), (9, 7), (13, 7), (15, 10)] inner_shape = [(3, 0), (3, 1), (3, 2), (5, 2), (5, 3), (5, 4), (5, 5)] + # for i in range(n_scales): scales[:, :, i] = convolve(image, _octagon_filter(outer_shape[i][0], From 212874fae340f34b1ff14ccbc7bcfffd9476e0e4 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Fri, 2 Aug 2013 13:35:29 +0530 Subject: [PATCH 451/736] Adding the STAR mode --- skimage/feature/censure.py | 52 ++++++++++++++++++++++++++++++++++---- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/skimage/feature/censure.py b/skimage/feature/censure.py index d0ac94a9..179eac70 100644 --- a/skimage/feature/censure.py +++ b/skimage/feature/censure.py @@ -49,6 +49,15 @@ def _get_filtered_image(image, n_scales, mode): _octagon_filter(outer_shape[i][0], outer_shape[i][1], inner_shape[i][0], inner_shape[i][1])) + else: + shape = [1, 2, 3, 4, 6, 8, 11, 12, 16, 22, 23, 32, 45, 46, 64, 90, + 128] + filter_shape = [(1, 0), (3, 1), (4, 2), (5, 3), (7, 4), (8, 5), + (9, 6),(11, 8), (13, 10), (14, 11), (15, 12), (16, 14)] + for i in range(n_scales): + scales[:, :, i] = convolve(image, + _star_filter(shape[filter_shape[i][0]], + shape[filter_shape[i][1]])) return scales @@ -70,13 +79,46 @@ def _oct(m, n): def _octagon_filter(mo, no, mi, ni): outer = (mo + 2 * no)**2 - 2 * no * (no + 1) inner = (mi + 2 * ni)**2 - 2 * ni * (ni + 1) - outer_wt = 1.0 / (outer - inner) - inner_wt = 1.0 / inner + outer_weight = 1.0 / (outer - inner) + inner_weight = 1.0 / inner c = ((mo + 2 * no) - (mi + 2 * ni)) / 2 outer_oct = _oct(mo, no) inner_oct = np.zeros((mo + 2 * no, mo + 2 * no)) - inner_oct[c:-c, c:-c] = _oct(mi, ni) - bfilter = outer_wt * outer_oct - (outer_wt + inner_wt) * inner_oct + inner_oct[c: -c, c: -c] = _oct(mi, ni) + bfilter = (outer_weight * outer_oct - + (outer_weight + inner_weight) * inner_oct) + return bfilter + + +def _star(a): + if a == 1: + bfilter = np.zeros((3, 3)) + bfilter[:] = 1 + return bfilter + m = 2 * a + 1 + n = a / 2 + selem_square = np.zeros((m + 2 * n, m + 2 * n), dtype=np.uint8) + selem_square[n: m + n, n: m + n] = 1 + selem_triangle = np.zeros((m + 2 * n, m + 2 * n), dtype=np.uint8) + selem_triangle[(m + 2 * n - 1) / 2, 0] = 1 + selem_triangle[(m + 1) / 2, n - 1] = 1 + selem_triangle[(m + 4 * n - 3) / 2, n - 1] = 1 + selem_triangle = convex_hull_image(selem_triangle).astype(int) + selem_triangle += selem_triangle[:, ::-1] + selem_triangle.T + selem_triangle.T[::-1, :] + return selem_square + selem_triangle + + +def _star_filter(m, n): + outer = 4 * m**2 + 4 * m + 1 + 4 * (m / 2)**2 + inner = 4 * n**2 + 4 * n + 1 + 4 * (n / 2)**2 + outer_weight = 1.0 / (outer - inner) + inner_weight = 1.0 / inner + c = m + m / 2 - n - n / 2 + outer_star = _star(m) + inner_star = np.zeros((outer_star.shape)) + inner_star[c: -c, c: -c] = _star(n) + bfilter = (outer_weight * outer_star - + (outer_weight + inner_weight) * inner_star) return bfilter @@ -106,7 +148,7 @@ def censure_keypoints(image, n_scales=7, mode='DoB', threshold=0.03, Number of scales to extract keypoints from. The keypoints will be extracted from all the scales except the first and the last. - mode : {'DoB', 'Octagon', 'STAR'} + mode : ('DoB', 'Octagon', 'STAR') Type of bilevel filter used to get the scales of input image. Possible values are 'DoB', 'Octagon' and 'STAR'. From 8ef8ba59d0c8792fe5618da0fe4ab415e355c855 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Fri, 2 Aug 2013 19:02:16 +0530 Subject: [PATCH 452/736] Making the weight calculation statements more readable --- skimage/feature/censure.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/skimage/feature/censure.py b/skimage/feature/censure.py index 179eac70..1f3506bb 100644 --- a/skimage/feature/censure.py +++ b/skimage/feature/censure.py @@ -109,14 +109,12 @@ def _star(a): def _star_filter(m, n): - outer = 4 * m**2 + 4 * m + 1 + 4 * (m / 2)**2 - inner = 4 * n**2 + 4 * n + 1 + 4 * (n / 2)**2 - outer_weight = 1.0 / (outer - inner) - inner_weight = 1.0 / inner c = m + m / 2 - n - n / 2 outer_star = _star(m) inner_star = np.zeros((outer_star.shape)) inner_star[c: -c, c: -c] = _star(n) + outer_weight = 1.0 / (np.sum(outer_star - inner_star)) + inner_weight = 1.0 / np.sum(inner_star) bfilter = (outer_weight * outer_star - (outer_weight + inner_weight) * inner_star) return bfilter From 973ab0a5480ad4c2fa6d997aa88f01d6dbb5e36f Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Sat, 3 Aug 2013 14:14:35 +0530 Subject: [PATCH 453/736] PEP8 corrections --- skimage/feature/censure.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/skimage/feature/censure.py b/skimage/feature/censure.py index 1f3506bb..6ca6fef4 100644 --- a/skimage/feature/censure.py +++ b/skimage/feature/censure.py @@ -51,7 +51,7 @@ def _get_filtered_image(image, n_scales, mode): inner_shape[i][1])) else: shape = [1, 2, 3, 4, 6, 8, 11, 12, 16, 22, 23, 32, 45, 46, 64, 90, - 128] + 128] filter_shape = [(1, 0), (3, 1), (4, 2), (5, 3), (7, 4), (8, 5), (9, 6),(11, 8), (13, 10), (14, 11), (15, 12), (16, 14)] for i in range(n_scales): @@ -104,7 +104,8 @@ def _star(a): selem_triangle[(m + 1) / 2, n - 1] = 1 selem_triangle[(m + 4 * n - 3) / 2, n - 1] = 1 selem_triangle = convex_hull_image(selem_triangle).astype(int) - selem_triangle += selem_triangle[:, ::-1] + selem_triangle.T + selem_triangle.T[::-1, :] + selem_triangle += (selem_triangle[:, ::-1] + selem_triangle.T + + selem_triangle.T[::-1, :]) return selem_square + selem_triangle From 87ef133e809e6399ed65638ae7cb2ada4446121d Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Mon, 5 Aug 2013 13:26:59 +0530 Subject: [PATCH 454/736] Replacing threshold by nms_threshold --- skimage/feature/censure.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/skimage/feature/censure.py b/skimage/feature/censure.py index 6ca6fef4..0ad69fca 100644 --- a/skimage/feature/censure.py +++ b/skimage/feature/censure.py @@ -132,7 +132,7 @@ def _suppress_line(response, sigma, rpc_threshold): return response -def censure_keypoints(image, n_scales=7, mode='DoB', threshold=0.03, +def censure_keypoints(image, n_scales=7, mode='DoB', nms_threshold=0.03, rpc_threshold=10): """ Extracts Censure keypoints along with the corresponding scale using @@ -151,7 +151,7 @@ def censure_keypoints(image, n_scales=7, mode='DoB', threshold=0.03, Type of bilevel filter used to get the scales of input image. Possible values are 'DoB', 'Octagon' and 'STAR'. - threshold : float + nms_threshold : float Threshold value used to suppress maximas and minimas with a weak magnitude response obtained after Non-Maximal Suppression. @@ -193,9 +193,9 @@ def censure_keypoints(image, n_scales=7, mode='DoB', threshold=0.03, minimas = (minimum_filter(scales, (3, 3, 3)) == scales) * scales maximas = (maximum_filter(scales, (3, 3, 3)) == scales) * scales - # Suppressing minimas and maximas weaker than threshold - minimas[np.abs(minimas) < threshold] = 0 - maximas[np.abs(maximas) < threshold] = 0 + # Suppressing minimas and maximas weaker than nms_threshold + minimas[np.abs(minimas) < nms_threshold] = 0 + maximas[np.abs(maximas) < nms_threshold] = 0 response = maximas + minimas for i in range(1, n_scales - 1): From 54f9b06e464741631e6dbd59fe6f2791834a9ebb Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Mon, 5 Aug 2013 23:02:23 +0530 Subject: [PATCH 455/736] Returning the keypoints and scales separately --- skimage/feature/censure.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/skimage/feature/censure.py b/skimage/feature/censure.py index 0ad69fca..fa7bd133 100644 --- a/skimage/feature/censure.py +++ b/skimage/feature/censure.py @@ -10,7 +10,7 @@ from .censure_cy import _censure_dob_loop def _get_filtered_image(image, n_scales, mode): - # TODO : Implement the STAR mode + scales = np.zeros((image.shape[0], image.shape[1], n_scales), dtype=np.double) @@ -132,7 +132,7 @@ def _suppress_line(response, sigma, rpc_threshold): return response -def censure_keypoints(image, n_scales=7, mode='DoB', nms_threshold=0.03, +def censure_keypoints(image, n_scales=7, mode='DoB', nms_threshold=0.15, rpc_threshold=10): """ Extracts Censure keypoints along with the corresponding scale using @@ -161,8 +161,11 @@ def censure_keypoints(image, n_scales=7, mode='DoB', nms_threshold=0.03, Returns ------- - keypoints : (N, 3) array - Location of extracted keypoints along with the corresponding scale. + keypoints : (N, 2) array + Location of extracted keypoints. + + scale : (N, 1) array + The corresponding scale of the N extracted keypoints. References ---------- @@ -206,6 +209,10 @@ def censure_keypoints(image, n_scales=7, mode='DoB', nms_threshold=0.03, rpc_threshold) # Returning keypoints with its scale - keypoints = (np.transpose(np.nonzero(response[:, :, 1:n_scales - 1])) + keypoints_with_scale = (np.transpose(np.nonzero(response[:, :, 1:n_scales - 1])) + [0, 0, 2]) - return keypoints + + keypoints = keypoints_with_scale[:, :2] + scale = keypoints_with_scale[:, -1] + + return keypoints, scale From ccbca1349b18eea9c1eb00b9197427223972290b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Mon, 5 Aug 2013 23:34:16 +0200 Subject: [PATCH 456/736] Fix bugs in censure keypoint detector and improve code --- skimage/feature/censure.py | 67 ++++++++++++++++---------------------- 1 file changed, 28 insertions(+), 39 deletions(-) diff --git a/skimage/feature/censure.py b/skimage/feature/censure.py index fa7bd133..966921f2 100644 --- a/skimage/feature/censure.py +++ b/skimage/feature/censure.py @@ -1,12 +1,12 @@ import numpy as np from scipy.ndimage.filters import maximum_filter, minimum_filter, convolve -from ..transform import integral_image -from ..feature.corner import _compute_auto_correlation -from ..util import img_as_float -from ..morphology import convex_hull_image +from skimage.transform import integral_image +from skimage.feature.corner import _compute_auto_correlation +from skimage.util import img_as_float +from skimage.morphology import convex_hull_image -from .censure_cy import _censure_dob_loop +from skimage.feature.censure_cy import _censure_dob_loop def _get_filtered_image(image, n_scales, mode): @@ -43,15 +43,13 @@ def _get_filtered_image(image, n_scales, mode): (15, 10)] inner_shape = [(3, 0), (3, 1), (3, 2), (5, 2), (5, 3), (5, 4), (5, 5)] - # for i in range(n_scales): scales[:, :, i] = convolve(image, _octagon_filter(outer_shape[i][0], outer_shape[i][1], inner_shape[i][0], inner_shape[i][1])) else: - shape = [1, 2, 3, 4, 6, 8, 11, 12, 16, 22, 23, 32, 45, 46, 64, 90, - 128] + shape = [1, 2, 3, 4, 6, 8, 11, 12, 16, 22, 23, 32, 45, 46, 64, 90, 128] filter_shape = [(1, 0), (3, 1), (4, 2), (5, 3), (7, 4), (8, 5), (9, 6),(11, 8), (13, 10), (14, 11), (15, 12), (16, 14)] for i in range(n_scales): @@ -69,7 +67,7 @@ def _oct(m, n): f[n, 0] = 1 f[0, m + n -1] = 1 f[m + n - 1, 0] = 1 - f[-1, n] = 1 + f[-1, n] = 1 f[n, -1] = 1 f[-1, m + n - 1] = 1 f[m + n - 1, -1] = 1 @@ -121,19 +119,15 @@ def _star_filter(m, n): return bfilter -def _suppress_line(response, sigma, rpc_threshold): - Axx, Axy, Ayy = _compute_auto_correlation(response, sigma) - detA = Axx * Ayy - Axy**2 - traceA = Axx + Ayy - - # ratio of principal curvatures - rpc = traceA**2 / (detA + 0.001) - response[rpc > rpc_threshold] = 0 - return response +def _suppress_lines(feature_mask, image, sigma, line_threshold): + Axx, Axy, Ayy = _compute_auto_correlation(image, sigma) + feature_mask[(Axx + Ayy) * (Axx + Ayy) + < line_threshold * (Axx * Ayy - Axy * Axy)] = 0 + return feature_mask -def censure_keypoints(image, n_scales=7, mode='DoB', nms_threshold=0.15, - rpc_threshold=10): +def censure_keypoints(image, n_scales=7, mode='DoB', non_max_threshold=0.15, + line_threshold=10): """ Extracts Censure keypoints along with the corresponding scale using either Difference of Boxes, Octagon or STAR bilevel filter. @@ -151,11 +145,11 @@ def censure_keypoints(image, n_scales=7, mode='DoB', nms_threshold=0.15, Type of bilevel filter used to get the scales of input image. Possible values are 'DoB', 'Octagon' and 'STAR'. - nms_threshold : float + non_max_threshold : float Threshold value used to suppress maximas and minimas with a weak magnitude response obtained after Non-Maximal Suppression. - rpc_threshold : float + line_threshold : float Threshold for rejecting interest points which have ratio of principal curvatures greater than this value. @@ -176,7 +170,7 @@ def censure_keypoints(image, n_scales=7, mode='DoB', nms_threshold=0.15, .. [2] Adam Schmidt, Marek Kraft, Michal Fularz and Zuzanna Domagala "Comparative Assessment of Point Feature Detectors and - Descriptors in the Context of Robot Navigation" + Descriptors in the Context of Robot Navigation" http://www.jamris.org/01_2013/saveas.php?QUEST=JAMRIS_No01_2013_P_11-20.pdf """ @@ -189,30 +183,25 @@ def censure_keypoints(image, n_scales=7, mode='DoB', nms_threshold=0.15, image = np.ascontiguousarray(image) # Generating all the scales - scales = _get_filtered_image(image, n_scales, mode) + filter_response = _get_filtered_image(image, n_scales, mode) # Suppressing points that are neither minima or maxima in their 3 x 3 x 3 # neighbourhood to zero - minimas = (minimum_filter(scales, (3, 3, 3)) == scales) * scales - maximas = (maximum_filter(scales, (3, 3, 3)) == scales) * scales + minimas = minimum_filter(filter_response, (3, 3, 3)) == filter_response + maximas = maximum_filter(filter_response, (3, 3, 3)) == filter_response - # Suppressing minimas and maximas weaker than nms_threshold - minimas[np.abs(minimas) < nms_threshold] = 0 - maximas[np.abs(maximas) < nms_threshold] = 0 - response = maximas + minimas + feature_mask = minimas | maximas + feature_mask[filter_response < non_max_threshold] = False for i in range(1, n_scales - 1): # sigma = (window_size - 1) / 6.0 # window_size = 7 + 2 * i # Hence sigma = 1 + i / 3.0 - response[:, :, i] = _suppress_line(response[:, :, i], (1 + i / 3.0), - rpc_threshold) + feature_mask[:, :, i] = _suppress_lines(feature_mask[:, :, i], image, + (1 + i / 3.0), line_threshold) - # Returning keypoints with its scale - keypoints_with_scale = (np.transpose(np.nonzero(response[:, :, 1:n_scales - 1])) - + [0, 0, 2]) + rows, cols, scales = np.nonzero(feature_mask[..., 1:n_scales - 1]) + keypoints = np.column_stack([rows, cols]) + scales = scales + 2 - keypoints = keypoints_with_scale[:, :2] - scale = keypoints_with_scale[:, -1] - - return keypoints, scale + return keypoints, scales From bea9aa4414d923431cc1b3c3cc14d35a4321e55f Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Tue, 6 Aug 2013 05:12:25 +0530 Subject: [PATCH 457/736] Changing the variable names of constant objects --- skimage/feature/censure.py | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/skimage/feature/censure.py b/skimage/feature/censure.py index 966921f2..c04f7d24 100644 --- a/skimage/feature/censure.py +++ b/skimage/feature/censure.py @@ -14,6 +14,13 @@ def _get_filtered_image(image, n_scales, mode): scales = np.zeros((image.shape[0], image.shape[1], n_scales), dtype=np.double) + OCTAGON_OUTER_SHAPE = [(5, 2), (5, 3), (7, 3), (9, 4), (9, 7), (13, 7), + (15, 10)] + OCTAGON_INNER_SHAPE = [(3, 0), (3, 1), (3, 2), (5, 2), (5, 3), (5, 4), (5, 5)] + + STAR_SHAPE = [1, 2, 3, 4, 6, 8, 11, 12, 16, 22, 23, 32, 45, 46, 64, 90, 128] + STAR_FILTER_SHAPE = [(1, 0), (3, 1), (4, 2), (5, 3), (7, 4), (8, 5), + (9, 6),(11, 8), (13, 10), (14, 11), (15, 12), (16, 14)] if mode == 'DoB': for i in range(n_scales): n = i + 1 @@ -39,23 +46,18 @@ def _get_filtered_image(image, n_scales, mode): # later for a much cleaner implementation. elif mode == 'Octagon': # TODO : Decide the shapes of Octagon filters for scales > 7 - outer_shape = [(5, 2), (5, 3), (7, 3), (9, 4), (9, 7), (13, 7), - (15, 10)] - inner_shape = [(3, 0), (3, 1), (3, 2), (5, 2), (5, 3), (5, 4), (5, 5)] for i in range(n_scales): scales[:, :, i] = convolve(image, - _octagon_filter(outer_shape[i][0], - outer_shape[i][1], inner_shape[i][0], - inner_shape[i][1])) + _octagon_filter_kernel(OCTAGON_OUTER_SHAPE[i][0], + OCTAGON_OUTER_SHAPE[i][1], OCTAGON_INNER_SHAPE[i][0], + OCTAGON_INNER_SHAPE[i][1])) else: - shape = [1, 2, 3, 4, 6, 8, 11, 12, 16, 22, 23, 32, 45, 46, 64, 90, 128] - filter_shape = [(1, 0), (3, 1), (4, 2), (5, 3), (7, 4), (8, 5), - (9, 6),(11, 8), (13, 10), (14, 11), (15, 12), (16, 14)] + for i in range(n_scales): scales[:, :, i] = convolve(image, - _star_filter(shape[filter_shape[i][0]], - shape[filter_shape[i][1]])) + _star_filter_kernel(STAR_SHAPE[STAR_FILTER_SHAPE[i][0]], + STAR_SHAPE[STAR_FILTER_SHAPE[i][1]])) return scales @@ -74,7 +76,7 @@ def _oct(m, n): return convex_hull_image(f).astype(int) -def _octagon_filter(mo, no, mi, ni): +def _octagon_filter_kernel(mo, no, mi, ni): outer = (mo + 2 * no)**2 - 2 * no * (no + 1) inner = (mi + 2 * ni)**2 - 2 * ni * (ni + 1) outer_weight = 1.0 / (outer - inner) @@ -107,7 +109,7 @@ def _star(a): return selem_square + selem_triangle -def _star_filter(m, n): +def _star_filter_kernel(m, n): c = m + m / 2 - n - n / 2 outer_star = _star(m) inner_star = np.zeros((outer_star.shape)) From 5f46fd01be76ac8c7252ab35ba58609e81ad0ca0 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Tue, 6 Aug 2013 19:45:44 +0530 Subject: [PATCH 458/736] Correcting a bug in Line Suppression --- skimage/feature/censure.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/feature/censure.py b/skimage/feature/censure.py index c04f7d24..726af80d 100644 --- a/skimage/feature/censure.py +++ b/skimage/feature/censure.py @@ -124,7 +124,7 @@ def _star_filter_kernel(m, n): def _suppress_lines(feature_mask, image, sigma, line_threshold): Axx, Axy, Ayy = _compute_auto_correlation(image, sigma) feature_mask[(Axx + Ayy) * (Axx + Ayy) - < line_threshold * (Axx * Ayy - Axy * Axy)] = 0 + > line_threshold * (Axx * Ayy - Axy * Axy)] = 0 return feature_mask From 54247e3a2667c757b3d358f21ccb20917313ab5a Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Tue, 6 Aug 2013 20:29:49 +0530 Subject: [PATCH 459/736] Putting constant variables outside the definition --- skimage/feature/censure.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/skimage/feature/censure.py b/skimage/feature/censure.py index 726af80d..6178335c 100644 --- a/skimage/feature/censure.py +++ b/skimage/feature/censure.py @@ -9,18 +9,20 @@ from skimage.morphology import convex_hull_image from skimage.feature.censure_cy import _censure_dob_loop +OCTAGON_OUTER_SHAPE = [(5, 2), (5, 3), (7, 3), (9, 4), (9, 7), (13, 7), + (15, 10)] +OCTAGON_INNER_SHAPE = [(3, 0), (3, 1), (3, 2), (5, 2), (5, 3), (5, 4), (5, 5)] + +STAR_SHAPE = [1, 2, 3, 4, 6, 8, 11, 12, 16, 22, 23, 32, 45, 46, 64, 90, 128] +STAR_FILTER_SHAPE = [(1, 0), (3, 1), (4, 2), (5, 3), (7, 4), (8, 5), + (9, 6),(11, 8), (13, 10), (14, 11), (15, 12), (16, 14)] + + def _get_filtered_image(image, n_scales, mode): scales = np.zeros((image.shape[0], image.shape[1], n_scales), dtype=np.double) - OCTAGON_OUTER_SHAPE = [(5, 2), (5, 3), (7, 3), (9, 4), (9, 7), (13, 7), - (15, 10)] - OCTAGON_INNER_SHAPE = [(3, 0), (3, 1), (3, 2), (5, 2), (5, 3), (5, 4), (5, 5)] - - STAR_SHAPE = [1, 2, 3, 4, 6, 8, 11, 12, 16, 22, 23, 32, 45, 46, 64, 90, 128] - STAR_FILTER_SHAPE = [(1, 0), (3, 1), (4, 2), (5, 3), (7, 4), (8, 5), - (9, 6),(11, 8), (13, 10), (14, 11), (15, 12), (16, 14)] if mode == 'DoB': for i in range(n_scales): n = i + 1 From 91bb4baaf8da6b288d0028662b65ed69fceec2bd Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Tue, 6 Aug 2013 20:36:05 +0530 Subject: [PATCH 460/736] Making the docs more explicit --- skimage/feature/_brief.py | 6 +++--- skimage/feature/censure.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/skimage/feature/_brief.py b/skimage/feature/_brief.py index 22b8e75a..35ee04bc 100644 --- a/skimage/feature/_brief.py +++ b/skimage/feature/_brief.py @@ -16,7 +16,7 @@ def brief(image, keypoints, descriptor_size=256, mode='normal', patch_size=49, image : 2D ndarray Input image. keypoints : (P, 2) ndarray - Array of keypoint locations. + Array of keypoint locations in the format (row, col). descriptor_size : int Size of BRIEF descriptor about each keypoint. Sizes 128, 256 and 512 preferred by the authors. Default is 256. @@ -44,8 +44,8 @@ def brief(image, keypoints, descriptor_size=256, mode='normal', patch_size=49, (i, j) either being True or False representing the outcome of Intensity comparison about ith keypoint on jth decision pixel-pair. keypoints : (Q, 2) ndarray - Keypoints after removing out those that are near border. - Returned only if return_keypoints is True. + Location i.e. (row, col) of keypoints after removing out those that + are near border. References ---------- diff --git a/skimage/feature/censure.py b/skimage/feature/censure.py index 6178335c..6e403e49 100644 --- a/skimage/feature/censure.py +++ b/skimage/feature/censure.py @@ -160,7 +160,7 @@ def censure_keypoints(image, n_scales=7, mode='DoB', non_max_threshold=0.15, Returns ------- keypoints : (N, 2) array - Location of extracted keypoints. + Location of the extracted keypoints in the (row, col) format. scale : (N, 1) array The corresponding scale of the N extracted keypoints. From 14411590bf869ed89503db99fe187d8dd4b2b6a7 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Tue, 6 Aug 2013 20:38:08 +0530 Subject: [PATCH 461/736] Correcting the imports --- skimage/feature/tests/test_brief.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skimage/feature/tests/test_brief.py b/skimage/feature/tests/test_brief.py index b78270f7..1d26cbbd 100644 --- a/skimage/feature/tests/test_brief.py +++ b/skimage/feature/tests/test_brief.py @@ -2,9 +2,9 @@ import numpy as np from numpy.testing import assert_array_equal, assert_raises from skimage import data from skimage import transform as tf -from skimage.feature.corner import corner_peaks, corner_harris from skimage.color import rgb2gray -from skimage.feature import brief, match_keypoints_brief +from skimage.feature import (brief, match_keypoints_brief, corner_peaks, + corner_harris) def test_brief_color_image_unsupported_error(): From c3e18b0aeeb945cf1a73422ebc25d2f09e72793c Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Tue, 6 Aug 2013 20:46:41 +0530 Subject: [PATCH 462/736] Assigning boolean to the feature mask in line suppression --- skimage/feature/censure.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/feature/censure.py b/skimage/feature/censure.py index 6e403e49..774250bd 100644 --- a/skimage/feature/censure.py +++ b/skimage/feature/censure.py @@ -126,7 +126,7 @@ def _star_filter_kernel(m, n): def _suppress_lines(feature_mask, image, sigma, line_threshold): Axx, Axy, Ayy = _compute_auto_correlation(image, sigma) feature_mask[(Axx + Ayy) * (Axx + Ayy) - > line_threshold * (Axx * Ayy - Axy * Axy)] = 0 + > line_threshold * (Axx * Ayy - Axy * Axy)] = False return feature_mask From 4b0b342eadfcf17820de758524efd8fbf35073c1 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Wed, 7 Aug 2013 14:55:32 +0530 Subject: [PATCH 463/736] Actively filtering border keypoints on all scales --- skimage/feature/censure.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/skimage/feature/censure.py b/skimage/feature/censure.py index 774250bd..79765ccf 100644 --- a/skimage/feature/censure.py +++ b/skimage/feature/censure.py @@ -204,6 +204,22 @@ def censure_keypoints(image, n_scales=7, mode='DoB', non_max_threshold=0.15, feature_mask[:, :, i] = _suppress_lines(feature_mask[:, :, i], image, (1 + i / 3.0), line_threshold) + if mode == 'Octagon': + for i in range(1, n_scales - 1): + c = (OCTAGON_OUTER_SHAPE[i][0] - 1) / 2 + OCTAGON_OUTER_SHAPE[i][1] + feature_mask[:c, :, i] = False + feature_mask[:, :c, i] = False + feature_mask[-c:, :, i] = False + feature_mask[:, -c:, i] = False + + elif mode == 'STAR': + for i in range(1, n_scales - 1): + c = STAR_SHAPE[STAR_FILTER_SHAPE[i][0]] + STAR_SHAPE[STAR_FILTER_SHAPE[i][0]] / 2 + feature_mask[:c, :, i] = False + feature_mask[:, :c, i] = False + feature_mask[-c:, :, i] = False + feature_mask[:, -c:, i] = False + rows, cols, scales = np.nonzero(feature_mask[..., 1:n_scales - 1]) keypoints = np.column_stack([rows, cols]) scales = scales + 2 From 3752f19cf92c3afa6e988d0df3e1f9e84447539a Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Thu, 8 Aug 2013 05:57:55 +0530 Subject: [PATCH 464/736] Adding tests for all the modes in censure --- skimage/feature/tests/test_censure.py | 72 +++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 skimage/feature/tests/test_censure.py diff --git a/skimage/feature/tests/test_censure.py b/skimage/feature/tests/test_censure.py new file mode 100644 index 00000000..2a582a9a --- /dev/null +++ b/skimage/feature/tests/test_censure.py @@ -0,0 +1,72 @@ +import numpy as np +from numpy.testing import assert_array_equal, assert_raises +from skimage.data import moon +from skimage.feature import censure_keypoints + + +def test_censure_keypoints_color_image_unsupported_error(): + """Censure keypoints can be extracted from gray-scale images only.""" + img = np.zeros((20, 20, 3)) + assert_raises(ValueError, censure_keypoints, img) + + +def test_censure_keypoints_moon_image_DoB(): + """Verify the actual Censure keypoints and their corresponding scale with + the expected values for DoB filter.""" + img = moon() + actual_kp_DoB, actual_scale = censure_keypoints(img, 7, 'DoB', 0.15) + expected_kp_DoB = np.array([[ 4, 507], + [ 8, 503], + [ 12, 499], + [ 21, 497], + [ 36, 46], + [119, 350], + [185, 177], + [287, 250], + [357, 239], + [463, 116], + [464, 132], + [467, 260]]) + expected_scale = np.array([2, 4, 6, 3, 4, 4, 2, 2, 3, 2, 2, 2]) + assert_array_equal(expected_kp_DoB, actual_kp_DoB) + assert_array_equal(expected_scale, actual_scale) + + +def test_censure_keypoints_moon_image_Octagon(): + """Verify the actual Censure keypoints and their corresponding scale with + the expected values for Octagon filter.""" + img = moon() + actual_kp_Octagon, actual_scale = censure_keypoints(img, 7, 'Octagon', 0.15) + expected_kp_Octagon = np.array([[ 21, 496], + [ 35, 46], + [287, 250], + [356, 239], + [463, 116]]) + expected_scale = np.array([3, 4, 2, 2, 2]) + assert_array_equal(expected_kp_Octagon, actual_kp_Octagon) + assert_array_equal(expected_scale, actual_scale) + + +def test_censure_keypoints_moon_image_STAR(): + """Verify the actual Censure keypoints and their corresponding scale with + the expected values for STAR filter.""" + img = moon() + actual_kp_STAR, actual_scale = censure_keypoints(img, 7, 'STAR', 0.15) + expected_kp_STAR = np.array([[ 21, 497], + [ 36, 46], + [117, 356], + [185, 177], + [260, 227], + [287, 250], + [357, 239], + [451, 281], + [463, 116], + [467, 260]]) + expected_scale = np.array([3, 3, 6, 2, 3, 2, 3, 5, 2, 2]) + assert_array_equal(expected_kp_STAR, actual_kp_STAR) + assert_array_equal(expected_scale, actual_scale) + + +if __name__ == '__main__': + from numpy import testing + testing.run_module_suite() From c9a93a1ce39fe2a5fdd915b46fb9658e454e807f Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Thu, 8 Aug 2013 07:51:03 +0530 Subject: [PATCH 465/736] Debugging Travis error --- skimage/feature/tests/test_censure.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/skimage/feature/tests/test_censure.py b/skimage/feature/tests/test_censure.py index 2a582a9a..3d4e1a1c 100644 --- a/skimage/feature/tests/test_censure.py +++ b/skimage/feature/tests/test_censure.py @@ -28,6 +28,8 @@ def test_censure_keypoints_moon_image_DoB(): [464, 132], [467, 260]]) expected_scale = np.array([2, 4, 6, 3, 4, 4, 2, 2, 3, 2, 2, 2]) + print actual_kp_DoB + print actual_scale assert_array_equal(expected_kp_DoB, actual_kp_DoB) assert_array_equal(expected_scale, actual_scale) @@ -43,6 +45,8 @@ def test_censure_keypoints_moon_image_Octagon(): [356, 239], [463, 116]]) expected_scale = np.array([3, 4, 2, 2, 2]) + print actual_kp_Octagon + print actual_scale assert_array_equal(expected_kp_Octagon, actual_kp_Octagon) assert_array_equal(expected_scale, actual_scale) @@ -63,6 +67,8 @@ def test_censure_keypoints_moon_image_STAR(): [463, 116], [467, 260]]) expected_scale = np.array([3, 3, 6, 2, 3, 2, 3, 5, 2, 2]) + print actual_kp_STAR + print actual_scale assert_array_equal(expected_kp_STAR, actual_kp_STAR) assert_array_equal(expected_scale, actual_scale) From fb4050ed37168bb283410ba34a896d96d62c60cf Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Thu, 8 Aug 2013 09:57:01 +0530 Subject: [PATCH 466/736] Reverting back to last but one commit --- skimage/feature/tests/test_censure.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/skimage/feature/tests/test_censure.py b/skimage/feature/tests/test_censure.py index 3d4e1a1c..deebc9c1 100644 --- a/skimage/feature/tests/test_censure.py +++ b/skimage/feature/tests/test_censure.py @@ -28,8 +28,7 @@ def test_censure_keypoints_moon_image_DoB(): [464, 132], [467, 260]]) expected_scale = np.array([2, 4, 6, 3, 4, 4, 2, 2, 3, 2, 2, 2]) - print actual_kp_DoB - print actual_scale + assert_array_equal(expected_kp_DoB, actual_kp_DoB) assert_array_equal(expected_scale, actual_scale) @@ -45,8 +44,7 @@ def test_censure_keypoints_moon_image_Octagon(): [356, 239], [463, 116]]) expected_scale = np.array([3, 4, 2, 2, 2]) - print actual_kp_Octagon - print actual_scale + assert_array_equal(expected_kp_Octagon, actual_kp_Octagon) assert_array_equal(expected_scale, actual_scale) @@ -67,8 +65,7 @@ def test_censure_keypoints_moon_image_STAR(): [463, 116], [467, 260]]) expected_scale = np.array([3, 3, 6, 2, 3, 2, 3, 5, 2, 2]) - print actual_kp_STAR - print actual_scale + assert_array_equal(expected_kp_STAR, actual_kp_STAR) assert_array_equal(expected_scale, actual_scale) From 963f681e48d1722c67cbda25892e4506e711e444 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Thu, 8 Aug 2013 11:26:53 +0530 Subject: [PATCH 467/736] Making the code compatible with Python 3 --- skimage/feature/censure.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/skimage/feature/censure.py b/skimage/feature/censure.py index 79765ccf..1a2f495c 100644 --- a/skimage/feature/censure.py +++ b/skimage/feature/censure.py @@ -83,7 +83,7 @@ def _octagon_filter_kernel(mo, no, mi, ni): inner = (mi + 2 * ni)**2 - 2 * ni * (ni + 1) outer_weight = 1.0 / (outer - inner) inner_weight = 1.0 / inner - c = ((mo + 2 * no) - (mi + 2 * ni)) / 2 + c = ((mo + 2 * no) - (mi + 2 * ni)) // 2 outer_oct = _oct(mo, no) inner_oct = np.zeros((mo + 2 * no, mo + 2 * no)) inner_oct[c: -c, c: -c] = _oct(mi, ni) @@ -98,13 +98,13 @@ def _star(a): bfilter[:] = 1 return bfilter m = 2 * a + 1 - n = a / 2 + n = a // 2 selem_square = np.zeros((m + 2 * n, m + 2 * n), dtype=np.uint8) selem_square[n: m + n, n: m + n] = 1 selem_triangle = np.zeros((m + 2 * n, m + 2 * n), dtype=np.uint8) - selem_triangle[(m + 2 * n - 1) / 2, 0] = 1 - selem_triangle[(m + 1) / 2, n - 1] = 1 - selem_triangle[(m + 4 * n - 3) / 2, n - 1] = 1 + selem_triangle[(m + 2 * n - 1) // 2, 0] = 1 + selem_triangle[(m + 1) // 2, n - 1] = 1 + selem_triangle[(m + 4 * n - 3) // 2, n - 1] = 1 selem_triangle = convex_hull_image(selem_triangle).astype(int) selem_triangle += (selem_triangle[:, ::-1] + selem_triangle.T + selem_triangle.T[::-1, :]) @@ -112,7 +112,7 @@ def _star(a): def _star_filter_kernel(m, n): - c = m + m / 2 - n - n / 2 + c = m + m // 2 - n - n // 2 outer_star = _star(m) inner_star = np.zeros((outer_star.shape)) inner_star[c: -c, c: -c] = _star(n) @@ -206,7 +206,7 @@ def censure_keypoints(image, n_scales=7, mode='DoB', non_max_threshold=0.15, if mode == 'Octagon': for i in range(1, n_scales - 1): - c = (OCTAGON_OUTER_SHAPE[i][0] - 1) / 2 + OCTAGON_OUTER_SHAPE[i][1] + c = (OCTAGON_OUTER_SHAPE[i][0] - 1) // 2 + OCTAGON_OUTER_SHAPE[i][1] feature_mask[:c, :, i] = False feature_mask[:, :c, i] = False feature_mask[-c:, :, i] = False @@ -214,7 +214,7 @@ def censure_keypoints(image, n_scales=7, mode='DoB', non_max_threshold=0.15, elif mode == 'STAR': for i in range(1, n_scales - 1): - c = STAR_SHAPE[STAR_FILTER_SHAPE[i][0]] + STAR_SHAPE[STAR_FILTER_SHAPE[i][0]] / 2 + c = STAR_SHAPE[STAR_FILTER_SHAPE[i][0]] + STAR_SHAPE[STAR_FILTER_SHAPE[i][0]] // 2 feature_mask[:c, :, i] = False feature_mask[:, :c, i] = False feature_mask[-c:, :, i] = False From 1227507c9e42f44e5f8de08664315f7d4641063c Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Thu, 8 Aug 2013 18:08:49 +0530 Subject: [PATCH 468/736] Actively filtering border keypoints for all the scales part 2 --- skimage/feature/censure.py | 39 +++++++++++++++------------ skimage/feature/tests/test_censure.py | 34 ++++++++++++----------- skimage/feature/util.py | 13 +++++---- 3 files changed, 49 insertions(+), 37 deletions(-) diff --git a/skimage/feature/censure.py b/skimage/feature/censure.py index 1a2f495c..7500106d 100644 --- a/skimage/feature/censure.py +++ b/skimage/feature/censure.py @@ -5,6 +5,7 @@ from skimage.transform import integral_image from skimage.feature.corner import _compute_auto_correlation from skimage.util import img_as_float from skimage.morphology import convex_hull_image +from skimage.feature.util import _remove_border_keypoints from skimage.feature.censure_cy import _censure_dob_loop @@ -204,24 +205,28 @@ def censure_keypoints(image, n_scales=7, mode='DoB', non_max_threshold=0.15, feature_mask[:, :, i] = _suppress_lines(feature_mask[:, :, i], image, (1 + i / 3.0), line_threshold) - if mode == 'Octagon': - for i in range(1, n_scales - 1): - c = (OCTAGON_OUTER_SHAPE[i][0] - 1) // 2 + OCTAGON_OUTER_SHAPE[i][1] - feature_mask[:c, :, i] = False - feature_mask[:, :c, i] = False - feature_mask[-c:, :, i] = False - feature_mask[:, -c:, i] = False - - elif mode == 'STAR': - for i in range(1, n_scales - 1): - c = STAR_SHAPE[STAR_FILTER_SHAPE[i][0]] + STAR_SHAPE[STAR_FILTER_SHAPE[i][0]] // 2 - feature_mask[:c, :, i] = False - feature_mask[:, :c, i] = False - feature_mask[-c:, :, i] = False - feature_mask[:, -c:, i] = False - rows, cols, scales = np.nonzero(feature_mask[..., 1:n_scales - 1]) keypoints = np.column_stack([rows, cols]) scales = scales + 2 - return keypoints, scales + if mode == 'DoB': + return keypoints, scales + + filtered_keypoints = np.empty((0, 2), dtype=np.int32) + filtered_scales = np.empty((0), dtype=np.int32) + + if mode == 'Octagon': + for i in range(2, n_scales): + c = (OCTAGON_OUTER_SHAPE[i - 1][0] - 1) // 2 + OCTAGON_OUTER_SHAPE[i - 1][1] + filtered_keypoints_for_scale = _remove_border_keypoints(image, keypoints[scales == i], c) + filtered_keypoints = np.vstack((filtered_keypoints, filtered_keypoints_for_scale)) + filtered_scales = np.hstack((filtered_scales, np.asarray(len(filtered_keypoints_for_scale) * [i], dtype=np.int32))) + + elif mode == 'STAR': + for i in range(2, n_scales): + c = STAR_SHAPE[STAR_FILTER_SHAPE[i - 1][0]] + STAR_SHAPE[STAR_FILTER_SHAPE[i - 1][0]] // 2 + filtered_keypoints_for_scale = _remove_border_keypoints(image, keypoints[scales == i], c) + filtered_keypoints = np.vstack((filtered_keypoints, filtered_keypoints_for_scale)) + filtered_scales = np.hstack((filtered_scales, np.asarray(len(filtered_keypoints_for_scale) * [i], dtype=np.int32))) + + return filtered_keypoints, filtered_scales diff --git a/skimage/feature/tests/test_censure.py b/skimage/feature/tests/test_censure.py index deebc9c1..4f1ddcfb 100644 --- a/skimage/feature/tests/test_censure.py +++ b/skimage/feature/tests/test_censure.py @@ -28,7 +28,8 @@ def test_censure_keypoints_moon_image_DoB(): [464, 132], [467, 260]]) expected_scale = np.array([2, 4, 6, 3, 4, 4, 2, 2, 3, 2, 2, 2]) - + print actual_scale + print actual_kp_DoB assert_array_equal(expected_kp_DoB, actual_kp_DoB) assert_array_equal(expected_scale, actual_scale) @@ -38,13 +39,15 @@ def test_censure_keypoints_moon_image_Octagon(): the expected values for Octagon filter.""" img = moon() actual_kp_Octagon, actual_scale = censure_keypoints(img, 7, 'Octagon', 0.15) - expected_kp_Octagon = np.array([[ 21, 496], - [ 35, 46], - [287, 250], + expected_kp_Octagon = np.array([[287, 250], [356, 239], - [463, 116]]) - expected_scale = np.array([3, 4, 2, 2, 2]) + [463, 116], + [ 21, 496], + [ 35, 46]]) + expected_scale = np.array([2, 2, 2, 3, 4], dtype=np.int32) + print actual_scale + print actual_kp_Octagon assert_array_equal(expected_kp_Octagon, actual_kp_Octagon) assert_array_equal(expected_scale, actual_scale) @@ -54,18 +57,19 @@ def test_censure_keypoints_moon_image_STAR(): the expected values for STAR filter.""" img = moon() actual_kp_STAR, actual_scale = censure_keypoints(img, 7, 'STAR', 0.15) - expected_kp_STAR = np.array([[ 21, 497], - [ 36, 46], - [117, 356], - [185, 177], - [260, 227], + expected_kp_STAR = np.array([[185, 177], [287, 250], + [463, 116], + [467, 260], + [ 21, 497], + [ 36, 46], + [260, 227], [357, 239], [451, 281], - [463, 116], - [467, 260]]) - expected_scale = np.array([3, 3, 6, 2, 3, 2, 3, 5, 2, 2]) - + [117, 356]]) + expected_scale = np.array([2, 2, 2, 2, 3, 3, 3, 3, 5, 6], dtype=np.int32) + print actual_scale + print actual_kp_STAR assert_array_equal(expected_kp_STAR, actual_kp_STAR) assert_array_equal(expected_scale, actual_scale) diff --git a/skimage/feature/util.py b/skimage/feature/util.py index aec4dfc8..f327cff5 100644 --- a/skimage/feature/util.py +++ b/skimage/feature/util.py @@ -1,3 +1,4 @@ +import numpy as np def _remove_border_keypoints(image, keypoints, dist): @@ -5,11 +6,13 @@ def _remove_border_keypoints(image, keypoints, dist): width = image.shape[0] height = image.shape[1] - keypoints = keypoints[(dist - 1 < keypoints[:, 0]) - & (keypoints[:, 0] < width - dist + 1) - & (dist - 1 < keypoints[:, 1]) - & (keypoints[:, 1] < height - dist + 1)] - + try: + keypoints = keypoints[(dist - 1 < keypoints[:, 0]) + & (keypoints[:, 0] < width - dist + 1) + & (dist - 1 < keypoints[:, 1]) + & (keypoints[:, 1] < height - dist + 1)] + except IndexError: + return np.empty((0, 2), dtype=np.int32) return keypoints From 6fc84dc7e6cbcd69fbb16559f974ca8ae248a8f4 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Thu, 8 Aug 2013 23:50:29 +0530 Subject: [PATCH 469/736] Removing print statements --- skimage/feature/tests/test_censure.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/skimage/feature/tests/test_censure.py b/skimage/feature/tests/test_censure.py index 4f1ddcfb..1822ec0a 100644 --- a/skimage/feature/tests/test_censure.py +++ b/skimage/feature/tests/test_censure.py @@ -28,8 +28,7 @@ def test_censure_keypoints_moon_image_DoB(): [464, 132], [467, 260]]) expected_scale = np.array([2, 4, 6, 3, 4, 4, 2, 2, 3, 2, 2, 2]) - print actual_scale - print actual_kp_DoB + assert_array_equal(expected_kp_DoB, actual_kp_DoB) assert_array_equal(expected_scale, actual_scale) @@ -46,8 +45,7 @@ def test_censure_keypoints_moon_image_Octagon(): [ 35, 46]]) expected_scale = np.array([2, 2, 2, 3, 4], dtype=np.int32) - print actual_scale - print actual_kp_Octagon + assert_array_equal(expected_kp_Octagon, actual_kp_Octagon) assert_array_equal(expected_scale, actual_scale) @@ -68,8 +66,7 @@ def test_censure_keypoints_moon_image_STAR(): [451, 281], [117, 356]]) expected_scale = np.array([2, 2, 2, 2, 3, 3, 3, 3, 5, 6], dtype=np.int32) - print actual_scale - print actual_kp_STAR + assert_array_equal(expected_kp_STAR, actual_kp_STAR) assert_array_equal(expected_scale, actual_scale) From 4f109d18e2542d8308366cc82678d53b57bbe7fb Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Thu, 8 Aug 2013 23:55:44 +0530 Subject: [PATCH 470/736] Returning mask in _remove_border_keypoints --- skimage/feature/util.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/skimage/feature/util.py b/skimage/feature/util.py index f327cff5..c644af50 100644 --- a/skimage/feature/util.py +++ b/skimage/feature/util.py @@ -1,4 +1,3 @@ -import numpy as np def _remove_border_keypoints(image, keypoints, dist): @@ -6,14 +5,12 @@ def _remove_border_keypoints(image, keypoints, dist): width = image.shape[0] height = image.shape[1] - try: - keypoints = keypoints[(dist - 1 < keypoints[:, 0]) - & (keypoints[:, 0] < width - dist + 1) - & (dist - 1 < keypoints[:, 1]) - & (keypoints[:, 1] < height - dist + 1)] - except IndexError: - return np.empty((0, 2), dtype=np.int32) - return keypoints + keypoints_filtering_mask = (dist - 1 < keypoints[:, 0] + & keypoints[:, 0] < width - dist + 1 + & dist - 1 < keypoints[:, 1] + & keypoints[:, 1] < height - dist + 1) + + return keypoints_filtering_mask def pairwise_hamming_distance(array1, array2): From 62eb4ef998fbdf830b9abfb8454ef68a26fd8507 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Fri, 9 Aug 2013 00:42:58 +0530 Subject: [PATCH 471/736] Filtering out border keypoints using masking --- skimage/feature/_brief.py | 2 +- skimage/feature/censure.py | 13 ++++--------- skimage/feature/tests/test_censure.py | 25 +++++++++++++------------ skimage/feature/util.py | 8 ++++---- 4 files changed, 22 insertions(+), 26 deletions(-) diff --git a/skimage/feature/_brief.py b/skimage/feature/_brief.py index 35ee04bc..73230188 100644 --- a/skimage/feature/_brief.py +++ b/skimage/feature/_brief.py @@ -142,7 +142,7 @@ def brief(image, keypoints, descriptor_size=256, mode='normal', patch_size=49, # Removing keypoints that are within (patch_size / 2) distance from the # image border - keypoints = _remove_border_keypoints(image, keypoints, patch_size // 2) + keypoints = keypoints[_remove_border_keypoints(image, keypoints, patch_size // 2)] keypoints = np.ascontiguousarray(keypoints) descriptors = np.zeros((keypoints.shape[0], descriptor_size), dtype=bool, diff --git a/skimage/feature/censure.py b/skimage/feature/censure.py index 7500106d..62f2976c 100644 --- a/skimage/feature/censure.py +++ b/skimage/feature/censure.py @@ -212,21 +212,16 @@ def censure_keypoints(image, n_scales=7, mode='DoB', non_max_threshold=0.15, if mode == 'DoB': return keypoints, scales - filtered_keypoints = np.empty((0, 2), dtype=np.int32) - filtered_scales = np.empty((0), dtype=np.int32) + cumulative_mask = np.zeros(keypoints.shape[0], dtype=np.bool) if mode == 'Octagon': for i in range(2, n_scales): c = (OCTAGON_OUTER_SHAPE[i - 1][0] - 1) // 2 + OCTAGON_OUTER_SHAPE[i - 1][1] - filtered_keypoints_for_scale = _remove_border_keypoints(image, keypoints[scales == i], c) - filtered_keypoints = np.vstack((filtered_keypoints, filtered_keypoints_for_scale)) - filtered_scales = np.hstack((filtered_scales, np.asarray(len(filtered_keypoints_for_scale) * [i], dtype=np.int32))) + cumulative_mask = cumulative_mask | (_remove_border_keypoints(image, keypoints, c) & (scales == i)) elif mode == 'STAR': for i in range(2, n_scales): c = STAR_SHAPE[STAR_FILTER_SHAPE[i - 1][0]] + STAR_SHAPE[STAR_FILTER_SHAPE[i - 1][0]] // 2 - filtered_keypoints_for_scale = _remove_border_keypoints(image, keypoints[scales == i], c) - filtered_keypoints = np.vstack((filtered_keypoints, filtered_keypoints_for_scale)) - filtered_scales = np.hstack((filtered_scales, np.asarray(len(filtered_keypoints_for_scale) * [i], dtype=np.int32))) + cumulative_mask = cumulative_mask | (_remove_border_keypoints(image, keypoints, c) & (scales == i)) - return filtered_keypoints, filtered_scales + return keypoints[cumulative_mask], scales[cumulative_mask] diff --git a/skimage/feature/tests/test_censure.py b/skimage/feature/tests/test_censure.py index 1822ec0a..1f8a01dd 100644 --- a/skimage/feature/tests/test_censure.py +++ b/skimage/feature/tests/test_censure.py @@ -38,13 +38,13 @@ def test_censure_keypoints_moon_image_Octagon(): the expected values for Octagon filter.""" img = moon() actual_kp_Octagon, actual_scale = censure_keypoints(img, 7, 'Octagon', 0.15) - expected_kp_Octagon = np.array([[287, 250], + expected_kp_Octagon = np.array([[ 21, 496], + [ 35, 46], + [287, 250], [356, 239], - [463, 116], - [ 21, 496], - [ 35, 46]]) + [463, 116]]) - expected_scale = np.array([2, 2, 2, 3, 4], dtype=np.int32) + expected_scale = np.array([3, 4, 2, 2, 2], dtype=np.int32) assert_array_equal(expected_kp_Octagon, actual_kp_Octagon) assert_array_equal(expected_scale, actual_scale) @@ -55,17 +55,18 @@ def test_censure_keypoints_moon_image_STAR(): the expected values for STAR filter.""" img = moon() actual_kp_STAR, actual_scale = censure_keypoints(img, 7, 'STAR', 0.15) - expected_kp_STAR = np.array([[185, 177], - [287, 250], - [463, 116], - [467, 260], - [ 21, 497], + expected_kp_STAR = np.array([[ 21, 497], [ 36, 46], + [117, 356], + [185, 177], [260, 227], + [287, 250], [357, 239], [451, 281], - [117, 356]]) - expected_scale = np.array([2, 2, 2, 2, 3, 3, 3, 3, 5, 6], dtype=np.int32) + [463, 116], + [467, 260]]) + + expected_scale = np.array([3, 3, 6, 2, 3, 2, 3, 5, 2, 2], dtype=np.intp) assert_array_equal(expected_kp_STAR, actual_kp_STAR) assert_array_equal(expected_scale, actual_scale) diff --git a/skimage/feature/util.py b/skimage/feature/util.py index c644af50..513fa05d 100644 --- a/skimage/feature/util.py +++ b/skimage/feature/util.py @@ -5,10 +5,10 @@ def _remove_border_keypoints(image, keypoints, dist): width = image.shape[0] height = image.shape[1] - keypoints_filtering_mask = (dist - 1 < keypoints[:, 0] - & keypoints[:, 0] < width - dist + 1 - & dist - 1 < keypoints[:, 1] - & keypoints[:, 1] < height - dist + 1) + keypoints_filtering_mask = ((dist - 1 < keypoints[:, 0]) & + (keypoints[:, 0] < width - dist + 1) & + (dist - 1 < keypoints[:, 1]) & + (keypoints[:, 1] < height - dist + 1)) return keypoints_filtering_mask From 1bd261c23fccd8a3deb0cfe4c3840a9ef7f8e67d Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Fri, 9 Aug 2013 00:44:18 +0530 Subject: [PATCH 472/736] Replacing int32 by intp --- skimage/feature/tests/test_censure.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/feature/tests/test_censure.py b/skimage/feature/tests/test_censure.py index 1f8a01dd..41d1886e 100644 --- a/skimage/feature/tests/test_censure.py +++ b/skimage/feature/tests/test_censure.py @@ -44,7 +44,7 @@ def test_censure_keypoints_moon_image_Octagon(): [356, 239], [463, 116]]) - expected_scale = np.array([3, 4, 2, 2, 2], dtype=np.int32) + expected_scale = np.array([3, 4, 2, 2, 2], dtype=np.intp) assert_array_equal(expected_kp_Octagon, actual_kp_Octagon) assert_array_equal(expected_scale, actual_scale) From 157c22bbc9bf592b003f1562ed28bd1050d5234e Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Fri, 9 Aug 2013 14:54:41 +0530 Subject: [PATCH 473/736] Changing _remove_border_keypoints to _mask_border_keypoints --- skimage/feature/_brief.py | 4 ++-- skimage/feature/censure.py | 6 +++--- skimage/feature/util.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/skimage/feature/_brief.py b/skimage/feature/_brief.py index 73230188..27a8d085 100644 --- a/skimage/feature/_brief.py +++ b/skimage/feature/_brief.py @@ -2,7 +2,7 @@ import numpy as np from scipy.ndimage.filters import gaussian_filter from ..util import img_as_float -from .util import _remove_border_keypoints, pairwise_hamming_distance +from .util import _mask_border_keypoints, pairwise_hamming_distance from ._brief_cy import _brief_loop @@ -142,7 +142,7 @@ def brief(image, keypoints, descriptor_size=256, mode='normal', patch_size=49, # Removing keypoints that are within (patch_size / 2) distance from the # image border - keypoints = keypoints[_remove_border_keypoints(image, keypoints, patch_size // 2)] + keypoints = keypoints[_mask_border_keypoints(image, keypoints, patch_size // 2)] keypoints = np.ascontiguousarray(keypoints) descriptors = np.zeros((keypoints.shape[0], descriptor_size), dtype=bool, diff --git a/skimage/feature/censure.py b/skimage/feature/censure.py index 62f2976c..e3264436 100644 --- a/skimage/feature/censure.py +++ b/skimage/feature/censure.py @@ -5,7 +5,7 @@ from skimage.transform import integral_image from skimage.feature.corner import _compute_auto_correlation from skimage.util import img_as_float from skimage.morphology import convex_hull_image -from skimage.feature.util import _remove_border_keypoints +from skimage.feature.util import _mask_border_keypoints from skimage.feature.censure_cy import _censure_dob_loop @@ -217,11 +217,11 @@ def censure_keypoints(image, n_scales=7, mode='DoB', non_max_threshold=0.15, if mode == 'Octagon': for i in range(2, n_scales): c = (OCTAGON_OUTER_SHAPE[i - 1][0] - 1) // 2 + OCTAGON_OUTER_SHAPE[i - 1][1] - cumulative_mask = cumulative_mask | (_remove_border_keypoints(image, keypoints, c) & (scales == i)) + cumulative_mask = cumulative_mask | (_mask_border_keypoints(image, keypoints, c) & (scales == i)) elif mode == 'STAR': for i in range(2, n_scales): c = STAR_SHAPE[STAR_FILTER_SHAPE[i - 1][0]] + STAR_SHAPE[STAR_FILTER_SHAPE[i - 1][0]] // 2 - cumulative_mask = cumulative_mask | (_remove_border_keypoints(image, keypoints, c) & (scales == i)) + cumulative_mask = cumulative_mask | (_mask_border_keypoints(image, keypoints, c) & (scales == i)) return keypoints[cumulative_mask], scales[cumulative_mask] diff --git a/skimage/feature/util.py b/skimage/feature/util.py index 513fa05d..eb3817e8 100644 --- a/skimage/feature/util.py +++ b/skimage/feature/util.py @@ -1,6 +1,6 @@ -def _remove_border_keypoints(image, keypoints, dist): +def _mask_border_keypoints(image, keypoints, dist): """Removes keypoints that are within dist pixels from the image border.""" width = image.shape[0] height = image.shape[1] From 0b37611df60bf1da7542324f77e9d52093aab824 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 11 Aug 2013 11:02:24 +0200 Subject: [PATCH 474/736] Fix several bugs in DoB method and improve overall code quality --- skimage/feature/censure.py | 76 +++++++++++++++------------ skimage/feature/censure_cy.pyx | 21 ++++---- skimage/feature/tests/test_censure.py | 15 +++--- 3 files changed, 59 insertions(+), 53 deletions(-) diff --git a/skimage/feature/censure.py b/skimage/feature/censure.py index e3264436..583e3d0a 100644 --- a/skimage/feature/censure.py +++ b/skimage/feature/censure.py @@ -24,7 +24,16 @@ def _get_filtered_image(image, n_scales, mode): scales = np.zeros((image.shape[0], image.shape[1], n_scales), dtype=np.double) - if mode == 'DoB': + if mode == 'dob': + + # make scales[:, :, i] contiguous memory block + item_size = scales.itemsize + scales.strides = (item_size * scales.shape[0], + item_size, + item_size * scales.shape[0] * scales.shape[1]) + + integral_img = integral_image(image) + for i in range(n_scales): n = i + 1 @@ -34,33 +43,29 @@ def _get_filtered_image(image, n_scales, mode): inner_weight = (1.0 / (2 * n + 1)**2) outer_weight = (1.0 / (12 * n**2 + 4 * n)) - integral_img = integral_image(image) - - filtered_image = np.zeros(image.shape) - _censure_dob_loop(image, n, integral_img, filtered_image, + _censure_dob_loop(n, integral_img, scales[:, :, i], inner_weight, outer_weight) - scales[:, :, i] = filtered_image - # NOTE : For the Octagon shaped filter, we implemented and evaluated the # slanted integral image based image filtering but the performance was # more or less equal to image filtering using # scipy.ndimage.filters.convolve(). Hence we have decided to use the # later for a much cleaner implementation. - elif mode == 'Octagon': + elif mode == 'octagon': # TODO : Decide the shapes of Octagon filters for scales > 7 for i in range(n_scales): + mo, no = OCTAGON_OUTER_SHAPE[i] + mi, ni = OCTAGON_INNER_SHAPE[i] scales[:, :, i] = convolve(image, - _octagon_filter_kernel(OCTAGON_OUTER_SHAPE[i][0], - OCTAGON_OUTER_SHAPE[i][1], OCTAGON_INNER_SHAPE[i][0], - OCTAGON_INNER_SHAPE[i][1])) - else: + _octagon_filter_kernel(mo, no, mi, ni)) + elif mode == 'star': for i in range(n_scales): + m = STAR_SHAPE[STAR_FILTER_SHAPE[i][0]] + n = STAR_SHAPE[STAR_FILTER_SHAPE[i][1]] scales[:, :, i] = convolve(image, - _star_filter_kernel(STAR_SHAPE[STAR_FILTER_SHAPE[i][0]], - STAR_SHAPE[STAR_FILTER_SHAPE[i][1]])) + _star_filter_kernel(m, n)) return scales @@ -115,7 +120,7 @@ def _star(a): def _star_filter_kernel(m, n): c = m + m // 2 - n - n // 2 outer_star = _star(m) - inner_star = np.zeros((outer_star.shape)) + inner_star = np.zeros_like(outer_star) inner_star[c: -c, c: -c] = _star(n) outer_weight = 1.0 / (np.sum(outer_star - inner_star)) inner_weight = 1.0 / np.sum(inner_star) @@ -128,7 +133,6 @@ def _suppress_lines(feature_mask, image, sigma, line_threshold): Axx, Axy, Ayy = _compute_auto_correlation(image, sigma) feature_mask[(Axx + Ayy) * (Axx + Ayy) > line_threshold * (Axx * Ayy - Axy * Axy)] = False - return feature_mask def censure_keypoints(image, n_scales=7, mode='DoB', non_max_threshold=0.15, @@ -141,19 +145,15 @@ def censure_keypoints(image, n_scales=7, mode='DoB', non_max_threshold=0.15, ---------- image : 2D ndarray Input image. - n_scales : positive integer Number of scales to extract keypoints from. The keypoints will be extracted from all the scales except the first and the last. - mode : ('DoB', 'Octagon', 'STAR') Type of bilevel filter used to get the scales of input image. Possible values are 'DoB', 'Octagon' and 'STAR'. - non_max_threshold : float Threshold value used to suppress maximas and minimas with a weak magnitude response obtained after Non-Maximal Suppression. - line_threshold : float Threshold for rejecting interest points which have ratio of principal curvatures greater than this value. @@ -162,8 +162,7 @@ def censure_keypoints(image, n_scales=7, mode='DoB', non_max_threshold=0.15, ------- keypoints : (N, 2) array Location of the extracted keypoints in the (row, col) format. - - scale : (N, 1) array + scales : (N, 1) array The corresponding scale of the N extracted keypoints. References @@ -183,10 +182,15 @@ def censure_keypoints(image, n_scales=7, mode='DoB', non_max_threshold=0.15, image = np.squeeze(image) if image.ndim != 2: raise ValueError("Only 2-D gray-scale images supported.") - image = img_as_float(image) + image = img_as_float(image) image = np.ascontiguousarray(image) + mode = mode.lower() + + if mode not in ('dob', 'octagon', 'star'): + raise ValueError('Mode must be one of "DoB", "Octagon", "STAR".') + # Generating all the scales filter_response = _get_filtered_image(image, n_scales, mode) @@ -199,29 +203,33 @@ def censure_keypoints(image, n_scales=7, mode='DoB', non_max_threshold=0.15, feature_mask[filter_response < non_max_threshold] = False for i in range(1, n_scales - 1): - # sigma = (window_size - 1) / 6.0 + # sigma = (window_size - 1) / 6.0, so the window covers > 99% of the + # kernel's distribution # window_size = 7 + 2 * i # Hence sigma = 1 + i / 3.0 - feature_mask[:, :, i] = _suppress_lines(feature_mask[:, :, i], image, - (1 + i / 3.0), line_threshold) + _suppress_lines(feature_mask[:, :, i], image, + (1 + i / 3.0), line_threshold) rows, cols, scales = np.nonzero(feature_mask[..., 1:n_scales - 1]) keypoints = np.column_stack([rows, cols]) scales = scales + 2 - if mode == 'DoB': + if mode == 'dob': return keypoints, scales cumulative_mask = np.zeros(keypoints.shape[0], dtype=np.bool) - if mode == 'Octagon': + if mode == 'octagon': for i in range(2, n_scales): - c = (OCTAGON_OUTER_SHAPE[i - 1][0] - 1) // 2 + OCTAGON_OUTER_SHAPE[i - 1][1] - cumulative_mask = cumulative_mask | (_mask_border_keypoints(image, keypoints, c) & (scales == i)) - - elif mode == 'STAR': + c = (OCTAGON_OUTER_SHAPE[i - 1][0] - 1) // 2 \ + + OCTAGON_OUTER_SHAPE[i - 1][1] + cumulative_mask |= _mask_border_keypoints(image, keypoints, c) \ + & (scales == i) + elif mode == 'star': for i in range(2, n_scales): - c = STAR_SHAPE[STAR_FILTER_SHAPE[i - 1][0]] + STAR_SHAPE[STAR_FILTER_SHAPE[i - 1][0]] // 2 - cumulative_mask = cumulative_mask | (_mask_border_keypoints(image, keypoints, c) & (scales == i)) + c = STAR_SHAPE[STAR_FILTER_SHAPE[i - 1][0]] \ + + STAR_SHAPE[STAR_FILTER_SHAPE[i - 1][0]] // 2 + cumulative_mask |= _mask_border_keypoints(image, keypoints, c) \ + & (scales == i) return keypoints[cumulative_mask], scales[cumulative_mask] diff --git a/skimage/feature/censure_cy.pyx b/skimage/feature/censure_cy.pyx index 93c7c142..cfd1260f 100644 --- a/skimage/feature/censure_cy.pyx +++ b/skimage/feature/censure_cy.pyx @@ -3,29 +3,28 @@ #cython: nonecheck=False #cython: wraparound=False -cimport numpy as cnp -import numpy as np - -def _censure_dob_loop(double[:, ::1] image, Py_ssize_t n, +def _censure_dob_loop(Py_ssize_t n, double[:, ::1] integral_img, double[:, ::1] filtered_image, double inner_weight, double outer_weight): cdef Py_ssize_t i, j cdef double inner, outer + cdef Py_ssize_t n2 = 2 * n + cdef double total_weight = inner_weight + outer_weight - for i in range(2 * n, image.shape[0] - 2 * n): - for j in range(2 * n, image.shape[1] - 2 * n): + for i in range(n2, integral_img.shape[0] - n2): + for j in range(n2, integral_img.shape[1] - n2): inner = (integral_img[i + n, j + n] + integral_img[i - n - 1, j - n - 1] - integral_img[i + n, j - n - 1] - integral_img[i - n - 1, j + n]) - outer = (integral_img[i + 2 * n, j + 2 * n] - + integral_img[i - 2 * n - 1, j - 2 * n - 1] - - integral_img[i + 2 * n, j - 2 * n - 1] - - integral_img[i - 2 * n - 1, j + 2 * n]) + outer = (integral_img[i + n2, j + n2] + + integral_img[i - n2 - 1, j - n2 - 1] + - integral_img[i + n2, j - n2 - 1] + - integral_img[i - n2 - 1, j + n2]) filtered_image[i, j] = (outer_weight * outer - - (inner_weight + outer_weight) * inner) + - total_weight * inner) diff --git a/skimage/feature/tests/test_censure.py b/skimage/feature/tests/test_censure.py index 41d1886e..14e97a39 100644 --- a/skimage/feature/tests/test_censure.py +++ b/skimage/feature/tests/test_censure.py @@ -1,6 +1,7 @@ import numpy as np from numpy.testing import assert_array_equal, assert_raises from skimage.data import moon +from skimage.util import img_as_ubyte from skimage.feature import censure_keypoints @@ -15,10 +16,7 @@ def test_censure_keypoints_moon_image_DoB(): the expected values for DoB filter.""" img = moon() actual_kp_DoB, actual_scale = censure_keypoints(img, 7, 'DoB', 0.15) - expected_kp_DoB = np.array([[ 4, 507], - [ 8, 503], - [ 12, 499], - [ 21, 497], + expected_kp_DoB = np.array([[ 21, 497], [ 36, 46], [119, 350], [185, 177], @@ -27,7 +25,7 @@ def test_censure_keypoints_moon_image_DoB(): [463, 116], [464, 132], [467, 260]]) - expected_scale = np.array([2, 4, 6, 3, 4, 4, 2, 2, 3, 2, 2, 2]) + expected_scale = np.array([3, 4, 4, 2, 2, 3, 2, 2, 2]) assert_array_equal(expected_kp_DoB, actual_kp_DoB) assert_array_equal(expected_scale, actual_scale) @@ -37,14 +35,15 @@ def test_censure_keypoints_moon_image_Octagon(): """Verify the actual Censure keypoints and their corresponding scale with the expected values for Octagon filter.""" img = moon() - actual_kp_Octagon, actual_scale = censure_keypoints(img, 7, 'Octagon', 0.15) + actual_kp_Octagon, actual_scale = censure_keypoints(img, 7, 'Octagon', + 0.15) expected_kp_Octagon = np.array([[ 21, 496], [ 35, 46], [287, 250], [356, 239], [463, 116]]) - expected_scale = np.array([3, 4, 2, 2, 2], dtype=np.intp) + expected_scale = np.array([3, 4, 2, 2, 2]) assert_array_equal(expected_kp_Octagon, actual_kp_Octagon) assert_array_equal(expected_scale, actual_scale) @@ -66,7 +65,7 @@ def test_censure_keypoints_moon_image_STAR(): [463, 116], [467, 260]]) - expected_scale = np.array([3, 3, 6, 2, 3, 2, 3, 5, 2, 2], dtype=np.intp) + expected_scale = np.array([3, 3, 6, 2, 3, 2, 3, 5, 2, 2]) assert_array_equal(expected_kp_STAR, actual_kp_STAR) assert_array_equal(expected_scale, actual_scale) From 7665281c4a76f2d0799bf154b4999853f401fe8b Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Mon, 12 Aug 2013 16:27:35 +0530 Subject: [PATCH 475/736] Lowercasing variables; removing unused import; replacing _remove by _mask --- skimage/feature/tests/test_censure.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/skimage/feature/tests/test_censure.py b/skimage/feature/tests/test_censure.py index 14e97a39..9acdd2e9 100644 --- a/skimage/feature/tests/test_censure.py +++ b/skimage/feature/tests/test_censure.py @@ -1,7 +1,6 @@ import numpy as np from numpy.testing import assert_array_equal, assert_raises from skimage.data import moon -from skimage.util import img_as_ubyte from skimage.feature import censure_keypoints @@ -11,12 +10,12 @@ def test_censure_keypoints_color_image_unsupported_error(): assert_raises(ValueError, censure_keypoints, img) -def test_censure_keypoints_moon_image_DoB(): +def test_censure_keypoints_moon_image_dob(): """Verify the actual Censure keypoints and their corresponding scale with the expected values for DoB filter.""" img = moon() - actual_kp_DoB, actual_scale = censure_keypoints(img, 7, 'DoB', 0.15) - expected_kp_DoB = np.array([[ 21, 497], + actual_kp_dob, actual_scale = censure_keypoints(img, 7, 'DoB', 0.15) + expected_kp_dob = np.array([[ 21, 497], [ 36, 46], [119, 350], [185, 177], @@ -27,7 +26,7 @@ def test_censure_keypoints_moon_image_DoB(): [467, 260]]) expected_scale = np.array([3, 4, 4, 2, 2, 3, 2, 2, 2]) - assert_array_equal(expected_kp_DoB, actual_kp_DoB) + assert_array_equal(expected_kp_dob, actual_kp_dob) assert_array_equal(expected_scale, actual_scale) @@ -35,9 +34,9 @@ def test_censure_keypoints_moon_image_Octagon(): """Verify the actual Censure keypoints and their corresponding scale with the expected values for Octagon filter.""" img = moon() - actual_kp_Octagon, actual_scale = censure_keypoints(img, 7, 'Octagon', + actual_kp_octagon, actual_scale = censure_keypoints(img, 7, 'Octagon', 0.15) - expected_kp_Octagon = np.array([[ 21, 496], + expected_kp_octagon = np.array([[ 21, 496], [ 35, 46], [287, 250], [356, 239], @@ -45,7 +44,7 @@ def test_censure_keypoints_moon_image_Octagon(): expected_scale = np.array([3, 4, 2, 2, 2]) - assert_array_equal(expected_kp_Octagon, actual_kp_Octagon) + assert_array_equal(expected_kp_octagon, actual_kp_octagon) assert_array_equal(expected_scale, actual_scale) @@ -53,8 +52,8 @@ def test_censure_keypoints_moon_image_STAR(): """Verify the actual Censure keypoints and their corresponding scale with the expected values for STAR filter.""" img = moon() - actual_kp_STAR, actual_scale = censure_keypoints(img, 7, 'STAR', 0.15) - expected_kp_STAR = np.array([[ 21, 497], + actual_kp_star, actual_scale = censure_keypoints(img, 7, 'STAR', 0.15) + expected_kp_star = np.array([[ 21, 497], [ 36, 46], [117, 356], [185, 177], @@ -67,7 +66,7 @@ def test_censure_keypoints_moon_image_STAR(): expected_scale = np.array([3, 3, 6, 2, 3, 2, 3, 5, 2, 2]) - assert_array_equal(expected_kp_STAR, actual_kp_STAR) + assert_array_equal(expected_kp_star, actual_kp_star) assert_array_equal(expected_scale, actual_scale) From 229910efe739b2a53841af3faaaa9bb69f83d177 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Mon, 12 Aug 2013 16:37:09 +0530 Subject: [PATCH 476/736] Replacing censure_keypoints by keypoints_censure, n_scales by max_scale --- skimage/feature/__init__.py | 4 ++-- skimage/feature/censure.py | 24 ++++++++++++------------ skimage/feature/tests/test_censure.py | 18 +++++++++--------- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/skimage/feature/__init__.py b/skimage/feature/__init__.py index 53c21d31..bde81e59 100644 --- a/skimage/feature/__init__.py +++ b/skimage/feature/__init__.py @@ -9,7 +9,7 @@ from .corner_cy import corner_moravec from .template import match_template from ._brief import brief, match_keypoints_brief from .util import pairwise_hamming_distance -from .censure import censure_keypoints +from .censure import keypoints_censure __all__ = ['daisy', 'hog', @@ -28,4 +28,4 @@ __all__ = ['daisy', 'brief', 'pairwise_hamming_distance', 'match_keypoints_brief', - 'censure_keypoints'] + 'keypoints_censure'] diff --git a/skimage/feature/censure.py b/skimage/feature/censure.py index 583e3d0a..3d11177b 100644 --- a/skimage/feature/censure.py +++ b/skimage/feature/censure.py @@ -19,9 +19,9 @@ STAR_FILTER_SHAPE = [(1, 0), (3, 1), (4, 2), (5, 3), (7, 4), (8, 5), (9, 6),(11, 8), (13, 10), (14, 11), (15, 12), (16, 14)] -def _get_filtered_image(image, n_scales, mode): +def _get_filtered_image(image, max_scale, mode): - scales = np.zeros((image.shape[0], image.shape[1], n_scales), + scales = np.zeros((image.shape[0], image.shape[1], max_scale), dtype=np.double) if mode == 'dob': @@ -34,7 +34,7 @@ def _get_filtered_image(image, n_scales, mode): integral_img = integral_image(image) - for i in range(n_scales): + for i in range(max_scale): n = i + 1 # Constant multipliers for the outer region and the inner region @@ -54,14 +54,14 @@ def _get_filtered_image(image, n_scales, mode): elif mode == 'octagon': # TODO : Decide the shapes of Octagon filters for scales > 7 - for i in range(n_scales): + for i in range(max_scale): mo, no = OCTAGON_OUTER_SHAPE[i] mi, ni = OCTAGON_INNER_SHAPE[i] scales[:, :, i] = convolve(image, _octagon_filter_kernel(mo, no, mi, ni)) elif mode == 'star': - for i in range(n_scales): + for i in range(max_scale): m = STAR_SHAPE[STAR_FILTER_SHAPE[i][0]] n = STAR_SHAPE[STAR_FILTER_SHAPE[i][1]] scales[:, :, i] = convolve(image, @@ -135,7 +135,7 @@ def _suppress_lines(feature_mask, image, sigma, line_threshold): > line_threshold * (Axx * Ayy - Axy * Axy)] = False -def censure_keypoints(image, n_scales=7, mode='DoB', non_max_threshold=0.15, +def keypoints_censure(image, max_scale=7, mode='DoB', non_max_threshold=0.15, line_threshold=10): """ Extracts Censure keypoints along with the corresponding scale using @@ -145,7 +145,7 @@ def censure_keypoints(image, n_scales=7, mode='DoB', non_max_threshold=0.15, ---------- image : 2D ndarray Input image. - n_scales : positive integer + max_scale : positive integer Number of scales to extract keypoints from. The keypoints will be extracted from all the scales except the first and the last. mode : ('DoB', 'Octagon', 'STAR') @@ -192,7 +192,7 @@ def censure_keypoints(image, n_scales=7, mode='DoB', non_max_threshold=0.15, raise ValueError('Mode must be one of "DoB", "Octagon", "STAR".') # Generating all the scales - filter_response = _get_filtered_image(image, n_scales, mode) + filter_response = _get_filtered_image(image, max_scale, mode) # Suppressing points that are neither minima or maxima in their 3 x 3 x 3 # neighbourhood to zero @@ -202,7 +202,7 @@ def censure_keypoints(image, n_scales=7, mode='DoB', non_max_threshold=0.15, feature_mask = minimas | maximas feature_mask[filter_response < non_max_threshold] = False - for i in range(1, n_scales - 1): + for i in range(1, max_scale - 1): # sigma = (window_size - 1) / 6.0, so the window covers > 99% of the # kernel's distribution # window_size = 7 + 2 * i @@ -210,7 +210,7 @@ def censure_keypoints(image, n_scales=7, mode='DoB', non_max_threshold=0.15, _suppress_lines(feature_mask[:, :, i], image, (1 + i / 3.0), line_threshold) - rows, cols, scales = np.nonzero(feature_mask[..., 1:n_scales - 1]) + rows, cols, scales = np.nonzero(feature_mask[..., 1:max_scale - 1]) keypoints = np.column_stack([rows, cols]) scales = scales + 2 @@ -220,13 +220,13 @@ def censure_keypoints(image, n_scales=7, mode='DoB', non_max_threshold=0.15, cumulative_mask = np.zeros(keypoints.shape[0], dtype=np.bool) if mode == 'octagon': - for i in range(2, n_scales): + for i in range(2, max_scale): c = (OCTAGON_OUTER_SHAPE[i - 1][0] - 1) // 2 \ + OCTAGON_OUTER_SHAPE[i - 1][1] cumulative_mask |= _mask_border_keypoints(image, keypoints, c) \ & (scales == i) elif mode == 'star': - for i in range(2, n_scales): + for i in range(2, max_scale): c = STAR_SHAPE[STAR_FILTER_SHAPE[i - 1][0]] \ + STAR_SHAPE[STAR_FILTER_SHAPE[i - 1][0]] // 2 cumulative_mask |= _mask_border_keypoints(image, keypoints, c) \ diff --git a/skimage/feature/tests/test_censure.py b/skimage/feature/tests/test_censure.py index 9acdd2e9..5401eed1 100644 --- a/skimage/feature/tests/test_censure.py +++ b/skimage/feature/tests/test_censure.py @@ -1,20 +1,20 @@ import numpy as np from numpy.testing import assert_array_equal, assert_raises from skimage.data import moon -from skimage.feature import censure_keypoints +from skimage.feature import keypoints_censure -def test_censure_keypoints_color_image_unsupported_error(): +def test_keypoints_censure_color_image_unsupported_error(): """Censure keypoints can be extracted from gray-scale images only.""" img = np.zeros((20, 20, 3)) - assert_raises(ValueError, censure_keypoints, img) + assert_raises(ValueError, keypoints_censure, img) -def test_censure_keypoints_moon_image_dob(): +def test_keypoints_censure_moon_image_dob(): """Verify the actual Censure keypoints and their corresponding scale with the expected values for DoB filter.""" img = moon() - actual_kp_dob, actual_scale = censure_keypoints(img, 7, 'DoB', 0.15) + actual_kp_dob, actual_scale = keypoints_censure(img, 7, 'DoB', 0.15) expected_kp_dob = np.array([[ 21, 497], [ 36, 46], [119, 350], @@ -30,11 +30,11 @@ def test_censure_keypoints_moon_image_dob(): assert_array_equal(expected_scale, actual_scale) -def test_censure_keypoints_moon_image_Octagon(): +def test_keypoints_censure_moon_image_Octagon(): """Verify the actual Censure keypoints and their corresponding scale with the expected values for Octagon filter.""" img = moon() - actual_kp_octagon, actual_scale = censure_keypoints(img, 7, 'Octagon', + actual_kp_octagon, actual_scale = keypoints_censure(img, 7, 'Octagon', 0.15) expected_kp_octagon = np.array([[ 21, 496], [ 35, 46], @@ -48,11 +48,11 @@ def test_censure_keypoints_moon_image_Octagon(): assert_array_equal(expected_scale, actual_scale) -def test_censure_keypoints_moon_image_STAR(): +def test_keypoints_censure_moon_image_STAR(): """Verify the actual Censure keypoints and their corresponding scale with the expected values for STAR filter.""" img = moon() - actual_kp_star, actual_scale = censure_keypoints(img, 7, 'STAR', 0.15) + actual_kp_star, actual_scale = keypoints_censure(img, 7, 'STAR', 0.15) expected_kp_star = np.array([[ 21, 497], [ 36, 46], [117, 356], From 6d1be9aee40cf2b51205c1be491b927d829cb500 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Mon, 12 Aug 2013 16:59:09 +0530 Subject: [PATCH 477/736] Documenting the code --- skimage/feature/censure.py | 10 ++++++++-- skimage/feature/censure_cy.pyx | 4 ++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/skimage/feature/censure.py b/skimage/feature/censure.py index 3d11177b..18313eaf 100644 --- a/skimage/feature/censure.py +++ b/skimage/feature/censure.py @@ -149,8 +149,14 @@ def keypoints_censure(image, max_scale=7, mode='DoB', non_max_threshold=0.15, Number of scales to extract keypoints from. The keypoints will be extracted from all the scales except the first and the last. mode : ('DoB', 'Octagon', 'STAR') - Type of bilevel filter used to get the scales of input image. Possible - values are 'DoB', 'Octagon' and 'STAR'. + Type of bilevel filter used to get the scales of the input image. + Possible values are 'DoB', 'Octagon' and 'STAR'. The three modes + represent the shape of the bilevel filters i.e. box(square), octagon + and star respectively. For instance, a bilevel octagon filter consists + of a smaller inner octagon and a larger outer octagon with the filter + weights being uniformly negative in both the inner octagon while + uniformly positive in the difference region. Use STAR and Octagon for + better features and DoB for better performance. non_max_threshold : float Threshold value used to suppress maximas and minimas with a weak magnitude response obtained after Non-Maximal Suppression. diff --git a/skimage/feature/censure_cy.pyx b/skimage/feature/censure_cy.pyx index cfd1260f..a9980071 100644 --- a/skimage/feature/censure_cy.pyx +++ b/skimage/feature/censure_cy.pyx @@ -8,6 +8,10 @@ def _censure_dob_loop(Py_ssize_t n, double[:, ::1] integral_img, double[:, ::1] filtered_image, double inner_weight, double outer_weight): + # This function calculates the value in the DoB filtered image using + # integral images. If r = right. l = left, u = up, d = down, the sum of + # pixel values in the rectangle formed by (u, l), (u, r), (d, r), (d, l) + # is calculated as I(d, r) + I(u - 1, l - 1) - I(u - 1, r) - I(d, l - 1). cdef Py_ssize_t i, j cdef double inner, outer From c3b6ce2c8270abd5801afcd1633f7600f3b2f071 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Mon, 12 Aug 2013 23:51:19 +0530 Subject: [PATCH 478/736] Including min_scale as another input parameter --- skimage/feature/censure.py | 55 +++++++++++++++------------ skimage/feature/censure_cy.pyx | 42 +++++++++++++++++++- skimage/feature/tests/test_censure.py | 10 ++--- 3 files changed, 76 insertions(+), 31 deletions(-) diff --git a/skimage/feature/censure.py b/skimage/feature/censure.py index 18313eaf..996f7e16 100644 --- a/skimage/feature/censure.py +++ b/skimage/feature/censure.py @@ -19,10 +19,10 @@ STAR_FILTER_SHAPE = [(1, 0), (3, 1), (4, 2), (5, 3), (7, 4), (8, 5), (9, 6),(11, 8), (13, 10), (14, 11), (15, 12), (16, 14)] -def _get_filtered_image(image, max_scale, mode): +def _get_filtered_image(image, min_scale, max_scale, mode): - scales = np.zeros((image.shape[0], image.shape[1], max_scale), - dtype=np.double) + scales = np.zeros((image.shape[0], image.shape[1], + max_scale - min_scale +1), dtype=np.double) if mode == 'dob': @@ -34,8 +34,8 @@ def _get_filtered_image(image, max_scale, mode): integral_img = integral_image(image) - for i in range(max_scale): - n = i + 1 + for i in range(max_scale - min_scale + 1): + n = min_scale + i # Constant multipliers for the outer region and the inner region # of the bilevel filters with the constraint of keeping the @@ -54,16 +54,16 @@ def _get_filtered_image(image, max_scale, mode): elif mode == 'octagon': # TODO : Decide the shapes of Octagon filters for scales > 7 - for i in range(max_scale): - mo, no = OCTAGON_OUTER_SHAPE[i] - mi, ni = OCTAGON_INNER_SHAPE[i] + for i in range(max_scale - min_scale + 1): + mo, no = OCTAGON_OUTER_SHAPE[min_scale + i - 1] + mi, ni = OCTAGON_INNER_SHAPE[min_scale + i - 1] scales[:, :, i] = convolve(image, _octagon_filter_kernel(mo, no, mi, ni)) elif mode == 'star': - for i in range(max_scale): - m = STAR_SHAPE[STAR_FILTER_SHAPE[i][0]] - n = STAR_SHAPE[STAR_FILTER_SHAPE[i][1]] + for i in range(max_scale - min_scale + 1): + m = STAR_SHAPE[STAR_FILTER_SHAPE[min_scale + i - 1][0]] + n = STAR_SHAPE[STAR_FILTER_SHAPE[min_scale + i - 1][1]] scales[:, :, i] = convolve(image, _star_filter_kernel(m, n)) @@ -135,8 +135,8 @@ def _suppress_lines(feature_mask, image, sigma, line_threshold): > line_threshold * (Axx * Ayy - Axy * Axy)] = False -def keypoints_censure(image, max_scale=7, mode='DoB', non_max_threshold=0.15, - line_threshold=10): +def keypoints_censure(image, min_scale=1, max_scale=7, mode='DoB', + non_max_threshold=0.15, line_threshold=10): """ Extracts Censure keypoints along with the corresponding scale using either Difference of Boxes, Octagon or STAR bilevel filter. @@ -145,9 +145,12 @@ def keypoints_censure(image, max_scale=7, mode='DoB', non_max_threshold=0.15, ---------- image : 2D ndarray Input image. + min_scale : positive integer + Minimum scale to extract keypoints from. max_scale : positive integer - Number of scales to extract keypoints from. The keypoints will be - extracted from all the scales except the first and the last. + Maximum scale to extract keypoints from. The keypoints will be + extracted from all the scales except the first and the last i.e. + from the scales in the range [min_scale + 1, max_scale - 1]. mode : ('DoB', 'Octagon', 'STAR') Type of bilevel filter used to get the scales of the input image. Possible values are 'DoB', 'Octagon' and 'STAR'. The three modes @@ -197,8 +200,12 @@ def keypoints_censure(image, max_scale=7, mode='DoB', non_max_threshold=0.15, if mode not in ('dob', 'octagon', 'star'): raise ValueError('Mode must be one of "DoB", "Octagon", "STAR".') + if max_scale - min_scale < 2: + raise ValueError('The number of scales should be greater than or' + 'equal to 3.') + # Generating all the scales - filter_response = _get_filtered_image(image, max_scale, mode) + filter_response = _get_filtered_image(image, min_scale, max_scale, mode) # Suppressing points that are neither minima or maxima in their 3 x 3 x 3 # neighbourhood to zero @@ -208,17 +215,17 @@ def keypoints_censure(image, max_scale=7, mode='DoB', non_max_threshold=0.15, feature_mask = minimas | maximas feature_mask[filter_response < non_max_threshold] = False - for i in range(1, max_scale - 1): + for i in range(1, max_scale - min_scale): # sigma = (window_size - 1) / 6.0, so the window covers > 99% of the # kernel's distribution - # window_size = 7 + 2 * i - # Hence sigma = 1 + i / 3.0 + # window_size = 7 + 2 * (min_scale - 1 + i) + # Hence sigma = 1 + (min_scale - 1 + i)/ 3.0 _suppress_lines(feature_mask[:, :, i], image, - (1 + i / 3.0), line_threshold) + (1 + (min_scale + i - 1) / 3.0), line_threshold) - rows, cols, scales = np.nonzero(feature_mask[..., 1:max_scale - 1]) + rows, cols, scales = np.nonzero(feature_mask[..., 1:max_scale - min_scale]) keypoints = np.column_stack([rows, cols]) - scales = scales + 2 + scales = scales + min_scale + 1 if mode == 'dob': return keypoints, scales @@ -226,13 +233,13 @@ def keypoints_censure(image, max_scale=7, mode='DoB', non_max_threshold=0.15, cumulative_mask = np.zeros(keypoints.shape[0], dtype=np.bool) if mode == 'octagon': - for i in range(2, max_scale): + for i in range(min_scale + 1, max_scale): c = (OCTAGON_OUTER_SHAPE[i - 1][0] - 1) // 2 \ + OCTAGON_OUTER_SHAPE[i - 1][1] cumulative_mask |= _mask_border_keypoints(image, keypoints, c) \ & (scales == i) elif mode == 'star': - for i in range(2, max_scale): + for i in range(min_scale + 1, max_scale): c = STAR_SHAPE[STAR_FILTER_SHAPE[i - 1][0]] \ + STAR_SHAPE[STAR_FILTER_SHAPE[i - 1][0]] // 2 cumulative_mask |= _mask_border_keypoints(image, keypoints, c) \ diff --git a/skimage/feature/censure_cy.pyx b/skimage/feature/censure_cy.pyx index a9980071..1c352fde 100644 --- a/skimage/feature/censure_cy.pyx +++ b/skimage/feature/censure_cy.pyx @@ -18,8 +18,46 @@ def _censure_dob_loop(Py_ssize_t n, cdef Py_ssize_t n2 = 2 * n cdef double total_weight = inner_weight + outer_weight - for i in range(n2, integral_img.shape[0] - n2): - for j in range(n2, integral_img.shape[1] - n2): + # top-left pixel + inner = (integral_img[n2 + n, n2 + n] + + integral_img[n2 - n - 1, n2 - n - 1] + - integral_img[n2 + n, n2 - n - 1] + - integral_img[n2 - n - 1, n2 + n]) + + outer = integral_img[2 * n2, 2 * n2] + + filtered_image[n2, n2] = (outer_weight * outer + - total_weight * inner) + + # left column + for i in range(n2 + 1, integral_img.shape[0] - n2): + inner = (integral_img[i + n, n2 + n] + + integral_img[i - n - 1, n2 - n - 1] + - integral_img[i + n, n2 - n - 1] + - integral_img[i - n - 1, n2 + n]) + + outer = (integral_img[i + n2, 2 * n2] + - integral_img[i - n2 - 1, 2 * n2]) + + filtered_image[i, n2] = (outer_weight * outer + - total_weight * inner) + + # top row + for j in range(n2 + 1, integral_img.shape[1] - n2): + inner = (integral_img[n2 + n, j + n] + + integral_img[n2 - n - 1, j - n - 1] + - integral_img[n2 + n, j - n - 1] + - integral_img[n2 - n - 1, j + n]) + + outer = (integral_img[2 * n2, j + n2] + - integral_img[2 * n2, j - n2 - 1]) + + filtered_image[n2, j] = (outer_weight * outer + - total_weight * inner) + + # remaining block + for i in range(n2 + 1, integral_img.shape[0] - n2): + for j in range(n2 + 1, integral_img.shape[1] - n2): inner = (integral_img[i + n, j + n] + integral_img[i - n - 1, j - n - 1] - integral_img[i + n, j - n - 1] diff --git a/skimage/feature/tests/test_censure.py b/skimage/feature/tests/test_censure.py index 5401eed1..26555697 100644 --- a/skimage/feature/tests/test_censure.py +++ b/skimage/feature/tests/test_censure.py @@ -14,7 +14,7 @@ def test_keypoints_censure_moon_image_dob(): """Verify the actual Censure keypoints and their corresponding scale with the expected values for DoB filter.""" img = moon() - actual_kp_dob, actual_scale = keypoints_censure(img, 7, 'DoB', 0.15) + actual_kp_dob, actual_scale = keypoints_censure(img, 1, 7, 'DoB', 0.15) expected_kp_dob = np.array([[ 21, 497], [ 36, 46], [119, 350], @@ -30,11 +30,11 @@ def test_keypoints_censure_moon_image_dob(): assert_array_equal(expected_scale, actual_scale) -def test_keypoints_censure_moon_image_Octagon(): +def test_keypoints_censure_moon_image_octagon(): """Verify the actual Censure keypoints and their corresponding scale with the expected values for Octagon filter.""" img = moon() - actual_kp_octagon, actual_scale = keypoints_censure(img, 7, 'Octagon', + actual_kp_octagon, actual_scale = keypoints_censure(img, 1, 7, 'Octagon', 0.15) expected_kp_octagon = np.array([[ 21, 496], [ 35, 46], @@ -48,11 +48,11 @@ def test_keypoints_censure_moon_image_Octagon(): assert_array_equal(expected_scale, actual_scale) -def test_keypoints_censure_moon_image_STAR(): +def test_keypoints_censure_moon_image_star(): """Verify the actual Censure keypoints and their corresponding scale with the expected values for STAR filter.""" img = moon() - actual_kp_star, actual_scale = keypoints_censure(img, 7, 'STAR', 0.15) + actual_kp_star, actual_scale = keypoints_censure(img, 1, 7, 'STAR', 0.15) expected_kp_star = np.array([[ 21, 497], [ 36, 46], [117, 356], From 6c27d278d0f6ae107ea4a24b6ca1664cfb5c3fb5 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Tue, 13 Aug 2013 00:21:25 +0530 Subject: [PATCH 479/736] Adding remaining tests --- skimage/feature/censure.py | 7 +++---- skimage/feature/tests/test_censure.py | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/skimage/feature/censure.py b/skimage/feature/censure.py index 996f7e16..d52583c2 100644 --- a/skimage/feature/censure.py +++ b/skimage/feature/censure.py @@ -192,11 +192,7 @@ def keypoints_censure(image, min_scale=1, max_scale=7, mode='DoB', if image.ndim != 2: raise ValueError("Only 2-D gray-scale images supported.") - image = img_as_float(image) - image = np.ascontiguousarray(image) - mode = mode.lower() - if mode not in ('dob', 'octagon', 'star'): raise ValueError('Mode must be one of "DoB", "Octagon", "STAR".') @@ -204,6 +200,9 @@ def keypoints_censure(image, min_scale=1, max_scale=7, mode='DoB', raise ValueError('The number of scales should be greater than or' 'equal to 3.') + image = img_as_float(image) + image = np.ascontiguousarray(image) + # Generating all the scales filter_response = _get_filtered_image(image, min_scale, max_scale, mode) diff --git a/skimage/feature/tests/test_censure.py b/skimage/feature/tests/test_censure.py index 26555697..4cd2ad68 100644 --- a/skimage/feature/tests/test_censure.py +++ b/skimage/feature/tests/test_censure.py @@ -10,6 +10,20 @@ def test_keypoints_censure_color_image_unsupported_error(): assert_raises(ValueError, keypoints_censure, img) +def test_keypoints_censure_mode_validity_error(): + """Mode argument in keypoints_censure can be either DoB, Octagon or + STAR.""" + img = np.zeros((20, 20)) + assert_raises(ValueError, keypoints_censure, img, mode='dummy') + + +def test_keypoints_censure_scale_range_error(): + """Difference between the the max_scale and min_scale parameters in + keypoints_censure should be greater than or equal to two.""" + img = np.zeros((20, 20)) + assert_raises(ValueError, keypoints_censure, img, min_scale=1, max_scale=2) + + def test_keypoints_censure_moon_image_dob(): """Verify the actual Censure keypoints and their corresponding scale with the expected values for DoB filter.""" From f2632f26bb4ec2586b4150250a54c471b818a667 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Tue, 13 Aug 2013 20:11:37 +0530 Subject: [PATCH 480/736] Adding gallery example for plotting censure keypoints --- doc/examples/plot_censure_keypoints.py | 46 ++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 doc/examples/plot_censure_keypoints.py diff --git a/doc/examples/plot_censure_keypoints.py b/doc/examples/plot_censure_keypoints.py new file mode 100644 index 00000000..c2119b46 --- /dev/null +++ b/doc/examples/plot_censure_keypoints.py @@ -0,0 +1,46 @@ +""" +=========================== +Censure Keypoints Detection +=========================== + +In this example, we detect and plot the Censure Keypoints at various scales +using Difference of Boxes, Octagon and Star shaped bi-level filters. + +""" +from skimage.feature import keypoints_censure +from skimage.data import lena +from skimage.color import rgb2gray +import matplotlib.pyplot as plt + +# Initializing the parameters for Censure keypoints +img = lena() +gray_img = rgb2gray(img) +min_scale = 1 +max_scale = 7 +nms_threshold = 0.15 +rpc_threshold = 10 + + +# Plotting features for the following modes +for mode in ['dob', 'octagon', 'star']: + + kp_censure, scale = keypoints_censure(gray_img, min_scale, max_scale, + mode, nms_threshold,rpc_threshold) + f, axarr = plt.subplots((max_scale - min_scale + 1) // 3, 3) + + # Plotting Censure features at all the scales + for i in range(max_scale - min_scale - 1): + keypoints = kp_censure[scale == i + min_scale + 1] + num = len(keypoints) + x = keypoints[:, 1] + y = keypoints[:, 0] + s = 5 * 2**(i + min_scale + 1) + axarr[i // 3, i - (i // 3) * 3].imshow(img) + axarr[i // 3, i - (i // 3) * 3].scatter(x, y, s, facecolors='none', + edgecolors='g') + axarr[i // 3, i - (i // 3) * 3].set_title(' %s %s-Censure features at ' + 'scale %d' % (num, mode, i + + min_scale + 1)) + + plt.suptitle('NMS threshold = %f, RPC threshold = %d' % (nms_threshold, rpc_threshold)) +plt.show() From b61b3303559da8c8ca4018c09228f1c32b933d2b Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Wed, 14 Aug 2013 14:25:15 +0530 Subject: [PATCH 481/736] Adding a short description of the algorithm --- skimage/feature/censure.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/skimage/feature/censure.py b/skimage/feature/censure.py index d52583c2..272218f6 100644 --- a/skimage/feature/censure.py +++ b/skimage/feature/censure.py @@ -187,7 +187,17 @@ def keypoints_censure(image, min_scale=1, max_scale=7, mode='DoB', http://www.jamris.org/01_2013/saveas.php?QUEST=JAMRIS_No01_2013_P_11-20.pdf """ - + # (1) First we generate the required scales on the input grayscale image + # using a bilevel filter and stack them up in `filter_response`. + # (2) We then perform Non-Maximal suppression in 3 x 3 x 3 window on the + # filter_response to suppress points that are neither minima or maxima in + # 3 x 3 x 3 neighbourhood. We obtain a boolean ndarray `feature_mask` + # containing all the minimas and maximas in `filter_response` as True. + # (3) Then we suppress all the points in the `feature_mask` for which the + # corresponding point in the image at a particular scale has the ratio of + # principal curvatures greater than `line_threshold`. + # (4) Finally, we remove the border keypoints and return the keypoints + # along with its corresponding scale. image = np.squeeze(image) if image.ndim != 2: raise ValueError("Only 2-D gray-scale images supported.") From 3ee183fdf8714ca5d9f7aabc3e0581b2c13c0833 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Wed, 14 Aug 2013 14:36:56 +0530 Subject: [PATCH 482/736] PEP8 corrections and stylistic changes --- doc/examples/plot_censure_keypoints.py | 10 +++++----- skimage/feature/censure.py | 9 +++++---- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/doc/examples/plot_censure_keypoints.py b/doc/examples/plot_censure_keypoints.py index c2119b46..7f08d409 100644 --- a/doc/examples/plot_censure_keypoints.py +++ b/doc/examples/plot_censure_keypoints.py @@ -20,17 +20,16 @@ max_scale = 7 nms_threshold = 0.15 rpc_threshold = 10 - -# Plotting features for the following modes +# Detecting Censure keypoints for the following filters for mode in ['dob', 'octagon', 'star']: kp_censure, scale = keypoints_censure(gray_img, min_scale, max_scale, - mode, nms_threshold,rpc_threshold) + mode, nms_threshold, rpc_threshold) f, axarr = plt.subplots((max_scale - min_scale + 1) // 3, 3) # Plotting Censure features at all the scales for i in range(max_scale - min_scale - 1): - keypoints = kp_censure[scale == i + min_scale + 1] + keypoints = kp_censure[scale == i + min_scale + 1] num = len(keypoints) x = keypoints[:, 1] y = keypoints[:, 0] @@ -42,5 +41,6 @@ for mode in ['dob', 'octagon', 'star']: 'scale %d' % (num, mode, i + min_scale + 1)) - plt.suptitle('NMS threshold = %f, RPC threshold = %d' % (nms_threshold, rpc_threshold)) + plt.suptitle('NMS threshold = %f, RPC threshold = %d' + % (nms_threshold, rpc_threshold)) plt.show() diff --git a/skimage/feature/censure.py b/skimage/feature/censure.py index 272218f6..d90b36cf 100644 --- a/skimage/feature/censure.py +++ b/skimage/feature/censure.py @@ -16,13 +16,13 @@ OCTAGON_INNER_SHAPE = [(3, 0), (3, 1), (3, 2), (5, 2), (5, 3), (5, 4), (5, 5)] STAR_SHAPE = [1, 2, 3, 4, 6, 8, 11, 12, 16, 22, 23, 32, 45, 46, 64, 90, 128] STAR_FILTER_SHAPE = [(1, 0), (3, 1), (4, 2), (5, 3), (7, 4), (8, 5), - (9, 6),(11, 8), (13, 10), (14, 11), (15, 12), (16, 14)] + (9, 6), (11, 8), (13, 10), (14, 11), (15, 12), (16, 14)] def _get_filtered_image(image, min_scale, max_scale, mode): scales = np.zeros((image.shape[0], image.shape[1], - max_scale - min_scale +1), dtype=np.double) + max_scale - min_scale + 1), dtype=np.double) if mode == 'dob': @@ -75,7 +75,7 @@ def _oct(m, n): f = np.zeros((m + 2*n, m + 2*n)) f[0, n] = 1 f[n, 0] = 1 - f[0, m + n -1] = 1 + f[0, m + n - 1] = 1 f[m + n - 1, 0] = 1 f[-1, n] = 1 f[n, -1] = 1 @@ -198,6 +198,7 @@ def keypoints_censure(image, min_scale=1, max_scale=7, mode='DoB', # principal curvatures greater than `line_threshold`. # (4) Finally, we remove the border keypoints and return the keypoints # along with its corresponding scale. + image = np.squeeze(image) if image.ndim != 2: raise ValueError("Only 2-D gray-scale images supported.") @@ -230,7 +231,7 @@ def keypoints_censure(image, min_scale=1, max_scale=7, mode='DoB', # window_size = 7 + 2 * (min_scale - 1 + i) # Hence sigma = 1 + (min_scale - 1 + i)/ 3.0 _suppress_lines(feature_mask[:, :, i], image, - (1 + (min_scale + i - 1) / 3.0), line_threshold) + (1 + (min_scale + i - 1) / 3.0), line_threshold) rows, cols, scales = np.nonzero(feature_mask[..., 1:max_scale - min_scale]) keypoints = np.column_stack([rows, cols]) From 7c4152b62bae274228272b3966cb400681cc0d4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Thu, 15 Aug 2013 08:36:09 +0200 Subject: [PATCH 483/736] Fix censure example and fix some minor issues --- doc/examples/plot_censure_keypoints.py | 63 +++++++++++++++----------- skimage/feature/censure.py | 58 ++++++++++++------------ 2 files changed, 65 insertions(+), 56 deletions(-) diff --git a/doc/examples/plot_censure_keypoints.py b/doc/examples/plot_censure_keypoints.py index 7f08d409..95fa0e58 100644 --- a/doc/examples/plot_censure_keypoints.py +++ b/doc/examples/plot_censure_keypoints.py @@ -1,12 +1,14 @@ """ -=========================== -Censure Keypoints Detection -=========================== +========================= +CenSurE Feature Detection +========================= -In this example, we detect and plot the Censure Keypoints at various scales -using Difference of Boxes, Octagon and Star shaped bi-level filters. +In this example we detect and plot the CenSurE (Center Surround Extrema) +features at various scales using Difference of Boxes, Octagon and Star shaped +bi-level filters. """ + from skimage.feature import keypoints_censure from skimage.data import lena from skimage.color import rgb2gray @@ -15,32 +17,39 @@ import matplotlib.pyplot as plt # Initializing the parameters for Censure keypoints img = lena() gray_img = rgb2gray(img) -min_scale = 1 -max_scale = 7 -nms_threshold = 0.15 -rpc_threshold = 10 +min_scale = 2 +max_scale = 6 +non_max_threshold = 0.15 +line_threshold = 10 + + +f, ax = plt.subplots(nrows=(max_scale - min_scale - 1), ncols=3, + figsize=(6, 6)) +plt.subplots_adjust(wspace=0.02, hspace=0.02, top=0.94, + bottom=0.02, left=0.06, right=0.98) # Detecting Censure keypoints for the following filters -for mode in ['dob', 'octagon', 'star']: +for col, mode in enumerate(['dob', 'octagon', 'star']): - kp_censure, scale = keypoints_censure(gray_img, min_scale, max_scale, - mode, nms_threshold, rpc_threshold) - f, axarr = plt.subplots((max_scale - min_scale + 1) // 3, 3) + ax[0, col].set_title(mode.upper(), fontsize=12) + + keypoints, scales = keypoints_censure(gray_img, min_scale, max_scale, + mode, non_max_threshold, + line_threshold) # Plotting Censure features at all the scales - for i in range(max_scale - min_scale - 1): - keypoints = kp_censure[scale == i + min_scale + 1] - num = len(keypoints) - x = keypoints[:, 1] - y = keypoints[:, 0] - s = 5 * 2**(i + min_scale + 1) - axarr[i // 3, i - (i // 3) * 3].imshow(img) - axarr[i // 3, i - (i // 3) * 3].scatter(x, y, s, facecolors='none', - edgecolors='g') - axarr[i // 3, i - (i // 3) * 3].set_title(' %s %s-Censure features at ' - 'scale %d' % (num, mode, i + - min_scale + 1)) + for row, scale in enumerate(range(min_scale + 1, max_scale)): + keypoints_i = keypoints[scales == scale] + num = len(keypoints_i) + x = keypoints_i[:, 1] + y = keypoints_i[:, 0] + s = 0.5 * 2 ** (scale + min_scale + 1) + ax[row, col].imshow(img) + ax[row, col].scatter(x, y, s, facecolors='none', edgecolors='b') + ax[row, col].set_xticks([]) + ax[row, col].set_yticks([]) + ax[row, col].axis((0, img.shape[1], img.shape[0], 0)) + if col == 0: + ax[row, col].set_ylabel('Scale %d' % scale, fontsize=12) - plt.suptitle('NMS threshold = %f, RPC threshold = %d' - % (nms_threshold, rpc_threshold)) plt.show() diff --git a/skimage/feature/censure.py b/skimage/feature/censure.py index d90b36cf..2dda9306 100644 --- a/skimage/feature/censure.py +++ b/skimage/feature/censure.py @@ -19,18 +19,17 @@ STAR_FILTER_SHAPE = [(1, 0), (3, 1), (4, 2), (5, 3), (7, 4), (8, 5), (9, 6), (11, 8), (13, 10), (14, 11), (15, 12), (16, 14)] -def _get_filtered_image(image, min_scale, max_scale, mode): +def _filter_image(image, min_scale, max_scale, mode): - scales = np.zeros((image.shape[0], image.shape[1], - max_scale - min_scale + 1), dtype=np.double) + response = np.zeros((image.shape[0], image.shape[1], + max_scale - min_scale + 1), dtype=np.double) if mode == 'dob': - # make scales[:, :, i] contiguous memory block - item_size = scales.itemsize - scales.strides = (item_size * scales.shape[0], - item_size, - item_size * scales.shape[0] * scales.shape[1]) + # make response[:, :, i] contiguous memory block + item_size = response.itemsize + response.strides = (item_size * response.shape[0], item_size, + item_size * response.shape[0] * response.shape[1]) integral_img = integral_image(image) @@ -38,12 +37,12 @@ def _get_filtered_image(image, min_scale, max_scale, mode): n = min_scale + i # Constant multipliers for the outer region and the inner region - # of the bilevel filters with the constraint of keeping the + # of the bi-level filters with the constraint of keeping the # DC bias 0. inner_weight = (1.0 / (2 * n + 1)**2) outer_weight = (1.0 / (12 * n**2 + 4 * n)) - _censure_dob_loop(n, integral_img, scales[:, :, i], + _censure_dob_loop(n, integral_img, response[:, :, i], inner_weight, outer_weight) # NOTE : For the Octagon shaped filter, we implemented and evaluated the @@ -57,17 +56,17 @@ def _get_filtered_image(image, min_scale, max_scale, mode): for i in range(max_scale - min_scale + 1): mo, no = OCTAGON_OUTER_SHAPE[min_scale + i - 1] mi, ni = OCTAGON_INNER_SHAPE[min_scale + i - 1] - scales[:, :, i] = convolve(image, - _octagon_filter_kernel(mo, no, mi, ni)) + response[:, :, i] = convolve(image, + _octagon_filter_kernel(mo, no, mi, ni)) + elif mode == 'star': for i in range(max_scale - min_scale + 1): m = STAR_SHAPE[STAR_FILTER_SHAPE[min_scale + i - 1][0]] n = STAR_SHAPE[STAR_FILTER_SHAPE[min_scale + i - 1][1]] - scales[:, :, i] = convolve(image, - _star_filter_kernel(m, n)) + response[:, :, i] = convolve(image, _star_filter_kernel(m, n)) - return scales + return response # TODO : Import from selem after getting #669 merged. @@ -138,24 +137,24 @@ def _suppress_lines(feature_mask, image, sigma, line_threshold): def keypoints_censure(image, min_scale=1, max_scale=7, mode='DoB', non_max_threshold=0.15, line_threshold=10): """ - Extracts Censure keypoints along with the corresponding scale using - either Difference of Boxes, Octagon or STAR bilevel filter. + Extracts CenSurE keypoints along with the corresponding scale using + either Difference of Boxes, Octagon or STAR bi-level filter. Parameters ---------- image : 2D ndarray Input image. - min_scale : positive integer + min_scale : int Minimum scale to extract keypoints from. - max_scale : positive integer + max_scale : int Maximum scale to extract keypoints from. The keypoints will be extracted from all the scales except the first and the last i.e. from the scales in the range [min_scale + 1, max_scale - 1]. - mode : ('DoB', 'Octagon', 'STAR') - Type of bilevel filter used to get the scales of the input image. + mode : {'DoB', 'Octagon', 'STAR'} + Type of bi-level filter used to get the scales of the input image. Possible values are 'DoB', 'Octagon' and 'STAR'. The three modes - represent the shape of the bilevel filters i.e. box(square), octagon - and star respectively. For instance, a bilevel octagon filter consists + represent the shape of the bi-level filters i.e. box(square), octagon + and star respectively. For instance, a bi-level octagon filter consists of a smaller inner octagon and a larger outer octagon with the filter weights being uniformly negative in both the inner octagon while uniformly positive in the difference region. Use STAR and Octagon for @@ -170,7 +169,7 @@ def keypoints_censure(image, min_scale=1, max_scale=7, mode='DoB', Returns ------- keypoints : (N, 2) array - Location of the extracted keypoints in the (row, col) format. + Location of the extracted keypoints in the ``(row, col)`` format. scales : (N, 1) array The corresponding scale of the N extracted keypoints. @@ -187,8 +186,9 @@ def keypoints_censure(image, min_scale=1, max_scale=7, mode='DoB', http://www.jamris.org/01_2013/saveas.php?QUEST=JAMRIS_No01_2013_P_11-20.pdf """ + # (1) First we generate the required scales on the input grayscale image - # using a bilevel filter and stack them up in `filter_response`. + # using a bi-level filter and stack them up in `filter_response`. # (2) We then perform Non-Maximal suppression in 3 x 3 x 3 window on the # filter_response to suppress points that are neither minima or maxima in # 3 x 3 x 3 neighbourhood. We obtain a boolean ndarray `feature_mask` @@ -207,15 +207,15 @@ def keypoints_censure(image, min_scale=1, max_scale=7, mode='DoB', if mode not in ('dob', 'octagon', 'star'): raise ValueError('Mode must be one of "DoB", "Octagon", "STAR".') - if max_scale - min_scale < 2: - raise ValueError('The number of scales should be greater than or' - 'equal to 3.') + if min_scale < 1 or max_scale < 1 or max_scale - min_scale < 2: + raise ValueError('The scales must be >= 1 and the number of scales ' + 'should be >= 3.') image = img_as_float(image) image = np.ascontiguousarray(image) # Generating all the scales - filter_response = _get_filtered_image(image, min_scale, max_scale, mode) + filter_response = _filter_image(image, min_scale, max_scale, mode) # Suppressing points that are neither minima or maxima in their 3 x 3 x 3 # neighbourhood to zero From 8d6d5f7fa783ad4cd51daef42b6129f067d3bf0a Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Thu, 15 Aug 2013 14:47:10 +0530 Subject: [PATCH 484/736] Adding Octagon shapes for higher scales --- skimage/feature/censure.py | 63 +++++++++++++++++++------------------- 1 file changed, 32 insertions(+), 31 deletions(-) diff --git a/skimage/feature/censure.py b/skimage/feature/censure.py index 2dda9306..199cfbaa 100644 --- a/skimage/feature/censure.py +++ b/skimage/feature/censure.py @@ -11,25 +11,27 @@ from skimage.feature.censure_cy import _censure_dob_loop OCTAGON_OUTER_SHAPE = [(5, 2), (5, 3), (7, 3), (9, 4), (9, 7), (13, 7), - (15, 10)] -OCTAGON_INNER_SHAPE = [(3, 0), (3, 1), (3, 2), (5, 2), (5, 3), (5, 4), (5, 5)] + (15, 10), (15, 11), (15, 12), (17, 13), (17, 14)] +OCTAGON_INNER_SHAPE = [(3, 0), (3, 1), (3, 2), (5, 2), (5, 3), (5, 4), (5, 5), + (7, 5), (7, 6), (9, 6), (9, 7)] STAR_SHAPE = [1, 2, 3, 4, 6, 8, 11, 12, 16, 22, 23, 32, 45, 46, 64, 90, 128] STAR_FILTER_SHAPE = [(1, 0), (3, 1), (4, 2), (5, 3), (7, 4), (8, 5), (9, 6), (11, 8), (13, 10), (14, 11), (15, 12), (16, 14)] -def _filter_image(image, min_scale, max_scale, mode): +def _get_filtered_image(image, min_scale, max_scale, mode): - response = np.zeros((image.shape[0], image.shape[1], - max_scale - min_scale + 1), dtype=np.double) + scales = np.zeros((image.shape[0], image.shape[1], + max_scale - min_scale + 1), dtype=np.double) if mode == 'dob': - # make response[:, :, i] contiguous memory block - item_size = response.itemsize - response.strides = (item_size * response.shape[0], item_size, - item_size * response.shape[0] * response.shape[1]) + # make scales[:, :, i] contiguous memory block + item_size = scales.itemsize + scales.strides = (item_size * scales.shape[0], + item_size, + item_size * scales.shape[0] * scales.shape[1]) integral_img = integral_image(image) @@ -37,12 +39,12 @@ def _filter_image(image, min_scale, max_scale, mode): n = min_scale + i # Constant multipliers for the outer region and the inner region - # of the bi-level filters with the constraint of keeping the + # of the bilevel filters with the constraint of keeping the # DC bias 0. inner_weight = (1.0 / (2 * n + 1)**2) outer_weight = (1.0 / (12 * n**2 + 4 * n)) - _censure_dob_loop(n, integral_img, response[:, :, i], + _censure_dob_loop(n, integral_img, scales[:, :, i], inner_weight, outer_weight) # NOTE : For the Octagon shaped filter, we implemented and evaluated the @@ -56,17 +58,17 @@ def _filter_image(image, min_scale, max_scale, mode): for i in range(max_scale - min_scale + 1): mo, no = OCTAGON_OUTER_SHAPE[min_scale + i - 1] mi, ni = OCTAGON_INNER_SHAPE[min_scale + i - 1] - response[:, :, i] = convolve(image, - _octagon_filter_kernel(mo, no, mi, ni)) - + scales[:, :, i] = convolve(image, + _octagon_filter_kernel(mo, no, mi, ni)) elif mode == 'star': for i in range(max_scale - min_scale + 1): m = STAR_SHAPE[STAR_FILTER_SHAPE[min_scale + i - 1][0]] n = STAR_SHAPE[STAR_FILTER_SHAPE[min_scale + i - 1][1]] - response[:, :, i] = convolve(image, _star_filter_kernel(m, n)) + scales[:, :, i] = convolve(image, + _star_filter_kernel(m, n)) - return response + return scales # TODO : Import from selem after getting #669 merged. @@ -137,24 +139,24 @@ def _suppress_lines(feature_mask, image, sigma, line_threshold): def keypoints_censure(image, min_scale=1, max_scale=7, mode='DoB', non_max_threshold=0.15, line_threshold=10): """ - Extracts CenSurE keypoints along with the corresponding scale using - either Difference of Boxes, Octagon or STAR bi-level filter. + Extracts Censure keypoints along with the corresponding scale using + either Difference of Boxes, Octagon or STAR bilevel filter. Parameters ---------- image : 2D ndarray Input image. - min_scale : int + min_scale : positive integer Minimum scale to extract keypoints from. - max_scale : int + max_scale : positive integer Maximum scale to extract keypoints from. The keypoints will be extracted from all the scales except the first and the last i.e. from the scales in the range [min_scale + 1, max_scale - 1]. - mode : {'DoB', 'Octagon', 'STAR'} - Type of bi-level filter used to get the scales of the input image. + mode : ('DoB', 'Octagon', 'STAR') + Type of bilevel filter used to get the scales of the input image. Possible values are 'DoB', 'Octagon' and 'STAR'. The three modes - represent the shape of the bi-level filters i.e. box(square), octagon - and star respectively. For instance, a bi-level octagon filter consists + represent the shape of the bilevel filters i.e. box(square), octagon + and star respectively. For instance, a bilevel octagon filter consists of a smaller inner octagon and a larger outer octagon with the filter weights being uniformly negative in both the inner octagon while uniformly positive in the difference region. Use STAR and Octagon for @@ -169,7 +171,7 @@ def keypoints_censure(image, min_scale=1, max_scale=7, mode='DoB', Returns ------- keypoints : (N, 2) array - Location of the extracted keypoints in the ``(row, col)`` format. + Location of the extracted keypoints in the (row, col) format. scales : (N, 1) array The corresponding scale of the N extracted keypoints. @@ -186,9 +188,8 @@ def keypoints_censure(image, min_scale=1, max_scale=7, mode='DoB', http://www.jamris.org/01_2013/saveas.php?QUEST=JAMRIS_No01_2013_P_11-20.pdf """ - # (1) First we generate the required scales on the input grayscale image - # using a bi-level filter and stack them up in `filter_response`. + # using a bilevel filter and stack them up in `filter_response`. # (2) We then perform Non-Maximal suppression in 3 x 3 x 3 window on the # filter_response to suppress points that are neither minima or maxima in # 3 x 3 x 3 neighbourhood. We obtain a boolean ndarray `feature_mask` @@ -207,15 +208,15 @@ def keypoints_censure(image, min_scale=1, max_scale=7, mode='DoB', if mode not in ('dob', 'octagon', 'star'): raise ValueError('Mode must be one of "DoB", "Octagon", "STAR".') - if min_scale < 1 or max_scale < 1 or max_scale - min_scale < 2: - raise ValueError('The scales must be >= 1 and the number of scales ' - 'should be >= 3.') + if max_scale - min_scale < 2: + raise ValueError('The number of scales should be greater than or' + 'equal to 3.') image = img_as_float(image) image = np.ascontiguousarray(image) # Generating all the scales - filter_response = _filter_image(image, min_scale, max_scale, mode) + filter_response = _get_filtered_image(image, min_scale, max_scale, mode) # Suppressing points that are neither minima or maxima in their 3 x 3 x 3 # neighbourhood to zero From cd5316c36de726fa8b7b9a2a46153fe94f6ea8a9 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Thu, 15 Aug 2013 15:30:08 +0530 Subject: [PATCH 485/736] Importing star and octagon from morphplogy.selem --- skimage/feature/censure.py | 43 +++++--------------------------------- 1 file changed, 5 insertions(+), 38 deletions(-) diff --git a/skimage/feature/censure.py b/skimage/feature/censure.py index 199cfbaa..a15195a2 100644 --- a/skimage/feature/censure.py +++ b/skimage/feature/censure.py @@ -4,7 +4,7 @@ from scipy.ndimage.filters import maximum_filter, minimum_filter, convolve from skimage.transform import integral_image from skimage.feature.corner import _compute_auto_correlation from skimage.util import img_as_float -from skimage.morphology import convex_hull_image +from skimage.morphology import convex_hull_image, octagon, star from skimage.feature.util import _mask_border_keypoints from skimage.feature.censure_cy import _censure_dob_loop @@ -71,58 +71,25 @@ def _get_filtered_image(image, min_scale, max_scale, mode): return scales -# TODO : Import from selem after getting #669 merged. -def _oct(m, n): - f = np.zeros((m + 2*n, m + 2*n)) - f[0, n] = 1 - f[n, 0] = 1 - f[0, m + n - 1] = 1 - f[m + n - 1, 0] = 1 - f[-1, n] = 1 - f[n, -1] = 1 - f[-1, m + n - 1] = 1 - f[m + n - 1, -1] = 1 - return convex_hull_image(f).astype(int) - - def _octagon_filter_kernel(mo, no, mi, ni): outer = (mo + 2 * no)**2 - 2 * no * (no + 1) inner = (mi + 2 * ni)**2 - 2 * ni * (ni + 1) outer_weight = 1.0 / (outer - inner) inner_weight = 1.0 / inner c = ((mo + 2 * no) - (mi + 2 * ni)) // 2 - outer_oct = _oct(mo, no) + outer_oct = octagon(mo, no) inner_oct = np.zeros((mo + 2 * no, mo + 2 * no)) - inner_oct[c: -c, c: -c] = _oct(mi, ni) + inner_oct[c: -c, c: -c] = octagon(mi, ni) bfilter = (outer_weight * outer_oct - (outer_weight + inner_weight) * inner_oct) return bfilter -def _star(a): - if a == 1: - bfilter = np.zeros((3, 3)) - bfilter[:] = 1 - return bfilter - m = 2 * a + 1 - n = a // 2 - selem_square = np.zeros((m + 2 * n, m + 2 * n), dtype=np.uint8) - selem_square[n: m + n, n: m + n] = 1 - selem_triangle = np.zeros((m + 2 * n, m + 2 * n), dtype=np.uint8) - selem_triangle[(m + 2 * n - 1) // 2, 0] = 1 - selem_triangle[(m + 1) // 2, n - 1] = 1 - selem_triangle[(m + 4 * n - 3) // 2, n - 1] = 1 - selem_triangle = convex_hull_image(selem_triangle).astype(int) - selem_triangle += (selem_triangle[:, ::-1] + selem_triangle.T + - selem_triangle.T[::-1, :]) - return selem_square + selem_triangle - - def _star_filter_kernel(m, n): c = m + m // 2 - n - n // 2 - outer_star = _star(m) + outer_star = star(m) inner_star = np.zeros_like(outer_star) - inner_star[c: -c, c: -c] = _star(n) + inner_star[c: -c, c: -c] = star(n) outer_weight = 1.0 / (np.sum(outer_star - inner_star)) inner_weight = 1.0 / np.sum(inner_star) bfilter = (outer_weight * outer_star - From ffbeeaee27207b924f5e966e45cc202d4b9a2d70 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Thu, 15 Aug 2013 16:37:07 +0530 Subject: [PATCH 486/736] Correcting selem.star for Python 3 --- skimage/morphology/selem.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skimage/morphology/selem.py b/skimage/morphology/selem.py index f85b3a95..4df52e94 100644 --- a/skimage/morphology/selem.py +++ b/skimage/morphology/selem.py @@ -280,10 +280,10 @@ def star(a, dtype=np.uint8): bfilter[:] = 1 return bfilter m = 2 * a + 1 - n = a / 2 + n = a // 2 selem_square = np.zeros((m + 2 * n, m + 2 * n)) selem_square[n: m + n, n: m + n] = 1 - c = (m + 2 * n - 1) / 2 + c = (m + 2 * n - 1) // 2 selem_rotated = np.zeros((m + 2 * n, m + 2 * n)) selem_rotated[0, c] = selem_rotated[-1, c] = selem_rotated[c, 0] = selem_rotated[c, -1] = 1 selem_rotated = convex_hull_image(selem_rotated).astype(int) From 56686fe809aca16ce4a02d7238fd68b773543d04 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Thu, 15 Aug 2013 17:28:08 +0530 Subject: [PATCH 487/736] Reverting back to changes made by Johannes --- skimage/feature/censure.py | 60 +++++++++++++++++++------------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/skimage/feature/censure.py b/skimage/feature/censure.py index a15195a2..dee2c81c 100644 --- a/skimage/feature/censure.py +++ b/skimage/feature/censure.py @@ -4,7 +4,7 @@ from scipy.ndimage.filters import maximum_filter, minimum_filter, convolve from skimage.transform import integral_image from skimage.feature.corner import _compute_auto_correlation from skimage.util import img_as_float -from skimage.morphology import convex_hull_image, octagon, star +from skimage.morphology import octagon, star from skimage.feature.util import _mask_border_keypoints from skimage.feature.censure_cy import _censure_dob_loop @@ -20,18 +20,17 @@ STAR_FILTER_SHAPE = [(1, 0), (3, 1), (4, 2), (5, 3), (7, 4), (8, 5), (9, 6), (11, 8), (13, 10), (14, 11), (15, 12), (16, 14)] -def _get_filtered_image(image, min_scale, max_scale, mode): +def _filter_image(image, min_scale, max_scale, mode): - scales = np.zeros((image.shape[0], image.shape[1], - max_scale - min_scale + 1), dtype=np.double) + response = np.zeros((image.shape[0], image.shape[1], + max_scale - min_scale + 1), dtype=np.double) if mode == 'dob': - # make scales[:, :, i] contiguous memory block - item_size = scales.itemsize - scales.strides = (item_size * scales.shape[0], - item_size, - item_size * scales.shape[0] * scales.shape[1]) + # make response[:, :, i] contiguous memory block + item_size = response.itemsize + response.strides = (item_size * response.shape[0], item_size, + item_size * response.shape[0] * response.shape[1]) integral_img = integral_image(image) @@ -39,12 +38,12 @@ def _get_filtered_image(image, min_scale, max_scale, mode): n = min_scale + i # Constant multipliers for the outer region and the inner region - # of the bilevel filters with the constraint of keeping the + # of the bi-level filters with the constraint of keeping the # DC bias 0. inner_weight = (1.0 / (2 * n + 1)**2) outer_weight = (1.0 / (12 * n**2 + 4 * n)) - _censure_dob_loop(n, integral_img, scales[:, :, i], + _censure_dob_loop(n, integral_img, response[:, :, i], inner_weight, outer_weight) # NOTE : For the Octagon shaped filter, we implemented and evaluated the @@ -58,17 +57,17 @@ def _get_filtered_image(image, min_scale, max_scale, mode): for i in range(max_scale - min_scale + 1): mo, no = OCTAGON_OUTER_SHAPE[min_scale + i - 1] mi, ni = OCTAGON_INNER_SHAPE[min_scale + i - 1] - scales[:, :, i] = convolve(image, - _octagon_filter_kernel(mo, no, mi, ni)) + response[:, :, i] = convolve(image, + _octagon_filter_kernel(mo, no, mi, ni)) + elif mode == 'star': for i in range(max_scale - min_scale + 1): m = STAR_SHAPE[STAR_FILTER_SHAPE[min_scale + i - 1][0]] n = STAR_SHAPE[STAR_FILTER_SHAPE[min_scale + i - 1][1]] - scales[:, :, i] = convolve(image, - _star_filter_kernel(m, n)) + response[:, :, i] = convolve(image, _star_filter_kernel(m, n)) - return scales + return response def _octagon_filter_kernel(mo, no, mi, ni): @@ -106,24 +105,24 @@ def _suppress_lines(feature_mask, image, sigma, line_threshold): def keypoints_censure(image, min_scale=1, max_scale=7, mode='DoB', non_max_threshold=0.15, line_threshold=10): """ - Extracts Censure keypoints along with the corresponding scale using - either Difference of Boxes, Octagon or STAR bilevel filter. + Extracts CenSurE keypoints along with the corresponding scale using + either Difference of Boxes, Octagon or STAR bi-level filter. Parameters ---------- image : 2D ndarray Input image. - min_scale : positive integer + min_scale : int Minimum scale to extract keypoints from. - max_scale : positive integer + max_scale : int Maximum scale to extract keypoints from. The keypoints will be extracted from all the scales except the first and the last i.e. from the scales in the range [min_scale + 1, max_scale - 1]. - mode : ('DoB', 'Octagon', 'STAR') - Type of bilevel filter used to get the scales of the input image. + mode : {'DoB', 'Octagon', 'STAR'} + Type of bi-level filter used to get the scales of the input image. Possible values are 'DoB', 'Octagon' and 'STAR'. The three modes - represent the shape of the bilevel filters i.e. box(square), octagon - and star respectively. For instance, a bilevel octagon filter consists + represent the shape of the bi-level filters i.e. box(square), octagon + and star respectively. For instance, a bi-level octagon filter consists of a smaller inner octagon and a larger outer octagon with the filter weights being uniformly negative in both the inner octagon while uniformly positive in the difference region. Use STAR and Octagon for @@ -138,7 +137,7 @@ def keypoints_censure(image, min_scale=1, max_scale=7, mode='DoB', Returns ------- keypoints : (N, 2) array - Location of the extracted keypoints in the (row, col) format. + Location of the extracted keypoints in the ``(row, col)`` format. scales : (N, 1) array The corresponding scale of the N extracted keypoints. @@ -155,8 +154,9 @@ def keypoints_censure(image, min_scale=1, max_scale=7, mode='DoB', http://www.jamris.org/01_2013/saveas.php?QUEST=JAMRIS_No01_2013_P_11-20.pdf """ + # (1) First we generate the required scales on the input grayscale image - # using a bilevel filter and stack them up in `filter_response`. + # using a bi-level filter and stack them up in `filter_response`. # (2) We then perform Non-Maximal suppression in 3 x 3 x 3 window on the # filter_response to suppress points that are neither minima or maxima in # 3 x 3 x 3 neighbourhood. We obtain a boolean ndarray `feature_mask` @@ -175,15 +175,15 @@ def keypoints_censure(image, min_scale=1, max_scale=7, mode='DoB', if mode not in ('dob', 'octagon', 'star'): raise ValueError('Mode must be one of "DoB", "Octagon", "STAR".') - if max_scale - min_scale < 2: - raise ValueError('The number of scales should be greater than or' - 'equal to 3.') + if min_scale < 1 or max_scale < 1 or max_scale - min_scale < 2: + raise ValueError('The scales must be >= 1 and the number of scales ' + 'should be >= 3.') image = img_as_float(image) image = np.ascontiguousarray(image) # Generating all the scales - filter_response = _get_filtered_image(image, min_scale, max_scale, mode) + filter_response = _filter_image(image, min_scale, max_scale, mode) # Suppressing points that are neither minima or maxima in their 3 x 3 x 3 # neighbourhood to zero From 550dfce134083ada14d23dd9ccf3baf87a1b75c5 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Thu, 15 Aug 2013 18:20:03 +0530 Subject: [PATCH 488/736] Final changes in censure example --- doc/examples/plot_censure_keypoints.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/doc/examples/plot_censure_keypoints.py b/doc/examples/plot_censure_keypoints.py index 95fa0e58..eb6f2a16 100644 --- a/doc/examples/plot_censure_keypoints.py +++ b/doc/examples/plot_censure_keypoints.py @@ -23,7 +23,7 @@ non_max_threshold = 0.15 line_threshold = 10 -f, ax = plt.subplots(nrows=(max_scale - min_scale - 1), ncols=3, +_, ax = plt.subplots(nrows=(max_scale - min_scale - 1), ncols=3, figsize=(6, 6)) plt.subplots_adjust(wspace=0.02, hspace=0.02, top=0.94, bottom=0.02, left=0.06, right=0.98) @@ -39,10 +39,9 @@ for col, mode in enumerate(['dob', 'octagon', 'star']): # Plotting Censure features at all the scales for row, scale in enumerate(range(min_scale + 1, max_scale)): - keypoints_i = keypoints[scales == scale] - num = len(keypoints_i) - x = keypoints_i[:, 1] - y = keypoints_i[:, 0] + mask = scales == scale + x = keypoints[mask][:, 1] + y = keypoints[mask][:, 0] s = 0.5 * 2 ** (scale + min_scale + 1) ax[row, col].imshow(img) ax[row, col].scatter(x, y, s, facecolors='none', edgecolors='b') From 9ad7c78c8e8cea70a8c27c66e5292e281f0d2d16 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Thu, 15 Aug 2013 19:14:47 +0530 Subject: [PATCH 489/736] Documenting the choice of filter sizes --- doc/examples/plot_censure_keypoints.py | 4 ++-- skimage/feature/censure.py | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/doc/examples/plot_censure_keypoints.py b/doc/examples/plot_censure_keypoints.py index eb6f2a16..fbba4e1b 100644 --- a/doc/examples/plot_censure_keypoints.py +++ b/doc/examples/plot_censure_keypoints.py @@ -40,8 +40,8 @@ for col, mode in enumerate(['dob', 'octagon', 'star']): # Plotting Censure features at all the scales for row, scale in enumerate(range(min_scale + 1, max_scale)): mask = scales == scale - x = keypoints[mask][:, 1] - y = keypoints[mask][:, 0] + x = keypoints[mask, 1] + y = keypoints[mask, 0] s = 0.5 * 2 ** (scale + min_scale + 1) ax[row, col].imshow(img) ax[row, col].scatter(x, y, s, facecolors='none', edgecolors='b') diff --git a/skimage/feature/censure.py b/skimage/feature/censure.py index dee2c81c..00be16a8 100644 --- a/skimage/feature/censure.py +++ b/skimage/feature/censure.py @@ -10,11 +10,18 @@ from skimage.feature.util import _mask_border_keypoints from skimage.feature.censure_cy import _censure_dob_loop +# The paper(Reference [1]) mentions the sizes of the Octagon shaped filter +# kernel for the first seven scales only. The sizes of the later scales +# have been extrapolated based on the following statement in the paper. +# "These octagons scale linearly and were experimentally chosen to correspond +# to the seven DOBs described in the previous section." OCTAGON_OUTER_SHAPE = [(5, 2), (5, 3), (7, 3), (9, 4), (9, 7), (13, 7), (15, 10), (15, 11), (15, 12), (17, 13), (17, 14)] OCTAGON_INNER_SHAPE = [(3, 0), (3, 1), (3, 2), (5, 2), (5, 3), (5, 4), (5, 5), (7, 5), (7, 6), (9, 6), (9, 7)] +# The sizes for the STAR shaped filter kernel for different scales have been +# taken from the OpenCV implementation. STAR_SHAPE = [1, 2, 3, 4, 6, 8, 11, 12, 16, 22, 23, 32, 45, 46, 64, 90, 128] STAR_FILTER_SHAPE = [(1, 0), (3, 1), (4, 2), (5, 3), (7, 4), (8, 5), (9, 6), (11, 8), (13, 10), (14, 11), (15, 12), (16, 14)] From e5fcece9dce1cd570446dddaf3b93f0985e958a0 Mon Sep 17 00:00:00 2001 From: Matt Terry Date: Thu, 15 Aug 2013 09:15:39 -0700 Subject: [PATCH 490/736] spell ciede correctly --- skimage/color/tests/test_delta_e.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/color/tests/test_delta_e.py b/skimage/color/tests/test_delta_e.py index 52799c9d..84f13b48 100644 --- a/skimage/color/tests/test_delta_e.py +++ b/skimage/color/tests/test_delta_e.py @@ -144,7 +144,7 @@ def test_single_color_cie76(): deltaE_cie76(lab1, lab2) -def test_single_color_cidede94(): +def test_single_color_ciede94(): lab1 = (0.5, 0.5, 0.5) lab2 = (0.4, 0.4, 0.4) deltaE_ciede94(lab1, lab2) From 0182f7c98a7420e90a7b7da62eedf76f2243c731 Mon Sep 17 00:00:00 2001 From: Matt Terry Date: Thu, 15 Aug 2013 11:18:28 -0700 Subject: [PATCH 491/736] better roundoff handing of dH --- skimage/color/delta_e.py | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/skimage/color/delta_e.py b/skimage/color/delta_e.py index bf9e316e..43b4a7b2 100644 --- a/skimage/color/delta_e.py +++ b/skimage/color/delta_e.py @@ -108,7 +108,7 @@ def deltaE_ciede94(lab1, lab2, kH=1, kC=1, kL=1, k1=0.045, k2=0.015): dL = L1 - L2 dC = C1 - C2 - dH = np.sqrt(deltaE_cie76(lab1, lab2) ** 2 - dL ** 2 - dC ** 2) + dH = get_dH(lab1, lab2) SL = 1 SC = 1 + k1 * C1 @@ -289,7 +289,7 @@ def deltaE_cmc(lab1, lab2, kL=1, kC=1): dC = C1 - C2 dL = L1 - L2 - dH = np.sqrt(deltaE_cie76(lab1, lab2) ** 2 - dL ** 2 - dC ** 2) + dH = get_dH(lab1, lab2) T = np.where(np.logical_and(np.rad2deg(h1) >= 164, np.rad2deg(h1) <= 345), 0.56 + 0.2 * np.abs(np.cos(h1 + np.deg2rad(168))), @@ -306,3 +306,27 @@ def deltaE_cmc(lab1, lab2, kL=1, kC=1): dE2 += (dC / (kC * SC)) ** 2 dE2 += (dH / SH) ** 2 return np.sqrt(dE2) + + +def get_dH(lab1, lab2): + """numerically well behaved calculation of dH + + Given a1, b1, a2, b2 + c1 = sqrt(a1**2 + b1**2) + c2 = sqrt(a2**2 + b2**2) + term = (a1-a2)**2 + (b1-b2)**2 - (c1-c2)**2 + dH = sqrt(term) + """ + lab1 = np.asarray(lab1) + lab2 = np.asarray(lab2) + + r1 = np.rollaxis(lab1, -1)[1:3] + r2 = np.rollaxis(lab2, -1)[1:3] + + C1 = np.hypot(r1[0], r1[1]) + C2 = np.hypot(r2[0], r2[1]) + + term = C1 * C2 + term -= (r1 * r2).sum(0) + term *= 2 + return np.sqrt(term) From 5afb53a1260bb33611270db65ce8eae098bef2b0 Mon Sep 17 00:00:00 2001 From: Marianne Corvellec Date: Fri, 16 Aug 2013 18:47:19 -0400 Subject: [PATCH 492/736] Edited docstring according to comments. --- skimage/data/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skimage/data/__init__.py b/skimage/data/__init__.py index c3d9d5d8..cea2a9f5 100644 --- a/skimage/data/__init__.py +++ b/skimage/data/__init__.py @@ -191,8 +191,8 @@ def coffee(): """Coffee cup. This photograph is courtesy of Pikolo Espresso Bar. - It shows several shapes (including an ellipse). It may be used to - illustrate ellipse detection (using the Hough transform). + It contains several elliptical shapes as well as varying texture (smooth + porcelain to course wood grain). Notes ----- From 752e3958350cd5c7a8f51ee2f285e2faf53319a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sat, 17 Aug 2013 15:48:48 +0200 Subject: [PATCH 493/736] Improve moments code and use typed memoryviews --- skimage/measure/_moments.pyx | 50 +++++++++++++++++---------------- skimage/measure/_regionprops.py | 2 +- 2 files changed, 27 insertions(+), 25 deletions(-) diff --git a/skimage/measure/_moments.pyx b/skimage/measure/_moments.pyx index 145f6052..f58f9d63 100644 --- a/skimage/measure/_moments.pyx +++ b/skimage/measure/_moments.pyx @@ -7,50 +7,52 @@ import numpy as np cimport numpy as cnp -def central_moments(cnp.ndarray[cnp.double_t, ndim=2] array, double cr, - double cc, int order): +def moments(double[:, :] image, Py_ssize_t order=3): + return central_moments(image, 0, 0, order) + + +def central_moments(double[:, :] image, double cr, double cc, + Py_ssize_t order=3): cdef Py_ssize_t p, q, r, c - cdef cnp.ndarray[cnp.double_t, ndim=2] mu - mu = np.zeros((order + 1, order + 1), 'double') + cdef double[:, ::1] mu = np.zeros((order + 1, order + 1), dtype=np.double) for p in range(order + 1): for q in range(order + 1): - for r in range(array.shape[0]): - for c in range(array.shape[1]): - mu[p,q] += array[r,c] * (r - cr) ** q * (c - cc) ** p - return mu + for r in range(image.shape[0]): + for c in range(image.shape[1]): + mu[p, q] += image[r, c] * (r - cr) ** q * (c - cc) ** p + return np.asarray(mu) -def normalized_moments(cnp.ndarray[cnp.double_t, ndim=2] mu, int order): +def normalized_moments(double[:, :] mu, Py_ssize_t order=3): cdef Py_ssize_t p, q - cdef cnp.ndarray[cnp.double_t, ndim=2] nu - nu = np.zeros((order + 1, order + 1), 'double') + cdef double[:, ::1] nu = np.zeros((order + 1, order + 1), dtype=np.double) for p in range(order + 1): for q in range(order + 1): if p + q >= 2: - nu[p,q] = mu[p,q] / mu[0,0]**((p + q) / 2 + 1) + nu[p,q] = mu[p, q] / mu[0, 0] ** ((p + q) / 2 + 1) else: nu[p,q] = np.nan - return nu + return np.asarray(nu) -def hu_moments(cnp.ndarray[cnp.double_t, ndim=2] nu): - cdef cnp.ndarray[cnp.double_t, ndim=1] hu = np.zeros((7,), 'double') - cdef double t0 = nu[3,0] + nu[1,2] - cdef double t1 = nu[2,1] + nu[0,3] +def hu_moments(double[:, :] nu): + cdef double[::1] hu = np.zeros((7, ), dtype=np.double) + cdef double t0 = nu[3, 0] + nu[1, 2] + cdef double t1 = nu[2, 1] + nu[0, 3] cdef double q0 = t0 * t0 cdef double q1 = t1 * t1 - cdef double n4 = 4 * nu[1,1] - cdef double s = nu[2,0] + nu[0,2] - cdef double d = nu[2,0] - nu[0,2] + cdef double n4 = 4 * nu[1, 1] + cdef double s = nu[2, 0] + nu[0, 2] + cdef double d = nu[2, 0] - nu[0, 2] hu[0] = s - hu[1] = d * d + n4 * nu[1,1] + hu[1] = d * d + n4 * nu[1, 1] hu[3] = q0 + q1 hu[5] = d * (q0 - q1) + n4 * t0 * t1 t0 *= q0 - 3 * q1 t1 *= 3 * q0 - q1 - q0 = nu[3,0]- 3 * nu[1,2] - q1 = 3 * nu[2,1] - nu[0,3] + q0 = nu[3, 0]- 3 * nu[1, 2] + q1 = 3 * nu[2, 1] - nu[0, 3] hu[2] = q0 * q0 + q1 * q1 hu[4] = q0 * t0 + q1 * t1 hu[6] = q1 * t0 - q0 * t1 - return hu + return np.asarray(hu) diff --git a/skimage/measure/_regionprops.py b/skimage/measure/_regionprops.py index e3d02bf3..dc9ff644 100644 --- a/skimage/measure/_regionprops.py +++ b/skimage/measure/_regionprops.py @@ -216,7 +216,7 @@ class _RegionProperties(object): @_cached_property def moments(self): - return _moments.central_moments(self._image_double, 0, 0, 3) + return _moments.moments(self._image_double, 3) @_cached_property def local_centroid(self): From 13957123b7824b6520bb3887dc845082af26d413 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sat, 17 Aug 2013 18:00:53 +0200 Subject: [PATCH 494/736] Make moments functions public, rename, add tests --- skimage/measure/__init__.py | 7 +- skimage/measure/_moments.pyx | 135 +++++++++++++++++++++- skimage/measure/_regionprops.py | 46 ++++---- skimage/measure/tests/test_regionprops.py | 24 ++-- 4 files changed, 170 insertions(+), 42 deletions(-) diff --git a/skimage/measure/__init__.py b/skimage/measure/__init__.py index 423aa9a7..616071d3 100755 --- a/skimage/measure/__init__.py +++ b/skimage/measure/__init__.py @@ -2,6 +2,7 @@ from .find_contours import find_contours from ._regionprops import regionprops, perimeter from ._structural_similarity import structural_similarity from ._polygon import approximate_polygon, subdivide_polygon +from ._moments import moments, moments_central, moments_normalized, moments_hu from .fit import LineModel, CircleModel, EllipseModel, ransac from .block import block_reduce @@ -16,4 +17,8 @@ __all__ = ['find_contours', 'CircleModel', 'EllipseModel', 'ransac', - 'block_reduce'] + 'block_reduce', + 'moments', + 'moments_central', + 'moments_normalized', + 'moments_hu'] diff --git a/skimage/measure/_moments.pyx b/skimage/measure/_moments.pyx index f58f9d63..104acf8f 100644 --- a/skimage/measure/_moments.pyx +++ b/skimage/measure/_moments.pyx @@ -4,15 +4,79 @@ #cython: wraparound=False import numpy as np -cimport numpy as cnp - def moments(double[:, :] image, Py_ssize_t order=3): - return central_moments(image, 0, 0, order) + """Calculate all raw image moments up to a certain order. + + The following properties can be calculated from raw image moments: + * Area as ``m[0, 0]``. + * Centroid as {``m[0, 1] / m[0, 0]``, ``m[1, 0] / m[0, 0]``}. + + Note that raw moments are whether translation, scale nor rotation + invariant. + + Parameters + ---------- + image : 2D double array + Rasterized shape as image. + order : int, optional + Maximum order of moments. Default is 3. + + Returns + ------- + m : (``order + 1``, ``order + 1``) array + Raw image moments. + + References + ---------- + .. [1] Wilhelm Burger, Mark Burge. Principles of Digital Image Processing: + Core Algorithms. Springer-Verlag, London, 2009. + .. [2] B. Jähne. Digital Image Processing. Springer-Verlag, + Berlin-Heidelberg, 6. edition, 2005. + .. [3] T. H. Reiss. Recognizing Planar Objects Using Invariant Image + Features, from Lecture notes in computer science, p. 676. Springer, + Berlin, 1993. + .. [4] http://en.wikipedia.org/wiki/Image_moment + + """ + return moments_central(image, 0, 0, order) -def central_moments(double[:, :] image, double cr, double cc, +def moments_central(double[:, :] image, double cr, double cc, Py_ssize_t order=3): + """Calculate all central image moments up to a certain order. + + Note that central moments are translation invariant but not scale and + rotation invariant. + + Parameters + ---------- + image : 2D double array + Rasterized shape as image. + cr : double + Center row coordinate. + cc : double + Center column coordinate. + order : int, optional + Maximum order of moments. Default is 3. + + Returns + ------- + mu : (``order + 1``, ``order + 1``) array + Central image moments. + + References + ---------- + .. [1] Wilhelm Burger, Mark Burge. Principles of Digital Image Processing: + Core Algorithms. Springer-Verlag, London, 2009. + .. [2] B. Jähne. Digital Image Processing. Springer-Verlag, + Berlin-Heidelberg, 6. edition, 2005. + .. [3] T. H. Reiss. Recognizing Planar Objects Using Invariant Image + Features, from Lecture notes in computer science, p. 676. Springer, + Berlin, 1993. + .. [4] http://en.wikipedia.org/wiki/Image_moment + + """ cdef Py_ssize_t p, q, r, c cdef double[:, ::1] mu = np.zeros((order + 1, order + 1), dtype=np.double) for p in range(order + 1): @@ -23,7 +87,36 @@ def central_moments(double[:, :] image, double cr, double cc, return np.asarray(mu) -def normalized_moments(double[:, :] mu, Py_ssize_t order=3): +def moments_normalized(double[:, :] mu, Py_ssize_t order=3): + """Calculate all normalized central image moments up to a certain order. + + Note that normalized central moments are translation and scale invariant + but not rotation invariant. + + Parameters + ---------- + mu : (M, M) array + Central image moments, where M must be > ``order``. + order : int, optional + Maximum order of moments. Default is 3. + + Returns + ------- + nu : (``order + 1``, ``order + 1``) array + Normalized central image moments. + + References + ---------- + .. [1] Wilhelm Burger, Mark Burge. Principles of Digital Image Processing: + Core Algorithms. Springer-Verlag, London, 2009. + .. [2] B. Jähne. Digital Image Processing. Springer-Verlag, + Berlin-Heidelberg, 6. edition, 2005. + .. [3] T. H. Reiss. Recognizing Planar Objects Using Invariant Image + Features, from Lecture notes in computer science, p. 676. Springer, + Berlin, 1993. + .. [4] http://en.wikipedia.org/wiki/Image_moment + + """ cdef Py_ssize_t p, q cdef double[:, ::1] nu = np.zeros((order + 1, order + 1), dtype=np.double) for p in range(order + 1): @@ -35,7 +128,37 @@ def normalized_moments(double[:, :] mu, Py_ssize_t order=3): return np.asarray(nu) -def hu_moments(double[:, :] nu): +def moments_hu(double[:, :] nu): + """Calculate Hu's set of image moments. + + Note that this set of moments is proofed to be translation, scale and + rotation invariant. + + Parameters + ---------- + nu : (M, M) array + Normalized central image moments, where M must be > 4. + + Returns + ------- + nu : (7, 1) array + Hu's set of image moments. + + References + ---------- + .. [1] M. K. Hu, "Visual Pattern Recognition by Moment Invariants", + IRE Trans. Info. Theory, vol. IT-8, pp. 179-187, 1962 + .. [2] Wilhelm Burger, Mark Burge. Principles of Digital Image Processing: + Core Algorithms. Springer-Verlag, London, 2009. + .. [3] B. Jähne. Digital Image Processing. Springer-Verlag, + Berlin-Heidelberg, 6. edition, 2005. + .. [4] T. H. Reiss. Recognizing Planar Objects Using Invariant Image + Features, from Lecture notes in computer science, p. 676. Springer, + Berlin, 1993. + .. [5] http://en.wikipedia.org/wiki/Image_moment + + + """ cdef double[::1] hu = np.zeros((7, ), dtype=np.double) cdef double t0 = nu[3, 0] + nu[1, 2] cdef double t1 = nu[2, 1] + nu[0, 3] diff --git a/skimage/measure/_regionprops.py b/skimage/measure/_regionprops.py index dc9ff644..35952b48 100644 --- a/skimage/measure/_regionprops.py +++ b/skimage/measure/_regionprops.py @@ -18,7 +18,7 @@ STREL_8 = np.ones((3, 3), 'int8') PROPS = { 'Area': 'area', 'BoundingBox': 'bbox', - 'CentralMoments': 'central_moments', + 'CentralMoments': 'moments_central', 'Centroid': 'centroid', 'ConvexArea': 'convex_area', # 'ConvexHull', @@ -31,7 +31,7 @@ PROPS = { # 'Extrema', 'FilledArea': 'filled_area', 'FilledImage': 'filled_image', - 'HuMoments': 'hu_moments', + 'HuMoments': 'moments_hu', 'Image': 'image', 'Label': 'label', 'MajorAxisLength': 'major_axis_length', @@ -40,7 +40,7 @@ PROPS = { 'MinIntensity': 'min_intensity', 'MinorAxisLength': 'minor_axis_length', 'Moments': 'moments', - 'NormalizedMoments': 'normalized_moments', + 'NormalizedMoments': 'moments_normalized', 'Orientation': 'orientation', 'Perimeter': 'perimeter', # 'PixelIdxList', @@ -49,9 +49,9 @@ PROPS = { # 'SubarrayIdx' 'WeightedCentralMoments': 'weighted_central_moments', 'WeightedCentroid': 'weighted_centroid', - 'WeightedHuMoments': 'weighted_hu_moments', + 'WeightedHuMoments': 'weighted_moments_hu', 'WeightedMoments': 'weighted_moments', - 'WeightedNormalizedMoments': 'weighted_normalized_moments' + 'WeightedNormalizedMoments': 'weighted_moments_normalized' } @@ -128,9 +128,9 @@ class _RegionProperties(object): return row + self._slice[0].start, col + self._slice[1].start @_cached_property - def central_moments(self): + def moments_central(self): row, col = self.local_centroid - return _moments.central_moments(self._image_double, row, col, 3) + return _moments.moments_central(self._image_double, row, col, 3) @_cached_property def convex_area(self): @@ -177,8 +177,8 @@ class _RegionProperties(object): return ndimage.binary_fill_holes(self.image, STREL_8) @_cached_property - def hu_moments(self): - return _moments.hu_moments(self.normalized_moments) + def moments_hu(self): + return _moments.moments_hu(self.moments_normalized) @_cached_property def image(self): @@ -190,7 +190,7 @@ class _RegionProperties(object): @_cached_property def inertia_tensor(self): - mu = self.central_moments + mu = self.moments_central a = mu[2, 0] / mu[0, 0] b = -mu[1, 1] / mu[0, 0] c = mu[0, 2] / mu[0, 0] @@ -248,8 +248,8 @@ class _RegionProperties(object): return 4 * sqrt(l2) @_cached_property - def normalized_moments(self): - return _moments.normalized_moments(self.central_moments, 3) + def moments_normalized(self): + return _moments.moments_normalized(self.moments_central, 3) @_cached_property def orientation(self): @@ -274,7 +274,7 @@ class _RegionProperties(object): @_cached_property def weighted_central_moments(self): row, col = self.weighted_local_centroid - return _moments.central_moments(self._intensity_image_double, + return _moments.moments_central(self._intensity_image_double, row, col, 3) @_cached_property @@ -290,16 +290,16 @@ class _RegionProperties(object): return row, col @_cached_property - def weighted_hu_moments(self): - return _moments.hu_moments(self.weighted_normalized_moments) + def weighted_moments_hu(self): + return _moments.moments_hu(self.weighted_moments_normalized) @_cached_property def weighted_moments(self): - return _moments.central_moments(self._intensity_image_double, 0, 0, 3) + return _moments.moments_central(self._intensity_image_double, 0, 0, 3) @_cached_property - def weighted_normalized_moments(self): - return _moments.normalized_moments(self.weighted_central_moments, 3) + def weighted_moments_normalized(self): + return _moments.moments_normalized(self.weighted_central_moments, 3) def __getitem__(self, key): value = getattr(self, key, None) @@ -347,7 +347,7 @@ def regionprops(label_image, properties=None, Number of pixels of region. **bbox** : tuple Bounding box `(min_row, min_col, max_row, max_col)` - **central_moments** : (3, 3) ndarray + **moments_central** : (3, 3) ndarray Central moments (translation invariant) up to 3rd order:: mu_ji = sum{ array(x, y) * (x - x_c)^j * (y - y_c)^i } @@ -379,7 +379,7 @@ def regionprops(label_image, properties=None, **filled_image** : (H, J) ndarray Binary region image with filled holes which has the same size as bounding box. - **hu_moments** : tuple + **moments_hu** : tuple Hu moments (translation, scale and rotation invariant). **image** : (H, J) ndarray Sliced binary region image which has the same size as bounding box. @@ -407,7 +407,7 @@ def regionprops(label_image, properties=None, m_ji = sum{ array(x, y) * x^j * y^i } where the sum is over the `x`, `y` coordinates of the region. - **normalized_moments** : (3, 3) ndarray + **moments_normalized** : (3, 3) ndarray Normalized moments (translation and scale invariant) up to 3rd order:: nu_ji = mu_ji / m_00^[(i+j)/2 + 1] @@ -433,7 +433,7 @@ def regionprops(label_image, properties=None, **weighted_centroid** : array Centroid coordinate tuple `(row, col)` weighted with intensity image. - **weighted_hu_moments** : tuple + **weighted_moments_hu** : tuple Hu moments (translation, scale and rotation invariant) of intensity image. **weighted_moments** : (3, 3) ndarray @@ -442,7 +442,7 @@ def regionprops(label_image, properties=None, wm_ji = sum{ array(x, y) * x^j * y^i } where the sum is over the `x`, `y` coordinates of the region. - **weighted_normalized_moments** : (3, 3) ndarray + **weighted_moments_normalized** : (3, 3) ndarray Normalized moments (translation and scale invariant) of intensity image up to 3rd order:: diff --git a/skimage/measure/tests/test_regionprops.py b/skimage/measure/tests/test_regionprops.py index 29da0456..da6bb421 100644 --- a/skimage/measure/tests/test_regionprops.py +++ b/skimage/measure/tests/test_regionprops.py @@ -47,8 +47,8 @@ def test_bbox(): assert_array_almost_equal(bbox, (0, 0, SAMPLE.shape[0], SAMPLE.shape[1]-1)) -def test_central_moments(): - mu = regionprops(SAMPLE)[0].central_moments +def test_moments_central(): + mu = regionprops(SAMPLE)[0].moments_central # determined with OpenCV assert_almost_equal(mu[0,2], 436.00000000000045) # different from OpenCV results, bug in OpenCV @@ -129,8 +129,8 @@ def test_extent(): assert_almost_equal(extent, 0.4) -def test_hu_moments(): - hu = regionprops(SAMPLE)[0].hu_moments +def test_moments_hu(): + hu = regionprops(SAMPLE)[0].moments_hu ref = np.array([ 3.27117627e-01, 2.63869194e-02, @@ -216,8 +216,8 @@ def test_moments(): assert_almost_equal(m[3,0], 95588.0) -def test_normalized_moments(): - nu = regionprops(SAMPLE)[0].normalized_moments +def test_moments_normalized(): + nu = regionprops(SAMPLE)[0].moments_normalized # determined with OpenCV assert_almost_equal(nu[0,2], 0.08410493827160502) assert_almost_equal(nu[1,1], -0.016846707818929982) @@ -260,9 +260,9 @@ def test_solidity(): assert_almost_equal(solidity, 0.580645161290323) -def test_weighted_central_moments(): +def test_weighted_moments(): wmu = regionprops(SAMPLE, intensity_image=INTENSITY_SAMPLE - )[0].weighted_central_moments + )[0].weighted_moments_central ref = np.array( [[ 7.4000000000e+01, -2.1316282073e-13, 4.7837837838e+02, -7.5943608473e+02], @@ -283,9 +283,9 @@ def test_weighted_centroid(): assert_array_almost_equal(centroid, (5.540540540540, 9.445945945945)) -def test_weighted_hu_moments(): +def test_weighted_moments_hu(): whu = regionprops(SAMPLE, intensity_image=INTENSITY_SAMPLE - )[0].weighted_hu_moments + )[0].weighted_moments_hu ref = np.array([ 3.1750587329e-01, 2.1417517159e-02, @@ -314,9 +314,9 @@ def test_weighted_moments(): assert_array_almost_equal(wm, ref) -def test_weighted_normalized_moments(): +def test_weighted_moments_normalized(): wnu = regionprops(SAMPLE, intensity_image=INTENSITY_SAMPLE - )[0].weighted_normalized_moments + )[0].weighted_moments_normalized ref = np.array( [[ np.nan, np.nan, 0.0873590903, -0.0161217406], [ np.nan, -0.0160405109, -0.0031421072, -0.0031376984], From c4caf8b7b8d7c16418ea02db62313c165d8b6774 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sat, 17 Aug 2013 18:04:07 +0200 Subject: [PATCH 495/736] Sort functions in alphabetical order --- skimage/measure/_regionprops.py | 82 ++++++++++++++++----------------- 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/skimage/measure/_regionprops.py b/skimage/measure/_regionprops.py index 35952b48..acf251af 100644 --- a/skimage/measure/_regionprops.py +++ b/skimage/measure/_regionprops.py @@ -127,11 +127,6 @@ class _RegionProperties(object): row, col = self.local_centroid return row + self._slice[0].start, col + self._slice[1].start - @_cached_property - def moments_central(self): - row, col = self.local_centroid - return _moments.moments_central(self._image_double, row, col, 3) - @_cached_property def convex_area(self): return np.sum(self.convex_image) @@ -176,10 +171,6 @@ class _RegionProperties(object): def filled_image(self): return ndimage.binary_fill_holes(self.image, STREL_8) - @_cached_property - def moments_hu(self): - return _moments.moments_hu(self.moments_normalized) - @_cached_property def image(self): return self._label_image[self._slice] == self.label @@ -214,10 +205,6 @@ class _RegionProperties(object): def _intensity_image_double(self): return self.intensity_image.astype(np.double) - @_cached_property - def moments(self): - return _moments.moments(self._image_double, 3) - @_cached_property def local_centroid(self): m = self.moments @@ -247,6 +234,19 @@ class _RegionProperties(object): _, l2 = self.inertia_tensor_eigvals return 4 * sqrt(l2) + @_cached_property + def moments(self): + return _moments.moments(self._image_double, 3) + + @_cached_property + def moments_central(self): + row, col = self.local_centroid + return _moments.moments_central(self._image_double, row, col, 3) + + @_cached_property + def moments_hu(self): + return _moments.moments_hu(self.moments_normalized) + @_cached_property def moments_normalized(self): return _moments.moments_normalized(self.moments_central, 3) @@ -271,12 +271,6 @@ class _RegionProperties(object): def solidity(self): return self.moments[0, 0] / np.sum(self.convex_image) - @_cached_property - def weighted_central_moments(self): - row, col = self.weighted_local_centroid - return _moments.moments_central(self._intensity_image_double, - row, col, 3) - @_cached_property def weighted_centroid(self): row, col = self.weighted_local_centroid @@ -289,14 +283,20 @@ class _RegionProperties(object): col = m[1, 0] / m[0, 0] return row, col - @_cached_property - def weighted_moments_hu(self): - return _moments.moments_hu(self.weighted_moments_normalized) - @_cached_property def weighted_moments(self): return _moments.moments_central(self._intensity_image_double, 0, 0, 3) + @_cached_property + def weighted_central_moments(self): + row, col = self.weighted_local_centroid + return _moments.moments_central(self._intensity_image_double, + row, col, 3) + + @_cached_property + def weighted_moments_hu(self): + return _moments.moments_hu(self.weighted_moments_normalized) + @_cached_property def weighted_moments_normalized(self): return _moments.moments_normalized(self.weighted_central_moments, 3) @@ -347,13 +347,6 @@ def regionprops(label_image, properties=None, Number of pixels of region. **bbox** : tuple Bounding box `(min_row, min_col, max_row, max_col)` - **moments_central** : (3, 3) ndarray - Central moments (translation invariant) up to 3rd order:: - - mu_ji = sum{ array(x, y) * (x - x_c)^j * (y - y_c)^i } - - where the sum is over the `x`, `y` coordinates of the region, - and `x_c` and `y_c` are the coordinates of the region's centroid. **centroid** : array Centroid coordinate tuple `(row, col)`. **convex_area** : int @@ -379,8 +372,6 @@ def regionprops(label_image, properties=None, **filled_image** : (H, J) ndarray Binary region image with filled holes which has the same size as bounding box. - **moments_hu** : tuple - Hu moments (translation, scale and rotation invariant). **image** : (H, J) ndarray Sliced binary region image which has the same size as bounding box. **inertia_tensor** : (2, 2) ndarray @@ -407,6 +398,15 @@ def regionprops(label_image, properties=None, m_ji = sum{ array(x, y) * x^j * y^i } where the sum is over the `x`, `y` coordinates of the region. + **moments_central** : (3, 3) ndarray + Central moments (translation invariant) up to 3rd order:: + + mu_ji = sum{ array(x, y) * (x - x_c)^j * (y - y_c)^i } + + where the sum is over the `x`, `y` coordinates of the region, + and `x_c` and `y_c` are the coordinates of the region's centroid. + **moments_hu** : tuple + Hu moments (translation, scale and rotation invariant). **moments_normalized** : (3, 3) ndarray Normalized moments (translation and scale invariant) up to 3rd order:: @@ -422,6 +422,15 @@ def regionprops(label_image, properties=None, through the centers of border pixels using a 4-connectivity. **solidity** : float Ratio of pixels in the region to pixels of the convex hull image. + **weighted_centroid** : array + Centroid coordinate tuple `(row, col)` weighted with intensity + image. + **weighted_moments** : (3, 3) ndarray + Spatial moments of intensity image up to 3rd order:: + + wm_ji = sum{ array(x, y) * x^j * y^i } + + where the sum is over the `x`, `y` coordinates of the region. **weighted_central_moments** : (3, 3) ndarray Central moments (translation invariant) of intensity image up to 3rd order:: @@ -430,18 +439,9 @@ def regionprops(label_image, properties=None, where the sum is over the `x`, `y` coordinates of the region, and `x_c` and `y_c` are the coordinates of the region's centroid. - **weighted_centroid** : array - Centroid coordinate tuple `(row, col)` weighted with intensity - image. **weighted_moments_hu** : tuple Hu moments (translation, scale and rotation invariant) of intensity image. - **weighted_moments** : (3, 3) ndarray - Spatial moments of intensity image up to 3rd order:: - - wm_ji = sum{ array(x, y) * x^j * y^i } - - where the sum is over the `x`, `y` coordinates of the region. **weighted_moments_normalized** : (3, 3) ndarray Normalized moments (translation and scale invariant) of intensity image up to 3rd order:: From ce924c72795d342605bb4409d5217fe99c807ace Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sat, 17 Aug 2013 18:04:35 +0200 Subject: [PATCH 496/736] Add test cases for moments functions --- skimage/measure/tests/test_moments.py | 72 +++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 skimage/measure/tests/test_moments.py diff --git a/skimage/measure/tests/test_moments.py b/skimage/measure/tests/test_moments.py new file mode 100644 index 00000000..0667f688 --- /dev/null +++ b/skimage/measure/tests/test_moments.py @@ -0,0 +1,72 @@ +from numpy.testing import assert_equal, assert_almost_equal +import numpy as np + +from skimage.measure import (moments, moments_central, moments_normalized, + moments_hu) + + +def test_moments(): + image = np.zeros((20, 20), dtype=np.double) + image[14, 14] = 1 + image[15, 15] = 1 + image[14, 15] = 0.5 + image[15, 14] = 0.5 + m = moments(image) + assert_equal(m[0, 0], 3) + assert_almost_equal(m[0, 1] / m[0, 0], 14.5) + assert_almost_equal(m[1, 0] / m[0, 0], 14.5) + + +def test_moments_central(): + image = np.zeros((20, 20), dtype=np.double) + image[14, 14] = 1 + image[15, 15] = 1 + image[14, 15] = 0.5 + image[15, 14] = 0.5 + mu = moments_central(image, 14.5, 14.5) + + # shift image by dx=2, dy=2 + image2 = np.zeros((20, 20), dtype=np.double) + image2[16, 16] = 1 + image2[17, 17] = 1 + image2[16, 17] = 0.5 + image2[17, 16] = 0.5 + mu2 = moments_central(image2, 14.5 + 2, 14.5 + 2) + # central moments must be translation invariant + assert_equal(mu, mu2) + + +def test_moments_normalized(): + image = np.zeros((20, 20), dtype=np.double) + image[13:17, 13:17] = 1 + mu = moments_central(image, 14.5, 14.5) + nu = moments_normalized(mu) + # shift image by dx=-3, dy=-3 and scale by 0.5 + image2 = np.zeros((20, 20), dtype=np.double) + image2[11:13, 11:13] = 1 + mu2 = moments_central(image2, 11.5, 11.5) + nu2 = moments_normalized(mu2) + # central moments must be translation and scale invariant + assert_almost_equal(nu, nu2, decimal=1) + + +def test_moments_hu(): + image = np.zeros((20, 20), dtype=np.double) + image[13:15, 13:17] = 1 + mu = moments_central(image, 13.5, 14.5) + nu = moments_normalized(mu) + hu = moments_hu(nu) + # shift image by dx=2, dy=3, scale by 0.5 and rotate by 90deg + image2 = np.zeros((20, 20), dtype=np.double) + image2[11, 11:13] = 1 + image2 = image2.T + mu2 = moments_central(image2, 11.5, 11) + nu2 = moments_normalized(mu2) + hu2 = moments_hu(nu2) + # central moments must be translation and scale invariant + assert_almost_equal(hu, hu2, decimal=1) + + +if __name__ == "__main__": + from numpy.testing import run_module_suite + run_module_suite() From 9fe1e26583f6809a1951774b5b0e3000d5e650d8 Mon Sep 17 00:00:00 2001 From: Matt Terry Date: Sat, 17 Aug 2013 11:06:00 -0700 Subject: [PATCH 497/736] more consistency in naming, better docstring --- skimage/color/delta_e.py | 47 +++++++++++++++++++++++----------------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/skimage/color/delta_e.py b/skimage/color/delta_e.py index 43b4a7b2..a7f9b775 100644 --- a/skimage/color/delta_e.py +++ b/skimage/color/delta_e.py @@ -108,7 +108,7 @@ def deltaE_ciede94(lab1, lab2, kH=1, kC=1, kL=1, k1=0.045, k2=0.015): dL = L1 - L2 dC = C1 - C2 - dH = get_dH(lab1, lab2) + dH2 = get_dH2(lab1, lab2) SL = 1 SC = 1 + k1 * C1 @@ -116,7 +116,7 @@ def deltaE_ciede94(lab1, lab2, kH=1, kC=1, kL=1, k1=0.045, k2=0.015): dE2 = (dL / (kL * SL)) ** 2 dE2 += (dC / (kC * SC)) ** 2 - dE2 += (dH / (kH * SH)) ** 2 + dE2 += dH2 / (kH * SH) ** 2 return np.sqrt(dE2) @@ -289,7 +289,7 @@ def deltaE_cmc(lab1, lab2, kL=1, kC=1): dC = C1 - C2 dL = L1 - L2 - dH = get_dH(lab1, lab2) + dH2 = get_dH2(lab1, lab2) T = np.where(np.logical_and(np.rad2deg(h1) >= 164, np.rad2deg(h1) <= 345), 0.56 + 0.2 * np.abs(np.cos(h1 + np.deg2rad(168))), @@ -304,29 +304,36 @@ def deltaE_cmc(lab1, lab2, kL=1, kC=1): dE2 = (dL / (kL * SL)) ** 2 dE2 += (dC / (kC * SC)) ** 2 - dE2 += (dH / SH) ** 2 + dE2 += dH2 / (SH ** 2) return np.sqrt(dE2) -def get_dH(lab1, lab2): - """numerically well behaved calculation of dH +def get_dH2(lab1, lab2): + """squared hue difference term occurring in deltaE_cmc and deltaE_ciede94 - Given a1, b1, a2, b2 - c1 = sqrt(a1**2 + b1**2) - c2 = sqrt(a2**2 + b2**2) - term = (a1-a2)**2 + (b1-b2)**2 - (c1-c2)**2 - dH = sqrt(term) + Despite its name "dH" is not a simple difference of hue values. We avoid + working directly with the hue value directly since differencing angles is + troublesome. The hue term is usually written as: + c1 = sqrt(a1**2 + b1**2) + c2 = sqrt(a2**2 + b2**2) + term = (a1-a2)**2 + (b1-b2)**2 - (c1-c2)**2 + dH = sqrt(term) + + However, this has poor roundoff properties when a or b is dominant. + Instead, r is a vector with elements a and b. The same dH term can be + re-written as: + |r1-r2|**2 - (|r1| - |r2|)**2 + and then simplified to: + 2*|r1|*|r2| - 2*dot(r1, r2) """ lab1 = np.asarray(lab1) lab2 = np.asarray(lab2) + a1, b1 = np.rollaxis(lab1, -1)[1:3] + a2, b2 = np.rollaxis(lab2, -1)[1:3] - r1 = np.rollaxis(lab1, -1)[1:3] - r2 = np.rollaxis(lab2, -1)[1:3] + # magnitude of (a, b) is the chroma + C1 = np.hypot(a1, b1) + C2 = np.hypot(a2, b2) - C1 = np.hypot(r1[0], r1[1]) - C2 = np.hypot(r2[0], r2[1]) - - term = C1 * C2 - term -= (r1 * r2).sum(0) - term *= 2 - return np.sqrt(term) + term = (C1 * C2) - (a1 * a2 + b1 * b2) + return 2*term From d86b624d433a7eed289db7b88ad232f564a4a653 Mon Sep 17 00:00:00 2001 From: Matt Terry Date: Sat, 17 Aug 2013 11:11:23 -0700 Subject: [PATCH 498/736] r is confusing, use ab instead --- skimage/color/delta_e.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/skimage/color/delta_e.py b/skimage/color/delta_e.py index a7f9b775..18cfca98 100644 --- a/skimage/color/delta_e.py +++ b/skimage/color/delta_e.py @@ -320,11 +320,11 @@ def get_dH2(lab1, lab2): dH = sqrt(term) However, this has poor roundoff properties when a or b is dominant. - Instead, r is a vector with elements a and b. The same dH term can be + Instead, ab is a vector with elements a and b. The same dH term can be re-written as: - |r1-r2|**2 - (|r1| - |r2|)**2 + |ab1-ab2|**2 - (|ab1| - |ab2|)**2 and then simplified to: - 2*|r1|*|r2| - 2*dot(r1, r2) + 2*|ab1|*|ab2| - 2*dot(ab1, ab2) """ lab1 = np.asarray(lab1) lab2 = np.asarray(lab2) From cc91324625fe9db28823c8994d74085815b4ed8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sat, 17 Aug 2013 21:42:27 +0200 Subject: [PATCH 499/736] Fix PEP8 issues and typos --- skimage/measure/_moments.pyx | 4 ++-- skimage/measure/_regionprops.py | 15 +++++++-------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/skimage/measure/_moments.pyx b/skimage/measure/_moments.pyx index 104acf8f..6b7197d2 100644 --- a/skimage/measure/_moments.pyx +++ b/skimage/measure/_moments.pyx @@ -122,9 +122,9 @@ def moments_normalized(double[:, :] mu, Py_ssize_t order=3): for p in range(order + 1): for q in range(order + 1): if p + q >= 2: - nu[p,q] = mu[p, q] / mu[0, 0] ** ((p + q) / 2 + 1) + nu[p, q] = mu[p, q] / mu[0, 0] ** ((p + q) / 2 + 1) else: - nu[p,q] = np.nan + nu[p, q] = np.nan return np.asarray(nu) diff --git a/skimage/measure/_regionprops.py b/skimage/measure/_regionprops.py index acf251af..0cc56635 100644 --- a/skimage/measure/_regionprops.py +++ b/skimage/measure/_regionprops.py @@ -311,7 +311,6 @@ class _RegionProperties(object): return getattr(self, PROPS[key]) - def regionprops(label_image, properties=None, intensity_image=None, cache=True): """Measure properties of labelled image regions. @@ -346,15 +345,15 @@ def regionprops(label_image, properties=None, **area** : int Number of pixels of region. **bbox** : tuple - Bounding box `(min_row, min_col, max_row, max_col)` + Bounding box ``(min_row, min_col, max_row, max_col)`` **centroid** : array - Centroid coordinate tuple `(row, col)`. + Centroid coordinate tuple ``(row, col)``. **convex_area** : int Number of pixels of convex hull image. **convex_image** : (H, J) ndarray Binary convex hull image which has the same size as bounding box. **coords** : (N, 2) ndarray - Coordinate list `(row, col)` of the region. + Coordinate list ``(row, col)`` of the region. **eccentricity** : float Eccentricity of the ellipse that has the same second-moments as the region. The eccentricity is the ratio of the distance between its @@ -366,7 +365,7 @@ def regionprops(label_image, properties=None, subtracted by number of holes (8-connectivity). **extent** : float Ratio of pixels in the region to pixels in the total bounding box. - Computed as `Area / (rows*cols)` + Computed as ``area / (rows * cols)`` **filled_area** : int Number of pixels of filled region. **filled_image** : (H, J) ndarray @@ -375,11 +374,11 @@ def regionprops(label_image, properties=None, **image** : (H, J) ndarray Sliced binary region image which has the same size as bounding box. **inertia_tensor** : (2, 2) ndarray - Inertia tensor of the region for the rotation around its masss. + Inertia tensor of the region for the rotation around its mass. **inertia_tensor_eigvals** : tuple The two eigen values of the inertia tensor in decreasing order. **label** : int - The label in the labelled input image. + The label in the labeled input image. **major_axis_length** : float The length of the major axis of the ellipse that has the same normalized second central moments as the region. @@ -423,7 +422,7 @@ def regionprops(label_image, properties=None, **solidity** : float Ratio of pixels in the region to pixels of the convex hull image. **weighted_centroid** : array - Centroid coordinate tuple `(row, col)` weighted with intensity + Centroid coordinate tuple ``(row, col)`` weighted with intensity image. **weighted_moments** : (3, 3) ndarray Spatial moments of intensity image up to 3rd order:: From cd9f3bd92e013f58de5e860979c08222481646cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sat, 17 Aug 2013 22:07:45 +0200 Subject: [PATCH 500/736] Use typed memoryviews in draw package --- skimage/draw/_draw.pyx | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/skimage/draw/_draw.pyx b/skimage/draw/_draw.pyx index e95a2366..17dee0f7 100644 --- a/skimage/draw/_draw.pyx +++ b/skimage/draw/_draw.pyx @@ -47,8 +47,6 @@ def line(Py_ssize_t y, Py_ssize_t x, Py_ssize_t y2, Py_ssize_t x2): """ - cdef cnp.ndarray[cnp.intp_t, ndim=1, mode="c"] rr, cc - cdef char steep = 0 cdef Py_ssize_t dx = abs(x2 - x) cdef Py_ssize_t dy = abs(y2 - y) @@ -69,8 +67,8 @@ def line(Py_ssize_t y, Py_ssize_t x, Py_ssize_t y2, Py_ssize_t x2): sx, sy = sy, sx d = (2 * dy) - dx - rr = np.zeros(int(dx) + 1, dtype=np.intp) - cc = np.zeros(int(dx) + 1, dtype=np.intp) + cdef Py_ssize_t[:] rr = np.zeros(int(dx) + 1, dtype=np.intp) + cdef Py_ssize_t[:] cc = np.zeros(int(dx) + 1, dtype=np.intp) for i in range(dx): if steep: @@ -149,8 +147,8 @@ def polygon(y, x, shape=None): # make contigous arrays for r, c coordinates cdef cnp.ndarray contiguous_rdata, contiguous_cdata - contiguous_rdata = np.ascontiguousarray(y, 'double') - contiguous_cdata = np.ascontiguousarray(x, 'double') + contiguous_rdata = np.ascontiguousarray(y, dtype=np.double) + contiguous_cdata = np.ascontiguousarray(x, dtype=np.double) cdef cnp.double_t* rptr = contiguous_rdata.data cdef cnp.double_t* cptr = contiguous_cdata.data From 3172f508d6f31247aada306b559b850658e2bb8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sat, 17 Aug 2013 22:35:13 +0200 Subject: [PATCH 501/736] Use typed memoryviews in feature package --- skimage/feature/_template.pyx | 8 +++---- skimage/feature/_texture.pyx | 42 ++++++++++++++++------------------- skimage/feature/corner_cy.pyx | 21 +++++------------- skimage/feature/texture.py | 2 +- 4 files changed, 30 insertions(+), 43 deletions(-) diff --git a/skimage/feature/_template.pyx b/skimage/feature/_template.pyx index 03695959..855ece23 100644 --- a/skimage/feature/_template.pyx +++ b/skimage/feature/_template.pyx @@ -50,9 +50,9 @@ from skimage.transform import integral def match_template(cnp.ndarray[float, ndim=2, mode="c"] image, cnp.ndarray[float, ndim=2, mode="c"] template): - cdef cnp.ndarray[float, ndim=2, mode="c"] corr - cdef cnp.ndarray[float, ndim=2, mode="c"] image_sat - cdef cnp.ndarray[float, ndim=2, mode="c"] image_sqr_sat + cdef float[:, ::1] corr + cdef float[:, ::1] image_sat + cdef float[:, ::1] image_sqr_sat cdef float template_mean = np.mean(template) cdef float template_ssd cdef float inv_area @@ -94,4 +94,4 @@ def match_template(cnp.ndarray[float, ndim=2, mode="c"] image, den = sqrt((window_sqr_sum - window_mean_sqr) * template_ssd) corr[r, c] /= den - return corr + return np.asarray(corr) diff --git a/skimage/feature/_texture.pyx b/skimage/feature/_texture.pyx index f98ed4ca..0e17fd1d 100644 --- a/skimage/feature/_texture.pyx +++ b/skimage/feature/_texture.pyx @@ -8,15 +8,9 @@ from libc.math cimport sin, cos, abs from skimage._shared.interpolation cimport bilinear_interpolation -def _glcm_loop(cnp.ndarray[dtype=cnp.uint8_t, ndim=2, - negative_indices=False, mode='c'] image, - cnp.ndarray[dtype=cnp.float64_t, ndim=1, - negative_indices=False, mode='c'] distances, - cnp.ndarray[dtype=cnp.float64_t, ndim=1, - negative_indices=False, mode='c'] angles, - int levels, - cnp.ndarray[dtype=cnp.uint32_t, ndim=4, - negative_indices=False, mode='c'] out): +def _glcm_loop(cnp.uint8_t[:, ::1] image, double[:] distances, + double[:] angles, Py_ssize_t levels, + cnp.uint32_t[:, :, :, ::1] out): """Perform co-occurrence matrix accumulation. Parameters @@ -81,7 +75,7 @@ cdef inline int _bit_rotate_right(int value, int length): return (value >> 1) | ((value & 1) << (length - 1)) -def _local_binary_pattern(cnp.ndarray[double, ndim=2] image, +def _local_binary_pattern(double[:, ::1] image, int P, float R, char method='D'): """Gray scale and rotation invariant LBP (Local Binary Patterns). @@ -92,8 +86,8 @@ def _local_binary_pattern(cnp.ndarray[double, ndim=2] image, image : (N, M) double array Graylevel image. P : int - Number of circularly symmetric neighbour set points (quantization of the - angular space). + Number of circularly symmetric neighbour set points (quantization of + the angular space). R : float Radius of circle (spatial resolution of the operator). method : {'D', 'R', 'U', 'V'} @@ -111,19 +105,20 @@ def _local_binary_pattern(cnp.ndarray[double, ndim=2] image, """ # texture weights - cdef cnp.ndarray[int, ndim=1] weights = 2 ** np.arange(P, dtype=np.int32) + cdef int[:] weights = 2 ** np.arange(P, dtype=np.int32) # local position of texture elements - rp = - R * np.sin(2 * np.pi * np.arange(P, dtype=np.double) / P) - cp = R * np.cos(2 * np.pi * np.arange(P, dtype=np.double) / P) - cdef cnp.ndarray[double, ndim=2] coords = np.round(np.vstack([rp, cp]).T, 5) + rr = - R * np.sin(2 * np.pi * np.arange(P, dtype=np.double) / P) + cc = R * np.cos(2 * np.pi * np.arange(P, dtype=np.double) / P) + cdef double[:] rp = np.round(rr, 5) + cdef double[:] cp = np.round(cc, 5) - # pre allocate arrays for computation - cdef cnp.ndarray[double, ndim=1] texture = np.zeros(P, np.double) - cdef cnp.ndarray[char, ndim=1] signed_texture = np.zeros(P, np.int8) - cdef cnp.ndarray[int, ndim=1] rotation_chain = np.zeros(P, np.int32) + # pre-allocate arrays for computation + cdef double[:] texture = np.zeros(P, dtype=np.double) + cdef char[:] signed_texture = np.zeros(P, dtype=np.int8) + cdef int[:] rotation_chain = np.zeros(P, dtype=np.int32) output_shape = (image.shape[0], image.shape[1]) - cdef cnp.ndarray[double, ndim=2] output = np.zeros(output_shape, np.double) + cdef double[:, ::1] output = np.zeros(output_shape, dtype=np.double) cdef Py_ssize_t rows = image.shape[0] cdef Py_ssize_t cols = image.shape[1] @@ -133,8 +128,9 @@ def _local_binary_pattern(cnp.ndarray[double, ndim=2] image, for r in range(image.shape[0]): for c in range(image.shape[1]): for i in range(P): - texture[i] = bilinear_interpolation(image.data, - rows, cols, r + coords[i, 0], c + coords[i, 1], 'C', 0) + texture[i] = bilinear_interpolation(&image[0, 0], rows, cols, + r + rp[i], c + cp[i], + 'C', 0) # signed / thresholded texture for i in range(P): if texture[i] - image[r, c] >= 0: diff --git a/skimage/feature/corner_cy.pyx b/skimage/feature/corner_cy.pyx index d4bc5e5a..7d558e52 100644 --- a/skimage/feature/corner_cy.pyx +++ b/skimage/feature/corner_cy.pyx @@ -59,16 +59,8 @@ def corner_moravec(image, Py_ssize_t window_size=1): cdef Py_ssize_t rows = image.shape[0] cdef Py_ssize_t cols = image.shape[1] - cdef cnp.ndarray[dtype=cnp.double_t, ndim=2, mode='c'] cimage, out - - if image.ndim == 3: - cimage = rgb2grey(image) - cimage = np.ascontiguousarray(img_as_float(image)) - - out = np.zeros(image.shape, dtype=np.double) - - cdef double* image_data = cimage.data - cdef double* out_data = out.data + cdef double[:, ::1] cimage = np.ascontiguousarray(img_as_float(image)) + cdef double[:, ::1] out = np.zeros(image.shape, dtype=np.double) cdef double msum, min_msum cdef Py_ssize_t r, c, br, bc, mr, mc, a, b @@ -81,11 +73,10 @@ def corner_moravec(image, Py_ssize_t window_size=1): msum = 0 for mr in range(- window_size, window_size + 1): for mc in range(- window_size, window_size + 1): - a = (r + mr) * cols + c + mc - b = (br + mr) * cols + bc + mc - msum += (image_data[a] - image_data[b]) ** 2 + msum += (cimage[r + mr, c + mc] + - cimage[br + mr, bc + mc]) ** 2 min_msum = min(msum, min_msum) - out_data[r * cols + c] = min_msum + out[r, c] = min_msum - return out + return np.asarray(out) diff --git a/skimage/feature/texture.py b/skimage/feature/texture.py index 7655b82a..7549cfdd 100644 --- a/skimage/feature/texture.py +++ b/skimage/feature/texture.py @@ -271,6 +271,6 @@ def local_binary_pattern(image, P, R, method='default'): 'uniform': ord('U'), 'var': ord('V') } - image = np.array(image, dtype='double', copy=True) + image = np.ascontiguousarray(image, dtype=np.double) output = _local_binary_pattern(image, P, R, methods[method.lower()]) return output From f76ba9dff3d3645f380b894ddeb4d73b17f69401 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sat, 17 Aug 2013 22:47:03 +0200 Subject: [PATCH 502/736] Use typed memoryviews in filter package --- skimage/filter/_ctmf.pyx | 21 ++++-------- skimage/filter/_denoise_cy.pyx | 60 ++++++++++++++-------------------- 2 files changed, 32 insertions(+), 49 deletions(-) diff --git a/skimage/filter/_ctmf.pyx b/skimage/filter/_ctmf.pyx index e3e845e4..70a59aa3 100644 --- a/skimage/filter/_ctmf.pyx +++ b/skimage/filter/_ctmf.pyx @@ -731,13 +731,8 @@ cdef int c_median_filter(Py_ssize_t rows, return 0 -def median_filter(cnp.ndarray[dtype=cnp.uint8_t, ndim=2, - negative_indices=False, mode='c'] data, - cnp.ndarray[dtype=cnp.uint8_t, ndim=2, - negative_indices=False, mode='c'] mask, - cnp.ndarray[dtype=cnp.uint8_t, ndim=2, - negative_indices=False, mode='c'] output, - int radius, +def median_filter(cnp.uint8_t[:, ::1] data, cnp.uint8_t[:, ::1] mask, + cnp.uint8_t[:, ::1] output, int radius, cnp.int32_t percent): """Median filter with octagon shape and masking. @@ -773,12 +768,10 @@ def median_filter(cnp.ndarray[dtype=cnp.uint8_t, ndim=2, raise ValueError('Data shape (%d, %d) is not output shape (%d, %d)' % (data.shape[0], data.shape[1], output.shape[0], output.shape[1])) - if c_median_filter(data.shape[0], - data.shape[1], - data.strides[0], - data.strides[1], + if c_median_filter(data.shape[0], data.shape[1], + data.strides[0], data.strides[1], radius, percent, - data.data, - mask.data, - output.data): + &data[0, 0], + &mask[0, 0], + &output[0, 0]): raise MemoryError('Failed to allocate scratchpad memory') diff --git a/skimage/filter/_denoise_cy.pyx b/skimage/filter/_denoise_cy.pyx index 3459171c..0c4f2539 100644 --- a/skimage/filter/_denoise_cy.pyx +++ b/skimage/filter/_denoise_cy.pyx @@ -113,11 +113,8 @@ def denoise_bilateral(image, Py_ssize_t win_size=5, sigma_range=None, double max_value - cnp.ndarray[dtype=cnp.double_t, ndim=3, mode='c'] cimage - cnp.ndarray[dtype=cnp.double_t, ndim=3, mode='c'] out - - double* image_data - double* out_data + double[:, :, ::1] cimage + double[:, :, ::1] out double* color_lut double* range_lut @@ -143,8 +140,6 @@ def denoise_bilateral(image, Py_ssize_t win_size=5, sigma_range=None, cimage = np.ascontiguousarray(image) out = np.zeros((rows, cols, dims), dtype=np.double) - image_data = cimage.data - out_data = out.data color_lut = _compute_color_lut(bins, csigma_range, max_value) range_lut = _compute_range_lut(win_size, sigma_spatial) dist_scale = bins / dims / max_value @@ -159,11 +154,10 @@ def denoise_bilateral(image, Py_ssize_t win_size=5, sigma_range=None, for r in range(rows): for c in range(cols): - pixel_addr = r * cols * dims + c * dims total_weight = 0 for d in range(dims): total_values[d] = 0 - centres[d] = image_data[pixel_addr + d] + centres[d] = cimage[r, c, d] for wr in range(-window_ext, window_ext + 1): rr = wr + r kr = wr + window_ext @@ -175,7 +169,7 @@ def denoise_bilateral(image, Py_ssize_t win_size=5, sigma_range=None, # distance between centre stack and current position dist = 0 for d in range(dims): - value = get_pixel3d(image_data, rows, cols, dims, + value = get_pixel3d(&cimage[0, 0, 0], rows, cols, dims, rr, cc, d, cmode, cval) values[d] = value dist += (centres[d] - value)**2 @@ -189,7 +183,7 @@ def denoise_bilateral(image, Py_ssize_t win_size=5, sigma_range=None, total_values[d] += values[d] * weight total_weight += weight for d in range(dims): - out_data[pixel_addr + d] = total_values[d] / total_weight + out[r, c, d] = total_values[d] / total_weight free(color_lut) free(range_lut) @@ -197,11 +191,11 @@ def denoise_bilateral(image, Py_ssize_t win_size=5, sigma_range=None, free(centres) free(total_values) - return np.squeeze(out) + return np.squeeze(np.asarray(out)) def denoise_tv_bregman(image, double weight, int max_iter=100, double eps=1e-3, - isotropic=True): + char isotropic=True): """Perform total-variation denoising using split-Bregman optimization. Total-variation denoising (also know as total-variation regularization) @@ -258,21 +252,17 @@ def denoise_tv_bregman(image, double weight, int max_iter=100, double eps=1e-3, Py_ssize_t total = rows * cols * dims - shape_ext = (rows2, cols2, dims) + shape_ext = (rows2, cols2, dims) + u = np.zeros(shape_ext, dtype=np.double) - cnp.ndarray[dtype=cnp.double_t, ndim=3, mode='c'] cimage = \ - np.ascontiguousarray(image) - cnp.ndarray[dtype=cnp.double_t, ndim=3, mode='c'] u = \ - np.zeros(shape_ext, dtype=np.double) + cdef: + double[:, :, ::1] cimage = np.ascontiguousarray(image) + double[:, :, ::1] cu = u - cnp.ndarray[dtype=cnp.double_t, ndim=3, mode='c'] dx = \ - np.zeros(shape_ext, dtype=np.double) - cnp.ndarray[dtype=cnp.double_t, ndim=3, mode='c'] dy = \ - np.zeros(shape_ext, dtype=np.double) - cnp.ndarray[dtype=cnp.double_t, ndim=3, mode='c'] bx = \ - np.zeros(shape_ext, dtype=np.double) - cnp.ndarray[dtype=cnp.double_t, ndim=3, mode='c'] by = \ - np.zeros(shape_ext, dtype=np.double) + double[:, :, ::1] dx = np.zeros(shape_ext, dtype=np.double) + double[:, :, ::1] dy = np.zeros(shape_ext, dtype=np.double) + double[:, :, ::1] bx = np.zeros(shape_ext, dtype=np.double) + double[:, :, ::1] by = np.zeros(shape_ext, dtype=np.double) double ux, uy, uprev, unew, bxx, byy, dxx, dyy, s int i = 0 @@ -296,19 +286,19 @@ def denoise_tv_bregman(image, double weight, int max_iter=100, double eps=1e-3, for r in range(1, rows + 1): for c in range(1, cols + 1): - uprev = u[r, c, k] + uprev = cu[r, c, k] # forward derivatives - ux = u[r, c + 1, k] - uprev - uy = u[r + 1, c, k] - uprev + ux = cu[r, c + 1, k] - uprev + uy = cu[r + 1, c, k] - uprev # Gauss-Seidel method unew = ( lam * ( - + u[r + 1, c, k] - + u[r - 1, c, k] - + u[r, c + 1, k] - + u[r, c - 1, k] + + cu[r + 1, c, k] + + cu[r - 1, c, k] + + cu[r, c + 1, k] + + cu[r, c - 1, k] + dx[r, c - 1, k] - dx[r, c, k] @@ -321,7 +311,7 @@ def denoise_tv_bregman(image, double weight, int max_iter=100, double eps=1e-3, + by[r, c, k] ) + weight * cimage[r - 1, c - 1, k] ) / norm - u[r, c, k] = unew + cu[r, c, k] = unew # update root mean square error rmse += (unew - uprev)**2 @@ -360,4 +350,4 @@ def denoise_tv_bregman(image, double weight, int max_iter=100, double eps=1e-3, rmse = sqrt(rmse / total) i += 1 - return np.squeeze(u[1:-1, 1:-1]) + return np.squeeze(np.asarray(u[1:-1, 1:-1])) From e78f9ad39d20731f00a592f1ff369a60af69d93b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sat, 17 Aug 2013 22:56:11 +0200 Subject: [PATCH 503/736] Fix reference of corner_foerstner --- skimage/feature/corner.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/skimage/feature/corner.py b/skimage/feature/corner.py index 1e7968d9..53b2241e 100644 --- a/skimage/feature/corner.py +++ b/skimage/feature/corner.py @@ -277,8 +277,7 @@ def corner_foerstner(image, sigma=1): References ---------- - .. [1] http://www.ipb.uni-bonn.de/uploads/tx_ikgpublication/\ - foerstner87.fast.pdf + .. [1] http://www.ipb.uni-bonn.de/uploads/tx_ikgpublication/foerstner87.fast.pdf .. [2] http://en.wikipedia.org/wiki/Corner_detection Examples From 5b38bdac59a00d6a089cb934437caf4de09d7145 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sat, 17 Aug 2013 23:07:00 +0200 Subject: [PATCH 504/736] Fix missing conversion of typed memoryview to numpy array --- skimage/feature/_texture.pyx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/feature/_texture.pyx b/skimage/feature/_texture.pyx index 0e17fd1d..9daa215d 100644 --- a/skimage/feature/_texture.pyx +++ b/skimage/feature/_texture.pyx @@ -177,4 +177,4 @@ def _local_binary_pattern(double[:, ::1] image, output[r, c] = lbp - return output + return np.asarray(output) From a8f0d46ab299dd7d30af32890e5d4c4508dd9b3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sat, 17 Aug 2013 23:17:05 +0200 Subject: [PATCH 505/736] Fix missing conversion of typed memoryview to numpy array --- skimage/draw/_draw.pyx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/draw/_draw.pyx b/skimage/draw/_draw.pyx index 17dee0f7..132ba1d4 100644 --- a/skimage/draw/_draw.pyx +++ b/skimage/draw/_draw.pyx @@ -86,7 +86,7 @@ def line(Py_ssize_t y, Py_ssize_t x, Py_ssize_t y2, Py_ssize_t x2): rr[dx] = y2 cc[dx] = x2 - return rr, cc + return np.asarray(rr), np.asarray(cc) def polygon(y, x, shape=None): From 93e83b347f3460c4c529dba66ede4a7eb4c0153c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Mon, 19 Aug 2013 18:37:30 +0200 Subject: [PATCH 506/736] Improve tests of graph package --- skimage/graph/tests/test_mcp.py | 2 +- skimage/graph/tests/test_spath.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/skimage/graph/tests/test_mcp.py b/skimage/graph/tests/test_mcp.py index a1021380..560f19d0 100644 --- a/skimage/graph/tests/test_mcp.py +++ b/skimage/graph/tests/test_mcp.py @@ -152,4 +152,4 @@ def _test_random(shape): if __name__ == "__main__": - run_module_suite() + np.testing.run_module_suite() diff --git a/skimage/graph/tests/test_spath.py b/skimage/graph/tests/test_spath.py index 62f9f303..d018449a 100644 --- a/skimage/graph/tests/test_spath.py +++ b/skimage/graph/tests/test_spath.py @@ -1,5 +1,5 @@ import numpy as np -from numpy.testing import * +from numpy.testing import assert_equal, assert_array_equal import skimage.graph.spath as spath @@ -33,4 +33,4 @@ def test_non_square(): if __name__ == "__main__": - run_module_suite() + np.testing.run_module_suite() From 8524203f138b0644a6f32ecfe8a7ffe708fdbec4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Mon, 19 Aug 2013 18:42:08 +0200 Subject: [PATCH 507/736] Use typed memoryviews in measure package --- skimage/measure/_find_contours.pyx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/skimage/measure/_find_contours.pyx b/skimage/measure/_find_contours.pyx index d05d9aa7..e2f5a49b 100644 --- a/skimage/measure/_find_contours.pyx +++ b/skimage/measure/_find_contours.pyx @@ -4,8 +4,6 @@ #cython: wraparound=False import numpy as np -cimport numpy as cnp - cdef inline double _get_fraction(double from_value, double to_value, double level): @@ -14,7 +12,7 @@ cdef inline double _get_fraction(double from_value, double to_value, return ((level - from_value) / (to_value - from_value)) -def iterate_and_store(cnp.ndarray[double, ndim=2] array, +def iterate_and_store(double[:, :] array, double level, Py_ssize_t vertex_connect_high): """Iterate across the given array in a marching-squares fashion, looking for segments that cross 'level'. If such a segment is @@ -46,7 +44,7 @@ def iterate_and_store(cnp.ndarray[double, ndim=2] array, # Calculate the number of iterations we'll need cdef Py_ssize_t num_square_steps = (array.shape[0] - 1) \ - * (array.shape[1] - 1) + * (array.shape[1] - 1) cdef unsigned char square_case = 0 cdef tuple top, bottom, left, right From b8b2a638843914d3940f2b1843476056cdd11f9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Mon, 19 Aug 2013 19:14:56 +0200 Subject: [PATCH 508/736] Use typed memoryviews in morphology package --- skimage/morphology/_convex_hull.pyx | 40 ++++++++++++++++--------- skimage/morphology/_greyreconstruct.pyx | 4 +-- skimage/morphology/_pnpoly.pyx | 19 ++++++------ skimage/morphology/_skeletonize_cy.pyx | 39 ++++++++++-------------- skimage/morphology/_watershed.pyx | 15 ++++------ skimage/morphology/cmorph.pyx | 34 +++++++++------------ 6 files changed, 72 insertions(+), 79 deletions(-) diff --git a/skimage/morphology/_convex_hull.pyx b/skimage/morphology/_convex_hull.pyx index 7298e1ed..cd9270cc 100644 --- a/skimage/morphology/_convex_hull.pyx +++ b/skimage/morphology/_convex_hull.pyx @@ -7,7 +7,7 @@ import numpy as np cimport numpy as cnp -def possible_hull(cnp.ndarray[dtype=cnp.uint8_t, ndim=2, mode="c"] img): +def possible_hull(cnp.uint8_t[:, ::1] img): """Return positions of pixels that possibly belong to the convex hull. Parameters @@ -30,31 +30,43 @@ def possible_hull(cnp.ndarray[dtype=cnp.uint8_t, ndim=2, mode="c"] img): # cols storage slots for top boundary pixels # rows storage slots for right boundary pixels # cols storage slots for bottom boundary pixels - cdef cnp.ndarray[dtype=cnp.intp_t, ndim=2] nonzero = \ - np.ones((2 * (rows + cols), 2), dtype=np.intp) - nonzero *= -1 + coords = np.ones((2 * (rows + cols), 2), dtype=np.intp) + coords *= -1 + + cdef Py_ssize_t[:, ::1] nonzero = coords + cdef Py_ssize_t rows_cols = rows + cols + cdef Py_ssize_t rows_2_cols = 2 * rows + cols + cdef Py_ssize_t rows_cols_r, rows_c for r in range(rows): + + rows_cols_r = rows_cols + r + for c in range(cols): + if img[r, c] != 0: + + rows_c = rows + c + rows_2_cols_c = rows_2_cols + c + # Left check if nonzero[r, 1] == -1: nonzero[r, 0] = r nonzero[r, 1] = c # Right check - elif nonzero[rows + cols + r, 1] < c: - nonzero[rows + cols + r, 0] = r - nonzero[rows + cols + r, 1] = c + elif nonzero[rows_cols_r, 1] < c: + nonzero[rows_cols_r, 0] = r + nonzero[rows_cols_r, 1] = c # Top check - if nonzero[rows + c, 1] == -1: - nonzero[rows + c, 0] = r - nonzero[rows + c, 1] = c + if nonzero[rows_c, 1] == -1: + nonzero[rows_c, 0] = r + nonzero[rows_c, 1] = c # Bottom check - elif nonzero[2 * rows + cols + c, 0] < r: - nonzero[2 * rows + cols + c, 0] = r - nonzero[2 * rows + cols + c, 1] = c + elif nonzero[rows_2_cols_c, 0] < r: + nonzero[rows_2_cols_c, 0] = r + nonzero[rows_2_cols_c, 1] = c - return nonzero[nonzero[:, 0] != -1] + return coords[coords[:, 0] != -1] diff --git a/skimage/morphology/_greyreconstruct.pyx b/skimage/morphology/_greyreconstruct.pyx index e8a84f3b..fa92ecec 100644 --- a/skimage/morphology/_greyreconstruct.pyx +++ b/skimage/morphology/_greyreconstruct.pyx @@ -21,8 +21,8 @@ def reconstruction_loop(cnp.ndarray[dtype=cnp.uint32_t, ndim=1, negative_indices=False, mode='c'] anext, cnp.ndarray[dtype=cnp.int32_t, ndim=1, negative_indices=False, mode='c'] astrides, - int current_idx, - int image_stride): + Py_ssize_t current_idx, + Py_ssize_t image_stride): """The inner loop for reconstruction. This algorithm uses the rank-order of pixels. If low intensity pixels have diff --git a/skimage/morphology/_pnpoly.pyx b/skimage/morphology/_pnpoly.pyx index f32778cc..12b48e5d 100644 --- a/skimage/morphology/_pnpoly.pyx +++ b/skimage/morphology/_pnpoly.pyx @@ -29,7 +29,7 @@ def grid_points_inside_poly(shape, verts): True where the grid falls inside the polygon. """ - cdef cnp.ndarray[cnp.double_t, ndim=1, mode="c"] vx, vy + cdef double[:] vx, vy verts = np.asarray(verts) vx = verts[:, 0].astype(np.double) @@ -45,8 +45,7 @@ def grid_points_inside_poly(shape, verts): for m in range(M): for n in range(N): - out[m, n] = point_in_polygon(V, vx.data, vy.data, - m, n) + out[m, n] = point_in_polygon(V, &vx[0], &vy[0], m, n) return out.view(bool) @@ -57,18 +56,18 @@ def points_inside_poly(points, verts): Parameters ---------- points : (N, 2) array - Input points, ``(x, y)``. + Input points, ``(x, y)``. verts : (M, 2) array - Vertices of the polygon, sorted either clockwise or anti-clockwise. - The first point may (but does not need to be) duplicated. + Vertices of the polygon, sorted either clockwise or anti-clockwise. + The first point may (but does not need to be) duplicated. Returns ------- mask : (N,) array of bool - True if corresponding point is inside the polygon. + True if corresponding point is inside the polygon. """ - cdef cnp.ndarray[cnp.double_t, ndim=1, mode="c"] x, y, vx, vy + cdef double[:] x, y, vx, vy points = np.asarray(points) verts = np.asarray(verts) @@ -82,8 +81,8 @@ def points_inside_poly(points, verts): cdef cnp.ndarray[cnp.uint8_t, ndim=1] out = \ np.zeros(x.shape[0], dtype=np.uint8) - points_in_polygon(vx.shape[0], vx.data, vy.data, - x.shape[0], x.data, y.data, + points_in_polygon(vx.shape[0], &vx[0], &vy[0], + x.shape[0], &x[0], &y[0], out.data) return out.astype(bool) diff --git a/skimage/morphology/_skeletonize_cy.pyx b/skimage/morphology/_skeletonize_cy.pyx index 13e303d4..805cdad7 100644 --- a/skimage/morphology/_skeletonize_cy.pyx +++ b/skimage/morphology/_skeletonize_cy.pyx @@ -19,16 +19,9 @@ import numpy as np cimport numpy as cnp -def _skeletonize_loop(cnp.ndarray[dtype=cnp.uint8_t, ndim=2, - negative_indices=False, mode='c'] result, - cnp.ndarray[dtype=cnp.intp_t, ndim=1, - negative_indices=False, mode='c'] i, - cnp.ndarray[dtype=cnp.intp_t, ndim=1, - negative_indices=False, mode='c'] j, - cnp.ndarray[dtype=cnp.int32_t, ndim=1, - negative_indices=False, mode='c'] order, - cnp.ndarray[dtype=cnp.uint8_t, ndim=1, - negative_indices=False, mode='c'] table): +def _skeletonize_loop(cnp.uint8_t[:, ::1] result, + cnp.uint8_t[:] i, cnp.uint8_t[:] j, + cnp.int32_t[:] order, cnp.uint8_t[:] table): """ Inner loop of skeletonize function @@ -65,9 +58,11 @@ def _skeletonize_loop(cnp.ndarray[dtype=cnp.uint8_t, ndim=2, pixels. """ cdef: - cnp.int32_t accumulator + Py_ssize_t accumulator Py_ssize_t index, order_index Py_ssize_t ii, jj + Py_ssize_t rows = result.shape[0] + Py_ssize_t cols = result.shape[1] for index in range(order.shape[0]): accumulator = 16 @@ -80,26 +75,25 @@ def _skeletonize_loop(cnp.ndarray[dtype=cnp.uint8_t, ndim=2, accumulator += 1 if result[ii - 1, jj]: accumulator += 2 - if jj < result.shape[1] - 1 and result[ii - 1, jj + 1]: + if jj < cols - 1 and result[ii - 1, jj + 1]: accumulator += 4 if jj > 0 and result[ii, jj - 1]: accumulator += 8 - if jj < result.shape[1] - 1 and result[ii, jj + 1]: + if jj < cols - 1 and result[ii, jj + 1]: accumulator += 32 - if ii < result.shape[0]-1: + if ii < rows - 1: if jj > 0 and result[ii + 1, jj - 1]: accumulator += 64 if result[ii + 1, jj]: accumulator += 128 - if jj < result.shape[1] - 1 and result[ii + 1, jj + 1]: + if jj < cols - 1 and result[ii + 1, jj + 1]: accumulator += 256 # Assign the value of table corresponding to the configuration result[ii, jj] = table[accumulator] -def _table_lookup_index(cnp.ndarray[dtype=cnp.uint8_t, ndim=2, - negative_indices=False, mode='c'] image): +def _table_lookup_index(cnp.uint8_t[:, ::1] image): """ Return an index into a table per pixel of a binary image @@ -120,9 +114,8 @@ def _table_lookup_index(cnp.ndarray[dtype=cnp.uint8_t, ndim=2, hardwired kernel. """ cdef: - cnp.ndarray[dtype=cnp.int32_t, ndim=2, - negative_indices=False, mode='c'] indexer - cnp.int32_t *p_indexer + Py_ssize_t[:, ::1] indexer + Py_ssize_t *p_indexer cnp.uint8_t *p_image Py_ssize_t i_stride Py_ssize_t i_shape @@ -133,9 +126,9 @@ def _table_lookup_index(cnp.ndarray[dtype=cnp.uint8_t, ndim=2, i_shape = image.shape[0] j_shape = image.shape[1] - indexer = np.zeros((i_shape, j_shape), np.int32) - p_indexer = indexer.data - p_image = image.data + indexer = np.zeros((i_shape, j_shape), dtype=np.intp) + p_indexer = &indexer[0, 0] + p_image = &image[0, 0] i_stride = image.strides[0] assert i_shape >= 3 and j_shape >= 3, \ "Please use the slow method for arrays < 3x3" diff --git a/skimage/morphology/_watershed.pyx b/skimage/morphology/_watershed.pyx index 122f0262..beaf874b 100644 --- a/skimage/morphology/_watershed.pyx +++ b/skimage/morphology/_watershed.pyx @@ -23,17 +23,12 @@ include "heap_watershed.pxi" @cython.boundscheck(False) -def watershed(np.ndarray[DTYPE_INT32_t, ndim=1, negative_indices=False, - mode='c'] image, - np.ndarray[DTYPE_INT32_t, ndim=2, negative_indices=False, - mode='c'] pq, +def watershed(DTYPE_INT32_t[:] image, + DTYPE_INT32_t[:, ::1] pq, Py_ssize_t age, - np.ndarray[DTYPE_INT32_t, ndim=2, negative_indices=False, - mode='c'] structure, - np.ndarray[DTYPE_BOOL_t, ndim=1, negative_indices=False, - mode='c'] mask, - np.ndarray[DTYPE_INT32_t, ndim=1, negative_indices=False, - mode='c'] output): + DTYPE_INT32_t[:, ::1] structure, + DTYPE_BOOL_t[:] mask, + DTYPE_INT32_t[:] output): """Do heavy lifting of watershed algorithm Parameters diff --git a/skimage/morphology/cmorph.pyx b/skimage/morphology/cmorph.pyx index c96039eb..4d491c3b 100644 --- a/skimage/morphology/cmorph.pyx +++ b/skimage/morphology/cmorph.pyx @@ -8,9 +8,9 @@ cimport numpy as np from libc.stdlib cimport malloc, free -def _dilate(np.ndarray[np.uint8_t, ndim=2] image, - np.ndarray[np.uint8_t, ndim=2] selem, - np.ndarray[np.uint8_t, ndim=2] out=None, +def _dilate(np.uint8_t[:, :] image, + np.uint8_t[:, :] selem, + np.uint8_t[:, :] out=None, char shift_x=0, char shift_y=0): """Return greyscale morphological dilation of an image. @@ -52,12 +52,9 @@ def _dilate(np.ndarray[np.uint8_t, ndim=2] image, else: out = np.ascontiguousarray(out) - cdef np.uint8_t* out_data = out.data - cdef np.uint8_t* image_data = image.data - cdef Py_ssize_t r, c, rr, cc, s, value, local_max - cdef Py_ssize_t selem_num = np.sum(selem != 0) + cdef Py_ssize_t selem_num = np.sum(np.asarray(selem) != 0) cdef Py_ssize_t* sr = malloc(selem_num * sizeof(Py_ssize_t)) cdef Py_ssize_t* sc = malloc(selem_num * sizeof(Py_ssize_t)) @@ -76,21 +73,21 @@ def _dilate(np.ndarray[np.uint8_t, ndim=2] image, rr = r + sr[s] cc = c + sc[s] if 0 <= rr < rows and 0 <= cc < cols: - value = image_data[rr * cols + cc] + value = image[rr, cc] if value > local_max: local_max = value - out_data[r * cols + c] = local_max + out[r, c] = local_max free(sr) free(sc) - return out + return np.asarray(out) -def _erode(np.ndarray[np.uint8_t, ndim=2] image, - np.ndarray[np.uint8_t, ndim=2] selem, - np.ndarray[np.uint8_t, ndim=2] out=None, +def _erode(np.uint8_t[:, :] image, + np.uint8_t[:, :] selem, + np.uint8_t[:, :] out=None, char shift_x=0, char shift_y=0): """Return greyscale morphological erosion of an image. @@ -131,12 +128,9 @@ def _erode(np.ndarray[np.uint8_t, ndim=2] image, else: out = np.ascontiguousarray(out) - cdef np.uint8_t* out_data = out.data - cdef np.uint8_t* image_data = image.data - cdef int r, c, rr, cc, s, value, local_min - cdef Py_ssize_t selem_num = np.sum(selem != 0) + cdef Py_ssize_t selem_num = np.sum(np.asarray(selem) != 0) cdef Py_ssize_t* sr = malloc(selem_num * sizeof(Py_ssize_t)) cdef Py_ssize_t* sc = malloc(selem_num * sizeof(Py_ssize_t)) @@ -155,13 +149,13 @@ def _erode(np.ndarray[np.uint8_t, ndim=2] image, rr = r + sr[s] cc = c + sc[s] if 0 <= rr < rows and 0 <= cc < cols: - value = image_data[rr * cols + cc] + value = image[rr, cc] if value < local_min: local_min = value - out_data[r * cols + c] = local_min + out[r, c] = local_min free(sr) free(sc) - return out + return np.asarray(out) From c8f619e384860d17fab724e622ea239b0b1dacc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Mon, 19 Aug 2013 19:22:28 +0200 Subject: [PATCH 509/736] Use typed memoryviews in transform package --- skimage/segmentation/_felzenszwalb_cy.pyx | 3 ++- skimage/segmentation/_slic.pyx | 20 ++++++++------------ skimage/transform/_warps_cy.pyx | 13 +++++-------- 3 files changed, 15 insertions(+), 21 deletions(-) diff --git a/skimage/segmentation/_felzenszwalb_cy.pyx b/skimage/segmentation/_felzenszwalb_cy.pyx index 3babb86e..8590e17d 100644 --- a/skimage/segmentation/_felzenszwalb_cy.pyx +++ b/skimage/segmentation/_felzenszwalb_cy.pyx @@ -12,7 +12,8 @@ from skimage.morphology.ccomp cimport find_root, join_trees from ..util import img_as_float -def _felzenszwalb_grey(image, double scale=1, sigma=0.8, Py_ssize_t min_size=20): +def _felzenszwalb_grey(image, double scale=1, sigma=0.8, + Py_ssize_t min_size=20): """Felzenszwalb's efficient graph based segmentation for a single channel. Produces an oversegmentation of a 2d image using a fast, minimum spanning diff --git a/skimage/segmentation/_slic.pyx b/skimage/segmentation/_slic.pyx index bfa6cd1a..5df747ad 100644 --- a/skimage/segmentation/_slic.pyx +++ b/skimage/segmentation/_slic.pyx @@ -2,15 +2,11 @@ #cython: boundscheck=False #cython: nonecheck=False #cython: wraparound=False -import collections as coll import numpy as np -from time import time from scipy import ndimage -cimport numpy as cnp - -from ..util import img_as_float, regular_grid -from ..color import rgb2lab, gray2rgb +from skimage.util import img_as_float, regular_grid +from skimage.color import rgb2lab, gray2rgb def _slic_cython(double[:, :, :, ::1] image_zyx, @@ -19,18 +15,18 @@ def _slic_cython(double[:, :, :, ::1] image_zyx, double[:, ::1] means, Py_ssize_t max_iter, Py_ssize_t n_segments): """Helper function for SLIC segmentation. - + Parameters ---------- - image_zyx : 4D np.ndarray of double, shape (Z, Y, X, 6) + image_zyx : 4D array of double, shape (Z, Y, X, 6) The image with embedded coordinates, that is, `image_zyx[i, j, k]` is `array([i, j, k, r, g, b])` or `array([i, j, k, L, a, b])`, depending on the colorspace. - nearest_mean : 3D np.ndarray of int, shape (Z, Y, X) + nearest_mean : 3D array of int, shape (Z, Y, X) The (initially empty) label field. - distance : 3D np.ndarray of double, shape (Z, Y, X) + distance : 3D array of double, shape (Z, Y, X) The (initially infinity) array of distances to the nearest centroid. - means : 2D np.ndarray of double, shape (n_segments, 6) + means : 2D array of double, shape (n_segments, 6) The centroids obtained by SLIC. max_iter : int The maximum number of k-means iterations. @@ -39,7 +35,7 @@ def _slic_cython(double[:, :, :, ::1] image_zyx, Returns ------- - nearest_mean : 3D np.ndarray of int, shape (Z, Y, X) + nearest_mean : 3D array of int, shape (Z, Y, X) The label field/superpixels found by SLIC. """ diff --git a/skimage/transform/_warps_cy.pyx b/skimage/transform/_warps_cy.pyx index f51eb8d3..0eb7efdd 100644 --- a/skimage/transform/_warps_cy.pyx +++ b/skimage/transform/_warps_cy.pyx @@ -83,10 +83,8 @@ def _warp_fast(cnp.ndarray image, cnp.ndarray H, output_shape=None, """ - cdef cnp.ndarray[dtype=cnp.double_t, ndim=2, mode="c"] img = \ - np.ascontiguousarray(image, dtype=np.double) - cdef cnp.ndarray[dtype=cnp.double_t, ndim=2, mode="c"] M = \ - np.ascontiguousarray(H) + cdef double[:, ::1] img = np.ascontiguousarray(image, dtype=np.double) + cdef double[:, ::1] M = np.ascontiguousarray(H) if mode not in ('constant', 'wrap', 'reflect', 'nearest'): raise ValueError("Invalid mode specified. Please use " @@ -101,8 +99,7 @@ def _warp_fast(cnp.ndarray image, cnp.ndarray H, output_shape=None, out_r = output_shape[0] out_c = output_shape[1] - cdef cnp.ndarray[dtype=cnp.double_t, ndim=2] out = \ - np.zeros((out_r, out_c), dtype=np.double) + cdef double[:, ::1] out = np.zeros((out_r, out_c), dtype=np.double) cdef Py_ssize_t tfr, tfc cdef double r, c @@ -122,8 +119,8 @@ def _warp_fast(cnp.ndarray image, cnp.ndarray H, output_shape=None, for tfr in range(out_r): for tfc in range(out_c): - _matrix_transform(tfc, tfr, M.data, &c, &r) - out[tfr, tfc] = interp_func(img.data, rows, cols, r, c, + _matrix_transform(tfc, tfr, &M[0, 0], &c, &r) + out[tfr, tfc] = interp_func(&img[0, 0], rows, cols, r, c, mode_c, cval) return out From 5f82c206b48a63966cd33b8a757eb85529fe67b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Mon, 19 Aug 2013 19:31:40 +0200 Subject: [PATCH 510/736] Fix bugs in usage of typed memoryviews --- skimage/morphology/_skeletonize_cy.pyx | 4 ++-- skimage/transform/_warps_cy.pyx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/skimage/morphology/_skeletonize_cy.pyx b/skimage/morphology/_skeletonize_cy.pyx index 805cdad7..10e70d98 100644 --- a/skimage/morphology/_skeletonize_cy.pyx +++ b/skimage/morphology/_skeletonize_cy.pyx @@ -20,7 +20,7 @@ cimport numpy as cnp def _skeletonize_loop(cnp.uint8_t[:, ::1] result, - cnp.uint8_t[:] i, cnp.uint8_t[:] j, + Py_ssize_t[:] i, Py_ssize_t[:] j, cnp.int32_t[:] order, cnp.uint8_t[:] table): """ Inner loop of skeletonize function @@ -207,4 +207,4 @@ def _table_lookup_index(cnp.uint8_t[:, ::1] image): indexer[i - 1, j_shape - 1] += 128 indexer[i, j_shape - 1] += 16 indexer[i + 1, j_shape - 1] += 2 - return indexer + return np.asarray(indexer) diff --git a/skimage/transform/_warps_cy.pyx b/skimage/transform/_warps_cy.pyx index 0eb7efdd..5dc2ffd0 100644 --- a/skimage/transform/_warps_cy.pyx +++ b/skimage/transform/_warps_cy.pyx @@ -123,4 +123,4 @@ def _warp_fast(cnp.ndarray image, cnp.ndarray H, output_shape=None, out[tfr, tfc] = interp_func(&img[0, 0], rows, cols, r, c, mode_c, cval) - return out + return np.asarray(out) From 600305eb856c4e9cf25fe130e39a5eb59caca728 Mon Sep 17 00:00:00 2001 From: Matt Terry Date: Mon, 19 Aug 2013 11:04:29 -0700 Subject: [PATCH 511/736] doc quickfix --- skimage/color/delta_e.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/color/delta_e.py b/skimage/color/delta_e.py index 18cfca98..ea08e13f 100644 --- a/skimage/color/delta_e.py +++ b/skimage/color/delta_e.py @@ -312,7 +312,7 @@ def get_dH2(lab1, lab2): """squared hue difference term occurring in deltaE_cmc and deltaE_ciede94 Despite its name "dH" is not a simple difference of hue values. We avoid - working directly with the hue value directly since differencing angles is + working directly with the hue value since differencing angles is troublesome. The hue term is usually written as: c1 = sqrt(a1**2 + b1**2) c2 = sqrt(a2**2 + b2**2) From 7e7fb2fc859362973fe11df76d890e038cecdac8 Mon Sep 17 00:00:00 2001 From: Matt Terry Date: Mon, 19 Aug 2013 11:26:07 -0700 Subject: [PATCH 512/736] comma usage --- skimage/color/delta_e.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skimage/color/delta_e.py b/skimage/color/delta_e.py index ea08e13f..9119ecf4 100644 --- a/skimage/color/delta_e.py +++ b/skimage/color/delta_e.py @@ -311,8 +311,8 @@ def deltaE_cmc(lab1, lab2, kL=1, kC=1): def get_dH2(lab1, lab2): """squared hue difference term occurring in deltaE_cmc and deltaE_ciede94 - Despite its name "dH" is not a simple difference of hue values. We avoid - working directly with the hue value since differencing angles is + Despite its name, "dH" is not a simple difference of hue values. We avoid + working directly with the hue value, since differencing angles is troublesome. The hue term is usually written as: c1 = sqrt(a1**2 + b1**2) c2 = sqrt(a2**2 + b2**2) From d9836a6add9d71b61c9d06eb3214d66674364a34 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Wed, 21 Aug 2013 00:10:33 +0200 Subject: [PATCH 513/736] Remove logger in favor of warnings. --- skimage/__init__.py | 84 ++++++++++++++++++++++++++-------------- skimage/_shared/utils.py | 15 ++++++- skimage/util/dtype.py | 12 +++--- 3 files changed, 72 insertions(+), 39 deletions(-) diff --git a/skimage/__init__.py b/skimage/__init__.py index daac238c..23e5d3a7 100644 --- a/skimage/__init__.py +++ b/skimage/__init__.py @@ -38,8 +38,6 @@ util Utility Functions ----------------- -get_log - Returns the ``skimage`` log. Use this to print debug output. img_as_float Convert an image to floating point format, with values in [0, 1]. img_as_uint @@ -54,6 +52,7 @@ img_as_ubyte import os.path as _osp import imp as _imp import functools as _functools +from skimage._shared.utils import deprecated as _deprecated pkg_dir = _osp.abspath(_osp.dirname(__file__)) data_dir = _osp.join(pkg_dir, 'data') @@ -62,6 +61,7 @@ try: from .version import version as __version__ except ImportError: __version__ = "unbuilt-dev" +del version try: @@ -88,6 +88,57 @@ test_verbose = _functools.partial(test, verbose=True) test_verbose.__doc__ = test.__doc__ +from exceptions import Warning as _Warning + +class _Log(_Warning): + pass + +class _FakeLog(object): + def __init__(self, name): + """ + Parameters + ---------- + name : str + Name of the log. + repeat : bool + Whether to print repeating messages more than once (False by + default). + """ + self._name = name + + import warnings + warnings.simplefilter("always", _Log) + + self._warnings = warnings + + def _warn(self, msg, wtype): + self._warnings.warn('%s: %s' % (wtype, msg), _Log) + + def debug(self, msg): + self._warn(msg, 'DEBUG') + + def info(self, msg): + self._warn(msg, 'INFO') + + def warning(self, msg): + self._warn(msg, 'WARNING') + + warn = warning + + def error(self, msg): + self._warn(msg, 'ERROR') + + def critical(self, msg): + self._warn(msg, 'CRITICAL') + + def addHandler(*args): + pass + + def setLevel(*args): + pass + + +@_deprecated() def get_log(name=None): """Return a console logger. @@ -105,39 +156,12 @@ def get_log(name=None): http://docs.python.org/library/logging.html """ - import logging - if name is None: name = 'skimage' else: name = 'skimage.' + name - log = logging.getLogger(name) - return log + return _FakeLog(name) -def _setup_log(): - """Configure root logger. - - """ - import logging - import sys - - formatter = logging.Formatter( - '%(name)s: %(levelname)s: %(message)s' - ) - - try: - handler = logging.StreamHandler(stream=sys.stdout) - except TypeError: - handler = logging.StreamHandler(strm=sys.stdout) - handler.setFormatter(formatter) - - log = get_log() - log.addHandler(handler) - log.setLevel(logging.WARNING) - log.propagate = False - -_setup_log() - from .util.dtype import * diff --git a/skimage/_shared/utils.py b/skimage/_shared/utils.py index ea631716..18cd25c9 100644 --- a/skimage/_shared/utils.py +++ b/skimage/_shared/utils.py @@ -1,6 +1,7 @@ import warnings import functools import sys +from exceptions import Warning from . import six @@ -8,6 +9,14 @@ from . import six __all__ = ['deprecated', 'get_bound_method_class'] +class skimage_deprecation(Warning): + """Create our own deprecation class, since Python >= 2.7 + silences deprecations by default. + + """ + pass + + class deprecated(object): """Decorator to mark deprecated functions with warning. @@ -39,12 +48,14 @@ class deprecated(object): def wrapped(*args, **kwargs): if self.behavior == 'warn': func_code = six.get_function_code(func) + warnings.simplefilter('always', skimage_deprecation) + warnings.warn(msg, category=skimage_deprecation) warnings.warn_explicit(msg, - category=DeprecationWarning, + category=skimage_deprecation, filename=func_code.co_filename, lineno=func_code.co_firstlineno + 1) elif self.behavior == 'raise': - raise DeprecationWarning(msg) + raise skimage_deprecation(msg) return func(*args, **kwargs) # modify doc string to display deprecation warning diff --git a/skimage/util/dtype.py b/skimage/util/dtype.py index 8daea421..3f85cc2b 100644 --- a/skimage/util/dtype.py +++ b/skimage/util/dtype.py @@ -1,12 +1,10 @@ from __future__ import division import numpy as np +from warnings import warn __all__ = ['img_as_float', 'img_as_int', 'img_as_uint', 'img_as_ubyte', 'img_as_bool', 'dtype_limits'] -from .. import get_log -log = get_log('dtype_converter') - dtype_range = {np.bool_: (False, True), np.bool8: (False, True), np.uint8: (0, 255), @@ -101,12 +99,12 @@ def convert(image, dtype, force_copy=False, uniform=False): raise ValueError("can not convert %s to %s." % (dtypeobj_in, dtypeobj)) def sign_loss(): - log.warn("Possible sign loss when converting negative image of type " - "%s to positive image of type %s." % (dtypeobj_in, dtypeobj)) + warn("Possible sign loss when converting negative image of type " + "%s to positive image of type %s." % (dtypeobj_in, dtypeobj)) def prec_loss(): - log.warn("Possible precision loss when converting from " - "%s to %s" % (dtypeobj_in, dtypeobj)) + warn("Possible precision loss when converting from " + "%s to %s" % (dtypeobj_in, dtypeobj)) def _dtype(itemsize, *dtypes): # Return first of `dtypes` with itemsize greater than `itemsize` From ca768b0b95045c405472604d2367b834bb0d1a76 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Wed, 21 Aug 2013 00:59:37 +0200 Subject: [PATCH 514/736] Remove unnecessary import of Warning. --- doc/examples/plot_line_hough_transform.py | 2 +- skimage/__init__.py | 4 +--- skimage/_shared/utils.py | 1 - 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/doc/examples/plot_line_hough_transform.py b/doc/examples/plot_line_hough_transform.py index e19c37d7..cd0ae008 100644 --- a/doc/examples/plot_line_hough_transform.py +++ b/doc/examples/plot_line_hough_transform.py @@ -1,4 +1,4 @@ -""" +r""" ============================= Straight line Hough transform ============================= diff --git a/skimage/__init__.py b/skimage/__init__.py index 23e5d3a7..53d31eae 100644 --- a/skimage/__init__.py +++ b/skimage/__init__.py @@ -88,9 +88,7 @@ test_verbose = _functools.partial(test, verbose=True) test_verbose.__doc__ = test.__doc__ -from exceptions import Warning as _Warning - -class _Log(_Warning): +class _Log(Warning): pass class _FakeLog(object): diff --git a/skimage/_shared/utils.py b/skimage/_shared/utils.py index 18cd25c9..0909b232 100644 --- a/skimage/_shared/utils.py +++ b/skimage/_shared/utils.py @@ -1,7 +1,6 @@ import warnings import functools import sys -from exceptions import Warning from . import six From 05aeb7c7fe89a70fa18294d69c9e12dd58fa5c08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Tue, 20 Aug 2013 14:57:12 +0200 Subject: [PATCH 515/736] Convert to int for Py_ssize_t --- skimage/morphology/greyreconstruct.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/morphology/greyreconstruct.py b/skimage/morphology/greyreconstruct.py index 049488f8..17455b55 100644 --- a/skimage/morphology/greyreconstruct.py +++ b/skimage/morphology/greyreconstruct.py @@ -159,7 +159,7 @@ def reconstruction(seed, mask, method='dilation', selem=None, offset=None): # Create a list of strides across the array to get the neighbors within # a flattened array value_stride = np.array(images.strides[1:]) / images.dtype.itemsize - image_stride = images.strides[0] / images.dtype.itemsize + image_stride = images.strides[0] // images.dtype.itemsize selem_mgrid = np.mgrid[[slice(-o, d - o) for d, o in zip(selem.shape, offset)]] selem_offsets = selem_mgrid[:, selem].transpose() From 4215ad11a123494d7d2db7d073eede53a9e0c951 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Wed, 21 Aug 2013 11:09:02 +0200 Subject: [PATCH 516/736] Remove leftover debug statement. --- skimage/_shared/utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/skimage/_shared/utils.py b/skimage/_shared/utils.py index 0909b232..b7071521 100644 --- a/skimage/_shared/utils.py +++ b/skimage/_shared/utils.py @@ -48,7 +48,6 @@ class deprecated(object): if self.behavior == 'warn': func_code = six.get_function_code(func) warnings.simplefilter('always', skimage_deprecation) - warnings.warn(msg, category=skimage_deprecation) warnings.warn_explicit(msg, category=skimage_deprecation, filename=func_code.co_filename, From 2b0f037422c07274bb3005321e474e2b8ceafddd Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Wed, 21 Aug 2013 17:46:38 +0200 Subject: [PATCH 517/736] Remove Image wrapper from io. Improve tags handling. --- doc/source/api_changes.txt | 11 +++++++--- skimage/io/_io.py | 33 +++++++++++++++++------------ skimage/io/tests/test_collection.py | 2 +- skimage/io/tests/test_pil.py | 5 +++-- 4 files changed, 31 insertions(+), 20 deletions(-) diff --git a/doc/source/api_changes.txt b/doc/source/api_changes.txt index 57486723..c363288d 100644 --- a/doc/source/api_changes.txt +++ b/doc/source/api_changes.txt @@ -1,9 +1,14 @@ +Version 0.9 +----------- +- No longer wrap ``imread`` output in an ``Image`` class. + +Version 0.4 +----------- +- Switch mask and radius arguments for ``median_filter`` + Version 0.3 ----------- - Remove ``as_grey``, ``dtype`` keyword from ImageCollection - Remove ``dtype`` from imread - Generalise ImageCollection to accept a load_func -Version 0.4 ------------ -- Switch mask and radius arguments for median_filter diff --git a/skimage/io/_io.py b/skimage/io/_io.py index 63d3d7e4..a7df4694 100644 --- a/skimage/io/_io.py +++ b/skimage/io/_io.py @@ -35,28 +35,33 @@ class Image(np.ndarray): These objects have tags for image metadata and IPython display protocol methods for image display. - """ - tags = {'filename': '', - 'EXIF': {}, - 'info': {}} + Parameters + ---------- + arr : ndarray + Image data. + kwargs : Image tags as keywords + Specified in the form ``tag0=value``, ``tag1=value``. + + Attributes + ---------- + tags : dict + Meta-data. + + """ def __new__(cls, arr, **kwargs): """Set the image data and tags according to given parameters. - Parameters - ---------- - arr : ndarray - Image data. - kwargs : Image tags as keywords - Specified in the form ``tag0=value``, ``tag1=value``. - """ x = np.asarray(arr).view(cls) - for tag, value in Image.tags.items(): - setattr(x, tag, kwargs.get(tag, getattr(arr, tag, value))) + x.tags = kwargs + return x + def __array_finalize__(self, obj): + self.tags = getattr(obj, 'tags', {}) + def _repr_png_(self): return self._repr_image_format('png') @@ -147,7 +152,7 @@ def imread(fname, as_grey=False, plugin=None, flatten=None, if as_grey and getattr(img, 'ndim', 0) >= 3: img = rgb2grey(img) - return Image(img) + return img def imread_collection(load_pattern, conserve_memory=True, diff --git a/skimage/io/tests/test_collection.py b/skimage/io/tests/test_collection.py index 25cd1152..f56d753b 100644 --- a/skimage/io/tests/test_collection.py +++ b/skimage/io/tests/test_collection.py @@ -57,7 +57,7 @@ class TestImageCollection(): def test_getitem(self): num = len(self.collection) for i in range(-num, num): - assert type(self.collection[i]) is ioImage + assert type(self.collection[i]) is np.ndarray assert_array_almost_equal(self.collection[0], self.collection[-num]) diff --git a/skimage/io/tests/test_pil.py b/skimage/io/tests/test_pil.py index 664bcf4d..61ec5ce3 100644 --- a/skimage/io/tests/test_pil.py +++ b/skimage/io/tests/test_pil.py @@ -6,7 +6,8 @@ from numpy.testing.decorators import skipif from tempfile import NamedTemporaryFile from skimage import data_dir -from skimage.io import imread, imsave, use_plugin, reset_plugins +from skimage.io import (imread, imsave, use_plugin, reset_plugins, + Image as ioImage) from skimage._shared.six.moves import StringIO @@ -83,7 +84,7 @@ def test_imread_uint16(): @skipif(not PIL_available) def test_repr_png(): img_path = os.path.join(data_dir, 'camera.png') - original_img = imread(img_path) + original_img = ioImage(imread(img_path)) original_img_str = original_img._repr_png_() with NamedTemporaryFile(suffix='.png') as temp_png: From 7df2ef1e850cefd5fcbd62d1dd27bae1bef975a1 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Wed, 21 Aug 2013 17:50:21 +0200 Subject: [PATCH 518/736] Add tests for image tags. --- skimage/io/tests/test_image.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 skimage/io/tests/test_image.py diff --git a/skimage/io/tests/test_image.py b/skimage/io/tests/test_image.py new file mode 100644 index 00000000..6c54695b --- /dev/null +++ b/skimage/io/tests/test_image.py @@ -0,0 +1,17 @@ +from skimage.io import Image + +from numpy.testing import assert_equal, assert_array_equal + +def test_tags(): + f = Image([1, 2, 3], foo='bar', sigma='delta') + g = Image([3, 2, 1], sun='moon') + h = Image([1, 1, 1]) + + assert_equal(f.tags['foo'], 'bar') + assert_array_equal((g + 2).tags['sun'], 'moon') + assert_equal(h.tags, {}) + +if __name__ == "__main__": + from numpy.testing import run_module_suite + run_module_suite() + From 22873b803f3b1c805a2d11fc23656c9f1f6f6137 Mon Sep 17 00:00:00 2001 From: Alexis Mignon Date: Thu, 22 Aug 2013 15:05:22 +0200 Subject: [PATCH 519/736] Added non rotation invariant local binary patterns --- skimage/feature/_texture.pyx | 72 +++++++++++++++++++++++++++++------- skimage/feature/texture.py | 3 ++ 2 files changed, 62 insertions(+), 13 deletions(-) diff --git a/skimage/feature/_texture.pyx b/skimage/feature/_texture.pyx index 9daa215d..a36e2a12 100644 --- a/skimage/feature/_texture.pyx +++ b/skimage/feature/_texture.pyx @@ -90,12 +90,13 @@ def _local_binary_pattern(double[:, ::1] image, the angular space). R : float Radius of circle (spatial resolution of the operator). - method : {'D', 'R', 'U', 'V'} + method : {'D', 'R', 'U', 'N', 'V'} Method to determine the pattern. * 'D': 'default' * 'R': 'ror' * 'U': 'uniform' + * 'N': 'nri_uniform' * 'V': 'var' Returns @@ -125,6 +126,9 @@ def _local_binary_pattern(double[:, ::1] image, cdef double lbp cdef Py_ssize_t r, c, changes, i + cdef Py_ssize_t r, c, changes, i, rot_index, n_ones + cdef cnp.int8_t first_zero, first_one + for r in range(image.shape[0]): for c in range(image.shape[1]): for i in range(P): @@ -146,19 +150,61 @@ def _local_binary_pattern(double[:, ::1] image, changes = 0 for i in range(P - 1): changes += abs(signed_texture[i] - signed_texture[i + 1]) - - if changes <= 2: - for i in range(P): - lbp += signed_texture[i] - else: - lbp = P + 1 - - if method == 'V': - var = np.var(texture) - if var != 0: - lbp /= var + if method == 'N': + # Non rotation invariant LBP. + # for P there are P + 1 ror and uniform patterns + # ex: P = 4 + # 1: 0 0 0 0 + # 2: 0 0 0 1 -> 0 0 1 0, 0 1 0 0, 1 0 0 0 + # 3: 0 0 1 1 -> 0 1 1 0, 1 1 0 0, 1 0 0 1 + # 4: 0 1 1 1 -> 1 1 1 0, 1 1 0 1, 1 0 1 1 + # 5: 1 1 1 1 + # The first and last patterns are always invariant under + # rotation. The (P - 1) remaining patterns have P rotated + # variants hence we have P * (P - 1) + 2 uniform patterns + # to which we add the non uniform pattern. + if changes <= 2: + # We have a uniform pattern + n_ones = 0 # determies the number of ones + first_one = -1 # position was the first one + first_zero = -1 # position of the first zero + for i in range(P): + if signed_texture[i]: + n_ones += 1 + if first_one == -1: + first_one = i + else: + if first_zero == -1: + first_zero = i + if n_ones == 0: + lbp = 0 + elif n_ones == P: + lbp = P * (P - 1) + 1 + else: + # There are (P - n_ones) patterns starting with 0. + # This patterns are indexed starting from the + # position where all zeros are on the right and + # applying circular right shifts. + if first_zero == 0: + var_index = P - n_ones - first_one + else: + var_index = P - first_zero + lbp = 1 + (n_ones - 1) * P + var_index + else: # changes > 2 + lbp = P * (P - 1) + 2 + else: # method != 'N' + if changes <= 2: + for i in range(P): + lbp += signed_texture[i] else: - lbp = np.nan + lbp = P + 1 + + if method == 'V': + var = np.var(texture) + if var != 0: + lbp /= var + else: + lbp = np.nan else: # method == 'default' for i in range(P): diff --git a/skimage/feature/texture.py b/skimage/feature/texture.py index 7549cfdd..5a162333 100644 --- a/skimage/feature/texture.py +++ b/skimage/feature/texture.py @@ -248,6 +248,8 @@ def local_binary_pattern(image, P, R, method='default'): * 'uniform': improved rotation invariance with uniform patterns and finer quantization of the angular space which is gray scale and rotation invariant. + * 'nri_uniform': non rotation-invariant uniform patterns variant + which is only gray scale invariant. * 'var': rotation invariant variance measures of the contrast of local image texture which is rotation but not gray scale invariant. @@ -269,6 +271,7 @@ def local_binary_pattern(image, P, R, method='default'): 'default': ord('D'), 'ror': ord('R'), 'uniform': ord('U'), + 'nri_uniform': ord('N'), 'var': ord('V') } image = np.ascontiguousarray(image, dtype=np.double) From eae691cf2177132527fd399a9c1ee83329fdcacd Mon Sep 17 00:00:00 2001 From: Alexis Mignon Date: Thu, 22 Aug 2013 15:24:49 +0200 Subject: [PATCH 520/736] Corrected duplicate variable declarations, variable name change from 'var_index' to 'rot_index' and corrected minor bugs --- skimage/feature/_texture.pyx | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/skimage/feature/_texture.pyx b/skimage/feature/_texture.pyx index a36e2a12..7b052d08 100644 --- a/skimage/feature/_texture.pyx +++ b/skimage/feature/_texture.pyx @@ -126,7 +126,7 @@ def _local_binary_pattern(double[:, ::1] image, cdef double lbp cdef Py_ssize_t r, c, changes, i - cdef Py_ssize_t r, c, changes, i, rot_index, n_ones + cdef Py_ssize_t rot_index, n_ones cdef cnp.int8_t first_zero, first_one for r in range(image.shape[0]): @@ -145,7 +145,7 @@ def _local_binary_pattern(double[:, ::1] image, lbp = 0 # if method == 'uniform' or method == 'var': - if method == 'U' or method == 'V': + if method == 'U' or method == 'N' or method == 'V': # determine number of 0 - 1 changes changes = 0 for i in range(P - 1): @@ -181,15 +181,16 @@ def _local_binary_pattern(double[:, ::1] image, elif n_ones == P: lbp = P * (P - 1) + 1 else: - # There are (P - n_ones) patterns starting with 0. + # There are (P - n_ones) patterns starting with 0 + # followed by n_ones patterns starting with 1. # This patterns are indexed starting from the # position where all zeros are on the right and # applying circular right shifts. if first_zero == 0: - var_index = P - n_ones - first_one + rot_index = P - n_ones - first_one else: - var_index = P - first_zero - lbp = 1 + (n_ones - 1) * P + var_index + rot_index = P - first_zero + lbp = 1 + (n_ones - 1) * P + rot_index else: # changes > 2 lbp = P * (P - 1) + 2 else: # method != 'N' From 0c2f0dc532622bb9bb86c66eb6cdb86357f8ca98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Sun, 25 Aug 2013 10:34:25 +0200 Subject: [PATCH 521/736] Review the procedure --- CONTRIBUTING.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CONTRIBUTING.txt b/CONTRIBUTING.txt index 331b4ed8..d6e2746f 100644 --- a/CONTRIBUTING.txt +++ b/CONTRIBUTING.txt @@ -167,6 +167,8 @@ successfully passes all tests. To do so, * Go to your `profile page `__ and switch on your scikit-image fork + * Go to your scikit-image fork on github, and activate the travis-ci's hook in settings. + It corresponds to steps one and two in `Travis-CI documentation `__ (Step three is already done in scikit-image). From cb28bba6ee642828df473383ea469a6aa46ca59c Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Sun, 25 Aug 2013 11:04:50 +0200 Subject: [PATCH 522/736] Add note describing array copy if discontiguous --- skimage/util/unique.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/skimage/util/unique.py b/skimage/util/unique.py index 6034d9a4..ecd6e8cc 100644 --- a/skimage/util/unique.py +++ b/skimage/util/unique.py @@ -18,6 +18,12 @@ def unique_rows(ar): ------ ValueError : if `ar` is not two-dimensional. + Notes + ----- + The function will generate a copy of `ar` if it is not + C-contiguous, which will negatively affect performance for large + input arrays. + Examples -------- >>> ar = np.array([[1, 0, 1], From 072eb7640a42b249633f3f495156a3d8a203300e Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Sun, 25 Aug 2013 11:06:31 +0200 Subject: [PATCH 523/736] Add docstring note explaining coord use case --- skimage/util/unique.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/skimage/util/unique.py b/skimage/util/unique.py index ecd6e8cc..e65c05ce 100644 --- a/skimage/util/unique.py +++ b/skimage/util/unique.py @@ -4,6 +4,9 @@ import numpy as np def unique_rows(ar): """Remove repeated rows from a 2D array. + In particular, if given an array of coordinates of shape + (Npoints, Ndim), it will remove repeated points. + Parameters ---------- ar : 2D np.ndarray From 944b45ad693f9156a3392ec8410d61ccc7999a0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Sun, 25 Aug 2013 12:01:35 +0200 Subject: [PATCH 524/736] DOC: this step seems to be useless --- CONTRIBUTING.txt | 2 -- 1 file changed, 2 deletions(-) diff --git a/CONTRIBUTING.txt b/CONTRIBUTING.txt index d6e2746f..331b4ed8 100644 --- a/CONTRIBUTING.txt +++ b/CONTRIBUTING.txt @@ -167,8 +167,6 @@ successfully passes all tests. To do so, * Go to your `profile page `__ and switch on your scikit-image fork - * Go to your scikit-image fork on github, and activate the travis-ci's hook in settings. - It corresponds to steps one and two in `Travis-CI documentation `__ (Step three is already done in scikit-image). From bd2a61baf86d75cf95b97066ab8a92f796083ea3 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Sun, 25 Aug 2013 12:42:12 +0200 Subject: [PATCH 525/736] Add PyAMG to the optional dependencies list --- DEPENDS.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/DEPENDS.txt b/DEPENDS.txt index 227283e2..04b745c2 100644 --- a/DEPENDS.txt +++ b/DEPENDS.txt @@ -42,6 +42,10 @@ functionality is only available with the following installed: The ``freeimage`` plugin provides support for reading various types of image file formats, including multi-page TIFFs. +* `PyAMG `__ + The ``pyamg`` module is used for the fast `cg_mg` mode of random + walker segmentation. + Testing requirements -------------------- * `Nose `__ From 13e83be692bff2bf2c51050b920c11ed9f8c5bf7 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Sun, 25 Aug 2013 12:44:19 +0200 Subject: [PATCH 526/736] Update PyAMG URL (no longer in Google Code) --- skimage/segmentation/random_walker_segmentation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skimage/segmentation/random_walker_segmentation.py b/skimage/segmentation/random_walker_segmentation.py index bd143ed6..9ef3a8fc 100644 --- a/skimage/segmentation/random_walker_segmentation.py +++ b/skimage/segmentation/random_walker_segmentation.py @@ -214,7 +214,7 @@ def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True, - 'cg_mg' (conjugate gradient with multigrid preconditioner): a preconditioner is computed using a multigrid solver, then the solution is computed with the Conjugate Gradient method. This mode - requires that the pyamg module (http://code.google.com/p/pyamg/) is + requires that the pyamg module (http://pyamg.org/) is installed. For images of size > 512x512, this is the recommended (fastest) mode. @@ -379,7 +379,7 @@ def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True, if mode == 'cg_mg': if not amg_loaded: warnings.warn( - """pyamg (http://code.google.com/p/pyamg/)) is needed to use + """pyamg (http://pyamg.org/)) is needed to use this mode, but is not installed. The 'cg' mode will be used instead.""") X = _solve_cg(lap_sparse, B, tol=tol, From 082586c10cec63d2e8e8c0a4b1423c0f7b2850ae Mon Sep 17 00:00:00 2001 From: Emmanuelle Gouillart Date: Sun, 25 Aug 2013 14:50:55 +0200 Subject: [PATCH 527/736] Added a wrapper around ndimage's Gaussian filter This version of the Gaussian filter * uses 'nearest' as the default boundary mode. This can be discussed, but I had the impression that for images this is the most relevant mode ('extending' boundaries) * has a `multichannel` keyword, so that each color channel can be filtered separately. For now no attempt is made at guessing whether the image has color channels or not. --- skimage/filter/__init__.py | 2 + skimage/filter/_gaussian.py | 91 +++++++++++++++++++++++++++ skimage/filter/tests/test_gaussian.py | 29 +++++++++ 3 files changed, 122 insertions(+) create mode 100644 skimage/filter/_gaussian.py create mode 100644 skimage/filter/tests/test_gaussian.py diff --git a/skimage/filter/__init__.py b/skimage/filter/__init__.py index 1e13a87e..67088b20 100644 --- a/skimage/filter/__init__.py +++ b/skimage/filter/__init__.py @@ -1,5 +1,6 @@ from .lpi_filter import inverse, wiener, LPIFilter2D from .ctmf import median_filter +from ._gaussian import gaussian_filter from ._canny import canny from .edges import (sobel, hsobel, vsobel, scharr, hscharr, vscharr, prewitt, hprewitt, vprewitt, roberts , roberts_positive_diagonal, @@ -16,6 +17,7 @@ __all__ = ['inverse', 'wiener', 'LPIFilter2D', 'median_filter', + 'gaussian_filter', 'canny', 'sobel', 'hsobel', diff --git a/skimage/filter/_gaussian.py b/skimage/filter/_gaussian.py new file mode 100644 index 00000000..6f0f9a64 --- /dev/null +++ b/skimage/filter/_gaussian.py @@ -0,0 +1,91 @@ +from scipy import ndimage +from skimage.util import img_as_float + +__all__ = ['gaussian_filter'] + + +def gaussian_filter(image, sigma, output=None, mode='nearest', cval=0, + multichannel=False): + """ + Multi-dimensional Gaussian filter + + Parameters + ---------- + + image : array-like + input image to filter + sigma : scalar or sequence of scalars + standard deviation for Gaussian kernel. The standard + deviations of the Gaussian filter are given for each axis as a + sequence, or as a single number, in which case it is equal for + all axes. + output : array, optional + The ``output`` parameter passes an array in which to store the + filter output. + mode : {'reflect','constant','nearest','mirror', 'wrap'}, optional + The ``mode`` parameter determines how the array borders are + handled, where ``cval`` is the value when mode is equal to + 'constant'. Default is 'nearest'. + cval : scalar, optional + Value to fill past edges of input if ``mode`` is 'constant'. Default + is 0.0 + multichannel : bool, optional (default: False) + Whether the last axis of the image is to be interpreted as multiple + channels. Only 3 channels are supported. If `None`, the function will + attempt to guess this, and raise a warning if ambiguous, when the + array has shape (M, N, 3). + + + Returns + ------- + + filtered_image : ndarray + the filtered array + + Notes + ----- + + This function is a wrapper around :func:`scipy.ndimage.gaussian_filter`. + + Integer arrays are converted to float. + + The multi-dimensional filter is implemented as a sequence of + one-dimensional convolution filters. The intermediate arrays are + stored in the same data type as the output. Therefore, for output + types with a limited precision, the results may be imprecise + because intermediate results may be stored with insufficient + precision. + + Examples + -------- + + >>> a = np.zeros((3, 3)) + >>> a[1, 1] = 1 + >>> a + array([[ 0., 0., 0.], + [ 0., 1., 0.], + [ 0., 0., 0.]]) + >>> gaussian_filter(a, sigma=0.4) # mild smoothing + array([[ 0.00163116, 0.03712502, 0.00163116], + [ 0.03712502, 0.84496158, 0.03712502], + [ 0.00163116, 0.03712502, 0.00163116]]) + >>> gaussian_filter(a, sigma=1) # more smooting + array([[ 0.05855018, 0.09653293, 0.05855018], + [ 0.09653293, 0.15915589, 0.09653293], + [ 0.05855018, 0.09653293, 0.05855018]]) + >>> # Several modes are possible for handling boundaries + >>> gaussian_filter(a, sigma=1, mode='reflect') + array([[ 0.08767308, 0.12075024, 0.08767308], + [ 0.12075024, 0.16630671, 0.12075024], + [ 0.08767308, 0.12075024, 0.08767308]]) + >>> # For RGB images, each is filtered separately + >>> from skimage.data import lena + >>> image = lena() + >>> filtered_lena = gaussian_filter(image, sigma=1, multichannel=True) + """ + if multichannel: + # do not filter across channels + ndim = image.ndim + sigma = [sigma] * (ndim - 1) + [0] + image = img_as_float(image) + return ndimage.gaussian_filter(image, sigma, mode=mode, cval=cval) diff --git a/skimage/filter/tests/test_gaussian.py b/skimage/filter/tests/test_gaussian.py new file mode 100644 index 00000000..afae5079 --- /dev/null +++ b/skimage/filter/tests/test_gaussian.py @@ -0,0 +1,29 @@ +import numpy as np +from skimage.filter._gaussian import gaussian_filter + + +def test_null_sigma(): + a = np.zeros((3, 3)) + a[1, 1] = 1. + assert np.all(gaussian_filter(a, 0) == a) + + +def test_energy_decrease(): + a = np.zeros((3, 3)) + a[1, 1] = 1. + gaussian_a = gaussian_filter(a, sigma=1, mode='reflect') + assert gaussian_a.std() < a.std() + + +def test_multichannel(): + a = np.zeros((3, 3, 3)) + a[1, 1] = np.arange(1, 4) + gaussian_rgb_a = gaussian_filter(a, sigma=1, mode='reflect', + multichannel=True) + assert np.allclose([a[..., i].mean() for i in range(3)], + [gaussian_rgb_a[..., i].mean() for i in range(3)]) + + +if __name__ == "__main__": + from numpy import testing + testing.run_module_suite() From c68dab766e21f3583100d94a98a9a5cec08955ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Sun, 25 Aug 2013 14:52:30 +0200 Subject: [PATCH 528/736] DOC: add titles to plots --- doc/examples/plot_watershed.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/examples/plot_watershed.py b/doc/examples/plot_watershed.py index 88f54ba7..f003c122 100644 --- a/doc/examples/plot_watershed.py +++ b/doc/examples/plot_watershed.py @@ -52,8 +52,11 @@ fig, axes = plt.subplots(ncols=3, figsize=(8, 2.7)) ax0, ax1, ax2 = axes ax0.imshow(image, cmap=plt.cm.gray, interpolation='nearest') +ax0.set_title('Overlapping objects') ax1.imshow(-distance, cmap=plt.cm.jet, interpolation='nearest') +ax1.set_title('Distances') ax2.imshow(labels, cmap=plt.cm.spectral, interpolation='nearest') +ax2.set_title('Separated objects') for ax in axes: ax.axis('off') From 7b022c8b5fc5b6346fb6bdf3caabec263b7fd579 Mon Sep 17 00:00:00 2001 From: Alexis Mignon Date: Sun, 25 Aug 2013 14:56:13 +0200 Subject: [PATCH 529/736] Finalized changes. Added reference for non rotation-invariant uniform patterns. Added test case. Modified comments to clarify the process, and changed the code to make it more consistent with explanations and actuall encoding --- skimage/feature/_texture.pyx | 56 +++++++++++++++++---------- skimage/feature/tests/test_texture.py | 11 ++++++ skimage/feature/texture.py | 6 ++- 3 files changed, 52 insertions(+), 21 deletions(-) diff --git a/skimage/feature/_texture.pyx b/skimage/feature/_texture.pyx index 7b052d08..6caa7ea3 100644 --- a/skimage/feature/_texture.pyx +++ b/skimage/feature/_texture.pyx @@ -151,18 +151,39 @@ def _local_binary_pattern(double[:, ::1] image, for i in range(P - 1): changes += abs(signed_texture[i] - signed_texture[i + 1]) if method == 'N': - # Non rotation invariant LBP. - # for P there are P + 1 ror and uniform patterns - # ex: P = 4 - # 1: 0 0 0 0 - # 2: 0 0 0 1 -> 0 0 1 0, 0 1 0 0, 1 0 0 0 - # 3: 0 0 1 1 -> 0 1 1 0, 1 1 0 0, 1 0 0 1 - # 4: 0 1 1 1 -> 1 1 1 0, 1 1 0 1, 1 0 1 1 - # 5: 1 1 1 1 - # The first and last patterns are always invariant under - # rotation. The (P - 1) remaining patterns have P rotated - # variants hence we have P * (P - 1) + 2 uniform patterns - # to which we add the non uniform pattern. + # Uniform local binary patterns are defined as patterns + # with at most 2 value changes (from 0 to 1 or from 1 to + # 0). Uniform patterns can be caraterized by their number + # `n_ones` of 1. The possible values for `n_ones` range + # from 0 to P. + # Here is an example for P = 4: + # n_ones=0: 0000 + # n_ones=1: 0001, 1000, 0100, 0010 + # n_ones=2: 0011, 1001, 1100, 0110 + # n_ones=3: 0111, 1011, 1101, 1110 + # n_ones=4: 1111 + # + # For a pattern of size P there are 2 constant patterns + # corresponding to n_ones=0 and n_ones=P. For each other + # value of `n_ones` , i.e n_ones=[1..P-1], there are P + # possible patterns which are related to each other through + # circular permutations. The total number of uniform + # patterns is thus (2 + P * (P - 1)). + # Given any pattern (uniform or not) we must be able to + # associate a unique code: + # 1. Constant patterns patterns (with n_ones=0 and + # n_ones=P) and non uniform patterns are given fixed + # code values. + # 2. Other uniform patterns are indexed considering the + # value of n_ones, and an index called 'rot_index' + # reprenting the number of circular right shifts + # required to obtain the pattern starting from a + # reference position (corresponding to all zeros stacked + # on the right). This number of rotations (or circular + # right shifts) 'rot_index' is efficiently computed by + # considering the positions of the first 1 and the first + # 0 found in the pattern. + if changes <= 2: # We have a uniform pattern n_ones = 0 # determies the number of ones @@ -181,15 +202,10 @@ def _local_binary_pattern(double[:, ::1] image, elif n_ones == P: lbp = P * (P - 1) + 1 else: - # There are (P - n_ones) patterns starting with 0 - # followed by n_ones patterns starting with 1. - # This patterns are indexed starting from the - # position where all zeros are on the right and - # applying circular right shifts. - if first_zero == 0: - rot_index = P - n_ones - first_one + if first_one == 0: + rot_index = n_ones - first_zero else: - rot_index = P - first_zero + rot_index = P - first_one lbp = 1 + (n_ones - 1) * P + rot_index else: # changes > 2 lbp = P * (P - 1) + 2 diff --git a/skimage/feature/tests/test_texture.py b/skimage/feature/tests/test_texture.py index d48a14f7..e4fb6acb 100644 --- a/skimage/feature/tests/test_texture.py +++ b/skimage/feature/tests/test_texture.py @@ -199,5 +199,16 @@ class TestLBP(): np.testing.assert_array_almost_equal(lbp, ref) + def test_nri_uniform(self): + lbp = local_binary_pattern(self.image, 8, 1, 'nri_uniform') + ref = np.array([[ 0, 54, 0, 57, 12, 57], + [34, 0, 58, 58, 3, 22], + [58, 57, 15, 50, 0, 47], + [10, 3, 40, 42, 35, 0], + [57, 7, 57, 58, 0, 56], + [ 9, 58, 0, 57, 7, 14]]) + np.testing.assert_array_almost_equal(lbp, ref) + + if __name__ == '__main__': np.testing.run_module_suite() diff --git a/skimage/feature/texture.py b/skimage/feature/texture.py index 5a162333..1ead9f7d 100644 --- a/skimage/feature/texture.py +++ b/skimage/feature/texture.py @@ -249,7 +249,7 @@ def local_binary_pattern(image, P, R, method='default'): finer quantization of the angular space which is gray scale and rotation invariant. * 'nri_uniform': non rotation-invariant uniform patterns variant - which is only gray scale invariant. + which is only gray scale invariant [2]. * 'var': rotation invariant variance measures of the contrast of local image texture which is rotation but not gray scale invariant. @@ -265,6 +265,10 @@ def local_binary_pattern(image, P, R, method='default'): Timo Ojala, Matti Pietikainen, Topi Maenpaa. http://www.rafbis.it/biplab15/images/stories/docenti/Danielriccio/\ Articoliriferimento/LBP.pdf, 2002. + .. [2] Face recognition with local binary patterns. + Timo Ahonen, Abdenour Hadid, Matti PietikainenAhonen, + http://masters.donntu.edu.ua/2011/frt/dyrul/library/article8.pdf, + 2004. """ methods = { From 15b4a4d979f73a7b1d24220a767d3f9e02697c37 Mon Sep 17 00:00:00 2001 From: Emmanuelle Gouillart Date: Sun, 25 Aug 2013 15:04:22 +0200 Subject: [PATCH 530/736] DOC: corrected description of multichannel parameter --- skimage/filter/_gaussian.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/skimage/filter/_gaussian.py b/skimage/filter/_gaussian.py index 6f0f9a64..0d6cdfbe 100644 --- a/skimage/filter/_gaussian.py +++ b/skimage/filter/_gaussian.py @@ -31,9 +31,8 @@ def gaussian_filter(image, sigma, output=None, mode='nearest', cval=0, is 0.0 multichannel : bool, optional (default: False) Whether the last axis of the image is to be interpreted as multiple - channels. Only 3 channels are supported. If `None`, the function will - attempt to guess this, and raise a warning if ambiguous, when the - array has shape (M, N, 3). + channels. If True, each channel is filtered separately (channels are + not mixed together). Returns From 515335e9175d8ee82972504313ca8bbc2b626052 Mon Sep 17 00:00:00 2001 From: Alexis Mignon Date: Sun, 25 Aug 2013 15:05:35 +0200 Subject: [PATCH 531/736] Corrected name in reference [2] --- skimage/feature/texture.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/feature/texture.py b/skimage/feature/texture.py index 1ead9f7d..94e5343c 100644 --- a/skimage/feature/texture.py +++ b/skimage/feature/texture.py @@ -266,7 +266,7 @@ def local_binary_pattern(image, P, R, method='default'): http://www.rafbis.it/biplab15/images/stories/docenti/Danielriccio/\ Articoliriferimento/LBP.pdf, 2002. .. [2] Face recognition with local binary patterns. - Timo Ahonen, Abdenour Hadid, Matti PietikainenAhonen, + Timo Ahonen, Abdenour Hadid, Matti Pietikainen, http://masters.donntu.edu.ua/2011/frt/dyrul/library/article8.pdf, 2004. """ From b4242ca3fb1005ac1839b63ac30cf87367f02015 Mon Sep 17 00:00:00 2001 From: Emmanuelle Gouillart Date: Sun, 25 Aug 2013 16:41:37 +0200 Subject: [PATCH 532/736] [BUG] iterable sigma and multichannel + docstring improvement --- skimage/filter/_gaussian.py | 20 ++++++++++++-------- skimage/filter/tests/test_gaussian.py | 7 +++++++ 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/skimage/filter/_gaussian.py b/skimage/filter/_gaussian.py index 0d6cdfbe..8cb2be1f 100644 --- a/skimage/filter/_gaussian.py +++ b/skimage/filter/_gaussian.py @@ -1,3 +1,5 @@ +import collections as coll +import numpy as np from scipy import ndimage from skimage.util import img_as_float @@ -13,7 +15,8 @@ def gaussian_filter(image, sigma, output=None, mode='nearest', cval=0, ---------- image : array-like - input image to filter + input image (grayscale or color) to filter. If color channels are + to be filtered separately, use ``multichannel=True``. sigma : scalar or sequence of scalars standard deviation for Gaussian kernel. The standard deviations of the Gaussian filter are given for each axis as a @@ -22,19 +25,18 @@ def gaussian_filter(image, sigma, output=None, mode='nearest', cval=0, output : array, optional The ``output`` parameter passes an array in which to store the filter output. - mode : {'reflect','constant','nearest','mirror', 'wrap'}, optional - The ``mode`` parameter determines how the array borders are - handled, where ``cval`` is the value when mode is equal to + mode : {'reflect', 'constant', 'nearest', 'mirror', 'wrap'}, optional + The `mode` parameter determines how the array borders are + handled, where `cval` is the value when mode is equal to 'constant'. Default is 'nearest'. cval : scalar, optional - Value to fill past edges of input if ``mode`` is 'constant'. Default + Value to fill past edges of input if `mode` is 'constant'. Default is 0.0 multichannel : bool, optional (default: False) Whether the last axis of the image is to be interpreted as multiple channels. If True, each channel is filtered separately (channels are not mixed together). - Returns ------- @@ -84,7 +86,9 @@ def gaussian_filter(image, sigma, output=None, mode='nearest', cval=0, """ if multichannel: # do not filter across channels - ndim = image.ndim - sigma = [sigma] * (ndim - 1) + [0] + if not isinstance(sigma, coll.Iterable): + sigma = [sigma] * (image.ndim - 1) + if len(sigma) != image.ndim: + sigma = np.concatenate((np.asarray(sigma), [0])) image = img_as_float(image) return ndimage.gaussian_filter(image, sigma, mode=mode, cval=cval) diff --git a/skimage/filter/tests/test_gaussian.py b/skimage/filter/tests/test_gaussian.py index afae5079..77c01a63 100644 --- a/skimage/filter/tests/test_gaussian.py +++ b/skimage/filter/tests/test_gaussian.py @@ -20,6 +20,13 @@ def test_multichannel(): a[1, 1] = np.arange(1, 4) gaussian_rgb_a = gaussian_filter(a, sigma=1, mode='reflect', multichannel=True) + # Check that the mean value is conserved in each channel + # (color channels are not mixed together) + assert np.allclose([a[..., i].mean() for i in range(3)], + [gaussian_rgb_a[..., i].mean() for i in range(3)]) + # Iterable sigma + gaussian_rgb_a = gaussian_filter(a, sigma=[1, 2], mode='reflect', + multichannel=True) assert np.allclose([a[..., i].mean() for i in range(3)], [gaussian_rgb_a[..., i].mean() for i in range(3)]) From f4a097713cb3b617c11e1de8efaceacdae507242 Mon Sep 17 00:00:00 2001 From: Alexis Mignon Date: Sun, 25 Aug 2013 17:13:25 +0200 Subject: [PATCH 533/736] Changed link to the reference [2] --- skimage/feature/texture.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/feature/texture.py b/skimage/feature/texture.py index 94e5343c..f9bf6c9f 100644 --- a/skimage/feature/texture.py +++ b/skimage/feature/texture.py @@ -267,7 +267,7 @@ def local_binary_pattern(image, P, R, method='default'): Articoliriferimento/LBP.pdf, 2002. .. [2] Face recognition with local binary patterns. Timo Ahonen, Abdenour Hadid, Matti Pietikainen, - http://masters.donntu.edu.ua/2011/frt/dyrul/library/article8.pdf, + http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.214.6851, 2004. """ From 9795d3fad4bb4c039d1515bece5e85cc88d76999 Mon Sep 17 00:00:00 2001 From: Emmanuelle Gouillart Date: Thu, 29 Aug 2013 23:14:30 +0200 Subject: [PATCH 534/736] Gaussian filter function: changed the default value of multichannel Now we try to guess automatically whether the image is grayscale or RGB --- skimage/filter/_gaussian.py | 23 +++++++++++++++++------ skimage/filter/tests/test_gaussian.py | 8 +++++++- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/skimage/filter/_gaussian.py b/skimage/filter/_gaussian.py index 8cb2be1f..af63ba10 100644 --- a/skimage/filter/_gaussian.py +++ b/skimage/filter/_gaussian.py @@ -1,13 +1,16 @@ import collections as coll import numpy as np from scipy import ndimage -from skimage.util import img_as_float +import warnings + +from ..util import img_as_float +from ..color import guess_spatial_dimensions __all__ = ['gaussian_filter'] def gaussian_filter(image, sigma, output=None, mode='nearest', cval=0, - multichannel=False): + multichannel=None): """ Multi-dimensional Gaussian filter @@ -15,8 +18,7 @@ def gaussian_filter(image, sigma, output=None, mode='nearest', cval=0, ---------- image : array-like - input image (grayscale or color) to filter. If color channels are - to be filtered separately, use ``multichannel=True``. + input image (grayscale or color) to filter. sigma : scalar or sequence of scalars standard deviation for Gaussian kernel. The standard deviations of the Gaussian filter are given for each axis as a @@ -32,10 +34,12 @@ def gaussian_filter(image, sigma, output=None, mode='nearest', cval=0, cval : scalar, optional Value to fill past edges of input if `mode` is 'constant'. Default is 0.0 - multichannel : bool, optional (default: False) + multichannel : bool, optional (default: None) Whether the last axis of the image is to be interpreted as multiple channels. If True, each channel is filtered separately (channels are - not mixed together). + not mixed together). Only 3 channels are supported. If `None`, + the function will attempt to guess this, and raise a warning if + ambiguous, when the array has shape (M, N, 3). Returns ------- @@ -84,6 +88,13 @@ def gaussian_filter(image, sigma, output=None, mode='nearest', cval=0, >>> image = lena() >>> filtered_lena = gaussian_filter(image, sigma=1, multichannel=True) """ + spatial_dims = guess_spatial_dimensions(image) + if spatial_dims is None and multichannel is None: + msg = ("Images with dimensions (M, N, 3) are interpreted as 2D+RGB" + + " by default. Use `multichannel=False` to interpret as " + + " 3D image with last dimension of length 3.") + warnings.warn(RuntimeWarning(msg)) + multichannel = True if multichannel: # do not filter across channels if not isinstance(sigma, coll.Iterable): diff --git a/skimage/filter/tests/test_gaussian.py b/skimage/filter/tests/test_gaussian.py index 77c01a63..9118bfae 100644 --- a/skimage/filter/tests/test_gaussian.py +++ b/skimage/filter/tests/test_gaussian.py @@ -16,12 +16,18 @@ def test_energy_decrease(): def test_multichannel(): - a = np.zeros((3, 3, 3)) + a = np.zeros((5, 5, 3)) a[1, 1] = np.arange(1, 4) gaussian_rgb_a = gaussian_filter(a, sigma=1, mode='reflect', multichannel=True) # Check that the mean value is conserved in each channel # (color channels are not mixed together) + assert np.allclose([a[..., i].mean() for i in range(3)], + [gaussian_rgb_a[..., i].mean() for i in range(3)]) + # Test multichannel = None + gaussian_rgb_a = gaussian_filter(a, sigma=1, mode='reflect') + # Check that the mean value is conserved in each channel + # (color channels are not mixed together) assert np.allclose([a[..., i].mean() for i in range(3)], [gaussian_rgb_a[..., i].mean() for i in range(3)]) # Iterable sigma From 5eba5a42a7e3f0c4d8ff3e50c9dff98a90d7b950 Mon Sep 17 00:00:00 2001 From: "Josh Warner (Mac)" Date: Sat, 16 Mar 2013 21:12:13 -0500 Subject: [PATCH 535/736] FEAT: Full 3d anisotropic implementation of marching cubes Also includes surface area calculation algorithm from generated mesh. Convenient output to visualize with `mayavi.mlab`. Efficient Cython implementation. --- skimage/measure/_marching_cubes.pyx | 924 ++++++++++++++++++++++++++++ skimage/measure/marching_cubes.py | 200 ++++++ 2 files changed, 1124 insertions(+) create mode 100644 skimage/measure/_marching_cubes.pyx create mode 100644 skimage/measure/marching_cubes.py diff --git a/skimage/measure/_marching_cubes.pyx b/skimage/measure/_marching_cubes.pyx new file mode 100644 index 00000000..8858acf4 --- /dev/null +++ b/skimage/measure/_marching_cubes.pyx @@ -0,0 +1,924 @@ +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False +import numpy as np +cimport numpy as cnp + + +cdef inline double _get_fraction(double from_value, double to_value, + double level): + if (to_value == from_value): + return 0 + return ((level - from_value) / (to_value - from_value)) + + +def iterate_and_store_3d(cnp.ndarray[double, ndim=3] arr, + double level, tuple sampling=(1., 1., 1.)): + """Iterate across the given array in a marching-cubes fashion, + looking for volumes with edges that cross 'level'. If such a volume is + found, appropriate triangulations are added to a growing list of + triangles to be returned by the function. + + If `sampling` is not provided, vertices are returned in the indexing + coordinate system (assuming all 3 spatial dimensions sampled equally). + If `sampling` is provided, vertices will be returned in volume coordinates + relative to the origin, regularly spaced as specified in each dimension. + + """ + if arr.shape[0] < 2 or arr.shape[1] < 2 or arr.shape[2] < 2: + raise ValueError("Input array must be at least 2x2x2.") + if len(sampling) != 3: + raise ValueError("`sampling` must be of form (double, double, double)") + + cdef list tri_list = [] + cdef list norm_list = [] + cdef Py_ssize_t n + cdef bint odd_sampling, plus_z + plus_z = False + if ((sampling == (1., 1., 1.)) or + (sampling == (1., 1., 1)) or + (sampling == (1., 1, 1.)) or + (sampling == (1, 1., 1.)) or + (sampling == (1, 1, 1.)) or + (sampling == (1., 1, 1)) or + (sampling == (1, 1., 1)) or + (sampling == (1, 1, 1))): + odd_sampling = False + else: + odd_sampling = True + + # The plan is to iterate a 2x2x2 cube across the input array. This means + # the upper-left corner of the cube needs to iterate across a sub-array + # of size one-less-large in each direction (so we can get away with no + # bounds checking in Cython). The cube is represented by eight vertices: + # v1, v2, ..., v8, oriented thus (see Lorensen, Figure 4): + # + # v8 ------ v7 + # / | / | y + # / | / | ^ z + # v4 ------ v3 | | / + # | v5 ----|- v6 |/ (note: NOT right handed!) + # | / | / ----> x + # |/ | / + # v1 ------ v2 + # + # We also maintain the current 2D coordinates for v1, and ensure the array + # is of type 'double' and is C-contiguous (last index varies fastest). + + # Coords start at (0, 0, 0). + cdef Py_ssize_t[3] coords + coords[0] = 0 + coords[1] = 0 + coords[2] = 0 + + # Extract doubles from `sampling` for speed + cdef double[3] sampling2 + sampling2[0] = sampling[0] + sampling2[1] = sampling[1] + sampling2[2] = sampling[2] + + # Calculate the number of iterations we'll need + cdef Py_ssize_t num_cube_steps = ((arr.shape[0] - 1) * + (arr.shape[1] - 1) * + (arr.shape[2] - 1)) + + cdef unsigned char cube_case = 0 + cdef tuple e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12 + cdef double v1, v2, v3, v4, v5, v6, v7, v8, r0, r1, c0, c1, d0, d1 + cdef Py_ssize_t x0, y0, z0, x1, y1, z1 + e5, e6, e7, e8 = (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0) + + for n in range(num_cube_steps): + # There are 255 unique values for `cube_case`. This algorithm follows + # the Lorensen paper in vertex and edge labeling, however, it should + # be noted that Lorensen used a left-handed coordinate system while + # NumPy uses a proper right handed system. Transforming between these + # coordinate systems was handled in the definitions of the cube + # vertices v1, v2, ..., v8. + # + # Refer to the paper, figure 4, for cube edge designations e1, ... e12 + + # Standard Py_ssize_t coordinates for indexing + x0, y0, z0 = coords[0], coords[1], coords[2] + x1, y1, z1 = x0 + 1, y0 + 1, z0 + 1 + + if odd_sampling: + # These doubles are the modified world coordinates; they are only + # calculated if non-default `sampling` provided. + r0 = coords[0] * sampling2[0] + c0 = coords[1] * sampling2[1] + d0 = coords[2] * sampling2[2] + r1 = r0 + sampling2[0] + c1 = c0 + sampling2[1] + d1 = d0 + sampling2[2] + else: + r0, c0, d0, r1, c1, d1 = x0, y0, z0, x1, y1, z1 + + # We use a right-handed coordinate system, UNlike the paper, but want + # to index in agreement - the coordinate adjustment takes place here. + v1 = arr[x0, y0, z0] + v2 = arr[x1, y0, z0] + v3 = arr[x1, y1, z0] + v4 = arr[x0, y1, z0] + v5 = arr[x0, y0, z1] + v6 = arr[x1, y0, z1] + v7 = arr[x1, y1, z1] + v8 = arr[x0, y1, z1] + + # Unique triangulation cases + cube_case = 0 + if (v1 > level): cube_case += 1 + if (v2 > level): cube_case += 2 + if (v3 > level): cube_case += 4 + if (v4 > level): cube_case += 8 + if (v5 > level): cube_case += 16 + if (v6 > level): cube_case += 32 + if (v7 > level): cube_case += 64 + if (v8 > level): cube_case += 128 + + if (cube_case != 0 and cube_case != 255): + # Only do anything if there's a plane intersecting the cube. + # Cases 0 and 255 are entirely below/above the contour. + + if cube_case > 127: + cube_case = 255 - cube_case + + # Calculate cube edges, to become triangulation vertices. + # If we moved in a convenient direction, save 1/3 of the effort by + # re-assigning prior results. + if plus_z: + # Reassign prior calculated edges + e1 = e5 + e2 = e6 + e3 = e7 + e4 = e8 + else: + # Calculate edges normally + if odd_sampling: + e1 = r0 + _get_fraction(v1, v2, level) * sampling2[0], c0, d0 + e2 = r1, c0 + _get_fraction(v2, v3, level) * sampling2[1], d0 + e3 = r0 + _get_fraction(v4, v3, level) * sampling2[0], c1, d0 + e4 = r0, c0 + _get_fraction(v1, v4, level) * sampling2[1], d0 + else: + e1 = r0 + _get_fraction(v1, v2, level), c0, d0 + e2 = r1, c0 + _get_fraction(v2, v3, level), d0 + e3 = r0 + _get_fraction(v4, v3, level), c1, d0 + e4 = r0, c0 + _get_fraction(v1, v4, level), d0 + + # These must be calculated at each point uunless we implemented a + # large, growing lookup table for all adjacent values; could save + # ~30% in terms of runtime at the expense of memory usage and + # much greater complexity. + if odd_sampling: + e5 = r0 + _get_fraction(v5, v6, level) * sampling2[0], c0, d1 + e6 = r1, c0 + _get_fraction(v6, v7, level) * sampling2[1], d1 + e7 = r0 + _get_fraction(v8, v7, level) * sampling2[0], c1, d1 + e8 = r0, c0 + _get_fraction(v5, v8, level) * sampling2[1], d1 + e9 = r0, c0, d0 + _get_fraction(v1, v5, level) * sampling2[2] + e10 = r1, c0, d0 + _get_fraction(v2, v6, level) * sampling2[2] + e11 = r0, c1, d0 + _get_fraction(v4, v8, level) * sampling2[2] + e12 = r1, c1, d0 + _get_fraction(v3, v7, level) * sampling2[2] + else: + e5 = r0 + _get_fraction(v5, v6, level), c0, d1 + e6 = r1, c0 + _get_fraction(v6, v7, level), d1 + e7 = r0 + _get_fraction(v8, v7, level), c1, d1 + e8 = r0, c0 + _get_fraction(v5, v8, level), d1 + e9 = r0, c0, d0 + _get_fraction(v1, v5, level) + e10 = r1, c0, d0 + _get_fraction(v2, v6, level) + e11 = r0, c1, d0 + _get_fraction(v4, v8, level) + e12 = r1, c1, d0 + _get_fraction(v3, v7, level) + + + # Append appropriate triangles to the growing output `tri_list` + _append_tris(tri_list, cube_case, e1, e2, e3, e4, e5, + e6, e7, e8, e9, e10, e11, e12) + + # Advance the coords indices + if coords[2] < arr.shape[2] - 2: + coords[2] += 1 + plus_z = True + elif coords[1] < arr.shape[1] - 2: + coords[1] += 1 + coords[2] = 0 + plus_z = False + else: + coords[0] += 1 + coords[1] = 0 + coords[2] = 0 + plus_z = False + + return tri_list + + +def _append_tris(list tri_list, unsigned char case, tuple e1, tuple e2, + tuple e3, tuple e4, tuple e5, tuple e6, tuple e7, tuple e8, + tuple e9, tuple e10, tuple e11, tuple e12): + # Permits recursive use for duplicated planes to conserve code - it's + # quite long enough as-is. + + if (case == 1): + # front lower left corner + tri_list.append([e1, e4, e9]) + elif (case == 2): + # front lower right corner + tri_list.append([e10, e2, e1]) + elif (case == 3): + # front lower plane + tri_list.append([e2, e4, e9]) + tri_list.append([e2, e9, e10]) + elif (case == 4): + # front upper right corner + tri_list.append([e12, e3, e2]) + elif (case == 5): + # lower left, upper right corners + _append_tris(tri_list, 1, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 4, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 6): + # front right plane + tri_list.append([e12, e3, e1]) + tri_list.append([e12, e1, e10]) + elif (case == 7): + # Shelf including v1, v2, v3 + tri_list.append([e3, e4, e12]) + tri_list.append([e4, e9, e12]) + tri_list.append([e12, e9, e10]) + elif (case == 8): + # front upper left corner + tri_list.append([e3, e11, e4]) + elif (case == 9): + # front left plane + tri_list.append([e3, e11, e9]) + tri_list.append([e3, e9, e1]) + elif (case == 10): + # upper left, lower right corners + _append_tris(tri_list, 2, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 8, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 11): + # Shelf including v4, v1, v2 + tri_list.append([e3, e11, e2]) + tri_list.append([e11, e10, e2]) + tri_list.append([e11, e9, e10]) + elif (case == 12): + # front upper plane + tri_list.append([e11, e4, e12]) + tri_list.append([e2, e4, e12]) + elif (case == 13): + # Shelf including v1, v4, v3 + tri_list.append([e11, e9, e12]) + tri_list.append([e12, e9, e1]) + tri_list.append([e12, e1, e2]) + elif (case == 14): + # Shelf including v2, v3, v4 + tri_list.append([e11, e10, e12]) + tri_list.append([e11, e4, e10]) + tri_list.append([e4, e1, e10]) + elif (case == 15): + # Plane parallel to x-axis through middle + tri_list.append([e11, e9, e12]) + tri_list.append([e12, e9, e10]) + elif (case == 16): + # back lower left corner + tri_list.append([e8, e9, e5]) + elif (case == 17): + # lower left plane + tri_list.append([e4, e1, e8]) + tri_list.append([e8, e1, e5]) + elif (case == 18): + # lower left back, lower right front corners + _append_tris(tri_list, 16, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 2, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 19): + # Shelf including v1, v2, v5 + tri_list.append([e8, e4, e2]) + tri_list.append([e8, e2, e10]) + tri_list.append([e8, e10, e5]) + elif (case == 20): + # lower left back, upper right front corners + _append_tris(tri_list, 16, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 4, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 21): + # lower left plane + upper right front corner, v1, v3, v5 + _append_tris(tri_list, 17, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 4, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 22): + # front right plane + lower left back corner, v2, v3, v5 + _append_tris(tri_list, 16, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 6, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 23): + # Rotated case 14 in the paper + tri_list.append([e3, e10, e8]) + tri_list.append([e3, e10, e12]) + tri_list.append([e8, e10, e5]) + tri_list.append([e3, e4, e8]) + elif (case == 24): + # upper front left, lower back left corners + _append_tris(tri_list, 16, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 8, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 25): + # Shelf including v1, v4, v5 + tri_list.append([e1, e5, e3]) + tri_list.append([e3, e8, e11]) + tri_list.append([e3, e5, e8]) + elif (case == 26): + # Three isolated corners + _append_tris(tri_list, 2, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 8, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 16, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 27): + # Full corner v1, case 9 in paper: (v1, v2, v4, v5) + tri_list.append([e11, e3, e2]) + tri_list.append([e11, e2, e10]) + tri_list.append([e10, e11, e8]) + tri_list.append([e8, e5, e10]) + elif (case == 28): + # upper front plane + corner v5 + _append_tris(tri_list, 16, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 12, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 29): + # special case of 11 in the paper: (v1, v3, v4, v5) + tri_list.append([e11, e5, e2]) + tri_list.append([e11, e12, e2]) + tri_list.append([e11, e5, e8]) + tri_list.append([e2, e1, e5]) + elif (case == 30): + # Shelf (v2, v3, v4) and lower left back corner + _append_tris(tri_list, 14, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 16, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 31): + # Shelf: (v6, v7, v8) by inversion + tri_list.append([e11, e12, e10]) + tri_list.append([e11, e8, e10]) + tri_list.append([e8, e10, e5]) + elif (case == 32): + # lower right back corner + tri_list.append([e6, e5, e10]) + elif (case == 33): + # lower right back, lower left front corners + _append_tris(tri_list, 1, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 32, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 34): + # lower right plane + tri_list.append([e1, e2, e5]) + tri_list.append([e2, e6, e5]) + elif (case == 35): + # Shelf: v1, v2, v6 + tri_list.append([e4, e2, e6]) + tri_list.append([e4, e9, e6]) + tri_list.append([e6, e9, e5]) + elif (case == 36): + # upper right front, lower right back corners + _append_tris(tri_list, 32, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 4, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 37): + # lower left front, upper right front, lower right back corners + _append_tris(tri_list, 32, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 4, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 1, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 38): + # Shelf: v2, v3, v6 + tri_list.append([e3, e1, e5]) + tri_list.append([e3, e5, e12]) + tri_list.append([e12, e5, e6]) + elif (case == 39): + # Full corner v2: (v1, v2, v3, v6) + tri_list.append([e3, e4, e5]) + tri_list.append([e4, e9, e5]) + tri_list.append([e3, e5, e6]) + tri_list.append([e3, e12, e6]) + elif (case == 40): + # upper left front, lower right back corners + _append_tris(tri_list, 32, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 8, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 41): + # front left plane, lower right back corner + _append_tris(tri_list, 32, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 9, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 42): + # lower right plane, upper front left corner + _append_tris(tri_list, 8, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 34, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 43): + # Rotated case 11 in paper + tri_list.append([e11, e3, e9]) + tri_list.append([e3, e9, e6]) + tri_list.append([e3, e2, e6]) + tri_list.append([e9, e5, e6]) + elif (case == 44): + # upper front plane, lower right back corner + _append_tris(tri_list, 12, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 32, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 45): + # Shelf: (v1, v3, v4) + lower right back corner + _append_tris(tri_list, 13, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 32, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 46): + # Rotated case 14 in paper + tri_list.append([e4, e11, e12]) + tri_list.append([e4, e12, e5]) + tri_list.append([e12, e5, e6]) + tri_list.append([e4, e5, e1]) + elif (case == 47): + # Shelf: (v5, v8, v7) by inversion + tri_list.append([e11, e9, e12]) + tri_list.append([e12, e9, e5]) + tri_list.append([e12, e5, e6]) + elif (case == 48): + # Back lower plane + tri_list.append([e9, e10, e6]) + tri_list.append([e9, e6, e8]) + elif (case == 49): + # Shelf: (v1, v5, v6) + tri_list.append([e4, e8, e6]) + tri_list.append([e4, e6, e1]) + tri_list.append([e6, e1, e10]) + elif (case == 50): + # Shelf: (v2, v5, v6) + tri_list.append([e8, e6, e2]) + tri_list.append([e8, e2, e1]) + tri_list.append([e8, e9, e1]) + elif (case == 51): + # Plane through middle of cube, parallel to x-z axis + tri_list.append([e4, e8, e2]) + tri_list.append([e8, e2, e6]) + elif (case == 52): + # Back lower plane, and front upper right corner + _append_tris(tri_list, 48, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 4, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 53): + # Shelf (v1, v5, v6) and front upper right corner + _append_tris(tri_list, 49, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 4, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 54): + # Rotated case 11 from paper (v2, v3, v5, v6) + tri_list.append([e1, e9, e3]) + tri_list.append([e9, e3, e6]) + tri_list.append([e9, e8, e6]) + tri_list.append([e12, e3, e6]) + elif (case == 55): + # Shelf: (v4, v8, v7) by inversion + tri_list.append([e4, e8, e6]) + tri_list.append([e4, e6, e3]) + tri_list.append([e6, e3, e12]) + elif (case == 56): + # Back lower plane + upper left front corner + _append_tris(tri_list, 48, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 8, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 57): + # Rotated case 14 from paper (v4, v1, v5, v6) + tri_list.append([e3, e11, e8]) + tri_list.append([e3, e8, e10]) + tri_list.append([e10, e6, e8]) + tri_list.append([e3, e1, e10]) + elif (case == 58): + # Shelf: (v2, v6, v5) + upper left front corner + _append_tris(tri_list, 50, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 8, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 59): + # Shelf: (v3, v7, v8) by inversion + tri_list.append([e2, e6, e8]) + tri_list.append([e8, e2, e3]) + tri_list.append([e8, e3, e11]) + elif (case == 60): + # AMBIGUOUS CASE: parallel planes (front upper, back lower) + _append_tris(tri_list, 48, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 12, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 61): + # Upper back plane + lower right front corner by inversion + _append_tris(tri_list, 63, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 2, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 62): + # Upper back plane + lower left front corner by inversion + _append_tris(tri_list, 63, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 1, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 63): + # Upper back plane + tri_list.append([e11, e12, e6]) + tri_list.append([e11, e8, e6]) + elif (case == 64): + # Upper right back corner + tri_list.append([e12, e7, e6]) + elif (case == 65): + # upper right back, lower left front corners + _append_tris(tri_list, 64, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 1, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 66): + # upper right back, lower right front corners + _append_tris(tri_list, 64, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 2, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 67): + # lower front plane + upper right back corner + _append_tris(tri_list, 64, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 3, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 68): + # upper right plane + tri_list.append([e3, e2, e6]) + tri_list.append([e3, e7, e6]) + elif (case == 69): + # Upper right plane, lower left front corner + _append_tris(tri_list, 68, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 1, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 70): + # Shelf: (v2, v3, v7) + tri_list.append([e1, e3, e7]) + tri_list.append([e1, e10, e7]) + tri_list.append([e7, e10, e6]) + elif (case == 71): + # Rotated version of case 11 in paper (v1, v2, v3, v7) + tri_list.append([e10, e7, e4]) + tri_list.append([e4, e3, e7]) + tri_list.append([e10, e4, e9]) + tri_list.append([e7, e10, e6]) + elif (case == 72): + # upper left front, upper right back corners + _append_tris(tri_list, 64, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 8, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 73): + # front left plane, upper right back corner + _append_tris(tri_list, 64, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 9, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 74): + # Three isolated corners, exactly case 7 in paper + _append_tris(tri_list, 64, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 8, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 2, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 75): + # Shelf: (v1, v2, v4) + upper right back corner + _append_tris(tri_list, 64, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 11, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 76): + # Shelf: (v4, v3, v7) + tri_list.append([e4, e2, e6]) + tri_list.append([e4, e11, e7]) + tri_list.append([e4, e7, e6]) + elif (case == 77): + # Rotated case 14 in paper (v1, v4, v3, v7) + tri_list.append([e11, e9, e1]) + tri_list.append([e11, e1, e6]) + tri_list.append([e1, e6, e2]) + tri_list.append([e11, e6, e7]) + elif (case == 78): + # Full corner v3: (v2, v3, v4, v7) + tri_list.append([e1, e4, e7]) + tri_list.append([e1, e7, e6]) + tri_list.append([e4, e11, e7]) + tri_list.append([e1, e10, e6]) + elif (case == 79): + # Shelf: (v6, v5, v8) by inversion + tri_list.append([e9, e11, e10]) + tri_list.append([e11, e7, e10]) + tri_list.append([e7, e10, e6]) + elif (case == 80): + # lower left back, upper right back corners (v5, v7) + _append_tris(tri_list, 64, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 16, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 81): + # lower left plane, upper right back corner + _append_tris(tri_list, 64, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 17, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 82): + # isolated corners (v2, v5, v7) + _append_tris(tri_list, 64, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 16, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 2, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 83): + # Shelf: (v1, v2, v5) + upper right back corner + _append_tris(tri_list, 64, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 19, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 84): + # upper right plane, lower left back corner + _append_tris(tri_list, 16, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 68, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 85): + # AMBIGUOUS CASE: upper right and lower left parallel planes + _append_tris(tri_list, 17, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 68, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 86): + # Shelf: (v2, v3, v7) + lower left back corner + _append_tris(tri_list, 70, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 16, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 87): + # Upper left plane + lower right back corner, by inversion + _append_tris(tri_list, 119, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 32, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 88): + # Isolated corners v4, v5, v7 + _append_tris(tri_list, 64, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 16, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 8, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 89): + # Shelf: (v1, v4, v5) + isolated corner v7 + _append_tris(tri_list, 25, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 64, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 90): + # Four isolated corners v2, v4, v5, v7 + _append_tris(tri_list, 2, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 8, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 16, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 64, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 91): + # Three isolated corners, v3, v6, v8 by inversion + _append_tris(tri_list, 4, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 32, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 127, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 92): + # Shelf (v4, v3, v7) + isolated corner v5 + _append_tris(tri_list, 76, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 16, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 93): + # Lower right plane + isolated corner v8 by inversion + _append_tris(tri_list, 127, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 34, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 94): + # Isolated corners v1, v6, v8 by inversion + _append_tris(tri_list, 1, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 32, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 127, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 95): + # Isolated corners v6, v8 by inversion + _append_tris(tri_list, 32, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 127, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 96): + # back right plane + tri_list.append([e7, e12, e5]) + tri_list.append([e5, e10, e12]) + elif (case == 97): + # back right plane + isolated corner v1 + _append_tris(tri_list, 96, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 1, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 98): + # Shelf: (v2, v6, v7) + tri_list.append([e1, e7, e5]) + tri_list.append([e7, e1, e12]) + tri_list.append([e1, e12, e2]) + elif (case == 99): + # Rotated case 14 in paper: (v1, v2, v6, v7) + tri_list.append([e9, e2, e7]) + tri_list.append([e9, e2, e4]) + tri_list.append([e2, e7, e12]) + tri_list.append([e7, e9, e5]) + elif (case == 100): + # Shelf: (v3, v6, v7) + tri_list.append([e3, e7, e5]) + tri_list.append([e3, e5, e2]) + tri_list.append([e2, e5, e10]) + elif (case == 101): + # Shelf: (v3, v6, v7) + isolated corner v1 + _append_tris(tri_list, 100, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 1, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 102): + # Plane bisecting left-right halves of cube + tri_list.append([e1, e3, e7]) + tri_list.append([e1, e7, e5]) + elif (case == 103): + # Shelf: (v4, v5, v8) by inversion + tri_list.append([e3, e7, e5]) + tri_list.append([e3, e5, e4]) + tri_list.append([e4, e5, e9]) + elif (case == 104): + # Back right plane + isolated corner v4 + _append_tris(tri_list, 8, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 96, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 105): + # AMBIGUOUS CASE: back right and front left planes + _append_tris(tri_list, 96, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 9, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 106): + # Shelf: (v2, v6, v7) + isolated corner v4 + _append_tris(tri_list, 98, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 8, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 107): + # Back left plane + isolated corner v3 by inversion + _append_tris(tri_list, 4, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 111, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 108): + # Rotated case 11 from paper: (v4, v3, v7, v6) + tri_list.append([e4, e10, e7]) + tri_list.append([e4, e10, e2]) + tri_list.append([e4, e11, e7]) + tri_list.append([e7, e10, e5]) + elif (case == 109): + # Back left plane + isolated corner v2 by inversion + _append_tris(tri_list, 111, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 2, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 110): + # Shelf: (v1, v5, v8) by inversion + tri_list.append([e1, e5, e7]) + tri_list.append([e1, e7, e11]) + tri_list.append([e1, e11, e4]) + elif (case == 111): + # Back left plane + tri_list.append([e11, e9, e7]) + tri_list.append([e9, e7, e5]) + elif (case == 112): + # Shelf: (v5, v6, v7) + tri_list.append([e9, e10, e12]) + tri_list.append([e9, e12, e7]) + tri_list.append([e9, e7, e8]) + elif (case == 113): + # Exactly case 11 from paper: (v1, v5, v6, v7) + tri_list.append([e1, e8, e12]) + tri_list.append([e1, e8, e4]) + tri_list.append([e8, e7, e12]) + tri_list.append([e12, e1, e10]) + elif (case == 114): + # Full corner v6: (v2, v6, v7, v5) + tri_list.append([e1, e9, e7]) + tri_list.append([e1, e7, e12]) + tri_list.append([e1, e12, e2]) + tri_list.append([e9, e8, e7]) + elif (case == 115): + # Shelf: (v3, v4, v8) + tri_list.append([e2, e4, e8]) + tri_list.append([e2, e12, e7]) + tri_list.append([e2, e8, e7]) + elif (case == 116): + # Rotated case 14 in paper: (v5, v6, v7, v3) + tri_list.append([e9, e2, e7]) + tri_list.append([e9, e2, e10]) + tri_list.append([e9, e8, e7]) + tri_list.append([e2, e3, e7]) + elif (case == 117): + # upper left plane + isolated corner v2 by inversion + _append_tris(tri_list, 2, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 119, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 118): + # Shelf: (v1, v4, v8) + tri_list.append([e1, e3, e7]) + tri_list.append([e7, e1, e8]) + tri_list.append([e1, e8, e9]) + elif (case == 119): + # Upper left plane + tri_list.append([e4, e3, e7]) + tri_list.append([e4, e8, e7]) + elif (case == 120): + # Shelf: (v1, v2, v3) + isolated corner v8 + _append_tris(tri_list, 7, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 127, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 121): + # Front right plane + isolated corner v8 + _append_tris(tri_list, 6, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 127, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 122): + # Isolated corners v1, v3, v8 + _append_tris(tri_list, 1, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 4, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 127, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 123): + # Isolated corners v3, v8 + _append_tris(tri_list, 4, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 127, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 124): + # Front lower plane + isolated corner v8 + _append_tris(tri_list, 3, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 127, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 125): + # Isolated corners v2, v8 + _append_tris(tri_list, 2, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 127) + elif (case == 126): + # Isolated corners v1, v8 + _append_tris(tri_list, 1, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 127, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 127): + # Isolated corner v8 + tri_list.append([e11, e7, e8]) + + return diff --git a/skimage/measure/marching_cubes.py b/skimage/measure/marching_cubes.py new file mode 100644 index 00000000..10980580 --- /dev/null +++ b/skimage/measure/marching_cubes.py @@ -0,0 +1,200 @@ +import numpy as np +from . import _marching_cubes + + +def marching_cubes(volume, level, sampling=(1., 1., 1.)): + """ + Marching cubes algorithm to find iso-valued surfaces in 3d volumetric data + + Parameters + ---------- + volume : (M, N, P) array of doubles + Input data volume to find isosurfaces. Will be cast to `np.float64` if + not provided in this format. + level : float + Contour value to search for isosurfaces in `volume`. + sampling : length-3 tuple of floats + Voxel spacing in spatial dimensions corresponding to numpy array + indexing dimensions (M, N, P) as in `volume`. + + Returns + ------- + vert_list : list + Every entry in this list is a unique vertex on the isosurface. + tri_list : list + Every entry in this list is a length-3 list of integers. These + represent triangular faces; the integers in each sub-list correspond + to vertices held in `vert_list`. + + Notes + ----- + The marching cubes algorithm is implemented as described in [1]_. + A simple explanation is available here:: + + http://www.essi.fr/~lingrand/MarchingCubes/algo.html + + There are several known ambiguous cases in the marching cubes algorithm. + Using point labeling as in [1]_, Figure 4, as shown: + + v8 ------ v7 + / | / | y + / | / | ^ z + v4 ------ v3 | | / + | v5 ----|- v6 |/ (note: NOT right handed!) + | / | / ----> x + |/ | / + v1 ------ v2 + + Most notably, if v4, v8, v2, and v6 are all >= `level` (or any + generalization of this case) two parallel planes are generated by this + algorithm, separating v4 and v8 from v2 and v6. An equally valid + interpretation would be a single connected thin surface enclosing all + four points. This is the best known ambiguity, though there are others. + + This algorithm does not attempt to resolve such ambiguities; it is a naive + implementation of marching cubes as in [1]_, but may be a good beginning + for work with more recent techniques (Dual Marching Cubes, Extended + Marching Cubes, Cubic Marching Squares, etc.). + + Because of interactions between neighboring cubes, the isosurface(s) + generated by this algorithm are NOT guaranteed to be closed, particularly + for complicated contours. Furthermore, this algorithm does not guarantee + a single contour will be returned. Indeed, ALL isosurfaces which cross + `level` will be found, regardless of connectivity. + + The output is a triangular mesh consisting of a set of unique vertices and + connecting triangles. The order of these vertices and triangles in the + output list is determined by the position of the smallest ``x,y,z`` (in + lexicographical order) coordinate in the contour. This is a side-effect + of how the input array is traversed, but can be relied upon. + + To quantify the area of an isosurface generated by this algorithm, pass + the output directly into `skimage.measure.mesh_surface_area`. + + Regarding visualization of algorithm output, the ``mayavi`` package + is recommended. To contour a volume named `myvolume` about the level 0.0: + + >>> from mayavi import mlab + >>> verts, tris = marching_cubes(myvolume, 0.0, (1., 1., 2.)) + >>> mlab.triangular_mesh([vert[0] for vert in verts], + [vert[1] for vert in verts], + [vert[2] for vert in verts], + tris) + >>> mlab.show() + + References + ---------- + .. [1] Lorensen, William and Harvey E. Cline. Marching Cubes: A High + Resolution 3D Surface Construction Algorithm. Computer Graphics + (SIGGRAPH 87 Proceedings) 21(4) July 1987, p. 163-170). + + See Also + -------- + skimage.measure.mesh_surface_area + + """ + # Check inputs + if volume.ndim != 3: + raise ValueError("Input volume must be 3d.") + if volume.dtype.kind == 'f': + volume = volume.astype(np.float) + else: + from skimage.util import img_as_float + # If incorrect type provided, convert BOTH contour value and input + # volume using same method + level = img_as_float(np.array(level, dtype=volume.dtype))[0] + volume = img_as_float(volume) + + # Extract raw triangles using marching cubes in Cython + # Returns a list of length-3 lists, each sub-list containing three + # tuples. The tuples hold (x, y, z) coordinates for triangle vertices. + # Note: this algorithm is fast, but returns degenerate "triangles" which + # have repeated vertices - and equivalent vertices are redundantly + # placed in every triangle they connect with. + raw_tris = _marching_cubes.iterate_and_store_3d(volume, float(level), + sampling) + + # Find and collect unique vertices, storing triangle verts as indices. + # Removes much redundancy and eliminates degenerate "triangles". + vert_list, tri_list = _unpack_unique_verts(raw_tris) + + return vert_list, tri_list + + +def _unpack_unique_verts(trilist): + """ + Converts a list of lists of tuples corresponding to triangle vertices into + a unique vertex list, and a list of triangle faces w/indices corresponding + to entries of the vertex list. + + """ + idx = 0 + vert_index = {} + vert_list = [] + tri_list = [] + + # Iterate over triangles + for i in range(len(trilist)): + templist = [] + + # Only parse vertices from non-degenerate triangles + if not ((trilist[i][0] == trilist[i][1]) or + (trilist[i][0] == trilist[i][2]) or + (trilist[i][1] == trilist[i][2])): + + # Iterate over vertices within each triangle + for j in range(3): + vert = trilist[i][j] + + # Check if a new unique vertex found + if vert not in vert_index: + vert_index[vert] = idx + templist.append(idx) + vert_list.append(vert) + idx += 1 + else: + templist.append(vert_index[vert]) + + tri_list.append(templist) + + return vert_list, tri_list + + +def mesh_surface_area(verts, tris): + """ + Compute surface area, given vertices & triangular faces + + Parameters + ---------- + verts : list + List of length-3 NumPy arrays containing vertex coordinates. + Units in each dimension should be consistent. + + tris : list + List of length-3 lists of integers, referencing vertex coordinates as + provided in `verts` + + Returns + ------- + area : float + Surface area of mesh. Units in coordinates maintained, but squared. + + Notes + ----- + The arguments expected by this function are the exact outputs from + `skimage.measure.marching_cubes`. For unit correct output, ensure correct + `spacing` was passed to `skimage.measure.marching_cubes`. + + See Also + -------- + skimage.measure.marching_cubes + + """ + # Define two vector arrays `a` and `b` from triangle vertices + actual_verts = np.array([[verts[i] for i in tri] for tri in tris]) + a = actual_verts[:, 0, :] - actual_verts[:, 1, :] + b = actual_verts[:, 0, :] - actual_verts[:, 2, :] + del actual_verts + + # Area of triangle = 1/2 * Euclidean norm of cross product + return ((np.cross(a, b) ** 2).sum(axis=1) ** 0.5).sum() / 2. From 84618af7c7fa945cca946522e2b5483a195cb762 Mon Sep 17 00:00:00 2001 From: "Josh Warner (Mac)" Date: Sat, 16 Mar 2013 21:14:13 -0500 Subject: [PATCH 536/736] import `marching_cubes` and `mesh_surface_area` --- skimage/measure/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/skimage/measure/__init__.py b/skimage/measure/__init__.py index 616071d3..108cd7d9 100755 --- a/skimage/measure/__init__.py +++ b/skimage/measure/__init__.py @@ -1,4 +1,5 @@ from .find_contours import find_contours +from ._marching_cubes import marching_cubes, mesh_surface_area from ._regionprops import regionprops, perimeter from ._structural_similarity import structural_similarity from ._polygon import approximate_polygon, subdivide_polygon @@ -21,4 +22,7 @@ __all__ = ['find_contours', 'moments', 'moments_central', 'moments_normalized', - 'moments_hu'] + 'moments_hu', + 'sum_blocks', + 'marching_cubes', + 'mesh_surface_area'] From 855031ad477012a8ec4bda3c1cf504923f7fc4cc Mon Sep 17 00:00:00 2001 From: "Josh Warner (Mac)" Date: Sat, 16 Mar 2013 21:51:33 -0500 Subject: [PATCH 537/736] add _marching_cubes.pyx to list of Cython modules to build --- skimage/measure/setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/skimage/measure/setup.py b/skimage/measure/setup.py index 21d9964e..6bc8212d 100644 --- a/skimage/measure/setup.py +++ b/skimage/measure/setup.py @@ -14,6 +14,7 @@ def configuration(parent_package='', top_path=None): cython(['_find_contours.pyx'], working_path=base_path) cython(['_moments.pyx'], working_path=base_path) + cython(['_marching_cubes.pyx'], working_path=base_path) config.add_extension('_find_contours', sources=['_find_contours.c'], include_dirs=[get_numpy_include_dirs()]) From 5b990a0856b7568aeab6dd486b37cce0ea44b618 Mon Sep 17 00:00:00 2001 From: "Josh Warner (Mac)" Date: Sat, 16 Mar 2013 22:22:26 -0500 Subject: [PATCH 538/736] fix import of _marching_cubes --- skimage/measure/setup.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/skimage/measure/setup.py b/skimage/measure/setup.py index 6bc8212d..4b18a3a0 100644 --- a/skimage/measure/setup.py +++ b/skimage/measure/setup.py @@ -20,6 +20,8 @@ def configuration(parent_package='', top_path=None): include_dirs=[get_numpy_include_dirs()]) config.add_extension('_moments', sources=['_moments.c'], include_dirs=[get_numpy_include_dirs()]) + config.add_extension('_marching_cubes', sources=['_marching_cubes.c'], + include_dirs=[get_numpy_include_dirs()]) return config From 9779cd5e4a0017082c9ede6fba2b0d64ca456b99 Mon Sep 17 00:00:00 2001 From: "Josh Warner (Mac)" Date: Sun, 17 Mar 2013 00:46:39 -0500 Subject: [PATCH 539/736] fix: bug in second `_append_tris` call in case 125 --- skimage/measure/_marching_cubes.pyx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/skimage/measure/_marching_cubes.pyx b/skimage/measure/_marching_cubes.pyx index 8858acf4..be7c601f 100644 --- a/skimage/measure/_marching_cubes.pyx +++ b/skimage/measure/_marching_cubes.pyx @@ -910,7 +910,8 @@ def _append_tris(list tri_list, unsigned char case, tuple e1, tuple e2, # Isolated corners v2, v8 _append_tris(tri_list, 2, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 127) + _append_tris(tri_list, 127, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) elif (case == 126): # Isolated corners v1, v8 _append_tris(tri_list, 1, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, From 9a86aba2263e4729bf0b0c396b9fc514f0ae5b3a Mon Sep 17 00:00:00 2001 From: "Josh Warner (Mac)" Date: Sun, 17 Mar 2013 08:48:32 -0500 Subject: [PATCH 540/736] rename files to conform with naming conventions --- skimage/measure/{marching_cubes.py => _marching_cubes.py} | 7 +++---- .../{_marching_cubes.pyx => _marching_cubes_cy.pyx} | 2 +- skimage/measure/setup.py | 4 ++-- 3 files changed, 6 insertions(+), 7 deletions(-) rename skimage/measure/{marching_cubes.py => _marching_cubes.py} (97%) rename skimage/measure/{_marching_cubes.pyx => _marching_cubes_cy.pyx} (99%) diff --git a/skimage/measure/marching_cubes.py b/skimage/measure/_marching_cubes.py similarity index 97% rename from skimage/measure/marching_cubes.py rename to skimage/measure/_marching_cubes.py index 10980580..d375c9fe 100644 --- a/skimage/measure/marching_cubes.py +++ b/skimage/measure/_marching_cubes.py @@ -1,5 +1,5 @@ import numpy as np -from . import _marching_cubes +from . import _marching_cubes_cy def marching_cubes(volume, level, sampling=(1., 1., 1.)): @@ -111,8 +111,8 @@ def marching_cubes(volume, level, sampling=(1., 1., 1.)): # Note: this algorithm is fast, but returns degenerate "triangles" which # have repeated vertices - and equivalent vertices are redundantly # placed in every triangle they connect with. - raw_tris = _marching_cubes.iterate_and_store_3d(volume, float(level), - sampling) + raw_tris = _marching_cubes_cy.iterate_and_store_3d(volume, float(level), + sampling) # Find and collect unique vertices, storing triangle verts as indices. # Removes much redundancy and eliminates degenerate "triangles". @@ -169,7 +169,6 @@ def mesh_surface_area(verts, tris): verts : list List of length-3 NumPy arrays containing vertex coordinates. Units in each dimension should be consistent. - tris : list List of length-3 lists of integers, referencing vertex coordinates as provided in `verts` diff --git a/skimage/measure/_marching_cubes.pyx b/skimage/measure/_marching_cubes_cy.pyx similarity index 99% rename from skimage/measure/_marching_cubes.pyx rename to skimage/measure/_marching_cubes_cy.pyx index be7c601f..68faa8de 100644 --- a/skimage/measure/_marching_cubes.pyx +++ b/skimage/measure/_marching_cubes_cy.pyx @@ -166,7 +166,7 @@ def iterate_and_store_3d(cnp.ndarray[double, ndim=3] arr, e3 = r0 + _get_fraction(v4, v3, level), c1, d0 e4 = r0, c0 + _get_fraction(v1, v4, level), d0 - # These must be calculated at each point uunless we implemented a + # These must be calculated at each point unless we implemented a # large, growing lookup table for all adjacent values; could save # ~30% in terms of runtime at the expense of memory usage and # much greater complexity. diff --git a/skimage/measure/setup.py b/skimage/measure/setup.py index 4b18a3a0..19d69429 100644 --- a/skimage/measure/setup.py +++ b/skimage/measure/setup.py @@ -14,13 +14,13 @@ def configuration(parent_package='', top_path=None): cython(['_find_contours.pyx'], working_path=base_path) cython(['_moments.pyx'], working_path=base_path) - cython(['_marching_cubes.pyx'], working_path=base_path) + cython(['_marching_cubes_cy.pyx'], working_path=base_path) config.add_extension('_find_contours', sources=['_find_contours.c'], include_dirs=[get_numpy_include_dirs()]) config.add_extension('_moments', sources=['_moments.c'], include_dirs=[get_numpy_include_dirs()]) - config.add_extension('_marching_cubes', sources=['_marching_cubes.c'], + config.add_extension('_marching_cubes_cy', sources=['_marching_cubes.c'], include_dirs=[get_numpy_include_dirs()]) return config From d2e51aa03533613c26c78d36c785af40bcceca4d Mon Sep 17 00:00:00 2001 From: "Josh Warner (Mac)" Date: Sun, 17 Mar 2013 09:07:56 -0500 Subject: [PATCH 541/736] fix import error from `_marching_cubes_cy` --- skimage/measure/setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/skimage/measure/setup.py b/skimage/measure/setup.py index 19d69429..be57ca7b 100644 --- a/skimage/measure/setup.py +++ b/skimage/measure/setup.py @@ -20,7 +20,8 @@ def configuration(parent_package='', top_path=None): include_dirs=[get_numpy_include_dirs()]) config.add_extension('_moments', sources=['_moments.c'], include_dirs=[get_numpy_include_dirs()]) - config.add_extension('_marching_cubes_cy', sources=['_marching_cubes.c'], + config.add_extension('_marching_cubes_cy', + sources=['_marching_cubes_cy.c'], include_dirs=[get_numpy_include_dirs()]) return config From 44c04996c7c25a1d531744aec431a889a22aff12 Mon Sep 17 00:00:00 2001 From: JDWarner Date: Wed, 20 Mar 2013 15:42:57 -0500 Subject: [PATCH 542/736] move `unpack_unique_verts` to Cython for speed --- skimage/measure/_marching_cubes.py | 41 +------------------------ skimage/measure/_marching_cubes_cy.pyx | 42 ++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 40 deletions(-) diff --git a/skimage/measure/_marching_cubes.py b/skimage/measure/_marching_cubes.py index d375c9fe..e5c4bcff 100644 --- a/skimage/measure/_marching_cubes.py +++ b/skimage/measure/_marching_cubes.py @@ -116,46 +116,7 @@ def marching_cubes(volume, level, sampling=(1., 1., 1.)): # Find and collect unique vertices, storing triangle verts as indices. # Removes much redundancy and eliminates degenerate "triangles". - vert_list, tri_list = _unpack_unique_verts(raw_tris) - - return vert_list, tri_list - - -def _unpack_unique_verts(trilist): - """ - Converts a list of lists of tuples corresponding to triangle vertices into - a unique vertex list, and a list of triangle faces w/indices corresponding - to entries of the vertex list. - - """ - idx = 0 - vert_index = {} - vert_list = [] - tri_list = [] - - # Iterate over triangles - for i in range(len(trilist)): - templist = [] - - # Only parse vertices from non-degenerate triangles - if not ((trilist[i][0] == trilist[i][1]) or - (trilist[i][0] == trilist[i][2]) or - (trilist[i][1] == trilist[i][2])): - - # Iterate over vertices within each triangle - for j in range(3): - vert = trilist[i][j] - - # Check if a new unique vertex found - if vert not in vert_index: - vert_index[vert] = idx - templist.append(idx) - vert_list.append(vert) - idx += 1 - else: - templist.append(vert_index[vert]) - - tri_list.append(templist) + vert_list, tri_list = _marching_cubes_cy.unpack_unique_verts(raw_tris) return vert_list, tri_list diff --git a/skimage/measure/_marching_cubes_cy.pyx b/skimage/measure/_marching_cubes_cy.pyx index 68faa8de..186ea8e8 100644 --- a/skimage/measure/_marching_cubes_cy.pyx +++ b/skimage/measure/_marching_cubes_cy.pyx @@ -13,6 +13,48 @@ cdef inline double _get_fraction(double from_value, double to_value, return ((level - from_value) / (to_value - from_value)) +def unpack_unique_verts(list trilist): + """ + Converts a list of lists of tuples corresponding to triangle vertices into + a unique vertex list, and a list of triangle faces w/indices corresponding + to entries of the vertex list. + + """ + cdef Py_ssize_t idx = 0 + cdef Py_ssize_t n_tris = len(trilist) + cdef Py_ssize_t i, j + cdef dict vert_index = {} + cdef list vert_list = [] + cdef list tri_list = [] + cdef list templist + + # Iterate over triangles + for i in range(n_tris): + templist = [] + + # Only parse vertices from non-degenerate triangles + if not ((trilist[i][0] == trilist[i][1]) or + (trilist[i][0] == trilist[i][2]) or + (trilist[i][1] == trilist[i][2])): + + # Iterate over vertices within each triangle + for j in range(3): + vert = trilist[i][j] + + # Check if a new unique vertex found + if vert not in vert_index: + vert_index[vert] = idx + templist.append(idx) + vert_list.append(vert) + idx += 1 + else: + templist.append(vert_index[vert]) + + tri_list.append(templist) + + return vert_list, tri_list + + def iterate_and_store_3d(cnp.ndarray[double, ndim=3] arr, double level, tuple sampling=(1., 1., 1.)): """Iterate across the given array in a marching-cubes fashion, From 296d1020474540f2397a111f815d189927c925c2 Mon Sep 17 00:00:00 2001 From: JDWarner Date: Wed, 20 Mar 2013 16:35:55 -0500 Subject: [PATCH 543/736] fix: symmetric ambiguous dual-plane cases require special treatment --- skimage/measure/_marching_cubes_cy.pyx | 29 +++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/skimage/measure/_marching_cubes_cy.pyx b/skimage/measure/_marching_cubes_cy.pyx index 186ea8e8..2fe1a1bd 100644 --- a/skimage/measure/_marching_cubes_cy.pyx +++ b/skimage/measure/_marching_cubes_cy.pyx @@ -184,7 +184,10 @@ def iterate_and_store_3d(cnp.ndarray[double, ndim=3] arr, # Cases 0 and 255 are entirely below/above the contour. if cube_case > 127: - cube_case = 255 - cube_case + if ((cube_case != 150) and + (cube_case != 170) and + (cube_case != 195)): + cube_case = 255 - cube_case # Calculate cube edges, to become triangulation vertices. # If we moved in a convenient direction, save 1/3 of the effort by @@ -963,5 +966,29 @@ def _append_tris(list tri_list, unsigned char case, tuple e1, tuple e2, elif (case == 127): # Isolated corner v8 tri_list.append([e11, e7, e8]) + elif (case == 150): + # AMBIGUOUS CASE: back right and front left planes + # In these cube_case > 127 cases, the vertices are identical BUT + # they are connected in the opposite fashion. + _append_tris(tri_list, 6, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 111, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 170): + # AMBIGUOUS CASE: upper left and lower right planes + # In these cube_case > 127 cases, the vertices are identical BUT + # they are connected in the opposite fashion. + _append_tris(tri_list, 119, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 34, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + elif (case == 195): + # AMBIGUOUS CASE: back upper and front lower planes + # In these cube_case > 127 cases, the vertices are identical BUT + # they are connected in the opposite fashion. + _append_tris(tri_list, 63, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) + _append_tris(tri_list, 3, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + e11, e12) return From 165de870d0cbee07ee7547c4df3ea4f35c55aaba Mon Sep 17 00:00:00 2001 From: "Josh Warner (Mac)" Date: Thu, 28 Mar 2013 14:56:57 -0500 Subject: [PATCH 544/736] fix error in triangulation of case 120 --- skimage/measure/_marching_cubes_cy.pyx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/skimage/measure/_marching_cubes_cy.pyx b/skimage/measure/_marching_cubes_cy.pyx index 2fe1a1bd..cf3ad93d 100644 --- a/skimage/measure/_marching_cubes_cy.pyx +++ b/skimage/measure/_marching_cubes_cy.pyx @@ -920,10 +920,10 @@ def _append_tris(list tri_list, unsigned char case, tuple e1, tuple e2, tri_list.append([e4, e3, e7]) tri_list.append([e4, e8, e7]) elif (case == 120): - # Shelf: (v1, v2, v3) + isolated corner v8 - _append_tris(tri_list, 7, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + # Shelf: (v5, v6, v7) + isolated corner v4 + _append_tris(tri_list, 112, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 127, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(tri_list, 8, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) elif (case == 121): # Front right plane + isolated corner v8 From f8cc4799da581057849416ced72fc33198cae45b Mon Sep 17 00:00:00 2001 From: "Josh Warner (Mac)" Date: Fri, 31 May 2013 09:46:11 -0500 Subject: [PATCH 545/736] FIX: dimension ordering now correct (x, y, z) --- skimage/measure/_marching_cubes_cy.pyx | 27 ++++++++++---------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/skimage/measure/_marching_cubes_cy.pyx b/skimage/measure/_marching_cubes_cy.pyx index cf3ad93d..18e14ddb 100644 --- a/skimage/measure/_marching_cubes_cy.pyx +++ b/skimage/measure/_marching_cubes_cy.pyx @@ -78,14 +78,7 @@ def iterate_and_store_3d(cnp.ndarray[double, ndim=3] arr, cdef Py_ssize_t n cdef bint odd_sampling, plus_z plus_z = False - if ((sampling == (1., 1., 1.)) or - (sampling == (1., 1., 1)) or - (sampling == (1., 1, 1.)) or - (sampling == (1, 1., 1.)) or - (sampling == (1, 1, 1.)) or - (sampling == (1., 1, 1)) or - (sampling == (1, 1., 1)) or - (sampling == (1, 1, 1))): + if [float(i) for i in sampling] == [1.0, 1.0, 1.0]: odd_sampling = False else: odd_sampling = True @@ -159,14 +152,14 @@ def iterate_and_store_3d(cnp.ndarray[double, ndim=3] arr, # We use a right-handed coordinate system, UNlike the paper, but want # to index in agreement - the coordinate adjustment takes place here. - v1 = arr[x0, y0, z0] - v2 = arr[x1, y0, z0] - v3 = arr[x1, y1, z0] - v4 = arr[x0, y1, z0] - v5 = arr[x0, y0, z1] - v6 = arr[x1, y0, z1] - v7 = arr[x1, y1, z1] - v8 = arr[x0, y1, z1] + v1 = arr[z0, y0, x0] + v2 = arr[z0, y0, x1] + v3 = arr[z0, y1, x1] + v4 = arr[z0, y1, x0] + v5 = arr[z1, y0, x0] + v6 = arr[z1, y0, x1] + v7 = arr[z1, y1, x1] + v8 = arr[z1, y1, x0] # Unique triangulation cases cube_case = 0 @@ -240,7 +233,7 @@ def iterate_and_store_3d(cnp.ndarray[double, ndim=3] arr, e6, e7, e8, e9, e10, e11, e12) # Advance the coords indices - if coords[2] < arr.shape[2] - 2: + if coords[2] < arr.shape[0] - 2: coords[2] += 1 plus_z = True elif coords[1] < arr.shape[1] - 2: From 164b416bd9abd4a13fee7bc3dc70a8f9fd17ea20 Mon Sep 17 00:00:00 2001 From: "Josh Warner (Mac)" Date: Fri, 31 May 2013 10:27:54 -0500 Subject: [PATCH 546/736] FIX: volume now directly cast, docfixes, C-contiguous ordering --- skimage/measure/_marching_cubes.py | 51 ++++++++++++++---------------- 1 file changed, 24 insertions(+), 27 deletions(-) diff --git a/skimage/measure/_marching_cubes.py b/skimage/measure/_marching_cubes.py index e5c4bcff..418f945e 100644 --- a/skimage/measure/_marching_cubes.py +++ b/skimage/measure/_marching_cubes.py @@ -19,12 +19,12 @@ def marching_cubes(volume, level, sampling=(1., 1., 1.)): Returns ------- - vert_list : list - Every entry in this list is a unique vertex on the isosurface. - tri_list : list - Every entry in this list is a length-3 list of integers. These - represent triangular faces; the integers in each sub-list correspond - to vertices held in `vert_list`. + verts : (V, 3) array + Spatial (x, y, z) coordinates for V unique vertices in mesh. + faces : (F, 3) array + Define triangular faces via referencing vertex indices from ``verts``. + This algorithm specifically outputs triangles, so each face has + exactly three indices. Notes ----- @@ -69,7 +69,7 @@ def marching_cubes(volume, level, sampling=(1., 1., 1.)): of how the input array is traversed, but can be relied upon. To quantify the area of an isosurface generated by this algorithm, pass - the output directly into `skimage.measure.mesh_surface_area`. + the outputs directly into `skimage.measure.mesh_surface_area`. Regarding visualization of algorithm output, the ``mayavi`` package is recommended. To contour a volume named `myvolume` about the level 0.0: @@ -93,17 +93,12 @@ def marching_cubes(volume, level, sampling=(1., 1., 1.)): skimage.measure.mesh_surface_area """ - # Check inputs + # Check inputs and ensure `volume` is C-contiguous for memoryviews if volume.ndim != 3: - raise ValueError("Input volume must be 3d.") - if volume.dtype.kind == 'f': - volume = volume.astype(np.float) - else: - from skimage.util import img_as_float - # If incorrect type provided, convert BOTH contour value and input - # volume using same method - level = img_as_float(np.array(level, dtype=volume.dtype))[0] - volume = img_as_float(volume) + raise ValueError("Input volume must have 3 dimensions.") + if level < volume.min() or level > volume.max(): + raise ValueError("Contour level must be within volume data range.") + volume = np.array(volume, dtype=np.float64, order="C") # Extract raw triangles using marching cubes in Cython # Returns a list of length-3 lists, each sub-list containing three @@ -115,10 +110,10 @@ def marching_cubes(volume, level, sampling=(1., 1., 1.)): sampling) # Find and collect unique vertices, storing triangle verts as indices. - # Removes much redundancy and eliminates degenerate "triangles". - vert_list, tri_list = _marching_cubes_cy.unpack_unique_verts(raw_tris) + # Returns a true mesh with no degenerate faces. + verts, faces = _marching_cubes_cy.unpack_unique_verts(raw_tris) - return vert_list, tri_list + return np.asarray(verts), np.asarray(faces) def mesh_surface_area(verts, tris): @@ -127,17 +122,16 @@ def mesh_surface_area(verts, tris): Parameters ---------- - verts : list - List of length-3 NumPy arrays containing vertex coordinates. - Units in each dimension should be consistent. - tris : list + verts : (V, 3) array of floats + Array containing (x, y, z) coordinates for V unique mesh vertices. + faces : (F, 3) array of ints List of length-3 lists of integers, referencing vertex coordinates as provided in `verts` Returns ------- area : float - Surface area of mesh. Units in coordinates maintained, but squared. + Surface area of mesh. Units now [coordinate units] ** 2. Notes ----- @@ -145,13 +139,16 @@ def mesh_surface_area(verts, tris): `skimage.measure.marching_cubes`. For unit correct output, ensure correct `spacing` was passed to `skimage.measure.marching_cubes`. + This algorithm works properly only if the ``faces`` provided are all + triangles. + See Also -------- skimage.measure.marching_cubes """ - # Define two vector arrays `a` and `b` from triangle vertices - actual_verts = np.array([[verts[i] for i in tri] for tri in tris]) + # Fancy indexing to define two vector arrays from triangle vertices + actual_verts = verts[tris] a = actual_verts[:, 0, :] - actual_verts[:, 1, :] b = actual_verts[:, 0, :] - actual_verts[:, 2, :] del actual_verts From fcd20e5660742aa3d5d085ff79249caefe8a8bb3 Mon Sep 17 00:00:00 2001 From: "Josh Warner (Mac)" Date: Fri, 31 May 2013 10:29:09 -0500 Subject: [PATCH 547/736] FIX: use memoryviews, docfix, and change tri_list to face_list --- skimage/measure/_marching_cubes_cy.pyx | 666 ++++++++++++------------- 1 file changed, 333 insertions(+), 333 deletions(-) diff --git a/skimage/measure/_marching_cubes_cy.pyx b/skimage/measure/_marching_cubes_cy.pyx index 18e14ddb..7a280366 100644 --- a/skimage/measure/_marching_cubes_cy.pyx +++ b/skimage/measure/_marching_cubes_cy.pyx @@ -13,11 +13,11 @@ cdef inline double _get_fraction(double from_value, double to_value, return ((level - from_value) / (to_value - from_value)) -def unpack_unique_verts(list trilist): +def unpack_unique_vert_list(list trilist): """ - Converts a list of lists of tuples corresponding to triangle vertices into - a unique vertex list, and a list of triangle faces w/indices corresponding - to entries of the vertex list. + Convert_list a list of lists of tuples corresponding to triangle vertices + into a unique vertex list, and a list of triangle faces w/indices + corresponding to entries of the vertex list. """ cdef Py_ssize_t idx = 0 @@ -25,7 +25,7 @@ def unpack_unique_verts(list trilist): cdef Py_ssize_t i, j cdef dict vert_index = {} cdef list vert_list = [] - cdef list tri_list = [] + cdef list face_list = [] cdef list templist # Iterate over triangles @@ -50,13 +50,13 @@ def unpack_unique_verts(list trilist): else: templist.append(vert_index[vert]) - tri_list.append(templist) + face_list.append(templist) - return vert_list, tri_list + return vert_list, face_list -def iterate_and_store_3d(cnp.ndarray[double, ndim=3] arr, - double level, tuple sampling=(1., 1., 1.)): +def iterate_and_store_3d(double[:, :, ::1] arr, double level, + tuple sampling=(1., 1., 1.)): """Iterate across the given array in a marching-cubes fashion, looking for volumes with edges that cross 'level'. If such a volume is found, appropriate triangulations are added to a growing list of @@ -71,9 +71,9 @@ def iterate_and_store_3d(cnp.ndarray[double, ndim=3] arr, if arr.shape[0] < 2 or arr.shape[1] < 2 or arr.shape[2] < 2: raise ValueError("Input array must be at least 2x2x2.") if len(sampling) != 3: - raise ValueError("`sampling` must be of form (double, double, double)") + raise ValueError("`sampling` must be (double, double, double)") - cdef list tri_list = [] + cdef list face_list = [] cdef list norm_list = [] cdef Py_ssize_t n cdef bint odd_sampling, plus_z @@ -228,8 +228,8 @@ def iterate_and_store_3d(cnp.ndarray[double, ndim=3] arr, e12 = r1, c1, d0 + _get_fraction(v3, v7, level) - # Append appropriate triangles to the growing output `tri_list` - _append_tris(tri_list, cube_case, e1, e2, e3, e4, e5, + # Append appropriate triangles to the growing output `face_list` + _append_tris(face_list, cube_case, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) # Advance the coords indices @@ -246,10 +246,10 @@ def iterate_and_store_3d(cnp.ndarray[double, ndim=3] arr, coords[2] = 0 plus_z = False - return tri_list + return face_list -def _append_tris(list tri_list, unsigned char case, tuple e1, tuple e2, +def _append_tris(list face_list, unsigned char case, tuple e1, tuple e2, tuple e3, tuple e4, tuple e5, tuple e6, tuple e7, tuple e8, tuple e9, tuple e10, tuple e11, tuple e12): # Permits recursive use for duplicated planes to conserve code - it's @@ -257,731 +257,731 @@ def _append_tris(list tri_list, unsigned char case, tuple e1, tuple e2, if (case == 1): # front lower left corner - tri_list.append([e1, e4, e9]) + face_list.append([e1, e4, e9]) elif (case == 2): # front lower right corner - tri_list.append([e10, e2, e1]) + face_list.append([e10, e2, e1]) elif (case == 3): # front lower plane - tri_list.append([e2, e4, e9]) - tri_list.append([e2, e9, e10]) + face_list.append([e2, e4, e9]) + face_list.append([e2, e9, e10]) elif (case == 4): # front upper right corner - tri_list.append([e12, e3, e2]) + face_list.append([e12, e3, e2]) elif (case == 5): # lower left, upper right corners - _append_tris(tri_list, 1, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 1, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 4, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 4, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) elif (case == 6): # front right plane - tri_list.append([e12, e3, e1]) - tri_list.append([e12, e1, e10]) + face_list.append([e12, e3, e1]) + face_list.append([e12, e1, e10]) elif (case == 7): # Shelf including v1, v2, v3 - tri_list.append([e3, e4, e12]) - tri_list.append([e4, e9, e12]) - tri_list.append([e12, e9, e10]) + face_list.append([e3, e4, e12]) + face_list.append([e4, e9, e12]) + face_list.append([e12, e9, e10]) elif (case == 8): # front upper left corner - tri_list.append([e3, e11, e4]) + face_list.append([e3, e11, e4]) elif (case == 9): # front left plane - tri_list.append([e3, e11, e9]) - tri_list.append([e3, e9, e1]) + face_list.append([e3, e11, e9]) + face_list.append([e3, e9, e1]) elif (case == 10): # upper left, lower right corners - _append_tris(tri_list, 2, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 2, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 8, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 8, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) elif (case == 11): # Shelf including v4, v1, v2 - tri_list.append([e3, e11, e2]) - tri_list.append([e11, e10, e2]) - tri_list.append([e11, e9, e10]) + face_list.append([e3, e11, e2]) + face_list.append([e11, e10, e2]) + face_list.append([e11, e9, e10]) elif (case == 12): # front upper plane - tri_list.append([e11, e4, e12]) - tri_list.append([e2, e4, e12]) + face_list.append([e11, e4, e12]) + face_list.append([e2, e4, e12]) elif (case == 13): # Shelf including v1, v4, v3 - tri_list.append([e11, e9, e12]) - tri_list.append([e12, e9, e1]) - tri_list.append([e12, e1, e2]) + face_list.append([e11, e9, e12]) + face_list.append([e12, e9, e1]) + face_list.append([e12, e1, e2]) elif (case == 14): # Shelf including v2, v3, v4 - tri_list.append([e11, e10, e12]) - tri_list.append([e11, e4, e10]) - tri_list.append([e4, e1, e10]) + face_list.append([e11, e10, e12]) + face_list.append([e11, e4, e10]) + face_list.append([e4, e1, e10]) elif (case == 15): # Plane parallel to x-axis through middle - tri_list.append([e11, e9, e12]) - tri_list.append([e12, e9, e10]) + face_list.append([e11, e9, e12]) + face_list.append([e12, e9, e10]) elif (case == 16): # back lower left corner - tri_list.append([e8, e9, e5]) + face_list.append([e8, e9, e5]) elif (case == 17): # lower left plane - tri_list.append([e4, e1, e8]) - tri_list.append([e8, e1, e5]) + face_list.append([e4, e1, e8]) + face_list.append([e8, e1, e5]) elif (case == 18): # lower left back, lower right front corners - _append_tris(tri_list, 16, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 16, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 2, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 2, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) elif (case == 19): # Shelf including v1, v2, v5 - tri_list.append([e8, e4, e2]) - tri_list.append([e8, e2, e10]) - tri_list.append([e8, e10, e5]) + face_list.append([e8, e4, e2]) + face_list.append([e8, e2, e10]) + face_list.append([e8, e10, e5]) elif (case == 20): # lower left back, upper right front corners - _append_tris(tri_list, 16, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 16, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 4, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 4, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) elif (case == 21): # lower left plane + upper right front corner, v1, v3, v5 - _append_tris(tri_list, 17, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 17, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 4, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 4, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) elif (case == 22): # front right plane + lower left back corner, v2, v3, v5 - _append_tris(tri_list, 16, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 16, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 6, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 6, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) elif (case == 23): # Rotated case 14 in the paper - tri_list.append([e3, e10, e8]) - tri_list.append([e3, e10, e12]) - tri_list.append([e8, e10, e5]) - tri_list.append([e3, e4, e8]) + face_list.append([e3, e10, e8]) + face_list.append([e3, e10, e12]) + face_list.append([e8, e10, e5]) + face_list.append([e3, e4, e8]) elif (case == 24): # upper front left, lower back left corners - _append_tris(tri_list, 16, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 16, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 8, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 8, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) elif (case == 25): # Shelf including v1, v4, v5 - tri_list.append([e1, e5, e3]) - tri_list.append([e3, e8, e11]) - tri_list.append([e3, e5, e8]) + face_list.append([e1, e5, e3]) + face_list.append([e3, e8, e11]) + face_list.append([e3, e5, e8]) elif (case == 26): # Three isolated corners - _append_tris(tri_list, 2, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 2, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 8, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 8, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 16, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 16, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) elif (case == 27): # Full corner v1, case 9 in paper: (v1, v2, v4, v5) - tri_list.append([e11, e3, e2]) - tri_list.append([e11, e2, e10]) - tri_list.append([e10, e11, e8]) - tri_list.append([e8, e5, e10]) + face_list.append([e11, e3, e2]) + face_list.append([e11, e2, e10]) + face_list.append([e10, e11, e8]) + face_list.append([e8, e5, e10]) elif (case == 28): # upper front plane + corner v5 - _append_tris(tri_list, 16, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 16, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 12, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 12, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) elif (case == 29): # special case of 11 in the paper: (v1, v3, v4, v5) - tri_list.append([e11, e5, e2]) - tri_list.append([e11, e12, e2]) - tri_list.append([e11, e5, e8]) - tri_list.append([e2, e1, e5]) + face_list.append([e11, e5, e2]) + face_list.append([e11, e12, e2]) + face_list.append([e11, e5, e8]) + face_list.append([e2, e1, e5]) elif (case == 30): # Shelf (v2, v3, v4) and lower left back corner - _append_tris(tri_list, 14, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 14, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 16, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 16, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) elif (case == 31): # Shelf: (v6, v7, v8) by inversion - tri_list.append([e11, e12, e10]) - tri_list.append([e11, e8, e10]) - tri_list.append([e8, e10, e5]) + face_list.append([e11, e12, e10]) + face_list.append([e11, e8, e10]) + face_list.append([e8, e10, e5]) elif (case == 32): # lower right back corner - tri_list.append([e6, e5, e10]) + face_list.append([e6, e5, e10]) elif (case == 33): # lower right back, lower left front corners - _append_tris(tri_list, 1, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 1, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 32, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 32, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) elif (case == 34): # lower right plane - tri_list.append([e1, e2, e5]) - tri_list.append([e2, e6, e5]) + face_list.append([e1, e2, e5]) + face_list.append([e2, e6, e5]) elif (case == 35): # Shelf: v1, v2, v6 - tri_list.append([e4, e2, e6]) - tri_list.append([e4, e9, e6]) - tri_list.append([e6, e9, e5]) + face_list.append([e4, e2, e6]) + face_list.append([e4, e9, e6]) + face_list.append([e6, e9, e5]) elif (case == 36): # upper right front, lower right back corners - _append_tris(tri_list, 32, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 32, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 4, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 4, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) elif (case == 37): # lower left front, upper right front, lower right back corners - _append_tris(tri_list, 32, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 32, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 4, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 4, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 1, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 1, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) elif (case == 38): # Shelf: v2, v3, v6 - tri_list.append([e3, e1, e5]) - tri_list.append([e3, e5, e12]) - tri_list.append([e12, e5, e6]) + face_list.append([e3, e1, e5]) + face_list.append([e3, e5, e12]) + face_list.append([e12, e5, e6]) elif (case == 39): # Full corner v2: (v1, v2, v3, v6) - tri_list.append([e3, e4, e5]) - tri_list.append([e4, e9, e5]) - tri_list.append([e3, e5, e6]) - tri_list.append([e3, e12, e6]) + face_list.append([e3, e4, e5]) + face_list.append([e4, e9, e5]) + face_list.append([e3, e5, e6]) + face_list.append([e3, e12, e6]) elif (case == 40): # upper left front, lower right back corners - _append_tris(tri_list, 32, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 32, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 8, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 8, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) elif (case == 41): # front left plane, lower right back corner - _append_tris(tri_list, 32, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 32, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 9, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 9, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) elif (case == 42): # lower right plane, upper front left corner - _append_tris(tri_list, 8, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 8, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 34, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 34, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) elif (case == 43): # Rotated case 11 in paper - tri_list.append([e11, e3, e9]) - tri_list.append([e3, e9, e6]) - tri_list.append([e3, e2, e6]) - tri_list.append([e9, e5, e6]) + face_list.append([e11, e3, e9]) + face_list.append([e3, e9, e6]) + face_list.append([e3, e2, e6]) + face_list.append([e9, e5, e6]) elif (case == 44): # upper front plane, lower right back corner - _append_tris(tri_list, 12, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 12, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 32, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 32, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) elif (case == 45): # Shelf: (v1, v3, v4) + lower right back corner - _append_tris(tri_list, 13, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 13, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 32, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 32, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) elif (case == 46): # Rotated case 14 in paper - tri_list.append([e4, e11, e12]) - tri_list.append([e4, e12, e5]) - tri_list.append([e12, e5, e6]) - tri_list.append([e4, e5, e1]) + face_list.append([e4, e11, e12]) + face_list.append([e4, e12, e5]) + face_list.append([e12, e5, e6]) + face_list.append([e4, e5, e1]) elif (case == 47): # Shelf: (v5, v8, v7) by inversion - tri_list.append([e11, e9, e12]) - tri_list.append([e12, e9, e5]) - tri_list.append([e12, e5, e6]) + face_list.append([e11, e9, e12]) + face_list.append([e12, e9, e5]) + face_list.append([e12, e5, e6]) elif (case == 48): # Back lower plane - tri_list.append([e9, e10, e6]) - tri_list.append([e9, e6, e8]) + face_list.append([e9, e10, e6]) + face_list.append([e9, e6, e8]) elif (case == 49): # Shelf: (v1, v5, v6) - tri_list.append([e4, e8, e6]) - tri_list.append([e4, e6, e1]) - tri_list.append([e6, e1, e10]) + face_list.append([e4, e8, e6]) + face_list.append([e4, e6, e1]) + face_list.append([e6, e1, e10]) elif (case == 50): # Shelf: (v2, v5, v6) - tri_list.append([e8, e6, e2]) - tri_list.append([e8, e2, e1]) - tri_list.append([e8, e9, e1]) + face_list.append([e8, e6, e2]) + face_list.append([e8, e2, e1]) + face_list.append([e8, e9, e1]) elif (case == 51): # Plane through middle of cube, parallel to x-z axis - tri_list.append([e4, e8, e2]) - tri_list.append([e8, e2, e6]) + face_list.append([e4, e8, e2]) + face_list.append([e8, e2, e6]) elif (case == 52): # Back lower plane, and front upper right corner - _append_tris(tri_list, 48, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 48, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 4, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 4, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) elif (case == 53): # Shelf (v1, v5, v6) and front upper right corner - _append_tris(tri_list, 49, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 49, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 4, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 4, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) elif (case == 54): # Rotated case 11 from paper (v2, v3, v5, v6) - tri_list.append([e1, e9, e3]) - tri_list.append([e9, e3, e6]) - tri_list.append([e9, e8, e6]) - tri_list.append([e12, e3, e6]) + face_list.append([e1, e9, e3]) + face_list.append([e9, e3, e6]) + face_list.append([e9, e8, e6]) + face_list.append([e12, e3, e6]) elif (case == 55): # Shelf: (v4, v8, v7) by inversion - tri_list.append([e4, e8, e6]) - tri_list.append([e4, e6, e3]) - tri_list.append([e6, e3, e12]) + face_list.append([e4, e8, e6]) + face_list.append([e4, e6, e3]) + face_list.append([e6, e3, e12]) elif (case == 56): # Back lower plane + upper left front corner - _append_tris(tri_list, 48, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 48, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 8, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 8, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) elif (case == 57): # Rotated case 14 from paper (v4, v1, v5, v6) - tri_list.append([e3, e11, e8]) - tri_list.append([e3, e8, e10]) - tri_list.append([e10, e6, e8]) - tri_list.append([e3, e1, e10]) + face_list.append([e3, e11, e8]) + face_list.append([e3, e8, e10]) + face_list.append([e10, e6, e8]) + face_list.append([e3, e1, e10]) elif (case == 58): # Shelf: (v2, v6, v5) + upper left front corner - _append_tris(tri_list, 50, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 50, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 8, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 8, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) elif (case == 59): # Shelf: (v3, v7, v8) by inversion - tri_list.append([e2, e6, e8]) - tri_list.append([e8, e2, e3]) - tri_list.append([e8, e3, e11]) + face_list.append([e2, e6, e8]) + face_list.append([e8, e2, e3]) + face_list.append([e8, e3, e11]) elif (case == 60): # AMBIGUOUS CASE: parallel planes (front upper, back lower) - _append_tris(tri_list, 48, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 48, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 12, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 12, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) elif (case == 61): # Upper back plane + lower right front corner by inversion - _append_tris(tri_list, 63, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 63, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 2, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 2, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) elif (case == 62): # Upper back plane + lower left front corner by inversion - _append_tris(tri_list, 63, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 63, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 1, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 1, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) elif (case == 63): # Upper back plane - tri_list.append([e11, e12, e6]) - tri_list.append([e11, e8, e6]) + face_list.append([e11, e12, e6]) + face_list.append([e11, e8, e6]) elif (case == 64): # Upper right back corner - tri_list.append([e12, e7, e6]) + face_list.append([e12, e7, e6]) elif (case == 65): # upper right back, lower left front corners - _append_tris(tri_list, 64, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 64, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 1, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 1, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) elif (case == 66): # upper right back, lower right front corners - _append_tris(tri_list, 64, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 64, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 2, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 2, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) elif (case == 67): # lower front plane + upper right back corner - _append_tris(tri_list, 64, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 64, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 3, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 3, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) elif (case == 68): # upper right plane - tri_list.append([e3, e2, e6]) - tri_list.append([e3, e7, e6]) + face_list.append([e3, e2, e6]) + face_list.append([e3, e7, e6]) elif (case == 69): # Upper right plane, lower left front corner - _append_tris(tri_list, 68, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 68, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 1, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 1, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) elif (case == 70): # Shelf: (v2, v3, v7) - tri_list.append([e1, e3, e7]) - tri_list.append([e1, e10, e7]) - tri_list.append([e7, e10, e6]) + face_list.append([e1, e3, e7]) + face_list.append([e1, e10, e7]) + face_list.append([e7, e10, e6]) elif (case == 71): # Rotated version of case 11 in paper (v1, v2, v3, v7) - tri_list.append([e10, e7, e4]) - tri_list.append([e4, e3, e7]) - tri_list.append([e10, e4, e9]) - tri_list.append([e7, e10, e6]) + face_list.append([e10, e7, e4]) + face_list.append([e4, e3, e7]) + face_list.append([e10, e4, e9]) + face_list.append([e7, e10, e6]) elif (case == 72): # upper left front, upper right back corners - _append_tris(tri_list, 64, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 64, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 8, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 8, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) elif (case == 73): # front left plane, upper right back corner - _append_tris(tri_list, 64, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 64, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 9, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 9, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) elif (case == 74): # Three isolated corners, exactly case 7 in paper - _append_tris(tri_list, 64, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 64, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 8, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 8, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 2, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 2, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) elif (case == 75): # Shelf: (v1, v2, v4) + upper right back corner - _append_tris(tri_list, 64, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 64, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 11, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 11, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) elif (case == 76): # Shelf: (v4, v3, v7) - tri_list.append([e4, e2, e6]) - tri_list.append([e4, e11, e7]) - tri_list.append([e4, e7, e6]) + face_list.append([e4, e2, e6]) + face_list.append([e4, e11, e7]) + face_list.append([e4, e7, e6]) elif (case == 77): # Rotated case 14 in paper (v1, v4, v3, v7) - tri_list.append([e11, e9, e1]) - tri_list.append([e11, e1, e6]) - tri_list.append([e1, e6, e2]) - tri_list.append([e11, e6, e7]) + face_list.append([e11, e9, e1]) + face_list.append([e11, e1, e6]) + face_list.append([e1, e6, e2]) + face_list.append([e11, e6, e7]) elif (case == 78): # Full corner v3: (v2, v3, v4, v7) - tri_list.append([e1, e4, e7]) - tri_list.append([e1, e7, e6]) - tri_list.append([e4, e11, e7]) - tri_list.append([e1, e10, e6]) + face_list.append([e1, e4, e7]) + face_list.append([e1, e7, e6]) + face_list.append([e4, e11, e7]) + face_list.append([e1, e10, e6]) elif (case == 79): # Shelf: (v6, v5, v8) by inversion - tri_list.append([e9, e11, e10]) - tri_list.append([e11, e7, e10]) - tri_list.append([e7, e10, e6]) + face_list.append([e9, e11, e10]) + face_list.append([e11, e7, e10]) + face_list.append([e7, e10, e6]) elif (case == 80): # lower left back, upper right back corners (v5, v7) - _append_tris(tri_list, 64, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 64, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 16, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 16, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) elif (case == 81): # lower left plane, upper right back corner - _append_tris(tri_list, 64, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 64, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 17, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 17, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) elif (case == 82): # isolated corners (v2, v5, v7) - _append_tris(tri_list, 64, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 64, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 16, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 16, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 2, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 2, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) elif (case == 83): # Shelf: (v1, v2, v5) + upper right back corner - _append_tris(tri_list, 64, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 64, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 19, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 19, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) elif (case == 84): # upper right plane, lower left back corner - _append_tris(tri_list, 16, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 16, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 68, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 68, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) elif (case == 85): # AMBIGUOUS CASE: upper right and lower left parallel planes - _append_tris(tri_list, 17, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 17, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 68, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 68, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) elif (case == 86): # Shelf: (v2, v3, v7) + lower left back corner - _append_tris(tri_list, 70, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 70, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 16, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 16, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) elif (case == 87): # Upper left plane + lower right back corner, by inversion - _append_tris(tri_list, 119, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 119, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 32, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 32, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) elif (case == 88): # Isolated corners v4, v5, v7 - _append_tris(tri_list, 64, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 64, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 16, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 16, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 8, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 8, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) elif (case == 89): # Shelf: (v1, v4, v5) + isolated corner v7 - _append_tris(tri_list, 25, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 25, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 64, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 64, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) elif (case == 90): # Four isolated corners v2, v4, v5, v7 - _append_tris(tri_list, 2, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 2, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 8, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 8, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 16, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 16, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 64, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 64, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) elif (case == 91): # Three isolated corners, v3, v6, v8 by inversion - _append_tris(tri_list, 4, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 4, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 32, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 32, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 127, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 127, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) elif (case == 92): # Shelf (v4, v3, v7) + isolated corner v5 - _append_tris(tri_list, 76, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 76, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 16, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 16, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) elif (case == 93): # Lower right plane + isolated corner v8 by inversion - _append_tris(tri_list, 127, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 127, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 34, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 34, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) elif (case == 94): # Isolated corners v1, v6, v8 by inversion - _append_tris(tri_list, 1, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 1, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 32, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 32, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 127, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 127, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) elif (case == 95): # Isolated corners v6, v8 by inversion - _append_tris(tri_list, 32, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 32, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 127, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 127, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) elif (case == 96): # back right plane - tri_list.append([e7, e12, e5]) - tri_list.append([e5, e10, e12]) + face_list.append([e7, e12, e5]) + face_list.append([e5, e10, e12]) elif (case == 97): # back right plane + isolated corner v1 - _append_tris(tri_list, 96, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 96, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 1, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 1, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) elif (case == 98): # Shelf: (v2, v6, v7) - tri_list.append([e1, e7, e5]) - tri_list.append([e7, e1, e12]) - tri_list.append([e1, e12, e2]) + face_list.append([e1, e7, e5]) + face_list.append([e7, e1, e12]) + face_list.append([e1, e12, e2]) elif (case == 99): # Rotated case 14 in paper: (v1, v2, v6, v7) - tri_list.append([e9, e2, e7]) - tri_list.append([e9, e2, e4]) - tri_list.append([e2, e7, e12]) - tri_list.append([e7, e9, e5]) + face_list.append([e9, e2, e7]) + face_list.append([e9, e2, e4]) + face_list.append([e2, e7, e12]) + face_list.append([e7, e9, e5]) elif (case == 100): # Shelf: (v3, v6, v7) - tri_list.append([e3, e7, e5]) - tri_list.append([e3, e5, e2]) - tri_list.append([e2, e5, e10]) + face_list.append([e3, e7, e5]) + face_list.append([e3, e5, e2]) + face_list.append([e2, e5, e10]) elif (case == 101): # Shelf: (v3, v6, v7) + isolated corner v1 - _append_tris(tri_list, 100, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 100, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 1, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 1, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) elif (case == 102): # Plane bisecting left-right halves of cube - tri_list.append([e1, e3, e7]) - tri_list.append([e1, e7, e5]) + face_list.append([e1, e3, e7]) + face_list.append([e1, e7, e5]) elif (case == 103): # Shelf: (v4, v5, v8) by inversion - tri_list.append([e3, e7, e5]) - tri_list.append([e3, e5, e4]) - tri_list.append([e4, e5, e9]) + face_list.append([e3, e7, e5]) + face_list.append([e3, e5, e4]) + face_list.append([e4, e5, e9]) elif (case == 104): # Back right plane + isolated corner v4 - _append_tris(tri_list, 8, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 8, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 96, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 96, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) elif (case == 105): # AMBIGUOUS CASE: back right and front left planes - _append_tris(tri_list, 96, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 96, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 9, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 9, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) elif (case == 106): # Shelf: (v2, v6, v7) + isolated corner v4 - _append_tris(tri_list, 98, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 98, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 8, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 8, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) elif (case == 107): # Back left plane + isolated corner v3 by inversion - _append_tris(tri_list, 4, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 4, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 111, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 111, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) elif (case == 108): # Rotated case 11 from paper: (v4, v3, v7, v6) - tri_list.append([e4, e10, e7]) - tri_list.append([e4, e10, e2]) - tri_list.append([e4, e11, e7]) - tri_list.append([e7, e10, e5]) + face_list.append([e4, e10, e7]) + face_list.append([e4, e10, e2]) + face_list.append([e4, e11, e7]) + face_list.append([e7, e10, e5]) elif (case == 109): # Back left plane + isolated corner v2 by inversion - _append_tris(tri_list, 111, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 111, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 2, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 2, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) elif (case == 110): # Shelf: (v1, v5, v8) by inversion - tri_list.append([e1, e5, e7]) - tri_list.append([e1, e7, e11]) - tri_list.append([e1, e11, e4]) + face_list.append([e1, e5, e7]) + face_list.append([e1, e7, e11]) + face_list.append([e1, e11, e4]) elif (case == 111): # Back left plane - tri_list.append([e11, e9, e7]) - tri_list.append([e9, e7, e5]) + face_list.append([e11, e9, e7]) + face_list.append([e9, e7, e5]) elif (case == 112): # Shelf: (v5, v6, v7) - tri_list.append([e9, e10, e12]) - tri_list.append([e9, e12, e7]) - tri_list.append([e9, e7, e8]) + face_list.append([e9, e10, e12]) + face_list.append([e9, e12, e7]) + face_list.append([e9, e7, e8]) elif (case == 113): # Exactly case 11 from paper: (v1, v5, v6, v7) - tri_list.append([e1, e8, e12]) - tri_list.append([e1, e8, e4]) - tri_list.append([e8, e7, e12]) - tri_list.append([e12, e1, e10]) + face_list.append([e1, e8, e12]) + face_list.append([e1, e8, e4]) + face_list.append([e8, e7, e12]) + face_list.append([e12, e1, e10]) elif (case == 114): # Full corner v6: (v2, v6, v7, v5) - tri_list.append([e1, e9, e7]) - tri_list.append([e1, e7, e12]) - tri_list.append([e1, e12, e2]) - tri_list.append([e9, e8, e7]) + face_list.append([e1, e9, e7]) + face_list.append([e1, e7, e12]) + face_list.append([e1, e12, e2]) + face_list.append([e9, e8, e7]) elif (case == 115): # Shelf: (v3, v4, v8) - tri_list.append([e2, e4, e8]) - tri_list.append([e2, e12, e7]) - tri_list.append([e2, e8, e7]) + face_list.append([e2, e4, e8]) + face_list.append([e2, e12, e7]) + face_list.append([e2, e8, e7]) elif (case == 116): # Rotated case 14 in paper: (v5, v6, v7, v3) - tri_list.append([e9, e2, e7]) - tri_list.append([e9, e2, e10]) - tri_list.append([e9, e8, e7]) - tri_list.append([e2, e3, e7]) + face_list.append([e9, e2, e7]) + face_list.append([e9, e2, e10]) + face_list.append([e9, e8, e7]) + face_list.append([e2, e3, e7]) elif (case == 117): # upper left plane + isolated corner v2 by inversion - _append_tris(tri_list, 2, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 2, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 119, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 119, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) elif (case == 118): # Shelf: (v1, v4, v8) - tri_list.append([e1, e3, e7]) - tri_list.append([e7, e1, e8]) - tri_list.append([e1, e8, e9]) + face_list.append([e1, e3, e7]) + face_list.append([e7, e1, e8]) + face_list.append([e1, e8, e9]) elif (case == 119): # Upper left plane - tri_list.append([e4, e3, e7]) - tri_list.append([e4, e8, e7]) + face_list.append([e4, e3, e7]) + face_list.append([e4, e8, e7]) elif (case == 120): # Shelf: (v5, v6, v7) + isolated corner v4 - _append_tris(tri_list, 112, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 112, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 8, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 8, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) elif (case == 121): # Front right plane + isolated corner v8 - _append_tris(tri_list, 6, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 6, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 127, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 127, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) elif (case == 122): # Isolated corners v1, v3, v8 - _append_tris(tri_list, 1, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 1, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 4, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 4, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 127, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 127, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) elif (case == 123): # Isolated corners v3, v8 - _append_tris(tri_list, 4, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 4, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 127, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 127, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) elif (case == 124): # Front lower plane + isolated corner v8 - _append_tris(tri_list, 3, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 3, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 127, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 127, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) elif (case == 125): # Isolated corners v2, v8 - _append_tris(tri_list, 2, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 2, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 127, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 127, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) elif (case == 126): # Isolated corners v1, v8 - _append_tris(tri_list, 1, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 1, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 127, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 127, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) elif (case == 127): # Isolated corner v8 - tri_list.append([e11, e7, e8]) + face_list.append([e11, e7, e8]) elif (case == 150): # AMBIGUOUS CASE: back right and front left planes # In these cube_case > 127 cases, the vertices are identical BUT # they are connected in the opposite fashion. - _append_tris(tri_list, 6, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 6, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 111, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 111, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) elif (case == 170): # AMBIGUOUS CASE: upper left and lower right planes # In these cube_case > 127 cases, the vertices are identical BUT # they are connected in the opposite fashion. - _append_tris(tri_list, 119, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 119, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 34, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 34, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) elif (case == 195): # AMBIGUOUS CASE: back upper and front lower planes # In these cube_case > 127 cases, the vertices are identical BUT # they are connected in the opposite fashion. - _append_tris(tri_list, 63, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 63, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) - _append_tris(tri_list, 3, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, + _append_tris(face_list, 3, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12) return From 19b8831a0d2cf3325cbfa2868bf8ad9432db0ccd Mon Sep 17 00:00:00 2001 From: "Josh Warner (Mac)" Date: Sat, 1 Jun 2013 21:26:16 -0500 Subject: [PATCH 548/736] FIX: incorrect name for `unpack_unique_verts`, revert axis ordering --- skimage/measure/_marching_cubes_cy.pyx | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/skimage/measure/_marching_cubes_cy.pyx b/skimage/measure/_marching_cubes_cy.pyx index 7a280366..ec817b15 100644 --- a/skimage/measure/_marching_cubes_cy.pyx +++ b/skimage/measure/_marching_cubes_cy.pyx @@ -13,9 +13,9 @@ cdef inline double _get_fraction(double from_value, double to_value, return ((level - from_value) / (to_value - from_value)) -def unpack_unique_vert_list(list trilist): +def unpack_unique_verts(list trilist): """ - Convert_list a list of lists of tuples corresponding to triangle vertices + Convert a list of lists of tuples corresponding to triangle vertices into a unique vertex list, and a list of triangle faces w/indices corresponding to entries of the vertex list. @@ -152,14 +152,14 @@ def iterate_and_store_3d(double[:, :, ::1] arr, double level, # We use a right-handed coordinate system, UNlike the paper, but want # to index in agreement - the coordinate adjustment takes place here. - v1 = arr[z0, y0, x0] - v2 = arr[z0, y0, x1] - v3 = arr[z0, y1, x1] - v4 = arr[z0, y1, x0] - v5 = arr[z1, y0, x0] - v6 = arr[z1, y0, x1] - v7 = arr[z1, y1, x1] - v8 = arr[z1, y1, x0] + v1 = arr[x0, y0, z0] + v2 = arr[x1, y0, z0] + v3 = arr[x1, y1, z0] + v4 = arr[x0, y1, z0] + v5 = arr[x0, y0, z1] + v6 = arr[x1, y0, z1] + v7 = arr[x1, y1, z1] + v8 = arr[x0, y1, z1] # Unique triangulation cases cube_case = 0 @@ -233,7 +233,7 @@ def iterate_and_store_3d(double[:, :, ::1] arr, double level, e6, e7, e8, e9, e10, e11, e12) # Advance the coords indices - if coords[2] < arr.shape[0] - 2: + if coords[2] < arr.shape[2] - 2: coords[2] += 1 plus_z = True elif coords[1] < arr.shape[1] - 2: From b6f25906d652f60631ac769f551785e0582d78e4 Mon Sep 17 00:00:00 2001 From: "Josh Warner (Mac)" Date: Thu, 6 Jun 2013 15:56:29 -0500 Subject: [PATCH 549/736] DOCFIX: clarify docstrings and output vertex dimension ordering --- skimage/measure/_marching_cubes.py | 6 +++--- skimage/measure/_marching_cubes_cy.pyx | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/skimage/measure/_marching_cubes.py b/skimage/measure/_marching_cubes.py index 418f945e..6af60672 100644 --- a/skimage/measure/_marching_cubes.py +++ b/skimage/measure/_marching_cubes.py @@ -9,8 +9,7 @@ def marching_cubes(volume, level, sampling=(1., 1., 1.)): Parameters ---------- volume : (M, N, P) array of doubles - Input data volume to find isosurfaces. Will be cast to `np.float64` if - not provided in this format. + Input data volume to find isosurfaces. Will be cast to `np.float64`. level : float Contour value to search for isosurfaces in `volume`. sampling : length-3 tuple of floats @@ -20,7 +19,8 @@ def marching_cubes(volume, level, sampling=(1., 1., 1.)): Returns ------- verts : (V, 3) array - Spatial (x, y, z) coordinates for V unique vertices in mesh. + Spatial coordinates for V unique mesh vertices. Coordinate order + matches input `volume` (M, N, P). faces : (F, 3) array Define triangular faces via referencing vertex indices from ``verts``. This algorithm specifically outputs triangles, so each face has diff --git a/skimage/measure/_marching_cubes_cy.pyx b/skimage/measure/_marching_cubes_cy.pyx index ec817b15..4343c1e5 100644 --- a/skimage/measure/_marching_cubes_cy.pyx +++ b/skimage/measure/_marching_cubes_cy.pyx @@ -60,7 +60,7 @@ def iterate_and_store_3d(double[:, :, ::1] arr, double level, """Iterate across the given array in a marching-cubes fashion, looking for volumes with edges that cross 'level'. If such a volume is found, appropriate triangulations are added to a growing list of - triangles to be returned by the function. + faces to be returned by this function. If `sampling` is not provided, vertices are returned in the indexing coordinate system (assuming all 3 spatial dimensions sampled equally). From 997339beae952b0a3d63644546d02b257164a1a2 Mon Sep 17 00:00:00 2001 From: "Josh Warner (Mac)" Date: Tue, 18 Jun 2013 23:09:43 -0500 Subject: [PATCH 550/736] FEAT: add tests for marching cubes and mesh surface area --- skimage/measure/tests/test_marching_cubes.py | 141 +++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 skimage/measure/tests/test_marching_cubes.py diff --git a/skimage/measure/tests/test_marching_cubes.py b/skimage/measure/tests/test_marching_cubes.py new file mode 100644 index 00000000..e8135788 --- /dev/null +++ b/skimage/measure/tests/test_marching_cubes.py @@ -0,0 +1,141 @@ +import numpy as np +from numpy.testing import assert_raises +from scipy.special import (ellipkinc as ellip_F, ellipeinc as ellip_E) + +from skimage.measure import marching_cubes, mesh_surface_area + + +def _ellipsoid(a, b, c, sampling=(1., 1., 1.), info=False, tight=False, + levelset=False): + """ + Generates ellipsoid with semimajor axes aligned with grid dimensions, + on grid with specified `sampling`. + + Parameters + ---------- + a : float + Length of semimajor axis aligned with x-axis + b : float + Length of semimajor axis aligned with y-axis + c : float + Length of semimajor axis aligned with z-axis + sampling : tuple of floats, length 3 + Sampling in each spatial dimension + info : bool + If False, only `bool_arr` returned. + If True, (`bool_arr`, `vol`, `surf`) returned; the additional + values are analytical volume and surface area calculated for + this ellipsoid. + tight : bool + Controls if the ellipsoid will precisely be contained within + the returned volume (tight=True) or if each dimension will be + 2 longer than necessary (tight=False). For algorithms which + need both sides of a contour, use False. + levelset : bool + If True, returns the level set for this ellipsoid (signed level + set about zero, with positive denoting interior) as np.float64. + False returns a binarized version of said level set. + + Returns + ------- + bool_arr : (N, M, P) array + Sphere in an appropriately sized boolean array. + vol : float + Analytically calculated volume of ellipsoid. Only returned if + `info` is True. + surf : float + Analytically calculated surface area of ellipsoid. Only returned + if `info` is True. + + """ + if not tight: + offset = np.r_[1, 1, 1] * np.r_[sampling] + else: + offset = np.r_[0, 0, 0] + + # Calculate limits, and ensure output volume is odd & symmetric + low = np.ceil((-np.r_[a, b, c] - offset)) + high = np.floor((np.r_[a, b, c] + offset + 1)) + for dim in range(3): + if (high[dim] - low[dim]) % 2 == 0: + low[dim] -= 1 + num = np.arange(low[dim], high[dim], sampling[dim]) + if 0 not in num: + low[dim] -= np.max(num[num < 0]) + + # Generate (anisotropic) spatial grid + x, y, z = np.mgrid[low[0]:high[0]:sampling[0], + low[1]:high[1]:sampling[1], + low[2]:high[2]:sampling[2]] + + if not levelset: + arr = ((x / float(a)) ** 2 + + (y / float(b)) ** 2 + + (z / float(c)) ** 2) <= 1 + else: + arr = ((x / float(a)) ** 2 + + (y / float(b)) ** 2 + + (z / float(c)) ** 2) - 1 + + if not info: + return arr + else: + # Surface calculation requires a >= b >= c and a != c. + abc = [a, b, c] + abc.sort(reverse=True) + a = abc[0] + b = abc[1] + c = abc[2] + + # Volume + vol = 4 / 3. * np.pi * a * b * c + + # Analytical ellipsoid surface area + phi = np.arcsin((1. - (c ** 2 / (a ** 2.))) ** 0.5) + d = float((a ** 2 - c ** 2) ** 0.5) + m = (a ** 2 * (b ** 2 - c ** 2) / + float(b ** 2 * (a ** 2 - c ** 2))) + F = ellip_F(phi, m) + E = ellip_E(phi, m) + + surf = 2 * np.pi * (c ** 2 + + b * c ** 2 / d * F + + b * d * E) + + return arr, vol, surf + + +def test_marching_cubes_isotropic(): + ellipsoid_isotropic, _, surf = _ellipsoid(6, 10, 16, + levelset=True, + info=True) + verts, faces = marching_cubes(ellipsoid_isotropic, 0.) + surf_calc = mesh_surface_area(verts, faces) + + # Test within 1% tolerance for isotropic. Will always underestimate. + assert surf > surf_calc and surf_calc > surf * 0.99 + + +def test_marching_cubes_anisotropic(): + sampling = (1., 10 / 6., 16 / 6.) + ellipsoid_isotropic, _, surf = _ellipsoid(6, 10, 16, + sampling=sampling, + levelset=True, + info=True) + verts, faces = marching_cubes(ellipsoid_isotropic, 0., + sampling=sampling) + surf_calc = mesh_surface_area(verts, faces) + # Test within 1.5% tolerance for anisotropic. Will always underestimate. + assert surf > surf_calc and surf_calc > surf * 0.985 + + +def test_invalid_input(): + assert_raises(ValueError, marching_cubes, np.zeros((2, 2, 1)), 0) + assert_raises(ValueError, marching_cubes, np.zeros((2, 2, 1)), 1) + assert_raises(ValueError, marching_cubes, np.ones((3, 3, 3)), 1, + sampling=(1, 2)) + assert_raises(ValueError, marching_cubes, np.zeros((20, 20)), 0) + + +if __name__ == '__main__': + np.testing.run_module_suite() From 1bcc2b418a13421ca6898208d0b24dff26567fef Mon Sep 17 00:00:00 2001 From: "K.-Michael Aye" Date: Sat, 31 Aug 2013 22:48:49 -0700 Subject: [PATCH 551/736] removing some unused selem declarations --- doc/examples/applications/plot_rank_filters.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/doc/examples/applications/plot_rank_filters.py b/doc/examples/applications/plot_rank_filters.py index ef333e32..87dad0d0 100644 --- a/doc/examples/applications/plot_rank_filters.py +++ b/doc/examples/applications/plot_rank_filters.py @@ -152,7 +152,6 @@ the central one. from skimage.filter.rank import bilateral_mean noisy_image = img_as_ubyte(data.camera()) -selem = disk(10) bilat = bilateral_mean(noisy_image.astype(np.uint16), disk(20), s0=10, s1=10) @@ -254,7 +253,6 @@ picture. from skimage.filter.rank import autolevel noisy_image = img_as_ubyte(data.camera()) -selem = disk(10) auto = autolevel(noisy_image.astype(np.uint16), disk(20)) From 74b932e1be5b852c448a504bf791b29f03319be5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 1 Sep 2013 16:03:14 +0200 Subject: [PATCH 552/736] Remove unnecessary mirror option --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 6b2dc4c6..91b9b5fa 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,7 +21,7 @@ install: - sudo apt-get install libfreeimage3 - if [[ $PYVER == '2.7' ]]; then sudo apt-get install $PYTHON-matplotlib; fi - if [[ $PYVER == '3.2' ]]; then sudo pip-$PYVER install git+git://github.com/matplotlib/matplotlib.git@v1.2.x; fi - - sudo pip-$PYVER install flake8 --use-mirrors + - sudo pip-$PYVER install flake8 - $PYTHON setup.py build - sudo $PYTHON setup.py install script: From fd729a4e30f87fc6b35ad5e81f241c7065f87683 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sat, 31 Aug 2013 17:28:48 +0200 Subject: [PATCH 553/736] Improve SLIC --- skimage/segmentation/_slic.pyx | 80 ++++++++++++++---------- skimage/segmentation/slic_superpixels.py | 41 +++++++----- skimage/segmentation/tests/test_slic.py | 2 +- 3 files changed, 73 insertions(+), 50 deletions(-) diff --git a/skimage/segmentation/_slic.pyx b/skimage/segmentation/_slic.pyx index 5df747ad..13e05bc2 100644 --- a/skimage/segmentation/_slic.pyx +++ b/skimage/segmentation/_slic.pyx @@ -2,31 +2,32 @@ #cython: boundscheck=False #cython: nonecheck=False #cython: wraparound=False -import numpy as np -from scipy import ndimage +from libc.float cimport DBL_MAX -from skimage.util import img_as_float, regular_grid -from skimage.color import rgb2lab, gray2rgb +import numpy as np +cimport numpy as cnp + +from skimage.util import regular_grid def _slic_cython(double[:, :, :, ::1] image_zyx, Py_ssize_t[:, :, ::1] nearest_mean, double[:, :, ::1] distance, - double[:, ::1] means, + double[:, ::1] clusters, Py_ssize_t max_iter, Py_ssize_t n_segments): """Helper function for SLIC segmentation. Parameters ---------- - image_zyx : 4D array of double, shape (Z, Y, X, 6) + image_zyx : 4D array of double, shape (Z, Y, X, C) The image with embedded coordinates, that is, `image_zyx[i, j, k]` is - `array([i, j, k, r, g, b])` or `array([i, j, k, L, a, b])`, depending + `array([i, j, k, c])`, depending on the colorspace. nearest_mean : 3D array of int, shape (Z, Y, X) The (initially empty) label field. distance : 3D array of double, shape (Z, Y, X) The (initially infinity) array of distances to the nearest centroid. - means : 2D array of double, shape (n_segments, 6) + clusters : 2D array of double, shape (n_segments, 6) The centroids obtained by SLIC. max_iter : int The maximum number of k-means iterations. @@ -43,36 +44,43 @@ def _slic_cython(double[:, :, :, ::1] image_zyx, cdef Py_ssize_t depth, height, width depth, height, width = (image_zyx.shape[0], image_zyx.shape[1], image_zyx.shape[2]) + + cdef Py_ssize_t n_features = clusters.shape[1] + cdef Py_ssize_t n_clusters = clusters.shape[0] + # approximate grid size for desired n_segments cdef Py_ssize_t step_z, step_y, step_x slices = regular_grid((depth, height, width), n_segments) step_z, step_y, step_x = [int(s.step) for s in slices] - n_means = means.shape[0] cdef Py_ssize_t i, k, x, y, z, x_min, x_max, y_min, y_max, z_min, z_max, \ changes cdef double dist_mean cdef double tmp + + cdef Py_ssize_t[:] n_cluster_elems = np.zeros(n_clusters, dtype=np.intp) + for i in range(max_iter): changes = 0 - distance[:, :, :] = np.inf - # assign pixels to means - for k in range(n_means): + distance[:, :, :] = DBL_MAX + + # assign pixels to clusters + for k in range(n_clusters): # compute windows: - z_min = int(max(means[k, 0] - 2 * step_z, 0)) - z_max = int(min(means[k, 0] + 2 * step_z, depth)) - y_min = int(max(means[k, 1] - 2 * step_y, 0)) - y_max = int(min(means[k, 1] + 2 * step_y, height)) - x_min = int(max(means[k, 2] - 2 * step_x, 0)) - x_max = int(min(means[k, 2] + 2 * step_x, width)) + z_min = int(max(clusters[k, 0] - 2 * step_z, 0)) + z_max = int(min(clusters[k, 0] + 2 * step_z, depth)) + y_min = int(max(clusters[k, 1] - 2 * step_y, 0)) + y_max = int(min(clusters[k, 1] + 2 * step_y, height)) + x_min = int(max(clusters[k, 2] - 2 * step_x, 0)) + x_max = int(min(clusters[k, 2] + 2 * step_x, width)) for z in range(z_min, z_max): for y in range(y_min, y_max): for x in range(x_min, x_max): dist_mean = 0 - for c in range(6): + for c in range(n_features): # you would think the compiler can optimize the # squaring itself. mine can't (with O2) - tmp = image_zyx[z, y, x, c] - means[k, c] + tmp = image_zyx[z, y, x, c] - clusters[k, c] dist_mean += tmp * tmp if distance[z, y, x] > dist_mean: nearest_mean[z, y, x] = k @@ -80,15 +88,23 @@ def _slic_cython(double[:, :, :, ::1] image_zyx, changes = 1 if changes == 0: break - # recompute means: - nearest_mean_ravel = np.asarray(nearest_mean).ravel() - means_list = [] - for j in range(6): - image_zyx_ravel = ( - np.ascontiguousarray(image_zyx[:, :, :, j]).ravel()) - means_list.append(np.bincount(nearest_mean_ravel, - image_zyx_ravel)) - in_mean = np.bincount(nearest_mean_ravel) - in_mean[in_mean == 0] = 1 - means = (np.vstack(means_list) / in_mean).T.copy("C") - return np.ascontiguousarray(nearest_mean) + + # recompute clusters + + # sum features for all clusters + n_cluster_elems[:] = 0 + clusters[:, :] = 0 + for z in range(depth): + for y in range(height): + for x in range(width): + k = nearest_mean[z, y, x] + n_cluster_elems[k] += 1 + for c in range(n_features): + clusters[k, c] += image_zyx[z, y, x, c] + + # divide by number of elements per cluster to obtain mean + for k in range(n_clusters): + for c in range(n_features): + clusters[k, c] /= n_cluster_elems[k] + + return np.asarray(nearest_mean) diff --git a/skimage/segmentation/slic_superpixels.py b/skimage/segmentation/slic_superpixels.py index b905f199..28f6ef8f 100644 --- a/skimage/segmentation/slic_superpixels.py +++ b/skimage/segmentation/slic_superpixels.py @@ -21,7 +21,7 @@ def slic(image, n_segments=100, compactness=10., max_iter=10, sigma=1, (see `multichannel` parameter). n_segments : int, optional (default: 100) The (approximate) number of labels in the segmented output image. - compactness: float, optional (default: 10) + compactness : float, optional (default: 10) Balances color-space proximity and image-space proximity. Higher values give more weight to image-space. As `compactness` tends to infinity, superpixel shapes become square/cubic. @@ -37,14 +37,14 @@ def slic(image, n_segments=100, compactness=10., max_iter=10, sigma=1, array has shape (M, N, 3). convert2lab : bool, optional (default: True) Whether the input should be converted to Lab colorspace prior to - segmentation. For this purpose, the input is assumed to be RGB. Highly + segmentation. For this purpose, the input is assumed to be RGB. Highly recommended. ratio : float, optional Synonym for `compactness`. This keyword is deprecated. Returns ------- - segment_mask : (width, height) ndarray + segment_mask : (width, height, depth) array Integer mask indicating segment labels. Raises @@ -99,34 +99,41 @@ def slic(image, n_segments=100, compactness=10., max_iter=10, sigma=1, (multichannel and image.ndim not in [3, 4]) or (multichannel and image.shape[-1] != 3)): ValueError("Only 1- or 3-channel 2- or 3-D images are supported.") + image = img_as_float(image) - if not multichannel: - image = gray2rgb(image) + image = np.atleast_3d(image) + if image.ndim == 3: # See 2D RGB image as 3D RGB image with Z = 1 image = image[np.newaxis, ...] + if not isinstance(sigma, coll.Iterable): sigma = np.array([sigma, sigma, sigma, 0]) if (sigma > 0).any(): image = ndimage.gaussian_filter(image, sigma) - if convert2lab: + + if image.shape[3] == 3 and convert2lab: image = rgb2lab(image) - # initialize on grid: + # initialize on grid depth, height, width = image.shape[:3] + # approximate grid size for desired n_segments grid_z, grid_y, grid_x = np.mgrid[:depth, :height, :width] slices = regular_grid(image.shape[:3], n_segments) step_z, step_y, step_x = [int(s.step) for s in slices] - means_z = grid_z[slices] - means_y = grid_y[slices] - means_x = grid_x[slices] + clusters_z = grid_z[slices] + clusters_y = grid_y[slices] + clusters_x = grid_x[slices] + + clusters_color = np.zeros(clusters_z.shape + (image.shape[3],)) + clusters = np.concatenate([clusters_z[..., np.newaxis], + clusters_y[..., np.newaxis], + clusters_x[..., np.newaxis], + clusters_color + ], axis=-1).reshape(-1, 3 + image.shape[3]) + clusters = np.ascontiguousarray(clusters) - means_color = np.zeros(means_z.shape + (3,)) - means = np.concatenate([means_z[..., np.newaxis], means_y[..., np.newaxis], - means_x[..., np.newaxis], means_color - ], axis=-1).reshape(-1, 6) - means = np.ascontiguousarray(means) # we do the scaling of ratio in the same way as in the SLIC paper # so the values have the same meaning ratio = float(max((step_z, step_y, step_x))) / compactness @@ -134,9 +141,9 @@ def slic(image, n_segments=100, compactness=10., max_iter=10, sigma=1, grid_y[..., np.newaxis], grid_x[..., np.newaxis], image * ratio], axis=-1).copy("C") - nearest_mean = np.zeros((depth, height, width), dtype=np.intp) + nearest_cluster = np.empty((depth, height, width), dtype=np.intp) distance = np.empty((depth, height, width), dtype=np.float) - segment_map = _slic_cython(image_zyx, nearest_mean, distance, means, + segment_map = _slic_cython(image_zyx, nearest_cluster, distance, clusters, max_iter, n_segments) if segment_map.shape[0] == 1: segment_map = segment_map[0] diff --git a/skimage/segmentation/tests/test_slic.py b/skimage/segmentation/tests/test_slic.py index 9e6a39e2..6ecd928d 100644 --- a/skimage/segmentation/tests/test_slic.py +++ b/skimage/segmentation/tests/test_slic.py @@ -28,7 +28,7 @@ def test_color_2d(): def test_gray_2d(): rnd = np.random.RandomState(0) - img = np.zeros((20, 21)) + img = np.zeros((20, 20)) img[:10, :10] = 0.33 img[10:, :10] = 0.67 img[10:, 10:] = 1.00 From 6b5556b27969fd051b8c9046fb8b8c29f8dd813e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sat, 31 Aug 2013 18:25:18 +0200 Subject: [PATCH 554/736] Reduce memory footprint --- skimage/segmentation/_slic.pyx | 67 +++++++++++++++--------- skimage/segmentation/slic_superpixels.py | 11 ++-- 2 files changed, 48 insertions(+), 30 deletions(-) diff --git a/skimage/segmentation/_slic.pyx b/skimage/segmentation/_slic.pyx index 13e05bc2..86654b11 100644 --- a/skimage/segmentation/_slic.pyx +++ b/skimage/segmentation/_slic.pyx @@ -11,7 +11,7 @@ from skimage.util import regular_grid def _slic_cython(double[:, :, :, ::1] image_zyx, - Py_ssize_t[:, :, ::1] nearest_mean, + Py_ssize_t[:, :, ::1] nearest_clusters, double[:, :, ::1] distance, double[:, ::1] clusters, Py_ssize_t max_iter, Py_ssize_t n_segments): @@ -21,13 +21,12 @@ def _slic_cython(double[:, :, :, ::1] image_zyx, ---------- image_zyx : 4D array of double, shape (Z, Y, X, C) The image with embedded coordinates, that is, `image_zyx[i, j, k]` is - `array([i, j, k, c])`, depending - on the colorspace. - nearest_mean : 3D array of int, shape (Z, Y, X) + `array([i, j, k, c])`, depending on the colorspace. + nearest_clusters : 3D array of int, shape (Z, Y, X) The (initially empty) label field. distance : 3D array of double, shape (Z, Y, X) The (initially infinity) array of distances to the nearest centroid. - clusters : 2D array of double, shape (n_segments, 6) + clusters : 2D array of double, shape (n_segments, 3 + C) The centroids obtained by SLIC. max_iter : int The maximum number of k-means iterations. @@ -36,7 +35,7 @@ def _slic_cython(double[:, :, :, ::1] image_zyx, Returns ------- - nearest_mean : 3D array of int, shape (Z, Y, X) + nearest_clusters : 3D array of int, shape (Z, Y, X) The label field/superpixels found by SLIC. """ @@ -45,48 +44,63 @@ def _slic_cython(double[:, :, :, ::1] image_zyx, depth, height, width = (image_zyx.shape[0], image_zyx.shape[1], image_zyx.shape[2]) - cdef Py_ssize_t n_features = clusters.shape[1] cdef Py_ssize_t n_clusters = clusters.shape[0] + # number of features [X, Y, Z, ...] + cdef Py_ssize_t n_features = clusters.shape[1] # approximate grid size for desired n_segments cdef Py_ssize_t step_z, step_y, step_x slices = regular_grid((depth, height, width), n_segments) step_z, step_y, step_x = [int(s.step) for s in slices] - cdef Py_ssize_t i, k, x, y, z, x_min, x_max, y_min, y_max, z_min, z_max, \ - changes + cdef Py_ssize_t i, c, k, x, y, z, x_min, x_max, y_min, y_max, z_min, \ + z_max cdef double dist_mean - cdef double tmp + + cdef char change + + cdef double cx, cy, cz, dy, dz cdef Py_ssize_t[:] n_cluster_elems = np.zeros(n_clusters, dtype=np.intp) for i in range(max_iter): - changes = 0 + change = 0 distance[:, :, :] = DBL_MAX # assign pixels to clusters for k in range(n_clusters): - # compute windows: + + # compute windows z_min = int(max(clusters[k, 0] - 2 * step_z, 0)) z_max = int(min(clusters[k, 0] + 2 * step_z, depth)) y_min = int(max(clusters[k, 1] - 2 * step_y, 0)) y_max = int(min(clusters[k, 1] + 2 * step_y, height)) x_min = int(max(clusters[k, 2] - 2 * step_x, 0)) x_max = int(min(clusters[k, 2] + 2 * step_x, width)) + + # cluster coordinate centers + cz = clusters[k, 0] + cy = clusters[k, 1] + cx = clusters[k, 2] + for z in range(z_min, z_max): + dz = (cz - z) ** 2 for y in range(y_min, y_max): + dy = (cy - y) ** 2 for x in range(x_min, x_max): - dist_mean = 0 - for c in range(n_features): - # you would think the compiler can optimize the - # squaring itself. mine can't (with O2) - tmp = image_zyx[z, y, x, c] - clusters[k, c] - dist_mean += tmp * tmp + # you would think the compiler can optimize the + # squaring itself. mine can't (with O2) + dist_mean = dz + dy + (cx - x) ** 2 + for c in range(3, n_features): + dist_mean += (image_zyx[z, y, x, c - 3] + - clusters[k, c]) ** 2 if distance[z, y, x] > dist_mean: - nearest_mean[z, y, x] = k + nearest_clusters[z, y, x] = k distance[z, y, x] = dist_mean - changes = 1 - if changes == 0: + change = 1 + + # stop if no pixel changed its cluster + if change == 0: break # recompute clusters @@ -97,14 +111,17 @@ def _slic_cython(double[:, :, :, ::1] image_zyx, for z in range(depth): for y in range(height): for x in range(width): - k = nearest_mean[z, y, x] + k = nearest_clusters[z, y, x] n_cluster_elems[k] += 1 - for c in range(n_features): - clusters[k, c] += image_zyx[z, y, x, c] + clusters[k, 0] += z + clusters[k, 1] += y + clusters[k, 2] += x + for c in range(3, n_features): + clusters[k, c] += image_zyx[z, y, x, c - 3] # divide by number of elements per cluster to obtain mean for k in range(n_clusters): for c in range(n_features): clusters[k, c] /= n_cluster_elems[k] - return np.asarray(nearest_mean) + return np.asarray(nearest_clusters) diff --git a/skimage/segmentation/slic_superpixels.py b/skimage/segmentation/slic_superpixels.py index 28f6ef8f..1db2754a 100644 --- a/skimage/segmentation/slic_superpixels.py +++ b/skimage/segmentation/slic_superpixels.py @@ -137,14 +137,15 @@ def slic(image, n_segments=100, compactness=10., max_iter=10, sigma=1, # we do the scaling of ratio in the same way as in the SLIC paper # so the values have the same meaning ratio = float(max((step_z, step_y, step_x))) / compactness - image_zyx = np.concatenate([grid_z[..., np.newaxis], - grid_y[..., np.newaxis], - grid_x[..., np.newaxis], - image * ratio], axis=-1).copy("C") + image = image * ratio + nearest_cluster = np.empty((depth, height, width), dtype=np.intp) distance = np.empty((depth, height, width), dtype=np.float) - segment_map = _slic_cython(image_zyx, nearest_cluster, distance, clusters, + + segment_map = _slic_cython(image, nearest_cluster, distance, clusters, max_iter, n_segments) + if segment_map.shape[0] == 1: segment_map = segment_map[0] + return segment_map From 471b293a9e982dbea5b11e1ddeb1b5da6a265293 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sat, 31 Aug 2013 18:34:18 +0200 Subject: [PATCH 555/736] Remove unnecessary parameters and update doc string --- skimage/segmentation/_slic.pyx | 26 ++++++++++-------------- skimage/segmentation/slic_superpixels.py | 18 +++++++--------- 2 files changed, 18 insertions(+), 26 deletions(-) diff --git a/skimage/segmentation/_slic.pyx b/skimage/segmentation/_slic.pyx index 86654b11..6faaa69b 100644 --- a/skimage/segmentation/_slic.pyx +++ b/skimage/segmentation/_slic.pyx @@ -11,27 +11,18 @@ from skimage.util import regular_grid def _slic_cython(double[:, :, :, ::1] image_zyx, - Py_ssize_t[:, :, ::1] nearest_clusters, - double[:, :, ::1] distance, double[:, ::1] clusters, - Py_ssize_t max_iter, Py_ssize_t n_segments): + Py_ssize_t max_iter): """Helper function for SLIC segmentation. Parameters ---------- image_zyx : 4D array of double, shape (Z, Y, X, C) - The image with embedded coordinates, that is, `image_zyx[i, j, k]` is - `array([i, j, k, c])`, depending on the colorspace. - nearest_clusters : 3D array of int, shape (Z, Y, X) - The (initially empty) label field. - distance : 3D array of double, shape (Z, Y, X) - The (initially infinity) array of distances to the nearest centroid. - clusters : 2D array of double, shape (n_segments, 3 + C) - The centroids obtained by SLIC. + The input image. + clusters : 2D array of double, shape (N, 3 + C) + The initial centroids obtained by SLIC as [Z, Y, X, C...]. max_iter : int The maximum number of k-means iterations. - n_segments : int - The approximate/desired number of segments. Returns ------- @@ -39,7 +30,7 @@ def _slic_cython(double[:, :, :, ::1] image_zyx, The label field/superpixels found by SLIC. """ - # initialize on grid: + # initialize on grid cdef Py_ssize_t depth, height, width depth, height, width = (image_zyx.shape[0], image_zyx.shape[1], image_zyx.shape[2]) @@ -50,9 +41,14 @@ def _slic_cython(double[:, :, :, ::1] image_zyx, # approximate grid size for desired n_segments cdef Py_ssize_t step_z, step_y, step_x - slices = regular_grid((depth, height, width), n_segments) + slices = regular_grid((depth, height, width), n_clusters) step_z, step_y, step_x = [int(s.step) for s in slices] + cdef Py_ssize_t[:, :, ::1] nearest_clusters \ + = np.empty((depth, height, width), dtype=np.intp) + cdef float[:, :, ::1] distance \ + = np.empty((depth, height, width), dtype=np.float32) + cdef Py_ssize_t i, c, k, x, y, z, x_min, x_max, y_min, y_max, z_min, \ z_max cdef double dist_mean diff --git a/skimage/segmentation/slic_superpixels.py b/skimage/segmentation/slic_superpixels.py index 1db2754a..42d30593 100644 --- a/skimage/segmentation/slic_superpixels.py +++ b/skimage/segmentation/slic_superpixels.py @@ -44,7 +44,7 @@ def slic(image, n_segments=100, compactness=10., max_iter=10, sigma=1, Returns ------- - segment_mask : (width, height, depth) array + labels : (width, height, depth) array Integer mask indicating segment labels. Raises @@ -82,6 +82,7 @@ def slic(image, n_segments=100, compactness=10., max_iter=10, sigma=1, >>> # Increasing the ratio parameter yields more square regions >>> segments = slic(img, n_segments=100, ratio=20) """ + if ratio is not None: msg = 'Keyword `ratio` is deprecated. Use `compactness` instead.' warnings.warn(msg) @@ -115,10 +116,9 @@ def slic(image, n_segments=100, compactness=10., max_iter=10, sigma=1, if image.shape[3] == 3 and convert2lab: image = rgb2lab(image) - # initialize on grid depth, height, width = image.shape[:3] - # approximate grid size for desired n_segments + # initialize cluster centroids for desired number of segments grid_z, grid_y, grid_x = np.mgrid[:depth, :height, :width] slices = regular_grid(image.shape[:3], n_segments) step_z, step_y, step_x = [int(s.step) for s in slices] @@ -139,13 +139,9 @@ def slic(image, n_segments=100, compactness=10., max_iter=10, sigma=1, ratio = float(max((step_z, step_y, step_x))) / compactness image = image * ratio - nearest_cluster = np.empty((depth, height, width), dtype=np.intp) - distance = np.empty((depth, height, width), dtype=np.float) + labels = _slic_cython(image, clusters, max_iter) - segment_map = _slic_cython(image, nearest_cluster, distance, clusters, - max_iter, n_segments) + if labels.shape[0] == 1: + labels = labels[0] - if segment_map.shape[0] == 1: - segment_map = segment_map[0] - - return segment_map + return labels From 84579a6c2c82894ef88e88e6048cb198243a8265 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sat, 31 Aug 2013 18:37:35 +0200 Subject: [PATCH 556/736] Remove legacy comment --- skimage/segmentation/_slic.pyx | 2 -- 1 file changed, 2 deletions(-) diff --git a/skimage/segmentation/_slic.pyx b/skimage/segmentation/_slic.pyx index 6faaa69b..030bcb4e 100644 --- a/skimage/segmentation/_slic.pyx +++ b/skimage/segmentation/_slic.pyx @@ -84,8 +84,6 @@ def _slic_cython(double[:, :, :, ::1] image_zyx, for y in range(y_min, y_max): dy = (cy - y) ** 2 for x in range(x_min, x_max): - # you would think the compiler can optimize the - # squaring itself. mine can't (with O2) dist_mean = dz + dy + (cx - x) ** 2 for c in range(3, n_features): dist_mean += (image_zyx[z, y, x, c - 3] From bc7efb01e4fe7e0d6f9a07c205101adf01f92ed8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sat, 31 Aug 2013 19:38:11 +0200 Subject: [PATCH 557/736] Fix dtype bug --- skimage/segmentation/_slic.pyx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skimage/segmentation/_slic.pyx b/skimage/segmentation/_slic.pyx index 030bcb4e..4a57ee93 100644 --- a/skimage/segmentation/_slic.pyx +++ b/skimage/segmentation/_slic.pyx @@ -46,8 +46,8 @@ def _slic_cython(double[:, :, :, ::1] image_zyx, cdef Py_ssize_t[:, :, ::1] nearest_clusters \ = np.empty((depth, height, width), dtype=np.intp) - cdef float[:, :, ::1] distance \ - = np.empty((depth, height, width), dtype=np.float32) + cdef double[:, :, ::1] distance \ + = np.empty((depth, height, width), dtype=np.double) cdef Py_ssize_t i, c, k, x, y, z, x_min, x_max, y_min, y_max, z_min, \ z_max From bd388383a02327250dee00481c7b0eab66d193fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sat, 31 Aug 2013 19:59:00 +0200 Subject: [PATCH 558/736] Fix bug in window extent determination --- skimage/segmentation/_slic.pyx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/skimage/segmentation/_slic.pyx b/skimage/segmentation/_slic.pyx index 4a57ee93..c61455e5 100644 --- a/skimage/segmentation/_slic.pyx +++ b/skimage/segmentation/_slic.pyx @@ -66,19 +66,19 @@ def _slic_cython(double[:, :, :, ::1] image_zyx, # assign pixels to clusters for k in range(n_clusters): - # compute windows - z_min = int(max(clusters[k, 0] - 2 * step_z, 0)) - z_max = int(min(clusters[k, 0] + 2 * step_z, depth)) - y_min = int(max(clusters[k, 1] - 2 * step_y, 0)) - y_max = int(min(clusters[k, 1] + 2 * step_y, height)) - x_min = int(max(clusters[k, 2] - 2 * step_x, 0)) - x_max = int(min(clusters[k, 2] + 2 * step_x, width)) - # cluster coordinate centers cz = clusters[k, 0] cy = clusters[k, 1] cx = clusters[k, 2] + # compute windows + z_min = max(cz - 2 * step_z, 0) + z_max = min(cz + 2 * step_z + 1, depth) + y_min = max(cy - 2 * step_y, 0) + y_max = min(cy + 2 * step_y + 1, height) + x_min = max(cx - 2 * step_x, 0) + x_max = min(cx + 2 * step_x + 1, width) + for z in range(z_min, z_max): dz = (cz - z) ** 2 for y in range(y_min, y_max): From f5cfbcfe97f17f5e9c6ce782e6869eaf96dc7016 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sat, 31 Aug 2013 20:04:36 +0200 Subject: [PATCH 559/736] Reorder variable declarations --- skimage/segmentation/_slic.pyx | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/skimage/segmentation/_slic.pyx b/skimage/segmentation/_slic.pyx index c61455e5..834c5181 100644 --- a/skimage/segmentation/_slic.pyx +++ b/skimage/segmentation/_slic.pyx @@ -32,8 +32,9 @@ def _slic_cython(double[:, :, :, ::1] image_zyx, # initialize on grid cdef Py_ssize_t depth, height, width - depth, height, width = (image_zyx.shape[0], image_zyx.shape[1], - image_zyx.shape[2]) + depth = image_zyx.shape[0] + height = image_zyx.shape[1] + width = image_zyx.shape[2] cdef Py_ssize_t n_clusters = clusters.shape[0] # number of features [X, Y, Z, ...] @@ -48,17 +49,12 @@ def _slic_cython(double[:, :, :, ::1] image_zyx, = np.empty((depth, height, width), dtype=np.intp) cdef double[:, :, ::1] distance \ = np.empty((depth, height, width), dtype=np.double) - - cdef Py_ssize_t i, c, k, x, y, z, x_min, x_max, y_min, y_max, z_min, \ - z_max - cdef double dist_mean - - cdef char change - - cdef double cx, cy, cz, dy, dz - cdef Py_ssize_t[:] n_cluster_elems = np.zeros(n_clusters, dtype=np.intp) + cdef Py_ssize_t i, c, k, x, y, z, x_min, x_max, y_min, y_max, z_min, z_max + cdef char change + cdef double dist_mean, cx, cy, cz, dy, dz + for i in range(max_iter): change = 0 distance[:, :, :] = DBL_MAX From e5eea8e135264dacac28be5bf6d468f1f3cc2c57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 1 Sep 2013 16:15:00 +0200 Subject: [PATCH 560/736] Improve documentation of sigma --- skimage/segmentation/slic_superpixels.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/skimage/segmentation/slic_superpixels.py b/skimage/segmentation/slic_superpixels.py index 42d30593..8522b482 100644 --- a/skimage/segmentation/slic_superpixels.py +++ b/skimage/segmentation/slic_superpixels.py @@ -27,9 +27,10 @@ def slic(image, n_segments=100, compactness=10., max_iter=10, sigma=1, infinity, superpixel shapes become square/cubic. max_iter : int, optional (default: 10) Maximum number of iterations of k-means. - sigma : float, optional (default: 1) - Width of Gaussian smoothing kernel for preprocessing. Zero means no - smoothing. + sigma : float or array of floats, optional (default: 1) + Width of Gaussian smoothing kernel for pre-processing for each + dimension of the image. The same sigma is applied to each dimension in + case of a scalar value. Zero means no smoothing. multichannel : bool, optional (default: None) Whether the last axis of the image is to be interpreted as multiple channels. Only 3 channels are supported. If `None`, the function will From eeddd9e35fabb22d399da7cc564f25b9276705a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 1 Sep 2013 16:44:59 +0200 Subject: [PATCH 561/736] Revert multichannel magic and improve parameter docs --- skimage/segmentation/slic_superpixels.py | 58 ++++++++++-------------- skimage/segmentation/tests/test_slic.py | 10 ++-- 2 files changed, 30 insertions(+), 38 deletions(-) diff --git a/skimage/segmentation/slic_superpixels.py b/skimage/segmentation/slic_superpixels.py index 8522b482..c239baf0 100644 --- a/skimage/segmentation/slic_superpixels.py +++ b/skimage/segmentation/slic_superpixels.py @@ -6,37 +6,34 @@ from scipy import ndimage import warnings from ..util import img_as_float, regular_grid -from ..color import rgb2lab, gray2rgb, guess_spatial_dimensions from ._slic import _slic_cython -def slic(image, n_segments=100, compactness=10., max_iter=10, sigma=1, - multichannel=None, convert2lab=True, ratio=None): - """Segments image using k-means clustering in Color-(x,y) space. +def slic(image, n_segments=100, compactness=10., max_iter=20, sigma=1, + multichannel=True, convert2lab=True, ratio=None): + """Segments image using k-means clustering in Color-(x,y,z) space. Parameters ---------- - image : (width, height [, depth] [, 3]) ndarray - Input image, which can be 2D or 3D, and grayscale or multi-channel + image : 2D, 3D or 4D ndarray + Input image, which can be 2D or 3D, and grayscale or multichannel (see `multichannel` parameter). - n_segments : int, optional (default: 100) + n_segments : int The (approximate) number of labels in the segmented output image. - compactness : float, optional (default: 10) + compactness : float Balances color-space proximity and image-space proximity. Higher values give more weight to image-space. As `compactness` tends to infinity, superpixel shapes become square/cubic. - max_iter : int, optional (default: 10) + max_iter : int Maximum number of iterations of k-means. - sigma : float or array of floats, optional (default: 1) + sigma : float or (3,) array of floats Width of Gaussian smoothing kernel for pre-processing for each dimension of the image. The same sigma is applied to each dimension in case of a scalar value. Zero means no smoothing. - multichannel : bool, optional (default: None) + multichannel : bool Whether the last axis of the image is to be interpreted as multiple - channels. Only 3 channels are supported. If `None`, the function will - attempt to guess this, and raise a warning if ambiguous, when the - array has shape (M, N, 3). - convert2lab : bool, optional (default: True) + channels or another spatial dimension. + convert2lab : bool Whether the input should be converted to Lab colorspace prior to segmentation. For this purpose, the input is assumed to be RGB. Highly recommended. @@ -45,7 +42,7 @@ def slic(image, n_segments=100, compactness=10., max_iter=10, sigma=1, Returns ------- - labels : (width, height, depth) array + labels : 2D or 3D array Integer mask indicating segment labels. Raises @@ -88,33 +85,28 @@ def slic(image, n_segments=100, compactness=10., max_iter=10, sigma=1, msg = 'Keyword `ratio` is deprecated. Use `compactness` instead.' warnings.warn(msg) compactness = ratio - spatial_dims = guess_spatial_dimensions(image) - if spatial_dims is None and multichannel is None: - msg = ("Images with dimensions (M, N, 3) are interpreted as 2D+RGB" + - " by default. Use `multichannel=False` to interpret as " + - " 3D image with last dimension of length 3.") - warnings.warn(RuntimeWarning(msg)) - multichannel = True - elif multichannel is None: - multichannel = (spatial_dims + 1 == image.ndim) - if ((not multichannel and image.ndim not in [2, 3]) or - (multichannel and image.ndim not in [3, 4]) or - (multichannel and image.shape[-1] != 3)): - ValueError("Only 1- or 3-channel 2- or 3-D images are supported.") image = img_as_float(image) image = np.atleast_3d(image) if image.ndim == 3: - # See 2D RGB image as 3D RGB image with Z = 1 - image = image[np.newaxis, ...] + if multichannel: + # Make 2D image 3D with depth = 1 + image = image[np.newaxis, ...] + else: + # Add channel as single last dimension + image = image[..., np.newaxis] if not isinstance(sigma, coll.Iterable): - sigma = np.array([sigma, sigma, sigma, 0]) + sigma = np.array([sigma, sigma, sigma]) if (sigma > 0).any(): + sigma = list(sigma) + [0] image = ndimage.gaussian_filter(image, sigma) - if image.shape[3] == 3 and convert2lab: + if convert2lab: + + if not multichannel or image.shape[3] != 3: + raise ValueError("Lab colorspace conversion requires a RGB image.") image = rgb2lab(image) depth, height, width = image.shape[:3] diff --git a/skimage/segmentation/tests/test_slic.py b/skimage/segmentation/tests/test_slic.py index 6ecd928d..cb16e1b7 100644 --- a/skimage/segmentation/tests/test_slic.py +++ b/skimage/segmentation/tests/test_slic.py @@ -28,15 +28,15 @@ def test_color_2d(): def test_gray_2d(): rnd = np.random.RandomState(0) - img = np.zeros((20, 20)) + img = np.zeros((20, 21)) img[:10, :10] = 0.33 img[10:, :10] = 0.67 img[10:, 10:] = 1.00 img += 0.0033 * rnd.normal(size=img.shape) img[img > 1] = 1 img[img < 0] = 0 - seg = slic(img, sigma=0, n_segments=4, compactness=20.0, - multichannel=False) + seg = slic(img, sigma=0, n_segments=4, compactness=1, + multichannel=False, convert2lab=False) assert_equal(len(np.unique(seg)), 4) assert_array_equal(seg[:10, :10], 0) @@ -80,8 +80,8 @@ def test_gray_3d(): img += 0.001 * rnd.normal(size=img.shape) img[img > 1] = 1 img[img < 0] = 0 - seg = slic(img, sigma=0, n_segments=8, compactness=20.0, - multichannel=False) + seg = slic(img, sigma=0, n_segments=8, compactness=1, + multichannel=False, convert2lab=False) assert_equal(len(np.unique(seg)), 8) for s, c in zip(slices, range(8)): From a6d1b10e254912ab8a3dbb0c377842cbdd98d52a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 1 Sep 2013 16:53:05 +0200 Subject: [PATCH 562/736] Use absolute imports --- skimage/segmentation/slic_superpixels.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skimage/segmentation/slic_superpixels.py b/skimage/segmentation/slic_superpixels.py index c239baf0..6e009120 100644 --- a/skimage/segmentation/slic_superpixels.py +++ b/skimage/segmentation/slic_superpixels.py @@ -5,8 +5,8 @@ import numpy as np from scipy import ndimage import warnings -from ..util import img_as_float, regular_grid -from ._slic import _slic_cython +from skimage.util import img_as_float, regular_grid +from skimage.segmentation._slic import _slic_cython def slic(image, n_segments=100, compactness=10., max_iter=20, sigma=1, From 80ed8751474d705706bb5469ad73108470c81b9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 1 Sep 2013 17:12:10 +0200 Subject: [PATCH 563/736] Add missing rgb2lab import --- skimage/segmentation/slic_superpixels.py | 1 + 1 file changed, 1 insertion(+) diff --git a/skimage/segmentation/slic_superpixels.py b/skimage/segmentation/slic_superpixels.py index 6e009120..5a6d4506 100644 --- a/skimage/segmentation/slic_superpixels.py +++ b/skimage/segmentation/slic_superpixels.py @@ -7,6 +7,7 @@ import warnings from skimage.util import img_as_float, regular_grid from skimage.segmentation._slic import _slic_cython +from skimage.color import rgb2lab def slic(image, n_segments=100, compactness=10., max_iter=20, sigma=1, From b0d3b92e74cdef187830513c360cc16b7306fec5 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Sun, 1 Sep 2013 23:44:23 +0200 Subject: [PATCH 564/736] Add skip parameter to view_as_windows. --- skimage/util/shape.py | 18 ++++++++++++++---- skimage/util/tests/test_shape.py | 16 ++++++++++++++++ 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/skimage/util/shape.py b/skimage/util/shape.py index 0126d2e3..1d606b42 100644 --- a/skimage/util/shape.py +++ b/skimage/util/shape.py @@ -98,7 +98,7 @@ def view_as_blocks(arr_in, block_shape): return arr_out -def view_as_windows(arr_in, window_shape): +def view_as_windows(arr_in, window_shape, step=1): """Rolling window view of the input n-dimensional array. Windows are overlapping views of the input array, with adjacent windows @@ -108,10 +108,12 @@ def view_as_windows(arr_in, window_shape): ---------- arr_in: ndarray The n-dimensional input array. - window_shape: tuple Defines the shape of the elementary n-dimensional orthotope (better know as hyperrectangle [1]_) of the rolling window view. + step : int + Number of elements to skip when moving the window forward (by + default, move forward by one). Returns ------- @@ -212,6 +214,9 @@ def view_as_windows(arr_in, window_shape): if not (len(window_shape) == arr_in.ndim): raise ValueError("'window_shape' is incompatible with 'arr_in.shape'") + if step < 1: + raise ValueError("`step` must be >= 1") + arr_shape = np.array(arr_in.shape) window_shape = np.array(window_shape, dtype=arr_shape.dtype) @@ -224,8 +229,13 @@ def view_as_windows(arr_in, window_shape): # -- build rolling window view arr_in = np.ascontiguousarray(arr_in) - new_shape = tuple(arr_shape - window_shape + 1) + tuple(window_shape) - new_strides = arr_in.strides + arr_in.strides + new_shape = tuple((arr_shape - window_shape) // step + 1) + \ + tuple(window_shape) + + arr_strides = np.array(arr_in.strides) + new_strides = np.concatenate( + (arr_strides * step, arr_strides) + ) arr_out = as_strided(arr_in, shape=new_shape, strides=new_strides) diff --git a/skimage/util/tests/test_shape.py b/skimage/util/tests/test_shape.py index 8c62a191..b6975d0f 100644 --- a/skimage/util/tests/test_shape.py +++ b/skimage/util/tests/test_shape.py @@ -141,5 +141,21 @@ def test_view_as_windows_2D(): [17, 18, 19]]]])) +def test_view_as_windows_With_skip(): + A = np.arange(20).reshape((5, 4)) + B = view_as_windows(A, (2, 2), step=2) + assert_equal(B, [[[[0, 1], + [4, 5]], + [[2, 3], + [6, 7]]], + [[[8, 9], + [12, 13]], + [[10, 11], + [14, 15]]]]) + + C = view_as_windows(A, (2, 2), step=4) + assert_equal(C.shape, (1, 1, 2, 2)) + + if __name__ == '__main__': np.testing.run_module_suite() From d0d9fee36e997d1578a424e49edacb8db202f863 Mon Sep 17 00:00:00 2001 From: "Josh Warner (Mac)" Date: Sun, 1 Sep 2013 20:49:58 -0500 Subject: [PATCH 565/736] DOC: Minor documentation formatting fixes in marching cubes --- skimage/measure/_marching_cubes.py | 6 +++--- skimage/measure/_marching_cubes_cy.pyx | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/skimage/measure/_marching_cubes.py b/skimage/measure/_marching_cubes.py index 6af60672..772c52f1 100644 --- a/skimage/measure/_marching_cubes.py +++ b/skimage/measure/_marching_cubes.py @@ -41,8 +41,8 @@ def marching_cubes(volume, level, sampling=(1., 1., 1.)): / | / | ^ z v4 ------ v3 | | / | v5 ----|- v6 |/ (note: NOT right handed!) - | / | / ----> x - |/ | / + | / | / ----> x + | / | / v1 ------ v2 Most notably, if v4, v8, v2, and v6 are all >= `level` (or any @@ -153,5 +153,5 @@ def mesh_surface_area(verts, tris): b = actual_verts[:, 0, :] - actual_verts[:, 2, :] del actual_verts - # Area of triangle = 1/2 * Euclidean norm of cross product + # Area of triangle in 3D = 1/2 * Euclidean norm of cross product return ((np.cross(a, b) ** 2).sum(axis=1) ** 0.5).sum() / 2. diff --git a/skimage/measure/_marching_cubes_cy.pyx b/skimage/measure/_marching_cubes_cy.pyx index 4343c1e5..a0f63d1c 100644 --- a/skimage/measure/_marching_cubes_cy.pyx +++ b/skimage/measure/_marching_cubes_cy.pyx @@ -94,8 +94,8 @@ def iterate_and_store_3d(double[:, :, ::1] arr, double level, # / | / | ^ z # v4 ------ v3 | | / # | v5 ----|- v6 |/ (note: NOT right handed!) - # | / | / ----> x - # |/ | / + # | / | / ----> x + # | / | / # v1 ------ v2 # # We also maintain the current 2D coordinates for v1, and ensure the array From 14a0685838b53ce70d7674625b127e33c7a05625 Mon Sep 17 00:00:00 2001 From: "Josh Warner (Mac)" Date: Sun, 1 Sep 2013 20:51:20 -0500 Subject: [PATCH 566/736] MAINT: Refactor ellipsoid generator into skimage.draw --- skimage/draw/__init__.py | 3 + skimage/draw/draw3d.py | 117 +++++++++++++++++++ skimage/measure/tests/test_marching_cubes.py | 117 ++----------------- 3 files changed, 128 insertions(+), 109 deletions(-) create mode 100644 skimage/draw/draw3d.py diff --git a/skimage/draw/__init__.py b/skimage/draw/__init__.py index 38c114f7..4fb222d1 100644 --- a/skimage/draw/__init__.py +++ b/skimage/draw/__init__.py @@ -1,11 +1,14 @@ from .draw import circle, ellipse, set_color from ._draw import line, polygon, ellipse_perimeter, circle_perimeter, \ bezier_segment +from .draw3d import ellipsoid, ellipsoid_stats __all__ = ['line', 'polygon', 'ellipse', 'ellipse_perimeter', + 'ellipsoid', + 'ellipsoid_stats', 'circle', 'circle_perimeter', 'set_color'] diff --git a/skimage/draw/draw3d.py b/skimage/draw/draw3d.py new file mode 100644 index 00000000..e247e360 --- /dev/null +++ b/skimage/draw/draw3d.py @@ -0,0 +1,117 @@ +# coding: utf-8 +import numpy as np +from scipy.special import (ellipkinc as ellip_F, ellipeinc as ellip_E) + + +def ellipsoid(a, b, c, sampling=(1., 1., 1.), levelset=False): + """ + Generates ellipsoid with semimajor axes aligned with grid dimensions + on grid with specified `sampling`. + + Parameters + ---------- + a : float + Length of semimajor axis aligned with x-axis. + b : float + Length of semimajor axis aligned with y-axis. + c : float + Length of semimajor axis aligned with z-axis. + sampling : tuple of floats, length 3 + Sampling in (x, y, z) spatial dimensions. + levelset : bool + If True, returns the level set for this ellipsoid (signed level + set about zero, with positive denoting interior) as np.float64. + False returns a binarized version of said level set. + + Returns + ------- + ellip : (N, M, P) array + Ellipsoid centered in a correctly sized array for given `sampling`. + Boolean dtype unless `levelset=True`, in which case a float array is + returned with the level set above 0.0 representing the ellipsoid. + + """ + if (a <= 0) or (b <= 0) or (c <= 0): + raise ValueError('Parameters a, b, and c must all be > 0') + + offset = np.r_[1, 1, 1] * np.r_[sampling] + + # Calculate limits, and ensure output volume is odd & symmetric + low = np.ceil((- np.r_[a, b, c] - offset)) + high = np.floor((np.r_[a, b, c] + offset + 1)) + + for dim in range(3): + if (high[dim] - low[dim]) % 2 == 0: + low[dim] -= 1 + num = np.arange(low[dim], high[dim], sampling[dim]) + if 0 not in num: + low[dim] -= np.max(num[num < 0]) + + # Generate (anisotropic) spatial grid + x, y, z = np.mgrid[low[0]:high[0]:sampling[0], + low[1]:high[1]:sampling[1], + low[2]:high[2]:sampling[2]] + + if not levelset: + arr = ((x / float(a)) ** 2 + + (y / float(b)) ** 2 + + (z / float(c)) ** 2) <= 1 + else: + arr = ((x / float(a)) ** 2 + + (y / float(b)) ** 2 + + (z / float(c)) ** 2) - 1 + + return arr + + +def ellipsoid_stats(a, b, c, sampling=(1., 1., 1.)): + """ + Calculates analytical surface area and volume for ellipsoid with + semimajor axes aligned with grid dimensions of specified `sampling`. + + Parameters + ---------- + a : float + Length of semimajor axis aligned with x-axis. + b : float + Length of semimajor axis aligned with y-axis. + c : float + Length of semimajor axis aligned with z-axis. + sampling : tuple of floats, length 3 + Sampling in (x, y, z) spatial dimensions. + + Returns + ------- + vol : float + Analytically calculated volume of ellipsoid. + surf : float + Analytically calculated surface area of ellipsoid. + + """ + if (a <= 0) or (b <= 0) or (c <= 0): + raise ValueError('Parameters a, b, and c must all be > 0') + + # Calculate volume & surface area + # Surface calculation requires a >= b >= c and a != c. + abc = [a, b, c] + abc.sort(reverse=True) + a = abc[0] + b = abc[1] + c = abc[2] + + # Volume + vol = 4 / 3. * np.pi * a * b * c + + # Analytical ellipsoid surface area + phi = np.arcsin((1. - (c ** 2 / (a ** 2.))) ** 0.5) + d = float((a ** 2 - c ** 2) ** 0.5) + m = (a ** 2 * (b ** 2 - c ** 2) / + float(b ** 2 * (a ** 2 - c ** 2))) + F = ellip_F(phi, m) + E = ellip_E(phi, m) + + surf = 2 * np.pi * (c ** 2 + + b * c ** 2 / d * F + + b * d * E) + + return vol, surf diff --git a/skimage/measure/tests/test_marching_cubes.py b/skimage/measure/tests/test_marching_cubes.py index e8135788..991da060 100644 --- a/skimage/measure/tests/test_marching_cubes.py +++ b/skimage/measure/tests/test_marching_cubes.py @@ -1,114 +1,13 @@ import numpy as np from numpy.testing import assert_raises -from scipy.special import (ellipkinc as ellip_F, ellipeinc as ellip_E) +from skimage.draw import ellipsoid, ellipsoid_stats from skimage.measure import marching_cubes, mesh_surface_area -def _ellipsoid(a, b, c, sampling=(1., 1., 1.), info=False, tight=False, - levelset=False): - """ - Generates ellipsoid with semimajor axes aligned with grid dimensions, - on grid with specified `sampling`. - - Parameters - ---------- - a : float - Length of semimajor axis aligned with x-axis - b : float - Length of semimajor axis aligned with y-axis - c : float - Length of semimajor axis aligned with z-axis - sampling : tuple of floats, length 3 - Sampling in each spatial dimension - info : bool - If False, only `bool_arr` returned. - If True, (`bool_arr`, `vol`, `surf`) returned; the additional - values are analytical volume and surface area calculated for - this ellipsoid. - tight : bool - Controls if the ellipsoid will precisely be contained within - the returned volume (tight=True) or if each dimension will be - 2 longer than necessary (tight=False). For algorithms which - need both sides of a contour, use False. - levelset : bool - If True, returns the level set for this ellipsoid (signed level - set about zero, with positive denoting interior) as np.float64. - False returns a binarized version of said level set. - - Returns - ------- - bool_arr : (N, M, P) array - Sphere in an appropriately sized boolean array. - vol : float - Analytically calculated volume of ellipsoid. Only returned if - `info` is True. - surf : float - Analytically calculated surface area of ellipsoid. Only returned - if `info` is True. - - """ - if not tight: - offset = np.r_[1, 1, 1] * np.r_[sampling] - else: - offset = np.r_[0, 0, 0] - - # Calculate limits, and ensure output volume is odd & symmetric - low = np.ceil((-np.r_[a, b, c] - offset)) - high = np.floor((np.r_[a, b, c] + offset + 1)) - for dim in range(3): - if (high[dim] - low[dim]) % 2 == 0: - low[dim] -= 1 - num = np.arange(low[dim], high[dim], sampling[dim]) - if 0 not in num: - low[dim] -= np.max(num[num < 0]) - - # Generate (anisotropic) spatial grid - x, y, z = np.mgrid[low[0]:high[0]:sampling[0], - low[1]:high[1]:sampling[1], - low[2]:high[2]:sampling[2]] - - if not levelset: - arr = ((x / float(a)) ** 2 + - (y / float(b)) ** 2 + - (z / float(c)) ** 2) <= 1 - else: - arr = ((x / float(a)) ** 2 + - (y / float(b)) ** 2 + - (z / float(c)) ** 2) - 1 - - if not info: - return arr - else: - # Surface calculation requires a >= b >= c and a != c. - abc = [a, b, c] - abc.sort(reverse=True) - a = abc[0] - b = abc[1] - c = abc[2] - - # Volume - vol = 4 / 3. * np.pi * a * b * c - - # Analytical ellipsoid surface area - phi = np.arcsin((1. - (c ** 2 / (a ** 2.))) ** 0.5) - d = float((a ** 2 - c ** 2) ** 0.5) - m = (a ** 2 * (b ** 2 - c ** 2) / - float(b ** 2 * (a ** 2 - c ** 2))) - F = ellip_F(phi, m) - E = ellip_E(phi, m) - - surf = 2 * np.pi * (c ** 2 + - b * c ** 2 / d * F + - b * d * E) - - return arr, vol, surf - - def test_marching_cubes_isotropic(): - ellipsoid_isotropic, _, surf = _ellipsoid(6, 10, 16, - levelset=True, - info=True) + ellipsoid_isotropic = ellipsoid(6, 10, 16, levelset=True) + _, surf = ellipsoid_stats(6, 10, 16, levelset=True) verts, faces = marching_cubes(ellipsoid_isotropic, 0.) surf_calc = mesh_surface_area(verts, faces) @@ -118,13 +17,13 @@ def test_marching_cubes_isotropic(): def test_marching_cubes_anisotropic(): sampling = (1., 10 / 6., 16 / 6.) - ellipsoid_isotropic, _, surf = _ellipsoid(6, 10, 16, - sampling=sampling, - levelset=True, - info=True) - verts, faces = marching_cubes(ellipsoid_isotropic, 0., + ellipsoid_anisotropic, _, surf = ellipsoid(6, 10, 16, sampling=sampling, + levelset=True) + _, surf = ellipsoid_stats(6, 10, 16, sampling=sampling, levelset=True) + verts, faces = marching_cubes(ellipsoid_anisotropic, 0., sampling=sampling) surf_calc = mesh_surface_area(verts, faces) + # Test within 1.5% tolerance for anisotropic. Will always underestimate. assert surf > surf_calc and surf_calc > surf * 0.985 From c678fa55f44872c470ddc7830775b0f54007325b Mon Sep 17 00:00:00 2001 From: "Josh Warner (Mac)" Date: Sun, 1 Sep 2013 20:52:06 -0500 Subject: [PATCH 567/736] DOC: Add concise marching cubes example to gallery using Matplotlib --- doc/examples/plot_marching_cubes.py | 56 +++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 doc/examples/plot_marching_cubes.py diff --git a/doc/examples/plot_marching_cubes.py b/doc/examples/plot_marching_cubes.py new file mode 100644 index 00000000..0b321bfb --- /dev/null +++ b/doc/examples/plot_marching_cubes.py @@ -0,0 +1,56 @@ +""" +============== +Marching Cubes +============== + +Marching cubes is an algorithm to extract a 2D surface mesh from a 3D volume. +This can be conceptualized as a 3D generalization of isolines on topographical +or weather maps. It works by iterating across the volume, looking for regions +which cross the level of interest. If such regions are found, triangulations +are generated and added to an output mesh. The final result is a set of +vertices and a set of triangular faces. + +The algorithm requires a data volume and an isosurface value. For example, in +CT imaging Hounsfield units of +700 to +3000 represent bone. So, one potential +input would be a reconstructed CT set of data and the value +700, to extract +a mesh for regions of bone or bone-like density. + +This implementation also works correctly on anisotropic datasets, where the +voxel spacing is not equal for every spatial dimension, through use of the +`sampling` kwarg. + +""" +import numpy as np +import matplotlib.pyplot as plt +from mpl_toolkits.mplot3d import Axes3D +import mpl_toolkits.mplot3d as a3 + +from skimage import measure +from skimage.draw import ellipsoid + +# Generate a level set about zero of two identical ellipsoids in 3D +ellip_base = ellipsoid(6, 10, 16, levelset=True) +ellip_double = np.concatenate((ellip_base[:-1, ...], + ellip_base[2:, ...]), axis=0) + +# Use marching cubes to obtain the surface mesh of these ellipsoids +verts, faces = measure.marching_cubes(ellip_double, 0) + +# Display resulting triangular mesh using Matplotlib. This can also be done +# with mayavi (see skimage.measure.marching_cubes docstring). +fig = plt.figure(figsize=(10, 12)) +ax = fig.add_subplot(111, projection='3d') + +# Fancy indexing: `verts[faces]` to generate a Poly3DCollection +mesh = a3.art3d.Poly3DCollection(verts[faces]) +ax.add_collection3d(mesh) + +ax.set_xlabel("x-axis: a = 6 per ellipsoid") +ax.set_ylabel("y-axis: b = 10") +ax.set_zlabel("z-axis: c = 16") + +ax.set_xlim(0, 24) # a = 6 (times two for 2nd ellipsoid) +ax.set_ylim(0, 20) # b = 10 +ax.set_zlim(0, 32) # c = 16 + +plt.show() From 50d73fdd62503143f56b9c800b34a72d4e72b5ad Mon Sep 17 00:00:00 2001 From: "Josh Warner (Mac)" Date: Sun, 1 Sep 2013 21:31:53 -0500 Subject: [PATCH 568/736] TEST: Add test suite for draw3d (ellipsoid/ellipsoid_stats) --- skimage/draw/draw3d.py | 4 +- skimage/draw/tests/test_draw3d.py | 104 ++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 2 deletions(-) create mode 100644 skimage/draw/tests/test_draw3d.py diff --git a/skimage/draw/draw3d.py b/skimage/draw/draw3d.py index e247e360..0b6fcb2d 100644 --- a/skimage/draw/draw3d.py +++ b/skimage/draw/draw3d.py @@ -83,9 +83,9 @@ def ellipsoid_stats(a, b, c, sampling=(1., 1., 1.)): Returns ------- vol : float - Analytically calculated volume of ellipsoid. + Calculated volume of ellipsoid. surf : float - Analytically calculated surface area of ellipsoid. + Calculated surface area of ellipsoid. """ if (a <= 0) or (b <= 0) or (c <= 0): diff --git a/skimage/draw/tests/test_draw3d.py b/skimage/draw/tests/test_draw3d.py new file mode 100644 index 00000000..26c63cde --- /dev/null +++ b/skimage/draw/tests/test_draw3d.py @@ -0,0 +1,104 @@ +from numpy.testing import assert_array_equal +import numpy as np + +from skimage.draw import ellipsoid, ellipsoid_stats + + +def test_ellipsoid_bool(): + test = ellipsoid(2, 2, 2)[1:-1, 1:-1, 1:-1] + test_anisotropic = ellipsoid(2, 2, 4, sampling=(1., 1., 2.)) + test_anisotropic = test_anisotropic[1:-1, 1:-1, 1:-1] + + expected = np.r_[[[[0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 1, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0]], + + [[0, 0, 0, 0, 0], + [0, 1, 1, 1, 0], + [0, 1, 1, 1, 0], + [0, 1, 1, 1, 0], + [0, 0, 0, 0, 0]], + + [[0, 0, 1, 0, 0], + [0, 1, 1, 1, 0], + [1, 1, 1, 1, 1], + [0, 1, 1, 1, 0], + [0, 0, 1, 0, 0]], + + [[0, 0, 0, 0, 0], + [0, 1, 1, 1, 0], + [0, 1, 1, 1, 0], + [0, 1, 1, 1, 0], + [0, 0, 0, 0, 0]], + + [[0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 1, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0]]]] + + assert_array_equal(test, expected.astype(bool)) + assert_array_equal(test_anisotropic, expected.astype(bool)) + + +def test_ellipsoid_levelset(): + test = ellipsoid(2, 2, 2, levelset=True)[1:-1, 1:-1, 1:-1] + test_anisotropic = ellipsoid(2, 2, 4, sampling=(1., 1., 2.), + levelset=True) + test_anisotropic = test_anisotropic[1:-1, 1:-1, 1:-1] + + expected = np.r_[[[[ 2. , 1.25, 1. , 1.25, 2. ], + [ 1.25, 0.5 , 0.25, 0.5 , 1.25], + [ 1. , 0.25, 0. , 0.25, 1. ], + [ 1.25, 0.5 , 0.25, 0.5 , 1.25], + [ 2. , 1.25, 1. , 1.25, 2. ]], + + [[ 1.25, 0.5 , 0.25, 0.5 , 1.25], + [ 0.5 , -0.25, -0.5 , -0.25, 0.5 ], + [ 0.25, -0.5 , -0.75, -0.5 , 0.25], + [ 0.5 , -0.25, -0.5 , -0.25, 0.5 ], + [ 1.25, 0.5 , 0.25, 0.5 , 1.25]], + + [[ 1. , 0.25, 0. , 0.25, 1. ], + [ 0.25, -0.5 , -0.75, -0.5 , 0.25], + [ 0. , -0.75, -1. , -0.75, 0. ], + [ 0.25, -0.5 , -0.75, -0.5 , 0.25], + [ 1. , 0.25, 0. , 0.25, 1. ]], + + [[ 1.25, 0.5 , 0.25, 0.5 , 1.25], + [ 0.5 , -0.25, -0.5 , -0.25, 0.5 ], + [ 0.25, -0.5 , -0.75, -0.5 , 0.25], + [ 0.5 , -0.25, -0.5 , -0.25, 0.5 ], + [ 1.25, 0.5 , 0.25, 0.5 , 1.25]], + + [[ 2. , 1.25, 1. , 1.25, 2. ], + [ 1.25, 0.5 , 0.25, 0.5 , 1.25], + [ 1. , 0.25, 0. , 0.25, 1. ], + [ 1.25, 0.5 , 0.25, 0.5 , 1.25], + [ 2. , 1.25, 1. , 1.25, 2. ]]]] + + assert_array_equal(test, expected.astype(bool)) + assert_array_equal(test_anisotropic, expected.astype(bool)) + + +def test_ellipsoid_stats(): + # Test comparison values generated by Wolfram Alpha + vol, surf = ellipsoid_stats(6, 10, 16) + assert(round(1280 * np.pi, 4) == round(vol, 4)) + assert(1383.28 == round(surf, 2)) + + # Test when a <= b <= c does not hold + vol, surf = ellipsoid_stats(16, 6, 10) + assert(round(1280 * np.pi, 4) == round(vol, 4)) + assert(1383.28 == round(surf, 2)) + + # Larger test to ensure reliability over broad range + vol, surf = ellipsoid_stats(17, 27, 169) + assert(round(103428 * np.pi, 4) == round(vol, 4)) + assert(37426.3 == round(surf, 1)) + + +if __name__ == "__main__": + np.testing.run_module_suite() From cd306bad5704ac66c3ec8594ef30e93f11e4af3e Mon Sep 17 00:00:00 2001 From: "Josh Warner (Mac)" Date: Sun, 1 Sep 2013 21:41:50 -0500 Subject: [PATCH 569/736] FIX: Add _marching_cubes_cy.pyx to bento.info --- bento.info | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bento.info b/bento.info index acdb155d..b1169244 100644 --- a/bento.info +++ b/bento.info @@ -51,6 +51,9 @@ Library: Extension: skimage.measure._moments Sources: skimage/measure/_moments.pyx + Extension: skimage.measure._marching_cubes_cy + Sources: + skimage/measure/_marching_cubes_cy.pyx Extension: skimage.graph._mcp Sources: skimage/graph/_mcp.pyx From 1174d6fbc5ec7b9799e243ca20540d8effa39831 Mon Sep 17 00:00:00 2001 From: "Josh Warner (Mac)" Date: Sun, 1 Sep 2013 22:05:39 -0500 Subject: [PATCH 570/736] FIX: Errors in unit tests --- skimage/draw/tests/test_draw3d.py | 4 ++-- skimage/measure/tests/test_marching_cubes.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/skimage/draw/tests/test_draw3d.py b/skimage/draw/tests/test_draw3d.py index 26c63cde..14bf140f 100644 --- a/skimage/draw/tests/test_draw3d.py +++ b/skimage/draw/tests/test_draw3d.py @@ -79,8 +79,8 @@ def test_ellipsoid_levelset(): [ 1.25, 0.5 , 0.25, 0.5 , 1.25], [ 2. , 1.25, 1. , 1.25, 2. ]]]] - assert_array_equal(test, expected.astype(bool)) - assert_array_equal(test_anisotropic, expected.astype(bool)) + assert_array_equal(test, expected) + assert_array_equal(test_anisotropic, expected) def test_ellipsoid_stats(): diff --git a/skimage/measure/tests/test_marching_cubes.py b/skimage/measure/tests/test_marching_cubes.py index 991da060..7a1fd40a 100644 --- a/skimage/measure/tests/test_marching_cubes.py +++ b/skimage/measure/tests/test_marching_cubes.py @@ -7,7 +7,7 @@ from skimage.measure import marching_cubes, mesh_surface_area def test_marching_cubes_isotropic(): ellipsoid_isotropic = ellipsoid(6, 10, 16, levelset=True) - _, surf = ellipsoid_stats(6, 10, 16, levelset=True) + _, surf = ellipsoid_stats(6, 10, 16) verts, faces = marching_cubes(ellipsoid_isotropic, 0.) surf_calc = mesh_surface_area(verts, faces) @@ -17,9 +17,9 @@ def test_marching_cubes_isotropic(): def test_marching_cubes_anisotropic(): sampling = (1., 10 / 6., 16 / 6.) - ellipsoid_anisotropic, _, surf = ellipsoid(6, 10, 16, sampling=sampling, - levelset=True) - _, surf = ellipsoid_stats(6, 10, 16, sampling=sampling, levelset=True) + ellipsoid_anisotropic = ellipsoid(6, 10, 16, sampling=sampling, + levelset=True) + _, surf = ellipsoid_stats(6, 10, 16, sampling=sampling) verts, faces = marching_cubes(ellipsoid_anisotropic, 0., sampling=sampling) surf_calc = mesh_surface_area(verts, faces) From abc7a16a8099ce0110b02b2dd934ef4873048943 Mon Sep 17 00:00:00 2001 From: "Josh Warner (Mac)" Date: Mon, 2 Sep 2013 15:31:51 -0500 Subject: [PATCH 571/736] STYLE: Use `np.array` instead of `np.r_` and import Poly3DCollection --- doc/examples/plot_marching_cubes.py | 6 +- skimage/draw/tests/test_draw3d.py | 106 ++++++++++++++-------------- 2 files changed, 56 insertions(+), 56 deletions(-) diff --git a/doc/examples/plot_marching_cubes.py b/doc/examples/plot_marching_cubes.py index 0b321bfb..a57a40a2 100644 --- a/doc/examples/plot_marching_cubes.py +++ b/doc/examples/plot_marching_cubes.py @@ -23,7 +23,7 @@ voxel spacing is not equal for every spatial dimension, through use of the import numpy as np import matplotlib.pyplot as plt from mpl_toolkits.mplot3d import Axes3D -import mpl_toolkits.mplot3d as a3 +from mpl_toolkits.mplot3d.art3d import Poly3DCollection from skimage import measure from skimage.draw import ellipsoid @@ -41,8 +41,8 @@ verts, faces = measure.marching_cubes(ellip_double, 0) fig = plt.figure(figsize=(10, 12)) ax = fig.add_subplot(111, projection='3d') -# Fancy indexing: `verts[faces]` to generate a Poly3DCollection -mesh = a3.art3d.Poly3DCollection(verts[faces]) +# Fancy indexing: `verts[faces]` to generate a collection of triangles +mesh = Poly3DCollection(verts[faces]) ax.add_collection3d(mesh) ax.set_xlabel("x-axis: a = 6 per ellipsoid") diff --git a/skimage/draw/tests/test_draw3d.py b/skimage/draw/tests/test_draw3d.py index 14bf140f..59a7f6b3 100644 --- a/skimage/draw/tests/test_draw3d.py +++ b/skimage/draw/tests/test_draw3d.py @@ -1,5 +1,5 @@ -from numpy.testing import assert_array_equal import numpy as np +from numpy.testing import assert_array_equal, assert_allclose from skimage.draw import ellipsoid, ellipsoid_stats @@ -9,35 +9,35 @@ def test_ellipsoid_bool(): test_anisotropic = ellipsoid(2, 2, 4, sampling=(1., 1., 2.)) test_anisotropic = test_anisotropic[1:-1, 1:-1, 1:-1] - expected = np.r_[[[[0, 0, 0, 0, 0], - [0, 0, 0, 0, 0], - [0, 0, 1, 0, 0], - [0, 0, 0, 0, 0], - [0, 0, 0, 0, 0]], + expected = np.array([[[0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 1, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0]], - [[0, 0, 0, 0, 0], - [0, 1, 1, 1, 0], - [0, 1, 1, 1, 0], - [0, 1, 1, 1, 0], - [0, 0, 0, 0, 0]], + [[0, 0, 0, 0, 0], + [0, 1, 1, 1, 0], + [0, 1, 1, 1, 0], + [0, 1, 1, 1, 0], + [0, 0, 0, 0, 0]], - [[0, 0, 1, 0, 0], - [0, 1, 1, 1, 0], - [1, 1, 1, 1, 1], - [0, 1, 1, 1, 0], - [0, 0, 1, 0, 0]], + [[0, 0, 1, 0, 0], + [0, 1, 1, 1, 0], + [1, 1, 1, 1, 1], + [0, 1, 1, 1, 0], + [0, 0, 1, 0, 0]], - [[0, 0, 0, 0, 0], - [0, 1, 1, 1, 0], - [0, 1, 1, 1, 0], - [0, 1, 1, 1, 0], - [0, 0, 0, 0, 0]], + [[0, 0, 0, 0, 0], + [0, 1, 1, 1, 0], + [0, 1, 1, 1, 0], + [0, 1, 1, 1, 0], + [0, 0, 0, 0, 0]], - [[0, 0, 0, 0, 0], - [0, 0, 0, 0, 0], - [0, 0, 1, 0, 0], - [0, 0, 0, 0, 0], - [0, 0, 0, 0, 0]]]] + [[0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 1, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0]]]) assert_array_equal(test, expected.astype(bool)) assert_array_equal(test_anisotropic, expected.astype(bool)) @@ -49,38 +49,38 @@ def test_ellipsoid_levelset(): levelset=True) test_anisotropic = test_anisotropic[1:-1, 1:-1, 1:-1] - expected = np.r_[[[[ 2. , 1.25, 1. , 1.25, 2. ], - [ 1.25, 0.5 , 0.25, 0.5 , 1.25], - [ 1. , 0.25, 0. , 0.25, 1. ], - [ 1.25, 0.5 , 0.25, 0.5 , 1.25], - [ 2. , 1.25, 1. , 1.25, 2. ]], + expected = np.array([[[ 2. , 1.25, 1. , 1.25, 2. ], + [ 1.25, 0.5 , 0.25, 0.5 , 1.25], + [ 1. , 0.25, 0. , 0.25, 1. ], + [ 1.25, 0.5 , 0.25, 0.5 , 1.25], + [ 2. , 1.25, 1. , 1.25, 2. ]], - [[ 1.25, 0.5 , 0.25, 0.5 , 1.25], - [ 0.5 , -0.25, -0.5 , -0.25, 0.5 ], - [ 0.25, -0.5 , -0.75, -0.5 , 0.25], - [ 0.5 , -0.25, -0.5 , -0.25, 0.5 ], - [ 1.25, 0.5 , 0.25, 0.5 , 1.25]], + [[ 1.25, 0.5 , 0.25, 0.5 , 1.25], + [ 0.5 , -0.25, -0.5 , -0.25, 0.5 ], + [ 0.25, -0.5 , -0.75, -0.5 , 0.25], + [ 0.5 , -0.25, -0.5 , -0.25, 0.5 ], + [ 1.25, 0.5 , 0.25, 0.5 , 1.25]], - [[ 1. , 0.25, 0. , 0.25, 1. ], - [ 0.25, -0.5 , -0.75, -0.5 , 0.25], - [ 0. , -0.75, -1. , -0.75, 0. ], - [ 0.25, -0.5 , -0.75, -0.5 , 0.25], - [ 1. , 0.25, 0. , 0.25, 1. ]], + [[ 1. , 0.25, 0. , 0.25, 1. ], + [ 0.25, -0.5 , -0.75, -0.5 , 0.25], + [ 0. , -0.75, -1. , -0.75, 0. ], + [ 0.25, -0.5 , -0.75, -0.5 , 0.25], + [ 1. , 0.25, 0. , 0.25, 1. ]], - [[ 1.25, 0.5 , 0.25, 0.5 , 1.25], - [ 0.5 , -0.25, -0.5 , -0.25, 0.5 ], - [ 0.25, -0.5 , -0.75, -0.5 , 0.25], - [ 0.5 , -0.25, -0.5 , -0.25, 0.5 ], - [ 1.25, 0.5 , 0.25, 0.5 , 1.25]], + [[ 1.25, 0.5 , 0.25, 0.5 , 1.25], + [ 0.5 , -0.25, -0.5 , -0.25, 0.5 ], + [ 0.25, -0.5 , -0.75, -0.5 , 0.25], + [ 0.5 , -0.25, -0.5 , -0.25, 0.5 ], + [ 1.25, 0.5 , 0.25, 0.5 , 1.25]], - [[ 2. , 1.25, 1. , 1.25, 2. ], - [ 1.25, 0.5 , 0.25, 0.5 , 1.25], - [ 1. , 0.25, 0. , 0.25, 1. ], - [ 1.25, 0.5 , 0.25, 0.5 , 1.25], - [ 2. , 1.25, 1. , 1.25, 2. ]]]] + [[ 2. , 1.25, 1. , 1.25, 2. ], + [ 1.25, 0.5 , 0.25, 0.5 , 1.25], + [ 1. , 0.25, 0. , 0.25, 1. ], + [ 1.25, 0.5 , 0.25, 0.5 , 1.25], + [ 2. , 1.25, 1. , 1.25, 2. ]]]) - assert_array_equal(test, expected) - assert_array_equal(test_anisotropic, expected) + assert_allclose(test, expected) + assert_allclose(test_anisotropic, expected) def test_ellipsoid_stats(): From d2bc02c10bf4446f5b8a8d678641ea8330800dd4 Mon Sep 17 00:00:00 2001 From: "Josh Warner (Mac)" Date: Mon, 2 Sep 2013 20:23:00 -0500 Subject: [PATCH 572/736] Credit myself for Marching Cubes --- CONTRIBUTORS.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index b304646b..28203676 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -114,7 +114,7 @@ - Joshua Warner Multichannel random walker segmentation, unified peak finder backend, - n-dimensional array padding, bug and doc fixes. + n-dimensional array padding, marching cubes, bug and doc fixes. - Petter Strandmark Perimeter calculation in regionprops. From d9e64b43e6c3d3434ba49fef971a33b301436407 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Tue, 3 Sep 2013 08:40:18 +0200 Subject: [PATCH 573/736] Make image C-contiguous --- skimage/segmentation/slic_superpixels.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/segmentation/slic_superpixels.py b/skimage/segmentation/slic_superpixels.py index 5a6d4506..4f10e75f 100644 --- a/skimage/segmentation/slic_superpixels.py +++ b/skimage/segmentation/slic_superpixels.py @@ -131,7 +131,7 @@ def slic(image, n_segments=100, compactness=10., max_iter=20, sigma=1, # we do the scaling of ratio in the same way as in the SLIC paper # so the values have the same meaning ratio = float(max((step_z, step_y, step_x))) / compactness - image = image * ratio + image = np.ascontiguousarray(image * ratio) labels = _slic_cython(image, clusters, max_iter) From 770e28d2bb6a37a7a32f9ed2a151a6a44b44e0a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Tue, 3 Sep 2013 10:47:49 +0200 Subject: [PATCH 574/736] Rename clusters to segments --- skimage/segmentation/_slic.pyx | 72 ++++++++++++------------ skimage/segmentation/slic_superpixels.py | 20 +++---- 2 files changed, 46 insertions(+), 46 deletions(-) diff --git a/skimage/segmentation/_slic.pyx b/skimage/segmentation/_slic.pyx index 834c5181..ac45a5c4 100644 --- a/skimage/segmentation/_slic.pyx +++ b/skimage/segmentation/_slic.pyx @@ -11,7 +11,7 @@ from skimage.util import regular_grid def _slic_cython(double[:, :, :, ::1] image_zyx, - double[:, ::1] clusters, + double[:, ::1] segments, Py_ssize_t max_iter): """Helper function for SLIC segmentation. @@ -19,14 +19,14 @@ def _slic_cython(double[:, :, :, ::1] image_zyx, ---------- image_zyx : 4D array of double, shape (Z, Y, X, C) The input image. - clusters : 2D array of double, shape (N, 3 + C) + segments : 2D array of double, shape (N, 3 + C) The initial centroids obtained by SLIC as [Z, Y, X, C...]. max_iter : int The maximum number of k-means iterations. Returns ------- - nearest_clusters : 3D array of int, shape (Z, Y, X) + nearest_segments : 3D array of int, shape (Z, Y, X) The label field/superpixels found by SLIC. """ @@ -36,36 +36,36 @@ def _slic_cython(double[:, :, :, ::1] image_zyx, height = image_zyx.shape[1] width = image_zyx.shape[2] - cdef Py_ssize_t n_clusters = clusters.shape[0] + cdef Py_ssize_t n_segments = segments.shape[0] # number of features [X, Y, Z, ...] - cdef Py_ssize_t n_features = clusters.shape[1] + cdef Py_ssize_t n_features = segments.shape[1] # approximate grid size for desired n_segments cdef Py_ssize_t step_z, step_y, step_x - slices = regular_grid((depth, height, width), n_clusters) + slices = regular_grid((depth, height, width), n_segments) step_z, step_y, step_x = [int(s.step) for s in slices] - cdef Py_ssize_t[:, :, ::1] nearest_clusters \ + cdef Py_ssize_t[:, :, ::1] nearest_segments \ = np.empty((depth, height, width), dtype=np.intp) cdef double[:, :, ::1] distance \ = np.empty((depth, height, width), dtype=np.double) - cdef Py_ssize_t[:] n_cluster_elems = np.zeros(n_clusters, dtype=np.intp) + cdef Py_ssize_t[:] n_segment_elems = np.zeros(n_segments, dtype=np.intp) cdef Py_ssize_t i, c, k, x, y, z, x_min, x_max, y_min, y_max, z_min, z_max cdef char change - cdef double dist_mean, cx, cy, cz, dy, dz + cdef double dist_center, cx, cy, cz, dy, dz for i in range(max_iter): change = 0 distance[:, :, :] = DBL_MAX - # assign pixels to clusters - for k in range(n_clusters): + # assign pixels to segments + for k in range(n_segments): - # cluster coordinate centers - cz = clusters[k, 0] - cy = clusters[k, 1] - cx = clusters[k, 2] + # segment coordinate centers + cz = segments[k, 0] + cy = segments[k, 1] + cx = segments[k, 2] # compute windows z_min = max(cz - 2 * step_z, 0) @@ -80,38 +80,38 @@ def _slic_cython(double[:, :, :, ::1] image_zyx, for y in range(y_min, y_max): dy = (cy - y) ** 2 for x in range(x_min, x_max): - dist_mean = dz + dy + (cx - x) ** 2 + dist_center = dz + dy + (cx - x) ** 2 for c in range(3, n_features): - dist_mean += (image_zyx[z, y, x, c - 3] - - clusters[k, c]) ** 2 - if distance[z, y, x] > dist_mean: - nearest_clusters[z, y, x] = k - distance[z, y, x] = dist_mean + dist_center += (image_zyx[z, y, x, c - 3] + - segments[k, c]) ** 2 + if distance[z, y, x] > dist_center: + nearest_segments[z, y, x] = k + distance[z, y, x] = dist_center change = 1 - # stop if no pixel changed its cluster + # stop if no pixel changed its segment if change == 0: break - # recompute clusters + # recompute segment centers - # sum features for all clusters - n_cluster_elems[:] = 0 - clusters[:, :] = 0 + # sum features for all segments + n_segment_elems[:] = 0 + segments[:, :] = 0 for z in range(depth): for y in range(height): for x in range(width): - k = nearest_clusters[z, y, x] - n_cluster_elems[k] += 1 - clusters[k, 0] += z - clusters[k, 1] += y - clusters[k, 2] += x + k = nearest_segments[z, y, x] + n_segment_elems[k] += 1 + segments[k, 0] += z + segments[k, 1] += y + segments[k, 2] += x for c in range(3, n_features): - clusters[k, c] += image_zyx[z, y, x, c - 3] + segments[k, c] += image_zyx[z, y, x, c - 3] - # divide by number of elements per cluster to obtain mean - for k in range(n_clusters): + # divide by number of elements per segment to obtain mean + for k in range(n_segments): for c in range(n_features): - clusters[k, c] /= n_cluster_elems[k] + segments[k, c] /= n_segment_elems[k] - return np.asarray(nearest_clusters) + return np.asarray(nearest_segments) diff --git a/skimage/segmentation/slic_superpixels.py b/skimage/segmentation/slic_superpixels.py index 4f10e75f..c5470992 100644 --- a/skimage/segmentation/slic_superpixels.py +++ b/skimage/segmentation/slic_superpixels.py @@ -116,24 +116,24 @@ def slic(image, n_segments=100, compactness=10., max_iter=20, sigma=1, grid_z, grid_y, grid_x = np.mgrid[:depth, :height, :width] slices = regular_grid(image.shape[:3], n_segments) step_z, step_y, step_x = [int(s.step) for s in slices] - clusters_z = grid_z[slices] - clusters_y = grid_y[slices] - clusters_x = grid_x[slices] + segments_z = grid_z[slices] + segments_y = grid_y[slices] + segments_x = grid_x[slices] - clusters_color = np.zeros(clusters_z.shape + (image.shape[3],)) - clusters = np.concatenate([clusters_z[..., np.newaxis], - clusters_y[..., np.newaxis], - clusters_x[..., np.newaxis], - clusters_color + segments_color = np.zeros(segments_z.shape + (image.shape[3],)) + segments = np.concatenate([segments_z[..., np.newaxis], + segments_y[..., np.newaxis], + segments_x[..., np.newaxis], + segments_color ], axis=-1).reshape(-1, 3 + image.shape[3]) - clusters = np.ascontiguousarray(clusters) + segments = np.ascontiguousarray(segments) # we do the scaling of ratio in the same way as in the SLIC paper # so the values have the same meaning ratio = float(max((step_z, step_y, step_x))) / compactness image = np.ascontiguousarray(image * ratio) - labels = _slic_cython(image, clusters, max_iter) + labels = _slic_cython(image, segments, max_iter) if labels.shape[0] == 1: labels = labels[0] From 8137c41e222c72941454a25fb0e214f6fc0c1ba9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Tue, 3 Sep 2013 10:48:53 +0200 Subject: [PATCH 575/736] Add optional description to parameters --- skimage/segmentation/slic_superpixels.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/skimage/segmentation/slic_superpixels.py b/skimage/segmentation/slic_superpixels.py index c5470992..1adba964 100644 --- a/skimage/segmentation/slic_superpixels.py +++ b/skimage/segmentation/slic_superpixels.py @@ -19,22 +19,22 @@ def slic(image, n_segments=100, compactness=10., max_iter=20, sigma=1, image : 2D, 3D or 4D ndarray Input image, which can be 2D or 3D, and grayscale or multichannel (see `multichannel` parameter). - n_segments : int + n_segments : int, optional The (approximate) number of labels in the segmented output image. - compactness : float + compactness : float, optional Balances color-space proximity and image-space proximity. Higher values give more weight to image-space. As `compactness` tends to infinity, superpixel shapes become square/cubic. - max_iter : int + max_iter : int, optional Maximum number of iterations of k-means. - sigma : float or (3,) array of floats + sigma : float or (3,) array of floats, optional Width of Gaussian smoothing kernel for pre-processing for each dimension of the image. The same sigma is applied to each dimension in case of a scalar value. Zero means no smoothing. - multichannel : bool + multichannel : bool, optional Whether the last axis of the image is to be interpreted as multiple channels or another spatial dimension. - convert2lab : bool + convert2lab : bool, optional Whether the input should be converted to Lab colorspace prior to segmentation. For this purpose, the input is assumed to be RGB. Highly recommended. From 22ca0ae45729af1bb4a6bfe261f758b408aa46ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Tue, 3 Sep 2013 20:31:24 +0200 Subject: [PATCH 576/736] Fix indentation --- skimage/segmentation/_slic.pyx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/segmentation/_slic.pyx b/skimage/segmentation/_slic.pyx index ac45a5c4..d818a501 100644 --- a/skimage/segmentation/_slic.pyx +++ b/skimage/segmentation/_slic.pyx @@ -83,7 +83,7 @@ def _slic_cython(double[:, :, :, ::1] image_zyx, dist_center = dz + dy + (cx - x) ** 2 for c in range(3, n_features): dist_center += (image_zyx[z, y, x, c - 3] - - segments[k, c]) ** 2 + - segments[k, c]) ** 2 if distance[z, y, x] > dist_center: nearest_segments[z, y, x] = k distance[z, y, x] = dist_center From 34b1b7b4143868a9ab16c7f46d6edf3f2175fe13 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Sat, 7 Sep 2013 00:51:21 +1000 Subject: [PATCH 577/736] Fix errors in relabel_from_one docs; PEP8 --- skimage/segmentation/_join.py | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/skimage/segmentation/_join.py b/skimage/segmentation/_join.py index 454da71e..4afc3a66 100644 --- a/skimage/segmentation/_join.py +++ b/skimage/segmentation/_join.py @@ -1,5 +1,6 @@ import numpy as np + def join_segmentations(s1, s2): """Return the join of the two input segmentations. @@ -41,6 +42,7 @@ def join_segmentations(s1, s2): j = relabel_from_one(j)[0] return j + def relabel_from_one(label_field): """Convert labels in an arbitrary label field to {1, ... number_of_labels}. @@ -50,14 +52,29 @@ def relabel_from_one(label_field): Parameters ---------- - label_field : numpy ndarray (integer type) + label_field : numpy array of int, arbitrary shape Returns ------- - relabeled : numpy array of same shape as ar - forward_map : 1d numpy array of length np.unique(ar) + 1 - inverse_map : 1d numpy array of length len(np.unique(ar)) - The length is len(np.unique(ar)) + 1 if 0 is not in np.unique(ar) + relabeled : numpy array of int, same shape as `label_field` + The input label field with labels mapped to + {1, ..., number_of_labels}. + forward_map : numpy array of int, shape ``(label_field.max() + 1,)`` + The map from the original label space to the returned label + space. Can be used to re-apply the same mapping. See examples + for usage. + inverse_map : numpy array of int, shape ``(len(np.unique(label_field)),)`` + The map from the new label space to the original space. This + can be used to reconstruct the original label field from the + relabeled one. + + Notes + ----- + The forward map can be extremely big for some inputs, since its + length is given by the maximum of the label field. However, in most + situations, `label_field.max()` is much smaller than + `label_field.size`, and in these cases the forward map is + guaranteed to be smaller than either the input or output images. Examples -------- @@ -91,3 +108,4 @@ def relabel_from_one(label_field): labels = np.concatenate(([0], labels)) inverse_map = labels return forward_map[label_field], forward_map, inverse_map + From 6f5f6a0a7a1754dbd480db85e9e9e061920d5832 Mon Sep 17 00:00:00 2001 From: "Josh Warner (Mac)" Date: Sat, 7 Sep 2013 00:39:04 -0500 Subject: [PATCH 578/736] DOC: Further improvements to naming conventions. --- skimage/util/noise.py | 20 ++++++++++---------- skimage/util/tests/test_random_noise.py | 8 ++++---- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/skimage/util/noise.py b/skimage/util/noise.py index 71b69aad..d0a3c5f6 100644 --- a/skimage/util/noise.py +++ b/skimage/util/noise.py @@ -32,10 +32,10 @@ def random_noise(image, mode='gaussian', seed=None, **kwargs): var : float Variance of random distribution. Used in 'gaussian' and 'speckle'. Note: variance = (standard deviation) ** 2. Default : 0.01 - prop_replace : float + amount : float Proportion of image pixels to replace with noise on range [0, 1]. Used in 'salt', 'pepper', and 'salt & pepper'. Default : 0.05 - prop_salt : float + salt_vs_pepper : float Proportion of salt vs. pepper noise for 's&p' on range [0, 1]. Higher values represent more salt. Default : 0.5 (equal amounts) @@ -61,13 +61,13 @@ def random_noise(image, mode='gaussian', seed=None, **kwargs): kwdefaults = { 'mean': 0., 'var': 0.01, - 'prop_replace': 0.05, - 'prop_salt': 0.5} + 'amount': 0.05, + 'salt_vs_pepper': 0.5} allowedkwargs = { 'gaussian_values': ['mean', 'var'], - 'sp_values': ['prop_replace'], - 's&p_values': ['prop_replace', 'prop_salt']} + 'sp_values': ['amount'], + 's&p_values': ['amount', 'salt_vs_pepper']} for key in kwargs: if key not in allowedkwargs[allowedtypes[mode]]: @@ -95,12 +95,12 @@ def random_noise(image, mode='gaussian', seed=None, **kwargs): elif mode == 'salt': # Re-call function with mode='s&p' and p=1 (all salt noise) out = random_noise(image, mode='s&p', seed=seed, - prop_replace=kwargs['prop_replace'], prop_salt=1.) + amount=kwargs['amount'], salt_vs_pepper=1.) elif mode == 'pepper': # Re-call function with mode='s&p' and p=1 (all pepper noise) out = random_noise(image, mode='s&p', seed=seed, - prop_replace=kwargs['prop_replace'], prop_salt=0.) + amount=kwargs['amount'], salt_vs_pepper=0.) elif mode == 's&p': # This mode makes no effort to avoid repeat sampling. Thus, the @@ -109,14 +109,14 @@ def random_noise(image, mode='gaussian', seed=None, **kwargs): # Salt mode num_salt = np.ceil( - kwargs['prop_replace'] * image.size * kwargs['prop_salt']) + kwargs['amount'] * image.size * kwargs['salt_vs_pepper']) coords = [np.random.randint(0, i - 1, int(num_salt)) for i in image.shape] out[coords] = 1 # Pepper mode num_pepper = np.ceil( - kwargs['prop_replace'] * image.size * (1. - kwargs['prop_salt'])) + kwargs['amount'] * image.size * (1. - kwargs['salt_vs_pepper'])) coords = [np.random.randint(0, i - 1, int(num_pepper)) for i in image.shape] out[coords] = 0 diff --git a/skimage/util/tests/test_random_noise.py b/skimage/util/tests/test_random_noise.py index 6b6ba5c4..87005d60 100644 --- a/skimage/util/tests/test_random_noise.py +++ b/skimage/util/tests/test_random_noise.py @@ -15,7 +15,7 @@ def test_set_seed(): def test_salt(): seed = 42 cam = img_as_float(camera()) - cam_noisy = random_noise(cam, seed=seed, mode='salt', prop_replace=0.15) + cam_noisy = random_noise(cam, seed=seed, mode='salt', amount=0.15) saltmask = cam != cam_noisy # Ensure all changes are to 1.0 @@ -29,7 +29,7 @@ def test_salt(): def test_pepper(): seed = 42 cam = img_as_float(camera()) - cam_noisy = random_noise(cam, seed=seed, mode='pepper', prop_replace=0.15) + cam_noisy = random_noise(cam, seed=seed, mode='pepper', amount=0.15) peppermask = cam != cam_noisy # Ensure all changes are to 1.0 @@ -43,8 +43,8 @@ def test_pepper(): def test_salt_and_pepper(): seed = 42 cam = img_as_float(camera()) - cam_noisy = random_noise(cam, seed=seed, mode='s&p', prop_replace=0.15, - prop_salt=0.25) + cam_noisy = random_noise(cam, seed=seed, mode='s&p', amount=0.15, + salt_vs_pepper=0.25) saltmask = np.logical_and(cam != cam_noisy, cam_noisy == 1.) peppermask = np.logical_and(cam != cam_noisy, cam_noisy == 0.) From a315ae718d50da67d40fa4c26e99605d74ed363c Mon Sep 17 00:00:00 2001 From: radioxoma Date: Mon, 12 Aug 2013 22:47:13 +0300 Subject: [PATCH 579/736] Add Yen threshold method --- skimage/filter/thresholding.py | 48 ++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/skimage/filter/thresholding.py b/skimage/filter/thresholding.py index 40e62d12..3258c468 100644 --- a/skimage/filter/thresholding.py +++ b/skimage/filter/thresholding.py @@ -133,3 +133,51 @@ def threshold_otsu(image, nbins=256): idx = np.argmax(variance12) threshold = bin_centers[:-1][idx] return threshold + + +def threshold_yen(image, nbins=256): + """Return threshold value based on Yen's method. + + Parameters + ---------- + image : array + Input image. + nbins : int + Number of bins used to calculate histogram. This value is ignored for + integer arrays. + + Returns + ------- + threshold : float + Threshold value. + + References + ---------- + .. [1] Yen J.C., Chang F.J., and Chang S. (1995) "A New Criterion + for Automatic Multilevel Thresholding" IEEE Trans. on Image + Processing, 4(3): 370-378 + .. [2] Sezgin M. and Sankur B. (2004) "Survey over Image Thresholding + Techniques and Quantitative Performance Evaluation" Journal of + Electronic Imaging, 13(1): 146-165, + http://www.busim.ee.boun.edu.tr/~sankur/SankurFolder/Threshold_survey.pdf + .. [3] ImageJ AutoThresholder code, http://fiji.sc/wiki/index.php/Auto_Threshold + + Examples + -------- + >>> from skimage.data import camera + >>> image = camera() + >>> thresh = threshold_yen(image) + >>> binary = image > thresh + """ + hist, bin_centers = histogram(img, nbins) + hist = hist.astype(float) + norm_histo = hist / hist.sum() # Probability mass function + P1 = np.cumsum(norm_histo) # Cumulative normalized histogram + P1_sq = np.cumsum(norm_histo ** 2) + P2_sq = np.cumsum(norm_histo[::-1] ** 2)[::-1] + # P2_sq indexes is shifted +1. I assume, with P1[:-1] it's help avoid '-inf' in crit. + # In ImageJ Yen implementation, all invalid values replaced by zero. + crit = -1*np.log(P1_sq[:-1]*P2_sq[1:]) + 2.0*np.log(P1[:-1]*(1.0-P1[:-1])) + max_crit = np.argmax(crit) + threshold = bin_centers[:-1][max_crit] + return threshold \ No newline at end of file From e41887c66d05ecd8c1dd247900d9621d4ae71ebe Mon Sep 17 00:00:00 2001 From: radioxoma Date: Tue, 13 Aug 2013 11:52:27 +0300 Subject: [PATCH 580/736] Codestyle improvement --- CONTRIBUTORS.txt | 3 +++ skimage/filter/thresholding.py | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 28203676..33f763ba 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -148,3 +148,6 @@ - Matt Terry Color difference functions + +- Eugene Dvoretsky + Yen threshold implementation. diff --git a/skimage/filter/thresholding.py b/skimage/filter/thresholding.py index 3258c468..ce82500a 100644 --- a/skimage/filter/thresholding.py +++ b/skimage/filter/thresholding.py @@ -1,4 +1,4 @@ -__all__ = ['threshold_otsu', 'threshold_adaptive'] +__all__ = ['threshold_otsu', 'threshold_adaptive', 'threshold_yen'] import numpy as np import scipy.ndimage @@ -170,10 +170,10 @@ def threshold_yen(image, nbins=256): >>> binary = image > thresh """ hist, bin_centers = histogram(img, nbins) - hist = hist.astype(float) - norm_histo = hist / hist.sum() # Probability mass function + norm_histo = hist.astype(float) / hist.sum() # Probability mass function P1 = np.cumsum(norm_histo) # Cumulative normalized histogram P1_sq = np.cumsum(norm_histo ** 2) + # Get cumsum calculated from end of squared array: P2_sq = np.cumsum(norm_histo[::-1] ** 2)[::-1] # P2_sq indexes is shifted +1. I assume, with P1[:-1] it's help avoid '-inf' in crit. # In ImageJ Yen implementation, all invalid values replaced by zero. From 8beb2ae605cbbfa7205610bee0ba6de13677cbfd Mon Sep 17 00:00:00 2001 From: radioxoma Date: Thu, 15 Aug 2013 23:03:54 +0300 Subject: [PATCH 581/736] Yen PEP8 and docstrings --- skimage/filter/thresholding.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/skimage/filter/thresholding.py b/skimage/filter/thresholding.py index ce82500a..10a3ab41 100644 --- a/skimage/filter/thresholding.py +++ b/skimage/filter/thresholding.py @@ -95,14 +95,15 @@ def threshold_otsu(image, nbins=256): ---------- image : array Input image. - nbins : int + nbins : int, optional Number of bins used to calculate histogram. This value is ignored for integer arrays. Returns ------- threshold : float - Threshold value. + Upper threshold value. All pixels intensities that less or equal of + this value assumed as foreground. References ---------- @@ -113,7 +114,7 @@ def threshold_otsu(image, nbins=256): >>> from skimage.data import camera >>> image = camera() >>> thresh = threshold_otsu(image) - >>> binary = image > thresh + >>> binary = image <= thresh """ hist, bin_centers = histogram(image, nbins) hist = hist.astype(float) @@ -142,14 +143,15 @@ def threshold_yen(image, nbins=256): ---------- image : array Input image. - nbins : int + nbins : int, optional Number of bins used to calculate histogram. This value is ignored for integer arrays. Returns ------- threshold : float - Threshold value. + Upper threshold value. All pixels intensities that less or equal of + this value assumed as foreground. References ---------- @@ -167,7 +169,7 @@ def threshold_yen(image, nbins=256): >>> from skimage.data import camera >>> image = camera() >>> thresh = threshold_yen(image) - >>> binary = image > thresh + >>> binary = image <= thresh """ hist, bin_centers = histogram(img, nbins) norm_histo = hist.astype(float) / hist.sum() # Probability mass function @@ -175,9 +177,10 @@ def threshold_yen(image, nbins=256): P1_sq = np.cumsum(norm_histo ** 2) # Get cumsum calculated from end of squared array: P2_sq = np.cumsum(norm_histo[::-1] ** 2)[::-1] - # P2_sq indexes is shifted +1. I assume, with P1[:-1] it's help avoid '-inf' in crit. - # In ImageJ Yen implementation, all invalid values replaced by zero. - crit = -1*np.log(P1_sq[:-1]*P2_sq[1:]) + 2.0*np.log(P1[:-1]*(1.0-P1[:-1])) + # P2_sq indexes is shifted +1. I assume, with P1[:-1] it's help avoid '-inf' + # in crit. ImageJ Yen implementation replaces those values by zero. + crit = -1 * np.log(P1_sq[:-1] * P2_sq[1:]) + \ + 2.0 * np.log(P1[:-1] * (1.0 - P1[:-1])) max_crit = np.argmax(crit) threshold = bin_centers[:-1][max_crit] - return threshold \ No newline at end of file + return threshold From 9e74f4eb70b368acf51aeaeea15482f1f73f12ab Mon Sep 17 00:00:00 2001 From: radioxoma Date: Fri, 16 Aug 2013 19:58:35 +0300 Subject: [PATCH 582/736] Added Yen tests, typo fixed --- skimage/filter/tests/test_thresholding.py | 34 ++++++++++++++++++++++- skimage/filter/thresholding.py | 4 +-- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/skimage/filter/tests/test_thresholding.py b/skimage/filter/tests/test_thresholding.py index 97d3d9e3..0edfe4e7 100644 --- a/skimage/filter/tests/test_thresholding.py +++ b/skimage/filter/tests/test_thresholding.py @@ -3,7 +3,9 @@ from numpy.testing import assert_array_equal import skimage from skimage import data -from skimage.filter.thresholding import threshold_otsu, threshold_adaptive +from skimage.filter.thresholding import (threshold_adaptive, + threshold_otsu, + threshold_yen) class TestSimpleImage(): @@ -25,6 +27,26 @@ class TestSimpleImage(): image = np.float64(self.image) assert 2 <= threshold_otsu(image) < 3 + def test_yen(self): + assert threshold_yen(self.image) == 2 + + def test_yen_negative_int(self): + image = self.image - 2 + assert threshold_yen(image) == 0 + + def test_yen_float_image(self): + image = np.float64(self.image) + assert 2 <= threshold_yen(image) < 3 + + def test_yen_arange(self): + image = np.arange(256) + assert threshold_yen(image) == 127 + + def test_yen_binary(self): + image = np.zeros([2,256], dtype='uint8') + image[0] = 255 + assert threshold_yen(image) < 1 + def test_threshold_adaptive_generic(self): def func(arr): return arr.sum() / arr.shape[0] @@ -92,5 +114,15 @@ def test_otsu_lena_image(): assert 140 < threshold_otsu(lena) < 142 +def test_yen_coins_image(): + coins = skimage.img_as_ubyte(data.coins()) + assert 109 < threshold_yen(coins) < 111 + + +def test_yen_coins_image_as_float(): + coins = skimage.img_as_float(data.coins()) + assert 0.43 < threshold_yen(coins) < 0.44 + + if __name__ == '__main__': np.testing.run_module_suite() diff --git a/skimage/filter/thresholding.py b/skimage/filter/thresholding.py index 10a3ab41..8c78427b 100644 --- a/skimage/filter/thresholding.py +++ b/skimage/filter/thresholding.py @@ -1,4 +1,4 @@ -__all__ = ['threshold_otsu', 'threshold_adaptive', 'threshold_yen'] +__all__ = ['threshold_adaptive', 'threshold_otsu', 'threshold_yen'] import numpy as np import scipy.ndimage @@ -171,7 +171,7 @@ def threshold_yen(image, nbins=256): >>> thresh = threshold_yen(image) >>> binary = image <= thresh """ - hist, bin_centers = histogram(img, nbins) + hist, bin_centers = histogram(image, nbins) norm_histo = hist.astype(float) / hist.sum() # Probability mass function P1 = np.cumsum(norm_histo) # Cumulative normalized histogram P1_sq = np.cumsum(norm_histo ** 2) From 04804cc81ccc390e7e64e9a1fe3b214f11151d0d Mon Sep 17 00:00:00 2001 From: radioxoma Date: Sun, 18 Aug 2013 15:42:58 +0300 Subject: [PATCH 583/736] Logarithm optimisation --- skimage/filter/thresholding.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/skimage/filter/thresholding.py b/skimage/filter/thresholding.py index 8c78427b..ede3c76f 100644 --- a/skimage/filter/thresholding.py +++ b/skimage/filter/thresholding.py @@ -1,4 +1,4 @@ -__all__ = ['threshold_adaptive', 'threshold_otsu', 'threshold_yen'] +__all__ = ['threshold_otsu', 'threshold_adaptive', 'threshold_yen'] import numpy as np import scipy.ndimage @@ -179,8 +179,8 @@ def threshold_yen(image, nbins=256): P2_sq = np.cumsum(norm_histo[::-1] ** 2)[::-1] # P2_sq indexes is shifted +1. I assume, with P1[:-1] it's help avoid '-inf' # in crit. ImageJ Yen implementation replaces those values by zero. - crit = -1 * np.log(P1_sq[:-1] * P2_sq[1:]) + \ - 2.0 * np.log(P1[:-1] * (1.0 - P1[:-1])) + crit = np.log(((P1_sq[:-1] * P2_sq[1:]) ** -1) * \ + (P1[:-1] * (1.0 - P1[:-1])) ** 2) max_crit = np.argmax(crit) threshold = bin_centers[:-1][max_crit] return threshold From c8c95c0e633680e56c553a2c0ad9bf4ac3ee1538 Mon Sep 17 00:00:00 2001 From: radioxoma Date: Wed, 21 Aug 2013 20:18:34 +0300 Subject: [PATCH 584/736] Fix import list sequence --- skimage/filter/thresholding.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/filter/thresholding.py b/skimage/filter/thresholding.py index ede3c76f..bc052d8a 100644 --- a/skimage/filter/thresholding.py +++ b/skimage/filter/thresholding.py @@ -1,4 +1,4 @@ -__all__ = ['threshold_otsu', 'threshold_adaptive', 'threshold_yen'] +__all__ = ['threshold_adaptive', 'threshold_otsu', 'threshold_yen'] import numpy as np import scipy.ndimage From 2af5c41571e09b7c2e79a8c048dd84102b22e0eb Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Mon, 9 Sep 2013 13:53:33 +1000 Subject: [PATCH 585/736] Fix type hierarchy check in remove_small_objects --- skimage/morphology/misc.py | 2 +- skimage/morphology/tests/test_misc.py | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/skimage/morphology/misc.py b/skimage/morphology/misc.py index 88770371..5c157e7f 100644 --- a/skimage/morphology/misc.py +++ b/skimage/morphology/misc.py @@ -52,7 +52,7 @@ def remove_small_objects(ar, min_size=64, connectivity=1, in_place=False): True """ # Should use `issubdtype` for bool below, but there's a bug in numpy 1.7 - if not (ar.dtype == bool or np.issubdtype(ar.dtype, int)): + if not (ar.dtype == bool or np.issubdtype(ar.dtype, np.integer)): raise TypeError("Only bool or integer image types are supported. " "Got %s." % ar.dtype) diff --git a/skimage/morphology/tests/test_misc.py b/skimage/morphology/tests/test_misc.py index a5ab7ba9..94d27f64 100644 --- a/skimage/morphology/tests/test_misc.py +++ b/skimage/morphology/tests/test_misc.py @@ -42,6 +42,19 @@ def test_labeled_image(): assert_array_equal(observed, expected) +def test_uint_image(): + labeled_image = np.array([[2, 2, 2, 0, 1], + [2, 2, 2, 0, 1], + [2, 0, 0, 0, 0], + [0, 0, 3, 3, 3]], dtype=np.uint8) + expected = np.array([[2, 2, 2, 0, 0], + [2, 2, 2, 0, 0], + [2, 0, 0, 0, 0], + [0, 0, 3, 3, 3]], dtype=np.uint8) + observed = remove_small_objects(labeled_image, min_size=3) + assert_array_equal(observed, expected) + + def test_float_input(): float_test = np.random.rand(5, 5) assert_raises(TypeError, remove_small_objects, float_test) From 05aaeef5ac71d14f4d54eef2d9b8f5fb3c445346 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sat, 14 Sep 2013 10:08:07 +0200 Subject: [PATCH 586/736] Change default value of sigma --- skimage/segmentation/slic_superpixels.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/segmentation/slic_superpixels.py b/skimage/segmentation/slic_superpixels.py index 1adba964..4c4a7921 100644 --- a/skimage/segmentation/slic_superpixels.py +++ b/skimage/segmentation/slic_superpixels.py @@ -10,7 +10,7 @@ from skimage.segmentation._slic import _slic_cython from skimage.color import rgb2lab -def slic(image, n_segments=100, compactness=10., max_iter=20, sigma=1, +def slic(image, n_segments=100, compactness=10., max_iter=20, sigma=0, multichannel=True, convert2lab=True, ratio=None): """Segments image using k-means clustering in Color-(x,y,z) space. From bc2f23d1a8d3798c64753ed1e6a01505361b750a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Mon, 16 Sep 2013 07:59:57 +0200 Subject: [PATCH 587/736] Revert default value of maximum iterations --- skimage/segmentation/slic_superpixels.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/segmentation/slic_superpixels.py b/skimage/segmentation/slic_superpixels.py index 4c4a7921..2b2d8441 100644 --- a/skimage/segmentation/slic_superpixels.py +++ b/skimage/segmentation/slic_superpixels.py @@ -10,7 +10,7 @@ from skimage.segmentation._slic import _slic_cython from skimage.color import rgb2lab -def slic(image, n_segments=100, compactness=10., max_iter=20, sigma=0, +def slic(image, n_segments=100, compactness=10., max_iter=10, sigma=0, multichannel=True, convert2lab=True, ratio=None): """Segments image using k-means clustering in Color-(x,y,z) space. From ea1566fffb2cd08e7632be8032fd60977f30a1e2 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Mon, 16 Sep 2013 16:02:24 +1000 Subject: [PATCH 588/736] Fix image dimension sanitizing at function start `np.atleast_3d` will add a singleton dimension at the end of an array if needed. This is not the correct thing to do if `multichannel=False` based on the subsequent lines. If the input image was 2D with shape `(40, 50)` and `multichannel=False`, then `np.atleast_3d` gives it shape `(40, 50, 1)`, and then, because `multichannel=False`, the rest of the code gives it shape `(40, 50, 1, 1)`. This results in the final returned array having shape `(40, 50, 1)` instead of the desired `(40, 50)`. This commit fixes that and updates the test to detect this failure. --- skimage/segmentation/slic_superpixels.py | 31 ++++++++++++------------ skimage/segmentation/tests/test_slic.py | 2 ++ 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/skimage/segmentation/slic_superpixels.py b/skimage/segmentation/slic_superpixels.py index 4c4a7921..c088e17a 100644 --- a/skimage/segmentation/slic_superpixels.py +++ b/skimage/segmentation/slic_superpixels.py @@ -51,14 +51,12 @@ def slic(image, n_segments=100, compactness=10., max_iter=20, sigma=0, ValueError If: - the image dimension is not 2 or 3 and `multichannel == False`, OR - - the image dimension is not 3 or 4 and `multichannel == True`, OR - - `multichannel == True` and the length of the last dimension of - the image is not 3, OR + - the image dimension is not 3 or 4 and `multichannel == True` Notes ----- - If `sigma > 0` as is default, the image is smoothed using a Gaussian kernel - prior to segmentation. + If `sigma > 0`, the image is smoothed using a Gaussian kernel prior to + segmentation. The image is rescaled to be in [0, 1] prior to processing. @@ -88,15 +86,18 @@ def slic(image, n_segments=100, compactness=10., max_iter=20, sigma=0, compactness = ratio image = img_as_float(image) - image = np.atleast_3d(image) - - if image.ndim == 3: - if multichannel: - # Make 2D image 3D with depth = 1 - image = image[np.newaxis, ...] - else: - # Add channel as single last dimension - image = image[..., np.newaxis] + is2d = False + if image.ndim == 2: + # 2D grayscale image + image = image[np.newaxis, ..., np.newaxis] + is2d = True + elif image.ndim == 3 and multichannel: + # Make 2D multichannel image 3D with depth = 1 + image = image[np.newaxis, ...] + is2d = True + elif image.ndim == 3 and not multichannel: + # Add channel as single last dimension + image = image[..., np.newaxis] if not isinstance(sigma, coll.Iterable): sigma = np.array([sigma, sigma, sigma]) @@ -135,7 +136,7 @@ def slic(image, n_segments=100, compactness=10., max_iter=20, sigma=0, labels = _slic_cython(image, segments, max_iter) - if labels.shape[0] == 1: + if is2d: labels = labels[0] return labels diff --git a/skimage/segmentation/tests/test_slic.py b/skimage/segmentation/tests/test_slic.py index cb16e1b7..6d00716f 100644 --- a/skimage/segmentation/tests/test_slic.py +++ b/skimage/segmentation/tests/test_slic.py @@ -20,6 +20,7 @@ def test_color_2d(): # we expect 4 segments assert_equal(len(np.unique(seg)), 4) + assert_equal(seg.shape, img.shape[:-1]) assert_array_equal(seg[:10, :10], 0) assert_array_equal(seg[10:, :10], 2) assert_array_equal(seg[:10, 10:], 1) @@ -39,6 +40,7 @@ def test_gray_2d(): multichannel=False, convert2lab=False) assert_equal(len(np.unique(seg)), 4) + assert_equal(seg.shape, img.shape) assert_array_equal(seg[:10, :10], 0) assert_array_equal(seg[10:, :10], 2) assert_array_equal(seg[:10, 10:], 1) From 88d84c720ade54dd6f3eb69a15a428fc93d1fd59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Mon, 16 Sep 2013 08:48:46 +0200 Subject: [PATCH 589/736] Raise warning for sigma parameter and add to TODO for next release --- TODO.txt | 18 +++++++++--------- skimage/segmentation/slic_superpixels.py | 10 +++++++--- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/TODO.txt b/TODO.txt index 7ad40875..502961c8 100644 --- a/TODO.txt +++ b/TODO.txt @@ -1,15 +1,15 @@ Version 0.10 ------------ -* Remove deprecated functions: - - ``skimage.filter.rank.*`` -* Remove deprecated parameter ``epsilon`` of ``skimage.viewer.LineProfile`` -* Remove backwards-compatability of ``skimage.measure.regionprops`` +* Remove deprecated functions in `skimage.filter.rank.*` +* Remove deprecated parameter `epsilon` of `skimage.viewer.LineProfile` +* Remove backwards-compatability of `skimage.measure.regionprops` +* Remove {`ratio`, `sigma`} deprecation warnings of `skimage.segmentation.slic` Version 0.9 ----------- * Remove deprecated functions - - ``skimage.filter.denoise_tv_chambolle`` - - ``skimage.morphology.is_local_maximum`` - - ``skimage.transform.hough`` - - ``skimage.transform.probabilistic_hough`` - - ``skimage.transform.hough_peaks`` + - `skimage.filter.denoise_tv_chambolle` + - `skimage.morphology.is_local_maximum` + - `skimage.transform.hough` + - `skimage.transform.probabilistic_hough` + - `skimage.transform.hough_peaks` diff --git a/skimage/segmentation/slic_superpixels.py b/skimage/segmentation/slic_superpixels.py index 010d0c62..03017cf6 100644 --- a/skimage/segmentation/slic_superpixels.py +++ b/skimage/segmentation/slic_superpixels.py @@ -10,7 +10,7 @@ from skimage.segmentation._slic import _slic_cython from skimage.color import rgb2lab -def slic(image, n_segments=100, compactness=10., max_iter=10, sigma=0, +def slic(image, n_segments=100, compactness=10., max_iter=10, sigma=None, multichannel=True, convert2lab=True, ratio=None): """Segments image using k-means clustering in Color-(x,y,z) space. @@ -80,9 +80,13 @@ def slic(image, n_segments=100, compactness=10., max_iter=10, sigma=0, >>> segments = slic(img, n_segments=100, ratio=20) """ + if sigma is None: + warnings.warn('Default value of keyword `sigma` changed from ``1`` ' + 'to ``0``.') + sigma = 0 if ratio is not None: - msg = 'Keyword `ratio` is deprecated. Use `compactness` instead.' - warnings.warn(msg) + warnings.warn('Keyword `ratio` is deprecated. Use `compactness` ' + 'instead.') compactness = ratio image = img_as_float(image) From bf5f08e89487983bbea771a5bc8c604066f90aaf Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Mon, 16 Sep 2013 17:30:48 +1000 Subject: [PATCH 590/736] Update SLIC docstring to remove deprecated example The `ratio` keyword has been deprecated but it was still being used in the example in the SLIC docstring. This replaces that usage by the new `compactness` keyword. --- skimage/segmentation/slic_superpixels.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/skimage/segmentation/slic_superpixels.py b/skimage/segmentation/slic_superpixels.py index 03017cf6..07bb54a5 100644 --- a/skimage/segmentation/slic_superpixels.py +++ b/skimage/segmentation/slic_superpixels.py @@ -75,9 +75,9 @@ def slic(image, n_segments=100, compactness=10., max_iter=10, sigma=None, >>> from skimage.segmentation import slic >>> from skimage.data import lena >>> img = lena() - >>> segments = slic(img, n_segments=100, ratio=10) - >>> # Increasing the ratio parameter yields more square regions - >>> segments = slic(img, n_segments=100, ratio=20) + >>> segments = slic(img, n_segments=100, compactness=10) + >>> # Increasing the compactness parameter yields more square regions + >>> segments = slic(img, n_segments=100, compactness=20) """ if sigma is None: From 9d86c9a1e1199c223ac435cb16a39c230c3de313 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Mon, 16 Sep 2013 17:33:08 +1000 Subject: [PATCH 591/736] Ignore `convert2lab` keyword if not multichannel --- skimage/segmentation/slic_superpixels.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/skimage/segmentation/slic_superpixels.py b/skimage/segmentation/slic_superpixels.py index 07bb54a5..08ea7126 100644 --- a/skimage/segmentation/slic_superpixels.py +++ b/skimage/segmentation/slic_superpixels.py @@ -109,9 +109,8 @@ def slic(image, n_segments=100, compactness=10., max_iter=10, sigma=None, sigma = list(sigma) + [0] image = ndimage.gaussian_filter(image, sigma) - if convert2lab: - - if not multichannel or image.shape[3] != 3: + if convert2lab and multichannel: + if image.shape[3] != 3: raise ValueError("Lab colorspace conversion requires a RGB image.") image = rgb2lab(image) From 610a0d1793f1b3fb91544c1921311ecf20ff355d Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Mon, 16 Sep 2013 17:49:07 +1000 Subject: [PATCH 592/736] Add support for list sigma input in SLIC Previously, having a different `sigma` for different dimensions required an array input. This allows the user to use a simple list, which gets converted to an array internally. Importantly, it removes a very unhelpful error: ```python >>> im = np.random.rand(10, 20) >>> from skimage import segmentation as seg Exception AttributeError: "'UmfpackContext' object has no attribute '_symbolic'" in > ignored >>> s = seg.slic(im, 2, sigma=[2, 1]) --------------------------------------------------------------------------- AttributeError Traceback (most recent call last) in () ----> 1 s = seg.slic(im, 2, sigma=[2, 1]) /Users/nuneziglesiasj/venv/skimdev2/lib/python2.7/site-packages/scikit_image-0.9dev-py2.7-macosx-10.5-x86_64.egg/skimage/segmentation/slic_superpixels.pyc in slic(image, n_segments, compactness, max_iter, sigma, multichannel, convert2lab, ratio) 106 if not isinstance(sigma, coll.Iterable): 107 sigma = np.array([sigma, sigma, sigma]) --> 108 if (sigma > 0).any(): 109 sigma = list(sigma) + [0] 110 image = ndimage.gaussian_filter(image, sigma) AttributeError: 'bool' object has no attribute 'any' ``` --- skimage/segmentation/slic_superpixels.py | 6 ++++-- skimage/segmentation/tests/test_slic.py | 11 +++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/skimage/segmentation/slic_superpixels.py b/skimage/segmentation/slic_superpixels.py index 08ea7126..be07fafe 100644 --- a/skimage/segmentation/slic_superpixels.py +++ b/skimage/segmentation/slic_superpixels.py @@ -27,7 +27,7 @@ def slic(image, n_segments=100, compactness=10., max_iter=10, sigma=None, infinity, superpixel shapes become square/cubic. max_iter : int, optional Maximum number of iterations of k-means. - sigma : float or (3,) array of floats, optional + sigma : float or (3,) array-like of floats, optional Width of Gaussian smoothing kernel for pre-processing for each dimension of the image. The same sigma is applied to each dimension in case of a scalar value. Zero means no smoothing. @@ -104,7 +104,9 @@ def slic(image, n_segments=100, compactness=10., max_iter=10, sigma=None, image = image[..., np.newaxis] if not isinstance(sigma, coll.Iterable): - sigma = np.array([sigma, sigma, sigma]) + sigma = np.array([sigma, sigma, sigma], float) + elif type(sigma) in [list, tuple]: + sigma = np.array(sigma, float) if (sigma > 0).any(): sigma = list(sigma) + [0] image = ndimage.gaussian_filter(image, sigma) diff --git a/skimage/segmentation/tests/test_slic.py b/skimage/segmentation/tests/test_slic.py index 6d00716f..2a190de8 100644 --- a/skimage/segmentation/tests/test_slic.py +++ b/skimage/segmentation/tests/test_slic.py @@ -90,6 +90,17 @@ def test_gray_3d(): assert_array_equal(seg[s], c) +def test_list_sigma(): + rnd = np.random.RandomState(0) + img = np.array([[1, 1, 1, 0, 0, 0], + [0, 0, 0, 1, 1, 1]], np.float) + img += 0.1 * rnd.normal(size=img.shape) + result_sigma = np.array([[0, 0, 0, 1, 1, 1], + [0, 0, 0, 1, 1, 1]], np.int) + seg_sigma = slic(img, n_segments=2, sigma=[1, 50, 1], multichannel=False) + assert_equal(seg_sigma, result_sigma) + + if __name__ == '__main__': from numpy import testing testing.run_module_suite() From 846765e5f9e35cd9be24d0a391a4fde106ba6b9e Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Mon, 16 Sep 2013 17:52:25 +1000 Subject: [PATCH 593/736] Add spacing support for new, speeded-up SLIC --- skimage/segmentation/_slic.pyx | 15 +++++++++++---- skimage/segmentation/slic_superpixels.py | 12 ++++++++++-- skimage/segmentation/tests/test_slic.py | 18 ++++++++++++++++++ 3 files changed, 39 insertions(+), 6 deletions(-) diff --git a/skimage/segmentation/_slic.pyx b/skimage/segmentation/_slic.pyx index d818a501..57c9990a 100644 --- a/skimage/segmentation/_slic.pyx +++ b/skimage/segmentation/_slic.pyx @@ -12,7 +12,8 @@ from skimage.util import regular_grid def _slic_cython(double[:, :, :, ::1] image_zyx, double[:, ::1] segments, - Py_ssize_t max_iter): + Py_ssize_t max_iter, + double[:] spacing): """Helper function for SLIC segmentation. Parameters @@ -23,6 +24,7 @@ def _slic_cython(double[:, :, :, ::1] image_zyx, The initial centroids obtained by SLIC as [Z, Y, X, C...]. max_iter : int The maximum number of k-means iterations. + spacing : 1D array of double, shape (3,) Returns ------- @@ -55,6 +57,11 @@ def _slic_cython(double[:, :, :, ::1] image_zyx, cdef char change cdef double dist_center, cx, cy, cz, dy, dz + cdef double sz, sy, sx + sz = spacing[0] + sy = spacing[1] + sx = spacing[2] + for i in range(max_iter): change = 0 distance[:, :, :] = DBL_MAX @@ -76,11 +83,11 @@ def _slic_cython(double[:, :, :, ::1] image_zyx, x_max = min(cx + 2 * step_x + 1, width) for z in range(z_min, z_max): - dz = (cz - z) ** 2 + dz = (sz * (cz - z)) ** 2 for y in range(y_min, y_max): - dy = (cy - y) ** 2 + dy = (sy * (cy - y)) ** 2 for x in range(x_min, x_max): - dist_center = dz + dy + (cx - x) ** 2 + dist_center = dz + dy + (sx * (cx - x)) ** 2 for c in range(3, n_features): dist_center += (image_zyx[z, y, x, c - 3] - segments[k, c]) ** 2 diff --git a/skimage/segmentation/slic_superpixels.py b/skimage/segmentation/slic_superpixels.py index be07fafe..e9ae8ee7 100644 --- a/skimage/segmentation/slic_superpixels.py +++ b/skimage/segmentation/slic_superpixels.py @@ -11,7 +11,7 @@ from skimage.color import rgb2lab def slic(image, n_segments=100, compactness=10., max_iter=10, sigma=None, - multichannel=True, convert2lab=True, ratio=None): + spacing=None, multichannel=True, convert2lab=True, ratio=None): """Segments image using k-means clustering in Color-(x,y,z) space. Parameters @@ -31,6 +31,9 @@ def slic(image, n_segments=100, compactness=10., max_iter=10, sigma=None, Width of Gaussian smoothing kernel for pre-processing for each dimension of the image. The same sigma is applied to each dimension in case of a scalar value. Zero means no smoothing. + spacing : (3,) array-like of floats, optional + The voxel spacing along each image dimension. By default, `slic` + assumes uniform spacing (same voxel resolution along z, y and x). multichannel : bool, optional Whether the last axis of the image is to be interpreted as multiple channels or another spatial dimension. @@ -103,11 +106,16 @@ def slic(image, n_segments=100, compactness=10., max_iter=10, sigma=None, # Add channel as single last dimension image = image[..., np.newaxis] + if spacing is None: + spacing = np.ones(3) + elif type(spacing) in [list, tuple]: + spacing = np.array(spacing, float) if not isinstance(sigma, coll.Iterable): sigma = np.array([sigma, sigma, sigma], float) elif type(sigma) in [list, tuple]: sigma = np.array(sigma, float) if (sigma > 0).any(): + sigma /= spacing.astype(float) sigma = list(sigma) + [0] image = ndimage.gaussian_filter(image, sigma) @@ -139,7 +147,7 @@ def slic(image, n_segments=100, compactness=10., max_iter=10, sigma=None, ratio = float(max((step_z, step_y, step_x))) / compactness image = np.ascontiguousarray(image * ratio) - labels = _slic_cython(image, segments, max_iter) + labels = _slic_cython(image, segments, max_iter, spacing) if is2d: labels = labels[0] diff --git a/skimage/segmentation/tests/test_slic.py b/skimage/segmentation/tests/test_slic.py index 2a190de8..a4657785 100644 --- a/skimage/segmentation/tests/test_slic.py +++ b/skimage/segmentation/tests/test_slic.py @@ -101,6 +101,24 @@ def test_list_sigma(): assert_equal(seg_sigma, result_sigma) +def test_spacing(): + rnd = np.random.RandomState(0) + img = np.array([[1, 1, 1, 0, 0], + [1, 1, 0, 0, 0]], np.float) + result_non_spaced = np.array([[0, 0, 0, 1, 1], + [0, 0, 1, 1, 1]], np.int) + result_spaced = np.array([[0, 0, 0, 0, 0], + [1, 1, 1, 1, 1]], np.int) + img += 0.1 * rnd.normal(size=img.shape) + seg_non_spaced = slic(img, n_segments=2, sigma=0, multichannel=False, + compactness=1.0) + seg_spaced = slic(img, n_segments=2, sigma=0, spacing=[1, 500, 1], + compactness=1.0, multichannel=False) + assert_equal(seg_non_spaced, result_non_spaced) + assert_equal(seg_spaced, result_spaced) + + + if __name__ == '__main__': from numpy import testing testing.run_module_suite() From 00e5ff263bc1f32b41f02f9ca8dbafbb05459668 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Mon, 16 Sep 2013 21:26:44 +1000 Subject: [PATCH 594/736] Add `spacing` descriptions; use np.double Implement @ahojnnes's comments on pull request. Use `np.double` as dtype for arrays because in Cython, `float` is not `np.double`. And add further clarification about the `spacing` parameter. --- skimage/segmentation/_slic.pyx | 3 +++ skimage/segmentation/slic_superpixels.py | 19 +++++++++++++------ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/skimage/segmentation/_slic.pyx b/skimage/segmentation/_slic.pyx index 57c9990a..3247dd79 100644 --- a/skimage/segmentation/_slic.pyx +++ b/skimage/segmentation/_slic.pyx @@ -25,6 +25,9 @@ def _slic_cython(double[:, :, :, ::1] image_zyx, max_iter : int The maximum number of k-means iterations. spacing : 1D array of double, shape (3,) + The voxel spacing along each image dimension. This parameter + controls the weights of the distances along z, y, and x during + k-means clustering. Returns ------- diff --git a/skimage/segmentation/slic_superpixels.py b/skimage/segmentation/slic_superpixels.py index e9ae8ee7..3cb67874 100644 --- a/skimage/segmentation/slic_superpixels.py +++ b/skimage/segmentation/slic_superpixels.py @@ -34,6 +34,8 @@ def slic(image, n_segments=100, compactness=10., max_iter=10, sigma=None, spacing : (3,) array-like of floats, optional The voxel spacing along each image dimension. By default, `slic` assumes uniform spacing (same voxel resolution along z, y and x). + This parameter controls the weights of the distances along z, y, + and x during k-means clustering. multichannel : bool, optional Whether the last axis of the image is to be interpreted as multiple channels or another spatial dimension. @@ -61,6 +63,11 @@ def slic(image, n_segments=100, compactness=10., max_iter=10, sigma=None, If `sigma > 0`, the image is smoothed using a Gaussian kernel prior to segmentation. + If `sigma > 0` and `spacing` is provided, the kernel width is divided + along each dimension by the spacing. For example, if `sigma=1` and + `spacing=[5, 1, 1]`, the effective `sigma` is `[0.2, 1, 1]`. This + ensures sensible smoothing for anisotropic images. + The image is rescaled to be in [0, 1] prior to processing. Images of shape (M, N, 3) are interpreted as 2D RGB images by default. To @@ -108,14 +115,14 @@ def slic(image, n_segments=100, compactness=10., max_iter=10, sigma=None, if spacing is None: spacing = np.ones(3) - elif type(spacing) in [list, tuple]: - spacing = np.array(spacing, float) + elif isinstance(spacing, (list, tuple)): + spacing = np.array(spacing, np.double) if not isinstance(sigma, coll.Iterable): - sigma = np.array([sigma, sigma, sigma], float) - elif type(sigma) in [list, tuple]: - sigma = np.array(sigma, float) + sigma = np.array([sigma, sigma, sigma], np.double) + elif isinstance(spacing, (list, tuple)): + sigma = np.array(sigma, np.double) if (sigma > 0).any(): - sigma /= spacing.astype(float) + sigma /= spacing.astype(np.double) sigma = list(sigma) + [0] image = ndimage.gaussian_filter(image, sigma) From 05fbc3fbfcc00157841c18b15b171d6477bfb5da Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Mon, 16 Sep 2013 22:42:48 +1000 Subject: [PATCH 595/736] Bug fix: typo: wrote spacing instead of sigma --- skimage/segmentation/slic_superpixels.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/segmentation/slic_superpixels.py b/skimage/segmentation/slic_superpixels.py index 3cb67874..422dff98 100644 --- a/skimage/segmentation/slic_superpixels.py +++ b/skimage/segmentation/slic_superpixels.py @@ -119,7 +119,7 @@ def slic(image, n_segments=100, compactness=10., max_iter=10, sigma=None, spacing = np.array(spacing, np.double) if not isinstance(sigma, coll.Iterable): sigma = np.array([sigma, sigma, sigma], np.double) - elif isinstance(spacing, (list, tuple)): + elif isinstance(sigma, (list, tuple)): sigma = np.array(sigma, np.double) if (sigma > 0).any(): sigma /= spacing.astype(np.double) From 0ed1e22034d6665890b482db1ede65ed4401d678 Mon Sep 17 00:00:00 2001 From: Ankit Agrawal Date: Thu, 19 Sep 2013 10:23:03 +0530 Subject: [PATCH 596/736] Correcting bug in SimialarityTransform --- skimage/transform/_geometric.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index 4153b5bd..6117008a 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -515,7 +515,7 @@ class SimilarityTransform(ProjectiveTransform): [math.sin(rotation), math.cos(rotation), 0], [ 0, 0, 1] ]) - self._matrix *= scale + self._matrix[0:2, 0:2] *= scale self._matrix[0:2, 2] = translation else: # default to an identity transform From a086f03f39187858bbd65389a2a4b2e302d37e7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Thu, 19 Sep 2013 08:36:56 +0200 Subject: [PATCH 597/736] Document default value API change --- doc/source/api_changes.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/source/api_changes.txt b/doc/source/api_changes.txt index c363288d..bf9ddb60 100644 --- a/doc/source/api_changes.txt +++ b/doc/source/api_changes.txt @@ -1,6 +1,8 @@ Version 0.9 ----------- -- No longer wrap ``imread`` output in an ``Image`` class. +- No longer wrap ``imread`` output in an ``Image`` class +- Change default value of `sigma` parameter in ``skimage.segmentation.slic`` + to 0 Version 0.4 ----------- From e17e7699b328c2489b4aea0a8aacd312b5e90a12 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Sat, 21 Sep 2013 00:12:12 +1000 Subject: [PATCH 598/736] Add note about (z, y, x) order in slic cython doc --- skimage/segmentation/_slic.pyx | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/skimage/segmentation/_slic.pyx b/skimage/segmentation/_slic.pyx index 3247dd79..36cfa1a5 100644 --- a/skimage/segmentation/_slic.pyx +++ b/skimage/segmentation/_slic.pyx @@ -33,6 +33,27 @@ def _slic_cython(double[:, :, :, ::1] image_zyx, ------- nearest_segments : 3D array of int, shape (Z, Y, X) The label field/superpixels found by SLIC. + + Notes + ----- + The image is considered to be in (z, y, x) order, which can be + surprising. More commonly, the order (x, y, z) is used. However, + in 3D image analysis, 'z' is usually the "special" dimension, with, + for example, a different effective resolution than the other two + axes. Therefore, x and y are often processed together, or viewed as + a cut-plane through the volume. So, if the order was (x, y, z) and + we wanted to look at the 5th cut plane, we would write:: + + my_z_plane = img3d[:, :, 5] + + but, assuming a C-contiguous array, this would grab a discontiguous + slice of memory, which is bad for performance. In contrast, if we + see the image as (z, y, x) ordered, we would do:: + + my_z_plane = img3d[5] + + and get back a contiguous block of memory. This is better both for + performance and for readability. """ # initialize on grid From c540ef11c9efcfe7c2376e5df774a77f3d6d5d26 Mon Sep 17 00:00:00 2001 From: Riaan van den Dool Date: Fri, 20 Sep 2013 19:14:10 +0200 Subject: [PATCH 599/736] Forgotten contribution by riaanvddool: skimage.io plugin - GDAL --- CONTRIBUTORS.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 33f763ba..689f552b 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -151,3 +151,6 @@ - Eugene Dvoretsky Yen threshold implementation. + +- Riaan van den Dool + skimage.io plugin: GDAL From 54530d16f6740bc486d288799ef1902803e098a8 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Sun, 22 Sep 2013 13:37:04 +1000 Subject: [PATCH 600/736] Update snippets in notes to use double backticks --- skimage/segmentation/_join.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skimage/segmentation/_join.py b/skimage/segmentation/_join.py index 4afc3a66..b726c81e 100644 --- a/skimage/segmentation/_join.py +++ b/skimage/segmentation/_join.py @@ -72,8 +72,8 @@ def relabel_from_one(label_field): ----- The forward map can be extremely big for some inputs, since its length is given by the maximum of the label field. However, in most - situations, `label_field.max()` is much smaller than - `label_field.size`, and in these cases the forward map is + situations, ``label_field.max()`` is much smaller than + ``label_field.size``, and in these cases the forward map is guaranteed to be smaller than either the input or output images. Examples From 845d083470c065cfb1316e788ebb160abd222e12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 22 Sep 2013 07:22:09 +0200 Subject: [PATCH 601/736] Define 1d arrays as contiguous --- skimage/segmentation/_slic.pyx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skimage/segmentation/_slic.pyx b/skimage/segmentation/_slic.pyx index 36cfa1a5..c3d95ee0 100644 --- a/skimage/segmentation/_slic.pyx +++ b/skimage/segmentation/_slic.pyx @@ -13,7 +13,7 @@ from skimage.util import regular_grid def _slic_cython(double[:, :, :, ::1] image_zyx, double[:, ::1] segments, Py_ssize_t max_iter, - double[:] spacing): + double[::1] spacing): """Helper function for SLIC segmentation. Parameters @@ -75,7 +75,7 @@ def _slic_cython(double[:, :, :, ::1] image_zyx, = np.empty((depth, height, width), dtype=np.intp) cdef double[:, :, ::1] distance \ = np.empty((depth, height, width), dtype=np.double) - cdef Py_ssize_t[:] n_segment_elems = np.zeros(n_segments, dtype=np.intp) + cdef Py_ssize_t[::1] n_segment_elems = np.zeros(n_segments, dtype=np.intp) cdef Py_ssize_t i, c, k, x, y, z, x_min, x_max, y_min, y_max, z_min, z_max cdef char change From 8fb6fd02a0975941f888c9271e49f65483298f29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Thu, 26 Sep 2013 16:49:55 +0200 Subject: [PATCH 602/736] Scale sigma only in scalar case --- skimage/segmentation/slic_superpixels.py | 42 +++++++++++++----------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/skimage/segmentation/slic_superpixels.py b/skimage/segmentation/slic_superpixels.py index 422dff98..8276b1a8 100644 --- a/skimage/segmentation/slic_superpixels.py +++ b/skimage/segmentation/slic_superpixels.py @@ -31,6 +31,8 @@ def slic(image, n_segments=100, compactness=10., max_iter=10, sigma=None, Width of Gaussian smoothing kernel for pre-processing for each dimension of the image. The same sigma is applied to each dimension in case of a scalar value. Zero means no smoothing. + Note, that `sigma` is automatically scaled if it is scalar and a + manual voxel spacing is provided (see Notes section). spacing : (3,) array-like of floats, optional The voxel spacing along each image dimension. By default, `slic` assumes uniform spacing (same voxel resolution along z, y and x). @@ -60,19 +62,19 @@ def slic(image, n_segments=100, compactness=10., max_iter=10, sigma=None, Notes ----- - If `sigma > 0`, the image is smoothed using a Gaussian kernel prior to - segmentation. + * If `sigma > 0`, the image is smoothed using a Gaussian kernel prior to + segmentation. - If `sigma > 0` and `spacing` is provided, the kernel width is divided - along each dimension by the spacing. For example, if `sigma=1` and - `spacing=[5, 1, 1]`, the effective `sigma` is `[0.2, 1, 1]`. This - ensures sensible smoothing for anisotropic images. + * If `sigma` is scalar and `spacing` is provided, the kernel width is + divided along each dimension by the spacing. For example, if ``sigma=1`` + and ``spacing=[5, 1, 1]``, the effective `sigma` is ``[0.2, 1, 1]``. This + ensures sensible smoothing for anisotropic images. - The image is rescaled to be in [0, 1] prior to processing. + * The image is rescaled to be in [0, 1] prior to processing. - Images of shape (M, N, 3) are interpreted as 2D RGB images by default. To - interpret them as 3D with the last dimension having length 3, use - `multichannel=False`. + * Images of shape (M, N, 3) are interpreted as 2D RGB images by default. To + interpret them as 3D with the last dimension having length 3, use + `multichannel=False`. References ---------- @@ -100,15 +102,15 @@ def slic(image, n_segments=100, compactness=10., max_iter=10, sigma=None, compactness = ratio image = img_as_float(image) - is2d = False + is_2d = False if image.ndim == 2: # 2D grayscale image image = image[np.newaxis, ..., np.newaxis] - is2d = True + is_2d = True elif image.ndim == 3 and multichannel: # Make 2D multichannel image 3D with depth = 1 image = image[np.newaxis, ...] - is2d = True + is_2d = True elif image.ndim == 3 and not multichannel: # Add channel as single last dimension image = image[..., np.newaxis] @@ -116,13 +118,15 @@ def slic(image, n_segments=100, compactness=10., max_iter=10, sigma=None, if spacing is None: spacing = np.ones(3) elif isinstance(spacing, (list, tuple)): - spacing = np.array(spacing, np.double) + spacing = np.array(spacing, dtype=np.double) + if not isinstance(sigma, coll.Iterable): - sigma = np.array([sigma, sigma, sigma], np.double) - elif isinstance(sigma, (list, tuple)): - sigma = np.array(sigma, np.double) - if (sigma > 0).any(): + sigma = np.array([sigma, sigma, sigma], dtype=np.double) sigma /= spacing.astype(np.double) + elif isinstance(sigma, (list, tuple)): + sigma = np.array(sigma, dtype=np.double) + if (sigma > 0).any(): + # add zero smoothing for multichannel dimension sigma = list(sigma) + [0] image = ndimage.gaussian_filter(image, sigma) @@ -156,7 +160,7 @@ def slic(image, n_segments=100, compactness=10., max_iter=10, sigma=None, labels = _slic_cython(image, segments, max_iter, spacing) - if is2d: + if is_2d: labels = labels[0] return labels From a17a1395c385605fcac506fcd0dc2d0e2c2a09e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sat, 28 Sep 2013 14:23:15 +0200 Subject: [PATCH 603/736] Fix doc string of warp functions --- skimage/transform/_geometric.py | 9 +++++++-- skimage/transform/_warps_cy.pyx | 20 ++++++++------------ 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index 6117008a..23359a93 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -962,8 +962,13 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, Shape of the output image generated. By default the shape of the input image is preserved. order : int, optional - The order of the spline interpolation, default is 3. The order has to - be in the range 0-5. + The order of interpolation. The order has to be in the range 0-5: + * 0: Nearest-neighbor + * 1: Bi-linear (default) + * 2: Bi-quadratic + * 3: Bi-cubic + * 4: Bi-quartic + * 5: Bi-quintic mode : string, optional Points outside the boundaries of the input are filled according to the given mode ('constant', 'nearest', 'reflect' or 'wrap'). diff --git a/skimage/transform/_warps_cy.pyx b/skimage/transform/_warps_cy.pyx index 5dc2ffd0..b3136c22 100644 --- a/skimage/transform/_warps_cy.pyx +++ b/skimage/transform/_warps_cy.pyx @@ -40,22 +40,18 @@ def _warp_fast(cnp.ndarray image, cnp.ndarray H, output_shape=None, """Projective transformation (homography). Perform a projective transformation (homography) of a - floating point image, using bi-linear interpolation. + floating point image, using interpolation. For each pixel, given its homogeneous coordinate :math:`\mathbf{x} = [x, y, 1]^T`, its target position is calculated by multiplying with the given matrix, :math:`H`, to give :math:`H \mathbf{x}`. - E.g., to rotate by theta degrees clockwise, the matrix should be - - :: + E.g., to rotate by theta degrees clockwise, the matrix should be:: [[cos(theta) -sin(theta) 0] [sin(theta) cos(theta) 0] [0 0 1]] - or, to translate x by 10 and y by 20, - - :: + or, to translate x by 10 and y by 20:: [[1 0 10] [0 1 20] @@ -69,12 +65,12 @@ def _warp_fast(cnp.ndarray image, cnp.ndarray H, output_shape=None, Transformation matrix H that defines the homography. output_shape : tuple (rows, cols), optional Shape of the output image generated (default None). - order : {0, 1}, optional + order : {0, 1, 2, 3}, optional Order of interpolation:: - * 0: Nearest-neighbour interpolation. - * 1: Bilinear interpolation (default). - * 2: Biquadratic interpolation. - * 3: Bicubic interpolation. + * 0: Nearest-neighbor + * 1: Bi-linear (default) + * 2: Bi-quadratic + * 3: Bi-cubic mode : {'constant', 'reflect', 'wrap', 'nearest'}, optional How to handle values outside the image borders (default is constant). cval : string, optional (default 0) From c01e7c9b105165967c1c200307011c202659516d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sat, 28 Sep 2013 14:39:40 +0200 Subject: [PATCH 604/736] Fix various doc strings using the warp function --- skimage/transform/_warps.py | 16 +++++++-------- skimage/transform/pyramids.py | 38 +++++++++++++++++------------------ 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/skimage/transform/_warps.py b/skimage/transform/_warps.py index d8da73d5..64b129dd 100644 --- a/skimage/transform/_warps.py +++ b/skimage/transform/_warps.py @@ -32,8 +32,8 @@ def resize(image, output_shape, order=1, mode='constant', cval=0.): Other parameters ---------------- order : int, optional - The order of the spline interpolation, default is 3. The order has to - be in the range 0-5. + The order of the spline interpolation, default is 1. The order has to + be in the range 0-5. See `skimage.transform.warp` for detail. mode : string, optional Points outside the boundaries of the input are filled according to the given mode ('constant', 'nearest', 'reflect' or 'wrap'). @@ -116,8 +116,8 @@ def rescale(image, scale, order=1, mode='constant', cval=0.): Other parameters ---------------- order : int, optional - The order of the spline interpolation, default is 3. The order has to - be in the range 0-5. + The order of the spline interpolation, default is 1. The order has to + be in the range 0-5. See `skimage.transform.warp` for detail. mode : string, optional Points outside the boundaries of the input are filled according to the given mode ('constant', 'nearest', 'reflect' or 'wrap'). @@ -172,8 +172,8 @@ def rotate(image, angle, resize=False, order=1, mode='constant', cval=0.): Other parameters ---------------- order : int, optional - The order of the spline interpolation, default is 3. The order has to - be in the range 0-5. + The order of the spline interpolation, default is 1. The order has to + be in the range 0-5. See `skimage.transform.warp` for detail. mode : string, optional Points outside the boundaries of the input are filled according to the given mode ('constant', 'nearest', 'reflect' or 'wrap'). @@ -315,8 +315,8 @@ def swirl(image, center=None, strength=1, radius=100, rotation=0, Shape of the output image generated. By default the shape of the input image is preserved. order : int, optional - The order of the spline interpolation, default is 3. The order has to - be in the range 0-5. + The order of the spline interpolation, default is 1. The order has to + be in the range 0-5. See `skimage.transform.warp` for detail. mode : string, optional Points outside the boundaries of the input are filled according to the given mode ('constant', 'nearest', 'reflect' or 'wrap'). diff --git a/skimage/transform/pyramids.py b/skimage/transform/pyramids.py index 19001de8..a68b7a70 100644 --- a/skimage/transform/pyramids.py +++ b/skimage/transform/pyramids.py @@ -6,11 +6,11 @@ from skimage.util import img_as_float def _smooth(image, sigma, mode, cval): - """Return image with each channel smoothed by the gaussian filter.""" + """Return image with each channel smoothed by the Gaussian filter.""" smoothed = np.empty(image.shape, dtype=np.double) - if image.ndim == 3: # apply gaussian filter to all dimensions independently + if image.ndim == 3: # apply Gaussian filter to all dimensions independently for dim in range(image.shape[2]): ndimage.gaussian_filter(image[..., dim], sigma, output=smoothed[..., dim], @@ -38,13 +38,13 @@ def pyramid_reduce(image, downscale=2, sigma=None, order=1, downscale : float, optional Downscale factor. sigma : float, optional - Sigma for gaussian filter. Default is `2 * downscale / 6.0` which + Sigma for Gaussian filter. Default is `2 * downscale / 6.0` which corresponds to a filter mask twice the size of the scale factor that - covers more than 99% of the gaussian distribution. + covers more than 99% of the Gaussian distribution. order : int, optional Order of splines used in interpolation of downsampling. See - `scipy.ndimage.map_coordinates` for detail. - mode : {'reflect', 'constant', 'nearest', 'mirror', 'wrap'}, optional + `skimage.transform.warp` for detail. + mode : {'reflect', 'constant', 'nearest', 'mirror', 'wrap'}, optional The mode parameter determines how the array borders are handled, where cval is the value when mode is equal to 'constant'. cval : float, optional @@ -92,13 +92,13 @@ def pyramid_expand(image, upscale=2, sigma=None, order=1, upscale : float, optional Upscale factor. sigma : float, optional - Sigma for gaussian filter. Default is `2 * upscale / 6.0` which + Sigma for Gaussian filter. Default is `2 * upscale / 6.0` which corresponds to a filter mask twice the size of the scale factor that - covers more than 99% of the gaussian distribution. + covers more than 99% of the Gaussian distribution. order : int, optional Order of splines used in interpolation of upsampling. See - `scipy.ndimage.map_coordinates` for detail. - mode : {'reflect', 'constant', 'nearest', 'mirror', 'wrap'}, optional + `skimage.transform.warp` for detail. + mode : {'reflect', 'constant', 'nearest', 'mirror', 'wrap'}, optional The mode parameter determines how the array borders are handled, where cval is the value when mode is equal to 'constant'. cval : float, optional @@ -137,7 +137,7 @@ def pyramid_expand(image, upscale=2, sigma=None, order=1, def pyramid_gaussian(image, max_layer=-1, downscale=2, sigma=None, order=1, mode='reflect', cval=0): - """Yield images of the gaussian pyramid formed by the input image. + """Yield images of the Gaussian pyramid formed by the input image. Recursively applies the `pyramid_reduce` function to the image, and yields the downscaled images. @@ -157,13 +157,13 @@ def pyramid_gaussian(image, max_layer=-1, downscale=2, sigma=None, order=1, downscale : float, optional Downscale factor. sigma : float, optional - Sigma for gaussian filter. Default is `2 * downscale / 6.0` which + Sigma for Gaussian filter. Default is `2 * downscale / 6.0` which corresponds to a filter mask twice the size of the scale factor that - covers more than 99% of the gaussian distribution. + covers more than 99% of the Gaussian distribution. order : int, optional Order of splines used in interpolation of downsampling. See - `scipy.ndimage.map_coordinates` for detail. - mode : {'reflect', 'constant', 'nearest', 'mirror', 'wrap'}, optional + `skimage.transform.warp` for detail. + mode : {'reflect', 'constant', 'nearest', 'mirror', 'wrap'}, optional The mode parameter determines how the array borders are handled, where cval is the value when mode is equal to 'constant'. cval : float, optional @@ -238,13 +238,13 @@ def pyramid_laplacian(image, max_layer=-1, downscale=2, sigma=None, order=1, downscale : float, optional Downscale factor. sigma : float, optional - Sigma for gaussian filter. Default is `2 * downscale / 6.0` which + Sigma for Gaussian filter. Default is `2 * downscale / 6.0` which corresponds to a filter mask twice the size of the scale factor that - covers more than 99% of the gaussian distribution. + covers more than 99% of the Gaussian distribution. order : int, optional Order of splines used in interpolation of downsampling. See - `scipy.ndimage.map_coordinates` for detail. - mode : {'reflect', 'constant', 'nearest', 'mirror', 'wrap'}, optional + `skimage.transform.warp` for detail. + mode : {'reflect', 'constant', 'nearest', 'mirror', 'wrap'}, optional The mode parameter determines how the array borders are handled, where cval is the value when mode is equal to 'constant'. cval : float, optional From f81fbf5b25bc44af191981115f218fd70b0d141a Mon Sep 17 00:00:00 2001 From: Luis Pedro Coelho Date: Tue, 1 Oct 2013 13:07:17 +0200 Subject: [PATCH 605/736] ENH Faster perimeter computation Measured a 30% speedup on a 640x640 image. Fundamentally, this makes fewer passes over the image; so it should never be worse that the previous implementation. --- skimage/measure/_regionprops.py | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/skimage/measure/_regionprops.py b/skimage/measure/_regionprops.py index 0cc56635..d383faf1 100644 --- a/skimage/measure/_regionprops.py +++ b/skimage/measure/_regionprops.py @@ -513,28 +513,40 @@ def perimeter(image, neighbourhood=4): a Perimeter Estimator. The Queen's University of Belfast. http://www.cs.qub.ac.uk/~d.crookes/webpubs/papers/perimeter.doc """ + from skimage.exposure import histogram if neighbourhood == 4: strel = STREL_4 else: strel = STREL_8 + image = image.astype(np.uint8) eroded_image = ndimage.binary_erosion(image, strel, border_value=0) border_image = image - eroded_image # perimeter contribution: corresponding values in convolved image - perimeter_weights = { + perimeter_weights_matrix = { 1: (5, 7, 15, 17, 25, 27), sqrt(2): (21, 33), (1 + sqrt(2)) / 2: (13, 23) } + + # specifying the perimeter_weights array inline did not make a measurable + # difference in execution time (for a 640x640 image), but made the code + # harder to follow, so we build it everytime: + perimeter_weights = np.zeros(34, float) + for v,indices in perimeter_weights_matrix.items(): + for i in indices: perimeter_weights[i] = v + perimeter_image = ndimage.convolve(border_image, np.array([[10, 2, 10], [ 2, 1, 2], [10, 2, 10]]), mode='constant', cval=0) - total_perimeter = 0 - for weight, values in perimeter_weights.items(): - num_values = 0 - for value in values: - num_values += np.sum(perimeter_image == value) - total_perimeter += num_values * weight + # You can also write + # return perimeter_weights[perimeter_image].sum() + # but that was measured as taking much longer than histogram + np.dot (5x + # as much time) + + perimeter_histogram,_ = histogram(perimeter_image, nbins=50) + size = min(len(perimeter_histogram), len(perimeter_weights)) + total_perimeter = np.dot(perimeter_histogram[:size], perimeter_weights[:size]) return total_perimeter From ff0b315110f232df78777bd84ac78642bc18f264 Mon Sep 17 00:00:00 2001 From: Luis Pedro Coelho Date: Tue, 1 Oct 2013 15:19:42 +0200 Subject: [PATCH 606/736] RFCT Simpler code for initializing array Also, replaced skimage.measures.histogram by np.bincount. Both changes were suggested in the pull request discussion by Juan Nunez-Iglesias (jni): https://github.com/scikit-image/scikit-image/pull/746 --- skimage/measure/_regionprops.py | 25 +++++++------------------ 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/skimage/measure/_regionprops.py b/skimage/measure/_regionprops.py index d383faf1..0156a6bc 100644 --- a/skimage/measure/_regionprops.py +++ b/skimage/measure/_regionprops.py @@ -513,7 +513,6 @@ def perimeter(image, neighbourhood=4): a Perimeter Estimator. The Queen's University of Belfast. http://www.cs.qub.ac.uk/~d.crookes/webpubs/papers/perimeter.doc """ - from skimage.exposure import histogram if neighbourhood == 4: strel = STREL_4 else: @@ -522,19 +521,11 @@ def perimeter(image, neighbourhood=4): eroded_image = ndimage.binary_erosion(image, strel, border_value=0) border_image = image - eroded_image - # perimeter contribution: corresponding values in convolved image - perimeter_weights_matrix = { - 1: (5, 7, 15, 17, 25, 27), - sqrt(2): (21, 33), - (1 + sqrt(2)) / 2: (13, 23) - } + perimeter_weights = np.zeros(50, float) + perimeter_weights[[5, 7, 15, 17, 25, 27]] = 1 + perimeter_weights[[21, 33]] = sqrt(2) + perimeter_weights[[13, 23]] = (1 + sqrt(2)) / 2 - # specifying the perimeter_weights array inline did not make a measurable - # difference in execution time (for a 640x640 image), but made the code - # harder to follow, so we build it everytime: - perimeter_weights = np.zeros(34, float) - for v,indices in perimeter_weights_matrix.items(): - for i in indices: perimeter_weights[i] = v perimeter_image = ndimage.convolve(border_image, np.array([[10, 2, 10], [ 2, 1, 2], @@ -543,10 +534,8 @@ def perimeter(image, neighbourhood=4): # You can also write # return perimeter_weights[perimeter_image].sum() - # but that was measured as taking much longer than histogram + np.dot (5x + # but that was measured as taking much longer than bincount + np.dot (5x # as much time) - - perimeter_histogram,_ = histogram(perimeter_image, nbins=50) - size = min(len(perimeter_histogram), len(perimeter_weights)) - total_perimeter = np.dot(perimeter_histogram[:size], perimeter_weights[:size]) + perimeter_histogram = np.bincount(perimeter_image.ravel(), minlength=50) + total_perimeter = np.dot(perimeter_histogram, perimeter_weights) return total_perimeter From 99fb8b992e9635fb596754c5b63da382b069a3c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Wed, 2 Oct 2013 13:55:52 +0200 Subject: [PATCH 607/736] Some improvements for perimeter --- skimage/measure/_regionprops.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/skimage/measure/_regionprops.py b/skimage/measure/_regionprops.py index 0156a6bc..e89d33e0 100644 --- a/skimage/measure/_regionprops.py +++ b/skimage/measure/_regionprops.py @@ -13,8 +13,8 @@ __all__ = ['regionprops'] STREL_4 = np.array([[0, 1, 0], [1, 1, 1], - [0, 1, 0]]) -STREL_8 = np.ones((3, 3), 'int8') + [0, 1, 0]], dtype=np.uint8) +STREL_8 = np.ones((3, 3), dtype=np.uint8) PROPS = { 'Area': 'area', 'BoundingBox': 'bbox', @@ -498,14 +498,14 @@ def perimeter(image, neighbourhood=4): Parameters ---------- image : array - binary image + Binary image. neighbourhood : 4 or 8, optional - neighbourhood connectivity for border pixel determination, default 4 + Neighborhood connectivity for border pixel determination. Returns ------- perimeter : float - total perimeter of all objects in binary image + Total perimeter of all objects in binary image. References ---------- @@ -521,7 +521,7 @@ def perimeter(image, neighbourhood=4): eroded_image = ndimage.binary_erosion(image, strel, border_value=0) border_image = image - eroded_image - perimeter_weights = np.zeros(50, float) + perimeter_weights = np.zeros(50, dtype=np.float32) perimeter_weights[[5, 7, 15, 17, 25, 27]] = 1 perimeter_weights[[21, 33]] = sqrt(2) perimeter_weights[[13, 23]] = (1 + sqrt(2)) / 2 From c1ea012c9b6fddd60093db194fd6f3979970a82b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Wed, 2 Oct 2013 13:56:32 +0200 Subject: [PATCH 608/736] Add support for unsigned integer label images --- skimage/measure/_regionprops.py | 2 -- skimage/measure/tests/test_regionprops.py | 4 +++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/skimage/measure/_regionprops.py b/skimage/measure/_regionprops.py index e89d33e0..93abc816 100644 --- a/skimage/measure/_regionprops.py +++ b/skimage/measure/_regionprops.py @@ -470,8 +470,6 @@ def regionprops(label_image, properties=None, >>> props[0].centroid # centroid of first labelled object >>> props[0]['centroid'] # centroid of first labelled object """ - if not np.issubdtype(label_image.dtype, 'int'): - raise TypeError('Labelled image must be of integer dtype.') if properties is not None: warnings.warn('The ``properties`` argument is deprecated and is ' diff --git a/skimage/measure/tests/test_regionprops.py b/skimage/measure/tests/test_regionprops.py index da6bb421..c21418ab 100644 --- a/skimage/measure/tests/test_regionprops.py +++ b/skimage/measure/tests/test_regionprops.py @@ -28,7 +28,9 @@ def test_all_props(): regions[prop] -def test_unsupported_dtype(): +def test_dtype(): + regionprops(np.zeros((10, 10), dtype=np.int)) + regionprops(np.zeros((10, 10), dtype=np.uint)) assert_raises(TypeError, regionprops, np.zeros((10, 10), dtype=np.double)) From d7824000fe92f50b565a9c6a7d49006814101ac0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Wed, 2 Oct 2013 14:14:29 +0200 Subject: [PATCH 609/736] Raise error for non 2-D images --- skimage/measure/_regionprops.py | 5 +++++ skimage/measure/tests/test_regionprops.py | 7 +++++++ 2 files changed, 12 insertions(+) diff --git a/skimage/measure/_regionprops.py b/skimage/measure/_regionprops.py index 93abc816..884311b1 100644 --- a/skimage/measure/_regionprops.py +++ b/skimage/measure/_regionprops.py @@ -471,6 +471,11 @@ def regionprops(label_image, properties=None, >>> props[0]['centroid'] # centroid of first labelled object """ + label_image = np.squeeze(label_image) + + if label_image.ndim != 2: + raise TypeError('Only 2-D images supported.') + if properties is not None: warnings.warn('The ``properties`` argument is deprecated and is ' 'not needed any more as properties are ' diff --git a/skimage/measure/tests/test_regionprops.py b/skimage/measure/tests/test_regionprops.py index c21418ab..647c09a7 100644 --- a/skimage/measure/tests/test_regionprops.py +++ b/skimage/measure/tests/test_regionprops.py @@ -34,6 +34,13 @@ def test_dtype(): assert_raises(TypeError, regionprops, np.zeros((10, 10), dtype=np.double)) +def test_ndim(): + regionprops(np.zeros((10, 10), dtype=np.int)) + regionprops(np.zeros((10, 10, 1), dtype=np.int)) + regionprops(np.zeros((10, 10, 1, 1), dtype=np.int)) + assert_raises(TypeError, regionprops, np.zeros((10, 10, 2), dtype=np.int)) + + def test_area(): area = regionprops(SAMPLE)[0].area assert area == np.sum(SAMPLE) From 78a849cc37258dcee85d24e31eb9dea2a9851b7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Wed, 2 Oct 2013 14:16:24 +0200 Subject: [PATCH 610/736] Fix dtype bug in perimeter --- skimage/measure/_regionprops.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/measure/_regionprops.py b/skimage/measure/_regionprops.py index 884311b1..9078df6f 100644 --- a/skimage/measure/_regionprops.py +++ b/skimage/measure/_regionprops.py @@ -524,7 +524,7 @@ def perimeter(image, neighbourhood=4): eroded_image = ndimage.binary_erosion(image, strel, border_value=0) border_image = image - eroded_image - perimeter_weights = np.zeros(50, dtype=np.float32) + perimeter_weights = np.zeros(50, dtype=np.double) perimeter_weights[[5, 7, 15, 17, 25, 27]] = 1 perimeter_weights[[21, 33]] = sqrt(2) perimeter_weights[[13, 23]] = (1 + sqrt(2)) / 2 From c414d0efd5440bb575d7b6cbb6bf925c3af664c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Wed, 2 Oct 2013 16:48:27 +0200 Subject: [PATCH 611/736] Hide experimental GSoC functions for 0.9 release --- skimage/feature/__init__.py | 10 ++-------- skimage/feature/_brief.py | 8 ++++++-- skimage/feature/censure.py | 3 ++- .../feature/tests/{test_brief.py => _test_brief.py} | 0 .../tests/{test_censure.py => _test_censure.py} | 0 skimage/feature/util.py | 4 +++- 6 files changed, 13 insertions(+), 12 deletions(-) rename skimage/feature/tests/{test_brief.py => _test_brief.py} (100%) rename skimage/feature/tests/{test_censure.py => _test_censure.py} (100%) diff --git a/skimage/feature/__init__.py b/skimage/feature/__init__.py index bde81e59..4a6518d6 100644 --- a/skimage/feature/__init__.py +++ b/skimage/feature/__init__.py @@ -7,9 +7,7 @@ from .corner import (corner_kitchen_rosenfeld, corner_harris, corner_peaks) from .corner_cy import corner_moravec from .template import match_template -from ._brief import brief, match_keypoints_brief -from .util import pairwise_hamming_distance -from .censure import keypoints_censure + __all__ = ['daisy', 'hog', @@ -24,8 +22,4 @@ __all__ = ['daisy', 'corner_subpix', 'corner_peaks', 'corner_moravec', - 'match_template', - 'brief', - 'pairwise_hamming_distance', - 'match_keypoints_brief', - 'keypoints_censure'] + 'match_template'] diff --git a/skimage/feature/_brief.py b/skimage/feature/_brief.py index 27a8d085..ecc2ec11 100644 --- a/skimage/feature/_brief.py +++ b/skimage/feature/_brief.py @@ -9,7 +9,9 @@ from ._brief_cy import _brief_loop def brief(image, keypoints, descriptor_size=256, mode='normal', patch_size=49, sample_seed=1, variance=2): - """Extract BRIEF Descriptor about given keypoints for a given image. + """**Experimental function**. + + Extract BRIEF Descriptor about given keypoints for a given image. Parameters ---------- @@ -178,7 +180,9 @@ def brief(image, keypoints, descriptor_size=256, mode='normal', patch_size=49, def match_keypoints_brief(keypoints1, descriptors1, keypoints2, descriptors2, threshold=0.15): - """Match keypoints described using BRIEF descriptors in one image to + """**Experimental function**. + + Match keypoints described using BRIEF descriptors in one image to those in second image. Parameters diff --git a/skimage/feature/censure.py b/skimage/feature/censure.py index 00be16a8..4bb7fdda 100644 --- a/skimage/feature/censure.py +++ b/skimage/feature/censure.py @@ -111,7 +111,8 @@ def _suppress_lines(feature_mask, image, sigma, line_threshold): def keypoints_censure(image, min_scale=1, max_scale=7, mode='DoB', non_max_threshold=0.15, line_threshold=10): - """ + """**Experimental function**. + Extracts CenSurE keypoints along with the corresponding scale using either Difference of Boxes, Octagon or STAR bi-level filter. diff --git a/skimage/feature/tests/test_brief.py b/skimage/feature/tests/_test_brief.py similarity index 100% rename from skimage/feature/tests/test_brief.py rename to skimage/feature/tests/_test_brief.py diff --git a/skimage/feature/tests/test_censure.py b/skimage/feature/tests/_test_censure.py similarity index 100% rename from skimage/feature/tests/test_censure.py rename to skimage/feature/tests/_test_censure.py diff --git a/skimage/feature/util.py b/skimage/feature/util.py index eb3817e8..a5267d44 100644 --- a/skimage/feature/util.py +++ b/skimage/feature/util.py @@ -14,7 +14,9 @@ def _mask_border_keypoints(image, keypoints, dist): def pairwise_hamming_distance(array1, array2): - """Calculate hamming dissimilarity measure between two sets of + """**Experimental function**. + + Calculate hamming dissimilarity measure between two sets of vectors. Parameters From c3d40447f80da493e697272197087f3af5e79472 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Wed, 2 Oct 2013 16:58:32 +0200 Subject: [PATCH 612/736] Remove call to deprecated is_gray function --- skimage/color/colorconv.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skimage/color/colorconv.py b/skimage/color/colorconv.py index 5d41a063..7562b650 100644 --- a/skimage/color/colorconv.py +++ b/skimage/color/colorconv.py @@ -684,9 +684,9 @@ def gray2rgb(image): """ if np.squeeze(image).ndim == 3 and image.shape[2] in (3, 4): return image - elif is_gray(image): + elif image.ndim != 1 and np.squeeze(image).ndim in (1, 2, 3): image = image[..., np.newaxis] - return np.concatenate((image,)*3, axis=-1) + return np.concatenate(3 * (image,), axis=-1) else: raise ValueError("Input image expected to be RGB, RGBA or gray.") From 3e3a52331dd974c2026bb1a500e36fe61f2eb0ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Wed, 2 Oct 2013 17:07:23 +0200 Subject: [PATCH 613/736] Misc doc string fixes --- skimage/util/_regular_grid.py | 6 +++--- skimage/util/montage.py | 5 +++-- skimage/util/shape.py | 10 +++++----- skimage/util/unique.py | 4 ++-- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/skimage/util/_regular_grid.py b/skimage/util/_regular_grid.py index e304be20..898a4aed 100644 --- a/skimage/util/_regular_grid.py +++ b/skimage/util/_regular_grid.py @@ -3,7 +3,7 @@ import numpy as np def regular_grid(ar_shape, n_points): """Find `n_points` regularly spaced along `ar_shape`. - + The returned points (as slices) should be as close to cubically-spaced as possible. Essentially, the points are spaced by the Nth root of the input array size, where N is the number of dimensions. However, if an array @@ -13,7 +13,7 @@ def regular_grid(ar_shape, n_points): Parameters ---------- ar_shape : array-like of ints - The shape of the space embedding the grid. `len(ar_shape)` is the + The shape of the space embedding the grid. ``len(ar_shape)`` is the number of dimensions. n_points : int The (approximate) number of points to embed in the space. @@ -66,7 +66,7 @@ def regular_grid(ar_shape, n_points): break starts = stepsizes // 2 stepsizes = np.round(stepsizes) - slices = [slice(start, None, step) for + slices = [slice(start, None, step) for start, step in zip(starts, stepsizes)] slices = [slices[i] for i in unsort_dim_idxs] return slices diff --git a/skimage/util/montage.py b/skimage/util/montage.py index bdafe6ed..805b78a8 100644 --- a/skimage/util/montage.py +++ b/skimage/util/montage.py @@ -10,7 +10,7 @@ def montage2d(arr_in, fill='mean', rescale_intensity=False, grid_shape=None): """Create a 2-dimensional 'montage' from a 3-dimensional input array representing an ensemble of equally shaped 2-dimensional images. - For example, montage2d(arr_in, fill) with the following `arr_in` + For example, ``montage2d(arr_in, fill)`` with the following `arr_in` +---+---+---+ | 1 | 2 | 3 | @@ -37,7 +37,8 @@ def montage2d(arr_in, fill='mean', rescale_intensity=False, grid_shape=None): rescale_intensity: bool, optional Whether to rescale the intensity of each image to [0, 1]. grid_shape: tuple, optional - The desired grid shape for the montage (tiles_y, tiles_x). Tthe default aspect ratio is square. + The desired grid shape for the montage (tiles_y, tiles_x). + The default aspect ratio is square. Returns ------- diff --git a/skimage/util/shape.py b/skimage/util/shape.py index 1d606b42..5fe27a36 100644 --- a/skimage/util/shape.py +++ b/skimage/util/shape.py @@ -208,11 +208,11 @@ def view_as_windows(arr_in, window_shape, step=1): # -- basic checks on arguments if not isinstance(arr_in, np.ndarray): - raise TypeError("'arr_in' must be a numpy ndarray") + raise TypeError("`arr_in` must be a numpy ndarray") if not isinstance(window_shape, tuple): - raise TypeError("'window_shape' must be a tuple") + raise TypeError("`window_shape` must be a tuple") if not (len(window_shape) == arr_in.ndim): - raise ValueError("'window_shape' is incompatible with 'arr_in.shape'") + raise ValueError("`window_shape` is incompatible with `arr_in.shape`") if step < 1: raise ValueError("`step` must be >= 1") @@ -221,10 +221,10 @@ def view_as_windows(arr_in, window_shape, step=1): window_shape = np.array(window_shape, dtype=arr_shape.dtype) if ((arr_shape - window_shape) < 0).any(): - raise ValueError("'window_shape' is too large") + raise ValueError("`window_shape` is too large") if ((window_shape - 1) < 0).any(): - raise ValueError("'window_shape' is too small") + raise ValueError("`window_shape` is too small") # -- build rolling window view arr_in = np.ascontiguousarray(arr_in) diff --git a/skimage/util/unique.py b/skimage/util/unique.py index e65c05ce..16a83f6b 100644 --- a/skimage/util/unique.py +++ b/skimage/util/unique.py @@ -9,12 +9,12 @@ def unique_rows(ar): Parameters ---------- - ar : 2D np.ndarray + ar : 2-D ndarray The input array. Returns ------- - ar_out : 2D np.ndarray + ar_out : 2-D ndarray A copy of the input array with repeated rows removed. Raises From beeb597ddf19d05197e9cd4256a07add55707b14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Wed, 2 Oct 2013 17:10:46 +0200 Subject: [PATCH 614/736] Only clip for ndimage.map_coordinates case --- skimage/transform/_geometric.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index 23359a93..ee1eef1b 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -1052,15 +1052,17 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, out = ndimage.map_coordinates(image, coords, prefilter=prefilter, mode=mode, order=order, cval=cval) - # The spline filters sometimes return results outside [0, 1], - # so clip to ensure valid data - clipped = np.clip(out, 0, 1) + # The spline filters sometimes return results outside [0, 1], + # so clip to ensure valid data + clipped = np.clip(out, 0, 1) - if mode == 'constant' and not (0 <= cval <= 1): - clipped[out == cval] = cval + if mode == 'constant' and not (0 <= cval <= 1): + clipped[out == cval] = cval - if clipped.ndim == 3 and orig_ndim == 2: - # remove singleton dim introduced by atleast_3d - return clipped[..., 0] + out = clipped + + if out.ndim == 3 and orig_ndim == 2: + # remove singleton dimension introduced by atleast_3d + return out[..., 0] else: - return clipped + return out From 9741bfda15d7044c1945d55cfa465d3adcc726a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Wed, 2 Oct 2013 17:13:31 +0200 Subject: [PATCH 615/736] Add note about internal usage of transformation matrix --- skimage/transform/_geometric.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index ee1eef1b..d3fd10ad 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -955,7 +955,7 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, Inverse coordinate map. A function that transforms a (N, 2) array of ``(x, y)`` coordinates in the *output image* into their corresponding coordinates in the *source image* (e.g. a transformation object or its - inverse). + inverse). See example section for usage. map_args : dict, optional Keyword arguments passed to `inverse_map`. output_shape : tuple (rows, cols), optional @@ -976,6 +976,12 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, Used in conjunction with mode 'constant', the value outside the image boundaries. + Notes + ----- + In case of a `SimilarityTransform`, `AffineTransform` and + `ProjectiveTransform` this function uses the underlying transformation + matrix to warp the image with a much faster routine. + Examples -------- Shift an image to the right: @@ -995,6 +1001,7 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, >>> from skimage.transform import SimilarityTransform >>> tform = SimilarityTransform(scale=0.1, rotation=0.1) >>> warp(image, tform) + >>> warp(image, tform.inverse) """ # Backward API compatibility From ed8521175bbdc2d82f782d5aeb78d1b8e6deb556 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Sun, 25 Aug 2013 10:04:20 +0200 Subject: [PATCH 616/736] import Fedor's contribution --- CONTRIBUTORS.txt | 3 ++ doc/examples/plot_shapes.py | 4 ++ skimage/draw/_draw.pyx | 82 +++++++++++++++++++++++++++---------- 3 files changed, 67 insertions(+), 22 deletions(-) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 689f552b..37e619fe 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -154,3 +154,6 @@ - Riaan van den Dool skimage.io plugin: GDAL + +- Fedor Morozov + Drawing: Wu's anti-aliased circle diff --git a/doc/examples/plot_shapes.py b/doc/examples/plot_shapes.py index 507452ef..ec385640 100644 --- a/doc/examples/plot_shapes.py +++ b/doc/examples/plot_shapes.py @@ -48,6 +48,10 @@ img[rr,cc,2] = 255 rr, cc = circle_perimeter(120, 400, 15) img[rr, cc, :] = (255, 0, 0) +# anti-aliased circle +rr, cc, val = circle_perimeter(120, 400, 70, 'wu') +img[rr, cc, 1] = val * 255 + # ellipses rr, cc = ellipse_perimeter(120, 400, 60, 20, orientation=math.pi / 4.) img[rr, cc, :] = (255, 0, 255) diff --git a/skimage/draw/_draw.pyx b/skimage/draw/_draw.pyx index 132ba1d4..e3dab793 100644 --- a/skimage/draw/_draw.pyx +++ b/skimage/draw/_draw.pyx @@ -175,9 +175,10 @@ def circle_perimeter(Py_ssize_t cy, Py_ssize_t cx, Py_ssize_t radius, Centre coordinate of circle. radius: int Radius of circle. - method : {'bresenham', 'andres'}, optional + method : {'bresenham', 'andres', 'wu'}, optional bresenham : Bresenham method (default) andres : Andres method + wu : Wu's method Returns ------- @@ -192,6 +193,8 @@ def circle_perimeter(Py_ssize_t cy, Py_ssize_t cx, Py_ssize_t radius, circles create a disc whereas Bresenham can make holes. There is also less distortions when Andres circles are rotated. Bresenham method is also known as midpoint circle algorithm. + Wu's method draws anti-aliased circle. This implementation doesn't use + lookup table optimization. References ---------- @@ -199,6 +202,7 @@ def circle_perimeter(Py_ssize_t cy, Py_ssize_t cx, Py_ssize_t radius, plotter", 4 (1965) 25-30. .. [2] E. Andres, "Discrete circles, rings and spheres", 18 (1994) 695-706. + .. [3] X. Wu, "Fast anti-aliased circle generation", 2 (1995) 446-450. Examples -------- @@ -222,10 +226,15 @@ def circle_perimeter(Py_ssize_t cy, Py_ssize_t cx, Py_ssize_t radius, cdef list rr = list() cdef list cc = list() + cdef list val = list() cdef Py_ssize_t x = 0 cdef Py_ssize_t y = radius cdef Py_ssize_t d = 0 + + cdef double dceil = 0 + cdef double dceil_prev = 0 + cdef char cmethod if method == 'bresenham': d = 3 - 2 * radius @@ -233,33 +242,62 @@ def circle_perimeter(Py_ssize_t cy, Py_ssize_t cx, Py_ssize_t radius, elif method == 'andres': d = radius - 1 cmethod = 'a' + elif method == 'wu': + cmethod = 'w' else: raise ValueError('Wrong method') - while y >= x: - rr.extend([y, -y, y, -y, x, -x, x, -x]) - cc.extend([x, x, -x, -x, y, y, -y, -y]) + if cmethod == 'a' or cmethod == 'b': + while y >= x: + rr.extend([y, -y, y, -y, x, -x, x, -x]) + cc.extend([x, x, -x, -x, y, y, -y, -y]) - if cmethod == 'b': - if d < 0: - d += 4 * x + 6 - else: - d += 4 * (x - y) + 10 - y -= 1 + if cmethod == 'b': + if d < 0: + d += 4 * x + 6 + else: + d += 4 * (x - y) + 10 + y -= 1 + x += 1 + elif cmethod == 'a': + if d >= 2 * (x - 1): + d = d - 2 * x + x = x + 1 + elif d <= 2 * (radius - y): + d = d + 2 * y - 1 + y = y - 1 + else: + d = d + 2 * (y - x - 1) + y = y - 1 + x = x + 1 + return (np.array(rr, dtype=np.intp) + cy, + np.array(cc, dtype=np.intp) + cx) + + elif cmethod == 'w': + dceil_prev = 0 + + rr.extend([y, x, y, x, -y, -x, -y, -x]) + cc.extend([x, y, -x, -y, x, y, -x, -y]) + val.extend([1] * 8) + + while y > x + 1: x += 1 - elif cmethod == 'a': - if d >= 2 * (x - 1): - d = d - 2 * x - x = x + 1 - elif d <= 2 * (radius - y): - d = d + 2 * y - 1 - y = y - 1 - else: - d = d + 2 * (y - x - 1) - y = y - 1 - x = x + 1 + dceil = math.sqrt(radius**2 - x**2) + dceil = math.ceil(dceil) - dceil + if dceil < dceil_prev: + y -= 1 + rr.extend([y, y - 1, x, x, y, y - 1, x, x]) + cc.extend([x, x, y, y - 1, -x, -x, -y, 1 - y]) - return np.array(rr, dtype=np.intp) + cy, np.array(cc, dtype=np.intp) + cx + rr.extend([-y, 1 - y, -x, -x, -y, 1 - y, -x, -x]) + cc.extend([x, x, y, y - 1, -x, -x, -y, 1 - y]) + + val.extend([1 - dceil, dceil] * 8) + dceil_prev = dceil + + return (np.array(rr, dtype=np.intp) + cy, + np.array(cc, dtype=np.intp) + cx, + np.array(val, dtype=np.float)) def ellipse_perimeter(Py_ssize_t cy, Py_ssize_t cx, Py_ssize_t yradius, From 36e23b106fa9435cf2ea3abea9f1ace1d76d6894 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Sun, 25 Aug 2013 10:31:50 +0200 Subject: [PATCH 617/736] TEST: add unittest and check with travis --- skimage/draw/tests/test_draw.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/skimage/draw/tests/test_draw.py b/skimage/draw/tests/test_draw.py index f15fba01..d3f42c7d 100644 --- a/skimage/draw/tests/test_draw.py +++ b/skimage/draw/tests/test_draw.py @@ -215,6 +215,39 @@ def test_circle_perimeter_andres(): assert_array_equal(img, img_) +def test_circle_perimeter_wu(): + img = np.zeros((15, 15), 'uint8') + rr, cc = circle_perimeter(7, 7, 0, method='wu') + img[rr, cc] = 1 + assert(np.sum(img) == 1) + assert(img[7][7] == 1) + + img = np.zeros((17, 15), 'uint8') + rr, cc = circle_perimeter(7, 7, 7, method='wu') + img[rr, cc] = 1 + print(img) + img_ = np.array( + [[0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0], + [0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0], + [0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0], + [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0], + [1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1], + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], + [1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1], + [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0], + [0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0], + [0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0], + [0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]] + ) + assert_array_equal(img, img_) + + def test_ellipse(): img = np.zeros((15, 15), 'uint8') From 4c14e86952a23119234d7be0778b7b95ea609b17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Sun, 25 Aug 2013 10:51:20 +0200 Subject: [PATCH 618/736] DOC: update return according to wu's method --- skimage/draw/_draw.pyx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/skimage/draw/_draw.pyx b/skimage/draw/_draw.pyx index e3dab793..75c2210c 100644 --- a/skimage/draw/_draw.pyx +++ b/skimage/draw/_draw.pyx @@ -183,10 +183,16 @@ def circle_perimeter(Py_ssize_t cy, Py_ssize_t cx, Py_ssize_t radius, Returns ------- rr, cc : (N,) ndarray of int + Bresenham and Andres' method: Indices of pixels that belong to the circle perimeter. May be used to directly index into an array, e.g. ``img[rr, cc] = 1``. + rr, cc, val : (N,) ndarray of int + Wu' method: + Indices of pixels and intensity values. + ``img[rr, cc] = val``. + Notes ----- Andres method presents the advantage that concentric From 62dbd7b9b9155c7fe63177503ee9b12900666fa2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Sun, 25 Aug 2013 10:51:34 +0200 Subject: [PATCH 619/736] TEST: fix returned tuple --- skimage/draw/tests/test_draw.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/skimage/draw/tests/test_draw.py b/skimage/draw/tests/test_draw.py index d3f42c7d..802bdfc0 100644 --- a/skimage/draw/tests/test_draw.py +++ b/skimage/draw/tests/test_draw.py @@ -217,14 +217,14 @@ def test_circle_perimeter_andres(): def test_circle_perimeter_wu(): img = np.zeros((15, 15), 'uint8') - rr, cc = circle_perimeter(7, 7, 0, method='wu') + rr, cc, val = circle_perimeter(7, 7, 0, method='wu') img[rr, cc] = 1 assert(np.sum(img) == 1) assert(img[7][7] == 1) img = np.zeros((17, 15), 'uint8') - rr, cc = circle_perimeter(7, 7, 7, method='wu') - img[rr, cc] = 1 + rr, cc, val = circle_perimeter(7, 7, 7, method='wu') + img[rr, cc] = val print(img) img_ = np.array( [[0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0], From 62240c1f111afe76ed283c26ef61152747bbf11b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Sun, 25 Aug 2013 11:01:10 +0200 Subject: [PATCH 620/736] TEST: fix unittest --- skimage/draw/tests/test_draw.py | 43 ++++++++++++++++----------------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/skimage/draw/tests/test_draw.py b/skimage/draw/tests/test_draw.py index 802bdfc0..8dac897a 100644 --- a/skimage/draw/tests/test_draw.py +++ b/skimage/draw/tests/test_draw.py @@ -222,29 +222,28 @@ def test_circle_perimeter_wu(): assert(np.sum(img) == 1) assert(img[7][7] == 1) - img = np.zeros((17, 15), 'uint8') - rr, cc, val = circle_perimeter(7, 7, 7, method='wu') - img[rr, cc] = val - print(img) + img = np.zeros((17, 17), 'uint8') + rr, cc, val = circle_perimeter(8, 8, 7, method='wu') + img[rr, cc] = val * 255 img_ = np.array( - [[0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0], - [0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0], - [0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0], - [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0], - [1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1], - [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], - [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], - [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], - [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], - [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], - [1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1], - [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0], - [0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0], - [0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0], - [0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]] - ) + [[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [ 0, 0, 0, 0, 0, 82, 180, 236, 255, 236, 180, 82, 0, 0, 0, 0, 0], + [ 0, 0, 0, 0, 189, 172, 74, 18, 0, 18, 74, 172, 189, 0, 0, 0, 0], + [ 0, 0, 0, 229, 25, 0, 0, 0, 0, 0, 0, 0, 25, 229, 0, 0, 0], + [ 0, 0, 189, 25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 25, 189, 0, 0], + [ 0, 82, 172, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 172, 82, 0], + [ 0, 180, 74, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 74, 180, 0], + [ 0, 236, 18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 236, 0], + [ 0, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0], + [ 0, 236, 18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 236, 0], + [ 0, 180, 74, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 74, 180, 0], + [ 0, 82, 172, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 172, 82, 0], + [ 0, 0, 189, 25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 25, 189, 0, 0], + [ 0, 0, 0, 229, 25, 0, 0, 0, 0, 0, 0, 0, 25, 229, 0, 0, 0], + [ 0, 0, 0, 0, 189, 172, 74, 18, 0, 18, 74, 172, 189, 0, 0, 0, 0], + [ 0, 0, 0, 0, 0, 82, 180, 236, 255, 236, 180, 82, 0, 0, 0, 0, 0], + [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]] + ) assert_array_equal(img, img_) From dcf02235a4907599cbdd1d556d5a12a8cd166fde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Sun, 25 Aug 2013 20:37:13 +0200 Subject: [PATCH 621/736] DOC: fix refs --- skimage/draw/_draw.pyx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/skimage/draw/_draw.pyx b/skimage/draw/_draw.pyx index 75c2210c..90ba001c 100644 --- a/skimage/draw/_draw.pyx +++ b/skimage/draw/_draw.pyx @@ -189,7 +189,7 @@ def circle_perimeter(Py_ssize_t cy, Py_ssize_t cx, Py_ssize_t radius, ``img[rr, cc] = 1``. rr, cc, val : (N,) ndarray of int - Wu' method: + Wu's method: Indices of pixels and intensity values. ``img[rr, cc] = val``. @@ -205,10 +205,11 @@ def circle_perimeter(Py_ssize_t cy, Py_ssize_t cx, Py_ssize_t radius, References ---------- .. [1] J.E. Bresenham, "Algorithm for computer control of a digital - plotter", 4 (1965) 25-30. - .. [2] E. Andres, "Discrete circles, rings and spheres", - 18 (1994) 695-706. - .. [3] X. Wu, "Fast anti-aliased circle generation", 2 (1995) 446-450. + plotter", IBM Systems journal, 4 (1965) 25-30. + .. [2] E. Andres, "Discrete circles, rings and spheres", Computers & + Graphics, 18 (1994) 695-706. + .. [3] X. Wu, "An efficient antialiasing technique", In ACM SIGGRAPH + Computer Graphics, 25 (1991) 143-152. Examples -------- From 260649482835a01a08af0caea1c147f40596e24c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Sun, 25 Aug 2013 21:13:40 +0200 Subject: [PATCH 622/736] MAINT: put AA method in draw.*_aa --- doc/examples/plot_shapes.py | 5 +- skimage/draw/__init__.py | 5 +- skimage/draw/_draw.pyx | 143 +++++++++++++++++++------------- skimage/draw/tests/test_draw.py | 12 +-- 4 files changed, 97 insertions(+), 68 deletions(-) diff --git a/doc/examples/plot_shapes.py b/doc/examples/plot_shapes.py index ec385640..2d957977 100644 --- a/doc/examples/plot_shapes.py +++ b/doc/examples/plot_shapes.py @@ -14,7 +14,8 @@ This example shows how to fill several different shapes: import numpy as np import matplotlib.pyplot as plt -from skimage.draw import line, polygon, circle, circle_perimeter, \ +from skimage.draw import line, polygon, circle, \ + circle_perimeter, circle_perimeter_aa, \ ellipse, ellipse_perimeter import numpy as np import math @@ -49,7 +50,7 @@ rr, cc = circle_perimeter(120, 400, 15) img[rr, cc, :] = (255, 0, 0) # anti-aliased circle -rr, cc, val = circle_perimeter(120, 400, 70, 'wu') +rr, cc, val = circle_perimeter_aa(120, 400, 70) img[rr, cc, 1] = val * 255 # ellipses diff --git a/skimage/draw/__init__.py b/skimage/draw/__init__.py index 4fb222d1..285ba49a 100644 --- a/skimage/draw/__init__.py +++ b/skimage/draw/__init__.py @@ -1,6 +1,6 @@ from .draw import circle, ellipse, set_color -from ._draw import line, polygon, ellipse_perimeter, circle_perimeter, \ - bezier_segment +from ._draw import (line, polygon, ellipse_perimeter, circle_perimeter, + circle_perimeter_aa, bezier_segment) from .draw3d import ellipsoid, ellipsoid_stats __all__ = ['line', @@ -11,4 +11,5 @@ __all__ = ['line', 'ellipsoid_stats', 'circle', 'circle_perimeter', + 'circle_perimeter_aa', 'set_color'] diff --git a/skimage/draw/_draw.pyx b/skimage/draw/_draw.pyx index 90ba001c..f68bb2f2 100644 --- a/skimage/draw/_draw.pyx +++ b/skimage/draw/_draw.pyx @@ -178,7 +178,6 @@ def circle_perimeter(Py_ssize_t cy, Py_ssize_t cx, Py_ssize_t radius, method : {'bresenham', 'andres', 'wu'}, optional bresenham : Bresenham method (default) andres : Andres method - wu : Wu's method Returns ------- @@ -188,19 +187,13 @@ def circle_perimeter(Py_ssize_t cy, Py_ssize_t cx, Py_ssize_t radius, May be used to directly index into an array, e.g. ``img[rr, cc] = 1``. - rr, cc, val : (N,) ndarray of int - Wu's method: - Indices of pixels and intensity values. - ``img[rr, cc] = val``. - Notes ----- Andres method presents the advantage that concentric circles create a disc whereas Bresenham can make holes. There is also less distortions when Andres circles are rotated. Bresenham method is also known as midpoint circle algorithm. - Wu's method draws anti-aliased circle. This implementation doesn't use - lookup table optimization. + Anti-aliased circle generator is available with `circle_perimeter_aa`. References ---------- @@ -208,8 +201,6 @@ def circle_perimeter(Py_ssize_t cy, Py_ssize_t cx, Py_ssize_t radius, plotter", IBM Systems journal, 4 (1965) 25-30. .. [2] E. Andres, "Discrete circles, rings and spheres", Computers & Graphics, 18 (1994) 695-706. - .. [3] X. Wu, "An efficient antialiasing technique", In ACM SIGGRAPH - Computer Graphics, 25 (1991) 143-152. Examples -------- @@ -233,7 +224,6 @@ def circle_perimeter(Py_ssize_t cy, Py_ssize_t cx, Py_ssize_t radius, cdef list rr = list() cdef list cc = list() - cdef list val = list() cdef Py_ssize_t x = 0 cdef Py_ssize_t y = radius @@ -249,62 +239,97 @@ def circle_perimeter(Py_ssize_t cy, Py_ssize_t cx, Py_ssize_t radius, elif method == 'andres': d = radius - 1 cmethod = 'a' - elif method == 'wu': - cmethod = 'w' else: raise ValueError('Wrong method') - if cmethod == 'a' or cmethod == 'b': - while y >= x: - rr.extend([y, -y, y, -y, x, -x, x, -x]) - cc.extend([x, x, -x, -x, y, y, -y, -y]) + while y >= x: + rr.extend([y, -y, y, -y, x, -x, x, -x]) + cc.extend([x, x, -x, -x, y, y, -y, -y]) - if cmethod == 'b': - if d < 0: - d += 4 * x + 6 - else: - d += 4 * (x - y) + 10 - y -= 1 - x += 1 - elif cmethod == 'a': - if d >= 2 * (x - 1): - d = d - 2 * x - x = x + 1 - elif d <= 2 * (radius - y): - d = d + 2 * y - 1 - y = y - 1 - else: - d = d + 2 * (y - x - 1) - y = y - 1 - x = x + 1 - return (np.array(rr, dtype=np.intp) + cy, - np.array(cc, dtype=np.intp) + cx) - - elif cmethod == 'w': - dceil_prev = 0 - - rr.extend([y, x, y, x, -y, -x, -y, -x]) - cc.extend([x, y, -x, -y, x, y, -x, -y]) - val.extend([1] * 8) - - while y > x + 1: - x += 1 - dceil = math.sqrt(radius**2 - x**2) - dceil = math.ceil(dceil) - dceil - if dceil < dceil_prev: + if cmethod == 'b': + if d < 0: + d += 4 * x + 6 + else: + d += 4 * (x - y) + 10 y -= 1 - rr.extend([y, y - 1, x, x, y, y - 1, x, x]) - cc.extend([x, x, y, y - 1, -x, -x, -y, 1 - y]) + x += 1 + elif cmethod == 'a': + if d >= 2 * (x - 1): + d = d - 2 * x + x = x + 1 + elif d <= 2 * (radius - y): + d = d + 2 * y - 1 + y = y - 1 + else: + d = d + 2 * (y - x - 1) + y = y - 1 + x = x + 1 + return (np.array(rr, dtype=np.intp) + cy, + np.array(cc, dtype=np.intp) + cx) - rr.extend([-y, 1 - y, -x, -x, -y, 1 - y, -x, -x]) - cc.extend([x, x, y, y - 1, -x, -x, -y, 1 - y]) - val.extend([1 - dceil, dceil] * 8) - dceil_prev = dceil +def circle_perimeter_aa(Py_ssize_t cy, Py_ssize_t cx, Py_ssize_t radius): + """Generate anti-aliased circle perimeter coordinates. - return (np.array(rr, dtype=np.intp) + cy, - np.array(cc, dtype=np.intp) + cx, - np.array(val, dtype=np.float)) + Parameters + ---------- + cy, cx : int + Centre coordinate of circle. + radius: int + Radius of circle. + + Returns + ------- + rr, cc, val : (N,) ndarray of int + Indices of pixels (`rr`, `cc`) and intensity values (`val`). + ``img[rr, cc] = val``. + + Notes + ----- + Wu's method draws anti-aliased circle. This implementation doesn't use + lookup table optimization. + + References + ---------- + .. [1] X. Wu, "An efficient antialiasing technique", In ACM SIGGRAPH + Computer Graphics, 25 (1991) 143-152. + + """ + + cdef list rr = list() + cdef list cc = list() + cdef list val = list() + + cdef Py_ssize_t x = 0 + cdef Py_ssize_t y = radius + cdef Py_ssize_t d = 0 + + cdef double dceil = 0 + + dceil_prev = 0 + + rr.extend([y, x, y, x, -y, -x, -y, -x]) + cc.extend([x, y, -x, -y, x, y, -x, -y]) + val.extend([1] * 8) + + while y > x + 1: + x += 1 + dceil = math.sqrt(radius**2 - x**2) + dceil = math.ceil(dceil) - dceil + if dceil < dceil_prev: + y -= 1 + rr.extend([y, y - 1, x, x, y, y - 1, x, x]) + cc.extend([x, x, y, y - 1, -x, -x, -y, 1 - y]) + + rr.extend([-y, 1 - y, -x, -x, -y, 1 - y, -x, -x]) + cc.extend([x, x, y, y - 1, -x, -x, -y, 1 - y]) + + val.extend([1 - dceil, dceil] * 8) + dceil_prev = dceil + + return (np.array(rr, dtype=np.intp) + cy, + np.array(cc, dtype=np.intp) + cx, + np.array(val, dtype=np.float)) def ellipse_perimeter(Py_ssize_t cy, Py_ssize_t cx, Py_ssize_t yradius, diff --git a/skimage/draw/tests/test_draw.py b/skimage/draw/tests/test_draw.py index 8dac897a..152797a7 100644 --- a/skimage/draw/tests/test_draw.py +++ b/skimage/draw/tests/test_draw.py @@ -1,8 +1,10 @@ from numpy.testing import assert_array_equal import numpy as np -from skimage.draw import line, polygon, circle, circle_perimeter, \ - ellipse, ellipse_perimeter, bezier_segment +from skimage.draw import (line, polygon, circle, circle_perimeter, + circle_perimeter_aa, ellipse, + ellipse_perimeter, bezier_segment, + ) def test_line_horizontal(): @@ -215,15 +217,15 @@ def test_circle_perimeter_andres(): assert_array_equal(img, img_) -def test_circle_perimeter_wu(): +def test_circle_perimeter_aa(): img = np.zeros((15, 15), 'uint8') - rr, cc, val = circle_perimeter(7, 7, 0, method='wu') + rr, cc, val = circle_perimeter_aa(7, 7, 0) img[rr, cc] = 1 assert(np.sum(img) == 1) assert(img[7][7] == 1) img = np.zeros((17, 17), 'uint8') - rr, cc, val = circle_perimeter(8, 8, 7, method='wu') + rr, cc, val = circle_perimeter_aa(8, 8, 7) img[rr, cc] = val * 255 img_ = np.array( [[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], From bf31517843636074100f0dd65193ce8b855c11d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Tue, 27 Aug 2013 08:05:27 +0200 Subject: [PATCH 623/736] DOC: remove wu in methods --- skimage/draw/_draw.pyx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/draw/_draw.pyx b/skimage/draw/_draw.pyx index f68bb2f2..a8716983 100644 --- a/skimage/draw/_draw.pyx +++ b/skimage/draw/_draw.pyx @@ -175,7 +175,7 @@ def circle_perimeter(Py_ssize_t cy, Py_ssize_t cx, Py_ssize_t radius, Centre coordinate of circle. radius: int Radius of circle. - method : {'bresenham', 'andres', 'wu'}, optional + method : {'bresenham', 'andres'}, optional bresenham : Bresenham method (default) andres : Andres method From 90335efdda23570fdb2a3c6f54b83c526fa5110f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Tue, 27 Aug 2013 16:36:19 +0200 Subject: [PATCH 624/736] Implement line_aa --- skimage/draw/__init__.py | 6 ++- skimage/draw/_draw.pyx | 81 +++++++++++++++++++++++++++++++++ skimage/draw/tests/test_draw.py | 45 ++++++++++++++++-- 3 files changed, 127 insertions(+), 5 deletions(-) diff --git a/skimage/draw/__init__.py b/skimage/draw/__init__.py index 285ba49a..5ca5afb1 100644 --- a/skimage/draw/__init__.py +++ b/skimage/draw/__init__.py @@ -1,9 +1,11 @@ from .draw import circle, ellipse, set_color -from ._draw import (line, polygon, ellipse_perimeter, circle_perimeter, - circle_perimeter_aa, bezier_segment) from .draw3d import ellipsoid, ellipsoid_stats +from ._draw import (line, line_aa, polygon, ellipse_perimeter, + circle_perimeter, circle_perimeter_aa, + bezier_segment) __all__ = ['line', + 'line_aa', 'polygon', 'ellipse', 'ellipse_perimeter', diff --git a/skimage/draw/_draw.pyx b/skimage/draw/_draw.pyx index a8716983..3e835d93 100644 --- a/skimage/draw/_draw.pyx +++ b/skimage/draw/_draw.pyx @@ -27,6 +27,10 @@ def line(Py_ssize_t y, Py_ssize_t x, Py_ssize_t y2, Py_ssize_t x2): May be used to directly index into an array, e.g. ``img[rr, cc] = 1``. + Notes + ----- + Anti-aliased line generator is available with `line_aa`. + Examples -------- >>> from skimage.draw import line @@ -89,6 +93,83 @@ def line(Py_ssize_t y, Py_ssize_t x, Py_ssize_t y2, Py_ssize_t x2): return np.asarray(rr), np.asarray(cc) +def line_aa(Py_ssize_t y1, Py_ssize_t x1, Py_ssize_t y2, Py_ssize_t x2): + """Generate line pixel coordinates. + + Parameters + ---------- + y1, x1 : int + Starting position (row, column). + y2, x2 : int + End position (row, column). + + Returns + ------- + rr, cc, val : (N,) ndarray of int + Indices of pixels that belong to the line. + May be used to directly index into an array, e.g. + ``img[rr, cc] = val``. + + References + ---------- + .. [1] A Rasterizing Algorithm for Drawing Curves, A. Zingl, 2012 + http://members.chello.at/easyfilter/Bresenham.pdf + + """ + cdef list rr = list() + cdef list cc = list() + cdef list val = list() + + cdef int dx = abs(x1 - x2) + cdef int dy = abs(y1 - y2) + cdef int err = dx - dy + cdef int x, y, e, ed, sign_x, sign_y + + if x1 < x2: + sign_x = 1 + else: + sign_x = -1 + + if y1 < y2: + sign_y = 1 + else: + sign_y = -1 + + if dx + dy == 0: + ed = 1 + else: + ed = (sqrt(dx*dx + dy*dy)) + + x, y = x1, y1 + while True: + cc.append(x) + rr.append(y) + val.append(255 * abs(err - dx + dy) / ed) + e = err + if 2 * e >= -dx: + if x == x2: + break + if e + dy < ed: + cc.append(x) + rr.append(y + sign_y) + val.append(255 * abs(e + dy) / ed) + err -= dy + x += sign_x + if 2 * e <= dy: + if y == y2: + break + if dx - e < ed: + cc.append(x) + rr.append(y) + val.append(255 * abs(dx - e) / ed) + err += dx + y += sign_y + + return (np.array(rr, dtype=np.intp), + np.array(cc, dtype=np.intp), + 255 - np.array(val, dtype=np.intp)) + + def polygon(y, x, shape=None): """Generate coordinates of pixels within polygon. diff --git a/skimage/draw/tests/test_draw.py b/skimage/draw/tests/test_draw.py index 152797a7..23bbff4e 100644 --- a/skimage/draw/tests/test_draw.py +++ b/skimage/draw/tests/test_draw.py @@ -1,8 +1,10 @@ -from numpy.testing import assert_array_equal +from numpy.testing import assert_array_equal, assert_equal import numpy as np -from skimage.draw import (line, polygon, circle, circle_perimeter, - circle_perimeter_aa, ellipse, +from skimage.draw import (line, line_aa, + polygon, circle, + circle_perimeter, circle_perimeter_aa, + ellipse, ellipse_perimeter, bezier_segment, ) @@ -54,6 +56,43 @@ def test_line_diag(): assert_array_equal(img, img_) +def test_line_aa_horizontal(): + img = np.zeros((10, 10)) + + rr, cc, val = line_aa(0, 0, 0, 9) + img[rr, cc] = val + + img_ = np.zeros((10, 10)) + img_[0, :] = 255 + + assert_array_equal(img, img_) + + +def test_line_aa_vertical(): + img = np.zeros((10, 10)) + + rr, cc, val = line_aa(0, 0, 9, 0) + img[rr, cc] = val + + img_ = np.zeros((10, 10)) + img_[:, 0] = 255 + + assert_array_equal(img, img_) + + +def test_line_aa_diagonal(): + img = np.zeros((10, 10)) + + rr, cc, val = line_aa(0, 0, 9, 6) + img[rr, cc] = 1 + + # Check that each pixel belonging to line, + # also belongs to line_aa + r, c = line(0, 0, 9, 6) + for x, y in zip(r, c): + assert_equal(img[r, c], 1) + + def test_polygon_rectangle(): img = np.zeros((10, 10), 'uint8') poly = np.array(((1, 1), (4, 1), (4, 4), (1, 4), (1, 1))) From 1a0f13767955039c3432291f3ce28d6b8cd92247 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Tue, 27 Aug 2013 16:37:58 +0200 Subject: [PATCH 625/736] Anti-aliasing --- CONTRIBUTORS.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 37e619fe..f8f78c52 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -132,7 +132,8 @@ Dense DAISY feature description, circle perimeter drawing. - François Boulogne - Drawing: Andres Method for circle perimeter, ellipse perimeter drawing, Bezier curve. + Drawing: Andres Method for circle perimeter, ellipse perimeter drawing, + Bezier curve, anti-aliasing. Circular and elliptical Hough Transforms Various fixes From a4f1704d6e859f4e830fe73169cea540c1a8d02c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Tue, 27 Aug 2013 16:39:55 +0200 Subject: [PATCH 626/736] MAINT: bezier_segment is private --- skimage/draw/__init__.py | 2 +- skimage/draw/_draw.pyx | 12 ++++++------ skimage/draw/tests/test_draw.py | 6 +++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/skimage/draw/__init__.py b/skimage/draw/__init__.py index 5ca5afb1..8e455343 100644 --- a/skimage/draw/__init__.py +++ b/skimage/draw/__init__.py @@ -2,7 +2,7 @@ from .draw import circle, ellipse, set_color from .draw3d import ellipsoid, ellipsoid_stats from ._draw import (line, line_aa, polygon, ellipse_perimeter, circle_perimeter, circle_perimeter_aa, - bezier_segment) + _bezier_segment) __all__ = ['line', 'line_aa', diff --git a/skimage/draw/_draw.pyx b/skimage/draw/_draw.pyx index 3e835d93..dc56b2a7 100644 --- a/skimage/draw/_draw.pyx +++ b/skimage/draw/_draw.pyx @@ -533,23 +533,23 @@ def ellipse_perimeter(Py_ssize_t cy, Py_ssize_t cx, Py_ssize_t yradius, iyd = int(floor(ya * w + 0.5)) # Draw the 4 quadrants - rr, cc = bezier_segment(iy0 + iyd, ix0, iy0, ix0, iy0, ix0 + ixd, 1-w) + rr, cc = _bezier_segment(iy0 + iyd, ix0, iy0, ix0, iy0, ix0 + ixd, 1-w) py.extend(rr) px.extend(cc) - rr, cc = bezier_segment(iy0 + iyd, ix0, iy1, ix0, iy1, ix1 - ixd, w) + rr, cc = _bezier_segment(iy0 + iyd, ix0, iy1, ix0, iy1, ix1 - ixd, w) py.extend(rr) px.extend(cc) - rr, cc = bezier_segment(iy1 - iyd, ix1, iy1, ix1, iy1, ix1 - ixd, 1-w) + rr, cc = _bezier_segment(iy1 - iyd, ix1, iy1, ix1, iy1, ix1 - ixd, 1-w) py.extend(rr) px.extend(cc) - rr, cc = bezier_segment(iy1 - iyd, ix1, iy0, ix1, iy0, ix0 + ixd, w) + rr, cc = _bezier_segment(iy1 - iyd, ix1, iy0, ix1, iy0, ix0 + ixd, w) py.extend(rr) px.extend(cc) return np.array(py, dtype=np.intp), np.array(px, dtype=np.intp) -def bezier_segment(Py_ssize_t y0, Py_ssize_t x0, +def _bezier_segment(Py_ssize_t y0, Py_ssize_t x0, Py_ssize_t y1, Py_ssize_t x1, Py_ssize_t y2, Py_ssize_t x2, double weight): @@ -643,7 +643,7 @@ def bezier_segment(Py_ssize_t y0, Py_ssize_t x0, sy = floor((y0 + 2 * weight * y1 + y2) * xy * 0.5 + 0.5) dx = floor((weight * x1 + x0) * xy + 0.5) dy = floor((y1 * weight + y0) * xy + 0.5) - return bezier_segment(y0, x0, (dy), (dx), + return _bezier_segment(y0, x0, (dy), (dx), (sy), (sx), cur) err = dx + dy - xy diff --git a/skimage/draw/tests/test_draw.py b/skimage/draw/tests/test_draw.py index 23bbff4e..6b49e6a4 100644 --- a/skimage/draw/tests/test_draw.py +++ b/skimage/draw/tests/test_draw.py @@ -5,7 +5,7 @@ from skimage.draw import (line, line_aa, polygon, circle, circle_perimeter, circle_perimeter_aa, ellipse, - ellipse_perimeter, bezier_segment, + ellipse_perimeter, _bezier_segment, ) @@ -433,7 +433,7 @@ def test_bezier_segment_straight(): y1 = 50 x2 = 150 y2 = 150 - rr, cc = bezier_segment(x0, y0, x1, y1, x2, y2, 0) + rr, cc = _bezier_segment(x0, y0, x1, y1, x2, y2, 0) image [rr, cc] = 1 image2 = np.zeros((200, 200), dtype=int) @@ -444,7 +444,7 @@ def test_bezier_segment_straight(): def test_bezier_segment_curved(): img = np.zeros((25, 25), 'uint8') - rr, cc = bezier_segment(20, 20, 20, 2, 2, 2, 1) + rr, cc = _bezier_segment(20, 20, 20, 2, 2, 2, 1) img[rr, cc] = 1 img_ = np.array( [[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], From d998166ede4e5ea55e0f7baa6bc32f7a79341468 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Tue, 27 Aug 2013 16:47:21 +0200 Subject: [PATCH 627/736] MAINT: val in [0,1] --- skimage/draw/_draw.pyx | 12 ++++++------ skimage/draw/tests/test_draw.py | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/skimage/draw/_draw.pyx b/skimage/draw/_draw.pyx index dc56b2a7..a8df35c3 100644 --- a/skimage/draw/_draw.pyx +++ b/skimage/draw/_draw.pyx @@ -105,7 +105,7 @@ def line_aa(Py_ssize_t y1, Py_ssize_t x1, Py_ssize_t y2, Py_ssize_t x2): Returns ------- - rr, cc, val : (N,) ndarray of int + rr, cc, val : (N,) ndarray of (int, int, float) Indices of pixels that belong to the line. May be used to directly index into an array, e.g. ``img[rr, cc] = val``. @@ -144,7 +144,7 @@ def line_aa(Py_ssize_t y1, Py_ssize_t x1, Py_ssize_t y2, Py_ssize_t x2): while True: cc.append(x) rr.append(y) - val.append(255 * abs(err - dx + dy) / ed) + val.append(abs(err - dx + dy) / ed) e = err if 2 * e >= -dx: if x == x2: @@ -152,7 +152,7 @@ def line_aa(Py_ssize_t y1, Py_ssize_t x1, Py_ssize_t y2, Py_ssize_t x2): if e + dy < ed: cc.append(x) rr.append(y + sign_y) - val.append(255 * abs(e + dy) / ed) + val.append(abs(e + dy) / ed) err -= dy x += sign_x if 2 * e <= dy: @@ -161,13 +161,13 @@ def line_aa(Py_ssize_t y1, Py_ssize_t x1, Py_ssize_t y2, Py_ssize_t x2): if dx - e < ed: cc.append(x) rr.append(y) - val.append(255 * abs(dx - e) / ed) + val.append(abs(dx - e) / ed) err += dx y += sign_y return (np.array(rr, dtype=np.intp), np.array(cc, dtype=np.intp), - 255 - np.array(val, dtype=np.intp)) + 1 - np.array(val, dtype=np.float)) def polygon(y, x, shape=None): @@ -361,7 +361,7 @@ def circle_perimeter_aa(Py_ssize_t cy, Py_ssize_t cx, Py_ssize_t radius): Returns ------- - rr, cc, val : (N,) ndarray of int + rr, cc, val : (N,) ndarray (int, int, float) Indices of pixels (`rr`, `cc`) and intensity values (`val`). ``img[rr, cc] = val``. diff --git a/skimage/draw/tests/test_draw.py b/skimage/draw/tests/test_draw.py index 6b49e6a4..4a79e819 100644 --- a/skimage/draw/tests/test_draw.py +++ b/skimage/draw/tests/test_draw.py @@ -63,7 +63,7 @@ def test_line_aa_horizontal(): img[rr, cc] = val img_ = np.zeros((10, 10)) - img_[0, :] = 255 + img_[0, :] = 1 assert_array_equal(img, img_) @@ -75,7 +75,7 @@ def test_line_aa_vertical(): img[rr, cc] = val img_ = np.zeros((10, 10)) - img_[:, 0] = 255 + img_[:, 0] = 1 assert_array_equal(img, img_) From 088b2995a99ae9870e4d9508ca8c11a75be2c627 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Tue, 27 Aug 2013 17:17:13 +0200 Subject: [PATCH 628/736] DOC: split non-AA/AA --- doc/examples/plot_shapes.py | 42 ++++++++++++++++++++++++++++++------- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/doc/examples/plot_shapes.py b/doc/examples/plot_shapes.py index 2d957977..04882068 100644 --- a/doc/examples/plot_shapes.py +++ b/doc/examples/plot_shapes.py @@ -1,9 +1,9 @@ """ -=========== -Fill shapes -=========== +====== +Shapes +====== -This example shows how to fill several different shapes: +This example shows how to draw several different shapes: * line * polygon @@ -49,10 +49,6 @@ img[rr,cc,2] = 255 rr, cc = circle_perimeter(120, 400, 15) img[rr, cc, :] = (255, 0, 0) -# anti-aliased circle -rr, cc, val = circle_perimeter_aa(120, 400, 70) -img[rr, cc, 1] = val * 255 - # ellipses rr, cc = ellipse_perimeter(120, 400, 60, 20, orientation=math.pi / 4.) img[rr, cc, :] = (255, 0, 255) @@ -63,3 +59,33 @@ img[rr, cc, :] = (255, 255, 255) plt.imshow(img) plt.show() + +""" + +Anti-aliasing drawing for: +* line +* circle + +""" + +import numpy as np +import matplotlib.pyplot as plt + +from skimage.draw import line_aa, \ + circle_perimeter_aa +import numpy as np + +img = np.zeros((100, 100), dtype=np.uint8) + +# anti-aliased line +rr, cc, val = line_aa(12, 12, 20, 50) +img[rr, cc] = val * 255 + +# anti-aliased circle +rr, cc, val = circle_perimeter_aa(60, 40, 30) +img[rr, cc] = val * 255 + + +plt.imshow(img, cmap=plt.cm.gray, interpolation='nearest') +plt.title('Anti-aliasing') +plt.show() From 718989edc53eb260f4e433339f517c69b0fc0154 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Tue, 27 Aug 2013 17:17:44 +0200 Subject: [PATCH 629/736] FIX: division for value --- skimage/draw/_draw.pyx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/skimage/draw/_draw.pyx b/skimage/draw/_draw.pyx index a8df35c3..dc1ae305 100644 --- a/skimage/draw/_draw.pyx +++ b/skimage/draw/_draw.pyx @@ -144,7 +144,7 @@ def line_aa(Py_ssize_t y1, Py_ssize_t x1, Py_ssize_t y2, Py_ssize_t x2): while True: cc.append(x) rr.append(y) - val.append(abs(err - dx + dy) / ed) + val.append(1. * abs(err - dx + dy) / (ed)) e = err if 2 * e >= -dx: if x == x2: @@ -152,7 +152,7 @@ def line_aa(Py_ssize_t y1, Py_ssize_t x1, Py_ssize_t y2, Py_ssize_t x2): if e + dy < ed: cc.append(x) rr.append(y + sign_y) - val.append(abs(e + dy) / ed) + val.append(1. * abs(e + dy) / (ed)) err -= dy x += sign_x if 2 * e <= dy: @@ -161,13 +161,13 @@ def line_aa(Py_ssize_t y1, Py_ssize_t x1, Py_ssize_t y2, Py_ssize_t x2): if dx - e < ed: cc.append(x) rr.append(y) - val.append(abs(dx - e) / ed) + val.append(abs(dx - e) / (ed)) err += dx y += sign_y return (np.array(rr, dtype=np.intp), np.array(cc, dtype=np.intp), - 1 - np.array(val, dtype=np.float)) + 1. - np.array(val, dtype=np.float)) def polygon(y, x, shape=None): From 0467b920341e678c995eeb78334a028511598b29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Tue, 27 Aug 2013 17:22:18 +0200 Subject: [PATCH 630/736] PEP8 --- doc/examples/plot_shapes.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/doc/examples/plot_shapes.py b/doc/examples/plot_shapes.py index 04882068..995a278a 100644 --- a/doc/examples/plot_shapes.py +++ b/doc/examples/plot_shapes.py @@ -15,16 +15,15 @@ import numpy as np import matplotlib.pyplot as plt from skimage.draw import line, polygon, circle, \ - circle_perimeter, circle_perimeter_aa, \ + circle_perimeter, \ ellipse, ellipse_perimeter -import numpy as np import math img = np.zeros((500, 500, 3), dtype=np.uint8) # draw line rr, cc = line(120, 123, 20, 400) -img[rr,cc,0] = 255 +img[rr, cc, 0] = 255 # fill polygon poly = np.array(( @@ -34,16 +33,16 @@ poly = np.array(( (220, 590), (300, 300), )) -rr, cc = polygon(poly[:,0], poly[:,1], img.shape) -img[rr,cc,1] = 255 +rr, cc = polygon(poly[:, 0], poly[:, 1], img.shape) +img[rr, cc, 1] = 255 # fill circle rr, cc = circle(200, 200, 100, img.shape) -img[rr,cc,:] = (255, 255, 0) +img[rr, cc, :] = (255, 255, 0) # fill ellipse rr, cc = ellipse(300, 300, 100, 200, img.shape) -img[rr,cc,2] = 255 +img[rr, cc, 2] = 255 # circle rr, cc = circle_perimeter(120, 400, 15) @@ -73,7 +72,6 @@ import matplotlib.pyplot as plt from skimage.draw import line_aa, \ circle_perimeter_aa -import numpy as np img = np.zeros((100, 100), dtype=np.uint8) From 5c423475e6df4698dffba01598dae11f81b1ebb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Tue, 27 Aug 2013 17:24:23 +0200 Subject: [PATCH 631/736] PEP8 --- skimage/draw/_draw.pyx | 8 ++++---- skimage/draw/tests/test_draw.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/skimage/draw/_draw.pyx b/skimage/draw/_draw.pyx index dc1ae305..1a42ef6b 100644 --- a/skimage/draw/_draw.pyx +++ b/skimage/draw/_draw.pyx @@ -550,9 +550,9 @@ def ellipse_perimeter(Py_ssize_t cy, Py_ssize_t cx, Py_ssize_t yradius, def _bezier_segment(Py_ssize_t y0, Py_ssize_t x0, - Py_ssize_t y1, Py_ssize_t x1, - Py_ssize_t y2, Py_ssize_t x2, - double weight): + Py_ssize_t y1, Py_ssize_t x1, + Py_ssize_t y2, Py_ssize_t x2, + double weight): """Generate Bezier segment coordinates. Parameters @@ -644,7 +644,7 @@ def _bezier_segment(Py_ssize_t y0, Py_ssize_t x0, dx = floor((weight * x1 + x0) * xy + 0.5) dy = floor((y1 * weight + y0) * xy + 0.5) return _bezier_segment(y0, x0, (dy), (dx), - (sy), (sx), cur) + (sy), (sx), cur) err = dx + dy - xy while dy <= xy and dx >= xy: diff --git a/skimage/draw/tests/test_draw.py b/skimage/draw/tests/test_draw.py index 4a79e819..1f30fb1d 100644 --- a/skimage/draw/tests/test_draw.py +++ b/skimage/draw/tests/test_draw.py @@ -434,11 +434,11 @@ def test_bezier_segment_straight(): x2 = 150 y2 = 150 rr, cc = _bezier_segment(x0, y0, x1, y1, x2, y2, 0) - image [rr, cc] = 1 + image[rr, cc] = 1 image2 = np.zeros((200, 200), dtype=int) rr, cc = line(x0, y0, x2, y2) - image2 [rr, cc] = 1 + image2[rr, cc] = 1 assert_array_equal(image, image2) From 1548364b650ab49c95476aa3595b8637522a3142 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Tue, 27 Aug 2013 18:24:47 +0200 Subject: [PATCH 632/736] ADD: bezier_curve --- skimage/draw/__init__.py | 3 +- skimage/draw/_draw.pyx | 112 ++++++++++++++++++++++++++++++++ skimage/draw/tests/test_draw.py | 106 ++++++++++++++++++++++++++++-- 3 files changed, 214 insertions(+), 7 deletions(-) diff --git a/skimage/draw/__init__.py b/skimage/draw/__init__.py index 8e455343..5f788ea7 100644 --- a/skimage/draw/__init__.py +++ b/skimage/draw/__init__.py @@ -2,10 +2,11 @@ from .draw import circle, ellipse, set_color from .draw3d import ellipsoid, ellipsoid_stats from ._draw import (line, line_aa, polygon, ellipse_perimeter, circle_perimeter, circle_perimeter_aa, - _bezier_segment) + _bezier_segment, bezier_curve) __all__ = ['line', 'line_aa', + 'bezier_curve', 'polygon', 'ellipse', 'ellipse_perimeter', diff --git a/skimage/draw/_draw.pyx b/skimage/draw/_draw.pyx index 1a42ef6b..dac8e90a 100644 --- a/skimage/draw/_draw.pyx +++ b/skimage/draw/_draw.pyx @@ -677,6 +677,118 @@ def _bezier_segment(Py_ssize_t y0, Py_ssize_t x0, return np.array(py, dtype=np.intp), np.array(px, dtype=np.intp) +def bezier_curve(Py_ssize_t y0, Py_ssize_t x0, + Py_ssize_t y1, Py_ssize_t x1, + Py_ssize_t y2, Py_ssize_t x2, + double weight): + """Generate Bezier curve coordinates. + + Parameters + ---------- + y0, x0 : int + Coordinates of the first point + y1, x1 : int + Coordinates of the middle point + y2, x2 : int + Coordinates of the last point + weight : double + Middle point weight, it describes the line tension. + + Returns + ------- + rr, cc : (N,) ndarray of int + Indices of pixels that belong to the Bezier curve. + May be used to directly index into an array, e.g. + ``img[rr, cc] = 1``. + + Notes + ----- + The algorithm is the rational quadratic algorithm presented in + reference [1]. + + References + ---------- + .. [1] A Rasterizing Algorithm for Drawing Curves, A. Zingl, 2012 + http://members.chello.at/easyfilter/Bresenham.pdf + """ + # Pixels + cdef list px = list() + cdef list py = list() + + cdef int x, y + cdef double xx, yy, ww, t, q + x = x0 - 2 * x1 + x2 + y = y0 - 2 * y1 + y2 + + xx = x0 - x1 + yy = y0 - y1 + + if xx * (x2 - x1) > 0: + if yy * (y2 - y1): + if abs(xx * y) > abs(yy * x): + x0 = x2 + x2 = (xx + x1) + y0 = y2 + y2 = (yy + y1) + if (x0 == x2) or (weight == 1.): + t = (x0 - x1) / x + else: + q = sqrt(4. * weight * weight * (x0 - x1) * (x2 - x1) + (x2 - x0) * floor(x2 - x0)) + if (x1 < x0): + q = -q + t = (2. * weight * (x0 - x1) - x0 + x2 + q) / (2. * (1. - weight) * (x2 - x0)) + + q = 1. / (2. * t * (1. - t) * (weight - 1.) + 1.0) + xx = (t * t * (x0 - 2. * weight * x1 + x2) + 2. * t * (weight * x1 - x0) + x0) * q + yy = (t * t * (y0 - 2. * weight * y1 + y2) + 2. * t * (weight * y1 - y0) + y0) * q + ww = t * (weight - 1.) + 1. + ww *= ww * q + weight = ((1. - t) * (weight - 1.) + 1.) * sqrt(q) + x = (xx + 0.5) + y = (yy + 0.5) + yy = (xx - x0) * (y1 - y0) / (x1 - x0) + y0 + + rr, cc = _bezier_segment(y0, x0, (yy + 0.5), x, y, x, ww) + px.extend(rr) + py.extend(cc) + + yy = (xx - x2) * (y1 - y2) / (x1 - x2) + y2 + y1 = (yy + 0.5) + x0 = x1 = x + y0 = y + if (y0 - y1) * floor(y2 - y1) > 0: + if (y0 == y2) or (weight == 1): + t = (y0 - y1) / (y0 - 2. * y1 + y2) + else: + q = sqrt(4. * weight * weight * (y0 - y1) * (y2 - y1) + (y2 - y0) * floor(y2 - y0)) + if y1 < y0: + q = -q + t = (2. * weight * (y0 - y1) - y0 + y2 + q) / (2. * (1. - weight) * (y2 - y0)) + q = 1. / (2. * t * (1. - t) * (weight - 1.) + 1.) + xx = (t * t * (x0 - 2. * weight * x1 + x2) + 2. * t * (weight * x1 - x0) + x0) * q + yy = (t * t * (y0 - 2. * weight * y1 + y2) + 2. * t * (weight * y1 - y0) + y0) * q + ww = t * (weight - 1.) + 1. + ww *= ww * q + weight = ((1. - t) * (weight - 1.) + 1.) * sqrt(q) + x = (xx + 0.5) + y = (yy + 0.5) + xx = (x1 - x0) * (yy - y0) / (y1 - y0) + x0 + + rr, cc = _bezier_segment(y0, x0, y, (xx + 0.5), y, x, ww) + px.extend(rr) + py.extend(cc) + + xx = (x1 - x2) * (yy - y2) / (y1 - y2) + x2 + x1 = (xx + 0.5) + x0 = x + y0 = y1 = y + + rr, cc = _bezier_segment(y0, x0, y1, x1, y2, x2, weight * weight) + px.extend(rr) + py.extend(cc) + return np.array(px, dtype=np.intp), np.array(py, dtype=np.intp) + + def set_color(img, coords, color): """Set pixel color in the image at the given coordinates. diff --git a/skimage/draw/tests/test_draw.py b/skimage/draw/tests/test_draw.py index 1f30fb1d..d3cab811 100644 --- a/skimage/draw/tests/test_draw.py +++ b/skimage/draw/tests/test_draw.py @@ -1,11 +1,10 @@ from numpy.testing import assert_array_equal, assert_equal import numpy as np -from skimage.draw import (line, line_aa, - polygon, circle, - circle_perimeter, circle_perimeter_aa, - ellipse, - ellipse_perimeter, _bezier_segment, +from skimage.draw import (line, line_aa, polygon, + circle, circle_perimeter, circle_perimeter_aa, + ellipse, ellipse_perimeter, + _bezier_segment, bezier_curve, ) @@ -444,7 +443,10 @@ def test_bezier_segment_straight(): def test_bezier_segment_curved(): img = np.zeros((25, 25), 'uint8') - rr, cc = _bezier_segment(20, 20, 20, 2, 2, 2, 1) + x1, y1 = 20, 20 + x2, y2 = 20, 2 + x3, y3 = 2, 2 + rr, cc = _bezier_segment(x1, y1, x2, y2, x3, y3, 1) img[rr, cc] = 1 img_ = np.array( [[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], @@ -473,9 +475,101 @@ def test_bezier_segment_curved(): [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]] ) + assert_equal(img[x1, y1], 1) + assert_equal(img[x3, y3], 1) assert_array_equal(img, img_) +def test_bezier_curve_straight(): + image = np.zeros((200, 200), dtype=int) + x0 = 50 + y0 = 50 + x1 = 150 + y1 = 50 + x2 = 150 + y2 = 150 + rr, cc = bezier_curve(x0, y0, x1, y1, x2, y2, 0) + image [rr, cc] = 1 + + image2 = np.zeros((200, 200), dtype=int) + rr, cc = line(x0, y0, x2, y2) + image2 [rr, cc] = 1 + assert_array_equal(image, image2) + + +def test_bezier_curved_weight_eq_1(): + img = np.zeros((23, 8), 'uint8') + x1, y1 = (1, 1) + x2, y2 = (11, 11) + x3, y3 = (21, 1) + rr, cc = bezier_curve(x1, y1, x2, y2, x3, y3, 1) + img[rr, cc] = 1 + assert_equal(img[x1, y1], 1) + assert_equal(img[x3, y3], 1) + img_ = np.array( + [[0, 0, 0, 0, 0, 0, 0, 0], + [0, 1, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 0, 0, 0, 0, 1, 0], + [0, 0, 0, 0, 0, 0, 1, 0], + [0, 0, 0, 0, 0, 0, 1, 0], + [0, 0, 0, 0, 0, 0, 1, 0], + [0, 0, 0, 0, 0, 0, 1, 0], + [0, 0, 0, 0, 0, 0, 1, 0], + [0, 0, 0, 0, 0, 0, 1, 0], + [0, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 1, 0, 0, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 0], + [0, 1, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0]] + ) + assert_equal(img, img_) + + +def test_bezier_curved_weight_neq_1(): + img = np.zeros((23, 10), 'uint8') + x1, y1 = (1, 1) + x2, y2 = (11, 11) + x3, y3 = (21, 1) + rr, cc = bezier_curve(x1, y1, x2, y2, x3, y3, 2) + img[rr, cc] = 1 + assert_equal(img[x1, y1], 1) + assert_equal(img[x3, y3], 1) + img_ = np.array( + [[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 1, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 1, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 1, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 1, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 1, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 1, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 1, 0], + [0, 0, 0, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 0, 0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 1, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 0, 0, 0], + [0, 1, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]] + ) + assert_equal(img, img_) + if __name__ == "__main__": from numpy.testing import run_module_suite run_module_suite() From 9ceb489ba8c92a4a5465723d5c0618ca637624b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Tue, 27 Aug 2013 18:28:32 +0200 Subject: [PATCH 633/736] DOC: add bezier_curve --- doc/examples/plot_shapes.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/doc/examples/plot_shapes.py b/doc/examples/plot_shapes.py index 995a278a..197e133b 100644 --- a/doc/examples/plot_shapes.py +++ b/doc/examples/plot_shapes.py @@ -6,6 +6,7 @@ Shapes This example shows how to draw several different shapes: * line +* Bezier curve * polygon * circle * ellipse @@ -16,7 +17,8 @@ import matplotlib.pyplot as plt from skimage.draw import line, polygon, circle, \ circle_perimeter, \ - ellipse, ellipse_perimeter + ellipse, ellipse_perimeter, \ + bezier_curve import math img = np.zeros((500, 500, 3), dtype=np.uint8) @@ -48,6 +50,10 @@ img[rr, cc, 2] = 255 rr, cc = circle_perimeter(120, 400, 15) img[rr, cc, :] = (255, 0, 0) +# Bezier curve +rr, cc = bezier_curve(70, 100, 10, 10, 150, 100, 1) +img[rr, cc, :] = (255, 0, 0) + # ellipses rr, cc = ellipse_perimeter(120, 400, 60, 20, orientation=math.pi / 4.) img[rr, cc, :] = (255, 0, 255) From 296f8dad20ee56cafd811dbedddff1dd747e3edb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Wed, 2 Oct 2013 09:39:36 +0200 Subject: [PATCH 634/736] MINOR: doctrings + various improvements --- doc/examples/plot_shapes.py | 14 +++---- skimage/draw/_draw.pyx | 84 +++++++++++++++++++++++++++++-------- 2 files changed, 73 insertions(+), 25 deletions(-) diff --git a/doc/examples/plot_shapes.py b/doc/examples/plot_shapes.py index 197e133b..8e082579 100644 --- a/doc/examples/plot_shapes.py +++ b/doc/examples/plot_shapes.py @@ -15,10 +15,10 @@ This example shows how to draw several different shapes: import numpy as np import matplotlib.pyplot as plt -from skimage.draw import line, polygon, circle, \ - circle_perimeter, \ - ellipse, ellipse_perimeter, \ - bezier_curve +from skimage.draw import (line, polygon, circle, + circle_perimeter, + ellipse, ellipse_perimeter, + bezier_curve) import math img = np.zeros((500, 500, 3), dtype=np.uint8) @@ -67,7 +67,7 @@ plt.show() """ -Anti-aliasing drawing for: +Anti-aliased drawing for: * line * circle @@ -76,8 +76,8 @@ Anti-aliasing drawing for: import numpy as np import matplotlib.pyplot as plt -from skimage.draw import line_aa, \ - circle_perimeter_aa +from skimage.draw import (line_aa, + circle_perimeter_aa) img = np.zeros((100, 100), dtype=np.uint8) diff --git a/skimage/draw/_draw.pyx b/skimage/draw/_draw.pyx index dac8e90a..bb190b8b 100644 --- a/skimage/draw/_draw.pyx +++ b/skimage/draw/_draw.pyx @@ -105,9 +105,8 @@ def line_aa(Py_ssize_t y1, Py_ssize_t x1, Py_ssize_t y2, Py_ssize_t x2): Returns ------- - rr, cc, val : (N,) ndarray of (int, int, float) - Indices of pixels that belong to the line. - May be used to directly index into an array, e.g. + rr, cc, val : (N,) ndarray (int, int, float) + Indices of pixels (`rr`, `cc`) and intensity values (`val`). ``img[rr, cc] = val``. References @@ -115,6 +114,23 @@ def line_aa(Py_ssize_t y1, Py_ssize_t x1, Py_ssize_t y2, Py_ssize_t x2): .. [1] A Rasterizing Algorithm for Drawing Curves, A. Zingl, 2012 http://members.chello.at/easyfilter/Bresenham.pdf + Examples + -------- + >>> from skimage.draw import line_aa + >>> img = np.zeros((10, 10), dtype=np.uint8) + >>> rr, cc, val = line_aa(1, 1, 8, 8) + >>> img[rr, cc] = val * 255 + >>> img + array([[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [ 0, 255, 56, 0, 0, 0, 0, 0, 0, 0], + [ 0, 56, 255, 56, 0, 0, 0, 0, 0, 0], + [ 0, 0, 56, 255, 56, 0, 0, 0, 0, 0], + [ 0, 0, 0, 56, 255, 56, 0, 0, 0, 0], + [ 0, 0, 0, 0, 56, 255, 56, 0, 0, 0], + [ 0, 0, 0, 0, 0, 56, 255, 56, 0, 0], + [ 0, 0, 0, 0, 0, 0, 56, 255, 56, 0], + [ 0, 0, 0, 0, 0, 0, 0, 56, 255, 0], + [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=uint8) """ cdef list rr = list() cdef list cc = list() @@ -375,11 +391,28 @@ def circle_perimeter_aa(Py_ssize_t cy, Py_ssize_t cx, Py_ssize_t radius): .. [1] X. Wu, "An efficient antialiasing technique", In ACM SIGGRAPH Computer Graphics, 25 (1991) 143-152. + Examples + -------- + >>> from skimage.draw import circle_perimeter_aa + >>> img = np.zeros((10, 10), dtype=np.uint8) + >>> rr, cc, val = circle_perimeter_aa(4, 4, 3) + >>> img[rr, cc] = val * 255 + >>> img + array([[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [ 0, 0, 60, 211, 255, 211, 60, 0, 0, 0], + [ 0, 60, 194, 43, 0, 43, 194, 60, 0, 0], + [ 0, 211, 43, 0, 0, 0, 43, 211, 0, 0], + [ 0, 255, 0, 0, 0, 0, 0, 255, 0, 0], + [ 0, 211, 43, 0, 0, 0, 43, 211, 0, 0], + [ 0, 60, 194, 43, 0, 43, 194, 60, 0, 0], + [ 0, 0, 60, 211, 255, 211, 60, 0, 0, 0], + [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=uint8) """ - cdef list rr = list() - cdef list cc = list() - cdef list val = list() + cdef list rr = [y, x, y, x, -y, -x, -y, -x] + cdef list cc = [x, y, -x, -y, x, y, -x, -y] + cdef list val = [1] * 8 cdef Py_ssize_t x = 0 cdef Py_ssize_t y = radius @@ -389,10 +422,6 @@ def circle_perimeter_aa(Py_ssize_t cy, Py_ssize_t cx, Py_ssize_t radius): dceil_prev = 0 - rr.extend([y, x, y, x, -y, -x, -y, -x]) - cc.extend([x, y, -x, -y, x, y, -x, -y]) - val.extend([1] * 8) - while y > x + 1: x += 1 dceil = math.sqrt(radius**2 - x**2) @@ -558,13 +587,13 @@ def _bezier_segment(Py_ssize_t y0, Py_ssize_t x0, Parameters ---------- y0, x0 : int - Coordinates of the first point + Coordinates of the first control point. y1, x1 : int - Coordinates of the middle point + Coordinates of the middle control point. y2, x2 : int - Coordinates of the last point + Coordinates of the last control point. weight : double - Middle point weight, it describes the line tension. + Middle control point weight, it describes the line tension. Returns ------- @@ -686,13 +715,13 @@ def bezier_curve(Py_ssize_t y0, Py_ssize_t x0, Parameters ---------- y0, x0 : int - Coordinates of the first point + Coordinates of the first control point. y1, x1 : int - Coordinates of the middle point + Coordinates of the middle control point. y2, x2 : int - Coordinates of the last point + Coordinates of the last control point. weight : double - Middle point weight, it describes the line tension. + Middle control point weight, it describes the line tension. Returns ------- @@ -710,6 +739,25 @@ def bezier_curve(Py_ssize_t y0, Py_ssize_t x0, ---------- .. [1] A Rasterizing Algorithm for Drawing Curves, A. Zingl, 2012 http://members.chello.at/easyfilter/Bresenham.pdf + + Examples + -------- + >>> import numpy as np + >>> from skimage.draw import bezier_curve + >>> img = np.zeros((10, 10), dtype=np.uint8) + >>> rr, cc = bezier_curve(1, 5, 5, -2, 8, 8, 2) + >>> img[rr, cc] = 1 + >>> img + array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 1, 0, 0, 0, 0], + [0, 0, 0, 1, 1, 0, 0, 0, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 0, 0, 0], + [0, 1, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 1, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 1, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 1, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 1, 1, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=uint8) """ # Pixels cdef list px = list() From 7299753602df96dd67d8bd0ea967f0af62861146 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Wed, 2 Oct 2013 10:38:11 +0200 Subject: [PATCH 635/736] FIX: broken test (thanks unittest!) --- skimage/draw/_draw.pyx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/skimage/draw/_draw.pyx b/skimage/draw/_draw.pyx index bb190b8b..f4814f84 100644 --- a/skimage/draw/_draw.pyx +++ b/skimage/draw/_draw.pyx @@ -410,17 +410,16 @@ def circle_perimeter_aa(Py_ssize_t cy, Py_ssize_t cx, Py_ssize_t radius): [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=uint8) """ - cdef list rr = [y, x, y, x, -y, -x, -y, -x] - cdef list cc = [x, y, -x, -y, x, y, -x, -y] - cdef list val = [1] * 8 - cdef Py_ssize_t x = 0 cdef Py_ssize_t y = radius cdef Py_ssize_t d = 0 cdef double dceil = 0 + cdef double dceil_prev = 0 - dceil_prev = 0 + cdef list rr = [y, x, y, x, -y, -x, -y, -x] + cdef list cc = [x, y, -x, -y, x, y, -x, -y] + cdef list val = [1] * 8 while y > x + 1: x += 1 From 055e820e720d89ca6a8c8e7915fc80f52a89ffbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Wed, 2 Oct 2013 17:21:35 +0200 Subject: [PATCH 636/736] MINOR: some comments --- skimage/draw/_draw.pyx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/skimage/draw/_draw.pyx b/skimage/draw/_draw.pyx index f4814f84..3d48ce86 100644 --- a/skimage/draw/_draw.pyx +++ b/skimage/draw/_draw.pyx @@ -6,7 +6,7 @@ import math import numpy as np cimport numpy as cnp -from libc.math cimport sqrt, sin, cos, floor +from libc.math cimport sqrt, sin, cos, floor, ceil from skimage._shared.geometry cimport point_in_polygon @@ -94,7 +94,7 @@ def line(Py_ssize_t y, Py_ssize_t x, Py_ssize_t y2, Py_ssize_t x2): def line_aa(Py_ssize_t y1, Py_ssize_t x1, Py_ssize_t y2, Py_ssize_t x2): - """Generate line pixel coordinates. + """Generate anti-aliased line pixel coordinates. Parameters ---------- @@ -231,9 +231,9 @@ def polygon(y, x, shape=None): cdef Py_ssize_t nr_verts = x.shape[0] cdef Py_ssize_t minr = int(max(0, y.min())) - cdef Py_ssize_t maxr = int(math.ceil(y.max())) + cdef Py_ssize_t maxr = int(ceil(y.max())) cdef Py_ssize_t minc = int(max(0, x.min())) - cdef Py_ssize_t maxc = int(math.ceil(x.max())) + cdef Py_ssize_t maxc = int(ceil(x.max())) # make sure output coordinates do not exceed image size if shape is not None: @@ -423,8 +423,8 @@ def circle_perimeter_aa(Py_ssize_t cy, Py_ssize_t cx, Py_ssize_t radius): while y > x + 1: x += 1 - dceil = math.sqrt(radius**2 - x**2) - dceil = math.ceil(dceil) - dceil + dceil = sqrt(radius**2 - x**2) + dceil = ceil(dceil) - dceil if dceil < dceil_prev: y -= 1 rr.extend([y, y - 1, x, x, y, y - 1, x, x]) @@ -604,7 +604,7 @@ def _bezier_segment(Py_ssize_t y0, Py_ssize_t x0, Notes ----- The algorithm is the rational quadratic algorithm presented in - reference [1]. + reference [1]_. References ---------- @@ -732,7 +732,7 @@ def bezier_curve(Py_ssize_t y0, Py_ssize_t x0, Notes ----- The algorithm is the rational quadratic algorithm presented in - reference [1]. + reference [1]_. References ---------- From 40f5e78353433f80db72b573993ad4e2c8a73154 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Wed, 2 Oct 2013 17:33:17 +0200 Subject: [PATCH 637/736] Add support for matrix as inverse_map --- skimage/transform/_geometric.py | 20 +++++++++----- skimage/transform/tests/test_warps.py | 39 ++++++++++++++++++++++----- 2 files changed, 46 insertions(+), 13 deletions(-) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index d3fd10ad..f1b611eb 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -951,7 +951,7 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, ---------- image : 2-D or 3-D array Input image. - inverse_map : transformation object, callable ``xy = f(xy, **kwargs)`` + inverse_map : transformation object, callable ``xy = f(xy, **kwargs)``, (3, 3) array Inverse coordinate map. A function that transforms a (N, 2) array of ``(x, y)`` coordinates in the *output image* into their corresponding coordinates in the *source image* (e.g. a transformation object or its @@ -1022,16 +1022,21 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, if order in range(4) and not map_args: matrix = None - if inverse_map in HOMOGRAPHY_TRANSFORMS: + if isinstance(inverse_map, np.ndarray) and inverse_map.shape == (3, 3): + matrix = inverse_map + + elif inverse_map in HOMOGRAPHY_TRANSFORMS: matrix = inverse_map._matrix - elif hasattr(inverse_map, '__name__') \ - and inverse_map.__name__ == 'inverse' \ - and get_bound_method_class(inverse_map) in HOMOGRAPHY_TRANSFORMS: + elif (hasattr(inverse_map, '__name__') + and inverse_map.__name__ == 'inverse' + and get_bound_method_class(inverse_map) + in HOMOGRAPHY_TRANSFORMS): matrix = np.linalg.inv(six.get_method_self(inverse_map)._matrix) if matrix is not None: + matrix = matrix.astype(np.double) # transform all bands dims = [] for dim in range(image.shape[2]): @@ -1049,12 +1054,15 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, rows, cols = output_shape[:2] + if isinstance(inverse_map, np.ndarray) and inverse_map.shape == (3, 3): + inverse_map = ProjectiveTransform(matrix=inverse_map) + def coord_map(*args): return inverse_map(*args, **map_args) coords = warp_coords(coord_map, (rows, cols, bands)) - # Prefilter not necessary for order 1 interpolation + # Prefilter not necessary for order 0, 1 interpolation prefilter = order > 1 out = ndimage.map_coordinates(image, coords, prefilter=prefilter, mode=mode, order=order, cval=cval) diff --git a/skimage/transform/tests/test_warps.py b/skimage/transform/tests/test_warps.py index af2b95da..7f7ef47d 100644 --- a/skimage/transform/tests/test_warps.py +++ b/skimage/transform/tests/test_warps.py @@ -11,10 +11,9 @@ from skimage import transform as tf, data, img_as_float from skimage.color import rgb2gray -def test_warp(): - x = np.zeros((5, 5), dtype=np.uint8) - x[2, 2] = 255 - x = img_as_float(x) +def test_warp_tform(): + x = np.zeros((5, 5), dtype=np.double) + x[2, 2] = 1 theta = - np.pi / 2 tform = SimilarityTransform(scale=1, rotation=theta, translation=(0, 4)) @@ -25,10 +24,36 @@ def test_warp(): assert_array_almost_equal(x90, np.rot90(x)) +def test_warp_callable(): + x = np.zeros((5, 5), dtype=np.double) + x[2, 2] = 1 + refx = np.zeros((5, 5), dtype=np.double) + refx[1, 1] = 1 + + shift = lambda xy: xy + 1 + + outx = warp(x, shift, order=1) + assert_array_almost_equal(outx, refx) + + +def test_warp_matrix(): + x = np.zeros((5, 5), dtype=np.double) + x[2, 2] = 1 + refx = np.zeros((5, 5), dtype=np.double) + refx[1, 1] = 1 + + matrix = np.array([[1, 0, 1], [0, 1, 1], [0, 0, 1]]) + + # _warp_fast + outx = warp(x, matrix, order=1) + assert_array_almost_equal(outx, refx) + # check for ndimage.map_coordinates + outx = warp(x, matrix, order=5) + + def test_homography(): - x = np.zeros((5, 5), dtype=np.uint8) - x[1, 1] = 255 - x = img_as_float(x) + x = np.zeros((5, 5), dtype=np.double) + x[1, 1] = 1 theta = -np.pi / 2 M = np.array([[np.cos(theta), - np.sin(theta), 0], [np.sin(theta), np.cos(theta), 4], From ee24f1c2c5af56e1872e088536c72becedfcd28a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Wed, 2 Oct 2013 17:45:05 +0200 Subject: [PATCH 638/736] Improve example of warp function to show all available options --- skimage/transform/_geometric.py | 37 ++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/skimage/transform/_geometric.py b/skimage/transform/_geometric.py index f1b611eb..25a13765 100644 --- a/skimage/transform/_geometric.py +++ b/skimage/transform/_geometric.py @@ -979,28 +979,41 @@ def warp(image, inverse_map=None, map_args={}, output_shape=None, order=1, Notes ----- In case of a `SimilarityTransform`, `AffineTransform` and - `ProjectiveTransform` this function uses the underlying transformation - matrix to warp the image with a much faster routine. + `ProjectiveTransform` and `order` in [0, 3] this function uses the + underlying transformation matrix to warp the image with a much faster + routine. Examples -------- - Shift an image to the right: - >>> from skimage.transform import warp >>> from skimage import data >>> image = data.camera() - >>> - >>> def shift_right(xy): - ... xy[:, 0] -= 10 - ... return xy - >>> - >>> warp(image, shift_right) - Use a geometric transform to warp an image: + The following image warps are all equal but differ substantially in + execution time. + + Use a geometric transform to warp an image (fast): >>> from skimage.transform import SimilarityTransform - >>> tform = SimilarityTransform(scale=0.1, rotation=0.1) + >>> tform = SimilarityTransform(translation=(0, -10)) >>> warp(image, tform) + + Shift an image to the right with a callable (slow): + + >>> def shift(xy): + ... xy[:, 1] -= 10 + ... return xy + >>> warp(image, shift_right) + + Use a transformation matrix to warp an image (fast): + + >>> matrix = np.array([[1, 0, 0], [0, 1, -10], [0, 0, 1]]) + >>> warp(image, matrix) + >>> from skimage.transform import ProjectiveTransform + >>> warp(image, ProjectiveTransform(matrix=matrix)) + + You can also use the inverse of a geometric transformation (fast): + >>> warp(image, tform.inverse) """ From 2459ca14a030e67a3d75b581e76c81a1606673ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Wed, 2 Oct 2013 17:57:20 +0200 Subject: [PATCH 639/736] Fix deprecated function name in example --- skimage/filter/_denoise.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/skimage/filter/_denoise.py b/skimage/filter/_denoise.py index 87850759..ff89b78b 100644 --- a/skimage/filter/_denoise.py +++ b/skimage/filter/_denoise.py @@ -37,7 +37,7 @@ def _denoise_tv_chambolle_3d(im, weight=100, eps=2.e-4, n_iter_max=200): >>> mask = (x - 22)**2 + (y - 20)**2 + (z - 17)**2 < 8**2 >>> mask = mask.astype(np.float) >>> mask += 0.2 * np.random.randn(*mask.shape) - >>> res = denoise_tv(mask, weight=100) + >>> res = denoise_tv_chambolle(mask, weight=100) """ @@ -127,7 +127,7 @@ def _denoise_tv_chambolle_2d(im, weight=50, eps=2.e-4, n_iter_max=200): >>> from skimage import color, data >>> lena = color.rgb2gray(data.lena()) >>> lena += 0.5 * lena.std() * np.random.randn(*lena.shape) - >>> denoised_lena = denoise_tv(lena, weight=60) + >>> denoised_lena = denoise_tv_chambolle(lena, weight=60) """ @@ -227,7 +227,7 @@ def denoise_tv_chambolle(im, weight=50, eps=2.e-4, n_iter_max=200, >>> from skimage import color, data >>> lena = color.rgb2gray(data.lena()) >>> lena += 0.5 * lena.std() * np.random.randn(*lena.shape) - >>> denoised_lena = denoise_tv(lena, weight=60) + >>> denoised_lena = denoise_tv_chambolle(lena, weight=60) 3D example on synthetic data: @@ -235,7 +235,7 @@ def denoise_tv_chambolle(im, weight=50, eps=2.e-4, n_iter_max=200, >>> mask = (x - 22)**2 + (y - 20)**2 + (z - 17)**2 < 8**2 >>> mask = mask.astype(np.float) >>> mask += 0.2*np.random.randn(*mask.shape) - >>> res = denoise_tv(mask, weight=100) + >>> res = denoise_tv_chambolle(mask, weight=100) """ From e807e25fda0e7e706835d0606a5ca7a327bc38ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Wed, 2 Oct 2013 17:59:44 +0200 Subject: [PATCH 640/736] Temporarily remove censure keypoints example --- doc/examples/plot_censure_keypoints.py | 54 -------------------------- 1 file changed, 54 deletions(-) delete mode 100644 doc/examples/plot_censure_keypoints.py diff --git a/doc/examples/plot_censure_keypoints.py b/doc/examples/plot_censure_keypoints.py deleted file mode 100644 index fbba4e1b..00000000 --- a/doc/examples/plot_censure_keypoints.py +++ /dev/null @@ -1,54 +0,0 @@ -""" -========================= -CenSurE Feature Detection -========================= - -In this example we detect and plot the CenSurE (Center Surround Extrema) -features at various scales using Difference of Boxes, Octagon and Star shaped -bi-level filters. - -""" - -from skimage.feature import keypoints_censure -from skimage.data import lena -from skimage.color import rgb2gray -import matplotlib.pyplot as plt - -# Initializing the parameters for Censure keypoints -img = lena() -gray_img = rgb2gray(img) -min_scale = 2 -max_scale = 6 -non_max_threshold = 0.15 -line_threshold = 10 - - -_, ax = plt.subplots(nrows=(max_scale - min_scale - 1), ncols=3, - figsize=(6, 6)) -plt.subplots_adjust(wspace=0.02, hspace=0.02, top=0.94, - bottom=0.02, left=0.06, right=0.98) - -# Detecting Censure keypoints for the following filters -for col, mode in enumerate(['dob', 'octagon', 'star']): - - ax[0, col].set_title(mode.upper(), fontsize=12) - - keypoints, scales = keypoints_censure(gray_img, min_scale, max_scale, - mode, non_max_threshold, - line_threshold) - - # Plotting Censure features at all the scales - for row, scale in enumerate(range(min_scale + 1, max_scale)): - mask = scales == scale - x = keypoints[mask, 1] - y = keypoints[mask, 0] - s = 0.5 * 2 ** (scale + min_scale + 1) - ax[row, col].imshow(img) - ax[row, col].scatter(x, y, s, facecolors='none', edgecolors='b') - ax[row, col].set_xticks([]) - ax[row, col].set_yticks([]) - ax[row, col].axis((0, img.shape[1], img.shape[0], 0)) - if col == 0: - ax[row, col].set_ylabel('Scale %d' % scale, fontsize=12) - -plt.show() From bac7ede8b2072c8ebbe86dcdc996e3986784db27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Wed, 2 Oct 2013 18:12:01 +0200 Subject: [PATCH 641/736] PEP8 --- skimage/draw/draw.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/draw/draw.py b/skimage/draw/draw.py index d01bc2b0..cbf3ced2 100644 --- a/skimage/draw/draw.py +++ b/skimage/draw/draw.py @@ -52,7 +52,7 @@ def ellipse(cy, cx, yradius, xradius, shape=None): dc = 1 / float(xradius) r, c = np.ogrid[-1:1:dr, -1:1:dc] - rr, cc = np.nonzero(r ** 2 + c ** 2 < 1) + rr, cc = np.nonzero(r ** 2 + c ** 2 < 1) rr.flags.writeable = True cc.flags.writeable = True From 741d6fda95e7c1c94f7ff06a7428603805c971d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Wed, 2 Oct 2013 17:44:05 +0200 Subject: [PATCH 642/736] PEP8: comparison --- skimage/io/video.py | 2 +- skimage/morphology/greyreconstruct.py | 2 +- skimage/morphology/watershed.py | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/skimage/io/video.py b/skimage/io/video.py index 6cb1a8e9..003a1f53 100644 --- a/skimage/io/video.py +++ b/skimage/io/video.py @@ -256,7 +256,7 @@ class Video(object): Backend to use. """ def __init__(self, source=None, size=None, sync=False, backend=None): - if backend == None: + if backend is None: # select backend that is available if gstreamer_available: self.video = GstVideo(source, size, sync) diff --git a/skimage/morphology/greyreconstruct.py b/skimage/morphology/greyreconstruct.py index 17455b55..3fffd28e 100644 --- a/skimage/morphology/greyreconstruct.py +++ b/skimage/morphology/greyreconstruct.py @@ -133,7 +133,7 @@ def reconstruction(seed, mask, method='dilation', selem=None, offset=None): else: selem = selem.copy() - if offset == None: + if offset is None: if not all([d % 2 == 1 for d in selem.shape]): ValueError("Footprint dimensions must all be odd") offset = np.array([d // 2 for d in selem.shape]) diff --git a/skimage/morphology/watershed.py b/skimage/morphology/watershed.py index fbd63281..097e3129 100644 --- a/skimage/morphology/watershed.py +++ b/skimage/morphology/watershed.py @@ -124,13 +124,13 @@ def watershed(image, markers, connectivity=None, offset=None, mask=None): separate overlapping spheres. """ - if connectivity == None: + if connectivity is None: c_connectivity = scipy.ndimage.generate_binary_structure(image.ndim, 1) else: c_connectivity = np.array(connectivity, bool) if c_connectivity.ndim != image.ndim: raise ValueError("Connectivity dimension must be same as image") - if offset == None: + if offset is None: if any([x % 2 == 0 for x in c_connectivity.shape]): raise ValueError("Connectivity array must have an unambiguous " "center") @@ -162,7 +162,7 @@ def watershed(image, markers, connectivity=None, offset=None, mask=None): "as image (ndim=%d)" % (c_markers.ndim, c_image.ndim)) if c_markers.shape != c_image.shape: raise ValueError("image and markers must have the same shape") - if mask != None: + if mask is not None: c_mask = np.ascontiguousarray(mask, dtype=bool) if c_mask.ndim != c_markers.ndim: raise ValueError("mask must have same # of dimensions as image") @@ -398,7 +398,7 @@ def _slow_watershed(image, markers, connectivity=8, mask=None): continue if labels[x, y]: continue - if mask != None and not mask[x, y]: + if mask is not None and not mask[x, y]: continue # label the pixel labels[x, y] = pix_label From f82db285656d325d112f74264a28446a67281445 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Wed, 2 Oct 2013 17:47:25 +0200 Subject: [PATCH 643/736] MAINT: unused import --- doc/examples/plot_entropy.py | 1 - 1 file changed, 1 deletion(-) diff --git a/doc/examples/plot_entropy.py b/doc/examples/plot_entropy.py index f5001e31..9f208c73 100644 --- a/doc/examples/plot_entropy.py +++ b/doc/examples/plot_entropy.py @@ -7,7 +7,6 @@ Image entropy is a quantity which is used to describe the amount of information coded in an image. """ -import numpy as np import matplotlib.pyplot as plt from skimage import data From 2e2a82ab309a4b93330cc5d14bd9105e20cac5ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Wed, 2 Oct 2013 17:48:09 +0200 Subject: [PATCH 644/736] MAINT: matplotlib is in requierements.txt --- skimage/viewer/canvastools/painttool.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/skimage/viewer/canvastools/painttool.py b/skimage/viewer/canvastools/painttool.py index 0678c460..3b4132f0 100644 --- a/skimage/viewer/canvastools/painttool.py +++ b/skimage/viewer/canvastools/painttool.py @@ -1,12 +1,8 @@ import numpy as np - -try: - import matplotlib.pyplot as plt - import matplotlib.colors as mcolors - LABELS_CMAP = mcolors.ListedColormap(['white', 'red', 'dodgerblue', 'gold', +import matplotlib.pyplot as plt +import matplotlib.colors as mcolors +LABELS_CMAP = mcolors.ListedColormap(['white', 'red', 'dodgerblue', 'gold', 'greenyellow', 'blueviolet']) -except ImportError: - print("Could not import matplotlib -- skimage.viewer not available.") from skimage.viewer.canvastools.base import CanvasToolBase @@ -192,7 +188,6 @@ class CenteredWindow(object): if __name__ == '__main__': np.testing.rundocs() - import matplotlib.pyplot as plt from skimage import data image = data.camera() From 628d484942d376177a60aab866269a06feec4abd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Wed, 2 Oct 2013 17:49:47 +0200 Subject: [PATCH 645/736] DOC: no import numpy in doc --- skimage/segmentation/_join.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/skimage/segmentation/_join.py b/skimage/segmentation/_join.py index b726c81e..1eda9176 100644 --- a/skimage/segmentation/_join.py +++ b/skimage/segmentation/_join.py @@ -20,7 +20,6 @@ def join_segmentations(s1, s2): Examples -------- - >>> import numpy as np >>> from skimage.segmentation import join_segmentations >>> s1 = np.array([[0, 0, 1, 1], ... [0, 2, 1, 1], @@ -78,7 +77,6 @@ def relabel_from_one(label_field): Examples -------- - >>> import numpy as np >>> from skimage.segmentation import relabel_from_one >>> label_field = array([1, 1, 5, 5, 8, 99, 42]) >>> relab, fw, inv = relabel_from_one(label_field) From 90db96f3ebf9df4b23c010c82c70fefc2259e0bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Wed, 2 Oct 2013 18:03:23 +0200 Subject: [PATCH 646/736] DOC: add missing import --- skimage/morphology/_skeletonize.py | 1 + 1 file changed, 1 insertion(+) diff --git a/skimage/morphology/_skeletonize.py b/skimage/morphology/_skeletonize.py index bd9f225c..8872aada 100644 --- a/skimage/morphology/_skeletonize.py +++ b/skimage/morphology/_skeletonize.py @@ -212,6 +212,7 @@ def medial_axis(image, mask=None, return_distance=False): Examples -------- + >>> from skimage import morphology >>> square = np.zeros((7, 7), dtype=np.uint8) >>> square[1:-1, 2:-2] = 1 >>> square From 8f20fff3f8b6335d7e9dcec239bfc3df121cf0c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Wed, 2 Oct 2013 18:03:42 +0200 Subject: [PATCH 647/736] PEP8: fix indentation --- skimage/filter/rank/generic.py | 8 ++++---- skimage/filter/thresholding.py | 22 +++++++++++----------- skimage/measure/find_contours.py | 2 +- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/skimage/filter/rank/generic.py b/skimage/filter/rank/generic.py index 247701d5..5ed9a46b 100644 --- a/skimage/filter/rank/generic.py +++ b/skimage/filter/rank/generic.py @@ -249,8 +249,8 @@ def maximum(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Note ---- - * the lower algorithm complexity makes the rank.maximum() more efficient for - larger images and structuring elements + * the lower algorithm complexity makes the rank.maximum() more efficient + for larger images and structuring elements """ @@ -299,7 +299,7 @@ def mean(image, selem, out=None, mask=None, shift_x=False, shift_y=False): def subtract_mean(image, selem, out=None, mask=None, shift_x=False, - shift_y=False): + shift_y=False): """Return image subtracted from its local mean. Parameters @@ -439,7 +439,7 @@ def modal(image, selem, out=None, mask=None, shift_x=False, shift_y=False): def enhance_contrast(image, selem, out=None, mask=None, shift_x=False, - shift_y=False): + shift_y=False): """Enhance an image replacing each pixel by the local maximum if pixel greylevel is closest to maximimum than local minimum OR local minimum otherwise. diff --git a/skimage/filter/thresholding.py b/skimage/filter/thresholding.py index bc052d8a..7f980387 100644 --- a/skimage/filter/thresholding.py +++ b/skimage/filter/thresholding.py @@ -1,4 +1,4 @@ -__all__ = ['threshold_adaptive', 'threshold_otsu', 'threshold_yen'] +__all__ = ['threshold_adaptive', 'threshold_otsu', 'threshold_yen'] import numpy as np import scipy.ndimage @@ -65,7 +65,7 @@ def threshold_adaptive(image, block_size, method='gaussian', offset=0, thresh_image = np.zeros(image.shape, 'double') if method == 'generic': scipy.ndimage.generic_filter(image, param, block_size, - output=thresh_image, mode=mode) + output=thresh_image, mode=mode) elif method == 'gaussian': if param is None: # automatically determine sigma which covers > 99% of distribution @@ -73,17 +73,17 @@ def threshold_adaptive(image, block_size, method='gaussian', offset=0, else: sigma = param scipy.ndimage.gaussian_filter(image, sigma, output=thresh_image, - mode=mode) + mode=mode) elif method == 'mean': mask = 1. / block_size * np.ones((block_size,)) # separation of filters to speedup convolution scipy.ndimage.convolve1d(image, mask, axis=0, output=thresh_image, - mode=mode) + mode=mode) scipy.ndimage.convolve1d(thresh_image, mask, axis=1, - output=thresh_image, mode=mode) + output=thresh_image, mode=mode) elif method == 'median': scipy.ndimage.median_filter(image, block_size, output=thresh_image, - mode=mode) + mode=mode) return image > (thresh_image - offset) @@ -146,7 +146,7 @@ def threshold_yen(image, nbins=256): nbins : int, optional Number of bins used to calculate histogram. This value is ignored for integer arrays. - + Returns ------- threshold : float @@ -155,11 +155,11 @@ def threshold_yen(image, nbins=256): References ---------- - .. [1] Yen J.C., Chang F.J., and Chang S. (1995) "A New Criterion - for Automatic Multilevel Thresholding" IEEE Trans. on Image + .. [1] Yen J.C., Chang F.J., and Chang S. (1995) "A New Criterion + for Automatic Multilevel Thresholding" IEEE Trans. on Image Processing, 4(3): 370-378 - .. [2] Sezgin M. and Sankur B. (2004) "Survey over Image Thresholding - Techniques and Quantitative Performance Evaluation" Journal of + .. [2] Sezgin M. and Sankur B. (2004) "Survey over Image Thresholding + Techniques and Quantitative Performance Evaluation" Journal of Electronic Imaging, 13(1): 146-165, http://www.busim.ee.boun.edu.tr/~sankur/SankurFolder/Threshold_survey.pdf .. [3] ImageJ AutoThresholder code, http://fiji.sc/wiki/index.php/Auto_Threshold diff --git a/skimage/measure/find_contours.py b/skimage/measure/find_contours.py index 298c09de..d36c2110 100755 --- a/skimage/measure/find_contours.py +++ b/skimage/measure/find_contours.py @@ -116,7 +116,7 @@ def find_contours(array, level, raise ValueError('Parameters "fully_connected" and' ' "positive_orientation" must be either "high" or "low".') point_list = _find_contours.iterate_and_store(array, level, - fully_connected == 'high') + fully_connected == 'high') contours = _assemble_contours(_take_2(point_list)) if positive_orientation == 'high': contours = [c[::-1] for c in contours] From 4a7d2e8429f258b5c3f4f359732a4e0c09a9af3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Wed, 2 Oct 2013 18:11:03 +0200 Subject: [PATCH 648/736] PEP8 --- skimage/morphology/selem.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/skimage/morphology/selem.py b/skimage/morphology/selem.py index 4df52e94..7e566773 100644 --- a/skimage/morphology/selem.py +++ b/skimage/morphology/selem.py @@ -172,10 +172,10 @@ def octahedron(radius, dtype=np.uint8): """ # note that in contrast to diamond(), this method allows non-integer radii n = 2 * radius + 1 - Z, Y, X = np.mgrid[ -radius:radius:n*1j, - -radius:radius:n*1j, - -radius:radius:n*1j] - s = np.abs(X) + np.abs(Y) + np.abs(Z) + Z, Y, X = np.mgrid[-radius:radius:n*1j, + -radius:radius:n*1j, + -radius:radius:n*1j] + s = np.abs(X) + np.abs(Y) + np.abs(Z) return np.array(s <= radius, dtype=dtype) @@ -203,9 +203,9 @@ def ball(radius, dtype=np.uint8): are 1 and 0 otherwise. """ n = 2 * radius + 1 - Z, Y, X = np.mgrid[ -radius:radius:n*1j, - -radius:radius:n*1j, - -radius:radius:n*1j] + Z, Y, X = np.mgrid[-radius:radius:n*1j, + -radius:radius:n*1j, + -radius:radius:n*1j] s = X**2 + Y**2 + Z**2 return np.array(s <= radius * radius, dtype=dtype) @@ -240,9 +240,9 @@ def octagon(m, n, dtype=np.uint8): selem = np.zeros((m + 2*n, m + 2*n)) selem[0, n] = 1 selem[n, 0] = 1 - selem[0, m + n -1] = 1 + selem[0, m + n - 1] = 1 selem[m + n - 1, 0] = 1 - selem[-1, n] = 1 + selem[-1, n] = 1 selem[n, -1] = 1 selem[-1, m + n - 1] = 1 selem[m + n - 1, -1] = 1 From a1373269e60fc96f6abfb05e6e3d27ef258ed02e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Wed, 2 Oct 2013 18:28:14 +0200 Subject: [PATCH 649/736] DOC: fix indent --- skimage/filter/rank/generic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/filter/rank/generic.py b/skimage/filter/rank/generic.py index 5ed9a46b..50c9a370 100644 --- a/skimage/filter/rank/generic.py +++ b/skimage/filter/rank/generic.py @@ -250,7 +250,7 @@ def maximum(image, selem, out=None, mask=None, shift_x=False, shift_y=False): Note ---- * the lower algorithm complexity makes the rank.maximum() more efficient - for larger images and structuring elements + for larger images and structuring elements """ From 9e00e24f81a4fefaa908b842a2a44cf56e3d5899 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Thu, 3 Oct 2013 00:33:57 +0200 Subject: [PATCH 650/736] Update PR plotter. --- doc/tools/plot_pr.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/doc/tools/plot_pr.py b/doc/tools/plot_pr.py index 08f4fc0e..60f5de54 100644 --- a/doc/tools/plot_pr.py +++ b/doc/tools/plot_pr.py @@ -25,7 +25,9 @@ releases = OrderedDict([ ('0.3', u'2011-10-10 03:28:47 -0700'), ('0.4', u'2011-12-03 14:31:32 -0800'), ('0.5', u'2012-02-26 21:00:51 -0800'), - ('0.6', u'2012-06-24 21:37:05 -0700')]) + ('0.6', u'2012-06-24 21:37:05 -0700'), + ('0.7', u'2012-09-29 18:08:49 -0700'), + ('0.8', u'2013-03-04 20:46:09 +0100')]) month_duration = 24 @@ -106,7 +108,7 @@ this_month = datetime(year=now.year, month=now.month, day=1, bins = [this_month - relativedelta(months=i) \ for i in reversed(range(-1, month_duration))] bins = seconds_from_epoch(bins) -plt.hist(dates_f, bins=bins) +n, bins, _ = plt.hist(dates_f, bins=bins) ax = plt.gca() ax.xaxis.set_major_formatter(FuncFormatter(date_formatter)) @@ -124,10 +126,15 @@ for version, date in releases.items(): plt.title('Pull request activity').set_y(1.05) plt.xlabel('Date') -plt.ylabel('PRs created') +plt.ylabel('PRs per month') plt.legend(loc=2, title='Release') plt.subplots_adjust(top=0.875, bottom=0.225) +import numpy as np +ax2 = plt.twinx() +ax2.plot(bins[:-1], np.cumsum(n), 'black', linewidth=2) +ax2.set_ylabel('Total PRs') + plt.savefig('PRs.png') plt.show() From 5ff79a795df33a8ccac15ea3c5ce59a3f2970817 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Thu, 3 Oct 2013 06:38:10 +0200 Subject: [PATCH 651/736] Correctly plot cumulative sum. --- doc/tools/plot_pr.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/doc/tools/plot_pr.py b/doc/tools/plot_pr.py index 60f5de54..96860e7f 100644 --- a/doc/tools/plot_pr.py +++ b/doc/tools/plot_pr.py @@ -131,8 +131,11 @@ plt.legend(loc=2, title='Release') plt.subplots_adjust(top=0.875, bottom=0.225) import numpy as np +cumulative = np.cumsum(n) +cumulative += len(dates) - cumulative[-1] + ax2 = plt.twinx() -ax2.plot(bins[:-1], np.cumsum(n), 'black', linewidth=2) +ax2.plot(bins[:-1], cumulative, 'black', linewidth=2) ax2.set_ylabel('Total PRs') plt.savefig('PRs.png') From 18438299179cb6598745a13ace1ceaa52a677d41 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Thu, 3 Oct 2013 16:08:29 +1000 Subject: [PATCH 652/736] Add relabel_sequential, deprecate relabel_from_one --- skimage/segmentation/_join.py | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/skimage/segmentation/_join.py b/skimage/segmentation/_join.py index 1eda9176..ce440b23 100644 --- a/skimage/segmentation/_join.py +++ b/skimage/segmentation/_join.py @@ -1,4 +1,5 @@ import numpy as np +from skimage._shared.utils import deprecated def join_segmentations(s1, s2): @@ -42,9 +43,18 @@ def join_segmentations(s1, s2): return j +@deprecated('relabel_sequential') def relabel_from_one(label_field): """Convert labels in an arbitrary label field to {1, ... number_of_labels}. + This function is deprecated, see ``relabel_sequential`` for more. + """ + return relabel_sequential(label_field, offset=1) + + +def relabel_sequential(label_field, offset=1): + """Relabel arbitrary labels to {`offset`, ... `offset` + number_of_labels}. + This function also returns the forward map (mapping the original labels to the reduced labels) and the inverse map (mapping the reduced labels back to the original ones). @@ -52,6 +62,10 @@ def relabel_from_one(label_field): Parameters ---------- label_field : numpy array of int, arbitrary shape + An array of labels. + offset : int, optional + The return labels will start at `offset`, which should be + strictly positive. Returns ------- @@ -62,13 +76,15 @@ def relabel_from_one(label_field): The map from the original label space to the returned label space. Can be used to re-apply the same mapping. See examples for usage. - inverse_map : numpy array of int, shape ``(len(np.unique(label_field)),)`` + inverse_map : 1D numpy array of int, of length offset + number of labels The map from the new label space to the original space. This can be used to reconstruct the original label field from the relabeled one. Notes ----- + The label 0 is assumed to denote the background and is never remapped. + The forward map can be extremely big for some inputs, since its length is given by the maximum of the label field. However, in most situations, ``label_field.max()`` is much smaller than @@ -79,7 +95,7 @@ def relabel_from_one(label_field): -------- >>> from skimage.segmentation import relabel_from_one >>> label_field = array([1, 1, 5, 5, 8, 99, 42]) - >>> relab, fw, inv = relabel_from_one(label_field) + >>> relab, fw, inv = relabel_sequential(label_field) >>> relab array([1, 1, 2, 2, 3, 5, 4]) >>> fw @@ -94,6 +110,9 @@ def relabel_from_one(label_field): True >>> (inv[relab] == label_field).all() True + >>> relab, fw, inv = relabel_sequential(label_field, offset=5) + >>> relab + array([5, 5, 6, 6, 7, 9, 8]) """ labels = np.unique(label_field) labels0 = labels[labels != 0] @@ -101,9 +120,11 @@ def relabel_from_one(label_field): if m == len(labels0): # nothing to do, already 1...n labels return label_field, labels, labels forward_map = np.zeros(m+1, int) - forward_map[labels0] = np.arange(1, len(labels0) + 1) + forward_map[labels0] = np.arange(offset, offset + len(labels0) + 1) if not (labels == 0).any(): labels = np.concatenate(([0], labels)) - inverse_map = labels - return forward_map[label_field], forward_map, inverse_map + inverse_map = np.zeros(offset - 1 + len(labels), dtype=np.intp) + inverse_map[(offset - 1):] = labels + relabeled = forward_map[label_field] + return relabeled, forward_map, inverse_map From 37f66f769b748d8f78838accf5033ede4dba4a83 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Thu, 3 Oct 2013 16:09:27 +1000 Subject: [PATCH 653/736] Add relabel_sequential to __init__ import --- skimage/segmentation/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/skimage/segmentation/__init__.py b/skimage/segmentation/__init__.py index 107111bd..aea6c70f 100644 --- a/skimage/segmentation/__init__.py +++ b/skimage/segmentation/__init__.py @@ -4,7 +4,7 @@ from .slic_superpixels import slic from ._quickshift import quickshift from .boundaries import find_boundaries, visualize_boundaries, mark_boundaries from ._clear_border import clear_border -from ._join import join_segmentations, relabel_from_one +from ._join import join_segmentations, relabel_from_one, relabel_sequential __all__ = ['random_walker', @@ -16,4 +16,5 @@ __all__ = ['random_walker', 'mark_boundaries', 'clear_border', 'join_segmentations', - 'relabel_from_one'] + 'relabel_from_one', + 'relabel_sequential'] From bfab4931337fd09d6780244c000bf42285de4fb5 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Thu, 3 Oct 2013 16:11:52 +1000 Subject: [PATCH 654/736] Add new tests for relabel_sequential --- skimage/segmentation/tests/test_join.py | 31 ++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/skimage/segmentation/tests/test_join.py b/skimage/segmentation/tests/test_join.py index f03244e9..548fcc8d 100644 --- a/skimage/segmentation/tests/test_join.py +++ b/skimage/segmentation/tests/test_join.py @@ -1,6 +1,6 @@ import numpy as np from numpy.testing import assert_array_equal, assert_raises -from skimage.segmentation import join_segmentations, relabel_from_one +from skimage.segmentation import join_segmentations, relabel_sequential def test_join_segmentations(): s1 = np.array([[0, 0, 1, 1], @@ -24,9 +24,10 @@ def test_join_segmentations(): s3 = np.array([[0, 0, 1, 1], [0, 2, 2, 1]]) assert_raises(ValueError, join_segmentations, s1, s3) -def test_relabel_from_one(): + +def test_relabel_sequential_offset1(): ar = np.array([1, 1, 5, 5, 8, 99, 42]) - ar_relab, fw, inv = relabel_from_one(ar) + ar_relab, fw, inv = relabel_sequential(ar) ar_relab_ref = np.array([1, 1, 2, 2, 3, 5, 4]) assert_array_equal(ar_relab, ar_relab_ref) fw_ref = np.zeros(100, int) @@ -36,5 +37,29 @@ def test_relabel_from_one(): assert_array_equal(inv, inv_ref) +def test_relabel_sequential_offset5(): + ar = np.array([1, 1, 5, 5, 8, 99, 42]) + ar_relab, fw, inv = relabel_sequential(ar, offset=5) + ar_relab_ref = np.array([5, 5, 6, 6, 7, 9, 8]) + assert_array_equal(ar_relab, ar_relab_ref) + fw_ref = np.zeros(100, int) + fw_ref[1] = 5; fw_ref[5] = 6; fw_ref[8] = 7; fw_ref[42] = 8; fw_ref[99] = 9 + assert_array_equal(fw, fw_ref) + inv_ref = np.array([0, 0, 0, 0, 0, 1, 5, 8, 42, 99]) + assert_array_equal(inv, inv_ref) + + +def test_relabel_sequential_offset5_with0(): + ar = np.array([1, 1, 5, 5, 8, 99, 42, 0]) + ar_relab, fw, inv = relabel_sequential(ar, offset=5) + ar_relab_ref = np.array([5, 5, 6, 6, 7, 9, 8, 0]) + assert_array_equal(ar_relab, ar_relab_ref) + fw_ref = np.zeros(100, int) + fw_ref[1] = 5; fw_ref[5] = 6; fw_ref[8] = 7; fw_ref[42] = 8; fw_ref[99] = 9 + assert_array_equal(fw, fw_ref) + inv_ref = np.array([0, 0, 0, 0, 0, 1, 5, 8, 42, 99]) + assert_array_equal(inv, inv_ref) + + if __name__ == "__main__": np.testing.run_module_suite() From a8488bfecd4c80b33b67bdb792f15954bb983795 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Thu, 3 Oct 2013 16:14:30 +1000 Subject: [PATCH 655/736] Remove deprecated use of relabel_from_one --- skimage/segmentation/_join.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/skimage/segmentation/_join.py b/skimage/segmentation/_join.py index ce440b23..bb2268b3 100644 --- a/skimage/segmentation/_join.py +++ b/skimage/segmentation/_join.py @@ -5,9 +5,9 @@ from skimage._shared.utils import deprecated def join_segmentations(s1, s2): """Return the join of the two input segmentations. - The join J of S1 and S2 is defined as the segmentation in which two voxels - are in the same segment in J if and only if they are in the same segment - in *both* S1 and S2. + The join J of S1 and S2 is defined as the segmentation in which two + voxels are in the same segment if and only if they are in the same + segment in *both* S1 and S2. Parameters ---------- @@ -36,10 +36,10 @@ def join_segmentations(s1, s2): if s1.shape != s2.shape: raise ValueError("Cannot join segmentations of different shape. " + "s1.shape: %s, s2.shape: %s" % (s1.shape, s2.shape)) - s1 = relabel_from_one(s1)[0] - s2 = relabel_from_one(s2)[0] + s1 = relabel_sequential(s1)[0] + s2 = relabel_sequential(s2)[0] j = (s2.max() + 1) * s1 + s2 - j = relabel_from_one(j)[0] + j = relabel_sequential(j)[0] return j From a9afb241cc915b3b06da77898a2b4bfcf7554c45 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Thu, 3 Oct 2013 16:23:30 +1000 Subject: [PATCH 656/736] Rename deprecated relabel_from_one in docstring example --- skimage/segmentation/_join.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/segmentation/_join.py b/skimage/segmentation/_join.py index bb2268b3..462bb18f 100644 --- a/skimage/segmentation/_join.py +++ b/skimage/segmentation/_join.py @@ -93,7 +93,7 @@ def relabel_sequential(label_field, offset=1): Examples -------- - >>> from skimage.segmentation import relabel_from_one + >>> from skimage.segmentation import relabel_sequential >>> label_field = array([1, 1, 5, 5, 8, 99, 42]) >>> relab, fw, inv = relabel_sequential(label_field) >>> relab From a15312e3c882cd5a425faad46f06f38f123020ad Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Fri, 4 Oct 2013 15:21:50 +1000 Subject: [PATCH 657/736] Only raise umfpack warning if cg mode is used --- skimage/segmentation/random_walker_segmentation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/segmentation/random_walker_segmentation.py b/skimage/segmentation/random_walker_segmentation.py index 9ef3a8fc..6ab7e1ce 100644 --- a/skimage/segmentation/random_walker_segmentation.py +++ b/skimage/segmentation/random_walker_segmentation.py @@ -324,7 +324,7 @@ def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True, """ - if UmfpackContext is None: + if UmfpackContext is None and mode == 'cg': warnings.warn('SciPy was built without UMFPACK. Consider rebuilding ' 'SciPy with UMFPACK, this will greatly speed up the ' 'random walker functions. You may also install pyamg ' From fc15f75f8dc45302a411307896255822f408a92c Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Sat, 5 Oct 2013 14:27:51 +1000 Subject: [PATCH 658/736] Monkey-patch UmfpackContext __del__ By putting the failing statement in a try: except: clause, the exception is caught and the error message is silenced. See https://groups.google.com/d/msg/scikit-image/FrM5IGP6wh4/1hp-FtVZmfcJ and http://stackoverflow.com/questions/13977970/ignore-exceptions-printed-to-stderr-in-del/13977992?noredirect=1#comment28386412_13977992 --- skimage/segmentation/random_walker_segmentation.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/skimage/segmentation/random_walker_segmentation.py b/skimage/segmentation/random_walker_segmentation.py index 6ab7e1ce..e71e7f09 100644 --- a/skimage/segmentation/random_walker_segmentation.py +++ b/skimage/segmentation/random_walker_segmentation.py @@ -9,14 +9,25 @@ significantly the performance. """ import warnings +import sys +import os import numpy as np from scipy import sparse, ndimage + try: from scipy.sparse.linalg.dsolve import umfpack + old_del = umfpack.UmfpackContext.__del__ + def new_del(self): + try: + old_del(self) + except AttributeError: + pass + umfpack.UmfpackContext.__del__ = new_del UmfpackContext = umfpack.UmfpackContext() except: UmfpackContext = None + try: from pyamg import ruge_stuben_solver amg_loaded = True From 248ac46dae4638fe57aeaafdb2a4ee4d9e7bf696 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Mon, 7 Oct 2013 13:25:13 +1100 Subject: [PATCH 659/736] Remove unused imports in random_walker source --- skimage/segmentation/random_walker_segmentation.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/skimage/segmentation/random_walker_segmentation.py b/skimage/segmentation/random_walker_segmentation.py index e71e7f09..9079dda9 100644 --- a/skimage/segmentation/random_walker_segmentation.py +++ b/skimage/segmentation/random_walker_segmentation.py @@ -9,8 +9,6 @@ significantly the performance. """ import warnings -import sys -import os import numpy as np from scipy import sparse, ndimage From fb86bac3504c08ef43e12b96652990ec097dc19b Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Mon, 7 Oct 2013 13:33:17 +1100 Subject: [PATCH 660/736] Add comment and links explaining Umfpack import --- skimage/segmentation/random_walker_segmentation.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/skimage/segmentation/random_walker_segmentation.py b/skimage/segmentation/random_walker_segmentation.py index 9079dda9..aff0c131 100644 --- a/skimage/segmentation/random_walker_segmentation.py +++ b/skimage/segmentation/random_walker_segmentation.py @@ -13,6 +13,12 @@ import warnings import numpy as np from scipy import sparse, ndimage +# executive summary for next code block: try to import umfpack from +# scipy, but make sure not to raise a fuss if it fails since it's only +# needed to speed up a few cases. +# See discussions at: +# https://groups.google.com/d/msg/scikit-image/FrM5IGP6wh4/1hp-FtVZmfcJ +# http://stackoverflow.com/questions/13977970/ignore-exceptions-printed-to-stderr-in-del/13977992?noredirect=1#comment28386412_13977992 try: from scipy.sparse.linalg.dsolve import umfpack old_del = umfpack.UmfpackContext.__del__ From bdafd1e3d6ae13941d916a422f1dea45301cb6e0 Mon Sep 17 00:00:00 2001 From: Kemal Eren Date: Mon, 7 Oct 2013 16:33:39 -0700 Subject: [PATCH 661/736] ENH: optimized label2rgb() --- skimage/color/colorlabel.py | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/skimage/color/colorlabel.py b/skimage/color/colorlabel.py index d6379037..3eb900d4 100644 --- a/skimage/color/colorlabel.py +++ b/skimage/color/colorlabel.py @@ -66,7 +66,7 @@ def label2rgb(label, image=None, colors=None, alpha=0.3, colors = [_rgb_vector(c) for c in colors] if image is None: - colorized = np.zeros(label.shape + (3,), dtype=np.float64) + img_layer = np.zeros(label.shape + (3,), dtype=np.float64) # Opacity doesn't make sense if no image exists. alpha = 1 else: @@ -77,11 +77,19 @@ def label2rgb(label, image=None, colors=None, alpha=0.3, warnings.warn("Negative intensities in `image` are not supported") image = img_as_float(rgb2gray(image)) - colorized = gray2rgb(image) * image_alpha + (1 - image_alpha) + img_layer = gray2rgb(image) * image_alpha + (1 - image_alpha) + + # need to ensure that all labels are >= 0 + offset = label.min() + if offset < 0: + label += abs(offset) + bg_label += offset labels = list(set(label.flat)) color_cycle = itertools.cycle(colors) + remove_background = bg_label in labels and bg_color is None + if bg_label in labels: labels.remove(bg_label) if bg_color is not None: @@ -89,8 +97,15 @@ def label2rgb(label, image=None, colors=None, alpha=0.3, bg_color = _rgb_vector(bg_color) color_cycle = itertools.chain(bg_color, color_cycle) - for c, i in zip(color_cycle, labels): - mask = (label == i) - colorized[mask] = c * alpha + colorized[mask] * (1 - alpha) + label_to_color = np.zeros((max(labels) + 1, 3)) + for lab, c in zip(labels, color_cycle): + label_to_color[lab] = c + + label_layer = label_to_color[label] + result = label_layer * alpha + img_layer * (1 - alpha) - return colorized + # remove background label if its color was not specified + if remove_background: + result[label == bg_label] = img_layer[label == bg_label] + + return result From bfaf89e2f35e87b022c1e55cfa8fde3feb22d990 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Tue, 8 Oct 2013 13:23:50 +1100 Subject: [PATCH 662/736] Deprecate default mode 'bf' in random_walker --- skimage/segmentation/random_walker_segmentation.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/skimage/segmentation/random_walker_segmentation.py b/skimage/segmentation/random_walker_segmentation.py index aff0c131..c475dcb7 100644 --- a/skimage/segmentation/random_walker_segmentation.py +++ b/skimage/segmentation/random_walker_segmentation.py @@ -187,7 +187,7 @@ def _build_laplacian(data, mask=None, beta=50, depth=1., multichannel=False): #----------- Random walker algorithm -------------------------------- -def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True, +def random_walker(data, labels, beta=130, mode=None, tol=1.e-3, copy=True, multichannel=False, return_full_prob=False, depth=1.): """Random walker algorithm for segmentation from markers. @@ -339,6 +339,12 @@ def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True, """ + if mode is None: + mode = 'bf' + warnings.warn("Default mode will change in the next release from 'bf' " + "to 'cg_mg' if pyamg is installed, else to 'cg' if " + "SciPy was built with UMFPACK, or to 'bf' otherwise.") + if UmfpackContext is None and mode == 'cg': warnings.warn('SciPy was built without UMFPACK. Consider rebuilding ' 'SciPy with UMFPACK, this will greatly speed up the ' From 0680f59fe170ef61934de0f447f48d269b28ce83 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Tue, 8 Oct 2013 13:28:39 +1100 Subject: [PATCH 663/736] Add change in random_walker default mode to TODO --- TODO.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/TODO.txt b/TODO.txt index 502961c8..db7ffe48 100644 --- a/TODO.txt +++ b/TODO.txt @@ -4,6 +4,8 @@ Version 0.10 * Remove deprecated parameter `epsilon` of `skimage.viewer.LineProfile` * Remove backwards-compatability of `skimage.measure.regionprops` * Remove {`ratio`, `sigma`} deprecation warnings of `skimage.segmentation.slic` +* Change default mode of random_walker segmentation to 'cg_mg' > 'cg' > 'bf', + depending on which optional dependencies are available. Version 0.9 ----------- From bfe70fe0919e5892b190a0e42fb42ef78260f6fb Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Tue, 8 Oct 2013 14:18:45 +1100 Subject: [PATCH 664/736] Update SLIC in example to most recent interface SLIC has been updated a few times since this example was created, adding support for grayscale images (so converting to RGB is no longer necessary) and changing the keyword "ratio" to "compactness". This commit brings the example up to date. --- doc/examples/plot_join_segmentations.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/doc/examples/plot_join_segmentations.py b/doc/examples/plot_join_segmentations.py index 2cafab15..facd5171 100644 --- a/doc/examples/plot_join_segmentations.py +++ b/doc/examples/plot_join_segmentations.py @@ -20,7 +20,6 @@ from skimage.morphology import watershed from skimage.color import label2rgb from skimage import data - coins = data.coins() # make segmentation using edge-detection and watershed @@ -34,11 +33,8 @@ ws = watershed(edges, markers) seg1 = nd.label(ws == foreground)[0] # make segmentation using SLIC superpixels - -# make the RGB equivalent of `coins` -coins_colour = np.tile(coins[..., np.newaxis], (1, 1, 3)) -seg2 = slic(coins_colour, n_segments=30, max_iter=160, sigma=1, ratio=9, - convert2lab=False) +seg2 = slic(coins, n_segments=117, max_iter=160, sigma=1, compactness=0.75, + multichannel=False) # combine the two segj = join_segmentations(seg1, seg2) From 794176b7c421bd8014bc25dcc58078c8ea8bc75b Mon Sep 17 00:00:00 2001 From: Kemal Eren Date: Mon, 7 Oct 2013 20:53:48 -0700 Subject: [PATCH 665/736] fixed error in label2rgb() when label is a binary array --- skimage/color/colorlabel.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/skimage/color/colorlabel.py b/skimage/color/colorlabel.py index 3eb900d4..b87af980 100644 --- a/skimage/color/colorlabel.py +++ b/skimage/color/colorlabel.py @@ -79,11 +79,15 @@ def label2rgb(label, image=None, colors=None, alpha=0.3, image = img_as_float(rgb2gray(image)) img_layer = gray2rgb(image) * image_alpha + (1 - image_alpha) - # need to ensure that all labels are >= 0 + # need to ensure that all labels are ints >= 0 offset = label.min() - if offset < 0: - label += abs(offset) - bg_label += offset + if offset != 0: + label -= offset + bg_label -= offset + new_type = np.min_scalar_type(label.max()) + if new_type == np.bool: + new_type = np.uint8 + label = label.astype(new_type) labels = list(set(label.flat)) color_cycle = itertools.cycle(colors) @@ -100,7 +104,7 @@ def label2rgb(label, image=None, colors=None, alpha=0.3, label_to_color = np.zeros((max(labels) + 1, 3)) for lab, c in zip(labels, color_cycle): label_to_color[lab] = c - + label_layer = label_to_color[label] result = label_layer * alpha + img_layer * (1 - alpha) From 4db1e1b83c5c2b2573dcc4d4124cf0ddafb674c3 Mon Sep 17 00:00:00 2001 From: Kemal Eren Date: Mon, 7 Oct 2013 23:17:52 -0700 Subject: [PATCH 666/736] handle case when there are no labels --- skimage/color/colorlabel.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/skimage/color/colorlabel.py b/skimage/color/colorlabel.py index b87af980..9b878ecc 100644 --- a/skimage/color/colorlabel.py +++ b/skimage/color/colorlabel.py @@ -101,6 +101,9 @@ def label2rgb(label, image=None, colors=None, alpha=0.3, bg_color = _rgb_vector(bg_color) color_cycle = itertools.chain(bg_color, color_cycle) + if len(labels) == 0: + return img_layer + label_to_color = np.zeros((max(labels) + 1, 3)) for lab, c in zip(labels, color_cycle): label_to_color[lab] = c From 6cf12ac0d88545727cf7d983b5f212163f0e6140 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Wed, 9 Oct 2013 09:28:38 +0000 Subject: [PATCH 667/736] Fix join_segmentations example using img_as_float data.coins() returns a ubyte image in Python 2.7.x but a float32 image in Python 3.x. This ensures that the image always has the same type for the rest of the example. --- doc/examples/plot_join_segmentations.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/doc/examples/plot_join_segmentations.py b/doc/examples/plot_join_segmentations.py index facd5171..625113e5 100644 --- a/doc/examples/plot_join_segmentations.py +++ b/doc/examples/plot_join_segmentations.py @@ -18,16 +18,16 @@ from skimage.filter import sobel from skimage.segmentation import slic, join_segmentations from skimage.morphology import watershed from skimage.color import label2rgb -from skimage import data +from skimage import data, img_as_float -coins = data.coins() +coins = img_as_float(data.coins()) # make segmentation using edge-detection and watershed edges = sobel(coins) markers = np.zeros_like(coins) foreground, background = 1, 2 -markers[coins < 30] = background -markers[coins > 150] = foreground +markers[coins < 30.0 / 255] = background +markers[coins > 150.0 / 255] = foreground ws = watershed(edges, markers) seg1 = nd.label(ws == foreground)[0] @@ -60,3 +60,4 @@ for ax in axes: ax.axis('off') plt.subplots_adjust(hspace=0.01, wspace=0.01, top=1, bottom=0, left=0, right=1) plt.show() + From ea357a446466b3470090f75aee983ca60686c761 Mon Sep 17 00:00:00 2001 From: cgohlke Date: Thu, 10 Oct 2013 21:16:14 -0700 Subject: [PATCH 668/736] BUG: basestring not defined on PY3 --- skimage/io/_plugins/pil_plugin.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/skimage/io/_plugins/pil_plugin.py b/skimage/io/_plugins/pil_plugin.py index 3ce79c47..2ddbfe26 100644 --- a/skimage/io/_plugins/pil_plugin.py +++ b/skimage/io/_plugins/pil_plugin.py @@ -11,6 +11,8 @@ except ImportError: from skimage.util import img_as_ubyte +from skimage._shared import six + def imread(fname, dtype=None): """Load an image from file. @@ -104,7 +106,7 @@ def imsave(fname, arr, format_str=None): arr = arr.astype(np.uint8) # default to PNG if file-like object - if not isinstance(fname, basestring) and format_str is None: + if not isinstance(fname, six.string_types) and format_str is None: format_str = "PNG" img = Image.fromstring(mode, (arr.shape[1], arr.shape[0]), arr.tostring()) From be3be537e6672b6d5e4769db4dd36bc0310ce82d Mon Sep 17 00:00:00 2001 From: cgohlke Date: Thu, 10 Oct 2013 21:22:40 -0700 Subject: [PATCH 669/736] TST: use BytesIO to save images into file-like object StringIO does not work on Python 3 --- skimage/io/tests/test_pil.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skimage/io/tests/test_pil.py b/skimage/io/tests/test_pil.py index 61ec5ce3..aa582ebc 100644 --- a/skimage/io/tests/test_pil.py +++ b/skimage/io/tests/test_pil.py @@ -8,7 +8,7 @@ from tempfile import NamedTemporaryFile from skimage import data_dir from skimage.io import (imread, imsave, use_plugin, reset_plugins, Image as ioImage) -from skimage._shared.six.moves import StringIO +from skimage._shared.six import BytesIO try: @@ -132,7 +132,7 @@ class TestSave: def test_imsave_filelike(): shape = (2, 2) image = np.zeros(shape) - s = StringIO() + s = BytesIO() # save to file-like object imsave(s, image) From c519f60285ba3f94ad3489a745af60f0f69aa293 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Thu, 10 Oct 2013 23:26:02 -0500 Subject: [PATCH 670/736] Add support for consistent color labels for sparse labels. --- skimage/color/colorlabel.py | 70 ++++++++++++++++---------- skimage/color/tests/test_colorlabel.py | 12 +++++ 2 files changed, 55 insertions(+), 27 deletions(-) diff --git a/skimage/color/colorlabel.py b/skimage/color/colorlabel.py index 9b878ecc..8d7787aa 100644 --- a/skimage/color/colorlabel.py +++ b/skimage/color/colorlabel.py @@ -33,8 +33,32 @@ def _rgb_vector(color): """ if isinstance(color, six.string_types): color = color_dict[color] - # slice to handle RGBA colors - return np.array(color[:3]).reshape(1, 3) + # Slice to handle RGBA colors. + return np.array(color[:3]) + + +def _match_label_with_color(label, colors, bg_label, bg_color): + """Return `unique_labels` and `color_cycle` for label array and color list. + + Colors are cycled for normal labels, but the background color should only + be used for the background. + """ + # Temporarily set background color; it will be removed later. + if bg_color is None: + bg_color = (0, 0, 0) + bg_color = _rgb_vector([bg_color]) + + unique_labels = list(set(label.flat)) + # Ensure that the background label is in front to match call to `chain`. + if bg_label in unique_labels: + unique_labels.remove(bg_label) + unique_labels.insert(0, bg_label) + + # Modify labels and color cycle so background color is used only once. + color_cycle = itertools.cycle(colors) + color_cycle = itertools.chain(bg_color, color_cycle) + + return unique_labels, color_cycle def label2rgb(label, image=None, colors=None, alpha=0.3, @@ -66,7 +90,7 @@ def label2rgb(label, image=None, colors=None, alpha=0.3, colors = [_rgb_vector(c) for c in colors] if image is None: - img_layer = np.zeros(label.shape + (3,), dtype=np.float64) + image = np.zeros(label.shape + (3,), dtype=np.float64) # Opacity doesn't make sense if no image exists. alpha = 1 else: @@ -77,42 +101,34 @@ def label2rgb(label, image=None, colors=None, alpha=0.3, warnings.warn("Negative intensities in `image` are not supported") image = img_as_float(rgb2gray(image)) - img_layer = gray2rgb(image) * image_alpha + (1 - image_alpha) + image = gray2rgb(image) * image_alpha + (1 - image_alpha) - # need to ensure that all labels are ints >= 0 - offset = label.min() + # Ensure that all labels are non-negative so we can index into + # `label_to_color` correctly. + offset = min(label.min(), bg_label) if offset != 0: - label -= offset + label = label - offset # Make sure you don't modify the input array. bg_label -= offset + new_type = np.min_scalar_type(label.max()) if new_type == np.bool: new_type = np.uint8 label = label.astype(new_type) - labels = list(set(label.flat)) - color_cycle = itertools.cycle(colors) + unique_labels, color_cycle = _match_label_with_color(label, colors, + bg_label, bg_color) - remove_background = bg_label in labels and bg_color is None + if len(unique_labels) == 0: + return image - if bg_label in labels: - labels.remove(bg_label) - if bg_color is not None: - labels.insert(0, bg_label) - bg_color = _rgb_vector(bg_color) - color_cycle = itertools.chain(bg_color, color_cycle) + dense_labels = range(max(unique_labels) + 1) + label_to_color = np.array([c for i, c in zip(dense_labels, color_cycle)]) - if len(labels) == 0: - return img_layer + result = label_to_color[label] * alpha + image * (1 - alpha) - label_to_color = np.zeros((max(labels) + 1, 3)) - for lab, c in zip(labels, color_cycle): - label_to_color[lab] = c - - label_layer = label_to_color[label] - result = label_layer * alpha + img_layer * (1 - alpha) - - # remove background label if its color was not specified + # Remove background label if its color was not specified. + remove_background = bg_label in unique_labels and bg_color is None if remove_background: - result[label == bg_label] = img_layer[label == bg_label] + result[label == bg_label] = image[label == bg_label] return result diff --git a/skimage/color/tests/test_colorlabel.py b/skimage/color/tests/test_colorlabel.py index fa6ffcf3..ab1fc894 100644 --- a/skimage/color/tests/test_colorlabel.py +++ b/skimage/color/tests/test_colorlabel.py @@ -69,6 +69,18 @@ def test_bg_and_color_cycle(): assert_close(pixel, color) +def test_label_consistency(): + """Assert that the same labels map to the same colors.""" + label_1 = np.arange(5).reshape(1, -1) + label_2 = np.array([2, 4]) + colors = [(1, 0, 0), (0, 1, 0), (0, 0, 1), (1, 1, 0), (1, 0, 1)] + # Set alphas just in case the defaults change + rgb_1 = label2rgb(label_1, colors=colors) + rgb_2 = label2rgb(label_2, colors=colors) + for label_id in label_2.flat: + assert_close(rgb_1[label_1 == label_id], rgb_2[label_2 == label_id]) + + if __name__ == '__main__': testing.run_module_suite() From 458556723dae3fe670ee380de4833361ad02dfae Mon Sep 17 00:00:00 2001 From: cgohlke Date: Thu, 10 Oct 2013 21:48:29 -0700 Subject: [PATCH 671/736] BUG: Fix ValueError: Buffer dtype mismatch, expected 'long' but got 'long long' on win-amd64 --- skimage/transform/_hough_transform.pyx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/transform/_hough_transform.pyx b/skimage/transform/_hough_transform.pyx index a3aa2649..e5fd3ed8 100644 --- a/skimage/transform/_hough_transform.pyx +++ b/skimage/transform/_hough_transform.pyx @@ -149,7 +149,7 @@ def hough_ellipse(cnp.ndarray img, int threshold=4, double accuracy=1, if img.ndim != 2: raise ValueError('The input image must be 2D.') - cdef long[:, :] pixels = np.transpose(np.nonzero(img)) + cdef Py_ssize_t[:, :] pixels = np.transpose(np.nonzero(img)) cdef Py_ssize_t num_pixels = pixels.shape[0] cdef list acc = list() cdef list results = list() From 4d46bc0912129d02a8d09b0d58ae1dfab2c8c48c Mon Sep 17 00:00:00 2001 From: cgohlke Date: Thu, 10 Oct 2013 21:51:56 -0700 Subject: [PATCH 672/736] TST: Fix ValueError: Buffer dtype mismatch, expected 'intp_t' but got 'long' --- skimage/transform/tests/test_hough_transform.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/skimage/transform/tests/test_hough_transform.py b/skimage/transform/tests/test_hough_transform.py index 148f4d43..651861f3 100644 --- a/skimage/transform/tests/test_hough_transform.py +++ b/skimage/transform/tests/test_hough_transform.py @@ -122,7 +122,7 @@ def test_hough_circle(): y, x = circle_perimeter(y_0, x_0, radius) img[x, y] = 1 - out = tf.hough_circle(img, np.array([radius])) + out = tf.hough_circle(img, np.array([radius], dtype=np.intp)) x, y = np.where(out[0] == out[0].max()) assert_equal(x[0], x_0) @@ -138,7 +138,8 @@ def test_hough_circle_extended(): y, x = circle_perimeter(y_0, x_0, radius) img[x[np.where(x > 0)], y[np.where(x > 0)]] = 1 - out = tf.hough_circle(img, np.array([radius]), full_output=True) + out = tf.hough_circle(img, np.array([radius], dtype=np.intp), + full_output=True) x, y = np.where(out[0] == out[0].max()) # Offset for x_0, y_0 From de42ba831a125db2913545a364fd14989cb5db83 Mon Sep 17 00:00:00 2001 From: "Josh Warner (Mac)" Date: Fri, 11 Oct 2013 01:53:15 -0500 Subject: [PATCH 673/736] FIX: Fix and improve Poisson random noise generator The Poissson generator now works. The improved Poisson generator now infers the bit depth of the image after conversion to a floating point image, by analyzing the unique values present and finding the next power of two. This value is then used to scale the floating point image up, after which Poisson noise is generated, and then image is then scaled back down. --- skimage/util/noise.py | 37 +++++++++++++++++++------ skimage/util/tests/test_random_noise.py | 12 +++++++- 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/skimage/util/noise.py b/skimage/util/noise.py index d0a3c5f6..5238a2e6 100644 --- a/skimage/util/noise.py +++ b/skimage/util/noise.py @@ -5,6 +5,25 @@ from .dtype import img_as_float __all__ = ['random_noise'] +def _next_pow2(n): + """ + Returns next integer power of two. + """ + + next_pow = 0 + if n == np.inf: + return np.inf + + if n < 1: + raise ValueError("Unable to determine next power of two for %i" % (n)) + + while True: + if 2 ** next_pow >= n: + return next_pow + else: + next_pow += 1 + + def random_noise(image, mode='gaussian', seed=None, **kwargs): """ Function to add random noise of various types to a floating-point image. @@ -52,7 +71,7 @@ def random_noise(image, mode='gaussian', seed=None, **kwargs): allowedtypes = { 'gaussian': 'gaussian_values', - 'poisson': '', + 'poisson': 'poisson_values', 'salt': 'sp_values', 'pepper': 'sp_values', 's&p': 's&p_values', @@ -67,7 +86,8 @@ def random_noise(image, mode='gaussian', seed=None, **kwargs): allowedkwargs = { 'gaussian_values': ['mean', 'var'], 'sp_values': ['amount'], - 's&p_values': ['amount', 'salt_vs_pepper']} + 's&p_values': ['amount', 'salt_vs_pepper'], + 'poisson_values': []} for key in kwargs: if key not in allowedkwargs[allowedtypes[mode]]: @@ -84,13 +104,14 @@ def random_noise(image, mode='gaussian', seed=None, **kwargs): out = np.clip(image + noise, 0., 1.) elif mode == 'poisson': + # Determine unique values present in image + vals = len(np.unique(image)) + + # Calculate the next lowest power of two + vals = 2 ** _next_pow2(vals) + # Generating noise for each unique value in image. - out = np.zeros_like(image) - for val in np.unique(image): - # Generate mask for a unique value, replace w/values drawn from - # Poisson distribution about the unique value - mask = image == val - out[mask] = np.poisson(val, mask.sum()) + out = np.random.poisson(image * vals) / float(vals) elif mode == 'salt': # Re-call function with mode='s&p' and p=1 (all salt noise) diff --git a/skimage/util/tests/test_random_noise.py b/skimage/util/tests/test_random_noise.py index 87005d60..fd05084a 100644 --- a/skimage/util/tests/test_random_noise.py +++ b/skimage/util/tests/test_random_noise.py @@ -75,7 +75,7 @@ def test_gaussian(): def test_speckle(): seed = 42 data = np.zeros((128, 128)) + 0.1 - np.random.seed(seed=42) + np.random.seed(seed=seed) noise = np.random.normal(0.1, 0.02 ** 0.5, (128, 128)) expected = np.clip(data + data * noise, 0, 1) @@ -84,6 +84,16 @@ def test_speckle(): assert_allclose(expected, data_speckle) +def test_poisson(): + seed = 42 + data = camera() # 512x512 grayscale uint8 + cam_noisy = random_noise(data, mode='poisson', seed=seed) + + np.random.seed(seed=seed) + expected = np.random.poisson(img_as_float(data) * 256) / 256. + assert_allclose(cam_noisy, expected) + + def test_bad_mode(): data = np.zeros((64, 64)) assert_raises(KeyError, random_noise, data, 'perlin') From f10c362b1a88535b2d5fee3210102a8c6e3a1366 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Fri, 11 Oct 2013 17:34:47 +0200 Subject: [PATCH 674/736] Fix euler number bug for scipy-0.13 --- skimage/measure/_regionprops.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/skimage/measure/_regionprops.py b/skimage/measure/_regionprops.py index 9078df6f..e6fe5c5e 100644 --- a/skimage/measure/_regionprops.py +++ b/skimage/measure/_regionprops.py @@ -4,7 +4,7 @@ from math import sqrt, atan2, pi as PI import numpy as np from scipy import ndimage -from skimage.morphology import convex_hull_image +from skimage.morphology import convex_hull_image, label from skimage.measure import _moments @@ -155,8 +155,8 @@ class _RegionProperties(object): @_cached_property def euler_number(self): euler_array = self.filled_image != self.image - _, num = ndimage.label(euler_array, STREL_8) - return -num + _, num = label(euler_array, neighbors=8, return_num=True) + return -num + 1 @_cached_property def extent(self): From 4e9cb03aebdbc26fac60d835a7150ec9b997eb8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Fri, 11 Oct 2013 17:37:26 +0200 Subject: [PATCH 675/736] Add missing perimeter function to __all__ --- skimage/measure/_regionprops.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/measure/_regionprops.py b/skimage/measure/_regionprops.py index e6fe5c5e..10ac785d 100644 --- a/skimage/measure/_regionprops.py +++ b/skimage/measure/_regionprops.py @@ -8,7 +8,7 @@ from skimage.morphology import convex_hull_image, label from skimage.measure import _moments -__all__ = ['regionprops'] +__all__ = ['regionprops', 'perimeter'] STREL_4 = np.array([[0, 1, 0], From c2e4442eaff871e4ac205dcc35e4b13546aeb06a Mon Sep 17 00:00:00 2001 From: "Josh Warner (Mac)" Date: Fri, 11 Oct 2013 10:51:07 -0500 Subject: [PATCH 676/736] ENH: More concise next-power-of-2 calculation --- skimage/util/noise.py | 21 +-------------------- 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/skimage/util/noise.py b/skimage/util/noise.py index 5238a2e6..7d7c28d7 100644 --- a/skimage/util/noise.py +++ b/skimage/util/noise.py @@ -5,25 +5,6 @@ from .dtype import img_as_float __all__ = ['random_noise'] -def _next_pow2(n): - """ - Returns next integer power of two. - """ - - next_pow = 0 - if n == np.inf: - return np.inf - - if n < 1: - raise ValueError("Unable to determine next power of two for %i" % (n)) - - while True: - if 2 ** next_pow >= n: - return next_pow - else: - next_pow += 1 - - def random_noise(image, mode='gaussian', seed=None, **kwargs): """ Function to add random noise of various types to a floating-point image. @@ -108,7 +89,7 @@ def random_noise(image, mode='gaussian', seed=None, **kwargs): vals = len(np.unique(image)) # Calculate the next lowest power of two - vals = 2 ** _next_pow2(vals) + vals = 2 ** np.ceil(np.log2(vals)) # Generating noise for each unique value in image. out = np.random.poisson(image * vals) / float(vals) From a9995b6a70bc0ec274a8e272504c36f14183eaa0 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Fri, 11 Oct 2013 18:44:30 +0200 Subject: [PATCH 677/736] Test whether labels are left alone in label2rgb. --- skimage/color/tests/test_colorlabel.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/skimage/color/tests/test_colorlabel.py b/skimage/color/tests/test_colorlabel.py index ab1fc894..dcfbe4ea 100644 --- a/skimage/color/tests/test_colorlabel.py +++ b/skimage/color/tests/test_colorlabel.py @@ -3,7 +3,8 @@ import itertools import numpy as np from numpy import testing from skimage.color.colorlabel import label2rgb -from numpy.testing import assert_array_almost_equal as assert_close +from numpy.testing import (assert_array_almost_equal as assert_close, + assert_array_equal) def test_shape_mismatch(): @@ -80,6 +81,14 @@ def test_label_consistency(): for label_id in label_2.flat: assert_close(rgb_1[label_1 == label_id], rgb_2[label_2 == label_id]) +def test_leave_labels_alone(): + labels = np.array([-1, 0, 1]) + labels_saved = labels.copy() + + label2rgb(labels) + label2rgb(labels, bg_label=1) + assert_array_equal(labels, labels_saved) + if __name__ == '__main__': testing.run_module_suite() From 6a8889d0b0bb89714755e6f7f0821cd533076af4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Thu, 3 Oct 2013 08:39:15 +0200 Subject: [PATCH 678/736] Fix numpy 1.8 bug --- skimage/morphology/binary.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skimage/morphology/binary.py b/skimage/morphology/binary.py index e2e0f20b..eaea6773 100644 --- a/skimage/morphology/binary.py +++ b/skimage/morphology/binary.py @@ -29,7 +29,7 @@ def binary_erosion(image, selem, out=None): """ - conv = ndimage.convolve(image > 0, selem, output=out, + conv = ndimage.convolve((image > 0).view(np.uint8), selem, output=out, mode='constant', cval=1) if conv is not None: out = conv @@ -64,7 +64,7 @@ def binary_dilation(image, selem, out=None): """ - conv = ndimage.convolve(image > 0, selem, output=out, + conv = ndimage.convolve((image > 0).view(np.uint8), selem, output=out, mode='constant', cval=0) if conv is not None: out = conv From 895b025a3a875b49ab183c5f43dd7f5364d8faed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Thu, 3 Oct 2013 08:39:19 +0200 Subject: [PATCH 679/736] Split grey and binary erosion tests in two files --- skimage/morphology/tests/test_binary.py | 49 +++++++++++++++++++++++++ skimage/morphology/tests/test_grey.py | 37 +------------------ 2 files changed, 50 insertions(+), 36 deletions(-) create mode 100644 skimage/morphology/tests/test_binary.py diff --git a/skimage/morphology/tests/test_binary.py b/skimage/morphology/tests/test_binary.py new file mode 100644 index 00000000..2f47c917 --- /dev/null +++ b/skimage/morphology/tests/test_binary.py @@ -0,0 +1,49 @@ +import numpy as np +from numpy import testing + +from skimage import data, color +from skimage.util import img_as_bool +from skimage.morphology import binary, grey, selem + + +lena = color.rgb2gray(data.lena()) +bw_lena = lena > 100 + + +def test_non_square_image(): + strel = selem.square(3) + binary_res = binary.binary_erosion(bw_lena[:100, :200], strel) + grey_res = img_as_bool(grey.erosion(bw_lena[:100, :200], strel)) + testing.assert_array_equal(binary_res, grey_res) + + +def test_binary_erosion(): + strel = selem.square(3) + binary_res = binary.binary_erosion(bw_lena, strel) + grey_res = img_as_bool(grey.erosion(bw_lena, strel)) + testing.assert_array_equal(binary_res, grey_res) + + +def test_binary_dilation(): + strel = selem.square(3) + binary_res = binary.binary_dilation(bw_lena, strel) + grey_res = img_as_bool(grey.dilation(bw_lena, strel)) + testing.assert_array_equal(binary_res, grey_res) + + +def test_binary_closing(): + strel = selem.square(3) + binary_res = binary.binary_closing(bw_lena, strel) + grey_res = img_as_bool(grey.closing(bw_lena, strel)) + testing.assert_array_equal(binary_res, grey_res) + + +def test_binary_opening(): + strel = selem.square(3) + binary_res = binary.binary_opening(bw_lena, strel) + grey_res = img_as_bool(grey.opening(bw_lena, strel)) + testing.assert_array_equal(binary_res, grey_res) + + +if __name__ == '__main__': + testing.run_module_suite() diff --git a/skimage/morphology/tests/test_grey.py b/skimage/morphology/tests/test_grey.py index 244ec566..e2a3928d 100644 --- a/skimage/morphology/tests/test_grey.py +++ b/skimage/morphology/tests/test_grey.py @@ -6,7 +6,7 @@ from numpy import testing import skimage from skimage import data_dir from skimage.util import img_as_bool -from skimage.morphology import binary, grey, selem +from skimage.morphology import grey, selem lena = np.load(os.path.join(data_dir, 'lena_GRAY_U8.npy')) @@ -155,40 +155,5 @@ class TestDTypes(): self._test_image(image) -def test_non_square_image(): - strel = selem.square(3) - binary_res = binary.binary_erosion(bw_lena[:100, :200], strel) - grey_res = img_as_bool(grey.erosion(bw_lena[:100, :200], strel)) - testing.assert_array_equal(binary_res, grey_res) - - -def test_binary_erosion(): - strel = selem.square(3) - binary_res = binary.binary_erosion(bw_lena, strel) - grey_res = img_as_bool(grey.erosion(bw_lena, strel)) - testing.assert_array_equal(binary_res, grey_res) - - -def test_binary_dilation(): - strel = selem.square(3) - binary_res = binary.binary_dilation(bw_lena, strel) - grey_res = img_as_bool(grey.dilation(bw_lena, strel)) - testing.assert_array_equal(binary_res, grey_res) - - -def test_binary_closing(): - strel = selem.square(3) - binary_res = binary.binary_closing(bw_lena, strel) - grey_res = img_as_bool(grey.closing(bw_lena, strel)) - testing.assert_array_equal(binary_res, grey_res) - - -def test_binary_opening(): - strel = selem.square(3) - binary_res = binary.binary_opening(bw_lena, strel) - grey_res = img_as_bool(grey.opening(bw_lena, strel)) - testing.assert_array_equal(binary_res, grey_res) - - if __name__ == '__main__': testing.run_module_suite() From 95a5a98c937a1f8d9cdb5e09de90620704e6cc0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Mon, 7 Oct 2013 22:56:56 +0200 Subject: [PATCH 680/736] Fix overflow error --- skimage/morphology/binary.py | 45 +++++++++++++++++++------ skimage/morphology/tests/test_binary.py | 21 ++++++++++++ 2 files changed, 56 insertions(+), 10 deletions(-) diff --git a/skimage/morphology/binary.py b/skimage/morphology/binary.py index eaea6773..70fcd6b4 100644 --- a/skimage/morphology/binary.py +++ b/skimage/morphology/binary.py @@ -1,7 +1,38 @@ import numpy as np +import warnings from scipy import ndimage +def _convolve(image, selem, out, cval): + + # determine the smallest integer dtype which does not overflow + selem = selem != 0 + selem_sum = np.sum(selem) + if selem_sum < 2 ** 8: + out_dtype = np.uint8 + elif selem_sum < 2 ** 16: + out_dtype = np.uint16 + else: + out_dtype = np.uint32 + + if out is None: + out = np.zeros_like(image, dtype=out_dtype) + else: + iinfo = np.iinfo(out.dtype) + if iinfo.max - iinfo.min < selem_sum: + raise ValueError('Sum of structuring (=%d) element results in ' + 'overflow for dtype of `out`. You must raise the ' + 'bit-depth.') + + conv = ndimage.convolve(image > 0, selem, output=out, + mode='constant', cval=cval) + + if conv is not None: + out = conv + + return out, selem_sum + + def binary_erosion(image, selem, out=None): """Return fast binary morphological erosion of an image. @@ -29,11 +60,8 @@ def binary_erosion(image, selem, out=None): """ - conv = ndimage.convolve((image > 0).view(np.uint8), selem, output=out, - mode='constant', cval=1) - if conv is not None: - out = conv - return np.equal(out, np.sum(selem), out=out) + out, selem_sum = _convolve(image, selem, out, 1) + return np.equal(out, selem_sum, out=out).astype(np.bool, copy=False) def binary_dilation(image, selem, out=None): @@ -64,11 +92,8 @@ def binary_dilation(image, selem, out=None): """ - conv = ndimage.convolve((image > 0).view(np.uint8), selem, output=out, - mode='constant', cval=0) - if conv is not None: - out = conv - return np.not_equal(out, 0, out=out) + out, _ = _convolve(image, selem, out, 0) + return np.not_equal(out, 0, out=out).astype(np.bool, copy=False) def binary_opening(image, selem, out=None): diff --git a/skimage/morphology/tests/test_binary.py b/skimage/morphology/tests/test_binary.py index 2f47c917..5f831669 100644 --- a/skimage/morphology/tests/test_binary.py +++ b/skimage/morphology/tests/test_binary.py @@ -17,6 +17,27 @@ def test_non_square_image(): testing.assert_array_equal(binary_res, grey_res) +def test_selem_overflow(): + strel = np.ones((17, 17), dtype=np.uint8) + img = np.zeros((20, 20)) + img[2:19, 2:19] = 1 + binary_res = binary.binary_erosion(img, strel) + grey_res = img_as_bool(grey.erosion(img, strel)) + testing.assert_array_equal(binary_res, grey_res) + + +def test_selem_overflow_exception(): + strel = np.ones((17, 17), dtype=np.uint8) + img = np.zeros((20, 20)) + img[2:19, 2:19] = 1 + out = np.zeros((20, 20), dtype=np.uint8) + testing.assert_raises(ValueError, binary.binary_erosion, img, strel, out) + out = np.zeros((20, 20), dtype=np.uint16) + binary_res = binary.binary_erosion(img, strel, out=out) + grey_res = img_as_bool(grey.erosion(img, strel)) + testing.assert_array_equal(binary_res, grey_res) + + def test_binary_erosion(): strel = selem.square(3) binary_res = binary.binary_erosion(bw_lena, strel) From ecf10a7a48ed0205a61da10a91153345266dcd1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Mon, 7 Oct 2013 23:33:12 +0200 Subject: [PATCH 681/736] Reduce number of specializations --- skimage/morphology/binary.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/skimage/morphology/binary.py b/skimage/morphology/binary.py index 70fcd6b4..7262c573 100644 --- a/skimage/morphology/binary.py +++ b/skimage/morphology/binary.py @@ -10,10 +10,8 @@ def _convolve(image, selem, out, cval): selem_sum = np.sum(selem) if selem_sum < 2 ** 8: out_dtype = np.uint8 - elif selem_sum < 2 ** 16: - out_dtype = np.uint16 else: - out_dtype = np.uint32 + out_dtype = np.intp if out is None: out = np.zeros_like(image, dtype=out_dtype) From 2a84e4538927a689fec6d6aa26c1af482fb2f933 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Mon, 7 Oct 2013 23:35:54 +0200 Subject: [PATCH 682/736] Remove unused import --- skimage/morphology/binary.py | 1 - 1 file changed, 1 deletion(-) diff --git a/skimage/morphology/binary.py b/skimage/morphology/binary.py index 7262c573..4a7b1578 100644 --- a/skimage/morphology/binary.py +++ b/skimage/morphology/binary.py @@ -1,5 +1,4 @@ import numpy as np -import warnings from scipy import ndimage From b187144a5fe24d0370a77d36c656eeabfd885690 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Thu, 10 Oct 2013 09:04:17 +0200 Subject: [PATCH 683/736] Add support older numpy versions --- skimage/morphology/binary.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/skimage/morphology/binary.py b/skimage/morphology/binary.py index 4a7b1578..d248d8db 100644 --- a/skimage/morphology/binary.py +++ b/skimage/morphology/binary.py @@ -58,7 +58,8 @@ def binary_erosion(image, selem, out=None): """ out, selem_sum = _convolve(image, selem, out, 1) - return np.equal(out, selem_sum, out=out).astype(np.bool, copy=False) + return np.array(np.equal(out, selem_sum, out=out), dtype=np.bool, + copy=False) def binary_dilation(image, selem, out=None): @@ -90,7 +91,7 @@ def binary_dilation(image, selem, out=None): """ out, _ = _convolve(image, selem, out, 0) - return np.not_equal(out, 0, out=out).astype(np.bool, copy=False) + return np.array(np.not_equal(out, 0, out=out), dtype=np.bool, copy=False) def binary_opening(image, selem, out=None): From f8d34e8bf91f1fed3970b770abdfec94efc949df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Thu, 10 Oct 2013 09:10:42 +0200 Subject: [PATCH 684/736] Deprecate out parameter --- TODO.txt | 1 + skimage/morphology/binary.py | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/TODO.txt b/TODO.txt index db7ffe48..29d1ebac 100644 --- a/TODO.txt +++ b/TODO.txt @@ -6,6 +6,7 @@ Version 0.10 * Remove {`ratio`, `sigma`} deprecation warnings of `skimage.segmentation.slic` * Change default mode of random_walker segmentation to 'cg_mg' > 'cg' > 'bf', depending on which optional dependencies are available. +* Remove deprecated `out` parameter of `skimage.morphology.binary_*` Version 0.9 ----------- diff --git a/skimage/morphology/binary.py b/skimage/morphology/binary.py index d248d8db..11a14d49 100644 --- a/skimage/morphology/binary.py +++ b/skimage/morphology/binary.py @@ -1,3 +1,4 @@ +import warnings import numpy as np from scipy import ndimage @@ -15,6 +16,9 @@ def _convolve(image, selem, out, cval): if out is None: out = np.zeros_like(image, dtype=out_dtype) else: + warnings.warn('Parameter `out` is deprecated and it does not equal ' + 'the output image if the sum of the structuring element ' + 'overflows the dtype of `out`.') iinfo = np.iinfo(out.dtype) if iinfo.max - iinfo.min < selem_sum: raise ValueError('Sum of structuring (=%d) element results in ' From 2ac11113286ba298af045ae82f3a68bdb6272555 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Fri, 11 Oct 2013 19:16:49 +0200 Subject: [PATCH 685/736] Add labels to releases. --- doc/tools/plot_pr.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/tools/plot_pr.py b/doc/tools/plot_pr.py index 96860e7f..aa9a6653 100644 --- a/doc/tools/plot_pr.py +++ b/doc/tools/plot_pr.py @@ -22,7 +22,7 @@ cache = '_pr_cache.txt' releases = OrderedDict([ #('0.1', u'2009-10-07 13:52:19 +0200'), #('0.2', u'2009-11-12 14:48:45 +0200'), - ('0.3', u'2011-10-10 03:28:47 -0700'), + #('0.3', u'2011-10-10 03:28:47 -0700'), ('0.4', u'2011-12-03 14:31:32 -0800'), ('0.5', u'2012-02-26 21:00:51 -0800'), ('0.6', u'2012-06-24 21:37:05 -0700'), @@ -123,11 +123,12 @@ for l in labels: for version, date in releases.items(): date = seconds_from_epoch([date])[0] plt.axvline(date, color='r', label=version) + plt.text(date, n.max() * 0.9, version, color='orange', rotation=90, + fontsize=16) plt.title('Pull request activity').set_y(1.05) plt.xlabel('Date') plt.ylabel('PRs per month') -plt.legend(loc=2, title='Release') plt.subplots_adjust(top=0.875, bottom=0.225) import numpy as np From 90d4fb2823217fdf32e55b8e9ae7f09cb5c9d7c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Fri, 11 Oct 2013 19:24:52 +0200 Subject: [PATCH 686/736] Improve shape plot --- doc/examples/plot_shapes.py | 47 ++++++++++++++++++++----------------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/doc/examples/plot_shapes.py b/doc/examples/plot_shapes.py index 8e082579..2ae842a5 100644 --- a/doc/examples/plot_shapes.py +++ b/doc/examples/plot_shapes.py @@ -4,7 +4,6 @@ Shapes ====== This example shows how to draw several different shapes: - * line * Bezier curve * polygon @@ -12,6 +11,7 @@ This example shows how to draw several different shapes: * ellipse """ +import math import numpy as np import matplotlib.pyplot as plt @@ -19,9 +19,12 @@ from skimage.draw import (line, polygon, circle, circle_perimeter, ellipse, ellipse_perimeter, bezier_curve) -import math -img = np.zeros((500, 500, 3), dtype=np.uint8) + +fig, (ax1, ax2) = plt.subplots(ncols=2, nrows=1, figsize=(10, 6)) + + +img = np.zeros((500, 500, 3), dtype=np.double) # draw line rr, cc = line(120, 123, 20, 400) @@ -36,34 +39,35 @@ poly = np.array(( (300, 300), )) rr, cc = polygon(poly[:, 0], poly[:, 1], img.shape) -img[rr, cc, 1] = 255 +img[rr, cc, 1] = 1 # fill circle rr, cc = circle(200, 200, 100, img.shape) -img[rr, cc, :] = (255, 255, 0) +img[rr, cc, :] = (1, 1, 0) # fill ellipse rr, cc = ellipse(300, 300, 100, 200, img.shape) -img[rr, cc, 2] = 255 +img[rr, cc, 2] = 1 # circle rr, cc = circle_perimeter(120, 400, 15) -img[rr, cc, :] = (255, 0, 0) +img[rr, cc, :] = (1, 0, 0) # Bezier curve rr, cc = bezier_curve(70, 100, 10, 10, 150, 100, 1) -img[rr, cc, :] = (255, 0, 0) +img[rr, cc, :] = (1, 0, 0) # ellipses rr, cc = ellipse_perimeter(120, 400, 60, 20, orientation=math.pi / 4.) -img[rr, cc, :] = (255, 0, 255) +img[rr, cc, :] = (1, 0, 1) rr, cc = ellipse_perimeter(120, 400, 60, 20, orientation=-math.pi / 4.) -img[rr, cc, :] = (0, 0, 255) +img[rr, cc, :] = (0, 0, 1) rr, cc = ellipse_perimeter(120, 400, 60, 20, orientation=math.pi / 2.) -img[rr, cc, :] = (255, 255, 255) +img[rr, cc, :] = (1, 1, 1) -plt.imshow(img) -plt.show() +ax1.imshow(img) +ax1.set_title('No anti-aliasing') +ax1.axis('off') """ @@ -73,23 +77,22 @@ Anti-aliased drawing for: """ -import numpy as np -import matplotlib.pyplot as plt +from skimage.draw import line_aa, circle_perimeter_aa -from skimage.draw import (line_aa, - circle_perimeter_aa) -img = np.zeros((100, 100), dtype=np.uint8) +img = np.zeros((100, 100), dtype=np.double) # anti-aliased line rr, cc, val = line_aa(12, 12, 20, 50) -img[rr, cc] = val * 255 +img[rr, cc] = val # anti-aliased circle rr, cc, val = circle_perimeter_aa(60, 40, 30) -img[rr, cc] = val * 255 +img[rr, cc] = val -plt.imshow(img, cmap=plt.cm.gray, interpolation='nearest') -plt.title('Anti-aliasing') +ax2.imshow(img, cmap=plt.cm.gray, interpolation='nearest') +ax2.set_title('Anti-aliasing') +ax2.axis('off') + plt.show() From f435afebd5453e679f1eb0325914cb3747215709 Mon Sep 17 00:00:00 2001 From: "Josh Warner (Mac)" Date: Fri, 11 Oct 2013 16:39:22 -0500 Subject: [PATCH 687/736] ENH: Add optional `clip` kwarg, docs, & handling of signed inputs. --- skimage/util/noise.py | 62 ++++++++++++++++++++++--- skimage/util/tests/test_random_noise.py | 60 ++++++++++++++++++++++++ 2 files changed, 115 insertions(+), 7 deletions(-) diff --git a/skimage/util/noise.py b/skimage/util/noise.py index 7d7c28d7..db470498 100644 --- a/skimage/util/noise.py +++ b/skimage/util/noise.py @@ -5,7 +5,7 @@ from .dtype import img_as_float __all__ = ['random_noise'] -def random_noise(image, mode='gaussian', seed=None, **kwargs): +def random_noise(image, mode='gaussian', seed=None, clip=True, **kwargs): """ Function to add random noise of various types to a floating-point image. @@ -26,6 +26,11 @@ def random_noise(image, mode='gaussian', seed=None, **kwargs): seed : int If provided, this will set the random seed before generating noise, for valid pseudo-random comparisons. + clip : bool + If True (default), the output will be clipped after noise applied + for modes `'speckle'`, `'poisson'`, and `'gaussian'`. This is + needed to maintain the proper image data range. If False, clipping + is not applied, and the output may extend beyond the range [-1, 1]. mean : float Mean of random distribution. Used in 'gaussian' and 'speckle'. Default : 0. @@ -42,10 +47,42 @@ def random_noise(image, mode='gaussian', seed=None, **kwargs): Returns ------- out : ndarray - Output floating-point image data on range [0, 1]. + Output floating-point image data on range [0, 1] or [-1, 1] if the + input `image` was unsigned or signed, respectively. + + Notes + ----- + Speckle, Poisson, and Gaussian noise may generate noise outside the valid + image range. The default is to clip (not alias) these values, but they may + be preserved by setting `clip=False`. Note that in this case the output + may contain values outside the ranges [0, 1] or [-1, 1]. Use with care. + + Because of the prevalence of exclusively positive floating-point images in + intermediate calculations, it is not possible to intuit if an input is + signed based on dtype alone. Instead, negative values are explicity + searched for. Only if found does this function assume signed input. + Unexpected results only occur in rare, poorly exposes cases (e.g. if all + values are above 50 percent gray in a signed `image`). In this event, + manually scaling the input to the positive domain will solve the problem. + + The Poisson distribution is only defined for positive integers. To apply + this noise type, the number of unique values in the image is found and + the next round power of two is used to scale up the floating-point result, + after which it is scaled back down to the floating-point image range. + + To generate Poisson noise against a signed image, the signed image is + temporarily converted to an unsigned image in the floating point domain, + Poisson noise is generated, then it is returned to the original range. """ mode = mode.lower() + + # Detect if a signed image was input + if image.min() < 0: + low_clip = -1. + else: + low_clip = 0. + image = img_as_float(image) if seed is not None: np.random.seed(seed=seed) @@ -82,18 +119,25 @@ def random_noise(image, mode='gaussian', seed=None, **kwargs): if mode == 'gaussian': noise = np.random.normal(kwargs['mean'], kwargs['var'] ** 0.5, image.shape) - out = np.clip(image + noise, 0., 1.) + out = image + noise elif mode == 'poisson': - # Determine unique values present in image + # Determine unique values in image & calculate the next power of two vals = len(np.unique(image)) - - # Calculate the next lowest power of two vals = 2 ** np.ceil(np.log2(vals)) + # Ensure image is exclusively positive + if low_clip == -1.: + old_max = image.max() + image = (image + 1.) / (old_max + 1.) + # Generating noise for each unique value in image. out = np.random.poisson(image * vals) / float(vals) + # Return image to original range if input was signed + if low_clip == -1.: + out = out * (old_max + 1.) - 1. + elif mode == 'salt': # Re-call function with mode='s&p' and p=1 (all salt noise) out = random_noise(image, mode='s&p', seed=seed, @@ -126,6 +170,10 @@ def random_noise(image, mode='gaussian', seed=None, **kwargs): elif mode == 'speckle': noise = np.random.normal(kwargs['mean'], kwargs['var'] ** 0.5, image.shape) - out = np.clip(image + image * noise, 0., 1.) + out = image + image * noise + + # Clip back to original range, if necessary + if clip: + out = np.clip(out, low_clip, 1.0) return out diff --git a/skimage/util/tests/test_random_noise.py b/skimage/util/tests/test_random_noise.py index fd05084a..f5c9f9d0 100644 --- a/skimage/util/tests/test_random_noise.py +++ b/skimage/util/tests/test_random_noise.py @@ -94,6 +94,66 @@ def test_poisson(): assert_allclose(cam_noisy, expected) +def test_clip_poisson(): + seed = 42 + data = camera() # 512x512 grayscale uint8 + data_signed = (data / 255.) * 2. - 1. # Same image, on range [-1, 1] + + # Signed and unsigned, clipped + cam_poisson = random_noise(data, mode='poisson', seed=seed, clip=True) + cam_poisson2 = random_noise(data_signed, mode='poisson', seed=seed, + clip=True) + assert (cam_poisson.max() == 1.) and (cam_poisson.min() == 0.) + assert (cam_poisson2.max() == 1.) and (cam_poisson2.min() == -1.) + + # Signed and unsigned, unclipped + cam_poisson = random_noise(data, mode='poisson', seed=seed, clip=False) + cam_poisson2 = random_noise(data_signed, mode='poisson', seed=seed, + clip=False) + assert (cam_poisson.max() > 1.15) and (cam_poisson.min() == 0.) + assert (cam_poisson2.max() > 1.3) and (cam_poisson2.min() == -1.) + + +def test_clip_gaussian(): + seed = 42 + data = camera() # 512x512 grayscale uint8 + data_signed = (data / 255.) * 2. - 1. # Same image, on range [-1, 1] + + # Signed and unsigned, clipped + cam_gauss = random_noise(data, mode='gaussian', seed=seed, clip=True) + cam_gauss2 = random_noise(data_signed, mode='gaussian', seed=seed, + clip=True) + assert (cam_gauss.max() == 1.) and (cam_gauss.min() == 0.) + assert (cam_gauss2.max() == 1.) and (cam_gauss2.min() == -1.) + + # Signed and unsigned, unclipped + cam_gauss = random_noise(data, mode='gaussian', seed=seed, clip=False) + cam_gauss2 = random_noise(data_signed, mode='gaussian', seed=seed, + clip=False) + assert (cam_gauss.max() > 1.22) and (cam_gauss.min() < -0.36) + assert (cam_gauss2.max() > 1.219) and (cam_gauss2.min() < -1.337) + + +def test_clip_speckle(): + seed = 42 + data = camera() # 512x512 grayscale uint8 + data_signed = (data / 255.) * 2. - 1. # Same image, on range [-1, 1] + + # Signed and unsigned, clipped + cam_speckle = random_noise(data, mode='speckle', seed=seed, clip=True) + cam_speckle2 = random_noise(data_signed, mode='speckle', seed=seed, + clip=True) + assert (cam_speckle.max() == 1.) and (cam_speckle.min() == 0.) + assert (cam_speckle2.max() == 1.) and (cam_speckle2.min() == -1.) + + # Signed and unsigned, unclipped + cam_speckle = random_noise(data, mode='speckle', seed=seed, clip=False) + cam_speckle2 = random_noise(data_signed, mode='speckle', seed=seed, + clip=False) + assert (cam_speckle.max() > 1.219) and (cam_speckle.min() == 0.) + assert (cam_speckle2.max() > 1.219) and (cam_speckle2.min() < -1.306) + + def test_bad_mode(): data = np.zeros((64, 64)) assert_raises(KeyError, random_noise, data, 'perlin') From 9f7a2f4fbc17f1fb1987d7a7bfd58dbfe6b49464 Mon Sep 17 00:00:00 2001 From: "Josh Warner (Mac)" Date: Fri, 11 Oct 2013 16:53:16 -0500 Subject: [PATCH 688/736] ENH: Tighten tests, all noise types now support signed I/O --- skimage/util/noise.py | 2 +- skimage/util/tests/test_random_noise.py | 19 ++++++++++++++++--- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/skimage/util/noise.py b/skimage/util/noise.py index db470498..aa8af300 100644 --- a/skimage/util/noise.py +++ b/skimage/util/noise.py @@ -165,7 +165,7 @@ def random_noise(image, mode='gaussian', seed=None, clip=True, **kwargs): kwargs['amount'] * image.size * (1. - kwargs['salt_vs_pepper'])) coords = [np.random.randint(0, i - 1, int(num_pepper)) for i in image.shape] - out[coords] = 0 + out[coords] = low_clip elif mode == 'speckle': noise = np.random.normal(kwargs['mean'], kwargs['var'] ** 0.5, diff --git a/skimage/util/tests/test_random_noise.py b/skimage/util/tests/test_random_noise.py index f5c9f9d0..b1dece36 100644 --- a/skimage/util/tests/test_random_noise.py +++ b/skimage/util/tests/test_random_noise.py @@ -23,12 +23,14 @@ def test_salt(): # Ensure approximately correct amount of noise was added proportion = float(saltmask.sum()) / (cam.shape[0] * cam.shape[1]) - assert 0.11 < proportion <= 0.18 + assert 0.11 < proportion <= 0.15 def test_pepper(): seed = 42 cam = img_as_float(camera()) + data_signed = (cam / 255.) * 2. - 1. # Same image, on range [-1, 1] + cam_noisy = random_noise(cam, seed=seed, mode='pepper', amount=0.15) peppermask = cam != cam_noisy @@ -37,7 +39,16 @@ def test_pepper(): # Ensure approximately correct amount of noise was added proportion = float(peppermask.sum()) / (cam.shape[0] * cam.shape[1]) - assert 0.11 < proportion <= 0.18 + assert 0.11 < proportion <= 0.15 + + # Check to make sure pepper gets added properly to signed images + orig_zeros = (data_signed == -1).sum() + cam_noisy_signed = random_noise(data_signed, seed=seed, mode='pepper', + amount=.15) + + proportion = (float((cam_noisy_signed == -1).sum() - orig_zeros) / + (cam.shape[0] * cam.shape[1])) + assert 0.11 < proportion <= 0.15 def test_salt_and_pepper(): @@ -88,10 +99,12 @@ def test_poisson(): seed = 42 data = camera() # 512x512 grayscale uint8 cam_noisy = random_noise(data, mode='poisson', seed=seed) + cam_noisy2 = random_noise(data, mode='poisson', seed=seed, clip=False) np.random.seed(seed=seed) expected = np.random.poisson(img_as_float(data) * 256) / 256. - assert_allclose(cam_noisy, expected) + assert_allclose(cam_noisy, np.clip(expected, 0., 1.)) + assert_allclose(cam_noisy2, expected) def test_clip_poisson(): From e9f3bd66ac70cfce7eb3b665238a5ca0d7297b29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Tue, 6 Aug 2013 14:18:03 +0200 Subject: [PATCH 689/736] TEST: fix mistake --- skimage/transform/tests/test_hough_transform.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/skimage/transform/tests/test_hough_transform.py b/skimage/transform/tests/test_hough_transform.py index 651861f3..c1917b66 100644 --- a/skimage/transform/tests/test_hough_transform.py +++ b/skimage/transform/tests/test_hough_transform.py @@ -152,9 +152,9 @@ def test_hough_ellipse_zero_angle(): a = 6 b = 8 x0 = 12 - y0 = 12 + y0 = 15 angle = 0 - rr, cc = ellipse_perimeter(x0, x0, b, a) + rr, cc = ellipse_perimeter(y0, x0, b, a) img[rr, cc] = 1 result = tf.hough_ellipse(img, threshold=9) assert_equal(result[0][0], x0) @@ -165,13 +165,13 @@ def test_hough_ellipse_zero_angle(): def test_hough_ellipse_non_zero_angle(): - img = np.zeros((20, 20), dtype=int) + img = np.zeros((30, 20), dtype=int) a = 6 b = 9 x0 = 10 - y0 = 10 + y0 = 15 angle = np.pi / 1.35 - rr, cc = ellipse_perimeter(x0, x0, b, a, orientation=angle) + rr, cc = ellipse_perimeter(y0, x0, b, a, orientation=angle) img[rr, cc] = 1 result = tf.hough_ellipse(img, threshold=15, accuracy=3) assert_almost_equal(result[0][0] / 100., x0 / 100., decimal=1) From 7a1b1c28de7fd7c1c8ad4fb43d4a9ddf1afad2f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Tue, 6 Aug 2013 15:26:47 +0200 Subject: [PATCH 690/736] FIX: fix angle convention --- skimage/transform/_hough_transform.pyx | 9 +++++++- .../transform/tests/test_hough_transform.py | 23 ++++++++++++++++--- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/skimage/transform/_hough_transform.pyx b/skimage/transform/_hough_transform.pyx index e5fd3ed8..db39172e 100644 --- a/skimage/transform/_hough_transform.pyx +++ b/skimage/transform/_hough_transform.pyx @@ -206,9 +206,16 @@ def hough_ellipse(cnp.ndarray img, int threshold=4, double accuracy=1, hist_max = np.max(hist) if hist_max > threshold: angle = np.arctan2(p1x - p2x, p1y - p2y) - # pi - angle to keep ellipse_perimeter() convention + # to keep ellipse_perimeter() convention if angle != 0: angle = np.pi - angle + # When angle is not in [-pi:pi] + # it would mean in ellipse_perimeter() + # that a < b. But we keep a > b. + if angle > np.pi: + angle = angle - np.pi / 2. + elif angle < - np.pi: + angle = angle + np.pi / 2. b = sqrt(bin_edges[hist.argmax()]) results.append((x0, y0, diff --git a/skimage/transform/tests/test_hough_transform.py b/skimage/transform/tests/test_hough_transform.py index c1917b66..3e5ed71c 100644 --- a/skimage/transform/tests/test_hough_transform.py +++ b/skimage/transform/tests/test_hough_transform.py @@ -165,9 +165,9 @@ def test_hough_ellipse_zero_angle(): def test_hough_ellipse_non_zero_angle(): - img = np.zeros((30, 20), dtype=int) + img = np.zeros((30, 24), dtype=int) a = 6 - b = 9 + b = 12 x0 = 10 y0 = 15 angle = np.pi / 1.35 @@ -176,10 +176,27 @@ def test_hough_ellipse_non_zero_angle(): result = tf.hough_ellipse(img, threshold=15, accuracy=3) assert_almost_equal(result[0][0] / 100., x0 / 100., decimal=1) assert_almost_equal(result[0][1] / 100., y0 / 100., decimal=1) - assert_almost_equal(result[0][2] / 100., b / 100., decimal=1) + assert_almost_equal(result[0][2] / 10., b / 10., decimal=1) assert_almost_equal(result[0][3] / 100., a / 100., decimal=1) assert_almost_equal(result[0][4], angle, decimal=1) +def test_hough_ellipse_non_zero_angle2(): + img = np.zeros((30, 24), dtype=int) + b = 6 + a = 12 + x0 = 10 + y0 = 15 + angle = np.pi / 1.35 + rr, cc = ellipse_perimeter(y0, x0, b, a, orientation=angle) + img[rr, cc] = 1 + result = tf.hough_ellipse(img, threshold=15, accuracy=3) + assert_almost_equal(result[0][0] / 100., x0 / 100., decimal=1) + assert_almost_equal(result[0][1] / 100., y0 / 100., decimal=1) + assert_almost_equal(result[0][2] / 100., a / 100., decimal=1) + assert_almost_equal(result[0][3] / 100., b / 100., decimal=1) + assert_almost_equal(result[0][4], angle, decimal=1) + + if __name__ == "__main__": np.testing.run_module_suite() From 7e970b18cd69de0ab9c59b275c9d4c12a615756c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Tue, 6 Aug 2013 15:54:01 +0200 Subject: [PATCH 691/736] MAINT change HT return API --- ...lot_circular_elliptical_hough_transform.py | 10 +++---- skimage/transform/_hough_transform.pyx | 8 +++--- .../transform/tests/test_hough_transform.py | 28 +++++++++---------- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/doc/examples/plot_circular_elliptical_hough_transform.py b/doc/examples/plot_circular_elliptical_hough_transform.py index bd036e03..ca30b136 100755 --- a/doc/examples/plot_circular_elliptical_hough_transform.py +++ b/doc/examples/plot_circular_elliptical_hough_transform.py @@ -122,11 +122,11 @@ edges = filter.canny(image_gray, sigma=2.0, accum = hough_ellipse(edges, accuracy=10, threshold=170, min_size=50) accum.sort(key=lambda x:x[5]) # Estimated parameters for the ellipse -center_y = int(accum[-1][0]) -center_x = int(accum[-1][1]) -xradius = int(accum[-1][2]) -yradius = int(accum[-1][3]) -angle = np.pi - accum[-1][4] +center_y = int(accum[-1][1]) +center_x = int(accum[-1][2]) +xradius = int(accum[-1][3]) +yradius = int(accum[-1][4]) +angle = np.pi - accum[-1][5] # Draw the ellipse on the original image cx, cy = ellipse_perimeter(center_y, center_x, diff --git a/skimage/transform/_hough_transform.pyx b/skimage/transform/_hough_transform.pyx index db39172e..577313a1 100644 --- a/skimage/transform/_hough_transform.pyx +++ b/skimage/transform/_hough_transform.pyx @@ -122,7 +122,7 @@ def hough_ellipse(cnp.ndarray img, int threshold=4, double accuracy=1, Returns ------- - res : list of tuples [(x0, y0, a, b, angle, accumulator)] + res : list of tuples [(accumulator, x0, y0, a, b, angle)] Where (x0, y0) is the center, (a, b) major and minor axis. The angle value follows `draw.ellipse_perimeter()` convention. @@ -132,7 +132,7 @@ def hough_ellipse(cnp.ndarray img, int threshold=4, double accuracy=1, >>> rr, cc = draw.ellipse_perimeter(10, 10, 6, 8) >>> img[cc, rr] = 1 >>> result = hough_ellipse(img, threshold=8) - [(10.0, 10.0, 8.0, 6.0, 0.0, 10)] + [(10, 10.0, 10.0, 8.0, 6.0, 0.0)] Notes ----- @@ -217,12 +217,12 @@ def hough_ellipse(cnp.ndarray img, int threshold=4, double accuracy=1, elif angle < - np.pi: angle = angle + np.pi / 2. b = sqrt(bin_edges[hist.argmax()]) - results.append((x0, + results.append((hist_max, # Accumulator + x0, y0, a, b, angle, - hist_max, # Accumulator )) acc = [] diff --git a/skimage/transform/tests/test_hough_transform.py b/skimage/transform/tests/test_hough_transform.py index 3e5ed71c..6c6acb96 100644 --- a/skimage/transform/tests/test_hough_transform.py +++ b/skimage/transform/tests/test_hough_transform.py @@ -157,11 +157,11 @@ def test_hough_ellipse_zero_angle(): rr, cc = ellipse_perimeter(y0, x0, b, a) img[rr, cc] = 1 result = tf.hough_ellipse(img, threshold=9) - assert_equal(result[0][0], x0) - assert_equal(result[0][1], y0) - assert_almost_equal(result[0][2], b, decimal=1) - assert_almost_equal(result[0][3], a, decimal=1) - assert_equal(result[0][4], angle) + assert_equal(result[0][1], x0) + assert_equal(result[0][2], y0) + assert_almost_equal(result[0][3], b, decimal=1) + assert_almost_equal(result[0][4], a, decimal=1) + assert_equal(result[0][5], angle) def test_hough_ellipse_non_zero_angle(): @@ -174,11 +174,11 @@ def test_hough_ellipse_non_zero_angle(): rr, cc = ellipse_perimeter(y0, x0, b, a, orientation=angle) img[rr, cc] = 1 result = tf.hough_ellipse(img, threshold=15, accuracy=3) - assert_almost_equal(result[0][0] / 100., x0 / 100., decimal=1) - assert_almost_equal(result[0][1] / 100., y0 / 100., decimal=1) - assert_almost_equal(result[0][2] / 10., b / 10., decimal=1) - assert_almost_equal(result[0][3] / 100., a / 100., decimal=1) - assert_almost_equal(result[0][4], angle, decimal=1) + assert_almost_equal(result[0][1] / 100., x0 / 100., decimal=1) + assert_almost_equal(result[0][2] / 100., y0 / 100., decimal=1) + assert_almost_equal(result[0][3] / 10., b / 10., decimal=1) + assert_almost_equal(result[0][4] / 100., a / 100., decimal=1) + assert_almost_equal(result[0][5], angle, decimal=1) def test_hough_ellipse_non_zero_angle2(): @@ -191,11 +191,11 @@ def test_hough_ellipse_non_zero_angle2(): rr, cc = ellipse_perimeter(y0, x0, b, a, orientation=angle) img[rr, cc] = 1 result = tf.hough_ellipse(img, threshold=15, accuracy=3) - assert_almost_equal(result[0][0] / 100., x0 / 100., decimal=1) - assert_almost_equal(result[0][1] / 100., y0 / 100., decimal=1) - assert_almost_equal(result[0][2] / 100., a / 100., decimal=1) + assert_almost_equal(result[0][1] / 100., x0 / 100., decimal=1) + assert_almost_equal(result[0][2] / 100., y0 / 100., decimal=1) + assert_almost_equal(result[0][3] / 100., a / 100., decimal=1) assert_almost_equal(result[0][3] / 100., b / 100., decimal=1) - assert_almost_equal(result[0][4], angle, decimal=1) + assert_almost_equal(result[0][5], angle, decimal=1) if __name__ == "__main__": From debd4d54d6d81e491f3ca712b8da4d5a721c5d9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Tue, 6 Aug 2013 16:03:42 +0200 Subject: [PATCH 692/736] ENH: use heapq to select the best match --- ...lot_circular_elliptical_hough_transform.py | 15 ++++---- skimage/transform/_hough_transform.pyx | 10 ++++-- .../transform/tests/test_hough_transform.py | 34 +++++++++++-------- 3 files changed, 35 insertions(+), 24 deletions(-) diff --git a/doc/examples/plot_circular_elliptical_hough_transform.py b/doc/examples/plot_circular_elliptical_hough_transform.py index ca30b136..d500be79 100755 --- a/doc/examples/plot_circular_elliptical_hough_transform.py +++ b/doc/examples/plot_circular_elliptical_hough_transform.py @@ -119,14 +119,15 @@ edges = filter.canny(image_gray, sigma=2.0, # The accuracy corresponds to the bin size of a major axis. # The value is chosen in order to get a single high accumulator. # The threshold eliminates low accumulators -accum = hough_ellipse(edges, accuracy=10, threshold=170, min_size=50) -accum.sort(key=lambda x:x[5]) +accum = hough_ellipse(edges, accuracy=10, threshold=150, min_size=50) +# Select the highest accumulator +best = heapq.nlargest(1, accum)[0] # Estimated parameters for the ellipse -center_y = int(accum[-1][1]) -center_x = int(accum[-1][2]) -xradius = int(accum[-1][3]) -yradius = int(accum[-1][4]) -angle = np.pi - accum[-1][5] +center_y = int(best[1]) +center_x = int(best[2]) +xradius = int(best[3]) +yradius = int(best[4]) +angle = np.pi - best[5] # Draw the ellipse on the original image cx, cy = ellipse_perimeter(center_y, center_x, diff --git a/skimage/transform/_hough_transform.pyx b/skimage/transform/_hough_transform.pyx index 577313a1..958bacc4 100644 --- a/skimage/transform/_hough_transform.pyx +++ b/skimage/transform/_hough_transform.pyx @@ -3,6 +3,7 @@ #cython: nonecheck=False #cython: wraparound=False import numpy as np +import heapq cimport numpy as cnp cimport cython @@ -131,8 +132,12 @@ def hough_ellipse(cnp.ndarray img, int threshold=4, double accuracy=1, >>> img = np.zeros((25, 25), dtype=int) >>> rr, cc = draw.ellipse_perimeter(10, 10, 6, 8) >>> img[cc, rr] = 1 - >>> result = hough_ellipse(img, threshold=8) + >>> result = hough_ellipse(img, threshold=4) + >>> # extract the highest accumulator + >>> heapq.nlargest(1, result) [(10, 10.0, 10.0, 8.0, 6.0, 0.0)] + >>> # To sort them all + >>> results = [heappop(results) for i in range(len(results))] Notes ----- @@ -217,7 +222,8 @@ def hough_ellipse(cnp.ndarray img, int threshold=4, double accuracy=1, elif angle < - np.pi: angle = angle + np.pi / 2. b = sqrt(bin_edges[hist.argmax()]) - results.append((hist_max, # Accumulator + heapq.heappush(results, + (hist_max, # Accumulator x0, y0, a, diff --git a/skimage/transform/tests/test_hough_transform.py b/skimage/transform/tests/test_hough_transform.py index 6c6acb96..57d14278 100644 --- a/skimage/transform/tests/test_hough_transform.py +++ b/skimage/transform/tests/test_hough_transform.py @@ -1,4 +1,5 @@ import numpy as np +import heapq from numpy.testing import (assert_almost_equal, assert_equal, ) @@ -157,11 +158,12 @@ def test_hough_ellipse_zero_angle(): rr, cc = ellipse_perimeter(y0, x0, b, a) img[rr, cc] = 1 result = tf.hough_ellipse(img, threshold=9) - assert_equal(result[0][1], x0) - assert_equal(result[0][2], y0) - assert_almost_equal(result[0][3], b, decimal=1) - assert_almost_equal(result[0][4], a, decimal=1) - assert_equal(result[0][5], angle) + best = heapq.nlargest(1, result)[0] + assert_equal(best[1], x0) + assert_equal(best[2], y0) + assert_almost_equal(best[3], b, decimal=1) + assert_almost_equal(best[4], a, decimal=1) + assert_equal(best[5], angle) def test_hough_ellipse_non_zero_angle(): @@ -174,11 +176,12 @@ def test_hough_ellipse_non_zero_angle(): rr, cc = ellipse_perimeter(y0, x0, b, a, orientation=angle) img[rr, cc] = 1 result = tf.hough_ellipse(img, threshold=15, accuracy=3) - assert_almost_equal(result[0][1] / 100., x0 / 100., decimal=1) - assert_almost_equal(result[0][2] / 100., y0 / 100., decimal=1) - assert_almost_equal(result[0][3] / 10., b / 10., decimal=1) - assert_almost_equal(result[0][4] / 100., a / 100., decimal=1) - assert_almost_equal(result[0][5], angle, decimal=1) + best = heapq.nlargest(1, result)[0] + assert_almost_equal(best[1] / 100., x0 / 100., decimal=1) + assert_almost_equal(best[2] / 100., y0 / 100., decimal=1) + assert_almost_equal(best[3] / 10., b / 10., decimal=1) + assert_almost_equal(best[4] / 100., a / 100., decimal=1) + assert_almost_equal(best[5], angle, decimal=1) def test_hough_ellipse_non_zero_angle2(): @@ -191,11 +194,12 @@ def test_hough_ellipse_non_zero_angle2(): rr, cc = ellipse_perimeter(y0, x0, b, a, orientation=angle) img[rr, cc] = 1 result = tf.hough_ellipse(img, threshold=15, accuracy=3) - assert_almost_equal(result[0][1] / 100., x0 / 100., decimal=1) - assert_almost_equal(result[0][2] / 100., y0 / 100., decimal=1) - assert_almost_equal(result[0][3] / 100., a / 100., decimal=1) - assert_almost_equal(result[0][3] / 100., b / 100., decimal=1) - assert_almost_equal(result[0][5], angle, decimal=1) + best = heapq.nlargest(1, result)[0] + assert_almost_equal(best[1] / 100., x0 / 100., decimal=1) + assert_almost_equal(best[2] / 100., y0 / 100., decimal=1) + assert_almost_equal(best[3] / 100., a / 100., decimal=1) + assert_almost_equal(best[3] / 100., b / 100., decimal=1) + assert_almost_equal(best[5], angle, decimal=1) if __name__ == "__main__": From 742699c570ef03b41ee61c9d1c2513afa99e3ba0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Tue, 6 Aug 2013 16:18:14 +0200 Subject: [PATCH 693/736] TEST: fix precision --- skimage/transform/tests/test_hough_transform.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/transform/tests/test_hough_transform.py b/skimage/transform/tests/test_hough_transform.py index 57d14278..db8d1bbc 100644 --- a/skimage/transform/tests/test_hough_transform.py +++ b/skimage/transform/tests/test_hough_transform.py @@ -197,7 +197,7 @@ def test_hough_ellipse_non_zero_angle2(): best = heapq.nlargest(1, result)[0] assert_almost_equal(best[1] / 100., x0 / 100., decimal=1) assert_almost_equal(best[2] / 100., y0 / 100., decimal=1) - assert_almost_equal(best[3] / 100., a / 100., decimal=1) + assert_almost_equal(best[3] / 10., a / 10., decimal=1) assert_almost_equal(best[3] / 100., b / 100., decimal=1) assert_almost_equal(best[5], angle, decimal=1) From 594a228a0651343dbb06a136fea7b3575c06dfed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Tue, 6 Aug 2013 16:19:14 +0200 Subject: [PATCH 694/736] TEST: fix bad subs --- skimage/transform/tests/test_hough_transform.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/transform/tests/test_hough_transform.py b/skimage/transform/tests/test_hough_transform.py index db8d1bbc..69e92492 100644 --- a/skimage/transform/tests/test_hough_transform.py +++ b/skimage/transform/tests/test_hough_transform.py @@ -198,7 +198,7 @@ def test_hough_ellipse_non_zero_angle2(): assert_almost_equal(best[1] / 100., x0 / 100., decimal=1) assert_almost_equal(best[2] / 100., y0 / 100., decimal=1) assert_almost_equal(best[3] / 10., a / 10., decimal=1) - assert_almost_equal(best[3] / 100., b / 100., decimal=1) + assert_almost_equal(best[4] / 100., b / 100., decimal=1) assert_almost_equal(best[5], angle, decimal=1) From 1a9d5bb4cafcd120255fa482cf96a52b79114a35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Tue, 6 Aug 2013 16:36:55 +0200 Subject: [PATCH 695/736] TEST: revert precision --- skimage/transform/tests/test_hough_transform.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/transform/tests/test_hough_transform.py b/skimage/transform/tests/test_hough_transform.py index 69e92492..8cb4f980 100644 --- a/skimage/transform/tests/test_hough_transform.py +++ b/skimage/transform/tests/test_hough_transform.py @@ -197,7 +197,7 @@ def test_hough_ellipse_non_zero_angle2(): best = heapq.nlargest(1, result)[0] assert_almost_equal(best[1] / 100., x0 / 100., decimal=1) assert_almost_equal(best[2] / 100., y0 / 100., decimal=1) - assert_almost_equal(best[3] / 10., a / 10., decimal=1) + assert_almost_equal(best[3] / 100., a / 100., decimal=1) assert_almost_equal(best[4] / 100., b / 100., decimal=1) assert_almost_equal(best[5], angle, decimal=1) From 6a114e8708f106b07143296eba192ddf65a6dacd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Tue, 6 Aug 2013 16:40:04 +0200 Subject: [PATCH 696/736] MINOR: fix non ascii char --- doc/examples/plot_circular_elliptical_hough_transform.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/examples/plot_circular_elliptical_hough_transform.py b/doc/examples/plot_circular_elliptical_hough_transform.py index d500be79..d8793ca8 100755 --- a/doc/examples/plot_circular_elliptical_hough_transform.py +++ b/doc/examples/plot_circular_elliptical_hough_transform.py @@ -120,7 +120,7 @@ edges = filter.canny(image_gray, sigma=2.0, # The value is chosen in order to get a single high accumulator. # The threshold eliminates low accumulators accum = hough_ellipse(edges, accuracy=10, threshold=150, min_size=50) -# Select the highest accumulator +# Select the highest accumulator best = heapq.nlargest(1, accum)[0] # Estimated parameters for the ellipse center_y = int(best[1]) From 7c652c74d001f14a4a6fb9c86ce4606a12a0baff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Tue, 6 Aug 2013 17:03:55 +0200 Subject: [PATCH 697/736] add missing import heapq --- doc/examples/plot_circular_elliptical_hough_transform.py | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/examples/plot_circular_elliptical_hough_transform.py b/doc/examples/plot_circular_elliptical_hough_transform.py index d8793ca8..a371bf24 100755 --- a/doc/examples/plot_circular_elliptical_hough_transform.py +++ b/doc/examples/plot_circular_elliptical_hough_transform.py @@ -104,6 +104,7 @@ References Conference on. Vol. 2. IEEE, 2002 """ import matplotlib.pyplot as plt +import heapq from skimage import data, filter, color from skimage.transform import hough_ellipse From e27b798ffab87f3d3c4554da0c57c65081e6b1fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Tue, 6 Aug 2013 19:38:03 +0200 Subject: [PATCH 698/736] FIX: handle correctly main axis def --- skimage/transform/_hough_transform.pyx | 13 +- .../transform/tests/test_hough_transform.py | 177 +++++++++++++++--- 2 files changed, 160 insertions(+), 30 deletions(-) diff --git a/skimage/transform/_hough_transform.pyx b/skimage/transform/_hough_transform.pyx index 958bacc4..b14b2d5b 100644 --- a/skimage/transform/_hough_transform.pyx +++ b/skimage/transform/_hough_transform.pyx @@ -123,8 +123,8 @@ def hough_ellipse(cnp.ndarray img, int threshold=4, double accuracy=1, Returns ------- - res : list of tuples [(accumulator, x0, y0, a, b, angle)] - Where (x0, y0) is the center, (a, b) major and minor axis. + res : list of tuples [(accumulator, y0, x0, ry, rx, angle)] + Where (y0, x0) is the center, (ry, rx) main axis. The angle value follows `draw.ellipse_perimeter()` convention. Examples @@ -135,7 +135,7 @@ def hough_ellipse(cnp.ndarray img, int threshold=4, double accuracy=1, >>> result = hough_ellipse(img, threshold=4) >>> # extract the highest accumulator >>> heapq.nlargest(1, result) - [(10, 10.0, 10.0, 8.0, 6.0, 0.0)] + [(10, 10.0, 10.0, 6.0, 8.0, 0.0)] >>> # To sort them all >>> results = [heappop(results) for i in range(len(results))] @@ -211,6 +211,7 @@ def hough_ellipse(cnp.ndarray img, int threshold=4, double accuracy=1, hist_max = np.max(hist) if hist_max > threshold: angle = np.arctan2(p1x - p2x, p1y - p2y) + b = sqrt(bin_edges[hist.argmax()]) # to keep ellipse_perimeter() convention if angle != 0: angle = np.pi - angle @@ -219,13 +220,11 @@ def hough_ellipse(cnp.ndarray img, int threshold=4, double accuracy=1, # that a < b. But we keep a > b. if angle > np.pi: angle = angle - np.pi / 2. - elif angle < - np.pi: - angle = angle + np.pi / 2. - b = sqrt(bin_edges[hist.argmax()]) + a, b = b, a heapq.heappush(results, (hist_max, # Accumulator - x0, y0, + x0, a, b, angle, diff --git a/skimage/transform/tests/test_hough_transform.py b/skimage/transform/tests/test_hough_transform.py index 8cb4f980..281d049d 100644 --- a/skimage/transform/tests/test_hough_transform.py +++ b/skimage/transform/tests/test_hough_transform.py @@ -150,56 +150,187 @@ def test_hough_circle_extended(): def test_hough_ellipse_zero_angle(): img = np.zeros((25, 25), dtype=int) - a = 6 - b = 8 + rx = 6 + ry = 8 x0 = 12 y0 = 15 angle = 0 - rr, cc = ellipse_perimeter(y0, x0, b, a) + rr, cc = ellipse_perimeter(y0, x0, ry, rx) img[rr, cc] = 1 result = tf.hough_ellipse(img, threshold=9) best = heapq.nlargest(1, result)[0] - assert_equal(best[1], x0) - assert_equal(best[2], y0) - assert_almost_equal(best[3], b, decimal=1) - assert_almost_equal(best[4], a, decimal=1) + assert_equal(best[1], y0) + assert_equal(best[2], x0) + assert_almost_equal(best[3], ry, decimal=1) + assert_almost_equal(best[4], rx, decimal=1) assert_equal(best[5], angle) + # Check if I re-draw the ellipse, points are the same! + # ie check API compatibility between hough_ellipse and ellipse_perimeter + rr2, cc2 = ellipse_perimeter(y0, x0, int(best[3]), int(best[4]), orientation=best[5]) + assert_equal(rr, rr2) + assert_equal(cc, cc2) -def test_hough_ellipse_non_zero_angle(): +def test_hough_ellipse_non_zero_posangle1(): + # ry > rx, angle in [0:pi/2] img = np.zeros((30, 24), dtype=int) - a = 6 - b = 12 + rx = 6 + ry = 12 x0 = 10 y0 = 15 angle = np.pi / 1.35 - rr, cc = ellipse_perimeter(y0, x0, b, a, orientation=angle) + rr, cc = ellipse_perimeter(y0, x0, ry, rx, orientation=angle) img[rr, cc] = 1 result = tf.hough_ellipse(img, threshold=15, accuracy=3) best = heapq.nlargest(1, result)[0] - assert_almost_equal(best[1] / 100., x0 / 100., decimal=1) - assert_almost_equal(best[2] / 100., y0 / 100., decimal=1) - assert_almost_equal(best[3] / 10., b / 10., decimal=1) - assert_almost_equal(best[4] / 100., a / 100., decimal=1) + assert_almost_equal(best[1] / 100., y0 / 100., decimal=1) + assert_almost_equal(best[2] / 100., x0 / 100., decimal=1) + assert_almost_equal(best[3] / 10., ry / 10., decimal=1) + assert_almost_equal(best[4] / 100., rx / 100., decimal=1) assert_almost_equal(best[5], angle, decimal=1) + # Check if I re-draw the ellipse, points are the same! + # ie check API compatibility between hough_ellipse and ellipse_perimeter + rr2, cc2 = ellipse_perimeter(y0, x0, int(best[3]), int(best[4]), orientation=best[5]) + assert_equal(rr, rr2) + assert_equal(cc, cc2) -def test_hough_ellipse_non_zero_angle2(): +def test_hough_ellipse_non_zero_posangle2(): + # ry < rx, angle in [0:pi/2] img = np.zeros((30, 24), dtype=int) - b = 6 - a = 12 + rx = 12 + ry = 6 x0 = 10 y0 = 15 angle = np.pi / 1.35 - rr, cc = ellipse_perimeter(y0, x0, b, a, orientation=angle) + rr, cc = ellipse_perimeter(y0, x0, ry, rx, orientation=angle) img[rr, cc] = 1 result = tf.hough_ellipse(img, threshold=15, accuracy=3) best = heapq.nlargest(1, result)[0] - assert_almost_equal(best[1] / 100., x0 / 100., decimal=1) - assert_almost_equal(best[2] / 100., y0 / 100., decimal=1) - assert_almost_equal(best[3] / 100., a / 100., decimal=1) - assert_almost_equal(best[4] / 100., b / 100., decimal=1) + assert_almost_equal(best[1] / 100., y0 / 100., decimal=1) + assert_almost_equal(best[2] / 100., x0 / 100., decimal=1) + assert_almost_equal(best[3] / 10., ry / 10., decimal=1) + assert_almost_equal(best[4] / 100., rx / 100., decimal=1) assert_almost_equal(best[5], angle, decimal=1) + # Check if I re-draw the ellipse, points are the same! + # ie check API compatibility between hough_ellipse and ellipse_perimeter + rr2, cc2 = ellipse_perimeter(y0, x0, int(best[3]), int(best[4]), orientation=best[5]) + assert_equal(rr, rr2) + assert_equal(cc, cc2) + + +def test_hough_ellipse_non_zero_posangle3(): + # ry < rx, angle in [pi/2:pi] + img = np.zeros((30, 24), dtype=int) + rx = 12 + ry = 6 + x0 = 10 + y0 = 15 + angle = np.pi / 1.35 + np.pi / 2. + rr, cc = ellipse_perimeter(y0, x0, ry, rx, orientation=angle) + img[rr, cc] = 1 + result = tf.hough_ellipse(img, threshold=15, accuracy=3) + best = heapq.nlargest(1, result)[0] + # Check if I re-draw the ellipse, points are the same! + # ie check API compatibility between hough_ellipse and ellipse_perimeter + rr2, cc2 = ellipse_perimeter(y0, x0, int(best[3]), int(best[4]), orientation=best[5]) + assert_equal(rr, rr2) + assert_equal(cc, cc2) + + +def test_hough_ellipse_non_zero_posangle4(): + # ry < rx, angle in [pi:3pi/4] + img = np.zeros((30, 24), dtype=int) + rx = 12 + ry = 6 + x0 = 10 + y0 = 15 + angle = np.pi / 1.35 + np.pi + rr, cc = ellipse_perimeter(y0, x0, ry, rx, orientation=angle) + img[rr, cc] = 1 + result = tf.hough_ellipse(img, threshold=15, accuracy=3) + best = heapq.nlargest(1, result)[0] + # Check if I re-draw the ellipse, points are the same! + # ie check API compatibility between hough_ellipse and ellipse_perimeter + rr2, cc2 = ellipse_perimeter(y0, x0, int(best[3]), int(best[4]), orientation=best[5]) + assert_equal(rr, rr2) + assert_equal(cc, cc2) + + +def test_hough_ellipse_non_zero_negangle1(): + # ry > rx, angle in [0:-pi/2] + img = np.zeros((30, 24), dtype=int) + rx = 6 + ry = 12 + x0 = 10 + y0 = 15 + angle = - np.pi / 1.35 + rr, cc = ellipse_perimeter(y0, x0, ry, rx, orientation=angle) + img[rr, cc] = 1 + result = tf.hough_ellipse(img, threshold=15, accuracy=3) + best = heapq.nlargest(1, result)[0] + # Check if I re-draw the ellipse, points are the same! + # ie check API compatibility between hough_ellipse and ellipse_perimeter + rr2, cc2 = ellipse_perimeter(y0, x0, int(best[3]), int(best[4]), orientation=best[5]) + assert_equal(rr, rr2) + assert_equal(cc, cc2) + + +def test_hough_ellipse_non_zero_negangle2(): + # ry < rx, angle in [0:-pi/2] + img = np.zeros((30, 24), dtype=int) + rx = 12 + ry = 6 + x0 = 10 + y0 = 15 + angle = - np.pi / 1.35 + rr, cc = ellipse_perimeter(y0, x0, ry, rx, orientation=angle) + img[rr, cc] = 1 + result = tf.hough_ellipse(img, threshold=15, accuracy=3) + best = heapq.nlargest(1, result)[0] + # Check if I re-draw the ellipse, points are the same! + # ie check API compatibility between hough_ellipse and ellipse_perimeter + rr2, cc2 = ellipse_perimeter(y0, x0, int(best[3]), int(best[4]), orientation=best[5]) + assert_equal(rr, rr2) + assert_equal(cc, cc2) + + +def test_hough_ellipse_non_zero_negangle3(): + # ry < rx, angle in [-pi/2:-pi] + img = np.zeros((30, 24), dtype=int) + rx = 12 + ry = 6 + x0 = 10 + y0 = 15 + angle = - np.pi / 1.35 - np.pi / 2. + rr, cc = ellipse_perimeter(y0, x0, ry, rx, orientation=angle) + img[rr, cc] = 1 + result = tf.hough_ellipse(img, threshold=15, accuracy=3) + best = heapq.nlargest(1, result)[0] + # Check if I re-draw the ellipse, points are the same! + # ie check API compatibility between hough_ellipse and ellipse_perimeter + rr2, cc2 = ellipse_perimeter(y0, x0, int(best[3]), int(best[4]), orientation=best[5]) + assert_equal(rr, rr2) + assert_equal(cc, cc2) + + +def test_hough_ellipse_non_zero_negangle4(): + # ry < rx, angle in [-pi:-3pi/4] + img = np.zeros((30, 24), dtype=int) + rx = 12 + ry = 6 + x0 = 10 + y0 = 15 + angle = - np.pi / 1.35 - np.pi + rr, cc = ellipse_perimeter(y0, x0, ry, rx, orientation=angle) + img[rr, cc] = 1 + result = tf.hough_ellipse(img, threshold=15, accuracy=3) + best = heapq.nlargest(1, result)[0] + # Check if I re-draw the ellipse, points are the same! + # ie check API compatibility between hough_ellipse and ellipse_perimeter + rr2, cc2 = ellipse_perimeter(y0, x0, int(best[3]), int(best[4]), orientation=best[5]) + assert_equal(rr, rr2) + assert_equal(cc, cc2) if __name__ == "__main__": From 313bf0ea9a3e4102e36dbe37d078de13c8f990ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Tue, 6 Aug 2013 20:01:38 +0200 Subject: [PATCH 699/736] fix example according to new API --- doc/examples/plot_circular_elliptical_hough_transform.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/examples/plot_circular_elliptical_hough_transform.py b/doc/examples/plot_circular_elliptical_hough_transform.py index a371bf24..7ada848c 100755 --- a/doc/examples/plot_circular_elliptical_hough_transform.py +++ b/doc/examples/plot_circular_elliptical_hough_transform.py @@ -126,12 +126,12 @@ best = heapq.nlargest(1, accum)[0] # Estimated parameters for the ellipse center_y = int(best[1]) center_x = int(best[2]) -xradius = int(best[3]) -yradius = int(best[4]) -angle = np.pi - best[5] +yradius = int(best[3]) +xradius = int(best[4]) +angle = best[5] # Draw the ellipse on the original image -cx, cy = ellipse_perimeter(center_y, center_x, +cy, cx = ellipse_perimeter(center_y, center_x, yradius, xradius, orientation=angle) image_rgb[cy, cx] = (0, 0, 1) # Draw the edge (white) and the resulting ellipse (red) From df2ee4d6364688839f5b38da593ae0f0186d2bb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Sun, 29 Sep 2013 20:01:24 +0200 Subject: [PATCH 700/736] remove heapq --- ...lot_circular_elliptical_hough_transform.py | 7 +++-- skimage/transform/_hough_transform.pyx | 12 +++------ .../transform/tests/test_hough_transform.py | 27 ++++++++++++------- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/doc/examples/plot_circular_elliptical_hough_transform.py b/doc/examples/plot_circular_elliptical_hough_transform.py index 7ada848c..4c148b39 100755 --- a/doc/examples/plot_circular_elliptical_hough_transform.py +++ b/doc/examples/plot_circular_elliptical_hough_transform.py @@ -104,7 +104,6 @@ References Conference on. Vol. 2. IEEE, 2002 """ import matplotlib.pyplot as plt -import heapq from skimage import data, filter, color from skimage.transform import hough_ellipse @@ -120,9 +119,9 @@ edges = filter.canny(image_gray, sigma=2.0, # The accuracy corresponds to the bin size of a major axis. # The value is chosen in order to get a single high accumulator. # The threshold eliminates low accumulators -accum = hough_ellipse(edges, accuracy=10, threshold=150, min_size=50) -# Select the highest accumulator -best = heapq.nlargest(1, accum)[0] +accum = hough_ellipse(edges, accuracy=10, threshold=170, min_size=50) +accum.sort(key=lambda x:x[0]) +best = accum[-1] # Estimated parameters for the ellipse center_y = int(best[1]) center_x = int(best[2]) diff --git a/skimage/transform/_hough_transform.pyx b/skimage/transform/_hough_transform.pyx index b14b2d5b..47185d79 100644 --- a/skimage/transform/_hough_transform.pyx +++ b/skimage/transform/_hough_transform.pyx @@ -3,7 +3,6 @@ #cython: nonecheck=False #cython: wraparound=False import numpy as np -import heapq cimport numpy as cnp cimport cython @@ -132,12 +131,8 @@ def hough_ellipse(cnp.ndarray img, int threshold=4, double accuracy=1, >>> img = np.zeros((25, 25), dtype=int) >>> rr, cc = draw.ellipse_perimeter(10, 10, 6, 8) >>> img[cc, rr] = 1 - >>> result = hough_ellipse(img, threshold=4) - >>> # extract the highest accumulator - >>> heapq.nlargest(1, result) - [(10, 10.0, 10.0, 6.0, 8.0, 0.0)] - >>> # To sort them all - >>> results = [heappop(results) for i in range(len(results))] + >>> result = hough_ellipse(img, threshold=8) + [(10, 10.0, 8.0, 6.0, 0.0, 10.0)] Notes ----- @@ -221,8 +216,7 @@ def hough_ellipse(cnp.ndarray img, int threshold=4, double accuracy=1, if angle > np.pi: angle = angle - np.pi / 2. a, b = b, a - heapq.heappush(results, - (hist_max, # Accumulator + results.append((hist_max, # Accumulator y0, x0, a, diff --git a/skimage/transform/tests/test_hough_transform.py b/skimage/transform/tests/test_hough_transform.py index 281d049d..82b538b2 100644 --- a/skimage/transform/tests/test_hough_transform.py +++ b/skimage/transform/tests/test_hough_transform.py @@ -1,5 +1,4 @@ import numpy as np -import heapq from numpy.testing import (assert_almost_equal, assert_equal, ) @@ -158,7 +157,7 @@ def test_hough_ellipse_zero_angle(): rr, cc = ellipse_perimeter(y0, x0, ry, rx) img[rr, cc] = 1 result = tf.hough_ellipse(img, threshold=9) - best = heapq.nlargest(1, result)[0] + best = result[-1] assert_equal(best[1], y0) assert_equal(best[2], x0) assert_almost_equal(best[3], ry, decimal=1) @@ -182,7 +181,8 @@ def test_hough_ellipse_non_zero_posangle1(): rr, cc = ellipse_perimeter(y0, x0, ry, rx, orientation=angle) img[rr, cc] = 1 result = tf.hough_ellipse(img, threshold=15, accuracy=3) - best = heapq.nlargest(1, result)[0] + result.sort(key=lambda x:x[0]) + best = result[-1] assert_almost_equal(best[1] / 100., y0 / 100., decimal=1) assert_almost_equal(best[2] / 100., x0 / 100., decimal=1) assert_almost_equal(best[3] / 10., ry / 10., decimal=1) @@ -206,7 +206,8 @@ def test_hough_ellipse_non_zero_posangle2(): rr, cc = ellipse_perimeter(y0, x0, ry, rx, orientation=angle) img[rr, cc] = 1 result = tf.hough_ellipse(img, threshold=15, accuracy=3) - best = heapq.nlargest(1, result)[0] + result.sort(key=lambda x:x[0]) + best = result[-1] assert_almost_equal(best[1] / 100., y0 / 100., decimal=1) assert_almost_equal(best[2] / 100., x0 / 100., decimal=1) assert_almost_equal(best[3] / 10., ry / 10., decimal=1) @@ -230,7 +231,8 @@ def test_hough_ellipse_non_zero_posangle3(): rr, cc = ellipse_perimeter(y0, x0, ry, rx, orientation=angle) img[rr, cc] = 1 result = tf.hough_ellipse(img, threshold=15, accuracy=3) - best = heapq.nlargest(1, result)[0] + result.sort(key=lambda x:x[0]) + best = result[-1] # Check if I re-draw the ellipse, points are the same! # ie check API compatibility between hough_ellipse and ellipse_perimeter rr2, cc2 = ellipse_perimeter(y0, x0, int(best[3]), int(best[4]), orientation=best[5]) @@ -249,7 +251,8 @@ def test_hough_ellipse_non_zero_posangle4(): rr, cc = ellipse_perimeter(y0, x0, ry, rx, orientation=angle) img[rr, cc] = 1 result = tf.hough_ellipse(img, threshold=15, accuracy=3) - best = heapq.nlargest(1, result)[0] + result.sort(key=lambda x:x[0]) + best = result[-1] # Check if I re-draw the ellipse, points are the same! # ie check API compatibility between hough_ellipse and ellipse_perimeter rr2, cc2 = ellipse_perimeter(y0, x0, int(best[3]), int(best[4]), orientation=best[5]) @@ -268,7 +271,8 @@ def test_hough_ellipse_non_zero_negangle1(): rr, cc = ellipse_perimeter(y0, x0, ry, rx, orientation=angle) img[rr, cc] = 1 result = tf.hough_ellipse(img, threshold=15, accuracy=3) - best = heapq.nlargest(1, result)[0] + result.sort(key=lambda x:x[0]) + best = result[-1] # Check if I re-draw the ellipse, points are the same! # ie check API compatibility between hough_ellipse and ellipse_perimeter rr2, cc2 = ellipse_perimeter(y0, x0, int(best[3]), int(best[4]), orientation=best[5]) @@ -287,7 +291,8 @@ def test_hough_ellipse_non_zero_negangle2(): rr, cc = ellipse_perimeter(y0, x0, ry, rx, orientation=angle) img[rr, cc] = 1 result = tf.hough_ellipse(img, threshold=15, accuracy=3) - best = heapq.nlargest(1, result)[0] + result.sort(key=lambda x:x[0]) + best = result[-1] # Check if I re-draw the ellipse, points are the same! # ie check API compatibility between hough_ellipse and ellipse_perimeter rr2, cc2 = ellipse_perimeter(y0, x0, int(best[3]), int(best[4]), orientation=best[5]) @@ -306,7 +311,8 @@ def test_hough_ellipse_non_zero_negangle3(): rr, cc = ellipse_perimeter(y0, x0, ry, rx, orientation=angle) img[rr, cc] = 1 result = tf.hough_ellipse(img, threshold=15, accuracy=3) - best = heapq.nlargest(1, result)[0] + result.sort(key=lambda x:x[0]) + best = result[-1] # Check if I re-draw the ellipse, points are the same! # ie check API compatibility between hough_ellipse and ellipse_perimeter rr2, cc2 = ellipse_perimeter(y0, x0, int(best[3]), int(best[4]), orientation=best[5]) @@ -325,7 +331,8 @@ def test_hough_ellipse_non_zero_negangle4(): rr, cc = ellipse_perimeter(y0, x0, ry, rx, orientation=angle) img[rr, cc] = 1 result = tf.hough_ellipse(img, threshold=15, accuracy=3) - best = heapq.nlargest(1, result)[0] + result.sort(key=lambda x:x[0]) + best = result[-1] # Check if I re-draw the ellipse, points are the same! # ie check API compatibility between hough_ellipse and ellipse_perimeter rr2, cc2 = ellipse_perimeter(y0, x0, int(best[3]), int(best[4]), orientation=best[5]) From 1236e97ff3626a1796b540bc4e00984b089f356e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Wed, 2 Oct 2013 15:38:18 +0200 Subject: [PATCH 701/736] MAINT: np.pi -> M_PI --- skimage/transform/_hough_transform.pyx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/skimage/transform/_hough_transform.pyx b/skimage/transform/_hough_transform.pyx index 47185d79..7f2b153c 100644 --- a/skimage/transform/_hough_transform.pyx +++ b/skimage/transform/_hough_transform.pyx @@ -209,12 +209,12 @@ def hough_ellipse(cnp.ndarray img, int threshold=4, double accuracy=1, b = sqrt(bin_edges[hist.argmax()]) # to keep ellipse_perimeter() convention if angle != 0: - angle = np.pi - angle + angle = M_PI - angle # When angle is not in [-pi:pi] # it would mean in ellipse_perimeter() # that a < b. But we keep a > b. - if angle > np.pi: - angle = angle - np.pi / 2. + if angle > M_PI: + angle = angle - M_PI / 2. a, b = b, a results.append((hist_max, # Accumulator y0, From 003dbe419d9e35b30892af9103dc5c1e15eb6e77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Wed, 2 Oct 2013 15:53:38 +0200 Subject: [PATCH 702/736] MINOR: add missing type --- skimage/transform/_hough_transform.pyx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/transform/_hough_transform.pyx b/skimage/transform/_hough_transform.pyx index 7f2b153c..21f6daf7 100644 --- a/skimage/transform/_hough_transform.pyx +++ b/skimage/transform/_hough_transform.pyx @@ -153,7 +153,7 @@ def hough_ellipse(cnp.ndarray img, int threshold=4, double accuracy=1, cdef Py_ssize_t num_pixels = pixels.shape[0] cdef list acc = list() cdef list results = list() - cdef bin_size = accuracy**2 + cdef double bin_size = accuracy**2 cdef int max_b_squared if max_size is None: From 4f58c076d46eca78949f80739fd6b0722daf4ff7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Wed, 2 Oct 2013 16:17:42 +0200 Subject: [PATCH 703/736] FIX: import M_PI --- skimage/transform/_hough_transform.pyx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/transform/_hough_transform.pyx b/skimage/transform/_hough_transform.pyx index 21f6daf7..2d09751f 100644 --- a/skimage/transform/_hough_transform.pyx +++ b/skimage/transform/_hough_transform.pyx @@ -7,7 +7,7 @@ import numpy as np cimport numpy as cnp cimport cython -from libc.math cimport abs, fabs, sqrt, ceil +from libc.math cimport abs, fabs, sqrt, ceil, M_PI from libc.stdlib cimport rand from skimage.draw import circle_perimeter From def4ab8ccce26181ff19bbe080e9eb268607c2c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Sun, 6 Oct 2013 22:22:13 +0200 Subject: [PATCH 704/736] PEP8 --- doc/examples/plot_circular_elliptical_hough_transform.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/examples/plot_circular_elliptical_hough_transform.py b/doc/examples/plot_circular_elliptical_hough_transform.py index 4c148b39..e1c868fe 100755 --- a/doc/examples/plot_circular_elliptical_hough_transform.py +++ b/doc/examples/plot_circular_elliptical_hough_transform.py @@ -120,7 +120,7 @@ edges = filter.canny(image_gray, sigma=2.0, # The value is chosen in order to get a single high accumulator. # The threshold eliminates low accumulators accum = hough_ellipse(edges, accuracy=10, threshold=170, min_size=50) -accum.sort(key=lambda x:x[0]) +accum.sort(key=lambda x: x[0]) best = accum[-1] # Estimated parameters for the ellipse center_y = int(best[1]) From e68ba0db019568325ed648f56dcfa20b256efbb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Fri, 11 Oct 2013 18:05:40 +0200 Subject: [PATCH 705/736] Use libc.math.atan2 --- skimage/transform/_hough_transform.pyx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skimage/transform/_hough_transform.pyx b/skimage/transform/_hough_transform.pyx index 2d09751f..2ea636fd 100644 --- a/skimage/transform/_hough_transform.pyx +++ b/skimage/transform/_hough_transform.pyx @@ -7,7 +7,7 @@ import numpy as np cimport numpy as cnp cimport cython -from libc.math cimport abs, fabs, sqrt, ceil, M_PI +from libc.math cimport abs, fabs, sqrt, ceil, atan2, M_PI from libc.stdlib cimport rand from skimage.draw import circle_perimeter @@ -205,7 +205,7 @@ def hough_ellipse(cnp.ndarray img, int threshold=4, double accuracy=1, hist, bin_edges = np.histogram(acc, bins=bins) hist_max = np.max(hist) if hist_max > threshold: - angle = np.arctan2(p1x - p2x, p1y - p2y) + angle = atan2(p1x - p2x, p1y - p2y) b = sqrt(bin_edges[hist.argmax()]) # to keep ellipse_perimeter() convention if angle != 0: From 6c677388087ac9524f8d7623809e39c480f90887 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Fri, 11 Oct 2013 18:52:27 +0200 Subject: [PATCH 706/736] Miscellaneous fixes and improvements --- ...lot_circular_elliptical_hough_transform.py | 42 +++++------ skimage/draw/_draw.pyx | 4 +- skimage/transform/_hough_transform.pyx | 75 ++++++++++--------- .../transform/tests/test_hough_transform.py | 71 +++++++++++------- 4 files changed, 105 insertions(+), 87 deletions(-) diff --git a/doc/examples/plot_circular_elliptical_hough_transform.py b/doc/examples/plot_circular_elliptical_hough_transform.py index e1c868fe..7fb67046 100755 --- a/doc/examples/plot_circular_elliptical_hough_transform.py +++ b/doc/examples/plot_circular_elliptical_hough_transform.py @@ -74,7 +74,6 @@ for idx in np.argsort(accums)[::-1][:5]: image[cy, cx] = (220, 20, 20) ax.imshow(image, cmap=plt.cm.gray) -plt.show() """ @@ -96,13 +95,13 @@ an ellipse passes to them. A good match corresponds to high accumulator values. A full description of the algorithm can be found in reference [1]_. - References ---------- .. [1] Xie, Yonghong, and Qiang Ji. "A new efficient ellipse detection method." Pattern Recognition, 2002. Proceedings. 16th International Conference on. Vol. 2. IEEE, 2002 """ + import matplotlib.pyplot as plt from skimage import data, filter, color @@ -110,7 +109,7 @@ from skimage.transform import hough_ellipse from skimage.draw import ellipse_perimeter # Load picture, convert to grayscale and detect edges -image_rgb = data.load('coffee.png')[0:220, 100:450] +image_rgb = data.coffee()[0:220, 160:420] image_gray = color.rgb2gray(image_rgb) edges = filter.canny(image_gray, sigma=2.0, low_threshold=0.55, high_threshold=0.8) @@ -119,30 +118,31 @@ edges = filter.canny(image_gray, sigma=2.0, # The accuracy corresponds to the bin size of a major axis. # The value is chosen in order to get a single high accumulator. # The threshold eliminates low accumulators -accum = hough_ellipse(edges, accuracy=10, threshold=170, min_size=50) -accum.sort(key=lambda x: x[0]) -best = accum[-1] +result = hough_ellipse(edges, accuracy=20, threshold=250, + min_size=100, max_size=120) +result.sort(order='accumulator') + # Estimated parameters for the ellipse -center_y = int(best[1]) -center_x = int(best[2]) -yradius = int(best[3]) -xradius = int(best[4]) -angle = best[5] +best = result[-1] +yc = int(best[1]) +xc = int(best[2]) +a = int(best[3]) +b = int(best[4]) +orientation = best[5] # Draw the ellipse on the original image -cy, cx = ellipse_perimeter(center_y, center_x, - yradius, xradius, orientation=angle) -image_rgb[cy, cx] = (0, 0, 1) +cy, cx = ellipse_perimeter(yc, xc, a, b, orientation) +image_rgb[cy, cx] = (0, 0, 255) # Draw the edge (white) and the resulting ellipse (red) edges = color.gray2rgb(edges) edges[cy, cx] = (250, 0, 0) -fig = plt.subplots(figsize=(10, 6)) -plt.subplot(1, 2, 1) -plt.title('Original picture') -plt.imshow(image_rgb) -plt.subplot(1, 2, 2) -plt.title('Edge (white) and result (red)') -plt.imshow(edges) +fig2, (ax1, ax2) = plt.subplots(ncols=2, nrows=1, figsize=(10, 6)) + +ax1.set_title('Original picture') +ax1.imshow(image_rgb) + +ax2.set_title('Edge (white) and result (red)') +ax2.imshow(edges) plt.show() diff --git a/skimage/draw/_draw.pyx b/skimage/draw/_draw.pyx index 3d48ce86..c22600a4 100644 --- a/skimage/draw/_draw.pyx +++ b/skimage/draw/_draw.pyx @@ -449,9 +449,9 @@ def ellipse_perimeter(Py_ssize_t cy, Py_ssize_t cx, Py_ssize_t yradius, ---------- cy, cx : int Centre coordinate of ellipse. - yradius, xradius: int + yradius, xradius : int Minor and major semi-axes. ``(x/xradius)**2 + (y/yradius)**2 = 1``. - orientation: double, optional (default 0) + orientation : double, optional (default 0) Major axis orientation in clockwise direction as radians. Returns diff --git a/skimage/transform/_hough_transform.pyx b/skimage/transform/_hough_transform.pyx index 2ea636fd..29344fa8 100644 --- a/skimage/transform/_hough_transform.pyx +++ b/skimage/transform/_hough_transform.pyx @@ -122,14 +122,15 @@ def hough_ellipse(cnp.ndarray img, int threshold=4, double accuracy=1, Returns ------- - res : list of tuples [(accumulator, y0, x0, ry, rx, angle)] - Where (y0, x0) is the center, (ry, rx) main axis. - The angle value follows `draw.ellipse_perimeter()` convention. + result : ndarray with fields [(accumulator, y0, x0, a, b, orientation)] + Where ``(yc, xc)`` is the center, ``(a, b)`` the major and minor + axes, respectively. The `orientation` value follows + `skimage.draw.ellipse_perimeter` convention. Examples -------- - >>> img = np.zeros((25, 25), dtype=int) - >>> rr, cc = draw.ellipse_perimeter(10, 10, 6, 8) + >>> img = np.zeros((25, 25), dtype=np.uint8) + >>> rr, cc = ellipse_perimeter(10, 10, 6, 8) >>> img[cc, rr] = 1 >>> result = hough_ellipse(img, threshold=8) [(10, 10.0, 8.0, 6.0, 0.0, 10.0)] @@ -149,47 +150,47 @@ def hough_ellipse(cnp.ndarray img, int threshold=4, double accuracy=1, if img.ndim != 2: raise ValueError('The input image must be 2D.') - cdef Py_ssize_t[:, :] pixels = np.transpose(np.nonzero(img)) - cdef Py_ssize_t num_pixels = pixels.shape[0] + cdef Py_ssize_t[:, ::1] pixels = np.row_stack(np.nonzero(img)) + cdef Py_ssize_t num_pixels = pixels.shape[1] cdef list acc = list() cdef list results = list() - cdef double bin_size = accuracy**2 + cdef double bin_size = accuracy ** 2 cdef int max_b_squared if max_size is None: if img.shape[0] < img.shape[1]: - max_b_squared = np.round(0.5 * img.shape[0])**2 + max_b_squared = np.round(0.5 * img.shape[0]) ** 2 else: - max_b_squared = np.round(0.5 * img.shape[1])**2 + max_b_squared = np.round(0.5 * img.shape[1]) ** 2 else: max_b_squared = max_size**2 cdef Py_ssize_t p1, p2, p3, p1x, p1y, p2x, p2y, p3x, p3y - cdef double x0, y0, a, b, d, k - cdef double cos_tau_squared, b_squared, f_squared, angle + cdef double xc, yc, a, b, d, k + cdef double cos_tau_squared, b_squared, f_squared, orientation for p1 in range(num_pixels): - p1x = pixels[p1, 1] - p1y = pixels[p1, 0] + p1x = pixels[1, p1] + p1y = pixels[0, p1] for p2 in range(p1): - p2x = pixels[p2, 1] - p2y = pixels[p2, 0] + p2x = pixels[1, p2] + p2y = pixels[0, p2] - # Candidate: center (x0, y0) and main axis a + # Candidate: center (xc, yc) and main axis a a = 0.5 * sqrt((p1x - p2x)**2 + (p1y - p2y)**2) if a > 0.5 * min_size: - x0 = 0.5 * (p1x + p2x) - y0 = 0.5 * (p1y + p2y) + xc = 0.5 * (p1x + p2x) + yc = 0.5 * (p1y + p2y) for p3 in range(num_pixels): - p3x = pixels[p3, 1] - p3y = pixels[p3, 0] + p3x = pixels[1, p3] + p3y = pixels[0, p3] - d = sqrt((p3x - x0)**2 + (p3y - y0)**2) + d = sqrt((p3x - xc)**2 + (p3y - yc)**2) if d > min_size: f_squared = (p3x - p1x)**2 + (p3y - p1y)**2 - cos_tau_squared = ((a**2 + d**2 - f_squared) \ + cos_tau_squared = ((a**2 + d**2 - f_squared) / (2 * a * d))**2 # Consider b2 > 0 and avoid division by zero k = a**2 - d**2 * cos_tau_squared @@ -205,27 +206,29 @@ def hough_ellipse(cnp.ndarray img, int threshold=4, double accuracy=1, hist, bin_edges = np.histogram(acc, bins=bins) hist_max = np.max(hist) if hist_max > threshold: - angle = atan2(p1x - p2x, p1y - p2y) + orientation = atan2(p1x - p2x, p1y - p2y) b = sqrt(bin_edges[hist.argmax()]) # to keep ellipse_perimeter() convention - if angle != 0: - angle = M_PI - angle - # When angle is not in [-pi:pi] + if orientation != 0: + orientation = M_PI - orientation + # When orientation is not in [-pi:pi] # it would mean in ellipse_perimeter() # that a < b. But we keep a > b. - if angle > M_PI: - angle = angle - M_PI / 2. + if orientation > M_PI: + orientation = orientation - M_PI / 2. a, b = b, a results.append((hist_max, # Accumulator - y0, - x0, - a, - b, - angle, - )) + yc, xc, + a, b, + orientation)) acc = [] - return results + return np.array(results, dtype=[('accumulator', np.intp), + ('yc', np.double), + ('xc', np.double), + ('a', np.double), + ('b', np.double), + ('orientation', np.double)]) def hough_line(cnp.ndarray img, diff --git a/skimage/transform/tests/test_hough_transform.py b/skimage/transform/tests/test_hough_transform.py index 82b538b2..fb19d8c1 100644 --- a/skimage/transform/tests/test_hough_transform.py +++ b/skimage/transform/tests/test_hough_transform.py @@ -1,7 +1,5 @@ import numpy as np -from numpy.testing import (assert_almost_equal, - assert_equal, - ) +from numpy.testing import assert_almost_equal, assert_equal import skimage.transform as tf from skimage.draw import line, circle_perimeter, ellipse_perimeter @@ -81,8 +79,10 @@ def test_hough_line_peaks_dist(): img[:, 30] = True img[:, 40] = True hspace, angles, dists = tf.hough_line(img) - assert len(tf.hough_line_peaks(hspace, angles, dists, min_distance=5)[0]) == 2 - assert len(tf.hough_line_peaks(hspace, angles, dists, min_distance=15)[0]) == 1 + assert len(tf.hough_line_peaks(hspace, angles, dists, + min_distance=5)[0]) == 2 + assert len(tf.hough_line_peaks(hspace, angles, dists, + min_distance=15)[0]) == 1 def test_hough_line_peaks_angle(): @@ -91,18 +91,24 @@ def test_hough_line_peaks_angle(): img[0, :] = True hspace, angles, dists = tf.hough_line(img) - assert len(tf.hough_line_peaks(hspace, angles, dists, min_angle=45)[0]) == 2 - assert len(tf.hough_line_peaks(hspace, angles, dists, min_angle=90)[0]) == 1 + assert len(tf.hough_line_peaks(hspace, angles, dists, + min_angle=45)[0]) == 2 + assert len(tf.hough_line_peaks(hspace, angles, dists, + min_angle=90)[0]) == 1 theta = np.linspace(0, np.pi, 100) hspace, angles, dists = tf.hough_line(img, theta) - assert len(tf.hough_line_peaks(hspace, angles, dists, min_angle=45)[0]) == 2 - assert len(tf.hough_line_peaks(hspace, angles, dists, min_angle=90)[0]) == 1 + assert len(tf.hough_line_peaks(hspace, angles, dists, + min_angle=45)[0]) == 2 + assert len(tf.hough_line_peaks(hspace, angles, dists, + min_angle=90)[0]) == 1 theta = np.linspace(np.pi / 3, 4. / 3 * np.pi, 100) hspace, angles, dists = tf.hough_line(img, theta) - assert len(tf.hough_line_peaks(hspace, angles, dists, min_angle=45)[0]) == 2 - assert len(tf.hough_line_peaks(hspace, angles, dists, min_angle=90)[0]) == 1 + assert len(tf.hough_line_peaks(hspace, angles, dists, + min_angle=45)[0]) == 2 + assert len(tf.hough_line_peaks(hspace, angles, dists, + min_angle=90)[0]) == 1 def test_hough_line_peaks_num(): @@ -165,7 +171,8 @@ def test_hough_ellipse_zero_angle(): assert_equal(best[5], angle) # Check if I re-draw the ellipse, points are the same! # ie check API compatibility between hough_ellipse and ellipse_perimeter - rr2, cc2 = ellipse_perimeter(y0, x0, int(best[3]), int(best[4]), orientation=best[5]) + rr2, cc2 = ellipse_perimeter(y0, x0, int(best[3]), int(best[4]), + orientation=best[5]) assert_equal(rr, rr2) assert_equal(cc, cc2) @@ -181,7 +188,7 @@ def test_hough_ellipse_non_zero_posangle1(): rr, cc = ellipse_perimeter(y0, x0, ry, rx, orientation=angle) img[rr, cc] = 1 result = tf.hough_ellipse(img, threshold=15, accuracy=3) - result.sort(key=lambda x:x[0]) + result.sort(order='accumulator') best = result[-1] assert_almost_equal(best[1] / 100., y0 / 100., decimal=1) assert_almost_equal(best[2] / 100., x0 / 100., decimal=1) @@ -190,7 +197,8 @@ def test_hough_ellipse_non_zero_posangle1(): assert_almost_equal(best[5], angle, decimal=1) # Check if I re-draw the ellipse, points are the same! # ie check API compatibility between hough_ellipse and ellipse_perimeter - rr2, cc2 = ellipse_perimeter(y0, x0, int(best[3]), int(best[4]), orientation=best[5]) + rr2, cc2 = ellipse_perimeter(y0, x0, int(best[3]), int(best[4]), + orientation=best[5]) assert_equal(rr, rr2) assert_equal(cc, cc2) @@ -206,7 +214,7 @@ def test_hough_ellipse_non_zero_posangle2(): rr, cc = ellipse_perimeter(y0, x0, ry, rx, orientation=angle) img[rr, cc] = 1 result = tf.hough_ellipse(img, threshold=15, accuracy=3) - result.sort(key=lambda x:x[0]) + result.sort(order='accumulator') best = result[-1] assert_almost_equal(best[1] / 100., y0 / 100., decimal=1) assert_almost_equal(best[2] / 100., x0 / 100., decimal=1) @@ -215,7 +223,8 @@ def test_hough_ellipse_non_zero_posangle2(): assert_almost_equal(best[5], angle, decimal=1) # Check if I re-draw the ellipse, points are the same! # ie check API compatibility between hough_ellipse and ellipse_perimeter - rr2, cc2 = ellipse_perimeter(y0, x0, int(best[3]), int(best[4]), orientation=best[5]) + rr2, cc2 = ellipse_perimeter(y0, x0, int(best[3]), int(best[4]), + orientation=best[5]) assert_equal(rr, rr2) assert_equal(cc, cc2) @@ -231,11 +240,12 @@ def test_hough_ellipse_non_zero_posangle3(): rr, cc = ellipse_perimeter(y0, x0, ry, rx, orientation=angle) img[rr, cc] = 1 result = tf.hough_ellipse(img, threshold=15, accuracy=3) - result.sort(key=lambda x:x[0]) + result.sort(order='accumulator') best = result[-1] # Check if I re-draw the ellipse, points are the same! # ie check API compatibility between hough_ellipse and ellipse_perimeter - rr2, cc2 = ellipse_perimeter(y0, x0, int(best[3]), int(best[4]), orientation=best[5]) + rr2, cc2 = ellipse_perimeter(y0, x0, int(best[3]), int(best[4]), + orientation=best[5]) assert_equal(rr, rr2) assert_equal(cc, cc2) @@ -251,11 +261,12 @@ def test_hough_ellipse_non_zero_posangle4(): rr, cc = ellipse_perimeter(y0, x0, ry, rx, orientation=angle) img[rr, cc] = 1 result = tf.hough_ellipse(img, threshold=15, accuracy=3) - result.sort(key=lambda x:x[0]) + result.sort(order='accumulator') best = result[-1] # Check if I re-draw the ellipse, points are the same! # ie check API compatibility between hough_ellipse and ellipse_perimeter - rr2, cc2 = ellipse_perimeter(y0, x0, int(best[3]), int(best[4]), orientation=best[5]) + rr2, cc2 = ellipse_perimeter(y0, x0, int(best[3]), int(best[4]), + orientation=best[5]) assert_equal(rr, rr2) assert_equal(cc, cc2) @@ -271,11 +282,12 @@ def test_hough_ellipse_non_zero_negangle1(): rr, cc = ellipse_perimeter(y0, x0, ry, rx, orientation=angle) img[rr, cc] = 1 result = tf.hough_ellipse(img, threshold=15, accuracy=3) - result.sort(key=lambda x:x[0]) + result.sort(order='accumulator') best = result[-1] # Check if I re-draw the ellipse, points are the same! # ie check API compatibility between hough_ellipse and ellipse_perimeter - rr2, cc2 = ellipse_perimeter(y0, x0, int(best[3]), int(best[4]), orientation=best[5]) + rr2, cc2 = ellipse_perimeter(y0, x0, int(best[3]), int(best[4]), + orientation=best[5]) assert_equal(rr, rr2) assert_equal(cc, cc2) @@ -291,11 +303,12 @@ def test_hough_ellipse_non_zero_negangle2(): rr, cc = ellipse_perimeter(y0, x0, ry, rx, orientation=angle) img[rr, cc] = 1 result = tf.hough_ellipse(img, threshold=15, accuracy=3) - result.sort(key=lambda x:x[0]) + result.sort(order='accumulator') best = result[-1] # Check if I re-draw the ellipse, points are the same! # ie check API compatibility between hough_ellipse and ellipse_perimeter - rr2, cc2 = ellipse_perimeter(y0, x0, int(best[3]), int(best[4]), orientation=best[5]) + rr2, cc2 = ellipse_perimeter(y0, x0, int(best[3]), int(best[4]), + orientation=best[5]) assert_equal(rr, rr2) assert_equal(cc, cc2) @@ -311,11 +324,12 @@ def test_hough_ellipse_non_zero_negangle3(): rr, cc = ellipse_perimeter(y0, x0, ry, rx, orientation=angle) img[rr, cc] = 1 result = tf.hough_ellipse(img, threshold=15, accuracy=3) - result.sort(key=lambda x:x[0]) + result.sort(order='accumulator') best = result[-1] # Check if I re-draw the ellipse, points are the same! # ie check API compatibility between hough_ellipse and ellipse_perimeter - rr2, cc2 = ellipse_perimeter(y0, x0, int(best[3]), int(best[4]), orientation=best[5]) + rr2, cc2 = ellipse_perimeter(y0, x0, int(best[3]), int(best[4]), + orientation=best[5]) assert_equal(rr, rr2) assert_equal(cc, cc2) @@ -331,11 +345,12 @@ def test_hough_ellipse_non_zero_negangle4(): rr, cc = ellipse_perimeter(y0, x0, ry, rx, orientation=angle) img[rr, cc] = 1 result = tf.hough_ellipse(img, threshold=15, accuracy=3) - result.sort(key=lambda x:x[0]) + result.sort(order='accumulator') best = result[-1] # Check if I re-draw the ellipse, points are the same! # ie check API compatibility between hough_ellipse and ellipse_perimeter - rr2, cc2 = ellipse_perimeter(y0, x0, int(best[3]), int(best[4]), orientation=best[5]) + rr2, cc2 = ellipse_perimeter(y0, x0, int(best[3]), int(best[4]), + orientation=best[5]) assert_equal(rr, rr2) assert_equal(cc, cc2) From f20aa5e66191cb80b1e04725b85b4bf08da5cf75 Mon Sep 17 00:00:00 2001 From: "Josh Warner (Mac)" Date: Sat, 12 Oct 2013 09:32:46 -0500 Subject: [PATCH 707/736] FIX: Better handling of skimage.data.camera for Python3 compatibility --- skimage/util/tests/test_random_noise.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/skimage/util/tests/test_random_noise.py b/skimage/util/tests/test_random_noise.py index b1dece36..4d1752a0 100644 --- a/skimage/util/tests/test_random_noise.py +++ b/skimage/util/tests/test_random_noise.py @@ -29,7 +29,7 @@ def test_salt(): def test_pepper(): seed = 42 cam = img_as_float(camera()) - data_signed = (cam / 255.) * 2. - 1. # Same image, on range [-1, 1] + data_signed = cam * 2. - 1. # Same image, on range [-1, 1] cam_noisy = random_noise(cam, seed=seed, mode='pepper', amount=0.15) peppermask = cam != cam_noisy @@ -109,8 +109,8 @@ def test_poisson(): def test_clip_poisson(): seed = 42 - data = camera() # 512x512 grayscale uint8 - data_signed = (data / 255.) * 2. - 1. # Same image, on range [-1, 1] + data = camera() # 512x512 grayscale uint8 + data_signed = img_as_float(data) * 2. - 1. # Same image, on range [-1, 1] # Signed and unsigned, clipped cam_poisson = random_noise(data, mode='poisson', seed=seed, clip=True) @@ -129,8 +129,8 @@ def test_clip_poisson(): def test_clip_gaussian(): seed = 42 - data = camera() # 512x512 grayscale uint8 - data_signed = (data / 255.) * 2. - 1. # Same image, on range [-1, 1] + data = camera() # 512x512 grayscale uint8 + data_signed = img_as_float(data) * 2. - 1. # Same image, on range [-1, 1] # Signed and unsigned, clipped cam_gauss = random_noise(data, mode='gaussian', seed=seed, clip=True) @@ -149,8 +149,8 @@ def test_clip_gaussian(): def test_clip_speckle(): seed = 42 - data = camera() # 512x512 grayscale uint8 - data_signed = (data / 255.) * 2. - 1. # Same image, on range [-1, 1] + data = camera() # 512x512 grayscale uint8 + data_signed = img_as_float(data) * 2. - 1. # Same image, on range [-1, 1] # Signed and unsigned, clipped cam_speckle = random_noise(data, mode='speckle', seed=seed, clip=True) From 98dcc736e1e1a8509776fc4d1e740932c9813579 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sat, 12 Oct 2013 09:38:29 -0500 Subject: [PATCH 708/736] Plot cleanup and tweak to total PRs line. --- doc/tools/plot_pr.py | 89 ++++++++++++++++++++++++-------------------- 1 file changed, 49 insertions(+), 40 deletions(-) diff --git a/doc/tools/plot_pr.py b/doc/tools/plot_pr.py index aa9a6653..ee5ca88d 100644 --- a/doc/tools/plot_pr.py +++ b/doc/tools/plot_pr.py @@ -1,14 +1,15 @@ -import urllib import json -import copy +import urllib +import dateutil.parser from collections import OrderedDict +from datetime import datetime, timedelta +from dateutil.relativedelta import relativedelta +import numpy as np import matplotlib.pyplot as plt from matplotlib.ticker import FuncFormatter +from matplotlib.transforms import blended_transform_factory -import dateutil.parser -from dateutil.relativedelta import relativedelta -from datetime import datetime, timedelta cache = '_pr_cache.txt' @@ -32,8 +33,6 @@ releases = OrderedDict([ month_duration = 24 -for r in releases: - releases[r] = dateutil.parser.parse(releases[r]) def fetch_PRs(user='scikit-image', repo='scikit-image', state='open'): params = {'state': state, @@ -48,12 +47,12 @@ def fetch_PRs(user='scikit-image', repo='scikit-image', state='open'): 'repo': repo, 'params': urllib.urlencode(params)} - fetch_status = 'Fetching page %(page)d (state=%(state)s)' % params + \ - ' from %(user)s/%(repo)s...' % config + fetch_status = ('Fetching page %(page)d (state=%(state)s)' % params + + ' from %(user)s/%(repo)s...' % config) print(fetch_status) f = urllib.urlopen( - 'https://api.github.com/repos/%(user)s/%(repo)s/pulls?%(params)s' \ + 'https://api.github.com/repos/%(user)s/%(repo)s/pulls?%(params)s' % config ) @@ -69,6 +68,31 @@ def fetch_PRs(user='scikit-image', repo='scikit-image', state='open'): return data + +def seconds_from_epoch(dates): + seconds = [(dt - epoch).total_seconds() for dt in dates] + return seconds + + +def get_month_bins(dates): + now = datetime.now(tz=dates[0].tzinfo) + this_month = datetime(year=now.year, month=now.month, day=1, + tzinfo=dates[0].tzinfo) + + bins = [this_month - relativedelta(months=i) + for i in reversed(range(-1, month_duration))] + return seconds_from_epoch(bins) + + +def date_formatter(value, _): + dt = epoch + timedelta(seconds=value) + return dt.strftime('%Y/%m') + + +for r in releases: + releases[r] = dateutil.parser.parse(releases[r]) + + try: PRs = json.loads(open(cache, 'r').read()) print('Loaded PRs from cache...') @@ -89,28 +113,13 @@ dates = [dateutil.parser.parse(pr['created_at']) for pr in PRs] epoch = datetime(2009, 1, 1, tzinfo=dates[0].tzinfo) -def seconds_from_epoch(dates): - seconds = [(dt - epoch).total_seconds() for dt in dates] - return seconds - dates_f = seconds_from_epoch(dates) +bins = get_month_bins(dates) -def date_formatter(value, _): - dt = epoch + timedelta(seconds=value) - return dt.strftime('%Y/%m') +fig, ax = plt.subplots(figsize=(7, 5)) -plt.figure(figsize=(7, 5)) +n, bins, _ = ax.hist(dates_f, bins=bins, color='blue', alpha=0.6) -now = datetime.now(tz=dates[0].tzinfo) -this_month = datetime(year=now.year, month=now.month, day=1, - tzinfo=dates[0].tzinfo) - -bins = [this_month - relativedelta(months=i) \ - for i in reversed(range(-1, month_duration))] -bins = seconds_from_epoch(bins) -n, bins, _ = plt.hist(dates_f, bins=bins) - -ax = plt.gca() ax.xaxis.set_major_formatter(FuncFormatter(date_formatter)) ax.set_xticks(bins[:-1]) @@ -119,26 +128,26 @@ for l in labels: l.set_rotation(40) l.set_size(10) +mixed_transform = blended_transform_factory(ax.transData, ax.transAxes) for version, date in releases.items(): date = seconds_from_epoch([date])[0] - plt.axvline(date, color='r', label=version) - plt.text(date, n.max() * 0.9, version, color='orange', rotation=90, - fontsize=16) + ax.axvline(date, color='black', linestyle=':', label=version) + ax.text(date, 1, version, color='r', va='bottom', ha='center', + transform=mixed_transform) -plt.title('Pull request activity').set_y(1.05) -plt.xlabel('Date') -plt.ylabel('PRs per month') -plt.subplots_adjust(top=0.875, bottom=0.225) +ax.set_title('Pull request activity').set_y(1.05) +ax.set_xlabel('Date') +ax.set_ylabel('PRs per month', color='blue') +fig.subplots_adjust(top=0.875, bottom=0.225) -import numpy as np cumulative = np.cumsum(n) cumulative += len(dates) - cumulative[-1] -ax2 = plt.twinx() -ax2.plot(bins[:-1], cumulative, 'black', linewidth=2) -ax2.set_ylabel('Total PRs') +ax2 = ax.twinx() +ax2.plot(bins[1:], cumulative, color='black', linewidth=2) +ax2.set_ylabel('Total PRs', color='black') -plt.savefig('PRs.png') +fig.savefig('PRs.png') plt.show() From e7b36b56eee63713f9aa09bce297c62323c213b9 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sat, 12 Oct 2013 10:06:33 -0500 Subject: [PATCH 709/736] Add hough_circles change to API doc --- doc/source/api_changes.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/source/api_changes.txt b/doc/source/api_changes.txt index bf9ddb60..c7293bc5 100644 --- a/doc/source/api_changes.txt +++ b/doc/source/api_changes.txt @@ -3,6 +3,8 @@ Version 0.9 - No longer wrap ``imread`` output in an ``Image`` class - Change default value of `sigma` parameter in ``skimage.segmentation.slic`` to 0 +- ``hough_circle`` now returns a stack of arrays that are the same size as the + input image. Set the ``full_output`` flag to True for the old behavior. Version 0.4 ----------- From e93e8651983ed6293c245ebdf83d003d56f798b4 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Sat, 12 Oct 2013 10:17:55 -0500 Subject: [PATCH 710/736] Label dates every 3 months instead of every month --- doc/tools/plot_pr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/tools/plot_pr.py b/doc/tools/plot_pr.py index ee5ca88d..5f9b4aa6 100644 --- a/doc/tools/plot_pr.py +++ b/doc/tools/plot_pr.py @@ -121,7 +121,7 @@ fig, ax = plt.subplots(figsize=(7, 5)) n, bins, _ = ax.hist(dates_f, bins=bins, color='blue', alpha=0.6) ax.xaxis.set_major_formatter(FuncFormatter(date_formatter)) -ax.set_xticks(bins[:-1]) +ax.set_xticks(bins[2:-1:3]) # Date label every 3 months. labels = ax.get_xticklabels() for l in labels: From b022dd6dfbd0b5ecc8349e54faec23bac0b8e5db Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Sat, 12 Oct 2013 19:10:23 +0200 Subject: [PATCH 711/736] Simplify output handling for binary morphology. --- skimage/morphology/binary.py | 68 +++++++------------------ skimage/morphology/tests/test_binary.py | 21 -------- 2 files changed, 18 insertions(+), 71 deletions(-) diff --git a/skimage/morphology/binary.py b/skimage/morphology/binary.py index 11a14d49..81294d80 100644 --- a/skimage/morphology/binary.py +++ b/skimage/morphology/binary.py @@ -3,46 +3,15 @@ import numpy as np from scipy import ndimage -def _convolve(image, selem, out, cval): - - # determine the smallest integer dtype which does not overflow - selem = selem != 0 - selem_sum = np.sum(selem) - if selem_sum < 2 ** 8: - out_dtype = np.uint8 - else: - out_dtype = np.intp - - if out is None: - out = np.zeros_like(image, dtype=out_dtype) - else: - warnings.warn('Parameter `out` is deprecated and it does not equal ' - 'the output image if the sum of the structuring element ' - 'overflows the dtype of `out`.') - iinfo = np.iinfo(out.dtype) - if iinfo.max - iinfo.min < selem_sum: - raise ValueError('Sum of structuring (=%d) element results in ' - 'overflow for dtype of `out`. You must raise the ' - 'bit-depth.') - - conv = ndimage.convolve(image > 0, selem, output=out, - mode='constant', cval=cval) - - if conv is not None: - out = conv - - return out, selem_sum - - def binary_erosion(image, selem, out=None): """Return fast binary morphological erosion of an image. This function returns the same result as greyscale erosion but performs faster for binary images. - Morphological erosion sets a pixel at (i,j) to the minimum over all pixels - in the neighborhood centered at (i,j). Erosion shrinks bright regions and - enlarges dark regions. + Morphological erosion sets a pixel at ``(i,j)`` to the minimum over all + pixels in the neighborhood centered at ``(i,j)``. Erosion shrinks bright + regions and enlarges dark regions. Parameters ---------- @@ -50,20 +19,20 @@ def binary_erosion(image, selem, out=None): Image array. selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray + out : ndarray of bool The array to store the result of the morphology. If None is passed, a new array will be allocated. Returns ------- - eroded : bool array + eroded : ndarray of bool The result of the morphological erosion. """ - - out, selem_sum = _convolve(image, selem, out, 1) - return np.array(np.equal(out, selem_sum, out=out), dtype=np.bool, - copy=False) + selem = (selem != 0) + selem_sum = np.sum(selem) + out = ndimage.convolve(image > 0, selem, mode='constant', cval=1) + return np.equal(out, selem_sum, out=out) def binary_dilation(image, selem, out=None): @@ -83,19 +52,19 @@ def binary_dilation(image, selem, out=None): Image array. selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray + out : ndarray of bool The array to store the result of the morphology. If None, is passed, a new array will be allocated. Returns ------- - dilated : bool array + dilated : ndarray of bool The result of the morphological dilation. """ - - out, _ = _convolve(image, selem, out, 0) - return np.array(np.not_equal(out, 0, out=out), dtype=np.bool, copy=False) + selem = (selem != 0) + out = ndimage.convolve(image > 0, selem, mode='constant', cval=0) + return np.not_equal(out, 0, out=out) def binary_opening(image, selem, out=None): @@ -115,17 +84,16 @@ def binary_opening(image, selem, out=None): Image array. selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray + out : ndarray of bool The array to store the result of the morphology. If None is passed, a new array will be allocated. Returns ------- - opening : bool array + opening : ndarray of bool The result of the morphological opening. """ - eroded = binary_erosion(image, selem) out = binary_dilation(eroded, selem, out=out) return out @@ -148,13 +116,13 @@ def binary_closing(image, selem, out=None): Image array. selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. - out : ndarray + out : ndarray of bool The array to store the result of the morphology. If None, is passed, a new array will be allocated. Returns ------- - closing : bool array + closing : ndarray of bool The result of the morphological closing. """ diff --git a/skimage/morphology/tests/test_binary.py b/skimage/morphology/tests/test_binary.py index 5f831669..2f47c917 100644 --- a/skimage/morphology/tests/test_binary.py +++ b/skimage/morphology/tests/test_binary.py @@ -17,27 +17,6 @@ def test_non_square_image(): testing.assert_array_equal(binary_res, grey_res) -def test_selem_overflow(): - strel = np.ones((17, 17), dtype=np.uint8) - img = np.zeros((20, 20)) - img[2:19, 2:19] = 1 - binary_res = binary.binary_erosion(img, strel) - grey_res = img_as_bool(grey.erosion(img, strel)) - testing.assert_array_equal(binary_res, grey_res) - - -def test_selem_overflow_exception(): - strel = np.ones((17, 17), dtype=np.uint8) - img = np.zeros((20, 20)) - img[2:19, 2:19] = 1 - out = np.zeros((20, 20), dtype=np.uint8) - testing.assert_raises(ValueError, binary.binary_erosion, img, strel, out) - out = np.zeros((20, 20), dtype=np.uint16) - binary_res = binary.binary_erosion(img, strel, out=out) - grey_res = img_as_bool(grey.erosion(img, strel)) - testing.assert_array_equal(binary_res, grey_res) - - def test_binary_erosion(): strel = selem.square(3) binary_res = binary.binary_erosion(bw_lena, strel) From 9ff4316cbbfa76741d3f67860213956e2e4a2ea7 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Sat, 12 Oct 2013 19:23:54 +0200 Subject: [PATCH 712/736] Fix overflow in NumPy 1.7 as suggested by Julian Taylor. --- skimage/morphology/binary.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/skimage/morphology/binary.py b/skimage/morphology/binary.py index 81294d80..de002c7e 100644 --- a/skimage/morphology/binary.py +++ b/skimage/morphology/binary.py @@ -31,7 +31,13 @@ def binary_erosion(image, selem, out=None): """ selem = (selem != 0) selem_sum = np.sum(selem) - out = ndimage.convolve(image > 0, selem, mode='constant', cval=1) + + if selem_sum > 255: + binary = (image != 0).view(np.uint8) + else: + binary = (image != 0).astype(np.intp) + + out = ndimage.convolve(binary, selem, mode='constant', cval=1) return np.equal(out, selem_sum, out=out) @@ -63,7 +69,12 @@ def binary_dilation(image, selem, out=None): """ selem = (selem != 0) - out = ndimage.convolve(image > 0, selem, mode='constant', cval=0) + if np.sum(selem) > 255: + binary = (image != 0).view(np.uint8) + else: + binary = (image != 0).astype(np.intp) + + out = ndimage.convolve(binary, selem, mode='constant', cval=0) return np.not_equal(out, 0, out=out) From 9329d0ad56a055d0c96d38a5ee919af1361b568f Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Sat, 12 Oct 2013 19:44:14 +0200 Subject: [PATCH 713/736] Restore @ahojnnes's overflow test. Correctly assign out argument. --- skimage/morphology/binary.py | 18 ++++++++++++------ skimage/morphology/tests/test_binary.py | 9 +++++++++ 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/skimage/morphology/binary.py b/skimage/morphology/binary.py index de002c7e..5746c545 100644 --- a/skimage/morphology/binary.py +++ b/skimage/morphology/binary.py @@ -32,13 +32,16 @@ def binary_erosion(image, selem, out=None): selem = (selem != 0) selem_sum = np.sum(selem) - if selem_sum > 255: + if selem_sum <= 255: binary = (image != 0).view(np.uint8) else: binary = (image != 0).astype(np.intp) - out = ndimage.convolve(binary, selem, mode='constant', cval=1) - return np.equal(out, selem_sum, out=out) + conv = ndimage.convolve(binary, selem, mode='constant', cval=1) + + if out is None: + out = np.zeros_like(binary, dtype=bool) + return np.equal(conv, selem_sum, out=out) def binary_dilation(image, selem, out=None): @@ -69,13 +72,16 @@ def binary_dilation(image, selem, out=None): """ selem = (selem != 0) - if np.sum(selem) > 255: + if np.sum(selem) <= 255: binary = (image != 0).view(np.uint8) else: binary = (image != 0).astype(np.intp) - out = ndimage.convolve(binary, selem, mode='constant', cval=0) - return np.not_equal(out, 0, out=out) + conv = ndimage.convolve(binary, selem, mode='constant', cval=0) + + if out is None: + out = np.zeros_like(binary, dtype=bool) + return np.not_equal(conv, 0, out=out) def binary_opening(image, selem, out=None): diff --git a/skimage/morphology/tests/test_binary.py b/skimage/morphology/tests/test_binary.py index 2f47c917..dcddc066 100644 --- a/skimage/morphology/tests/test_binary.py +++ b/skimage/morphology/tests/test_binary.py @@ -45,5 +45,14 @@ def test_binary_opening(): testing.assert_array_equal(binary_res, grey_res) +def test_selem_overflow(): + strel = np.ones((17, 17), dtype=np.uint8) + img = np.zeros((20, 20)) + img[2:19, 2:19] = 1 + binary_res = binary.binary_erosion(img, strel) + grey_res = img_as_bool(grey.erosion(img, strel)) + testing.assert_array_equal(binary_res, grey_res) + + if __name__ == '__main__': testing.run_module_suite() From ceb2e4c5d4e861f5cbbd00b2e03b6a8d5e31af87 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Sat, 12 Oct 2013 19:51:00 +0200 Subject: [PATCH 714/736] Test that output argument is utilized. --- skimage/morphology/tests/test_binary.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/skimage/morphology/tests/test_binary.py b/skimage/morphology/tests/test_binary.py index dcddc066..e1521296 100644 --- a/skimage/morphology/tests/test_binary.py +++ b/skimage/morphology/tests/test_binary.py @@ -54,5 +54,14 @@ def test_selem_overflow(): testing.assert_array_equal(binary_res, grey_res) +def test_out_argument(): + for func in (binary.binary_erosion, binary.binary_dilation): + strel = np.ones((3, 3), dtype=np.uint8) + img = np.ones((10, 10)) + out = np.zeros_like(img) + out_saved = out.copy() + func(img, strel, out=out) + testing.assert_(np.any(out != out_saved)) + if __name__ == '__main__': testing.run_module_suite() From 4f74a007d9cd7ee34b2c41a01e450867c23ccfff Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Sat, 12 Oct 2013 19:57:35 +0200 Subject: [PATCH 715/736] Test that output argument is correct. --- skimage/morphology/tests/test_binary.py | 1 + 1 file changed, 1 insertion(+) diff --git a/skimage/morphology/tests/test_binary.py b/skimage/morphology/tests/test_binary.py index e1521296..deab3d82 100644 --- a/skimage/morphology/tests/test_binary.py +++ b/skimage/morphology/tests/test_binary.py @@ -62,6 +62,7 @@ def test_out_argument(): out_saved = out.copy() func(img, strel, out=out) testing.assert_(np.any(out != out_saved)) + testing.assert_array_equal(out, func(img, strel)) if __name__ == '__main__': testing.run_module_suite() From 6111831994c7db4dc5454884f440f0904dea3fba Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Sat, 12 Oct 2013 21:55:13 +0200 Subject: [PATCH 716/736] Revert to >0 for determining binary values. Document that input should be binary. --- skimage/morphology/binary.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/skimage/morphology/binary.py b/skimage/morphology/binary.py index 5746c545..efd98ea1 100644 --- a/skimage/morphology/binary.py +++ b/skimage/morphology/binary.py @@ -16,7 +16,7 @@ def binary_erosion(image, selem, out=None): Parameters ---------- image : ndarray - Image array. + Binary input image. selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. out : ndarray of bool @@ -33,9 +33,9 @@ def binary_erosion(image, selem, out=None): selem_sum = np.sum(selem) if selem_sum <= 255: - binary = (image != 0).view(np.uint8) + binary = (image > 0).view(np.uint8) else: - binary = (image != 0).astype(np.intp) + binary = (image > 0).astype(np.intp) conv = ndimage.convolve(binary, selem, mode='constant', cval=1) @@ -50,15 +50,15 @@ def binary_dilation(image, selem, out=None): This function returns the same result as greyscale dilation but performs faster for binary images. - Morphological dilation sets a pixel at (i,j) to the maximum over all pixels - in the neighborhood centered at (i,j). Dilation enlarges bright regions - and shrinks dark regions. + Morphological dilation sets a pixel at ``(i,j)`` to the maximum over all + pixels in the neighborhood centered at ``(i,j)``. Dilation enlarges bright + regions and shrinks dark regions. Parameters ---------- image : ndarray - Image array. + Binary input image. selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. out : ndarray of bool @@ -73,9 +73,9 @@ def binary_dilation(image, selem, out=None): """ selem = (selem != 0) if np.sum(selem) <= 255: - binary = (image != 0).view(np.uint8) + binary = (image > 0).view(np.uint8) else: - binary = (image != 0).astype(np.intp) + binary = (image > 0).astype(np.intp) conv = ndimage.convolve(binary, selem, mode='constant', cval=0) @@ -98,7 +98,7 @@ def binary_opening(image, selem, out=None): Parameters ---------- image : ndarray - Image array. + Binary input image. selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. out : ndarray of bool @@ -130,7 +130,7 @@ def binary_closing(image, selem, out=None): Parameters ---------- image : ndarray - Image array. + Binary input image. selem : ndarray The neighborhood expressed as a 2-D array of 1's and 0's. out : ndarray of bool From 1c5dc10f4d8c8add889c79b6f2a6aed6808fa799 Mon Sep 17 00:00:00 2001 From: "Josh Warner (Mac)" Date: Sun, 13 Oct 2013 12:21:29 -0500 Subject: [PATCH 717/736] FEAT: Add 'localvar' mode to random_noise --- skimage/util/noise.py | 27 ++++++++++++++++++++----- skimage/util/tests/test_random_noise.py | 25 +++++++++++++++++++++++ 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/skimage/util/noise.py b/skimage/util/noise.py index aa8af300..9283f537 100644 --- a/skimage/util/noise.py +++ b/skimage/util/noise.py @@ -17,6 +17,8 @@ def random_noise(image, mode='gaussian', seed=None, clip=True, **kwargs): One of the following strings, selecting the type of noise to add: 'gaussian' Gaussian-distributed additive noise. + 'localvar' Gaussian-distributed additive noise, with specified + local variance at each point of `image` 'poisson' Poisson-distributed noise generated from the data. 'salt' Replaces random pixels with 1. 'pepper' Replaces random pixels with 0. @@ -37,6 +39,9 @@ def random_noise(image, mode='gaussian', seed=None, clip=True, **kwargs): var : float Variance of random distribution. Used in 'gaussian' and 'speckle'. Note: variance = (standard deviation) ** 2. Default : 0.01 + local_vars : ndarray + Array of positive floats, same shape as `image`, defining the local + variance at every image point. Used in 'localvar'. amount : float Proportion of image pixels to replace with noise on range [0, 1]. Used in 'salt', 'pepper', and 'salt & pepper'. Default : 0.05 @@ -52,10 +57,11 @@ def random_noise(image, mode='gaussian', seed=None, clip=True, **kwargs): Notes ----- - Speckle, Poisson, and Gaussian noise may generate noise outside the valid - image range. The default is to clip (not alias) these values, but they may - be preserved by setting `clip=False`. Note that in this case the output - may contain values outside the ranges [0, 1] or [-1, 1]. Use with care. + Speckle, Poisson, Localvar, and Gaussian noise may generate noise outside + the valid image range. The default is to clip (not alias) these values, + but they may be preserved by setting `clip=False`. Note that in this case + the output may contain values outside the ranges [0, 1] or [-1, 1]. + Use this option with care. Because of the prevalence of exclusively positive floating-point images in intermediate calculations, it is not possible to intuit if an input is @@ -89,6 +95,7 @@ def random_noise(image, mode='gaussian', seed=None, clip=True, **kwargs): allowedtypes = { 'gaussian': 'gaussian_values', + 'localvar': 'localvar_values', 'poisson': 'poisson_values', 'salt': 'sp_values', 'pepper': 'sp_values', @@ -99,10 +106,12 @@ def random_noise(image, mode='gaussian', seed=None, clip=True, **kwargs): 'mean': 0., 'var': 0.01, 'amount': 0.05, - 'salt_vs_pepper': 0.5} + 'salt_vs_pepper': 0.5, + 'local_vars': np.zeros_like(image) + 0.01} allowedkwargs = { 'gaussian_values': ['mean', 'var'], + 'localvar_values': ['local_vars'], 'sp_values': ['amount'], 's&p_values': ['amount', 'salt_vs_pepper'], 'poisson_values': []} @@ -121,6 +130,14 @@ def random_noise(image, mode='gaussian', seed=None, clip=True, **kwargs): image.shape) out = image + noise + elif mode == 'localvar': + # Ensure local variance input is correct + if (kwargs['local_vars'] <= 0).any(): + raise ValueError('All values of `local_vars` must be > 0.') + + # Safe shortcut usage broadcasts kwargs['local_vars'] as a ufunc + out = image + np.random.normal(0, kwargs['local_vars'] ** 0.5) + elif mode == 'poisson': # Determine unique values in image & calculate the next power of two vals = len(np.unique(image)) diff --git a/skimage/util/tests/test_random_noise.py b/skimage/util/tests/test_random_noise.py index 4d1752a0..39477cb0 100644 --- a/skimage/util/tests/test_random_noise.py +++ b/skimage/util/tests/test_random_noise.py @@ -83,6 +83,31 @@ def test_gaussian(): assert 0.012 < data_gaussian.var() < 0.018 +def test_localvar(): + seed = 42 + data = np.zeros((128, 128)) + 0.5 + local_vars = np.zeros((128, 128)) + 0.001 + local_vars[:64, 64:] = 0.1 + local_vars[64:, :64] = 0.25 + local_vars[64:, 64:] = 0.45 + + data_gaussian = random_noise(data, mode='localvar', seed=seed, + local_vars=local_vars, clip=False) + assert 0. < data_gaussian[:64, :64].var() < 0.002 + assert 0.095 < data_gaussian[:64, 64:].var() < 0.105 + assert 0.245 < data_gaussian[64:, :64].var() < 0.255 + assert 0.445 < data_gaussian[64:, 64:].var() < 0.455 + + # Ensure local variance bounds checking works properly + bad_local_vars = np.zeros_like(data) + assert_raises(ValueError, random_noise, data, mode='localvar', seed=seed, + local_vars=bad_local_vars) + bad_local_vars += 0.1 + bad_local_vars[0, 0] = -1 + assert_raises(ValueError, random_noise, data, mode='localvar', seed=seed, + local_vars=bad_local_vars) + + def test_speckle(): seed = 42 data = np.zeros((128, 128)) + 0.1 From e5e1918a2bc671f9fc5b58a5bed8c6c732b531fe Mon Sep 17 00:00:00 2001 From: "Josh Warner (Mac)" Date: Sun, 13 Oct 2013 12:38:35 -0500 Subject: [PATCH 718/736] REBASE: Resolve first conflict --- .../random_walker_segmentation.py | 59 ++++++++------- .../segmentation/tests/test_random_walker.py | 72 +++++++++++++++++++ 2 files changed, 106 insertions(+), 25 deletions(-) diff --git a/skimage/segmentation/random_walker_segmentation.py b/skimage/segmentation/random_walker_segmentation.py index c475dcb7..1fbec7ae 100644 --- a/skimage/segmentation/random_walker_segmentation.py +++ b/skimage/segmentation/random_walker_segmentation.py @@ -77,14 +77,14 @@ def _make_graph_edges_3d(n_x, n_y, n_z): return edges -def _compute_weights_3d(data, beta=130, eps=1.e-6, depth=1., +def _compute_weights_3d(data, spacing, beta=130, eps=1.e-6, multichannel=False): # Weight calculation is main difference in multispectral version # Original gradient**2 replaced with sum of gradients ** 2 gradients = 0 for channel in range(0, data.shape[-1]): gradients += _compute_gradients_3d(data[..., channel], - depth=depth) ** 2 + spacing) ** 2 # All channels considered together in this standard deviation beta /= 10 * data.std() if multichannel: @@ -97,11 +97,11 @@ def _compute_weights_3d(data, beta=130, eps=1.e-6, depth=1., return weights -def _compute_gradients_3d(data, depth=1.): - gr_deep = np.abs(data[:, :, :-1] - data[:, :, 1:]).ravel() / depth - gr_right = np.abs(data[:, :-1] - data[:, 1:]).ravel() - gr_down = np.abs(data[:-1] - data[1:]).ravel() - return np.r_[gr_deep, gr_right, gr_down] +def _compute_gradients_3d(data, spacing): + gr_deep = np.abs(data[:, :, :-1] - data[:, :, 1:]).ravel() / spacing[2] + gr_right = np.abs(data[:, :-1] - data[:, 1:]).ravel() / spacing[1] + gr_down = np.abs(data[:-1] - data[1:]).ravel() / spacing[0] + return np.r_[gr_down, gr_right, gr_deep] def _make_laplacian_sparse(edges, weights): @@ -116,9 +116,10 @@ def _make_laplacian_sparse(edges, weights): lap = sparse.coo_matrix((data, (i_indices, j_indices)), shape=(pixel_nb, pixel_nb)) connect = - np.ravel(lap.sum(axis=1)) - lap = sparse.coo_matrix((np.hstack((data, connect)), - (np.hstack((i_indices, diag)), np.hstack((j_indices, diag)))), - shape=(pixel_nb, pixel_nb)) + lap = sparse.coo_matrix( + (np.hstack((data, connect)), (np.hstack((i_indices, diag)), + np.hstack((j_indices, diag)))), + shape=(pixel_nb, pixel_nb)) return lap.tocsr() @@ -172,10 +173,11 @@ def _mask_edges_weights(edges, weights, mask): return edges, weights -def _build_laplacian(data, mask=None, beta=50, depth=1., multichannel=False): - l_x, l_y, l_z = data.shape[:3] +def _build_laplacian(data, spacing, mask=None, beta=50, + multichannel=False): + l_x, l_y, l_z = tuple(data.shape[i] * spacing[i] for i in range(3)) edges = _make_graph_edges_3d(l_x, l_y, l_z) - weights = _compute_weights_3d(data, beta=beta, eps=1.e-10, depth=depth, + weights = _compute_weights_3d(data, spacing, beta=beta, eps=1.e-10, multichannel=multichannel) if mask is not None: edges, weights = _mask_edges_weights(edges, weights, mask) @@ -187,8 +189,9 @@ def _build_laplacian(data, mask=None, beta=50, depth=1., multichannel=False): #----------- Random walker algorithm -------------------------------- -def random_walker(data, labels, beta=130, mode=None, tol=1.e-3, copy=True, - multichannel=False, return_full_prob=False, depth=1.): +def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True, + multichannel=False, return_full_prob=False, depth=1., + spacing=None): """Random walker algorithm for segmentation from markers. Random walker algorithm is implemented for gray-level or multichannel @@ -246,12 +249,16 @@ def random_walker(data, labels, beta=130, mode=None, tol=1.e-3, copy=True, return_full_prob : bool, default False If True, the probability that a pixel belongs to each of the labels will be returned, instead of only the most likely label. - depth : float, default 1. + depth : float, default 1. [DEPRECATED] Correction for non-isotropic voxel depths in 3D volumes. Default (1.) implies isotropy. This factor is derived as follows: depth = (out-of-plane voxel spacing) / (in-plane voxel spacing), where in-plane voxel spacing represents the first two spatial dimensions and out-of-plane voxel spacing represents the third spatial dimension. + `depth` is deprecated as of 0.9, in favor of `spacing`. + spacing : iterable of floats + spacing between voxels in each spatial dimension. If `None`, then + the spacing between pixels/voxels in each dimension is assumed 1. Returns ------- @@ -274,12 +281,9 @@ def random_walker(data, labels, beta=130, mode=None, tol=1.e-3, copy=True, Multichannel inputs are scaled with all channel data combined. Ensure all channels are separately normalized prior to running this algorithm. - The `depth` argument is specifically for certain types of 3-dimensional - volumes which, due to how they were acquired, have different spacing - along in-plane and out-of-plane dimensions. This is commonly encountered - in medical imaging. The `depth` argument corrects gradients calculated - along the third spatial dimension for the otherwise inherent assumption - that all points are equally spaced. + The `spacing` argument is specifically for anisotropic datasets, where + data points are spaced differently in one or more spatial dmensions. + Anisotropic data is commonly encountered in medical imaging. The algorithm was first proposed in *Random walks for image segmentation*, Leo Grady, IEEE Trans Pattern Anal Mach Intell. @@ -351,6 +355,11 @@ def random_walker(data, labels, beta=130, mode=None, tol=1.e-3, copy=True, 'random walker functions. You may also install pyamg ' 'and run the random walker function in cg_mg mode ' '(see the docstrings)') + if depth != 1.: + warnings.warn('`depth` kwarg is deprecated, and will be removed in the' + ' next major version. Use `spacing` instead.') + if spacing is None: + spacing = (1., 1.) + (depth, ) # Parse input data if not multichannel: @@ -384,10 +393,10 @@ def random_walker(data, labels, beta=130, mode=None, tol=1.e-3, copy=True, del filled labels = np.atleast_3d(labels) if np.any(labels < 0): - lap_sparse = _build_laplacian(data, mask=labels >= 0, beta=beta, - depth=depth, multichannel=multichannel) + lap_sparse = _build_laplacian(data, spacing, mask=labels >= 0, + beta=beta, multichannel=multichannel) else: - lap_sparse = _build_laplacian(data, beta=beta, depth=depth, + lap_sparse = _build_laplacian(data, spacing, beta=beta, multichannel=multichannel) lap_sparse, B = _buildAB(lap_sparse, labels) # We solve the linear system diff --git a/skimage/segmentation/tests/test_random_walker.py b/skimage/segmentation/tests/test_random_walker.py index 1cc0a1ee..050a8590 100644 --- a/skimage/segmentation/tests/test_random_walker.py +++ b/skimage/segmentation/tests/test_random_walker.py @@ -1,5 +1,6 @@ import numpy as np from skimage.segmentation import random_walker +from skimage.transform import resize def make_2d_syntheticdata(lx, ly=None): @@ -181,6 +182,77 @@ def test_multispectral_3d(): return data, multi_labels, single_labels, labels +def test_depth(): + n = 30 + lx, ly, lz = n, n, n + data, _ = make_3d_syntheticdata(lx, ly, lz) + + # Rescale `data` along Z axis + data_aniso = np.zeros((n, n, n // 2)) + for i, yz in enumerate(data): + data_aniso[i, :, :] = resize(yz, (n, n // 2)) + + # Generate new labels + small_l = int(lx // 5) + labels_aniso = np.zeros_like(data_aniso) + labels_aniso[lx // 5, ly // 5, lz // 5] = 1 + labels_aniso[lx // 2 + small_l // 4, + ly // 2 - small_l // 4, + lz // 4 - small_l // 8] = 2 + + # Test with `depth` kwarg + labels_aniso = random_walker(data_aniso, labels_aniso, mode='cg', + depth=0.5) + + assert (labels_aniso[13:17, 13:17, 7:9] == 2).all() + + +def test_spacing(): + n = 30 + lx, ly, lz = n, n, n + data, _ = make_3d_syntheticdata(lx, ly, lz) + + # Rescale `data` along Y axis + # `resize` is not yet 3D capable, so this must be done by looping in 2D. + data_aniso = np.zeros((n, n * 2, n)) + for i, yz in enumerate(data): + data_aniso[i, :, :] = resize(yz, (n * 2, n)) + + # Generate new labels + small_l = int(lx // 5) + labels_aniso = np.zeros_like(data_aniso) + labels_aniso[lx // 5, ly // 5, lz // 5] = 1 + labels_aniso[lx // 2 + small_l // 4, + ly - small_l // 2, + lz // 2 - small_l // 4] = 2 + + # Test with `spacing` kwarg + # First, anisotropic along Y + labels_aniso = random_walker(data_aniso, labels_aniso, mode='cg', + spacing=(1., 2., 1.)) + assert (labels_aniso[13:17, 26:34, 13:17] == 2).all() + + # Rescale `data` along X axis + # `resize` is not yet 3D capable, so this must be done by looping in 2D. + data_aniso = np.zeros((n, n * 2, n)) + for i in range(data.shape[1]): + data_aniso[i, :, :] = resize(data[:, 1, :], (n * 1.5, n)) + + # Generate new labels + small_l = int(lx // 5) + labels_aniso = np.zeros_like(data_aniso) + labels_aniso[lx // 5, ly // 5, lz // 5] = 1 + labels_aniso[lx - small_l // 2, + ly // 2 + small_l // 4, + lz // 2 - small_l // 4] = 2 + + # Anisotropic along X + labels_aniso2 = random_walker(np.rollaxis(data_aniso, 1).copy(), + np.rollaxis(labels_aniso, 1).copy(), + mode='cg', spacing=(2., 1., 1.)) + assert (labels_aniso2[26:34, 13:17, 13:17] == 2).all() + + if __name__ == '__main__': from numpy import testing testing.run_module_suite() From f25ca3a835a7f90251fc28ac2e82bf55ef5fe4d5 Mon Sep 17 00:00:00 2001 From: "Josh Warner (Mac)" Date: Sat, 12 Oct 2013 00:41:33 -0500 Subject: [PATCH 719/736] ENH: `spacing` kwarg for random_walker and improved tests --- .../random_walker_segmentation.py | 18 ++++-- .../segmentation/tests/test_random_walker.py | 64 ++++++++++--------- 2 files changed, 46 insertions(+), 36 deletions(-) diff --git a/skimage/segmentation/random_walker_segmentation.py b/skimage/segmentation/random_walker_segmentation.py index 1fbec7ae..7c3916eb 100644 --- a/skimage/segmentation/random_walker_segmentation.py +++ b/skimage/segmentation/random_walker_segmentation.py @@ -169,13 +169,13 @@ def _mask_edges_weights(edges, weights, mask): # Reassign edges labels to 0, 1, ... edges_number - 1 order = np.searchsorted(np.unique(edges.ravel()), np.arange(max_node_index + 1)) - edges = order[edges] + edges = order[edges.astype(np.int64)] return edges, weights def _build_laplacian(data, spacing, mask=None, beta=50, multichannel=False): - l_x, l_y, l_z = tuple(data.shape[i] * spacing[i] for i in range(3)) + l_x, l_y, l_z = tuple(data.shape[i] for i in range(3)) edges = _make_graph_edges_3d(l_x, l_y, l_z) weights = _compute_weights_3d(data, spacing, beta=beta, eps=1.e-10, multichannel=multichannel) @@ -257,7 +257,7 @@ def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True, out-of-plane voxel spacing represents the third spatial dimension. `depth` is deprecated as of 0.9, in favor of `spacing`. spacing : iterable of floats - spacing between voxels in each spatial dimension. If `None`, then + Spacing between voxels in each spatial dimension. If `None`, then the spacing between pixels/voxels in each dimension is assumed 1. Returns @@ -357,9 +357,17 @@ def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True, '(see the docstrings)') if depth != 1.: warnings.warn('`depth` kwarg is deprecated, and will be removed in the' - ' next major version. Use `spacing` instead.') + ' next major release. Use `spacing` instead.') + + # Spacing kwarg checks if spacing is None: - spacing = (1., 1.) + (depth, ) + spacing = (1., 1.) + (depth, ) + elif len(spacing) == 2: + spacing = tuple(spacing) + (depth, ) + elif len(spacing) == 3: + pass + else: + raise ValueError('Input argument `spacing` incorrect, see docstring.') # Parse input data if not multichannel: diff --git a/skimage/segmentation/tests/test_random_walker.py b/skimage/segmentation/tests/test_random_walker.py index 050a8590..46a82d8e 100644 --- a/skimage/segmentation/tests/test_random_walker.py +++ b/skimage/segmentation/tests/test_random_walker.py @@ -8,16 +8,16 @@ def make_2d_syntheticdata(lx, ly=None): ly = lx np.random.seed(1234) data = np.zeros((lx, ly)) + 0.1 * np.random.randn(lx, ly) - small_l = int(lx / 5) - data[lx / 2 - small_l:lx / 2 + small_l, - ly / 2 - small_l:ly / 2 + small_l] = 1 - data[lx / 2 - small_l + 1:lx / 2 + small_l - 1, - ly / 2 - small_l + 1:ly / 2 + small_l - 1] = \ - 0.1 * np.random.randn(2 * small_l - 2, 2 * small_l - 2) - data[lx / 2 - small_l, ly / 2 - small_l / 8:ly / 2 + small_l / 8] = 0 + small_l = int(lx // 5) + data[lx // 2 - small_l:lx // 2 + small_l, + ly // 2 - small_l:ly // 2 + small_l] = 1 + data[lx // 2 - small_l + 1:lx // 2 + small_l - 1, + ly // 2 - small_l + 1:ly // 2 + small_l - 1] = ( + 0.1 * np.random.randn(2 * small_l - 2, 2 * small_l - 2)) + data[lx // 2 - small_l, ly // 2 - small_l // 8:ly // 2 + small_l // 8] = 0 seeds = np.zeros_like(data) - seeds[lx / 5, ly / 5] = 1 - seeds[lx / 2 + small_l / 4, ly / 2 - small_l / 4] = 2 + seeds[lx // 5, ly // 5] = 1 + seeds[lx // 2 + small_l // 4, ly // 2 - small_l // 4] = 2 return data, seeds @@ -28,21 +28,23 @@ def make_3d_syntheticdata(lx, ly=None, lz=None): lz = lx np.random.seed(1234) data = np.zeros((lx, ly, lz)) + 0.1 * np.random.randn(lx, ly, lz) - small_l = int(lx / 5) - data[lx / 2 - small_l:lx / 2 + small_l, - ly / 2 - small_l:ly / 2 + small_l, - lz / 2 - small_l:lz / 2 + small_l] = 1 - data[lx / 2 - small_l + 1:lx / 2 + small_l - 1, - ly / 2 - small_l + 1:ly / 2 + small_l - 1, - lz / 2 - small_l + 1:lz / 2 + small_l - 1] = 0 + small_l = int(lx // 5) + data[lx // 2 - small_l:lx // 2 + small_l, + ly // 2 - small_l:ly // 2 + small_l, + lz // 2 - small_l:lz // 2 + small_l] = 1 + data[lx // 2 - small_l + 1:lx // 2 + small_l - 1, + ly // 2 - small_l + 1:ly // 2 + small_l - 1, + lz // 2 - small_l + 1:lz // 2 + small_l - 1] = 0 # make a hole - hole_size = np.max([1, small_l / 8]) - data[lx / 2 - small_l, - ly / 2 - hole_size:ly / 2 + hole_size, - lz / 2 - hole_size:lz / 2 + hole_size] = 0 + hole_size = np.max([1, small_l // 8]) + data[lx // 2 - small_l, + ly // 2 - hole_size:ly // 2 + hole_size, + lz // 2 - hole_size:lz // 2 + hole_size] = 0 seeds = np.zeros_like(data) - seeds[lx / 5, ly / 5, lz / 5] = 1 - seeds[lx / 2 + small_l / 4, ly / 2 - small_l / 4, lz / 2 - small_l / 4] = 2 + seeds[lx // 5, ly // 5, lz // 5] = 1 + seeds[lx // 2 + small_l // 4, + ly // 2 - small_l // 4, + lz // 2 - small_l // 4] = 2 return data, seeds @@ -102,7 +104,7 @@ def test_types(): lx = 70 ly = 100 data, labels = make_2d_syntheticdata(lx, ly) - data = 255 * (data - data.min()) / (data.max() - data.min()) + data = 255 * (data - data.min()) // (data.max() - data.min()) data = data.astype(np.uint8) labels_cg_mg = random_walker(data, labels, beta=90, mode='cg_mg') assert (labels_cg_mg[25:45, 40:60] == 2).all() @@ -236,19 +238,19 @@ def test_spacing(): # `resize` is not yet 3D capable, so this must be done by looping in 2D. data_aniso = np.zeros((n, n * 2, n)) for i in range(data.shape[1]): - data_aniso[i, :, :] = resize(data[:, 1, :], (n * 1.5, n)) + data_aniso[i, :, :] = resize(data[:, 1, :], (n * 2, n)) # Generate new labels small_l = int(lx // 5) - labels_aniso = np.zeros_like(data_aniso) - labels_aniso[lx // 5, ly // 5, lz // 5] = 1 - labels_aniso[lx - small_l // 2, - ly // 2 + small_l // 4, - lz // 2 - small_l // 4] = 2 + labels_aniso2 = np.zeros_like(data_aniso) + labels_aniso2[lx // 5, ly // 5, lz // 5] = 1 + labels_aniso2[lx - small_l // 2, + ly // 2 + small_l // 4, + lz // 2 - small_l // 4] = 2 # Anisotropic along X - labels_aniso2 = random_walker(np.rollaxis(data_aniso, 1).copy(), - np.rollaxis(labels_aniso, 1).copy(), + labels_aniso2 = random_walker(data_aniso, + labels_aniso2, mode='cg', spacing=(2., 1., 1.)) assert (labels_aniso2[26:34, 13:17, 13:17] == 2).all() From 927ba2cd8ec9c337989eda60352b6841d2495ac1 Mon Sep 17 00:00:00 2001 From: "Josh Warner (Mac)" Date: Sat, 12 Oct 2013 01:05:58 -0500 Subject: [PATCH 720/736] FIX: roll back incorrect testing change of gradient order --- skimage/segmentation/random_walker_segmentation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/segmentation/random_walker_segmentation.py b/skimage/segmentation/random_walker_segmentation.py index 7c3916eb..213cf94f 100644 --- a/skimage/segmentation/random_walker_segmentation.py +++ b/skimage/segmentation/random_walker_segmentation.py @@ -101,7 +101,7 @@ def _compute_gradients_3d(data, spacing): gr_deep = np.abs(data[:, :, :-1] - data[:, :, 1:]).ravel() / spacing[2] gr_right = np.abs(data[:, :-1] - data[:, 1:]).ravel() / spacing[1] gr_down = np.abs(data[:-1] - data[1:]).ravel() / spacing[0] - return np.r_[gr_down, gr_right, gr_deep] + return np.r_[gr_deep, gr_right, gr_down] def _make_laplacian_sparse(edges, weights): From 03349250bcb90ad980c778d2a78786e1c4c4321e Mon Sep 17 00:00:00 2001 From: "Josh Warner (Mac)" Date: Sat, 12 Oct 2013 09:26:44 -0500 Subject: [PATCH 721/736] DOC: Add removal of `depth` from random_walker in 0.10 --- TODO.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/TODO.txt b/TODO.txt index 29d1ebac..5f5171ec 100644 --- a/TODO.txt +++ b/TODO.txt @@ -7,6 +7,7 @@ Version 0.10 * Change default mode of random_walker segmentation to 'cg_mg' > 'cg' > 'bf', depending on which optional dependencies are available. * Remove deprecated `out` parameter of `skimage.morphology.binary_*` +* Remove deprecated parameter `depth` in `skimage.segmentation.random_walker` Version 0.9 ----------- From 961c47e8013df3ccc7b9cb8766a32b194faa7dde Mon Sep 17 00:00:00 2001 From: "Josh Warner (Mac)" Date: Sun, 13 Oct 2013 12:34:30 -0500 Subject: [PATCH 722/736] DOC: Fix minor typo in docstring --- skimage/segmentation/random_walker_segmentation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/segmentation/random_walker_segmentation.py b/skimage/segmentation/random_walker_segmentation.py index 213cf94f..17c4c98b 100644 --- a/skimage/segmentation/random_walker_segmentation.py +++ b/skimage/segmentation/random_walker_segmentation.py @@ -282,7 +282,7 @@ def random_walker(data, labels, beta=130, mode='bf', tol=1.e-3, copy=True, channels are separately normalized prior to running this algorithm. The `spacing` argument is specifically for anisotropic datasets, where - data points are spaced differently in one or more spatial dmensions. + data points are spaced differently in one or more spatial dimensions. Anisotropic data is commonly encountered in medical imaging. The algorithm was first proposed in *Random walks for image From 1de6d93850e221aaac71fe845bec47aad0e21f0a Mon Sep 17 00:00:00 2001 From: "Josh Warner (Mac)" Date: Sun, 13 Oct 2013 13:00:40 -0500 Subject: [PATCH 723/736] DOC: Change sampling kwarg name to spacing in marching_cubes The preference is to follow VTK's convention for anisotropic rectangularly sampled data, using the keyword `spacing` rather than `sampling`. This change converts the last remaining use of `sampling` in scikit-image to `spacing`. --- doc/examples/plot_marching_cubes.py | 2 +- skimage/measure/_marching_cubes.py | 6 +- skimage/measure/_marching_cubes_cy.pyx | 72 ++++++++++---------- skimage/measure/tests/test_marching_cubes.py | 10 +-- 4 files changed, 45 insertions(+), 45 deletions(-) diff --git a/doc/examples/plot_marching_cubes.py b/doc/examples/plot_marching_cubes.py index a57a40a2..36685434 100644 --- a/doc/examples/plot_marching_cubes.py +++ b/doc/examples/plot_marching_cubes.py @@ -17,7 +17,7 @@ a mesh for regions of bone or bone-like density. This implementation also works correctly on anisotropic datasets, where the voxel spacing is not equal for every spatial dimension, through use of the -`sampling` kwarg. +`spacing` kwarg. """ import numpy as np diff --git a/skimage/measure/_marching_cubes.py b/skimage/measure/_marching_cubes.py index 772c52f1..37dab1c0 100644 --- a/skimage/measure/_marching_cubes.py +++ b/skimage/measure/_marching_cubes.py @@ -2,7 +2,7 @@ import numpy as np from . import _marching_cubes_cy -def marching_cubes(volume, level, sampling=(1., 1., 1.)): +def marching_cubes(volume, level, spacing=(1., 1., 1.)): """ Marching cubes algorithm to find iso-valued surfaces in 3d volumetric data @@ -12,7 +12,7 @@ def marching_cubes(volume, level, sampling=(1., 1., 1.)): Input data volume to find isosurfaces. Will be cast to `np.float64`. level : float Contour value to search for isosurfaces in `volume`. - sampling : length-3 tuple of floats + spacing : length-3 tuple of floats Voxel spacing in spatial dimensions corresponding to numpy array indexing dimensions (M, N, P) as in `volume`. @@ -107,7 +107,7 @@ def marching_cubes(volume, level, sampling=(1., 1., 1.)): # have repeated vertices - and equivalent vertices are redundantly # placed in every triangle they connect with. raw_tris = _marching_cubes_cy.iterate_and_store_3d(volume, float(level), - sampling) + spacing) # Find and collect unique vertices, storing triangle verts as indices. # Returns a true mesh with no degenerate faces. diff --git a/skimage/measure/_marching_cubes_cy.pyx b/skimage/measure/_marching_cubes_cy.pyx index a0f63d1c..085108ab 100644 --- a/skimage/measure/_marching_cubes_cy.pyx +++ b/skimage/measure/_marching_cubes_cy.pyx @@ -56,32 +56,32 @@ def unpack_unique_verts(list trilist): def iterate_and_store_3d(double[:, :, ::1] arr, double level, - tuple sampling=(1., 1., 1.)): + tuple spacing=(1., 1., 1.)): """Iterate across the given array in a marching-cubes fashion, looking for volumes with edges that cross 'level'. If such a volume is found, appropriate triangulations are added to a growing list of faces to be returned by this function. - If `sampling` is not provided, vertices are returned in the indexing + If `spacing` is not provided, vertices are returned in the indexing coordinate system (assuming all 3 spatial dimensions sampled equally). - If `sampling` is provided, vertices will be returned in volume coordinates + If `spacing` is provided, vertices will be returned in volume coordinates relative to the origin, regularly spaced as specified in each dimension. """ if arr.shape[0] < 2 or arr.shape[1] < 2 or arr.shape[2] < 2: raise ValueError("Input array must be at least 2x2x2.") - if len(sampling) != 3: - raise ValueError("`sampling` must be (double, double, double)") + if len(spacing) != 3: + raise ValueError("`spacing` must be (double, double, double)") cdef list face_list = [] cdef list norm_list = [] cdef Py_ssize_t n - cdef bint odd_sampling, plus_z + cdef bint odd_spacing, plus_z plus_z = False - if [float(i) for i in sampling] == [1.0, 1.0, 1.0]: - odd_sampling = False + if [float(i) for i in spacing] == [1.0, 1.0, 1.0]: + odd_spacing = False else: - odd_sampling = True + odd_spacing = True # The plan is to iterate a 2x2x2 cube across the input array. This means # the upper-left corner of the cube needs to iterate across a sub-array @@ -107,11 +107,11 @@ def iterate_and_store_3d(double[:, :, ::1] arr, double level, coords[1] = 0 coords[2] = 0 - # Extract doubles from `sampling` for speed - cdef double[3] sampling2 - sampling2[0] = sampling[0] - sampling2[1] = sampling[1] - sampling2[2] = sampling[2] + # Extract doubles from `spacing` for speed + cdef double[3] spacing2 + spacing2[0] = spacing[0] + spacing2[1] = spacing[1] + spacing2[2] = spacing[2] # Calculate the number of iterations we'll need cdef Py_ssize_t num_cube_steps = ((arr.shape[0] - 1) * @@ -138,15 +138,15 @@ def iterate_and_store_3d(double[:, :, ::1] arr, double level, x0, y0, z0 = coords[0], coords[1], coords[2] x1, y1, z1 = x0 + 1, y0 + 1, z0 + 1 - if odd_sampling: + if odd_spacing: # These doubles are the modified world coordinates; they are only - # calculated if non-default `sampling` provided. - r0 = coords[0] * sampling2[0] - c0 = coords[1] * sampling2[1] - d0 = coords[2] * sampling2[2] - r1 = r0 + sampling2[0] - c1 = c0 + sampling2[1] - d1 = d0 + sampling2[2] + # calculated if non-default `spacing` provided. + r0 = coords[0] * spacing2[0] + c0 = coords[1] * spacing2[1] + d0 = coords[2] * spacing2[2] + r1 = r0 + spacing2[0] + c1 = c0 + spacing2[1] + d1 = d0 + spacing2[2] else: r0, c0, d0, r1, c1, d1 = x0, y0, z0, x1, y1, z1 @@ -193,11 +193,11 @@ def iterate_and_store_3d(double[:, :, ::1] arr, double level, e4 = e8 else: # Calculate edges normally - if odd_sampling: - e1 = r0 + _get_fraction(v1, v2, level) * sampling2[0], c0, d0 - e2 = r1, c0 + _get_fraction(v2, v3, level) * sampling2[1], d0 - e3 = r0 + _get_fraction(v4, v3, level) * sampling2[0], c1, d0 - e4 = r0, c0 + _get_fraction(v1, v4, level) * sampling2[1], d0 + if odd_spacing: + e1 = r0 + _get_fraction(v1, v2, level) * spacing2[0], c0, d0 + e2 = r1, c0 + _get_fraction(v2, v3, level) * spacing2[1], d0 + e3 = r0 + _get_fraction(v4, v3, level) * spacing2[0], c1, d0 + e4 = r0, c0 + _get_fraction(v1, v4, level) * spacing2[1], d0 else: e1 = r0 + _get_fraction(v1, v2, level), c0, d0 e2 = r1, c0 + _get_fraction(v2, v3, level), d0 @@ -208,15 +208,15 @@ def iterate_and_store_3d(double[:, :, ::1] arr, double level, # large, growing lookup table for all adjacent values; could save # ~30% in terms of runtime at the expense of memory usage and # much greater complexity. - if odd_sampling: - e5 = r0 + _get_fraction(v5, v6, level) * sampling2[0], c0, d1 - e6 = r1, c0 + _get_fraction(v6, v7, level) * sampling2[1], d1 - e7 = r0 + _get_fraction(v8, v7, level) * sampling2[0], c1, d1 - e8 = r0, c0 + _get_fraction(v5, v8, level) * sampling2[1], d1 - e9 = r0, c0, d0 + _get_fraction(v1, v5, level) * sampling2[2] - e10 = r1, c0, d0 + _get_fraction(v2, v6, level) * sampling2[2] - e11 = r0, c1, d0 + _get_fraction(v4, v8, level) * sampling2[2] - e12 = r1, c1, d0 + _get_fraction(v3, v7, level) * sampling2[2] + if odd_spacing: + e5 = r0 + _get_fraction(v5, v6, level) * spacing2[0], c0, d1 + e6 = r1, c0 + _get_fraction(v6, v7, level) * spacing2[1], d1 + e7 = r0 + _get_fraction(v8, v7, level) * spacing2[0], c1, d1 + e8 = r0, c0 + _get_fraction(v5, v8, level) * spacing2[1], d1 + e9 = r0, c0, d0 + _get_fraction(v1, v5, level) * spacing2[2] + e10 = r1, c0, d0 + _get_fraction(v2, v6, level) * spacing2[2] + e11 = r0, c1, d0 + _get_fraction(v4, v8, level) * spacing2[2] + e12 = r1, c1, d0 + _get_fraction(v3, v7, level) * spacing2[2] else: e5 = r0 + _get_fraction(v5, v6, level), c0, d1 e6 = r1, c0 + _get_fraction(v6, v7, level), d1 diff --git a/skimage/measure/tests/test_marching_cubes.py b/skimage/measure/tests/test_marching_cubes.py index 7a1fd40a..20689d44 100644 --- a/skimage/measure/tests/test_marching_cubes.py +++ b/skimage/measure/tests/test_marching_cubes.py @@ -16,12 +16,12 @@ def test_marching_cubes_isotropic(): def test_marching_cubes_anisotropic(): - sampling = (1., 10 / 6., 16 / 6.) - ellipsoid_anisotropic = ellipsoid(6, 10, 16, sampling=sampling, + spacing = (1., 10 / 6., 16 / 6.) + ellipsoid_anisotropic = ellipsoid(6, 10, 16, spacing=spacing, levelset=True) - _, surf = ellipsoid_stats(6, 10, 16, sampling=sampling) + _, surf = ellipsoid_stats(6, 10, 16, spacing=spacing) verts, faces = marching_cubes(ellipsoid_anisotropic, 0., - sampling=sampling) + spacing=spacing) surf_calc = mesh_surface_area(verts, faces) # Test within 1.5% tolerance for anisotropic. Will always underestimate. @@ -32,7 +32,7 @@ def test_invalid_input(): assert_raises(ValueError, marching_cubes, np.zeros((2, 2, 1)), 0) assert_raises(ValueError, marching_cubes, np.zeros((2, 2, 1)), 1) assert_raises(ValueError, marching_cubes, np.ones((3, 3, 3)), 1, - sampling=(1, 2)) + spacing=(1, 2)) assert_raises(ValueError, marching_cubes, np.zeros((20, 20)), 0) From 8185289a88d4c411c97cf826116b1cee1afc6bad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Sun, 13 Oct 2013 14:46:10 +0200 Subject: [PATCH 724/736] Add logger deprecation to todo list --- TODO.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/TODO.txt b/TODO.txt index 5f5171ec..9bc05571 100644 --- a/TODO.txt +++ b/TODO.txt @@ -8,6 +8,7 @@ Version 0.10 depending on which optional dependencies are available. * Remove deprecated `out` parameter of `skimage.morphology.binary_*` * Remove deprecated parameter `depth` in `skimage.segmentation.random_walker` +* Remove deprecated logger function in ``skimage/__init__.py`` Version 0.9 ----------- From a229d19eb031f7628cc0bb8aad06f75933f6266b Mon Sep 17 00:00:00 2001 From: "Josh Warner (Mac)" Date: Sun, 13 Oct 2013 13:11:11 -0500 Subject: [PATCH 725/736] DOC: Change sampling to spacing in skimage.draw.ellipsoid and tests --- skimage/draw/draw3d.py | 26 +++++++++----------- skimage/draw/tests/test_draw3d.py | 4 +-- skimage/measure/tests/test_marching_cubes.py | 2 +- 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/skimage/draw/draw3d.py b/skimage/draw/draw3d.py index 0b6fcb2d..a69471d5 100644 --- a/skimage/draw/draw3d.py +++ b/skimage/draw/draw3d.py @@ -3,10 +3,10 @@ import numpy as np from scipy.special import (ellipkinc as ellip_F, ellipeinc as ellip_E) -def ellipsoid(a, b, c, sampling=(1., 1., 1.), levelset=False): +def ellipsoid(a, b, c, spacing=(1., 1., 1.), levelset=False): """ Generates ellipsoid with semimajor axes aligned with grid dimensions - on grid with specified `sampling`. + on grid with specified `spacing`. Parameters ---------- @@ -16,8 +16,8 @@ def ellipsoid(a, b, c, sampling=(1., 1., 1.), levelset=False): Length of semimajor axis aligned with y-axis. c : float Length of semimajor axis aligned with z-axis. - sampling : tuple of floats, length 3 - Sampling in (x, y, z) spatial dimensions. + spacing : tuple of floats, length 3 + spacing in (x, y, z) spatial dimensions. levelset : bool If True, returns the level set for this ellipsoid (signed level set about zero, with positive denoting interior) as np.float64. @@ -26,7 +26,7 @@ def ellipsoid(a, b, c, sampling=(1., 1., 1.), levelset=False): Returns ------- ellip : (N, M, P) array - Ellipsoid centered in a correctly sized array for given `sampling`. + Ellipsoid centered in a correctly sized array for given `spacing`. Boolean dtype unless `levelset=True`, in which case a float array is returned with the level set above 0.0 representing the ellipsoid. @@ -34,7 +34,7 @@ def ellipsoid(a, b, c, sampling=(1., 1., 1.), levelset=False): if (a <= 0) or (b <= 0) or (c <= 0): raise ValueError('Parameters a, b, and c must all be > 0') - offset = np.r_[1, 1, 1] * np.r_[sampling] + offset = np.r_[1, 1, 1] * np.r_[spacing] # Calculate limits, and ensure output volume is odd & symmetric low = np.ceil((- np.r_[a, b, c] - offset)) @@ -43,14 +43,14 @@ def ellipsoid(a, b, c, sampling=(1., 1., 1.), levelset=False): for dim in range(3): if (high[dim] - low[dim]) % 2 == 0: low[dim] -= 1 - num = np.arange(low[dim], high[dim], sampling[dim]) + num = np.arange(low[dim], high[dim], spacing[dim]) if 0 not in num: low[dim] -= np.max(num[num < 0]) # Generate (anisotropic) spatial grid - x, y, z = np.mgrid[low[0]:high[0]:sampling[0], - low[1]:high[1]:sampling[1], - low[2]:high[2]:sampling[2]] + x, y, z = np.mgrid[low[0]:high[0]:spacing[0], + low[1]:high[1]:spacing[1], + low[2]:high[2]:spacing[2]] if not levelset: arr = ((x / float(a)) ** 2 + @@ -64,10 +64,10 @@ def ellipsoid(a, b, c, sampling=(1., 1., 1.), levelset=False): return arr -def ellipsoid_stats(a, b, c, sampling=(1., 1., 1.)): +def ellipsoid_stats(a, b, c): """ Calculates analytical surface area and volume for ellipsoid with - semimajor axes aligned with grid dimensions of specified `sampling`. + semimajor axes aligned with grid dimensions of specified `spacing`. Parameters ---------- @@ -77,8 +77,6 @@ def ellipsoid_stats(a, b, c, sampling=(1., 1., 1.)): Length of semimajor axis aligned with y-axis. c : float Length of semimajor axis aligned with z-axis. - sampling : tuple of floats, length 3 - Sampling in (x, y, z) spatial dimensions. Returns ------- diff --git a/skimage/draw/tests/test_draw3d.py b/skimage/draw/tests/test_draw3d.py index 59a7f6b3..2e1198eb 100644 --- a/skimage/draw/tests/test_draw3d.py +++ b/skimage/draw/tests/test_draw3d.py @@ -6,7 +6,7 @@ from skimage.draw import ellipsoid, ellipsoid_stats def test_ellipsoid_bool(): test = ellipsoid(2, 2, 2)[1:-1, 1:-1, 1:-1] - test_anisotropic = ellipsoid(2, 2, 4, sampling=(1., 1., 2.)) + test_anisotropic = ellipsoid(2, 2, 4, spacing=(1., 1., 2.)) test_anisotropic = test_anisotropic[1:-1, 1:-1, 1:-1] expected = np.array([[[0, 0, 0, 0, 0], @@ -45,7 +45,7 @@ def test_ellipsoid_bool(): def test_ellipsoid_levelset(): test = ellipsoid(2, 2, 2, levelset=True)[1:-1, 1:-1, 1:-1] - test_anisotropic = ellipsoid(2, 2, 4, sampling=(1., 1., 2.), + test_anisotropic = ellipsoid(2, 2, 4, spacing=(1., 1., 2.), levelset=True) test_anisotropic = test_anisotropic[1:-1, 1:-1, 1:-1] diff --git a/skimage/measure/tests/test_marching_cubes.py b/skimage/measure/tests/test_marching_cubes.py index 20689d44..b3c2ddc1 100644 --- a/skimage/measure/tests/test_marching_cubes.py +++ b/skimage/measure/tests/test_marching_cubes.py @@ -19,7 +19,7 @@ def test_marching_cubes_anisotropic(): spacing = (1., 10 / 6., 16 / 6.) ellipsoid_anisotropic = ellipsoid(6, 10, 16, spacing=spacing, levelset=True) - _, surf = ellipsoid_stats(6, 10, 16, spacing=spacing) + _, surf = ellipsoid_stats(6, 10, 16) verts, faces = marching_cubes(ellipsoid_anisotropic, 0., spacing=spacing) surf_calc = mesh_surface_area(verts, faces) From b9f5dd3ad5f3dace93f26f00570163e1fe5f96ba Mon Sep 17 00:00:00 2001 From: "Josh Warner (Mac)" Date: Sun, 13 Oct 2013 13:15:08 -0500 Subject: [PATCH 726/736] DOC: Fix capitalization in ellipsoid docstring. --- skimage/draw/draw3d.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/draw/draw3d.py b/skimage/draw/draw3d.py index a69471d5..db2f0d39 100644 --- a/skimage/draw/draw3d.py +++ b/skimage/draw/draw3d.py @@ -17,7 +17,7 @@ def ellipsoid(a, b, c, spacing=(1., 1., 1.), levelset=False): c : float Length of semimajor axis aligned with z-axis. spacing : tuple of floats, length 3 - spacing in (x, y, z) spatial dimensions. + Spacing in (x, y, z) spatial dimensions. levelset : bool If True, returns the level set for this ellipsoid (signed level set about zero, with positive denoting interior) as np.float64. From 89d703ca4678611f21456b2a3a5bf912cb80a1c5 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Mon, 14 Oct 2013 12:58:27 +0200 Subject: [PATCH 727/736] Pre-allocate memory for convolution and re-use for output, if no user-override specified. --- skimage/morphology/binary.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/skimage/morphology/binary.py b/skimage/morphology/binary.py index efd98ea1..fe91f148 100644 --- a/skimage/morphology/binary.py +++ b/skimage/morphology/binary.py @@ -37,10 +37,11 @@ def binary_erosion(image, selem, out=None): else: binary = (image > 0).astype(np.intp) - conv = ndimage.convolve(binary, selem, mode='constant', cval=1) + conv = np.empty_like(image, dtype=np.intp) + ndimage.convolve(binary, selem, mode='constant', cval=1, output=conv) if out is None: - out = np.zeros_like(binary, dtype=bool) + out = conv return np.equal(conv, selem_sum, out=out) @@ -77,10 +78,11 @@ def binary_dilation(image, selem, out=None): else: binary = (image > 0).astype(np.intp) - conv = ndimage.convolve(binary, selem, mode='constant', cval=0) + conv = np.empty_like(image, dtype=np.intp) + ndimage.convolve(binary, selem, mode='constant', cval=0, output=conv) if out is None: - out = np.zeros_like(binary, dtype=bool) + out = conv return np.not_equal(conv, 0, out=out) From d5e2ab0b135dc959ff07c5eddf67af808d8c908e Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Mon, 14 Oct 2013 13:02:38 +0200 Subject: [PATCH 728/736] Allocate less memory, if possible. --- skimage/morphology/binary.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/skimage/morphology/binary.py b/skimage/morphology/binary.py index fe91f148..6bd5d234 100644 --- a/skimage/morphology/binary.py +++ b/skimage/morphology/binary.py @@ -34,10 +34,11 @@ def binary_erosion(image, selem, out=None): if selem_sum <= 255: binary = (image > 0).view(np.uint8) + conv = np.empty_like(image, dtype=np.uint8) else: binary = (image > 0).astype(np.intp) + conv = np.empty_like(image, dtype=np.intp) - conv = np.empty_like(image, dtype=np.intp) ndimage.convolve(binary, selem, mode='constant', cval=1, output=conv) if out is None: @@ -75,10 +76,11 @@ def binary_dilation(image, selem, out=None): selem = (selem != 0) if np.sum(selem) <= 255: binary = (image > 0).view(np.uint8) + conv = np.empty_like(image, dtype=np.uint8) else: binary = (image > 0).astype(np.intp) + conv = np.empty_like(image, dtype=np.intp) - conv = np.empty_like(image, dtype=np.intp) ndimage.convolve(binary, selem, mode='constant', cval=0, output=conv) if out is None: From 2a944ef689314c8dee4b809a56647f9581178b92 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Mon, 14 Oct 2013 13:11:24 +0200 Subject: [PATCH 729/736] Update docs to match output. --- skimage/morphology/binary.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/skimage/morphology/binary.py b/skimage/morphology/binary.py index 6bd5d234..d74874a6 100644 --- a/skimage/morphology/binary.py +++ b/skimage/morphology/binary.py @@ -25,8 +25,8 @@ def binary_erosion(image, selem, out=None): Returns ------- - eroded : ndarray of bool - The result of the morphological erosion. + eroded : ndarray of bool or intp + The result of the morphological erosion with values in [0, 1]. """ selem = (selem != 0) @@ -69,8 +69,8 @@ def binary_dilation(image, selem, out=None): Returns ------- - dilated : ndarray of bool - The result of the morphological dilation. + dilated : ndarray of bool or intp + The result of the morphological dilation with values in [0, 1]. """ selem = (selem != 0) From f83c7a95e3eb42ee9cd4e48af7b51f0219b8097e Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Mon, 14 Oct 2013 13:58:04 +0200 Subject: [PATCH 730/736] Tweak markup of docstring. --- skimage/morphology/binary.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skimage/morphology/binary.py b/skimage/morphology/binary.py index d74874a6..0b98ed6c 100644 --- a/skimage/morphology/binary.py +++ b/skimage/morphology/binary.py @@ -26,7 +26,7 @@ def binary_erosion(image, selem, out=None): Returns ------- eroded : ndarray of bool or intp - The result of the morphological erosion with values in [0, 1]. + The result of the morphological erosion with values in ``[0, 1]``. """ selem = (selem != 0) @@ -70,7 +70,7 @@ def binary_dilation(image, selem, out=None): Returns ------- dilated : ndarray of bool or intp - The result of the morphological dilation with values in [0, 1]. + The result of the morphological dilation with values in ``[0, 1]``. """ selem = (selem != 0) From bba66db9c2c30076ce58ecff246075ffd658c146 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Mon, 14 Oct 2013 14:02:28 +0200 Subject: [PATCH 731/736] Provide stable doc version by naming docs directory 0.9.x, e.g. --- doc/gh-pages.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/gh-pages.py b/doc/gh-pages.py index dd798c02..af992158 100644 --- a/doc/gh-pages.py +++ b/doc/gh-pages.py @@ -85,6 +85,10 @@ if __name__ == '__main__': for l in setup_lines: if l.startswith('VERSION'): tag = l.split("'")[1] + + # Rename to, e.g., 0.9.x + tag = '.'.join(tag.split('.')[:-1] + ['x']) + break if "dev" in tag: From eb4c5c9ee4baf11b613a6b6613410974a97d7092 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Mon, 14 Oct 2013 16:14:30 +0200 Subject: [PATCH 732/736] PEP8 --- doc/examples/plot_join_segmentations.py | 1 - doc/examples/plot_marching_cubes.py | 1 - skimage/__init__.py | 1 + skimage/data/__init__.py | 1 - skimage/exposure/exposure.py | 2 +- skimage/filter/__init__.py | 2 +- skimage/filter/_denoise.py | 2 +- skimage/filter/_rank_order.py | 12 ++++++------ skimage/filter/edges.py | 6 +++--- skimage/morphology/convex_hull.py | 2 +- skimage/segmentation/_join.py | 5 ++--- viewer_examples/plugins/watershed_demo.py | 1 + 12 files changed, 17 insertions(+), 19 deletions(-) diff --git a/doc/examples/plot_join_segmentations.py b/doc/examples/plot_join_segmentations.py index 625113e5..8ccc5038 100644 --- a/doc/examples/plot_join_segmentations.py +++ b/doc/examples/plot_join_segmentations.py @@ -60,4 +60,3 @@ for ax in axes: ax.axis('off') plt.subplots_adjust(hspace=0.01, wspace=0.01, top=1, bottom=0, left=0, right=1) plt.show() - diff --git a/doc/examples/plot_marching_cubes.py b/doc/examples/plot_marching_cubes.py index 36685434..5dad1680 100644 --- a/doc/examples/plot_marching_cubes.py +++ b/doc/examples/plot_marching_cubes.py @@ -22,7 +22,6 @@ voxel spacing is not equal for every spatial dimension, through use of the """ import numpy as np import matplotlib.pyplot as plt -from mpl_toolkits.mplot3d import Axes3D from mpl_toolkits.mplot3d.art3d import Poly3DCollection from skimage import measure diff --git a/skimage/__init__.py b/skimage/__init__.py index 53d31eae..c73a79b7 100644 --- a/skimage/__init__.py +++ b/skimage/__init__.py @@ -91,6 +91,7 @@ test_verbose.__doc__ = test.__doc__ class _Log(Warning): pass + class _FakeLog(object): def __init__(self, name): """ diff --git a/skimage/data/__init__.py b/skimage/data/__init__.py index cea2a9f5..ecd261f7 100644 --- a/skimage/data/__init__.py +++ b/skimage/data/__init__.py @@ -200,4 +200,3 @@ def coffee(): """ return load("coffee.png") - diff --git a/skimage/exposure/exposure.py b/skimage/exposure/exposure.py index 9c50ab7d..fd5d53dd 100644 --- a/skimage/exposure/exposure.py +++ b/skimage/exposure/exposure.py @@ -287,7 +287,7 @@ def adjust_log(image, gain=1, inv=False): inv : float If True, it performs inverse logarithmic correction, else correction will be logarithmic. Defaults to False. - + Returns ------- out : ndarray diff --git a/skimage/filter/__init__.py b/skimage/filter/__init__.py index 67088b20..0d1c33bb 100644 --- a/skimage/filter/__init__.py +++ b/skimage/filter/__init__.py @@ -3,7 +3,7 @@ from .ctmf import median_filter from ._gaussian import gaussian_filter from ._canny import canny from .edges import (sobel, hsobel, vsobel, scharr, hscharr, vscharr, prewitt, - hprewitt, vprewitt, roberts , roberts_positive_diagonal, + hprewitt, vprewitt, roberts, roberts_positive_diagonal, roberts_negative_diagonal) from ._denoise import denoise_tv_chambolle, tv_denoise from ._denoise_cy import denoise_bilateral, denoise_tv_bregman diff --git a/skimage/filter/_denoise.py b/skimage/filter/_denoise.py index ff89b78b..5a810336 100644 --- a/skimage/filter/_denoise.py +++ b/skimage/filter/_denoise.py @@ -250,7 +250,7 @@ def denoise_tv_chambolle(im, weight=50, eps=2.e-4, n_iter_max=200, out = np.zeros_like(im) for c in range(im.shape[2]): out[..., c] = _denoise_tv_chambolle_2d(im[..., c], weight, eps, - n_iter_max) + n_iter_max) else: out = _denoise_tv_chambolle_3d(im, weight, eps, n_iter_max) else: diff --git a/skimage/filter/_rank_order.py b/skimage/filter/_rank_order.py index f878702f..cdd992ff 100644 --- a/skimage/filter/_rank_order.py +++ b/skimage/filter/_rank_order.py @@ -8,7 +8,7 @@ Copyright (c) 2009-2011 Broad Institute All rights reserved. Original author: Lee Kamentstky """ -import numpy +import numpy as np def rank_order(image): @@ -47,14 +47,14 @@ def rank_order(image): (array([0, 1, 2, 1], dtype=uint32), array([-1. , 2.5, 3.1])) """ flat_image = image.ravel() - sort_order = flat_image.argsort().astype(numpy.uint32) + sort_order = flat_image.argsort().astype(np.uint32) flat_image = flat_image[sort_order] - sort_rank = numpy.zeros_like(sort_order) + sort_rank = np.zeros_like(sort_order) is_different = flat_image[:-1] != flat_image[1:] - numpy.cumsum(is_different, out=sort_rank[1:]) - original_values = numpy.zeros((sort_rank[-1] + 1,), image.dtype) + np.cumsum(is_different, out=sort_rank[1:]) + original_values = np.zeros((sort_rank[-1] + 1,), image.dtype) original_values[0] = flat_image[0] original_values[1:] = flat_image[1:][is_different] - int_image = numpy.zeros_like(sort_order) + int_image = np.zeros_like(sort_order) int_image[sort_order] = sort_rank return (int_image.reshape(image.shape), original_values) diff --git a/skimage/filter/edges.py b/skimage/filter/edges.py index 7a70f00b..764c7d34 100644 --- a/skimage/filter/edges.py +++ b/skimage/filter/edges.py @@ -31,8 +31,8 @@ HPREWITT_WEIGHTS = np.array([[ 1, 1, 1], [-1,-1,-1]]) / 3.0 VPREWITT_WEIGHTS = HPREWITT_WEIGHTS.T -ROBERTS_PD_WEIGHTS = np.array([[ 1, 0], - [ 0, -1]], dtype=np.double) +ROBERTS_PD_WEIGHTS = np.array([[1, 0], + [0, -1]], dtype=np.double) ROBERTS_ND_WEIGHTS = np.array([[0, 1], [-1, 0]], dtype=np.double) @@ -346,7 +346,7 @@ def roberts(image, mask=None): """Find the edge magnitude using Roberts' cross operator. Parameters - ---------- + ---------- image : 2-D array Image to process. mask : 2-D array, optional diff --git a/skimage/morphology/convex_hull.py b/skimage/morphology/convex_hull.py index 3d592dca..adc63607 100644 --- a/skimage/morphology/convex_hull.py +++ b/skimage/morphology/convex_hull.py @@ -44,7 +44,7 @@ def convex_hull_image(image): (-0.5, 0.5, 0, 0))): coords_corners[i * N:(i + 1) * N] = coords + [x_offset, y_offset] - # repeated coordinates can *sometimes* cause problems in + # repeated coordinates can *sometimes* cause problems in # scipy.spatial.Delaunay, so we remove them. coords = unique_rows(coords_corners) diff --git a/skimage/segmentation/_join.py b/skimage/segmentation/_join.py index 462bb18f..6095382d 100644 --- a/skimage/segmentation/_join.py +++ b/skimage/segmentation/_join.py @@ -94,7 +94,7 @@ def relabel_sequential(label_field, offset=1): Examples -------- >>> from skimage.segmentation import relabel_sequential - >>> label_field = array([1, 1, 5, 5, 8, 99, 42]) + >>> label_field = np.array([1, 1, 5, 5, 8, 99, 42]) >>> relab, fw, inv = relabel_sequential(label_field) >>> relab array([1, 1, 2, 2, 3, 5, 4]) @@ -117,7 +117,7 @@ def relabel_sequential(label_field, offset=1): labels = np.unique(label_field) labels0 = labels[labels != 0] m = labels.max() - if m == len(labels0): # nothing to do, already 1...n labels + if m == len(labels0): # nothing to do, already 1...n labels return label_field, labels, labels forward_map = np.zeros(m+1, int) forward_map[labels0] = np.arange(offset, offset + len(labels0) + 1) @@ -127,4 +127,3 @@ def relabel_sequential(label_field, offset=1): inverse_map[(offset - 1):] = labels relabeled = forward_map[label_field] return relabeled, forward_map, inverse_map - diff --git a/viewer_examples/plugins/watershed_demo.py b/viewer_examples/plugins/watershed_demo.py index 683e8a30..612ec6c9 100644 --- a/viewer_examples/plugins/watershed_demo.py +++ b/viewer_examples/plugins/watershed_demo.py @@ -7,6 +7,7 @@ from skimage.viewer import ImageViewer from skimage.viewer.widgets import history from skimage.viewer.plugins.labelplugin import LabelPainter + class OKCancelButtons(history.OKCancelButtons): def update_original_image(self): From 69bdfe0339416cc5063bbb17ba4d7380075a235a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Mon, 14 Oct 2013 16:22:29 +0200 Subject: [PATCH 733/736] PEP8 another one --- skimage/util/shape.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/skimage/util/shape.py b/skimage/util/shape.py index 5fe27a36..f91286c3 100644 --- a/skimage/util/shape.py +++ b/skimage/util/shape.py @@ -233,9 +233,7 @@ def view_as_windows(arr_in, window_shape, step=1): tuple(window_shape) arr_strides = np.array(arr_in.strides) - new_strides = np.concatenate( - (arr_strides * step, arr_strides) - ) + new_strides = np.concatenate((arr_strides * step, arr_strides)) arr_out = as_strided(arr_in, shape=new_shape, strides=new_strides) From 65f73ee17123b13488c97a17cba661f394f284c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Mon, 14 Oct 2013 16:25:29 +0200 Subject: [PATCH 734/736] DOC: fix syntax error --- skimage/util/unique.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/skimage/util/unique.py b/skimage/util/unique.py index 16a83f6b..635f6e89 100644 --- a/skimage/util/unique.py +++ b/skimage/util/unique.py @@ -30,9 +30,9 @@ def unique_rows(ar): Examples -------- >>> ar = np.array([[1, 0, 1], - [0, 1, 0], - [1, 0, 1]], np.uint8) - >>> aru = unique_rows(ar) + ... [0, 1, 0], + ... [1, 0, 1]], np.uint8) + >>> unique_rows(ar) array([[0, 1, 0], [1, 0, 1]], dtype=uint8) """ From ca6ecf08e6bdc5b02f186e6efbf0f04d88c8564a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Mon, 14 Oct 2013 16:28:53 +0200 Subject: [PATCH 735/736] DOCTEST: fix --- skimage/measure/_marching_cubes.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/skimage/measure/_marching_cubes.py b/skimage/measure/_marching_cubes.py index 37dab1c0..41bafd90 100644 --- a/skimage/measure/_marching_cubes.py +++ b/skimage/measure/_marching_cubes.py @@ -77,9 +77,9 @@ def marching_cubes(volume, level, spacing=(1., 1., 1.)): >>> from mayavi import mlab >>> verts, tris = marching_cubes(myvolume, 0.0, (1., 1., 2.)) >>> mlab.triangular_mesh([vert[0] for vert in verts], - [vert[1] for vert in verts], - [vert[2] for vert in verts], - tris) + ... [vert[1] for vert in verts], + ... [vert[2] for vert in verts], + ... tris) >>> mlab.show() References From d182286e53fee484e5e304bd879a9b5da1a3e7e0 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Mon, 14 Oct 2013 16:41:08 +0200 Subject: [PATCH 736/736] Always use uint8 for binary view. --- skimage/morphology/binary.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/skimage/morphology/binary.py b/skimage/morphology/binary.py index 0b98ed6c..89ae8057 100644 --- a/skimage/morphology/binary.py +++ b/skimage/morphology/binary.py @@ -33,12 +33,11 @@ def binary_erosion(image, selem, out=None): selem_sum = np.sum(selem) if selem_sum <= 255: - binary = (image > 0).view(np.uint8) conv = np.empty_like(image, dtype=np.uint8) else: - binary = (image > 0).astype(np.intp) conv = np.empty_like(image, dtype=np.intp) + binary = (image > 0).view(np.uint8) ndimage.convolve(binary, selem, mode='constant', cval=1, output=conv) if out is None: @@ -74,13 +73,13 @@ def binary_dilation(image, selem, out=None): """ selem = (selem != 0) + if np.sum(selem) <= 255: - binary = (image > 0).view(np.uint8) conv = np.empty_like(image, dtype=np.uint8) else: - binary = (image > 0).astype(np.intp) conv = np.empty_like(image, dtype=np.intp) + binary = (image > 0).view(np.uint8) ndimage.convolve(binary, selem, mode='constant', cval=0, output=conv) if out is None: