C# OpenCVSharp使用 读光-票证检测矫正模型

先看看官网模型介绍👇

读光-票证检测矫正模型 · 模型库

票证检测矫正模型介绍

【读光商用矫正模型开源,快来体验吧】票证检测矫正模型在实际生活中有着广泛的需求,例如信息抽取、图像质量判断、证件扫描、票据审计等领等场景,可以大幅提高工作效率和准确性。本次 读光团队 开源了商用票证检测矫正模型,基于海量的真实数据训练,可以从容应对多种复杂场景的票证检测矫正任务,该模型具有以下优点:

  • 支持任意角度、多卡证票据等混贴场景,同时检测输入图像任意角度的多个子图区域;
  • 基于海量真实数据训练,效果满足国内常见的卡证票据的检测矫正需求;
  • 支持子图区域复印件判断、四方向判断,准确率高达 99%;
  • 矫正效果、推理速度远高于 modelScope 同类模型,详见本文测试报告。

模型效果评测

卡证场景

评测方式 det_precison det_recall det_f-score 方向判别 复印件判别 推理速度(FPS)
卡证检测矫正模型 99.63% 96.80% 98.19% 无此能力 无此能力 1.83
读光-票证检测矫正模型 99.64% 99.85% 99.75% 99.78% 99.70% 14.30

备注:1)私有测试集,该测试集包含身份证、银行卡、驾驶证、行驶证等常见的卡证数据共计 1200 张;2)测试的速度均不包含后处理的图像透视变换;测试使用 GPU 型号:Tesla A100-PCIE-80GB

票据场景

评测方式 det_precison det_recall det_f-score 方向判别 复印件判别 推理速度(FPS)
卡证检测矫正模型 98.90% 82.43% 89.92% 无此能力 无此能力 1.79
读光-票证检测矫正模型 99.92% 100.00% 99.96% 99.83% 99.17% 12.55

备注:1)私有测试集,该测试集包含营业执照、增值税发票、机动车销售发票、出生证明等常见的票据和资质数据共计 1200 张;2)测试的速度均不包含后处理的图像透视变换;测试使用 GPU 型号:Tesla A100-PCIE-80GB

模型描述

下图是实现流程:输入图片,基于 Resnet18-FPN 提取特征后,在 1/4 尺寸处通过三条分支分别识别出票证的中心点、偏移量(中心点到4个顶点距离)、中心点偏移量(为了得到精准的中心点),即可解码数出票证区域的四边形框;再用透视变换将票证拉平得到矫正后的票证信息;与此同时,分类分支识别出子图朝向,用于而切割的子图转正。

效果展示

下图是模型效果:

OpenCVSharp实现👇

全部代码如下:

///CardDetector.cs

using System;

using System.Collections.Generic;

using System.Drawing;

using System.Linq;

using System.Windows.Forms;

using OpenCvSharp;

using OpenCvSharp.Dnn;

using Size = OpenCvSharp.Size;

namespace CardCorrection

{

public class CardDetector

{

private Net model;

private string[] outputNames;

// 模型参数

private const int InputSize = 768;

private const int OutputSize = 192;

private const float ScoreThreshold = 0.5f;

private const int MaxDetections = 10;

// 预处理参数

private readonly Scalar mean = new Scalar(0.408, 0.447, 0.470);

private readonly Scalar std = new Scalar(0.289, 0.274, 0.278);

public CardDetector(string modelPath)

{

model = CvDnn.ReadNetFromOnnx(modelPath);

outputNames = model.GetUnconnectedOutLayersNames();

Console.WriteLine($"模型加载成功,输出层: {string.Join(", ", outputNames)}");

}

public List<CardDetection> Detect(Mat image)

{

// 预处理

Mat blob = Preprocess(image);

// 推理

model.SetInput(blob);

Mat[] outputs = outputNames.Select(_=>new Mat()).ToArray();

model.Forward(outputs, outputNames);

// 后处理

var detections = Postprocess(outputs, image);

// 清理资源

blob.Dispose();

foreach (var output in outputs)

{

output.Dispose();

}

return detections;

}

private Mat Preprocess(Mat image)

{

// 调整大小并填充

Mat resized = new Mat();

Cv2.Resize(image, resized, new Size(InputSize, InputSize));

// 归一化

Mat normalized = new Mat();

resized.ConvertTo(normalized, MatType.CV_32F, 1.0 / 255.0);

// 减去均值除以标准差

Mat[] channels = new Mat[3];

Cv2.Split(normalized, out channels);

for (int i = 0; i < 3; i++)

{

channels[i] = (channels[i] - mean.Val0) / std.Val0;

}

Cv2.Merge(channels, normalized);

// 创建blob

Mat blob = CvDnn.BlobFromImage(normalized);

// 清理

resized.Dispose();

normalized.Dispose();

foreach (var channel in channels)

{

channel.Dispose();

}

return blob;

}

private List<CardDetection> Postprocess(Mat[] outputs, Mat originalImage)

{

// 解析输出

// outputs[4]: heatmap [1,1,192,192] - 目标热力图

// outputs[0]: angle_cls [1,4,192,192] - 角度分类

// outputs[1]: ftype_cls [1,2,192,192] - 类型分类

// outputs[2]: wh [1,8,192,192] - 边界框坐标

// outputs[3]: reg [1,2,192,192] - 回归偏移

Mat heatmap = outputs[4]; // 热力图

Mat angleCls = outputs[0]; // 角度分类

Mat ftypeCls = outputs[1]; // 类型分类

Mat wh = outputs[2]; // 边界框

Mat reg = outputs[3]; // 回归偏移

// 应用sigmoid激活

Mat heatmapSigmoid = Sigmoid(heatmap);

Mat angleClsSigmoid = Sigmoid(angleCls);

Mat ftypeClsSigmoid = Sigmoid(ftypeCls);

// 找到热力图中的峰值点(局部最大值)

var peaks = FindPeaks(heatmapSigmoid, ScoreThreshold, MaxDetections);

List<CardDetection> detections = new List<CardDetection>();

foreach (var peak in peaks)

{

// 获取该位置的所有信息

var detection = ProcessDetection(peak, heatmapSigmoid, angleClsSigmoid,

ftypeClsSigmoid, wh, reg, originalImage);

if (detection != null)

{

detections.Add(detection);

}

}

// 清理临时矩阵

heatmapSigmoid.Dispose();

angleClsSigmoid.Dispose();

ftypeClsSigmoid.Dispose();

return detections;

}

private List<Peak> FindPeaks(Mat heatmap, float threshold, int maxPeaks)

{

List<Peak> peaks = new List<Peak>();

// 将热力图转换为二维

Mat heatmap2D = heatmap.Reshape(0, new int[] { OutputSize, OutputSize });

// 使用非极大值抑制找到局部最大值

Mat dilated = new Mat();

Cv2.Dilate(heatmap2D, dilated, Mat.Ones(3, 3, MatType.CV_8UC1));

Mat localMaxima = new Mat();

Cv2.Compare(heatmap2D, dilated, localMaxima, CmpType.EQ);

// 收集满足阈值的峰值点

for (int y = 0; y < OutputSize; y++)

{

for (int x = 0; x < OutputSize; x++)

{

if (localMaxima.At<byte>(y, x) != 0 &&

heatmap2D.At<float>(y, x) > threshold)

{

peaks.Add(new Peak

{

X = x,

Y = y,

Score = heatmap2D.At<float>(y, x)

});

}

}

}

// 按分数排序并取前maxPeaks个

peaks = peaks.OrderByDescending(p => p.Score)

.Take(maxPeaks)

.ToList();

heatmap2D.Dispose();

dilated.Dispose();

localMaxima.Dispose();

Console.WriteLine($"找到 {peaks.Count} 个检测点");

return peaks;

}

private CardDetection ProcessDetection(Peak peak, Mat heatmap, Mat angleCls,

Mat ftypeCls, Mat wh, Mat reg, Mat originalImage)

{

try

{

// 1. 应用回归偏移修正中心点

float regX = GetValueAt(reg, 0, 0, peak.Y, peak.X);

float regY = GetValueAt(reg, 0, 1, peak.Y, peak.X);

float centerX = peak.X + regX;

float centerY = peak.Y + regY;

// 2. 获取边界框坐标(相对于中心点的偏移)

float[] bboxOffsets = new float[8];

for (int i = 0; i < 8; i++)

{

bboxOffsets[i] = GetValueAt(wh, 0, i, peak.Y, peak.X);

}

// 3. 计算实际边界框坐标

Point2f[] corners = new Point2f[4];

for (int i = 0; i < 4; i++)

{

float x = centerX - bboxOffsets[i * 2];

float y = centerY - bboxOffsets[i * 2 + 1];

corners[i] = new Point2f(x, y);

}

// 4. 获取分类信息

int angleClass = GetMaxClass(angleCls, peak.Y, peak.X);

int typeClass = GetMaxClass(ftypeCls, peak.Y, peak.X);

// 5. 将坐标从特征图空间转换到原始图像空间

Point2f[] originalCorners = TransformToOriginalImage(corners, originalImage);

// 6. 裁剪并旋转图像

Mat cropped = CropAndRotateCard(originalImage, originalCorners, angleClass);

return new CardDetection

{

Score = peak.Score,

Corners = originalCorners,

AngleClass = angleClass,

TypeClass = typeClass,

Center = new Point2f(centerX * 4, centerY * 4), // 乘以4因为下采样

CroppedImage = cropped

};

}

catch (Exception ex)

{

Console.WriteLine($"处理检测点时出错: {ex.Message}");

return null;

}

}

private Point2f[] TransformToOriginalImage(Point2f[] corners, Mat originalImage)

{

// 从192x192特征图坐标转换到768x768预处理图像坐标

float scaleX = (float)originalImage.Width / InputSize;

float scaleY = (float)originalImage.Height / InputSize;

Point2f[] result = new Point2f[4];

for (int i = 0; i < 4; i++)

{

// 首先乘以4(从192到768),然后缩放到原始图像尺寸

float x = corners[i].X * 4 * scaleX;

float y = corners[i].Y * 4 * scaleY;

// 确保坐标在图像范围内

x = Math.Max(0, Math.Min(originalImage.Width - 1, x));

y = Math.Max(0, Math.Min(originalImage.Height - 1, y));

result[i] = new Point2f(x, y);

}

return result;

}

private Mat CropAndRotateCard(Mat image, Point2f[] corners, int angleClass)

{

if (corners.Length != 4)

return new Mat();

// 计算裁剪后的尺寸

float width = Math.Max(

Distance(corners[0], corners[1]),

Distance(corners[2], corners[3])

);

float height = Math.Max(

Distance(corners[0], corners[3]),

Distance(corners[1], corners[2])

);

// 目标点(矩形)

Point2f[] dstPoints = {

new Point2f(0, 0),

new Point2f(width - 1, 0),

new Point2f(width - 1, height - 1),

new Point2f(0, height - 1)

};

// 透视变换

Mat transform = Cv2.GetPerspectiveTransform(corners, dstPoints);

Mat cropped = new Mat();

Cv2.WarpPerspective(image, cropped, transform, new Size((int)width, (int)height));

// 根据角度分类进行旋转

RotateFlags rotateFlag = GetRotateFlag(angleClass);

if (rotateFlag != RotateFlags.Rotate90Clockwise) // 默认不需要旋转

{

Mat rotated = new Mat();

Cv2.Rotate(cropped, rotated, rotateFlag);

cropped.Dispose();

cropped = rotated;

}

transform.Dispose();

return cropped;

}

// 辅助方法

private float GetValueAt(Mat mat, int batch, int channel, int y, int x)

{

// 直接从4D张量中获取值 [batch, channel, height, width]

return mat.At<float>(batch, channel, y, x);

}

private int GetMaxClass(Mat clsMap, int y, int x)

{

int numClasses = clsMap.Size(1);

float maxScore = -1;

int maxClass = 0;

for (int c = 0; c < numClasses; c++)

{

float score = GetValueAt(clsMap, 0, c, y, x);

if (score > maxScore)

{

maxScore = score;

maxClass = c;

}

}

return maxClass;

}

private Mat Sigmoid(Mat x)

{

Mat result = new Mat();

Cv2.Exp(-x, result);

result = 1.0 / (1 + result);

return result;

}

private float Distance(Point2f p1, Point2f p2)

{

return (float)Math.Sqrt(Math.Pow(p1.X - p2.X, 2) + Math.Pow(p1.Y - p2.Y, 2));

}

private RotateFlags GetRotateFlag(int angleClass)

{

switch (angleClass)

{

case 1: return RotateFlags.Rotate180;

case 2: return RotateFlags.Rotate90Counterclockwise;

case 3: return RotateFlags.Rotate90Clockwise;

default: return RotateFlags.Rotate90Clockwise; // 0度,但OpenCV没有0度旋转标志

}

}

}

// 辅助类

public class Peak

{

public int X { get; set; }

public int Y { get; set; }

public float Score { get; set; }

}

public class CardDetection

{

public float Score { get; set; }

public Point2f[] Corners { get; set; }

public int AngleClass { get; set; }

public int TypeClass { get; set; }

public Point2f Center { get; set; }

public Mat CroppedImage { get; set; }

public Rectangle GetBoundingBox()

{

if (Corners == null || Corners.Length == 0)

return new Rectangle();

float minX = Corners.Min(p => p.X);

float minY = Corners.Min(p => p.Y);

float maxX = Corners.Max(p => p.X);

float maxY = Corners.Max(p => p.Y);

return new Rectangle((int)minX, (int)minY, (int)(maxX - minX), (int)(maxY - minY));

}

}

}

///Visualization.cs

using System;

using System.Collections.Generic;

using System.Linq;

using OpenCvSharp;

namespace CardCorrection

{

public static class Visualization

{

public static void DrawDetections(Mat image, List<CardDetection> detections, string outputPath)

{

Mat result = image.Clone();

Random rnd = new Random();

foreach (var detection in detections)

{

// 随机颜色

Scalar color = new Scalar(rnd.Next(0, 255), rnd.Next(0, 255), rnd.Next(0, 255));

// 绘制边界框

Point[] points = detection.Corners.Select(p => new Point((int)p.X, (int)p.Y)).ToArray();

Cv2.Polylines(result, new Point[][] { points }, true, color, 3);

// 绘制中心点

Cv2.Circle(result, new Point((int)detection.Center.X, (int)detection.Center.Y), 8, color, -1);

// 绘制分数和角度信息

string info = $"Score: {detection.Score:F2}, Angle: {detection.AngleClass}";

Cv2.PutText(result, info,

new Point((int)detection.Center.X + 10, (int)detection.Center.Y - 10),

HersheyFonts.HersheySimplex, 0.7, color, 2);

}

Cv2.ImWrite(outputPath, result);

result.Dispose();

Console.WriteLine($"检测结果已保存: {outputPath}");

}

public static void SaveCroppedImages(List<CardDetection> detections, string prefix = "card")

{

for (int i = 0; i < detections.Count; i++)

{

if (!detections[i].CroppedImage.Empty())

{

string path = $"{prefix}_{i:00}.jpg";

Cv2.ImWrite(path, detections[i].CroppedImage);

Console.WriteLine($"裁剪图像已保存: {path} ({detections[i].CroppedImage.Width}x{detections[i].CroppedImage.Height})");

Cv2.ImShow("CroppedImage"+i, detections[i].CroppedImage);

}

}

}

public static void CreateDetectionReport(Mat originalImage, List<CardDetection> detections, string outputPath)

{

if (detections.Count == 0)

{

Console.WriteLine("没有检测到卡片,无法生成报告");

return;

}

// 创建水平拼接的图像

List<Mat> images = new List<Mat> { originalImage.Clone() };

images.AddRange(detections.Select(d => d.CroppedImage));

// 调整所有图像到相同高度

int targetHeight = images.Min(img => img.Height);

List<Mat> resizedImages = new List<Mat>();

int totalWidth = 0;

foreach (var img in images)

{

float aspect = (float)img.Width / img.Height;

int newWidth = (int)(targetHeight * aspect);

Mat resized = new Mat();

Cv2.Resize(img, resized, new Size(newWidth, targetHeight));

resizedImages.Add(resized);

totalWidth += newWidth;

}

// 创建拼接图像

Mat report = new Mat(targetHeight, totalWidth, MatType.CV_8UC3, Scalar.Black);

int xOffset = 0;

for (int i = 0; i < resizedImages.Count; i++)

{

Mat roi = report[new Rect(xOffset, 0, resizedImages[i].Width, resizedImages[i].Height)];

resizedImages[i].CopyTo(roi);

// 添加标题

string title = i == 0 ? "Original" : $"Card {i - 1}";

Cv2.PutText(report, title, new Point(xOffset + 10, 30),

HersheyFonts.HersheySimplex, 1.0, Scalar.White, 2);

xOffset += resizedImages[i].Width;

resizedImages[i].Dispose();

}

Cv2.ImWrite(outputPath, report);

report.Dispose();

Console.WriteLine($"检测报告已保存: {outputPath}");

}

}

}

使用方法:

static void Main6()

{

string imagePath = "D:\\cv_resnet18_card_correction-opencv-dnn-main\\cv_resnet18_card_correction-opencv-dnn-main\\testimgs\\demo.jpg";

string modelPath = "D:\\学习\\OPENCV\\OPENCV-ZXING\\OPENCV-BLOB\\bin\\x64\\Debug\\model\\cv_resnet18_card_correction.onnx";

try

{

Console.WriteLine("卡片检测与校正程序");

Console.WriteLine("==================");

// 1. 加载图像

Console.WriteLine($"加载图像: {imagePath}");

Mat image = Cv2.ImRead(imagePath);

if (image.Empty())

{

Console.WriteLine("无法加载图像");

return;

}

Console.WriteLine($"图像尺寸: {image.Width}x{image.Height}");

// 2. 加载模型

Console.WriteLine("加载检测模型...");

CardDetector detector = new CardDetector(modelPath);

// 3. 执行检测

Console.WriteLine("开始检测...");

List<CardDetection> detections = detector.Detect(image);

// 4. 显示结果

Console.WriteLine($"检测到 {detections.Count} 张卡片");

for (int i = 0; i < detections.Count; i++)

{

var det = detections[i];

Console.WriteLine($"卡片 {i}: 置信度={det.Score:F3}, 角度={det.AngleClass}, 类型={det.TypeClass}");

}

// 5. 可视化结果

Console.WriteLine("生成可视化结果...");

CardCorrection.Visualization.DrawDetections(image, detections, "detection_result.jpg");

CardCorrection.Visualization.SaveCroppedImages(detections, "cropped_card");

CardCorrection.Visualization.CreateDetectionReport(image, detections, "detection_report.jpg");

Console.WriteLine("处理完成!");

// 清理资源

image.Dispose();

foreach (var detection in detections)

{

detection.CroppedImage?.Dispose();

}

}

catch (Exception ex)

{

Console.WriteLine($"错误: {ex.Message}");

Console.WriteLine($"堆栈跟踪: {ex.StackTrace}");

}

}

模型链接及代码参考:
GitHub - hpc203/cv_resnet18_card_correction-opencv-dnn: 使用opencv部署读光-票证检测矫正模型,包含C++和Python两个版本的程序,只依赖opencv库就能运行

相关推荐
霜绛2 小时前
C#知识补充(二)——命名空间、泛型、委托和事件
开发语言·学习·unity·c#
大千AI助手2 小时前
HotpotQA:推动多跳推理问答发展的标杆数据集
人工智能·神经网络·llm·qa·大千ai助手·hotpotqa·多跳推理能力
红尘炼丹客2 小时前
《DeepSeek-OCR: Contexts Optical Compression》速览
人工智能·python·自然语言处理·ocr
TiAmo zhang2 小时前
现代C++的AI革命:C++20/C++23核心特性解析与实战应用
c++·人工智能·c++20
好望角雾眠2 小时前
第四阶段C#通讯开发-6:Socket之UDP
开发语言·笔记·学习·udp·c#
mwq301232 小时前
从傅里叶变换到 RoPE:解构位置编码的数学灵魂
人工智能
深圳佛手3 小时前
AI 编程工具Claude Code 介绍
人工智能·python·机器学习·langchain
沃达德软件3 小时前
智能识别车辆驾驶人特征
人工智能·目标检测·计算机视觉·目标跟踪·视觉检测