1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

レシートOCRの精度向上に向けて文字の位置座標を補正する

Last updated at Posted at 2023-10-31

コード実装編

前回まで

レシートが折れ曲がっている状態で、撮影するとその折れ曲がりを境に、文字の斜めっている角度が異なってしまいます。これを改善するアプローチを考えてきました。詳細は、前回までの記事を読んでみてください。

全体の流れ

今回は大まかに次のようにまとめました。
1.レシート内最大要素の選択
2.最大要素を用いて左右の縦方向のずれを取得
3.その他要素の回転と縦方向の移動

レシート内最大要素の選択

ここでは横幅、つまりboundingboxの全体のx座標の差を取得し、一番大きいものの要素番号を取得しています。レシートデータについて説明すると、Linesには画像から取得した文字要素のデータが文字列ごとにまとめられています。そのデータの中にBoundingBoxが入っており、今回はこのデータのみを使用します。詳細なデータ構造に関しては、Gitに公開しているReceipt.csを確認してください。

main
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として作成しました。

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を獲得します。

main
var biggestCenter = CalcCenter(ReadResult.Lines[NumOfMaxIdx].BoundingBox);
var BiggestData = RotDiv(ReadResult.Lines[NumOfMaxIdx].BoundingBox);
double div = CalcDiv(BiggestData.leftBb,BiggestData.rightBb);

CalcCenterでは要素の中心座標を計算します。

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を返り値として取得します。それと、今回は使用しませんが、回転に使用した弧度法であらわした角度も返します。

RotDiv
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を計算します。

CalcDiv
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度としたときの傾きを計算します。

CalcAngle
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座標の差をなるべく少なくすることが可能です。

main
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の要素を引数に入れることで傾きを得ることができます。

CalcBias
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)));
    }

RotateBbBoundingBoxを回転する関数で、傾きと要素の中心の座標を引数に入れることで、回転された後のBoundingBoxを返します。

RotateBb
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で囲った画像は二枚あります。一枚目は前回までのすべてを同じ傾きで、回転したものです。二枚目は今回の方法で補正したもので、全体的に同じ行の要素が、水平に表示されていると思います。もともと水平だったものは、ずれが増加している部分もありますが、同じ行をまとめてコンソールに出力したものを見ると、正確に判別できていることが確認できます。

SevenOuttextLine.png

output.png

output_sevenEleven_after.png

さいごに

今後は、このアルゴリズムを改善していき、より高精度の行の判別を実現していく予定です。
また、これらを利用したOCRは、LINEからどなたでも無料でご利用いただけますので、
ぜひ試してください!

また、OCRのでも体験がこちらのページからできます!

参考

コードをGitに掲載しているのでそちらも確認してください。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?