Skip to content

Commit 54b7e04

Browse files
Fix #2518 (#2519)
* OilPaint benchmark * fix #2518 * Update OilPaintingProcessor{TPixel}.cs * clamp the vector to 0..1 and undo buffer overallocation * throw ImageProcessingException instead of clamping --------- Co-authored-by: James Jackson-South <[email protected]>
1 parent 9335a16 commit 54b7e04

File tree

3 files changed

+55
-24
lines changed

3 files changed

+55
-24
lines changed

src/ImageSharp/Processing/Processors/Effects/OilPaintingProcessor{TPixel}.cs

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Buffers;
55
using System.Numerics;
66
using System.Runtime.CompilerServices;
7+
using System.Runtime.InteropServices;
78
using SixLabors.ImageSharp.Advanced;
89
using SixLabors.ImageSharp.Memory;
910
using SixLabors.ImageSharp.PixelFormats;
@@ -34,17 +35,25 @@ public OilPaintingProcessor(Configuration configuration, OilPaintingProcessor de
3435
/// <inheritdoc/>
3536
protected override void OnFrameApply(ImageFrame<TPixel> source)
3637
{
38+
int levels = Math.Clamp(this.definition.Levels, 1, 255);
3739
int brushSize = Math.Clamp(this.definition.BrushSize, 1, Math.Min(source.Width, source.Height));
3840

3941
using Buffer2D<TPixel> targetPixels = this.Configuration.MemoryAllocator.Allocate2D<TPixel>(source.Size());
4042

4143
source.CopyTo(targetPixels);
4244

43-
RowIntervalOperation operation = new(this.SourceRectangle, targetPixels, source.PixelBuffer, this.Configuration, brushSize >> 1, this.definition.Levels);
44-
ParallelRowIterator.IterateRowIntervals(
45+
RowIntervalOperation operation = new(this.SourceRectangle, targetPixels, source.PixelBuffer, this.Configuration, brushSize >> 1, levels);
46+
try
47+
{
48+
ParallelRowIterator.IterateRowIntervals(
4549
this.Configuration,
4650
this.SourceRectangle,
4751
in operation);
52+
}
53+
catch (Exception ex)
54+
{
55+
throw new ImageProcessingException("The OilPaintProcessor failed. The most likely reason is that a pixel component was outside of its' allowed range.", ex);
56+
}
4857

4958
Buffer2D<TPixel>.SwapOrCopyContent(source.PixelBuffer, targetPixels);
5059
}
@@ -105,18 +114,18 @@ public void Invoke(in RowInterval rows)
105114
Span<Vector4> targetRowVector4Span = targetRowBuffer.Memory.Span;
106115
Span<Vector4> targetRowAreaVector4Span = targetRowVector4Span.Slice(this.bounds.X, this.bounds.Width);
107116

108-
ref float binsRef = ref bins.GetReference();
109-
ref int intensityBinRef = ref Unsafe.As<float, int>(ref binsRef);
110-
ref float redBinRef = ref Unsafe.Add(ref binsRef, (uint)this.levels);
111-
ref float blueBinRef = ref Unsafe.Add(ref redBinRef, (uint)this.levels);
112-
ref float greenBinRef = ref Unsafe.Add(ref blueBinRef, (uint)this.levels);
117+
Span<float> binsSpan = bins.GetSpan();
118+
Span<int> intensityBinsSpan = MemoryMarshal.Cast<float, int>(binsSpan);
119+
Span<float> redBinSpan = binsSpan[this.levels..];
120+
Span<float> blueBinSpan = redBinSpan[this.levels..];
121+
Span<float> greenBinSpan = blueBinSpan[this.levels..];
113122

114123
for (int y = rows.Min; y < rows.Max; y++)
115124
{
116125
Span<TPixel> sourceRowPixelSpan = this.source.DangerousGetRowSpan(y);
117126
Span<TPixel> sourceRowAreaPixelSpan = sourceRowPixelSpan.Slice(this.bounds.X, this.bounds.Width);
118127

119-
PixelOperations<TPixel>.Instance.ToVector4(this.configuration, sourceRowAreaPixelSpan, sourceRowAreaVector4Span);
128+
PixelOperations<TPixel>.Instance.ToVector4(this.configuration, sourceRowAreaPixelSpan, sourceRowAreaVector4Span, PixelConversionModifiers.Scale);
120129

121130
for (int x = this.bounds.X; x < this.bounds.Right; x++)
122131
{
@@ -140,29 +149,29 @@ public void Invoke(in RowInterval rows)
140149
int offsetX = x + fxr;
141150
offsetX = Numerics.Clamp(offsetX, 0, maxX);
142151

143-
Vector4 vector = sourceOffsetRow[offsetX].ToVector4();
152+
Vector4 vector = sourceOffsetRow[offsetX].ToScaledVector4();
144153

145154
float sourceRed = vector.X;
146155
float sourceBlue = vector.Z;
147156
float sourceGreen = vector.Y;
148157

149158
int currentIntensity = (int)MathF.Round((sourceBlue + sourceGreen + sourceRed) / 3F * (this.levels - 1));
150159

151-
Unsafe.Add(ref intensityBinRef, (uint)currentIntensity)++;
152-
Unsafe.Add(ref redBinRef, (uint)currentIntensity) += sourceRed;
153-
Unsafe.Add(ref blueBinRef, (uint)currentIntensity) += sourceBlue;
154-
Unsafe.Add(ref greenBinRef, (uint)currentIntensity) += sourceGreen;
160+
intensityBinsSpan[currentIntensity]++;
161+
redBinSpan[currentIntensity] += sourceRed;
162+
blueBinSpan[currentIntensity] += sourceBlue;
163+
greenBinSpan[currentIntensity] += sourceGreen;
155164

156-
if (Unsafe.Add(ref intensityBinRef, (uint)currentIntensity) > maxIntensity)
165+
if (intensityBinsSpan[currentIntensity] > maxIntensity)
157166
{
158-
maxIntensity = Unsafe.Add(ref intensityBinRef, (uint)currentIntensity);
167+
maxIntensity = intensityBinsSpan[currentIntensity];
159168
maxIndex = currentIntensity;
160169
}
161170
}
162171

163-
float red = MathF.Abs(Unsafe.Add(ref redBinRef, (uint)maxIndex) / maxIntensity);
164-
float blue = MathF.Abs(Unsafe.Add(ref blueBinRef, (uint)maxIndex) / maxIntensity);
165-
float green = MathF.Abs(Unsafe.Add(ref greenBinRef, (uint)maxIndex) / maxIntensity);
172+
float red = redBinSpan[maxIndex] / maxIntensity;
173+
float blue = blueBinSpan[maxIndex] / maxIntensity;
174+
float green = greenBinSpan[maxIndex] / maxIntensity;
166175
float alpha = sourceRowVector4Span[x].W;
167176

168177
targetRowVector4Span[x] = new Vector4(red, green, blue, alpha);
@@ -171,7 +180,7 @@ public void Invoke(in RowInterval rows)
171180

172181
Span<TPixel> targetRowAreaPixelSpan = this.targetPixels.DangerousGetRowSpan(y).Slice(this.bounds.X, this.bounds.Width);
173182

174-
PixelOperations<TPixel>.Instance.FromVector4Destructive(this.configuration, targetRowAreaVector4Span, targetRowAreaPixelSpan);
183+
PixelOperations<TPixel>.Instance.FromVector4Destructive(this.configuration, targetRowAreaVector4Span, targetRowAreaPixelSpan, PixelConversionModifiers.Scale);
175184
}
176185
}
177186
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Six Labors Split License.
3+
4+
using BenchmarkDotNet.Attributes;
5+
using SixLabors.ImageSharp.PixelFormats;
6+
using SixLabors.ImageSharp.Processing;
7+
8+
namespace SixLabors.ImageSharp.Benchmarks.Processing;
9+
10+
[Config(typeof(Config.MultiFramework))]
11+
public class OilPaint
12+
{
13+
[Benchmark]
14+
public void DoOilPaint()
15+
{
16+
using Image<RgbaVector> image = new Image<RgbaVector>(1920, 1200, new(127, 191, 255));
17+
image.Mutate(ctx => ctx.OilPaint());
18+
}
19+
}

tests/ImageSharp.Tests/Processing/Processors/Effects/OilPaintTest.cs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,26 +27,29 @@ public class OilPaintTest
2727
[WithFileCollection(nameof(InputImages), nameof(OilPaintValues), PixelTypes.Rgba32)]
2828
public void FullImage<TPixel>(TestImageProvider<TPixel> provider, int levels, int brushSize)
2929
where TPixel : unmanaged, IPixel<TPixel>
30-
{
31-
provider.RunValidatingProcessorTest(
30+
=> provider.RunValidatingProcessorTest(
3231
x =>
3332
{
3433
x.OilPaint(levels, brushSize);
3534
return $"{levels}-{brushSize}";
3635
},
3736
ImageComparer.TolerantPercentage(0.01F),
3837
appendPixelTypeToFileName: false);
39-
}
4038

4139
[Theory]
4240
[WithFileCollection(nameof(InputImages), nameof(OilPaintValues), PixelTypes.Rgba32)]
4341
[WithTestPatternImages(nameof(OilPaintValues), 100, 100, PixelTypes.Rgba32)]
4442
public void InBox<TPixel>(TestImageProvider<TPixel> provider, int levels, int brushSize)
4543
where TPixel : unmanaged, IPixel<TPixel>
46-
{
47-
provider.RunRectangleConstrainedValidatingProcessorTest(
44+
=> provider.RunRectangleConstrainedValidatingProcessorTest(
4845
(x, rect) => x.OilPaint(levels, brushSize, rect),
4946
$"{levels}-{brushSize}",
5047
ImageComparer.TolerantPercentage(0.01F));
48+
49+
[Fact]
50+
public void Issue2518_PixelComponentOutsideOfRange_ThrowsImageProcessingException()
51+
{
52+
using Image<RgbaVector> image = new(10, 10, new RgbaVector(1, 1, 100));
53+
Assert.Throws<ImageProcessingException>(() => image.Mutate(ctx => ctx.OilPaint()));
5154
}
5255
}

0 commit comments

Comments
 (0)