OpenCV2学习笔记(十四):基于OpenCV卡通图片处理
得知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; // 設定攝像頭編號為0if(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檢測之前,須要對圖像進行的預操作有:
生成的素描效果:
三、將圖像卡通化
在項目中調用一些運算量大的算法時,通常須要考慮到效率問題,比方這里將要用到的雙邊濾波器。
這里我們利用雙邊濾波器的平滑區域及保持邊緣銳化的特性,將其運用到卡通圖片效果生成中。
而考慮到雙邊濾波器執行效率較低,因此考慮在更低的分辨率中使用,這對效果影響不大,可是執行速度大大加快。
這里使用的策略是將要處理的圖像的寬度和高度縮小為原來的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_Hcartoon.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(); }版權聲明:本文博主原創文章。博客,未經同意不得轉載。
轉載于:https://www.cnblogs.com/zfyouxi/p/4838928.html
總結
以上是生活随笔為你收集整理的OpenCV2学习笔记(十四):基于OpenCV卡通图片处理的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Sql-事务
- 下一篇: initWithFrame方法的理解(转