OpenCV笔记(C++版)

00 环境配置与搭建 + 显示1张图片_寒山拾不得的博客-CSDN博客

Excerpt

00 环境配置与搭建 + 显示1张图片

00 环境配置与搭建 + 显示1张图片

本课解决的问题:

  • 如何安装Visual Studio 2022 pro?
  • 如何安装OpenCV 4.6.0?
  • 如何配置与搭建opencv开发环境?
  • 如何显示1张图片?

1.Visual Stdio 2022 Pro

选择 C++桌面开发 + 通用Windows开发 + .NET 桌面开发 这3个就可以了,激活码百度即可。

在这里插入图片描述

在这里插入图片描述

2.OpenCV 4.6.0

OpenCV官网:opencv.org 下载最新版本的OpenCV

在这里插入图片描述

设置自己的opencv包路径,然后解压。

在这里插入图片描述

我们可以看到有buildsources两个文件夹
解释如下:

  • build - 顾名思义,build后Windows可以接受的东西
  • sources - 包含OpenCV 4.6.0的所有源码

在这里插入图片描述

沿着build → x64 我们又看到vc14vc15两个文件夹,它们的目录下,都有binlib
解释如下:

  • vc14 - 指vc14运行库,对应VC++2015运行库,是Windows系统下运行Visual Studio 2015开发的C++应用程序所必需的,并与 Visual C+ 库动态链接。
  • vc15 - 指vc15运行库,对应VC++2017运行库…………
    (2017及以后的版本,我们都选择vc15就可以了)

在这里插入图片描述

3.环境配置与搭建

准备

5LiN5b6X,size_20,color_FFFFFF,t_70,g_se,x_16)

项目名称随便,位置自己去安排好一个文件夹

在这里插入图片描述

改为Release x64

在这里插入图片描述

关于Release和Debug的说明

引用文章:Debug和Release的区别是什么?

  • Debug 版本
    Debug 是“调试”的意思,Debug 版本就是为调试而生的,编译器在生成 Debug 版本的程序时会加入调试辅助信息,并且很少会进行优化,程序还是“原汁原味”的。
    不是任何一个程序都可以调试的,程序中必须包含额外的辅助信息才能调试,否则调试器也无从下手。
  • Release 版本
    Release 是“发行”的意思,Release 版本就是最终交给用户的程序,编译器会使尽浑身解数对它进行优化,以提高执行效率,虽然最终的运行结果仍然是我们期望的,但底层的执行流程可能已经改变了。
    编译器还会尽量降低 Release 版本的体积,把没用的数据一律剔除,包括调试信息。
    最终,Release 版本是一个小巧精悍、非常纯粹、为用户而生的程序。

总结

  • Debug 版本的存在是为了方便程序员开发和调试,性能和体积不是它的重点;
  • Release版本是最终交给用户的程序,性能和体积是需要重点优化的两个方面。

所以

  • 在开发过程中,我们一般使用 Debug 版本,只有等到开发完成,确认没有任何Bug 之后,希望交给用户时再生成 Release 版本。
    基本所有的集成开发环境(IDE)都可以在 Debug 版本和 Release 版本之间进行切换

然后去,视图→属性管理器

在这里插入图片描述

因为我们选择了Release版本,所以我们配置Release x64这个

2022版本可能没有Microsoft.Cpp.x64.user,需要下载MSBuild并复制到某个文件夹
VS2019没有Microsoft.Cpp.x64.user配置文件解决方法

右键→属性,我们开始配置
在这里插入图片描述

配置包含目录

在VC++目录里配置,先把VC++目录中的所有目录全清空
在这里插入图片描述

首先是包含目录,根据自己opencv的安装位置配置
在这里插入图片描述

配置库目录

然后是库目录,根据自己opencv的安装位置配置
在这里插入图片描述

配置链接器

再然后是附加依赖项

在这里插入图片描述

先找一下要配置的信息

在这里插入图片描述

因为是Release版本,选择所以这里配置成这样

这里补充一下,无论是配置Debug模式还是Release模式,最好只保留一个,不然会遇到很多问题

  • Debug模式下,只使用带d的lib文件
  • Release模式下,只使用不带d的lib文件

在这里插入图片描述

配置环境变量

系统变量→path,添加自己对应的bin目录。
这样我们就配置完了,重新启动VS Pro 2022 配置生效,或者重新启动电脑配置生效。

在这里插入图片描述

4.显示1张图片

#include<opencv2\opencv.hpp>
#include<iostream>

using namespace cv;
using namespace std;

int main(int argc, char** argv) {

//路径要根据自己的图片而定
Mat src = imread("D:/Workspace/VS-Project/OpenCV/images/lena.png");

imshow("input", src);

waitKey(0);
destroyAllWindows();

return 0;
}

在这里插入图片描述

01 图像读取与显示

Excerpt

01 图像读取与显示


01 图像读取与显示

opencv知识点:

  • 图像存储 - Mat
  • 读取图像 - imread()
  • 显示图像 - imshow()
  • 等待键 - waitKey()
  • 销毁所有窗口 - destroyAllWindows()

本课解决的问题:

  • 如何加载为灰度图像?
  • 如何加载其他情况下的图片?
  • 如何使窗口可调大小?
  • 如何判断是否读取图像成功?
  • 为什么检查图像深度会为1?

1.代码解释

代码解释

#include<opencv2\opencv.hpp>
#include<iostream>

using namespace cv;
using namespace std;

int main(int argc, char** argv) {

Mat src = imread("D:/Workspace/VS-Project/OpenCV/images/lena.png");
/*
Mat
读进来的图像以矩阵的方式存储
imread()
读取图像
共2个参数
第1个参数 图片路径
第2个参数 色彩标志(默认为1,即彩色图像)
*/
imshow("input", src);
/*
imshow
显示图像
共2个参数
第1个参数 窗口名称
第2个参数 输出对象
*/
waitKey(0);
/*
waitKey
等待键
共1个参数
第1个参数 等待时间(ms)规定0为永远
*/
destroyAllWindows();
/*
destroyAllwindows
销毁所有窗口
无参数
*/
return 0;
}

2.imread()用法

彩色图像

imread方式加载进来的图像,都会变成“彩色图像”
图像路径后面还有一个带缺省值的参数IMREAD_COLOR = 1,即加载时总是转化图片为3通道BGR图像
在这里插入图片描述

灰度图像

如果我们想显示一张灰度图片,那该怎么办?
我们选择IMREAD_GRAYSCALE,即可加载成灰度图像

在这里插入图片描述

在这里插入图片描述

其他情况

透明信息

实际上图像颜色有很多种,比如4通道的图像(BGR+透明通道)。
当png图片有透明通道时,透明通道也是要加载的,怎么办呢?

这时候要选择IMREAD_UNCHANGED这种方式,这样通道数加载进来便不会被改变

在这里插入图片描述

色彩格式/数据类型

比如HSV色彩的图像,浮点数据类型的图像,就要选择如下的格式

  • IMREAD_ANYCOLOR - 如hsv色彩的图
  • IMREAD_ANYDEPTH - 如图像是16位,32位等的时候

在这里插入图片描述

3.imshow()用法

当使用imshow时,默认方式是WINDOW_AUTOSIZE,产生窗口大小和图像匹配,但无法更改。

所以会遇到这样的问题:图像大,窗口也大,就会超出物理屏幕,该怎么办呢?
这时候我们创建namedWindow窗口并指定为WINDOW_FREERATIOWINDOW_NORMAL,就可以调整窗口了

在这里插入图片描述
在这里插入图片描述

imshow的显示问题

要注意的是,imshow显示图像时是不支持透明通道的。
即使我们imread时设置了IMREAD_UNCHANGED,imshow也无法显示透明的信息,会显示成变成黑色/白色

4.图像判空操作

通常情况下,再读取图像后都会增加一个判空操作,如下所示

当没有判空操作时,有错就会提示一种断言式空,一般是图片路径的问题

#include<opencv2\opencv.hpp>
#include<iostream>

using namespace cv;
using namespace std;

int main(int argc, char** argv) {

//路径要根据自己的图片而定
Mat src = imread("D:/Workspace/VS-Project/OpenCV/images/lena.png000",IMREAD_GRAYSCALE);

if (src.empty()) {
printf("could not load image……\n");
return -1;
}

namedWindow("输入窗口", WINDOW_FREERATIO);

imshow("输入窗口", src);

waitKey(0);
destroyAllWindows();

return 0;
}

在这里插入图片描述

补充知识

图像深度为“1”的解释

首先介绍一下什么是图像深度(image depth)

比如imread加载进来的图片是3通道的,每个通道1个字节(8bit),3*8=24位,图像深度就是24。

在opencv中,call API src.depth()检查深度时,不会直接告诉你24位。

因为24位在opencv中只是一个深度的表示,opencv用一个枚举类型来表示它,这个枚举类型的值可能是1。

所以有的时候,检查深度时得到1也不要诧异。
还有,获取某些其他属性时也会出现这种情况,因为opencv规定了很多的枚举数值。

本课所用API查阅

OpenCV 4.6.0 官方文档

imread()

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

imshow()

在这里插入图片描述
在这里插入图片描述

namedWindow()

在这里插入图片描述
在这里插入图片描述

waitKey()

在这里插入图片描述
在这里插入图片描述

destroyAllWindows()

在这里插入图片描述
在这里插入图片描述

02 图像色彩空间转换

Excerpt

02 图像色彩空间转换

02 图像色彩空间转换

opencv知识点:

  • 色彩空间转换函数 - cvtColor()
  • 图像保存 - imwrite()
  • 图像显示 - imshow()

本课解决的问题:

  • 如何对图片进行色彩空间转换?
  • 如何保存图像?

1.准备事项

创建头文件quickopencv.h 以及 源文件 quickdemo.cppmain.cpp

本课程只是为了方便演示,所以采用本种格式。

quickopencv.h

#pragma once

#include<opencv2/opencv.hpp>

using namespace cv;

/*
顾名思义:快速的演示。随着课程的推进,里面会有各种demo
*/
class QuickDemo {
public:

/*
这就是第1个demo,色彩空间转换demo
*/
void colorSpace_Demo(Mat& image);
/*
`
`
`
之后写的demo
·
·
·
*/
};

在这里插入图片描述

本种写法的配置

如果不进行相应配置,在写quickdemo.cpp的时候,会出现include的错误提示

我们去,右键→属性→VC++→包含目录,进行配置

在这里插入图片描述

对于笔者来说,要填写的信息

在这里插入图片描述
在这里插入图片描述

2.色彩空间转换+图像保存

接下来我们进行图像的色彩空间转换,并把转换后的图像保存下来。

cvtColor
色彩空间转换
共4个参数
第1个参数 输入
第2个参数 输出
第3个参数 色彩空间转换方式
第4个参数 通道数(不输入,则根据输入自动计算)
imwrite
图像保存
共3个参数
第1个参数 路径+文件名
第2个参数 输入
第3个参数 特定格式编码对(一般用不到)

色彩空间转换有4种常用的方式,本文只演示了两种。

  • 彩色到灰度 - COLOR_BGR2GRAY 对应数值 6
  • 灰度到彩色 - COLOR_GRAY2BGR 对应数值 8
  • BGR到HSV - COLOR_BGR2HSV 对应数值 40
  • HSV到BGR - COLOR_HSV2BGR 对应数值 54

quickdemo.cpp

#include<quickopencv.h>

void QuickDemo::colorSpace_Demo(Mat& image) {

Mat gray, hsv;

cvtColor(image, hsv, COLOR_BGR2HSV);
cvtColor(image, gray, COLOR_BGR2GRAY);

imshow("HSV", hsv);
imshow("灰度", gray);

imwrite("D:/Workspace/VS-Project/OpenCV/images/hsv.png", hsv);
imwrite("D:/Workspace/VS-Project/OpenCV/images/gray.png", gray);


}

main.cpp

#include<opencv2\opencv.hpp>
#include<quickopencv.h>
#include<iostream>

using namespace cv;
using namespace std;

int main(int argc, char** argv) {

Mat src = imread("D:/Workspace/VS-Project/OpenCV/images/hahaha.jpg");

if (src.empty()) {
printf("could not load image……\n");
return -1;
}

imshow("输入窗口", src);
//创建QuickDemo的对象,然后调用方法
QuickDemo qd;
qd.colorSpace_Demo(src);

waitKey(0);
destroyAllWindows();

return 0;
}

运行结果
在这里插入图片描述
在这里插入图片描述

补充知识

imshow的两种方式

  • 默认支持显8位的图像,即[0,255]
  • 支持显示数据类型为浮点数的图像,这时[0,1]会映射到[0,255]

除了这两种,其他的显示多多少少会有问题,当我们用imshow的图像,输入图像最好是8位。

那什么图像是8位的?之后会讲解,不过目前可以知道,用imread读进来的,就是8位的

RGB和HSV

彩色图像的通道是什么呢?

通常彩色图像有BGR三个通道
B,G,R,即按照蓝 绿 红的通道顺序
3个通道都是[0,255],即有256 * 256 * 256种组合
如果加上透明通道A(alpha),透明通道也是[0,255],就有256 * 256 * 256 * 256种组合

HSV通道呢?

H(色调)范围[0,180]
S(饱和度)范围[0,255]
V(明度)范围[0,255]
其中HS表示颜色,V表示亮度

所以,不同的通道有不同的作用

比如:有时候有些东西不好处理,它没有一个专门的亮度通道
那我们调整亮度,我们就到HSV色彩空间处理就会更好一点,处理完之后再返回BGR色彩空间

本课所用API查阅

OpenCV 4.6.0 官方文档

cvtColor()

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

imwrite()

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

imshow()

在这里插入图片描述
在这里插入图片描述

03 图像对象的创建与赋值

Excerpt

03 图像对象的创建与赋值


03 图像对象的创建与赋值

opencv知识点:

  • Mat类
  • 图像复制的3种方法
  • 图像属性的获取
  • Mat对象的创建
  • Mat对象的赋值

本课解决的问题:

  • Mat是什么?
  • Mat对象克隆/拷贝与赋值的区别?
  • 如何获取图像的属性?
  • 如何创建空白图像?
  • 如何对Mat对象赋值?

1.Mat类及其实例

Mat简述

什么是Mat呢,Mat其实就是matrix(矩阵)的缩写
我们看到的图像,就是以数字矩阵的形式存储在计算机中,在opencv中,我们用Mat类的对象存储图像。

在opencv中,Mat类分为两个部分

  • 矩阵头
  • 矩阵数据

矩阵头

图像有很多属性。如:大小,宽和高,数据类型,通道数。这些数据存储在矩阵头

矩阵数据

图像也有很多的数据,图像的数据部分是所有像素的值的一个集合,存储在矩阵数据

在这里插入图片描述

Mat对象复制

看上图,我们发现Mat对象复制是有三种方法的

  • 克隆
  • 拷贝
  • 赋值

这里说一下它们的区别

赋值:相当于浅复制,只复制了矩阵头,指向的是同一个数据块。

克隆/拷贝:相当于深复制,还会复制相应的数据块

//1.赋值——浅复制
Mat src = imread("……");
Mat m3 = src;

//2.克隆——深复制
Mat src = imread("……");
Mat m1 = src.clone();

//3.拷贝——深复制
Mat src = imread("……");
Mat m2;
src.copyTo(m2);


Mat对象属性

Mat对象存储了一些属性,如:列数,行数,通道数(维度),位深度,图像类型

怎么获取它们呢?

Mat image= imread("……");

//很简单,通过这些操作即可
image.cols;
image.rows;
image.channels();
image.depth();
image.type();
数据类型和通道数

图像的数据类型type由两部分组成

  • 类型
  • 通道数

在opencv中type是枚举类型的数值
CV_8UC3:表示 8位无符号整数(字节类型)三通道。枚举数值16
在这里插入图片描述

忽略掉前面的字符,我们只关注Cx,可以很快的发现,Cx即表示通道数channels
如C1——单通道,C2——双通道……

注意

  • 当为单通道时,C1可以省略,直接为CV_8U
  • 单通道为灰度图像,三通道为彩色图像
深度

图像深度depth和数据类型关联密切,其在opencv中也为枚举数值。

在这里插入图片描述
图像深度有真实值和枚举值之分。

  • 枚举值

图像depth的枚举值跟通道数无关,相同类型下如CV_8UC1和CV_8UC3,图像深度的枚举值是一样的

  • 真实值

图像depth的真实值还要考虑通道数,如CV_8UC3 的通道数为 8*3=24位

2.Mat对象的创建

Mat对象创建,常用的是创建空白图像。如下演示了三种方法

Mat src = imread("……");


Mat m4 = Mat::zeros(src.size(),src.type())
/*
矩阵填充0
行列为src行列
数据类型为src的数据类型
*/

Mat m5 = Mat::zeros(Size(512,512),CV_8UC3);
/*
矩阵填充0
行列为512*512
数据类型为CV_8UC3(8UC指8位无符号字符,3指3个通道)
*/

Mat m6 = Mat::ones(Size(512,512),CV_8UC3);
/*
矩阵填充1
其余与上相同
*/

C11创建图像的新方式

Mat kernel = (Mat_<char>(3,3)<<0,-1,0,-1,5,-1,0,-1,0);

3.Mat对象的赋值

只会创建图像是不够的,这里我们再说一下Mat对象赋值的事情

常用的有4种方法

  • Mat::zeros()
  • Mat::ones();
  • =
  • Scalar

完整代码放在前面

//函数定义
void mat_creation_demo(Mat& image);

//函数实现
void QuickDemo::mat_creation_demo(Mat& image) {

//创建空白图像
Mat m = Mat::zeros(Size(8, 8), CV_8UC1);
//Mat m = Mat::zeros(Size(8, 8), CV_8UC3);

//Mat m = Mat::ones(Size(8, 8), CV_8UC1);
//Mat m = Mat::ones(Size(8, 8), CV_8UC3);

//m=127;

//m=Scalar(127,127,127);

std::cout << m << std::endl;//为了说明以上语句的不同,我们把矩阵进行打印
}

现在开始分别说明

Mat::zeros
Mat m = Mat::zeros(Size(8, 8), CV_8UC1);
Mat m = Mat::zeros(Size(8, 8), CV_8UC3);

矩阵宽度 = 图像的列 * 通道数

当为CV_8UC1时,单通道,矩阵宽度8
当为CV_8UC3时,三通道,矩阵宽度24
在这里插入图片描述
在这里插入图片描述

Mat::ones
Mat m = Mat::ones(Size(8, 8), CV_8UC1);
Mat m = Mat::ones(Size(8, 8), CV_8UC3);

当填充为1的时候,要特别小心

单通道没问题,但是三通道这种多通道,只在第一个通道为1
在这里插入图片描述
在这里插入图片描述

重载的 =
m=127;

和Mat::ones相同,单通道没问题,多通道只赋值第一个
在这里插入图片描述

Scalar

前面的三种方式,只用Mat::zeros实现了多通道的赋值,但是只能赋值为0,非常局限。

下面介绍一个非常常用的赋值方法,比如我们赋值三通道,全变127

m=Scalar(127,127,127);

在这里插入图片描述

4.Mat对象的显示

在opencv中Mat矩阵就是图像,那我们来显示一下全127对应的图像

void QuickDemo::mat_creation_demo(Mat& image) {

//创建空白图像
Mat m3 = Mat::ones(Size(400, 400), CV_8UC3);

m3 = Scalar(127, 127, 127);

imshow("创建图像", m3);
}

在这里插入图片描述

本课所用API查阅

OpenCV 4.6.0 官方文档

Mat::zeros()

在这里插入图片描述
在这里插入图片描述

Mat A;
A = Mat::zeros(3, 3, CV_32F);

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

Mat::ones()

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

Scalar()

在这里插入图片描述

04 图像像素的读写操作

Excerpt

04 图像像素的读写操作


04 图像像素的读写操作

opencv知识点:

  • 获取/改变图像的某个像素 - Mat::at
  • 图像像素 - 数组遍历
  • 图像像素 - 指针遍历

本课所解决的问题:

  • 如何获取/改变图像的某个像素?
  • 如何利用数组遍历图像像素?
  • 如何利用指针遍历图像像素?

1.获取某个像素

我们获取/改变图像的某个像素,要用到Mat::at< >( )

image.at<uchar>(row, col);
/*
获取指定位置的像素值

这里只是at方法的一种演示,一共6种传参方式,具体可查看文档
说明:
at<>中必须标明矩阵的数据类型,一般图像是uchar(8位无符号整型)类型
这里我们采用二维度坐标的方式传参
*/

2.图像像素的数组遍历

为了演示图像像素遍历,我们在遍历的同时进行一个反色处理

//函数定义
void pixel_visit_demo(Mat& image);

//函数实现
void QuickDemo::pixel_visit_demo(Mat& image) {

int h = image.rows;
int w = image.cols;
int dims = image.channels();
/*
灰度图像——通道为1
彩色图像——通道为3
*/
for(int row = 0; row < h; row++) {
for (int col = 0; col < w; col++) {

if (dims == 1) {//灰度图像
int pv = image.at<uchar>(row, col);

image.at<uchar>(row, col) = 255 - pv;
}

if (dims == 3) {//彩色图像
Vec3b bgr = image.at<Vec3b>(row, col);

image.at<Vec3b>(row, col)[0] = 255 - bgr[0];
image.at<Vec3b>(row, col)[1] = 255 - bgr[1];
image.at<Vec3b>(row, col)[2] = 255 - bgr[2];
}
}
}
imshow("像素读写演示", image);
}

如上,我们可以看到,不同通道数的处理方式是不同的

  • 对于单通道图像的使用方法:
int pv = image.at<uchar>(row, col);

image.at<uchar>(row, col) = 255 - pv;//对访问的像素进行反色
  • 对于RGB三通道图像的使用方法:
Vec3b bgr = image.at<Vec3b>(row, col);

image.at<Vec3b>(row, col)[0] = 255 - bgr[0];//对访问的像素进行反色
image.at<Vec3b>(row, col)[1] = 255 - bgr[1];//对访问的像素进行反色
image.at<Vec3b>(row, col)[2] = 255 - bgr[2];//对访问的像素进行反色

这里我们解释一下Vec3b是什么

Vec3b可以看作是vector<uchar, 3>。
简单而言就是一个uchar类型的,长度为3的vector向量。

根据通道数的不同,数据类型的不同,就有了很多变化,下面是常用的三种

8U 类型的 RGB 彩色图像可以使用 < Vec3b >
3 通道 float 类型的矩阵可以使用 < Vec3f >
3 通道 int 类型的矩阵可以使用 < Vec3i >

在这里插入图片描述

3.图像像素的指针遍历

另一种更快的方式就是指针遍历

通过设置每一行的首地址指针,我们可以实现更快的遍历

for(int row = 0; row < h; row++) {

uchar* curren_row = image.ptr<uchar>(row);
//相当于每次获取行的首地址
for (int col = 0; col < w; col++) {

//灰度图像
if (dims == 1) {
*curren_row++ = 255 - *curren_row;
}
//彩色图像
if (dims == 3) {
*curren_row++ = 255 - *curren_row;
*curren_row++ = 255 - *curren_row;
*curren_row++ = 255 - *curren_row;
}
}
}

取完每行的首地址之后

  • 对于单通道图像的使用方法:
*curren_row++ = 255 - pv;
  • 对于RGB三通道图像的使用方法:
//与数组遍历相比,3通道时执行三次就可以
*curren_row++ = 255 - *curren_row;
*curren_row++ = 255 - *curren_row;
*curren_row++ = 255 - *curren_row;

本课所用API查阅

OpenCV 4.6.0 官方文档

Mat::at

虽然有12种重载,但只有6种传参方式

1. 单维度坐标——i0:沿维度 0 的索引
在这里插入图片描述在这里插入图片描述
在这里插入图片描述
2. 双维度坐标——row 沿维度 0 的索引 ;col 沿维度 1 的索引
在这里插入图片描述

3. 三维度坐标——i0 沿维度 0 的索引;i1 沿维度 1 的索引;i2 沿维度 2 的索引
在这里插入图片描述

4. 维度数组坐标——int数组在这里插入图片描述

5. 维度数组坐标——vector数组在这里插入图片描述

6. 像素点的坐标——point类
List item

05 图像像素的算术操作

Excerpt

05 图像像素的算术操作


05 图像像素的算术操作

opencv知识点:

  • 图像像素算术操作 - 运算符
  • 值的截断 - saturate_cast<>()
  • 图像像素算术操作 - 专用函数

本课所解决的问题:

  • 如何改变图像的亮度?
  • 如何进行图像像素的算术操作?
  • 如何对可能溢出的值进行截断?

1.改变图像亮度

对于改变图像的亮度,我们可以采用图像像素的算术操作实现

本文采用如下两种方法演示

  • 四种运算运算符
  • 四种专用函数

2.四种运算符和截断

运算符

首先,我们采用运算符的方式

//函数定义
void operators_demo(Mat& image);
//函数实现
void QuickDemo::operators_demo(Mat& image) {

Mat dst;

dst = image + Scalar(50, 50, 50);
//dst = image - Scalar(50, 50, 50);
//dst = image * Scalar(5, 5, 5);//会溢出
//dst = image / Scalar(5, 5, 5);//会截断为0
imshow("加法操作", dst);

}

加法:可能会溢出

在这里插入图片描述

减法: 会自动截断为0

在这里插入图片描述 乘法

可能会溢出,提示溢出的错误。
本案例就溢出了,所以没有结果展示

除法:如果除数较大,结果最终会被自动截断为0

在这里插入图片描述

截断函数

采用运算符时加法,乘法有溢出隐患,有什么办法解决吗?
这就用到了saturate_cast<uchar>,当使用它时会保证BGR图像的像素值在[0,255]

对于截断的演示,我们用运算符加法演示

void QuickDemo::operators_demo(Mat& image) {

Mat dst = Mat::zeros(image.size(), image.type());
Mat m = Mat::zeros(image.size(), image.type());
m = Scalar(50, 50, 50);

int h = image.rows;
int w = image.cols;
int dims = image.channels();

for (int row = 0; row < h; row++) {
for (int col = 0; col < w; col++) {
Vec3b p1 = image.at<Vec3b>(row, col);
Vec3b p2 = m.at<Vec3b>(row, col);
dst.at<Vec3b>(row,col)[0] = saturate_cast<uchar>(p1[0] + p2[0]);
dst.at<Vec3b>(row,col)[1] = saturate_cast<uchar>(p1[1] + p2[1]);
dst.at<Vec3b>(row,col)[2] = saturate_cast<uchar>(p1[2] + p2[2]);
}
}
imshow("加法操作", dst);

}

3.专用函数

有没有更简单的方式呢?

OpenCV有自带的专用函数,专用函数都有截断处理,以乘法为例。

void QuickDemo::operators_demo(Mat& image) {

Mat dst = Mat::zeros(image.size(), image.type());
Mat m = Mat::zeros(image.size(), image.type());
m = Scalar(50, 50, 50);

//add(image, m, dst);
//subtract(image, m, dst);
multiply(image, m,dst);
//divide(image, m, dst);

imshow("加法操作", dst);

}

在这里插入图片描述

本课所用API查阅

OpenCV 4.6.0 官方文档

Scalar()

在这里插入图片描述

add()

在这里插入图片描述

subtract()

在这里插入图片描述

multiply()

在这里插入图片描述

divide()

在这里插入图片描述

saturate_cast<>()

有11种重载,对应11种不同的的数据类型
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

uchar a = saturate_cast<uchar>(-100); // a = 0 (UCHAR_MIN)
short b = saturate_cast<short>(33333.33333); // b = 32767 (SHRT_MAX)

在这里插入图片描述

06 滚动条操作1.0

Excerpt

06 滚动条操作1.0 - 调整图像亮度


06 滚动条操作1.0 - 调整图像亮度

opencv知识点:

  • 创建滚动条 - createTrackbar()
  • 处理滚动条事件的函数 - TrackbarCallback()

本课所解决的问题:

  • 如何创建滚动条?
  • 如何通过滚动条进行亮度调整?

1.滚动条的创建

上一课中,我们调整亮度只能一次一次的去调整,现在我们来试一下通过滚动条调整亮度。

OpenCV中,我们要想在图像上通过滚动条调整亮度,要用到两个API

  • createTrackbar
  • TrackbarCallback(用户自写)
createTrackbar
创建滚动条
共6个参数
第1个参数 滚动条名称
第2个参数 窗口名称
第3个参数 初始的位置
第4个参数 最大位置(最小位置始终为0)
第5个参数 处理轨迹栏事件类型的回调函数
(opencv中这里默认是onChange = 0,实际应用中我们要自己进行函数定义及实现)
第6个参数 userdata 传递给回调函数的用户数据(任何我们想传的,比如图像)
TrackbarCallback类型的函数
共2个参数
第1个参数 指定轨迹栏的当前位置
第2个参数 void*类型的可选传入数据

2.通过滚动条调整亮度

这里的代码只是初步的实现,并没有充分利用creatTrackbar的6个参数
同时部分地方也不符合面向对象的编程,所以我们在下一课进行了代码优化

//函数定义
void tracking_bar_demo(Mat& image);

//函数实现
Mat src, dst, m;
int lightness;

static void on_track(int, void*) {

m = Scalar(lightness, lightness, lightness);
add(src, m, dst);
//subtract(src, m, dst);
imshow("亮度调整", dst);

}

void QuickDemo::tracking_bar_demo(Mat& image) {

src = image;
dst = Mat::zeros(image.size(), image.type());
m = Mat::zeros(image.size(), image.type());
namedWindow("亮度调整", WINDOW_AUTOSIZE);

int max_value = 100;
lightness = 50;

createTrackbar("Value Bar:", "亮度调整", &lightness, max_value, on_track);

//on_track(lightness, 0);
    /*
课程中的这一句代码,是多此一举,这是一个回调函数不需要我们手动调用
这个lightness在createTrackbar中就已经传入了,这里没必要再去调用传入
*/

}
提高亮度
add(src, m, dst);

在这里插入图片描述

降低亮度
subtract(src, m, dst);

在这里插入图片描述

本课所用API查阅

OpenCV 4.6.0 官方文档

creatTrackbar()

在这里插入图片描述
在这里插入图片描述

TrackbarCallback()

在这里插入图片描述

07 滚动条操作2.0

Excerpt

07 滚动条操作2.0 - 调整亮度与对比度


07 滚动条操作2.0 - 调整亮度与对比度

opencv知识点:

  • 创建滚动条 - createTrackbar()
  • 处理滚动条事件的函数 - TrackbarCallback()
  • 亮度与对比度的概念
  • 巧用图片融合 - addWeighted()

本课所解决的问题:

  • 如何利用createTrackbar的userdata?
  • 调整亮度/对比度的内涵是什么?
  • 如何利用图片融合addWeighted调整亮度/对比度?

1.参数userdata

对上一次课代码的改进,利用上userdata这个参数

合理运用第6个参数userdata,进行参数传递,从而调整亮度,以降低亮度为例。

static void on_track(int light, void* userdata) {

Mat image = *((Mat*)(userdata));

Mat dst = Mat::zeros(image.size(), image.type());
Mat m = Mat::zeros(image.size(), image.type());

m = Scalar(light, light, light);

//add(image, m, dst);
subtract(image, m, dst);
imshow("亮度调整", dst);

}

void QuickDemo::tracking_bar_demo(Mat& image) {

namedWindow("亮度调整", WINDOW_AUTOSIZE);

int lightness = 50;
int max_light = 100;

createTrackbar("Value Bar:", "亮度调整", &lightness, max_light, on_track,(void*)(&image));

//on_track(lightness, &image);
/*
课程中的这一句代码,是多此一举,这是一个回调函数不需要我们手动调用

这个lightness在createTrackbar中就已经传入了,这里没必要再去调用传入
同时,我们使用了第6个参数,那么图像也已经传入,这里没必要再去调用传入
*/

}

在这里插入图片描述

2.亮度与对比度

亮度

调整亮度,就是整体的像素点值的调整,相当于整体同时加上一个数(可正可负)。

对比度

调整对比度,就是调整像素点之间的差值,相当于乘上一个数,如0.5,1.3。
像素点的值在变化的同时,像素点之间的差值也在变化。

3.调整亮度与对比度

调整亮度,对比度,我们也可以通过addWeighted实现

addWeighted
共7个参数
第1个参数 第一个输入 src1
第2个参数 第一个输入的权重 alpha
第3个参数 第二个输入 src2
第4个参数 第二个输入的权重 beta
第5个参数 每个数要增加的标量 gamma
第6个参数 输出 dst

第7个参数 输出的可选深度(一般用不到)

公式
dst = src1 * alpha + src2 * beta + gamma 
范围截断在[0,255]

完整代码

static void on_light(int lightness, void* userdata) {

Mat image = *((Mat*)(userdata));
Mat dst = Mat::zeros(image.size(), image.type());
Mat m = Mat::zeros(image.size(), image.type());

addWeighted(image, 1.0, m, 0, lightness, dst);
/*
由公式:dst = src1 * alpha + src2 * beta + gamma 可知
设置第1个权重1,第二个权重0
相当于图片融合时,只有第1个的成分
设置要增加的标量为lightness
可以实现亮度的调整
*/
imshow("亮度/对比度调整", dst);

}

static void on_contrast(int contrast, void* userdata) {

Mat image = *((Mat*)(userdata));
Mat dst = Mat::zeros(image.size(), image.type());
Mat m = Mat::zeros(image.size(), image.type());

double contra = contrast / 100.0;
/*
这里的contrast初始值1 是一个[0,2]范围的数
*/
addWeighted(image, contra, m, 0, 0, dst);
/*
由公式:dst = src1 * alpha + src2 * beta + gamma 可知
设置第1个权重cotrast,第二个权重0
相当于图片融合时,只有第1个的成分
同时可以实现对比度的调整
*/

imshow("亮度/对比度调整", dst);

}

void QuickDemo::tracking_bar_demo(Mat& image) {

namedWindow("亮度/对比度调整", WINDOW_AUTOSIZE);

int lightness = 50;
int max_light = 100;

int contrast = 100;
int max_contrast = 200;

createTrackbar("Value Bar:", "亮度/对比度调整", &lightness, max_light, on_light, (void*)(&image));
createTrackbar("Contrast Bar:", "亮度/对比度调整", &contrast, max_contrast, on_contrast, (void*)(&image));

/*无用语句,理由同上*/
//on_light(lightness, &image);
//on_contrast(lightness, &image);

}

关于亮度的调整

为什么会变亮呢?

第一张图权重alpha,权重始终为1
第二张图权重beta,权重始终为0
加gmama标量,增大了每个像素点的值,相当于每个像素点的值都在接近255,所以就会变亮

那该怎么变暗呢?

我们只需要让gamma标量是负数,这样就可以减小每个像素点的值
每个像素点的值,都在接近0,所以就会变暗

由于课程中的代码只实现了增大亮度,我们改进一下,改成能够提高/降低亮度

static void on_light(int lightness, void* userdata) {

Mat image = *((Mat*)(userdata));
Mat dst = Mat::zeros(image.size(), image.type());
Mat m = Mat::zeros(image.size(), image.type());

double light = lightness - 50.0;
/*
gamma范围变成[-50,50]
这样我们就实现了增大/降低亮度,
*/

addWeighted(image, 1.0, m, 0, light, dst);

imshow("亮度/对比度调整", dst);

}

中间为原图
在这里插入图片描述

关于对比度的调整

为什么会变黑?

第一张图权重alpha,权重初始为1
第二张图权重beta,权重始终为0
第一张图的权重alpha变化时,当alpha从1.0→0,图像的每个像素点 * 这个变化alpha
这意味着第一张图的像素点的值在变小,在接近0,同时像素点之间的差值也在变小
因为最小就是0,最后每个像素点的值变成0,就变成了黑色

为什么会变白?

第一张图的权重alpha变化时,当alpha从1.0→2.0,图像的每个像素点 * 这个变化alpha
这意味着第一张图的像素点的值在变大,接近255,同时像素点之间的差值也在变大
因为最大就是255,最后每个像素点的值变成255,就变成了白色

中间为原图

在这里插入图片描述

本课所用API查阅

OpenCV 4.6.0 官方文档

creatTrackbar()

在这里插入图片描述
在这里插入图片描述

TrackbarCallback()

在这里插入图片描述

addWeighted()

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

dst = src1*alpha + src2*beta + gamma;

在这里插入图片描述

08 回调函数+键盘响应操作

Excerpt

08 回调函数+键盘响应操作


08 回调函数+键盘响应操作

opencv知识点:

  • 回调函数 - callback
  • 键盘响应 - waitKey()

本课所解决的问题:

  • 什么是回调函数(callback)?
  • 在opencv中如何进行键盘响应?
  • 如何利用键盘响应对图像进行操作?
  • 处理视频的时候waitKey的参数应为多少?

1.回顾createTrackbar

在前面的课中,调用createTrackbar时,我们把函数on_lighton_contrast作为参数传入了,这种特别的方式,它被称为callback。回调函数(callback)在很多的UI和界面编程中非常常用 ,它最早在Windows叫做消息机制。

这里我们详细讲解一下回调函数,来加深对这种方式的理解

什么是回调函数

作者:no.body
链接:回调函数(callback)是什么?
来源:知乎

我们绕点远路来回答这个问题。

编程分为两类:系统编程(system programming)和应用编程(application programming)

  • 所谓系统编程,简单来说,就是编写
  • 而应用编程就是利用写好的各种库来编写具某种功用的程序,也就是应用

系统程序员会给自己写的库留下一些接口,即API(应用编程接口),以供应用程序员使用。
所以在抽象层的图示里,库位于应用的底下。

当程序跑起来时,一般情况下,应用程序(application program)会时常通过API调用库里所预先备好的函数。
但是有些库函数(library function)却要求应用先传给它一个函数,好在合适的时候调用,以完成目标任务。

这个被传入的、后又被调用的函数就称为回调函数(callback function)

打个比方,有一家旅馆提供叫醒服务,但是要求旅客自己决定叫醒的方法。可以是打客房电话,也可以是派服务员去敲门,睡得死怕耽误事的,还可以要求往自己头上浇盆水。

  • 这里,“叫醒”这个行为是旅馆提供的,相当于库函数
  • 但是叫醒的方式是由旅客决定并告诉旅馆的,也就是回调函数
  • 而旅客告诉旅馆怎么叫醒自己的动作,也就是把回调函数传入库函数的动作,称为登记回调函数

如下图所示(图片来源:维基百科):
在这里插入图片描述

回调机制的优势

从上面的例子可以看出,回调机制提供了非常大的灵活性。

请注意,从现在开始,我们把图中的库函数改称为中间函数了。
这是因为回调并不仅仅用在应用和库之间,任何时候,只要想获得类似于上面情况的灵活性,都可以利用回调。

这种灵活性是怎么实现的呢?

乍看起来,回调似乎只是函数间的调用,但仔细一琢磨,可以发现两者之间的一个关键的不同:
在回调中,我们利用某种方式,把回调函数像参数一样传入中间函数。
可以这么理解,在传入一个回调函数之前,中间函数是不完整的。

换句话说,程序可以在运行时,通过登记不同的回调函数,来决定、改变中间函数的行为

这就比简单的函数调用,要灵活太多了。

2.键盘响应

接下来是本课的内容,键盘响应。

在opencv中,利用waitKey(),可以实现键盘事件的响应

本函数没用显示图片,点击的图片是main函数中显示的图片
注意:无论是main函数的,还是key_demo函数的,都可以触发键盘事件

//函数定义
void key_demo(Mat& image);
//函数实现
void QuickDemo::key_demo(Mat& image) {

while (true) {

char c = waitKey(1000);
/*
用char存储,能存储一些字符,但像esc这些就无法存储
为了存储这些功能键,我们可以转为int存储方式,存储它们对应的ASCII码值
*/
//int c = waitKey(1000);
std::cout << c << std::endl;
}
}

我们点击图像,按键盘就会打印对应的键

在这里插入图片描述

3.利用键盘进行图像操作

我们利用键盘响应,来实现对图像的操作

void QuickDemo::key_demo(Mat& image) {

Mat dst = Mat::zeros(image.size(), image.type());

while (true) {

int c = waitKey(100);
if (c == 27) {//退出
break;
}
if (c == 49) {//1 图像转为灰度
std::cout << "1" << std::endl;
cvtColor(image,dst,COLOR_BGR2GRAY);
}
if (c == 50) {//2 图像转为HSV
std::cout << "2" << std::endl;
cvtColor(image, dst, COLOR_BGR2HSV);
}
if (c == 51) {//3 图像亮度+50
std::cout << "3" << std::endl;
dst = Scalar(50, 50, 50);
add(image, dst, dst);
}
imshow("键盘响应", dst);
}
}

初始为黑色

在这里插入图片描述

按下1,变为灰度图像

在这里插入图片描述

按下2,变为HSV图像

在这里插入图片描述

按下3,亮度+50

在这里插入图片描述

4.视频处理的waitKey参数

当我们处理视频的时候,waitKey参数应为通常为1,即waitKey(1)

本课所用API查阅

OpenCV 4.6.0 官方文档

waitKey()

在这里插入图片描述
在这里插入图片描述

09 OpenCV自带颜色表操作

Excerpt

09 OpenCV自带颜色表操作


09 OpenCV自带颜色表操作

opencv知识点:

  • opencv4的颜色表
  • 应用图像映射 - applyColorMap()

本科所解决的问题:

  • 如何使用OpenCV中的颜色表?
  • 如何循环转换各种颜色风格?

1.Opencv4的颜色表

opencv提供22种颜色风格的查找表映射,官方文档查阅ColormapTypes即可

2.颜色表的使用

在使用的时候要用到applyColorMap,传入对应colormap,就可以进行颜色风格的转换

applyColorMap
应用颜色图
共3个参数 
第1个参数 输入
第2个参数 输出
第3个参数 颜色图(查阅官方文档可知)

如我们传入COLORMAP_DEEPGREEN,就会有如下效果

void QuickDemo::color_style_demo(Mat& image) {

Mat dst;

applyColorMap(image, dst, COLORMAP_DEEPGREEN);
imshow("COLORMAP_DEEPGREEN", dst);
}

在这里插入图片描述

3.循环转换颜色风格

我们首先把颜色表做成一个枚举数组

int colormap[]= {
     COLORMAP_AUTUMN,
     COLORMAP_BONE,
     COLORMAP_JET,
     COLORMAP_WINTER,
     COLORMAP_RAINBOW,
     COLORMAP_OCEAN,
     COLORMAP_SUMMER,
     COLORMAP_SPRING,
     COLORMAP_COOL,
     COLORMAP_HSV,//10
 COLORMAP_PINK,
 COLORMAP_HOT,
     COLORMAP_PARULA,
     COLORMAP_MAGMA,
     COLORMAP_INFERNO,
     COLORMAP_PLASMA,
     COLORMAP_VIRIDIS,
     COLORMAP_CIVIDIS,
     COLORMAP_TWILIGHT,
     COLORMAP_TWILIGHT_SHIFTED,//20
     COLORMAP_TURBO,
     COLORMAP_DEEPGREEN
};

通过applyColorMap以及1个while循环,我们便可以实现颜色风格的循环转换

void QuickDemo::color_style_demo(Mat& image) {

int colormap[] = {
 COLORMAP_AUTUMN,
 COLORMAP_BONE,
 COLORMAP_JET,
 COLORMAP_WINTER,
 COLORMAP_RAINBOW,
 COLORMAP_OCEAN,
 COLORMAP_SUMMER,
 COLORMAP_SPRING,
 COLORMAP_COOL,
 COLORMAP_HSV,//10
 COLORMAP_PINK,
 COLORMAP_HOT,
 COLORMAP_PARULA,
 COLORMAP_MAGMA,
 COLORMAP_INFERNO,
 COLORMAP_PLASMA,
 COLORMAP_VIRIDIS,
 COLORMAP_CIVIDIS,
 COLORMAP_TWILIGHT,
 COLORMAP_TWILIGHT_SHIFTED,//20
 COLORMAP_TURBO,
 COLORMAP_DEEPGREEN
};

Mat dst;
int index = 0;
while (true) {

int c = waitKey(500);

if (c == 27) {//退出
break;
}
applyColorMap(image, dst, colormap[index % 22]);

index++;
imshow("22种颜色风格",dst);
}
}

在这里插入图片描述
在这里插入图片描述

本课所用API查阅

OpenCV 4.6.0 官方文档

applyColorMap()

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

ColormapTypes

在这里插入图片描述
在这里插入图片描述

10 图像像素的逻辑操作

Excerpt

10 图像像素的逻辑操作


10 图像像素的逻辑操作

opencv知识点:

  • 绘制矩形 - rectangle()
  • 位运算 - 四种逻辑操作

本课所解决的问题:

  • 如何绘制矩形?
  • 绘制图形的最后一个参数shift有什么作用?
  • 如何对图像进行位运算?

1.绘制矩形

我们先来绘制两个矩形,绘制矩形有两种传参方式

rectangele
绘制矩形
共7个参数
第1个参数 输入
第2个参数 矩形左上坐标
第3个参数 矩形右下坐标
第4个参数 矩形颜色
第5个参数 线宽
如果参数 >=0,则表示绘制矩形(如为1,表示绘制的矩形边为1个像素)
如果参数 < 0,则表示填充矩形(如-1,表示填充整个矩形)
第6个参数 lineType
关于图像锯齿,有几种方式处理
不管不顾,就用LINE_4 或者 LINE_8
消除锯齿,就用LINE_AA (AA就是反锯齿)
第7个参数  缩小图像,同时缩短矩形左上顶点与(0,0)位置的距离
  0表示不变
  1表示图像*1/2,同时距离(0,0)的x方向和y方向距离*1/2
  2表示图像*(1/2)^2,同时距离(0,0)的x方向和y方向距离*(1/2)^2
rectangele
绘制矩形
共6个参数
第1个参数 输入
第2个参数 矩形的左上点+往对角方向延伸的距离(x1,x2,延伸长度1,延伸长度2)
第3个参数 矩形颜色
第4个参数 线宽
如果参数 >=0,则表示绘制矩形(如为1,表示绘制的矩形边为1个像素)
如果参数 < 0,则表示填充矩形(如-1,表示填充整个矩形)
第5个参数 lineType
关于图像锯齿,有几种方式处理
不管不顾,就用LINE_4 或者 LINE_8
消除锯齿,就用LINE_AA (AA就是反锯齿)
第6个参数  缩小图像,同时缩短矩形左上顶点与(0,0)位置的距离
  0表示不变
  1表示图像*1/2,同时距离(0,0)的x方向和y方向距离*1/2
  2表示图像*(1/2)^2,同时距离(0,0)的x方向和y方向距离*(1/2)^2
//函数定义
void bitwise_demo(Mat& image);
//函数实现
void QuickDemo::bitwise_demo(Mat& image) {

Mat m1 = Mat::zeros(Size(256, 256), CV_8UC3);
Mat m2 = Mat::zeros(Size(256, 256), CV_8UC3);

//rectangle有两种传参方式,这里分别进行了示范
rectangle(m1, Point(100, 100), Point(180, 180), Scalar(255, 255, 0), -1, LINE_8,0);

rectangle(m2, Rect(150, 150, 80, 80), Scalar(0, 255, 255), -1, LINE_8, 0);

imshow("m1", m1);
imshow("m2", m2);
}

在这里插入图片描述

这里重点说一下最后1个参数int shift = 0

这个参数的作用是:缩小图像,同时缩短矩形左上顶点与(0,0)位置的距离

  • 0表示不变
  • 1表示图像 * 1/2,同时距离(0,0)的x方向和y方向距离 * 1/2
  • 2表示图像 * (1/2)^2, 同时距离(0,0)的x方向和y方向距离 * (1/2)^2
  • 3表示图像 * (1/2)^3, 同时距离(0,0)的x方向和y方向距离 * (1/2)^3

如下就是3,2,1,0对应的效果
在这里插入图片描述

2.位运算

在opencv中,图像的为运算有4种

  • 异或

接下来,我们分别进行演示

void QuickDemo::bitwise_demo(Mat& image) {

Mat m1 = Mat::zeros(Size(256, 256), CV_8UC3);
Mat m2 = Mat::zeros(Size(256, 256), CV_8UC3);

rectangle(m1, Point(100, 100), Point(180, 180), Scalar(255, 255, 0), -1, LINE_4,0);

rectangle(m2, Rect(150, 150, 80, 80), Scalar(0, 255, 255), -1, 0);

Mat dst;

bitwise_and(m1, m2, dst);
//bitwise_or(m1, m2, dst);
//bitwise_not(m1, dst);
//bitwise_xor(m1, m2, dst);

imshow("位运算",dst);

}
bitwise_and(m1, m2, dst);

在这里插入图片描述

bitwise_or(m1, m2, dst);

在这里插入图片描述

为了更好的看到 非 的效果,这里我们选择对传入的图像image进行 非 运算
bitwise_not(image, dst);

还有取反符号版本
dst = ~image;

在这里插入图片描述

bitwise_xor(m1, m2, dst);

在这里插入图片描述

本课所用API查阅

OpenCV 4.6.0 官方文档

rectangle()

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

bitwise_and()

在这里插入图片描述

在这里插入图片描述

bitwise_or()

在这里插入图片描述

在这里插入图片描述

bitwise_not()

在这里插入图片描述

在这里插入图片描述

bitwise_xor()

在这里插入图片描述

在这里插入图片描述

11 通道分离与合并

Excerpt

11 通道分离与合并


11 通道分离与合并

opencv知识点:

  • 通道分离 - split()
  • 通道合并 - merge()
  • 通道混合 - mixChannels()

本课所解决的问题:

  • 如何分离RGB三通道?
  • 如何合并RGB三通道?
  • 如何对通道进行混合?

1.RGB三通道的解释

彩色图像,是由RGB三个通道合并起来得到的。

如果R,G,B分离,它们就分别对应一个单通道图像(因为都是单通道,所以为灰度图像)
当然,这三个单通道图像再经过合并,就会恢复成原本的彩色图像了

下图中间的R,G,B图,并不是分离,而是3通道中其他两个通道置0了 。
这时,如果再通过BGR2GRAY转换色彩空间,就可以得到对应的单通道图像。

在这里插入图片描述

2.通道分离

分离通道要用到split
根据文档,我们有两种分离方式

第一种方式

//函数定义
void channels_demo(Mat& image);

//函数实现—
void QuickDemo::channels_demo(Mat& image) {

Mat mvt[3];
/*
第一种方式
通过创建图像数组,存储每个单通道图像
*/
split(image, mvt);

imshow("蓝色单通道", mvt[0]);
imshow("绿色单通道", mvt[1]);
imshow("蓝色单通道", mvt[2]);
}

第二种方式

void QuickDemo::channels_demo(Mat& image) {

std::vector<Mat> mvt;
/*
第二种方式
通过创建动态数组,存储每个单通道图像
*/
split(image, mvt);

imshow("蓝色单通道", mvt[0]);
imshow("绿色单通道", mvt[1]);
imshow("红色单通道", mvt[2]);
}

分离后得到的结果
在这里插入图片描述

3.通道合并

合并通道要用到merge
根据文档,我们有两种合并方式

第一种方式

void QuickDemo::channels_demo(Mat& image) {

Mat mvt[3];

split(image, mvt);

imshow("蓝色单通道", mvt[0]);
imshow("绿色单通道", mvt[1]);
imshow("红色单通道", mvt[2]);

Mat dst;

merge(mvt,3,dst);
/*
这里的3指,共有3个单通道图像
*/
imshow("分离再合并",dst);

}

第二种方式

void QuickDemo::channels_demo(Mat& image) {

std::vector<Mat> mvt;

split(image, mvt);

imshow("蓝色单通道", mvt[0]);
imshow("绿色单通道", mvt[1]);
imshow("红色单通道", mvt[2]);

Mat dst;

merge(mvt, dst);

imshow("分离再合并",dst);

}

合并后得到的结果

在这里插入图片描述

4.通道混合

通道混合要用到mixChannels

mixChannels
混合通道
共6个参数
第1个参数 输入
第2个参数 输入的矩阵数
第3个参数 输出
第4个参数 输出的矩阵数
第5个参数 从哪个通道 变成 哪个通道
第6个参数 要变的对数

这里我们进行一个演示,实现如下通道的混合

  • 0通道→2通道
  • 1通道不变
  • 2通道→1通道

这个混合的意思是,彩色图像本来是bgr的顺序,经过通道混合就变成了rgb

  • 0通道的单通道图像,变成了2通道的单通道图像
  • 1通道不变
  • 2通道的单通道图像,变成了0通道的单通道图像
void QuickDemo::channels_demo(Mat& image) {

Mat dst = Mat::zeros(image.size(), image.type());

int from_to[] = { 0,2,1,1,2,0 };

mixChannels(&image, 1, &dst, 1, from_to, 3);

imshow("通道混合",dst);
}

混合通道后的结果

在这里插入图片描述

本课所用API查阅

OpenCV 4.6.0 官方文档

split()

在这里插入图片描述
在这里插入图片描述在这里插入图片描述
在这里插入图片描述

   char d[] = {1,2,3,4,5,6,7,8,9,10,11,12};
    Mat m(2, 2, CV_8UC3, d);
    Mat channels[3];
    split(m, channels);
    /*
    channels[0] =
    [  1,   4;
       7,  10]
    channels[1] =
    [  2,   5;
       8,  11]
    channels[2] =
    [  3,   6;
       9,  12]
    */

在这里插入图片描述
在这里插入图片描述

merge()

在这里插入图片描述
在这里插入图片描述

    Mat m1 = (Mat_<uchar>(2,2) << 1,4,7,10);
    Mat m2 = (Mat_<uchar>(2,2) << 2,5,8,11);
    Mat m3 = (Mat_<uchar>(2,2) << 3,6,9,12);
    Mat channels[3] = {m1, m2, m3};
    Mat m;
    merge(channels, 3, m);
    /*
    m =
    [  1,   2,   3,   4,   5,   6;
       7,   8,   9,  10,  11,  12]
    m.channels() = 3
    */

在这里插入图片描述

在这里插入图片描述

mixChannels()

在这里插入图片描述
在这里插入图片描述

Mat bgra( 100, 100, CV_8UC4, Scalar(255,0,0,255) );
Mat bgr( bgra.rows, bgra.cols, CV_8UC3 );
Mat alpha( bgra.rows, bgra.cols, CV_8UC1 );
// forming an array of matrices is a quite efficient operation,
// because the matrix data is not copied, only the headers
Mat out[] = { bgr, alpha };
// bgra[0] -> bgr[2], bgra[1] -> bgr[1],
// bgra[2] -> bgr[0], bgra[3] -> alpha[0]
int from_to[] = { 0,2, 1,1, 2,0, 3,3 };
mixChannels( &bgra, 1, out, 2, from_to, 4 );

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

12 图像色彩空间转换

Excerpt

12 图像色彩空间转换 - 进阶


12 图像色彩空间转换 - 进阶

opencv知识点:

  • 色彩空间转换 - cvtColor()
  • 提取指定色彩范围区域 - inRange()
  • 更换图像背景 - copyTo的mask用法

本课所解决的问题:

  • 如何提取指定色彩范围的区域?
  • 如何更换图像的背景?

1.HSV色彩空间

HSV相较于BGR色彩空间,颜色的区分度比较明显,对某个颜色来说可以很容易的提取出来。

在这里插入图片描述

2.提取指定色彩范围区域

opencv中,我们提取指定色彩范围的区域,采用inRange实现,这样的一块区域,学名叫做ROI(region of interest),感兴趣区域

关于inRange的提取原理

图像中,对于在指定色彩范围内的颜色,将置为255(白色),不在的则置为0(黑色)
对于多通道的输入,输出结果是各个通道的结果相与,当各通道结果都在上下限之内时,输出为255,否则为0。
因此也有人将输出理解为mask掩码模板,作为mask使用。

inRange
提取指定色彩范围内的区域
共4个参数
第1个参数 输入
第2个参数 色彩下界
第3个参数 色彩上界
第4个参数 输出
//函数定义
void inrange_demo(Mat& image);

//函数实现
void QuickDemo::inrange_demo(Mat& image) {

Mat dst;
cvtColor(image, dst, COLOR_BGR2HSV);
/*
为了提取指定色彩范围内的区域,我们先把图像转换为HSV色彩空间
*/

Mat mask;

inRange(dst, Scalar(100, 43, 46), Scalar(124, 255, 255), mask);

imshow("提取", mask);
}

在这里插入图片描述

3.更换图像背景

更换图像背景,这里利用到了重载的copyTo

copyTo
重载的拷贝
共2个参数
第1个参数 输出
第2个参数 输入

当我们传入mask时,这里的copyTo只会拷贝到mask中不为0的像素点,即白色区域(255)

inRange提取后,指定色彩区域为255,roi区域为0。
为了利用copyto,我们需要对提取后的图像进行非操作,这样roi区域就会变为255。

void QuickDemo::inrange_demo(Mat& image) {

Mat dst;
cvtColor(image, dst, COLOR_BGR2HSV);

Mat mask;

inRange(dst, Scalar(100, 43, 46), Scalar(124, 255, 255), mask);

imshow("提取", mask);

Mat greenback = Mat::zeros(image.size(), image.type());
greenback = Scalar(40, 200, 40);

bitwise_not(mask, mask);
imshow("非", mask);

image.copyTo(greenback, mask);
/*
mask剩下的黑色区域,由greenback填充
*/
imshow("绿色", greenback);

}

在这里插入图片描述

本课所用API查阅

OpenCV 4.6.0 官方文档

cvtColor()

在这里插入图片描述
在这里插入图片描述在这里插入图片描述
在这里插入图片描述

img *= 1./255;
cvtColor(img, img, COLOR_BGR2Luv);

在这里插入图片描述

inRange()

在这里插入图片描述

copyTo()

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

13 图像像素值统计

Excerpt

13 图像像素值统计


13 图像像素值统计

opencv知识点:

  • 图像像素最小/最大值 - minMaxLoc()
  • 图像像素均值/标准差 - meanStdDev()

本课所解决的问题:

  • 如何获取图像像素的最小/最大值?
  • 如何获取图像像素的均值/标准差?
  • 图像像素统计值用途是什么?

1.常用的像素值统计

在图像分析的时候,我们经常需要对单通道图像的像素进行统计,以下4种是比较常用的

  • 最小值(min)
  • 最大值(max)
  • 平均值(mean)
  • 标准差(standard deviation)

获得这4种统计,我们就要用到以下两个API

  • minMaxLoc
  • meanStdDev

这两个API非常有用,日后有很多地方用得到

2.像素值统计计算

接下来,我们来应用两个API,求取4种像素值统计

首先是最小值,最大值

minMaxLoc
求取单通道图像像素的最小值,最大值
共6个参数
第1个参数 输入单通道图像
第2个参数 输出最小值
第3个参数 输出最大值
第4个参数 输出最小值点的坐标
第5个参数 输出最大值点的坐标

第6个参数 输入图像的子数组(有时候我们会求取ROI区域的最小/最大值,就会传入mask图像)
(这里的子数组,是一种图像掩模,可以实现加东西/扣东西) 
//函数定义
void pixel_statistics_demo(Mat& image);
//函数实现
void QuickDemo::pixel_statistics_demo(Mat& image) {

double minv, maxv;
Point minLoc, maxLoc;

std::vector<Mat> mvt;
split(image, mvt);

for (int i = 0; i < mvt.size(); i++) {
minMaxLoc(mvt[i], &minv, &maxv, &minLoc, &maxLoc);
std::cout << "通道:" << i << " 最小值:" << minv << " 最大值:" << maxv << std::endl;
}

}

在这里插入图片描述

然后是,平均值和标准差

meanStdDev
求取平均值,标准差
共4个参数
第1个参数 输入
第2个参数 输出图像像素的平均值,每个通道都会输出一个
第3个参数 输出图像像素的标准差,每个通道都会输出一个

第4个参数 输入图像的子数组(有时候我们会求取ROI区域的平均值/标准差,就会传入mask图像)
(这里的子数组,是一种图像掩模,可以实现加东西/扣东西) 
void QuickDemo::pixel_statistics_demo(Mat& image) {

Mat mean, stddev;

meanStdDev(image, mean, stddev);

std::cout << "平均值" << std::endl << mean << std::endl;
std::cout << "标准差" << std::endl<<stddev << std::endl;
}

在这里插入图片描述

上图中,输出平均值和标准差,是把所有通道的都输出了,那如果怎么输出单通道的呢?
很简单,只要用到Mat::at

//opencv为了保证精度,平均值,标准差矩阵的数据类型是double类型

/*根据上面的的排列可知,00,10,20分别代表三个通道*/
std::cout<<"平均值"<< mean.at<double>(0, 0)<<std::endl;
std::cout << "标准差" << stddev.at<double>(0, 0) << std::endl;

在这里插入图片描述

3.图像统计值分析

这里简单提一下图像分析的事情

图像的平均值和标准差会给我们带来一定的信息。
当平均值恒定,标准差很小时,我们可以想到是基本纯色的图片,也就是低对比度的图。

在图像分析的时候,我们关注图像的有效信息,通过图像像素的统计值,我们就可以对图像的有效信息作出判断。
当图像标准差很小时,图像所携带的有效信息会很少,我们就要对图像进行筛选,通过一些手段过滤掉一些东西。

本课所用API查阅

OpenCV 4.6.0 官方文档

minMaxLoc()

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

meanStdDev()

在这里插入图片描述
在这里插入图片描述在这里插入图片描述

在这里插入图片描述

Mat::at

虽然有12种重载,但只有6种传参方式

1. 单维度坐标——i0:沿维度 0 的索引
在这里插入图片描述在这里插入图片描述
在这里插入图片描述
2. 双维度坐标——row 沿维度 0 的索引 ;col 沿维度 1 的索引
在这里插入图片描述

3. 三维度坐标——i0 沿维度 0 的索引;i1 沿维度 1 的索引;i2 沿维度 2 的索引
在这里插入图片描述

4. 维度数组坐标——int数组在这里插入图片描述

5. 维度数组坐标——vector数组在这里插入图片描述

6. 点的坐标——point类
List item

14 图像几何形状绘制

Excerpt

14 图像几何形状绘制


个人资料,仅供学习使用
学习课程:OpenCV4 C++ 快速入门视频30讲——贾志刚

14 图像几何形状绘制

opencv知识点:

  • 四种几何图形绘制 - 矩形 圆形 线段 椭圆
  • 图片融合 - addWeighted()

本课所解决的问题:

  • 如何绘制几何图形?

1.图形绘制

opencv中,图像的坐标是,↓y,→x,在填写参数的时候一定要注意
接下来,我们开始绘制几何图形

矩形

之前已经介绍过两种rectangle的所有参数,这里不再赘述
//函数定义
void drawing_demo(Mat& image);

//函数实现
void QuickDemo::drawing_demo(Mat& image) {

Rect rect;//关于rect,我们只要记住四个属性即可
rect.x = 50;
rect.y = 50;
rect.width = 50;
rect.height = 50;

Mat dst = Mat::zeros(image.size(), image.type());
rectangle(dst, rect, Scalar(0, 0, 255), -1, LINE_8, 0);

imshow("绘制图形", dst);

}

在这里插入图片描述

圆形

circle
绘制圆形
共7个参数
第1个参数 输入
第2个参数 圆心点
第3个参数 圆形半径
第4个参数 圆形颜色
第5个参数 线宽
第6个参数 lineType
第7个参数  缩小图像,同时缩短圆心与(0,0)位置的距离
  0表示不变
  1表示图像*1/2,同时距离(0,0)的x方向和y方向距离*1/2
  2表示图像*(1/2)^2,同时距离(0,0)的x方向和y方向距离*(1/2)^2
void QuickDemo::drawing_demo(Mat& image) {

Mat dst = Mat::zeros(image.size(), image.type());

circle(dst, Point(100, 100), 50, Scalar(200, 0, 0), -1, 8, 0);

imshow("绘制图形", dst);
}

在这里插入图片描述

线段

line
绘制线段
共7个参数
第1个参数 输入
第2个参数 起点
第3个参数 终点
第4个参数 线段颜色
第5个参数 线宽(注意,这个时候线宽只能>=0)
第6个参数 lineType
第7个参数  缩短线段左上顶点与(0,0)位置的距离
  0表示不变
  1表示图像*1/2,同时距离(0,0)的x方向和y方向距离*1/2
  2表示图像*(1/2)^2,同时距离(0,0)的x方向和y方向距离*(1/2)^2
void QuickDemo::drawing_demo(Mat& image) {

Mat dst = Mat::zeros(image.size(), image.type());

line(dst, Point(100, 100), Point(300, 300), Scalar(33, 55, 66), 30, 8, 0);


imshow("绘制图形", dst);
}

在这里插入图片描述

椭圆
在opencv中,椭圆有两种传参方式

第一种,全功能版本

ellipse
绘制椭圆
共10个参数
第1个参数 输入
第2个参数 椭圆中心
第3个参数 椭圆两个轴的一半(类似于圆的半径)
第4个参数 椭圆的初始角度
第5个参数 绘制的起点角度
第6个参数 绘制的终点角度
第7个三叔 椭圆的颜色
第8个参数 线宽
第9个参数 lineType
第10个参数 缩小图像,同时缩短圆心与(0,0)位置的距离
  0表示不变
  1表示图像*1/2,同时距离(0,0)的x方向和y方向距离*1/2
  2表示图像*(1/2)^2,同时距离(0,0)的x方向和y方向距离*(1/2)^2
void QuickDemo::drawing_demo(Mat& image) {

Mat dst = Mat::zeros(image.size(), image.type());

ellipse(dst, Point(200, 200), Size(100, 50), 0, 0, 270, Scalar(200, 0, 0), 10, 8, 0);

imshow("绘制图形", dst);
}

在这里插入图片描述

第二种,简易版本

ellipse
绘制椭圆
共5个参数
第1个参数 输入
第2个参数 RotatedRect
第3个参数 椭圆颜色
第4个参数 线宽
第5个参数 lineType
void QuickDemo::drawing_demo(Mat& image) {

Mat dst = Mat::zeros(image.size(), image.type());

RotatedRect rrt;

rrt.center = Point(200, 200);//椭圆的中心
rrt.size = Size(100, 50);//椭圆两个轴大小的一半
rrt.angle = 0;//椭圆的旋转角度

ellipse(dst,rrt,Scalar(200,0,0),10,8);

imshow("绘制图形", dst);
}

在这里插入图片描述

2.图像融合几何图形

接下来,这里演示一种很有意思的用法

  • 利用addWeighted融合几何图形与原图像
void QuickDemo::drawing_demo(Mat& image) {

Mat dst = Mat::zeros(image.size(), image.type());

RotatedRect rrt;

rrt.center = Point(200, 200);//椭圆的中心
rrt.size = Size(100, 50);//椭圆两个轴大小的一半
rrt.angle = 0;//椭圆的旋转角度

ellipse(dst,rrt,Scalar(200,0,0),10,8);

addWeighted(image, 0.7, dst, 0.3, 0, dst);

imshow("绘制图形", dst);
}

可以看到,这种隐约的图形效果很nice,只在原图像上绘制图形是达不到这种效果的

在这里插入图片描述

本课所用API查阅

OpenCV 4.6.0 官方文档

rectangle()

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

circle()

在这里插入图片描述
在这里插入图片描述

line()

在这里插入图片描述
在这里插入图片描述

ellipse()

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

addWeighted()

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

dst = src1*alpha + src2*beta + gamma;

在这里插入图片描述

15 随机数与随机颜色

Excerpt

15 随机数与随机颜色


15 随机数与随机颜色

opencv知识点:

  • 随机数 - RNG
  • 线段绘制 - line()

本课所解决的问题:

  • 如何在绘制图形中利用到随机数?

1.利用随机数设置坐标与颜色

我们绘制一个线段,把两个点的坐标,还有三个通道的颜色都设置为了随机数得到

//函数定义
void randow_drawing();

//函数实现
void QuickDemo::randow_drawing() {

Mat canvas = Mat::zeros(Size(512, 512), CV_8UC3);

int w = canvas.cols;
int h = canvas.rows;

RNG rng(5465);//我们随便输入1个数作为种子

while (true) {

int c = waitKey(10);
if (c == 27) {//退出
break;
}

int x1 = rng.uniform(0, w);
int y1 = rng.uniform(0, h);
int x2 = rng.uniform(0, w);
int y2 = rng.uniform(0, h);

int b = rng.uniform(0, 255);
int g = rng.uniform(0, 255);
int r = rng.uniform(0, 255);

line(canvas, Point(x1, y1), Point(x2, y2), Scalar(b, g, r), 1, 8, 0);
imshow("随机线段", canvas);
}
}

在这里插入图片描述

本课所用API查阅

OpenCV 4.6.0 官方文档

RNG

RNG(Random Number Generator,随机数生成器)是opencv中的一个随机数生成器类
uniform是RNG中的一个方法,uniform(a,b),指定数的范围为(a,b)

在这里插入图片描述

line()

在这里插入图片描述
在这里插入图片描述

16 多边形填充与绘制

Excerpt

16 多边形填充与绘制


16 多边形填充与绘制

opencv知识点:

  • 绘制多条多边形曲线 - polylines()
  • 填充绘制的多边形 - fillPoly()
  • 画轮廓 - drawContours()

本课所解决的问题:

  • 如何根据指定的点绘制多边形?
  • 如何填充绘制的多边形?
  • 如何在绘制的同时,同时进行填充?

1.绘制多边形

根据几个点绘制多条边的时候,我们要用到polylines()

polylines
绘制多条多边形曲线
共7个参数
第1个参数 输入
第2个参数 输入多边形点集(绘制边的顺序,与点集数组中点的顺序有关)
第3个参数 isclosed(bool类型)
第4个参数 多边形颜色
第5个参数 线宽
第6个参数 lineType
第7个参数 shift(之前已经讲过,这里不再赘述)

要注意的是,绘制边的顺序,与点集数组中点的顺序有关

//函数定义
void polyline_drawing_demo();

//函数实现
void QuickDemo::polyline_drawing_demo() {

Mat canvas = Mat::zeros(Size(256, 256), CV_8UC3);

Point p1(40,40),p2(80,80),p3(80,60),p4(120,80),p5(120,60),p6(160,40);

std::vector<Point> pts;

pts.push_back(p1);
pts.push_back(p2);
pts.push_back(p3);
pts.push_back(p4);
pts.push_back(p5);
pts.push_back(p6);

polylines(canvas, pts, true, Scalar(200, 0, 0), 5, 8, 0);


imshow("000",canvas);

}

在这里插入图片描述

2.填充绘制的多边形

填充我们刚才绘制的轮廓,就要用到fillPoly()

fillPoly
填充绘制的多边形
共6个参数
第1个参数 输入
第2个参数 点集数组
第3个参数 填充颜色
第4个参数 lineType
第5个参数 shift(不再赘述)
第6个参数 轮廓所有点的可选偏移
void QuickDemo::polyline_drawing_demo() {

Mat canvas = Mat::zeros(Size(256, 256), CV_8UC3);

Point p1(40,40),p2(80,80),p3(80,60),p4(120,80),p5(120,60),p6(160,40);

std::vector<Point> pts;

pts.push_back(p1);
pts.push_back(p2);
pts.push_back(p3);
pts.push_back(p4);
pts.push_back(p5);
pts.push_back(p6);

polylines(canvas, pts, true, Scalar(200, 0, 0), 5, 8, 0);

fillPoly(canvas, pts, Scalar(200, 0, 0), 8, 0);

imshow("000",canvas);
}

在这里插入图片描述

3.绘制与填充同时进行

那有没有一种办法,可以在绘制的同时,进行填充呢?
这就用到了drawContours()

drawContours
绘制轮廓轮廓/填充轮廓
共9个参数,这里我们只介绍前5个(剩下的有缺省值)
第1个参数 输入
第2个参数 多边形轮廓的数组
第3个参数 选择的多边形轮廓(在数组中的编号,-1为全选)
第4个参数 多边形颜色
第5个参数 填充/绘制(-1为填充)
void QuickDemo::polyline_drawing_demo() {

Mat canvas = Mat::zeros(Size(256, 256), CV_8UC3);

Point p1(40,40),p2(80,80),p3(80,60),p4(120,80),p5(120,60),p6(160,40);

std::vector<Point> pts;

pts.push_back(p1);
pts.push_back(p2);
pts.push_back(p3);
pts.push_back(p4);
pts.push_back(p5);
pts.push_back(p6);

std::vector<std::vector<Point>> contours;

contours.push_back(pts);

drawContours(canvas, contours, -1, Scalar(200, 0, 0), -1);

imshow("000",canvas);
}

在这里插入图片描述

本课所用API查阅

OpenCV 4.6.0 官方文档

polylines()

在这里插入图片描述在这里插入图片描述
在这里插入图片描述

fillPoly()

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

drawContours()

在这里插入图片描述
在这里插入图片描述

17 鼠标操作与响应

Excerpt

17 鼠标操作与响应


17 鼠标操作与响应

opencv知识点:

  • 设置指定窗口的鼠标处理程序 - setMouseCallback()
  • 鼠标事件回调函数 - MouseCallback()
  • 简单提取矩形ROI区域 - image(box)

本课所解决的问题:

  • 如何通过鼠标绘制矩形?
  • 如何通过矩形选择ROI区域?

1.鼠标事件

要想在图像上,通过鼠标绘制图形,我们需要了解两个API

  • setMouseCallback
  • MouseCallback类型的回调函数
setMouseCallback
设置指定窗口的鼠标处理程序
共3个参数
第1个参数 窗口名称
第2个参数 处理鼠标事件的回调函数
第3个参数 传递给回调函数的可选参数

MouseCallback类型的函数
共5个参数,这5个参数全是针对鼠标的捕捉
第1个参数 捕捉的鼠标事件(查阅文档可知,都有什么类型)
第2个参数 捕捉的鼠标事件的x坐标
第3个参数 捕捉的鼠标事件的y坐标
第4个参数 标志捕捉的事件是哪一个键(有5种,查阅文档可知)
第5个参数 void*类型的可选传入数据

2.通过鼠标绘制矩形

为了演示通过鼠标绘制矩形,我们选择三个鼠标事件

  • 左键按下 - EVENT_LBUTTONDOWN
  • 鼠标移动 - EVENT_MOUSEMOVE
  • 左键松开 - EVENT_LBUTTONUP

这里要注意几点

  • setMouseCallback要传入窗口的名字,所以要使用namedWindow事先创建一个窗口,不然会报错
  • 我们绘制矩形的时候,如果终点在图像外就会报错,只要根据if判断即可,不过本文没有处理

我们这里,实现了通过鼠标在四个区域绘制矩形

第一版 - 四区域绘制矩形

//函数定义
void mouse_drawing_demo(Mat& image);
//函数实现
Point sp(-1, -1), ep(-1, -1);
static void on_draw(int event, int x, int y, int flags, void* userdata) {

Mat image = *((Mat*)userdata);

if (event == EVENT_LBUTTONDOWN) {//左键按下
sp.x = x;
sp.y = y;
std::cout << "起点:" << sp;
}

if (event == EVENT_LBUTTONUP) {//左键松开
ep.x = x;
ep.y = y;
std::cout << "终点:" << ep << std::endl;

int w = ep.x - sp.x;
int h = ep.y - sp.y;


if (w < 0 && h < 0) {//↖
rectangle(image, Rect(sp.x+w, sp.y+h, -w, -h), Scalar(0, 0, 255), 2, 8, 0);
}
if (w < 0 && h >= 0) {//↙
rectangle(image, Rect(sp.x+w, sp.y, -w, h), Scalar(0, 0, 255), 2, 8, 0);
}
if (w >= 0 && h < 0) {//↗
rectangle(image, Rect(sp.x, sp.y+h, w, -h), Scalar(0, 0, 255), 2, 8, 0);
}

if (w >= 0 && h >= 0) {//↘
rectangle(image, Rect(sp.x, sp.y, w, h), Scalar(0, 0, 255), 2, 8, 0);
}
imshow("鼠标绘制矩形", image);
}
}

void QuickDemo::mouse_drawing_demo(Mat& image) {

namedWindow("鼠标绘制矩形", WINDOW_AUTOSIZE);

setMouseCallback("鼠标绘制矩形", on_draw, (void*)(&image));
imshow("鼠标绘制矩形", image);

}

在这里插入图片描述

第二版 - 增加绘制轨迹

Point sp(-1, -1), ep(-1, -1);
static void on_draw(int event, int x, int y, int flags, void* userdata) {

Mat image = *((Mat*)userdata);

if (event == EVENT_LBUTTONDOWN) {//左键按下
sp.x = x;
sp.y = y;
std::cout << "起点:" << sp;

}


if (flags == EVENT_FLAG_LBUTTON && event == EVENT_MOUSEMOVE) {//按下左键+鼠标移动
ep.x = x;
ep.y = y;

int w = ep.x - sp.x;
int h = ep.y - sp.y;


if (w < 0 && h < 0) {//↖
rectangle(image, Rect(sp.x + w, sp.y + h, -w, -h), Scalar(0, 0, 255), 2, 8, 0);
}
if (w < 0 && h >= 0) {//↙
rectangle(image, Rect(sp.x + w, sp.y, -w, h), Scalar(0, 0, 255), 2, 8, 0);
}
if (w >= 0 && h < 0) {//↗
rectangle(image, Rect(sp.x, sp.y + h, w, -h), Scalar(0, 0, 255), 2, 8, 0);
}

if (w >= 0 && h >= 0) {//↘
rectangle(image, Rect(sp.x, sp.y, w, h), Scalar(0, 0, 255), 2, 8, 0);
}
imshow("鼠标绘制矩形", image);
}
if (event == EVENT_LBUTTONUP) {//左键松开
ep.x = x;
ep.y = y;
std::cout << "终点:" << ep << std::endl;

int w = ep.x - sp.x;
int h = ep.y - sp.y;


if (w < 0 && h < 0) {//↖
rectangle(image, Rect(sp.x+w, sp.y+h, -w, -h), Scalar(0, 0, 255), 2, 8, 0);
}
if (w < 0 && h >= 0) {//↙
rectangle(image, Rect(sp.x+w, sp.y, -w, h), Scalar(0, 0, 255), 2, 8, 0);
}
if (w >= 0 && h < 0) {//↗
rectangle(image, Rect(sp.x, sp.y+h, w, -h), Scalar(0, 0, 255), 2, 8, 0);
}

if (w >= 0 && h >= 0) {//↘
rectangle(image, Rect(sp.x, sp.y, w, h), Scalar(0, 0, 255), 2, 8, 0);
}
imshow("鼠标绘制矩形", image);
}
}
void QuickDemo::mouse_drawing_demo(Mat& image) {

namedWindow("鼠标绘制矩形", WINDOW_AUTOSIZE);

setMouseCallback("鼠标绘制矩形", on_draw, (void*)(&image));
imshow("鼠标绘制矩形", image);

}

在这里插入图片描述

第三版 - 只保留最新的绘制轨迹

Point sp(-1, -1), ep(-1, -1);
Mat temp;//保存最初图像
static void on_draw(int event, int x, int y, int flags, void* userdata) {

Mat image = *((Mat*)userdata);

if (event == EVENT_LBUTTONDOWN) {//左键按下
sp.x = x;
sp.y = y;
std::cout << "起点:" << sp;

}


if (flags == EVENT_FLAG_LBUTTON && event == EVENT_MOUSEMOVE) {//按下左键+鼠标移动
ep.x = x;
ep.y = y;

int w = ep.x - sp.x;
int h = ep.y - sp.y;

temp.copyTo(image);
if (w < 0 && h < 0) {//↖
rectangle(image, Rect(sp.x + w, sp.y + h, -w, -h), Scalar(0, 0, 255), 2, 8, 0);
}
if (w < 0 && h >= 0) {//↙
rectangle(image, Rect(sp.x + w, sp.y, -w, h), Scalar(0, 0, 255), 2, 8, 0);
}
if (w >= 0 && h < 0) {//↗
rectangle(image, Rect(sp.x, sp.y + h, w, -h), Scalar(0, 0, 255), 2, 8, 0);
}

if (w >= 0 && h >= 0) {//↘
rectangle(image, Rect(sp.x, sp.y, w, h), Scalar(0, 0, 255), 2, 8, 0);
}
imshow("鼠标绘制矩形", image);
}
if (event == EVENT_LBUTTONUP) {//左键松开
ep.x = x;
ep.y = y;
std::cout << "终点:" << ep << std::endl;

int w = ep.x - sp.x;
int h = ep.y - sp.y;


if (w < 0 && h < 0) {//↖
rectangle(image, Rect(sp.x+w, sp.y+h, -w, -h), Scalar(0, 0, 255), 2, 8, 0);
}
if (w < 0 && h >= 0) {//↙
rectangle(image, Rect(sp.x+w, sp.y, -w, h), Scalar(0, 0, 255), 2, 8, 0);
}
if (w >= 0 && h < 0) {//↗
rectangle(image, Rect(sp.x, sp.y+h, w, -h), Scalar(0, 0, 255), 2, 8, 0);
}

if (w >= 0 && h >= 0) {//↘
rectangle(image, Rect(sp.x, sp.y, w, h), Scalar(0, 0, 255), 2, 8, 0);
}
imshow("鼠标绘制矩形", image);
}
}

void QuickDemo::mouse_drawing_demo(Mat& image) {

namedWindow("鼠标绘制矩形", WINDOW_AUTOSIZE);

setMouseCallback("鼠标绘制矩形", on_draw, (void*)(&image));
imshow("鼠标绘制矩形", image);

temp = image.clone();
}

在这里插入图片描述

3.通过矩形选择ROI区域

接下来,我么试一下通过绘制矩形提取ROI区域

Point sp(-1, -1), ep(-1, -1);
Mat temp;//保存最初图像
static void on_draw(int event, int x, int y, int flags, void* userdata) {

Mat image = *((Mat*)userdata);

if (event == EVENT_LBUTTONDOWN) {//左键按下
sp.x = x;
sp.y = y;
std::cout << "起点:" << sp;
}
if (flags == EVENT_FLAG_LBUTTON && event == EVENT_MOUSEMOVE) {//按下左键+鼠标移动

temp.copyTo(image);//恢复最初图像

ep.x = x;
ep.y = y;
int w = ep.x - sp.x;
int h = ep.y - sp.y;

if (w < 0 && h < 0) {//↖
rectangle(image, Rect(sp.x + w, sp.y + h, -w, -h), Scalar(0, 0, 255), 2, 8, 0);
}
if (w < 0 && h >= 0) {//↙
rectangle(image, Rect(sp.x + w, sp.y, -w, h), Scalar(0, 0, 255), 2, 8, 0);
}
if (w >= 0 && h < 0) {//↗
rectangle(image, Rect(sp.x, sp.y + h, w, -h), Scalar(0, 0, 255), 2, 8, 0);
}

if (w >= 0 && h >= 0) {//↘
rectangle(image, Rect(sp.x, sp.y, w, h), Scalar(0, 0, 255), 2, 8, 0);
}
imshow("鼠标绘制矩形", image);

}
if (event == EVENT_LBUTTONUP) {//左键松开
ep.x = x;
ep.y = y;
std::cout << "终点:" << ep << std::endl;
int w = ep.x - sp.x;
int h = ep.y - sp.y;

Rect box;//提取ROI用
if (w < 0 && h < 0) {//↖
rectangle(image, Rect(sp.x+w, sp.y+h, -w, -h), Scalar(0, 0, 255), 2, 8, 0);
box = Rect(sp.x + w, sp.y + h, -w, -h);
}
if (w < 0 && h >= 0) {//↙
rectangle(image, Rect(sp.x+w, sp.y, -w, h), Scalar(0, 0, 255), 2, 8, 0);
box = Rect(sp.x + w, sp.y, -w, h);
}
if (w >= 0 && h < 0) {//↗
rectangle(image, Rect(sp.x, sp.y+h, w, -h), Scalar(0, 0, 255), 2, 8, 0);
box = Rect(sp.x, sp.y + h, w, -h);
}

if (w >= 0 && h >= 0) {//↘
rectangle(image, Rect(sp.x, sp.y, w, h), Scalar(0, 0, 255), 2, 8, 0);
box = Rect(sp.x, sp.y, w, h);
}

imshow("鼠标绘制矩形", image);

imshow("ROI区域",temp(box));

}
}

void QuickDemo::mouse_drawing_demo(Mat& image) {

namedWindow("鼠标绘制矩形", WINDOW_AUTOSIZE);

setMouseCallback("鼠标绘制矩形", on_draw, (void*)(&image));
imshow("鼠标绘制矩形", image);

temp = image.clone();
}

在这里插入图片描述
这里要注意几点

因为这只是一种简单的,通过矩形进行的ROI提取,提取的区域有时会有一些问题。
比如:提取矩形ROI区域时,如果box的width比较小,就会出现部分覆盖提取的问题。

在这里插入图片描述

本课所用API查阅

OpenCV 4.6.0 官方文档

setMouseCallback()

在这里插入图片描述
在这里插入图片描述

MouseCallback()

在这里插入图片描述
在这里插入图片描述

MouseEventFlags

在这里插入图片描述
在这里插入图片描述

MouseEventTypes

在这里插入图片描述
在这里插入图片描述

18 OpenCV4 C++ 快速入门

Excerpt

18 图像像素类型转换与归一化


个人资料,仅供学习使用
修改时间——2022年2月13日 21:01:34
学习课程:OpenCV4 C++ 快速入门视频30讲
视频老师:贾志刚

18 图像像素类型转换与归一化

opencv知识点:

  • 数据类型转换 - convertTo
  • 数据类型 - CV_bit位数+U/S/F+C通道数
  • 归一化 - normalize
  • 归一化类型 - NormTypes

本课所解决的问题:

  • 如何转换图像数据类型?
  • 如何归一化图像像素取值?
  • 归一化有什么用?

1.图像数据类型与归一化类型

opencv当中,我们可以通过API,对图像数据类型进行转换,以及对数据的取值空间范围进行转换。

它们分别要用到的API:

  • convertTo
  • normalize

这里分别对它们进行解释

convertTo

convertTO
数据类型转换
本文采用了第一种传参方式
共2个参数
第1个参数 输入
第2个参数 将要转换的数据类型(查阅文档可知)

normalize

normalize
归一化,归一指归为同一范围
共7个参数
第1个参数 输入
第2个参数 输出

第3个参数 alpha 规范值/归一化范围下限
第4个参数 beta 归一化范围上限
默认规范值为1,即数值规范为1,此时beta = 0
(所以这两处有两种传参方式
第一种 1.0 0,默认的这种,取值规范为[0,1]
第二种 0,b 下限上限的这种。取值规范为指定范围
两种方式都可以使用)

第5个参数 归一化类型(查阅文档可知)
第6个参数 默认类型与src一直
负数,则类型与src一直
否则,通道数和src一致,depth=指定的图像深度(图像深度= 图像列数*通道数)
第7个参数 可选mask

关于归一化类型,常用的有4种:

  • NORM_L1——求和归一
  • NORM_L2——三维向量转单位向量归一
  • NORM_INF——根据最大值归一
  • NORM_MINMAX——根绝最大最小值差值归一(最为常用)

官方文档解释如下
在这里插入图片描述

2.图像数据类型转换

一般彩色图像的数据类型是CV_8UC3,它代表什么含义呢?下面进行解释

数据类型公式:
CV_bit位数+U/S/F+C通道数。当单通道时,C1可以省略

U/S/F解释:

  • S——signed int——有符号整形
  • U——unsigned int——无符号整形
  • F——float——单精度浮点型

如CV_8UC3

  • 8指8it
  • U指无符号整型
  • C指通道数

接下来进行数据类型转换

//函数定义
void norm_demo(Mat& image);
//函数实现
void QuickDemo::norm_demo(Mat& image) {

//数据类型 CV_8UC3 对应16
std::cout << image.type() << std::endl;

image.convertTo(image, CV_32F);//转为32位浮点,通道数没有变
//数据类型 CV_32FC3 对应21
std::cout << image.type() << std::endl;

}

在这里插入图片描述
如果我们显示一下转换后的图像,会是什么样?
我们发现,转换为浮点数据类型后,并不能正确显示

这是因为imshow如果想要正确显示浮点类型的图像,必须进行归一化,把取值空间归一化为[0.1]
在这里插入图片描述

3.图像像素值归一化

oid QuickDemo::norm_demo(Mat& image) {

//数据类型 CV_8UC3 对应16
std::cout << image.type() << std::endl;

image.convertTo(image, CV_32F);//转为32位浮点,通道数没有变
//数据类型 CV_32FC3 对应21
std::cout << image.type() << std::endl;

Mat dst;

//对像素取值空间进行归一化
normalize(image, dst, 1.0, 0, NORM_MINMAX);
/*
根据我们最先的解释,所以 1.0 0和0 1.0都可以实现归一为取值范围1

要注意的是,数据类型是不变的
如果U没有转成浮点型的数据类型,那么归一化后的取值只有0/1两种,没有意义
*/
std::cout << dst.type() << std::endl;
imshow("norm",dst);

}

进行数据类型转换image.convertTo(image, CV_32F);//转为32位浮点,通道数没有变

转换后的
在这里插入图片描述

不进行转换的
在这里插入图片描述

4.归一化延伸知识

本文链接:图像的像素归一化
作者:Pierce_KK

图像的像素归一化是一个图像的预处理过程。

当我们做计算机视觉-深度学习的,通常要模型训练。
我们可以直接将原始图像的像素真实值直接作为神经网络模型的训练数据,
但是这可能会给我们模型的训练过程带来一些问题, 因为在深度神经网络训练时一般使用较小的权重值来进行拟合,而当训练数据的值是较大整数值时,可能会减慢模型训练的过程。

如果我们在将图像输入到神经网络之前对图像做像素值归一化的处理,即将像素值缩放到0-1之间,就能够避免很多不必要的麻烦。

(虽然图像的像素处于0-1范围时,opencv会自动*255,由于仍然介于0-255之间,所以图像依旧是有效的,并且可以正常查看图像。)

计算机视觉-深度学习相关:

本课所用API查阅

数据类型——原始类型

在这里插入图片描述

数据类型——typedef

在这里插入图片描述

数据类型——#define

在这里插入图片描述

NormTypes——归一化类型

在这里插入图片描述

normalize

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

convertTo

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

imshow

在这里插入图片描述
在这里插入图片描述

19 OpenCV4 C++ 快速入门

Excerpt

19 图像放缩与插值


19 图像放缩与插值

opencv知识点:

  • 图像放缩 - resize
  • 插值算法 - Interpolation

本课所解决的问题:

  • 如何对图像放缩?
  • 什么是插值算法?

1.图像放缩

opencv当中,如果我们想对一个图像放缩,我们要用到这样一个API

  • resize

介绍如下

resize
重设图像宽长
共6个参数
第1个参数 输入
第2个参数 输出

第3个参数 输出图像的size
第4个参数 fx 沿水平的比例因子
第5个参数 fy 沿垂直的比例因子
      
可以使用size做放缩插值,也可以使用fx,fy卷积做放缩插值

第6个参数 插值方法(查阅文档可知,下文会进行简单介绍)

resize的应用场景有很多,其中最常见的就是:

在做一些神经网络训练,深度网络训练,卷积网络训练等的时候,为了方便处理,会把图像resize到指定大小

2.插值算法

关于插值算法,这里简单介绍一下

首先,图像放缩的时候为什么要用插值算法呢?

这是因为,图像放缩时,像素点的位置会发生变化
要想得到放缩后图像像素点的位置,就要经过某种算法计算得来
这种计算的算法就是插值算法。

插值算法有很多相关的应用场景,比如:

  • 几何变换
  • 透视变换
  • 插值计算新像素
  • resize

而常见的插值算法有4种,其中前两种比较快,后两种比较慢

  • INTER_NEAREST = 0 ——最近邻插值
  • INTER_LINEAR = 1 ——线性插值
  • INTER_CUBIC = 2 ——立方插值
  • INTER_LANCZOS4 = 4 ——Lanczos插值

opencv4支持的插值算法如下

在这里插入图片描述

3.对一张图像进行放缩

接下来我们通过resize对一张图像进行缩小,放大

//函数定义
void resize_demo(Mat& image);

//函数实现
void QuickDemo::resize_demo(Mat& image) {

Mat zoomin, zoomout;//缩小,放大

int w = image.cols;
int h = image.rows;

imshow("原图", image);

//resize(image, zoomin, Size(w / 2, h / 2), 0, 0, INTER_LINEAR);
//imshow("缩小", zoomin);

resize(image, zoomout, Size(w * 2, h * 2), 0, 0, INTER_LINEAR);
imshow("放大", zoomout);

}

缩小
在这里插入图片描述
放大
在这里插入图片描述

4.插值算法 - 进阶

笔者目前层次,暂时用不到进阶的插值插值算法

关于插值的更深学习,具体的可以查课贾志刚老师的博客

本课所用API查阅

1.resize
在这里插入图片描述
在这里插入图片描述
2.InterpolationFlags
在这里插入图片描述
在这里插入图片描述
3.InterpolationMasks

在这里插入图片描述

20 OpenCV4 C++ 快速入门

Excerpt

20 图像翻转


20 图像翻转

opencv知识点:

  • 图像翻转 - flip

本课所解决的问题:

  • 如何对图像进行翻转?

1.图像翻转

opencv中,如果我们想对一个图像进行翻转,要用到这样一个API

  • flip

介绍如下

flip
图像翻转
共3个参数
第1个参数 输入
第2个参数 输出
第3个参数 翻转方式
0表示上下翻转
1表示左右翻转
    -1表示上下,左右同时翻转

2.对一张图像进行翻转

接下来,我们进行图像翻转演示

//函数定义
void flip_demo(Mat& image);

//函数实现
void QuickDemo::flip_demo(Mat& image) {

Mat dst;

//flip(image, dst, 0);//上下翻转
//flip(image, dst, 1);//左右翻转
flip(image, dst, -1);//上下,左右同时翻转

//imshow("原图", image);
imshow("图像翻转", dst);
}

从左至右,依次为:

  • 原图
  • 上下翻转
  • 左右翻转
  • 上下左右同时翻转
    在这里插入图片描述

本课所用API查阅

1.flip
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

21 OpenCV4 C++ 快速入门

Excerpt

21 图像旋转


21 图像旋转

opencv知识点:

  • 仿射变换 - warpAffine
  • 计算二维旋转的仿射矩阵 - getRotationMatrix2D

本课所解决的问题:

  • 如何理解图像几何变换和图像变换?
  • 图像几何变换都有什么变换?
  • 如何理解仿射变换和透视变换?
  • 如何实现图像的旋转?

提示在前:

笔者为了理解图像旋转的warpAffine,引申了很多其他的概念

1.图像几何变换和图像变换

为了更好的理解图像翻转,图像旋转等,我们首先介绍一下变换相关的概念

图像的变换,从严格意义上来说分为两种

  • 几何变换
  • 图像变换

简述

图像几何变换:

改变图像的大小或形状。
比如图像的平移、旋转、放大、缩小等,这些方法在图像配准中使用较多。

图像变换:

通过数学映射的方法,将空域的图像信息转换到频域、时频域等空间上进行分析。
比如傅里叶变换、小波变换等。

区别

文章引用:简述图像几何变换和图像变换的区别

图像几何变换和图像变换的区别为:性质不同,包括不同,原始图像不同。

(1)性质

  • 图像几何变换是从具有几何结构之集合至其自身或其他此类集合的一种对射。
  • 图像变换将原定义在图像空间的图像以某种形式转换到另外的空间,利用空间的特有性质方便地进行一定的加工,最后再转换回图像空间以得到所需的效果。

(2)包括

  • 图像几何变换包括翻折变换、平移变换、旋转变换等。
  • 图像变换包括傅里叶变换、沃尔什-阿达玛变换等。

(3)原始图像

  • 图像几何变换的原始图像为平面域图像。
  • 图像变换的原始图像为空间域图像。

2.图像几何变换

文章引用:图像的仿射变换 - 程序员阿德的文章 - 知乎

而图像的几何变换,又可以分为三类:

  • 刚性变换
  • 仿射变换
  • 透视变换

三类变换

刚性变换:

只有物体的位置(平移变换)和朝向(旋转变换)发生改变,而形状不变,得到的变换称为刚性变换。

仿射变换:

仿射变换是从一个二维坐标系变换到另一个二维坐标系,属于线性变换。
通过已知3对坐标点可以求得变换矩阵。

透视变换:

透视变换是从一个二维坐标系变换到一个三维坐标系,属于非线性变换。
通过已知4对坐标点可以求得变换矩阵。

基本变换、仿射变换和透视变换

图像的几何变换包含很多变换,其中有一些基本变换,具体如下

  • 平移(Translation)
  • 缩放(Scale)
  • 旋转(Rotation)
  • 翻转(Flip)
  • 错切(Shear)

仿射变换透视变换就是对这些基本变换进行组合实现的

也就是说,仿射变换透视变换包含所有的基本变换,同时也作为基本变换的某种组合

opencv中,针对它们已经封装好了对应的API,分别为

  • 仿射变换 - warpAffine
  • 透视变换 - warpPerspective

3.图像旋转

说回图像旋转

从上文我们知道了一些关键点

  • 图像旋转就是图像几何变换中,基本变换的一种
  • 仿射变换和透视变换包含所有的基本变换

所以我们可以通过仿射变换API——warpAffine实现图像旋转

在opencv中,如果我们想对一个图像进行旋转,要用到两个API

  • warpAffine
  • getRotationMatrix2D

这里分别对它们进行解释

warpAffine

warpAffine
仿射变换
共7个参数
第1个参数 输入
第2个参数 输出
第3个参数 输入的变换矩阵(2行3列)
第4个参数 输出图像的size
第5个参数 插值方法
第6个参数 边界模式(暂且不学习。深入探讨的话,还会涉及很多的东西)
第7个参数 边界颜色(暂且不学习。深入探讨的话,还会涉及很多的东西)

getRotationMatrix2D

getRotationMatrix2D
计算二维旋转的仿射矩阵
共3个参数
第1个参数 图像的旋转中心
第2个参数 旋转角度
(规定坐标原点左上角,正值表示逆时针旋转)
第3个参数 各向同性比例因子(对图像本身大小的放缩)

变换矩阵

介绍完毕后,我们知道图像旋转的关键就是那个变换矩阵
变换矩阵可以由getRotationMatrix2D得到

现在我们来研究一下得到的变换矩阵

由于本变换矩阵是由getRotationMatrix2D得到,是专门对标仿射变换API的
在opencv中规定其为2行3列的矩阵。

在这里插入图片描述
当旋转的角度为Θ,且旋转中心为x,y时,即这个矩阵的通式为
在这里插入图片描述

当旋转中心为左上角时,这时候的变换矩阵为
在这里插入图片描述

像素点位置

最后当我们调用warpAffinie时,经过如下公式的计算,就能得到变换后的像素点位置
我们就能实现图像旋转
在这里插入图片描述

4.对一张图像进行旋转

接下来,我们进行对一张图像进行旋转的演示

第一版 - 原图像大小

//函数定义
void rotate_demo(Mat& image);

//函数实现
void QuickDemo::rotate_demo(Mat& image) {

Mat dst, M;//M为变换矩阵

int w = image.cols;
int h = image.rows;

M = getRotationMatrix2D(Point2f(w / 2, h / 2), 45, 1.0);

warpAffine(image, dst, M, image.size(), INTER_LINEAR, 0, Scalar(0, 200, 0));

imshow("旋转演示", dst);

}

在这里插入图片描述

第二版 - 新图像大小

第一版中,旋转后的图像并没有完全显示出来,而是为原图像的大小

如果我们想让图像全部显示出来,该怎么处理呢?

处理方法如下:
在这里插入图片描述
通过如上图,我们可以计算新图像的宽度,高度,旋转中心的通式

  • 新宽度nw = w_cosΘ + h_sinΘ
  • 新宽度nh = w_sinΘ + h_cosΘ
  • 新旋转中心x +=( nw/2 - w/2)
  • 新旋转中心y +=( nh/2 - h/2)
void QuickDemo::rotate_demo(Mat& image) {

Mat dst, M;//M为变换矩阵

int w = image.cols;
int h = image.rows;
M = getRotationMatrix2D(Point2f(w / 2, h / 2), 45, 1.0);

//C++的abs则可以自然支持对整数和浮点数两个版本(实际上还能够支持复数)
double cos = abs(M.at<double>(0, 0));
double sin = abs(M.at<double>(0, 1));

int nw = w * cos + h * sin;
int nh = w * sin + h * cos;

//新图像的旋转中心
M.at<double>(0, 2) += (nw / 2 - w / 2);
M.at<double>(1, 2) += (nh / 2 - h / 2);

warpAffine(image, dst, M, Size(nw,nh),INTER_LINEAR,0,Scalar(0,200,0));

imshow("旋转演示", dst);

}

在这里插入图片描述

本课所用API查阅

1.warpAffine
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
2.getRotationMatrix2D
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

22 OpenCV4 C++ 快速入门

Excerpt

22 视频文件摄像头使用


22 视频文件摄像头使用

opencv知识点:

  • VIdeoCapture类
  • 读取视频/相机 - 三种方式
  • 读取视频帧 - 两种方式

本文所解决的问题:

  • 如何读取视频/相机?
  • 如何读取视频帧?

本课所解决的问题:

1.VideoCapture类以及视频读操作

引用文章:opencv学习—VideoCapture 类基础知识

opencv中,
关于视频的读操作是通过VideoCapture类来完成的;
关于视频的写操作是通过VideoWriter类来实现的。

读取视频/相机

当我们要读取一个视频文件,或者相机时,一般有3种方式

  • 从文件中读取视频

视频捕获对象创建以后,OpenCV将会打开文件并做好准备读取它。
如果打开成功,我们将可以开始读取视频的帧,并且cv::VideoCapture的成员函数isOpened()将会返回true。
(建议在打开视频或摄像头时都使用该成员函数判断是否打开成功)

VideoCapture capture(const string& filename);  // 从视频文件读取 
VideoCapture capture("D:/WorkSpace/Opencv/videos/mouse.mp4");  // 从视频文件读取 
  • 从摄像机中读取视频。

这种情况下,我们会给出一个标识符,用于表示我们想要访问的摄像机,及其与操作系统的握手方式。
对于摄像机而言,这个标志符就是一个标志数字

  • 如果只有1个摄像机,那么就是0
  • 如果系统中有多个摄像机,那么只要将其向上增加即可。
VideoCapture capture(0);  //从摄像机读取
  • 先创建一个视频捕获对象,然后通过成员函数open来设定打开的信息。
VideoCapture capture;
VideoCapture.open("D:/WorkSpace/Opencv/videos/mouse.mp4");  

读取视频捕获对象的视频帧

将视频帧读取到Mat矩阵中,一般有两种方式:

  • 一种是read()操作
  • 另一种是 “>>”操作。
Mat frame;  
cap.read(frame); //读取方式一  
cap >> frame; //读取方式二  相当于输入流,cap流入frame中

2.对摄像头和视频进行读取

本文采取的方式为,

  • 先利用VideoCapture创建视频捕获对象
  • 再使用其方法read获取帧

现在我们对read方法进行解释

read
抓取,解码并返回下一个视频帧
共1个参数
第1个参数 视频帧的输出图像

读取摄像头

这里简要说一下摄像头,本文只实现了对1个摄像头的读取

如果果只有1个摄像机,那么就是0,如果系统中有多个摄像机,那么只要将其向上增加即可

不过我们要注意一点:

虽然摄像机将在 VideoCapture 析构函数中自动取消初始化,但我们最好在末尾写上release,
相机是关键资源,要确保被释放
当然,视频文件更要在末尾写上release

我们先来试一下摄像头

//函数定义
void video_demo(Mat& image);

//函数实现
void QuickDemo::video_demo(Mat& image) {

VideoCapture cap(0);//0表示,对摄像头进行捕获;
Mat frame;
while (true) {

cap.read(frame);//frame为输出,read是将捕获到的视频一帧一帧的传入frame

//对视频读取时,同图像一样会有判空操作
if (frame.empty()) {
break;
}
//因为摄像头是镜像的,所以我们要左右翻转一下
flip(frame, frame, 1);

/*
read获得的视频帧也是图像,
如果我们想进行一些操作,之前的方法也都适用,如色彩空间转换等
*/
imshow("frame", frame);

int c = waitKey(1);//一般是1,相当于每秒1000张图片
if (c == 27) {
break;
}
}
cap.release();
}

在这里插入图片描述

读取视频文件

我们再试一下视频文件

void QuickDemo::video_demo(Mat& image) {

VideoCapture cap("D:/WorkSpace/Opencv/videos/mouse.mp4");//读取视频文件
Mat frame;
while (true) {

cap.read(frame);//frame为输出,read是将捕获到的视频一帧一帧的传入frame

//对视频读取时,同图像一样会有判空操作
if (frame.empty()) {
break;
}

/*
read获得的视频帧也是图像,
如果我们想进行一些操作,之前的方法也都适用,如色彩空间转换等
*/

imshow("frame", frame);

int c = waitKey(1);
if (c == 27) {
break;
}
}
}

在这里插入图片描述

3.对读取的视频帧进行操作

这里演示一下,调用我们之前写的colorSpace_Demo();

void QuickDemo::video_demo(Mat& image) {

VideoCapture cap("D:/WorkSpace/Opencv/videos/mouse.mp4");//读取视频文件
Mat frame;
while (true) {

cap.read(frame);//frame为输出,read是将捕获到的视频一帧一帧的传入frame

//对视频读取时,同图像一样会有判空操作
if (frame.empty()) {
break;
}

colorSpace_Demo(frame);

imshow("frame", frame);

int c = waitKey(1);
if (c == 27) {
break;
}
}
}

在这里插入图片描述

本课所用API查阅

1.VideoCapture——构造和析构
在这里插入图片描述
在这里插入图片描述

2.VideoCapture——其他方法
在这里插入图片描述

3.VideoCapture——详细说明

从视频文件、图像序列或相机中捕获视频的类。

该类提供 C++ API 用于从摄像机捕获视频或读取视频文件和图像序列。

以下是如何使用该类:

#include <opencv2/core.hpp>
#include <opencv2/videoio.hpp>
#include <opencv2/highgui.hpp>
#include <iostream>
#include <stdio.h>
using namespace cv;
using namespace std;
int main(int, char**)
{
    Mat frame;
    
    //--- 初始化视频捕捉
    VideoCapture cap;
    
     // 使用默认 API 打开默认相机
    // cap.open(0);
    
    // 或提前使用:选择任何 API 后端
    int deviceID = 0;             // 0 = 打开默认摄像头
    int apiID = cv::CAP_ANY;     // 0 = 自动检测默认 API
     
     // 使用选定的 API 打开选定的相机
    cap.open(deviceID, apiID);
    
   // 检查我们是否成功
    if (!cap.isOpened()) {
        cerr << "ERROR! Unable to open camera\n";
        return -1;
    }
    
    //--- 抓取和写入循环
    cout << "Start grabbing" << endl<< "Press any key to terminate" << endl;
    for (;;)
    {
        // 等待来自相机的新帧并将其存储到“帧”中
        cap.read(frame);
        
        // 检查我们是否成功
        if (frame.empty()) {
            cerr << "ERROR! blank frame grabbed\n";
            break;
        }
      // 实时显示并等待一个超时时间足够长的键来显示图像
        imshow("Live", frame);
        if (waitKey(5) >= 0)
            break;
    }
  // 摄像机将在 VideoCapture 析构函数中自动取消初始化
    return 0;
}

3.VideoCapture::read
在这里插入图片描述
在这里插入图片描述

23 OpenCV4 C++ 快速入门

Excerpt

23 视频处理与保存


23 视频处理与保存

opencv知识点:

  • VideoWriter类
  • 获取视频属性 - VideoWriter::get
  • 视频保存 - VideoWriter::write

本课所解决的问题:

  • 分辨率都有哪几个等级?
  • 如何获取视频的属性?
  • 如何保存视频?

1.视频属性

视频有很多的属性,有时长,分辨率,帧宽度,帧高度,帧速率等

视频属性中,由于国内互联网视频网站的定义,我们对分辨率的区分有些误区。
所以这里重新介绍一下视频的分辨率,至于其他属性,一般不会有什么误区。

分辨率

引用文章:视频画质标清、高清、超清,各是多大分辨率啊?

通常国际标准,我们把视频分辨率分为三类

  • SD—— 标清
  • HD——高清
  • UD——超高清

简要介绍如下

  • 标清(Standard Definition)

是物理分辨率在720p以下的一种视频格式。

  • 高清(High Definition)

将“高清”定义为720p、1080i与1080p三种标准形式
而1080P又有另外一种称呼—全高清(FullHigh Definition)。
关于高清标准,国际上公认的有两条:

  • 视频垂直分辨率超过720p或1080i
  • 视频宽纵比为16:9。
  • 超高清(Ultra High-Definition)

来自国际电信联盟 (International Telecommunication Union)最新批准的信息显示,
“4K分辨率 (3840×2160 像素)” 的正式名称被定为“超高清 Ultra HD(UltraHigh-Definition)”。
同时,这个名称也适用于“8K分辨率 (7680×4320像素)”。

CEA要求,所有的消费级显示器和电视机必须满足以下几个条件之后,才能 贴上“超高清 Ultra HD” 的标签:

  • 首先屏幕最小的像素必须达到800 万有效像素(3840×2160)
  • 在不改变屏幕分辨率的情况下,至少有一路传输端可以传输 4K视频,4K内容的显示必须原生,
    不可上变频,纵横比至少为16:9。

Ps:

  • 720p格式,分辨率为1280×720p/60Hz,行频为45kHz 。
  • 4K分辨率是1080p的4倍 3840×2160 =1920×2×1080×2 。
  • 8K分辨率是4K的4倍 7680×4320 = 3840×2×2160×2

虽然介绍了分辨率,但本文主要演示一些帧相关的属性,具体如下

  • 帧宽度——frame_width
  • 帧高度——frame_height
  • 总帧数——frame_count
  • 帧速率—— FPS(Frames Per Second)

2.获取视频属性

opencv中,我们如果要获取视频的属性,就要用到VideoCapture类的一个方法

  • get

具体介绍如下

get
返回指定VideoCapture属性
共1个参数
第1个参数 指定的属性

VideoCapture属性,有很多很多,具体可查阅文档

本文只用到4种属性

  • CAP_PROP_FRAME_WIDTH - 视频流中帧的宽度
  • CAP_PROP_FRAME_HEIGHT - 视频流中帧的高度
  • CAP_PROP_FRAME_COUNT - 视频文件中的帧数
  • CAP_PROP_FPS - 帧率

演示如下

void QuickDemo::video_demo(Mat& image) {

VideoCapture cap("D:/WorkSpace/Opencv/videos/mouse.mp4");//读取视频文件

int frame_width = cap.get(CAP_PROP_FRAME_WIDTH);
int frame_height = cap.get(CAP_PROP_FRAME_HEIGHT);
int frame_count = cap.get(CAP_PROP_FRAME_COUNT);
double fps = cap.get(CAP_PROP_FPS);

std::cout << "frame_width:" << frame_width << std::endl;
std::cout << "frame_height:" << frame_height << std::endl;
std::cout << "frame_count:" << frame_count << std::endl;
std::cout << "fps:" << fps << std::endl;

Mat frame;
while (true) {

cap.read(frame);//frame为输出,read是将捕获到的视频一帧一帧的传入frame

//对视频读取时,同图像一样会有判空操作
if (frame.empty()) {
break;
}

imshow("frame", frame);

int c = waitKey(1);
if (c == 27) {
break;
}
}
cap.release();
}

在这里插入图片描述

这里有一点要提

既然有get,那就有对应的set。但是我们使用set会涉及很多的问题

比如上一课中摄像头

当我们使用set对摄像头传来的视频操作时
如果超出了摄像头硬件的范围,即使设置的再好也不会显示

3.视频保存

在opencv中,关于视频的写操作是通过VideoWriter类来实现的。

在进行图像保存的时候,我们一般要两步

  • 通过 VideoWriter创建一个视频写入对象。(创建时,我们要传入各种写入要求)
  • 再通过writer写入。

对它们的介绍如下

VideoWriter

VideoWriter (const String &filename, int fourcc, double fps, Size frameSize, bool isColor=true)
视频写入对象
共5个参数
第1个参数 视频文件路径
第2个参数 视频编码方式(我们可以通过VideoCapture::get(CAP_PROP_FOURCC)获得)
第3个参数 fps
第4个参数 size
第5个参数 是否为彩色

write

write
写入下一个视频帧
共1个参数
第1个参数 输入的视频帧

这里提一句

同VideoCapture类对象一样,VideoWirter对象也要在末尾写上release
演示如下

void QuickDemo::video_demo(Mat& image) {

VideoCapture cap("D:/WorkSpace/Opencv/videos/mouse.mp4");//读取视频文件

int frame_width = cap.get(CAP_PROP_FRAME_WIDTH);
int frame_height = cap.get(CAP_PROP_FRAME_HEIGHT);
int frame_count = cap.get(CAP_PROP_FRAME_COUNT);
double fps = cap.get(CAP_PROP_FPS);

std::cout << "frame_width:" << frame_width << std::endl;
std::cout << "frame_height:" << frame_height << std::endl;
std::cout << "frame_count:" << frame_count << std::endl;
std::cout << "fps:" << fps << std::endl;

VideoWriter wri("D:/WorkSpace/Opencv/videos/wri.mp4", cap.get(CAP_PROP_FOURCC), fps, Size(frame_width, frame_height), true);

Mat frame;
while (true) {

cap.read(frame);//frame为输出,read是将捕获到的视频一帧一帧的传入frame

//对视频读取时,同图像一样会有判空操作
if (frame.empty()) {
break;
}

wri.write(frame);

imshow("frame", frame);

int c = waitKey(1);
if (c == 27) {
break;
}
}
cap.release();
wri.release();

}

在这里插入图片描述

4.视频处理注意事项

最后有两点要提

  • opencv,只专注于视频画面的处理,没有声音,不处理音频,如果想处理音频,要涉及其他领域
  • opencv保存视频是有一定限制的,理论上说单个视频不要超过2G

本课所用API查阅

1.VIdeoCapture::get
在这里插入图片描述
在这里插入图片描述
2.VideoCaptureProperties
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
3.VideoWriter——构造/析构
在这里插入图片描述
4.VideoWriter——其他方法
在这里插入图片描述
6.VideoWriter()——构造方法之一
在这里插入图片描述
在这里插入图片描述

5.VideoWriter::write
在这里插入图片描述
在这里插入图片描述

24 OpenCV4 C++ 快速入门

Excerpt

24 图像直方图


24 图像直方图

opencv知识点:

  • 计算直方图数据 - calcHist
  • 四舍五入浮点数 - cvRound

本课所解决的问题:

  • 什么是图像直方图?
  • 如何绘制彩色图像的一维直方图?

1.图像直方图

图像有很多基础概念,在我们学习的过程中因为一些原因无法涉及,但这并不代表它们不重要

今天,我们就来介绍一个概念——图像直方图

图像直方图,是图像处理中很重要的一个基础概念,
有很多的算法,比如传统的特征工程,跟它都有千丝万缕的关系

图像直方图是图像像素值的统计学特征。
由于其计算代价较小,且具有图像平移、旋转、缩放不变性等众多优点,广泛地应用于图像处理的各个领域,特别是灰度图像的阈值分割、基于颜色的图像检索以及图像分类、反响投影跟踪。

图像直方图常见的分为:

  • 灰度直方图
  • 颜色直方图

示例如下
在这里插入图片描述

在这里插入图片描述

图像直方图(Image Histogram)是用以表示数字图像中亮度分布的直方图,标绘了图像中每个亮度值的像素数。这种直方图中

  • 横坐标的左侧为纯黑、较暗的区域,
  • 右侧为较亮、纯白的区域。

因此一张较暗图片的直方图中的数据多集中于左侧和中间部分,而整体明亮、只有少量阴影的图像则相反。
CV 领域常借助图像直方图来实现图像的二值化。

图像直方图中,也有直方图的两个概念

  • bin——bin是的X轴上每一组的长度,比如组长为1,就是bin中包含1个像素值。
    bin由bins和取值范围决定
  • bins——binsX轴上的组数,对于像素值取值在0~255之间,如果bins = 256,则bin = 1
    此外对于该取值范围来说,bins还可以有16、32、48、128等。
    但一定要满足,256除以bin的大小应该是整数

简单来说,图像直方图就是对图像像素数据进行统计的一种方法
一幅灰度图像:图像直方图将0-255不同值分布在坐标系的X轴上,对应像素值的数量分布在Y轴上。
当bins=256时,此时(100,50),就表示值是100的像素有50个。

图像直方图可以唯一标识一张图像吗?

通常直方图的维数要低于原始数据,所以它的信息有缺,图像直方图并不能唯一表示一张图像。

那什么可以作为图像的"DNA",唯一标识一张图像呢?

特征工程就可以,这种真正的特征描述值会把图像变成一堆向量。
有很多种的法可以做特征工程,比如传统图像处理的特征提取

本文涉及图像直方图的知识只是冰山一角,进阶知识的学习如下

2.绘制彩色图像的一维直方图

opencv中,如果我们想绘制彩色图像的一维直方图,要用到两个API

  • calcHist
  • cvRound

介绍如下
calcHist

calcHist
计算一维数组的直方图(输入图像可以有多通道)
共10个参数
第1个参数 图像数组
第2个参数 输入图像数量
第3个参数 通道数组
第4个参数 可选mask

第5个参数 输出直方图数据(值与对应频次)的n维数组
第6个参数 直方图维数

当通道为1个时,我们选择维度为1维,此时直方图数据就为一维数组
当维度为2个时,我们选择维度为2维,此时直方图数据就为二维数组
………………
也就是说,n张图像 每张图像m个通道 也可以计算出相应的直方图数据

但对于绘制来说,一般都只绘制到2维,3维及以上就很复杂了

第7个参数 histSize( bins数组,x轴长度)
第8个参数 ranges(取值范围数组)

//以下参数暂时用不到
第9个参数 指示直方图bin间隔是否一致
默认为true,即等间隔取值
如果为false,则range不能写{0,255}这种,就要写{1,1,……,1}这种

第10个参数 累计标志(默认为false)
    当多张图像的时候,
    如果为true,则绘制直每张方图的时候,不会从头清空
    会在前者直方图的基础上继续

按照文档来说,我们可以计算多个图像,每个图像有多个通道的直方图数据
但这就涉及到了二维及二维以上直方图数据数组的计算,下一课中会介绍二维直方图

cvRound

cvRound
将浮点数四舍五入到最近的整数
共1个参数
第1个参数 要处理的浮点数

本课中计算的直方图维数为1维,采取方式为

  • 先把bgr三通道分离
  • 然后进行每个通道的直方图数据计算,得到一维数组
  • 再然后对直方图一维数组进行归一化处理
  • 最后利用直方图一维数组绘制直方图

为什么这里要归一化呢?

因为一张图中,有些值的频次会过大,不方便绘制直方图

为什么还要要利用得到的数据数组绘制,而不是直接显示它?

因为计算的到直方图数据数组,是一个大小为256的一维数组,它的每个值对应一个频次
当我们用cout输出直方图数据,会得到256 * 1的列矩阵
直接显示这个数组的话就是如下。

在这里插入图片描述

从下图可以看出,这还远远不够,我还们还要进一步的去绘制直方图
在这里插入图片描述

3.绘制直方图演示

//函数定义
void showHistogram_demo(Mat& image);

//函数实现
void QuickDemo::showHistogram_demo(Mat& image) {

//三通道分离
std::vector<Mat> bgr;
split(image, bgr);

//定义参数变量
const int channels[1] = { 0 };
Mat b_hist, g_hist, r_hist;
const int bins[1] = { 256 };

float xrange[2] = { 0,255 };
const float* ranges[1] = { xrange };

//计算Blue,Green,Red三通道各自的直方图
calcHist(&bgr[0], 1, channels, Mat(), b_hist, 1, bins, ranges);
calcHist(&bgr[1], 1, channels, Mat(), g_hist, 1, bins, ranges);
calcHist(&bgr[2], 1, channels, Mat(), r_hist, 1, bins, ranges);

//imshow("00", b_hist);
//std::cout << b_hist;

//显示直方图
int hist_w = 512;
int hist_h = 400;
int bin_w = cvRound((double)hist_w / bins[0]);
Mat histImage = Mat::zeros(Size(hist_w, hist_h), CV_8UC3);

//归一化直方图数据为指定范围
normalize(b_hist, b_hist, 0, hist_h, NORM_MINMAX, -1, Mat());
normalize(g_hist, g_hist, 0, hist_h, NORM_MINMAX, -1, Mat());
normalize(r_hist, r_hist, 0, hist_h, NORM_MINMAX, -1, Mat());

//绘制直方图曲线
for (int i = 0; i < 256; i++) {

Point p01(bin_w * i, hist_h - cvRound(b_hist.at<float>(i)));
/*
第一个点横向坐标:bin_w*i: 即 512/256 再 * i
第二个点纵向坐标:直方图纵高 - 根据直方图纵高归一化后的频次,即为纵向坐标

当频次很低时,减的少,就靠下,反之靠上
*/
//线段的下一个点
Point p02(bin_w * i + 1, hist_h - cvRound(b_hist.at<float>(i + 1)));

Point p11(bin_w * i, hist_h - cvRound(g_hist.at<float>(i)));
Point p12(bin_w * i + 1, hist_h - cvRound(g_hist.at<float>(i + 1)));

Point p21(bin_w * i, hist_h - cvRound(r_hist.at<float>(i)));
Point p22(bin_w * i + 1, hist_h - cvRound(r_hist.at<float>(i + 1)));


line(histImage, p01, p02, Scalar(255, 0, 0), 1, 8, 0);
line(histImage, p11, p12, Scalar(0, 255, 0), 1, 8, 0);
line(histImage, p21, p22, Scalar(0, 0, 255), 1, 8, 0);

}

imshow("直方图", histImage);

}

在这里插入图片描述

本课所用API查阅

calcHist

在这里插入图片描述
在这里插入图片描述

#include <opencv2/imgproc.hpp>
#include <opencv2/highgui.hpp>
using namespace cv;
int main( int argc, char** argv )
{
    Mat src, hsv;
    if( argc != 2 || !(src=imread(argv[1], 1)).data )
        return -1;
    cvtColor(src, hsv, COLOR_BGR2HSV);
    
     // 将色调量化为 30 级
    // 饱和度为 32 级
    int hbins = 30, sbins = 32;
    int histSize[] = {hbins, sbins};
    
    // 色调从 0 到 179 变化,见 cvtColor
    float hranges[] = { 0, 180 };
    
    // 饱和度从 0(黑-灰-白)到
    // 255(纯光谱颜色)
    float sranges[] = { 0, 256 };
    const float* ranges[] = { hranges, sranges };
    
    MatND hist;
    // 我们从第 0 和第 1 通道计算直方图
    int channels[] = {0, 1};
    calcHist( &hsv, 1, channels, Mat(), // 不使用掩码
             hist, 2, histSize, ranges,
             true, // 直方图是统一的
             false );
    double maxVal=0;
    minMaxLoc(hist, 0, &maxVal, 0, 0);
    int scale = 10;
    Mat histImg = Mat::zeros(sbins*scale, hbins*10, CV_8UC3);
    for( int h = 0; h < hbins; h++ )
        for( int s = 0; s < sbins; s++ )
        {
            float binVal = hist.at<float>(h, s);
            int intensity = cvRound(binVal*255/maxVal);
            rectangle( histImg, Point(h*scale, s*scale),
                        Point( (h+1)*scale - 1, (s+1)*scale - 1),
                        Scalar::all(intensity),
                        -1 );
        }
    namedWindow( "Source", 1 );
    imshow( "Source", src );
    namedWindow( "H-S Histogram", 1 );
    imshow( "H-S Histogram", histImg );
    waitKey();
}

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

cvRound

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

25 OpenCV4 C++ 快速入门

Excerpt

25 二维直方图


25 二维直方图

opencv知识点:

  • 计算直方图数据 - calcHist
  • 四舍五入浮点数 - cvRound
  • 寻找最小/最大值 - minMaxLoc

本课所解决的问题:

  • 如何绘制HSV图像的二维直方图?

1.二维直方图

上节课中,我们学习了一维直方图的绘制,那我们该如何绘制二维直方图呢?

关于二维直方图的绘制,我们通常选择HSV模式下的图像

我们回顾HSV的知识,可以发现

  • H(色调)范围是[0,180]
  • S(饱和度)范围是[0,255]
  • V(明度)范围是[0,255]

即HS两个通道就可以表示颜色,非常方便二维直方图的绘制

opencv中,如果我们想绘制二维维直方图,要用到三个API

  • calcHist
  • cvRound
  • minMaxLoc

介绍如下
calcHist

calcHist
计算一维数组的直方图(输入图像可以有多通道)
共10个参数
第1个参数 图像数组
第2个参数 输入图像数量
第3个参数 通道数组
第4个参数 可选mask

第5个参数 输出直方图数据(值与对应频次)的n维数组
第6个参数 直方图维数

当通道为1个时,我们选择维度为1维,此时直方图数据就为一维数组
当维度为2个时,我们选择维度为2维,此时直方图数据就为二维数组
………………
最大支持32维

也就是说,n张图像 每张图像m个通道 可以计算出相应的直方图数据

但对于绘制来说,一般都只绘制到2维,3维及以上就很复杂了

第7个参数 histSize( bins数组,x轴长度)
第8个参数 ranges(取值范围数组)

//以下参数暂时用不到
第9个参数 指示直方图bin间隔是否一致
默认为true,即等间隔取值
如果为false,则range不能写{0,255}这种,就要写{1,1,……,1}这种

第10个参数 累计标志(默认为false)
    当多张图像的时候,
    如果为true,则绘制直每张方图的时候,不会从头清空
    会在前者直方图的基础上继续

cvRound

cvRound
将浮点数四舍五入到最近的整数
共1个参数
第1个参数 要处理的浮点数

minMaxLoc

minMaxLoc
寻找最小/最大值
共5个参数 
第1个参数 输入
第2个参数 输出的最小值
第3个参数 输出的最大值
第4个参数 最小值下标
第5个参数 最大值下标

2.绘制二维直方图

本课中计算的直方图维数为2维,采取方式为

  • 先转换色彩空间为HSV
  • 然后进行每个通道的直方图数据计算,得到二维数组
  • 最后利用直方图二维数组绘制直方图

我们先来输出一下得到的直方图数据

//函数定义
void histogram_2d_demo(Mat& image);

//函数实现
void QuickDemo::histogram_2d_demo(Mat& image) {

// 2D 直方图
Mat hsv, hs_hist;
cvtColor(image, hsv, COLOR_BGR2HSV);

int hbins = 30;
int sbins = 32;
int hist_bins[] = { hbins, sbins };

float h_range[] = { 0, 180 };
float s_range[] = { 0, 256 };
const float* hs_ranges[] = { h_range, s_range };

int hs_channels[] = { 0, 1 };

calcHist(&hsv, 1, hs_channels, Mat(), hs_hist, 2, hist_bins, hs_ranges);

std::cout << hs_hist;
}

可以看见,不同于上一课的256 * 1的列矩阵,这里变成了30 * 32的二维矩阵
在这里插入图片描述

接下来,我们开始绘制二维直方图

//函数实现
void QuickDemo::histogram_2d_demo(Mat& image) {

// 2D 直方图
Mat hsv, hs_hist;
cvtColor(image, hsv, COLOR_BGR2HSV);


int hbins = 30;
int sbins = 32;
int hist_bins[] = { hbins, sbins };

float h_range[] = { 0, 180 };
float s_range[] = { 0, 256 };
const float* hs_ranges[] = { h_range, s_range };

int hs_channels[] = { 0, 1 };

calcHist(&hsv, 1, hs_channels, Mat(), hs_hist, 2, hist_bins, hs_ranges);

std::cout << hs_hist;

double maxVal = 0;//寻找直方图数据中的最大值
minMaxLoc(hs_hist, 0, &maxVal, 0, 0);


int scale = 10;
//行320 列300
Mat hist2d_image = Mat::zeros(sbins * scale, hbins * scale, CV_8UC3);

//h30行,s32列,一行一行的绘制矩形
for (int h = 0; h < hbins; h++) {
for (int s = 0; s < sbins; s++)
{
float binVal = hs_hist.at<float>(h, s);//位于横h,列s处的频次

int intensity = cvRound(binVal * 255 / maxVal);//白色的强度,频次越大,小矩形越接近白色

Point p1(h * scale, s * scale);
/*
矩形左上角的点
*/
Point p2((h + 1) * scale - 1, (s + 1) * scale - 1);
/*
矩形右下角的点
-1只是为了不与其他矩形的左上角重合,不-1差异也不大
*/

rectangle(hist2d_image, p1, p2, Scalar::all(intensity), -1);
}
}

//灰色的图像不容看出差异,这里我们转换色彩风格
applyColorMap(hist2d_image, hist2d_image,COLORMAP_DEEPGREEN);

imshow("H-S Histogram", hist2d_image);

}

在这里插入图片描述

本课所用API查阅

calcHist

在这里插入图片描述
在这里插入图片描述

#include <opencv2/imgproc.hpp>
#include <opencv2/highgui.hpp>
using namespace cv;
int main( int argc, char** argv )
{
    Mat src, hsv;
    if( argc != 2 || !(src=imread(argv[1], 1)).data )
        return -1;
    cvtColor(src, hsv, COLOR_BGR2HSV);
    
     // 将色调量化为 30 级
    // 饱和度为 32 级
    int hbins = 30, sbins = 32;
    int histSize[] = {hbins, sbins};
    
    // 色调从 0 到 179 变化,见 cvtColor
    float hranges[] = { 0, 180 };
    
    // 饱和度从 0(黑-灰-白)到
    // 255(纯光谱颜色)
    float sranges[] = { 0, 256 };
    const float* ranges[] = { hranges, sranges };
    
    MatND hist;
    // 我们从第 0 和第 1 通道计算直方图
    int channels[] = {0, 1};
    calcHist( &hsv, 1, channels, Mat(), // 不使用掩码
             hist, 2, histSize, ranges,
             true, // 直方图是统一的
             false );
    double maxVal=0;
    minMaxLoc(hist, 0, &maxVal, 0, 0);
    int scale = 10;
    Mat histImg = Mat::zeros(sbins*scale, hbins*10, CV_8UC3);
    for( int h = 0; h < hbins; h++ )
        for( int s = 0; s < sbins; s++ )
        {
            float binVal = hist.at<float>(h, s);
            int intensity = cvRound(binVal*255/maxVal);
            rectangle( histImg, Point(h*scale, s*scale),
                        Point( (h+1)*scale - 1, (s+1)*scale - 1),
                        Scalar::all(intensity),
                        -1 );
        }
    namedWindow( "Source", 1 );
    imshow( "Source", src );
    namedWindow( "H-S Histogram", 1 );
    imshow( "H-S Histogram", histImg );
    waitKey();
}

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

cvRound

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

minMaxLoc

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

26 OpenCV4 C++ 快速入门

Excerpt

26 直方图均衡化


26 直方图均衡化

opencv知识点:

  • 均衡灰度图像的直方图 - equalizeHist

本课所解决的问题:

  • 什么是图像直方图均衡化?
  • 如何均衡化灰度图像的直方图?
  • 如何均衡化彩色图像的直方图?

1.图像直方图均衡化

在opemcv中,实现图像直方图均衡化并不难,但如何理解却要花点时间。
所以在本课的开始,我们来先来了解一下图像直方图均衡化相关的知识

直方图均衡化引入

作者:云时之间
链接:数字图像处理:直方图均衡化

话说回直方图,我们引入直方图,很大程度上是为了让我们可以根据直方图的形态,判断图像的质量,比如根据下图所示,会很快发现一张图片是过亮还是过暗
在这里插入图片描述
如果直方图偏暗,偏亮或者亮度过于集中,我们就要对直方图进行修整

在数字图像处理中关于直方图的修整,有两种方法:

  • 直方图均衡化
  • 直方图规定化

我们本课涉及的直方图均衡化,就是比较常用的那一种

直方图均衡化是将原图像通过某种变换,得到一幅灰度直方图为均匀分布的新图像的方法。
其基本思想是

  • 对在图像中像素个数多的灰度级进行展宽
  • 对像素个数少的灰度级进行缩减。

从而达到清晰图像的目的。

在这里插入图片描述

直方图均衡化原理

现在我们举一个例子说明它的原理

假设有一幅图像64*64大小,共有4096个像素,8个灰度级各灰度级概率分布见下表 ,试将其直方图均匀化。

k灰度级rk像素数nk概率Pk(rk)
007900.19
11/710230.25
22/78500.21
33/76560.16
44/73290.08
55/72450.06
66/71220.03
77/7810.02

推导如下:

  • 首先计算直方图概率的累加值S(i),直到最后一个灰度级,总和为1
    - 根据公式计算sk

  • 然后根据公式求取像素映射关系.
    在这里插入图片描述
    得到如下的映射关系
    在这里插入图片描述

这样就找到了原图像和均衡化图像灰度的对应关系。
如果再对原图进行操作,将每个像素映射成新的像素,就完成了图像均衡化。

直方图均衡化用途

图像直方图均衡化可以用于图像增强、对输入图像进行直方图均衡化处理,提升后续对象检测的准确率等。
它在OpenCV人脸检测的代码演示中已经很常见。
此外对医学影像图像与卫星遥感图像也经常通过直方图均衡化来提升图像质量。

引用文章:

直方图均衡化
数字图像处理:直方图均衡化

2.灰度图像直方图均衡化

在opencv中,如果要实现灰度图像直方图的均衡化,只要用到一个API

  • equalizeHist

具体介绍如下

equalizeHist
均衡灰度图像的直方图
共2个参数
第1个参数 输入
第2个参数 输出

演示如下

//函数定义
void histogram_eq_demo(Mat& image);
//函数实现
void QuickDemo::histogram_eq_demo(Mat& image) {

Mat gray;
cvtColor(image, gray, COLOR_BGR2GRAY);
imshow("灰度图像", gray);

Mat dst;
equalizeHist(gray, dst);
imshow("直方图均衡化演示",dst);

}

在这里插入图片描述

3.彩色图像直方图均衡化

从上文我们可以知道,直方图均衡化就是对亮度的一个调整,使之分布均匀
而在HSV色彩空间中,V通道关于亮度的,所以我们实现彩色图像的均衡化可以从V通道着手。

步骤如下

  • 先把图像色彩空间转为HSV,并把HSV三个通道分离
  • 然后V通道进行均衡化,再把三个通道合并
  • 最后转回BGR色彩空间

这样就实现了彩色图像直方图的均衡化

void QuickDemo::histogram_eq_demo(Mat& image) {

Mat hsv,dst[3],src;
cvtColor(image,hsv, COLOR_BGR2HSV);
split(hsv, dst);

equalizeHist(dst[2], dst[2]);
merge(dst,3,src);

cvtColor(src, src,COLOR_HSV2BGR);

imshow("00",src);

}

在这里插入图片描述

本课所查阅API

equalizeHist

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

merge

在这里插入图片描述
在这里插入图片描述

27 OpenCV4 C++ 快速入门

Excerpt

27 图像卷积操作


27 图像卷积操作

opencv知识点:

  • 均值卷积 - blur
  • 边缘处理方式 - BorderTypes

本课所解决的问题

  • 如何理解卷积?
  • 如何理解图像卷积?
  • 如何实现对图像的均值卷积?

1.图像卷积 - 上篇

引用文章:(非常感谢这两篇博客,受益匪浅)

卷积与图像卷积

卷积

首先我们先说一下卷积

卷积一词最开始出现在信号与线性系统中,其物理意义是描述当信号激励一个线性时不变系统后发生的变化。
在这里插入图片描述
(1)连续时间信号的卷积:
对连续时间信号而言,卷积是一种特殊的积分运算。
它的过程就是一个函数固定不动,另一个函数先以y轴为对称轴反转,然后不断执行相乘,积分,滑动。
在这里插入图片描述
(2.)连续时间信号离散化后的卷积:
其中x(n)和h(n)是参与运算的离散时间信号。
在这个定义中,卷积的过程尤为清晰:
在坐标轴上让x(n)保持不动,先把h(n)反转,然后不断执行二者重合部分相乘求和然后让h(n)滑动的过程。
在这里插入图片描述
离散时间信号可以看作是一串序列,它是一维的。

图像卷积

如果我们把一维序列扩充为二维序列,那会怎样?那就变成了二维卷积
我们知道图像的本质就是灰度值的二维序列,扩充后的二维卷积自然就可以应用在图像上

在计算机视觉领域中,数字图像是一个二维的离散信号。
对数字图像做卷积操作,

  • 其实就是利用卷积核(卷积模板)在图像上滑动,将图像点上的像素灰度值与对应的卷积核上的数值相乘,
  • 然后将所有相乘后的值相加作为卷积核中间像素对应的图像上像素的灰度值,并最终滑动完所有图像的过程。

现在我们举几个例子

  • 图1是一个直观求卷积的示意图,从左到右看,原像素经过卷积由1变成-8。

在这里插入图片描述

  • 图2是另一张图像与另一个卷积核,并得到了不处理边缘的卷积结果在这里插入图片描述
图像卷积的卷积核

图像卷积的概念,我们有了一个简单了解,那我们现在说一下什么是卷积核

  • 卷积核就是一种求和的规则,是一种映射的规则。
    由卷积核规则的不同衍生出了不同的卷积方式,滤波方式,不同的梯度运算方式等。

图像卷积的用途

在图像处理中,我们不会为了卷积而去卷积
因为按照卷积的定义,它只是与卷积核相乘求和的结果。
从上文的描述可以看到,单单卷积确实没什么用,因为图像卷积的用武之处不在于此。

那图像卷积的用武之地在哪?

图像卷积常常用于图像滤波(平滑化),图像梯度,开运算,闭运算,黑帽运算,顶帽运算等形态处理,以及基于梯度运算的边缘提取中。

常用图像卷积

  • 均值卷积(均值滤波/均值模糊)的卷积核如下图
    它也是按照卷积运算的过程相乘求和再滑动,只不过它的核里每个值都是1,在求和之后还除以核的大小来取平均。
    在这里插入图片描述

  • 高斯卷积(高斯滤波/高斯模糊)的卷积核如下图
    它的核是离中心越近值越大,也就是不同位置的权重不同。
    在相乘求和之后会除以核内数值的求和值以保证灰度值不会超出范围
    在这里插入图片描述

滤波/模糊

为什么上面例子的卷积也叫滤波/模糊呢?

首先说一下为什么叫滤波

滤波的本质就是卷积,不同的是要按照一定的特殊规则去卷积。
同时滤波使用的卷积核不是随意的,而是有既定的规矩的,比如:

  • 卷积核应取3x3,5x5这样的具有中心的奇数核
  • 核内数值的分布,要视不同的滤波方式而定。 如上文写到的均值滤波,它的卷积核全部都是1除以核大小。

卷积的一种应用形式就是滤波,当然不同的卷积核有不同的卷积效果,所以卷积还有诸如梯度运算等其他的应用形式,差别就在于卷积核的不同

那滤波什么作用呢?

在数字信号处理中接触的滤波,如高通滤波,是为了滤除高频信号(变化很快的信号)
而在图像滤波中也是如此,不过是过滤图像的高频成分

还是拿均值滤波来说
原图像与均值滤波卷积核卷积,结果就是原图像的像素值乘以卷积核对应位置的值相加,
对于5x5大小的核来说核内的值都是1/25,那这样看很显然,卷积的结果就是把原图像像素值相加取平均值,
这样一来像素与像素之间的差异性就变小了。
滤波后像素与像素之间的差异性减小了,这不就意味着滤除了高频成分吗?

最后说一下为什么也叫模糊

滤波后的结果就是,像素与像素之间的差异性就变小了。
图像中分明的线条和边界就是像素值迥然的差异所导致的,差异性减小导致边界就模糊了。
所以我们称其为模糊

上篇 - 总结

总结起来就是:

  • 图像卷积靠卷积核完成
  • 卷积核规定了运算的规则
  • 滤波/模糊是卷积运算所带来的效果

不同的卷积核所得到的卷积效果不同,故衍生出了不同种类的滤波/模糊,形态运算,梯度运算等概念。

由此可见:卷积是图像处理的基础,许许多多处理方式都是离不开卷积的

2.图像卷积 - 下篇

接下来,我们看一下视频课程中的图,就很容易理解了

它采用的是

  • 均值卷积方式
  • 卷积核3x3
  • 不处理边缘

在这里插入图片描述

边缘处理

上面的图片演示了图像的卷积操作,但是直观的看出,卷积后的图片和卷积前的图片尺寸不一致。

对于这个问题,有两种处理策略:

  • 一是把它扔掉不管它(比如上图)
  • 二是把它周围填充起来进行处理(多数的深度学习)

深度学习有两种方式,就分别对应了上面的两种方式

  • 看重中心的——valid padding
  • 填充的——same padding

这里补充一下,卷积方式是卷积方式的,边缘处理策略是边缘处理策略的,
只要策略得当,均值卷积也能处理边缘的。

说了这么多,那怎么进行边缘填充?

提示:

  • 这里只介绍图示概念,具体实现操作在后续课程中
  • 更多的边缘处理方法,查阅官方文档可知

通常有四种方法:

在这里插入图片描述

不同卷积核的意义

最后,我们来介绍一下,不同卷积核下的图像卷积意义

图像处理中,平滑、模糊、去燥、锐化、边缘提取等等工作,其实都可以通过卷积操作来完成。
下面几个典型的卷积核效果如下:

在这里插入图片描述
在这里插入图片描述

从以上示例可以看出,不同的卷积核作用于图像,可以更清晰的获得图像的某种特征,如轮廓、颜色等。

3.图像均值卷积的演示

手动实现

理解了图像卷积的概念之后,现在我们来手动实现图像的均值卷积

int h = src.rows;
int w = src.cols;
Mat result = src.clone();

//最外面一边我们选择不处理,忽略掉
for (int row = 1; row < h - 1; row++) {
for (int col = 1; col < w - 1; col++) {
// 3x3卷积核
int sb = src.at<Vec3b>(row, col)[0] + src.at<Vec3b>(row - 1, col - 1)[0] + src.at<Vec3b>(row - 1, col)[0] +
src.at<Vec3b>(row - 1, col + 1)[0] + src.at<Vec3b>(row, col - 1)[0] + src.at<Vec3b>(row, col + 1)[0] +
src.at<Vec3b>(row + 1, col - 1)[0] + src.at<Vec3b>(row + 1, col)[0] + src.at<Vec3b>(row + 1, col + 1)[0];

int sg = src.at<Vec3b>(row, col)[1] + src.at<Vec3b>(row - 1, col - 1)[1] + src.at<Vec3b>(row - 1, col)[1] +
src.at<Vec3b>(row - 1, col + 1)[1] + src.at<Vec3b>(row, col - 1)[1] + src.at<Vec3b>(row, col + 1)[1] +
src.at<Vec3b>(row + 1, col - 1)[1] + src.at<Vec3b>(row + 1, col)[1] + src.at<Vec3b>(row + 1, col + 1)[1];

int sr = src.at<Vec3b>(row, col)[2] + src.at<Vec3b>(row - 1, col - 1)[2] + src.at<Vec3b>(row - 1, col)[2] +
src.at<Vec3b>(row - 1, col + 1)[2] + src.at<Vec3b>(row, col - 1)[2] + src.at<Vec3b>(row, col + 1)[2] +
src.at<Vec3b>(row + 1, col - 1)[2] + src.at<Vec3b>(row + 1, col)[2] + src.at<Vec3b>(row + 1, col + 1)[2];
result.at<Vec3b>(row, col)[0] = sb / 9;
result.at<Vec3b>(row, col)[1] = sg / 9;
result.at<Vec3b>(row, col)[2] = sr / 9;
}
}
imshow("conv-demo", result);

在这里插入图片描述

API实现

opencv中,如果我们想实现图像的均值卷积(均值模糊/均值滤波),我们要用到这样一个API

  • blur

具体介绍如下

blur
使用归一化框滤镜模糊图像
共5个参数
第1个参数 输入
第2个参数 输出
第3个参数 卷积核size(卷积核越大,图像模糊程度越高)
第4个参数 锚点
(默认值Point(-1,-1)表示锚点位于卷积后的内核中心,卷积后的值在这里更新)
第5个参数 图像边缘处理方式
(默认参数 BORDER_DEFAULT 边缘有很多处理方式,查阅官方文档可知)

这个默认的形式,是一种镜像的边缘处理
在这里插入图片描述

//函数定义
void blur_demo(Mat& image);

//函数实现
void  QuickDemo::blur_demo(Mat& image) {

imshow("原图",image);

Mat dst;

//二维卷积
blur(image, dst, Size(3, 3));
//blur(image, dst, Size(13, 13));

//一维卷积
blur(image, dst, Size(13, 1));//行卷积
blur(image, dst, Size(1, 13));//列卷积


imshow("图像模糊",dst);

}
二维卷积

3 * 3
对比上面的手动实现,我们发现效果基本一样
在这里插入图片描述
13 * 13
在这里插入图片描述

一维卷积

blur也可以实现一维的卷积

13 * 1
在这里插入图片描述
1 * 13
在这里插入图片描述

本课所用API查阅

blur

在这里插入图片描述
在这里插入图片描述

BorderTypes

在这里插入图片描述

28 OpenCV4 C++ 快速入门

Excerpt

28 高斯模糊


28 高斯模糊

opencv知识点:

  • 高斯模糊 - GaussianBlur

本课所解决的问题:

  • 如何理解高斯模糊?
  • 如果实现高斯模糊?

1.高斯模糊

常用的模糊算法有两种,一种是均值(盒子),一种是高斯。
现在我们来介绍一下高斯

模糊

首先我们了解一下什么是模糊

模糊就是对图像进行平滑化处理。
平滑化处理,就是用平滑滤波函数,生成卷积核对应的权重,然后对图像进行卷积操作。

均值模糊可以做到让图片模糊,但是它的模糊不是很平滑。
不平滑主要在于距离中心点很远的点与距离中心点很近的所带的权重值相同,产生的模糊效果一样。

在这里插入图片描述

而想要做到平滑,让权重值跟随中心点位置距离不同而不同,则可以利用正态分布(中间大,两端小)来实现。
高斯模糊之所以叫高斯模糊,就是因为它运用了高斯的正态分布的密度函数(概率论知识)。
高斯模糊保留的轮廓信息比较多,所以它的模糊平滑要好于均值模糊

高斯模糊

引用文章: 高斯模糊与图像卷积滤波一些知识点

要想实现高斯模糊的特点,就需要通过构建对应的权重矩阵来进行滤波。
接下来我们开始介绍如何利用正态分布构建对应的权重矩阵。

正态分布中,越接近中心点,取值越大,越远离中心,取值越小。
计算平均值的时候,我们只需要将"中心点"作为原点,其他点按照其在正态曲线上的位置分配权重,
就可以得到一个加权平均值。正态分布显然是一种可取的权重分配模式。

在这里插入图片描述

如何反映出正态分布?那就要用高斯函数来实现。

上面的正态分布是一维的,而对于图像都是二维的,所以我们需要二维的正态分布。
在这里插入图片描述

正态分布的密度函数叫做"高斯函数"(Gaussian function)。
它的一维形式是:
在这里插入图片描述
其中,μ是x的均值,σ是x的方差。
因为计算平均值的时候,中心点就是原点,所以μ等于0。于是公式进一步简化为:
在这里插入图片描述
根据一维高斯函数,可以推导得到二维高斯函数:
在这里插入图片描述
有了这个函数 ,就可以计算每个点的权重了。

高斯模糊示例

在这里插入图片描述
除以总值这个过程就是问之前课上所讲的归一化,目的是让滤镜的权重总值等于1。
在这里插入图片描述
将这9个值加起来,就是中心点的高斯模糊的值。对所有点重复这个过程,就得到了高斯模糊后的图像。

2.高斯模糊演示

opencv中,如果我们想要实现高斯模糊,就要用到这样一个API

  • GaussianBlur

具体介绍如下

GaussianBlur
使用高斯滤镜模糊图像
共6个参数 
第1个参数 输入
第2个参数 输出
第3个参数 高斯卷积核size(必须是整数和奇数,可以是0,然后根据sigma计算他们)

第4个参数 x方向的高斯核标准差
第5个参数 y方向的高斯核标准差(如果sigmaY = 0,则设置其 = sigmaX)
(如果两个标准差都为0,则根据高斯卷积核size计算)
(官方文档建议:三个参数最好都指定)

 虽然size和sigma都可以实现模糊,但sigma的影响更大

第6个参数 图像边缘处理方式
(超出初学者范围,暂不学习)

演示如下

//函数定义
void gaussian_blur_demo(Mat& image);

//函数实现
void QuickDemo::gaussian_blur_demo(Mat& image) {

imshow("原图", image);

Mat dst;
GaussianBlur(image, dst, Size(5, 5), 15);

imshow("高斯模糊", dst);

}

在这里插入图片描述

本课所用API查阅

GaussianBlur

在这里插入图片描述
在这里插入图片描述

29 OpenCV4 C++ 快速入门

Excerpt

29 高斯双边模糊


29 高斯双边模糊

opencv知识点:

  • 高斯双边模糊 - bilateralFilter

本课所解决的问题:

  • 如何理解高斯双边模糊?
  • 如何实现高斯双边模糊?

1.高斯双边模糊

前面我们介绍的图像卷积处理无论是均值还是高斯都是属于模糊卷积,它们都有一个共同的特点就是模糊之后图像的边缘信息不复存在,受到了破坏。

边缘保留滤波算法(EPF)有能力通过卷积处理实现图像模糊的同时对图像边缘不会造成破坏,滤波之后的输出完整的保存了图像整体边缘(轮廓)信息。

最常见的边缘保留滤波算法有以下几种:

  • 高斯双边模糊
  • Meanshift均值迁移模糊
  • 局部均方差模糊
  • opencv中对边缘保留滤波还有一个专门的API

高斯模糊是考虑图像空间位置对权重的影响,但是它没有考虑图像像素分布对图像卷积输出的影响。
双边模糊考虑了像素值分布的影响,对像素值空间分布差异较大的进行保留从而完整的保留了图像的边缘信息。

双边模糊可以去除无关噪声,同时保持较好的边缘信息。
但是,其速度比绝大多数滤波器都慢。

2.高斯双边模糊演示

opencv中,如果我们想要实现高斯双边模糊,就要用到这样一个API

  • bilateralFilter

具体介绍如下

bilateralFilter
将双边过滤器应用于图像
共6个参数
第1个参数 输入
第2个参数 输出
第3个参数 过滤期间使用的每个像素邻域的直径(如为非正数,则根据sigmaSpac计算)

第4个参数 sigmaColor(在颜色空间中的过滤标准差)

sigmaColor一般取值大一点,
大一点的话根据二维高斯函数计算所得的值越小,越趋近于0,影响越低

第5个参数 sigmaSpace(在坐标空间中的过滤标准差)

第6个参数  图像边缘处理方式
(超出初学者范围,暂不学习)

演示如下

//函数实现
void bifilter_demo(Mat& image);
//函数定义

void QuickDemo::bifilter_demo(Mat& image) {

imshow("原图", image);

Mat dst;

bilateralFilter(image, dst, 0, 100, 10);
imshow("高斯双边模糊", dst);

}

在这里插入图片描述

本课所用API查阅

bilateralFilter

在这里插入图片描述
在这里插入图片描述

30 OpenCV4 C++ 快速入门

Excerpt

30 案例:实施人脸检测


30 案例:实施人脸检测

opencv知识点:

  • 创建和操作综合人工神经网络 - dnn::Net类
  • 读取以TensorFlow框架格式存储的网络模型 - readNetFromTensorflow
  • 从图像创建4维blob - blobFromImage
  • 设置网络的新输入值- Net::setInput
  • 运行正向传递以计算指定层的输出 - Net::forward

本课所解决的问题:

  • 如何运行opencv4提供的人脸检测模型

1.OpenCV 4

本节课的内容主要基于opencv 4 实现,开始我们先了解一下opencv4。

OpenCV4.x发布以来,其依靠良好的接口代码、系统级别的优化、更加通用易学的函数调用,集成OpenVINO与tensorflow、caffe等模型加速推断、实现了从传统的图像处理到基于深度学习的视觉处理路线图的完整拓展。

OpenCV4 毫无疑问是一个OpenCV发展历史的一个重要里程碑之作。
官方的宣传口号 OpenCV4 is more than OpenCV
也充分说明OpenCV4 是整合深度学习的新一代计算机视觉开发框架!

本课将介绍一种opencv4自带的,一个进行人脸检测的模型

2.下载人脸检测模型和

为了方便,我们不采用视频的方式,我们就直接去下载课程源码zip了

下载地址:OpenCV学堂 / OpenCV课程资料

下载完之后到图中目录:opencv_tutorial_data-master → models → face_detector

在这里插入图片描述
目标文件夹:安装opencv目录下的sources → samples → dnn → face_detector
在这里插入图片描述

3.人脸检测模型演示

开始前,我们可以先把头文件命名空间补上

在这里插入图片描述
这里提一下,当我们使用Net导入模型时,关于模型的各种参数可以从这个文档查看
在这里插入图片描述

在运行这个人脸检测模型之前,我们需要先来了解几个API

  • dnn::Net类
  • readNetFromTensorflow
  • blobFromImage
  • Net::setInput方法
  • Net::forward方法
  • Mat::ptr

具体介绍如下

dnn::Net类

dnn::Net类
创建和操作综合人工神经网络

神经网络表示为有向无环图 (DAG),其中顶点是层实例,边指定层输入和输出之间的关系。

每个网络层在其网络内都有唯一的整数id 和唯一的字符串名称。LayerId 可以存储图层名称或图层ID。

此类支持其实例的引用计数,即副本指向同一个实例。

readNetFromTensorflow

readNetFromTensorflow
读取以TensorFlow框架格式存储的网络模型
共2个参数
第1个参数 pb文件路径
第2个参数 pbtxt文件路径

blobFromImage

blobFromImage
从图像创建4维blob(斑点,深度学习相关知识)
共7个参数
第1个参数 输入
第2个参数 图像空间大小(默认1.0,即图像仍保持在0~255色彩空间)
第3个参数 size
第4个参数 均值
第5个参数 是否交换RB通道(默认false)
第6个参数 调整图像大小后是否剪切(默认false)
第7个参数 输出的blob深度(默认CV_32F)

returns
按照NCHW(数量,通道,高度,宽度)顺序的4维Mat,blob就是这个4维Mat

Net::setInput方法

setInput
设置网络的新输入值
共4个参数
第1个参数 输入斑点
第2个参数 输入层名称(默认为空“”)
第3个参数 可选的标准化比列
第4个参数 可选的平均值

Net::forward方法

forward
运行正向传递以计算名为 outputName 的层的输出。
共1个参数
第1个参数 需要输出的层的名称
returns
返回也有四个值
第1个维度 表示有多少张图像,并且每个图像有一个编号。imageId
第2个维度 图像的批次。batchId
第3个维度 有多少个框(行)
第4个维度 每个框有7个值(列)
前两个值 标明类型 + index
第3个值 得分(得分越高,越有可能是人脸)
后四个值 矩形左上角和右下角的坐标

Mat::ptr

二维单通道元素可以用MAT::at(i,j),i是行号,j是列号

但对于多通道的非uchar类型矩阵来说,以上方法不适用
可以用Mat::ptr()来获得指行某行元素的指针,在通过行数与通道数计算相应点的指针

演示如下

//函数定义
void face_detection_demo();

//函数实现
void QuickDemo::face_detection_demo() {

//文件夹路径
string root_dir = "D:/meta/opencv/opencv-4.5.5/sources/samples/dnn/face_detector/";

//读取以TensorFlow框架格式存储的网络模型
dnn::Net net = dnn::readNetFromTensorflow(root_dir + "opencv_face_detector_uint8.pb", root_dir + "opencv_face_detector.pbtxt");

//对摄像头进行人脸检测
VideoCapture cap(0);
Mat frame;
while (true) {

cap.read(frame);//frame为输出,read是将捕获到的视频一帧一帧的传入frame

if (frame.empty()) {
break;
}

flip(frame, frame, 1);//左右翻转

//准备深度学习模型需要的数据 (blob-斑点)
Mat blob = dnn::blobFromImage(frame, 1.0, Size(300, 300), Scalar(104, 177, 123), false, false);
net.setInput(blob);

//完成推理
Mat probs = net.forward();

Mat detectionMat(probs.size[2], probs.size[3], CV_32F, probs.ptr<float>());

//解析结果
for (int i = 0; i < detectionMat.rows; i++) {

float confidence = detectionMat.at<float>(i, 2);//第三个值 得分

if (confidence > 0.5) {

//因为预测来的值为[0,1]范围的数,我们还需要*原图像的宽度和长度
int x1 = static_cast<int>(detectionMat.at<float>(i, 3) * frame.cols);
int y1 = static_cast<int>(detectionMat.at<float>(i, 4) * frame.rows);
int x2 = static_cast<int>(detectionMat.at<float>(i, 5) * frame.cols);
int y2 = static_cast<int>(detectionMat.at<float>(i, 6) * frame.rows);

Rect rect(x1, y1, x2 - x1, y2 - y1);
rectangle(frame, rect, Scalar(0, 0, 255), 2, 8, 0);

}
}

imshow("人脸检测",frame);

int c = waitKey(1);
if (c == 27) {
break;
}
}
cap.release();

}

在这里插入图片描述

本课所用API查阅

1.dnn::Net
在这里插入图片描述
2.dnn::readNetFromTensorflow
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
3.dnn::blobFromImage
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
4.Net::setInput
在这里插入图片描述
在这里插入图片描述
5.Net::forward
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • 17
    点赞
  • 88
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值