OpenCV + YOLOで車をぼかす

## はじめに
人物の顔をぼかす記事の続きです。

https://qiita.com/yanagi-m/items/7650345068fef8f33113

今回は人ではなく車をぼかします。

今回もChatGPTの力を借りながら初めて画像処理に挑戦してみたので、その結果を備忘録として残しておきます。

## やりたいこと
画像に車が映っていた場合、車全体をぼかす

## テストで使用する画像
今回は、[写真AC](https://www.photo-ac.com/)のフリー画像を使用します。
![23990617_m.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3570844/2100b77d-8c92-06b1-022f-db1656b2aeec.jpeg)

## 環境
Google Colaboratoryを使用します。

## YOLOのセットアップ
YOLO(You Only Look Once)とは、2016年に発表された物体検出手法です。画像全体をグリッド分割し、各セルが物体を持つか及びその物体のカテゴリと位置を一度の処理で判定します。 (Wikipediaより)

https://ja.wikipedia.org/?curid=4254133

まず、YOLOを動かすための環境をセットアップします。以下のコードを実行することで、必要なライブラリと重みファイルを取得できます。

https://github.com/AlexeyAB/darknet
```python
!git clone https://github.com/AlexeyAB/darknet.git
%cd darknet
!sed -i 's/OPENCV=0/OPENCV=1/' Makefile
!sed -i 's/GPU=0/GPU=1/' Makefile
!sed -i 's/CUDNN=0/CUDNN=1/' Makefile
!make
!wget https://pjreddie.com/media/files/yolov3.weights
```

## 画像をアップロード
次に、検出対象の写真をアップロードします。
```python
from google.colab import files
uploaded = files.upload()
for fn in uploaded.keys():
    image_path = fn
```

## ぼかし処理
```python
import cv2
import numpy as np
from matplotlib import pyplot as plt

"""
    非最大抑制 (NMS) を適用して、重複するバウンディングボックスを統合または除去する
    @param boxes (list): バウンディングボックスのリスト
    @param confidences (list): 各バウンディングボックスの信頼度のリスト
    @param threshold (float): NMSのしきい値
    @return list: 重複を除去した後のバウンディングボックスのインデックス
"""
def apply_nms(boxes, confidences, threshold=0.5):
    indices = cv2.dnn.NMSBoxes(boxes, confidences, score_threshold=0.5, nms_threshold=threshold)

    return indices


"""
    YOLOを使用して画像のナンバープレートを検出し、ぼかし処理を行う
    @param image_path (str): 画像のパス
    @param configPath (str): YOLOの設定ファイルへのパス
    @param weightPath (str): YOLOの重みファイルへのパス
    @return ndarray: ぼかし処理を施した画像
"""
def detect_and_blur_plate(image_path, configPath, weightPath):
    # Load YOLO model
    net = cv2.dnn.readNetFromDarknet(configPath, weightPath)

    # Load the image
    image = cv2.imread(image_path)
    height, width = image.shape[:2]

    # Preprocess the image
    blob = cv2.dnn.blobFromImage(image, 1/255.0, (416, 416), swapRB=True, crop=False)
    net.setInput(blob)

    # Get names of YOLO output layers
    layer_names = net.getLayerNames()
    output_layer_indices = net.getUnconnectedOutLayers().flatten().tolist()
    output_layers = [layer_names[i - 1] for i in output_layer_indices]

    detections = net.forward(output_layers)

    boxes = []
    confidences = []

    # Analyze detection results
    for detection in detections[0]:
        scores = detection[5:]
        class_id = np.argmax(scores)
        confidence = scores[class_id]
        if confidence > 0.5:
            center_x = int(detection[0] * width)
            center_y = int(detection[1] * height)
            w = int(detection[2] * width)
            h = int(detection[3] * height)
            x = int(center_x - w / 2)
            y = int(center_y - h / 2)

            boxes.append([x, y, w, h])
            confidences.append(float(confidence))

    # Apply Non-Maximum Suppression
    indices = apply_nms(boxes, confidences)

    # Apply blur and draw rectangles
    for idx in indices:
        if isinstance(idx, (np.ndarray, list)) and len(idx) > 0:
            i = idx[0]
        else:
            i = idx
        box = boxes[i]
        x, y, w, h = box[0], box[1], box[2], box[3]

        roi = image[y:y+h, x:x+w]
        blurred = cv2.GaussianBlur(roi, (15, 15), 0)
        image[y:y+h, x:x+w] = blurred

        # Draw a rectangle around the blurred region
        cv2.rectangle(image, (x, y), (x+w, y+h), (0, 0, 255), 2)

    return image


# Set paths for the configuration and weights
configPath = "./cfg/yolov3.cfg"
weightPath = "./yolov3.weights"

blurred_image = detect_and_blur_plate(image_path, configPath, weightPath)
plt.imshow(cv2.cvtColor(blurred_image, cv2.COLOR_BGR2RGB))
plt.axis('off')
plt.show()
```
こんな感じにぼかすことができました。
ちなみに、ぼかした位置が分からない時があったので、分かりやすいようにぼかした範囲を赤枠で囲むようにしました。
![ダウンロード_15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3570844/9fe682e4-dcdd-71df-caab-43ebe42b2437.png)

## ぼかしを強くする

ぼかしの強さは、`cv2.GaussianBlur()` 関数の第2引数であるカーネルサイズに応じて制御していて、カーネルサイズが大きいほど、ぼかしは強くなります。ただし、カーネルサイズは奇数である必要があります。

試しに、カーネルサイズを35にしてみるとこんな感じです。
```python
cv2.GaussianBlur(roi, (35, 35), 0)
```
![ダウンロード_35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3570844/1be3b419-3eea-266e-0bf0-1d13e9408570.png)

若干、ぼかしが強くなりました。
| カーネルサイズ:15 | カーネルサイズ:35 |
| :-: | :-: | 
| ![ダウンロード_15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3570844/9fe682e4-dcdd-71df-caab-43ebe42b2437.png) | ![ダウンロード_35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3570844/1be3b419-3eea-266e-0bf0-1d13e9408570.png)

## ちなみに
`cv2.GaussianBlur()` 関数の第3引数は横方向の標準偏差を指しています。
値が小さいほど、よりカーネルの中心(≠画像の中心)に強くぼかしがかかり、逆に、値が大きいほど、より均一にカーネル全体にぼかしがかかるようになります。

## おわりに
今回はこちらの企画の一環で、写真をアップロードする際にプライバシーな情報が映っていたらぼかしたい！と思い、挑戦してみました。
当初、ナンバープレートのみぼかそうと考えていましたががうまくできず、、
車を検出し、車自体をぼかすようにしてみたところ、思うようにぼかすことができました。

https://sakinokoto.esm.co.jp/plan-detail/machitopia

余談ですが、企画自体は一旦クローズとなりました、、経緯などはこちらのブログに載せています。気になったかたは是非ご覧ください。

https://sakinokoto.esm.co.jp/blog-detail/machitopia-report