彻底重写:回到高斯模糊+除法归一化,天然免疫阴影

减法方案(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:
2026-03-19 17:19:36 +08:00
parent 487a4d5c05
commit bbc61c935a

View File

@@ -322,8 +322,9 @@ public static class DocumentScanner
// ==========================================
// 第3步图像增强
// 核心用morphologyEx(CLOSE)做背景估计(比高斯模糊更贴合局部亮度)
// 阴影区域的背景估计也低 → bg-gray差值小 → 阴影自然不变
// 除法归一化result = (gray / background) * 255
// 除法对阴影天然免疫阴影处gray和bg同比例变暗比值不变
// 高斯模糊做背景估计(不会有底色问题)
// ==========================================
private static Mat EnhanceDocument(Mat src)
{
@@ -331,28 +332,19 @@ public static class DocumentScanner
Mat gray = new Mat();
Cv2.CvtColor(src, gray, ColorConversionCodes.BGR2GRAY);
// --- b: 背景估计(形态学闭运算---
// 闭运算 = 先膨胀后腐蚀,填平暗区域(文字),保留亮区域(背景)
// 核要足够大,能完全覆盖文字笔画和行间距,否则文字密集区域背景被拉低
// 用多次迭代小核代替单次超大核(性能更好)
int morphSize = 41;
int iterations = Math.Max(gray.Width, gray.Height) / 200;
if (iterations < 3) iterations = 3;
// --- b: 背景估计(高斯模糊,大核---
int blurSize = Math.Max(gray.Width, gray.Height) / 8;
if (blurSize % 2 == 0) blurSize++;
if (blurSize < 51) blurSize = 51;
Mat morphKernel = Cv2.GetStructuringElement(MorphShapes.Ellipse,
new OpenCvSharp.Size(morphSize, morphSize));
Mat background = new Mat();
Cv2.MorphologyEx(gray, background, MorphTypes.Close, morphKernel,
null, iterations);
morphKernel.Dispose();
Cv2.GaussianBlur(gray, background, new OpenCvSharp.Size(blurSize, blurSize), 0);
// 对背景估计做模糊,消除形态学的块状伪影
int smoothSize = morphSize * iterations / 2;
if (smoothSize % 2 == 0) smoothSize++;
if (smoothSize < 21) smoothSize = 21;
Cv2.GaussianBlur(background, background, new OpenCvSharp.Size(smoothSize, smoothSize), 0);
// --- c: 减法提取墨迹 ---
// --- c: 除法归一化 ---
// result = (gray / background) * 255
// 文字处gray < bg → 比值 < 1 → 结果 < 255偏暗
// 背景处gray ≈ bg → 比值 ≈ 1 → 结果 ≈ 255白色
// 阴影处gray和bg同比例变暗 → 比值仍 ≈ 1 → 结果仍 ≈ 255白色
int w = gray.Width;
int h = gray.Height;
byte[] grayData = new byte[w * h];
@@ -362,63 +354,55 @@ public static class DocumentScanner
Marshal.Copy(background.Data, bgData, 0, bgData.Length);
background.Dispose();
// 95百分位数增益
int[] inkHist = new int[256];
for (int i = 0; i < grayData.Length; i++)
{
int ink = bgData[i] - grayData[i];
if (ink < 0) ink = 0;
if (ink > 255) ink = 255;
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;
int bg = bgData[i];
if (bg < 1) bg = 1;
int val = (int)((double)grayData[i] / bg * 255.0);
if (val > 255) val = 255;
resultData[i] = (byte)val;
}
Mat enhanced = new Mat(h, w, MatType.CV_8U);
Marshal.Copy(resultData, 0, enhanced.Data, resultData.Length);
Mat normU8 = new Mat(h, w, MatType.CV_8U);
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();
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();
Cv2.AddWeighted(enhanced, 1.8, blurred, -0.8, 0, sharpened);
Cv2.AddWeighted(contrasted, 1.8, blurred, -0.8, 0, sharpened);
blurred.Dispose();
enhanced.Dispose();
// --- e: 对比度加强 ---
Cv2.ConvertScaleAbs(sharpened, sharpened, 1.3, -20);
contrasted.Dispose();
// --- f: 白底清理 ---
Cv2.Threshold(sharpened, sharpened, 220, 255, ThresholdTypes.Trunc);
Cv2.ConvertScaleAbs(sharpened, sharpened, 255.0 / 220.0, 0);
Cv2.Threshold(sharpened, sharpened, 230, 255, ThresholdTypes.Trunc);
Cv2.ConvertScaleAbs(sharpened, sharpened, 255.0 / 230.0, 0);
// 转回3通道
Mat output = new Mat();