替换边缘扫描为形态学大面积暗区域检测

边缘扫描只能处理从边缘开始的连续暗像素,无法处理折角三角形
阴影等不规则形状。改用形态学方法:
1. 灰度二值化找所有暗像素(<纸面亮度65%)
2. 大核腐蚀去掉文字笔画(小面积暗区域)
3. 膨胀回来得到大面积阴影掩码
4. 掩码内低方差像素推白,高方差像素(文字)保留

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-19 17:09:12 +08:00
parent f17fd5cb85
commit 3f30a9e79d

View File

@@ -536,61 +536,59 @@ public static class DocumentScanner
} }
} }
// --- 边缘阴影修复:从边缘向内扫描连续暗像素 --- // --- 大面积阴影检测(形态学方法)---
byte[] shadowFlag = new byte[w * h]; // 思路:
int darkThresh = (int)(paperBright * 0.55); // 1. 对原始灰度二值化,找出所有暗像素
int maxScanDepth = Math.Max(w, h) / 8; // 2. 用大核腐蚀,去掉小面积暗区域(文字笔画)
// 3. 再膨胀回来,剩下的就是大面积暗区域(阴影/折痕)
// 4. 在这些区域内,只保留高方差像素(文字),其余推白
Mat grayForShadow = new Mat(h, w, MatType.CV_8U, grayData);
// 上边缘 // 二值化:暗像素=255, 亮像素=0
for (int x = 0; x < w; x++) int shadowBinThresh = (int)(paperBright * 0.65);
{ Mat darkBin = new Mat();
for (int y = 0; y < Math.Min(maxScanDepth, h); y++) Cv2.Threshold(grayForShadow, darkBin, shadowBinThresh, 255, ThresholdTypes.BinaryInv);
{ grayForShadow.Dispose();
if (grayData[y * w + x] < darkThresh)
shadowFlag[y * w + x] = 1;
else
break; // 遇到亮像素就停止
}
}
// 下边缘
for (int x = 0; x < w; x++)
{
for (int y = h - 1; y >= Math.Max(0, h - maxScanDepth); y--)
{
if (grayData[y * w + x] < darkThresh)
shadowFlag[y * w + x] = 1;
else
break;
}
}
// 左边缘
for (int y = 0; y < h; y++)
{
for (int x = 0; x < Math.Min(maxScanDepth, w); x++)
{
if (grayData[y * w + x] < darkThresh)
shadowFlag[y * w + x] = 1;
else
break;
}
}
// 右边缘
for (int y = 0; y < h; y++)
{
for (int x = w - 1; x >= Math.Max(0, w - maxScanDepth); x--)
{
if (grayData[y * w + x] < darkThresh)
shadowFlag[y * w + x] = 1;
else
break;
}
}
// 把阴影区域的结果设为白色 // 腐蚀:去掉小面积暗区域(文字笔画宽度一般<10px
// 腐蚀核要大于文字笔画宽度
int erodeSize = Math.Max(w, h) / 80;
if (erodeSize < 15) erodeSize = 15;
if (erodeSize % 2 == 0) erodeSize++;
Mat erodeK = Cv2.GetStructuringElement(MorphShapes.Ellipse,
new OpenCvSharp.Size(erodeSize, erodeSize));
Mat eroded = new Mat();
Cv2.Erode(darkBin, eroded, erodeK);
darkBin.Dispose();
// 膨胀回来(比腐蚀核稍大,确保覆盖阴影边缘)
int dilateSize = erodeSize + 10;
if (dilateSize % 2 == 0) dilateSize++;
Mat dilateK = Cv2.GetStructuringElement(MorphShapes.Ellipse,
new OpenCvSharp.Size(dilateSize, dilateSize));
Mat shadowMask = new Mat();
Cv2.Dilate(eroded, shadowMask, dilateK);
eroded.Dispose();
erodeK.Dispose();
dilateK.Dispose();
byte[] shadowMaskData = new byte[w * h];
Marshal.Copy(shadowMask.Data, shadowMaskData, 0, shadowMaskData.Length);
shadowMask.Dispose();
// 在阴影区域内:高方差像素保留(文字),低方差推白
for (int i = 0; i < resultData.Length; i++) for (int i = 0; i < resultData.Length; i++)
{ {
if (shadowFlag[i] == 1) if (shadowMaskData[i] > 128)
resultData[i] = 255; {
// 在大面积暗区域内
if (varDataS[i] < 12)
{
// 低方差 → 阴影本身 → 白色
resultData[i] = 255;
}
// 高方差 → 文字 → 保留
}
} }
Mat enhanced = new Mat(h, w, MatType.CV_8U); Mat enhanced = new Mat(h, w, MatType.CV_8U);