Python Raspbeyyi Pi プログラム 回路 電子工作 高専3年生

【Raspberry Pi】電子工作入門⑤~OpenCVの物体認識~【Python】

前回はOpenCV を使った基本的な画像処理を学びました。

いよいよ 画像の中から特定のものを見つける「物体検出」や「認識」 を体験します。

  • 🧭 画像処理と「特徴」を使ったシンプルな輪郭検出(Contour Detection)
  • 📐 形状認識(四角形・円形などを判別)
  • 🚶‍♂️ 物体認識の考え方(AIモデルを使う手前まで)
  • 📝 実際に Pi Camera で撮影した画像を使って輪郭や図形を検出する

OpenCVを使えば、特別なAIモデルを使わなくても、画像中から図形や領域を自動的に見つけることができます。
まずはこの「古典的な画像認識手法」から学んでいきましょう。

物体検出と認識の基本的な考え方

画像の中から物体を見つけるには、主に次のステップに従って行います

📷 撮影 → 画像処理(グレースケール・二値化など)
 ↓
🧭 特徴の抽出(輪郭・形状・色など)
 ↓
🔍 物体を検出(領域を見つける)
 ↓
🤖 認識(「これは○○だ」と分類する)

OpenCV では、機械学習を使わなくても「輪郭」や「形状」「色」などの情報から
シンプルな物体検出・認識を行うことができます。

本日は以下のような画像の形状を判別してみます。

輪郭の検出

まずは Pi Camera で撮影した画像を二値化し、輪郭線を抽出します。
OpenCV の findContours 関数を使うと、二値画像の白い領域の境界線を検出できます。

📄 contour_test.py

from picamera2 import Picamera2
import cv2
import time

def show_small(winname, img, width=640):
    h, w = img.shape[:2]
    scale = width / w
    cv2.imshow(winname, cv2.resize(img, (width, int(h * scale))))

def main():
    # ① カメラ初期化(640x360)
    picam2 = Picamera2()
    picam2.configure(picam2.create_preview_configuration(main={"size": (640, 360)}))
    picam2.start()
    time.sleep(0.2)

    while True:
        # ② フレーム取得 → グレースケール
        rgb = picam2.capture_array()
        bgr = cv2.cvtColor(rgb, cv2.COLOR_RGB2BGR)
        gray = cv2.cvtColor(bgr, cv2.COLOR_BGR2GRAY)

        # ③ 二値化:白背景×黒物体 → Otsu + 反転 で「物体=白」に
        _, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)

        # ④ 輪郭検出(最外輪郭のみ)
        contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

        # ⑤ 可視化(赤で輪郭描画)
        vis = bgr.copy()
        cv2.drawContours(vis, contours, -1, (0, 0, 255), 2)
        cv2.putText(vis, f"Contours: {len(contours)}", (10, 28),
                    cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 200, 255), 2)

        # ⑥ 表示(縮小表示は見やすさのため・処理には影響なし)
        show_small("Binary (INV+Otsu)", binary, 480)
        show_small("Detected Contours", vis, 640)

        # ⑦ 操作
        key = cv2.waitKey(1) & 0xFF
        if key == ord('s'):
            cv2.imwrite("contours_frame.jpg", vis)
            print("Saved: contours_frame.jpg")
        if key == ord('q'):
            break

    cv2.destroyAllWindows()
    picam2.stop()

if __name__ == "__main__":
    main()

画像中の物体(白い領域)の輪郭が赤線で表示されれば成功です。
この輪郭は図形の境界を表しており、位置や形状の認識に利用できます。

形状の認識

検出した輪郭をさらに解析することで、「これは四角形っぽい」「これは円形っぽい」など形状の判別を行うことができます。

OpenCVでは approxPolyDP を使って輪郭を多角形近似することで、辺の数を調べられます。

from picamera2 import Picamera2
import cv2
import numpy as np
import time

def show_small(winname, img, width=640):
    h, w = img.shape[:2]
    scale = width / w
    cv2.imshow(winname, cv2.resize(img, (width, int(h * scale))))

def classify_shape(cnt):
    """輪郭cntから図形名を返す。小さすぎる輪郭はNone"""
    area = cv2.contourArea(cnt)
    if area < 80:  # ノイズ除外(必要に応じ調整)
        return None, None

    peri = cv2.arcLength(cnt, True)
    approx = cv2.approxPolyDP(cnt, 0.02 * peri, True)  # 近似精度 0.02*周長
    v = len(approx)

    if v == 3:
        name = "Triangle"
    elif v == 4:
        # 正方形/長方形の簡易判定(外接矩形の縦横比)
        x, y, w, h = cv2.boundingRect(approx)
        aspect = w / float(h)
        name = "Square" if 0.90 <= aspect <= 1.10 else "Rectangle"
    else:
        # 円判定の補強:円形度(1に近いほど真円)
        circularity = 4.0 * np.pi * area / (peri * peri) if peri > 0 else 0
        name = "Circle" if circularity > 0.80 else "Polygon"

    return name, approx

def main():
    picam2 = Picamera2()
    picam2.configure(picam2.create_preview_configuration(main={"size": (640, 360)}))
    picam2.start()
    time.sleep(0.2)

    while True:
        # 入力 → Gray → (軽いぼかし)→ Otsu+INV で黒図形を白化
        rgb = picam2.capture_array()
        bgr = cv2.cvtColor(rgb, cv2.COLOR_RGB2BGR)
        gray = cv2.cvtColor(bgr, cv2.COLOR_BGR2GRAY)
        blur = cv2.GaussianBlur(gray, (3, 3), 0.8)
        _, binary = cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)

        # 輪郭(最外)
        contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

        # カウント
        counts = {"Triangle": 0, "Square": 0, "Rectangle": 0, "Circle": 0, "Polygon": 0}

        vis = bgr.copy()
        for c in contours:
            name, approx = classify_shape(c)
            if not name:
                continue
            counts[name] = counts.get(name, 0) + 1

            # 描画(輪郭&ラベル)
            cv2.drawContours(vis, [approx], -1, (0, 0, 255), 2)
            x, y, w, h = cv2.boundingRect(approx)
            cv2.putText(vis, name, (x, y - 6),
                        cv2.FONT_HERSHEY_SIMPLEX, 0.6, (50, 50, 255), 2)

        # 画面上部に集計表示
        summary = f"Tri:{counts['Triangle']}  Sq:{counts['Square']}  Rect:{counts['Rectangle']}  Cir:{counts['Circle']}"
        cv2.putText(vis, summary, (10, 28), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 180, 255), 2)

        # 表示
        show_small("Binary (INV+Otsu)", binary, 480)
        show_small("Shape Detection (Live)", vis, 640)

        key = cv2.waitKey(1) & 0xFF
        if key == ord('s'):
            cv2.imwrite("shape_live_frame.jpg", vis)
            print("Saved: shape_live_frame.jpg")
        if key == ord('q'):
            break

    cv2.destroyAllWindows()
    picam2.stop()

if __name__ == "__main__":
    main()

三角形・四角形・円形が検出できた場合、それぞれの形の名前が画像に描かれます。
この方法は、ロボットが床に置かれた目印(マーカー)の形を認識する用途などにも使えます。

物体検出と認識の応用

ここまで扱った方法は、「しきい値」「輪郭」「形の頂点数」といった画像処理ベースの手法です。
一方、近年の物体認識は「機械学習(AI)」による分類が主流です。

手法特徴向いている場面
画像処理ベース高速・シンプル。特定の形や色を検出できるロボットのマーカー検出、簡易分類など
AIベース(学習)柔軟で高精度。複雑な対象も分類可能人・モノの認識、複雑な背景の検出など

例えば、AIベースでは YOLO(You Only Look Once)や MobileNet などのモデルを使って
「犬」「猫」「人」などを判定できますが、今回はそこまで踏み込みません。
まずは「形を見つける」ことから始めるのが大切です。

問題8

現在のプログラムでは cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU を使って自動で二値化しています。

これをやめて、手動でしきい値を指定する方式に書き換えましょう。

cv2.createTrackbar を使い、リアルタイムでしきい値を変更しながら結果を観察できるようにしてください。

ヒント:

  • cv2.threshold(gray, value, 255, cv2.THRESH_BINARY_INV) を使う
  • value を Trackbar で変更できるようにする
  • 輪郭の数や認識結果がどのように変化するか観察する
回答例はこちら
from picamera2 import Picamera2
import cv2
import time

def show_small(winname, img, width=480):
    h, w = img.shape[:2]
    scale = width / w
    cv2.imshow(winname, cv2.resize(img, (width, int(h * scale))))

def nothing(x):
    pass

# カメラ初期化
picam2 = Picamera2()
picam2.configure(picam2.create_preview_configuration(main={"size": (640, 360)}))
picam2.start()
time.sleep(0.2)

# Trackbarウィンドウ作成
cv2.namedWindow("Controls")
cv2.createTrackbar("Threshold", "Controls", 100, 255, nothing)  # 初期値100、最大255

while True:
    # カメラ画像取得 → グレースケール化
    rgb = picam2.capture_array()
    bgr = cv2.cvtColor(rgb, cv2.COLOR_RGB2BGR)
    gray = cv2.cvtColor(bgr, cv2.COLOR_BGR2GRAY)

    # Trackbarの値を取得
    t = cv2.getTrackbarPos("Threshold", "Controls")

    # 手動しきい値で二値化(白背景×黒物体 → 反転する)
    _, binary = cv2.threshold(gray, t, 255, cv2.THRESH_BINARY_INV)

    # 輪郭検出
    contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

    vis = bgr.copy()
    cv2.drawContours(vis, contours, -1, (0, 0, 255), 2)
    cv2.putText(vis, f"Threshold={t}", (10, 30),
                cv2.FONT_HERSHEY_SIMPLEX, 0.8, (255, 255, 0), 2)

    show_small("Binary", binary)
    show_small("Contours", vis, 640)

    key = cv2.waitKey(1) & 0xFF
    if key == ord('q'):
        break

cv2.destroyAllWindows()
picam2.stop()
  • cv2.threshold() の第2引数に手動で設定した値を使っています。
  • Trackbarを動かして値を上下させると、背景と物体の二値化境界がリアルタイムで変わる様子が確認できます。
  • 明るい背景では適切なしきい値(例:100〜150)が必要ですが、暗くすると輪郭が欠けたりノイズが増えたりします。
  • Otsu法は最適値を自動で計算するのに対し、この課題ではその裏にある「しきい値による白黒分類の挙動」を体感できます。

問題9

輪郭を検出したあと、それぞれの図形について以下を表示してください:

  • 中心座標(x, y)
  • 面積(pixel数)
  • 図形名(Triangle, Square, Rectangle, Circle)

それらを図形の上に cv2.putText() で描画してください。

ヒント:

  • cv2.moments() を使うと重心座標を計算できます。
  • cv2.contourArea(cnt) で面積を求める
  • 面積が極端に小さい輪郭はノイズとして除外するとよい
M = cv2.moments(cnt)
cx = int(M["m10"]/M["m00"])
cy = int(M["m01"]/M["m00"])
解答例はこちら
from picamera2 import Picamera2
import cv2
import numpy as np
import time

def show_small(winname, img, width=480):
    h, w = img.shape[:2]
    cv2.imshow(winname, cv2.resize(img, (width, int(h * (width/w)))))

def main():
    picam2 = Picamera2()
    picam2.configure(picam2.create_preview_configuration(main={"size": (640, 360)}))
    picam2.start()
    time.sleep(0.2)

    while True:
        rgb = picam2.capture_array()
        bgr = cv2.cvtColor(rgb, cv2.COLOR_RGB2BGR)
        gray = cv2.cvtColor(bgr, cv2.COLOR_BGR2GRAY)

        # Otsu + 反転(二値化)
        _, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)

        contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

        vis = bgr.copy()
        for cnt in contours:
            area = cv2.contourArea(cnt)
            if area < 50:
                continue

            # 重心計算
            M = cv2.moments(cnt)
            if M["m00"] == 0:  # division by zero対策
                continue
            cx = int(M["m10"] / M["m00"])
            cy = int(M["m01"] / M["m00"])

            # 輪郭と情報表示
            cv2.drawContours(vis, [cnt], -1, (0, 0, 255), 2)
            text = f"Area:{int(area)} ({cx},{cy})"
            cv2.putText(vis, text, (cx - 40, cy - 10),
                        cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 0), 2)
            cv2.circle(vis, (cx, cy), 3, (0, 255, 0), -1)

        show_small("Area & Position", vis, 640)

        key = cv2.waitKey(1) & 0xFF
        if key == ord('q'):
            break

    cv2.destroyAllWindows()
    picam2.stop()

if __name__ == "__main__":
    main()
  • cv2.contourArea() → 図形のピクセル面積を取得
  • cv2.moments() → 輪郭のモーメントから重心(cx, cy)を計算
  • 小さなノイズ輪郭は面積でフィルタすることで安定した処理が可能になります。
  • 重心と面積を使うことで、「大きさ」や「位置」を数値で扱えるようになり、ロボット制御や物体追跡の基礎になります。

次回はこちら

Comming Soon

Follow me!

-Python, Raspbeyyi Pi, プログラム, 回路, 電子工作, 高専3年生
-, , , , ,

PAGE TOP