From 88d02f43ab2c1dac7eda5571fa6af0b433e63290 Mon Sep 17 00:00:00 2001 From: Anihilatorgunn Date: Mon, 4 Nov 2024 16:57:10 +0300 Subject: [PATCH 01/42] convex hull 2d --- docs/index.md | 2 + imops/__version__.py | 2 +- imops/morphology.py | 92 +++++++++++++++++++++++++++ imops/src/_convex_hull.pyx | 125 +++++++++++++++++++++++++++++++++++++ 4 files changed, 220 insertions(+), 1 deletion(-) create mode 100644 imops/src/_convex_hull.pyx diff --git a/docs/index.md b/docs/index.md index 90ca24f5..a5115d02 100644 --- a/docs/index.md +++ b/docs/index.md @@ -48,6 +48,8 @@ pip install imops[numba] # additionally install Numba backend ::: imops.morphology.distance_transform_edt +::: imops.morphology.convex_hull_image_2d + ::: imops.measure.label ::: imops.measure.center_of_mass diff --git a/imops/__version__.py b/imops/__version__.py index e4e49b3b..8969d496 100644 --- a/imops/__version__.py +++ b/imops/__version__.py @@ -1 +1 @@ -__version__ = '0.9.0' +__version__ = '0.9.1' diff --git a/imops/morphology.py b/imops/morphology.py index 357d1243..dfac366b 100644 --- a/imops/morphology.py +++ b/imops/morphology.py @@ -1,3 +1,4 @@ +from itertools import product from typing import Callable, Tuple, Union from warnings import warn @@ -5,12 +6,15 @@ from edt import edt from scipy.ndimage import distance_transform_edt as scipy_distance_transform_edt, generate_binary_structure from scipy.ndimage._nd_image import euclidean_feature_transform +from scipy.spatial import ConvexHull, QhullError from skimage.morphology import ( binary_closing as scipy_binary_closing, binary_dilation as scipy_binary_dilation, binary_erosion as scipy_binary_erosion, binary_opening as scipy_binary_opening, ) +from skimage.util import unique_rows +from skimage._shared.utils import warn as skimage_warn from .backend import BackendLike, Cython, Scipy, resolve_backend from .box import add_margin, box_to_shape, mask_to_box, shape_to_box @@ -22,6 +26,7 @@ _binary_erosion as cython_fast_binary_erosion, ) from .src._morphology import _binary_dilation as cython_binary_dilation, _binary_erosion as cython_binary_erosion +from .src._convex_hull import _grid_points_in_poly from .utils import morphology_composition_args, normalize_num_threads @@ -517,3 +522,90 @@ def distance_transform_edt( return result[0] return None + + +def convex_hull_image_2d(image, offset_coordinates=True): + """ + Fast convex hull of an image. Similar to skimage.morphology.convex_hull_image with include_borders=True + + Parameters + ---------- + image: np.ndarray + input image, must be 2D + offset_coordinates: bool + If True, a pixel at coordinate, e.g., (4, 7) will be represented by coordinates + (3.5, 7), (4.5, 7), (4, 6.5), and (4, 7.5). + This adds some “extent” to a pixel when computing the hull. + + Returns + ------- + output: np.ndarray + resulting convex hull of the input image + + Examples + -------- + ```python + chull = convex_hull_image_2d(x) + ``` + """ + + ndim = image.ndim + assert ndim == 2, f'Expected image to have ndim=2 got {ndim}' + if ndim != 2: + raise RuntimeError() + + if np.count_nonzero(image) == 0: + warn( + "Input image is entirely zero, no valid convex hull. " + "Returning empty image", + UserWarning, + ) + return np.zeros(image.shape, dtype=bool) + + # In 2D, we do an optimisation by choosing only pixels that are + # the starting or ending pixel of a row or column. This vastly + # limits the number of coordinates to examine for the virtual hull. + image = np.ascontiguousarray(image, dtype=np.uint8) + + im_any = np.any(image, axis=1) + x_indices = np.arange(0, image.shape[0])[im_any] + y_indices_left = np.argmax(image[im_any], axis=1) + y_indices_right = image.shape[1] - 1 - np.argmax(image[im_any][:,::-1], axis=1) + + left = np.stack((x_indices, y_indices_left), axis=-1) + right = np.stack((x_indices, y_indices_right), axis=-1) + + coords = np.vstack((left, right)) + if offset_coordinates: + offsets = _offsets_diamond(ndim) + coords = (coords[:, np.newaxis, :] + offsets).reshape(-1, ndim) + coords = unique_rows(coords) + + # Find the convex hull + try: + hull = ConvexHull(coords) + except QhullError as err: + skimage_warn( + f"Failed to get convex hull image. " + f"Returning empty image, see error message below:\n" + f"{err}" + ) + return np.zeros(image.shape, dtype=bool) + + vertices = hull.points[hull.vertices] + + #return vertices + + # If 2D, use fast Cython function to locate convex hull pixels + labels = _grid_points_in_poly( + np.ascontiguousarray(vertices[:,0], dtype=np.float32), + np.ascontiguousarray(vertices[:,1], dtype=np.float32), + image.shape[0], + image.shape[1], + len(vertices), + ) + + # edge points and vertices are included + mask = labels.view(bool) + + return mask diff --git a/imops/src/_convex_hull.pyx b/imops/src/_convex_hull.pyx new file mode 100644 index 00000000..0ccbf26d --- /dev/null +++ b/imops/src/_convex_hull.pyx @@ -0,0 +1,125 @@ +#cython: cdivision=True +#cython: boundscheck=False +#cython: nonecheck=False +#cython: wraparound=False +import numpy as np + +from libc.math cimport ceilf, floorf + +cimport numpy as cnp +cimport cython + +cnp.import_array() + +FP_BOUND_DTYPE = np.dtype([ + ('lb', np.float32), + ('rb', np.float32), + ('assigned', np.uint8) +]) + +INT_BOUND_DTYPE = np.dtype([ + ('lb', np.int32), + ('rb', np.int32), + ('assigned', np.uint8) +]) + + +def _grid_points_in_poly(float[:] vx, float[:] vy, Py_ssize_t M, Py_ssize_t N, Py_ssize_t nr_verts): + cdef intBound tmp_int_bound + cdef Py_ssize_t m, n, i + cdef float prev_x, prev_y, curr_x, curr_ys + cdef float tmp_from_x, tmp_from_y, tmp_to_x, tmp_to_y + cdef float lerp_t, bound_y + cdef Py_ssize_t x_set, x_start, x_stop + + cdef cnp.ndarray[dtype=cnp.uint8_t, ndim=2, mode="c"] out = \ + np.zeros((M, N), dtype=np.uint8) + + cdef fpBound[:] fpBounds = np.empty(M, dtype=FP_BOUND_DTYPE) + cdef intBound[:] intBounds = np.empty(M, dtype=INT_BOUND_DTYPE) + + for i in range(M): + fpBounds[i].assigned = False + intBounds[i].assigned = False + + prev_x = vx[nr_verts - 1] + prev_y = vy[nr_verts - 1] + + for i in range(nr_verts): + curr_x = vx[i] + curr_y = vy[i] + + if prev_x == curr_x: + x_set = prev_x + fpBounds[x_set] = set_bound(set_bound(fpBounds[x_set], prev_y), curr_y) + else: + if prev_x < curr_x: + tmp_from_x = prev_x + tmp_from_y = prev_y + tmp_to_x = curr_x + tmp_to_y = curr_y + else: + tmp_from_x = curr_x + tmp_from_y = curr_y + tmp_to_x = prev_x + tmp_to_y = prev_y + + x_start = ceilf(tmp_from_x) + x_stop = ceilf(tmp_to_x) + + for x_set in range(x_start, x_stop): + lerp_t = (x_set - tmp_from_x) / (tmp_to_x - tmp_from_x) + bound_y = lerp(tmp_from_y, tmp_to_y, lerp_t) + fpBounds[x_set] = set_bound(fpBounds[x_set], bound_y) + + prev_x = curr_x + prev_y = curr_y + + for m in range(M): + intBounds[m] = intify(fpBounds[m], 0, N) + + for m in range(M): + tmp_int_bound = intBounds[m] + + if tmp_int_bound.assigned: + for n in range(tmp_int_bound.lb, tmp_int_bound.rb): + out[m, n] = True + + + return out + + +cdef inline intBound intify(fpBound bound, Py_ssize_t min_idx, Py_ssize_t max_idx): + if bound.assigned: + return intBound(lb = max(min_idx, floorf(bound.lb)), rb = min(max_idx, ceilf(bound.rb)), assigned=True) + + return intBound(lb=0, rb=0, assigned=False) + + +cdef inline fpBound set_bound(fpBound bound, float new_bound): + cdef new_lb, new_rb + + if bound.assigned: + new_lb = min(bound.lb, new_bound) + new_rb = max(bound.rb, new_bound) + else: + new_lb = new_bound + new_rb = new_bound + + return fpBound(lb=new_lb, rb=new_rb, assigned=True) + + +cdef packed struct fpBound: + float lb + float rb + unsigned char assigned + + +cdef packed struct intBound: + int lb + int rb + unsigned char assigned + + +cdef inline float lerp(float y0, float y1, float t): + return y0 * (1 - t) + y1 * t From 653a40689444483c25203c64839ac9bd35e92e9b Mon Sep 17 00:00:00 2001 From: Anihilatorgunn Date: Sat, 16 Nov 2024 03:07:43 +0300 Subject: [PATCH 02/42] a bit faster, more precise --- imops/morphology.py | 11 ++---- imops/src/_convex_hull.pyx | 70 +++++++++++++++++++++++++++----------- 2 files changed, 52 insertions(+), 29 deletions(-) diff --git a/imops/morphology.py b/imops/morphology.py index dfac366b..a2e58f6a 100644 --- a/imops/morphology.py +++ b/imops/morphology.py @@ -26,7 +26,7 @@ _binary_erosion as cython_fast_binary_erosion, ) from .src._morphology import _binary_dilation as cython_binary_dilation, _binary_erosion as cython_binary_erosion -from .src._convex_hull import _grid_points_in_poly +from .src._convex_hull import _grid_points_in_poly, _left_right_bounds from .utils import morphology_composition_args, normalize_num_threads @@ -567,15 +567,8 @@ def convex_hull_image_2d(image, offset_coordinates=True): # limits the number of coordinates to examine for the virtual hull. image = np.ascontiguousarray(image, dtype=np.uint8) - im_any = np.any(image, axis=1) - x_indices = np.arange(0, image.shape[0])[im_any] - y_indices_left = np.argmax(image[im_any], axis=1) - y_indices_right = image.shape[1] - 1 - np.argmax(image[im_any][:,::-1], axis=1) + coords = _left_right_bounds(image) - left = np.stack((x_indices, y_indices_left), axis=-1) - right = np.stack((x_indices, y_indices_right), axis=-1) - - coords = np.vstack((left, right)) if offset_coordinates: offsets = _offsets_diamond(ndim) coords = (coords[:, np.newaxis, :] + offsets).reshape(-1, ndim) diff --git a/imops/src/_convex_hull.pyx b/imops/src/_convex_hull.pyx index 0ccbf26d..cabff97a 100644 --- a/imops/src/_convex_hull.pyx +++ b/imops/src/_convex_hull.pyx @@ -40,18 +40,24 @@ def _grid_points_in_poly(float[:] vx, float[:] vy, Py_ssize_t M, Py_ssize_t N, P for i in range(M): fpBounds[i].assigned = False + fpBounds[i].lb = float('inf') + fpBounds[i].rb = -1 intBounds[i].assigned = False prev_x = vx[nr_verts - 1] prev_y = vy[nr_verts - 1] + # algorithm relies on vertex validity and counterclockwise orientation of the vertices for i in range(nr_verts): curr_x = vx[i] curr_y = vy[i] if prev_x == curr_x: - x_set = prev_x - fpBounds[x_set] = set_bound(set_bound(fpBounds[x_set], prev_y), curr_y) + x_set = (floorf(prev_x) if prev_y < curr_y else ceilf(prev_x)) + + fpBounds[x_set].assigned = True + fpBounds[x_set].lb = min(fpBounds[x_set].lb, prev_y, curr_y) + fpBounds[x_set].rb = max(fpBounds[x_set].rb, prev_y, curr_y) else: if prev_x < curr_x: tmp_from_x = prev_x @@ -64,28 +70,34 @@ def _grid_points_in_poly(float[:] vx, float[:] vy, Py_ssize_t M, Py_ssize_t N, P tmp_to_x = prev_x tmp_to_y = prev_y + # vertices are treated as points on image, so include x_stop x_start = ceilf(tmp_from_x) - x_stop = ceilf(tmp_to_x) + x_stop = floorf(tmp_to_x + 1) for x_set in range(x_start, x_stop): lerp_t = (x_set - tmp_from_x) / (tmp_to_x - tmp_from_x) bound_y = lerp(tmp_from_y, tmp_to_y, lerp_t) - fpBounds[x_set] = set_bound(fpBounds[x_set], bound_y) + + fpBounds[x_set].assigned = True + fpBounds[x_set].lb = min(fpBounds[x_set].lb, bound_y) + fpBounds[x_set].rb = max(fpBounds[x_set].rb, bound_y) prev_x = curr_x prev_y = curr_y + # bounds are computed as point interpolation + # so bounds must be valid indices for out array for m in range(M): - intBounds[m] = intify(fpBounds[m], 0, N) + intBounds[m] = intify(fpBounds[m], 0, N - 1) for m in range(M): tmp_int_bound = intBounds[m] if tmp_int_bound.assigned: - for n in range(tmp_int_bound.lb, tmp_int_bound.rb): + # Do not forget to fill right bound + for n in range(tmp_int_bound.lb, tmp_int_bound.rb + 1): out[m, n] = True - return out @@ -96,19 +108,6 @@ cdef inline intBound intify(fpBound bound, Py_ssize_t min_idx, Py_ssize_t max_id return intBound(lb=0, rb=0, assigned=False) -cdef inline fpBound set_bound(fpBound bound, float new_bound): - cdef new_lb, new_rb - - if bound.assigned: - new_lb = min(bound.lb, new_bound) - new_rb = max(bound.rb, new_bound) - else: - new_lb = new_bound - new_rb = new_bound - - return fpBound(lb=new_lb, rb=new_rb, assigned=True) - - cdef packed struct fpBound: float lb float rb @@ -123,3 +122,34 @@ cdef packed struct intBound: cdef inline float lerp(float y0, float y1, float t): return y0 * (1 - t) + y1 * t + + +cpdef _left_right_bounds(cnp.uint8_t[:,:] image): + cdef Py_ssize_t i, j, M = image.shape[0], N = image.shape[1], curr_pos = 0, left, right + cdef cnp.ndarray[dtype=int, ndim=2, mode="c"] left_right_bounds = np.zeros((2 * M, 2), dtype=np.int32) + cdef unsigned char found = False + + for i in range(M): + found = False + + for j in range(N): + if image[i, j]: + left = j + found = True + break + + for j in range(N): + if image[i, N - 1 - j]: + right = N - 1 - j + found = True + break + + if found: + left_right_bounds[2 * curr_pos, 0] = i + left_right_bounds[2 * curr_pos, 1] = left + left_right_bounds[2 * curr_pos + 1, 0] = i + left_right_bounds[2 * curr_pos + 1, 1] = right + + curr_pos += 1 + + return np.ascontiguousarray(left_right_bounds[: 2 * curr_pos, :]) From 9dac9df8fb24a62811894f2c2727fad7554251d1 Mon Sep 17 00:00:00 2001 From: Anihilatorgunn Date: Sat, 16 Nov 2024 03:08:24 +0300 Subject: [PATCH 03/42] proposal --- imops/src/_convex_hull.pyx | 1 + 1 file changed, 1 insertion(+) diff --git a/imops/src/_convex_hull.pyx b/imops/src/_convex_hull.pyx index cabff97a..adca183a 100644 --- a/imops/src/_convex_hull.pyx +++ b/imops/src/_convex_hull.pyx @@ -101,6 +101,7 @@ def _grid_points_in_poly(float[:] vx, float[:] vy, Py_ssize_t M, Py_ssize_t N, P return out +# TODO: maybe use round instead of floorf and ceilf? cdef inline intBound intify(fpBound bound, Py_ssize_t min_idx, Py_ssize_t max_idx): if bound.assigned: return intBound(lb = max(min_idx, floorf(bound.lb)), rb = min(max_idx, ceilf(bound.rb)), assigned=True) From cb9267bbed3adccdd0e0d7c0f5aaa3a3bd8cd7d0 Mon Sep 17 00:00:00 2001 From: Anihilatorgunn Date: Sat, 16 Nov 2024 15:23:12 +0300 Subject: [PATCH 04/42] fix --- imops/morphology.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/imops/morphology.py b/imops/morphology.py index a2e58f6a..0287f8c8 100644 --- a/imops/morphology.py +++ b/imops/morphology.py @@ -550,9 +550,8 @@ def convex_hull_image_2d(image, offset_coordinates=True): """ ndim = image.ndim - assert ndim == 2, f'Expected image to have ndim=2 got {ndim}' if ndim != 2: - raise RuntimeError() + raise RuntimeError(f'Expected image to have ndim=2 got {ndim}') if np.count_nonzero(image) == 0: warn( From c26a49152f6f4e8f19935efc4ac4269e98c21612 Mon Sep 17 00:00:00 2001 From: Anihilatorgunn Date: Sat, 16 Nov 2024 15:34:52 +0300 Subject: [PATCH 05/42] offset + unique fused --- imops/morphology.py | 8 +-- imops/src/_convex_hull.pyx | 105 +++++++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+), 4 deletions(-) diff --git a/imops/morphology.py b/imops/morphology.py index 0287f8c8..170a35d6 100644 --- a/imops/morphology.py +++ b/imops/morphology.py @@ -26,7 +26,7 @@ _binary_erosion as cython_fast_binary_erosion, ) from .src._morphology import _binary_dilation as cython_binary_dilation, _binary_erosion as cython_binary_erosion -from .src._convex_hull import _grid_points_in_poly, _left_right_bounds +from .src._convex_hull import _grid_points_in_poly, _left_right_bounds, _offset_unique from .utils import morphology_composition_args, normalize_num_threads @@ -569,9 +569,9 @@ def convex_hull_image_2d(image, offset_coordinates=True): coords = _left_right_bounds(image) if offset_coordinates: - offsets = _offsets_diamond(ndim) - coords = (coords[:, np.newaxis, :] + offsets).reshape(-1, ndim) - coords = unique_rows(coords) + coords = _offset_unique(coords) + else: + coords = unique_rows(coords) # Find the convex hull try: diff --git a/imops/src/_convex_hull.pyx b/imops/src/_convex_hull.pyx index adca183a..e51cf5d2 100644 --- a/imops/src/_convex_hull.pyx +++ b/imops/src/_convex_hull.pyx @@ -154,3 +154,108 @@ cpdef _left_right_bounds(cnp.uint8_t[:,:] image): curr_pos += 1 return np.ascontiguousarray(left_right_bounds[: 2 * curr_pos, :]) + + +cdef inline int set_unique_curr(float* expanded_bounds, int x, int l, int r): + if l == r: + expanded_bounds[0] = x + expanded_bounds[1] = l - 0.5 + + expanded_bounds[2] = x - 0.5 + expanded_bounds[3] = l + + expanded_bounds[4] = x + expanded_bounds[5] = l + 0.5 + + return 3 + elif r == l + 1: + expanded_bounds[0] = x + expanded_bounds[1] = l - 0.5 + + expanded_bounds[2] = x - 0.5 + expanded_bounds[3] = l + + expanded_bounds[4] = x + expanded_bounds[5] = l + 0.5 + + expanded_bounds[6] = x - 0.5 + expanded_bounds[7] = r + + expanded_bounds[8] = x + expanded_bounds[9] = r + 0.5 + + return 5 + + else: + expanded_bounds[0] = x + expanded_bounds[1] = l - 0.5 + + expanded_bounds[2] = x - 0.5 + expanded_bounds[3] = l + + expanded_bounds[4] = x + expanded_bounds[5] = l + 0.5 + + expanded_bounds[6] = x + expanded_bounds[7] = r - 0.5 + + expanded_bounds[8] = x - 0.5 + expanded_bounds[9] = r + + expanded_bounds[10] = x + expanded_bounds[11] = r + 0.5 + + return 6 + + +cpdef _offset_unique(int[:,:] left_right_bounds): + cdef Py_ssize_t N = left_right_bounds.shape[0], i, curr_pos = 0 + cdef cnp.ndarray[dtype=float, ndim=2, mode="c"] expanded_bounds = np.zeros((4 * N, 2), dtype=np.float32) + + cdef int x_l_prev, y_l_prev, x_r_prev, y_r_prev, x_l_curr, y_l_curr, x_r_curr, y_r_curr, shift + + x_l_prev = left_right_bounds[0, 0] + y_l_prev = left_right_bounds[0, 1] + x_r_prev = left_right_bounds[1, 0] + y_r_prev = left_right_bounds[1, 1] + + curr_pos += set_unique_curr(&expanded_bounds[0, 0], x_l_prev, y_l_prev, y_r_prev) + + for i in range(1, N // 2): + x_l_curr = left_right_bounds[2 * i, 0] + y_l_curr = left_right_bounds[2 * i, 1] + x_r_curr = left_right_bounds[2 * i + 1, 0] + y_r_curr = left_right_bounds[2 * i + 1, 1] + + curr_pos += set_unique_curr(&expanded_bounds[curr_pos, 0], x_l_curr, y_l_curr, y_r_curr) + + if x_l_prev + 1 == x_l_curr and (y_l_prev == y_l_curr or y_l_prev == y_r_curr): + pass + else: + expanded_bounds[curr_pos, 0] = x_l_prev + 0.5 + expanded_bounds[curr_pos, 1] = y_l_prev + curr_pos += 1 + + if x_l_prev + 1 == x_l_curr and (y_r_prev == y_l_curr or y_r_prev == y_r_curr) and (y_r_prev != y_l_prev): + pass + else: + expanded_bounds[curr_pos, 0] = x_l_prev + 0.5 + expanded_bounds[curr_pos, 1] = y_r_prev + curr_pos += 1 + + x_l_prev = x_l_curr + y_l_prev = y_l_curr + x_r_prev = x_r_curr + y_r_prev = y_r_curr + + expanded_bounds[curr_pos, 0] = x_l_prev + 0.5 + expanded_bounds[curr_pos, 1] = y_l_prev + curr_pos += 1 + + if y_r_prev != y_l_prev: + expanded_bounds[curr_pos, 0] = x_l_prev + 0.5 + expanded_bounds[curr_pos, 1] = y_r_prev + curr_pos += 1 + + + return np.ascontiguousarray(expanded_bounds[:curr_pos, :]) From f36a10d46d08eeae0530be4bb4ad00a39a0f8fae Mon Sep 17 00:00:00 2001 From: Anihilatorgunn Date: Sat, 16 Nov 2024 17:03:43 +0300 Subject: [PATCH 06/42] test convex_hull_2d --- tests/test_convex_hull2d.py | 52 +++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 tests/test_convex_hull2d.py diff --git a/tests/test_convex_hull2d.py b/tests/test_convex_hull2d.py new file mode 100644 index 00000000..4367aa16 --- /dev/null +++ b/tests/test_convex_hull2d.py @@ -0,0 +1,52 @@ +import numpy as np +from skimage import data +from skimage.morphology import convex_hull_image +from skimage.morphology.convex_hull import _offsets_diamond +from skimage.util import invert, unique_rows + +from imops.morphology import convex_hull_image_2d +from imops.src._convex_hull import _left_right_bounds, _offset_unique + + +def test_bounds(): + image = np.zeros((100, 100), dtype=bool) + image[20:70, 20:90] = (np.random.randn(50, 70) > 0.5) + + im_any = np.any(image, axis=1) + x_indices = np.arange(0, image.shape[0])[im_any] + y_indices_left = np.argmax(image[im_any], axis=1) + y_indices_right = image.shape[1] - 1 - np.argmax(image[im_any][:,::-1], axis=1) + left = np.stack((x_indices, y_indices_left), axis=-1) + right = np.stack((x_indices, y_indices_right), axis=-1) + coords_ref = np.vstack((left, right)) + + coords = _left_right_bounds(image) + + # _left_right_bounds has another order + assert len(coords_ref) == len(unique_rows(np.concatenate((coords, coords_ref), 0))) + + +def test_offset(): + image = np.zeros((100, 100), dtype=bool) + image[20:70, 20:90] = (np.random.randn(50, 70) > 0.5) + + coords = _left_right_bounds(image) + + offsets = _offsets_diamond(2) + coords_ref = unique_rows((coords[:, None, :] + offsets).reshape(-1, 2)) + + coords = _offset_unique(coords) + + # _left_right_bounds has another order + assert len(coords_ref) == len(unique_rows(np.concatenate((coords, coords_ref), 0))) + + +def test_convex_hull_image_2d(): + image = invert(data.horse()) + + chull_ref = convex_hull_image(image, offset_coordinates=True, include_borders=True) + + chull = convex_hull_image_2d(image, offset_coordinates=True) + + assert not (chull < image).any() + assert not (chull < chull_ref).any() From df8f5800d080bf543c208f82afd165d6c7f78237 Mon Sep 17 00:00:00 2001 From: Anihilatorgunn Date: Sat, 16 Nov 2024 19:07:43 +0300 Subject: [PATCH 07/42] naming --- docs/index.md | 2 +- imops/morphology.py | 6 +++--- tests/{test_convex_hull2d.py => test_convex_hull.py} | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) rename tests/{test_convex_hull2d.py => test_convex_hull.py} (91%) diff --git a/docs/index.md b/docs/index.md index a5115d02..3ffc0fae 100644 --- a/docs/index.md +++ b/docs/index.md @@ -48,7 +48,7 @@ pip install imops[numba] # additionally install Numba backend ::: imops.morphology.distance_transform_edt -::: imops.morphology.convex_hull_image_2d +::: imops.morphology.convex_hull_image ::: imops.measure.label diff --git a/imops/morphology.py b/imops/morphology.py index 170a35d6..e1f7cf1a 100644 --- a/imops/morphology.py +++ b/imops/morphology.py @@ -524,7 +524,7 @@ def distance_transform_edt( return None -def convex_hull_image_2d(image, offset_coordinates=True): +def convex_hull_image(image, offset_coordinates=True): """ Fast convex hull of an image. Similar to skimage.morphology.convex_hull_image with include_borders=True @@ -545,13 +545,13 @@ def convex_hull_image_2d(image, offset_coordinates=True): Examples -------- ```python - chull = convex_hull_image_2d(x) + chull = convex_hull_image(x) ``` """ ndim = image.ndim if ndim != 2: - raise RuntimeError(f'Expected image to have ndim=2 got {ndim}') + raise RuntimeError(f'convex_hull_image is currently implemented only for 2D arrays, got {ndim}D array') if np.count_nonzero(image) == 0: warn( diff --git a/tests/test_convex_hull2d.py b/tests/test_convex_hull.py similarity index 91% rename from tests/test_convex_hull2d.py rename to tests/test_convex_hull.py index 4367aa16..44bc4d62 100644 --- a/tests/test_convex_hull2d.py +++ b/tests/test_convex_hull.py @@ -4,7 +4,7 @@ from skimage.morphology.convex_hull import _offsets_diamond from skimage.util import invert, unique_rows -from imops.morphology import convex_hull_image_2d +from imops.morphology import convex_hull_image from imops.src._convex_hull import _left_right_bounds, _offset_unique @@ -41,12 +41,12 @@ def test_offset(): assert len(coords_ref) == len(unique_rows(np.concatenate((coords, coords_ref), 0))) -def test_convex_hull_image_2d(): +def test_convex_hull_image(): image = invert(data.horse()) chull_ref = convex_hull_image(image, offset_coordinates=True, include_borders=True) - chull = convex_hull_image_2d(image, offset_coordinates=True) + chull = convex_hull_image(image, offset_coordinates=True) assert not (chull < image).any() assert not (chull < chull_ref).any() From bf8058a3fb7a60d43d729349e09ca5a8b4e44fa0 Mon Sep 17 00:00:00 2001 From: Anihilatorgunn Date: Sat, 16 Nov 2024 19:08:53 +0300 Subject: [PATCH 08/42] black + isort --- imops/morphology.py | 21 ++++++++------------- imops/src/_convex_hull.pyx | 4 ++-- tests/test_convex_hull.py | 6 +++--- 3 files changed, 13 insertions(+), 18 deletions(-) diff --git a/imops/morphology.py b/imops/morphology.py index e1f7cf1a..417ce80e 100644 --- a/imops/morphology.py +++ b/imops/morphology.py @@ -7,6 +7,7 @@ from scipy.ndimage import distance_transform_edt as scipy_distance_transform_edt, generate_binary_structure from scipy.ndimage._nd_image import euclidean_feature_transform from scipy.spatial import ConvexHull, QhullError +from skimage._shared.utils import warn as skimage_warn from skimage.morphology import ( binary_closing as scipy_binary_closing, binary_dilation as scipy_binary_dilation, @@ -14,19 +15,18 @@ binary_opening as scipy_binary_opening, ) from skimage.util import unique_rows -from skimage._shared.utils import warn as skimage_warn from .backend import BackendLike, Cython, Scipy, resolve_backend from .box import add_margin, box_to_shape, mask_to_box, shape_to_box from .compat import _ni_support from .crop import crop_to_box from .pad import restore_crop +from .src._convex_hull import _grid_points_in_poly, _left_right_bounds, _offset_unique from .src._fast_morphology import ( _binary_dilation as cython_fast_binary_dilation, _binary_erosion as cython_fast_binary_erosion, ) from .src._morphology import _binary_dilation as cython_binary_dilation, _binary_erosion as cython_binary_erosion -from .src._convex_hull import _grid_points_in_poly, _left_right_bounds, _offset_unique from .utils import morphology_composition_args, normalize_num_threads @@ -534,7 +534,7 @@ def convex_hull_image(image, offset_coordinates=True): input image, must be 2D offset_coordinates: bool If True, a pixel at coordinate, e.g., (4, 7) will be represented by coordinates - (3.5, 7), (4.5, 7), (4, 6.5), and (4, 7.5). + (3.5, 7), (4.5, 7), (4, 6.5), and (4, 7.5). This adds some “extent” to a pixel when computing the hull. Returns @@ -555,8 +555,7 @@ def convex_hull_image(image, offset_coordinates=True): if np.count_nonzero(image) == 0: warn( - "Input image is entirely zero, no valid convex hull. " - "Returning empty image", + "Input image is entirely zero, no valid convex hull. " "Returning empty image", UserWarning, ) return np.zeros(image.shape, dtype=bool) @@ -577,21 +576,17 @@ def convex_hull_image(image, offset_coordinates=True): try: hull = ConvexHull(coords) except QhullError as err: - skimage_warn( - f"Failed to get convex hull image. " - f"Returning empty image, see error message below:\n" - f"{err}" - ) + skimage_warn(f"Failed to get convex hull image. " f"Returning empty image, see error message below:\n" f"{err}") return np.zeros(image.shape, dtype=bool) vertices = hull.points[hull.vertices] - #return vertices + # return vertices # If 2D, use fast Cython function to locate convex hull pixels labels = _grid_points_in_poly( - np.ascontiguousarray(vertices[:,0], dtype=np.float32), - np.ascontiguousarray(vertices[:,1], dtype=np.float32), + np.ascontiguousarray(vertices[:, 0], dtype=np.float32), + np.ascontiguousarray(vertices[:, 1], dtype=np.float32), image.shape[0], image.shape[1], len(vertices), diff --git a/imops/src/_convex_hull.pyx b/imops/src/_convex_hull.pyx index e51cf5d2..1790c363 100644 --- a/imops/src/_convex_hull.pyx +++ b/imops/src/_convex_hull.pyx @@ -4,10 +4,10 @@ #cython: wraparound=False import numpy as np +cimport cython +cimport numpy as cnp from libc.math cimport ceilf, floorf -cimport numpy as cnp -cimport cython cnp.import_array() diff --git a/tests/test_convex_hull.py b/tests/test_convex_hull.py index 44bc4d62..1a6ead7f 100644 --- a/tests/test_convex_hull.py +++ b/tests/test_convex_hull.py @@ -10,12 +10,12 @@ def test_bounds(): image = np.zeros((100, 100), dtype=bool) - image[20:70, 20:90] = (np.random.randn(50, 70) > 0.5) + image[20:70, 20:90] = np.random.randn(50, 70) > 0.5 im_any = np.any(image, axis=1) x_indices = np.arange(0, image.shape[0])[im_any] y_indices_left = np.argmax(image[im_any], axis=1) - y_indices_right = image.shape[1] - 1 - np.argmax(image[im_any][:,::-1], axis=1) + y_indices_right = image.shape[1] - 1 - np.argmax(image[im_any][:, ::-1], axis=1) left = np.stack((x_indices, y_indices_left), axis=-1) right = np.stack((x_indices, y_indices_right), axis=-1) coords_ref = np.vstack((left, right)) @@ -28,7 +28,7 @@ def test_bounds(): def test_offset(): image = np.zeros((100, 100), dtype=bool) - image[20:70, 20:90] = (np.random.randn(50, 70) > 0.5) + image[20:70, 20:90] = np.random.randn(50, 70) > 0.5 coords = _left_right_bounds(image) From 78bc0ea3db8e0546e325b570c6aeffaab0bbe031 Mon Sep 17 00:00:00 2001 From: Anihilatorgunn Date: Sat, 16 Nov 2024 19:14:42 +0300 Subject: [PATCH 09/42] fix import --- imops/morphology.py | 1 - tests/test_convex_hull.py | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/imops/morphology.py b/imops/morphology.py index 417ce80e..07537d72 100644 --- a/imops/morphology.py +++ b/imops/morphology.py @@ -1,4 +1,3 @@ -from itertools import product from typing import Callable, Tuple, Union from warnings import warn diff --git a/tests/test_convex_hull.py b/tests/test_convex_hull.py index 1a6ead7f..92fdeea7 100644 --- a/tests/test_convex_hull.py +++ b/tests/test_convex_hull.py @@ -4,7 +4,7 @@ from skimage.morphology.convex_hull import _offsets_diamond from skimage.util import invert, unique_rows -from imops.morphology import convex_hull_image +from imops.morphology import convex_hull_image as convex_hull_image_fast from imops.src._convex_hull import _left_right_bounds, _offset_unique @@ -46,7 +46,7 @@ def test_convex_hull_image(): chull_ref = convex_hull_image(image, offset_coordinates=True, include_borders=True) - chull = convex_hull_image(image, offset_coordinates=True) + chull = convex_hull_image_fast(image, offset_coordinates=True) assert not (chull < image).any() assert not (chull < chull_ref).any() From 4ae8e0d3188547dd310a7c9281a10eb8a55c13a3 Mon Sep 17 00:00:00 2001 From: Anihilatorgunn Date: Sat, 16 Nov 2024 19:16:51 +0300 Subject: [PATCH 10/42] quotes --- imops/morphology.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/imops/morphology.py b/imops/morphology.py index 07537d72..042be088 100644 --- a/imops/morphology.py +++ b/imops/morphology.py @@ -554,7 +554,7 @@ def convex_hull_image(image, offset_coordinates=True): if np.count_nonzero(image) == 0: warn( - "Input image is entirely zero, no valid convex hull. " "Returning empty image", + 'Input image is entirely zero, no valid convex hull. ' 'Returning empty image', UserWarning, ) return np.zeros(image.shape, dtype=bool) @@ -575,7 +575,7 @@ def convex_hull_image(image, offset_coordinates=True): try: hull = ConvexHull(coords) except QhullError as err: - skimage_warn(f"Failed to get convex hull image. " f"Returning empty image, see error message below:\n" f"{err}") + skimage_warn(f'Failed to get convex hull image. ' f'Returning empty image, see error message below:\n' f'{err}') return np.zeros(image.shape, dtype=bool) vertices = hull.points[hull.vertices] From 933c67aaf1f289c8e613e11d550f8e7bf5e0a917 Mon Sep 17 00:00:00 2001 From: Anihilatorgunn Date: Sat, 16 Nov 2024 19:31:36 +0300 Subject: [PATCH 11/42] style --- imops/src/_convex_hull.pyx | 63 +++++++++++++++++++------------------- 1 file changed, 32 insertions(+), 31 deletions(-) diff --git a/imops/src/_convex_hull.pyx b/imops/src/_convex_hull.pyx index 1790c363..76c48de4 100644 --- a/imops/src/_convex_hull.pyx +++ b/imops/src/_convex_hull.pyx @@ -1,10 +1,9 @@ -#cython: cdivision=True -#cython: boundscheck=False -#cython: nonecheck=False -#cython: wraparound=False +# cython: cdivision=True +# cython: boundscheck=False +# cython: nonecheck=False +# cython: wraparound=False import numpy as np -cimport cython cimport numpy as cnp from libc.math cimport ceilf, floorf @@ -27,13 +26,12 @@ INT_BOUND_DTYPE = np.dtype([ def _grid_points_in_poly(float[:] vx, float[:] vy, Py_ssize_t M, Py_ssize_t N, Py_ssize_t nr_verts): cdef intBound tmp_int_bound cdef Py_ssize_t m, n, i - cdef float prev_x, prev_y, curr_x, curr_ys + cdef float prev_x, prev_y, curr_x, curr_y cdef float tmp_from_x, tmp_from_y, tmp_to_x, tmp_to_y cdef float lerp_t, bound_y cdef Py_ssize_t x_set, x_start, x_stop - cdef cnp.ndarray[dtype=cnp.uint8_t, ndim=2, mode="c"] out = \ - np.zeros((M, N), dtype=np.uint8) + cdef cnp.ndarray[dtype=cnp.uint8_t, ndim=2, mode="c"] out = np.zeros((M, N), dtype=np.uint8) cdef fpBound[:] fpBounds = np.empty(M, dtype=FP_BOUND_DTYPE) cdef intBound[:] intBounds = np.empty(M, dtype=INT_BOUND_DTYPE) @@ -86,7 +84,7 @@ def _grid_points_in_poly(float[:] vx, float[:] vy, Py_ssize_t M, Py_ssize_t N, P prev_y = curr_y # bounds are computed as point interpolation - # so bounds must be valid indices for out array + # so bounds must be valid indices for out array for m in range(M): intBounds[m] = intify(fpBounds[m], 0, N - 1) @@ -104,7 +102,11 @@ def _grid_points_in_poly(float[:] vx, float[:] vy, Py_ssize_t M, Py_ssize_t N, P # TODO: maybe use round instead of floorf and ceilf? cdef inline intBound intify(fpBound bound, Py_ssize_t min_idx, Py_ssize_t max_idx): if bound.assigned: - return intBound(lb = max(min_idx, floorf(bound.lb)), rb = min(max_idx, ceilf(bound.rb)), assigned=True) + return intBound( + lb = max(min_idx, floorf(bound.lb)), + rb = min(max_idx, ceilf(bound.rb)), + assigned=True + ) return intBound(lb=0, rb=0, assigned=False) @@ -125,7 +127,7 @@ cdef inline float lerp(float y0, float y1, float t): return y0 * (1 - t) + y1 * t -cpdef _left_right_bounds(cnp.uint8_t[:,:] image): +cpdef _left_right_bounds(cnp.uint8_t[:, :] image): cdef Py_ssize_t i, j, M = image.shape[0], N = image.shape[1], curr_pos = 0, left, right cdef cnp.ndarray[dtype=int, ndim=2, mode="c"] left_right_bounds = np.zeros((2 * M, 2), dtype=np.int32) cdef unsigned char found = False @@ -156,63 +158,63 @@ cpdef _left_right_bounds(cnp.uint8_t[:,:] image): return np.ascontiguousarray(left_right_bounds[: 2 * curr_pos, :]) -cdef inline int set_unique_curr(float* expanded_bounds, int x, int l, int r): - if l == r: +cdef inline int set_unique_curr(float* expanded_bounds, int x, int l_b, int r_b): + if l_b == r_b: expanded_bounds[0] = x - expanded_bounds[1] = l - 0.5 + expanded_bounds[1] = l_b - 0.5 expanded_bounds[2] = x - 0.5 - expanded_bounds[3] = l + expanded_bounds[3] = l_b expanded_bounds[4] = x - expanded_bounds[5] = l + 0.5 + expanded_bounds[5] = l_b + 0.5 return 3 - elif r == l + 1: + elif r_b == l_b + 1: expanded_bounds[0] = x - expanded_bounds[1] = l - 0.5 + expanded_bounds[1] = l_b - 0.5 expanded_bounds[2] = x - 0.5 - expanded_bounds[3] = l + expanded_bounds[3] = l_b expanded_bounds[4] = x - expanded_bounds[5] = l + 0.5 + expanded_bounds[5] = l_b + 0.5 expanded_bounds[6] = x - 0.5 - expanded_bounds[7] = r + expanded_bounds[7] = r_b expanded_bounds[8] = x - expanded_bounds[9] = r + 0.5 + expanded_bounds[9] = r_b + 0.5 return 5 else: expanded_bounds[0] = x - expanded_bounds[1] = l - 0.5 + expanded_bounds[1] = l_b - 0.5 expanded_bounds[2] = x - 0.5 - expanded_bounds[3] = l + expanded_bounds[3] = l_b expanded_bounds[4] = x - expanded_bounds[5] = l + 0.5 + expanded_bounds[5] = l_b + 0.5 expanded_bounds[6] = x - expanded_bounds[7] = r - 0.5 + expanded_bounds[7] = r_b - 0.5 expanded_bounds[8] = x - 0.5 - expanded_bounds[9] = r + expanded_bounds[9] = r_b expanded_bounds[10] = x - expanded_bounds[11] = r + 0.5 + expanded_bounds[11] = r_b + 0.5 return 6 -cpdef _offset_unique(int[:,:] left_right_bounds): +cpdef _offset_unique(int[:, :] left_right_bounds): cdef Py_ssize_t N = left_right_bounds.shape[0], i, curr_pos = 0 cdef cnp.ndarray[dtype=float, ndim=2, mode="c"] expanded_bounds = np.zeros((4 * N, 2), dtype=np.float32) - cdef int x_l_prev, y_l_prev, x_r_prev, y_r_prev, x_l_curr, y_l_curr, x_r_curr, y_r_curr, shift + cdef int x_l_prev, y_l_prev, x_r_prev, y_r_prev, x_l_curr, y_l_curr, x_r_curr, y_r_curr x_l_prev = left_right_bounds[0, 0] y_l_prev = left_right_bounds[0, 1] @@ -257,5 +259,4 @@ cpdef _offset_unique(int[:,:] left_right_bounds): expanded_bounds[curr_pos, 1] = y_r_prev curr_pos += 1 - return np.ascontiguousarray(expanded_bounds[:curr_pos, :]) From e3fbd0bb08c4e2d5aa28cece679f3bf589908d13 Mon Sep 17 00:00:00 2001 From: Anihilatorgunn Date: Sat, 16 Nov 2024 19:35:57 +0300 Subject: [PATCH 12/42] style --- imops/src/_convex_hull.pyx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/imops/src/_convex_hull.pyx b/imops/src/_convex_hull.pyx index 76c48de4..9fb52a24 100644 --- a/imops/src/_convex_hull.pyx +++ b/imops/src/_convex_hull.pyx @@ -103,8 +103,8 @@ def _grid_points_in_poly(float[:] vx, float[:] vy, Py_ssize_t M, Py_ssize_t N, P cdef inline intBound intify(fpBound bound, Py_ssize_t min_idx, Py_ssize_t max_idx): if bound.assigned: return intBound( - lb = max(min_idx, floorf(bound.lb)), - rb = min(max_idx, ceilf(bound.rb)), + lb = max(min_idx, floorf(bound.lb)), + rb = min(max_idx, ceilf(bound.rb)), assigned=True ) From 750a047fd30a1ceff5f3a0db705cec73ce324d85 Mon Sep 17 00:00:00 2001 From: Anihilatorgunn Date: Sat, 16 Nov 2024 19:45:38 +0300 Subject: [PATCH 13/42] bfix build --- _build_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_build_utils.py b/_build_utils.py index c5f032ca..59292e31 100644 --- a/_build_utils.py +++ b/_build_utils.py @@ -63,7 +63,7 @@ def get_ext_modules(): '/O3' if on_windows else '-O3', ] # FIXME: account for higher gcc versions - modules = ['backprojection', 'measure', 'morphology', 'numeric', 'radon', 'zoom'] + modules = ['backprojection', 'measure', 'morphology', 'numeric', 'radon', 'zoom', 'convex_hull'] modules_to_link_against_numpy_core_math_lib = ['numeric'] src_dir = Path(__file__).parent / name / 'src' From 2faa7afadc05033d0cb5ebc36f224debbf3bae57 Mon Sep 17 00:00:00 2001 From: Anihilatorgunn Date: Sun, 17 Nov 2024 13:14:40 +0300 Subject: [PATCH 14/42] skimage_warn -> warn, style --- imops/morphology.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/imops/morphology.py b/imops/morphology.py index 042be088..88a067a9 100644 --- a/imops/morphology.py +++ b/imops/morphology.py @@ -6,7 +6,6 @@ from scipy.ndimage import distance_transform_edt as scipy_distance_transform_edt, generate_binary_structure from scipy.ndimage._nd_image import euclidean_feature_transform from scipy.spatial import ConvexHull, QhullError -from skimage._shared.utils import warn as skimage_warn from skimage.morphology import ( binary_closing as scipy_binary_closing, binary_dilation as scipy_binary_dilation, @@ -575,7 +574,7 @@ def convex_hull_image(image, offset_coordinates=True): try: hull = ConvexHull(coords) except QhullError as err: - skimage_warn(f'Failed to get convex hull image. ' f'Returning empty image, see error message below:\n' f'{err}') + warn(f'Failed to get convex hull image. ' f'Returning empty image, see error message below: \n' f'{err}') return np.zeros(image.shape, dtype=bool) vertices = hull.points[hull.vertices] From b1693442806f8291c93638e9857b980255db51a3 Mon Sep 17 00:00:00 2001 From: Anihilatorgunn Date: Sun, 17 Nov 2024 14:04:05 +0300 Subject: [PATCH 15/42] fix import for old scipy --- imops/morphology.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/imops/morphology.py b/imops/morphology.py index 88a067a9..abd3f49e 100644 --- a/imops/morphology.py +++ b/imops/morphology.py @@ -6,6 +6,13 @@ from scipy.ndimage import distance_transform_edt as scipy_distance_transform_edt, generate_binary_structure from scipy.ndimage._nd_image import euclidean_feature_transform from scipy.spatial import ConvexHull, QhullError + + +try: + from scipy.spatial import QhullError +except ImportError: + from scipy.spatial.qhull import QhullError # Old scipy has another structure + from skimage.morphology import ( binary_closing as scipy_binary_closing, binary_dilation as scipy_binary_dilation, From 461aa6ff7c8f982d776bbf06c206aca5f0cd9818 Mon Sep 17 00:00:00 2001 From: Anihilatorgunn Date: Sun, 17 Nov 2024 14:53:32 +0300 Subject: [PATCH 16/42] fix --- imops/morphology.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imops/morphology.py b/imops/morphology.py index abd3f49e..1d9466ab 100644 --- a/imops/morphology.py +++ b/imops/morphology.py @@ -5,7 +5,7 @@ from edt import edt from scipy.ndimage import distance_transform_edt as scipy_distance_transform_edt, generate_binary_structure from scipy.ndimage._nd_image import euclidean_feature_transform -from scipy.spatial import ConvexHull, QhullError +from scipy.spatial import ConvexHull try: From e01ace77ff45b02c5e82ea7db075b9c67465d236 Mon Sep 17 00:00:00 2001 From: AnihilatorGun Date: Sun, 17 Nov 2024 16:42:08 +0300 Subject: [PATCH 17/42] test fix for py3.7 --- tests/test_convex_hull.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_convex_hull.py b/tests/test_convex_hull.py index 92fdeea7..d2801fec 100644 --- a/tests/test_convex_hull.py +++ b/tests/test_convex_hull.py @@ -44,7 +44,10 @@ def test_offset(): def test_convex_hull_image(): image = invert(data.horse()) - chull_ref = convex_hull_image(image, offset_coordinates=True, include_borders=True) + try: + chull_ref = convex_hull_image(image, offset_coordinates=True, include_borders=True) + except TypeError: + chull_ref = convex_hull_image(image, offset_coordinates=True) chull = convex_hull_image_fast(image, offset_coordinates=True) From d9bc10bc7dba32631fd0cced29ff265b0d13c606 Mon Sep 17 00:00:00 2001 From: AnihilatorGun Date: Mon, 18 Nov 2024 21:48:15 +0300 Subject: [PATCH 18/42] test offset_coordinates, test corner cases --- tests/test_convex_hull.py | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/tests/test_convex_hull.py b/tests/test_convex_hull.py index d2801fec..66775179 100644 --- a/tests/test_convex_hull.py +++ b/tests/test_convex_hull.py @@ -8,6 +8,11 @@ from imops.src._convex_hull import _left_right_bounds, _offset_unique +@pytest.fixture(params=[False, True]) +def offset_coordinates(request): + return request.param + + def test_bounds(): image = np.zeros((100, 100), dtype=bool) image[20:70, 20:90] = np.random.randn(50, 70) > 0.5 @@ -41,15 +46,29 @@ def test_offset(): assert len(coords_ref) == len(unique_rows(np.concatenate((coords, coords_ref), 0))) -def test_convex_hull_image(): +def test_convex_hull_image(offset_coordinates): image = invert(data.horse()) try: - chull_ref = convex_hull_image(image, offset_coordinates=True, include_borders=True) + chull_ref = convex_hull_image(image, offset_coordinates=offset_coordinates, include_borders=True) except TypeError: - chull_ref = convex_hull_image(image, offset_coordinates=True) + chull_ref = convex_hull_image(image, offset_coordinates=offset_coordinates) - chull = convex_hull_image_fast(image, offset_coordinates=True) + chull = convex_hull_image_fast(image, offset_coordinates=offset_coordinates) assert not (chull < image).any() assert not (chull < chull_ref).any() + + +def test_convex_hull_image_cornercases(offset_coordinates): + image = np.zeros((3, 3, 3), dtype=bool) + + with pytest.raises(RuntimeError): + chull = convex_hull_image_fast(image, offset_coordinates=offset_coordinates) + + image = np.zeros((10, 10), dtype=bool) + + with pytest.warns(UserWarning): + chull = convex_hull_image_fast(image, offset_coordinates=offset_coordinates) + + assert (chull == np.zeros_like(chull)).all() From 567da9a4f9fa2dfdce00b1759d710aab544ed819 Mon Sep 17 00:00:00 2001 From: AnihilatorGun Date: Mon, 18 Nov 2024 21:59:06 +0300 Subject: [PATCH 19/42] fix --- tests/test_convex_hull.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/tests/test_convex_hull.py b/tests/test_convex_hull.py index 66775179..1ef87648 100644 --- a/tests/test_convex_hull.py +++ b/tests/test_convex_hull.py @@ -1,4 +1,5 @@ import numpy as np +import pytest from skimage import data from skimage.morphology import convex_hull_image from skimage.morphology.convex_hull import _offsets_diamond @@ -60,13 +61,26 @@ def test_convex_hull_image(offset_coordinates): assert not (chull < chull_ref).any() -def test_convex_hull_image_cornercases(offset_coordinates): +def test_convex_hull_image_non2d(offset_coordinates): image = np.zeros((3, 3, 3), dtype=bool) with pytest.raises(RuntimeError): chull = convex_hull_image_fast(image, offset_coordinates=offset_coordinates) + +def test_convex_hull_image_empty(offset_coordinates): + image = np.zeros((10, 10), dtype=bool) + + with pytest.warns(UserWarning): + chull = convex_hull_image_fast(image, offset_coordinates=offset_coordinates) + + assert (chull == np.zeros_like(chull)).all() + + +def test_convex_hull_image_qhullsrc_issues(): image = np.zeros((10, 10), dtype=bool) + image[1, 1] = True + image[-2, -2] = True with pytest.warns(UserWarning): chull = convex_hull_image_fast(image, offset_coordinates=offset_coordinates) From 780df145251b6e439f744a36878c0b1f46c268a1 Mon Sep 17 00:00:00 2001 From: AnihilatorGun Date: Mon, 18 Nov 2024 22:04:58 +0300 Subject: [PATCH 20/42] style... --- tests/test_convex_hull.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_convex_hull.py b/tests/test_convex_hull.py index 1ef87648..b7a8ec2d 100644 --- a/tests/test_convex_hull.py +++ b/tests/test_convex_hull.py @@ -65,7 +65,7 @@ def test_convex_hull_image_non2d(offset_coordinates): image = np.zeros((3, 3, 3), dtype=bool) with pytest.raises(RuntimeError): - chull = convex_hull_image_fast(image, offset_coordinates=offset_coordinates) + _ = convex_hull_image_fast(image, offset_coordinates=offset_coordinates) def test_convex_hull_image_empty(offset_coordinates): From 190d677cb908ccf21a9bb65f84f5b42180258ccd Mon Sep 17 00:00:00 2001 From: AnihilatorGun Date: Mon, 18 Nov 2024 22:10:59 +0300 Subject: [PATCH 21/42] fix :bonk: --- tests/test_convex_hull.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_convex_hull.py b/tests/test_convex_hull.py index b7a8ec2d..04e3621a 100644 --- a/tests/test_convex_hull.py +++ b/tests/test_convex_hull.py @@ -83,6 +83,6 @@ def test_convex_hull_image_qhullsrc_issues(): image[-2, -2] = True with pytest.warns(UserWarning): - chull = convex_hull_image_fast(image, offset_coordinates=offset_coordinates) + chull = convex_hull_image_fast(image, offset_coordinates=False) assert (chull == np.zeros_like(chull)).all() From bd4bce262f165efed41ed59fc8cbfeab9bcf6efb Mon Sep 17 00:00:00 2001 From: AnihilatorGun Date: Wed, 20 Nov 2024 12:54:03 +0300 Subject: [PATCH 22/42] mb fix?.. --- .github/workflows/test_build.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/test_build.yml b/.github/workflows/test_build.yml index 8f0d1718..7d30f644 100644 --- a/.github/workflows/test_build.yml +++ b/.github/workflows/test_build.yml @@ -32,6 +32,9 @@ jobs: ls /usr/local/bin/g++* gcc --version g++ --version + export PATH="/usr/local/opt/llvm/bin:$PATH" + export CC="/usr/local/opt/llvm/bin/clang" + export CXX="/usr/local/opt/llvm/bin/clang++" - name: Install g++-11 for ubuntu if: matrix.os == 'ubuntu-22.04' id: install_cc From 67842e0e151ea120f751ff0126b2d4c084b49c02 Mon Sep 17 00:00:00 2001 From: AnihilatorGun Date: Wed, 20 Nov 2024 15:25:18 +0300 Subject: [PATCH 23/42] revert --- .github/workflows/test_build.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/test_build.yml b/.github/workflows/test_build.yml index 7d30f644..8f0d1718 100644 --- a/.github/workflows/test_build.yml +++ b/.github/workflows/test_build.yml @@ -32,9 +32,6 @@ jobs: ls /usr/local/bin/g++* gcc --version g++ --version - export PATH="/usr/local/opt/llvm/bin:$PATH" - export CC="/usr/local/opt/llvm/bin/clang" - export CXX="/usr/local/opt/llvm/bin/clang++" - name: Install g++-11 for ubuntu if: matrix.os == 'ubuntu-22.04' id: install_cc From 73fdd649df40f8cea9edf415cfd59d7b3772e338 Mon Sep 17 00:00:00 2001 From: Philipenko Vladimir Date: Thu, 21 Nov 2024 01:56:18 +0300 Subject: [PATCH 24/42] mb macos-13 --- .github/workflows/test_build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test_build.yml b/.github/workflows/test_build.yml index 8f0d1718..5baa4da9 100644 --- a/.github/workflows/test_build.yml +++ b/.github/workflows/test_build.yml @@ -9,7 +9,7 @@ jobs: build_wheels: strategy: matrix: - os: [ubuntu-22.04, windows-2019, macOS-12 ] + os: [ubuntu-22.04, windows-2019, macOS-13 ] name: Build wheels on ${{ matrix.os }} runs-on: ${{ matrix.os }} @@ -22,7 +22,7 @@ jobs: - name: Install cibuildwheel run: python -m pip install cibuildwheel==2.17.0 - name: Install gcc for mac - if: matrix.os == 'macOS-12' + if: matrix.os == 'macOS-13' run: | brew install llvm libomp echo $PATH From cdfd4df2321a7a3fa586e9da8e0263d3d6b00282 Mon Sep 17 00:00:00 2001 From: Philipenko Vladimir Date: Thu, 21 Nov 2024 02:01:38 +0300 Subject: [PATCH 25/42] Revert "mb macos-13" This reverts commit 73fdd649df40f8cea9edf415cfd59d7b3772e338. --- .github/workflows/test_build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test_build.yml b/.github/workflows/test_build.yml index 5baa4da9..8f0d1718 100644 --- a/.github/workflows/test_build.yml +++ b/.github/workflows/test_build.yml @@ -9,7 +9,7 @@ jobs: build_wheels: strategy: matrix: - os: [ubuntu-22.04, windows-2019, macOS-13 ] + os: [ubuntu-22.04, windows-2019, macOS-12 ] name: Build wheels on ${{ matrix.os }} runs-on: ${{ matrix.os }} @@ -22,7 +22,7 @@ jobs: - name: Install cibuildwheel run: python -m pip install cibuildwheel==2.17.0 - name: Install gcc for mac - if: matrix.os == 'macOS-13' + if: matrix.os == 'macOS-12' run: | brew install llvm libomp echo $PATH From 5182ac8d062212ff5f1635b32f611aba7aabd6dd Mon Sep 17 00:00:00 2001 From: Philipenko Vladimir Date: Thu, 21 Nov 2024 02:04:43 +0300 Subject: [PATCH 26/42] mb this --- .github/workflows/test_build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_build.yml b/.github/workflows/test_build.yml index 8f0d1718..e72c172d 100644 --- a/.github/workflows/test_build.yml +++ b/.github/workflows/test_build.yml @@ -53,7 +53,7 @@ jobs: python -m cibuildwheel --output-dir wheelhouse env: CIBW_ENVIRONMENT_MACOS: > - PATH="/usr/local/opt/llvm/bin:$PATH" LDFLAGS="-L/usr/local/opt/llvm/lib" CPPFLAGS="-I/usr/local/opt/llvm/include" + PATH="/usr/local/opt/llvm/bin:$PATH" LDFLAGS="$LDFLAGS -Wl,-rpath,/usr/local/opt/libomp/lib -L/usr/local/opt/libomp/lib -lomp" CPPFLAGS="$CPPFLAGS -Xpreprocessor -fopenmp" CFLAGS="$CFLAGS -I/usr/local/opt/libomp/include" CXXFLAGS="$CXXFLAGS -I/usr/local/opt/libomp/include" CIBW_BUILD: cp37-* cp39-* cp312-* CIBW_SKIP: "*manylinux_x86_64" CIBW_BEFORE_BUILD_LINUX: 'if [ $(python -c "import sys; print(sys.version_info[1])") -ge 9 ]; then python -m pip install "numpy<3.0.0" --config-settings=setup-args="-Dallow-noblas=true"; fi' From a6f02ca76c7a5273928cb63b520c460aa0a5cc04 Mon Sep 17 00:00:00 2001 From: Vladimir Philipenko <32431463+vovaf709@users.noreply.github.com> Date: Fri, 22 Nov 2024 02:45:25 +0300 Subject: [PATCH 27/42] maybe this --- .github/workflows/test_build.yml | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/.github/workflows/test_build.yml b/.github/workflows/test_build.yml index e72c172d..a0c31d3e 100644 --- a/.github/workflows/test_build.yml +++ b/.github/workflows/test_build.yml @@ -24,14 +24,7 @@ jobs: - name: Install gcc for mac if: matrix.os == 'macOS-12' run: | - brew install llvm libomp - echo $PATH - ln -sf /usr/local/bin/gcc-11 /usr/local/bin/gcc - ln -sf /usr/local/bin/g++-11 /usr/local/bin/g++ - ls /usr/local/bin/gcc* - ls /usr/local/bin/g++* - gcc --version - g++ --version + brew install llvm@19.1.0 - name: Install g++-11 for ubuntu if: matrix.os == 'ubuntu-22.04' id: install_cc @@ -53,7 +46,7 @@ jobs: python -m cibuildwheel --output-dir wheelhouse env: CIBW_ENVIRONMENT_MACOS: > - PATH="/usr/local/opt/llvm/bin:$PATH" LDFLAGS="$LDFLAGS -Wl,-rpath,/usr/local/opt/libomp/lib -L/usr/local/opt/libomp/lib -lomp" CPPFLAGS="$CPPFLAGS -Xpreprocessor -fopenmp" CFLAGS="$CFLAGS -I/usr/local/opt/libomp/include" CXXFLAGS="$CXXFLAGS -I/usr/local/opt/libomp/include" + CC="/usr/local/Cellar/llvm/19.1.0/bin/clang" CXX="/usr/local/Cellar/llvm/19.1.0/bin/clang++" CIBW_BUILD: cp37-* cp39-* cp312-* CIBW_SKIP: "*manylinux_x86_64" CIBW_BEFORE_BUILD_LINUX: 'if [ $(python -c "import sys; print(sys.version_info[1])") -ge 9 ]; then python -m pip install "numpy<3.0.0" --config-settings=setup-args="-Dallow-noblas=true"; fi' From f9f34fe61e3dfa4833dd2da071023d382b29325f Mon Sep 17 00:00:00 2001 From: Vladimir Philipenko <32431463+vovaf709@users.noreply.github.com> Date: Fri, 22 Nov 2024 02:52:36 +0300 Subject: [PATCH 28/42] Fix --- .github/workflows/test_build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test_build.yml b/.github/workflows/test_build.yml index a0c31d3e..ff0e219a 100644 --- a/.github/workflows/test_build.yml +++ b/.github/workflows/test_build.yml @@ -24,7 +24,7 @@ jobs: - name: Install gcc for mac if: matrix.os == 'macOS-12' run: | - brew install llvm@19.1.0 + brew install llvm - name: Install g++-11 for ubuntu if: matrix.os == 'ubuntu-22.04' id: install_cc @@ -46,7 +46,7 @@ jobs: python -m cibuildwheel --output-dir wheelhouse env: CIBW_ENVIRONMENT_MACOS: > - CC="/usr/local/Cellar/llvm/19.1.0/bin/clang" CXX="/usr/local/Cellar/llvm/19.1.0/bin/clang++" + CC="/opt/homebrew/opt/llvm/bin/clang" CXX="/opt/homebrew/opt/llvm/bin/clang++" CIBW_BUILD: cp37-* cp39-* cp312-* CIBW_SKIP: "*manylinux_x86_64" CIBW_BEFORE_BUILD_LINUX: 'if [ $(python -c "import sys; print(sys.version_info[1])") -ge 9 ]; then python -m pip install "numpy<3.0.0" --config-settings=setup-args="-Dallow-noblas=true"; fi' From 239416f5166e7d2556f5b2943df517175401ba3c Mon Sep 17 00:00:00 2001 From: Vladimir Philipenko <32431463+vovaf709@users.noreply.github.com> Date: Fri, 22 Nov 2024 10:58:45 +0300 Subject: [PATCH 29/42] Fix --- .github/workflows/test_build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_build.yml b/.github/workflows/test_build.yml index ff0e219a..eab4cd34 100644 --- a/.github/workflows/test_build.yml +++ b/.github/workflows/test_build.yml @@ -46,7 +46,7 @@ jobs: python -m cibuildwheel --output-dir wheelhouse env: CIBW_ENVIRONMENT_MACOS: > - CC="/opt/homebrew/opt/llvm/bin/clang" CXX="/opt/homebrew/opt/llvm/bin/clang++" + CC="/usr/local/Cellar/llvm/19.1.0/bin/clang" CXX="/usr/local/Cellar/llvm/19.1.0/bin/clang++" CIBW_BUILD: cp37-* cp39-* cp312-* CIBW_SKIP: "*manylinux_x86_64" CIBW_BEFORE_BUILD_LINUX: 'if [ $(python -c "import sys; print(sys.version_info[1])") -ge 9 ]; then python -m pip install "numpy<3.0.0" --config-settings=setup-args="-Dallow-noblas=true"; fi' From ffad7ac9bb9f436b6fdca5a80ffd6d0cacbd971d Mon Sep 17 00:00:00 2001 From: Vladimir Filipenko Date: Sat, 23 Nov 2024 01:26:32 +0300 Subject: [PATCH 30/42] fixes for macos --- .github/workflows/release.yml | 17 +++++------------ .github/workflows/test_build.yml | 6 +++--- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index acd64184..ff11312f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -35,7 +35,7 @@ jobs: needs: [ check_version ] strategy: matrix: - os: [ ubuntu-22.04, windows-2019, macOS-12 ] + os: [ ubuntu-22.04, windows-2019, macOS-13 ] name: Build wheels on ${{ matrix.os }} runs-on: ${{ matrix.os }} @@ -47,17 +47,10 @@ jobs: python-version: '3.9' - name: Install cibuildwheel run: python -m pip install cibuildwheel==2.17.0 - - name: Install gcc for mac - if: matrix.os == 'macOS-12' + - name: Install llvm for mac + if: matrix.os == 'macOS-13' run: | - brew install llvm libomp - echo $PATH - ln -sf /usr/local/bin/gcc-11 /usr/local/bin/gcc - ln -sf /usr/local/bin/g++-11 /usr/local/bin/g++ - ls /usr/local/bin/gcc* - ls /usr/local/bin/g++* - gcc --version - g++ --version + brew install llvm - name: Install g++-11 for ubuntu if: matrix.os == 'ubuntu-22.04' id: install_cc @@ -79,7 +72,7 @@ jobs: python -m cibuildwheel --output-dir wheelhouse env: CIBW_ENVIRONMENT_MACOS: > - PATH="/usr/local/opt/llvm/bin:$PATH" LDFLAGS="-L/usr/local/opt/llvm/lib" CPPFLAGS="-I/usr/local/opt/llvm/include" + CC="/usr/local/Cellar/llvm/19.1.0/bin/clang" CXX="/usr/local/Cellar/llvm/19.1.0/bin/clang++" CIBW_BUILD: cp37-* cp38-* cp39-* cp310-* cp311-* cp312-* CIBW_BEFORE_BUILD_LINUX: 'if [ $(python -c "import sys; print(sys.version_info[1])") -ge 9 ]; then python -m pip install "numpy<3.0.0" --config-settings=setup-args="-Dallow-noblas=true"; fi' - uses: actions/upload-artifact@v3 diff --git a/.github/workflows/test_build.yml b/.github/workflows/test_build.yml index eab4cd34..0d33d1e4 100644 --- a/.github/workflows/test_build.yml +++ b/.github/workflows/test_build.yml @@ -9,7 +9,7 @@ jobs: build_wheels: strategy: matrix: - os: [ubuntu-22.04, windows-2019, macOS-12 ] + os: [ubuntu-22.04, windows-2019, macOS-13 ] name: Build wheels on ${{ matrix.os }} runs-on: ${{ matrix.os }} @@ -21,8 +21,8 @@ jobs: python-version: '3.9' - name: Install cibuildwheel run: python -m pip install cibuildwheel==2.17.0 - - name: Install gcc for mac - if: matrix.os == 'macOS-12' + - name: Install llvm for mac + if: matrix.os == 'macOS-13' run: | brew install llvm - name: Install g++-11 for ubuntu From 4d5c22e7f76ebbecdd848a2c6f26b7c50773a511 Mon Sep 17 00:00:00 2001 From: Vladimir Filipenko Date: Sat, 23 Nov 2024 01:44:54 +0300 Subject: [PATCH 31/42] Fix `normalize_num_threads` for macos --- imops/utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/imops/utils.py b/imops/utils.py index 4fe7ad3e..d1c58f99 100644 --- a/imops/utils.py +++ b/imops/utils.py @@ -1,4 +1,5 @@ import os +import platform from contextlib import contextmanager from itertools import permutations from typing import Callable, Optional, Sequence, Tuple, Union @@ -53,7 +54,7 @@ def normalize_num_threads(num_threads: int, backend: Backend, warn_stacklevel: i env_num_threads = os.environ.get(env_num_threads_var_name, '').strip() env_num_threads = int(env_num_threads) if env_num_threads else None # TODO: maybe let user set the absolute maximum number of threads? - num_available_cpus = len(os.sched_getaffinity(0)) + num_available_cpus = os.cpu_count() if platform.system() == 'Darwin' else len(os.sched_getaffinity(0)) max_num_threads = min(filter(bool, [IMOPS_NUM_THREADS, env_num_threads, num_available_cpus])) From 0106fa02eb25352652ae30a0efeca811e8e1d816 Mon Sep 17 00:00:00 2001 From: Vladimir Filipenko Date: Sat, 23 Nov 2024 01:56:01 +0300 Subject: [PATCH 32/42] Fix --- .github/workflows/release.yml | 2 +- .github/workflows/test_build.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ff11312f..cd1d9baa 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -72,7 +72,7 @@ jobs: python -m cibuildwheel --output-dir wheelhouse env: CIBW_ENVIRONMENT_MACOS: > - CC="/usr/local/Cellar/llvm/19.1.0/bin/clang" CXX="/usr/local/Cellar/llvm/19.1.0/bin/clang++" + CC="$(brew --prefix llvm)/bin/clang" CXX="$(brew --prefix llvm)/bin/clang++" CIBW_BUILD: cp37-* cp38-* cp39-* cp310-* cp311-* cp312-* CIBW_BEFORE_BUILD_LINUX: 'if [ $(python -c "import sys; print(sys.version_info[1])") -ge 9 ]; then python -m pip install "numpy<3.0.0" --config-settings=setup-args="-Dallow-noblas=true"; fi' - uses: actions/upload-artifact@v3 diff --git a/.github/workflows/test_build.yml b/.github/workflows/test_build.yml index 0d33d1e4..d5022263 100644 --- a/.github/workflows/test_build.yml +++ b/.github/workflows/test_build.yml @@ -46,7 +46,7 @@ jobs: python -m cibuildwheel --output-dir wheelhouse env: CIBW_ENVIRONMENT_MACOS: > - CC="/usr/local/Cellar/llvm/19.1.0/bin/clang" CXX="/usr/local/Cellar/llvm/19.1.0/bin/clang++" + CC="$(brew --prefix llvm)/bin/clang" CXX="$(brew --prefix llvm)/bin/clang++" CIBW_BUILD: cp37-* cp39-* cp312-* CIBW_SKIP: "*manylinux_x86_64" CIBW_BEFORE_BUILD_LINUX: 'if [ $(python -c "import sys; print(sys.version_info[1])") -ge 9 ]; then python -m pip install "numpy<3.0.0" --config-settings=setup-args="-Dallow-noblas=true"; fi' From 170eee416f8090a16e37d3f96fefd93690a79d90 Mon Sep 17 00:00:00 2001 From: Vladimir Filipenko Date: Sun, 24 Nov 2024 01:28:16 +0300 Subject: [PATCH 33/42] some :nail_care: --- imops/morphology.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/imops/morphology.py b/imops/morphology.py index 1d9466ab..0be6a6c1 100644 --- a/imops/morphology.py +++ b/imops/morphology.py @@ -529,7 +529,7 @@ def distance_transform_edt( return None -def convex_hull_image(image, offset_coordinates=True): +def convex_hull_image(image: np.ndarray, offset_coordinates: bool = True) -> np.ndarray: """ Fast convex hull of an image. Similar to skimage.morphology.convex_hull_image with include_borders=True @@ -560,7 +560,7 @@ def convex_hull_image(image, offset_coordinates=True): if np.count_nonzero(image) == 0: warn( - 'Input image is entirely zero, no valid convex hull. ' 'Returning empty image', + 'Input image is entirely zero, no valid convex hull. Returning empty image', UserWarning, ) return np.zeros(image.shape, dtype=bool) From db8f9a864049dece2758fe9fc3ea6bbddf63030a Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 30 Nov 2024 11:19:19 +0100 Subject: [PATCH 34/42] improved package structure --- .github/workflows/lint.yml | 27 ++++++++++++++++++++++++ .github/workflows/tests.yml | 11 ---------- .gitignore | 1 + MANIFEST.in | 2 +- _build_utils.py => imops/_build_utils.py | 2 +- imops/compat.py | 11 ++++++++-- imops/morphology.py | 12 ++--------- pyproject.toml | 20 ++++++++---------- setup.py | 9 ++++---- 9 files changed, 55 insertions(+), 40 deletions(-) create mode 100644 .github/workflows/lint.yml rename _build_utils.py => imops/_build_utils.py (98%) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 00000000..7b50bf31 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,27 @@ +name: Lint + +on: [ pull_request ] + +env: + MODULE_NAME: imops + +jobs: + lint: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Check python code style + run: | + pip install -r requirements-linters.txt + flake8 . + isort --check . + black --check . + - name: Check Cython code style + run: | + pip install cython-lint + cython-lint imops/src diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 627ada08..634a26d5 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -43,17 +43,6 @@ jobs: echo $MODULE_PARENT echo "MODULE_PARENT=$(echo $MODULE_PARENT)" >> $GITHUB_ENV - - name: Check python code style - run: | - pip install -r requirements-linters.txt - flake8 . - isort --check . - black --check . - - name: Check Cython code style - run: | - pip install cython-lint - cython-lint imops/src - - name: Test with pytest run: | pytest tests -m "not nonumba" --junitxml=reports/junit-${{ matrix.python-version }}.xml --cov="$MODULE_PARENT/$MODULE_NAME" --cov-report=xml --cov-branch diff --git a/.gitignore b/.gitignore index 87e1733b..8d93aa0a 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ imops/src/_fast*.pyx dist/ *.so .vscode/ +.idea/ diff --git a/MANIFEST.in b/MANIFEST.in index d25ac5dc..c77969d0 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,6 @@ include *.md include requirements.txt include pyproject.toml -include _build_utils.py recursive-include imops *.py recursive-include imops/cpp *.h *.hpp *.cpp +exclude tests/* diff --git a/_build_utils.py b/imops/_build_utils.py similarity index 98% rename from _build_utils.py rename to imops/_build_utils.py index 59292e31..12c61668 100644 --- a/_build_utils.py +++ b/imops/_build_utils.py @@ -66,7 +66,7 @@ def get_ext_modules(): modules = ['backprojection', 'measure', 'morphology', 'numeric', 'radon', 'zoom', 'convex_hull'] modules_to_link_against_numpy_core_math_lib = ['numeric'] - src_dir = Path(__file__).parent / name / 'src' + src_dir = Path(__file__).parent / 'src' for module in modules: # Cython extension and .pyx source file names must be the same to compile # https://stackoverflow.com/questions/8024805/cython-compiled-c-extension-importerror-dynamic-module-does-not-define-init-fu diff --git a/imops/compat.py b/imops/compat.py index 07a8a1df..0d0b2eed 100644 --- a/imops/compat.py +++ b/imops/compat.py @@ -9,6 +9,13 @@ from numpy import VisibleDeprecationWarning try: - from scipy.ndimage._morphology import _ni_support + from scipy.ndimage._morphology._ni_support import _normalize_sequence as normalize_sequence except ImportError: - from scipy.ndimage.morphology import _ni_support + from scipy.ndimage.morphology._ni_support import _normalize_sequence as normalize_sequence + +try: + from scipy.spatial import QhullError +except ImportError: + from scipy.spatial.qhull import QhullError + +from scipy.ndimage._nd_image import euclidean_feature_transform # noqa diff --git a/imops/morphology.py b/imops/morphology.py index 0be6a6c1..39d45a13 100644 --- a/imops/morphology.py +++ b/imops/morphology.py @@ -4,15 +4,7 @@ import numpy as np from edt import edt from scipy.ndimage import distance_transform_edt as scipy_distance_transform_edt, generate_binary_structure -from scipy.ndimage._nd_image import euclidean_feature_transform from scipy.spatial import ConvexHull - - -try: - from scipy.spatial import QhullError -except ImportError: - from scipy.spatial.qhull import QhullError # Old scipy has another structure - from skimage.morphology import ( binary_closing as scipy_binary_closing, binary_dilation as scipy_binary_dilation, @@ -23,7 +15,7 @@ from .backend import BackendLike, Cython, Scipy, resolve_backend from .box import add_margin, box_to_shape, mask_to_box, shape_to_box -from .compat import _ni_support +from .compat import QhullError, euclidean_feature_transform, normalize_sequence from .crop import crop_to_box from .pad import restore_crop from .src._convex_hull import _grid_points_in_poly, _left_right_bounds, _offset_unique @@ -499,7 +491,7 @@ def distance_transform_edt( if image.dtype != bool: image = np.atleast_1d(np.where(image, 1, 0)) if sampling is not None: - sampling = _ni_support._normalize_sequence(sampling, image.ndim) + sampling = normalize_sequence(sampling, image.ndim) sampling = np.asarray(sampling, dtype=np.float64) if not sampling.flags.contiguous: sampling = sampling.copy() diff --git a/pyproject.toml b/pyproject.toml index eed6e582..2f7a669b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,17 +4,17 @@ build-backend = 'setuptools.build_meta' [project] name = 'imops' -dynamic = ['version'] +dynamic = ['dependencies', 'version'] description = 'Efficient parallelizable algorithms for multidimensional arrays to speed up your data pipelines' readme = 'README.md' requires-python = '>=3.7' license = { file = 'LICENSE' } keywords = ['image processing', 'fast', 'ndarray', 'data pipelines'] authors = [ - {name = 'maxme1', email = 'max@aumi.ai'}, - {name = 'vovaf709', email = 'vovaf709@yandex.ru'}, - {name = 'talgat', email = 'saparov2130@gmail.com'}, - {name = 'alexeybelkov', email='fpmbelkov@gmail.com'} + { name = 'maxme1', email = 'max@aumi.ai' }, + { name = 'vovaf709', email = 'vovaf709@yandex.ru' }, + { name = 'talgat', email = 'saparov2130@gmail.com' }, + { name = 'alexeybelkov', email = 'fpmbelkov@gmail.com' } ] classifiers = [ 'Development Status :: 5 - Production/Stable', @@ -51,23 +51,21 @@ line_length = 120 lines_after_imports = 2 profile = 'black' combine_as_imports = true -skip_glob=['.asv/*', '.eggs/*'] +skip_glob = ['.asv/*', '.eggs/*'] [tool.cython-lint] max-line-length = 120 -[tool.setuptools] -py-modules = ['_build_utils'] - [tool.setuptools.cmdclass] -build_py = "_build_utils.PyprojectBuild" +build_py = "imops._build_utils.PyprojectBuild" [tool.setuptools.packages.find] include = ['imops'] +exclude = ['tests'] [tool.setuptools.package-data] imops = ['py.typed'] [tool.setuptools.dynamic] -version = {attr = 'imops.__version__.__version__'} +version = { attr = 'imops.__version__.__version__' } dependencies = { file = ['requirements.txt'] } diff --git a/setup.py b/setup.py index e6e3c50e..a2fee0c8 100644 --- a/setup.py +++ b/setup.py @@ -23,13 +23,14 @@ long_description = file.read() version = runpy.run_path(root / name / '__version__.py')['__version__'] -scope = {'__file__': __file__} -exec((root / '_build_utils.py').read_text(), scope) +build_utils = root / name / '_build_utils.py' +scope = {'__file__': str(build_utils)} +exec(build_utils.read_text(), scope) ext_modules = scope['get_ext_modules']() setup( name=name, - packages=find_packages(include=(name,)), + packages=find_packages(include=(name,), exclude=('tests', 'tests.*')), include_package_data=True, version=version, description='Efficient parallelizable algorithms for multidimensional arrays to speed up your data pipelines', @@ -51,8 +52,8 @@ extras_require={'numba': ['numba'], 'all': ['numba']}, setup_requires=[ 'setuptools<69.0.0', - 'Cython>=3.0.0,<4.0.0', 'numpy<3.0.0', + 'Cython>=3.0.0,<4.0.0', 'pybind11', ], ext_modules=ext_modules, From 9eee5150853720bf9147555b6d33d9fe79fa69c2 Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 30 Nov 2024 11:37:21 +0100 Subject: [PATCH 35/42] fixed compat --- imops/compat.py | 7 +++++-- imops/crop.py | 10 +++++++--- imops/interp1d.py | 4 ++-- imops/interp2d.py | 7 ++++--- imops/measure.py | 12 ++++++------ imops/morphology.py | 24 ++++++++++++------------ imops/numeric.py | 8 ++++---- imops/pad.py | 8 ++++---- imops/radon.py | 8 ++++---- imops/utils.py | 4 +++- imops/zoom.py | 8 ++++---- 11 files changed, 55 insertions(+), 45 deletions(-) diff --git a/imops/compat.py b/imops/compat.py index 0d0b2eed..5cfa7b8a 100644 --- a/imops/compat.py +++ b/imops/compat.py @@ -9,9 +9,9 @@ from numpy import VisibleDeprecationWarning try: - from scipy.ndimage._morphology._ni_support import _normalize_sequence as normalize_sequence + from scipy.ndimage._morphology import _ni_support except ImportError: - from scipy.ndimage.morphology._ni_support import _normalize_sequence as normalize_sequence + from scipy.ndimage.morphology import _ni_support try: from scipy.spatial import QhullError @@ -19,3 +19,6 @@ from scipy.spatial.qhull import QhullError from scipy.ndimage._nd_image import euclidean_feature_transform # noqa + + +normalize_sequence = _ni_support._normalize_sequence # noqa diff --git a/imops/crop.py b/imops/crop.py index d1e1a772..dbb2c331 100644 --- a/imops/crop.py +++ b/imops/crop.py @@ -1,3 +1,5 @@ +from typing import Optional + import numpy as np from .backend import BackendLike @@ -6,7 +8,9 @@ from .utils import AxesLike, AxesParams, assert_subdtype, broadcast_axis, fill_by_indices -def crop_to_shape(x: np.ndarray, shape: AxesLike, axis: AxesLike = None, ratio: AxesParams = 0.5) -> np.ndarray: +def crop_to_shape( + x: np.ndarray, shape: AxesLike, axis: Optional[AxesLike] = None, ratio: AxesParams = 0.5 +) -> np.ndarray: """ Crop `x` to match `shape` along `axis`. @@ -57,8 +61,8 @@ def crop_to_shape(x: np.ndarray, shape: AxesLike, axis: AxesLike = None, ratio: def crop_to_box( x: np.ndarray, box: np.ndarray, - axis: AxesLike = None, - padding_values: AxesParams = None, + axis: Optional[AxesLike] = None, + padding_values: Optional[AxesParams] = None, num_threads: int = _NUMERIC_DEFAULT_NUM_THREADS, backend: BackendLike = None, ) -> np.ndarray: diff --git a/imops/interp1d.py b/imops/interp1d.py index 269cf217..861a0b69 100644 --- a/imops/interp1d.py +++ b/imops/interp1d.py @@ -1,4 +1,4 @@ -from typing import Union +from typing import Optional, Union from warnings import warn import numpy as np @@ -71,7 +71,7 @@ def __init__( kind: Union[int, str] = 'linear', axis: int = -1, copy: bool = True, - bounds_error: bool = None, + bounds_error: Optional[bool] = None, fill_value: Union[float, str] = np.nan, assume_sorted: bool = False, num_threads: int = -1, diff --git a/imops/interp2d.py b/imops/interp2d.py index 5a3df754..306801c4 100644 --- a/imops/interp2d.py +++ b/imops/interp2d.py @@ -1,4 +1,5 @@ from platform import python_version +from typing import Optional import numpy as np from scipy.spatial import KDTree @@ -47,9 +48,9 @@ class Linear2DInterpolator(Linear2DInterpolatorCpp): def __init__( self, points: np.ndarray, - values: np.ndarray = None, + values: Optional[np.ndarray] = None, num_threads: int = 1, - triangles: np.ndarray = None, + triangles: Optional[np.ndarray] = None, **kwargs, ): if triangles is not None: @@ -77,7 +78,7 @@ def __init__( # FIXME: add backend dispatch self.num_threads = normalize_num_threads(num_threads, Cython(), warn_stacklevel=3) - def __call__(self, points: np.ndarray, values: np.ndarray = None, fill_value: float = 0.0) -> np.ndarray: + def __call__(self, points: np.ndarray, values: Optional[np.ndarray] = None, fill_value: float = 0.0) -> np.ndarray: """ Evaluate the interpolant diff --git a/imops/measure.py b/imops/measure.py index 056f2286..1a716826 100644 --- a/imops/measure.py +++ b/imops/measure.py @@ -1,6 +1,6 @@ from collections import namedtuple from platform import python_version -from typing import List, NamedTuple, Sequence, Tuple, Union +from typing import List, NamedTuple, Optional, Sequence, Tuple, Union from warnings import warn import numpy as np @@ -32,12 +32,12 @@ # TODO: Make it work and test on immutable arrays as soon as `cc3d` package is fixed def label( label_image: np.ndarray, - background: int = None, - connectivity: int = None, + background: Optional[int] = None, + connectivity: Optional[int] = None, return_num: bool = False, return_labels: bool = False, return_sizes: bool = False, - dtype: type = None, + dtype: Optional[type] = None, ) -> Union[np.ndarray, NamedTuple]: """ Fast version of `skimage.measure.label` which optionally returns number of connected components, labels and sizes. @@ -139,8 +139,8 @@ def label( def center_of_mass( array: np.ndarray, - labels: np.ndarray = None, - index: Union[int, Sequence[int]] = None, + labels: Union[np.ndarray, None] = None, + index: Union[int, Sequence[int], None] = None, num_threads: int = -1, backend: BackendLike = None, ) -> Union[Tuple[float, ...], List[Tuple[float, ...]]]: diff --git a/imops/morphology.py b/imops/morphology.py index 39d45a13..a57243c6 100644 --- a/imops/morphology.py +++ b/imops/morphology.py @@ -1,4 +1,4 @@ -from typing import Callable, Tuple, Union +from typing import Callable, Optional, Tuple, Union from warnings import warn import numpy as np @@ -32,8 +32,8 @@ def morphology_op_wrapper( ) -> Callable: def wrapped( image: np.ndarray, - footprint: np.ndarray = None, - output: np.ndarray = None, + footprint: Optional[np.ndarray] = None, + output: Optional[np.ndarray] = None, boxed: bool = False, num_threads: int = -1, backend: BackendLike = None, @@ -163,8 +163,8 @@ def wrapped( def binary_dilation( image: np.ndarray, - footprint: np.ndarray = None, - output: np.ndarray = None, + footprint: Optional[np.ndarray] = None, + output: Optional[np.ndarray] = None, boxed: bool = False, num_threads: int = -1, backend: BackendLike = None, @@ -217,8 +217,8 @@ def binary_dilation( def binary_erosion( image: np.ndarray, - footprint: np.ndarray = None, - output: np.ndarray = None, + footprint: Optional[np.ndarray] = None, + output: Optional[np.ndarray] = None, boxed: bool = False, num_threads: int = -1, backend: BackendLike = None, @@ -271,8 +271,8 @@ def binary_erosion( def binary_closing( image: np.ndarray, - footprint: np.ndarray = None, - output: np.ndarray = None, + footprint: Optional[np.ndarray] = None, + output: Optional[np.ndarray] = None, boxed: bool = False, num_threads: int = -1, backend: BackendLike = None, @@ -326,8 +326,8 @@ def binary_closing( def binary_opening( image: np.ndarray, - footprint: np.ndarray = None, - output: np.ndarray = None, + footprint: Optional[np.ndarray] = None, + output: Optional[np.ndarray] = None, boxed: bool = False, num_threads: int = -1, backend: BackendLike = None, @@ -371,7 +371,7 @@ def binary_opening( def distance_transform_edt( image: np.ndarray, - sampling: Tuple[float] = None, + sampling: Optional[Tuple[float]] = None, return_distances: bool = True, return_indices: bool = False, num_threads: int = -1, diff --git a/imops/numeric.py b/imops/numeric.py index 8cca57f4..e9259589 100644 --- a/imops/numeric.py +++ b/imops/numeric.py @@ -1,4 +1,4 @@ -from typing import Callable, Sequence, Union +from typing import Callable, Optional, Sequence, Union import numpy as np @@ -99,7 +99,7 @@ def _choose_cython_copy(ndim: int, is_fp16: bool, fast: bool) -> Callable: def pointwise_add( nums: np.ndarray, summand: Union[np.array, int, float], - output: np.ndarray = None, + output: Optional[np.ndarray] = None, num_threads: int = _NUMERIC_DEFAULT_NUM_THREADS, backend: BackendLike = None, ) -> np.ndarray: @@ -256,7 +256,7 @@ def fill_( def full( shape: Union[int, Sequence[int]], fill_value: Union[np.number, int, float], - dtype: Union[type, str] = None, + dtype: Union[type, str, None] = None, order: str = 'C', num_threads: int = _NUMERIC_DEFAULT_NUM_THREADS, backend: BackendLike = None, @@ -302,7 +302,7 @@ def full( def copy( nums: np.ndarray, - output: np.ndarray = None, + output: Optional[np.ndarray] = None, order: str = 'K', num_threads: int = _NUMERIC_DEFAULT_NUM_THREADS, backend: BackendLike = None, diff --git a/imops/pad.py b/imops/pad.py index b277dcea..5aa0e64d 100644 --- a/imops/pad.py +++ b/imops/pad.py @@ -1,4 +1,4 @@ -from typing import Callable, Sequence, Union +from typing import Callable, Optional, Sequence, Union import numpy as np @@ -10,7 +10,7 @@ def pad( x: np.ndarray, padding: Union[AxesLike, Sequence[Sequence[int]]], - axis: AxesLike = None, + axis: Optional[AxesLike] = None, padding_values: Union[AxesParams, Callable] = 0, num_threads: int = _NUMERIC_DEFAULT_NUM_THREADS, backend: BackendLike = None, @@ -76,7 +76,7 @@ def pad( def pad_to_shape( x: np.ndarray, shape: AxesLike, - axis: AxesLike = None, + axis: Optional[AxesLike] = None, padding_values: Union[AxesParams, Callable] = 0, ratio: AxesParams = 0.5, num_threads: int = _NUMERIC_DEFAULT_NUM_THREADS, @@ -135,7 +135,7 @@ def pad_to_shape( def pad_to_divisible( x: np.ndarray, divisor: AxesLike, - axis: AxesLike = None, + axis: Optional[AxesLike] = None, padding_values: Union[AxesParams, Callable] = 0, ratio: AxesParams = 0.5, remainder: AxesLike = 0, diff --git a/imops/radon.py b/imops/radon.py index a3d122b5..5c068f58 100644 --- a/imops/radon.py +++ b/imops/radon.py @@ -1,4 +1,4 @@ -from typing import Sequence, Tuple, Union +from typing import Optional, Sequence, Tuple, Union import numpy as np from scipy.fftpack import fft, ifft @@ -15,7 +15,7 @@ def radon( image: np.ndarray, - axes: Tuple[int, int] = None, + axes: Optional[Tuple[int, int]] = None, theta: Union[int, Sequence[float]] = 180, return_fill: bool = False, num_threads: int = -1, @@ -104,8 +104,8 @@ def radon( def inverse_radon( sinogram: np.ndarray, - axes: Tuple[int, int] = None, - theta: Union[int, Sequence[float]] = None, + axes: Optional[Tuple[int, int]] = None, + theta: Union[int, Sequence[float], None] = None, fill_value: float = 0, a: float = 0, b: float = 1, diff --git a/imops/utils.py b/imops/utils.py index d1c58f99..4b1bd971 100644 --- a/imops/utils.py +++ b/imops/utils.py @@ -169,7 +169,9 @@ def wrapper( return wrapper -def build_slices(start: Sequence[int], stop: Sequence[int] = None, step: Sequence[int] = None) -> Tuple[slice, ...]: +def build_slices( + start: Sequence[int], stop: Optional[Sequence[int]] = None, step: Optional[Sequence[int]] = None +) -> Tuple[slice, ...]: """ Returns a tuple of slices built from `start` and `stop` with `step`. diff --git a/imops/zoom.py b/imops/zoom.py index a3ceb9f0..b5a0bdeb 100644 --- a/imops/zoom.py +++ b/imops/zoom.py @@ -1,5 +1,5 @@ from platform import python_version -from typing import Callable, Sequence, Union +from typing import Callable, Optional, Sequence, Union from warnings import warn import numpy as np @@ -72,7 +72,7 @@ def _choose_numba_zoom(ndim: int, order: int) -> Callable: def zoom( x: np.ndarray, scale_factor: AxesParams, - axis: AxesLike = None, + axis: Optional[AxesLike] = None, order: int = 1, fill_value: Union[float, Callable] = 0, num_threads: int = -1, @@ -129,7 +129,7 @@ def zoom( def zoom_to_shape( x: np.ndarray, shape: AxesLike, - axis: AxesLike = None, + axis: Optional[AxesLike] = None, order: int = 1, fill_value: Union[float, Callable] = 0, num_threads: int = -1, @@ -191,7 +191,7 @@ def zoom_to_shape( def _zoom( image: np.ndarray, zoom: Sequence[float], - output: np.ndarray = None, + output: Optional[np.ndarray] = None, order: int = 1, mode: str = 'constant', cval: float = 0.0, From 091fe012ca6a422ca3a3eaffaf8b9c28a51bdd51 Mon Sep 17 00:00:00 2001 From: AnihilatorGun Date: Mon, 2 Dec 2024 23:29:12 +0300 Subject: [PATCH 36/42] morphology --- imops/morphology.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imops/morphology.py b/imops/morphology.py index 1d9466ab..3dfb9c87 100644 --- a/imops/morphology.py +++ b/imops/morphology.py @@ -556,7 +556,7 @@ def convex_hull_image(image, offset_coordinates=True): ndim = image.ndim if ndim != 2: - raise RuntimeError(f'convex_hull_image is currently implemented only for 2D arrays, got {ndim}D array') + raise ValueError(f'convex_hull_image is currently implemented only for 2D arrays, got {ndim}D array') if np.count_nonzero(image) == 0: warn( From 109fa1239a45b6bd189e8c79b8fd448984266cef Mon Sep 17 00:00:00 2001 From: AnihilatorGun Date: Mon, 2 Dec 2024 23:33:11 +0300 Subject: [PATCH 37/42] fix tests --- tests/test_convex_hull.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/test_convex_hull.py b/tests/test_convex_hull.py index 04e3621a..4c160334 100644 --- a/tests/test_convex_hull.py +++ b/tests/test_convex_hull.py @@ -57,14 +57,16 @@ def test_convex_hull_image(offset_coordinates): chull = convex_hull_image_fast(image, offset_coordinates=offset_coordinates) - assert not (chull < image).any() - assert not (chull < chull_ref).any() + assert (chull >= image).all() + assert (chull >= chull_ref).all() + + assert ((chull > chull_ref).sum() / chull_ref.sum()) < 1e-2 def test_convex_hull_image_non2d(offset_coordinates): image = np.zeros((3, 3, 3), dtype=bool) - with pytest.raises(RuntimeError): + with pytest.raises(ValueError): _ = convex_hull_image_fast(image, offset_coordinates=offset_coordinates) From 92e50e8a7839d08c31f4c852b9b5e97c1bcddec9 Mon Sep 17 00:00:00 2001 From: AnihilatorGun Date: Mon, 2 Dec 2024 23:37:34 +0300 Subject: [PATCH 38/42] randomized data test --- tests/test_convex_hull.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/tests/test_convex_hull.py b/tests/test_convex_hull.py index 4c160334..24c84360 100644 --- a/tests/test_convex_hull.py +++ b/tests/test_convex_hull.py @@ -9,6 +9,10 @@ from imops.src._convex_hull import _left_right_bounds, _offset_unique +np.random.seed(1337) +N_STRESS = 250 + + @pytest.fixture(params=[False, True]) def offset_coordinates(request): return request.param @@ -60,7 +64,26 @@ def test_convex_hull_image(offset_coordinates): assert (chull >= image).all() assert (chull >= chull_ref).all() - assert ((chull > chull_ref).sum() / chull_ref.sum()) < 1e-2 + assert ((chull > chull_ref).sum() / chull_ref.sum()) < 2e-2 + + +def test_convex_hull_image_random(offset_coordinates): + for _ in range(N_STRESS): + image = np.zeros((200, 200), dtype=bool) + + image[30:-30, 20:-40] = np.random.randn(140, 140) > 2 + + try: + chull_ref = convex_hull_image(image, offset_coordinates=offset_coordinates, include_borders=True) + except TypeError: + chull_ref = convex_hull_image(image, offset_coordinates=offset_coordinates) + + chull = convex_hull_image_fast(image, offset_coordinates=offset_coordinates) + + assert (chull >= image).all() + assert (chull >= chull_ref).all() + + assert ((chull > chull_ref).sum() / chull_ref.sum()) < 2e-2 def test_convex_hull_image_non2d(offset_coordinates): From 8fd1d5687c24be8a48b7965451e9abef0206a3d4 Mon Sep 17 00:00:00 2001 From: AnihilatorGun Date: Wed, 4 Dec 2024 16:21:16 +0300 Subject: [PATCH 39/42] a bit cooler hull with smaller error --- imops/src/_convex_hull.pyx | 4 +-- tests/test_convex_hull.py | 50 ++++++++++++++++++++------------------ 2 files changed, 28 insertions(+), 26 deletions(-) diff --git a/imops/src/_convex_hull.pyx b/imops/src/_convex_hull.pyx index 9fb52a24..351a70b1 100644 --- a/imops/src/_convex_hull.pyx +++ b/imops/src/_convex_hull.pyx @@ -103,8 +103,8 @@ def _grid_points_in_poly(float[:] vx, float[:] vy, Py_ssize_t M, Py_ssize_t N, P cdef inline intBound intify(fpBound bound, Py_ssize_t min_idx, Py_ssize_t max_idx): if bound.assigned: return intBound( - lb = max(min_idx, floorf(bound.lb)), - rb = min(max_idx, ceilf(bound.rb)), + lb = max(min_idx, ceilf(bound.lb - 0.2)), + rb = min(max_idx, floorf(bound.rb + 0.2)), assigned=True ) diff --git a/tests/test_convex_hull.py b/tests/test_convex_hull.py index 24c84360..4e171efc 100644 --- a/tests/test_convex_hull.py +++ b/tests/test_convex_hull.py @@ -10,7 +10,7 @@ np.random.seed(1337) -N_STRESS = 250 +N_STRESS = 1000 @pytest.fixture(params=[False, True]) @@ -19,36 +19,38 @@ def offset_coordinates(request): def test_bounds(): - image = np.zeros((100, 100), dtype=bool) - image[20:70, 20:90] = np.random.randn(50, 70) > 0.5 + for _ in range(N_STRESS): + image = np.zeros((100, 100), dtype=bool) + image[20:70, 20:90] = np.random.randn(50, 70) > 1.5 - im_any = np.any(image, axis=1) - x_indices = np.arange(0, image.shape[0])[im_any] - y_indices_left = np.argmax(image[im_any], axis=1) - y_indices_right = image.shape[1] - 1 - np.argmax(image[im_any][:, ::-1], axis=1) - left = np.stack((x_indices, y_indices_left), axis=-1) - right = np.stack((x_indices, y_indices_right), axis=-1) - coords_ref = np.vstack((left, right)) + im_any = np.any(image, axis=1) + x_indices = np.arange(0, image.shape[0])[im_any] + y_indices_left = np.argmax(image[im_any], axis=1) + y_indices_right = image.shape[1] - 1 - np.argmax(image[im_any][:, ::-1], axis=1) + left = np.stack((x_indices, y_indices_left), axis=-1) + right = np.stack((x_indices, y_indices_right), axis=-1) + coords_ref = np.vstack((left, right)) - coords = _left_right_bounds(image) + coords = _left_right_bounds(image) - # _left_right_bounds has another order - assert len(coords_ref) == len(unique_rows(np.concatenate((coords, coords_ref), 0))) + # _left_right_bounds has another order + assert len(unique_rows(coords_ref)) == len(unique_rows(np.concatenate((coords, coords_ref), 0))) def test_offset(): - image = np.zeros((100, 100), dtype=bool) - image[20:70, 20:90] = np.random.randn(50, 70) > 0.5 + for _ in range(N_STRESS): + image = np.zeros((100, 100), dtype=bool) + image[20:70, 20:90] = np.random.randn(50, 70) > 1.5 - coords = _left_right_bounds(image) + coords = _left_right_bounds(image) - offsets = _offsets_diamond(2) - coords_ref = unique_rows((coords[:, None, :] + offsets).reshape(-1, 2)) + offsets = _offsets_diamond(2) + coords_ref = unique_rows((coords[:, None, :] + offsets).reshape(-1, 2)) - coords = _offset_unique(coords) + coords = _offset_unique(coords) - # _left_right_bounds has another order - assert len(coords_ref) == len(unique_rows(np.concatenate((coords, coords_ref), 0))) + # _left_right_bounds has another order + assert len(coords_ref) == len(unique_rows(np.concatenate((coords, coords_ref), 0))) def test_convex_hull_image(offset_coordinates): @@ -64,14 +66,14 @@ def test_convex_hull_image(offset_coordinates): assert (chull >= image).all() assert (chull >= chull_ref).all() - assert ((chull > chull_ref).sum() / chull_ref.sum()) < 2e-2 + assert ((chull > chull_ref).sum() / chull_ref.sum()) < 5e-3 def test_convex_hull_image_random(offset_coordinates): for _ in range(N_STRESS): image = np.zeros((200, 200), dtype=bool) - image[30:-30, 20:-40] = np.random.randn(140, 140) > 2 + image[15:-15, 5:-25] = np.random.randn(170, 170) > 3 try: chull_ref = convex_hull_image(image, offset_coordinates=offset_coordinates, include_borders=True) @@ -83,7 +85,7 @@ def test_convex_hull_image_random(offset_coordinates): assert (chull >= image).all() assert (chull >= chull_ref).all() - assert ((chull > chull_ref).sum() / chull_ref.sum()) < 2e-2 + assert ((chull > chull_ref).sum() / chull_ref.sum()) < 5e-3 def test_convex_hull_image_non2d(offset_coordinates): From 1aa32abf99cad56cfbea6324c336a58eaea02ba3 Mon Sep 17 00:00:00 2001 From: AnihilatorGun Date: Wed, 4 Dec 2024 18:32:51 +0300 Subject: [PATCH 40/42] No warn on empty image --- imops/morphology.py | 4 ---- tests/test_convex_hull.py | 4 +--- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/imops/morphology.py b/imops/morphology.py index 14eadc10..e9e2bcc1 100644 --- a/imops/morphology.py +++ b/imops/morphology.py @@ -551,10 +551,6 @@ def convex_hull_image(image: np.ndarray, offset_coordinates: bool = True) -> np. raise ValueError(f'convex_hull_image is currently implemented only for 2D arrays, got {ndim}D array') if np.count_nonzero(image) == 0: - warn( - 'Input image is entirely zero, no valid convex hull. Returning empty image', - UserWarning, - ) return np.zeros(image.shape, dtype=bool) # In 2D, we do an optimisation by choosing only pixels that are diff --git a/tests/test_convex_hull.py b/tests/test_convex_hull.py index 4e171efc..8d83273a 100644 --- a/tests/test_convex_hull.py +++ b/tests/test_convex_hull.py @@ -97,9 +97,7 @@ def test_convex_hull_image_non2d(offset_coordinates): def test_convex_hull_image_empty(offset_coordinates): image = np.zeros((10, 10), dtype=bool) - - with pytest.warns(UserWarning): - chull = convex_hull_image_fast(image, offset_coordinates=offset_coordinates) + chull = convex_hull_image_fast(image, offset_coordinates=offset_coordinates) assert (chull == np.zeros_like(chull)).all() From b8c2d72b1584c4e900fabd5ddd0db1e2240e386d Mon Sep 17 00:00:00 2001 From: Vladimir Filipenko Date: Thu, 5 Dec 2024 02:13:10 +0300 Subject: [PATCH 41/42] 3.13 --- .github/workflows/release.yml | 2 +- .github/workflows/test_build.yml | 2 +- .github/workflows/tests.yml | 2 +- pyproject.toml | 1 + setup.py | 1 + 5 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cd1d9baa..fa0216b7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -73,7 +73,7 @@ jobs: env: CIBW_ENVIRONMENT_MACOS: > CC="$(brew --prefix llvm)/bin/clang" CXX="$(brew --prefix llvm)/bin/clang++" - CIBW_BUILD: cp37-* cp38-* cp39-* cp310-* cp311-* cp312-* + CIBW_BUILD: cp37-* cp38-* cp39-* cp310-* cp311-* cp312-* cp313-* CIBW_BEFORE_BUILD_LINUX: 'if [ $(python -c "import sys; print(sys.version_info[1])") -ge 9 ]; then python -m pip install "numpy<3.0.0" --config-settings=setup-args="-Dallow-noblas=true"; fi' - uses: actions/upload-artifact@v3 with: diff --git a/.github/workflows/test_build.yml b/.github/workflows/test_build.yml index d5022263..947dbe85 100644 --- a/.github/workflows/test_build.yml +++ b/.github/workflows/test_build.yml @@ -47,6 +47,6 @@ jobs: env: CIBW_ENVIRONMENT_MACOS: > CC="$(brew --prefix llvm)/bin/clang" CXX="$(brew --prefix llvm)/bin/clang++" - CIBW_BUILD: cp37-* cp39-* cp312-* + CIBW_BUILD: cp37-* cp39-* cp312-* cp313-* CIBW_SKIP: "*manylinux_x86_64" CIBW_BEFORE_BUILD_LINUX: 'if [ $(python -c "import sys; print(sys.version_info[1])") -ge 9 ]; then python -m pip install "numpy<3.0.0" --config-settings=setup-args="-Dallow-noblas=true"; fi' diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 634a26d5..69659b92 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-22.04 strategy: matrix: - python-version: [ '3.7', '3.8', '3.9', '3.10', '3.11', '3.12'] + python-version: [ '3.7', '3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] steps: - uses: actions/checkout@v4 diff --git a/pyproject.toml b/pyproject.toml index 2f7a669b..a8a423eb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,7 @@ classifiers = [ 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: 3.13', ] [options] diff --git a/setup.py b/setup.py index a2fee0c8..10cbca17 100644 --- a/setup.py +++ b/setup.py @@ -15,6 +15,7 @@ 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: 3.13', ] with open(root / 'requirements.txt', encoding='utf-8') as file: From aa5deac9b03895b8f6472fab7a752f15aa894964 Mon Sep 17 00:00:00 2001 From: Vladimir Filipenko Date: Thu, 5 Dec 2024 02:40:33 +0300 Subject: [PATCH 42/42] revert --- .github/workflows/release.yml | 2 +- .github/workflows/test_build.yml | 2 +- .github/workflows/tests.yml | 2 +- pyproject.toml | 1 - setup.py | 1 - 5 files changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fa0216b7..cd1d9baa 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -73,7 +73,7 @@ jobs: env: CIBW_ENVIRONMENT_MACOS: > CC="$(brew --prefix llvm)/bin/clang" CXX="$(brew --prefix llvm)/bin/clang++" - CIBW_BUILD: cp37-* cp38-* cp39-* cp310-* cp311-* cp312-* cp313-* + CIBW_BUILD: cp37-* cp38-* cp39-* cp310-* cp311-* cp312-* CIBW_BEFORE_BUILD_LINUX: 'if [ $(python -c "import sys; print(sys.version_info[1])") -ge 9 ]; then python -m pip install "numpy<3.0.0" --config-settings=setup-args="-Dallow-noblas=true"; fi' - uses: actions/upload-artifact@v3 with: diff --git a/.github/workflows/test_build.yml b/.github/workflows/test_build.yml index 947dbe85..d5022263 100644 --- a/.github/workflows/test_build.yml +++ b/.github/workflows/test_build.yml @@ -47,6 +47,6 @@ jobs: env: CIBW_ENVIRONMENT_MACOS: > CC="$(brew --prefix llvm)/bin/clang" CXX="$(brew --prefix llvm)/bin/clang++" - CIBW_BUILD: cp37-* cp39-* cp312-* cp313-* + CIBW_BUILD: cp37-* cp39-* cp312-* CIBW_SKIP: "*manylinux_x86_64" CIBW_BEFORE_BUILD_LINUX: 'if [ $(python -c "import sys; print(sys.version_info[1])") -ge 9 ]; then python -m pip install "numpy<3.0.0" --config-settings=setup-args="-Dallow-noblas=true"; fi' diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 69659b92..634a26d5 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-22.04 strategy: matrix: - python-version: [ '3.7', '3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] + python-version: [ '3.7', '3.8', '3.9', '3.10', '3.11', '3.12'] steps: - uses: actions/checkout@v4 diff --git a/pyproject.toml b/pyproject.toml index a8a423eb..2f7a669b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,6 @@ classifiers = [ 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', - 'Programming Language :: Python :: 3.13', ] [options] diff --git a/setup.py b/setup.py index 10cbca17..a2fee0c8 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,6 @@ 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', - 'Programming Language :: Python :: 3.13', ] with open(root / 'requirements.txt', encoding='utf-8') as file: