人工智能团队博客

愤怒的猪蹄小组 Beta展现——算式识别与计算

1.背景介绍

如何在计算机上辅助计算公式?目前的方案主要包括mathOCR、爱做业、做业帮等表明,以下:
图片2
图片3
图片4node

它们的缺点主要包括:算法

不支持手写字符输入、
不支持复杂题型、
不支持题库外的式子等等,
对于复杂的公式难以识别,对于简单的公式则应用场景很少。express

所以咱们但愿解决上述痛点,加强产品功能。
此类产品主要针对有辅助计算需求的学者和论文写做者,但还有一个基本的要求是近年来不断升级的验证码识别,以算术形式出现的验证码每每由被修改过的字符以异常的形式排列,所以不要求识别工具对于复杂式子的识别,但有对识别准确率的要求。算式识别工具能够做为API接口提供服务,便于爬虫等须要自动访问网页的工具调用,提升对算术验证码的识别、经过能力。编程

2. 架构设计

**识别目标:对于四则运算+-*/ 和分式具备识别和计算能力**canvas

  • 架构设计:
    • pipeline处理,流程以下
    • gui读取用户输入图片
    • 作图像预处理,包括二值处理、滤波、切割
    • 送进CNN识别
    • 结合每一个字符的识别结果和位置关系捆绑空间上不连续的字符造成符号(切割时按边界切割)
    • 按定义好的支持文法实现递归降低语法制导翻译,对输入记号流递归获得求解结果
    • 展现在gui上

3. 系统实现

  • 图片预处理
    • 图片预处理以OpenCV做为主要工具。预处理的主要目的是把图片中的字符切割出来,同时避免无关变量对字符识别的影响。
    • 主要步骤包括:灰度化、二值化、高斯滤波、字符切割

工具介绍:微信

  1. 卷积神经网络模型(CNN)
  2. 国际数学公式识别比赛数据集(CROHME)
    海量字符集图片
    与实际输入类似
  • 原图
    网络

  • binary image
    架构

  • extracted components
    app

项目重要文件介绍:框架

  1. 项目配置文件:
    • 图像处理:opencv
    • 分类器:tensorflow
    • gui: kivy

操做说明:

  1. 运行程序:
    • test.py提供无gui调用方式展现
    • gui直接运行可带着gui运行
  2. gui
  3. 在界面左边输入手写字符
  4. 右侧显示识别结果,界面上端显示计算结果
  5. 点击Solve按钮:展现结果
  6. 点击Clear按钮:清楚输入状态
  7. 在右侧栏中间显示识别出的表达式,方便差错

4. 实验结果

测试样例:
微信图片_20190603155830

img

img

img

img

img

5. 总结

优势

  • 实现了基本的完整算式识别流程
  • 实现了全字符的识别和捆绑
  • 在当前框架上拓展文法较方便,能够支持更多运算

缺点

  • 全字符识别致使分类器对于类似字符的辨别效果较差(如稍有点弯曲的1和括号)
  • 当前的处理流程输入图片先送入CNN完成识别和捆绑后将捆绑后的符号送进Parser进行语法分析,这样的方式致使书写潦草时效果较差,出现非同类符号识别时将致使整个表达式理解错误,简单地归纳就是词法分析限制了语法分析,能够考虑在语法分析中加入冗余容错,例如不应读到括号时尝试从新解析当前的字符为数字。

Postmoterm报告

总述

  • 成员
    • 每一个成员在beta阶段更加积极配合完成任务,因为课业影响,整个项目执行期较短,但成员基本都能加急完成分配的任务,并致力于找bug和debug。
  • 吸取教训
    • 在alpha和beta阶段的时间安排都不算很合理,不过beta阶段的预备时间比alpha阶段多了50%以上,算是作了必定的准备工做。其次因为目标更为清晰,但实现难度较大,实现过程不那么顺利。
  • 开发评价
    • 实现过程遇到的主要苦难时在图像处理阶段而不是模型训练阶段
    • 如何将字符切割出来并将不连续字符根据空间关系进行捆绑是很琐碎的工做,很容易出现逻辑错误
    • 语法制导翻译的过程当中因为运算符关系优先级至关复杂,手工实现的工做量太大,咱们最终没实现较多复杂运算符的复合运算支持,只实现了+-*/分式的表达式识别。

设想和目标

  • 设想
    • 咱们的软件要解决什么问题?是否认义得很清楚?是否对典型用户和典型场景有清晰的描述?
      • 咱们但愿设计一个支持算式识别解析的工具,实现的初衷是提供一个包输入一张表达式图解析出计算结果,用于验证码识别,在更复杂的运算符集上拓展后可用于智能的算式解析。
      • 出于展现目的咱们在上一阶段的GUI基础上作了修改也支持了gui展现功能。
  • 目标
    • 咱们达到目标了么(原计划的功能作到了几个? 按照原计划交付时间交付了么? 原计划达到的用户数量达到了么?)
    • 实现了。Alpha版本实现了对单个数字的识别,Beta版本实现了手写算式识别。按照预约时间交付。目前暂未推向市场,未得到用户。
  • 软件质量
    • 和上一个阶段相比,团队软件工程的质量提升了么? 在什么地方有提升,具体提升了多少,如何衡量的?
    • 咱们在代码质量上有所提升,具体是计算核心算法被更新,UI被重写。
  • 经验教训:有什么经验教训? 若是历史重来一遍, 咱们会作什么改进?
    • 项目难度较大,须要关于图像处理的一些知识,还须要编译的知识。
    • 组员的水平差别较大,半数以上不是计算机学院的,没有相关的知识积累,因此部分核心功能的实现团队成员参与度不高。

计划

是否有充足的时间来作计划?

  • 是的。

团队在计划阶段是如何解决同事们对于计划的不一样意见的?

  • 微信在线讨论。

你原计划的工做是否最后都作完了? 若是有没作完的,为何?

  • 咱们原计划的工做基本完成,但未实如今验证码工具上的完整应用程序,暂未将识别范围拓展至更多类型的算式。

是否项目的整个过程都按照计划进行,项目出了什么意外?有什么风险是当时没有估计到的,为何没有估计到?

  • 图像切割后捆绑算符、词法分析、语法分析都有不少细枝末节的问题,实现起来很头疼。算是预估到的难题,可是依然解决起来比较艰难。

咱们学到了什么? 若是历史重来一遍, 咱们会作什么改进?

  • 提早开始基础知识的学习,提升成员参与度。

资源

咱们有足够的资源来完成各项任务么?

  • 充足,模型训练不算是项目的核心问题,其余核心功能都有成员以前有过接触。

测试的时间,人力和软件/硬件资源是否足够? 对于那些不须要编程的资源 (美工设计/文案)是否低估难度?

  • 充足,UI不是咱们设计初衷的重点,只是为了展现。

变动管理

每一个相关的成员都及时知道了变动的消息?

  • 是的。

咱们采用了什么办法决定“推迟”和“必须实现”的功能?

  • 取决于当时全部人员的空闲状况,以及交付的紧急程度。

成员是否可以有效地处理意料以外的工做请求?

  • 目前都已处理。

设计/实现

设计工做在何时,由谁来完成的?是合适的时间,合适的人么?

  • 是在项目的启动阶段,由团队讨论完成。

设计工做有没有碰到模棱两可的状况,团队是如何解决的?

  • 好比对于产品功能的定位,最初的设计意见不一,最终讨论决定先作稳一点的验证码识别工具。

什么功能产生的Bug最多,为何?在发布以后发现了什么重要的bug? 为何咱们在设计/开发的时候没有想到这些状况?

  • 字符识别功能Bug最多,缘由是初始时野心较大,但愿支持较多的运算符,从而分类类别比咱们实际使用的多,识别效果有所降低。
  • 词法分析、语法分析等递归调用的函数中都容易有未触及的分支,因为时间限制和成员技能差别,咱们没有进行高覆盖率的单元测试,测试都是在尝试使用中进行的,效率较低,可靠性较低。

完整处理流程代码展现

  • gui
import solver
from PIL import Image

from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.uix.label import Label
from kivy.uix.widget import Widget
from kivy.graphics import Color, Line

class PaintWidget(Widget):
    color = (254, 0, 0, 1)  # Pen color  画笔颜色
    thick = 8  # Pen thickness  画笔粗度

    def __init__(self, root, **kwargs):
        super().__init__(**kwargs)
        self.parent_widget = root

    # Touch down motion:
    # If the touch position is located in the painting board, draw lines.
    # 按下动做:
    # 若是触摸位置在画板内,则在画板上划线
    def on_touch_down(self, touch):
        with self.canvas:
            Color(*self.color, mode='rgba')
            if touch.x > self.width or touch.y < self.parent_widget.height - self.height:
                return
            touch.ud['line'] = Line(points=(touch.x, touch.y), width=self.thick)

    # Touch move motion:
    # Draw line with mouse/hand moving
    # 移动动做:
    # 随着鼠标/手指的移动画线
    def on_touch_move(self, touch):
        with self.canvas:
            if touch.x > self.width or touch.y < self.parent_widget.height - self.height:
                return
            touch.ud['line'].points += [touch.x, touch.y]

    # Touch up motion:
    # When ending drawing line, save the picture, and call the prediction component to do prediction
    # 抬起动做:
    # 结束画线,保存图片成文件,并调用预测相关的组件作预测
    def on_touch_up(self, touch):
        if touch.x > self.width or touch.y < self.parent_widget.height - self.height:
            return
        #self.parent.parent.do_predictions()

    def export_image(self):
        input_img_name = './input_expression.png'
        self.export_to_png(input_img_name)
        im = Image.open(input_img_name)
        x, y = im.size
        p = Image.new('RGBA', im.size, (255, 255, 255))
        p.paste(im, (0, 0, x, y), im)
        p.save('white_bg.png')

        return 'white_bg.png'
        
  class Recognizer(BoxLayout):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.number = -1  # Variable to store the predicted number  保存识别的数字的变量
        self.orientation = 'horizontal'  # UI related  UI相关
        self.draw_window()

    # function to declare the components of the application, and add them to the window
    # 声明程序UI组件的函数,而且将它们添加到窗口上
    def draw_window(self):
        # Clear button  清除按钮
        self.clear_button = Button(text='CLEAR', font_name=HandwrittenMathCalculator.font_name, size_hint=(1, 4 / 45),
                                   background_color=(255, 165 / 255, 0, 1))
        self.solve_button = Button(text='SOLVE', font_name=HandwrittenMathCalculator.font_name, size_hint=(1, 4 / 45),
                                   background_color=(255, 165 / 255, 0, 1))
        # Painting board  画板
        self.painter = PaintWidget(self, size_hint=(1, 8 / 9))
        # Label for hint text  提示文字标签
        self.hint_label = Label(font_name=HandwrittenMathCalculator.font_name, size_hint=(1, 1 / 45))
        # Label for predicted number  识别数字展现标签
        self.result_label = Label(font_size=120, size_hint=(1, 1 / 3))
        # Label for some info  展现一些信息的标签
        self.info_board = Label(font_size=24, size_hint=(1, 22 / 45))

        # BoxLayout  盒子布局
        first_column = BoxLayout(orientation='vertical', size_hint=(2 / 3, 1))
        second_column = BoxLayout(orientation='vertical', size_hint=(1 / 3, 1))
        # Add widgets to the window  将各个组件加到应用窗口上
        first_column.add_widget(self.painter)
        first_column.add_widget(self.hint_label)
        second_column.add_widget(self.result_label)
        second_column.add_widget(self.info_board)
        second_column.add_widget(self.solve_button)
        second_column.add_widget(self.clear_button)
        self.add_widget(first_column)
        self.add_widget(second_column)

        # motion binding  动做绑定
        # Bind the click of the clear button to the clear_paint function
        # 将清除按钮的点击事件绑定到clear_paint函数上
        self.clear_button.bind(on_release=self.clear_paint)
        self.solve_button.bind(on_release=self.solve_expression)

        self.clear_paint()  # Initialize the state of the app  初始化应用状态

        # Clear the painting board and initialize the state of the app.
    def clear_paint(self):
        self.painter.export_image()
        #call solver to solve


    # Clear the painting board and initialize the state of the app.
    def clear_paint(self, obj=None):
        self.painter.canvas.clear()
        self.number = -1
        self.result_label.text = '?'
        self.hint_label.text = 'Write math expression above'
        self.info_board.text = 'Detected expression:\n'

    # Extract info from the predictions, and display them on the window
    # 从预测结果中提取信息,并展现在窗口上
    def show_info(self, result, detected_expression='8+7'):
        if result == None:
            self.number = 'Error'
        else:
            self.number = result
        self.result_label.text = str(self.number)
        self.hint_label.text = 'Detected expression and result is shown.Press clear to Retry!'
        self.info_board.text += detected_expression

    def solve_expression(self, obj=None):
        img = self.painter.export_image()
        self.info_board.text = 'Detected expression:\n'
        (result,detected_expression) = solver.solve(img)
        self.show_info(result,detected_expression)



# Main app class
# 主程序类
class HandwrittenMathCalculator(App):
    font_name = r'Arial.ttf'
    def build(self):
        return Recognizer()
  • solver
    • 各模块功能汇总脚本
    • 依次进行图片二值处理
    • 进行图像分割
    • 进行非连续符号合并捆绑
    • 调用CNN分类器进行分类
    • 根据位置进行从左到右从上到下排序,符合算式符号的结合逻辑
      • 至此至关于实现了lexer
    • 调用parser进行语法制导计算,返回结果
def solve(filename,mode = 'product'):
    original_img, binary_img = read_img_and_convert_to_binary(filename)

    symbols = binary_img_segment(binary_img, original_img)

    sort_symbols = sort_characters(symbols)
    process.detect_uncontinous_symbols(sort_symbols, binary_img)
    length = len(symbols)
    symbols_to_be_predicted = normalize_matrix_value([x['src_img'] for x in symbols])

    predict_input_fn = tf.estimator.inputs.numpy_input_fn(
        x={"x": np.array(symbols_to_be_predicted)},
        shuffle=False)

    predictions = cnn_symbol_classifier.predict(input_fn=predict_input_fn)

    characters = []
    for i,p in enumerate(predictions):
        # print(p['classes'],FILELIST[p['classes']])
        candidates = get_candidates(p['probabilities'])
        characters.append({'location':symbols[i]['location'],'candidates':candidates})
    #print([x['location'] for x in characters])

    modify_characters(characters)

    # print('排序后的字符序列')
    # print([[x['location'], x['candidates']] for x in characters])
    tokens = process.group_into_tokens(characters)
    # print('识别出的token')
    print(tokens)
    node_list = characters_to_nodes(characters)
    print(node_list)
    exp_parser = Exp_parser()
    result=exp_parser.expression(node_list)
    str = ''
    for token in tokens:
        str += token['token_string']
    print(result)
    if result is None:
        return None, str
    else:
        return (round(result,2),str)
相关文章
相关标签/搜索