300行代碼實現(xiàn)手寫漢字識別

原文發(fā)表在個人博客Technology-手寫漢字識別,轉(zhuǎn)載請注明出處睬关。

本文主要介紹如何通過機器學(xué)習(xí)來實現(xiàn)手寫漢字識別矮燎,核心算法采用C++編寫,不足300行转砖,代碼開源在Github-HandWriteRecognition须鼎。

主要思路:

  • 提取每個漢字的筆畫特征,保存成一個字庫;
  • 通過手寫板或者觸摸板獲取用戶的手寫軌跡坐標(biāo);
  • 坐標(biāo)預(yù)處理;
  • 通過 KNN 算法府蔗,與字庫中的每個漢字進(jìn)行比較;
  • 根據(jù)比較距離的大小進(jìn)行排序莉兰,輸出結(jié)果。

字庫

這里的字庫礁竞,是基于Tomoe Handwriting Dictionary字庫進(jìn)行特殊處理后糖荒,生成的model文件。

Tomoe字庫收集了漢字的每一筆的起始和轉(zhuǎn)折點模捂,可以認(rèn)為是特征點捶朵。例如
下面是“丁”字的表示蜘矢,分為兩筆,第一筆只有起點和終點综看,第二筆還包含了轉(zhuǎn)擇點:

<character>
    <utf8>丁</utf8>
    <strokes>
      <stroke>
        <point x="93" y="198"/>
        <point x="913" y="205"/>
      </stroke>
      <stroke>
        <point x="495" y="203"/>
        <point x="470" y="847"/>
        <point x="405" y="784"/>
      </stroke>
    </strokes>
  </character>

但是Tomoe字庫的缺點很明顯品腹,首先,其坐標(biāo)都是基于其本身的畫板大小的(1000*1000)红碑,而我們在進(jìn)行手寫識別時舞吭,無法預(yù)先知道觸摸屏或者手寫板的區(qū)域大小,所以析珊,必須對數(shù)據(jù)歸一到同一大小的面板中羡鸥,其次,其用的是xml格式忠寻,導(dǎo)致冗余字段非常多惧浴,字庫很大(8.8M),非常占空間奕剃,而且加載時很慢衷旅。

針對,數(shù)據(jù)歸一處理的問題纵朋,后面的算法環(huán)節(jié)會提及處理方法柿顶。字庫文件過大的問題,這里采用自定義的格式操软,以“丁”字為例:

丁:[[(93,198),(913,205)][(495,203)(470,847)(405,784)]]

這樣嘁锯,將原來8.8M的大小壓縮到僅有1.5M,后面根據(jù)算法需要會進(jìn)一步壓縮寺鸥。

特征點

由于用戶手寫的坐標(biāo),是連續(xù)的品山,而且非常多胆建,我們必須從中提取特征點,用于與詞庫中的特征點做比較肘交。特征點提取笆载,這里,我們用的是點到直線的距離來判斷涯呻,以“丁”字為例凉驻,其第二筆的特征點,首先是起始和結(jié)束點复罐,其次是轉(zhuǎn)擇點涝登,明顯可以看出轉(zhuǎn)折點離起始和結(jié)束點連成的直線,距離最遠(yuǎn)效诅。因此胀滚,只要我們設(shè)置合適的閾值趟济,就可以通過點到直線的距離,來找出轉(zhuǎn)折點咽笼,加上起始和結(jié)束點顷编,作為特征點。

因為剑刑,一筆筆畫可能有多個轉(zhuǎn)折點媳纬,所以,我們通過遞歸來完成:

void turnPoints(Stroke *stroke, std::vector<Point> *points, int pointIndex1, int pointIndex2){
    if(pointIndex1 < 0 || pointIndex2 <= 0 || pointIndex1 >= pointIndex2 - 1)
        return;
    const float a = stroke->points[pointIndex2].x - stroke->points[pointIndex1].x;
    const float b = stroke->points[pointIndex2].y - stroke->points[pointIndex1].y;
    const float c = stroke->points[pointIndex1].x * stroke->points[pointIndex2].y - stroke->points[pointIndex2].x * stroke->points[pointIndex1].y;
    float max = 3000;
    int maxDistPointIndex = -1;
    for(int i = pointIndex1 + 1; i < pointIndex2; ++i){
        Point point = stroke->points[i];
        const float dist = fabs((a * point.y) -(b * point.x) + c);
        std::cout << dist << std::endl;
        if (dist > max) {
            max = dist;
            maxDistPointIndex = i;
        }
    }
    if(maxDistPointIndex != -1){
        turnPoints(stroke, points, pointIndex1, maxDistPointIndex);
        points->push_back(stroke->points[maxDistPointIndex]);
        turnPoints(stroke, points, maxDistPointIndex, pointIndex2);
    }
}

算法

Frechet

這里用到的算法施掏,一開始是采用Frechet Distance來判斷的钮惠。

Frechet Distance:首先找出曲線A到曲線B的最遠(yuǎn)點,然后計算該點到曲線B的最近距離其监,再反過來計算曲線B到曲線A的最短距離萌腿,取兩個最短距離的最大值,作為兩條曲線的相似度抖苦。

但是毁菱,在實驗中發(fā)現(xiàn),Frechet Distance有著很大的缺陷锌历,首先贮庞,如果把整個字作為曲線,與另一個字比較究西,顯然是不行的窗慎,因為有些字可能非常復(fù)雜,例如“橢”卤材,曲線存在交叉遮斥,計算出來的距離沒有參考意義;其次扇丛,如果通過單筆畫進(jìn)行比較术吗,由于用戶的手寫區(qū)域與字庫的區(qū)域不一樣是重合的,所以帆精,計算出來的距離也會有問題较屿,例如:

字庫的“一”字坐標(biāo):(0, 50)->(20, 50)
手寫的“一”字坐標(biāo):(0, 80)->(20, 80),距離:30
手寫的"|"字坐標(biāo):(10, 40)->(10, 60)卓练,距離:14

所以隘蝎,在實驗后,決定放棄使用Frechet Distance來判斷字的相似度襟企,而通過特征點構(gòu)成的直線的角度來比較嘱么,使用KNN算法。

KNN

首先顽悼,計算單筆畫的相似度拱撵,單筆畫的特征點與前一點的直線的角度辉川,計算方式:

double diretion(const Point &lastPoint, const Point &startPoint){
    double result = -1;
    result = atan2(startPoint.y - lastPoint.y,  startPoint.x - lastPoint.x) * 10;
    return result;
}

特征點的數(shù)量可能不對應(yīng),可以采用兩種方式來處理拴测,一種是插值乓旗,一種是采樣,這里是采樣的方式集索,另外屿愚,對于每一筆,還需要加上其起始點與上一筆的終點構(gòu)成直線的角度务荆,避免“丁”字識別成“十”字的情況妆距,計算方式如下:

double distBetweenStrokes(const Stroke &stroke1, const Stroke &stroke2){
    double strokeDist = MAXFLOAT;
    double dist = 0.0f;
    int minLength = fmin(stroke1.points.size(), stroke2.points.size());
    Stroke largeStroke = stroke1.points.size() > minLength ? stroke1 : stroke2;
    Stroke smallStroke = stroke1.points.size() > minLength ? stroke2 : stroke1;
    for(int j = 1; j < minLength; ++j){
        double diretion1 = largeStroke.points[j].diretion;
        double diretion2 = smallStroke.points[j].diretion;
        dist += fabs(diretion1 - diretion2);
    }
    // 當(dāng)前筆與上一筆的位置差異
    dist += fabs(largeStroke.points[0].diretion - smallStroke.points[0].diretion);
    strokeDist = dist / minLength;
    return strokeDist;
}

到此,我們可以獲取到用戶手寫字與字庫里面字函匕,單筆畫的相似程度娱据,通過加法,就可以得到整個字的相似程度盅惜,但是由于存在連筆的情況中剩,例如,一筆寫成兩筆抒寂,所以结啼,我們允許筆畫數(shù)的誤差在2以內(nèi),但是在排序時屈芜,筆畫數(shù)越接近的郊愧,優(yōu)先級越高,計算方法如下:

double dist(const Character &character1, const Character &character2){
    double dist = MAXFLOAT;
    if(character2.strokeCount >= character1.strokeCount && character2.strokeCount <= character1.strokeCount + 2){
        double allStrokeDist = 0.0f;
        for(int i = 0; i < character1.strokeCount; ++i){
            Stroke stroke1 = character1.strokes[i];
            Stroke stroke2 = character2.strokes[i];
            double strokeDist = distBetweenStrokes(stroke1, stroke2);
            allStrokeDist += strokeDist;
            if(strokeDist > MAX_DIFF_PER_STROKE){
                allStrokeDist = MAXFLOAT;
                return allStrokeDist;
            }
        }
        // 筆畫更接近的優(yōu)先級更高
        return allStrokeDist / character1.strokeCount + character2.strokeCount - character1.strokeCount;
    }
    return dist;
}

注意井佑,到這里属铁,我們用到的只是兩點直線的角度,與坐標(biāo)的實際大小已經(jīng)沒有聯(lián)系躬翁,所以焦蘑,可以將字庫進(jìn)一步精簡為:

丁:[[0, 0.08][31.3, 16.09, 23.71]]

進(jìn)一步精簡詞庫的大小。

效果

通過搭建詞庫姆另,取特征點喇肋,以及匹配算法坟乾,我們可以看到手寫識別的最終效果如下:

只要手寫不是特別潦草迹辐,基本上第一個字就能識別出來。但是依然存在著依賴筆畫順序的問題甚侣,后面有空再優(yōu)化明吩。

看了又看:

如何在一周內(nèi)做一款拼音輸入法
iOS-線程同步詳解

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市殷费,隨后出現(xiàn)的幾起案子印荔,更是在濱河造成了極大的恐慌低葫,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,723評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件仍律,死亡現(xiàn)場離奇詭異嘿悬,居然都是意外死亡,警方通過查閱死者的電腦和手機水泉,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評論 2 382
  • 文/潘曉璐 我一進(jìn)店門善涨,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人草则,你說我怎么就攤上這事钢拧。” “怎么了炕横?”我有些...
    開封第一講書人閱讀 152,998評論 0 344
  • 文/不壞的土叔 我叫張陵源内,是天一觀的道長。 經(jīng)常有香客問我份殿,道長膜钓,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,323評論 1 279
  • 正文 為了忘掉前任伯铣,我火速辦了婚禮呻此,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘腔寡。我一直安慰自己焚鲜,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,355評論 5 374
  • 文/花漫 我一把揭開白布放前。 她就那樣靜靜地躺著忿磅,像睡著了一般。 火紅的嫁衣襯著肌膚如雪凭语。 梳的紋絲不亂的頭發(fā)上葱她,一...
    開封第一講書人閱讀 49,079評論 1 285
  • 那天,我揣著相機與錄音似扔,去河邊找鬼吨些。 笑死,一個胖子當(dāng)著我的面吹牛炒辉,可吹牛的內(nèi)容都是我干的豪墅。 我是一名探鬼主播,決...
    沈念sama閱讀 38,389評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼黔寇,長吁一口氣:“原來是場噩夢啊……” “哼偶器!你這毒婦竟也來了媳瞪?” 一聲冷哼從身側(cè)響起刑峡,我...
    開封第一講書人閱讀 37,019評論 0 259
  • 序言:老撾萬榮一對情侶失蹤房蝉,失蹤者是張志新(化名)和其女友劉穎叔遂,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體霎苗,經(jīng)...
    沈念sama閱讀 43,519評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡姆吭,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,971評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了唁盏。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片猾编。...
    茶點故事閱讀 38,100評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖升敲,靈堂內(nèi)的尸體忽然破棺而出答倡,到底是詐尸還是另有隱情,我是刑警寧澤驴党,帶...
    沈念sama閱讀 33,738評論 4 324
  • 正文 年R本政府宣布瘪撇,位于F島的核電站,受9級特大地震影響港庄,放射性物質(zhì)發(fā)生泄漏倔既。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,293評論 3 307
  • 文/蒙蒙 一鹏氧、第九天 我趴在偏房一處隱蔽的房頂上張望渤涌。 院中可真熱鬧,春花似錦把还、人聲如沸实蓬。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,289評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽安皱。三九已至,卻和暖如春艇炎,著一層夾襖步出監(jiān)牢的瞬間酌伊,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,517評論 1 262
  • 我被黑心中介騙來泰國打工缀踪, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留居砖,地道東北人。 一個月前我還...
    沈念sama閱讀 45,547評論 2 354
  • 正文 我出身青樓驴娃,卻偏偏與公主長得像奏候,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子托慨,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,834評論 2 345

推薦閱讀更多精彩內(nèi)容

  • 注:題中所指的『機器學(xué)習(xí)』不包括『深度學(xué)習(xí)』鼻由。本篇文章以理論推導(dǎo)為主暇榴,不涉及代碼實現(xiàn)厚棵。 前些日子定下了未來三年左右...
    我偏笑_NSNirvana閱讀 39,911評論 12 145
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,515評論 25 707
  • 出于各種各樣的理由婆硬,在某一些語境下(比如當(dāng)我們談?wù)摢毩⒂螒驎r)狠轻,談?wù)摗赣螒蛩囆g(shù)」而對「游戲商業(yè)」避而不談有時成了一...
    IndieACE閱讀 515評論 0 2
  • 回憶,相戀 蔣垚還清楚的記得李青第一次主動吻她的時候彬犯,那天是李青的二十歲生日向楼。李青牽著蔣垚一起來到和室友相...
    桃堯華閱讀 395評論 0 3
  • 當(dāng)脾氣來的時候,福氣就走了谐区!人的優(yōu)雅關(guān)鍵在于控制自己的情緒湖蜕。用嘴傷人是最愚蠢的一種行為。一個能控制住不良情緒的人宋列,...
    禪夢閱讀 249評論 0 0