Kalan's Blog

Kalan 頭像照片,在淡水拍攝,淺藍背景

四零二曜日電子報上線啦!訂閱訂起來

本部落主要是關於前端、軟體開發以及我在日本的生活,也會寫一些對時事的觀察和雜感
本部落格支援 RSS feed(全文章內容),可點擊下方 RSS 連結或透過第三方服務設定。若技術文章裡有程式碼語法等特殊樣式,仍建議至原網站瀏覽以獲得最佳體驗。

目前主題 亮色

我會把一些不成文的筆記或是最近的生活雜感放在短筆記,如果有興趣的話可以來看看唷!

JPG 與離散餘弦轉換

大家耳熟能詳的 JPG,背後有許多值得令人學習的壓縮技巧,看到前人們用各種聰明的 Engineering 技巧將圖片壓縮,會覺得能活在這個世代實在太棒了。

JPEG

JPEG 其實嚴格來說並不是一種檔案格式,而是一種演算法。檔案的格式則是由 JFIF 所規範的。

首先我們先從 JPEG 的顏色儲存方式開始。

科學家們發現,人類對亮度的反應比顏色敏感很多,因此在選擇顏色空間時,會使用 YCrCb 來儲存,將亮度。

離散餘弦轉換

如果說 JPEG 有什麼演算法讓圖片可以神奇地被壓縮,那麼背後最重要的功臣就是離散餘弦轉換。為什麼圖片可以被壓縮,又跟離散餘弦轉換有什麼關係?

圖片中的高頻與低頻

為了簡化並讓離散餘弦轉換更容易理解,先用 1D 陣列當作範例。如果我們將圖片當作信號來看,以像素灰階值當作 y 軸高度的話,可以這樣表示:

把圖片當作信號來看有什麼好處?就跟其他訊號一樣,我們可以對它做處理,在數學上,信號可以用多項式。

離散餘弦轉換的公式是這樣的:

Xk=k=0n1xkcos[πnm(k+12)]X_{k} = \sum_{k=0}^{n-1} x_k \cos \left[\frac{\pi}{n} m \left(k+\frac{1}{2}\right) \right]

直接看看經過處理後會變什麼樣子:

對於這個公式,我們可以把轉換後的訊號可以解讀成

cos 的各個頻率分別在圖片中佔多少比例

或者也可以想成,「任何圖片都可以被當作不同頻率的餘弦波合成」。人類來說低頻才是我們想要的信號,我們大可以將原信號的高頻係數降低,甚至調整為 0,也不會影響原圖太多。

2-D 離散餘弦轉換

2D 的離散餘弦轉換公式較為複雜,但背後原理仍然相同。圖片會先拆成 8x8 的小區塊並轉換成 DCT 係數。

img

Qauntitation Table

如果直接保存 DCT 的係數矩陣,那麼原圖並沒有任何壓縮。在實務上會將 DCT 與一個量化表相乘,會得到一個新的 DCT 係數矩陣,這個係數矩陣大部分高頻的係數為零。JPG 演算法會將圖片拆分為數個 8x8 的小區塊,並在這些小區塊間計算 DCT。

在選擇壓縮率的時候,量化表裡頭的係數也會不同。如果沒有壓縮,那麼量化表裡頭的元素都會是 1(圖片的每個像素都保留)。

Zig-zag

DCT 係數矩陣和量化表相乘之後,高頻的部分大部分的元素都是 0。矩陣的左上方是低頻、越往右下則越高頻。為了將低頻的數字放到前面,在壓縮圖片檔時會以下圖的方式走訪。

img

舉例來說,如果和量化表相乘後的矩陣是這樣:

[4569135010000000]\begin{bmatrix} 4 & 5 & 6 & 9 \\ 1 & 3 & 5 & 0 \\ 1 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 \end{bmatrix}

那麼實際在編碼時會把它變成:

[4, 5, 1, 1, 3, 6, 9, 5, 0, 0, 0, 0, 0, 0, 0, 0]

這是為了之後的壓縮方便。

霍夫曼編碼

霍夫曼編碼是一個無損數據壓縮的演算法,可以很有效地將字串編碼。在 JPG 編碼的時候,會有一大部分的元素是 0,霍夫曼編碼能夠將出現頻率高的字母使用較短的編碼,頻率低的字母則使用較長的編碼。在 JPG 當中,DCT 矩陣會經過霍夫曼編碼後儲存。

實際解碼 JPEG

解碼 JPEG 的過程如下:

  • 將檔案中的霍夫曼編碼表取出
  • 將檔案中的 DCT 係數矩陣取出
  • 將計算 DCT 逆矩陣後乘上量化表係數,得到圖片該處的像素
  • 將 YCbCr 轉換為 RGB

以下是簡易的 JavaScript 實作,有很多部分是參考這篇文章(Understanding and Decoding a JPEG Image using Python),可以在 Codepen 看到完整程式碼,將解碼後的結果渲染到 canvas。

注意這邊不是用 putImageData,而是直接讀取 JPG 檔案的內容後一格一格渲染到 canvas 上。(當然,在實際開發中我們不會這麼做)

function parseJPG(data) {
  const hfTables = {};
  const quantizationTables = {};
  let quantMapping;
  let d = new Uint8Array(data);
  let w;
  let h;
  while (true) {
    const marker = new Uint8Array(d.slice(0, 2));
    // console.log(marker[0].toString(16), marker[1].toString(16));
    if (marker[0] === 0xff && marker[1] === 0xd8) {
      console.log("start of image");
      d = d.slice(2);
    } else if (marker[0] === 0xff && marker[1] === 0xd9) {
      console.log("end of image");
      return;
    } else {
      const lenchunk = d.slice(2, 4);
      let len = (lenchunk[0] << 8) + lenchunk[1] + 2;

      const chunk = d.slice(4, len);

      if (marker[0] === 0xff && marker[1] === 0xc4) {
        // console.log(d, chunk);
        const { table, header } = decodeHuffman(chunk);
        hfTables[header] = table;
      } else if (marker[0] === 0xff && marker[1] === 0xdb) {
        const { header, table } = buildQuantizationTables(chunk);
        quantizationTables[header] = table;
      } else if (marker[0] === 0xff && marker[1] === 0xc0) {
        // start of frame
        const { result, width, height } = baseDCT(chunk);
        quantMapping = result;
        w = width;
        h = height;
      } else if (marker[0] === 0xff && marker[1] === 0xda) {
        // console.log(quantMapping, quantizationTables);
        len = startOfScan({
          data: d,
          hdrlen: len,
          width: w,
          height: h,
          quantizationTables,
          quantMapping,
          hfTables,
        });
      }

      d = d.slice(len);
    }
    if (d.length === 0) {
      break;
    }
  }

JPG 檔案裡有很多區塊,區塊之間以一個 0xff 的標記符來識別:

  • 0xffc4 為霍夫曼編碼表
  • 0xffda 為 start of scan
  • 0xffdb 為量化表
  • 0xffd8 為 JPG 的檔案開頭標記符
  • 0xffd9 為 JPG 的檔案結尾標記符

先將 binary 轉為對應的資料(霍夫曼編碼表、量化表)後再做對應計算就可以得到矩陣了!

測試之後發現沒辦法成功解碼,應該是程式碼有問題,如果有看實作的話,可以發現計算其實還蠻煩瑣的。由於時間不夠這邊就先丟出來給大家參考~

failture

(上面似乎有渲染到正確的顏色,但剩下的部分都綠綠的)

後記

在實際應用上,GPU 或是 CPU 都有圖片編解碼的電路,通常不用另外寫程式去處理。然而 JPG 背後的演算法結合了離散餘弦轉換、霍夫曼編碼、zig-zag 走訪,成功減少了圖片的儲存空間。我很喜歡這種平時視為理所當然的事情,深入研究之後才發現有很多值得令人學習的知識。

上一篇

逆矩陣沒有想像中的好

下一篇

那些比較冷門的 Networking 理論 (1)

如果覺得這篇文章對你有幫助的話,可以考慮到下面的連結請我喝一杯 ☕️ 可以讓我平凡的一天變得閃閃發光 ✨

Buy me a coffee