naps2/NAPS2.Images/Bitwise/BilateralFilterOp.cs
2024-04-07 17:09:22 -07:00

217 lines
8.5 KiB
C#

namespace NAPS2.Images.Bitwise;
/// <summary>
/// Runs a bilateral filter operation, which reduces noise without losing edges or fine details.
/// https://en.wikipedia.org/wiki/Bilateral_filter
/// </summary>
internal class BilateralFilterOp : BinaryBitwiseImageOp
{
// The color distance (in the 0-255 range) at which pixels are weighted to 0.
// The weight linearly scales up as the color distance approaches 0.
private const int COLOR_DIST_MAX = 32;
// The size of the square around the current pixel to check.
// Should be an odd number for symmetry.
// Pixels in the square are weighted by how close they are to the center.
private const int FILTER_SIZE = 15;
protected override LockMode SrcLockMode => LockMode.ReadOnly;
protected override LockMode DstLockMode => LockMode.WriteOnly;
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 if (src.bytesPerPixel == 1 && dst.bytesPerPixel == 1)
{
PerformGray(src, dst, partStart, partEnd);
}
else
{
throw new InvalidOperationException("Unsupported pixel format");
}
}
private unsafe void PerformRgba(BitwiseImageData src, BitwiseImageData dst, int partStart, int partEnd)
{
bool copyAlpha = src.hasAlpha && dst.hasAlpha;
const int s = FILTER_SIZE / 2;
var filter = BuildFilter();
var diffWeights = BuildRgbaDiffWeights();
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 r = *(srcPixel + src.rOff);
int g = *(srcPixel + src.gOff);
int b = *(srcPixel + src.bOff);
if (j > s && j < src.w - s && i > s && i < src.h - s)
{
bool skipPixel = false;
if (r == 255 && g == 255 & b == 255)
{
var prevPixel = src.ptr + src.stride * i + src.bytesPerPixel * j - 1;
var nextPixel = src.ptr + src.stride * i + src.bytesPerPixel * j + 1;
byte prevR = *(prevPixel + src.rOff);
byte prevG = *(prevPixel + src.gOff);
byte prevB = *(prevPixel + src.bOff);
byte nextR = *(nextPixel + src.rOff);
byte nextG = *(nextPixel + src.gOff);
byte nextB = *(nextPixel + src.bOff);
if (prevR == 255 && prevG == 255 && prevB == 255 && nextR == 255 && nextG == 255 &&
nextB == 255)
{
skipPixel = true;
}
}
if (!skipPixel)
{
int rTotal = 0, gTotal = 0, bTotal = 0;
int weightTotal = 0;
for (int filterX = 0; filterX < FILTER_SIZE; filterX++)
{
for (int filterY = 0; filterY < FILTER_SIZE; filterY++)
{
int imageX = j - s + filterX;
int imageY = i - s + filterY;
var pixel = src.ptr + src.stride * imageY + src.bytesPerPixel * imageX;
var r2 = *(pixel + src.rOff);
var g2 = *(pixel + src.gOff);
var b2 = *(pixel + src.bOff);
// TODO: Better color distance
var diff = (r + g + b) - (r2 + g2 + b2) + 256 * 3;
var weight = filter[filterX + filterY * FILTER_SIZE] * diffWeights[diff];
weightTotal += weight;
rTotal += r2 * weight;
gTotal += g2 * weight;
bTotal += b2 * weight;
}
}
r = rTotal / weightTotal;
g = gTotal / weightTotal;
b = bTotal / weightTotal;
}
}
*(dstPixel + dst.rOff) = (byte) r;
*(dstPixel + dst.gOff) = (byte) g;
*(dstPixel + dst.bOff) = (byte) b;
if (copyAlpha)
{
*(dstPixel + dst.aOff) = *(srcPixel + src.aOff);
}
}
}
}
private unsafe void PerformGray(BitwiseImageData src, BitwiseImageData dst, int partStart, int partEnd)
{
const int s = FILTER_SIZE / 2;
var filter = BuildFilter();
var diffWeights = BuildGrayDiffWeights();
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 lum = *srcPixel;
if (j > s && j < src.w - s && i > s && i < src.h - s)
{
bool skipPixel = false;
if (lum == 255)
{
var prevPixel = src.ptr + src.stride * i + src.bytesPerPixel * j - 1;
var nextPixel = src.ptr + src.stride * i + src.bytesPerPixel * j + 1;
byte prev = *prevPixel;
byte next = *nextPixel;
if (prev == 255 && next == 255)
{
skipPixel = true;
}
}
if (!skipPixel)
{
int lumTotal = 0;
int weightTotal = 0;
for (int filterX = 0; filterX < FILTER_SIZE; filterX++)
{
for (int filterY = 0; filterY < FILTER_SIZE; filterY++)
{
int imageX = j - s + filterX;
int imageY = i - s + filterY;
var pixel = src.ptr + src.stride * imageY + src.bytesPerPixel * imageX;
var lum2 = *pixel;
var diff = lum - lum2 + 256;
var weight = filter[filterX + filterY * FILTER_SIZE] * diffWeights[diff];
weightTotal += weight;
lumTotal += lum2 * weight;
}
}
lum = lumTotal / weightTotal;
}
}
*dstPixel = (byte) lum;
}
}
}
private static int[] BuildFilter()
{
const int s = FILTER_SIZE / 2;
var filter = new int[FILTER_SIZE * FILTER_SIZE];
for (int filterX = 0; filterX < FILTER_SIZE; filterX++)
{
for (int filterY = 0; filterY < FILTER_SIZE; filterY++)
{
int dx = filterX - s;
int dy = filterY - s;
var dmax = Math.Sqrt(2 * s * s);
var d = Math.Sqrt(dx * dx + dy * dy) / dmax;
filter[filterX + filterY * FILTER_SIZE] = (int) ((1 - d) * 256);
}
}
return filter;
}
private static int[] BuildRgbaDiffWeights()
{
var diffWeights = new int[256 * 3 * 2];
for (int i = 0; i < COLOR_DIST_MAX * 3; i++)
{
diffWeights[256 * 3 + i] = COLOR_DIST_MAX - i / 3;
diffWeights[256 * 3 - i] = COLOR_DIST_MAX - i / 3;
}
return diffWeights;
}
private static int[] BuildGrayDiffWeights()
{
var diffWeights = new int[256 * 2];
for (int i = 0; i < COLOR_DIST_MAX; i++)
{
diffWeights[256 + i] = COLOR_DIST_MAX - i;
diffWeights[256 - i] = COLOR_DIST_MAX - i;
}
return diffWeights;
}
}