【Swift】什麼是QOI?相當OK的圖片?

最近正好看到一個Godot支援的圖片新格式QOI - Quite OK Image,是無損圖片壓縮格式,跟PNG - Portable Network Graphics一樣,但是是使用很簡單方式來壓縮,是不是聽起來很厲害啊?其實還有一個叫QOA - Quite OK Audio,它是有損音頻壓縮格式,跟MP3 - MPEG-2 Audio Layer III類似,不過影音不太會用無損壓縮的,傳輸太慢了,檔案又大,接下來我們來一起了解QOI的壓縮演算法

做一個長得像這樣的東西

說明

圖片大小

  • 我們大家都知道,顏色就是由三原色 + 透明度組成的,也就是網頁上常看到的RGBA,所以1張800 x 600的彩色圖片,它的大小就是:

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功能

  • 就是開頭是0xFE0xFF的值…
  • 其實很單純,第一個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用掉了,所以會少兩個…

編碼判斷順序

編碼器通常會依照效率去判斷要用哪一種方式。 概念上可以理解成:

  1. 先看是不是可以用 Run
  2. 再看是不是可以用 Index
  3. 再看是不是可以用 Diff
  4. 再看是不是可以用 Luma
  5. 不行就用 RGBRGBA

實作細節可能不同,但大方向就是先嘗試最省空間的表示法,解壓縮的話,就是倒回來做。

總結

  • QOI 對「規則且重複多」的圖很強,對「雜亂且變化大」的圖較弱
圖片類型 適合 / 不適合 原因
CG 圖、插畫、角色圖 適合 顏色區塊規則、重複色多,容易命中 RunIndexDiffLuma
UI 圖示、App 截圖、介面元素 適合 大量平坦背景與固定色塊,連續相同或相近像素很多。
大面積純色背景 適合 Run 特別有效,連續重複像素可用很少資料表示。
有透明通道的簡單圖 適合 RGBA 若分布規律,快取與差分仍然很好用。
低噪聲、低細節的人工圖像 適合 像素變化小,差分編碼更容易壓縮。
漸層平順的圖 適合 顏色變化連續且規律,Diff / Luma 容易命中。
照片 不適合 細節多、變化複雜,QOI 的簡化編碼不容易有效壓縮。
高雜訊圖片 不適合 像素常常不重複,RunIndex 命中率低。
細碎紋理很多的圖 不適合 例如毛髮、草地、顆粒背景,差分與索引效果通常較差。
隨機噪點圖 不適合 顏色不可預測,可能導致壓縮效果差,甚至膨脹。
  • 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),這一般人就很難去實作了…