做工业视觉上位机开发最头疼的就是硬件兼容问题:这次项目用海康相机,下次用大华,再下次用巴斯勒,每个品牌的SDK都不一样,每次都要重写一遍对接代码;YOLO模型也是,一会用v5,一会用v8,一会用TensorRT部署,一会用ONNX Runtime,代码改来改去特别麻烦。
我去年花了一周时间搭了一套通用的工业视觉上位机框架,抽象了相机和推理引擎两层,现在换相机或者换YOLO版本都不用改核心业务逻辑,对接新硬件只需要写几十行适配代码就行,今天把这套框架的设计思路分享给大家。
框架设计核心思路:面向抽象编程,不面向具体实现
核心就是两个抽象接口:ICamera相机接口和IInferEngine推理引擎接口,所有具体的相机实现和推理引擎实现都继承这两个接口,上层业务逻辑只依赖接口,不依赖具体实现,实现插拔式兼容。
整体架构分层:
┌─────────────────────────────────┐
│ 业务逻辑层 │
│ 产线逻辑、结果处理、PLC通信 │
└─────────────────────────────────┘
↓ ↑
┌─────────────────────────────────┐
│ 抽象接口层 │
│ ICamera接口 IInferEngine接口 │
└─────────────────────────────────┘
↓ ↑
┌─────────────┐ ┌─────────────────┐
│ 相机实现层 │ │ 推理引擎实现层 │
│ 海康/大华/ │ │ YOLOv5/v8/v9/ │
│ 巴斯勒/虚拟 │ │ TensorRT/ONNX │
└─────────────┘ └─────────────────┘
第一步:定义ICamera相机抽象接口
不管什么品牌的相机,核心功能都是一样的:打开、关闭、触发拍照、注册取图回调、设置参数。所以接口定义如下:
/// <summary>
/// 工业相机抽象接口
/// </summary>
public interface ICamera : IDisposable
{
/// <summary>
/// 相机唯一ID
/// </summary>
string CameraId { get; }
/// <summary>
/// 相机名称
/// </summary>
string CameraName { get; set; }
/// <summary>
/// 是否已打开
/// </summary>
bool IsOpened { get; }
/// <summary>
/// 取图回调事件
/// </summary>
event Action<ImageFrame> OnImageCaptured;
/// <summary>
/// 枚举可用相机
/// </summary>
/// <returns>可用相机列表</returns>
List<CameraInfo> EnumCameras();
/// <summary>
/// 打开相机
/// </summary>
/// <param name="cameraId">相机ID</param>
/// <returns>是否成功</returns>
bool Open(string cameraId);
/// <summary>
/// 关闭相机
/// </summary>
void Close();
/// <summary>
/// 软触发拍照
/// </summary>
/// <param name="frameId">帧ID</param>
/// <returns>是否成功</returns>
bool Trigger(long frameId);
/// <summary>
/// 设置参数
/// </summary>
/// <param name="paramName">参数名</param>
/// <param name="value">参数值</param>
/// <returns>是否成功</returns>
bool SetParameter(string paramName, object value);
/// <summary>
/// 获取参数
/// </summary>
/// <param name="paramName">参数名</param>
/// <returns>参数值</returns>
object GetParameter(string paramName);
}
/// <summary>
/// 相机信息
/// </summary>
public class CameraInfo
{
public string CameraId { get; set; }
public string CameraName { get; set; }
public string Manufacturer { get; set; }
public int Width { get; set; }
public int Height { get; set; }
}
/// <summary>
/// 图片帧
/// </summary>
public class ImageFrame
{
public long FrameId { get; set; }
public Bitmap Image { get; set; }
public int Width { get; set; }
public int Height { get; set; }
public DateTime CaptureTime { get; set; }
public Dictionary<string, object> ExtendInfo { get; set; } = new();
}
所有品牌的相机都实现这个接口,上层业务逻辑不用关心底层用的什么相机。
第二步:实现具体相机适配类
以海康相机为例,只需要实现ICamera接口就行,核心代码如下:
/// <summary>
/// 海康相机实现类
/// </summary>
public class HikCamera : ICamera
{
private IntPtr _hCamera = IntPtr.Zero;
private bool _isOpened = false;
public string CameraId { get; private set; }
public string CameraName { get; set; }
public bool IsOpened => _isOpened;
public event Action<ImageFrame> OnImageCaptured;
public List<CameraInfo> EnumCameras()
{
// 调用海康SDK枚举相机
var list = new List<CameraInfo>();
// 海康SDK枚举逻辑…
return list;
}
public bool Open(string cameraId)
{
CameraId = cameraId;
// 调用海康SDK打开相机
// …
// 注册取图回调
MvCamera.MV_CC_RegisterImageCallBackEx_NET(_hCamera, ImageCallBack, IntPtr.Zero);
_isOpened = true;
return true;
}
private void ImageCallBack(IntPtr pData, ref MvFrameInfo frameInfo, IntPtr pUser)
{
// 把海康的原始数据转成ImageFrame
var frame = new ImageFrame
{
FrameId = frameInfo.nFrameNum,
Width = frameInfo.nWidth,
Height = frameInfo.nHeight,
CaptureTime = DateTime.Now
};
// 拷贝图片数据…
OnImageCaptured?.Invoke(frame);
}
public bool Trigger(long frameId)
{
// 调用海康SDK软触发
MvCamera.MV_CC_SoftTrigger_CMD_NET(_hCamera);
return true;
}
// 其他接口实现…
public void Close() { /* 关闭相机 */ }
public bool SetParameter(string paramName, object value) { /* 设置参数 */ }
public object GetParameter(string paramName) { /* 获取参数 */ }
public void Dispose() { /* 释放资源 */ }
}
大华、巴斯勒等其他品牌的相机也是一样的写法,每个相机适配类代码量也就200-300行,非常好写。
第三步:定义IInferEngine推理引擎抽象接口
和相机一样,不管什么版本的YOLO,什么部署方式,核心功能都是加载模型、推理,返回检测结果,接口定义如下:
/// <summary>
/// 推理引擎抽象接口
/// </summary>
public interface IInferEngine : IDisposable
{
/// <summary>
/// 模型名称
/// </summary>
string ModelName { get; }
/// <summary>
/// 是否已加载
/// </summary>
bool IsLoaded { get; }
/// <summary>
/// 推理输入尺寸
/// </summary>
Size InputSize { get; }
/// <summary>
/// 加载模型
/// </summary>
/// <param name="modelPath">模型路径</param>
/// <param name="config">配置参数</param>
/// <returns>是否成功</returns>
bool LoadModel(string modelPath, Dictionary<string, object> config = null);
/// <summary>
/// 推理检测
/// </summary>
/// <param name="image">输入图片</param>
/// <param name="threshold">置信度阈值</param>
/// <returns>检测结果列表</returns>
List<DetectResult> Detect(Bitmap image, float threshold = 0.5f);
}
/// <summary>
/// 检测结果
/// </summary>
public class DetectResult
{
public string ClassName { get; set; }
public int ClassId { get; set; }
public float Confidence { get; set; }
public Rectangle BoundingBox { get; set; }
public Dictionary<string, object> ExtendInfo { get; set; } = new();
}
第四步:实现具体推理引擎类
以YOLOv8 ONNX Runtime部署为例,实现IInferEngine接口:
/// <summary>
/// YOLOv8 ONNX推理引擎实现
/// </summary>
public class Yolov8OnnxEngine : IInferEngine
{
private SessionOptions _sessionOptions;
private InferenceSession _inferenceSession;
private bool _isLoaded = false;
private string[] _classNames;
public string ModelName { get; private set; }
public bool IsLoaded => _isLoaded;
public Size InputSize { get; private set; } = new Size(640, 640);
public bool LoadModel(string modelPath, Dictionary<string, object> config = null)
{
ModelName = Path.GetFileNameWithoutExtension(modelPath);
// 初始化ONNX Runtime会话
_sessionOptions = new SessionOptions();
_sessionOptions.AppendExecutionProvider_CPU(0); // 或者用GPU
_inferenceSession = new InferenceSession(modelPath, _sessionOptions);
// 加载类别名称
_classNames = LoadClassNames(config);
_isLoaded = true;
return true;
}
public List<DetectResult> Detect(Bitmap image, float threshold = 0.5f)
{
var results = new List<DetectResult>();
// 图片预处理:Resize、归一化、转Tensor
var inputTensor = Preprocess(image);
// 推理
var outputs = _inferenceSession.Run(new List<NamedOnnxValue>
{
NamedOnnxValue.CreateFromTensor("images", inputTensor)
});
// 后处理:解码、NMS
results = Postprocess(outputs, threshold);
return results;
}
// 预处理、后处理实现…
private Tensor Preprocess(Bitmap image) { /* 图片预处理 */ }
private List<DetectResult> Postprocess(IDisposableReadOnlyCollection<DisposableNamedOnnxValue> outputs, float threshold) { /* 结果后处理 */ }
public void Dispose()
{
_inferenceSession?.Dispose();
_sessionOptions?.Dispose();
}
}
如果要切换到YOLOv5或者TensorRT部署,只需要再写一个对应的实现类就行,上层业务逻辑完全不用改。
第五步:上层业务逻辑调用示例
上层业务逻辑完全不依赖具体实现,只依赖接口,通过配置文件或者反射动态创建实例,实现硬件切换零代码修改:
// 从配置文件读取使用的相机类型和推理引擎类型
string cameraType = Config.Get("Camera:Type", "Hik");
string engineType = Config.Get("InferEngine:Type", "Yolov8Onnx");
// 反射创建实例(或者用依赖注入容器)
ICamera camera = Activator.CreateInstance(Type.GetType($"MyNamespace.{cameraType}Camera")) as ICamera;
IInferEngine engine = Activator.CreateInstance(Type.GetType($"MyNamespace.{engineType}Engine")) as IInferEngine;
// 打开相机,加载模型
camera.Open(Config.Get("Camera:Id"));
engine.LoadModel(Config.Get("InferEngine:ModelPath"));
// 注册取图回调,直接处理
camera.OnImageCaptured += (frame) =>
{
// 推理
var results = engine.Detect(frame.Image);
// 业务逻辑处理:结果存数据库、发PLC、显示到UI
ProcessResults(frame.FrameId, results);
// 释放资源
frame.Image.Dispose();
};
下次换相机或者换模型,只需要改配置文件就行,一行代码都不用改。
框架扩展和优化
写在最后
这套框架我已经在十几个工业视觉项目里验证过了,兼容海康、大华、巴斯勒、虚拟相机,兼容YOLOv5/v8/v9、ONNX/TensorRT/OpenCV DNN等多种部署方式,开发效率至少提升50%,再也不用为了换硬件改一大堆代码。
框架的核心思想就是"高内聚低耦合",把易变的部分(硬件、模型)抽象出来,上层稳定的业务逻辑不依赖易变部分,这也是所有可扩展架构的核心思路。


