优化折痕检测:双级方差保护文字

单级方差核(15)太大,折痕附近的文字方差被稀释导致误伤。
改为双级方差:
- 小核(7):精确检测文字笔画高频特征,>=5即保护
- 大核(31):检测折痕低频渐变,<12才触发抑制
两级方差都低时才判定为折痕,大幅减少文字误伤。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-19 17:03:34 +08:00
parent c62db4af45
commit 473eeb6e53

View File

@@ -420,42 +420,49 @@ public static class DocumentScanner
inkData[i] = (byte)(255 - val); // 墨迹强度0=无墨, 255=纯黑 inkData[i] = (byte)(255 - val); // 墨迹强度0=无墨, 255=纯黑
} }
// 计算局部方差图(用灰度图,不是墨迹图) // 计算两级局部方差
// 文字区域:灰度变化大(笔画边缘),方差高 // 小核(7):检测文字笔画(高频),文字处方差高
// 折痕区域:灰度缓慢变化,方差低 // 大核(31):检测折痕渐变(低频),折痕处方差低
// 用简化方法:|gray - 局部均值| 的局部均值 ≈ 局部标准差 // 只有两级方差都低时才抑制,避免误伤折痕附近的文字
Mat grayMat = new Mat(h, w, MatType.CV_8U, grayData); 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(); Mat localMeanS = new Mat();
Cv2.Absdiff(grayMat, localMean, diff); Cv2.Blur(grayMat, localMeanS, new OpenCvSharp.Size(7, 7));
localMean.Dispose(); Mat diffS = new Mat();
Cv2.Absdiff(grayMat, localMeanS, diffS);
localMeanS.Dispose();
Mat localVarS = new Mat();
Cv2.Blur(diffS, localVarS, new OpenCvSharp.Size(7, 7));
diffS.Dispose();
// 对 diff 再做一次均值模糊,得到局部方差的近似 // 大核方差(检测折痕)
Mat localVar = new Mat(); Mat localMeanL = new Mat();
Cv2.Blur(diff, localVar, new OpenCvSharp.Size(varKernSize, varKernSize)); Cv2.Blur(grayMat, localMeanL, new OpenCvSharp.Size(31, 31));
diff.Dispose(); Mat diffL = new Mat();
Cv2.Absdiff(grayMat, localMeanL, diffL);
localMeanL.Dispose();
Mat localVarL = new Mat();
Cv2.Blur(diffL, localVarL, new OpenCvSharp.Size(31, 31));
diffL.Dispose();
grayMat.Dispose(); grayMat.Dispose();
byte[] varData = new byte[w * h]; byte[] varDataS = new byte[w * h];
Marshal.Copy(localVar.Data, varData, 0, varData.Length); byte[] varDataL = new byte[w * h];
localVar.Dispose(); Marshal.Copy(localVarS.Data, varDataS, 0, varDataS.Length);
Marshal.Copy(localVarL.Data, varDataL, 0, varDataL.Length);
// 用局部方差来决定是否抑制 localVarS.Dispose();
// 高方差(> 阈值)= 文字,保留 localVarL.Dispose();
// 低方差 + 有墨迹 = 折痕/阴影,抑制
int varThresh = 8; // 方差阈值,低于此值认为是平坦区域
// 抑制逻辑:
// 小核方差 < 5 且 大核方差 < 12 → 确定是平坦渐变区域(折痕/阴影)
// 小核方差 >= 5 → 有文字笔画,不抑制(即使大核方差低)
for (int i = 0; i < resultData.Length; i++) for (int i = 0; i < resultData.Length; i++)
{ {
if (inkData[i] > 10 && varData[i] < varThresh) if (inkData[i] > 10 && varDataS[i] < 5 && varDataL[i] < 12)
{ {
// 有墨迹但局部方差低 → 折痕/阴影,推向白色 // 两级方差低 → 折痕/阴影
// 抑制程度和方差成正比:方差越低抑制越强 double suppressRatio = (double)varDataS[i] / 5.0;
double suppressRatio = (double)varData[i] / varThresh;
int origVal = resultData[i]; int origVal = resultData[i];
resultData[i] = (byte)(origVal + (int)((255 - origVal) * (1.0 - suppressRatio))); resultData[i] = (byte)(origVal + (int)((255 - origVal) * (1.0 - suppressRatio)));
} }