OpenCV 图像基本操作

OpenCV快速通关

第一章:OpenCV 简介与环境搭建

第二章:OpenCV 图像基本操作


OpenCV 图像基本操作


第二章:OpenCV 图像基本操作

一、相关结构体与函数介绍

(一)cv::Mat 结构体

cv::Mat是 OpenCV 中用于表示图像和矩阵的核心数据结构。

  • 成员变量
    • rows:表示矩阵的行数,对于图像来说就是图像的高度。例如,对于一个 480 行的图像,rows的值为 480。
    • cols:表示矩阵的列数,即图像的宽度。若图像宽度为 640 像素,则cols为 640。
    • channels:表示图像的通道数。对于灰度图像,其值为 1;对于常见的 BGR 彩色图像,值为 3。例如,在处理彩色图像时,可以通过channels的值来确定如何访问每个像素的不同颜色通道。
    • data:指向存储图像数据的内存地址的指针。通过这个指针,可以直接访问图像的像素值。例如,在一些对性能要求极高且确定不会越界的情况下,可以利用data指针快速遍历图像像素,但需要开发者自行谨慎处理指针运算,确保不出现越界等错误。
  • 构造函数
    • cv::Mat::Mat():默认构造函数,创建一个空的矩阵,可以后续通过create函数等方法分配内存并初始化。例如,可以先创建一个空的cv::Mat对象,然后根据实际需求确定其大小和类型后再进行初始化。
    • cv::Mat::Mat(int rows, int cols, int type):创建一个指定行数、列数和数据类型的矩阵。例如cv::Mat(3, 3, CV_8UC1)创建一个 3x3 的单通道 8 位无符号字符类型(即灰度图像)的矩阵。这里CV_8UC1表示 8 位无符号字符类型,单通道。如果要创建一个 3 通道的 8 位无符号字符类型矩阵,可以使用CV_8UC3
    • cv::Mat::Mat(cv::Size size, int type):使用cv::Size结构指定矩阵的大小(cv::Size(width, height))和数据类型来创建矩阵。例如cv::Mat(cv::Size(100, 200), CV_16SC3)创建一个宽 100 像素,高 200 像素的 3 通道 16 位有符号整数类型的矩阵。
    • cv::Mat::Mat(int rows, int cols, int type, const void* data):使用指定的已存在数据指针来创建矩阵,可用于将外部数据包装成cv::Mat结构进行处理。比如,如果有一个已经在内存中分配好的图像数据缓冲区,可以使用这个构造函数将其转换为cv::Mat对象以便后续使用 OpenCV 的函数进行处理。

(二)cv::imread 函数

  • 函数原型cv::Mat cv::imread(const std::string& filename, int flags = cv::IMREAD_COLOR)
  • 参数说明
    • filename:要读取的图像文件的路径和文件名。例如,如果图像文件名为test.jpg且位于当前项目目录下,可以传入"test.jpg"作为参数。
    • flags:指定图像读取的方式,常用的取值有:
      • cv::IMREAD_COLOR(默认值):以彩色图像方式读取,对于彩色图像,会忽略图像的透明度通道(如果有的话),将图像解码为三通道的 BGR 格式。这是最常用的读取彩色图像的方式,适用于大多数不需要处理透明度信息的场景。
      • cv::IMREAD_GRAYSCALE:以灰度图像方式读取,将图像转换为单通道灰度图像。在一些只关注图像亮度信息,不需要颜色信息的应用中,如某些图像边缘检测算法的预处理阶段,使用该标志可以减少数据量并简化后续处理。
      • cv::IMREAD_UNCHANGED:按图像的原始格式读取,包括图像的颜色通道、透明度通道等信息。如果图像具有透明度通道或者是一些特殊格式的图像,使用该标志可以完整地读取图像数据。
  • 返回值
    • 如果图像读取成功,返回一个cv::Mat对象,表示读取到的图像数据。如果读取失败(例如文件不存在、文件格式不支持等原因),则返回一个空的cv::Mat对象(可以通过mat.empty()函数判断)。例如,在读取图像后,可以使用if (img.empty())来检查图像是否成功读取,如果为空,则可以输出错误信息并进行相应的错误处理。

(三)cv::namedWindow 函数

  • 函数原型void cv::namedWindow(const std::string& winname, int flags = cv::WINDOW_AUTOSIZE)
  • 参数说明
    • winname:窗口的名称,用于标识创建的窗口。例如,可以传入"My Image"作为窗口名称,后续在显示图像等操作时可以通过这个名称来引用该窗口。
    • flags:指定窗口的属性,常用的取值有:
      • cv::WINDOW_AUTOSIZE(默认值):窗口大小会自动根据图像大小调整,用户不能手动改变窗口大小。这种模式适用于只需要展示图像原始大小,不需要用户交互调整窗口的情况。
      • cv::WINDOW_NORMAL:窗口大小可以由用户手动调整。在一些需要用户仔细观察图像细节,可能需要放大或缩小窗口的应用场景中,如图像编辑软件中的图像查看功能,可以使用该标志创建可调整大小的窗口。
  • 功能 :创建一个用于显示图像的窗口。在使用cv::imshow函数显示图像之前,需要先创建一个窗口来承载图像。

(四)cv::imshow 函数

  • 函数原型void cv::imshow(const std::string& winname, InputArray mat)
  • 参数说明
    • winname:要显示图像的窗口名称,该名称必须与之前使用cv::namedWindow创建的窗口名称一致。例如,如果之前创建了名为"My Image"的窗口,这里就需要传入"My Image"
    • mat:要显示的cv::Mat类型的图像数据。例如,可以将读取到的图像cv::Mat对象传入该函数,以在指定窗口中显示图像。
  • 功能:在指定的窗口中显示图像。它是将图像数据可视化的关键函数,通过将图像数据与窗口关联起来,实现图像的展示。

(五)cv::waitKey 函数

  • 函数原型int cv::waitKey(int delay = 0)
  • 参数说明
    • delay:等待按键的时间(以毫秒为单位)。如果delay为 0,则表示无限等待,直到用户按下任意键;如果delay为一个正整数,例如 1000,则表示等待 1000 毫秒(即 1 秒),如果在这段时间内用户按下了键,则函数立即返回,否则在等待时间结束后返回。
  • 返回值:返回用户按下的键的 ASCII 码值。例如,如果用户按下了空格键,可能会返回 32(空格的 ASCII 码)。可以根据返回值来判断用户的操作,从而进行相应的后续处理,如根据不同的按键来切换显示不同的图像或者执行不同的图像处理操作。

(六)cv::destroyAllWindows 函数

  • 功能:销毁所有由 OpenCV 创建的窗口。在程序结束或者不再需要显示图像时,可以调用该函数来释放窗口资源,关闭所有打开的图像显示窗口。例如,在一个图像处理程序中,当完成所有图像的处理和展示后,可以调用该函数来清理界面,避免窗口资源的浪费。

(七)cv::add 函数

  • 函数原型void cv::add(InputArray src1, InputArray src2, OutputArray dst, InputArray mask = noArray(), int dtype = -1)
  • 参数说明
    • src1src2:要相加的两个图像(或矩阵)。它们必须具有相同的大小和类型。例如,可以是两个cv::Mat类型的图像对象,分别表示要进行加法运算的两个图像数据。
    • dst:输出结果图像,用于存储加法运算的结果。其大小和类型与src1src2相同(如果dtype参数未指定特殊类型)。
    • mask:可选的掩码图像,用于指定哪些像素需要进行加法运算(如果不指定,则对所有像素进行运算)。掩码图像是一个与src1等大小的单通道图像,其中非零像素对应的位置才会进行加法运算,零像素对应的位置则保持目标图像dst原来的值不变。
    • dtype:输出图像的数据类型,如果为 -1,则根据输入图像自动确定。例如,如果src1src2都是 8 位无符号整数类型的图像,且dtype为 -1,则dst也将是 8 位无符号整数类型。可以通过指定dtype来改变输出图像的数据类型,如将结果转换为 16 位整数类型等,以满足不同的计算需求。
  • 功能 :实现图像加法运算,将src1src2对应像素的值相加,并将结果存储在dst中。该函数会进行饱和运算,即当相加结果超过数据类型的最大值时,会将结果截断为最大值。例如,对于 8 位无符号整数类型,相加结果超过 255 时,结果将被设置为 255。

(八)cv::subtract 函数

  • 函数原型void cv::subtract(InputArray src1, InputArray src2, OutputArray dst, InputArray mask = noArray(), int dtype = -1)
  • 参数说明
    • cv::add函数类似,src1src2是要相减的两个图像(或矩阵),dst是输出结果图像,mask是可选的掩码图像,dtype是输出图像的数据类型(默认 -1,自动确定)。
  • 功能 :实现图像减法运算,将src1中每个像素的值减去src2对应像素的值,并将结果存储在dst中。同样会进行饱和运算,当减法结果小于数据类型的最小值时,会被截断为最小值。例如,对于 8 位无符号整数类型,相减结果小于 0 时,结果将被设置为 0。

(九)cv::multiply 函数

  • 函数原型void cv::multiply(InputArray src1, InputArray src2, OutputArray dst, double scale = 1, int dtype = -1)
  • 参数说明
    • src1src2:要相乘的两个图像(或矩阵)。
    • dst:输出结果图像。
    • scale:是一个可选的缩放因子,用于在乘法运算后对结果进行缩放。例如,如果scale为 0.5,则乘法运算后的结果会缩小一半。
    • dtype:输出图像的数据类型(默认 -1,自动确定)。
  • 功能 :实现图像乘法运算,将src1src2对应像素的值相乘,并根据scale因子进行缩放后存储在dst中。可用于调整图像的亮度、对比度等,如通过创建一个与图像大小相同且元素值合适的系数矩阵与图像相乘,来改变图像的整体亮度或对比度效果。

(十)cv::divide 函数

  • 函数原型void cv::divide(InputArray src1, InputArray src2, OutputArray dst, double scale = 1, int dtype = -1)
  • 参数说明
    • src1src2:被除数和除数图像(或矩阵)。需要注意分母src2不能为 0,否则会导致错误结果或异常。
    • dst:输出结果图像。
    • scale:可选的缩放因子。
    • dtype:输出图像的数据类型。
  • 功能 :实现图像除法运算,将src1中每个像素的值除以src2对应像素的值,再根据scale因子进行缩放后存储在dst中。在一些特定的图像处理任务中,如对图像进行归一化处理或者根据某种比例关系调整图像像素值时可能会用到。

(十一)cv::remap 函数

  • 函数原型void cv::remap(InputArray src, OutputArray dst, InputArray map1, InputArray map2, int interpolation, int borderMode = BORDER_CONSTANT, const Scalar& borderValue = Scalar())
  • 参数说明
    • src:源图像,即要进行重映射的原始图像,是一个cv::Mat对象。
    • dst:目标图像,用于存储重映射后的图像结果,其大小和类型与src相关,由重映射过程确定。
    • map1map2:分别指定源图像中每个像素在目标图像中的新的 x 坐标和 y 坐标的映射矩阵,它们必须是单通道的cv::Mat对象,且大小与源图像相同。例如,在进行图像旋转、缩放等几何变换时,需要根据变换公式计算出每个像素在目标图像中的新坐标,并存储在这两个映射矩阵中。
    • interpolation:插值方法,用于确定当映射后的坐标不是整数时如何计算像素值,常见的插值方法有:
      • cv::INTER_LINEAR(双线性插值):通过对相邻像素值进行线性插值来计算非整数坐标处的像素值,在图像缩放、旋转等操作中能提供较好的视觉效果,是一种常用的插值方法。
      • cv::INTER_NEAREST(最近邻插值):直接取离非整数坐标最近的像素值作为结果,计算速度相对较快,但可能会导致图像边缘出现锯齿状,在对图像质量要求不高或者计算资源有限的情况下可以使用。
    • borderMode:边界处理模式,用于指定当映射后的坐标超出源图像范围时如何处理边界像素,常见的取值有:
      • BORDER_CONSTANT(默认值):用指定的常数填充边界像素,borderValue参数用于指定这个常数。例如,可以将边界像素设置为黑色(Scalar(0, 0, 0))或白色(Scalar(255, 255, 255))等。
      • BORDER_REPLICATE:复制边界像素的值来填充超出范围的区域。
    • borderValue:当borderModeBORDER_CONSTANT时,用于指定边界填充的值。
  • 功能 :根据给定的映射矩阵map1map2对源图像src进行重映射,得到目标图像dst,常用于图像矫正、几何变换等操作。例如,可以通过计算特定的映射矩阵来实现图像的翻转、旋转、透视变换等效果。

二、访问像素值

在 OpenCV 中,cv::Mat 是用于存储图像数据的核心数据结构。访问图像像素值是图像处理的基础操作之一,其方式取决于图像的数据类型和通道数。

(一)单通道图像像素值访问

对于单通道图像(如灰度图像),可以通过 at 方法来访问像素值。例如,对于一个名为 gray_img 的单通道图像,其像素值访问方式如下:

cpp 复制代码
#include <opencv2\core.hpp>
#include <opencv2\highgui.hpp>
#include <iostream>

int main(int argc, char** argv[])
{
	// 读取灰度图像
	cv::Mat gray_img = cv::imread("E:/pro/cv_code/res/test_img.png", cv::IMREAD_GRAYSCALE);

	// 检查图像是否成功读取
	if (gray_img.empty()) {
		std::cerr << "read gray_img failed!" << std::endl;
		return -1;
	}

	// 遍历图像的每个像素并反转灰度值
	for (int i = 0; i < gray_img.rows; i++) {
		for (int j = 0; j < gray_img.cols; j++) {
			// 使用 at 方法访问像素值,数据类型为 uchar (单通道 8 位无符号整数)
			uchar pixel_value = gray_img.at<uchar>(i, j);
			// 反转灰度值
			gray_img.at<uchar>(i, j) = 255 - pixel_value;
		}
	}
	// 保存反转后的图像"E:/pro/cv_code/res/inverted_gray_img.png"
	cv::imwrite("E:/pro/cv_code/res/inverted_gray_img.png", gray_img);

	return 0;
}

在上述代码中,首先使用 cv::imread 函数以灰度模式读取图像,并通过 empty 方法检查图像是否成功读取。然后,通过两层嵌套循环遍历图像的每一个像素,使用 at 方法获取像素值,并对其进行反转,最后保存。

这里的 at 方法是 cv::Mat 类的模板成员函数,它提供了一种安全的方式来访问矩阵元素(包括图像像素)。其模板参数指定了要访问的元素的数据类型,对于单通道图像,通常为 ucharat 方法会进行边界检查,如果访问的坐标超出了图像的范围,会抛出异常。这有助于在开发过程中及时发现错误,但在一些对性能要求极高且确定不会越界的情况下,可以考虑使用其他更高效但不进行边界检查的方法。

(二)多通道图像像素访问

对于多通道图像(如常见的 BGR 彩色图像),at 方法的使用略有不同。例如,对于一个名为 bgr_img 的 BGR 彩色图像:

cpp 复制代码
#include <opencv2\core.hpp>
#include <opencv2\highgui.hpp>
#include <iostream>

int main(int argc, char** argv[]) {
	// 读取彩色图像
	cv::Mat bgr_img = cv::imread("E:/pro/cv_code/res/test_img.png");

	// 检查图像是否成功读取
	if (bgr_img.empty()) {
		std::cerr << "read bgr_img failed!" << std::endl;
		return -1;
	}

	// 遍历图像的每个像素并将蓝色通道设为0
	for (int i = 0; i < bgr_img.rows; i++) {
		for (int j = 0; j < bgr_img.cols; j++) {
			// 访问 B 通道像素值
			uchar blue_value = bgr_img.at<cv::Vec3b>(i, j)[0];
			// 访问 G 通道像素值
			uchar green_value = bgr_img.at<cv::Vec3b>(i, j)[1];
			// 访问 R 通道像素值
			uchar red_value = bgr_img.at<cv::Vec3b>(i, j)[2];

			// 蓝色通道值设为0
			bgr_img.at<cv::Vec3b>(i, j)[0] = 0;
		}
	}

	// 保存修改后的图像
	cv::imwrite("E:/pro/cv_code/res/no_blue_value.png", bgr_img);
	return 0;
}

这里 cv::Vec3b 是 OpenCV 中用于表示 3 个字节(8 位无符号整数)的向量类型,对应 BGR 三个通道。bgr_img.at<cv::Vec3b>(i, j) 访问第 i 行第 j 列的像素,它是一个 cv::Vec3b 类型的对象,通过 [0][1][2] 分别访问 B、G、R 通道的像素值。这种方式同样存在效率问题,在对性能要求较高的场景下可能不太适用。

补充:Vec3b简单介绍

  • cv::Vec3b 可以看作是 vector<uchar, 3>,即一个 uchar(无符号字符)类型、长度为3的向量。简单来说,cv::Vec3b 就是一个 uchar 类型的数组,长度为3
  • 在图像处理中,cv::Vec3b 用于表示一个像素点的三个颜色通道值,通常用于存储BGR(蓝、绿、红)颜色模型的像素数据。在OpenCV中,颜色通道顺序是BGR而不是RGB
  • 当使用 cv::imread 读取彩色图像时,每个像素点的数据都是 cv::Vec3b 类型。通过 at 方法可以访问和修改特定像素的通道值
    。例如,mat.atcv::Vec3b(row, col)[0] 访问的是第 row 行、第 col 列像素的蓝色通道值,mat.atcv::Vec3b(row, col)[1] 是绿色通道值,mat.atcv::Vec3b(row, col)[2] 是红色通道值

二、用指针扫描图像

使用指针扫描图像可以提高访问像素的效率,特别是在处理大数据量的图像时。通过获取图像数据的指针,可以直接在内存中遍历像素。

(一)单通道图像指针扫描

对于单通道图像:

cpp 复制代码
#include <opencv2\core.hpp>
#include <opencv2\highgui.hpp>
#include <iostream>

// 单通道图像指针扫描
int main(int argc, char** argv[]) {

	cv::Mat gray_img = cv::imread("E:/pro/cv_code/res/test_img.png", cv::IMREAD_GRAYSCALE);

	// 检查图像是否成功读取
	if (gray_img.empty()) {
		std::cerr << "read gray_img failed!" << std::endl;
		return -1;
	}

	// 获取图像指针
	uchar* ptr = gray_img.ptr<uchar>(0);

	// 遍历图像的每个像素值并将像素值都+10(防止溢出)
	for (int i = 0; i < gray_img.rows; i++) {
		for (int j = 0; j < gray_img.cols; j++) {
			// 通过指针访问像素值
			uchar pixel_value = ptr[i * gray_img.cols + j];
			// 进行操作,如将像素值+10,(注意防止溢出)
			// 使用 cv::saturate_cast 进行操作,防止溢出
			ptr[i * gray_img.cols + j] = cv::saturate_cast<uchar>(pixel_value + 10);
		}
	}

	cv::imwrite("E:/pro/cv_code/res/enhanced_gray_img.png", gray_img);

	return 0;
}

gray_img.ptr<uchar>(0) 获取指向图像第一行数据的指针。在循环中,通过 ptr[i * gray_img.cols + j] 可以直接访问第 i 行第 j 列的像素值,这种方式避免了 at 方法的一些额外开销,如边界检查等,提高了访问速度。但需要注意,由于没有边界检查,如果指针操作不当可能导致内存访问错误。

(二)多通道图像指针扫描

对于多通道图像:

cpp 复制代码
#include <opencv2\core.hpp>
#include <opencv2\highgui.hpp>
#include <iostream>

int main(int argc, char** argv[]) {
	// 读取彩色图像
	cv::Mat bgr_img = cv::imread("E:/pro/cv_code/res/test_img.png");
	if (bgr_img.empty()) {
		std::cerr << "read bgr_img failed!" << std::endl;
		return -1;
	}
	// 获取图像指针
	cv::Vec3b* ptr = bgr_img.ptr<cv::Vec3b>(0);

	// 遍历图像的每个像素
	for (int i = 0; i < bgr_img.rows; i++) {
		for (int j = 0; j < bgr_img.cols; j++) {
			// 访问单通道像素值
			uchar blue_val = ptr[i * bgr_img.cols + j][0];
			uchar green_val = ptr[i * bgr_img.cols + j][1];
			uchar red_val = ptr[i * bgr_img.cols + j][2];

			// 将绿色通道值设为红色和蓝色通道值的平均值
			ptr[i * bgr_img.cols + j][1] = (blue_val + red_val) / 2;
		}
	}
	// 保存处理后的图像
	cv::imwrite("E:/pro/cv_code/res/modified_color_image.jpg", bgr_img);
	return 0;
}

这里 bgr_img.ptr<cv::Vec3b>(0) 获取指向彩色图像第一行数据的指针,每个元素是 cv::Vec3b 类型,通过 [0][1][2] 访问 B、G、R 通道像素值。同样,使用指针扫描多通道图像时也需要谨慎处理边界情况。

三、用迭代器扫描图像

OpenCV 提供了迭代器来扫描图像,这种方式使得代码在一定程度上更加简洁和安全,尤其是在处理图像边界等问题时。

(一)单通道图像迭代器扫描

对于单通道图像:

cpp 复制代码
//	单通道图像迭代器扫描
int main(int argc, char** argv[]) {
	cv::Mat gray_img = cv::imread("E:/pro/cv_code/res/test_img.png", cv::IMREAD_GRAYSCALE);

	// 检查图像是否成功读取
	if (gray_img.empty()) {
		std::cerr << "read gray_img failed!" << std::endl;
		return -1;
	}

	// 创建迭代器(可修改)
	cv::MatIterator_<uchar> it = gray_img.begin<uchar>();
	cv::MatIterator_<uchar> it_end = gray_img.end<uchar>();
	// 遍历每个像素
	for (; it != it_end; ++it) {
		// 访问像素值
		uchar pixel_value = *it;
		// 对像素值取反
		*it = 255 - pixel_value;
	}
	// 保存处理后的图像
	cv::imwrite("E:/pro/cv_code/res/inverted_gray_image.jpg", gray_img);

	return 0;
}

gray_img.begin<uchar>() 创建指向图像第一个像素的迭代器,gray_img.end<uchar>() 创建指向图像最后一个像素之后的迭代器。通过迭代器遍历图像像素,代码更加简洁明了,并且不用担心越界问题,因为迭代器会自动处理边界情况。但迭代器的使用可能会引入一些额外的开销,相比指针扫描在效率上可能稍逊一筹。

(二)多通道图像迭代器扫描

对于多通道图像:

cpp 复制代码
int main() {
	// 读取彩色图像
	cv::Mat bgr_img = cv::imread("E:/pro/cv_code/res/test_img.png");

	// 检查图像是否成功读取
	if (bgr_img.empty()) {
		std::cerr << "read bgr_img failed!" << std::endl;
		return -1;
	}

	// 创建可修改的迭代器
	cv::MatIterator_<cv::Vec3b> it = bgr_img.begin<cv::Vec3b>();
	cv::MatIterator_<cv::Vec3b> it_end = bgr_img.end<cv::Vec3b>();

	// 遍历图像的每个像素
	for (; it != it_end; ++it) {
		// 访问通道像素值
		uchar blue_value = (*it)[0];
		uchar green_value = (*it)[1];
		uchar red_value = (*it)[2];

		// 将蓝色通道值设为绿色通道值的两倍,确保不超过255
		(*it)[0] = std::min(static_cast<int>(green_value) * 2, 255);
	}

	// 保存处理后的图像
	cv::imwrite("E:/pro/cv_code/res/modified_color_image.png", bgr_img);

	return 0;
}

bgr_img.begin<cv::Vec3b>()bgr_img.end<cv::Vec3b>() 分别创建和结束多通道图像的迭代器,通过 (*it)[0](*it)[1](*it)[2] 访问通道像素值。

四、编写高效的图像扫描循环

在编写图像扫描循环时,除了选择合适的访问方式(如指针、迭代器或 at 方法),还需要考虑一些优化技巧。

(一)避免循环内无关计算

首先,尽量避免在循环体内进行复杂的计算或函数调用,特别是那些与图像像素值无关的操作。例如:

cpp 复制代码
cv::Mat img = cv::imread("image.jpg");
if (img.empty()) {
    std::cerr << "无法读取图像!" << std::endl;
    return -1;
}
int sum = 0;
for (int i = 0; i < img.rows; i++) {
    for (int j = 0; j < img.cols; j++) {
        // 访问像素值并计算总和
        sum += img.at<uchar>(i, j);
    }
}
// 不要在循环内进行无关计算,如下方代码会降低效率
// for (int i = 0; i < img.rows; i++) {
//     for (int j = 0; j < img.cols; j++) {
//         // 访问像素值并计算总和
//         sum += img.at<uchar>(i, j);
//         // 无关计算,计算当前像素坐标的平方和
//         int temp = i * i + j * j; 
//     }
// }

在上述代码中,如果在循环内添加与像素值无关的计算,会增加每次循环的执行时间,降低整体效率。因为循环会对图像的每个像素进行操作,额外的计算会被重复执行多次。

(二)多线程并行处理

其次,如果可能,可以将图像分割成多个子区域,利用多线程并行处理不同的子区域,以提高处理速度。例如,使用 OpenMP 库来实现简单的并行图像扫描:

cpp 复制代码
#include <iostream>
#include <opencv2/opencv.hpp>
#include <omp.h>

int main() {
	// 读取灰度图像
	cv::Mat img = cv::imread("E:/pro/cv_code/res/test_img.png", cv::IMREAD_GRAYSCALE);
	if (img.empty()) {
		std::cerr << "read bgr_img failed!" << std::endl;
		return -1;
	}

	// 变量用于存储像素值总和
	long long sum = 0; // 使用 long long 以避免溢出

	// 使用 OpenMP 并行区域
#pragma omp parallel
	{
		long long private_sum = 0; // 每个线程的私有总和

		// 使用 OpenMP 并行循环
#pragma omp for
		for (int i = 0; i < img.rows; i++) {
			for (int j = 0; j < img.cols; j++) {
				// 访问像素值并计算总和
				private_sum += img.at<uchar>(i, j);
			}
		}

		// 使用 reduction 汇总每个线程的结果
#pragma omp atomic
		sum += private_sum;
	}

	// 输出结果
	std::cout << "图像像素值总和: " << sum << std::endl;

	return 0;
}

在上述代码中,#pragma omp parallel for reduction(+:sum) 指令告诉编译器将外层循环并行化,并将每个线程计算的部分和累加到 sum 变量中。OpenMP 会自动根据系统的核心数分配线程,提高计算效率。但需要注意,在实际应用中,并非所有的图像处理任务都适合并行化,有些任务可能存在数据依赖等问题,需要仔细分析。例如,如果在处理过程中某个像素的计算依赖于相邻像素的处理结果,那么简单的并行化可能会导致错误的结果。

五、扫描图像并访问相邻像素

在很多图像处理算法中,不仅需要访问当前像素值,还需要访问其相邻像素。例如,在图像边缘检测算法中,需要比较当前像素与其周围像素的灰度值变化。

以下是一个简单的示例,计算图像中每个像素与其相邻像素的灰度值差值之和:

cpp 复制代码
#include <opencv2\core.hpp>
#include <opencv2\highgui.hpp>
#include <iostream>

int main() {
	// 读取灰度图像
	cv::Mat gray_img = cv::imread("E:/pro/sdl_code/res/test_img.png", cv::IMREAD_GRAYSCALE);
	if (gray_img.empty()) {
		std::cerr << "read gray_img failed!" << std::endl;
		return -1;
	}

	cv::Mat diff_img(gray_img.size(), CV_16S, cv::Scalar(0));

	// 遍历图像的每个像素(排除边界)
	for (int i = 1; i < gray_img.rows - 1; i++) {
		for (int j = 1; j < gray_img.cols - 1; j++) {
			short diff_sum = 0;

			// 访问当前像素的 8 个相邻像素并计算差值
			for (int di = -1; di <= 1; di++) {
				for (int dj = -1; dj <= 1; dj++) {
					if (di == 0 && dj == 0) {
						continue; // 跳过
					}
					uchar neb_val = gray_img.at<uchar>(i + di, j + dj);
					diff_sum += static_cast<short>(gray_img.at<uchar>(i, j) - neb_val);
				}
			}
			// 将差值存储到差值图像中
			diff_img.at<short>(i, j) = diff_sum;
		}
	}
	cv::imwrite("E:/pro/sdl_code/res/ahazhi_image.png", gray_img);

	return 0;
}

在上述代码中,首先创建了一个与原图像大小相同的 diff_img 图像,用于存储每个像素与其相邻像素的灰度值差值之和。然后,通过两层嵌套循环遍历图像内部的像素(避开边界像素,因为边界像素没有完整的 8 个相邻像素),再通过两层嵌套循环遍历当前像素的 8 个相邻像素,计算灰度值差值并累加到 diff_sum 变量中,最后将 diff_sum 存储到 diff_img 图像的对应位置。这里使用 CV_16S 类型来存储差值,是为了避免在计算差值时可能出现的溢出问题,因为灰度值相减可能得到负数。如果使用 uchar 类型存储差值,当差值为负时会出现错误结果。

详细分析:

如果觉着不太明白可以看下面啰嗦点的示例:

好的,下面我将逐步详细解释如何计算每个像素与其 8 个相邻像素的差值,并以示例图像为基础进行说明。

示例图像

假设我们有以下 5x5 灰度图像:

[ 100, 150, 200, 150, 100 ]
[ 120, 180, 240, 180, 120 ]
[ 130, 190, 255, 190, 130 ]
[ 120, 180, 240, 180, 120 ]
[ 100, 150, 200, 150, 100 ]

我们将重点分析位于 (2, 2) 的像素(值为 255)以及它的 8 个相邻像素。

1. 确定当前像素和相邻像素

当前像素为 (2, 2),其值为 255。其相邻像素为:

(1, 1) = 180   (1, 2) = 240   (1, 3) = 180
(2, 1) = 190   (2, 2) = 255   (2, 3) = 190
(3, 1) = 180   (3, 2) = 240   (3, 3) = 180

2. 计算差值

我们将计算当前像素值与每个相邻像素的差值。计算公式为:

差值 = 当前像素值 - 相邻像素值

3.具体计算步骤

  1. 与上方相邻像素 (1, 1)

    • 当前像素:255
    • 相邻像素:180
    • 差值:255 - 180 = 75
  2. 与上方右侧相邻像素 (1, 2)

    • 当前像素:255
    • 相邻像素:240
    • 差值:255 - 240 = 15
  3. 与上方左侧相邻像素 (1, 3)

    • 当前像素:255
    • 相邻像素:180
    • 差值:255 - 180 = 75
  4. 与左侧相邻像素 (2, 1)

    • 当前像素:255
    • 相邻像素:190
    • 差值:255 - 190 = 65
  5. 与右侧相邻像素 (2, 3)

    • 当前像素:255
    • 相邻像素:190
    • 差值:255 - 190 = 65
  6. 与下方左侧相邻像素 (3, 1)

    • 当前像素:255
    • 相邻像素:180
    • 差值:255 - 180 = 75
  7. 与下方相邻像素 (3, 2)

    • 当前像素:255
    • 相邻像素:240
    • 差值:255 - 240 = 15
  8. 与下方右侧相邻像素 (3, 3)

    • 当前像素:255
    • 相邻像素:180
    • 差值:255 - 180 = 75

4. 汇总差值

将所有差值相加,得到 diff_sum

diff_sum = 75 + 15 + 75 + 65 + 65 + 75 + 15 + 75
          = 465

5.存储结果

将计算得到的 diff_sum 存储在差值图像 diff_img 的对应位置 (2, 2) 中。这样,diff_img 中的 (2, 2) 位置的值将为 465

6.处理整个图像

对于整个图像,您将重复上述步骤,计算每个像素与其 8 个相邻像素的差值。在边界像素处,您将跳过无法访问的邻居(即不在图像范围内的像素)。

六、实现简单的图像运算

图像运算在图像处理中非常常见,包括图像加法、减法、乘法、除法等。

(一)图像加法

图像加法可以用于图像融合、亮度调整等。OpenCV 提供了 cv::add 函数来实现图像加法。例如:

cpp 复制代码
cv::Mat img1 = cv::imread("image1.jpg");
cv::Mat img2 = cv::imread("image2.jpg");
if (img1.empty() || img2.empty()) {
    std::cerr << "无法读取图像!" << std::endl;
    return -1;
}
// 确保两张图像大小和类型相同
if (img1.size() == img2.size() && img1.type() == img2.type()) {
    cv::Mat result;
    cv::add(img1, img2, result);  // 图像加法
    cv::imshow("Result of Addition", result);
    cv::waitKey(0);
} else {
    std::cout << "图像大小或类型不匹配" << std::endl;
}

cv::add 函数将 img1img2 对应位置的像素值相加,并将结果存储在 result 图像中。如果像素值相加超过了数据类型的范围(如 8 位无符号整数的 255),cv::add 函数会进行饱和运算,即超过范围的值会被截断为最大值。这种饱和运算可以避免出现溢出错误导致的图像异常。

也可以使用重载的 + 运算符来实现图像加法:

cpp 复制代码
cv::Mat result = img1 + img2;

但需要注意,使用 + 运算符时,如果像素值相加超过了数据类型的范围,会发生溢出,例如对于 8 位无符号整数类型,相加结果大于 255 时会出现错误结果。所以在对结果准确性要求较高且可能出现溢出的情况下,建议使用 cv::add 函数。

(二)图像减法

图像减法可用于检测图像之间的差异,例如在背景减除算法中。OpenCV 提供了 cv::subtract 函数:

cpp 复制代码
cv::Mat img1 = cv::imread("image1.jpg");
cv::Mat img2 = cv::imread("image2.jpg");
if (img1.empty() || img2.empty()) {
    std::cerr << "无法读取图像!" << std::endl;
    return -1;
}
if (img1.size() == img2.size() && img1.type() == img2.type()) {
    cv::Mat result;
    cv::subtract(img1, img2, result);  // 图像减法
    cv::imshow("Result of Subtraction", result);
    cv::waitKey(0);
} else {
    std::cout << "图像大小或类型不匹配" << std::endl;
}

cv::subtract 函数将 img1 中每个像素值减去 img2 对应位置的像素值,并将结果存储在 result 中。同样,使用 - 运算符也可实现图像减法,但存在溢出问题,cv::subtract 函数会对结果进行截断处理,确保结果在数据类型的有效范围内。例如,当用 8 位无符号整数类型存储像素值时,相减结果小于 0 时会被截断为 0,而不是出现负数结果导致图像显示异常。

(三)图像乘法

图像乘法可以用于调整图像的对比度等。OpenCV 提供了 cv::multiply 函数:

cpp 复制代码
cv::Mat img = cv::imread("image.jpg");
if (img.empty()) {
    std::cerr << "无法读取图像!" << std::endl;
    return -1;
}
// 创建一个与图像大小相同的系数矩阵,例如将图像亮度降低一半
cv::Mat alpha = cv::Mat::ones(img.size(), img.type()) * 0.5;
cv::Mat result;
cv::multiply(img, alpha, result);  // 图像乘法
cv::imshow("Result of Multiplication", result);
cv::waitKey(0);

在上述代码中,首先创建了一个与原图像 img 大小相同且数据类型一致的 alpha 矩阵,其元素值均为 0.5。然后通过 cv::multiply 函数将 imgalpha 对应元素相乘,得到调整对比度后的图像 result。这里需要注意,如果 alpha 矩阵中的值过大或过小,可能会导致图像像素值超出有效范围,从而使图像显示效果变差。例如,若 alpha 矩阵元素值大于 1,图像可能会过度曝光;若小于 0,图像可能会变为全黑或出现奇怪的颜色变化。

(四)图像除法

图像除法相对较少使用,但在一些特定的图像处理任务中也有应用。OpenCV 提供了 cv::divide 函数:

cpp 复制代码
cv::Mat img1 = cv::imread("image1.jpg");
cv::Mat img2 = cv::imread("image2.jpg");
if (img1.empty() || img2.empty()) {
    std::cerr << "无法读取图像!" << std::endl;
    return -1;
}
if (img1.size() == img2.size() && img1.type() == img2.type()) {
    cv::Mat result;
    cv::divide(img1, img2, result);  // 图像除法
    cv::imshow("Result of Division", result);
    cv::waitKey(0);
} else {
    std::cout << "图像大小或类型不匹配" << std::endl;
}

使用 cv::divide 函数时,要确保 img2 中的像素值不为 0,否则会出现除零错误。在实际应用中,例如在某些图像归一化或比例调整的算法中,可能会用到图像除法来根据特定的规则调整图像像素值之间的比例关系。

七、图像重映射

图像重映射是将图像中的像素从一个位置映射到另一个位置的操作,常用于图像矫正、几何变换等。

OpenCV 中通过 cv::remap 函数实现图像重映射。该函数需要两个映射矩阵 map_xmap_y,分别指定源图像中每个像素在目标图像中的新的 x 坐标和 y 坐标。

以下是一个简单的示例,实现图像的垂直翻转:

cpp 复制代码
cv::Mat img = cv::imread("image.jpg");
if (img.empty()) {
    std::cerr << "无法读取图像!" << std::endl;
    return -1;
}
cv::Mat map_x(img.size(), CV_32FC1);
cv::Mat map_y(img.size(), CV_32FC1);
for (int i = 0; i < img.rows; i++) {
    for (int j = 0; j < img.cols; j++) {
        // 垂直翻转的映射关系,将原图像的第 i 行映射到目标图像的倒数第 i 行
        map_x.at<float>(i, j) = j;
        map_y.at<float>(i, j) = img.rows - 1 - i;
    }
}
cv::Mat remapped_img;
cv::remap(img, remapped_img, map_x, map_y, cv::INTER_LINEAR);
cv::imshow("Original Image", img);
cv::imshow("Remapped Image", remapped_img);
cv::waitKey(0);

在上述代码中,首先创建了与原图像大小相同的 map_xmap_y 映射矩阵,其数据类型为 CV_32FC1(32 位浮点数单通道)。然后根据垂直翻转的规则填充映射矩阵,即 map_x 保持列坐标不变,map_y 将原图像的行坐标映射到目标图像的倒数行坐标。最后使用 cv::remap 函数根据映射矩阵对原图像进行重映射,得到垂直翻转后的图像 remapped_img。其中 cv::INTER_LINEAR 是重映射时使用的插值方法,它采用双线性插值,能够在像素位置发生变化时,根据周围像素的值进行平滑过渡,使图像在变换后看起来更加自然。如果选择其他插值方法,如 cv::INTER_NEAREST(最近邻插值),则在变换后的图像中可能会出现锯齿状边缘等不连续的现象,但最近邻插值的计算速度相对较快,在对图像质量要求不高且追求速度的场景下可以使用。

图像重映射还可以实现更复杂的几何变换,如旋转、缩放、透视变换等,只需要根据相应的几何变换公式计算出映射矩阵即可。例如,对于图像的旋转,可以根据旋转角度和旋转中心计算出每个像素在旋转后的新坐标,填充到映射矩阵中,然后使用 cv::remap 函数实现图像旋转。假设要对图像绕其中心旋转 theta 角度,旋转中心坐标为 (cx, cy),则对于图像中的任意像素点 (x, y),其旋转后的坐标 (x', y') 可以通过以下公式计算:

根据这个公式,可以创建映射矩阵 map_xmap_y,将计算得到的旋转后坐标填充进去,再使用 cv::remap 函数进行图像旋转操作。

总结

通过对以上这些 OpenCV 图像基本操作的学习,能够为进一步深入学习 OpenCV 的图像处理、计算机视觉算法奠定坚实的基础,如利用这些操作构建图像滤波算法、图像特征提取算法的基础模块等。在实际应用中,这些基本操作也常常被组合使用,以实现复杂的图像处理和分析任务。例如,在图像增强算法中,可能会先进行图像扫描获取像素统计信息,然后根据这些信息进行图像运算调整像素值,最后通过图像重映射实现特定的几何变换效果,从而达到增强图像视觉效果或提取特定图像信息的目的。

相关推荐
加德霍克1 分钟前
Opencv之识别图片颜色并绘制轮廓
图像处理·人工智能·opencv·学习·计算机视觉
OJAC近屿智能4 分钟前
苹果全家桶接入ChatGPT,近屿智能邀您共绘AI蓝图
人工智能·ai·chatgpt·aigc
jndingxin13 分钟前
OpenCV相机标定与3D重建(15)计算给定图像点对应的极线(epipolar lines)函数computeCorrespondEpilines()的使用
opencv·3d
MongoDB 数据平台15 分钟前
Alan Chhabra:MongoDB AI应用程序计划(MAAP) 为客户提供价值
数据库·人工智能·mongodb
网智圈--一起AI赋能网络18 分钟前
【AI+网络/仿真数据集】1分钟搭建云原生端到端5G网络
网络·人工智能·5g·云原生·信息与通信
三月七(爱看动漫的程序员)18 分钟前
The Rise and Potential of Large Language ModelBased Agents:A Survey---代理社会
人工智能·深度学习·机器学习·语言模型·自然语言处理·chatgpt·langchain
mingdaoyun21 分钟前
零代码平台在智慧城市政务信息化软件项目管理领域的实践
人工智能·智慧城市·政务
肉包之30 分钟前
OpenCV实验:图片加水印
人工智能·python·opencv·计算机视觉
带带老表学爬虫36 分钟前
opencv礼帽和黑帽运算
人工智能·opencv·计算机视觉
卧蚕土豆1 小时前
【有啥问啥】大语言模型Prompt中的“System指令”:深入剖析与误区澄清
人工智能·语言模型·prompt