ゆるゆる新卒1年生 Advent Calendar 2019、23日目へようこそ。
ゆるいにも程がある新卒1年生、 [@NamedPython] (https://qiita.com/NamedPython) です
今日は僕の隠し特技である OpenCVによる画像認識 についてです。
画像認識について書くことはあらかじめ決めていましたが、新しく何かネタを見つけるのもきつかったので、学校の授業で頑張って書いたのにどこにも供養してなかった 回答用紙の領域抽出 をした記録をここに残します
何を作ろうとしたのさ
授業と言いましたが、授業名は プログラミング応用 で、普段授業で学んでいること・学生の持つ技術を絡めて、ソフトウェア開発による問題解決をチームで取り組んでみましょうねというものです。
じゃあ、問題解決のテーマは何かというと、
先生が行なっている試験の電子採点を楽にしてくれ
というものでした。そもそも電子採点している時点でかなりナウでヤングなんですが要件をまとめると
- 学生の試験回答をスキャンしてiPadに取り込んでいる(ナウい)
- 回答用紙は解答欄を用意しているわけではなく、自由記述
- 上記のこともあり回答の位置が生徒によってバラバラ
- なので、取り込んだ画像の回答の領域をApple Pencilで色分けして囲んでいる(ナウい)
- 同じ問題(同じ色分け)を一気に採点したい
- 君たち、なんとかしてくれ
って感じでした。
チーム
実はこの授業の一年前に、
- バックエンドAPI、Webフロントの @NamedPython
- iOSの @Taillook
- Androidの @youta1119
- PythonとRasPi制御、自然言語処理の @Kyoskk
というそこそこ最強のチームを組んで大批判を浴びました。その影響で、チームを組むメンツに制限がかかりました
それでも、先述の @Taillook と、もう一人仲の良いクラスメイトを引っ張って3人のチームを組みました。
ソリューション
先述のチーム組みで、iOS書けるやつを引っ張ってきました。
せっかくなので、何から何までiOSに乗っける以下のような構成のシステムを提案しました(最終発表のプレゼンから抽出)。 iOS周りの実装をありがとう、@Taillook。
この図はKeynoteでゴリゴリ頑張りました。フォントは**M+**と、筑紫B丸ゴシック。
認識概要
- 色分けする運用が既にあるので、彩度のある部分のみを抽出すれば良さそう
-
BGR
空間からHSV
空間に変換して、S
(彩度のチャネル)のみ抽出
-
- 二値化
- Sチャネル抽出の時点でそこそこ二値化されているため、単純な
判別分析法
でOK
- Sチャネル抽出の時点でそこそこ二値化されているため、単純な
- 領域をそれぞれ切り出す
-
findContours
による辺検出 - 様々な辺が検出されるため、
cv2.RETR_TREE
による辺の構造化- 内側にさらに辺があるもの
OR
面積が一定以下 をスキップ
- 内側にさらに辺があるもの
-
- その領域がどの色で塗られているかを判別する
- 抽出された辺の斜め左上(
x - 1
,y - 1
)のピクセルの色を20点ほど抽出 - 平均をとって、その辺の色とする
- 抽出された辺の斜め左上(
対象画像
当時は動作検証用の実物の回答用紙をもらいましたが、ここで持ってくるには権利周りがグレーな予感がしたので、Pages職人 の力を使ってサンプルを生成しました。
実装
さて、もうそろそろ退屈なのでドーンとソースコードを。
FROM jjanzic/docker-python3-opencv
な Dockerfile
を用意してどこでも開発できるように用意しました。最終的に画像認識部分は僕がフルで開発したのでオーバーキルでしたが。
Dockerの中身は以下。
- Python 3.7.0
- OpenCV 3.4.1
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
のみ抽出
cv2.cvtColor(image, cv2.COLOR_BGR2HSV)[:, :, 1] # 0: H, 1: S, 2: V
もうこの時点でほぼ二値化なんですよね。すぎょい。
判別分析法
による二値化
cv2.THRESH_OTSU
を指定するだけ。イージー。
cv2.threshold(extracted, 0, 255, cv2.THRESH_OTSU)
少しノイズが入る場合は、このあとmorphologyEX
をかけてノイズ除去をするとよきです。
辺検出
もうもはやOpenCVがすごい。findContours
がすごい。論文を一応読んだんですが、辺の構造情報まで認識するとか変態的すぎる。
_, contours, hierarchy = cv2.findContours(thrshld_ex, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)
hierarchy
に、contours
内のどのindexの辺がのindexの辺の横にあるとか、内包してるとかを格納しています。すぎょい。
辺を点の集合で表し、紫色で描画しています。
辺の色を抽出 + 描画
ここはゴリ押しです。色の平均をこんなにも簡単に取れる numpy
に感謝。
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)
図形の中心を割り出し、そこに平均をとった色で円を描画しています。
(実はこの画像よく見ると、20サンプルの対象ピクセルに紫色がついています。)
だいたい辺と同じ色で良い感じ。
まとめ
ということで、学生時代に組んだ画像処理のプログラムをここで供養しました
僕からすれば振り返りだったのですが、これを読んでPython
+ OpenCV
の可能性を感じていただければ幸いです。
ただ一つ注意点として、OpenCV
は理論を知っていなくても多くの事例から学んで使えてしまいますが、やはり問題解決に対する手段適用の最短経路を導くには理論が先決な気がしているので、情報工学は大事です。僕は軽くすっぽかしたためぎりぎりの理論しかありません。
ただ以下は確実に言えるでしょう。
-
OpenCV
はすごい -
Python
+OpenCV
はやりやすい-
numpy
との相性がいい
-
ということです。実はこの記事を書く前にRust
によるOpenCV
を試したりもしたんですが、Python
+ OpenCV
の使い心地と比べてしまうとイマイチでした。まぁRust
の実装に慣れていない僕が言うので参考にはならないかもしれませんが
Python
だし遅いんでしょー?って話もありそうですが、描画を除くとなんと 0.3 [sec] ほど。
ただの C-binding
だしね。すげえや。
僕が開発に携わっている自転車通販サイト cyma -サイマ-でも、なんだか画像認識をやる気運が上がってきたので、力を発揮していきたい。
@shimura_atsushi が Ateam cyma Advent Calendar 2019 で2記事書いているので興味がある方はどうぞ。
おわりに
ゆるゆる新卒1年生 Advent Calendar 2019、23日目いかがでしたか?
24日目は、バックエンドもわかるつよつよフロントエンドエンジニア @cheez921 のターンです。デュエル、スタンバイ!
よいクリスマス、よい年末、よい年始、よい画像認識を。