人工智能團隊博客

憤怒的豬蹄小組 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)
相關文章
相關標籤/搜索