マークシートの自動読み取りプログラム

技術士

プログラムの作成(後半)

マーク読み取りプログラムの流れ

集計はエクセルがやりやすいので、

・エクセルのVBAでマークシートの画像ファイルを選択し、フルパスをPythonに渡す
・Pythonで画像ファイルを開き、マークを読み取り、結果をVBAに返す
・VBAで結果をセルに反映する

という流れで作ります。

読み取りプログラム(Python)

まずはPythonのコードから。このプログラムに渡す引数の1つ目は画像ファイルのフルパス、2つ目は結果を画像で確認するかどうかのフラグ、です。

Python
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%を超えているならばマークされていると判定

します。あわせて、その四角形を塗りつぶします。

50%以下ならばマークされていないと判定

します。あわせて、四角形の枠だけを画像に上書きます。

さらに、割合の数字をその四角形の上に書かせています(その画像は最後に)。

50%というのは画像の読み取りの濃さなどに依存するため、薄い画像なら割合は小さく、濃い画像なら大きくします。この数字をどうするかは試行錯誤です。汎用的なものにするには、この割合も引数などで渡した方がいいでしょう。

残りのコードはこまごまとしたものになります。

集計・表示プログラム(エクセル・VBA)

次は、このPythonコードを呼び出すVBAです。
まずエクセルの画面設計はこんな感じです。

「読み込み」ボタンを押すと、マークシートの画像ファイルを格納するフォルダにある、すべての画像ファイルのパスを順次読みだし、Pythonに1つづつそのファイルパスを渡し、Pythonにてマーク判断した結果を受け取って、B4セル以降に展開します。

講義の中では、この画面を見ながら、この設問はどのくらいの難易度であったのか、ミスした方はどのマークが多かったのか、を述べつつ、間違いやすい点などを重点的に説明しています。

VBAのコードは以下の通りです。

VB
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%の分岐点も機能しており、なかなかうまくいきました。

さて、このプログラム(システム)では、50枚くらいのマークシートの読み取りが準備含めて3分くらい、マークの読み取りが2分くらいで、全体でも10分以内で十分に終わります。

手動で読み取っていたころと比べると、圧倒的に正確かつ楽になりました。

ただ、ScanSnap(2kg)とノートパソコン(1kg)、充電器やケーブルなど含めてあわせて4kg弱を持ち運ぶため、重いといえば重い・・・

まとめ

・汎用的なマークシートを読み取り、自動集計させるプログラムを作りました。
・使ったのは、
  ポータブルスキャナ(ScanSnap iX1300)・・・マークシートの連続スキャン
  Python+OpenCV・・・マークの有無の判定
  エクセル+VBA・・・画像ファイルの連続指定、結果の集計
 です。
・マークの読み取りミスもなく、快適に動作しています。
・マークの絶対位置を調べるプログラムと、マークを読み取るプログラムを別々に作りましたが、一つにすると、いろいろなマークシートに適用できそうです。

以上、ご覧いただきましてありがとうございました。