文章目录
- Opencv官方资料
- BUG集合
- 一、入门基础
- 1.1、头文件说明:#include <opencv2/opencv.hpp>
- 1.2、头文件说明:#include <opencv2/highgui/highgui.hpp>
- 1.3、计算消费时间函数
- 1.3.1、耗时:getTickCount()
- 1.3.2、频率:getTickFrequency()
- 1.3.3、实战案例
- 1.4、鼠标与轨迹条操作
- (1)鼠标事件的回调函数:cv::MouseCallback
- (2)设置鼠标事件的回调函数:cv::setMouseCallback()
- (3)创建轨迹条,并将其附加到指定的窗口:cv::createTrackbar()
- (4)获取指定轨迹条的当前位置:cv::getTrackbarPos()
- (5)设置指定轨迹条在指定窗口中的位置:cv::setTrackbarPos()
- 二、图像处理
- 2.0、实战案例:调整亮度 + 对比度 + 饱和度 + 高光 + 暖色调 + 阴影
- 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、实战案例
- 三、Mat矩阵
- 3.0、C++的数据类型+字节数+取值范围
- 3.1、Mat对象:n 维单/多通道的密集矩阵
- 3.1.1、创建 Mat 矩阵
- 3.1.2、获取像素1:img.at<uchar>(y,x)
- 3.1.3、获取像素2(防止颜色溢出):saturate_cast<uchar>(y,x)
- 3.1.4、Mat矩阵常用属性
- 3.2、基本数据类型
- 3.2.1、Point类:cv::Point()
- 3.2.2、Scalar类:cv::Scalar()
- 3.2.3、Size类:cv::Size()
- 3.2.4、Rect类:cv::Rect()
- 3.2.5、Matx类:cv::Matx()
- 1.2.6、Vec类:cv::Vec()
- 3.2.7、Range类:cv::Range()
- 3.3、随机数:cv::RNG
- 3.3.1、生成一个随机数:cv::RNG::uniform() + cv::RNG::gaussian()
- 3.3.2、获取下一个随机数:next + operator
- 3.3.3、用随机数填充矩阵:cv::RNG::fill()
Opencv官方资料
Opencv(Open Source Computer Vision)官方资料。支持在线查看 / 搜索函数,有超详细的参数说明与函数使用说明(全英文版)。
OpenCV图像增强:直方图均衡化、拉普拉斯、Log、Gamma
可参考专栏0:OpenCV C++ 问题清单
可参考专栏1:霍夫圆/直线
可参考专栏2:图像多功能滤镜
数字图像处理(2): 颜色空间/模型—— RGB, CMY/CMYK, HSI, HSV, YUV
传统目标检测
头文件highgui.hpp
可参考文献
1、HIGHGUI对图像读取与写入的处理
2、HIGHGUI对视频读取与写入的处理
BUG集合
(1)报错提示:无法打开包括文件
"math.h"
解决方案:配置属性 + 常规 + Windows SDK版本 + 最新安装的版本
(2)报错提示:未定义标识符
"CV_WINDOW_AUTOSIZE"
解决方案:添加头文件#include <opencv2/highgui/highgui.hpp>
(3)报错提示:未定义标识符
"string"
解决方案:需同时添加头文件#include <string>
、using namespace std;
c++ 学习:未声明的标识符"string"(using namespace std;)
(4)报错提示:无法找到Visual Studio 2015(v140)的生成工具
解决方案:配置属性 + 常规 + 平台工具集 + 选择当前VS版本对应的工具
无法找到Visual Studio 2015(v140)的生成工具
(5)报错提示:当前不会命中断点。源代码与原始版本不同
解决方案:复制该文件的内容后删除该文件,并在新建文件中粘贴内容,并修改新建文件名为原文件名。
当前不会命中断点。源代码与原始版本不同 (VS2012)
一、入门基础
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
1.3、计算消费时间函数
1.3.1、耗时:getTickCount()
函数说明:int64 cv::getTickCount();
函数作用:通过读取函数调用前后的时间刻度,来计算执行该函数所损耗的时间。
1.3.2、频率:getTickFrequency()
函数说明:double cv::getTickFrequency();
函数作用:将损耗时间除以该函数(频率)以进行单位转换,返回时间的刻度数单位:秒。
1.3.3、实战案例
#include <opencv2/opencv.hpp>
//using namespace cv;
//using namespace std;
int main(int argc, const char* argv[])
{
double count1 = cv::getTickCount();
//处理
//处理
//处理
double count2 = cv::getTickCount();
double time_consume = (count2 - count1) / cv::getTickFrequency();
std::cout << "耗时:" << time_consume << std::endl;
}
1.4、鼠标与轨迹条操作
【OpenCV】OpenCV基础教程(11)—— HighGUI图形用户界面
(1)鼠标事件的回调函数:cv::MouseCallback
采用回调函数来处理鼠标事件。首先创建一个回调函数,并输入回调函数触发事件以及触发位置。函数还需要被告知用户是否在触发鼠标事件的同时触发了Shift或者Alt等键。
#include <opencv2/highgui.hpp>
函数说明:typedef void(* cv::MouseCallback) (int event, int x, int y, int flags, void *userdata)
输入参数:
11、event:鼠标事件
事件名称 数值 说明
CV_EVENT_MOUSEMOVE 0 指示鼠标指针已在窗口上移动。
CV_EVENT_LBUTTONDOWN 1 表示按下了鼠标左键。
CV_EVENT_RBUTTONDOWN 2 表示按下了鼠标右键。
CV_EVENT_MBUTTONDOWN 3 表示按下了鼠标中键。
CV_EVENT_LBUTTONUP 4 表示释放了鼠标左键。
CV_EVENT_RBUTTONUP 5 表示释放了鼠标右键。
CV_EVENT_MBUTTONUP 6 表示释放了鼠标中键。
CV_EVENT_LBUTTONDBLCLK 7 表示双击鼠标左键。
CV_EVENT_RBUTTONDBLCLK 8 表示双击鼠标右键。
CV_EVENT_MBUTTONDBLCLK 9 表示双击鼠标中键。
22、(x, y):触发鼠标事件的坐标位置
33、flags:鼠标状态
标志名称 数值 说明
CV_EVENT_FLAG_LBUTTON 1 表示鼠标左键已按下。
CV_EVENT_FLAG_RBUTTON 2 表示鼠标右键已按下。
CV_EVENT_FLAG_MBUTTON 4 表示鼠标中键已按下。
CV_EVENT_FLAG_CTRLKEY 8 表示按下了Ctrl键(8~15)。
CV_EVENT_FLAG_SHIFTKEY 16 表示按下了Shift键(16~31)。
CV_EVENT_FLAG_ALTKEY 32 表示按下了Alt键(32~39)。
44、param:(可选参数)可以以任何结构方式传递额外的参数信息。
(2)设置鼠标事件的回调函数:cv::setMouseCallback()
#include <opencv2/highgui.hpp>
函数说明:void cv::setMouseCallback(const String &winname, MouseCallback onMouse, void *userdata = (void *)0)
输入参数:
winname 窗口的名称。
onMouse 鼠标事件的回调函数。
userdata 传递给回调的可选参数(默认0)。
(3)创建轨迹条,并将其附加到指定的窗口:cv::createTrackbar()
- 创建一个具有指定名称和范围的轨迹条(滑块或范围控件),分配一个变量值作为与轨迹条同步的位置,并指定在轨迹条位置更改时调用的回调函数onChange。
- 创建的轨迹条将显示在指定的窗口winname中。
#include <opencv2/highgui.hpp>
函数说明:int cv::createTrackbar( const String &trackbarname, const String &winname, int *value, int count, TrackbarCallback onChange = 0, void *userdata = 0 )
输入参数:
trackbarname 创建轨迹条的名称。
winname 作为轨迹条的父窗口的名称。
value 指向整数变量的可选指针,该变量的值反映滑块的位置。创建时,滑块位置由该变量定义。
count 滑块的最大位置。最小位置始终为0。
onChange 指向每当滑块改变位置时要调用的函数的指针(默认0)。这个函数应该被原型化为void Foo(int,void*),其中第一个参数是轨迹条位置,第二个参数是用户数据(见下一个参数)。如果回调是NULL指针,则不会调用回调,但只更新值。
userdata 按原样传递给回调的用户数据(默认0)。它可以在不使用全局变量的情况下用于处理轨迹条事件。
(4)获取指定轨迹条的当前位置:cv::getTrackbarPos()
#include <opencv2/highgui.hpp>
函数说明:int cv::getTrackbarPos(const String &trackbarname, const String &winname)
输入参数:
trackbarname 轨迹条的名称。
winname 作为轨迹条的父窗口的名称。
备注:如果轨迹条连接到控制面板,winname可以为空。
(5)设置指定轨迹条在指定窗口中的位置:cv::setTrackbarPos()
#include <opencv2/highgui.hpp>
函数说明:void cv::setTrackbarPos( const String &trackbarname, const String &winname, int pos )
输入参数:
trackbarname 轨迹条的名称。
winname 作为轨迹条的父窗口的名称。
pos 新的位置。
备注:如果轨迹条连接到控制面板,winname可以为空。
二、图像处理
2.0、实战案例:调整亮度 + 对比度 + 饱和度 + 高光 + 暖色调 + 阴影
OpenCV专栏:翟天保Steven
#include<opencv2\opencv.hpp>
//using namespace cv;
//using namespace std;
#define max2(a,b) (a>b?a:b)
#define max3(a,b,c) (a>b?max2(a,c):max2(b,c))
#define min2(a,b) (a<b?a:b)
#define min3(a,b,c) (a<b?min2(a,c):min2(b,c))
//函数申明
cv::Mat Brightness(cv::Mat src, float brightness, int contrast); //亮度+对比度。
cv::Mat Saturation(cv::Mat src, int saturation); //饱和度
cv::Mat HighLight(cv::Mat src, int highlight); //高光
cv::Mat ColorTemperature(cv::Mat src, int warm); //暖色调
cv::Mat Shadow(cv::Mat src, int shadow); //阴影
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;
}
//色彩度调整:亮度、对比度、饱和度、高光、暖色调
float brightness = 1; //[0, 10] 亮度。暗~亮:[0, 1] ~ [1, 10]
int contrast = 0; //[-100, 100] 对比度。
int saturation = 0; //[-100, 100] 饱和度。
int highlight = 0; //[-100, 100] 高光。
int warm = 0; //[-100, 100] 暖色调。
int shadow = 50; //[-100, 100] 阴影。
cv::Mat dst = src.clone();
if (brightness != 1)
dst = Brightness(dst, brightness, 0);
//cv::imshow("dst", dst);
if (contrast != 0)
dst = Brightness(dst, 1, contrast);
//cv::imshow("dst", dst);
if (saturation != 0)
dst = Saturation(dst, saturation);
//cv::imshow("dst", dst);
if (highlight != 0)
dst = HighLight(dst, highlight);
//cv::imshow("dst", dst);
if (warm != 0)
dst = ColorTemperature(dst, warm);
//cv::imshow("dst", dst);
if (shadow != 0)
dst = Shadow(dst, shadow);
//(4)显示图像
cv::imshow("src", src);
cv::imshow("dst", dst);
cv::waitKey(0); //等待用户任意按键后结束暂停功能
return 0;
}
//调整对比度与亮度
cv::Mat Brightness(cv::Mat src, float brightness, int contrast)
{
cv::Mat dst;
dst = cv::Mat::zeros(src.size(), src.type()); //新建空白模板:大小/类型与原图像一致,像素值全0。
int height = src.rows; //获取图像高度
int width = src.cols; //获取图像宽度
float alpha = brightness; //亮度(0~1为暗,1~正无穷为亮)
float beta = contrast; //对比度
cv::Mat template1;
src.convertTo(template1, CV_32F); //将CV_8UC1转换为CV32F1数据格式。
for (int row = 0; row < height; row++)
{
for (int col = 0; col < width; col++)
{
if (src.channels() == 3)
{
float b = template1.at<cv::Vec3f>(row, col)[0]; //获取通道的像素值(blue)
float g = template1.at<cv::Vec3f>(row, col)[1]; //获取通道的像素值(green)
float r = template1.at<cv::Vec3f>(row, col)[2]; //获取通道的像素值(red)
//cv::saturate_cast<uchar>(vaule):需注意,value值范围必须在0~255之间。
dst.at<cv::Vec3b>(row, col)[0] = cv::saturate_cast<uchar>(b * alpha + beta); //修改通道的像素值(blue)
dst.at<cv::Vec3b>(row, col)[1] = cv::saturate_cast<uchar>(g * alpha + beta); //修改通道的像素值(green)
dst.at<cv::Vec3b>(row, col)[2] = cv::saturate_cast<uchar>(r * alpha + beta); //修改通道的像素值(red)
}
else if (src.channels() == 1)
{
float v = src.at<uchar>(row, col); //获取通道的像素值(单)
dst.at<uchar>(row, col) = cv::saturate_cast<uchar>(v * alpha + beta); //修改通道的像素值(单)
//saturate_cast<uchar>:主要是为了防止颜色溢出操作。如果color<0,则color等于0;如果color>255,则color等于255。
}
}
}
return dst;
}
// 饱和度
cv::Mat Saturation(cv::Mat src, int saturation)
{
float Increment = saturation * 1.0f / 100;
cv::Mat temp = src.clone();
int row = src.rows;
int col = src.cols;
for (int i = 0; i < row; ++i)
{
uchar *t = temp.ptr<uchar>(i);
uchar *s = src.ptr<uchar>(i);
for (int j = 0; j < col; ++j)
{
uchar b = s[3 * j];
uchar g = s[3 * j + 1];
uchar r = s[3 * j + 2];
float max = max3(r, g, b);
float min = min3(r, g, b);
float delta, value;
float L, S, alpha;
delta = (max - min) / 255;
if (delta == 0)
continue;
value = (max + min) / 255;
L = value / 2;
if (L < 0.5)
S = delta / value;
else
S = delta / (2 - value);
if (Increment >= 0)
{
if ((Increment + S) >= 1)
alpha = S;
else
alpha = 1 - Increment;
alpha = 1 / alpha - 1;
t[3 * j + 2] =static_cast<uchar>( r + (r - L * 255) * alpha);
t[3 * j + 1] = static_cast<uchar>(g + (g - L * 255) * alpha);
t[3 * j] = static_cast<uchar>(b + (b - L * 255) * alpha);
}
else
{
alpha = Increment;
t[3 * j + 2] = static_cast<uchar>(L * 255 + (r - L * 255) * (1 + alpha));
t[3 * j + 1] = static_cast<uchar>(L * 255 + (g - L * 255) * (1 + alpha));
t[3 * j] = static_cast<uchar>(L * 255 + (b - L * 255) * (1 + alpha));
}
}
}
return temp;
}
// 高光
cv::Mat HighLight(cv::Mat src, int highlight)
{
// 生成灰度图
cv::Mat gray = cv::Mat::zeros(src.size(), CV_32FC1);
cv::Mat f = src.clone();
f.convertTo(f, CV_32FC3);
std::vector<cv::Mat> pics;
split(f, pics);
gray = 0.299f*pics[2] + 0.587*pics[2] + 0.114*pics[0];
gray = gray / 255.f;
// 确定高光区
cv::Mat thresh = cv::Mat::zeros(gray.size(), gray.type());
thresh = gray.mul(gray);
// 取平均值作为阈值
cv::Scalar t = mean(thresh);
cv::Mat mask = cv::Mat::zeros(gray.size(), CV_8UC1);
mask.setTo(255, thresh >= t[0]);
// 参数设置
int max = 4;
float bright = highlight / 100.0f / max;
float mid = 1.0f + max * bright;
// 边缘平滑过渡
cv::Mat midrate = cv::Mat::zeros(src.size(), CV_32FC1);
cv::Mat brightrate = cv::Mat::zeros(src.size(), CV_32FC1);
for (int i = 0; i < src.rows; ++i)
{
uchar *m = mask.ptr<uchar>(i);
float *th = thresh.ptr<float>(i);
float *mi = midrate.ptr<float>(i);
float *br = brightrate.ptr<float>(i);
for (int j = 0; j < src.cols; ++j)
{
if (m[j] == 255)
{
mi[j] = mid;
br[j] = bright;
}
else {
mi[j] = (mid - 1.0f) / t[0] * th[j] + 1.0f;
br[j] = (1.0f / t[0] * th[j])*bright;
}
}
}
// 高光提亮,获取结果图
cv::Mat result = cv::Mat::zeros(src.size(), src.type());
for (int i = 0; i < src.rows; ++i)
{
float *mi = midrate.ptr<float>(i);
float *br = brightrate.ptr<float>(i);
uchar *in = src.ptr<uchar>(i);
uchar *r = result.ptr<uchar>(i);
for (int j = 0; j < src.cols; ++j)
{
for (int k = 0; k < 3; ++k)
{
float temp = pow(float(in[3 * j + k]) / 255.f, 1.0f / mi[j])*(1.0 / (1 - br[j]));
if (temp > 1.0f)
temp = 1.0f;
if (temp < 0.0f)
temp = 0.0f;
uchar utemp = uchar(255*temp);
r[3 * j + k] = utemp;
}
}
}
return result;
}
// 暖色调
cv::Mat ColorTemperature(cv::Mat src, int warm)
{
cv::Mat result = src.clone();
int row = src.rows;
int col = src.cols;
int level = warm/2;
for (int i = 0; i < row; ++i)
{
uchar* a = src.ptr<uchar>(i);
uchar* r = result.ptr<uchar>(i);
for (int j = 0; j < col; ++j)
{
int R,G,B;
// R通道
R = a[j * 3 + 2];
R = R + level;
if (R > 255) {
r[j * 3 + 2] = 255;
}
else if (R < 0) {
r[j * 3 + 2] = 0;
}
else {
r[j * 3 + 2] = R;
}
// G通道
G = a[j * 3 + 1];
G = G + level;
if (G > 255) {
r[j * 3 + 1] = 255;
}
else if (G < 0) {
r[j * 3 + 1] = 0;
}
else {
r[j * 3 + 1] = G;
}
// B通道
B = a[j * 3];
B = B - level;
if (B > 255) {
r[j * 3] = 255;
}
else if (B < 0) {
r[j * 3] = 0;
}
else {
r[j * 3] = B;
}
}
}
return result;
}
// 阴影
cv::Mat Shadow(cv::Mat src, int shadow)
{
// 生成灰度图
cv::Mat gray = cv::Mat::zeros(src.size(), CV_32FC1);
cv::Mat f = src.clone();
f.convertTo(f, CV_32FC3);
std::vector<cv::Mat> pics;
split(f, pics);
gray = 0.299f*pics[2] + 0.587*pics[2] + 0.114*pics[0];
gray = gray / 255.f;
// 确定阴影区
cv::Mat thresh = cv::Mat::zeros(gray.size(), gray.type());
thresh = (1.0f - gray).mul(1.0f - gray);
// 取平均值作为阈值
cv::Scalar t = mean(thresh);
cv::Mat mask = cv::Mat::zeros(gray.size(), CV_8UC1);
mask.setTo(255, thresh >= t[0]);
// 参数设置
int max = 4;
float bright = shadow / 100.0f / max;
float mid = 1.0f + max * bright;
// 边缘平滑过渡
cv::Mat midrate = cv::Mat::zeros(src.size(), CV_32FC1);
cv::Mat brightrate = cv::Mat::zeros(src.size(), CV_32FC1);
for (int i = 0; i < src.rows; ++i)
{
uchar *m = mask.ptr<uchar>(i);
float *th = thresh.ptr<float>(i);
float *mi = midrate.ptr<float>(i);
float *br = brightrate.ptr<float>(i);
for (int j = 0; j < src.cols; ++j)
{
if (m[j] == 255)
{
mi[j] = mid;
br[j] = bright;
}
else {
mi[j] = (mid - 1.0f) / t[0] * th[j]+ 1.0f;
br[j] = (1.0f / t[0] * th[j])*bright;
}
}
}
// 阴影提亮,获取结果图
cv::Mat result = cv::Mat::zeros(src.size(), src.type());
for (int i = 0; i < src.rows; ++i)
{
float *mi = midrate.ptr<float>(i);
float *br = brightrate.ptr<float>(i);
uchar *in = src.ptr<uchar>(i);
uchar *r = result.ptr<uchar>(i);
for (int j = 0; j < src.cols; ++j)
{
for (int k = 0; k < 3; ++k)
{
float temp = pow(float(in[3 * j + k]) / 255.f, 1.0f / mi[j])*(1.0 / (1 - br[j]));
if (temp > 1.0f)
temp = 1.0f;
if (temp < 0.0f)
temp = 0.0f;
uchar utemp = uchar(255*temp);
r[3 * j + k] = utemp;
}
}
}
return result;
}
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 = 128 If 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_AUTOSIZE
WINDOW_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|ijklmno
cv::BORDER_REFLECT101 = 6 same as BORDER_REFLECT_101
cv::BORDER_DEFAULT = 7 same as BORDER_REFLECT_101
cv::BORDER_ISOLATED = 8 do not look outside of ROI
value = 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 );
输入参数:
src 8位单通道图像。
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|ijklmno
cv::BORDER_REFLECT101 = 6 same as BORDER_REFLECT_101
cv::BORDER_DEFAULT = 7 same as BORDER_REFLECT_101
cv::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|ijklmno
cv::BORDER_REFLECT101 = 6 same as BORDER_REFLECT_101
cv::BORDER_DEFAULT = 7 same as BORDER_REFLECT_101
cv::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计算。
sigmaX X方向上的高斯核标准偏差。
sigmaY = 0 Y方向上的高斯核标准偏差。如果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|ijklmno
cv::BORDER_REFLECT101 = 6 same as BORDER_REFLECT_101
cv::BORDER_DEFAULT = 7 same as BORDER_REFLECT_101
cv::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|ijklmno
cv::BORDER_REFLECT101 = 6 same as BORDER_REFLECT_101
cv::BORDER_DEFAULT = 7 same as BORDER_REFLECT_101
cv::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|ijklmno
cv::BORDER_REFLECT101 = 6 same as BORDER_REFLECT_101
cv::BORDER_DEFAULT = 7 same as BORDER_REFLECT_101
cv::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|ijklmno
cv::BORDER_REFLECT101 = 6 same as BORDER_REFLECT_101
cv::BORDER_DEFAULT = 7 same as BORDER_REFLECT_101
cv::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.cols
fy = 0 沿垂直轴的缩放比例;当它等于0时,它被计算为:(double)dsize.height/src.rows
interpolation = 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)M 2×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|ijklmno
cv::BORDER_REFLECT101 = 6 same as BORDER_REFLECT_101
cv::BORDER_DEFAULT = 7 same as BORDER_REFLECT_101
cv::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)M 3×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|ijklmno
cv::BORDER_REFLECT101 = 6 same as BORDER_REFLECT_101
cv::BORDER_DEFAULT = 7 same as BORDER_REFLECT_101
cv::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 输入图像
tx x轴平移距离
ty y轴平移距离
--------------------------------------------------*/
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|ijklmno
cv::BORDER_REFLECT101 = 6 same as BORDER_REFLECT_101
cv::BORDER_DEFAULT = 7 same as BORDER_REFLECT_101
cv::BORDER_ISOLATED = 8 do not look outside of ROI
borderValue = 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|ijklmno
cv::BORDER_REFLECT101 = 6 same as BORDER_REFLECT_101
cv::BORDER_DEFAULT = 7 same as BORDER_REFLECT_101
cv::BORDER_ISOLATED = 8 do not look outside of ROI
borderValue = 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|ijklmno
cv::BORDER_REFLECT101 = 6 same as BORDER_REFLECT_101
cv::BORDER_DEFAULT = 7 same as BORDER_REFLECT_101
cv::BORDER_ISOLATED = 8 do not look outside of ROI
borderValue = 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)=1
cv::MORPH_CROSS = 1 十字形结构元素:E(i,j)=10 if i=anchor.y or j=anchor.x else 0
cv::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/30
int y_size = binary.rows / 30; //像素的垂直长度=height/30
cv::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)。
dx x方向的求导阶数。一般为0,1,2。其中,0表示该方向没有求导。
dy y方向的求导阶数。一般为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|ijklmno
cv::BORDER_REFLECT101 = 6 same as BORDER_REFLECT_101
cv::BORDER_DEFAULT = 7 same as BORDER_REFLECT_101
cv::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)。
dx x方向的求导阶数。一般为0,1,2。其中,0表示该方向没有求导。
dy y方向的求导阶数。一般为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|ijklmno
cv::BORDER_REFLECT101 = 6 same as BORDER_REFLECT_101
cv::BORDER_DEFAULT = 7 same as BORDER_REFLECT_101
cv::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|ijklmno
cv::BORDER_REFLECT101 = 6 same as BORDER_REFLECT_101
cv::BORDER_DEFAULT = 7 same as BORDER_REFLECT_101
cv::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 );
输入参数:
image 8位输入图像。
edges 输出图像。其具有与输入图像相同大小和类型。
threshold1 第一个阈值(低阈值)。
threshold2 第二个阈值(高阈值)。
apertureSize = 3 Sobel算子孔径尺寸 。默认为3。可以是1、3、5、7
L2gradient = 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_COMPLEX
cv::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和相位谱ph
cv::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 邻域大小。
ksize slobel算子的卷积核大小。一般为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|ijklmno
cv::BORDER_REFLECT101 = 6 same as BORDER_REFLECT_101
cv::BORDER_DEFAULT = 7 same as BORDER_REFLECT_101
cv::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 = 1 distance = |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.3998
cv::DIST_WELSCH = 6 distance = c^2/2(1-exp(-(x/c)^2)), c = 2.9846
cv::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 = 0 mask=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_8UC3
markers 标记图像。数据类型必须是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);
else
watershedImage.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);
else
fY = 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);
else
fX = 7.787f * X + param_16116;
if (Z > 0.008856)
fZ = pow(Z, param_13);
else
fZ = 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;
//RGB2Lab
for (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 = -1
vector<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;
}
三、Mat矩阵
3.0、C++的数据类型+字节数+取值范围
数据类型 | 字节数 | 取值范围 |
---|---|---|
bool型(布尔型) | 1 | [0 or 1] |
BOOL型(int型) | 4 | [TRUE or FALSE] |
sbyte型(有符号8位整数) | 1 | [128 ~ 127] |
bytet型(无符号8位整数)8U | 2 | [0 ~ 255] |
short型(有符号16位整数)16S | 2 | [-32,768 ~ 32,767] |
ushort型(无符号16位整数)16U | 2 | [0 ~ 65,535] |
int型(有符号32位整数)32S | 4 | [-2,147,483 ~ 2,147,483,647] |
uint型(无符号32位整数) | 4 | [0 ~ 4,294,967,295] |
long型(64位有符号整数) | 8 | [9,223,372,036,854,775,808 ~ 9,223,372,036,854,775,807] |
ulong型(64位无符号整数) | 8 | [0 ~ 18,446,744,073,709,551,615] |
float型(32位单精度实数)32F | 4 | [3.4E+10的负38次方 ~ 3.4E+10的38次方] |
double型(64位双精度实数)64F | 8 | [1.7E+10的负308次方 ~ 1.7E+10的正308次方] |
指针 | 4 |
注意:int,float,double占多少个字节是由编译器决定的,不同的编译器,规定也不一样。ANSI标准定义int是占2个字节。
3.1、Mat对象:n 维单/多通道的密集矩阵
Mat(n-dimensional dense array class): 用于存储数值、向量、矩阵、灰度或彩色图像、体素体积、向量场、点云、张量、直方图。
Mat类:创建并访问二维、三维、四维、五维矩阵
Mat类:矩阵相乘——点乘、dot、mul运算详解
OpenCV C++使用 Mat 来存储图像数据,而 Mat 由多个像素点组成。常规图像的存储格式是8位(8bite),即一个像素点占用8个字节空间。
图像的数据格式由两个部分组成:存储格式 + 通道数。包括:CV_{8U, 16S, 16U, 32S, 32F, 64F}C{1, 2, 3, 4}。
(1)存储格式包括: 8U(unsigned char), 16S(short int), 16U(unsigned short int),
32S(signed int), 32F(float), 64F(double)
(2)通道数包括:C{1, 2, 3, 4}。
其中,通道数代表每个像素点能存放多少个像素值。如:RGB彩色图中,每个像素点都有三个像素值。
C1(单通道图像)。如:灰度图。一般图像是8位(8bite)存储格式,即CV_8UC1。
C3(3通道图像)。如:RGB彩色图
C4(4通道图像)。如:带有透明度通道的RGB图像
例如:CV_8UC3表示8位无符号整型三通道矩阵
00、假设3通道矩阵Mat=(100, 100, 3)
11、RGB图像有100 x 100 x 3=30000个像素点
22、每个像素点在内存空间所占的空间大小是8位(8bite),即CV_8;。
33、每个像素点都存放3个值。
- 提问:
在对图像的像素处理时,为什么要把读取图像时的CV_8UC3(Vec3b)转换成CV_32F(Vec3F),且处理完之后还需要转换回CV_8UC3(Vec3b)无符号整型?- 回答:
8U取值范围=[0, 255],16U=[0, 65535],32f=[3.4E+10的负38次方 ~ 3.4E+10的38次方]。
(1)对像素进行图像处理时,无需转换可以直接处理。
(2)对像素进行数学计算时,需要保持高精度,避免信息损失。
3.1.1、创建 Mat 矩阵
函数说明:cv::Mat M_temp(row, column, data_type, cv::Scalar(0, 0, 255))
输入参数:
row 矩阵的行。
column 矩阵的列。
data_type 图像的数据格式。
Scalar(0, 0, 255) 初始化RGB的像素值。表示R/G全为0,B全为255。
//
cv::Mat img1(480, 640, CV_8UC3); //新建矩阵
cv::Mat img2(img1); //复制矩阵
cv::Mat img7 = cv::Mat::zeros(rows, cols, CV_8UC3); //值全0矩阵。
cv::Mat img8 = cv::Mat::ones(rows, cols, CV_64FC1); //值全1矩阵。
cv::Mat img9 = cv::Mat::eye(rows, cols, CV_16SC2); //单位矩阵。
3.1.2、获取像素1:img.at(y,x)
(0)
CV_8UC1(uchar)
:8位无符号整型单通道矩阵。
(1)CV_8UC3(Vec3b)
:8位无符号整型三通道矩阵。
(2)CV_32FC3(Vec3F)
:32位浮点型三通道矩阵。
cv::Mat img = cv::Mat::ones(240, 320, CV_8UC1); //创建单通道Mat矩阵
cv::Mat img1 = cv::Mat::ones(480, 640, CV_8UC3); //创建多通道Mat矩阵
float elem = img.at<uchar>(10, 10); //获取(单通道)像素点的像素值:img.at<uchar>(y,x)
cv::Vec3b elem = img1.at<cv::Vec3b>(10, 10); //获取(多通道)像素点的像素值:img.at<cv::Vec3b>(y,x)
elem_B = elem[0]; //蓝色通道数值(全255)
elem_G = elem[1]; //绿色通道数值(全255)
elem_R = elem[2]; //红色通道数值(全0)
3.1.3、获取像素2(防止颜色溢出):saturate_cast(y,x)
可以解决边界溢出:若像素值大于255,则赋值255;若像素值小于0,则赋值0。
//原理如下
if (data_value < 0)
data_value = 0;
else if (data_value > 255)
data_value = 255;
#include <opencv2/opencv.hpp>
//using namespace cv;
//using namespace std;
int main(int argc, const 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 dst1, dst2;
dst1 = cv::Mat::zeros(src.size(), src.type());
dst2 = cv::Mat::zeros(src.size(), src.type());
//三个for循环:dst(i,j) =a * src(i,j) + b
for (int y = 0; y < src.rows; y++)
{
for (int x = 0; x < src.cols; x++)
{
for (int c = 0; c < 3; c++) //三个通道
{
dst1.at<cv::Vec3b>(y, x)[c] = src.at<cv::Vec3b>(y, x)[c] * 2; //不饱和滤除
dst2.at<cv::Vec3b>(y, x)[c] = cv::saturate_cast<uchar>(src.at<cv::Vec3b>(y, x)[c] * 2); //饱和滤除
}
}
}
//(4)显示图像
cv::imshow("src", src);
cv::imshow("dst1", dst1);
cv::imshow("dst2", dst2);
cv::waitKey(0);
return 0;
}
3.1.4、Mat矩阵常用属性
cv::Mat img(320, 320, CV_8UC3, cv::Scalar(255, 255, 0)); //创建320×320的3通道彩色图像。
Mat.rows 获取矩阵 行数
Mat.cols 获取矩阵 列数
Mat.dims 获取矩阵 维数(单通道是二维,多通道是三维)
Mat.channels() 获取矩阵 通道数
Mat.size(); 获取矩阵 大小
Mat.total() 获取矩阵 面积=[行数*列数](与通道数无关)
Mat.depth() 获取矩阵 存储格式(返回0~6):enum{CV_8U=0, CV_8S=1, CV_16U=2, CV_16S=3, CV_32S=4, CV_32F=5, CV_64F=6}
Mat.row(0) 获取矩阵 第一行元素组成的数组。
Mat.col(0) 获取矩阵 第一列元素组成的数组。
Mat.rowRange(0, 10) 获取矩阵 第0-10行元素组成的数组。
Mat.colRange(0, 10) 获取矩阵 第0-10列元素组成的数组。
Mat.empty(); 判断矩阵是否为空,若为空,则返回true。
Mat.clone() 复制矩阵并赋值给新矩阵。
Mat.setTo(0); 将矩阵全部设置为指定值(0)。如:src.setTo(0, src<10);当src中的某个像素值小于10,就将该值设置成0。
MatA.copyTo(MatB) 将矩阵A复制到矩阵B中。
Mat.convertTo(img, CV_32F) 将img的数据格式转换为CV_32F。
Mat.push_back() 将一个或多个元素添加到矩阵底部。其类型和列数必须与Mat矩阵中的相同。
3.2、基本数据类型
3.2.1、Point类:cv::Point()
cv::Point类由两个部分组成:cv::Point{2, 3}{b, s, i, f, d}。
(1)维度 2、3代表维度(分别代表2个点、3个点)
(2)存储类型 b 表示无符号字符
s 表示短整型
i 表示32位整型
f 表示32位浮点数
d 表示64位浮点数
Point类支持的操作:
默认构造函数: cv::Point2i p;
cv::Point3i p;
复制构造函数: cv::Point3f p2(p1);
值构造函数: cv::Point2i(x0,x1);
cv::Point3d p(x0,x1,x2);
构造固定向量类: cv::Vec3f p;
成员访问: p.x, p.y;
点乘: float x = p1.dot(p2);
双精度点乘: double x =p1.ddot(p2);
叉乘: p1.cross(p2);
3.2.2、Scalar类:cv::Scalar()
cv::Scalar本质上是一个四维向量,其可以产生任意四元素的向量,一般是double类型。
Scalar类支持的操作:
默认构造函数: cv::Scalar s;
复制构造函数: cv::Scalar s2(s1);
值构造函数: cv::Scalar s(x0);
cv::Scalar s(x0, x1, x2, x3);
返回所有元素都为标量k: cv::Scalar::all(k)
元素相乘: s1.mul(s2);
(四元数)共轭: s.conj(); //return cv::Scalar(s0,-s1,-s2,-s2)
(四元数)真值测试: s.isReal(); //return ture, if s1==s2==s3==0.
3.2.3、Size类:cv::Size()
Size类支持的操作:
默认构造函数: cv::Size sz;
cv::Size2i sz;
cv::Size2f sz;
复制构造函数: cv::Size sz2(sz1);
值构造函数: cv::Size2f sz(w,h);
成员访问: sz.width, sz.height;
计算面积: sz.area();
3.2.4、Rect类:cv::Rect()
Rect类包含:
(1)Point类的成员(x, y)。 矩形左上角
(2)Size类的成员(width, height)。 代表矩形的大小
4.1 Rect类支持的操作:
默认构造函数: cv::Rect r;
复制构造函数: cv::Rect r2(r1);
值构造函数: cv::Rect(x, y, w, h);
由起始点和大小构造: cv::Rect(p, sz);
由两个对角构造: cv::Rect(p1, p2);
成员访问: r.x, r.y, r.width, r.height;
计算面积: r.area();
提取左上角和右下角: r.tl(), r.br();
判断p点是否在矩形r内: r.contains(p);
4.2 Rect类的覆写操作:
矩形r1和矩形r2的交集: cv::Rect r3 = r1&r2; r1 &= r2;
矩形r1和矩形r2的并集: cv::Rect r3 = r1|r2; r1 |= r2;
平移矩形r x个数量: cv::Rect rx = r+x; r += x;
扩大矩形r s大小: cv::Rect rs = r+s; r += s;
比较矩形r1和矩形r2是否相等: bool eq = (r1 == r2);
比较矩形r1和矩形r2是否不相等: bool ne = (r1 != r2);
3.2.5、Matx类:cv::Matx()
固定矩阵类(Matx类):
是为了使编译时就已知矩阵的维度而设计,其内部所有数据都是在堆栈上分配的。
其是Opencv的c++接口基本类型的核心。
其继承于固定矩阵类。而其他的类:要么继承于固定向量类,要么转换成固定向量类。
其是一个模板cv::Matx<>,但独立的矩阵通常通过别名分配。别名的基础格式为cv::Matx{1,2,3,4,5,6}{1,2,3,4,5,6}{f,d}。
5.1 Matx类支持的操作:
默认构造函数: cv::Matx33f m22f;
cv::Matx43d m43d;
复制构造函数: cv::Matx22d m22d(n22d);
值构造函数: cv::Matx21f m(x0,x1);
含相同元素的矩阵: m33f = cv::Matx33f::all(x);
全零矩阵: m23d = cv::Matx23d::zeros();
元素全是1的矩阵: m16f = cv::Matx16f::ones();
单位矩阵: m33f = cv::Matx33f::eye();
均匀分布矩阵: m33f = cv::Matx33f::randu(min,max);
正态分布矩阵: m33f = cv::Matx33f::nrandu(mean,variance);
成员访问: m(i,j), m(i);
矩阵能正常的进行加减乘除:
点积: float x = m1.dot(m2);
双精度点积: double x = m1.ddot(m2);
变换操作符: m44f = (Matx44f) m44d;
提取(i, j)处的2*2子矩阵: m44f.get_minor<2,2>(i, j);
提取第i行或者j列: m14f = m44f.row(i),m41f = m44f.col(j);
提取矩阵的对角线: m41f = m44f.diag();
计算矩阵转置: n44f = m44f.t();
计算矩阵的逆: n44f = m44f.inv(method);
每个元素的乘法: m1.mul(m2);
1.2.6、Vec类:cv::Vec()
6.1 Vec类支持的操作:
默认构造函数: cv::Vec2s v2s;
复制构造函数: cv::Vec3f u3f(v3f);
值构造函数: cv::Vec2f v2f(x0,x1);
成员访问: v4f[i]; v3w(j); //[] 和()都是可以的
向量叉乘: v3f, cross(u3f);
3.2.7、Range类:cv::Range()
cv::Range类:用于确定一个连续的整数序列。
函数说明:cv::Range(int start,int end)
输入参数:
start 起始点
end 终止点
备注:左闭右开,与Python的range()类似。
举例:cv::Range rng(0, 4) 包含[0,1,2,3],但是不包含4。
3.3、随机数:cv::RNG
(1)
cv::RNG rng(int seed);
:使用随机数种子seed产生一个64位的随机整数,默认-1。计算机产生的随机数都是伪随机数,是根据种子点seed和特定算法计算出来的。所以,只要种子数一定,算法一定,则每次产生的随机数也是一定的。用系统时间做种子点(2)
cv::RNG rng((unsigned)time(NULL));
用系统时间作为种子点,需添加头文件#include <time.h>
。
3.3.1、生成一个随机数:cv::RNG::uniform() + cv::RNG::gaussian()
基于随机数的3种生成方法:
cv::RNG rng(int seed) 使用随机数种子seed产生一个64位随机整数,默认-1。
(1)cv::RNG::uniform(a, b): 返回一个[a,b)范围的均匀分布的随机数。a, b的数据类型要一致,而且必须是int、float、double中的一种,默认是int。
(2)cv::RNG::gaussian(σ): 返回一个均值为0,标准差为σ的随机数。若要产生均值为λ,标准差为σ的随机数:λ+RNG::gaussian(σ)
#include<opencv2\opencv.hpp>
#include <string>
//using namespace cv;
//using namespace std;
int main(int argc,char* argv[])
{
cv::RNG rng(-1);
int randomNumber1 = rng;
double randomNumber2 = rng.uniform(0,99);
double randomNumber3 = rng.gaussian(2);
std::cout << "randomNumber1=" << randomNumber1 << std::endl; //randomNumber=130063606
std::cout << "randomNumber2=" << randomNumber2 << std::endl; //randomNumber=14
std::cout << "randomNumber3=" << randomNumber3 << std::endl; //randomNumber=-1.40186
return 0;
}
3.3.2、获取下一个随机数:next + operator
(1) cv::RNG:: next 返回下一个64位随机整数。
(2) cv::RNG:: operator 返回下一个指定类型的随机数。
#include<opencv2\opencv.hpp>
#include <string>
//using namespace cv;
//using namespace std;
int main(int argc,char* argv[])
{
cv::RNG rng(-1);
int randomNumber = rng.next(); //返回下一个随机整数,即N1.next();
//返回下一个指定类型的随机数
int randomNumber1 = rng.operator uchar(); //返回下一个无符号字符数
int randomNumber2 = rng.operator schar(); //返回下一个有符号字符数
int randomNumber3 = rng.operator ushort(); //返回下一个无符号短型
int randomNumber4 = rng.operator short int(); //返回下一个短整型数
int randomNumber5 = rng.operator int(); //返回下一个整型数
int randomNumber6 = rng.operator unsigned int(); //返回下一个无符号整型数
int randomNumber7 = rng.operator float(); //返回下一个浮点数
int randomNumber8 = rng.operator double(); //返回下一个double型数
int randomNumber9 = rng.operator ()(); //和rng.next( )等价
int randomNumber10 = rng.operator ()(100); //返回[0,100)范围内的随机数
std::cout << "randomNumber=" << randomNumber << std::endl; //randomNumber=130063605
std::cout << "randomNumber1=" << randomNumber1 << std::endl; //randomNumber=156
std::cout << "randomNumber2=" << randomNumber2 << std::endl; //randomNumber=-116
std::cout << "randomNumber3=" << randomNumber3 << std::endl; //randomNumber=24389
std::cout << "randomNumber4=" << randomNumber4 << std::endl; //randomNumber=31943
std::cout << "randomNumber5=" << randomNumber5 << std::endl; //randomNumber=-1348951784
std::cout << "randomNumber6=" << randomNumber6 << std::endl; //randomNumber=94037301
std::cout << "randomNumber7=" << randomNumber7 << std::endl; //randomNumber=0
std::cout << "randomNumber8=" << randomNumber8 << std::endl; //randomNumber=0
std::cout << "randomNumber9=" << randomNumber9 << std::endl; //randomNumber=776868985
std::cout << "randomNumber10=" << randomNumber10 << std::endl; //randomNumber=94
return 0;
}
3.3.3、用随机数填充矩阵:cv::RNG::fill()
函数说明:void fill( InputOutputArray mat, int distType, InputArray a, InputArray b, bool saturateRange=false );
输入参数:
(1)mat 输入矩阵。2D或N维矩阵,最多支持4通道,超过4通道先用reshape()改变结构。
(2)distType 分布类型。
cv::RNG::UNIFORM 均匀分布。
cv::RNG::NORMAL 高斯分布。
(3)a 第一分布参数。均匀分布时表示一个下边界(闭区间),正态分布时表示平均值。
(4)b 第二分布参数。均匀分布时表示一个上边界(开区间),正态分布时表示标准差。
(5)saturateRange=false 只针对均匀分布有效。当为真的时候,会先把产生随机数的范围变换到数据类型的范围,再产生随机数。如果为假,会先产生随机数,再进行截断到数据类型的有效区间。
#include<opencv2\opencv.hpp>
#include <string>
//using namespace cv;
//using namespace std;
int main(int argc,char* argv[])
{
cv::RNG rng(-1);
cv::Mat_<int>fillM1(3, 3); //新建3x3矩阵,int类型
cv::Mat_<double>fillM2(3, 3); //新建3x3矩阵,double类型
rng.fill(fillM1, cv::RNG::UNIFORM, 1, 100); //随机生成[1,100)均匀分布的int数,并填充fillM。
rng.fill(fillM2, cv::RNG::NORMAL, 1, 3); //随机生成均值为1,标准差为3的double数,并填fillN。
std::cout << "filM = " << fillM1 << std::endl;
std::cout << "filN = " << fillM2 << std::endl;
return 0;
}