文章目录
Opencv官方资料一、入门基础1.1、头文件说明:#include <opencv2/opencv.hpp>1.2、头文件说明:#include <opencv2/highgui/highgui.hpp> 二、图像处理2.1、图像加载、保存与显示2.1.1、加载图像:cv::imread() —— 返回Mat对象2.1.2、保存图像:cv::imwrite() —— 只支持JPG/PNG/TIFF格式2.1.3、显示图像:cv::imshow()2.1.4、实战案例 2.2、窗口操作2.2.1、创建窗口:cv::namedWindow()2.2.2、销毁指定/所有窗口:cv::destoryWindow() + cv::destoryAllWindow()2.2.3、移动窗口到指定位置:cv::moveWindow()2.2.4、调整窗口大小:cv::resizeWindow()2.2.5、等待按键:cv::WaitKey() 2.3、颜色空间转换:cv::cvtColor()2.4、边缘填充:cv::copyMakeBorder()2.5、图像融合:cv::addWeighted()2.6、图像的三色图2.6.1、通道分离:cv::split()2.6.2、通道合并:cv::merge()2.6.3、实战案例 2.7、阈值化处理(基于灰度图分割目标1与背景0)2.7.1、二值化处理:cv::threshold()2.7.2、自适应二值化处理:cv::adaptiveThreshold()2.7.3、实战案例 2.8、滤波处理2.8.1、均值滤波器:cv::blur()2.8.2、方框滤波器:cv::boxFilter()2.8.3、高斯滤波器:cv::GaussianBlur()2.8.4、中值滤波器:cv::medianBlur()2.8.5、双边滤波器:cv::bilateralFilter()2.8.6、自定义卷积:cv::filter2D()2.8.7、可分离滤波器:cv::sepFilter2D()2.8.8、实战案例 2.9、图像变换2.9.1、图像缩放:cv::resize()2.9.2、图像翻转:cv::flip()2.9.3、图像旋转(计算仿射变换的旋转矩阵):cv::getRotationMatrix2D()2.9.4、计算仿射变换的2×3矩阵:cv::getAffineTransform()2.9.5、计算透视变换的3×3矩阵:cv::getPerspectiveTransform()2.9.6、仿射变换:cv::warpAffine()2.9.7、透视转换:cv::warpPerspective()2.9.8、实战案例 2.10、形态学操作2.10.1、腐蚀:cv::erode()2.10.2、膨胀:cv::dilate()2.10.3、形态学变化:cv::morphologyEx()。(腐蚀、膨胀、开运算、闭运算、顶帽、黑帽、基本梯度、击中击不中)2.10.4、获取指定大小和形状的结构化元素:cv::getStructuringElement()2.10.5、实战案例:提取水平线与垂直线2.10.6、实战案例 2.11、图像金字塔2.11.1、降采样:cv::pyrDown()2.11.2、上采样:cv::pyrUp()2.11.3、实战案例 2.12、边缘检测(基于灰度图提取边缘特征)2.12.1、sobel算子:cv::Sobel()2.12.2、Scharr算子:cv::Scharr()2.12.3、拉普拉斯算子:cv::Laplacian()2.12.4、Canny算子:cv::Canny()2.12.5、实战案例 2.13、轮廓检测(基于二值化提取轮廓特征)2.13.1、提取轮廓:cv::findContours()2.13.2、绘制轮廓:cv::drawContours()2.13.3、曲线轮廓(1)计算曲线长度或闭合轮廓周长:cv::arcLength()(2)计算与原始曲线最大距离的近似曲线的坐标:cv::approxPolyDP()(3)绘制近似曲线的轮廓:通过drawContours 2.13.4、矩形轮廓(1)计算矩形的左上角坐标与宽高:cv::boundingRect()(2)绘制矩形框:cv::rectangle() 2.13.5、外接圆轮廓(1)计算最小封闭圆的中心点与半径:cv::minEnclosingCircle()(2)绘制圆形框:cv::circle() 2.13.6、实战案例 2.14、绘制多种图形2.14.1、绘制直线:cv::line()2.14.2、绘制椭圆:cv::ellipse()2.14.3、填充多边形:cv::fillPoly()2.14.4、添加文字:cv::putText()2.14.5、实战案例 2.15、模板匹配2.15.1、将模板与图像进行滑动比较:cv::matchTemplate()2.15.1、查找全局最小值和最大值及其位置:cv::minMaxLoc()2.15.3、实战案例 2.16、直方图(基于灰度图像计算)2.16.1、计算一个/多个数组的直方图:cv::calcHist()2.16.2、比较两个直方图并返回指标:cv::compareHist()2.16.3、直方图均衡化:cv::equalizeHist()2.16.4、自适应直方图均衡化:cv::createCLAHE()2.16.5、实战案例 2.17、基于傅里叶变换的(低通滤波 + 高通滤波)2.17.1、傅里叶变换:cv::dft()2.17.2、傅里叶反变换:cv::idft()2.17.3、计算相位谱:cv::phase()2.17.4、计算幅度谱:cv::magnitude()2.17.5、计算x和y的坐标:cv::polarToCart()2.17.6、获取最适合傅里叶正变换的宽 / 高:cv::getOptimalDFTSize()2.17.7、实战案例 2.18、角点检测2.18.1、算法原理2.18.2、Harris角点检测:cv::cornerHarris()2.18.3、实战案例 2.19、目标分割(分水岭) —— 对图像质量和参数设置要求较高,需根据实况相应调整。2.19.1、算法原理2.19.2、距离变换(计算二值图像中每个像素与最近的零像素点的距离):cv::distanceTransform()2.19.3、归一化:cv::normalize()2.19.4、基于标记的分水岭算法:cv::watershed()2.19.5、实战案例 —— 基于(自动)标记的分水岭算法2.19.6、实战案例 —— 基于(手动)标记的分水岭算法2.19.7、实战案例 —— 基于(自动标记)边缘检测的分水岭算法 2.20、目标分割(超像素分割SLIC) —— 生成密集细胞图2.20.1、SLIC算法原理2.20.2、实战案例 2.21、霍夫变换2.21.1、霍夫直线变换(基于边缘图像)2.21.2、霍夫圆变换Opencv官方资料
Opencv(Open Source Computer Vision)官方资料。支持在线查看 / 搜索函数,有超详细的参数说明与函数使用说明(全英文版)。
(BUG1)报错提示:无法打开包括文件
"math.h"
解决方案:配置属性 + 常规 + Windows SDK版本 + 最新安装的版本
(BUG2)报错提示:未定义标识符
"CV_WINDOW_AUTOSIZE"
解决方案:添加头文件
#include <opencv2/highgui/highgui.hpp>
(BUG3)报错提示:未定义标识符
"string"
解决方案:需同时添加头文件
#include <string>
、using namespace std;
c++ 学习:未声明的标识符"string"(using namespace std;)
(BUG4)报错提示:无法找到Visual Studio (v140)的生成工具
解决方案:配置属性 + 常规 + 平台工具集 + 选择当前VS版本对应的工具
无法找到Visual Studio (v140)的生成工具
(BUG5)报错提示:当前不会命中断点。源代码与原始版本不同
解决方案:复制该文件的内容后删除该文件,并在新建文件中粘贴内容,并修改新建文件名为原文件名。
当前不会命中断点。源代码与原始版本不同 (VS)
一、入门基础
1.1、头文件说明:#include <opencv2/opencv.hpp>
在编辑器中点击opencv.hpp,其汇总了OpenCV图像处理相关的所有头文件(共15个)。如:图像处理模块头文件
imgproc.hpp
、高层GUI图形用户界面模块头文件highgui.hpp
、2D特征模块头文件features2d.hpp
等等。
所以,我们在编写
core 、 objdetect 、 imgproc 、 photo 、 video 、 features2d 、 calib3d 、 ml 、 highgui
模块的应用程序时,只需要添加该头文件即可。
1.2、头文件说明:#include <opencv2/highgui/highgui.hpp>
HighGUI(high-level graphical user interface)是一个可以移植的图形工具包。可以实现硬件(摄像机)、文件系统和操作系统的交互功能。
(1)硬件相关:用来对于视频的操作。
VideoCapture、VidoeWriter
(2)文件系统:用来对于图像的操作。
imread、imwrite、imshow
(3)操作系统:用来对于窗口的操作。
namedWindow、destoryWindow、moveWindow、resizeWindow、WaitKey
二、图像处理
2.1、图像加载、保存与显示
2.1.1、加载图像:cv::imread() —— 返回Mat对象
支持的文件格式:
* .bmp、* .dib、* .jpeg、* .jpg、*.jpe、* .jp2、* .png、* .webp、* .pbm、* .pgm、* .ppm、* .pxm、* .pnm、* .tiff、* .tif。
注意事项:
11、在指定的图像路径和文件名中,支持中文和空格。opencv默认图片通道顺序是BGR,而不是RGB。22、如果无法读取图像(文件丢失,权限不正确,格式不支持或无效),不会报错,而是返回一个空矩阵(Mat中的data项为NULL)。
函数说明:Mat cv::imread( const string &filename, int flag=1 )输入参数:filename加载的图像路径(包括文件名)flag标志类型(默认1)cv::ImreadModes {cv::IMREAD_UNCHANGED = -1,If set, return the loaded image as is (with alpha channel, otherwise it gets cropped). Ignore EXIF orientation.cv::IMREAD_GRAYSCALE = 0,If set, always convert image to the single channel grayscale image (codec internal conversion).cv::IMREAD_COLOR = 1,If set, return the loaded image as is (with alpha channel, otherwise it gets cropped). Ignore EXIF orientation.cv::IMREAD_ANYDEPTH = 2,If set, return 16-bit/32-bit image when the input has the corresponding depth, otherwise convert it to 8-bit.cv::IMREAD_ANYCOLOR = 4,If set, the image is read in any possible color format.cv::IMREAD_LOAD_GDAL = 8,If set, use the gdal driver for loading the image.cv::IMREAD_REDUCED_GRAYSCALE_2 = 16,If set, always convert image to the single channel grayscale image and the image size reduced 1/2.cv::IMREAD_REDUCED_COLOR_2 = 17,If set, always convert image to the 3 channel BGR color image and the image size reduced 1/2.cv::IMREAD_REDUCED_GRAYSCALE_4 = 32,If set, always convert image to the single channel grayscale image and the image size reduced 1/4.cv::IMREAD_REDUCED_COLOR_4 = 33,If set, always convert image to the 3 channel BGR color image and the image size reduced 1/4.cv::IMREAD_REDUCED_GRAYSCALE_8 = 64,If set, always convert image to the single channel grayscale image and the image size reduced 1/8.cv::IMREAD_REDUCED_COLOR_8 = 65,If set, always convert image to the 3 channel BGR color image and the image size reduced 1/8.cv::IMREAD_IGNORE_ORIENTATION = 128If set, do not rotate the image according to EXIF's orientation flag.}
2.1.2、保存图像:cv::imwrite() —— 只支持JPG/PNG/TIFF格式
函数说明:bool cv::imwrite( const string &filename, InputArray img, const vector<int>¶ms=vector<int>() )输入参数:filename保存的图像路径(包括文件名)img待保存的图像params设置压缩参数来控制图片的质量(可选参数)。一般情况下,图片格式都是经过压缩的。11、该参数是一个vector<int>类型,里面分别存入paramId_1, paramValue_1, paramId_2, paramValue_2, ... 即存入一对属性值。22、若不设置该参数,则程序会自动根据所保存的图像格式采用一个默认的参数。返回值:保存成功返回1,失败返回0。备注1:该函数目前只支持8位、16位JPG/PNG/TIFF的单通道或者三通道BGR图像格式,而不是所有Mat类型都支持。备注2:如果Mat类型数据的深度和通道数不满足上面的要求,则需要使用convertTo()函数和cvtColor()函数来进行转换。
2.1.3、显示图像:cv::imshow()
函数说明:void cv::imshow( const string &winname, IputArray mat )输入参数:winname窗口名称image待显示的图像备注1:imshow()函数通常与waitKey()函数连用,否则程序执行完毕,闪一下就消失了。备注2:waitKey(0):其中,0表示等待用户任意按键后结束暂停功能;其余值表示等待指定时间,单位为毫秒。
2.1.4、实战案例
#include<opencv2\opencv.hpp>#include <string>//using namespace cv;//using namespace std;int main(int argc,char* argv[]){//输入图像(字符串)路径的五种方法://(1)单左斜线法string imgpath = "C:/Users/pc/Desktop/test.jpg";//(2)双右斜线法string imgpath = "C:\\Users\\pc\\Desktop\\test.jpg";//(3)双左斜线法string imgpath = "C://Users//pc//Desktop//test.jpg";//(4)以上三种混合法string imgpath = "C:/Users//pc\\Desktop//test.jpg";//(5)相对路径法string imgpath = "test.jpg";//(1)读取图像std::string img_path = "test.jpg";cv::Mat img = cv::imread(img_path, 1);//(2)判断图像是否读取成功if(img.empty()){std::cout << "can't read image!" << std::endl;return -1;}//(3)打印图像信息std::cout << "宽度:"<<img.cols << std::endl;std::cout << "高度:" << img.rows << std::endl;std::cout << "通道数:" << img.channels() << std::endl;std::cout << "字节数:" << img.elemSize() / img.channels()*8 << std::endl;unsigned char* pData = img.data;std::cout << "数据地址:" << &pData << std::endl;//std::cout << "打印矩阵:" << img << std::endl;//(4)保存与显示图像cv::imwrite("C:/Users/my/Desktop/save.jpg", img);//保存图像cv::imshow("img", img);//显示图像cv::waitKey(0);//等待用户任意按键后结束暂停功能return 0;}
2.2、窗口操作
2.2.1、创建窗口:cv::namedWindow()
函数namedWindow创建一个窗口,该窗口可以用作图像和轨迹条的占位符。创建的窗口由其名称引用。如果已经存在具有相同名称的窗口,则该函数将不执行任何操作。
#include <opencv2/highgui.hpp>函数说明:void cv::namedWindow(const String &winname, int flags = WINDOW_AUTOSIZE)输入参数:winname:窗口标题中,可以用作窗口标识符的窗口名称。flags :窗口标志符。默认WINDOW_AUTOSIZEWINDOW_NORMAL用户可以调整窗口大小(无限制)/也可以将全屏窗口切换到正常大小。WINDOW_AUTOSIZE 用户无法调整窗口大小,窗口大小受显示图像的限制。WINDOW_OPENGL 支持opengl的窗口。WINDOW_FULLSCREEN 将窗口更改为全屏。WINDOW_FREERATIO 调整图像而没有比率约束。WINDOW_KEEPRATIO 图像的比例得到了尊重。WINDOW_GUI_EXPANDED (旧方法)在没有状态栏和工具栏的情况下绘制窗口(一种新的增强型GUI)WINDOW_GUI_NORMAL (旧方法)在没有状态栏和工具栏的情况下绘制窗口
2.2.2、销毁指定/所有窗口:cv::destoryWindow() + cv::destoryAllWindow()
#include <opencv2/highgui.hpp>函数作用:销毁给定名称的窗口。函数说明:void cv::destroyWindow( const String &winname )输入参数:winname:要销毁的窗口名称。
#include <opencv2/highgui.hpp>函数作用:销毁所有打开的HighGUI窗口。函数说明:void cv::destroyAllWindows()
2.2.3、移动窗口到指定位置:cv::moveWindow()
#include <opencv2/highgui.hpp>函数说明:void cv::moveWindow( const string &winname, int x, int y )输入参数:winname窗口名称。x窗口的新x坐标。y窗口的新y坐标。
2.2.4、调整窗口大小:cv::resizeWindow()
只能在窗口标记符 flags 不等于 cv::WINDOW_AUTOSIZE 的情况下,创建的窗口才能调整大小。
#include <opencv2/highgui.hpp>函数说明:void cv::resizeWindow(const string &winname, int width, int height)输入参数: winname窗口名称。width新窗口宽度。height新窗口高度。
2.2.5、等待按键:cv::WaitKey()
函数waitKey无限等待键事件(当延迟≤0时),或等待延迟毫秒(当延迟>0正时)。返回所按下键的代码,若在指定时间内没有按下任何键,则返回-1。要检查按键是否按下但不等待,请使用int cv::pollKey()
。函数waitKey和pollKey是HighGUI中唯一可以获取和处理GUI事件的方法。只有当至少创建了一个HighGUI窗口并且该窗口处于活动状态时,WaitKey才能工作。如果有多个HighGUI窗口,其中任何一个都可以处于活动状态。
#include <opencv2/highgui.hpp>函数说明:int cv::waitKey( int delay=0 )输入参数:当delay<=0无限等待键事件,直到用户触发一个按键。当delay>0等待延迟毫秒。备注:延迟delay(以毫秒为单位)。0是表示“永远”的特殊值。
2.3、颜色空间转换:cv::cvtColor()
该函数将输入图像从一个颜色空间转换为另一个颜色。在从RGB颜色空间转换到的情况下,应明确指定通道的顺序(RGB或BGR)。请注意,OpenCV中的默认颜色格式是BGR。
R、G、B通道值的常规范围为:(1)CV_8U图像:0到255;(2)CV_16U图像:0到65535;(3)CV_32F图像:0到1。
更多颜色空间转换码,请看官网:Color Space Conversions
函数说明:void cv::cvtColor( InputArray src, OutputArray dst, int code, int dstCn = 0 )输入参数:src输入图像:8位无符号、16位无符号(CV_16UC…)或单精度浮点。dst输出与src大小和深度相同的图像。dstCn输出图像的通道数,默认0。如果参数为0,那么通道的数量将自动从src和code中导出。code颜色空间转换码。总计有200种左右,只列出其中常用转换码。cv::ColorConversionCodes {cv::COLOR_BGR2RGB = 4,cv::COLOR_RGB2BGR = COLOR_BGR2RGB,cv::COLOR_BGR2GRAY = 6,cv::COLOR_RGB2GRAY = 7,cv::COLOR_GRAY2BGR = 8,cv::COLOR_GRAY2RGB = COLOR_GRAY2BGR}备注1:若设置dst==src,即实现原图的转换。但不改变原矩阵,而是将src.data存放在编译器新建的内存地址中。
#include<opencv2\opencv.hpp>#include <string>//using namespace cv;//using namespace std;int main(int argc, char* argv[]){//(1)读取图像std::string img_path = "test.jpg";cv::Mat img = cv::imread(img_path, 1);//(2)判断图像是否读取成功if (img.empty()){std::cout << "can't read image!" << std::endl;return -1;}//(3)空间颜色转换cv::Mat img_RGB, img_gray;cv::cvtColor(img, img_RGB, cv::COLOR_BGR2RGB);cv::cvtColor(img, img_gray, cv::COLOR_BGR2GRAY);//(4)显示图像cv::imshow("img", img);cv::imshow("RGB", img_RGB);cv::imshow("gray", img_gray);cv::waitKey(0);//等待用户任意按键后结束暂停功能return 0;}
2.4、边缘填充:cv::copyMakeBorder()
作用:在图像四周填充指定像素形成边框。
在过滤函数中,也有边界类型borderType参数,效果等同。区别是,该函数可以独立运行。
#include <opencv2/core.hpp>函数说明:void cv::copyMakeBorder( InputArray src, OutputArray dst, int top, int bottom, int left, int right, int borderType, const Scalar &value = Scalar() );输入参数:src输入图像。dst与src类型相同的输出图像,大小为size(src.cols+left+right, src.rows+top+bottom)。top顶部像素bottom底部像素left左侧像素right右侧像素。指定在源图像的每个方向上要外推的像素数。top=1,bottom=1,left=1,right=1意味着需要构建像素宽为1的边界。borderType 边框类型(即边界填充方式)。其中,BORDER_TRANSPARENT不可用。cv::BORDER_CONSTANT = 0 iiiiii|abcdefgh|iiiiiii常量法。填充常数值cv::BORDER_REPLICATE = 1 aaaaaa|abcdefgh|hhhhhhh复制法。复制最边缘像素cv::BORDER_REFLECT = 2 fedcba|abcdefgh|hgfedcb反射法。以两边为轴cv::BORDER_WRAP = 3 cdefgh|abcdefgh|abcdefg外包装法。cv::BORDER_REFLECT_101 = 4 gfedcb|abcdefgh|gfedcba反射法。以最边缘像素为轴cv::BORDER_TRANSPARENT = 5 uvwxyz|abcdefgh|ijklmnocv::BORDER_REFLECT101 = 6 same as BORDER_REFLECT_101cv::BORDER_DEFAULT = 7 same as BORDER_REFLECT_101cv::BORDER_ISOLATED = 8 do not look outside of ROIvalue = Scalar() 如果borderType==Border_CONSTANT,则为边界值。
#include <opencv2/opencv.hpp>#include <opencv2/highgui/highgui.hpp>//using namespace std;//using namespace cv;int main(int argc, char* argv[]){//(1)读取图像std::string img_path = "test.jpg";cv::Mat src = cv::imread(img_path, 1);//(2)判断图像是否读取成功if (!src.data){std::cout << "can't read image!" << std::endl;return -1;}//(3)边缘填充float border_width = 0.2;int top= (int)(border_width * src.rows);int bottom= (int)(border_width * src.rows);int left= (int)(border_width * src.cols);int right= (int)(border_width * src.cols);cv::RNG rng;cv::Mat img1, img2, img3, img4, img5, img6, img7, img8, img9;cv::Scalar color = cv::Scalar(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255));cv::copyMakeBorder(src, img1, top, bottom, left, right, cv::BORDER_CONSTANT, color);cv::copyMakeBorder(src, img2, top, bottom, left, right, cv::BORDER_REPLICATE, color);cv::copyMakeBorder(src, img3, top, bottom, left, right, cv::BORDER_REFLECT, color);cv::copyMakeBorder(src, img4, top, bottom, left, right, cv::BORDER_WRAP, color);cv::copyMakeBorder(src, img5, top, bottom, left, right, cv::BORDER_REFLECT_101, color);//cv::copyMakeBorder(src, img6, top, bottom, left, right, cv::BORDER_TRANSPARENT, color);//不可用cv::copyMakeBorder(src, img7, top, bottom, left, right, cv::BORDER_REFLECT101, color);cv::copyMakeBorder(src, img8, top, bottom, left, right, cv::BORDER_DEFAULT, color);cv::copyMakeBorder(src, img9, top, bottom, left, right, cv::BORDER_ISOLATED, color);//(4)显示图像cv::imshow("src", src);cv::imshow("img1", img1);cv::imshow("img2", img2);cv::imshow("img3", img3);cv::imshow("img4", img4);cv::imshow("img5", img5);//cv::imshow("img6", img6);//不可用cv::imshow("img7", img7);cv::imshow("img8", img8);cv::imshow("img9", img9);cv::waitKey(0);return 0;}
2.5、图像融合:cv::addWeighted()
计算两个数组的加权和:
dst = src1*alpha + src2*beta + gamma;
注意:两张图像的大小和类型必须一致才行。
#include <opencv2/core.hpp>函数说明:void cv::addWeighted( InputArray src1, double alpha, InputArray src2, double beta, double gamma, OutputArray dst, int dtype = -1 )输入参数:src1第一输入阵列。alpha第一阵列元素的权重。src2具有与src1相同大小和通道数的第二输入阵列。beta第二阵列元素的权重。gamma给阵列的每个元素添加一个标量(截距)dst具有与输入阵列相同大小和通道数的输出阵列。dtype输出阵列的可选深度;当两个输入数组具有相同的深度时,dtype可以设置为-1(默认),等效于src1.depth()。
#include<opencv2\opencv.hpp>#include <string>//using namespace cv;//using namespace std;int main(int argc,char* argv[]){//(1)读取图像std::string img_path1 = "test.jpg";std::string img_path2 = "flower.jpg";cv::Mat img1 = cv::imread(img_path1, 1);cv::Mat img2 = cv::imread(img_path2, 1);//(2)判断图像是否读取成功if(img1.empty() || img2.empty()){std::cout << "can't read image!" << std::endl;return -1;}//(3)判断两张图像的宽高是否相同if(img1.rows != img2.rows || img1.cols != img2.cols){cv::resize(img2, img2, img1.size());std::cout << "img1:" << img1.rows <<":" << img1.cols << std::endl;std::cout << "img2:" << img2.rows <<":" << img2.cols << std::endl;}//(4)计算两个数组的加权和cv::Mat img3;double alpha = 0.6;cv::addWeighted(img1, alpha, img2, (1-alpha), 0, img3);//显示图像cv::imshow("img1", img1);cv::imshow("img2", img2);cv::imshow("img3", img3);cv::waitKey(0);//等待用户任意按键后结束暂停功能return 0;}
2.6、图像的三色图
2.6.1、通道分离:cv::split()
将多通道阵列划分为多个单通道阵列。
#include <opencv2/core.hpp>函数说明:void cv::split( InputArray m, OutputArrayOfArrays mv );输入参数:m输入多通道阵列。mv输出阵列;阵列的数量与src.channels()相匹配;如果需要,将重新分配阵列本身。char d[] = {1,2,3,4,5,6,7,8,9,10,11,12};Mat m(2, 2, CV_8UC3, d);Mat channels[3];split(m, channels);/*channels[0] = [1, 4; 7, 10]channels[1] = [2, 5; 8, 11]channels[2] = [3, 6; 9, 12]*/
2.6.2、通道合并:cv::merge()
将多个数组合并为一个多通道数组。
#include <opencv2/core.hpp>函数说明:void cv::merge( InputArrayOfArrays mv, OutputArray dst );输入参数:mv待合并矩阵的输入向量;mv中的所有矩阵必须具有相同的大小和相同的深度。dst与mv[0]具有相同大小和相同深度的输出阵列;通道的数量将是矩阵阵列中通道的总数。Mat m1 = (Mat_<uchar>(2,2) << 1,4,7,10);Mat m2 = (Mat_<uchar>(2,2) << 2,5,8,11);Mat m3 = (Mat_<uchar>(2,2) << 3,6,9,12);Mat channels[3] = {m1, m2, m3};Mat m;merge(channels, m);/*m = [1, 2, 3, 4, 5, 6; 7, 8, 9, 10, 11, 12]m.channels() = 3*/
2.6.3、实战案例
#include<opencv2\opencv.hpp>#include <string>//using namespace cv;//using namespace std;int main(int argc,char* argv[]){//(1)读取图像std::string img_path = "test.jpg";cv::Mat src = cv::imread(img_path, 1);//(2)判断图像是否读取成功if(src.empty()){std::cout << "can't read image!" << std::endl;return -1;}//(3)将图像分割成多个通道(分离后的每个通道都是灰度图;而新建其余两通道全0,则可以显示对应的通道颜色)std::vector<cv::Mat> rgbChannels(3);cv::split(src, rgbChannels);//(4)新建其余两通道全0,并显示对应的通道颜色。cv::Mat blank_ch;blank_ch = cv::Mat::zeros(cv::Size(src.cols, src.rows), CV_8UC1);//新建全0矩阵:大小与Mat相同,类型为CV_8UC1。//(4.1)显示红色通道std::vector<cv::Mat> channels_r;channels_r.push_back(blank_ch);//Mat.push_back:将一个或多个元素添加到矩阵底部。其类型和列数必须与Mat矩阵中的相同。channels_r.push_back(blank_ch);channels_r.push_back(rgbChannels[2]);//(4.2)显示绿色通道std::vector<cv::Mat> channels_g;channels_g.push_back(blank_ch);channels_g.push_back(rgbChannels[1]);channels_g.push_back(blank_ch);//(4.3)显示蓝色通道std::vector<cv::Mat> channels_b;channels_b.push_back(rgbChannels[0]);channels_b.push_back(blank_ch);channels_b.push_back(blank_ch);//(5)合并三个通道cv::Mat r_img, g_img, b_img;// 分离后的每个通道都是灰度图;而新建其余两通道全0,则可以显示对应的通道颜色。cv::merge(channels_r, r_img);//(显示红色通道,其余两通道全部置0)cv::merge(channels_g, g_img);//(显示绿色通道,其余两通道全部置0)cv::merge(channels_b, b_img);//(显示蓝色通道,其余两通道全部置0)//(6)显示图像cv::imshow("src", src);cv::imshow("R_img", r_img);cv::imshow("G_img", g_img);cv::imshow("B_img", b_img);cv::imshow("R_gray", rgbChannels.at(0));cv::imshow("G_gray", rgbChannels.at(1));cv::imshow("B_gray", rgbChannels.at(2));cv::waitKey(0);return 0;}
2.7、阈值化处理(基于灰度图分割目标1与背景0)
彩色图像:三通道,像素值一般为0~255;
灰度图像:单通道,像素值一般为0~255;
二值图像:单通道,像素值一般为0(黑色)、255(白色);
常用方法有两种:
(1)
cv::threshold()
:设定固定阈值,实现图像的阈值化分割,但缺乏灵活性,且有时候难以达到理想的分割效果。(2)cv::adaptiveThreshold()
:通过图像的像素邻域块的分布特征,自适应确定区域的二值化阈值。即把图像分成N个邻域块,然后采用某种算法计算邻域块的二值化阈值,并对这些小块进行阈值化处理。
2.7.1、二值化处理:cv::threshold()
将设置的阈值应用于每个通道(阵列)的每个像素,进而过滤太小或太大的像素值。常用于去除噪声。缺点:具有主观性,难以达到理想的分割效果。opencv 二值化阈值分割(图解+阈值类型详解)
#include <opencv2/imgproc.hpp>函数说明:double cv::threshold( InputArray src, OutputArray dst, double thresh, double maxval, int type );输入参数:src输入阵列(多通道、8位或32位浮点)。dst与src具有相同大小和类型以及相同通道数的输出数组。thresh阈值。maxval与THRESH_BINARY和THRESH_ BINARY_INV阈值类型一起使用的最大值。type阈值类型。cv::THRESH_BINARY = 0 若大于thresh,则设置为maxval,否则设置为0。(常用)cv::THRESH_BINARY_INV = 1 若大于thresh,则设置为0,否则设置为maxval(反操作)。cv::THRESH_TRUNC = 2 若大于thresh,则设置为thresh,否则保持不变。cv::THRESH_TOZERO = 3 若大于thresh,则保持不变,否则设置为0。cv::THRESH_TOZERO_INV = 4 若大于thresh,则设置为0,否则保持不变(反操作)。cv::THRESH_MASK = 5 cv::THRESH_OTSU = 6 全局自适应阈值化(仅适用于8位单通道图像)。适合于直方图具有双峰的情况,其在双峰之间找到阈值;对于非双峰图像不是很好用。cv::THRESH_TRIANGLE = 7 全局自适应阈值化(仅适用于8位单通道图像)。在直方图中,在最亮到最暗处连接一条直线,得到的最大直线距离所对应的直方图位置就是阈值thresh。输出参数:仅当阈值类型为Otsu或Triangle方法时使用,输出自适应的阈值。
2.7.2、自适应二值化处理:cv::adaptiveThreshold()
通过窗口内像素的分布特征自适应计算阈值,并进行阈值化处理。详细步骤如下:
(1)将图像拆分为M x N个区域;(2)采用自适应阈值算法(窗口均值阈值法、高斯分布阈值法),计算每个区域的(均值、高斯均值),该值即当前区域的二值化阈值;(3)根据每个窗口计算得到的不同阈值(动态),进行阈值化处理。opencv 自适应二值化处理
#include <opencv2/imgproc.hpp>函数说明:void cv::adaptiveThreshold( InputArray src, OutputArray dst, double maxValue, int adaptiveMethod, int thresholdType, int blockSize, double C );输入参数:src8位单通道图像。dst与src大小和类型相同的目标图像。maxValue指定给满足条件的像素的非零值adaptiveMethod自适应阈值算法。BORDER_REPLICATE|BORDER_ISOLATED用于处理边界。cv::ADAPTIVE_THRESH_MEAN_C = 0 窗口均值阈值法。计算出领域的平均值再减去参数double C的值cv::ADAPTIVE_THRESH_GAUSSIAN_C = 1高斯分布阈值法。计算出领域的高斯均值再减去参数double C的值thresholdType阈值化类型(只有两个取值)。cv::THRESH_BINARY = 0 若大于thresh,则设置为maxval,否则设置为0。(常用)cv::THRESH_BINARY_INV = 1 若大于thresh,则设置为0,否则设置为maxval(反操作)。blockSize像素邻域大小(单位):3、5、7,依此类推。自适应阈值算法的阈值计算时使用。C偏移值。自适应阈值算法的阈值计算时使用。
2.7.3、实战案例
#include <opencv2/opencv.hpp>#include <opencv2/highgui/highgui.hpp>//using namespace std;//using namespace cv;int main(int argc,char* argv[]){//(1)读取图像std::string img_path = "test.jpg";cv::Mat src = cv::imread(img_path, 1);//(2)判断图像是否读取成功if (!src.data){std::cout << "can't read image!" << std::endl;return -1;}//(3)转换为灰度图cv::Mat srcGray;cv::cvtColor(src, srcGray, cv::COLOR_RGB2GRAY);//(4)阈值化处理cv::Mat THRESH_BINARY, THRESH_BINARY_INV, THRESH_TRUNC, THRESH_TOZERO, THRESH_TOZERO_INV, THRESH_MASK, THRESH_OTSU, THRESH_TRIANGLE;double thresh = 125;double maxval = 255;cv::threshold(srcGray, THRESH_BINARY, thresh, maxval, 0);cv::threshold(srcGray, THRESH_BINARY_INV, thresh, maxval, 1);cv::threshold(srcGray, THRESH_TRUNC, thresh, maxval, 2);cv::threshold(srcGray, THRESH_TOZERO, thresh, maxval, 3);cv::threshold(srcGray, THRESH_TOZERO_INV, thresh, maxval, 4);//cv::threshold(srcGray, THRESH_MASK, thresh, maxval, 5);//cv::threshold(srcGray, THRESH_OTSU, thresh, maxval, 6);//cv::threshold(srcGray, THRESH_TRIANGLE,thresh, maxval, 7);//(5)自适应阈值化处理cv::Mat ADAPTIVE_THRESH_MEAN_C0, ADAPTIVE_THRESH_MEAN_C1, ADAPTIVE_THRESH_GAUSSIAN_C0, ADAPTIVE_THRESH_GAUSSIAN_C1;int blockSize = 5;int constValue = 10;const int maxVal = 255;cv::adaptiveThreshold(srcGray, ADAPTIVE_THRESH_MEAN_C0, maxVal, cv::ADAPTIVE_THRESH_MEAN_C, cv::THRESH_BINARY, blockSize, constValue);cv::adaptiveThreshold(srcGray, ADAPTIVE_THRESH_MEAN_C1, maxVal, cv::ADAPTIVE_THRESH_MEAN_C, cv::THRESH_BINARY, blockSize, constValue);cv::adaptiveThreshold(srcGray, ADAPTIVE_THRESH_GAUSSIAN_C0, maxVal, cv::ADAPTIVE_THRESH_GAUSSIAN_C, cv::THRESH_BINARY_INV, blockSize, constValue);cv::adaptiveThreshold(srcGray, ADAPTIVE_THRESH_GAUSSIAN_C1, maxVal, cv::ADAPTIVE_THRESH_GAUSSIAN_C, cv::THRESH_BINARY_INV, blockSize, constValue);//(6)显示图像cv::imshow("srcGray", srcGray);cv::imshow("img1", THRESH_BINARY);cv::imshow("img2", THRESH_BINARY_INV);cv::imshow("img3", THRESH_TRUNC);cv::imshow("img4", THRESH_TOZERO);cv::imshow("img5", THRESH_TOZERO_INV);//cv::imshow("img6", THRESH_MASK);//cv::imshow("img7", THRESH_OTSU);//cv::imshow("img8", THRESH_TRIANGLE);cv::imshow("img11", ADAPTIVE_THRESH_MEAN_C0);cv::imshow("img22", ADAPTIVE_THRESH_MEAN_C1);cv::imshow("img33", ADAPTIVE_THRESH_GAUSSIAN_C0);cv::imshow("img44", ADAPTIVE_THRESH_GAUSSIAN_C1);cv::waitKey(0);return 0;}
2.8、滤波处理
椒盐噪声:噪声的幅值基本上相同,但是噪声出现的位置是随机的;(中值滤波效果好)高斯噪声:每一点都存在噪声,但噪声的幅值是随机分布的。
滤波器分为线性滤波和非线性滤波:
非线性滤波:中值滤波、双边滤波。线性滤波:方框滤波、均值滤波、高斯滤波、图像滤波器、可分离滤波器。
(1)低通滤波器:允许低频率通过;
(2)高通滤波器:允许高频率通过;
(3)带通滤波器 :允许一定区域的频率通过;
(4)带阻滤波器 :阻止一定范围内的频率并且允许其他频率通过;
(5)全通滤波器 :允许所有频率通过,仅仅改变相位;
(6)陷波滤波器(Band stop filter):阻止一个狭窄频率范围通过的特殊带阻滤波器。
滤波器的优缺点:
(1)均值滤波器 cv::blur():可以减少噪点或失真情况,缺点边缘信息丢失而导致图像变得模糊。(2)方框滤波器 cv::boxFilter():很少用到。当归一化参数normalize=true时,等同于均值滤波器。(3)高斯滤波器 cv::GaussianBlur():可以有效的去除高斯噪音,但不能完全避免边缘特征丢失问题。(4)中值滤波器 cv::medianBlur():可以有效的消除脉冲噪声和椒盐噪声,且能保护边缘特征。缺点是花费的时间是均值滤波的5倍以上。(5)双边滤波器 cv::bilateralFilter():在高斯滤波的基础上加入灰度信息权重,去噪的同时避免边缘信息丢失。缺点是需要更多的处理时间。(6)自定义卷积 cv::filter2D():使用自定义二维卷积核实现卷积操作。(7)可分离滤波器 cv::sepFilter2D():先对X方向进行一维滤波得到一维行向量,再对Y方向进行一维滤波得到一维列向量,然后将两个向量相乘得到一个值并替换卷积核。效果等同于连续调用两次的filter2D()。
备注:卷积核大小与平滑的效果直接相关,卷积核越大平滑效果越好,但卷积核过大会使边缘信息的损失过大,进而导致输出的图像变得模糊,因此需合理选择卷积核大小。
2.8.1、均值滤波器:cv::blur()
作用:取卷积核的平均值替换卷积核。
使用卷积核进行图像平滑:
#include <opencv2/imgproc.hpp>函数说明:void cv::blur( InputArray src, OutputArray dst, Size ksize, Point anchor = Point(-1,-1), int borderType = BORDER_DEFAULT );输入参数:src输入图像。可以有任意数量的通道,这些通道是独立处理的,但深度应该是CV_8U、CV_16U、CV_16S、CV_32F或CV_64F。dst输出与src大小和类型相同的图像。ksize卷积核大小。anchor = Point(-1,-1)锚点。默认值Point(-1,-1):表示锚点位于内核中心。borderType = BORDER_DEFAULT 边界类型(即边界填充方式)。默认BORDER_DEFAULT。不支持BORDER_WRAP。cv::BORDER_CONSTANT = 0 iiiiii|abcdefgh|iiiiiii常量法。填充常数值cv::BORDER_REPLICATE = 1 aaaaaa|abcdefgh|hhhhhhh复制法。复制最边缘像素cv::BORDER_REFLECT = 2 fedcba|abcdefgh|hgfedcb反射法。以两边为轴cv::BORDER_WRAP = 3 cdefgh|abcdefgh|abcdefg外包装法。cv::BORDER_REFLECT_101 = 4 gfedcb|abcdefgh|gfedcba反射法。以最边缘像素为轴cv::BORDER_TRANSPARENT = 5 uvwxyz|abcdefgh|ijklmnocv::BORDER_REFLECT101 = 6 same as BORDER_REFLECT_101cv::BORDER_DEFAULT = 7 same as BORDER_REFLECT_101cv::BORDER_ISOLATED = 8 do not look outside of ROI
2.8.2、方框滤波器:cv::boxFilter()
(非归一化)方框滤波器可用于计算每个像素邻域上的各种积分特性。例如:图像导数的协方差矩阵(用于密集光流算法等)。(归一化)方框滤波器等同于均值滤波器。
使用卷积核进行图像平滑:
#include <opencv2/imgproc.hpp>函数说明:void cv::boxFilter( InputArray src, OutputArray dst, int ddepth, Size ksize, Point anchor = Point(-1,-1), bool normalize = true, int borderType = BORDER_DEFAULT );输入参数:src输入图像。dst输出与src大小和类型相同的图像。ddepth输出图像深度(使用src.depth()时为-1)。ksize卷积核大小。anchor = Point(-1,-1)锚点。默认值Point(-1,-1):表示锚点位于内核中心。normalize = true指定内核是否按其面积进行规范化(默认True)。borderType = BORDER_DEFAULT边界类型(即边界填充方式)。默认BORDER_DEFAULT。不支持BORDER_WRAP。cv::BORDER_CONSTANT = 0 iiiiii|abcdefgh|iiiiiii常量法。填充常数值cv::BORDER_REPLICATE = 1 aaaaaa|abcdefgh|hhhhhhh复制法。复制最边缘像素cv::BORDER_REFLECT = 2 fedcba|abcdefgh|hgfedcb反射法。以两边为轴cv::BORDER_WRAP = 3 cdefgh|abcdefgh|abcdefg外包装法。cv::BORDER_REFLECT_101 = 4 gfedcb|abcdefgh|gfedcba反射法。以最边缘像素为轴cv::BORDER_TRANSPARENT = 5 uvwxyz|abcdefgh|ijklmnocv::BORDER_REFLECT101 = 6 same as BORDER_REFLECT_101cv::BORDER_DEFAULT = 7 same as BORDER_REFLECT_101cv::BORDER_ISOLATED = 8 do not look outside of ROI
2.8.3、高斯滤波器:cv::GaussianBlur()
作用:对整幅图像进行加权平均。每一个像素点的值,都由其本身和邻域内的其他像素值经过加权平均后得到。原理:加权是因为符合高斯分布,平均是因为符合正态分布=[0, 1]
。高斯核中心的值最大,其余根据距离中心元素的距离递减。
#include <opencv2/imgproc.hpp>函数说明:void cv::GaussianBlur( InputArray src, OutputArray dst, Size ksize, double sigmaX, double sigmaY = 0, int borderType = BORDER_DEFAULT );输入参数:src输入图像;图像可以具有任意数量的通道,这些通道是独立处理的,但深度应该是CV_8U、CV_16U、CV_16S、CV_32F或CV_64F。dst输出与src大小和类型相同的图像。ksize高斯核大小。ksize.width和ksize.height可以不同,但它们都必须是正数和奇数。或者,它们可以是零,然后根据sigma计算。sigmaXX方向上的高斯核标准偏差。sigmaY = 0Y方向上的高斯核标准偏差。如果sigmaY为零(默认),则设置为等于sigmaX;如果两个sigma都为零,那么分别从ksize.width和ksize.height计算。borderType = BORDER_DEFAULT边界类型(即边界填充方式)。默认BORDER_DEFAULT。不支持BORDER_WRAP。cv::BORDER_CONSTANT = 0 iiiiii|abcdefgh|iiiiiii常量法。填充常数值cv::BORDER_REPLICATE = 1 aaaaaa|abcdefgh|hhhhhhh复制法。复制最边缘像素cv::BORDER_REFLECT = 2 fedcba|abcdefgh|hgfedcb反射法。以两边为轴cv::BORDER_WRAP = 3 cdefgh|abcdefgh|abcdefg外包装法。cv::BORDER_REFLECT_101 = 4 gfedcb|abcdefgh|gfedcba反射法。以最边缘像素为轴cv::BORDER_TRANSPARENT = 5 uvwxyz|abcdefgh|ijklmnocv::BORDER_REFLECT101 = 6 same as BORDER_REFLECT_101cv::BORDER_DEFAULT = 7 same as BORDER_REFLECT_101cv::BORDER_ISOLATED = 8 do not look outside of ROI
2.8.4、中值滤波器:cv::medianBlur()
作用:卷积核排序,取其中间值(不是平均值)替换该卷积核。在内部使用 BORDER_REPLICATE 来处理边界像素。
#include <opencv2/imgproc.hpp>函数说明:void cv::medianBlur( InputArray src, OutputArray dst, int ksize );输入参数:src输入图像。1通道、3通道或4通道图像;当ksize为3或5时,图像深度应为CV_8U、CV_16U或CV_32F,对于较大的孔径尺寸,它只能是CV_8U。dst输出与src大小和类型相同的图像。ksize卷积核大小。必须是奇数并且大于1,例如:3、5、7...
2.8.5、双边滤波器:cv::bilateralFilter()
原理:同时使用空间高斯权重和灰度值相似性高斯权重。
(1)空间距离:指的是邻域内某点与中心点的欧式距离。
(2)灰度距离:指的是邻域内某点灰度与中心点灰度的差的绝对值。
高斯滤波:基于空间距离对图像进行加权平均。即在邻域内,越接近中心点的像素点,其权重越大。双边滤波:在高斯滤波的基础上,加入灰度信息权重。即在邻域内,灰度值越接近中心点,其权重更大。最终卷积核的权重大小由空间域高斯核函数和值域高斯核函数共同确定。双边滤波(bilateralFilter)原理
#include <opencv2/imgproc.hpp>函数说明:void cv::bilateralFilter( InputArray src, OutputArray dst, int d, double sigmaColor, double sigmaSpace, int borderType = BORDER_DEFAULT );输入参数:src输入图像。8位或浮点、1通道或3通道图像。dst输出与src大小和类型相同的图像。d像素邻域的直径[5, 9...]。若d>0,由d直接指定邻域直径;若d<=0,则会自动由sigmaSpace的值确定,且与之成正比。sigmaColor在颜色空间[0, 255]中过滤小于sigmaColor的像素值。值越大,卡通化效果越明显。sigma两个参数常设置相同sigmaSpace在坐标空间[0, + ∞]中过滤小于sigmaSpace的像素值。值越大,卡通化效果越明显。borderType = BORDER_DEFAULT边界类型(即边界填充方式)。默认BORDER_DEFAULT。不支持BORDER_WRAP。cv::BORDER_CONSTANT = 0 iiiiii|abcdefgh|iiiiiii常量法。填充常数值cv::BORDER_REPLICATE = 1 aaaaaa|abcdefgh|hhhhhhh复制法。复制最边缘像素cv::BORDER_REFLECT = 2 fedcba|abcdefgh|hgfedcb反射法。以两边为轴cv::BORDER_WRAP = 3 cdefgh|abcdefgh|abcdefg外包装法。cv::BORDER_REFLECT_101 = 4 gfedcb|abcdefgh|gfedcba反射法。以最边缘像素为轴cv::BORDER_TRANSPARENT = 5 uvwxyz|abcdefgh|ijklmnocv::BORDER_REFLECT101 = 6 same as BORDER_REFLECT_101cv::BORDER_DEFAULT = 7 same as BORDER_REFLECT_101cv::BORDER_ISOLATED = 8 do not look outside of ROI
2.8.6、自定义卷积:cv::filter2D()
作用:使用自定义二维卷积核实现卷积操作。在多通道图像的情况下,每个通道都是独立处理的。若需要将不同的卷积核应用于不同的通道,可使用cv::split。
该函数实际上计算的是相关性,而不是卷积:
#include <opencv2/imgproc.hpp>函数说明:void cv::filter2D( InputArray src, OutputArray dst, int ddepth, InputArray kernel, Point anchor = Point(-1,-1), double delta = 0, int borderType = BORDER_DEFAULT );输入参数:src输入图像。dst输出与src大小和类型相同的图像。ddepth输出图像深度(使用src.depth()时为-1)。kernel卷积核大小(单通道浮点矩阵)。anchor = Point(-1,-1)锚点。位于卷积核内;默认值(-1,-1):表示锚点位于内核中心。delta = 0偏移量,卷积结果要加上这个数字。borderType = BORDER_DEFAULT边界类型(即边界填充方式)。默认BORDER_DEFAULT。不支持BORDER_WRAP。cv::BORDER_CONSTANT = 0 iiiiii|abcdefgh|iiiiiii常量法。填充常数值cv::BORDER_REPLICATE = 1 aaaaaa|abcdefgh|hhhhhhh复制法。复制最边缘像素cv::BORDER_REFLECT = 2 fedcba|abcdefgh|hgfedcb反射法。以两边为轴cv::BORDER_WRAP = 3 cdefgh|abcdefgh|abcdefg外包装法。cv::BORDER_REFLECT_101 = 4 gfedcb|abcdefgh|gfedcba反射法。以最边缘像素为轴cv::BORDER_TRANSPARENT = 5 uvwxyz|abcdefgh|ijklmnocv::BORDER_REFLECT101 = 6 same as BORDER_REFLECT_101cv::BORDER_DEFAULT = 7 same as BORDER_REFLECT_101cv::BORDER_ISOLATED = 8 do not look outside of ROI
2.8.7、可分离滤波器:cv::sepFilter2D()
可分离滤波器:将自定义卷积分离为 x 方向和 y 方向两个独立的 1 维滤波器。
作用:先对X方向进行一维滤波得到一维行向量,再对Y方向进行一维滤波得到一维列向量,然后将两个向量相乘得到一个值并替换卷积核。
效果等同于连续调用两次的filter2D()。
#include <opencv2/imgproc.hpp>函数说明:void cv::sepFilter2D( InputArray src, OutputArray dst, int ddepth, InputArray kernelX, InputArray kernelY, Point anchor = Point(-1,-1), double delta = 0, int borderType = BORDER_DEFAULT );输入参数:src输入图像。dst输出与src大小和类型相同的图像。ddepth输出图像深度(使用src.depth()时为-1)。kernelX用于过滤每一行的系数。kernelY用于过滤每一列的系数。anchor = Point(-1,-1)锚点。位于卷积核内;默认值(-1,-1):表示锚点位于内核中心。delta = 0偏移量,卷积结果要加上这个数字。borderType = BORDER_DEFAULT边界类型(即边界填充方式)。默认BORDER_DEFAULT。不支持BORDER_WRAP。cv::BORDER_CONSTANT = 0 iiiiii|abcdefgh|iiiiiii常量法。填充常数值cv::BORDER_REPLICATE = 1 aaaaaa|abcdefgh|hhhhhhh复制法。复制最边缘像素cv::BORDER_REFLECT = 2 fedcba|abcdefgh|hgfedcb反射法。以两边为轴cv::BORDER_WRAP = 3 cdefgh|abcdefgh|abcdefg外包装法。cv::BORDER_REFLECT_101 = 4 gfedcb|abcdefgh|gfedcba反射法。以最边缘像素为轴cv::BORDER_TRANSPARENT = 5 uvwxyz|abcdefgh|ijklmnocv::BORDER_REFLECT101 = 6 same as BORDER_REFLECT_101cv::BORDER_DEFAULT = 7 same as BORDER_REFLECT_101cv::BORDER_ISOLATED = 8 do not look outside of ROI
2.8.8、实战案例
#include <opencv2/opencv.hpp>#include <opencv2/highgui/highgui.hpp>//using namespace std;//using namespace cv;int main(int argc, char* argv[]){//(1)读取图像std::string img_path = "test.jpg";cv::Mat src = cv::imread(img_path, 1);//(2)判断图像是否读取成功if (!src.data){std::cout << "can't read image!" << std::endl;return -1;}//(3)滤波处理(由于滤波器的边界类型默认为自动填充=1,故滤波前后的尺寸不变。)cv::Mat img1, img2, img3, img4, img5, img6, img7;cv::blur(src, img1, cv::Size(3, 3));//均值滤波cv::boxFilter(src, img2, src.depth(), cv::Size(3, 3));//方框滤波cv::GaussianBlur(src, img3, cv::Size(3, 3), 0, 0);//高斯滤波cv::medianBlur(src, img4, 3);//中值滤波cv::bilateralFilter(src, img5, 5, 150, 150);//双边滤波cv::Mat kernel = (cv::Mat_<float>(3, 3) << 1, 1, 1, 1, 3, 1, 1, 1, 1) / int(1 + 1 + 1 + 1 + 3 + 1 + 1 + 1 + 1);cv::filter2D(src, img6, src.depth(), kernel);//自定义卷积cv::Mat kx = (cv::Mat_<float>(1, 3) << 0, -1, 0);cv::Mat ky = (cv::Mat_<float>(1, 3) << -1, 0, -1);cv::sepFilter2D(src, img7, src.depth(), kx, ky);//可分离滤波std::cout << "高:" << src.rows << "宽" << src.cols << std::endl;std::cout << "高:" << img1.rows << "宽" << img1.cols << std::endl;//(4)显示图像cv::imshow("src", src);cv::imshow("均值", img1);cv::imshow("方框", img2);cv::imshow("高斯", img3);cv::imshow("中值", img4);cv::imshow("双边", img5);cv::imshow("自定义", img6);cv::imshow("可分离", img7);cv::waitKey(0);return 0;}
2.9、图像变换
2.9.1、图像缩放:cv::resize()
将图像大小调整为指定的大小。
11、若调整src与dst对齐:
resize(src, dst, dst.size(), 0, 0, interpolation);
22、若在输入图像的基础上进行等比例缩放:
resize(src, dst, Size(), 0.5, 0.5, interpolation);
#include <opencv2/imgproc.hpp>函数说明:void cv::resize( InputArray src, OutputArray dst, Size dsize, double fx = 0, double fy = 0, int interpolation = INTER_LINEAR )输入参数:src输入图像:8位无符号、16位无符号(CV_16UC…)或单精度浮点。dst输出图像;大小为dsize(当它为非零时)或根据src.size()、fx和fy计算的大小;dst的类型与src的类型相同。dsize输出图像大小;dsize=None,则计算为:dsize = Size(round(fx*src.cols), round(fy*src.rows)),dsize或fx和fy都必须为非零。fx = 0沿水平轴的缩放比例;当它等于0时,它被计算为:(double)dsize.width/src.colsfy = 0沿垂直轴的缩放比例;当它等于0时,它被计算为:(double)dsize.height/src.rowsinterpolation = INTER_LINEAR插值方法。cv::InterpolationFlags{cv::INTER_NEAREST最近邻插值cv::INTER_LINEAR双线性插值(默认)cv::INTER_CUBIC 双三次插值cv::INTER_AREA 使用像素面积关系进行重新采样。cv::INTER_LANCZOS4 8x8邻域上的Lanczos插值cv::INTER_LINEAR_EXACT 位精确双线性插值cv::INTER_NEAREST_EXACT位精确最近邻插值。这将产生与PIL、scikit图像或Matlab中的最近邻方法相同的结果。cv::INTER_MAX 插值代码掩码cv::WARP_FILL_OUTLIERS 标志,填充所有目的地图像像素。如果其中一些对应于源图像中的异常值,则将其设置为零。cv::WARP_INVERSE_MAP 标志,逆变换}
2.9.2、图像翻转:cv::flip()
将二维数组沿着上下翻转、左右翻转或对两个轴同时翻转。
flipCode = 0:上下翻转图像。flipCode > 0:左右翻转图像。flipCode < 0:同时上下和左右翻转图像。
#include <opencv2/core.hpp>函数说明:void cv::flip( InputArray src, OutputArray dst, int flipCode );输入参数:src输入数组。dst输出数组。与src相同大小和类型。flipCode翻转标志。11、0表示绕x轴翻转。22、正值(例如1)表示绕y轴翻转。33、负值(例如-1)意味着在两个轴上翻转。
2.9.3、图像旋转(计算仿射变换的旋转矩阵):cv::getRotationMatrix2D()
矩阵计算如下:
#include <opencv2/imgproc.hpp>函数说明:Mat cv::getRotationMatrix2D( Point2f center, double angle, double scale );输入参数:center输入图像的旋转中心坐标。一般取图像的中心点,可自定义。angle旋转角度(以度为单位)。正值表示逆时针旋转(坐标原点假定为左上角)。scale各向比例尺度因子。
2.9.4、计算仿射变换的2×3矩阵:cv::getAffineTransform()
#include <opencv2/imgproc.hpp>函数说明:Mat cv::getAffineTransform( const Point2f src[], const Point2f dst[] );输入参数:src输入图像中三角形的顶点坐标。dst输出图像中对应三角形的顶点坐标。
2.9.5、计算透视变换的3×3矩阵:cv::getPerspectiveTransform()
#include <opencv2/imgproc.hpp>函数说明:Mat cv::getPerspectiveTransform( InputArray src, InputArray dst, int solveMethod = DECOMP_LU );输入参数:src输入图像中四边形的顶点坐标。dst输入图像中对应四边形的顶点坐标。solveMethod = DECOMP_LU解决方法。cv::DECOMP_LU 选择最优的元素进行高斯消除。DECOMP_SVD 奇异值分解法。系统可以是过定义的,并且/或者矩阵src1可以是奇异的DECOMP_EIG 特征值分解。矩阵src1必须是对称的DECOMP_CHOLESKY Cholesky LLT分解。矩阵src1必须是对称的并且是正定义的DECOMP_QR QR分解。系统可以是过定义的,并且/或者矩阵src1可以是奇异的DECOMP_NORMAL虽然前面的所有标志都是互斥的,但这个标志可以与前面的任何标志一起使用;这意味着使用通用公式(src1转置*src1*dst=src1转置*src2),而不是原系统(src1⋅dst=src2)
2.9.6、仿射变换:cv::warpAffine()
仿射变换(Affine Transformation):二维坐标到二维坐标的变换,其本质上就是多种变换的叠加。包括:缩放、平移、旋转、反射。仿射变换(Affine Transformation)原理及应用
仿射的含义:
(1)共线性:若几个点在一条线上(变换前),则变换后仍然在一条线上。
(2)平行性:若两条线平行(变换前),则(变换后)仍然平行。
(3)共线比例不变性:若两条线段成比例(变换前),则(变换后)比例不变。
由于仿射特性,变换后仍是平行四边形,故只需要非共线的三个点就能确定。三个坐标点没有固定顺序,但变换前后的矩阵必须是对应的。
矩阵计算如下:(z表示缩放比例因子,z=1表示不进行缩放)(
i=0,1,2
表示三个顶点)矩阵变换如下:
#include <opencv2/imgproc.hpp>函数说明:void cv::warpAffine( InputArray src, OutputArray dst, InputArray M, Size dsize, int flags = INTER_LINEAR, int borderMode = BORDER_CONSTANT, const Scalar &borderValue = Scalar() );输入参数:(1)src输入图像。(2)dst输出图像。输出大小为dsize且类型与src相同的图像。(3)M2×3变换矩阵。2x3是三角形的三个顶点[x, y]。仿射变换只需要三个点,三点即可确定一个平行四边形。(4)dsize输出图像的大小。(5)flags = INTER_LINEAR插值方法。cv::INTER_NEAREST最近邻插值cv::INTER_LINEAR双线性插值(默认)cv::INTER_CUBIC 双三次插值cv::INTER_AREA 使用像素面积关系进行重新采样。cv::INTER_LANCZOS4 8x8邻域上的Lanczos插值cv::INTER_LINEAR_EXACT 位精确双线性插值cv::INTER_NEAREST_EXACT位精确最近邻插值。这将产生与PIL、scikit图像或Matlab中的最近邻方法相同的结果。cv::INTER_MAX 插值代码掩码cv::WARP_FILL_OUTLIERS 标志,填充所有目的地图像像素。如果其中一些对应于源图像中的异常值,则将其设置为零。cv::WARP_INVERSE_MAP 标志,逆变换(6)borderMode = BORDER_CONSTANT边界类型(即边界填充方式)。cv::BORDER_CONSTANT = 0 iiiiii|abcdefgh|iiiiiii常量法。填充常数值cv::BORDER_REPLICATE = 1 aaaaaa|abcdefgh|hhhhhhh复制法。复制最边缘像素cv::BORDER_REFLECT = 2 fedcba|abcdefgh|hgfedcb反射法。以两边为轴cv::BORDER_WRAP = 3 cdefgh|abcdefgh|abcdefg外包装法。cv::BORDER_REFLECT_101 = 4 gfedcb|abcdefgh|gfedcba反射法。以最边缘像素为轴cv::BORDER_TRANSPARENT = 5 uvwxyz|abcdefgh|ijklmnocv::BORDER_REFLECT101 = 6 same as BORDER_REFLECT_101cv::BORDER_DEFAULT = 7 same as BORDER_REFLECT_101cv::BORDER_ISOLATED = 8 do not look outside of ROI(7)borderValue = Scalar()边界值(在边界不变的情况下)。缺省值是0。
2.9.7、透视转换:cv::warpPerspective()
透视变换(Perspective Transformation):将二维图片投影到三维平面上,然后再转换到二维坐标下,所以也称为投影映射(Projective Mapping)。透视变换包括了所有的仿射变换。透视变换相比仿射变换更加灵活,变换后会产生一个新的四边形,但不一定是平行四边形,所以需要非共线的四个点才能确定。四个坐标点没有固定顺序,但变换前后的矩阵必须是对应的。OpenCV4 详解仿射变换和透视变换和C++实现
矩阵计算如下:(z表示缩放比例因子,z=1表示不进行缩放)(
i=0,1,2,3
表示四个顶点)矩阵变换如下:
二维(x, y) -> 三维(X, Y, Z) -> 二维(x’, y’)
详细过程如下:
#include <opencv2/imgproc.hpp>函数说明:void cv::warpPerspective( InputArray src, OutputArray dst, InputArray M, Size dsize, int flags = INTER_LINEAR, int borderMode = BORDER_CONSTANT, const Scalar &borderValue = Scalar() );输入参数:(1)src输入图像。(2)dst输出图像。输出大小为dsize且类型与src相同的图像。(3)M3×3变换矩阵。(4)dsize输出图像的大小。(5)flags = INTER_LINEAR插值方法。cv::INTER_NEAREST最近邻插值cv::INTER_LINEAR双线性插值(默认)cv::INTER_CUBIC 双三次插值cv::INTER_AREA 使用像素面积关系进行重新采样。cv::INTER_LANCZOS4 8x8邻域上的Lanczos插值cv::INTER_LINEAR_EXACT 位精确双线性插值cv::INTER_NEAREST_EXACT位精确最近邻插值。这将产生与PIL、scikit图像或Matlab中的最近邻方法相同的结果。cv::INTER_MAX 插值代码掩码cv::WARP_FILL_OUTLIERS 标志,填充所有目的地图像像素。如果其中一些对应于源图像中的异常值,则将其设置为零。cv::WARP_INVERSE_MAP 标志,逆变换(6)borderMode = BORDER_CONSTANT边界类型(即边界填充方式)。cv::BORDER_CONSTANT = 0 iiiiii|abcdefgh|iiiiiii常量法。填充常数值cv::BORDER_REPLICATE = 1 aaaaaa|abcdefgh|hhhhhhh复制法。复制最边缘像素cv::BORDER_REFLECT = 2 fedcba|abcdefgh|hgfedcb反射法。以两边为轴cv::BORDER_WRAP = 3 cdefgh|abcdefgh|abcdefg外包装法。cv::BORDER_REFLECT_101 = 4 gfedcb|abcdefgh|gfedcba反射法。以最边缘像素为轴cv::BORDER_TRANSPARENT = 5 uvwxyz|abcdefgh|ijklmnocv::BORDER_REFLECT101 = 6 same as BORDER_REFLECT_101cv::BORDER_DEFAULT = 7 same as BORDER_REFLECT_101cv::BORDER_ISOLATED = 8 do not look outside of ROI(7)borderValue = Scalar()边界值(在边界不变的情况下)。缺省值是0。
2.9.8、实战案例
#include<opencv2\opencv.hpp>#include <string>//using namespace cv;//using namespace std;cv::Mat Rotate(cv::Mat src, int rotate_angle);cv::Mat Translation(cv::Mat src, int tx, int ty);cv::Mat WARPAFFINE(cv::Mat src);int main(int argc,char* argv[]){//(1)读取图像std::string img_path1 = "test.jpg";std::string img_path2 = "flower.jpg";cv::Mat src = cv::imread(img_path1, 1);cv::Mat src2 = cv::imread(img_path2, 1);//(2)判断图像是否读取成功if(src.empty() || src2.empty()){std::cout << "can't read image!" << std::endl;return -1;}//(3)将(图像2)拉伸与(图像1)相同尺寸。if(src.rows != src2.rows || src.cols != src2.cols){std::cout << "src=" << src.rows <<":" << src.cols << std::endl;std::cout << "src2=" << src2.rows <<":" << src2.cols << std::endl;cv::resize(src2, src2, src.size(), 0, 0, cv::INTER_LINEAR);std::cout << "src11=" << src.rows <<":" << src.cols << std::endl;std::cout << "src22=" << src2.rows <<":" << src2.cols << std::endl;}//(4)图像缩放(等比例放大 + 等比例缩小)cv::Mat resize_B, resize_S;cv::resize(src, resize_B, cv::Size(0, 0), 1, 2, cv::INTER_LINEAR);cv::resize(src, resize_S, cv::Size(0, 0), 0.8, 0.8, cv::INTER_LINEAR);std::cout << "resize_B=" << resize_B.rows <<":" << resize_B.cols << std::endl;std::cout << "resize_S=" << resize_S.rows <<":" << resize_S.cols << std::endl;//(5)图像翻转cv::Mat src_flip;cv::flip(src, src_flip, 0);//=0:上下翻转>0:左右翻转<0:上下和左右同时翻转//(6)图像旋转(封装函数)cv::Mat src_rotate;src_rotate = Rotate(src, 45);//(7)图像平移(封装函数)cv::Mat src_trans;src_trans = Translation(src, 20, 50);//(8)仿射变换cv::Mat src_wrap;cv::Point2f src_xy[3];//三个点坐标(x,y),其中x、y是浮点型。cv::Point2f dst_xy[3];src_xy[0] = cv::Point2f(0, 0);//计算输入图像的三点坐标src_xy[1] = cv::Point2f(src.cols - 1, 0);src_xy[2] = cv::Point2f(0, src.rows - 1);dst_xy[0] = cv::Point2f(src.cols*0.0, src.rows*0.33);//计算输入图像变换后对应的三点坐标dst_xy[1] = cv::Point2f(src.cols*0.85, src.rows*0.25);dst_xy[2] = cv::Point2f(src.cols*0.15, src.rows*0.7);cv::Mat warp_mat = cv::getAffineTransform(src_xy, dst_xy);//计算仿射变换矩阵cv::warpAffine(src, src_wrap, warp_mat, src.size());//仿射变换//标记坐标点cv::Mat src_WW(src);for (int i = 0; i < 4; i++){circle(src_WW, src_xy[i], 2, cv::Scalar(0, 0, 255), 2);circle(src_wrap, dst_xy[i], 2, cv::Scalar(0, 0, 255), 2);}cv::imshow("src_WW", src_WW);//(9)透视变换//11、若输入坐标超过图像尺寸,则显示异常。22、若输入坐标不对应,则显示异常cv::Mat src_Pers;cv::Point2f scrPoints[4] = {cv::Point2f(0, 0), cv::Point2f(src.cols-1, 0), cv::Point2f(0, src.rows-1), cv::Point2f(src.cols-1, src.rows-1) };cv::Point2f dstPoints[4] = {cv::Point2f(0, 0), cv::Point2f(100, 0), cv::Point2f(0, 100), cv::Point2f(150, 120) };cv::Mat Trans = cv::getPerspectiveTransform(scrPoints, dstPoints);//计算透视变换矩阵cv::warpPerspective(src, src_Pers, Trans, cv::Size(src.cols, src.rows));//透视变换//标记坐标点cv::Mat src_PP(src);for (int i = 0; i < 4; i++){circle(src_PP, scrPoints[i], 2, cv::Scalar(0, 0, 255), 2);circle(src_Pers, dstPoints[i], 2, cv::Scalar(0, 0, 255), 2);}cv::imshow("src_PP", src_PP);//显示图像cv::imshow("src", src);cv::imshow("src2", src2);cv::imshow("resize_B", resize_B);cv::imshow("resize_S", resize_S);cv::imshow("src_flip", src_flip);cv::imshow("src_rotate", src_rotate);cv::imshow("src_trans", src_trans);cv::imshow("src_wrap", src_wrap);cv::imshow("src_Pers", src_Pers);cv::waitKey(0);//等待用户任意按键后结束暂停功能return 0;}/*--------------------------------------------------函数说明:图像平移--------------------------------------------------输入参数:src输入图像txx轴平移距离tyy轴平移距离--------------------------------------------------*/cv::Mat Translation(cv::Mat src, int tx, int ty){cv::Mat dst;int height = src.cols;//获取图像的高度int width = src.rows;//获取图像的宽度// 使用tx和ty创建平移矩阵float warp_values[] = {1.0, 0.0, tx, 0.0, 1.0, ty };cv::Mat translation_matrix = cv::Mat(2, 3, CV_32F, warp_values);// 基于平移矩阵进行仿射变换cv::warpAffine(src, dst, translation_matrix, src.size());return dst;}/*--------------------------------------------------函数说明:图像旋转--------------------------------------------------输入参数:src输入图像angle旋转角度--------------------------------------------------*/cv::Mat Rotate(cv::Mat src, int angle){cv::Mat dst;int wight = src.cols;int height = src.rows;//获取旋转矩阵cv::Mat Matrix = cv::getRotationMatrix2D(cv::Point2f(wight / 2, height / 2), angle, 1.0);//获取旋转后图像的尺寸double cos = abs(Matrix.at<double>(0, 0));double sin = abs(Matrix.at<double>(0, 1));int nw = cos * wight + sin * height;int nh = sin * wight + cos * height;//获取x, y方向的偏移量Matrix.at<double>(0, 2) += (nw / 2 - wight / 2);Matrix.at<double>(1, 2) += (nh / 2 - height / 2);//基于旋转矩阵进行仿射变换cv::warpAffine(src, dst, Matrix, cv::Size(nh, nw));return dst;}
2.10、形态学操作
opencv c++ 图像形态学变化
2.10.1、腐蚀:cv::erode()
腐蚀可以应用多次(迭代)。在多通道图像的情况下,每个通道都是独立处理的。若需要将不同的卷积核应用于不同的通道,可使用cv::split。
#include <opencv2/imgproc.hpp>函数说明:void cv::erode( InputArray src, OutputArray dst, InputArray kernel, Point anchor = Point(-1,-1), int iterations = 1, int borderType = BORDER_CONSTANT, const Scalar &borderValue = morphologyDefaultBorderValue() );输入参数:src输入图像;通道的数量可以是任意的,这些通道是独立处理的,但深度应该是CV_8U、CV_16U、CV_16S、CV_32F或CV_64F中的一个。dst输出与src大小和类型相同的图像。kernel卷积核大小(单通道浮点矩阵)。如果element=Mat(),则使用一个3x3矩形结构化元素。内核可以使用getStructureElement创建。anchor = Point(-1,-1)锚点。位于卷积核内;默认值(-1,-1):表示锚点位于内核中心。iterations = 1腐蚀迭代次数(默认1)。迭代N次与连续调用N次的效果是不等同的。borderType = BORDER_CONSTANT边界类型(即边界填充方式)。默认BORDER_CONSTANT。不支持BORDER_WRAP。cv::BORDER_CONSTANT = 0 iiiiii|abcdefgh|iiiiiii常量法。填充常数值cv::BORDER_REPLICATE = 1 aaaaaa|abcdefgh|hhhhhhh复制法。复制最边缘像素cv::BORDER_REFLECT = 2 fedcba|abcdefgh|hgfedcb反射法。以两边为轴cv::BORDER_WRAP = 3 cdefgh|abcdefgh|abcdefg外包装法。cv::BORDER_REFLECT_101 = 4 gfedcb|abcdefgh|gfedcba反射法。以最边缘像素为轴cv::BORDER_TRANSPARENT = 5 uvwxyz|abcdefgh|ijklmnocv::BORDER_REFLECT101 = 6 same as BORDER_REFLECT_101cv::BORDER_DEFAULT = 7 same as BORDER_REFLECT_101cv::BORDER_ISOLATED = 8 do not look outside of ROIborderValue = morphologyDefaultBorderValue() 边界值(在边界不变的情况下)
2.10.2、膨胀:cv::dilate()
膨胀可以应用多次(迭代)。在多通道图像的情况下,每个通道都是独立处理的。若需要将不同的卷积核应用于不同的通道,可使用
cv::split
。
#include <opencv2/imgproc.hpp>函数说明:void cv::dilate( InputArray src, OutputArray dst, InputArray kernel, Point anchor = Point(-1,-1), int iterations = 1, int borderType = BORDER_CONSTANT, const Scalar &borderValue = morphologyDefaultBorderValue() );输入参数:src输入图像;通道的数量可以是任意的,这些通道是独立处理的,但深度应该是CV_8U、CV_16U、CV_16S、CV_32F或CV_64F中的一个。dst输出与src大小和类型相同的图像。kernel卷积核大小(单通道浮点矩阵)。如果element=Mat(),则使用一个3x3矩形结构化元素。内核可以使用getStructureElement创建。anchor = Point(-1,-1)锚点。位于卷积核内;默认值(-1,-1):表示锚点位于内核中心。iterations = 1膨胀迭代次数(默认1)。迭代N次与连续调用N次的效果是不等同的。borderType = BORDER_CONSTANT边界类型(即边界填充方式)。默认BORDER_CONSTANT。不支持BORDER_WRAP。cv::BORDER_CONSTANT = 0 iiiiii|abcdefgh|iiiiiii常量法。填充常数值cv::BORDER_REPLICATE = 1 aaaaaa|abcdefgh|hhhhhhh复制法。复制最边缘像素cv::BORDER_REFLECT = 2 fedcba|abcdefgh|hgfedcb反射法。以两边为轴cv::BORDER_WRAP = 3 cdefgh|abcdefgh|abcdefg外包装法。cv::BORDER_REFLECT_101 = 4 gfedcb|abcdefgh|gfedcba反射法。以最边缘像素为轴cv::BORDER_TRANSPARENT = 5 uvwxyz|abcdefgh|ijklmnocv::BORDER_REFLECT101 = 6 same as BORDER_REFLECT_101cv::BORDER_DEFAULT = 7 same as BORDER_REFLECT_101cv::BORDER_ISOLATED = 8 do not look outside of ROIborderValue = morphologyDefaultBorderValue() 边界值(在边界不变的情况下)
2.10.3、形态学变化:cv::morphologyEx()。(腐蚀、膨胀、开运算、闭运算、顶帽、黑帽、基本梯度、击中击不中)
膨胀可以应用多次(迭代)。在多通道图像的情况下,每个通道都是独立处理的。若需要将不同的卷积核应用于不同的通道,可使用cv::split。
#include <opencv2/imgproc.hpp>函数说明:void cv::morphologyEx( InputArray src, OutputArray dst, int op, InputArray kernel, Point anchor = Point(-1,-1), int iterations = 1, int borderType = BORDER_CONSTANT, const Scalar &borderValue = morphologyDefaultBorderValue() );输入参数:src输入图像;通道的数量可以是任意的,这些通道是独立处理的,但深度应该是CV_8U、CV_16U、CV_16S、CV_32F或CV_64F中的一个。dst输出与src大小和类型相同的图像。op形态学运算的类型。cv::MORPH_ERODE = 0腐蚀操作cv::MORPH_DILATE = 1膨胀操作cv::MORPH_OPEN = 2开运算:先腐蚀再膨胀。用于去除微小干扰点/块。cv::MORPH_CLOSE = 3闭运算:先膨胀再腐蚀。用于填充闭合区域。cv::MORPH_GRADIENT = 4基本梯度:膨胀图(减去)腐蚀图cv::MORPH_TOPHAT = 5顶帽:原图(减去)开运算图。注:顶帽和黑帽用于获取图像中的微小细节。cv::MORPH_BLACKHAT = 6黑帽:闭运算图(减去)原图cv::MORPH_HITMISS = 7“命中或未命中”。仅支持CV_8UC1二进制图像。kernel卷积核大小(单通道浮点矩阵)。如果element=Mat(),则使用一个3x3矩形结构化元素。内核可以使用getStructureElement创建。anchor = Point(-1,-1)锚点。位于卷积核内;默认值(-1,-1):表示锚点位于内核中心。iterations = 1腐蚀和膨胀的迭代次数(默认1)。两次迭代的顺序:侵蚀+侵蚀+扩张+扩张(而不是侵蚀+扩张+侵蚀+扩张)。borderType = BORDER_CONSTANT边界类型(即边界填充方式)。默认BORDER_CONSTANT。不支持BORDER_WRAP。cv::BORDER_CONSTANT = 0 iiiiii|abcdefgh|iiiiiii常量法。填充常数值cv::BORDER_REPLICATE = 1 aaaaaa|abcdefgh|hhhhhhh复制法。复制最边缘像素cv::BORDER_REFLECT = 2 fedcba|abcdefgh|hgfedcb反射法。以两边为轴cv::BORDER_WRAP = 3 cdefgh|abcdefgh|abcdefg外包装法。cv::BORDER_REFLECT_101 = 4 gfedcb|abcdefgh|gfedcba反射法。以最边缘像素为轴cv::BORDER_TRANSPARENT = 5 uvwxyz|abcdefgh|ijklmnocv::BORDER_REFLECT101 = 6 same as BORDER_REFLECT_101cv::BORDER_DEFAULT = 7 same as BORDER_REFLECT_101cv::BORDER_ISOLATED = 8 do not look outside of ROIborderValue = morphologyDefaultBorderValue() 边界值(在边界不变的情况下)
2.10.4、获取指定大小和形状的结构化元素:cv::getStructuringElement()
构造并返回可以进一步传递以腐蚀、膨胀或形态Ex的结构化元素。也可以自己构造任意的二进制掩码,并将其用作结构化元素。
#include <opencv2/imgproc.hpp>函数说明:Mat cv::getStructuringElement( int shape, Size ksize, Point anchor = Point(-1,-1) );输入参数:shape结构化元素的形状。cv::MORPH_RECT = 0 矩形结构化元素:E(i,j)=1cv::MORPH_CROSS= 1十字形结构元素:E(i,j)=10 if i=anchor.y or j=anchor.x else 0cv::MORPH_ELLIPSE = 2椭圆形结构元素:即内切到矩形Rect(0, 0, esize.width, 0.esize.height)中的填充椭圆。ksize结构化元素的大小。anchor锚点。默认值(−1,−1)表示锚点位于中心。注意,只有十字形元件的形状取决于锚定位置。在其他情况下,锚点只是调节形态学运算的结果偏移了多少。
2.10.5、实战案例:提取水平线与垂直线
#include <opencv2/opencv.hpp>#include <opencv2/highgui/highgui.hpp>//using namespace std;//using namespace cv;int main(int argc, char* argv[]){//(1)读取图像std::string img_path = "test.jpg";cv::Mat src = cv::imread(img_path, 1);//(2)判断图像是否读取成功if (!src.data){std::cout << "can't read image!" << std::endl;return -1;}//(3)灰度化+二值化cv::Mat gray, binary;cv::cvtColor(src, gray, cv::COLOR_BGR2GRAY);cv::threshold(gray, binary, 0, 255, cv::THRESH_BINARY_INV | cv::THRESH_OTSU);//(4)形态学变化cv::Mat img_h, img_v;int x_size = binary.cols / 30;//像素的水平长度=width/30int y_size = binary.rows / 30;//像素的垂直长度=height/30cv::Mat h_line = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(x_size, 1), cv::Point(-1, -1));cv::Mat v_line = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(1, y_size), cv::Point(-1, -1));cv::morphologyEx(binary, img_h, cv::MORPH_OPEN, h_line , cv::Point(-1, -1), 1, 0);//开运算(提取水平线)cv::morphologyEx(binary, img_v, cv::MORPH_CLOSE, v_line, cv::Point(-1, -1), 1, 0);//闭运算(提取垂直线)//(4)显示图像cv::imshow("binary", binary);cv::imshow("img_h", img_h);cv::imshow("img_v", img_v);cv::waitKey(0);return 0;}
2.10.6、实战案例
#include <opencv2/opencv.hpp>#include <opencv2/highgui/highgui.hpp>//using namespace std;//using namespace cv;int main(int argc, char* argv[]){//(1)读取图像std::string img_path = "test.jpg";cv::Mat src = cv::imread(img_path, 1);//(2)判断图像是否读取成功if (!src.data){std::cout << "can't read image!" << std::endl;return -1;}//(3)灰度化+二值化cv::Mat gray, binary;cv::cvtColor(src, gray, cv::COLOR_BGR2GRAY);cv::threshold(gray, binary, 0, 255, cv::THRESH_BINARY_INV | cv::THRESH_OTSU);//(4)形态学变化int kernel_size = 5;//getStructuringElement:返回指定大小和形状的结构化元素以进行形态学运算。cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(kernel_size, kernel_size), cv::Point(-1, -1));cv::Mat img1, img2, img3, img4;cv::erode(binary, img1, kernel);//腐蚀cv::erode(binary, img2, kernel, cv::Point(-1, -1), 3);//腐蚀(迭代三次)cv::dilate(binary, img3, kernel);//膨胀cv::dilate(binary, img4, kernel, cv::Point(-1, -1), 3);//膨胀(迭代三次)cv::Mat img11, img22, img33, img44, img55, img66, img77, img88;cv::morphologyEx(binary, img11, cv::MORPH_ERODE, kernel, cv::Point(-1, -1), 1, 0);//腐蚀cv::morphologyEx(binary, img22, cv::MORPH_DILATE, kernel, cv::Point(-1, -1), 1, 0);//膨胀cv::morphologyEx(binary, img33, cv::MORPH_OPEN, kernel, cv::Point(-1, -1), 1, 0);//开运算cv::morphologyEx(binary, img44, cv::MORPH_CLOSE, kernel, cv::Point(-1, -1), 1, 0);//闭运算cv::morphologyEx(binary, img55, cv::MORPH_GRADIENT, kernel, cv::Point(-1, -1), 1, 0);//基本梯度cv::morphologyEx(binary, img66, cv::MORPH_TOPHAT, kernel, cv::Point(-1, -1), 1, 0);//顶帽cv::morphologyEx(binary, img77, cv::MORPH_BLACKHAT, kernel, cv::Point(-1, -1), 1, 0);//黑帽cv::morphologyEx(binary, img88, cv::MORPH_HITMISS, kernel, cv::Point(-1, -1), 1, 0);//击中击不中//(4)显示图像cv::imshow("src", src);cv::imshow("腐蚀1", img1);cv::imshow("腐蚀3", img2);cv::imshow("膨胀1", img3);cv::imshow("膨胀3", img4);cv::imshow("腐蚀", img11);cv::imshow("膨胀", img22);cv::imshow("开运算", img33);cv::imshow("闭运算", img44);cv::imshow("基本梯度", img55);cv::imshow("顶帽", img66);cv::imshow("黑帽", img77);cv::imshow("击中击不中", img88);cv::waitKey(0);return 0;}
2.11、图像金字塔
图像金字塔由多个分辨率的一组图像组成,最底层是图像尺寸最大的一张,最顶层是图像尺寸最小的一张。从空间上由上向下看,就像古埃及的金字塔。在图像处理中常需要调整图像大小,尽管几何变换也可以实现图像放大(zoom in)和缩小(zoom out),但图像金字塔在神经网络中更常见且方便快捷。
高斯不同(Difference of Gaussian,DOG)
定义:将同一张图像在不同的参数下做高斯模糊之后的结果相减,得到输出图像。应用:高斯不同是图像的内在特征。常用于灰度图像增强,角点检测。
高斯金字塔:通过高斯滤波和下采样不断地将图像的尺寸缩小。拉普拉斯金字塔:在高斯金字塔的基础上,为了实现图像重建而存在。由图可得,第n层拉普拉斯图像实际上是第n层高斯图像与第n+1层高斯图像经上采样后的差值。
由于高斯滤波器是一种低通滤波器,所以我们可以说某一级的拉普拉斯金字塔可以反映出其同级的高斯金字塔的高频分量。图像金字塔、高斯金字塔、拉普拉斯金字塔是怎么回事?
2.11.1、降采样:cv::pyrDown()
作用:通过高斯卷积使图像模糊,并对其进行下采样。(宽高各缩小一倍)尺寸:默认情况下,输出图像的大小计算为size=((src.cols+1)/2,(src.rows+1)/2)
。该函数执行高斯金字塔构造的下采样步骤:将源图像与高斯卷积核进行卷积(模糊化),然后删除偶数行和偶数列来对图像进行下采样。
提问:为什么高斯金字塔在下采样操作之前要先进行高斯低通滤波?回答:可以保留高斯金字塔低通滤波特点,且能对图像进行平滑,使下采样得到的图像不会出现边界缝隙。
pyrDown使用的高斯核如下:
#include <opencv2/imgproc.hpp>函数说明:void cv::pyrDown( InputArray src, OutputArray dst, const Size &dstsize = Size(), int borderType = BORDER_DEFAULT);输入参数:src输入图像。dst输出图像。它具有指定的大小、且与src具有相同的类型。dstsize输出图像的大小。borderType = BORDER_DEFAULT边界类型(即边界填充方式)。默认且只支持BORDER_DEFAULT。cv::BORDER_REFLECT_101 = 4 gfedcb|abcdefgh|gfedcba反射法。以最边缘像素为轴cv::BORDER_DEFAULT = 7 same as BORDER_REFLECT_101
2.11.2、上采样:cv::pyrUp()
作用:对图像进行上采样,然后通过高斯卷积使其模糊。(宽高各放大一倍)尺寸:默认情况下,输出图像的大小计算为size=(src.cols*2,src.rows*2)
。该函数执行高斯金字塔构造的上采样步骤:通过均匀地隔行隔列的方式,填充全零行和全零列来对源图像进行上采样,然后将结果与(cv::pyrDown的相同高斯卷积核乘以4)进行卷积(模糊化)。
pyrDown使用的高斯核如下:
#include <opencv2/imgproc.hpp>函数说明:void cv::pyrUp( InputArray src, OutputArray dst, const Size &dstsize = Size(), int borderType = BORDER_DEFAULT);输入参数:src输入图像。dst输出图像。它具有指定的大小、且与src具有相同的类型。dstsize输出图像的大小。borderType = BORDER_DEFAULT边界类型(即边界填充方式)。默认且只支持BORDER_DEFAULT。cv::BORDER_REFLECT_101 = 4 gfedcb|abcdefgh|gfedcba反射法。以最边缘像素为轴cv::BORDER_DEFAULT = 7 same as BORDER_REFLECT_101
2.11.3、实战案例
#include <opencv2/opencv.hpp>#include <opencv2/highgui/highgui.hpp>//using namespace std;//using namespace cv;int main(int argc, char* argv[]){//(1)读取图像std::string img_path = "test.jpg";cv::Mat src = cv::imread(img_path, 1);//(2)判断图像是否读取成功if (!src.data){std::cout << "can't read image!" << std::endl;return -1;}//(3)图像金字塔cv::Mat img_up, img_down;cv::pyrUp(src, img_up, cv::Size(src.cols * 2, src.rows * 2));//上采样cv::pyrDown(src, img_down, cv::Size(src.cols / 2, src.rows / 2));//降采样//(4)显示图像cv::imshow("src", src);cv::imshow("img_up", img_up);cv::imshow("img_down", img_down);cv::waitKey(0);return 0;}
2.12、边缘检测(基于灰度图提取边缘特征)
边缘检测技术:利用灰度的变化信息来检测物体的边缘,进而得到物体轮廓,实现图像分割。
边缘特征(Edge)
:图像中的像素值发生跃迁的地方,此时图像的局部特征出现不连续性。主要变现在灰度值突变、颜色突变、纹理结构突变等等。如:黑色到白色的突变。
图(a)是理想边缘模型:每个灰度级跃变到一个垂直的台阶上。图(b)是实际边缘模型:在实际的数据采集中,图像的质量受到多因素的印象(如:设备性能、采样率、照明条件等),因此得到的边缘特征往往是模糊的,进而导致模糊的边缘" 呈斜面 “,而清晰的边缘变” 窄 "。
图像的边缘有两种属性:方向和幅度。边缘特征通常采用一阶导数或二阶导数检测得到。
一阶导数:以极大值或极小值作为对应边缘的位置。用于检测一个点是不是边缘点;导数值越大,说明像素在该方向变化越大,边缘信号越强。二阶导数:以过零点作为对应边缘的位置。用于判断一个边缘点是在边缘亮的一边还是暗的一边。
备注:边缘检测主要基于导数计算,但导数通常对噪声很敏感。故需要在边缘检测之前,采用形态学变化、高斯滤波、阈值化等进行前处理。
边缘检测算子的分类
(1)一阶导数的边缘算子:
Sobel算子、Scharr算子、Roberts算子、Prewitt算子
。后两种在Opencv中不支持,想要研究的可参考Python版本(2)二阶导数的边缘算子:Laplacian算子
。(3)非导数的边缘算子:Canny算子
。
边缘检测算子的优缺点(最常用
Sobel算子、Laplacian算子Canny算子
)(1)Roberts 算子:对具有陡峭边缘且含噪声少的图像效果较好,尤其是边缘正负45度较多的图像,但定位准确率较差导致丢失一部分边缘,不具备抑制噪声能力。(2)
Sobel 算子
:对噪声较多的图像效果更好,边缘定位效果不错,但检测出的边缘容易出现多像素宽度。(3)Scharr算子:Sobel算子的改进版,可以j解决Sobel算子在3×3的检测效果不好的问题。两种算子在实现方式上类似。(4)Prewitt 算子:对灰度渐变的图像效果较好,但没有考虑相邻点的距离远近对当前像素点的影响,与Sobel 算子类似,不同的是在平滑部分的权重大小有些差异;(5)Laplacian 算子
:对图像中的阶跃型边缘点定位准确,但对噪声非常敏感,会丢失一部分边缘,造成一些不连续的检测边缘。因此很少用于边缘检测,常用于判断边缘是明区还是暗区(黑与白),提供更加清晰锐利的边缘效果。(6)Canny 算子
:不容易受噪声干扰,通过双边阈值检测和边缘连接能够很好地检测弱边缘。比前几种效果好,但实现较为复杂。
2.12.1、sobel算子:cv::Sobel()
又被称为一阶微分算子。
作用:将图像与
sobel卷积核
进行卷积运算。分别在水平x与垂直y方向上进行一阶求导,得到对应的梯度图像。原理:根据当前像素点上、下、左、右4个相邻点的灰度加权差,在边缘处达到极值进而检测边缘。其认为相邻点的距离远近对当前像素点的影响是不同的,距离越近的像素点对应当前像素的影响越大,从而实现图像锐化并突出边缘轮廓。特点:结合了高斯平滑和微分求导(分化),因此具有一定的抗噪性,但所得边缘较粗。应用:常用于噪声较多、灰度渐变的图像。当对精度要求不是很高时,Sobel算子是一种较为常用的边缘检测方法。
使用sobel算子获取边缘信息的两种不同的调用方式:
方式1:只调用一次sobel算子,即同时设置
(dx=1,dy=1)
,获取图像在两个方向上的梯度。方式2:分别调用两次sobel算子,并分别设置为(dx=1,dy=0)、(dx=0,dy=1)
,即分别计算图像在水平方向和垂直方向的边缘信息。最后通过图像融合cv::addWeighted()
得到两个方向的边缘信息。
一般采用方式2,边缘信息提取效果更好。
目标像素点求得的值小于0或大于255怎么办?
正值负值:白到黑是正数(255 ~ 0),黑到白就是负数(0 ~ 255)。OpenCV 默认采用截断操作(即小于0,置0;大于255,置255)。但不适用于边缘检测,故需要通过
cv::convertScaleAbs
取绝对值(即小于0,取绝对值;大于255,置255)。
#include <opencv2/imgproc.hpp>函数说明:void cv::Sobel( InputArray src, OutputArray dst, int ddepth, int dx, int dy, int ksize = 3, double scale = 1, double delta = 0, int borderType = BORDER_DEFAULT );输入参数:src输入图像。dst与src具有相同大小和相同通道数的输出图像。ddepth输出图像深度(使用src.depth()时为-1)。dxx方向的求导阶数。一般为0,1,2。其中,0表示该方向没有求导。dyy方向的求导阶数。一般为0,1,2。其中,0表示该方向没有求导。ksize = 3卷积核大小。一般为1、3、5或7。scale = 1计算导数值的可选比例因子;默认1,即不应用缩放。delta = 0偏移量,卷积结果要加上这个数字。borderType = BORDER_DEFAULT边界类型(即边界填充方式)。默认BORDER_DEFAULT。不支持BORDER_WRAP。cv::BORDER_CONSTANT = 0 iiiiii|abcdefgh|iiiiiii常量法。填充常数值cv::BORDER_REPLICATE = 1 aaaaaa|abcdefgh|hhhhhhh复制法。复制最边缘像素cv::BORDER_REFLECT = 2 fedcba|abcdefgh|hgfedcb反射法。以两边为轴cv::BORDER_WRAP = 3 cdefgh|abcdefgh|abcdefg外包装法。cv::BORDER_REFLECT_101 = 4 gfedcb|abcdefgh|gfedcba反射法。以最边缘像素为轴cv::BORDER_TRANSPARENT = 5 uvwxyz|abcdefgh|ijklmnocv::BORDER_REFLECT101 = 6 same as BORDER_REFLECT_101cv::BORDER_DEFAULT = 7 same as BORDER_REFLECT_101cv::BORDER_ISOLATED = 8 do not look outside of ROI
2.12.2、Scharr算子:cv::Scharr()
作用:将图像与Scharr卷积核
进行卷积运算。分别在水平x与垂直y方向上进行一阶求导,得到对应的梯度图像。
#include <opencv2/imgproc.hpp>函数说明:void cv::Scharr( InputArray src, OutputArray dst, int ddepth, int dx, int dy, double scale = 1, double delta = 0, int borderType = BORDER_DEFAULT );输入参数:src输入图像。dst与src具有相同大小和相同通道数的输出图像。ddepth输出图像深度(使用src.depth()时为-1)。dxx方向的求导阶数。一般为0,1,2。其中,0表示该方向没有求导。dyy方向的求导阶数。一般为0,1,2。其中,0表示该方向没有求导。ksize = 3卷积核大小。它必须是1、3、5或7。scale = 1计算导数值的可选比例因子;默认1,即不应用缩放。delta = 0偏移量,卷积结果要加上这个数字。borderType = BORDER_DEFAULT边界类型(即边界填充方式)。默认BORDER_DEFAULT。不支持BORDER_WRAP。cv::BORDER_CONSTANT = 0 iiiiii|abcdefgh|iiiiiii常量法。填充常数值cv::BORDER_REPLICATE = 1 aaaaaa|abcdefgh|hhhhhhh复制法。复制最边缘像素cv::BORDER_REFLECT = 2 fedcba|abcdefgh|hgfedcb反射法。以两边为轴cv::BORDER_WRAP = 3 cdefgh|abcdefgh|abcdefg外包装法。cv::BORDER_REFLECT_101 = 4 gfedcb|abcdefgh|gfedcba反射法。以最边缘像素为轴cv::BORDER_TRANSPARENT = 5 uvwxyz|abcdefgh|ijklmnocv::BORDER_REFLECT101 = 6 same as BORDER_REFLECT_101cv::BORDER_DEFAULT = 7 same as BORDER_REFLECT_101cv::BORDER_ISOLATED = 8 do not look outside of ROI
2.12.3、拉普拉斯算子:cv::Laplacian()
又被称为二阶微分算子
作用:将图像与
Laplacian卷积核
进行卷积运算。即分别对x和y方向上进行二次求导,然后相加。本质:计算图像中心像素与其在上、下、左、右4个相邻点的灰度差值。其具有旋转不变性。应用:拉普拉斯算子的检测性能较好,常用于图像增强领域和边缘提取。
#include <opencv2/imgproc.hpp>函数说明:void cv::Laplacian( InputArray src, OutputArray dst, int ddepth, int ksize = 1, double scale = 1, double delta = 0, int borderType = BORDER_DEFAULT );输入参数:src输入图像。dst与src具有相同大小和相同通道数的输出图像。ddepth输出图像深度(使用src.depth()时为-1)。ksize = 1卷积核大小。它必须是1、3、5或7。scale = 1计算导数值的可选比例因子;默认1,即不应用缩放。delta = 0偏移量,卷积结果要加上这个数字。borderType = BORDER_DEFAULT边界类型(即边界填充方式)。默认BORDER_DEFAULT。不支持BORDER_WRAP。cv::BORDER_CONSTANT = 0 iiiiii|abcdefgh|iiiiiii常量法。填充常数值cv::BORDER_REPLICATE = 1 aaaaaa|abcdefgh|hhhhhhh复制法。复制最边缘像素cv::BORDER_REFLECT = 2 fedcba|abcdefgh|hgfedcb反射法。以两边为轴cv::BORDER_WRAP = 3 cdefgh|abcdefgh|abcdefg外包装法。cv::BORDER_REFLECT_101 = 4 gfedcb|abcdefgh|gfedcba反射法。以最边缘像素为轴cv::BORDER_TRANSPARENT = 5 uvwxyz|abcdefgh|ijklmnocv::BORDER_REFLECT101 = 6 same as BORDER_REFLECT_101cv::BORDER_DEFAULT = 7 same as BORDER_REFLECT_101cv::BORDER_ISOLATED = 8 do not look outside of ROI
2.12.4、Canny算子:cv::Canny()
canny边缘检测算法步骤:
1、使用高斯滤波器对图像进行平滑处理。2、利用一阶偏导算子找到灰度图像沿着水平方向Gx和垂直方向Gy的偏导数,并计算梯度的幅值和方向。3、对梯度幅值进行NMS非极大值抑制,获取局部梯度的最大值。
在3X3窗口中,将给定像素P与沿着梯度线方向的两个像素进行比较,若P的梯度幅值小于该两个像素的梯度幅值,则令P=0;否则保留原幅值。
备注:将梯度方向分为4种来比较梯度幅值的强度:水平方向、垂直方向、正方向、-45°方向。4、用双边阈值检测和边缘连接。
分三种情况:
(1)若像素值大于高阈值,则该像素一定是边缘像素(强边缘点),置为255;
(2)若小于低阈值,则一定不是,置为0;
(3)若像素值大于低阈值但小于高阈值,则观察该像素的(3X3)8邻域像素中是否有大于高阈值的像素点,若有则该像素是边缘像素,并将该点置为255,用以连接强边缘点;否则不是,则该点置为0。
#include <opencv2/imgproc.hpp>函数说明:void cv::Canny( InputArray image, OutputArray edges, double threshold1, double threshold2, int apertureSize = 3, bool L2gradient = false );输入参数:image8位输入图像。edges输出图像。其具有与输入图像相同大小和类型。threshold1第一个阈值(低阈值)。threshold2第二个阈值(高阈值)。apertureSize = 3Sobel算子孔径尺寸 。默认为3。可以是1、3、5、7L2gradient = false选择L1 or L2范数计算图像梯度大小。(L2graduation=false)默认L1范数=|dI/dx|+|dI/dy|。(L2gradient=true)L2范数= (dI/dx)^2 + (dI/dy)^2。注释:高阈值比较严格,求的边缘很少,一般认为高阈值的边缘都是有效。低阈值比较宽松,求的边缘很多(一般包括高阈值求到的边缘),其中不少是无效的边缘。Canny求得的边缘希望是连在一起的(通常是封闭的)(1)先用高阈值将要提取轮廓的物体与背景区分开来,但可能边缘轮廓不连续或者不够平滑。(2)然后低阈值平滑边缘的轮廓,将不连续的部分连接起来。
2.12.5、实战案例
#include <opencv2/opencv.hpp>#include <opencv2/highgui/highgui.hpp>//using namespace std;//using namespace cv;int main(int argc, char* argv[]){//(1)读取图像std::string img_path = "test.jpg";cv::Mat src = cv::imread(img_path, 1);//(2)判断图像是否读取成功if (!src.data){std::cout << "can't read image!" << std::endl;return -1;}//(3)图像预处理cv::Mat src_Gray, src_Gaus;cv::GaussianBlur(src, src_Gaus, cv::Size(3, 3), 0, 0);//高斯滤波cv::cvtColor(src, src_Gray, cv::COLOR_BGR2GRAY);//灰度化//(4)边缘检测cv::Mat Sobel_X, Sobel_Y, Sobel_X_abs, Sobel_Y_abs, Sobel_XY, Sobel_XY1;cv::Sobel(src_Gray, Sobel_X, src_Gray.depth(), 1, 0);//计算 x 轴方向cv::Sobel(src_Gray, Sobel_Y, src_Gray.depth(), 0, 1);//计算 y 轴方向cv::convertScaleAbs(Sobel_X, Sobel_X_abs);//取绝对值cv::convertScaleAbs(Sobel_Y, Sobel_Y_abs);//取绝对值cv::addWeighted(Sobel_X_abs, 0.5, Sobel_Y_abs, 0.5, 0, Sobel_XY);//图像融合cv::Sobel(src_Gray, Sobel_XY1, src_Gray.depth(), 1, 1);//同时计算 x和y 轴方向cv::Mat Scharr_X, Scharr_Y, Scharr_X_abs, Scharr_Y_abs, Scharr_XY, Scharr_XY1; cv::Scharr(src_Gray, Scharr_X, src_Gray.depth(), 1, 0);//计算 x 轴方向cv::Scharr(src_Gray, Scharr_Y, src_Gray.depth(), 0, 1);//计算 y 轴方向cv::convertScaleAbs(Scharr_X, Scharr_X_abs);//取绝对值cv::convertScaleAbs(Scharr_Y, Scharr_Y_abs);//取绝对值cv::addWeighted(Scharr_X_abs, 0.5, Scharr_Y_abs, 0.5, 0, Scharr_XY);//图像融合//cv::Scharr(src_Gray, Scharr_XY1, src_Gray.depth(), 1, 1);//同时计算 x和y 轴方向cv::Mat src_Laplacian, src_Canny;cv::Laplacian(src_Gray, src_Laplacian, src_Gray.depth());cv::Canny(src_Gray, src_Canny, 10, 100);//(5)显示图像cv::imshow("src", src);//cv::imshow("Sobel_X", Sobel_X);//cv::imshow("Sobel_Y", Sobel_Y);//cv::imshow("Sobel_X_abs", Sobel_X_abs);//cv::imshow("Sobel_Y_abs", Sobel_Y_abs);cv::imshow("Sobel_XY", Sobel_XY);//cv::imshow("Sobel_XY1", Sobel_XY1);//cv::imshow("Scharr_X", Scharr_X);//cv::imshow("Scharr_Y", Scharr_Y);//cv::imshow("Scharr_X_abs", Scharr_X_abs);//cv::imshow("Scharr_Y_abs", Scharr_Y_abs);cv::imshow("Scharr_XY", Scharr_XY);//cv::imshow("Scharr_XY1", Scharr_XY1);cv::imshow("src_Laplacian", src_Laplacian);cv::imshow("src_Canny", src_Canny);cv::waitKey(0);return 0;}
2.13、轮廓检测(基于二值化提取轮廓特征)
2.13.1、提取轮廓:cv::findContours()
在
(0, 1)
二进制图像中提取轮廓。轮廓是用于形状分析、物体检测和识别的显著特征。
#include <opencv2/imgproc.hpp>函数说明:void cv::findContours( InputArray image, OutputArrayOfArrays contours, OutputArray hierarchy, int mode, int method, Point offset = Point() );输入参数:(1)image输入图像。8位单通道图像。二进制图像:非零像素被视为1,零像素保持为0。(2)contours所有的轮廓点。每个轮廓都存储为点的矢量,例如:std::vector<std::vector<cv::Point>>(3)hierarchy每个轮廓对应的属性。主要是图像的拓扑信息。其与轮廓点具有相同数量。00、使用[0][i]访问第i个轮廓的层次结构元素。11、对于第i个轮廓的每个元素hierarch[i][0]、hierarch[i][1]、hierarch[i][2]和hierarch[i][3]22、分别被设置为相同层次结构的下一个轮廓、前一个轮廓、第一个子轮廓、第一个父轮廓。33、若没有对应的轮廓,则层次结构[i]的相应元素将为负。(4)mode轮廓检索模式。cv::RETR_EXTERNAL = 0只检索最外部的轮廓。它为所有轮廓设置层次结构[i][2]=hierarchy[i][3]=-1。cv::RETR_LIST = 1 检索所有轮廓。不建立任何层次关系cv::RETR_CCOMP = 2 检索所有轮廓。并建立两个等级的层次结构:顶层是外部边界。内层是边界信息。cv::RETR_TREE = 3 检索所有轮廓。并建立一个等级树的完整层次结构。(最常用)cv::RETR_FLOODFILL = 4 官网没有说明。(5)method轮廓近似方法。cv::CHAIN_APPROX_NONE = 0 存储所有的轮廓点。例如:矩形的四条边(最常用)。相邻2个轮廓点的位置差不超过1(连成线)。cv::CHAIN_APPROX_SIMPLE = 1 只保留端点坐标。例如:矩形的四个角。压缩水平、垂直、对角线方向的元素。cv::CHAIN_APPROX_TC89_L1 = 2 应用Teh-Chin链近似算法的一种风格cv::CHAIN_APPROX_TC89_KCOS = 3 应用Teh-Chin链近似算法的一种风格(6)offset = Point()每个轮廓点移动的可选偏移量。若需要从图像ROI中提取轮廓,可以在整个图像的上下文中对其进行分析。
2.13.2、绘制轮廓:cv::drawContours()
绘制轮廓线或填充轮廓。如果轮廓线厚度 ≥ 0,该函数将在图像中绘制轮廓;如果厚度<0,则该函数将填充由轮廓限定的区域。
#include <opencv2/imgproc.hpp>函数说明:void cv::drawContours( InputOutputArray image, InputArrayOfArrays contours, int contourIdx, const Scalar &color, int thickness = 1, int lineType = LINE_8, InputArray hierarchy = noArray(), int maxLevel = INT_MAX, Point offset = Point() );输入参数:(1)image目标图像。(2)contours所有输入轮廓。每个轮廓都存储为一个点向量。(3)contourIdx指示要绘制的轮廓的参数。如果为-1,则绘制所有轮廓。(4)color颜色。(5)thickness = 1线厚度。如果为负值(例如,厚度=FILLED),则绘制轮廓内部。(6)lineType = LINE_8线的类型。cv::FILLED 填充。当线厚度为负值时使用cv::LINE_4 4连接线型cv::LINE_8 8连接线型cv::LINE_AA 抗锯齿线(7)hierarchy = noArray()层次结构的可选信息。只有当您只想绘制一些轮廓时才需要它(请参见maxLevel)。(8)maxLevel = INT_MAX绘制轮廓的最大水平。00、只有当有可用的层次结构时,才会考虑此参数。11、如果为0,则仅绘制指定的轮廓。22、如果为1,函数将绘制轮廓和所有嵌套的轮廓。33、如果为2,则函数绘制轮廓、所有嵌套的轮廓、所有从嵌套到嵌套的轮廓等。(9)offset = Point()可选轮廓偏移参数。将所有绘制的轮廓移动指定的偏移量=(dx, dy)。
2.13.3、曲线轮廓
(1)计算曲线长度或闭合轮廓周长:cv::arcLength()
#include <opencv2/imgproc.hpp>函数说明:double cv::arcLength(InputArray curve, bool closed );输入参数:curve输入2D点集,存储在std::vector或Mat中。closed指示曲线是否闭合的标志返回值:epsilon原始曲线与近似曲线的距离。可以将epsilon乘以系数k=[0,1],控制近似曲线的形状。其中,1表示真实轮廓,值越大拟合越粗糙。
(2)计算与原始曲线最大距离的近似曲线的坐标:cv::approxPolyDP()
将一条曲线或一个多边形与另一条顶点较少的曲线/多边形进行近似,使它们之间的距离小于或等于指定的精度。
#include <opencv2/imgproc.hpp>函数说明:void cv::approxPolyDP( InputArray curve, OutputArray approxCurve, double epsilon, bool closed );输入参数:(1)curve输入2D点集,存储在std::vector或Mat中。(2)approxCurve输出近似曲线的点集坐标。(3)epsilon原始曲线与近似曲线的距离。(4)closed指示曲线是否闭合的标志。
(3)绘制近似曲线的轮廓:通过drawContours
2.13.4、矩形轮廓
(1)计算矩形的左上角坐标与宽高:cv::boundingRect()
#include <opencv2/imgproc.hpp>函数说明:Rect cv::boundingRect(InputArray array);输入参数:array输入灰度图像或2D点集,存储在std::vector或Mat中。输出参数:rect返回矩形的四个参数:左上角坐标(x,y),宽高(w,h)
(2)绘制矩形框:cv::rectangle()
#include <opencv2/imgproc.hpp>函数说明:void cv::rectangle( InputOutputArray img, Point pt1, Point pt2, const Scalar &color, int thickness = 1, int lineType = LINE_8, int shift = 0 );输入参数:(1)img输入灰度图像或2D点集,存储在std::vector或Mat中。(2)pt1矩形的左上角顶点坐标。(3)pt2矩形的右下角顶点坐标。(4)color颜色或亮度(灰度图像)。(5)thickness = 1线条厚度。如果为负值(例如,厚度=FILLED),意味着函数将绘制一个填充的矩形。(6)lineType = LINE_8线的类型。cv::FILLED 填充。当线厚度为负值时使用cv::LINE_4 4连接线型cv::LINE_8 8连接线型cv::LINE_AA 抗锯齿线(7)shift = 0点坐标中的小数位数。
2.13.5、外接圆轮廓
(1)计算最小封闭圆的中心点与半径:cv::minEnclosingCircle()
#include <opencv2/imgproc.hpp>函数说明:void cv::minEnclosingCircle(InputArray points, Point2f ¢er, float &radius );输入参数:(1)points输入灰度图像或2D点集,存储在std::vector或Mat中。(2)center(输出)圆的中心。(3)radius(输出)圆的半径。
(2)绘制圆形框:cv::circle()
#include <opencv2/imgproc.hpp>函数说明:void cv::circle( InputOutputArray img, Point center, int radius, const Scalar &color, int thickness = 1, int lineType = LINE_8, int shift = 0 );输入参数:(1)img输入灰度图像或2D点集,存储在std::vector或Mat中。(2)center圆的中心。(3)radius圆的半径。(4)color颜色。(5)thickness = 1线条厚度。如果为负值(例如,厚度=FILLED),意味着函数将绘制一个填充的矩形。(6)lineType = LINE_8线的类型。cv::FILLED 填充。当线厚度为负值时使用cv::LINE_4 4连接线型cv::LINE_8 8连接线型cv::LINE_AA 抗锯齿线(7)shift = 0中心坐标和半径值中的小数位数。
2.13.6、实战案例
#include <opencv2/opencv.hpp>#include <opencv2/highgui/highgui.hpp>//using namespace std;//using namespace cv;int main(int argc, char* argv[]){//(1)读取图像std::string img_path = "test.jpg";cv::Mat src = cv::imread(img_path, 1);//(2)判断图像是否读取成功if (!src.data){std::cout << "can't read image!" << std::endl;return -1;}//(3)图像预处理cv::Mat src_Gray, src_binary;cv::cvtColor(src, src_Gray, cv::COLOR_BGR2GRAY);//灰度化cv::threshold(src_Gray, src_binary, 125, 255, cv::THRESH_BINARY);//二值化//(4.1)轮廓检测cv::Mat src_binary1 = src_binary.clone();//复制矩阵std::vector<std::vector<cv::Point>> contours;std::vector<cv::Vec4i> hierarchy;//hierarchy = cv::noArray()cv::findContours(src_binary1, contours, hierarchy, cv::RETR_TREE, cv::CHAIN_APPROX_NONE);//(4.2)在全黑画板上绘制轮廓cv::Mat src_binary2 = src_binary1.clone();//复制矩阵src_binary2 = cv::Scalar::all(0);//返回所有元素都为标量0的矩阵。cv::drawContours(src_binary2, contours, -1, cv::Scalar(255, 255, 2555));//(5)用近似曲线拟合轮廓的边界(全黑画板)cv::Mat src_binary3 = src_binary1.clone();//复制矩阵src_binary3 = cv::Scalar::all(0);//返回所有元素都为标量0的矩阵。std::vector<std::vector<cv::Point>> contours_poly(contours.size());//用于存放曲线点集for (int i = 0; i < contours.size(); i++){//int epsilon = cv::arcLength(cv::Mat(contours[i]), true);//5.1计算周长cv::approxPolyDP(cv::Mat(contours[i]), contours_poly[i], 15, true);//5.2近似曲线坐标cv::drawContours(src_binary3, contours_poly, i, cv::Scalar(255, 255, 255)); //5.3绘制曲线}//(6)矩形画出轮廓的边界(全黑画板)cv::Mat src_binary4 = src_binary1.clone();//复制矩阵src_binary4 = cv::Scalar::all(0);//返回所有元素都为标量0的矩阵。for (int i = 0; i < contours.size(); i++){cv::Rect rect = cv::boundingRect(cv::Mat(contours[i]));//6.1矩形坐标cv::rectangle(src_binary4, rect, cv::Scalar(255, 255, 255)); //6.2绘制矩形}//(7)圆形画出轮廓的边界(全黑画板)cv::Mat src_binary5 = src_binary1.clone();//复制矩阵src_binary5 = cv::Scalar::all(0);//返回所有元素都为标量0的矩阵。cv::Point2f center;float radius = 0;for (int i = 0; i < contours.size(); i++){cv::minEnclosingCircle(cv::Mat(contours[i]), center, radius);//7.1圆形坐标cv::circle(src_binary5, center, radius, cv::Scalar(255, 255, 255)); //7.2绘制圆形}//(8)显示图像(由于是单通道,故颜色三通道必须都有值)cv::imshow("src", src);cv::imshow("dst", src_binary1);cv::imshow("cont", src_binary2);cv::imshow("appr", src_binary3);cv::imshow("rect", src_binary4);cv::imshow("circ", src_binary5);cv::waitKey(0);return 0;}
2.14、绘制多种图形
2.14.1、绘制直线:cv::line()
#include <opencv2/imgproc.hpp>函数说明:void cv::line( InputOutputArray img, Point pt1, Point pt2, const Scalar &color, int thickness = 1, int lineType = LINE_8, int shift = 0 );输入参数:(1)img输入灰度图像或2D点集,存储在std::vector或Mat中。(2)pt1线段的第一个点。(3)pt2线段的第二个点。(4)color颜色。(5)thickness = 1线条厚度。如果为负值(例如,厚度=FILLED),意味着函数将绘制一个填充的矩形。(6)lineType = LINE_8线的类型。cv::FILLED 填充。当线厚度为负值时使用cv::LINE_4 4连接线型cv::LINE_8 8连接线型cv::LINE_AA 抗锯齿线(7)shift = 0中心坐标和半径值中的小数位数。
2.14.2、绘制椭圆:cv::ellipse()
如果要绘制整个椭圆,而不是圆弧,请通过startAngle=0和endAngle=360。如果startAngle大于endAngle,则它们将被交换。
#include <opencv2/imgproc.hpp>函数说明:void cv::ellipse( InputOutputArray img, Point center, Size axes, double angle, double startAngle, double endAngle, const Scalar &color, int thickness = 1, int lineType = LINE_8, int shift = 0 );输入参数:(1)img输入灰度图像或2D点集,存储在std::vector或Mat中。(2)center椭圆中心坐标。(3)axes椭圆x,y的半径。(4)angle椭圆旋转角度,单位为度。(5)startAngle椭圆弧的起始角,单位为度。[0, 360]、[0, 180](6)endAngle椭圆弧的终止角,单位为度。(7)color颜色。(8)thickness = 1线条厚度。如果为负值(例如,厚度=FILLED),意味着函数将绘制一个填充的矩形。(9)lineType = LINE_8线的类型。cv::FILLED 填充。当线厚度为负值时使用cv::LINE_4 4连接线型cv::LINE_8 8连接线型cv::LINE_AA 抗锯齿线(10)shift = 0中心坐标和半径值中的小数位数。
2.14.3、填充多边形:cv::fillPoly()
填充由多个多边形轮廓限定的区域。可以填充复杂区域,例如,具有孔的区域、具有自相交的轮廓(其某些部分)等。
#include <opencv2/imgproc.hpp>函数说明:void cv::fillPoly( InputOutputArray img, InputArrayOfArrays pts, const Scalar &color, int lineType = LINE_8, int shift = 0, Point offset = Point() );输入参数:(1)img输入灰度图像或2D点集,存储在std::vector或Mat中。(2)pts多边形阵列,其中每个多边形表示为点阵列。(3)color颜色。(4)lineType = LINE_8线的类型。cv::FILLED 填充。当线厚度为负值时使用cv::LINE_4 4连接线型cv::LINE_8 8连接线型cv::LINE_AA 抗锯齿线(5)shift = 0中心坐标和半径值中的小数位数。(6)offset = Point()可选轮廓偏移参数。将所有绘制的轮廓移动指定的偏移量=(dx, dy)。
2.14.4、添加文字:cv::putText()
在图像中呈现指定的文本字符串。无法使用指定字体呈现的符号将替换为问号。
#include <opencv2/imgproc.hpp>函数说明:void cv::putText( InputOutputArray img, const String & text, Point org, int fontFace, double fontScale, Scalar color, int thickness = 1, int lineType = LINE_8, bool bottomLeftOrigin = false );输入参数:(1)img输入灰度图像或2D点集,存储在std::vector或Mat中。(2)text文本字符串。(3)org图像中文本字符串的左下角。(4)fontFace字体类型。cv::FONT_HERSHEY_SIMPLEX 普通大小无衬线字体cv::FONT_HERSHEY_PLAIN 小型无衬线字体cv::FONT_HERSHEY_DUPLEX 普通大小无衬线字体(比font_HERSHEY_SIMPLEX更复杂)cv::FONT_HERSHEY_COMPLEX 普通尺寸衬线字体cv::FONT_HERSHEY_TRIPLEX 普通大小衬线字体(比font_HERSHEY_complex更复杂)cv::FONT_HERSHEY_COMPLEX_SMALL 较小版本的FONT_HERSHEY_COMPLEXcv::FONT_HERSHEY_SCRIPT_SIMPLEX 手写字体cv::FONT_HERSHEY_SCRIPT_COMPLEX 更复杂的FONT_HERSHEY_SCRIPT_SIMPLEX变体cv::FONT_ITALIC 斜体标志(5)fontScale字体比例因子,乘以特定于字体的基本大小。(6)color颜色。(7)thickness = 1线条厚度。如果为负值(例如,厚度=FILLED),意味着函数将绘制一个填充的矩形。(8)lineType = LINE_8线的类型。cv::FILLED 填充。当线厚度为负值时使用cv::LINE_4 4连接线型cv::LINE_8 8连接线型cv::LINE_AA 抗锯齿线(9)bottomLeftOrigin = false如果为true,则图像数据原点位于左下角。否则,它在左上角。
2.14.5、实战案例
#include <opencv2/opencv.hpp>#include <opencv2/highgui/highgui.hpp>//using namespace std;//using namespace cv;int main(int argc, char* argv[]){//绘制多种图形cv::Mat img = cv::Mat::zeros(500, 500, CV_8UC3);//值全0矩阵。std::cout << img.cols << img.rows << std::endl;cv::Scalar color(0, 255, 255);//指定颜色(RGB)//(1)绘制线cv::Point p1(50, 200);//起点(y,x)cv::Point p2(150, 150);//起点1(y,x)cv::Point p3(250, 200);//起点2(y,x)cv::line(img, p1, p2, color);cv::line(img, p2, p3, color);//(2)绘制矩形cv::Point PT1(10, 10);//左上角坐标(y,x)cv::Point PT2(100, 100);//右下角坐标(y,x)cv::rectangle(img, PT1, PT2, color); //(3)绘制圆形cv::Point P_Y(150, 300);//中心坐标(y,x)int radius = 20;//半径cv::circle(img, P_Y, radius, color); //(4)绘制椭圆cv::Point P_TY(150, 300);//中心坐标(y,x)cv::Size radius_TY(50, 100);//x,y的半径int angle = 90;int angle_start = 0;int angle_end = 360;cv::ellipse(img, P_TY, radius_TY, angle, angle_start, angle_end, color); //(5)填充自定义的四边形cv::Point pts[1][5];//左上角、右上角、右下角、左下角、左上角。pts[0][0] = cv::Point(200, 10);pts[0][1] = cv::Point(300, 10);pts[0][2] = cv::Point(300, 100);pts[0][3] = cv::Point(200, 100);pts[0][4] = cv::Point(200, 10);const cv::Point* ppts[] = {pts[0] };int npt[] = {5 };cv::fillPoly(img, ppts, npt, 1, color);//(6)添加文字cv::Point Putt(60, 220);cv::putText(img, "Hi, Pearson!", Putt, cv::FONT_HERSHEY_COMPLEX, 1.0, color);//(7)循环线图cv::Mat img11 = img.clone();//复制矩阵img11 = cv::Scalar::all(0);//返回所有元素都为标量0的矩阵。cv::RNG rng(-1);cv::Point pt1;cv::Point pt2;for (int i = 0; i < 10; i++){pt1.x = rng.uniform(0, img11.cols);pt2.x = rng.uniform(0, img11.cols);pt1.y = rng.uniform(0, img11.rows);pt2.y = rng.uniform(0, img11.rows);//cv::Scalar color0 = (rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255));//每次生成数固定cv::Scalar color0 = cv::Scalar(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255));//每次生成数随机cv::waitKey(50);cv::line(img11, pt1, pt2, color0);cv::imshow("img11", img11);}//(3)显示图像cv::imshow("img", img);cv::waitKey(0);return 0;}
2.15、模板匹配
2.15.1、将模板与图像进行滑动比较:cv::matchTemplate()
在图像中滑动,使用指定的方法将大小为w×h的重叠补丁与templ进行比较,并将比较结果存储在结果中。在函数完成比较后,可以使用minMaxLoc函数找到全局最小值(当使用TM_SQDIFF时)或最大值(当采用TM_CCORR或TM_CCOEFF时)的最佳匹配。
#include <opencv2/imgproc.hpp>函数说明:void cv::matchTemplate( InputArray image, InputArray templ, OutputArray result, int method, InputArray mask = noArray() );输入参数:(1)image输入图像。它必须是8位或32位浮点。(2)templ匹配模板。它必须不大于源映像并且具有相同的数据类型。(3)result匹配结果。它必须是单通道32位浮点。若图像为W×H,时间为W×H,则结果为(W−W+1)×(H−H+1)。(4)method模板比较方法cv::TM_SQDIFF = 0 计算平方差。 计算出来的值越接近0,越相关cv::TM_SQDIFF_NORMED = 1计算(归一化)平方差。 计算出来的值越接近0,越相关cv::TM_CCORR = 2计算相关性。 计算出来的值越大,越相关cv::TM_CCORR_NORMED = 3 计算(归一化)相关性。 计算出来的值越接近1,越相关cv::TM_CCOEFF = 4计算相关系数。 计算出来的值越大,越相关cv::TM_CCOEFF_NORMED = 5 计算(归一化)相关系数。 计算出来的值越接近1,越相关(5)mask = noArray()可选掩码mask。大小必须与templ相同。0、它必须具有与模板相同数量的通道,或者只有一个通道,然后用于所有模板和图像通道。1、若数据类型为CV_8U,则掩码被解释为二进制掩码,即只使用掩码为非零的元素,并且保持不变,与实际掩码值无关(权重等于1)。2、若数据类型为CV_32F,则掩码值被用作权重。TemplateMatchMode中记录了确切的公式。
2.15.1、查找全局最小值和最大值及其位置:cv::minMaxLoc()
在数组中查找全局最小值和最大值及其位置。在整个数组中搜索极值,如果掩码不是空数组,则在指定的数组区域中搜索极值。不适用于多通道阵列。如果需要在所有通道中找到最小或最大元素,请首先使用Mat::reshape将数组重新解释为单个通道。或者,可以使用extractImageCOI、mixChannels或 split 提取特定通道。
#include <opencv2/core.hpp>函数说明:void cv::minMaxLoc( InputArray src, double *minVal, double *maxVal = 0, Point *minLoc = 0, Point *maxLoc = 0, InputArray mask = noArray() );输入参数:(1)src输入单通道阵列。(2)minVal = 0输出最小值(指针)。如果不需要,则使用NULL。(3)maxVal = 0 输出最大值(指针)。如果不需要,则使用NULL。(4)minLoc = 0 输出最小位置(指针),在2D情况下。如果不需要,则使用NULL。(5)maxLoc = 0 输出最大位置(指针),在2D情况下。如果不需要,则使用NULL。(6)mask = noArray()可选掩码mask。
2.15.3、实战案例
#include <opencv2/opencv.hpp>#include <opencv2/highgui/highgui.hpp>//using namespace std;//using namespace cv;int main(int argc, char* argv[]){//(1)读取图像std::string img_path1 = "test.jpg";std::string img_path2 = "test1.jpg";cv::Mat src = cv::imread(img_path1, 1);cv::Mat temp = cv::imread(img_path2, 1);//截取于原图的一部分//(2)判断图像是否读取成功if (!src.data || !temp.data){std::cout << "can't read image!" << std::endl;return -1;}//(3)模板匹配cv::Mat src_result;double minVal, maxVal;cv::Point minLoc, maxLoc;cv::Mat src_S = src.clone();cv::matchTemplate(src_S, temp, src_result, cv::TM_SQDIFF);cv::minMaxLoc(src_result, &minVal, &maxVal, &minLoc, &maxLoc);cv::rectangle(src_S, cv::Point(minLoc.x, minLoc.y), cv::Point((minLoc.x + temp.cols), (minLoc.y + temp.rows)), cv::Scalar(0, 0, 255), 2);cv::Mat src_SN = src.clone();cv::matchTemplate(src_SN, temp, src_result, cv::TM_SQDIFF_NORMED);cv::minMaxLoc(src_result, &minVal, &maxVal, &minLoc, &maxLoc);cv::rectangle(src_SN, cv::Point(minLoc.x, minLoc.y), cv::Point((minLoc.x + temp.cols), (minLoc.y + temp.rows)), cv::Scalar(0, 0, 255), 2);cv::Mat src_C = src.clone();cv::matchTemplate(src_C, temp, src_result, cv::TM_CCORR);cv::minMaxLoc(src_result, &minVal, &maxVal, &minLoc, &maxLoc);cv::rectangle(src_C, cv::Point(minLoc.x, minLoc.y), cv::Point((minLoc.x + temp.cols), (minLoc.y + temp.rows)), cv::Scalar(0, 0, 255), 2);cv::Mat src_CN = src.clone();cv::matchTemplate(src_CN, temp, src_result, cv::TM_CCORR_NORMED);cv::minMaxLoc(src_result, &minVal, &maxVal, &minLoc, &maxLoc);cv::rectangle(src_CN, cv::Point(minLoc.x, minLoc.y), cv::Point((minLoc.x + temp.cols), (minLoc.y + temp.rows)), cv::Scalar(0, 0, 255), 2);cv::Mat src_CF = src.clone();cv::matchTemplate(src_CF, temp, src_result, cv::TM_CCOEFF);cv::minMaxLoc(src_result, &minVal, &maxVal, &minLoc, &maxLoc);cv::rectangle(src_CF, cv::Point(minLoc.x, minLoc.y), cv::Point((minLoc.x + temp.cols), (minLoc.y + temp.rows)), cv::Scalar(0, 0, 255), 2);cv::Mat src_CFN = src.clone();cv::matchTemplate(src_CFN, temp, src_result, cv::TM_CCOEFF_NORMED);cv::minMaxLoc(src_result, &minVal, &maxVal, &minLoc, &maxLoc);cv::rectangle(src_CFN, cv::Point(minLoc.x, minLoc.y), cv::Point((minLoc.x + temp.cols), (minLoc.y + temp.rows)), cv::Scalar(0, 0, 255), 2);//(8)显示图像(由于是单通道,故颜色三通道必须都有值)cv::imshow("src_S", src_S);cv::imshow("src_SN", src_SN);cv::imshow("src_C", src_C);cv::imshow("src_CN", src_CN);cv::imshow("src_CF", src_CF);cv::imshow("src_CFN", src_CFN);cv::waitKey(0);return 0;}
2.16、直方图(基于灰度图像计算)
直方图的灰度分布规律:均匀分布、过亮、过暗。
直方图均衡化(或自适应):都是用于提高图像的对比度。
直方图均衡化:对图像进行整体对比度提高;自适应直方图均衡化:对图像差分成多个区域,并分别进行局部对比度提高。
2.16.1、计算一个/多个数组的直方图:cv::calcHist()
在灰度图像中,统计每个灰度像素值(0~255)出现的频率次数。其反映了图像灰度值的分布情况。
#include <opencv2/imgproc.hpp>函数说明:void cv::calcHist( const Mat * images, int nimages, const int * channels, InputArray mask, OutputArray hist, int dims, const int * histSize, const float ** ranges, bool uniform = true, bool accumulate = false );输入参数:(1)images输入数组。它们都应该具有相同的深度,CV_8U、CV_16U或CV_32F,并且具有相同的尺寸。它们中的每一个都可以具有任意数量的通道。(2)nimages输入图像的数量。(3)channels选择输入图像的通道。(4)mask掩膜。与输入数组大小相同。None表示处理整个图像(常用),1表示需要处理的部分,0表示不需要处理。(5)hist输出的直方图。是一个密集或稀疏的dims维数组。(6)dims直方图维度。必须为正且不大于CV_MAX_DIMS(在当前OpenCV版本中等于32)。(7)histSize灰度级个数(柱子数量)。一般256。(8)ranges像素值范围。一般[0, 255]。11、当直方图是均匀的(uniform =true)时,,每个范围[i]是一个包含2个元素的数组。22、当直方图不均匀时(uniform=false),不包含L0和UhistSize[i]−1之间的数组元素。(9)uniform = true直方图是否均匀的标志(见上文)。(10)accumulate = false累加标志。如果true,则不清除直方图,直接累加计算多个数组的直方图,或者及时更新直方图。
2.16.2、比较两个直方图并返回指标:cv::compareHist()
使用指定的方法比较两个密集直方图或两个稀疏直方图。可以很好地处理1、2、3维密集直方图,但它可能不适用于高维稀疏直方图。
#include <opencv2/imgproc.hpp>函数说明:double cv::compareHist( InputArray H1, InputArray H2, int method );输入参数:(1)H1输入第一个直方图。(2)H2输入第二个直方图。(大小类型与H1相同)(3)method比较方法。详细公式见官网cv::HISTCMP_CORREL = 0相关性cv::HISTCMP_CHISQR = 1 卡方cv::HISTCMP_INTERSECT = 2 相交cv::HISTCMP_BHATTACHARYYA = 3 巴塔查里亚距离cv::HISTCMP_HELLINGER = 3 HISTCMP_BHATTACHARYYA的同义词。cv::HISTCMP_CHISQR_ALT = 4 选择卡方cv::HISTCMP_KL_DIV = 5 Kullback-Leibler散度
2.16.3、直方图均衡化:cv::equalizeHist()
直方图均衡化(Histogram Equalization,HE):将输入的灰度图像直方图通过
累积分布函数
转换成近似于均匀分布
的图像,从而增强图像的对比度。优点:适用于像素值分布比较均衡的图像。缺点:对于过亮或过暗的区域,就差强人意(但可以使用自适应,将超过阈值部分裁剪到其他灰度级)。
直方图均衡化的特点:
(1)像素值映射前后的大小关系不变,较亮仍亮,较暗仍暗,只是改变权重;(2)映射函数的值域在0和255之间,不能越界。
采用累积分布函数(原因):
(1)单调增函数(控制大小关系);(2)值域=[0,1](控制越界问题)。
详细步骤:直方图均衡化原理(举例说明)
(1)扫描输入图像的每一个像素,统计并计算灰度图像的直方图;(2)根据累积分布函数计算每个灰度级的映射关系;(3)将(每个灰度级)映射后的值替换原值,完成图像的均衡化。
举例说明:
#include <opencv2/imgproc.hpp>函数说明:void cv::equalizeHist( InputArray src,OutputArray dst );输入参数:(1)src输入8位单通道图像。(2)dst与src大小和类型相同的目标图像。
2.16.4、自适应直方图均衡化:cv::createCLAHE()
自适应直方图均衡化(Adaptive Histgram Equalization,AHE):会过度放大图像中相同区域的噪声。限制对比度自适应直方图均衡(Contrast Limited Adaptive Histgram Equalization,CLAHE):对每个小区域都使用对比度幅值限制,可以克服AHE的过度放大噪音问题。直方图均衡化和自适应直方图均衡化(实战)
详细步骤:
(1)将输入图像均匀切分为M x N个区域,并设置一个对比度限制阈值;(2)计算每个区域的直方图;(3)对直方图的每个灰度级进行阈值限制,超过该阈值则直接裁剪,并将裁剪部分通过直方图均衡化的累积分布函数进而 均匀分布到其他灰度级 ,最后得到每个区域的重构直方图。自适应直方图均衡AHE、CLAHE(图解)
#include <opencv2/imgproc.hpp>函数说明:cv::Ptr<cv::CLAHE> cv::createCLAHE( double clipLimit = 40.0, Size tileGridSize = Size(8, 8) );输入参数:(1)clipLimit = 40.0对比度限制阈值。(2)tileGridSize = Size(8, 8)网格大小(行和列)。将输入图像划分为大小相等的M × N块。
2.16.5、实战案例
#include <opencv2/opencv.hpp>#include <opencv2/highgui/highgui.hpp>//using namespace std;//using namespace cv;int main(int argc, char* argv[]){//(1)读取图像std::string img_path = "test.jpg";cv::Mat src = cv::imread(img_path, 1);//(2)判断图像是否读取成功if (!src.data){std::cout << "can't read image!" << std::endl;return -1;}//(4)直方图//4.1、通道分割std::vector<cv::Mat> bgr;cv::split(src, bgr);//4.2、计算直方图cv::Mat b_hist, g_hist, r_hist;int numbins = 256;//灰度级个数(柱子数量)。一般256。float range[] = {0, 256 };//像素值范围。一般[0, 255]。const float* histRange = {range };cv::calcHist(&bgr[0], 1, 0, cv::Mat(), b_hist, 1, &numbins, &histRange);cv::calcHist(&bgr[1], 1, 0, cv::Mat(), g_hist, 1, &numbins, &histRange);cv::calcHist(&bgr[2], 1, 0, cv::Mat(), r_hist, 1, &numbins, &histRange);//4.3、新建空白直方图int hist_width = 512;int hist_height = 300;cv::Mat hist_Image(hist_height, hist_width, CV_8UC3, cv::Scalar(20, 20, 20));//4.4、标准化:将图像直方图的高度与输出直方图的高度保持一致cv::normalize(b_hist, b_hist, 0, hist_height, cv::NORM_MINMAX);cv::normalize(g_hist, g_hist, 0, hist_height, cv::NORM_MINMAX);cv::normalize(r_hist, r_hist, 0, hist_height, cv::NORM_MINMAX);//4.5、线图int binStep = cvRound((float)hist_width / (float)numbins);for (int i = 1; i < numbins; i++){//11、将宽度除以数组大小,进行标准化。//22、统计像素值在[0, 255]中的数量。//33、绘制曲线。x范围[i-1, i];y是对应xi中的像素值。cv::line(hist_Image, cv::Point(binStep * (i - 1), hist_height - cvRound(b_hist.at<float>(i - 1))),cv::Point(binStep * (i), hist_height - cvRound(b_hist.at<float>(i))),cv::Scalar(255, 0, 0));cv::line(hist_Image, cv::Point(binStep * (i - 1), hist_height - cvRound(g_hist.at<float>(i - 1))),cv::Point(binStep * (i), hist_height - cvRound(g_hist.at<float>(i))),cv::Scalar(0, 255, 0));cv::line(hist_Image, cv::Point(binStep * (i - 1), hist_height - cvRound(r_hist.at<float>(i - 1))),cv::Point(binStep * (i), hist_height - cvRound(r_hist.at<float>(i))),cv::Scalar(0, 0, 255));}//(5)直方图均衡化double cpH1 = cv::compareHist(b_hist, g_hist, cv::HISTCMP_CORREL);double cpH2 = cv::compareHist(b_hist, g_hist, cv::HISTCMP_CHISQR);double cpH3 = cv::compareHist(b_hist, g_hist, cv::HISTCMP_INTERSECT);double cpH4 = cv::compareHist(b_hist, g_hist, cv::HISTCMP_BHATTACHARYYA);double cpH5 = cv::compareHist(b_hist, g_hist, cv::HISTCMP_HELLINGER);double cpH6 = cv::compareHist(b_hist, g_hist, cv::HISTCMP_CHISQR_ALT);double cpH7 = cv::compareHist(b_hist, g_hist, cv::HISTCMP_KL_DIV);std::cout << cpH1 << std::endl;std::cout << cpH2 << std::endl;std::cout << cpH3 << std::endl;std::cout << cpH4 << std::endl;std::cout << cpH5 << std::endl;std::cout << cpH6 << std::endl;std::cout << cpH7 << std::endl;//(6)直方图均衡化cv::Mat image_EH0, image_EH1, image_EH2;cv::equalizeHist(bgr[0], image_EH0);cv::equalizeHist(bgr[1], image_EH1);cv::equalizeHist(bgr[2], image_EH2);//(7)自适应直方图均衡化cv::Mat img_clahe0, img_clahe1, img_clahe2;cv::Ptr<cv::CLAHE> clahe = cv::createCLAHE();//实例化CLAHE算法clahe->setClipLimit(4);//在CLAHE对象上,设置对比度限制阈值clahe->setTilesGridSize(cv::Size(8, 8));//在CLAHE对象上,设置均匀切分的区域clahe->apply(bgr[0], img_clahe0);//在CLAHE对象上,调用.apply()方法(对B通道)来应用直方图均衡化clahe->apply(bgr[1], img_clahe1);//在CLAHE对象上,调用.apply()方法(对G通道)来应用直方图均衡化clahe->apply(bgr[2], img_clahe2);//在CLAHE对象上,调用.apply()方法(对R通道)来应用直方图均衡化//(8)显示图像(由于是单通道,故颜色三通道必须都有值)cv::imshow("hist", hist_Image);cv::imshow("EH0", image_EH0);cv::imshow("EH1", image_EH1);cv::imshow("EH2", image_EH2);cv::imshow("clahe0", img_clahe0);cv::imshow("clahe1", img_clahe1);cv::imshow("clahe2", img_clahe2);cv::waitKey(0);return 0;}
2.17、基于傅里叶变换的(低通滤波 + 高通滤波)
时域分析:以时间作为参照来观察动态世界。世间万物都在随着时间不停的改变,并且永远不会静止下来。
频域分析:在频域中,你会发现世界是静止的、永恒不变的。
傅里叶分析:任何周期函数,都可以看作是不同振幅,不同相位正弦波的叠加。
举例:利用对不同琴键不同力度,不同时间点的敲击,可以组合出任何一首乐曲。分类:傅里叶级数(Fourier Serie)和傅里叶变换(Fourier Transformation)。作用
(1)高频:变化剧烈的灰度分量,例如:边缘、噪点
(2)低频:变化缓慢的灰度分量,例如:一片大海滤波器
(1)低通滤波器:只保留低频,会使得图像模糊。一般用于去噪,因为噪点是高频信息。
(2)高通滤波器:只保留高频,会使得图像增强。保留图像的边缘和噪点等。
2.17.1、傅里叶变换:cv::dft()
执行一维或二维浮点数组的正向或反向离散傅里叶变换。
#include <opencv2/core.hpp>函数说明:void cv::dft( InputArray src, OutputArray dst, int flags = 0, int nonzeroRows = 0 );输入参数:(1)src输入数组。可以是实数也可以是复数。(2)dst输出数组。其大小和类型取决于flags。(3)flags = 0转换标志。cv::DFT_INVERSE 执行1D或2D逆变换,而不是默认的正转换。cv::DFT_SCALE 缩放比例标识符,输出的结果会以1/N进行缩放。N=数组元素的数量cv::DFT_ROWS 对输入矩阵的每一行执行正变换或逆变换。能够同时处理多个向量,并减少3D和高维变换的开销。cv::DFT_COMPLEX_OUTPUT 一维或二维实数数组正变换。cv::DFT_REAL_OUTPUT 一维或二维复数数组逆变换。cv::DFT_COMPLEX_INPUT 指定输入为实数输入。输入必须有2个通道。cv::DCT_INVERSE执行逆1D或2D转换,而不是默认的正向转换。cv::DCT_ROWS 对输入矩阵的每一行执行正变换或逆变换。能够同时处理多个向量,并减少3D和高维变换的开销。(4)nonzeroRows = 0默认值为0。若设为非零值,dft函数会将该值作为非零行的有效区间长度,只对非零行进行处理,提高计算效率。
2.17.2、傅里叶反变换:cv::idft()
计算一维或二维阵列的离散傅里叶反变换。默认情况下,dft和idft都不会缩放结果。因此,您应该显式地将DFT_SCALE传递给dft或idft中的一个,以使这些变换相互逆。
#include <opencv2/core.hpp>函数说明:void cv::idft( InputArray src, OutputArray dst, int flags = 0, int nonzeroRows = 0 );输入参数:(1)src输入数组。可以是实数也可以是复数。(2)dst输出数组。其大小和类型取决于flags。(3)flags = 0转换标志。cv::DFT_INVERSE 执行1D或2D逆变换,而不是默认的正转换。cv::DFT_SCALE 缩放比例标识符,输出的结果会以1/N进行缩放。N=数组元素的数量cv::DFT_ROWS 对输入矩阵的每一行执行正变换或逆变换。能够同时处理多个向量,并减少3D和高维变换的开销。cv::DFT_COMPLEX_OUTPUT 一维或二维实数数组正变换。cv::DFT_REAL_OUTPUT 一维或二维复数数组逆变换。cv::DFT_COMPLEX_INPUT 指定输入为实数输入。输入必须有2个通道。cv::DCT_INVERSE执行逆1D或2D转换,而不是默认的正向转换。cv::DCT_ROWS 对输入矩阵的每一行执行正变换或逆变换。能够同时处理多个向量,并减少3D和高维变换的开销。(4)nonzeroRows = 0默认值为0。若设为非零值,dft函数会将该值作为非零行的有效区间长度,只对非零行进行处理,提高计算效率。
2.17.3、计算相位谱:cv::phase()
计算由x和y的对应元素组成的每个2D矢量的旋转角度。计算公式:angle(I)=atan2(y(I), x(I))
角度估计精度约为0.3度。当x(I)=y(I)=0时,对应角(I)设为0。
#include <opencv2/core.hpp>函数说明:void cv::phase( InputArray x, InputArray y, OutputArray angle, bool angleInDegrees = false );输入参数:(1)x输入二维矢量的x坐标的浮点数组。(2)y输入二维矢量的y坐标数组。它必须和x有相同的大小和类型。(3)angle角度。输出与x大小和类型相同的数组。(4)angleInDegrees = false当为true时,该函数以度数计算角度,否则以弧度计算。
2.17.4、计算幅度谱:cv::magnitude()
计算由x和y数组的相应元素组成的2D向量的大小。计算公式:dst(I) = sqrt(x(I)^2 + y(I)^2)
#include <opencv2/core.hpp>函数说明:void cv::magnitude( InputArray x, InputArray y, OutputArray magnitude );输入参数:(1)x输入二维矢量的x坐标的浮点数组。(2)y输入二维矢量的y坐标数组。它必须和x有相同的大小和类型。(3)magnitude 幅值。输出与x大小和类型相同的数组。
2.17.5、计算x和y的坐标:cv::polarToCart()
通过二维矢量的大小和角度来计算x和y的坐标。估计坐标的相对精度约为1e-6。计算公式:x(I) = magnitude(I) * cos(angle(I))
计算公式:y(I) = magnitude(I) * sin(angle(I))
#include <opencv2/core.hpp>函数说明:void cv::polarToCart( InputArray magnitude, InputArray angle, OutputArray x, OutputArray y, bool angleInDegrees = false );输入参数:(1)magnitude输入二维矢量(大小)的浮点数组。若为空矩阵,则假设所有的大小都是=1;若不为空,则必须具有与angle相同的大小和类型。(2)angle输入二维矢量(角度)的浮点数组。(3)x二维矢量的x坐标输出数组。它有相同的尺寸和类型的角度。(4)y二维矢量的y坐标输出数组。它有相同的尺寸和类型的角度。(5)angleInDegrees = false 当为true时,输入角以度数表示,否则以弧度表示。
2.17.6、获取最适合傅里叶正变换的宽 / 高:cv::getOptimalDFTSize()
DFT(傅里叶正变换)性能不是向量大小的单调函数。因此,当您计算两个数组的卷积或执行数组的频谱分析时,通常有必要在输入数据中填充零,以获得比原始数组转换速度快得多的更大的数组。大小为2的幂(2,4,8,16,32,…)的数组处理速度最快。但是,数组的大小是2、3和5的乘积(例如,300 = 55322)的处理效率也很高。
#include <opencv2/core.hpp>函数说明:int cv::getOptimalDFTSize( int vecsize);输入参数:vecsize给定向量。如果vecsize太大(非常接近INT_MAX),则返回一个负数。返回值:N返回大于或等于vecsize的最小数 N。
2.17.7、实战案例
傅里叶变换及低通滤波再反变换(C++&&opencv)
#include <opencv2\opencv.hpp>#include <opencv2/highgui/highgui.hpp>//using namespace cv;//using namespace std;void FourierTransform(cv::Mat& image, int value){//(1)数据准备image.convertTo(image, CV_32F);//数据格式转换std::vector<cv::Mat> channels;cv::split(image, channels); //RGB通道分离cv::Mat image_B = channels[0];int m1 = cv::getOptimalDFTSize(image_B.rows); //选取最适合做fft的宽int n1 = cv::getOptimalDFTSize(image_B.cols);//选取最适合做fft的高cv::Mat padded;//填充cv::copyMakeBorder(image_B, padded, 0, m1 - image_B.rows, 0, n1 - image_B.cols, cv::BORDER_CONSTANT, cv::Scalar::all(0));cv::Mat planes[] = {cv::Mat_<float>(padded), cv::Mat::zeros(padded.size(), CV_32F) };cv::Mat complexI;//(2)傅里叶正变换//planes[0], planes[1]是实部和虚部cv::merge(planes, 2, complexI); //通道合并cv::dft(complexI, complexI, cv::DFT_SCALE | cv::DFT_COMPLEX_OUTPUT);//傅里叶正变换cv::split(complexI, planes);//通道分离//由实部planes[0]和虚部planes[1]得到幅度谱mag和相位谱phcv::Mat ph, mag, idft;cv::phase(planes[0], planes[1], ph);cv::magnitude(planes[0], planes[1], mag); //(3)重新排列傅里叶图像中的象限,使得原点位于图像中心int cx = mag.cols / 2;int cy = mag.rows / 2;cv::Mat q0(mag, cv::Rect(0, 0, cx, cy)); //左上角图像划定ROI区域cv::Mat q1(mag, cv::Rect(cx, 0, cx, cy));//右上角图像cv::Mat q2(mag, cv::Rect(0, cy, cx, cy));//左下角图像cv::Mat q3(mag, cv::Rect(cx, cy, cx, cy));//右下角图像//3.1、变换左上角和右下角象限cv::Mat tmp;q0.copyTo(tmp);q3.copyTo(q0);tmp.copyTo(q3);//3.2、变换右上角和左下角象限q1.copyTo(tmp);q2.copyTo(q1);tmp.copyTo(q2);cv::imshow("mag", mag);//3.3、滤波器for (int i = 0; i < mag.cols;i++){for (int j = 0; j < mag.rows; j++){if (abs(i - mag.cols / 2) > mag.cols / 10 || abs(j - mag.rows / 2) > mag.rows / 10)mag.at<float>(j, i) = value;}} cv::imshow("mag2", mag);//3.4、变换左上角和右下角象限q0.copyTo(tmp);q3.copyTo(q0);tmp.copyTo(q3);//3.5、变换右上角和左下角象限q1.copyTo(tmp);q2.copyTo(q1);tmp.copyTo(q2);//(4)傅里叶逆变换cv::polarToCart(mag, ph, planes[0], planes[1]); //由幅度谱mag和相位谱ph恢复实部planes[0]和虚部planes[1]cv::merge(planes, 2, idft);cv::dft(idft, idft, cv::DFT_INVERSE | cv::DFT_REAL_OUTPUT);image_B = idft(cv::Rect(0, 0, image.cols & -2, image.rows & -2));image_B.copyTo(channels[0]);cv::merge(channels, image);image.convertTo(image, CV_8U);}int main(){//(1)读取图像std::string img_path = "test.jpg";cv::Mat img = cv::imread(img_path, 0);//读取灰度图cv::imshow("src", img);//(2)判断图像是否读取成功if (!img.data){std::cout << "can't read image!" << std::endl;return -1;}//cvtColor(img, img, COLOR_BGR2GRAY);//(3)傅里叶变换int value = 1;//0低通滤波器、1高通滤波器FourierTransform(img, value);cv::imshow("Filter_img", img);cv::waitKey();return 0;}
2.18、角点检测
角点:指边缘轮廓的一个角特征点。简单来说,角点既是边缘特征点也是角特征点。
2.18.1、算法原理
检测原理:通过滑动窗在各个方向上进行移动,检测窗口内的平均像素灰度值的变换情况。
(1)当检测窗口移动时,若灰度值在水平、竖直两个方向上的变化均较小,判定为平原地带;
(2)当检测窗口移动时,若灰度值仅在水平或竖直其中之一上的有较大变化,判定为边缘地带;
(3)当检测窗口移动时,若灰度值在水平、竖直两个方向上的变化均较大,判定为角点地带;
算法实现步骤
(1)利用Soble的
SobelX
和SobleY
算子,分别计算出X和Y方向的梯度值;(2)计算出
Ix, Iy的二阶偏导Ix^2, Iy^2
与梯度乘积Ix * Iy
;(3)利用高斯函数对 M 矩阵进行滤波。矩阵M由步骤二组成。
(4)计算局部特征结果矩阵M的特征值和响应函数:
R = det(M)-k(trace(M))^2。其中 (0.04<=k<=0.06)
(5)将计算出响应函数的值C进行非极大值抑制,滤除一些不是角点的点,同时要满足大于设定的阈值。
2.18.2、Harris角点检测:cv::cornerHarris()
图像中的角点可以作为该响应图的局部最大值。对于每个像素(x,y),它计算blockSize×blockSize邻域上的2×2梯度协方差矩阵M(x,y)。
函数说明:void cv::cornerHarris( InputArray src, OutputArray dst, int blockSize, int ksize, double k, int borderType = BORDER_DEFAULT );输入参数:src输入单通道8位或浮点图像dst输入单通道32位浮点图像(CV_32FC1)。大小与src相同。blockSize邻域大小。ksizeslobel算子的卷积核大小。一般为1、3、5或7。k探测器自由参数。borderType = BORDER_DEFAULT 边界类型(即边界填充方式)。默认BORDER_DEFAULT。不支持BORDER_WRAP。cv::BORDER_CONSTANT = 0 iiiiii|abcdefgh|iiiiiii常量法。填充常数值cv::BORDER_REPLICATE = 1 aaaaaa|abcdefgh|hhhhhhh复制法。复制最边缘像素cv::BORDER_REFLECT = 2 fedcba|abcdefgh|hgfedcb反射法。以两边为轴cv::BORDER_WRAP = 3 cdefgh|abcdefgh|abcdefg外包装法。cv::BORDER_REFLECT_101 = 4 gfedcb|abcdefgh|gfedcba反射法。以最边缘像素为轴cv::BORDER_TRANSPARENT = 5 uvwxyz|abcdefgh|ijklmnocv::BORDER_REFLECT101 = 6 same as BORDER_REFLECT_101cv::BORDER_DEFAULT = 7 same as BORDER_REFLECT_101cv::BORDER_ISOLATED = 8 do not look outside of ROI
2.18.3、实战案例
图像处理_OpenCV# Harris角点检测原理及C++实现
Harris角点检测原理及C++实现(底层代码)
#include <opencv2\opencv.hpp>#include <opencv2/highgui/highgui.hpp>//using namespace std;//using namespace cv;int thresh = 130;int max_count = 255;cv::Mat img, img_gray;const char* output_title = "Result";void Harris_Demo(int, void *);int main(int argv, char** argc) {//(1)读取图像std::string img_path = "test.jpg";img = cv::imread(img_path, 1);//(2)判断图像是否读取成功if (img.empty()){std::cout << "can't read image!" << std::endl;return -1;}//(3)角点检测cv::namedWindow(output_title, cv::WINDOW_AUTOSIZE);cv::cvtColor(img, img_gray, cv::COLOR_BGR2GRAY);cv::createTrackbar("Threshold", output_title, &thresh, max_count, Harris_Demo);Harris_Demo(0, 0);cv::waitKey(0);return 0;}void Harris_Demo(int, void *) {cv::Mat dst, norm_dst, normScaleDst;dst = cv::Mat::zeros(img_gray.size(), CV_32FC1);cv::cornerHarris(img_gray, dst, 2, 3, 0.04, cv::BORDER_DEFAULT);//最大最小值归一化[0, 255]cv::normalize(dst, norm_dst, 0, 255, cv::NORM_MINMAX, CV_32FC1, cv::Mat());cv::convertScaleAbs(norm_dst, normScaleDst);cv::Mat resultImg = img.clone();for (int row = 0; row < resultImg.rows; row++) {//定义每一行的指针uchar* currentRow = normScaleDst.ptr(row);for (int col = 0; col < resultImg.cols; col++) {int value = (int)*currentRow;if (value > thresh) {circle(resultImg, cv::Point(col, row), 2, cv::Scalar(0, 0, 255), 2, 8, 0);}currentRow++;}}cv::imshow(output_title, resultImg);}
2.19、目标分割(分水岭) —— 对图像质量和参数设置要求较高,需根据实况相应调整。
适用范围:背景全白或全黑,对于背景较为复杂或重叠区域效果非常差(比如:细胞分割)。
2.19.1、算法原理
分水岭算法:核心是寻找局部极小值,但由于真实图像存在噪声点或多种干扰因素,最终会得到很多冗余的局部极值点,导致过度分割现象。基于标记的分水岭算法:通过指定水位高度(灰度层级)为起始点,而不是盆地,进而得到标记(mark)图像。可以避免很小的噪声极值区域,进而避免过度分割现象。
分水岭算法(watershed):是一种形态学分割算法。
算法原理(举例说明):
(1)将灰度图像转换为梯度图像。梯度图可以看成是一副凹凸不平的山丘图(图a),由盆地和山岭组成。其中:梯度值看作高低起伏的山岭(图像背景),局部极小值看作盆地(图像前景)。(2)当暴雨注入山丘中,水会顺着地势优先注入地势最低的盆地(图b)(3)然后随着水位升高再注入更高一级的波谷(图c),依次向上。(4)为了保证先注满第一个波谷,需要在波谷两侧修建大坝(图d)。(5)随着水位越来越高,会设置更多更高的大坝,大坝的最高处等于图像中的最大灰度值。最终,所有区域都在分水岭线上相遇。而修建的这些大坝完成了对整个图像的像素分区。 目标分割算法之分水岭算法
算法实现步骤:
(1)将灰度图像转换为梯度图像;(2)对梯度图像中的所有像素点根据灰度级进行分类,并设定一个测地距离阈值(即注水起始点,又叫标记点),以标记点作为种子(前景),提取mark图像。若标记点为灰度值最低点(默认),则是分水岭算法,否则就是基于标记的分水岭算法。(3)当水平面上升过程中,会开始接触mark图像的邻域像素点。(4)计算(mask)邻域范围的像素点与最近的零像素点的测地距离;若小于阈值,则将这些像素淹没(背景),否则在这些像素上设置大坝(前景)。(5)随着水位越来越高,会设置更多更高的大坝,大坝的最高处等于图像中的最大灰度值。最终,所有区域都在分水岭线上相遇。而修建的这些大坝完成了对整个图像的像素分区。为了区分不同区域,将区域之间的分界处的值置为-1。形态学分水岭算法原理及示例实现
2.19.2、距离变换(计算二值图像中每个像素与最近的零像素点的距离):cv::distanceTransform()
距离变换与mask有关:计算某像素点与最近的零像素点的距离时,并不仅仅只考虑该点,还需考虑该点邻域范围的像素点(mask)与最近的零像素点的距离,最后得到加权后的结果。图像上越亮的点,代表离零点的距离越远。
#include <opencv2/imgproc.hpp>函数说明:void cv::distanceTransform( InputArray src, OutputArray dst, OutputArray labels, int distanceType, int maskSize, int labelType = DIST_LABEL_CCOMP );输入参数:(1)src(二值化)输入图像。必须是CV_8UC1。(2)dst输出图像计算距离。与src大小相同,类型为CV_8UC1、或CV_32FC1。(3)labels输出二维标签数组(离散Voronoi图)。与src大小相同,类型为CV_32SC1。(4)distanceType距离计算的类型。常用类型=1,2,3.cv::DIST_USER = 0 用户自定义距离。cv::DIST_L1 = 1distance = |x1-x2| + |y1-y2|cv::DIST_L2 = 2 欧几里得距离cv::DIST_C = 3 distance = max(|x1-x2|,|y1-y2|)cv::DIST_L12 = 4 L1-L2度量:distance = 2(sqrt(1+x*x/2) - 1))cv::DIST_FAIR = 5 distance = c^2(|x|/c-log(1+|x|/c)), c = 1.3998cv::DIST_WELSCH = 6 distance = c^2/2(1-exp(-(x/c)^2)), c = 2.9846cv::DIST_HUBER = 7 distance = |x|<c ? x^2/2 : c(|x|-c/2), c=1.345(5)maskSize距离变换的mask大小。距离类型为DIST_L1或DIST_C,该参数强制为3,因为3×3、5×5、或更大的mask都将给出相同的结果。cv::DIST_MASK_3 = 0mask=3对于粗略的距离估计DIST_L2,使用3×3掩码。cv::DIST_MASK_5 = 1 mask=5对于精确的距离估计DIST_L2,使用5×5掩码。cv::DIST_MASK_PRECISE = 2 官网没有说明(不支持)(6)labelType = DIST_LABEL_CCOMP标签类型。cv::DIST_LABEL_CCOMP 在src中每个为零的连接组件(以及最接近连接组件的所有非零像素)将被分配相同的标签。cv::DIST_LABEL_PIXEL 每个零像素(以及最接近它的所有非零像素)都有自己的标签。
2.19.3、归一化:cv::normalize()
#include <opencv2/core.hpp>函数说明:void cv::normalize(InputArray src, InputOutputArray dst, double alpha = 1, double beta = 0, int norm_type = NORM_L2, int dtype = -1, InputArray mask = noArray() );输入参数:src输入数组dst输出数组。与src大小相同alpha = 1范数值,在范围归一化的情况下归一化到或较低的范围边界。beta = 0只用于规范上限范围。因此只在NORM_MINMAX中起作用;norm_type = NORM_L2规范化类型。cv::NORM_INF 输出矩阵的数值为:原始矩阵数值除以矩阵最大值的结果。alpha可以控制倍数,beta值无用。cv::NORM_L1 输出矩阵的数值为:原始矩阵数值除以矩阵数据绝对值和的结果。alpha可以控制倍数,beta值无用。cv::NORM_L2 输出矩阵的数值为:原始矩阵数值除以矩阵数据平方和再开根号的结果。alpha可以控制倍数,beta值无用。cv::NORM_L2SQR cv::NORM_HAMMING 当有一个输入数组时,从0开始计算该数组的汉明距离;当有两个输入数组时,计算数组之间的汉明距离。cv::NORM_HAMMING2 类似于NORM_HAMMING,但在计算中,输入序列的每两个比特将被添加并作为单个比特处理,用于与NORM_HAMMING相同的计算。cv::NORM_TYPE_MASK 位掩码,可用于将规范类型与规范标志分开cv::NORM_RELATIVE cv::NORM_MINMAX alpha和beta的最大值是归一化的最大值,两者的最小值是归一化的最小值,alpha为1,beta为0;同alpha为0,beta为1。dtype = -1数据类型。负值表示输出数组的类型与src相同。否则,它具有与src和depth=CV_MAT_DEPTH(dtype)相同的通道数。mask = noArray()选择感兴趣区域。选定后只对该区域进行操作。
2.19.4、基于标记的分水岭算法:cv::watershed()
主要框图:输入图像 + 灰度化 + 二值化 + 距离变换 + 寻找种子 + 生成标记图像 + 分水岭算法 + 输出图像
#include <opencv2/imgproc.hpp>函数说明:void cv::watershed( InputArray image, InputOutputArray markers );输入参数:image输入图像。数据类型必须是CV_8UC3markers标记图像。数据类型必须是CV_32SC1,大小与image相同。输入是mark图像,输出是分割图像。
2.19.5、实战案例 —— 基于(自动)标记的分水岭算法
分水岭算法c++实现
基于边缘检测的分水岭算法c++
#include<opencv2\opencv.hpp>#include <string>//using namespace cv;//using namespace std;cv::Mat WaterSegment(cv::Mat src);int main(int argc, char* argv[]){//(1)读取图像std::string img_path = "coin.jpg";cv::Mat src = cv::imread(img_path, 1);//(2)判断图像是否读取成功if (src.empty()){std::cout << "can't read image!" << std::endl;return -1;}//(3)分水岭算法cv::Mat dst = WaterSegment(src);cv::imshow("dst", dst);cv::waitKey(0);//等待用户任意按键后结束暂停功能return 0;}/*--------------------------------------------------函数说明:分水岭算法--------------------------------------------------输入参数:src输入图像返回值:dst输出图像--------------------------------------------------*/cv::Mat WaterSegment(cv::Mat src){//(1)图像处理cv::Mat grayImage;cv::cvtColor(src, grayImage, cv::COLOR_BGR2GRAY);//灰度化cv::imshow("GRAY", grayImage);cv::threshold(grayImage, grayImage, 0, 255, cv::THRESH_BINARY | cv::THRESH_OTSU);//二值化(使用大津法)cv::imshow("OTSU", grayImage);cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(9, 9), cv::Point(-1, -1));//获取结构化元素cv::morphologyEx(grayImage, grayImage, cv::MORPH_CLOSE, kernel);//闭运算cv::imshow("CLOSE1", grayImage);//(2)二次处理cv::distanceTransform(grayImage, grayImage, cv::DIST_L2, cv::DIST_MASK_3, 5);//距离变换cv::normalize(grayImage, grayImage, 0, 1, cv::NORM_MINMAX);//由于变换后结果非常小,故需要归一化到[0-1]cv::imshow("normalize", grayImage);grayImage.convertTo(grayImage, CV_8UC1);//数据类型转换:8位无符号整型单通道:(0-255)cv::threshold(grayImage, grayImage, 0, 255, cv::THRESH_BINARY);//(二次)二值化(使用大津法)cv::imshow("threshold", grayImage);cv::morphologyEx(grayImage, grayImage, cv::MORPH_CLOSE, kernel);//(二次)闭运算cv::imshow("CLOSE2", grayImage);//(3)标记mark图像std::vector<std::vector<cv::Point>> contours;cv::findContours(grayImage, contours, cv::RETR_TREE, cv::CHAIN_APPROX_SIMPLE, cv::Point(-1, -1));//检测轮廓cv::Mat marks = cv::Mat::zeros(grayImage.size(), CV_32SC1);//数据类型转换:32位有符号整型三通道(提高计算精度)for (size_t i = 0; i < contours.size(); i++){//saturate_cast<uchar>(x)://11、可以解决边界溢出问题。若像素值大于255,则赋值255;若像素值小于0,则赋值0。//22、为了区别不同区域,对每个区域进行编号:区域1、区域2、区域3...。将区域之间的分界处的值置为-1。cv::drawContours(marks, contours, static_cast<int>(i), cv::Scalar::all(static_cast<int>(i + 1)), 2);//绘制轮廓}//(4)分水岭算法(提取分割目标)cv::Mat kernel0 = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(3, 3), cv::Point(-1, -1));//获取结构化元素cv::morphologyEx(src, src, cv::MORPH_ERODE, kernel0);//腐蚀:去掉原图中的噪声或不相关信息cv::watershed(src, marks);//分水岭算法//(5)随机分配颜色(给每个轮廓)std::vector<cv::Vec3b> colors;for (size_t i = 0; i < contours.size(); i++){int r = cv::theRNG().uniform(0, 255);int g = cv::theRNG().uniform(0, 255);int b = cv::theRNG().uniform(0, 255);colors.push_back(cv::Vec3b((uchar)b, (uchar)g, (uchar)r));//将元素添加到向量最后位置}//(6)对每一个区域进行颜色填充cv::Mat dst = cv::Mat::zeros(marks.size(), CV_8UC3);//数据类型转换:8位无符号整型三通道int row = src.rows;int col = src.cols;int index = 0;for (int i = 0; i < row; i++){for (int j = 0; j < col; j++){index = marks.at<int>(i, j);if (index > 0 && index <= contours.size())//给每一个区域随机颜色{dst.at<cv::Vec3b>(i, j) = colors[index - 1];}else if (index == -1)//区域之间的边界为-1,全白{dst.at<cv::Vec3b>(i, j) = cv::Vec3b(255, 255, 255);}else//只检测到一个轮廓,全黑 {dst.at<cv::Vec3b>(i, j) = cv::Vec3b(0, 0, 0);}}}return dst;}
2.19.6、实战案例 —— 基于(手动)标记的分水岭算法
【OpenCV(C++)】分水岭算法
分水岭算法(手动标记):只对标记部分进行分割。标记的越细致,分割效果越好。
//手动标记分水岭算法,标记的越细致,分割效果越好。#include <opencv2/opencv.hpp>#include <opencv2/highgui/highgui.hpp>using namespace cv;using namespace std;#define WINDOW_NAME1 "【原图窗口】" #define WINDOW_NAME2 "【分水岭图】" Mat g_maskImage, g_srcImage;Point prevPt(-1, -1);static void ShowHelpText();//窗口帮助文档static void on_Mouse(int event, int x, int y, int flags, void*);//获取鼠标绘制区域int main(int argc, char** argv){//(1)窗口帮助文档system("color 6F");ShowHelpText();//(2)读取图像g_srcImage = imread("cell.jpg", 1);imshow(WINDOW_NAME1, g_srcImage);//(3)图像处理Mat srcImage, grayImage;g_srcImage.copyTo(srcImage);cvtColor(g_srcImage, g_maskImage, COLOR_BGR2GRAY);cvtColor(g_maskImage, grayImage, COLOR_GRAY2BGR);g_maskImage = Scalar::all(0);//(4)获取鼠标绘制区域setMouseCallback(WINDOW_NAME1, on_Mouse, 0);//设置鼠标事件的回调函数while (1)//可以多次运行{//给定多个按键,控制不同功能。int c = waitKey(0);//等待用户任意按键后结束暂停功能if ((char)c == 27)//Esc:退出程序break;else if ((char)c == '2')//2:恢复原始图像{g_maskImage = Scalar::all(0);srcImage.copyTo(g_srcImage);imshow(WINDOW_NAME1, g_srcImage);}else if ((char)c == '1' || (char)c == ' ')//1 or 空格:运行分水岭分割算法{//(5)检测轮廓int i, j, compCount = 0;vector<vector<Point> > contours;vector<Vec4i> hierarchy;findContours(g_maskImage, contours, hierarchy, cv::RETR_CCOMP, cv::CHAIN_APPROX_SIMPLE);if (contours.empty())continue;//(6)绘制轮廓Mat maskImage(g_maskImage.size(), CV_32S);maskImage = Scalar::all(0);for (int index = 0; index >= 0; index = hierarchy[index][0], compCount++)drawContours(maskImage, contours, index, Scalar::all(compCount + 1), -1, 8, hierarchy, INT_MAX);if (compCount == 0)continue;//(7)分水岭 + 计算耗时double dTime = (double)getTickCount();watershed(srcImage, maskImage);dTime = (double)getTickCount() - dTime;printf("\t处理时间 = %gms\n", dTime * 1000. / getTickFrequency());//(8)随机颜色vector<Vec3b> colorTab;for (i = 0; i < compCount; i++){int b = theRNG().uniform(0, 255);int g = theRNG().uniform(0, 255);int r = theRNG().uniform(0, 255);colorTab.push_back(Vec3b((uchar)b, (uchar)g, (uchar)r));}//(9)对每一个区域进行颜色填充Mat watershedImage(maskImage.size(), CV_8UC3);for (i = 0; i < maskImage.rows; i++)for (j = 0; j < maskImage.cols; j++){int index = maskImage.at<int>(i, j);if (index == -1)watershedImage.at<Vec3b>(i, j) = Vec3b(255, 255, 255);else if (index <= 0 || index > compCount)watershedImage.at<Vec3b>(i, j) = Vec3b(0, 0, 0);elsewatershedImage.at<Vec3b>(i, j) = colorTab[index - 1];}//(10)通道融合watershedImage = watershedImage * 0.5 + grayImage * 0.5;imshow(WINDOW_NAME2, watershedImage);}}return 0;}static void on_Mouse(int event, int x, int y, int flags, void*){if (x < 0 || x >= g_srcImage.cols || y < 0 || y >= g_srcImage.rows)return;if (event == cv::EVENT_LBUTTONUP || !(flags & cv::EVENT_FLAG_LBUTTON))prevPt = Point(-1, -1);else if (event == cv::EVENT_LBUTTONDOWN)prevPt = Point(x, y);else if (event == cv::EVENT_MOUSEMOVE && (flags & cv::EVENT_FLAG_LBUTTON)){Point pt(x, y);if (prevPt.x < 0)prevPt = pt;line(g_maskImage, prevPt, pt, Scalar::all(255), 5, 8, 0);line(g_srcImage, prevPt, pt, Scalar::all(255), 5, 8, 0);prevPt = pt;imshow(WINDOW_NAME1, g_srcImage);}}static void ShowHelpText(){printf("\n----------------------------------------------------------------------------\n");printf("\t(1)使用鼠标在原图中绘制mask区域,\n\n\t(2)通过键盘控制算法。""\n\n\t按键操作说明: \n\n""\t\t键盘按键【1】:运行分水岭分割算法\n""\t\t键盘按键【2】:恢复原图\n""\t\t键盘按键【ESC】:退出程序\n\n\n");}
2.19.7、实战案例 —— 基于(自动标记)边缘检测的分水岭算法
基于边缘检测的分水岭算法c++
#include <opencv2/highgui/highgui.hpp>#include <opencv2/opencv.hpp>//using namespace std;//using namespace cv;cv::Vec3b RandomColor(int value) //生成随机颜色函数{value = value % 255; //生成0~255的随机数cv::RNG rng;int aa = rng.uniform(0, value);int bb = rng.uniform(0, value);int cc = rng.uniform(0, value);return cv::Vec3b(aa, bb, cc);}int main(int argc, char** argv){//(1)读取图像std::string img_path = "save11.jpg";cv::Mat rgb_image = cv::imread(img_path, 1);//(2)判断图像是否读取成功if (rgb_image.empty()){std::cout << "can't read image!" << std::endl;return -1;}//(3)边缘检测cv::Mat rgb_image_blur, rgb_image_canny;cv::GaussianBlur(rgb_image, rgb_image_blur, cv::Size(5, 5), 0, 0);//高斯滤波(去噪)cv::Canny(rgb_image_blur, rgb_image_canny, 10, 120, 3, false);//边缘算子(提取边缘特征)cv::imshow("blur", rgb_image_blur);cv::imshow("binary", rgb_image_canny);//(4)轮廓检测std::vector<std::vector<cv::Point>>contours;std::vector<cv::Vec4i>hierarchy;cv::findContours(rgb_image_canny, contours, hierarchy, cv::RETR_LIST, cv::CHAIN_APPROX_SIMPLE, cv::Point());cv::Mat imageContours = cv::Mat::zeros(rgb_image.size(), CV_8UC1);cv::Mat marks(rgb_image.size(), CV_32S);marks = cv::Scalar::all(0);int index = 0;int compCount = 0;for (; index >= 0; index = hierarchy[index][0], compCount++){//对marks进行标记,对不同区域的轮廓进行编号,相当于设置注水点,有多少轮廓,就有多少注水点drawContours(marks, contours, index, cv::Scalar::all(compCount + 1), 1, 8, hierarchy);drawContours(imageContours, contours, index, cv::Scalar(255), 1, 8, hierarchy);}cv::Mat marksShows;cv::convertScaleAbs(marks, marksShows);cv::imshow("mark", marksShows);cv::imshow("轮廓", imageContours);//(5)分水岭算法cv::watershed(rgb_image, marks);cv::Mat afterWatershed;cv::convertScaleAbs(marks, afterWatershed);cv::imshow("watershed", afterWatershed);//(6)随机分配颜色(为每一个目标)std::vector<cv::Vec3b> colors;for (size_t i = 0; i < contours.size(); i++){int r = cv::theRNG().uniform(0, 255);int g = cv::theRNG().uniform(0, 255);int b = cv::theRNG().uniform(0, 255);colors.push_back(cv::Vec3b((uchar)b, (uchar)g, (uchar)r));//将元素添加到向量最后位置}//(7)对每一个区域进行颜色填充cv::Mat PerspectiveImage = cv::Mat::zeros(rgb_image.size(), CV_8UC3);for (int i = 0; i < marks.rows; i++){for (int j = 0; j < marks.cols; j++){int index = marks.at<int>(i, j);if (marks.at<int>(i, j) == -1){PerspectiveImage.at<cv::Vec3b>(i, j) = cv::Vec3b(255, 255, 255);}else{PerspectiveImage.at<cv::Vec3b>(i, j) = RandomColor(index);}}}cv::imshow("ColorFill", PerspectiveImage);cv::waitKey(0);return 0;}
2.20、目标分割(超像素分割SLIC) —— 生成密集细胞图
超像素分割的常见方法:TurboPixel,SLIC,NCut,Graph-based,Watershed(Marker-based Watershed),Meanshift等等。SLIC 超像素分割算法详细介绍
2.20.1、SLIC算法原理
超像素分割:利用像素之间的特征相似性(相似纹理、相似颜色、相似亮度),对所有像素进行K-means聚类,然后使用少量的超像素代替原图。可以很大程度上降低图像后处理的复杂度。超像素分割SLIC算法原理
SLIC优点:(1)计算效率高。(2)超像素图由多个紧凑细胞构成,邻域特征比较容易表达。(3)同时适用于彩色图、灰度图。(4)只需要设置一个参数:超像素数量K。(超像素数量K与K-means聚类,K代表不同意思)
算法步骤:(缺点:比较耗时)
(1)输入图像大小(M x N),空间转换(RGB to LAB),LAB颜色更全面。(2)设置参数
K
,表示生成的超像素数量(即细胞总数量)。将图像切分为 K 个小块,每个小块大小= (M x N) / K
个像素。(3)假设每个小块的长和宽都均匀分布,则长和宽均可定义S = sqrt(M x N / K)
(4)遍历操作,获取每个小块的中心点坐标(x, y)及 Lab 值。(5)每个小块的中心点坐标(默认)=(S/2,S/2)
。改点坐标可能会出现在噪音点或边缘点。通过差分方法进行梯度计算,调整中心点。即对中心点的 8 领域像素点,计算最小梯度值对应的像素点,并将其作为新的中心点,差分计算梯度的公式:Gradient(x,y)=dx(i,j) + dy(i,j);
、dx(i,j) = I(i+1,j) - I(i,j);
、dy(i,j) = I(i,j+1) - I(i,j);
。遍历现中心点的 8 领域像素点,将其中计算得到最小 Gradient 值的像素点作为新的中心点(6)确定中心点后,通过K-means 算法对所有像素点进行聚类,并通过变换的欧氏聚距离迭代聚类的中心坐标。为了节省时间,只遍历每个小块中心点周边
2S*2S
区域内的像素点,计算该区域内每个像素点距离哪一个超像素块的中心点最近,并将其划分到其中;完成一次迭代后,重新计算每个超像素块的中心点坐标,并重新进行迭代(注:一般选择迭代 10 次,效率最高且效果最好)
Lab颜色空间:
L表示亮度:黑色至白色的范围(L值域:0到100)。A表示洋红色至绿色的范围(A值域:负值为绿色,正值为品红)B表示黄色至蓝色的范围(B值域:负值为蓝色,正值为黄色)。
Lab特点:更接近人类生理视觉,可以呈现人的肉眼所能感知的所有色彩。不仅包含RGB / CMYK的所有色域,且弥补了RGB色彩分布不均的问题。因为RGB在蓝色到绿色之间的过渡色彩过多,而在绿色到红色之间又缺少黄色和其他色彩。所以,在数字图形处理中,如果想要尽量保留宽阔的色域和丰富的色彩,最好选择Lab。
2.20.2、实战案例
论文浮现:基于C++实现SLIC 超像素分割算法
//(可优化)调用opencv c++直线读取与输出图像之间的矩阵转换,程序中其它的图像操作均未调用任何 opencv 的函数。所有操作均面向提取后的矩阵。# include <iostream># include <opencv2/opencv.hpp># include <vector># include <map>const float param_13 = 1.0f / 3.0f;const float param_16116 = 16.0f / 116.0f;const float Xn = 0.950456f;const float Yn = 1.0f;const float Zn = 1.088754f;using namespace std;using namespace cv;float gamma(float x){return x > 0.04045 ? powf((x + 0.055f) / 1.055f, 2.4f) : (x / 12.92);}float gamma_XYZ2RGB(float x){return x > 0.0031308 ? (1.055f * powf(x, (1 / 2.4f)) - 0.055) : (x * 12.92);}void XYZ2RGB(float X, float Y, float Z, int* R, int* G, int* B){float RR, GG, BB;RR = 3.2404542f * X - 1.5371385f * Y - 0.4985314f * Z;GG = -0.9692660f * X + 1.8760108f * Y + 0.0415560f * Z;BB = 0.0556434f * X - 0.2040259f * Y + 1.0572252f * Z;RR = gamma_XYZ2RGB(RR);GG = gamma_XYZ2RGB(GG);BB = gamma_XYZ2RGB(BB);RR = int(RR * 255.0 + 0.5);GG = int(GG * 255.0 + 0.5);BB = int(BB * 255.0 + 0.5);*R = RR;*G = GG;*B = BB;}void Lab2XYZ(float L, float a, float b, float* X, float* Y, float* Z){float fX, fY, fZ;fY = (L + 16.0f) / 116.0;fX = a / 500.0f + fY;fZ = fY - b / 200.0f;if (powf(fY, 3.0) > 0.008856)*Y = powf(fY, 3.0);else*Y = (fY - param_16116) / 7.787f;if (powf(fX, 3) > 0.008856)*X = fX * fX * fX;else*X = (fX - param_16116) / 7.787f;if (powf(fZ, 3.0) > 0.008856)*Z = fZ * fZ * fZ;else*Z = (fZ - param_16116) / 7.787f;(*X) *= (Xn);(*Y) *= (Yn);(*Z) *= (Zn);}void RGB2XYZ(int R, int G, int B, float* X, float* Y, float* Z){float RR = gamma((float)R / 255.0f);float GG = gamma((float)G / 255.0f);float BB = gamma((float)B / 255.0f);*X = 0.4124564f * RR + 0.3575761f * GG + 0.1804375f * BB;*Y = 0.2126729f * RR + 0.7151522f * GG + 0.0721750f * BB;*Z = 0.0193339f * RR + 0.1191920f * GG + 0.9503041f * BB;}void XYZ2Lab(float X, float Y, float Z, float* L, float* a, float* b){float fX = Xn, fY = Yn, fZ = Zn;if (Y > 0.008856f)fY = pow(Y, param_13);elsefY = 7.787f * Y + param_16116;*L = 116.0f * fY - 16.0f;*L = *L > 0.0f ? *L : 0.0f;if (X > 0.008856f)fX = pow(X, param_13);elsefX = 7.787f * X + param_16116;if (Z > 0.008856)fZ = pow(Z, param_13);elsefZ = 7.787f * Z + param_16116;*a = 500.0f * (fX - fY);*b = 200.0f * (fY - fZ);}void RGB2Lab(int R, int G, int B, float* L, float* a, float* b){float X, Y, Z;RGB2XYZ(R, G, B, &X, &Y, &Z);XYZ2Lab(X, Y, Z, L, a, b);}void Lab2RGB(float L, float a, float b, int* R, int* G, int* B){float X, Y, Z;Lab2XYZ(L, a, b, &X, &Y, &Z);XYZ2RGB(X, Y, Z, R, G, B);}int main(int argc, char** argv){Mat src = imread("test.jpg");if (src.empty()){cout << "read error" << endl;return 0;}imshow("src", src);vector<vector<vector<float>>> image; //x,y,(L,a,b)int rows = src.rows;int cols = src.cols;int N = rows * cols;int K = 200; //K个超像素int M = 40;int S = (int)sqrt(N / K); //以步距为S的距离划分超像素cout << "rows:" << rows << " cols:" << cols << endl;cout << "cluster num:" << K << endl;cout << "S:" << S << endl;//RGB2Labfor (int i = 0; i < rows; i++){vector<vector<float>> line;for (int j = 0; j < cols; j++){vector<float> pixel;float L;float a;float b;RGB2Lab(src.at<Vec3b>(i, j)[2], src.at<Vec3b>(i, j)[1], src.at<Vec3b>(i, j)[0], &L, &a,&b);pixel.push_back(L);pixel.push_back(a);pixel.push_back(b);line.push_back(pixel);}image.push_back(line);}cout << "RGB2Lab is finished" << endl;//聚类中心,[x y l a b]vector<vector<float>> Cluster;//生成所有聚类中心for (int i = S / 2; i < rows; i += S){for (int j = S / 2; j < cols; j += S){vector<float> c;c.push_back((float)i);c.push_back((float)j);c.push_back(image[i][j][0]);c.push_back(image[i][j][1]);c.push_back(image[i][j][2]);Cluster.push_back(c);}}int cluster_num = Cluster.size();cout << "init cluster is finished" << endl;//获得最小梯度值作为新中心点for (int c = 0; c < cluster_num; c++){int c_row = (int)Cluster[c][0];int c_col = (int)Cluster[c][1];//梯度以右侧和下侧两个像素点来计算,分别计算Lab三个的梯度来求和//需要保证当前点右侧和下侧是存在的点,否则就向左上移动来替代梯度值if (c_row + 1 >= rows){c_row = rows - 2;}if (c_col + 1 >= cols){c_col = cols - 2;}float c_gradient =image[c_row + 1][c_col][0] + image[c_row][c_col + 1][0] - 2 * image[c_row][c_col][0] +image[c_row + 1][c_col][1] + image[c_row][c_col + 1][1] - 2 * image[c_row][c_col][1] +image[c_row + 1][c_col][2] + image[c_row][c_col + 1][2] - 2 * image[c_row][c_col][2];for (int i = -1; i <= 1; i++){for (int j = -1; j <= 1; j++){int tmp_row = c_row + i;int tmp_col = c_col + j;if (tmp_row + 1 >= rows){tmp_row = rows - 2;}if (tmp_col + 1 >= cols){tmp_col = cols - 2;}float tmp_gradient =image[tmp_row + 1][tmp_col][0] + image[tmp_row][tmp_col + 1][0] -image[tmp_row][tmp_col][0] + image[tmp_row + 1][tmp_col][1] +image[tmp_row][tmp_col + 1][1] - 2 * image[tmp_row][tmp_col][1] +image[tmp_row + 1][tmp_col][2] + image[tmp_row][tmp_col + 1][2] -image[tmp_row][tmp_col][2];if (tmp_gradient < c_gradient){Cluster[c][0] = (float)tmp_row;Cluster[c][1] = (float)tmp_col;Cluster[c][2] = image[tmp_row][tmp_col][0];Cluster[c][3] = image[tmp_row][tmp_col][1];Cluster[c][3] = image[tmp_row][tmp_col][2];c_gradient = tmp_gradient;}}}}cout << "move cluster is finished";//创建一个dis的矩阵for each pixel = ∞vector<vector<double>> distance;for (int i = 0; i < rows; ++i){vector<double> tmp;for (int j = 0; j < cols; ++j){tmp.push_back(INT_MAX);}distance.push_back(tmp);}//创建一个dis的矩阵for each pixel = -1vector<vector<int>> label;for (int i = 0; i < rows; ++i){vector<int> tmp;for (int j = 0; j < cols; ++j){tmp.push_back(-1);}label.push_back(tmp);}//为每一个Cluster创建一个pixel集合vector<vector<vector<int>>> pixel(Cluster.size());//核心代码部分,迭代计算for (int t = 0; t < 10; t++){cout << endl << "iteration num:" << t + 1 << " ";//遍历所有的中心点,在2S范围内进行像素搜索int c_num = 0;for (int c = 0; c < cluster_num; c++){if (c - c_num >= (cluster_num / 10)){cout << "+";c_num = c;}int c_row = (int)Cluster[c][0];int c_col = (int)Cluster[c][1];float c_L = Cluster[c][2];float c_a = Cluster[c][3];float c_b = Cluster[c][4];for (int i = c_row - 2 * S; i <= c_row + 2 * S; i++){if (i < 0 || i >= rows){continue;}for (int j = c_col - 2 * S; j <= c_col + 2 * S; j++){if (j < 0 || j >= cols){continue;}float tmp_L = image[i][j][0];float tmp_a = image[i][j][1];float tmp_b = image[i][j][2];double Dc = sqrt((tmp_L - c_L) * (tmp_L - c_L) + (tmp_a - c_a) * (tmp_a - c_a) +(tmp_b - c_b) * (tmp_b - c_b));double Ds = sqrt((i - c_row) * (i - c_row) + (j - c_col) * (j - c_col));double D = sqrt((Dc / (double)M) * (Dc / (double)M) + (Ds / (double)S) * (Ds / (double)S));if (D < distance[i][j]){if (label[i][j] == -1){//还没有被标记过label[i][j] = c;vector<int> point;point.push_back(i);point.push_back(j);pixel[c].push_back(point);}else{int old_cluster = label[i][j];vector<vector<int>>::iterator iter;for (iter = pixel[old_cluster].begin(); iter != pixel[old_cluster].end(); iter++){if ((*iter)[0] == i && (*iter)[1] == j){pixel[old_cluster].erase(iter);break;}}label[i][j] = c;vector<int> point;point.push_back(i);point.push_back(j);pixel[c].push_back(point);}distance[i][j] = D;}}}}cout << " start update cluster";for (int c = 0; c < Cluster.size(); c++){int sum_i = 0;int sum_j = 0;int number = 0;for (int p = 0; p < pixel[c].size(); p++){sum_i += pixel[c][p][0];sum_j += pixel[c][p][1];number++;}int tmp_i = (int)((double)sum_i / (double)number);int tmp_j = (int)((double)sum_j / (double)number);Cluster[c][0] = (float)tmp_i;Cluster[c][1] = (float)tmp_j;Cluster[c][2] = image[tmp_i][tmp_j][0];Cluster[c][3] = image[tmp_i][tmp_j][1];Cluster[c][4] = image[tmp_i][tmp_j][2];}}//导出Lab空间的矩阵vector<vector<vector<float>>> out_image = image;//x,y,(L,a,b)for (int c = 0; c < Cluster.size(); c++){for (int p = 0; p < pixel[c].size(); p++){out_image[pixel[c][p][0]][pixel[c][p][1]][0] = Cluster[c][2];out_image[pixel[c][p][0]][pixel[c][p][1]][1] = Cluster[c][3];out_image[pixel[c][p][0]][pixel[c][p][1]][2] = Cluster[c][4];}out_image[(int)Cluster[c][0]][(int)Cluster[c][1]][0] = 0;out_image[(int)Cluster[c][0]][(int)Cluster[c][1]][1] = 0;out_image[(int)Cluster[c][0]][(int)Cluster[c][1]][2] = 0;}cout << endl << "export image mat finished" << endl;Mat dst = src.clone();for (int i = 0; i < rows; i++){for (int j = 0; j < cols; j++){float L = out_image[i][j][0];float a = out_image[i][j][1];float b = out_image[i][j][2];int R, G, B;Lab2RGB(L, a, b, &R, &G, &B);Vec3b vec3b;vec3b[0] = B;vec3b[1] = G;vec3b[2] = R;dst.at<Vec3b>(i, j) = vec3b;}}imshow("dst", dst);waitKey(0); //暂停,保持图像显示,等待按键结束return 0;}
2.21、霍夫变换
Opencv(C++):霍夫直线 + 霍夫圆
2.21.1、霍夫直线变换(基于边缘图像)
霍夫直线变换:原理图解 + 实战案例
主要目的:将数据从平面坐标转换到极坐标空间,完成直线提取。
平面坐标系:通过之间的斜率k和截距b来确定一条直线。y = kx+b极坐标系:通过半径r、角度θ来确定一条直线。r = xcosθ + y sinθ
非常有用的结论:
(1)图像空间中的每条直线在参数空间中都对应着单独一个点;
(2)图像空间中的直线上任何一部分线段在参数空间对应的是同一个点。
#include <opencv2/imgproc.hpp>函数说明:void cv::HoughLinesP( InputArray image, OutputArray lines, double rho, double theta, int threshold, double minLineLength = 0, double maxLineGap = 0 );输入参数:(1)image8位、单通道二进制源图像。该功能可以修改图像。(2)lines行的输出矢量。每条线由四元矢量(x1,y1,x2,y2)表示,其中(x1,y1)和(x2,y2)是每个检测线段的终点。(3)rho累加器的距离分辨率(以像素为单位)。(4)theta累加器的角度分辨率(以弧度为单位)。(5)threshold累加器阈值参数。大于阈值的行才会返回。(6)minLineLength = 0最小线路长度。小于该长度的线段将被拒绝。(7)maxLineGap = 0在同一条线上连接点的最大允许间距。
# include <opencv2/opencv.hpp>int main(int argc, char** argv){cv::Mat src = cv::imread("test.jpg");if(src.empty()){printf("could not load image..\n");return -1;}cv::Mat canny, dst;cv::Canny(src, canny, 150, 200); //canny算子cv::cvtColor(canny, dst, cv::COLOR_GRAY2BGR); //灰度图转换为彩色图std::vector<cv::Vec4f> plines;cv::HoughLinesP(canny, plines, 1, CV_PI / 180.0, 5, 0, 10);cv::Scalar color = cv::Scalar(0, 0, 255);for(size_t i = 0; i < plines.size(); i++){cv::Vec4f hline = plines[i];cv::line(dst, cv::Point(hline[0], hline[1]), cv::Point(hline[2], hline[3]), color, 3, cv::LINE_AA);}cv::imshow("input", src);cv::imshow("edge", canny);cv::imshow("output", dst);cv::waitKey(0);return 0;}
2.21.2、霍夫圆变换
与霍夫直线检测相似。
通常,该函数可以很好地检测圆心。然而,它可能无法找到正确的半径(可以人为指定半径范围)。
霍夫变换对噪声比较敏感,所以需要对图像做中值滤波。霍夫变换圆检测时基于图像梯度的实现:(1)检测边缘发现可能的圆心。(2)基于第一步从候选圆心开始计算最佳半径大小。
#include <opencv2/imgproc.hpp>函数说明:void cv::HoughCircles( InputArray image, OutputArray circles, int method, double dp, double minDist, double param1 = 100, double param2 = 100, int minRadius = 0, int maxRadius = 0 );输入参数:(1)image8位、单通道、灰度级输入图像。(2)circles圆的输出矢量。每个矢量编码为3或4元素浮点矢量(x,y,radius)或(x,y,radius,votes)。(3)method检测方法。cv::HOUGH_STANDARD = 0经典或标准霍夫变换。每条线由两个浮点数(ρ,θ)表示,其中ρ是(0,0)点和线之间的距离,θ是x轴和线法线之间的角度。因此,矩阵必须是(创建的序列将是)CV_32FC2类型cv::HOUGH_PROBABILISTIC = 1概率霍夫变换。(在图片包含几个长线性段的情况下更有效)它返回线段,而不是整条线。每个线段由起点和终点表示,矩阵必须是CV_32SC4类型的(创建的序列将是)。cv::HOUGH_MULTI_SCALE = 2经典霍夫变换的多尺度变体。这些行的编码方式与HOUGH_STANDARD相同。cv::HOUGH_GRADIENT = 321HT。cv::HOUGH_GRADIENT_ALT = 4HOUGH_GRADIENT的变化以获得更好的精度(4)dp累加器分辨率与图像分辨率的反比。如果dp=1,则累加器具有与输入图像相同的分辨率。如果dp=2,则蓄能器的宽度和高度是蓄能器宽度和高度的一半。对于HOUGH_GRADIENT_ALT,建议值为dp=1.5,除非需要检测一些非常小的圆圈。(5)minDist检测到的圆的中心之间的最小距离。如果参数太小,除了一个真实的相邻圆之外,还可能错误地检测到多个相邻圆。如果它太大,可能会漏掉一些圆圈。(6)param1 = 100第一个特定于方法的参数。在HOUGH_GRADIENT和HOUGH_RADIENT_ALT的情况下,它是传递到Canny边缘检测器的两个阈值中的较高阈值(较低的阈值小两倍)。注意,HOUGH_GRADIENT_ALT使用沙尔算法来计算图像导数,因此阈值通常应该更高,例如300或正常曝光和对比度图像。(7)param2 = 100第二个方法特定参数。在HOUGH_GRADIENT的情况下,它是检测阶段圆心的累加器阈值。它越小,检测到的假圆圈就越多。将首先返回与较大累加器值相对应的圆圈。在HOUGH_GRADIENT_ALT算法的情况下,这是圆的“完美度”度量。它越接近1,算法选择的圆形越好。在大多数情况下,0.9应该是可以的。如果你想更好地检测小圆圈,你可以将其降低到0.85、0.8甚至更低。但也要尝试限制搜索范围[minRadius,maxRadius],以避免出现许多假圆圈。(8)minRadius = 0最小圆半径。(9)maxRadius = 0最大圆半径。如果<=0,则使用最大图像尺寸。如果<0,则HOUGH_GRADIENT返回中心,但未找到半径。HOUGH_GRADIENT_ALT始终计算圆半径。
# include <opencv2/opencv.hpp>int main(int argc, char** argv){cv::Mat src = cv::imread("test.jpg");if(src.empty()){printf("could not load image..\n");return -1;}cv::Mat temp, gray, dst;cv::medianBlur(src, temp, 3); // 中值滤波cv::cvtColor(temp, gray, cv::COLOR_BGR2GRAY); // 灰度化// 霍夫圆检测std::vector<cv::Vec3f> pcircles;cv::HoughCircles(gray, pcircles, cv::HOUGH_GRADIENT, 1, 20, 100, 100, 1, 100);src.copyTo(dst);for(size_t i = 0; i < pcircles.size(); i++){cv::Vec3f cc = pcircles[i]; // [x, y, r]cv::circle(dst, cv::Point(cc[0], cc[1]), cc[2], cv::Scalar(0, 0, 255), 2, cv::LINE_AA); // 可视化圆弧 cv::circle(dst, cv::Point(cc[0], cc[1]), 1, cv::Scalar(255, 0, 0), 2, cv::LINE_AA); // 可视化圆心}cv::imshow("input", src);cv::imshow("output", dst);cv::waitKey(0);return 0;}