マークリーダー（Fortranを使用しない最新版あり）

#環境
macOS Cataina
10.15.7

python 3.8.1
fortran 95

スキャナ
canon
ソフト
CaptureOnTouch
読み取り
グレースケール
200dpi
jpeg
3標準

![スクリーンショット 2021-06-08 13.12.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1101426/16c8d500-3eb9-a7b8-0be2-2eb44a91c7b7.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)


#必要なライブラリ
cv2

```
$ pip install cv2
```

#必要なもの
・記入済みマークシート用紙を含むディレクトリ（`student_sheet`）
・マーカー（`maker.jpg`）←スキャナで読み取った画像から作成
・本プログラム（`mark30.sh`）
・主に画像認証用（`mark_f30.py`）
・正答率の計算用（`rate_windows.f95`）
・空のディレクトリ（`answersheet`）
・結果出力用のディレクトリ（`answersheet_rate`）

これを同一ディレクトリに入れて実行

```
$ ./mark30.sh
```

すると，
結果出力用のディレクトリ（`answersheet_rate`）に
答案用紙が作成されます．

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1101426/ab5c740d-8790-1e6a-3eaf-6c99504ccafe.png)
答案用紙

#各プログラムソースコード

本プログラム

```mark30.sh
#!/bin/sh

for number in {1..9}
do
    echo "■■■■■■■■■■■■■■■■■■■■■■■■"
    echo "student_"00${number}".jpg"  
    python mark_f30.py 00${number}
done

for number in {10..45}
do
    echo "■■■■■■■■■■■■■■■■■■■■■■■■"
    echo "student_"0${number}".jpg"  
    python mark_f30.py 0${number}
done

#for number in {100..120}
#do
#    echo "■■■■■■■■■■■■■■■■■■■■■■■■"
#    echo "student_"${number}".jpg"  
#    python mark_f30.py ${number}
#done

echo "■■■■■■■■■■■■■■■■■■■■■■■■"
echo "■■■■■■■■■■■■■■■■■■■■■■■■"
echo "■■■■■■■■■■■■■■■■■■■■■■■■"
echo "全てのマークシートの読取完了"
ls answersheet > list.txt
gfortran -o rate rate_windows.f95
./rate
```
画像認証用プログラム

```mark_f30.py
import numpy as np
import cv2
#import pandas as pd
import sys

#print("##################")
#正解，配点記入欄#
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] #番号
eanswer = [6, 9, 3, 2, 2, 1, 2, 3, 5,  4,  2,  6,  6,  6,  5,  3,  5,  4,  4,  6,  2,  3,  6,  2,  1,  4,  4,  9,  6,  8] #正解
allo    = [2, 3, 3, 3, 2, 3, 3, 3, 4,  3,  2,  3,  3,  3,  3,  3,  3,  3,  3,  3,  3,  3,  3,  3,  3,  3,  3,  3,  3,  3] #配点
#score = 0
T = "1"
F = "0"
E = "20"#マークミスエラー用

####################################################################################
n_col = 11 # マークの列数
n_row = 35 # マークの行数
margin_top = 3 # 上余白行数
margin_bottom = 0 # 下余白行数
n_row_total = n_row + margin_top + margin_bottom # 行数 (マーク行  + 上余白  + 下余白 )
start = 5 #出席番号の分の列#百の位〜解答欄までの列の総計

#グレースケール (mode = 0)でファイルを読み込む
marker=cv2.imread('marker.jpg',0) 

#コマンドラインから引数#########
#例 >python mark.py 001#####
args = sys.argv
n = str(args[1])
##########################################

#スキャン画像を読み込む
img = cv2.imread('student_sheet/student_%s.jpg' %n, 0)

#マーカー検出
res = cv2.matchTemplate(img, marker, cv2.TM_CCOEFF_NORMED)
threshold = 0.5#閾値
loc = np.where( res >= threshold)
#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(255*300))#閾値
    
#ここから番号の読み取り#########
#組番号の読み取り
cl = np.where(Result[0]==True)[0]#+1
if len(cl)>1:
      print("ダブルマーク")
      print("読取失敗")
      sys.exit( )
elif len(cl)==1:
      cla = str(cl[0])
else:
      print("ノーマーク")
      print("読取失敗")
      sys.exit( )

#cla = str(cl[0])

#出席番号の読み取り
num = []
for y in range(1,3):
    re = np.where(Result[y]==True)[0]#+1
    if len(re)>1:
      print("ダブルマーク")
      print("読取失敗")
      sys.exit( )
    elif len(re)==1:
      num.append(int(re))
    else:
     print("ノーマーク")
     print("読取失敗")
     sys.exit( )
nu = str(num[0])+ str(num[1])
code = str(cla) + str(nu)
print("出席番号%s" % code)#ターミナルに表示

#出力ファイル作成#生徒に配るやつ##################################
with open('answersheet/%s.txt' % code,  'w') as f:#出力ファイル作成

#回答番号の読み取り
 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)#リスト化

#ファイルに出力##
# print("\n出席番号%s" % code, file=f)
# print("\n", file=f)
# print("番号", "正解",  "回答", "正誤", "配点", file=f)

 for nu, an, ea, al in zip(number, answer, eanswer, allo):
    if str(an) == str(20):
        print(str(nu).rjust(3), str(ea).rjust(3), E.rjust(3), F.rjust(3), str(al).rjust(3), file=f)
    elif str(an) == str(ea):
        print(str(nu).rjust(3), str(ea).rjust(3), str(an).rjust(3), T.rjust(3), str(al).rjust(3), file=f)
#        score = score + al
    else:
        print(str(nu).rjust(3), str(ea).rjust(3), str(an).rjust(3), F.rjust(3), str(al).rjust(3), file=f)
# print("\n合計 %d　点" % score, file=f)

f.close()

print("読取完了")
#おわり

```
正答率計算用

```rate_windows.f95
program main
  
  implicit none
  integer, parameter :: n = 1000
  integer i, j, nd, ndd, x(n,n), y(n,n), z(n,n), v(n,n), w(n,n), total(n), totals(n), k, totalm
  integer f0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10
  character :: a(n)*7, b*12, bb*17, c(n)*19, cc(n)*24
  character t*1, f*1, e*1
  real r(n), av, s, ss(n), ssm
  b = 'answersheet/'
  bb = 'answersheet_rate/'
  t = 'o'
  f = 'x'
  e = '?'

 !リストからファイル読み込み
  open(60,file='list.txt')
  do i = 1, n
     read(60,*,end=100) a(i)
     c(i) = b//a(i)
     cc(i)= bb//a(i)
  end do

100 close(60)
  nd = i - 1

 !初期値リセット  
  total = 0
  r = 0
  av = 0
  s = 0
  
  do i = 1, nd !ファイル(40人学級なら40列)
     open(20,file=c(i))
     do j = 1, n !紙(問題数が20なら20列)
        read(20,*,end=101) x(j,i), y(j,i), z(j,i), v(j,i), w(j,i)
     end do
101  close(20)
  end do
  
  ndd = j - 1


 !合計点の計算
  do i = 1, nd
     do j = 1, ndd
        if  (y(j,i) ==  z(j,i)) then
           total(i) = total(i) + w(j,i)
        else
        endif
     end do
  end do
  
 !平均点の計算
  do i = 1, nd
     av = av + total(i)
  end do
  av = av/nd

 !標準偏差の計算
  do i = 1, nd
     s = s + (total(i) - av)**2
  end do
  s = s/nd
  s = sqrt(s)
  
  !偏差値の計算
 do i = 1, nd 
    ss(i) = ((total(i) - av)/s)*10 + 50
 end do

 !正答率の計算  
  do j = 1, ndd
     do i = 1, nd
        r(j) = r(j) + v(j,i)
     end do
     r(j) = (r(j)/nd)*100 !正答率の計算
  end do

  !度数分布作成
  f0 = 0
  f1 = 0
  f2 = 0
  f3 = 0
  f4 = 0
  f5 = 0
  f6 = 0
  f7 = 0
  f8 = 0
  f9 = 0
  f10 = 0
  
  do  i = 1, nd
   if (total(i) >= 0 .and. total(i) < 10) then
        f0 = f0 + 1
     else if (total(i) >= 10 .and. total(i) < 20) then
        f1 = f1 + 1
     else if (total(i) >= 20 .and. total(i) < 30) then 
        f2 = f2 + 1
     else if (total(i) >= 30 .and. total(i) < 40) then
        f3 = f3 + 1
     else if (total(i) >= 40 .and. total(i) < 50) then 
        f4 = f4 + 1
     else if (total(i) >= 50 .and. total(i) < 60) then 
        f5 = f5 + 1
     else if (total(i) >= 60 .and. total(i) < 70) then 
        f6 = f6 + 1
     else if (total(i) >= 70 .and. total(i) < 80) then
        f7 = f7 + 1
     else if (total(i) >= 80 .and. total(i) < 90) then 
        f8 = f8 + 1
     else if (total(i) >= 90 .and. total(i) < 100) then 
        f9 = f9 + 1
     else if (total(i) == 100) then
        f10 = f10 + 1
     endif
  end do

 !結果出力
  do i = 1, nd
     open(30,file=cc(i))
     write(30,*)
     write(30,*) ' 出席番号 ', a(i)(1:3)
     write(30,*)
     write(30,*)
     write(30,*) '  番号', ' 正解', ' 回答', ' 正誤', ' 配点', ' 正答率'
     do j = 1, ndd
        if (z(j,i) == 20) then
           write(30,'(2i5,2a5,i6,f8.1)') x(j,i), y(j,i), e, e,  w(j,i), r(j)
        else
           if (v(j,i) == 1) then
              write(30,'(3i5,a5,i6,f8.1)') x(j,i), y(j,i), z(j,i), t,  w(j,i), r(j)
           else if (v(j,i) == 0) then
              write(30,'(3i5,a5,i6,f8.1)') x(j,i), y(j,i), z(j,i), f,  w(j,i), r(j)
           endif
        endif
     end do
     write(30,*)
     write(30,*)' 合計点', '  偏差値'
     write(30,'(i5,f8.1)') total(i), ss(i)
     write(30,*)'----------------------------------'
     write(30,*)' 平均点', '  標準偏差'
     write(30,'(f7.1,f6.1)') av, s
     write(30,*)
     write(30,*)' 度数分布'
     write(30,*)' 100', '  ', f10
     write(30,*)' 90~99', f9
     write(30,*)' 80~89', f8
     write(30,*)' 70~79', f7
     write(30,*)' 60~69', f6
     write(30,*)' 50~59', f5
     write(30,*)' 40~49', f4 
     write(30,*)' 30~39', f3
     write(30,*)' 20~29', f2
     write(30,*)' 10~19', f1
     write(30,*)' 0 ~ 9', f0
     close(30)
  end do



!模範解答
   i = 1
     open(40,file='answersheet_rate/model_answer.txt')
     write(40,*)
     write(40,*) ' 模範解答 '
     write(40,*)
     write(40,*)
     write(40,*) '  番号', ' 正解', ' 回答', ' 正誤', ' 配点', ' 正答率'
     do j = 1, ndd
              write(40,'(3i5,a5,i6,f8.1)') x(j,i), y(j,i), y(j,i), t,  w(j,i), r(j)
           end do
           totalm = 0
           do j = 1, ndd
              totalm = totalm + w(j,i)
           end do

             !偏差値の計算
           do i = 1, nd 
              ssm = ((totalm - av)/s)*10 + 50
           end do 
     write(40,*)
     write(40,*)' 合計点', '  偏差値'
     write(40,'(i5,f8.1)') totalm, ssm
     write(40,*)'----------------------------------'
     write(40,*)' 平均点', '  標準偏差'
     write(40,'(f7.1,f6.1)') av, s
     write(40,*)
     write(40,*)' 度数分布'
     write(40,*)' 100', '  ', f10
     write(40,*)' 90~99', f9
     write(40,*)' 80~89', f8
     write(40,*)' 70~79', f7
     write(40,*)' 60~69', f6
     write(40,*)' 50~59', f5
     write(40,*)' 40~49', f4 
     write(40,*)' 30~39', f3
     write(40,*)' 20~29', f2
     write(40,*)' 10~19', f1
     write(40,*)' 0 ~ 9', f0
     close(40)
  
endprogram main
```

参考
https://qiita.com/sbtseiji/items/6438ec2bf970d63817b8