美文网首页OpenCV 学习笔记
OpenCV 笔记(4):图像的算术运算、逻辑运算

OpenCV 笔记(4):图像的算术运算、逻辑运算

作者: fengzhizi715 | 来源:发表于2023-10-29 13:37 被阅读0次

    1. 图像的算术运算

    图像的本质是一个矩阵,所以可以对它进行一些常见的算术运算,例如加、减、乘、除、平方根、对数、绝对值等等。除此之外,还可以对图像进行逻辑运算和几何变换。

    我们先从简单的图像加、减、逻辑运算开始介绍。后续会有专门的内容介绍图像的几何变换等。

    1.1 图像加法

    图像的加法是将两个大小、类型相同的图像按照逐个像素进行相加,最后得到一个新的图像。

    图像的加、减、乘、除运算,都是两个大小、类型相同的图像进行运算。

    1.1.1 加法的例子

    图像相加的公式:dst = src1 + src2

    也可以使用:dst += src1,其中 += 是 C++ 可重载的运算符。

    举个简单的例子:

    Mat a = imread(".../cat.jpg");// 加载了一张猫的图片
    imshow("a", a);
    
    Mat b = Mat(Size(a.cols,a.rows),a.type(), Scalar(0,0,255));// 生成跟a大小类型一样,红色的图像
    
    Mat c;
    cv::add(a,b,c);// 将 a、b 相加,结果为c
    imshow("c", c);
    
    add.png

    上述代码中 Mat 对象 c 是 Mat 对象 a、b 相加得到的产物。如果将 b 改成白色也就是 Scalar(255,255,255)。那么 c 会变成什么呢?答案依然是白色。因为加法是像素相加,如果两个像素点超出255,那么依旧会变成255。

    1.1.2 实现 add() 函数的功能

    为了解释上面的问题,我们尝试自己实现一个 add 函数的功能。

    Mat a = imread(".../cat.jpg"); // 加载 cat 的图像
    imshow("a", a);
    
    Mat b = Mat(Size(a.cols,a.rows),a.type(), Scalar(0,0,255));
    
    int h = a.rows; // 图像 a 的高
    int w = a.cols; // 图像 a 的宽
    
    Mat c = Mat::zeros(a.size(), a.type());
    for (int row = 0; row < h; row++)
    {
        for (int col = 0; col < w; col++)
        {
            Vec3b p1 = a.at<Vec3b>(row, col);
            Vec3b p2 = b.at<Vec3b>(row, col);
            c.at<Vec3b>(row, col)[0] = saturate_cast<uchar>(p1[0] + p2[0]);
            c.at<Vec3b>(row, col)[1] = saturate_cast<uchar>(p1[1] + p2[1]);
            c.at<Vec3b>(row, col)[2] = saturate_cast<uchar>(p1[2] + p2[2]);
        }
    }
    
    imshow("c", c);
    

    通过2层for循环遍历 a、b 图像的每个像素点,并将结果相加赋值给 c 图像对应的像素点。在相加的时候,使用了 saturate_cast() 函数。

    saturate_cast() 是一个模版函数,它的作用是防止溢出。它支持 uchar、short、int、float、double 等各种类型。

    对于 uchar 类型,如果像素值超过255,使用 saturate_cast() 函数后它的值变为255。这也正好解释了,如果 b 是白色,那么最终得到的 c 对象也会是白色。

    1.1.3 使用 copyTo() 函数实现的图像叠加

    前面的文章我们曾介绍过 copyTo() 函数,它可以将 Mat 对象拷贝到另一个 Mat 对象上。

    现在再来回顾一下它的使用

    Mat a = imread(".../cat.jpg"); // 加载 cat 的图像
    
    Mat b = imread(".../leaf.png"); // 加载一张小尺寸的树叶的图像
    
    Mat roi = a(Rect(0,0,b.cols,b.rows));
    
    b.copyTo(roi);
    
    imshow("result", a);
    

    在上述代码中, roi 对象是从 a 对象中截取一块区域,并且该区域跟 b 对象大小一样。由于提取 roi 的操作是浅拷贝,将 b 对象复制到 roi 对象之后,就会改变 a 对象本身。

    下面是执行的结果:

    copyTo.png

    因此,可以借助 copyTo() 函数来实现图像的叠加。

    1.2 图像的线性混合(linear blending)

    图像的线性混合公式:dst = src1*alpha + src2*beta + gamma

    其中,alpha、beta 分别表示图像1和图像2的权重,gamma 是亮度调节量。当 alpha = beta = 1 且 gamma = 0 时,表示两个图像的相加。

    进行线性混合的两个图像,也必须大小和类型一致。

    Mat a = imread(".../cat.jpg"); // 加载 cat 的图像
    
    Mat b = imread(".../chinese_flag.png"); // 加载五星红旗的图像
    
    resize(a, a,Size(b.cols,b.rows));// 缩放a的大小,跟b保持一致
    
    Mat dst;
    addWeighted(a, 0.5, b, 0.5,0, dst);
    
    imshow("dst", dst);
    

    由于图像 a、b 大小不一样,因此在线性混合之前需要用 resize() 函数将图像 a 的大小按照图像 b 的大小进行缩放。

    linear_lending.png

    上面的代码,将猫和五星红旗完成了线性混合。如果还想尝试做一个国庆版本的渐变头像,则需要离红旗越近,红旗的权重越大。

    我们可以这样写代码:

    Mat a = imread(".../cat.jpg"); // 加载 cat 的图像
    
    Mat flag = imread(".../chinese_flag.png");
    int flag_width = flag.cols;
    int flag_height = flag.rows;
    
    Mat dst;
    
    resize(a, dst, Size(flag_width, flag_height));
    
    int radius = 0;
    if (flag_width > flag_height) {
        radius = flag_width;
    } else {
        radius = flag_height;
    }
    
    for (int i=0; i < dst.rows; i++) {
        for (int j=0; j < dst.cols; j++) {
    
            int distance = std::sqrt(i*i+j*j);
    
            double alpha;
            if (distance > radius) {
                alpha =  1;
            }  else {
                alpha = (double) distance / radius;
            }
    
            double beta = 1 - alpha;
    
            Vec3b v1 = dst.at<Vec3b>(i, j);
            dst.at<Vec3b>(i, j)[0]= alpha * v1[0] + beta * flag.at<Vec3b>(i, j)[0];
            dst.at<Vec3b>(i, j)[1]= alpha * v1[1] + beta * flag.at<Vec3b>(i, j)[1];
            dst.at<Vec3b>(i, j)[2]= alpha * v1[2] + beta * flag.at<Vec3b>(i, j)[2];
        }
    }
    
    imshow("dst", dst);
    
    avatar.png

    1.3 图像减法

    图像相减是两个图像按照逐个像素进行相减,图像相减可以检测出两个图像的差异。利用这个差异可以做各种检测,因此图像减法在很多领域都有实际的用途。

    图像相减的公式:dst = src1 - src2

    也可以使用:dst -= src1,其中 -= 是 C++ 可重载的运算符。

    举个简单的例子:

    Mat a = imread(".../cat.jpg"); // 加载 cat 的图像
    
    int width = a.cols;
    int height = a.rows;
    
    Mat b = Mat(Size(width,height), a.type(),Scalar(0,0,0));
    circle(b, Point(width/2, height/2), 600, Scalar(255,255,255), -1);
    
    Mat dst;
    subtract(a,b,dst);
    
    imshow("dst", dst);
    
    subtract.png

    上述执行的结果是图像 a 减去图像 b 之后得到的结果,将中间的猫“抠掉”了。如果只想要中间的猫,而不要背景该怎么做呢?本文后续会用 bitwise_and 运算来获取。

    再举个例子,对加载图像进行高斯模糊,然后用原图减去高斯模糊后的图,会得到两张图像的差异。

    Mat a = imread(".../cat.jpg"); // 加载 cat 的图像
    imshow("a",a);
    
    Mat b;
    GaussianBlur(a, b,Size(15,15),0,0);
    imshow("b",b);
    
    Mat dst;
    subtract(a,b,dst);
    imshow("dst",dst);
    
    diff.png

    图像的减法介绍完之后,图像的乘法(multiply)、除法(divide)、差的绝对值(absdiff)的用法都很类似,在实际工作中也经常会用到。特别是 absdiff() 函数,用公式表示:dst = |src1 − src2|
    可以用它获取差分图,经常应用在视频分析中。

    2. 图像的逻辑运算

    2.1 掩模的基础知识

    在介绍图像的逻辑运算之前,再来回顾一下掩模(mask)的知识,因为 OpenCV 很多的函数中都会用到 mask 这个参数。

    图像的算术运算、逻辑运算都支持 mask。

    掩模是小于或等于源图像的单通道矩阵,掩模中的值分为 0 和非 0。

    图像掩模是用选定的图像、图形或物体,对处理的图像(全部或局部)进行遮挡,来控制图像处理的区域或处理过程。

    掩模的作用:

    • 提取 ROI
    • 屏蔽作用
    • 提取结果特征
    • 制作特殊形状的图像

    掩模的生成方式有很多种。

    我们可以自己创建一个,将图像减法的第一个例子图像 b 稍微改一下即可。因为 mask 是单通道的矩阵。

    Mat mask = Mat(Size(width,height), CV_8UC1,Scalar(0,0,0));
    circle(mask, Point(width/2, height/2), 600, Scalar(255,255,255), -1);
    

    我们也可以通过图像二值化阈值分割来提取 mask,例如:

    Mat src = imread(".../leaf.png"); // 加载一张小尺寸的树叶的图像
    imshow("src",src);
    
    Mat gray;
    cvtColor(src,gray,COLOR_BGR2GRAY);
    
    Mat mask;
    threshold(gray, mask, 0, 255, THRESH_BINARY_INV|THRESH_OTSU);
    
    imshow("mask",mask);
    
    mask.png

    图像二值化的相关内容后续文章会专门介绍。总之,mask 的制作有很多方式。

    2.2 逻辑运算

    两个图像可以进行与、或、异或等逻辑运算。下面是逻辑操作的真值表:

    a b a AND b a OR b a XOR b NOT a
    0 0 0 0 0 1
    0 1 0 1 1 1
    1 0 0 1 1 0
    1 1 1 1 0 0

    其中,

    • 与运算的原理:如果 a、b 两个值有0,则与的结果为0;如果 a、b 全为1,则与的结果为1。
    • 或运算的原理:如果 a、b 两个值有1,则或的结果为1;如果 a、b 全为0,则与或的结果为0。
    • 异或运算的原理:如果 a、b 两个值不相同,则异或结果为1;如果 a、b 两个值相同,则异或结果为0。
    • 非运算的原理:如果 a 的值为1,则非运算的结果为0;如果 a 的值为0,则非运算的结果为1。

    图像的逻辑运算也需要两个大小、类型相同的图像才能进行运算。

    Mat a = imread(".../cat.jpg"); // 加载 cat 的图像
    
    Mat b = Mat(Size(a.cols,a.rows),a.type(), Scalar(0,0,255));// 生成跟a大小类型一样,红色的图像
    
    Mat dst1,dst2,dst3,dst4;
    bitwise_and(a,b,dst1);
    bitwise_or(a,b,dst2);
    bitwise_xor(a,b,dst3);
    bitwise_not(a,dst4);
    
    imshow("bitwise_and", dst1);
    imshow("bitwise_or", dst2);
    imshow("bitwise_xor", dst3);
    imshow("bitwise_not", dst4);
    
    bitwise_op.png

    OpenCV 中的逻辑与、或、异或、非运算对应的函数分别是 bitwise_and、bitwise_or、bitwise_xor、bitwise_not。上图也分别展示了这些函数的执行结果。

    现在我们来回答一下前面的问题,如何只“抠掉”中间的猫?答案是只要使用 bitwise_and 函数即可。

    Mat a = imread(".../cat.jpg"); // 加载 cat 的图像
    
    int width = a.cols;
    int height = a.rows;
    
    Mat b = Mat(Size(width,height), a.type(),Scalar(0,0,0));
    circle(b, Point(width/2, height/2), 600, Scalar(255,255,255), -1);
    
    Mat dst;
    bitwise_and(a,b,dst);
    imshow("dst", dst);
    
    bitwise_and.png

    2.3 利用 mask 进行图像融合

    对刚才的代码稍微改动一下,把图像 b 的类型改成 CV_8UC1 之后,并改名成 mask。bitwise_and 函数的使用也稍作调整。当 mask 参与 bitwise_and 运算的时候,执行的结果跟刚才是一致的。

    Mat a = imread(".../cat.jpg"); // 加载 cat 的图像
    
    int width = a.cols;
    int height = a.rows;
    
    Mat mask = Mat(Size(width,height), CV_8UC1,Scalar(0,0,0));
    circle(mask, Point(width/2, height/2), 600, Scalar(255,255,255), -1);
    
    Mat dst;
    bitwise_and(a,a, dst,mask);
    imshow("dst", dst);
    

    因为,当 bitwise_and 函数使用 mask 参数时,该运算只会在掩模值非空的像素点执行。所以可以用来去除背景提取 ROI。

    利用 mask 进行“逻辑与”运算,即掩膜图像白色区域是对需要处理图像像素的保留,黑色区域则是对需要处理图像像素的剔除,其余逻辑操作原理类似只是效果不同而已。

    之前使用 copyTo() 函数实现的图像叠加生成的图片,效果并不理想,因为树叶不是透明的。

    下面,尝试一下将两张图像完美的融合。

    Mat a = imread(".../cat.jpg"); // 加载 cat 的图像
    
    Mat b = imread(".../leaf.png"); // 加载一张小尺寸的树叶的图像
    
    Mat b2gray;
    cvtColor(b,b2gray,COLOR_BGR2GRAY); // 对 b 转换成灰度图像
    imshow("b2gray", b2gray);
    
    Mat mask,mask_inv;
    threshold(b2gray, mask, 0, 255, THRESH_BINARY_INV|THRESH_OTSU);// 二值分割获取 mask
    imshow("mask", mask);
    
    bitwise_not(mask,mask_inv);
    imshow("mask_inv", mask_inv);
    
    Mat roi = a(Rect(0,0,b.cols,b.rows));
    Mat fg,bg;
    bitwise_and(roi,roi,bg, mask_inv);
    imshow("bg", bg); // 提取 roi 的背景
    bitwise_and(b,b,fg,mask);
    imshow("fg", fg); // 提取 b 的前景
    
    Mat dst;
    add(bg,fg,dst);
    dst.copyTo(roi);
    
    imshow("result", a);
    

    首先加载两张图像,分别为 a、b 对象。

    将 b 对象转换成灰度图像,然后通过二值分割获取 mask,以及对 mask 进行非运算获得 mask_inv。

    对 a 对象进行截取 roi 的操作,roi 的大小跟 b 对象一致。

    然后分别用 与运算 提取 roi 的背景和 b 对象的前景。将两者相加,并将结果拷贝到 roi 对象上。最后,我们可以看到两张图像完美融合的结果。

    下面的几张图分别展示了代码中各个阶段生成的对象,以及最后的结果。

    step1.png step2.png result.png

    3. 总结

    本文分成两个部分。第一部分介绍了图像的算术运算,主要是介绍了图像加法、减法以及它们的实现原理和使用场景,还介绍了图像的线性混合。

    第二部分介绍了图像的逻辑运算,回顾了 mask 的用途,以及如何在 bitwise_and 函数中使用 mask。

    相关文章

      网友评论

        本文标题:OpenCV 笔记(4):图像的算术运算、逻辑运算

        本文链接:https://www.haomeiwen.com/subject/hjlvidtx.html