はじめに
最近、オプティカルフローや特徴点検出などの画像処理の勉強をしております。
カメラ映像の「動き」を検出して、残像エフェクトをつけるコードを作成しました。
- 手を振ると残像が残る
- 速く動かすと派手になる
- ゲームのエフェクトっぽい見た目
というものです。
使っている技術
■ オプティカルフロー
フレーム間の差分から「各ピクセルがどの方向にどれくらい動いたか」を計算する技術です。
今回はOpenCVの
cv2.calcOpticalFlowFarneback()
を使っています。
処理の流れ(ざっくり)
① 前フレームと現在フレームを比較
② ピクセルごとの動き(ベクトル)を取得
③ 動きの強さ(速度)に変換
④ 速度に応じて色をつける
⑤ 時間方向に蓄積して「残像」にする
コード
import cv2
import numpy as np
EFFECT_FIRE = 'fire'
EFFECT_GHOST = 'ghost'
EFFECT_PURE = 'pure'
EFFECT_NONE = 'none'
def get_color_for_speed(speed_map, mode):
t = np.clip(speed_map, 0, 1)
if mode == EFFECT_FIRE:
r = np.clip(t * 2, 0, 1)
g = np.clip(t * 2 - 1, 0, 1)
b = np.zeros_like(t)
elif mode == EFFECT_GHOST:
v = t * 0.8
r, g = v, v
b = np.clip(v + 0.1, 0, 1)
else: # pure
r = g = b = t
return np.stack([b, g, r], axis=2) # BGR
def main():
cap = cv2.VideoCapture(0)
if not cap.isOpened():
print("カメラが見つかりません")
return
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
# パラメータ
decay = 0.85 # 残像の長さ (0〜1, 高いほど長く残る)
sensitivity = 15 # 感度 (低いほど微細な動きに反応)
mode = EFFECT_FIRE # fire / ghost / pure
modes = [EFFECT_FIRE, EFFECT_GHOST, EFFECT_PURE, EFFECT_NONE]
mode_idx = 0
prev_gray = None
ghost_buffer = None # float32 BGR残像バッファ
print("==== 残像エフェクト ====")
print("操作:")
print(" Q / ESC : 終了")
print(" M : エフェクト切替 (fire/ghost/pure/none)")
print(" ↑ / ↓ : 残像の長さ調整")
print(" ← / → : 感度調整")
print(f"現在: mode={mode}, decay={decay:.2f}, sensitivity={sensitivity}")
while True:
ret, frame = cap.read()
frame_copy = frame.copy()
if not ret or frame is None:
break
#frame = cv2.flip(frame, 1) # 鏡反転
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
h, w = gray.shape
if ghost_buffer is None:
ghost_buffer = np.zeros((h, w, 3), dtype=np.float32)
prev_gray = gray
continue
# オプティカルフロー(Farnebäck)
flow = cv2.calcOpticalFlowFarneback(
prev_gray, gray,
None,
pyr_scale=0.5,
levels=3,
winsize=15,
iterations=3,
poly_n=5,
poly_sigma=1.2,
flags=0
)
mag, _ = cv2.cartToPolar(flow[..., 0], flow[..., 1])
# 速度を正規化
speed = np.clip(mag / (sensitivity * 1.5), 0, 1)
# 速度 → 色
color_frame = get_color_for_speed(speed, mode) * 255.0 # float BGR 0-255
# 残像バッファを更新(指数減衰 + 新しい動き成分を加算)
ghost_buffer = ghost_buffer * decay + color_frame * (1 - decay)
# 合成
if mode == EFFECT_PURE:
output = ghost_buffer.astype(np.uint8)
elif mode == EFFECT_NONE:
output = frame_copy
else:
output = frame.astype(np.float32)
ghost_alpha = np.clip(ghost_buffer.max(axis=2, keepdims=True) / 255.0, 0, 1)
output = output * (1 - ghost_alpha) + ghost_buffer * ghost_alpha
output = np.clip(output, 0, 255).astype(np.uint8)
# HUDオーバーレイ
mode_label = f"Mode: {mode} | Decay: {decay:.2f} | Sensitivity: {sensitivity}"
cv2.putText(output, mode_label, (10, 25),
cv2.FONT_HERSHEY_SIMPLEX, 0.55, (200, 200, 200), 1, cv2.LINE_AA)
cv2.putText(output, "M:mode UP/DOWN:decay LEFT/RIGHT:sensitivity Q:quit",
(10, h - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.42, (150, 150, 150), 1, cv2.LINE_AA)
cv2.imshow("Hokuto Ghost Effect", output)
prev_gray = gray
key = cv2.waitKey(1) & 0xFF
if key in (ord('q'), 27): # Q or ESC
break
elif key == ord('m'):
mode_idx = (mode_idx + 1) % len(modes)
mode = modes[mode_idx]
print(f"Mode: {mode}")
elif key == 82 or key == ord('w'): # UP
decay = min(decay + 0.02, 0.99)
print(f"Decay: {decay:.2f}")
elif key == 84 or key == ord('s'): # DOWN
decay = max(decay - 0.02, 0.1)
print(f"Decay: {decay:.2f}")
elif key == 83 or key == ord('d'): # RIGHT
sensitivity = min(sensitivity + 1, 50)
print(f"Sensitivity: {sensitivity}")
elif key == 81 or key == ord('a'): # LEFT
sensitivity = max(sensitivity - 1, 1)
print(f"Sensitivity: {sensitivity}")
cap.release()
cv2.destroyAllWindows()
if __name__ == "__main__":
main()
動作させた様子
残像がでます
コードの解説
① オプティカルフローで動きを取得
flow = cv2.calcOpticalFlowFarneback(prev_gray, gray, None, ...)
これで以下が取れます
- flow[..., 0] → x方向の動き
- flow[..., 1] → y方向の動き
② 動きの強さ(速度)を計算
mag, _ = cv2.cartToPolar(flow[..., 0], flow[..., 1])
ベクトルの長さ=スピード
③ 速度を0〜1に正規化
speed = np.clip(mag / (sensitivity * 1.5), 0, 1)
- sensitivityを下げる → 小さい動きにも反応
- sensitivityを上げる → 大きな動きだけ強調
④ 速度 → 色に変換
color_frame = get_color_for_speed(speed, mode)
例:
- fire → 赤〜黄色(炎)
- ghost → 青白い(幽霊)
👉 「速いほど派手」になる
⑤ 残像の生成
ghost_buffer = ghost_buffer * decay + color_frame * (1 - decay)
過去のフレームを少しずつ残しながら、新しい情報を追加する
- decayが大きい → 残像が長く残る
- 小さい → すぐ消える
⑥ 元映像と合成
output = output * (1 - ghost_alpha) + ghost_buffer * ghost_alpha
動いている部分だけ残像を重ねる
パラメータ調整
| パラメータ | 説明 |
|---|---|
| decay | 残像の長さ |
| sensitivity | 動きの検出感度 |
| mode | 色のスタイル |
操作方法
-
M→ モード切替 -
↑ / ↓→ 残像の長さ -
← / →→ 感度 -
Q / ESC→ 終了
まとめ
- オプティカルフローで「動き」が取れる
- それを可視化するとエフェクトになる
おわりに
本当は、北斗の拳の無想転生をやりたかったのですが、無想転生は「残像が出る技」という意味合いの技ではなかったみたいです。(すごいうろ覚えでした)
