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

技術士

この取り組みの背景

講師のお仕事の中で、区切り区切りで受講生の皆様にテストを受けて頂く回があります。テストはマークシート回答方式で、回答後、マークシートは本部に送られて処理され、後日個人別のマイページで正誤が開示されるようになっています。

私の場合、テスト1時間と解説1.5時間の間にある休憩10分の間に、集めたマークシートを「目で」高速に読み取り、設問ごとの正答数と間違いが多いマークを手動でノートに記録し、また、設問ごとの点数とテストの受講者数から、クラスの平均点を出しています。そうすると、1.5時間の解説を有意義に進めることができます。

ただ、この「目」で読み取って記録する方式なのですが、テストが25問(1問あたり5マーク)くらいとすると、25人分くらいが10分で読み取れる限界であるということ、それでもとんでもない集中力が必要で疲れるということと、さらにだんだん視力が弱くなってきた(老眼・・)こともありまして、PCを使って自動化したいなと思いました。

イメージとしては、テスト終了直後にポータブルスキャナで全員分のマークシートをスキャンして画像として取り込み、その後マークの有無を判定するソフトを使って集計する、というものです。

そのためのいいソフトがないか、いろいろと調べたのですが、いずれも

専用のマークシートが必要

であったり、

マークを判定するための特定の印が必要(結局は専用のマークシートに近い・・)

でして。フリーソフトの Formscanner が、カスタマイズもできていい感じだったのですが、隅に黒四角などのマーカーが必要で、そのマーカーからの相対位置でマークを認識する仕様でした。汎用的なものもあるようなのですが、えらく高額でして。

例えばスキャンのたびに用紙がずれ、マークシート画像のずれや傾きが大きければ、特定のマーカーが必要なのかもしれませんが、マークシートはやや厚く、連続スキャンしても大きくずれる気がしません。そうすると、

マーク位置そのものを絶対位置で指定すればいいように思うのですが、需要がないからなのか、そのようなソフトがなさそう。

そこで自作することにしました。

スキャナの購入

スキャナは持ち運べる重量でないといけないので、ScanSnapのiX100(重量400g)にしようかと思ったのですが、合間の10分での作業となるため、連続で読み取りできた方がよく、一つ上位機種のiX1300を購入しました(重量2kg)

ScanSnap iX1300
ついでに買ったiX1300用のケース。ACアダプタとUSBケーブルも入って便利

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

Pythonの環境準備

普段はエクセルで集計作業をすることが多いのでエクセルのVBAを多用するのですが、画像処理などは難しいので、そこはPython+OpenCVで作ることに。開発環境はAnaconda3です。OpenCVは、anaconda_promptから、

(base) C:\Users\User>pip install opencv-python

でインストールできます。

マーク画像の絶対位置を知る(Python)

まず、読み取ったマークシートの画像ファイルから、マーク位置の座標(絶対位置)を知る必要があるため、そのプログラムを作ります。

座標を知るプログラム

Python
import cv2
import tkinter as tk
from tkinter import filedialog

# Tkinterでファイル選択ダイアログを開く
root = tk.Tk()
root.withdraw()  # Tkウィンドウを表示しない
file_path = filedialog.askopenfilename(
    title="画像を選択してください",
    initialdir="C:/Users/User/MarkSheet",  # 初期フォルダ
    filetypes=[("画像ファイル", "*.jpg;*.png;*.bmp;*.jpeg")]
)

if not file_path:
    print("画像が選択されませんでした。")
    exit()

# 画像を読み込む
img = cv2.imread(file_path)

if img is None:
    print("画像が正しく読み込めませんでした。")
    exit()

# ウィンドウ設定
cv2.namedWindow('Image', cv2.WINDOW_NORMAL)
cv2.resizeWindow('Image', 800, 600)

# スクロール用変数
scroll_x = 0
scroll_y = 0
win_w, win_h = 800, 600

def update_display():
    """ スクロール位置に応じて画像を表示 """
    global scroll_x, scroll_y
    max_x = max(0, img.shape[1] - win_w)
    max_y = max(0, img.shape[0] - win_h)
    scroll_x = min(scroll_x, max_x)
    scroll_y = min(scroll_y, max_y)
    
    cropped = img[scroll_y:scroll_y + win_h, scroll_x:scroll_x + win_w]
    cv2.imshow('Image', cropped)

def click_event(event, x, y, flags, param):
    """ マウスクリック時のイベント処理 """
    if event == cv2.EVENT_LBUTTONDOWN:
        real_x, real_y = scroll_x + x, scroll_y + y
        print(f"Clicked at: ({real_x}, {real_y})")
        cv2.circle(img, (real_x, real_y), 5, (0, 0, 255), -1)
        update_display()

def on_trackbar_x(val):
    """ 横スクロールバーの処理 """
    global scroll_x
    scroll_x = val
    update_display()

def on_trackbar_y(val):
    """ 縦スクロールバーの処理 """
    global scroll_y
    scroll_y = val
    update_display()

# スクロールバー作成
cv2.createTrackbar('X Scroll', 'Image', 0, max(0, img.shape[1] - win_w), on_trackbar_x)
cv2.createTrackbar('Y Scroll', 'Image', 0, max(0, img.shape[0] - win_h), on_trackbar_y)

# マウスイベントを設定
cv2.setMouseCallback('Image', click_event)

update_display()
cv2.waitKey(0)
cv2.destroyAllWindows()

最初にファイルダイアログで、マークシートの画像を呼び出します。画像の座標を知りたいところをクリックすると、座標の絶対位置(x,y)が出力されます。知りたい位置は、以下の通りです。

1列1行目の一番左のマークの左上位置・・・a
  絶対位置=a
1列1行目の一番左のマークの右上位置・・・b
  マークの幅(w)=b(x)-a(x)
1列1行目の一番左のマークの左下位置・・・c
  マークの高さ(h)=c(y)-a(y)
1列1行目の一番右のマークの左上位置・・・d
  マークの間隔=(d(x)-a(x))/(1行のマーク数-1)
1列最終行の一番左のマークの左上位置・・・e
  マークの行間隔=(e(y)-a(y))/(行数-1)
最終列1行目の一番左のマークの左上位置・・・f
  マークの列間隔=(f(x)-a(x))/(列数-1)

上記のプログラムを実行して、スキャンしたマークシートのjpgファイルを選択して表示し、画面上をクリックすると以下のように赤い点で表示され(マークシートが赤色なので見えづらくてすみません)、同時に座標の位置が出力されます。Spyder上で実行しています。

マークシート1列名の1行目、一番左のマークからa~cを、一番右のマークの左上dの座標の位置を取得
マークシート1列名の最終行、一番左の左上eの位置を取得
最終列(2列しか使わないのでここでの最終列は2列)の1行目、一番左のマーク左上fの座標の位置を取得
画像のクリックの都度、座標の位置が出力される。上から順にa~f

以上の結果から、以下の表の値が取得できました。

内容
絶対位置 ax=546, y=4015
マークの幅(w)=b(x)-a(x)50
マークの高さ(h)=c(y)-a(y)44
マークの間隔=(d(x)-a(x))/(1行のマーク数-1)398/4
マークの行間隔=(e(y)-a(y))/(行数-1)1677/14
マークの列間隔=(f(x)-a(x))/(列数-1)1000

いよいよ次はマークを読み取ります。