数値解析系

2022年6月28日 (火)

ラベル(四角形で囲まれた部分)を自動で抜き出して台形補正してくれるやーつを作ってみた

社内で、ラベル部分を撮影してその部分を補正しつつ自動的に抜き出すプログラムコードを作ってみました。
何を言っているのかというと、

Image3

例えばこういう写真を撮ると

Output

ラベルの部分だけを抜き出して、補正した状態で保存してくれるものを作った、ということです。
いろんなところを参考にしたのと、社内の有志(勇士)にも頼んで作った結果、できました。
参考にしたサイトは、以下。

OpenCVの使い方9 ~ 台形補正2 - つれづれなる備忘録

【label_daikei.py】

import cv2
import numpy as np
import math
import itertools

# 比率調整
import pandas as pd
# 入力画像のパス
input_file_path = "./data/image3.jpg"
# 出力画像のパス
output_file_path = "./output.jpg"
# 入力画像の読み込み
img = cv2.imread(input_file_path,0)
# 二値化
# 閾値の設定
threshold_min = 160
threshold_max = 230
# 二値化
ret, img_thresh = cv2.threshold(img, threshold_min, threshold_max , cv2.THRESH_BINARY)

#四角抽出
tmp= cv2.findContours(img_thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
contours = tmp[0] if len(tmp) == 2 else tmp[1]
#一定以上の面積の図形のみを抽出
areas = []
for cnt in contours:
    area = cv2.contourArea(cnt)
    if area > 70000:
        epsilon = 0.1*cv2.arcLength(cnt,True)
        approx = cv2.approxPolyDP(cnt,epsilon,True)
        areas.append(approx)
pt=[]
# arrayをdataframeに変換して整形
df_a = pd.DataFrame(areas[0].reshape(-1,2))
# 全4pointについてy座標でsort
df_a.sort_values([df_a.columns[0], df_a.columns[1]], inplace=True)
# 上下の2pointずつを分ける
df_upper = df_a.iloc[:2, :]
df_lower = df_a.iloc[2:, :]
# 下部2pointについてx座標でsort
df_lower.sort_values([df_lower.columns[1], df_lower.columns[0]], inplace=True)
# 上下を合体
df_a = pd.concat([df_upper, df_lower])
for j in range(df_a.shape[0]):
    # arrayにしてリスト化
    p = np.array([df_a.iloc[j, :].tolist()])
    pt.append(p)
# 全体をarray化
pt = np.array(pt)
#頂点の座標を得る
pt1=pt[0] #左上
pt2=pt[1] #左下
pt3=pt[2] #右下
pt4=pt[3] #右上
print(pt[0]) #左上
print(pt[1]) #右上
print(pt[2]) #右下
print(pt[3]) #左下
pts = np.float32(np.array([pt1,pt2,pt3,pt4]))
o_width = np.linalg.norm(pt2 - pt1)
o_width=int(np.floor(o_width))
o_height = np.linalg.norm(pt3 - pt1)
o_height=int(np.floor(o_height))
dst_cor=np.float32([[0,0],[o_width,0],[0, o_height],[o_width, o_height]])
# 変換行列
M = cv2.getPerspectiveTransform(pts, dst_cor)
# 射影変換・透視変換する
output = cv2.warpPerspective(img_thresh, M,(o_width, o_height)).T
cv2.imshow("cut", output)
cv2.waitKey()
# 射影変換・透視変換した画像の保存
cv2.imwrite(output_file_path, output)

参考にしたサイトの通りにすると、うまく抜き出せなかったのですが、これでようやく正常な画像として抜き出せました。
最後の方の「output = ~」のところに転置(.T)がついてて奇妙なコードですが、なんかもうこれで動いちゃったので、勘弁してください。

で、「data」というフォルダに入っているimage3.jpgという画像ファイルを読み込み、四角形で囲まれた部分を抜き出して補正するというものですが、18、19行目にある「100」、「230」という数値は、その画像抜出時に二値化するための閾値です。適宜、いじってください。

また、画像ファイルの横幅は1000程度にしておいた方がいいです。あまり大きいと、うまく読み込みません。
もし大きな画像サイズでじっしするときは、31行目あたりの

if area > 70000:

という行の「70000」という数値を大きくした方がいいでしょう。

この程度の台形補正ならば、Microsoft Pixあたりでも可能です。その方が楽ですね。
が、台形補正 → OCR処理 という連続処理をさせたかったがために、アプリではなくPythonのコード化をする必要に迫られた次第です。
もし、前処理として画像の歪みを補正する必要に迫られたら、ぜひご参考に。


OpenCVではじめよう ディープラーニングによる画像認識

2022年5月19日 (木)

「小説家になろう 第10回 ネット小説大賞」の一次選考通過作品のタイトルで共起ネットワーク図を作る手順

私事ですが、現在、選考が行われている「小説家になろう 第10回 ネット小説大賞」の一次選考に、以下の一作品が通過いたしました。

宇宙からやって来た技術武官とスマホ好き幽霊の王都事件簿

これ、第9回でも一次選考を通過した作品。今回で二度目。ということは、二次選考は……

と、それはともかく、実は一次選考した作品

 第10回ネット小説大賞:一次選考通過作品発表! | TOPICS | ネット小説大賞

のタイトルを眺めていたら、なんとなく分析したくなりました。

で、KHCoderを使い、その画像をTwitterに上げてみたところ、プチバズしまして。

かなりの方がご興味を惹かれたようです。

ならばと、その手順を残してみようかと思った次第です。

例えばアンケート分析などにも応用できるため、ぜひご覧ください。

まず、上の一次選考のタイトルを取得し、csvファイルにします。

私はブラウザ上のタイトルをマウスでぐるっと選択し、

Cgcon10_03

それをExcelに張り付けました。

Cgcon10_05

保存する前に、1行目にラベル名を入れます。ここでは「title」「name」としておきました。

(作者名は使わないので、ここを一列にしてもらってもいいです)

Cgcon10_06

この状態で、csvファイルとして保存。

さて、ここで分析用ソフトを入手します。

KHCoderというソフトを、以下から入手します。

 KH Coder: 計量テキスト分析・テキストマイニングのためのフリーソフトウェア

フリーソフトながら、なかなか強力なテキストマイニングツールです。

ここの「KH Coder 3 ダウンロード 」をクリックし、ダウンロードします。

で、得られたExeファイルをダブルクリックすると、以下のような窓が開くので、

Cgcon10_07

「Unzip」をクリックします。

Cgcon10_08

こんなアイコンが、デスクトップ上にできているはずです。

Cgcon10_09

このアイコンをダブルクリックし、開いたフォルダ内の「kh_coder.exe」をダブルクリック。

Cgcon10_10

こんなのが現れます。

Cgcon10_11

では、先ほどのcsvファイルを開きます。「プロジェクト」の「新規」をクリックすると、

Cgcon10_12

こんな画面が開くので、「参照」でファイルを選ぶか、ファイルを直接ドラッグ&ドロップします。

Cgcon10_12a

「分析対象とする列」が「title」になっていることを確認し、「OK」をクリック。

Cgcon10_13

こんな窓が開くので、「前処理」の中の「前処理の実行」をクリックします。

Cgcon10_14

これは、CPUのシングルコア性能によりけりですが、まあまあ時間がかかります。

Cgcon10_15

前処理が終わったら、「抽出語」‐「抽出語リスト」を選択。

Cgcon10_16

中で使われている単語が出てきます。

が、ここでちょっと、おかしな単語が。

「x{301c}」という変な単語があります。おそらく、ブラウザ上のテキストをコピペした際に入り込んだ文字コードだと思われます。

これは無意味な単語なので、消しておきたいですね。

また、トップに「世界」があります。

が、「異世界」がない。

日本語という言語は、英語などと違い、単語間にスペースが入っておりません。このため、分析するためにまず単語同士を区切らなければなりません。これが、前処理で行っている作業の一つです。これを「分かち書き」と言います。

この分かち書きは、辞書を使って行われるんですが、標準辞書には「異世界」という言葉がないようです。このため「異」「世界」とわけられてしまいます。

他にも「悪役令嬢」もないため、「悪役」「令嬢」に分かれます。

これらをただすために、以下の操作を実行。

Cgcon10_17

「前処理」‐「語の取捨選択」を選びます。

Cgcon10_18

こんな画面が開くので、先ほどの「異世界」や「悪役令嬢」といった、ひと塊にしたい単語を「force pick up」に、消したい単語を「force ignore」のところに入れます。

Cgcon10_19

こんな具合。

できたら、OKをクリックし、再び前処理を実行。

Cgcon10_20

目論見通り、「異世界」という単語が出てきました。全部で151個あるようです。

Cgcon10_22

さて、いよいよ分析ですが、その前にこのリストをざっと200位くらいまで眺めます。

なんとなく私は、170位、頻度9までの単語を表示しようと決めました。

Cgcon10_21

その後に、「ツール」‐「抽出語」‐「共起ネットワーク」を選択します。

Cgcon10_23

こんな窓が現れますが、ここで「最小出現数」を9、「上位」のところを170としておきます。

「プロットサイズ」を960としておくのをお勧めします。

Cgcon10_24

で、OKをクリックすると、御覧の通り、共起ネットワーク図が出てきます。

「共起」とは、すなわち単語同士の関連の強さを表すもので、関連のある単語同士が線で結ばれてます。

ある程度のクラスタリングも行われており、同じクラスタと思われる者同士が同じ色に塗られてます。

ざっと眺めてみると、なるほどよく見かける単語が多いのが分かります。最も大きいのは「異世界」で、共起度の高い単語に「転生」「召喚」「モブ」などがあることが分かりますね。

あと「勇者」「追放」や「悪役令嬢」「婚約」も共起度の高い単語同士であると分かります。

つまり一次選考を通過した作品には、そういうタイトルが多いということです。

もっとも、この傾向を反映したタイトルの作品が一次選考を通過しやすいかどうかは、また別の問題でしょうが。

なお、Twitterに載せた画像とはちょっと違うと思われますが、これは単語の並び方や色付けが毎回ランダムで行われていること、あと抽出する単語の条件や前処理が、この間とはちょっと異なるためです。

これでほぼ目的は達成されますが、もうちょっとだけ補足。

このネットワーク上の単語をクリックすると、

Cgcon10_25

こんなものが現れます(「勇者」をクリックした例)。

その単語を持つタイトルが、ずらずらっと現れます。

これをさらにダブルクリックすると、そのタイトルと、作者名が出てきます。

Cgcon10_26

こんな具合に、より詳細を確認することができます。

Cgcon10_27

もう少し、共起ネットワーク図の単語を絞り込みたいと思う場合は、まず「抽出語」‐「関連語検索」を選択し、

Cgcon10_28

例えば「追放」を検索し、その下にある「共起ネット」をクリックします。

Cgcon10_30

すると、「追放」に関するワードのみで共起ネットワーク図が形成されます。

なお、上の図はこのネットワーク図の下にある「調整」というボタンをクリックし、上位を200まで上げた際の結果図です。

とまあ、こんな具合にこのKHCoderは、膨大な文章データの傾向分析や可視化を行うことができる便利なツールです。

私は会社で、よくアンケート分析に使用しております。

できれば一次選考を通過した作品名の横にカテゴリーもあると、より面白い分析ができたんですけどねぇ。

なお、小説そのものを分析したこともあります。

「砲撃手の妻は呪術師(シャーマン)」をKH Coderの「共起ネットワーク」で分析してみた: EeePCの軌跡 

句読点で改行させるなどの事前処理が必要となりますが、小説の癖や傾向を見ることができて、これもなかなか面白いです。

アンケート分析や、自分の小説の分析など、使い道いろいろな分析ツールです。


動かして学ぶ! はじめてのテキストマイニング: フリー・ソフトウェアを用いた自由記述の計量テキスト分析 KH Coder オフィシャルブック II (KH Coder OFFICIAL BOOK 2)

2022年3月26日 (土)

Interface 2022年5月号を買った

久しぶりに買った気がします。

というのも、タイトルがこれだったので。

Img_1376

はい、いかにもデータ処理やります的な特集です。

Img_1377

目次を見るだけでも、その道の人にとってはワクワクしてきますね。

Img_1381

中身にはあまり触れませんが、とりあえず、今回の特集をよくあらわす記事のタイトルをば。

米こうじの温度管理とか、まあ普通はそんなもの、作りませんよね。そういうニッチなネタとなると、当事者が作るしかない。まさにそんな感じのネタです。

常日頃、思うんですけど、Interfaceの中の人って、すごくアンテナ広いですよね。よくこんな事例、思いつきます。しかも、作り上げてしまうところはいつも惚れてしまう。

Img_1378

ところで、こんな付録もついてます。ぱっと見た目には、Python初心者向けの別冊付録かしら?と期待を胸に抱いてしまいます。

Img_1379

が、目次を見たあたりで、なんだか雲行きが怪しくなります。

Img_1380

フーリエ変換の解説とか、数式付きで出てくるあたり、なかなかです。

こういう懐の深さ(?)も、Interfaceらしいところで。

こんな中身が濃いのに、1130円で買えてしまいます。

ご興味ある方は、ぜひ。

Interface 2022年 5月号

2022年3月 9日 (水)

AIに認識されなくなるシャツ

いわゆる「AI迷彩」という、物体検出手法から認識されるのを防ぐ模様というものが存在するのですが、それをシャツにしたものが存在するようです。

着ると「AIに人として認識されなくなる」ファッションアイテムが登場―AI監視社会から身を守る | 知財図鑑

まるでファンタジーものでよくある「認識阻害」ってやつですね。

ところで、物体検出には主に「SSD」と「YOLO」という手法がよく知られてますが、どちらに対しても有効なんでしょうかね?

一度、試してみたい気がします。

国によっては、誰がどこを通過したか、まで把握されてしまうらしいので、そういうところでは切実な悩みがありそう。そういうところでは、これが活躍するんですかね?

Jetson Nano 超入門 改訂第2版

2022年2月22日 (火)

OpenCVでサイゼリヤの間違い探しを解くやつ

最近、なぜかサイゼリヤがトレンドに上がってますね。

いいお店だと思います。あそこのミラノ風ドリア、私は好きです。

と、そんな話は置いておき、最近、2つの画像を比較して、その変化部分を抽出するやつを作る必要に迫られました。

このため、まずサイゼリヤの間違い探しをさせてみよう、ということになった次第です。

参考サイトは、以下。

 エンターテイメント|サイゼリヤ

OpenCVを使ってサイゼリヤの間違い探しを簡単に解いちゃう!! - Qiita

サイゼリヤの間違い探しを画像処理で解いてみた - Kasasagi’s memorandum

まず、サイゼリヤのサイトから、間違い探しの画像を一つチョイスし、それの左右をトリムして、別々の画像ファイルとして保存しておきます。

その際に、ちょっとだけ左右画像のカット位置を変えておきました。この両者、あえて位置をずらしてます。

Zeria1

Zeria2

その位置補正までを、プログラムにやらせるのが目的です。

で、コードは以下。


import cv2
import numpy as np

# --------------------------------------------------- #
# 画像合成                                             #
# --------------------------------------------------- #
def ImageComposition(img2, result):
    img3 = cv2.cvtColor(img2,cv2.COLOR_BGR2GRAY) # グレースケール化
    img3 = cv2.cvtColor(img3,cv2.COLOR_GRAY2BGR) # グレースケールのままカラー画像にする
    # コントラスト、明るさを変更する。
    img3 = adjust(img3, alpha=0.25)
    add = cv2.add(img3, result) # 画像を合成する
    return add

# α  はゲイン (gain) 、βはバイアス (bias)
def adjust(img, alpha=1.0, beta=0.0):
    # 積和演算を行う。
    dst = alpha * img + beta
    # [0, 255] でクリップし、uint8 型にする。
    return np.clip(dst, 0, 255).astype(np.uint8)

def FitImageSize_small(img1, img2):
    # height
    if img1.shape[0] > img2.shape[0]:
        height = img2.shape[0]
        width = img1.shape[1]
        img1 = img1[:height,:width] # ゼロ点合わせで大きい方の画像の下をカット
    else:
        height = img1.shape[0]
        width = img2.shape[1]
        img2 = img2[:height,:width]

    # width
    if img1.shape[1] > img2.shape[1]:
        height = img1.shape[0]
        width = img2.shape[1]
        img1 = img1[:height,:width] # ゼロ点合わせで大きい方の画像の右をカット
    else:
        height = img2.shape[0]
        width = img1.shape[1]
        img2 = img2[:height,:width]
    return img1, img2

try:
    # === 画像位置合わせを実施 ===
    # 使う画像は「サイゼリヤの間違い探し」: https://www.saizeriya.co.jp/entertainment/
    # 参照: https://qiita.com/suuungwoo/items/9598cbac5adf5d5f858e
    #       https://qiita.com/h-yanai/items/1e33fd93e5cb1ac98398?utm_campaign=popular_items&utm_medium=feed&utm_source=popular_items

    float_img = cv2.imread('img/zeria1.jpg')
    ref_img = cv2.imread('img/zeria2.jpg')

    akaze = cv2.AKAZE_create()
    float_kp, float_des = akaze.detectAndCompute(float_img, None)
    ref_kp, ref_des = akaze.detectAndCompute(ref_img, None)

    bf = cv2.BFMatcher()
    matches = bf.knnMatch(float_des, ref_des, k=2)

    good_matches = []
    for m, n in matches:
        if m.distance < 0.75 * n.distance:
            good_matches.append([m])

    # 適切なキーポイントを選択
    ref_matched_kpts = np.float32(
        [float_kp[m[0].queryIdx].pt for m in good_matches]).reshape(-1, 1, 2)
    sensed_matched_kpts = np.float32(
        [ref_kp[m[0].trainIdx].pt for m in good_matches]).reshape(-1, 1, 2)

    # ホモグラフィを計算
    H, status = cv2.findHomography(
        ref_matched_kpts, sensed_matched_kpts, cv2.RANSAC, 5.0)

    # 画像を変換
    warped_image = cv2.warpPerspective(
        float_img, H, (float_img.shape[1], float_img.shape[0]))

    #cv2.imwrite('warped.jpg', warped_image) # 確認用:変換後の画像を保存

    # ==== ここから、間違い探し部分 ====

    img_diffs = []
    paddings = []
    for padding in range(1, 50):
        img1 =  warped_image
        img2 = ref_img


        # 画像サイズを合わせる(小さい方に)
        img1, img2 = FitImageSize_small(img1, img2)

        # 2つの画像の差分を算出
        img_diff = cv2.absdiff(img2, img1)
        img_diff_sum = np.sum(img_diff)

        img_diffs.append((img_diff, img_diff_sum))
        paddings.append(padding)

    # 差分が最も少ないものを選ぶ
    img_diff, _ = min(img_diffs, key=lambda x: x[1])
    index = img_diffs.index(min(img_diffs, key=lambda x: x[1]))
    cv2.imshow("img_diff", img_diff)
    cv2.imwrite('img_diff.jpg', img_diff)

    padding = paddings[index]
    img1 = float_img
    img2 = ref_img
    cv2.imshow("img1",img1)
    cv2.imshow("img2",img2)

    # 画像サイズを合わせる(小さい方に)
    img2, img_diff = FitImageSize_small(img2, img_diff)
    # 画像合成
    add = ImageComposition(img2, img_diff)
    cv2.imshow("add",add)
    cv2.imwrite('img_diff_add.jpg', add)

    cv2.waitKey(0)
    cv2.destroyAllWindows()
except:
    import sys
    print("Error:", sys.exc_info()[0])
    print(sys.exc_info()[1])
    import traceback
    print(traceback.format_tb(sys.exc_info()[2]))
   

これを「pydiff.py」という名前で保存しておきます。

御覧の通り、OpenCVとnumpyしか使ってませんね。

これで、平行位置と台形歪み程度なら補正し、2画像の位置と大きさを合わせた後に、左右の差分を検出してくれます。

キーになるのは、AKAZEという画像特徴量を使った位置合わせと、画像サイズ合わせ、差分を取るところですが……その辺りは、コードを解読願います(適当)

これを実行すると、差分のみのファイル「img_diff.jpg」と、それを元画像と重ねた「img_diff_add.jpg」とが得られます。

Img_diff

Img_diff_add

10か所の間違いがありますが、すべて、あぶりだしております(うっすらと、中央のどでかいストローの刺さったコップの水面が出てますが……ここだけは違います)

うん、正解ですね。なかなか優秀じゃん。

と思いきや、これがサイトの綺麗な画像ではなく、写真を使うと、いろいろと問題が出ました。

明るさや色がちょっとでも異なると、途端に差分が増えます。

写真に対して使う際は、コントラスト調整や畳み込み処理などを駆使して、どうにかその差分のある所だけを出せるようにする工夫が要ります。

その前段階の、位置合わせと差分位置を出す部分だけを公開しておきます。

例によって、自己責任にてお使いください。

なお、よくTwitter上に現れる間違い探し広告なんかも、これを使えば解けてしまいそうです。

にしても、サイゼリヤの間違い探しを眺めていたら。

サイゼリヤに行きたくなってきましたねぇ。。。

まだ、エスカルゴを食べたことがないんです。一度、食べてみたい。

サイゼリヤ おいしいから売れるのではない 売れているのがおいしい料理だ (日経ビジネス人文庫)

2021年12月31日 (金)

2021年まとめ

そういえば今年は、自分用のデジタル端末をまったく買わなかった年ですね。

いや、厳密には全く買わなかったわけではないのですが……ジャンク品と家族用を除いた、自分用のものという意味ですが。

さて、年末恒例、今年の振り返りです。

【1月】

簡単にSFなCGが作れるツール「とてかんCG」を使ってみた: EeePCの軌跡

そういえば、こんなツール使いましたね。これ、今年の1月だったんですか。もっと以前かと思ってました。

最近は、全然使ってませんね。相変わらず、熱しやすく、冷めやすいです。

【2月】

AUKEY製バッテリー&充電器、ケーブル2本到着: EeePCの軌跡

まったく外に行く機会がないというのに、モバイルな用品ばかりそろえてました。

去年買ったSurface Goを強化するために買ったのですが、今のところ、本来の出番はありません。

もし今、出張があれば、これを持ち歩くところなんですけどねぇ……東京はおろか、名古屋出張すらありませんし。

なお、この時買ったバッテリーは、ちょくちょく使ってます。

【3月】

久しぶりの大須(コロナ禍で2回目): EeePCの軌跡

大須に行きましたね。コロナ禍でもなんだかんだと、2度目です。

なお今年は8月と11月、そして12月にも、大須に立ち寄ってます。遠出ができない分、近場のここに立ち寄ることが多いです。

そういえば、今年はデジタル端末買ってないと書いてますが、こういうものは買っているんですよね。Xperia Z4。iPhone 6/6 Plusと同時期のスマホです。

2200円で買ったわりに、綺麗でした。デザインもいい。

が、遅すぎて使い物にならないんですよ、これ。我が家でもまだiPhone 6/6 Plusは使われているというのに、ここまで遅くはないですけどね。Androidってやっぱり、製品寿命が短いです。

【4月】

非接触温度計で”体温”を測ってみた: EeePCの軌跡

非接触温度計を買いました。一応、体温計ではありません。

が、体温計モードがついてます。

といっても、体表面温度+4度という補正をやってるだけのようですが。

体温測定の時間がかかってしょうがないので、多少不正確でもいいから素早く測れるやつと思って購入したんですが。

結論からいうと、ワクチン接種の副反応時に、異常を検知できませんでした。37.8度の時に、36.7度と返してきました。

やはり、安物ですね。

【5月】

iPhone SE(第2世代)を購入: EeePCの軌跡

iPhone SE(第2世代)、買いました。

といっても、長男用です。

今でも元気に使われてます。速度面、バッテリーでは、まったく不満はないようです。

1TBのWD製M.2 NVME SSDとワイヤレス静音マウス買った: EeePCの軌跡

さて、自分向けとしては、こんなものも買ってます。

1TB M.2 NVME SSDです。メインPCであるGR1650TGF-T用に買いました。

元々が512GB SSDがついてましたが、やはり1TBはないと使い辛いですね。重宝してます。なによりもWD製ということで安心。Windows 11に変えた今でも、快適に動作してます。

【6月】

Jetson Nanoの固定カメラで人の動きを分析する「動線分析」をやらせてみた: EeePCの軌跡

そういえば、Jetson Nano 2GB版を買ったんですよ。

それを応用したのが、こちらの事例。

今年は物体検出の応用にちょくちょく手出ししてたのですが、今のところ、あまり役になってませんね。

来年くらいからでしょうか、この辺が使われだすのは。

USB-C to MagSafe2ケーブルと65W USB-C電源でMacBook Airを充電してみた: EeePCの軌跡

またしても、出張した時に備えた買い物をしました。

先に買ったこの65W電源と合わせて使えば、MacBook Airも持ち出しやすくなります。

が、先のモバイルバッテリー同様、未だにこいつの真価を発揮する使い方をできてません。

【7月】

月の土地、買いました: EeePCの軌跡

スケールだけは、過去最大規模の買い物です。

月のこの辺りに、1200坪の土地を買いました。

ただし、未だに現地に行けていないので、手に入れた実感がないのが残念です。

死ぬまでに、いけるかなぁ……

【8月】

SONYのCD/ラジオ/BluetoothスピーカーのCMT-X3CDを購入: EeePCの軌跡

前から欲しかったものを、ようやく手に入れました。

SONY製のBluetoothスピーカー兼ラジオ兼CDプレーヤーです。

予想以上に、ラジオの出番が多いです。Bluetoothスピーカーがメインだったつもりですが、気が付いたらラジオばかりつけてますね。

ということで、近頃はCDもよく使ってます。が、最初のローディングが遅すぎて、デジタル音楽に慣れ切った体には、ちょっと辛いかも。

第9回 ネット小説大賞 一次選考通過は2作品: EeePCの軌跡

性懲りもなく、今年もチャレンジしてます。

相変わらず2次通過はできなかったのですが、今回初めて、下記の連載中の作品が一次選考を通過しました。

新鋭艦隊のポンコツ旗艦の提督と戦乙女たち

なおこの作品、11月には1000pt、10万PV、100万文字を突破してます。

【9月】

在宅勤務用のUSB式ヘッドセット買ってみた: EeePCの軌跡

在宅ワーク用に、買いました。

うちのメインPCのGR1650TGF-Tのマイク端子が死んでいて、USB接続でないとヘッドセットが使えないことが判明。

もう何度か使ってますが、使い勝手は良好です。

怪しいパルスオキシメーター買ってみたんですが……: EeePCの軌跡

こんなものも購入。

どちらかというと、こういう製品の闇を暴いてやろうと思って買ったのですが、私の能力では暴けないほど、一見まっとうな動きをしてくれます。

なお、値が正確かどうかは、まったく分かりません。確かめるすべがないですから。

【10月】

エアコン設置のために机を片付けていたら、Windows黎明期の雑誌に魅了された件: EeePCの軌跡

エアコン設置のために、その真下にあるPC机を動かしたんですが、ついでにいろいろと整理整頓してたら、昔の雑誌が出てきて読みふけるという事案が発生。

例えば上の画像は28年前のベーマガですが、他のページを見ると「ああ、28年のコンピューターの発達ぶりは凄まじいものがあるなぁ」と感動するんですが、教育に関しては、ほぼ28年前と今で状況が変わっていないという……

ここだけ、現代の話をしているのかと思いました。ほんとに。

機械が発達しても、人間ってやつ(いや、日本の教育ってやつ)はほとんど変わっていないことの証左です。

【11月】

「新鋭艦隊のポンコツ旗艦の提督と戦乙女たち」が1000pt越えを達成: EeePCの軌跡

新鋭艦隊のポンコツ旗艦の提督と戦乙女たち

連載中の「新鋭艦隊のポンコツ旗艦の提督と戦乙女たち」という小説が、1000ptを超えました。

なお、この小説、8月のところにも書いた通り、10万PV、100万文字も超えております。

ネット小説大賞で1次も突破し(2次は落選……)、久々にいろいろと大台を超えた作品となりつつあります。

 Windows 11にアップグレードしてみた: EeePCの軌跡

メインPCであるGALLERIA GC1650TGF-Tを、思い切ってWindows 11にアップグレードしてみました。

不具合だらけでヒャッハーってなるのかと覚悟してましたが、思ったより安定してて拍子抜けです。

スタートメニューなどが大きく変わったものの、あれが使い易いとはあまり思えませんが……ただ、今どきのデザインになり、ちょっとだけ新鮮な気分を味わいました。

このブログのアクセス数がついに1600万PVに!: EeePCの軌跡

ブログのアクセス数が、1600万PVを超えました。

100万の桁を更新するのは、実に2年8か月ぶり。

Yogibo買いました: EeePCの軌跡

「人をダメにするクッション」に、ついに手を出してしまいました。

実は今年一番の散財は、これです。

昨年はいろいろ買ったためか、今年はあまり大物を買いませんでしたね。

なお、座り心地はなかなかです。

【12月】

12月は、いろいろと買いました。

まずは、Fire HD 10。

Fire HD 10を購入: EeePCの軌跡

お次は、iPhone 13。

iPhone 13買いました(ただし妻用): EeePCの軌跡

実は両方とも、妻用です。

12月に、自分向けに買ったのは、こいつ。

Traybo 2.0を買ってみた: EeePCの軌跡

yogiboで使うPCテーブルです。在宅勤務で使いましたが、リモート会議をこれでやると、映像が揺れますね。一人作業向けです。

気づけば、風呂上がりで足から汗が噴き出るときに、PCを湿気から守るとき以外は、あまり使ってませんね。

……というわけで、今年は静かな1年でした。

といっても、決して穏やかではありません。なんせ、まだコロナ禍ですから。

デジタル端末を買わなかったということもあるんでしょうが、特筆すべき何かがあったという年ではありません。

別に買おうと思ったものがなかったというわけではありませんが。実際、iPad miniの第6世代をどうしようかと思ったのは事実。

ところが、近所のApple専門店に行っても、入荷未定というありさま。まさに、モノ売ってるレベルじゃねえ、ですよ。ほんと。

来年は、こういうのが解消されているといいんですが。

今年も残すところ、あとわずか。

これが、今年最後の更新となります。

というわけで、皆さま、良いお年を。


Mr.PC (ミスターピーシー) 2022年2月号 [雑誌]

2021年9月18日 (土)

アナログメーターの画像から針の角度を読み取るやつ作ってみた(OpenCV)

会社でアナログメーターを撮影し、そこからメーターの値を自動で読み取りたい、という相談を受けました。

てことで、そういうものを早速、作ってみました。

なお、世の中にはこういうソリューション(物体検出を使用)もあるそうですが。

「アナログメーター解析AI」を創りました! | | 株式会社プライムキャスト(PRIMECAST)

ここでは敢えて、機械学習を使いません。数字はともかく、メーターの針の角度くらい、画像解析だけで行けるんじゃないか、と。

ということで、OpenCVのハフ変換というのを使います。

ここのサイトを、参考にしました。

 アナログメーター 1(直線検出)|uPyC|note

で、ハフ変換というのは、画像中の直線や円などの図形を読み取る手法、とでも思ってください。

その会社の方の相談で読み取りたいメーターは、ガスボンベのメーターでした。

自宅には手ごろなメーターがないので、ネットのメーカーのサイトやらなんやらやらを探って、テスト画像を手に入れてきました。

Meter2

このメーターの針の角度を、真下を0度として、時計回りに角度を読み取るというものを作りました。

コードは以下。

「meter_read.py」


# cv2.HoughLines() 関数
import cv2
import numpy as np
import statistics

img = cv2.imread('./meter.jpg')
# 画像の大きさを取得
height, width, channels = img.shape[:3]
# 二値化
threshold = 100
ret,img_thresh = cv2.threshold(img, threshold, 255, cv2.THRESH_BINARY)
# エッジ画像へ変換(ハフ変換で直線を求めるため)
edges = cv2.Canny(img_thresh,50,200,apertureSize = 3)
cv2.imwrite('houghlines2.jpg',edges)
cv2.imwrite('houghlines1.jpg',img_thresh)

# 自動的に直線が2本となるパラメータを検出
# minn:何点の点が並んでいたら、直線を引くか?のパラメーター値
for m in range(10,161,1):
    lines = cv2.HoughLines(edges,1,np.pi/180,m)
    if lines is None:
        break
    print(len(lines))

    if len(lines)==2:
        minn = m

print('minn = ', minn)
lines = cv2.HoughLines(edges,1,np.pi/180,minn)

theta_t = [] # 原点から直線に向かって下した法線と、水平線との角度 (ラジアン) を格納する配列
aa = []   # 直線の傾きを格納する配列
bb = []   # 直線の切片を格納する配列

i = 0

for i in range(len(lines)):
   for rho,theta in lines[i]:
        print('rho = ', rho)
        print('theta = ', theta)
        theta_t.append(theta)
        a = np.cos(theta)
        b = np.sin(theta)
        x0 = a*rho
        y0 = b*rho
        x1 = int(x0 + 1000*(-b))
        y1 = int(y0 + 1000*(a))
        x2 = int(x0 - 1000*(-b))
        y2 = int(y0 - 1000*(a))
        cv2.line(img,(x1,y1),(x2,y2),(0,0,255),2)

        # 2点を通る直線の式は、y = (y2-y1)/(x2-x1)x - (y2-y1)/(x2-x1)x1 + y1
        # 傾き a = (y2-y1)/(x2-x1) 、 b = y1 - (y2-y1)/(x2-x1)x1

        a0 = (y2 - y1) / (x2 - x1)
        b0 = y1 - (y2 - y1) / (x2 - x1)* x1

        aa.append(a0)
        bb.append(b0)

# 針が画像の左上、左下、右上、右下 のどこにいるかを、2直線の交点の位置で判断し、角度の式を変更
# なお、針の中心は画像の中心にあるとして、計算
# 交点の式は、((b[1] - b[0]) / (a[0] - a[1]) , (a[0] * b[1] - b[0] * a[1]) / (a[0] - a[1]) )

x_t = (bb[1] - bb[0]) / (aa[0] - aa[1])
y_t = (aa[0] * bb[1] - bb[0] * aa[1]) / (aa[0] - aa[1])

if x_t < width/2# 針が左上か左下にいるとき
    theta_hor = statistics.mean(theta_t)*180/np.pi
else# 針が右上か右下にいるとき
    theta_hor = 270 - (90 - statistics.mean(theta_t)*180/np.pi)

print(theta_hor)

cv2.imwrite('meter_line.jpg',img)

必要なライブラリは、たった2つ。OpenCV、Numpyです。

> pip install opencv-python numpy

で、使い方は、上のメーターの画像(ここではmeter.jpg)を同じフォルダ内に置いて、Windows PowerShellなどで

> python meter_read.py

と打ち込むだけ。

先のリンクではグレースケールの画像からエッジ処理をかけてましたが、ここでは二値化した画像からエッジを読み込ませてます。

その方が、安定していたので。

Houghlines2

で、ここから針の部分の直線の実を読み出すために、21行目の「for m in range(10,161,1):」以降の繰り返し分で最適なパラメータ値を決めています。

具体的には、2本の直線を抽出できた時のパラメータ値のみを探り出します。

で、ハフ変換を実行。

Meter2line

すると、こんな感じの直線が得られます。

ちなみに、この時の針の角度は、この2本の線の中間の角度から割り出してます。

この場合は約78度と返ってきました。真下が0度なので……いい感じに読み取れてます。

なお、針の角度を出すのには、ちょっと工夫が必要です。

実は針が左半分を向いているときと、右半分の方向を向いているかで、角度の式を変えないといけません。

具体的には、左下を向いているか、右上を向いているかが、このハフ変換から得られた直線の角度だけでは判定できないんです。

そこで、得られた2本の直線の交点が、右か左かどっちにあるかで、その式を分けるように工夫してます。

詳しくは、OpenCVのハフ変換のリファレンス、および上のコードをご覧ください。

なお、本当に針の向きが変わっても読み取れるのかが心配だったため、画像を回して確認してみました。

Meter23

例えばこの向きでも、

Meter23line

こういう画像が出力されて、角度は「348度」と返ってきます。

うん、いい感じですね。

メーターごとに角度→目盛り値の変換係数を定義してやれば、物体検出など無しに使えます。

それこそ、Raspberry Piでも動かせるほど軽いので、Raspberry Pi Zero + カメラだけで運用できます。

なお、針の向きが右か左かを判定する際に、画像の中心から右か左かで判定させてますが、本当ならこのメーターの中心点から見て右か左かを見ないといけないのですが、そこはさぼってますね。

本来なら、ハフ変換で円を抽出させ、そこから中心点を探すというのを入れた方がより精度が上がります。

この辺が、参考になります。

アナログメーター 2(円を検出)|uPyC|note

最近ですが、何でもかんでも物体検出、画像認識を使うよりも、まずこの手の画像処理を試すことから始めた方が良いことが多いです。

事前処理をした上で画像認識などを使う方が、認識率が上がることも多いですし。

もし仕事で、自動でアナログメーターを読ませたくなったら、ぜひご参考になさって下さい。


Raspberry Pi Zero W スターターセット (USB小型電源, 高速型32GB MicroSD, USBスイッチケーブル, ケース, HDMIケーブル, MicroUSB変換アダプター付き)

2021年8月 9日 (月)

KerasのVGG16でファインチューニングする場合は、画像データを255で割っちゃダメ!?

ちょっとマニアックな記事です。分かる人には分かる。

以前、VGG16のファインチューニングコード: EeePCの軌跡という記事を書きました。

その中で、画像データを読み込んだ後に、numpy形式に変換し、

X = X / 255

のように、255で割っているところがあります。

これは、画像データの三原色(R、G、B)ごとのデータ値が0~255となっているため、数値計算上、正規化するためにやってる操作なのですが。

最近、この「255で割る」をやらない方が精度が上がるという妙な現象に出くわしまして。

実際、自宅にある適当な画像セットで、30サイクルほどを回した結果が、以下のようになります。

Vgg16_01_20210808172501

点線が、255で割ってる時のLoss値(train、val)の減少、実線の方が255で割らなかった時のLoss値のグラフ。

明らかに、255で割らない方が収束が速く、Loss値も小さくなる、という結果でした。

これは、会社でやっても同じだったんです。

で、ある人から、以下のサイトを教えていただきました。

 TensorFlow, KerasでVGG16などの学習済みモデルを利用 | note.nkmk.me

この記事の中ほどに「画像の前処理: preprocess_input()」という項目があるんですが、その中で、この入力する画像データの前提として

  • 画素値の範囲: 0 - 255
  • 色の並び: RGB
  • channel last(サンプル数, 縦, 横, チャンネル数)

って、書いてあるんです(VGG16だけではなく)。

で、ここにもある通り、画素値の範囲の前提が0~255となってるんです。

えっ!?やっぱり255で割っちゃいけないってこと!?

もちろんこれは、推論コードでも同じです。

まだそれほど試してませんけど、会社では推論後にGrad-CAMでヒートマップ出力させてますけど、明らかに255で割らない方がしっくりする結果が出てきたりしてます。

どうやら、keras等の事前学習データを用いるときって、注意が必要みたいです。

あまり考えてませんでしたが、これからはちゃんとリファレンスを読んだ方がよさそうですね。

ちなみに、Qiitaなどに上がっている記事では、このVGG16のファインチューニングコードでけっこう255で割ってたりします。というか、割ってるものしか見当たらない。

ということで、本当にこの解釈が正しいのかどうか自信が持てないんですが……まあ、そういう話もあるということで。

高校数学からはじめるディープラーニング 初歩からわかる人工知能が働くしくみ (ブルーバックス)

2021年3月30日 (火)

Yolov3で駐車場空き状況把握をトライ中

なんか急に、そういうものを調べようということになりまして。

というのも、会社で「駐車場の空き状況を把握できないか」という話が出ました。

やり方は色々とあるのですが、最も手っ取り早い方法を試すことに。

それは、汎用の物体検出手法を使って「車」を検出させて、その数・位置から空き状況を把握する、というもの。

そこで以前、最新の物体検出YOLO v3 (Keras2.2.0+TensorFlow 1.8.0)を独自データで学習できるようにしてみた: EeePCの軌跡でも紹介したYolov3を使ってみました。

20210326_173609

幸い、我が家のすぐ横が駐車場なので、それを利用させてもらいます。

タイムラプス撮影を行い、その後、その写真で車の数をカウントさせるというところまでを実行。

元のコードをちょっと書き換えて、バウンディングボックスの中心点と、その中に数を表示するようにしました。

Yolo_20210326_170236

で、結果はこの通り。見えにくいですが、赤い丸の中に数字が書かれてます。

実は側面からなので、ちょっとカウントしづらい環境でしたが、まあまあ正確なカウントと位置把握はできたんじゃないかと。

と、思いきや、よく見ると奥の車はダブルカウントしてますね。

やっぱり、正面 or 背面から撮影しないと、うまくいかなさそうです。

まあ、プロトタイプができたので、これをもって社内に売り込もうかと。

定点観察なので、駐車場枠の中のアスファルト路面が見えるか見えないかでやるのが、一番確実な気もします。あるいは、Yoloなどとそれを組み合わせて、精度を上げるというのもありかもしれませんね。

なお最近、社内外でこのYolov3がよく使われてます。

Yoloの最新版はv5なのですが、あちらは商用利用に難ありだったりするので、手軽なv3に人気があるようです。で、私の以前書いた上の記事がマニュアルとしてよく使われてると聞いてます。

実際、SSDよりYoloの方が検出数や精度が高く、かつ速いですからね。

Google Colabを使うという手もあるんですが、手軽な反面、業務用ではやや手を出しにくい(Google Drive上に業務データを置くことになるので)という事情もあって、オンプレで利用したいという声が多いですね。そうなると、オンプレ上での活用が進んでいるYolov3に人気が集まる、ということのようです。

実は実業務で物体検出を使ったことがなかったのですが、これを機にいろいろと活用してみましょうかね。


今すぐ試したい! 機械学習・深層学習(ディープラーニング) 画像認識プログラミングレシピ

2021年2月27日 (土)

Raspberry Pi × 画像認識 で「後出しじゃんけん機」作ってみた

久しぶりの、Raspberry Piネタです。

今日は、どちらかというと「教育用」なネタです。

Raspberry Piと画像認識を応用した「後出しじゃんけん機」なるものを作ってみました。

なんじゃそら?という名前ですが、簡単に言うと、

(1) グー、チョキ、パー をラズパイカメラで撮影して記録 (各30枚づつくらい)

(2) 上の写真を用いて深層学習(CNN)を実施

(3) できたモデルを使い、カメラで撮影した「手」を推論する

(4) 推論結果に対し、勝つ手の画像を表示する

……という、Raspberry Piを使った一種のAIです。

これ、要するに、身近なもので画像認識AIを体感しよう!って趣旨の工作&プログラムです。

通常なら、Raspberry PiとPCを組み合わせるところですが、この両者の行き来が煩わしいので、学習用のデータ取りから学習、そして推論までを、すべてRaspberry Pi上でできるようにしてます。

まず、準備ですが、以下の3つのコードをコピペして持って行ってください。

「1_camera.py」


import glob
import time
import os
import io

# for Raspberry Pi
import RPi.GPIO as GPIO
import picamera

GPIO.cleanup()

#for Raspberry Pi
GPIO.setmode(GPIO.BCM)
port1 = 17 # gu
port2 = 27 # choki
port3 = 22 # pa

GPIO.setup(port1, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
GPIO.setup(port2, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
GPIO.setup(port3, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)

count = 1
flag = 0

# File remove
file_list = glob.glob('./data/0_gu/*')
for file in file_list:
    os.remove(file)
file_list = glob.glob('./1_choki/*')
for file in file_list:
    os.remove(file)
file_list = glob.glob('./data/2_pa/*')
for file in file_list:
    os.remove(file)

print('Ready!')

try:
    while True:
        sc = str(count)
        ssc = sc.zfill(4)
        #GPIOの17,27,22がオンになったら、画像を取り込んで認識を開始
        if GPIO.input(port1):
            label = '0_gu'
            flag = 1
        elif GPIO.input(port2):
            label = '1_choki'
            flag = 1
        elif GPIO.input(port3):
            label = '2_pa'
            flag = 1
            
        if flag ==1 :
            print(ssc + ':' + label)
            with picamera.PiCamera() as camera:
                    camera.resolution = (12896)
                    camera.start_preview()
                    camera.capture('./data/'+label+'/'+label+ssc+'.jpg')
            count +=1
            flag = 0

        time.sleep(0.01)

except KeyboardInterrupt:
    GPIO.cleanup()

「2_train.py」


#!/usr/bin/env python
# -*- coding: utf-8 -*-

import os
import sys
import numpy as np
import tensorflow as tf
import matplotlib.pyplot as plt

import keras
from keras.utils import np_utils
from keras.models import Sequential
from keras.layers import Dense,Dropout,Flatten
from keras.layers import Conv2D,MaxPooling2D
from keras.preprocessing.image import array_to_img,img_to_array,load_img
from keras import backend as K
from sklearn.model_selection import train_test_split
from keras.models import load_model
from keras.callbacks import ModelCheckpoint

# ======= hypter param =====
batch_size = 10
epochs = 50
# ==========================

path=os.getcwd()+'/data/'

class_count = 0
folder_list=os.listdir(path)

for folder in folder_list:

  class_count = class_count+1

NUM_CLASSES = class_count
IMAGE_SIZE = 28

# Loss
def plot_history_loss(fit):
    # Plot the loss in the history
    axL.plot(fit.history['loss'],label="loss for training")
    axL.plot(fit.history['val_loss'],label="loss for validation")
    axL.set_title('model loss')
    axL.set_xlabel('epoch')
    axL.set_ylabel('loss')
    axL.legend(bbox_to_anchor=(10), loc='lower right'borderaxespad=1fontsize=10)

# Accurascy
def plot_history_acc(fit):
    # Plot the loss in the history
    axR.plot(fit.history['acc'],label="loss for training")
    axR.plot(fit.history['val_acc'],label="loss for validation")
    axR.set_title('model accuracy')
    axR.set_xlabel('epoch')
    axR.set_ylabel('accuracy')
    axR.legend(bbox_to_anchor=(11), loc='upper right'borderaxespad=1fontsize=10)

if __name__ == '__main__':

    count=0
    folder_list = sorted(os.listdir(path))

    train_image = []
    train_label = []
    test_image = []
    test_label = []
    X = []
    Y = []

    label = 'label.txt'
    
    f = open(label, 'w')
    for folder in folder_list:
        subfolder = os.path.join(path,folder)
        file_list = sorted(os.listdir(subfolder))

        filemax = 0

        i = 0

        for file in file_list:

            i = i + 1

            img = img_to_array(load_img('./data/' + folder + '/' + file,target_size=(28,28)))
            X.append(img)
            Y.append(count)
        
        label_name = folder + ' ' + str(count) + '\n'
        f.write(label_name)

        count +=1

    X = np.asarray(X)
    Y = np.asarray(Y)
    X = X.astype('float32')
    X = X / 255.0

    Y = np_utils.to_categorical(Y, NUM_CLASSES)

    train_image, test_image, train_label, test_label = train_test_split(X,Y,test_size=0.20)
    
    f.close()
    print(u'画像読み込み終了')

    input_shape = (IMAGE_SIZE, IMAGE_SIZE, 3)

    model = Sequential()
    model.add(Conv2D(32,kernel_size=(3,3),
                     activation='relu',
                     padding='same'
                     input_shape=input_shape))
    model.add(MaxPooling2D(pool_size=(2,2)))

    model.add(Conv2D(64, (3,3), activation='relu'padding='same'))
    model.add(MaxPooling2D(pool_size=(2,2)))

    model.add(Flatten())
    model.add(Dense(512activation='relu'))
    model.add(Dropout(0.5))
    model.add(Dense(NUM_CLASSES, activation='softmax'))

    model.compile(loss=keras.losses.categorical_crossentropy,
                  optimizer=keras.optimizers.Adadelta(),
                  metrics=['accuracy']
                  )

    chkpt = './model_28.h5'
    cp_cb = ModelCheckpoint(filepath = chkpt, monitor='val_loss'verbose=1
                            save_best_only=Truemode='auto')

    history = model.fit(train_image, train_label,
              batch_size=batch_size,
              epochs=epochs,
              verbose=1,
              validation_data=(test_image, test_label),
              callbacks=[cp_cb],
              )

    model.summary()

    score = model.evaluate(test_image, test_label, verbose=0)

    fig, (axL, axR) = plt.subplots(ncols=2figsize=(10,4))

    plot_history_loss(history)
    plot_history_acc(history)

    fig.savefig('./loss_acc.png')
    plt.close()

「3_atdashi.py」


#!/usr/bin/env python

import os
import sys
import numpy as np
import tensorflow as tf

import keras
from keras.utils import np_utils
from keras.models import Sequential
from keras.layers import Dense,Dropout,Flatten
from keras.layers import Conv2D,MaxPooling2D
from keras.preprocessing.image import array_to_img,img_to_array,load_img
from keras import backend as K
from sklearn.model_selection import train_test_split
from keras.models import load_model
import time

import RPi.GPIO as GPIO
import picamera

i = 0
label_name = []

label = 'label.txt'

f = open(FLAGS.label,'r')
for line in f:
  line = line.rstrip()
  l = line.rstrip()
  label_name.append(l)
  i = i + 1

NUM_CLASSES = i
IMAGE_SIZE = 28

if __name__ == '__main__':
    test_image = []
    
    # model read
    model = load_model('./model_28.h5')
    model.summary()
    
    # for Raspberry Pi

    GPIO.cleanup()

    #for Raspberry Pi
    GPIO.setmode(GPIO.BCM)
    port1 = 24 # switch

    GPIO.setup(port1, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)

    print('Ready!')
    try:
        while True:
            if GPIO.input(port1):
                with picamera.PiCamera() as camera:
                    camera.resolution = (12896)
                    camera.start_preview()
                    camera.capture('./tmp.jpg')
                
                img = img_to_array(load_img('./tmp.jpg' , target_size=(28,28)))
                test_image.append(img)
                test_image = np.asarray(test_image)
                test_image = test_image.astype('float32')
                test_image = test_image / 255.0
    
                predictions = model.predict_classes(test_image)
    
                print(label_name[predictions[0]], u'です。')
                
                test_image = []
            
            time.sleep(0.01)

    except KeyboardInterrupt:
        GPIO.cleanup()

事前に、TensorFlow、Keras、numpyなどのライブラリをpip3コマンドで取り込んでおいてください。

(なお、TensorFlowは1.14、kerasは2.2.4 を推奨)

で、Raspberry Piの回路図は以下の通り。4つのタクトスイッチを使います。

Atdasi01

ちょっとわかりにくいですが、各タクトスイッチにつながる線で、黒は「GPIO24」、青は「GPIO17」、黄は「GPIO27」、緑は「GPIO22」、そして赤は「3.3V」につながってます。

あ、Raspberry Pi用のカメラも当然、つないでおきます。

で、先の3つのプログラムコードを、一つのフォルダに入れます。

Atdashi02

また、同じフォルダ内に「data」という名前のフォルダを作り

Atdashi03

その下に、上のような「グー」「チョキ」「パー」に当たる3種類のフォルダを作っておきます。

Img_0159

で、我が家のRaspberry Piで組んでみました。

Img_0164

こんな感じに、4つのタクトスイッチとつないでます。

なお、カメラは5インチ液晶の裏に両面テープで張り付けておきました。

これで、準備完了です。

(1) グー、チョキ、パー をラズパイカメラで撮影して記録 (各30枚づつくらい)

まず、自分の手を使って、「グー」「チョキ」「パー」の手を集めます。

Raspberry Pi上でターミナルを開き、

> python3 1_camera.py

と入力し、実行。

ターミナル上に「Ready!」と表示されたら、準備完了です。撮影を開始します。

Raspberry Piのカメラの前で「グー」「チョキ」「パー」の3種類の手を構えて、先の回路図にあるボタン「グー」「チョキ」「パー」で、手にあったものを選んで押します。

Img_0162

すると以下のような感じに、各フォルダ内に画像がたまっていきます。

Atdashi04

各ラベルごとに、20~30枚づつくらい撮影してください。

なお、合計が100枚を超えると、Raspberry Pi 3B+ではメモリーが飛ぶっぽいです(うちは飛びました)。

(2) 上の写真を用いて深層学習(CNN)を実施

(1)で教師データができたので、学習を実行します。同じフォルダ内で、

> python3 2_train.py

と実行します。

デフォルトでは、50エポックほど流れます。大体5分くらいで終わります。

なお、この50エポック内の最良のモデルのみが保存される仕組みになっているため、学習が終わればそのまま(3)へと移行します。

が、その前に、学習の状況を確認しておきましょう。

作業フォルダー中に、「loss_acc.png」という画像ファイルができているはずです。

Loss_acc

その名の通り、Loss(損失)値とAccuracy(精度)値のグラフが出てきます。Loss値は0に近いほど、Accuracyは1に近いほど、高精度なモデルができている目安になります。

が、よく見るとどちらも2色あります。これは、学習用データでの検証値(Train)と、評価用データでの検証値(Val)となります。

簡単に言うと、上の画像のようにLoss、Accuracy共に、この両者がほぼ同じところに収束していれば、学習としてはまずまずです。

これがTrainとValとが離れていると、いわゆる”過学習”と呼ばれる状態になります。その場合は汎用性が落ちているので、パラメータを変更するなり、(1)からやり直すなりして、再学習する必要が出てきます。

(例えば、batchの値を10→20に変えてみる 等)

単純に、何も変えずにもう一度実行するだけでよくなることもあります。何度実行しても改善されないときのみ、パラメータをいじるか、あるいは写真の撮り直しを実行してみてください。

グラフを確かめてから、(3)へ行きます。

(3) できたモデルを使い、カメラで撮影した「手」を推論する

ここでは、「3_atodashi.py」を使います。

> python3 3_atodashi.py

と実行したのち、

Img_0163

こんな感じに、カメラの前で手を構えます。

もし、写真のようにチョキを出しているときに、プロンプト上で

 > 2_choki

と表示されたら、推論成功です。

グーやパーだと勘違いしていたら、学習に失敗している可能性があります。

(1)か、(2)あたりからやり直してください。

あるいは、手が近すぎる or 遠すぎることもあります。何度か実験した経験での話ですが、ちょっと動かしてみると、精度が上がる距離がどこかにあることが多いです。

さて、画像認識としてはここまでで終了ですが、これではまだ「後出しじゃんけん機」ではありませんね。

(4) 推論結果に対し、勝つ手の画像を表示する

Raspberry Piに後出しじゃんけんをさせるために、「3_atodashi.py」を以下のコードに置き換えます。


#!/usr/bin/env python

import os
import sys
import numpy as np
import tensorflow as tf
import cv2

import keras
from keras.utils import np_utils
from keras.models import Sequential
from keras.layers import Dense,Dropout,Flatten
from keras.layers import Conv2D,MaxPooling2D
from keras.preprocessing.image import array_to_img,img_to_array,load_img
from keras import backend as K
from sklearn.model_selection import train_test_split
from keras.models import load_model
import time

import RPi.GPIO as GPIO
import picamera

i = 0
label_name = []

label = 'label.txt'

f = open(label,'r')
for line in f:
  line = line.rstrip()
  l = line.rstrip()
  label_name.append(l)
  i = i + 1

NUM_CLASSES = i
IMAGE_SIZE = 28

if __name__ == '__main__':
    test_image = []
    
    # model read
    model = load_model('model_28.h5')
    model.summary()
    
    # for Raspberry Pi

    GPIO.cleanup()

    #for Raspberry Pi
    GPIO.setmode(GPIO.BCM)
    port1 = 24 # switch

    GPIO.setup(port1, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)

    print('Ready!')
    try:
        while True:
            if GPIO.input(port1):
                with picamera.PiCamera() as camera:
                    camera.resolution = (12896)
                    camera.start_preview()
                    camera.capture('./tmp.jpg')
                
                img = img_to_array(load_img('./tmp.jpg' , target_size=(28,28)))
                test_image.append(img)
                test_image = np.asarray(test_image)
                test_image = test_image.astype('float32')
                test_image = test_image / 255.0
    
                predictions = model.predict_classes(test_image)
    
                print(label_name[predictions[0]], u'です。')

                #後出し処理
                if predictions[0] == 0# 「グー」だった時
                    reac = "pa.png" # 「パー」を返す
                elif predictions[0] == 1# 「チョキ」だった時
                    reac = "gu.png" # 「グー」を返す
                else# 残り、すなわち「パー」だった時
                    reac = "choki.png" # 「チョキ」を返す

                img = cv2.imread(reac)
                cv2.imshow("reaction",img)
                key = cv2.waitKey(1000
                cv2.destroyAllWindows()
                
                test_image = []
            
            time.sleep(0.01)

    except KeyboardInterrupt:
        GPIO.cleanup()

「#後出し処理」というコメント行がある後ろ当たりに、その後出しじゃんけんの処理が追加されてます。

また、画像表示にはOpenCVを用いてますので、pip3コマンドでOpenCVをインストールしておいてください。

> pip3 install python-opencv

で、このほかに画像が3枚、つまり、グー、チョキ、パーの画像をそろえる必要があります。

私はいらすとやで落としましたが、なんでもいいです。

Atodashi05

こんな感じに「グー」「チョキ」「パー」それぞれ「gu.png」「choki.png」「pa.png」として、Raspberry Pi上のプログラムコードの入った同じフォルダに入れておいてください。

で、この状態で(3)と同様に動かすと、推論の直後に、Raspberry Piが勝つと思う手を表示してきます。

Img_0205

こんな具合です。

(3)でそこそこの精度が出るモデルであれば、正しい「勝ち手」を出してくれるはずですね。

はい、以上が「後出しじゃんけん機」を作るまで、です。

(2)で使う「Train.py」という学習用コードを読めばわかる通り、28×28の画像で学習、推論させてます。この解像度じゃないと、Raspberry Piでは学習できません。

かなり低い解像度ですが、案外この解像度でも、じゃんけんくらいは認識できるようです。

もっとも、一筋縄ではいかないところもありますね。背景に依存しちゃったり、あるいは過学習で現実の手をうまく認識しなかったり……その辺りは、トライアンドエラーで実験してみるのがよいかと思います。

今回入れてませんが、Grad‐Camを使ったりして、モデルの確からしさを調べるのもいいですね。実際、会社にある同じコードでは、Grad-Camを組み込んでます。

どちらかというと、画像認識の理屈よりも、それを実際に使ってみてノウハウっぽいものを学ぶというのが狙いのプログラムコード。やってみると、思ったよりも精度は出ることもあるし、思い通りにいかないこともあります。

もちろん、ここに書いただけでは不十分な話が多いです。パラメータまで含めたら、一介のブログ記事では書ききれないほどいろいろあります。

このほか、精度におけるカメラと手の距離の依存性が高いので、HC-SR04などの距離センサーと組み合わせて撮影させてみるなど、電子工作の題材にもぴったりですね。

とまあ、我ながらうまい仕掛けを作ったものだと思っていたんですが。

これを作り上げた後に、「人気ブロガーからあげ先生のとにかく楽しいAI自作教室」(日経BP)という本にも、同じようにじゃんけんの手を学習させるというコードがあることが判明。

で、Kindle版で購入してみてみたんですが……あちらはRaspberry Pi上ではなく、しかも画像セットはあらかじめ準備されたものを使用、かつ、Google Colab上で動かすというものでした。いやはや、幸いにもこちらのやってることとは、かぶってませんでしたね。

まあ、似たようなことは皆さん、考えるものですね。

ちなみにこの本、画像だけでなく、自然言語など、幅広いカテゴリーの話が載っていたりと、なかなか面白い本です。買って損はありません。

著者のからあげ氏のブログは、Raspberry Pi関係のコードではとてもお世話になってますね。私もよく、参考にさせてもらってます。

以上、「画像認識を体で覚える」仕組みを作ってみた、というお話でした。


人気ブロガーからあげ先生のとにかく楽しいAI自作教室

より以前の記事一覧

当ブログ内検索

スポンサード リンク

ブログ村

無料ブログはココログ