使用Tensorflow Object Detection API进行集装箱识别并对集装箱号进行OCR识别
两年多之前我在“ex公司”的时候,有一个明确的项目需求是集装箱识别并计数,然后通过OCR识别出之前计数的每一个集装箱号,与其余业务系统的数据进行交换,以实现特定的整体需求。当时正好Tensorflow Object Detection API 发布了,就放弃了YOLO或者SSD的选项,考虑用TF实现Demo做POC验证了。
背景
之前也并未接触过Deep Learning相关的事情,为了验证这个需求可以实现并满足交付要求,从入门到了解到选择到学习到准备图片并标注到完成基本的Python Demo验证一个人前后差不多用了2个月时间(那时候用的还是12年中的MacBook Air),验证完成后,交给了C++和C#的小伙伴们去实现。在这个过程中感受到了Deep Learning的魅力与强大以及对于未来的无限可能,就带着我的研发小伙伴们走向这条路前进了,人脸、语音这些都尝试了,也都嵌入了已有都产品中,也不会因为只有第三方算法而各种踩坑了。
最近重装了Mac,重写个Demo验证下Inteld对于CPU的DL支持有多大改善,说实话,代码不重要,一点不复杂,我更想借这个Demo说一下做AI技术工程化落地的思考方法和实现过程。
问题分析
1、明确要具体解决的问题
这个需求有两个关键点,主要应用场景是在只有单摄像头监控的位置,实现集装箱装卸过程中:如何准确识别集装箱,识别后如何去重才能保证“计数”的准确;如何在单摄像头且非约束的场景下做集装箱号的OCR。
已有的解决方案,集装箱识别——没有;集装箱号OCR——有,但是都是约束场景下的识别。即:集装箱与摄像头的位置关系有明确限制且很有可能使用多达3个摄像头做联合判断后输出结果,与实际场景不符,等于没有。市面上大多在“卡口”做集装箱号识别的方案主流是在停车发卡的位置固定三路摄像机拍摄。
所以这个需求能否实现,关键就在于单摄像头非约束场景下的detection与OCR了。
2、为什么不用SSD与YOLO
这两个目标检测的方法都很快,识别准确率也足够,之前确实考虑选其中之一来实现,但是想明白了这件事怎么做,也准备好了几千张图片准备验证的时候 Object Detection API 发布了,DL这种对未来影响深远的事情,选择一个最有可能成为大生态环境的平台就很重要了,Google虽然之前有“说关就关”的“前科”但是在我的理解,不论软件还是硬件层面,DL的inference能力就是未来的OS之上最重要的事情了,甚至可以理解为就是OS的一部分(比如SoC),所以,这事情Google任性可能性微乎其微,从这两年发展来看,Google对TF持续投入,而且做为“大厂”的标志之一似乎就是你有没有开源的DL相关工作。
3、当时不担心搞不定么?
在此之前整个计数团队仅有一人在微软参与过人脸识别相关的工作,而且还是机器学习实现,非深学习实现。非常担心,真的担心搞不定,所以连一块1080Ti的采购需求都没敢提,前期工作全部用我的12年老MBA完成的。就是由于真的怕搞不定,所以我没有玩弄所谓的“权术”。我是公司的研发总,所以理论上,我把这个Demo的预研工作交出去,找两个人去搞定,搞定了我工作依然完成了,搞不定我还有“替死鬼”。我也不是没事干,产品、架构、评审、项目、质量保证、销售以及没完没了的各种会议,但是这个事情真没底,不知道能不能搞定,安排工作简单,但是安排自己都没把握的工作出去虽然是一种“管理智慧”,但是不是本人做事、为人风格。于是就白天干工作职责之内的事情,下班后再来做这件事,能通宵就通宵,周末?周末是不可能有的。所以说起来2个月时间,其实是2个月的业余时间。
深度学习在2018年持续火爆,想上车的同学越来越多,在此如果你机会看到这段文字,以我的实际情况来看,我可以负责任的告诉你:只要你全情投入,肯定可以上车并且走的很远。
基本pipeline
内容很长,先说结论。
我把这套pipeline的定义分析过程叫做“最大误差分析法”。即:先找到整个环节中潜在误差最大的环节,先推导出有可能实现这个误差最大环节的最优解,再基于这个最优解向前后寻找pipeline其余环节的最优解,这样定义出来的pipeline有极大概率是解决整个问题的最优解。
定义问题
如前所述,需求说起来很简单,只有三个核心:单摄像头、计数、OCR。
定义问题解决路线
做为一个做泛安防与视频的公司,多少是有一些技术积累和对计算机视觉的认识,站在DL与CV的角度,从当时看到的资料整体判断与分析,集装箱识别的实现以及准确性肯定是要优于集装箱号的OCR的,要整体解决工程需求,实现工程落地的准确性,应该先设计并寻找让集装箱号OCR准确性能提高的技术路线,因为这个才是工程最终准确率的关键瓶颈。
所有的工作都要在一个摄像头的视频流中完成,那么我们解决问题的路线图应该是:–>寻找OCR的最优解–>基于OCR的最优解寻找集装箱识别与计数的最优解–>整体分析最优解的实现可能并预估最终结果的可接受度。如果,判断最终结果不可接受,那就回归到原点重新换思路。
先大致说一下集装箱。集装箱做为一个立方体,共有6个面,底面肯定是摆放用的,不需要考虑,那么剩下5个面都有什么信息?一般来说5个面都会喷涂有集装箱号,不过是大小和细节位置的区别,其中集装箱门(标准用词:端门,door)这一面的信息最全,除了集装箱编号以外其余的信息都在这一面。集装箱的设计为了轻量化和能承受堆叠载荷,侧面都是用的瓦楞钢板可以大幅提高轴向载荷能力,另外在顶面也喷涂有集装箱号。
确定OCR目标区域
集装箱的侧面都是用的瓦楞钢板可以大幅提高轴向载荷能力,对于OCR来说这就是极大的挑战了,因为不是一个平面了,视角不同,肉眼看都会出错,先排除4个侧面,就剩一个顶面的集装箱号了。事实上,这个位置的集装箱号做OCR确实很合适,第一位置相对规范,喷涂位置在集装箱的钢架上,第二此处是一个平面,第三此处的集装箱号很少出现污损的情况。如果要确保能准确采集到这个位置的集装箱号,那么摄像机一定是俯拍,才有可行性,那么带来的问题就是,如果要俯拍,那么就要确保有安装位置。但是由于业务场景限制,并非集装箱装卸一定是龙门吊,还有可能是正面吊(这两个名词,还请自行Google了),如果都是龙门吊,是可以考虑在吊臂安装摄像机的(大家要是天天看新闻联播,说过一次“自动化无人码头”,这里的自动化就是用的吊臂安装摄像机做OCR后,告诉无人车把这个箱子送到什么位置)。但是,对于正面吊,装摄像机是没有任何可能性的,就算想法子装上去,估计损毁几率也非常高,所以,首先放弃的就是条件最好的这个位置。
回头再看初步放弃的四个侧面,只有集装箱门这一面绝大多数的集装箱所有者都会把集装箱号喷涂在相对较小的那块平面上,所以,对于我们的场景,取这个位置的集装箱号做OCR是有且仅有的靠谱方案。
如何确保OCR的基础准确性
怎么理解这个“基础准确性”呢?熟悉OpenCV的同学都知道,用cv2.dilate,cv2.erode的方法膨胀、腐蚀来一组,再找一下轮廓,也能实现文字块的检测,前面已经初步确定了要用集装箱门的集装箱号来做OCR,如果把整幅图片交给OpenCV来找到文字块,再交给后端的OCR接手理论上是可以,但是,仔细观察一下集装箱门,文字块太多了,先不谈检测准确性,仅集装箱号检测这一个任务“噪音与干扰”太大了,最终结果不好保障,所以,要先解决集装箱号这个文字块的提取工作,后面的OCR才会更有准确率保障。
不用OpenCV,直接用NN网络来一步到位解决文字检测和OCR的问题是不更好呢?嗯,理论上NN肯定效果更好,但是,如何做Demo的验证,如何找大量数据集做训练?这是个短时间无解的问题,大家可以了解下FSNS dataset的规模,做为中期技术路线规划,可以考虑用NN去做OCR。天下武功,唯快不破,但是需求就在眼前,客户可不会听你解释几个小时技术方案和技术先进性,然后说我要半年去收集数据,半年后我拿Demo来,你看满意不。你觉得,你是客户,你答应么?所以还要思考别的解决方案。
目前两难的情况是:让OpenCV去大面积找字符,准确性不好保障,用NN来做,准确性有可能保障,但是时间窗口是没有的。后者,无解;那么就看前者有没有破局的解法。目标检测是干嘛的?为什么不能把集装箱号这个文字区域当做一个“目标”来检测呢?这样得到的就是一个仅有集装箱号的小文字块,后面不管怎么OCR都不需要再去考虑多余文字的噪音干扰问题了。想到就干,标了一个通宵的图,老破本跑了一天的训练,测试下来行得通,单检测集装箱号准确率非常高。OCR的输入问题解决了,起码在这个层面上不会对最终准确性有大的影响了。
到此,整个pipeline是这样了:目标检测识别集装箱号–>送入OCR引擎–>OCR结果输出 。下面就是思考OCR引擎如何实现了。
OCR引擎选择
之前已经放弃了短期内用NN实现OCR的考虑了,那么只能回到传统的机器学习的思路了,这就简单了,不用选了显然是考虑Tesseract了。那时候还是3.05版本,4.0的LSTM版本还没有正式发布,不敢用,Tesseract怎么训练可以参考我的这篇文章:使用Tesseract训练并识别集装箱号。到真正开干的时候,手头有大几千张集装箱图片,手动扣出来,再做点处理,凑个万把张集装箱号图片还是可以的,训练tesseract还是够用的,但是想都不用想,准确性超8成都不容易,原因是在这里Attention-based Extraction of Structured Information from Street View Imagery有这么一段话:
Disclaimer This code is a modified version of the internal model we used for our paper. Currently it reaches 83.79% full sequence accuracy after 400k steps of training. The main difference between this version and the version used in the paper - for the paper we used a distributed training with 50 GPU (K80) workers (asynchronous updates), the provided checkpoint was created using this code after ~6 days of training on a single GPU (Titan X) (it reached 81% after 24 hours of training), the coordinate encoding is disabled by default.
所以,在Tesseract之外,应该还有预备另一套解决方案,否则就是明知有可能死还要去死了。听起来像是笑话:以你手头的资源和技术,你已经没路可选了,你还要给自己多留一条路?嗯,世上无难事,只怕肯琢磨。在训练Tesseract标注的过程中,最大的收获就是基本理解了Tesseract OCR的实现pipeline:图片二值化–>字符边框–>分割–>识别。一遍一遍的训练,再傻也琢磨出来了:关键还是边框要准,边框准,识别就会准很多。前面已经验证过了用目标检测的方法找到集装箱号这个区域是很准确的,那么我把检测到到的集装箱号再做一次标注,再训练一个36分类的目标检测,不是就变相的实习了OCR么?
集装箱号是顺序的,这么目标检测打散了输出,怎么再拼回原来的特定顺序?这个不难。修改一下visualization_utils.py里的visualize_boxes_and_labels_on_image_array函数的定义,让他多返回一个box_to_color_map参数,这个返回值是一个Dictionary,其中key是我们训练定义的label,value是一个四个值组成的List,四个值分别是输出的bounding box的相对原始图片坐标原点的相对偏移值,拿这个list沿着X轴,Y轴分别做一次排序,label的顺序就是集装箱号的顺序了。
集装箱识别与计数
到目前为止,构想中的pipeline已经实现了OCR部分的设计,下面就是集装箱识别与计数了。之前已经给定了一个条件:单摄像机,现在又多了一个条件:需要集装箱门(端门)出现在摄像机画面中才行,否则无法找到我们需要用来做OCR的集装箱号区域。所以,集装箱识别就简单了,理论上,只需要标注集装箱门、集装箱号这两个标签,就够用了。这又带来一个新问题:“如何确保始终是集装箱门可以对着摄像机位置?”。与用户沟通、现场实地勘查后确定:工作过程中不可能要求并约束集装箱门的朝向,即使这样要求,也无法确保工作人员一定就按你要求来。
继续考虑单摄像机方案,就意味着至少50%概率要OCR识别的是集装箱后门(标准用词:盲端,end_door)喷涂的集装箱号,结果就是即使短时间做出来Demo了,验证准确性也不会好,做出来和做不出来会变成一个结果。
毫不谦虚的说,这个时候就体现出了我这个非典型码农的复合知识结构价值了。
直接在现场勘查阶段和用户沟通确定,既然做不到肯定给我识别到集装箱门,那么只有现场架设两个摄像机对拍,只要集装箱出现,肯定有一个摄像机画面中是集装箱门。然后,进一步确定了摄像机的安装位置、角度、光照条件(正好东西走向,早上、傍晚总有一个摄像机是大逆光)、网络、供电等一系列施工要求;再然后,描述了整个识别业务流程和基本的页面展示效果、会展示的数据以及对他们对接数据对接要求,最后明确了最终效果:不可能做到100%准确这个标准,大致准确率在8成以上,这样的结果如何进行人工后续复核与干预。一切谈拢,回去就是一个字:干!
算法训练与产品实现
划重点:不会销售的产品不是好码农
用户沟通三要素:
- 明确需求、目标
- 明确权利、义务
- 明确交付标准、控制期望
采集数据、产品设计
前期做简单验证的时候,图片数据只有一千张不到,这个量级肯定不够,发动所有销售和现场交付人员,有条件没条件都要创造条件去集装箱货场、码头拍照片,到最后总算弄了、8千张照片回来,逐一筛选后,留下了七千张能用的。本来数据集就小,不去人工做筛选,弄点脏数据进去,坑自己啊。 普通的产品经理接到这样的任务可能就好干了:pipeline你也确定了,页面展示和数据你也确定了,目标检测的标签和标注内容你也确定了,那我就按你说的干呗,肯定没错,领导说的嘛。嗯,我真遇到了一个这样的普通产品经理,一点没有扩展思路和增加产品价值的想法,于是,我把她开了,这事情从头你就在跟,现场你也跟着去了看了,和用户怎么谈的全程你也参与了,回来我也给你说了我的想法和这个产品怎么做出更多让用户看到后惊喜的东西,但是,你都没有get到……
好的产品经理不是万里挑一就是千里挑一,比例不会再高了。
最后,定了6个label,3个是不同位置的集装箱编号,3个是集装箱的侧面和两个端面。因为,做的是一个用户的需求的事情,站在产品的层面需要考虑的是一个行业的事情,这个事情做出来,落地了,在行业内别的用户怎么把这种技术导入到人家的需求里面,这个是我在标注图片的时候考虑的最多的事情,反正标注图片是体力活,脑袋闲着也是闲着。只有多定义标签后续才有更多的利用可能。
选择模型、训练
目标检测模型选择这个事情么,之前更小的数据集已经做过训练验证,对于最终的准确性是放心的,反正我做的是迁移学习,训练集也不大,10万步也不要几天,只需要在快和准之间做好平衡就好,最后定的是生产用faster_rcnn_inception_v2,后来做人脸,我也用的他做了人脸检测的尝试,也把数据集从VOC格式转到YOLO格式,在DarkNet下也训练了两个YOLO v2的模型出来,然后把tiny版本的部署到NVIDIA Jetson TX2上面,跑跑看效果,琢磨下如果用Tetson TX 2做边缘设备,把这个落地方案做成边缘版本的可能性。后面这一波操作都是自娱自乐,总要有学习新东西的快乐么,当然了,到这个阶段我办公室已经有2*1080Ti的PC和4*1080Ti的服务器\4*P100的服务器各一台了,我可以一边训练一边愉快的玩耍了。
如果说目标检测训练是玩耍的话,那么Tesseract的训练就是折磨,训练结果果然不出意外最初就6、7成的样子,泛化能力很一般。刚开始百思不解为什么结果会这么意外,都已经准备动手执行目标检测的36类方案了。花了大概两周的时间,一遍一遍的训练,验证(Tesseract3.05就这点好,数据准备好,训练一轮分分钟的事情)。最后找到了泛化差的原因:训练的图片原始分辨率高低波动很大,训练出的lang文件打开来看,内部本来就有训练的错误,所以别指望输出能对。
于是同时做了两件事:1、花了好几天,把几千张图片按初始分辨率分为了4类,又再每一类里再细分3类,也就是说把训练样本定义为了12类,每个小类单独训练。因为Tesseract的Clustering操作有个特性是可以把多个样本的训练输出数据放在一起聚类打包,所以,我就愉快的在这15类之间做排列组合;2、让销售协调用户尽快按最终的部署场景给采集一批实际作业视频与图片,协调的结果是销售哥们儿自己买了2个摄像机配上三脚架,问人家晚上有作业的时间,顶着刺骨寒风,坐在集装箱上面用笔记本录了两晚上的视频,然后,嗯感冒了。然后,目标检测的算法已经好了,视频拿来跑一圈,把集装箱号裁剪出来,拿来做Tesseract的验证,就这样痛苦的折磨了自己差不多2个月,总算基本搞定,再加上一些工程化的技巧和数据处理,基本具备拿出去给用户POC的条件了。
pipeline的demo实现
代码在这里:https://github.com/lonelygo/container_detection
写在最后的话
写到这里,篇文章也算要结尾了。
重装了电脑,装了一个Intel预编译版的TF也不知道干点啥好,据说是比Google预编译的CPU版本的快很多,但是我现在手头没有GPU的电脑啊,没法比,所以就拿手里有的一部分数据,重新把17年的这个过程再玩一遍,当时CPU什么速度,GPU什么速度我还是记得的,这样比对我而言最容易。