OpenCV C++ 動画保存時のフレーム欠損
VC++でのOpenCVのよくあるビデオカメラの読み込みと動画の保存のサンプルは、ほとんど空のプロジェクトを作成して、main() の中に全部入れてしまえ!ってやつですよね。
でも単純ループさせると、録画のフレームが全く時間管理できてない状態になってしまいます。
実際によくあるサンプルで録画したファイルを再生すると大抵早送り状態になってしまうと思います。そこで何かの参考や気づきになるかもと思い、ちょっと実験してみました。
カメラによる差
単純ループで、
cv::VideoWriter writer; //動画保存インスタンス
に対して、MATのインスタンス、大抵 frame といったネーミングになってます。
while (cap.read(frame)){ cv::imshow("", frame);//画像を表示. writer << frame; }
このようなループで出来たファイルを再生すると早送り状態になります。
じゃあ何がどうなんだろうという事で、OpenCVにかかわる時間を計測してみました。
for (;;) { //1フレームキャプチャ tts1 = clock(); cap >> frame; tts2 = clock(); //1フレーム表示 cv::imshow("", frame); tts3 = clock(); //フレーム連結 writer.write(frame); tts4 = clock(); }
計測点はそれぞれの関数の所。で2つのカメラで計測してみました。以下の結果テーブルは別の値も含んでいるので、まず #1to2=005, #2to3=01, #3to4=02 #lap という部分だけご覧ください。
#1to2 : キャプチャにかかった時間
#2to3 : 1フレームをWindow表示にかかった時間
#3to4 : writerインスタンスに1フレームを書き込んだ時間
すべて mSec 単位
5秒間のキャプチャ
マシンは Corei3 3.5GHz 12Gメモリ で少々ダル、でもそれなりに動作するレベルなPC
カメラA ELECOMの1.3Mpixelと書いてある一般的なWEBカメラ。
TTS #1to2=459, #2to3=17, #3to4=41, lap=517, nShots=14, LOOP=014 TTS #1to2=004, #2to3=01, #3to4=08, lap=54, nShots=1, LOOP=015 TTS #1to2=004, #2to3=02, #3to4=07, lap=22, nShots=1, LOOP=016 TTS #1to2=041, #2to3=02, #3to4=07, lap=57, nShots=1, LOOP=017 TTS #1to2=008, #2to3=02, #3to4=05, lap=22, nShots=1, LOOP=018 TTS #1to2=053, #2to3=03, #3to4=42, lap=103, nShots=1, LOOP=019 TTS #1to2=004, #2to3=02, #3to4=11, lap=59, nShots=2, LOOP=021 TTS #1to2=004, #2to3=03, #3to4=02, lap=20, nShots=0, LOOP=021 TTS #1to2=074, #2to3=03, #3to4=15, lap=94, nShots=3, LOOP=024 TTS #1to2=004, #2to3=02, #3to4=02, lap=23, nShots=0, LOOP=024 TTS #1to2=052, #2to3=02, #3to4=08, lap=64, nShots=2, LOOP=026 TTS #1to2=005, #2to3=02, #3to4=06, lap=21, nShots=1, LOOP=027 TTS #1to2=057, #2to3=02, #3to4=08, lap=73, nShots=2, LOOP=029 TTS #1to2=003, #2to3=02, #3to4=02, lap=15, nShots=0, LOOP=029 TTS #1to2=080, #2to3=02, #3to4=07, lap=91, nShots=2, LOOP=031 TTS #1to2=004, #2to3=01, #3to4=05, lap=17, nShots=1, LOOP=032 TTS #1to2=061, #2to3=02, #3to4=07, lap=75, nShots=2, LOOP=034 TTS #1to2=003, #2to3=02, #3to4=02, lap=14, nShots=0, LOOP=034 TTS #1to2=062, #2to3=02, #3to4=10, lap=76, nShots=2, LOOP=036 TTS #1to2=004, #2to3=02, #3to4=05, lap=21, nShots=1, LOOP=037 TTS #1to2=057, #2to3=03, #3to4=10, lap=75, nShots=2, LOOP=039 TTS #1to2=004, #2to3=03, #3to4=02, lap=19, nShots=0, LOOP=039 TTS #1to2=075, #2to3=02, #3to4=09, lap=89, nShots=3, LOOP=042 TTS #1to2=004, #2to3=01, #3to4=02, lap=17, nShots=0, LOOP=042 TTS #1to2=059, #2to3=02, #3to4=07, lap=70, nShots=2, LOOP=044 TTS #1to2=004, #2to3=02, #3to4=02, lap=15, nShots=0, LOOP=044 TTS #1to2=064, #2to3=01, #3to4=08, lap=75, nShots=2, LOOP=046 TTS #1to2=004, #2to3=01, #3to4=06, lap=20, nShots=1, LOOP=047 TTS #1to2=058, #2to3=03, #3to4=09, lap=76, nShots=2, LOOP=049 TTS #1to2=005, #2to3=02, #3to4=02, lap=18, nShots=0, LOOP=049 TTS #1to2=060, #2to3=01, #3to4=09, lap=72, nShots=2, LOOP=051 TTS #1to2=003, #2to3=02, #3to4=02, lap=17, nShots=0, LOOP=051 TTS #1to2=078, #2to3=01, #3to4=12, lap=93, nShots=3, LOOP=054 TTS #1to2=003, #2to3=02, #3to4=02, lap=19, nShots=0, LOOP=054 TTS #1to2=061, #2to3=01, #3to4=10, lap=74, nShots=2, LOOP=056 TTS #1to2=004, #2to3=01, #3to4=06, lap=21, nShots=1, LOOP=057 TTS #1to2=056, #2to3=02, #3to4=07, lap=72, nShots=2, LOOP=059 TTS #1to2=004, #2to3=01, #3to4=02, lap=14, nShots=0, LOOP=059 TTS #1to2=064, #2to3=01, #3to4=08, lap=76, nShots=2, LOOP=061 TTS #1to2=004, #2to3=02, #3to4=05, lap=20, nShots=1, LOOP=062 TTS #1to2=075, #2to3=01, #3to4=08, lap=89, nShots=2, LOOP=064 TTS #1to2=004, #2to3=02, #3to4=02, lap=16, nShots=0, LOOP=064 TTS #1to2=063, #2to3=02, #3to4=08, lap=75, nShots=2, LOOP=066 TTS #1to2=004, #2to3=02, #3to4=05, lap=19, nShots=1, LOOP=067 TTS #1to2=058, #2to3=02, #3to4=14, lap=79, nShots=2, LOOP=069 TTS #1to2=065, #2to3=01, #3to4=07, lap=87, nShots=2, LOOP=071 TTS #1to2=003, #2to3=02, #3to4=04, lap=17, nShots=1, LOOP=072 TTS #1to2=078, #2to3=01, #3to4=08, lap=91, nShots=2, LOOP=074 TTS #1to2=003, #2to3=02, #3to4=02, lap=15, nShots=0, LOOP=074 TTS #1to2=064, #2to3=02, #3to4=09, lap=77, nShots=3, LOOP=077 TTS #1to2=003, #2to3=02, #3to4=02, lap=16, nShots=0, LOOP=077 TTS #1to2=061, #2to3=01, #3to4=10, lap=74, nShots=2, LOOP=079 TTS #1to2=005, #2to3=02, #3to4=02, lap=19, nShots=0, LOOP=079 TTS #1to2=060, #2to3=03, #3to4=07, lap=72, nShots=2, LOOP=081 TTS #1to2=004, #2to3=01, #3to4=04, lap=16, nShots=1, LOOP=082 TTS #1to2=061, #2to3=02, #3to4=07, lap=75, nShots=2, LOOP=084 TTS #1to2=004, #2to3=02, #3to4=02, lap=15, nShots=0, LOOP=084 TTS #1to2=077, #2to3=02, #3to4=10, lap=91, nShots=3, LOOP=087 TTS #1to2=005, #2to3=02, #3to4=02, lap=19, nShots=0, LOOP=087 TTS #1to2=060, #2to3=02, #3to4=07, lap=72, nShots=2, LOOP=089 TTS #1to2=004, #2to3=01, #3to4=02, lap=15, nShots=0, LOOP=089 TTS #1to2=061, #2to3=01, #3to4=07, lap=72, nShots=2, LOOP=091 TTS #1to2=004, #2to3=02, #3to4=04, lap=17, nShots=1, LOOP=092 TTS #1to2=062, #2to3=01, #3to4=07, lap=74, nShots=2, LOOP=094 TTS #1to2=003, #2to3=02, #3to4=02, lap=14, nShots=0, LOOP=094 TTS #1to2=081, #2to3=02, #3to4=12, lap=97, nShots=3, LOOP=097 TTS #1to2=004, #2to3=01, #3to4=02, lap=19, nShots=0, LOOP=097 TTS #1to2=058, #2to3=03, #3to4=14, lap=78, nShots=2, LOOP=099 TTS #1to2=005, #2to3=04, #3to4=07, lap=30, nShots=1, LOOP=100 TTS #1to2=048, #2to3=01, #3to4=05, lap=61, nShots=1, LOOP=101 TTS #1to2=004, #2to3=02, #3to4=06, lap=18, nShots=1, LOOP=102 TTS #1to2=059, #2to3=02, #3to4=09, lap=76, nShots=2, LOOP=104 TTS #1to2=004, #2to3=02, #3to4=02, lap=17, nShots=0, LOOP=104 TTS #1to2=079, #2to3=01, #3to4=09, lap=91, nShots=3, LOOP=107 TTS #1to2=004, #2to3=02, #3to4=02, lap=17, nShots=0, LOOP=107 TTS #1to2=062, #2to3=02, #3to4=08, lap=74, nShots=2, LOOP=109 TTS #1to2=004, #2to3=02, #3to4=04, lap=18, nShots=1, LOOP=110 TTS #1to2=059, #2to3=02, #3to4=08, lap=74, nShots=2, LOOP=112 TTS #1to2=004, #2to3=02, #3to4=02, lap=16, nShots=0, LOOP=112 TTS #1to2=062, #2to3=02, #3to4=08, lap=74, nShots=2, LOOP=114 TTS #1to2=003, #2to3=02, #3to4=02, lap=15, nShots=0, LOOP=114 TTS #1to2=063, #2to3=02, #3to4=07, lap=74, nShots=2, LOOP=116 TTS #1to2=004, #2to3=01, #3to4=04, lap=16, nShots=1, LOOP=117 TTS #1to2=076, #2to3=02, #3to4=08, lap=90, nShots=2, LOOP=119 TTS #1to2=003, #2to3=02, #3to4=04, lap=17, nShots=1, LOOP=120 TTS #1to2=060, #2to3=02, #3to4=08, lap=75, nShots=2, LOOP=122 TTS #1to2=005, #2to3=02, #3to4=02, lap=17, nShots=0, LOOP=122 TTS #1to2=063, #2to3=02, #3to4=07, lap=74, nShots=2, LOOP=124 TTS #1to2=004, #2to3=02, #3to4=02, lap=15, nShots=0, LOOP=124 TTS #1to2=061, #2to3=02, #3to4=06, lap=71, nShots=2, LOOP=126 TTS #1to2=090, #2to3=05, #3to4=12, lap=113, nShots=3, LOOP=129 TTS #1to2=004, #2to3=03, #3to4=04, lap=23, nShots=1, LOOP=130 TTS #1to2=051, #2to3=01, #3to4=09, lap=65, nShots=2, LOOP=132 TTS #1to2=003, #2to3=02, #3to4=02, lap=16, nShots=0, LOOP=132 TTS #1to2=062, #2to3=02, #3to4=07, lap=73, nShots=2, LOOP=134 TTS #1to2=004, #2to3=01, #3to4=02, lap=15, nShots=0, LOOP=134 TTS #1to2=062, #2to3=02, #3to4=07, lap=74, nShots=2, LOOP=136 TTS #1to2=004, #2to3=02, #3to4=04, lap=17, nShots=1, LOOP=137 TTS #1to2=061, #2to3=03, #3to4=09, lap=77, nShots=2, LOOP=139 TTS #1to2=004, #2to3=02, #3to4=03, lap=18, nShots=0, LOOP=139 TTS #1to2=076, #2to3=02, #3to4=09, lap=90, nShots=3, LOOP=142 TTS #1to2=003, #2to3=02, #3to4=03, lap=17, nShots=0, LOOP=142 TTS #1to2=062, #2to3=02, #3to4=08, lap=75, nShots=2, LOOP=144 TTS #1to2=004, #2to3=01, #3to4=05, lap=19, nShots=1, LOOP=145 TTS #1to2=058, #2to3=01, #3to4=08, lap=73, nShots=2, LOOP=147 TTS #1to2=004, #2to3=02, #3to4=03, lap=17, nShots=0, LOOP=147 TTS #1to2=062, #2to3=01, #3to4=07, lap=73, nShots=2, LOOP=149 TTS #1to2=004, #2to3=02, #3to4=02, lap=15, nShots=0, LOOP=149
次に
カメラB メーカー不明のMegaPixelと書いてある物。
TTS #1to2=1466, #2to3=21, #3to4=110, lap=1597, nShots=44, LOOP=044 TTS #1to2=008, #2to3=02, #3to4=21, lap=142, nShots=4, LOOP=048 TTS #1to2=005, #2to3=02, #3to4=07, lap=36, nShots=1, LOOP=049 TTS #1to2=061, #2to3=03, #3to4=36, lap=107, nShots=2, LOOP=051 TTS #1to2=005, #2to3=01, #3to4=06, lap=48, nShots=1, LOOP=052 TTS #1to2=043, #2to3=02, #3to4=07, lap=58, nShots=2, LOOP=054 TTS #1to2=004, #2to3=02, #3to4=02, lap=15, nShots=0, LOOP=054 TTS #1to2=061, #2to3=02, #3to4=07, lap=72, nShots=2, LOOP=056 TTS #1to2=004, #2to3=01, #3to4=03, lap=15, nShots=0, LOOP=056 TTS #1to2=097, #2to3=02, #3to4=13, lap=115, nShots=3, LOOP=059 TTS #1to2=005, #2to3=04, #3to4=05, lap=27, nShots=1, LOOP=060 TTS #1to2=081, #2to3=02, #3to4=08, lap=96, nShots=3, LOOP=063 TTS #1to2=004, #2to3=01, #3to4=02, lap=16, nShots=0, LOOP=063 TTS #1to2=095, #2to3=02, #3to4=08, lap=107, nShots=3, LOOP=066 TTS #1to2=004, #2to3=01, #3to4=02, lap=15, nShots=0, LOOP=066 TTS #1to2=095, #2to3=01, #3to4=09, lap=108, nShots=3, LOOP=069 TTS #1to2=004, #2to3=02, #3to4=04, lap=19, nShots=1, LOOP=070 TTS #1to2=106, #2to3=01, #3to4=10, lap=121, nShots=3, LOOP=073 TTS #1to2=004, #2to3=01, #3to4=05, lap=20, nShots=1, LOOP=074 TTS #1to2=091, #2to3=02, #3to4=12, lap=110, nShots=3, LOOP=077 TTS #1to2=004, #2to3=03, #3to4=02, lap=22, nShots=0, LOOP=077 TTS #1to2=104, #2to3=02, #3to4=09, lap=118, nShots=3, LOOP=080 TTS #1to2=003, #2to3=02, #3to4=04, lap=18, nShots=1, LOOP=081 TTS #1to2=092, #2to3=01, #3to4=11, lap=108, nShots=3, LOOP=084 TTS #1to2=004, #2to3=02, #3to4=02, lap=19, nShots=0, LOOP=084 TTS #1to2=108, #2to3=02, #3to4=09, lap=121, nShots=4, LOOP=088 TTS #1to2=004, #2to3=01, #3to4=02, lap=17, nShots=0, LOOP=088 TTS #1to2=092, #2to3=02, #3to4=09, lap=106, nShots=3, LOOP=091 TTS #1to2=003, #2to3=02, #3to4=02, lap=16, nShots=0, LOOP=091 TTS #1to2=110, #2to3=01, #3to4=13, lap=127, nShots=4, LOOP=095 TTS #1to2=004, #2to3=02, #3to4=02, lap=21, nShots=0, LOOP=095 TTS #1to2=105, #2to3=02, #3to4=12, lap=121, nShots=4, LOOP=099 TTS #1to2=004, #2to3=01, #3to4=03, lap=21, nShots=0, LOOP=099 TTS #1to2=090, #2to3=01, #3to4=10, lap=104, nShots=3, LOOP=102 TTS #1to2=003, #2to3=02, #3to4=02, lap=17, nShots=0, LOOP=102 TTS #1to2=107, #2to3=02, #3to4=11, lap=122, nShots=4, LOOP=106 TTS #1to2=004, #2to3=01, #3to4=02, lap=18, nShots=0, LOOP=106 TTS #1to2=093, #2to3=02, #3to4=09, lap=106, nShots=3, LOOP=109 TTS #1to2=005, #2to3=02, #3to4=04, lap=20, nShots=1, LOOP=110 TTS #1to2=107, #2to3=02, #3to4=13, lap=126, nShots=3, LOOP=113 TTS #1to2=004, #2to3=02, #3to4=03, lap=22, nShots=1, LOOP=114 TTS #1to2=087, #2to3=02, #3to4=07, lap=100, nShots=2, LOOP=116 TTS #1to2=004, #2to3=02, #3to4=06, lap=20, nShots=1, LOOP=117 TTS #1to2=106, #2to3=02, #3to4=09, lap=123, nShots=3, LOOP=120 TTS #1to2=004, #2to3=01, #3to4=05, lap=19, nShots=1, LOOP=121 TTS #1to2=091, #2to3=02, #3to4=08, lap=106, nShots=3, LOOP=124 TTS #1to2=004, #2to3=02, #3to4=02, lap=16, nShots=0, LOOP=124 TTS #1to2=110, #2to3=02, #3to4=09, lap=123, nShots=3, LOOP=127 TTS #1to2=004, #2to3=02, #3to4=04, lap=19, nShots=1, LOOP=128 TTS #1to2=091, #2to3=02, #3to4=09, lap=106, nShots=3, LOOP=131 TTS #1to2=004, #2to3=02, #3to4=02, lap=17, nShots=0, LOOP=131 TTS #1to2=109, #2to3=02, #3to4=10, lap=123, nShots=4, LOOP=135 TTS #1to2=004, #2to3=02, #3to4=02, lap=18, nShots=0, LOOP=135 TTS #1to2=092, #2to3=02, #3to4=08, lap=104, nShots=3, LOOP=138 TTS #1to2=004, #2to3=02, #3to4=02, lap=16, nShots=0, LOOP=138 TTS #1to2=110, #2to3=02, #3to4=10, lap=124, nShots=4, LOOP=142 TTS #1to2=004, #2to3=02, #3to4=02, lap=18, nShots=0, LOOP=142 TTS #1to2=092, #2to3=02, #3to4=09, lap=105, nShots=3, LOOP=145 TTS #1to2=004, #2to3=01, #3to4=05, lap=19, nShots=1, LOOP=146 TTS #1to2=106, #2to3=03, #3to4=08, lap=122, nShots=3, LOOP=149 TTS #1to2=004, #2to3=03, #3to4=03, lap=18, nShots=0, LOOP=149
この結果を見ると、まず、
#lap これはfor一周にかかった時間で、30fpsなら33.33mSec以内でないとフレーム欠損している状況
つまり多量にフレーム落ちしています。カメラBの安物はまったく遅いですね。 でその処理の部分はと言えば、#1to2で表した、キャプチャの部分です。ここの数値が非常に大きくなっています。
特に1回目のループでは何をしているのかカメラBでは、1466mSec秒もかかっています。WindowsなのでOSがどのように時間配分したのか分りませんが、少なくともOpenCVは明らかに別スレッドで走っているので、Windowsアプリに組み込んでも邪魔はしません。でもかかり過ぎですね。カメラAでも459mSec。マシですが、10枚以上のフレームが落ちてます。
これをそのままビデオ保存して再生すると、5秒の録画時間が3秒程度に縮まります。
そこで、考えられるのは、タイマ割り込みで処理する方法。
これやってみました。例えば WM_TIMER で OnTimer() 云々の定番パターン。SetTIimeを1mSecとして33mSecをカウントしても、Timerそのものを33mSecにしても、結局キャプチャ時間が遅いシステムではコードが状態遷移したり複雑になるだけで、あまり意味が無いことに気付き始めました。なのでここでは全部割愛!(汗)
おそらくいろんなシステムで動作するであろうのは、実時間と録画フレーム数の関係を保ち続けることだと考え、以下の方針でコードを作成してみました。
Loop をキャプチャ回数とすれば、経過時間-(Loop*(1秒/fps)>0 なら
その値を1000/fpsで割った回数分、同じ画像を書き込んで辻褄を合わせ、必要回数Loopしたことにする。
何のことだ?と思われる方にはコードを見てもらった方が早いので、実験コードを掲示しておきます。
このコードでは、時間をスーパーインポーズして再生時にわかりやすくしています。
#include <opencv2\opencv.hpp> #include <opencv2\highgui\highgui_c.h> #include <stdio.h> #include <Windows.h> int main(void) { //ビデオ関連初期化 cv::VideoCapture cap(0); //動画キャプチャインスタンス CAM番号 0,1,.... if (cap.isOpened() == false) { return -1; } int width = (int)cap.get(cv::CAP_PROP_FRAME_WIDTH); //フレーム幅取得 int height = (int)cap.get(cv::CAP_PROP_FRAME_HEIGHT); //フレーム縦取得 double fps = cap.get(cv::CAP_PROP_FPS); //フレームレートFPS取得 int fourcc = cv::VideoWriter::fourcc('M', 'P', '4', 'V'); //エンコードMPEG4指定 cv::VideoWriter writer; //動画保存インスタンス writer.open("d:\\_temp\\recordtest.mp4", fourcc, fps, cv::Size(width, height)); cv::Mat frame; //画像用行列インスタンス //時間計測用ワーク DWORD tts0 = clock(); DWORD tts1; DWORD tts2; DWORD tts3; DWORD tts4; //フレーム調整用 DWORD msecStart = clock(); DWORD nLoop = 0; DWORD msecLap = 0; //プロセス for (;;) { //1フレームキャプチャ tts1 = clock(); cap >> frame; //経過時間をインポーズ char StrLapTime[100]; sprintf_s(StrLapTime, "%06u", msecLap); cv::putText(frame,StrLapTime,cv::Point(0,50),cv::FONT_HERSHEY_COMPLEX,2,cv::Scalar(255, 255, 0),5); tts2 = clock(); //1フレーム表示 cv::imshow("", frame); tts3 = clock(); msecLap = clock() - msecStart; //経過時間 //フレーム連結 //nLoop がここを通過した回数なので、経過時間-(nLoop*(1秒/fps)>0 なら //その値を1000/fpsで割った回数分書き込んでその回数をnLoopに加算して補正しておく int diff = msecLap - (nLoop * 1000 / fps); int nShoot = diff < 0 ? 0 : diff / (1000 / fps); nLoop += nShoot; for (int i = 0; i < nShoot; i++) //同じフレームで穴埋め writer.write(frame); //終了トリガ if (cv::waitKey(1) == 'q' || msecLap > 5000 ) //5000mSec break; tts4 = clock(); printf("TTS #1to2=%03d, #2to3=%02d, #3to4=%02d, lap=%02d, nShots=%01d, LOOP=%03d\n", tts2-tts1, tts3-tts2, tts4-tts3, tts4-tts0, nShoot, nLoop); tts0 = tts3; } cv::destroyAllWindows(); return 0; }
上の方の時間計測は、このコードの実行結果です。
#nShot は一周にかかった時間から何フレーム分の画像データを録画するか計算して、同じ画像ではありますが、必要枚数詰め込んで辻褄を合わせています。
5秒分のビデオ画像ですが、最初の一秒は止まってしまってます。ただ時間軸は正しく補正されているので、長時間録画しても実時間とのずれは無いようです。
実際のアプリケーションで使う場合は、動画のトリガとなるのが、キーボードであることは少なくて、Desktopアプリならマウスボタンのクリック、組み込みならBluetoothやらWi-Fi通信での端末からの指令というパターンが多いはず。あとはタイマー自動起動とかでしょうか。いずれの場合でもUIなど他のスレッドで動作しているプログラムが停止してしまうことは許されないはずなので、いろんな手段での工夫が必要なようです。
ずっとOpenCVを動画保存でも使っておられる方はもっとスマートな方法を使ってるのかもしれませんが、ひとまず人間が見て違和感のない範囲で許されるならこの程度の補正で大丈夫なようです。コストを掛けて良いなら、キャプチャデータをもっと高速で吐き出せるカメラを用意する方が先かもしれません。データキャプチャは高速で安定してできるなら、逆に時間待ちだけですべて処理できますからね。
ここまで実験した結果でした。