灰度:從rgb2gray到數(shù)據(jù)類型避坑指南)
彩色圖轉(zhuǎn)灰度這大概是很多人用MATLAB“入坑圖像處理”的第一課。凡是看過教程的基本都能敲出rgb2gray這一行命令。但你有沒有想過為什么一張彩色照片經(jīng)過這行代碼之后看起來并不只是“丟了顏色”某些色塊之間的對(duì)比度還會(huì)發(fā)生微妙的變化為什么有人轉(zhuǎn)換后圖像發(fā)黑有人轉(zhuǎn)換后顏色失真還有人直接用平均值算出一個(gè)“灰蒙蒙”的結(jié)果跟函數(shù)自帶的效果完全不一樣這篇東西我不會(huì)只給你丟一個(gè)函數(shù)而是把灰度轉(zhuǎn)換這件事從原理到踩坑完整拆一遍。包括彩色圖像在MATLAB里到底是什么數(shù)據(jù)結(jié)構(gòu)、灰度圖的本質(zhì)是什么、內(nèi)置函數(shù)背后的加權(quán)公式是怎么來的、uint8和double兩類數(shù)據(jù)在顯示時(shí)的隱藏差異以及我實(shí)際處理批量圖像時(shí)遇到的幾個(gè)詭異報(bào)錯(cuò)和解決思路??赐曛竽悴粌H能準(zhǔn)確地把彩色圖變成灰度圖還能搞清楚為什么網(wǎng)上那些“一行代碼搞定灰度轉(zhuǎn)換”的博文其實(shí)漏掉了很多關(guān)鍵細(xì)節(jié)。1. 彩色圖轉(zhuǎn)灰度不只是“丟掉顏色”這么簡(jiǎn)單1.1 為什么要有灰度圖彩色三通道到底存了什么在走進(jìn)代碼之前先搞清楚我們操作的對(duì)象。MATLAB里用imread讀進(jìn)來的絕大多數(shù)彩色照片本質(zhì)上是一個(gè)height × width × 3的三維數(shù)組。第三個(gè)維度是三個(gè)通道習(xí)慣上按順序叫R紅、G綠、B藍(lán)。每個(gè)通道都是一張二維矩陣?yán)锩婷總€(gè)元素的值代表這個(gè)像素點(diǎn)在該顏色分量上的強(qiáng)度。你可以把一張RGB彩色圖像想象成三張疊在一起的灰度底片一張只記錄紅色亮度一張只記錄綠色亮度一張只記錄藍(lán)色亮度。顯示的時(shí)候屏幕把三個(gè)通道按對(duì)應(yīng)顏色混合在一起你的眼睛就看到了“彩色”。這也就解釋了一個(gè)現(xiàn)象如果三個(gè)通道的值完全相同畫面就是標(biāo)準(zhǔn)的黑白灰圖像。所謂灰度圖本質(zhì)上就是一張只有一個(gè)亮度通道的二維矩陣每個(gè)像素用一個(gè)數(shù)值來表示明暗程度0代表純黑255代表純白中間是連續(xù)過渡的灰色。這個(gè)過程看起來是在“降低信息量”但很多場(chǎng)景下反而是一種必要預(yù)處理。比如做邊緣檢測(cè)、圖像分割、特征提取時(shí)算法通常只關(guān)心亮度變化和結(jié)構(gòu)信息顏色不僅幫不上忙反而會(huì)增加計(jì)算負(fù)擔(dān)。把彩色圖轉(zhuǎn)成灰度圖去掉的是顏色這一維對(duì)算法決策的干擾保留的是圖像最有價(jià)值的結(jié)構(gòu)特征。1.2 轉(zhuǎn)換的核心難題怎么把三維信息壓成一維還不失真彩色圖像有RGB三個(gè)數(shù)值灰度圖只有一個(gè)數(shù)值。從數(shù)學(xué)上講這是把一個(gè)三維向量映射到一維標(biāo)量。問題在于這個(gè)映射關(guān)系不是唯一的而且不同的映射方式會(huì)帶來觀感明顯不同的結(jié)果。如果單純把三個(gè)通道的值取平均一個(gè)純紅色像素255, 0, 0和另一個(gè)深綠色像素0, 128, 0在灰度結(jié)果里可能都是128左右人眼看過去明顯不一樣的顏色轉(zhuǎn)成灰度以后變得難以區(qū)分。這就是“信息失真”。所以灰度轉(zhuǎn)換的核心矛盾是如何用最合理的方式壓縮三維信息使得亮度差異盡量符合人眼對(duì)明暗的感知。要解決這個(gè)問題就必須搞清楚一個(gè)事實(shí)——人眼對(duì)紅、綠、藍(lán)三種顏色的敏感度不是一樣的。綠色在最亮區(qū)域貢獻(xiàn)最大紅色次之藍(lán)色最弱。這也是為什么夜視儀和很多顯示設(shè)備的亮度信號(hào)都偏向綠色成分的原因。既然對(duì)顏色的敏感度不同那數(shù)學(xué)上最合理的映射方式自然是按照敏感度給三個(gè)通道分配不同的權(quán)重。這個(gè)權(quán)重系數(shù)不是拍腦袋定的而是有行業(yè)標(biāo)準(zhǔn)的。2. 核心轉(zhuǎn)換方法與公式取舍2.1 三種常見算法對(duì)比你該用哪一種先看三個(gè)最常見的灰度映射方案我把它們的轉(zhuǎn)換效果和適用場(chǎng)景整理成了一張表。轉(zhuǎn)換方法計(jì)算公式特點(diǎn)適用場(chǎng)景平均值法Y (R G B) / 3實(shí)現(xiàn)簡(jiǎn)單三個(gè)通道完全平等快速測(cè)試、通道間無顯著差異的圖像最大值法Y max(R, G, B)結(jié)果整體偏亮保留高光信息高光檢測(cè)、考慮亮度上限的場(chǎng)景加權(quán)平均法Y 0.299R 0.587G 0.114B符合人眼感知行業(yè)標(biāo)準(zhǔn)做法絕大多數(shù)正式圖像處理流程平均值法最大的問題在于忽視了人眼的感知特性。比如一塊純藍(lán)色0, 0, 255和一塊純紅色255, 0, 0平均值都是85但它們實(shí)際給人的明暗感受差異很大。最大值法得到的灰度圖整體偏亮因?yàn)樗x取的永遠(yuǎn)是三個(gè)通道里最大的值導(dǎo)致暗部細(xì)節(jié)被壓縮不過在高光區(qū)域的表現(xiàn)確實(shí)更好。第三種加權(quán)平均法是我最推薦的方案也是MATLAB內(nèi)置函數(shù)走的路線。它的綠色權(quán)重接近0.6紅色接近0.3藍(lán)色只有約0.1完全對(duì)應(yīng)人眼的視覺響應(yīng)曲線。這個(gè)方案得到的灰度圖在明暗關(guān)系上最接近你直接用肉眼觀察彩色原圖時(shí)的感受。2.2 為什么MATLAB的rgb2gray默認(rèn)用這套權(quán)重MATLAB的rgb2gray函數(shù)內(nèi)部使用的公式是Y 0.299R 0.587G 0.114B這里的權(quán)重來自國(guó)際標(biāo)準(zhǔn)最早源于NTSC電視制式的亮度方程。0.299、0.587、0.114這組數(shù)值不是臨時(shí)定的而是基于人眼對(duì)三原色亮度感知的測(cè)量結(jié)果。后來高清電視標(biāo)準(zhǔn)里也有一組稍有不同的系數(shù)0.2126、0.7152、0.0722更偏向現(xiàn)代顯示設(shè)備的色域特性但MATLAB內(nèi)置函數(shù)默認(rèn)還是用了經(jīng)典的NTSC系數(shù)。原因很簡(jiǎn)單這組權(quán)重已經(jīng)被驗(yàn)證了很多年在不同圖像上的表現(xiàn)都比較穩(wěn)定而且很多圖像處理算法和教材都以它為基準(zhǔn)。我遇到過不少初學(xué)者自己寫灰度轉(zhuǎn)換代碼時(shí)喜歡用0.3R 0.6G 0.1B這種近似值雖然在多數(shù)情況下肉眼分不出區(qū)別但如果你的項(xiàng)目對(duì)精度有要求或者需要和其他用rgb2gray處理的結(jié)果做對(duì)比建議始終用標(biāo)準(zhǔn)公式。否則兩個(gè)流程之間會(huì)有微小但真實(shí)的數(shù)值差異。2.3 用原理解釋幾個(gè)容易混淆的概念在寫代碼之前有幾個(gè)詞容易把人繞暈亮度、灰度、灰度級(jí)。亮度是對(duì)物體表面明暗程度的物理描述是個(gè)感知量?;叶仁菙?shù)字圖像里用來量化亮度的數(shù)值通常用0到255的整數(shù)表示這就是8bit灰度級(jí)。還有人說“灰度圖”是8位的一個(gè)像素占一個(gè)字節(jié)這句沒問題。但如果你讀入一張16bit的灰度圖每個(gè)像素值的范圍就會(huì)變成0到65535這時(shí)候再用imshow直接顯示就會(huì)出現(xiàn)大片黑色——這個(gè)坑后面細(xì)說。另外一個(gè)高頻混淆點(diǎn)是灰度圖不等于黑白圖更不等于二值圖?;叶葓D有256個(gè)灰度級(jí)能表現(xiàn)連續(xù)的明暗過渡二值圖只有0和255兩個(gè)值或者邏輯0和邏輯1只能表示純黑和純白。做OCR或者文檔識(shí)別之前經(jīng)常需要把灰度圖進(jìn)一步轉(zhuǎn)成二值圖但那是imbinarize的事跟灰度轉(zhuǎn)換是兩個(gè)步驟。很多沒經(jīng)驗(yàn)的朋友試圖用rgb2gray一步到位得到“黑白效果”結(jié)果發(fā)現(xiàn)圖像還是灰撲撲的其實(shí)是概念搞混了。3. 實(shí)操完整實(shí)現(xiàn)一個(gè)灰度轉(zhuǎn)換3.1 準(zhǔn)備環(huán)境與讀入圖像數(shù)據(jù)檢查是關(guān)鍵一步環(huán)境不需要特別的安裝包MATLAB基礎(chǔ)工具箱就自帶rgb2gray、imread、imshow、imwrite這幾個(gè)函數(shù)R2014a之后的版本都能跑通。我建議操作之前先把當(dāng)前目錄設(shè)置到圖片所在文件夾或者直接用完整路徑讀入可以減少很多路徑報(bào)錯(cuò)問題。代碼第一步是讀入圖像然后立刻檢查它的尺寸和數(shù)據(jù)類型。這一步很多人會(huì)跳過但它直接影響后面的轉(zhuǎn)換操作是否正確。% 讀入彩色圖像 img imread(sample.jpg); disp(size(img)); % 查看維度彩色圖應(yīng)顯示為 H W 3 disp(class(img)); % 查看數(shù)據(jù)類型大概率是 uint8這里在命令行里輸出兩行信息你應(yīng)該能看到類似1024 768 3的尺寸。如果第三個(gè)維度不是3那就說明讀進(jìn)來的不是常規(guī)RGB圖像而是索引圖像或者帶透明通道的PNG我在后面問題排查部分會(huì)專門說。3.2 一行代碼方案rgb2gray的用法與坑環(huán)境確認(rèn)沒問題之后最直接的做法就是用內(nèi)置函數(shù)。% 32位浮點(diǎn)結(jié)果范圍保持在0~1 gray_img im2double(img); gray_img 0.299 * gray_img(:,:,1) 0.587 * gray_img(:,:,2) 0.114 * gray_img(:,:,3);第4行代碼就完成了灰度變換。img(:,:,1)提取R通道img(:,:,2)提取G通道img(:,:,3)提取B通道。注意這里我先把uint8類型轉(zhuǎn)成了double原因是uint8做乘法或者加法運(yùn)算時(shí)一旦結(jié)果超過255就會(huì)被截?cái)喑?55而不是自然進(jìn)位或溢出這會(huì)產(chǎn)生大面積高光區(qū)域變成純白色的失真效果。先轉(zhuǎn)double再計(jì)算每個(gè)通道的數(shù)值范圍是0到1加權(quán)計(jì)算后得到的結(jié)果也在0到1之間避免了所有溢出問題。上面這段代碼用到了將被加權(quán)的三個(gè)通道能不能看得懂其實(shí)等價(jià)寫法是把三通道變成灰度圖gray_img rgb2gray(img);同樣的結(jié)果。我平時(shí)處理單張圖像直接用rgb2gray(img)但如果是研究或者教學(xué)、演示需要跟學(xué)生講清楚灰度是怎么來的就傾向于手寫加權(quán)公式讓大家直觀看到三個(gè)通道是怎么被合并的。3.3 手寫加權(quán)平均實(shí)現(xiàn)順便理解uint8和double這里我詳細(xì)走一遍手寫的流程。假設(shè)img是uint8類型如果直接寫0.299 * img(:,:,1)結(jié)果會(huì)因?yàn)轭愋筒黄ヅ涑霈F(xiàn)很怪異的輸出。受限于uint8的數(shù)值范圍和整數(shù)類型MATLAB的算術(shù)運(yùn)算會(huì)自動(dòng)做了類型轉(zhuǎn)換和截?cái)嘟Y(jié)果并不符合我們預(yù)期。所以第一步必須用double轉(zhuǎn)換或者用im2double把圖像映射到0到1的浮點(diǎn)范圍。% 手寫加權(quán)平均實(shí)現(xiàn) img_d im2double(img); R img_d(:,:,1); G img_d(:,:,2); B img_d(:,:,3); gray_result 0.299 * R 0.587 * G 0.114 * B;如果用im2double轉(zhuǎn)換uint8的255變成1.0像素值都縮放到0到1區(qū)間這樣加權(quán)計(jì)算的結(jié)果同樣保持在0到1。如果直接用double(img)轉(zhuǎn)換像素值還是0到255加權(quán)結(jié)果也在這個(gè)范圍。兩種做法的物理含義一樣但顯示方式不同。imshow對(duì)double類型的圖像默認(rèn)按0到1范圍解釋所以如果你用double(img)加權(quán)后得到0到255范圍的灰度矩陣直接imshow可能顯示成一片純白。這個(gè)時(shí)候你需要用imshow(gray_result/255)把值域壓回0到1或者干脆用imshow(uint8(gray_result))轉(zhuǎn)回8bit整數(shù)再顯示。實(shí)戰(zhàn)里我建議直接用im2double在后續(xù)做濾波、閾值、直方圖操作時(shí)0到1區(qū)間的浮點(diǎn)灰度圖是各種圖像處理算法的統(tǒng)一輸入標(biāo)準(zhǔn)省去很多來回轉(zhuǎn)換的麻煩。3.4 批量轉(zhuǎn)換與保存給圖片加個(gè)批處理流程單張圖像誰都會(huì)處理工程項(xiàng)目里往往是幾十張上百?gòu)垐D需要整體轉(zhuǎn)灰度并保存。批量處理和保存的核心在于文件名遍歷和類型轉(zhuǎn)換看下面這個(gè)示例% 批量轉(zhuǎn)換灰度圖并保存 srcDir origin_images/; dstDir gray_images/; if ~exist(dstDir, dir) mkdir(dstDir); end files dir(fullfile(srcDir, *.jpg)); for i 1:length(files) imgPath fullfile(srcDir, files(i).name); rgbImg imread(imgPath); grayImg rgb2gray(rgbImg); [~, name, ext] fileparts(files(i).name); imwrite(grayImg, fullfile(dstDir, [name, _gray, ext])); enddir(fullfile(srcDir, *.jpg))這一句能讀取所有JPG文件fileparts把文件名和擴(kuò)展名拆開方便拼接新文件名。保存時(shí)注意imwrite會(huì)根據(jù)擴(kuò)展名自動(dòng)選擇編碼格式所以_gray.jpg會(huì)保存成JPEG格式灰度圖如果想存成無損格式把擴(kuò)展名改成.png就行。這個(gè)批量流程我實(shí)測(cè)過100張1MB左右的照片在普通電腦上幾秒就能跑完瓶頸基本在硬盤讀寫而不是計(jì)算。4. 常見問題排查與提速技巧4.1 圖像顯示全黑或全白大多是數(shù)據(jù)類型在作祟寫灰度轉(zhuǎn)換的人十個(gè)有八個(gè)會(huì)遇到這個(gè)問題明明轉(zhuǎn)換成功了用imshow一看圖像要么漆黑一片要么慘白到什么都看不見。原因基本都指向數(shù)據(jù)類型的值域問題。uint8類型下數(shù)值范圍0到255double類型下如果值是0到255imshow默認(rèn)只把0到1的部分顯示成黑色到白色超過1的數(shù)值全部被當(dāng)成白色處理于是圖像變成大片亮白。反過來如果圖像數(shù)據(jù)是0到1范圍的double類型但被錯(cuò)誤地當(dāng)成uint8做顯示屏幕上就只剩一片深色。標(biāo)準(zhǔn)做法是先確認(rèn)圖像類型再?zèng)Q定顯示方式。whos gray_img命令能快速查看變量類型和屬性。如果是double類型且范圍在0到1直接imshow如果是0到255范圍的double用imshow(gray_img/255)如果本身就是uint8直接imshow就好。4.2 四通道PNG和索引圖像讀進(jìn)來就不是三通道另一個(gè)讓我印象深刻的坑是PNG格式。PNG支持透明度通道所以imread一個(gè)帶alpha通道的PNG返回的矩陣可能是H × W × 4第四個(gè)通道就是透明度。直接調(diào)用rgb2gray會(huì)報(bào)錯(cuò)因?yàn)楹瘮?shù)要求輸入必須是三通道圖像。解決辦法是先提取前三個(gè)通道再進(jìn)行轉(zhuǎn)換if size(img, 3) 4 img img(:,:,1:3); end gray_img rgb2gray(img);另外還有一類圖叫索引圖像讀進(jìn)來是H × W的二維矩陣但里面存的不是像素亮度值而是調(diào)色板索引。這時(shí)候如果直接rgb2gray或把它當(dāng)成灰度圖保存結(jié)果會(huì)完全錯(cuò)亂。正確的做法是用ind2rgb函數(shù)先把索引圖像轉(zhuǎn)成RGB三通道再做灰度轉(zhuǎn)換% 索引圖像轉(zhuǎn)灰度 [X, map] imread(indexed.png); rgb_img ind2rgb(X, map); gray_img rgb2gray(rgb_img);實(shí)際做圖像采集和處理項(xiàng)目時(shí)我習(xí)慣用一個(gè)通用預(yù)處理函數(shù)進(jìn)來先統(tǒng)一轉(zhuǎn)成三通道RGB再做后續(xù)操作。這個(gè)習(xí)慣幫我少踩了非常多的雷。4.3 批量轉(zhuǎn)換較慢幾個(gè)小技巧讓循環(huán)提速批量處理大量圖像時(shí)如果感覺速度不理想通常瓶頸不在轉(zhuǎn)換算法本身而在imread和imwrite的磁盤讀寫。無論是處理哪一類都需要逐張讀入、轉(zhuǎn)換、保存。想在代碼層面優(yōu)化第一是避免在循環(huán)內(nèi)部頻繁拼接字符串路徑可以提前用fullfile構(gòu)建路徑數(shù)組第二是如果有并行工具箱把普通for循環(huán)改成parfor能讓多核CPU分擔(dān)計(jì)算壓力。但要注意parfor里每個(gè)迭代的變量必須相互獨(dú)立且保存文件名不能沖突。如果電腦核心數(shù)較多1000張圖的批量轉(zhuǎn)換從幾分鐘縮短到幾十秒是比較典型的收益。如果不想引入并行復(fù)雜度也可以把讀寫路徑從HDD換到SSD上這塊的速度提升往往比改代碼更明顯。4.4 報(bào)錯(cuò)信息與解決方案速查表我在實(shí)際操作中整理了這樣一張排查對(duì)照表直接按圖索驥就行。問題現(xiàn)象常見原因解決方案直接調(diào)用rgb2gray報(bào)錯(cuò)提示輸入必須為二維或三維真彩色讀入的是四通道PNG或者索引圖像截取前3通道或用ind2rgb轉(zhuǎn)為RGB轉(zhuǎn)換后圖像顯示全黑double類型值域?yàn)?~1但數(shù)據(jù)實(shí)際是0~255顯示時(shí)被壓縮除以255或轉(zhuǎn)uint8后顯示圖像顯示慘白一片double類型值域超過1被當(dāng)作滿幅白色歸一化到0~1后再顯示轉(zhuǎn)換后的圖像偏綠或偏藍(lán)通道順序錯(cuò)誤RGB被當(dāng)成BGR處理檢查通道順序確保按R、G、B取用保存的灰度圖看起來奇怪保存時(shí)沒有處理數(shù)據(jù)類型或?qū)懭隤NG卻保留了alpha信息用uint8保存并去掉alpha通道自己寫加權(quán)公式后圖像有噪點(diǎn)uint8類型算術(shù)溢出被截?cái)嗟?55先轉(zhuǎn)double或im2double后再運(yùn)算5. 灰度轉(zhuǎn)換后的下一步你在實(shí)際項(xiàng)目中會(huì)怎么做5.1 灰度圖做預(yù)處理后續(xù)邊緣檢測(cè)和閾值分割怎么銜接實(shí)際項(xiàng)目里灰度轉(zhuǎn)換很少是終點(diǎn)它通常只是圖像處理流水線的第一環(huán)。緊接著的常見操作包括直方圖均衡化、濾波去噪、邊緣檢測(cè)和閾值分割。用灰度圖做直方圖均衡化時(shí)用histeq函數(shù)就能增強(qiáng)對(duì)比度做邊緣檢測(cè)時(shí)edge(gray_img, canny)是很多人選擇的經(jīng)典方案。還有閾值分割imbinarize(gray_img)函數(shù)會(huì)基于OTSU算法自動(dòng)選擇一個(gè)全局閾值讓灰度圖變成二值圖像。舉個(gè)我自己做過的模擬項(xiàng)目例子一批金屬零件表面缺陷檢測(cè)原始采集圖是彩色的但缺陷特征劃痕、凹坑在顏色上并無明顯異常真正的差異體現(xiàn)在局部亮度梯度和紋理結(jié)構(gòu)上。所以我先做了灰度轉(zhuǎn)換然后做中值濾波去掉椒鹽噪聲再用Canny算子提取邊緣最后通過連通域分析篩選可疑缺陷區(qū)域。整個(gè)流程如果直接對(duì)RGB三通道圖像操作計(jì)算量會(huì)變成三倍而且在亮度梯度響應(yīng)上反而會(huì)引入不必要的通道間干擾?;叶葓D在這個(gè)流程里起到了降維去噪、聚焦核心信息的作用。5.2 哪些場(chǎng)景別急著轉(zhuǎn)灰度彩色信息也是寶貴信號(hào)灰度化不是萬能的甚至在某些場(chǎng)景下是錯(cuò)誤的選擇。比如醫(yī)學(xué)圖像分析中細(xì)胞染色圖像的顏色信息本身就是核心信號(hào)免疫組化染色的陽性區(qū)域靠的是特定顏色分布來判定的如果提前轉(zhuǎn)成灰度等于直接把判定依據(jù)扔掉了。還有工業(yè)質(zhì)檢中如果檢測(cè)目標(biāo)是顏色區(qū)分比如不同顏色的標(biāo)簽混料灰度圖根本無法區(qū)分色相一致的明暗變化這時(shí)候應(yīng)該考慮用HSV色彩空間或者直接在RGB通道上做分析。回到灰度轉(zhuǎn)換本身。很多人在這個(gè)基礎(chǔ)步驟上栽過跟頭但說到底它的核心就是一件事把三維色彩信號(hào)按感知權(quán)重壓縮成一維亮度信號(hào)。理解了這個(gè)本質(zhì)后面無論你接觸HSV、YCbCr、Lab哪個(gè)色彩空間都會(huì)覺得清晰很多。我在實(shí)際處理圖像時(shí)有個(gè)習(xí)慣凡是拿不準(zhǔn)是否會(huì)丟失關(guān)鍵信息都會(huì)先做一個(gè)小實(shí)驗(yàn)把轉(zhuǎn)換后的灰度圖和原圖并排顯示用眼睛確認(rèn)關(guān)鍵特征在灰度圖里依然清晰可辨。這個(gè)習(xí)慣雖然簡(jiǎn)單但能幫你過濾掉至少一半的轉(zhuǎn)換失誤。希望這篇內(nèi)容能讓你少走一些彎路如果后面碰到圖像顯示異?;蛘吲刻幚韴?bào)錯(cuò)回頭看看第4節(jié)的排查表大概率能找到答案。