MAGAZINE
ルーターマガジン
統合報告書から画像処理とSVMでSDGsアイコンを取る
世はまさに大SDGs時代。Sustainable Development Goals(持続可能な開発目標)へ向けて、世の中が大きく動いているのを日々感じます。特に最近は脱炭素への流れが凄まじいですね。
企業は競ってSDGsに向けた取り組みを投資家にアピールする時代です。
そんな中、企業が毎年出す渾身の一撃、『統合報告書』を見ていきましょう。「〇〇社 統合報告書」と検索すれば、ヒットしたPDFの中に17の開発目標に関する取り組みが散りばめられています。
17個。すべて大事な目標です。でもちょっと楽したい。「エネルギー問題に関する部分だけ読みたいなー」という人がいても、責められはしないと思います。そんなあなたに、画像認識を。
ルーターの伊崎がお送りします。
おことわり
本ブログではディープラーニングは扱いません! ディープラーニングで物体検知も楽しいですが、今回は根性的な画像処理とSVMでなんとかします。それはそれで楽しい。ディープラーニングでやってみたい方は、PyTorchのこちらのライブラリをどうぞ。facebookresearch / detectron2 わりと簡単に扱えて、精度良好です。おすすめ! さあ、本題へ!
本題
問題を整理しましょう。次のようなPDFがあります。
こちらからお借りしました。持続可能な開発目標 (SDGs)と日本の取組
統合報告書ではなく外務省の資料ですが、そこはお気になさらず。
このなかに、どのSDGsアイコンがあるかを知る。それが我々のやりたいことです。
手順① PDFを画像にする
PDFは画像ではない、自明のことです。画像にしましょう。使う道具はPoppler一択です!
pdftoppm -png -r 300 SDGs_pamphlet.pdf sdgs
PDFがページ単位でPNGファイルになります。sdgs-1.png、sdgs-2.pngという具合です。画像認識を実行する準備ができました。
手順② 画像からアイコンに似ているものを取る
いきなりアイコンを取るなんて、どうやればいいかわからない、ですよね? いきなり取るなんて乱暴なことが許されるのはディープラーニングです。我々は使わないと宣言しました。我々はまず、アイコン、っぽいものを取ります。そう、正方形のもの全部です!
さあコードです。みんな大好き、OpenCV-Pythonです。
import cv2 as cv
import argparse
def extract_square(img):
# 画像のエッジ部分を取り出す
thresh = cv.adaptiveThreshold(img, 255, cv.ADAPTIVE_THRESH_GAUSSIAN_C, cv.THRESH_BINARY, 11, 2)
# エッジから輪郭とみなせる部分を計算
contours, _ = cv.findContours(thresh, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE)
for cnt in contours:
approx = cv.approxPolyDP(cnt, 3, True) # 輪郭を多角形で近似する
area = cv.contourArea(approx) # 多角形で囲まれた面積を計算
if len(approx) == 4 and area > 1000: # 一定の面積を持つ四角形に絞る
perimeter = cv.arcLength(approx, True) # 四角形の周長を計算
ratio = perimeter ** 2 / area # 周長の2乗と面積の比を計算(厳密な正方形では16となる)
epsilon = 0.5 # 厳密な正方形との許容誤差
if abs(ratio - 16) < epsilon: # 正方形に絞る
x = approx.ravel()[0] # 正方形のx座標
y = approx.ravel()[1] # 正方形のy座標
l = int(perimeter / 4) # 正方形の1辺の長さ
cut_img = img[y : y + l, x : x + l] # 正方形の部分のみ切り取る
cut_img_path = f'cut_img/{str(x)}_{str(y)}_{str(l)}.png'
cv.imwrite(cut_img_path, cut_img) # 切り取った正方形をファイルに書き出す
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='画像から正方形を切り出す')
parser.add_argument("img_path", type=str, help="画像ファイルパス")
args = parser.parse_args()
img_path = args.img_path
img = cv.imread(img_path, 0) # 画像をグレースケールで読み込む
extract_square(img)
square.py
とでも名付けてファイルを保存します。先程の画像化したPDFの4ページ目をsdgs.png
と名付けておきましょう。
$ python square.py sdgs.png
画像がいっぱい生成されます。すべて正方形。こんな感じ。
ゴミが混じっていますが、気にしない。ここから機械学習にかけてあげます。
手順③ SVMで仕分け
SVM、いいですよね。動作原理がはっきりしていて、なにより軽い。これを使います。
順番に行きます。まずは、ゴミを取り除く。次に、17個のアイコンを仕分けしてあげる。この2段構えでいきます! OpenCV-PythonにSVMが入っているので、それを使いましょう。
アイコン画像と、アイコン画像じゃない正方形画像を片っ端から集めてください。ここは人力です。集めたら、以下のプログラムを実行しましょう。
import glob
import cv2 as cv
import numpy as np
def img_array(img_paths): # 画像の配列を作る
imgs = []
for img_path in img_paths:
img = cv.imread(img_path)
img = cv.resize(img, (100, 100))
_, result = cv.threshold(img, 220, 255, cv.THRESH_BINARY)
img = result.flatten()
imgs.append(img)
return np.array(imgs, np.float32)
def train():
icon_img_paths = glob.glob('icon/*.png') # アイコン画像たちのパス
not_icon_img_paths = glob.glob('not_icon/*.png') # アイコンでない画像たちのパス
# 画像の配列を作る
icon_imgs = img_array(icon_img_paths)
not_icon_imgs = img_array(not_icon_img_paths)
imgs = np.r_[icon_imgs, not_icon_imgs]
# 正解ラベルの配列を作る
icon_labels = np.full(len(icon_imgs), 0, np.int32) # 正解を0としている
not_icon_labels = np.full(len(not_icon_imgs), 1, np.int32) # 不正解を1としている
labels = np.array([np.r_[icon_labels, not_icon_labels]])
# SVMモデルの設定
svm = cv.ml.SVM_create()
svm.setType(cv.ml.SVM_C_SVC)
svm.setKernel(cv.ml.SVM_LINEAR)
svm.setGamma(1)
svm.setC(1)
svm.setTermCriteria((cv.TERM_CRITERIA_COUNT, 100, 1.e-06))
# 学習
svm.train(imgs, cv.ml.ROW_SAMPLE, labels)
# 学習結果を保存
svm.save('trained_data.xml')
if __name__ == '__main__':
train()
アイコン400個、アイコンでないもの300個で、10秒くらいで学習が終わります。では、いざ実験。
import cv2 as cv
import numpy as np
import argparse
def predict(img):
# 学習したモデルのロード
svm = cv.ml.SVM_load("trained_data.xml")
# 判別する
img = cv.resize(img, (100, 100))
_, result = cv.threshold(img, 220, 255, cv.THRESH_BINARY)
img = result.flatten()
img = np.array([img], np.float32)
predicted = svm.predict(img)
result = predicted[1]
if result[0] == 1.0:
print("アイコンじゃないよ")
else:
print("アイコンだよ")
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='アイコンを判別する')
parser.add_argument("img_path", type=str, help="画像ファイルパス")
args = parser.parse_args()
img_path = args.img_path
img = cv.imread(img_path)
predict(img)
predict.py
と名前をつけましょう。動かします。
$ python predict.py icon.png
アイコンだよ
$ python predict.py not_icon.png
アイコンじゃないよ
できました。しかし、これで終わりではありません! 17種類のアイコンを判別してこそ、われわれの目的が達せられます。とはいえ、もうゴールはそこにあります。学習・予測はアイコン/アイコンじゃない、のときと一緒です。2個が17個になるだけ。ので、割愛します!
肝心の精度ですが、まあ、実用ラインぎりぎりです。写真の中にアイコンがあったりして、境界が曖昧だとわりとお手上げです。今回みたいなはっきりしたアイコンなら、ほぼ取れると思ってもらって大丈夫です。SVMというより、画像処理部分が難しいです。あと人力でアイコンを分けるのがとんでもなく面倒。
締めのことば
画像認識楽しいですね。正直ちょっと昔のコードを引っ張ってきたのですが、非ディープラーニングの方法も悪くないないなと初心に返りました。ルーターでは(広い意味でのスクレイピングとして)画像認識も機械学習もやっています。ぜひ一緒にお仕事をしましょう! おしまい!
CONTACT
お問い合わせ・ご依頼はこちらから