Files
camscanner/CamScanner.cs
尘曲 a65c86bb24 改进阴影检测:用原始灰度值判断而非仅依赖背景估计
背景估计受大核模糊影响,阴影处的值可能被周围亮区域拉高。
改为同时检测原始灰度和背景估计,低于中位数45%/50%的区域
识别为阴影,并用渐变过渡避免硬边。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 16:36:56 +08:00

1088 lines
38 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[] grayHist = new int[256];
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];
if (ink < 0) ink = 0;
// 归一化到 0-1
double inkNorm = (double)ink / ink95;
if (inkNorm > 1.0) inkNorm = 1.0;
// 非线性加深pow(x, 0.45) 让浅墨迹也变深
// 0.45 < 1 所以小值被放大(浅色文字变深)
double darkness = Math.Pow(inkNorm, 0.45);
// 映射到灰度0=白(255), 1=黑(0)
int val = (int)(255.0 * (1.0 - darkness));
if (val < 0) val = 0;
if (val > 255) val = 255;
resultData[i] = (byte)val;
}
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);