プログラムの作成(後半)
マーク読み取りプログラムの流れ
集計はエクセルがやりやすいので、
・Pythonで画像ファイルを開き、マークを読み取り、結果をVBAに返す
・VBAで結果をセルに反映する
という流れで作ります。
読み取りプログラム(Python)
まずはPythonのコードから。このプログラムに渡す引数の1つ目は画像ファイルのフルパス、2つ目は結果を画像で確認するかどうかのフラグ、です。
import cv2
import numpy as np
import sys
# コマンドライン引数から画像パスを取得
if len(sys.argv) < 2:
print("ERROR: 画像ファイルが指定されていません")
sys.exit(1)
image_path = sys.argv[1] # VBA から渡された画像パス
image_yesno = sys.argv[2]
# 画像の読み込み(グレースケール)
img = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
if img is None:
print("ERROR: 画像を読み込めません")
sys.exit(1)
# 画像の2値化
_, thresh = cv2.threshold(img, 150, 255, cv2.THRESH_BINARY_INV)
# マークの配置パラメータ
start_x, start_y = 540, 4010 # 最初のマークの左上基準点
w, h = 50, 40 # マークの幅と高さ
x_spacing = 1001 # 列間の距離
y_spacing = 1685 / 14 # 行間の距離
mark_spacing = 100 # 1問内のマーク間の距離(縦)
# 判定結果を標準出力に出力(1行ずつ)
for col in range(3):
for row in range(15):
base_x = int(start_x + col * x_spacing)
base_y = int(start_y + row * y_spacing)
row_result = []
for mark_index in range(5):
x = int(base_x + mark_index * mark_spacing)
y = int(base_y)
roi_img = thresh[y:y+h, x:x+w]
# ROI内の白ピクセル(塗りつぶし=マーク)の割合を計算
mark_ratio = np.sum(roi_img == 255) / (w * h)
# しきい値(50%以上の黒領域ならマークありと判定)
if mark_ratio > 0.5:
cv2.rectangle(img, (x, y), (x + w, y + h), (0, 0, 255), -1)
cv2.putText(img, "{:.2f}".format(mark_ratio), (x, y - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 0, 255), 2) # テキスト表示
marked = 1
else:
cv2.rectangle(img, (x, y), (x + w, y + h), (0, 0, 255), 2)
cv2.putText(img, "{:.2f}".format(mark_ratio), (x, y - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 0, 255), 2) # テキスト表示
marked = 0
row_result.append(str(marked))
# "1,0,1,0,0" のような形式で1行ずつ出力
print(",".join(row_result))
img = img[3500:6000, 0:5000]
img = cv2.resize(img, dsize=None, fx=0.4, fy=0.4)
# 画像を表示
if image_yesno == "1":
cv2.imshow("ROI", img)
cv2.waitKey(0) # キーが押されるまで表示
cv2.destroyAllWindows()
ポイント(読み取りプログラムの中核)
ここでいくつかポイントとなるコードがあります。
# マークの配置パラメータ
start_x, start_y = 540, 4010 # 最初のマークの左上基準点
w, h = 50, 40 # マークの幅と高さ
x_spacing = 1001 # 列間の距離
y_spacing = 1685 / 14 # 行間の距離
mark_spacing = 100 # 1問内のマーク間の距離(縦)
こちらは、先ほど表に示しました、座標位置(絶対位置)から得られる値です。VBAからこの値を渡すようにすると、いろいろなマークシートに対応する汎用的なものになるのですが、講義の中で使われるマークシートは1種類(マークシートの色は違うのですが、位置は全く一緒)なので、コードに埋め込んでいます。
先ほどの表の値と微妙に異なりますが、スキャン時にやや傾いたり、前後にずれたりするので、何枚か読み取って平均的な値を入れています。マークの高さは前後にややずれることもあることを加味して、やや薄くしています。
_, thresh = cv2.threshold(img, 150, 255, cv2.THRESH_BINARY_INV)
#
roi_img = thresh[y:y+h, x:x+w]
mark_ratio = np.sum(roi_img == 255) / (w * h)
こちらがマーク読み取りの核となる部分です。マークに位置合わせた四角形の内部を2値化して色を反転させ、
を求めています。
# しきい値(50%以上の黒領域ならマークありと判定)
if mark_ratio > 0.5:
cv2.rectangle(img, (x, y), (x + w, y + h), (0, 0, 255), -1)
cv2.putText(img, "{:.2f}".format(mark_ratio), (x, y - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 0, 255), 2) # テキスト表示
marked = 1
else:
cv2.rectangle(img, (x, y), (x + w, y + h), (0, 0, 255), 2)
cv2.putText(img, "{:.2f}".format(mark_ratio), (x, y - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 0, 255), 2) # テキスト表示
marked = 0
その結果、マーク内の四角形内部が塗りつぶされている割合が
します。あわせて、その四角形を塗りつぶします。
します。あわせて、四角形の枠だけを画像に上書きます。
さらに、割合の数字をその四角形の上に書かせています(その画像は最後に)。
50%というのは画像の読み取りの濃さなどに依存するため、薄い画像なら割合は小さく、濃い画像なら大きくします。この数字をどうするかは試行錯誤です。汎用的なものにするには、この割合も引数などで渡した方がいいでしょう。
残りのコードはこまごまとしたものになります。
集計・表示プログラム(エクセル・VBA)
次は、このPythonコードを呼び出すVBAです。
まずエクセルの画面設計はこんな感じです。

「読み込み」ボタンを押すと、マークシートの画像ファイルを格納するフォルダにある、すべての画像ファイルのパスを順次読みだし、Pythonに1つづつそのファイルパスを渡し、Pythonにてマーク判断した結果を受け取って、B4セル以降に展開します。
VBAのコードは以下の通りです。
Sub main()
Call ReadMarkSheet
Call Irotsuke
Call Keisan
End Sub
Sub ReadMarkSheet()
Dim objShell As Object
Dim objExec As Object
Dim strCommand As String
Dim pythonExe As String
Dim scriptPath As String
Dim imagePath As String
Dim imageFile As String
Dim line As String
Dim errRow As Long
Dim imageYesNo As String
Dim i, j As Integer
' Python実行ファイルのパスを指定(環境に合わせて変更)
pythonExe = "C:\Users\User\anaconda3\python.exe"
' Pythonスクリプトのパスを指定(環境に合わせて変更)
scriptPath = "C:\Users\User\MarkSheet.py"
imagePath = "C:\Users\User\MarkSheet\"
imageFile = "*.jpg"
imageFile = Dir(imagePath & imageFile)
imageYesNo = Cells(1, "B").Value
Do While imageFile <> ""
' コマンド実行(画像のパスと画像ファイル表示の有無を引数に追加)
strCommand = pythonExe & " " & scriptPath & " """ & imagePath & imageFile & """" & " " & imageYesNo
Set objShell = CreateObject("WScript.Shell")
Set objExec = objShell.Exec(strCommand)
' Excel の書き込み開始位置
i = 4 ' 4行目から書き込み
' Python の標準出力からデータを受け取る
Do While Not objExec.StdOut.AtEndOfStream
line = objExec.StdOut.ReadLine
Dim values() As String
values = Split(line, ",") ' "1,0,1,0,0" → 配列に分割
If UBound(values) < 4 Then
errRow = Cells(Rows.Count, "Q").End(xlUp).Row + 1
Cells(errRow, "Q").Value = imageFile
Cells(errRow, "R").Value = values(0)
Else
' Excel に書き込み(B列からF列)
For j = 0 To UBound(values)
Cells(i, j + 2).Value = Cells(i, j + 2).Value + values(j)
Next j
End If
i = i + 1
Loop
' 後処理
Set objShell = Nothing
Set objExec = Nothing
imageFile = Dir()
Loop
MsgBox "マークの判定結果を Excel に書き込みました", vbInformation
End Sub
Sub Keisan()
Dim i, j As Integer
Dim Arr1, Arr2 As Variant
Arr1 = Array("あ", "い", "う", "え", "お")
Arr2 = Array("ア", "イ", "ウ", "エ", "オ")
For i = 4 To Cells(Rows.Count, "I").End(xlUp).Row
j = 0
Do While j < 5
If Cells(i, "I").Value = Arr1(j) Or Cells(i, "I").Value = Arr2(j) Then
Cells(i, "J").Value = Cells(i, j + 2).Value
j = 5
End If
j = j + 1
Loop
Next i
Exit Sub
End Sub
Sub Irotsuke()
Dim i, j As Integer
Dim Arr1, Arr2 As Variant
Arr1 = Array("あ", "い", "う", "え", "お")
Arr2 = Array("ア", "イ", "ウ", "エ", "オ")
Cells.Interior.ColorIndex = xlNone
For i = 4 To Cells(Rows.Count, "I").End(xlUp).Row
j = 0
Do While j < 5
If Cells(i, "I").Value = Arr1(j) Or Cells(i, "I").Value = Arr2(j) Then
Cells(i, j + 2).Interior.Color = vbYellow
j = 5
End If
j = j + 1
Loop
Next i
Exit Sub
End Sub
自分だけが使うので動けばいいわで作っております。
画像ファイルが格納されているフォルダの指定については、最初に選択させたり、どこかのセルにパスを書いておいてそれを取得してもいいんですが、ScanSnapで読み取ったマークシート画像の格納フォルダは固定しているので、選択させる必要もなく、コードに埋め込んでいます。
画像の表示あり、とした場合は、以下のように画面に表示されるので、マーク読み取り位置がずれていないか、マークあり/なしの白ピクセルの割合は適切か、を確認します。
(0.5を超えるとマークあり、0.5以下だとマークなし、と判定)

さて、このプログラム(システム)では、50枚くらいのマークシートの読み取りが準備含めて3分くらい、マークの読み取りが2分くらいで、全体でも10分以内で十分に終わります。
手動で読み取っていたころと比べると、圧倒的に正確かつ楽になりました。
ただ、ScanSnap(2kg)とノートパソコン(1kg)、充電器やケーブルなど含めてあわせて4kg弱を持ち運ぶため、重いといえば重い・・・
まとめ
・汎用的なマークシートを読み取り、自動集計させるプログラムを作りました。
・使ったのは、
ポータブルスキャナ(ScanSnap iX1300)・・・マークシートの連続スキャン
Python+OpenCV・・・マークの有無の判定
エクセル+VBA・・・画像ファイルの連続指定、結果の集計
です。
・マークの読み取りミスもなく、快適に動作しています。
・マークの絶対位置を調べるプログラムと、マークを読み取るプログラムを別々に作りましたが、一つにすると、いろいろなマークシートに適用できそうです。
以上、ご覧いただきましてありがとうございました。