文章出處

CSharpGL(24)用ComputeShader實現一個簡單的圖像邊緣檢測功能

效果圖

這是紅寶書里的例子,在這個例子中,下述功能全部登場,因此這個例子可作為使用Compute Shader的典型示例。

用imageLoad從紋理中讀取數據。

用imageStore將數據寫入紋理。

用vertex/fragment shader顯示出compute shader的計算結果。

下面是3個測試用例。

下載

CSharpGL已在GitHub開源,歡迎對OpenGL有興趣的同學加入(https://github.com/bitzhuwei/CSharpGL

Image Processing

渲染結果

先解決簡單的問題:把compute shader計算后的結果(一個紋理)顯示出來。這用到如下的vertex shader和fragment shader,非常簡單。

 1 #version 430 core
 2 
 3 in vec3 vert;
 4 in vec2 uv;
 5 out vec2 passUV;
 6 uniform mat4 mvp;
 7 
 8 void main(void)
 9 {
10     gl_Position = mvp * vec4(vert, 1.0f);
11     passUV = uv;
12 }
vertex shader
 1 #version 430 core
 2 
 3 layout (location = 0) out vec4 color;
 4 in vec2 passUV;
 5 layout (binding = 0) uniform sampler2D output_image;
 6 
 7 void main(void)
 8 {
 9     color = texture(output_image, passUV);
10 }
fragment shader

其模型用一個四邊形即可。

邊緣檢測算法

理論

在一個圖像上,什么是邊緣?如果相鄰的兩個像素顏色差別很大,就可以算是邊緣。差別越大,就越能被視作邊緣。

這個例子實現了一個簡單的邊緣檢測算法,使用一個邊緣檢測濾波器對輸入的圖像(作為紋理)進行卷積操作。這個例子中的濾波器是可分離的(separable filter),就是說,可以對多維度空間的各個維度都單獨處理。這里,我們將它應用到2維圖像上,首先對水平維度進行處理,然后對垂直維度進行處理。

為了實現這個算法,compute shader的每個請求都要處理輸入圖像的一個像素。它需要讀取輸入圖像的內容,然后減去該像素旁邊的采樣值。這意味著一個請求要從輸入圖像中讀取2次。

為避免多于的內存訪問,這里用一個shared數組來存儲輸入圖形的一行。我們在每個請求中讀取輸入圖像的目標像素,然后存儲到shared數組。當所有請求都讀取輸入圖像后,這個shared數組就含有輸入圖像當前行的所有像素值。之后每個請求都可以直接從此shared數組中讀取像素值,這個讀取速度是非常快的。

Compute Shader

實現邊緣檢測算法的compute shader如下。

 1 #version 430 core
 2 // 最大支持寬度為512的圖像
 3 layout (local_size_x = 512, local_size_y = 1, local_size_z = 1) in;
 4 // 要進行檢測的圖像
 5 layout (rgba32f, binding = 0) uniform image2D input_image;
 6 // 檢測結果
 7 layout (rgba32f, binding = 1) uniform image2D output_image;
 8 // 共享數組,存儲當前行的像素
 9 shared vec4 scanline[512];
10 
11 void main(void)
12 {
13     // 請求的位置
14     ivec2 pos = ivec2(gl_GlobalInvocationID.xy);
15     // 讀取當前位置的像素
16     scanline[pos.x] = imageLoad(input_image, pos);
17     // 等待所有請求都走到這里
18     barrier();
19     // 計算邊緣值,存儲到output_image
20     vec4 result = scanline[min(pos.x + 1, 511)] - scanline[max(pos.x - 1, 0)];
21     // pos.yx:把輸出圖像翻轉,這樣就可以使用同一compute shader進行2維卷積。
22     imageStore(output_image, pos.yx, result);
23 }

執行

可以看到,上面的compute shader的一個local work group只能處理圖像的一個維度上的一行。這一點由這一行代碼決定:

layout (local_size_x = 512, local_size_y = 1, local_size_z = 1) in;

為了處理此維度上的全部行,在調用此compute shader時要這樣:

GL.GetDelegateFor<GL.glDispatchCompute>()(1, 512, 1);

即指定在Y軸上執行512個local work group。這樣就完成了在X軸維度上的計算。這時我們得到了一個中間圖像intermediate_image。

★從這里可以看到設定local work group和global work group的理由:shader里的local_size_*大小有限,借助glDispatchCompute才能實現更大規模的計算,且更靈活。

然后要對這個intermediate_image的Y軸維度執行算法。這時你注意到,在上面的compute shader里,我們用

imageStore(output_image, pos.yx, result);

而不是

imageStore(output_image, pos.xy, result);

這是把原圖翻轉了一下。因此,如果繼續對intermediate_image執行上面的compute shader,實際上就實現了對原圖在第二個維度上執行此算法。

因此總的計算過程如下。

 1 computeProgram.Bind();
 2 glBindImageTexture(0, input_image[0], 0, false, 0, GL.GL_READ_WRITE, GL.GL_RGBA32F);
 3 glBindImageTexture(1, intermediate_image[0], 0, false, 0, GL.GL_READ_WRITE, GL.GL_RGBA32F);
 4 // 在X軸上執行邊緣檢測算法
 5 glDispatchCompute(1, 512, 1);
 6 // 確保所有compute shader請求都執行完成
 7 glMemoryBarrier(GL.GL_SHADER_IMAGE_ACCESS_BARRIER_BIT);
 8 
 9 glBindImageTexture(0, intermediate_image[0], 0, false, 0, GL.GL_READ_WRITE, GL.GL_RGBA32F);
10 glBindImageTexture(1, output_image[0], 0, false, 0, GL.GL_READ_WRITE, GL.GL_RGBA32F);
11 // 在Y軸上執行邊緣檢測算法
12 glDispatchCompute(1, 512, 1);
13 glMemoryBarrier(GL.GL_SHADER_IMAGE_ACCESS_BARRIER_BIT);

總結

經過這個例子,開始正視創建紋理過程中的各項參數。

原CSharpGL的其他功能(3ds解析器、TTF2Bmp、CSSL等),我將逐步加入新CSharpGL。

歡迎對OpenGL有興趣的同學關注(https://github.com/bitzhuwei/CSharpGL


文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()