summaryrefslogtreecommitdiff
path: root/src/silx/image/marchingsquares/_mergeimpl.pyx
diff options
context:
space:
mode:
Diffstat (limited to 'src/silx/image/marchingsquares/_mergeimpl.pyx')
-rw-r--r--src/silx/image/marchingsquares/_mergeimpl.pyx1319
1 files changed, 1319 insertions, 0 deletions
diff --git a/src/silx/image/marchingsquares/_mergeimpl.pyx b/src/silx/image/marchingsquares/_mergeimpl.pyx
new file mode 100644
index 0000000..5a7a3b5
--- /dev/null
+++ b/src/silx/image/marchingsquares/_mergeimpl.pyx
@@ -0,0 +1,1319 @@
+# coding: utf-8
+# /*##########################################################################
+# Copyright (C) 2018-2020 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.
+#
+# ############################################################################*/
+"""
+Marching squares implementation based on a merge of segements and polygons.
+"""
+
+__authors__ = ["Almar Klein", "Jerome Kieffer", "Valentin Valls"]
+__license__ = "MIT"
+__date__ = "23/04/2018"
+
+import numpy
+cimport numpy as cnumpy
+
+from libcpp.vector cimport vector
+from libcpp.list cimport list as clist
+from libcpp.set cimport set as cset
+from libcpp.map cimport map
+from libcpp cimport bool
+from libc.math cimport fabs
+from libc.math cimport floor
+
+from cython.parallel import prange
+from cython.operator cimport dereference
+from cython.operator cimport preincrement
+cimport libc.stdlib
+cimport libc.string
+
+cimport cython
+
+from ...utils._have_openmp cimport COMPILED_WITH_OPENMP
+"""Store in the module if it was compiled with OpenMP"""
+
+cdef double EPSILON = numpy.finfo(numpy.float64).eps
+
+# Windows compatibility: Cross-platform INFINITY
+from libc.float cimport DBL_MAX
+cdef double INFINITY = DBL_MAX + DBL_MAX
+# from libc.math cimport INFINITY
+
+cdef extern from "include/patterns.h":
+ cdef unsigned char EDGE_TO_POINT[][2]
+ cdef unsigned char CELL_TO_EDGE[][5]
+ cdef struct coord_t:
+ short x
+ short y
+
+ctypedef cnumpy.uint32_t point_index_t
+"""Type of the unique index identifying a connection for the polygons."""
+
+"""Define a point of a polygon."""
+cdef struct point_t:
+ cnumpy.float32_t x
+ cnumpy.float32_t y
+
+"""Description of a non-final polygon."""
+cdef cppclass PolygonDescription:
+ point_index_t begin
+ point_index_t end
+ clist[point_t] points
+
+ PolygonDescription() nogil:
+ pass
+
+"""Description of a tile context.
+
+It contains structure to store intermediate and final data of a thread.
+Pixels and contours structures are merged together as it looks to have
+mostly no cost.
+"""
+cdef cppclass TileContext:
+ int pos_x
+ int pos_y
+ int dim_x
+ int dim_y
+
+ # Only used to find contours
+ clist[PolygonDescription*] final_polygons
+ map[point_index_t, PolygonDescription*] polygons
+
+ # Only used to find pixels
+ clist[coord_t] final_pixels
+ cset[coord_t] pixels
+
+ TileContext() nogil:
+ pass
+
+
+cdef class _MarchingSquaresAlgorithm(object):
+ """Abstract class managing a marching squares algorithm.
+
+ It provides common methods to execute the process, with the support of
+ OpenMP, plus some hooks. Mostly created to be able to reuse part of the
+ logic between `_MarchingSquaresContours` and `_MarchingSquaresPixels`.
+ """
+
+ cdef cnumpy.float32_t *_image_ptr
+ cdef cnumpy.int8_t *_mask_ptr
+ cdef int _dim_x
+ cdef int _dim_y
+ cdef int _group_size
+ cdef bool _use_minmax_cache
+ cdef bool _force_sequencial_reduction
+
+ cdef TileContext* _final_context
+
+ cdef cnumpy.float32_t *_min_cache
+ cdef cnumpy.float32_t *_max_cache
+
+ def __cinit__(self):
+ self._use_minmax_cache = False
+ self._force_sequencial_reduction = False
+
+ @cython.boundscheck(False)
+ @cython.wraparound(False)
+ @cython.cdivision(True)
+ cdef void marching_squares(self, cnumpy.float64_t level) nogil:
+ """
+ Main method to execute the marching squares.
+
+ :param level: The level expected.
+ """
+ cdef:
+ TileContext** contexts
+ TileContext** valid_contexts
+ int nb_contexts, nb_valid_contexts
+ int i, j
+ TileContext* context
+ int dim_x, dim_y
+
+ contexts = self.create_contexts(level, &dim_x, &dim_y, &nb_valid_contexts)
+ nb_contexts = dim_x * dim_y
+
+ if nb_valid_contexts == 0:
+ # shortcut
+ self._final_context = new TileContext()
+ libc.stdlib.free(contexts)
+ return
+
+ j = 0
+ valid_contexts = <TileContext **>libc.stdlib.malloc(nb_valid_contexts * sizeof(TileContext*))
+ for i in xrange(nb_contexts):
+ if contexts[i] != NULL:
+ valid_contexts[j] = contexts[i]
+ j += 1
+
+ # openmp
+ for i in prange(nb_valid_contexts, nogil=True):
+ self.marching_squares_mp(valid_contexts[i], level)
+
+ if nb_valid_contexts == 1:
+ # shortcut
+ self._final_context = valid_contexts[0]
+ libc.stdlib.free(valid_contexts)
+ libc.stdlib.free(contexts)
+ return
+
+ if self._force_sequencial_reduction:
+ self.sequencial_reduction(nb_valid_contexts, valid_contexts)
+ # FIXME can only be used if compiled with openmp
+ # elif copenmp.omp_get_num_threads() <= 1:
+ # self._sequencial_reduction(nb_valid_contexts, valid_contexts)
+ else:
+ self.reduction_2d(dim_x, dim_y, contexts)
+
+ libc.stdlib.free(valid_contexts)
+ libc.stdlib.free(contexts)
+
+ @cython.boundscheck(False)
+ @cython.wraparound(False)
+ @cython.cdivision(True)
+ cdef void reduction_2d(self, int dim_x, int dim_y, TileContext **contexts) nogil:
+ """
+ Reduce the problem merging first neighbours together in a recursive
+ process. Optimized with OpenMP.
+
+ :param dim_x: Number of contexts in the x dimension
+ :param dim_y: Number of contexts in the y dimension
+ :param contexts: Array of contexts
+ """
+ cdef:
+ int x1, y1, x2, y2, i1, i2
+ int delta = 1
+
+ while True:
+ if delta >= dim_x and delta >= dim_y:
+ break
+ # NOTE: Cython 0.21.1 is buggy with prange + steps
+ # It is needed to add a delta and the 'to'
+ # Here is what we can use with Cython 0.28:
+ # for i in prange(0, dim_x, (delta + delta)):
+ for i1 in prange(0, dim_x + (delta + delta - 1), delta + delta, nogil=True):
+ x1 = i1
+ if x1 + delta < dim_x:
+ y1 = 0
+ while y1 < dim_y:
+ self.merge_array_contexts(contexts, y1 * dim_x + x1, y1 * dim_x + x1 + delta)
+ y1 = y1 + delta
+
+ # NOTE: Cython 0.21.1 is buggy with prange + steps
+ # It is needed to add a delta and the 'to'
+ # Here is what we can use with Cython 0.28:
+ # for i in prange(0, dim_y, (delta + delta)):
+ for i2 in prange(0, dim_y + (delta + delta - 1), delta + delta, nogil=True):
+ y2 = i2
+ if y2 + delta < dim_y:
+ x2 = 0
+ while x2 < dim_x:
+ self.merge_array_contexts(contexts, y2 * dim_x + x2, (y2 + delta) * dim_x + x2)
+ x2 = x2 + delta + delta
+ delta <<= 1
+
+ self._final_context = contexts[0]
+
+ @cython.boundscheck(False)
+ @cython.wraparound(False)
+ @cython.cdivision(True)
+ cdef inline void merge_array_contexts(self,
+ TileContext **contexts,
+ int index1,
+ int index2) nogil:
+ """
+ Merge contexts from `index2` to `index1` and delete the one from index2.
+ If the one from index1 was NULL, the one from index2 is moved to index1
+ and is not deleted.
+
+ This intermediate function was needed to avoid compilation problem of
+ Cython + OpenMP.
+ """
+ cdef:
+ TileContext *context1
+ TileContext *context2
+
+ context1 = contexts[index1]
+ context2 = contexts[index2]
+ if context1 != NULL and context2 != NULL:
+ self.merge_context(context1, context2)
+ del context2
+ elif context2 != NULL:
+ contexts[index1] = context2
+ # for sanity
+ # contexts[index2] = NULL
+
+ @cython.boundscheck(False)
+ @cython.wraparound(False)
+ @cython.cdivision(True)
+ cdef void sequencial_reduction(self,
+ int nb_contexts,
+ TileContext **contexts) nogil:
+ """
+ Reduce the problem sequencially without taking care of the topology
+
+ :param nb_contexts: Number of contexts
+ :param contexts: Array of contexts
+ """
+ cdef:
+ int i
+ # merge
+ self._final_context = new TileContext()
+ for i in xrange(nb_contexts):
+ if contexts[i] != NULL:
+ self.merge_context(self._final_context, contexts[i])
+ del contexts[i]
+
+ @cython.boundscheck(False)
+ @cython.wraparound(False)
+ @cython.cdivision(True)
+ cdef void marching_squares_mp(self,
+ TileContext *context,
+ cnumpy.float64_t level) nogil:
+ """
+ Main entry of the marching squares algorithm for each threads.
+
+ :param context: Context used by the thread to store data
+ :param level: The requested level
+ """
+ cdef:
+ int x, y, pattern
+ cnumpy.float64_t tmpf
+ cnumpy.float32_t *image_ptr
+ cnumpy.int8_t *mask_ptr
+
+ image_ptr = self._image_ptr + (context.pos_y * self._dim_x + context.pos_x)
+ if self._mask_ptr != NULL:
+ mask_ptr = self._mask_ptr + (context.pos_y * self._dim_x + context.pos_x)
+ else:
+ mask_ptr = NULL
+
+ for y in range(context.pos_y, context.pos_y + context.dim_y):
+ for x in range(context.pos_x, context.pos_x + context.dim_x):
+ # Calculate index.
+ pattern = 0
+ if image_ptr[0] > level:
+ pattern += 1
+ if image_ptr[1] > level:
+ pattern += 2
+ if image_ptr[self._dim_x] > level:
+ pattern += 8
+ if image_ptr[self._dim_x + 1] > level:
+ pattern += 4
+
+ # Resolve ambiguity
+ if pattern == 5 or pattern == 10:
+ # Calculate value of cell center (i.e. average of corners)
+ tmpf = 0.25 * (image_ptr[0] +
+ image_ptr[1] +
+ image_ptr[self._dim_x] +
+ image_ptr[self._dim_x + 1])
+ # If below level, swap
+ if tmpf <= level:
+ if pattern == 5:
+ pattern = 10
+ else:
+ pattern = 5
+
+ # Cache mask information
+ if mask_ptr != NULL:
+ # Note: Store the mask in the index. It could be usefull to
+ # generate accurate segments in some cases, but yet it
+ # is not used
+ if mask_ptr[0] > 0:
+ pattern += 16
+ if mask_ptr[1] > 0:
+ pattern += 32
+ if mask_ptr[self._dim_x] > 0:
+ pattern += 128
+ if mask_ptr[self._dim_x + 1] > 0:
+ pattern += 64
+ mask_ptr += 1
+
+ if pattern < 16 and pattern != 0 and pattern != 15:
+ self.insert_pattern(context, x, y, pattern, level)
+
+ image_ptr += 1
+
+ # There is a missing pixel at the end of each rows
+ image_ptr += self._dim_x - context.dim_x
+ if mask_ptr != NULL:
+ mask_ptr += self._dim_x - context.dim_x
+
+ self.after_marching_squares(context)
+
+ @cython.boundscheck(False)
+ @cython.wraparound(False)
+ @cython.cdivision(True)
+ cdef void after_marching_squares(self, TileContext *context) nogil:
+ """
+ Called by each threads after execution of the marching squares
+ algorithm. Called before merging together the contextes.
+
+ :param context: Context used by the thread to store data
+ """
+ pass
+
+ @cython.boundscheck(False)
+ @cython.wraparound(False)
+ @cython.cdivision(True)
+ cdef void insert_pattern(self,
+ TileContext *context,
+ int x,
+ int y,
+ int pattern,
+ cnumpy.float64_t level) nogil:
+ """
+ Called by the marching squares algorithm each time a pattern is found.
+
+ :param context: Context used by the thread to store data
+ :param x: X location of the pattern
+ :param y: Y location of the pattern
+ :param pattern: Binary-field identifying lower and higher pixel levels
+ :param level: The requested level
+ """
+ pass
+
+ @cython.boundscheck(False)
+ @cython.wraparound(False)
+ @cython.cdivision(True)
+ cdef void merge_context(self,
+ TileContext *context,
+ TileContext *other) nogil:
+ """
+ Merge into a context another context.
+
+ :param context: Context which will contains the merge result
+ :param other: Context to merge into the other one. The merging process
+ is destructive. The context may returns empty.
+ """
+ pass
+
+ @cython.boundscheck(False)
+ @cython.wraparound(False)
+ @cython.cdivision(True)
+ cdef TileContext** create_contexts(self,
+ cnumpy.float64_t level,
+ int* dim_x,
+ int* dim_y,
+ int* nb_valid_contexts) nogil:
+ """
+ Create and initialize a 2d-array of contexts.
+
+ If the minmax cache is used, only useful context will be created.
+ Thous with the minmax range excluding the level will not be created and
+ will have a `NULL` reference in the context array.
+
+ :param level: The requested level
+ :param dim_x: Resulting X dimension of context array
+ :param dim_x: Resulting Y dimension of context array
+ :param nb_valid_contexts: Resulting number of created contexts
+ :return: The context array
+ """
+ cdef:
+ int context_dim_x, context_dim_y
+ int context_size, valid_contexts
+ int x, y
+ int icontext
+ TileContext* context
+ TileContext** contexts
+
+ context_dim_x = self._dim_x // self._group_size + (self._dim_x % self._group_size > 0)
+ context_dim_y = self._dim_y // self._group_size + (self._dim_y % self._group_size > 0)
+ context_size = context_dim_x * context_dim_y
+ contexts = <TileContext **>libc.stdlib.malloc(context_size * sizeof(TileContext*))
+ libc.string.memset(contexts, 0, context_size * sizeof(TileContext*))
+
+ valid_contexts = 0
+ icontext = 0
+ y = 0
+ while y < self._dim_y - 1:
+ x = 0
+ while x < self._dim_x - 1:
+ if self._use_minmax_cache:
+ if level < self._min_cache[icontext] or level > self._max_cache[icontext]:
+ icontext += 1
+ x += self._group_size
+ continue
+ context = self.create_context(x, y, self._group_size, self._group_size)
+ contexts[icontext] = context
+ icontext += 1
+ valid_contexts += 1
+ x += self._group_size
+ y += self._group_size
+
+ # dereference is not working here... then we uses array index but
+ # it is not the proper way
+ dim_x[0] = context_dim_x
+ dim_y[0] = context_dim_y
+ nb_valid_contexts[0] = valid_contexts
+ return contexts
+
+ @cython.boundscheck(False)
+ @cython.wraparound(False)
+ @cython.cdivision(True)
+ cdef TileContext *create_context(self,
+ int x,
+ int y,
+ int dim_x,
+ int dim_y) nogil:
+ """
+ Allocate and initialize a context.
+
+ :param x: Left location of the context into the image
+ :param y: Top location of the context into the image
+ :param dim_x: Size of the context in the X dimension of the image
+ :param dim_y: Size of the context in the Y dimension of the image
+ :return: The context
+ """
+ cdef:
+ TileContext *context
+ context = new TileContext()
+ context.pos_x = x
+ context.pos_y = y
+ context.dim_x = dim_x
+ context.dim_y = dim_y
+ if x + context.dim_x > self._dim_x - 1:
+ context.dim_x = self._dim_x - 1 - x
+ if y + context.dim_y > self._dim_y - 1:
+ context.dim_y = self._dim_y - 1 - y
+ if context.dim_x <= 0 or context.dim_y <= 0:
+ del context
+ return NULL
+ return context
+
+ @cython.boundscheck(False)
+ @cython.wraparound(False)
+ @cython.cdivision(True)
+ cdef void compute_point(self,
+ cnumpy.uint32_t x,
+ cnumpy.uint32_t y,
+ cnumpy.uint8_t edge,
+ cnumpy.float64_t level,
+ point_t *result_point) nogil:
+ """
+ Compute the location of a point of the polygons according to the level
+ and the neighbours.
+
+ :param x: X location of the 4-pixels
+ :param y: Y location of the 4-pixels
+ :param edge: Enumeration identifying the 2-pixels to process
+ :param level: The requested level
+ :param result_point: Resulting value of the point
+ """
+ cdef:
+ int dx1, dy1, index1
+ int dx2, dy2, index2
+ cnumpy.float64_t fx, fy, ff, weight1, weight2
+ # Use these to look up the relative positions of the pixels to interpolate
+ dx1, dy1 = EDGE_TO_POINT[edge][0], EDGE_TO_POINT[edge][1]
+ dx2, dy2 = EDGE_TO_POINT[edge + 1][0], EDGE_TO_POINT[edge + 1][1]
+ # Define "strength" of each corner of the cube that we need
+ index1 = (y + dy1) * self._dim_x + x + dx1
+ index2 = (y + dy2) * self._dim_x + x + dx2
+ weight1 = 1.0 / (EPSILON + fabs(self._image_ptr[index1] - level))
+ weight2 = 1.0 / (EPSILON + fabs(self._image_ptr[index2] - level))
+ # Apply a kind of center-of-mass method
+ fx, fy, ff = 0.0, 0.0, 0.0
+ fx += dx1 * weight1
+ fy += dy1 * weight1
+ ff += weight1
+ fx += dx2 * weight2
+ fy += dy2 * weight2
+ ff += weight2
+ fx /= ff
+ fy /= ff
+ result_point.x = x + fx
+ result_point.y = y + fy
+
+ @cython.boundscheck(False)
+ @cython.wraparound(False)
+ @cython.cdivision(True)
+ cdef void compute_ipoint(self,
+ cnumpy.uint32_t x,
+ cnumpy.uint32_t y,
+ cnumpy.uint8_t edge,
+ cnumpy.float64_t level,
+ coord_t *result_coord) nogil:
+ """
+ Compute the location of pixel which contains the point of the polygons
+ according to the level and the neighbours.
+
+ This implementation is supposed to be faster than `compute_point` when
+ we only request the location of the pixel.
+
+ :param x: X location of the 4-pixels
+ :param y: Y location of the 4-pixels
+ :param edge: Enumeration identifying the 2-pixels to process
+ :param level: The requested level
+ :param result_coord: Resulting location of the pixel
+ """
+ cdef:
+ int dx1, dy1, index1
+ int dx2, dy2, index2
+ cnumpy.float64_t fx, fy, ff, weight1, weight2
+ # Use these to look up the relative positions of the pixels to interpolate
+ dx1, dy1 = EDGE_TO_POINT[edge][0], EDGE_TO_POINT[edge][1]
+ dx2, dy2 = EDGE_TO_POINT[edge + 1][0], EDGE_TO_POINT[edge + 1][1]
+ # Define "strength" of each corner of the cube that we need
+ index1 = (y + dy1) * self._dim_x + x + dx1
+ index2 = (y + dy2) * self._dim_x + x + dx2
+ weight1 = EPSILON + fabs(self._image_ptr[index1] - level)
+ weight2 = EPSILON + fabs(self._image_ptr[index2] - level)
+ # Apply a kind of center-of-mass method
+ if edge == 0:
+ result_coord.x = x + (weight1 > weight2)
+ result_coord.y = y
+ elif edge == 1:
+ result_coord.x = x + 1
+ result_coord.y = y + (weight1 > weight2)
+ elif edge == 2:
+ result_coord.x = x + (weight1 < weight2)
+ result_coord.y = y + 1
+ elif edge == 3:
+ result_coord.x = x
+ result_coord.y = y + (weight1 < weight2)
+
+ @cython.boundscheck(False)
+ @cython.wraparound(False)
+ @cython.cdivision(True)
+ cdef point_index_t create_point_index(self, int yx, cnumpy.uint8_t edge) nogil:
+ """
+ Create a unique identifier for a point of a polygon based on the
+ pattern location and the edge.
+
+ A index can be shared by different pixel coordinates. For example,
+ the index of the tuple (x=0, y=0, edge=2) is equal to the one of
+ (x=1, y=0, edge=0).
+
+ :param yx: Index of the location of the pattern in the image
+ :param edge: Enumeration identifying the edge of the pixel
+ :return: An index
+ """
+ if edge == 2:
+ yx += self._dim_x
+ edge = 0
+ elif edge == 1:
+ yx += 1
+ elif edge == 3:
+ edge = 1
+
+ # Reserve the zero value
+ yx += 1
+
+ return edge + (yx << 1)
+
+
+cdef class _MarchingSquaresContours(_MarchingSquaresAlgorithm):
+ """Implementation of the marching squares algorithm to find iso contours.
+ """
+
+ @cython.boundscheck(False)
+ @cython.wraparound(False)
+ @cython.cdivision(True)
+ cdef void insert_pattern(self,
+ TileContext *context,
+ int x,
+ int y,
+ int pattern,
+ cnumpy.float64_t level) nogil:
+ cdef:
+ int segment
+ for segment in range(CELL_TO_EDGE[pattern][0]):
+ begin_edge = CELL_TO_EDGE[pattern][1 + segment * 2 + 0]
+ end_edge = CELL_TO_EDGE[pattern][1 + segment * 2 + 1]
+ self.insert_segment(context, x, y, begin_edge, end_edge, level)
+
+ @cython.boundscheck(False)
+ @cython.wraparound(False)
+ @cython.cdivision(True)
+ cdef void insert_segment(self, TileContext *context,
+ int x, int y,
+ cnumpy.uint8_t begin_edge,
+ cnumpy.uint8_t end_edge,
+ cnumpy.float64_t level) nogil:
+ cdef:
+ int i, yx
+ point_t point
+ point_index_t begin, end
+ PolygonDescription *description
+ PolygonDescription *description_begin
+ PolygonDescription *description_end
+ map[point_index_t, PolygonDescription*].iterator it_begin
+ map[point_index_t, PolygonDescription*].iterator it_end
+
+ yx = self._dim_x * y + x
+ begin = self.create_point_index(yx, begin_edge)
+ end = self.create_point_index(yx, end_edge)
+
+ it_begin = context.polygons.find(begin)
+ it_end = context.polygons.find(end)
+ if it_begin == context.polygons.end() and it_end == context.polygons.end():
+ # insert a new polygon
+ description = new PolygonDescription()
+ description.begin = begin
+ description.end = end
+ self.compute_point(x, y, begin_edge, level, &point)
+ description.points.push_back(point)
+ self.compute_point(x, y, end_edge, level, &point)
+ description.points.push_back(point)
+ context.polygons[begin] = description
+ context.polygons[end] = description
+ elif it_begin == context.polygons.end():
+ # insert the beginning point to an existing polygon
+ self.compute_point(x, y, begin_edge, level, &point)
+ description = dereference(it_end).second
+ context.polygons.erase(it_end)
+ if end == description.begin:
+ # insert at start
+ description.points.push_front(point)
+ description.begin = begin
+ context.polygons[begin] = description
+ else:
+ # insert on tail
+ description.points.push_back(point)
+ description.end = begin
+ context.polygons[begin] = description
+ elif it_end == context.polygons.end():
+ # insert the ending point to an existing polygon
+ self.compute_point(x, y, end_edge, level, &point)
+ description = dereference(it_begin).second
+ context.polygons.erase(it_begin)
+ if begin == description.begin:
+ # insert at start
+ description.points.push_front(point)
+ description.begin = end
+ context.polygons[end] = description
+ else:
+ # insert on tail
+ description.points.push_back(point)
+ description.end = end
+ context.polygons[end] = description
+ else:
+ # merge 2 polygons using this segment
+ description_begin = dereference(it_begin).second
+ description_end = dereference(it_end).second
+ if description_begin == description_end:
+ # The segment closes a polygon
+ # FIXME: this intermediate assign is not needed
+ point = description_begin.points.front()
+ description_begin.points.push_back(point)
+ context.polygons.erase(begin)
+ context.polygons.erase(end)
+ context.final_polygons.push_back(description_begin)
+ else:
+ if ((begin == description_begin.begin or end == description_begin.begin) and
+ (begin == description_end.end or end == description_end.end)):
+ # worst case, let's make it faster
+ description = description_end
+ description_end = description_begin
+ description_begin = description
+
+ # FIXME: We can recycle a description instead of creating a new one
+ description = new PolygonDescription()
+
+ # Make sure the last element of the list is the one to connect
+ if description_begin.begin == begin or description_begin.begin == end:
+ # O(n)
+ description_begin.points.reverse()
+ description.begin = description_begin.end
+ else:
+ description.begin = description_begin.begin
+
+ # O(1)
+ description.points.splice(description.points.end(), description_begin.points)
+
+ # Make sure the first element of the list is the one to connect
+ if description_end.end == begin or description_end.end == end:
+ description_end.points.reverse()
+ description.end = description_end.begin
+ else:
+ description.end = description_end.end
+
+ description.points.splice(description.points.end(), description_end.points)
+
+ context.polygons.erase(it_begin)
+ context.polygons.erase(it_end)
+ context.polygons[description.begin] = description
+ context.polygons[description.end] = description
+
+ @cython.boundscheck(False)
+ @cython.wraparound(False)
+ @cython.cdivision(True)
+ cdef void merge_context(self, TileContext *context, TileContext *other) nogil:
+ cdef:
+ map[point_index_t, PolygonDescription*].iterator it_begin
+ map[point_index_t, PolygonDescription*].iterator it_end
+ map[point_index_t, PolygonDescription*].iterator it
+ PolygonDescription *description_other
+ PolygonDescription *description
+ PolygonDescription *description2
+ point_index_t point_index
+ vector[PolygonDescription*] mergeable_polygons
+ size_t i
+
+ # merge final polygons
+ context.final_polygons.splice(context.final_polygons.end(), other.final_polygons)
+
+ # mergeable_polygons.reserve(other.polygons.size() / 2)
+ it = other.polygons.begin()
+ while it != other.polygons.end():
+ point_index = dereference(it).first
+ description_other = dereference(it).second
+ if description_other.begin == point_index:
+ mergeable_polygons.push_back(description_other)
+ preincrement(it)
+
+ for i in range(mergeable_polygons.size()):
+ description_other = mergeable_polygons[i]
+ it_begin = context.polygons.find(description_other.begin)
+ it_end = context.polygons.find(description_other.end)
+
+ if it_begin == context.polygons.end() and it_end == context.polygons.end():
+ # It's a new polygon
+ context.polygons[description_other.begin] = description_other
+ context.polygons[description_other.end] = description_other
+ elif it_end == context.polygons.end():
+ # The head of the polygon have to be merged
+ description = dereference(it_begin).second
+ context.polygons.erase(description.begin)
+ context.polygons.erase(description.end)
+ if description.begin == description_other.begin:
+ description.begin = description.end
+ description.points.reverse()
+ description.end = description_other.end
+ # remove the dup element
+ description_other.points.pop_front()
+ description.points.splice(description.points.end(), description_other.points)
+ context.polygons[description.begin] = description
+ context.polygons[description.end] = description
+ del description_other
+ elif it_begin == context.polygons.end():
+ # The tail of the polygon have to be merged
+ description = dereference(it_end).second
+ context.polygons.erase(description.begin)
+ context.polygons.erase(description.end)
+ if description.begin == description_other.end:
+ description.begin = description.end
+ description.points.reverse()
+ description.end = description_other.begin
+ description_other.points.reverse()
+ # remove the dup element
+ description_other.points.pop_front()
+ description.points.splice(description.points.end(), description_other.points)
+ context.polygons[description.begin] = description
+ context.polygons[description.end] = description
+ del description_other
+ else:
+ # Both sides have to be merged
+ description = dereference(it_begin).second
+ description2 = dereference(it_end).second
+ if description == description2:
+ # It became a closed polygon
+ context.polygons.erase(description.begin)
+ context.polygons.erase(description.end)
+ if description.begin == description_other.begin:
+ description.begin = description.end
+ description.points.reverse()
+ description.end = description_other.end
+ # remove the dup element
+ description_other.points.pop_front()
+ description.points.splice(description.points.end(), description_other.points)
+ context.final_polygons.push_back(description)
+ del description_other
+ else:
+ context.polygons.erase(description.begin)
+ context.polygons.erase(description.end)
+ context.polygons.erase(description2.begin)
+ context.polygons.erase(description2.end)
+ if description.begin == description_other.begin:
+ description.begin = description.end
+ description.points.reverse()
+ if description2.end == description_other.end:
+ description.end = description2.begin
+ description2.points.reverse()
+ else:
+ description.end = description2.end
+ description_other.points.pop_front()
+ description2.points.pop_front()
+ description.points.splice(description.points.end(), description_other.points)
+ description.points.splice(description.points.end(), description2.points)
+ context.polygons[description.begin] = description
+ context.polygons[description.end] = description
+ del description_other
+ del description2
+
+ @cython.boundscheck(False)
+ @cython.wraparound(False)
+ @cython.cdivision(True)
+ cdef extract_polygons(self):
+ cdef:
+ size_t i
+ int i_pixel
+ cnumpy.uint8_t index
+ map[point_index_t, PolygonDescription*].iterator it
+ vector[PolygonDescription*] descriptions
+ clist[point_t].iterator it_points
+ PolygonDescription *description
+ cnumpy.float32_t[:, ::1] polygon
+
+ if self._final_context == NULL:
+ return []
+
+ # move all the polygons in a final structure
+ with nogil:
+ it = self._final_context.polygons.begin()
+ while it != self._final_context.polygons.end():
+ description = dereference(it).second
+ if dereference(it).first == description.begin:
+ # polygones are stored 2 times
+ # only use one
+ descriptions.push_back(description)
+ preincrement(it)
+ self._final_context.polygons.clear()
+
+ descriptions.insert(descriptions.end(),
+ self._final_context.final_polygons.begin(),
+ self._final_context.final_polygons.end())
+ self._final_context.final_polygons.clear()
+
+ del self._final_context
+ self._final_context = NULL
+
+ # create result and clean up allocated memory
+ polygons = []
+ for i in range(descriptions.size()):
+ description = descriptions[i]
+ polygon = numpy.empty((description.points.size(), 2), dtype=numpy.float32)
+ it_points = description.points.begin()
+ i_pixel = 0
+ while it_points != description.points.end():
+ polygon[i_pixel, 0] = dereference(it_points).y
+ polygon[i_pixel, 1] = dereference(it_points).x
+ i_pixel += 1
+ preincrement(it_points)
+ polygons.append(numpy.asarray(polygon))
+ del description
+
+ return polygons
+
+
+cdef class _MarchingSquaresPixels(_MarchingSquaresAlgorithm):
+ """Implementation of the marching squares algorithm to find pixels of the
+ image containing points of the polygons of the iso contours.
+ """
+
+ @cython.boundscheck(False)
+ @cython.wraparound(False)
+ @cython.cdivision(True)
+ cdef void insert_pattern(self,
+ TileContext *context,
+ int x,
+ int y,
+ int pattern,
+ cnumpy.float64_t level) nogil:
+ cdef:
+ int segment
+ for segment in range(CELL_TO_EDGE[pattern][0]):
+ begin_edge = CELL_TO_EDGE[pattern][1 + segment * 2 + 0]
+ end_edge = CELL_TO_EDGE[pattern][1 + segment * 2 + 1]
+ self.insert_segment(context, x, y, begin_edge, end_edge, level)
+
+ @cython.boundscheck(False)
+ @cython.wraparound(False)
+ @cython.cdivision(True)
+ cdef void insert_segment(self, TileContext *context,
+ int x, int y,
+ cnumpy.uint8_t begin_edge,
+ cnumpy.uint8_t end_edge,
+ cnumpy.float64_t level) nogil:
+ cdef:
+ coord_t coord
+ self.compute_ipoint(x, y, begin_edge, level, &coord)
+ context.pixels.insert(coord)
+ self.compute_ipoint(x, y, end_edge, level, &coord)
+ context.pixels.insert(coord)
+
+ @cython.boundscheck(False)
+ @cython.wraparound(False)
+ @cython.cdivision(True)
+ cdef void after_marching_squares(self, TileContext *context) nogil:
+ cdef:
+ coord_t coord
+ cset[coord_t].iterator it_coord
+ cset[coord_t].iterator it_coord_erase
+ pass
+
+ it_coord = context.pixels.begin()
+ while it_coord != context.pixels.end():
+ coord = dereference(it_coord)
+ if (coord.x > context.pos_x and coord.x < context.pos_x + context.dim_x - 1 and
+ coord.y > context.pos_y and coord.y < context.pos_y + context.dim_y - 1):
+ it_coord_erase = it_coord
+ preincrement(it_coord)
+ context.pixels.erase(it_coord_erase)
+ context.final_pixels.push_back(coord)
+ else:
+ preincrement(it_coord)
+
+ @cython.boundscheck(False)
+ @cython.wraparound(False)
+ @cython.cdivision(True)
+ cdef void merge_context(self, TileContext *context, TileContext *other) nogil:
+ cdef:
+ cset[coord_t].iterator it_coord
+
+ # merge final pixels
+ context.final_pixels.splice(context.final_pixels.end(), other.final_pixels)
+
+ # merge final pixels
+ # NOTE: This is not declared in Cython
+ # context.final_pixels.insert(other.final_pixels.begin(), other.final_pixels.end())
+ it_coord = other.pixels.begin()
+ while it_coord != other.pixels.end():
+ context.pixels.insert(dereference(it_coord))
+ preincrement(it_coord)
+
+ @cython.boundscheck(False)
+ @cython.wraparound(False)
+ @cython.cdivision(True)
+ cdef extract_pixels(self):
+ cdef:
+ int i, x, y
+ point_index_t index
+ cset[coord_t].iterator it
+ clist[coord_t].iterator it_coord
+ coord_t coord
+ cnumpy.int32_t[:, ::1] pixels
+
+ if self._final_context == NULL:
+ return numpy.empty((0, 2), dtype=numpy.int32)
+
+ # create result
+ it = self._final_context.pixels.begin()
+ while it != self._final_context.pixels.end():
+ coord = dereference(it)
+ self._final_context.final_pixels.push_back(coord)
+ preincrement(it)
+
+ pixels = numpy.empty((self._final_context.final_pixels.size(), 2), dtype=numpy.int32)
+ i = 0
+
+ it_coord = self._final_context.final_pixels.begin()
+ while it_coord != self._final_context.final_pixels.end():
+ coord = dereference(it_coord)
+ pixels[i, 0] = coord.y
+ pixels[i, 1] = coord.x
+ i += 1
+ preincrement(it_coord)
+
+ del self._final_context
+ self._final_context = NULL
+
+ return numpy.asarray(pixels)
+
+
+cdef class MarchingSquaresMergeImpl(object):
+ """
+ Marching squares implementation based on a merge of segements and polygons.
+
+ The main logic is based on the common marching squares algorithms.
+ Segments of the iso-valued contours are identified using a pattern based
+ on blocks of 2*2 pixels. The image is read sequencially and when a segment
+ is identified it is inserted at a right place is a set of valid polygons.
+ This process can grow up polygons on bounds, or merge polygons together.
+
+ The algorithm can take care of a mask. If a pixel is invalidated by a
+ non-zero value of the mask at it's location, the computation of the pattern
+ cancelled and no segements are generated.
+
+ This implementation based on merge allow to use divide and conquer
+ implementation in multi process using OpenMP.The image is subdivised into
+ many tiles, each one is processed independantly. The result is finally
+ reduced by consecutives polygon merges.
+
+ The OpenMP group size can also by used to skip part of the image using
+ pre-computed informations. `use_minmax_cache` can enable the computation of
+ minimum and maximum pixel levels available on each tile groups. It was
+ designed to improve the efficiency of the extraction of many contour levels
+ from the same gradient image.
+
+ Finally the implementation provides an implementation to reach polygons
+ (:meth:`find_contours`) or pixels (:meth:`find_pixels`) from the iso-valued
+ data.
+
+ .. code-block:: python
+
+ # Example using a mask
+ shape = 100, 100
+ image = numpy.random.random(shape)
+ mask = numpy.random.random(shape) < 0.01
+ ms = MarchingSquaresMergeImpl(image, mask)
+ polygons = ms.find_contours(level=0.5)
+ for polygon in polygons:
+ print(polygon)
+
+ .. code-block:: python
+
+ # Example using multi requests
+ shape = 1000, 1000
+ image = numpy.random.random(shape)
+ ms = MarchingSquaresMergeImpl(image)
+ levels = numpy.arange(0, 1, 0.05)
+ for level in levels:
+ polygons = ms.find_contours(level=level)
+
+ .. code-block:: python
+
+ # Efficient cache using multi requests
+ shape = 1000, 1000
+ image = numpy.arange(shape[0] * shape[1]) / (shape[0] * shape[1])
+ image.shape = shape
+ ms = MarchingSquaresMergeImpl(image, use_minmax_cache=True)
+ levels = numpy.arange(0, 1, 0.05)
+ for level in levels:
+ polygons = ms.find_contours(level=level)
+
+ :param numpy.ndarray image: Image to process.
+ If the image is not a continuous array of native float 32bits, the data
+ will be first normalized. This can reduce efficiency.
+ :param numpy.ndarray mask: An optional mask (a non-zero value invalidate
+ the pixels of the image)
+ If the image is not a continuous array of signed integer 8bits, the
+ data will be first normalized. This can reduce efficiency.
+ :param int group_size: Specify the size of the tile to split the
+ computation with OpenMP. It is also used as tile size to compute the
+ min/max cache
+ :param bool use_minmax_cache: If true the min/max cache is enabled.
+ """
+
+ cdef cnumpy.float32_t[:, ::1] _image
+ cdef cnumpy.int8_t[:, ::1] _mask
+
+ cdef cnumpy.float32_t *_image_ptr
+ cdef cnumpy.int8_t *_mask_ptr
+ cdef int _dim_x
+ cdef int _dim_y
+ cdef int _group_size
+ cdef bool _use_minmax_cache
+
+ cdef cnumpy.float32_t *_min_cache
+ cdef cnumpy.float32_t *_max_cache
+
+ cdef _MarchingSquaresContours _contours_algo
+ cdef _MarchingSquaresPixels _pixels_algo
+
+ def __init__(self,
+ image, mask=None,
+ group_size=256,
+ use_minmax_cache=False):
+ if not isinstance(image, numpy.ndarray) or len(image.shape) != 2:
+ raise ValueError("Only 2D arrays are supported.")
+ if image.shape[0] < 2 or image.shape[1] < 2:
+ raise ValueError("Input array must be at least 2x2.")
+ # Force contiguous native array
+ self._image = numpy.ascontiguousarray(image, dtype='=f4')
+ self._image_ptr = &self._image[0][0]
+ if mask is not None:
+ if not isinstance(mask, numpy.ndarray):
+ raise ValueError("Only 2D arrays are supported.")
+ if image.shape != mask.shape:
+ raise ValueError("Mask size and image size must be the same.")
+ # Force contiguous native array
+ self._mask = numpy.ascontiguousarray(mask, dtype='=i1')
+ self._mask_ptr = &self._mask[0][0]
+ else:
+ self._mask = None
+ self._mask_ptr = NULL
+ self._group_size = group_size
+ self._use_minmax_cache = use_minmax_cache
+ self._min_cache = NULL
+ self._max_cache = NULL
+ with nogil:
+ self._dim_y = self._image.shape[0]
+ self._dim_x = self._image.shape[1]
+ self._contours_algo = None
+ self._pixels_algo = None
+
+ def __dealloc__(self):
+ if self._min_cache != NULL:
+ libc.stdlib.free(self._min_cache)
+ if self._max_cache != NULL:
+ libc.stdlib.free(self._max_cache)
+
+ @cython.boundscheck(False)
+ @cython.wraparound(False)
+ @cython.cdivision(True)
+ cdef void _compute_minmax_on_block(self, int block_x, int block_y, int block_index) nogil:
+ """
+ Initialize the minmax cache.
+
+ The cache is computed for each tiles of the image. It reuses the OpenMP
+ group size for the size of the tile, which allow to skip a full OpenMP
+ context in case the requested level do not match the cache.
+
+ The minmax is compuded with an overlap of 1 pixel, in order to match
+ the marching squares algorithm.
+
+ The mask is taking into accound. As result if a tile is fully masked,
+ the minmax cache result for this tile will have infinit values.
+
+ :param block_x: X location of tile in block unit
+ :param block_y: Y location of tile in block unit
+ :param block_index: Index of the tile in the minmax cache structure
+ """
+ cdef:
+ int x, y
+ int pos_x, end_x, pos_y, end_y
+ cnumpy.float32_t minimum, maximum, value
+ cnumpy.float32_t *image_ptr
+ cnumpy.int8_t *mask_ptr
+
+ pos_x = block_x * self._group_size
+ end_x = pos_x + self._group_size + 1
+ if end_x > self._dim_x:
+ end_x = self._dim_x
+ pos_y = block_y * self._group_size
+ end_y = pos_y + self._group_size + 1
+ if end_y > self._dim_y:
+ end_y = self._dim_y
+
+ image_ptr = self._image_ptr + (pos_y * self._dim_x + pos_x)
+ if self._mask_ptr != NULL:
+ mask_ptr = self._mask_ptr + (pos_y * self._dim_x + pos_x)
+ else:
+ mask_ptr = NULL
+ minimum = INFINITY
+ maximum = -INFINITY
+
+ for y in range(pos_y, end_y):
+ for x in range(pos_x, end_x):
+ if mask_ptr != NULL:
+ if mask_ptr[0] != 0:
+ image_ptr += 1
+ mask_ptr += 1
+ continue
+ value = image_ptr[0]
+ if value < minimum:
+ minimum = value
+ if value > maximum:
+ maximum = value
+ image_ptr += 1
+ if mask_ptr != NULL:
+ mask_ptr += 1
+ image_ptr += self._dim_x + pos_x - end_x
+ if mask_ptr != NULL:
+ mask_ptr += self._dim_x + pos_x - end_x
+
+ self._min_cache[block_index] = minimum
+ self._max_cache[block_index] = maximum
+
+ @cython.boundscheck(False)
+ @cython.wraparound(False)
+ @cython.cdivision(True)
+ cdef void _create_minmax_cache(self) nogil:
+ """
+ Create and initialize minmax cache.
+ """
+ cdef:
+ int icontext, context_x, context_y
+ int context_dim_x, context_dim_y, context_size
+
+ context_dim_x = self._dim_x // self._group_size + (self._dim_x % self._group_size > 0)
+ context_dim_y = self._dim_y // self._group_size + (self._dim_y % self._group_size > 0)
+ context_size = context_dim_x * context_dim_y
+
+ self._min_cache = <cnumpy.float32_t *>libc.stdlib.malloc(context_size * sizeof(cnumpy.float32_t))
+ self._max_cache = <cnumpy.float32_t *>libc.stdlib.malloc(context_size * sizeof(cnumpy.float32_t))
+
+ for icontext in prange(context_size, nogil=True):
+ context_x = icontext % context_dim_x
+ context_y = icontext // context_dim_x
+ self._compute_minmax_on_block(context_x, context_y, icontext)
+
+ @cython.boundscheck(False)
+ @cython.wraparound(False)
+ @cython.cdivision(True)
+ def find_pixels(self, level):
+ """
+ Compute the pixels from the image over the requested iso contours
+ at this `level`. Pixels are those over the bound of the segments.
+
+ :param float level: Level of the requested iso contours.
+ :returns: An array of y-x coordinates.
+ :rtype: numpy.ndarray
+ """
+ if self._use_minmax_cache and self._min_cache == NULL:
+ self._create_minmax_cache()
+
+ if self._pixels_algo is None:
+ algo = _MarchingSquaresPixels()
+ algo._image_ptr = self._image_ptr
+ algo._mask_ptr = self._mask_ptr
+ algo._dim_x = self._dim_x
+ algo._dim_y = self._dim_y
+ algo._group_size = self._group_size
+ algo._use_minmax_cache = self._use_minmax_cache
+ algo._force_sequencial_reduction = COMPILED_WITH_OPENMP == 0
+ if self._use_minmax_cache:
+ algo._min_cache = self._min_cache
+ algo._max_cache = self._max_cache
+ self._pixels_algo = algo
+ else:
+ algo = self._pixels_algo
+
+ algo.marching_squares(level)
+ pixels = algo.extract_pixels()
+ return pixels
+
+ @cython.boundscheck(False)
+ @cython.wraparound(False)
+ @cython.cdivision(True)
+ def find_contours(self, level=None):
+ """
+ Compute the list of polygons of the iso contours at this `level`.
+
+ :param float level: Level of the requested iso contours.
+ :returns: A list of array containg y-x coordinates of points
+ :rtype: List[numpy.ndarray]
+ """
+ if self._use_minmax_cache and self._min_cache == NULL:
+ self._create_minmax_cache()
+
+ if self._contours_algo is None:
+ algo = _MarchingSquaresContours()
+ algo._image_ptr = self._image_ptr
+ algo._mask_ptr = self._mask_ptr
+ algo._dim_x = self._dim_x
+ algo._dim_y = self._dim_y
+ algo._group_size = self._group_size
+ algo._use_minmax_cache = self._use_minmax_cache
+ algo._force_sequencial_reduction = COMPILED_WITH_OPENMP == 0
+ if self._use_minmax_cache:
+ algo._min_cache = self._min_cache
+ algo._max_cache = self._max_cache
+ self._contours_algo = algo
+ else:
+ algo = self._contours_algo
+
+ algo.marching_squares(level)
+ polygons = algo.extract_polygons()
+ return polygons