LoginSignup
4

More than 3 years have passed since last update.

posted at

updated at

Organization

試験の電子採点を画像認識でサポートした話

ゆるゆる新卒1年生 Advent Calendar 2019、23日目へようこそ。
ゆるいにも程がある新卒1年生、 @NamedPython です:joy:

今日は僕の隠し特技である OpenCVによる画像認識 についてです。

画像認識について書くことはあらかじめ決めていましたが、新しく何かネタを見つけるのもきつかったので、学校の授業で頑張って書いたのにどこにも供養してなかった 回答用紙の領域抽出 をした記録をここに残します:pencil2:

何を作ろうとしたのさ

授業と言いましたが、授業名は プログラミング応用 で、普段授業で学んでいること・学生の持つ技術を絡めて、ソフトウェア開発による問題解決をチームで取り組んでみましょうねというものです。

じゃあ、問題解決のテーマは何かというと、

先生が行なっている試験の電子採点を楽にしてくれ:pray:

というものでした。そもそも電子採点している時点でかなりナウでヤングなんですが要件をまとめると

  • 学生の試験回答をスキャンしてiPadに取り込んでいる(ナウい)
  • 回答用紙は解答欄を用意しているわけではなく、自由記述
  • 上記のこともあり回答の位置が生徒によってバラバラ
  • なので、取り込んだ画像の回答の領域をApple Pencilで色分けして囲んでいる(ナウい)
  • 同じ問題(同じ色分け)を一気に採点したい
  • 君たち、なんとかしてくれ:pray:

って感じでした。

チーム

実はこの授業の一年前に、

というそこそこ最強のチームを組んで大批判を浴びました。その影響で、チームを組むメンツに制限がかかりました:frowning2:

それでも、先述の @Taillook と、もう一人仲の良いクラスメイトを引っ張って3人のチームを組みました。

ソリューション

先述のチーム組みで、iOS書けるやつを引っ張ってきました。

せっかくなので、何から何までiOSに乗っける以下のような構成のシステムを提案しました(最終発表のプレゼンから抽出)。 iOS周りの実装をありがとう、@Taillook

image.png

この図はKeynoteでゴリゴリ頑張りました。フォントはM+と、筑紫B丸ゴシック

認識概要

  • 色分けする運用が既にあるので、彩度のある部分のみを抽出すれば良さそう
    • BGR空間からHSV空間に変換して、S(彩度のチャネル)のみ抽出
  • 二値化
    • Sチャネル抽出の時点でそこそこ二値化されているため、単純な判別分析法でOK
  • 領域をそれぞれ切り出す
    • findContoursによる辺検出
    • 様々な辺が検出されるため、cv2.RETR_TREEによる辺の構造化
      • 内側にさらに辺があるもの OR 面積が一定以下 をスキップ
  • その領域がどの色で塗られているかを判別する
    • 抽出された辺の斜め左上(x - 1, y - 1)のピクセルの色を20点ほど抽出
    • 平均をとって、その辺の色とする

対象画像

当時は動作検証用の実物の回答用紙をもらいましたが、ここで持ってくるには権利周りがグレーな予感がしたので、Pages職人 の力を使ってサンプルを生成しました。

image.png

実装

さて、もうそろそろ退屈なのでドーンとソースコードを。
FROM jjanzic/docker-python3-opencvDockerfileを用意してどこでも開発できるように用意しました。最終的に画像認識部分は僕がフルで開発したのでオーバーキルでしたが。

Dockerの中身は以下。

  • Python 3.7.0
  • OpenCV 3.4.1
source.py
import os
import cv2
import numpy as np

DIR = os.path.dirname(os.path.abspath(__file__))

image = cv2.imread(f'{DIR}/image/sample_answer_sheet.png')
if image is None:
    print("File not found.")
    exit()


def write_out(img, name):
    cv2.imwrite(f'{DIR}/image/result/{name}.png', img)


def extract_inside(contours, hierarchy, debug=False):
    extracted = []
    contours_drawed = image.copy()
    if debug:
        print(f'len: {len(contours)}')
        print(hierarchy)

    for index in range(len(contours)):
        if hierarchy[0, index, 2] != -1 or cv2.contourArea(contours[index]) < 8000:
            continue
        extracted.append(contours[index])

        if debug:
            cv2.drawContours(contours_drawed, contours, index, (255, 0, 255), 10)

        print(f'{index} : {cv2.contourArea(contours[index])}')

    if debug:
        write_out(contours_drawed, 'out_contours')

    return extracted


width, height = image.shape[:2]

extracted = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)[:, :, 1]

write_out(extracted, 'after_cvt')

ret, thrshld_ex = cv2.threshold(extracted, 0, 255, cv2.THRESH_OTSU)

write_out(thrshld_ex, 'thrshld')

kernel = np.ones((3, 1), np.uint8)
thrshld_ex = cv2.morphologyEx(thrshld_ex, cv2.MORPH_OPEN, kernel)

write_out(thrshld_ex, 'thrshld_ex')

_, contours, hierarchy = cv2.findContours(thrshld_ex, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)

inside_contours = extract_inside(contours, hierarchy, True)

print(f'extracted {len(inside_contours)} countours')

clr_img_cp = image.copy()
for circle in inside_contours:
    colors = []
    av_color = 0
    M = cv2.moments(circle)
    center_of_circle = (int(M['m10']/M['m00']), int(M['m01']/M['m00']))
    cnt = 0
    for approx in circle:
        if cnt == 20:
            break
        cv2.circle(clr_img_cp, (approx[0, 0] - 1, approx[0, 1] - 1), 1, (255, 0, 255), 1)
        colors.append(image[approx[0, 1] - 1, approx[0, 0] - 1])
        cnt += 1
    cav = np.mean(colors, axis=0)
    cv2.circle(clr_img_cp, center_of_circle, 100, cav, 20)

write_out(clr_img_cp, 'out')

先ほどの認識概要に書いていたものをソースコードに落とすとこんな感じです。こんな感じでPythonでプロトタイピングしてから Objective-C に移植して @Taillook にSwiftとブリッヂしてもらいました。

結果

  • HSV 色空間への変換をして S のみ抽出
  • 判別分析法 による二値化
  • 辺検出
  • 辺の色を抽出

の4工程それぞれの結果を載せます。

HSV 色空間への変換をして S のみ抽出

extract_s_of_hsv.py
cv2.cvtColor(image, cv2.COLOR_BGR2HSV)[:, :, 1] # 0: H, 1: S, 2: V

image.png

もうこの時点でほぼ二値化なんですよね。すぎょい。

判別分析法 による二値化

cv2.THRESH_OTSUを指定するだけ。イージー。

thresold_with_otsu.py
cv2.threshold(extracted, 0, 255, cv2.THRESH_OTSU)

image.png

少しノイズが入る場合は、このあとmorphologyEXをかけてノイズ除去をするとよきです。

辺検出

もうもはやOpenCVがすごい。findContoursがすごい。論文を一応読んだんですが、辺の構造情報まで認識するとか変態的すぎる。

find_contours_with_structure.py
_, contours, hierarchy = cv2.findContours(thrshld_ex, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)

hierarchyに、contours内のどのindexの辺がのindexの辺の横にあるとか、内包してるとかを格納しています。すぎょい。

image.png

辺を点の集合で表し、紫色で描画しています。

辺の色を抽出 + 描画

ここはゴリ押しです。色の平均をこんなにも簡単に取れる numpy に感謝。

sampling_color_and_averaging_and_plotting.py
clr_img_cp = image.copy()
for circle in inside_contours:
    colors = []
    av_color = 0
    M = cv2.moments(circle)
    center_of_circle = (int(M['m10']/M['m00']), int(M['m01']/M['m00']))
    cnt = 0
    for approx in circle:
        if cnt == 20:
            break
        cv2.circle(clr_img_cp, (approx[0, 0] - 1, approx[0, 1] - 1), 1, (255, 0, 255), 1)
        colors.append(image[approx[0, 1] - 1, approx[0, 0] - 1])
        cnt += 1
    cav = np.mean(colors, axis=0)
    cv2.circle(clr_img_cp, center_of_circle, 100, cav, 20)

image.png

図形の中心を割り出し、そこに平均をとった色で円を描画しています。
(実はこの画像よく見ると、20サンプルの対象ピクセルに紫色がついています。)
だいたい辺と同じ色で良い感じ。

まとめ

ということで、学生時代に組んだ画像処理のプログラムをここで供養しました :coffin:

僕からすれば振り返りだったのですが、これを読んでPython + OpenCV の可能性を感じていただければ幸いです。

ただ一つ注意点として、OpenCV は理論を知っていなくても多くの事例から学んで使えてしまいますが、やはり問題解決に対する手段適用の最短経路を導くには理論が先決な気がしているので、情報工学は大事です。僕は軽くすっぽかしたためぎりぎりの理論しかありません。

ただ以下は確実に言えるでしょう。

  • OpenCV はすごい
  • Python + OpenCV はやりやすい
    • numpy との相性がいい

ということです。実はこの記事を書く前にRustによるOpenCVを試したりもしたんですが、Python + OpenCVの使い心地と比べてしまうとイマイチでした。まぁRustの実装に慣れていない僕が言うので参考にはならないかもしれませんが:yum:

Pythonだし遅いんでしょー?って話もありそうですが、描画を除くとなんと 0.3 [sec] ほど。
ただの C-binding だしね。すげえや。
僕が開発に携わっている自転車通販サイト cyma -サイマ-でも、なんだか画像認識をやる気運が上がってきたので、力を発揮していきたい。

@shimura_atsushiAteam cyma Advent Calendar 2019 で2記事書いているので興味がある方はどうぞ。

おわりに

ゆるゆる新卒1年生 Advent Calendar 2019、23日目いかがでしたか?

24日目は、バックエンドもわかるつよつよフロントエンドエンジニア @cheez921 のターンです。デュエル、スタンバイ!

よいクリスマス、よい年末、よい年始、よい画像認識を。

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
What you can do with signing up
4