世はまさに大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というより、画像処理部分が難しいです。あと人力でアイコンを分けるのがとんでもなく面倒。

締めのことば

画像認識楽しいですね。正直ちょっと昔のコードを引っ張ってきたのですが、非ディープラーニングの方法も悪くないないなと初心に返りました。ルーターでは(広い意味でのスクレイピングとして)画像認識も機械学習もやっています。ぜひ一緒にお仕事をしましょう! おしまい!

Pocket