Reputation: 283163
Given an image such as below, I want to blur it such that none of the original black pixels change in value. The blur should have a nice S curve fade-off to white.
If I apply a 15px gaussian blur to this image I get this:
It fades out nicely, but if I put a red square where the original black square was, you can see that actually the blur has exposed some parts of it:
Now I recently found out that a 15px gaussian blur means 1 std deviation is 15px, the actual full radius of affected pixels is closer to ¾√2π*σ. Technically infinite, but most programs limit it to that, from what I hear. So for a 15px blur, if I expand my box by 29px then gaussian blur by 15px, the extend of the blur ought to just barely reach back to my original box, leaving it undisturbed, right?
That looks pretty good, but if I put my red box back again we can see that there is still a difference:
The red doesn't show through (much), but I can still see a hard outline.
Is there a better suited blur algorithm for this than gaussian? What I'm imagining is something like we find the normals for the edges of the box/shape and then draw an S curve along that normal. This would probably be easier if I had a vector image, but I have bitmaps, so I don't know what the closest equivalent would be.
Upvotes: -1
Views: 123
Reputation: 283163
Given the original image:
I came up with this:
It's a bit easier to see when colorized:
The red is the original mask, the blue is the blur. The blue fade should begin where the red edge stops.
Applying a simple convolution to the whole image causes the 'fade' to cut into the original mask, so to fix this I expanded/dilated the mask by taking the max value over the same radius as the kernel. This is built into torch as max_pool2d
.
Here's the full code (in Python):
def run(self, *, mask: torch.Tensor, radius: int):
assert mask.ndim == 3, "Expected mask to have shape [B,H,W]"
assert radius > 0, "Radius must be positive"
# Define kernel size (2 * radius + 1 to cover the full area)
kernel_size = 2 * radius + 1
# Create a round Gaussian kernel
y, x = torch.meshgrid(
torch.arange(-radius, radius + 1, device=mask.device),
torch.arange(-radius, radius + 1, device=mask.device),
indexing="ij"
)
distance = torch.sqrt(x ** 2 + y ** 2)
gaussian_kernel = torch.exp(-0.5 * (distance / radius) ** 2)
gaussian_kernel /= gaussian_kernel.sum()
gaussian_kernel = gaussian_kernel.view(1, 1, kernel_size, kernel_size)
# Expand the mask by replicating the edges before applying max_pool2d
padded_mask = F.pad(mask, (radius, radius, radius, radius), mode="replicate")
# Apply max pooling to simulate dilation
dilated_mask = F.max_pool2d(padded_mask.unsqueeze(1), kernel_size, stride=1, padding=radius, ceil_mode=True)
# Apply Gaussian blur to the dilated mask
blurred_mask = F.conv2d(dilated_mask, gaussian_kernel, padding=0) # Implicitly crops back to original size
# Remove the channel dimension
blurred_mask = blurred_mask.squeeze(1)
# Shouldn't be necessary...
# final = torch.clamp(torch.maximum(blurred_mask, mask), 0, 1)
return self.output({"blurred_mask": blurred_mask})
The only thing I don't love is that the circles get squared off a bit, but it's not too bad.
Upvotes: 0
Reputation: 15510
The problem is that a Gaussian never becomes zero. Just very close to it.
No matter of how much dilation (shifting) you apply, it'll never stop.
The only thing making a Gaussian "stop" is quantization, e.g. having values 0 to 255 instead of "actual" real numbers.
You could pick something other than a Gaussian. Anything with a finite support region would do. "Good" old box blur is such a thing. Repeated application of a box blur approaches a Gaussian, yet stays finite in the strict mathematical sense.
If you're going the path of a distance transform and apply a curve to the distance to get your "fading out", the input data will have to be binary/binarized. On a distance transform, you can apply any function you can think of. Even the positive half of a Gaussian, because the distance is a scalar, not an image.
That "S-curve" you speak of sounds somewhat like a "Raised cosine".
Upvotes: 1
Reputation: 56
The problem is that design of Gaussian blur is isotropic, meaning it diffuses the intensity of each pixel equally in all directions.
You should try generating a binary mask of the black region, computing its distance transform, and than applying a scaling curve (exponential decay) to map the distance values to intensity values.
If I understood correctly what you are trying to achieve, reading this might help you with the realization.
Upvotes: 2