Change rmse and invert to bitwise ops/primitives

This commit is contained in:
Ben Olden-Cooligan 2022-09-25 13:07:01 -07:00
parent c8f724efd5
commit 10a2a1df8f
9 changed files with 259 additions and 88 deletions

View File

@ -1,5 +1,6 @@
using System.Drawing;
using System.Drawing.Imaging;
using NAPS2.Images.Bitwise;
namespace NAPS2.Images.Gdi;
@ -73,22 +74,14 @@ internal static class GdiPixelFormatFixer
return true;
}
private static unsafe void InvertPalette(Bitmap bitmap)
private static void InvertPalette(Bitmap bitmap)
{
var p = bitmap.Palette;
p.Entries[0] = Color.Black;
p.Entries[1] = Color.White;
bitmap.Palette = p;
using var lockState = GdiImageLockState.Create(bitmap, LockMode.ReadWrite, out var data);
for (int i = 0; i < data.h; i++)
{
var row = data.ptr + i * data.stride;
for (int j = 0; j < data.stride; j++)
{
var b = row + j;
*b = (byte) (~*b & 0xFF);
}
}
BitwisePrimitives.Invert(data);
}
private static void Redraw(ref Bitmap bitmap, PixelFormat newPixelFormat)

View File

@ -58,6 +58,8 @@ public abstract class BinaryBitwiseImageOp : BitwiseImageOp
ValidateConsistency(dst);
ValidateCore(src, dst);
StartCore();
var partitionSize = GetPartitionSize(src, dst);
var partitionCount = GetPartitionCount(src, dst);
if (partitionCount == 1)
@ -73,11 +75,13 @@ public abstract class BinaryBitwiseImageOp : BitwiseImageOp
PerformCore(src, dst, start, end);
});
}
FinishCore();
}
protected virtual int GetPartitionSize(BitwiseImageData src, BitwiseImageData dst) => src.h;
protected virtual int GetPartitionCount(BitwiseImageData src, BitwiseImageData dst) => 1;
protected virtual int GetPartitionCount(BitwiseImageData src, BitwiseImageData dst) => DefaultPartitionCount;
protected virtual void ValidateCore(BitwiseImageData src, BitwiseImageData dst)
{

View File

@ -7,7 +7,7 @@ public class BitwiseImageOp
public const int B_MULT = 114;
protected int DefaultPartitionCount { get; } = Math.Max(Math.Min(Environment.ProcessorCount / 2, 4), 1);
protected unsafe void ValidateConsistency(BitwiseImageData data)
{
if (data.ptr == (byte*)IntPtr.Zero)
@ -47,4 +47,12 @@ public class BitwiseImageOp
throw new ArgumentException("Invalid alpha");
}
}
protected virtual void StartCore()
{
}
protected virtual void FinishCore()
{
}
}

View File

@ -0,0 +1,64 @@
namespace NAPS2.Images.Bitwise;
public static class BitwisePrimitives
{
public static unsafe void Invert(BitwiseImageData data, int partStart = -1, int partEnd = -1)
{
if (partStart == -1) partStart = 0;
if (partEnd == -1) partEnd = data.h;
var longCount = data.stride / 8;
var remainingStart = longCount * 8;
for (int i = partStart; i < partEnd; i++)
{
var row = data.ptr + data.stride * i;
var rowL = (long*) row;
// Use long operations as much as possible for speed
for (int j = 0; j < longCount; j++)
{
var ptr = rowL + j;
var value = *ptr;
*ptr = ~value;
}
for (int j = remainingStart; j < data.stride; j++)
{
var ptr = row + j;
var value = *ptr;
*ptr = (byte) (~value & 0xFF);
}
}
}
public static unsafe void Fill(BitwiseImageData data, byte value, int partStart = -1, int partEnd = -1)
{
if (partStart == -1) partStart = 0;
if (partEnd == -1) partEnd = data.h;
var longCount = data.stride / 8;
var remainingStart = longCount * 8;
long valueL = 0;
for (int i = 0; i < 8; i++)
{
valueL = (valueL << 8) | value;
}
for (int i = partStart; i < partEnd; i++)
{
var row = data.ptr + data.stride * i;
var rowL = (long*) row;
// Use long operations as much as possible for speed
for (int j = 0; j < longCount; j++)
{
var ptr = rowL + j;
*ptr = valueL;
}
for (int j = remainingStart; j < data.stride; j++)
{
var ptr = row + j;
*ptr = value;
}
}
}
}

View File

@ -86,7 +86,7 @@ public class CopyBitwiseImageOp : BinaryBitwiseImageOp
}
if (src.invertColorSpace ^ dst.invertColorSpace)
{
Invert(dst, partStart, partEnd);
BitwisePrimitives.Invert(dst, partStart, partEnd);
}
}
@ -272,18 +272,4 @@ public class CopyBitwiseImageOp : BinaryBitwiseImageOp
Buffer.MemoryCopy(srcRow, dstRow, bytesPerRow, bytesPerRow);
}
}
private unsafe void Invert(BitwiseImageData data, int partStart, int partEnd)
{
for (int i = partStart; i < partEnd; i++)
{
var row = data.ptr + data.stride * i;
// TODO: Optimize with long operations
for (int j = 0; j < data.stride; j++)
{
var b = *(row + j);
*(row + j) = (byte) (~b & 0xFF);
}
}
}
}

View File

@ -0,0 +1,67 @@
namespace NAPS2.Images.Bitwise;
public class RmseBitwiseImageOp : BinaryBitwiseImageOp
{
protected override LockMode SrcLockMode => LockMode.ReadOnly;
protected override LockMode DstLockMode => LockMode.ReadOnly;
private long _count;
private long _total;
public double Rmse { get; private set; }
protected override void PerformCore(BitwiseImageData src, BitwiseImageData dst, int partStart, int partEnd)
{
if (src.bytesPerPixel is 3 or 4 && dst.bytesPerPixel is 3 or 4)
{
PerformRgba(src, dst, partStart, partEnd);
}
else
{
throw new InvalidOperationException("Unsupported pixel format");
}
}
private unsafe void PerformRgba(BitwiseImageData src, BitwiseImageData dst, int partStart, int partEnd)
{
long partCount = 0;
for (int i = partStart; i < partEnd; i++)
{
var srcRow = src.ptr + src.stride * i;
var dstRow = dst.ptr + dst.stride * i;
for (int j = 0; j < src.w; j++)
{
var srcPixel = srcRow + j * src.bytesPerPixel;
var dstPixel = dstRow + j * dst.bytesPerPixel;
int r1 = *(srcPixel + src.rOff);
int g1 = *(srcPixel + src.gOff);
int b1 = *(srcPixel + src.bOff);
int r2 = *(dstPixel + src.rOff);
int g2 = *(dstPixel + src.gOff);
int b2 = *(dstPixel + src.bOff);
partCount += (r1 - r2) * (r1 - r2) + (g1 - g2) * (g1 - g2) + (b1 - b2) * (b1 - b2);
// TODO: Should we validate alpha is 255 if only one image has alpha?
if (src.hasAlpha && dst.hasAlpha)
{
byte a1 = *(srcPixel + src.aOff);
byte a2 = *(dstPixel + dst.aOff);
partCount += (a1 - a2) * (a1 - a2);
}
}
}
lock (this)
{
_count += partCount;
_total += src.w * (partEnd - partStart) * (src.hasAlpha ? 4 : 3);
}
}
protected override void FinishCore()
{
Rmse = Math.Sqrt(_count / (double) _total);
}
}

View File

@ -22,6 +22,8 @@ public abstract class UnaryBitwiseImageOp : BitwiseImageOp
ValidateConsistency(data);
ValidateCore(data);
StartCore();
var partitionSize = GetPartitionSize(data);
var partitionCount = GetPartitionCount(data);
if (partitionCount == 1)
@ -37,6 +39,8 @@ public abstract class UnaryBitwiseImageOp : BitwiseImageOp
PerformCore(data, start, end);
});
}
FinishCore();
}
protected virtual int GetPartitionSize(BitwiseImageData data) => data.h;

View File

@ -77,43 +77,9 @@ public static class ImageAsserts
first = first.PerformTransform(new ColorBitDepthTransform());
second = second.PerformTransform(new ColorBitDepthTransform());
// TODO: Wrap in a bitwise op
using var lock1 = first.Lock(LockMode.ReadOnly, out var data1);
using var lock2 = second.Lock(LockMode.ReadOnly, out var data2);
int width = first.Width;
int height = first.Height;
long total = 0;
long div = width * height * 3;
for (int y = 0; y < height; y++)
{
byte* row1 = data1.ptr + data1.stride * y;
byte* row2 = data2.ptr + data2.stride * y;
for (int x = 0; x < width; x++)
{
byte* pixel1 = row1 + x * data1.bytesPerPixel;
byte* pixel2 = row2 + x * data2.bytesPerPixel;
byte r1 = *pixel1;
byte g1 = *(pixel1 + 1);
byte b1 = *(pixel1 + 2);
byte r2 = *pixel2;
byte g2 = *(pixel2 + 1);
byte b2 = *(pixel2 + 2);
total += (r1 - r2) * (r1 - r2) + (g1 - g2) * (g1 - g2) + (b1 - b2) * (b1 - b2);
// TODO: Should we validate alpha is 255 if there's a bpp mismatch?
if (data1.hasAlpha && data2.hasAlpha)
{
byte a1 = *(pixel1 + 3);
byte a2 = *(pixel2 + 3);
total += (a1 - a2) * (a1 - a2);
}
}
}
double rmse = Math.Sqrt(total / (double) div);
var op = new RmseBitwiseImageOp();
op.Perform(first, second);
var rmse = op.Rmse;
if (isSimilar)
{
Assert.True(rmse <= rmseThreshold, $"RMSE was {rmse}, expected <= {rmseThreshold}");

View File

@ -1,72 +1,87 @@
using NAPS2.Images.Bitwise;
using Xunit;
using Xunit.Abstractions;
namespace NAPS2.Sdk.Tests.Images;
public class BitwisePerfTests : ContextualTests
{
private const int SIZE = 10000;
private const int SIZE = 7000;
private readonly ITestOutputHelper _output;
public BitwisePerfTests(ITestOutputHelper output)
{
_output = output;
}
[Fact]
public void CopyFast()
{
var image1 = ImageContext.Create(SIZE, SIZE, ImagePixelFormat.ARGB32);
var image2 = ImageContext.Create(SIZE, SIZE, ImagePixelFormat.ARGB32);
var image1 = CreateAndFill(ImagePixelFormat.ARGB32);
var image2 = CreateAndFill(ImagePixelFormat.ARGB32);
using var _ = Timer();
new CopyBitwiseImageOp().Perform(image1, image2);
}
[Fact]
public void CopyColor()
{
var image1 = ImageContext.Create(SIZE, SIZE, ImagePixelFormat.RGB24);
var image2 = ImageContext.Create(SIZE, SIZE, ImagePixelFormat.ARGB32);
var image1 = CreateAndFill(ImagePixelFormat.RGB24);
var image2 = CreateAndFill(ImagePixelFormat.ARGB32);
using var _ = Timer();
new CopyBitwiseImageOp().Perform(image1, image2);
}
[Fact]
public void CopyToGray()
{
var image1 = ImageContext.Create(SIZE, SIZE, ImagePixelFormat.ARGB32);
var image2 = ImageContext.Create(SIZE, SIZE, ImagePixelFormat.Gray8);
var image1 = CreateAndFill(ImagePixelFormat.ARGB32);
var image2 = CreateAndFill(ImagePixelFormat.Gray8);
using var _ = Timer();
new CopyBitwiseImageOp().Perform(image1, image2);
}
[Fact]
public void CopyFromGray()
{
var image1 = ImageContext.Create(SIZE, SIZE, ImagePixelFormat.Gray8);
var image2 = ImageContext.Create(SIZE, SIZE, ImagePixelFormat.ARGB32);
var image1 = CreateAndFill(ImagePixelFormat.Gray8);
var image2 = CreateAndFill(ImagePixelFormat.ARGB32);
using var _ = Timer();
new CopyBitwiseImageOp().Perform(image1, image2);
}
[Fact]
public void CopyToBit()
{
var image1 = ImageContext.Create(SIZE, SIZE, ImagePixelFormat.ARGB32);
var image2 = ImageContext.Create(SIZE, SIZE, ImagePixelFormat.BW1);
var image1 = CreateAndFill(ImagePixelFormat.ARGB32);
var image2 = CreateAndFill(ImagePixelFormat.BW1);
using var _ = Timer();
new CopyBitwiseImageOp().Perform(image1, image2);
}
[Fact]
public void CopyFromBit()
{
var image1 = ImageContext.Create(SIZE, SIZE, ImagePixelFormat.BW1);
var image2 = ImageContext.Create(SIZE, SIZE, ImagePixelFormat.ARGB32);
var image1 = CreateAndFill(ImagePixelFormat.BW1);
var image2 = CreateAndFill(ImagePixelFormat.ARGB32);
using var _ = Timer();
new CopyBitwiseImageOp().Perform(image1, image2);
}
[Fact]
public void CopyAlignedBit()
{
var image1 = ImageContext.Create(SIZE, SIZE, ImagePixelFormat.BW1);
var image2 = ImageContext.Create(SIZE, SIZE, ImagePixelFormat.BW1);
var image1 = CreateAndFill(ImagePixelFormat.BW1);
var image2 = CreateAndFill(ImagePixelFormat.BW1);
using var _ = Timer();
new CopyBitwiseImageOp
{
SourceXOffset = 8,
@ -78,9 +93,10 @@ public class BitwisePerfTests : ContextualTests
[Fact]
public void CopyUnalignedBit()
{
var image1 = ImageContext.Create(SIZE, SIZE, ImagePixelFormat.BW1);
var image2 = ImageContext.Create(SIZE, SIZE, ImagePixelFormat.BW1);
var image1 = CreateAndFill(ImagePixelFormat.BW1);
var image2 = CreateAndFill(ImagePixelFormat.BW1);
using var _ = Timer();
new CopyBitwiseImageOp
{
SourceXOffset = 1,
@ -92,28 +108,36 @@ public class BitwisePerfTests : ContextualTests
[Fact]
public void Brightness()
{
var image = ImageContext.Create(SIZE, SIZE, ImagePixelFormat.ARGB32);
var image = CreateAndFill(ImagePixelFormat.ARGB32);
using var _ = Timer();
new BrightnessBitwiseImageOp(0.5f).Perform(image);
}
[Fact]
public void Contrast()
{
var image = ImageContext.Create(SIZE, SIZE, ImagePixelFormat.ARGB32);
var image = CreateAndFill(ImagePixelFormat.ARGB32);
using var _ = Timer();
new ContrastBitwiseImageOp(0.5f).Perform(image);
}
[Fact]
public void HueShift()
{
var image = ImageContext.Create(SIZE, SIZE, ImagePixelFormat.ARGB32);
var image = CreateAndFill(ImagePixelFormat.ARGB32);
using var _ = Timer();
new HueShiftBitwiseImageOp(0.5f).Perform(image);
}
[Fact]
public void Saturation()
{
var image = ImageContext.Create(SIZE, SIZE, ImagePixelFormat.ARGB32);
var image = CreateAndFill(ImagePixelFormat.ARGB32);
using var _ = Timer();
new SaturationBitwiseImageOp(0.5f).Perform(image);
}
@ -121,15 +145,70 @@ public class BitwisePerfTests : ContextualTests
public void Sharpness()
{
// Using a smaller size as sharpening is super slow
var image = ImageContext.Create(SIZE / 4, SIZE / 4, ImagePixelFormat.ARGB32);
var image2 = ImageContext.Create(SIZE / 4, SIZE / 4, ImagePixelFormat.ARGB32);
var image = CreateAndFill(ImagePixelFormat.ARGB32, SIZE / 4);
var image2 = CreateAndFill(ImagePixelFormat.ARGB32, SIZE / 4);
using var _ = Timer();
new SharpenBitwiseImageOp(0.5f).Perform(image, image2);
}
[Fact]
public void LogicalPixelFormat()
{
var image = ImageContext.Create(SIZE, SIZE, ImagePixelFormat.ARGB32);
var image = CreateAndFill(ImagePixelFormat.ARGB32);
using var _ = Timer();
new LogicalPixelFormatOp().Perform(image);
}
[Fact]
public void Fill()
{
var image = CreateAndFill(ImagePixelFormat.ARGB32);
using var imageLock = image.Lock(LockMode.ReadWrite, out var data);
using var _ = Timer();
BitwisePrimitives.Fill(data, 0x83);
}
[Fact]
public void Invert()
{
var image = CreateAndFill(ImagePixelFormat.ARGB32);
using var imageLock = image.Lock(LockMode.ReadWrite, out var data);
using var _ = Timer();
BitwisePrimitives.Invert(data);
}
private IMemoryImage CreateAndFill(ImagePixelFormat pixelFormat, int size = SIZE)
{
var image = ImageContext.Create(size, size, pixelFormat);
using var _ = image.Lock(LockMode.ReadWrite, out var data);
// Ensure memory is actually materialized and not just committed
BitwisePrimitives.Fill(data, 0);
return image;
}
private IDisposable Timer()
{
return new TimingRecorder(_output);
}
private class TimingRecorder : IDisposable
{
private readonly ITestOutputHelper _output;
private readonly Stopwatch _stopwatch;
public TimingRecorder(ITestOutputHelper output)
{
_output = output;
_stopwatch = Stopwatch.StartNew();
}
public void Dispose()
{
_output.WriteLine($"Execution time: {_stopwatch.ElapsedMilliseconds}");
}
}
}