opencv(C++)操作图像像素

文章目录

    • 添加噪点的案例
    • 图像像素值
      • 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)来平滑过渡像素值的变化。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/901167.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Python实现贪吃蛇一

贪吃蛇是一款经典的小游戏&#xff0c;最近尝试用Python实现它。先做一个基础版本实现以下目标&#xff1a; 1、做一个按钮&#xff0c;控制游戏开始 2、按Q键退出游戏 3、右上角显示一个记分牌 4、随机生成一个食物&#xff0c;蛇吃到食物后长度加一&#xff0c;得10分 5、蛇碰…

《AI大模型应知应会100篇》第13篇:大模型评测标准:如何判断一个模型的优劣

第13篇&#xff1a;大模型评测标准&#xff1a;如何判断一个模型的优劣 摘要 近年来&#xff0c;大语言模型&#xff08;LLMs&#xff09;在自然语言处理、代码生成、多模态任务等领域取得了显著进展。然而&#xff0c;随着模型数量和规模的增长&#xff0c;如何科学评估这些模…

工会考试重点内容有哪些:核心考点与备考指南

工会考试重点内容总结&#xff1a;核心考点与备考指南 工会考试主要考察考生对工会法律法规、职能职责、实务操作等内容的掌握程度&#xff0c;适用于企事业单位工会干部、社会化工会工作者等岗位的选拔。本文梳理工会考试的核心考点&#xff0c;帮助考生高效备考。 一、工会…

Verilog学习-1.模块的结构

module aoi(a,b,c,d,f);/*模块名为aoi&#xff0c;端口列表a、b、c、d、f*/ input a,b,c,d;/*模块的输入端口为a,b,c,d*/ output f;;/*模块的输出端口为f*/ wire a,b,c,d,f;/*定义信号的数据类型*/ assign f~((a&b)|(~(c&d)));/*逻辑功能描述*/ endmoduleveirlog hdl 程…

MySQL数据库备份与恢复详解

在数据库管理中&#xff0c;数据的备份与恢复是至关重要的一环。对于MySQL数据库&#xff0c;定期备份不仅能防止数据丢失&#xff0c;还能在发生故障时快速恢复数据库。本文将详细介绍MySQL数据库的备份与恢复方法&#xff0c;覆盖所有常用备份和恢复方式&#xff0c;帮助大家…

FFMPEG和opencv的编译

首先 sudo apt-get update -qq && sudo apt-get -y install autoconf automake build-essential cmake git-core libass-dev libfreetype6-dev libgnutls28-dev libmp3lame-dev libsdl2-dev libtool libva-dev libvdpau-dev libvorbis-de…

华为机试—最大最小路

题目 对于给定的无向无根树&#xff0c;第 i 个节点上有一个权值 wi​ 。我们定义一条简单路径是好的&#xff0c;当且仅当&#xff1a;路径上的点的点权最小值小于等于 a &#xff0c;路径上的点的点权最大值大于等于 b 。 保证给定的 a<b&#xff0c;你需要计算有多少条简…

spring cloud微服务开发中声明式服务调用详解及主流框架/解决方案对比

声明式服务调用详解 1. 核心概念 定义&#xff1a;通过配置或注解声明服务调用逻辑&#xff0c;而非手动编写客户端代码&#xff0c;提升开发效率与可维护性。核心特性&#xff1a; 解耦&#xff1a;调用逻辑与业务代码分离内置容错&#xff1a;熔断、超时、重试等动态发现&am…

基于springboot+vue的秦皇岛旅游景点管理系统

开发语言&#xff1a;Java框架&#xff1a;springbootJDK版本&#xff1a;JDK1.8服务器&#xff1a;tomcat7数据库&#xff1a;mysql 5.7数据库工具&#xff1a;Navicat11开发软件&#xff1a;eclipse/myeclipse/ideaMaven包&#xff1a;Maven3.3.9 系统展示 用户登录 旅游路…

【数据结构】之二叉树

二叉树是我们在数据结构中学到的第一个非线性结构&#xff0c;是后续学习更为复杂的树、图结构的基础。本文整理了二叉树的概念定义、基本操作、遍历算法、伪代码与代码实现以及实例说明&#xff0c;方便大家随时查找对应。 一、定义与基本术语 二叉树是一种树形结构&#xf…

Honeyview:快速浏览各类图像

Honeyview是一款免费、轻量级图片查看工具‌&#xff0c;专为快速浏览各类图像设计&#xff0c;支持Windows系统‌。其核心优势在于‌极速加载‌与‌广泛格式兼容性‌&#xff0c;可替代系统自带的图片查看工具&#xff0c;尤其适合需要处理专业图像&#xff08;如PSD、RAW&…

Streamlit性能优化:缓存与状态管理实战

目录 &#x1f4cc; 核心特性 &#x1f4cc; 运行原理 &#xff08;1&#xff09;全脚本执行 &#xff08;2&#xff09;差异更新 &#x1f4cc; 缓存机制 ❓为什么使用缓存&#xff1f; 使用st.cache_data的优化方案 缓存适用场景 使用st.session_state的优化方案 &…

十七、TCP编程

TCP 编程是网络通信的核心&#xff0c;其 API 围绕面向连接的特性设计&#xff0c;涵盖服务端和客户端的交互流程。以下是基于 ​C 语言的 TCP 编程核心 API 及使用流程的详细解析&#xff1a; 核心 API 概览 ​函数​角色​描述socket()通用创建套接字&#xff0c;指定协议族…

将外网下载的 Docker 镜像拷贝到内网运行

将外网下载的 Docker 镜像拷贝到内网运行&#xff0c;可以通过以下步骤实现&#xff1a; 一、在有外网访问权限的机器上操作 下载镜像 使用docker pull命令下载所需的镜像。例如&#xff0c;如果你需要下载一个名为nginx的镜像&#xff0c;可以运行以下命令&#xff1a;docke…

《深入理解生命周期与作用域:以C语言为例》

&#x1f680;个人主页&#xff1a;BabyZZの秘密日记 &#x1f4d6;收入专栏&#xff1a;C语言 &#x1f30d;文章目入 一、生命周期&#xff1a;变量的存在时间&#xff08;一&#xff09;生命周期的定义&#xff08;二&#xff09;C语言中的生命周期类型&#xff08;三&#…

Hqst的超薄千兆变压器HM82409S在Unitree宇树Go2智能机器狗的应用

本期拆解带来的是宇树科技推出的Go2智能机器狗&#xff0c;这款机器狗采用狗身体形态&#xff0c;前端设有激光雷达&#xff0c;摄像头和照明灯。在腿部设有12个铝合金精密关节电机&#xff0c;并配有足端力传感器&#xff0c;通过关节运动模拟狗的运动&#xff0c;并可做出多种…

壹起航:15年深耕,引领中国工厂出海新征程

在全球化浪潮汹涌澎湃的当下&#xff0c;中国工厂正以前所未有的热情和决心&#xff0c;将目光投向广阔的海外市场。然而&#xff0c;出海之路并非一帆风顺&#xff0c;建立品牌、获取稳定询盘、降低营销成本等难题&#xff0c;如同横亘在企业面前的高山&#xff0c;阻碍着他们…

【差分隐私相关概念】基础合成定理和高级合成技术简单关系

差分隐私中的合成定理用于分析多个机制组合时的隐私损失。基础合成定理和高级合成技术分别在不同场景下提供了隐私预算增长的估计&#xff0c;其关系如下&#xff1a; 基础合成定理&#xff08;线性增长&#xff09; 机制组合&#xff1a;当k个满足(ε, δ)-DP的机制按顺序组…

【异常处理】Clion IDE中cmake时头文件找不到 头文件飘红

如图所示是我的clion项目目录 我自定义的data_structure.h和func_declaration.h在unit_test.c中无法检索到 cmakelists.txt配置文件如下所示&#xff1a; cmake_minimum_required(VERSION 3.30) project(noc C) #设置头文件的目录 include_directories(${CMAKE_SOURCE_DIR}/…

MOS的驱动电流怎么计算?

一、MOS 驱动电流的计算方法 MOS 管在开关时&#xff0c;驱动电路主要是给栅极充放电。栅极电流 不是用来维持电流&#xff0c;而是用来克服电容的充放电需求&#xff0c;尤其是总栅极电荷 Qg。 驱动电流估算公式如下&#xff1a; I_drive Qg f_sw&#xff08;Qg&#xff…