summaryrefslogtreecommitdiff
path: root/silx/image/shapes.pyx
blob: 4fb2e338515f8672fa9762f6b810515039526797 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2015-2017 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.
#
# ############################################################################*/
"""This module provides functions making masks on an image.

- :func:`circle_fill` function generates coordinates of a circle in an image.
- :func:`draw_line` function generates coordinates of a line in an image.
- :func:`polygon_fill_mask` function generates a mask from a set of points
  defining a polygon.

The :class:`Polygon` class provides checking if a point is inside a polygon.

The whole module uses the (row, col) (i.e., (y, x))) convention
for 2D coordinates.
"""

__authors__ = ["Jérôme Kieffer", "T. Vincent"]
__license__ = "MIT"
__date__ = "15/02/2019"
__status__ = "dev"


cimport cython
import numpy
from libc.math cimport ceil, fabs


cdef class Polygon(object):
    """Define a polygon that provides inside check and mask generation.

    :param vertices: corners of the polygon
    :type vertices: Nx2 array of floats of (row, col)
    """

    cdef float[:,:] vertices
    cdef int nvert

    def __init__(self, vertices):
        self.vertices = numpy.ascontiguousarray(vertices, dtype=numpy.float32)
        self.nvert = self.vertices.shape[0]

    def is_inside(self, row, col):
        """Check if (row, col) is inside or outside the polygon

        :param float row:
        :param float col:
        :return: True if position is inside polygon, False otherwise
        """
        return self.c_is_inside(row, col)

    @cython.cdivision(True)
    @cython.wraparound(False)
    @cython.boundscheck(False)
    cdef bint c_is_inside(self, float row, float col) nogil:
        """Check if (row, col) is inside or outside the polygon

        Pure C_Cython class implementation.

        :param float row:
        :param float col:
        :return: True if position is inside polygon, False otherwise
        """
        cdef int index, is_inside
        cdef float pt1x, pt1y, pt2x, pt2y, xinters
        is_inside = 0

        pt1x = self.vertices[self.nvert-1, 1]
        pt1y = self.vertices[self.nvert-1, 0]
        for index in range(self.nvert):
            pt2x = self.vertices[index, 1]
            pt2y = self.vertices[index, 0]

            if (((pt1y <= row and row < pt2y) or
                    (pt2y <= row and row < pt1y)) and
                    # Extra (optional) condition to avoid some computation
                    (col <= pt1x or col <= pt2x)):
                xinters = (row - pt1y) * (pt2x - pt1x) / (pt2y - pt1y) + pt1x
                is_inside ^= col < xinters
            pt1x, pt1y = pt2x, pt2y
        return is_inside

    @cython.cdivision(True)
    @cython.wraparound(False)
    @cython.boundscheck(False)
    def make_mask(self, int height, int width):
        """Create a mask array representing the filled polygon

        :param int height: Height of the mask array
        :param int width: Width of the mask array
        :return: 2D array (height, width)
        """
        cdef unsigned char[:, :] mask = numpy.zeros((height, width),
                                                    dtype=numpy.uint8)
        cdef int row_min, row_max, col_min, col_max  # mask subpart to update
        cdef int row, col, index  # Loop indixes
        cdef float pt1x, pt1y, pt2x, pt2y  # segment end points
        cdef int xinters, is_inside, current

        row_min = max(int(min(self.vertices[:, 0])), 0)
        row_max = min(int(max(self.vertices[:, 0])) + 1, height)

        # Can be replaced by prange(row_min, row_max, nogil=True)
        with nogil:
            for row in range(row_min, row_max):
                # For each line of the image, mark intersection of all segments
                # in the line and then run a xor scan to fill inner parts
                # Adapted from http://alienryderflex.com/polygon_fill/
                pt1x = self.vertices[self.nvert-1, 1]
                pt1y = self.vertices[self.nvert-1, 0]
                col_min = width - 1
                col_max = 0
                is_inside = 0  # Init with whether first col is inside or not

                for index in range(self.nvert):
                    pt2x = self.vertices[index, 1]
                    pt2y = self.vertices[index, 0]

                    if ((pt1y <= row and row < pt2y) or
                            (pt2y <= row and row < pt1y)):
                        # Intersection casted to int so that ]x, x+1] => x
                        xinters = (<int>ceil(pt1x + (row - pt1y) *
                                   (pt2x - pt1x) / (pt2y - pt1y))) - 1

                        # Update column range to patch
                        if xinters < col_min:
                            col_min = xinters
                        if xinters > col_max:
                            col_max = xinters

                        if xinters < 0:
                            # Add an intersection to init value of xor scan
                            is_inside ^= 1
                        elif xinters < width:
                            # Mark intersection in mask
                            mask[row, xinters] ^= 1
                        # else: do not consider intersection on the right

                    pt1x, pt1y = pt2x, pt2y

                if col_min < col_max:
                    # Clip column range to mask
                    if col_min < 0:
                        col_min = 0
                    if col_max > width - 1:
                        col_max = width - 1

                    # xor exclusive scan
                    for col in range(col_min, col_max + 1):
                        current = mask[row, col]
                        mask[row, col] = is_inside
                        is_inside = current ^ is_inside

        # Ensures the result is exported as numpy array and not memory view.
        return numpy.asarray(mask)


def polygon_fill_mask(vertices, shape):
    """Return a mask of boolean, True for pixels inside a polygon.

    :param vertices: Strip of segments end points (row, column) or (y, x)
    :type vertices: numpy.ndarray like container of dimension Nx2
    :param shape: size of the mask as (height, width)
    :type shape: 2-tuple of int
    :return: Mask corresponding to the polygon
    :rtype: numpy.ndarray of dimension shape
    """
    return Polygon(vertices).make_mask(shape[0], shape[1])


@cython.wraparound(False)
@cython.boundscheck(False)
def draw_line(int row0, int col0, int row1, int col1, int width=1):
    """Line includes both end points.
    Width is handled by drawing parallel lines, so junctions of lines belonging
    to different octant with width > 1 will not look nice.

    Using Bresenham line algorithm:
    Bresenham, J. E.
    Algorithm for computer control of a digital plotter.
    IBM Systems Journal. Vol 4 No 1. 1965. pp 25-30

    :param int row0: Start point row
    :param int col0: Start point col
    :param int row1: End point row
    :param int col1: End point col
    :param int width: Thickness of the line in pixels (default 1)
                      Width must be at least 1.
    :return: Array coordinates of points inside the line (might be negative)
    :rtype: 2-tuple of numpy.ndarray (rows, cols)
    """
    cdef int drow, dcol, invert_coords
    cdef int db, da, delta, b, a, step_a, step_b
    cdef int index, offset  # Loop indices

    # Store coordinates of points of the line
    cdef int[:, :] b_coords
    cdef int[:, :] a_coords

    dcol = abs(col1 - col0)
    drow = abs(row1 - row0)
    invert_coords = dcol < drow

    if dcol == 0 and drow == 0:
        return (numpy.array((row0,), dtype=numpy.int32),
                numpy.array((col0,), dtype=numpy.int32))

    if width < 1:
        width = 1

    # Set a and b according to segment octant
    if not invert_coords:
        da = dcol
        db = drow
        step_a = 1 if col1 > col0 else -1
        step_b = 1 if row1 > row0 else -1
        a = col0
        b = row0

    else:
        da = drow
        db = dcol
        step_a = 1 if row1 > row0 else -1
        step_b = 1 if col1 > col0 else -1
        a = row0
        b = col0

    b_coords = numpy.empty((da + 1, width), dtype=numpy.int32)
    a_coords = numpy.empty((da + 1, width), dtype=numpy.int32)

    with nogil:
        b -= (width - 1) // 2
        delta = 2 * db - da
        for index in range(da + 1):
            for offset in range(width):
                b_coords[index, offset] = b + offset
                a_coords[index, offset] = a

            if delta >= 0:  # M2: Move by step_a + step_b
                b += step_b
                delta -= 2 * da
            # else M1: Move by step_a

            a += step_a
            delta += 2 * db

    if not invert_coords:
        return (numpy.asarray(b_coords).reshape(-1),
                numpy.asarray(a_coords).reshape(-1))
    else:
        return (numpy.asarray(a_coords).reshape(-1),
                numpy.asarray(b_coords).reshape(-1))


def circle_fill(int crow, int ccol, float radius):
    """Generates coordinate of image points lying in a disk.

    :param int crow: Row of the center of the disk
    :param int ccol: Column of the center of the disk
    :param float radius: Radius of the disk
    :return: Array coordinates of points inside the disk (might be negative)
    :rtype: 2-tuple of numpy.ndarray (rows, cols)
    """
    cdef int i_radius, len_coords

    radius = fabs(radius)
    i_radius = <int>radius

    coords = numpy.arange(-i_radius, ceil(radius) + 1,
                          dtype=numpy.float32) ** 2
    len_coords = len(coords)
    # rows, cols = where(row**2 + col**2 < radius**2)
    rows, cols = numpy.where(coords.reshape(1, len_coords) +
                             coords.reshape(len_coords, 1) < radius ** 2)
    return rows + crow - i_radius, cols + ccol - i_radius


def ellipse_fill(int crow, int ccol, float radius_r, float radius_c):
    """Generates coordinate of image points lying in a ellipse.

    :param int crow: Row of the center of the ellipse
    :param int ccol: Column of the center of the ellipse
    :param float radius_r: Radius of the ellipse in the row
    :param float radius_c: Radius of the ellipse in the column
    :return: Array coordinates of points inside the ellipse (might be negative)
    :rtype: 2-tuple of numpy.ndarray (rows, cols)
    """
    cdef int i_radius_r
    cdef int i_radius_c

    i_radius_r = <int>fabs(radius_r)
    i_radius_c = <int>fabs(radius_c)

    x_coords = numpy.arange(-i_radius_r, ceil(radius_r) + 1, dtype=numpy.float32).reshape(-1, 1)
    y_coords = numpy.arange(-i_radius_c, ceil(radius_c) + 1, dtype=numpy.float32).reshape(1, -1)

    # rows, cols = where(x**2 + col**2 < 1)
    rows, cols = numpy.where(x_coords**2 / radius_r**2 + y_coords**2 / radius_c**2 < 1.0)
    return rows + crow - i_radius_r, cols + ccol - i_radius_c