一、整体解决方案架构
核心思路:WinForm(C#)作为前端交互层 ,负责界面展示、用户操作、图像显示;Python 脚本作为后端算法层,负责 YOLO 模型的 ONNX 推理、模型训练;C# 与 Python 通过「进程调用 + JSON 数据交互」实现通信(避免 Pythonnet 的环境冲突问题,稳定性更高)。
| 模块 | 技术栈 | 核心职责 |
|---|---|---|
| 前端交互层 | C# + WinForm + OpenCVSharp | 界面、图像显示、调用 Python |
| 后端算法层 | Python 3.9+ + Ultralytics + ONNX Runtime | YOLO 推理、训练、导出 ONNX |
| 通信层 | 进程调用(Process)+ JSON | 传递参数 / 结果、日志输出 |
二、环境准备
1. Python 环境配置
安装依赖包(建议用虚拟环境):
python
pip install ultralytics==8.0.200 # YOLOv8核心库(支持训练/推理/导出ONNX)
pip install onnxruntime==1.15.1 # ONNX推理引擎
pip install opencv-python==4.8.1.78 numpy==1.26.0 json5 pillow==10.0.1
2. C# WinForm 环境配置(VS 2022)
-
创建项目:选择「Windows Forms App (.NET 6/7/8)」(或.NET Framework 4.8)。
-
安装 NuGet 包:
csInstall-Package OpenCVSharp4 # OpenCV C#封装 Install-Package OpenCVSharp4.runtime.win # Windows运行时 Install-Package Newtonsoft.Json # JSON解析 Install-Package System.Diagnostics.Process # 进程调用(默认已包含)
三、Python 脚本实现
1. YOLO ONNX 推理脚本(yolo_onnx_infer.py)
接收 C# 传递的「模型路径、图像路径」,执行推理后返回 JSON 格式的检测结果(类别、坐标、置信度):
python
import cv2
import onnxruntime as ort
import numpy as np
import json
import sys
# YOLOv8 ONNX后处理(适配YOLOv8输出格式)
def postprocess(outputs, img_shape, conf_thres=0.5, iou_thres=0.45):
predictions = np.squeeze(outputs[0]).T
scores = np.max(predictions[:, 4:], axis=1)
predictions = predictions[scores > conf_thres, :]
scores = scores[scores > conf_thres]
class_ids = np.argmax(predictions[:, 4:], axis=1)
# 转换坐标:xywh -> xyxy
boxes = predictions[:, :4]
h, w = img_shape
boxes[:, 0] = (boxes[:, 0] - boxes[:, 2] / 2) * w # x1
boxes[:, 1] = (boxes[:, 1] - boxes[:, 3] / 2) * h # y1
boxes[:, 2] = (boxes[:, 0] + boxes[:, 2]) * w # x2
boxes[:, 3] = (boxes[:, 1] + boxes[:, 3]) * h # y2
# 非极大值抑制(NMS)
indices = cv2.dnn.NMSBoxes(boxes[:, :4].tolist(), scores.tolist(), conf_thres, iou_thres)
if len(indices) == 0:
return []
# 整理结果
results = []
for i in indices.flatten():
results.append({
"class_id": int(class_ids[i]),
"confidence": float(scores[i]),
"x1": float(boxes[i][0]),
"y1": float(boxes[i][1]),
"x2": float(boxes[i][2]),
"y2": float(boxes[i][3])
})
return results
# 主函数(接收C#传参:模型路径、图像路径)
if __name__ == "__main__":
try:
# 从命令行获取参数
onnx_model_path = sys.argv[1]
img_path = sys.argv[2]
conf_thres = float(sys.argv[3]) if len(sys.argv) > 3 else 0.5
# 1. 读取图像并预处理(YOLOv8要求640x640,BGR转RGB,归一化)
img = cv2.imread(img_path)
img_shape = img.shape[:2] # (h, w)
img_input = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
img_input = cv2.resize(img_input, (640, 640))
img_input = img_input / 255.0
img_input = np.transpose(img_input, (2, 0, 1)) # (3, 640, 640)
img_input = np.expand_dims(img_input, axis=0).astype(np.float32)
# 2. ONNX Runtime推理
session = ort.InferenceSession(onnx_model_path, providers=['CPUExecutionProvider']) # 若有GPU可加CUDAProvider
input_name = session.get_inputs()[0].name
outputs = session.run(None, {input_name: img_input})
# 3. 后处理获取检测结果
det_results = postprocess(outputs, img_shape, conf_thres)
# 4. 输出JSON结果(供C#解析)
print(json.dumps(det_results, ensure_ascii=False))
except Exception as e:
# 异常时输出JSON格式错误信息
print(json.dumps({"error": str(e)}, ensure_ascii=False))
2. YOLO 训练脚本(yolo_train.py)
接收 C# 传递的训练参数(数据集路径、epochs、批次等),执行训练并实时输出日志,训练完成后导出 ONNX 模型:
python
from ultralytics import YOLO
import sys
import json
# 主函数(接收C#传参:配置参数JSON字符串)
if __name__ == "__main__":
try:
# 解析C#传递的训练配置
train_config = json.loads(sys.argv[1])
data_yaml = train_config["data_yaml"] # 数据集yaml路径(YOLO格式)
epochs = int(train_config["epochs"])
batch_size = int(train_config["batch_size"])
pretrained_model = train_config["pretrained_model"] # 预训练模型(如yolov8n.pt)
save_onnx = train_config["save_onnx"] # 是否导出ONNX
output_dir = train_config["output_dir"]
# 1. 加载预训练模型
model = YOLO(pretrained_model)
# 2. 开始训练(实时打印日志供C#捕获)
results = model.train(
data=data_yaml,
epochs=epochs,
batch=batch_size,
project=output_dir,
name="train_exp",
exist_ok=True,
verbose=True
)
# 3. 训练完成后导出ONNX
if save_onnx:
model.export(format="onnx", imgsz=640) # 导出ONNX到训练输出目录
# 4. 输出训练完成信息
print(json.dumps({"status": "success", "output_dir": f"{output_dir}/train_exp"}, ensure_ascii=False))
except Exception as e:
print(json.dumps({"error": str(e)}, ensure_ascii=False))
四、C# WinForm 实现
1. 界面设计(核心控件)
| 控件类型 | 名称 | 用途 |
|---|---|---|
| PictureBox | pbImage | 显示原图 / 检测结果图 |
| TextBox | txtModelPath | 输入 ONNX 模型路径 |
| TextBox | txtImagePath | 输入图像路径 |
| Button | btnSelectImage | 选择图像 |
| Button | btnInfer | 执行推理 |
| TextBox | txtDetResult | 显示检测结果(JSON) |
| TextBox | txtTrainConfig | 训练配置(JSON 格式) |
| Button | btnTrain | 开始训练 |
| TextBox | txtTrainLog | 显示训练日志(多行) |
2. 核心 C# 代码
cs
using System;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Windows.Forms;
using Newtonsoft.Json;
using OpenCvSharp;
using OpenCvSharp.Extensions;
namespace YOLOWinForm
{
public partial class MainForm : Form
{
// Python路径(根据实际安装路径修改)
private readonly string _pythonPath = @"C:\Python39\python.exe";
// Python脚本路径
private readonly string _inferScriptPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "yolo_onnx_infer.py");
private readonly string _trainScriptPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "yolo_train.py");
public MainForm()
{
InitializeComponent();
// 初始化控件(如设置多行日志框)
txtTrainLog.Multiline = true;
txtTrainLog.ScrollBars = ScrollBars.Vertical;
txtDetResult.Multiline = true;
txtDetResult.ScrollBars = ScrollBars.Vertical;
}
#region 1. 推理功能
// 选择图像按钮点击事件
private void btnSelectImage_Click(object sender, EventArgs e)
{
using (OpenFileDialog ofd = new OpenFileDialog())
{
ofd.Filter = "图像文件|*.jpg;*.png;*.jpeg";
if (ofd.ShowDialog() == DialogResult.OK)
{
txtImagePath.Text = ofd.FileName;
// 显示原图
using (Mat img = Cv2.ImRead(ofd.FileName))
{
pbImage.Image = BitmapConverter.ToBitmap(img);
}
}
}
}
// 推理按钮点击事件
private void btnInfer_Click(object sender, EventArgs e)
{
try
{
// 校验参数
if (!File.Exists(txtModelPath.Text) || !File.Exists(txtImagePath.Text))
{
MessageBox.Show("模型或图像路径不存在!");
return;
}
// 构造Python推理命令参数
string[] args = new[]
{
_inferScriptPath,
txtModelPath.Text, // 参数1:ONNX模型路径
txtImagePath.Text, // 参数2:图像路径
"0.5" // 参数3:置信度阈值
};
// 调用Python脚本并获取结果
string inferResult = RunPythonScript(_pythonPath, args, out string errorMsg);
if (!string.IsNullOrEmpty(errorMsg))
{
txtDetResult.Text = $"推理错误:{errorMsg}";
return;
}
// 解析JSON结果
var detResults = JsonConvert.DeserializeObject<DetResult[]>(inferResult);
txtDetResult.Text = JsonConvert.SerializeObject(detResults, Formatting.Indented);
// 绘制检测框并显示
DrawDetectionBoxes(txtImagePath.Text, detResults);
}
catch (Exception ex)
{
MessageBox.Show($"推理失败:{ex.Message}");
}
}
// 绘制检测框
private void DrawDetectionBoxes(string imgPath, DetResult[] detResults)
{
using (Mat img = Cv2.ImRead(imgPath))
{
foreach (var result in detResults)
{
// 绘制矩形框(红色,线宽2)
Cv2.Rectangle(img,
new OpenCvSharp.Point((int)result.x1, (int)result.y1),
new OpenCvSharp.Point((int)result.x2, (int)result.y2),
new Scalar(0, 0, 255), 2);
// 绘制类别+置信度文本
string label = $"Class {result.class_id}: {result.confidence:F2}";
Cv2.PutText(img, label,
new OpenCvSharp.Point((int)result.x1, (int)result.y1 - 10),
HersheyFonts.HersheySimplex, 0.5, new Scalar(0, 0, 255), 1);
}
// 显示绘制后的图像
pbImage.Image = BitmapConverter.ToBitmap(img);
}
}
#endregion
#region 2. 训练功能
// 训练按钮点击事件
private void btnTrain_Click(object sender, EventArgs e)
{
try
{
// 校验训练配置(示例:JSON格式 {"data_yaml":"dataset/data.yaml","epochs":10,"batch_size":8,...})
if (string.IsNullOrEmpty(txtTrainConfig.Text))
{
MessageBox.Show("请输入训练配置JSON!");
return;
}
// 构造训练参数
string[] args = new[]
{
_trainScriptPath,
txtTrainConfig.Text // 参数1:训练配置JSON字符串
};
// 异步执行训练(避免界面卡死)
Task.Run(() =>
{
RunPythonScriptWithLog(_pythonPath, args, (log) =>
{
// 实时更新训练日志(跨线程更新UI)
Invoke(new Action(() =>
{
txtTrainLog.AppendText($"{DateTime.Now:HH:mm:ss} | {log}\r\n");
txtTrainLog.ScrollToCaret();
}));
});
});
}
catch (Exception ex)
{
MessageBox.Show($"训练启动失败:{ex.Message}");
}
}
#endregion
#region 通用:调用Python脚本(无实时日志)
private string RunPythonScript(string pythonExe, string[] args, out string errorMsg)
{
errorMsg = string.Empty;
using (Process process = new Process())
{
process.StartInfo.FileName = pythonExe;
process.StartInfo.Arguments = string.Join(" ", args);
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;
process.StartInfo.CreateNoWindow = true; // 不显示Python控制台窗口
// 执行脚本
process.Start();
string output = process.StandardOutput.ReadToEnd();
errorMsg = process.StandardError.ReadToEnd();
process.WaitForExit();
return output;
}
}
#endregion
#region 通用:调用Python脚本(带实时日志输出)
private void RunPythonScriptWithLog(string pythonExe, string[] args, Action<string> logCallback)
{
using (Process process = new Process())
{
process.StartInfo.FileName = pythonExe;
process.StartInfo.Arguments = string.Join(" ", args);
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;
process.StartInfo.CreateNoWindow = true;
// 实时捕获输出(日志)
process.OutputDataReceived += (s, e) =>
{
if (!string.IsNullOrEmpty(e.Data))
logCallback.Invoke(e.Data);
};
process.ErrorDataReceived += (s, e) =>
{
if (!string.IsNullOrEmpty(e.Data))
logCallback.Invoke($"[ERROR] {e.Data}");
};
// 启动并开始监听输出
process.Start();
process.BeginOutputReadLine();
process.BeginErrorReadLine();
process.WaitForExit();
}
}
#endregion
}
// 检测结果实体类(与Python脚本输出JSON对应)
public class DetResult
{
public int class_id { get; set; }
public float confidence { get; set; }
public float x1 { get; set; }
public float y1 { get; set; }
public float x2 { get; set; }
public float y2 { get; set; }
}
}
五、关键说明与调试技巧
1. 数据集格式(YOLO 训练要求)
训练脚本依赖 YOLO 格式的数据集,需准备data.yaml配置文件:
python
# data.yaml示例
train: ./train/images # 训练集图像路径
val: ./val/images # 验证集图像路径
nc: 2 # 类别数
names: ['cat', 'dog'] # 类别名称
标注文件需为.txt(与图像同名),每行格式:class_id x_center y_center width height(归一化坐标)。
2. 路径问题(核心避坑点)
- Python 脚本中所有路径建议用绝对路径(C# 传递参数时转换为绝对路径)。
- C# 中
AppDomain.CurrentDomain.BaseDirectory可获取程序运行目录,将 Python 脚本放在该目录下避免路径错误。
3. 性能优化
- 推理时若有 GPU,修改 Python 脚本中 ONNX Runtime 的
providers为['CUDAExecutionProvider'](需安装 CUDA 版 onnxruntime)。 - 训练时建议用 GPU(需安装 CUDA、cuDNN,Ultralytics 会自动识别)。
4. 调试技巧
- 先单独运行 Python 脚本测试功能(如
python yolo_onnx_infer.py model.onnx test.jpg 0.5),确保脚本无错误。 - C# 中捕获 Python 的
StandardError输出,定位参数传递、环境依赖等问题。
六、扩展功能
- 视频推理:修改 Python 脚本支持视频帧处理,C# 通过 OpenCVSharp 逐帧读取视频并调用推理。
- 模型导出:在 C# 中增加「导出 ONNX」按钮,调用 Python 脚本执行
model.export(format="onnx")。 - 批量推理:遍历文件夹内所有图像,批量执行推理并保存结果。