為了避免自己一直欠著就忘了寫
還是早點把這個"黑白漫畫"(sketch)做一個了斷吧!
前情提要請參考上集:
===========分隔線===========
上集有提到,這兩篇的作法都是先有Photoshop的處理
再想辦法將這段Photoshop流成轉為程式
不同於上集參考網路教學,下集則是完全靠Art同事對影像處理的敏感度完成!
當初一步一步記下他用了photoshop哪些功能,再一步一步用程式實作來逼近
雖然有捨棄到一些步驟,但個人覺得結果比上集的更接近Comic一點!
老樣子,看看結果先...
為了方便比較,當然還是拿出相同的長頸鹿做為範例了!
相較於上集的作法,這次的功能相對起來更複雜了一些
就結果來看,並不像上次一樣是純粹的黑白影像,比較像是貼上網點之後的結果吧!!
只不過,跟上次那種隨便跑起來都60fps的情況比起來
這次的部份功能是會有效能問題要另外克服的!
相信丟到GPU去做會有更好的結果
回到重點
同樣的,用示意圖與條列步驟方式快速瞭解一下流程:
1.將原始影像轉為灰階
最一開始的Gray Level就不提了,單純的灰階化!
雖然這個作法似乎能套用到彩色圖上(?)
2.對灰階影像調整Levels(色階),得到調整後的影像L
色階的功能是在於重新調整影像亮暗部份的分配
(一般來說其實多半都是用"曲線")
Photoshop的介面長這樣:
Photoshop中,中間的比例值範圍為9.99~0.01
為了方便,程式實作上就設為0.0~1.0
假設高低值分別是H、L,中間比例為m,因此中間值M = ((H - L) * m),I為輸入的Intensity(灰階值)
那處理結果f(I)以公式表示為:
以程式的效能考量,一次把Intensity 0~255的對應的f(I)都先建在array中
在使用上就可以以Lookup table的方式快速得到對應的值
此部份的程式碼如下:
調整完後的結果如下:
3.對影像I套入Posterize(色調分離)得到影像G
簡單來說,所謂的Posterize就是將原先0~255的值分成k等份的結果
若k = 2,就跟Threshold的結果一樣,會的到0, 255兩種值
若k = 3,則是分為0~84, 85~170, 171~255
以上是k = 7的結果,可以看到原先背景的細節通通被規類為相同顏色
這個效果主要就是用來製造海報效果的,詳細的可以參考wiki
程式的部份跟Levels一樣,都是採用Lookup table的方式實作,
篇幅考量,這裡就不列程式碼了
4.對影像I做Edge Detection(邊緣偵測)
說到Edge Detection,這就是一門可以多花個幾篇好好介紹的大學問了
簡單一句話描述就是:
講到這裡我相信不是對Image Processing有興趣的人應該半放棄了
不過不用擔心,我們有強大的OpenCV,它會幫我們省去掉裡頭的技術細節!
Canny Edge
一般來說講到Edge Detection,直覺想到的就是最多人使用的Canny Edge
它有著低錯誤率、最小化的邊緣等優點
另外因為知明度高,相對著網路上的資源就很多
OpenCV的官網就有一篇教學在講怎麼使用[連結]
先實際看一下程式碼的部份:
1:去除雜訊
為了去除雜訊,一般的都會先對影像做一次模糊運算,這裡與官方的教學一樣使用3x3的Kernel
2:計算Canny Edge
OpenCV提供的Canny Edge共有三個參數可以控制,分別是低、高 Threshold以及計算gradient時用的Kernel Size
Canny在判斷Edge時會用H, L兩個Threshold來判斷,這個技術稱為Hysteresis
gradient高於H的會被判斷為Edge,低於L會被視為Not Edge,介在H跟L中間的則處在一種待確定的狀態
若待確定的點與判斷為Edge的點有所相連,就會被視為Edge,反之,就被歸列為Not Edge
以下是做完Canny Edge的結果:
可以看到Canny Edge在尋找邊緣這件事情上處理的很好,大部份的Edge也有被連接上。
但做為漫畫風的描邊來說,這些Edge有點太細了
我的解法是透過Dilation(膨脹)的方式將線變粗,關於Dilation請參考往這邊走
至於結果好不好...我想在看到我還試了別的方法就知道結果並不是想像中的滿意
這點到下一個階段再討論
Sobel Edge
Sobel Edge是一種計算gradient,並根據計算的數值決定一個Threshold來找出Edge的方法
(其實OpenCV的Canny中,計算gradient就是用Sobel Edge)
因為沒有後續的處理,所以Sobel Edge找出來的相對起來就比較雜,也比較不連續
實際透過OpenCV的程式碼如下:
8~14:計算X方向與Y方向的Gradient
cv::Sobel,第一個參數為Input Image,第二個則為Output Image,第三個則為Output Image的格式
接下來三個為X order, Y order, Kernel Size,其中 Kernel只支援奇數
前兩者會決定是對那個計算Gradient,對X方向就設為1, 0,反之為0, 1
_fSobelScale與_fSobelDelta,則會影響計算出來的值。
最後的cv::BORDER_DEFAULT,則是用來決定影像周圍部份的計算方式
cv::convertScaleAbs,是將CV_16S的grad_x & grad_y 重新調整為CV_8UC,這樣才能繪出
以下是做完Sobel的結果:
5.將影像G減去邊緣偵測的結果就可得到影像M
由於Edge Detection的結果都是邊緣部份接近255, 非邊緣部份接近0
因為處理方式就是將做完Posterize的影像與Edge Detection的結果相減
這樣原圖中邊緣的部份就會被加黑
首先是Canny Edge的結果:(左邊是單純用Canny, 右邊則用Dilation加粗線條)
再來是Sobel Edge的結果:
純粹以結果來看,Sobel Edge是比較符合當初在製作時所想要的
Canny Edge因為處理出比較細膩的結果,反而有點像是硬加上去的描邊
Sobel Edge則就相較是把整體所有細小的邊緣都加強了
(當然也可能是我個人的參數調整不當)
6.對影像M進行Smart Blur(智慧模糊)
終於來到最後一步的Smart Blur,這是當時卡最久的一關
因為並沒有找到哪個模糊演算法真的叫做這個名字XD
花了許多時間Search才發現,最接近的應該是Bilateral Filter(雙通濾波器)
又是一個可以解釋上好幾篇幅的東西...這裡就交給前人吧!感謝逍遙文工作室
簡單描述一下,它是一種可以保持邊緣,模糊內容的模糊演算法
這點滿符合我們在漫畫風的需求,"整片的網點填色+清楚的黑邊邊緣"
程式碼的部份,再次感謝偉大的OpenCV:
關於bilateralFilter的參數,筆者是參考網路上其他人的用法來改的(當初沒有記下連結Orz)
筆者只能肯定,這樣設定的話_iBulrLevel要大於3左右才開始有效果
但是到6~7左右,整個fps會大幅掉落
詳細介紹可以參考OpenCV的官方文件
以下是Sobel Edge套入Bilateral Filter的結果,_iBlurLevel = 6:
可以看到背景上細小的雜訊都不見了,長頸路臉上也更均勻,但邊緣的銳利依然在
完整程式碼的部份請拉到最下面喔!!
總結:
相信明眼人都看得出來,
上集的結果雖然沒那麼漂亮,但處理的部份相對單純,需要調整的參數也不多
較為適合做Real-time的呈現
下集的作法,結果好上許多
但套用上最後的Smart Blur,大約只有10~15 fps,應該還要花上許多功夫去做效能調整
不過最主要的,還是這之中有太多參數要調整
光是Level的調整就要花上許多功夫了
因此比較適合做單張的運算
===========分隔線===========
總算完成這個系列了(也不過上下兩集)
原本其實打算專心解決Kinect V2,但想到"黑白吧!長頸鹿!"背後的程式都快要滿週年了
就下定決定好好的寫完它
中間就像是影像處理小複習一樣的感覺
每次這種時候都會開始後悔當初不好好上課,實作也沒有好好搞懂
但...人生嘛
===========分隔線===========
完整的程式碼:
就結果來看,並不像上次一樣是純粹的黑白影像,比較像是貼上網點之後的結果吧!!
只不過,跟上次那種隨便跑起來都60fps的情況比起來
這次的部份功能是會有效能問題要另外克服的!
相信丟到GPU去做會有更好的結果
回到重點
同樣的,用示意圖與條列步驟方式快速瞭解一下流程:
- 將原始影像轉為灰階
- 對灰階影像調整Levels(色階),得到調整後的影像L
- 對影像I套入Posterize(色調分離)得到影像G
- 對影像I做Edge Detection(邊緣偵測)
- 將影像G減去邊緣偵測的結果就可得到影像M
- 對影像M進行Smart Blur(智慧模糊)
1.將原始影像轉為灰階
最一開始的Gray Level就不提了,單純的灰階化!
雖然這個作法似乎能套用到彩色圖上(?)
2.對灰階影像調整Levels(色階),得到調整後的影像L
色階的功能是在於重新調整影像亮暗部份的分配
(一般來說其實多半都是用"曲線")
Photoshop的介面長這樣:
Photoshop中,中間的比例值範圍為9.99~0.01
為了方便,程式實作上就設為0.0~1.0
假設高低值分別是H、L,中間比例為m,因此中間值M = ((H - L) * m),I為輸入的Intensity(灰階值)
那處理結果f(I)以公式表示為:
以程式的效能考量,一次把Intensity 0~255的對應的f(I)都先建在array中
在使用上就可以以Lookup table的方式快速得到對應的值
此部份的程式碼如下:
1: void testApp::UpdateLevelsLookupTable()
2: {
3: const uint8 ucMiddleLevel_ = (uint8)floor(abs(_ucLevelsHigh - _ucLevelsLow) * _fLevelsMiddle);
4: const uint8 ucMiddleToLow_ = ucMiddleLevel_ - _ucLevelsLow;
5: const uint8 ucHighMiddleMiddle_ = _ucLevelsHigh - ucMiddleLevel_;
6: for(int idx_ = 0; idx_ < 256; ++idx_)
7: {
8: if(idx_ <= _ucLevelsLow)
9: {
10: _aucLookupTable[idx_] = 0;
11: }
12: else if(idx_ >= _ucLevelsHigh)
13: {
14: _aucLookupTable[idx_] = 255;
15: }
16: else
17: {
18: if(idx_ < ucMiddleLevel_)
19: {
20: _aucLookupTable[idx_] = (uint8) floor((float)(idx_ - _ucLevelsLow)/ucMiddleToLow_ * 127);
21: }
22: else
23: {
24: _aucLookupTable[idx_] = (uint8) floor( (float)(idx_ - ucMiddleLevel_)/ucHighMiddleMiddle_ * 128 + 127);
25: }
26: }
27: }
28: }
調整完後的結果如下:
3.對影像I套入Posterize(色調分離)得到影像G
簡單來說,所謂的Posterize就是將原先0~255的值分成k等份的結果
若k = 2,就跟Threshold的結果一樣,會的到0, 255兩種值
若k = 3,則是分為0~84, 85~170, 171~255
以上是k = 7的結果,可以看到原先背景的細節通通被規類為相同顏色
這個效果主要就是用來製造海報效果的,詳細的可以參考wiki
程式的部份跟Levels一樣,都是採用Lookup table的方式實作,
篇幅考量,這裡就不列程式碼了
4.對影像I做Edge Detection(邊緣偵測)
說到Edge Detection,這就是一門可以多花個幾篇好好介紹的大學問了
簡單一句話描述就是:
尋找Intensity變化強烈的地方
一般來說都是透過gradient(梯度)來去判斷,因此就會牽扯到微分...講到這裡我相信不是對Image Processing有興趣的人應該半放棄了
不過不用擔心,我們有強大的OpenCV,它會幫我們省去掉裡頭的技術細節!
Canny Edge
一般來說講到Edge Detection,直覺想到的就是最多人使用的Canny Edge
它有著低錯誤率、最小化的邊緣等優點
另外因為知明度高,相對著網路上的資源就很多
OpenCV的官網就有一篇教學在講怎麼使用[連結]
先實際看一下程式碼的部份:
1: blur( oMatChangeLevelsImg_, oMatCanny_, cv::Size(3,3));
2: cv::Canny(oMatCanny_, oMatCanny_, _fCannyThreshold, _fCannyThreshold * _fThresholdRadio);
1:去除雜訊
為了去除雜訊,一般的都會先對影像做一次模糊運算,這裡與官方的教學一樣使用3x3的Kernel
2:計算Canny Edge
OpenCV提供的Canny Edge共有三個參數可以控制,分別是低、高 Threshold以及計算gradient時用的Kernel Size
Canny在判斷Edge時會用H, L兩個Threshold來判斷,這個技術稱為Hysteresis
gradient高於H的會被判斷為Edge,低於L會被視為Not Edge,介在H跟L中間的則處在一種待確定的狀態
若待確定的點與判斷為Edge的點有所相連,就會被視為Edge,反之,就被歸列為Not Edge
以下是做完Canny Edge的結果:
可以看到Canny Edge在尋找邊緣這件事情上處理的很好,大部份的Edge也有被連接上。
但做為漫畫風的描邊來說,這些Edge有點太細了
我的解法是透過Dilation(膨脹)的方式將線變粗,關於Dilation請參考往這邊走
至於結果好不好...我想在看到我還試了別的方法就知道結果並不是想像中的滿意
這點到下一個階段再討論
Sobel Edge
Sobel Edge是一種計算gradient,並根據計算的數值決定一個Threshold來找出Edge的方法
(其實OpenCV的Canny中,計算gradient就是用Sobel Edge)
因為沒有後續的處理,所以Sobel Edge找出來的相對起來就比較雜,也比較不連續
實際透過OpenCV的程式碼如下:
1: void testApp::SobelEdgeDetection(cv::Mat& oMat)
2: {
3: int ddepth = CV_16S;
4:
5: // Generate grad_x and grad_y
6: cv::Mat grad_x, grad_y;
7: cv::Mat abs_grad_x, abs_grad_y;
8:
9: // Gradient X
10: cv::Sobel( oMat, grad_x, ddepth, 1, 0, 3, _fSobelScale, _fSobelDelta, cv::BORDER_DEFAULT );
11: cv::convertScaleAbs( grad_x, abs_grad_x );
12:
13: // Gradient Y
14: Sobel( oMat, grad_y, ddepth, 0, 1, 3, _fSobelScale, _fSobelDelta, cv::BORDER_DEFAULT );
15: convertScaleAbs( grad_y, abs_grad_y );
16:
17: // Total Gradient (approximate)
18: cv::addWeighted( abs_grad_x, 0.5, abs_grad_y, 0.5, 0, oMat );
19: }
8~14:計算X方向與Y方向的Gradient
cv::Sobel,第一個參數為Input Image,第二個則為Output Image,第三個則為Output Image的格式
接下來三個為X order, Y order, Kernel Size,其中 Kernel只支援奇數
前兩者會決定是對那個計算Gradient,對X方向就設為1, 0,反之為0, 1
_fSobelScale與_fSobelDelta,則會影響計算出來的值。
最後的cv::BORDER_DEFAULT,則是用來決定影像周圍部份的計算方式
cv::convertScaleAbs,是將CV_16S的grad_x & grad_y 重新調整為CV_8UC,這樣才能繪出
以下是做完Sobel的結果:
5.將影像G減去邊緣偵測的結果就可得到影像M
由於Edge Detection的結果都是邊緣部份接近255, 非邊緣部份接近0
因為處理方式就是將做完Posterize的影像與Edge Detection的結果相減
這樣原圖中邊緣的部份就會被加黑
首先是Canny Edge的結果:(左邊是單純用Canny, 右邊則用Dilation加粗線條)
再來是Sobel Edge的結果:
純粹以結果來看,Sobel Edge是比較符合當初在製作時所想要的
Canny Edge因為處理出比較細膩的結果,反而有點像是硬加上去的描邊
Sobel Edge則就相較是把整體所有細小的邊緣都加強了
(當然也可能是我個人的參數調整不當)
6.對影像M進行Smart Blur(智慧模糊)
終於來到最後一步的Smart Blur,這是當時卡最久的一關
因為並沒有找到哪個模糊演算法真的叫做這個名字XD
花了許多時間Search才發現,最接近的應該是Bilateral Filter(雙通濾波器)
又是一個可以解釋上好幾篇幅的東西...這裡就交給前人吧!感謝逍遙文工作室
簡單描述一下,它是一種可以保持邊緣,模糊內容的模糊演算法
這點滿符合我們在漫畫風的需求,"整片的網點填色+清楚的黑邊邊緣"
程式碼的部份,再次感謝偉大的OpenCV:
1: cv::Mat oMatSmoothPosterResult_;
2: int iTmp_ = _iBlurLevel * 2 + 1;
3: cv::bilateralFilter(oMatPosterResult_, oMatSmoothPosterResult_, iTmp_, iTmp_*2, iTmp_ /2);
關於bilateralFilter的參數,筆者是參考網路上其他人的用法來改的(當初沒有記下連結Orz)
筆者只能肯定,這樣設定的話_iBulrLevel要大於3左右才開始有效果
但是到6~7左右,整個fps會大幅掉落
詳細介紹可以參考OpenCV的官方文件
以下是Sobel Edge套入Bilateral Filter的結果,_iBlurLevel = 6:
可以看到背景上細小的雜訊都不見了,長頸路臉上也更均勻,但邊緣的銳利依然在
完整程式碼的部份請拉到最下面喔!!
總結:
相信明眼人都看得出來,
上集的結果雖然沒那麼漂亮,但處理的部份相對單純,需要調整的參數也不多
較為適合做Real-time的呈現
下集的作法,結果好上許多
但套用上最後的Smart Blur,大約只有10~15 fps,應該還要花上許多功夫去做效能調整
不過最主要的,還是這之中有太多參數要調整
光是Level的調整就要花上許多功夫了
因此比較適合做單張的運算
===========分隔線===========
總算完成這個系列了(也不過上下兩集)
原本其實打算專心解決Kinect V2,但想到"黑白吧!長頸鹿!"背後的程式都快要滿週年了
就下定決定好好的寫完它
中間就像是影像處理小複習一樣的感覺
每次這種時候都會開始後悔當初不好好上課,實作也沒有好好搞懂
但...人生嘛
===========分隔線===========
完整的程式碼:
1: void testApp::Dilation(cv::Mat& oResult)
2: {
3: cv::Mat pMatElement_ = cv::getStructuringElement( cv::MORPH_ELLIPSE, cv::Size( 2*_iDilationSize + 1, 2*_iDilationSize+1 ), cv::Point( _iDilationSize, _iDilationSize ) );
4: dilate( oResult, oResult, pMatElement_ );
5: }
6:
7: void testApp::SobelEdgeDetection(cv::Mat& oMat)
8: {
9: int ddepth = CV_16S;
10:
11: // Generate grad_x and grad_y
12: cv::Mat grad_x, grad_y;
13: cv::Mat abs_grad_x, abs_grad_y;
14:
15: // Gradient X
16: cv::Sobel( oMat, grad_x, ddepth, 1, 0, 3, _fSobelScale, _fSobelDelta, cv::BORDER_DEFAULT );
17: cv::convertScaleAbs( grad_x, abs_grad_x );
18:
19: // Gradient Y
20: Sobel( oMat, grad_y, ddepth, 0, 1, 3, _fSobelScale, _fSobelDelta, cv::BORDER_DEFAULT );
21: convertScaleAbs( grad_y, abs_grad_y );
22:
23: // Total Gradient (approximate)
24: cv::addWeighted( abs_grad_x, 0.5, abs_grad_y, 0.5, 0, oMat );
25: }
26:
27: void testApp::ComicFilterVerJordan()
28: {
29: //RGB to Gray
30: cv::Mat oMatGrayImg_;
31: cv::cvtColor(_oImage, oMatGrayImg_, CV_RGB2GRAY);
32:
33: //Change Level
34: cv::Mat oMatChangeLevelsImg_ = oMatGrayImg_.clone();
35: for(int idxY_ = 0 ; idxY_ < oMatChangeLevelsImg_.rows; ++idxY_)
36: {
37: for(int idxX_ = 0; idxX_ < oMatChangeLevelsImg_.cols; ++idxX_)
38: {
39: uint8 ucSourceValue_ = oMatChangeLevelsImg_.at<uint8>(idxY_, idxX_);
40: oMatChangeLevelsImg_.at<uint8>(idxY_, idxX_) = _aucLookupTable[ucSourceValue_];
41: }
42: }
43:
44: //Poster edges
45: //posterization
46: cv::Mat oMatPosterization_ = oMatChangeLevelsImg_.clone();
47: for(int idxY_ = 0 ; idxY_ < oMatPosterization_.rows; ++idxY_)
48: {
49: for(int idxX_ = 0; idxX_ < oMatPosterization_.cols; ++idxX_)
50: {
51: uint8 ucSourceValue_ = oMatPosterization_.at<uint8>(idxY_, idxX_);
52: oMatPosterization_.at<uint8>(idxY_, idxX_) = _aucPosterizationLookupTable[ucSourceValue_];
53: }
54: }
55:
56: cv::Mat oMatPosterResult_ = oMatPosterization_;
57: //Find Edge
58: if(_bUseCannyEdge)
59: {
60: cv::Mat oMatCanny_;
61: blur( oMatChangeLevelsImg_, oMatCanny_, cv::Size(3,3));
62: cv::Canny(oMatCanny_, oMatCanny_, _dCannyThreshold, _dCannyThreshold * _dThresholdRadio);
63: this->Dilation(oMatCanny_);
64: oMatPosterResult_ -= oMatCanny_;
65: }
66:
67: if(_bUseSobelEdge)
68: {
69: cv::Mat oMatSobel_ = oMatChangeLevelsImg_.clone();
70: this->SobelEdgeDetection(oMatSobel_);
71: oMatPosterResult_ -= oMatSobel_;
72: }
73:
74: //Smart blur
75: if(_bUseBlur)
76: {
77: cv::Mat oMatSmoothPosterResult_;
78: int iTmp_ = _iBlurLevel * 2 + 1;
79: cv::bilateralFilter(oMatPosterResult_, oMatSmoothPosterResult_, iTmp_, iTmp_*2, iTmp_ /2);
80: _oResult = oMatSmoothPosterResult_;
81: }
82: else
83: {
84: _oResult = oMatPosterResult_;
85: }
86: }
沒有留言:
張貼留言