撮影の処理時間を簡潔に測定する方法

# はじめに

　カメラの露出時間を変更したときに処理時間がどう変わるか確認する方法は色々あるかと思います。本記事では私的に簡潔に書くとこうなる、というのを載せます。

# 条件
　以下の条件に従って作成します。
- 処理時間は移動平均にする
- 移動平均の結果はコンソールに表示する
- 表示は見やすいように整形された形にする

# 最終的に完成するもの
　露出時間を変更したときの処理時間を測定するデモです。処理時間が変化するのが分かります。

![camera_stopwatch.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/314095/97094f5f-36e1-8a60-cc64-b87d8d06e8f1.gif)

```python: 使用したコード
import cv2
from stopwatch import stopwatch

cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)
cap.set(cv2.CAP_PROP_SETTINGS, 1)

while True:
    with stopwatch():
        _, img = cap.read()
    cv2.imshow("img", cv2.resize(img, None, fx=0.5, fy=0.5))  # 動画撮影用に小さくしています
    cv2.waitKey(1)
```

　ここからstopwatchの作り方について解説していきます。


# 処理時間の測定

　下記のように書いても良いのですが、前後にコードを書くのを避けるため今回はコンテキストマネージャを使用します。
```python:
start_time = time.time()
# 測定したい処理
print(time.time() - start_time)
```

```python: コンテキストマネージャを使用した処理時間測定関数
from contextlib import contextmanager

@contextmanager
def stopwatch():
    start_time = time.time()
    yield
    print(time.time() - start_time)
```

コンテキストマネージャを使用することで処理開始時に`start_time`が記録され、終了時に`time.time()-start_time`の計算結果がprintされます。

```python: 使用例
with stopwatch():
    time.sleep(0.1)  # 0.10009241104125977

with stopwatch():
    time.sleep(0.2)  # 0.20009565353393555
```

簡潔ですね。

# 移動平均を求める

　下記のように書けば一応は撮影時間が計算できます。しかし大抵の場合、更新間隔が速すぎて読みにくくなります。

```python
with stopwatch():
    _, img = cap.read()
```

　そこで処理時間を移動平均で表示できるようにします。ゴリゴリに書いても良いのですが、今回は簡潔に書きたいので以下のようなクラスを作成します。
- 移動平均の計算対象となるリストの長さを指定できる
- 平均値を計算できる
- 制限された長さ未満でも平均値を計算できる
- 制限された長さ以上の入力があった場合、最初の入力を削除する(FIFO)

作成したクラスはこちら。

```python: moving_average.py
from collections import deque

class MovingAverage:
    def __init__(self, max_size):
        """MovingAverageクラスの初期化

        Args:
            max_size (int): リストの最大サイズ
        """
        self.data = deque(maxlen=max_size)

    def add(self, item):
        """要素をリストに追加します。

        Args:
            item: 追加する要素
        """
        self.data.append(item)

    def get_average(self):
        """リストの要素の平均値を計算して返します。

        Returns:
            float: リストの要素の平均値
        """
        return sum(self.data) / len(self.data)

    def get_data(self):
        """リストの要素を取得します。

        Returns:
            list: リストの要素を格納したリスト
        """
        return list(self.data)
```

```python: 使用例
moving_average = MovingAverage(5)  # 最大長が5のリストにする

moving_average.add(1)
moving_average.add(2)
moving_average.add(3)

# 最大長未満の動作確認
print(moving_average.get_data())  # [1, 2, 3]
print(moving_average.get_average())  # 2.0

moving_average.add(4)
moving_average.add(5)
moving_average.add(6)

# FIFOの確認
print(moving_average.get_data())  # [2, 3, 4, 5, 6]
print(moving_average.get_average())  # 4.0
```

ばっちりですね。

# 表示部分の作成

　`print`で表示するとコンソールに文字が流れていくので読みにくいです。`putText`で画像中に表示するのも良いですが、今回は画像に手を加えたくないので`print`で表示させます。
　そんなときに便利なのが`print("\r表示させたい文字列", end="")`です。`\r`はカーソルを行頭に移動させる特殊文字です。これに改行を無効化する`end=""`を組み合わせてその場で表示を更新させます。

```python: その場で表示を更新する
import time

while True:
    print(f"\r{time.time()}", end="")
    time.sleep(0.5)
```

# 最終的な処理時間表示関数
　ここまでの処理を一つにまとめます。さらに`print`する際のもうひと工夫で下記も追加しています。
- 秒オーダーからミリ秒オーダーへの変換
- 少数点第一位まで表示
- 2桁ms～4桁msまでの変化するため、見やすくなるように表示文字長を指定
 
```python: stopwatch.py
from contextlib import contextmanager
from limited_list import LimitedList
import time

LIST_LENGTH = 10
limited_list = LimitedList(LIST_LENGTH)

@contextmanager
def stopwatch():
    start_time = time.time()
    yield
    limited_list.add(time.time() - start_time)
    print(f"\r処理時間:{(limited_list.get_average()*1000): >6.1f}ms", end="")
```

　これを[冒頭のコード](#最終的に完成するもの)のように呼び出してあげればOKです。非常に簡潔ですね！