-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
Implement distance_transform_edt and the DistanceTransformEDT transform #6981
Conversation
Signed-off-by: Matthias Hadlich <matthiashadlich@posteo.de>
880189e
to
3d93616
Compare
Signed-off-by: Matthias Hadlich <matthiashadlich@posteo.de>
3d93616
to
cbac352
Compare
If that PR is accepted I would change the code in following functions to use the new code: That should speed up at least the SurfaceDice a lot, currently it is rather slow on big volumes. Please give feedback if that would be fine as well, thanks! |
looks great, please add the unit test file names to this list to skip them in the minimal testing environments Line 31 in c22a2bd
|
Signed-off-by: Matthias Hadlich <matthiashadlich@posteo.de>
0084bb0
to
150d59b
Compare
Signed-off-by: Matthias Hadlich <matthiashadlich@posteo.de>
7d3a313
to
2349f89
Compare
I just added the addtional |
Please keep them separate, modifying existing algorithm will require more checks to prevent regression... |
@wyli How can I fix the optional import error?
|
yes, I tried running it locally, the parameter expand decorator should be put before the skipping decorator:
also |
Signed-off-by: Matthias Hadlich <matthiashadlich@posteo.de>
Signed-off-by: Matthias Hadlich <matthiashadlich@posteo.de>
f9ce214
to
f32841d
Compare
Signed-off-by: Matthias Hadlich <matthiashadlich@posteo.de>
Signed-off-by: Matthias Hadlich <matthiashadlich@posteo.de>
/build |
Signed-off-by: Matthias Hadlich <matthiashadlich@posteo.de>
Signed-off-by: Matthias Hadlich <matthiashadlich@posteo.de>
Hey, I just noticed this PR and we seem to be working on similar functionality regarding the euclidean distance transform. So before any merging, we should make sure that this PR and #7008 don't duplicate functionality and make the changes in a consistent implementation. |
it seems #7008 could be refactored to use the utility function defined in this PR... I'd suggest @john-zielke-snkeos please also help review this PR and we merge this first, what do you think? |
Co-authored-by: YunLiu <55491388+KumoLiu@users.noreply.github.com> Signed-off-by: Matthias Hadlich <matthiashadlich@yahoo.de>
Co-authored-by: YunLiu <55491388+KumoLiu@users.noreply.github.com> Signed-off-by: Matthias Hadlich <matthiashadlich@yahoo.de>
Signed-off-by: Matthias Hadlich <matthiashadlich@posteo.de>
Hopefully final questions: Those two flags do complicate the code a lot and I'm not sure how to solve it without duplication then: distance_transform_edt, has_cucim = optional_import(
"cucim.core.operations.morphology", name="distance_transform_edt"
)
use_cp = has_cp and has_cucim and isinstance(img, torch.Tensor) and img.device != torch.device("cpu")
if not return_distances and not return_indices:
raise RuntimeError("Neither return_distances nor return_indices True")
distances, indices = [], []
if use_cp:
img_ = convert_to_cupy(img)
for channel in img_:
outputs = distance_transform_edt(channel, sampling=sampling, return_distances=return_distances, return_indices=return_indices)
if return_distances and return_indices:
distances.append(outputs[0])
indices.append(outputs[1])
elif return_distances:
distances.append(outputs)
elif return_indices:
indices.append(outputs)
else:
if not has_ndimage:
raise RuntimeError("scipy.ndimage required if cupy is not available")
img_ = convert_to_numpy(img)
for channel in img_:
outputs = ndimage.distance_transform_edt(channel, sampling=sampling, return_distances=return_distances, return_indices=return_indices)
if return_distances and return_indices:
distances.append(outputs[0])
indices.append(outputs[1])
elif return_distances:
distances.append(outputs)
elif return_indices:
indices.append(outputs)
d_out, i_out = None, None
if use_cp:
if return_distances and return_indices:
d_out = cp.stack(distances)
if return_indices:
i_out = cp.stack(indices)
else:
if return_distances and return_indices:
d_out = np.stack(distances)
if return_indices:
i_out = np.stack(indices)
return d_out, i_out |
Signed-off-by: Matthias Hadlich <matthiashadlich@posteo.de>
Signed-off-by: Matthias Hadlich <matthiashadlich@posteo.de>
Hi @matt3o, both are ok for me.
Thanks! |
How about sth like this: def distance_transform_edt(
img: NdarrayOrTensor,
sampling: None | float | list[float] = None,
return_distances: bool = True,
return_indices: bool = False,
distances=None,
indices=None,
*,
block_params=None,
float64_distances=False,
) -> NdarrayOrTensor:
"""
Euclidean distance transform, either GPU based with CuPy / cuCIM
or CPU based with scipy.ndimage.
Choice depends on cuCIM being available or scipy can be forced with the ``force_scipy`` flag.
Note that the runtime running on the CPU may be really depending on the inputs size.
Args:
...
"""
distance_transform_edt, has_cucim = optional_import(
"cucim.core.operations.morphology", name="distance_transform_edt"
)
use_cp = has_cp and has_cucim and isinstance(img, torch.Tensor) and img.device != torch.device("cpu")
if not return_distances and not return_indices:
raise RuntimeError("Neither return_distances nor return_indices True")
distances_original, indices_original = distances, indices
distances, indices = None, None
if use_cp:
distances_, indices_ = None, None
if return_distances:
dtype = torch.float64 if float64_distances else torch.float32
if distances is None:
distances = torch.zeros_like(img, dtype=dtype)
else:
if not isinstance(distances, torch.Tensor) and distances.device != img.device:
raise TypeError("distances must be a torch.Tensor on the same device as img")
if not distances.dtype == dtype:
raise TypeError("distances must be a torch.Tensor of dtype float32 or float64")
distances_ = convert_to_cupy(distances)
if return_indices:
dtype = torch.int32
if indices is None:
indices = torch.zeros((img.dim(),) + img.shape, dtype=dtype)
else:
if not isinstance(indices, torch.Tensor) and indices.device != img.device:
raise TypeError("indices must be a torch.Tensor on the same device as img")
if not indices.dtype == dtype:
raise TypeError("indices must be a torch.Tensor of dtype int32")
indices_ = convert_to_cupy(indices)
img_ = convert_to_cupy(img)
for channel_idx in range(img_.shape[0]):
distance_transform_edt(
img_[channel_idx],
sampling=sampling,
return_distances=return_distances,
return_indices=return_indices,
distances=distances_[channel_idx] if distances_ is not None else None,
indices=indices_[channel_idx] if indices_ is not None else None,
block_params=block_params,
float64_distances=float64_distances,
)
else:
if not has_ndimage:
raise RuntimeError("scipy.ndimage required if cupy is not available")
img_ = convert_to_numpy(img)
if return_distances:
if distances is None:
distances = np.zeros_like(img_, dtype=np.float64)
else:
if not isinstance(distances, np.ndarray):
raise TypeError("distances must be a numpy.ndarray")
if not distances.dtype == np.float64:
raise TypeError("distances must be a numpy.ndarray of dtype float64")
if return_indices:
if indices is None:
indices = np.zeros((img_.ndim,) + img_.shape, dtype=np.int32)
else:
if not isinstance(indices, np.ndarray):
raise TypeError("indices must be a numpy.ndarray")
if not indices.dtype == np.int32:
raise TypeError("indices must be a numpy.ndarray of dtype int32")
for channel_idx in range(img_.shape[0]):
ndimage.distance_transform_edt(
img_[channel_idx],
sampling=sampling,
return_distances=return_distances,
return_indices=return_indices,
distances=distances[channel_idx] if distances is not None else None,
indices=indices[channel_idx] if indices is not None else None,
)
r_vals = []
if return_distances and distances_original is None:
r_vals.append(distances)
if return_indices and indices_original is None:
r_vals.append(indices)
if not r_vals:
return None
if len(r_vals) == 1:
return r_vals[0]
return tuple(r_vals) |
Looking good @john-zielke-snkeos, thanks! My only concern is: Does MONAI want to expose the parameters of just cupy? Those flags don't do anything for scipy. Is that important to your use case to have |
I think that's fine. We should mention that these are cucim specific just like they do in the cucim docs. And since they are simply ignored in the other case, I think that is fine. With the proper docs and them being keyword-only arguments, normal users will not really stumble upon these and users that need the options (especially the precision one) still have the possibility to use them. |
@matt3o @wyli What I just wondered is if it's not better to check for |
thanks, I agree, we should be more specific in the if condition. |
Signed-off-by: Matthias Hadlich <matthiashadlich@posteo.de>
Signed-off-by: Matthias Hadlich <matthiashadlich@posteo.de>
Sure, I'll match that right now. I will have to change the check to |
for more information, see https://pre-commit.ci
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thank you, it looks good to me, please help address the type annotation issues https://github.com/Project-MONAI/MONAI/actions/runs/6316993047/job/17169932102?pr=6981 and the docstring issues https://github.com/Project-MONAI/MONAI/actions/runs/6316993047/job/17169930462?pr=6981 then I'll trigger more tests and merged the PR.
Signed-off-by: Matthias Hadlich <matthiashadlich@posteo.de>
9a037a3
to
94db8ea
Compare
Signed-off-by: Matthias Hadlich <matthiashadlich@posteo.de>
/build |
many thanks @matt3o for the PR, and @john-zielke-snkeos @KumoLiu for the detailed comments, all premerge tests passed, and I'm merging this PR to include it in the upcoming monai v1.3 , please feel free to create follow-ups.. |
That's great news, thank you very much for the support! I'll adjust my Pull Request ASAP, would be nice if we could include the faster Metrics in 1.3 as well. |
Related to #6845, this commits adds an EDT distance transform to MONAI.
Most importantly this enables GPU based distance transforms which lead to a huge speedup.
Description
A few sentences describing the changes proposed in this pull request.
Types of changes
./runtests.sh -f -u --net --coverage
../runtests.sh --quick --unittests --disttests
.make html
command in thedocs/
folder.