近期咱們開發了一個銀行卡 OCR 項目。需求是用手機對着銀行卡拍攝之後,經過推理,能夠識別出卡片上的卡號。git
工程開發過程當中,咱們發現手機拍攝之後的圖像,並不能知足模型的輸入要求。以 Android 爲例,從攝像頭獲取到的預覽圖像是帶 90 度旋轉的 NV21 格式的圖片,而咱們的模型要求的輸入,只須要卡片區域這一塊的圖像,而且須要轉成固定尺寸的 BGR 格式。因此在圖像輸入到模型以前,咱們須要對採集到的圖像作圖像處理,以下圖所示:github
在開發的過程當中,咱們對 YUV 圖像格式和 libyuv 進行了研究,也積累了一些經驗。
下文咱們結合銀行卡 OCR 項目,講一講裏面涉及到的一些基礎知識:算法
想要對採集到的YUV格式的圖像進行處理,首先咱們須要瞭解什麼是 YUV 格式。bash
YUV 是一種顏色編碼方法,YUV,分爲三個份量:網絡
「Y」 表示明亮度(Luminance或Luma),也就是灰度值;架構
「U」和「V」 表示的則是色度(Chrominance或Chroma)。ide
主流的採樣方式有三種,YUV4:4:4
,YUV4:2:2
,YUV4:2:0
。學習
這部分專業的知識,網絡上有詳細的解釋。咱們簡單理解一下,RGB 和 YUV 都使用三個值來描述一個像素點,只是這三個值的意義不一樣。經過固定的公式,咱們能夠對 RGB 和 YUV 進行相互轉換。編碼
工程裏常見的I420,NV21,NV12,都是屬於YUV420,每四個Y共用一組UV份量。YUV420主要包含兩種格式,YUV420SP 和YUV420P。spa
瞭解了YUV的圖像格式之後,咱們就能夠嘗試對圖片進行裁剪和旋轉了。
咱們的想法是先在圖片上裁剪出銀行卡的區域,再進行一次旋轉。
YUV420SP 和 YUV420P 裁剪的過程相似,以 YUV420SP 爲例,咱們要裁剪圖中的這塊區域:
在圖上看起來就很是明顯了,只要找到裁剪區域對應的Y份量和UV份量,按行拷貝到目標空間裏就能夠了。 咱們再來看一張圖,是否能夠用上面的方法來裁剪圖中的這塊區域呢? 答案是否認的,若是你按照上面說的方法來操做,最後你會發現你保存出來的圖,顏色基本是不對的,甚至會有內存錯誤。緣由很簡單,仔細觀察一下,當 ClipLeft 或者 ClipTop 是奇數的時候,會致使拷貝的時候UV份量錯亂。對上文裁剪後的圖像作順時針90度旋轉,相比裁剪,轉換要稍微複雜一些。
基本方法是同樣的,拷貝對應的 Y 份量和 UV 份量到目標空間裏。
在瞭解了裁剪和旋轉的方法之後,咱們發如今學習的過程當中不可避免地遇到了 Stride 這個詞。
那它在圖像中的做用是什麼呢?
Stride 是很是重要的一個概念,Stride 指在內存中每行像素所佔的空間,它是一個大於等於圖像寬度的內存對齊的長度。以下圖所示:
回過頭來看咱們上面說到的裁剪和旋轉,是否有什麼問題?
以 Android 上的YV12爲例,Google Doc 裏是這樣描述的:
YV12 is a 4:2:0 YCrCb planar format comprised of a WxH Y plane followed by (W/2) x (H/2) Cr and Cb planes.
This format assumes
• an even width
• an even height
• a horizontal stride multiple of 16 pixels
• a vertical stride equal to the height
y_size = stride * height
c_stride = ALIGN(stride / 2, 16)
c_size = c_stride * height / 2
size = y_size + c_size * 2
cr_offset = y_size
cb_offize = y_size + c_size
複製代碼
因此在不一樣的平臺和設備上,須要按照文檔和 stride 來進行計算。例如計算 Buffer 的大小,不少文章都是簡單的 「*3/2」 ,仔細考慮一下,這實際上是有問題的。
若是不考慮 stride ,會有帶來什麼後果?若是 「運氣」 足夠好,一切看起來很正常。「運氣」不夠好,你會發現不少奇怪的問題,例如花屏,綠條紋,內存錯誤等等。這和咱們日常工做中遇到的不少的奇怪問題同樣,實際上背後都是有深層次的緣由的。
通過裁剪和旋轉,咱們只須要把圖像縮放成模型須要的尺寸,轉成模型須要的BGR格式就能夠了。
以縮放爲例,有臨近插值,線性插值,立方插值,蘭索斯插值等算法。YUV 和 RGB 之間的轉換,轉換的公式也有不少種,例如量化和非量化。這些涉及到專業的知識,須要大量的時間去學習和理解。
這麼多的轉換,咱們是否都要本身去實現?
不少優秀的開源項目已經提供了完善的 API 給咱們調用,例如 OpenCV,libyuv 等。咱們須要作的是理解基本的原理,站在別人的肩膀上,作到內心有數,這樣即便遇到問題,也能很快地定位解決。
通過調查和比較,咱們選擇了 libyuv 來作圖像處理的庫。libyuv 是 Google 開源的實現各類 YUV 與 RGB 之間相互轉換、旋轉、縮放的庫。它是跨平臺的,可在 Windows、Linux、Mac、Android 等操做系統,x8六、x6四、arm 架構上進行編譯運行,支持 SSE、AVX、NEON等SIMD 指令加速。
引入libyuv之後,咱們只須要調用libyuv提供的相關API就能夠了。
在銀行卡OCR工程使用的過程當中,咱們主要遇到了2個問題:
1,在Android開發的初期,咱們發現識別率和咱們的指望存在必定的差距。
咱們懷疑是模型的輸入數據有問題,經過排查發現是使用libyuv的時候,沒注意到它是little endian。例如這個方法:int BGRAToARGB(...),BGRA little endian,在內存裏順序實際是ARGB。因此在使用的時候須要弄清楚你的數據在內存裏是什麼順序的,修改這個問題後識別率達到了咱們的預期。
2,在大部分機型上運行正常,但在部分機型上出現了 Native 層的內存異常。
經過屢次定位,最後發現是 stride 和 buffersize 的計算錯誤引發的。
經過銀行卡 OCR 項目,咱們積累了相關的經驗。另外,因爲 libyuv 是 C/C++ 實現的,使用的時候不是那麼的便捷。爲了提升開發效率,咱們提取了一個 Vision 組件,對libyuv封裝了一層 JNI 接口,包括了一些基礎的轉換和一些 sample,這樣使用起來更加簡單方便了。做爲AOE SDK 裏的圖像處理組件,還在不斷開發和完善中。
歡迎你們來使用和提建議: github.com/didi/aoe