opencv_zoo-main\models\license_plate_detection_yunet中有个lpd_yunet车牌检测的例子,只有python版本的,笔者平时OpencvSharp用的较多,于是想着用OpencvSharp来实现,但是笔者对python不怎么熟悉,经过一番折腾,终于实现了该功能。

效果如下:(改用openvinosharp实现会更快,下面只贴opencvsharp相关代码)
免责声明:车牌图片来源网络,仅供效果测试和学习使用,如有侵权请联系作者删除

模型信息:

全部代码如下:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using OpenCvSharp;
using OpenCvSharp.Dnn;
namespace LPD_YuNetTest
{
public class LPD_YuNet : IDisposable
{
private string modelPath;
private Size inputSize;
private float confidenceThreshold;
private float nmsThreshold;
private int topK;
private int keepTopK;
private Backend backendId;
private Target targetId;
private Net model;
private Mat priors;
private readonly string[] outputNames = { "loc", "conf", "iou" };
private readonly int[][] minSizes = new int[][]
{
new int[] { 10, 16, 24 },
new int[] { 32, 48 },
new int[] { 64, 96 },
new int[] { 128, 192, 256 }
};
private readonly int[] steps = { 8, 16, 32, 64 };
private readonly float[] variance = { 0.1f, 0.2f };
public LPD_YuNet(string modelPath, Size inputSize = default, float confThreshold = 0.8f,
float nmsThreshold = 0.3f, int topK = 5000, int keepTopK = 750,
Backend backendId = Backend.OPENCV, Target targetId = Target.CPU)
{
this.modelPath = modelPath;
this.inputSize = inputSize.Width == 0 ? new Size(320, 240) : inputSize;
this.confidenceThreshold = confThreshold;
this.nmsThreshold = nmsThreshold;
this.topK = topK;
this.keepTopK = keepTopK;
this.backendId = backendId;
this.targetId = targetId;
// Load model
model = Net.ReadNet(modelPath);
model.SetPreferableBackend(backendId);
model.SetPreferableTarget(targetId);
// Generate anchors/priorboxes
PriorGen();
}
public void SetInputSize(Size size)
{
inputSize = size;
PriorGen();
}
private Mat Preprocess(Mat image)
{
// 调整为模型期望的输入尺寸
var resized = new Mat();
Cv2.Resize(image, resized, inputSize);
return CvDnn.BlobFromImage(resized, 1.0, inputSize, new Scalar(0, 0, 0), true, false);
}
public Mat[] Infer(Mat image)
{
// Preprocess
var inputBlob = Preprocess(image);
// Forward
model.SetInput(inputBlob);
var outputBlobs = outputNames.Select(name => model.Forward(name)).ToArray();
// Postprocess
var results = Postprocess(outputBlobs);
foreach (var blob in outputBlobs)
{
blob.Dispose();
}
return new Mat[] { results };
}
private Mat Postprocess(Mat[] blob)
{
// Decode
var dets = Decode(blob);
if (dets.Empty())
return new Mat(0, 9, MatType.CV_32F);
// 准备NMS数据
var rects = new List<Rect2d>();
var scores = new List<float>();
var indices = new List<int>();
for (int i = 0; i < dets.Rows; i++)
{
var score = dets.At<float>(i, 8);
if (score < confidenceThreshold)
continue;
// 获取四个点并计算边界矩形
var points = new Point2f[4];
for (int j = 0; j < 4; j++)
{
points[j] = new Point2f(
dets.At<float>(i, j * 2),
dets.At<float>(i, j * 2 + 1)
);
}
var rect = Cv2.BoundingRect(points);
rects.Add(new Rect2d(rect.X, rect.Y, rect.Width, rect.Height));
scores.Add(score);
indices.Add(i);
}
if (rects.Count == 0)
return new Mat(0, 9, MatType.CV_32F);
// NMS
CvDnn.NMSBoxes(rects, scores, confidenceThreshold, nmsThreshold, out int[] keepIndices, 1f, topK);
if (keepIndices.Length == 0)
return new Mat(0, 9, MatType.CV_32F);
// 创建结果矩阵
var result = new Mat(Math.Min(keepIndices.Length, keepTopK), 9, MatType.CV_32F);
for (int i = 0; i < Math.Min(keepIndices.Length, keepTopK); i++)
{
var originalIndex = indices[keepIndices[i]];
dets.Row(originalIndex).CopyTo(result.Row(i));
}
return result;
}
private void PriorGen()
{
int w = inputSize.Width;
int h = inputSize.Height;
var featureMap2th = new int[] { ((h + 1) / 2) / 2, ((w + 1) / 2) / 2 };
var featureMap3th = new int[] { featureMap2th[0] / 2, featureMap2th[1] / 2 };
var featureMap4th = new int[] { featureMap3th[0] / 2, featureMap3th[1] / 2 };
var featureMap5th = new int[] { featureMap4th[0] / 2, featureMap4th[1] / 2 };
var featureMap6th = new int[] { featureMap5th[0] / 2, featureMap5th[1] / 2 };
var featureMaps = new int[][] { featureMap3th, featureMap4th, featureMap5th, featureMap6th };
var priorsList = new List<float[]>();
for (int k = 0; k < featureMaps.Length; k++)
{
var minSizesK = minSizes[k];
var f = featureMaps[k];
var step = steps[k];
for (int i = 0; i < f[0]; i++)
{
for (int j = 0; j < f[1]; j++)
{
foreach (var minSize in minSizesK)
{
float s_kx = minSize / (float)w;
float s_ky = minSize / (float)h;
float cx = (j + 0.5f) * step / w;
float cy = (i + 0.5f) * step / h;
priorsList.Add(new float[] { cx, cy, s_kx, s_ky });
}
}
}
}
priors?.Dispose();
priors = new Mat(priorsList.Count, 4, MatType.CV_32F);
for (int i = 0; i < priorsList.Count; i++)
{
priors.Set<float>(i, 0, priorsList[i][0]);
priors.Set<float>(i, 1, priorsList[i][1]);
priors.Set<float>(i, 2, priorsList[i][2]);
priors.Set<float>(i, 3, priorsList[i][3]);
}
}
private Mat Decode(Mat[] blob)
{
var loc = blob[0]; // [4385, 14]
var conf = blob[1]; // [4385, 2]
var iou = blob[2]; // [4385, 1]
int numPriors = loc.Rows;
var dets = new Mat(numPriors, 9, MatType.CV_32F);
// 使用指针操作提高性能
unsafe
{
float* locPtr = (float*)loc.Data;
float* confPtr = (float*)conf.Data;
float* iouPtr = (float*)iou.Data;
float* priorsPtr = (float*)priors.Data;
float* detsPtr = (float*)dets.Data;
for (int i = 0; i < numPriors; i++)
{
// 计算分数
float clsScore = confPtr[i * 2 + 1]; // 第二个类的分数
float iouScore = Math.Max(0, Math.Min(1, iouPtr[i]));
float score = (float)Math.Sqrt(clsScore * iouScore);
// 获取先验框参数
float cx = priorsPtr[i * 4];
float cy = priorsPtr[i * 4 + 1];
float s_kx = priorsPtr[i * 4 + 2];
float s_ky = priorsPtr[i * 4 + 3];
// 计算四个角点
float* locRow = locPtr + i * 14;
// 点1 (左上)
detsPtr[i * 9] = (cx + locRow[4] * variance[0] * s_kx) * inputSize.Width;
detsPtr[i * 9 + 1] = (cy + locRow[5] * variance[0] * s_ky) * inputSize.Height;
// 点2 (右上)
detsPtr[i * 9 + 2] = (cx + locRow[6] * variance[0] * s_kx) * inputSize.Width;
detsPtr[i * 9 + 3] = (cy + locRow[7] * variance[0] * s_ky) * inputSize.Height;
// 点3 (右下)
detsPtr[i * 9 + 4] = (cx + locRow[10] * variance[0] * s_kx) * inputSize.Width;
detsPtr[i * 9 + 5] = (cy + locRow[11] * variance[0] * s_ky) * inputSize.Height;
// 点4 (左下)
detsPtr[i * 9 + 6] = (cx + locRow[12] * variance[0] * s_kx) * inputSize.Width;
detsPtr[i * 9 + 7] = (cy + locRow[13] * variance[0] * s_ky) * inputSize.Height;
// 分数
detsPtr[i * 9 + 8] = score;
}
}
return dets;
}
public void Dispose()
{
model?.Dispose();
priors?.Dispose();
}
}
public class LPD_YuNetDemo
{
public static Mat Visualize(Mat image, Mat dets, double scaleX = 1.0, double scaleY = 1.0, Scalar lineColor = default, Scalar textColor = default, double? fps = null)
{
lineColor = lineColor == default ? new Scalar(0, 255, 0) : lineColor;
textColor = textColor == default ? new Scalar(0, 0, 255) : textColor;
Mat output = image.Clone();
if (fps.HasValue)
{
Cv2.PutText(output, $"FPS: {fps.Value:F2}", new Point(10, 30),
HersheyFonts.HersheySimplex, 0.7, textColor, 2);
}
if (dets.Empty())
return output;
for (int i = 0; i < dets.Rows; i++)
{
var bbox = new float[8];
Marshal.Copy(dets.Row(i).Data,bbox,0,8);
//dets.Row(i).GetArray(out bbox);
// 缩放坐标到原始图像尺寸
var points = new Point[]
{
new Point((int)(bbox[0] * scaleX), (int)(bbox[1] * scaleY)),
new Point((int)(bbox[2] * scaleX), (int)(bbox[3] * scaleY)),
new Point((int)(bbox[4] * scaleX), (int)(bbox[5] * scaleY)),
new Point((int)(bbox[6] * scaleX), (int)(bbox[7] * scaleY))
};
// 绘制车牌边框
for (int j = 0; j < 4; j++)
{
Cv2.Line(output, points[j], points[(j + 1) % 4], lineColor, 2);
}
// 显示置信度
float score = dets.At<float>(i, 8);
Cv2.PutText(output, $"{score:F2}", points[0],
HersheyFonts.HersheySimplex, 0.5, textColor, 1);
}
return output;
}
public static void Main2()
{
string modelPath = "D:\\opencvsharp\\1\\opencv_zoo-main\\opencv_zoo-main\\models\\license_plate_detection_yunet\\license_plate_detection_lpd_yunet_2023mar.onnx";
if (!System.IO.File.Exists(modelPath))
{
Console.WriteLine($"Model file not found: {modelPath}");
return;
}
var inputSize = new Size(320, 240);
var model = new LPD_YuNet(
modelPath: modelPath,
inputSize: inputSize,
confThreshold: 0.6f, // 降低阈值提高检测率
nmsThreshold: 0.3f,
topK: 1000, // 减少处理的数量
keepTopK: 10,
backendId: 0, // OpenCV backend
targetId: 0 // CPU
);
var choice = "1";
if (choice == "1")
{
// 摄像头模式
var cap = new VideoCapture(0);
if (!cap.IsOpened())
{
Console.WriteLine("Cannot open camera!");
return;
}
cap.Set(VideoCaptureProperties.FrameWidth, 640);
cap.Set(VideoCaptureProperties.FrameHeight, 480);
var stopwatch = new Stopwatch();
int frameCount = 0;
double fps = 0;
while (true)
{
var frame = new Mat();
if (!cap.Read(frame) || frame.Empty())
{
Console.WriteLine("No frames grabbed!");
break;
}
stopwatch.Start();
// 推理
var results = model.Infer(frame);
var result = results[0];
stopwatch.Stop();
frameCount++;
// 计算FPS
if (stopwatch.Elapsed.TotalSeconds >= 1.0)
{
fps = frameCount / stopwatch.Elapsed.TotalSeconds;
frameCount = 0;
stopwatch.Restart();
}
// 计算缩放比例
double scaleX = (double)frame.Width / inputSize.Width;
double scaleY = (double)frame.Height / inputSize.Height;
// 可视化结果
var displayFrame = Visualize(frame, result, scaleX, scaleY, fps: fps);
Cv2.ImShow("LPD-YuNet Demo", displayFrame);
// 显示检测数量
Console.WriteLine($"Detected: {result.Rows} license plates, FPS: {fps:F2}");
if (Cv2.WaitKey(1) == 27) // ESC键退出
break;
}
}
else
{
// 图像文件模式
Console.Write("Enter image path: ");
var imagePath = Console.ReadLine();
if (!System.IO.File.Exists(imagePath))
{
Console.WriteLine($"Image file not found: {imagePath}");
return;
}
var image = Cv2.ImRead(imagePath);
if (image.Empty())
{
Console.WriteLine("Failed to load image!");
return;
}
// 设置模型输入尺寸为图像尺寸
model.SetInputSize(new Size(image.Width, image.Height));
var stopwatch = Stopwatch.StartNew();
var results = model.Infer(image);
stopwatch.Stop();
var result = results[0];
Console.WriteLine($"Inference time: {stopwatch.ElapsedMilliseconds}ms");
Console.WriteLine($"Detected {result.Rows} license plates");
// 可视化结果
var resultImage = Visualize(image, result);
Cv2.ImShow("Detection Result", resultImage);
Cv2.WaitKey(0);
// 保存结果
Cv2.ImWrite("detection_result.jpg", resultImage);
Console.WriteLine("Result saved to detection_result.jpg");
}
}
}
}