重写阴影处理:改为后处理阶段,用区域级检测避免误伤文字
不再在墨迹提取循环中逐像素判断阴影(容易误判文字为阴影导致空心字), 改为先正常处理所有像素,最后用大核模糊的原始灰度图+OTSU自动阈值 生成阴影掩码,区域级地把阴影覆盖为白色。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -374,64 +374,18 @@ public static class DocumentScanner
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 阴影识别 + 墨迹映射
|
// 墨迹映射(不做阴影判断,先全部正常处理)
|
||||||
//
|
|
||||||
// 阴影特征:原始灰度值本身很低(暗区域)
|
|
||||||
// 文字特征:原始灰度虽然比背景低,但绝对值不会太低(白纸上的黑字)
|
|
||||||
//
|
|
||||||
// 策略:用原始灰度值判断,如果太暗就认为是阴影
|
|
||||||
// 同时用背景估计值辅助:如果背景本身就暗,也是阴影
|
|
||||||
//
|
|
||||||
// 计算灰度中位数作为参考
|
|
||||||
int[] grayHist = new int[256];
|
|
||||||
for (int i = 0; i < grayData.Length; i++)
|
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];
|
int ink = bgData[i] - grayData[i];
|
||||||
if (ink < 0) ink = 0;
|
if (ink < 0) ink = 0;
|
||||||
|
|
||||||
// 归一化到 0-1
|
|
||||||
double inkNorm = (double)ink / ink95;
|
double inkNorm = (double)ink / ink95;
|
||||||
if (inkNorm > 1.0) inkNorm = 1.0;
|
if (inkNorm > 1.0) inkNorm = 1.0;
|
||||||
|
|
||||||
// 非线性加深:pow(x, 0.45) 让浅墨迹也变深
|
// 非线性加深:pow(x, 0.45) 让浅墨迹也变深
|
||||||
// 0.45 < 1 所以小值被放大(浅色文字变深)
|
|
||||||
double darkness = Math.Pow(inkNorm, 0.45);
|
double darkness = Math.Pow(inkNorm, 0.45);
|
||||||
|
|
||||||
// 映射到灰度:0=白(255), 1=黑(0)
|
|
||||||
int val = (int)(255.0 * (1.0 - darkness));
|
int val = (int)(255.0 * (1.0 - darkness));
|
||||||
if (val < 0) val = 0;
|
if (val < 0) val = 0;
|
||||||
if (val > 255) val = 255;
|
if (val > 255) val = 255;
|
||||||
@@ -450,13 +404,54 @@ public static class DocumentScanner
|
|||||||
enhanced.Dispose();
|
enhanced.Dispose();
|
||||||
|
|
||||||
// --- e: 对比度加强 ---
|
// --- e: 对比度加强 ---
|
||||||
// 文字更黑,背景更白
|
|
||||||
Cv2.ConvertScaleAbs(sharpened, sharpened, 1.3, -20);
|
Cv2.ConvertScaleAbs(sharpened, sharpened, 1.3, -20);
|
||||||
|
|
||||||
// --- f: 白底清理 ---
|
// --- f: 白底清理 ---
|
||||||
Cv2.Threshold(sharpened, sharpened, 220, 255, ThresholdTypes.Trunc);
|
Cv2.Threshold(sharpened, sharpened, 220, 255, ThresholdTypes.Trunc);
|
||||||
Cv2.ConvertScaleAbs(sharpened, sharpened, 255.0 / 220.0, 0);
|
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通道
|
// 转回3通道
|
||||||
Mat output = new Mat();
|
Mat output = new Mat();
|
||||||
Cv2.CvtColor(sharpened, output, ColorConversionCodes.GRAY2BGR);
|
Cv2.CvtColor(sharpened, output, ColorConversionCodes.GRAY2BGR);
|
||||||
|
|||||||
Reference in New Issue
Block a user