OpenCV入门学习-核心功能(一)
环境:
- win11 x64
- OpenCV 4.9.0
- VS 2022 (v143)
- C++ (ISO C++ 14 标准)
1. Mat
类
1.1. 数据结构与内存管理
Mat
是OpenCV的核心图像容器类,采用自动内存管理机制,无需手动分配/释放内存。其结构分为两部分:
- 矩阵头(Header)
固定大小(约数十字节),存储元信息:- 矩阵维度(rows, cols, channels)
- 数据类型(如
CV_8UC3
) - 存储布局(step:每行的字节数)
- 引用计数器
- 数据指针地址
- 数据块(Data)
动态内存区域,存储实际像素值,采用引用计数机制管理:- 浅拷贝时多个
Mat
对象共享数据块 - 最后一个引用对象销毁时自动释放内存
- 浅拷贝时多个
1.2. 深浅拷贝对比
1 | cv::Mat A = cv::imread("dog.jpg", cv::IMREAD_COLOR); |
操作类型 | 内存开销 | 数据独立性 | 适用场景 |
---|---|---|---|
浅拷贝 | 极小 | 共享数据 | 快速创建视图/ROI |
深拷贝 | 大 | 完全独立 | 需独立修改像素的场景 |
📌 关键机制
- 修改浅拷贝对象的像素会直接影响原对象
- 使用
clone()
或copyTo()
进行显式深拷贝
1.3. 色彩空间与存储布局
- 默认色彩空间:BGR(非 RGB ),与图像显示系统兼容
- 通道顺序:对于3通道矩阵,像素按
B-G-R
顺序存储 - 内存布局:连续存储(可通过
isContinuous()
判断),行优先存储
1.4. 构造函数全解析
1.4.1. 默认构造函数(延迟初始化)
1 | cv::Mat matEmpty; // 创建空矩阵(Header无数据指针) |
1.4.2. 指定尺寸与类型
1 | // 创建480x640的3通道图像(BGR格式) |
1.4.3. 初始化值构造函数
1 | // 创建3x3蓝色矩阵(B=255, G=0, R=0) |
1.4.4. 外部数据共享
1 | float externalData[6] = {1,2,3,4,5,6}; |
1.4.5. ROI构造函数
1 | cv::Rect roiArea(10, 10, 100, 100); // (x,y,width,height) |
1.4.6. 特殊矩阵生成
1 | cv::Mat zeros = cv::Mat::zeros(3, 3, CV_32F); // 3x3浮点零矩阵 |
1.5. 数据访问与输出
1 | // 正确输出矩阵内容(需指定格式) |
⚠️ 注意事项
- 使用
at<T>(row,col)
访问元素时需确保类型匹配(如CV_8UC3
对应cv::Vec3b
)- 多通道矩阵的 cv::Scalar 初始化顺序为
(B,G,R)
- 外部数据构造的 cv::Mat 对象不管理原数据内存,需手动维护生命周期
1.6. 其他操作
多维矩阵支持
cv::Mat
可处理N维数据,通过dims
参数指定维度:1
2
3// Mat(int ndims, const int* sizes, int type, const cv::Scalar& s);
int sizes[] = {3, 4, 5}; // 3维:3x4x5
cv::Mat ndMat(3, sizes, CV_8UC1, cv::Scalar::all(0));内存连续性优化
使用create()
方法重新分配连续内存:1
mat.create(rows, cols, type); // 重置矩阵尺寸/类型
表达式计算优化
OpenCV重载了矩阵运算符(+
,*
等),自动避免中间变量拷贝:1
2cv::Mat C = A.mul(B) + 0.5; // 对应元素相乘再加 0.5
// ps: 矩阵乘法时直接用 *
1.7. 补充 CV_
数据类型
OpenCV 中的 CV_
数据结构(数据类型)是通过预定义宏(CV_<位数><类型>C<通道数>
)表示的矩阵数据类型,主要用于定义
cv::Mat
或图像的数据类型。常见类型如下:
1.7.1. 命名规则
格式:CV_<bit_depth>(S|U|F)C<通道数>
bit_depth
:数据位数(如 8, 16, 32, 64)。S|U|F
:U
:无符号整数(Unsigned,如uchar
)。S
:有符号整数(Signed,如int
)。F
:浮点数(Float,如float
或double
)。
C<通道数>
:通道数(1-4,更高通道数需要自定义类型)。
1.7.2. 常见数据类型
数据类型 | 说明 | C++ 类型等效 | 典型用途 |
---|---|---|---|
CV_8UC1 |
8 位无符号单通道 | uchar |
灰度图像 |
CV_8UC3 |
8 位无符号三通道 | cv::Vec3b |
BGR 彩色图像(默认格式) |
CV_8UC4 |
8 位无符号四通道 | cv::Vec4b |
带透明度(BGRA)的图像 |
CV_16UC1 |
16 位无符号单通道 | ushort |
深度图像(如深度传感器) |
CV_16SC1 |
16 位有符号单通道 | short |
特定算法的中间结果 |
CV_32SC1 |
32 位有符号单通道 | int |
像素索引或整数计算 |
CV_32FC1 |
32 位浮点单通道 | float |
图像处理中间结果(如边缘检测) |
CV_32FC3 |
32 位浮点三通道 | cv::Vec3f |
浮点彩色数据(如 HDR) |
CV_64FC1 |
64 位双精度浮点单通道 | double |
高精度计算(如几何变换) |
1.7.3. 特殊类型与限制
通道数限制: 默认支持 1~4 通道。超过 4 通道需自定义,如通过
CV_MAKETYPE
宏创建:1
默认图像类型: OpenCV 读取图像的默认格式为
CV_8UC3
(BGR 三通道彩色)。浮点图像范围:
CV_32F
或CV_64F
类型的数据范围通常为[0.0, 1.0]
或[0, 255]
,需根据算法做归一化。
1.7.4. 类型转换与创建
创建指定类型的
cv::Mat
矩阵:1
2cv::Mat img_float(480, 640, CV_32FC3); // 创建一个 32 位浮点三通道矩阵
cv::Mat img_gray(100, 100, CV_8UC1, cv::Scalar(0)); // 创建单通道灰度图类型转换: 使用
cv::Mat::convertTo()
或cv::cvtColor()
:1
2cv::Mat img_uint8;
img_float.convertTo(img_uint8, CV_8UC3, 255.0); // 将浮点图像转为 8UC3
1.7.5. 示例
1 |
|
1.7.6. 总结
CV_
数据类型定义了矩阵中元素的存储格式,选择时需考虑以下因素:
- 数据范围(如 8 位足够表示 0-255 的像素)。
- 是否需要浮点运算(如几何变换需要高精度)。
- 通道数需求(如彩色图像需 3 通道)。
2. 图像扫描、查找表与性能优化
通过一个实际的颜色空间缩减示例,快速掌握 OpenCV 中图像处理的三大核心技能:图像扫描方法、查找表(LUT)的应用以及算法性能测量。
2.1. 图像矩阵的内存存储
在 OpenCV 中,图像数据以 Mat
对象存储,其内存布局与颜色通道紧密相关:
- 灰度图像:每个像素对应一个
uchar
值(0-255),按行连续存储。 - 彩色图像(BGR):每个像素包含3个通道(蓝、绿、红),每个通道一个
uchar
值。
注意:OpenCV 默认使用 BGR 顺序而非 RGB !
内存中的存储方式
- 按行连续存储:像素按行优先顺序排列,每行(row)称为一个 “高度”维度。
- 多通道数据:每个像素的通道值连续存储。
- 例如,BGR 图像的像素存储顺序为
[B0, G0, R0, B1, G1, R1, ...]
。
- 例如,BGR 图像的像素存储顺序为
- 可能的填充字节 (Padding)
- 某些情况下,每行末尾可能有对齐填充(用
step
属性表示实际每行的字节数)。
- 某些情况下,每行末尾可能有对齐填充(用
通过 Mat::isContinuous()
可判断矩阵是否连续存储。若连续,可将图像视为一维数组处理,提升遍历效率。
2.2. 颜色空间缩减示例
假设我们需要将图像颜色值从 256 级缩减到更少的级别(例如每 10
个值合并为一个)。直接计算公式为:
\[\nonumber
I_{\text{new}} = \lfloor \frac{I_{\text{old}}}{10} \rfloor \times 10
\]
但频繁的除法和乘法会拖慢性能。此时,查找表(Lookup Table, LUT)可预先计算所有可能的输入值对应的输出值,后续直接替换,大幅提升效率。
2.2.1. 查找表生成代码
1 | int divideWith = 10; |
2.3. 图像扫描方法对比
2.3.1. 高效方法:指针访问
通过指针直接操作内存,适合连续存储的图像。
1 | cv::Mat& ScanImageWithPointer(cv::Mat& image, const uchar* table) { |
2.3.2. 安全方法:迭代器
使用 OpenCV 迭代器自动处理行间隙,代码更安全。
1 | cv::Mat& ScanImageWithIterator(cv::Mat& image, const uchar* table) { |
2.3.3. 动态地址计算:at
方法
适用于随机访问,但全图扫描时性能较差。
1 | cv::Mat& ScanImageWithAt(cv::Mat& image, const uchar* table) { |
2.4. 终极优化:OpenCV 内置 LUT 函数
使用 cv::LUT()
函数,底层优化更高效,支持多线程。
void cv::LUT (InputArray src, InputArray lut, OutputArray dst)
1 | cv::Mat ApplyLUT(cv::Mat& image, const uchar* table) { |
2.5. 性能对比
通过 cv::getTickCount()
和
cv::getTickFrequency()
测量时间:
1 | double t = (double)cv::getTickCount(); |
方法 | 平均耗时(毫秒) |
---|---|
指针访问 | 79.47 |
迭代器 | 83.72 |
动态地址计算(at) | 93.78 |
LUT函数 | 32.58 |
结论:
- 优先使用 OpenCV 内置函数(如
LUT()
),性能最佳。 - 需要自定义遍历时,指针访问最快,迭代器更安全。
- 避免在循环中使用
at
方法进行全图扫描。
2.6. 实战建议
- 连续存储优化:若图像连续(
isContinuous() == true
),可将其视为一维数组处理。 - 多通道处理:注意 BGR 顺序,遍历时需分别操作每个通道。
- 调试技巧:在 Debug模 式下,
at
方法会检查越界访问,适合快速定位错误。
3. 图像处理中的掩膜操作
图像处理是计算机视觉的核心技术之一,而 OpenCV 作为最流行的开源库,为开发者提供了丰富的工具。本节将介绍 OpenCV 中的掩膜操作(Mask Operations),以图像对比度增强为例,理解其原理与实现。
3.1. 什么是掩膜操作?
掩膜操作通过一个称为 内核(Kernel) 或 掩膜(Mask) 的小矩阵,对图像中的每个像素进行加权计算,从而调整其值。这种操作本质上是将当前像素及其邻域像素的值按权重相加,常用于图像滤波、边缘检测、锐化等任务。
例如,对比度增强的公式为:
\[ \nonumber I(i,j) = 5 \cdot I(i,j) -
[I(i-1,j) + I(i+1,j) + I(i,j-1) + I(i,j+1)] \]
对应的内核矩阵为:
\[ \nonumber
M = \begin{bmatrix}
0 & -1 & 0 \\
-1 & 5 & -1 \\
0 & -1 & 0
\end{bmatrix}
\]
中心像素权重为 5,上下左右像素权重为 -1,其余为 0
。通过该内核,可以突出中心像素,增强图像细节。
3.2. 手动实现掩膜操作
3.2.1. 关键步骤
- 边界处理:图像边缘的像素缺少邻域,通常直接设为0。
- 遍历像素:使用指针逐行访问像素,避免直接修改原图。
- 加权计算:根据内核公式重新计算每个像素的值。
3.2.2. 代码示例(简化版)
1 | void Sharpen(const cv::Mat& input, cv::Mat& output) { |
cv::saturate_cast
cv::saturate_cast
在图像处理方面,无论是加是减,乘除,都可能会超出一个像素灰度值的范围(0~255),saturate_cast
函数的作用即是:当运算完之后,结果为负,则转为 0,结果超出 255,则为
255。
3.3. 使用OpenCV内置函数
filter2D
手动实现掩膜操作虽然直观,但代码冗长且效率较低。OpenCV 提供了
filter2D
函数,可直接应用内核矩阵,代码更简洁且性能更优。
3.3.1. 代码示例
1 | cv::Mat kernel = (cv::Mat_<char>(3, 3) << |
3.3.2. 优势
- 高效性:内置函数经过优化,速度更快(测试中手动实现需
31ms,
filter2D
仅需 13ms)。 - 灵活性:支持自定义内核中心、边界处理模式(如填充、镜像等)。
3.4. 性能对比与适用场景
方法 | 优点 | 缺点 |
---|---|---|
手动实现 | 理解底层原理,灵活调试 | 代码复杂,性能较低 |
filter2D |
简洁高效,支持高级功能 | 需熟悉内核矩阵的构建 |
推荐场景:
- 学习阶段:建议手动实现,深入理解像素操作原理。
- 实际项目:优先使用
filter2D
,提升效率并减少代码量。
3.5. 总结
掩膜操作是图像处理的基础技术,OpenCV 提供了灵活的实现方式。通过对比手动实现与内置函数,开发者可以根据需求选择最佳方案。掌握这一技术后,可进一步探索高斯模糊、边缘检测(如 Sobel 算子)等进阶应用。
动手尝试:
- 修改内核矩阵,观察图像变化(例如锐化、模糊)。
- 对比不同边界处理模式的效果(如
BORDER_REFLECT
或BORDER_CONSTANT
)。
4. 图像基础操作
介绍图像处理的基础操作,涵盖图像加载、像素操作、内存管理及可视化等核心内容。
4.1. 图像输入输出
4.1.1. 加载图像
使用 cv::imread
加载图像时,默认读取为 BGR
三通道格式。若需灰度图,需显式指定参数:
1 | cv::Mat img = cv::imread("image.jpg"); // 三通道BGR图像 |
4.1.2. 保存图像
图像保存格式由文件扩展名决定:
1 | cv::imwrite("output.png", img); // 保存为PNG格式 |
注意:若需从内存读写图像,使用
cv::imdecode
和cv::imencode
。
4.2. 像素操作
4.2.1. 访问像素值
单通道灰度图(8UC1):
1
uchar intensity = img.at<uchar>(y, x); // 注意坐标顺序为(y, x)
三通道 BGR 图像(8UC3):
1
2
3
4cv::Vec3b bgr_pixel = img.at<cv::Vec3b>(y, x);
uchar blue = bgr_pixel[0]; // B通道
uchar green = bgr_pixel[1]; // G通道
uchar red = bgr_pixel[2]; // R通道
4.2.2. 修改像素值
1 | img.at<uchar>(y, x) = 128; // 将灰度图像素设为128 |
4.3. 内存管理与引用计数
4.3.1. 数据共享机制
cv::Mat
通过引用计数管理内存,多个实例可共享同一数据块:
1 | std::vector<cv::Point3f> points; |
4.3.2. 显式复制数据
需独立操作数据时,使用 clone()
或
copyTo()
:
1 | cv::Mat img_clone = img.clone(); // 深拷贝 |
4.4. 基本图像操作
4.4.1. 图像置黑
1 | img = cv::Scalar(0); // 所有像素设为0(黑色) |
4.4.2. 选择感兴趣区域(ROI)
1 | cv::Rect roi(10, 10, 100, 100); // (x, y, width, height) |
4.4.3. 颜色空间转换
1 | cv::Mat gray; |
4.4.4. 图像类型转换
1 | cv::Mat src_32f; |
4.5. 图像可视化
4.5.1. 显示 8 位图像
1 | cv::namedWindow("Display", cv::WINDOW_AUTOSIZE); |
4.5.2. 显示浮点图像
需将 32F 类型归一化到 0~255 范围:
1 | cv::Mat sobelx; |
4.6. 总结
介绍了 OpenCV 中图像处理的基础操作,包括加载/保存、像素访问、内存管理和可视化。可以进一步结合官方文档探索高级功能(如滤波、特征检测等)。
5. 使用cv::addWeighted实现图像线性混合
在计算机视觉中,图像混合是一项基础且实用的技术。通过调整权重参数,可以实现两张图片的平滑过渡效果,例如幻灯片切换或视频转场。本节介绍如何使用
cv::addWeighted
函数实现图像的线性混合。
5.1. 理论背景
线性混合的数学表达式为:
\[\nonumber
g(x) = (1 - \alpha) \cdot f_0(x) + \alpha \cdot f_1(x)
\]
其中,\(\alpha\) 的取值范围为 \([0, 1]\)。当 \(\alpha\) 从 0 逐渐增加到 1 时,输出图像会从
\(f_0\) 平滑过渡到 \(f_1\)。
5.2. 代码实现
以下是使用 C++ 和 OpenCV 实现图像混合的完整代码:
#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
double alpha = 0.5;
double beta, user_input;
// 读取两张图片
::Mat src1 = cv::imread(cv::samples::findFile("LinuxLogo.jpg"));
cv::Mat src2 = cv::imread(cv::samples::findFile("WindowsLogo.jpg"));
cv
// 检查图片是否加载成功
if (src1.empty()) {
std::cout << "错误:无法加载src1" << std::endl;
return -1;
}
if (src2.empty()) {
std::cout << "错误:无法加载src2" << std::endl;
return -1;
}
// 获取用户输入的alpha值
std::cout << "请输入alpha值(0.0-1.0): ";
std::cin >> user_input;
if (user_input >= 0 && user_input <= 1) {
= user_input;
alpha }
= 1.0 - alpha;
beta ::Mat dst;
cv
// 执行线性混合
// void addWeighted(InputArray src1, double alpha, InputArray src2,
// double beta, double gamma, OutputArray dst,
// int //dtype = -1);
::addWeighted(src1, alpha, src2, beta, 0.0, dst);
cv
// 显示结果
::imshow("混合结果", dst);
cv::waitKey(0);
cv
return 0;
}
5.3. 关键代码解析
- 加载图片
- 使用
cv::imread
读取图片,并通过cv::samples::findFile
确保路径正确。
- 使用
- 输入验证
- 用户输入的 \(\alpha\) 值通过
std::cin
获取,并限制在[0, 1]
范围内。
- 用户输入的 \(\alpha\) 值通过
- 混合计算
cv::addWeighted
函数的参数依次为:src1
:第一张输入图像。
alpha
:第一张图像的权重。
src2
:第二张输入图像。
beta
:第二张图像的权重(通常为1 - alpha
)。
gamma
:标量偏移量(此处设为0)。
dst
:输出图像。
5.4. 注意事项
- 图片尺寸与类型
- 输入的两张图像必须具有相同的尺寸(宽度和高度)和数据类型,否则会导致运行时错误。
- 输入的两张图像必须具有相同的尺寸(宽度和高度)和数据类型,否则会导致运行时错误。
- 扩展应用
- 此技术可用于创建动态效果,如视频淡入淡出、AR 场景叠加等。
5.5. 总结
通过 cv::addWeighted
函数,OpenCV
为图像混合提供了一种简洁高效的实现方式。掌握这一技术后,可以进一步探索更复杂的图像处理任务,如图像融合、遮罩处理等。
6. 参考资料
[1] OpenCV 官方文档