MotionProで画像からアニメーション作った: 6.学習データ生成
いよいよ今回でこのシリーズの最終です。これだけ長いものを書いたのは初めてなので少し疲れましたが、自分でも結構勉強になりました。前回学習実行方法を書きその中である程度学習データにも触れました。
まず、必要ライブラリを導入します。
(motionpro)$ pip install einshape
また画像や動画のサイズを合わせるため以下のコマンドもインストールします
(motionpro)$ apt install ffmpeg
まずMotionPro/data/dot_single_video に動画をいれます(ここでは Sample.mp4)。
そのあと、サイズの正規化をします。そのうえでそれをベースに学習データを作ります
(motionpro)$ ffmpeg -y -i Sample.mp4 \
-vf "scale=-2:320,crop=512:320,fps=8" -an -pix_fmt yuv420p \
Sample_Norm.mp4
(motionpro)$ mv Sample_Norm.mp4 Sample.mp4
MotionPro のフォルダーにもどります。今のデータ構造が以下です
(motionpro)$ tree data/dot_single_video -L 2
data/dot_single_video
├── Sample.mp4
├── __pycache__
│ └── utils.cpython-310.pyc
├── checkpoints
│ ├── cvo_raft_patch_8.pth
│ ├── movi_f_cotracker2_patch_4_wind_8.pth
│ ├── movi_f_cotracker_patch_4_wind_8.pth
│ └── movi_f_raft_patch_4_alpha.pth
├── configs
│ ├── cotracker2_patch_4_wind_8.json
│ ├── cotracker_patch_4_wind_8.json
│ ├── dot_single_video_1105.yaml
│ ├── raft_patch_4_alpha.json
│ └── raft_patch_8.json
├── dot
│ ├── __init__.py
│ ├── __pycache__
│ ├── data
│ ├── models
│ └── utils
├── out
├── precess_dataset_with_dot_single_video_return_position.py
├── process_dataset_with_dot_single_video_wo_vis_return_flow.py
├── run_dot_single_video.sh
└── utils.py
以下のコマンドで学習データを生成します。
(motionpro)$ python data/dot_single_video/process_dataset_with_dot_single_video_wo_vis_return_flow.py \
--dot_config data/dot_single_video/configs/dot_single_video_1105.yaml \
--video_path data/dot_single_video/Sample.mp4 \
--save_path data/dot_single_video/out \
--eval_frame_fps 8 \
--track_time 2
すると、上のコマンドのsave_pathで指定したフォルダーに1セットのデータができます。
(motionpro)$ tree data/dot_single_video/out
data/dot_single_video/out
└── Sample
├── Sample.jpg
├── Sample.mp4
└── Sample.npy
ただこのままでスト学習に使えないため、16分割します。
以下分割するためのPYTHONを使います。
**split_dot_npy_to_per_frame.py (MotionProの直下に保存、そこで実行)
#!/usr/bin/env python3
# split_dot_npy_to_per_frame.py
import os, argparse, json, shutil, numpy as np
import cv2
from PIL import Image
def load_flow_npy(path):
import numpy as np
arr = np.load(path, allow_pickle=True)
# 1) そのまま dict の場合
if isinstance(arr, dict):
for k in ("flow", "flows", "dense_flow"):
if k in arr:
return np.asarray(arr[k])
# 2) 0次元 object 配列に dict が入っている場合
if isinstance(arr, np.ndarray) and arr.dtype == object and arr.ndim == 0:
obj = arr.item()
if isinstance(obj, dict):
for k in ("flow", "flows", "dense_flow"):
if k in obj:
return np.asarray(obj[k])
if isinstance(obj, np.ndarray):
arr = obj # 下で判定に回す
# 3) 1次元 object 配列で各要素が (H,W,2) の場合(listをnp.saveしたようなケース)
if isinstance(arr, np.ndarray) and arr.dtype == object and arr.ndim == 1:
elems = [np.asarray(x) for x in arr]
if all(e.ndim == 3 and e.shape[-1] == 2 for e in elems):
return np.stack(elems, axis=0)
# 4) すでに数値配列 (T,H,W,2) の場合
if isinstance(arr, np.ndarray) and arr.ndim == 4 and arr.shape[-1] == 2:
return arr
# 5) 単一フロー (H,W,2) の場合は T=1 として扱う
if isinstance(arr, np.ndarray) and arr.ndim == 3 and arr.shape[-1] == 2:
return arr[None, ...]
raise ValueError(f"Unrecognized .npy content: type={type(arr)}, shape={getattr(arr,'shape',None)}, dtype={getattr(arr,'dtype',None)}")
def hsv_viz(f, clip_pct=95):
u, v = f[...,0], f[...,1]
mag = np.sqrt(u*u + v*v)
ang = (np.arctan2(v, u) * 180 / np.pi) % 360
mag_clip = np.percentile(mag, clip_pct) if np.isfinite(mag).all() else 1.0
if mag_clip <= 0: mag_clip = 1.0
mag_norm = np.clip(mag / mag_clip, 0, 1)
hsv = np.zeros((*mag.shape, 3), np.uint8)
hsv[...,0] = (ang/2).astype(np.uint8) # 0..180
hsv[...,1] = 255
hsv[...,2] = (mag_norm*255).astype(np.uint8)
return cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR)
def main(npy_path, video_src=None, out_dir="split_out", mask_percentile=90):
os.makedirs(out_dir, exist_ok=True)
flow = load_flow_npy(npy_path) # (T or T-1, H, W, 2)
T = flow.shape[0]
H, W = flow.shape[1], flow.shape[2]
# per-frame flow, mask, 可視化を書き出し
for i in range(T):
f = flow[i]
np.save(os.path.join(out_dir, f"flow_{i+1:02d}.npy"), f)
# per-frame mask(フローマグの上位%を動きありと見なす)
mag = np.linalg.norm(f, axis=2)
thr = np.percentile(mag, mask_percentile)
mask = (mag > thr).astype(np.uint8) * 255
Image.fromarray(mask, "L").save(os.path.join(out_dir, f"visible_mask_{i+1:02d}.jpg"))
# 参考:色付き可視化(任意)
color = hsv_viz(f, clip_pct=max(mask_percentile, 95))
cv2.imwrite(os.path.join(out_dir, f"flow_color_{i+1:02d}.png"), color)
# 元動画も並べたい場合はコピー
if video_src and os.path.exists(video_src):
dst = os.path.join(out_dir, os.path.basename(video_src))
if os.path.abspath(video_src) != os.path.abspath(dst):
shutil.copy2(video_src, dst)
print(f"OK: {out_dir} に flow_XX.npy / visible_mask_XX.jpg / flow_color_XX.png を出力しました。")
print(f"T={T}, H={H}, W={W}")
if __name__ == "__main__":
ap = argparse.ArgumentParser()
ap.add_argument("--npy", required=True, help="まとめflowの .npy(Sample.npy など)")
ap.add_argument("--video", default=None, help="同じフォルダの動画 (任意)")
ap.add_argument("--out", default="split_out", help="出力フォルダ")
ap.add_argument("--mask_pct", type=int, default=90, help="マスクのパーセンタイル閾値(例:90)")
args = ap.parse_args()
main(args.npy, args.video, args.out, args.mask_pct)
実行
(motionpro)$ python split_dot_npy_to_per_frame.py \
--npy data/dot_single_video/out/Sample/Sample.npy \
--video data/dot_single_video/out/Sample/Sample.mp4 \
--out data/dot_single_video/out/Sample/Sample_split \
--mask_pct 90 # 必要に応じて調整
すると以下のフォルダーに分割された各種ファイルができます。
(motionpro)$ ls data/dot_single_video/out/Sample/Sample_split/
Sample.mp4 flow_07.npy flow_14.npy flow_color_05.png flow_color_12.png visible_mask_03.jpg visible_mask_10.jpg
flow_01.npy flow_08.npy flow_15.npy flow_color_06.png flow_color_13.png visible_mask_04.jpg visible_mask_11.jpg
flow_02.npy flow_09.npy flow_16.npy flow_color_07.png flow_color_14.png visible_mask_05.jpg visible_mask_12.jpg
flow_03.npy flow_10.npy flow_color_01.png flow_color_08.png flow_color_15.png visible_mask_06.jpg visible_mask_13.jpg
flow_04.npy flow_11.npy flow_color_02.png flow_color_09.png flow_color_16.png visible_mask_07.jpg visible_mask_14.jpg
flow_05.npy flow_12.npy flow_color_03.png flow_color_10.png visible_mask_01.jpg visible_mask_08.jpg visible_mask_15.jpg
flow_06.npy flow_13.npy flow_color_04.png flow_color_11.png visible_mask_02.jpg visible_mask_09.jpg visible_mask_16.jpg
ここで生成されたファイルですが
*** flow_XX.npy
光流(オプティカルフロー)のベクトル場。配列形状は (H, W, 2)、float32です。
[..., 0] = u = Δx(右が +)、[..., 1] = v = Δy(下が +)※画像座標系
例えば flow_01.npy は「フレーム1 → フレーム2」 の動き、flow_02.npy は「2→3」…という対応。
16フレームなら フローは通常 15 本(01〜15)。
*** flow_color_XX.png
上のフロー(flow_XX.npy)を HSVで可視化したPNG(目視確認用のみ・学習では未使用)。
Hue(色相)=方向、明るさ/彩度=速度の大きさ
例:赤系=右、緑系=下、青系=左、紫系=上…
***visible_mask_XX.jpg
可視/動領域マスクのグレースケール画像(0〜255)。白(255)=使う/信頼できる、黒(0)=無視。
DOT:可視性(visibility)に基づく。
この分割スクリプトで作ったものは:フローマグ(‖flow‖)の上位% を1にした簡易マスク(--mask_pctで調整:今回は--mask_pct 90)。学習時、損失の重み付けや動き領域の選別に利用。
それでは再調整した動画からできた学習データをさらにフレーム分割したものを学習データフォルダーにまずフォルダー事コピーします。
(motionpro)$ cp -r data/dot_single_video/out/Sample/Sample_split data/folders/
不要なデータを削除し、フローは16フレーム間の15フローにして動画ファイル名をvideo.mp4と改名します。
(motionpro)$ rm data/folders/Sample_split/flow_color*.png
(motionpro)$ rm data/folders/Sample_split/flow_16.npy
(motionpro)$ rm data/folders/Sample_split/visible_mask_16.jpg
(motionpro)$ mv data/folders/Sample_split/Sample.mp4 data/folders/Sample_split/video.mp4
これで準備はできました。
学習させます。
(motionpro)$ python train_ddp_spawn.py \
--base configs/train_debug_from_folder.yaml \
--train True \
--logdir all_results/train \
--scale_lr False
batch_num が4になっていることがわかります。前回は3学習セットで3でしたが今回の子の学習データセットをいれたことで4になりました。
ちなみに、以下のエラーは無視してもOKです。
Exception ignored in: <function _MultiProcessingDataLoaderIter.__del__ at 0x7dbc3bdd1ea0>
Traceback (most recent call last):
File "/root/.pyenv/versions/motionpro/lib/python3.10/site-packages/torch/utils/data/dataloader.py", line 1477, in __del__
self._shutdown_workers()
File "/root/.pyenv/versions/motionpro/lib/python3.10/site-packages/torch/utils/data/dataloader.py", line 1460, in _shutdown_workers
if w.is_alive():
File "/root/.pyenv/versions/3.10.16/lib/python3.10/multiprocessing/process.py", line 160, in is_alive
assert self._parent_pid == os.getpid(), 'can only test a child process'
AssertionError: can only test a child process
これは学習が終わった後、Python 終了時に DataLoader のワーカープロセスを片付ける (del) タイミングで出るメッセージですがDataLoader は num_workers>0 だとサブプロセスを立てますが、Lightning/DeepSpeed の並列実行と組み合わさると、ワーカー終了時の「親プロセスID」が入れ替わって見えることがあります。その結果、multiprocessing 側のアサート
assert self._parent_pid == os.getpid(), 'can only test a child process'
が発火します。Exception ignored in: del と出ているとおり、処理自体は完了しています。
Discussion