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

242 lines
8.3 KiB
C#

namespace NAPS2.Images.Bitwise;
/// <summary>
/// Corrects images with poor calibration for white/black values.
/// </summary>
internal class WhiteBlackPointOp : UnaryBitwiseImageOp
{
// When we've identified the block of pixel values that we consider white (or black),
// this is the percentile (counting from the mid levels) at which we set the
// actual white/black point. 0 is maximally aggressive correction and flattens
// out near-white and near-black colors to pure white/black, while 1 leaves all
// of the variability/noise.
private const double PERCENTILE = 0.2;
/// <summary>
/// Performs this operation including pre-processing steps.
/// </summary>
/// <param name="image"></param>
/// <param name="mode"></param>
public static void PerformFullOp(IMemoryImage image, CorrectionMode mode)
{
var op1 = new WhiteBlackPointPreOp(mode);
op1.Perform(image);
new WhiteBlackPointOp(op1).Perform(image);
}
private readonly CorrectionMode _mode;
private readonly bool _valid;
private readonly int _whitePoint;
private readonly int _blackPoint;
public WhiteBlackPointOp(WhiteBlackPointPreOp preOp)
{
_mode = preOp.Mode;
var counts = preOp.PixelLumCounts;
var total = preOp.PixelTotalCount;
var peaks = FindPeaks(counts, total);
var whitePeak = peaks.OrderByDescending(WhitePeakScore).FirstOrDefault();
var blackPeak = peaks.OrderByDescending(BlackPeakScore).FirstOrDefault();
if (whitePeak == null || blackPeak == null || whitePeak.Value <= blackPeak.Value)
{
_valid = false;
return;
}
_whitePoint = GetWhitePoint(counts, whitePeak);
_blackPoint = GetBlackPoint(counts, blackPeak);
_valid = true;
// Console.WriteLine($"Correcting with whitepoint {_whitePoint} blackpoint {_blackPoint}");
}
private static int GetWhitePoint(int[] counts, Peak whitePeak)
{
var whiteTotal = counts.Skip(whitePeak.Left).Sum();
long whiteCumul = 0;
var whitePoint = whitePeak.Right;
for (int i = whitePeak.Left; i < whitePeak.Right; i++)
{
whiteCumul += counts[i];
if (whiteCumul >= PERCENTILE * whiteTotal)
{
whitePoint = i;
break;
}
}
return whitePoint;
}
private static int GetBlackPoint(int[] counts, Peak blackPeak)
{
var blackTotal = counts.Take(blackPeak.Right + 1).Sum();
long blackCumul = 0;
var blackPoint = blackPeak.Left;
for (int i = blackPeak.Right; i > blackPeak.Left; i--)
{
blackCumul += counts[i];
if (blackCumul >= PERCENTILE * blackTotal)
{
blackPoint = i;
break;
}
}
return blackPoint;
}
private double BlackPeakScore(Peak peak)
{
var vScore = Math.Pow(1 - peak.Value / 255.0, 3);
var hScore = Magnitude(peak.Height) - Magnitude(Math.Min(peak.LeftBottom, peak.RightBottom));
return vScore * hScore;
}
private double Magnitude(double h)
{
return Math.Log10(1e4 * h + 1);
}
private double WhitePeakScore(Peak peak)
{
var vScore = Math.Pow(peak.Value / 255.0, 3);
var mScore = peak.Mass > 0.1 ? Math.Log10(100 * peak.Mass) : 10 * peak.Mass;
return vScore * mScore;
}
private static List<Peak> FindPeaks(int[] counts, long total)
{
var peaks = new List<Peak>();
var dCounts = counts.Select(x => x / (double) total).ToList();
var dCountsPlus = new double[] { 0, 0 }.Concat(dCounts).Concat(new double[] { 0, 0 }).ToList();
for (int i = 0; i < 256; i++)
{
if (dCounts[i] > dCountsPlus[i + 1] && dCounts[i] > dCountsPlus[i] &&
dCounts[i] > dCountsPlus[i + 4] && dCounts[i] > dCountsPlus[i + 3])
{
var p = new Peak
{
Value = i,
Height = dCounts[i],
LeftBottom = dCounts[i],
RightBottom = dCounts[i],
Left = i,
Right = i,
Mass = dCounts[i]
};
for (int j = i - 1; j >= 0; j--)
{
if (dCounts[j] < p.LeftBottom || j > 0 && dCounts[j - 1] < p.LeftBottom)
{
p.LeftBottom = Math.Min(p.LeftBottom, dCounts[j]);
p.Left = j;
p.Mass += dCounts[j];
}
else
{
break;
}
}
for (int j = i + 1; j < 256; j++)
{
if (dCounts[j] < p.RightBottom || j < 255 && dCounts[j + 1] < p.RightBottom)
{
p.RightBottom = Math.Min(p.RightBottom, dCounts[j]);
p.Right = j;
p.Mass += dCounts[j];
}
else
{
break;
}
}
peaks.Add(p);
}
}
return peaks;
}
protected override unsafe void PerformCore(BitwiseImageData data, int partStart, int partEnd)
{
if (!_valid)
return;
bool flatten = _mode == CorrectionMode.Document;
bool retainColor = _mode == CorrectionMode.Photo;
var iToL = GammaTables.IntensityToLum;
var lToI = GammaTables.LumToIntensity;
var blackL = iToL[_blackPoint];
var whiteL = iToL[_whitePoint];
for (int i = partStart; i < partEnd; i++)
{
var row = data.ptr + data.stride * i;
for (int j = 0; j < data.w; j++)
{
var pixel = row + j * data.bytesPerPixel;
int r = *(pixel + data.rOff);
int g = *(pixel + data.gOff);
int b = *(pixel + data.bOff);
if (flatten)
{
var white = _whitePoint;
var black = _blackPoint;
if (r < black)
r = black;
if (g < black)
g = black;
if (b < black)
b = black;
if (r > white)
r = white;
if (g > white)
g = white;
if (b > white)
b = white;
// Use a gamma function to convert to the luminescence space
int rL = iToL[r];
int gL = iToL[g];
int bL = iToL[b];
// Scale the color values in the luminescence space
rL = (rL - blackL) * GammaTables.MAX_LUM / (whiteL - blackL);
gL = (gL - blackL) * GammaTables.MAX_LUM / (whiteL - blackL);
bL = (bL - blackL) * GammaTables.MAX_LUM / (whiteL - blackL);
// Convert back to the intensity space
r = lToI[rL];
g = lToI[gL];
b = lToI[bL];
}
if (retainColor)
{
var min = Math.Min(r, Math.Min(g, b));
var max = Math.Max(r, Math.Max(g, b));
var black = Math.Min(min, _blackPoint);
var white = Math.Max(max, _whitePoint);
r = (r - black) * 255 / (white - black);
g = (g - black) * 255 / (white - black);
b = (b - black) * 255 / (white - black);
}
*(pixel + data.rOff) = (byte) r;
*(pixel + data.gOff) = (byte) g;
*(pixel + data.bOff) = (byte) b;
}
}
}
private class Peak
{
public int Value { get; set; }
public int Left { get; set; }
public int Right { get; set; }
public double Height { get; set; }
public double LeftBottom { get; set; }
public double RightBottom { get; set; }
public double Mass { get; set; }
}
}