Files
camscanner/CamScanner.cs
尘曲 f17fd5cb85 新增条带状折痕检测:按列/行投影识别窄条暗带
深色窄折痕的边缘方差高(亮暗跳变),双级方差检测不到。
新增条带检测:按列/行统计暗像素比例,超过40%的列/行
标记为折痕条带,条带内低方差像素推白,高方差像素(文字)保留。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 17:06:01 +08:00

1242 lines
44 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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;
}
// 方差高 → 是文字,保留不动
}
}
// --- 边缘阴影修复:从边缘向内扫描连续暗像素 ---
byte[] shadowFlag = new byte[w * h];
int darkThresh = (int)(paperBright * 0.55);
int maxScanDepth = Math.Max(w, h) / 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);
// --- 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);