問題描述
最近在做 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:
圖片向右旋轉 90°,實質就是設法將 A 變爲 A’ ——
這可以通過原矩陣一次 轉置、與多次初等 列 變換(逆序排列各列)得到:
同理,圖片向左旋轉 90°,實際上就是得到矩陣 A’' :
這可以通過原矩陣一次 轉置、與多次初等 行 變換(逆序排列各行)得到——
基本原理2——像素數組與矩陣的對應關係
由於 ImageData.data
對應一個數組,對於 4 × 3
的圖片而言,ImageData.data
就是一個具有 48 個元素的數組 D,不妨每個元素的值就是其下標值,則:
其中:
元組 (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 的各個元素:
可見各像素點是按照 從左至右、從上至下 的順序排列的。設圖片總寬度像素爲 W,總高度像素爲 H,任一像素點 P 的座標爲 (x, y)
,P 的紅色值在數組 D 的下標爲 R(x, y)
,則:
驗證:(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)
:
其中,式(8-3)用於 轉置 運算;式(8-1)、式(8-2)分別用於 初等行變換 及 初等列變換。
具體實現
基本思路:
- 通過
canvas
獲取目標圖片的ImageData
對象; - 轉置原圖片數組,得到數組 AT;
- 對 AT 執行一組初等行變換,使各行逆序排列,得到左旋 90° 效果;
- 對 AT 執行一組初等列變換,使各列逆序排列,得到右旋 90° 效果;
- 將新的像素數組寫回圖片源標籤。
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°:
示例文件
鏈接: https://pan.baidu.com/s/1w1_5qh3Tg95VUUjLmhrvTA
提取碼: v7f6