OpenCV学习文档¶
OpenCV常用操作¶
获取执行时间¶
getTickCount():用于返回从操作系统启动到当前所经的计时周期数
getTickFrequency():用于返回CPU的频率。get Tick Frequency。这里的单位是秒,也就是一秒内重复的次数。
double t = getTickCount();
Mat kernal = (Mat_<char>(3, 3) << 0, -1, 0,
-1, 5, -1,
0, -1, 0);
filter2D(src, dst, src.depth(), kernal);
double timecousume = (getTickCount() - t) / getTickFrequency();
printf("time consume %.3f", timecousume);
这里是对矩阵掩膜操作的一个时间统计,其中src和dst都是之前定义用Mat对象定义的。
对图像的基本操作¶
加载、修改和保存图像¶
加载图像(cv::imread)¶
imread功能是加载图像文件成为一个Mat对象,其中第一个参数表示图像文件名称
第二个参数,表示加载的图像是什么类型,支持常见的三个参数值:
- IMREAD_UNCHANGED (<0) 表示加载原图,不做任何改变
- IMREAD_GRAYSCALE ( 0)表示把原图作为灰度图像加载进来
- IMREAD_COLOR (>0) 表示把原图作为RGB图像加载进来
注意:OpenCV支持JPG、PNG、TIFF等常见格式图像文件加载
显示图像(cv::namedWindos 与cv::imshow)¶
namedWindos功能是创建一个OpenCV窗口,它是由OpenCV自动创建与释放,你无需取销毁它
常见用法namedWindow(“Window Title”, WINDOW_AUTOSIZE)
- WINDOW_AUTOSIZE会自动根据图像大小,显示窗口大小,不能人为改变窗口大小
- WINDOW_NORMAL,跟QT集成的时候会使用,允许修改窗口大小
imshow根据窗口名称显示图像到指定的窗口上去,第一个参数是窗口名称,第二参数是Mat对象
修改图像(cv::cvtColor)¶
cvtColor的功能是把图像从一个彩色空间转换到另外一个色彩空间,有三个参数,第一个参数表示源图像、第二参数表示色彩空间转换之后的图像、第三个参数表示源和目标色彩空间如:COLOR_BGR2HLS 、COLOR_BGR2GRAY等
cvtColor( image, gray_image, COLOR_BGR2GRAY );
保存图像(cv::imwrite)¶
保存图像文件到指定目录路径
只有8位、16位的PNG、JPG、Tiff文件格式而且是单通道或者三通道的BGR的图像才可以通过这种方式保存
保存PNG格式的时候可以保存透明通道的图片
可以指定压缩参数
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
int main(int argc, char** argv) {
Mat src = imread("D:/1.jpg");
if (src.empty()) {
printf("Could not load image...\n");
return -1;
}
namedWindow("test opencv setup", CV_WINDOW_AUTOSIZE);
imshow("test opencv setup", src);
namedWindow("output windows", CV_WINDOW_AUTOSIZE); //自动形式图像大小,而且不可改变
Mat output_image;
cvtColor(src, output_image, CV_BGR2HLS);
imshow("output windows", output_image);
imwrite("D:/2.png", output_image);
waitKey(0);
return 0;
}
Mat对象¶
Mat对象OpenCV2.0之后引进的图像数据结构、自动分配内存、不存在内存泄漏的问题,是面向对象的数据结构。Mat的主要作用是操作图像和矩阵。Mat对象用来存储图像矩阵的各种信息。(大小、值、数字通道等)
cv::Mat分为两个部分,头部与数据部分。头部包含了矩阵的所有相关信息(大小、通道数量、数据类型等)。头部有一个指向数据块的指针,即data属性,也就是我们有明确要求的时候,内存块才会被复制。实际上,大多数操作仅仅复制了cv::Mat的头部,因此多个对象会指向同一个数据块。这种内存管理模式可以提高应用程序运行效率,避免内存泄露。
数据块包含了图像中所有像素的值。
Mat对象构造函数与常用方法¶
常用方法¶
- Mat clone():完全拷贝 首先src通过imread读取一张图像,接下来:
Mat dst = src.clone();
imshow("output", dst);
就会完全克隆一张一模一样的图像。
- void copyTo(Mat mat):完全拷贝
Mat dst;
src.copyTo(dst);
imshow("output", dst);
部分复制一般情况下只会复制Mat对象的头和指针部分,不会复制数据部分。比如下面情况:
Mat A= imread(imgFilePath);
Mat B(A);
完全复制如果想把Mat对象的头部和数据部分一起复制,可以通过上面两个API实现,即
Mat F = A.clone(); 或 Mat G; A.copyTo(G);
- void convertTo(Mat dst, int type):用于进行数据类型转换
如把CV_8UC1转换到CV32F1实现如下:
src.convertTo(dst, CV_32F);
- int channels():查看图像通道情况
Mat dst;
cvtColor(src, dst, CV_BGR2GRAY);
printf("input image channels : %d\n", src.channels());
printf("output image channels : %d", dst.channels());
imshow("output", dst);
可以发现变成灰度图像之后输出通道数变为1
- uchar* ptr(i=0):获取图片像素具体值,i表示行数
Mat dst;
cvtColor(src, dst, CV_BGR2GRAY);
const uchar *firstRow = dst.ptr<uchar>(0);
printf("first pixel value : %d", *firstRow);
imshow("output", dst);
这样我们就可以获取第一行第一个像素的灰度值信息了。
- .cols, .rows:获取行数和列数
Mat dst;
cvtColor(src, dst, CV_BGR2GRAY);
int cols = dst.cols;
int rows = dst.rows;
printf("rows = %d cols = %d", rows, cols);
imshow("output", dst);
构造函数继续举例(Mat对象创建)¶
如果是三个通道:
Mat M(3, 3, CV_8UC3, Scalar(0, 0, 255)); //scale要和通道数目一致
cout << "M:" << endl << M << endl;
其中前两个参数分别表示行(row)跟列(column)、第三个CV_8UC3中的8表示每个通道占8位、U表示无符号、C表示Char类型、3表示通道数目是3,第四个参数是向量表示初始化每个像素值是多少,向量长度对应通道数目一致
Scalar给每一个通道赋一个值,第一个和第二个通道的值全部都是0,第三个通道的值是255。
[ 0, 0, 255, 0, 0, 255, 0, 0, 255;
0, 0, 255, 0, 0, 255, 0, 0, 255;
0, 0, 255, 0, 0, 255, 0, 0, 255]
如果变成了一个通道:
Mat M(100, 100, CV_8UC1, Scalar(127)); //scale要和通道数目一致
creat创建对象¶
Mat m1;
m1.create(src.size(), src.type());
m1 = Scalar(0, 0, 255);
imshow("output", m1);
零初始化¶
Mat m2 = Mat::eye(2, 2, CV_8UC1);
cout << "m2 = " << endl << m2 << endl;
对像素的操作¶
读写像素¶
读写单通道像素¶
- 读一个GRAY像素点的像素值(CV_8UC1)
Scalar intensity = img.at<uchar>(y, x);
//或者
Scalar intensity = img.at<uchar>(Point(x, y));
具体代码:
//单通道
Mat gray_src;
cvtColor(src, gray_src, CV_BGR2GRAY);
int height = gray_src.rows;
int width = gray_src.cols;
for (int row = 0; row < height; row++) {
for (int col = 0; col < width; col++) {
int gray = gray_src.at<uchar>(row, col);
gray_src.at<uchar>(row, col) = 255 - gray;
}
}
imshow("output", gray_src);
这个代码实现每个像素的像素值得翻转操作。
读写三通道像素¶
Vec3f intensity = img.at<Vec3f>(y, x);
float blue = intensity.val[0];
float green = intensity.val[1];
float red = intensity.val[2];
Vec3b对应三通道的顺序是blue、green、red的uchar类型数据。
Vec3f对应三通道的float类型数据
具体代码如下:
//三通道
Mat dst;
dst.create(src.size(), src.type());
int height = src.rows;
int width = src.cols;
int nc = src.channels();
for (int row = 0; row < height; row++) {
for (int col = 0; col < width; col++) {
if (nc == 1) //如果是单通道,按照原先的方式进行处理
{
int gray = dst.at<uchar>(row, col);
dst.at<uchar>(row, col) = 255 - gray;
}
else if (nc == 3) //如果是三通道
{
int b = dst.at<Vec3b>(row, col)[0];
int g = dst.at<Vec3b>(row, col)[1];
int r = dst.at<Vec3b>(row, col)[2];
dst.at<Vec3b>(row, col)[0] = 255 - b;
dst.at<Vec3b>(row, col)[1] = 255 - g;
dst.at<Vec3b>(row, col)[2] = 255 - r;
}
}
}
当然,有更为简单的操作,用于实现图像像素255 - pixel
//三通道
Mat dst;
dst.create(src.size(), src.type());
bitwise_not(src, dst);
bitwise是位操作,not是非操作。也就是1变成0,0变成1。
处理图像的常用方法¶
矩阵掩膜操作¶
所谓掩膜其实就是一个矩阵,然后根据这个矩阵重新计算图片中像素的值。掩膜(mask也被称为kernel)
image
这里,我们用掩膜来提高图像对比度。用到的掩膜是:
image
红色是中心像素,从上到下,从左到右对每个像素做同样的处理操作,得到最终结果就是对比度提高之后的输出图像Mat对象。
使用方法如下:
定义掩膜:Mat kernel = (Mat_
filter2D( src, dst, src.depth(), kernel );其中src与dst是Mat类型变量、src.depth表示位图深度,有32、24、8等。
#include <opencv2/opencv.hpp>
#include <iostream>
#include <math.h>
using namespace cv;
using namespace std;
int main(int argc, char** argv) {
Mat src;
// src = imread("D:/1.jpg");
src = imread("D:/WireRope/change.jpg");
if (!src.data) {
printf("could not load image...\n");
return -1;
}
namedWindow("input image", CV_WINDOW_AUTOSIZE);
imshow("input image", src);
Mat dst;
dst = Mat(src.size(), src.type());
Mat kernal = (Mat_<char>(3, 3) << 0, -1, 0,
-1, 5, -1,
0, -1, 0);
filter2D(src, dst, src.depth(), kernal);
imshow("contract image demo", dst);
imwrite("D:/WireRope/contrast_change.jpg", dst);
waitKey(0);
return 0;
}
用这种方法就提高了图像的对比度。我们给filter2D什么样的掩膜,他就会替你执行什么样的操作。
像素范围处理saturate_cast¶
- saturate_cast
(-100),返回 0。 - saturate_cast
(288),返回255 - saturate_cast
(100),返回100
这个函数的功能是确保RGB值得范围在0~255之间
基本阈值操作¶
- 阈值二值化(threshold binary)
蓝色表示阈值线,红色表示像素的分布情况。需要注意的是这里的阈值操作以及接下来的阈值操作对应的都是灰度图像而言的。
image
- 阈值反二值化(threshold binary Inverted)
image
- 截断 (truncate)
image
- 阈值取零 (threshold to zero)
image
- 阈值反取零 (threshold to zero inverted)
image
这里给出指令名称:
image
要知道的是,这五个值应该被宏定义过,实际上其背后分别是数字0-4,所以写成数字0-4也是可以的。
下面是阈值二值化的代码:
#include <opencv2/opencv.hpp>
#include <iostream>
#include <math.h>
using namespace cv;
Mat src, dst, gray_src;
int threshold_value = 127; //像素值是0-255,取中间值就是127
int threshold_max = 255;
const char* output_title = "binary image";
void Threshold_Demo(int, void*);
int main(int argc, char** argv) {
src = imread("D:/2.jpg");
if (!src.data) {
printf("could not load image...\n");
return -1;
}
namedWindow("input image", CV_WINDOW_AUTOSIZE);
namedWindow(output_title, CV_WINDOW_AUTOSIZE);
imshow("input image", src);
createTrackbar("Threshold Value", output_title, &threshold_value, threshold_max, Threshold_Demo); //创建一个拖动条
Threshold_Demo(0, 0);
waitKey(0);
return 0;
}
void Threshold_Demo(int, void*) {
cvtColor(src, gray_src, CV_BGR2GRAY);
threshold(gray_src, dst, threshold_value, threshold_max, THRESH_TOZERO_INV); //更改这里的值,就可以实现5种阈值操作转换
imshow(output_title, dst);
}
下面是5个值一起创建拖动条的效果:
#include <opencv2/opencv.hpp>
#include <iostream>
#include <math.h>
using namespace cv;
Mat src, dst, gray_src;
int threshold_value = 127; //像素值是0-255,取中间值就是127
int threshold_max = 255;
int type_value = 2;
int type_max = 4; //定义5种操作,5种操作所对应的实际值分别是0-4
const char* output_title = "binary image";
void Threshold_Demo(int, void*);
int main(int argc, char** argv) {
src = imread("D:/test.jpg");
if (!src.data) {
printf("could not load image...\n");
return -1;
}
namedWindow("input image", CV_WINDOW_AUTOSIZE);
namedWindow(output_title, CV_WINDOW_AUTOSIZE);
imshow("input image", src);
createTrackbar("Threshold Value", output_title, &threshold_value, threshold_max, Threshold_Demo); //创建一个拖动条
createTrackbar("Type Value", output_title, &type_value, type_max, Threshold_Demo); //创建一个拖动条
Threshold_Demo(0, 0);
waitKey(0);
return 0;
}
void Threshold_Demo(int, void*) {
cvtColor(src, gray_src, CV_BGR2GRAY);
threshold(gray_src, dst, threshold_value, threshold_max, type_value); //更改这里的值,就可以实现5种阈值操作转换
imshow(output_title, dst);
}
image
- 自动计算阈值
实现方法是THRESH_OTSU和THRESH_TRIANGLE
这两种方法可以用于自动计算阈值,背后都有一些对应的数学原理。
threshold(gray_src, dst, threshold_value, threshold_max, THRESH_OTSU | type_value); //更改这里的值,就可以实现5种阈值操作转换
更改这里就可以实现自动求阈值。
图像滤波¶
自定义线性滤波¶
Robert算子¶
//Robert算子在x方向
Mat kernal_x = (Mat_<int>(2, 2) << 1, 0, 0, -1);
filter2D(src, dst, src.depth(), kernal_x, Point(-1, -1), 0.0);
imshow("output image", dst);
//Robert算子在y方向
Mat kernal_y = (Mat_<int>(2, 2) << 0, 1, -1, 0);
filter2D(src, dst, src.depth(), kernal_y, Point(-1, -1), 0.0);
imshow("output image", dst);
Sober算子¶
// Sobel算子在x方向
Mat kernal_x = (Mat_<int>(3, 3) << -1, 0, 1, -2, 0, 2, -1, 0, 1);
filter2D(src, dst, src.depth(), kernal_x, Point(-1, -1), 0.0);
// Sobel算子在y方向
Mat kernal_y = (Mat_<int>(3, 3) << -1, -2, -1, 0, 0, 0, 1, 2, 1);
filter2D(src, dst, src.depth(), kernal_y, Point(-1, -1), 0.0);
拉普拉斯算子¶
Mat kernal = (Mat_<int>(3, 3) << 0, -1, 0, -1, 4, -1, 0, -1, 0);
filter2D(src, dst, src.depth(), kernal, Point(-1, -1), 0.0);
自定义卷积filter2D¶
filter2D(
Mat src, //输入图像
Mat dst, // 模糊图像
int depth, // 图像深度32/8,如果不知道一般都写默认最大深度
Mat kernel, // 卷积核/模板,输入的卷积核大小一般都是基数3、5、7、9等等
Point anchor, // 锚点位置,锚点直接写(-1,-1)就会自动帮你找到锚点的中心位置
double delta // 计算出来的像素+delta
)
其中 kernal是可以自定义的卷积核
处理边缘¶
卷积边界问题及其处理¶
边界问题¶
image
卷积边界问题是指的图像卷积的时候边界像素,不能被卷积操作,原因在于边界像素没有完全跟kernel重叠,所以当3x3滤波时候有1个像素(最上面一行的像素)的边缘没有被处理,5x5滤波的时候有2个像素的边缘没有被处理。
处理¶
在卷积开始之前增加边缘像素,填充的像素值为0或者RGB黑色,比如3x3在四周各填充1个像素的边缘,这样就确保图像的边缘被处理,在卷积处理之后再去掉这些边缘。openCV中默认的处理方法是: BORDER_DEFAULT
,此外常用的还有如下几种:
- BORDER_CONSTANT:用指定像素填充边缘
- BORDER_REPLICATE:用已知边缘像素值来填充边缘像素值
- BORDER_WRAP:用另外一边的像素来补偿填充
下面是给图像自定义添加边缘
copyMakeBorder:给图像添加边缘API
copyMakeBorder(
Mat src, // 输入图像
Mat dst, // 添加边缘图像
int top, // 边缘长度,一般上下左右都取相同值,
int bottom,
int left,
int right,
int borderType // 边缘类型
Scalar value // Scalar用于指定颜色,边缘类型为 BORDER_CONSTANT 时,有效
)
下面代码展示一下如何具体使用:
int top = (int)0.05*src.rows;
int bottom = (int)0.05*src.rows;
int left = (int)0.05*src.cols;
int right = (int)0.05*src.cols;
Scalar color = Scalar(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255));
copyMakeBorder(src, dst, top, bottom, left, right, borderType, color);
imshow(OUTPUT_WIN, dst);
这是完整演示四种方法如何切换的代码:
#include <opencv2/opencv.hpp>
#include <iostream>
#include <math.h>
using namespace cv;
Mat src, dst, kernal;
int main(int argc, char** argv) {
src = imread("D:/1.jpg");
if (!src.data) {
printf("could not load image...\n");
return -1;
}
char INPUT_WIN[] = "input image";
char OUTPUT_WIN[] = "result image";
namedWindow(INPUT_WIN, CV_WINDOW_AUTOSIZE);
namedWindow(OUTPUT_WIN, CV_WINDOW_AUTOSIZE);
imshow("input image", src);
int top = 0.05*src.rows;
int bottom = 0.05*src.rows;
int left = 0.05*src.cols;
int right = 0.05*src.cols;
RNG rng(12345); //生成随机数
int borderType = BORDER_DEFAULT;
int c = 0;
while (true)
{
c = waitKey(500);
if ((char)c ==27 ) //按下键盘ESC对应的数值就是27,也就是按下键盘推出while循环
{
break;
}
if ((char)c == 'r')
{
borderType = BORDER_REPLICATE;
}
else if ((char)c == 'w')
{
borderType = BORDER_WRAP;
}
else if ((char)c == 'c')
{
borderType = BORDER_CONSTANT;
}
else if((char)c == 'd')
{
borderType = BORDER_DEFAULT;
}
Scalar color = Scalar(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255)); //生成0-255之前随机颜色值
copyMakeBorder(src, dst, top, bottom, left, right, borderType, color);
imshow(OUTPUT_WIN, dst);
}
return 0;
}
Sobel算子¶
卷积的应用:图像边缘提取¶
边缘是图像像素发生显著跃迁的地方,通过求一阶导数可以很好地捕捉边缘。
delta = f(x) – f(x-1), delta越大,说明像素在X方向变化越大,边缘信号越强。
image
Sobel算子¶
Sobel算子又被称为一阶微分算子,求导算子,在水平和垂直两个方向上求导,得到图像X方法与Y方向梯度图像。它是离散微分算子(discrete differentiation operator),用来计算灰度图像的近似梯度。
Soble算子功能集合高斯平滑和微分求导。
image
我们以水平梯度为例。他的水平方向上面变化十分的明显,在水平方向上给不同的权重,通过权重值来扩大差异。
image
最终图像梯度如上图所示,一般为了让计算机算的更快一些,我们会取绝对值的形式。
Sobel算子API¶
cv::Sobel (
InputArray Src // 输入图像
OutputArray dst// 输出图像,大小与输入图像一致
int depth // 输出图像深度.
Int dx. // X方向,几阶导数,如果想求x方向的时候就让这个数取1,y方向上取0
int dy // Y方向,几阶导数.
int ksize, SOBEL算子kernel大小,必须是1、3、5、7、
double scale = 1
double delta = 0
int borderType = BORDER_DEFAULT
)
image
这里关于深度说一下,因为考虑两个图像像素之间的差值,可能做差之后超过255,超过255的8U灰度图像就会被截断,所以相比于输入,输出会上升一个层次。(-1就是表示选择和原先的一样)
Sobel算子改进版:Scharr
image
cv::Scharr (
InputArray Src // 输入图像
OutputArray dst// 输出图像,大小与输入图像一致
int depth // 输出图像深度.
Int dx. // X方向,几阶导数
int dy // Y方向,几阶导数.
double scale = 1
double delta = 0
int borderType = BORDER_DEFAULT
)
处理流程:
- GaussianBlur( src, dst, Size(3,3), 0, 0, BORDER_DEFAULT );
- cvtColor( src, gray, COLOR_RGB2GRAY );
- addWeighted( A, 0.5,B, 0.5, 0, AB);
- convertScaleAbs(A, B)// 计算图像A的像素绝对值,输出到图像B
Sobel实现代码如下,Scharr代码一样。
#include <opencv2/opencv.hpp>
#include <iostream>
#include <math.h>
using namespace cv;
Mat src, dst;
int main(int argc, char** argv) {
src = imread("D:/1.jpg");
if (!src.data) {
printf("could not load image...\n");
return -1;
}
imshow("input image", src);
GaussianBlur(src, dst, Size(3, 3), 0, 0);
Mat gray_src;
cvtColor(src, gray_src, CV_BGR2GRAY);
imshow("gray image", gray_src);
Mat xgrad, ygrad;
Sobel(gray_src, xgrad, CV_16S, 1, 0, 3); //我们这里对于CV_8U的输入图像,向上取一个数量级,使得不会发生超过255被截断的事情发生
Sobel(gray_src, ygrad, CV_16S, 0, 1, 3);
convertScaleAbs(xgrad, xgrad); //计算的时候也可能出现负数,负数的话因为不是0-255之间,会被强制变成0,这不是我们想要的,我们这样强制把他们变成正的
convertScaleAbs(ygrad, ygrad);
imshow("xgrad", xgrad);
imshow("ygrad", ygrad);
Mat xygrad = Mat(xgrad.size(), xgrad.type());
/* 注释的代码是不使用函数求xgrad和ygrad的合起来的值 */
//int width = xgrad.cols;
//int height = ygrad.rows;
//for (int row = 0; row < height; row++)
//{
// for (int col = 0; col < width; col++)
// {
// int xg = xgrad.at<uchar>(row, col);
// int yg = ygrad.at<uchar>(row, col);
// int xy = xg + yg;
// xygrad.at<uchar>(row, col) = saturate_cast<uchar>(xy);
// }
//}
addWeighted(xgrad, 0.5, ygrad, 0.5, 0, xygrad);
imshow("Final result", xygrad);
waitKey(0);
return 0;
}
Laplacian算子¶
image
在二阶导数的时候,最大变化处的值为零即边缘是零值。通过二阶导数计算,依据此理论我们可以计算图像二阶导数,提取边缘。
cv::Laplacian¶
Laplacian(
InputArray src,
OutputArray dst,
int depth, //深度CV_16S
int kisze, // 3
double scale = 1,
double delta =0.0,
int borderType = 4
)
处理流程是
- 高斯模糊 – 去噪声GaussianBlur()
- 转换为灰度图像cvtColor()
- 拉普拉斯 – 二阶导数计算Laplacian()
- 取绝对值convertScaleAbs()
- 显示结果
这里再说一下取绝对值的意义,不管算的值是负的还是正的,都代表是的图像之间的差异,不能因为是负的数就直接删掉不管了。所以我们需要取绝对值来保留这份差异。
具体图像处理代码:
#include <opencv2/opencv.hpp>
#include <iostream>
#include <math.h>
using namespace cv;
Mat src, dst;
int main(int argc, char** argv) {
src = imread("D:/1.jpg");
if (!src.data) {
printf("could not load image...\n");
return -1;
}
namedWindow("input image", CV_WINDOW_AUTOSIZE);
imshow("input image", src);
Mat gray_src, edge_image;
GaussianBlur(src, dst, Size(3, 3), 0, 0);
cvtColor(dst, gray_src, CV_BGR2GRAY);
Laplacian(gray_src, edge_image, CV_16S, 3);
convertScaleAbs(edge_image, edge_image);
threshold(edge_image, edge_image, 0, 255, THRESH_OTSU | THRESH_BINARY); //用Otsu算法获取最优二值化的值进行图像二值化处理
imshow("output image", edge_image);
waitKey(0);
return 0;
}
Canny算法¶
Canny是边缘检测算法,在1986年提出的。是一个十分常用和实用的边缘检测算法。
算法流程¶
算法大致流程:
- 高斯模糊 - GaussianBlur
- 灰度转换 - cvtColor
- 计算梯度 – Sobel/Scharr
- 非最大信号抑制
- 高低阈值输出二值图像
这里说一下高斯模糊的作用,高斯模糊的主要作用就是降噪。防止异常值影响最终结果。
非最大信号抑制是关于边缘我们只能有一个像素一个值,关于非最大值要进行一定抑制,来突出最大边缘。非最大信号抑制具体来说就是对于该方向上的点,如果不是最大信号,我们就把它去掉。
高低阈值连接是非最大信号抑制之后的图像都是一些像素点,需要把他们连接成线。这里如果大于最高阈值的像素,我们都要把他们保留下来,小于最大阈值的全部舍弃。然后介于最大阈值和最小阈值之间的我们会对其进行一个阈值连接。边缘连接之后就得到一个二值图像然后把他们输出。
大概这是完整的使用canny算法的流程。
image
如图所示图片中的左侧是Sobel算子,$\theta$表示的是梯度的变化情况,看哪个方向上梯度变化更大,以此来确定角度。右图所示的就是角度区间。在每一个扇区,我们会对当前的像素和上下两个像素进行比较,如果当前的像素小于上下两个像素,那么上下两个像素保留,当前的像素舍弃,如果当前像素大于上下两个像素,那么上下两个像素被舍弃,当前像素保留。(我们只在每个扇区选择与他相近的两个像素)
高低阈值的选取¶
什么样的阈值是好的阈值,高低阈值到底该怎么选取呢?在实际编程中T1,T2为阈值,凡是高于T2的都保留,凡是小于T1都丢弃,从高于T2的像素出发,凡是大于T1而且相互连接的,都保留。最终得到一个输出二值图像。
推荐的高低阈值比值为 T2: T1 = 3:1/2:1 其中T2为高阈值,T1为低阈值。
cv::Canny¶
Canny(
InputArray src, // 8-bit的输入图像,不支持彩色图像,一定要提前转为灰度
OutputArray edges,// 输出边缘图像, 一般都是二值图像,背景是黑色
double threshold1,// 低阈值,常取高阈值的1/2或者1/3
double threshold2,// 高阈值
int aptertureSize,// Soble算子的size,通常3x3,取值3
bool L2gradient // 选择 true表示是L2来归一化,否则用L1归一化(L2是二范数,L1是一范数)
)
关于归一化,一般情况下为了计算速度,通常选择L1归一化。所以参数设置为false。
完整代码实现如下:
#include <opencv2/opencv.hpp>
#include <iostream>
#include <math.h>
using namespace cv;
Mat src, dst, gray_src;
int t1_value = 50;
int max_value = 255;
void Canny_Demo(int, void*);
int main(int argc, char** argv) {
src = imread("D:/1.jpg");
if (!src.data) {
printf("could not load image...\n");
return -1;
}
namedWindow("input image", CV_WINDOW_AUTOSIZE);
namedWindow("output image", CV_WINDOW_AUTOSIZE);
imshow("input image", src);
cvtColor(src, gray_src, CV_BGR2GRAY);
createTrackbar("Threshold Value:", "output image", &t1_value, max_value, Canny_Demo); //创建一个拖动条,触发拖动条的回调函数为Canny_Demo
Canny_Demo(0, 0);
waitKey(0);
return 0;
}
void Canny_Demo(int, void*) {
Mat edge_output;
blur(gray_src, gray_src, Size(3, 3), Point(-1, -1), BORDER_DEFAULT);
Canny(gray_src, edge_output, t1_value, t1_value * 2, 3, false);
/* 注释掉部分是用彩色图像显示canny算子,如果不加的话就是用黑白像素来显示canny算子处理结果,如果用彩色像素的话,处理速度会更慢一些
dst.create(src.size(), src.type);
src.copyTo(dst, edge_output);
imshow("output image", dst);
*/
imshow("output image", edge_output);
}
这样处理的图片最后是黑底,白色的边:
image
如果翻转过来,改成白边黑底可能看起来效果会更好,我们只需要更改这个操作
imshow("output image", ~edge_output); //~表示取反,像素取反就可以变成白底黑边了
最后说一下,影响Canny算法的主要成像因素是低阈值和高阈值之间的选择。
模板匹配¶
模板匹配介绍¶

- 模板匹配就是在整个图像区域发现与给定子图像匹配的小块区域。
- 所以模板匹配首先需要一个模板图像T(给定的子图像)
- 另外需要一个待检测的图像-源图像S
- 工作方法,在带检测图像上,从左到右,从上向下计算模板图像与重叠子图像的匹配度,匹配程度越大,两者相同的可能性越大。
API介绍¶
matchTemplate(
InputArray image,// 源图像,必须是8-bit或者32-bit浮点数图像
InputArray templ,// 模板图像,类型与输入图像一致
OutputArray result,// 输出结果,必须是单通道32位浮点数,假设源图像WxH,模板图像wxh,
则结果必须为W-w+1, H-h+1的大小。(w是宽,h是高)
int method,//使用的匹配方法,一般推荐使用归一化的方法
InputArray mask=noArray()//(optional)
)
image
代码演示¶
#include <opencv2/opencv.hpp>
#include <iostream>
#include <math.h>
using namespace cv;
Mat src, temp, dst;
int match_method = CV_TM_SQDIFF;
int max_track = 5;
void Match_Demo(int, void*);
int main(int argc, char** argv) {
src = imread("D:/temp/6.bmp");
temp = imread("D:/temp/temp.png");
if (!src.data || !temp.data) {
printf("could not load image...\n");
return -1;
}
namedWindow("input image", CV_WINDOW_NORMAL);
namedWindow("output image", CV_WINDOW_NORMAL);
namedWindow("template match-demo", CV_WINDOW_NORMAL);
imshow("input image", src);
const char* trackbar_title = "Match Algo Type";
createTrackbar(trackbar_title, "output image", &match_method, max_track, Match_Demo);
Match_Demo(0, 0); //先调用一下,保证初始值不为空
waitKey(0);
return 0;
}
void Match_Demo(int, void*)
{
int width = src.cols - temp.cols + 1;
int height = src.rows - temp.rows + 1;
Mat result(width, height, CV_32FC1); //必须是32位浮点数
matchTemplate(src, temp, result, match_method, Mat()); //到时候从trackbar上面获取match_method
//下面是归一化,把结果变成0到1之间
normalize(result, result, 0, 1, NORM_MINMAX, -1, Mat());
//下面要找出模板匹配最大值最小值的位置,也就是和哪个位置匹配
Point minLoc; // 找出最小值的位置
Point maxLoc; // 找出最大值的位置
double min, max;
//OpenCV提供API来找出最大最小值的位置
minMaxLoc(result, &min, &max, &minLoc, &maxLoc, Mat());
//用矩形框来把最大最小值来标出来
src.copyTo(dst); //在dst上面进行绘制工作
Point temLoc;
if (match_method == CV_TM_SQDIFF || match_method == CV_TM_SQDIFF_NORMED) //对于这两种方法而言,应该是最小值来匹配。其他方法是最大值来匹配
{
temLoc = minLoc;
}
else {
temLoc = maxLoc;
}
rectangle(dst, Rect(temLoc.x, temLoc.y, temp.cols, temp.rows), Scalar(0, 0, 255), 2, 8); //在最终输出图像上绘制一个矩形
rectangle(result, Rect(temLoc.x, temLoc.y, temp.cols, temp.rows), Scalar(0, 0, 255), 2, 8); //在result结果上面输出一个矩形
imshow("output image", result);
imshow("template match-demo", dst);
}
数据介绍¶
数据地址:https://github.com/hromi/SMILEsmileD
数据包含13165张灰度图片,每张图片的尺寸是64*64。这个数据集并不算平衡,13165张图片中,有9475张图片不是笑脸图片,有3690张图片是笑脸图片。数据差异很大。
数据预处理¶
首先导入相应的包:
from sklearn.preprocessing import LabelEncoder
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report
from keras.preprocessing.image import img_to_array
from keras.utils import np_utils
from imutils import paths
import matplotlib.pyplot as plt
import numpy as np
import imutils
import cv2
import os
from keras.models import Sequential
from keras.layers.convolutional import Conv2D
from keras.layers.convolutional import MaxPooling2D
from keras.layers.core import Activation
from keras.layers.core import Flatten
from keras.layers.core import Dense
dataset_dir = os.path.abspath(r"./SMILEs/") #smile数据集路径
model_dir = os.path.abspath(r"./model/lenet.hdf5") #训练模型保存路径
data = []
labels = []
for imagePath in sorted(list(paths.list_images(dataset_dir))):
image = cv2.imread(imagePath)
image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) # 转换成灰度图像
image = imutils.resize(image, width = 28) #将图像尺寸改成28*28
image = img_to_array(image) #使用Keras的img_to_array转换成浮点型和(28*28*1),便于接下来神经网络学习
data.append(image)
label = imagePath.split(os.path.sep)[-3]
label = "smiling" if label == "positives" else "not_smiling" #如果label字符串里面有positive就重命名为smiling
labels.append(label)
# 将data和labels都转换为numpy类型
data = np.array(data, dtype= "float") / 255.0 #将像素转换到[0, 1]范围之内
labels = np.array(labels)
# 对label进行one-hot编码
le = LabelEncoder().fit(labels) # LabelEncoder可以将标签分配一个0—n_classes-1之间的编码
# transform用来标准化,将labels中'not_smiling'和‘smiling’的数据转换成0和1的形式
labels = np_utils.to_categorical(le.transform(labels), 2) # 2是num_class表示输出的是2列数据的意思
下面需要解决一下样本不平衡问题。
数据集里面有9475个笑脸样本,和3690个非笑脸样本。下面的代码中classTotals就是按列加和labels的one-hot编码,所以结果是[9475, 3690] 我们要解决数据不平衡问题可以使用classWeight权重,相比于笑脸,我们给非笑脸以2.56倍的权重。损失函数权重计算的时候对非笑脸进行相应扩大,以此来解决数据不平衡问题。
classTotals = labels.sum(axis=0)
classWeight = classTotals.max() / classTotals
stratify是为了保持split前类的分布。比如有100个数据,80个属于A类,20个属于B类。如果train_test_split(… test_size=0.25, stratify = y_all), 那么split之后数据如下:
training: 75个数据,其中60个属于A类,15个属于B类。
testing: 25个数据,其中20个属于A类,5个属于B类。
用了stratify参数,training集和testing集的类的比例是 A:B= 4:1,等同于split前的比例(80:20)。通常在这种类分布不平衡的情况下会用到stratify
(trainX, testX, trainY, testY) = train_test_split(data, labels, test_size = 0.20,
stratify = labels, random_state = 42)
使用LeNet实现笑脸检测分类¶
下面是模型实现部分:
model = Sequential()
# first set of CONV => RELU => POOL layers
model.add(Conv2D(input_shape=(28, 28, 1), kernel_size=(5, 5), filters=20, activation='relu'))
model.add(MaxPooling2D(pool_size=(2,2), strides=2, padding='same'))
# second set of CONV => RELU => POOL layers
model.add(Conv2D(kernel_size=(5, 5), filters=50, activation='relu', padding='same'))
model.add(MaxPooling2D(pool_size=(2,2), strides=2, padding='same'))
# first (and only) set of FC => RELU layers
model.add(Flatten())
model.add(Dense(500, activation='relu'))
model.add(Dense(2, activation='softmax'))
model.compile(loss = "binary_crossentropy", optimizer = "adam", metrics = ["accuracy"])
H = model.fit(trainX, trainY, validation_data = (testX, testY),
class_weight = classWeight, batch_size = 64, epochs = 15, verbose = 1) #verbose = 1显示进度条
keras没有直接可以统计recall和f1值的办法。可以用sklearn。 但是sklearn没有办法直接处理Keras的数据,所以要经过一些处理。Keras计算需要二维数组,但classification_report可以处理的是一维数列,所以这里使用argmax按行返回二维数组最大索引,这样也算是一种0-1标签的划分了。
predictions = model.predict(testX, batch_size = 64)
print(classification_report(testY.argmax(axis = 1), predictions.argmax(axis = 1),
target_names = le.classes_)) # le.classes是['not_smiling', 'smiling']组成的数组
model.save(model_dir)
输出结果:
precision recall f1-score support
not_smiling 0.95 0.91 0.93 1895
smiling 0.79 0.87 0.83 738
avg / total 0.90 0.90 0.90 2633
plt.style.use("ggplot")
plt.figure()
plt.plot(np.arange(0, 15), H.history["loss"], label = "train_loss")
plt.plot(np.arange(0, 15), H.history["val_loss"], label = "val_loss")
plt.plot(np.arange(0, 15), H.history["acc"], label = "acc")
plt.plot(np.arange(0, 15), H.history["val_acc"], label = "val_acc")
plt.title("Training Loss and Accuracy")
plt.xlabel("Epoch")le
plt.ylabel("Loss/Accuracy")
plt.legend()
plt.show()
image
人脸检测实现¶
这里使用OpenCV的Haar特征和级联分类器来实现实时人脸检测,关于Haar特征和级联分类器的理论知识,可以看这里
我们在代码中使用了OpenCV这个工具来具体实现,在OpenCV中,相应算法都已经做好了封装,直接调用就可以了。值得一提的是,人脸检测的模型已经提前训练好了,这里我们直接调用模型就可以在,是一个XML格式的文件“haarcascade_frontalface_default.xml”,一般在opencv-3.4\opencv\sources\data\haarcascades路径下可以找到。下面是具体代码实现:
from keras.preprocessing.image import img_to_array
from keras.models import load_model
import numpy as np
import imutils
import cv2
import os
import argparse
ap = argparse.ArgumentParser()
ap.add_argument("-c", "--cascade", required= True,
help= "path to where the face cascade resides")
ap.add_argument("-m", "--model", required= True,
help= "path to pre-trained smile detector CNN")
ap.add_argument("-v", "--video",
help="path to the (optional) video file")
args = vars(ap.parse_args())
detector = cv2.CascadeClassifier(args["cascade"]) #从对应路径中加载人脸检测级联分类器
model = load_model(args["model"])
# 对是从相机中检测人脸还是从视频中检测人脸做判断
if not args.get("video", False):
camera = cv2.VideoCapture(0)
else:
camera = cv2.VideoCapture(args["video"])
while True:
# grabbed和frame是read的两个返回值,grabbed是布尔类型的返回值,如果读取帧是正确的返回True,当文件读到结尾的时候返回False
# frame是每一帧的图像,是一个三维矩阵
(grabbed, frame) = camera.read()
if args.get("video") and not grabbed:
break
frame = imutils.resize(frame, width = 300) #把图像宽度重新指定为300像素
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) #因为模型训练是对灰度图像处理,所以这里要转换成灰度图像
frameClone = frame.copy() #重新克隆frame,用于接下来绘制边界框
rects = detector.detectMultiScale(gray, scaleFactor=1.1, minNeighbors=5, minSize=(30, 30),
flags=cv2.CASCADE_SCALE_IMAGE)
for (fX, fY, fW, fH) in rects:
roi = gray[fY:fY + fH, fX:fX + fW]
roi = cv2.resize(roi, (28, 28))
roi = roi.astype("float") / 255.0
roi = img_to_array(roi)
roi = np.expand_dims(roi, axis = 0)
(notSmiling, smiling) = model.predict(roi)[0]
label = "Smiling" if smiling > notSmiling else "Not Smiling"
cv2.putText(frameClone, label, (fX, fY - 10), cv2.FONT_HERSHEY_SIMPLEX,
0.45, (0, 0, 255), 2)
cv2.rectangle(frameClone, (fX, fY), (fX + fW, fY + fH),
(0, 0, 255), 2)
cv2.imshow("Face", frameClone)
if cv2.waitKey(1) & 0xFF == ord("q"):
break
camera.release()
cv2.destroyAllWindows()
detector.detectMultiScale¶
这里,对detector.detectMultiScale
做一点说明:
为了检测到不同大小的目标,一般有两种做法:逐步缩小图像;或者,逐步放大检测窗口。缩小图像就是把图像长宽同时按照一定比例(默认1.1 or 1.2)逐步缩小,然后检测;放大检测窗口是把检测窗口长宽按照一定比例逐步放大,这时位于检测窗口内的特征也会对应放大,然后检测。在默认的情况下,OpenCV是采取逐步缩小的情况,如下图所示,最先检测的图片是底部那张大图。
image
然后,对应每张图,级联分类器的大小固定的检测窗口器开始遍历图像,以便在图像找到位置不同的目标。对照程序来看,这个固定的大小就是上图的红色框。
void CascadeClassifier::detectMultiScale( InputArray image,
CV_OUT std::vector<Rect>& objects,
double scaleFactor,
int minNeighbors, int flags,
Size minSize,
Size maxSize )
参数1:image–待检测图片,一般为灰度图像以加快检测速度;
参数2:objects–被检测物体的矩形框向量组;为输出量,如某特征检测矩阵Mat
参数3:scaleFactor–表示在前后两次相继的扫描中,搜索窗口的比例系数。默认为1.1即每次搜索窗口依次扩大10%
参数4:minNeighbors–表示构成检测目标的相邻矩形的最小个数(默认为3个)。 如果组成检测目标的小矩形的个数和小于 min_neighbors - 1 都会被排除。 如果min_neighbors 为 0, 则函数不做任何操作就返回所有的被检候选矩形框, 这种设定值一般用在用户自定义对检测结果的组合程序上;
参数5:flags=0:可以取如下这些值: CASCADE_DO_CANNY_PRUNING=1, 利用canny边缘检测来排除一些边缘很少或者很多的图像区域 CASCADE_SCALE_IMAGE=2, 正常比例检测 CASCADE_FIND_BIGGEST_OBJECT=4, 只检测最大的物体 CASCADE_DO_ROUGH_SEARCH=8 初略的检测 6. minObjectSize maxObjectSize:匹配物体的大小范围
参数6、7:minSize和maxSize用来限制得到的目标区域的范围。也就是我本次训练得到实际项目尺寸大小 函数介绍: detectMultiscale函数为多尺度多目标检测: 多尺度:通常搜索目标的模板尺寸大小是固定的,但是不同图片大小不同,所以目标对象的大小也是不定的,所以多尺度即不断缩放图片大小(缩放到与模板匹配),通过模板滑动窗函数搜索匹配;同一副图片可能在不同尺度下都得到匹配值,所以多尺度检测函数detectMultiscale是多尺度合并的结果。 多目标:通过检测符合模板匹配对象,可得到多个目标,均输出到objects向量里面。
minNeighbors=3:匹配成功所需要的周围矩形框的数目,每一个特征匹配到的区域都是一个矩形框,只有多个矩形框同时存在的时候,才认为是匹配成功,比如人脸,这个默认值是3。
因为代码中使用了argparse,所以可以通过命令来指定,如果是想用webcam(PC自带摄像头)的话,可以输入:
python detect_smile.py --cascade haarcascade_frontalface_default.xml
--model output/lenet.hdf5
如果是用视频的话,可以输入如下命令:
python detect_smile.py --cascade haarcascade_frontalface_default.xml
--model output/lenet.hdf5
--video path/to/your/video.mov