边缘扫描只能处理从边缘开始的连续暗像素,无法处理折角三角形 阴影等不规则形状。改用形态学方法: 1. 灰度二值化找所有暗像素(<纸面亮度65%) 2. 大核腐蚀去掉文字笔画(小面积暗区域) 3. 膨胀回来得到大面积阴影掩码 4. 掩码内低方差像素推白,高方差像素(文字)保留 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1240 lines
44 KiB
C#
1240 lines
44 KiB
C#
// Quicker C# 模块 - 扫描全能王工具 v2
|
||
// 运行线程: 后台线程(STA)
|
||
//
|
||
// ====== DLL 部署说明 ======
|
||
// 1. 从 NuGet 下载以下两个包(版本必须一致,推荐 4.7.0):
|
||
// - OpenCvSharp4 → 取出 OpenCvSharp.dll
|
||
// - OpenCvSharp4.runtime.win → 取出 OpenCvSharpExtern.dll
|
||
// (在 nupkg 解压后的 runtimes/win-x64/native/ 目录下)
|
||
// - itextsharp 5.5.x → 取出 itextsharp.dll
|
||
//
|
||
// 2. 把这三个文件都放到同一个目录,例如:
|
||
// D:\UserFiles\Documents\Quicker\_packages\OpenCvSharp\4.7.0\
|
||
//
|
||
// 3. Quicker "引用DLL库"栏填写(每行一个完整路径):
|
||
// D:\...\OpenCvSharp.dll
|
||
// D:\...\itextsharp.dll
|
||
// (OpenCvSharpExtern.dll 不填,代码会自动从同目录加载)
|
||
//
|
||
// 4. 不需要引用 OpenCvSharp.Extensions.dll
|
||
|
||
//css_reference OpenCvSharp.dll;
|
||
//css_reference itextsharp.dll;
|
||
|
||
using System;
|
||
using System.Collections.Generic;
|
||
using System.Drawing;
|
||
using System.Drawing.Imaging;
|
||
using System.IO;
|
||
using System.Linq;
|
||
using System.Runtime.InteropServices;
|
||
using System.Windows.Forms;
|
||
using OpenCvSharp;
|
||
using iTextSharp.text;
|
||
using iTextSharp.text.pdf;
|
||
|
||
/// <summary>
|
||
/// 文档扫描处理核心 - 全自动流水线
|
||
/// 透视矫正 → 倾斜校正 → 图像增强,一步到位
|
||
/// </summary>
|
||
public static class DocumentScanner
|
||
{
|
||
/// <summary>
|
||
/// 主处理入口:对单张图像执行完整扫描增强流水线
|
||
/// </summary>
|
||
public static Mat ProcessImage(Mat src)
|
||
{
|
||
Mat result = src.Clone();
|
||
|
||
// 第1步:文档边缘检测 + 透视矫正
|
||
result = DetectAndCorrectPerspective(result);
|
||
|
||
// 第2步:倾斜校正
|
||
result = Deskew(result);
|
||
|
||
// 第3步:图像增强(Magic Color效果)
|
||
result = EnhanceDocument(result);
|
||
|
||
return result;
|
||
}
|
||
|
||
// ==========================================
|
||
// 第1步:文档边缘检测 + 透视矫正 + 自动裁边
|
||
// ==========================================
|
||
private static Mat DetectAndCorrectPerspective(Mat src)
|
||
{
|
||
int origW = src.Width;
|
||
int origH = src.Height;
|
||
|
||
// 缩小图像加速边缘检测
|
||
double scale = 1.0;
|
||
Mat resized = src.Clone();
|
||
if (Math.Max(origW, origH) > 1000)
|
||
{
|
||
scale = 1000.0 / Math.Max(origW, origH);
|
||
resized = new Mat();
|
||
Cv2.Resize(src, resized, new OpenCvSharp.Size(0, 0), scale, scale);
|
||
}
|
||
|
||
// 灰度 + 模糊
|
||
Mat gray = new Mat();
|
||
Cv2.CvtColor(resized, gray, ColorConversionCodes.BGR2GRAY);
|
||
Cv2.GaussianBlur(gray, gray, new OpenCvSharp.Size(5, 5), 0);
|
||
|
||
// 多策略边缘检测:先用宽松阈值,再用严格阈值
|
||
OpenCvSharp.Point[] docContour = null;
|
||
double imgArea = resized.Width * resized.Height;
|
||
|
||
int[][] cannyParams = new int[][] {
|
||
new int[] { 30, 120 }, // 宽松
|
||
new int[] { 50, 200 }, // 中等
|
||
new int[] { 75, 250 } // 严格
|
||
};
|
||
|
||
for (int p = 0; p < cannyParams.Length && docContour == null; p++)
|
||
{
|
||
Mat edged = new Mat();
|
||
Cv2.Canny(gray, edged, cannyParams[p][0], cannyParams[p][1]);
|
||
|
||
// 膨胀 + 闭运算,使边缘更连续
|
||
Mat k1 = Cv2.GetStructuringElement(MorphShapes.Rect, new OpenCvSharp.Size(5, 5));
|
||
Cv2.Dilate(edged, edged, k1);
|
||
Mat k2 = Cv2.GetStructuringElement(MorphShapes.Rect, new OpenCvSharp.Size(7, 7));
|
||
Cv2.MorphologyEx(edged, edged, MorphTypes.Close, k2);
|
||
|
||
OpenCvSharp.Point[][] contours;
|
||
HierarchyIndex[] hierarchy;
|
||
Cv2.FindContours(edged, out contours, out hierarchy,
|
||
RetrievalModes.List, ContourApproximationModes.ApproxSimple);
|
||
|
||
// 按面积降序
|
||
Array.Sort(contours, delegate(OpenCvSharp.Point[] a, OpenCvSharp.Point[] b) {
|
||
return Cv2.ContourArea(b).CompareTo(Cv2.ContourArea(a));
|
||
});
|
||
|
||
for (int i = 0; i < Math.Min(contours.Length, 15); i++)
|
||
{
|
||
double area = Cv2.ContourArea(contours[i]);
|
||
if (area < imgArea * 0.05) break; // 降低到5%
|
||
|
||
double peri = Cv2.ArcLength(contours[i], true);
|
||
OpenCvSharp.Point[] approx = Cv2.ApproxPolyDP(contours[i], 0.02 * peri, true);
|
||
|
||
if (approx.Length == 4 && Cv2.IsContourConvex(approx))
|
||
{
|
||
docContour = approx;
|
||
break;
|
||
}
|
||
}
|
||
|
||
edged.Dispose();
|
||
k1.Dispose();
|
||
k2.Dispose();
|
||
}
|
||
|
||
// 回退策略:如果没找到四边形,尝试用最大轮廓的最小外接矩形
|
||
if (docContour == null)
|
||
{
|
||
Mat edged2 = new Mat();
|
||
Cv2.Canny(gray, edged2, 30, 100);
|
||
Mat kd = Cv2.GetStructuringElement(MorphShapes.Rect, new OpenCvSharp.Size(9, 9));
|
||
Cv2.Dilate(edged2, edged2, kd);
|
||
Cv2.MorphologyEx(edged2, edged2, MorphTypes.Close, kd);
|
||
|
||
OpenCvSharp.Point[][] contours2;
|
||
HierarchyIndex[] hierarchy2;
|
||
Cv2.FindContours(edged2, out contours2, out hierarchy2,
|
||
RetrievalModes.External, ContourApproximationModes.ApproxSimple);
|
||
|
||
double maxArea = 0;
|
||
int maxIdx = -1;
|
||
for (int i = 0; i < contours2.Length; i++)
|
||
{
|
||
double a = Cv2.ContourArea(contours2[i]);
|
||
if (a > maxArea) { maxArea = a; maxIdx = i; }
|
||
}
|
||
|
||
if (maxIdx >= 0 && maxArea > imgArea * 0.05)
|
||
{
|
||
RotatedRect rr = Cv2.MinAreaRect(contours2[maxIdx]);
|
||
Point2f[] pts = rr.Points();
|
||
docContour = new OpenCvSharp.Point[4];
|
||
for (int i = 0; i < 4; i++)
|
||
{
|
||
docContour[i] = new OpenCvSharp.Point((int)pts[i].X, (int)pts[i].Y);
|
||
}
|
||
}
|
||
|
||
edged2.Dispose();
|
||
kd.Dispose();
|
||
}
|
||
|
||
gray.Dispose();
|
||
if (resized != src) resized.Dispose();
|
||
|
||
if (docContour == null) return src; // 实在找不到,原样返回
|
||
|
||
// 将坐标映射回原始尺寸
|
||
Point2f[] srcPts = new Point2f[4];
|
||
for (int i = 0; i < 4; i++)
|
||
{
|
||
srcPts[i] = new Point2f(
|
||
(float)(docContour[i].X / scale),
|
||
(float)(docContour[i].Y / scale));
|
||
}
|
||
|
||
// 对四个角点排序:左上、右上、右下、左下
|
||
srcPts = OrderPoints(srcPts);
|
||
|
||
// 计算目标矩形尺寸
|
||
double widthTop = Distance(srcPts[0], srcPts[1]);
|
||
double widthBot = Distance(srcPts[3], srcPts[2]);
|
||
int maxW = (int)Math.Max(widthTop, widthBot);
|
||
|
||
double heightLeft = Distance(srcPts[0], srcPts[3]);
|
||
double heightRight = Distance(srcPts[1], srcPts[2]);
|
||
int maxH = (int)Math.Max(heightLeft, heightRight);
|
||
|
||
if (maxW < 100 || maxH < 100) return src; // 太小不处理
|
||
|
||
Point2f[] dstPts = new Point2f[] {
|
||
new Point2f(0, 0),
|
||
new Point2f(maxW - 1, 0),
|
||
new Point2f(maxW - 1, maxH - 1),
|
||
new Point2f(0, maxH - 1)
|
||
};
|
||
|
||
Mat M = Cv2.GetPerspectiveTransform(srcPts, dstPts);
|
||
Mat warped = new Mat();
|
||
Cv2.WarpPerspective(src, warped, M, new OpenCvSharp.Size(maxW, maxH),
|
||
InterpolationFlags.Linear, BorderTypes.Constant, Scalar.White);
|
||
M.Dispose();
|
||
src.Dispose();
|
||
|
||
return warped;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 对四个点排序:左上、右上、右下、左下
|
||
/// </summary>
|
||
private static Point2f[] OrderPoints(Point2f[] pts)
|
||
{
|
||
// 按 x+y 排序找左上和右下
|
||
Point2f[] sorted = new Point2f[4];
|
||
List<Point2f> list = new List<Point2f>(pts);
|
||
|
||
// 左上 = x+y 最小,右下 = x+y 最大
|
||
list.Sort(delegate(Point2f a, Point2f b) {
|
||
return (a.X + a.Y).CompareTo(b.X + b.Y);
|
||
});
|
||
sorted[0] = list[0]; // 左上
|
||
sorted[2] = list[3]; // 右下
|
||
|
||
// 右上 = x-y 最大,左下 = x-y 最小
|
||
list.Sort(delegate(Point2f a, Point2f b) {
|
||
return (a.X - a.Y).CompareTo(b.X - b.Y);
|
||
});
|
||
sorted[1] = list[3]; // 右上
|
||
sorted[3] = list[0]; // 左下
|
||
|
||
return sorted;
|
||
}
|
||
|
||
private static double Distance(Point2f a, Point2f b)
|
||
{
|
||
double dx = a.X - b.X;
|
||
double dy = a.Y - b.Y;
|
||
return Math.Sqrt(dx * dx + dy * dy);
|
||
}
|
||
|
||
// ==========================================
|
||
// 第2步:倾斜校正(Deskew)
|
||
// ==========================================
|
||
private static Mat Deskew(Mat src)
|
||
{
|
||
Mat gray = new Mat();
|
||
Cv2.CvtColor(src, gray, ColorConversionCodes.BGR2GRAY);
|
||
|
||
// 二值化后检测边缘,比直接Canny更稳定
|
||
Mat thresh = new Mat();
|
||
Cv2.AdaptiveThreshold(gray, thresh, 255,
|
||
AdaptiveThresholdTypes.GaussianC,
|
||
ThresholdTypes.BinaryInv, 15, 5);
|
||
|
||
Mat edges = new Mat();
|
||
Cv2.Canny(thresh, edges, 50, 150, 3);
|
||
thresh.Dispose();
|
||
|
||
// 膨胀使文字行连成线段
|
||
Mat dk = Cv2.GetStructuringElement(MorphShapes.Rect, new OpenCvSharp.Size(15, 1));
|
||
Cv2.Dilate(edges, edges, dk);
|
||
dk.Dispose();
|
||
|
||
// 霍夫直线检测 — 降低阈值,缩短最小线段长度
|
||
LineSegmentPoint[] lines = Cv2.HoughLinesP(edges, 1, Math.PI / 180.0,
|
||
50, 50, 10);
|
||
|
||
gray.Dispose();
|
||
edges.Dispose();
|
||
|
||
if (lines == null || lines.Length == 0) return src;
|
||
|
||
// 收集所有线段角度,按线段长度加权
|
||
List<double> angles = new List<double>();
|
||
List<double> weights = new List<double>();
|
||
for (int i = 0; i < lines.Length; i++)
|
||
{
|
||
double dx = lines[i].P2.X - lines[i].P1.X;
|
||
double dy = lines[i].P2.Y - lines[i].P1.Y;
|
||
double len = Math.Sqrt(dx * dx + dy * dy);
|
||
double angle = Math.Atan2(dy, dx) * 180.0 / Math.PI;
|
||
|
||
// 只关注接近水平的线(-30° ~ 30°)
|
||
if (Math.Abs(angle) < 30)
|
||
{
|
||
angles.Add(angle);
|
||
weights.Add(len);
|
||
}
|
||
}
|
||
|
||
if (angles.Count == 0) return src;
|
||
|
||
// 加权中位数:按长度排序后取加权中间位置
|
||
// 简化:先按角度排序,取中位数
|
||
angles.Sort();
|
||
double medianAngle = angles[angles.Count / 2];
|
||
|
||
// 降低阈值:只要 > 0.1° 就校正,上限放宽到 20°
|
||
if (Math.Abs(medianAngle) < 0.1 || Math.Abs(medianAngle) > 20)
|
||
return src;
|
||
|
||
// 旋转校正
|
||
Point2f center = new Point2f(src.Width / 2f, src.Height / 2f);
|
||
Mat rotMat = Cv2.GetRotationMatrix2D(center, medianAngle, 1.0);
|
||
Mat rotated = new Mat();
|
||
Cv2.WarpAffine(src, rotated, rotMat, src.Size(),
|
||
InterpolationFlags.Cubic, BorderTypes.Constant, Scalar.White);
|
||
rotMat.Dispose();
|
||
src.Dispose();
|
||
|
||
return rotated;
|
||
}
|
||
|
||
// ==========================================
|
||
// 第3步:图像增强
|
||
// 减法提取墨迹深度 → 非线性加深 → 锐化
|
||
// ==========================================
|
||
private static Mat EnhanceDocument(Mat src)
|
||
{
|
||
// --- a: 转灰度 ---
|
||
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;
|
||
|
||
Mat background = new Mat();
|
||
Cv2.GaussianBlur(gray, background, new OpenCvSharp.Size(blurSize, blurSize), 0);
|
||
|
||
// --- c: 减法提取墨迹 + 非线性加深 ---
|
||
int w = gray.Width;
|
||
int h = gray.Height;
|
||
byte[] grayData = new byte[w * h];
|
||
byte[] bgData = new byte[w * h];
|
||
byte[] resultData = new byte[w * h];
|
||
Marshal.Copy(gray.Data, grayData, 0, grayData.Length);
|
||
Marshal.Copy(background.Data, bgData, 0, bgData.Length);
|
||
background.Dispose();
|
||
|
||
// 用百分位数而非最大值来计算增益(排除极端值)
|
||
// 先收集所有正墨迹值
|
||
int[] inkHist = new int[256];
|
||
for (int i = 0; i < grayData.Length; i++)
|
||
{
|
||
int ink = bgData[i] - grayData[i];
|
||
if (ink < 0) ink = 0;
|
||
if (ink > 255) ink = 255;
|
||
inkHist[ink]++;
|
||
}
|
||
|
||
// 找 95 百分位数作为参考墨迹深度
|
||
int totalPixels = w * h;
|
||
int target95 = (int)(totalPixels * 0.95);
|
||
int cumulative = 0;
|
||
int ink95 = 10;
|
||
for (int i = 0; i < 256; i++)
|
||
{
|
||
cumulative += inkHist[i];
|
||
if (cumulative >= target95)
|
||
{
|
||
ink95 = Math.Max(i, 5);
|
||
break;
|
||
}
|
||
}
|
||
|
||
// 计算"正常纸面亮度":取灰度图中心区域的均值
|
||
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];
|
||
if (ink < 0) ink = 0;
|
||
|
||
double inkNorm = (double)ink / ink95;
|
||
if (inkNorm > 1.0) inkNorm = 1.0;
|
||
|
||
double darkness = Math.Pow(inkNorm, 0.45);
|
||
|
||
int val = (int)(255.0 * (1.0 - darkness));
|
||
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();
|
||
|
||
// 在阴影区域内:高方差像素保留(文字),低方差推白
|
||
for (int i = 0; i < resultData.Length; i++)
|
||
{
|
||
if (shadowMaskData[i] > 128)
|
||
{
|
||
// 在大面积暗区域内
|
||
if (varDataS[i] < 12)
|
||
{
|
||
// 低方差 → 阴影本身 → 白色
|
||
resultData[i] = 255;
|
||
}
|
||
// 高方差 → 文字 → 保留
|
||
}
|
||
}
|
||
|
||
Mat enhanced = new Mat(h, w, MatType.CV_8U);
|
||
Marshal.Copy(resultData, 0, enhanced.Data, resultData.Length);
|
||
|
||
// --- d: USM锐化 ---
|
||
Mat blurred = new Mat();
|
||
Cv2.GaussianBlur(enhanced, blurred, new OpenCvSharp.Size(0, 0), 1.0);
|
||
Mat sharpened = new Mat();
|
||
Cv2.AddWeighted(enhanced, 1.8, blurred, -0.8, 0, sharpened);
|
||
blurred.Dispose();
|
||
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);
|
||
|
||
// 转回3通道
|
||
Mat output = new Mat();
|
||
Cv2.CvtColor(sharpened, output, ColorConversionCodes.GRAY2BGR);
|
||
|
||
gray.Dispose();
|
||
sharpened.Dispose();
|
||
src.Dispose();
|
||
|
||
return output;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// PDF 读写工具类
|
||
/// </summary>
|
||
public static class PdfHelper
|
||
{
|
||
/// <summary>
|
||
/// 从PDF提取所有页面中的嵌入图像
|
||
/// </summary>
|
||
public static List<Bitmap> ExtractImagesFromPdf(string pdfPath)
|
||
{
|
||
List<Bitmap> images = new List<Bitmap>();
|
||
PdfReader reader = new PdfReader(pdfPath);
|
||
|
||
for (int pageNum = 1; pageNum <= reader.NumberOfPages; pageNum++)
|
||
{
|
||
PdfDictionary page = reader.GetPageN(pageNum);
|
||
PdfDictionary resources = page.GetAsDict(PdfName.RESOURCES);
|
||
if (resources == null) continue;
|
||
|
||
PdfDictionary xObjects = resources.GetAsDict(PdfName.XOBJECT);
|
||
if (xObjects == null) continue;
|
||
|
||
foreach (PdfName name in xObjects.Keys)
|
||
{
|
||
PdfObject obj = xObjects.Get(name);
|
||
if (!obj.IsIndirect()) continue;
|
||
|
||
PdfDictionary tg = (PdfDictionary)PdfReader.GetPdfObject(obj);
|
||
PdfName subtype = tg.GetAsName(PdfName.SUBTYPE);
|
||
if (!PdfName.IMAGE.Equals(subtype)) continue;
|
||
|
||
try
|
||
{
|
||
int xrefIdx = ((PRIndirectReference)obj).Number;
|
||
PdfObject pdfObj = reader.GetPdfObject(xrefIdx);
|
||
PdfStream stream = (PdfStream)pdfObj;
|
||
byte[] rawBytes = PdfReader.GetStreamBytesRaw((PRStream)stream);
|
||
|
||
PdfName filter = stream.GetAsName(PdfName.FILTER);
|
||
if (PdfName.DCTDECODE.Equals(filter))
|
||
{
|
||
// JPEG 直接解码
|
||
using (MemoryStream ms = new MemoryStream(rawBytes))
|
||
{
|
||
Bitmap bmp = new Bitmap(ms);
|
||
images.Add(new Bitmap(bmp));
|
||
}
|
||
}
|
||
else if (PdfName.FLATEDECODE.Equals(filter) || filter == null)
|
||
{
|
||
byte[] decoded = PdfReader.GetStreamBytes((PRStream)stream);
|
||
int width = tg.GetAsNumber(PdfName.WIDTH).IntValue;
|
||
int height = tg.GetAsNumber(PdfName.HEIGHT).IntValue;
|
||
int bpc = tg.GetAsNumber(PdfName.BITSPERCOMPONENT).IntValue;
|
||
PdfName cs = tg.GetAsName(PdfName.COLORSPACE);
|
||
|
||
Bitmap bmp = RawToBitmap(decoded, width, height, bpc, cs);
|
||
if (bmp != null) images.Add(bmp);
|
||
}
|
||
else
|
||
{
|
||
byte[] decoded = PdfReader.GetStreamBytes((PRStream)stream);
|
||
try
|
||
{
|
||
using (MemoryStream ms = new MemoryStream(decoded))
|
||
{
|
||
images.Add(new Bitmap(new Bitmap(ms)));
|
||
}
|
||
}
|
||
catch { }
|
||
}
|
||
}
|
||
catch { continue; }
|
||
}
|
||
}
|
||
reader.Close();
|
||
|
||
if (images.Count == 0)
|
||
{
|
||
throw new Exception("无法从PDF中提取图像。该PDF可能是纯文本或使用了不支持的编码。");
|
||
}
|
||
return images;
|
||
}
|
||
|
||
private static Bitmap RawToBitmap(byte[] data, int w, int h, int bpc, PdfName colorSpace)
|
||
{
|
||
if (bpc != 8) return null;
|
||
bool isRgb = PdfName.DEVICERGB.Equals(colorSpace);
|
||
bool isGray = PdfName.DEVICEGRAY.Equals(colorSpace);
|
||
|
||
if (isRgb && data.Length >= w * h * 3)
|
||
{
|
||
Bitmap bmp = new Bitmap(w, h, PixelFormat.Format24bppRgb);
|
||
BitmapData bd = bmp.LockBits(
|
||
new System.Drawing.Rectangle(0, 0, w, h),
|
||
ImageLockMode.WriteOnly, PixelFormat.Format24bppRgb);
|
||
for (int y = 0; y < h; y++)
|
||
{
|
||
for (int x = 0; x < w; x++)
|
||
{
|
||
int si = (y * w + x) * 3;
|
||
int di = y * bd.Stride + x * 3;
|
||
Marshal.WriteByte(bd.Scan0, di, data[si + 2]);
|
||
Marshal.WriteByte(bd.Scan0, di + 1, data[si + 1]);
|
||
Marshal.WriteByte(bd.Scan0, di + 2, data[si]);
|
||
}
|
||
}
|
||
bmp.UnlockBits(bd);
|
||
return bmp;
|
||
}
|
||
else if (isGray && data.Length >= w * h)
|
||
{
|
||
Bitmap bmp = new Bitmap(w, h, PixelFormat.Format24bppRgb);
|
||
BitmapData bd = bmp.LockBits(
|
||
new System.Drawing.Rectangle(0, 0, w, h),
|
||
ImageLockMode.WriteOnly, PixelFormat.Format24bppRgb);
|
||
for (int y = 0; y < h; y++)
|
||
{
|
||
for (int x = 0; x < w; x++)
|
||
{
|
||
byte g = data[y * w + x];
|
||
int di = y * bd.Stride + x * 3;
|
||
Marshal.WriteByte(bd.Scan0, di, g);
|
||
Marshal.WriteByte(bd.Scan0, di + 1, g);
|
||
Marshal.WriteByte(bd.Scan0, di + 2, g);
|
||
}
|
||
}
|
||
bmp.UnlockBits(bd);
|
||
return bmp;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 将多张图像保存为单个PDF
|
||
/// </summary>
|
||
public static void SaveImagesToPdf(List<Bitmap> images, string outputPath, int jpegQuality)
|
||
{
|
||
Document doc = null;
|
||
PdfWriter writer = null;
|
||
FileStream fs = null;
|
||
try
|
||
{
|
||
fs = new FileStream(outputPath, FileMode.Create);
|
||
Bitmap first = images[0];
|
||
doc = new Document(new iTextSharp.text.Rectangle(first.Width, first.Height), 0, 0, 0, 0);
|
||
writer = PdfWriter.GetInstance(doc, fs);
|
||
doc.Open();
|
||
|
||
for (int i = 0; i < images.Count; i++)
|
||
{
|
||
Bitmap bmp = images[i];
|
||
if (i > 0)
|
||
{
|
||
doc.SetPageSize(new iTextSharp.text.Rectangle(bmp.Width, bmp.Height));
|
||
doc.NewPage();
|
||
}
|
||
|
||
byte[] imgBytes;
|
||
using (MemoryStream ms = new MemoryStream())
|
||
{
|
||
EncoderParameters ep = new EncoderParameters(1);
|
||
ep.Param[0] = new EncoderParameter(
|
||
System.Drawing.Imaging.Encoder.Quality, (long)jpegQuality);
|
||
ImageCodecInfo codec = GetJpegCodec();
|
||
bmp.Save(ms, codec, ep);
|
||
imgBytes = ms.ToArray();
|
||
}
|
||
|
||
iTextSharp.text.Image pdfImg = iTextSharp.text.Image.GetInstance(imgBytes);
|
||
pdfImg.SetAbsolutePosition(0, 0);
|
||
pdfImg.ScaleAbsolute(bmp.Width, bmp.Height);
|
||
doc.Add(pdfImg);
|
||
}
|
||
}
|
||
finally
|
||
{
|
||
if (doc != null && doc.IsOpen()) doc.Close();
|
||
if (writer != null) writer.Close();
|
||
if (fs != null) fs.Close();
|
||
}
|
||
}
|
||
|
||
private static ImageCodecInfo GetJpegCodec()
|
||
{
|
||
ImageCodecInfo[] codecs = ImageCodecInfo.GetImageDecoders();
|
||
for (int i = 0; i < codecs.Length; i++)
|
||
{
|
||
if (codecs[i].FormatID == ImageFormat.Jpeg.Guid) return codecs[i];
|
||
}
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 简化版界面 - 一键处理,无需选模式
|
||
/// </summary>
|
||
public class ScannerForm : Form
|
||
{
|
||
private ListBox lstFiles;
|
||
private Button btnAddImages, btnAddPdf, btnRemove, btnClear;
|
||
private Button btnMoveUp, btnMoveDown, btnProcess;
|
||
private NumericUpDown nudQuality;
|
||
private ProgressBar progressBar;
|
||
private Label lblStatus;
|
||
private PictureBox picPreview;
|
||
|
||
private List<string> inputFiles = new List<string>();
|
||
private bool isPdfInput = false;
|
||
|
||
public ScannerForm()
|
||
{
|
||
InitUI();
|
||
}
|
||
|
||
private void InitUI()
|
||
{
|
||
this.Text = "文档扫描增强工具 v2";
|
||
this.Size = new System.Drawing.Size(720, 600);
|
||
this.StartPosition = FormStartPosition.CenterScreen;
|
||
this.FormBorderStyle = FormBorderStyle.FixedDialog;
|
||
this.MaximizeBox = false;
|
||
this.BackColor = System.Drawing.Color.FromArgb(245, 245, 250);
|
||
|
||
// 文件列表标题
|
||
Label lblFiles = new Label();
|
||
lblFiles.Text = "输入文件:";
|
||
lblFiles.Location = new System.Drawing.Point(15, 12);
|
||
lblFiles.AutoSize = true;
|
||
lblFiles.Font = new System.Drawing.Font("微软雅黑", 9f, System.Drawing.FontStyle.Bold);
|
||
this.Controls.Add(lblFiles);
|
||
|
||
// 文件列表
|
||
lstFiles = new ListBox();
|
||
lstFiles.Location = new System.Drawing.Point(15, 35);
|
||
lstFiles.Size = new System.Drawing.Size(460, 180);
|
||
lstFiles.Font = new System.Drawing.Font("Consolas", 9f);
|
||
lstFiles.HorizontalScrollbar = true;
|
||
lstFiles.SelectedIndexChanged += delegate(object s, EventArgs ev) {
|
||
int idx = lstFiles.SelectedIndex;
|
||
if (idx >= 0 && idx < inputFiles.Count && !isPdfInput)
|
||
{
|
||
try
|
||
{
|
||
if (picPreview.Image != null) picPreview.Image.Dispose();
|
||
picPreview.Image = new Bitmap(inputFiles[idx]);
|
||
}
|
||
catch { }
|
||
}
|
||
};
|
||
this.Controls.Add(lstFiles);
|
||
|
||
// 右侧按钮
|
||
int bx = 490;
|
||
int bw = 100;
|
||
|
||
btnAddImages = new Button();
|
||
btnAddImages.Text = "添加图片";
|
||
btnAddImages.Location = new System.Drawing.Point(bx, 35);
|
||
btnAddImages.Size = new System.Drawing.Size(bw, 32);
|
||
btnAddImages.Click += BtnAddImages_Click;
|
||
this.Controls.Add(btnAddImages);
|
||
|
||
btnAddPdf = new Button();
|
||
btnAddPdf.Text = "选择PDF";
|
||
btnAddPdf.Location = new System.Drawing.Point(bx + 110, 35);
|
||
btnAddPdf.Size = new System.Drawing.Size(bw, 32);
|
||
btnAddPdf.Click += BtnAddPdf_Click;
|
||
this.Controls.Add(btnAddPdf);
|
||
|
||
btnRemove = new Button();
|
||
btnRemove.Text = "移除选中";
|
||
btnRemove.Location = new System.Drawing.Point(bx, 75);
|
||
btnRemove.Size = new System.Drawing.Size(bw, 32);
|
||
btnRemove.Click += delegate {
|
||
if (lstFiles.SelectedIndex >= 0)
|
||
{
|
||
int idx = lstFiles.SelectedIndex;
|
||
inputFiles.RemoveAt(idx);
|
||
lstFiles.Items.RemoveAt(idx);
|
||
if (inputFiles.Count == 0) isPdfInput = false;
|
||
}
|
||
};
|
||
this.Controls.Add(btnRemove);
|
||
|
||
btnClear = new Button();
|
||
btnClear.Text = "清空列表";
|
||
btnClear.Location = new System.Drawing.Point(bx + 110, 75);
|
||
btnClear.Size = new System.Drawing.Size(bw, 32);
|
||
btnClear.Click += delegate {
|
||
inputFiles.Clear();
|
||
lstFiles.Items.Clear();
|
||
isPdfInput = false;
|
||
if (picPreview.Image != null) picPreview.Image.Dispose();
|
||
picPreview.Image = null;
|
||
lblStatus.Text = "已清空";
|
||
};
|
||
this.Controls.Add(btnClear);
|
||
|
||
btnMoveUp = new Button();
|
||
btnMoveUp.Text = "↑ 上移";
|
||
btnMoveUp.Location = new System.Drawing.Point(bx, 115);
|
||
btnMoveUp.Size = new System.Drawing.Size(bw, 32);
|
||
btnMoveUp.Click += delegate {
|
||
int idx = lstFiles.SelectedIndex;
|
||
if (idx > 0) SwapItems(idx, idx - 1);
|
||
};
|
||
this.Controls.Add(btnMoveUp);
|
||
|
||
btnMoveDown = new Button();
|
||
btnMoveDown.Text = "↓ 下移";
|
||
btnMoveDown.Location = new System.Drawing.Point(bx + 110, 115);
|
||
btnMoveDown.Size = new System.Drawing.Size(bw, 32);
|
||
btnMoveDown.Click += delegate {
|
||
int idx = lstFiles.SelectedIndex;
|
||
if (idx >= 0 && idx < lstFiles.Items.Count - 1) SwapItems(idx, idx + 1);
|
||
};
|
||
this.Controls.Add(btnMoveDown);
|
||
|
||
// JPEG质量
|
||
GroupBox grp = new GroupBox();
|
||
grp.Text = "输出设置";
|
||
grp.Location = new System.Drawing.Point(15, 222);
|
||
grp.Size = new System.Drawing.Size(685, 55);
|
||
grp.Font = new System.Drawing.Font("微软雅黑", 9f);
|
||
this.Controls.Add(grp);
|
||
|
||
Label lblQ = new Label();
|
||
lblQ.Text = "PDF图像质量:";
|
||
lblQ.Location = new System.Drawing.Point(15, 23);
|
||
lblQ.AutoSize = true;
|
||
grp.Controls.Add(lblQ);
|
||
|
||
nudQuality = new NumericUpDown();
|
||
nudQuality.Location = new System.Drawing.Point(110, 20);
|
||
nudQuality.Size = new System.Drawing.Size(60, 25);
|
||
nudQuality.Minimum = 50;
|
||
nudQuality.Maximum = 100;
|
||
nudQuality.Value = 92;
|
||
grp.Controls.Add(nudQuality);
|
||
|
||
Label lblQH = new Label();
|
||
lblQH.Text = "(自动执行:透视矫正 → 倾斜校正 → 增强美化,一步到位)";
|
||
lblQH.Location = new System.Drawing.Point(180, 23);
|
||
lblQH.AutoSize = true;
|
||
lblQH.ForeColor = System.Drawing.Color.FromArgb(100, 100, 100);
|
||
lblQH.Font = new System.Drawing.Font("微软雅黑", 8f);
|
||
grp.Controls.Add(lblQH);
|
||
|
||
// 预览
|
||
Label lblPrev = new Label();
|
||
lblPrev.Text = "预览:";
|
||
lblPrev.Location = new System.Drawing.Point(15, 285);
|
||
lblPrev.AutoSize = true;
|
||
lblPrev.Font = new System.Drawing.Font("微软雅黑", 9f, System.Drawing.FontStyle.Bold);
|
||
this.Controls.Add(lblPrev);
|
||
|
||
picPreview = new PictureBox();
|
||
picPreview.Location = new System.Drawing.Point(15, 308);
|
||
picPreview.Size = new System.Drawing.Size(685, 190);
|
||
picPreview.SizeMode = PictureBoxSizeMode.Zoom;
|
||
picPreview.BorderStyle = BorderStyle.FixedSingle;
|
||
picPreview.BackColor = System.Drawing.Color.White;
|
||
this.Controls.Add(picPreview);
|
||
|
||
// 底部
|
||
progressBar = new ProgressBar();
|
||
progressBar.Location = new System.Drawing.Point(15, 510);
|
||
progressBar.Size = new System.Drawing.Size(480, 25);
|
||
this.Controls.Add(progressBar);
|
||
|
||
lblStatus = new Label();
|
||
lblStatus.Text = "就绪 - 添加文件后点击处理";
|
||
lblStatus.Location = new System.Drawing.Point(15, 538);
|
||
lblStatus.Size = new System.Drawing.Size(480, 20);
|
||
lblStatus.ForeColor = System.Drawing.Color.DarkGray;
|
||
this.Controls.Add(lblStatus);
|
||
|
||
btnProcess = new Button();
|
||
btnProcess.Text = "一键处理并导出PDF";
|
||
btnProcess.Location = new System.Drawing.Point(510, 508);
|
||
btnProcess.Size = new System.Drawing.Size(190, 35);
|
||
btnProcess.Font = new System.Drawing.Font("微软雅黑", 10f, System.Drawing.FontStyle.Bold);
|
||
btnProcess.BackColor = System.Drawing.Color.FromArgb(0, 120, 215);
|
||
btnProcess.ForeColor = System.Drawing.Color.White;
|
||
btnProcess.FlatStyle = FlatStyle.Flat;
|
||
btnProcess.Click += BtnProcess_Click;
|
||
this.Controls.Add(btnProcess);
|
||
}
|
||
|
||
private void SwapItems(int a, int b)
|
||
{
|
||
string tf = inputFiles[a];
|
||
inputFiles[a] = inputFiles[b];
|
||
inputFiles[b] = tf;
|
||
object ti = lstFiles.Items[a];
|
||
lstFiles.Items[a] = lstFiles.Items[b];
|
||
lstFiles.Items[b] = ti;
|
||
lstFiles.SelectedIndex = b;
|
||
}
|
||
|
||
private void BtnAddImages_Click(object sender, EventArgs e)
|
||
{
|
||
if (isPdfInput)
|
||
{
|
||
MessageBox.Show("已选择PDF,请先清空列表。", "提示");
|
||
return;
|
||
}
|
||
OpenFileDialog ofd = new OpenFileDialog();
|
||
ofd.Title = "选择图片";
|
||
ofd.Filter = "图片|*.jpg;*.jpeg;*.png;*.bmp;*.tiff;*.tif;*.gif|所有文件|*.*";
|
||
ofd.Multiselect = true;
|
||
if (ofd.ShowDialog() == DialogResult.OK)
|
||
{
|
||
foreach (string f in ofd.FileNames)
|
||
{
|
||
if (!inputFiles.Contains(f))
|
||
{
|
||
inputFiles.Add(f);
|
||
lstFiles.Items.Add(Path.GetFileName(f));
|
||
}
|
||
}
|
||
lblStatus.Text = string.Format("共 {0} 个文件", inputFiles.Count);
|
||
}
|
||
}
|
||
|
||
private void BtnAddPdf_Click(object sender, EventArgs e)
|
||
{
|
||
if (inputFiles.Count > 0 && !isPdfInput)
|
||
{
|
||
MessageBox.Show("已添加图片,请先清空列表。", "提示");
|
||
return;
|
||
}
|
||
OpenFileDialog ofd = new OpenFileDialog();
|
||
ofd.Title = "选择PDF";
|
||
ofd.Filter = "PDF|*.pdf";
|
||
if (ofd.ShowDialog() == DialogResult.OK)
|
||
{
|
||
inputFiles.Clear();
|
||
lstFiles.Items.Clear();
|
||
inputFiles.Add(ofd.FileName);
|
||
lstFiles.Items.Add(Path.GetFileName(ofd.FileName));
|
||
isPdfInput = true;
|
||
lblStatus.Text = string.Format("已选择: {0}", Path.GetFileName(ofd.FileName));
|
||
}
|
||
}
|
||
|
||
private void BtnProcess_Click(object sender, EventArgs e)
|
||
{
|
||
if (inputFiles.Count == 0)
|
||
{
|
||
MessageBox.Show("请先添加文件!", "提示");
|
||
return;
|
||
}
|
||
|
||
SaveFileDialog sfd = new SaveFileDialog();
|
||
sfd.Title = "保存处理后的PDF";
|
||
sfd.Filter = "PDF|*.pdf";
|
||
sfd.FileName = "scanned_output.pdf";
|
||
if (sfd.ShowDialog() != DialogResult.OK) return;
|
||
|
||
string outputPath = sfd.FileName;
|
||
int quality = (int)nudQuality.Value;
|
||
SetUI(false);
|
||
|
||
try
|
||
{
|
||
// 加载源图像
|
||
List<Bitmap> sourceImages = new List<Bitmap>();
|
||
if (isPdfInput)
|
||
{
|
||
lblStatus.Text = "正在从PDF提取图像...";
|
||
Application.DoEvents();
|
||
sourceImages = PdfHelper.ExtractImagesFromPdf(inputFiles[0]);
|
||
}
|
||
else
|
||
{
|
||
for (int i = 0; i < inputFiles.Count; i++)
|
||
{
|
||
lblStatus.Text = string.Format("加载 {0}/{1}...", i + 1, inputFiles.Count);
|
||
Application.DoEvents();
|
||
sourceImages.Add(new Bitmap(inputFiles[i]));
|
||
}
|
||
}
|
||
|
||
progressBar.Maximum = sourceImages.Count;
|
||
progressBar.Value = 0;
|
||
|
||
// 逐张处理
|
||
List<Bitmap> results = new List<Bitmap>();
|
||
for (int i = 0; i < sourceImages.Count; i++)
|
||
{
|
||
lblStatus.Text = string.Format("处理第 {0}/{1} 页(矫正+增强)...", i + 1, sourceImages.Count);
|
||
Application.DoEvents();
|
||
|
||
// Bitmap → 内存PNG → Mat → 处理 → Mat → 内存PNG → Bitmap
|
||
Mat mat = BitmapToMat(sourceImages[i]);
|
||
Mat processed = DocumentScanner.ProcessImage(mat);
|
||
Bitmap resultBmp = MatToBitmap(processed);
|
||
results.Add(resultBmp);
|
||
|
||
// 预览
|
||
if (picPreview.Image != null) picPreview.Image.Dispose();
|
||
picPreview.Image = new Bitmap(resultBmp,
|
||
new System.Drawing.Size(picPreview.Width, picPreview.Height));
|
||
Application.DoEvents();
|
||
|
||
mat.Dispose();
|
||
processed.Dispose();
|
||
progressBar.Value = i + 1;
|
||
}
|
||
|
||
// 生成PDF
|
||
lblStatus.Text = "正在生成PDF...";
|
||
Application.DoEvents();
|
||
PdfHelper.SaveImagesToPdf(results, outputPath, quality);
|
||
|
||
// 清理
|
||
foreach (Bitmap b in sourceImages) b.Dispose();
|
||
foreach (Bitmap b in results) b.Dispose();
|
||
|
||
lblStatus.Text = string.Format("完成!已保存: {0}", Path.GetFileName(outputPath));
|
||
|
||
DialogResult dr = MessageBox.Show(
|
||
string.Format("处理完成!共 {0} 页。\n\n是否打开所在目录?", results.Count),
|
||
"完成", MessageBoxButtons.YesNo, MessageBoxIcon.Information);
|
||
if (dr == DialogResult.Yes)
|
||
{
|
||
System.Diagnostics.Process.Start("explorer.exe", "/select," + outputPath);
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
MessageBox.Show("出错: " + ex.Message, "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||
lblStatus.Text = "失败: " + ex.Message;
|
||
}
|
||
finally
|
||
{
|
||
SetUI(true);
|
||
}
|
||
}
|
||
|
||
private void SetUI(bool enabled)
|
||
{
|
||
btnAddImages.Enabled = enabled;
|
||
btnAddPdf.Enabled = enabled;
|
||
btnRemove.Enabled = enabled;
|
||
btnClear.Enabled = enabled;
|
||
btnMoveUp.Enabled = enabled;
|
||
btnMoveDown.Enabled = enabled;
|
||
btnProcess.Enabled = enabled;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Bitmap → Mat(通过内存PNG中转,绕开 System.Drawing.Common 类型冲突)
|
||
/// </summary>
|
||
private static Mat BitmapToMat(Bitmap bmp)
|
||
{
|
||
using (MemoryStream ms = new MemoryStream())
|
||
{
|
||
bmp.Save(ms, System.Drawing.Imaging.ImageFormat.Png);
|
||
byte[] buf = ms.ToArray();
|
||
return Cv2.ImDecode(buf, ImreadModes.Color);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Mat → Bitmap(通过内存PNG中转)
|
||
/// </summary>
|
||
private static Bitmap MatToBitmap(Mat mat)
|
||
{
|
||
byte[] buf;
|
||
Cv2.ImEncode(".png", mat, out buf);
|
||
using (MemoryStream ms = new MemoryStream(buf))
|
||
{
|
||
return new Bitmap(ms);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Quicker 入口 - 运行线程选择: 后台线程(STA)
|
||
/// </summary>
|
||
public static void Exec(Quicker.Public.IStepContext context)
|
||
{
|
||
// 在调用任何 OpenCvSharp 方法之前,手动加载原生 DLL
|
||
// 从 OpenCvSharp.dll 所在目录查找 OpenCvSharpExtern.dll
|
||
try
|
||
{
|
||
string asmPath = typeof(Cv2).Assembly.Location;
|
||
string asmDir = Path.GetDirectoryName(asmPath);
|
||
string externPath = Path.Combine(asmDir, "OpenCvSharpExtern.dll");
|
||
|
||
if (File.Exists(externPath))
|
||
{
|
||
LoadLibrary(externPath);
|
||
}
|
||
else
|
||
{
|
||
// 尝试 runtimes 子目录(NuGet 标准结构)
|
||
string runtimePath = Path.Combine(asmDir, "runtimes", "win-x64", "native", "OpenCvSharpExtern.dll");
|
||
if (File.Exists(runtimePath))
|
||
{
|
||
LoadLibrary(runtimePath);
|
||
}
|
||
}
|
||
}
|
||
catch { }
|
||
|
||
Application.EnableVisualStyles();
|
||
Application.SetCompatibleTextRenderingDefault(false);
|
||
Application.Run(new ScannerForm());
|
||
}
|
||
|
||
[DllImport("kernel32.dll", SetLastError = true)]
|
||
private static extern IntPtr LoadLibrary(string dllToLoad);
|