彻底重写:回到高斯模糊+除法归一化,天然免疫阴影
减法方案(bg-gray)的根本缺陷:阴影处bg被远处亮区域拉高, 差值大,阴影变黑。形态学闭运算核大小两难无解。 除法方案(gray/bg*255)天然免疫阴影: - 阴影处gray和bg同比例变暗,比值≈1,结果≈255(白色) - 文字处gray<bg,比值<1,结果<255(偏暗) - 背景处gray≈bg,比值≈1,结果≈255(白色) 用LUT分段映射增强对比度:0-180压暗(文字变黑), 180-255拉亮(背景变白)。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
116
CamScanner.cs
116
CamScanner.cs
@@ -322,8 +322,9 @@ public static class DocumentScanner
|
|||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// 第3步:图像增强
|
// 第3步:图像增强
|
||||||
// 核心:用morphologyEx(CLOSE)做背景估计(比高斯模糊更贴合局部亮度)
|
// 除法归一化:result = (gray / background) * 255
|
||||||
// 阴影区域的背景估计也低 → bg-gray差值小 → 阴影自然不变黑
|
// 除法对阴影天然免疫:阴影处gray和bg同比例变暗,比值不变
|
||||||
|
// 高斯模糊做背景估计(不会有底色问题)
|
||||||
// ==========================================
|
// ==========================================
|
||||||
private static Mat EnhanceDocument(Mat src)
|
private static Mat EnhanceDocument(Mat src)
|
||||||
{
|
{
|
||||||
@@ -331,28 +332,19 @@ public static class DocumentScanner
|
|||||||
Mat gray = new Mat();
|
Mat gray = new Mat();
|
||||||
Cv2.CvtColor(src, gray, ColorConversionCodes.BGR2GRAY);
|
Cv2.CvtColor(src, gray, ColorConversionCodes.BGR2GRAY);
|
||||||
|
|
||||||
// --- b: 背景估计(形态学闭运算)---
|
// --- b: 背景估计(高斯模糊,大核)---
|
||||||
// 闭运算 = 先膨胀后腐蚀,填平暗区域(文字),保留亮区域(背景)
|
int blurSize = Math.Max(gray.Width, gray.Height) / 8;
|
||||||
// 核要足够大,能完全覆盖文字笔画和行间距,否则文字密集区域背景被拉低
|
if (blurSize % 2 == 0) blurSize++;
|
||||||
// 用多次迭代小核代替单次超大核(性能更好)
|
if (blurSize < 51) blurSize = 51;
|
||||||
int morphSize = 41;
|
|
||||||
int iterations = Math.Max(gray.Width, gray.Height) / 200;
|
|
||||||
if (iterations < 3) iterations = 3;
|
|
||||||
|
|
||||||
Mat morphKernel = Cv2.GetStructuringElement(MorphShapes.Ellipse,
|
|
||||||
new OpenCvSharp.Size(morphSize, morphSize));
|
|
||||||
Mat background = new Mat();
|
Mat background = new Mat();
|
||||||
Cv2.MorphologyEx(gray, background, MorphTypes.Close, morphKernel,
|
Cv2.GaussianBlur(gray, background, new OpenCvSharp.Size(blurSize, blurSize), 0);
|
||||||
null, iterations);
|
|
||||||
morphKernel.Dispose();
|
|
||||||
|
|
||||||
// 对背景估计做模糊,消除形态学的块状伪影
|
// --- c: 除法归一化 ---
|
||||||
int smoothSize = morphSize * iterations / 2;
|
// result = (gray / background) * 255
|
||||||
if (smoothSize % 2 == 0) smoothSize++;
|
// 文字处:gray < bg → 比值 < 1 → 结果 < 255(偏暗)
|
||||||
if (smoothSize < 21) smoothSize = 21;
|
// 背景处:gray ≈ bg → 比值 ≈ 1 → 结果 ≈ 255(白色)
|
||||||
Cv2.GaussianBlur(background, background, new OpenCvSharp.Size(smoothSize, smoothSize), 0);
|
// 阴影处:gray和bg同比例变暗 → 比值仍 ≈ 1 → 结果仍 ≈ 255(白色!)
|
||||||
|
|
||||||
// --- c: 减法提取墨迹 ---
|
|
||||||
int w = gray.Width;
|
int w = gray.Width;
|
||||||
int h = gray.Height;
|
int h = gray.Height;
|
||||||
byte[] grayData = new byte[w * h];
|
byte[] grayData = new byte[w * h];
|
||||||
@@ -362,63 +354,55 @@ public static class DocumentScanner
|
|||||||
Marshal.Copy(background.Data, bgData, 0, bgData.Length);
|
Marshal.Copy(background.Data, bgData, 0, bgData.Length);
|
||||||
background.Dispose();
|
background.Dispose();
|
||||||
|
|
||||||
// 95百分位数增益
|
|
||||||
int[] inkHist = new int[256];
|
|
||||||
for (int i = 0; i < grayData.Length; i++)
|
for (int i = 0; i < grayData.Length; i++)
|
||||||
{
|
{
|
||||||
int ink = bgData[i] - grayData[i];
|
int bg = bgData[i];
|
||||||
if (ink < 0) ink = 0;
|
if (bg < 1) bg = 1;
|
||||||
if (ink > 255) ink = 255;
|
int val = (int)((double)grayData[i] / bg * 255.0);
|
||||||
inkHist[ink]++;
|
|
||||||
}
|
|
||||||
int totalPixels = w * h;
|
|
||||||
int target95 = (int)(totalPixels * 0.95);
|
|
||||||
int cumulative = 0;
|
|
||||||
int ink95 = 10;
|
|
||||||
for (int i = 0; i < 256; i++)
|
|
||||||
{
|
|
||||||
cumulative += inkHist[i];
|
|
||||||
if (cumulative >= target95)
|
|
||||||
{
|
|
||||||
ink95 = Math.Max(i, 5);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 墨迹映射(纯净版,不做任何阴影抑制)
|
|
||||||
for (int i = 0; i < grayData.Length; i++)
|
|
||||||
{
|
|
||||||
int ink = bgData[i] - grayData[i];
|
|
||||||
if (ink < 0) ink = 0;
|
|
||||||
|
|
||||||
double inkNorm = (double)ink / ink95;
|
|
||||||
if (inkNorm > 1.0) inkNorm = 1.0;
|
|
||||||
|
|
||||||
double darkness = Math.Pow(inkNorm, 0.45);
|
|
||||||
|
|
||||||
int val = (int)(255.0 * (1.0 - darkness));
|
|
||||||
if (val < 0) val = 0;
|
|
||||||
if (val > 255) val = 255;
|
if (val > 255) val = 255;
|
||||||
resultData[i] = (byte)val;
|
resultData[i] = (byte)val;
|
||||||
}
|
}
|
||||||
|
|
||||||
Mat enhanced = new Mat(h, w, MatType.CV_8U);
|
Mat normU8 = new Mat(h, w, MatType.CV_8U);
|
||||||
Marshal.Copy(resultData, 0, enhanced.Data, resultData.Length);
|
Marshal.Copy(resultData, 0, normU8.Data, resultData.Length);
|
||||||
|
|
||||||
// --- d: USM锐化 ---
|
// --- d: 非线性对比度增强(让文字更黑)---
|
||||||
|
// 除法归一化后文字大约在 180-230 范围(偏亮),需要拉黑
|
||||||
|
// 用 LUT 做分段映射:
|
||||||
|
// 0-180: 线性映射到 0-80(文字区域压暗)
|
||||||
|
// 180-255: 线性映射到 80-255(背景区域保持亮)
|
||||||
|
byte[] lut = new byte[256];
|
||||||
|
for (int i = 0; i < 256; i++)
|
||||||
|
{
|
||||||
|
if (i <= 180)
|
||||||
|
{
|
||||||
|
// 文字区域:压暗
|
||||||
|
lut[i] = (byte)(i * 80 / 180);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 背景区域:拉亮
|
||||||
|
lut[i] = (byte)(80 + (i - 180) * 175 / 75);
|
||||||
|
}
|
||||||
|
if (lut[i] > 255) lut[i] = 255;
|
||||||
|
}
|
||||||
|
Mat lutMat = new Mat(1, 256, MatType.CV_8U, lut);
|
||||||
|
Mat contrasted = new Mat();
|
||||||
|
Cv2.LUT(normU8, lutMat, contrasted);
|
||||||
|
lutMat.Dispose();
|
||||||
|
normU8.Dispose();
|
||||||
|
|
||||||
|
// --- e: USM锐化 ---
|
||||||
Mat blurred = new Mat();
|
Mat blurred = new Mat();
|
||||||
Cv2.GaussianBlur(enhanced, blurred, new OpenCvSharp.Size(0, 0), 1.0);
|
Cv2.GaussianBlur(contrasted, blurred, new OpenCvSharp.Size(0, 0), 1.0);
|
||||||
Mat sharpened = new Mat();
|
Mat sharpened = new Mat();
|
||||||
Cv2.AddWeighted(enhanced, 1.8, blurred, -0.8, 0, sharpened);
|
Cv2.AddWeighted(contrasted, 1.8, blurred, -0.8, 0, sharpened);
|
||||||
blurred.Dispose();
|
blurred.Dispose();
|
||||||
enhanced.Dispose();
|
contrasted.Dispose();
|
||||||
|
|
||||||
// --- e: 对比度加强 ---
|
|
||||||
Cv2.ConvertScaleAbs(sharpened, sharpened, 1.3, -20);
|
|
||||||
|
|
||||||
// --- f: 白底清理 ---
|
// --- f: 白底清理 ---
|
||||||
Cv2.Threshold(sharpened, sharpened, 220, 255, ThresholdTypes.Trunc);
|
Cv2.Threshold(sharpened, sharpened, 230, 255, ThresholdTypes.Trunc);
|
||||||
Cv2.ConvertScaleAbs(sharpened, sharpened, 255.0 / 220.0, 0);
|
Cv2.ConvertScaleAbs(sharpened, sharpened, 255.0 / 230.0, 0);
|
||||||
|
|
||||||
// 转回3通道
|
// 转回3通道
|
||||||
Mat output = new Mat();
|
Mat output = new Mat();
|
||||||
|
|||||||
Reference in New Issue
Block a user