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?

オプティカルフローで「残像エフェクト」を作る

0
Last updated at Posted at 2026-03-21

はじめに

最近、オプティカルフローや特徴点検出などの画像処理の勉強をしております。
カメラ映像の「動き」を検出して、残像エフェクトをつけるコードを作成しました。

  • 手を振ると残像が残る
  • 速く動かすと派手になる
  • ゲームのエフェクトっぽい見た目

というものです。


使っている技術

■ オプティカルフロー

フレーム間の差分から「各ピクセルがどの方向にどれくらい動いたか」を計算する技術です。

今回は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()

動作させた様子

Screenshot 2026-03-21 090359 (Phone).png

残像がでます


コードの解説

① オプティカルフローで動きを取得

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 → 終了

まとめ

  • オプティカルフローで「動き」が取れる
  • それを可視化するとエフェクトになる

おわりに

本当は、北斗の拳の無想転生をやりたかったのですが、無想転生は「残像が出る技」という意味合いの技ではなかったみたいです。(すごいうろ覚えでした)

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?