【YOLOとtiling処理の活用】Pythonで高精度な人物検知

Pythonの人物検知について学習するにあたり、人物検知の精度をどうしたら高められるかが気になりtiling処理にたどり着きました。

人物検知で有名？なYOLO(v8)では、YOLOに渡す画像のサイズは640x640に固定されています。どれだけ高精度なカメラで撮影してもサイズが固定されているために精度を上げるのには限界があります。そこでtiling処理の出番です。

---

### 1 YOLOとは何か？
#### 1-1 YOLOの基本概念
YOLO (You Only Look Once) は、物体検出アルゴリズムの一つで、一度の処理で画像全体を解析し、物体の位置とクラスを特定します。その名前の通り、一度のパスで物体検出を行うため、高速で効率的な検出が可能です。これにより、リアルタイムのビデオ処理や高速な画像解析が求められるアプリケーションに適しています。

#### 1-2 他の検出アルゴリズムとの比較
YOLOは、そのスピードと効率性から他の物体検出アルゴリズムと比較して優れた点があります。例えば、Faster R-CNNやSSD (Single Shot MultiBox Detector) と比較すると、YOLOはより高速でありながら、精度も高い水準を維持しています。しかし、複雑なシーンや小さな物体の検出においては、他のアルゴリズムに劣る場合もあります。

### 2 PythonでのYOLOの実装方法
#### 2-1 ライブラリの導入
PythonでYOLOを実装するためには、まず必要なライブラリを導入する必要があります。主なライブラリとしては、OpenCV、TensorFlow、Kerasなどがあります。これらのライブラリを利用することで、YOLOのモデルを簡単にロードし、画像やビデオの解析を行うことができます。

#### 2-2 コードその１（デフォルト処理）
以下に、PythonでYOLOを実装するための基本的なコード例を示します。

```python
from ultralytics import YOLO
import cv2
import matplotlib.pyplot as plt
from pathlib import Path

def process_images_in_folder(model, img_folder, conf_threshold):
    # 画像フォルダの設定
    img_folder = Path(img_folder)
    output_folder = Path("results")
    output_folder.mkdir(parents=True, exist_ok=True)

    # フォルダ内のすべての画像を処理
    for img_path in img_folder.glob("*.jpg"):  # jpgファイルを対象としていますが、他の拡張子も必要なら追加できます
        # 予測を実行
        results = model.predict(source=str(img_path), conf=conf_threshold, save=True)

        # 画像を読み込む
        img = cv2.imread(str(img_path))
        if img is None:
            print(f"Error: Could not read the image from {img_path}")
            continue

        # 検出結果を取得
        for result in results:
            for box in result.boxes:
                class_id = int(box.cls[0])
                if result.names[class_id] == 'person':
                    x0, y0, x1, y1 = map(int, box.xyxy[0])
                    conf = box.conf[0]

                    # バウンディングボックスとラベルの描画
                    cv2.rectangle(img, (x0, y0), (x1, y1), (0, 255, 0), 2)
                    label = f"person: {conf:.2f}"
                    cv2.putText(img, label, (x0, y0 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2)

        # 保存する画像のパスを設定
        output_image_path = output_folder / f"result_{img_path.stem}.jpg"
        cv2.imwrite(str(output_image_path), img)
        print(f"Result image saved to {output_image_path}")

        # 結果を表示（オプション）
        img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
        plt.imshow(img_rgb)
        plt.axis('off')
        plt.show()

def main():
    # モデルのパス
    model_path = 'yolov8n.pt'
    # 画像フォルダのパス
    img_folder = 'img'  # ここを変更
    # 信頼度の閾値
    conf_threshold = 0.6

    # モデルをロード
    model = YOLO(model_path)

    # フォルダ内の画像を処理
    process_images_in_folder(model, img_folder, conf_threshold)

if __name__ == '__main__':
    main()
```

#### 2-3 コードその１（デフォルト処理）実行結果
写真に写る人のサイズが小さすぎるため、人物を検知できていません。

![test02.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2958142/1bfdf274-1a8b-1437-4acd-6140e19efd80.jpeg)

#### 2-4 コードその2（tiling処理）
640x640のサイズに切り分けて推論を行い、最後に繋ぎ合わせる処理が追加されています。
信頼度やNMSなどの閾値は一旦気にしないで見ましょう。

```python
import cv2
import torch
import numpy as np
from loguru import logger
from pathlib import Path
from ultralytics import YOLO

def adjust_contrast_brightness(img, contrast=1.0, brightness=0):
    # コントラストと明るさを調整
    return cv2.convertScaleAbs(img, alpha=contrast, beta=brightness)

def split_image(img, tile_size, overlap):
    tiles = []
    img_h, img_w = img.shape[:2]
    step = tile_size - overlap
    for y in range(0, img_h, step):
        for x in range(0, img_w, step):
            tile = img[y:y+tile_size, x:x+tile_size]
            tiles.append((tile, x, y))
    return tiles

def process_tile(model, tile, conf_threshold):
    orig_h, orig_w = tile.shape[:2]

    # 推論
    results = model(tile, conf=conf_threshold)

    processed_results = []
    for result in results[0].boxes:
        if result.cls == 0:  # person class
            x0, y0, x1, y1 = result.xyxy[0]
            score = result.conf[0]
            processed_results.append((int(x0), int(y0), int(x1), int(y1), score))
    
    return processed_results

def apply_soft_nms(boxes, scores, sigma=0.5, thresh=0.001, iou_thresh=0.3):
    """Applies Soft-NMS."""
    keep = []
    N = len(boxes)
    for i in range(N):
        max_pos = i
        max_score = scores[i]

        for j in range(i + 1, N):
            if scores[j] > max_score:
                max_score = scores[j]
                max_pos = j

        boxes[i], boxes[max_pos] = boxes[max_pos], boxes[i]
        scores[i], scores[max_pos] = scores[max_pos], scores[i]

        keep.append(i)

        for j in range(i + 1, N):
            iou = compute_iou(boxes[i], boxes[j])

            if iou > iou_thresh:
                scores[j] *= np.exp(-(iou * iou) / sigma)

    keep = [k for k in keep if scores[k] >= thresh]

    return keep

def compute_iou(box1, box2):
    """Computes the IoU of two bounding boxes."""
    x1, y1, x2, y2 = box1
    x1_p, y1_p, x2_p, y2_p = box2

    xi1 = max(x1, x1_p)
    yi1 = max(y1, y1_p)
    xi2 = min(x2, x2_p)
    yi2 = min(y2, y2_p)

    inter_area = max(0, xi2 - xi1 + 1) * max(0, yi2 - yi1 + 1)

    box1_area = (x2 - x1 + 1) * (y2 - y1 + 1)
    box2_area = (x2_p - x1_p + 1) * (y2_p - y1_p + 1)

    iou = inter_area / float(box1_area + box2_area - inter_area)

    return iou

def process_image(model, img_path, output_dir, preprocessed_dir, conf_threshold, tile_size=640, overlap=320, iou_threshold=0.5):
    # 画像の読み込み
    img = cv2.imread(img_path)
    if img is None:
        logger.error(f"Error: Could not read the image from {img_path}")
        return

    # オリジナルの画像の寸法を保存
    orig_h, orig_w = img.shape[:2]

    # 画像のコントラストと明るさを調整
    img = adjust_contrast_brightness(img, contrast=1.0, brightness=0)

    # 画像をタイルに分割
    tiles = split_image(img, tile_size, overlap)
    
    # 各タイルを処理
    all_results = []
    tile_idx = 0
    for tile, x_offset, y_offset in tiles:
        # タイルを保存
        tile_filename = preprocessed_dir / f"{Path(img_path).stem}_tile_{tile_idx}.jpg"
        cv2.imwrite(str(tile_filename), tile)
        tile_idx += 1
        
        # タイルで推論
        results = process_tile(model, tile, conf_threshold)
        for x0, y0, x1, y1, score in results:
            all_results.append((x0 + x_offset, y0 + y_offset, x1 + x_offset, y1 + y_offset, score))

    # Soft NMSを適用して重複を除去
    if len(all_results) > 0:
        boxes = np.array([[r[0], r[1], r[2], r[3]] for r in all_results])
        scores = np.array([r[4] for r in all_results])
        keep = apply_soft_nms(boxes, scores, iou_thresh=iou_threshold)

        final_results = [all_results[i] for i in keep]
    else:
        final_results = []

    # 結果をオリジナル画像に描画
    for x0, y0, x1, y1, score in final_results:
        color = (0, 255, 0)
        text = f'person: {score:.1f}'
        img = cv2.rectangle(img, (x0, y0), (x1, y1), color, 2)
        img = cv2.putText(img, text, (x0, y0 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 2)

    # 結果画像の保存
    result_path = output_dir / f"result_{Path(img_path).stem}.jpg"
    cv2.imwrite(str(result_path), img)
    logger.info(f"Result image saved to {result_path}")

def main():
    # モデルのロード
    model = YOLO("yolov8n.pt")

    # 画像フォルダの設定
    img_dir = Path("img")  # 画像フォルダのパスを指定
    output_dir = Path("results")
    output_dir.mkdir(parents=True, exist_ok=True)
    
    preprocessed_dir = Path("preprocessed")
    preprocessed_dir.mkdir(parents=True, exist_ok=True)

    # 信頼度の閾値
    conf_threshold = 0.6

    # フォルダ内のすべての画像を処理
    for img_path in img_dir.glob("*.jpg"):  # jpgファイルを対象としていますが、他の拡張子も必要なら追加できます
        process_image(model, img_path, output_dir, preprocessed_dir, conf_threshold)

if __name__ == '__main__':
    main()

```

#### 2-5 コードその2（tiling処理）実行結果
tiling処理をしても人物が小さく検知できていない人が多いですが、デフォルトの処理に比べると明らかに検知率が上昇していることが分かります。

![result_test02.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2958142/28f001ec-e05f-32bc-d46e-3c5ed0b5fef1.jpeg)

#### 2-6 tiling処理された画像
処理をよく見てみるとtilingされた画像の範囲が重なっていることが分かります。
これはタイリングした画像の狭間にいる人が見切れて検知されなくなることを防ぐ処理です。

![素材集.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2958142/e021036d-3a48-c784-99de-566a348a7de9.png)

また、画面右側・下側のタイルは640x640でタイル化できなかったので画像が細くなっています。
このように細くなり指定したサイズに満たない場合は以下のように加工されて渡されます。

![切れ端.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2958142/0c995cbb-e0bf-a633-236a-6890cc28df53.png)

### 3 デフォルト処理の特徴
#### 3-1 デフォルト処理の利点
デフォルト処理は、設定が簡単であり、一般的なシナリオで適切な性能を発揮します。特に、小規模なプロジェクトや単純なデータセットにおいては、複雑な調整が不要であるため、迅速に導入できます。また、多くの既存のツールやライブラリがデフォルト処理をサポートしているため、互換性の面でも優れています。

#### 3-2 デフォルト処理の欠点
一方で、デフォルト処理には限界があります。特に、大規模なデータセットや複雑なシナリオにおいては、性能が低下する可能性があります。また、特定のニーズや条件に合わせたカスタマイズが難しいため、精度や効率を最大限に引き出すことが難しくなります。このような場合には、tiling処理などの代替手法を検討する必要があります。

### 4 tiling処理の特徴
#### 4-1 tiling処理の利点
tiling処理は、大規模なデータセットや複雑なシナリオにおいて、より高い性能を発揮します。データを小さなタイル（タイル）に分割して処理することで、メモリ使用量を抑えつつ、高精度な分析が可能となります。また、タイルごとに異なる処理を適用できるため、細かい調整が可能です。

#### 4-2 tiling処理の欠点
tiling処理の欠点としては、実装が複雑であり、設定や調整に時間がかかる点が挙げられます。特に、適切なタイルサイズの選定や、タイル間のデータ統合に関する課題があります。これらの問題を解決するためには、高度な専門知識や経験が求められることが多く、初心者にとっては敷居が高い方法です。

### 5フォルト処理とtiling処理の比較
#### 5-1 処理速度の比較
デフォルト処理は、一般的に設定が簡単であり、迅速に結果を得ることができます。しかし、大規模なデータセットに対しては、処理速度が低下することがあります。一方、tiling処理は、大量のデータを効率的に処理することができ、結果として全体の処理速度を向上させることができます。ただし、タイルの分割や統合に時間がかかるため、初期設定には時間がかかる場合があります。

#### 5-2 リソース使用量の比較
デフォルト処理は、システムリソースの消費が少ないため、小規模なプロジェクトやリソースが限られている環境での使用に適しています。一方、tiling処理は、複雑なシナリオに対応するため、メモリやCPUリソースを多く消費することがあります。しかし、リソース管理が適切に行われれば、全体的な効率を向上させることが可能です。

### 6 精度の違い
#### 6-1 検出精度の比較
デフォルト処理は、一般的なシナリオに対しては十分な精度を提供しますが、複雑なシナリオや特定の条件下では精度が低下することがあります。tiling処理は、データを細かく分割して処理するため、より高い精度を実現することができます。特に、大規模なデータセットや複雑なパターンの検出においては、tiling処理の優位性が顕著です。

#### 6-2 誤検出率の比較
デフォルト処理は、設定が簡単でありながら、誤検出率が高くなることがあります。特に、複雑な背景や多くの物体が存在するシナリオでは、誤検出が発生しやすいです。tiling処理は、各タイルごとに詳細な解析を行うため、誤検出率を低減することができます。ただし、タイル間の境界処理や統合に注意を払わないと、誤検出が発生する可能性があります。

### 7 まとめ
人物検知と属性解析は、現代の多くのアプリケーションで重要な役割を果たしています。特にYOLOを用いたPythonでの実装は、高速で効率的な解析が可能であり、実用性が高いです。デフォルト処理とtiling処理の違いを理解し、適切な手法を選択することで、精度と効率を最大限に引き出すことができます。防犯カメラからスマートシティまで、さまざまな分野での応用が期待される技術です。

## よくある質問 (Q&A)
### Q1 人物検知におけるYOLOの利点は何ですか？
A1: YOLOは、一度のパスで画像全体を解析するため、高速で効率的な人物検知が可能です。これにより、リアルタイムのビデオ処理や高速な画像解析が求められるアプリケーションに適しています。

### Q2 デフォルト処理とtiling処理の違いは何ですか？
A2: デフォルト処理は設定が簡単であり、一般的なシナリオで適切な性能を発揮します。一方、tiling処理はデータを細かく分割して処理するため、大規模なデータセットや複雑なシナリオにおいて高い精度と効率を提供します。

### Q3 PythonでYOLOを実装するために必要なライブラリは何ですか？
A3: 主なライブラリとして、OpenCV、TensorFlow、Kerasなどがあります。これらのライブラリを利用することで、YOLOのモデルを簡単にロードし、画像やビデオの解析を行うことができます。