Skip to content

Commit

Permalink
Add Outline Object Effect (#971)
Browse files Browse the repository at this point in the history
* Change Outline > OutlineEdge, change Feather name to Feather Object
  • Loading branch information
potatoes1286 authored Sep 16, 2024
1 parent 609484c commit b9e28ac
Show file tree
Hide file tree
Showing 12 changed files with 270 additions and 12 deletions.
6 changes: 4 additions & 2 deletions Pinta.Effects/CoreEffectsExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,14 @@ public void Initialize ()
PintaCore.Effects.RegisterEffect (new GaussianBlurEffect (services));
PintaCore.Effects.RegisterEffect (new GlowEffect (services));
PintaCore.Effects.RegisterEffect (new FeatherEffect (services));
PintaCore.Effects.RegisterEffect (new OutlineObjectEffect (services));
PintaCore.Effects.RegisterEffect (new InkSketchEffect (services));
PintaCore.Effects.RegisterEffect (new JuliaFractalEffect (services));
PintaCore.Effects.RegisterEffect (new MandelbrotFractalEffect (services));
PintaCore.Effects.RegisterEffect (new MedianEffect (services));
PintaCore.Effects.RegisterEffect (new MotionBlurEffect (services));
PintaCore.Effects.RegisterEffect (new OilPaintingEffect (services));
PintaCore.Effects.RegisterEffect (new OutlineEffect (services));
PintaCore.Effects.RegisterEffect (new OutlineEdgeEffect (services));
PintaCore.Effects.RegisterEffect (new PencilSketchEffect (services));
PintaCore.Effects.RegisterEffect (new PixelateEffect (services));
PintaCore.Effects.RegisterEffect (new PolarInversionEffect (services));
Expand Down Expand Up @@ -120,13 +121,14 @@ public void Uninitialize ()
PintaCore.Effects.UnregisterInstanceOfEffect (typeof (GaussianBlurEffect));
PintaCore.Effects.UnregisterInstanceOfEffect (typeof (GlowEffect));
PintaCore.Effects.UnregisterInstanceOfEffect (typeof (FeatherEffect));
PintaCore.Effects.UnregisterInstanceOfEffect (typeof (OutlineObjectEffect));
PintaCore.Effects.UnregisterInstanceOfEffect (typeof (InkSketchEffect));
PintaCore.Effects.UnregisterInstanceOfEffect (typeof (JuliaFractalEffect));
PintaCore.Effects.UnregisterInstanceOfEffect (typeof (MandelbrotFractalEffect));
PintaCore.Effects.UnregisterInstanceOfEffect (typeof (MedianEffect));
PintaCore.Effects.UnregisterInstanceOfEffect (typeof (MotionBlurEffect));
PintaCore.Effects.UnregisterInstanceOfEffect (typeof (OilPaintingEffect));
PintaCore.Effects.UnregisterInstanceOfEffect (typeof (OutlineEffect));
PintaCore.Effects.UnregisterInstanceOfEffect (typeof (OutlineEdgeEffect));
PintaCore.Effects.UnregisterInstanceOfEffect (typeof (PencilSketchEffect));
PintaCore.Effects.UnregisterInstanceOfEffect (typeof (PixelateEffect));
PintaCore.Effects.UnregisterInstanceOfEffect (typeof (PolarInversionEffect));
Expand Down
2 changes: 1 addition & 1 deletion Pinta.Effects/Effects/FeatherEffect.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public sealed class FeatherEffect : BaseEffect
// Takes two passes, so must be multithreaded internally
public sealed override bool IsTileable => false;

public override string Name => Translations.GetString ("Feather");
public override string Name => Translations.GetString ("Feather Object");

public override bool IsConfigurable => true;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

namespace Pinta.Effects;

public sealed class OutlineEffect : LocalHistogramEffect
public sealed class OutlineEdgeEffect : LocalHistogramEffect
{
private int thickness;
private int intensity;
Expand All @@ -23,20 +23,20 @@ public sealed class OutlineEffect : LocalHistogramEffect

public sealed override bool IsTileable => true;

public override string Name => Translations.GetString ("Outline");
public override string Name => Translations.GetString ("Outline Edge");

public override bool IsConfigurable => true;

public override string EffectMenuCategory => Translations.GetString ("Stylize");

public OutlineData Data => (OutlineData) EffectData!; // NRT - Set in constructor
public OutlineEdgeData Data => (OutlineEdgeData) EffectData!; // NRT - Set in constructor

private readonly IChromeService chrome;

public OutlineEffect (IServiceProvider services)
public OutlineEdgeEffect (IServiceProvider services)
{
chrome = services.GetService<IChromeService> ();
EffectData = new OutlineData ();
EffectData = new OutlineEdgeData ();
}

public override void LaunchConfiguration ()
Expand Down Expand Up @@ -131,7 +131,7 @@ public override void Render (ImageSurface src, ImageSurface dest, ReadOnlySpan<R

#endregion

public sealed class OutlineData : EffectData
public sealed class OutlineEdgeData : EffectData
{
[Caption ("Thickness"), MinimumValue (1), MaximumValue (200)]
public int Thickness { get; set; } = 3;
Expand Down
194 changes: 194 additions & 0 deletions Pinta.Effects/Effects/OutlineObjectEffect.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Threading.Tasks;
using Cairo;
using Pinta.Core;
using Pinta.Gui.Widgets;

namespace Pinta.Effects;

public sealed class OutlineObjectEffect : BaseEffect
{

public override string Icon => Pinta.Resources.Icons.EffectsStylizeOutline;

// Takes two passes, so must be multithreaded internally
public sealed override bool IsTileable => false;

public override string Name => Translations.GetString ("Outline Object");

public override bool IsConfigurable => true;

public override string EffectMenuCategory => Translations.GetString ("Object");

public OutlineObjectData Data => (OutlineObjectData) EffectData!; // NRT - Set in constructor

private readonly IChromeService chrome;
private readonly ISystemService system;
private readonly IPaletteService palette;

public OutlineObjectEffect (IServiceProvider services)
{
chrome = services.GetService<IChromeService> ();
system = services.GetService<ISystemService> ();
palette = services.GetService<IPaletteService> ();
EffectData = new OutlineObjectData ();
}

public override void LaunchConfiguration ()
=> chrome.LaunchSimpleEffectDialog (this);

protected override void Render (ImageSurface src, ImageSurface dest, RectangleI roi)
{
int top = roi.Top;
int bottom = roi.Bottom;
int left = roi.Left;
int right = roi.Right;
int srcHeight = src.Height;
int srcWidth = src.Width;
int radius = Data.Radius;
int threads = system.RenderThreads;

ColorBgra primaryColor = palette.PrimaryColor.ToColorBgra ();
ColorBgra secondaryColor = palette.SecondaryColor.ToColorBgra ();
ConcurrentBag<PointI> borderPixels = new ConcurrentBag<PointI> ();

// First pass
// Clean up dest, then collect all border pixels
Parallel.For (top, bottom + 1, new ParallelOptions { MaxDegreeOfParallelism = threads }, y => {
var srcData = src.GetReadOnlyPixelData ();

// reset dest to src
// Removing this causes preview to not update to lower radius levels
var srcRow = srcData.Slice (y * srcWidth, srcWidth);
var dstRow = dest.GetPixelData ().Slice (y * srcWidth, srcWidth);
srcRow.CopyTo (dstRow);

// Produces different behaviour at radius == 0 and radius == 1
// When radius == 0, only consider direct border pixels
// When radius == 1, consider border pixels on diagonal
Span<PointI> pixels = stackalloc PointI[8];
// Collect a list of pixels that surround the object (border pixels)
for (int x = left; x <= right; x++) {
PointI potentialBorderPixel = new (x, y);
if (Data.OutlineBorder && (x == 0 || x == srcWidth - 1 || y == 0 || y == srcHeight - 1)) {
borderPixels.Add (potentialBorderPixel);
} else if (src.GetColorBgra (srcData, srcWidth, potentialBorderPixel).A <= Data.Tolerance) {
// Test pixel above, below, left, & right
pixels[0] = new (x - 1, y);
pixels[1] = new (x + 1, y);
pixels[2] = new (x, y - 1);
pixels[3] = new (x, y + 1);
int pixelCount = 4;
if (radius == 1) {
// if radius == 1, also test pixels on diagonals
pixels[4] = new (x - 1, y - 1);
pixels[5] = new (x - 1, y + 1);
pixels[6] = new (x + 1, y - 1);
pixels[7] = new (x + 1, y + 1);
pixelCount = 8;
}

for (int i = 0; i < pixelCount; i++) {
var px = pixels[i].X;
var py = pixels[i].Y;
if (px < 0 || px >= srcWidth || py < 0 || py >= srcHeight)
continue;
if (src.GetColorBgra (srcData, srcWidth, new PointI (px, py)).A > Data.Tolerance) {
borderPixels.Add (potentialBorderPixel);
// Remove comments below to draw border pixels
// You will also have to comment out the 2nd pass because it will overwrite this
//int pos = srcWidth * y + x;
//borderData[pos].Bgra = 0;
//borderData[pos].A = 255;

break;
}
}


}
}
});


// Second pass
// Generate outline and blend to dest
Parallel.For (top, bottom + 1, new ParallelOptions { MaxDegreeOfParallelism = threads }, y => {
// otherwise produces nothing at radius == 0
if (radius == 0)
radius = 1;
var relevantBorderPixels = borderPixels.Where (borderPixel => borderPixel.Y > y - radius && borderPixel.Y < y + radius).ToArray ();
var destRow = dest.GetPixelData ().Slice (y * srcWidth, srcWidth);
Span<ColorBgra> outlineRow = stackalloc ColorBgra[destRow.Length];

for (int x = left; x <= right; x++) {
byte highestAlpha = 0;

// optimization: no change if destination has max alpha already
if (destRow[x].A == 255)
continue;

if (Data.FillObjectBackground && destRow[x].A >= Data.Tolerance)
highestAlpha = 255;

// Grab nearest border pixel, and calculate outline alpha based off it
foreach (var borderPixel in relevantBorderPixels) {
if (borderPixel.X == x && borderPixel.Y == y)
highestAlpha = 255;

if (highestAlpha == 255)
break;

if (borderPixel.X > x - radius && borderPixel.X < x + radius) {
var dx = borderPixel.X - x;
var dy = borderPixel.Y - y;
float distance = MathF.Sqrt (dx * dx + dy * dy);
if (distance <= radius) {
float mult = 1 - distance / radius;
if (mult <= 0)
continue;
byte alpha = (byte) (255 * mult);
if (alpha > highestAlpha)
highestAlpha = alpha;
}
}
}

// Handle color gradient / no alpha gradient option
var color = primaryColor;
if (Data.ColorGradient)
color = ColorBgra.Blend (secondaryColor, primaryColor, highestAlpha);
if (!Data.AlphaGradient && highestAlpha != 0)
highestAlpha = 255;

outlineRow[x] = color.NewAlpha (highestAlpha).ToPremultipliedAlpha ();
}
// Performs alpha blending
new UserBlendOps.NormalBlendOp ().Apply (outlineRow, destRow);
outlineRow.CopyTo (destRow);
});
}

public sealed class OutlineObjectData : EffectData
{
[Caption ("Radius"), MinimumValue (0), MaximumValue (100)]
public int Radius { get; set; } = 6;

[Caption ("Tolerance"), MinimumValue (0), MaximumValue (255)]
public int Tolerance { get; set; } = 20;

[Caption ("Alpha Gradient")]
public bool AlphaGradient { get; set; } = true;

[Caption ("Color Gradient")]
public bool ColorGradient { get; set; } = true;

[Caption ("Outline Border")]
public bool OutlineBorder { get; set; } = false;

[Caption ("Fill Object Background")]
public bool FillObjectBackground { get; set; } = true;
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
66 changes: 64 additions & 2 deletions tests/Pinta.Effects.Tests/EffectsTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -340,14 +340,14 @@ public void OilPainting2 ()
[Test]
public void Outline1 ()
{
OutlineEffect effect = new (Utilities.CreateMockServices ());
OutlineEdgeEffect effect = new (Utilities.CreateMockServices ());
Utilities.TestEffect (effect, "outline1.png");
}

[Test]
public void Outline2 ()
{
OutlineEffect effect = new (Utilities.CreateMockServices ());
OutlineEdgeEffect effect = new (Utilities.CreateMockServices ());
effect.Data.Thickness = 25;
effect.Data.Intensity = 20;
Utilities.TestEffect (effect, "outline2.png");
Expand Down Expand Up @@ -634,10 +634,72 @@ public void AlignObject2 ()
effect.Data.Position = AlignPosition.Center;
Utilities.TestEffect (effect, "alignobject2.png", source_image_name: "alignobjectinput.png");
}
[Test]
public void AlignObject3 ()
{
AlignObjectEffect effect = new (Utilities.CreateMockServices ());
effect.Data.Position = AlignPosition.BottomRight;
Utilities.TestEffect (effect, "alignobject3.png", source_image_name: "alignobjectinput.png");
}

[Test]
public void OutlineObject1 ()
{
OutlineObjectEffect effect = new (Utilities.CreateMockServices ());
effect.Data.Radius = 10;
effect.Data.Tolerance = 130;
effect.Data.AlphaGradient = true;
effect.Data.ColorGradient = false;
effect.Data.OutlineBorder = false;
effect.Data.FillObjectBackground = false;
Utilities.TestEffect (effect, "outlineobject1.png", source_image_name: "outlineobjectinput.png");
}
[Test]
public void OutlineObject2 ()
{
OutlineObjectEffect effect = new (Utilities.CreateMockServices ());
effect.Data.Radius = 10;
effect.Data.Tolerance = 20;
effect.Data.AlphaGradient = false;
effect.Data.ColorGradient = true;
effect.Data.OutlineBorder = true;
effect.Data.FillObjectBackground = false;
Utilities.TestEffect (effect, "outlineobject2.png", source_image_name: "outlineobjectinput.png");
}
[Test]
public void OutlineObject3 ()
{
OutlineObjectEffect effect = new (Utilities.CreateMockServices ());
effect.Data.Radius = 10;
effect.Data.Tolerance = 20;
effect.Data.AlphaGradient = true;
effect.Data.ColorGradient = true;
effect.Data.OutlineBorder = false;
effect.Data.FillObjectBackground = true;
Utilities.TestEffect (effect, "outlineobject3.png", source_image_name: "outlineobjectinput.png");
}
[Test]
public void OutlineObject4 ()
{
OutlineObjectEffect effect = new (Utilities.CreateMockServices ());
effect.Data.Radius = 1;
effect.Data.Tolerance = 20;
effect.Data.AlphaGradient = false;
effect.Data.ColorGradient = false;
effect.Data.OutlineBorder = false;
effect.Data.FillObjectBackground = false;
Utilities.TestEffect (effect, "outlineobject4.png", source_image_name: "outlineobjectinput.png");
}
[Test]
public void OutlineObject5 ()
{
OutlineObjectEffect effect = new (Utilities.CreateMockServices ());
effect.Data.Radius = 0;
effect.Data.Tolerance = 20;
effect.Data.AlphaGradient = false;
effect.Data.ColorGradient = false;
effect.Data.OutlineBorder = false;
effect.Data.FillObjectBackground = false;
Utilities.TestEffect (effect, "outlineobject5.png", source_image_name: "outlineobjectinput.png");
}
}
2 changes: 1 addition & 1 deletion tests/PintaBenchmarks/EffectsBenchmarks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ public void OilPaintingEffect ()
[Benchmark]
public void OutlineEffect ()
{
var effect = new OutlineEffect (Utilities.CreateMockServices ());
var effect = new OutlineEdgeEffect (Utilities.CreateMockServices ());
effect.Render (surface, dest_surface, bounds);
}

Expand Down

0 comments on commit b9e28ac

Please sign in to comment.