文章目录
- 添加噪点的案例
- 图像像素值
- 1、访问图像属性
- 2、像素访问方法 at
- 灰度图像
- 彩色图像
- 3、OpenCV 的向量类型
- 4、 图像传递方式
- The cv::Mat_ 类
- 1、作用及优点
- 2、使用 cv::Mat_ 简化像素访问
- 用指针扫描图像
- 背景
- 算法
- 案例
- 原理
- 1. 图像数据存储的基本结构
- 2、行填充(Padding)与有效宽度
- 3、计算每行的像素值数量
- 4、使用指针运算访问图像数据
- 颜色缩减方案
- 1、方法一:整数除法
- 2、方法二:取模运算
- 3、方法三:位运算
- 参数的输入与输出
- 1、原地处理(In-place Transformation)
- 2. 提供灵活性的函数设计
- 3. 灵活函数的实现
- 高效扫描连续图像
- 优点
- 适用场景
- 低级指针运算
- 核心概念
- 1、图像数据的起始地址
- 2、行与列的偏移
- 3、像素地址计算
- 优点
- 缺点
- 使用迭代器扫描图像
- 核心思想
- 1、迭代器的声明:
- 2、迭代器的使用:
- 3、颜色缩减:
- 编写高效的图像扫描循环
- 通过邻域访问扫描图像
- 准备工作
- 实现方法
- 锐化滤波
- 执行简单图像算术
- 图像加法
- 图像减法
- 乘法和除法
- 逐通道操作
- 重载图像操作
- 分割图像通道
- 重映射图像
- 简单实现
添加噪点的案例
#include "base_function_image.h"
#include <iostream>
#include <random>
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>#define IMAGE_1 "1.jpeg"
#define IMAGE_LOGO "logo.jpg"
#define IMAGE_LOGO_2 "logo_2.jpeg"void salt(cv::Mat &image, int n)
{// 检查输入图像是否为空if (image.empty()) {std::cerr << "Error: Input image is empty!" << std::endl;return;}// C++11 随机数生成器std::default_random_engine generator(std::random_device{}());std::uniform_int_distribution<int> randomRow(0, image.rows - 1);std::uniform_int_distribution<int> randomCol(0, image.cols - 1);for (int k = 0; k < n; ++k) {// 随机生成图像坐标int i = randomCol(generator); // 列索引int j = randomRow(generator); // 行索引// 根据图像类型设置像素值if (image.type() == CV_8UC1) { // 灰度图像(单通道)image.at<uchar>(j, i) = 255; // 设置为白色} else if (image.type() == CV_8UC3) { // 彩色图像(三通道)image.at<cv::Vec3b>(j, i)[0] = 255; // B通道image.at<cv::Vec3b>(j, i)[1] = 255; // G通道image.at<cv::Vec3b>(j, i)[2] = 255; // R通道} else {std::cerr << "Error: Unsupported image type!" << std::endl;return;}}
}int main()
{// 加载图像cv::Mat image = cv::imread(IMAGE_1);if (image.empty()) {std::cerr << "Error: Could not load the image!" << std::endl;return -1;}// 显示原始图像cv::imshow("Original Image", image);// 添加盐噪声int numSaltNoisePoints = 1000; // 噪声点数量salt(image, numSaltNoisePoints);// 显示处理后的图像cv::imshow("Image with Salt Noise", image);// 保存结果cv::imwrite("salt_noise_image.jpg", image);// 等待用户按键后退出cv::waitKey(0);return 0;
}
图像像素值
1、访问图像属性
在 OpenCV 中,cv::Mat 类提供了多种方法来访问图像的不同属性。其中,cols 和 rows 是两个公共成员变量,用于获取图像的列数和行数。
int numCols = image.cols; // 获取图像的列数
int numRows = image.rows; // 获取图像的行数// 如果图像大小为 640x480,则 image.cols 返回 640,image.rows 返回 480。
2、像素访问方法 at
为了访问图像中的像素,cv::Mat 提供了模板方法 at(int y, int x),其中:
- x 是列索引(水平方向)。
- y 是行索引(垂直方向)。
- T 是像素的数据类型。
由于 cv::Mat 可以存储任意类型的元素,因此程序员需要显式指定返回类型。例如:
灰度图像
对于单通道灰度图像,每个像素是一个 8 位无符号整数(uchar),可以这样访问:
image.at<uchar>(j, i) = 255; // 将第 j 行、第 i 列的像素值设置为 255(白色)
彩色图像
对于三通道彩色图像,每个像素是一个包含三个 8 位无符号整数的向量(蓝色、绿色和红色)。OpenCV 定义了一个专门的类型 cv::Vec3b 来表示这种短向量。
image.at<cv::Vec3b>(j, i)[0] = 255; // 设置蓝色通道值为 255
image.at<cv::Vec3b>(j, i)[1] = 255; // 设置绿色通道值为 255
image.at<cv::Vec3b>(j, i)[2] = 255; // 设置红色通道值为 255
或者,可以直接使用 cv::Vec3b 向量赋值:
image.at<cv::Vec3b>(j, i) = cv::Vec3b(255, 255, 255); // 设置像素为白色
3、OpenCV 的向量类型
OpenCV 提供了一系列向量类型,用于表示不同长度和数据类型的向量。这些类型基于模板类 cv::Vec<T, N>,其中:
- T 是元素类型。
- N 是向量的长度。
常见类型
2 元素向量:cv::Vec2b(2 个字节)、cv::Vec2f(2 个浮点数)、cv::Vec2i(2 个整数)。
3 元素向量:cv::Vec3b(3 个字节,常用于 RGB 颜色)。
4 元素向量:cv::Vec4b(4 个字节,常用于 RGBA 颜色)。
命名规则
最后一个字母表示数据类型:
b:8 位无符号整数(unsigned char)。
f:单精度浮点数(float)。
s:短整型(short)。
i:整型(int)。
d:双精度浮点数(double)。
4、 图像传递方式
在 OpenCV 中,即使通过值传递图像对象,它们仍然共享相同的图像数据。这是因为 cv::Mat 内部使用引用计数机制管理数据。
以下函数通过值传递图像参数,并修改其内容:
void modifyImage(cv::Mat image) {for(int i = 300; i < 600; ++i){for(int j = 300; j < 600; ++j){image.at<uchar>(i, j) = 255; // 修改像素值}}}int main() {cv::Mat img = cv::imread(IMAGE_LOGO);modifyImage(img); // 调用函数cv::imshow("Modified Image", img); // 显示修改后的图像cv::waitKey(0);return 0;
}
尽管 modifyImage 函数的参数是通过值传递的,但由于 cv::Mat 的内部机制,原始图像的内容也会被修改。
The cv::Mat_ 类
1、作用及优点
在 OpenCV 中,cv::Mat 是一个通用的矩阵类,可以存储任意类型的元素。然而,使用 cv::Mat 的 at 方法访问像素时,需要显式指定模板参数(如 uchar 或 cv::Vec3b),这有时会显得繁琐。
为了简化操作,OpenCV 提供了一个模板子类 cv::Mat_,它继承自 cv::Mat。通过 cv::Mat_,可以在创建变量时指定矩阵元素的类型,从而避免每次调用 at 方法时重复指定类型。
优点:
- 减少冗余:在频繁访问像素时,cv::Mat_ 可以避免每次都指定模板参数。
- 提高可读性:使用 operator() 的代码更短、更直观。
- 兼容性:cv::Mat_ 是 cv::Mat 的子类,两者可以无缝转换。例如,您可以将 cv::Mat 对象直接赋值给 cv::Mat_ 对象,反之亦然。
2、使用 cv::Mat_ 简化像素访问
cv::Mat_ 提供了一个额外的操作符 operator(),可以直接访问矩阵元素。与 cv::Mat 的 at 方法相比,operator() 更加简洁,因为类型在创建 cv::Mat_ 对象时已经确定。
#include <opencv2/opencv.hpp>
#include <iostream>int main() {// 加载图像cv::Mat image = cv::imread("input.jpg", cv::IMREAD_GRAYSCALE); // 灰度图像if (image.empty()) {std::cerr << "Error: Could not load the image!" << std::endl;return -1;}// 转换为 cv::Mat_<uchar> 类型cv::Mat_<uchar> img(image);// 使用 operator() 访问像素img(50, 100) = 0; // 将第 50 行、第 100 列的像素值设置为 0(黑色)// 显示修改后的图像cv::imshow("Modified Image", img);cv::waitKey(0);return 0;
}
用指针扫描图像
由于像素数量庞大,需要高效地实现扫描。
背景
彩色图像由 3 通道像素组成(红、绿、蓝),每个通道是一个 8 位无符号整数(0-255)。因此,总颜色数为 256 × 256 × 256 种颜色。
为了简化分析,有时需要减少图像中的颜色数量。一种简单的方法是将 RGB 颜色空间划分为等大小的立方体。例如,如果每个维度的颜色数量减少为原来的 1/8,则总颜色数将变为 32 × 32 × 32 = 32768 种颜色。
算法
设 N 为颜色缩减因子:
1、对每个像素的每个通道值进行整数除法:value / N。
2、再乘以 N:(value / N) * N,得到小于或等于原值的最大 N 的倍数。
3、加上 N/2,使结果位于区间的中心位置:(value / N) * N + N/2。
重复上述步骤对每个通道(R、G、B)进行处理后,颜色总数将减少为 (256/N) × (256/N) × (256/N) 种。
案例
定义了一个用于颜色缩减的函数 colorReduce
/*
cv::Mat image:输入图像(彩色或灰度图像)。
int div = 64:每个通道的颜色缩减因子,默认值为 64。
*/
void colorReduce(cv::Mat image, int div = 64) {int nl = image.rows; // 图像的行数int nc = image.cols * image.channels(); // 每行的总元素数(列数 × 通道数)for (int j = 0; j < nl; j++) { // 遍历每一行uchar* data = image.ptr<uchar>(j); // 获取第 j 行的指针for (int i = 0; i < nc; i++) { // 遍历当前行的所有像素// 对每个像素进行处理data[i] = data[i] / div * div + div / 2;}}
}
int main() {// 加载图像cv::Mat image = cv::imread("boldt.jpg");if (image.empty()) {std::cerr << "Error: Could not load the image!" << std::endl;return -1;}// 处理图像colorReduce(image, 64);// 显示结果cv::namedWindow("Reduced Color Image", cv::WINDOW_AUTOSIZE);cv::imshow("Reduced Color Image", image);// 等待用户按键后退出cv::waitKey(0);return 0;
}
原理
1. 图像数据存储的基本结构
在 OpenCV 中,彩色图像的数据存储遵循以下规则:
1、每个像素由 3 个字节组成,分别对应蓝色(B)、绿色(G)和红色(R)通道。
2、图像数据按行优先存储:
- 第一行的第一个像素对应图像左上角,其数据是 3 个字节(BGR 值)。
- 第二个像素是第一行的第二个像素,依此类推。
3、一个宽度为 W、高度为 H 的彩色图像需要的内存大小为:W × H × 3 字节。
2、行填充(Padding)与有效宽度
为了提高效率,OpenCV 有时会在每一行末尾填充额外的字节。这些填充字节的作用包括:
- 对齐内存:使每行的长度对齐到特定的边界(如 8 字节对齐),以更好地利用硬件特性。
- 性能优化:某些图像处理算法在对齐的内存上运行得更快。
尽管有填充字节,这些额外的数据并不会显示或保存,实际图像的宽度仍然保持不变。
相关属性
- 真实宽度:image.cols 返回图像的真实列数。
- 有效宽度:image.step 返回每行的实际字节数(包括填充字节)。
如果没有填充,image.step 等于 image.cols × image.elemSize()。 - 像素元素大小:image.elemSize() 返回单个像素占用的字节数。
例如,对于 3 通道的短整型矩阵(CV_16SC3),每个像素占用 6 字节(3 × 2 字节)。 - 总像素数:image.total() 返回图像中像素的总数(即矩阵元素数)。
3、计算每行的像素值数量
每行的像素值数量可以通过以下公式计算:
// image.cols 是图像的列数。
// image.channels() 是每个像素的通道数(灰度图像为 1,彩色图像为 3)。
int nc = image.cols * image.channels();
4、使用指针运算访问图像数据
以下是一个典型的双层循环实现,用于遍历图像的所有像素:
for (int j = 0; j < image.rows; j++) { // 遍历每一行uchar* data = image.ptr<uchar>(j); // 获取第 j 行的指针for (int i = 0; i < nc; i++) { // 遍历当前行的所有像素data[i] = data[i] / div * div + div / 2; // 处理每个像素}
}
如果希望进一步简化指针操作,可以在处理过程中直接移动指针。例如:
for (int j = 0; j < image.rows; j++) {uchar* data = image.ptr<uchar>(j);for (int i = 0; i < nc; i++) {*data++ = *data / div * div + div / 2; // 使用指针运算}
}
- *data++ 表示先访问 data 指向的值,然后将指针向前移动一个字节。
- 这种方式避免了显式的索引操作,但需要注意指针的边界。
颜色缩减方案
1、方法一:整数除法
通过整数除法将像素值映射到最近的区间中心位置:
- data[i] / div:将像素值整除 div,得到最接近的倍数。
- (data[i] / div) * div:恢复到该倍数。
-
- div / 2:偏移到区间的中心位置。
// 假设 div = 64,像素值范围为 [0, 255]
// 将像素值分组为若干区间(如 [0, 63], [64, 127], [128, 191], [192, 255])
// 每个区间内的像素值会被映射到该区间的中心位置(如 [0, 63] 映射到 32)
data[i] = (data[i] / div) * div + div / 2;
2、方法二:取模运算
通过取模运算找到最接近的倍数,并调整到区间的中心位置:
- data[i] % div:计算当前像素值相对于 div 的余数。
- data[i] - data[i] % div:得到小于或等于当前像素值的最大倍数。
-
- div / 2:偏移到区间的中心位置。
/* 取模运算可以快速找到像素值所属的区间
例如,当 div = 64 时,像素值 100 的处理过程如下:100 % 64 = 36,计算余数。100 - 36 = 64,得到最接近的倍数。64 + 32 = 96,偏移到区间的中心位置。
*/
data[i] = data[i] - data[i] % div + div / 2;
3、方法三:位运算
如果 div 是 2 的幂(即 div = pow(2, n)),可以使用位运算高效地完成颜色缩减:
- mask = 0xFF << n:生成一个掩码,用于屏蔽最低的 n 位。
- *data &= mask:通过按位与操作保留高阶位,丢弃低阶位。
- *data += div >> 1:加上 div / 2,偏移到区间的中心位置。
/*
假设 div = 16,则 n = 4(因为 16 = 2^4)。
掩码 mask = 0xFF << 4 = 0xF0(十六进制表示为 11110000)。
对于像素值 100 的处理过程如下:100 & 0xF0 = 96,屏蔽低 4 位。96 + 8 = 104,偏移到区间的中心位置。
*/
uchar mask = 0xFF << n; // e.g., for div=16, mask=0xF0
*data &= mask; // 屏蔽低 n 位
*data++ += div >> 1; // 加上 div/2
- 效率高:位运算是硬件级的操作,比整数除法和取模运算更快。
- 适用场景:当 div 是 2 的幂时,位运算是最佳选择。
方法 | 操作 | 优点 | 缺点 |
---|---|---|---|
整数除法 | (data[i] / div) * div + div / 2 | 简单直观,适用于任意 div | 运算速度较慢 |
取模运算 | data[i] - data[i] % div + div / 2 | 计算逻辑清晰 | 速度略优于整数除法,但仍较慢 |
位运算 | *data &= mask; *data++ += div >> 1 | 极其高效,适合 div 为 2 的幂 | 不适用于非 2 的幂的 div |
- 实时处理:位运算因其高效性,特别适合需要高性能的应用场景(如视频处理)。
- 通用性:整数除法和取模运算适用于任意缩减因子,灵活性更高。
- 内存优化:位运算减少了不必要的计算开销,适合嵌入式设备或资源受限的环境。
参数的输入与输出
1、原地处理(In-place Transformation)
在颜色缩减的例子中,我们直接对输入图像进行修改,这被称为原地处理。
然而,在某些应用场景中,用户可能希望保留原始图像不变。此时,用户需要在调用函数前手动复制一份图像。例如:
// 读取图像
cv::Mat image = cv::imread("boldt.jpg");// 克隆图像
cv::Mat imageClone = image.clone();// 对克隆图像进行处理,保持原始图像不变
colorReduce(imageClone);// 显示处理后的图像
cv::namedWindow("Image Result");
cv::imshow("Image Result", imageClone);
通过调用 clone() 方法,可以轻松创建一个图像的深拷贝(Deep Copy),从而避免修改原始图像
2. 提供灵活性的函数设计
为了避免用户手动复制图像,我们可以设计一个更灵活的函数,允许用户选择是否进行原地处理。该函数如下:
void colorReduce(const cv::Mat &image, // 输入图像cv::Mat &result, // 输出图像int div = 64); // 颜色缩减因子 默认值为 64
3. 灵活函数的实现
OpenCV 提供了一个便捷的方法 create,用于确保输出矩阵具有与输入矩阵相同的大小和类型。如果输出矩阵已经满足要求,则不会重新分配内存。
void colorReduce(const cv::Mat &image, cv::Mat &result, int div = 64) {// 确保输出图像具有正确的大小和类型result.create(image.rows, image.cols, image.type());int nl = image.rows; // 图像的行数int nc = image.cols * image.channels(); // 每行的总元素数for (int j = 0; j < nl; j++) { // 遍历每一行const uchar* data_in = image.ptr<uchar>(j); // 获取输入图像第 j 行的指针uchar* data_out = result.ptr<uchar>(j); // 获取输出图像第 j 行的指针for (int i = 0; i < nc; i++) { // 遍历每个像素// 颜色缩减处理data_out[i] = data_in[i] / div * div + div / 2;}}
}
高效扫描连续图像
在 OpenCV 中,如果图像没有填充额外的字节(即每行末尾没有多余像素),它实际上可以被视为一个一维数组。这种特性可以通过 isContinuous 方法检测,或者通过检查 image.step == image.cols * image.elemSize() 来验证。
void colorReduce(cv::Mat image, int div = 64) {int nl = image.rows; // 行数int nc = image.cols * image.channels(); // 每行总元素数// 检查图像是否连续// 如果图像连续,则将其视为一个长的一维数组,减少外层循环次数。// if (image.isContinuous()) {nc = nc * nl; // 总像素数nl = 1; // 将图像视为一维数组// image.reshape(1, 1); // 调整为单行矩阵 (另一方案)}// 计算掩码和 div/2// 使用掩码 mask 和右移操作快速完成颜色缩减int n = static_cast<int>(log(static_cast<double>(div)) / log(2.0) + 0.5);uchar mask = 0xFF << n; // 掩码uchar div2 = div >> 1; // div/2// 扫描图像for (int j = 0; j < nl; j++) {uchar* data = image.ptr<uchar>(j); // 获取第 j 行指针for (int i = 0; i < nc; i++) {*data &= mask; // 屏蔽低 n 位*data++ += div2; // 偏移到区间中心}}
}
优点
- 提高扫描效率:避免不必要的外层循环。
- 灵活性强:支持连续性和非连续性图像
适用场景
- 大规模图像处理任务。
- 需要高效内存访问的应用场景。
低级指针运算
在 OpenCV 的 cv::Mat 类中,图像数据存储在一个连续的内存块中,数据类型通常为 unsigned char。通过直接操作指针,可以高效地访问和处理图像数据。
核心概念
1、图像数据的起始地址
- 使用 image.data 获取图像数据块的起始地址。
- image.data 返回一个指向图像第一个像素的 unsigned char* 指针。
2、行与列的偏移
- 图像的每一行可能包含填充字节,因此每行的实际字节数由 image.step 表示。
- 列的偏移量由每个像素的大小(image.elemSize())决定
3、像素地址计算
任意像素 (j, i) 的地址可以通过以下公式计算
/*
j 是行号。
i 是列号。
image.step 是每行的总字节数(包括填充字节)。
image.elemSize() 是每个像素的字节大小
*/
data = image.data + j * image.step + i * image.elemSize();
void colorReduce(cv::Mat image, int div = 64) {uchar* data = image.data; // 获取图像数据的起始地址for (int j = 0; j < image.rows; j++) { // 遍历每一行uchar* row = image.ptr<uchar>(j); // 获取第 j 行的指针for (int i = 0; i < image.cols * image.channels(); i++) { // 遍历每个像素row[i] = row[i] / div * div + div / 2; // 处理像素}}
}
优点
低级指针运算提供了对图像数据的完全控制,适合性能要求极高的场景。
缺点
- 容易出错,尤其是在处理多通道图像或填充字节时。
- 可读性差,代码维护困难。
使用迭代器扫描图像
cv::Mat 提供了迭代器类(cv::MatIterator_),可以方便地遍历图像的每个像素。迭代器隐藏了底层实现细节,使代码更简洁、安全。
核心思想
1、迭代器的声明:
- 使用 cv::Mat_cv::Vec3b::iterator 声明迭代器。
- cv::Vec3b 表示彩色图像的每个像素(包含 BGR 三个通道)。
2、迭代器的使用:
- 使用 image.begincv::Vec3b() 和 image.endcv::Vec3b() 获取起始和结束迭代器。
- 遍历图像时,通过解引用操作符 *it 访问当前像素。
3、颜色缩减:
- 对每个像素的 BGR 通道值进行位运算和偏移操作。
void colorReduce(cv::Mat image, int div = 64)
{// 确保 div 是 2 的幂int n = static_cast<int>(log(static_cast<double>(div)) / log(2.0) + 0.5);uchar mask = 0xFF << n; // 掩码uchar div2 = div >> 1; // div/2// 获取迭代器// cv::Vec3b 表示每个像素的 BGR 通道值cv::Mat_<cv::Vec3b>::iterator it = image.begin<cv::Vec3b>();cv::Mat_<cv::Vec3b>::iterator itend = image.end<cv::Vec3b>();// 遍历所有像素for (; it != itend; ++it) {// 使用 (*it)[i] 访问第 i 个通道(B=0, G=1, R=2)(*it)[0] &= mask; (*it)[0] += div2; // 蓝色通道(*it)[1] &= mask; (*it)[1] += div2; // 绿色通道(*it)[2] &= mask; (*it)[2] += div2; // 红色通道}
}
编写高效的图像扫描循环
OpenCV 提供了一个方便的函数 cv::getTickCount()。该函数返回自计算机启动以来的时钟周期数。通过在代码执行前后分别获取时钟周期数,可以计算出代码的执行时间。
要将执行时间转换为秒,可以使用另一个方法 cv::getTickFrequency(),它返回每秒的时钟周期数(假设 CPU 频率固定,尽管现代处理器不一定如此)。
const int64 start = cv::getTickCount(); // 获取起始时钟周期
colorReduce(image); // 调用函数
// 计算执行时间(秒)
double duration = (cv::getTickCount() - start) / cv::getTickFrequency();
通过邻域访问扫描图像
在图像处理中,经常需要根据像素的邻域值计算每个像素的新值。当邻域包含前一行和后一行的像素时,就需要同时扫描图像的多行。本节将展示如何实现这一操作。
准备工作
图像锐化的原理是:从图像中减去拉普拉斯算子的结果,可以增强图像边缘,使图像更清晰。
锐化后的像素值计算公式如下:
// left 是当前像素左侧的像素
// up 是上一行对应的像素
sharpened_pixel = 5 * current - left - right - up - down;
实现方法
由于需要访问邻域像素,无法在原图上直接进行处理,必须提供一个输出图像。
使用三个指针分别指向当前行、上一行和下一行。此外,由于每个像素的计算需要邻域信息,无法处理图像的第一行、最后一行以及第一列和最后一列的像素。循环代码如下:
void sharpen(const cv::Mat &image, cv::Mat &result)
{// 如果需要,分配输出图像result.create(image.size(), image.type());int nchannels = image.channels(); // 获取通道数// 遍历所有行(除第一行和最后一行)for (int j = 1; j < image.rows - 1; j++) {const uchar* previous = image.ptr<const uchar>(j - 1); // 上一行const uchar* current = image.ptr<const uchar>(j); // 当前行const uchar* next = image.ptr<const uchar>(j + 1); // 下一行uchar* output = result.ptr<uchar>(j); // 输出行// 遍历所有列(除第一列和最后一列)for (int i = nchannels; i < (image.cols - 1) * nchannels; i++) {// 应用锐化算子*output++ = cv::saturate_cast<uchar>(5 * current[i] - current[i - nchannels] -current[i + nchannels] - previous[i] - next[i]);}}// 将未处理的像素设置为 0// 无法处理第一行、最后一行、第一列和最后一列的像素,因此将这些像素设置为 0result.row(0).setTo(cv::Scalar(0)); // 第一行result.row(result.rows - 1).setTo(cv::Scalar(0)); // 最后一行result.col(0).setTo(cv::Scalar(0)); // 第一列result.col(result.cols - 1).setTo(cv::Scalar(0)); // 最后一列
}
锐化滤波
0 -1 0
-1 5 -1
0 -1 0
为了满足锐化滤波器的要求,当前像素的四个水平和垂直邻居被乘以-1,而当前像素本身则乘以5。
将核应用于图像不仅是方便的表示方法,它是信号处理中卷积概念的基础。
OpenCV定义了一个执行此任务的特殊函数:cv::filter2D 函数。只需定义一个核(以矩阵形式),然后用图像和核调用该函数,它返回滤波后的图像。利用这个函数,重新定义我们的锐化函数如下:
void sharpen2D(const cv::Mat &image, cv::Mat &result) {// 构造核(所有元素初始化为0)cv::Mat kernel(3, 3, CV_32F, cv::Scalar(0));// 赋值给核kernel.at<float>(1, 1) = 5.0;kernel.at<float>(0, 1) = -1.0;kernel.at<float>(2, 1) = -1.0;kernel.at<float>(1, 0) = -1.0;kernel.at<float>(1, 2) = -1.0;// 应用滤波cv::filter2D(image, result, image.depth(), kernel);
}
此实现产生的结果与之前的实现完全相同(且效率相同)。如果输入的是彩色图像,则相同的核会被应用到所有三个通道。 当使用较大的核时,使用
filter2D 函数特别有利,因为它在这种情况下会使用更高效的算法。
执行简单图像算术
由于图像是规则的矩阵,因此可以对它们进行加法、减法、乘法或除法运算。
图像加法
可以通过cv::add函数实现,也可以直接通过矩阵操作如image1 + image2来完成。
当像素值相加后超过255(对于8位无符号图像),需要使用饱和处理,即超过255的值会被截断为255。
cv::Mat result;
cv::add(image1, image2, result); // 使用add函数
// 或者
result = image1 + image2; // 直接相加
指定权重作为标量乘数参与运算
// c[i] = k1 * a[i] + k2 * b[i] + k3;
cv::addWeighted(imageA, k1, imageB, k2, k3, resultC);
指定一个掩码(mask)
// if (mask[i]) c[i] = a[i] + b[i];
cv::add(imageA, imageB, resultC, mask);
如果应用了掩码,则操作仅对掩码值非零的像素执行(掩码必须是单通道的)。可以查看 cv::subtract、cv::absdiff、cv::multiply 和 cv::divide 等函数的不同形式。
OpenCV还提供了按位操作符(对像素的二进制表示逐位操作):cv::bitwise_and、cv::bitwise_or、cv::bitwise_xor和 cv::bitwise_not。cv::min 和 cv::max 操作也非常有用,它们分别计算元素级别的最小值和最大值。
在所有情况下,都会使用 cv::saturate_cast 函数,以确保结果保持在定义的像素值范围内(即避免溢出或下溢)。
图像必须具有相同的大小和类型(如果输出图像的大小与输入不匹配,则会重新分配)。由于操作是逐元素进行的,因此可以将其中一个输入图像用作输出。
还有一些接受单张图像作为输入的操作符可用,例如:
- cv::sqrt(平方根)
- cv::pow(幂运算)
- cv::abs(绝对值)
- cv::cuberoot(立方根)
- cv::exp(指数运算)
- cv::log(对数运算)
图像减法
可以通过cv::subtract函数或直接减法操作完成。这有助于检测图像之间的差异。
cv::Mat result;
cv::subtract(image1, image2, result); // 使用subtract函数
// 或者
result = image1 - image2; // 直接相减
乘法和除法
图像乘法和除法也能以类似的方式完成,分别使用cv::multiply和cv::divide函数,或者直接使用*和/操作符。
cv::Mat result;
cv::multiply(image1, image2, result); // 使用multiply函数
// 或者
result = image1 * image2; // 直接相乘cv::divide(image1, image2, result); // 使用divide函数
// 或者
result = image1 / image2; // 直接相除
在图像融合时可能需要用到加法操作;在比较两个相似图像的不同之处时,则可能用到减法操作。同时,考虑到数值溢出或下溢的问题,合理利用OpenCV提供的函数(如cv::addWeighted用于带权重的加法)可以帮助更有效地处理这些问题
逐通道操作
std::vector<cv::Mat> channels;
cv::split(image, channels); // 分离通道
channels[0] = channels[0] * 2.0; // 对第一个通道进行操作
cv::merge(channels, image); // 合并通道回原图像
重载图像操作
大多数算术函数都有对应的运算符重载。意味着可直接使用C++的运算符来代替调用特定的OpenCV函数,使代码更加紧凑和易读。例如,cv::addWeighted函数可以这样写:
result = 0.7 * image1 + 0.9 * image2;
许多C++运算符都被重载了,包括按位运算符&, |, ^, 和 ~; 最小值、最大值和绝对值函数;以及比较运算符<, <=, ==, !=, >, 和 >=(返回8位二进制图像)。你还可以找到矩阵乘法m1 * m2(其中m1和m2都是cv::Mat实例),矩阵求逆m1.inv(),转置m1.t(),行列式m1.determinant(),向量范数v1.norm(),叉积v1.cross(v2),点积v1.dot(v2)等。当适用时,相应的复合赋值运算符也被定义了(如+=)。
image = (image & cv::Scalar(mask, mask, mask)) + cv::Scalar(div / 2, div / 2, div / 2);
使用cv::Scalar是因为我们处理的是彩色图像。利用这些图像运算符可以使代码变得非常简单,极大地提高了编程效率,因此在多数情况下都应考虑使用它们。
分割图像通道
有时可能希望独立地处理图像的不同通道。
例如,可能只想对图像的一个通道执行某些操作。虽然可以在扫描图像像素的循环中完成这一任务,但也可以使用cv::split函数将一个彩色图像的三个通道复制到三个独立的cv::Mat实例中。
假设想要仅向蓝色通道添加另一张图像,可以按照以下步骤操作:
// 创建包含3个图像的vector
std::vector<cv::Mat> planes;// 将一个3通道图像拆分为3个单通道图像
cv::split(image1, planes);// 向蓝色通道添加另一张图像
planes[0] += image2;// 将3个单通道图像合并为一个3通道图像
// cv::merge函数执行相反的操作,即从三个单通道图像创建一个彩色图像
cv::merge(planes, result);
重映射图像
通过移动图像中的像素来改变其外观。
这个过程中像素的值不会改变,而是每个像素的位置被重新映射到一个新的位置。这种方法可用于创建图像的特殊效果或纠正由镜头引起的图像失真。
简单实现
为了使用OpenCV的remap函数,首先需要定义重映射过程中要使用的映射图,然后将此映射应用于输入图像。
显然,定义映射的方式决定了最终产生的效果。定义了一个变换函数,该函数将在图像上创建波动效果:
// 通过创建波浪效果进行图像重映射
void wave(const cv::Mat &image, cv::Mat &result) {// 映射函数cv::Mat srcX(image.rows, image.cols, CV_32F); // x映射cv::Mat srcY(image.rows, image.cols, CV_32F); // y映射// 创建映射for (int i = 0; i < image.rows; i++) {for (int j = 0; j < image.cols; j++) {// 像素(i,j)的新位置srcX.at<float>(i, j) = j; // 保持在同一列// 原本在第i行的像素现在跟随正弦波移动srcY.at<float>(i, j) = i + 5 * sin(j / 10.0);}}// 应用映射cv::remap(image, // 源图像result, // 目标图像srcX, // x方向映射srcY, // y方向映射cv::INTER_LINEAR // 插值方法);
}
原始位于(i, j)的像素点,在重映射后,其x坐标保持不变(即仍然在原来的列),而y坐标则根据一个正弦函数变化,这样就会产生一种波动的效果。
通过调整正弦函数的参数,可以控制波动的幅度和频率。
cv::remap函数接受源图像、目标图像以及两个映射矩阵(分别对应于x和y方向上的映射)作为输入,并允许指定插值方法以确定如何计算新位置处的像素值。在例子中,使用了线性插值(cv::INTER_LINEAR)来平滑过渡像素值的变化。