当前位置: 首页 > news >正文

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
    // 无法处理第一行、最后一行、第一列和最后一列的像素,因此将这些像素设置为 0
    result.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)来平滑过渡像素值的变化。

相关文章:

  • 做投票网站有没有免费的crm系统软件
  • 天元建设集团最新现状seo中心
  • 凡科免费建微信小程序网站怎么做表格
  • 百度做任务的网站百度网站排名搜行者seo
  • .me做社区网站企业网站营销的优缺点
  • 网站建设及相关流程图佛山做网站建设
  • 【NLP 面经 8】
  • pycharm连接autodl训练遇到绝对路径问题
  • 如何应对客户频繁变更需求
  • CMake使用
  • 李贵永任香港共工新闻社副社长
  • /sys/fs/cgroup/memory/memory.stat 关键指标说明
  • 山东大学离散数学第八章习题解析
  • 力扣hot100_回溯(2)_python版本
  • 升级 SAP S/4 HANA 之 EWM 攻略
  • aws(学习笔记第三十八课) codepipeline-build-deploy-github-manual
  • 系统配置篇,修改sem值
  • Docker 全面解析:从基础概念到实际应用
  • ARP攻击 DAI动态ARP检测学习笔记(超详细)
  • python网络爬虫
  • 一种反激变换器的设计思路(01)
  • golang-defer延迟机制
  • Spring Boot 邮件发送配置遇到的坑:解决 JavaMailSenderImpl 未找到的错误
  • 力扣第444场周赛
  • springMVC-拦截器详解
  • 算法精讲【整数二分】(实战教学)