Skip to content

Add segmentation with the Chan-Vese active contours method - #343

Merged
rapids-bot[bot] merged 25 commits into
rapidsai:branch-22.08from
grlee77:add-chan_vese
Aug 2, 2022
Merged

rapids-bot[bot] merged 25 commits into
rapidsai:branch-22.08from
grlee77:add-chan_vese

Conversation

@grlee77

@grlee77 grlee77 commented Jul 26, 2022

Copy link
Copy Markdown
Contributor

closes #335

This PR adds a function for segmentation based on active-contours. Test cases are directly from scikit-image, but the code itself was heavily refactored to make extensive use of kernel fusion via cupy.fuse(). I will add benchmark results here soon.

It should be reviewed after #318 is merged. The new commits only start from 502f41a

@grlee77 grlee77 added feature request New feature or request non-breaking Introduces a non-breaking change labels Jul 26, 2022
@grlee77 grlee77 added this to the v22.08.00 milestone Jul 26, 2022
@grlee77
grlee77 requested a review from a team as a code owner July 26, 2022 14:51
@grlee77 grlee77 changed the title Add chan vese Add segmentation with the Chan-Vese active contours method Jul 26, 2022
@grlee77

grlee77 commented Jul 27, 2022

Copy link
Copy Markdown
Contributor Author

This shows a huge speedup relative to scikit-image as image size increases (>200x):

shape Duration (CPU) Duration (GPU) Acceleration
(256, 256) 0.679 +/- 0.014 0.209 +/- 0.005 3.251
(512, 512) 3.196 +/- 0.014 0.208 +/- 0.002 15.397
(1024, 1024) 14.350 +/- 0.000 0.206 +/- 0.001 69.603
(2048, 2048) 68.054 +/- 0.000 0.426 +/- 0.000 159.710
(3072, 3072) 199.156 +/- 0.000 0.874 +/- 0.000 227.748
benchmark script
import cupy as cp
import matplotlib.pyplot as plt
from cupyx.profiler import benchmark
from skimage import data, segmentation

from cucim.skimage import img_as_float
from cucim.skimage.segmentation import chan_vese


_image = img_as_float(cp.array(data.camera()))

print("shape | Duration (CPU) | Duration (GPU) | Acceleration")
print("------|----------------|----------------|-------------")
for n in [0.5, 1, 2, 4, 6]:
    if n == 0.5:
        image = _image[128:-128, 128:-128]
    else:
        image = cp.tile(_image, (n, n))
    image_cpu = cp.asnumpy(image)
    kwargs = dict(mu=0.25, lambda1=1, lambda2=1, tol=1e-3, max_num_iter=200, dt=0.5, init_level_set="checkerboard", extended_output=True)
    perf_cpu = benchmark(segmentation.chan_vese, (image_cpu, ), kwargs=kwargs, n_warmup=0, n_repeat=10, max_duration=5)
    t_cpu = perf_cpu.gpu_times
    perf_gpu = benchmark(chan_vese, (image, ), kwargs=kwargs, n_warmup=4, n_repeat=100, max_duration=5)
    t_gpu = perf_gpu.gpu_times
    print(f"{image.shape} | {t_cpu.mean():0.3f} +/- {t_cpu.std():0.3f} | {t_gpu.mean():0.3f} +/- {t_gpu.std():0.3f} | {t_cpu.mean() / t_gpu.mean():0.3f}")

rapids-bot Bot pushed a commit that referenced this pull request Jul 28, 2022
### Overview

closes #319

This PR adds an implementation of `scipy.ndimage.distance_transform_edt`. For each foreground pixel in a binary image, this function computes the minimal Euclidean distance to reach a background pixel. This is a SciPy function rather than a scikit-image one so I have put it under `cucim.core` instead of `cucim.skimage`. Longer term we could move this upstream to CuPy, but I think we want to have a copy here so that we can use it in the near term to implement some of the missing scikit-image functionality. This function is used by the following functions in the scikit-image API, so this PR will help us implement these in future PRs:

`skimage.morphology.medial_axis` (**update**: see #342)
`skimage.segmentation.expand_labels` (**update**: see #341)
`skimage.segmentation.chan_vese` (**update**: see #343)
It is also used in examples related to `skimage.segmentation.watershed` and is needed for implementation of ITK's `SignedMaurerDistanceMapImageFilter` for [itk-cucim](InsightSoftwareConsortium/itk_cucim#13).

The algorithm used here is adapted from the C++ code in the [PBA+ repository](https://github.com/orzzzjq/Parallel-Banding-Algorithm-plus) (MIT-licensed)

### extensions made to PBA+ kernel source
- larger sizes can be supported due to runtime code generation of the integer datatypes and encode/decode functions.

### current known limitations
- 2D and 3D only (this should cover the most common use cases)
- The `sampling` argument is not fully supported. This can likely be done with a bit of effort, but will require modifications to the CUDA kernels
- User-specified output arguments are not currently supported. We can potentially add this in the future
- The indices returned by `return_indices=True` are equally valid, but not always identical to those returned by SciPy. This is because there can be multiple indices with an identical distance, so which one gets chosen in that case is implementation-dependent.

### initial benchmarks relative to SciPy

Here `% true` is the percentage of the image that corresponds to foreground pixels. Even for fairly small images there is some benefit, with the benefit becoming two orders of magnitude at larger sizes.

shape      | % true |  cuCIM |  SciPy  | acceleration
-----------------|--------|---------|---------|--------------
(256, 256) | 5 | 0.00108 | 0.00353 | 3.25
(512, 512) | 5 | 0.00108 | 0.01552 | 14.42
(2048, 2048) | 5 | 0.00252 | 0.34434 | 136.86
(4096, 4096) | 5 | 0.00765 | 1.58948 | 207.88
(32, 32, 32) | 5 | 0.00103 | 0.00305 | 2.98
(128, 128, 128) | 5 | 0.00153 | 0.30103 | 196.26
(256, 256, 256) | 5 | 0.00763 | 3.17872 | 416.37
(384, 384, 384) | 5 | 0.02460 | 14.28779 | 580.89
(256, 256) | 50 | 0.00107 | 0.00430 | 4.01
(512, 512) | 50 | 0.00109 | 0.01878 | 17.30
(2048, 2048) | 50 | 0.00299 | 0.39304 | 131.60
(4096, 4096) | 50 | 0.00896 | 1.84686 | 206.19
(32, 32, 32) | 50 | 0.00102 | 0.00361 | 3.53
(128, 128, 128) | 50 | 0.00163 | 0.31657 | 194.66
(256, 256, 256) | 50 | 0.00914 | 3.35914 | 367.49
(384, 384, 384) | 50 | 0.03005 | 13.83219 | 460.30
(256, 256) | 95 | 0.00107 | 0.00344 | 3.22
(512, 512) | 95 | 0.00108 | 0.01638 | 15.17
(2048, 2048) | 95 | 0.00314 | 0.36996 | 117.81
(4096, 4096) | 95 | 0.00943 | 1.90475 | 202.05
(32, 32, 32) | 95 | 0.00102 | 0.00314 | 3.07
(128, 128, 128) | 95 | 0.00180 | 0.28843 | 159.80
(256, 256, 256) | 95 | 0.01073 | 3.23450 | 301.41
(384, 384, 384) | 95 | 0.03577 | 12.40526 | 346.82

### other comments
- can likely reduce memory overhead and improve performance a bit by refactoring some of the pre/post-processing code into elementwise kernels. (e.g. `encode3d`/`decode3d`, etc.)

- (JK) This may be able to leverage CuPy in the future ( cupy/cupy#6919 )

Authors:
  - Gregory Lee (https://github.com/grlee77)

Approvers:
  - Gigon Bae (https://github.com/gigony)
  - https://github.com/jakirkham

URL: #318
@jakirkham

Copy link
Copy Markdown
Member

@grlee77, could you please take a look at the conflict here? Think it may be related to PR ( #318 )

@grlee77

grlee77 commented Jul 28, 2022

Copy link
Copy Markdown
Contributor Author

fixed the conflict

@jakirkham jakirkham left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks Greg! 🙏

Had a few comments below

def _cv_curvature(phi):
"""Returns the 'curvature' of a level set 'phi'.
"""
P = cp.pad(phi, 1, mode='edge')

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given the slicing done below. Wonder if pading could be avoided by doing different slicing on the original image

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure. Aside from the border it seems like it should be. Maybe we could do all but the border without padding and then handle the border explicitly. I did manage to reduce it from the upstream implementation where some of the same slices are taken multiple times.

General slicing within a CuPy fused kernel is not supported, but passing in the sliced arrays as done here works fine.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah this is what I'm wondering. Maybe we can revisit after the release ( #372 ).

Comment thread python/cucim/src/cucim/skimage/segmentation/_chan_vese.py
"""Returns the variation of level set 'phi' based on algorithm parameters.
"""
eta = 1e-16
P = cp.pad(phi, 1, mode='edge')

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar question to above about whether pad can be avoided by using different slicing below

Comment thread python/cucim/src/cucim/skimage/segmentation/_chan_vese.py Outdated
Co-authored-by: jakirkham <jakirkham@gmail.com>
@jakirkham

Copy link
Copy Markdown
Member

@gpucibot merge

@rapids-bot
rapids-bot Bot merged commit 4814905 into rapidsai:branch-22.08 Aug 2, 2022
@jakirkham

Copy link
Copy Markdown
Member

Thanks Greg! 🙏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature request New feature or request non-breaking Introduces a non-breaking change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEA] add cucim.skimage.segmentation.chan_vese

2 participants