摘要
在非标机器视觉项目中,产品型号一旦从单一型号扩展到多个型号,软件复杂度往往会快速上升。相机曝光、视觉参数、ROI、检测阈值、PLC地址、存图规则等配置如果散落在代码和界面控件中,后期维护会非常困难。本文结合工业视觉软件实际开发场景,系统介绍一套基于 C# 的 Recipe 配方管理设计思路,包括配方数据结构、运行时快照、参数切换、版本管理、导入导出及异常回滚等内容。
1. 为什么视觉软件一定要设计Recipe
很多视觉软件第一个版本通常只有一个产品。
程序里直接写:
Exposure = 3500;
Threshold = 80;
Score = 0.75;
或者:
if (productType == "A")
{
score = 0.75;
}
else if (productType == "B")
{
score = 0.82;
}
产品只有两个的时候似乎问题不大。
但实际产线很容易变成:
A01
A02
A03
B01
B02
C01
C02
...
而且不同型号之间改变的并不只是一个阈值。
可能同时涉及:
相机曝光
相机增益
触发模式
ROI
模板文件
测量参数
深度学习模型
PLC地址
结果映射
存图路径
检测项启用状态
如果这些参数全部散落在:
Form控件
配置文件
代码常量
VisionPro ToolBlock
HALCON程序
PLC逻辑
项目后期基本一定会失控。
所以我现在更倾向于把:
一个产品型号运行所需要的全部参数
统一定义成一个:
Recipe
2. Recipe到底应该保存什么
Recipe并不只是:
算法参数。
对于一套完整的工业视觉系统,我更建议把它理解成:
一个产品型号的完整运行环境。
例如:
Recipe
│
├─ Product
│
├─ Camera
│
├─ Vision
│
├─ Communication
│
├─ Save
│
└─ Runtime
C#可以设计成:
public class Recipe
{
public string Name { get; set; }
public string ProductCode { get; set; }
public int Version { get; set; }
public CameraRecipe Camera { get; set; }
public VisionRecipe Vision { get; set; }
public CommunicationRecipe Communication { get; set; }
public SaveRecipe Save { get; set; }
}
不要把所有字段全部塞进一个类。
否则最后会出现:
public class Recipe
{
public double Camera1Exposure;
public double Camera2Exposure;
public double Camera3Exposure;
public double Score1;
public double Score2;
public int PlcAddress1;
public string SavePath;
// 几百个参数......
}
这种结构后期非常难维护。
3. 相机参数应该独立管理
例如:
public class CameraRecipe
{
public List<CameraParameter> Cameras { get; set; }
public CameraRecipe()
{
Cameras = new List<CameraParameter>();
}
}
单个相机:
public class CameraParameter
{
public string CameraName { get; set; }
public double ExposureTime { get; set; }
public double Gain { get; set; }
public bool TriggerMode { get; set; }
public int TriggerDelay { get; set; }
}
这样以后不管项目是:
1台相机
4台相机
9台相机
Recipe结构都不需要重新设计。
4. 视觉参数也不要直接绑定UI控件
这是一个比较常见的问题。
例如:
double score =
Convert.ToDouble(txtScore.Text);
然后算法直接读取:
txtScore
这相当于:
视觉算法依赖UI。
后期很容易出现问题。
正确关系应该是:
UI
↓
Recipe
↓
VisionEngine
而不是:
VisionEngine
↓
UI Control
例如:
public class VisionRecipe
{
public double MatchScore { get; set; }
public double Threshold { get; set; }
public bool EnableOCR { get; set; }
public bool EnableMeasurement { get; set; }
public string ModelFile { get; set; }
}
算法只读取:
recipe.Vision.MatchScore;
而不是:
txtMatchScore.Text;
5. Recipe文件建议使用什么格式
对于中小型视觉项目,我一般更喜欢:
JSON
原因很简单。
可读。
可复制。
可比较。
可版本管理。
也方便人工检查。
例如:
{
"Name": "Product_A",
"ProductCode": "A001",
"Version": 3,
"Vision": {
"MatchScore": 0.82,
"Threshold": 75,
"EnableOCR": true,
"EnableMeasurement": true,
"ModelFile": "Models/A001.vpp"
}
}
如果配方数量特别多,或者需要:
复杂查询
权限
历史版本
修改记录
也可以使用 SQLite。
但不要为了"高级"而强行上数据库。
如果项目只有:
10~30个型号
JSON往往已经足够。
6. Recipe目录怎么设计
我比较推荐:
Recipes
│
├─ Product_A
│ ├─ Recipe.json
│ ├─ Vision.vpp
│ ├─ Model.shm
│ └─ Template.bmp
│
├─ Product_B
│ ├─ Recipe.json
│ ├─ Vision.vpp
│ └─ Template.bmp
│
└─ Product_C
这样一个产品需要的文件全部放在一起。
而不是:
Config
Models
Images
VPP
Halcon
然后不同型号的文件全部混在一起。
项目一多以后非常容易拿错文件。
7. 配方切换最危险的问题是什么
很多程序切换型号的时候直接:
_currentRecipe =
LoadRecipe(name);
然后立刻开始生产。
但一个完整配方切换可能包含:
停止检测
停止触发
加载Recipe
加载模板
加载VPP
加载DL模型
更新相机曝光
更新PLC参数
初始化检测工具
检查结果
重新允许生产
这其实是一个:
事务过程。
只要中间任何一步失败,都不应该让设备继续生产。
8. 配方切换建议采用状态机
例如:
Idle
↓
Loading
↓
ApplyingCamera
↓
LoadingVision
↓
Validating
↓
Ready
失败则:
Error
伪代码:
public bool SwitchRecipe(string recipeName)
{
Recipe newRecipe = null;
try
{
SetState("Loading");
newRecipe =
_recipeManager.Load(recipeName);
SetState("ApplyingCamera");
ApplyCameraParameters(newRecipe);
SetState("LoadingVision");
LoadVisionProgram(newRecipe);
SetState("Validating");
ValidateRecipe(newRecipe);
_currentRecipe = newRecipe;
SetState("Ready");
return true;
}
catch (Exception ex)
{
WriteLog(ex.ToString());
SetState("Error");
return false;
}
}
9. 不建议直接修改正在运行的Recipe
例如程序正在检测产品A。
操作员打开设置页面,把:
MatchScore
从:
0.75
改成:
0.85
如果算法直接读取同一个对象,就可能出现:
同一批产品前一半使用0.75,后一半使用0.85。
这对于追溯来说非常麻烦。
所以我更推荐:
编辑Recipe和运行Recipe分开。
10. 使用Runtime Snapshot
运行时可以复制一份:
public class RuntimeRecipe
{
public string RecipeName { get; set; }
public int Version { get; set; }
public double MatchScore { get; set; }
public double Threshold { get; set; }
}
当开始运行一个产品时:
Recipe
↓
Create Snapshot
↓
Inspection
后面用户即使修改配置,也不会影响当前产品。
只有下一次重新应用Recipe后才生效。
11. Recipe一定要有版本号
例如:
Product_A
Version 1
Version 2
Version 3
为什么?
因为客户经常会问:
上周这个产品为什么检测OK,今天为什么NG?
如果只保存:
Product_A
你根本不知道当时使用的是哪套参数。
建议每一次保存Recipe都增加:
Version
同时日志记录:
Recipe = Product_A
Version = 12
这样以后追溯非常方便。
12. 推荐保存修改记录
可以定义:
public class RecipeChangeRecord
{
public DateTime Time { get; set; }
public string User { get; set; }
public string RecipeName { get; set; }
public int OldVersion { get; set; }
public int NewVersion { get; set; }
public string Description { get; set; }
}
例如:
2026-09-12 10:35
User: Engineer
Recipe: Product_A
V12 → V13
MatchScore:
0.78 → 0.82
真正到了量产阶段,这种功能的价值非常高。
13. Recipe参数最好分权限
不是所有人都应该能修改全部参数。
例如:
Operator
只允许:
切换型号
启动
停止
Engineer:
修改阈值
修改ROI
重新训练模板
Administrator:
修改通讯
删除Recipe
系统设置
这不是为了把软件做复杂。
而是因为工业现场最怕:
不知道谁改了一个参数。
14. Recipe保存前必须校验
不要用户点击:
保存
就直接写文件。
应该先验证:
private bool ValidateRecipe(
Recipe recipe,
out string error)
{
if (recipe.Vision.MatchScore < 0 ||
recipe.Vision.MatchScore > 1)
{
error = "MatchScore必须位于0~1。";
return false;
}
if (recipe.Camera == null)
{
error = "Camera配置不存在。";
return false;
}
error = null;
return true;
}
还可以检查:
模板文件是否存在
VPP是否存在
模型是否加载成功
相机参数是否合法
路径是否存在
防止保存一套根本不能运行的Recipe。
15. 保存时不要直接覆盖原文件
更可靠的流程:
Recipe.json
↓
Recipe.tmp
↓
验证写入成功
↓
替换Recipe.json
甚至可以保留:
Recipe.bak
这样软件突然断电时,不容易造成:
JSON只写了一半
最终整个配方损坏。
16. Recipe导入导出非常值得做
客户经常会有:
设备A
设备B
设备C
如果同一个型号需要复制过去,不应该让工程师重新调一遍参数。
可以支持:
Export Recipe
导出:
Product_A.recipe
内部其实可以是ZIP:
Recipe.json
Vision.vpp
Template.bmp
Model文件
导入时:
检查版本
检查文件完整性
解压
注册Recipe
这对于设备复制和售后维护非常方便。
17. Recipe和算法版本最好分开
例如:
RecipeVersion = 15
并不代表:
AlgorithmVersion = 15
因为可能只是:
曝光修改
并没有改算法。
所以建议记录:
RecipeVersion
VisionProgramVersion
SoftwareVersion
日志例如:
Software = 2.3.1
Recipe = A001 V15
Vision = VPP 3.2
以后出了问题,可以准确还原当时的软件环境。
18. 一个比较完整的Recipe架构
最终可以设计成:
RecipeManager
│
├─ Load
├─ Save
├─ Delete
├─ Copy
├─ Import
├─ Export
├─ Validate
└─ VersionControl
│
↓
Recipe
│
├─ Camera
├─ Vision
├─ PLC
├─ Save
└─ Product
│
↓
RuntimeRecipe
│
↓
Inspection
UI永远只操作:
RecipeManager
而不要自己到处:
File.ReadAllText()
19. 一个好的Recipe系统解决的不是"保存参数"
Recipe真正解决的是:
产品管理
参数一致性
版本追溯
软件维护
设备复制
权限管理
异常恢复
所以我现在认为:
Recipe不是视觉软件的附加功能,而是工业视觉软件的核心模块之一。
很多项目刚开始为了赶进度觉得:
以后再做。
最后往往会发现,后面补Recipe比一开始设计好困难很多。
总结
对于只有一个产品的Demo来说:
配置文件 + 几个TextBox
完全够用。
但只要项目进入:
多型号
长期运行
多人维护
多设备复制
参数追溯
阶段,就非常有必要建立一个完整的Recipe系统。
真正成熟的Recipe系统应该具备:
结构化参数
版本管理
切换状态机
运行快照
合法性检查
权限控制
修改记录
导入导出
异常回滚
这些功能看起来和视觉算法没有直接关系。
但它们恰恰决定了一套视觉软件:
是一个调试程序,还是一个真正可以长期维护的工业软件。