この取り組みの背景
講師のお仕事の中で、区切り区切りで受講生の皆様にテストを受けて頂く回があります。テストはマークシート回答方式で、回答後、マークシートは本部に送られて処理され、後日個人別のマイページで正誤が開示されるようになっています。
私の場合、テスト1時間と解説1.5時間の間にある休憩10分の間に、集めたマークシートを「目で」高速に読み取り、設問ごとの正答数と間違いが多いマークを手動でノートに記録し、また、設問ごとの点数とテストの受講者数から、クラスの平均点を出しています。そうすると、1.5時間の解説を有意義に進めることができます。
ただ、この「目」で読み取って記録する方式なのですが、テストが25問(1問あたり5マーク)くらいとすると、25人分くらいが10分で読み取れる限界であるということ、それでもとんでもない集中力が必要で疲れるということと、さらにだんだん視力が弱くなってきた(老眼・・)こともありまして、PCを使って自動化したいなと思いました。
そのためのいいソフトがないか、いろいろと調べたのですが、いずれも
であったり、
でして。フリーソフトの Formscanner が、カスタマイズもできていい感じだったのですが、隅に黒四角などのマーカーが必要で、そのマーカーからの相対位置でマークを認識する仕様でした。汎用的なものもあるようなのですが、えらく高額でして。
例えばスキャンのたびに用紙がずれ、マークシート画像のずれや傾きが大きければ、特定のマーカーが必要なのかもしれませんが、マークシートはやや厚く、連続スキャンしても大きくずれる気がしません。そうすると、
そこで自作することにしました。
スキャナの購入
スキャナは持ち運べる重量でないといけないので、ScanSnapのiX100(重量400g)にしようかと思ったのですが、合間の10分での作業となるため、連続で読み取りできた方がよく、一つ上位機種のiX1300を購入しました(重量2kg)


プログラムの作成(前半)
Pythonの環境準備
普段はエクセルで集計作業をすることが多いのでエクセルのVBAを多用するのですが、画像処理などは難しいので、そこはPython+OpenCVで作ることに。開発環境はAnaconda3です。OpenCVは、anaconda_promptから、
(base) C:\Users\User>pip install opencv-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)が出力されます。知りたい位置は、以下の通りです。
絶対位置=a
マークの幅(w)=b(x)-a(x)
マークの高さ(h)=c(y)-a(y)
マークの間隔=(d(x)-a(x))/(1行のマーク数-1)
マークの行間隔=(e(y)-a(y))/(行数-1)
マークの列間隔=(f(x)-a(x))/(列数-1)
上記のプログラムを実行して、スキャンしたマークシートのjpgファイルを選択して表示し、画面上をクリックすると以下のように赤い点で表示され(マークシートが赤色なので見えづらくてすみません)、同時に座標の位置が出力されます。Spyder上で実行しています。




以上の結果から、以下の表の値が取得できました。
内容 | 値 |
絶対位置 a | x=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 |
いよいよ次はマークを読み取ります。