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

Part11.  图像的算术运算

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

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

11.1 图像加法

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

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

1.1.1 加法的例子

图像相加的公式:

也可以使用: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);
e652cd2c39032b9a33d8980d6cbfc7f3.jpeg
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 对象本身。

下面是执行的结果:

2c569c933cc58502bd1382c73856cddc.jpeg
copyTo.png

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

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

图像的线性混合公式:$$dst = src1alpha + src2beta + 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 的大小进行缩放。

02db30447a3998ea6c3028b4dd52adcd.jpeg
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);
b5c58c05ccebdee7f367b9164b981ae0.jpeg
avatar.png

31.3 图像减法

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

图像相减的公式:

也可以使用: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);
ab81bd140cefb86fb98759a9a6c67dae.jpeg
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);
15eab01ffd75bb6cb5a6b10e2c8c89db.jpeg
diff.png

图像的减法介绍完之后,图像的乘法(multiply)、除法(divide)、差的绝对值(absdiff)的用法都很类似,在实际工作中也经常会用到。特别是 absdiff() 函数,用公式表示:

可以用它获取 差分图,经常应用在视频分析中。

Part22. 图像的逻辑运算

42.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);
f8b06902fdb95bc438ddf363532a8033.jpeg
mask.png

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

52.2 逻辑运算

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

aba AND ba OR ba XOR bNOT a
000001
010111
100110
111100

其中,

  • 与运算的原理:如果 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);
92cff44b254575d5858af9a7b2dc1069.jpeg
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);
cfbcdfe246fa305478844a28507a2989.jpeg
bitwise_and.png

62.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 对象上。最后,我们可以看到两张图像完美融合的结果。

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

e71b5480bcd394a135c84945cd0ae50f.jpeg
step1.png
4fa4e3ed656152439725791737731ace.jpeg
step2.png
3ee7cc086c684619ad80dbefbb72d40f.jpeg
result.png

Part33. 总结

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

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

企业 Tomcat 运维 部署tomcat反向代理集群

一、Tomcat 简介 Tomcat服务器是一个免费的开放源代码的Web应用服务器&#xff0c;属于轻量级应用服务器&#xff0c; Tomcat和Nginx、Apache(httpd)、Web服务器一样&#xff0c;具有处理HTML页面的功能不过Tomcat处理静态HTML的能力不如Nginx/Apache服务器 一个tomcat默认并…

ip划分与私公网ip、ip的传递

报文问路&#xff1a;1、不知道跳转默认路由器&#xff0c;2、知道路径&#xff0c;向对应路径发出报文&#xff0c;3、路口路由器&#xff0c;下一步就是目标主机在哪。 想要通信必须同在一个局域网&#xff0c;其实将公网就可以看作一个大型的局域网。 在同一个局域网内发送…

61. 旋转链表、Leetcode的Python实现

博客主页&#xff1a;&#x1f3c6;李歘歘的博客 &#x1f3c6; &#x1f33a;每天不定期分享一些包括但不限于计算机基础、算法、后端开发相关的知识点&#xff0c;以及职场小菜鸡的生活。&#x1f33a; &#x1f497;点关注不迷路&#xff0c;总有一些&#x1f4d6;知识点&am…

恶意软件防范和拦截: 提供防范恶意软件攻击的策略

恶意软件&#xff0c;或者俗称的“病毒”&#xff0c;一直是IT领域的一个严重威胁。这些恶意软件可以窃取敏感信息、损害系统稳定性&#xff0c;甚至对企业和个人造成重大经济损失。在这篇博客文章中&#xff0c;我们将讨论如何防范和拦截恶意软件攻击&#xff0c;包括使用反病…

【unity/vufornia】Duplicate virtual buttons with name.../同一个ImageTarget上多个按钮失灵

问题&#xff1a;在同一个ImageTarget上添加多个按钮时无法触发对应按钮的事件。 解决过程&#xff1a; 1.查看报错&#xff1a;“Duplicate virtual buttons with name...”这一行&#xff0c;顾名思义&#xff0c;命名重复。 2.英文搜索到以下文章&#xff0c;应该在inspe…

算法通过村第十八关-回溯|青铜笔记|什么叫回溯(中篇)

文章目录 前言回溯的核心问题撤销操作解释总结 前言 提示&#xff1a;阳光好的时候&#xff0c;会感觉还可以活很久&#xff0c;甚至可以活出喜悦。 --余秀华 回溯是非常重要的算法思想之一&#xff0c;主要解决一些暴力枚举也搞不定的问题&#xff08;这里埋个坑&#x1f4a3;…

Kubernetes 概述以及Kubernetes 集群架构与组件

目录 Kubernetes概述 K8S 是什么 为什么要用 K8S K8S 的特性 Kubernetes 集群架构与组件 核心组件 Master 组件 Node 组件 ​编辑 Kubernetes 核心概念 常见的K8S按照部署方式 Kubernetes概述 K8S 是什么 K8S 的全称为 Kubernetes,Kubernetes 是一个可移植、可扩…

多种方法解决leetcode经典题目-LCR 155. 将二叉搜索树转化为排序的双向链表, 同时弄透引用变更带来的bug

1 描述 2 解法一: 使用list列表粗出中序遍历的结果&#xff0c;然后再依次处理list中的元素并且双向链接 public Node treeToDoublyList2(Node root) {if(rootnull)return root;Node dummynew Node(-10000);List<Node>ansnew ArrayList<>();dfs2(root,ans);Node p…

Maven本地配置获取nexus私服的依赖

场景 Nexus-在项目中使用Maven私服&#xff0c;Deploy到私服、上传第三方jar包、在项目中使用私服jar包&#xff1a; Nexus-在项目中使用Maven私服&#xff0c;Deploy到私服、上传第三方jar包、在项目中使用私服jar包_nexus maven-releases 允许deploy-CSDN博客 在上面讲的是…

stm32中断

目录 简介 什么是NVIC 中断优先级 EXTI 简介 总结 hal库初始化代码 标准库初始化代码 简介 什么是中断&#xff1f;正常情况下&#xff0c;微处理器根据代码内容&#xff0c;按顺序执行指令。执行过程中&#xff0c;如果遇到其它紧急的事件需要处理&#xff0c;则先暂停当…

在ffmpeg中,如何把h264转换为rgb格式

在ffmpeg中&#xff0c;网络视频流h264为什么默认的转为YUV而不是其他格式 文章中介绍了&#xff0c;h264解码的时候是直接解码为yuv的&#xff0c;如果在使用的过程中 需要用到rgb的格式&#xff0c;我们该如何来转换这种格式呢&#xff1f; 在上面的文章中&#xff0c;我们已…

[架构之路-252/创业之路-83]:目标系统 - 纵向分层 - 企业信息化的呈现形态:常见企业信息化软件系统 - 企业应用信息系统集成

目录 第一章 什么是企业应用信息系统集成What 1.1 简介 1.2 架构 二、为什么需要企业应用信息系统集成Why 三、如何实现企业应用信息系统集成 3.1 步骤 3.2 企业应用集成的层次 3.3 业务流程重组 第一章 什么是企业应用信息系统集成What 1.1 简介 企业应用信息系统集…

【项目源码解析】某3C产品自动光学检测系统

解决方案源码解析思维导图 一、带有桁架机械手的自动光学检测系统介绍 二、关于机械手运动控制&#xff08;是否需要机器人学方面的知识&#xff09; 机械手的运动控制不需要深入了解机器人学方面的知识的情况包括&#xff1a; 预配置和任务单一性&#xff1a;如果机械手已经预…

深入理解元素的高度、行高、行盒和vertical-align

1.块级元素的高度 当没有设置高度时&#xff0c;高度由内容撑开&#xff0c;实际上是由行高撑开&#xff0c;当有多行时&#xff0c;高度为每行的行高高度之和。 行高为什么存在&#xff1f; 因为每行都由一个行盒包裹&#xff0c;行高实际上是行盒的高度。 2.什么是行盒&am…

一天掌握python爬虫【基础篇】 涵盖 requests、beautifulsoup、selenium

大家好&#xff0c;我是python222小锋老师。前段时间卷了一套 Python3零基础7天入门实战 以及1小时掌握Python操作Mysql数据库之pymysql模块技术 近日锋哥又卷了一波课程&#xff0c;python爬虫【基础篇】 涵盖 requests、beautifulsoup、selenium&#xff0c;文字版视频版。1…

使用Objective-C和ASIHTTPRequest库进行Douban电影分析

概述 Douban是一个提供图书、音乐、电影等文化内容的社交网站&#xff0c;它的电影频道包含了大量的电影信息和用户评价。本文将介绍如何使用Objective-C语言和ASIHTTPRequest库进行Douban电影分析&#xff0c;包括如何获取电影数据、如何解析JSON格式的数据、如何使用代理IP技…

网站如何改成HTTPS访问

在今天的互联网环境中&#xff0c;将网站更改成HTTPS访问已经成为了一种标准做法。HTTPS不仅有助于提高网站的安全性&#xff0c;还可以提高搜索引擎排名&#xff0c;并增强用户信任。因此&#xff0c;转换为HTTPS是一个重要的举措&#xff0c;无论您拥有个人博客、电子商务网站…

HarmonyOS(二)—— 初识ArkTS开发语言(上)之TypeScript入门

前言 Mozilla创造了JS&#xff0c;Microsoft创建了TS&#xff0c;而Huawei进一步推出了ArkTS。因此在学习使用ArkTS前&#xff0c;需要掌握基本的TS开发技能。 ArkTS介绍 ArkTS是HarmonyOS优选的主力应用开发语言。它在TypeScript&#xff08;简称TS&#xff09;的基础上&am…

神经网络的解释方法之CAM、Grad-CAM、Grad-CAM++、LayerCAM

原理优点缺点GAP将多维特征映射降维为一个固定长度的特征向量①减少了模型的参数量&#xff1b;②保留更多的空间位置信息&#xff1b;③可并行计算&#xff0c;计算效率高&#xff1b;④具有一定程度的不变性①可能导致信息的损失&#xff1b;②忽略不同尺度的空间信息CAM利用…

【mfc/VS2022】计图实验:绘图工具设计知识笔记3

实现类对串行化的支持 如果要用CArchive类保存对象的话&#xff0c;那么这个对象的类必须支持串行化。一个可串行化的类通常有一个Serialize成员函数。要想使一个类可串行化&#xff0c;要经历以下5个步骤&#xff1a; 1、从CObject派生类 2、重写Serialize成员函数 3、使用DE…