Vyntek

双工具串联: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 —— 链路图与交付物清单 · 查看完整源文件

下文片段均为源码节选,标注行号,与仓库当前源码逐字一致。

学习路线

按四步读,每步对应下文片段:

  1. 句柄怎么进出 Mat(片段 1):OpenRead 逐行拷进连续 cv::Mat,不碰 stride 负值坑。
  2. 收图工具怎么写(片段 2):cv::imread → 转灰 → PixelImage::Copy 一步产出可发布的 ReadOnly 句柄。
  3. 算子工具怎么写(片段 3):从 InputImage 拿句柄 → 转 Mat → cv::mean,只读不写。
  4. 组怎么连起来(片段 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/kos01

Windows(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 $?
0

jpg 均值 180+(产线零件图偏亮)、*_label.bmp 均值 0(全黑掩膜),两者交替出现说明 Link 把每张图的句柄送对了工具。

退出码

  • 0 —— 全部图片跑完(单张失败只打印 FAILED 不改退出码)
  • 1 —— 目录无图像(扩展名只认 jpg/jpeg/png/bmp/tiff)
  • 2 —— registry.Create 或 AddTool 失败
  • 3 —— Link 失败(类型不匹配,看打印的 diagnostics)
  • 4 —— group->Validate() 有 Error

On this page