标签:qt opencv 边缘检测 皮肤检测 android
学习OpenCV已有一段时间,除了研究各种算法的内容,在空闲之余,根据书本及资料的引导,尝试结合图像处理算法和日常生活联系起来,首先在台式机上(带摄像头)完成一系列视频流处理功能,开发平台为Qt5.3.2+OpenCV2.4.9。
本次试验实现的功能主要有:
本节所有的算法均由类cartoon中的函数cartoonTransform()来实现:
// Frame:输入每一帧图像 output:输出图像
cartoonTransform(cv::Mat &Frame, cv::Mat &output)
后续将使用更多的OpenCV技巧实现更多功能,并将该应用一直到Android系统上。
一、使用OpenCV访问摄像头
OpenCV提供了一个简便易用的框架以提取视频文件和USB摄像头中的图像帧,如果你只是想读取某个视频,你只需要创建一个cv::VideoCapture实例,然后在循环中提取每一帧。这里需要访问摄像头,因此需要创建一个cv::VideoCapture对象,简单调用对象的open()方法。这里访问摄像头的函数如下,首先在Qt中创建控制台项目,在main函数中添加:
int cameraNumber = 0; // 设定摄像头编号为0
if(argc > 1)
cameraNumber = atoi(argv[1]);
// 开启摄像头
cv::VideoCapture camera;
camera.open(cameraNumber);
if(!camera.isOpened())
{
qDebug() << "Error: Could not open the camera.";
exit(1);
}
// 调整摄像头的输出分辨率
camera.set(CV_CAP_PROP_FRAME_WIDTH, 640);
camera.set(CV_CAP_PROP_FRAME_HEIGHT, 480);
在摄像头被初始化后,可以使用C++流运算符将cv::VideoCapture对象转换成cv::Mat对象,这样可以获取视频的每一帧图像。关于视频流读取可参考: http://blog.csdn.net/liyuefeilong/article/details/44066097
二、将帧图像转换为素描效果图片
要将一幅图像转换为素描效果图,可以使用不同的边缘检测算法实现,如常用的基于Sobel、Canny、Robert、Prewitt、Laplacian等算子的滤波器均可以实现这一操作,但处理效果各异。
1.Sobel算子:边缘检测中最常用的一种方法,在技术上它是以离散型的差分算子,用来运算图像亮度函数的梯度的近似值,缺点是Sobel算子并没有将图像的主题与背景严格地区分开来,换言之就是Sobel算子并没有基于图像灰度进行处理,由于Sobel算子并没有严格地模拟人的视觉生理特征,所以提取的图像轮廓有时并不能令人满意。
2.Robert算子:根据任一相互垂直方向上的差分都用来估计梯度,Robert算子采用对角方向相邻像素之差。
3.Prewitt算子:该算子与Sobel算子类似,只是权值有所变化,但两者实现起来功能还是有差距的,据经验得知Sobel要比Prewitt更能准确检测图像边缘。
4.Laplacian算子:该算子是一种二阶微分算子,若只考虑边缘点的位置而不考虑周围的灰度差时可用该算子进行检测。对于阶跃状边缘,其二阶导数在边缘点出现零交叉,并且边缘点两旁的像素的二阶导数异号。
5.Canny算子:该算子的基本性能比前面几种要好,但是相对来说算法复杂。Canny算子是一个具有滤波,增强,检测的多阶段的优化算子,在进行处理前,Canny算子先利用高斯平滑滤波器来平滑图像以除去噪声,Canny分割算法采用一阶偏导的有限差分来计算梯度幅值和方向,在处理过程中,Canny算子还将经过一个非极大值抑制的过程,最后Canny算子还采用两个阈值来连接边缘。
相比Sobel等其他算子,Canny和Laplacian算子能得到更清晰的素描效果,而Laplacian的噪声抑制要优于Canny边缘检测,而事实上素描边缘在不同帧之间经常有剧烈的变化,因此我们选择Laplacian边缘滤波器进行图像处理。
一般在进行Laplacian检测之前,需要对图像进行的预操作有:
void cartoon::cartoonTransform(cv::Mat &Frame, cv::Mat &output)
{
cv::Mat grayImage;
cv::cvtColor(Frame, grayImage, CV_BGR2GRAY);
// 设置中值滤波器参数
cv::medianBlur(grayImage, grayImage, 7);
// Laplacian边缘检测
cv::Mat edge; // 用于存放边缘检测输出结果
cv::Laplacian(grayImage, edge, CV_8U, 5);
// 对边缘检测结果进行二值化
cv::Mat Binaryzation; // 用于存放二值化输出结果
cv::threshold(edge, Binaryzation, 80, 255, cv::THRESH_BINARY_INV);
}
生成的素描效果:
三、将图像卡通化
在项目中调用一些运算量大的算法时,通常需要考虑到效率问题,比如这里将要用到的双边滤波器。这里我们利用双边滤波器的平滑区域及保持边缘锐化的特性,将其运用到卡通图片效果生成中。而考虑到双边滤波器运行效率较低,因此考虑在更低的分辨率中使用,这对效果影响不大,但是运行速度大大加快。
这里使用的策略是将要处理的图像的宽度和高度缩小为原来的1/2,经过双边滤波器处理后,再将其恢复为原来的尺寸。在函数cartoonTransform()中添加以下代码:
// 采用双边滤波器
// 由于算法复杂,因此需减少图像尺寸
cv::Size size = Frame.size();
cv::Size reduceSize;
reduceSize.width = size.width / 2;
reduceSize.height = size.height / 2;
cv::Mat reduceImage = cv::Mat(reduceSize, CV_8UC3);
cv::resize(Frame, reduceImage, reduceSize);
// 双边滤波器实现过程
cv::Mat tmp = cv::Mat(reduceSize, CV_8UC3);
int repetitions = 7;
for (int i=0 ; i < repetitions; i++)
{
int kernelSize = 9;
double sigmaColor = 9;
double sigmaSpace = 7;
cv::bilateralFilter(reduceImage, tmp, kernelSize, sigmaColor, sigmaSpace);
cv::bilateralFilter(tmp, reduceImage, kernelSize, sigmaColor, sigmaSpace);
}
// 由于图像是缩小后的图像,需要恢复
cv::Mat magnifyImage;
cv::resize(reduceImage, magnifyImage, size);
为了得到更好的效果,在以上代码中添加以下函数,将恢复尺寸后的图像与上一部分的素描结果相叠加,得到卡通版的图像~~
cv::Mat dst;
dst.setTo(0);
magnifyImage.copyTo(dst, Binaryzation);
//output = dst; //输出
卡通效果,阈值各方面有待优化:
四、简单地生成“怪物”形象
这里是结合了边缘滤波器和中值滤波器的另一个小应用,即通过小的边缘滤波器找到图像中的各处边缘,之后使用中值滤波器来合并这些边缘。具体实现步骤如下:
详细代码如下,同样在函数cartoonTransform()中添加:
// 怪物模式
cv::Mat gray ,maskMonster;
cv::cvtColor(Frame, gray, CV_BGR2GRAY);
// 先对输入帧进行中值滤波
cv::medianBlur(gray, gray, 7);
// Scharr滤波器
cv::Mat edge1, edge2;
cv::Scharr(gray, edge1, CV_8U, 1, 0);
cv::Scharr(gray, edge2, CV_8U, 1, 0, -1);
edge1 += edge2; // 合并x和y方向的边缘
cv::threshold(edge1, maskMonster, 12, 255, cv::THRESH_BINARY_INV);
cv::medianBlur(maskMonster, maskMonster, 3);
output = maskMonster; //输出
五、人脸肤色变换
皮肤检测算法有很多种,比如基于RGB color space、Ycrcb之cr分量+otsu阈值化、基于混合模型的复杂机器学习算法等。由于这里只是一个轻量级的应用,因此不考虑使用太复杂的算法。考虑到未来要将这些图像处理算法移植到安卓上,而移动设备上的微型摄像头传感器对颜色的反应往往差异很大,而且要在没有标定的情况下对不同肤色的人进行皮肤检测,因此对算法的鲁棒性要求较高。
这里使用了一个技巧,即在图像中规定一个区域,用户需要将脸部放到指定区域中来确定人脸在图像中的位置(事实上有些手机应用也会采取这种方法),对于移动设备来说这不是一件难事。
因此,我们需要规定人脸的区域,同样在函数cartoonTransform()中添加以下代码:
// 怪物模式
cv::Mat gray ,maskMonster;
cv::cvtColor(Frame, gray, CV_BGR2GRAY);
// 先对输入帧进行中值滤波
cv::medianBlur(gray, gray, 7);
// Scharr滤波器
cv::Mat edge1, edge2;
cv::Scharr(gray, edge1, CV_8U, 1, 0);
cv::Scharr(gray, edge2, CV_8U, 1, 0, -1);
edge1 += edge2; // 合并x和y方向的边缘
cv::threshold(edge1, maskMonster, 12, 255, cv::THRESH_BINARY_INV);
cv::medianBlur(maskMonster, maskMonster, 3);
output = maskMonster; //输出
// 换肤模式
// 绘制脸部区域
cv::Mat faceFrame = cv::Mat::zeros(size, CV_8UC3);
cv::Scalar color = CV_RGB(128, 0, 128); // 颜色
int thickness = 4;
// 使之占整个图像高度的70%
int width = size.width;
int height = size.height;
int faceHeight = height/2 * 70/100;
int faceWidth = faceHeight * 72/100;
cv::ellipse(faceFrame, cv::Point(width/2, height/2), cv::Size(faceWidth, faceHeight),
0, 0, 360, color, thickness, CV_AA);
// imshow("test3", faceFrame);
// 绘制眼睛区域
int eyeHeight = faceHeight * 11/100;
int eyeWidth = faceWidth * 23/100;
int eyeY = faceHeight * 13/100;
int eyeX = faceWidth * 48/100;
cv::Size eyeSize = cv::Size(eyeWidth, eyeHeight);
int eyeAngle = 15; //角度
int eyeYShift = 11;
// 画右眼的上眼皮
cv::ellipse(faceFrame, cv::Point(width/2 - eyeX, height/2 - eyeY),
eyeSize, 0, 180+eyeAngle, 360-eyeAngle, color, thickness, CV_AA);
// 画右眼的下眼皮
cv::ellipse(faceFrame, cv::Point(width/2 - eyeX, height/2 - eyeY - eyeYShift),
eyeSize, 0, 0+eyeAngle, 180-eyeAngle, color, thickness, CV_AA);
// 画左眼的上眼皮
cv::ellipse(faceFrame, cv::Point(width/2 + eyeX, height/2 - eyeY),
eyeSize, 0, 180+eyeAngle, 360-eyeAngle, color, thickness, CV_AA);
// 画左眼的下眼皮
cv::ellipse(faceFrame, cv::Point(width/2 + eyeX, height/2 - eyeY - eyeYShift),
eyeSize, 0, 0+eyeAngle, 180-eyeAngle, color, thickness, CV_AA);
char *Message = "Put your face here";
cv::putText(faceFrame, Message, cv::Point(width * 13/100, height * 10/100),
cv::FONT_HERSHEY_COMPLEX,
1.0f,
color,
2,
CV_AA);
cv::addWeighted(dst, 1.0, faceFrame, 0.7, 0, dst, CV_8UC3);
//output = dst;
效果:
皮肤变色器的实现基于OpenCV的floodFill()函数,该函数类似于一些绘图软件中的颜料桶(颜色填充)工具。 由于规定屏幕中间椭圆区域就是皮肤像素,因此只需要对该区域的像素进行各种颜色的漫水填充即可。
这里处理的图像是彩色图,而对于RGB格式的图像,改变颜色的效果不会太好,因为改变颜色需要脸部图像的亮度变化,而皮肤颜色也不能变化太大。这里使用YCrCb颜色空间来进行处理。在YCrCb颜色空间中,可以直接获得亮度值,而且通常的皮肤颜色取值唯一。
// 皮肤变色器
cv::Mat YUVImage = cv::Mat(reduceSize, CV_8UC3);
cv::cvtColor(reduceImage, YUVImage, CV_BGR2YCrCb);
int sw = reduceSize.width;
int sh = reduceSize.height;
cv::Mat mask, maskPlusBorder;
maskPlusBorder = cv::Mat::zeros(sh+2, sw+2, CV_8UC1);
mask = maskPlusBorder(cv::Rect(1, 1, sw, sh));
cv::resize(edge, mask, reduceSize);
const int EDGES_THRESHOLD = 80;
cv::threshold(mask, mask, EDGES_THRESHOLD, 255, cv::THRESH_BINARY);
cv::dilate(mask, mask, cv::Mat());
cv::erode(mask, mask, cv::Mat());
// output = mask;
// 创建6个点进行漫水填充算法
cv::Point skinPoint[6];
skinPoint[0] = cv::Point(sw/2, sh/2 - sh/6);
skinPoint[1] = cv::Point(sw/2 - sw/11, sh/2 - sh/6);
skinPoint[2] = cv::Point(sw/2 + sw/11, sh/2 - sh/6);
skinPoint[3] = cv::Point(sw/2, sh/2 + sh/6);
skinPoint[4] = cv::Point(sw/2 - sw/9, sh/2 + sh/6);
skinPoint[5] = cv::Point(sw/2 + sw/9, sh/2 + sh/6);
// 设定漫水填充算法的上下限
const int MIN_Y = 60;
const int MAX_Y = 80;
const int MIN_Cr = 25;
const int MAX_Cr = 15;
const int MIN_Cb = 20;
const int MAX_Cb = 15;
cv::Scalar Min = cv::Scalar(MIN_Y, MIN_Cr, MIN_Cb);
cv::Scalar Max = cv::Scalar(MAX_Y, MAX_Cr, MAX_Cb);
// 调用漫水填充算法
const int CONNECTED_COMPONENTS = 4;
const int flag = CONNECTED_COMPONENTS | cv::FLOODFILL_FIXED_RANGE | cv::FLOODFILL_MASK_ONLY;
cv::Mat edgeMask = mask.clone();
//
for(int i = 0; i < 6; i++)
{
cv::floodFill(YUVImage, maskPlusBorder, skinPoint[i], cv::Scalar(), NULL,
Min, Max, flag);
}
cv::Mat BGRImage;
cv::cvtColor(YUVImage, BGRImage, CV_YCrCb2BGR);
mask -= edgeMask;
int Red = 0;
int Green = 70;
int Blue = 0;
cv::Scalar color2 = CV_RGB(Red, Green, Blue); // 颜色
cv::add(BGRImage, color2, BGRImage, mask);
cv::Mat tt;
cv::resize(BGRImage, tt, size);
cv::add(dst, tt ,dst);
output = dst; // 换肤结果
由于在脸部区域中要对许多像素使用漫水填充算法,因此为了保证人脸图像的各种颜色和阴影都能得到处理,这里设置了前额、鼻子和脸颊6个点,他们的定位依赖于先前规定的脸部轮廓坐标。输出效果如下:
脸部不在识别区域内时:
脸部进入识别区域内时:
以上实现了几种图片卡通化效果,接着在学有余力时要对各种算法的效果进行优化,同时加入GUI界面,并将应用移植到移动设备上。
参考资料:《深入理解OpenCV:实用计算机视觉项目解析》
完整代码:
cartoon.h:
#ifndef CARTOON_H
#define CARTOON_H
#include <opencv2/core/core.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/highgui/highgui.hpp>
class cartoon
{
public:
void cartoonTransform(cv::Mat &Frame, cv::Mat &output);
};
#endif // CARTOON_H
cartoon.cpp:
#include "cartoon.h"
void cartoon::cartoonTransform(cv::Mat &Frame, cv::Mat &output)
{
cv::Mat grayImage;
cv::cvtColor(Frame, grayImage, CV_BGR2GRAY);
// 设置中值滤波器参数
cv::medianBlur(grayImage, grayImage, 7);
// Laplacian边缘检测
cv::Mat edge; // 用于存放边缘检测输出结果
cv::Laplacian(grayImage, edge, CV_8U, 5);
// 对边缘检测结果进行二值化
cv::Mat Binaryzation; // 用于存放二值化输出结果
cv::threshold(edge, Binaryzation, 80, 255, cv::THRESH_BINARY_INV);
// 以下操作生成彩色图像和卡通效果
// 采用双边滤波器
// 由于算法复杂,因此需减少图像尺寸
cv::Size size = Frame.size();
cv::Size reduceSize;
reduceSize.width = size.width / 2;
reduceSize.height = size.height / 2;
cv::Mat reduceImage = cv::Mat(reduceSize, CV_8UC3);
cv::resize(Frame, reduceImage, reduceSize);
// 双边滤波器实现过程
cv::Mat tmp = cv::Mat(reduceSize, CV_8UC3);
int repetitions = 7;
for (int i=0 ; i < repetitions; i++)
{
int kernelSize = 9;
double sigmaColor = 9;
double sigmaSpace = 7;
cv::bilateralFilter(reduceImage, tmp, kernelSize, sigmaColor, sigmaSpace);
cv::bilateralFilter(tmp, reduceImage, kernelSize, sigmaColor, sigmaSpace);
}
// 由于图像是缩小后的图像,需要恢复
cv::Mat magnifyImage;
cv::resize(reduceImage, magnifyImage, size);
cv::Mat dst;
dst.setTo(0);
magnifyImage.copyTo(dst, Binaryzation);
//output = dst; //输出
// 怪物模式
cv::Mat gray ,maskMonster;
cv::cvtColor(Frame, gray, CV_BGR2GRAY);
// 先对输入帧进行中值滤波
cv::medianBlur(gray, gray, 7);
// Scharr滤波器
cv::Mat edge1, edge2;
cv::Scharr(gray, edge1, CV_8U, 1, 0);
cv::Scharr(gray, edge2, CV_8U, 1, 0, -1);
edge1 += edge2; // 合并x和y方向的边缘
cv::threshold(edge1, maskMonster, 12, 255, cv::THRESH_BINARY_INV);
cv::medianBlur(maskMonster, maskMonster, 3);
output = maskMonster; //输出
// 换肤模式
// 绘制脸部区域
cv::Mat faceFrame = cv::Mat::zeros(size, CV_8UC3);
cv::Scalar color = CV_RGB(128, 0, 128); // 颜色
int thickness = 4;
// 使之占整个图像高度的70%
int width = size.width;
int height = size.height;
int faceHeight = height/2 * 70/100;
int faceWidth = faceHeight * 72/100;
cv::ellipse(faceFrame, cv::Point(width/2, height/2), cv::Size(faceWidth, faceHeight),
0, 0, 360, color, thickness, CV_AA);
// imshow("test3", faceFrame);
// 绘制眼睛区域
int eyeHeight = faceHeight * 11/100;
int eyeWidth = faceWidth * 23/100;
int eyeY = faceHeight * 13/100;
int eyeX = faceWidth * 48/100;
cv::Size eyeSize = cv::Size(eyeWidth, eyeHeight);
int eyeAngle = 15; //角度
int eyeYShift = 11;
// 画右眼的上眼皮
cv::ellipse(faceFrame, cv::Point(width/2 - eyeX, height/2 - eyeY),
eyeSize, 0, 180+eyeAngle, 360-eyeAngle, color, thickness, CV_AA);
// 画右眼的下眼皮
cv::ellipse(faceFrame, cv::Point(width/2 - eyeX, height/2 - eyeY - eyeYShift),
eyeSize, 0, 0+eyeAngle, 180-eyeAngle, color, thickness, CV_AA);
// 画左眼的上眼皮
cv::ellipse(faceFrame, cv::Point(width/2 + eyeX, height/2 - eyeY),
eyeSize, 0, 180+eyeAngle, 360-eyeAngle, color, thickness, CV_AA);
// 画左眼的下眼皮
cv::ellipse(faceFrame, cv::Point(width/2 + eyeX, height/2 - eyeY - eyeYShift),
eyeSize, 0, 0+eyeAngle, 180-eyeAngle, color, thickness, CV_AA);
char *Message = "Put your face here";
cv::putText(faceFrame, Message, cv::Point(width * 13/100, height * 10/100),
cv::FONT_HERSHEY_COMPLEX,
1.0f,
color,
2,
CV_AA);
cv::addWeighted(dst, 1.0, faceFrame, 0.7, 0, dst, CV_8UC3);
//output = dst;
// 皮肤变色器
cv::Mat YUVImage = cv::Mat(reduceSize, CV_8UC3);
cv::cvtColor(reduceImage, YUVImage, CV_BGR2YCrCb);
int sw = reduceSize.width;
int sh = reduceSize.height;
cv::Mat mask, maskPlusBorder;
maskPlusBorder = cv::Mat::zeros(sh+2, sw+2, CV_8UC1);
mask = maskPlusBorder(cv::Rect(1, 1, sw, sh));
cv::resize(edge, mask, reduceSize);
const int EDGES_THRESHOLD = 80;
cv::threshold(mask, mask, EDGES_THRESHOLD, 255, cv::THRESH_BINARY);
cv::dilate(mask, mask, cv::Mat());
cv::erode(mask, mask, cv::Mat());
// output = mask;
// 创建6个点进行漫水填充算法
cv::Point skinPoint[6];
skinPoint[0] = cv::Point(sw/2, sh/2 - sh/6);
skinPoint[1] = cv::Point(sw/2 - sw/11, sh/2 - sh/6);
skinPoint[2] = cv::Point(sw/2 + sw/11, sh/2 - sh/6);
skinPoint[3] = cv::Point(sw/2, sh/2 + sh/6);
skinPoint[4] = cv::Point(sw/2 - sw/9, sh/2 + sh/6);
skinPoint[5] = cv::Point(sw/2 + sw/9, sh/2 + sh/6);
// 设定漫水填充算法的上下限
const int MIN_Y = 60;
const int MAX_Y = 80;
const int MIN_Cr = 25;
const int MAX_Cr = 15;
const int MIN_Cb = 20;
const int MAX_Cb = 15;
cv::Scalar Min = cv::Scalar(MIN_Y, MIN_Cr, MIN_Cb);
cv::Scalar Max = cv::Scalar(MAX_Y, MAX_Cr, MAX_Cb);
// 调用漫水填充算法
const int CONNECTED_COMPONENTS = 4;
const int flag = CONNECTED_COMPONENTS | cv::FLOODFILL_FIXED_RANGE | cv::FLOODFILL_MASK_ONLY;
cv::Mat edgeMask = mask.clone();
//
for(int i = 0; i < 6; i++)
{
cv::floodFill(YUVImage, maskPlusBorder, skinPoint[i], cv::Scalar(), NULL,
Min, Max, flag);
}
cv::Mat BGRImage;
cv::cvtColor(YUVImage, BGRImage, CV_YCrCb2BGR);
mask -= edgeMask;
int Red = 0;
int Green = 70;
int Blue = 0;
cv::Scalar color2 = CV_RGB(Red, Green, Blue); // 颜色
cv::add(BGRImage, color2, BGRImage, mask);
cv::Mat tt;
cv::resize(BGRImage, tt, size);
cv::add(dst, tt ,dst);
output = dst; // 换肤结果
}
main函数:
#include "cartoon.h"
#include <QApplication>
#include <QDebug>
#include <opencv2/video/video.hpp>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
cartoon photo;
int cameraNumber = 0;
if(argc > 1)
cameraNumber = atoi(argv[1]);
// 开启摄像头
cv::VideoCapture camera;
camera.open(cameraNumber);
if(!camera.isOpened())
{
qDebug() << "Error: Could not open the camera.";
exit(1);
}
// 调整摄像头的分辨率
camera.set(CV_CAP_PROP_FRAME_WIDTH, 640);
camera.set(CV_CAP_PROP_FRAME_HEIGHT, 480);
while (1)
{
cv::Mat Frame;
camera >> Frame;
if(!Frame.data)
{
qDebug() << "Couldn‘t capture camera frame.";
exit(1);
}
// 创建一个用于存放输出图像的数据结构
cv::Mat output(Frame.size(), CV_8UC3);
photo.cartoonTransform(Frame, output);
// 使用图像处理技术将获取的帧经过处理后输入到output中
cv::imshow("Original", Frame);
cv::imshow("Carton", output);
char keypress = cv::waitKey(20);
if(keypress == 27)
{
break;
}
}
return a.exec();
}
标签:qt opencv 边缘检测 皮肤检测 android
原文地址:http://blog.csdn.net/liyuefeilong/article/details/44340327