mirror of
https://github.com/cyanfish/naps2.git
synced 2024-09-17 18:58:11 +03:00
Change rmse and invert to bitwise ops/primitives
This commit is contained in:
parent
c8f724efd5
commit
10a2a1df8f
@ -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)
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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()
|
||||
{
|
||||
}
|
||||
}
|
64
NAPS2.Images/Bitwise/BitwisePrimitives.cs
Normal file
64
NAPS2.Images/Bitwise/BitwisePrimitives.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
67
NAPS2.Images/Bitwise/RmseBitwiseImageOp.cs
Normal file
67
NAPS2.Images/Bitwise/RmseBitwiseImageOp.cs
Normal 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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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}");
|
||||
|
@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user