Opencv 学习笔记

简介

图像处理(Image Processing)是用计算机对图像进行分析,以达到所需结果的技术,又称影像处理。图像处理技术一般包括图像压缩,增强和复原,匹配、描述和识别3个部分。图像处理一般指数字图像处理(Digital Image Processing)。其中,数字图像是指用工业相机、摄像机、扫描仪等设备经过拍摄得到的一个大的二维数组。该数组的元素称为像素,其值称为灰度值。而数字图像处理是通过计算机对图像进行去除噪声、增强、复原、分割、提取特征等处理的方法和技术。

计算机视觉(Computer Vision)是一门研究如何使机器“看”的科学,具体地说,就是是指用摄影机和电脑代替人眼对目标进行识别、跟踪和测量等机器视觉,并进一步做图形处理,用电脑处理使之成为更适合人眼观察或传送给仪器检测的图像的一门学科。作为一门科学学科,计算机视觉研究相关的理论和技术,试图建立能够从图像或者多维数据中获取“信息”的人工智能系统。因为感知可以看做是从感官信号中提取信息,所以计算机视觉也可以看做是研究如何使人工系统从图像或多维数据中“感知”的科学。

图像处理和计算机视觉的区别在于:图像处理侧重于“处理”图像——如增强,还原,去噪,分割,等等;而计算机视觉重点在于使用计算机(也许是可移动式的)来模拟人的视觉,因此模拟才是计算机视觉领域的最终目标。

而OpenCV(Open Source Computer Vision Library),是一个基于开源发行的跨平台计算机视觉库,它实现了图像处理和计算机视觉方面的很多通用算法,已经成为了计算机视觉领域最有力的研究工具之一。

OpenCV的初衷:

  • 为基本的视觉应用提供开放且优化的源代码,以促进视觉研究的发展,从而有效地避免“闭门造车”。
  • 通过提供一个通用的架构来传播视觉知识,开发者可以在这个架构上继续开展工作,所以代码应该是非常易读且可改写的。
  • OpenCV库采用的协议不要求商业产品继续开放代码,这使得可移植的、性能被优化的代码可以自由获取,可以促进基于视觉的商业应用的发展。

模块

【calib3d】——Calibration(校准)和3D这两个词的组合缩写。这个模块主要是相机校准和三维重建相关的内容,包括基本的多视角几何算法、单个立体摄像头标定、物体姿态估计、立体相似性算法、3D信息的重建等。

【contrib】——Contributed/Experimental Stuf的缩写。该模块包含了一些最近添加的不太稳定的可选功能,不用去多管。新增了新型人脸识别、立体匹配、人工视网膜模型等技术。

【core】——核心功能模块,包含如下内容:

  • OpenCV基本数据结构
  • 动态数据结构
  • 绘图函数
  • 数组操作相关函数
  • 辅助功能与系统函数和宏
  • 与OpenGL的互操作

【imgproc】——Image和Process这两个单词的缩写组合,图像处理模块。包含如下内容:

  • 线性和非线性的图像滤波
  • 图像的几何变换
  • 其他(Miscellaneous)图像转换
  • 直方图相关
  • 结构分析和形状描述
  • 运动分析和对象跟踪
  • 特征检测
  • 目标检测等内容

【features2d】——也就是Features2D,即2D功能框架,包含如下内容:

  • 特征检测和描述
  • 特征检测器(Feature Detectors)通用接口
  • 描述符提取器(Descriptor Extractors)通用接口
  • 描述符匹配器(Descriptor Matchers)通用接口
  • 通用描述符(Generic Descriptor)匹配器通用接口
  • 关键点绘制函数和匹配功能绘制函数

【flann】——Fast Library for Approximate Nearest Neighbors,高维的近似近邻快速搜索算法库,包含以下两个部分:

  • 快速近似最近邻搜索
  • 聚类

【gpu】——运用GPU加速的计算机视觉模块。

【highgui】——高层GUI图形用户界面,包含媒体的输入输出、视频捕捉、图像和视频的编码解码、图形交互界面的接口等内容。

【legacy】——一些已经废弃的代码库,保留下来作为向下兼容,包含如下内容:

  • 运动分析
  • 期望最大化
  • 直方图
  • 平面细分(C API)
  • 特征检测和描述(Feature Detection and Description)
  • 描述符提取器(Descriptor Extractors)的通用接口
  • 通用描述符(Generic Descriptor Matchers)的常用接口
  • 匹配器

【ml】——Machine Learning,机器学习模块,基本上是统计模型和分类算法,包含如下内容:

  • 统计模型(Statistical Models)
  • 一般贝叶斯分类器(Normal Bayes Classifier)
  • K-近邻(K-Nearest Neighbors)
  • 支持向量机(Support Vector Machines)
  • 决策树(Decision Trees)
  • 提升(Boosting)
  • 梯度提高树(Gradient Boosted Trees)
  • 随机树(Random Trees)
  • 超随机树(Extremely randomized trees)
  • 期望最大化(Expectation Maximization)
  • 神经网络(Neural Networks)
  • MLData

【nonfree】——一些具有专利的算法模块,包含特征检测和GPU相关的内容。最好不要商用。

【objdetect】——目标检测模块,包含Cascade Classification(级联分类)和Latent SVM这两个部分。

【ocl】——OpenCL-accelerated Computer Vision,运用OpenCL加速的计算机视觉组件模块。

【photo】——Computational Photography,包含图像修复和图像去噪两部分

【stitching】——images stitching,图像拼接模块,包含如下部分:

  • 拼接流水线
  • 特点寻找和匹配图像
  • 估计旋转
  • 自动校准
  • 图片歪斜
  • 接缝估测
  • 曝光补偿
  • 图片混合

【superres】——SuperResolution,超分辨率技术的相关功能模块。

【ts】——OpenCV测试相关代码,不用去管。

【video】——视频分析组件,该模块包括运动估计、背景分离、对象跟踪等视频处理相关内容。

【Videostab】——Video stabilization,视频稳定相关的组件,官方文档中没有多做介绍,不用管它。

正式版opencv与opencv_contrib之间的区别:
正式版opencv是稳定版本,opencv_contrib是新增但不稳定版本。

图片基本操作

显示图片

#include <opencv2/opencv.hpp>  //头文件
using namespace cv;  //包含cv命名空间

void main( )
{    
	// 【1】读入一张图片,载入图像
	Mat srcImage = imread("1.jpg");
	// 【2】显示载入的图片
	imshow("【原始图】",srcImage);
	// 【3】等待任意按键按下,正数为倒数时间,0和负数为无限
	waitKey(0);
}  

图像腐蚀

腐蚀,即用图像中的暗色部分“腐蚀”掉图像中的亮色部分。

#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>

using namespace cv;


int main()
{
   //载入原图  
   Mat srcImage = imread("C:/Users/Administrator/Desktop/pic.jpg");
   //显示原图
   imshow("【原图】腐蚀操作", srcImage);
   //进行腐蚀操作 
   Mat element = getStructuringElement(MORPH_RECT, Size(15, 15));
   Mat dstImage;
   erode(srcImage, dstImage, element);
   //显示效果图 
   imshow("【效果图】腐蚀操作", dstImage);
   waitKey(0);

   return 0;
}

其中, getStructuringElement()函数值为指定形状和尺寸的结构元素(内核矩阵)。

图像模糊

主要使用进行均值滤波操作的blur函数。


#include "opencv2/highgui/highgui.hpp" 
#include "opencv2/imgproc/imgproc.hpp" 
using namespace cv;

int main()
{
   //【1】载入原始图
   Mat srcImage = imread("C:/Users/Administrator/Desktop/pic.jpg");

   //【2】显示原始图
   imshow("均值滤波【原图】", srcImage);

   //【3】进行均值滤波操作
   Mat dstImage;
   blur(srcImage, dstImage, Size(7, 7));

   //【4】显示效果图
   imshow("均值滤波【效果图】", dstImage);

   waitKey(0);
}

canny边缘检测


#include <opencv2/opencv.hpp>
#include<opencv2/imgproc/imgproc.hpp>
using namespace cv;

int main()
{
   //【0】载入原始图  
   Mat srcImage = imread("C:/Users/Administrator/Desktop/pic.jpg");  
   imshow("【原始图】Canny边缘检测", srcImage); 	//显示原始图 
   Mat dstImage, edge, grayImage;	//参数定义

   //【1】创建与src同类型和大小的矩阵(dst)
   dstImage.create(srcImage.size(), srcImage.type());

   //【2】将原图像转换为灰度图像
   //此句代码的OpenCV2版为:
   //cvtColor( srcImage, grayImage, CV_BGR2GRAY );
   //此句代码的OpenCV3版为:
   cvtColor(srcImage, grayImage, COLOR_BGR2GRAY);

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

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

   //【5】显示效果图 
   imshow("【效果图】Canny边缘检测", edge);

   waitKey(0);

   return 0;
}

读取并播放视频

VedioCapture类。

#include <opencv2opencv.hpp>  
using namespace cv;


int main()
{
   //【1】读入视频
   VideoCapture capture("C:/Users/Administrator/Desktop/1.avi");

   //【2】循环显示每一帧
   while (1)
   {
      Mat frame;//定义一个Mat变量,用于存储每一帧的图像
      capture >> frame;  //读取当前帧
      imshow("读取视频", frame);  //显示当前帧
      waitKey(30);  //延时30ms
   }
   return 0;
}

调用摄像头采集图像

#include <opencv2opencv.hpp>  
using namespace cv;


int main()
{
   //【1】从摄像头读入视频
   VideoCapture capture(0);

   //【2】循环显示每一帧
   while (1)
   {
      Mat frame;  //定义一个Mat变量,用于存储每一帧的图像
      capture >> frame;  //读取当前帧
      imshow("读取视频", frame);  //显示当前帧
      waitKey(30);  //延时30ms
   }
   return 0;
}

HighGUI图形界面用户

图像的载入、显示与输出

Mat 类:图像类型,是用于保存图像以及其他矩阵数据的数据结构;
imread():读取图像,返回Mat类,第一个参数为图像名,第二个参数为色彩类型(默认为彩色);
imshow():用于在指定窗口显示一幅图像,第一个参数是窗口名,第二个参数是要显示的图像(类型为Mat );
namedWindow():给窗口命名;
imwrite():输出(保存)图片,第一个参数为要命名的图片名称,第二个参数为图片名(InputArray类),第三个参数为要保存的格式(一般不写)。

#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
using namespace cv;

int main()
{
   Mat girl = imread("C:/Users/Administrator/Desktop/girl.jpg"); //载入图像到Mat
   namedWindow("【1】动漫图"); //创建一个名为 "【1】动漫图"的窗口  
   imshow("【1】动漫图", girl);//显示名为 "【1】动漫图"的窗口  

   //-----------------------------------【二、初级图像混合】--------------------------------------
   //	描述:二、初级图像混合
   //--------------------------------------------------------------------------------------------------
   //载入图片
   Mat image = imread("C:/Users/Administrator/Desktop/dota.jpg", 199);
   Mat logo = imread("C:/Users/Administrator/Desktop/dota_logo.jpg");

   //载入后先显示
   namedWindow("【2】原画图");
   imshow("【2】原画图", image);

   namedWindow("【3】logo图");
   imshow("【3】logo图", logo);

   //// 定义一个Mat类型,用于存放,图像的ROI
   //Mat imageROI;
   ////方法一
   ////imageROI = image(Rect(800, 350, logo.cols, logo.rows));
   ////方法二
   //imageROI= image(Range(350,350+logo.rows),Range(800,800+logo.cols));

   //// 将logo加到原图上
   //addWeighted(imageROI, 0.5, logo, 0.3, 0., imageROI);

   ////显示结果
   //namedWindow("【4】原画+logo图");
   //imshow("【4】原画+logo图", image);

   //-----------------------------------【三、图像的输出】--------------------------------------
   //	描述:将一个Mat图像输出到图像文件
   //-----------------------------------------------------------------------------------------------
   //输出一张jpg图片到工程目录下
   imwrite("C:/Users/Administrator/Desktop/由imwrite生成的图片.jpg", image);

   waitKey();

   return 0;
}

滑动条的创建和使用

#include <opencv2/opencv.hpp>
#include <opencv2/highgui/highgui.hpp>
using namespace cv;


#define WINDOW_NAME "【滑动条的创建&线性混合示例】"        //为窗口标题定义的宏 

const int g_nMaxAlphaValue = 100;//Alpha值的最大值
int g_nAlphaValueSlider;//滑动条对应的变量
double g_dAlphaValue;
double g_dBetaValue;

//声明存储图像的变量
Mat g_srcImage1;
Mat g_srcImage2;
Mat g_dstImage;


//-----------------------------------【on_Trackbar( )函数】--------------------------------
//		描述:响应滑动条的回调函数
//------------------------------------------------------------------------------------------
void on_Trackbar(int, void*)
{
   //求出当前alpha值相对于最大值的比例
   g_dAlphaValue = (double)g_nAlphaValueSlider / g_nMaxAlphaValue;
   //则beta值为1减去alpha值
   g_dBetaValue = (1.0 - g_dAlphaValue);

   //根据alpha和beta值进行线性混合
   addWeighted(g_srcImage1, g_dAlphaValue, g_srcImage2, g_dBetaValue, 0.0, g_dstImage);

   //显示效果图
   imshow(WINDOW_NAME, g_dstImage);
}


//-----------------------------【ShowHelpText( )函数】--------------------------------------
//		描述:输出帮助信息
//-------------------------------------------------------------------------------------------------
//-----------------------------------【ShowHelpText( )函数】----------------------------------
//		描述:输出一些帮助信息
//----------------------------------------------------------------------------------------------
void ShowHelpText()
{
   //输出欢迎信息和OpenCV版本
   printf("

			非常感谢购买《OpenCV3编程入门》一书!
");
   printf("

			此为本书OpenCV3版的第17个配套示例程序
");
   printf("

			   当前使用的OpenCV版本为:" CV_VERSION);
   printf("

  ----------------------------------------------------------------------------
");
}


//--------------------------------------【main( )函数】-----------------------------------------
//		描述:控制台应用程序的入口函数,我们的程序从这里开始执行
//-----------------------------------------------------------------------------------------------
int main(int argc, char** argv)
{

   //显示帮助信息
   ShowHelpText();

   //加载图像 (两图像的尺寸需相同)
   g_srcImage1 = imread("C:/Users/Administrator/Desktop/1.jpg");
   g_srcImage2 = imread("C:/Users/Administrator/Desktop/2.jpg");
   if (!g_srcImage1.data) { printf("读取第一幅图片错误,请确定目录下是否有imread函数指定图片存在~! 
"); return -1; }
   if (!g_srcImage2.data) { printf("读取第二幅图片错误,请确定目录下是否有imread函数指定图片存在~!
"); return -1; }

   //设置滑动条初值为70
   g_nAlphaValueSlider = 70;

   //创建窗体
   namedWindow(WINDOW_NAME, 1);

   //在创建的窗体中创建一个滑动条控件
   char TrackbarName[50];
   printf(TrackbarName, "透明值 %d", g_nMaxAlphaValue);

   createTrackbar(TrackbarName, WINDOW_NAME, &g_nAlphaValueSlider, g_nMaxAlphaValue, on_Trackbar);

   //结果在回调函数中显示
   on_Trackbar(g_nAlphaValueSlider, 0);

   //按任意键退出
   waitKey(0);

   return 0;
}

createTrackbar这个函数我们以后会经常用到,它创建一个可以调整数值的轨迹条,并将轨迹条附加到指定的窗口上,使用起来很方便。首先大家要记住,它往往会和一个回调函数配合起来使用。先看下他的函数原型:

int createTrackbar(conststring& trackbarname, conststring& winname, int* value, int count, TrackbarCallback onChange=0,void* userdata=0);

第一个参数,const string&类型的trackbarname,表示轨迹条的名字,用来代表我们创建的轨迹条。
第二个参数,const string&类型的winname,填窗口的名字,表示这个轨迹条会依附到哪个窗口上,即对应namedWindow()创建窗口时填的某一个窗口名。
第三个参数,int* 类型的value,一个指向整型的指针,表示滑块的位置。并且在创建时,滑块的初始位置就是该变量当前的值。
第四个参数,int类型的count,表示滑块可以达到的最大位置的值。PS:滑块最小的位置的值始终为0。
第五个参数,TrackbarCallback类型的onChange,首先注意他有默认值0。这是一个指向回调函数的指针,每次滑块位置改变时,这个函数都会进行回调。并且这个函数的原型必须为void XXXX(int,void);其中第一个参数是轨迹条的位置,第二个参数是用户数据(看下面的第六个参数)。如果回调是NULL指针,表示没有回调函数的调用,仅第三个参数value有变化。
第六个参数,void
类型的userdata,他也有默认值0。这个参数是用户传给回调函数的数据,用来处理轨迹条事件。如果使用的第三个参数value实参是全局变量的话,完全可以不去管这个userdata参数。

getTrackbarPos函数,
用于获取当前轨迹条的位置并返回。

int getTrackbarPos(conststring& trackbarname, conststring& winname);

鼠标操作


#include <opencv2/opencv.hpp>
using namespace cv;

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


void on_MouseHandle(int event, int x, int y, int flags, void* param);
void DrawRectangle(cv::Mat& img, cv::Rect box);
void ShowHelpText();

//-----------------------------------【全局变量声明部分】-----------------------------------
//		描述:全局变量的声明
//-----------------------------------------------------------------------------------------------
Rect g_rectangle;
bool g_bDrawingBox = false;//是否进行绘制
RNG g_rng(12345);



int main(int argc, char** argv)
{
   //【0】改变console字体颜色
   system("color 9F");

   //【0】显示欢迎和帮助文字
   ShowHelpText();

   //【1】准备参数
   g_rectangle = Rect(-1, -1, 0, 0);
   Mat srcImage(600, 800, CV_8UC3), tempImage;
   srcImage.copyTo(tempImage);
   g_rectangle = Rect(-1, -1, 0, 0);
   srcImage = Scalar::all(0);

   //【2】设置鼠标操作回调函数
   namedWindow(WINDOW_NAME);
   setMouseCallback(WINDOW_NAME, on_MouseHandle, (void*)&srcImage);

   //【3】程序主循环,当进行绘制的标识符为真时,进行绘制
   while (1)
   {
      srcImage.copyTo(tempImage);//拷贝源图到临时变量
      if (g_bDrawingBox) DrawRectangle(tempImage, g_rectangle);//当进行绘制的标识符为真,则进行绘制
      imshow(WINDOW_NAME, tempImage);
      if (waitKey(10) == 27) break;//按下ESC键,程序退出
   }
   return 0;
}



//--------------------------------【on_MouseHandle( )函数】-----------------------------
//		描述:鼠标回调函数,根据不同的鼠标事件进行不同的操作
//-----------------------------------------------------------------------------------------------
void on_MouseHandle(int event, int x, int y, int flags, void* param)
{

   Mat& image = *(cv::Mat*) param;
   switch (event)
   {
      //鼠标移动消息
   case EVENT_MOUSEMOVE:
   {
      if (g_bDrawingBox)//如果是否进行绘制的标识符为真,则记录下长和宽到RECT型变量中
      {
         g_rectangle.width = x - g_rectangle.x;
         g_rectangle.height = y - g_rectangle.y;
      }
   }
   break;

   //左键按下消息
   case EVENT_LBUTTONDOWN:
   {
      g_bDrawingBox = true;
      g_rectangle = Rect(x, y, 0, 0);//记录起始点
   }
   break;

   //左键抬起消息
   case EVENT_LBUTTONUP:
   {
      g_bDrawingBox = false;//置标识符为false
      //对宽和高小于0的处理
      if (g_rectangle.width < 0)
      {
         g_rectangle.x += g_rectangle.width;
         g_rectangle.width *= -1;
      }

      if (g_rectangle.height < 0)
      {
         g_rectangle.y += g_rectangle.height;
         g_rectangle.height *= -1;
      }
      //调用函数进行绘制
      DrawRectangle(image, g_rectangle);
   }
   break;

   }
}



//-----------------------------------【DrawRectangle( )函数】------------------------------
//		描述:自定义的矩形绘制函数
//-----------------------------------------------------------------------------------------------
void DrawRectangle(cv::Mat& img, cv::Rect box)
{
   cv::rectangle(img, box.tl(), box.br(), cv::Scalar(g_rng.uniform(0, 255), g_rng.uniform(0, 255), g_rng.uniform(0, 255)));//随机颜色
}


//-----------------------------------【ShowHelpText( )函数】-----------------------------
//          描述:输出一些帮助信息
//----------------------------------------------------------------------------------------------
void ShowHelpText()
{
   //输出欢迎信息和OpenCV版本
   printf("

			非常感谢购买《OpenCV3编程入门》一书!
");
   printf("

			此为本书OpenCV3版的第18个配套示例程序
");
   printf("

			   当前使用的OpenCV版本为:" CV_VERSION);
   printf("

  ----------------------------------------------------------------------------
");
   //输出一些帮助信息
   printf("


	欢迎来到【鼠标交互演示】示例程序
");
   printf("

	请在窗口中点击鼠标左键并拖动以绘制矩形
");

}

core组件基础

Mat数据解构

Mat类由两个数据部分组成:矩阵头(包含矩阵尺寸、存储方法、存储地址等信息)和一个指向存储所有像素值的矩阵(根据所选存储方法的不同,矩阵可以是不同的维数)的指针。

Mat类使用引用计数功能,复制时只复制数据头,而共用同一个矩阵数据,当最后一个引用成员销毁时,引用计数为0,数据矩阵被销毁。

Mat A, C; // 仅创建信息头部分
A = imread(“1.jpg”, CV_LOAD_IMAGE_COLOR); // 这里为矩阵开辟内存
Mat B(A); //使用拷贝构造函数
C = A; //赋值运算符

但某些时候你仍会想复制矩阵本身(不只是信息头和矩阵指针),这时可以使用函数clone( )或者copyTo( )。

Mat F = A.clone( );
Mat G;
A.copyTo(G);

Mat不但是一个非常有用的图像容器类,同时也是一个通用的矩阵类,我们也可以用它来创建和操作多维矩阵。

像素值的存储方法

颜色系统有很多,它们各有优势,具体如下。

  • RGB是最常见的,这是因为人眼采用相似的工作机制,它也被显示设备所采用
  • HSV和HLS把颜色分解成色调、饱和度和亮度/明度。这是描述颜色更自然的方式,比如可以通过抛弃最后一个元素,使算法对输入图像的光照条件不敏感
  • YCrCb在JPEG图像格式中广泛使用
  • CIE L* a* b* 是一种在感知上均匀的颜色空间,它适合用来度量两个颜色之间的距离

常用数据结构和函数

Point点类

Point类数据结构表示了二维坐标系下的点,即由其图像坐标x和y指定的2D点。

Point point;
point.x = 10;
point.y = 8;

Point point = Point(10, 8);

另外,在OpenCV中有如下定义:

typedef Point_<int> Point2i;
typedef Point2i Point;
typedef Point_<float> Point2f;

所以,Point_<int>Point2iPoint互相等价,Point_<float>Point2f互相等价。

Point类构造函数:

//包含OpenCV的头文件
//参照github https://github.com/yoyoyo-yo/Gasyori100knock 
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace  std;
//使用OpenCV的命名空间
using namespace cv;
//Point 类型操作 类型
int main()
{
	//1.默认构造函数
	Point2i p1;//数据为[0,0]
	Point3f p2;//数据为[0,0,0]
	cout << p1 << '
' << p2 << endl;
 
	//2.赋值构造函数
	Point3f p3(1, 2,3);//数据为[1,2,3]
	Point3f p4(p3);//数据为[1,2,3]
	cout << p3 << "
" << p4 << endl;
 
	//3,带参数的构造函数
	Point2i p5(1, 2);//数据为[1,2]
	Point3f p6(1.1, 1.2, 1.3);//数据为[1.1,1.2,1.3]
	cout << p5 << '
' << p6 << endl;
 
	//4,隐式类型转换,转换为Vec3f
	Vec3f v = p6;//数据为[1.1,1.2,1.3]
	cout << v << endl;
 
	//5,成员函数访问
	cout << p5.x << '	' << p5.y << endl;
	cout << p6.x << '	' << p6.y << '	' << p6.z << endl;
 
	//6.点乘 --可以用来计算两个向量之间的夹角
	Point3f p7(2.0f, 3.0f, 4.0f);
	float value = p6.dot(p7);
	cout << value << endl;
	
	//7.十字相乘 仅仅适用与三维点影像
	//结果返回两个向量的垂直向量
	Point3f p8(1, 0, 0);//x方向的单位向量
	Point3f p9(0, 1, 0);//Y方向的单位向量
	Point3f p10 = p8.cross(p9);//计算出来的Z方向的单位向量
	cout << p10 << endl;
 
	//8.判断点是否在矩阵内 仅仅适用与二维点
	Rect2f r(0,0,1,1);//注意,这个构造函数是(x,y,width,height)X坐标系向右为正,Y坐标系向上为正
	Point2f p(0.5, 0.5);
	bool bValue = p.inside(r);//返回为true
	p.x = 2;
	bValue = p.inside(r);//返回为false
	return 0;
}

Scalar颜色类

Scalar( )表示具有4个元素的数组,在OpenCV中被大量用于传递像素值,如RGB颜色值。而RGB颜色值为三个参数,其实对于Scalar函数来说,如果用不到第四个参数,则不需要写出来;若只写三个参数,OpenCV会认为我们就想表示三个参数。

Scalar(a, b, c);

那么定义的RGB颜色值:红色分量为c,绿色分量为b,蓝色分量为a。

Scalar类的源头为Scalar_类,而Scalar_类是Vec4x的一个变种,我们常用的Scalar其实就是Scalar_<double>。这就解释了为什么很多函数的参数输入可以是Mat,也可以是Scalar。

Size尺寸类

原型:

typedef Size_<int> Size2i;
typedef Size2i Size;

所以,Size_<int>Size2iSize这三个类型名等价。

Size模版类中有很多重载函数,其中我们使用频率最高的是下面这个构造函数:

Size_(_Tp _width, _Tp _height);

Rect矩形类

Rect类的成员变量有x、y、width、height,分别为左上角点的坐标和矩形的宽和高。常用的成员函数有:Size( )返回值为Size;area( )返回矩形的面积;contains(Point)判断点是否在矩形内;inside(Rect)函数判断矩形是否在该矩形内;tl( )返回左上角点坐标;br( )返回右下角点坐标。值得注意的是,如果想求两个矩形的交集和并集,可以用如下格式:

Rect rect = rect1 & rect2;
Rect rect = rect1 | rect2;

如果想让矩形进行平移操作和缩放操作,甚至可以这样写:

Rect rectShift = rect + point;
Rect rectScale = rect + size;

cvtColor()颜色空间转换

cvtColor( )函数是OpenCV里的颜色空间转换函数,可以实现RGB颜色向HSV、HSI等颜色空间的转换,也可以转换为灰度图像。
函数原型:

void cvtColor(InputArray src, OutputArray dst, int code, int dstCn=0);

第一个参数为输入图像,第二个参数为输出图像,第三个参数为颜色空间转换的标识符,第四个参数为目标图像的通道数,若该参数是0,表示目标图像取源图像的通道数。下面是一个调用示例:

//此句代码的OpenCV2版为:
cvtColor(srcImage,dstImage, CV_GRAY2BGR);//转换原始图为灰度图
//此句代码的OpenCV3版为:
cvtColor(srcImage,dstImage, COLOR_GRAY2BGR);//转换原始图为灰度图

例子:

#include "opencv2/imgproc/imgproc.hpp"
#include "opencv2/highgui/highgui.hpp"
using namespace cv;
int main( )
{
    //【1】载入图片
    Mat srcImage=imread("1.jpg",1);
    Mat dstImage;
    //【2】转换颜色空间
    cvtColor(srcImage,dstImage, COLOR_BGR2Lab);
    //【3】显示效果图
    imshow("效果图",dstImage);
    //【4】保持窗口显示
    waitKey( );
    return 0;
}

其他常用的知识点

Matx是个轻量级的Mat,必须在使用前规定好大小,比如一个2* 3的float型的Matx,可以声明为Matx23f。

Vec是Matx的一个派生类,是一个一维的Matx,跟vector很相似。在OpenCV源码中有如下定义。

template<typename_Tp, int n> class Vec : public Matx<_Tp, n, 1> {...};
typedef Vec<uchar, 2> Vec2b;

Range类其实就是为了使OpenCV的使用更像MATLAB而产生的。比如Range::all( )其实就是MATLAB里的符号。而Range(a, b)其实就是MATLAB中的a:b,注意这里的a和b都应为整型。

OpenCV中防止内存溢出的函数有alignPtr、alignSize、allocate、deallocate、fastMalloc、fastFree等。

<math.h>里的一些函数使用起来很方便,有计算向量角度的函数fastAtan2、计算立方根的函数cubeRoot、向上取整函数cvCeil、向下取整函数cvFloor、四舍五入函数cvRound等。还有一些类似MATLAB里面的函数,比如cvIsInf判断自变量是否无穷大,cvIsNaN判断自变量是否不是一个数。

显示文字相关的函数有getTextSize、cvInitFont、putText。

作图相关的函数有circle、clipLine、ellipse、ellipse2Poly、line、rectangle、polylines、类LineIterator。

填充相关的函数有fillConvexPoly、fillPoly。

OpenCV中RNG( )函数的作用为初始化随机数状态的生成器。

基本图形的绘制

DrawEllipse()

画椭圆。

#include "opencv2/imgproc/imgproc.hpp"
#include "opencv2/highgui/highgui.hpp"
using namespace cv;
#define WINDOW_WIDTH 600//定义窗口大小的宏

//------------------------【DrawEllipse( )函数】------------------------
//          描述:自定义的绘制函数,实现了绘制不同角度、相同尺寸的椭圆
//------------------------------------------------------------------
void DrawEllipse( Mat img, double angle )
{
  int thickness = 2;
  int lineType = 8;

  ellipse( img,
       Point( WINDOW_WIDTH/2, WINDOW_WIDTH/2 ),
       Size( WINDOW_WIDTH/4, WINDOW_WIDTH/16 ),
       angle,
       0,
       360,
       Scalar( 255, 129, 0 ),
       thickness,
       lineType );
}

int main( )
{
    Mat dstImage=imread("/Users/cdq/Desktop/test.jpg");
    DrawEllipse(dstImage,0);
    imshow("",dstImage);
    
    waitKey( );
    return 0;
}

DrawFilledCircle()

画实心圆。

//---------------------【DrawFilledCircle( )函数】---------------------
//          描述:自定义的绘制函数,实现了实心圆的绘制
//------------------------------------------------------------------
void DrawFilledCircle( Mat img, Point center )
{
    int thickness = -1;
    int lineType = 8;
    
    circle( img,
        center,
        WINDOW_WIDTH/32,
        Scalar( 0, 0, 255 ),
        thickness,
        lineType );
}

函数DrawFilledCircle( )调用了OpenCV中的circle函数,将圆画到图像img上,圆心由点center定义,圆的半径为WINDOW_WIDTH/32,圆的颜色为Scalar(0,0,255),按BGR的格式为红色,线粗定义为thickness = -1,因此绘制的圆是实心的。

DrawPolygon()

void DrawPolygon( Mat img )
{
	Mat img(500, 500, CV_8U, Scalar(0));
	 
	Point root_points[1][4];
	root_points[0][0] = Point(215,220);
	root_points[0][1] = Point(460,225);
	root_points[0][2] = Point(466,450);
	root_points[0][3] = Point(235,465);

	const Point* ppt[1] = {root_points[0]};
	int npt[] = {4};
	polylines(img, ppt, npt, 1, 1, Scalar(255),1,8,0);
	imshow("Test", img);
	waitKey();
	fillPoly(img, ppt, npt, 1, Scalar(255));
	imshow("Test", img);
	waitKey();
}

函数DrawPolygon( )调用了OpenCV中的fillPoly函数,用于将多边形画到图像img上,其中多边形的顶点集为ppt,要绘制的多边形顶点数目为npt,要绘制的多边形数量仅为1,多边形的颜色定义为白色Scalar(255,255,255)。

DrawLine()

//-------------------------【DrawLine( )函数】-------------------------
//          描述:自定义的绘制函数,实现了线的绘制
//------------------------------------------------------------------
void DrawLine( Mat img, Point start, Point end )
{
  int thickness = 2;
  int lineType = 8;
  line( img,
    start,
    end,
    Scalar( 0, 0, 0 ),
    thickness,
    lineType );
}

DrawLin( )函数调用了OpenCV中的line函数,用于在图像img上画一条从点start到点end的直线段,线的颜色为Scalar(0,0,0)代表的黑色,线的粗细thickness为2,且此线为8联通(lineType = 8)。

core组件进阶

访问图像中的像素

图像在内存之中的存储方式

图像矩阵的大小取决于所用的颜色模型,确切地说,取决于所用通道数。
例如,灰度模型只有一个通道;RGB模型有R,G,B三个通道。

OpenCV中子列的通道顺序是反过来的——BGR而不是RGB。很多情况下,因为内存足够大,可实现连续存储,因此,图像中的各行就能一行一行地连接起来,形成一个长行。连续存储有助于提升图像扫描速度,我们可以使用isContinuous( )来判断矩阵是否是连续存储的。

颜色空间缩减

我们知道,若矩阵元素存储的是单通道像素,使用C或C++的无符号字符类型,那么像素可有256个不同值。但若是三通道图像,这种存储格式的颜色数就太多了(确切地说,有一千六百多万种)。用如此之多的颜色来进行处理,可能会对我们的算法性能造成严重影响。

其实,仅用这些颜色中具有代表性的很小的部分,就足以达到同样的效果。

如此,颜色空间缩减(color space reduction)便可以派上用场了,它在很多应用中可以大大降低运算复杂度。颜色空间缩减的做法是:将现有颜色空间值除以某个输入值,以获得较少的颜色数。也就是“做减法”,比如颜色值0到9可取为新值0,10到19可取为10,以此类推。

如uchar类型的三通道图像,每个通道取值可以是0~255,于是就有256×256个不同的值。我们可以定义:

0~9范围的像素值为0;

10~19范围的像素值为10;

20~29范围的像素值为20。

这样的操作将颜色取值降低为26×26×26种情况。这个操作可以用一个简单的公式来实现。因为C++中int类型除法操作会自动截余。例如:

Iold=14;Inew=(Iold/10)* 10=(14/10)* 10=1* 10=10;

uchar(无符号字符,即0到255之间取值的数)类型的值除以int值,结果仍是char。因为结果是char类型的,所以求出来小数也要向下取整。利用这一点,刚才提到在uchar定义域中进行的颜色缩减运算就可以表达为下面的形式:

因为C++中int类型除法操作会自动截余。比如:

Iold=14; Inew=(Iold/10)*10=(14/10)*10=1*10=10;

在处理图像像素时,每个像素需要进行一遍上述计算,也需要一定的时间花销。但我们注意到其实只有0~255种像素,即只有256种情况。进一步可以把256种计算好的结果提前存在表中table中,这样每种情况不需计算,直接从table中取结果即可。
批注:如何评判计算和取值的时间。

int divideWith=10;
uchar table[256];
for (int i = 0; i < 256; ++i)
	table[i] = divideWith*(i/divideWith);

于是table[i]存放的是值为i的像素减小颜色空间的结果,这样也就可以理解上述方法中的操作:

p[j] = table[p[j]];

这样,简单的颜色空间缩减算法就可由下面两步组成:
(1)遍历图像矩阵的每一个像素;

(2)对像素应用上述公式。

值得注意的是,我们这里用到了除法和乘法运算,而这两种运算又特别费时,所以,应尽可能用代价较低的加、减、赋值等运算来替换它们。此外,还应注意到,上述运算的输入仅能在某个有限范围内取值,如uchar类型可取256个值。

由此可知,对于较大的图像,有效的方法是预先计算所有可能的值,然后需要这些值的时候,利用查找表直接赋值即可。查找表是一维或多维数组,存储了不同输入值所对应的输出值,其优势在于只需读取、无须计算。
批注:读取速度比计算快?

LUT函数:Look up table操作

对于上文提到的Look up table操作,OpenCV官方文档中强烈推荐我们使用一个原型为operationsOnArrays:LUT( )<lut>的函数来进行。它用于批量进行图像元素查找、扫描与操作图像。其使用方法如下:

//首先我们建立一个mat型用于查表
Mat lookUpTable(1, 256, CV_8U);
uchar* p = lookUpTable.data;
for( int i = 0; i < 256; ++i)
    p[i] = table[i];

//然后我们调用函数 (I 是输入 J 是输出):
for (int i = 0; i < times; ++i)
    LUT(I, lookUpTable, J);

计时函数

可以利用这两个简便的计时函数——getTickCount()和getTickFrequency()。

getTickCount( )函数返回CPU自某个事件(如启动电脑)以来走过的时钟周期数。

getTickFrequency( )函数返回CPU一秒钟所走的时钟周期数。这样,我们就能轻松地以秒为单位对某运算计时。

double time0 = static_cast<double>(getTickCount( ));//记录起始时间
// 进行图像处理操作……
time0 = ((double)getTickCount( ) – time0)/getTickFrequency( );
cout<<"此方法运行时间为:"<<time0<<"秒"<<endl;  //输出运行时间

访问图像中像素的三类方法

  • 方法一 指针访问:C操作符[ ];
  • 方法二 迭代器iterator;
  • 方法三 动态地址计算。

这三种方法在访问速度上略有差异。debug模式下,这种差异非常明显,不过在release模式下,这种差异就不太明显了。我们通过一组程序来说明这几种方法。程序的目的是减少图像中颜色的数量,比如原来的图像是是256种颜色,我们希望将它变成64种颜色,那只需要将原来的颜色除以4(整除)以后再乘以4就可以了。

//----------------------【头文件、命名空间包含部分】----------------------
//          描述:包含程序所使用的头文件和命名空间
//------------------------------------------------------------------
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <iostream>
using namespace std;
using namespace cv;

//-------------------------【全局函数声明部分】-------------------------
//          描述:全局函数声明
//------------------------------------------------------------------
void colorReduce1(Mat& inputImage, Mat& outputImage, int div);
void colorReduce2(Mat& inputImage, Mat& outputImage, int div);
void colorReduce3(Mat& inputImage, Mat& outputImage, int div);

//---------------------------【main( )函数】---------------------------
//          描述:控制台应用程序的入口函数,我们的程序从这里开始执行
//------------------------------------------------------------------
int main( )
{
    //【1】创建原始图并显示
    Mat srcImage = imread("/Users/cdq/Desktop/test.jpg");
    imshow("原始图像",srcImage);

    //【2】按原始图的参数规格来创建创建效果图
    Mat dstImage;
    dstImage.create(srcImage.rows,srcImage.cols,srcImage.type( ));//效果图的大小、类型与原图片相同

    //【3】记录起始时间
    double time0 = static_cast<double>(getTickCount( ));

    //【4】调用颜色空间缩减函数
//    colorReduce1(srcImage,dstImage,32);
//    colorReduce2(srcImage,dstImage,32);
    colorReduce3(srcImage,dstImage,32);

    //【5】计算运行时间并输出
    time0 = ((double)getTickCount( ) - time0)/getTickFrequency( );
    cout<<"此方法运行时间为:"<<time0<<"秒"<<endl;  //输出运行时间

    //【6】显示效果图
    imshow("效果图",dstImage);
    waitKey(0);
}

//------------------------【colorReduce( )函数】------------------------
//          描述:使用【指针访问:C操作符[ ]】方法版的颜色空间缩减函数
//------------------------------------------------------------------
void colorReduce1(Mat& inputImage, Mat& outputImage, int div)
{
    //参数准备
    outputImage = inputImage.clone( );  //复制实参到临时变量
    int rowNumber = outputImage.rows;  //行数
    int colNumber = outputImage.cols*outputImage.channels( );  //列数×通道数=每一行元素的个数//双重循环,遍历所有的像素值
    for(int i = 0;i < rowNumber;i++)  //行循环
    {
       uchar* data = outputImage.ptr<uchar>(i);  //获取第i行的首地址
       for(int j = 0;j < colNumber;j++)   //列循环
       {
         // ---------【开始处理每个像素】-------------
         data[j] = data[j]/div*div + div/2;
         // ----------【处理结束】---------------------
       }  //行处理结束
    }
}

//------------------------【colorReduce( )函数】------------------------
//          描述:使用【迭代器】方法版本的颜色空间缩减函数
//------------------------------------------------------------------
void colorReduce2(Mat& inputImage, Mat& outputImage, int div)
{
    //参数准备
    outputImage = inputImage.clone( );  //复制实参到临时变量
    //获取迭代器
    Mat_<Vec3b>::iterator it = outputImage.begin<Vec3b>( );  //初始位置的迭代器
    Mat_<Vec3b>::iterator itend = outputImage.end<Vec3b>( );  //终止位置的迭代器

    //存取彩色图像像素
    for(;it != itend;++it)
    {
        // ---------------------【开始处理每个像素】---------------------
        (*it)[0] = (*it)[0]/div*div + div/2;
        (*it)[1] = (*it)[1]/div*div + div/2;
        (*it)[2] = (*it)[2]/div*div + div/2;
        // ------------------------【处理结束】------------------------
    }
}


//------------------------【colorReduce( )函数】------------------------
//          描述:使用【动态地址运算配合at】方法版本的颜色空间缩减函数
//------------------------------------------------------------------
void colorReduce3(Mat& inputImage, Mat& outputImage, int div)
{
    //参数准备
    outputImage = inputImage.clone( );  //复制实参到临时变量
    int rowNumber = outputImage.rows;  //行数
    int colNumber = outputImage.cols;  //列数

    //存取彩色图像像素
    for(int i = 0;i < rowNumber;i++)
    {
        for(int j = 0;j < colNumber;j++)
        {
            // ---------------------【开始处理每个像素】---------------------
            outputImage.at<Vec3b>(i,j)[0] =outputImage.at<Vec3b>(i,j)[0]/div*div + div/2;  //蓝色通道
            outputImage.at<Vec3b>(i,j)[1] =outputImage.at<Vec3b>(i,j)[1]/div*div + div/2;  //绿色通道
            outputImage.at<Vec3b>(i,j)[2] =outputImage.at<Vec3b>(i,j)[2]/div*div + div/2;  //红色通道
            // ------------------------【处理结束】------------------------
        }  // 行处理结束
    }
}
此方法运行时间为:0.00327002秒
此方法运行时间为:0.00733847秒
此方法运行时间为:0.00644152秒

ROI区域图像叠加&图像混合

感兴趣区域:ROI

在图像处理领域,我们常常需要设置感兴趣区域(ROI,region of interest),来专注或者简化工作过程。也就是从图像中选择的一个图像区域,这个区域是图像分析所关注的重点。我们圈定这个区域,以便进行进一步处理。而且,使用ROI指定想读入的目标,可以减少处理时间,增加精度,给图像处理来带不小的便利。

定义ROI区域有两种方法:第一种是使用表示矩形区域的Rect。它指定矩形的左上角坐标(构造函数的前两个参数)和矩形的长宽(构造函数的后两个参数)以定义一个矩形区域。

其中,image为已经载入好的图片。

//定义一个Mat类型并给其设定ROI区域
Mat imageROI;
//方法一
imageROI=image(Rect(500,250,logo.cols,logo.rows));

另一种定义ROI的方式是指定感兴趣行或列的范围(Range)。Range是指从起始索引到终止索引(不包括终止索引)的一连段连续序列。cRange可以用来定义Range。如果使用Range来定义ROI,那么前例中定义ROI的代码可以重写为:

//方法二
imageROI=image(Range(250,250+logoImage.rows),Range(200,200+logoImage .cols));

下面我们来看一个实例,显示如何利用ROI将一幅图加到另一幅图的指定位置。大家如果需要复制以下函数中的代码直接运行,可以自己建一个基于console的程序,然后把函数体中的内容复制到main函数中,然后找两幅大小合适的图片,加入到工程目录下,并和代码中读取的文件名一致即可。

在下面的代码中,我们通过一个图像掩膜(mask),直接将插入处的像素设置为logo图像的像素值,这样效果会很逼真。

//----------------------【头文件、命名空间包含部分】----------------------
//          描述:包含程序所使用的头文件和命名空间
//------------------------------------------------------------------
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <iostream>
using namespace std;
using namespace cv;


//---------------------------【main( )函数】---------------------------
//          描述:控制台应用程序的入口函数,我们的程序从这里开始执行
//------------------------------------------------------------------
int main( )
{

    //【1】读入图像
    Mat srcImage1= imread("/Users/cdq/Desktop/test.jpg");
    Mat logoImage= imread("/Users/cdq/Desktop/favicon.jpg");
    if(!srcImage1.data ) { printf("读取srcImage1错误~! 
"); }
    if(!logoImage.data ) { printf("读取logoImage错误~! 
"); }

    //【2】定义一个Mat类型并给其设定ROI区域
    Mat imageROI=srcImage1(Rect(200,250,logoImage.cols,logoImage.rows));

    //【3】加载掩模(必须是灰度图)
    Mat mask= imread("/Users/cdq/Desktop/favicon.jpg",0);

    //【4】将掩膜复制到ROI
    logoImage.copyTo(imageROI,mask);

    //【5】显示结果
    namedWindow("<1>利用ROI实现图像叠加示例窗口");
    imshow("<1>利用ROI实现图像叠加示例窗口",srcImage1);

    waitKey(0);
    
    return  0;
}

这个函数首先是载入了两张jpg图片到srcImage1和logoImage中,然后定义了一个Mat类型的imageROI,并使用Rect设置其感兴趣区域为srcImage1中的一块区域,将imageROI和srcImage1关联起来。接着定义了一个Mat类型的的mask并读入dota_logo.jpg,顺势使用Mat:: copyTo把mask中的内容复制到imageROI中,于是就得到了最终的效果图。namedWindow和imshow配合使用,显示出最终的结果。

线性混合操作

线性混合操作是一种典型的二元(两个输入)的像素操作,它的理论公式如下:

我们通过在范围0到1之间改变alpha值,来对两幅图像(f0(x)和f1(x))或两段视频(同样为(f0(x)和f1(x))产生时间上的画面叠化(cross-dissolve)效果,就像幻灯片放映和电影制作中的那样,也就是在幻灯片翻页时设置的前后页缓慢过渡叠加效果,以及电影情节过渡时经常出现的画面叠加效果。

实现方面,主要运用了OpenCV中addWeighted()函数,下面来一起全面地了解它。

这个函数的作用是计算两个数组(图像阵列)的加权和。原型如下:

void (InputArray src1, double alpha, InputArray src2, double beta, double gamma, OutputArray dst, int dtype=-1);

第一个参数,InputArray类型的src1,表示需要加权的第一个数组,常常填一个Mat;
第二个参数,double类型的alpha,表示第一个数组的权重;
第三个参数,InputArray类型的src2,表示第二个数组,它需要和第一个数组拥有相同的尺寸和通道数;
第四个参数,double类型的beta,表示第二个数组的权重值;
第五个参数,double类型的gamma,一个加到权重总和上的标量值。其含义通过接下来列出的式子自然会理解;
第六个参数,OutputArray类型的dst,输出的数组,它和输入的两个数组拥有相同的尺寸和通道数;
第七个参数,int类型的dtype,输出阵列的可选深度,有默认值-1。当两个输入数组具有相同的深度时,这个参数设置为-1(默认值),即等同于src1.depth( )。

下面的数学公式表示:用addWeighted函数计算以下两个数组(src1和src2)的加权和,得到结果输出给第四个参数,也就是addWeighted函数的作用的矩阵表达式。

dst = src1[I]*alpha+ src2[I]*beta + gamma;

其中I是多维数组元素的索引值。而且,在遇到多通道数组的时候,每个通道都需要独立地进行处理。另外需要注意的是,当输出数组的深度为CV_32S时,这个函数就不适用了,这时候就会内存溢出或者算出的结果压根不对。

理论和函数的讲解就是上面这些,接着我们来看代码实例,以融会贯通。
批注:未验证。

//----------------------【头文件、命名空间包含部分】----------------------
//          描述:包含程序所使用的头文件和命名空间
//------------------------------------------------------------------
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <iostream>
using namespace std;
using namespace cv;


//---------------------------【main( )函数】---------------------------
//          描述:控制台应用程序的入口函数,我们的程序从这里开始执行
//------------------------------------------------------------------
int main( )
{

    //【0】定义一些局部变量
    double alphaValue = 0.5;
    double betaValue;
    Mat srcImage2, srcImage3, dstImage;
    
    //【1】读取图像 ( 两幅图片需为同样的类型和尺寸 )
    srcImage2= imread("mogu.jpg");
    srcImage3= imread("rain.jpg");
    
    if(!srcImage2.data ) { printf("你妹,读取srcImage2错误~! 
"); return 0; }
    if(!srcImage3.data ) { printf("你妹,读取srcImage3错误~! 
"); return 0; }
    
    //【2】做图像混合加权操作
    betaValue= ( 1.0 - alphaValue );
    addWeighted(srcImage2, alphaValue, srcImage3, betaValue, 0.0, dstImage);
    
    //【3】创建并显示原图窗口
    namedWindow("<2>线性混合示例窗口【原图】 by浅墨", 1);
    imshow("<2>线性混合示例窗口【原图】 by浅墨", srcImage2 );
    
    namedWindow("<3>线性混合示例窗口【效果图】 by浅墨", 1);
    imshow("<3>线性混合示例窗口【效果图】 by浅墨", dstImage );
   

    waitKey(0);
    
    return  0;
}

批注:ROI是感兴趣区域,使用copy_to将覆盖原数据,使用addWeighted将进行图像混合。

分离颜色通道、多通道图像混合

而为了更好地观察一些图像材料的特征,有时需要对RGB三个颜色通道的分量进行分别显示和调整。通过OpenCV的split和merge方法可以很方便地达到目的。

split通道分离函数

split函数用于将一个多通道数组分离成几个单通道数组。
函数原型:

void split(const Mat& src, Mat* mvbegin);
void split(InputArray m,OutputArrayOfArrays mv);

第一个参数,InputArray类型的m或者const Mat&类型的src,填我们需要进行分离的多通道数组。
第二个参数,OutputArrayOfArrays类型的mv,填函数的输出数组或者输出的vector容器。

//----------------------【头文件、命名空间包含部分】----------------------
//          描述:包含程序所使用的头文件和命名空间
//------------------------------------------------------------------
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <iostream>
using namespace std;
using namespace cv;

//---------------------------【main( )函数】---------------------------
//          描述:控制台应用程序的入口函数,我们的程序从这里开始执行
//------------------------------------------------------------------
int main( )
{

    Mat srcImage;
    Mat imRed,imGreen,imBlue;
    vector<Mat> channels;
    srcImage= cv::imread("/Users/cdq/Desktop/2.jpg");

    // 把一个3通道图像转换成3个单通道图像
    split(srcImage,channels);//分离色彩通道
    imRed=channels.at(0);
    imGreen=channels.at(1);
    imBlue=channels.at(2);

    vector<Mat>Images(4);
    Images[0] = srcImage;
    Images[1] = imRed;
    Images[2] = imGreen;
    Images[3] = imBlue;

    imshow("src",srcImage);
    imshow("red",imRed);
    imshow("green",imGreen);
    imshow("blue",imBlue);
    waitKey(0);

    return  0;
}

merge通道合并函数

merge( )函数是split( )函数的逆向操作——将多个数组合并成一个多通道的数组。

void merge(const Mat* mv, size_tcount, OutputArray dst);
void merge(InputArrayOfArrays mv,OutputArray dst);

第一个参数,mv。填需要被合并的输入矩阵或vector容器的阵列,这个mv参数中所有的矩阵必须有着一样的尺寸和深度。
第二个参数,count。当mv为一个空白的C数组时,代表输入矩阵的个数,这个参数显然必须大于1。
第三个参数,dst。即输出矩阵,和mv[0]拥有一样的尺寸和深度,并且通道的数量是矩阵阵列中的通道的总数。

图像对比度、亮度值调整

首先了解一下算子的概念。一般的图像处理算子都是一个函数,它接受一个或多个输入图像,并产生输出图像。下面是算子的一般形式。

本节所讲解的图像亮度和对比度的调整操作,其实属于图像处理变换中比较简单的一种——点操作(pointoperators)。点操作有一个特点:仅仅根据输入像素值(有时可加上某些全局信息或参数),来计算相应的输出像素值。这类算子包括亮度(brightness)和对比度(contrast)调整、颜色校正(colorcorrection)和变换(transformations)。

访问图片中的像素

我们需要访问图像的每一个像素。因为是对GBR图像进行运算,每个像素有三个值(G、B、R),所以我们必须分别访问它们(OpenCV中的图像存储模式为GBR)。以下是访问像素的代码片段,使用了三个for循环。

//三个for循环,执行运算 new_image(i,j) =a*image(i,j) + b
for(int y = 0; y < image.rows; y++ )
    {
        for(int x = 0; x < image.cols; x++ )
        {
            for(int c = 0; c < 3; c++ )
            {
                  new_image.at<Vec3b>(y,x)[c]=saturate_cast<uchar>( (g_nContrastValue*0.01)*(image.at<Vec3b>(y,x)[ c] ) + g_nBrightValue );
            }
        }
    }
  • 为了访问图像的每一个像素,使用这样的语法:image.at<Vec3b>(y,x)[c]
    其中,y是像素所在的行,x是像素所在的列,c是R、G、B(对应0、1、2)其中之一。
  • 因为运算结果可能会超出像素取值范围(溢出),还可能是非整数(如果是浮点数的话),所以要用saturate_cast对结果进行转换,以确保它为有效值。
  • 这里的a也就是对比度,一般为了观察的效果,它的取值为0.0到3.0的浮点值,但是轨迹条一般取值都会取整数,因此在这里我们可以将其代表对比度值的nContrastValue参数设为0到300之间的整型,在最后的式子中乘以一个0.01,这样就完成了轨迹条中300个不同取值的变化。这就是为什么在式子中,会有saturate_cast<uchar>((g_nContrastValue* 0.01)* (image.at< Vec3b>(y,x)[c])+g_nBrightValue)中的g_nContrastValue* 0.01

离散傅里叶变换

离散傅里叶变换(Discrete Fourier Transform,缩写为DFT),是指傅里叶变换在时域和频域上都呈现离散的形式,将时域信号的采样变换为在离散时间傅里叶变换(DTFT)频域的采样。在形式上,变换两端(时域和频域上)的序列是有限长的,而实际上这两组序列都应当被认为是离散周期信号的主值序列。即使对有限长的离散信号做DFT,也应当对其经过周期延拓成为周期信号再进行变换。在实际应用中,通常采用快速傅里叶变换来高效计算DFT。

TODO.

输入输出XML和YAML文件

TODO.

图像处理

  • 三种线性滤波:方框滤波、均值滤波、高斯滤波
  • 两种非线性滤波:中值滤波、双边滤波
  • 7种图像处理形态学:腐蚀、膨胀,开运算、闭运算、形态学梯度、顶帽、黑帽

平滑处理

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

滤波器

滤波器的种类有很多,在新版本的OpenCV中,提供了如下5种常用的图像平滑处理操作方法,它们分别被封装在单独的函数中,使用起来非常方便。

  • 方框滤波——BoxBlur函数
  • 均值滤波(邻域平均滤波)——Blur函数
  • 高斯滤波——GaussianBlur函数
  • 中值滤波——medianBlur函数
  • 双边滤波——bilateralFilter函数

线性滤波器

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

几种常见的线性滤波器如下。

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

方框滤波

OpenCV还提供了方框滤波方式,与均值滤波的不同在于,方框滤波不会计算像素均值,在均值滤波中,滤波结果的像素值是任意一个点的邻域平均值,等于各邻域像素值之和除以领域面积,而在方框滤波中,可以自由选择是否对均值滤波的结果进行归一化,即可以自由选择滤波结果是邻域像素值之和的平均值,还是邻域像素值之和。

均值滤波是方框滤波归一化(normalized)后的特殊情况。

void boxFilter(InputArray src,OutputArray dst, int ddepth, Size ksize, Point anchor=Point(-1,-1), boolnormalize=true, int borderType=BORDER_DEFAULT);

第一个参数,InputArray类型的src,输入图像,即源图像,填Mat类的对象即可。该函数对通道是独立处理的,且可以处理任意通道数的图片。但需要注意,待处理的图片深度应该为CV_8U、CV_16U、CV_16S、CV_32F以及CV_64F之一。
第二个参数,OutputArray类型的dst,即目标图像,需要和源图片有一样的尺寸和类型。
第三个参数,int类型的ddepth,输出图像的深度,-1代表使用原图深度,即src.depth( )。
第四个参数,Size类型(对Size类型稍后有讲解)的ksize,内核的大小。一般用Size(w,h)来表示内核的大小,其中w为像素宽度,h为像素高度。Size(3,3)就表示3x3的核大小,Size(5,5)就表示5x5的核大小。
第五个参数,Point类型的anchor,表示锚点(即被平滑的那个点)。注意它有默认值Point(-1,-1)。如果这个点坐标是负值的话,就表示取核的中心为锚点,所以默认值Point(-1,-1)表示这个锚点在核的中心。
第六个参数,bool类型的normalize,默认值为true,一个标识符,表示内核是否被其区域归一化(normalized)了。
第七个参数,int类型的borderType,用于推断图像外部像素的某种边界模式。有默认值BORDER_DEFAULT,我们一般不去管它。

均值滤波

均值滤波,是最简单的一种滤波操作,输出图像的每一个像素是核窗口内输入图像对应像素的平均值(所有像素加权系数相等),其实说白了它就是归一化后的方框滤波。

均值滤波本身存在着固有的缺陷,即它不能很好地保护图像细节,在图像去噪的同时也破坏了图像的细节部分,从而使图像变得模糊,不能很好地去除噪声点。

void blur(InputArray src, OutputArraydst, Size ksize, Point anchor=Point(-1,-1), int borderType=BORDER_DEFAULT);

第一个参数,InputArray类型的src,输入图像,即源图像,填Mat类的对象即可。该函数对通道是独立处理的,且可以处理任意通道数的图片。但需要注意的是,待处理的图片深度应该为CV_8U、CV_16U、CV_16S、CV_32F以及CV_64F之一。
第二个参数,OutputArray类型的dst,即目标图像,需要和源图片有一样的尺寸和类型。比如可以用Mat::Clone,以源图片为模板,来初始化得到如假包换的目标图。
第三个参数,Size类型(对Size类型稍后有讲解)的ksize,内核的大小。一般写作Size(w,h)来表示内核的大小(其中,w为像素宽度,h为像素高度)。Size(3,3)就表示3×3的核大小,Size(5,5)就表示5×5的核大小
第四个参数,Point类型的anchor,表示锚点(即被平滑的那个点),注意它有默认值Point(-1,-1)。如果这个点坐标是负值,就表示取核的中心为锚点,所以默认值Point(-1,-1)表示这个锚点在核的中心。
第五个参数,int类型的borderType,用于推断图像外部像素的某种边界模式。有默认值BORDER_DEFAULT,我们一般不去管它。

高斯滤波

高斯滤波是一种线性平滑滤波,可以消除高斯噪声,广泛应用于图像处理的减噪过程。通俗地讲,高斯滤波就是对整幅图像进行加权平均的过程,每一个像素点的值,都由其本身和邻域内的其他像素值经过加权平均后得到。高斯滤波的具体操作是:用一个模板(或称卷积、掩模)扫描图像中的每一个像素,用模板确定的邻域内像素的加权平均灰度值去替代模板中心像素点的值。

大家常说高斯滤波是最有用的滤波操作,虽然它用起来效率往往不是最高的。高斯模糊技术生成的图像,其视觉效果就像是经过一个半透明屏幕在观察图像,这与镜头焦外成像效果散景以及普通照明阴影中的效果都明显不同。高斯平滑也用于计算机视觉算法中的预先处理阶段,以增强图像在不同比例大小下的图像效果(参见尺度空间表示以及尺度空间实现)。从数学的角度来看,图像的高斯模糊过程就是图像与正态分布做卷积。由于正态分布又叫作高斯分布,所以这项技术就叫作高斯模糊。

图像与圆形方框模糊做卷积将会生成更加精确的焦外成像效果。由于高斯函数的傅里叶变换是另外一个高斯函数,所以高斯模糊对于图像来说就是一个低通滤波操作。

高斯滤波器是一类根据高斯函数的形状来选择权值的线性平滑滤波器。高斯平滑滤波器对于抑制服从正态分布的噪声非常有效。

void GaussianBlur(InputArray src,OutputArray dst, Size ksize, double sigmaX, double sigmaY=0, intborderType=BORDER_DEFAULT);

第一个参数,InputArray类型的src,输入图像,即源图像,填Mat类的对象即可。它可以是单独的任意通道数的图片,但需要注意的是,其图片深度应该为CV_8U、CV_16U、CV_16S、CV_32F以及CV_64F之一。
第二个参数,OutputArray类型的dst,即目标图像,需要和源图片有一样的尺寸和类型。比如可以用Mat::Clone,以源图片为模板,来初始化得到如假包换的目标图。
第三个参数,Size类型的ksize高斯内核的大小。其中ksize.width和ksize.height可以不同,但它们都必须为正数和奇数,或者是零,这都由sigma计算而来。
第四个参数,double类型的sigmaX,表示高斯核函数在X方向的的标准偏差。
第五个参数,double类型的sigmaY,表示高斯核函数在Y方向的的标准偏差。若sigmaY为零,就将它设为sigmaX;如果sigmaX和sigmaY都是0,那么就由ksize.width和ksize.height计算出来。为了结果的正确性着想,最好是把第三个参数Size、第四个参数sigmaX和第五个参数sigmaY全部指定到。
第六个参数,int类型的borderType,用于推断图像外部像素的某种边界模式。有默认值BORDER_DEFAULT,我们一般不去管它。

非线性滤波器

上节中,我们所考虑的滤波器都是线性的,即两个信号之和的响应和它们各自响应之和相等。换句话说,每个像素的输出值是一些输入像素的加权和。线性滤波器易于构造,并且易于从频率响应角度来进行分析。

然而,在很多情况下,使用邻域像素的非线性滤波会得到更好的效果。比如在噪声是散粒噪声而不是高斯噪声,即图像偶尔会出现很大的值的时候,用高斯滤波器对图像进行模糊的话,噪声像素是不会被去除的,它们只是转换为更为柔和但仍然可见的散粒。这就到了中值滤波登场的时候了。

中值滤波

中值滤波(Median filter)是一种典型的非线性滤波技术,基本思想是用像素点邻域灰度值的中值来代替该像素点的灰度值,该方法在去除脉冲噪声、椒盐噪声的同时又能保留图像的边缘细节。

中值滤波是基于排序统计理论的一种能有效抑制噪声的非线性信号处理技术,其基本原理是把数字图像或数字序列中一点的值用该点的一个邻域中各点值的中值代替,让周围的像素值接近真实值,从而消除孤立的噪声点。这对于斑点噪声(speckle noise)和椒盐噪声(salt-and-pepper noise)来说尤其有用,因为它不依赖于邻域内那些与典型值差别很大的值。中值滤波器在处理连续图像窗函数时与线性滤波器的工作方式类似,但滤波过程却不再是加权运算。

中值滤波在一定的条件下可以克服常见线性滤波器,如最小均方滤波、方框滤波器、均值滤波等带来的图像细节模糊,而且对滤除脉冲干扰及图像扫描噪声非常有效,也常用于保护边缘信息。保存边缘的特性使它在不希望出现边缘模糊的场合也很有用,是非常经典的平滑噪声处理方法。

中值滤波与均值滤波器比较
优势:在均值滤波器中,由于噪声成分被放入平均计算中,所以输出受到了噪声的影响。但是在中值滤波器中,由于噪声成分很难选上,所以几乎不会影响到输出。因此同样用3×3区域进行处理,中值滤波消除的噪声能力更胜一筹。中值滤波无论是在消除噪声还是保存边缘方面都是一个不错的方法。

劣势:中值滤波花费的时间是均值滤波的5倍以上。

顾名思义,中值滤波选择每个像素的邻域像素中的中值作为输出,或者说中值滤波将每一像素点的灰度值设置为该点某邻域窗口内的所有像素点灰度值的中值。

中值滤波在一定条件下,可以克服线性滤波器(如均值滤波等)所带来的图像细节模糊,对滤除脉冲干扰即图像扫描噪声最为有效,而且在实际运算过程中并不需要图像的统计特性,也给计算带来不少方便。但是对一些细节(特别是细、尖顶等)多的图像不太适合。

void medianBlur(InputArray src,OutputArray dst, int ksize);

第一个参数,InputArray类型的src,函数的输入参数,填1、3或者4通道的Mat类型的图像。当ksize为3或者5的时候,图像深度需为CV_8U、CV_16U、CV_32F其中之一,而对于较大孔径尺寸的图片,它只能是CV_8U。
第二个参数:OutputArray类型的dst,即目标图像,函数的输出参数,需要和源图片有一样的尺寸和类型。我们可以用Mat::Clone,以源图片为模板,来初始化得到如假包换的目标图。
第三个参数:int类型的ksize,孔径的线性尺寸(aperture linear size),注意这个参数必须是大于1的奇数,比如:3、5、7、9……

双边滤波

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

void bilateralFilter(InputArray src, OutputArraydst, int d, double sigmaColor, double sigmaSpace, int borderType=BORDER_DEFAULT);

第一个参数:InputArray类型的src,输入图像,即源图像,需要为8位或者浮点型单通道、三通道的图像。
第二个参数:OutputArray类型的dst,即目标图像,需要和源图片有一样的尺寸和类型。
第三个参数:int类型的d,表示在过滤过程中每个像素邻域的直径。如果这个值被设为非正数,那么OpenCV会从第五个参数sigmaSpace来计算出它。
第四个参数:double类型的sigmaColor,颜色空间滤波器的sigma值。这个参数的值越大,就表明该像素邻域内有越宽广的颜色会被混合到一起,产生较大的半相等颜色区域。
第五个参数:double类型的sigmaSpace,坐标空间中滤波器的sigma值,坐标空间的标注方差。它的数值越大,意味着越远的像素会相互影响,从而使更大的区域中足够相似的颜色获取相同的颜色。当d>0时,d指定了邻域大小且与sigmaSpace无关。否则,d正比于sigmaSpace。
第六个参数:int类型的borderType,用于推断图像外部像素的某种边界模式。注意它有默认值BORDER_DEFAULT。

形态学滤波

形态学(morphology)一词通常表示生物学的一个分支,该分支主要研究动植物的形态和结构。而我们图像处理中的形态学,往往指的是数学形态学。下面一起来了解数学形态学的概念。
数学形态学(Mathematical morphology)是一门建立在格论和拓扑学基础之上的图像分析学科,是数学形态学图像处理的基本理论。其基本的运算包括:二值腐蚀和膨胀、二值开闭运算、骨架抽取、极限腐蚀、击中击不中变换、形态学梯度、Top-hat变换、颗粒分析、流域变换、灰值腐蚀和膨胀、灰值开闭运算、灰值形态学梯度等。

简单来讲,形态学操作就是基于形状的一系列图像处理操作。OpenCV为进行图像的形态学变换提供了快捷、方便的函数。最基本的形态学操作有两种,分别是:膨胀(dilate)与腐蚀(erode)。

膨胀与腐蚀能实现多种多样的功能,主要如下。

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

膨胀

膨胀(dilate)就是求局部最大值的操作。从数学角度来说,膨胀或者腐蚀操作就是将图像(或图像的一部分区域,称之为A)与核(称之为B)进行卷积。

核可以是任何形状和大小,它拥有一个单独定义出来的参考点,我们称其为锚点(anchorpoint)。多数情况下,核是一个小的,中间带有参考点和实心正方形或者圆盘。其实,可以把核视为模板或者掩码。

而膨胀就是求局部最大值的操作(元素或运算)。核B与图形卷积,即计算核B覆盖的区域的像素点的最大值,并把这个最大值赋值给参考点指定的像素。这样就会使图像中的高亮区域逐渐增长。

更多参考:https://blog.csdn.net/Chaolei3/article/details/79618602

void dilate(   
        InputArray src,   
        OutputArray dst,   
        InputArray kernel,   
        Point anchor=Point(-1,-1),   
        int iterations=1,   
        int borderType=BORDER_CONSTANT,   
        const Scalar& borderValue=morphologyDefaultBorderValue( )    
    );

第一个参数,InputArray类型的src,输入图像,即源图像,填Mat类的对象即可。图像通道的数量可以是任意的,但图像深度应为CV_8U、CV_16U、CV_16S、CV_32F或CV_64F其中之一。
第二个参数,OutputArray类型的dst,即目标图像,需要和源图片有一样的尺寸和类型。
第三个参数,InputArray类型的kernel,膨胀操作的核。当为NULL时,表示的是使用参考点位于中心3×3的核。
我们一般使用函数getStructuringElement配合这个参数的使用。getStructuringElement函数会返回指定形状和尺寸的结构元素(内核矩阵)。其中,getStructuringElement函数的第一个参数表示内核的形状,有如下三种形状可以选择。

  • 矩形:MORPH_RECT;
  • 交叉形:MORPH_CROSS;
  • 椭圆形:MORPH_ELLIPSE。

而getStructuringElement函数的第二和第三个参数分别是内核的尺寸以及锚点(原点)的位置。
第四个参数,Point类型的anchor,锚的位置,其有默认值(-1,-1),表示锚位于中心。
第五个参数,int类型的iterations,迭代使用erode( )函数的次数,默认值为1。
第六个参数,int类型的borderType,用于推断图像外部像素的某种边界模式。注意它有默认值BORDER_DEFAULT。
第七个参数,const Scalar&类型的borderValue,当边界为常数时的边界值,有默认值morphologyDefaultBorderValue( ),一般不用去管它。需要用到它时,可以看官方文档中的createMorphologyFilter( )函数,以得到更详细的解释。

腐蚀

腐蚀是膨胀相反,腐蚀是求局部最小值的操作(元素与运算)

void erode(   
        InputArray src,   
        OutputArray dst,   
        InputArray kernel,   
        Point anchor=Point(-1,-1),   
        int iterations=1,   
        int borderType=BORDER_CONSTANT,   
        const Scalar& borderValue=morphologyDefaultBorderValue( )   
     );

第一个参数,InputArray类型的src,输入图像,即源图像,填Mat类的对象即可。图像通道的数量可以是任意的,但图像深度应为CV_8U、CV_16U、CV_16S、CV_32F或CV_64F其中之一。
第二个参数,OutputArray类型的dst,即目标图像,需要和源图片有一样的尺寸和类型。
第三个参数,InputArray类型的kernel,腐蚀操作的内核。为NULL时,表示的是使用参考点位于中心3x3的核。一般使用函数getStructuringElement配合这个参数的使用。getStructuringElement函数会返回指定形状和尺寸的结构元素(内核矩阵,具体看上文中dilate函数的第三个参数讲解部分。
第四个参数,Point类型的anchor,锚的位置。其有默认值(-1,-1),表示锚位于单位(element)的中心,一般不用管它。
第五个参数,int类型的iterations,迭代使用erode( )函数的次数,默认值为1。
第六个参数,int类型的borderType,用于推断图像外部像素的某种边界模式。注意它有默认值BORDER_DEFAULT。
第七个参数,const Scalar&类型的borderValue,当边界为常数时的边界值,有默认值morphologyDefaultBorderValue( ),一般不用去管它。需要用到它时,可以看官方文档中的createMorphologyFilter( )函数以得到更详细的解释。

开运算

开运算(Opening Operation),其实就是先腐蚀后膨胀的过程。

dst=open(src,element)=dilate(erode(src,element)); 

闭运算

先膨胀后腐蚀的过程称为闭运算(Closing Operation)。

dst=clese(src,element)= erode(dilate(src,element));

形态学梯度

形态学梯度(Morphological Gradient)是膨胀图与腐蚀图之差。

dst=morph-grad(src,element)= dilate(src,element)- erode(src,element);

顶帽

顶帽运算(Top Hat)又常常被译为”礼帽“运算,是原图像与上文刚刚介绍的“开运算”的结果图之差。

dst=tophat(src,element)=src-open(src,element);

黑帽

黑帽(Black Hat)运算是闭运算的结果图与原图像之差。

dst=blackhat (src,element)=close(src,element)- src;   

黑帽运算后的效果图突出了比原图轮廓周围的区域更暗的区域,且这一操作和选择的核的大小相关。

所以,黑帽运算用来分离比邻近点暗一些的斑块,效果图有着非常完美的轮廓。

漫水填充

所谓漫水填充,简单来说,就是自动选中了和种子点相连的区域,接着将该区域替换成指定的颜色,这是个非常有用的功能,经常用来标记或者分离图像的一部分进行处理或分析。漫水填充也可以用来从输入图像获取掩码区域,掩码会加速处理过程,或者只处理掩码指定的像素点。

以此填充算法为基础,类似PhotoShop的魔术棒选择工具就很容易实现了。漫水填充(FloodFill)是查找和种子点连通的颜色相同的点,魔术棒选择工具则是查找和种子点连通的颜色相近的点,把和初始种子像素颜色相近的点压进栈做为新种子。

函数原型:

int floodFill(InputOutputArray image, Point seedPoint, Scalar newVal, Rect*rect=0, Scalar loDiff=Scalar( ), Scalar upDiff=Scalar( ), int flags=4 );
int floodFill(InputOutputArray image, InputOutputArray mask, Point seedPoint,Scalar newVal, Rect*rect=0, Scalar loDiff=Scalar( ), Scalar upDiff=Scalar( ), int flags=4 );

第一个参数,InputOutputArray类型的image,输入/输出1通道或3通道,8位或浮点图像,具体参数由之后的参数指明。
第二个参数,InputOutputArray类型的mask,这是第二个版本的floodFill独享的参数,表示操作掩模。它应该为单通道,8位,长和宽上都比输入图像image大两个像素点的图像。第二个版本的floodFill需要使用以及更新掩膜,所以对于这个mask参数,我们一定要将其准备好并填在此处。需要注意的是,漫水填充不会填充掩膜mask的非零像素区域。例如,一个边缘检测算子的输出可以用来作为掩膜,以防止填充到边缘。同样的,也可以在多次的函数调用中使用同一个掩膜,以保证填充的区域不会重叠。另外需要注意的是,掩膜mask会比需填充的图像大,所以mask中与输入图像(x,y)像素点相对应的点的坐标为(x+1,y+1)。
第三个参数,Point类型的seedPoint,漫水填充算法的起始点。
第四个参数,Scalar类型的newVal,像素点被染色的值,即在重绘区域像素的新值。
第五个参数,Rect* 类型的rect,有默认值0,一个可选的参数,用于设置floodFill函数将要重绘区域的最小边界矩形区域。
第六个参数,Scalar类型的loDiff,有默认值Scalar( ),表示当前观察像素值与其部件邻域像素值或者待加入该部件的种子像素之间的亮度或颜色之负差(lower brightness/color difference)的最大值。
第七个参数,Scalar类型的upDiff,有默认值Scalar( ),表示当前观察像素值与其部件邻域像素值或者待加入该部件的种子像素之间的亮度或颜色之正差(lower brightness/color difference)的最大值。
第八个参数,int类型的flags,操作标志符,此参数包含三个部分,比较复杂。

例程:

#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_dstImage, g_grayImage, g_maskImage;       //定义原始图,目标图,灰度图,掩模图(进一步控制那些区域将被填充颜色)
int g_nFillMode = 1;        //漫水填充的模式
int g_nLowDifference = 20, g_nUpDifference = 20;        //负差最大值,正差最大值
int g_nConnectivity = 4;        //表示floodFill函数标识符低8位的连通值,4,只考虑像素水平和垂直方向的连接点
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;
//调用floodFill函数之前的参数准备
    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_nNewMaskVal左移8位的值,16-23位为CV_FLOODFILL_FIXED_RANGE或者0
    //低8位用于控制算法的连通性,可取4(填充算法只考虑当前享受水平方向和垂直方向)/8(还考虑对角线方向),
    //高8位可为0/FLOODFILL_FIXED_RANGE(考虑当前像素与种子像素之间的差)/FLOODFILL_MASK_ONLY(不填充改变原始图像,去填充掩模图像)
    //中间8位制定填充掩码图像的值,
    int flags = g_nConnectivity + (g_nNewMaskVal << 8) + (g_nFillMode == 1 ? FLOODFILL_FIXED_RANGE : 0);

    //随机生成bgr值
    int b = (unsigned)theRNG() & 255;
    int g = (unsigned)theRNG() & 255;
    int r = (unsigned)theRNG() & 255;
    //重绘区域的最小边界矩阵区域
    Rect ccomp;
    //重绘区域享受的新值,若为彩色图,取Scalar(b,g,r),若为灰度图,取Scalar(r * 0.299 + g * 0.587 + b * 0.114)
    //bgr是随机的
    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)
    {
        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 << "个像素被重绘
" << endl;

}

int main()
{

    g_srcImage = imread("/Users/cdq/Desktop/2.jpg");
    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);


    //创建Trackbar
    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 << "程序退出" << endl;
            break;
        }

//根据案件不同 进行各种操作
        switch ((char) c)
        {
            //按"1",效果图在灰度图,彩色图之间相互转换
        case '1':
            if (g_bIsColor)     //原来为彩色图,转换为灰色图,并将掩模mask所有原始设为0
            {
                cout << "键盘“1”被按下,切换彩色/灰度模式" << endl;
                cvtColor(g_srcImage, g_grayImage, COLOR_BGR2GRAY);
                g_maskImage = Scalar::all(0);       //将所有元素设置为0
                g_bIsColor = false;         //将标识符置为false,表示当前图像为灰度
            }
            else                    //若原来为灰度值,将彩图image0再复制给image,病将mask设为0
            {
                cout << "键盘“1”被按下,切换彩色/灰度模式" << endl;
                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,恢复原始图像" << endl;
            g_srcImage.copyTo(g_dstImage);
            cvtColor(g_dstImage, g_grayImage, COLOR_BGR2GRAY);
            g_maskImage = Scalar::all(0);
            break;

            //按“4”,使用空范围的漫水填充
        case '4':
            cout << "按4,使用空范围漫水填充" << endl;
            g_nFillMode = 0;
            break;

            //按“5”,使用渐变,固定范围的漫水填充
        case '5':
            cout << "按5,使用渐变,固定的漫水填充" << endl;
            g_nFillMode = 1;
            break;

            //按“6”,使用渐变,浮动范围的漫水填充
        case '6':
            cout << "按6,使用渐变,浮动范围的漫水填充" << endl;
            g_nFillMode = 2;
            break;

            //按7,操作标识符的低8位使用4位的链接模式
        case '7':
            cout << "按7,操作标识符的低8位使用4位的链接模式" << endl;
            g_nConnectivity = 4;
            break;

            //按8,标识符的低8位使用8位的链接模式
        case '8':
            cout << "按8,标识符的低8位使用8位的链接模式" << endl;
            g_nConnectivity = 8;
            break;
        default:
            break;
        }
    }
    return 0;
}

图像金字塔与图片尺寸缩放

我们经常会将某种尺寸的图像转换为其他尺寸的图像,如果要放大或者缩小图片的尺寸,笼统来说,可以使用OpenCV提供的如下两种方法。

  • resize函数。这是最直接的方式
  • pyrUp( )、pyrDown( )函数。即图像金字塔相关的两个函数,对图像进行向上采样和向下采样的操作。

图像金字塔

图像金字塔是图像中多尺度表达的一种,最主要用于图像的分割,是一种以多分辨率来解释图像的有效但概念简单的结构。

图像金字塔最初用于机器视觉和图像压缩,一幅图像的金字塔是一系列以金字塔形状排列的,分辨率逐步降低且来源于同一张原始图的图像集合。其通过梯次向下采样获得,直到达到某个终止条件才停止采样。

金字塔的底部是待处理图像的高分辨率表示,而顶部是低分辨率的近似。

我们将一层一层的图像比喻成金字塔,层级越高,则图像越小,分辨率越低。

  • 高斯金字塔(Gaussianpyramid)——用来向下采样,主要的图像金字塔。
  • 拉普拉斯金字塔(Laplacianpyramid)——用来从金字塔低层图像重建上层未采样图像,在数字图像处理中也即是预测残差,可以对图像进行最大程度的还原,配合高斯金字塔一起使用。

两者的简要区别在于:高斯金字塔用来向下降采样图像,而拉普拉斯金字塔则用来从金字塔底层图像中向上采样,重建一个图像。

要从金字塔第i层生成第i+1层(我们将第i+1层表示为Gi+1 ),我们先要用高斯核对Gi 进行卷积,然后删除所有偶数行和偶数列,新得到图像面积会变为源图像的四分之一。按上述过程对输入图像G0 执行操作就可产生出整个金字塔。

当图像向金字塔的上层移动时,尺寸和分辨率会降低。OpenCV中,从金字塔中上一级图像生成下一级图像的可以用PryDown,而通过PryUp将现有的图像在每个维度都放大两遍。

图像金字塔中的向上和向下采样分别通过OpenCV的函数pyrUp和pyrDown实现。

概括起来就是:

  • 对图像向上采样——pyrUp函数;
  • 对图像向下采样——pyrDown函数。

这里的向下与向上采样,是针对图像的尺寸而言的(和金字塔的方向相反),向上就是图像尺寸加倍向下就是图像尺寸减半。而如果按图6.55和图6.56中演示的金字塔方向来理解,金字塔向上图像其实在缩小,这样刚好是反过来了。

但需要注意的是,PryUp和PryDown不是互逆的,即PryUp不是降采样的逆操作。这种情况下,图像首先在每个维度上扩大为原来的两倍,新增的行(偶数行)以0填充。然后给指定的滤波器进行卷积(实际上是一个在每个维度都扩大为原来两倍的过滤器)去估计“丢失”像素的近似值。

PryDown( )是一个会丢失信息的函数。为了恢复原来更高的分辨率的图像,我们要获得由降采样操作丢失的信息,这些数据就和拉普拉斯金字塔有关系了。

高斯金字塔

高斯金字塔是通过高斯平滑和亚采样获得一些列下采样图像,也就是说第K层高斯金字塔通过平滑、亚采样就可以获得K+1层高斯图像。高斯金字塔包含了一系列低通滤波器,其截止频率从上一层到下一层以因子2逐渐增加,所以高斯金字塔可以跨越很大的频率范围。

另外,每一层都按从下到上的次序编号,层级Gi+1 (表示为Gi+1 尺寸小于第i层Gi )。

向下取样
为了获取层级为Gi+1 的金字塔图像,我们采用如下方法:

(1)对图像Gi 进行高斯内核卷积;

(2)将所有偶数行和列去除。

得到的图像即为Gi+1 的图像。显而易见,结果图像只有原图的四分之一。通过对输入图像Gi (原始图像)不停迭代以上步骤就会得到整个金字塔。同时我们也可以看到,向下取样会逐渐丢失图像的信息。

以上就是对图像的向下取样操作,即缩小图像。

向上取样

如果想放大图像,则需要通过向上取样操作得到,具体做法如下。

(1)将图像在每个方向扩大为原来的两倍,新增的行和列以0填充。

(2)使用先前同样的内核(乘以4)与放大后的图像卷积,获得“新增像素”的近似值。

得到的图像即为放大后的图像,但是与原来的图像相比会发觉比较模糊,因为在缩放的过程中已经丢失了一些信息。如果想在缩小和放大整个过程中减少信息的丢失,这些数据就形成了拉普拉斯金字塔。

接下来一起看一下拉普拉斯金字塔的概念。

拉普拉斯金字塔

下式是拉普拉斯金字塔第i层的数学定义:

式中的Gi 表示第i层的图像。而UP( )操作是将源图像中位置为(x,y)的像素映射到目标图像的(2x+1,2y+1)位置,即在进行向上取样。符号⊗表示卷积,g5x5 为5x5的高斯内核。

我们下文将要介绍的pryUp,就是在进行上面这个式子的运算。

因此,我们可以直接用OpenCV进行拉普拉斯运算:Li =Gi -PyrUp(Gi+1 )

也就是说,拉普拉斯金字塔是通过源图像减去先缩小后再放大的图像的一系列图像构成的。

所以,我们可以将拉普拉斯金字塔理解为高斯金字塔的逆形式。

另外再提一点,关于图像金字塔非常重要的一个应用就是图像分割。图像分割的话,先要建立一个图像金字塔,然后对Gi 和Gi+1 的像素直接依照对应的关系,建立起“父与子”关系。而快速初始分割可以先在金字塔高层的低分辨率图像上完成,然后逐层对分割加以优化。

resize函数

resize()为OpenCV中专门用来调整图像大小的函数。

此函数将源图像精确地转换为指定尺寸的目标图像。如果源图像中设置了ROI(Region Of Interest ,感兴趣区域),那么resize( )函数会对源图像的ROI区域进行调整图像尺寸的操作,来输出到目标图像中。若目标图像中已经设置了ROI区域,不难理解resize( )将会对源图像进行尺寸调整并填充到目标图像的ROI中。

很多时候,我们并不用考虑第二个参数dst的初始图像尺寸和类型(即直接定义一个Mat类型,不用对其初始化),因为其尺寸和类型可以由src、dsize、fx和fy这几个参数来确定。

void resize(InputArray src,OutputArray dst, Size dsize, double fx=0, double fy=0, int interpolation=INTER_LINEAR);

第一个参数,InputArray类型的src,输入图像,即源图像,填Mat类的对象即可。
第二个参数,OutputArray类型的dst,输出图像,当其非零时,有着dsize (第三个参数)的尺寸,或者由src.size( )计算出来。
第三个参数,Size类型的dsize,输出图像的大小。如果它等于零,由下式进行计算:
dsize=Size(round(fx* src.cols),round(fy* src.rows))
其中,dsize、fx、fy都不能为0。
第四个参数,double类型的fx,沿水平轴的缩放系数,有默认值0,且当其等于0时,由下式进行计算:
(double)dsize.width/src.cols
第五个参数,double类型的fy,沿垂直轴的缩放系数,有默认值0,且当其等于0时,由下式进行计算:
(double)dsize.height/src.rows
第六个参数,int类型的interpolation,用于指定插值方式,默认为INTER_LINEAR(线性插值)。
可选的插值方式如下:

  • INTER_NEAREST——最近邻插值
  • INTER_LINEAR——线性插值(默认值)
  • INTER_AREA——区域插值(利用像素区域关系的重采样插值)
  • INTER_CUBIC——三次样条插值(超过4×4像素邻域内的双三次插值)
  • INTER_LANCZOS4——Lanczos插值(超过8×8像素邻域的Lanczos插值)

若要缩小图像,一般情况下最好用CV_INTER_AREA来插值;而若要放大图像,一般情况下最好用CV_INTER_CUBIC(效率不高,慢,不推荐使用)或CV_INTER_LINEAR(效率较高,速度较快,推荐使用)。

使用样例:

Mat dstImage=Mat::zeros(512 ,512, CV_8UC3 );//新建一张512x512尺寸的图片   
Mat srcImage=imread(“1.jpg”);   
//显式指定dsize=dstImage.size( ),那么fx和fy会其计算出来,不用额外指定。   
resize(srcImage, dstImage, dstImage.size( ));



Mat dstImage;   
Mat srcImage=imread(“1.jpg”)   
//指定fx和fy,让函数计算出目标图像的大小。   
resize(srcImage, dstImage, Size( ), 0.5, 0.5);

pyrUp()函数

pyrUp( )函数的作用是向上采样并模糊一张图像,说白了就是放大一张图片。

void pyrUp(InputArray src, OutputArraydst, const Size& dstsize=Size( ), int borderType=BORDER_DEFAULT);

第一个参数,InputArray类型的src,输入图像,即源图像,填Mat类的对象即可。
第二个参数,OutputArray类型的dst,输出图像,和源图片有一样的尺寸和类型。
第三个参数,const Size&类型的dstsize,输出图像的大小;有默认值Size( ),即默认情况下,由Size(src.cols* 2,src.rows* 2)来进行计算,且一直需要满足下列条件:
第四个参数,int类型的borderType,边界模式,一般不用去管它。

pyrDown()函数

pyrDown( )函数的作用是向下采样并模糊一张图片,说白了就是缩小一张图片。

void pyrDown(InputArray src,OutputArray dst, const Size& dstsize=Size( ), int borderType=BORDER_DEFAULT);

第一个参数,InputArray类型的src,输入图像,即源图像,填Mat类的对象即可。
第二个参数,OutputArray类型的dst,输出图像,和源图片有一样的尺寸和类型。
第三个参数,const Size&类型的dstsize,输出图像的大小;有默认值Size( ),即默认情况下,由Size Size((src.cols+1)/2,(src.rows+1)/2)来进行计算,且一直需要满足下列条件:
该pyrDown函数执行了高斯金字塔建造的向下采样的步骤。首先,它将源图像与如下内核做卷积运算:
接着,它通过对图像的偶数行和列做插值来进行向下采样操作。

阈值化

在对各种图形进行处理操作的过程中,我们常常需要对图像中的像素做出取舍与决策,直接剔除一些低于或者高于一定值的像素。

阈值可以被视作最简单的图像分割方法。比如,从一副图像中利用阈值分割出我们需要的物体部分(当然这里的物体可以是一部分或者整体)。这样的图像分割方法基于图像中物体与背景之间的灰度差异,而且此分割属于像素级的分割。为了从一副图像中提取出我们需要的部分,应该用图像中的每一个像素点的灰度值与选取的阈值进行比较,并作出相应的判断。注意:阈值的选取依赖于具体的问题。即物体在不同的图像中有可能会有不同的灰度值。

一旦找到了需要分割的物体的像素点,可以对这些像素点设定一些特定的值来表示。例如,可以将该物体的像素点的灰度值设定为“0”(黑色),其他的像素点的灰度值为“255”(白色)。当然像素点的灰度值可以任意,但最好设定的两种颜色对比度较强,以方便观察结果。

在OpenCV 2.X中,Threshold( )函数(基本阈值操作)和adaptiveThreshold( )函数(自适应阈值操作)可以完成这样的要求。它们的基本思想是:给定一个数组和一个阈值,然后根据数组中的每个元素的值是高于还是低于阈值而进行一些处理。

批注:阈值化即二值化。

Threshold()函数

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

double threshold(InputArray src,OutputArray dst, double thresh, double maxval, int type);

第一个参数,InputArray类型的src,输入数组,填单通道,8或32位浮点类型的Mat即可。
第二个参数,OutputArray类型的dst,函数调用后的运算结果存在这里,即这个参数用于存放输出结果,且和第一个参数中的Mat变量有一样的尺寸和类型。
第三个参数,double类型的thresh,阈值的具体值。
第四个参数,double类型的maxval,当第五个参数阈值类型type取CV_THRESH_BINARY或CV_THRESH_BINARY_INV时阈值类型时的最大值(对应地,OpenCV2中可以为CV_THRESH_BINARY和CV_THRESH_BINARY_INV)。
第五个参数,int类型的type,阈值类型。threshold( )函数支持的对图像取阈值的方法由其确定。

adaptiveThreshold()函数

adaptiveThreshold( )函数的作用是对矩阵采用自适应阈值操作,支持就地操作。函数原型如下。

void adaptiveThreshold(InputArray src, OutputArray dst, double maxValue, int adaptiveMethod, int thresholdType, int blockSize, double C);

第一个参数,InputArray类型的src,输入图像,即源图像,填Mat类的对象即可,且需为8位单通道浮点型图像。
第二个参数,OutputArray类型的dst,函数调用后的运算结果存在这里,需和源图片有一样的尺寸和类型。
第三个参数,double类型的maxValue,给像素赋的满足条件的非零值。具体看下面的讲解。
第四个参数,int类型的adaptiveMethod,用于指定要使用的自适应阈值算法,可取值为ADAPTIVE_THRESH_MEAN_C或ADAPTIVE_THRESH_GAUSSIAN_C。
第五个参数,int类型的thresholdType,阈值类型。取值必须为THRESH_BINARY、THRESH_BINARY_INV其中之一。
第六个参数,int类型的blockSize,用于计算阈值大小的一个像素的邻域尺寸,取值为3、5、7等。
第七个参数,double类型的C,减去平均或加权平均值后的常数值。通常其为正数,但少数情况下也可以为零或负数。

图像变换

边缘检测

边缘检测的一般步骤

【第一步】滤波
边缘检测的算法主要是基于图像强度的一阶和二阶导数,但导数通常对噪声很敏感,因此必须采用滤波器来改善与噪声有关的边缘检测器的性能。常见的滤波方法主要有高斯滤波,即采用离散化的高斯函数产生一组归一化的高斯核,然后基于高斯核函数对图像灰度矩阵的每一点进行加权求和。

【第二步】增强
增强边缘的基础是确定图像各点邻域强度的变化值。增强算法可以将图像灰度点邻域强度值有显著变化的点凸显出来。在具体编程实现时,可通过计算梯度幅值来确定。

【第三步】检测
经过增强的图像,往往邻域中有很多点的梯度值比较大,而在特定的应用中,这些点并不是要找的边缘点,所以应该采用某种方法来对这些点进行取舍。实际工程中,常用的方法是通过阈值化方法来检测。

canny算子

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

其中,Canny的目标是找到一个最优的边缘检测算法,让我们看一下最优边缘检测的三个主要评价标准。

  • 低错误率:标识出尽可能多的实际边缘,同时尽可能地减少噪声产生的误报。
  • 高定位性:标识出的边缘要与图像中的实际边缘尽可能接近。
  • 最小响应:图像中的边缘只能标识一次,并且可能存在的图像噪声不应标识为边缘。

为了满足这些要求,Canny使用了变分法,这是一种寻找满足特定功能的函数的方法。最优检测用4个指数函数项的和表示,但是它非常近似于高斯函数的一阶导数。

Canny函数利用Canny算子来进行图像的边缘检测操作。

void Canny(InputArray image,OutputArray edges, double threshold1, double threshold2, int apertureSize=3,bool L2gradient=false);

第一个参数,InputArray类型的image,输入图像,即源图像,填Mat类的对象即可,且需为单通道8位图像。
第二个参数,OutputArray类型的edges,输出的边缘图,需要和源图片有一样的尺寸和类型。
第三个参数,double类型的threshold1,第一个滞后性阈值。
第四个参数,double类型的threshold2,第二个滞后性阈值。
第五个参数,int类型的apertureSize,表示应用Sobel算子的孔径大小,其有默认值3。
第六个参数,bool类型的L2gradient,一个计算图像梯度幅值的标识,有默认值false。

sobel算子

Sobel算子是一个主要用于边缘检测的离散微分算子(discrete differentiation operator)。它结合了高斯平滑和微分求导,用来计算图像灰度函数的近似梯度。在图像的任何一点使用此算子,都将会产生对应的梯度矢量或是其法矢量。

Sobel函数使用扩展的Sobel算子,来计算一阶、二阶、三阶或混合图像差分。

void Sobel (   
    InputArray src,  
     OutputArray dst,  
     int ddepth,  
     int dx,   
     int dy,   
     int ksize=3,   
     double scale=1,   
     double delta=0,   
     int borderType=BORDER_DEFAULT );

(1)第一个参数,InputArray类型的src,为输入图像,填Mat类型即可。
(2)第二个参数,OutputArray类型的dst,即目标图像,函数的输出参数,需要和源图片有一样的尺寸和类型。
(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处得到更详细的信息。

Laplacian算子

Laplacian函数可以计算出图像经过拉普拉斯变换后的结果。

void Laplacian(InputArray src,OutputArray dst, int ddepth, int ksize=1, double scale=1, double delta=0, intborderType=BORDER_DEFAULT );   

第一个参数,InputArray类型的image,输入图像,即源图像,填Mat类的对象即可,且需为单通道8位图像。
第二个参数,OutputArray类型的edges,输出的边缘图,需要和源图片有一样的尺寸和通道数。
第三个参数,int类型的ddept,目标图像的深度。
第四个参数,int类型的ksize,用于计算二阶导数的滤波器的孔径尺寸,大小必须为正奇数,且有默认值1。
第五个参数,double类型的scale,计算拉普拉斯值的时候可选的比例因子,有默认值1。
第六个参数,double类型的delta,表示在结果存入目标图(第二个参数dst)之前可选的delta值,有默认值0。
第七个参数,int类型的borderType,边界模式,默认值为BORDER_DEFAULT。这个参数可以在官方文档中borderInterpolate( )处得到更详细的信息。

scharr滤波器

我们一般直接称scharr为滤波器,而不是算子。它在OpenCV中主要是配合Sobel算子的运算而存在的。

使用Scharr滤波器运算符计算x或y方向的图像差分。其实它的参数变量和Sobel基本上是一样的,除了没有ksize核的大小。

void Scharr(   
    InputArray src, //源图   
     OutputArray dst, //目标图   
     int ddepth,//图像深度   
     int dx,// x方向上的差分阶数   
     int dy,//y方向上的差分阶数   
     double scale=1,//缩放因子   
     double delta=0,// delta值   
     intborderType=BORDER_DEFAULT );// 边界模式

(1)第一个参数,InputArray类型的src,为输入图像,填Mat类型即可。
(2)第二个参数,OutputArray类型的dst,即目标图像,函数的输出参数,需要和源图片有一样的尺寸和类型。
(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,表示默认情况下是没有应用缩放的。我们可以在文档中查阅getDerivKernels的相关介绍,来得到这个参数的更多信息。
(7)第七个参数,double类型的delta,表示在结果存入目标图(第二个参数dst)之前可选的delta值,有默认值0。
(8)第八个参数,int类型的borderType,边界模式,默认值为BORDER_DEFAULT。这个参数可以在官方文档中borderInterpolate处得到更详细的信息。

霍夫变换

霍夫变换(Hough Transform)是图像处理中的一种特征提取技术,该过程在一个参数空间中通过计算累计结果的局部最大值得到一个符合该特定形状的集合作为霍夫变换结果。霍夫变换于1962年由PaulHough首次提出,最初的Hough变换是设计用来检测直线和曲线的。起初的方法要求知道物体边界线的解析方程,但不需要有关区域位置的先验知识。这种方法的一个突出优点是分割结果的Robustness,即对数据的不完全或噪声不是非常敏感。然而,要获得描述边界的解析表达常常是不可能的。后于1972年由Richard Duda & Peter Hart推广使用,经典霍夫变换用来检测图像中的直线,后来霍夫变换扩展到任意形状物体的识别,多为圆和椭圆。霍夫变换运用两个坐标空间之间的变换将在一个空间中具有相同形状的曲线或直线映射到另一个坐标空间的一个点上形成峰值,从而把检测任意形状的问题转化为统计峰值问题。

我们知道,霍夫线变换是一种用来寻找直线的方法.在使用霍夫线变换之前,首先要对图像进行边缘检测的处理,即霍夫线变换的直接输入只能是边缘二值图像。

OpenCV支持三种不同的霍夫线变换,它们分别是:标准霍夫变换(Standard Hough Transform,SHT)、多尺度霍夫变换(Multi-Scale Hough Transform,MSHT)和累计概率霍夫变换(Progressive Probabilistic Hough Transform,PPHT)。

其中,多尺度霍夫变换(MSHT)为经典霍夫变换(SHT)在多尺度下的一个变种。而累计概率霍夫变换(PPHT)算法是标准霍夫变换(SHT)算法的一个改进,它在一定的范围内进行霍夫变换,计算单独线段的方向以及范围,从而减少计算量,缩短计算时间。之所以称PPHT为“概率”的,是因为并不将累加器平面内的所有可能的点累加,而只是累加其中的一部分,该想法是如果峰值如果足够高,只用一小部分时间去寻找它就够了。按照猜想,可以实质性地减少计算时间。

在OpenCV中,可以用HoughLines函数来调用标准霍夫变换(SHT)和多尺度霍夫变换(MSHT)。

而HoughLinesP函数用于调用累计概率霍夫变换PPHT。累计概率霍夫变换执行效率很高,所有相比于HoughLines函数,我们更倾向于使用HoughLinesP函数。

标准霍夫变换:HoughLines( )函数

void HoughLines(InputArray image, OutputArray lines, double rho, double theta, int threshold, double srn=0, double stn=0);   

第一个参数,InputArray类型的image,输入图像,即源图像。需为8位的单通道二进制图像,可以将任意的源图载入进来,并由函数修改成此格式后,再填在这里。
第二个参数,InputArray类型的lines,经过调用HoughLines函数后储存了霍夫线变换检测到线条的输出矢量。每一条线由具有两个元素的矢量(ρ,θ)表示,其中,ρ是离坐标原点(0,0)(也就是图像的左上角)的距离,θ是弧度线条旋转角度(0度表示垂直线,π/2度表示水平线)。
第三个参数,double类型的rho,以像素为单位的距离精度。另一种表述方式是直线搜索时的进步尺寸的单位半径。(Latex中/rho即表示ρ)
第四个参数,double类型的theta,以弧度为单位的角度精度。另一种表述方式是直线搜索时的进步尺寸的单位角度。
第五个参数,int类型的threshold,累加平面的阈值参数,即识别某部分为图中的一条直线时它在累加平面中必须达到的值。大于阈值threshold的线段才可以被检测通过并返回到结果中。
第六个参数,double类型的srn,有默认值0。对于多尺度的霍夫变换,这是第三个参数进步尺寸rho的除数距离。粗略的累加器进步尺寸直接是第三个参数rho,而精确的累加器进步尺寸为rho/srn。
第七个参数,double类型的stn,有默认值0,对于多尺度霍夫变换,srn表示第四个参数进步尺寸的单位角度theta的除数距离。且如果srn和stn同时为0,就表示使用经典的霍夫变换。否则,这两个参数应该都为正数。

累计概率霍夫变换:HoughLinesP( )函数

void HoughLinesP(InputArray image, OutputArray lines, double rho, double theta, int threshold, double minLineLength=0, double maxLineGap=0); 

第一个参数,InputArray类型的image,输入图像,即源图像。需为8位的单通道二进制图像,可以将任意的源图载入进来后由函数修改成此格式后,再填在这里。
第二个参数,InputArray类型的lines,经过调用HoughLinesP函数后存储了检测到的线条的输出矢量,每一条线由具有4个元素的矢量(x_1,y_1, x_2, y_2)表示,其中,(x_1, y_1)和(x_2, y_2) 是是每个检测到的线段的结束点。
第三个参数,double类型的rho,以像素为单位的距离精度。另一种表述方式是直线搜索时的进步尺寸的单位半径。
第四个参数,double类型的theta,以弧度为单位的角度精度。另一种表述方式是直线搜索时的进步尺寸的单位角度。
第五个参数,int类型的threshold,累加平面的阈值参数,即识别某部分为图中的一条直线时它在累加平面中必须达到的值。大于阈值threshold的线段才可以被检测通过并返回到结果中。
第六个参数,double类型的minLineLength,有默认值0,表示最低线段的长度,比这个设定参数短的线段就不能被显现出来。
第七个参数,double类型的maxLineGap,有默认值0,允许将同一行点与点之间连接起来的最大的距离。

霍夫圆变换:HoughCircles( )函数

void HoughCircles(InputArray image,OutputArray circles, int method, double dp, double minDist, double param1=100,double param2=100, int minRadius=0, int maxRadius=0);

第一个参数,InputArray类型的image,输入图像,即源图像,需为8位的灰度单通道图像。
第二个参数,InputArray类型的circles,经过调用HoughCircles函数后此参数存储了检测到的圆的输出矢量,每个矢量由包含了3个元素的浮点矢量(x,y,radius)表示。
第三个参数,int类型的method,即使用的检测方法,目前OpenCV中就霍夫梯度法一种可以使用,它的标识符为HOUGH_GRADIENT(OpenCV2中可写作CV_HOUGH_GRADIENT),在此参数处填这个标识符即可。
第四个参数,double类型的dp,用来检测圆心的累加器图像的分辨率于输入图像之比的倒数,且此参数允许创建一个比输入图像分辨率低的累加器。例如,如果dp=1时,累加器和输入图像具有相同的分辨率。如果dp=2,累加器便有输入图像一半那么大的宽度和高度。
第五个参数,double类型的minDist,为霍夫变换检测到的圆的圆心之间的最小距离,即让算法能明显区分的两个不同圆之间的最小距离。这个参数如果太小的话,多个相邻的圆可能被错误地检测成了一个重合的圆。反之,这个参数设置太大,某些圆就不能被检测出来。
第六个参数,double类型的param1,有默认值100。它是第三个参数method设置的检测方法的对应的参数。对当前唯一的方法霍夫梯度法CV_HOUGH_GRADIENT,它表示传递给canny边缘检测算子的高阈值,而低阈值为高阈值的一半。
第七个参数,double类型的param2,也有默认值100。它是第三个参数method设置的检测方法的对应的参数。对当前唯一的方法霍夫梯度法CV_HOUGH_GRADIENT,它表示在检测阶段圆心的累加器阈值。它越小,就越可以检测到更多根本不存在的圆,而它越大的话,能通过检测的圆就更加接近完美的圆形了。
第八个参数,int类型的minRadius,有默认值0,表示圆半径的最小值。
第九个参数,int类型的maxRadius,也有默认值0,表示圆半径的最大值。

重映射

重映射,就是把一幅图像中某位置的像素放置到另一个图片指定位置的过程。为了完成映射过程,需要获得一些插值为非整数像素的坐标,因为源图像与目标图像的像素坐标不是一一对应的。

void remap(InputArray src, OutputArraydst, InputArray map1, InputArray map2, int interpolation, intborderMode=BORDER_CONSTANT, const Scalar& borderValue=Scalar( ));

参考链接:
https://book.douban.com/subject/26320896/
https://blog.csdn.net/poem_qianmo/article/details/20911629

原文地址:https://www.cnblogs.com/chendeqiang/p/13200942.html