Pythonで線画アニメーションを作ってみる（線画の応用）

みなさん、こんにちは！

先日、[Pythonで線画を作ってみる](https://qiita.com/yam_dev/items/beadcc5b74451c8c0f24)という記事を書きました。

これを応用した[水墨画の記事](https://qiita.com/yam_dev/items/7d073f2924797851fb01)も書きましたが、今度は、線画を動画として保存して、線画アニメーションぽくしてみようと思います。

# 要件
・画像ではなく、動画（mp4）として保存する
・ファイルサイズはできるだけ小さくする

# コード
環境構築については、[線画の記事](https://qiita.com/yam_dev/items/beadcc5b74451c8c0f24)をご確認ください。
```python:sample.py
import cv2
import numpy as np

def line_drawing_image(img):
    # 画像のコントラストを調整
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    gray = cv2.equalizeHist(gray)

    # ガウシアンブラーを適用
    gray_blurred = cv2.GaussianBlur(gray, (5, 5), 0)
    
    # エッジ検出で線画を生成
    edges = cv2.adaptiveThreshold(gray_blurred, 255,
                                  cv2.ADAPTIVE_THRESH_MEAN_C,
                                  cv2.THRESH_BINARY, 11, 7)
    
    return edges

def ink_painting_effect(img):
    # グレースケールに変換
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

    # バイラテラルフィルタを適用してぼかしを強化
    filtered = cv2.bilateralFilter(gray, 9, 200, 75)

    # CLAHEを適用してコントラストを強化
    clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(5, 5))
    contrast_enhanced = clahe.apply(filtered)

    # ハイライトを強調
    alpha = 1.3 # コントラスト
    beta = 40   # 明るさ
    highlighted = cv2.addWeighted(contrast_enhanced, alpha, np.zeros_like(contrast_enhanced), 0, beta)

    # エッジ検出（閾値を調整）
    edges = cv2.adaptiveThreshold(highlighted, 255,
                                  cv2.ADAPTIVE_THRESH_MEAN_C,
                                  cv2.THRESH_BINARY, 15, 7)

    # マスクを使用して元の画像とエッジを組み合わせる
    ink_painting = cv2.bitwise_and(highlighted, highlighted, mask=edges)

    return ink_painting

# VideoCaptureを作成
cap = cv2.VideoCapture(0)

# カメラのFPSを取得
fps = cap.get(cv2.CAP_PROP_FPS)

# カメラの解像度を取得
frame_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
frame_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))

if not cap.isOpened():
    print("カメラが開けません。")
    exit()

fourcc = cv2.VideoWriter_fourcc(*'mp4v')
frame_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
frame_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
# VideoWriterを作成
out = cv2.VideoWriter('line_painting.mp4', fourcc, fps, (frame_width, frame_height))

frame_count = 0

# カメラがオープンしている間はループ
while True:
    ret, frame = cap.read()
    if not ret:
        print("フレームの取得に失敗しました。")
        break

    # 線画風の画像を生成
    line_drawing = line_drawing_image(frame)
    # 水墨画風の画像を生成（こっちに切り替えると水墨画風になる）
    # line_drawing = ink_painting_effect(frame)

    # 20フレームごとに画像を保存（連続だとファイルサイズが大きくなるため）
    if frame_count != 0 and frame_count % 20 == 0:
        # グレースケール画像を3チャンネルのBGR画像に変換
        line_drawing_bgr = cv2.cvtColor(line_drawing, cv2.COLOR_GRAY2BGR)
        # VideoWriterを使用してフレームを書き込む
        out.write(line_drawing_bgr)

    frame_count += 1

    # 生成した画像を表示する
    cv2.imshow('Ink Painting Effect', line_drawing)


    # 'q'が押されたらループを抜ける
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

# ループ終了後の処理
cap.release()  # カメラのリリース
out.release()  # VideoWriterのリリース
cv2.destroyAllWindows()
```

# 実行結果
```bash
python sample.py
```
実行するとこうなりました。
![fKBmurg8.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407770/1f780927-03a6-fe76-7c88-3679af4d2cb5.gif)

ちゃんと動画として保存できてますね！

# 苦労した点
- **FPSの値がよく分からなかった**
カメラのFPS値を渡す必要があったのですが、この値が分からなかったです。
「cv2.CAP_PROP_FPS」で取得できることが分かり、どうにか解決しました。

- **グレースケールのままでは動画を保存できなかった**
最初、何度試しても、mp4のヘッダしか保存されず、ここが一番時間がかかりました。
試しにカメラから取得したframeをそのまま保存してみたらできたので、グレースケールが原因と判明。
「cv2.COLOR_GRAY2BGR」で解決しました。

# まとめ
今回は線画を応用して、アニメーションぽくしてみました。
簡単に保存できると思ってトライしてみましたが、思ったより苦戦しました。
しかし、初めて得た知識もあったので、チャレンジしてみて良かったです:smiley:

これからも日々チャレンジして、スキルアップを目指します！