summaryrefslogtreecommitdiff
path: root/silx/image/marchingsquares/_mergeimpl.pyx
diff options
context:
space:
mode:
Diffstat (limited to 'silx/image/marchingsquares/_mergeimpl.pyx')
-rw-r--r--silx/image/marchingsquares/_mergeimpl.pyx1319
1 files changed, 0 insertions, 1319 deletions
diff --git a/silx/image/marchingsquares/_mergeimpl.pyx b/silx/image/marchingsquares/_mergeimpl.pyx
deleted file mode 100644
index 5a7a3b5..0000000
--- a/silx/image/marchingsquares/_mergeimpl.pyx
+++ /dev/null
@@ -1,1319 +0,0 @@
-# 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