#cython: cdivision=True #cython: boundscheck=False #cython: nonecheck=False #cython: wraparound=False import numpy as np cdef inline double _get_fraction(double from_value, double to_value, double level): if (to_value == from_value): return 0 return ((level - from_value) / (to_value - from_value)) def iterate_and_store(double[:, :] array, double level, Py_ssize_t vertex_connect_high): """Iterate across the given array in a marching-squares fashion, looking for segments that cross 'level'. If such a segment is found, its coordinates are added to a growing list of segments, which is returned by the function. if vertex_connect_high is nonzero, high-values pixels are considered to be face+vertex connected into objects; otherwise low-valued pixels are. """ if array.shape[0] < 2 or array.shape[1] < 2: raise ValueError("Input array must be at least 2x2.") cdef list arc_list = [] cdef Py_ssize_t n # The plan is to iterate a 2x2 square across the input array. This means # that the upper-left corner of the square needs to iterate across a # sub-array that's one-less-large in each direction (so that the square # never steps out of bounds). The square is represented by four pointers: # ul, ur, ll, and lr (for 'upper left', etc.). We also maintain the current # 2D coordinates for the position of the upper-left pointer. Note that we # ensured that the array is of type 'double' and is C-contiguous (last # index varies the fastest). # Current coords start at 0,0. cdef Py_ssize_t[2] coords coords[0] = 0 coords[1] = 0 # Calculate the number of iterations we'll need cdef Py_ssize_t num_square_steps = (array.shape[0] - 1) \ * (array.shape[1] - 1) cdef unsigned char square_case = 0 cdef tuple top, bottom, left, right cdef double ul, ur, ll, lr cdef Py_ssize_t r0, r1, c0, c1 for n in range(num_square_steps): # There are sixteen different possible square types, diagramed below. # A + indicates that the vertex is above the contour value, and a - # indicates that the vertex is below or equal to the contour value. # The vertices of each square are: # ul ur # ll lr # and can be treated as a binary value with the bits in that order. Thus # each square case can be numbered: # 0-- 1+- 2-+ 3++ 4-- 5+- 6-+ 7++ # -- -- -- -- +- +- +- +- # # 8-- 9+- 10-+ 11++ 12-- 13+- 14-+ 15++ # -+ -+ -+ -+ ++ ++ ++ ++ # # The position of the line segment that cuts through (or # doesn't, in case 0 and 15) each square is clear, except in # cases 6 and 9. In this case, where the segments are placed # is determined by vertex_connect_high. If # vertex_connect_high is false, then lines like \\ are drawn # through square 6, and lines like // are drawn through square # 9. Otherwise, the situation is reversed. # Finally, recall that we draw the lines so that (moving from tail to # head) the lower-valued pixels are on the left of the line. So, for # example, case 1 entails a line slanting from the middle of the top of # the square to the middle of the left side of the square. r0, c0 = coords[0], coords[1] r1, c1 = r0 + 1, c0 + 1 ul = array[r0, c0] ur = array[r0, c1] ll = array[r1, c0] lr = array[r1, c1] # now in advance the coords indices if coords[1] < array.shape[1] - 2: coords[1] += 1 else: coords[0] += 1 coords[1] = 0 square_case = 0 if (ul > level): square_case += 1 if (ur > level): square_case += 2 if (ll > level): square_case += 4 if (lr > level): square_case += 8 if (square_case != 0 and square_case != 15): # only do anything if there's a line passing through the # square. Cases 0 and 15 are entirely below/above the contour. top = r0, c0 + _get_fraction(ul, ur, level) bottom = r1, c0 + _get_fraction(ll, lr, level) left = r0 + _get_fraction(ul, ll, level), c0 right = r0 + _get_fraction(ur, lr, level), c1 if (square_case == 1): # top to left arc_list.append(top) arc_list.append(left) elif (square_case == 2): # right to top arc_list.append(right) arc_list.append(top) elif (square_case == 3): # right to left arc_list.append(right) arc_list.append(left) elif (square_case == 4): # left to bottom arc_list.append(left) arc_list.append(bottom) elif (square_case == 5): # top to bottom arc_list.append(top) arc_list.append(bottom) elif (square_case == 6): if vertex_connect_high: arc_list.append(left) arc_list.append(top) arc_list.append(right) arc_list.append(bottom) else: arc_list.append(right) arc_list.append(top) arc_list.append(left) arc_list.append(bottom) elif (square_case == 7): # right to bottom arc_list.append(right) arc_list.append(bottom) elif (square_case == 8): # bottom to right arc_list.append(bottom) arc_list.append(right) elif (square_case == 9): if vertex_connect_high: arc_list.append(top) arc_list.append(right) arc_list.append(bottom) arc_list.append(left) else: arc_list.append(top) arc_list.append(left) arc_list.append(bottom) arc_list.append(right) elif (square_case == 10): # bottom to top arc_list.append(bottom) arc_list.append(top) elif (square_case == 11): # bottom to left arc_list.append(bottom) arc_list.append(left) elif (square_case == 12): # lef to right arc_list.append(left) arc_list.append(right) elif (square_case == 13): # top to right arc_list.append(top) arc_list.append(right) elif (square_case == 14): # left to top arc_list.append(left) arc_list.append(top) return arc_list