快速入门开发实现订单类图片识别结果抽象解析

简介: 本文主要介绍一种针对订单类图片识别结果进行行列解析的抽象流程和方案,帮助提高开发效率。

一、背景

面对订单数据纸质文件或图片,仅靠人眼识别的话效率很低,需引入机器学习来识别和解析图片以提高效率。当前市面上已有收费的图片识别服务,包括阿里、百度等,识别效果较好,但针对订单类图片,不仅要关注图片上的文字,还要关注文字所在的行列,来分出每条数据和数据详细字段。

本文主要介绍一种针对订单类图片识别结果进行行列解析的抽象流程和方案,帮助提高开发效率。

注:本文只提供思路,不提供源码。另外,本文不介绍人工智能图片识别,感兴趣的同学可以上网查询相关资料。

二、解析流程

对于图像处理,opencv算是比较优秀的工具,因此将其选做本文图像处理首选软件。

  • 为了使图片识别率更高,需要先做图片矫正,这里采用较为简单的霍夫变换加去噪声点算法矫正图片。
  • 图片矫正后,调用图片识别服务获取结果,一般结果格式包括响应码、错误描述、文字块列表(文字和四点坐标)等。
  • 然后使用抽象的俄罗斯方块法根据识别结果获取行列信息。
  • 最后根据行列信息组装每一行数据并显示。

三、细节处理

3.1 opencv安装概要

opencv安装,本文只做简单提示,不展开介绍,以后有时间单独发文。

1)windows

2)linux

  • 推荐使用ubuntu,并且最好是全新的系统,因为opencv会依赖很多包,对版本要求也高,解决冲突会很麻烦。
  • 下载源码
  • 安装依赖包
  • 编译安装

我们使用java调用opencv,这里需要安装获取到开发包,windows为opencv_javaxxx.dll,linux为libopencv_javaxxx.so,程序初始化时需要加载到jvm。详细代码如下:

System.load(PropertieUtil.getPropertie("这里是dll或so的完整路径");

3.2 图片矫正

3.2.1 矫正探索

图片矫正探索之路较为艰辛,起初我们想了一个比较简单的方案:

  • 先调用图片识别服务,获取到结果。
  • 然后根据每一个字块的四角坐标判断出每个字块的倾斜角。
  • 再根据去燥算法算出平均的倾斜角。

理论上这个方案是可行的,但实践证明我们错了,因为图片识别服务返回的坐标图片不准确,多数图片算出的结果都是错误的。

经查发现霍夫变换有可能解决这个问题,于是开始尝试学习霍夫变换和去燥算法,最终发现可行,并抽象出公共方法,仅需简单配置一些参数就能完成矫正。

图片矫正分为两步:

  • 第一步:正反矫正,判断图片倾斜角度是90°、180°、270°、0°,这个通过数学方法是无法判断的,需要引用机器学习。
  • 第二步:角度微调,一般为确定图片是正的,且倾斜角度在+-30°左右。

需要注意的是,上面说的办法不可能通过一套参数来对所有图片进行微调,但线上数据证明,针对一类图片,一套参数基本能让大多数图片都矫正正确。

3.2.2 霍夫变换概要

霍夫变换是数学界经典空间变换算法,用于检测直线,通过大量检测到的直线的斜率就能计算出图片倾斜角度。先进行二值化和边缘检测再进行霍夫变换效果更佳,详细算法内容请自行搜索,本文不展开。

3.2.3 去噪声点算法

基本公式:

上限=均值+n*标准差

下限=均值-n*标准差

其中n取值一般为1-4,数值越大表示筛选率越高。

最后再将符合的数据求均值。

核心代码如下:

/**
     * 利用标准差筛选
     * @param values
     * @return
     */
    private static double[] calcBestCornList(double[] values) {
        // 计算标准差
        StandardDeviation variance = new StandardDeviation();
        double evaluate = variance.evaluate(values);
        Mean mean = new Mean();
        double meanValue = mean.evaluate(values);
        double biggerValue = meanValue + CHOOSE_POWER * evaluate;
        double smallerValue = meanValue - CHOOSE_POWER * evaluate;
        List<Double> selected = Lists.newArrayList();
        for (double value : values) {
            if (value >= smallerValue && value <= biggerValue) {
                selected.add(value);
            }
        }
        double[] selectedValue = new double[selected.size()];
        for (int i = 0; i < selected.size(); i++) {
            selectedValue[i] = selected.get(i);
        }
        logger.info("占比:{}%,筛选后角度数组:{}", (selectedValue.length / (float)values.length) * 100F, selected);
        return selectedValue;
    }

3.2.4 霍夫变化抽象封装

基本流程:

  • 定义相关参数
  • 读取图片
  • 灰度二值化处理
  • 使用opencv画出轮廓
  • 根据参数要求多次画霍夫变换线,直到线数量满足参数为止
  • 遍历画出的线,分出横线和竖线,根据配置计算出每条线的角度
  • 使用去噪声算法(需要根据非0数自动重复计算)算出平均倾斜角度
  • 使用opencv旋转图片

核心代码如下:

/**
     * 矫正图片,通过霍夫变换矫正
     * @param oldImg 原始图片
     * @param rotateParam 旋转参数
     * @return
     */
    public static String rotateHoughLines(File oldFile, String oldImg, RotateParam rotateParam, String cid, String bankCode) throws Exception {

        Mat src= Imgcodecs.imread(oldFile.getAbsolutePath());
        //读取图像到矩阵中
        if(src.empty()){
            throw new Exception("no file " + oldFile.getAbsolutePath());
        }
        // 用于计算的图片矩阵
        Mat mathImg = src.clone();
        // 灰度化
        Imgproc.cvtColor(src, mathImg, Imgproc.COLOR_BGR2GRAY);
        logger.info("二值化完成");
        // 获取轮廓
        Imgproc.Canny(src, mathImg, rotateParam.getCvtThreshould1(), rotateParam.getCvtThreshould2());
        logger.info("轮廓完成");
        // 霍夫变换获取角度,详细代码略
        double corn = houghLines(mathImg, rotateParam, cid);
        logger.info("霍夫变换完成,角度:{}", corn);
        if(corn == 0) {
            return oldImg;
        }
        return rotateOpenv(oldFile, corn, cid, bankCode);
    }

3.3 常用图片识别方案

阿里、百度都有提供图片识别服务,如果有实力也可以自己实现,不过不建议自研,因为样本需求量巨大,时间成本过高。

3.4 识别结果解析

3.4.1 探索之路

本章节为本文重点内容,因为前文所提到的都是较为基础的服务和算法,大量开发内容都在本章。前期要开发的订单图片类型巨量(大于100种),每一类图片区别很大,我们有几个人分类型开发,但每个人所用的方法都不同,且张三开发出来的李四看不懂,不过毕竟面对的是图片,比较抽象,这是可以理解的。

开发一段时间后我们发现了问题:每种类型最快也要一周才能开发完成,而且解析成功率极低。开发出一套抽象的方法来把行列数据提取出来迫在眉睫。

通过调研发现,大家常用两种方法来提取行列数据,分别为坐标法和标题法,但这两种方法解析率都不高。经过几周思考,终于想出了一套较好的方法,命名为俄罗斯方块法,最终解决了问题。

3.4.2 俄罗斯方块法

思路概要:

  • 拿到识别结果数据。
  • 先把所有数据的y坐标进行排序。
  • 遍历排序结果,先把第一条放入第一列结果集中。
  • 从第二条开始和第一列结果集对比。
  • 对比方法:如果在第一列结果集其中一条数据的右侧,则认为是新列;如果在y轴方法和第一列结果集中某些数据重叠了,则认为是新列。
  • 如果以上两条都不是,则认为本条数据还在当前列中,放入第一列结果集。
  • 以此类推,继续对比,直到对比到最后一列最后一条数据。
  • 按照上述方法,反过来,以x轴为标准,能够得到行结果集。

思路图如下:

概要代码如下:

// 按照最左上角的x坐标排序
        OcrWordInfo[] sortL = NoTableParseResult.ParseUtil.bubbleSortX(ocrResponse.getPrism_wordsInfo(), false);
        NoTableParseResult ntpr = new NoTableParseResult(param);
        ntpr.setHeight(converImg.height());
        ntpr.setWight(converImg.width());
        for (int i = 0; i < sortL.length; i++) {
            // 当前要比较的数据
            OcrWordInfo ocrWordInfo = sortL[i];
            // 处理当前列数据
            ntpr.getUtil().testCurColData(ocrWordInfo);
        }
        // 处理最后一列
        ntpr.lastCol();

        /**
         * 判断是否为下一列,并处理
         * @param ocrWordInfo
         * @return
         */
        public void testCurColData(OcrWordInfo ocrWordInfo) {

            // 遍历当前列已存在的所有数据
            int size = this.test.getCol().size();
            if(size == 0) {
                this.test.addCol(ocrWordInfo);
                return;
            }
            for (int i = 0; i < size; i++) {
                OcrWordInfo temp = this.test.getCol().get(i);
                // 最右边的数据
                int x1 = temp.getPos().get(1).getX();
                int x2 = temp.getPos().get(2).getX();
                // 当前数据最左边
                int xx0 = ocrWordInfo.getPos().get(0).getX();
                int xx3 = ocrWordInfo.getPos().get(3).getX();

                int threholdx = this.test.param == null ? 0 : this.test.param.getCoverColXThrehold();
                if(xx0 >= (x1 - threholdx) && xx0 >= (x2 - threholdx) && xx3 >= (x1 - threholdx) && xx3 >= (x2 - threholdx)) {
                    // 当前数据在右边,说明换列了!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
                    this.test.colAdd();
                    this.test.addCol(ocrWordInfo);
                    return;
                } else {
                    // 判断是否覆盖坐标
                    int y0 = temp.getPos().get(0).getY();
                    int y3 = temp.getPos().get(3).getY();
                    int yy0 = ocrWordInfo.getPos().get(0).getY();
                    int yy3 = ocrWordInfo.getPos().get(3).getY();
                    int threhold = (int)Math.round((y3 - y0) * (this.test.param == null ? 0.25 : this.test.param.getCoverThrehold()));
                    if(!(yy3 <= (y0 + threhold) || yy0 >= (y3 - threhold))) {
                        // 当前列表数据重叠,说明换列了!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
                        this.test.colAdd();
                        this.test.addCol(ocrWordInfo);
                        return;
                    }
                }
            }
            // 执行到这说明没覆盖
            this.test.addCol(ocrWordInfo);
        }

3.4.3 解析行数据技巧

技巧总结:

1)俄罗斯方块法提供去除干扰项的参数,可以根据图片特点去除上下左右干扰数据来减少串行列现象。

2)解析数据大致有两种方法

  • 根据标题列号来判断数据,这种方法不通用,简单、规范的图片识别率高,但无法适配乱的图。
  • 把每一行数据以间隔符号分割拼到一起,使用正则表达式来‘扣’数据。因为一般同类型订单图片,关键字段的位置是有特点的,例如金额格式、借贷方向、日期等,这种方法通用,但识别率不高。

具体使用哪种方法,还需要根据图片特点进行取舍。

3)俄罗斯方块法提供一些微调参数,用于适配一些特殊场景,例如换行列阀值之类的。

4)中间需要保存一些过程图片,例如矫正过程的若干张图、俄罗斯方块法识别结果的连线图等。毕竟这种项目在查问题时靠日志是没用的,还得靠这些中间图才能更快查到问题。

四、总结

本文提到的方案不能完全解决所有订单类图片解析问题,可以做到新手快速入门快速开发,如果您有更好思路欢迎交流。

作者:刘鹏飞

来源:宜信技术学院

相关文章
|
Java 开发者
重学Java基础篇—Java类加载顺序深度解析
本文全面解析Java类的生命周期与加载顺序,涵盖从加载到卸载的七个阶段,并深入探讨初始化阶段的执行规则。通过单类、继承体系的实例分析,明确静态与实例初始化的顺序。同时,列举六种触发初始化的场景及特殊场景处理(如接口初始化)。提供类加载完整流程图与记忆口诀,助于理解复杂初始化逻辑。此外,针对空指针异常等问题提出排查方案,并给出最佳实践建议,帮助开发者优化程序设计、定位BUG及理解框架机制。最后扩展讲解类加载器层次与双亲委派机制,为深入研究奠定基础。
612 0
|
人工智能 API 开发者
HarmonyOS Next~鸿蒙应用框架开发实战:Ability Kit与Accessibility Kit深度解析
本书深入解析HarmonyOS应用框架开发,聚焦Ability Kit与Accessibility Kit两大核心组件。Ability Kit通过FA/PA双引擎架构实现跨设备协同,支持分布式能力开发;Accessibility Kit提供无障碍服务构建方案,优化用户体验。内容涵盖设计理念、实践案例、调试优化及未来演进方向,助力开发者打造高效、包容的分布式应用,体现HarmonyOS生态价值。
1019 27
|
人工智能 小程序 前端开发
【一步步开发AI运动小程序】十九、运动识别中如何解析RGBA帧图片?
本文介绍了如何将相机抽取的RGBA帧图像解析为`.jpg`或`.png`格式,适用于体测、赛事等场景。首先讲解了RGBA图像结构,其为一维数组,每四个元素表示一个像素的颜色与透明度值。接着通过`uni.createOffscreenCanvas()`创建离屏画布以减少绘制干扰,并提供代码实现,将RGBA数据逐像素绘制到画布上生成图片。最后说明了为何不直接使用拍照API及图像转换的调用频率建议,强调应先暂存帧数据,运动结束后再进行转换和上传,以优化性能。
|
存储 监控 安全
重学Java基础篇—类的生命周期深度解析
本文全面解析了Java类的生命周期,涵盖加载、验证、准备、解析、初始化、使用及卸载七个关键阶段。通过分阶段执行机制详解(如加载阶段的触发条件与技术实现),结合方法调用机制、内存回收保护等使用阶段特性,以及卸载条件和特殊场景处理,帮助开发者深入理解JVM运作原理。同时,文章探讨了性能优化建议、典型异常处理及新一代JVM特性(如元空间与模块化系统)。总结中强调安全优先、延迟加载与动态扩展的设计思想,并提供开发建议与进阶方向,助力解决性能调优、内存泄漏排查及框架设计等问题。
686 5
|
安全 IDE Java
重学Java基础篇—Java Object类常用方法深度解析
Java中,Object类作为所有类的超类,提供了多个核心方法以支持对象的基本行为。其中,`toString()`用于对象的字符串表示,重写时应包含关键信息;`equals()`与`hashCode()`需成对重写,确保对象等价判断的一致性;`getClass()`用于运行时类型识别;`clone()`实现对象复制,需区分浅拷贝与深拷贝;`wait()/notify()`支持线程协作。此外,`finalize()`已过时,建议使用更安全的资源管理方式。合理运用这些方法,并遵循最佳实践,可提升代码质量与健壮性。
562 1
|
数据可视化 测试技术 API
前后端分离开发:如何高效调试API?有工具 vs 无工具全解析
在前后端分离的开发模式中,API 调试的效率直接影响项目的质量和交付速度。通过本文的对比分析,我们可以看到无工具调试模式虽具备灵活性和代码复用能力,但在操作便利性和团队协作上稍显不足。而传统的外部调试工具带来了可视化、高效协作与扩展性,却可能存在工具切换带来的开发链路断层问题。Apipost-Hepler 融合了两者的优势,让开发者无需离开熟悉的 IDEA 环境,就能享受可视化调试工具的强大功能。
601 5
|
移动开发 前端开发 JavaScript
从入门到精通:H5游戏源码开发技术全解析与未来趋势洞察
H5游戏凭借其跨平台、易传播和开发成本低的优势,近年来发展迅猛。接下来,让我们深入了解 H5 游戏源码开发的技术教程以及未来的发展趋势。
|
设计模式 存储 安全
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
结构型模式描述如何将类或对象按某种布局组成更大的结构。它分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象结构型模式比类结构型模式具有更大的灵活性。 结构型模式分为以下 7 种: • 代理模式 • 适配器模式 • 装饰者模式 • 桥接模式 • 外观模式 • 组合模式 • 享元模式
947 140
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
|
算法 测试技术 C语言
深入理解HTTP/2:nghttp2库源码解析及客户端实现示例
通过解析nghttp2库的源码和实现一个简单的HTTP/2客户端示例,本文详细介绍了HTTP/2的关键特性和nghttp2的核心实现。了解这些内容可以帮助开发者更好地理解HTTP/2协议,提高Web应用的性能和用户体验。对于实际开发中的应用,可以根据需要进一步优化和扩展代码,以满足具体需求。
1558 29

热门文章

最新文章

推荐镜像

更多
  • DNS