Vaticで出力したデータをAVAデータセットに変換

コンピュータビジョン

はじめに

Vaticは,ビデオフレームからアノテーションを作成するための強力なツールです.このツールを使用すると,映像内のオブジェクトを識別して,それらに対してバウンディングボックスを作成することができます.しかし,Vaticで作成したデータは,行動認識に必要なAVAデータセット形式に変換する必要があります.この記事では,Vaticで出力したデータをAVAデータセット形式に変換する方法について説明します.

Vaticでアノテーションを作成する方法

Vaticを使用してアノテーションを作成するには,以下の手順を実行します.

https://hamaruki.com/quick-start-with-docker-compose-vatic/

行動認識とは?

行動認識とは,動画像データなどから人物の動作を識別し,その動作に基づいた特定の行動を自動的に認識する技術です.例えば,カメラで撮影された映像から,人物が歩いているか,立っているか,しゃがんでいるかなどを判断することができます.行動認識は,監視カメラの映像解析やスポーツの解析など,様々な分野で活用されています.近年では,深層学習を用いた手法が発展しており,高い精度で行動認識を行うことが可能になってきています.

AVAデータセットとは?

AVAデータセットは,コンピュータービジョンの分野で,行動認識に使用されるデータセットです.このデータセットには,約80の行動カテゴリがあり,それぞれの行動に対して画像が提供されています.これらのカテゴリは,ダンス,スキー,釣り,読書,泳ぐ,走る,歩くなどの行動をカバーしています.

AVAデータセットは,Googleが提供しているオープンソースのデータセットであり,現在最も広く使用されている行動認識のための画像データセットの一つです.

https://hamaruki.com/what-is-ava_datasets/

Ika-Actionとは?

スプラトゥーン3において,動画を使った行動解析は非常に重要です.プレイヤーが実際に行動している様子を見ることで,より詳細な分析が可能になります.また,動画を使った解析によって,プレイヤーの弱点や改善点を見つけることができます.これを「Ika-Action」と名付けました.

https://hamaruki.com/ika-action_video-action-recognition/

アノテートデータをエクスポート

コンテナ内に入ってからエクスポートします.


maki@maki-lab3060V3:~/prj/vatic-docker$ docker-compose exec vatic /bin/bash
root@abad62fe4548:~/vatic# turkic dump currentvideo -o /root/vatic/data/output.txt

出力したテキストファイルはこちら


0 327 226 399 345 2000 0 0 0 "paint"
0 327 226 399 345 2001 0 0 1 "paint"
0 327 226 399 345 2002 0 0 1 "paint"
0 327 226 399 345 2003 0 0 1 "paint"
0 327 226 399 345 2004 0 0 1 "paint"
0 327 226 399 345 2005 0 0 1 "paint"

...

Vaticデータの変換

VaticToAvaConverter

VaticToAvaConverterは,Vatic形式で作成された動画アノテーションデータをAVA形式に変換するためのクラスです.


import csv

class VaticToAvaConverter:

    def __init__(self, action_csv_path):
        """
        VaticToAvaConverterオブジェクトを初期化します.

        Parameters
        ----------
        action_csv_path : str
            アクションラベルが記載されたCSVファイルへのパス.

        Returns
        -------
        None
        """
        self.action_id_dict = {}
        with open(action_csv_path) as f:
            reader = csv.reader(f, delimiter=' ')
            for row in reader:
                for i, _act in enumerate(row):
                    self.action_id_dict[_act] = i+1
        print(self.action_id_dict.keys())

    def convert_vatic_to_ava(self, save_dir, video_id, vatic_path, img_size=(720, 405)):
        """
        VaticフォーマットのアノテーションデータをAVAフォーマットに変換します.

        Parameters
        ----------
        save_dir : str
            変換後のファイルを保存するディレクトリのパス.
        video_id : str
            変換対象の動画ID.
        vatic_path : str
            Vaticフォーマットのアノテーションファイルへのパス.
        img_size : tuple, optional
            動画の画像サイズ(横幅,縦幅).デフォルトは(720, 405).

        Returns
        -------
        None
        """
        output_path = save_dir + video_id + '_annotations.csv'
        with open(vatic_path, 'r') as f1, open(output_path, 'w', newline='') as f2:
            writer = csv.writer(f2)
            for line in f1:
                items = line.split()
                start_time = int(items[5]) / 30
                end_time = (int(items[5]) + 1) / 30
                action_id = self.action_id_dict[items[-1].replace("\"", "")]

                # 不適切なデータの場合,スキップする
                if (not action_id) or (int(items[-4])):
                    continue

                person_id = items[0]
                xtl = float(items[1])/img_size[0]
                ytl = float(items[2])/img_size[1]
                xbr = float(items[3])/img_size[0]
                ybr = float(items[4])/img_size[1]

                row = [video_id, '{:.03f}'.format((start_time + end_time) / 2), xtl, ytl, xbr, ybr, action_id, person_id]
                writer.writerow(row)

        print('Done!')

各関数の説明

init

initは,VaticToAvaConverterクラスのコンストラクタです.指定されたCSVファイルを読み取り,アクションのIDとラベル名の対応を辞書型のaction_id_dictに格納します.

  • 【引数】
    • action_csv_path (str) : アクションのIDとラベル名が含まれるCSVファイルのパス.

convert_vatic_to_ava

convert_vatic_to_avaは,Vatic形式で作成された動画アノテーションデータをAVA(Atomic Visual Actions)形式に変換するための関数です.変換後のデータは,指定されたファイルに書き出されます.

  • 【引数】
    • save_dir (str) : 変換後のデータの保存先のディレクトリ.
    • video_id (str) : アノテーション対象の動画のID.
    • vatic_path (str) : Vatic形式で作成されたアノテーションデータのパス.
    • img_size (tuple, optional) : 画像のサイズ.デフォルトは(720, 405).
  • 【戻り値】
    なし

使い方

step1:Vaticデータの準備

output.txt


0 327 226 399 345 2000 0 0 0 "paint"
0 327 226 399 345 2001 0 0 1 "paint"
0 327 226 399 345 2002 0 0 1 "paint"
0 327 226 399 345 2003 0 0 1 "paint"

...

7 327 137 353 175 3018 1 0 1 "enemy"
7 327 137 353 175 3019 1 0 1 "enemy"
7 327 137 353 175 3020 1 0 1 "enemy"
7 327 137 353 175 3021 1 0 0 "enemy"

step2:変換


root@2d50573c2a8a:/home# python Vatic2AVA.py 
dict_keys(['attack', 'paint', 'move', 'enemy', 'ally'])
Done!

データセットの可視化

extract_keyframe

このコードは,動画ファイルからフレームを取得し,アノテーションされたオブジェクトを視覚化するためのスクリプトです.スクリプトは,フレームからキーフレームを取得し,各キーフレームの注釈されたオブジェクトのバウンディングボックスを描画して出力します.また,各キーフレームから指定されたクリップを抽出して出力します.

スクリプトの使い方は,ターミナルから引数を指定して実行します.引数には,以下のものが含まれます.

  • input_videos_dir: 入力動画ファイルのあるディレクトリ
  • annotations_file: アノテーションファイル
  • outdir_keyframes: キーフレームの出力先ディレクトリ
  • outdir_bboxs: 注釈付きキーフレームの出力先ディレクトリ
  • outdir_clips: クリップの出力先ディレクトリ
  • clip_length: 抽出されたクリップの長さ
  • clip_time_padding: クリップ開始前に追加されるパディングの長さ

各関数の説明

このコードは,動画から注釈されたバウンディングボックス情報を読み込み,注釈されたアクションに従って各フレームにバウンディングボックスを可視化するためのスクリプトです.また,指定された時間位置から指定された長さのビデオクリップを切り出すこともできます.

具体的には,次の関数が実装されています.

  • load_action_name(annotations):アノテーションファイルからアクション名を読み込み,アクションIDとアクション名の対応を保持する辞書を返します.
  • load_labels(annotations):アノテーションファイルからアノテーションを読み込み,行ごとに情報を格納したリストと,ビデオIDとフレームIDの組をキー,アノテーションのインデックスを値とした辞書を返します.
  • hou_min_sec(millis):ミリ秒を受け取り,"時間:分:秒"の形式で時間を表す文字列を返します.
  • _supermakedirs(path, mode):ディレクトリを再帰的に作成するための補助関数です.
  • mkdir_p(path):ディレクトリを作成するための関数です.
  • get_keyframe(videofile, video_id, time_id, outdir_keyframes):指定された動画ファイルの指定された時間位置のフレームを取得し,指定された出力ディレクトリに保存します.
  • visual_bbox(anno_data, action_name, keyfname, video_id, time_id, bbox_ids):指定されたビデオの指定された時間位置のフレームに,指定されたバウンディングボックスの可視化を行います.
  • get_clips(videofile, video_id, video_extension, time_id):指定された動画ファイルから指定された時間位置のクリップを切り出し,指定された出力ディレクトリに保存します.

また,コマンドライン引数を解析するために,argparseライブラリが使用されています.ログの出力にはloguruライブラリが使用されており,pprintライブラリはデバッグ時に使用されることがあります.

このスクリプトの実行には,動画ファイルとアノテーションファイルが必要です.アノテーションファイルは,各フレームにおけるバウンディングボックスの情報が記録されたCSVファイルです.


def load_action_name(annotations):
    csvfile = open(annotations,'r')
    reader = list(csv.reader(csvfile))
    dic = {}
    for i in range(len(reader)-1):
        temp = (reader[i+1][1],reader[i+1][2])
        dic[i+1] = temp
    return dic

def load_labels(annotations):
    csvfile = open(annotations,'r')
    reader = list(csv.reader(csvfile))
    dic = {}
    for i in range(len(reader)):

        if (reader[i][0],reader[i][1]) in dic:
            dic[(reader[i][0],reader[i][1])].append(i)
        else:
            templist = []
            templist.append(i)
            dic[(reader[i][0],reader[i][1])] = templist
    return reader, dic

def hou_min_sec(millis):
    millis = int(millis)
    seconds = (millis / 1000) % 60
    seconds = int(seconds)
    minutes = (millis / (1000 * 60)) % 60
    minutes = int(minutes)
    hours = (millis / (1000 * 60 * 60))
    return "%d:%d:%d" % (hours, minutes, seconds)

def _supermakedirs(path, mode):
    if not path or os.path.exists(path):
        return []
    (head, _) = os.path.split(path)
    res = _supermakedirs(head, mode)
    os.mkdir(path)
    os.chmod(path, mode)
    res += [path]
    return res

def mkdir_p(path):
    try:
        _supermakedirs(path, 0o775) # Supporting Python 2 & 3
    except OSError: # Python >2.5
        pass

def get_keyframe(videofile, video_id, time_id, outdir_keyframes):
    outdir_folder = os.path.join(outdir_keyframes, video_id)
    mkdir_p(outdir_folder)
    outpath = os.path.join(outdir_folder, '%d.jpg' % (int(time_id)))
    ffmpeg_command = 'rm %(outpath)s; \
                      ffmpeg -ss %(timestamp)f -i %(videopath)s \
                      -frames:v 1 %(outpath)s' % {
                          'timestamp': float(time_id),
                          'videopath': videofile,
                          'outpath': outpath}

    subprocess.call(ffmpeg_command, shell=True)
    return outpath

def visual_bbox(anno_data, action_name, keyfname, video_id, time_id, bbox_ids):
    frame = cv2.imread(keyfname)
    frame_height, frame_width, channels = frame.shape
    outdir_folder = os.path.join(outdir_bboxs, video_id)
    mkdir_p(outdir_folder)
    outpath = os.path.join(outdir_folder, '%d_bbox.jpg' % (int(time_id)))
    draw_dic = {}

    for idx in bbox_ids:
        bbox = anno_data[idx][2:6]
        action_string = action_name[int(anno_data[idx][-2])]
        cv2.rectangle(frame, (int(float(bbox[0])*frame_width),int(float(bbox[1])*frame_height)), 
                (int(float(bbox[2])*frame_width),int(float(bbox[3])*frame_height)), [0,0,255], 1)
        x1 = int(float(bbox[0])*frame_width)
        y1 = int(float(bbox[1])*frame_height)
        print(x1, y1)

        if (x1,y1) in draw_dic:
            draw_dic[(x1,y1)] +=1
        else:
            draw_dic[(x1,y1)] = 1

        pt_to_draw = (x1,y1+20*draw_dic[(x1,y1)])          
        cv2.putText(frame, action_string[0], pt_to_draw, cv2.FONT_HERSHEY_SIMPLEX, fontScale=0.6, color=[0,255,255], thickness=1)
        draw_dic[pt_to_draw] = True

    cv2.imwrite(outpath, frame) 

def get_clips(videofile, video_id, video_extension, time_id):

    outdir_folder = os.path.join(outdir_clips, video_id)
    mkdir_p(outdir_folder)
    clip_start = time_id - clip_time_padding - float(clip_length) / 2
    if clip_start < 0:
        clip_start = 0
    clip_end = time_id + float(clip_length) / 2
    outpath_clip = os.path.join(outdir_folder, '%d.%s' % (int(time_id), video_extension))

    ffmpeg_command = 'rm %(outpath)s;  \
                      ffmpeg -ss %(start_timestamp)s -i \
                      %(videopath)s -g 1 -force_key_frames 0 \
                      -t %(clip_length)d %(outpath)s' % {
                          'start_timestamp': hou_min_sec(clip_start * 1000),
                          # 'end_timestamp': hou_min_sec(clip_end * 1000),
                          'clip_length': clip_length + clip_time_padding,
                          'videopath': videofile,
                          'outpath': outpath_clip}
    subprocess.call(ffmpeg_command, shell=True)

メインコード

このコードは,Pythonで書かれたスクリプトで,動画の前処理を行うために使用されます.このスクリプトは,引数を取り込み,指定されたディレクトリ内の動画ファイルを処理し,クリップ,キーフレーム,バウンディングボックスのデータを生成します.

このスクリプトは,argparseというライブラリを使用して,コマンドラインから引数を受け取ります.そして,このスクリプトを直接実行した場合にのみ,以下の処理が実行されます.

処理の詳細は以下の通りです.

  1. 引数のパース
    引数は,--video_dir,--annot_file,--actionlist_file,--output_dirという4つのパラメータが指定されます.それぞれ,動画ファイルのあるディレクトリパス,注釈ファイルのパス,アクションリストファイルのパス,出力ディレクトリのパスを表します.
  2. パラメータの設定
    パースされた引数から,それぞれのパラメータを設定します.
  3. ディレクトリの設定
    出力するクリップ,キーフレーム,バウンディングボックスのデータを保存するディレクトリを設定します.
  4. 変数の設定
    クリップの長さ,クリップの時間パディングなどの変数を設定します.
  5. アノテーションデータのロード
    注釈ファイルから,アノテーションデータとテーブルを読み込みます.
  6. アクション名のロード
    アクションリストファイルから,アクション名を読み込みます.
  7. 動画の各フレームに対して処理を実行
    動画の各フレームに対して,以下の処理を実行します.

    • キーフレームの抽出
    • バウンディングボックスの可視化
    • クリップの抽出

if __name__ == '__main__':

    parser = argparse.ArgumentParser()
    parser.add_argument("--video_dir", default="./video/trainval", help="Videos path.")
    parser.add_argument("--annot_file", default="ava_train_v2.1.csv",
                        help="Anotation file path.")
    parser.add_argument("--actionlist_file",
                        default="ava_action_list_v2.0.csv",
                        help="Action list file path.")
    parser.add_argument("--output_dir", default="./preproc/train", help="Output path.")

    FLAGS = parser.parse_args()

    videodir = FLAGS.video_dir
    annotfile = FLAGS.annot_file
    actionlistfile = FLAGS.actionlist_file
    outdir = FLAGS.output_dir

    outdir_clips = os.path.join(outdir, "clips")
    outdir_keyframes = os.path.join(outdir, "keyframes")
    outdir_bboxs = os.path.join(outdir, "bboxs")

    clip_length = 3 # seconds
    clip_time_padding = 1.0 # seconds

    # load data and labels from cvs files
    anno_data, table = load_labels(annotfile)
    action_name = load_action_name(actionlistfile) 

    # iterate each frame in a video
    for key in tqdm(sorted(table)):

        video_id = key[0]
        time_id = float(key[1])    
        bbox_ids = table[key]
        # print(bbox_ids)
        videofile_noext = os.path.join(videodir, video_id)

        videoflag = glob.glob(videofile_noext + "*")
        if(videoflag):
            videofile = subprocess.check_output('ls %s*' % videofile_noext, shell=True)

            videofile = videofile.split()[0]

            if sys.version > '3.0':
                videofile = videofile.decode('utf-8')
            video_extension = videofile.split('.')[-1]

            # OPEN VIDEO FOR INFORMATION IF NECESSARY
            vcap = cv2.VideoCapture(videofile) # 0=camera
            if vcap.isOpened():
                if cv2.__version__ < '3.0':
                    vidwidth = vcap.get(cv2.cv.CV_CAP_PROP_FRAME_WIDTH)   # float
                    vidheight = vcap.get(cv2.cv.CV_CAP_PROP_FRAME_HEIGHT) # float
                else:
                    vidwidth = vcap.get(cv2.CAP_PROP_FRAME_WIDTH)   # float
                    vidheight = vcap.get(cv2.CAP_PROP_FRAME_WIDTH)  # float
            else:
                exit(1)

            # Extract keyframe via ffmpeg
            # logger.info(">>> get_keyframe")
            fname = get_keyframe(videofile, video_id, time_id, outdir_keyframes)

            # Bbox visualization
            # logger.info(">>> visual_bbox")
            visual_bbox(anno_data, action_name, fname, video_id, time_id, bbox_ids)

            # Extract clips via ffmpeg
            # logger.info(">>> get_clips")
            get_clips(videofile, video_id, video_extension, time_id)

使い方

下記のコマンドで変換できます.


python3 extract_keyframe.py --annot_file annotations/ika_v0.0/20230316152944_annotations.csv \
--actionlist_file annotations/ika_v0.0/ika_action_list_v1.0.csv \
--video_dir ./video/ika-demo

変換後のアノテートファイルはこのようになります.


20230316152944,66.683,0.45416666666666666,0.5580246913580247,0.5541666666666667,0.8518518518518519,2,0
20230316152944,66.717,0.45416666666666666,0.5580246913580247,0.5541666666666667,0.8518518518518519,2,0
20230316152944,66.750,0.45416666666666666,0.5580246913580247,0.5541666666666667,0.8518518518518519,2,0
20230316152944,66.783,0.45416666666666666,0.5580246913580247,0.5541666666666667,0.8518518518518519,2,0
20230316152944,66.817,0.45416666666666666,0.5580246913580247,0.5541666666666667,0.8518518518518519,2,0
20230316152944,66.850,0.45416666666666666,0.5580246913580247,0.5541666666666667,0.8518518518518519,2,0
20230316152944,66.883,0.45416666666666666,0.5580246913580247,0.5541666666666667,0.8518518518518519,2,0
20230316152944,66.917,0.45416666666666666,0.5580246913580247,0.5541666666666667,0.8518518518518519,2,0

...

可視化

こちらpreproc\train\bboxsに可視化された画像が出力されます.

まとめ

この記事では,Vaticで出力したデータを行動認識AVAデータセットに変換する方法について説明しました.Vaticを使用してアノテーションを作成し,CSVファイルに出力します.そして,抽出した情報を使用して,AVAデータセット形式に変換します.また,行動認識についても説明しました.Vaticを使用することで,より正確な行動認識を行うことができます.

FAQs

Q1. Vaticとは何ですか?

Vaticは,ビデオフレームからアノテーションを作成するためのツールです.

Q2. AVAデータセットとは何ですか?

AVAデータセットは,行動認識に必要な映像データセットです.

Q3. 行動認識にはどのようなアルゴリズムが使用されますか?

代表的なアルゴリズムとして,Convolutional Neural Network (CNN)やLong Short-Term Memory (LSTM)などが使用されます.

Q4. Vaticで出力したデータをAVAデータセット形式に変換することは難しいですか?

Vaticで出力したデータをAVAデータセット形式に変換することは,比較的簡単です.この記事を参考にして,手順に従って実行すれば,問題なく変換することができます.

Q5. 行動認識の精度を上げるためにはどうすればよいですか?

行動認識の精度を上げるためには,データセットを増やすことが重要です.また,アルゴリズムのチューニングや,特徴量の抽出方法の改善なども有効です.

コメント

タイトルとURLをコピーしました