C# OnnxRuntime 实现车牌检测识别

目录

效果

模型信息

项目

代码

下载


效果

模型信息

car_plate_detect.onnx

Model Properties



Inputs


name:input

tensor:Float[1, 3, 640, 640]


Outputs


name:output

tensor:Float[1, 25200, 16]


plate_rec.onnx

Model Properties



Inputs


name:images

tensor:Float[1, 3, 48, 168]


Outputs


name:output

tensor:Float[1, 21, 78]


项目

代码

using Microsoft.ML.OnnxRuntime;

using Microsoft.ML.OnnxRuntime.Tensors;

using OpenCvSharp;

using System;

using System.Collections.Generic;

using System.Drawing;

using System.Drawing.Imaging;

using System.IO;

using System.Linq;

using System.Text;

using System.Windows.Forms;

namespace Onnx_Demo

{

public partial class Form1 : Form

{

public Form1()

{

InitializeComponent();

}

string fileFilter = "*.*|*.bmp;*.jpg;*.jpeg;*.tiff;*.tiff;*.png";

string image_path = "";

string startupPath;

DateTime dt1 = DateTime.Now;

DateTime dt2 = DateTime.Now;

Mat image;

Mat result_image;

// 车牌检测模型

private InferenceSession session_detect;

private string detect_model_path;

// 车牌识别模型

private InferenceSession session_rec;

private string rec_model_path;

private const float MEAN_VALUE = 0.588f;

private const float STD_VALUE = 0.193f;

private readonly string PLATE_CHARS ="#京沪津渝冀晋蒙辽吉黑苏浙皖闽赣鲁豫鄂湘粤桂琼川贵云藏陕甘青宁新学警港澳挂使领民航危0123456789ABCDEFGHJKLMNPQRSTUVWXYZ险品";

private void button1_Click(object sender, EventArgs e)

{

OpenFileDialog ofd = new OpenFileDialog();

ofd.Filter = fileFilter;

ofd.InitialDirectory = System.IO.Path.Combine(System.Windows.Forms.Application.StartupPath, "test_img");

if (ofd.ShowDialog() != DialogResult.OK) return;

pictureBox1.Image = null;

image_path = ofd.FileName;

pictureBox1.Image = new Bitmap(image_path);

textBox1.Text = "";

image = new Mat(image_path);

pictureBox2.Image = null;

}

private void button2_Click(object sender, EventArgs e)

{

if (string.IsNullOrEmpty(image_path))

return;

button2.Enabled = false;

pictureBox2.Image = null;

textBox1.Text = "";

Application.DoEvents();

// 读取原图

Mat img0 = new Mat(image_path);

if (img0.Empty())

{

button2.Enabled = true;

return;

}

// 深拷贝一份用于绘制(BGR)

Mat drawImg = img0.Clone();

// ---------- 检测预处理 ----------

Mat blob;

float ratio;

int padLeft, padTop;

Tensor<float> inputTensor = PreprocessDetect(img0, 640, 640, out blob, out ratio, out padLeft, out padTop);

// ---------- 运行检测模型 ----------

dt1 = DateTime.Now;

var container = new List<NamedOnnxValue>

{

NamedOnnxValue.CreateFromTensor("input", inputTensor)

};

List<PlateResult> plateResults = new List<PlateResult>();

using (var results = session_detect.Run(container))

{

var outputTensor = results.First().AsTensor<float>();

dt2 = DateTime.Now;

// 后处理:解析检测框

List<float[]> detBoxes = PostprocessDetect(outputTensor, ratio, padLeft, padTop, 0.4f, 0.5f);

// 识别并收集结果

foreach (var box in detBoxes)

{

float x1 = box[0], y1 = box[1], x2 = box[2], y2 = box[3];

float score = box[4];

int label = (int)box[13];

// 提取四个角点

Point2f[] landmarks = new Point2f[4];

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

{

landmarks[k] = new Point2f(box[5 + 2 * k], box[6 + 2 * k]);

}

// 四点透视变换获取校正车牌图

Mat roiImg = FourPointTransform(img0, landmarks);

if (roiImg.Empty())

continue;

string plateType = label == 1 ? "双层" : "单层";

if (label == 1)

{

roiImg = SplitMerge(roiImg);

if (roiImg.Empty())

continue;

}

// 识别车牌文字

string plateText = RecognizeText(roiImg);

roiImg.Dispose();

plateResults.Add(new PlateResult

{

X1 = x1,

Y1 = y1,

X2 = x2,

Y2 = y2,

Score = score,

Plate = plateText,

Type = plateType

});

}

// ---- 构建文本框输出信息 ----

StringBuilder sb = new StringBuilder();

sb.AppendLine("推理耗时:" + (dt2 - dt1).TotalMilliseconds.ToString("F2") + "ms");

sb.AppendLine("检测到 " + plateResults.Count + " 个车牌:");

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

{

var pr = plateResults[i];

sb.AppendLine(string.Format("{0}. [{1}] {2} 置信度:{3:F2} 坐标:({4:F0},{5:F0},{6:F0},{7:F0})",

i + 1, pr.Plate, pr.Type, pr.Score, pr.X1, pr.Y1, pr.X2, pr.Y2));

}

textBox1.Text = sb.ToString();

// ---------- 绘制结果到 drawImg ----------

byte[] imgBytes;

Cv2.ImEncode(".png", drawImg, out imgBytes);

using (MemoryStream ms = new MemoryStream(imgBytes))

{

Bitmap bmp = (Bitmap)Image.FromStream(ms);

drawImg.Dispose(); // 释放已编码的 Mat

using (Graphics g = Graphics.FromImage(bmp))

{

foreach (var pr in plateResults)

{

int ix1 = (int)Math.Max(0, pr.X1);

int iy1 = (int)Math.Max(0, pr.Y1);

int ix2 = (int)Math.Min(bmp.Width - 1, pr.X2);

int iy2 = (int)Math.Min(bmp.Height - 1, pr.Y2);

if (ix2 <= ix1 || iy2 <= iy1) continue;

// 画红色矩形框

using (Pen pen = new Pen(Color.Red, 3))

{

g.DrawRectangle(pen, ix1, iy1, ix2 - ix1, iy2 - iy1);

}

// 绘制车牌文字(带背景色)

string text = string.Format("{0} ({1}) {2:F2}", pr.Plate, pr.Type, pr.Score);

Font font = new Font("Microsoft YaHei", 16, FontStyle.Bold);

SizeF textSize = g.MeasureString(text, font);

float textX = ix1;

float textY = iy1 - textSize.Height - 4;

if (textY < 0) textY = iy1 + 4;

using (Brush bgBrush = new SolidBrush(Color.Red))

{

g.FillRectangle(bgBrush, textX, textY, textSize.Width + 4, textSize.Height + 4);

}

using (Brush textBrush = new SolidBrush(Color.White))

{

g.DrawString(text, font, textBrush, textX + 2, textY + 2);

}

}

}

pictureBox2.Image = bmp; // bmp 已包含绘制的框和文字

}

// 释放预处理时创建的临时

blob.Dispose();

}

img0.Dispose();

button2.Enabled = true;

}

// ---------------------- 下面为所有辅助方法(Letterbox、检测预处理/后处理、NMS、四点变换等)---------------------

private (Mat boxed, float ratio, int left, int top) Letterbox(Mat img, int targetW, int targetH)

{

int height = img.Height, width = img.Width;

float ratio = Math.Min((float)targetH / height, (float)targetW / width);

int newW = (int)(width * ratio);

int newH = (int)(height * ratio);

int left = (targetW - newW) / 2;

int top = (targetH - newH) / 2;

int right = targetW - newW - left;

int bottom = targetH - newH - top;

Mat resized = img.Resize(new OpenCvSharp.Size(newW, newH));

Mat boxed = new Mat();

Cv2.CopyMakeBorder(resized, boxed, top, bottom, left, right, BorderTypes.Constant, new Scalar(114, 114, 114));

resized.Dispose();

return (boxed, ratio, left, top);

}

private Tensor<float> PreprocessDetect(Mat img, int targetW, int targetH,

out Mat blob, out float ratio, out int padLeft, out int padTop)

{

var (boxed, r, left, top) = Letterbox(img, targetW, targetH);

blob = boxed;

ratio = r;

padLeft = left;

padTop = top;

// 转换为 RGB,归一化

Mat rgb = new Mat();

Cv2.CvtColor(blob, rgb, ColorConversionCodes.BGR2RGB);

rgb.ConvertTo(rgb, MatType.CV_32FC3, 1.0 / 255.0);

int h = rgb.Height, w = rgb.Width;

var tensor = new DenseTensor<float>(new[] { 1, 3, h, w });

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

{

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

{

Vec3f vec = rgb.At<Vec3f>(y, x);

tensor[0, 0, y, x] = vec.Item0; // R

tensor[0, 1, y, x] = vec.Item1; // G

tensor[0, 2, y, x] = vec.Item2; // B

}

}

rgb.Dispose();

return tensor;

}

private List<float[]> PostprocessDetect(Tensor<float> output, float ratio, int left, int top,

float confThresh, float iouThresh)

{

int numAnchors = output.Dimensions[1];

int numValues = output.Dimensions[2]; // 16

float[] raw = output.ToArray();

List<float[]> candidateBoxes = new List<float[]>();

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

{

int offset = i * numValues;

float objConf = raw[offset + 4];

if (objConf <= confThresh) continue;

float cls0 = raw[offset + 13] * objConf;

float cls1 = raw[offset + 14] * objConf;

float maxScore = Math.Max(cls0, cls1);

if (maxScore < confThresh) continue;

int label = cls0 >= cls1 ? 0 : 1;

float cx = raw[offset];

float cy = raw[offset + 1];

float w = raw[offset + 2];

float h = raw[offset + 3];

float x1 = cx - w / 2;

float y1 = cy - h / 2;

float x2 = cx + w / 2;

float y2 = cy + h / 2;

float lx0 = raw[offset + 5], ly0 = raw[offset + 6];

float lx1 = raw[offset + 7], ly1 = raw[offset + 8];

float lx2 = raw[offset + 9], ly2 = raw[offset + 10];

float lx3 = raw[offset + 11], ly3 = raw[offset + 12];

candidateBoxes.Add(new float[] {

x1, y1, x2, y2, maxScore,

lx0, ly0, lx1, ly1, lx2, ly2, lx3, ly3,

(float)label

});

}

if (candidateBoxes.Count == 0)

return new List<float[]>();

List<float[]> nmsResult = Nms(candidateBoxes, iouThresh);

List<float[]> finalBoxes = new List<float[]>();

foreach (var box in nmsResult)

{

finalBoxes.Add(RestoreBox(box, ratio, left, top));

}

return finalBoxes;

}

private float ComputeIoU(float[] boxA, float[] boxB)

{

float ax1 = boxA[0], ay1 = boxA[1], ax2 = boxA[2], ay2 = boxA[3];

float bx1 = boxB[0], by1 = boxB[1], bx2 = boxB[2], by2 = boxB[3];

float interX1 = Math.Max(ax1, bx1);

float interY1 = Math.Max(ay1, by1);

float interX2 = Math.Min(ax2, bx2);

float interY2 = Math.Min(ay2, by2);

float interW = Math.Max(0, interX2 - interX1);

float interH = Math.Max(0, interY2 - interY1);

float interArea = interW * interH;

float areaA = (ax2 - ax1) * (ay2 - ay1);

float areaB = (bx2 - bx1) * (by2 - by1);

float union = areaA + areaB - interArea;

if (union <= 1e-6f) return 0;

return interArea / union;

}

private List<float[]> Nms(List<float[]> boxes, float iouThresh)

{

var sorted = boxes.OrderByDescending(b => b[4]).ToList();

var keep = new List<float[]>();

while (sorted.Count > 0)

{

var current = sorted[0];

keep.Add(current);

sorted.RemoveAt(0);

var remaining = new List<float[]>();

foreach (var b in sorted)

{

if (ComputeIoU(current, b) <= iouThresh)

remaining.Add(b);

}

sorted = remaining;

}

return keep;

}

private float[] RestoreBox(float[] box, float ratio, int left, int top)

{

float[] restored = new float[box.Length];

Array.Copy(box, restored, box.Length);

int[] xIndices = { 0, 2, 5, 7, 9, 11 };

int[] yIndices = { 1, 3, 6, 8, 10, 12 };

foreach (int idx in xIndices)

restored[idx] = (restored[idx] - left) / ratio;

foreach (int idx in yIndices)

restored[idx] = (restored[idx] - top) / ratio;

return restored;

}

private Point2f[] OrderPoints(Point2f[] pts)

{

if (pts.Length != 4)

throw new ArgumentException("需要四个点");

Point2f[] rect = new Point2f[4];

float[] sums = pts.Select(p => p.X + p.Y).ToArray();

float[] diffs = pts.Select(p => p.Y - p.X).ToArray();

rect[0] = pts[Array.IndexOf(sums, sums.Min())];

rect[2] = pts[Array.IndexOf(sums, sums.Max())];

rect[1] = pts[Array.IndexOf(diffs, diffs.Min())];

rect[3] = pts[Array.IndexOf(diffs, diffs.Max())];

return rect;

}

private Mat FourPointTransform(Mat image, Point2f[] pts)

{

Point2f[] rect = OrderPoints(pts);

Point2f tl = rect[0], tr = rect[1], br = rect[2], bl = rect[3];

float widthA = (float)Math.Sqrt(Math.Pow(br.X - bl.X, 2) + Math.Pow(br.Y - bl.Y, 2));

float widthB = (float)Math.Sqrt(Math.Pow(tr.X - tl.X, 2) + Math.Pow(tr.Y - tl.Y, 2));

int maxWidth = Math.Max((int)widthA, (int)widthB);

if (maxWidth < 1) maxWidth = 1;

float heightA = (float)Math.Sqrt(Math.Pow(tr.X - br.X, 2) + Math.Pow(tr.Y - br.Y, 2));

float heightB = (float)Math.Sqrt(Math.Pow(tl.X - bl.X, 2) + Math.Pow(tl.Y - bl.Y, 2));

int maxHeight = Math.Max((int)heightA, (int)heightB);

if (maxHeight < 1) maxHeight = 1;

Point2f[] dst = new Point2f[]

{

new Point2f(0, 0),

new Point2f(maxWidth - 1, 0),

new Point2f(maxWidth - 1, maxHeight - 1),

new Point2f(0, maxHeight - 1)

};

Mat matrix = Cv2.GetPerspectiveTransform(rect, dst);

Mat warped = new Mat();

Cv2.WarpPerspective(image, warped, matrix, new OpenCvSharp.Size(maxWidth, maxHeight));

return warped;

}

private Mat SplitMerge(Mat img)

{

int height = img.Height;

int width = img.Width;

int upperHeight = (int)(5f / 12f * height);

int lowerStart = (int)(1f / 3f * height);

if (upperHeight <= 0 || lowerStart >= height) return img.Clone();

Mat upper = img[0, upperHeight, 0, width];

Mat lower = img[lowerStart, height, 0, width];

if (lower.Empty() || upper.Empty()) return img.Clone();

Mat upperResized = upper.Resize(new OpenCvSharp.Size(lower.Width, lower.Height));

Mat merged = new Mat();

Cv2.HConcat(new Mat[] { upperResized, lower }, merged);

upper.Dispose(); lower.Dispose(); upperResized.Dispose();

return merged;

}

private string RecognizeText(Mat roiImg)

{

Mat resized = roiImg.Resize(new OpenCvSharp.Size(168, 48));

Mat floatImg = new Mat();

resized.ConvertTo(floatImg, MatType.CV_32FC3, 1.0 / 255.0);

floatImg = (floatImg - MEAN_VALUE) / STD_VALUE;

int h = 48, w = 168;

var inputTensor = new DenseTensor<float>(new[] { 1, 3, h, w });

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

{

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

{

Vec3f vec = floatImg.At<Vec3f>(y, x);

// 识别模型输入通道顺序与训练一致,此处保持 BGR

inputTensor[0, 0, y, x] = vec.Item0; // B

inputTensor[0, 1, y, x] = vec.Item1; // G

inputTensor[0, 2, y, x] = vec.Item2; // R

}

}

floatImg.Dispose();

resized.Dispose();

var container = new List<NamedOnnxValue>

{

NamedOnnxValue.CreateFromTensor("images", inputTensor)

};

using (var results = session_rec.Run(container))

{

var output = results.First().AsTensor<float>();

int seqLen = output.Dimensions[1];

int numClasses = output.Dimensions[2];

int[] predIndices = new int[seqLen];

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

{

int maxIdx = 0;

float maxVal = output[0, i, 0];

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

{

float val = output[0, i, c];

if (val > maxVal)

{

maxVal = val;

maxIdx = c;

}

}

predIndices[i] = maxIdx;

}

return DecodePlate(predIndices);

}

}

private string DecodePlate(int[] preds)

{

int previous = 0;

StringBuilder sb = new StringBuilder();

foreach (int idx in preds)

{

if (idx != 0 && idx != previous && idx < PLATE_CHARS.Length)

{

sb.Append(PLATE_CHARS[idx]);

}

previous = idx;

}

return sb.ToString();

}

private class PlateResult

{

public float X1, Y1, X2, Y2;

public float Score;

public string Plate;

public string Type;

}

private void button3_Click(object sender, EventArgs e)

{

if (pictureBox2.Image == null)

return;

Bitmap output = new Bitmap(pictureBox2.Image);

SaveFileDialog sdf = new SaveFileDialog();

sdf.Title = "保存";

sdf.Filter = "Images (*.jpg)|*.jpg|Images (*.png)|*.png|Images (*.bmp)|*.bmp|Images (*.emf)|*.emf|Images (*.exif)|*.exif|Images (*.gif)|*.gif|Images (*.ico)|*.ico|Images (*.tiff)|*.tiff|Images (*.wmf)|*.wmf";

if (sdf.ShowDialog() == DialogResult.OK)

{

switch (sdf.FilterIndex)

{

case 1: output.Save(sdf.FileName, ImageFormat.Jpeg); break;

case 2: output.Save(sdf.FileName, ImageFormat.Png); break;

case 3: output.Save(sdf.FileName, ImageFormat.Bmp); break;

case 4: output.Save(sdf.FileName, ImageFormat.Emf); break;

case 5: output.Save(sdf.FileName, ImageFormat.Exif); break;

case 6: output.Save(sdf.FileName, ImageFormat.Gif); break;

case 7: output.Save(sdf.FileName, ImageFormat.Icon); break;

case 8: output.Save(sdf.FileName, ImageFormat.Tiff); break;

case 9: output.Save(sdf.FileName, ImageFormat.Wmf); break;

}

MessageBox.Show("保存成功,位置:" + sdf.FileName);

}

output.Dispose();

}

private void Form1_Load(object sender, EventArgs e)

{

startupPath = System.Windows.Forms.Application.StartupPath;

detect_model_path = System.IO.Path.Combine(startupPath, "model", "car_plate_detect.onnx");

rec_model_path = System.IO.Path.Combine(startupPath, "model", "plate_rec.onnx");

// MP初始化 ONNX 会话

SessionOptions options = new SessionOptions();

options.LogSeverityLevel = OrtLoggingLevel.ORT_LOGGING_LEVEL_INFO;

options.AppendExecutionProvider_CPU(0);

session_detect = new InferenceSession(detect_model_path, options);

session_rec = new InferenceSession(rec_model_path, new SessionOptions());

// 设置 textBox1 可显示多行

textBox1.Multiline = true;

textBox1.ScrollBars = ScrollBars.Vertical;

// 默认加载测试图片(若存在)

image_path = System.IO.Path.Combine(startupPath, "test_img", "0.jpg");

if (File.Exists(image_path))

{

pictureBox1.Image = new Bitmap(image_path);

image = new Mat(image_path);

}

}

private void pictureBox1_DoubleClick(object sender, EventArgs e)

{

if (pictureBox1.Image != null)

Common.ShowNormalImg(pictureBox1.Image);

}

private void pictureBox2_DoubleClick(object sender, EventArgs e)

{

if (pictureBox2.Image != null)

Common.ShowNormalImg(pictureBox2.Image);

}

}

}

复制代码
using Microsoft.ML.OnnxRuntime;
using Microsoft.ML.OnnxRuntime.Tensors;
using OpenCvSharp;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Linq;
using System.Text;
using System.Windows.Forms;

namespace Onnx_Demo
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        string fileFilter = "*.*|*.bmp;*.jpg;*.jpeg;*.tiff;*.tiff;*.png";
        string image_path = "";
        string startupPath;
        DateTime dt1 = DateTime.Now;
        DateTime dt2 = DateTime.Now;
        Mat image;
        Mat result_image;

        // 车牌检测模型
        private InferenceSession session_detect;
        private string detect_model_path;
        // 车牌识别模型
        private InferenceSession session_rec;
        private string rec_model_path;

        private const float MEAN_VALUE = 0.588f;
        private const float STD_VALUE = 0.193f;
        private readonly string PLATE_CHARS ="#京沪津渝冀晋蒙辽吉黑苏浙皖闽赣鲁豫鄂湘粤桂琼川贵云藏陕甘青宁新学警港澳挂使领民航危0123456789ABCDEFGHJKLMNPQRSTUVWXYZ险品";

        private void button1_Click(object sender, EventArgs e)
        {
            OpenFileDialog ofd = new OpenFileDialog();
            ofd.Filter = fileFilter;
            ofd.InitialDirectory = System.IO.Path.Combine(System.Windows.Forms.Application.StartupPath, "test_img");
            if (ofd.ShowDialog() != DialogResult.OK) return;
            pictureBox1.Image = null;
            image_path = ofd.FileName;
            pictureBox1.Image = new Bitmap(image_path);
            textBox1.Text = "";
            image = new Mat(image_path);
            pictureBox2.Image = null;
        }

        private void button2_Click(object sender, EventArgs e)
        {
            if (string.IsNullOrEmpty(image_path))
                return;

            button2.Enabled = false;
            pictureBox2.Image = null;
            textBox1.Text = "";
            Application.DoEvents();

            // 读取原图
            Mat img0 = new Mat(image_path);
            if (img0.Empty())
            {
                button2.Enabled = true;
                return;
            }

            // 深拷贝一份用于绘制(BGR)
            Mat drawImg = img0.Clone();

            // ---------- 检测预处理 ----------
            Mat blob;
            float ratio;
            int padLeft, padTop;
            Tensor<float> inputTensor = PreprocessDetect(img0, 640, 640, out blob, out ratio, out padLeft, out padTop);

            // ---------- 运行检测模型 ----------
            dt1 = DateTime.Now;
            var container = new List<NamedOnnxValue>
            {
                NamedOnnxValue.CreateFromTensor("input", inputTensor)
            };
            List<PlateResult> plateResults = new List<PlateResult>();
            using (var results = session_detect.Run(container))
            {
                var outputTensor = results.First().AsTensor<float>();
                dt2 = DateTime.Now;

                // 后处理:解析检测框
                List<float[]> detBoxes = PostprocessDetect(outputTensor, ratio, padLeft, padTop, 0.4f, 0.5f);
                // 识别并收集结果
                foreach (var box in detBoxes)
                {
                    float x1 = box[0], y1 = box[1], x2 = box[2], y2 = box[3];
                    float score = box[4];
                    int label = (int)box[13];
                    // 提取四个角点
                    Point2f[] landmarks = new Point2f[4];
                    for (int k = 0; k < 4; k++)
                    {
                        landmarks[k] = new Point2f(box[5 + 2 * k], box[6 + 2 * k]);
                    }

                    // 四点透视变换获取校正车牌图
                    Mat roiImg = FourPointTransform(img0, landmarks);
                    if (roiImg.Empty())
                        continue;

                    string plateType = label == 1 ? "双层" : "单层";
                    if (label == 1)
                    {
                        roiImg = SplitMerge(roiImg);
                        if (roiImg.Empty())
                            continue;
                    }

                    // 识别车牌文字
                    string plateText = RecognizeText(roiImg);
                    roiImg.Dispose();

                    plateResults.Add(new PlateResult
                    {
                        X1 = x1,
                        Y1 = y1,
                        X2 = x2,
                        Y2 = y2,
                        Score = score,
                        Plate = plateText,
                        Type = plateType
                    });
                }

                // ---- 构建文本框输出信息 ----
                StringBuilder sb = new StringBuilder();
                sb.AppendLine("推理耗时:" + (dt2 - dt1).TotalMilliseconds.ToString("F2") + "ms");
                sb.AppendLine("检测到 " + plateResults.Count + " 个车牌:");
                for (int i = 0; i < plateResults.Count; i++)
                {
                    var pr = plateResults[i];
                    sb.AppendLine(string.Format("{0}. [{1}] {2} 置信度:{3:F2} 坐标:({4:F0},{5:F0},{6:F0},{7:F0})",
                        i + 1, pr.Plate, pr.Type, pr.Score, pr.X1, pr.Y1, pr.X2, pr.Y2));
                }
                textBox1.Text = sb.ToString();

                // ---------- 绘制结果到 drawImg ----------
                byte[] imgBytes;
                Cv2.ImEncode(".png", drawImg, out imgBytes);
                using (MemoryStream ms = new MemoryStream(imgBytes))
                {
                    Bitmap bmp = (Bitmap)Image.FromStream(ms);
                    drawImg.Dispose();  // 释放已编码的 Mat

                    using (Graphics g = Graphics.FromImage(bmp))
                    {
                        foreach (var pr in plateResults)
                        {
                            int ix1 = (int)Math.Max(0, pr.X1);
                            int iy1 = (int)Math.Max(0, pr.Y1);
                            int ix2 = (int)Math.Min(bmp.Width - 1, pr.X2);
                            int iy2 = (int)Math.Min(bmp.Height - 1, pr.Y2);
                            if (ix2 <= ix1 || iy2 <= iy1) continue;

                            // 画红色矩形框
                            using (Pen pen = new Pen(Color.Red, 3))
                            {
                                g.DrawRectangle(pen, ix1, iy1, ix2 - ix1, iy2 - iy1);
                            }

                            // 绘制车牌文字(带背景色)
                            string text = string.Format("{0} ({1}) {2:F2}", pr.Plate, pr.Type, pr.Score);
                            Font font = new Font("Microsoft YaHei", 16, FontStyle.Bold);
                            SizeF textSize = g.MeasureString(text, font);
                            float textX = ix1;
                            float textY = iy1 - textSize.Height - 4;
                            if (textY < 0) textY = iy1 + 4;

                            using (Brush bgBrush = new SolidBrush(Color.Red))
                            {
                                g.FillRectangle(bgBrush, textX, textY, textSize.Width + 4, textSize.Height + 4);
                            }
                            using (Brush textBrush = new SolidBrush(Color.White))
                            {
                                g.DrawString(text, font, textBrush, textX + 2, textY + 2);
                            }
                        }
                    }

                    pictureBox2.Image = bmp;   // bmp 已包含绘制的框和文字
                }

                // 释放预处理时创建的临时 
                blob.Dispose();
            }

            img0.Dispose();
            button2.Enabled = true;
        }

        // ---------------------- 下面为所有辅助方法(Letterbox、检测预处理/后处理、NMS、四点变换等)---------------------

        private (Mat boxed, float ratio, int left, int top) Letterbox(Mat img, int targetW, int targetH)
        {
            int height = img.Height, width = img.Width;
            float ratio = Math.Min((float)targetH / height, (float)targetW / width);
            int newW = (int)(width * ratio);
            int newH = (int)(height * ratio);
            int left = (targetW - newW) / 2;
            int top = (targetH - newH) / 2;
            int right = targetW - newW - left;
            int bottom = targetH - newH - top;

            Mat resized = img.Resize(new OpenCvSharp.Size(newW, newH));
            Mat boxed = new Mat();
            Cv2.CopyMakeBorder(resized, boxed, top, bottom, left, right, BorderTypes.Constant, new Scalar(114, 114, 114));
            resized.Dispose();
            return (boxed, ratio, left, top);
        }

        private Tensor<float> PreprocessDetect(Mat img, int targetW, int targetH,
            out Mat blob, out float ratio, out int padLeft, out int padTop)
        {
            var (boxed, r, left, top) = Letterbox(img, targetW, targetH);
            blob = boxed;
            ratio = r;
            padLeft = left;
            padTop = top;

            // 转换为 RGB,归一化
            Mat rgb = new Mat();
            Cv2.CvtColor(blob, rgb, ColorConversionCodes.BGR2RGB);
            rgb.ConvertTo(rgb, MatType.CV_32FC3, 1.0 / 255.0);

            int h = rgb.Height, w = rgb.Width;
            var tensor = new DenseTensor<float>(new[] { 1, 3, h, w });
            for (int y = 0; y < h; y++)
            {
                for (int x = 0; x < w; x++)
                {
                    Vec3f vec = rgb.At<Vec3f>(y, x);
                    tensor[0, 0, y, x] = vec.Item0; // R
                    tensor[0, 1, y, x] = vec.Item1; // G
                    tensor[0, 2, y, x] = vec.Item2; // B
                }
            }
            rgb.Dispose();
            return tensor;
        }

        private List<float[]> PostprocessDetect(Tensor<float> output, float ratio, int left, int top,
            float confThresh, float iouThresh)
        {
            int numAnchors = output.Dimensions[1];
            int numValues = output.Dimensions[2];   // 16
            float[] raw = output.ToArray();

            List<float[]> candidateBoxes = new List<float[]>();
            for (int i = 0; i < numAnchors; i++)
            {
                int offset = i * numValues;
                float objConf = raw[offset + 4];
                if (objConf <= confThresh) continue;

                float cls0 = raw[offset + 13] * objConf;
                float cls1 = raw[offset + 14] * objConf;
                float maxScore = Math.Max(cls0, cls1);
                if (maxScore < confThresh) continue;
                int label = cls0 >= cls1 ? 0 : 1;

                float cx = raw[offset];
                float cy = raw[offset + 1];
                float w = raw[offset + 2];
                float h = raw[offset + 3];
                float x1 = cx - w / 2;
                float y1 = cy - h / 2;
                float x2 = cx + w / 2;
                float y2 = cy + h / 2;

                float lx0 = raw[offset + 5], ly0 = raw[offset + 6];
                float lx1 = raw[offset + 7], ly1 = raw[offset + 8];
                float lx2 = raw[offset + 9], ly2 = raw[offset + 10];
                float lx3 = raw[offset + 11], ly3 = raw[offset + 12];

                candidateBoxes.Add(new float[] {
                    x1, y1, x2, y2, maxScore,
                    lx0, ly0, lx1, ly1, lx2, ly2, lx3, ly3,
                    (float)label
                });
            }

            if (candidateBoxes.Count == 0)
                return new List<float[]>();

            List<float[]> nmsResult = Nms(candidateBoxes, iouThresh);
            List<float[]> finalBoxes = new List<float[]>();
            foreach (var box in nmsResult)
            {
                finalBoxes.Add(RestoreBox(box, ratio, left, top));
            }
            return finalBoxes;
        }

        private float ComputeIoU(float[] boxA, float[] boxB)
        {
            float ax1 = boxA[0], ay1 = boxA[1], ax2 = boxA[2], ay2 = boxA[3];
            float bx1 = boxB[0], by1 = boxB[1], bx2 = boxB[2], by2 = boxB[3];

            float interX1 = Math.Max(ax1, bx1);
            float interY1 = Math.Max(ay1, by1);
            float interX2 = Math.Min(ax2, bx2);
            float interY2 = Math.Min(ay2, by2);
            float interW = Math.Max(0, interX2 - interX1);
            float interH = Math.Max(0, interY2 - interY1);
            float interArea = interW * interH;

            float areaA = (ax2 - ax1) * (ay2 - ay1);
            float areaB = (bx2 - bx1) * (by2 - by1);
            float union = areaA + areaB - interArea;
            if (union <= 1e-6f) return 0;
            return interArea / union;
        }

        private List<float[]> Nms(List<float[]> boxes, float iouThresh)
        {
            var sorted = boxes.OrderByDescending(b => b[4]).ToList();
            var keep = new List<float[]>();
            while (sorted.Count > 0)
            {
                var current = sorted[0];
                keep.Add(current);
                sorted.RemoveAt(0);
                var remaining = new List<float[]>();
                foreach (var b in sorted)
                {
                    if (ComputeIoU(current, b) <= iouThresh)
                        remaining.Add(b);
                }
                sorted = remaining;
            }
            return keep;
        }

        private float[] RestoreBox(float[] box, float ratio, int left, int top)
        {
            float[] restored = new float[box.Length];
            Array.Copy(box, restored, box.Length);
            int[] xIndices = { 0, 2, 5, 7, 9, 11 };
            int[] yIndices = { 1, 3, 6, 8, 10, 12 };
            foreach (int idx in xIndices)
                restored[idx] = (restored[idx] - left) / ratio;
            foreach (int idx in yIndices)
                restored[idx] = (restored[idx] - top) / ratio;
            return restored;
        }

        private Point2f[] OrderPoints(Point2f[] pts)
        {
            if (pts.Length != 4)
                throw new ArgumentException("需要四个点");
            Point2f[] rect = new Point2f[4];
            float[] sums = pts.Select(p => p.X + p.Y).ToArray();
            float[] diffs = pts.Select(p => p.Y - p.X).ToArray();

            rect[0] = pts[Array.IndexOf(sums, sums.Min())];
            rect[2] = pts[Array.IndexOf(sums, sums.Max())];
            rect[1] = pts[Array.IndexOf(diffs, diffs.Min())];
            rect[3] = pts[Array.IndexOf(diffs, diffs.Max())];
            return rect;
        }

        private Mat FourPointTransform(Mat image, Point2f[] pts)
        {
            Point2f[] rect = OrderPoints(pts);
            Point2f tl = rect[0], tr = rect[1], br = rect[2], bl = rect[3];

            float widthA = (float)Math.Sqrt(Math.Pow(br.X - bl.X, 2) + Math.Pow(br.Y - bl.Y, 2));
            float widthB = (float)Math.Sqrt(Math.Pow(tr.X - tl.X, 2) + Math.Pow(tr.Y - tl.Y, 2));
            int maxWidth = Math.Max((int)widthA, (int)widthB);
            if (maxWidth < 1) maxWidth = 1;

            float heightA = (float)Math.Sqrt(Math.Pow(tr.X - br.X, 2) + Math.Pow(tr.Y - br.Y, 2));
            float heightB = (float)Math.Sqrt(Math.Pow(tl.X - bl.X, 2) + Math.Pow(tl.Y - bl.Y, 2));
            int maxHeight = Math.Max((int)heightA, (int)heightB);
            if (maxHeight < 1) maxHeight = 1;

            Point2f[] dst = new Point2f[]
            {
                new Point2f(0, 0),
                new Point2f(maxWidth - 1, 0),
                new Point2f(maxWidth - 1, maxHeight - 1),
                new Point2f(0, maxHeight - 1)
            };

            Mat matrix = Cv2.GetPerspectiveTransform(rect, dst);
            Mat warped = new Mat();
            Cv2.WarpPerspective(image, warped, matrix, new OpenCvSharp.Size(maxWidth, maxHeight));
            return warped;
        }

        private Mat SplitMerge(Mat img)
        {
            int height = img.Height;
            int width = img.Width;
            int upperHeight = (int)(5f / 12f * height);
            int lowerStart = (int)(1f / 3f * height);
            if (upperHeight <= 0 || lowerStart >= height) return img.Clone();

            Mat upper = img[0, upperHeight, 0, width];
            Mat lower = img[lowerStart, height, 0, width];
            if (lower.Empty() || upper.Empty()) return img.Clone();

            Mat upperResized = upper.Resize(new OpenCvSharp.Size(lower.Width, lower.Height));
            Mat merged = new Mat();
            Cv2.HConcat(new Mat[] { upperResized, lower }, merged);
            upper.Dispose(); lower.Dispose(); upperResized.Dispose();
            return merged;
        }

        private string RecognizeText(Mat roiImg)
        {
            Mat resized = roiImg.Resize(new OpenCvSharp.Size(168, 48));
            Mat floatImg = new Mat();
            resized.ConvertTo(floatImg, MatType.CV_32FC3, 1.0 / 255.0);
            floatImg = (floatImg - MEAN_VALUE) / STD_VALUE;

            int h = 48, w = 168;
            var inputTensor = new DenseTensor<float>(new[] { 1, 3, h, w });
            for (int y = 0; y < h; y++)
            {
                for (int x = 0; x < w; x++)
                {
                    Vec3f vec = floatImg.At<Vec3f>(y, x);
                    // 识别模型输入通道顺序与训练一致,此处保持 BGR
                    inputTensor[0, 0, y, x] = vec.Item0; // B
                    inputTensor[0, 1, y, x] = vec.Item1; // G
                    inputTensor[0, 2, y, x] = vec.Item2; // R
                }
            }
            floatImg.Dispose();
            resized.Dispose();

            var container = new List<NamedOnnxValue>
            {
                NamedOnnxValue.CreateFromTensor("images", inputTensor)
            };
            using (var results = session_rec.Run(container))
            {
                var output = results.First().AsTensor<float>();
                int seqLen = output.Dimensions[1];
                int numClasses = output.Dimensions[2];
                int[] predIndices = new int[seqLen];
                for (int i = 0; i < seqLen; i++)
                {
                    int maxIdx = 0;
                    float maxVal = output[0, i, 0];
                    for (int c = 1; c < numClasses; c++)
                    {
                        float val = output[0, i, c];
                        if (val > maxVal)
                        {
                            maxVal = val;
                            maxIdx = c;
                        }
                    }
                    predIndices[i] = maxIdx;
                }
                return DecodePlate(predIndices);
            }
        }

        private string DecodePlate(int[] preds)
        {
            int previous = 0;
            StringBuilder sb = new StringBuilder();
            foreach (int idx in preds)
            {
                if (idx != 0 && idx != previous && idx < PLATE_CHARS.Length)
                {
                    sb.Append(PLATE_CHARS[idx]);
                }
                previous = idx;
            }
            return sb.ToString();
        }

        private class PlateResult
        {
            public float X1, Y1, X2, Y2;
            public float Score;
            public string Plate;
            public string Type;
        }

        private void button3_Click(object sender, EventArgs e)
        {
            if (pictureBox2.Image == null)
                return;
            Bitmap output = new Bitmap(pictureBox2.Image);
            SaveFileDialog sdf = new SaveFileDialog();
            sdf.Title = "保存";
            sdf.Filter = "Images (*.jpg)|*.jpg|Images (*.png)|*.png|Images (*.bmp)|*.bmp|Images (*.emf)|*.emf|Images (*.exif)|*.exif|Images (*.gif)|*.gif|Images (*.ico)|*.ico|Images (*.tiff)|*.tiff|Images (*.wmf)|*.wmf";
            if (sdf.ShowDialog() == DialogResult.OK)
            {
                switch (sdf.FilterIndex)
                {
                    case 1: output.Save(sdf.FileName, ImageFormat.Jpeg); break;
                    case 2: output.Save(sdf.FileName, ImageFormat.Png); break;
                    case 3: output.Save(sdf.FileName, ImageFormat.Bmp); break;
                    case 4: output.Save(sdf.FileName, ImageFormat.Emf); break;
                    case 5: output.Save(sdf.FileName, ImageFormat.Exif); break;
                    case 6: output.Save(sdf.FileName, ImageFormat.Gif); break;
                    case 7: output.Save(sdf.FileName, ImageFormat.Icon); break;
                    case 8: output.Save(sdf.FileName, ImageFormat.Tiff); break;
                    case 9: output.Save(sdf.FileName, ImageFormat.Wmf); break;
                }
                MessageBox.Show("保存成功,位置:" + sdf.FileName);
            }
            output.Dispose();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            startupPath = System.Windows.Forms.Application.StartupPath;
            detect_model_path = System.IO.Path.Combine(startupPath, "model", "car_plate_detect.onnx");
            rec_model_path = System.IO.Path.Combine(startupPath, "model", "plate_rec.onnx");

            // MP初始化 ONNX 会话
            SessionOptions options = new SessionOptions();
            options.LogSeverityLevel = OrtLoggingLevel.ORT_LOGGING_LEVEL_INFO;
            options.AppendExecutionProvider_CPU(0);
            session_detect = new InferenceSession(detect_model_path, options);
            session_rec = new InferenceSession(rec_model_path, new SessionOptions());

            // 设置 textBox1 可显示多行
            textBox1.Multiline = true;
            textBox1.ScrollBars = ScrollBars.Vertical;

            // 默认加载测试图片(若存在)
            image_path = System.IO.Path.Combine(startupPath, "test_img", "0.jpg");
            if (File.Exists(image_path))
            {
                pictureBox1.Image = new Bitmap(image_path);
                image = new Mat(image_path);
            }
        }

        private void pictureBox1_DoubleClick(object sender, EventArgs e)
        {
            if (pictureBox1.Image != null)
                Common.ShowNormalImg(pictureBox1.Image);
        }

        private void pictureBox2_DoubleClick(object sender, EventArgs e)
        {
            if (pictureBox2.Image != null)
                Common.ShowNormalImg(pictureBox2.Image);
        }
    }
}

下载

源码下载

相关推荐
刚子编程1 小时前
C# Join 进阶:GroupJoin、性能对决与自定义比较器
java·servlet·c#·join
海盗12343 小时前
C#中的IEqualityComparer<T>使用
开发语言·c#
IT策士6 小时前
Python Word操作:从入门到精通
python·c#·word
时光追逐者6 小时前
2026 年 .NET 客户端常用 MVVM 框架推荐
c#·.net·mvvm·.net core
xiaoshuaishuai88 小时前
C# 继承与虚方法
开发语言·windows·c#
月昤昽8 小时前
C#实现AutoCAD旋转与直径标注
c#·.net·二次开发·autocad·autocad二次开发
FL16238631298 小时前
基于C#winform实现yolo26-plate中文车牌检测识别支持12种中文双层颜色车牌文字识别
开发语言·c#
Eiceblue9 小时前
锁定单元格 :C# 控制 Excel 单元格编辑权限
开发语言·c#·excel