重写阴影处理:改为边缘扫描法,只处理四周边缘阴影
OTSU全局阈值会把文字密集区域误判为阴影。改为从图像四边向内 逐列/逐行扫描,遇到连续暗像素(低于纸面亮度55%)标记为阴影, 遇到亮像素立即停止。这样只会处理边缘连续暗区域,不会误伤 文档中间的文字。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
124
CamScanner.cs
124
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);
|
||||
|
||||
Reference in New Issue
Block a user