Metal圖像處理——直方圖均衡化

2021-01-20 Cocoa開發者社區

本公眾號內容均為本號轉發,已儘可能註明出處。因未能核實來源或轉發內容圖片有權利瑕疵的,請及時聯繫本號,本號會第一時間進行修改或刪除。 QQ : 3442093904 


前言


Metal入門教程總結


正文

核心思路


首先,我們用直方圖來表示一張圖像:橫坐標代表的是顏色值,縱坐標代表的是該顏色值在圖像中出現次數。



如圖,對於某些圖像,可能出現顏色值集中分布在某個區間的情況。


直方圖均衡化(Histogram Equalization) ,指的是對圖像的顏色值進行重新分配,使得顏色值的分布更加均勻。


本文用compute shader對圖像的顏色值進行統計,然後計算得出映射關係,由fragment shader進行顏色映射處理。


效果展示



具體步驟


1、Metal的render管道、compute管道配置;


同前文,不再贅述,詳見Metal入門教程總結。


2、CPU進行直方圖均衡化處理;


2.1 把UIImage轉成Bytes;

2.2 顏色統計;

  
    Byte *color = (Byte *)spriteData;    for (int i = 0; i < width * height; ++i) {        for (int j = 0; j < LY_CHANNEL_NUM; ++j) {
            uint c = color[i * 4 + j];
            ++cpuColorBuffer.channel[j][c];
        }
    }




2.3 映射關係;


   int rgb[3][LY_CHANNEL_SIZE], sum = (int)(width * height);    int val[3] = {0};    // 顏色映射
    for (int i = 0; i < 3; ++i) {        for (int j = 0; j < LY_CHANNEL_SIZE; ++j) {
            val[i] += cpuColorBuffer.channel[i][j];
            rgb[i][j] = val[i] * 1.0 * (LY_CHANNEL_SIZE - 1) / sum;
        }
    }


2.4 顏色值修改;


   
    for (int i = 0; i < width * height; ++i) {        for (int j = 0; j < LY_CHANNEL_NUM; ++j) {
            uint c = color[i * 4 + j];
            color[i * 4 + j] = rgb[j][c];
        }
    }



3 GPU進行直方圖均衡化處理;



kernel voidgrayKernel(texture2d<float, access::read>  sourceTexture  [[textureLYKernelTextureIndexSource]], // 紋理輸入,
device LYColorBuffer &out [[buffer(LYKernelBufferIndexOutput)]], // 輸出的buffer
uint2                          grid         [[thread_position_in_grid]]) // 格子索引{    // 邊界保護

if(grid.x < sourceTexture.get_width() && grid.y < sourceTexture.get_height())
{
float4 color  = sourceTexture.read(grid); // 初始顏色

int3 rgb = int3(color.rgb * SIZE); // 乘以SIZE,得到[0, 255]的顏色值


// 顏色統計,每個像素點計一次

atomic_fetch_add_explicit(&out.channel[0][rgb.r], 1, memory_order_relaxed);

atomic_fetch_add_explicit(&out.channel[1][rgb.g], 1, memory_order_relaxed);
atomic_fetch_add_explicit(&out.channel[2][rgb.b], 1, memory_order_relaxed);
}
}</float, access::read>


atomic_fetch_add_explicit是用於在多線程進行數據操作,具體的函數解釋見這裡。



compute shader回調後,根據GPU統計的顏色分布結果,求出映射關係;


LYLocalBuffer *buffer = (LYLocalBuffer *)strongSelf.colorBuffer.contents; // GPU統計的結果
LYLocalBuffer *convertBuffer = self.convertBuffer.contents; // 顏色轉換的buffer
int sum = (int)(self.sourceTexture.width * self.sourceTexture.height); // 總的像素點
int val[3] = {0}; // 累計和
for (int i = 0; i < 3; ++i) {            for (int j = 0; j < LY_CHANNEL_SIZE; ++j) {
val[i] += buffer->channel[i][j]; // 當前[0, j]累計出現的總次數
convertBuffer->channel[i][j] = val[i] * 1.0 * (LY_CHANNEL_SIZE - 1) / sum;
// 對比CPU和GPU處理的結果
if (buffer->channel[i][j] != strongSelf->cpuColorBuffer.channel[i][j]) {                    // 如果不相同,則把對應的結果輸出
printf("%d, %d, gpuBuffer:%u  cpuBuffer:%u \n", i, j, buffer->channel[i][j], strongSelf->cpuColorBuffer.channel[i][j]);
}
}
}
memset(buffer, 0, strongSelf.colorBuffer.length);



fragment float4samplingShader(RasterizerData input [[stage_in]], // stage_in表示這個數據來自光柵化。(光柵化是頂點處理之後的步驟,業務層無法修改)
texture2d<float> colorTexture [[ texture(LYFragmentTextureIndexSource) ]], // texture表明是紋理數據,LYFragmentTextureIndexSource是索引
device LYLocalBuffer &convertBuffer [[buffer(LYFragmentBufferIndexConvert)]]) // 轉換的buffer{    constexpr sampler textureSampler (mag_filter::linear, min_filter::linear); // sampler是採樣器
float4 colorSample = colorTexture.sample(textureSampler, input.textureCoordinate); // 得到紋理對應位置的顏色
int3 rgb = int3(colorSample.rgb * SIZE); // 記得先乘以SIZE
colorSample.rgb = float3(convertBuffer.channel[0][rgb.r], convertBuffer.channel[1][rgb.g], convertBuffer.channel[2][rgb.b]) / SIZE; // 返回的值也要經過歸一化處理
return colorSample;
}</float>


遇到的問題


1、統計結果集中在頭部


問題表現:


統計結果異常,集中在前面兩個值。


如下,green通道的顏色集中在r[0]和r[1]上:



28269 4492 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0


問題分析:


實際上,gpu裡面存著的是0.0~1.0的值;(歸一化)


統計的值全部是在前面,是因為沒有乘以255!


先用CPU實現了直方圖均衡化,在實現shader的時候,參考CPU的代碼實現,犯了這個錯誤。


2、cpu和gpu統計結果相差較多


問題表現:


如下代碼,buffer是gpu統計的顏色值分布結果,cpuColorBuffer是cpu統計的顏色值分布結果。


理論上結果應該接近,但實際上printf出來的差異非常多。


for (int i = 0; i < 3; ++i) {            for (int j = 0; j < LY_CHANNEL_SIZE; ++j) {
val[i] += buffer->channel[i][j];
convertBuffer->channel[i][j] = val[i] * 1.0 * (LY_CHANNEL_SIZE - 1) / sum;
// 對比CPU和GPU處理的結果
if (buffer->channel[i][j] != strongSelf->cpuColorBuffer.channel[i][j]) {                    // 如果不相同,則把對應的結果輸出
printf("%d, %d, gpuBuffer:%u  cpuBuffer:%u \n", i, j, buffer->channel[i][j], strongSelf->cpuColorBuffer.channel[i][j]);
}
}
}


問題分析:


通過檢查代碼,先判定cpu統計的結果是正常。(cpu的處理過程就是正常的for循環,不易出錯)


仔細觀察log的不同:


0, 1, gpuBuffer:763 cpuBuffer:762


結果很接近,但是有細微的差距。


我們知道gpu是浮點數的處理,而cpu是整數型處理,浮點數到整數中間有精度的問題。


此時再看我們的shader,我們是以half來進行計算,這樣統計出來的結果會有點誤差。


grayKernel(texture2d<half, access::read>  sourceTexture  [[texture(LYFragmentTextureIndexTextureSource)]],
device LYColorBuffer &out [[buffer(LYKernelBufferIndexOutput)]],
uint2                          grid         [[thread_position_in_grid]])</half, access::read>


通過把精度從half改成float,cpu和gpu的統計差異就只有3個:


0, 248, gpuBuffer:23215  cpuBuffer:22854
1, 74, gpuBuffer:23201  cpuBuffer:22840
2, 64, gpuBuffer:23336  cpuBuffer:22975


3、gpu渲染的圖片為白色


問題表現:


在gpu統計的結果與cpu接近的情況下,把映射buffer傳給fragment shader,最後進行一次顏色處理。


但是結果是白色的圖片,shader的代碼如下:


fragment float4samplingShader(RasterizerData input [[stage_in]], // stage_in表示這個數據來自光柵化。(光柵化是頂點處理之後的步驟,業務層無法修改)
texture2d<float> colorTexture [[ texture(LYFragmentTextureIndexTextureSource) ]], // texture表明是紋理數據,LYFragmentTextureIndexTextureSource是索引
device LYLocalBuffer &localBuffer [[buffer(LYFragmentBufferIndexConvert)]]){    constexpr sampler textureSampler (mag_filter::linear,
min_filter::linear); // sampler是採樣器
float4 colorSample = colorTexture.sample(textureSampler, input.textureCoordinate); // 得到紋理對應位置的顏色
int3 rgb = int3(colorSample.rgb);
colorSample.rgb = float3(localBuffer.channel[0][rgb.r], localBuffer.channel[1][rgb.g], localBuffer.channel[2][rgb.b]);
return colorSample;
}</float>


問題分析:


我們先把colorSample.rgb = ...的這行代碼屏蔽,發現渲染結果是正常的,那麼問題就出現在映射處理上面。


再通過Xcode的Capture GPU Frame工具,查看傳入的映射buffer數據,也是正常的數據。


那麼問題可能出現int3 rgb的初始化,或者從映射buffer讀取數據。


觀察到int3 rgb = int3(colorSample.rgb),是有一個float->int的操作,聯想到前面提到的歸一化處理,馬上明白:在這裡的初始化時應該乘以SIZE。


那麼問題是否就此解決?不是的。


我們在進行顏色轉換的時候,float->int 需要乘以SIZE;


在獲取到映射buffer裡面對應顏色的值後,仍需要做一次int->float的處理,除以SIZE;


如果下:


float4 colorSample = colorTexture.sample(textureSampler, input.textureCoordinate); // 得到紋理對應位置的顏色
int3 rgb = int3(colorSample.rgb * size);
colorSample.rgb = float3(localBuffer.channel[0][rgb.r], localBuffer.channel[1][rgb.g], localBuffer.channel[2][rgb.b]) / size;


4、映射結果異常


問題表現:


問題如下,映射結果應該是0~255的值,但是通過Xcode看到最終的映射值遠超過255,甚至接近255*2的數字。



問題分析:


下面是映射的算法


int rgb[3][LY_CHANNEL_SIZE], sum = (int)(width * height);    int val[3] = {0};    for (int i = 0; i < 3; ++i) {        for (int j = 0; j < LY_CHANNEL_SIZE; ++j) {
val[i] += cpuColorBuffer.channel[i][j];
rgb[i][j] = val[i] * 1.0 * (LY_CHANNEL_SIZE - 1) / sum;
}
}


sum是固定值,LY_CHANNEL_SIZE是常量值256,那麼映射結果超過255的原因就是val[i]的統計結果太大!


通過Xcode調試,確實如此:



那麼,會是什麼原因導致?


在看到結果接近255的兩倍時,大概猜測可能是重複運算導致。


我們的均衡化處理是在MTKView的回調進行,如下:


- (void)drawInMTKView:(MTKView *)view {
[self customDraw];
}


這裡會回調多次,從而導致多次執行compute shader的顏色統計,這裡可以引入isDrawing的臨時變量解決:


- (void)drawInMTKView:(MTKView *)view {
if (!self.isDrawing) {
self.isDrawing = YES;
[self customDraw];
}
}


但是,問題並沒有徹底解決:首次統計正常,但是第二次處理的時候就會累積上一次的統計值。


如何對值進行清理?(這裡實際上只處理一次也行,但是debug過程中需要通過Xcode的GPU Capture Frame工具進行查看,而這個工具需要多次渲染)


我們知道MTLBuffer是cpu、gpu都可以操作的buffer,那麼在cpu直接清除這個數據即可。


在  commandBuffer addCompletedHandler:^(){}的結束回調中,使用memset(buffer, 0, strongSelf.colorBuffer.length)清理統計結果。


5、映射結果最大值為256


問題表現:


在踩過上面的各種坑之後,直方圖均衡化的效果也已經展現,但是仍有一點小問題:


映射結果buffer的數字範圍是0~256,而不是255。


問題分析:


根據直方圖均衡化的算法,我們知道是因為像素顏色值的統計,結果稍微偏大。


回顧Compute Shader的代碼:


kernel voidgrayKernel(texture2d<float, access::read>  sourceTexture  [[texture(LYFragmentTextureIndexTextureSource)]],
device LYColorBuffer &out [[buffer(LYKernelBufferIndexOutput)]],
uint2                          grid         [[thread_position_in_grid]]) {    // 邊界保護
if(grid.x <= sourceTexture.get_width() && grid.y <= sourceTexture.get_height())
{
float4 color  = sourceTexture.read(grid); // 初始顏色
int3 rgb = int3(color.rgb * size); //
atomic_fetch_add_explicit(&out.channel[0][rgb.r], 1, memory_order_relaxed);
atomic_fetch_add_explicit(&out.channel[1][rgb.g], 1, memory_order_relaxed);
atomic_fetch_add_explicit(&out.channel[2][rgb.b], 1, memory_order_relaxed);
}
}</float, access::read>


Metal的Compute Shader是按每組網格進行處理,那麼可能會出現邊界超過圖像的情況,所以添加了邊界保護。


但是,這裡存在誤判的情況:邊界判斷不應該是<=,而是<。


因為這個原因,會導致統計的結果偏大,最終出現256的情況。


在修復完這個問題後,Demo再無其他問題,GPU的處理結果也和CPU的處理結果完全一致!


總結


本文是在前文的Metal入門教程基礎上進行更複雜的嘗試,過程中也遇到較多問題,最終demo也順利完成,地址在Github。


作者:落影loyinglin

連結:https://www.jianshu.com/p/1c8e814edab4

相關焦點

  • 【圖像基礎教程】直方圖均衡化
    一、直方圖均衡化概述  直方圖均衡化(Histogram Equalization
  • 深入理解OpenCV+Python直方圖均衡化
    直方圖均衡化是圖像處理領域中利用圖像直方圖對對比度進行調整的方法。
  • OpenCV-Python 直方圖-2:直方圖均衡|二十七
    目標在本節中,我們將學習直方圖均衡化的概念,並利用它來提高圖像的對比度。理論考慮這樣一個圖像,它的像素值僅局限於某個特定的值範圍。例如,較亮的圖像將把所有像素限制在高值上。但是一幅好的圖像會有來自圖像所有區域的像素。
  • 紅外圖像處理中平臺實時直方圖均衡器的SoC實現
    O引言  直方圖均衡是紅外圖像處理中簡單有效的一種圖像增強方法[1]。直方圖均衡器在增強目標的同時也放大了背景和噪聲信號,因此有人提出了平臺直方圖均衡算法,該算法能達到增強目標且較好地抑制背景和噪聲的目的[2,5],具有很大的應用價值。
  • 基於直方圖的圖像增強算法(HE、CLAHE、Retinex)之(一)
    直方圖是圖像色彩統計特徵的抽象表述。基於直方圖可以實現很多有趣的算法。例如,圖像增強中利用直方圖來調整圖像的對比度、有人利用直方圖來進行大規模無損數據隱藏、還有人利用梯度直方圖HOG來構建圖像特徵進而實現目標檢測。
  • 一種自適應紅外圖像增強處理的FPGA實現
    但是由於受紅外探測器件的影響,紅外成像儀的成像效果還不夠理想,主要表現為圖像中的目標與背景區分不明顯、對比度低、噪聲大、信噪比低等缺點,因而紅外圖像處理首要解決的問題是圖像增強。要實現圖像的增強處理,主要有兩個途徑:一是改善探測器性能,一是在紅外圖像系統電子部分加入實時圖像處理功能。在目前條件下,加入實時圖像處理功能是快速而經濟的做法。
  • 用Keras和「直方圖均衡」為深度學習實現「圖像擴充」
    在本文中,我們將討論一些常見的、富有創意的方法,這些方法也是Keras深度學習庫為擴充圖像數據所提供的。之後我們將討論如何轉換keras預處理圖像文件,以啟用直方圖均衡法。我們將使用Keras附帶的cifar10數據集,但是為了使任務小到能夠順利在CPU上執行,我們將只會使用其中的貓和狗的圖像。首先,我們需要加載cifar10數據集並格式化其中的圖像,為卷積神經網絡做好準備。
  • MATLAB圖像處理之圖像增強(二)
    我們還是重點來學習直方圖灰度變換用到的函數以及實現的代碼和達到的效果:實現函數imhist函數用來計算和顯示圖像的直方圖 imhist(I) imhist(I,n) %I代表灰度圖像,n為指定的灰度級數目,默認值為256 imhist(X,map) %X為索引圖像
  • 光電圖像處理 | 一起來消費電量
    I = imread('圖片1.png');%讀取圖像I = uint8(rgb2gray(I));subplot(121),imshow(I); %顯示原圖像subplot(122),imhist(I); %顯示其直方圖title('直方圖')
  • Matlab圖像增強與復原技術在SEM圖像中的應用
    1圖像處理方法  1.1 直方圖均衡化  直方圖均衡化(Histogran Equalization,HE)是利用直方圖的統計數據進行直方圖的修改,能有效地處理原始圖像的直方圖分布情況,使各灰度級具有均勻的概率分布,通過調整圖像的灰度值的動態範圍,自動地增加整個圖像的對比度,以使圖像具有較大的反差,大部分細節清晰。
  • Matlab數字圖像處理初步
    0~255雙精度圖像可標準化圖示如imshow(A/255)彩色圖像處理 彩色圖像的通道分離與圖像存儲 對於RGB格式的彩色圖像矩陣A,B=A(:,:,1)即可提取彩色圖像的紅色通道值,其中B將以二維矩陣的形式存儲表示
  • 結合實例與代碼談數字圖像處理都研究什麼?
    圖像處理(以及機器視覺)在學校裡是一個很大的研究方向,很多研究生、博士生都在導師的帶領下從事著這方面的研究。另外,就工作而言,也確實有很多這方面的崗位和機會虛位以待。而且這種情勢也越來越凸顯。那麼圖像處理到底都研究哪些問題,今天我們就來談一談。圖像處理的話題其實非常非常廣,外延很深遠,新的話題還在不斷湧現。
  • 圖像算法工程師必備:灰度直方圖
    圖像的灰度直方圖,大概是數字圖像處理專業課接觸到的第一節講圖像具體算法的一堂課了。所謂的灰度直方圖,就是統計一個8比特的灰度圖像,其灰度等級從0到255的像素的分布情況。我們很容易就能想到,一幅圖像對應唯一的直方圖,而同一直方圖肯定會對應很多不同的圖像。
  • 圖像增強、銳化,利用 Python-OpenCV 來實現 4 種方法!
    圖像增強目的使得模糊圖片變得更加清晰、圖片模糊的原因是因為像素灰度差值變化不大,圖片各區域產生視覺效果似乎都是一樣的, 沒有較為突出的地方,看起來不清晰的感覺解決這個問題的最直接簡單辦法,放大像素灰度值差值、使圖像中的細節更加清晰。目前較為常用的幾個方法:伽馬變換、線性變換、分段線性變換、直方圖均衡化,對於圖像對比度增強,都能取得不錯的效果!
  • OpenCV-Python 直方圖-4:直方圖反投影|二十九
    它用於圖像分割或在圖像中查找感興趣的對象。簡而言之,它創建的圖像大小與輸入圖像相同(但只有一個通道),其中每個像素對應於該像素屬於我們物體的概率。用更簡單的話來說,與其餘部分相比,輸出圖像將在可能有對象的區域具有更多的白色值。好吧,這是一個直觀的解釋。(我無法使其更簡單)。直方圖反投影與camshift算法等配合使用。
  • 醫學信號與圖像處理算法中的並行化
    近年來,信號與圖像處理無論在算法上、系統結構上,還是在應用上以及普及程度上取得了長足的進展。但同時也面臨許多挑戰,其中最主要的問題就是如何提高解決實際複雜問題的綜合能力,就當前技術水平來說,這種綜合能力包括圖像處理的網絡化、複雜問題的求解與處理速度的高速化。  圖像並行處理技術是圖像處理中的一個重要方面,是提高圖像處理速度最為有效的技術。
  • 圖像學習之如何理解方向梯度直方圖(Histogram Of Gradient)
    特徵描述子(Feature Descriptor)特徵描述子就是圖像的表示,抽取了有用的信息,丟掉了不相關的信息。通常特徵描述子會把一個w*h*3(寬高3,3個channel)的圖像轉換成一個長度為n的向量/矩陣。比如一副64*128*3的圖像,經過轉換後輸出的圖像向量長度可以是3780。什麼樣子的特徵是有用的呢?
  • 高清成像技術讓條碼圖像更清晰
    高清條碼圖像處理的主要研究內容,如圖1所示,包括圖像採集、變換、增強復原、分割描述等過程。強化圖像高頻分量,突出條碼圖像條的部分,使圖像中物體輪廓清晰,細節明顯;強化低頻分量,可減少圖像噪聲的幹擾。為達到分析處理目的,在研究過程中,人為添加了高斯、椒鹽噪聲,經過中值濾波處理條碼圖像得到較好的復原,結果如圖4、5所示。
  • CLAHE算法實現圖像增強「AI工程論」
    ,主要用在醫學圖像上面。CLAHE起到的作用簡單來說就是增強圖像的對比度的同時可以抑制噪聲CLAHE的英文是Contrast Limited Adaptive Histogram Equalization 限制對比度的自適應直方圖均衡。
  • 【精彩論文推薦】西安郵電大學 王殿偉 等:基於細節特徵融合的低照度全景圖像增強
    本文在光照反射模型的基礎上,將全景圖像的對比度增強與細節增強相結合,提出了基於細節特徵加權融合的低照度全景圖像增強算法。具體步驟(見圖1)如下:1)將全景圖像從RGB色彩空間轉到HSV色彩空間,獲取圖像的亮度分量(V);2)利用雙邊濾波器對V分量進行處理,估計出全景圖像的光照分量;3)推導出三個光照輸入,輸入一為原始光照分量,輸入二與輸入三分別為對光照分量進行自適應伽馬校正與限制性直方圖均衡化的結果,提出基於像素級的細節特徵融合算法,對三個輸入進行加權融合,得到校正後的光照分量;4)對反射分量進行自適應調整,以增強圖像的細節信息