アロハ-Pythonとその他日常

※採点アプリを作成したりしています。

目指せ自動採点!(問題番号を消す編)

ブログの紹介

このブログは現役教員が独学でプログラミング(Python)を学習して採点アプリを作ろうと思い、試行錯誤しながら作成にあたっております。そして、学んだことをアウトプットすることで、意味を理解しようという目的で書き残しています。 そして完成にあたり、欠かせないのが「採点斬り」)。こちらをフル活用しつつ、作成にあたっております。ぜひ一度、手を動かしながら使用してみて欲しいです。

まとめ記事はこちらから

自動採点のまとめ記事はこちらにあります。まとめ部分の一部の紹介になります。

問題番号を消す

処理の流れは以下の通り。

  1. 問題番号の部分を白紙(white.jpg)の解答から検出
  2. しっかり検出できているか確認
  3. 塗りつぶし処理を開始

また、あらかじめ採点で切り取った画像(outputフォルダ)を別場所にコピー(deepフォルダ)しておき 、処理をしていきます。

以下、コードになります。

# 画像周りを白で塗るための関数
def line_square(img, bold=25):
    img_line2 = cv2.line(img, (0,0), (0,img.shape[0]), (255,255,255), bold)# 左縦
    img_line2 = cv2.line(img_line2, (0,0), (img.shape[1],0), (255,255,255), bold)# 上横
    img_line2 = cv2.line(img_line2, (img.shape[1],0), (img.shape[1],img.shape[0]), (255,255,255), bold) # 右縦
    img_line2 = cv2.line(img_line2, (0,img.shape[0]), (img.shape[1],img.shape[0]), (255,255,255), bold) # 下横
    return img_line2

↓これが検出するための関数です。面積の値(floor)は適宜調整します。解答番号を四角で囲むための4つの頂点を出力します。

# 解答番号を検出するための関数
def Kensyutu(file, floor=100):
    rects = []
    x1=[]
    x2=[]
    y1=[]
    y2=[]
# 画像の読み込み
    img = cv2.imread(file)
# 読み込んだ画像まわりを白で塗る。塗りすぎに気を付ける。
    img = line_square(img, bold=10)
# グレー画像にする
    img_g = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    gray = cv2.fastNlMeansDenoising(img_g, h=5)
# 膨張処理
    ret3,th3 = cv2.threshold(gray,0,255,cv2.THRESH_BINARY+cv2.THRESH_OTSU)
    img_adth_er = cv2.erode(th3, None, iterations = 7)
    img_adth_er = cv2.dilate(img_adth_er, None, iterations = 5)
# 白黒反転処理
    img_adth_er_re = cv2.bitwise_not(img_adth_er)
# 画像の中の面積になるものを検出
    contours, _ = cv2.findContours(img_adth_er_re, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    for i,contour in enumerate(contours):
# 一定の値(今回は100)以上のもののみ、検出する。
        if cv2.contourArea(contour) >floor:
            print(cv2.contourArea(contour))
            rect = cv2.minAreaRect(contour)
            rect_points = cv2.boxPoints(rect).astype(int)
            rects.append(rect_points)
# 左上から右下順になるようにソート
    rects = sorted(rects, key=lambda x: (x[1][1], x[0][0]))
    for i in rects:
        x1.append(min(i.T[0]))
        x2.append(max(i.T[0]))
        y1.append(min(i.T[1]))
        y2.append(max(i.T[1]))
    return x1,x2,y1,y2

これで、検出するための準備ができました。次に塗りつぶせるか確認処理をします。

# 4つの座標を受け取って白く塗るための関数
※Qは塗ったり、確認するための色を決める値です。
※drawは-1で塗りつぶし、整数で囲む太さを決めます。

def In_white(file,x1,x2,y1,y2, Q, draw):
    img = cv2.imread(file)
    for x,X,y,Y in zip(x1,x2,y1,y2):
        if x < img.shape[1]/3:
            img = cv2.rectangle(img,(0,0),(X+5,Y+5), Q, draw)
        else:
            img = cv2.rectangle(img,(x-5,0),(img.shape[1], img.shape[0]), Q, draw)
    return img

確認する場合はQを黒「Q=(255,255,255)」としてdrawを四角で囲む「draw=1」とすれば囲んでくれます。

実際に実行

  1. outputフォルダをまんまコピー、deepフォルダを作成します。
import os
import shutil

if os.path.exists("./deep"):
    folda_deep_list = os.listdir("./deep")
    for folda in folda_deep_list:
        shutil.rmtree("./deep/" + folda)
    shutil.rmtree("./deep")
shutil.copytree("./setting/output","./deep")
import cv2
import glob
from matplotlib import pyplot as plt
%matplotlib inline

Q = (255,255,255)
draw = 1 
# 必ず確認
folda_deep_list = os.listdir("./deep")[1:]
for folda in folda_deep_list:    
    # 白紙画像ファイルを取得
    file="./deep/" +  folda + "/white.jpg"
    print(folda)
    # 番号の座標検出
    x1,x2,y1,y2,gray = Kensyutu(file,floor=100)
    img_w=In_white(file,x1,x2,y1,y2, Q, draw)
    print(img_w.shape)
    plt.imshow(img_w,"gray")
    plt.show()

こんな感じで出力してくれました。

うまく行っていれば、Qの値を白(0,0,0)に、drawを-1にして実行すればOKです。

問題点があります

うまく検出されない問題

ノイズがひどいと、よくわかんないとこが検出されたり、大きく検出されすぎたりすることがあります。 その場合はペイントで編集するのがベストです。画像を白でぬりたくって行えばうまく検出してくれます。若干手間なのが難点ですが、、、

あくまで基準は白紙の解答用紙

座標で管理している以上、ずれが生じるの一番の難点です。あまりにもずれていると、半分しか隠れていないなんてこともありました。座標+5くらいしてあげてもいいかもしれません。

解答を隠してしまう可能性

問題番号の下やその付近に解答があるばあい、当然それも隠れてしまいます。。。これについては諦めました。。。

いやいや、そもそも番号を区切ればいいだけの話では?

やっていて思ったのは、解答用紙に予め、解答欄の番号と解答を書く場所の間に縦線をいれておけばいいのかもしれません。しかし、それだと自動検出に支障が出そうです。一度、試してみようとは思っています。 そして、問題番号を含まないようにすればいいだけの話。わざわざPythonで処理しなくても、作りてに一工夫あれば解決しそうです。
今回は以上です。ありがとうございました。