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

【Raspberry Pi】電子工作入門④~OpenCVを使った画像処理の基礎~【Python】

前回はこちら

Pi Cameraで撮影した画像をWebページに表示するところまで行いました。
今回はその続きとして、Pythonの強力な画像処理ライブラリ OpenCV を使い、
以下の処理を実際に体験していきます👇

  • 📷 Pi Camera で撮影した画像を Python で読み込む
  • 🖼 画像をグレースケールに変換する
  • 🧭 二値化(白と黒だけの画像)を行う
  • ✍️ エッジ検出(輪郭を抽出)を行う
  • 📈 加工結果をウィンドウで表示する

これらは画像認識の基礎となる重要な処理です。
例えば物体検出や形状認識などは、これらの前処理を組み合わせて行われます。

OpenCVのインストール

OpenCV(Open Source Computer Vision Library) は、
カメラ画像や写真、動画をコンピュータで処理・解析するための非常に有名なライブラリです。

  • OpenCVは「画像を数値の配列(=行列)」として扱います。
  • 画像処理・物体検出・顔認識・特徴抽出など、幅広い分野で使われています。
  • 研究だけでなく、ロボット制御、監視カメラ、スマホアプリなどにも実用例がたくさんあります。

Python では cv2 という名前で利用します。

aspberry Pi 5 には標準では OpenCV が入っていません。

まずは一度だけ、ターミナルで以下のコマンドを実行してインストールします

sudo apt update
sudo apt install -y python3-opencv

インストールが完了したら、Thonnyで以下を実行して正しくインポートできるか確認しましょう

import cv2
print("OpenCV version:", cv2.__version__)

エラーが出ずにバージョンが表示されればOKです。

OpenCVでカメラ映像を表示する

まずは Pi Camera で写真を撮影し、それを OpenCV で読み込んで表示してみます。

📄 opencv_test.py

from picamera2 import Picamera2
import cv2

def show_small(winname, img, width=480):
    """画像を小さく表示する(低解像度ディスプレイ対応)"""
    h, w = img.shape[:2]
    scale = width / w
    resized = cv2.resize(img, (width, int(h * scale)))
    cv2.imshow(winname, resized)

def main():
    # PiCamera2 オブジェクトを作成
    picam2 = Picamera2()

    # カメラの解像度を 640×360 に設定(軽量で学習向き)
    picam2.configure(picam2.create_preview_configuration(main={"size": (640, 360)}))

    # カメラ起動
    picam2.start()

    while True:
        # フレームをRGBで取得
        rgb = picam2.capture_array()

        # OpenCV用にRGB → BGRへ変換
        bgr = cv2.cvtColor(rgb, cv2.COLOR_RGB2BGR)

        # 小さくして表示
        show_small("Original (BGR)", bgr)

        # キー入力チェック
        key = cv2.waitKey(1) & 0xFF
        if key == ord('s'):
            # 画像を保存
            cv2.imwrite("original_640.jpg", bgr)
            print("Saved: original_640.jpg")
        if key == ord('q'):
            # 終了
            break

    # ウィンドウとカメラを閉じる
    cv2.destroyAllWindows()
    picam2.stop()

if __name__ == "__main__":
    main()

PiCamera2 → RGB

OpenCV → BGR
この2つの色順の違いに注意しましょう。変換には cv2.cvtColor() を使います。

グレースケール変換をしてみよう

カラー画像はRGB(赤・緑・青)の3チャンネルで構成されています。

一方、グレースケール画像は1チャンネルで明るさだけを表します。

画像処理では、このグレースケールに変換することでデータを簡素化し、処理を軽くすることが多いです。

from picamera2 import Picamera2
import cv2

def show_small(winname, img, width=480):
    """画像を横幅480pxで縮小して表示する"""
    h, w = img.shape[:2]
    scale = width / w
    resized = cv2.resize(img, (width, int(h * scale)))
    cv2.imshow(winname, resized)

def main():
    picam2 = Picamera2()
    # 解像度を640x360に設定
    picam2.configure(picam2.create_preview_configuration(main={"size": (640, 360)}))
    picam2.start()

    while True:
        # フレーム取得(RGB)
        rgb = picam2.capture_array()
        # OpenCV用にBGRへ変換
        bgr = cv2.cvtColor(rgb, cv2.COLOR_RGB2BGR)
        # BGR → グレースケール(1チャンネル)
        gray = cv2.cvtColor(bgr, cv2.COLOR_BGR2GRAY)

        # 表示(コンパクトサイズ)
        show_small("Original (BGR)", bgr)
        show_small("Gray", gray)

        # キー操作
        key = cv2.waitKey(1) & 0xFF
        if key == ord('s'):
            # 画像保存
            cv2.imwrite("gray_original_640.jpg", bgr)
            cv2.imwrite("gray_gray_640.jpg", gray)
            print("Saved: gray_*")
        if key == ord('q'):
            break

    cv2.destroyAllWindows()
    picam2.stop()

if __name__ == "__main__":
    main()

カラー画像に比べて、白黒のシンプルな画像が表示されます。

これは OpenCV が内部で各ピクセルの明るさを計算して変換しています。

二値化(しきい値処理)

グレースケール画像をさらに単純化したものが「二値化」です。
指定した「しきい値」を境にして、ピクセルを「白」または「黒」に振り分けます👇

  • 明るさ ≥ しきい値 → 白(255)
  • 明るさ < しきい値 → 黒(0)

しきい値の処理にはいくつかあります。

方法特徴
手動しきい値自分で値を決める(簡単だが環境依存)
Otsu(二値化+大津の手法)画像のヒストグラムから自動で最適なしきい値を決める
適応的しきい値明暗ムラや影がある環境に強い。周囲の明るさを見て局所的に判断する

3種類の処理方法を以下のコードで比べてみます。

from picamera2 import Picamera2
import cv2
import numpy as np

def show_small(winname, img, width=480):
    """画像を小さく表示(低解像度ディスプレイ対応)"""
    h, w = img.shape[:2]
    scale = width / w
    resized = cv2.resize(img, (width, int(h * scale)))
    cv2.imshow(winname, resized)

def nothing(x): pass

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

    # 手動しきい値を変えられるスライダーウィンドウ
    cv2.namedWindow("Controls", cv2.WINDOW_NORMAL)
    cv2.createTrackbar("Manual TH", "Controls", 128, 255, nothing)

    while True:
        # 入力画像取得と変換
        rgb = picam2.capture_array()
        bgr = cv2.cvtColor(rgb, cv2.COLOR_RGB2BGR)
        gray = cv2.cvtColor(bgr, cv2.COLOR_BGR2GRAY)

        # 手動しきい値(二値化)
        th = cv2.getTrackbarPos("Manual TH", "Controls")
        _, bin_manual = cv2.threshold(gray, th, 255, cv2.THRESH_BINARY)

        # Otsu(二値化+自動しきい値決定)
        _, bin_otsu = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)

        # 適応的二値化(局所的に判断)
        bin_adp = cv2.adaptiveThreshold(
            gray, 255,
            cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY,
            11, 2
        )

        # 小さく表示
        show_small("Original", bgr)
        show_small("Manual", bin_manual)
        show_small("Otsu", bin_otsu)
        show_small("Adaptive", bin_adp)

        # キー入力
        key = cv2.waitKey(1) & 0xFF
        if key == ord('s'):
            # 結果を保存
            cv2.imwrite("bin_manual_640.jpg", bin_manual)
            cv2.imwrite("bin_otsu_640.jpg", bin_otsu)
            cv2.imwrite("bin_adaptive_640.jpg", bin_adp)
            print("Saved: bin_*")
        if key == ord('q'):
            break

    cv2.destroyAllWindows()
    picam2.stop()

if __name__ == "__main__":
    main()

エッジ検出(輪郭抽出)

エッジとは

エッジとは、画像の中で明るさが急に変わる「境界線」のことです。
エッジ検出は、物体の輪郭を見つけるための基本的な手法です。

Canny法の流れ(4ステップ)

  1. ノイズ除去(ガウシアンぼかし)
  2. 勾配(明るさの変化)を計算
  3. 細線化
  4. 二つのしきい値によるヒステリシス処理(弱いエッジをつなぐ)

from picamera2 import Picamera2
import cv2

def show_small(winname, img, width=480):
    """画像を小さくして表示"""
    h, w = img.shape[:2]
    scale = width / w
    resized = cv2.resize(img, (width, int(h * scale)))
    cv2.imshow(winname, resized)

def nothing(x): pass

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

    # Cannyのしきい値を調整するためのスライダー
    cv2.namedWindow("Controls", cv2.WINDOW_NORMAL)
    cv2.createTrackbar("Low", "Controls", 50, 255, nothing)
    cv2.createTrackbar("High", "Controls", 150, 255, nothing)

    while True:
        # 入力と変換
        rgb = picam2.capture_array()
        bgr = cv2.cvtColor(rgb, cv2.COLOR_RGB2BGR)
        gray = cv2.cvtColor(bgr, cv2.COLOR_BGR2GRAY)

        # ぼかし(ノイズ低減)
        blur = cv2.GaussianBlur(gray, (5,5), 1.0)

        # スライダー値でしきい値を調整
        low = cv2.getTrackbarPos("Low", "Controls")
        high = cv2.getTrackbarPos("High", "Controls")
        high = max(high, low + 1)  # 高しきい値は低より大きく

        # Cannyエッジ検出
        edges = cv2.Canny(blur, low, high)

        # 縮小して表示
        show_small("Original", bgr)
        show_small("Blurred", blur)
        show_small("Edges", edges)

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

    cv2.destroyAllWindows()
    picam2.stop()

if __name__ == "__main__":
    main()

画像の輪郭部分だけが白い線で抽出されて表示されます。

この処理は、ロボットビジョンや物体検出の基本中の基本です。

問題7

カメラで撮影した映像に白い物体(例えば白いブロックや紙切れ)が映っているとします。
このとき、画像処理を使って「白い物体がいくつあるか」自動で数えるプログラムを作ってください。

条件

  • Raspberry Pi 5 とカメラ(640×360)を使用する
  • OpenCV を使って画像処理を行う
  • 影や明るさのムラがあっても正しく数えられるようにする
  • 画面上に「検出結果(赤い枠)」と「数」を表示する

たとえば下のようなシーンを想定します👇

カメラ画像例二値化+輪郭抽出例
白い紙の上に白いブロック白い物体に赤い枠、個数が数えられている

この課題を解くためには、第2回で学んだ以下の処理を組み合わせることがポイントです

技術目的関数
グレースケール変換カラーを明るさ情報だけにするcv2.cvtColor
平滑化(ぼかし)ノイズを減らして処理を安定させるcv2.GaussianBlur
適応的二値化照明ムラや影に強く白と黒に分けるcv2.adaptiveThreshold
形態学的処理(オープニング)小さなノイズを消すcv2.morphologyEx
輪郭抽出白い物体を検出するcv2.findContours
面積フィルタ+描画小さいノイズを除外、結果をわかりやすく表示cv2.contourArea, cv2.drawContours, cv2.boundingRect

ヒント1:

カメラ画像をグレースケール化 → 適応的二値化してみてください。
→ 白い物体が白、背景が黒になるように調整。

※なぜ「適応的二値化」を使うのか?

普通のしきい値(128など)だと、影や光のムラで一部が黒くなって検出漏れが出ることがあります。
一方、適応的二値化は「周囲の明るさを見て」しきい値を決めるので、照明が均一でなくても安定して対象物を抽出できます。

ヒント2:

二値化結果に小さなノイズが残る場合は、

  • GaussianBlur() で事前にぼかす
  • または morphologyEx(..., MORPH_OPEN) で小さいゴミを消す
    → 二値画像がきれいになると輪郭抽出の精度が上がる。

※ぼかし(GaussianBlur)や形態学的処理(オープニング)を加えると、検出漏れや誤カウントがぐっと減ります。
二値化だけで輪郭を取ると、小さいゴミが大量に数えられてしまうことが多いので注意です。

ヒント3:

cv2.findContours() で輪郭を抽出しよう。
小さなノイズまでカウントしないように、cv2.contourArea() で面積をチェックして除外する。

輪郭を取ったあと、小さい面積の輪郭を無視することで誤検出を防ぎます。
これはロボットビジョンでも非常によく使われる基本テクニックです。

ヒント4:

cv2.drawContours()cv2.rectangle() で検出結果を可視化し、
cv2.putText() で物体数を画面に表示するとわかりやすい。

画像認識用のモデルは必要に応じて以下の画像をダウンロードし、画面に表示させ、それをカメラで読み込むなどして対応してください。

うまく認識できれば以下のようになります。

回答例はこちら
from picamera2 import Picamera2
import cv2
import numpy as np
import time

def show_small(winname, img, width=480):
    """ウィンドウを横幅widthに縮小して表示(低解像度ディスプレイ用)"""
    h, w = img.shape[:2]
    scale = width / w
    resized = cv2.resize(img, (width, int(h * scale)))
    cv2.imshow(winname, resized)

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)

        # --------------------------------------------------------
        # ② ぼかし(GaussianBlur)
        # --------------------------------------------------------
        blur_ksize = 5   # 奇数(3,5,7...)。大きくするとノイズは減るが細かい形が消える
        blur_sigma = 1.0 # ぼかし強さ。大きいほどエッジがぼやける
        blur = cv2.GaussianBlur(gray, (blur_ksize, blur_ksize), blur_sigma)
        # この処理でノイズを軽減することで、後の二値化が安定する

        # --------------------------------------------------------
        # ③ 適応的二値化(Adaptive Threshold)
        # --------------------------------------------------------
        adp_block_size = 11  # 近傍領域サイズ(奇数)大きいほど広域を考慮
        adp_C = 2            # 補正値(しきい値から引かれる定数)
        binary = cv2.adaptiveThreshold(
            blur, 255,
            cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY,
            adp_block_size, adp_C
        )
        # block_size↑:照明ムラに強くなるが細部が潰れやすい
        # C↑:全体的に暗めのしきい値になり、黒が増える(白い物体が減る)

        # --------------------------------------------------------
        # ④ 形態学的処理(オープニング=小ノイズ除去)
        # --------------------------------------------------------
        morph_kernel_size = 3  # 小さいノイズだけ消したいときは3〜5程度
        morph_iter = 1         # 反復回数。多いほど強力だが細い物体も消える
        kernel = np.ones((morph_kernel_size, morph_kernel_size), np.uint8)
        opened = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel, iterations=morph_iter)
        # 白い点ノイズを除去。小さなブツブツが減って輪郭抽出が安定する

        # --------------------------------------------------------
        # ⑤ 輪郭抽出と面積フィルタ
        # --------------------------------------------------------
        contour_mode = cv2.RETR_EXTERNAL  # 最外輪郭のみ抽出
        contours, _ = cv2.findContours(opened, contour_mode, cv2.CHAIN_APPROX_SIMPLE)

        min_area = 50  # 輪郭の最小面積。小さすぎるものはゴミとみなして除外
        count = 0
        vis = bgr.copy()
        for c in contours:
            area = cv2.contourArea(c)
            if area < min_area:
                continue
            count += 1
            # 輪郭と矩形を描画
            cv2.drawContours(vis, [c], -1, (0, 0, 255), 2)
            x, y, w, h = cv2.boundingRect(c)
            cv2.rectangle(vis, (x, y), (x+w, y+h), (0, 255, 0), 1)

        # --------------------------------------------------------
        # ⑥ 結果の表示
        # --------------------------------------------------------
        cv2.putText(vis, f"Count: {count}", (10, 30),
                    cv2.FONT_HERSHEY_SIMPLEX, 1.0, (255, 255, 0), 2)
        show_small("Binary (opened)", opened, width=480)
        show_small("Count Objects", vis, width=480)

        # 終了処理
        key = cv2.waitKey(1) & 0xFF
        if key == ord('q'):
            break

    cv2.destroyAllWindows()
    picam2.stop()

if __name__ == "__main__":
    main()

うまくできた場合は様々な背景の色で自然に白色の物体が検出できるように考えてみてください。

ほかの色の物体を検出するにはどうしたらよいでしょうか?

考えてみてください。

次回はこちら

Comming Soon

Follow me!

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

PAGE TOP