利用 ImageData 實現圖片左右旋轉 90°

問題描述

最近在做 OCR 圖像識別。大致流程是先拿到預覽區圖片的 base64 字符串,根據接口要求壓縮 base64 字符串大小,再調用 OCR 相關接口獲取識別結果。然而,通過文件上傳 input 域、FileReader 讀到的 base64 字符串直接放入 img 標籤後,預覽出的圖片往往會出現 原本爲縱向拍攝的圖片默認按橫向圖片展示,從而導致後續 OCR 識別報錯。

方向正確:
在這裏插入圖片描述
方向錯誤:
在這裏插入圖片描述
究其原因,可能是 canvas.toDataURL(type, quality) 生成的 base64 字符串沒有圖片朝向相關的標識,賦給 img.src 後,img 標籤 默認將較長的一邊作爲寬度、較短的一邊作爲高度 來顯示圖片。要解決這個問題,需要在圖片加載完畢後,手動調節圖片的朝向(右轉或左轉 90°)。

網上關於圖片轉向的文章大多通過構造新的 canvas 畫布,設置具體的轉向角度後重繪圖片,最後在寫回原圖片。結合項目實際需求,只需要簡單左右旋轉 90° 即可。這可以通過 ImageData 對象的像素變換輕鬆實現。

基本原理1——像素矩陣變換

ImageData 是圖片經數據化處理後的對象,其中包含三個屬性:

  • width:圖片的總寬度像素值(整數)
  • height:圖片的總高度像素值(整數)
  • data:八位無符號整型固定數組、一個特殊的類型數組。該數組每 4 個元素的值,依次描述了對應像素點的 R、G、B、A 的取值,值域均爲 [0, 255]。

因此一個 4 × 3 像素的原始圖片,可以看作如下形式的像素矩陣 A
A=[a11a12a13a14a21a22a23a24a31a32a33a34](1) A = \left[ \begin{matrix} a_{11} & a_{12} & a_{13} & a_{14} \\ a_{21} & a_{22} & a_{23} & a_{24} \\ a_{31} & a_{32} & a_{33} & a_{34} \end{matrix} \right] \tag{1}
圖片向右旋轉 90°,實質就是設法將 A 變爲 A’ ——
A=[a31a21a11a32a22a12a33a23a13a34a24a14](2) A'= \left[ \begin{matrix} a_{31} & a_{21} & a_{11}\\ a_{32} & a_{22} & a_{12}\\ a_{33} & a_{23} & a_{13}\\ a_{34} & a_{24} & a_{14} \end{matrix} \right] \tag{2}

這可以通過原矩陣一次 轉置、與多次初等 變換(逆序排列各列)得到:

AT=[a11a12a13a14a21a22a23a24a31a32a33a34]T=[a11a21a31a12a22a32a13a23a33a14a24a34]=>[a31a21a11a32a22a12a33a23a13a34a24a14]=A(3) A^T=\left[ \begin{matrix} a_{11} & a_{12} & a_{13} & a_{14} \\ a_{21} & a_{22} & a_{23} & a_{24} \\ a_{31} & a_{32} & a_{33} & a_{34} \end{matrix} \right]^T= \left[ \begin{matrix} a_{11} & a_{21} & a_{31}\\ a_{12} & a_{22} & a_{32}\\ a_{13} & a_{23} & a_{33}\\ a_{14} & a_{24} & a_{34} \end{matrix} \right] => \left[ \begin{matrix} a_{31} & a_{21} & a_{11}\\ a_{32} & a_{22} & a_{12}\\ a_{33} & a_{23} & a_{13}\\ a_{34} & a_{24} & a_{14} \end{matrix} \right] = A' \tag{3}

同理,圖片向左旋轉 90°,實際上就是得到矩陣 A’'

A=[a14a24a34a13a23a33a12a22a32a11a21a31](4) A''= \left[ \begin{matrix} a_{14} & a_{24} & a_{34}\\ a_{13} & a_{23} & a_{33}\\ a_{12} & a_{22} & a_{32}\\ a_{11} & a_{21} & a_{31} \end{matrix} \right] \tag{4}
這可以通過原矩陣一次 轉置、與多次初等 變換(逆序排列各行)得到——

AT=[a11a12a13a14a21a22a23a24a31a32a33a34]T=[a11a21a31a12a22a32a13a23a33a14a24a34]=>[a14a24a34a13a23a33a12a22a32a11a21a31]=A(5) A^T = \left[ \begin{matrix} a_{11} & a_{12} & a_{13} & a_{14} \\ a_{21} & a_{22} & a_{23} & a_{24} \\ a_{31} & a_{32} & a_{33} & a_{34} \end{matrix} \right]^T= \left[ \begin{matrix} a_{11} & a_{21} & a_{31}\\ a_{12} & a_{22} & a_{32}\\ a_{13} & a_{23} & a_{33}\\ a_{14} & a_{24} & a_{34} \end{matrix} \right]=> \left[ \begin{matrix} a_{14} & a_{24} & a_{34}\\ a_{13} & a_{23} & a_{33}\\ a_{12} & a_{22} & a_{32}\\ a_{11} & a_{21} & a_{31} \end{matrix} \right] = A'' \tag{5}

基本原理2——像素數組與矩陣的對應關係

由於 ImageData.data 對應一個數組,對於 4 × 3 的圖片而言,ImageData.data 就是一個具有 48 個元素的數組 D,不妨每個元素的值就是其下標值,則:
D=[0,1,2,3,4,5,6,7...44,45,46,47](6) D = \left[0, 1, 2, 3, 4, 5, 6, 7... 44, 45, 46, 47\right]\tag{6}
其中:

元組 (0, 1, 2, 3) 表示第 1(= 0 / 4 + 1) 個像素的顏色爲 rgba(0, 1, 2, 3/255)
元組 (4, 5, 6, 7) 表示第 2(= 4 / 4 + 1) 個像素的顏色爲 rgba(4, 5, 6, 7/255)
元組 (8, 9, 10, 11) 表示第 3(= 8 / 4 + 1) 個像素的顏色爲 rgba(8, 9, 10, 11/255)

元組 (i, i+1, i+2, i+3) 表示第 (i / 4 + 1) 個像素的顏色爲 rgba(i, i+1, i+2, (i+3)/255)

元組 (44, 45, 46, 47) 表示第 12(= 44 / 4 + 1) 個像素的顏色爲 rgba(44, 45, 46, 47/255)

可見從 0 開始遍歷 D 數組,每次遞增 4 個單位,即可依次得到各個像素的紅色值 R,再依次加1、加2、加3,即得到對應的綠色值 G、藍色值 B、等效 α 通道值 A。

反之,如果知道圖片的像素尺寸爲 4 × 3,則可以通過下圖找到數組 D 的各個元素:

imageMatrix

可見各像素點是按照 從左至右、從上至下 的順序排列的。設圖片總寬度像素爲 W,總高度像素爲 H,任一像素點 P 的座標爲 (x, y)P 的紅色值在數組 D 的下標爲 R(x, y),則:
R(x,y)=(x+Wy)×4(7) R(x, y) = (x + W · y) × 4 \tag{7}
驗證:(x 與 y 均從 0 開始計數)

R(2, 1) = (2 + 1 × 4) × 4 = 24
R(1, 2) = (1 + 2 × 4) × 4 = 36
R(3, 1) = (3 + 1 × 4) × 4 = 28

拿到了 R(x, y),不難求出該像素的縱向中心對稱像素 Rh(x, y)、橫向中心對稱像素 Rw(x, y)、以及主對角線對稱像素 Rd(x, y)
Rh(x,y)=[x+W(H1y)]×4(8-1) Rh(x, y) = [x + W · (H - 1 - y)] × 4 \tag{8-1}

Rw(x,y)=[(W1x)+Wy]×4(8-2) Rw(x, y) = [(W - 1 - x) + W · y] × 4 \tag{8-2}

Rd(x,y)=(y+Hx)×4(8-3) Rd(x, y) = (y + H · x) × 4 \tag{8-3}

其中,式(8-3)用於 轉置 運算;式(8-1)、式(8-2)分別用於 初等行變換初等列變換

具體實現

基本思路:

  1. 通過 canvas 獲取目標圖片的 ImageData 對象;
  2. 轉置原圖片數組,得到數組 AT
  3. AT 執行一組初等行變換,使各行逆序排列,得到左旋 90° 效果;
  4. AT 執行一組初等列變換,使各列逆序排列,得到右旋 90° 效果;
  5. 將新的像素數組寫回圖片源標籤。

HTML

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Rotate by ImageData</title>
    <style>
        .image{ margin-top: 5px; }
    </style>
</head>
<body>
    <div class="btns">
        <input type="button" value="左轉 90°" id="turnLeft" />
        <input type="button" value="右轉 90°" id="turnRight" />
    </div>
    <div class="image">
        <img id="fruit" src="fruit.jpg" class="image" alt="fruit" title="fruit" />
    </div>
    <script src="imageRotate.js"></script>
</body>

</html>

imageRotate.js

document.querySelector('#turnLeft' ).addEventListener('click', e => rotateImage('l'))
document.querySelector('#turnRight').addEventListener('click', e => rotateImage('r'))

function rotateImage(direction = 'l') {
    // 1. Prepare ImageData
    let img = document.querySelector('#fruit')
    const { width: W, height: H } = img
    let cvs = document.createElement('canvas')
    cvs.width = W
    cvs.height = H
    let ctx = cvs.getContext('2d')
    ctx.drawImage(img, 0, 0)
    let imgDt0 = ctx.getImageData(0, 0, W, H)
    let imgDt1 = new ImageData(H, W)
    let imgDt2 = new ImageData(H, W)
    let dt0 = imgDt0.data
    let dt1 = imgDt1.data
    let dt2 = imgDt2.data

    // 2. Transpose
    let r = r1 = 0  // index of red pixel in old and new ImageData, respectively
    for (let y = 0, lenH = H; y < lenH; y++) {
        for (let x = 0, lenW = W; x < lenW; x++) {
            r  = (x + lenW * y) * 4
            r1 = (y + lenH * x) * 4
            dt1[r1 + 0] = dt0[r + 0]
            dt1[r1 + 1] = dt0[r + 1]
            dt1[r1 + 2] = dt0[r + 2]
            dt1[r1 + 3] = dt0[r + 3]
        }
    }
    
    // 3. Reverse width / height
    for (let y = 0, lenH = W; y < lenH; y++) {
        for (let x = 0, lenW = H; x < lenW; x++) {
            r  = (x + lenW * y) * 4
            r1 = direction === 'l'
                ? (x + lenW * (lenH - 1 - y)) * 4
                : ((lenW - 1 - x) + lenW * y) * 4
            dt2[r1 + 0] = dt1[r + 0]
            dt2[r1 + 1] = dt1[r + 1]
            dt2[r1 + 2] = dt1[r + 2]
            dt2[r1 + 3] = dt1[r + 3]
        }
    }
    
    // 4. Redraw image
    cvs.width = H
    cvs.height = W
    ctx.clearRect(0, 0, W, H)
    ctx.putImageData(imgDt2, 0, 0, 0, 0, H, W)
    img.src = cvs.toDataURL('image/jpeg', 1)
}

運行結果:

原始圖片:
原始圖片
左轉 90°:
在這裏插入圖片描述
右轉 90°:
右轉 90°

示例文件

鏈接: https://pan.baidu.com/s/1w1_5qh3Tg95VUUjLmhrvTA
提取碼: v7f6

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章