diff --git a/CamScanner.cs b/CamScanner.cs index f4aa19a..356c5f3 100644 --- a/CamScanner.cs +++ b/CamScanner.cs @@ -391,15 +391,18 @@ public static class DocumentScanner } int paperBright = (int)(centerSum / Math.Max(centerCount, 1)); - // 墨迹映射(带阴影/折痕保护) - // 文字特征:背景亮(接近paperBright),gray比背景低 → ink大 - // 折痕/阴影特征:gray本身就暗,背景估计也被拉低 → ink也可能大 - // 区分方法: - // 1. 看背景估计值是否接近正常纸面亮度(抑制阴影区域的墨迹) - // 2. 看原始灰度值本身是否偏暗(折痕处gray很低) - double bgThreshHigh = 0.92; // 背景低于纸面亮度92%就开始轻微抑制 - double bgThreshLow = 0.60; // 低于60%时完全抑制为白色 + // 墨迹映射(带折痕/阴影抑制) + // + // 核心区分逻辑:文字 vs 折痕/阴影 + // 文字 = 高频信号,笔画边缘锐利,局部灰度变化剧烈 + // 折痕/阴影 = 低频信号,缓慢渐变,局部灰度变化平缓 + // + // 方法:先正常生成墨迹图,然后用"墨迹图的局部方差"来区分 + // 高方差区域 = 文字(保留) + // 低方差但偏暗区域 = 折痕/阴影(推向白色) + // 先生成原始墨迹图(不做任何抑制) + byte[] inkData = new byte[w * h]; for (int i = 0; i < grayData.Length; i++) { int ink = bgData[i] - grayData[i]; @@ -410,40 +413,52 @@ public static class DocumentScanner double darkness = Math.Pow(inkNorm, 0.45); - // 阴影/折痕抑制 - double bgRatio = (double)bgData[i] / Math.Max(paperBright, 1); - - if (bgRatio < bgThreshHigh) - { - if (bgRatio <= bgThreshLow) - { - // 极暗区域,完全抑制 - darkness = 0; - } - else - { - // 平滑衰减:从bgThreshHigh到bgThreshLow,darkness从原值→0 - double t = (bgRatio - bgThreshLow) / (bgThreshHigh - bgThreshLow); - // 用三次曲线平滑过渡 - t = t * t * (3.0 - 2.0 * t); - darkness = darkness * t; - } - } - - // 额外保护:原始灰度本身很暗且不是文字(ink占gray比例小) - // 折痕:gray很低(比如100),ink可能只有20-30,ink/gray比例小 - // 文字:gray中等(比如150),ink可能50-80,ink/gray比例大 - double grayRatio = (double)grayData[i] / Math.Max(paperBright, 1); - if (grayRatio < 0.5 && ink < bgData[i] * 0.3) - { - // 原始灰度很暗,但墨迹占比小 → 不是文字,是阴影/折痕 - darkness = darkness * 0.1; - } - int val = (int)(255.0 * (1.0 - darkness)); if (val < 0) val = 0; if (val > 255) val = 255; resultData[i] = (byte)val; + inkData[i] = (byte)(255 - val); // 墨迹强度:0=无墨, 255=纯黑 + } + + // 计算局部方差图(用灰度图,不是墨迹图) + // 文字区域:灰度变化大(笔画边缘),方差高 + // 折痕区域:灰度缓慢变化,方差低 + // 用简化方法:|gray - 局部均值| 的局部均值 ≈ 局部标准差 + Mat grayMat = new Mat(h, w, MatType.CV_8U, grayData); + Mat localMean = new Mat(); + int varKernSize = 15; + Cv2.Blur(grayMat, localMean, new OpenCvSharp.Size(varKernSize, varKernSize)); + + // |gray - localMean| + Mat diff = new Mat(); + Cv2.Absdiff(grayMat, localMean, diff); + localMean.Dispose(); + + // 对 diff 再做一次均值模糊,得到局部方差的近似 + Mat localVar = new Mat(); + Cv2.Blur(diff, localVar, new OpenCvSharp.Size(varKernSize, varKernSize)); + diff.Dispose(); + grayMat.Dispose(); + + byte[] varData = new byte[w * h]; + Marshal.Copy(localVar.Data, varData, 0, varData.Length); + localVar.Dispose(); + + // 用局部方差来决定是否抑制 + // 高方差(> 阈值)= 文字,保留 + // 低方差 + 有墨迹 = 折痕/阴影,抑制 + int varThresh = 8; // 方差阈值,低于此值认为是平坦区域 + + for (int i = 0; i < resultData.Length; i++) + { + if (inkData[i] > 10 && varData[i] < varThresh) + { + // 有墨迹但局部方差低 → 折痕/阴影,推向白色 + // 抑制程度和方差成正比:方差越低抑制越强 + double suppressRatio = (double)varData[i] / varThresh; + int origVal = resultData[i]; + resultData[i] = (byte)(origVal + (int)((255 - origVal) * (1.0 - suppressRatio))); + } } // --- 边缘阴影修复:从边缘向内扫描连续暗像素 ---