Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 16 additions & 8 deletions smauglab/transforms/cpu/contrast.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from batchgeneratorsv2.transforms.intensity.gamma import GammaTransform

from smauglab.registry import AugId, AugType, Backend, register
from smauglab.transforms.kernels import laplace_kernel, scharr_kernels
from smauglab.transforms.kernels import laplace_kernel, laplacian_response, scharr_kernels


@register(
Expand Down Expand Up @@ -85,15 +85,23 @@ def _apply_to_image(self, img: torch.Tensor, **params) -> torch.Tensor:
orig_mean = torch.mean(img[c])
orig_std = torch.std(img[c])
img_ = img[c].unsqueeze(0).unsqueeze(0) # adds temp batch and channel dim
spatial_dims = img_.dim() - 2
if params["kernel_type"] == "Laplace":
tot_ = apply_filter(img_, params["kernel"])
# The separable identity rather than the dense kernel -- a quarter of
# the time for the same filter. See `kernels.laplacian_response`.
tot_ = laplacian_response(img_, spatial_dims)
elif params["kernel_type"] == "Scharr":
tot_ = torch.zeros_like(img_)
for kernel in params["kernel"]:
if params["absolute"]:
tot_ += torch.abs(apply_filter(img_, kernel))
else:
tot_ += apply_filter(img_, kernel)
# One convolution with every directional kernel in the output-channel
# axis, not one convolution per direction summed: bit-for-bit the same
# response, a third of the time. `params["kernel"]` still carries the
# kernels individually, so anything inspecting the parameters sees what
# it always did.
conv = F.conv3d if spatial_dims == 3 else F.conv2d
weight = torch.stack([k.to(img_) for k in params["kernel"]]).unsqueeze(1)
response = conv(img_, weight, padding="same")
if params["absolute"]:
response = response.abs()
tot_ = response.sum(dim=1, keepdim=True)
img[c] = tot_[0, 0]
if params["retain_stats"]:
mean = torch.mean(img[c])
Expand Down
38 changes: 27 additions & 11 deletions smauglab/transforms/cpu/fromSeg.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
from functools import partial
import math

import scipy.ndimage as ndi
import torch
from batchgeneratorsv2.transforms.base.basic_transform import BasicTransform
from scipy.stats import norm

from smauglab.registry import AugId, AugType, Backend, register

Expand Down Expand Up @@ -86,11 +85,18 @@ def aug_redistribute_seg(img, seg, classes=None, in_seg=0.2, retain_stats=False)
# Rank from the data, not hardcoded 3: scipy requires the structuring element
# to match the input's rank, so a 2-D image raised
# "structure rank must match input rank" here.
struct = ndi.iterate_structure(ndi.generate_binary_structure(l_mask_np.ndim, 1), 3)
l_mask_dilate_np = ndi.binary_dilation(l_mask_np, structure=struct)

# Convert back to PyTorch
l_mask_dilate = torch.tensor(l_mask_dilate_np, device=device)
#
# Three passes of the 6-connected element, not one pass of
# `iterate_structure(..., 3)`. They give bit-identical masks -- that is what
# iterating a structure means -- but the iterated element is a 7x7x7 stencil
# evaluated at every voxel, where three small passes are 3 x 7 taps: 9 ms
# against 45 ms for a 96^3 mask.
struct = ndi.generate_binary_structure(l_mask_np.ndim, 1)
l_mask_dilate_np = ndi.binary_dilation(l_mask_np, structure=struct, iterations=3)

# Convert back to PyTorch. `from_numpy` shares the buffer; `torch.tensor`
# copied it, and also warned about doing so.
l_mask_dilate = torch.from_numpy(l_mask_dilate_np).to(device)

# Create mask of the dilated mask excluding the original mask
l_mask_dilate_excl = l_mask_dilate & ~l_mask
Expand All @@ -107,12 +113,11 @@ def aug_redistribute_seg(img, seg, classes=None, in_seg=0.2, retain_stats=False)
torch.tensor([0.01], device=device),
)

redist = partial(norm.pdf, loc=l_mean.cpu().numpy(), scale=redist_std.cpu().numpy())

sign = 2 * torch.rand(1, device=device) - 1
if in_seg_bool:
to_add[l_mask] += torch.tensor(redist(img[l_mask].cpu().numpy()), device=device) * (2 * torch.rand(1, device=device) - 1)
to_add[l_mask] += _normal_pdf(img[l_mask], l_mean, redist_std) * sign
else:
to_add += torch.tensor(redist(img.cpu().numpy()), device=device) * (2 * torch.rand(1, device=device) - 1)
to_add += _normal_pdf(img, l_mean, redist_std) * sign

# Normalize to_add and apply it to the image.
#
Expand All @@ -139,6 +144,17 @@ def aug_redistribute_seg(img, seg, classes=None, in_seg=0.2, retain_stats=False)
return img, seg


def _normal_pdf(x: torch.Tensor, mean: torch.Tensor, std: torch.Tensor) -> torch.Tensor:
"""Gaussian density, in torch.

This was `scipy.stats.norm.pdf` reached through a `functools.partial`, with the
volume copied out to NumPy and the result copied back for every label. The
formula is the same one `scipy` evaluates; what goes away is the frozen
distribution's argument handling and two full-volume round trips per label.
"""
return torch.exp(-0.5 * ((x - mean) / std) ** 2) / (std * math.sqrt(2.0 * math.pi))


def combine_classes(seg, classes):
_seg = torch.zeros_like(seg)
for i, c in enumerate(classes):
Expand Down
Loading
Loading