コード実装編
前回まで
レシートが折れ曲がっている状態で、撮影するとその折れ曲がりを境に、文字の斜めっている角度が異なってしまいます。これを改善するアプローチを考えてきました。詳細は、前回までの記事を読んでみてください。
全体の流れ
今回は大まかに次のようにまとめました。
1.レシート内最大要素の選択
2.最大要素を用いて左右の縦方向のずれを取得
3.その他要素の回転と縦方向の移動
レシート内最大要素の選択
ここでは横幅、つまりboundingboxの全体のx
座標の差を取得し、一番大きいものの要素番号を取得しています。レシートデータについて説明すると、Lines
には画像から取得した文字要素のデータが文字列ごとにまとめられています。そのデータの中にBoundingBox
が入っており、今回はこのデータのみを使用します。詳細なデータ構造に関しては、Gitに公開しているReceipt.cs
を確認してください。
List<double> bbwidth = new List<double>();
for (int i = 0; i < ReadResult.Lines.Count(); i++)
{
double width = Calcwidth(ReadResult.Lines[i]);
bbwidth.Add(width);
}
int NumOfMaxIdx = bbwidth.IndexOf(bbwidth.Max());
要素の幅を計算する関数はCalcwidth
として作成しました。
public static double Calcwidth (Line line)
{
return (Math.Abs(line.BoundingBox[0] - line.BoundingBox[2])+ Math.Abs(line.BoundingBox[4] - line.BoundingBox[6]))/2.0;
}
最大要素を用いて要素の縦のずれを取得
ここでは先ほど取得した最大要素を、二つの基準から回転し、二通りのBoundingBox
を得ます。
その二つのBoundingBox
は右端と、左端のy
座標の差div
を計算します。そのコードを掲載します。main
では作成した関数を利用し、最大要素からdiv
を獲得します。
var biggestCenter = CalcCenter(ReadResult.Lines[NumOfMaxIdx].BoundingBox);
var BiggestData = RotDiv(ReadResult.Lines[NumOfMaxIdx].BoundingBox);
double div = CalcDiv(BiggestData.leftBb,BiggestData.rightBb);
CalcCenter
では要素の中心座標を計算します。
public static (double x, double y) CalcCenter(List<int> bb)
{
double x = bb.Where((val, idx) => idx % 2 == 0).Sum() / 4;
double y = bb.Where((val, idx) => idx % 2 == 1).Sum() / 4;
return (x, y);
}
RotDiv
では与えられたBoundinBox
から、左右の縦の直線の傾きを利用し、それぞれ回転して二つのBoundingBox
を返り値として取得します。それと、今回は使用しませんが、回転に使用した弧度法であらわした角度も返します。
public static (double thetaL, double thetaR, List<double> leftBb, List<double> rightBb) RotDiv(List<int> bb)
{
double LCx = (bb[0] + bb[6]) / 2;
double LCy = (bb[1] + bb[7]) / 2;
double RCx = (bb[2] + bb[4]) / 2;
double RCy = (bb[3] + bb[5]) / 2;
double Langle = CalcAngle(bb[0],bb[1],bb[6],bb[7]);
double Rangle = CalcAngle(bb[2],bb[3],bb[4],bb[5]);
double thetaL = (Math.PI / 2 - Math.Abs(Math.Atan(Langle))) * Math.Sign(Langle);
double thetaR = (Math.PI / 2 - Math.Abs(Math.Atan(Rangle))) * Math.Sign(Rangle);
List<double> leftBb = RotateBb(bb, LCx, LCy, thetaL);
List<double> rightBb = RotateBb(bb, RCx, RCy, thetaR);
return (thetaL, thetaR, leftBb, rightBb);
}
CalcDiv
では、得られた二つのBoundingBox
の中間座標からy
座標の差を利用して、div
を計算します。
static double CalcDiv(List<double> bb1, List<double> bb2)
{
double sum1 = bb1.Where((val, idx) => idx % 2 == 1).Sum();
double sum2 = bb2.Where((val, idx) => idx % 2 == 1).Sum();
return sum1 /4 - sum2/ 4;
}
CalcAngle
では、要素が水平にある時を0
度としたときの傾きを計算します。
public static double CalcAngle(int x1,int y1,int x2, int y2)
{ double ang;
if(x1 - x2 != 0)
{
ang = (y1 - y2) / (x1 - x2);
}
else
{
ang = double.PositiveInfinity;
}
return ang;
}
その他要素の回転とdivによる補正
最後にレシート上の要素をすべて補正していきます。すべて同様に水平になるように回転し、その後最大要素の中間座標より右側にあるものは、div
を引くことでレシートの右寄りと、左寄りの要素でのy
座標の差をなるべく少なくすることが可能です。
for (int i = 0; i < ReadResult.Lines.Count(); i++)
{
double bias = CalcBias(ReadResult.Lines[i]);
double Centertheta = -Math.Atan(bias);
var center = CalcCenter(ReadResult.Lines[i].BoundingBox);
double widthContent = Calcwidth(ReadResult.Lines[i]);
List<double> bbRe = RotateBb(ReadResult.Lines[i].BoundingBox, center.x, center.y, Centertheta);
if (center.x > biggestCenter.x )
{
bbRe = AddDiv(bbRe,div);
}
// boundingbox double to int
List<int> intbb = bbRe.Select(d => (int)Math.Round(d)).ToList();
ReadResult.Lines[i].BoundingBox = intbb;
}
これはCalcAngle
と同様の計算を行う関数で、Line
の要素を引数に入れることで傾きを得ることができます。
static double CalcBias(Line line)
{
return (double)((((line.BoundingBox[3] + line.BoundingBox[5]) / 2.0) - ((line.BoundingBox[7] + line.BoundingBox[1]) / 2.0)) / (((line.BoundingBox[2] + line.BoundingBox[4]) / 2.0) - ((line.BoundingBox[6] + line.BoundingBox[0]) / 2.0)));
}
RotateBb
はBoundingBox
を回転する関数で、傾きと要素の中心の座標を引数に入れることで、回転された後のBoundingBox
を返します。
public static List<double> RotateBb(List<int> bb, double Cx, double Cy, double angle)
{
List<double> bbRe = new List<double>();
for (int i = 0; i < 4; i++)
{
double x = bb[2 * i];
double y = bb[2 * i + 1];
bbRe.Add(Cx + (x - Cx) * Math.Cos(angle) - (y - Cy) * Math.Sin(angle));
bbRe.Add(Cy + (x - Cx) * Math.Sin(angle) + (y - Cy) * Math.Cos(angle));
}
return bbRe;
}
結果
このプログラムを適応することで、より正確にレシート上の行を認識することができると思います。
まだ、改善の余地はありますが、それは今後でき次第随時まとめていきます。
結果の画像はこちらになります。
レシートをBoundingBox
で囲った画像は二枚あります。一枚目は前回までのすべてを同じ傾きで、回転したものです。二枚目は今回の方法で補正したもので、全体的に同じ行の要素が、水平に表示されていると思います。もともと水平だったものは、ずれが増加している部分もありますが、同じ行をまとめてコンソールに出力したものを見ると、正確に判別できていることが確認できます。
さいごに
今後は、このアルゴリズムを改善していき、より高精度の行の判別を実現していく予定です。
また、これらを利用したOCRは、LINEからどなたでも無料でご利用いただけますので、
ぜひ試してください!
また、OCRのでも体験がこちらのページからできます!
参考
コードをGitに掲載しているのでそちらも確認してください。