0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

日本三大提灯祭り 360年続く伝統の二本松提灯祭りの提灯カウンターを作ってみた

Last updated at Posted at 2024-10-24

はじめに

去年、日本三大提灯祭り 360年続く伝統の二本松提灯祭りの提灯を最新のAIで画像認識してみた を投稿しました。去年に引き続き、今年も、続編となる記事を投稿します。
去年は、静止画を対象としていましたが、今年は、認識した結果を動画に組み立ててご覧いただけるようにしました。また、膨大な数の提灯を認識するので、その数をカウントするようにしました。

学習モデル

去年と同様YOLOにて物体検知しました。学習結果モデルは、去年と全く同じそのものを使用しました。

./yolov5/runs/train/chochin/weights/best.pt

なお、去年は、pythonコマンドでYOLOを動作させましたが、今年は、pythonの関数を呼ぶようにしました。
具体的には、下記の通り、yolov5をimportし、detect.run()で物体検知します。

import yolov5.detect as detect
detect.run(weights='best.pt',source=filename_result,save_csv=True)

提灯カウンター

山車が交差点を曲がる際、提灯がゆらゆらするので認識が難しかろうと、交差点を曲がる際の動画を1分ぶん対象としてカウントします。

このような祭りの山車ものって、まがるところが魅せ場だったりするんですよね。今回、山車がきて人混みができる前に、私がスタンバって待っていると、隣に子連れの若夫婦がいたので、旦那さんに「今からここで待ってるなんて、通だね。」って声をかけたら、「隣町のお祭りで若連やっていて、曲がり角が見ものなんでね。」って言ってました。さすが現役のお祭りストだと思いました。隣町のお祭りも面白いので今度見に行こうっと。

閑話休題、提灯カウンターの話に戻りましょう。
各字によって、曲がり方に特徴があり、あっさり曲がっていく場合もあり、ゆっくり曲がっていく場合もあり、最大1分程度かかるので、カウント対象の動画を1分としました。その1分の開始位置は、各動画でまちまちなので、目視確認後、その開始時間を入力するようにしました。

以下、YOLOで提灯を検知し、正しく認識できた数をカウントアップする関数を作りました。

import cv2
import yolov5.detect as detect
import shutil
import pandas as pd
import os

filename_result = 'result.png'                     # YOLOで物体検知する対象の画像
yolo_detect     = './yolov5/runs/detect/exp/'      # YOLOで物体検知した結果フォルダ
filename_yolo   = yolo_detect + filename_result    # YOLOで物体検知した結果画像

# 提灯カウンター
def count_chochin(
    filename_in,  # 動画ファイル名称(入力用:物体検知する対象の動画)
    filename_out, # 動画ファイル名称(出力用:提灯を認識した矩形と数を描画した動画)
    label,        # ラベル(どの字の動画か?)
    count_start,  # カウント開始秒数
    count_end):   # カウント終了秒数

    # 動画ファイルオープン
    cap = cv2.VideoCapture(filename_in)
    if not cap.isOpened():
        return -1
    # fpsの取得
    fps = int(cap.get(cv2.CAP_PROP_FPS))
    # カウント開始/終了をフレーム番号に変換
    count_start_frameno = fps * count_start
    count_end_frameno   = fps * count_end
    frameno             = 0
    total_true_count    = 0
    # 動画終了までのループ
    while True:
        # 1フレーム読み込み
        ret, frame = cap.read()
        if ret:
            print(frameno)
            if frameno==0:
                # 初回処理(出力動画を作成)
                fourcc = cv2.VideoWriter_fourcc('m','p','4', 'v')
                video  = cv2.VideoWriter(filename_out, fourcc, fps, (frame.shape[1], frame.shape[0]))

            # カウント対象の場合
            if count_start_frameno<=frameno and frameno<count_end_frameno:
                # 物体検知する静止画を、ファイル保存
                cv2.imwrite(filename_result,frame)
                # YOLOで物体検知
                detect.run(weights='best.pt',source=filename_result,save_csv=True)
                # 認識結果の画像
                img = cv2.imread(filename_yolo)
                # 正しく認識できた数の取得
                filename_predict = yolo_detect + 'predictions.csv'
                if os.path.exists(filename_predict):                        # ファイルが作成されていれば、
                    df_predict = pd.read_csv(filename_predict,header=None)  # 読み込んで、
                    df_predict = df_predict[df_predict[1]==label]           # 同じラベルのみを取得し、
                    true_count = df_predict.shape[0]                        # その数を取得する
                else:
                    true_count = 0                                          # ファイルが作成されなければ、0
                total_true_count += true_count
                # 認識結果ファイルをフォルダごと削除
                shutil.rmtree(yolo_detect)
            else:
                img = frame

            if total_true_count>0:
                # 正しく認識できた数を表示
                total_true_count_test = f'{total_true_count:>6}'
                cv2.putText(img, text=total_true_count_test,org=(1400, 1050),fontFace=cv2.FONT_HERSHEY_PLAIN,fontScale=8.0,color=(255, 255, 255),thickness=5,lineType=cv2.LINE_8)

            # 出力用動画に1フレーム追加書き込み
            video.write(img)
            frameno+=1
        else:
          video.release()
          return 0
    return

以下、解説します。

関数の入力パラメータ

入力パラメータは、

  • 動画ファイル名称(入力用:物体検知する対象の動画)
  • 動画ファイル名称(出力用:提灯を認識した矩形と数を描画した動画)
  • ラベル(どの字の動画か?)
  • カウント開始秒数
  • カウント終了秒数

処理概要は、ざっくりと、動画ファイル名称(入力用)を読み込んで、カウント開始秒数からカウント終了秒数までの各フレームでYOLOで物体検知し、認識した結果が、ラベル(どの字の動画か?)と同じだったらその数をカウントし、ラベル矩形と認識した数を画像に重ね合わせて表示し、動画ファイル名称(出力用)を作成する、といった感じです。

動画処理(入力動画)

opencvで動画ファイルをオープン(cap=cv2.VideoCapture)し、1フレームずつ処理します。(cap.read)後で動画に組み立て直すので、fpsも取得(cap.get)しておきます。また、入力パラメータのカウント開始/終了は秒ですので、fpsを使ってフレーム番号に変換します。

物体検知

detect.run()で物体検知してくれるのですが、ファイルで入力/出力するので、ちょっとめんどかったです。

入力ファイル

メモリのまま画像イメージを渡すことができなかったので、(実はできるのですかね?)一旦ファイルに書き込んで(cv2.imwrite)、そのファイルを物体検知するようにしました。

出力ファイル

検知結果も、メモリでもらうことができなかったので、ファイルに出力したものを、読み込むようにしました。検知した結果のファイルは、以下2ファイル作成されます。

  • result.png
    物体検知できた提灯の矩形とラベル、信頼度を表示した画像
    result50.png
    亀谷の提灯を認識してますが、なぜか、交通整理のおじさんのヘルメットも、認識してしまっています。

  • predictions.csv(save_csv=Trueとした場合)
    物体検知できた提灯のラベルと信頼度(0-1)
    単純に読み込んで先頭を表示すると以下のようになります。ヘッダがないですが、"画像ファイル名","ラベル","信頼度"の順になります。

0	1	2
0	result.png	亀谷	0.26
1	result.png	亀谷	0.27
2	result.png	亀谷	0.29
3	result.png	亀谷	0.31
4	result.png	亀谷	0.3

認識結果は、./yolov5/runs/detect/exp/に書き込まれるのですが、expフォルダが存在していれば、exp2が作成され書き込まれます。以下順に、exp3,exp4と作成されていきます。ですので、毎回shutil.rmtree()により、認識結果ファイルをフォルダごと削除し、必ずexpフォルダに格納されるようにました。

検知結果

predictions.csvができていれば、何らかの物体検知ができたことになるので、そのファイルを読み込み(pd.read_csv)、入力パラメータのラベルと同じラベルが認識できていれば(df_predict[1]==label)、その数をカウントします(df_predict.shape[0])。それを、トータルの数に積算していきます(total_true_count += true_count)。正しく認識できた数として、画像の右下に表示します(cv2.putText)。なお、今回は、信頼度は参照せず、信頼度が低くとも、正しく認識できればカウントするようにしました。

動画処理(出力動画)

初回のみ、opencvで動画ファイルを書き込みオープン(cv2.VideoWriter_fourcc、cv2.VideoWriter)します。その後、1フレームずつ、video.writeで書き込み、最後は、video.releaseで終了します。

動画ファイルのリスト

各字の動画ファイル名と、ラベル、カウント開始秒をリストで定義します。カウント開始秒は、前述の通り、目視で確認しました。カウント終了秒は、全て1分(60秒)なので、共通で60秒を設定します。

file_list = [
    # ファイル名、     ラベル、カウント開始秒
    ['1_Motomachi.MOV', '本町', 110],
    ['2_Kamegai.MOV',   '亀谷',  40],
    ['3_Takeda.MOV',    '竹田',   1],
    ['4_Matsuoka.MOV',  '松岡',  20],
    ['5_Nezaki.MOV',    '根崎',  75],
    ['6_Wakamiya.MOV',  '若宮',  40],
    ['7_Kakunai.MOV',   '郭内',  60],
]
# カウントする秒数
count_time = 60

処理実行

ファイルリストをループし、1動画ずつ、提灯カウンター関数をコールします。iPhoneで撮影したので、.MOVとして保存されていたので、.mp4として処理結果の動画を保存します。ここはちょっと力技になってしまいますが、ファイル名をreplace関数により、'.MOV'⇒'.mp4'に変換しています。

# ファイルリストのループ
for file in file_list:
    # 動画ファイル名称(入力用:物体検知する対象の動画)
    filename_in  = f'data/{file[0]}'
    # 動画ファイル名称(出力用:提灯を認識した矩形と数を描画した動画)
    filename_out = filename_in.replace('.MOV','.mp4')
    # ラベル
    label        = file[1]
    # カウント開始/終了秒数
    count_start  = file[2]
    count_end    = count_start + count_time
    print(filename_in,filename_out,label,count_start,count_end)
    # 提灯カウンターをコール
    count_chochin(filename_in,filename_out,label,count_start,count_end)```

音声を追加

動画を1フレームずつ処理し、物体検知した結果を、新しい動画として作成したが、音声がないので、オリジナル音声を追加します。

from moviepy.editor import VideoFileClip, AudioFileClip

# ファイルリストのループ
for file in file_list:
    # 音声ファイル(オリジナル動画)
    filename_audio  = f'data/{file[0]}'
    # 動画ファイル(認識結果動画)
    filename_movie  = filename_audio.replace('.MOV','.mp4')
    # 音声を追加した認識結果動画ファイル
    filename_out    = filename_movie.replace('.mp4','_count.mp4')
    print(filename_audio,filename_movie,filename_out)

    # 動画ファイルを指定
    video_clip = VideoFileClip(filename_movie)
    # 音声ファイルを指定
    audio_clip = AudioFileClip(filename_audio)

    # 音声をセット
    final_clip = video_clip.set_audio(audio_clip)
    # 音声を追加した動画を書き込み
    final_clip.write_videofile(filename_out, codec='libx264', audio_codec='aac')

物体検知するために定義したファイルリストをそのままループさせ、1動画ファイルずつ処理します。処理内容は、VideoFileClipに、AudioFileClipの音声をセットするだけになります。

提灯カウンターの処理結果

各字毎に、

  • 提灯に描かれた文字の認識のしやすさ
  • 交差点のどのコースをまがっていくか
  • カメラ手前でとどまってくれるか

などにより、正しく認識できた提灯の数は、各字毎で大きく異なります。

正しく認識できた数
本町 128,867個
亀谷 108,698個
竹田 48,689個
松岡 41,063個
根崎 110,297個
若宮 82,436個
郭内 85,497個

詳細は、動画をご確認ください。

考察

  • 去年の結果からも、「本町」の認識はそもそもよかったが、山車がカメラの手前でとどまってくれたので、さらに有利に働き、12万個以上の提灯を認識できた。
  • 「亀谷」は、「本町」と同じようなコースをたどり、そこそこ認識できたが、「龜」「谷」の二文字なので認識が難しく、本町には及ばなかった。
  • 「竹田」「松岡」は、どちらもやんわり(ゆっくり、とか、ちょっとずつ、とかの意味の土地言葉、特にお祭りで使われる。)曲がっていき、カメラから遠く、かつ、あっさり過ぎ去っていったので、認識できた提灯の数は低迷していた。やんわり曲がる場合に、遠くの小さい提灯を認識せざるを得ないケースでは、ほぼ「亀谷」と誤認識している。「龜」の字がごちゃごちゃしているので、遠目の小さい提灯はみんな「龜」に見えているのではないか?小さい提灯の教師データも作って再学習しないといけない。「松岡」は、近づいてきても「若宮」と誤認識するケースが多かった。松の異体字「枩」も「若」も横線が多く似ているためと考えられる。
  • 「根崎」は、「本町」と同じようなコースをたどったが、90度曲がる際に時間がかかり、正面からの映像が多く、映り込む提灯の数が少なかったため、結果的に、認識できた提灯の数が「本町」に及ばなかったのだろう。
  • 「若宮」「郭内」は、90度まがる位置が、カメラからちょっと遠かったので、認識が難しかったようである。「郭内」の「郭」も字画が多く、認識が難しいが、カメラ手前でとどまってくれたので提灯が大きく映り、「若宮」超えの認識数となった。
  • 「亀谷」「根崎」の動画に、交通整理のおじさんのヘルメットが映り込み、それを提灯と誤認識ケースが多発した。本来は、この原因究明と対策や、カウント方法の改善が必要だが、趣味でやっているので、このままにしておく。(ちょっと面白いので、)

【参考】去年の動画

おわりに

360年続く伝統のお祭りでは、提灯のデザインをモデルチェンジすることはありませんので、今年は教師データを作らず、学習モデルも去年のまま使用しました。しかし、動画として連続静止画を何枚も画像認識すると、さすがに認識率の悪さが目立つようにもなり、再学習が必要かなと思いました。来年は、教師データをとり直して、ラベリングし、再学習して認識率改善できるように努めたいなぁ。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?