2024年10月8日

【テンプレートマッチング】ベランダで栽培している野菜の収穫判定システムを作ってみた

  

「うわ、また巨大化してる!」

きゅうりを育てていると葉っぱに隠れてしまっていて収穫時期を逃すことが多々あります。

そこで大きくなったきゅうりを判別できるようなシステムを作ろう!と思い立ちました。

今回は画像処理において、対象の画像(テンプレート)を他の画像の中から探し出す手法であるテンプレートマッチングを使用して、ベランダのきゅうりの自動収穫度判定システムを作成してみました。

使用したアルゴリズム

使用したアルゴリズムはZNCCです。

ZNCC(Zero-mean Normalized Cross-Correlation、ゼロ平均正規化相互相関)は、画像のテンプレートマッチングにおいて、二つの画像間の類似度を評価するための手法です。
ZNCCは、特に照明条件の違いなどに強く、テンプレートと入力画像の局所的な明るさの変動に対して頑健という特徴があります。

ZNCCは、比較画像間で類似度(相関)が高いほど1に近い値を返し、類似度が低いほど-1に近い値を返します。

今回ZNCCを選択した理由は、きゅうりが外にあるので
1、局所的な明るさの変動に頑健であること
2、OpenCVで容易に実装ができること
だからです。

工夫したこと

テンプレート画像を複数用意した

テンプレートとして比較するきゅうりの画像数が多ければ多いほど検知できるきゅうりは多くなるので、画像を複数用意しました。

今回は、いろいろな角度から撮ったきゅうりの画像と画像生成AIによって作成したきゅうりの画像をそれぞれ3枚ずつ用意しました。
見る角度によってはツルなどが映ることを考慮し、テンプレート画像にもツルが映るようなきゅうりの画像を用意しました。

上手くテンプレート画像とマッチングさせるために3点工夫しました。

実際のきゅうりの画像から切り抜いたテンプレート画像(3枚結合版)
画像生成AIにより作成したきゅうりの画像(2枚結合版)

テンプレート画像を回転させた

実際の画像に映るきゅうりが斜めっていることがあるため、テンプレートとなるきゅうりの画像を回転させ、斜めになっているきゅうりに対して対応させるようにしました。

実装したプログラムでは、テンプレートとなるきゅうりの画像を-10~10°を1°刻みで回転させるようにしています。

テンプレート画像のスケールを調整した

画像の大きさによってはテンプレートとなるきゅうりの画像が大きすぎたり小さすぎたりすることがあります。
そのため、テンプレート画像のスケールを調整できるように実装しました。

今回は、テンプレート画像を10%、20%、30%、40%にしたものを使用しています。

結果

類似度のしきい値(=0.50、0.55、0.58、0.60)4つとテンプレート画像(=実際の画像、AIの画像、両方)の組み合わせで12種類の実行結果を以下に示します。

類似度のしきい値を高く設定すればするほど、テンプレート画像と似ている場所しか検知されないので、検知される領域は狭まっていきます。

しきい値=0.500.550.580.60
実際の画像検知不可
AIの画像検知不可
両方検知不可
①実際の画像、しきい値=0.50
②実際の画像、しきい値=0.55
③実際の画像、しきい値=0.58
④AIの画像、しきい値=0.50
⑤AI画像 しきい値=0.55
⑥AI画像、しきい値=0.58
⑦両方、しきい値=0.50
⑧両方、しきい値=0.55
⑨両方、しきい値=0.58

AIの画像よりも、実際の画像から切り抜いた画像をテンプレート画像としている時の方が良い結果が得られています。
ただ、精度としてはイマイチなものになってしまっています。

課題

しきい値の調整が大変

類似度のしきい値をかなりシビアに設定する必要があります。
結果から分かるように、しきい値の値が少しずれただけで、全く検知されなかったり、逆に多く検知されすぎたりしてしまいます。

良いテンプレート画像を用意するのが難しい

実際の環境にはまっすぐなきゅうりだけではなく、曲がっているきゅうり、葉っぱに隠れているきゅうりなどが存在しているので、それら全てを網羅するようなきゅうりの画像を用意するのは大変です。

また、テンプレートとなる画像に電柱が写っていたりすると、入力画像で電柱を検知してしまったりします。

その対策として背景に電柱などが映り込まないAI画像を使用するのは良いアイデアなように思いました。

実行時間がかかりすぎる

テンプレート画像の数を増やす、回転角を増やす、スケールの幅を増やせば増やすほど実行時間がどんどん長くなっていきます。

今回はテンプレート画像が多くても6枚でしたが、テンプレート画像が100枚とかになると実行時間もそれだけ増えるので問題になってきそうです。

作成コード

import cv2
import numpy as np
import glob
import os

def rotate_image(image, angle):
    """画像を指定された角度で回転させる"""
    center = (image.shape[1] // 2, image.shape[0] // 2)
    rot_matrix = cv2.getRotationMatrix2D(center, angle, 1.0)
    rotated_image = cv2.warpAffine(image, rot_matrix, (image.shape[1], image.shape[0]))
    return rotated_image

def scale_image(image, scale):
    """画像を指定されたスケールで拡大・縮小する"""
    width = int(image.shape[1] * scale)
    height = int(image.shape[0] * scale)
    scaled_image = cv2.resize(image, (width, height))
    return scaled_image

def zncc(template, image):
    """テンプレートと画像のZNCCを計算"""
    template = (template - np.mean(template)) / np.std(template)
    image = (image - np.mean(image)) / np.std(image)
    result = cv2.matchTemplate(image.astype(np.float32), template.astype(np.float32), cv2.TM_CCORR_NORMED)
    return result

# テンプレート画像を読み込む
template_files = glob.glob('template_img/template*.JPG')
templates = [cv2.imread(template_file, cv2.IMREAD_GRAYSCALE) for template_file in template_files]

# テンプレート画像が読み込めたか確認
for template_file, template in zip(template_files, templates):
    if template is None:
        raise FileNotFoundError(f"Template image {template_file} could not be read.")

# 畑の画像の読み込み
image = cv2.imread('input_img/field.JPG', cv2.IMREAD_GRAYSCALE)
image_color = cv2.imread('input_img/field.JPG')
if image is None:
    raise FileNotFoundError("Field image could not be read.")

# 回転角度のリスト
angles = range(-10, 10, 1)  # -10度から10度まで1度刻みで回転
# スケールのリスト
scales = [0.1, 0.2, 0.3, 0.4]  # テンプレートを10%、20%、30%、40%にスケール

# しきい値の設定
threshold = 0.50

# 各テンプレート、回転角度、スケールに対してZNCCを計算
detections = []

for template_index, template in enumerate(templates):
    for scale in scales:
        scaled_template = scale_image(template, scale)
        h, w = scaled_template.shape
        for angle in angles:
            rotated_template = rotate_image(scaled_template, angle)
            result = zncc(rotated_template, image)
            
            # しきい値を超える場所を見つける
            loc = np.where(result >= threshold)
            for pt in zip(*loc[::-1]):  # Y座標、X座標を入れ替える
                if not any(abs(pt[0] - d[0][0]) <= 2 and abs(pt[1] - d[0][1]) <= 2 for d in detections):
                    detections.append((pt, (w, h)))  # 座標とテンプレートサイズを保存
        print(len(detections))

if detections:
    print(f"収穫可能なきゅうりが{len(detections)}個見つかりました")
    for (top_left, (w, h)) in detections:
        bottom_right = (top_left[0] + w, top_left[1] + h)
        cv2.rectangle(image_color, top_left, bottom_right, (0, 0, 255), 5)  # 見つけた領域を赤の矩形で囲む
    
    # outputフォルダを作成(存在しない場合)
    output_dir = 'output'
    if not os.path.exists(output_dir):
        os.makedirs(output_dir)
    
    # 画像を保存
    output_path = os.path.join(output_dir, 'detected_cucumbers.jpg')
    cv2.imwrite(output_path, image_color)
    print(f"検出された画像を {output_path} に保存しました")
else:
    print("収穫可能なきゅうりは見つかりませんでした")

改善点

テンプレートマッチできゅうりの収穫判定プログラムを作成してみましたが、精度としては満足いく結果は得られませんでした。

次回は、機械学習なんかも組み合わせていきたいと思います。