2013年12月11日 星期三

OpenCL 模擬 iOS7 毛玻璃效果(frosted glass)

最近筆者將手機作業系統升級到 iOS7, 發現 iOS7 的介面有一個亮點 : 毛玻璃(frosted glass)效果, 透過毛玻璃所在的 layer, 可以隱約看到下層 layer 的內容. 如圖一所示.

圖一
搜尋一些資訊後發現 不少人在討論這個效果, 因為 apple 使用的方法無法確切得知, 所以筆者就以自己的想法實作.  首先 需要一個用以柔化影像的 low-pass filter. 筆者計畫使用 Gaussian filter 並且使用 GPU 做 Gaussian filter convolution. 關於 Gaussian filter, 可以參考下面兩個在 wiki 的介紹:
Gaussian kernel 的大小由 sigma 決定, Host 端的程式根據 sigma 決定 kernel size, 根據 gaussian filter 的定義計算 mask 中的每個常數, 將 mask 儲存在 OpenCL 的 image object.    kernel size 以及 mask 會給 OpenCL kernel 進行 convolution 時使用. 用以計算 Gaussian mask 的程式片段如下:

void
CreateBlurMask(float sigma, int * maskSizePointer) 
{
int maskSize = (int)ceil(3.0f*sigma);
g_mask.resize((maskSize*2+1)*(maskSize*2+1));

float sum = 0.0f;
float* pmask = &g_mask[0];
pmask += maskSize*(maskSize*2+1) + maskSize;

for(int a = -maskSize; a < maskSize+1; a++)
{
for(int b = -maskSize; b < maskSize+1; b++) 
{
float temp = exp(-((float)(a*a+b*b) / (2*sigma*sigma)));
sum += temp;
pmask[a+b*(maskSize*2+1)] = temp;
}
}
// Normalize the mask
for(int i = 0; i < (maskSize*2+1)*(maskSize*2+1); i++)
g_mask[i] = g_mask[i] / sum;

*maskSizePointer = maskSize;
}

根據我前一篇文章 OpenCL介紹 作修改, 在 host 端程式代碼的初始化, 大致上增加了以下的部分.
調用 CreateBlurMask, 並且給定 sigma = 3.0

CreateBlurMask(3.0, &g_maskSize);

接著, 建立一個 image object, 並且將 Gaussian mask 的內容由 host 端的 memory 複製到 device 端的 memory:
imgObject[2] = clCreateImage2D(GPU_context, CL_MEM_READ_ONLY,                                                                  &mask_fmt, g_maskSize*2+1, 
                                                       g_maskSize*2+1, 0, NULL, NULL);

err = clEnqueueWriteImage(commandQueue, imgObject[2], CL_TRUE, origin, 
                                         mask_region, 0, 0, &g_mask[0], 0, NULL, NULL);

if(err != CL_SUCCESS) 
{
cleanUpOpenCL(GPU_context, commandQueue, program, kernel, 
         memoryObjects, numberOfMemoryObjects,
 imgObject, numberOfImgObjects);
return 0;

}

設定好送進 kernel function 的參數
clSetKernelArg(kernel, 2, sizeof(cl_mem), &imgObject[2]);  // source image
clSetKernelArg(kernel, 3, sizeof(cl_int), &g_maskSize);       //  mask Size


Host 端處理每個 frame 的流程如下:
(1) 產生一份原始影像 scaling down 成較低解析度後的結果.
(2) 讓GPU對 較低解析度的影像 進行 convolution.
(3) 讀回結果.
(4) CPU 將讀回的結果scaling up 成和原始影像一樣解析度.
(5) CPU 根據 ROI,  將scaling up後的影像和原始影像合成.

圖二

因為要 composition, 故需要產生一份 composition 使用的 mask. 此mask 在 ROI 內的值為 0xff,其餘部分皆為 0.

camera >> Img1;
Mat mask(Img1.rows, Img1.cols, CV_8UC1);
mask.setTo(0);

Mat roi(mask, Rect(140,100,340,260));
roi = 0xff;

composition 的代碼如下, ocl_img 是從讀回讀回並 scaling up 的結果. Img0 則是原始影像.
ocl_img *= 0.3;
ocl_img.copyTo( Img0, mask );

接下來看 Kernel Function 的內容:
__constant sampler_t sampler = CLK_NORMALIZED_COORDS_FALSE |
                               CLK_ADDRESS_CLAMP | 
                               CLK_FILTER_NEAREST;

__kernel void laplacian( read_only  image2d_t src_image,                         
                         write_only image2d_t dst_image,
                         read_only  image2d_t mask,
                         __private int maskSize )
{
int x = get_global_id(0);
int y = get_global_id(1);

int2 pos = {get_global_id(0), get_global_id(1)};


    // Collect neighbor values and multiply with Gaussian
    float4 fSum  = 0.0f;
    float4 fMask;
    
    float4 fColor;
    uint4  uiColor;    
    
    for(int a = -maskSize; a < maskSize+1; a++) 
    {
        for(int b = -maskSize; b < maskSize+1; b++) 
        {
            int2 coord = (int2)((a+maskSize, b+maskSize));
            uiColor = read_imageui(src_image, sampler, pos + (int2)(a,b));
            fColor  = convert_float4(uiColor);
            
            fMask   = read_imagef(mask, sampler, coord);
            
            fSum += fColor * fMask.x;
        }
    }
       
    uiColor = convert_uint4 (fSum);   
    write_imageui(dst_image, pos, uiColor);    
}
Kernel 這邊的代碼 根據 mask size 進行 convolution, 再將結果寫到 output image object.