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

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

https://qiita.com/y-okada1412/items/d6c6036b0c7d972f1ad9

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

<a id="midasi1"></a>
## レシート内最大要素の選択
ここでは横幅、つまりboundingboxの全体の`x`座標の差を取得し、一番大きいものの要素番号を取得しています。レシートデータについて説明すると、`Lines`には画像から取得した文字要素のデータが文字列ごとにまとめられています。そのデータの中に`BoundingBox`が入っており、今回はこのデータのみを使用します。詳細なデータ構造に関しては、Gitに公開している`Receipt.cs`を確認してください。
```C#: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`として作成しました。
```C#: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;
    }
```

<a id="midasi2"></a>
## 最大要素を用いて要素の縦のずれを取得
ここでは先ほど取得した最大要素を、二つの基準から回転し、二通りの`BoundingBox`を得ます。
その二つの`BoundingBox`は右端と、左端の`y`座標の差`div`を計算します。そのコードを掲載します。`main`では作成した関数を利用し、最大要素から`div`を獲得します。

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

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

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

```C#: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`を計算します。

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

```C#: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;
    }
```

<a id="midasi3"></a>
## その他要素の回転とdivによる補正
最後にレシート上の要素をすべて補正していきます。すべて同様に水平になるように回転し、その後最大要素の中間座標より右側にあるものは、`div`を引くことでレシートの右寄りと、左寄りの要素での`y`座標の差をなるべく少なくすることが可能です。
```C#: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`の要素を引数に入れることで傾きを得ることができます。
```C#: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)));
    }

```
`RotateBb`は`BoundingBox`を回転する関数で、傾きと要素の中心の座標を引数に入れることで、回転された後の`BoundingBox`を返します。
```C#: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3566779/962c60cb-73b6-8e2a-8e10-aa101aed2956.png)

![output.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3566779/4dfabca2-bb20-2ad5-92f1-d5c30b25320b.png)


![output_sevenEleven_after.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3566779/6018e403-ace3-09df-9e6f-4731ecda6a34.png)

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

https://receiptroller.com/ja


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

https://ocr.receiptroller.com/read-receipt


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

https://github.com/y-okada1412/Receipt-get-line