【Swift】什麼是QOI?相當OK的圖片?
最近正好看到一個Godot支援的圖片新格式QOI - Quite OK Image,是無損圖片壓縮格式,跟PNG - Portable Network Graphics一樣,但是是使用很簡單方式來壓縮,是不是聽起來很厲害啊?其實還有一個叫QOA - Quite OK Audio,它是有損音頻壓縮格式,跟MP3 - MPEG-2 Audio Layer III類似,不過影音不太會用無損壓縮的,傳輸太慢了,檔案又大,接下來我們來一起了解QOI的壓縮演算法…
做一個長得像這樣的東西
說明
圖片大小
800 x 600 x 4 = 1,920,000 = 1.87 MB
- 也就是說,不管圖片是怎麼
壓縮的,顯示在畫面上,都是一個點一個點去顯示的,最後都會解回RAW的格式,都要用到1.87 MB的記憶體… - 像
zip文件也是一樣,最後一定是解壓縮出來,才能處理裡面的檔案嘛… - 而所謂的圖片壓縮,就是把
相同/相近顏色變成一個可重複使用的編碼,有點像霍夫曼編碼那樣子,也有點像麥當勞的1號餐、2號餐那樣子簡化,我就以訂便當做例子…
情境一
排骨飯 x 1
排骨飯 x 5
排骨飯 x 1
爌肉飯 x 1
爌肉飯 x 3
排骨飯 x 1
排骨飯 x 2
情境二
排骨飯 x 1
+ 5
+ 1
爌肉飯 x 1
+ 3
排骨飯 x 1
+ 2
- 結果都是
排骨飯 x 10、爌肉飯 x 4,但很明顯情境二的字數少很多,而且+1 / +2也都知道加的是上一個便當,其實QOI也是用類似的演算法,它使用64格空間來存有限的顏色色碼,然後用Index去取色碼就好了,不然就跟上一個顏色一樣,是不是類似啊?
color = colors[87] // RGB(123, 234, 128)
- 我們先來看看QOI的檔案標頭長什麼樣子?這裡用C說明,所以可以知道
header有14個bytes…。
qoi_header {
char magic[4];
uint32_t width;
uint32_t height;
uint8_t channels;
uint8_t colorspace;
}
| 變數名稱 | 說明 |
|---|---|
magic |
就是qoif四個字,表示這是一份QOI的格式文件 (Quite OK Image Format)。 |
width |
圖片寬度。 |
height |
圖片高度。 |
channels |
顏色頻道 (3 = RGB) / (4 = RGBA),也就是說QOI都是彩色圖片。 |
colorspace |
顏色顯示色域 (0 = sRGB + Alpha 通道是線性) / (1 = RGB 與 Alpha 都是線性空間)。 |
六大運算子
- 它只有簡單的六個
運算功能
✨ RGB / RGBA功能
- 就是開頭是
0xFE或0xFF的值… - 其實很單純,第一個byte就是
顏色頻道的類型,後面的bytes就是存顏色色碼… - 那它是怎麼存的呢?首先要算出
hash值,因為只有64格可以存,所以index盡量不要沖到…
hash = (r*3 +g*5 +b*7 +a * 11)
- 就是照下面的方式去算出
index,然後存在RAM中,但是因為只有64格,所以還是有可能會被蓋掉的,但對格式沒什麼影響就是了…
indexRGB = (0*3+0*5 +255* 7 +255 * 11) % 64 = 46
indexRGBA = (0*3 +255 * 5 + 0 * 7 + 128 * 11) % 64 = 59
Colors = Color[63] {}
Colors[46] = RGB (0, 0, 255)
Colors[59] = RGB (0, 255, 0, 128)
✨ index功能
- 就是前二個是
00的值,數值範圍在0 ~ 63之間… - 它就是要去64格的
顏色快取表去取顏色…
✨ diff功能
- 就是前二個是
01的值,數值範圍在0 ~ 63之間… - 指的就是
different功能,用來記錄相似顏色功能,跟上一個顏色的差距,差距範圍在-2 ~ 1之間… - 因為一般的顏色都是
連續的,不會有太大的差異,用這個功能紀錄,可以省下不小的空間…
Color = RGB (0, 255, 0, 128)
Color_diff = RGB (0 + 1, 255 - 2, 0 + 0, 128)
= RGB (1, 253, 0, 128)
✨ luma功能
- 就是前二個是
10的值,這個功能就是處理顏色差異太大,而不能用diff處理的功能… - 從圖上可以很清楚看到,
綠色是6 bits/紅色是4 bits/藍色是4 bits,難到作者是青鳥嗎? - 當然不是,而是因為人眼對綠色的靈敏度比較高,所以特別對綠色做了加強…
- 計算方式跟
diff差不多,這裡就不多作說明了…
Color = RGB (0, 255, 0, 128)
dr_dg = (cur.r - prev.r) - dg
db_dg = (cur.b - prev.b) - dg
cur.g = prev.g + dg = 255 + (-8) = 247
cur.r = prev.r + dg + dr_dg = 0 + (-8) + 3 = 255
cur.b = prev.b + dg + db_dg = 0 + (-8) + 3 = 255
Color_luma = RGBA(255, 247, 255, 128)
✨ run功能
- 最後就是
重複計數功能,它前二個是11的值,數值範圍在1 ~ 62之間,這個就是+1的功能… - 要注意的是,
0就是代表一次,因為…沒有就不用寫了嘛… - 另外,有二個數被運算子
RGB/RGBA用掉了,所以會少兩個…
編碼判斷順序
編碼器通常會依照效率去判斷要用哪一種方式。 概念上可以理解成:
- 先看是不是可以用
Run- 再看是不是可以用
Index- 再看是不是可以用
Diff- 再看是不是可以用
Luma- 不行就用
RGB或RGBA
實作細節可能不同,但大方向就是先嘗試最省空間的表示法,解壓縮的話,就是倒回來做。
總結
- QOI 對「規則且重複多」的圖很強,對「雜亂且變化大」的圖較弱
| 圖片類型 | 適合 / 不適合 | 原因 |
|---|---|---|
| CG 圖、插畫、角色圖 | 適合 | 顏色區塊規則、重複色多,容易命中 Run、Index、Diff、Luma。 |
| UI 圖示、App 截圖、介面元素 | 適合 | 大量平坦背景與固定色塊,連續相同或相近像素很多。 |
| 大面積純色背景 | 適合 | Run 特別有效,連續重複像素可用很少資料表示。 |
| 有透明通道的簡單圖 | 適合 | RGBA 若分布規律,快取與差分仍然很好用。 |
| 低噪聲、低細節的人工圖像 | 適合 | 像素變化小,差分編碼更容易壓縮。 |
| 漸層平順的圖 | 適合 | 顏色變化連續且規律,Diff / Luma 容易命中。 |
| 照片 | 不適合 | 細節多、變化複雜,QOI 的簡化編碼不容易有效壓縮。 |
| 高雜訊圖片 | 不適合 | 像素常常不重複,Run 和 Index 命中率低。 |
| 細碎紋理很多的圖 | 不適合 | 例如毛髮、草地、顆粒背景,差分與索引效果通常較差。 |
| 隨機噪點圖 | 不適合 | 顏色不可預測,可能導致壓縮效果差,甚至膨脹。 |
- QOI / PNG / RAW / JPEG 對照表
| 格式 | 壓縮方式 | 是否無損 | 優點 | 缺點 | 適合圖片類型 |
|---|---|---|---|---|---|
| QOI | 簡單的無損編碼,使用 Run / Index / Diff / Luma / RGB / RGBA | 是 | 編解碼很快、規格簡單、實作容易、對 CG 圖很有利 | 壓縮率通常不如 PNG 穩定,對照片和噪聲圖較弱 | CG 圖、插畫、UI、icon、截圖、純色區塊多的圖 |
| PNG | 無損壓縮,會搭配濾波與 DEFLATE | 是 | 壓縮率通常比 QOI 穩定,支援透明度,通用性高 | 編解碼較重,速度通常比 QOI 慢,檔案較大 | 圖標、插圖、介面圖、需要無損與透明的圖片 |
| RAW | 幾乎不壓縮,直接存原始像素資料 | 是 | 最簡單、讀寫最快、沒有壓縮損失 | 檔案最大,儲存成本高,不適合傳輸 | 內部處理、暫存、影像管線中間格式 |
| JPEG | 有損壓縮 | 否 | 照片通常壓得很小,通用性高 | 會有失真,不適合文字、線條、透明圖 | 照片、自然影像、細節很多的圖片 |
範例程式碼下載
後記
作這一行也算是有一段時間了,但是沒有碰到對二進制的處理,正好找到一個簡單的格式來試試看,相信對二進制的處理在硬體業是家常便飯了,這也算是對自己在軟體業上的補強吧?但也不得不佩服QOI的作者,對每個bit都處理的剛剛好,完全沒有用到高深的演算法,像jpeg就用到了離散餘弦轉換 (DCT - Discrete Cosine Transform),這一般人就很難去實作了…