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
お問い合わせ・ご依頼はこちらから