新4年生用

【Python入門4】OpenCVでそれっぽいことしてみる

はじめに

このテキストは、Python初心者むけのOpenCVライブラリを使った画像処理・コンピュータビジョンの基礎講座です。新大学4年生を対象とし、C言語の基礎知識はあるものの、Pythonはほとんど触れたことがない方向けです。Windows 11環境のSpyderを使用して、PCに搭載されたウェブカメラを活用した実践的な内容となっています。

前回はOpenCVの基本的な画像処理とカメラの制御を取り扱いました。

今回はより実践向けの内容として、以下の目標を設定して取り組みます。今回もすべて覚えようとせず、こういうことが出来るということだけでも覚えておいてもらうだけで、後々役に立つと思います。

学習目標

  • ウェブカメラからのリアルタイム画像処理ができる
  • 学んだ知識を応用した小規模プロジェクトが実装できる

所要時間: 約2時間
必要環境:

  • Windows 11
  • Spyder (Python IDE)
  • ウェブカメラ

目次

  1. 特徴検出と認識
    • エッジ検出
    • 輪郭検出
    • 顔検出
  2. 応用プロジェクト
    • リアルタイム顔認識フィルター
    • モーション検出

特徴検出と認識

エッジ検出

画像からエッジ(輪郭線)を検出する方法を学びます。

import cv2
import numpy as np
import matplotlib.pyplot as plt

# 画像の読み込み
img = cv2.imread('sample.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# 各種エッジ検出手法
# 1. Canny法
canny = cv2.Canny(gray, 100, 200)

# 2. Sobelフィルタ
sobelx = cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize=3)
sobely = cv2.Sobel(gray, cv2.CV_64F, 0, 1, ksize=3)
sobelx = np.absolute(sobelx)
sobely = np.absolute(sobely)
sobel = np.uint8(np.sqrt(sobelx**2 + sobely**2))

# 3. Laplacianフィルタ
laplacian = cv2.Laplacian(gray, cv2.CV_64F)
laplacian = np.uint8(np.absolute(laplacian))

# 表示
plt.figure(figsize=(12, 8))

plt.subplot(221)
plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
plt.title('Original')
plt.axis('off')

plt.subplot(222)
plt.imshow(canny, cmap='gray')
plt.title('Canny Edge Detection')
plt.axis('off')

plt.subplot(223)
plt.imshow(sobel, cmap='gray')
plt.title('Sobel Edge Detection')
plt.axis('off')

plt.subplot(224)
plt.imshow(laplacian, cmap='gray')
plt.title('Laplacian Edge Detection')
plt.axis('off')

plt.tight_layout()
plt.show()

輪郭検出

画像から輪郭を検出し、描画します。

import cv2
import numpy as np
import matplotlib.pyplot as plt

# 画像の読み込み
img = cv2.imread('sample.jpg')
img_copy = img.copy()

# グレースケール変換
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# 二値化
_, thresh = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)

# 輪郭検出
contours, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

print(f"検出された輪郭の数: {len(contours)}")

# 最初の10個の輪郭を描画
cv2.drawContours(img_copy, contours[:10], -1, (0, 255, 0), 3)

# 面積でフィルタリングして大きな輪郭のみ描画
img_large = img.copy()
for i, contour in enumerate(contours):
    area = cv2.contourArea(contour)
    if area > 500:  # 面積が500ピクセル以上の輪郭のみ
        cv2.drawContours(img_large, [contour], 0, (0, 0, 255), 2)

# 表示
plt.figure(figsize=(12, 8))

plt.subplot(221)
plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
plt.title('Original')
plt.axis('off')

plt.subplot(222)
plt.imshow(thresh, cmap='gray')
plt.title('Thresholded')
plt.axis('off')

plt.subplot(223)
plt.imshow(cv2.cvtColor(img_copy, cv2.COLOR_BGR2RGB))
plt.title('All Contours (first 10)')
plt.axis('off')

plt.subplot(224)
plt.imshow(cv2.cvtColor(img_large, cv2.COLOR_BGR2RGB))
plt.title('Large Contours Only')
plt.axis('off')

plt.tight_layout()
plt.show()

顔検出

OpenCVの顔検出機能を使って、画像から顔を検出します。

import cv2
import matplotlib.pyplot as plt

# 画像の読み込み
img = cv2.imread('sample.jpg')
img_copy = img.copy()

# グレースケール変換
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# 顔検出器の読み込み
face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_default.xml')

# 顔検出
faces = face_cascade.detectMultiScale(gray, 1.1, 4)

print(f"検出された顔の数: {len(faces)}")

# 検出された顔に四角形を描画
for (x, y, w, h) in faces:
    cv2.rectangle(img_copy, (x, y), (x+w, y+h), (255, 0, 0), 2)

# 表示
plt.figure(figsize=(12, 6))

plt.subplot(121)
plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
plt.title('Original')
plt.axis('off')

plt.subplot(122)
plt.imshow(cv2.cvtColor(img_copy, cv2.COLOR_BGR2RGB))
plt.title('Face Detection')
plt.axis('off')

plt.tight_layout()
plt.show()

ウェブカメラを使用したリアルタイム顔検出

ウェブカメラを使用して、リアルタイムで顔検出を行います。

import cv2

# 顔検出器の読み込み
face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_default.xml')
eye_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_eye.xml')

# カメラをキャプチャ
cap = cv2.VideoCapture(0)

if not cap.isOpened():
    print("カメラを開けませんでした")
    exit()

print("リアルタイム顔検出のデモです。終了するには 'q' キーを押してください。")

while True:
    # フレームを取得
    ret, frame = cap.read()
    
    if not ret:
        print("フレームを取得できませんでした")
        break
    
    # グレースケール変換
    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    
    # 顔検出
    faces = face_cascade.detectMultiScale(gray, 1.1, 4)
    
    # 検出された顔に四角形を描画
    for (x, y, w, h) in faces:
        cv2.rectangle(frame, (x, y), (x+w, y+h), (255, 0, 0), 2)
        
        # 顔領域内で目を検出
        roi_gray = gray[y:y+h, x:x+w]
        roi_color = frame[y:y+h, x:x+w]
        eyes = eye_cascade.detectMultiScale(roi_gray)
        for (ex, ey, ew, eh) in eyes:
            cv2.rectangle(roi_color, (ex, ey), (ex+ew, ey+eh), (0, 255, 0), 2)
    
    # 検出された顔の数を表示
    cv2.putText(frame, f"Faces: {len(faces)}", (10, 30), 
                cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)
    
    # フレームを表示
    cv2.imshow('Face Detection', frame)
    
    # 'q'キーが押されたら終了
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

# リソースを解放
cap.release()
cv2.destroyAllWindows()

6. 応用プロジェクト

リアルタイム顔認識フィルター

顔検出に基づいてフィルターを適用するアプリケーションを作成します。

import cv2
import numpy as np

def apply_glasses(frame, x, y, w, h):
    """顔に仮想メガネを追加"""
    # メガネの位置とサイズを調整(目の辺りに配置)
    glasses_y = y + int(h/4)
    glasses_height = int(h/3)
    glasses_width = w
    
    # 青色の四角形でメガネを表現
    cv2.rectangle(frame, (x, glasses_y), (x + glasses_width, glasses_y + glasses_height), 
                 (255, 0, 0), 2)
    # メガネの橋の部分
    cv2.rectangle(frame, (x + int(w/4), glasses_y), (x + int(3*w/4), glasses_y + int(glasses_height/3)), 
                 (255, 0, 0), 2)
    
    return frame

def apply_hat(frame, x, y, w, h):
    """顔に仮想帽子を追加"""
    # 帽子の位置とサイズを調整(頭の上に配置)
    hat_width = int(1.2 * w)
    hat_height = int(0.4 * h)
    hat_x = x - int((hat_width - w) / 2)
    hat_y = y - hat_height
    
    # 赤色の円形で帽子を表現
    cv2.ellipse(frame, (hat_x + int(hat_width/2), hat_y + int(hat_height/2)), 
               (int(hat_width/2), hat_height), 0, 0, 360, (0, 0, 255), -1)
    
    return frame

def apply_mustache(frame, x, y, w, h):
    """顔に仮想口ひげを追加"""
    # 口ひげの位置とサイズを調整(鼻の下あたりに配置)
    mustache_y = y + int(2*h/3)
    mustache_width = int(w/2)
    mustache_height = int(h/10)
    mustache_x = x + int(w/4)
    
    # 黒色の長方形で口ひげを表現
    cv2.rectangle(frame, (mustache_x, mustache_y), 
                 (mustache_x + mustache_width, mustache_y + mustache_height), 
                 (0, 0, 0), -1)
    
    return frame

# 顔検出器の読み込み
face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_default.xml')

# カメラをキャプチャ
cap = cv2.VideoCapture(0)

if not cap.isOpened():
    print("カメラを開けませんでした")
    exit()

print("顔認識フィルターのデモです。終了するには 'q' キーを押してください。")
print("フィルター切替: '1'=通常, '2'=メガネ, '3'=帽子, '4'=口ひげ, '5'=全部")

mode = 1  # 初期モードは通常表示

while True:
    # フレームを取得
    ret, frame = cap.read()
    
    if not ret:
        print("フレームを取得できませんでした")
        break
    
    # グレースケール変換
    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    
    # 顔検出
    faces = face_cascade.detectMultiScale(gray, 1.1, 4)
    
    # 検出された顔にフィルターを適用
    for (x, y, w, h) in faces:
        # 常に顔の枠を表示
        cv2.rectangle(frame, (x, y), (x+w, y+h), (255, 255, 0), 2)
        
        # モードに応じてフィルターを適用
        if mode == 2 or mode == 5:  # メガネ
            frame = apply_glasses(frame, x, y, w, h)
        if mode == 3 or mode == 5:  # 帽子
            frame = apply_hat(frame, x, y, w, h)
        if mode == 4 or mode == 5:  # 口ひげ
            frame = apply_mustache(frame, x, y, w, h)
    
    # 現在のモードを表示
    mode_text = f"Mode: {mode}"
    cv2.putText(frame, mode_text, (10, 30), 
                cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2)
    
    # フレームを表示
    cv2.imshow('Face Filters', frame)
    
    # キー入力をチェック
    key = cv2.waitKey(1) & 0xFF
    
    # 'q'で終了
    if key == ord('q'):
        break
    # '1'~'5'でモード切替
    elif key >= ord('1') and key <= ord('5'):
        mode = key - ord('0')  # '1'->1, '2'->2, ...

# リソースを解放
cap.release()
cv2.destroyAllWindows()

モーション検出

動きを検出して追跡するシステムを作成します。

import cv2
import numpy as np

# カメラをキャプチャ
cap = cv2.VideoCapture(0)

if not cap.isOpened():
    print("カメラを開けませんでした")
    exit()

print("モーション検出のデモです。終了するには 'q' キーを押してください。")
print("感度調整: '+' または '-' キーで調整")

# 最初のフレームを取得
ret, first_frame = cap.read()
if not ret:
    print("フレームを取得できませんでした")
    exit()

# 最初のフレームをグレースケールに変換してぼかし
prev_gray = cv2.cvtColor(first_frame, cv2.COLOR_BGR2GRAY)
prev_gray = cv2.GaussianBlur(prev_gray, (21, 21), 0)

# モーション検出の閾値(感度調整用)
threshold = 25
min_area = 500

while True:
    # フレームを取得
    ret, frame = cap.read()
    
    if not ret:
        print("フレームを取得できませんでした")
        break
    
    # フレームをグレースケールに変換してぼかし
    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    gray = cv2.GaussianBlur(gray, (21, 21), 0)
    
    # 現在のフレームと前フレームの差分を計算
    frame_diff = cv2.absdiff(prev_gray, gray)
    
    # 差分を二値化
    thresh = cv2.threshold(frame_diff, threshold, 255, cv2.THRESH_BINARY)
    
    # ノイズ除去のためのモルフォロジー演算
    thresh = cv2.dilate(thresh, None, iterations=2)
    
    # 輪郭を検出
    contours, _ = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    
    # 動きのある領域に四角形を描画
    motion_detected = False
    for contour in contours:
        if cv2.contourArea(contour) < min_area:
            continue
        
        motion_detected = True
        (x, y, w, h) = cv2.boundingRect(contour)
        cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2)
    
    # 動き検出の状態を表示
    status = "Motion Detected" if motion_detected else "No Motion"
    cv2.putText(frame, f"Status: {status}", (10, 20), cv2.FONT_HERSHEY_SIMPLEX,
                0.7, (0, 0, 255) if motion_detected else (0, 255, 0), 2)
    cv2.putText(frame, f"Threshold: {threshold}", (10, 50), cv2.FONT_HERSHEY_SIMPLEX,
                0.7, (0, 255, 0), 2)
    
    # フレームを表示
    cv2.imshow("Motion Detection", frame)
    # cv2.imshow("Threshold", thresh)  # デバッグ用
    
    # 現在のフレームを次のループのために保存
    prev_gray = gray
    
    # キー入力をチェック
    key = cv2.waitKey(1) & 0xFF
    
    # 'q'で終了
    if key == ord('q'):
        break
    # '+'で感度を上げる(閾値を下げる)
    elif key == ord('+') or key == ord('='):
        threshold = max(threshold - 1, 1)
    # '-'で感度を下げる(閾値を上げる)
    elif key == ord('-'):
        threshold = min(threshold + 1, 100)

# リソースを解放
cap.release()
cv2.destroyAllWindows()

最終プロジェクト: マルチモード画像処理アプリケーション

これまで学んだ知識を組み合わせた総合アプリケーションを作成します。

import cv2
import numpy as np
import time
import os

class ImageProcessingApp:
    def __init__(self):
        # 保存ディレクトリの作成
        self.save_dir = 'captured_images'
        if not os.path.exists(self.save_dir):
            os.makedirs(self.save_dir)
        
        # カメラのセットアップ
        self.cap = cv2.VideoCapture(0)
        if not self.cap.isOpened():
            raise Exception("カメラを開けませんでした")
        
        # 顔検出のセットアップ
        self.face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_default.xml')
        self.eye_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_eye.xml')
        
        # アプリケーションの状態
        self.mode = 0  # 0: 通常, 1: グレースケール, 2: エッジ検出, 3: 顔検出, 4: モーション検出, 5: カートゥーン
        self.modes = ["Normal", "Grayscale", "Edge Detection", "Face Detection", "Motion Detection", "Cartoon"]
        self.recording = False
        self.frames = []
        self.countdown = 0
        self.last_time = time.time()
        self.counter = 0
        
        # モーション検出用の変数
        self.prev_gray = None
        self.motion_threshold = 25
        self.min_area = 500
        
        print("マルチモード画像処理アプリケーションを起動しました")
        print("終了するには 'q' キーを押してください")
        print("モードの切り替え: '0'~'5'キー")
        print("写真撮影: 'p'キー (3秒カウントダウン)")
        print("動画録画: 'r'キー (開始/停止)")
        
    def process_frame(self, frame):
        """現在のモードに基づいてフレームを処理"""
        processed = frame.copy()
        
        if self.mode == 0:  # 通常
            return processed
        
        elif self.mode == 1:  # グレースケール
            gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
            return cv2.cvtColor(gray, cv2.COLOR_GRAY2BGR)  # 3チャンネルに戻す
        
        elif self.mode == 2:  # エッジ検出
            gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
            edges = cv2.Canny(gray, 100, 200)
            return cv2.cvtColor(edges, cv2.COLOR_GRAY2BGR)  # 3チャンネルに戻す
        
        elif self.mode == 3:  # 顔検出
            gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
            faces = self.face_cascade.detectMultiScale(gray, 1.1, 4)
            
            for (x, y, w, h) in faces:
                cv2.rectangle(processed, (x, y), (x+w, y+h), (255, 0, 0), 2)
                
                # 顔領域内で目を検出
                roi_gray = gray[y:y+h, x:x+w]
                roi_color = processed[y:y+h, x:x+w]
                eyes = self.eye_cascade.detectMultiScale(roi_gray)
                for (ex, ey, ew, eh) in eyes:
                    cv2.rectangle(roi_color, (ex, ey), (ex+ew, ey+eh), (0, 255, 0), 2)
            
            # 検出された顔の数を表示
            cv2.putText(processed, f"Faces: {len(faces)}", (10, 70), 
                        cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)
            
            return processed
        
        elif self.mode == 4:  # モーション検出
            gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
            gray = cv2.GaussianBlur(gray, (21, 21), 0)
            
            if self.prev_gray is None:
                self.prev_gray = gray
                return processed
            
            # 現在のフレームと前フレームの差分を計算
            frame_diff = cv2.absdiff(self.prev_gray, gray)
            
            # 差分を二値化
            thresh = cv2.threshold(frame_diff, self.motion_threshold, 255, cv2.THRESH_BINARY)
            
            # ノイズ除去のためのモルフォロジー演算
            thresh = cv2.dilate(thresh, None, iterations=2)
            
            # 輪郭を検出
            contours, _ = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
            
            # 動きのある領域に四角形を描画
            motion_detected = False
            for contour in contours:
                if cv2.contourArea(contour) < self.min_area:
                    continue
                
                motion_detected = True
                (x, y, w, h) = cv2.boundingRect(contour)
                cv2.rectangle(processed, (x, y), (x + w, y + h), (0, 255, 0), 2)
            
            # 動き検出の状態を表示
            status = "Motion Detected" if motion_detected else "No Motion"
            cv2.putText(processed, f"Status: {status}", (10, 70), cv2.FONT_HERSHEY_SIMPLEX,
                        0.7, (0, 0, 255) if motion_detected else (0, 255, 0), 2)
            
            # 現在のフレームを次のループのために保存
            self.prev_gray = gray
            
            return processed
        
        elif self.mode == 5:  # カートゥーン
            # エッジ検出
            gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
            gray = cv2.medianBlur(gray, 5)
            edges = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_MEAN_C, 
                                        cv2.THRESH_BINARY, 9, 9)
            
            # 色の量子化
            color = cv2.bilateralFilter(frame, 9, 300, 300)
            
            # エッジと色を組み合わせる
            cartoon = cv2.bitwise_and(color, color, mask=edges)
            
            return cartoon
    
    def run(self):
        """アプリケーションのメインループ"""
        while True:
            # フレームを取得
            ret, frame = self.cap.read()
            
            if not ret:
                print("フレームを取得できませんでした")
                break
            
            # 現在の時間
            current_time = time.time()
            
            # カウントダウン中の処理
            if self.countdown > 0:
                # 大きなカウントダウン表示
                cv2.putText(frame, str(self.countdown), 
                            (frame.shape//2 - 20, frame.shape[0]//2 + 20), 
                            cv2.FONT_HERSHEY_SIMPLEX, 3, (0, 255, 255), 5)
                
                # 1秒経過したらカウントダウンを減らす
                if current_time - self.last_time >= 1:
                    self.countdown -= 1
                    self.last_time = current_time
                    
                    # カウントダウンが0になったら撮影
                    if self.countdown == 0:
                        # 写真のファイル名
                        filename = os.path.join(self.save_dir, f'image_{self.counter}.jpg')
                        cv2.imwrite(filename, frame)
                        print(f"写真を保存しました: {filename}")
                        self.counter += 1
            
            # フレーム処理
            processed = self.process_frame(frame)
            
            # モード表示
            cv2.putText(processed, f"Mode: {self.modes[self.mode]}", (10, 30), 
                        cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2)
            
            # 録画中表示
            if self.recording:
                cv2.putText(processed, "REC", (processed.shape - 70, 30), 
                            cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2)
                self.frames.append(processed.copy())
            
            # フレームを表示
            cv2.imshow('Multi-mode Image Processing', processed)
            
            # キー入力をチェック
            key = cv2.waitKey(1) & 0xFF
            
            # 'q'で終了
            if key == ord('q'):
                break
            # '0'~'5'でモード切替
            elif key >= ord('0') and key <= ord('5'):
                self.mode = key - ord('0')
                print(f"モードを変更しました: {self.modes[self.mode]}")
                # モーション検出モードに切り替えた場合、前フレームをリセット
                if self.mode == 4:
                    self.prev_gray = None
            # 'p'で写真撮影(カウントダウン開始)
            elif key == ord('p') and self.

問題1: リアルタイムエッジ検出システム

ウェブカメラからのリアルタイム映像にエッジ検出技術を適用するプログラムを作成してください。以下の要件を満たすようにしてください:

  1. ウェブカメラからのリアルタイム映像を取得する
  2. キーボード入力で以下の3種類のエッジ検出手法を切り替えられるようにする:
    • ‘c’キー: Canny法
    • ‘s’キー: Sobelフィルタ
    • ‘l’キー: Laplacianフィルタ
    • ‘n’キー: 通常表示(エッジ検出なし)
  3. 元の映像とエッジ検出結果を左右に並べて表示する
  4. 現在のエッジ検出モードを画面に表示する
  5. ‘q’キーで終了できるようにする

ヒント: cv2.hstack()を使って、元の映像とエッジ検出結果を横に並べて表示できます。Sobelフィルタを使用する場合は、x方向とy方向の勾配を組み合わせることを忘れないでください。

解答例

import cv2
import numpy as np

def main():
    # カメラをキャプチャ
    cap = cv2.VideoCapture(0)  # カメラデバイスを開く

    if not cap.isOpened():
        print("カメラを開けませんでした")
        return

    # 初期モード設定
    mode = 'n'  # 'n'=通常, 'c'=Canny, 's'=Sobel, 'l'=Laplacian

    print("リアルタイムエッジ検出システム")
    print("モード切替: 'n'=通常, 'c'=Canny, 's'=Sobel, 'l'=Laplacian")
    print("終了するには 'q' キーを押してください")

    while True:
        # フレームを取得
        ret, frame = cap.read()  # カメラからフレームを読み込む

        if not ret:
            print("フレームを取得できませんでした")
            break

        # グレースケール変換
        gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)  # カラー画像をグレースケールに変換

        # 現在のモードに応じてエッジ検出を適用
        if mode == 'c':  # Canny法
            edges = cv2.Canny(gray, 100, 200)  # Cannyエッジ検出を適用
            mode_text = "Mode: Canny"
        elif mode == 's':  # Sobelフィルタ
            sobelx = cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize=3)  # x方向の勾配を計算
            sobely = cv2.Sobel(gray, cv2.CV_64F, 0, 1, ksize=3)  # y方向の勾配を計算
            sobelx = np.absolute(sobelx)  # 絶対値を取る
            sobely = np.absolute(sobely)  # 絶対値を取る
            edges = np.uint8(np.sqrt(sobelx**2 + sobely**2))  # 勾配の大きさを計算
            mode_text = "Mode: Sobel"
        elif mode == 'l':  # Laplacianフィルタ
            laplacian = cv2.Laplacian(gray, cv2.CV_64F)  # Laplacianフィルタを適用
            edges = np.uint8(np.absolute(laplacian))  # 絶対値を取りuint8に変換
            mode_text = "Mode: Laplacian"
        else:  # 通常表示
            edges = gray  # グレースケール画像をそのまま使用
            mode_text = "Mode: Normal"

        # エッジ検出結果を3チャンネルに変換(横に並べるため)
        edges_colored = cv2.cvtColor(edges, cv2.COLOR_GRAY2BGR)  # グレースケールを3チャンネルに変換

        # 元の画像とエッジ検出結果を横に並べる
        combined = np.hstack((frame, edges_colored))  # 画像を横に連結

        # モード表示
        cv2.putText(combined, mode_text, (10, 30), 
                    cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2)  # 現在のモードをテキスト表示

        # 結果表示
        cv2.imshow('Edge Detection', combined)  # 結合された画像を表示

        # キー入力をチェック
        key = cv2.waitKey(1) & 0xFF

        # 'q'キーで終了
        if key == ord('q'):
            break
        # モード切替
        elif key == ord('n') or key == ord('c') or key == ord('s') or key == ord('l'):
            mode = chr(key)  # 押されたキーに対応するモードに変更

    # リソースを解放
    cap.release()  # カメラをリリース
    cv2.destroyAllWindows()  # すべてのウィンドウを閉じる

if __name__ == "__main__":
    main()

解説

このプログラムは、ウェブカメラからリアルタイムで映像を取得し、様々なエッジ検出アルゴリズムを適用します。主な処理の流れは以下の通りです:

  1. cv2.VideoCapture(0)を使用してカメラを開きます
  2. while ループ内でフレームを継続的に取得します
  3. 取得したフレームをグレースケールに変換します
  4. キーボード入力に応じて異なるエッジ検出アルゴリズムを適用します:
    • Canny法は、2つの閾値を使用して強いエッジと弱いエッジを検出します
    • Sobelフィルタは、x方向とy方向の勾配を個別に計算し、ピタゴラスの定理を使って勾配の大きさを算出します
    • Laplacianフィルタは、2次微分を使ってエッジを検出します
  5. np.hstack()を使用して、元の画像とエッジ検出結果を横に並べて表示します
  6. cv2.putText()を使って、現在のモードを画面に表示します

このプログラムを実行すると、リアルタイムでエッジ検出の効果を確認でき、各アルゴリズムの特性の違いを視覚的に理解することができます。

問題2: モーション検出と顔認識の統合システム

ウェブカメラを使用して、モーション検出と顔認識を組み合わせたシステムを作成してください。以下の要件を満たすようにしてください:

  1. ウェブカメラからリアルタイム映像を取得する
  2. 画面内の動きを検出する(前のフレームとの差分を利用)
  3. 動きが検出された場合のみ、顔検出を実行する(計算リソースの節約のため)
  4. 検出された顔に赤色の四角形を描画する
  5. 動き検出の状態と検出された顔の数を画面に表示する
  6. 最後に顔が検出されてから5秒経過するとプログラムを終了する(または’q’キーで手動終了)

ヒント: モーション検出には cv2.absdiff() と cv2.threshold() の組み合わせが効果的です。また、time モジュールを使用して経過時間を計測できます。

解答例

import cv2
import numpy as np
import time

def main():
    # カメラをキャプチャ
    cap = cv2.VideoCapture(0)  # カメラデバイスを開く

    if not cap.isOpened():
        print("カメラを開けませんでした")
        return

    # 顔検出器を読み込み
    face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_default.xml')  # 顔検出用の分類器

    # モーション検出用のパラメータ
    threshold_value = 25  # 差分の閾値
    min_area = 500  # 動きと判断する最小領域サイズ

    # 初期フレームを取得
    ret, prev_frame = cap.read()  # 最初のフレームを読み込む
    if not ret:
        print("フレームを取得できませんでした")
        return

    prev_gray = cv2.cvtColor(prev_frame, cv2.COLOR_BGR2GRAY)  # グレースケールに変換
    prev_gray = cv2.GaussianBlur(prev_gray, (21, 21), 0)  # ぼかしを適用

    # 顔検出関連の変数
    last_face_time = time.time()  # 最後に顔を検出した時刻
    face_timeout = 5  # タイムアウト秒数

    print("モーション検出と顔認識の統合システム")
    print("終了条件: 顔が検出されない状態が5秒間続く、または 'q' キーを押す")

    while True:
        # 現在の時間
        current_time = time.time()

        # フレームを取得
        ret, frame = cap.read()  # カメラからフレームを読み込む

        if not ret:
            print("フレームを取得できませんでした")
            break

        # モーション検出
        # グレースケール変換とぼかし
        gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)  # カラー画像をグレースケールに変換
        gray = cv2.GaussianBlur(gray, (21, 21), 0)  # ノイズ低減のためにぼかしを適用

        # 前のフレームとの差分を計算
        frame_diff = cv2.absdiff(prev_gray, gray)  # 前フレームと現在のフレームの差分を計算

        # 差分を二値化
        thresh = cv2.threshold(frame_diff, threshold_value, 255, cv2.THRESH_BINARY)  # 閾値以上の差分を白(255)、それ以外を黒(0)にする

        # ノイズ除去のためのモルフォロジー演算
        thresh = cv2.dilate(thresh, None, iterations=2)  # 膨張処理でノイズを減らす

        # 輪郭を検出
        contours, _ = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)  # 二値化画像から輪郭を検出

        # 動きのある領域に緑色の四角形を描画
        motion_detected = False
        for contour in contours:
            if cv2.contourArea(contour) < min_area:  # 小さすぎる領域は無視
                continue

            motion_detected = True  # 動きありと判定
            (x, y, w, h) = cv2.boundingRect(contour)  # 検出された動きの領域を取得
            cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2)  # 緑色の四角形を描画

        # 動きが検出された場合のみ顔検出を実行
        num_faces = 0
        if motion_detected:
            faces = face_cascade.detectMultiScale(gray, 1.1, 4)  # 顔検出を実行

            num_faces = len(faces)  # 検出された顔の数

            # 顔が1つ以上検出された場合、最終検出時刻を更新
            if num_faces > 0:
                last_face_time = current_time  # 顔を検出した時刻を更新

            # 検出された顔に赤色の四角形を描画
            for (x, y, w, h) in faces:
                cv2.rectangle(frame, (x, y), (x+w, y+h), (0, 0, 255), 2)  # 赤色の四角形を描画

        # 動き検出と顔検出の状態を表示
        motion_status = "Motion: Detected" if motion_detected else "Motion: None"
        cv2.putText(frame, motion_status, (10, 30), 
                    cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2)  # 動き検出状態を表示

        cv2.putText(frame, f"Faces: {num_faces}", (10, 60), 
                    cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)  # 検出された顔の数を表示

        # 最後に顔が検出されてからの経過時間
        time_since_last_face = current_time - last_face_time  # 最後に顔を検出してからの経過秒数
        remaining_time = max(0, face_timeout - time_since_last_face)  # 残り時間

        cv2.putText(frame, f"Timeout in: {remaining_time:.1f}s", (10, 90), 
                    cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 0, 0), 2)  # 残り時間を表示

        # フレームを表示
        cv2.imshow('Motion and Face Detection', frame)  # 結果を表示

        # 現在のフレームを次のループのために保存
        prev_gray = gray  # 現在のフレームを前フレームとして保存

        # 最後に顔が検出されてから5秒経過したら終了
        if time_since_last_face >= face_timeout:
            print(f"{face_timeout}秒間顔が検出されなかったため、プログラムを終了します")
            break

        # 'q'キーで終了
        if cv2.waitKey(1) & 0xFF == ord('q'):
            break

    # リソースを解放
    cap.release()  # カメラをリリース
    cv2.destroyAllWindows()  # すべてのウィンドウを閉じる

if __name__ == "__main__":
    main()

解説

このプログラムは、モーション検出と顔認識を組み合わせたシステムを実装しています。主な処理の流れは以下の通りです:

  1. cv2.VideoCapture(0)を使用してカメラを開きます
  2. cv2.CascadeClassifier()を使用して顔検出器を初期化します
  3. 各フレームでモーション検出を行います:
    • 前のフレームと現在のフレームの差分を計算(cv2.absdiff())
    • 差分を二値化(cv2.threshold())
    • ノイズ除去のため膨張処理を適用(cv2.dilate())
    • 二値化画像から輪郭を検出(cv2.findContours())
    • 一定面積以上の輪郭を「動き」と判断
  4. 動きが検出された場合のみ、顔検出を実行します(計算リソースの節約)
  5. 検出された顔に赤色の四角形を描画します
  6. 最後に顔が検出された時刻を記録し、5秒間検出されない場合はプログラムを終了します

このプログラムは、リソース効率の良い監視システムのプロトタイプと考えることができます。動きがない場合は顔検出という重い処理を実行せず、動きがあった場合のみ顔検出を行うことで、CPUリソースを節約しています。また、一定時間顔が検出されない場合に自動終了する機能も実装しています。

問題3: 創造的なフィルターアプリケーション

テキストで学んだ知識を総合的に活用し、ウェブカメラを使った創造的なフィルターアプリケーションを作成してください。以下の要件を満たすようにしてください:

  1. ウェブカメラからリアルタイム映像を取得する
  2. 最低3種類の異なるフィルターを実装し、キーボード入力で切り替えられるようにする:
    • エッジ強調フィルター:エッジを検出して元の画像に重ねる
    • スケッチフィルター:鉛筆画風の効果を適用
    • 独自のフィルター:今まで学んだ技術を組み合わせたオリジナルのフィルター
  3. 顔が検出された場合は、適用しているフィルターに関わらず、顔の周りに特別なエフェクトを追加する
  4. 画面の隅に現在のフィルター名とフレームレート(FPS)を表示する
  5. ‘s’キーを押すと、現在表示されている画像を保存する機能を実装する

ヒント: スケッチフィルターは、エッジ検出結果を反転したものと考えることができます。フレームレートを計算するには、連続したフレーム処理の時間を計測してください。

解答例

import cv2
import numpy as np
import time
import os

def main():
    # 保存ディレクトリの作成
    save_dir = 'filtered_images'  # 画像保存用ディレクトリ
    if not os.path.exists(save_dir):
        os.makedirs(save_dir)  # ディレクトリがなければ作成

    # カメラをキャプチャ
    cap = cv2.VideoCapture(0)  # カメラデバイスを開く

    if not cap.isOpened():
        print("カメラを開けませんでした")
        return

    # 顔検出器の読み込み
    face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_default.xml')  # 顔検出用の分類器

    # 初期設定
    mode = 1  # 1:エッジ強調, 2:スケッチ, 3:オリジナル
    counter = 0  # 保存する画像の番号

    # FPS計算用の変数
    prev_time = time.time()  # 前回のフレーム処理時刻
    fps = 0  # フレームレート

    print("創造的なフィルターアプリケーション")
    print("モード切替: '1'=エッジ強調, '2'=スケッチ, '3'=オリジナル")
    print("'s'キーで画像を保存, 'q'キーで終了")

    while True:
        # フレーム処理の開始時刻
        start_time = time.time()  # 現在の処理開始時刻

        # フレームを取得
        ret, frame = cap.read()  # カメラからフレームを読み込む

        if not ret:
            print("フレームを取得できませんでした")
            break

        # フレームのコピーを作成
        result = frame.copy()  # 元のフレームをコピー

        # グレースケール変換
        gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)  # カラー画像をグレースケールに変換

        # 顔検出
        faces = face_cascade.detectMultiScale(gray, 1.1, 4)  # 顔検出を実行

        # モードに応じたフィルター処理
        if mode == 1:  # エッジ強調フィルター
            # Cannyエッジ検出
            edges = cv2.Canny(gray, 50, 150)  # エッジ検出

            # エッジを元の画像に重ねる
            result = frame.copy()  # 元のフレームをコピー
            result[edges == 255] = [0, 255, 255]  # エッジの部分を黄色に変更

            mode_name = "Edge Highlight"  # モード名

        elif mode == 2:  # スケッチフィルター
            # エッジ検出反転でスケッチ風に
            gray_blur = cv2.GaussianBlur(gray, (7, 7), 0)  # ぼかしを適用
            edges = cv2.Laplacian(gray_blur, cv2.CV_8U, ksize=5)  # ラプラシアンフィルタでエッジ検出

            # エッジの調整と反転
            ret, sketch = cv2.threshold(edges, 70, 255, cv2.THRESH_BINARY_INV)  # 閾値処理で白黒反転

            # グレースケールを3チャンネルに変換
            result = cv2.cvtColor(sketch, cv2.COLOR_GRAY2BGR)  # グレースケールを3チャンネルに変換

            mode_name = "Sketch"  # モード名

        elif mode == 3:  # オリジナルフィルター (カートゥーン風)
            # エッジ検出
            gray_blur = cv2.medianBlur(gray, 7)  # メディアンフィルタでぼかし
            edges = cv2.adaptiveThreshold(gray_blur, 255, cv2.ADAPTIVE_THRESH_MEAN_C, 
                                        cv2.THRESH_BINARY, 9, 9)  # 適応的閾値処理

            # 色の量子化
            color = cv2.bilateralFilter(frame, 9, 300, 300)  # バイラテラルフィルタで色を平滑化

            # エッジと色を組み合わせる
            result = cv2.bitwise_and(color, color, mask=edges)  # エッジをマスクとして色を組み合わせる

            mode_name = "Cartoon"  # モード名

        # 顔が検出された場合の特別なエフェクト
        for (x, y, w, h) in faces:
            # 顔の周りに光るような効果
            cv2.rectangle(result, (x, y), (x+w, y+h), (0, 255, 255), 2)  # 黄色の枠を描画

            # 顔の上に「笑顔」テキストを追加
            cv2.putText(result, "Smile!", (x, y - 10), 
                        cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0, 255, 255), 2)  # 顔の上にテキスト表示

            # 顔の周りに放射状のエフェクト(簡易版)
            center_x = x + w // 2  # 顔の中心x座標
            center_y = y + h // 2  # 顔の中心y座標
            radius = max(w, h) // 2  # 顔の半径

            # 放射状の円を描画
            cv2.circle(result, (center_x, center_y), radius + 10, (0, 255, 255), 2)  # 外側の円
            cv2.circle(result, (center_x, center_y), radius + 20, (0, 255, 255), 1)  # さらに外側の円

        # フレームレート(FPS)の計算
        end_time = time.time()  # 処理終了時刻
        process_time = end_time - start_time  # 1フレームの処理時間
        fps = 1.0 / process_time if process_time > 0 else 0  # フレームレートを計算

        # 情報表示
        cv2.putText(result, f"Filter: {mode_name}", (10, 30), 
                    cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2)  # フィルター名を表示

        cv2.putText(result, f"FPS: {fps:.1f}", (10, 60), 
                    cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2)  # フレームレートを表示

        # フレームを表示
        cv2.imshow('Creative Filters', result)  # 結果を表示

        # キー入力をチェック
        key = cv2.waitKey(1) & 0xFF

        # 'q'キーで終了
        if key == ord('q'):
            break
        # '1'~'3'キーでモード切替
        elif key >= ord('1') and key <= ord('3'):
            mode = key - ord('0')  # キーコードから数値に変換
        # 's'キーで画像を保存
        elif key == ord('s'):
            # 画像ファイル名
            filename = os.path.join(save_dir, f'{mode_name}_{counter}.jpg')  # 保存するファイル名を生成
            cv2.imwrite(filename, result)  # 画像を保存
            print(f"画像を保存しました: {filename}")  # 保存したことを通知
            counter += 1  # カウンターをインクリメント

    # リソースを解放
    cap.release()  # カメラをリリース
    cv2.destroyAllWindows()  # すべてのウィンドウを閉じる

if __name__ == "__main__":
    main()

解説

このプログラムは、ウェブカメラからのリアルタイム映像に様々なフィルターを適用し、顔検出機能を組み合わせた創造的なアプリケーションを実装しています。主な処理の流れは以下の通りです:

  1. カメラの初期化と顔検出器のセットアップを行います
  2. 複数のフィルタータイプを実装し、キーボード入力で切り替え可能にしています:
    • エッジ強調フィルター:Cannyエッジ検出の結果を元の画像に黄色で重ねます
    • スケッチフィルター:ラプラシアンフィルタと閾値処理で鉛筆画風の効果を実現します
    • カートゥーンフィルター:エッジ検出と色の量子化を組み合わせて漫画風の効果を作ります
  3. 顔検出を常に実行し、顔の周りに特別なエフェクト(黄色の枠、「Smile!」テキスト、放射状の円)を追加します
  4. フレームレート(FPS)を計算し、画面に表示します
  5. 's'キーを押すことで、現在表示されている画像を保存できるようにしています

このプログラムは、テキストで学んだ様々な画像処理技術を組み合わせています。特に:

  • エッジ検出技術(Canny、Laplacian)を使用してさまざまな視覚効果を作成
  • 顔検出を使用してインタラクティブなエフェクトを追加
  • フレームレート計算とファイル保存機能の実装

このようなアプリケーションは、ビデオチャットやストリーミングで使用されるフィルター効果の基本原理を理解するのに役立ちます。さらに拡張すれば、より複雑なフィルターやエフェクトを実装することも可能です。

まとめ

今回はOpenCVのライブラリを用いて顔の検出やモーションの検出など発展的な内容について学びました。

これらをマスターするのは難しいですが、こういうことできたなあ位の感覚でいてもらえたら、今後役に立つかと思います。