0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

マークリーダー(pythonのみ)※観点別あり 高校定期テスト向け

Last updated at Posted at 2024-07-15

導入の経緯

観点別評価という面倒なものが導入されたので,観点別に配点を計算するようにプログラムを改良

環境

PCというかOS
macOS 14.4.1 ←動作確認済
(pythonやOpen CVが使えれば何でもいい)

python
python 3.8.1

スキャナー
canonの自動送り(中古で2,3万?)

スキャナーソフト
CaptureOnTouch
 設定
 ・グレースケール
 ・200dpi
 ・jpeg
 ・3標準

スクリーンショット 2022-12-13 20.44.27.png
詳細設定で連番にするのを忘れずに
スクリーンショット 2022-12-13 20.47.23.png

この名前の付け方がおかしいと
プログラムをまわしたとき,エラーになります.

その他細かい設定
スクリーンショット 2021-06-08 13.12.17.png

読み取ったやつ

student_9.jpg

必要なライブラリ

cv2
numpy
statistics
glob

もしなければ

$ pip install cv2

で適宜インストール

必要なもの

・記入済みマークシート用紙を含むディレクトリ(student
・マーカー(maker.jpg
 (スキャナで読み取ったマークシートの黒四角を切り抜いたもの)
・プログラム(mark35.py
・空のディレクトリ(result
スクリーンショット 2022-12-13 20.38.35.png
これを同一ディレクトリ(上の図の場合l2)に入れて実行

$ python mark35.py

エラーがなければ,
結果出力用のディレクトリ(result)に
答案用紙が作成される.
スクリーンショット 2024-07-15 17.01.48.png

あとはこれらをまとめてコピーして配布

プログラムソースコード

mark35.py
#ライブラリを取得
import numpy as np
import cv2
import statistics
import glob

#正解,配点記入欄
number  = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35] #番号
eanswer = [3, 7, 5, 4, 4, 6, 3, 6, 9,  3,  8,  3,  2,  3,  4,  4,  4,  6,  2,  2,  3,  6,  6,  2,  4,  7,  8,  1,  4,  2,  2,  2,  8,  6,  5] #正解
allo    = [3, 3, 3, 3, 3, 3, 3, 3, 3,  3,  3,  3,  3,  3,  3,  3,  3,  3,  3,  3,  3,  3,  3,  3,  3,  3,  2,  2,  2,  2,  2,  3,  3,  3,  3] #配点
kant    = [2, 2, 2, 1, 1, 1, 1, 1, 1,  1,  1,  1,  2,  2,  2,  1,  1,  1,  2,  2,  1,  2,  1,  2,  1,  2,  1,  2,  2,  1,  1,  1,  1,  2,  2] #観点1知識技能2思考判断
#観点合計計算
model_kan1 = []
model_kan2 = []
for al, ka in zip(allo, kant):
  if str(ka) == str(1):
   model_kan1.append(al)
  elif str(ka) == str(2):
   model_kan2.append(al)
model_total_kan1 = round(sum(model_kan1),1) #観点1の合計
model_total_kan2 = round(sum(model_kan2),1) #観点2の合計
  
#正解は1,不正解は2
T = "1"
F = "0"
E = "20" #マークミスエラー用

#画像認識の閾値
threshold1 = 0.5     #閾値
threshold2 = 255*500 #閾値

#マークシートの形式
n_col = 11 # マークの列数(左黒四角の左辺〜右黒四角の左辺)
n_row = 40 # マークの行数(マークする番号の開始〜終了までの行数)
margin_top = 3 # 上余白行数(上黒四角の上辺〜組番号までの行数)
margin_bottom = 0 # 下余白行数(下黒四角の上辺〜最後の問までの行数)
start = 5 #出席番号の分の列#百の位〜解答欄までの列の総計
n_row_total = n_row + margin_top + margin_bottom #行数(マーク行  + 上余白  + 下余白)
#グレースケール (mode = 0)でファイルを読み込む
marker=cv2.imread('marker.jpg',0)

#ここまでは自分で調節
########################################################################
#スキャナーで読み取ったシートのファイル数を取得
jpgcount = len(glob.glob1('student','*.jpg'))
print('読み込みファイル数',jpgcount)

matrix = [] #生徒x問題x各項目(正誤,正答率等)のリスト作成
studentnamelist  = [] #出席番号のリスト
studentnamelist3 = [] #数字のみのリスト

#######################################
#ここから各生徒の回答を読み込むループ
for k in range(jpgcount):
 print('####################')
 #スキャン画像を読み込む
 img = cv2.imread('student/student_%s.jpg' %str(k+1), 0)
 #マーカー検出
 res = cv2.matchTemplate(img, marker, cv2.TM_CCOEFF_NORMED)
 loc = np.where( res >= threshold1)
 #cv2.imwrite('resimg.jpg',img)

 #マークシート部分の切り抜き
 mark_area={}
 mark_area['top_x']= min(loc[1])
 mark_area['top_y']= min(loc[0])
 mark_area['bottom_x']= max(loc[1])
 mark_area['bottom_y']= max(loc[0])
 img = img[mark_area['top_y']:mark_area['bottom_y'],\
           mark_area['top_x']:mark_area['bottom_x']]
 #cv2.imwrite('kiri.jpg',img)

 #リサイズ
 #Answer = cv2.imread('kiri.jpg', 0)
 Answer_Resize = cv2.resize(img, (n_col*100, n_row_total*100))
 #cv2.imwrite('kiri.jpg', Answer_Resize)

 #ぼかし
 Answer_Blur = cv2.GaussianBlur(Answer_Resize, (5,5), 0)
 #cv2.imwrite('Answer_Blur.jpg',Answer_Blur)

 #2値化
 RetVal, Answer_Binarization = \
  cv2.threshold(Answer_Blur, 50, 255, cv2.THRESH_BINARY+cv2.THRESH_OTSU)
 #cv2.imwrite('Answer_Binarization.jpg',Answer_Binarization)

 #白黒反転
 Answer_Reverse = 255 - Answer_Binarization
 #cv2.imwrite('Answer_Reverse.jpg',Answer_Reverse)

 #行と列に分解して各場所の階調の合計値を計算
 Result = []
 for row in range(margin_top+1, margin_top+1+n_row):
  tmp_Answer = Answer_Reverse [(row-1)*100:row*100,]#
  cv2.imwrite("PNG/Answer_Tmp%d.png" % row, tmp_Answer)#
  tmp_Answer = Answer_Reverse [(row-1)*100:row*100,]
  Area_sum = []  
  for col in range(n_col):
   Area_sum.append(np.sum(tmp_Answer[:,col*100:(col+1)*100]))
  Result.append(Area_sum > np.median(threshold2))#閾値
    
 #ここから番号の読み取り#########
 #組番号の読み取り
 cl = np.where(Result[0]==True)[0]#+1
 if len(cl)>1:
  print("ダブルマーク")
  print("組番号読取失敗x")
  continue
 elif len(cl)==1:
  cla = str(cl[0])
 else:
  print("ノーマーク")
  print("組番号読取失敗x")
  continue

 #出席番号の読み取り
 num = []
 #一桁目読み取り
 re = np.where(Result[1]==True)[0]#+1
 if len(re)>1:
  print("ダブルマーク")
  print("出席番号読取失敗x")
  continue
 elif len(re)==1:
  num.append(int(re))
 else:
  print("ノーマーク")
  print("出席番号読取失敗x")
  continue   
 #二桁目読み取り
 re = np.where(Result[2]==True)[0]#+1
 if len(re)>1:
  print("ダブルマーク")
  print("出席番号読取失敗x")
  continue
 elif len(re)==1:
  num.append(int(re))
 else:
  print("ノーマーク")
  print("出席番号読取失敗x")
  continue
      
 #出席番号読み取り成功後
 nu = str(num[0])+ str(num[1])
 code = str(cla) + str(nu)

 #ターミナルに表示
 print('出席番号%s' % code)

 #各生徒の空の配列をつくる
 exec_hako = 'student_' + str(code) + '=[]'
 exec(exec_hako)

 #出力ファイル作成(生徒に配るやつ)##################################
 #回答番号の読み取り
 answer = []
 for x in range(start,len(Result)):
  res = np.where(Result[x]==True)[0]#+1
  if len(res)>1:
   res = 20
   print("問%sダブルマーク" % (x-start+1))
   answer.append(res)
  elif len(res)==1:
   answer.append(int(res[0]))#リスト化
  else:
   res = 20
   print("問%sノーマーク" % (x-start+1))
   answer.append(res)#リスト化

 s_answer=[]
 r_or_w = []
 kant1 = []
 kant2 = []
 for nu, an, ea, al, ka in zip(number, answer, eanswer, allo, kant):
  if str(an) == str(20):
   s_answer.append(int(E))
   r_or_w.append(int(F))
   kant1.append(int(F))
   kant2.append(int(F))   
  elif str(an) == str(ea):
   s_answer.append(an)
   r_or_w.append(int(T))
   if str(ka) == str(1):
    kant1.append(int(T))#
    kant2.append(int(F))#
   else:
    kant1.append(int(F))#
    kant2.append(int(T))#
  else:
   s_answer.append(an)
   r_or_w.append(int(F))
   kant1.append(int(F))
   kant2.append(int(F))
   
 exec_append = 'student_' + str(code) + '.append(number)'#matrix0
 exec(exec_append)
 exec_append = 'student_' + str(code) + '.append(eanswer)'#1
 exec(exec_append)
 exec_append = 'student_' + str(code) + '.append(s_answer)'#2
 exec(exec_append)
 exec_append = 'student_' + str(code) + '.append(r_or_w)'#3
 exec(exec_append)
 exec_append = 'student_' + str(code) + '.append(allo)'#4
 exec(exec_append)
#観点別
 exec_append = 'student_' + str(code) + '.append(kant1)'#matrix5
 exec(exec_append)
 exec_append = 'student_' + str(code) + '.append(kant2)'#matrix6
 exec(exec_append)
#観点別合計
 exec_append = 'student_' + str(code) + '.append(kant)'#matrix7
 exec(exec_append)
 
 print("読取完了o")

 #リスト作成
 studentnamelist.append('%s.txt' % code)
 studentnamelist3.append(code)
 #おわり

#正答率などの計算#####################
#各生徒の問題x各項目(二次元)を連結して生徒x問題x各項目(三次元)
for i in range(len(studentnamelist)):
 exec_append = 'matrix.append(student_' + str(studentnamelist3[i]) + ')'
 exec(exec_append)

#合計点
total=[]
totalkan1=[]
totalkan2=[]
#生徒方向のループ
for i in range(len(studentnamelist)):
 #配列の作成
 exec_append = 'qu' + str(i+1) + '=[]'
 exec(exec_append)
 #配列の作成観点1
 exec_append = 'k1' + str(i+1) + '=[]'
 exec(exec_append)
 #配列の作成観点2
 exec_append = 'k2' + str(i+1) + '=[]'
 exec(exec_append)

 #問方向のループ
 for j in range(len(matrix[0][0])):
  exec_append = 'qu' + str(i+1) + '.append((matrix['+ str(i) + '][3][' + str(j) + '])\
  *(matrix[' + str(i) + '][4][' + str(j) + ']))'
  exec(exec_append)
  #観点1
  exec_append = 'k1' + str(i+1) + '.append((matrix['+ str(i) + '][5][' + str(j) + '])\
  *(matrix[' + str(i) + '][4][' + str(j) + ']))'
  exec(exec_append)
  #観点2
  exec_append = 'k2' + str(i+1) + '.append((matrix['+ str(i) + '][6][' + str(j) + '])\
  *(matrix[' + str(i) + '][4][' + str(j) + ']))'
  exec(exec_append)
#合計計算
 exec_append = 'total.append(sum(qu' + str(i+1) + '))'
 exec(exec_append)
#観点合計計算
 exec_append = 'totalkan1.append(sum(k1' + str(i+1) + '))'
 exec(exec_append)
 exec_append = 'totalkan2.append(sum(k2' + str(i+1) + '))'
 exec(exec_append)
 
#平均点
av = sum(total)/len(studentnamelist)
#観点1平均
av_kan1 = sum(totalkan1)/len(studentnamelist)
#観点2平均
av_kan2 = sum(totalkan2)/len(studentnamelist)
#標準偏差
std = statistics.stdev(total)
#中央値
med = statistics.median(total)
#偏差値
dv = []
for i in range(len(studentnamelist)):
 dvi = round(50 + ((total[i]-av)/std)*10,1)
 dv.append(dvi)

#度数分布
f00_04 = 0
f05_09 = 0
f10_14 = 0
f15_19 = 0
f20_24 = 0
f25_29 = 0
f30_34 = 0
f35_39 = 0
f40_44 = 0
f45_49 = 0
f50_54 = 0
f55_59 = 0
f60_64 = 0
f65_69 = 0
f70_74 = 0
f75_79 = 0
f80_84 = 0
f85_89 = 0
f90_94 = 0
f95_99 = 0
f100   = 0

for  i in range(len(total)):
 if total[i] >= 0 and total[i] < 5:
  f00_04 = f00_04 + 1
 elif total[i] >= 5 and total[i] < 10:
  f05_09 = f05_09 + 1
 elif total[i] >= 10 and total[i] < 15: 
   f10_14 = f10_14 + 1
 elif total[i] >= 15 and total[i] < 20:
  f15_19 = f15_19 + 1
 elif total[i] >= 20 and total[i] < 25: 
  f20_24 = f20_24 + 1
 elif total[i] >= 25 and total[i] < 30: 
  f25_29 = f25_29 + 1
 elif total[i] >= 30 and total[i] < 35:
  f30_34 = f30_34 + 1
 elif total[i] >= 35 and total[i] < 40:
  f35_39 = f35_39 + 1
 elif total[i] >= 40 and total[i] < 45:
  f40_44 = f40_44 + 1
 elif total[i] >= 45 and total[i] < 50:
  f45_49 = f45_49 + 1
 elif total[i] >= 50 and total[i] < 55:
  f50_54 = f50_54 + 1
 elif total[i] >= 55 and total[i] < 60: 
  f55_59 = f55_59 + 1
 elif total[i] >= 60 and total[i] < 65: 
  f60_64 = f60_64 + 1
 elif total[i] >= 65 and total[i] < 70:
  f65_69 = f65_69 + 1
 elif total[i] >= 70 and total[i] < 75:
  f70_74 = f70_74 + 1
 elif total[i] >= 75 and total[i] < 80:
  f75_79 = f75_79 + 1
 elif total[i] >= 80 and total[i] < 85:
  f80_84 = f80_84 + 1
 elif total[i] >= 85 and total[i] < 90:
  f85_89 = f85_89 + 1
 elif total[i] >= 90 and total[i] < 95:
  f90_94 = f90_94 + 1
 elif total[i] >= 95 and total[i] < 100:
  f95_99 = f95_99 + 1
 elif total[i] == 100:
  f100   = f100 + 1

#正答率の計算
#matrixからの切り出しright and wrong(raw)
for i in range(len(matrix[0][0])):
 exec_append = 'raw' + str(i+1) + '=[]'
 exec(exec_append)

#生徒方向へのループ
for i in range(len(studentnamelist)):
 #問題方向へのループ
 for j in range(len(matrix[0][0])):
  exec_append2 = 'raw' + str(j+1) + '.append(matrix[' + str(i) + \
  '][3][' + str(j) + '])'#[3]は正答率を書き込む列によって変更
  exec(exec_append2)

#Correct answer rate(car)
car = []
for i in range(len(matrix[0][0])):
 exec_append  = 'car.append(sum(raw' + str(i+1) + ')/len(raw' + str(i+1) + ')*100)'#正答率
 exec(exec_append)
#少数第二位まで四捨五入
car = [round(car[n], 1) for n in range(len(car))] 

#正答率をmatrixの列の端っこにくっつける
#つまり正答率はmatrix8
for row in matrix:
     row.append(car)

#正誤をoとxに置き換え
for i in range(len(studentnamelist)):
 matrix[i][3] = ['o' if j ==  1  else j for j in matrix[i][3]]
 matrix[i][3] = ['x' if j ==  0  else j for j in matrix[i][3]]
 matrix[i][2] = ['?' if j ==  20 else j for j in matrix[i][2]]
 #観点別の方も置き換え
 matrix[i][7] = ['' if j ==  1 else j for j in matrix[i][7]] 
 matrix[i][7] = ['' if j ==  2 else j for j in matrix[i][7]]
#平均,標準偏差を四捨五入
av      = round(av,1)
av_kan1 = round(av_kan1,1)
av_kan2 = round(av_kan2,1)
std     = round(std,1)

for i in range(len(studentnamelist)):
 temp = 0
 temp = 'result/' + str(studentnamelist[i])
 with open(temp, 'w') as f:
  print('出席番号%s' % (studentnamelist[i])[0:3], file=f)
  print('\n番号'.rjust(2),'正解'.rjust(2),'回答'.rjust(2),'正誤'.rjust(2),'配点'.rjust(2),'正答率'.rjust(2),'観点'.rjust(2),file=f)
  for j in range(len(matrix[0][0])):
   print(str(matrix[i][0][j]).rjust(4),str(matrix[i][1][j]).rjust(4),\
         str(matrix[i][2][j]).rjust(4),str(matrix[i][3][j]).rjust(4),\
         str(matrix[i][4][j]).rjust(4),' ',str(matrix[i][8][j]).rjust(4),str(matrix[i][7][j]).rjust(4),file=f)
  print('\n合計点',str(total[i]).rjust(3),'  偏差値',str(dv[i]).rjust(3),file=f)
  print('(基',str(totalkan1[i]).rjust(3) ,' 応',str(totalkan2[i]).rjust(3),'',file=f)
  print('------------------------------',file=f)
  print('平均点',str(av).rjust(3),'  標準偏差',str(std).rjust(3),file=f)
  print('(基',str(av_kan1).rjust(3),' 応',str(av_kan2).rjust(3),'',file=f) 
  print('\n度数分布',file=f)
  print('100        ',str(f100).rjust(2),  ' |             '       ,file=f)
  print('95 ~ 99    ',str(f95_99).rjust(2),' |  45 ~ 49    ',str(f45_49).rjust(2),file=f)
  print('90 ~ 94    ',str(f90_94).rjust(2),' |  40 ~ 44    ',str(f40_44).rjust(2),file=f)
  print('85 ~ 89    ',str(f85_89).rjust(2),' |  35 ~ 39    ',str(f35_39).rjust(2),file=f)
  print('80 ~ 84    ',str(f80_84).rjust(2),' |  30 ~ 34    ',str(f30_34).rjust(2),file=f)
  print('75 ~ 79    ',str(f75_79).rjust(2),' |  25 ~ 29    ',str(f25_29).rjust(2),file=f)
  print('70 ~ 74    ',str(f70_74).rjust(2),' |  20 ~ 24    ',str(f20_24).rjust(2),file=f)
  print('65 ~ 99    ',str(f65_69).rjust(2),' |  15 ~ 19    ',str(f15_19).rjust(2),file=f)
  print('60 ~ 64    ',str(f60_64).rjust(2),' |  10 ~ 14    ',str(f10_14).rjust(2),file=f)
  print('55 ~ 59    ',str(f55_59).rjust(2),' |   5 ~  9    ',str(f05_09).rjust(2),file=f)
  print('50 ~ 54    ',str(f50_54).rjust(2),' |   0 ~  4    ',str(f00_04).rjust(2),file=f)

#模範解答作成
model_dv         = round(50 + ((sum(matrix[0][4])-av)/std)*10,1)#偏差値
model_total      = round(sum(matrix[0][4]),1)

with open('result/model_answer.txt', 'w') as f:
  print('模範解答', file=f)
  print('\n番号'.rjust(2),'正解'.rjust(2),'回答'.rjust(2),'正誤'.rjust(2),'配点'.rjust(2),'正答率'.rjust(2),'観点'.rjust(2),file=f)
  for j in range(len(matrix[0][0])):
   print(str(matrix[i][0][j]).rjust(4),str(matrix[i][1][j]).rjust(4),str(matrix[i][1][j]).rjust(4),'o'.rjust(4),\
         str(matrix[i][4][j]).rjust(4),' ',str(matrix[i][8][j]).rjust(4),str(matrix[i][7][j]).rjust(4),file=f)
  print('\n合計点',str(model_total).rjust(3),'  偏差値',str(model_dv).rjust(3),file=f)
  print('(基',str(model_total_kan1).rjust(3) ,' 応',str(model_total_kan2).rjust(3),'',file=f)
  print('------------------------------',file=f)
  print('平均点',str(av).rjust(3),'  標準偏差',str(std).rjust(3),file=f)
  print('(基',str(av_kan1).rjust(3),' 応',str(av_kan2).rjust(3),'',file=f) 
  print('\n度数分布',file=f)
  print('100        ',str(f100).rjust(2),  ' |             '       ,file=f)
  print('95 ~ 99    ',str(f95_99).rjust(2),' |  45 ~ 49    ',str(f45_49).rjust(2),file=f)
  print('90 ~ 94    ',str(f90_94).rjust(2),' |  40 ~ 44    ',str(f40_44).rjust(2),file=f)
  print('85 ~ 89    ',str(f85_89).rjust(2),' |  35 ~ 39    ',str(f35_39).rjust(2),file=f)
  print('80 ~ 84    ',str(f80_84).rjust(2),' |  30 ~ 34    ',str(f30_34).rjust(2),file=f)
  print('75 ~ 79    ',str(f75_79).rjust(2),' |  25 ~ 29    ',str(f25_29).rjust(2),file=f)
  print('70 ~ 74    ',str(f70_74).rjust(2),' |  20 ~ 24    ',str(f20_24).rjust(2),file=f)
  print('65 ~ 99    ',str(f65_69).rjust(2),' |  15 ~ 19    ',str(f15_19).rjust(2),file=f)
  print('60 ~ 64    ',str(f60_64).rjust(2),' |  10 ~ 14    ',str(f10_14).rjust(2),file=f)
  print('55 ~ 59    ',str(f55_59).rjust(2),' |   5 ~  9    ',str(f05_09).rjust(2),file=f)
  print('50 ~ 54    ',str(f50_54).rjust(2),' |   0 ~  4    ',str(f00_04).rjust(2),file=f)
print('####################')
print('読み込みファイル数',jpgcount)
print('正常に読み込めた人数',len(studentnamelist))
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?