マークリーダー（pythonのみ）

# 環境
<b>PCというかOS</b>
macOS Cataina 10.15.7
(pythonやOpen CVが使えれば何でもいい)

<b>python</b>
python 3.8.1

<b>スキャナー</b>
canonの自動送り(中古で2,3万？)

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

![スクリーンショット 2022-12-13 20.44.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1101426/78db3c9c-29b4-1051-674f-fcd4d50b40ab.png)
詳細設定で連番にするのを忘れずに
![スクリーンショット 2022-12-13 20.47.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1101426/eff03908-9c95-aee2-a6a6-04ae699b98e9.png)

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

その他細かい設定
![スクリーンショット 2021-06-08 13.12.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1101426/b3ee608a-52c5-f819-18e4-0f7ca6995fc4.png)

# 読み取ったやつ
![student_9.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1101426/9f05e753-9252-1f32-bde0-ea81c34552c8.jpeg)




# 必要なライブラリ
cv2
numpy
statistics
glob

もしなければ
```
$ pip install cv2
```
で適宜インストール

# 必要なもの
・記入済みマークシート用紙を含むディレクトリ（`student`）
・マーカー（`maker.jpg`）
　(スキャナで読み取ったマークシートの黒四角を切り抜いたもの)
・プログラム（`mark35.py`）
・空のディレクトリ（`result`）
![スクリーンショット 2022-12-13 20.38.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1101426/e1b15986-2a8e-2cf6-b9e0-188740060e0f.png)
これを同一ディレクトリ（上の図の場合`l1`）に入れて実行
```
$ python mark35.py
```
エラーがなければ，
結果出力用のディレクトリ（`result`）に
答案用紙が作成される．
![スクリーンショット 2022-12-13 20.51.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1101426/453759d9-7848-246e-3aa3-e06acec8e00d.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 = [8, 4, 7, 2, 1, 5, 3, 2, 2,  7,  5,  2,  3,  4,  4,  4,  1,  6,  4,  4,  4,  1,  2,  1,  1,  1,  9,  2,  4,  3,  2,  1,  1,  1,  1] #正解
allo    = [3, 4, 3, 2, 2, 2, 4, 3, 2,  2,  3,  2,  3,  3,  3,  2,  3,  3,  3,  3,  3,  2,  3,  3,  3,  3,  3,  3,  3,  3,  3,  3,  4,  3,  3] #配点
#正解は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 = []
 for nu, an, ea, al in zip(number, answer, eanswer, allo):
  if str(an) == str(20):
   s_answer.append(int(E))
   r_or_w.append(int(F))         
  elif str(an) == str(ea):
   s_answer.append(an)
   r_or_w.append(int(T))  
  else:
   s_answer.append(an)
   r_or_w.append(int(F))  

 exec_append = 'student_' + str(code) + '.append(number)'
 exec(exec_append)
 exec_append = 'student_' + str(code) + '.append(eanswer)'
 exec(exec_append)
 exec_append = 'student_' + str(code) + '.append(s_answer)'
 exec(exec_append)
 exec_append = 'student_' + str(code) + '.append(r_or_w)'
 exec(exec_append)
 exec_append = 'student_' + str(code) + '.append(allo)'
 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=[]
#生徒方向のループ
for i in range(len(studentnamelist)):
 #配列の作成
 exec_append = 'qu' + 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)
 exec_append = 'total.append(sum(qu' + str(i+1) + '))'
 exec(exec_append)
 
#平均点
av = sum(total)/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の列の端っこにくっつける     
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]]

#平均，標準偏差を四捨五入
av=round(av,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(' ',file=f)
  print('\n番号'.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(3),str(matrix[i][1][j]).rjust(3),\
         str(matrix[i][2][j]).rjust(3),str(matrix[i][3][j]).rjust(3),\
         str(matrix[i][4][j]).rjust(3),' ',str(matrix[i][5][j]).rjust(4),file=f)
#  print('\n',file=f)
#  print(' ',file=f)
  print('\n合計点',str(total[i]).rjust(3),'　　偏差値',str(dv[i]).rjust(3),file=f)  
  print('------------------------------',file=f)
  print('平均点',str(av),'  標準偏差',str(std),file=f)
#  print(' ',file=f)
  print('\n度数分布',file=f)
  print('100        ',f100,  ' |             '       ,file=f)
  print('95 ~ 99    ',f95_99,' |  45 ~ 49    ',f45_49,file=f)
  print('90 ~ 94    ',f90_94,' |  40 ~ 44    ',f40_44,file=f)
  print('85 ~ 89    ',f85_89,' |  35 ~ 39    ',f35_39,file=f)
  print('80 ~ 84    ',f80_84,' |  30 ~ 34    ',f30_34,file=f)
  print('75 ~ 79    ',f75_79,' |  25 ~ 29    ',f25_29,file=f)
  print('70 ~ 74    ',f70_74,' |  20 ~ 24    ',f20_24,file=f)
  print('65 ~ 99    ',f65_69,' |  15 ~ 19    ',f15_19,file=f)
  print('60 ~ 64    ',f60_64,' |  10 ~ 14    ',f10_14,file=f)
  print('55 ~ 59    ',f55_59,' |   5 ~  9    ',f05_09,file=f)
  print('50 ~ 54    ',f50_54,' |   0 ~  4    ',f00_04,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),file=f)
  for j in range(len(matrix[0][0])):
   print(str(matrix[i][0][j]).rjust(3),str(matrix[i][1][j]).rjust(3),str(matrix[i][1][j]).rjust(3),'o'.rjust(3),\
         str(matrix[i][4][j]).rjust(3),' ',str(matrix[i][5][j]).rjust(4),file=f)
#  print('\n',file=f)
#  print(' ',file=f)
  print('\n合計点',str(model_total).rjust(3),'　　偏差値',str(model_dv).rjust(3),file=f)  
  print('------------------------------',file=f)
  print('平均点',str(av),'  標準偏差',str(std),file=f)
#  print(' ',file=f)
  print('\n度数分布',file=f)
  print('100        ',f100,  ' |             '       ,file=f)
  print('95 ~ 99    ',f95_99,' |  45 ~ 49    ',f45_49,file=f)
  print('90 ~ 94    ',f90_94,' |  40 ~ 44    ',f40_44,file=f)
  print('85 ~ 89    ',f85_89,' |  35 ~ 39    ',f35_39,file=f)
  print('80 ~ 84    ',f80_84,' |  30 ~ 34    ',f30_34,file=f)
  print('75 ~ 79    ',f75_79,' |  25 ~ 29    ',f25_29,file=f)
  print('70 ~ 74    ',f70_74,' |  20 ~ 24    ',f20_24,file=f)
  print('65 ~ 99    ',f65_69,' |  15 ~ 19    ',f15_19,file=f)
  print('60 ~ 64    ',f60_64,' |  10 ~ 14    ',f10_14,file=f)
  print('55 ~ 59    ',f55_59,' |   5 ~  9    ',f05_09,file=f)
  print('50 ~ 54    ',f50_54,' |   0 ~  4    ',f00_04,file=f)
print('####################')
print('読み込みファイル数',jpgcount)
print('正常に読み込めた人数',len(studentnamelist))