目录
效果




模型信息
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);
}
}
}