diff options
Diffstat (limited to 'examples/findContours.py')
-rw-r--r-- | examples/findContours.py | 704 |
1 files changed, 704 insertions, 0 deletions
diff --git a/examples/findContours.py b/examples/findContours.py new file mode 100644 index 0000000..a5bb663 --- /dev/null +++ b/examples/findContours.py @@ -0,0 +1,704 @@ +#!/usr/bin/env python +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2016-2018 European Synchrotron Radiation Facility +# +# 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. +# +# ###########################################################################*/ +"""Find contours examples + +.. note:: This module has an optional dependancy with sci-kit image library. + You might need to install it if you don't already have it. +""" + +import logging +import sys +import numpy +import time + +logging.basicConfig() +_logger = logging.getLogger("find_contours") + +from silx.gui import qt +import silx.gui.plot +from silx.gui.colors import Colormap +import silx.image.bilinear + + +try: + import skimage +except ImportError: + _logger.debug("Error while importing skimage", exc_info=True) + skimage = None + +if skimage is not None: + try: + from silx.image.marchingsquares._skimage import MarchingSquaresSciKitImage + except ImportError: + _logger.debug("Error while importing MarchingSquaresSciKitImage", exc_info=True) + MarchingSquaresSciKitImage = None +else: + MarchingSquaresSciKitImage = None + + +def rescale_image(image, shape): + y, x = numpy.ogrid[:shape[0], :shape[1]] + y, x = y * 1.0 * (image.shape[0] - 1) / (shape[0] - 1), x * 1.0 * (image.shape[1] - 1) / (shape[1] - 1) + b = silx.image.bilinear.BilinearImage(image) + # TODO: could be optimized using strides + x2d = numpy.zeros_like(y) + x + y2d = numpy.zeros_like(x) + y + result = b.map_coordinates((y2d, x2d)) + return result + + +def create_spiral(size, nb=1, freq=100): + half = size // 2 + y, x = numpy.ogrid[-half:half, -half:half] + coef = 1.0 / half + y, x = y * coef, x * coef + 0.0001 + distance = numpy.sqrt(x * x + y * y) + angle = numpy.arctan(y / x) + data = numpy.sin(angle * nb * 2 + distance * freq * half / 100, dtype=numpy.float32) + return data + + +def create_magnetic_field(size, x1=0.0, y1=0.0, x2=0.0, y2=0.0): + half = size // 2 + yy, xx = numpy.ogrid[-half:half, -half:half] + coef = 1.0 / half + yy1, xx1 = (yy + half * y1) * coef, (xx + half * x1) * coef + distance1 = numpy.sqrt(xx1 * xx1 + yy1 * yy1) + yy2, xx2 = (yy + half * y2) * coef, (xx + half * x2) * coef + distance2 = numpy.sqrt(xx2 * xx2 + yy2 * yy2) + return (numpy.arctan2(distance1, distance2) - numpy.pi * 0.25) * 1000 + + +def create_gravity_field(size, objects): + half = size // 2 + yy, xx = numpy.ogrid[-half:half, -half:half] + coef = 1.0 / half + + def distance(x, y): + yy1, xx1 = (yy + half * y) * coef, (xx + half * x) * coef + return numpy.sqrt(xx1 ** 2 + yy1 ** 2) + result = numpy.zeros((size, size), dtype=numpy.float32) + for x, y, m in objects: + result += m / distance(x, y) + return numpy.log(result) * 1000 + + +def create_gradient(size, dx=0, dy=0, sx=1.0, sy=1.0): + half = size // 2 + yy, xx = numpy.ogrid[-half:half, -half:half] + coef = 1.0 / half + yy, xx = (yy - (dy * half)) * coef, (xx - (dx * half)) * coef + 0.0001 + distance = numpy.sqrt(xx * xx * sx + yy * yy * sy) + return distance + + +def create_composite_gradient(size, dx=0, dy=0, sx=1.0, sy=1.0): + hole = (size - 4) // 4 + gap = 10 + base = create_gradient(size + hole + gap * 4, dx, dy, sx, sy) + result = numpy.zeros((size, size)) + width = (size - 2) // 2 + half_hole = hole // 2 + + def copy_module(x1, y1, x2, y2, width, height): + result[y1:y1 + height, x1:x1 + width] = base[y2:y2 + height, x2:x2 + width] + + y1 = 0 + y2 = 0 + copy_module(0, y1, half_hole, y2, width, hole) + copy_module(width + 1, y1, half_hole + width, y2, width, hole) + + y1 += hole + 1 + y2 += hole + gap + copy_module(0, y1, 0, y2, width, hole) + copy_module(width + 1, y1, width + hole, y2, width, hole) + + y1 += hole + 1 + y2 += hole + gap + copy_module(0, y1, half_hole, y2, width, hole) + copy_module(width + 1, y1, half_hole + width, y2, width, hole) + + y1 += hole + 1 + y2 += hole + gap + copy_module(0, y1, half_hole, y2, width, hole) + copy_module(width + 1, y1, half_hole + width, y2, width, hole) + + return result + + +def create_value_noise(shape, octaves=8, weights=None, first_array=None): + data = numpy.zeros(shape, dtype=numpy.float32) + t = 2 + for i in range(octaves): + if t > shape[0] and t > shape[1]: + break + if i == 0 and first_array is not None: + d = first_array + else: + if weights is None: + w = (256 >> i) - 1 + else: + w = weights[i] + d = numpy.random.randint(w, size=(t, t)).astype(dtype=numpy.uint8) + d = rescale_image(d, shape) + data = data + d + t = t << 1 + return data + + +def create_island(shape, summit, under_water): + # Force a centric shape + first_array = numpy.zeros((4, 4), dtype=numpy.uint8) + first_array[1:3, 1:3] = 255 + weights = [255] + [(256 >> (i)) - 1 for i in range(8)] + data = create_value_noise(shape, octaves=7, first_array=first_array, weights=weights) + # more slops + data *= data + # normalize the height + data -= data.min() + data = data * ((summit + under_water) / data.max()) - under_water + return data + + +def createRgbaMaskImage(mask, color): + """Generate an RGBA image where a custom color is apply to the location of + the mask. Non masked part of the image is transparent.""" + image = numpy.zeros((mask.shape[0], mask.shape[1], 4), dtype=numpy.uint8) + color = numpy.array(color) + image[mask == True] = color + return image + + +class FindContours(qt.QMainWindow): + """ + This window show an example of use of a Hdf5TreeView. + + The tree is initialized with a list of filenames. A panel allow to play + with internal property configuration of the widget, and a text screen + allow to display events. + """ + + def __init__(self, filenames=None): + """ + :param files_: List of HDF5 or Spec files (pathes or + :class:`silx.io.spech5.SpecH5` or :class:`h5py.File` + instances) + """ + qt.QMainWindow.__init__(self) + self.setWindowTitle("Silx HDF5 widget example") + + self.__plot = silx.gui.plot.Plot2D(parent=self) + dummy = numpy.array([[0]]) + self.__plot.addImage(dummy, legend="image", z=-10, replace=False) + dummy = numpy.array([[[0, 0, 0, 0]]]) + self.__plot.addImage(dummy, legend="iso-pixels", z=0, replace=False) + + self.__algo = None + self.__polygons = [] + self.__customPolygons = [] + self.__image = None + self.__mask = None + self.__customValue = None + + mainPanel = qt.QWidget(self) + layout = qt.QHBoxLayout() + layout.addWidget(self.__createConfigurationPanel(self)) + layout.addWidget(self.__plot) + mainPanel.setLayout(layout) + + self.setCentralWidget(mainPanel) + + def __createConfigurationPanel(self, parent): + panel = qt.QWidget(parent=parent) + layout = qt.QVBoxLayout() + panel.setLayout(layout) + + self.__kind = qt.QButtonGroup(self) + self.__kind.setExclusive(True) + + group = qt.QGroupBox(self) + group.setTitle("Image") + layout.addWidget(group) + groupLayout = qt.QVBoxLayout(group) + + button = qt.QRadioButton(parent=panel) + button.setText("Island") + button.clicked.connect(self.generateIsland) + button.setCheckable(True) + button.setChecked(True) + groupLayout.addWidget(button) + self.__kind.addButton(button) + + button = qt.QRadioButton(parent=panel) + button.setText("Gravity") + button.clicked.connect(self.generateGravityField) + button.setCheckable(True) + groupLayout.addWidget(button) + self.__kind.addButton(button) + + button = qt.QRadioButton(parent=panel) + button.setText("Magnetic") + button.clicked.connect(self.generateMagneticField) + button.setCheckable(True) + groupLayout.addWidget(button) + self.__kind.addButton(button) + + button = qt.QRadioButton(parent=panel) + button.setText("Spiral") + button.clicked.connect(self.generateSpiral) + button.setCheckable(True) + groupLayout.addWidget(button) + self.__kind.addButton(button) + + button = qt.QRadioButton(parent=panel) + button.setText("Gradient") + button.clicked.connect(self.generateGradient) + button.setCheckable(True) + groupLayout.addWidget(button) + self.__kind.addButton(button) + + button = qt.QRadioButton(parent=panel) + button.setText("Composite gradient") + button.clicked.connect(self.generateCompositeGradient) + button.setCheckable(True) + groupLayout.addWidget(button) + self.__kind.addButton(button) + + button = qt.QPushButton(parent=panel) + button.setText("Generate a new image") + button.clicked.connect(self.generate) + groupLayout.addWidget(button) + + # Contours + + group = qt.QGroupBox(self) + group.setTitle("Contours") + layout.addWidget(group) + groupLayout = qt.QVBoxLayout(group) + + button = qt.QCheckBox(parent=panel) + button.setText("Use the plot's mask") + button.setCheckable(True) + button.setChecked(True) + button.clicked.connect(self.updateContours) + groupLayout.addWidget(button) + self.__useMaskButton = button + + button = qt.QPushButton(parent=panel) + button.setText("Update contours") + button.clicked.connect(self.updateContours) + groupLayout.addWidget(button) + + # Implementations + + group = qt.QGroupBox(self) + group.setTitle("Implementation") + layout.addWidget(group) + groupLayout = qt.QVBoxLayout(group) + + self.__impl = qt.QButtonGroup(self) + self.__impl.setExclusive(True) + + button = qt.QRadioButton(parent=panel) + button.setText("silx") + button.clicked.connect(self.updateContours) + button.setCheckable(True) + button.setChecked(True) + groupLayout.addWidget(button) + self.__implMerge = button + self.__impl.addButton(button) + + button = qt.QRadioButton(parent=panel) + button.setText("silx with cache") + button.clicked.connect(self.updateContours) + button.setCheckable(True) + groupLayout.addWidget(button) + self.__implMergeCache = button + self.__impl.addButton(button) + + button = qt.QRadioButton(parent=panel) + button.setText("skimage") + button.clicked.connect(self.updateContours) + button.setCheckable(True) + groupLayout.addWidget(button) + self.__implSkimage = button + self.__impl.addButton(button) + if MarchingSquaresSciKitImage is None: + button.setEnabled(False) + button.setToolTip("skimage is not installed or not compatible") + + # Processing + + group = qt.QGroupBox(self) + group.setTitle("Processing") + layout.addWidget(group) + group.setLayout(self.__createInfoLayout(group)) + + # Processing + + group = qt.QGroupBox(self) + group.setTitle("Custom level") + layout.addWidget(group) + groupLayout = qt.QVBoxLayout(group) + + label = qt.QLabel(parent=panel) + self.__value = qt.QSlider(panel) + self.__value.setOrientation(qt.Qt.Horizontal) + self.__value.sliderMoved.connect(self.__updateCustomContours) + self.__value.valueChanged.connect(self.__updateCustomContours) + groupLayout.addWidget(self.__value) + + return panel + + def __createInfoLayout(self, parent): + layout = qt.QGridLayout() + + header = qt.QLabel(parent=parent) + header.setText("Time: ") + label = qt.QLabel(parent=parent) + label.setText("") + layout.addWidget(header, 0, 0) + layout.addWidget(label, 0, 1) + self.__timeLabel = label + + header = qt.QLabel(parent=parent) + header.setText("Nb polygons: ") + label = qt.QLabel(parent=parent) + label.setText("") + layout.addWidget(header, 2, 0) + layout.addWidget(label, 2, 1) + self.__polygonsLabel = label + + header = qt.QLabel(parent=parent) + header.setText("Nb points: ") + label = qt.QLabel(parent=parent) + label.setText("") + layout.addWidget(header, 1, 0) + layout.addWidget(label, 1, 1) + self.__pointsLabel = label + + return layout + + def __cleanCustomContour(self): + for name in self.__customPolygons: + self.__plot.removeCurve(name) + self.__customPolygons = [] + dummy = numpy.array([[[0, 0, 0, 0]]]) + item = self.__plot.getImage(legend="iso-pixels") + item.setData([[[0, 0, 0, 0]]]) + + def __cleanPolygons(self): + for name in self.__polygons: + self.__plot.removeCurve(name) + + def clean(self): + self.__cleanCustomContour() + self.__cleanPolygons() + self.__polygons = [] + self.__image = None + self.__mask = None + + def updateContours(self): + self.__redrawContours() + self.updateCustomContours() + + def __updateCustomContours(self, value): + self.__customValue = value + self.updateCustomContours() + + def updateCustomContours(self): + if self.__algo is None: + return + value = self.__customValue + self.__cleanCustomContour() + if value is None: + return + + # iso pixels + iso_pixels = self.__algo.find_pixels(value) + if len(iso_pixels) != 0: + mask = numpy.zeros(self.__image.shape, dtype=numpy.int8) + indexes = iso_pixels[:, 0] * self.__image.shape[1] + iso_pixels[:, 1] + mask = mask.ravel() + mask[indexes] = 1 + mask.shape = self.__image.shape + mask = createRgbaMaskImage(mask, color=numpy.array([255, 0, 0, 128])) + item = self.__plot.getImage(legend="iso-pixels") + item.setData(mask) + + # iso contours + polygons = self.__algo.find_contours(value) + for ipolygon, polygon in enumerate(polygons): + if len(polygon) == 0: + continue + isClosed = numpy.allclose(polygon[0], polygon[-1]) + x = polygon[:, 1] + 0.5 + y = polygon[:, 0] + 0.5 + legend = "custom-polygon-%d" % ipolygon + self.__customPolygons.append(legend) + self.__plot.addCurve(x=x, y=y, linestyle="--", color="red", linewidth=2.0, legend=legend, resetzoom=False) + + def __updateAlgo(self, image, mask=None): + if mask is None: + if self.__useMaskButton.isChecked(): + mask = self.__plot.getMaskToolsDockWidget().getSelectionMask() + + self.__image = image + self.__mask = mask + + implButton = self.__impl.checkedButton() + if implButton == self.__implMerge: + from silx.image.marchingsquares import MarchingSquaresMergeImpl + self.__algo = MarchingSquaresMergeImpl(self.__image, self.__mask) + elif implButton == self.__implMergeCache: + from silx.image.marchingsquares import MarchingSquaresMergeImpl + self.__algo = MarchingSquaresMergeImpl(self.__image, self.__mask, use_minmax_cache=True) + elif implButton == self.__implSkimage and MarchingSquaresSciKitImage is not None: + self.__algo = MarchingSquaresSciKitImage(self.__image, self.__mask) + else: + _logger.error("No algorithm available") + self.__algo = None + + def setData(self, image, mask=None, value=0.0): + self.clean() + + self.__updateAlgo(image, mask=None) + + # image + item = self.__plot.getImage(legend="image") + item.setData(image) + item.setColormap(self.__colormap) + + self.__plot.resetZoom() + + def __redrawContours(self): + self.__updateAlgo(self.__image) + if self.__algo is None: + return + self.__cleanPolygons() + self.__drawContours(self.__values, self.__lineStyleCallback) + + def __drawContours(self, values, lineStyleCallback=None): + if self.__algo is None: + return + + self.__values = values + self.__lineStyleCallback = lineStyleCallback + if self.__values is None: + return + + nbTime = 0 + nbPolygons = 0 + nbPoints = 0 + + # iso contours + ipolygon = 0 + for ivalue, value in enumerate(values): + startTime = time.time() + polygons = self.__algo.find_contours(value) + nbTime += (time.time() - startTime) + nbPolygons += len(polygons) + for polygon in polygons: + if len(polygon) == 0: + continue + nbPoints += len(polygon) + isClosed = numpy.allclose(polygon[0], polygon[-1]) + x = polygon[:, 1] + 0.5 + y = polygon[:, 0] + 0.5 + legend = "polygon-%d" % ipolygon + if lineStyleCallback is not None: + extraStyle = lineStyleCallback(value, ivalue, ipolygon) + else: + extraStyle = {"linestyle": "-", "linewidth": 1.0, "color": "black"} + self.__polygons.append(legend) + self.__plot.addCurve(x=x, y=y, legend=legend, resetzoom=False, **extraStyle) + ipolygon += 1 + + self.__timeLabel.setText("%0.3fs" % nbTime) + self.__polygonsLabel.setText("%d" % nbPolygons) + self.__pointsLabel.setText("%d" % nbPoints) + + def __defineDefaultValues(self, value=None): + # Do not use min and max to avoid to create iso contours on small + # and many artefacts + if value is None: + value = self.__image.mean() + self.__customValue = value + div = 12 + delta = (self.__image.max() - self.__image.min()) / div + self.__value.setValue(value) + self.__value.setRange(self.__image.min() + delta, + self.__image.min() + delta * (div - 1)) + self.updateCustomContours() + + def generate(self): + self.__kind.checkedButton().click() + + def generateSpiral(self): + shape = 512 + nb_spiral = numpy.random.randint(1, 8) + freq = numpy.random.randint(2, 50) + image = create_spiral(shape, nb_spiral, freq) + image *= 1000.0 + self.__colormap = Colormap("cool") + self.setData(image=image, mask=None) + self.__defineDefaultValues() + + def generateIsland(self): + shape = (512, 512) + image = create_island(shape, summit=4808.72, under_water=1500) + self.__colormap = Colormap("terrain") + self.setData(image=image, mask=None) + + values = range(-800, 5000, 200) + + def styleCallback(value, ivalue, ipolygon): + if value == 0: + style = {"linestyle": "-", "linewidth": 1.0, "color": "black"} + elif value % 1000 == 0: + style = {"linestyle": "--", "linewidth": 0.5, "color": "black"} + else: + style = {"linestyle": "--", "linewidth": 0.1, "color": "black"} + return style + + self.__drawContours(values, styleCallback) + + self.__value.setValue(0) + self.__value.setRange(0, 5000) + self.__updateCustomContours(0) + + def generateMagneticField(self): + shape = 512 + x1 = numpy.random.random() * 2 - 1 + y1 = numpy.random.random() * 2 - 1 + x2 = numpy.random.random() * 2 - 1 + y2 = numpy.random.random() * 2 - 1 + image = create_magnetic_field(shape, x1, y1, x2, y2) + self.__colormap = Colormap("coolwarm") + self.setData(image=image, mask=None) + + maximum = abs(image.max()) + m = abs(image.min()) + if m > maximum: + maximum = m + maximum = int(maximum) + values = range(-maximum, maximum, maximum // 20) + + def styleCallback(value, ivalue, ipolygon): + if (ivalue % 2) == 0: + style = {"linestyle": "-", "linewidth": 0.5, "color": "black"} + else: + style = {"linestyle": "-", "linewidth": 0.5, "color": "white"} + return style + + self.__drawContours(values, styleCallback) + self.__defineDefaultValues(value=0) + + def generateGravityField(self): + shape = 512 + nb = numpy.random.randint(2, 10) + objects = [] + for _ in range(nb): + x = numpy.random.random() * 2 - 1 + y = numpy.random.random() * 2 - 1 + m = numpy.random.random() * 10 + 1.0 + objects.append((x, y, m)) + image = create_gravity_field(shape, objects) + self.__colormap = Colormap("inferno") + self.setData(image=image, mask=None) + + delta = (image.max() - image.min()) / 30.0 + values = numpy.arange(image.min(), image.max(), delta) + + def styleCallback(value, ivalue, ipolygon): + return {"linestyle": "-", "linewidth": 0.1, "color": "white"} + + self.__drawContours(values, styleCallback) + self.__defineDefaultValues() + + def generateGradient(self): + shape = 512 + dx = numpy.random.random() * 2 - 1 + dy = numpy.random.random() * 2 - 1 + sx = numpy.random.randint(10, 5000) / 10.0 + sy = numpy.random.randint(10, 5000) / 10.0 + image = create_gradient(shape, dx=dx, dy=dy, sx=sx, sy=sy) + image *= 1000.0 + + def styleCallback(value, ivalue, ipolygon): + colors = ["#9400D3", "#4B0082", "#0000FF", "#00FF00", "#FFFF00", "#FF7F00", "#FF0000"] + color = colors[ivalue % len(colors)] + style = {"linestyle": "-", "linewidth": 2.0, "color": color} + return style + delta = (image.max() - image.min()) / 9.0 + values = numpy.arange(image.min(), image.max(), delta) + values = values[1:8] + + self.__colormap = Colormap("Greys") + self.setData(image=image, mask=None) + self.__drawContours(values, styleCallback) + self.__defineDefaultValues() + + def generateCompositeGradient(self): + shape = 512 + hole = 1 / 4.0 + dx = numpy.random.random() * hole - hole / 2.0 + dy = numpy.random.random() * hole - hole * 2 + sx = numpy.random.random() * 10.0 + 1 + sy = numpy.random.random() * 10.0 + 1 + image = create_composite_gradient(shape, dx, dy, sx, sy) + image *= 1000.0 + + def styleCallback(value, ivalue, ipolygon): + colors = ["#9400D3", "#4B0082", "#0000FF", "#00FF00", "#FFFF00", "#FF7F00", "#FF0000"] + color = colors[ivalue % len(colors)] + style = {"linestyle": "-", "linewidth": 2.0, "color": color} + return style + delta = (image.max() - image.min()) / 9.0 + values = numpy.arange(image.min(), image.max(), delta) + values = values[1:8] + + self.__colormap = Colormap("Greys") + self.setData(image=image, mask=None) + self.__drawContours(values, styleCallback) + self.__defineDefaultValues() + + +def main(): + app = qt.QApplication([]) + sys.excepthook = qt.exceptionHandler + window = FindContours() + window.generateIsland() + window.show() + result = app.exec_() + # remove ending warnings relative to QTimer + app.deleteLater() + return result + + +if __name__ == "__main__": + result = main() + sys.exit(result) |