From 3f30a9e79d5cda27dcdfae97189025ece69046e7 Mon Sep 17 00:00:00 2001 From: sinvo Date: Thu, 19 Mar 2026 17:09:12 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=BF=E6=8D=A2=E8=BE=B9=E7=BC=98=E6=89=AB?= =?UTF-8?q?=E6=8F=8F=E4=B8=BA=E5=BD=A2=E6=80=81=E5=AD=A6=E5=A4=A7=E9=9D=A2?= =?UTF-8?q?=E7=A7=AF=E6=9A=97=E5=8C=BA=E5=9F=9F=E6=A3=80=E6=B5=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 边缘扫描只能处理从边缘开始的连续暗像素,无法处理折角三角形 阴影等不规则形状。改用形态学方法: 1. 灰度二值化找所有暗像素(<纸面亮度65%) 2. 大核腐蚀去掉文字笔画(小面积暗区域) 3. 膨胀回来得到大面积阴影掩码 4. 掩码内低方差像素推白,高方差像素(文字)保留 Co-Authored-By: Claude Opus 4.6 --- CamScanner.cs | 100 +++++++++++++++++++++++++------------------------- 1 file changed, 49 insertions(+), 51 deletions(-) diff --git a/CamScanner.cs b/CamScanner.cs index cb038aa..fe9c047 100644 --- a/CamScanner.cs +++ b/CamScanner.cs @@ -536,61 +536,59 @@ public static class DocumentScanner } } - // --- 边缘阴影修复:从边缘向内扫描连续暗像素 --- - byte[] shadowFlag = new byte[w * h]; - int darkThresh = (int)(paperBright * 0.55); - int maxScanDepth = Math.Max(w, h) / 8; + // --- 大面积阴影检测(形态学方法)--- + // 思路: + // 1. 对原始灰度二值化,找出所有暗像素 + // 2. 用大核腐蚀,去掉小面积暗区域(文字笔画) + // 3. 再膨胀回来,剩下的就是大面积暗区域(阴影/折痕) + // 4. 在这些区域内,只保留高方差像素(文字),其余推白 + Mat grayForShadow = new Mat(h, w, MatType.CV_8U, grayData); - // 上边缘 - for (int x = 0; x < w; x++) - { - for (int y = 0; y < Math.Min(maxScanDepth, h); y++) - { - 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; - } - } + // 二值化:暗像素=255, 亮像素=0 + int shadowBinThresh = (int)(paperBright * 0.65); + Mat darkBin = new Mat(); + Cv2.Threshold(grayForShadow, darkBin, shadowBinThresh, 255, ThresholdTypes.BinaryInv); + grayForShadow.Dispose(); - // 把阴影区域的结果设为白色 + // 腐蚀:去掉小面积暗区域(文字笔画宽度一般<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++) { - if (shadowFlag[i] == 1) - resultData[i] = 255; + if (shadowMaskData[i] > 128) + { + // 在大面积暗区域内 + if (varDataS[i] < 12) + { + // 低方差 → 阴影本身 → 白色 + resultData[i] = 255; + } + // 高方差 → 文字 → 保留 + } } Mat enhanced = new Mat(h, w, MatType.CV_8U);