Skip to content

Add feather effect#953

Merged
cameronwhite merged 14 commits into
PintaProject:masterfrom
potatoes1286:patch-feather-2
Sep 2, 2024
Merged

Add feather effect#953
cameronwhite merged 14 commits into
PintaProject:masterfrom
potatoes1286:patch-feather-2

Conversation

@potatoes1286

Copy link
Copy Markdown
Contributor

Fixes #886

Adds feather tool to Effects > Stylize (Considered blur, but I think this is more fitting). This softens the edges of an image without spilling blur out of the image or blurring the contents of the image.

Radius adjusts the strength of the effect, Transparency Threshold is used to determine the "outline" of the object. Requires object be surrounded by transparency to function.

Example with a test image.

feather effect with test image

Only known issue I am aware of is that the effect will spill over outside selection in preview, however it does not spill over after application. Unsure how to fix this.

@cameronwhite cameronwhite 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 for contributing! Left a few suggestions in the comments

Comment thread Pinta.Effects/Effects/FeatherEffect.cs Outdated
if (sx < 0 || sx >= src_width || sy < 0 || sy >= src_height)
continue;

if (src.GetColorBgra (src_data, src_width, pixel).A != 0)

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.

Should this be comparing against the transparency threshold?

Comment thread Pinta.Effects/Effects/FeatherEffect.cs Outdated
continue;

if (src.GetColorBgra (src_data, src_width, pixel).A != 0)
borderPixels.Add (potentialBorderPixel);

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.

I think the same pixel could be added to the list several times potentially?

Comment thread Pinta.Effects/Effects/FeatherEffect.cs Outdated
// For each pixel, lower alpha based off distance to border pixel
foreach (var borderPixel in borderPixels) {
for (int y = borderPixel.Y - radius; y <= borderPixel.Y + radius; ++y) {
for (int x = borderPixel.X - radius; x <= borderPixel.X + radius; x++) {

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.

I think the issue with modifying the image outside the selection is because these coordinates aren't clamped to the regions being rendered (or the image size even?)

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.

The issue I am having, is that the border pixels have to affect pixels outside their roi. I can't make the system work backwards (each pixel modifies itself based on nearby border pixels) because of threading (Pixels may not be aware of some border pixels.) I dont think it's possible to see all the rois at once.

That being said, yeah i should be clamping to at least the border size...

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.

I think if you define IsTileable to be false, then you'll get a single region for the whole image (it's up to you to then do any multithreading internally)
That could be more suitable here if you want to do a first pass to find border pixels and then a second pass to adjust alpha, or something like that?

Comment thread Pinta.Effects/Effects/FeatherEffect.cs Outdated
[Caption ("Radius"), MinimumValue (1), MaximumValue (100)]
public int Radius { get; set; } = 6;

[Caption ("Transparency Threshold"), MinimumValue (0), MaximumValue (255)]

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.

Minor suggestion: Tolerance rather than Threshold would be a closer match for other terminology like the Magic Wand tool

@cameronwhite

Copy link
Copy Markdown
Member

One other note is that you can also add tests for the effect in https://github.com/PintaProject/Pinta/blob/master/tests/Pinta.Effects.Tests/EffectsTest.cs

@potatoes1286

Copy link
Copy Markdown
Contributor Author

One other note is that you can also add tests for the effect in https://github.com/PintaProject/Pinta/blob/master/tests/Pinta.Effects.Tests/EffectsTest.cs

I can, but because feather replies on transparency, the output is identical to the test input image.

@cameronwhite

Copy link
Copy Markdown
Member

One other note is that you can also add tests for the effect in https://github.com/PintaProject/Pinta/blob/master/tests/Pinta.Effects.Tests/EffectsTest.cs

I can, but because feather replies on transparency, the output is identical to the test input image.

Right, good point :) Feel free to adjust that TestEffect method to let you choose a different input image if that works

@potatoes1286

Copy link
Copy Markdown
Contributor Author

Should be good now.

Comment thread Pinta.Effects/Effects/FeatherEffect.cs Outdated

public override void Render (ImageSurface src, ImageSurface dest, ReadOnlySpan<RectangleI> rois)
{
foreach (var roi in rois) {

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.

With IsTileable = false, I think the effect should only ever be invoked with a single region, so it would be slightly simpler to instead override the Render signature that takes a single rectangle (similar to the Dithering effect)

Comment thread Pinta.Effects/Effects/FeatherEffect.cs Outdated
// crashes on test if try-catch not implemented
int threadCount = 0;
try {
threadCount = PintaCore.System.RenderThreads;

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.

The proper fix here would be to add an ISystemService similar to the IChromeService , so the effect doesn't need to depend on having all of PintaCore initialized and the unit tests can pass in a mock implementation

Comment thread Pinta.Effects/Effects/FeatherEffect.cs Outdated
} catch {
threadCount = 1;
}
var slaves = new Thread[threadCount];

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.

Using Parallel.For would likely be a lot simpler than manually managing threads (an example is in the Paint Bucket tool) - the code from the LivePreviewManager should probably be refactored to do this in the future too

For limiting the number of threads, something like https://stackoverflow.com/a/15931412 should work

Comment thread Pinta.Effects/Effects/FeatherEffect.cs Outdated
int pixel_index = py * src_width + px;
float mult = distance / radius;
byte alpha = (byte) (src_data[pixel_index].A * mult);
if (alpha < dst_data[pixel_index].A)

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.

I'm not sure this is thread safe - multiple threads could be modifying the same pixels simultaneously?

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 checked. Unfortunately, you are right.
I'll rewrite it to work on a per-pixel basis. Was avoiding that because it's more expensive and requires an extra pass, but I guess it's inevitable.

@potatoes1286

Copy link
Copy Markdown
Contributor Author

Implemented feedback. Should be thread safe now.
While I did implement ISystemService, I am not very familiar with DI so do correct me if I messed up a part of it.
I said I was avoiding working by-pixel instead of by-border pixel for performance reasons, but I feel like the new code is more responsive. (Guesswork. Haven't measured it.)

@cameronwhite cameronwhite 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.

Looks good! I just had one question in the comments about the loop range

Comment thread Pinta.Effects/Effects/FeatherEffect.cs Outdated
// Color in any pixel that the stencil says we need to fill
// First pass
// Clean up dest, then collect all border pixels
Parallel.For (0, bottom, new ParallelOptions { MaxDegreeOfParallelism = threads }, y => {

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.

Should this loop start from the top of the rectangle, rather than the top of the image?
Same for the second parallel loop too

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.

Oops... Good catch!

int src_width = src.Width;
var dst_data = dest.GetPixelData ();

var relevantBorderPixels = borderPixels.Where (borderPixel => borderPixel.Y > py - radius && borderPixel.Y < py + radius).ToArray ();

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.

A future improvement for another PR would be to set up an acceleration structure for the closest border pixel lookup (e.g. KD tree), to avoid going through all of the border pixels for every row. I think we have a spatial hashing implementation in the shape tools as well

@cameronwhite

Copy link
Copy Markdown
Member

Looks good, thanks for your contribution!

@cameronwhite cameronwhite merged commit 7a4f9d8 into PintaProject:master Sep 2, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feather tool

2 participants