做者:杨科git
近期咱们开发了一个银行卡 OCR 项目。需求是用手机对着银行卡拍摄之后,经过推理,能够识别出卡片上的卡号。github
工程开发过程当中,咱们发现手机拍摄之后的图像,并不能知足模型的输入要求。以 Android 为例,从摄像头获取到的预览图像是带 90 度旋转的 NV21 格式的图片,而咱们的模型要求的输入,只须要卡片区域这一块的图像,而且须要转成固定尺寸的 BGR 格式。因此在图像输入到模型以前,咱们须要对采集到的图像作图像处理,以下图所示:算法
在开发的过程当中,咱们对 YUV 图像格式和 libyuv 进行了研究,也积累了一些经验。
下文咱们结合银行卡 OCR 项目,讲一讲里面涉及到的一些基础知识:网络
想要对采集到的YUV格式的图像进行处理,首先咱们须要了解什么是 YUV 格式。架构
YUV 是一种颜色编码方法,YUV,分为三个份量:ide
“Y” 表示明亮度(Luminance或Luma),也就是灰度值;学习
“U”和“V” 表示的则是色度(Chrominance或Chroma)。编码
主流的采样方式有三种,YUV4:4:4
,YUV4:2:2
,YUV4:2:0
。操作系统
这部分专业的知识,网络上有详细的解释。咱们简单理解一下,RGB 和 YUV 都使用三个值来描述一个像素点,只是这三个值的意义不一样。经过固定的公式,咱们能够对 RGB 和 YUV 进行相互转换。3d
工程里常见的I420,NV21,NV12,都是属于YUV420,每四个Y共用一组UV份量。YUV420主要包含两种格式,YUV420SP 和YUV420P。
了解了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 里的图像处理组件,还在不断开发和完善中。
欢迎你们来使用和提建议: https://github.com/didi/aoe