Lチカで始めるテスト自動化(28)Webカメラの映像にフレームの情報を重畳する

## 1.はじめに
Webカメラの映像にdatetime.datetime.now()で取得した時刻情報を重畳すると時刻の間隔にムラがあることが[Lチカで始めるテスト自動化(27)矩形の画像を動画のフレームから切り出してファイルに保存する](https://qiita.com/pbjpkas/items/a99cdfa1b4e5d9e62b3e)で判明しました。

そこでWebカメラの映像に埋め込まれているタイムスタンプ情報を利用することを考えます。筆者が使用しているOpenCV Ver.4.5.4の[VideoCaptureProperties](https://docs.opencv.org/4.5.4/d4/d15/group__videoio__flags__base.html#gaeb8dd9c89c10a5c63c139bf7c4f5704d)によるとCAP_PROP_POS_MSEC[^1]を取得すれば良さそうです。併せてフレームの取りこぼしが発生しているかを確認できるようCAP_PROP_POS_FRAMES[^2]も取得します。

## 2.プログラム
[Lチカで始めるテスト自動化(20)複数のカメラ映像の同時録画 5.camera.py](https://qiita.com/pbjpkas/items/cdfc0238b54bf197e98a#5-camerapy)に以下の改修を行います。

### 2.1 text_over_img
タイムスタンプ情報を引数でもらうようにします。併せて文字の大きさや重畳する座標を調整します。

```python
    def text_over_img(self, img, timestamp):
        #timestamp = (datetime.datetime.now().strftime("%Y/%m/%d %H:%M:%S.%f")[:-3])
        videotxt  = self.get_video_txt()
        cv2.rectangle(img, (5,5), (self.w-5,45), (255,255,255), thickness=-1)
        cv2.putText(img, timestamp, ( 10,35), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0,0,0), 1, cv2.LINE_AA)
        cv2.putText(img, videotxt,  (950,35), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0,0,0), 1, cv2.LINE_AA)
```

### 2.2 capture_cam
datetime.datetime.now()を取得しtext_over_img()へ渡すようにします。

```python
    def capture_cam(self, filename):
        if self.cam == False:
            print("CAM Not Ready.")
            return False

        ret, img = self.cam.read()
        if ret == True:
            timestamp = (datetime.datetime.now().strftime("%Y/%m/%d %H:%M:%S.%f")[:-3])
            self.text_over_img(img, timestamp)
            cv2.imwrite(filename, img)
            return True
        else:
            return False
```

### 2.3 video_record
datetime.datetime.now()、CAP_PROP_POS_FRAMES、CAP_PROP_POS_MSECを取得しtext_over_img()へ渡すようにします。

```python
    def video_record(self, filename):
        if self.cam == False:
            print("CAM Not Ready.")
            return False

        fourcc = cv2.VideoWriter_fourcc('m','p','4','v')
        out = cv2.VideoWriter(filename, fourcc, self.f, (self.w, self.h))
        msg = "CAM " + str(self.camera_id) + " : q to quit"

        while True:
            ret, img = self.cam.read()
            pos_frames = str(self.cam.get(cv2.CAP_PROP_POS_FRAMES))
            pos_msec   = str(self.cam.get(cv2.CAP_PROP_POS_MSEC))
            timestamp = (datetime.datetime.now().strftime("%Y/%m/%d %H:%M:%S.%f")[:-3]) + "/" + pos_frames + "/" + pos_msec
            print(timestamp)
            if ret == True:
                self.text_over_img(img, timestamp)
                out.write(img)
                cv2.imshow(msg, img)

            if (cv2.waitKey(1) == ord('q')) or (self.get_video_rec() == False):
                print("### rec stop ###")
                break
```

### 2.4 実行例
Webカメラの画像をmov10s.mp4というファイル名で10秒録画します。

```csv:mov10s.csv
open_cam,1,1280,720
rec_start,1,mov10s.mp4
sleep,10
rec_stop,1
```

```
python testrunner.py runscript mov10s.csv mov10s_res.csv
```

print(timestamp)の出力の一部を以下に示します。
```
##### 2023/03/26 22:43:42 4 #####
4:sleep,10,
2023/03/26 22:43:44.693/21689529.0/722984293.1444
2023/03/26 22:43:44.807/21689533.0/722984426.4777
2023/03/26 22:43:44.881/21689534.0/722984459.8111
2023/03/26 22:43:44.919/21689535.0/722984493.1444
2023/03/26 22:43:44.982/21689537.0/722984559.8111
2023/03/26 22:43:45.043/21689539.0/722984626.4777
```

## 3. OpenCVのソースを見てみる
フレーム番号が飛んでいるのはともかく小数点一桁まで表示されているのは違和感がありますね。Webカメラが1枚の画像をインターレース方式で2回に分けて伝送しているとも思えないし、もしそうだとしても0ばかりというのもおかしなところです。そこでOpenCVのソースを見てみます。

### 3.1 手順
1. OpenCV 4.5.4のソースコードをチェックアウトする[^3]
    1. git clone https://github.com/opencv/opencv.git
    1. git checkout refs/tags/4.5.4
1. クローンしたフォルダをVisual Studio Codeで開く
1. CV_CAP_PROP_POS_FRAMESを検索する
1. （筆者の環境はmsmfを使用しているので）modules\videoio\src\cap_msmf.cppの当該コードを見る 

### 3.2 cap_msmf.cppの当該コード
cap_msmf.cpp内でCV_CAP_PROP_POS_FRAMESが登場するのは以下の関数です。
- double CvCapture_MSMF::getProperty( int property_id )
- bool CvCapture_MSMF::setProperty( int property_id, double value )

getに対応するコードから該当部分を引用します。

```cpp:cap_msmf.cpp
double CvCapture_MSMF::getProperty( int property_id ) const
{
    long cVal = 0;
    if (isOpen)
        switch (property_id)
        {
（略）
        case CV_CAP_PROP_POS_FRAMES:
            return floor(((double)sampleTime / 1e7)* captureFormat.getFramerate() + 0.5);
        case CV_CAP_PROP_POS_MSEC:
            return (double)sampleTime / 1e4;
（略）
        default:
            break;
        }
    return -1;
}
```

floor()関数で小数点以下を切り捨てしたdouble型の整数が返ってくることが分かりました。double型といっても整数なのでf文字列で小数点以下を割愛します。

```python
            #pos_frames = str(self.cam.get(cv2.CAP_PROP_POS_FRAMES))
            pos_frames = str(f'{self.cam.get(cv2.CAP_PROP_POS_FRAMES):.0f}')
```

フレーム番号がスッキリしました。
```
##### 2023/04/02 16:02:36 4 #####
4:sleep,10,
2023/04/02 16:02:38.188/1884971/62832380.2888
2023/04/02 16:02:38.347/1884976/62832546.9555
2023/04/02 16:02:38.399/1884977/62832580.2888
2023/04/02 16:02:38.436/1884979/62832646.9555
2023/04/02 16:02:38.484/1884980/62832680.2888
2023/04/02 16:02:38.540/1884981/62832713.6221
2023/04/02 16:02:38.571/1884982/62832746.9555
2023/04/02 16:02:38.596/1884983/62832780.2888
2023/04/02 16:02:38.620/1884984/62832813.6221
```

実使用上はprint(timestamp)をコメントアウトします。

## 4. タイムスタンプ情報を分析する
録画した映像の例を以下に示します。
![映像例](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221758/40e71c58-162b-29ba-2baa-c1be999d95fd.png)

録画開始直後を除き、フレーム番号が1ずつ増えていてフレームの取りこぼしが発生していないことが分かります。
![フレーム番号の増分](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221758/83911066-3c6c-619a-588c-841079f2ef4e.png)

フレーム間隔もnow()関数で取得している方は波打っていますがCV_CAP_PROP_POS_MSECで取得している方は録画開始直後を除き33msで安定しています。
![frame-interval.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221758/daa8755d-4638-8517-4528-c430426a8005.png)

## 5. おわりに
Webカメラの映像に付随するフレーム番号を参照することでフレームの取りこぼしが明確にわかるようになりました。また、フレーム間隔も安定した値が得られました。

## 付録A. camera.pyのソース
```python:camera.py
# -*- coding: utf-8 -*-
# camera.py
# python 3.x
#
# This software includes the work that is distributed
# in the Apache License 2.0
# ka's@pbjpkas 2021, 2023
#

import cv2
import datetime

class Camera:
    def __init__(self, camera_id):
        self.camera_id = camera_id
        self.w         = 640
        self.h         = 480
        self.f         = 30.0
        self.video_txt = "null" # Text over Video Frame
        self.video_rec = False  # Video Recording Thread flag
        self.thread    = False
        self.cam       = False  # cv2.VideoCapture() handler

    def set_video_txt(self, txt):
        self.video_txt = str(txt)

    def get_video_txt(self):
        return self.video_txt

    def set_video_rec(self, flag):
        if flag:
            self.video_rec = True
        else:
            self.video_rec = False

    def get_video_rec(self):
        return self.video_rec

    def open_cam(self, width, height):
        if self.cam == False:
            self.cam = cv2.VideoCapture(self.camera_id)
            self.cam.set(cv2.CAP_PROP_FRAME_WIDTH, width)
            self.cam.set(cv2.CAP_PROP_FRAME_HEIGHT, height)
            self.w = int(self.cam.get(cv2.CAP_PROP_FRAME_WIDTH))
            self.h = int(self.cam.get(cv2.CAP_PROP_FRAME_HEIGHT))
            self.f = self.cam.get(cv2.CAP_PROP_FPS)
            print(str(self.w) + "x" + str(self.h) + "," + str(self.f) + "fps")
        else:
            print("CAM " + str(self.camera_id) + " Already Opend.")

    def close_cam(self):
        if self.cam != False:
            self.cam.release()
            self.cam = False
            print("CAM " + str(self.camera_id) + " Released.")
        else:
            print("CAM " + str(self.camera_id) + " Not Opend.")

    def text_over_img(self, img, timestamp):
        #timestamp = (datetime.datetime.now().strftime("%Y/%m/%d %H:%M:%S.%f")[:-3])
        videotxt  = self.get_video_txt()
        cv2.rectangle(img, (5,5), (self.w-5,45), (255,255,255), thickness=-1)
        cv2.putText(img, timestamp, ( 10,35), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0,0,0), 1, cv2.LINE_AA)
        cv2.putText(img, videotxt,  (950,35), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0,0,0), 1, cv2.LINE_AA)

    def capture_cam(self, filename):
        if self.cam == False:
            print("CAM Not Ready.")
            return False

        ret, img = self.cam.read()
        if ret == True:
            timestamp = (datetime.datetime.now().strftime("%Y/%m/%d %H:%M:%S.%f")[:-3])
            self.text_over_img(img, timestamp)
            cv2.imwrite(filename, img)
            return True
        else:
            return False

    def video_record(self, filename):
        if self.cam == False:
            print("CAM Not Ready.")
            return False

        fourcc = cv2.VideoWriter_fourcc('m','p','4','v')
        out = cv2.VideoWriter(filename, fourcc, self.f, (self.w, self.h))
        msg = "CAM " + str(self.camera_id) + " : q to quit"

        while True:
            ret, img = self.cam.read()
            #pos_frames = str(self.cam.get(cv2.CAP_PROP_POS_FRAMES))
            pos_frames = str(f'{self.cam.get(cv2.CAP_PROP_POS_FRAMES):.0f}')
            pos_msec   = str(self.cam.get(cv2.CAP_PROP_POS_MSEC))
            timestamp = (datetime.datetime.now().strftime("%Y/%m/%d %H:%M:%S.%f")[:-3]) + "/" + pos_frames + "/" + pos_msec
            #print(timestamp)
            if ret == True:
                self.text_over_img(img, timestamp)
                out.write(img)
                cv2.imshow(msg, img)

            if (cv2.waitKey(1) == ord('q')) or (self.get_video_rec() == False):
                print("### rec stop ###")
                break

        out.release()
        return True
```

## 付録B. Lチカで始めるテスト自動化・記事一覧
1. [Lチカで始めるテスト自動化](https://qiita.com/pbjpkas/items/fd4bd2689deda2d793aa)
2. [Lチカで始めるテスト自動化(2)テストスクリプトの保守性向上](https://qiita.com/pbjpkas/items/337bce392281ee5f0526)
3. [Lチカで始めるテスト自動化(3)オシロスコープの組込み](https://qiita.com/pbjpkas/items/b80b7e199a5aed4688a4)
4. [Lチカで始めるテスト自動化(4)テストスクリプトの保守性向上(2)](https://qiita.com/pbjpkas/items/a02c4128baf5a3f2a46c)
5. [Lチカで始めるテスト自動化(5)WebカメラおよびOCRの組込み](https://qiita.com/pbjpkas/items/9af9e6441e1e20c5909e)
6. [Lチカで始めるテスト自動化(6)AI(機械学習)を用いたPass/Fail判定](https://qiita.com/pbjpkas/items/405d55f809da1221b867)
7. [Lチカで始めるテスト自動化(7)タイムスタンプの保存](https://qiita.com/pbjpkas/items/8e3c4330f4a81d30bd53)
8. [Lチカで始めるテスト自動化(8)HDMIビデオキャプチャデバイスの組込み](https://qiita.com/pbjpkas/items/58f84c347e44fb9d5635)
9. [Lチカで始めるテスト自動化(9)6DoFロボットアームの組込み](https://qiita.com/pbjpkas/items/7a4cb4c59886b1fd2cf9)
10. [Lチカで始めるテスト自動化(10)6DoFロボットアームの制御スクリプトの保守性向上](https://qiita.com/pbjpkas/items/c011d7ad6f98bc1458d5)
11. [Lチカで始めるテスト自動化(11)ロボットアームのコントローラ製作](https://qiita.com/pbjpkas/items/fa6d2956ea3dd18b3584)
12. [Lチカで始めるテスト自動化(12)書籍化の作業メモ](https://qiita.com/pbjpkas/items/e76a763de7f496d5430c)
13. [Lチカで始めるテスト自動化(13)外部プログラムの呼出し](https://qiita.com/pbjpkas/items/717b2aba39798d8cab1d)
14. [Lチカで始めるテスト自動化(14)sleepの時間をランダムに設定する](https://qiita.com/pbjpkas/items/4f023917f31f6096eb6b)
15. [Lチカで始めるテスト自動化(15)Raspberry Pi Zero WHでテストランナーを動かして秋月のIoT学習HATキットに進捗を表示する](https://qiita.com/pbjpkas/items/a2e7551ae10f7dce2505)
16. [Lチカで始めるテスト自動化(16)秋月のIoT学習HATキットにBME280を接続してテスト実行環境の温度・湿度・気圧を取得する](https://qiita.com/pbjpkas/items/0a48ca71a2de0042e188)
17. [Lチカで始めるテスト自動化(17)コマンド制御のBLE Keyboard & MouseをM5Stackで製作しiOSアプリをテストスクリプトで操作する](https://qiita.com/pbjpkas/items/3df58743644128a11b9e)
18. [Lチカで始めるテスト自動化(18)秋月のIoT学習HATキットの圧電ブザーでテスト終了時にpass/failに応じてメロディを流す](https://qiita.com/pbjpkas/items/43a609581cc68b7ea2fd)
19. [Lチカで始めるテスト自動化(19)Webカメラの映像を録画しながらテストスクリプトを実行する](https://qiita.com/pbjpkas/items/589243af5c9ff0dc933c)
20. [Lチカで始めるテスト自動化(20)複数のカメラ映像の同時録画](https://qiita.com/pbjpkas/items/cdfc0238b54bf197e98a)
21. [Lチカで始めるテスト自動化(21)キーボード入力待ちの実装](https://qiita.com/pbjpkas/items/9e3c21c3cd375635568a)
22. [Lチカで始めるテスト自動化(22)DACをコマンドで制御してLチカする](https://qiita.com/pbjpkas/items/35d1450804f3bfbe238e)
23. [Lチカで始めるテスト自動化(23)ブレッドボード互換のユニバーサル基板](https://qiita.com/pbjpkas/items/ca510b483e7f36e97d96)
24. [Lチカで始めるテスト自動化(24)コマンドの実行結果を任意のCSVファイルへ出力する](https://qiita.com/pbjpkas/items/7751a08eaaa0e3d50298)
25. [Lチカで始めるテスト自動化(25)Windowsでもテスト終了時にpass/failに応じてメロディを流す](https://qiita.com/pbjpkas/items/5d8ca74b011d4c8d6e43)
26. [Lチカで始めるテスト自動化(26)テストランナーのモジュール化](https://qiita.com/pbjpkas/items/ce2d032f9ee9d641a229)
27. [Lチカで始めるテスト自動化(27)矩形の画像を動画のフレームから切り出してファイルに保存する](https://qiita.com/pbjpkas/items/a99cdfa1b4e5d9e62b3e)

電子書籍化したものを[技術書典](https://techbookfest.org/)の[ka'sらぼ](https://techbookfest.org/organization/4777931404476416)で頒布しています。

1. [Lチカで始めるテスト自動化](https://techbookfest.org/product/5661312486998016)
2. [Lチカで始めるテスト自動化 -2- リレー駆動回路の設計](https://techbookfest.org/product/5299462253576192) (※書き下ろし)
3. [Lチカで始めるテスト自動化 -3- Raspberry Pi Zero WHとIoT学習HATキットで作るテストランナー](https://techbookfest.org/product/6323836549070848)

[^1]: Current position of the video file in milliseconds.
[^2]: 0-based index of the frame to be decoded/captured next.
[^3]: 最初からタグを指定してクローンしても構いません。