今回は, 手書き文字認識の前処理, 特徴抽出についてまとめるつもりであったが, 特徴抽出のコードが長くなりそうなのでまずは前処理のみについてまとめることにした。
1. 前処理
前処理は, ストロークが入力されるごとに行う処理で, ペン又は指で入力された座標点列について, 冗長な情報やイレギュラーな情報を除去する.
1.1 平滑化
タブレットからの入力座標値がばらつく場合などには, 複数座標点を使って平滑化を行う.
今回は, 特に入力座標値のばらつきはないので, 平滑化は行わない.
もし行う場合は, 例えば以下の式などで平滑化する.
x = (xi-1 + xi * 2 + xi+1) / 4
1.2 同一座標点の除去
ペン又は指の滞留などによる同一座標点を除去する.
[コード]
// 同一点及び近似点の除去を行う.
private void samplingPoint(ArrayList<Point> inPoints) {
int count = inPoints.size();
Point[] pts = new Point[count];
count--; // Pen up除く
int idx = 0;
Point p0 = new Point(inPoints.get(0));
pts[idx++] = p0; // ストロークの始点をセット
if(count > 1) { // 座標点が2点以上の場合
for(int i=1; i<count; i++) {
Point p1 = new Point(inPoints.get(i));
if((Math.abs(p0.x - p1.x) >= SAMPLING_DX)
|| (Math.abs(p0.y - p1.y) >= SAMPLING_DY)) {
pts[idx++] = p1; // サンプル点とする
p0 = p1;
}
else {
if(i == (count - 1)) {
if(idx == 1) { // サンプル点が始点のみの場合
if(p1.x == p0.x && p1.y == p0.y) {
pts[idx++] = new Point(p1.x + SAMPLING_DX, p1.y + SAMPLING_DY);
}
else {
pts[idx++] = p1;
}
}
else { // 1個以上のサンプル点がある場合
if(p1.x != p0.x || p1.y != p0.y) {
pts[idx++] = p1;
}
}
}
}
}
}
else { // 座標点が1点の場合
pts[idx++] = new Point(p0.x + SAMPLING_DX, p0.y + SAMPLING_DY);
}
pts[idx++] = new Point(Stroke.PEN_UP, Stroke.PEN_UP); // Pen up追加
Point[] tmpPoints = new Point[idx];
System.arraycopy(pts, 0, tmpPoints, 0, idx);
points = tmpPoints;
}
1.3 入り・ハネの除去
タブレットにペンで筆記する場合, ペンが滑ってストロークの開始位置に入りが生じたり, 次のストロークの開始位置に向けて, ハネが発生したりする場合がある. このような場合, 入り・ハネの除去を行う場合がある.
今回は, 入力されたストロークを繋いで一筆書きパターンとするので, 入り・ハネの除去は行わない.
1.4 ストロークの長さ/大きさ
セグメントの長さを累計して, ストロークの長さを求める.
また, ストロークに外接する枠の大きさを求める.
[コード]
// ストロークの特徴抽出
private void extractBoundingBox() {
int count = points.length - 1; // Pen up除く
Point[] pts = points;
Rect bx = null;
int len = 0;
for(int i=0; i<count; i++) {
if(bx == null) {
bx = new Rect(pts[i].x, pts[i].y, pts[i].x, pts[i].y);
}
else {
len += Segment.calcLength(pts[i-1], pts[i]);
if(bx.left > pts[i].x)
bx.left = pts[i].x;
if(bx.right < pts[i].x)
bx.right = pts[i].x;
if(bx.top < pts[i].y)
bx.top = pts[i].y;
if(bx.bottom > pts[i].y)
bx.bottom = pts[i].y;
}
}
boundingBox = bx;
length = len;
}
次回は, 手書き文字認識の特徴抽出(入力パターン)について考えてみる.
1990年代, 多くのPDAに手書き文字認識による文字入力手段が搭載されていたが, 最近はやりのiPhoneやAndroid端末ではあまり話を聞かない.
しかし, あの懐かしのGraffitiが, 「Graffiti for Android」として復活したという記事[1][2]を見かけた. またはやるのだろうか!?
私も, 昔から手書き文字認識には興味があったので, Androidで手書き文字認識にチャレンジしてみることにした.
1. 手書き文字の認識方法
手書き文字の認識方法は, 大きく分けると次の2種類に分類できる.
1.1 パターンマッチング方式
文字ごとに統計的に作成した参照パターンを持ち, 入力パターンとマッチングを行う. パターンとしては, 文字の筆跡を例えば座標点列のパターンとして扱うものと, 筆跡を画像パターンとして扱うものなどがある.
1.2 構造解析的方式(基本ストローク方式)
文字を基本的なストロークの集合として辞書に記述しておき, 入力された文字の各ストロークごとに基本ストロークと照合し, 辞書とのマッチングを行う.
今回は, 文字の筆跡を一筆書きのパターンとして扱い, DPマッチングにより参照パターンとの照合を行う方法でトライしてみようと思う.
2. データ収集ツール
パターンマッチング方式で文字認識を行うには, 参照パターンを作るための手書きデータが必要である. そこで, Androidアプリでのタッチ操作や座標データの扱いの勉強もかねて, まずは手書き文字データを収集するツールを作成してみた.
2.1 筆跡の表示[3]
指でタッチした部分を筆跡として表示する文字入力枠を, Viewクラスを継承したBoxViewクラスにまとめてみた.
文字入力枠は, 図のように左上を原点(0,0)とする幅/高さ90ドットの入力枠である.
コードの説明:
1) onTouchEvent(MotionEvent event)
文字入力枠内にタッチした時に, タッチイベントが発生しonTouchEvent()がコールされる. 座標値をバッファに格納するとともに, もしタッチアップ(指が離れる)の場合, (-1,-1)をバッファに追加する.
座標値セットした後, 再描画させるためにinvalidate()をコールする.
2) onDraw(Canvas canvas)
描画処理では, バッファ内の2つの座標点を順に線で結ぶことで筆跡を表示する. タッチアップ(-1, -1)の場合, 次の座標点をストロークの開始点とする.
2.2 筆跡データの保存
筆跡データは, 文字コードを含むヘッダと座標点のデータからなる.
今回は, 以下のフォーマットで保存することにした.
コードの説明:
1) savePenData(String fname)
手書きデータは, /sdcard/pendata/{データ保存ファイル}に保存する.
手書きデータはヘッダ部とデータ部で構成されており, ヘッダ部には入力文字のコードを, データ部には各ストロークの座標点を順に記録し, データの最後には文字終了コードを挿入する.
package com.moonlight.android.pendata;
import java.io.File;
import java.io.FileOutputStream;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Point;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
public class BoxView extends View {
// Viewクラスの定義
ArrayList<Point> PointBuf = new ArrayList<Point>();
byte [] CharCode = null;
public BoxView(Context context) {
super(context);
}
public BoxView(Context context, AttributeSet attrs) {
super(context, attrs);
}
// 描画処理
public void onDraw(Canvas canvas) {
canvas.drawColor(Color.WHITE);
Paint paint = new Paint();
paint.setColor(Color.BLUE);
paint.setStyle(Paint.Style.FILL);
paint.setStrokeWidth(3);
Point p0 = new Point(-1,-1);
for(int i=0; i<PointBuf.size(); i++) {
Point p1 = PointBuf.get(i);
if(p0.x >= 0 && p0.y >= 0) { // p0がPen upでない
if(p1.x >= 0 && p1.y >= 0) { // p1がPen upでない
canvas.drawLine(p0.x, p0.y, p1.x, p1.y, paint); // 2点からなる線を引く
}
}
p0 = p1;
}
}
// タッチイベントの処理
public boolean onTouchEvent(MotionEvent event) {
int x = (int)event.getX();
int y = (int)event.getY();
PointBuf.add(new Point(x,y));
if(event.getAction() == MotionEvent.ACTION_UP) {
PointBuf.add(new Point(-1, -1)); // Pen upの場合, (-1,-1)を挿入
}
invalidate(); // 通知
return true;
}
// 文字データの設定
public void setCharCode(String str)
{
try {
CharCode = str.getBytes("utf-8");
} catch(UnsupportedEncodingException e) {
e.printStackTrace();
}
}
// 座標データの保存処理
public void savePenData(String fname)
{
// 座標データがない場合リターン
if(PointBuf.size() == 0) {
return;
}
// 保存先の決定
File fdir = new File("/sdcard/pendata/");
fdir.mkdirs();
String file = fdir.getAbsolutePath() + "/" +fname;
Log.v("savePenData", file);
// 手書きデータをファイルに書き込む
try {
FileOutputStream out = new FileOutputStream(file, true);
int nByte = ((PointBuf.size() + 1) * 2);
int nPadding = 32 - nByte % 32;
if(nPadding != 0) {
nByte = ((nByte / 32) + 1) * 32;
}
// ヘッダ識別子(4bytes)を書き込む
out.write( 0xfe );
out.write( 0x01 );
out.write( 0xfe );
out.write( 0x01 );
// ヘッダ + データのサイズ(2bytes)を書き込む
out.write( (byte)((nByte >> 8) & 0xff) );
out.write( (byte)(nByte & 0xff) );
// 文字コード(2bytes)を書き込む
out.write( CharCode[0] );
out.write( CharCode[1] );
// ヘッダの残り部分を0でパディングする
for(int i=0; i<24; i++) {
out.write( 0x00 );
}
// x, yの順に座標データを書き込む
for(int i=0; i<PointBuf.size(); i++) {
out.write(PointBuf.get(i).x);
out.write(PointBuf.get(i).y);
String str = String.format("X=0x%02x, Y=0x%02x", PointBuf.get(i).x, PointBuf.get(i).y);
Log.v("Point", str);
}
// 文字終了コードを書き込む
out.write( 0xfc );
out.write( 0xfc );
// ブロックの残り部分を0でパディングする.
for(int i=0; i<nPadding; i++) {
out.write( 0x00 );
}
out.flush();
out.close();
} catch(Exception e) {
Log.v("Error", "File can't open");
}
}
}
手書きデータ収集ツールの動作例を以下に示す.
次は, 手書き文字認識の前処理や特徴抽出について考えてみる.
---
参照URL:
[1] Graffiti for Androidがバージョンアップ、日本語入力に対応
[2] ACCESS、一筆書き入力「Graffiti for Android」の日本語対応版をリリース
[3] Android で再開する Java プログラミング(11) - 手書きメモを作る
明日香
- 今日:
- 昨日:
- 累計: