天天看點

iOS簡單的手寫漢字識别

簡介

前一陣在班訊通上邊加了一個小的功能:根據拼音提示寫出漢字,送出之後軟體會打出分數,其界面如下:

iOS簡單的手寫漢字識别

下面簡單介紹一下第一個版本識别算法的實作:

  • 記錄漢字錄入軌迹

iOS中UIView視圖繼承了UIResponder類,該類中的四個方法是我們需要調用的:

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event; 

-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event; 

-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event; 

-(void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;

  1. 在前三個方法中,我們記錄一個筆畫的起始點、過程點和結束點,将點添加到儲存點的數組中,在touchesEnded方法裡,将儲存點的數組添加到儲存筆畫的數組。這樣,寫完一個字之後,儲存筆畫的數組就是我們要的整個字的點集。
  2. 将該點集儲存在資料庫模闆表中,用做模闆。
  3. 模闆完成之後,開始記錄使用者輸入的點集,并将資料記錄在使用者表。
// 采集點集
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
        for (UITouch *touch in touches) {
            NSMutableArray *newStackOfPoints = [NSMutableArray array];
            [newStackOfPoints addObject:[NSValue valueWithCGPoint:[touch locationInView:self]]];
            [self.strokes addObject:newStackOfPoints];
        }
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
        for (UITouch *touch in touches) {
            [[self.strokes lastObject] addObject:[NSValue valueWithCGPoint:[touch locationInView:self]]];
        }
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
        for (UITouch* touch in touches) {
            [[self.strokes lastObject] addObject:[NSValue valueWithCGPoint:[touch locationInView:self]]];
        }
}
           
// 插入資料庫
- (int)insertChar:(CharacterModel *)model {
    int charId = 0;
    [self.hanziDb executeUpdate:@"insert into characters(chinese, pinyin, pointsString) values(?,?,?)", model.chinese, model.pinyin, model.pointsString];
    
    NSString *queryString = [NSString stringWithFormat:@"select id from characters where chinese = '%@'", model.chinese];
    FMResultSet* set = [self.hanziDb executeQuery:queryString];
    
    if([set next]) {
        charId = [set intForColumn:@"id"];
    }
    for (int i=0; i<model.inputPointGrids.count; i++) {
        NSArray *aStroke = model.inputPointGrids[i];
        for (NSValue *aPointValue in aStroke) {
            CGPoint aPoint = [aPointValue CGPointValue];
            [self.hanziDb executeUpdate:@"insert into points(id, pointX, pointY, strokeid) values(?,?,?,?)", [NSNumber numberWithInt:charId],[NSNumber numberWithInt:(int)aPoint.x],[NSNumber numberWithInt:(int)aPoint.y], [NSNumber numberWithInt:i+1]];
        }
    }
    return charId;
}
           
  • 對比使用者輸入資料和模闆資料
  1. 有了資料,剩下的就是利用資料庫的select語句來對比了。但是在實際對比時候會發現~~根本沒幾個點能比對的上,原因很簡單:在第一步中,記錄的是點的絕對坐标。比如我們寫個“一”,錄入模闆的時候,記錄的坐标集是{(50, 200), (100, 200), (150, 200), (200, 200), (250, 200)},使用者輸入的時候記錄的是{(60, 220), (90, 220), (130, 220), (160, 220), (200, 220), (230, 220)}, 結果就是零比對。另外一點很重要,就是螢幕的分辨率......
  2. 鑒于這種情況, 将輸入區域劃分為50個格子,每經過一個格子,便将其記錄下來作為相對坐标,這樣上邊的兩條資料就成了{(10, 40), (20, 40 ), (30, 40), (40, 40), (50, 40)}和{(10, 40), (20, 40 ), (30, 40), (40, 40), (50, 40), (60, 40)}, 比對程度就提高了(當然,這隻是舉例資料,并不準确)。
  3. 改為相對坐标存儲之後,雖然比對程度提高不少,但是當筆畫上下或者左右位置偏差比較大的時候仍然識别不準确, 比如一個橫的模闆資料是{(10, 40), (20, 40 ), (30, 40), (40, 40), (50, 40)}, 使用者輸入的資料是{(10, 41), (20, 41 ), (30, 41), (40, 41), (50, 41)},這樣在查詢的時候就比對不到。是以在查詢的時候需要将使用者輸入的筆畫做一個偏移,将{(10, 40), (20, 40 ), (30, 40), (40, 40), (50, 40)}和{(10, 41), (20, 41 ), (30, 41), (40, 41), (50, 41)}比對起來。
// 取相對坐标
- (void)turnToGrids {
    self.strokeCount = self.inputStrokes.count;
    // 格子寬度
    float gridWidth = kScreenWidth/10;
    for (int k=0; k<self.inputStrokes.count; k++) {
        // 存儲一個筆畫的所有點到一個數組
        NSMutableArray *strokPointGrids = [NSMutableArray array];
        NSArray *inputStrokesArray = self.inputStrokes[k];
        for (int l = 0; l<inputStrokesArray.count; l++) {
            NSValue *value = inputStrokesArray[l];
            if (l == 0) {
                [self.strokeStartPoints addObject:value];
            }
            if (l == self.inputStrokes.count-1) {
                [self.strokeEndPoints addObject:value];
            }
            CGPoint point = [value CGPointValue];
            CGPoint grid = CGPointMake(ceil(point.x/gridWidth), ceil(point.y/gridWidth));
            BOOL shouldAdd = NO;
            if (strokPointGrids.count == 0) {
                shouldAdd = YES;
            } else {
                NSValue *lastValue = strokPointGrids.lastObject;
                CGPoint lastGrid = [lastValue CGPointValue];
                if (lastGrid.x != grid.x || lastGrid.y != grid.y) {
                    shouldAdd = YES;
                }
            }
            if (shouldAdd) {
                [strokPointGrids addObject:[NSValue valueWithCGPoint:grid]];
                if (![self.pointsString isEqualToString: @""] && ![self.pointsString hasSuffix:@"*"]) {
                    [self.pointsString appendString:[NSString stringWithFormat:@"|"]];
                }
                [self.pointsString appendString:[NSString stringWithFormat:@"%d,%d", (int)grid.x, (int)grid.y]];
            }
        }
        if (k != self.inputStrokes.count-1) {
            [self.pointsString appendString:@"*"];
        }
        [self.inputPointGrids addObject:strokPointGrids];
    }
}
           
// 最終的查詢語句
                NSString *queryString4 = [NSString stringWithFormat:@"select a.strokeid as strokeid,a.samecount as samecount,a.ucount as ucount,a.pcount as pcount from ( select strokeid, count(*) as samecount, (select count(*) from input_points where id=p.id and strokeid= %d ) as ucount, (select count(*) from points where id=p.id and strokeid=p.strokeid) as pcount from points p where exists(select * from input_points u where u.id=p.id and u.strokeid=%d and (abs(u.pointX-p.pointX) + abs(u.pointY-p.pointY))<2 ) and p.strokeid not in (%@) and p.id=%d group by strokeid ) a order by abs(a.pcount - a.samecount) asc", j, j, hasStrokeid, model.charID];
           

至此, 該功能子產品就可以簡單識别從手機螢幕寫入的漢字了,隻不過這樣很雞肋,因為如果模闆錄入和使用者輸入風格相差比較大或者使用者寫的偏上(偏下,左右都有可能)的話,識别率就直線下降,是以,我們在第二版本中徹底改變了識别算法,極大程度地提高了識别率。

關于手寫識别的第二個版本,請看下一篇博文