双工具串联:gray_mean_demo(OpenCV)
标准示例文档:image_load 收图 → Link 传句柄 → gray_mean 求均值;客户直链 opencv_world 的写法示范。
用途
用 OpenCV 跑通两个工具之间传图像的最短路径:ImageLoadTool 把图片读成 ImageGray8 句柄,经 Link 流进 GrayMeanTool 求灰度均值。host 只在包外换 ImagePath,画像数据不出工具组。
读完应能回答三个问题:图像在工具间以什么形态流转?Link 的类型检查卡在哪?客户自己的扩展工具该怎么写 OpenCV 代码?
源码
examples/gray_mean_tool.cpp—— 全部:两个工具 +main()建组连线循环(片段 1–5) · 查看完整源文件examples/CMakeLists.txt——gray_mean_demo目标:链接Vyntek::toolgroup+opencv_world,VYNTEK_WITH_OPENCV=OFF时跳过 · 查看完整源文件examples/README.md—— 链路图与交付物清单 · 查看完整源文件
下文片段均为源码节选,标注行号,与仓库当前源码逐字一致。
学习路线
按四步读,每步对应下文片段:
- 句柄怎么进出 Mat(片段 1):
OpenRead逐行拷进连续cv::Mat,不碰 stride 负值坑。 - 收图工具怎么写(片段 2):
cv::imread→ 转灰 →PixelImage::Copy一步产出可发布的ReadOnly句柄。 - 算子工具怎么写(片段 3):从
InputImage拿句柄 → 转 Mat →cv::mean,只读不写。 - 组怎么连起来(片段 4、5):
Link(load, "OutputImage", mean, "InputImage")类型精确匹配,循环换ImagePath依次Run。
片段 1 · 句柄转 Mat(gray_mean_tool.cpp:40-56)
查看完整源文件cv::Mat HandleToGrayMat(const PixelImageHandle& h) {
auto ro = h.OpenRead();
if (!ro || !(*ro)) return {};
const int w = ro->width();
const int hh = ro->height();
if (w <= 0 || hh <= 0) return {};
if (ro->info().pixel_format() != PixelFormat::Gray8) return {};
cv::Mat m(hh, w, CV_8UC1);
for (int y = 0; y < hh; ++y) {
std::size_t n = 0;
const std::uint8_t* p = ro->GetPixelBytes(y, &n);
if (p == nullptr || n < static_cast<std::size_t>(w)) return {};
std::memcpy(m.ptr(y), p, static_cast<std::size_t>(w));
}
return m;
}讲解:
- 只接受
Gray8,其它格式直接返回空 Mat——调用方(片段 3)把空 Mat 翻成ToolFailure诊断。 - 逐行拷贝而不是借指针:
GetPixelBytes的行跨度不保证等于宽(stride 可能 padding,极端还有负值),拼一块连续内存最稳,和src/imaging/opencv_bridge.cpp同一写法。
片段 2 · 收图:路径 → 句柄(gray_mean_tool.cpp:68-118)
查看完整源文件DeclareProperty("ImagePath", TypeDesc{Type::String, {}}, PropertyFlags{true, true, true, false});
AddOutput("OutputImage", TypeDesc{Type::ImageGray8, {}});const cv::Mat raw = cv::imread(path, cv::IMREAD_UNCHANGED);
if (raw.empty()) { /* … imread 失败 … */ }
if (raw.depth() != CV_8U) { /* … 仅支持 8bit … */ }
cv::Mat gray;
switch (raw.channels()) {
case 1: gray = raw; break;
case 3: cv::cvtColor(raw, gray, cv::COLOR_BGR2GRAY); break;
case 4: cv::cvtColor(raw, gray, cv::COLOR_BGRA2GRAY); break;
default: { /* … 不支持的通道数 … */ }
}
if (!gray.isContinuous()) gray = gray.clone();
const ImageInfo info(gray.cols, gray.rows, PixelFormat::Gray8);
PixelImageHandle h =
PixelImage::Copy(gray.data, gray.total(), info, vyntek::imaging::BufferAccess::ReadOnly);
WriteOutput("OutputImage", Value::Image(std::move(h), Type::ImageGray8));讲解:
IMREAD_UNCHANGED原样读,通道归一由工具自己做(BGR→Gray 是 OpenCV 的cvtColor,注意不是 RGB)。PixelImage::Copy(..., ReadOnly)一步产出已稳定发布的句柄,可直接WriteOutput。对比fake_tools.cpp的Rent + OpenWrite + Complete + OpenRead + ShareImage五步——Copy是"外部像素一次性导进来"的捷径,相机零拷贝路径才走五步。Copy参数非法抛std::invalid_argument,片段里try/catch转诊断,不要漏。
片段 3 · 算子:句柄 → 均值(gray_mean_tool.cpp:131-150)
查看完整源文件AddInput("InputImage", TypeDesc{Type::ImageGray8, {}});
AddOutput("Mean", TypeDesc{Type::Float64, {}});
AddOutput("Width", TypeDesc{Type::Int32, {}});
AddOutput("Height", TypeDesc{Type::Int32, {}});const Value in = ReadPort("InputImage");
const PixelImageHandle* h = in.image();
if (h == nullptr || !*h) { /* … 不是有效句柄 … */ }
const cv::Mat m = HandleToGrayMat(*h);
if (m.empty()) { /* … 需要 Gray8 … */ }
const double mean = cv::mean(m)[0];
WriteOutput("Mean", Value::Float64(mean));讲解:
- 输入是
Value::Image包着的句柄,不是像素——工具之间不传裸指针,in.image()拿出来只是借读。 - 输出只有标量(
Mean/Width/Height),想往下游继续传图就再加一个OutputImage端口,和片段 2 的套路一样。
片段 4 · 建组与连线(gray_mean_tool.cpp:176-191)
查看完整源文件auto group = ToolGroup::Create("gray_mean_group");
auto load = registry.Create(ImageLoadTool::kTypeId, "load");
auto mean = registry.Create(GrayMeanTool::kTypeId, "mean");
if (!load || !mean || !group->AddTool(std::move(load)) || !group->AddTool(std::move(mean))) {
std::fprintf(stderr, "AddTool 失败\n");
return 2;
}
// 图像数据流转:load.OutputImage -> mean.InputImage(ImageGray8 精确匹配)。
Tool* load_t = group->FindTool("load");
Tool* mean_t = group->FindTool("mean");
const LinkResult lr = group->Link(load_t, "OutputImage", mean_t, "InputImage");
if (!lr.ok) {
std::fprintf(stderr, "Link 失败:\n");
for (const auto& d : lr.diagnostics) std::fprintf(stderr, " - %s\n", d.message.c_str());
return 3;
}讲解:
Link按 §5.4.3 精确匹配端口类型:OutputImage是ImageGray8,InputImage也必须是ImageGray8,错一个lr.ok就是 false,且不留半边。FindTool返回的是非拥有观察指针,组拥有工具(AddTool接管了unique_ptr)。
片段 5 · 循环跑(gray_mean_tool.cpp:200-214)
查看完整源文件RunSession session(group);
std::printf("dir=%s count=%zu\n", dir.c_str(), imgs.size());
for (const auto& img : imgs) {
load_t->SetProperty("ImagePath", Value::Str(img));
const RunResult r = session.Run();
if (!r.ok()) {
std::printf("%s FAILED\n", fs::path(img).filename().string().c_str());
continue;
}
const Value mean_v = mean_t->ReadPort("Mean");
...
}讲解:
- 一张图一次
Run,ImagePath是包外 property(DeclareProperty时PropertyFlags{true, true, true, false}),host 换图只改它。 Run失败只跳过这张图继续下一张;ReadPort("Mean")读的是本次 Run 刚写的值。
常见误区
- 「示例含
<opencv2/>,库里也能含」 —— 不能。示例演示的是客户扩展写法(直链opencv_world);库内 operator 永远走vyntek::imaging::opencv::*桥接,公开头一律不含(见cmake/VyntekOpenCV.cmake铁律) - 「
Copy出来的句柄还要 Complete」 —— 不用。Copy一步就是稳定发布;五步走(Rent→OpenWrite→Complete→OpenRead→ShareImage)是相机零拷贝路径的写法 - 「label 图均值 0 是 bug」 —— 不是。
*_label.bmp是全黑掩膜,均值 0 恰好证明链路没把图搞混 - 「
OFF构建示例没了」 —— 对,VYNTEK_WITH_OPENCV=OFF时本 target 自动跳过,其它示例照跑 - 「客户也要从源码编 OpenCV 吗」 —— 不用。发
cpack产出的vyntek-sdk包(33M),客户find_package(Vyntek)即得Vyntek::opencv413,配置 0.3 秒。见 OpenCV SDK
构建与运行
需要 VYNTEK_WITH_OPENCV=ON(默认 ON,首编会拉 500M+ OpenCV 源码并全编,10~20 分钟正常)。二进制落在 build/<preset>/bin/,与 libopencv_world 同目录(缺它启动即报找不到库)。
Linux(本机 VPS)
cmake --preset ninja-debug-all
cmake --build --preset ninja-debug-all --target gray_mean_demo
./build/ninja-debug-all/bin/gray_mean_demo /home/keith/dataset/kos/kos01Windows(PowerShell,仓库在 D:\Works\Vyntek)
cmake --preset ninja-debug-all
cmake --build --preset ninja-debug-all --target gray_mean_demo
.\build\ninja-debug-all\bin\gray_mean_demo.exe D:\Works\kos01输出
本机实跑(Linux,2026-10-23,kos01 16 张):
$ ./build/ninja-debug-opencv/bin/gray_mean_demo /home/keith/dataset/kos/kos01
dir=/home/keith/dataset/kos/kos01 count=16
Part0.jpg mean=184.13 500x1263
Part0_label.bmp mean=0.00 500x1263
Part1.jpg mean=184.75 500x1261
Part1_label.bmp mean=0.00 500x1261
Part2.jpg mean=188.04 500x1254
Part2_label.bmp mean=0.00 500x1254
...
$ echo $?
0jpg 均值 180+(产线零件图偏亮)、*_label.bmp 均值 0(全黑掩膜),两者交替出现说明 Link 把每张图的句柄送对了工具。
退出码
0—— 全部图片跑完(单张失败只打印FAILED不改退出码)1—— 目录无图像(扩展名只认 jpg/jpeg/png/bmp/tiff)2——registry.Create或AddTool失败3——Link失败(类型不匹配,看打印的 diagnostics)4——group->Validate()有 Error