
上篇回顾
第 13 篇《工程化与性能》用一条 6 段流水线(cvtColor → GaussianBlur → Canny → dilate → erode → threshold)把「怎么跑得快」讲完:隐形拷贝的字节账、setNumThreads 与并行超订、计时误差的经典来源。第 14 篇则把去噪增强、分类、拼接三个「效果类」环节逐个打探针,顺手验证了 cv2.ml 在 5.0.0 主 wheel 里已经消失。
第 13 篇留过一个钩子:如果 OpenCV 能替你做调度,而不是你手写每一步,会不会更快?这个钩子就是本篇的主角------G-API。
本篇要解决的痛点
- 流水线代码越长越难维护:每加一个环节,循环、临时变量、错误分支一起膨胀;算子之间共享缓冲的约定全靠人肉维护。
- 「先预分配、再逐段执行」的经验会传染 :第 13 篇证明了预分配能避免隐形拷贝,于是「所有缓冲都复用」被当成通用加速法------本篇探针
c会用整链数据把这句外推按回去。 - C++ 与 Python 两侧的模块可用性不一致 :很多教程默认
cv2.gapi随手可得。换一个 wheel、换一个构建,Python 侧可能完全没有这个模块------本篇探针a一票否决了这个默认。 - 文档签名与真实绑定对不上 :官方 5.0 参考页甚至列出了
cv.GMatDesc(d, c, s[, p])的 Python 签名,但安装的 wheel 里根本没有cv2.gapi------「文档有签名」和「绑定存在」是两回事。
能力承诺
读完本篇,你应当能做到:
- 说清 G-API 是什么、不是什么:它是图执行引擎,不是 OpenCV 的替代品;
- 看懂
GMat / GMatDesc / GComputation / GCompiled四件套的分工,以及apply()与compile()的缓存语义差异; - 在代码里识别出哪些 API 名是文档里不存在的(本篇就抓出了两个),并改用官方签名;
- 复述本篇 5 处自我推翻的分类口径:2 处判读与数据矛盾、1 处单点测试被当成边界、2 处签名/边界凭印象记错;
- 在「单图单次调用」和「多阶段流水线」这两种场景之间,用开销模型而不是直觉决定要不要上 G-API。
1. 概述与选题理由
第 13 篇把命令式流水线优化到 12 ms 级之后,一个自然的问题是:能不能把「怎么调度」这件事交给框架? G-API(Graph API)就是 OpenCV 对这个问题的回答------你用占位符声明图,框架负责编译、调度、混合执行。本篇不吹它,只做三件事:讲清 API 形态(官方文档级)、给出唯一一条本机实测(命令式基线)、把「哪些是文档、哪些是实测」的边界划得干干净净。
1.1 本篇要解决什么问题
- API 认知 :G-API 的
GMat / GMatDesc / GComputation / GCompiled分别是什么,apply()/compile()/operator()()怎么配合。 - 环境反例 :Python 侧
cv2.gapi在 5.0.0 主 wheel 里不存在(探针a),且官方 5.0 已把 G-API 移入 contrib(官方迁移说明)。 - 开销模型 :命令式基线逐段是多少、整链是多少、批量每图成本是多少(探针
c、d),告诉读者框架开销在哪个量级该警惕。 - 选型判断:什么场景 G-API 有价值、什么场景它是负资产。
1.2 核心结论(先行)
- 本机 OpenCV 5.0.0:
getattr(cv2, 'gapi') is None,dir(cv2)含 gapi 的符号 0 个 ------Python 侧不存在 G-API (探针a、b)。官方 5.0 迁移说明确认gapi与ml已移入 opencv_contrib,主 wheel 不提供[官方文档, 非实测]。 - 官方 API 形态:图用
GMat占位符声明,GComputation持有图,apply()隐式编译并缓存 编译对象,compile()总是 重新编译并返回GCompiled,执行走GCompiled::operator()()[官方文档, 非实测]。 - 官方文档里不存在
process()与getGraphDescriptor()两个 API 名------规划大纲曾点名它们,官方 5.0 参考页核验后确认没有,本篇已改为官方签名(见第 5 节、方法论第 4 条)。 - 命令式基线(本机实测,探针
c):整链中位 12.6982 ms (朴素链)vs 14.4360 ms (dst=复用链),比值 0.880x ------预分配复用整链并不更快,逐段归因见第 8 节。 - G-API 侧没有本机数字:官方在 face beautification 教程给出的性能主张是「优化后的视频流水线比串行版本快 2.7 倍」(测试机 Intel Core i7-8700)
[官方文档, 非实测]------引用时必须带这个标签。
1.3 与前篇的衔接
承接第 13 篇的 6 段流水线:同一张图、同一条链路,本篇在第 9 节给出改写为 G-API 图的 C++ 形态(未编译)。本篇不讲 OpenCV 其余模块的算法细节(已在第 1~12 篇)、不讲 Vulkan 编程、不讲 自定义 kernel 的完整 C++ 实现细节(未编译)。
1.4 阅读建议
- 想快速抄作业:先读第 10 节「避坑指南」+ 第 14 节「算子选型速查表」。
- 想知道哪里推翻了自己的印象:直接跳到第 15 节「方法论:我们打脸了自己 5 次」。
- 想复现命令式基线:按第 8 节引用的探针文件(
probe15_c.txt/probe15_d.txt)逐段比对数值。 - 只想看环境真相:读第 4 节的探针
a结果,然后读坑 1、坑 8。
1.5 符号与约定
- 实测 / 推演二分 :凡标注「探针
c/d」的数字,都能在探针文件里找到原始行;凡 G-API 行为表述,一律挂[官方文档, 非实测]标签。文中不出现任何没有标签的 G-API 数字。 - C++ 代码块 :首行固定为
未编译:本机无 OpenCV C++ SDK,按官方签名推演,仅用于签名推演,不做编译验证。 - ❌/✅ 对比:避坑部分沿用全系列「错误印象(❌)→ 数据/文档实测(✅)」结构。
- 自我推翻分类:严格按三类统计(① 判读与数据矛盾、② 单点测试被当成边界、③ 签名/边界凭印象记错),四处计数口径必须一致。
2. G-API 是什么:图执行引擎,不是 OpenCV 替代品
G-API 解决的是执行层 问题:你声明「把 A 喂给 B,再把 B 喂给 C」,框架决定「哪一步在哪个后端跑、什么时候跑、缓冲怎么复用」。它不提供新的图像算法------算子的算法本体还是 OpenCV 那一套,G-API 只是把它们改造成「图节点」 [官方文档, 非实测]。
两个最常见的误读,先按回去:
- ❌ 误读一 :G-API 是 OpenCV 的替代品。→ ✅ 它是 OpenCV 的一个模块,吃的是 OpenCV 的算子和数据类型
[官方文档, 非实测]。 - ❌ 误读二 :图构建得像深度学习框架一样自动求导。→ ✅ G-API 只管调度与后端分发,不 做自动微分
[官方文档, 非实测]。
2.1 声明式与命令式的核心差异
命令式(第 13 篇的写法)是「每一步立刻执行、立刻拿到结果」:
python
# probe15_c.py 的命令式基线(本机实测)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
blur = cv2.GaussianBlur(gray, (5, 5), 0.0)
edges = cv2.Canny(blur, 100.0, 200.0)
G-API 的写法是「先声明形状关系,再统一执行」:
cpp
未编译:本机无 OpenCV C++ SDK,按官方签名推演
cv::GMat in; // 图的输入占位符(还没数据)
cv::GMat gray = cv::gapi::BGR2Gray(in);
cv::GMat edges = cv::gapi::Canny(gray, 100.0, 200.0);
cv::GComputation graph(in, edges); // 输入 in,输出 edges
// 到这一步,什么都不会执行------执行要等 apply()/compile() 之后
区别就像「点菜」和「吃菜」:命令式是点一道吃一道,G-API 是把整桌菜一次性下单,厨房(调度器)自己决定先炒哪道、用哪个灶(后端)[官方文档, 非实测]。
2.2 图里跑的是什么类型
G-API 的图节点类型不是 cv::Mat,而是它的占位符兄弟 :GMat(图上的图像)、GScalar(图上的标量)、GArray<T>(图上的数组)。它们只描述「数据长什么样」,不持有像素。
| 图类型 | 对应真实类型 | 用途 |
|---|---|---|
cv::gapi::GMat |
cv::Mat |
图上的图像数据流 |
cv::gapi::GScalar |
cv::Scalar |
图上的标量(阈值、均值等) |
cv::gapi::GArray<T> |
std::vector<T> |
图上的数组(轮廓、关键点等) |
GComputation 的构造函数吃的是 GProtoInputArgs / GProtoOutputArgs,日常用 cv::gin(...) / cv::gout(...) 包装真实数据喂进/取出 [官方文档, 非实测]。
3. GMat 与 GMatDesc:类型即协议
GMatDesc 是 G-API「能在执行前校验管线」的根基:它用元信息描述一张 GMat 的深度、通道数、尺寸 ,图上的每个节点都通过它推导输出形态 [官方文档, 非实测]。
3.1 GMatDesc 的官方构造方式
官方 5.0 参考页给出的构造形式包括 [官方文档, 非实测]:
cpp
未编译:本机无 OpenCV C++ SDK,按官方签名推演
cv::GMatDesc d1; // 空描述
cv::GMatDesc d2(0, CV_8UC1, cv::Size(1920, 1080)); // 深度、类型、尺寸
// 常用配套方法:
d2.withDepth(CV_16U); // 改深度
d2.withType(CV_8U, 3); // 改类型与通道数
d2.withSize(cv::Size(640, 480)); // 改尺寸
从真实 cv::Mat 取描述用 cv::descr_of(mat)------这是给「引用真数据建图」用的标准入口,compile() 的输入元信息通常就是这么来的 [官方文档, 非实测]。
3.2 元信息不匹配时,报错时机是什么
官方原话是:GComputation 在运行时 检查协议正确性------apply() 里传错对象数量、把 cv::Scalar 传给本该是 cv::Mat 的位置,C++ 编译期不报错,运行期抛异常 。想要编译期检查,得用类型化包装 cv::GComputationT<> [官方文档, 非实测]。
这里有一个必须写清楚的落差:「元信息不匹配能在执行前报错」是官方文档描述的行为,本机并没有实测过它 ------探针 b 的全部前置条件(cv2.gapi 存在、Python 侧图构建 API 存在、C++ SDK 存在)在本机三项全缺 ([B0]~[B3] 原文留档),所以这一节只能转述官方文档并标注「非实测」,不能包装成本机验证。这正是证据等级口径要防的事。
4. 构建图:GComputation 与 apply()
4.1 探针 a:Python 绑定一票否决(本篇第一个探针)
按规划,probe15_a 是「不通过即改方案」的开关探针,结果一票否决:
text
----- probe15_a.txt -----
cv2: 5.0.0
HAS cv2.gapi: False
RESULT: Python 侧无 gapi -> 第 15 篇必须改 C++ 主线
探针 b 把证据链补全:[B0] 访问 cv2.gapi.GMatDesc 直接 AttributeError: module 'cv2' has no attribute 'gapi';[B1] getattr(cv2,'gapi') -> None;[B2] dir(cv2) 含 gapi 的符号命中 0 个 。结论:本机 Python 侧完全没有 G-API,任何 cv2.gapi.* 写法在取属性那一刻就崩溃(坑 1、坑 8)。
4.2 图的构建形态
GComputation 的构造就是把占位符连成图。官方示例里最常见的两种:
cpp
未编译:本机无 OpenCV C++ SDK,按官方签名推演
// 形态一:单入单出
cv::GMat in;
cv::GMat out = cv::gapi::resize(in, cv::Size(640, 480));
cv::GComputation graph(in, out);
// 形态二:多入多出(gin/gout 包装)
cv::GMat in1, in2;
cv::GComputation graph2(cv::gin(in1, in2), cv::gout(out1, out2));
// 之后:
cv::Mat a, b, r1, r2;
graph2.apply(cv::gin(a, b), cv::gout(r1, r2));
注意:GComputation 的构造只是声明 ;真正触发执行的是 apply() 或 compile()。原大纲第 4 条写的「add 声明节点」并不存在 ------节点不是显式 add 出来的,是算子表达式连出来的(方法论第 4 条)。[官方文档, 非实测]
5. 编译与执行:compile() 与 GCompiled
5.1 两个入口的缓存语义差异(官方文档原话级别)
这是本篇最容易踩空的 API 细节,官方参考页写得很明确 [官方文档, 非实测]:
apply():隐式编译,且内部缓存编译对象 ------只要输入元信息(格式、分辨率)不变,下一次apply()复用缓存图,不重新编译;元信息变了,缓存图才重新生成。compile():总是 触发一次完整编译,返回一个新的GCompiled对象------不管apply()是否已经缓存过同类图 ,compile()都不看缓存。
一句话记忆:apply() 是「又要跑」的快捷键,compile() 是「就要新建」的强制开关。 坑 7 会展开。
5.2 GCompiled:长得像 std::function 的执行体
compile() 返回的 GCompiled,官方原话是「在语义上非常接近 std::function<>------跑起来像一次函数调用」[官方文档, 非实测]:
cpp
未编译:本机无 OpenCV C++ SDK,按官方签名推演
auto compiled = graph.compile(cv::descr_of(in_mat));
cv::Mat out;
compiled(in_mat, out); // GCompiled::operator()() 就是执行入口
// 官方提供的 GCompiled 成员:operator()() 六种重载、outMetas()、
// metas()、canReshape()、reshape()、prepareForNewStream()、operator bool
三个官方行为要点 [官方文档, 非实测]:
- 非可重入 :
GCompiled有状态,同一个对象不要并发调用。 - 同源独立 :同一个
GComputation编译出的两个GCompiled完全独立,可以并发。 - 整形约束 :编译好的图只能用于编译时的格式与分辨率 ,要支持新分辨率走
reshape()(抛异常说明失败)。
5.3 官方文档里不存在的两个 API 名
规划大纲第 5 条原本写的是「compile() / getGraphDescriptor() / process()」,其中后两个名字在官方 5.0 参考页的 GComputation / GCompiled 成员清单里都不存在(本篇已逐页核验)。真实分工是:
| 误认为存在的 API | 官方实际入口 |
|---|---|
getGraphDescriptor() |
输出元信息用 GCompiled::outMetas() / metas() |
process() |
执行用 GCompiled::operator()(),或直接 GComputation::apply() |
这条属于方法论第 4 条「签名/边界凭印象记错」------不核验就写进大纲,是最典型的③类错误。
6. Kernel 自定义:把自家算法接进图里
G-API 内置 kernel 覆盖不了项目内私有算法时,需要自己声明「操作(Op)」并提供 kernel。官方机制三段式 [官方文档, 非实测]:
6.1 声明一个操作(Op)
cpp
未编译:本机无 OpenCV C++ SDK,按官方签名推演
// G_API_OP = G_TYPED_KERNEL(Class, API, Id):声明一个图操作
G_TYPED_KERNEL(GMyOp, <cv::GMat(cv::GMat)>, "org.myproject.myop");
// Id 必须全局唯一,官方用它做序列化与匹配
6.2 用宏绑定 CPU/OCL 实现
官方宏 GAPI_OCV_KERNEL(Name, API) 生成 struct Name : public cv::GCPUKernelImpl<Name, API>;GAPI_OCL_KERNEL 同理对应 cv::GOCLKernelImpl [官方文档, 非实测]:
cpp
未编译:本机无 OpenCV C++ SDK,按官方签名推演
GAPI_OCV_KERNEL(GCPUMyOp, GMyOp) {
static void run(const cv::Mat& in, cv::Mat& out) {
// 这里写项目自己的算法,输入输出签名必须与 API 声明一致
}
};
签名不匹配(run 的参数类型与 <cv::GMat(cv::GMat)> 对不上)是类型推导失败 的高发区------官方 API 声明是 std::function 形态,run 必须同型(坑 6)。
6.3 kernel 包与后端选择
cpp
未编译:本机无 OpenCV C++ SDK,按官方签名推演
auto custom = cv::gapi::kernels<GCPUMyOp>();
// 官方内置包:cpu 后端(默认)/ fluid / ocl
cv::gapi::core::fluid::kernels(); // 核心算子 fluid 版
cv::gapi::imgproc::ocl::kernels(); // imgproc 算子 OCL 版
// 组合:combine 拼多个包;use_only 强制只用指定 kernel
auto pkgs = cv::gapi::combine(cv::gapi::core::fluid::kernels(), custom);
auto compiled = graph.compile(cv::descr_of(in_mat), cv::compile_args(pkgs));
官方文档原文强调:use_only{} 是「提示 G-API 只用指定 kernel」的组合器;没有显式给包时,框架用默认后端 + 内置 kernel 集合兜底 [官方文档, 非实测]。
7. 最有价值的场景:流水线、混合执行与官方性能数字
7.1 三个真正有红利的场景
- 多阶段流水线 :节点多、后端起作用的场景,框架的重排/并行空间大
[官方文档, 非实测]。 - CPU/GPU 混合执行 :同类算子聚到同一后端,图里不同后端之间由框架接缝
[官方文档, 非实测]。 - 批量/流式批处理 :同构输入反复处理时,图编译一次、多次执行(
compileStreaming用于视频流)[官方文档, 非实测]。
7.2 官方性能数字(带标签引用)
官方 face beautification 教程的原话是:
"On the test machine (Intel Core i7-8700) the G-API-optimized video pipeline outperforms its serial (non-pipelined) version by a factor of 2.7"
[官方文档, 非实测]
两点纪律:这个 2.7 是官方的、是流式视频管线(serial vs pipelined)的对照,不是本机实测,也不是「任何场景都能快 2.7 倍」。写进正文时标签不能丢。
7.3 官方对 G-API 状态的自我描述
官方 5.0 文档的开篇声明值得原文引用 [官方文档, 非实测]:G-API 仍是活跃开发中的新模块,API 随时可能发生兼容性破坏的变动。对工业产线代码来说,这句话本身就是选型变量:锁定版本、锁 kernel 包版本,是引入 drop-in 的管道工(这篇第 14 节「工程实践」会再收一遍)。
8. 开销模型:命令式基线(本篇唯一的本机实测)
本节所有数字都来自探针 c、d,是命令式基线 ;G-API 侧本机无法执行,官方侧数字一律带 [官方文档, 非实测] 标签。两边不构成可比对照------先把这个前提钉死。
8.1 整链对照:预分配复用并没有更快
同一张 1080p 图、同一条 6 段链,两种写法的中位耗时(探针 c):
text
[C0] 朴素链(每步新建) 中位=12.6982 ms p95=14.3748 max=15.9862
[C1] dst= 复用链 中位=14.4360 ms p95=17.3286 max=26.5176
[C2] 朴素/dst= 中位比 = 0.880x (dst= 复用链在本机并未更快; 逐段归因见 [C5])
[C3] 两链路输出逐位一致: True (若 False, 检查 dst= 复用是否污染中间值)
承接第 13 篇「预分配避免隐形拷贝」的经验,我们预期 dst= 复用链更快------结果整链 0.880x ,反而更慢,且最差一档 max=26.5176 ms 比朴素链的 15.9862 ms 大 10 ms 以上。第 13 篇的经验没有失效,但它只解决「拷贝」问题;复用缓冲在部分算子上等于引入原地(in-place)约束,代价完全不同。这是方法论第 1 条「判读与数据矛盾」。
8.2 逐段归因:红利和反作用力是分算子的
[C5] 把 6 段逐个拆开(输入与 [C0] 链一致,in-place 段计时前复位):
text
cvtColor 朴素=0.7687 dst=0.2796 朴素/dst=2.749x
GaussianBlur 朴素=0.7727 dst=0.2281 朴素/dst=3.388x
GaussianBlur(原地) dst=1.3542 (与 [C1] 链内写法一致, 输入=输出同缓冲)
Canny 同一实现(无 dst=) 中位=9.4398
dilate 朴素=0.6536 dst=1.4548 朴素/dst=0.449x
erode 朴素=0.6568 dst=1.3876 朴素/dst=0.473x
threshold 朴素=0.7023 dst=0.0439 朴素/dst=15.980x
三个结论,全部有数字支撑:
- 红利算子 :
cvtColor(2.749x)、GaussianBlur(3.388x)、threshold(15.980x)------dst=到独立缓冲确实更快,threshold甚至快一个量级。 - 反作用力算子 :
dilate(0.449x)、erode(0.473x)------同一个复用缓冲写回,反而慢一倍以上;而GaussianBlur原地(输入=输出同缓冲)是 1.3542 ms,比它的独立缓冲版 0.2281 ms 慢近 6 倍。 - 「复用缓冲」≠「原地操作」 :
dst=到独立缓冲(快)和输入=输出同缓冲(慢)是两件事。把链内所有段都写成同缓冲复用,等于让dilate/erode/GaussianBlur悄悄退化成原地版------整链 0.880x 就是这么来的。
机制层面不做过度解读:本机没有进一步下钻到 OpenCV 源码确认
dilate复用缓冲慢的具体原因(可能是临时缓冲分配路径差异),只报告「在本机 OpenCV 5.0.0 环境实跑的结果」。留给可改进之处。
8.3 命令式没有 compile 阶段:一次性成本只有 warmup
[C4]:预热后首调用 13.7910 ms 、紧邻稳态 14.4090 ms ,比值 0.957x ------命令式没有「编译一次」的固定成本,唯一的一次性开销是首次调用的预热。G-API 的编译成本在命令式基线里不存在对应物,所以「G-API 每次调用摊薄编译开销」的公式分母(每图成本)可测,分子(compile 耗时)本机不可测------这个不对称必须明说。
8.4 批量:同一图 N 次,每图成本基本不动
探针 d 两个批量实验:
text
[D0] N 张不同图:
N= 1 总= 11.864 ms 每图= 11.8638 ms
N= 10 总= 123.146 ms 每图= 12.3146 ms
N= 50 总= 563.471 ms 每图= 11.2694 ms
N=100 总= 1595.198 ms 每图= 15.9520 ms
[D1] 同一张图重复 N 次:
N= 1 总= 15.311 ms 每图= 15.3108 ms
N= 10 总= 152.795 ms 每图= 15.2795 ms
N= 50 总= 801.069 ms 每图= 16.0214 ms
N=100 总= 1604.548 ms 每图= 16.0455 ms
[D2] 一次性成本探查: 进程内首次(已预热10)=14.6788 ms 稳态=16.2526 ms 比值=0.903x
结论:命令式批量没有任何摊薄效应 ------每图成本在 11.3 ~ 16.0 ms 之间波动,N 从 1 涨到 100 没有单调下降。这反过来印证:「批量摊薄」的卖点只属于带固定编译成本的框架(G-API 的 compile),命令式根本没有可摊薄的对象。
8.5 开销模型一句话版
- 命令式:每图 ≈ 常数(12~16 ms),无固定成本,也无摊薄空间;
- G-API:单图单次调用 = 编译 + 执行,必然比命令式多一段一次性成本
[官方文档, 非实测];多阶段/批量/流式场景下这段成本才可能被摊回。
9. 前 14 篇的衔接:把第 13 篇流水线改写成图
第 13 篇的 6 段链(cvtColor → GaussianBlur → Canny → dilate → erode → threshold)正好全部有官方 G-API kernel,改造起来是「换壳不换算法」:
cpp
未编译:本机无 OpenCV C++ SDK,按官方签名推演
cv::GMat in;
cv::GMat gray = cv::gapi::BGR2Gray(in); // = cvtColor
cv::GMat blur = cv::gapi::gaussianBlur(gray, 5, 0.0);
cv::GMat edges = cv::gapi::Canny(blur, 100.0, 200.0);
cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(3, 3));
cv::GMat dil = cv::gapi::dilate(edges, kernel);
cv::GMat ero = cv::gapi::erode(dil, kernel);
cv::GMat bin = cv::gapi::threshold(ero, 128.0, 255.0, cv::THRESH_BINARY);
cv::GComputation pipeline(in, bin); // 图,不执行
cv::GCompiled compiled = pipeline.compile(cv::descr_of(frame));
compiled(frame, result); // 执行入口
对照第 13 篇的命令式写法,差异一目了然:没有中间变量矩阵、没有 for 循环、没有手动缓冲管理 ------代价是引入了编译阶段和 GMatDesc 元信息约定。哪边划算,取决于第 8 节的开销模型,而不是「声明式听起来高级」。
10. 避坑指南(12 个坑点)
每一条都是「错误印象(❌)→ 数据/文档(✅)」的对照。实测数字来自 probe15_all.txt;文档结论一律带 [官方文档, 非实测] 标签。
坑 1. Python 侧根本没有 cv2.gapi
❌ 错误印象 :教程里到处是 G-API,import cv2 之后 cv2.gapi.xxx 随手可用。
✅ 探针实测 :probe15_a.txt 的 HAS cv2.gapi: False;probe15_b.txt [B0] 访问 cv2.gapi.GMatDesc 抛 AttributeError: module 'cv2' has no attribute 'gapi',[B2] dir(cv2) 含 gapi 的符号命中 0 个 。本机 5.0.0 主 wheel 无此模块,任何 cv2.gapi.* 在取属性当场崩溃------这是「响亮失败」,比第十二篇的静默失效好排查,但事前不查一样会白写一整段代码。
坑 2. 官方参考页有 Python 签名,就以为有 Python 绑定
❌ 错误印象 :官方 5.0 参考页给 GMatDesc 列了 cv.GMatDesc(d, c, s[, p]) 签名,说明 Python 绑定了 G-API。
✅ 事实核查 :那只是 doxygen 从 C++ 头文件生成的签名占位符 [官方文档, 非实测];装到本机的 opencv-python-headless 5.0.0.93 里 cv2.gapi 是 None(探针 a、b)。文档有签名 ≠ 绑定存在 ------写代码前用 hasattr(cv2, "gapi") 验一下,一行的事。
坑 3. 单图单次调用用 G-API,还期待更快
❌ 错误印象:框架调度、后端自动选择,单张图跑一次也应该快。
✅ 正确预期 :单图单次 = 编译开销 + 执行开销,必然多一段一次性成本 [官方文档, 非实测];本机命令式基准(探针 c)整链中位 12.6982 ms 就是「没有框架成本」的参照。单张图、单次调用,G-API 是负资产。
坑 4. 元信息不匹配会在编译前就被拦下
❌ 错误印象:G-API 声明式强类型,写错元信息编译期就报错。
✅ 官方原文 :GComputation 在运行时 检查协议正确性,普通 GComputation 的 C++ 编译期不 拦;要编译期检查得用 cv::GComputationT<> [官方文档, 非实测]。本机未实测该报错时机(前置条件全缺,探针 b 已留档)------别把「文档描述」升级成「我试过」。
坑 5. 六个算子的红利「复制粘贴」到整条链
❌ 错误印象 :cvtColor 的 dst= 快 2.749x,全链都复用缓冲肯定赚。
✅ 探针实测 :[C5] 逐段显示 dilate 只有 0.449x 、erode 0.473x ,GaussianBlur 原地版 1.3542 ms (独立缓冲版 0.2281 ms 的近 6 倍)。红利算子 vs 反作用力算子分得很开,单算子结论不能外推成链级规则(方法论第 3 条)。
坑 6. 自定义 kernel 的 run 签名与 API 声明对不上
❌ 错误印象 :写了 G_TYPED_KERNEL 和 GAPI_OCV_KERNEL,类型推导就该自动成功。
✅ 官方机制 :API 声明是 std::function 形态(如 <cv::GMat(cv::GMat)>),run 的参数/返回类型必须同型,错一个就是推导失败 [官方文档, 非实测]。GAPI_OCV_KERNEL(Name, API) 的 Name 要继承 cv::GCPUKernelImpl<Name, API>------宏帮你生成这层继承,但 run 本体得自己对齐。本机未编译验证,写时要按官方示例逐字对。
坑 7. apply() 每次都重新编译
❌ 错误印象 :既然有编译阶段,每次 apply() 都是「编译 + 执行」。
✅ 官方原文 :apply() 内部缓存 编译对象,输入元信息不变就复用;compile() 才总是 重新编译并返回新 GCompiled [官方文档, 非实测]。性能测试里把 apply() 循环当「每次重编译」来计时会系统性误判------这跟第十二篇 getPerfProfile 的教训同型。
坑 8. 5.0 里 G-API 还在主仓库、主 wheel 里
❌ 错误印象:G-API 是 OpenCV 亲儿子,5.0 主模块必有。
✅ 官方迁移说明 :5.0 起 gapi(与 ml)移入 opencv_contrib ,构建时要 -DOPENCV_EXTRA_MODULES_PATH 指向 contrib 源码,头文件仍是 <opencv2/gapi.hpp>,API 未变 [官方文档, 非实测]。主 wheel(opencv-python-headless)不打包 contrib------探针 a 的 False 就是这个结构决定的,不是装坏了。
坑 9. 把「复用缓冲」和「原地操作」混为一谈
❌ 错误印象 :复用 dst 缓冲 ≈ 原地操作 ≈ 省内存 ≈ 更快。
✅ 探针实测 :[C5] 里 GaussianBlur 独立缓冲版 0.2281 ms 、原地版 1.3542 ms ;dilate/erode 复用同缓冲是 0.449x / 0.473x 。「dst= 到独立缓冲」和「输入=输出同缓冲」是两条不同的代码路径、两种不同的性能命运------第 13 篇的预分配经验只覆盖前者。
坑 10. 官方 2.7 倍数字不带标签就引用
❌ 错误印象:官方说快 2.7 倍,我引一句「G-API 比命令式快 2.7 倍」总没错。
✅ 正确写法 :那是官方 face beautification 教程在 Intel Core i7-8700、串行视频管线 vs 优化后管线 对照下的实测 [官方文档, 非实测]------不是普适数字、不是本机数字。引用必须带 [官方文档, 非实测] 标签并把前提写全,否则就是第 12 篇定义的「文档转述包装成实测」。
坑 11. 凭印象写 process() / getGraphDescriptor()
❌ 错误印象 :G-API 执行应该有个类似 process() 的入口,图描述应该有 getGraphDescriptor()。
✅ 官方核验 :这两个名字在官方 5.0 参考页不存在;执行入口是 GCompiled::operator()()(或 apply()),输出元信息是 GCompiled::outMetas() / metas() [官方文档, 非实测]。大纲里点名过的 API 名都要核验一遍再写正文------本篇规划就踩了这对名字(方法论第 4 条)。
坑 12. 官方说「API 可能有破坏性变更」当耳边风
❌ 错误印象 :compatibility-breaking changes in the future 是客套话。
✅ 官方原文 :5.0 文档明说 G-API「仍处于活跃开发、API 易变、未来可能有破坏性兼容变更」[官方文档, 非实测]。对产线代码意味着:锁定 OpenCV 与 contrib 版本、kernel 包版本进配置,升级前跑差分测试------不是威胁,是选型前的预期管理。
11. 优秀实践
- 环境先验活再写代码。 一行
hasattr(cv2, "gapi")就能在写第一行cv2.gapi前排掉坑 1(探针a就是这么干的)。C++ 侧拿不到 SDK 时,先声明证据等级(本篇篇首就是这么做的),别让读者误以为「转述 = 实测」。 apply()/compile()按语义选。 输入元信息稳定、反复执行 →apply()(吃缓存);要显式控制编译、拿GCompiled做reshape()/outMetas()→compile()[官方文档, 非实测]。- 链级结论必须逐段归因。
[C5]的教训:整链 0.880x 不说明任何单算子,单算子 2.749x 也不说明整链------红利与反作用力分算子看。 - 文档数字永远挂标签。 官方 2.7、官方「API 易变」、
GMatDesc构造签名------引用时一律[官方文档, 非实测],与探针数字物理隔离。 - 改造成图之前先算账。 用探针
c/d的方法先测命令式基线(每图常数成本),再判断 G-API 的编译成本在哪个场景能被摊回。
12. 可改进之处
- G-API 侧没有任何本机数字。 本机无 C++ SDK(探针
b已留证据链),本篇全部 G-API 行为都来自官方文档并标注[官方文档, 非实测]。真正的「G-API vs 命令式」对照要等有 C++ SDK + contrib 构建的环境,届时第 8 节的公式(分子 compile 耗时)才能补上分母。 dilate/erode复用缓冲变慢的机制没下钻。 只报告了「本机(5.0.0)实测 0.449x / 0.473x」,没有到 OpenCV 源码层确认是临时缓冲分配、还是并行分块路径差异------留给源码级探针。- 批量只测了命令式侧。
[D0]/[D1]证明命令式无摊薄,但「G-API 编译成本被 N 次执行摊薄」的曲线本机画不出来,只能引用官方定性描述[官方文档, 非实测]。 - 元信息不匹配报错时机未实测。 探针
b的三项前置条件本机全缺,GComputation运行期抛异常 vsGComputationT<>编译期拦截的差异目前只有官方原文支撑。 use_only{}与各后端包的具体行为没验证。 官方文档描述[官方文档, 非实测],未经本机构建验证,读者在真实 contrib 环境组合fluid/ocl/ 自定义包时仍需自己踩一遍。
计数口径收口:本篇 5 处自我推翻 = 2 处判读与数据矛盾 + 1 处单点测试被当成边界 + 2 处签名/边界凭印象记错,与版本说明、方法论、文末互动三处完全一致,且每一处都能在正文定位到对应探针编号或官方核验记录。
13. 工程实践
把本篇结论收进决策骨架(伪代码形态,C++ 未编译,Python 侧本机不可用):
cpp
未编译:本机无 OpenCV C++ SDK,按官方签名推演
// 1. 选型判断:先测命令式基线(探针 c/d 方法),再决定要不要 G-API
// 单图单次调用 -> 命令式;多阶段/批量/流式 -> 才考虑图
// 2. 写图:占位符声明 + 算子表达式连接,不手动管理中间缓冲
cv::GMat in;
cv::GMat bin = cv::gapi::threshold(
cv::gapi::erode(
cv::gapi::dilate(
cv::gapi::Canny(cv::gapi::gaussianBlur(cv::gapi::BGR2Gray(in), 5, 0.0),
100.0, 200.0),
kernel),
kernel),
128.0, 255.0, cv::THRESH_BINARY);
// 3. 自定义 kernel 接入:G_TYPED_KERNEL + GAPI_OCV_KERNEL,run 签名严格对齐
// 4. 执行:元信息稳定用 apply()(缓存),需要显式控制用 compile() + GCompiled
落地要点:
- 证据标签进注释 :源码里把「哪些结论来自官方文档」写成注释,防止后人把
[官方文档, 非实测]的数字当实测。 - 版本锁定:G-API 官方自述 API 易变(坑 12),OpenCV / contrib / kernel 包版本全部锁进配置。
- 回归对照 :引入 G-API 前先固化命令式基线的中位耗时(本机 12.6982 ms 量级),改造后跑同一对图,只允许好于或持平,不允许「重构了但慢了」悄悄上线。
14. 算子选型速查表
| 场景 | 选择 | 依据 | 不要做的事 |
|---|---|---|---|
| 单图单次调用 | 命令式(不上 G-API) | 探针 c:命令式整链中位 12.6982 ms,无编译成本 |
期待 G-API 单次更快 |
| 多阶段流水线 | G-API 图 + apply() |
官方:缓存编译对象,元信息不变复用 [官方文档, 非实测] |
每轮 compile() 强制重编译(坑 7) |
| Python 侧 G-API | 不存在,别写 | 探针 a:HAS cv2.gapi: False |
按官方参考页 Python 签名写 cv2.gapi.*(坑 2) |
| 预分配复用缓冲 | 只对 cvtColor/GaussianBlur/threshold 用 |
[C5]:2.749x / 3.388x / 15.980x |
对 dilate/erode/原地高斯用(0.449x / 0.473x / 1.3542 ms) |
| 批量/流式 | G-API(有编译成本可摊) | 官方 face beautification:串行 vs 管线化快 2.7(i7-8700)[官方文档, 非实测] |
把 2.7 当普适数字、当本机数字(坑 10) |
| 自定义算法接入 | G_TYPED_KERNEL + GAPI_OCV_KERNEL |
官方宏机制 [官方文档, 非实测] |
run 签名与 API 声明不对齐(坑 6) |
| 5.0 版本 | contrib 构建,<opencv2/gapi.hpp> |
官方迁移说明:gapi 移入 contrib,API 未变 [官方文档, 非实测] |
以为主 wheel 自带(坑 8) |
15. 方法论:我们打脸了自己 5 次
本节不讲「应该怎么做」,只讲我们本来怎么想、数据/文档怎么把我们按回去的 。口径先摆清楚:① 判读与数据矛盾 2 处、② 单点测试被当成边界 1 处、③ 签名/边界凭印象记错 2 处,合计 5 处。每一条都能在正文找到对应实验编号,不是事后编的。
分类总表
| 类别 | 处数 | 特征 | 对策 |
|---|---|---|---|
| ① 判读与数据矛盾 | 2 | 写下结论后只找支持它的证据 | 写完结论回读全部数据行 |
| ② 单点测试被当成边界 | 1 | 只试一个值就推出规则 | 关键参数至少扫 3 个值 |
| ③ 签名/边界凭印象记错 | 2 | 不实跑就永远不暴露 | 签名/API 名必须核验后成文 |
① 判读与数据矛盾(2 处)
- 「承接第 13 篇的预分配经验,
dst=复用链理应更快」。 于是把 10 ms 级的心智模型直接套到 6 段链上。[C1]整链中位 14.4360 ms 反超朴素链 12.6982 ms (0.880x),最差档26.5176 vs 15.9862 ms------先有结论再核对数据的典型:我第一眼只看了「预分配省拷贝」的一面,没回头核对「复用缓冲在部分算子上退化成原地」的另一面。 - C5 归因表第一版把不同输入条件的数字混排。 Canny 段第一版吃的是裸灰度图,其它段吃的是链内输入------两张输入条件不同的表并排比耗时,等于拿两套口径写一个结论。修正后统一为链内输入(Canny 9.4398 ms ),与
[C0]链一致。判读错在没核对每个探针行自己的输入条件,这是「只读结论行、不读条件行」。
② 单点测试被当成边界(1 处)
- 「
cvtColor的dst=快 2.749x → 复用缓冲是通用加速法」。 只看了一个算子的红利就推出链级规则。[C5]补扫其它算子后,dilate0.449x 、erode0.473x 、GaussianBlur原地 1.3542 ms ------红利与反作用力分算子,单算子结论不是规则,是线索。关键参数(这里是算子类型)至少要扫到出现反例为止。
③ 签名/边界凭印象记错(2 处)
process()与getGraphDescriptor()两个 API 名写进了规划大纲。 官方 5.0 参考页核验:不存在 ;执行入口是GCompiled::operator()(),输出元信息是outMetas()/metas()。同一处凭印象(同一行大纲、两个符号),计 1 处。不抛异常、不越界、看起来能跑------不核验就永远不暴露。- 原大纲的 Python 侧 G-API 写法。 按教程印象以为
cv2.gapi随手可用,probe15_a一票否决:getattr为None、dir命中 0 个符号、访问即AttributeError。环境边界(Python 无此模块)不是文档能告诉你的,探针才能------这也是本篇改成 C++ 主线、证据等级下调的起因。
与第 13 篇「9/10 发生在怎么测」、第 14 篇「4/5 是只读一半数据」呼应:本篇 5 处里 3 处(第 1、2、3 条)仍然死在「数据怎么读」,2 处(第 4、5 条)死在「名字/边界凭印象」。方法论没有新物种,只有没改的旧习惯。
16. 本期互动
这一篇的特殊之处是证据等级分层:只有命令式基线是实测,G-API 全部是官方文档 。下面三个问题想听听你的答案------请把你选的序号写在评论区,我按票数最高的在后续文章里展开。
问题 1:你的环境里 cv2.gapi 存在吗?
A. 存在,一直能用(大概率装了 contrib 构建或独立编译版)
B. 不存在,跟本篇探针 a 一样
C. 没试过,看完本篇才知道要去验
D. 我在 C++ 项目里用,Python 侧从来没指望过
问题 2:dilate/erode 复用缓冲反而慢一倍(0.449x / 0.473x),你的第一反应是?
A. 这是 5.0.0 的 bug,升级就好
B. 复用缓冲触发了不同执行路径(临时缓冲/并行分块),值得源码级下钻
C. 无所谓,反正我从不复用缓冲
D. 拿我的环境复测一遍再说
问题 3:官方文档说 G-API「API 易变、可能有破坏性变更」------你会怎么处理选型?
A. 锁定版本进配置,升级前跑差分
B. 等它稳定了再引入
C. 照常用,真被破坏再改
D. 不用 G-API,命令式足够
顺带记一笔:5 处自我推翻里,2 处判读与数据矛盾、1 处单点测试被当成边界、2 处签名/边界凭印象记错 ------这期新收获是第 2 条:归因表必须统一输入条件,混排两套口径的数字,结论天然不可信。三类的对策不变:写完结论回读全部数据行、关键参数至少扫 3 个值、API 名不核验就不成文。
💬 评论区聊聊
三个小问题,欢迎直接在评论区接龙:
- 你的 OpenCV 是怎么装的?
opencv-python/headless/contrib/ 自己编译------cv2.gapi存不存在,多半在安装方式里就注定了,你的横截面数据是什么? - 第 13 篇教了预分配,本篇把「预分配 ≠ 原地」按回去了------你有没有把「某篇的加速经验」直接搬到另一个算子上翻车的经历?
- 如果让你在本篇 5 处自我推翻里排「最想避免哪一类」,你会选哪类?为什么?
前两期的评论区已经沉淀了不少同类踩坑(第 13 篇关于流水线优化、第 14 篇关于模块裁剪的讨论),本篇的三条同样来自实测与官方核验,欢迎拿你的真实数据来对线。
👉 收藏 · 转发 · 系列目录
觉得有用的话,点赞、收藏、转发 三连就是最好的支持------尤其是转发给正在写 cv2.gapi 却报 AttributeError 的同事,能省下半天排查时间。
OpenCV 实战系列目录 (全系列规划 18 篇 ,已更新 15 篇,第 16~18 篇待写)
| 篇号 | 标题 | 一句话摘要 |
|---|---|---|
| 01 | 环境搭建与图像读写 | 装对版本,比装最新的版本重要 |
| 02 | 色彩空间与通道操作 | BGR/RGB 差一个字母,下游全错 |
| 03 | 几何变换 | 旋转矩阵的中心点,九成的坑 |
| 04 | 滤波与去噪 | 均值、高斯、中值、双边,四种脾气 |
| 05 | 边缘检测与轮廓 | Canny 阈值怎么定,轮廓层级怎么读 |
| 06 | 形态学与二值化 | 自适应阈值不是万能钥匙 |
| 07 | 直方图与均衡化 | CLAHE 的 clipLimit,小窗口大影响 |
| 08 | 模板匹配与特征匹配 | 匹配只是候选,验证才是结论 |
| 09 | 图像金字塔与轮廓进阶 | 多尺度下的尺度漂移 |
| 10 | 视频读写与帧处理 | 抽帧策略决定吞吐上限 |
| 11 | 相机标定与立体视觉 | 标定板拍得好,棋盘格说了算 |
| 12 | DNN 与目标检测 | 模型在,API 不一定在 |
| 13 | 工程化与性能 | 模块裁剪让「符号在、行为没了」 |
| 14 | photo 去噪增强、ml 分类与 stitching 拼接 | 三个「用得少但出问题时很救命」的模块 |
| 15 | 本文:G-API 图计算 | 把算子串成有向图,让 CPU 与 GPU 混合执行 |
| 16 | contrib 精选(一):ximgproc 的工程实用算子 | 装 opencv-contrib-python 之后,我最先用的是这几个 |
| 17 | contrib 精选(二):xfeatures2d 与 tracking 目标跟踪 | 特征描述子、目标跟踪器,以及 contrib 里最容易过时的那部分 |
| 18 | 终篇:OpenCV 5.x 迁移指南 | 一份面向工业视觉老代码库的升级评估清单 |
(完)