相關論文的鏈接:Combining Sketch and Tone for Pencil Drawing Production
第一次看《Combining Sketch and Tone for Pencil Drawing Production》一文是在兩年前,隨意看了一下,覺得論文裏的公式比較多,以爲實現有一定的難度,沒有去細究,最近在作者主頁上看到有 [code of direction classification] 部分代碼,下載後覺得還是有自己實現的可能,下面記錄下自己實現過程中的一些體會和心得。
鉛筆畫其實一直是一個比較難以獲得較爲理想效果的算法,我看到的論文裏這篇文章應該是說相當優秀的。總的來說,其算法分爲兩個步驟:
1、Line Drawing with Strokes 得到一幅圖 S。
2、Tone Mapping 得到另外一副圖T。
3、得到最終結果 R = S * T;
應該說第一步決定了最終的效果,作者通過以下四個步驟得到S圖。
(1)、對原圖進行邊緣檢測,作者論文給出的公式是:
按照這個公式實現的效果實際上檢測的效果很弱,我認爲作者真正意義上可能不是使用的改公式,因爲這一步對最終效果的影響很大, 我採用了一些其他能夠更好的檢測出效果的邊緣檢測算法,如果Sobel或者PS裏FindEges之類的算法。
作者認爲這個公式得到的結果含有太多的噪音並且邊緣部分的線條很多不連續,因此提出繼續一下幾個步驟得到更穩定的效果。
(2) 對得到的G進行8個方向的卷積,卷積核爲沿指定的方向爲1,其他的值均爲0(實際上考慮抗鋸齒問題,用了雙線性插值得到卷積核的),卷積核的大小論文提出爲圖像寬度或高度的1/30(這個我覺得有點不行,當太大時,會有明顯的不合理線條出現),具體公式如下:
在論文給出的相關代碼中,有如下部分:
%% convolution kernel with horizontal direction
kerRef = zeros(ks*2+1);
kerRef(ks+1,:) = 1;
%% classification
response = zeros(H,W,dirNum);
for ii = 0 : (dirNum-1)
ker = imrotate(kerRef, ii*180/dirNum, 'bilinear', 'crop');
response(:,:,ii+1) = conv2(imEdge, ker, 'same');
end
其實這裏的卷積就是按照指定的角度的運動模糊,我們可以用matlab的代碼來進行驗證:
比如當角度爲22.5(ii = 1),核的大小爲5(ks = 5)時,按照上面的代碼得到的ker變量爲:
歸一化後的結果爲:
而對應的運動模糊的卷積矩陣爲(對應的matlab代碼爲: H = fspecial('motion', 2*5+1, 22.5):
可見只有很小的差別。
(3) 得到各個方向的卷積結果後,對每一個像素點,具有最大的卷積值的那一個方向的響應設置爲G,而其他方向的響應設置爲0,原文的話語是:
The classification is performed by selecting the maximum value among the responses in all directions and is written as
我覺得有點不可思議的是上面語句說的是最大值,下面給出的公式確是最小值,這難道是大家的筆誤。(實際上應該是最大值的)。
還有注意的是這裏的G(p)是指公式(1)中的G。
(4)對得到的各個方面的響應再次進行方向卷積,即:
論文中提出要對這個結果進行卷積並進行反相處理得到結果S,這個其實就看你自己的編碼方式了。
這裏給出以上4個步驟一些中間結果:
原圖 邊緣檢測圖 22.5度的卷積圖 22.5度的響應圖 中間結果圖S
第二部的Tone Mapping 實際上也是由兩個步驟來實現的。
(1)直方圖匹配。
論文中並沒有這樣起小標題,而是用了較大的篇幅說了一堆事情,我總結一句話就是,根據對大量的手工繪製的鉛筆畫圖像數據的觀察和分析,其直方圖的分佈和我們拍攝的圖像有很大的不同,但是都成一定的規律,這個規律可以用一定的經驗公式來表達。因此,我們可以設定一個固定的直方圖,然後將圖像自身的直方圖映射到這個直方圖,作爲結果。
簡單的闡述下過程吧。借作者論文中的一副圖像來做說明。在下圖中,(a)是一副手工繪製的鉛筆畫,(b)圖是硬性劃分的陰影、中間調及高光圖像,(c)圖分別對應陰影、中間調及高光的直方圖分佈。可以看出,陰影部分基本成正態分佈,中間調大致爲均勻分佈,而高光部分成拉普拉斯分佈。因此,作者對三個部分分別構建了三個函數來模擬曲線。
a、高光部分。
在人手工繪製的鉛筆畫中,由於紙張一般都是白色,因此,高光佔有的比例實際上肯定是非常大的,這在直方圖中反應就是在接近色階255時分佈曲線越陡峭。作者提出以下函數作爲這部分的分佈曲線。
而關於中間調,則用一截水平分佈的線條來模擬:
而暗調部分則表明了圖像深度的變化,用一個高斯曲線來模擬:
以上公式對於不同的鉛筆畫來說,各部分的權重是不一樣,因此作者提出了一個綜合的公式來獲取最終的鉛筆畫對應的直方圖:
根據不同的需要,可以調節不同的權重係數來達到不同的混合效果。
作者根據經驗,提出了一組數據:
如果權重都相同,三部分的曲線如下圖所示:
可見暗調部分的比例和人工繪製的不協調,如果按照上表中論文給出的數據,得到的最終混合直方圖效果如下圖:
似乎也和人工的結果不一致,因此我認爲此處也是論文的一個筆誤或者錯誤,w1和W3的值很明顯弄反了,即W1應該爲52,W3爲11,修改後的直方圖爲:
基本和人工繪製的一致了,同時注意到上述曲線有兩個鉅變之處,實際處理時需要對曲線進行一定程度的平滑最好。
附繪製曲線的matlab代碼:
a=1:255;
p1 = 1/9*exp(-(255-a)/9);
p3=1/sqrt(2*pi*11) * exp(-(a-80).*(a-80) / (2.0*11*11));
p2= 1:255;
p2(:)=1/(225-105);
p2(1:105)=0;
p2(225:255)=0;
p = 0.52 * p1 + 0.37 * p2 + 0.11 * p3;
plot(a,p)
接下來的工作就是進行直方圖匹配,這方面的資料可以參考何斌那本VC++的數字圖像處理數,裏面有SML和GML兩種匹配方式。
(2)紋理渲染
這一部分論文說的也很簡單,就是求解一個方程:
這不是我擅長的東西,有興趣的朋友可能要自己研究下,我也沒有實現它,由這一步得到中間結果T。
那最後一步就是將S 和 T相乘,就類似於PS中的正片疊底 混合算法。
以上的一些操作都是針對灰度圖像,對於彩色圖像,如果直接將三通道分開,然後分別調用灰度算法,再合成這樣處理, 是有問題的,出來的結果很不理想,這主要是由於各通道在進行Line Drawing with Strokes時所獲得的線條方向不太可能完全一致,導致合成後偏色,解決的方式有多種,比如將RGB轉換到HSL空間,然後對L分量進行處理,然後在轉換到RGB空間,或者借用LAB空間作爲中轉平臺也是可以的。
下面貼出我的C++部分的核心代碼(並不能直接運行,大概體現了算法的思路):
extern IS_RET EdgeCoarse(TMatrix *Src, TMatrix *Dest); extern IS_RET MotionBlur(TMatrix *Src, TMatrix *Dest, int Length, float Angle, EdgeMode Edge); extern IS_RET SMLHistgramMaping(TMatrix *Src, TMatrix *Dest, int* HistgramB, int *HistgramG, int *HistgramR); extern IS_RET BlendImage(TMatrix *Base, TMatrix *Mixed, TMatrix *Result, BlendMode BlendOp, int Opacity); extern IS_RET GuassBlur(TMatrix *Src, TMatrix *Dest, float Radius); extern IS_RET DecolorizationWithContrastPreserved(TMatrix *Src, TMatrix *Dest, int Level = 64, float Sigma = 0.05); IS_RET __stdcall PencilDrawing(TMatrix *Src, TMatrix *Dest, int LineLength) { if (Src == NULL || Dest == NULL) return IS_RET_ERR_NULLREFERENCE; if (Src->Data == NULL || Dest->Data == NULL) return IS_RET_ERR_NULLREFERENCE; if (Src->Width != Dest->Width || Src->Height != Dest->Height || Src->Channel != Dest->Channel || Src->Depth != Dest->Depth || Src->WidthStep != Dest->WidthStep) return IS_RET_ERR_PARAMISMATCH; if (Src->Depth != IS_DEPTH_8U || Dest->Depth != IS_DEPTH_8U) return IS_RET_ERR_NOTSUPPORTED; IS_RET Ret = IS_RET_OK; if (Src->Data == Dest->Data) { TMatrix *Clone = NULL; Ret = IS_CloneMatrix(Src, &Clone); if (Ret != IS_RET_OK) return Ret; Ret = PencilDrawing(Clone, Dest, LineLength); IS_FreeMatrix(&Clone); return Ret; } if (Src->Channel == 1) { int Amount = 2 * LineLength + 1; int Width = Src->Width, Height = Src->Height; int X, Y, Z, Index, MaxValue, Sum; float Value; unsigned char *LinePS, *LinePD; TMatrix **Response = (TMatrix **)malloc(8 * sizeof(TMatrix)); TMatrix *ImageEdge = NULL; Ret = IS_CreateMatrix(Width, Height, IS_DEPTH_8U, 1, &ImageEdge); TMatrix *LineShape = NULL; Ret = IS_CreateMatrix(Width, Height, IS_DEPTH_8U, 1, &LineShape); if (Ret != IS_RET_OK) goto Done8; Ret = GuassBlur(Src, ImageEdge, 1); // 去除點噪音 if (Ret != IS_RET_OK) goto Done8; Ret = EdgeCoarse(ImageEdge, ImageEdge); // 兩個參數相同對速度無影響,對應論文公式1 if (Ret != IS_RET_OK) goto Done8; for (Z = 0; Z < 8; Z++) { Ret = IS_CreateMatrix(Width, Height, IS_DEPTH_8U, 1, &Response[Z]); if (Ret != IS_RET_OK) goto Done8; Ret = MotionBlur(ImageEdge, Response[Z], Amount, Z * 180.0 / 8, EdgeMode::Smear); // 對應論文公式2 if (Ret != IS_RET_OK) goto Done8; // 各個方向卷積 } for (Y = 0; Y < Height; Y++) { LinePS = ImageEdge->Data + Y * ImageEdge->WidthStep; for (X = 0; X < Width; X++) { MaxValue = 0; for (Z = 0; Z < 8; Z++) { LinePD = (Response[Z]->Data + Y * Response[Z]->WidthStep + X); if (MaxValue < LinePD[0]) { Index = Z; MaxValue = LinePD[0]; } } for (Z = 0; Z < 8; Z++) { LinePD = (Response[Z]->Data + Y * Response[Z]->WidthStep); // 對應公式3 if (Z == Index) LinePD[X] = LinePS[X]; else LinePD[X] = 0; } } } for (Z = 0; Z < 8; Z++) { MotionBlur(Response[Z], Response[Z], Amount, Z * 180.0 / 8, EdgeMode::Smear); // 對應公式S' } for (Y = 0; Y < Height; Y++) { LinePD = LineShape->Data + Y * LineShape->WidthStep; for (X = 0; X < Width; X++) { Sum = 0; for (Z = 0; Z < 8; Z++) { LinePS = (Response[Z]->Data + Y * Response[Z]->WidthStep); Sum += LinePS[X]; } LinePD[X] = (255 - ClampToByte(Sum) * 0.5); // The final pencil stroke map S is obtained by inverting pixel values and mapping them to [0,1]. } } float *HistgramF = (float *)IS_AllocMemory(256 * sizeof(float)); float *HistgramFC = (float *)IS_AllocMemory(256 * sizeof(float)); int *Histgram = (int *)IS_AllocMemory(256 * sizeof(int)); int Ua = 105, Ub = 225, Mud = 90, DeltaB = 9, DeltaD = 11, Omega1 = 76, Omega2 = 22, Omega3 = 2, Iter = 5; for (Y = 0; Y < 256; Y++) { if (Y < Ua || Y > Ub) // 表1中的參數 Value = 0; else Value = 1.0 / (Ub - Ua); HistgramF[Y] = (Omega2 * Value + 1.0 / DeltaB * exp(-(255.0 - Y) / DeltaB) * Omega1 + 1.0 /sqrt(2 * PI * 11) * exp(-(Y - Mud) * (Y - Mud) / (2.0 * DeltaD * DeltaD)) * Omega3) * 0.01; HistgramFC[Y] = HistgramF[Y]; // 拷貝一個備份 } for (Z = 0; Z < Iter; Z++) // 這樣的直方圖並不平滑,做一點平滑處理 { HistgramFC[0] = (HistgramF[0] + HistgramF[1]) / 2; // 第一點 for (Y = 1; Y < 255; Y++) HistgramFC[Y] = (HistgramF[Y - 1] + HistgramF[Y] + HistgramF[Y + 1]) / 3; // 中間的點 HistgramFC[255] = (HistgramF[254] + HistgramF[255]) / 2; // 最後一點 memcpy(HistgramF, HistgramFC, 256 * sizeof(float)); } for (Y = 0; Y < 256; Y++) Histgram[Y] = HistgramF[Y] * Width * Height; TMatrix *ToneMap = NULL; Ret = IS_CreateMatrix(Width, Height, IS_DEPTH_8U, 1, &ToneMap); if (Ret != IS_RET_OK) goto Done8; Ret = GuassBlur(Src, ToneMap, 1); // Initially, the grayscale input I is slightly Gaussian smoothed. if (Ret != IS_RET_OK) goto Done8; SMLHistgramMaping(ToneMap, ToneMap, Histgram, Histgram, Histgram); // we adjust the tone maps using simple histogram matching in all the three layers and superpose them again. BlendImage(ToneMap, LineShape, Dest, BlendMode::Multiply, 255); // We combine the pencil stroke S and tonal texture T by multiplying the stroke and texture values for each pixel to accentuate important contours Done8: IS_FreeMatrix(&ImageEdge); IS_FreeMatrix(&ToneMap); IS_FreeMatrix(&LineShape); IS_FreeMemory(HistgramF); IS_FreeMemory(HistgramFC); IS_FreeMemory(Histgram); for (Z = 0; Z < 8; Z++)IS_FreeMatrix(&Response[Z]); IS_FreeMemory(*Response); } else { unsigned char *LinePS, *LinePD; int X, Y, Z, Width = Src->Width, Height = Src->Height; TMatrix *Gray = NULL, *GrayC = NULL; IS_RET Ret = IS_CreateMatrix(Src->Width, Src->Height, IS_DEPTH_8U, 1, &Gray); if (Ret != IS_RET_OK) goto Done; Ret = IS_CreateMatrix(Src->Width, Src->Height, IS_DEPTH_8U, Src->Channel, &GrayC); Ret = DecolorizationWithContrastPreserved(Src, Gray); if (Ret != IS_RET_OK) goto Done; PencilDrawing(Gray, Gray, LineLength); for (Y = 0; Y < Height; Y++) { LinePS = Gray->Data + Y * Gray->WidthStep; LinePD = GrayC->Data + Y * GrayC->WidthStep; for (X = 0; X < Width; X++) // 恢復V分量 { LinePD[0] = LinePS[X]; LinePD[1] = LinePS[X]; LinePD[2] = LinePS[X]; LinePD += 3; } } BlendImage(Dest, GrayC, Dest, BlendMode::Luminosity, 255); Done: IS_FreeMatrix(&Gray); IS_FreeMatrix(&GrayC); } }
貼一些處理的效果:
原圖 處理結果圖
和論文裏處理的效果還有不小的差距,這主要是由於最後一步紋理沒有做,然後就是還有細節有問題,不過也算有點收穫。
提供一個測試小工具: http://files.cnblogs.com/files/Imageshop/PencilDrawing.rar