(2017年12月) PythonとOpenCVをこれからやってみる - 4 - 座標設定

#はじめに
PythonもOpenCVもこれまでまったく触れてこなかったけど、ちょっとやってみたいと思ってはじめたので、色々と試しているという状況です。
前回『[(2017年12月) PythonとOpenCVをこれからやってみる - 3 - 文字認識(1) - Qiita](https://qiita.com/rrryutaro/items/0c0a7382560e1f67123b)』はOCR的な事を行ないました。
前々回と合わせて、画像上の読み取り位置を指定する座標設定をする方法が必要な状況なので、今回は簡単なツールを用意する事にしました。

環境はWindows7 64bitです。

#座標設定
当初Excelに画像を貼り付けて、その上に図形を置いて、VBAで図形の座標を出力しようと思いましたが、そう言えば、Excelの図形はピクセルで座標を指定できないため、厄介です。
そこで、PowerPointで、その様にすることにしました。
使用するバージョンは2016版です。

まずは、PowerPointに元画像を配置します。
もし、画像が大きい場合、[デザイン]-[スライドのサイズ]でサイズ変更します。

イメージや図形はShapesで参照します。

```vbnet
Presentations(1).Slides(1).Shapes
```

とりあえず、今回は簡単に済ますため、最初にイメージを貼り付けて、その後に読み取り位置の図形を配置する事にしますので、`Shapes(1)`が元画像、`Shapes(2)`以降が読み取り位置の図形として座標設定を出力する形にします。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/156225/cfa6aed4-90b6-240f-1f05-785ed72c652a.png)

まずは、座標情報を処理しやすいように簡単なクラスを用意します。

```vbnet:オブジェクト名：Rectangle
Const vbDq As String = """"

Public x As Long
Public y As Long
Public width As Long
Public height As Long

Public Sub SetData(ByRef s As Shape)
    x = WorksheetFunction.Round(s.Left, 0)
    y = WorksheetFunction.Round(s.Top, 0)
    width = WorksheetFunction.Round(s.width, 0)
    height = WorksheetFunction.Round(s.height, 0)
End Sub

Public Sub SubtractPosition(ByRef r As Rectangle)
    x = x - r.x
    y = y - r.y
End Sub

Public Function GetJsonString() As String
Dim fields As Variant
Dim values As Variant
Dim list(3) As String
Dim i As Long
    fields = Array("x", "y", "width", "height")
    values = Array(x, y, width, height)
    For i = 0 To UBound(fields)
        list(i) = vbDq & fields(i) & vbDq & ":" & values(i)
    Next
    GetJsonString = "{" & Join(list, ",") & "}"
End Function
```

`Shapes`から得られる値が小数点以下の値でしたので、`WorksheetFunction.Round`を利用しています。
JSON形式の操作については、今回は構造が単純ですので、そのまま出力するようにしました。

続いて、標準モジュールにJSON形式でファイル出力するSubプロシージャを作成します。

```vbnet:標準モジュール
Sub OutputJsonFile(ByRef Shapes As Shapes, ByVal FileName As String)
Dim i As Long
Dim list() As String
Dim rect As Rectangle
Dim img As New Rectangle
Dim fs As New FileSystemObject

    ReDim list(Shapes.Count - 2)

    img.SetData Shapes(1)
    For i = 2 To Shapes.Count
        Set rect = New Rectangle
        rect.SetData Shapes(i)
        rect.SubtractPosition img
        list(i - 2) = rect.GetJsonString
    Next
    
    With CreateObject("ADODB.Stream")
        .Charset = "UTF-8"
        .Open
        .WriteText "[" & Join(list, ",") & "]"
        .SaveToFile FileName, 2
        .Close
    End With
    
End Sub
```

次のような感じで実行します。

```vbnet
OutputJsonFile Presentations(1).Slides(1).Shapes,"c:\hoge.json"
```

```json:hoge.json
[{"x":19,"y":10,"width":785,"height":94},{"x":19,"y":127,"width":57,"height":25},{"x":89,"y":127,"width":65,"height":25},{"x":19,"y":228,"width":117,"height":31},{"x":19,"y":297,"width":747,"height":79}]
```

#座標設定ファイルを読込んで読み取りを行なう
前回のコードを修正して、引数を指定するようにしました。

```py3:ocr.py
from PIL import Image
import sys
import pyocr
import pyocr.builders
import cv2
import numpy as np
import json 

tools = pyocr.get_available_tools()
if len(tools) == 0:
    print("No OCR tool found")
    sys.exit(1)

tool = tools[0]

args = sys.argv
img = cv2.imread(args[1], cv2.IMREAD_GRAYSCALE)

#座標の指定は x, y, width, Height
file = open(args[2], "r", encoding="utf-8_sig")
boxs = json.load(file)

for box in boxs:
    #イメージは OpenCV -> PIL に変換する
    txt = tool.image_to_string(Image.fromarray(img[box["y"]:box["y"]+box["height"], box["x"]:box["x"]+box["width"]]), lang="jpn", builder=pyocr.builders.TextBuilder(tesseract_layout=6))
    print(txt)
```

```:実行結果
>ocr.py hoge.png hoge.json
(②0①⑦ 年 ⑫ 月 ) Python と OpenCV を こ れ か ら
や っ て み る - ① - は じ め の 一 歩
pwhon
ocpancy
は じ め に
Python0 バ イ ソン ) を 中 々 使 う 機 会 が 無 く 、OpenC/ オ ー プ ン シ ー プ
 イ ) も 気 に は な り つ つ 使 う 橋 会 が 無 く 。
そ ん な 承 、 ち ょ っ と や っ て み よ う と 思 っ た の で 、 う か ら や る
 に あ た っ て の 導 入 や は じ め の 一 歩 な ど 。
邉 境 は Windows⑦ ⑥bt で や り ま す 。
```


#座標設定をExcelへ出力する
前々回のOMRについては、座標情報だけでなく、マーク有無を判断する閾値と、マーク読み取り時の出力設定なども必要になります。
このため、座標情報をExcel側に出力して、追加の情報をExcelで管理し、Excel側でJSONファイルを出力する事にします。

まずは、PowerPoint側の標準モジュールにExcelへ出力する処理を追加します。
（※参照設定で"Microsoft Excel 16.0 Object Library"を追加しています）

```vbnet:標準モジュール
Sub OutputExcel(ByRef Shapes As Shapes)
Dim s As Shape
Dim book As Workbook
Dim sheet As Worksheet
Dim r As Range
Dim i As Long
Dim list As New Collection
Dim img As New Rectangle
Dim rect As Rectangle

    img.SetData Shapes(1)

    Set book = Workbooks.Add
    book.Application.Visible = True
    Set sheet = book.Worksheets(1)
    sheet.Visible = xlSheetVisible
    sheet.Range("A1:D1") = Array("X", "Y", "Width", "Height")
    Set r = sheet.Range("A2")
    
    For i = 2 To Shapes.Count
        Set rect = New Rectangle
        rect.SetData Shapes(i)
        rect.SubtractPosition img
        
        r.Offset(, 0) = rect.x
        r.Offset(, 1) = rect.y
        r.Offset(, 2) = rect.width
        r.Offset(, 3) = rect.height
        Set r = r.Offset(1)
    Next
End Sub
```

次のように設定してExcelへ出力します。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/156225/995f9284-f7a1-7b91-1d43-351ee8578110.png)
なお、原因がよくわかっていませんが、画像を挿入した際に元のサイズと変わって挿入されてしまいましたので、次のようにイミディエイトウィンドウでサイズを直接打ち込んで調整しました

```vbnet
Presentations(1).Slides(2).Shapes(1).Width = 699
```

そして、次のように実行

```vbnet
OutputExcel Presentations(1).Slides(2).Shapes
```

![image.png](https://qiita-image-store.s3.amazonaws.com/0/156225/956f65da-421f-279e-6b3f-37d41b9037d9.png)

作成されたExcelのシートに閾値の設定と出力する内容の設定を次のようにしました。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/156225/6c9da4a8-7a04-eb97-0738-5322dc68ac05.png)

そして、出力用の処理を追加して、jsonファイルを出力します。

```vbnet
Const vbDq As String = """"

Sub OutputOMRJsonFile(ByRef ws As Worksheet, ByVal FileName As String)
Dim UsedRange As Range
Dim Caption As Range
Dim i As Long
Dim j As Long
Dim list1() As String
Dim list2() As String
    Set UsedRange = ws.UsedRange
    Set Caption = UsedRange.Rows(1)
    
    ReDim list1(UsedRange.Columns.Count - 1)
    ReDim list2(UsedRange.Rows.Count - 2)
    
    For i = 2 To UsedRange.Rows.Count
        For j = 1 To UsedRange.Columns.Count
            If Caption.Cells(, j) = "Output" Then
                list1(j - 1) = vbDq & Caption.Cells(, j) & vbDq & ":" & vbDq & UsedRange(i, j) & vbDq
            Else
                list1(j - 1) = vbDq & Caption.Cells(, j) & vbDq & ":" & UsedRange(i, j)
            End If
        Next
        list2(i - 2) = "{" & Join(list1, ",") & "}"
    Next
    

    With CreateObject("ADODB.Stream")
        .Charset = "UTF-8"
        .Open
        .WriteText "[" & Join(list2, ",") & "]"
        .SaveToFile FileName, 2
        .Close
    End With
End Sub
```

```vbnet
OutputOMRJsonFile ActiveSheet, "c:\omr.json"
```

```json:出力結果
[{"X":468,"Y":160,"Width":32,"Height":15,"Threshold":10,"Output":"1-Yes"},{"X":506,"Y":160,"Width":27,"Height":15,"Threshold":10,"Output":"1-No"},{"X":468,"Y":182,"Width":32,"Height":15,"Threshold":10,"Output":"2-Yes"},{"X":506,"Y":182,"Width":27,"Height":15,"Threshold":10,"Output":"2-No"},{"X":468,"Y":203,"Width":32,"Height":15,"Threshold":10,"Output":"3-Yes"},{"X":506,"Y":203,"Width":27,"Height":15,"Threshold":10,"Output":"3-No"},{"X":468,"Y":224,"Width":32,"Height":15,"Threshold":10,"Output":"4-Yes"},{"X":506,"Y":224,"Width":27,"Height":15,"Threshold":10,"Output":"4-No"},{"X":468,"Y":245,"Width":32,"Height":15,"Threshold":10,"Output":"5-Yes"},{"X":506,"Y":245,"Width":27,"Height":15,"Threshold":10,"Output":"5-No"},{"X":468,"Y":266,"Width":32,"Height":15,"Threshold":10,"Output":"6-Yes"},{"X":506,"Y":266,"Width":27,"Height":15,"Threshold":10,"Output":"6-No"},{"X":468,"Y":288,"Width":32,"Height":15,"Threshold":10,"Output":"7-Yes"},{"X":506,"Y":288,"Width":27,"Height":15,"Threshold":10,"Output":"7-No"},{"X":468,"Y":309,"Width":32,"Height":15,"Threshold":10,"Output":"8-Yes"},{"X":506,"Y":309,"Width":27,"Height":15,"Threshold":10,"Output":"8-No"},{"X":468,"Y":330,"Width":32,"Height":15,"Threshold":10,"Output":"9-Yes"},{"X":506,"Y":330,"Width":27,"Height":15,"Threshold":10,"Output":"9-No"},{"X":468,"Y":351,"Width":32,"Height":15,"Threshold":10,"Output":"10-Yes"},{"X":506,"Y":351,"Width":27,"Height":15,"Threshold":10,"Output":"10-No"}]
```

#マークを読み取る処理の改善
前々回『[(2017年12月) PythonとOpenCVをこれからやってみる - 2 - マークを読取る - Qiita](https://qiita.com/rrryutaro/items/10142d3c3397406e5e2b)』で作成したコードを次のように修正します。

```py3:omr.py
import sys
import cv2
import numpy as np
import json 

args = sys.argv
img1 = cv2.imread(args[1], cv2.IMREAD_GRAYSCALE)
img2 = cv2.imread(args[2], cv2.IMREAD_GRAYSCALE)
boxs = json.load(open(args[3], "r", encoding="utf-8_sig"))

for box in boxs:
	img1_2 = img1[box["Y"]:box["Y"]+box["Height"], box["X"]:box["X"]+box["Width"]]
	img2_2 = img2[box["Y"]:box["Y"]+box["Height"], box["X"]:box["X"]+box["Width"]]
	if box["Threshold"] < np.mean(img1_2) - np.mean(img2_2):
		print(box["Output"])
```

次の画像を読み取ってみます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/156225/900e2e6f-dd9e-72d8-bcf1-f3afed0543a4.png) ![image.png](https://qiita-image-store.s3.amazonaws.com/0/156225/f6e881c5-53e6-f14a-020c-593f9e0718bf.png)

```:実行結果
>omr.py cs1.png cs2.png omr.json
1-Yes
2-No
5-Yes
8-Yes
9-Yes
9-No
10-No
```

4行目のNoに丸をつけたつもりでしたが、記入の判断がされませんでした。
数値としては`4-No : 9.42716049383`となっていましたので、こうした部分について閾値で調整する事になってくるかとおもいます。

今回テストで使用している帳票は、本来は☐の部分を塗りつぶす形ですが、上から〇を付ける様なオーバーライトチェックマークを想定した帳票は、なぞるように薄く印刷されていたりするかと思います。
そうした場合、2階調にすればより精度が高くなるのではとも思います。


以上

#参考にした記事など
- [Office TANAKA - Excel VBA講座：ファイルの操作[UTF-8形式のテキストファイルに書き込む]](http://officetanaka.net/excel/vba/file/file11.htm)
- [PythonにおいてのJSONファイルの取扱いあれこれ - Qiita](https://qiita.com/wakaba130/items/5f54aed913156dc4438f)
- [WindowsでCP932(Shift-JIS)エンコード以外のファイルを開くのに苦労した話 - Qiita](https://qiita.com/Yuu94/items/9ffdfcb2c26d6b33792e)
- [コマンドライン引数 - Python入門から応用までの学習サイト](http://www.python-izm.com/contents/basis/command_line_arguments.shtml)

#これまでの記事
|前回|一覧|次回|
|:--|:--:|--:|
|[3 - 文字認識(1)](https://qiita.com/rrryutaro/items/0c0a7382560e1f67123b)|[一覧](https://qiita.com/rrryutaro/items/933ec2ed754a9f3a2d3e)|[5 - 学習(1)](https://qiita.com/rrryutaro/items/ad3107c2afd50b791db3)|