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

Implement Median Blur processor #2219

Merged
merged 18 commits into from
Sep 12, 2022
Merged

Conversation

ynse01
Copy link
Contributor

@ynse01 ynse01 commented Aug 30, 2022

Prerequisites

  • I have written a descriptive pull-request title
  • I have verified that there are no overlapping pull-requests open
  • I have verified that I am following the existing coding patterns and practice as demonstrated in the repository. These follow strict Stylecop rules 👮.
  • I have provided test coverage for my change (where applicable)

Description

This PR aims to solve #814 by implementing a Median Blur processor using the .NET runtime Span.Sort(). Results are comparable to Gimp.

@ynse01 ynse01 changed the title Median filter Implement Median Blur processor Aug 31, 2022
{
var k = xOffsets[baseXOffsetIndex + z];
var pixel = row[k];
kernelBuffer[index + z] = pixel.ToVector4();
Copy link
Member

Choose a reason for hiding this comment

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

Can we optimize this? Would be useful to assign a buffer large enough to allow the bulk operation like we do in the ConvolutionProcessor<T>

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Because the z loops runs over neighboring rows, this buffer would need to contain at least several rows of the source image.

Do you feel that is worth the memory?

Copy link
Member

Choose a reason for hiding this comment

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

We can still read/convert the source row slice one at a time though if you're sampling one kernel row at a time?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Please see in the comments below how this is done.

{
var k = xOffsets[baseXOffsetIndex + z];
var pixel = row[k];
kernelBuffer[index + z] = pixel.ToVector4();
Copy link
Member

Choose a reason for hiding this comment

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

Perhaps we should Make ReadOnlyKernel Kernel and add setters. That way you can use a DenseMatrix<T> and use all the existing APIs for Kernel access.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The ConvolutionProcessor uses a clever trick, where it builds up the target value row-by-row. This is allowed for ReadOnlyKernel type of RowOperation classes, where we're calculating weighted sums basically. These weighted sums can indeed be split arbitrary, as is done by clearing the target buffer before the loop and using += in line 152
However, the operation of finding a median, cannot be split up like this (or at least not without lots of caching). It mandates the traversal of the entire kernel in one pass, to get a single Span of pixel values to Sort. Hence the different logic here, compared to ConvolutionProcessor.

I'll update the code to use Unsafe.Add iso indexers.

Copy link
Member

Choose a reason for hiding this comment

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

Ah no, I didn't mean the processors themselves I just meant the ConvolutionState struct. It's designed to track all the offsets you're manually tracking yourself. ReadOnlyKernel could become Kernel with a read/write indexer (maybe ref based).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Made a separate MedianConvolutionState struct for this, as it's using Vector4 iso float.


// We use a rectangle with width set to 2 * kernelSize^2 + width, to allocate a buffer big enough
// for kernel source and target bulk pixel conversion.
Rectangle operationBounds = new Rectangle(interest.X, interest.Y, (2 * (kernelSize * kernelSize)) + interest.Width, interest.Height);
Copy link
Member

Choose a reason for hiding this comment

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

If you use 2x interest width here you can use the extra buffer space to allow bulk per-row ToVector4() against the source row in the operation.

new Rectangle(interest.X, interest.Y, (2 * (kernelSize * kernelSize)) + (2 * interest.Width), interest.Height);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Well, it's not that simple I think, as we need more rows for a single kernel.

I think I get your point about caching the Vector4 converted pixels, I'll take the challenge of making it work!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

On the multiple source rows that Median needs, the conversion is now done with BulkOperations. I think this is what you meant.

@JimBobSquarePants
Copy link
Member

@ynse01 I'll have a good look at this ASAP. Just trying to get #2189 through first.

Copy link
Member

@JimBobSquarePants JimBobSquarePants left a comment

Choose a reason for hiding this comment

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

Looks great now! Thanks for persevering!

@JimBobSquarePants JimBobSquarePants merged commit ea97bac into SixLabors:main Sep 12, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants