目标:实现一个
DlcvDemo3WinForms Demo。程序直接复制DlcvDemo2的项目结构和窗体布局后改写,业务逻辑固定为“两模型串联 + 原图固定裁图 + 模型2 batch 推理 + 结果统一回写原图坐标”。
- 软件名称:
C# 测试程序3 - 项目名称:
DlcvDemo3 - 解决方案:
OpenIVS.sln - 技术栈:
.NET Framework 4.7.2、WinForms、OpenCvSharp、Newtonsoft.Json、DlcvCsharpApi、ImageViewer、PressureTestRunner - 功能定位:面向固定业务场景的快速 Demo,执行“原图 -> 模型1推理 -> 读取每个定位结果中心点 -> 在原图上按中心点裁固定大小
128 x 192ROI -> 模型2按 batch 推理 -> 模型2结果回写原图坐标 -> 只输出模型2结果”
本程序不是通用编辑器,也不是多模型路由平台。程序只围绕两个固定模型工作:
模型1(定位):负责在整图上产出目标位置模型2(识别):负责对每个目标的固定尺寸局部图做 batch 推理
当前业务给定的模型文件如下,界面不把它们写死为默认值,仍然由用户自行选择与加载:
C:\Users\Administrator\Desktop\dvst速度优化\流程1-目标检测_120_50.dvstC:\Users\Administrator\Desktop\dvst速度优化\流程2-各项检测_120_50.dvst
程序直接复制 DlcvDemo2 项目并改写,不抽公共基类,不抽公共工具类。与当前业务强相关的推理管线实现放在 DlcvDemo3/Demo3Pipeline.cs(供 WinForms 与命令行基准共用);DlcvDemo3/Form1.cs 负责界面、模型加载以及调用 Demo3Pipeline.Run。模型2仅加载 一个 Model 实例,多线程并发 InferBatch 时共享该实例(与压力测试程序行为一致,由底层 SDK 支持并行推理)。
界面继续使用 DlcvDemo2 的“顶部参数区 + 下方左右分栏”布局,界面中固定以下内容:
模型1路径模型2路径图片路径固定裁图大小: 128 x 192模型2线程数(1-32)(默认值:4)释放模型
完整实现见 DlcvDemo3/Demo3Pipeline.cs 中的 Demo3Pipeline.Run,流程如下:
- 模型1整图
Infer,提取目标并做中心裁图(固定128 x 192),得到cropContexts。 - 读取
batchLimit = model2.GetMaxBatchSize(),将裁图列表按batchLimit切成多段chunks。 - 模型2并发:使用
System.Threading.Tasks.Task+ 线程池,按threadIndex轮询领取 chunk(c = tid; c < chunks.Count; c += threadCount)。所有任务共享同一个model2实例调用InferBatch(底层支持并行时无需多份模型)。 - 各段结果按 chunk 顺序合并回
FinalObjects(保持与单线程分段顺序一致),再组装DisplayResult。
性能要点:
- 错误做法:在每次推理或每个并行任务里临时
new Model(model2Path)。.dvst会解包并加载整套子模型,成本极高。 - 线程数:
threads上限受 batch 段数chunks.Count约束(不会超过段数)。在 SDK 支持单实例并行推理时,可将线程数调到接近ceil(裁图数 / batchLimit)(例如裁图336、batch=32时共11段)以拉高吞吐;具体最优值以本机压测为准。
固定尺寸中心裁图逻辑见 Demo3Pipeline.BuildCenteredCropContext(与下述伪代码一致):
private CenteredCropContext BuildCenteredCropContext(Mat fullImageRgb, Point2d center, int cropW, int cropH)
{
int requestLeft = (int)Math.Round(center.X - cropW / 2.0);
int requestTop = (int)Math.Round(center.Y - cropH / 2.0);
Rect requestedRect = new Rect(requestLeft, requestTop, cropW, cropH);
Rect imageRect = new Rect(0, 0, fullImageRgb.Width, fullImageRgb.Height);
Rect srcRect = Intersect(requestedRect, imageRect);
if (srcRect.Width <= 0 || srcRect.Height <= 0)
{
return CenteredCropContext.Invalid("裁图完全落在图像外");
}
Mat crop = new Mat(new Size(cropW, cropH), fullImageRgb.Type(), Scalar.Black);
Rect dstRect = new Rect(srcRect.X - requestLeft, srcRect.Y - requestTop, srcRect.Width, srcRect.Height);
using (var srcView = new Mat(fullImageRgb, srcRect))
using (var dstView = new Mat(crop, dstRect))
{
srcView.CopyTo(dstView);
}
return new CenteredCropContext
{
IsValid = true,
CropRgb = crop,
RequestedRect = requestedRect,
CropToFullAffine = new[] { 1.0, 0.0, (double)requestLeft, 0.0, 1.0, (double)requestTop }
};
}该设计有几个关键点:
- Demo 直接使用模型1输出结果。
- 固定尺寸裁图始终在原图
fullImageRgb上执行。 - 模型1只负责提供“目标中心点”,不参与最终结果输出。
- 每个裁图都强制保持
128 x 192,这样模型2才能稳定走InferBatch(List<Mat>)。 - 裁图越界时不缩小尺寸,而是保留固定尺寸并对越界区域补黑。
- 每个裁图都保存一份
CropToFullAffine,当前实现是纯平移矩阵。 - 模型2的输出类别、分数、框型都按原样保留。
- 最终结果列表只包含模型2回写结果。
- 操作系统:
Windows 10/11 x64 - 编译平台:
x64 - 解决方案级构建、项目级构建与发布前构建验证统一通过 MCP 构建工具执行,入口见
开发文档.md的“统一编译说明” - 启动项目:
DlcvDemo3 - 主窗体类型:
WinForms Form - 图像显示控件:
DLCV.ImageViewer - 依赖项目:
DlcvCsharpApi、ImageViewer、PressureTestRunner
- 模型1、模型2都通过
new Model(path, 0, false)加载。 device_id固定为0。- 不提供 GPU/CPU 切换。
- 不提供 RPC 模式开关。
- 不写死默认模型路径,不自动在启动时加载模型。
- 支持后缀:
*.dvt、*.dvo、*.dvp、*.dvst、*.dvso、*.dvsp - 两个模型都使用同一套文件过滤器。
- 模型2统一使用
InferBatch(List<Mat>)。 - batch 大小不提供 UI 配置,直接自动读取
model2.GetMaxBatchSize()。 - 若模型返回的最大 batch 小于等于
1,则自动退化为逐张推理,但仍走统一的 batch 代码路径。 - 因为所有局部图尺寸固定为
128 x 192,不需要像通用封装那样再按 shape 分桶。 - 模型2批次推理使用线程池并发执行,线程数可在 UI 中设置,默认
4,范围1~32。 - 仅保留 一个 模型2
Model实例;多线程同时对同一实例调用InferBatch,依赖 SDK 内部并行(与压力测试一致),不为每个线程再new Model。
- 读取图片时使用
Cv2.ImRead(path, ImreadModes.Unchanged),保留磁盘图像的原始通道数。 - 图像区显示使用解码后的原始
Mat,不在界面层额外改通道顺序。 - 送入模型1/模型2前由
DlcvDemo3自己完成输入整理:- 灰度图:直接按单通道送入模型。
- 三通道图:从 OpenCV 的
BGR转为RGB后送入模型。 - 四通道图:从
BGRA转为RGB后送入模型。
DlcvCsharpApi.Model与 Flow 入口不负责BGR/BGRA -> RGB颜色顺序转换;Demo 侧负责颜色图整理为 RGB,接口再按模型输入自动做灰度补三通道或彩色压灰度的最小通道规整。- 代码里的
imageRgb、fullImageRgb等命名沿用旧字段名;当输入是灰度图时,这些变量实际承载的是“送入模型的图像”,不强制为三通道。
- 允许分别选择
模型1(定位)与模型2(识别) - 两个模型路径右侧都有独立的
加载模型按钮 - 允许选择单张图片并执行一次推理
- 推理过程在后台线程执行,UI 保持响应
- 提供进度条和状态文本
- 文本区输出推理摘要
- 图像区展示最终回写到整图坐标后的全部结果
- 支持释放当前已加载模型
- 速度测试(见 5.6)
- 不提供裁图宽高输入框,固定使用
128 x 192 - 不提供 batch_size 输入框
- 提供模型2线程数输入框(默认
4) - 不提供启动时写死默认路径
- 不提供文件夹批量推理
- 不提供 JSON 输出按钮
- 不抽公共基类
- 不抽公共工具类
- 窗口标题:
C# 测试程序3 - 启动位置:屏幕居中
- 最小尺寸:宽度不低于
1200,高度不低于900 - 默认字体:与
DlcvDemo2保持一致
界面继续采用“顶部参数区 + 下方左右分栏”的结构,但删去 DlcvDemo2 中的滑窗参数区:
+------------------------------------------------------------------------------------------------------------------+
| 模型1路径(定位) [......................................] [浏览...] [加载模型] |
| 模型2路径(识别) [......................................] [浏览...] [加载模型] |
| 图片路径 [.............................................] [浏览...] [执行推理] [速度测试] |
| 固定裁图大小: 128 x 192 [释放模型] |
| 推理进度 [###############-----------------------] 42% 状态: 模型2 batch 推理 18/50 |
|------------------------------------------------------------------------------------------------------------------|
| 结果文本区 | 图像可视化区 |
| - 模型加载信息 | - 原图显示 |
| - 模型1目标数量 | - 最终结果框/标签 |
| - 裁图数量 / batch 数 | - 鼠标缩放与拖拽 |
| - 最终结果摘要 | |
+------------------------------------------------------------------------------------------------------------------+
- 文本框:
模型1路径 - 按钮:
浏览... - 按钮:
加载模型 - 文本框:
模型2路径 - 按钮:
浏览... - 按钮:
加载模型 - 文本框:
图片路径 - 按钮:
浏览... - 按钮:
执行推理 - 按钮:
速度测试(开关:运行中显示为停止) - 标签:
固定裁图大小: 128 x 192 - 按钮:
释放模型 - 进度条:
推理进度 - 状态文本:
推理状态 - 文本输出区:
richTextBox1 - 图像显示区:
imagePanel1
启动时状态如下:
- 两个模型路径文本框默认留空
- 图片路径默认留空
- 进度条显示
0% - 状态文本显示
空闲 - 不在界面里预填给定的那两个
.dvst路径
- 点击
浏览...选择模型1文件。 - 路径写入文本框。
- 点击
加载模型后才真正实例化模型1。 - 加载成功后,文本区显示“模型1加载成功”与完整路径。
- 加载失败时,文本区显示异常信息。
- 点击
浏览...选择模型2文件。 - 路径写入文本框。
- 点击
加载模型后实例化模型2。 - 加载成功后,文本区显示“模型2加载成功”与完整路径。
- 加载失败时,文本区显示异常信息。
- 点击
浏览...选择待推理图片。 - 图片路径写入文本框。
- 选择图片后不自动推理。
- 用户点击
执行推理后开始本次推理。
- 点击
执行推理后,先校验模型1、模型2、图片是否就绪。 - 任一项缺失时,文本区给出明确提示,不进入推理。
- 推理逻辑在后台线程执行,避免阻塞 UI。
- 推理过程中禁止重复点击进入。
- 进度至少覆盖以下阶段:
- 读取图片
- 模型1整图推理
- 在原图上固定尺寸裁图
- 模型2 batch 推理
- 整理结果与刷新显示
- 推理成功后:
- 图像区显示原图与最终结果
- 文本区显示本次摘要
- 当前图像保留在界面中,支持重复执行
- 点击
释放模型后,释放模型1、模型2的Model实例。 - 文本框中的路径不强制清空。
- 文本区输出释放完成信息。
- 若速度测试正在运行,应先停止测试再释放(实现上可在释放前自动停止)。
- 按钮为开关:未运行时显示
速度测试,运行中显示停止。 - 前置条件与 5.4 执行推理 相同(模型1、模型2、图片路径有效)。
- 启动时从磁盘读取当前图片,并按与 5.4 相同的规则整理成推理输入:灰度图直接送入,三通道图转为
RGB,四通道图转为RGB;整理后的图像保留在内存中供循环使用;每次回调仅执行Demo3Pipeline.Run(与 5.4 使用相同的管线,模型2线程数 取启动瞬间界面模型2线程数的值,与当次「执行推理」一致)。 - 使用
PressureTestRunner:ThreadCount=1、batchSize=1、targetRate取较大值以尽量满载;统计文本由GetStatistics(false)刷新,定时器周期500ms。 - 运行中仅更新文本区统计,不刷新图像区;关闭窗口前须先停止测试。
- 原始输入始终是送入模型1的整张推理图;代码变量名沿用
fullImageRgb,但当输入图片本身是灰度图时,这里保持单通道。 - 模型1只执行一次推理,不做滑窗,不做多路分流。
- Demo 直接使用模型1输出结果作为后续裁图锚点。
- 模型1结果不直接进入最终结果。
中心点提取规则如下:
- 如果结果是普通框:
bbox = [x, y, w, h]center_x = x + w / 2center_y = y + h / 2
- 如果结果是旋转框:
bbox = [cx, cy, w, h, angle]center_x = cxcenter_y = cy
模型1输出结果在当前阶段只用于提供中心位置。
- 固定裁图尺寸写死为:
crop_width = 128crop_height = 192
- 每个模型1结果都会在原图上生成一张局部图。
- 裁图方式是“以中心点为中心的固定窗口”,不是按模型1框宽高裁图。
- 裁图源图始终是送入模型1的整张推理图;代码变量名沿用
fullImageRgb,灰度图场景下保持单通道。 - 如果裁图超出原图边界:
- 仍然保持输出 Mat 尺寸为
128 x 192 - 超出边界的区域用黑色填充
- 有效图像区域拷贝到对应偏移位置
- 仍然保持输出 Mat 尺寸为
这样做的原因是:
- 保证模型2所有输入尺寸完全一致,方便 batch
- 保证模型2永远在原图细节上做检测
- 保证坐标换算简单稳定
- 避免边缘目标因裁图缩小而破坏 batch 维度一致性
- 将全部
128 x 192局部图按model2.GetMaxBatchSize()分块。 - 对每一块调用一次
model2.InferBatch(chunkMats, inferParams)。 batchResult.SampleResults[i]与chunkMats[i]一一对应。- 某个 crop 无结果时,不产生该 crop 的最终结果。
- 模型2输出最初位于“局部 crop 坐标系”。
- 当前设计下,crop 到整图只存在平移关系,因此回写矩阵固定为:
[1, 0, requestLeft, 0, 1, requestTop]
- 普通框回写后等价于:
full_x = local_x + requestLeftfull_y = local_y + requestTop
- 旋转框回写后等价于:
full_cx = local_cx + requestLeftfull_cy = local_cy + requestTopw / h / angle保持不变
- 最终结果只保留模型2回写后的对象。
- 当前版本不做跨 crop 的最终去重。
最终结果组织为单一整图结果列表,用于直接驱动 imagePanel1.UpdateImageAndResult(...)。
每条结果至少包含:
category_namescorebbox或rotated_bbox- 整图坐标
- 图像区始终显示原图。
- 可视化对象只使用模型2回写后的最终结果。
- 不展示模型1中间框。
- 不展示中间裁图。
- 标签显示沿用
DLCV.ImageViewer:默认格式为{category_name} {score:F2},可按C在类别+分数、仅类别、不显示三种模式间循环;+/-/0与Ctrl + 滚轮调整标签字体倍率。
文本区显示:
- 当前图片路径
- 模型1路径
- 模型2路径
- 固定裁图大小:
128 x 192 - 模型1目标数量
- 成功生成的裁图数量
- 模型2 batch 大小上限
- 模型2最终结果数量
- 推理耗时
- 结果明细
- 日志与异常信息
图片: D:\samples\board01.jpg
模型1: C:\Users\Administrator\Desktop\dvst速度优化\流程1-目标检测_120_50.dvst
模型2: C:\Users\Administrator\Desktop\dvst速度优化\流程2-各项检测_120_50.dvst
固定裁图大小: 128 x 192
模型1目标数: 42
有效裁图数: 42
模型2最大Batch: 8
最终结果数: 135
推理耗时: 68.37 ms
[1] 类别A score=0.98 rect=(1240.0, 860.0, 18.0, 16.0)
[2] 类别B score=0.95 rect=(1256.0, 871.0, 12.0, 14.0)
[3] 类别C score=0.91 rect=(1468.0, 802.0, 20.0, 22.0)
- 模型1无结果:文本区提示“模型1未检测到目标”,图像区仅显示原图
- 模型1结果框无效或完全落在图外:丢弃该条结果
- 某个中心点裁图完全落在图外:跳过该目标并记日志
- 裁图部分越界:补黑,不中断当前推理
- 模型2某个 batch 推理失败:记录异常,继续后续 batch
- 模型2某个 sample 无结果:该 sample 不产出任何最终结果
- 回写后结果完全落在图外:丢弃
- 回写后结果部分越界:裁剪到图像边界后再显示
- 推理进行中再次点击:提示“当前正在推理”
- 模型加载失败:文本区显示异常,不崩溃
- 图片解码失败:文本区显示异常,不崩溃
程序包含以下文件:
DlcvDemo3/DlcvDemo3.csprojDlcvDemo3/Form1.csDlcvDemo3/Form1.Designer.csDlcvDemo3/Program.csDlcvDemo3/Properties/AssemblyInfo.csDlcvDemo3/Properties/app.manifestOpenIVS.sln
Form1.cs 直接包含以下内容,不再拆到公共类或公共工具文件中:
- 模型字段:
model1、model2 - 状态字段:
imagePath、isInferenceRunning;速度测试相关字段(如PressureTestRunner、定时器、测速用推理输入图、启动时冻结的模型2线程数等) - 私有内部类:
PipelineRunResult - 私有内部类:
CenteredCropContext - 私有内部类:
InferenceProgressInfo - 私有方法:模型加载、模型释放、图片选择、执行推理
- 私有方法:
TryClampObjectToImage - 私有方法:
GetObjectCenter - 私有方法:
BuildCenteredCropContext - 私有方法:模型2 batch 推理与结果回写
- 私有方法:结果裁剪、文本输出、图像刷新
- 项目名为
DlcvDemo3 - 窗口标题为
C# 测试程序3 - 界面整体风格与
DlcvDemo2接近 - 界面上有两个模型选择入口,且各自带
加载模型按钮 - 启动时不预填默认模型路径
- 固定裁图大小为
128 x 192,且不暴露可编辑输入框 - 固定裁图始终在原图上执行
- 模型1只跑一次,不做滑窗
- 每个模型1结果都能正确换算中心点
- 边缘目标裁图越界时仍保持
128 x 192 - 模型2通过
InferBatch执行,不是简单串行Infer - 模型2结果能正确回写到原图坐标
- 最终结果只包含模型2结果,不包含模型1结果
- 文本区能看到模型1目标数、裁图数、batch 上限、最终结果数
- 图像区只展示最终回写结果