From 1eada92ca0ef283d89fa462b19d45623d3e2d91e Mon Sep 17 00:00:00 2001 From: sinvo Date: Thu, 19 Mar 2026 16:44:21 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E5=86=99=E9=98=B4=E5=BD=B1=E5=A4=84?= =?UTF-8?q?=E7=90=86=EF=BC=9A=E6=94=B9=E4=B8=BA=E8=BE=B9=E7=BC=98=E6=89=AB?= =?UTF-8?q?=E6=8F=8F=E6=B3=95=EF=BC=8C=E5=8F=AA=E5=A4=84=E7=90=86=E5=9B=9B?= =?UTF-8?q?=E5=91=A8=E8=BE=B9=E7=BC=98=E9=98=B4=E5=BD=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OTSU全局阈值会把文字密集区域误判为阴影。改为从图像四边向内 逐列/逐行扫描,遇到连续暗像素(低于纸面亮度55%)标记为阴影, 遇到亮像素立即停止。这样只会处理边缘连续暗区域,不会误伤 文档中间的文字。 Co-Authored-By: Claude Opus 4.6 --- CamScanner.cs | 124 ++++++++++++++++++++++++++++++++------------------ 1 file changed, 80 insertions(+), 44 deletions(-) diff --git a/CamScanner.cs b/CamScanner.cs index 5c9b9c3..cce66e9 100644 --- a/CamScanner.cs +++ b/CamScanner.cs @@ -374,7 +374,7 @@ public static class DocumentScanner } } - // 墨迹映射(不做阴影判断,先全部正常处理) + // 墨迹映射 for (int i = 0; i < grayData.Length; i++) { int ink = bgData[i] - grayData[i]; @@ -383,7 +383,6 @@ public static class DocumentScanner double inkNorm = (double)ink / ink95; if (inkNorm > 1.0) inkNorm = 1.0; - // 非线性加深:pow(x, 0.45) 让浅墨迹也变深 double darkness = Math.Pow(inkNorm, 0.45); int val = (int)(255.0 * (1.0 - darkness)); @@ -392,6 +391,85 @@ public static class DocumentScanner resultData[i] = (byte)val; } + // --- 阴影修复:从边缘向内扫描,把连续暗像素区域设为白色 --- + // 阴影只出现在文档四周边缘(拍照翘边/光照不均) + // 从上下左右四个方向向内扫描原始灰度,遇到连续暗区域就标记为阴影 + byte[] shadowFlag = new byte[w * h]; // 0=正常, 1=阴影 + + // 计算"正常纸面亮度":取灰度图中心区域的均值 + int cx1 = w / 4; + int cx2 = w * 3 / 4; + int cy1 = h / 4; + int cy2 = h * 3 / 4; + long centerSum = 0; + int centerCount = 0; + for (int y = cy1; y < cy2; y++) + { + for (int x = cx1; x < cx2; x++) + { + centerSum += grayData[y * w + x]; + centerCount++; + } + } + int paperBright = (int)(centerSum / Math.Max(centerCount, 1)); + // 阴影判定:原始灰度低于纸面亮度的55% + int darkThresh = (int)(paperBright * 0.55); + + // 从四个边缘向内扫描 + int maxScanDepth = Math.Max(w, h) / 8; // 最多扫描图像尺寸的1/8 + + // 上边缘 + 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; + } + } + + // 把阴影区域的结果设为白色 + for (int i = 0; i < resultData.Length; i++) + { + if (shadowFlag[i] == 1) + resultData[i] = 255; + } + Mat enhanced = new Mat(h, w, MatType.CV_8U); Marshal.Copy(resultData, 0, enhanced.Data, resultData.Length); @@ -410,48 +488,6 @@ public static class DocumentScanner Cv2.Threshold(sharpened, sharpened, 220, 255, ThresholdTypes.Trunc); Cv2.ConvertScaleAbs(sharpened, sharpened, 255.0 / 220.0, 0); - // --- g: 阴影区域后处理 --- - // 用小核模糊的原始灰度做区域级阴影检测(不是逐像素,避免误伤文字) - // 阴影 = 大面积暗区域,文字 = 小面积暗像素散布在亮背景中 - int shadowBlurSize = Math.Max(w, h) / 15; - if (shadowBlurSize % 2 == 0) shadowBlurSize++; - if (shadowBlurSize < 31) shadowBlurSize = 31; - - Mat grayBlurred = new Mat(); - Cv2.GaussianBlur(gray, grayBlurred, new OpenCvSharp.Size(shadowBlurSize, shadowBlurSize), 0); - - // 模糊后的灰度图:阴影区域整体偏暗,文字区域因为周围是白纸所以模糊后仍然亮 - // 找阈值:用 OTSU 自动找 - Mat shadowMask = new Mat(); - Cv2.Threshold(grayBlurred, shadowMask, 0, 255, ThresholdTypes.Binary | ThresholdTypes.Otsu); - grayBlurred.Dispose(); - - // shadowMask: 亮区域=255(正常), 暗区域=0(阴影) - // 对 mask 做膨胀,扩大阴影区域覆盖范围,避免边缘残留 - Mat dilateK = Cv2.GetStructuringElement(MorphShapes.Ellipse, new OpenCvSharp.Size(15, 15)); - Mat shadowMaskInv = new Mat(); - Cv2.BitwiseNot(shadowMask, shadowMaskInv); // 反转:阴影=255 - Cv2.Dilate(shadowMaskInv, shadowMaskInv, dilateK); - dilateK.Dispose(); - - // 在阴影区域把结果设为白色 - byte[] sharpenedData = new byte[w * h]; - byte[] maskData = new byte[w * h]; - Marshal.Copy(sharpened.Data, sharpenedData, 0, sharpenedData.Length); - Marshal.Copy(shadowMaskInv.Data, maskData, 0, maskData.Length); - shadowMask.Dispose(); - shadowMaskInv.Dispose(); - - for (int i = 0; i < sharpenedData.Length; i++) - { - if (maskData[i] > 128) - { - // 阴影区域 → 白色 - sharpenedData[i] = 255; - } - } - Marshal.Copy(sharpenedData, 0, sharpened.Data, sharpenedData.Length); - // 转回3通道 Mat output = new Mat(); Cv2.CvtColor(sharpened, output, ColorConversionCodes.GRAY2BGR);