1.环境安装1.1安装python下载:Download Python | Python.orgcmd命令验证1.2. 安装 PyTorchCPU 版本地训练 / 推理用pip install torch torchvision torchaudio -i https://pypi.tuna.tsinghua.edu.cn/simple1.3 安装 YOLOv8训练、导出模型、识别都靠它pip install ultralytics -i https://pypi.tuna.tsinghua.edu.cn/simple1.4 安装 OpenCV图片处理用的pip install opencv-python pillow matplotlib pandas tqdm -i https://pypi.tuna.tsinghua.edu.cn/simple2.创建训练素材文件夹3.去标记车牌位置1.1 标记网站https://makesense.bimant.com/1.2 导出yolo专用的标记文件4.开始训练4.1 编辑文件“data.ymal”。path: ./ train: images/train val: images/train nc: 1 names: [plate]或path: ./ train: images/train val: images/train nc: 1 names: 0: plate4.2 CMD命令开启训练yolo train modelyolo11s.pt datadata.yaml epochs100 imgsz640 rectTrue训练开始.....训练结束后会产生best.pt文件5.测试代码from ultralytics import YOLO import os import cv2 # 固定路径 model YOLO(r./best.pt) # 要检测的图片文件夹 source_path ./plate_local/images/train # 保存裁剪后车牌的文件夹 crop_save_dir ./plate_local/裁剪的车牌 os.makedirs(crop_save_dir, exist_okTrue) # 新增保存【画框标记后的原图】文件夹 marked_save_dir ./plate_local/标记原图 os.makedirs(marked_save_dir, exist_okTrue) # 开始检测 自动裁剪 print( 开始检测并裁剪车牌...) results model.predict(sourcesource_path, conf0.1) # 遍历每一张图的结果 for idx, result in enumerate(results): # 读取原图 img cv2.imread(result.path) img_name os.path.basename(result.path) # 复制一张图用来画框避免破坏原图 img_marked img.copy() # 遍历这张图里的所有框 for box_idx, box in enumerate(result.boxes): # 获取类别只处理 plate cls_id int(box.cls[0]) cls_name result.names[cls_id] if cls_name plate: # 框坐标 x1, y1, x2, y2 map(int, box.xyxy[0]) # 置信度 conf float(box.conf[0]) # 1. 裁剪车牌原有逻辑 crop_plate img[y1:y2, x1:x2] save_path os.path.join(crop_save_dir, f{os.path.splitext(img_name)[0]}_{box_idx}.jpg) cv2.imwrite(save_path, crop_plate) # 2. 在原图上画框 标文字 # 画矩形框绿色厚度2 cv2.rectangle(img_marked, (x1, y1), (x2, y2), (0, 255, 0), 2) # 标注文字plate 置信度 label f{cls_name} {conf:.2f} cv2.putText(img_marked, label, (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2) # 保存画框后的原图 marked_img_path os.path.join(marked_save_dir, img_name) cv2.imwrite(marked_img_path, img_marked) print(f\n✅ 全部完成) print(f 裁剪车牌{crop_save_dir}) print(f 标记原图{marked_save_dir})6.转.onnx的cmd命令和看模型网站yolo export modelbest.pt formatonnx simplifyTrue opset15 dynamicFalse nmsFalsebest.onnx7.转通用模型C#代码调用using SixLabors.ImageSharp; using SixLabors.ImageSharp.Drawing.Processing; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; using Microsoft.ML.OnnxRuntime; using Microsoft.ML.OnnxRuntime.Tensors; using SixLabors.Fonts; public class ConsoleProgressBar { private readonly int _total; private int _current; private readonly int _barWidth 40; private readonly char _fillChar ; private readonly char _emptyChar -; public ConsoleProgressBar(int total) { _total total; _current 0; } public void Update(int current) { _current current; Draw(); } public void Increment() { _current; Draw(); } private void Draw() { float percent (float)_current / _total; int filled (int)(percent * _barWidth); string bar new string(_fillChar, filled) new string(_emptyChar, _barWidth - filled); string percentStr ${percent:P0}.PadLeft(4); Console.Write($\r[{bar}] {percentStr} ({_current}/{_total})); if (_current _total) { Console.WriteLine(); } } } public class YoloBox { public float Cx { get; set; } public float Cy { get; set; } public float W { get; set; } public float H { get; set; } public float Conf { get; set; } public string Label { get; set; } } public class SmartPageDetector { private const int ModelSize 640; //根据模型大小而调整 private const float ConfThreshold 0.25f; //可信度 private const float NmsThreshold 0.45f; private readonly string[] _classNameMap { plate}; public void SmartDetect(string inputPath, string modelPath, string outputDir, string filterLabel null) { Directory.CreateDirectory(outputDir); if (File.Exists(inputPath)) ProcessSingleImage(inputPath, modelPath, outputDir, filterLabel); else if (Directory.Exists(inputPath)) ProcessFolder(inputPath, modelPath, outputDir, filterLabel); else Console.WriteLine(❌ 路径不存在); } private void ProcessSingleImage(string imgPath, string modelPath, string outputDir, string filterLabel) { try { using var s new InferenceSession(modelPath); ProcessAndSaveImage(imgPath, s, outputDir, filterLabel); } catch (Exception ex) { Console.WriteLine($❌ 失败{ex.Message}); } } private void ProcessFolder(string folderPath, string modelPath, string outputDir, string filterLabel) { var exts new[] { .jpg, .jpeg, .png, .bmp }; var files Directory.GetFiles(folderPath).Where(f exts.Contains(Path.GetExtension(f).ToLower())).ToList(); using var s new InferenceSession(modelPath); var progress new ConsoleProgressBar(files.Count); foreach (var f in files) { try { ProcessAndSaveImage(f, s, outputDir, filterLabel); progress.Increment(); } catch { Console.WriteLine($异常跳过{f}); } } } private void ProcessAndSaveImage(string imgPath, InferenceSession session, string outputDir, string filterLabel null) { using var original Image.LoadRgb24(imgPath); using var cleanOriginal original.Clone(); int origW original.Width; int origH original.Height; float ratio Math.Min((float)ModelSize / origW, (float)ModelSize / origH); int newW (int)(origW * ratio); int newH (int)(origH * ratio); int padX (ModelSize - newW) / 2; int padY (ModelSize - newH) / 2; using var resized original.Clone(ctx ctx.Resize(newW, newH)); using var inputImg new ImageRgb24(ModelSize, ModelSize, Color.Black); inputImg.Mutate(ctx ctx.DrawImage(resized, new Point(padX, padY), 1f)); var tensor new DenseTensorfloat(new[] { 1, 3, ModelSize, ModelSize }); for (int y 0; y ModelSize; y) for (int x 0; x ModelSize; x) { var p inputImg[x, y]; tensor[0, 0, y, x] p.R / 255f; tensor[0, 1, y, x] p.G / 255f; tensor[0, 2, y, x] p.B / 255f; } using var runOutputs session.Run(new[] { NamedOnnxValue.CreateFromTensor(images, tensor) }); var output runOutputs.First().AsTensorfloat(); int totalBoxes output.Dimensions[2]; int attrCount output.Dimensions[1]; bool isMultiClass attrCount 5; var boxes new ListYoloBox(); for (int i 0; i totalBoxes; i) { float objConf output[0, 4, i]; string label _classNameMap[0]; float finalConf objConf; // // 多类别计算最终置信度 // if (isMultiClass) { int classCount attrCount - 5; float maxScore 0; int classId 0; for (int c 0; c classCount; c) { float score output[0, 5 c, i]; if (score maxScore) { maxScore score; classId c; } } finalConf objConf * maxScore; // ✅ 多类别必须过滤最终置信度 if (finalConf ConfThreshold) continue; label classId _classNameMap.Length ? _classNameMap[classId] : $cls{classId}; } // // 单类别直接判断置信度 // else { if (finalConf ConfThreshold) continue; } // 过滤标记名 if (!string.IsNullOrEmpty(filterLabel) label ! filterLabel) continue; boxes.Add(new YoloBox { Cx output[0, 0, i], Cy output[0, 1, i], W output[0, 2, i], H output[0, 3, i], Conf finalConf, Label label }); } var finalBoxes Nms(boxes); var pen Pens.Solid(Color.Red, 3); var font SystemFonts.CreateFont(Arial, 20); int cropIndex 1; string nameNoExt Path.GetFileNameWithoutExtension(imgPath); string ext Path.GetExtension(imgPath); foreach (var box in finalBoxes) { float rx (box.Cx - padX) / ratio; float ry (box.Cy - padY) / ratio; float rw box.W / ratio; float rh box.H / ratio; float x1 rx - rw / 2; float y1 ry - rh / 2; original.Mutate(m m.Draw(pen, new RectangleF(x1, y1, rw, rh))); original.Mutate(m m.DrawText(${box.Label} {box.Conf:F2}, font, Color.Lime, new PointF(x1, y1 - 25))); try { int ix Math.Max(0, (int)x1); int iy Math.Max(0, (int)y1); int iw Math.Min((int)rw, origW - ix); int ih Math.Min((int)rh, origH - iy); using var cropImg cleanOriginal.Clone(m m.Crop(new Rectangle(ix, iy, iw, ih))); string cropPath Path.Combine(outputDir, crop); Directory.CreateDirectory(cropPath); cropImg.Save(Path.Combine(cropPath, ${nameNoExt}_{cropIndex}{ext})); } catch { } } string makePath Path.Combine(outputDir, mark); Directory.CreateDirectory(makePath); original.Save(Path.Combine(makePath, Path.GetFileName(imgPath))); } private ListYoloBox Nms(ListYoloBox boxes) { var s boxes.OrderByDescending(b b.Conf).ToList(); var res new ListYoloBox(); while (s.Count 0) { var b s[0]; res.Add(b); s.RemoveAt(0); s.RemoveAll(x Iou(b, x) NmsThreshold); } return res; } private float Iou(YoloBox a, YoloBox b) { float a1x a.Cx - a.W / 2, a1y a.Cy - a.H / 2; float a2x a.Cx a.W / 2, a2y a.Cy a.H / 2; float b1x b.Cx - b.W / 2, b1y b.Cy - b.H / 2; float b2x b.Cx b.W / 2, b2y b.Cy b.H / 2; float ix Math.Max(0, Math.Min(a2x, b2x) - Math.Max(a1x, b1x)); float iy Math.Max(0, Math.Min(a2y, b2y) - Math.Max(a1y, b1y)); float inter ix * iy; float union a.W * a.H b.W * b.H - inter; return union 0 ? 0 : inter / union; } }8.测试效果图