OpenCV实战(5)——图像运算详解
- 0. 前言
- 1. 图像基本运算
- 2. 重载图像运算符
- 2.1 加法运算符重载
- 2.2 分割图像通道
- 2.3 完整代码
- 3. 图像重映射
- 3.1 OpenCV 重映射函数
- 3.2 完整代码
- 小结
- 系列链接
0. 前言
图像可以以不同的方式进行组合,因为它们是正则矩阵,所以它们可以可以执行加、减、乘或除等运算。 OpenCV
提供了各种图像算术运算符,本节将学习如何使用不同的图像运算符。我们会加载二张图像,并使用算术运算符这两张图像结合起来。
1. 图像基本运算
在本节中,我们对两个图像执行加法运算,这在要创建一些特殊效果或在图像上叠加信息时非常有用。
(1) 调用 cv::add
函数,或者更准确地说,调用 cv::addWeighted
函数,因为我们需要求取图像的加权和:
cv::addWeighted(image1,0.7,image2,0.9,0.,result);
该操作会生成一个新图像,如下图所示:
所有 OpenCV
算术运算函数的工作方式都相同。提供了两个输入,第三个参数指定了输出。在某些情况下,可以指定在操作中用作标量乘数的权重,可以使用多种形式调用这些函数,例如,使用 cv::add
函数:
// c[i]= a[i]+b[i];
cv::add(imageA,imageB,resultC);
// c[i]= a[i]+k;
cv::add(imageA,cv::Scalar(k),resultC);
// c[i]= k1*a[1]+k2*b[i]+k3;
cv::addWeighted(imageA,k1,imageB,k2,k3,resultC);
// c[i]= k*a[1]+b[i];
cv::scaleAdd(imageA,k,imageB,resultC);
对于某些函数,我们还可以指定掩码进行调用:
// (mask[i]) c[i]= a[i]+b[i];
cv::add(imageA,imageB,resultC,mask);
如果应用掩码,则仅对掩码值不为空的像素执行操作(掩码必须是单通道),除了 cv::add
函数外,还可以调用 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::Mat
实例或 cv::Mat
实例的各个通道上使用常用的 C++
算术运算符。
2. 重载图像运算符
2.1 加法运算符重载
大多数算术函数都在 OpenCV
中重载了相应的运算符。 例如,对 cv::addWeighted
的调用可以使用以下形式:
result= 0.7*image1+0.9*image2;
使用以上代码更加紧凑,也更易于阅读,这种计算加权和的方式与 cv::addWeighted(image1,0.7,image2,0.9,0.,result);
是等价的,且 cv::saturate_cast
函数在这两种情况下都会被调用。
大多数 C++
运算符都已重载,其中包括按位运算符 &
、|
、^
和 ~
,min
、max
和 abs
函数等;比较运算符 <
、<=
、==
、!=
、>
和 >=
,它们返回一个 8
位二值图像。此外,还包括 m1*m2
矩阵乘法、m1.inv()
矩阵求逆、m1.t()
转置、m1.determinant()
行列式(其中 m1
和 m2
都是 cv::Mat
实例)、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
。统计使用此方法的执行时间,可以看到使用图像运算符使代码变得更简单,且程序的编写也更加高效,因此在大多数情况下应当考虑使用算术运算符。
2.2 分割图像通道
有时我们希望单独处理图像的不同通道。例如,我们可能只想对图像的一个通道执行操作。我们当然可以在图像扫描循环中实现这一操作。但除此之外,也可以使用 cv::split
函数将彩色图像的三个通道复制到三个不同的 cv::Mat
实例中。假设我们只想将第二个图像添加到蓝色通道:
// 创建3个图像的矢量
std::vector<cv::Mat> planes;
// 分割图像通道
cv::split(image1,planes);
// 将image2图像添加到蓝色通道上
planes[0] += image2;
// 合并三个通道
cv::merge(planes,result);
cv::merge 函数是cv::split函数的逆操作,它可以利用三个单通道图像创建一个彩色图像。
2.3 完整代码
完整代码如下所示:
#include <vector>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
int main() {
cv::Mat image1;
cv::Mat image2;
// 读取图像
image1 = cv::imread("1.png");
image2 = cv::imread("rain.jpg");
cv::resize(image2, image2, cv::Size(image1.cols, image1.rows));
if (!image1.data) return 0;
if (!image2.data) return 0;
cv::namedWindow("Image 1");
cv::imshow("Image 1", image1);
cv::namedWindow("Image 2");
cv::imshow("Image 2", image2);
cv::Mat result;
// 对两个图像执行相加运算
cv::addWeighted(image1, 0.7, image2, 0.9, 0.0, result);
cv::namedWindow("result");
cv::imshow("result", result);
//使用重载运算符
result = 0.7*image1+0.9*image2;
cv::namedWindow("Result with operators");
cv::imshow("Result with operators", result);
image2 = cv::imread("rain.jpg", 0);
cv::resize(image2, image2, cv::Size(image1.cols, image1.rows));
// 分割彩色图像通道
std::vector<cv::Mat> planes;
cv::split(image1, planes);
planes[0] += image2;
// 合并通道
cv::merge(planes, result);
cv::namedWindow("Result on blue channel");
cv::imshow("Result on blue channel", result);
cv::waitKey();
return 0;
}
3. 图像重映射
我们已经学习了如何读取和修改图像的像素值。最后,我们将学习如何通过移动像素来修改图像,在此过程不会改变像素值;而是将每个像素的位置重新映射到新位置。这对于在图像上创建特殊效果或校正由镜头等引起的图像失真时非常有用。
3.1 OpenCV 重映射函数
为了使用 OpenCV
重映射函数,首先需定义要在重映射过程中使用的映射,然后,将此映射应用于输入图像。显然,定义映射的方式将决定将产生的效果。在本节中,我们定义转换函数用于在图像上创建波浪效果。
重新映射的目的是生成一个新图像,其中像素的位置发生了变化。为了构建这个新图像,我们需要知道目标图像中每个像素的原始位置在源图像中的位置。因此,所需的映射函数需要根据原始像素位置计算新像素位置;反向映射 (backward mapping
) 则描述了新图像的像素如何映射回原始图像。在 OpenCV
中,反向映射使用两个映射来描述——一个用于 x
坐标,一个用于 y
坐标,它们都由浮点 cv::Mat
实例表示:
cv::Mat srcX(image.rows,image.cols,CV_32F); // x-map
cv::Mat srcY(image.rows,image.cols,CV_32F); // y-map
这些矩阵的大小将定义目标图像的大小,然后可以使用以下代码在源图像中读取目标图像的 (i, j)
像素值:
(srcX.at<float>(i,j), srcY.at<float>(i,j))
(1) 用两个参数创建 wave
函数获得波浪效果,一个输入图像和一个输出图像结果:
// 通过创建波浪效果重新映射图像
void wave(const cv::Mat &image, cv::Mat &result)
(2) 创建两个映射变量,一个用于 x
位置,另一个用于 y
位置,用于存储重新映射的新位置:
cv::Mat srcX(image.rows,image.cols,CV_32F);
cv::Mat srcY(image.rows,image.cols,CV_32F);
(3) 遍历每个像素:
// 创建映射
for (int i=0; i<image.rows; i++) {
for (int j=0; j<image.cols; j++){
(4) 将实际位置 j
分配给映射 x
位置:
srcX.at<float>(i,j) = j;
(5) 使用正弦函数计算 y
的新位置,使用 x
值作为 sin
的输入,然后关闭计算映射变量的循环:
srcY.at<float>(i,j) = i+5*sin(j/10.0);
}
}
(6) 将重映射函数应用于输入图像:
cv::remap(image, result, srcX, srcY, cv::INTER_LINEAR);
得到的结果如下图所示:
除了波浪效果外,我们也可以创建其他重映射效果。例如,图像翻转效果可以通过以下映射创建:
// 创建映射
for (int i=0; i<image.rows; i++) {
for (int j=0; j<image.cols; j++) {
// 水平翻转
srcX.at<float>(i,j)= image.cols-j-1;
srcY.at<float>(i,j)= i;
}
}
要生成结果图像,只需调用 OpenCV remap
函数:
// 应用映射
cv::remap(image, // 源图像
result, // 目标图像
scrX, // x映射
srcY, // y映射
cv::INTER_LINEAR, // 插值方法
)
这两个映射包含浮点值。因此,目标图像中的像素可以映射回非整数值(即像素之间的位置),这样我们就可以选择映射函数。例如,在我们的重映射函数中,我们使用正弦函数来定义我们的变换,这也意味着我们必须在真实像素之间插入新像素。有多种不同方法执行像素插值,remap
函数的最后一个参数允许我们选择像素插值方法。
3.2 完整代码
完整代码如下所示:
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <math.h>
// 重映射
void wave(const cv::Mat &image, cv::Mat &result) {
// 映射函数
cv::Mat srcX(image.rows, image.cols, CV_32F);
cv::Mat srcY(image.rows, image.cols, CV_32F);
// 创建映射
for (int i=0; i<image.rows; i++) {
for (int j=0; j<image.cols; j++) {
srcX.at<float>(i,j) = j;
srcY.at<float>(i,j) = i+3*sin(j/6.0);
//水平翻转
//srcX.at<float>(i,j) = image.cols-j-1;
//srcY.at<float>(i,j) = i;
}
}
// 应用映射
cv::remap(image, // 源图像
result, // 目标图像
srcX,
srcY,
cv::INTER_LINEAR);
}
int main() {
cv::Mat image = cv::imread("1.png");
cv::namedWindow("Image");
cv::imshow("Image", image);
// 重映射
cv::Mat result;
wave(image, result);
cv::namedWindow("Remapped image");
cv::imshow("Remapped image", result);
cv::waitKey();
return 0;
}
小结
图像处理技术是计算机视觉项目的核心,通常是计算机视觉项目中的关键工具,可以使用它们来完成各种计算机视觉任务。因此,如果要构建计算机视觉项目,就需要对图像处理有足够的了解。图像运算也是图像处理技术的一种,在本文中,我们介绍了如何对图像执行的常见算术运算,例如按位运算、加减法、形态变换等。
系列链接
OpenCV实战(1)——OpenCV与图像处理基础
OpenCV实战(2)——OpenCV核心数据结构
OpenCV实战(3)——图像感兴趣区域
OpenCV实战(4)——像素操作