From bbc61c935addf8ac1241d82eb53901f7461dc6f1 Mon Sep 17 00:00:00 2001 From: sinvo Date: Thu, 19 Mar 2026 17:19:36 +0800 Subject: [PATCH] =?UTF-8?q?=E5=BD=BB=E5=BA=95=E9=87=8D=E5=86=99=EF=BC=9A?= =?UTF-8?q?=E5=9B=9E=E5=88=B0=E9=AB=98=E6=96=AF=E6=A8=A1=E7=B3=8A+?= =?UTF-8?q?=E9=99=A4=E6=B3=95=E5=BD=92=E4=B8=80=E5=8C=96=EF=BC=8C=E5=A4=A9?= =?UTF-8?q?=E7=84=B6=E5=85=8D=E7=96=AB=E9=98=B4=E5=BD=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 减法方案(bg-gray)的根本缺陷:阴影处bg被远处亮区域拉高, 差值大,阴影变黑。形态学闭运算核大小两难无解。 除法方案(gray/bg*255)天然免疫阴影: - 阴影处gray和bg同比例变暗,比值≈1,结果≈255(白色) - 文字处gray --- CamScanner.cs | 116 ++++++++++++++++++++++---------------------------- 1 file changed, 50 insertions(+), 66 deletions(-) diff --git a/CamScanner.cs b/CamScanner.cs index de0ca89..1e87a13 100644 --- a/CamScanner.cs +++ b/CamScanner.cs @@ -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();