当前位置: 首页 > news >正文

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

先看看官网模型介绍👇

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

票证检测矫正模型介绍

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

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

模型效果评测

卡证场景

评测方式det_precisondet_recalldet_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_precisondet_recalldet_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个顶点距离)、中心点偏移量(为了得到精准的中心点),即可解码数出票证区域的四边形框;再用透视变换将票证拉平得到矫正后的票证信息;与此同时,分类分支识别出子图朝向,用于而切割的子图转正。

pipeline

效果展示

下图是模型效果:

pipeline_vis

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库就能运行

http://www.dtcms.com/a/586406.html

相关文章:

  • 更换空间对网站的影响开发app需要什么设备
  • 佛山网站制作公司沈阳网页设计招聘
  • 南宁比优建站wordpress管理工具栏
  • 企业如何建设自己的网站网站主页跳转index
  • 网站利用百度离线地图淘宝电脑版网页
  • 工业控制网关物联网解决方案软硬件定制:我是你的研发部
  • 第2章 传感器技术与数据处理
  • 【开题答辩全过程】以 房地产销售管理系统为例,包含答辩的问题和答案
  • 建设项目环评验收网站建设行业门户网站需要什么条件
  • maven常用的命令
  • 动态商务网站开发与管理合肥网站建设工作室
  • 设计的网站都有哪些内容新榜数据平台
  • 扶绥县住房和城乡建设局网站品牌网站建设精湛磐石网络
  • MCU 的SPI 关键部分配置注意事项(SPI多机通信时)
  • 付网站建设费如果做账wordpress修改版面
  • 网站建设的实训心得 500字新城建站
  • Linux网络-Socket 编程 UDP
  • Rust编程进阶 - 如何基于生成器设计一套协程(Coroutine)的方案, 从而方便编写大规模高性能异步程序
  • LangChain 中 ChatPromptTemplate 的几种使用方式
  • 怎么创建企业网站同源大厦 网站建设
  • 3网合一网站天眼企业查询系统官网
  • 网站建设人工智能开发怎样建个人网页免费
  • 1.2.3AOP的底层原理
  • Android 屏幕旋转流程
  • 简述电子商务网站的建站流程佛山app开发公司
  • 精准突破 0.5mm 透明玻璃测量瓶颈 —— 泓川科技激光位移传感器的技术革新与成本优势
  • C++笔记-24-文件读写操作
  • 做网站需要会哪些计算机语言net源码的网站建设步骤
  • 做网站一般都是织梦网站 展示板
  • 设计配色推荐的网站广州深圳外贸公司