キャプボからキャプチャしたゲーム画像を自動で縮小・変換する

さっさと本題に入りたい方は目次からどうぞ。

ゲーム画面の静止画をシェアしたいことってありますよね。
今時であればキャプチャー機能が付いているゲームハードは珍しくなく、手軽にSNSへシェアすることもできます。
しかし私は画面全体をキャプチャに含めなくてもいいなというときは、トリミングしてからシェアをしたいと思っていました。
例えば下記投稿はユーザーが書いたイラスト部分が主題であり、ユーザーの装備は画像に含める必要がありません。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">すき <a href="https://twitter.com/hashtag/Splatoon2?src=hash&amp;ref_src=twsrc%5Etfw">#Splatoon2</a> <a href="https://twitter.com/hashtag/%E3%82%B9%E3%83%97%E3%83%A9%E3%83%88%E3%82%A5%E3%83%BC%E3%83%B32?src=hash&amp;ref_src=twsrc%5Etfw">#スプラトゥーン2</a> <a href="https://twitter.com/hashtag/NintendoSwitch?src=hash&amp;ref_src=twsrc%5Etfw">#NintendoSwitch</a> <a href="https://t.co/wJAowYDEln">pic.twitter.com/wJAowYDEln</a></p>&mdash; あつし⛅ (@Anaakikutsushit) <a href="https://twitter.com/Anaakikutsushit/status/1106272374247161857?ref_src=twsrc%5Etfw">March 14, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

そんな中、ニンテンドーSwitch本体のアップデートでズーム機能が追加されました。
これで必要な部分だけ実質トリミングできる！と思った私でしたが、ズーム機能だけではその願いをかなえることはできませんでした。

<blockquote class="twitter-tweet" data-conversation="none"><p lang="ja" dir="ltr">ズーム機能は結構可能性を感じます。画像1枚目くらいまではズームすることが可能。ただし、この状態のままスクショボタンを押しても、本体に保存される画像はズームなしの画像（画像2枚目）になります。何かうまい使い道が出てきそうな予感。 <a href="https://t.co/ZaTh4yXO9J">pic.twitter.com/ZaTh4yXO9J</a></p>&mdash; あつし⛅ (@Anaakikutsushit) <a href="https://twitter.com/Anaakikutsushit/status/1117948791099777024?ref_src=twsrc%5Etfw">April 16, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

しかしそれでもトリミング画像をシェアしたい私は、**ゲーム画面でズームしたままにしておいてキャプチャーボードを起動し、パソコン側でキャプチャーボードに映ったズーム済みの画像をキャプチャーしてSNS投稿用に変換してからシェアする**という非常に回りくどい方法を取っていたのです。
キャプチャーするときにフォーマットを選べれば良いのですが、bmp形式での保存しか対応していないのが特にツラいところです（Twitterはbmp画像をアップロードできないっぽい？）。

いい加減面倒な方法だよなと感じたため、キャプボを起動して静止画キャプチャーする手間は減らせないものの、キャプチャー後の手間を減らすために今回のプログラムを作りました。

# 本題
次のようなプログラムにしました。

1. キャプチャーボードからのファイルの保存先フォルダは固定されている。
2. 保存先フォルダのファイルを監視し、bmpファイルが作成されたタイミングがわかるようにする。
3. 作成されたbmpファイルを縮小し、jpg形式で新規保存する。

## フォルダを監視する
[watchdog](https://pythonhosted.org/watchdog/)というモジュールを利用します。
下記記事の記述が分かりやすかったため、参考にさせて頂きました。

[ファイル監視にwatchdogがかなり便利な件 - Qiita](https://qiita.com/PyYoshi@github/items/9744a138c018734c69db)

今回はファイルが作成されたタイミングだけ知ることができればよいので、`on_created`のイベントだけ実装します。

ここで一つハマったポイントがありました。
私が利用しているキャプチャーボード`AVT-C875`および、その専用ソフト`RECentral`からのキャプチャーでファイルが作成されたとき、普通にイベントをハンドルしてしまうと`PermissionError`が発生したのです。

<blockquote class="twitter-tweet" data-conversation="none"><p lang="ja" dir="ltr">なんとなく書けたので実行。しかしpermission errorが出てしまいました。<br>キャプボから作成されたファイルについてはちゃんと補足できているみたいなんですが、なぜpermission error？</p>&mdash; あつし🌤️ (@Anaakikutsushit) <a href="https://twitter.com/Anaakikutsushit/status/1129258379329728514?ref_src=twsrc%5Etfw">May 17, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

デバッグでステップ実行すると同エラーは発生しません。
結果的に、イベントを検知したときに`time.sleep(1)`を入れることで安定して動作するようになりました。
キャプチャーソフトの動作は、ファイルを作成した瞬間にはまだ画像が「完成」しておらず、コントロールを掴んだままになっているのではないかと思っています。
その、まだ未完成の画像ファイルを読もうとしてしまったからエラーになったのではないかという推測ですね。

## 画像を変換する
`PIL`の機能を使って読み込み・リサイズ・保存まで実行します。
画像のキャンバスサイズは整数でなければいけないはずなので`round`関数で丸めました。単に縮小できればよかったので、誤差などは無視しています。

ところで、`Image.save()`関数ではフォーマットを指定できる引数が用意されているはずなんですが、`format='JPEG'`と指定しても拡張子なしで保存されてしまいました。どうやったら`format`引数にちゃんと保存形式を渡すことができるのでしょうか？ご存知の方がいらっしゃったら教えてください。

[Image.save(fp, format=None, **params)](https://pillow.readthedocs.io/en/4.0.x/reference/Image.html?highlight=image.save#PIL.Image.Image.save)

結局、`ファイル名 + '.jpg'`というパスを保存先として渡すことでjpg保存させることにしました。

# おわり
フォルダ監視って面倒なんじゃないかな～と思っていたんですが、簡単に実装できるモジュールがあって助かりました。
以下、今回作ったプログラムです。

[ConvertToJpg - GitHub](https://github.com/anaakikutushita/ConvertToJpg)

```python
# -*- coding: utf-8 -*-

import time
import os
from PIL import Image
import numpy as np

from watchdog.events import FileSystemEventHandler
from watchdog.observers import Observer

BASEDIR = os.path.abspath("E:\VideoGames\【キャプチャ映像】")

def get_ext(filename):
    return os.path.splitext(filename)[-1].lower()

class ChangeHandler(FileSystemEventHandler):

    def on_created(self, event):
        time.sleep(1)
        if event.is_directory:
            return
        if get_ext(event.src_path) == '.bmp':
            img_path = event.src_path
            bmp_img = Image.open(img_path)
            resized = bmp_img.resize(
                (round(bmp_img.width / 2), round(bmp_img.height / 2)),
                Image.NEAREST)
            filename = os.path.splitext(event.src_path)[0]
            resized.save(filename + '.jpg')

if __name__ in '__main__':
    while 1:
        event_handler = ChangeHandler()
        observer = Observer()
        observer.schedule(event_handler,BASEDIR,recursive=True)
        observer.start()
        try:
            while True:
                time.sleep(1)
        except KeyboardInterrupt:
            observer.stop()
        observer.join()
```