Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add function to remove near objects in nd-image #4165

Open
wants to merge 69 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 40 commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
d929614
Move shared nd-utility functions into _util module
lagru Sep 16, 2019
e35ffa3
Add remove_close_objects function
lagru Sep 16, 2019
03830d9
Test basic functionality of remove_close_objects
lagru Sep 16, 2019
bcac7ee
Add manual bounds checking
lagru Sep 16, 2019
f153fe9
Improve tests and handle edge cases better
lagru Sep 18, 2019
cac1a0c
Support boolean views on numeric dtypes
lagru Sep 18, 2019
0814f91
Simplify Cythonization in morphology.setup.py
lagru Sep 19, 2019
f3aeb68
Sort objects by their size if no priority is given
lagru Sep 27, 2019
c53a492
Add support for int and float type images
lagru Sep 27, 2019
fbe45c8
Add test for object with unequal values
lagru Sep 27, 2019
0d08d0f
Simplify/tweak helper functions in morphology._util
lagru Sep 29, 2019
ffcdb38
Only evaluate object surface in remove_close_objects
lagru Sep 29, 2019
0b36136
Use intp as label dtype
lagru Sep 29, 2019
b44c417
Add basic benchmarks for remove_close_objects
lagru Sep 29, 2019
27777e3
Correct faulty results in docstring example
lagru Sep 29, 2019
2a6e09e
Use "mergesort" instead of "stable" sort
lagru Sep 29, 2019
695b2c7
Use objects' size as priority in benchmark
lagru Sep 30, 2019
9dfe298
Document behavior for NaNs and provide test
lagru Sep 30, 2019
b4b0155
Support raveled offsets for dimensions smaller selem
lagru Oct 4, 2019
36cf967
Pass-through p-norm to KDTree
lagru Oct 4, 2019
fb5a75e
Address unintended reorering by np.unique
lagru Oct 4, 2019
0893179
Remove parameter not supported by Scipy 0.19
lagru Oct 4, 2019
a9a0204
Reorder parameters in remove_close_objects
lagru Oct 5, 2019
4cf5108
Rename to remove_near_objects
lagru Oct 6, 2019
0aaff28
Use in_place instead of inplace
lagru Oct 19, 2019
0439065
Add remove_near_objects to API and release notes
lagru Apr 10, 2020
4868214
Merge branch 'master' into remove-close-objects
lagru Apr 10, 2020
b7d96b8
Don't class-bundle remove_near_objects's tests
lagru Apr 10, 2020
7c79fcc
Assert Exception for mismatching priority shape
lagru Apr 10, 2020
2390fcf
Fix whitespace
lagru Apr 10, 2020
46c4edc
Add gallery example for removing objects
lagru Apr 10, 2020
9ea2b38
Fix doctest in remove_near_objects
lagru Apr 10, 2020
39fc5a6
Improve misleading doc wording
lagru Apr 13, 2020
1a3f4a2
Merge branch 'master' into remove-close-objects
lagru Jul 15, 2020
ba7aca5
Use shared fused dtype np_real_numeric
lagru Jul 16, 2020
8308572
Merge branch 'main' into remove-close-objects
lagru Mar 31, 2022
373d0d3
Use out argument instead of in_place
lagru Apr 10, 2022
78323dd
Group test's for remove_near_objects in class
lagru Apr 10, 2022
51f9415
Simplify input sanitation
lagru Apr 10, 2022
1f7df29
Handle memory layout more expicitely
lagru Apr 10, 2022
9b9c50e
Merge branch 'main' into remove-close-objects
lagru Oct 27, 2022
d4f1fe6
Remove trailing whitespace
lagru Oct 27, 2022
633658b
Merge branch 'main' into remove-close-objects
lagru Jan 20, 2023
729271d
Add _near_objects_cy to new meson system
lagru Jan 21, 2023
668927e
Address NumPy deprecation warning
lagru Jan 21, 2023
384d2c1
Add remove_near_objects to morpholoy.__all__
lagru Jan 21, 2023
faed84a
Merge branch 'main' into remove-close-objects
lagru Jan 25, 2023
1196471
Use labels as input and map priority to index
lagru Feb 19, 2023
9ef5415
Ignore labels which aren't surface
lagru Feb 19, 2023
c733e61
Add two new test cases one of which currently fails
lagru Feb 19, 2023
664a220
Merge branch 'main' into remove-close-objects
lagru Apr 18, 2024
72e58f6
Allow F-contiguous arrays
lagru Apr 19, 2024
c78dc8a
Deal with constant and negative input
lagru Apr 19, 2024
887968f
Remove obsolete _max_priority
lagru Apr 19, 2024
571aa4a
Release GIL in _remove_object
lagru Apr 19, 2024
bb2a9af
Update docstrings
lagru Apr 19, 2024
11ac443
Format with black
lagru Apr 19, 2024
d2f602e
Don't use boolean input in gallery example
lagru Apr 19, 2024
2573cd8
Use C++ set and release GIL
lagru Apr 20, 2024
e98eb6d
Exclude inner samples of objects from KDTree & critical loop
lagru Apr 20, 2024
20a5cee
Improve documentation of remove_near_objects
lagru Apr 20, 2024
4399993
Merge branch 'main' into remove-close-objects
lagru Apr 20, 2024
fefdb67
Color labels in gallery example
lagru Apr 20, 2024
6709e68
Revert to current version of 0.20 release notes
lagru Apr 20, 2024
fe1a8e8
Add support for anistropic data in remove_near_objects
lagru Apr 20, 2024
e6c02bd
Rename to min_distance like in peak_local_max
lagru Apr 21, 2024
7a59894
Test p-norm as well
lagru Apr 21, 2024
49ce7c3
Correct spelling
lagru Apr 21, 2024
c8756ca
Use sparser borders if possible
lagru Apr 21, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
41 changes: 40 additions & 1 deletion benchmarks/benchmark_morphology.py
Expand Up @@ -7,7 +7,7 @@
from numpy.lib import NumpyVersion as Version

import skimage
from skimage import data, morphology, util
from skimage import data, morphology, util, color


class Skeletonize3d(object):
Expand Down Expand Up @@ -157,3 +157,42 @@ def time_erosion(
self, shape, footprint, radius, *args
):
morphology.erosion(self.image, self.footprint)


class RemoveNearObjects(object):

param_names = ["minimal_distance"]
params = [5, 100]

def setup(self, *args):
image = data.hubble_deep_field()
image = color.rgb2gray(image)
self.objects = image > 0.18 # Chosen with threshold_li

def time_remove_near_objects(self, minimal_distance):
morphology.remove_near_objects(
self.objects, minimal_distance=minimal_distance
)

def peakmem_reference(self, *args):
"""Provide reference for memory measurement with empty benchmark.

Peakmem benchmarks measure the maximum amount of RAM used by a
function. However, this maximum also includes the memory used
during the setup routine (as of asv 0.2.1; see [1]_).
Measuring an empty peakmem function might allow us to disambiguate
between the memory used by setup and the memory used by target (see
other ``peakmem_`` functions below).

References
----------
.. [1]: https://asv.readthedocs.io/en/stable/writing_benchmarks.html#peak-memory
"""
pass

def peakmem_remove_near_objects(self, minimal_distance):
morphology.remove_near_objects(
self.objects,
minimal_distance=minimal_distance,
priority=self.objects,
)
45 changes: 45 additions & 0 deletions doc/examples/features_detection/plot_remove_objects.py
@@ -0,0 +1,45 @@
"""
================
Removing objects
================

scikit-image supports several ways to remove objects inside N-dimensional
images. In this context "objects" (and "holes") are defined as groups of
connected samples that are distinct from the background. A binary image can
contain several objects that are not connected to each other.

The code snippet below demonstrates two ways to remove objects inside an image
either by removing objects

- based on the number of samples that make up each object
- or based on their distance of each object to each other.
"""

import matplotlib.pyplot as plt
from skimage import data, morphology, color, filters

# Extract foreground by thresholding an image taken by the Hubble Telescope
image = color.rgb2gray(data.hubble_deep_field())
foreground = image > filters.threshold_li(image)

# Separate objects into regions larger and smaller than 100 pixels
large_objects = morphology.remove_small_objects(foreground, min_size=100)
small_objects = foreground ^ large_objects

# Remove objects until remaining ones are at least 100 pixels apart,
# smaller ones are removed in favor of larger ones by default
spaced_objects = morphology.remove_near_objects(
foreground, minimal_distance=100
)

# Plot the results
fig, ax = plt.subplots(2, 2, figsize=(10, 10))
ax[0, 0].set_title("original")
ax[0, 0].imshow(foreground)
ax[0, 1].set_title("large objects")
ax[0, 1].imshow(large_objects)
ax[1, 1].set_title("small objects")
ax[1, 1].imshow(small_objects)
ax[1, 0].set_title("spaced objects (nearest removed)")
ax[1, 0].imshow(spaced_objects)
plt.show()
2 changes: 2 additions & 0 deletions doc/release/release_dev.rst
Expand Up @@ -15,6 +15,8 @@ https://scikit-image.org

New Features
------------
- Added ``skimage.morphology.remove_near_objects`` to remove objects until a
minimal distance between remaining ones is ensured.



Expand Down
4 changes: 3 additions & 1 deletion skimage/morphology/__init__.py
Expand Up @@ -8,7 +8,8 @@
from ._skeletonize import skeletonize, medial_axis, thin, skeletonize_3d
from .convex_hull import convex_hull_image, convex_hull_object
from .grayreconstruct import reconstruction
from .misc import remove_small_objects, remove_small_holes
from .misc import (remove_small_objects, remove_small_holes,
remove_near_objects)
from .extrema import h_minima, h_maxima, local_maxima, local_minima
from ._flood_fill import flood, flood_fill
from .max_tree import (max_tree, area_opening, area_closing,
Expand Down Expand Up @@ -44,6 +45,7 @@
'convex_hull_object',
'reconstruction',
'remove_small_objects',
'remove_near_objects',
'remove_small_holes',
'h_minima',
'h_maxima',
Expand Down
156 changes: 156 additions & 0 deletions skimage/morphology/_near_objects_cy.pyx
@@ -0,0 +1,156 @@
#cython: cdivision=True
#cython: boundscheck=False
#cython: nonecheck=False
#cython: wraparound=False


"""Cython code used in `remove_near_objects` function."""


import numpy as np
cimport numpy as cnp

from _shared.fused_numerics cimport np_real_numeric

# Must be defined to use QueueWithHistory
ctypedef Py_ssize_t QueueItem


include "_queue_with_history.pxi"


def _remove_near_objects(
np_real_numeric[::1] image not None,
Py_ssize_t[::1] labels not None,
Py_ssize_t[::1] raveled_indices not None,
Py_ssize_t[::1] neighbor_offsets not None,
kdtree,
cnp.float64_t p_norm,
cnp.float64_t minimal_distance,
tuple shape,
):
"""Remove objects until a minimal distance is ensured.

Iterates over all objects (connected pixels that are True) inside an image
and removes neighboring objects until all remaining ones are at least a
minimal distance from each other.

Parameters
----------
image :
The raveled view of a n-dimensional array. which is modified inplace.
labels :
An array with labels for each object in `image` matching it in shape.
raveled_indices :
Indices into `image` and `labels` that determines the iteration order
and thus which objects take precedence.
neighbor_offsets :
A one-dimensional array that contains the offsets to find the
connected neighbors for any index in `image`.
kdtree : scipy.spatial.cKDTree
A KDTree containing the coordinates of all objects in `image`.
minimal_distance :
The minimal allowed distance between objects.
p_norm :
Which Minkowski p-norm to use to calculate the distance between
objects. Defaults to 2 which corresponds to the Euclidean distance
while 1 corresponds to the Manatten distance.
shape :
The shape of the unraveled `image`.

Notes
-----
This function and its partner function :func:`~._remove_object` can deal
with objects where `labels` is 0 inside objects as long as its enclosing
surface points (in the sense of the neighborhood) are labeled.
This significantly improves the performance by reducing number of queries
to the KDTree and its size.
This effect grows with the size to surface ratio of all evaluated objects.
"""
cdef:
Py_ssize_t i, j, index_i, index_j
list in_range
QueueWithHistory queue

queue_init(&queue, 64)
try:
for i in range(raveled_indices.shape[0]):
index_i = raveled_indices[i]

# Skip if point is part of a removed object
if image[index_i] == 0:
continue

in_range = kdtree.query_ball_point(
np.unravel_index(index_i, shape),
r=minimal_distance,
p=p_norm,
)

# Remove objects in `in_range` that don't share the same label id
for j in in_range:
index_j = raveled_indices[j]
if (
image[index_j] != 0
and labels[index_i] != labels[index_j]
):
_remove_object(
image=image,
labels=labels,
start_index=index_j,
neighbor_offsets=neighbor_offsets,
queue_ptr=&queue,
)
finally:
queue_exit(&queue)


cdef inline void _remove_object(
np_real_numeric[::1] image,
Py_ssize_t[::1] labels,
Py_ssize_t start_index,
Py_ssize_t[::1] neighbor_offsets,
QueueWithHistory* queue_ptr,
):
"""Remove single connected object.

Performs a flood-fill on the object with the value 0. Samples with a label
id == 0 and an image value != 0 are considered to be inside the evaluated
object.

Parameters
----------
image :
The raveled view of a n-dimensional array. which is modified inplace.
labels :
An array with labels for each object in `image` matching it in shape.
start_index :
Start position for the flood-fill.
neighbor_offsets :
A one-dimensional array that contains the offsets to find the
connected neighbors for any index in `image`.
queue_ptr :
Pointer to initialized (!) queue.
"""
cdef Py_ssize_t i, point, neighbor, max_index
cdef cnp.uint32_t label

max_index = image.shape[0]
queue_clear(queue_ptr)
queue_push(queue_ptr, &start_index)
image[start_index] = 0
label = labels[start_index]

while queue_pop(queue_ptr, &point):
for i in range(neighbor_offsets.shape[0]):
neighbor = point + neighbor_offsets[i]
# Bounds checking because image wasn't padded to signal the edge
if not 0 <= neighbor < max_index:
continue

# The algorithm might cross the image edge when two objects are
# neighbors in the raveled view -> check that the label id is
# either the same (object's surface) or 0 (inside object).
if image[neighbor] != 0 and labels[neighbor] in (0, label):
queue_push(queue_ptr, &neighbor)
image[neighbor] = 0