🎬 第五章 MFC+OpenCV视频采集及播放
📄 一句话概括

在 VS 里用 OpenCV,本质是三处配置:头文件去哪找、库文件去哪找、链接哪几个。 Debug 与 Release 必须各配一遍,这是最容易漏的一步。

📝 本节和前面几节的关系

前四章用的是 Qt,一路都在「调用别人的成品」。 这一章开始自己动像素——OpenCV 就是那个「把像素操作写成一行」的库。 但在 C++ 里用它之前,得先让编译器找得到它。 这一节讲的就是这件事,跟算法无关,纯粹是工程配置。

一、三处配置:包含目录、库目录、附加依赖项

C++ 的库分两步用:编译时要看到「长什么样」(头文件),链接时要连上「真实现」(lib)。 所以配置也是三处:

配在哪 填什么 解决什么问题
C/C++ → 常规 → 附加包含目录 ...\opencv\build\include #include 找得到头文件
链接器 → 常规 → 附加库目录 ...\opencv\build\x64\vc15\lib 找得到 .lib 文件
链接器 → 输入 → 附加依赖项 opencv_world450.lib 链接期能解析到符号

第三行那个文件名有两种流派: opencv_world 版:所有模块打成一个库,一行搞定,代价是体积大、换模块要重编。 分离版:opencv_core.lib、opencv_imgproc.lib、opencv_highgui.lib……按需写,链接更快, 但容易漏。

漏了会怎样:报 LNK2019,说某个符号无法解析。 这个错的名字叫「未解析的外部符号」,九成就是附加依赖项少写了一个 lib。

库目录还有个位数陷阱:x64\vc15\lib 与 x86\vc15\lib 是两套, 项目是 x64 却指到 x86 目录,会报一堆 LNK2038 或 模块计算机类型冲突。 这类错误跟代码完全无关,但看起来很吓人。

二、Debug 与 Release 各配一遍

这一节的重点在这儿。 VS 的项目属性是「按配置分开存」的:Debug 一套、Release 一套, 你在 Debug 里配完,切到 Release 全是空的。

而且库名也不一样:Debug 用带 d 后缀的版本(opencv_world450d.lib),Release 用不带后缀的。 两套 CRT 的运行时不兼容,混用会在链接期或运行时炸。

三种做法,从差到好:

做法 优点 缺点
只在 Debug 里配 快 切 Release 就崩,回头查半天
两个配置各配一遍 能用 改一次要改两处
用属性表 .props 配一次、两个配置共用 要学一个新概念

第三种是正解:视图 → 其他窗口 → 属性管理器,右键 Debug|x64 添加「新建属性表」, 在里面配好三处,再把这个属性表加到 Release|x64 上。 这样两个配置共享一份设置, 而且这个 .props 文件可以直接发给同事——他双击导入就配好了,不用照着文档点十几次。

还有一个「运行时缺 dll」的坑要提前说: 链接成功不等于能跑。程序启动时找不到 opencv_world450.dll, 报的是「计算机中丢失 xxx.dll」,看着像系统坏了。 做法是把 bin 目录加进 PATH,或者把 dll 拷到 exe 旁边。

三、本机这套环境长什么样

本机装的是 OpenCV 5.0.0 的 Python 绑定,不是教材那套 VS 2015 + OpenCV 4。 这件事要说清楚,因为两者的差别不只是版本号。

对比项 教材:VS 2015 + OpenCV 4 本机:Python + OpenCV 5
怎么用 配三处属性,写 C++ import cv2,写完就跑
缺库怎么办 自己下、自己配、自己拷 dll pip install
算法 API cv2::GaussianBlur cv2.GaussianBlur,同名
界面部分 MFC / HighGUI 窗口 无窗口,结果落盘看

第三行是这节课能真跑的原因:算法层的 API 几乎同名同参,GaussianBlur、bilateralFilter、 medianBlur 一个一个对得上。所以「磨皮那段逻辑」在两边是同一段话。

第四行是不能真跑的原因:imshow 需要一个窗口系统,沙箱里没有桌面。 所以本课的处理是「算出来,把数字和文件落盘」, 图像本身用数值指标代替眼睛——这反而更严格:眼睛看不出「边缘保留 33% 还是 85%」。

还要提醒一句:OpenCV 5 移除了一批旧 API(cv2.CV_... 那批老常量、 还有一些 cv::legacy 里的东西)。 教材里 4.x 能编过的代码,拿 5.x 可能编不过,但这一章用到的都是核心函数,没有受影响的。

四、动手 Demo:OpenCV 在这台机器上是什么样子

这段先查版本,再从 getBuildInformation() 里挑三行关键的, 最后验证「Mat 就是 ndarray」这件事。 FFMPEG = YES 那一行决定了 VideoCapture 能不能直接开视频文件, DIY 编译的 OpenCV 常常是 NO。

import cv2, numpy as np

print(cv2.__version__)
info = cv2.getBuildInformation()
for line in info.split("\n"):
    s = line.strip()
    if s.startswith("FFMPEG") or s.startswith("Parallel"):
        print(s)

img = np.zeros((480, 640, 3), dtype=np.uint8)
print(img.shape, img.dtype, img.strides[0])

px = np.zeros((1, 1, 3), dtype=np.uint8)
px[0, 0] = (10, 20, 240)          # 写成 B=10, G=20, R=240
print(cv2.cvtColor(px, cv2.COLOR_BGR2RGB))

实跑输出:

--------------------------------------------------------------------
OpenCV 在这台机器上是什么样子
--------------------------------------------------------------------
**先确认环境,再谈代码。**
**这一节要的三样东西:版本、有没有编进 FFmpeg、并行框架是谁。**

  版本:**5.0.0**

**`getBuildInformation()` 里挑三行看**(这是判断「能不能用」的地方):
  FFMPEG = YES (prebuilt binaries)
  Parallel framework = Concurrency

**第一行最关键**:**`FFMPEG = YES` 意味着 `VideoCapture` 能直接开 mp4、rtsp、http**,
不用你自己解码再把帧喂进来。
**DIY 版 OpenCV 常常是 NO**,那时候 `VideoCapture("a.mp4")` 直接返回空,报错还只有一句「打不开」。

**再看 Mat 与 ndarray 的关系**——**Python 里的 OpenCV 图像就是一个 numpy 数组。**
**同一块内存,没有包装层。**

  造一张 640×480 的彩色图:
    `img.shape` = (高, 宽, 通道)
    实际值 = **(480, 640, 3)**
    `img.dtype` = **uint8**,元素个数 = **921600**
    `img.nbytes` = **921600 字节**
    连续吗 = **True**,行跨度 = **1920 字节**

**「行跨度 1920」就是「宽 640 × 3 通道」**,**说明 numpy 这边是紧凑排列、没有填充。**
**这一点到 05.03 讲 MFC 显示时会变**——**Windows 的位图要求每行按 4 字节对齐,那时就要补空字节。**

**通道顺序是 OpenCV 最著名的坑**:

  一个像素写成 `(10, 20, 240)`,**OpenCV 认为它是 B=10、G=20、R=240,也就是接近纯红。**
  转成 RGB 之后是 **(240, 20, 10)**——**红蓝换位了。**

**所以这三条要一起记**:
  **OpenCV 读进来的、`imread` 得到的、`VideoCapture` 抓到的一律是 BGR。**
  **matplotlib、PIL、Qt 的 `QImage` 一律当 RGB 用。**
  **中间那一步转换(`cvtColor`)漏掉,颜色就是蓝脸。**

三行关键信息都在:版本 5.0.0、FFMPEG = YES (prebuilt binaries)、 并行框架是 Concurrency(Windows 上的并发运行时)。

(prebuilt binaries) 这个后缀值得注意:它说明 FFmpeg 是用预编译二进制链进去的, 不是从源码编的。 后果是「这个 OpenCV 带了哪些编码器,由那个预编译包决定」,你改不了——除非自己重编 OpenCV。

最后那行 (240, 20, 10) 是全章最该记住的一个数:写进去的是 (10, 20, 240), 转 RGB 之后变成 (240, 20, 10),首尾两个数换了位置。 这就是「BGR」这三个字母的全部含义。

strides[0] = 1920 也要留意:它是「一行多少字节」,等于宽 640 乘 3 通道,一点空隙都没有。 到 05.03 讲 MFC 显示时,这个假设就不成立了——Windows 的位图要求每行 4 字节对齐, 那时每行要补空字节。

30 秒实验

  1. 在 VS 里故意把「附加依赖项」那一行删掉,看报的错是什么(应该是 LNK2019)。
  2. 算一下:opencv_world450.lib 与分离版差在哪——分离版要写几个 lib?
  3. 判断一下:项目从 Debug 切到 Release 之后崩了,第一个该查什么?

常见误区

  1. 只在 Debug 里配属性。 切到 Release 全是空的,报 LNK2019。 正确做法是用属性表 .props,两个配置共用一份。
  2. Debug 配 Release 的 lib。 Debug 要用带 d 后缀的版本,两套 CRT 运行时不兼容, 混用会在链接期或运行期炸。
  3. 库目录指到 x86。 项目是 x64 却链 32 位库,报「模块计算机类型冲突」,跟代码毫无关系。
  4. 链接成功就以为万事大吉。 还得把 opencv_world450.dll 放到 exe 旁边或者加进 PATH, 否则启动就报「丢失 dll」。
自测三问(点开对答案) 1. 在 VS 里用 OpenCV 要配哪三处? —— 附加包含目录(头文件)、附加库目录(lib 在哪)、附加依赖项(链接哪个 lib)。 前两处是「找得到」,第三处是「连得上」。 2. 为什么 Debug 与 Release 要分开配? —— 两套配置的属性是分开存的,而且 Debug 版的库带 `d` 后缀、用的是另一套 CRT 运行时,不能混用。 3. `FFMPEG = YES` 这一行有什么用? —— 它说明 `VideoCapture` 能直接读 mp4、rtsp、http 这类格式,不用自己解码再喂帧。 DIY 编译的 OpenCV 常是 NO,那时只能读图片序列。

环境配好了,下一节开始动像素。第一个例子就是「磨皮美颜」——它听起来像美工活,其实是三种滤波器之间的取舍。

相关笔记