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

Supersampling for smoother Voronoi diagrams #1128

Merged
merged 5 commits into from
Nov 14, 2024
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
2 changes: 2 additions & 0 deletions Pinta.Core/Classes/Point.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ public readonly record struct PointI (int X, int Y)
public static PointI Zero { get; } = new (0, 0);
public override readonly string ToString () => $"{X}, {Y}";

public readonly PointD ToDouble () => new (X, Y);

public PointI Rotated90CCW () // Counterclockwise
=> new (-Y, X);

Expand Down
126 changes: 88 additions & 38 deletions Pinta.Effects/Effects/VoronoiDiagramEffect.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ namespace Pinta.Effects;
public sealed class VoronoiDiagramEffect : BaseEffect
{
public override string Icon
=> Pinta.Resources.Icons.EffectsRenderVoronoiDiagram;
=> Resources.Icons.EffectsRenderVoronoiDiagram;

public override bool IsTileable
=> false;
=> true;

public override string Name
=> Translations.GetString ("Voronoi Diagram");
Expand All @@ -31,9 +31,11 @@ public VoronoiDiagramData Data
=> (VoronoiDiagramData) EffectData!; // NRT - Set in constructor

private readonly IChromeService chrome;
private readonly ILivePreview live_preview;
public VoronoiDiagramEffect (IServiceProvider services)
{
chrome = services.GetService<IChromeService> ();
live_preview = services.GetService<ILivePreview> ();
EffectData = new VoronoiDiagramData ();
}

Expand All @@ -42,60 +44,77 @@ public override Task<bool> LaunchConfiguration ()

private sealed record VoronoiSettings (
Size size,
bool showPoints,
ImmutableArray<PointI> points,
ImmutableArray<PointD> controlPoints,
ImmutableArray<PointD> samplingLocations,
ImmutableArray<ColorBgra> colors,
Func<PointI, PointI, double> distanceCalculator);
Func<PointD, PointD, double> distanceCalculator);

private VoronoiSettings CreateSettings (ImageSurface dst, RectangleI roi)
private VoronoiSettings CreateSettings (ImageSurface dst)
{
ColorSorting colorSorting = Data.ColorSorting;
VoronoiDiagramData data = Data;

RectangleI roi = live_preview.RenderBounds;

ColorSorting colorSorting = data.ColorSorting;

IEnumerable<PointI> basePoints = CreatePoints (roi, Data.NumberOfCells, Data.RandomPointLocations);
ImmutableArray<PointI> points = SortPoints (basePoints, colorSorting).ToImmutableArray ();
PointD locationOffset = new (0.5, 0.5);

IEnumerable<ColorBgra> baseColors = CreateColors (points.Length, Data.RandomColors);
IEnumerable<PointI> basePoints = CreatePoints (roi, data.NumberOfCells, data.RandomPointLocations);
IEnumerable<PointI> pointCorners = SortPoints (basePoints, colorSorting).ToImmutableArray ();
ImmutableArray<PointD> controlPoints = pointCorners.Select (p => p.ToDouble () + locationOffset).ToImmutableArray ();

IEnumerable<ColorBgra> baseColors = CreateColors (controlPoints.Length, data.RandomColors);
IEnumerable<ColorBgra> positionSortedColors = SortColors (baseColors, colorSorting);
IEnumerable<ColorBgra> reversedSortingColors = Data.ReverseColorSorting ? positionSortedColors.Reverse () : positionSortedColors;
IEnumerable<ColorBgra> reversedSortingColors = data.ReverseColorSorting ? positionSortedColors.Reverse () : positionSortedColors;

return new (
size: dst.GetSize (),
showPoints: Data.ShowPoints,
points: points,
controlPoints: controlPoints,
samplingLocations: CreateSamplingLocations (data.Quality),
colors: reversedSortingColors.ToImmutableArray (),
distanceCalculator: GetDistanceCalculator (Data.DistanceMetric)
distanceCalculator: GetDistanceCalculator (data.DistanceMetric)
);
}

protected override void Render (ImageSurface src, ImageSurface dst, RectangleI roi)
protected override void Render (
ImageSurface src,
ImageSurface dst,
RectangleI roi)
{
VoronoiSettings settings = CreateSettings (dst, roi);
VoronoiSettings settings = CreateSettings (dst);
Span<ColorBgra> dst_data = dst.GetPixelData ();
foreach (var kvp in roi.GeneratePixelOffsets (settings.size).AsParallel ().Select (CreateColor))
foreach (var kvp in roi.GeneratePixelOffsets (settings.size).Select (CreateColor))
dst_data[kvp.Key] = kvp.Value;

KeyValuePair<int, ColorBgra> CreateColor (PixelOffset pixel)
{
int sampleCount = settings.samplingLocations.Length;
Span<ColorBgra> samples = stackalloc ColorBgra[sampleCount];
for (int i = 0; i < sampleCount; i++) {
PointD sampleLocation = pixel.coordinates.ToDouble () + settings.samplingLocations[i];
ColorBgra sample = GetColorForLocation (sampleLocation);
samples[i] = sample;
}
return KeyValuePair.Create (
pixel.memoryOffset,
ColorBgra.Blend (samples));
}

ColorBgra GetColorForLocation (PointD location)
{
double shortestDistance = double.MaxValue;
int closestIndex = 0;

for (var i = 0; i < settings.points.Length; i++) {
for (var i = 0; i < settings.controlPoints.Length; i++) {
// TODO: Acceleration structure that limits the search
// to a relevant subset of points, for better performance.
// Some ideas to consider: quadtree, spatial hashing
var point = settings.points[i];
double distance = settings.distanceCalculator (point, pixel.coordinates);
PointD controlPoint = settings.controlPoints[i];
double distance = settings.distanceCalculator (location, controlPoint);
if (distance > shortestDistance) continue;
shortestDistance = distance;
closestIndex = i;
}

ColorBgra finalColor =
settings.showPoints && shortestDistance == 0
? ColorBgra.Black
: settings.colors[closestIndex];

return KeyValuePair.Create (pixel.memoryOffset, finalColor);
return settings.colors[closestIndex];
}
}

Expand Down Expand Up @@ -135,7 +154,7 @@ or ColorSorting.VerticalG
typeof (ColorSorting)),
};

private static Func<PointI, PointI, double> GetDistanceCalculator (DistanceMetric distanceCalculationMethod)
private static Func<PointD, PointD, double> GetDistanceCalculator (DistanceMetric distanceCalculationMethod)
{
return distanceCalculationMethod switch {
DistanceMetric.Euclidean => Euclidean,
Expand All @@ -147,21 +166,21 @@ private static Func<PointI, PointI, double> GetDistanceCalculator (DistanceMetri
typeof (DistanceMetric)),
};

static double Euclidean (PointI targetPoint, PointI pixelLocation)
static double Euclidean (PointD targetPoint, PointD pixelLocation)
{
PointI difference = pixelLocation - targetPoint;
PointD difference = pixelLocation - targetPoint;
return difference.Magnitude ();
}

static double Manhattan (PointI targetPoint, PointI pixelLocation)
static double Manhattan (PointD targetPoint, PointD pixelLocation)
{
PointI difference = pixelLocation - targetPoint;
PointD difference = pixelLocation - targetPoint;
return Math.Abs (difference.X) + Math.Abs (difference.Y);
}

static double Chebyshev (PointI targetPoint, PointI pixelLocation)
static double Chebyshev (PointD targetPoint, PointD pixelLocation)
{
PointI difference = pixelLocation - targetPoint;
PointD difference = pixelLocation - targetPoint;
return Math.Max (Math.Abs (difference.X), Math.Abs (difference.Y));
}
}
Expand Down Expand Up @@ -198,6 +217,34 @@ private static IEnumerable<ColorBgra> CreateColors (int colorCount, RandomSeed c
}
}

/// <returns>
/// Offsets, from top left corner of points,
/// where samples should be taken.
/// </returns>
/// <remarks>
/// The resulting colors are intended to be blended.
/// </remarks>
private static ImmutableArray<PointD> CreateSamplingLocations (int quality)
{
var builder = ImmutableArray.CreateBuilder<PointD> ();
builder.Capacity = quality * quality;
double sectionSize = 1.0 / quality;
double initial = sectionSize / 2;

for (int h = 0; h < quality; h++) {
for (int v = 0; v < quality; v++) {
double hOffset = sectionSize * h;
double vOffset = sectionSize * v;
PointD currentPoint = new (
X: initial + hOffset,
Y: initial + vOffset);
builder.Add (currentPoint);
}
}

return builder.MoveToImmutable ();
}

public sealed class VoronoiDiagramData : EffectData
{
[Caption ("Distance Metric")]
Expand All @@ -206,9 +253,7 @@ public sealed class VoronoiDiagramData : EffectData
[Caption ("Number of Cells"), MinimumValue (1), MaximumValue (1024)]
public int NumberOfCells { get; set; } = 100;

// Translators: The user can choose whether or not to render the points used in the calculation of a Voronoi diagram
[Caption ("Show Points")]
public bool ShowPoints { get; set; } = false;
// TODO: Show points

[Caption ("Color Sorting")]
public ColorSorting ColorSorting { get; set; } = ColorSorting.Random;
Expand All @@ -222,6 +267,10 @@ public sealed class VoronoiDiagramData : EffectData

[Caption ("Random Point Locations")]
public RandomSeed RandomPointLocations { get; set; } = new (0);

[Caption ("Quality")]
[MinimumValue (1), MaximumValue (4)]
public int Quality { get; set; } = 3;
}

public enum DistanceMetric
Expand All @@ -235,6 +284,7 @@ public enum ColorSorting
{
[Caption ("Random")] Random,


// Translators: Horizontal color sorting with blue (B) as the leading term
[Caption ("Horizontal blue (B)")] HorizontalB,

Expand Down
Binary file modified tests/Pinta.Effects.Tests/Assets/voronoi1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/Pinta.Effects.Tests/Assets/voronoi2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/Pinta.Effects.Tests/Assets/voronoi3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/Pinta.Effects.Tests/Assets/voronoi4.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/Pinta.Effects.Tests/Assets/voronoi5.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading