MasteringOpenCV实战源码学习笔记 章节一

所有代码都可以直接复制到VS里运行,所以就不放运行结果图了
基于OpenCV 4.1.1
课本 https://github.com/MasteringOpenCV/code
没有写完!因为其他原因我弃坑去图形学了,发出来保存一下吧。

第一部分:简单的摄像头视频读取流程

//ver 4.1.1
#include <opencv2/opencv.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <iostream>

using namespace cv;
using namespace std;

void cartoonifyImage(cv::Mat input, cv::Mat output);

int main(int argc,char *argv[],char **env)
{
	//cv::Mat srcImage = imread("C:\\Users\\MaxLy\\Desktop\\girl.jpg");

	int cameraNumber = 0;
	if (argc > 1)
		cameraNumber = atoi(argv[1]);//字符串转换成整型

	//Get access to the camera.
	cv::VideoCapture camera;
	camera.open(cameraNumber);
	if (!camera.isOpened())
	{
		std:; cerr << "ERROR: Count not access the camera or video " << std::endl;
		exit(1);
	}

	//Try to set the camera resolution
	//camera.set(cv::CV_CAP_PROP_FRAME_WIDTH, 640);
	//camera.set(cv::CV__CAP_PROP_FRAME_HEIGHT, 480);

	while (true)
	{
		//Grab the next camera frame.
		cv::Mat cameraFrame;
		camera >> cameraFrame;
		if (cameraFrame.empty())
		{
			std::cerr << "ERROR: Couldn't grab a camera frame." << std::endl;
			exit(0);
		}
		//Create a blank output image,that we will draw noto.
		cv::Mat displayedFrame(cameraFrame.size(),CV_8UC3);

		//Run the cartoonifier filter on the camera frame.
		cartoonifyImage(cameraFrame, displayedFrame);

		//Display the processed image onto the screen.
		imshow("Cartoonifier", cameraFrame);

		//IMPORTANT:Wait for at least 20 milliseconds,
		//so that the image can be displayed on the screen!
		//Also checks if a key was pressed in the GUI window.
		//Note that it should be a "char" to support Linux.
		char keypress = cv::waitKey(30);
		if (keypress == 27)  //Escape Key
		{
			//Quit the program !
			break;
		}
	}//end while
	return 0;
}

void cartoonifyImage(cv::Mat input, cv::Mat output)
{

}

1.关于Main函数参数,以前从来没注意过

2.5 argc与argv参数解惑
2.5.1 初识main函数中的argc和argv

  在与OpenCV打交道时,我们常会在相关的实例程序中见到argc和argv这两个参数,入OpenCV的官方示例程序Samples中、OpenCV经典教材《Learning OpenCV》中,等等。这常常会让众多初学者疑惑,不清楚它们是何用途。本届内容正为解此惑而写。
  argc和argv中的arg指的是"参数"(例如:arguments,argument counter和argument vector)。其中,argc为整数,用来统计运行程序时送给main函数的命令行参数的个数;而*argv[]:位字符串数组,用来存放指向字符串参数的指针数组,每一个元素指向一个参数。
  Argc,argv这两个参数一般在用命令行编译程序时有用。在初学c++时,往往要弱化argc和argv的用法,main函数常常不带参数,如下。
  int main()
  {

  }
  而在opencv的官方示例程序中,main函数的写法常常会带上两个形参,一般为argv和argv,并且在函数体内部会使用到这两个形参,如下:
  int main(int argc,char** argv)
  {
  const char* imagename = argc > 1 ? argv[1] : “lena.jpg”;
   ……
  }
  其实带形参的main函数,入main(int argc,char * argv[],char **env),是UNIX、Linux以及MacOS操作系统中C/C++的main函数的标准写法,并且是血统最纯正的main函数的写法。可能是由于外国的专家们更习惯使用UNIX、Linux以及MacOS等操作系统,所以我们接触到由他们开发和维护的OpenCV这款开源视觉库的时候,自然会发现代码中常有argc和argv的出现。

2.5.2 argc、argv的具体含义

  argc和argv这两个参数一般在用命令行编译程序时有用。
  主函数main中变量(int argc,char *argv[])的含义
  有些编译器允许将main()的返回类型声明为void,这就已不再是合法的C++了。
  其实,main(int argc,char *argv[],char **env)才是UNIX和Linux中的标准写法。其中,第一个参数,int类型的argc,为整型,用来统计程序运行时发送给main函数的命令行参数的个数,在Visual Studio中默认值为1。第二个参数,char*类型的argv[];,为字符串数组,用来存放指向字符串参数的指针数组,每一个元素指向一个参数。各成员含义如下:

  • argv[0]指向程序运行的全路径名
  • argv[1]指向在DOS命令行中执行程序名后的第一个字符串
  • argv[2]指向执行程序名后的第二个字符串
  • argv[3]指向执行程序名后的第三个字符串
  • argv[argc]为NULL

   需要指出,argv[1]对应于【项目属性】→【配置属性】→【调试】→【命令参数】中的值。记住双引号也要带上,比如读取名为1.jpg的图片,如图2.30所示,就要在命令参数中填字符串“1.jpg”。
   而如果有多个字符串,则用空格隔开。比如要读入两张名称分别为1.jpg和2.jpg的图片,在命令参数填"1.jpg"“2.jpg”,两者之间用空格隔开即可,入图2.31所示。
第三个参数,char **类型的env,为字符串数组。env[]的每一个元素都包含ENVVAR=value形式的字符串。其中ENVVAR为环境变量,value为ENVVAR的对应值。在OpenCV中很少使用它。
   argc、argv和env是在main()函数之前被赋值的。其实,main()函数严格意义上并不是真正的程序入口点函数,往往入口点还与操作系统油管。而在Windows的控制台应用程序中,将main()函数作为程序入口点,并且很少使用argc、argv等命令行参数。

2.5.3 Visual Studio 中main函数的几种写法说明

   需要注意的是,在如今各版本的Visual Studio编译器中,main()函数带参数argc和argv或不带,也就是说,无论我们是否在函数体中使用argc和argv,返回值为void或不为void,都是合法的。
   即至少有如下3种写法合法。

  1. 【写法一】返回值为整型带参的main函数
    int main(int argc,char** argv)
    {
       //函数体内使用或不使用argc和argv都可行
       ……
       return 1;
    }
  2. 【写法二】返回值为整型不带参的main函数
    int main(int argc,char** argv)
    {
       //函数体内使用了argc或argv
       ……
       return 1;
    }
  3. 【写法三】返回值为void切不带参的main函数
    int main()
    {
       ……
       return 1;
    }

   在Visual Studio中,如果使用了argv或argc,即上文代码中的第一种写法,且在使用之前没有在【项目属性】→【配置属性】→【调试】→【命令参数】中指定参数的值,就会报错,常见的报错窗口如图2.32所示,这是研究OpenCV官方提供的示例程序时经常碰到的错误。
要想解决此问题并编译通过,除了上文讲过的在项目属性页中填上命令参数外,最简单的做法就是在不影响源氏程序的基础上,将和argv或argc有关的代码进行替换或注释。
   比如将"Mat srcImage = imread(argv[1],1);//读取字符串名为argv[1]的图片"替换为"Mat srcImage = imread(“1.jpg”,1);"//工程目录下有一张名为"1.jpg"的图片“。

2.5.4 总结

   讲解至此,读者应该对argc和argv有了比较透彻的认识。简单来说

  • int argc表示命令行字串的个数
  • char *argv[]表示命令行参数的字符串

   由于我们使用的开发环境为Visual Studio,往后若在OpenCV相关代码中遇到这两个参数,可以在代码中将其用路径字符串替换,或者在项目属性页中给其赋值,而对程序整体影响不大的部分就将其注释掉。

//引用自OpenCV3编程入门 毛星云 冷雪飞 等编著

2.关于一些类

VideoCapture

  是一个关于读取视频文件,图片序列和摄像机的类
  官方4.1.1示例教程
  camera.open(0)代表打开第一个摄像头
  camera>>cameraFrame 从摄像头视频流里读取数据发给cameraFrame

Scalar

  颜色的表示,Scalar()表示具有4个元素的数组,在OpenCV中被大量用于传递像素值,入RGB颜色值。而RGB颜色值为三个参数,其实对于Scalar函数来说,如果用不到第四个参数,则不需要写出来;若只写三个参数,OpenCV会认为我们就像表示三个参数。
  来看个例子,如果给出以下颜色参数表达式:
  Scalar(a,b,c)
  那么定义的RGB颜色值:红色分量为c,绿色分量为b,蓝色分量为a。
  Scalar类的源头为Scalar_类,而Scalar_类是Vec4x的一个变种,我们常用的Scalar其实就是Scalar_。这就解释了为什么很多函数的参数输入可以是Mat,也可以是Scalar。

Mat

4.1 基础图像容器Mat
4.1.1数字图像存储概述

  我们可以通过各种各样的方法从现实世界获取到数字图像,入借助相机、扫描仪、计算机摄像头或磁共振成像等。通常由显示屏删看到的都是真实而漂亮的图像,但是这些图像在转化到我们的数字设备中时,记录的却是图像中的每个点的数值。
  比如在图4.1中你可以看到草坪的颜色是一个包含众多强度值的像素点矩阵。可以这样说,矩阵就是图像在数码设备中的表现形式。OpenCV作为一个计算机视觉库,其主要的工作是出率和操作并进一步了解这些形式和信息。因此,理解OpenCV是如何存储和处理图像是非常有必要的。那么,就让我们来进一步了解和学习。

4.1.2 Mat结构的使用

  自2001年以来,OpenCV的函数库一直是基于C接口构建的,因此在最初的几个OpenCV版本中,一般使用名为IplImage的C语言结构体在内存中存储图像。时至今日,这扔出现在大多数的旧版教程和教学材料中,如最经典的OpenCV教程《Learning OpenCV》。
  对于OpenCV1.X时代的基于C语言接口而建的图像存储格式IplImage*,如果在退出前忘记release掉的话,就会造成内存泄露,而且用起来有些不便,我们在调试的时候,往往要话费很多时间在手动释放内存的问题上。虽然对于小型的程序来说,手动管理内存不是问题,但一旦需要书写和维护的代码越来越庞大,我们变回开始越来越多地纠缠于内存管理的问题,而不是着力解决最终的开发目标。这,就有些舍本逐末的感觉了。
  幸运的是,C++出现了,并且带来了类的概念,这使我们有了另外一个选择:自动的内存管理(非严格意义上的)。这对于广大图像处理领域的研究者来说,的确是一件可喜可贺的事情。也就是说,OpenCV在2.0版本中引入了一个新的C++接口,利用自动内存管理给出了解决问题的新方法。使用此方法,我们不需要再纠结在管理内存的问题,而且代码会变得干净而简洁。
  但C++接口唯一的不足是:当前许多嵌入式开发系统只支持C语言。所以,当开发目标不是仅能使用C语言作为开发语言时,便没有必要使用旧的C语言接口了,除非你真的很有自信。
  从OpenCV踏入2.0时代,使用Mat类数据结构作为主打之后,OpenCV变得越发像需要很少编程汉阳的Matlab那样,上手很方便。甚至有些函数名称都和Matlab一样,比如大家所熟知的imread、imwrite、imshow等函数。
  关于Mat类,首先我们要知道的是:
  (1) 不必再手动为其开辟空间。
  (2) 不必再在不需要时立即将空间释放。
  这里指的是手动开辟空间并非必须,但它依旧是存在的——大多数OpenCV函数仍会手动地为输出数据开辟空间。当传递一个已经存在的Mat对象时,开辟好的矩阵空间会被重用。也就是说,我们每次都使用大小正好的内存来完成任务。
  Mat总而言之,是一个类,由两个数据部分组成:矩阵头(包含矩阵尺寸、存储方法、存储地址等信息)和一个指向存储所有像素值的矩阵(根据所选存储方法的不同,矩阵可以是不同的维数)的指针。矩阵头的尺寸是常数值,但矩阵本身的尺寸会依图像的不同而不同,通常比矩阵头的尺寸大数个数量级。因此,当在程序中传递图像并创建副本时,大的开销是由矩阵造成的,而不是信息头。OpenCV是一个图像处理库,囊括了大量的图像处理函数,为了解决问题通常要使用库中的多个函数,因此在函数中传递图像是常有的事。同时不要忘了我们正在讨论的是计算量很大的图像处理算法,因此除非万不得已,不应该进行大图像的复制,因为这会降低程序的运行速度。
  为了解决此问题,OpenCV使用了引用计数机制,其思路是让每个Mat对象有自己的信息头,但共享同一个矩阵。这通过让矩阵指针指向同一地址而实现。而拷贝构造函数则只复制信息头和矩阵指针,而不复制矩阵。
  来看下面这段代码。
  Mat A,C;//仅创建信息头部分
  A = imread(“1.jpg”,CV_LOAD_IMAGE_COLOR);//这里为矩阵开辟内存
  Mat B(A); //使用拷贝构造函数
  C = A; //赋值运算符
  以上代码中的所有Mat对象最终都指向同一个也是唯一一个数据矩阵。虽然它们的信息头不同,但通过任何一个对象啊ing所做的改变也会影响其他对象。实际上,不同的对象只是访问相同数据的不同途径而已。这里还要提及一个比较棒的功能:我们可以创建只引用部分数据的信息头。比如想要创建一个感兴趣区域(ROI),只需要创建包含边界信息的信息头:
  Mat D(A,Rect(10,10,100,100)); //使用矩阵界定
  Mat E = A(Range::all(),Range(1,3)); //用行和列来界定
  现在你也许会问:如果矩阵属于多个Mat对象,那么当不再需要它时,谁来负责清理呢?
  简单的回答是:最后一个使用它的对象。通过引用计数机制来实现。我们无论什么时候复制一个Mat对象的信息头,都会增加矩阵的引用次数。反之,当一个头被释放之后,这个计数被减一;当计数值为零,矩阵会被清理。但某些时候你仍会想复制矩阵本身(不只是信息头和矩阵指针),这时可以使用函数clone()或者copyTo()。
  Mat F = A.clone();
  Mat G;
  A.copyTo(G);
  现在改变F或者G就不会影响Mat信息头所指向的矩阵。本小节可总结为如下4个要点。

  • OpenCV函数中输出图像的内存分配是自动完成的(如果不特别指定的话)。
  • 使用OpenCV的C++接口时不需要考虑内存释放问题。
  • 赋值运算符和拷贝构造函数(构造函数)只复制信息头。
    使用函数clone()或者copyTo()来复制一幅图像的矩阵。
4.1.3 像素值的存储方法

  本届我们将讲解如何存储像素值。存储像素值需要指定颜色空间和数据类型。其中,颜色空间是指针对一个给定的颜色,如何组合颜色元素以对其编码。最简单的颜色空间要属灰度级空间,只处理黑色和白色,对他们进行组合便可以产生不同程度的灰色。
  对于彩色方式则有更多种类的颜色空间,但不论哪种方式都是把颜色分成三个或者四个基元素,通过组合基元素可以产生所有的颜色。RGB颜色空间是最常用的一种颜色空间,这归功于它也是人眼内部构成颜色的方式。它的基色是红色、绿色和蓝色,有时为了表示透明颜色也会加入第四个元素alpha(A)。
  颜色系统有很多,他们各有优势,具体如下。

  • RGB是最常见的,这是因为人眼采用相似的工作机制,它也被显示设备所采用
  • HSV和HLS把颜色分解成色调、饱和度和亮度/明度。这是描述颜色更自然的方式,比如可以通过抛弃最后一个元素,使算法对输入图像的光照条件不敏感
  • YCrCb在JPEG图像格式中广泛使用
  • CIE L*a*b*是一种在感知上均匀的颜色空间,它适合用来度量两个颜色之间的距离
      每个组成元素都有其自己的定义域, 而定义域取决于其数据类型,如何存储一个元素决定了我们在其定义域上能够控制的精度。最小的数据类型是char,占一个字节或者8位,可以是有符号型(0到255之间)或无符号型(-127到+127之间)。尽管使用三个char型元素已经可以表示1600万种可能的颜色(使用RGB颜色空间),但若使用float(4字节,32位)或double(8字节,64位)则能给出更加精细的颜色分辨能力。但同时也要切记,增加元素的尺寸也会增加图像所占的内存空间。
4.1.4 显式创建Mat对象的七种方法

  在之前的章节中,我们已经讲解了如何使用函数imwrite()函数将一个矩阵写入图像文件中。但是作为debug,更加方便的方式是看实际值,我们可以通过Mat的运算符"<<“来实现。但要记住,Mat的运算符”<<"只对二维矩阵有效。
  Mat不但是一个非常有用的图像容器类,同时也是一个通用的矩阵类,我们也可以用它来创建和操作多维矩阵。
  创建一个Mat对象有多钟方法,列举如下。
1.【方法一】 使用Mat()构造函数
  最常用的方法是直接使用Mat()构造函数,这种方法简单明了,示范代码如下。
  Mat M(2,2,CV_8UC3,Scalar(0,0,255));
  cout<<"M = “<<endl<<” "<<M<<endl<<endl;
  对于二维通道图像,首先要定义其尺寸,即行数和列数。然后,需要指定存储元素的数据类型以及每个矩阵点的通道数。为此,依据下面的规则有多种定义:
  CV[The number of bits per item][Signed or Unsigned][Type Prefix]C[The channel number]
  即:
  CV_[位数][带符号与否][类型前缀]C[通道数]
   比如CV_8UC3表示使用8位的unsigned char型,每个像素由三个元素组成三通道。而预先定义的通道数可以多达四个。另外,Scalar是个short型的向量,能使用指定的定制化值来初始化矩阵,它还可以用于表示颜色,后卫有详细讲解。当然,若需要更多通道数,可以使用大写的宏并把通道数放在小括号中,如方法二中的代码所示。
2.【方法二】在C\C++中通过构造函数进行初始化
  这种方法为在C\C++中通过构造函数进行初始化,示范代码如下。
  int sz[3] = {2,2,2};
  Mat L(3,sz,CV_8UC,Scalar::all(0));
  上面的例子演示了如何创建一个超过两维的矩阵:指定维数,然后传递一个指向一个数组的指针,这个数组包含每个维度的尺寸;后续的两个参数与方法一种的相同。
3.【方法三】为已存在的IpIImage指针创建信息头,示范代码如下。
  IplImage* img = cvLoadImage(“1.jpg”,1);
  Mat mtx(img); //转换IplImage*->Mat
4.【方法四】利用Create()函数
  方法四是利用Mat类中的Create()成员函数进行Mat类的初始化操作,示范代码如下。
  M.create(4,4,CV_8UC(2));
  cout<<"M = “<<endl<<” "<<M<<endl<<endl;
  需要注意的是,此创建方法不能为矩阵设初值,只是在改变尺寸时重新为矩阵数据开辟内存而已。
5.【方法五】采用Matlab式的初始化方式
  方法五采用Matlab形式的初始化方式:zeros(),ones(),eyes()。使用一下方式制定尺寸和数据类型:
  Mat E = Mat::eye(4,4,CV_64F);
  cout<<"E = “<<endl<<” "<<E<<endl<<endl;

  Mat O = Mat::zeros(2,2,CV_32F);
  cout<<"O = “<<endl<<” "<<O<<endl<<endl;

  Mat Z = Mat::zeros(3,3,CV_8UC1);
  cout<<"Z = “<<endl<<” "<<Z<<endl<<endl;
6.【方法六】对小矩阵使用逗号分隔式初始化函数
  方法六为对小矩阵使用逗号分隔式初始化函数,示范代码如下。
  Mat C = (Mat_(3,3)<<0,-1,0,-1,5,-1,0,-1,0);
cout<<"C = “<<endl<<” "<<C<<endl<<endl;
7.【方法七】为已存在的对象创建新信息头
  方法七为使用成员函数clone()或者copyTo()为一个已存在的Mat对象创建一个新的信息头,示范代码如下。
  Mat RowClone = C.row(1).clone();
  cout<<"RowClone = “<<endl<<” "<<RowClone<<endl<<endl;

//引用自OpenCV3编程入门 毛星云 冷雪飞 等编著

第二部分:生成黑白素描风格

//ver 4.1.1
#include <opencv2/opencv.hpp>
//#include "cartoon.h"

using namespace cv;
using namespace std;

void cartoonifyImage(Mat srcColor, Mat dst);

int main(int argc,char *argv[],char **env)
{
	//cv::Mat srcImage = imread("C:\\Users\\MaxLy\\Desktop\\girl.jpg");

	int cameraNumber = 0;
	if (argc > 1)
		cameraNumber = atoi(argv[1]);//字符串转换成整型

	//Get access to the camera.
	cv::VideoCapture camera;
	camera.open(cameraNumber);
	if (!camera.isOpened())
	{
		std:; cerr << "ERROR: Count not access the camera or video " << std::endl;
		exit(1);
	}

	//Try to set the camera resolution
	//camera.set(cv::CV_CAP_PROP_FRAME_WIDTH, 640);
	//camera.set(cv::CV__CAP_PROP_FRAME_HEIGHT, 480);

	while (true)
	{
		//Grab the next camera frame.
		cv::Mat cameraFrame;
		camera >> cameraFrame;
		if (cameraFrame.empty())
		{
			std::cerr << "ERROR: Couldn't grab a camera frame." << std::endl;
			exit(0);
		}
		//Create a blank output image,that we will draw noto.
		cv::Mat displayedFrame(cameraFrame.size(),CV_8UC3);

		//Run the cartoonifier filter on the camera frame.
		cartoonifyImage(cameraFrame, displayedFrame);

		if (displayedFrame.empty())
		{
			std::cerr << "ERROR displayedFrame is empty" << std::endl;
			exit(0);
		}
	
		//Display the processed image onto the screen.
		imshow("Cartoonifier", displayedFrame);

		//IMPORTANT:Wait for at least 20 milliseconds,
		//so that the image can be displayed on the screen!
		//Also checks if a key was pressed in the GUI window.
		//Note that it should be a "char" to support Linux.
		char keypress = cv::waitKey(30);
		if (keypress == 27)  //Escape Key
		{
			//Quit the program !
			break;
		}
	}//end while
	return 0;
}

主要关注cartoonifyImage()方法

void cartoonifyImage(Mat srcColor, Mat dst)
{
	Mat srcGray;
	cvtColor(srcColor, srcGray, COLOR_BGR2GRAY);

	const int MEDIAN_BLUR_FILTER_SIZE = 7;
	medianBlur(srcGray, srcGray, MEDIAN_BLUR_FILTER_SIZE);

	Size size = srcColor.size();
	Mat edges = Mat(size, CV_8U);
	Mat mask = Mat(size, CV_8U);
	const int LAPLACIAN_FILTER_SIZE = 5;
	Laplacian(srcGray, edges, CV_8U, LAPLACIAN_FILTER_SIZE);
	const int EDGES_THRESHOLD = 80;
	threshold(edges, mask, EDGES_THRESHOLD, 255, THRESH_BINARY_INV);
	cvtColor(mask, dst, COLOR_GRAY2BGR);
}

1.cvtColor() 颜色空间转换

4.2.5 颜色空间转换:cvtColor()函数

  cvtColor()函数是OpenCV里的颜色空间转换函数,可以实现RGB颜色向HSV、HSI等颜色空间的转换,也可以转换为灰度图像。
  原型如下;
  c++:void cvtColor(InputArray src,OutputArray dst,int code,int dstCn=0)
  第一个参数为输入图像,第二个采纳数为输出图像,第三个参数为颜色空间转换的标识符,第四个参数为目标图像的通道数,若该参数是0,表示目标图像取源图像的通道数,下面是一个调用示例:
  //OpenCV3版
  cvtColor(srcImage,dstImage,COLOR_GRAY2BGR);//转换原始图为灰度图
  而随着OpenCV版本的升级,cvtColor()函数对于颜色空间种类的支持也是越来越多。详细见官方4.1.1code
  在这里需要再次提醒大家的是,OpenCV默认的图片通道存储顺序是BGR,即蓝绿红,而不是RGB。

//引用自OpenCV3编程入门 毛星云 冷雪飞 等编著

为什么要先转成灰度图?看看大家的看法

2.Blur 滤波

6.1 线性滤波:方框滤波、均值滤波、高斯滤波
6.1.1 平滑处理

  平滑处理(smoothing)也称模糊处理(bluring),是一种简单且使用频率很高的图像处理方法。平滑处理的用途有很多,最常见的是用来减少图像上的噪点或者失真。在涉及到降低图像分辨率时,平滑处理是非常好用的方法。

6.1.2 图像滤波与滤波器

  图像滤波,指在尽量保留图像细节特征的条件下对目标图像的噪声进行抑制,是图像预处理中不可缺少的操作,其处理效果的好坏价将直接影响到后续图像处理和分析的有效性和可靠性。
  消除图像中的噪声成分叫作图像的平滑化或滤波操作。信号或图像的能量大部分集中在幅度谱的低频和中频段,而在较高频段,有用的信息经常被噪声淹没。因此一个能降低高频成分幅度的滤波器就能够减弱噪声的影响。
  图像滤波的目的有两个:一个是抽出图像的特征作为图像识别的特征模式:另一个是为适应图像处理的要求,消除图像数字化时所混入的噪声。
  而对滤波处理的要求也有两条:一是不能损坏图像的轮廓及边缘等重要信息;二十使图像清晰视觉效果好。
  平滑滤波是低频增强的空间域滤波技术。它的目的有两类:一类是模糊;另一类是消除噪音。
  空间域的平滑滤波一般采用简单平均法进行,就是求临近像元点的平均亮度值。邻域的大小与平滑的效果直接相关,邻域越大平滑的效果越好,但邻域过大,平滑也会使边缘信息损失的越大,从而使输出的图像变得模糊,因此需合理选择邻域的大小。
  关于滤波器,一种形象的比喻是:可以把滤波器想象成一个包含加权系数的窗口,当使用这个滤波器平滑处理图像时,就把这个窗口放到图像之上,透过这个窗口来看我们得到的图像。
  滤波器的种类有很多,在新版本的OpenCV中,提供了如下5种常用的图像平滑处理操作方法,它们分别被封装在单独的函数中,使用起来非常方便。

  • 方框滤波——BoxBlur函数
  • 均值滤波(邻域平均滤波)——Blur函数
  • 高斯滤波——Gaussian函数
  • 中值滤波——medianBlur函数
  • 双边滤波——bilateralFilter函数
    本节要讲解的是作为线型滤波的方框滤波、均值滤波和高斯滤波。其他两种非线性滤波操作——中值滤波和双边滤波,我们留待下节讲解。
6.1.3 线性滤波器的简介

  线性滤波器:线性滤波器经常用于剔除输入信号中不想要的频率或者从许多频率中选择一个想要的频率。
  几种常见的线性滤波器如下。

  • 低通滤波器:允许低频率通过;
  • 高通滤波器:允许高频率通过;
  • 带通滤波器:允许一定范围频率通过;
  • 带阻滤波器:阻止一定范围频率通过并且允许其他频率通过;
  • 全通滤波器:允许所有频率通过,仅仅改变相位关系;
  • 陷波滤波器(Band-Stop Filter):阻止一个狭窄频率范围通过,是一种特殊带阻滤波器。
6.1.4滤波和模糊

  关于滤波和模糊,大家往往在初次接触的时候会弄混淆:“一会儿说滤波,一会儿又说模糊,似乎不太清楚。”
  没关系,在这里,我们就来分析一下,为大家扫清障碍。
  上文已经提到过,滤波是将信号中特定波段频率滤除的操作,是抑制和防止干扰的一项重要措施。
  为了方便说明,就拿我们经常用的高斯滤波来作例子把。滤波可分低通滤波和高通滤波两种:高斯滤波是指用高斯函数作为滤波函数的滤波操作,至于是不是模糊,要看高斯低通还是高斯高通,低通就是模糊,高通就是锐化。
  其实说白了很简单的:

  • 高斯滤波是指用高斯函数作为滤波函数的滤波操作;
  • 高斯模糊就是高斯低通滤波。
6.1.5邻域算子与线性邻域滤波

  邻域算子(局部算子)是利用给定像素周围的像素值的决定此像素的最终输出值的一种算子。而线性邻域滤波就是一种常用的邻域算子,像素的输出值取决于输入像素的加权和,具体过程如图6.1所示。
  邻域算子除了用于局部色调调整以外,还可以用于图像滤波,以实现图像的平滑和锐化,图像边缘增强或者图像噪声的去除。本节我们介绍的主角是线性邻域滤波算子,即用不同的权重去结合一个小邻域内的像素,来得到应有的处理效果。

在这里插入图片描述
  注:邻域滤波(卷积)——左边图像与中间图像的卷积产生右边图像中蓝色标记的像素是利用原图像中红色标记的像素计算得到的。
  线性滤波处理的输出像素值g(i,j)是输入像素值f(i+k,j+I)的加权和,如下
在这里插入图片描述
其中的h(k,I),我们称其为“核”,是滤波器的加权系数,即滤波器的“滤波系数”。
  上面的式子可以简单写作:
在这里插入图片描述
  其中f表示输入像素值,h表示甲醛系数“核”,g表示输出像素值。
  在新版本的OpenCV中,提供了如下三种常用的线性滤波操作,他们分别被封装在单独的函数中,使用起来非常方便。

  • 方框滤波——boxblur函数
  • 均值滤波——blur函数
  • 高斯滤波——GaussianBlur函数
//ver 4.1.1 三种滤波的测试
#include <opencv2/opencv.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <iostream>
//#include "cartoon.h"

using namespace cv;
using namespace std;

Mat g_srcImage, g_dstImage1, g_dstImage2, g_dstImage3;//存储图片的Mat类型
int g_nBoxFilterValue = 3;//方框滤波参数值
int g_nMeanBlurValue = 3;//均值滤波参数值
int g_nGaussianBlurValue = 3;//高斯滤波参数值
int g_nResizeFactor = 5;

//全局函数声明部分
//轨迹条的回调函数
static void on_BoxFilter(int, void*);//方框滤波
static void on_MeanBlur(int, void*); //均值滤波
static void on_GaussianBlur(int, void*); //高斯滤波
int main(int argc, char* argv[], char** env)
{
	g_srcImage = imread("C:\\Users\\MaxLy\\Desktop\\girl.jpg", 1);
	resize(g_srcImage, g_srcImage, Size(g_srcImage.cols / g_nResizeFactor, g_srcImage.rows / g_nResizeFactor));
	if (!g_srcImage.data)
	{
		printf("读取srcImage错误");
		return false;
	}

	//复制原图到三个Mat类型
	g_dstImage1 = g_srcImage.clone();
	g_dstImage2 = g_srcImage.clone();
	g_dstImage3 = g_srcImage.clone();

	//显示原图
	namedWindow("原图窗口", 1);
	imshow("原图窗口", g_srcImage);

	//方框滤波
	namedWindow("方框滤波", 1);
	//创建轨迹条
	createTrackbar("内核值:", "方框滤波", &g_nBoxFilterValue, 40, on_BoxFilter);
	on_BoxFilter(g_nMeanBlurValue, 0);
	imshow("方框滤波", g_dstImage1);

	//均值滤波
	//创建窗口
	namedWindow("均值滤波", 1);
	//创建轨迹条
	createTrackbar("内核值", "均值滤波", &g_nMeanBlurValue, 40, on_MeanBlur);
	on_MeanBlur(g_nMeanBlurValue, 0);

	//高斯滤波
	//创建窗口
	namedWindow("高斯滤波", 1);
	//创建轨迹条
	createTrackbar("内核值:", "高斯滤波", &g_nGaussianBlurValue, 40, on_GaussianBlur);
	on_GaussianBlur(g_nGaussianBlurValue, 0);


	//输出一些帮助信息
	cout << endl << "请调整滚动条观察图像效果\n\n" << "按下q,程序退出" << endl;

	while (char(waitKey(1))!='q')
	{

	}

	return 0;
}

//方框滤波操作的回调函数
static void on_BoxFilter(int, void*)
{
	//方框滤波操作
	boxFilter(g_srcImage, g_dstImage1, -1, Size(g_nBoxFilterValue + 1, g_nBoxFilterValue + 1));
	//显示窗口
	imshow("方框滤波", g_dstImage1);
}

//均值滤波操作的回调函数
static void on_MeanBlur(int, void*)
{
	//均值滤波操作
	blur(g_srcImage, g_dstImage2, Size(g_nMeanBlurValue + 1, g_nMeanBlurValue + 1), Point(-1, -1));
	//显示窗口
	imshow("均值滤波", g_dstImage2);
}

//高斯滤波操作的回调函数
static void on_GaussianBlur(int, void*)
{
	//高斯滤波操作
	GaussianBlur(g_srcImage, g_dstImage3, Size(g_nGaussianBlurValue * 2 + 1, g_nGaussianBlurValue * 2 + 1), 0, 0);
	imshow("高斯滤波", g_dstImage3);
}

其中的Size就是Kernel的大小,核,是一个矩阵,用Size表示。

4.2.3 尺寸的表示:Size类

  通过在代码中对Size类进行“转到定义”操作,我们可以在……\opencv\sources\modules\core\include\opencv2\core\core.hpp路径下,找到Size类相关的源代码:
  typedef Size_ Size2i;
  typedef Size2i Size;
  其中,Size_是个模板类,在这里Size_表示其类体内部的模版所代表的类型为int。那这两句代码的意思,就是首先给已知的数据类型Size_起个新名字,叫Size2i。然后又给已知的数据类型Size2i起个新名字,叫Size。所以,连起来就是,Size_、Size2i、Size这三个类型名等价。
下面给出一个示例,方便大家理解。
  Size(5,5);//构造出的Size宽度和高度都为5,即XXX.width和XXX.height都为5

(1)方框滤波

  滤波Kernel如下
在这里插入图片描述

(2)均值滤波

  均值滤波就是归一化的方框滤波
在这里插入图片描述
  当normalize=true时,方框滤波就变成了均值滤波
  均值滤波本身存在着固有的缺陷,即它不能很好地保护图像细节,在图像去噪的同时也破坏了图像的细节部分,从而使图像变得模糊,不能很好地去除噪声点。

(3)高斯滤波

公式看不懂

…………

6.2 非线性滤波:中值滤波、双边滤波

  正如我们在6.1节中讲到的,线性滤波可以实现很多种不同的图像变换。而非线性滤波,如中值滤波器和双边滤波器,有时可以达到更好的实现效果。

6.2.1 非线性滤波概述

  在6.1节中,我们所考虑的滤波器都是线性的,即两个信号之和的响应和它们各自响应之和相等。换句话说,每个像素的输出值是一些输入像素的加权和。线性滤波器易于构造,并且易于从概率响应角度来进行分析。
  然而,在很多情况下,使用邻域像素的非线性滤波会得到更好的效果。比如在噪声是散粒噪声而不是高斯噪声,即图像偶尔会出现很大的值的时候,用高斯滤波器对图像进行模糊的话,噪声像素是不会被去除的,它们只是转换为更为柔和但仍然可见的散粒。这就到了中值滤波登场的时候了。

6.2.2 中值滤波

  中值滤波(Median filter)是一种典型的非线性滤波技术,基本思想是用像素点邻域灰度值的中值来代替该像素点的灰度值,该方法在去除脉冲噪声、椒盐噪声的同时又能保留图像的边缘细节。
  中值滤波是基于排序统计理论的一种能有效抑制噪声的非线性信号处理技术,其基本原理是把数字图像或数字序列中一点的值用该点的另一邻域中各点值的中值代替,让周围的像素值接近真实值,从而消除孤立的噪声点。这对于斑点噪声(speckle noise)和椒盐噪声(salt-and-pepper noise)来说尤其有用,因为它不依赖与邻域内那些与典型值差别很大的值。中值滤波器在处理连续图像窗函数时与线性滤波器的工作方式类似,但滤波过程却不再是加权运算。
  中值滤波在一定的条件下可以克服常见线性滤波器,如最小均方滤波、方框滤波器、均值滤波等带来的图像细节模糊,而且对滤除脉冲干扰及图像扫描噪声非常有效,也常用于保护边缘信息。保存边缘信息的特性使它在不希望出现边缘模糊的场合也很有用,是非常经典的平滑噪声处理方法。

  • 中值滤波与均值滤波器比较
      优势:在均值滤波器中,由于噪声成分被放入平均计算中,所以输出受到了噪声的影响。但是在中值滤波器中,由于噪声成分很难选上,所以输出受到了噪声的影响。但是在中值滤波器中,由于噪声成分很难被选上,所以几乎不会影响到输出。因此同样用3x3区域进行处理,中值滤波消除的噪声能力更胜一筹。中值滤波无论是在消除噪声还是保存边缘方面是一个不错的方法。
      劣势:中值滤波花费的时间是均值滤波的5倍以上。
      顾名思义,中值滤波选择每个像素的邻域像素中的中值作为输出,或者说中值滤波将每一像素点的灰度值设置为该点某邻域窗口内的所有像素点灰度值的中值。
      例如,取3x3的函数窗,计算以点[i,j]为中心的函数窗像素中值,具体步骤如下。
    (1) 按强度值大小排列像素点。
    (2) 选择排序像素集的中间值作为点[i,j]的新值。
      一般来用奇数点的邻域来计算中值,但像素点数为偶数时,中值就取排序像素中间两点的平均值。
      中值滤波在一定条件下,可以克服线型滤波器(入均值滤波等)所带来的图像细节模糊,对滤除脉冲干扰即图像扫描噪声最为有效,而且在实际运算过程中并不需要图像的统计特性,也给计算带来了不少方便,但是对一些细节(特别是细、尖顶等)多的图像不太适合。
6.2.3 双边滤波

   双边滤波(Bilateral filter)是一种非线性的滤波方法,是结合图像的空间邻近度和像素值相似度的一种折中处理,同时考虑空域信息和灰度相似性,达到保边去噪的目的,具有简单、非迭代、局部的特点。
  双边滤波器的好处是可以做边缘保存(edge perserving)。以往常用维纳滤波或者高斯滤波去降噪,但二者都会较明显地模糊边缘,对于高频细节的保护效果并不明显。双边滤波器顾名思义,比高斯滤波多了一个高斯方差sigma-d,它是基于空间分布的高斯滤波函数,所以在边缘附近,离得较远的像素不会对边缘上的像素值影响太多,这样就保证了边缘附近像素值额保存。但是,由于保存了过多的高频信息,对于彩色图像里的高频噪声,双边滤波器不能够干净地滤掉,只能对于低频洗洗进行较好地滤波。
  在双边滤波器中,输出像素的值依赖于邻域像素值的加权值组合,公式如下。

//引用自OpenCV3编程入门 毛星云 冷雪飞 等编著

好吧双边滤波公式也没看懂

//ver 4.1.1 中值滤波和双边滤波测试
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <iostream>
using namespace std;
using namespace cv;

Mat g_srcImage, g_dstImage1, g_dstImage2;
int g_nMedianBlurValue = 10;  //中值滤波参数值
int g_nBilateralFilterValue = 10; //双边滤波参数值
int g_nSizeFactor = 2;

static void on_MedianBlur(int, void*);  //中值滤波器
static void on_BilateralFilter(int, void*);  //双边滤波器

int main()
{
	system("color 5E");
	g_srcImage = imread("C:\\Users\\MaxLy\\Desktop\\girl.jpg", 1);
	resize(g_srcImage, g_srcImage, Size(g_srcImage.cols / g_nSizeFactor, g_srcImage.rows / g_nSizeFactor));
	if (!g_srcImage.data)
	{
		cout << "读取srcImage错误!" << endl;
		return false;
	}

	//复制原图到Mat类型中
	g_dstImage1 = g_srcImage.clone();
	g_dstImage2 = g_srcImage.clone();

	//显示原图
	namedWindow("原图窗口", 1);
	imshow("原图窗口", g_srcImage);

	namedWindow("中值滤波", 1);
	createTrackbar("参数值", "中值滤波", &g_nMedianBlurValue, 50, on_MedianBlur);
	on_MedianBlur(g_nMedianBlurValue, 0);

	namedWindow("双边滤波", 1);
	createTrackbar("参数值", "双边滤波", &g_nBilateralFilterValue, 50, on_BilateralFilter);
	on_BilateralFilter(g_nBilateralFilterValue, 0);

	while (char(waitKey(1))!='q')
	{

	}
	return 0;
}

static void on_MedianBlur(int, void*)
{
	medianBlur(g_srcImage, g_dstImage1, g_nMedianBlurValue * 2 + 1);
	imshow("中值滤波", g_dstImage1);
}

static void on_BilateralFilter(int, void*)
{
	bilateralFilter(g_srcImage, g_dstImage2, g_nBilateralFilterValue, g_nBilateralFilterValue * 2, g_nBilateralFilterValue / 2);
	imshow("双边滤波", g_dstImage2);
}

(4)中值滤波

  中值滤波是指用模版核算子覆盖区域内所有像素值的排序,位置处在中间的像素值用来更新当前像素点值。如常见的核算子3x3,模版区域内的元素有9个,排序后为a1,a2,a3,a4,a5,a6,a7,a8,a9,中值滤波是指将当前像素点的值用9个元素排序后的第5个位置像素点的值a5来代替。中值滤波在边界的保存方面优于均值滤波,是经常使用的一种滤波器,但是在模版逐渐变大时,依然会存在一定的边界模糊,画面的清晰度基本保持,中值滤波对处理椒盐噪声非常有效。中值滤波能减弱或消除傅里叶空间的高频分量,同时也影响低频分量。
  中值滤波去除噪声的效果依赖于两个要素:邻域的空间范围和中值计算中涉及的像素数。一般来说,小于滤波器面积一半的亮或暗的物体基本上会被滤除,而较大的物体几乎会原封不动地保存下来,因此中值滤波器的空间尺寸必须根据现有的问题来进行调整。中值滤波是非线型滤波,线型滤波易于发现,且易于从频率响应的角度分析,但如果噪声是颗粒噪声而非高斯噪声时,线型滤波不能去除噪声。如果图像出现极值点,线型滤波只是将噪声转换为平缓但仍可见的颗粒,最佳的解决方式是通过非线性滤波来滤除噪声。
  第三个参数ksize,代表孔径的线性尺寸,注意这个参数必须是大于1的奇数,比如3,5,7,9……

(5)双边滤波

一样没看懂

3.边缘检测

7.1.1 边缘检测的一般步骤

1.【第一步】滤波
  边缘检测的算法主要是基于图像强度的一阶和二阶导数,但导数通常对噪声很敏感,因此必须采用滤波器来干山与噪声油管的边缘检测器的性能。常见的滤波方法主要由高斯滤波,即采用离散化的高斯函数产生一组归一化的高斯核,然后基于高斯核函数对图像灰度矩阵的每一点进行加权求和。
2.【第二步】增强
  增强边缘的基础是确定图像各点邻域强度的变化值。增强算法可以将图像灰度点邻域强度值有显著变化的点凸显出来。在具体编程实现时,可通过计算梯度幅值来确定。
3.【第三步】检测
  经过增强的图像,往往邻域中有很多点的梯度值比较大,而在特定的应用中,这些点并不是要找的边缘点,所以应该采用某种方法来对这些点进行取舍。实际工程中,常用的方法是通过阈值化方法来检测。
  另外,需要注意,下文中讲到的Laplacian算子、sobel算子和Scharr算子都是带方向的,所以,示例中我们分别写了X方向、Y方向和最终合成的效果图。

7.1.2 canny算子

1.canny算子简介
  Canny边缘检测算子是John F.Canny于1986年开发出来的一个多级边缘检测算法。更为重要的是,Canny创立了边缘检测计算理论(Computational theory ofedge detection),解释了这项技术是如何工作的。Canny边缘检测算法以Canny的名字命名,被很多人推崇为当今最优的边缘检测的算法。
  其中,Canny的目标是找到一个最优的边缘检测算法,让我们看一下最优边缘检测的三个主要评价标准。

  • 低错误率:标识出尽可能多的实际边缘,同时尽可能地减少噪声产生的误报。
  • 高定位性:标识出的边缘要与图像中的实际边缘尽可能接近。
  • 最小响应:图像中的边缘只能标识一次,并且可能存在的图像噪声不应标识位边缘。
      为了满足这些要求,Canny使用了变分法,这是一种寻找满足特定功能的函数的方法。最优检测用4个指数函数项的和标识,但是它非常近似于高斯函数的一阶导数。
//ver 4.1.1 Canny测试
#include <opencv2/opencv.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <iostream>
//#include "cartoon.h"

using namespace cv;
using namespace std;

int main()
{
	//载入原始图
	Mat src = imread("C:\\Users\\MaxLy\\Desktop\\girl.jpg");
	Mat src1 = src.clone();

	//显示原始图
	imshow("原始图", src);

	//转成灰度图,降噪,用Canny,最后将得到的边缘作为掩码,拷贝原图到效果图,得到彩色的边缘图
	Mat dst, edge, gray;

	//创建于src同类型和大小的矩阵(dst)
	dst.create(src1.size(), src1.type());

	//将原图像转换为灰度图像
	cvtColor(src1, gray, COLOR_BGR2GRAY);

	//先用使用3x3内核来降噪
	blur(gray, edge, Size(3, 3));

	//运行Canny算子
	Canny(edge, edge, 3, 9, 3);

	//将dst内的所有元素设置为0
	dst = Scalar::all(0);

	//使用Canny算子输出的边缘图edge作为掩码,来将原图src1拷到目标图dst中
	src1.copyTo(dst, edge);

	//显示效果图
	imshow("效果图", dst);

	waitKey(0);

	return 0;
}

Canny()函数

  • 第三个参数,double类型的threshold1,第一个滞后性阈值。
  • 第四个参数,double类型的threshold2,第二个滞后性阈值。
  • 第五个参数,int类型的apertureSize,表示应用Sobel算子的孔径大小,其有默认值3
  • 第六个参数,bool类型的L2gradient,一个计算图像梯度幅值的标识,有默认值false,为true时更加精确。
      需要注意的是,这个函数阈值1和阈值2两者中较小的值用于边缘连接,而较大的值用来控制强边缘的初始段,推荐的高低阈值比在2:1到3:1之间。
      低于阈值1的像素点会被认为不是边缘,高于阈值2的像素点会被认为是边缘,在阈值1和阈值2之间的像素点,若与第2步得到的边缘像素点相邻,则被认为是边缘,否则被认为不是边缘。
7.1.2 sobel算子

1.sobel算子的基本概念
  Sobel算子是一个主要用于边缘检测的离散微分算子(discrete differentiation operator)。它结合了高斯平滑和微分求导,用来计算图像灰度函数的近似梯度。在图像的任何一点使用此算子,都将会产生对应的梯度矢量或是其法矢量。
(3) 第三个参数,int类型的ddepth,输出图像的深度,支持如下src.depth()和ddepth的组合;

  • 若src.depth() = CV_8U,取ddepth = -1/CV_16S/CV_32F/CV/64F
  • 若src.depth() = CV_16U/CV_16S,取ddepth = -1/CV_32F/CV_64F
  • 若src.depth() = CV_32F,取ddepth = -1/CV_32F/CV_64F
  • 若src.depth() = CV_64F,取ddepth = -1/CV_64F
    (4)第四个参数,int类型dx,x方向上的差分阶数。
    (5)第五个参数,int类型dy,y方向上的差分阶数。
    (6)第六个参数,int类型ksize,有默认值3,表示Sobel核的大小,必须取1、3、5或7.
    (7)第七个参数,double类型scale,计算导数值时可选的缩放因子,默认值是1,表示默认情况下是没有应用缩放的。可以在文档中查阅getDerivKernels的相关介绍,来得到这个系数的更多信息。
    (8)第八个参数,double类型的delta,表示在结果存入目标图(第二个参数dst)之前可选的delta值,有默认值0.
    (9)第九个参数,int类型的borderType,边界模式,默认值为BORDER_DEFAULT。这个参数可以在官方文档中borderInterpolate处得到更详细的信息。
      一般情况下,都是用ksize*ksize内核来计算导数的。然而,有一种特殊情况——当ksize为1时,往往会使用3x1或者1x3的内核。且这种情况下,并没有进行高斯平滑的操作。
//ver 4.1.1 Sobel测试
#include <opencv2/opencv.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <iostream>
//#include "cartoon.h"

using namespace cv;
using namespace std;

int main()
{
	//载入原始图
	Mat src = imread("C:\\Users\\MaxLy\\Desktop\\girl.jpg");
	resize(src, src, Size(src.cols / 3, src.rows / 3));
	Mat grad_x, grad_y;
	Mat abs_grad_x, abs_grad_y, dst;

	//显示原始图
	imshow("原始图", src);

	//求x方向梯度
	Sobel(src, grad_x, CV_16S, 1, 0, 3, 1, 1, BORDER_DEFAULT);
	convertScaleAbs(grad_x, abs_grad_x);
	imshow("x方向", abs_grad_x);

	//求y方向梯度
	Sobel(src, grad_y, CV_16S, 0, 1, 3, 1, 1, BORDER_DEFAULT);
	convertScaleAbs(grad_y,abs_grad_y);
	imshow("y方向", abs_grad_y);

	//合并梯度(近似)
	addWeighted(abs_grad_x, 0.5, abs_grad_y, 0.5, 0, dst);
	imshow("整体方向", dst);

	waitKey(0);

	return 0;
}
7.1.4 Laplacian 算子
  1. 计算拉普拉斯变换:Laplacian()函数
      Laplacian函数可以计算出图像经过拉普拉斯变换后的结果。
  • 第三个参数,int类型的ddept,目标图像的深度。
  • 第四个参数,int类型的ksize,用于计算二阶导数的滤波器的孔径尺寸,大小必须为正奇数,且有默认值1.
  • 第五个参数,double类型的scale,计算拉普拉斯值的时候可选的比例因子,有默认值1.
  • 第六个参数,double类型的delta,表示在结果存入目标图(第二个参数dst)之前可选的delta值,有默认值0.
  • 第七个参数,int类型的borderType,边界模式,默认值为BORDER_DEFAULT。这个参数可以在官方文档中borderInterpolate()处得到更详细的的信息。
//ver 4.1.1 Laplacian测试
#include <opencv2/opencv.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <iostream>
//#include "cartoon.h"

using namespace cv;
using namespace std;

int main()
{
	//载入原始图
	Mat src = imread("C:\\Users\\MaxLy\\Desktop\\girl.jpg");
	resize(src, src, Size(src.cols / 3, src.rows / 3));
	Mat src_gray, dst, abs_dst;
	
	//显示原始图
	imshow("原始图", src);

	//使用高斯滤波消除噪声
	GaussianBlur(src, src, Size(3, 3), 0, 0, BORDER_DEFAULT);

	//转换为灰度图
	cvtColor(src, src_gray, COLOR_RGB2GRAY);

	//使用Laplace函数
	Laplacian(src_gray, dst, CV_16S, 3, 1, 0, BORDER_DEFAULT);

	//计算绝对值,并将结果转换成8位
	convertScaleAbs(dst, abs_dst);

	//显示效果图
	imshow("效果图", abs_dst);

	waitKey(0);

	return 0;
}
7.1.5 scharr滤波器

  我们一般直接称scharr为滤波器,而不是算子。上文已经讲到,它在openCV中主要是配合Sobel算子的运算而存在的。下面让我们直接来看看其函数讲解。
1.计算图像差分:Scharr()函数
  使用Scharr滤波器运算符计算x或y方向的图像差分。其实它的参数变量和Sobel基本上是一样的,除了没有ksize核的大小。
(3) 第三个参数,int类型的ddepth,输出图像的深度,支持如下src.depth()和ddepth的组合。

  • 若src.depth() = CV_8U,取ddepth = -1/CV_16S/CV_32F/CV_64F
  • 若src.depth() = CV_16U/CV_16S。取ddepth=-1/CV_32F/CV_64F
  • 若src.depth() =CV_32F,取ddepth = -1/CV_32F/CV_64F
  • 若src.depth() = CV_64F,取ddepth =-1/CV_64F
    (4)第四个参数,int类型dx,x方向上的差分阶数、
    (5)第五个参数,int类型dy,y方向上的差分阶数。
    (6)第六个参数,double类型的scale,计算导数值时可选的缩放因子,默认值是1,表示默认情况下是没有应用缩放的。
    (7)第七个参数,double类型的delta,表示在结果存入目标图(第二个参数dst)之前可选的delta值,有默认值0.
    (8)第八个参数,int类型的borderType,边界模式,默认值为BORDER_DEFAULT。这个参数可以在官方文档中borderInterpolate处得到更详细的信息。
      不难理解,如下两者是等价的,即:
      Scharr(src,dst,ddepth,dx,dy,scale,delta,borderType);
      与
      Sobel(src,dst,ddepth,dx,dy,CV_SCHARR,scale,delta,borderType);
//ver 4.1.1 Scharr测试
#include <opencv2/opencv.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <iostream>
//#include "cartoon.h"

using namespace cv;
using namespace std;

int main()
{
   //载入原始图
   Mat src = imread("C:\\Users\\MaxLy\\Desktop\\girl.jpg");
   resize(src, src, Size(src.cols / 3, src.rows / 3));

   Mat grad_x, grad_y;
   Mat abs_grad_x, abs_grad_y, dst;

   //显示原始图
   imshow("原始图", src);

   //求X方向梯度
   Scharr(src, grad_x, CV_16S, 1, 0, 1, 0, BORDER_DEFAULT);
   convertScaleAbs(grad_x, abs_grad_x);
   imshow("X方向Scharr", abs_grad_x);

   //求Y方向梯度
   Scharr(src, grad_y, CV_16S, 0, 1, 1, 0, BORDER_DEFAULT);
   convertScaleAbs(grad_y, abs_grad_y);
   imshow("Y方向Scharr", abs_grad_y);

   //合并梯度(近似)
   addWeighted(abs_grad_x, 0.5, abs_grad_y, 0.5, 0, dst);

   //显示效果图
   imshow("合并梯度后Scharr", dst);

   waitKey(0);

   return 0;
}

综合实例:边缘检测

//ver 4.1.1 综合测试
#include <opencv2/opencv.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <iostream>
//#include "cartoon.h"

using namespace cv;
using namespace std;

Mat g_srcImage, g_srcGrayImage, g_dstImage;

//Canny边缘检测相关变量
Mat g_cannyDetectedEdges;
int g_cannyLowThreshold = 1;//TrackBar 位置参数

//Sobel 边缘检测相关变量
Mat g_sobelGradient_X, g_sobelGradient_y;
Mat g_sobelAbsGradient_X, g_sobelAbsGradient_Y;
int g_sobelKernelSize = 1;//TrackBar 位置参数

//Scharr滤波器相关变量
Mat g_scharrGradient_X, g_scharrGradient_Y;
Mat g_scharrAbsGradient_X, g_scharrAbsGradient_Y;

static void on_Canny(int, void*);//Canny边缘检测窗口滚动条的回调函数
static void on_Sobel(int, void*);//Sobel边缘检测窗口滚动条的回调函数
void Scharr(); //封装了Scharr边缘检测相关代码的函数

int main()
{
   system("color 2F");

   //载入原始图
   g_srcImage = imread("C:\\Users\\MaxLy\\Desktop\\girl.jpg");
   resize(g_srcImage, g_srcImage, Size(g_srcImage.cols / 3, g_srcImage.rows / 3));
   if (!g_srcImage.data)
   {
   	cout << "读取srcImage错误" << endl;
   	return false;
   }

   //显示原始图
   namedWindow("原始图");
   imshow("原始图", g_srcImage);

   g_dstImage.create(g_srcImage.size(), g_srcImage.type());

   //将原图像转换为灰度图像
   cvtColor(g_srcImage, g_srcGrayImage, COLOR_BGR2GRAY);

   //创建显示窗口
   namedWindow("Canny边缘检测", WINDOW_AUTOSIZE);
   namedWindow("Sobel边缘检测", WINDOW_AUTOSIZE); 

   //创建trackbar
   createTrackbar("参数值", "Canny边缘检测", &g_cannyLowThreshold, 120, on_Canny);
   createTrackbar("参数值", "Sobel边缘检测", &g_sobelKernelSize, 3, on_Sobel);

   //调用回调函数
   on_Canny(0, 0);
   on_Sobel(0, 0);

   //调用并封装了Scharr边缘检测代码的函数
   Scharr();

   while (char(waitKey(1)!='q'))
   {

   }

   waitKey(0);
   return 0;
}

void on_Canny(int, void*)
{
   //先使用3x3内核来降噪
   blur(g_srcGrayImage, g_cannyDetectedEdges, Size(3, 3));

   //运行我们的Canny算子
   Canny(g_cannyDetectedEdges, g_cannyDetectedEdges, g_cannyLowThreshold, g_cannyLowThreshold * 3, 3);

   //先将g_dstImage内的所有元素设置为0
   g_dstImage = Scalar::all(0);

   //使用Canny算子输出的边缘图g_cannyDetectedEdges作为掩码,来将原图g_srcImage拷到目标图g_dstImage中
   g_srcImage.copyTo(g_dstImage, g_cannyDetectedEdges);

   //显示效果图
   imshow("Canny边缘检测", g_dstImage);
}

void on_Sobel(int, void*)
{
   //求X方向梯度
   Sobel(g_srcImage, g_sobelGradient_X, CV_16S,1, 0, (2 * g_sobelKernelSize+1), 1, 1, BORDER_DEFAULT);
   convertScaleAbs(g_sobelGradient_X, g_sobelAbsGradient_X);//计算绝对值,并将结果转换成8位

   //求Y方向梯度
   Sobel(g_srcImage, g_sobelGradient_y, CV_16S, 0, 1, (2 * g_sobelKernelSize + 1), 1, 1, BORDER_DEFAULT);
   convertScaleAbs(g_sobelGradient_y, g_sobelAbsGradient_Y);//计算绝对值,并将结果转换成8位

   //合并梯度
   addWeighted(g_sobelAbsGradient_X, 0.5, g_sobelAbsGradient_Y, 0.5, 0, g_dstImage);

   //显示效果图
   imshow("Sobel边缘检测", g_dstImage);
}

void Scharr()
{
   //求x方向梯度
   Scharr(g_srcImage, g_scharrGradient_X, CV_16S, 1, 0, 1, 0, BORDER_DEFAULT);
   convertScaleAbs(g_scharrGradient_X, g_scharrAbsGradient_X);//计算绝对值,并将结果转换成8位

   //求Y方向梯度
   Scharr(g_srcImage, g_scharrAbsGradient_Y, CV_16S, 0, 1, 1, 0, BORDER_DEFAULT);
   convertScaleAbs(g_sobelAbsGradient_Y, g_scharrAbsGradient_Y);//计算绝对值,并将结果转换成8位

   //合并梯度
   addWeighted(g_scharrAbsGradient_X, 0.5, g_scharrAbsGradient_Y, 0.5, 0, g_dstImage);

   //显示效果图
   imshow("Scharr滤波器", g_dstImage);
}

4.阈值

6.7.1 固定阈值操作:Threshold()函数

  函数Threshold()对单通道数组应用固定阈值操作。该函数的典型应用是对灰度图像进行阈值操作得到二值图像,(compare()函数也可以达到此目的)或者是去掉噪声,例如过滤很小或很大象素值的图像点。

  • 第三个参数,double类型的thresh,阈值的具体值。
  • 第四个参数,double类型的maxval,当第五个参数阈值类型type取CV_THRESH_BINARY或CV_THRESH_BINARY_INV时阈值类型时的最大值。
  • 第五个参数,int类型的type,阈值类型。threshold()函数支持的对图像取阈值的方法由其确定。
//ver 4.1.1 阈值操作实例
#include <opencv2/opencv.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <iostream>
//#include "cartoon.h"

using namespace cv;
using namespace std;

#define WINDOW_NAME "程序窗口"  //为窗口标题定义的宏

int g_nThresholdValue = 100;
int g_nThresholdType = 3;
Mat g_srcImage, g_grayImage, g_dstImage;

void on_Threshold(int, void*);  //回调函数

int main()
{
	g_srcImage = imread("C:\\Users\\MaxLy\\Desktop\\girl.jpg");
	resize(g_srcImage, g_srcImage, Size(g_srcImage.cols / 2, g_srcImage.rows / 2));
	if (!g_srcImage.data)
	{
		cout << "图片读取错误" << endl;
		return false;
	}

	//存留一份原图的灰度图
	cvtColor(g_srcImage, g_grayImage, COLOR_RGB2GRAY);

	//创建窗口并显示原始图
	namedWindow(WINDOW_NAME, WINDOW_AUTOSIZE);

	createTrackbar("模式", WINDOW_NAME, &g_nThresholdType, 4, on_Threshold);
	createTrackbar("参数值", WINDOW_NAME, &g_nThresholdValue, 255, on_Threshold);

	//初始化自定义的阈值回调函数
	on_Threshold(0, 0);
	
	while (char(waitKey(20)!=27))
	{

	}
}

void on_Threshold(int, void*)
{
	threshold(g_grayImage, g_dstImage, g_nThresholdValue, 255, g_nThresholdType);

	//更新效果图
	imshow(WINDOW_NAME, g_dstImage);
}

自己总结一下第二部分生成黑白轮廓的原理:

  1. 读取摄像头视频帧
  2. 准备边缘检测三部曲
  3. 转换成灰度图(因为滤波用灰度图)
  4. 使用medianBlur中值滤波,为了削弱噪声
  5. 使用Laplacian算子
  6. 使用阈值化再次加强边缘检测的效果

第三部分:生成彩色卡通效果

//ver 4.1.1
#include <opencv2/opencv.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <iostream>
//#include "cartoon.h"

using namespace cv;
using namespace std;


void cartoonifyImage(Mat srcColor, Mat dst);

int main(int argc,char *argv[],char **env)
{
	//cv::Mat srcImage = imread("C:\\Users\\MaxLy\\Desktop\\girl.jpg");
	//resize(srcImage, srcImage, Size(srcImage.cols / 3, srcImage.rows / 3));


	int cameraNumber = 0;
	if (argc > 1)
		cameraNumber = atoi(argv[1]);//字符串转换成整型

	//Get access to the camera.
	cv::VideoCapture camera;
	camera.open(cameraNumber);
	if (!camera.isOpened())
	{
		std:; cerr << "ERROR: Count not access the camera or video " << std::endl;
		exit(1);
	}

	//Try to set the camera resolution
	//camera.set(cv::CV_CAP_PROP_FRAME_WIDTH, 640);
	//camera.set(cv::CV__CAP_PROP_FRAME_HEIGHT, 480);

	while (true)
	{
		//Grab the next camera frame.
		cv::Mat cameraFrame;
		camera >> cameraFrame;
		if (cameraFrame.empty())
		{
			std::cerr << "ERROR: Couldn't grab a camera frame." << std::endl;
			exit(0);
		}
		//Create a blank output image,that we will draw noto.
		cv::Mat displayedFrame(cameraFrame.size(),CV_8UC3);

		//Run the cartoonifier filter on the camera frame.
		cartoonifyImage(cameraFrame, displayedFrame);

		if (displayedFrame.empty())
		{
			std::cerr << "ERROR displayedFrame is empty" << std::endl;
			exit(0);
		}
	
		//Display the processed image onto the screen.
		imshow("Cartoonifier", displayedFrame);

		//IMPORTANT:Wait for at least 20 milliseconds,
		//so that the image can be displayed on the screen!
		//Also checks if a key was pressed in the GUI window.
		//Note that it should be a "char" to support Linux.
		char keypress = cv::waitKey(100);
		if (keypress == 27)  //Escape Key
		{
			//Quit the program !
			break;
		}
	}//end while
	return 0;
}

void cartoonifyImage(Mat srcColor, Mat dst)
{
	Mat srcGray;
	cvtColor(srcColor, srcGray, COLOR_BGR2GRAY);    //将原图转换为灰度图

	const int MEDIAN_BLUR_FILTER_SIZE = 7;
	medianBlur(srcGray, srcGray, MEDIAN_BLUR_FILTER_SIZE);    //将灰度图进行中值滤波,消除噪点

	Size smallSize(srcColor.size().width/2,srcColor.size().height/2);
	Mat smallImg = Mat(smallSize, CV_8UC3);                 //创建低分辨率副本
	resize(srcColor, smallImg, smallSize, 0, 0, INTER_LINEAR);   //降低副本的分辨率

	Mat tmp = Mat(smallSize, CV_8UC3);    // tmp用来存储双边滤波的中间值,双边滤波的前两个参数不能一样。
	int repetitions = 4;//Repetitions for strong cartoon effect.
	for (int i = 0; i < repetitions; ++i)
	{
		int ksize = 9; //Filter size.Has a large effect on speed.
		double sigmaSpace = 7;  //Spatial strength.Affects speed.
		double sigmaColor = 9;  //Filter color strength.
		bilateralFilter(smallImg, tmp, ksize, sigmaColor, sigmaSpace);
		bilateralFilter(tmp, smallImg, ksize, sigmaColor, sigmaSpace);
	}     //将低分辨率副本多次进行双边滤波

	Mat edges = Mat(srcColor.size(), CV_8U);           //边缘图
	Mat mask = Mat(srcColor.size(), CV_8U);            //掩模图

	const int LAPLACIAN_FILTER_SIZE = 5;
	Laplacian(srcGray, edges, CV_8U, LAPLACIAN_FILTER_SIZE);   //将灰度图进行Laplacian边缘检测,将结果存入edges
	const int EDGES_THRESHOLD = 80;
	
	threshold(edges, mask, EDGES_THRESHOLD, 255, THRESH_BINARY_INV);   //将原本黑色和白色反转,并增强边缘效果

	resize(smallImg, smallImg, srcColor.size(), 0, 0, INTER_LINEAR);
	dst.setTo(0);    //将目标图初始化为全黑色,用来靠掩模描出黑边
	smallImg.copyTo(dst, mask); //掩模原理 https://blog.csdn.net/qq_38392229/article/details/92584652
}

1.像素压缩优化

  其实这个部分的原理就是1处理原图片,获得轮廓,2再将原图片模糊化与轮廓图叠加,就能出来卡通效果。但是因为卡通效果的图片不需要有很高的分辨率,所以可以将第2部分的图片降低分辨率再去模糊与轮廓图叠加,以加快运算速度。
  用resize改变图片的分辨率。

2.用掩模覆盖

总结下全部过程

  • 将原图转成灰度图,做中值滤波,以消除噪点
  • 创建低分辨率副本,多次进行双边滤波,为了消除图像边缘内部的颗粒,模拟卡通的画风
  • 将原图的灰度图进行Laplacian边缘检测,获得边缘图(边缘为白色,中间为黑色)
  • 将边缘图中的黑白反转,作为掩模
  • 将低分辨率副本放大,通过掩模拷贝给目标图。
    这样因为边缘图的边缘为黑色,所以掩模不会将同位置的低分辨率部分的彩色传给目标图,这样目标图中相同位置的初始黑色不会被修改,就形成了边缘黑边的效果。
    中间一些算法API的参数,就调参了。

第四部分:生成魔鬼效果

  翻译自原文

  图片和动漫总是有正面角色和反面角色。通过边缘检测的正确组合,就可以从大多数可爱的人身上穿件一个恐怖的图像!技巧就是用一个小型的边缘检测找一堆图片中的边缘,再对这些边缘使用中值滤波。
  我们得先对一个灰度进行降噪,因此之前用来将原图转为灰度图和使用7x7的核来中值滤波的代码可以拿来重用。不用Laplacian滤波和二分阈值化,只需用3x3的Scharr渐变滤波器对x和y处理,再应用一个临界值很低的二分阈值和3x3的中值滤波,就可以创建最终的魔鬼面具

  引用《Mastering OpenCV with Practical Computer Vision Projects》
  不打算怎么琢磨,因为效果实在是看不出哪里“魔鬼”了。

void evilImage(Mat srcColor, Mat dst)
{
	// Convert from BGR color to Grayscale
	Mat srcGray;
	cvtColor(srcColor, srcGray, COLOR_BGR2GRAY);

	// Remove the pixel noise with a good Median filter, before we start detecting edges.
	medianBlur(srcGray, srcGray, 7);

	Size size = srcColor.size();
	Mat mask = Mat(size, CV_8U);
	Mat edges = Mat(size, CV_8U);

	// Evil mode, making everything look like a scary bad guy.
	// (Where "srcGray" is the original grayscale image plus a medianBlur of size 7x7).
	Mat edges2;
	Scharr(srcGray, edges, CV_8U, 1, 0);
	Scharr(srcGray, edges2, CV_8U, 1, 0, -1);
	edges += edges2;
	threshold(edges, mask, 12, 255, THRESH_BINARY_INV);
	medianBlur(mask, mask, 3);

	// Do the bilateral filtering at a shrunken scale, since it
	// runs so slowly but doesn't need full resolution for a good effect.
	Size smallSize(size.width / 2, size.height / 2);
	Mat smallImg = Mat(smallSize, CV_8UC3);
	resize(srcColor, smallImg, smallSize, 0, 0, INTER_LINEAR);

	// Perform many iterations of weak bilateral filtering, to enhance the edges
	// while blurring the flat regions, like a cartoon.
	Mat tmp = Mat(smallSize, CV_8UC3);
	int repetitions = 7;        // Repetitions for strong cartoon effect.
	for (int i = 0; i < repetitions; i++)
	{
		int size = 9;           // Filter size. Has a large effect on speed.
		double sigmaColor = 9;  // Filter color strength.
		double sigmaSpace = 7;  // Positional strength. Effects speed.
		bilateralFilter(smallImg, tmp, size, sigmaColor, sigmaSpace);
		bilateralFilter(tmp, smallImg, size, sigmaColor, sigmaSpace);
	}
	// Go back to the original scale.
	resize(smallImg, srcColor, size, 0, 0, INTER_LINEAR);

	// Clear the output image to black, so that the cartoon line drawings will be black (ie: not drawn).
	dst.setTo(0);

	// Use the blurry cartoon image, except for the strong edges that we will leave black.
	srcColor.copyTo(dst, mask);
}

第五部分:使用皮肤检测实现外星人模式

  翻译自原文

  通过检测脸的皮肤区域再修改肤色为绿色

皮肤检测算法

用很多检测皮肤区域的技术,从简单的使用RGB或者HSV或者颜色直方图计算和再投影的颜色阈值到复杂的机器学习算法去跑大量的脸部数据。然而,即使是最麻烦的手段也并不一定准确,特别是受肤色,光线和各种摄像头的影响。因为我们不想在手机上用机器学习,所以我们将问题简单化。然而,颜色反馈在不同的设备中区别很大,因此我们需要比简单的颜色阈值更鲁棒的方法。
  比如,简单的HSV皮肤检测器可以处理每个像素的特征,但是移动设备摄像头通常白平衡很差,因此一个人的皮肤可能有些蓝而不是红,等等,这是简单HSV阈值判定的主要问题。
  一个更鲁棒的方法是用Haar或者LBP做面部检测器,接着检查颜色范围。你可以接着扫描整个图像或面部中心附近附近区域的类似图像。好处是极有可能找到真正的一部分皮肤区域,即使他的皮肤偏蓝。
  不幸的是,cascade classifier面部检测在移动设备上速度比较慢,因此这个方法可能在实时设备上不是很理想。另一方面,我们可以假定移动设备的摄像头是对准人物的,所以可以要求用户将他们的面部放在一定区域和距离上,而不是检测他们面部位置和大小。

  引用《Mastering OpenCV with Practical Computer Vision Projects》
  原理就是让用户把脸放到相应位置,然后对那6个固定位置使用漫水填充。

//ver 4.1.1
#include <opencv2/opencv.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <iostream>
//#include "cartoon.h"

using namespace cv;
using namespace std;


void cartoonifyImage(Mat srcColor, Mat dst);
void alienMode(Mat srcColor, Mat dst);
void removePepperNoise(Mat& mask);
void drawFaceStickFigure(Mat dst);
void changeFacialSkinColor(Mat smallImgBGR, Mat bigEdges, int debugType);

int main(int argc,char *argv[],char **env)
{
	//cv::Mat srcImage = imread("C:\\Users\\MaxLy\\Desktop\\girl.jpg");
	//resize(srcImage, srcImage, Size(srcImage.cols / 3, srcImage.rows / 3));


	int cameraNumber = 0;
	if (argc > 1)
		cameraNumber = atoi(argv[1]);//字符串转换成整型

	//Get access to the camera.
	cv::VideoCapture camera;
	camera.open(cameraNumber);
	if (!camera.isOpened())
	{
		std:; cerr << "ERROR: Count not access the camera or video " << std::endl;
		exit(1);
	}

	//Try to set the camera resolution
	//camera.set(cv::CV_CAP_PROP_FRAME_WIDTH, 640);
	//camera.set(cv::CV__CAP_PROP_FRAME_HEIGHT, 480);

	while (true)
	{
		//Grab the next camera frame.
		cv::Mat cameraFrame;
		camera >> cameraFrame;
		if (cameraFrame.empty())
		{
			std::cerr << "ERROR: Couldn't grab a camera frame." << std::endl;
			exit(0);
		}
		//Create a blank output image,that we will draw noto.
		cv::Mat displayedFrame(cameraFrame.size(),CV_8UC3);

		//Run the cartoonifier filter on the camera frame.
		//cartoonifyImage(cameraFrame, displayedFrame);
		//evilImage(cameraFrame, displayedFrame);
		alienMode(cameraFrame, displayedFrame);

		if (displayedFrame.empty())
		{
			std::cerr << "ERROR displayedFrame is empty" << std::endl;
			exit(0);
		}
	
		//Display the processed image onto the screen.
		imshow("Cartoonifier", displayedFrame);

		//IMPORTANT:Wait for at least 20 milliseconds,
		//so that the image can be displayed on the screen!
		//Also checks if a key was pressed in the GUI window.
		//Note that it should be a "char" to support Linux.
		char keypress = cv::waitKey(100);
		if (keypress == 27)  //Escape Key
		{
			//Quit the program !
			break;
		}
	}//end while
	return 0;
}

// Apply an "alien" filter, when given a shrunken BGR image and the full-res edge mask.
// Detects the color of the pixels in the middle of the image, then changes the color of that region to green.
void changeFacialSkinColor(Mat smallImgBGR, Mat bigEdges, int debugType)
{
	// Convert to Y'CrCb color-space, since it is better for skin detection and color adjustment.
	Mat yuv = Mat(smallImgBGR.size(), CV_8UC3);
	cvtColor(smallImgBGR, yuv, COLOR_BGR2YCrCb);

	// The floodFill mask has to be 2 pixels wider and 2 pixels taller than the small image.
	// The edge mask is the full src image size, so we will shrink it to the small size,
	// storing into the floodFill mask data.
	int sw = smallImgBGR.cols;
	int sh = smallImgBGR.rows;
	Mat maskPlusBorder = Mat::zeros(sh + 2, sw + 2, CV_8U);
	Mat mask = maskPlusBorder(Rect(1, 1, sw, sh));  // mask is a ROI in maskPlusBorder.
	resize(bigEdges, mask, smallImgBGR.size());

	// Make the mask values just 0 or 255, to remove weak edges.
	threshold(mask, mask, 80, 255, THRESH_BINARY);
	// Connect the edges together, if there was a pixel gap between them.
	dilate(mask, mask, Mat());
	erode(mask, mask, Mat());
	//imshow("constraints for floodFill", mask);

	// YCrCb Skin detector and color changer using multiple flood fills into a mask.
	// Apply flood fill on many points around the face, to cover different shades & colors of the face.
	// Note that these values are dependent on the face outline, drawn in drawFaceStickFigure().
	int const NUM_SKIN_POINTS = 6;
	Point skinPts[NUM_SKIN_POINTS];
	skinPts[0] = Point(sw / 2, sh / 2 - sh / 6);
	skinPts[1] = Point(sw / 2 - sw / 11, sh / 2 - sh / 6);
	skinPts[2] = Point(sw / 2 + sw / 11, sh / 2 - sh / 6);
	skinPts[3] = Point(sw / 2, sh / 2 + sh / 16);
	skinPts[4] = Point(sw / 2 - sw / 9, sh / 2 + sh / 16);
	skinPts[5] = Point(sw / 2 + sw / 9, sh / 2 + sh / 16);
	// Skin might be fairly dark, or slightly less colorful.
	// Skin might be very bright, or slightly more colorful but not much more blue.
	const int LOWER_Y = 60;
	const int UPPER_Y = 80;
	const int LOWER_Cr = 25;
	const int UPPER_Cr = 15;
	const int LOWER_Cb = 20;
	const int UPPER_Cb = 15;
	Scalar lowerDiff = Scalar(LOWER_Y, LOWER_Cr, LOWER_Cb);
	Scalar upperDiff = Scalar(UPPER_Y, UPPER_Cr, UPPER_Cb);
	// Instead of drawing into the "yuv" image, just draw 1's into the "maskPlusBorder" image, so we can apply it later.
	// The "maskPlusBorder" is initialized with the edges, because floodFill() will not go across non-zero mask pixels.
	Mat edgeMask = mask.clone();    // Keep an duplicate copy of the edge mask.
	for (int i = 0; i < NUM_SKIN_POINTS; i++) {
		// Use the floodFill() mode that stores to an external mask, instead of the input image.
		const int flags = 4 | FLOODFILL_FIXED_RANGE | FLOODFILL_MASK_ONLY;
		floodFill(yuv, maskPlusBorder, skinPts[i], Scalar(), NULL, lowerDiff, upperDiff, flags);
		if (debugType >= 1)
			circle(smallImgBGR, skinPts[i], 5, CV_RGB(0, 0, 255), 1);
	}
	if (debugType >= 2)
		imshow("flood mask", mask * 120); // Draw the edges as white and the skin region as grey.

	// After the flood fill, "mask" contains both edges and skin pixels, whereas
	// "edgeMask" just contains edges. So to get just the skin pixels, we can remove the edges from it.
	mask -= edgeMask;
	// "mask" now just contains 1's in the skin pixels and 0's for non-skin pixels.

	// Change the color of the skin pixels in the given BGR image.
	int Red = 0;
	int Green = 70;
	int Blue = 0;
	add(smallImgBGR, Scalar(Blue, Green, Red), smallImgBGR, mask);
}

// Remove black dots (upto 4x4 in size) of noise from a pure black & white image.
// ie: The input image should be mostly white (255) and just contains some black (0) noise
// in addition to the black (0) edges.
void removePepperNoise(Mat& mask)
{
	// For simplicity, ignore the top & bottom row border.
	for (int y = 2; y < mask.rows - 2; y++) {
		// Get access to each of the 5 rows near this pixel.
		uchar* pThis = mask.ptr(y);
		uchar* pUp1 = mask.ptr(y - 1);
		uchar* pUp2 = mask.ptr(y - 2);
		uchar* pDown1 = mask.ptr(y + 1);
		uchar* pDown2 = mask.ptr(y + 2);

		// For simplicity, ignore the left & right row border.
		pThis += 2;
		pUp1 += 2;
		pUp2 += 2;
		pDown1 += 2;
		pDown2 += 2;
		for (int x = 2; x < mask.cols - 2; x++) {
			uchar v = *pThis;   // Get the current pixel value (either 0 or 255).
			// If the current pixel is black, but all the pixels on the 2-pixel-radius-border are white
			// (ie: it is a small island of black pixels, surrounded by white), then delete that island.
			if (v == 0) {
				bool allAbove = *(pUp2 - 2) && *(pUp2 - 1) && *(pUp2) && *(pUp2 + 1) && *(pUp2 + 2);
				bool allLeft = *(pUp1 - 2) && *(pThis - 2) && *(pDown1 - 2);
				bool allBelow = *(pDown2 - 2) && *(pDown2 - 1) && *(pDown2) && *(pDown2 + 1) && *(pDown2 + 2);
				bool allRight = *(pUp1 + 2) && *(pThis + 2) && *(pDown1 + 2);
				bool surroundings = allAbove && allLeft && allBelow && allRight;
				if (surroundings == true) {
					// Fill the whole 5x5 block as white. Since we know the 5x5 borders
					// are already white, just need to fill the 3x3 inner region.
					*(pUp1 - 1) = 255;
					*(pUp1 + 0) = 255;
					*(pUp1 + 1) = 255;
					*(pThis - 1) = 255;
					*(pThis + 0) = 255;
					*(pThis + 1) = 255;
					*(pDown1 - 1) = 255;
					*(pDown1 + 0) = 255;
					*(pDown1 + 1) = 255;
				}
				// Since we just covered the whole 5x5 block with white, we know the next 2 pixels
				// won't be black, so skip the next 2 pixels on the right.
				pThis += 2;
				pUp1 += 2;
				pUp2 += 2;
				pDown1 += 2;
				pDown2 += 2;
			}
			// Move to the next pixel.
			pThis++;
			pUp1++;
			pUp2++;
			pDown1++;
			pDown2++;
		}
	}
}


// Draw an anti-aliased face outline, so the user knows where to put their face.
// Note that the skin detector for "alien" mode uses points around the face based on the face
// dimensions shown by this function.
void drawFaceStickFigure(Mat dst)
{
	Size size = dst.size();
	int sw = size.width;
	int sh = size.height;

	// Draw the face onto a color image with black background.
	Mat faceOutline = Mat::zeros(size, CV_8UC3);
	Scalar color = CV_RGB(255, 255, 0);   // Yellow
	int thickness = 4;
	// Use 70% of the screen height as the face height.
	int faceH = sh / 2 * 70 / 100;  // "faceH" is actually half the face height (ie: radius of the ellipse).
	// Scale the width to be the same nice shape for any screen width (based on screen height).
	int faceW = faceH * 72 / 100; // Use a face with an aspect ratio of 0.72
	// Draw the face outline.
	ellipse(faceOutline, Point(sw / 2, sh / 2), Size(faceW, faceH), 0, 0, 360, color, thickness);
	// Draw the eye outlines, as 2 half ellipses.
	int eyeW = faceW * 23 / 100;
	int eyeH = faceH * 11 / 100;
	int eyeX = faceW * 48 / 100;
	int eyeY = faceH * 13 / 100;
	// Set the angle and shift for the eye half ellipses.
	int eyeA = 15; // angle in degrees.
	int eyeYshift = 11;
	// Draw the top of the right eye.
	ellipse(faceOutline, Point(sw / 2 - eyeX, sh / 2 - eyeY), Size(eyeW, eyeH), 0, 180 + eyeA, 360 - eyeA, color, thickness);
	// Draw the bottom of the right eye.
	ellipse(faceOutline, Point(sw / 2 - eyeX, sh / 2 - eyeY - eyeYshift), Size(eyeW, eyeH), 0, 0 + eyeA, 180 - eyeA, color, thickness);
	// Draw the top of the left eye.
	ellipse(faceOutline, Point(sw / 2 + eyeX, sh / 2 - eyeY), Size(eyeW, eyeH), 0, 180 + eyeA, 360 - eyeA, color, thickness);
	// Draw the bottom of the left eye.
	ellipse(faceOutline, Point(sw / 2 + eyeX, sh / 2 - eyeY - eyeYshift), Size(eyeW, eyeH), 0, 0 + eyeA, 180 - eyeA, color, thickness);

	// Draw the bottom lip of the mouth.
	int mouthY = faceH * 53 / 100;
	int mouthW = faceW * 45 / 100;
	int mouthH = faceH * 6 / 100;
	ellipse(faceOutline, Point(sw / 2, sh / 2 + mouthY), Size(mouthW, mouthH), 0, 0, 180, color, thickness);

	// Draw anti-aliased text.
	int fontFace = FONT_HERSHEY_COMPLEX;
	float fontScale = 1.0f;
	int fontThickness = 2;
	putText(faceOutline, "Put your face here", Point(sw * 23 / 100, sh * 10 / 100), fontFace, fontScale, color, fontThickness);
	imshow("faceOutline", faceOutline);

	// Overlay the outline with alpha blending.
	addWeighted(dst, 1.0, faceOutline, 0.7, 0, dst, CV_8UC3);
}

void alienMode(Mat srcColor, Mat dst)
{
	// Convert from BGR color to Grayscale
	Mat srcGray;
	cvtColor(srcColor, srcGray, COLOR_BGR2GRAY);

	// Remove the pixel noise with a good Median filter, before we start detecting edges.
	medianBlur(srcGray, srcGray, 7);

	Size size = srcColor.size();
	Mat mask = Mat(size, CV_8U);
	Mat edges = Mat(size, CV_8U);
	// Generate a nice edge mask, similar to a pencil line drawing.
	Laplacian(srcGray, edges, CV_8U, 5);
	threshold(edges, mask, 80, 255, THRESH_BINARY_INV);
	// Mobile cameras usually have lots of noise, so remove small
	// dots of black noise from the black & white edge mask.
	removePepperNoise(mask);
	//imshow("edges", edges);
	//imshow("mask", mask);

	// Do the bilateral filtering at a shrunken scale, since it
	// runs so slowly but doesn't need full resolution for a good effect.
	Size smallSize;
	smallSize.width = size.width / 2;
	smallSize.height = size.height / 2;
	Mat smallImg = Mat(smallSize, CV_8UC3);
	resize(srcColor, smallImg, smallSize, 0, 0, INTER_LINEAR);

	// Perform many iterations of weak bilateral filtering, to enhance the edges
	// while blurring the flat regions, like a cartoon.
	Mat tmp = Mat(smallSize, CV_8UC3);
	int repetitions = 7;        // Repetitions for strong cartoon effect.
	for (int i = 0; i < repetitions; i++) {
		int size = 9;           // Filter size. Has a large effect on speed.
		double sigmaColor = 9;  // Filter color strength.
		double sigmaSpace = 7;  // Positional strength. Effects speed.
		bilateralFilter(smallImg, tmp, size, sigmaColor, sigmaSpace);
		bilateralFilter(tmp, smallImg, size, sigmaColor, sigmaSpace);
	}

	// Apply an "alien" filter, when given a shrunken image and the full-res edge mask.
	// Detects the color of the pixels in the middle of the image, then changes the color of that region to green.
	changeFacialSkinColor(smallImg, edges, 2);

	// Go back to the original scale.
	resize(smallImg, srcColor, size, 0, 0, INTER_LINEAR);

	// Clear the output image to black, so that the cartoon line drawings will be black (ie: not drawn).
	memset((char*)dst.data, 0, dst.step * dst.rows);

	// Use the blurry cartoon image, except for the strong edges that we will leave black.
	srcColor.copyTo(dst, mask);
}

  运行之后感觉。。。效果很差,校对过代码,大概率不是我的问题,不过好歹也算达到了那个意思。
  还是分析一下吧。
  关键代码主要在changeFacialSkinColor()

// Apply an "alien" filter, when given a shrunken BGR image and the full-res edge mask.
// Detects the color of the pixels in the middle of the image, then changes the color of that region to green.
void changeFacialSkinColor(Mat smallImgBGR, Mat bigEdges, int debugType)
{
	// Convert to Y'CrCb color-space, since it is better for skin detection and color adjustment.
	Mat yuv = Mat(smallImgBGR.size(), CV_8UC3);
	cvtColor(smallImgBGR, yuv, COLOR_BGR2YCrCb);

	// The floodFill mask has to be 2 pixels wider and 2 pixels taller than the small image.
	// The edge mask is the full src image size, so we will shrink it to the small size,
	// storing into the floodFill mask data.
	int sw = smallImgBGR.cols;
	int sh = smallImgBGR.rows;
	Mat maskPlusBorder = Mat::zeros(sh + 2, sw + 2, CV_8U);
	Mat mask = maskPlusBorder(Rect(1, 1, sw, sh));  // mask is a ROI in maskPlusBorder.
	resize(bigEdges, mask, smallImgBGR.size());

	// Make the mask values just 0 or 255, to remove weak edges.
	threshold(mask, mask, 80, 255, THRESH_BINARY);
	// Connect the edges together, if there was a pixel gap between them.
	dilate(mask, mask, Mat());
	erode(mask, mask, Mat());
	//imshow("constraints for floodFill", mask);

	// YCrCb Skin detector and color changer using multiple flood fills into a mask.
	// Apply flood fill on many points around the face, to cover different shades & colors of the face.
	// Note that these values are dependent on the face outline, drawn in drawFaceStickFigure().
	int const NUM_SKIN_POINTS = 6;
	Point skinPts[NUM_SKIN_POINTS];
	skinPts[0] = Point(sw / 2, sh / 2 - sh / 6);
	skinPts[1] = Point(sw / 2 - sw / 11, sh / 2 - sh / 6);
	skinPts[2] = Point(sw / 2 + sw / 11, sh / 2 - sh / 6);
	skinPts[3] = Point(sw / 2, sh / 2 + sh / 16);
	skinPts[4] = Point(sw / 2 - sw / 9, sh / 2 + sh / 16);
	skinPts[5] = Point(sw / 2 + sw / 9, sh / 2 + sh / 16);
	// Skin might be fairly dark, or slightly less colorful.
	// Skin might be very bright, or slightly more colorful but not much more blue.
	const int LOWER_Y = 60;
	const int UPPER_Y = 80;
	const int LOWER_Cr = 25;
	const int UPPER_Cr = 15;
	const int LOWER_Cb = 20;
	const int UPPER_Cb = 15;
	Scalar lowerDiff = Scalar(LOWER_Y, LOWER_Cr, LOWER_Cb);
	Scalar upperDiff = Scalar(UPPER_Y, UPPER_Cr, UPPER_Cb);
	// Instead of drawing into the "yuv" image, just draw 1's into the "maskPlusBorder" image, so we can apply it later.
	// The "maskPlusBorder" is initialized with the edges, because floodFill() will not go across non-zero mask pixels.
	Mat edgeMask = mask.clone();    // Keep an duplicate copy of the edge mask.
	for (int i = 0; i < NUM_SKIN_POINTS; i++) {
		// Use the floodFill() mode that stores to an external mask, instead of the input image.
		const int flags = 4 | FLOODFILL_FIXED_RANGE | FLOODFILL_MASK_ONLY;
		floodFill(yuv, maskPlusBorder, skinPts[i], Scalar(), NULL, lowerDiff, upperDiff, flags);
		if (debugType >= 1)
			circle(smallImgBGR, skinPts[i], 5, CV_RGB(0, 0, 255), 1);
	}
	if (debugType >= 2)
		imshow("flood mask", mask * 120); // Draw the edges as white and the skin region as grey.

	// After the flood fill, "mask" contains both edges and skin pixels, whereas
	// "edgeMask" just contains edges. So to get just the skin pixels, we can remove the edges from it.
	mask -= edgeMask;
	// "mask" now just contains 1's in the skin pixels and 0's for non-skin pixels.

	// Change the color of the skin pixels in the given BGR image.
	int Red = 0;
	int Green = 70;
	int Blue = 0;
	add(smallImgBGR, Scalar(Blue, Green, Red), smallImgBGR, mask);
}

1.YCrCb颜色空间

该颜色空间广泛的用于视频压缩和图像压缩方案,不能算是纯粹的颜色空间,因为它是BGR颜色空间的一种解码方式。 该颜色空间广泛的应用于MPEG和JPEG等视频和图像压缩方案。
Y表示亮度
Cr : RGB空间R通道和Y差值
Cb: RGB空间B通道和Y差值

引用OpenCV之色彩空间转换

为什么要转成这种格式呢?

  书中的解释是(翻译的不好)

修改肤色在RGB颜色空间中效果不是很好,因为你想允许脸上的亮度可以变化而禁止肤色变化很多,而且RGB并没有把颜色和亮度分开。一个解决办法就是用HSV颜色空间,因为它将亮度和色调和饱和度分开。不幸的是,HSV将色调值包裹在红色周围,并且由于皮肤主要是红色,这意味着您需要同时使用小于10%的色调和大于90%的色调,因为它们都是红色。依据此,我们用YCrCb颜色空间(YUV的变种),因为它将亮度从颜色中分出来,而且只用一个单独的肤色值范围而不是两个。注意大多数摄像机、图片和视频在转换为RGB之前都使用某些类型的YUV作为颜色空间,所以在很多情况下你不用转换就可以得到一个YUV图片。

2.MaskPlusBorder是什么?为什么要加2?

官方openCV floodFill说明

Operation mask that should be a single-channel 8-bit image, 2 pixels wider and 2 pixels taller than image. Since this is both an input and output parameter, you must take responsibility of initializing it. Flood-filling cannot go across non-zero pixels in the input mask. For example, an edge detector output can be used as a mask to stop filling at edges. On output, pixels in the mask corresponding to filled pixels in the image are set to 1 or to the a value specified in flags as described below. Additionally, the function fills the border of the mask with ones to simplify internal processing. It is therefore possible to use the same mask in multiple calls to the function to make sure the filled areas do not overlap.

mask参数所代表的掩码既可以作为FloodFill()的输入值(此时它控制可以被填充的区域),也可以作为FloodFill()的输出值 (此时它指已经被填充的区域)。如果mask非空,那么它必须是一个单通道、8位、像素宽度和高度均匀比源图像大两个像素的图像(这是为了使内部运算更简单快速)。mask图像的像素(x+1,y+1)与源图像的像素(x,y)相对应。注意:FlooFill()不会覆盖mask的非零像素点,因此如果不希望mask阻碍填充操作时,将其中元素设为0.源图像img和掩码图像mask均可以用漫水填充来染色。
————————————————
版权声明:本文为CSDN博主「知无涯者1996」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/SweetWind1996/article/details/81200418

  大意+2是为了计算方便。
  那么mask和maskPlusBorder都是做什么的呢。
  首先mask是maskPlusBorder的一个ROI,所以二者指针指向的是几乎同一个区域,mask是sh*sw大小,后者是(sh+2)*(sw+2)。而下文中的操作主要是对mask,maskPlusBorder只是用来做floodFill(),

3.形态学滤波

  dilate和erode是为了连接边缘之间的缝隙,其实不用也可以,只是用了之后,用漫水填充的更精确,能去掉一些细碎的边界,让涂色时可以覆盖更多区域。

形态学(morphology)一词通常表示生物学的一个分支,该分支主要研究动植物的形态和结构。而我们图像处理中的形态学,往往指的是数学形态学。下面一起来了解数学形态学的概念。
  数学形态学(Mathematical morphology)是一门建立在格论和拓扑学基础之上的图像分析学科,是数学形态学图像处理的基本理论。其基本的运算包括:二值腐蚀和膨胀、二值开闭运算。骨架抽取。极限腐蚀。击中击不中变换、形态学梯度、Top-hat变换、颗粒分析、流域变幻、灰值腐蚀和膨胀、灰值开闭运算、灰值形态学梯度等。
  简单来讲,形态学操作就是基于性状的一系列图像处理操作。OpenCV为进行图像的形态学变换提供了快捷、方便的函数。最基本的形态学操作有两种,分别是:膨胀(dilate)与腐蚀(erode)。
  膨胀与腐蚀能实现多种多样的功能,主要如下。

  • 消除噪声;
  • 分隔(isolate)出独立的图像元素,在图像中连接(join)相邻的元素;
  • 寻找图像中的明显的极大值区域或极小值区域;
  • 求出图像的梯度。
6.3.2 膨胀

  膨胀就是求局部最大值的操作。从数学角度来说,膨胀或者腐蚀操作就是将图像(或图像的一部分区域,称之为A)与核(称之为B)进行卷积。
  核可以是任何性状和大小,它拥有一个单独定义出来的参考点,我们称其为锚点(anchorpoint)。多数情况下,核是一个小的,中间带有参考点和实心正方形或者圆盘。其实,可以把核视为模版或者掩码。
  而膨胀就是求局部最大值的操作。核B与图像卷积,即计算核B覆盖的区域的像素点的最大值,并把这个最大值赋值给参考点指定的像素。这样就会使图像中的高亮区域逐渐增长。

6.3.3 腐蚀

  大家应该知道,膨胀和腐蚀(erode)是相反的一对操作,所以腐蚀就是求局部最小值的操作。

6.3.5 相关核心APi函数讲解

1.膨胀:dilate函数
  dilate函数使用像素邻域内的局部极大运算符来膨胀一张图片,从src输入,由dst输出。支持就地(in-place)操作。
  第三个参数,InputArray类型和kernel,膨胀操作的核。当为NULL时,表示的是使用参考点位于中心3x3的核。

#include <opencv2/opencv.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <iostream>
//#include "cartoon.h"

using namespace cv;
using namespace std;

int main()
{
	//载入原图
	Mat image = imread("C:\\Users\\MaxLy\\Desktop\\girl.jpg");
	
	//创建窗口
	namedWindow("原图");
	namedWindow("膨胀");

	//显示原图
	imshow("原图", image);

	//获取自定义核
	Mat element = getStructuringElement(MORPH_RECT, Size(15, 15));
	Mat out;
	//进行膨胀操作
	dilate(image, out, element);

	//显示效果图
	imshow("膨胀", out);

	waitKey(0);

	return 0;
}

2.腐蚀:erode函数
  erode函数使用像素邻域内的局部极小运算符来腐蚀一张图片,从src输入,由dst输出。支持就地(in-place)操作。
  第三个参数,InputArray类型的kernel,腐蚀操作的内核。为NULL时,表示的是使用参考点位于中心3x3的核。一般使用函数getStructuringElement配合这个参数的使用。getStructuringElement函数会反馈指定形状和尺寸的结构元素(内核矩阵)。
  第四个参数,Point类型的anchor,锚的位置。其有默认值(-1,-1),表示锚位于单位(element)的中心,一般不用管它。
  第五个参数,int类型的iterations,迭代使用erode()函数的次数,默认值为1.
  第六个参数,int类型的borderType,用于推断图像外部像素的某种边界模式,注意它有默认值BORDER_DEFAULT。
  第七个参数,const Scalar&类型的borderValue,当边界为常数时的边界值,有默认值morphologyDefaultBorderValue(),一般不用去管它。需要用到它时,可以看官方文档说明。
  同样的,使用erode函数,一般只需要填前面的三个参数,后面的四个参数都有默认值。而且往往结合getStructuringElement一起使用。
  调用范例如下。

#include <opencv2/opencv.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <iostream>
//#include "cartoon.h"

using namespace cv;
using namespace std;

int main()
{
	//载入原图
	Mat image = imread("C:\\Users\\MaxLy\\Desktop\\girl.jpg");
	
	namedWindow("原图");
	namedWindow("腐蚀");

	//显示原图
	imshow("原图", image);

	//获取自定义核
	Mat element = getStructuringElement(MORPH_RECT, Size(15, 15));
	Mat out;

	//进行腐蚀操作
	erode(image, out, element);

	imshow("腐蚀", out);

	waitKey(0);

	return 0;
}

类似的还有开运算,闭运算,形态学梯度,顶帽和黑帽,名称古怪但操作简单。

  • 开运算,是先腐蚀后膨胀,用来消除小物体,在纤细点处分离物体,并且在平滑较大物体的边界的同时不明显改变其面积。
  • 闭运算,是先膨胀后腐蚀,用来排除小型黑洞(黑色区域)
  • 形态学梯度,是膨胀图与腐蚀图之差,对二值图像进行这一操作可以将团块的边缘突出出来。我们可以用形态学梯度来保留物体的边缘轮廓。
  • 顶帽,又被称为礼帽运算,是原图像与开运算的结果图之差。因为开运算带来的结果是放大了裂缝或者局部低亮度的区域。因此,从原图中减去开运算后的图,得到的效果图突出了比原图轮廓周围的区域更明亮的区域,切这一操作与选择的核的大小相关。顶帽运算往往用来分离比临近点亮一些的斑块。在一幅图像具有大幅的背景,而微小物品比较有规律的情况下,可以使用顶帽运算进行背景提取。
  • 黑帽,是闭运算的结果与原图像之差。黑帽运算后的效果图突出了比原图轮廓周围的区域更暗的区域,且这一操作和选择的核的大小相关。所以,黑帽运算用来分离比邻近点暗一些的斑块,效果图有着非常完美的轮廓。
      核心API函数:morphologyEx()
  • 第三个参数,int类型的op,表示形态学运算的类型
标识符含义
MORPH_OPEN开运算
MORPH_CLOSE闭运算
MORPH_GRADIENT形态学梯度
MORPH_TOPHAT顶帽
MORPH_BLACKHAT黑帽
MORPH_ERODE腐蚀
MORPH_DILATE膨胀
  • 第四个参数,InputArray类型的kernel,形态学运算的内核。若为NULL,表示的是使用参考点位于中心3x3的核。一般使用函数getStructuringElement配合这个参数的使用。getStructuringElement函数会返回指定形状和尺寸的结构元素(内核矩阵)。关于getStructuringElement我们之前有讲到过,这里为了大家参阅方便,再写一遍。
    getStructuringElement函数的第一个参数表示内核的形状,我们可以选择如下三种形状之一:
  • 矩形——MORPH_RECT
  • 交叉形——MORPH_CROSS
  • 椭圆形——MORPH_ELLIPSE
      而getStructuringElement函数的第二和第三个参数分别是内核的尺寸以及锚点的位置。
      一般在调用erode以及dilate函数之前,要先定义一个Mat类型的变量来获得getStructuringElement函数的返回值。对于锚点的位置,有默认值Point(-1,-1),表示锚点位于中心。另外需要注意:十字星的element性状唯一依赖于锚点的位置。而在其他情况下,锚点只是影响形态学运算结果的偏移。
  • 第五个参数,Point类型的anchor,锚的位置,其有默认值(-1,-1),表示锚位于中心。
  • 第六个参数,int类型的iterations,迭代使用函数的次数,默认值为1.
  • 第七个参数,int类型的borderType,用于推断图像外部像素的某种边界模式。注意它有默认值BORDER_CONSTANT。
  • 第八个参数,const Scalar&类型的borderValue,当边界为常数时的边界值,有默认值morphologyDefaultBorderValue(),一般不用去管它。需要用到它时,可以看官方文档中的createMorphologyFilter()函数得到更详细的解释。其中的这些操作都可以进行就地(in-place)操作,切对于多通道图像,每一个通道都单独进行操作。

4.漫水填充

  那么现在边缘图阈值化过了,也进行了形态学滤波,那么其mask中保存的边缘已经变得比之前更简洁和明确,接下来就是填充颜色。之前有要求用户把脸放到中央位置,那么我们就在中央位置放6个点,然后从这6个点出发作为种子,向周围涂色,已达到修改肤色的目的。

6.5 漫水填充
6.5.1 漫水填充的定义

  漫水填充法是一种用特定的颜色填充连通区域,通过设置可连通像素的上下限以及连通方式来达到不同的填充效果的方法。漫水填充经常被用来标记或分离图像的一部分,以便对其进行进一步处理或分析,也可以用来从输入图像获取掩码区域,掩码会加速处理过程,或只处理掩码确定的像素点,操作的结果总是某个连续的区域。

6.5.2 漫水填充法的基本思想

  所谓漫水填充,简单的来说,就是自动选中了和种子点相连的区域,接着将该区域替换成指定的颜色,这是个非常有用的功能,经常用来标记或者分离图像的一部分进行处理或分析。漫水填充也可以用来从输入图像获取掩码区域,掩码会加速处理过程,或者只处理掩码指定的像素点。
  以此填充算法作为基础,类似PhotoShop的魔术棒选择工具就很容易实现了。漫水填充(FloodFIll)是查找和种子点连通的颜色相同的点,魔术棒选择工具则是查找和种子点连通的颜色相近的点,把核初始种子像素颜色相近的点压进栈做为新种子。
  在OpenCV中,漫水填充是填充算法中最通用的方法。切在OpenCV2.X版本中,使用C++重写过的FloodFill函数有两个版本:一个不带掩模mask的版本,和一个带mask的版本。这个掩模mask,就是用于进一步控制哪些区域将被填充颜色(比如说当对同一图像进行多次填充时)。这两个版本的FloodFill,都必须在图像中选择一个种子点,然后把邻近区域所有相似点填充上相同的颜色,不同的是,不一定将所有邻近像素点都染上同一颜色,漫水填充操作的结果总是某个连续的区域。当邻近像素点位于给定的范围(从loDiff到upDiff)内或在原始seedpoint像素值范围内时,FloodFill函数就会为这个点涂上颜色。

6.5.3 实现漫水填充算法:floodFill函数

  在OpenCV中,漫水填充算法由floodFill函数实现,其作用是用我们指定的颜色从种子点开始填充一个连接域。连通性由像素值的接近程度来衡量。
简单参数解释

  • 第三个参数,Point类型的seedPoint,漫水填充算法的起始点。
  • 第四个参数,Scalar类型的newVal,像素点被染色的值,即在重绘区域像素的新值。
  • 第五个参数,Rect*类型的rect,有默认值0,一个可选的参数,用于设置floodFill函数将要重绘区域的最小边界矩形区域。
  • 第六个参数,Scalar类型的loDiff,有默认值Scalar(),表示当前观察像素值与其部件邻域像素值或待加入该部件的种子像素之间的亮度或颜色之负差(lower brightness/color difference)的最大值。
  • 第七个参数,Scalar类型的upDiff,有默认值Scalar(),表示当前观察像素值与其部件邻域像素值或者待加入该部件的种子像素之间的亮度或颜色之正差(lower brightness/color difference)的最大值。
  • 第八个参数,int类型的flags,操作标识符,此参数包含三个部分,比较复杂,我们一起详细看看。
  • 低八位(第0~7位)用于控制算法的连通性,可取4(4位默认值)或者8.如果设为4,表示填充算法只考虑当前像素水平方向和垂直方向的相邻点;如果设为8,除上述相邻点外,还会包含对角线方向的相邻点。
  • 高八位部分(16~23位)可以为0或者如下两种选项标识符的组合。
  • FLOODFILL_FIXED_RANGE:如果设置这个标识符,就会考虑当前像素与种子像素之间的差,否则就考虑当前像素与其相邻像素的差。也就是说,这个范围是浮动的。
    FLOODFILL_MASK_ONLY。如果设置为这个标识符,函数不会去填充改变原始图像(也就是忽略第三个参数newVal),而是去填充掩模图像(mask)。这个标识符只对第二个版本的floodFill有用,因第一个版本里面压根就没有mask参数。
  • 中间八位部分,上面关于高八位FLOODFILL_MASK_ONLY标识符中已经说得很明显,需要输入符合要求的掩码。Floodfill的flags参数的中间八位的值就是用于指定填充掩码图像的值的。但如果flags中间八位的值为0,则掩码会用1来填充。
      而所有flags可以用or操作符连接起来,即“|“。例如,如果想用8邻域填充,并填充固定像素值范围,填充掩码而不是填充源图像,以及设填充值为38,那么输入的参数是下面这样:
    flag=8 | FLOODFILL_MASK_ONLY | FLOODFILL_FIXED_RANGE | (38<<8)
      接着,来看一个关于floodFill的简单的调用范例。
#include <opencv2/opencv.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <iostream>
//#include "cartoon.h"

using namespace cv;
using namespace std;

int main()
{
	cv::Mat srcImage = imread("C:\\Users\\MaxLy\\Desktop\\girl.jpg");
	imshow("原始图", srcImage);
	Rect ccomp;
	floodFill(srcImage, Point(50, 300), Scalar(155, 255, 55),
		&ccomp, Scalar(20, 20, 20), Scalar(20, 20, 20));
	imshow("效果图", srcImage);

	waitKey(0);

	return 0;
}

综合测试

#include <opencv2/opencv.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <iostream>
//#include "cartoon.h"

using namespace cv;
using namespace std;

Mat g_srcImage, g_dstImage, g_grayImage, g_maskImage;//定义原始图、目标图、灰度图、掩模图
int g_nFillMode = 1;//漫水填充的模式
int g_nLowDifference = 20, g_nUpDifference = 20;//负差最大值、正差最大值
int g_nConnectivity = 4; //表示floodFill函数标识符低八位的连通值
int g_bIsColor = true; //是否为彩色图的标识符号布尔值
bool g_bUseMask = false; //是否显示掩模窗口的布尔值
int g_nNewMaskVal = 255; //新的重新绘制的像素值

static void onMouse(int event, int x, int y, int, void*)
{
	//若鼠标左键没有按下,则返回
	if (event != EVENT_LBUTTONDOWN)
		return;

	Point seed = Point(x, y);
	int LowDifference = g_nFillMode == 0 ? 0 : g_nLowDifference;//空范围的漫水填充,此值设为0,否则设为全局的g_nLowDifference
	int UpDifference = g_nFillMode == 0 ? 0 : g_nUpDifference; //空范围的漫水填充,此值设为0,否则设为全局的g_nUpDifference

	//标识符0~7位为g_nConnectivity,8~15位为g_nNewMaslVal左移8位的值,16~23位为CV_FLOODFILL_FIXED_RANGE或者0.
	int flags = g_nConnectivity + (g_nNewMaskVal << 8) + (g_nFillMode == 1 ? FLOODFILL_FIXED_RANGE : 0);

	//随机生成BGR值
	int b = (unsigned)theRNG() & 255;//随即返回一个0~155之间的值
	int g = (unsigned)theRNG() & 255;//随即返回一个0~155之间的值
	int r = (unsigned)theRNG() & 255;//随即返回一个0~155之间的值
	Rect ccomp; //定义重绘区域的最小边界矩形区域

	//在重绘区域像素的新值,若是彩色图模式,取Scalar(b,g,r);若是灰度图模式,去Scalar(r*0.299+g*0.587+b*0.114)
	Scalar newVal = g_bIsColor ? Scalar(b, g, r) : Scalar(r * 0.299 + g * 0.587 + b * 0.114);

	Mat dst = g_bIsColor ? g_dstImage : g_grayImage;//目标图的赋值
	int area;

	//正式条用floodFill函数
	if (g_bUseMask)
	{
		//此句代码的OpenCV3版为:
		threshold(g_maskImage, g_maskImage, 1, 128, THRESH_BINARY);
		
		area = floodFill(dst, g_maskImage, seed, newVal, &ccomp, Scalar(LowDifference, LowDifference, LowDifference),
			Scalar(UpDifference, UpDifference, UpDifference), flags);
		imshow("mask", g_maskImage);
	}
	else
	{
		area = floodFill(dst, seed, newVal, &ccomp, Scalar(LowDifference, LowDifference, LowDifference),
			Scalar(UpDifference, UpDifference, UpDifference), flags);
	}

	imshow("效果图", dst);
	cout << area << "个像素被重绘\n";
}

int main()
{
	g_srcImage = imread("C:\\Users\\MaxLy\\Desktop\\girl.jpg");

	if (!g_srcImage.data)
	{
		printf("读取图片image0错误! \n");
		return false;
	}

	g_srcImage.copyTo(g_dstImage);  //赋值源图到目标图
	cvtColor(g_srcImage, g_grayImage, COLOR_BGR2GRAY);//转换三通道的image0到灰度图
	g_maskImage.create(g_srcImage.rows + 2, g_srcImage.cols + 2, CV_8UC1);//利用image0的尺寸来吃实话掩模mask

	namedWindow("效果图", WINDOW_AUTOSIZE);

	createTrackbar("负差最大值", "效果图", &g_nLowDifference, 255, 0);
	createTrackbar("正差最大值", "效果图", &g_nUpDifference, 255, 0);
	//鼠标回调函数
	setMouseCallback("效果图", onMouse, 0);

	//循环轮询按键
	while (1)
	{
		//先显示效果图
		imshow("效果图", g_bIsColor ? g_dstImage : g_grayImage);

		//获取键盘按键
		int c = waitKey(0);
		//判断ESC是否按下,若按下便退出
		if ((c && 255 == 27))
		{
			cout << "程序退出\n";
			break;
		}

		switch ((char)c)
		{
			//如果键盘"1"被按下,效果图在灰度图,彩色图之间互换
		case '1':
			if (g_bIsColor)//若原来为彩色,转为灰度图,并且将掩模mask所有元素设置为0
			{
				cout << "键盘1被按下,切换彩色/灰度模式,当前操作为将[彩色模式]切换为[灰度模式]\n";
				cvtColor(g_srcImage, g_grayImage, COLOR_BGR2GRAY);
				g_maskImage = Scalar::all(0);  //将mask所有元素设置为0
				g_bIsColor = false; //将标识符置为false,表示当前图像不为彩色,而是灰度
			}
			else  //若原来为灰度图,便将原来的彩图image0再次赋值给image,并且将掩模mask有元素设置为0
			{
				cout << "键盘1被按下,切换彩色/灰度模式,当前操作为将[彩色模式]切换为[灰度模式]\n";
				g_srcImage.copyTo(g_dstImage);
				g_maskImage = Scalar::all(0);
				g_bIsColor = true;  //将标识符置为true,表示当前图像模式为彩色
			}
			break;
			//如果键盘按键"2"被按下,显示/隐藏掩模窗口
		case '2':
			if (g_bUseMask)
			{
				destroyWindow("mask");
				g_bUseMask = false;
			}
			else
			{
				namedWindow("mask", 0);
				g_maskImage = Scalar::all(0);
				imshow("mask", g_maskImage);
				g_bUseMask = true;
			}
			break;
		//如果键盘按键"3"被按下,恢复原始图像
		case '3':
			cout << "按键3被按下,恢复原始图像\n";
			g_srcImage.copyTo(g_dstImage);
			cvtColor(g_dstImage, g_grayImage, COLOR_BGR2GRAY);
			g_maskImage = Scalar::all(0);
			break;
		//如果键盘按键4被按下,使用空范围的漫水填充
		case '4':
			cout << "按键4被按下,使用空范围的漫水填充\n";
			g_nFillMode = 0;
			break;
		//如果键盘按键5被按下,使用渐变、固定范围的漫水填充
		case '5':
			cout << "按键5被按下,使用渐变、固定范围的漫水填充\n";
			g_nFillMode = 1;
			break;
		//如果键盘按键6被按下,使用渐变、浮动范围的漫水填充
		case '6':
			cout << "按键6被按下,使用渐变、浮动范围的漫水填充\n";
			g_nFillMode = 2;
			break;
		//如果键盘按键7被按下,操作标识符的低八位使用4位的连接模式
		case '7':
			cout << "按键7被按下,操作标识符的低八位使用4位的连接模式\n";
			g_nConnectivity = 4;
			break;
		//如果键盘按键8被按下,操作标识符的低八位使用8位的连接模式
		case '8':
			cout << "按键8被按下,操作标识符的低八位使用8位的连接模式\n";
			g_nConnectivity = 8;
			break;
		}
	}
	return 0;
}

  在窗口图片上点击鼠标,就可以给其中不同的区域随机着色。程序功能非常丰富,有鼠标操作和键盘8个按键的操作,还可以调滑动条。

  注意,这里从代码中可以看出,漫水填充是要区分彩色模式和灰度模式的

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值