From 3075dcb0120e1d437a5080e91ba588888b08864f Mon Sep 17 00:00:00 2001 From: sinvo Date: Thu, 19 Mar 2026 16:39:48 +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=E5=90=8E=E5=A4=84=E7=90=86?= =?UTF-8?q?=E9=98=B6=E6=AE=B5=EF=BC=8C=E7=94=A8=E5=8C=BA=E5=9F=9F=E7=BA=A7?= =?UTF-8?q?=E6=A3=80=E6=B5=8B=E9=81=BF=E5=85=8D=E8=AF=AF=E4=BC=A4=E6=96=87?= =?UTF-8?q?=E5=AD=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 不再在墨迹提取循环中逐像素判断阴影(容易误判文字为阴影导致空心字), 改为先正常处理所有像素,最后用大核模糊的原始灰度图+OTSU自动阈值 生成阴影掩码,区域级地把阴影覆盖为白色。 Co-Authored-By: Claude Opus 4.6 --- CamScanner.cs | 91 ++++++++++++++++++++++++--------------------------- 1 file changed, 43 insertions(+), 48 deletions(-) diff --git a/CamScanner.cs b/CamScanner.cs index 4cd09bf..5c9b9c3 100644 --- a/CamScanner.cs +++ b/CamScanner.cs @@ -374,64 +374,18 @@ public static class DocumentScanner } } - // 阴影识别 + 墨迹映射 - // - // 阴影特征:原始灰度值本身很低(暗区域) - // 文字特征:原始灰度虽然比背景低,但绝对值不会太低(白纸上的黑字) - // - // 策略:用原始灰度值判断,如果太暗就认为是阴影 - // 同时用背景估计值辅助:如果背景本身就暗,也是阴影 - // - // 计算灰度中位数作为参考 - int[] grayHist = new int[256]; + // 墨迹映射(不做阴影判断,先全部正常处理) for (int i = 0; i < grayData.Length; i++) { - grayHist[grayData[i]]++; - } - int grayCum = 0; - int grayMedian = 128; - for (int i = 255; i >= 0; i--) - { - grayCum += grayHist[i]; - if (grayCum >= totalPixels / 2) - { - grayMedian = i; - break; - } - } - // 阴影阈值:原始灰度低于中位数的45%,或背景估计低于中位数的50% - int shadowGrayThresh = (int)(grayMedian * 0.45); - int shadowBgThresh = (int)(grayMedian * 0.50); - - for (int i = 0; i < grayData.Length; i++) - { - // 阴影检测:原始灰度很低 或 背景估计很低 → 阴影 → 白色 - if (grayData[i] < shadowGrayThresh || bgData[i] < shadowBgThresh) - { - // 渐变过渡,避免硬边 - // 越暗越白,用线性插值 - int darker = Math.Min(grayData[i], bgData[i]); - int thresh = Math.Max(shadowGrayThresh, shadowBgThresh); - if (darker < thresh) - { - double fade = (double)darker / thresh; // 0=极暗→全白, 1=阈值边界 - resultData[i] = (byte)(255 - (int)(fade * 40)); // 边界处约215,逐渐到255 - continue; - } - } - int ink = bgData[i] - grayData[i]; if (ink < 0) ink = 0; - // 归一化到 0-1 double inkNorm = (double)ink / ink95; if (inkNorm > 1.0) inkNorm = 1.0; // 非线性加深:pow(x, 0.45) 让浅墨迹也变深 - // 0.45 < 1 所以小值被放大(浅色文字变深) double darkness = Math.Pow(inkNorm, 0.45); - // 映射到灰度:0=白(255), 1=黑(0) int val = (int)(255.0 * (1.0 - darkness)); if (val < 0) val = 0; if (val > 255) val = 255; @@ -450,13 +404,54 @@ public static class DocumentScanner enhanced.Dispose(); // --- e: 对比度加强 --- - // 文字更黑,背景更白 Cv2.ConvertScaleAbs(sharpened, sharpened, 1.3, -20); // --- f: 白底清理 --- 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);