diff --git a/CamScanner.cs b/CamScanner.cs index c8158af..5fb7085 100644 --- a/CamScanner.cs +++ b/CamScanner.cs @@ -322,7 +322,8 @@ public static class DocumentScanner // ========================================== // 第3步:图像增强 - // 减法提取墨迹深度 → 非线性加深 → 锐化 + // 核心:用morphologyEx(CLOSE)做背景估计(比高斯模糊更贴合局部亮度) + // 阴影区域的背景估计也低 → bg-gray差值小 → 阴影自然不变黑 // ========================================== private static Mat EnhanceDocument(Mat src) { @@ -330,15 +331,26 @@ public static class DocumentScanner Mat gray = new Mat(); Cv2.CvtColor(src, gray, ColorConversionCodes.BGR2GRAY); - // --- b: 背景估计 --- - int blurSize = Math.Max(gray.Width, gray.Height) / 8; - if (blurSize % 2 == 0) blurSize++; - if (blurSize < 51) blurSize = 51; + // --- b: 背景估计(形态学闭运算)--- + // 闭运算 = 先膨胀后腐蚀,效果是填平暗区域(文字),保留亮区域(背景) + // 比高斯模糊更好:不会把远处的亮区域扩散到阴影处 + int morphSize = Math.Max(gray.Width, gray.Height) / 25; + if (morphSize < 25) morphSize = 25; + if (morphSize % 2 == 0) morphSize++; + Mat morphKernel = Cv2.GetStructuringElement(MorphShapes.Ellipse, + new OpenCvSharp.Size(morphSize, morphSize)); Mat background = new Mat(); - Cv2.GaussianBlur(gray, background, new OpenCvSharp.Size(blurSize, blurSize), 0); + Cv2.MorphologyEx(gray, background, MorphTypes.Close, morphKernel); + morphKernel.Dispose(); - // --- c: 减法提取墨迹 + 非线性加深 --- + // 对背景估计做轻度模糊,消除形态学的块状伪影 + int smoothSize = morphSize / 2; + if (smoothSize % 2 == 0) smoothSize++; + if (smoothSize < 3) smoothSize = 3; + Cv2.GaussianBlur(background, background, new OpenCvSharp.Size(smoothSize, smoothSize), 0); + + // --- c: 减法提取墨迹 --- int w = gray.Width; int h = gray.Height; byte[] grayData = new byte[w * h]; @@ -348,8 +360,7 @@ public static class DocumentScanner Marshal.Copy(background.Data, bgData, 0, bgData.Length); background.Dispose(); - // 用百分位数而非最大值来计算增益(排除极端值) - // 先收集所有正墨迹值 + // 95百分位数增益 int[] inkHist = new int[256]; for (int i = 0; i < grayData.Length; i++) { @@ -358,8 +369,6 @@ public static class DocumentScanner if (ink > 255) ink = 255; inkHist[ink]++; } - - // 找 95 百分位数作为参考墨迹深度 int totalPixels = w * h; int target95 = (int)(totalPixels * 0.95); int cumulative = 0; @@ -374,35 +383,7 @@ public static class DocumentScanner } } - // 计算"正常纸面亮度":取灰度图中心区域的均值 - 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)); - - // 墨迹映射(带折痕/阴影抑制) - // - // 核心区分逻辑:文字 vs 折痕/阴影 - // 文字 = 高频信号,笔画边缘锐利,局部灰度变化剧烈 - // 折痕/阴影 = 低频信号,缓慢渐变,局部灰度变化平缓 - // - // 方法:先正常生成墨迹图,然后用"墨迹图的局部方差"来区分 - // 高方差区域 = 文字(保留) - // 低方差但偏暗区域 = 折痕/阴影(推向白色) - - // 先生成原始墨迹图(不做任何抑制) - byte[] inkData = new byte[w * h]; + // 墨迹映射(纯净版,不做任何阴影抑制) for (int i = 0; i < grayData.Length; i++) { int ink = bgData[i] - grayData[i]; @@ -417,180 +398,6 @@ public static class DocumentScanner if (val < 0) val = 0; if (val > 255) val = 255; resultData[i] = (byte)val; - inkData[i] = (byte)(255 - val); // 墨迹强度:0=无墨, 255=纯黑 - } - - // 计算两级局部方差: - // 小核(7):检测文字笔画(高频),文字处方差高 - // 大核(31):检测折痕渐变(低频),折痕处方差低 - // 只有两级方差都低时才抑制,避免误伤折痕附近的文字 - Mat grayMat = new Mat(h, w, MatType.CV_8U, grayData); - - // 小核方差(保护文字) - Mat localMeanS = new Mat(); - Cv2.Blur(grayMat, localMeanS, new OpenCvSharp.Size(7, 7)); - 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(); - - // 大核方差(检测折痕) - Mat localMeanL = new Mat(); - Cv2.Blur(grayMat, localMeanL, new OpenCvSharp.Size(31, 31)); - 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(); - - byte[] varDataS = new byte[w * h]; - byte[] varDataL = new byte[w * h]; - Marshal.Copy(localVarS.Data, varDataS, 0, varDataS.Length); - Marshal.Copy(localVarL.Data, varDataL, 0, varDataL.Length); - localVarS.Dispose(); - localVarL.Dispose(); - - // 抑制逻辑: - // 小核方差 < 5 且 大核方差 < 12 → 确定是平坦渐变区域(折痕/阴影) - // 小核方差 >= 5 → 有文字笔画,不抑制(即使大核方差低) - for (int i = 0; i < resultData.Length; i++) - { - if (inkData[i] > 10 && varDataS[i] < 5 && varDataL[i] < 12) - { - double suppressRatio = (double)varDataS[i] / 5.0; - int origVal = resultData[i]; - resultData[i] = (byte)(origVal + (int)((255 - origVal) * (1.0 - suppressRatio))); - } - } - - // --- 条带状折痕检测 --- - // 折痕特征:垂直或水平方向上,某列/行有大量连续暗像素 - // 文字不会形成这种长条状连续暗区域 - // - // 对每列统计暗像素比例,高比例的列标记为折痕条带 - int inkThreshForStripe = 30; // 墨迹强度>30才算暗像素 - double stripeRatioThresh = 0.4; // 一列中40%以上是暗像素就是条带 - int stripeHalfWidth = 3; // 条带半宽(左右各扩展3像素) - - // 垂直条带检测(按列扫描) - bool[] vStripe = new bool[w]; - for (int x = 0; x < w; x++) - { - int darkCount = 0; - for (int y = 0; y < h; y++) - { - if (inkData[y * w + x] > inkThreshForStripe) darkCount++; - } - vStripe[x] = ((double)darkCount / h) > stripeRatioThresh; - } - - // 水平条带检测(按行扫描) - bool[] hStripe = new bool[h]; - for (int y = 0; y < h; y++) - { - int darkCount = 0; - for (int x = 0; x < w; x++) - { - if (inkData[y * w + x] > inkThreshForStripe) darkCount++; - } - hStripe[y] = ((double)darkCount / w) > stripeRatioThresh; - } - - // 扩展条带宽度并抑制 - for (int i = 0; i < resultData.Length; i++) - { - int x = i % w; - int y = i / w; - - bool inStripe = false; - // 检查是否在垂直条带范围内 - for (int dx = -stripeHalfWidth; dx <= stripeHalfWidth; dx++) - { - int nx = x + dx; - if (nx >= 0 && nx < w && vStripe[nx]) { inStripe = true; break; } - } - // 检查是否在水平条带范围内 - if (!inStripe) - { - for (int dy = -stripeHalfWidth; dy <= stripeHalfWidth; dy++) - { - int ny = y + dy; - if (ny >= 0 && ny < h && hStripe[ny]) { inStripe = true; break; } - } - } - - if (inStripe && inkData[i] > 5) - { - // 在条带内:检查这个像素是否真的是文字 - // 文字在条带内的特征:小核方差高(笔画边缘锐利) - if (varDataS[i] < 10) - { - // 方差不高 → 不是文字,是折痕本身 → 推白 - resultData[i] = 255; - } - // 方差高 → 是文字,保留不动 - } - } - - // --- 大面积阴影检测(形态学方法)--- - // 思路: - // 1. 对原始灰度二值化,找出所有暗像素 - // 2. 用大核腐蚀,去掉小面积暗区域(文字笔画) - // 3. 再膨胀回来,剩下的就是大面积暗区域(阴影/折痕) - // 4. 在这些区域内,只保留高方差像素(文字),其余推白 - Mat grayForShadow = new Mat(h, w, MatType.CV_8U, grayData); - - // 二值化:暗像素=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(); - - // 在阴影区域内:保护文字,只清除阴影背景 - // 判断逻辑: - // 墨迹强度高(inkData > 40)→ 有明显文字 → 保留 - // 墨迹强度低 → 阴影背景 → 推白 - for (int i = 0; i < resultData.Length; i++) - { - if (shadowMaskData[i] > 128) - { - if (inkData[i] < 40) - { - // 墨迹弱 → 阴影背景 → 白色 - resultData[i] = 255; - } - // 墨迹强 → 文字 → 保留原值 - } } Mat enhanced = new Mat(h, w, MatType.CV_8U);