2025年3月5日水曜日

Pythonで簡単にできる自動モザイク処理と動画編集方法(第3回) by裏方徹也

【初心者向け】動画処理と人物検出の基本!Mask R-CNNを使って動画から人物を自動検出する方法 第3回

【初心者向け】動画処理と人物検出の基本!Mask R-CNNを使って動画から人物を自動検出する方法 第3回

皆さん、こんにちは!裏方です!第1回では、Eclipseの導入から動画を単純に出力する方法を紹介し、第2回では、動画にフレーム番号や座標を表示する技術を紹介しました。そして今回は、さらに進んで、動画に映っている人物をMask R-CNNという方法で検出し、四角でマーキングする方法を紹介します。

目次

1. Mask R-CNNとは?

Mask R-CNNとは、簡単に言うと画像から物体を自動で検出する技術です。ここでは動画の中から人物という物体を検出するために使います。もっとわかりやすくするため、ウォーリーを探せで今回のプログラムを説明します。ウォーリーを探せでは、ウォーリーという人物の特徴を記憶して、絵の中から探し出しますよね。絵を1枚ずつ見て見つけ出しますが、今回のプログラムではウォーリーだけではなく、何千人、何万人の人の画像を記憶して動画からその画像に近しい人を探してきてくれます。しかも、1枚の画像だけではなく、何百枚、何千枚の画像の中から人物を自動で見つけだしてくれます。

2. 必要なライブラリの準備

それでは、今回追加するコードを見てみましょう。Mask R-CNNを使うために必要なモデルのロードや画像の前処理を行います。

import torch
from torchvision import models, transforms

# 事前学習済みのMask R-CNNモデルをロード
model = models.detection.maskrcnn_resnet50_fpn_v2(pretrained=True)
model.eval()  # 評価モードにする
                

この部分では、Mask R-CNNモデルをPyTorchのTorchvisionライブラリからロードし、評価モードに設定しています。要は大量の人の画像を記憶させ、動画の中からそれに近いものを検出します。検出された物体はどれくらい人に近いのかスコアという点数をつけて評価します。1に近ければ近いほど検出したものが人に近いです。これで、人物検出を始める準備が整いました。

3. 動画から人物を検出して四角形を描画

次に、実際に動画の中から人を探しだしてみます。

def process_torch(frame):
    input_image = preprocess_image(frame)
    with torch.no_grad():
        prediction = model(input_image)
    boxes = prediction[0]['boxes'].cpu().numpy()
    scores = prediction[0]['scores'].cpu().numpy()
    return boxes, scores

def preprocess_image(image):
    transform = transforms.Compose([transforms.ToTensor()])
    return transform(image).unsqueeze(0)  # バッチ次元を追加

def draw_box(frame, boxes, scores):
    threshold = 0.9  # 信頼度の閾値
    for i in range(len(boxes)):
        if scores[i] > threshold:
            x1, y1, x2, y2 = boxes[i]
            cv2.rectangle(frame, (int(x1), int(y1)), (int(x2), int(y2)), (255, 0, 0), 2)
            scores_M = int(scores[i] * 1000) / 1000
            cv2.putText(frame, f"{scores_M}", (int(x1), int(y1)), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 0), 2, cv2.LINE_AA)
    return frame
                

・物体の検出 (process_torch)

入力された動画を1枚ずつ処理し、物体の位置(四角形)とそのスコアを取得します。この結果を使って、どの物体が検出されたのかを確認できます。

・画像の前処理 (preprocess_image)

画像を「テンソル」という形式に変換し、物体検出モデルが正しく処理できる形に整えます。テンソルとは多次元の配列の事で簡単に言うと「数字が並んだ箱」の事です。

・物体の描画 (draw_box)

検出された物体の位置に四角形を描き、その四角の左上にスコアを表示します。これにより、どの物体がどれくらい信頼できるかを視覚的に確認できます。

  

4. コード全体

以下は、今回のコード全体です。

import tkinter as tk
from tkinter import filedialog
import cv2
import moviepy.editor as mp
import os

#第3回で追加#
import torch
from torchvision import models, transforms
#第3回で追加#

#第3回で追加#
# 事前学習済みのMask R-CNNモデルをロード
model = models.detection.maskrcnn_resnet50_fpn_v2(pretrained=True)
model.eval()  # 評価モードにする
#第3回で追加#

# ファイルダイアログで動画を選択
def select_video_file():
    root = tk.Tk()
    root.withdraw()  # Tkinterウィンドウを非表示にする
    video_path = filedialog.askopenfilename(title="動画ファイルを選択", filetypes=[("動画ファイル", "*.mp4 *.avi *.mov *.mkv")])
    return video_path

# 動画の音声と映像を出力
def process_video(input_video_path):
    # 中間ファイル作成(映像だけ出力)
    output_video_path_temp = input_video_path.rsplit('.', 1)[0] + "_outputtemp.mp4"

    # 出力ファイルのパスを生成(元の動画と同じディレクトリにoutput.mp4として保存)
    output_video_path = input_video_path.rsplit('.', 1)[0] + "_output.mp4"


    # 動画キャプチャの準備
    cap = cv2.VideoCapture(input_video_path)
    original_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
    original_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
    fps = cap.get(cv2.CAP_PROP_FPS)

    # 総フレーム数を取得
    total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))

    # 出力動画ファイルの作成(音声なしで映像のみ)
    fourcc = cv2.VideoWriter_fourcc(*'mp4v')
    out = cv2.VideoWriter(output_video_path_temp, fourcc, fps, (original_width, original_height))

    frame_count = 0

    while True:
        # 動画の1フレームを読み込む
        ret, frame = cap.read()
        if not ret:
            break  # 動画の終わりに達したら終了

        if 65 < frame_count < 215:
            boxes , scores = process_torch(frame) #Fasterでframeから人を検知する
            frame = draw_box(frame,boxes , scores)

        #フレーム番号
        frame = writeinfo(frame, frame_count, original_width, original_height)
        # 目盛りを描画
        frame = add_scale_to_frame(frame, original_width, original_height)

        frame_count += 1  # フレーム番号をインクリメント
        out.write(frame)

        cv2.imshow("frame", frame)
        cv2.waitKey(1)

    # 動画キャプチャと書き込みを解放
    cap.release()
    out.release()
    cv2.destroyAllWindows()

    # 動画と音声をそのまま出力
    add_audio_to_video(input_video_path, output_video_path_temp,output_video_path, total_frames,fps)

    print(f"出力ファイル: {output_video_path}")

##第3回で追加##
def process_torch(frame):

    input_image = preprocess_image(frame)

    with torch.no_grad():
        prediction = model(input_image)

    boxes = prediction[0]['boxes'].cpu().numpy()
    scores = prediction[0]['scores'].cpu().numpy()

    return boxes,scores

def preprocess_image(image):
    transform = transforms.Compose([
        transforms.ToTensor(),  # 画像をテンソルに変換
    ])
    return transform(image).unsqueeze(0)  # バッチ次元を追加


def draw_box(frame,boxes , scores):
    # 信頼度が一定値以上の検出結果に四角形を描画
    threshold = 0.9  # 信頼度の閾値(必要に応じて調整)
    for i in range(len(boxes)):
        if scores[i] > threshold:
            x1, y1, x2, y2 = boxes[i]
            # 四角形を描画 (色は青、太さは2)
            cv2.rectangle(frame, (int(x1), int(y1)), (int(x2), int(y2)), (255, 0, 0), 2)

            # scoreを丸める (小数第2位以下を切り捨て)
            scores_M = int(scores[i] * 1000) / 1000

            # 黒い文字でframe_countを表示
            cv2.putText(frame, f"{scores_M}", (int(x1), int(y1)), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 0), 2, cv2.LINE_AA)

    return frame
##第3回で追加##

##第2回で追加##
def writeinfo(frame,frame_count,width,height):
    # フレームにframe_countを表示
    # 白い背景の矩形を描画
    cv2.rectangle(frame, (0, 0), (100, 40), (255, 255, 255), -1)

    # 黒い文字でframe_countを表示
    cv2.putText(frame, f"{frame_count}", (0, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 0), 2, cv2.LINE_AA)

    return frame

def add_scale_to_frame(frame, width, height, scale_interval=100, large_scale_interval=500):
    # 左端と上端に目盛りを描画
    font = cv2.FONT_HERSHEY_SIMPLEX
    font_scale = 0.5
    font_thickness = 1
    text_color = (0, 0, 0)
    red_color = (0, 0, 255)  # 赤色
    green_color = (0, 255, 0)  # 緑色

    # 左端(縦の目盛り)
    for y in range(scale_interval, height, scale_interval):
        cv2.line(frame, (0, y), (10, y), (0, 0, 0), 2)  # 目盛りの線
        cv2.putText(frame, str(y), (15, y + 5), font, font_scale, text_color, font_thickness, cv2.LINE_AA)  # 数値

    # 上端(横の目盛り)
    for x in range(scale_interval, width, scale_interval):
        cv2.line(frame, (x, 0), (x, 10), (0, 0, 0), 2)  # 目盛りの線
        cv2.putText(frame, str(x), (x + 5, 20), font, font_scale, text_color, font_thickness, cv2.LINE_AA)  # 数値

    # 100ピクセルごとの交点に赤い点を描画
    for x in range(scale_interval, width, scale_interval):
        for y in range(scale_interval, height, scale_interval):
            cv2.circle(frame, (x, y), 3, red_color, -1)  # 赤い点
            cv2.putText(frame, f"({x},{y})", (x + 5, y + 10), font, font_scale, red_color, font_thickness, cv2.LINE_AA)  # 座標表示

    # 500ピクセルごとの交点に緑の点を描画
    for x in range(large_scale_interval, width, large_scale_interval):
        for y in range(large_scale_interval, height, large_scale_interval):
            cv2.circle(frame, (x, y), 3, green_color, -1)  # 緑の点
            cv2.putText(frame, f"({x},{y})", (x + 5, y + 10), font, font_scale, green_color, font_thickness, cv2.LINE_AA)  # 座標表示

    return frame
##第2回で追加##


# 音声を動画に統合する
def add_audio_to_video(input_video_path, output_video_path_temp,output_video_path, total_frames,fps):
    #moviepyで動画と音声を読み込む
    video_clip = mp.VideoFileClip(input_video_path)
    audio_clip = video_clip.audio

    # 映像部分の長さと音声の長さが一致するように、音声を調整
    duration = total_frames / fps  # 映像の長さ(秒)
    audio_clip = audio_clip.subclip(0, duration)  # 音声を映像の長さに合わせる


    # 映像と音声を統合
    final_video = mp.VideoFileClip(output_video_path_temp)
    final_video = final_video.set_audio(audio_clip)

    # 音声付きの動画を保存
    final_video.write_videofile(output_video_path, codec="libx264", audio_codec="aac")

    # 中間ファイル(output_video_path)を削除
    if os.path.exists(output_video_path_temp):
        os.remove(output_video_path_temp)
        print(f"中間ファイル {output_video_path_temp} を削除しました。")

    print(f"音声付きの動画が保存されました。最終ファイル名: {output_video_path_temp}")


# メイン処理
def main():
    video_file = select_video_file()
    if video_file:
        process_video(video_file)
    else:
        print("動画ファイルが選択されませんでした。")

if __name__ == "__main__":
    main()

                

5. 実行結果

実行前

実行前から実行後の変化をご覧ください。

実行後

6. 問題点

人を自動で検出できて便利である一方で以下のような問題点も存在します。

  • 処理に時間がかかる

まず、処理に時間がかかる点ですが、今回約150フレームを対象に処理してみたところ、処理が完了するまでに約9分かかりました。具体的には、1分間に約16.6フレーム分しか処理できないため、効率が悪く感じます。 たとえば今回の1万円チャレンジの動画のように30000フレームある場合、全て処理するのに約30時間近くかかってしまいます。このように、動画のフレーム数が多いと、処理時間が膨大になり、実用性に欠ける場合があります。

  • 全ての人がモザイクの対象となる

次に、全ての人がモザイクの対象となる点ですが、モザイクをかける人を選べないという事です。今回の動画では人込みででべそさん、清盛さんも映っている場合でも全員モザイクとなってしまいます。 でべそさん、清盛さん以外はモザイクをかけるといった指定が今のところできず、特定の人だけモザイクをかけないようにする場合には不便です。

7. まとめと次回予告

今回は、動画内の人物を検出し、四角でマーキングする方法を紹介しました。Mask R-CNNを使うことで、簡単に高精度な人物検出が可能となります。但し、動画の時間が長いと、処理に膨大な時間がかかるという問題点があるため、対策が必要となります。 次回は、一定間隔で人物を検出する方法について解説します。

0 件のコメント:

コメントを投稿