kazumori
@kazumori

Are you sure you want to delete the question?

Leaving a resolved question undeleted may help others!

【助けて!】FFmpeg APIで可変フレームレート動画の保存をするには?【知恵をください!】

解決したいこと

この質問の一環で、
長時間録画及び画像の表示を同時に行える、OpenCV&ffmpeg製システムを開発しようとしています。
しかし、録画した結果のfpsが狂っている様子です。
これを可変フレームレート化により解決しようと思いますが、どうすればよいでしょうか。

自前で多くのサイトを巡ったり、ヘッダー内部の解説をGPTに和訳させるなどしてそれっぽい機能の関数を探しましたが、見当たりませんでした。

迷える子羊にどうかお導きをいただけませんでしょうか。

処理の高速化、最適化などについても助言いただけると嬉しいです。

環境

  • Win10 x64 22h2
  • VC++ MFC VS2015
  • (試験環境)UVC規格準拠のノートPC内臓Webカメラ
  • (本番環境)UVC規格準拠の2592x1944 12fps 500万画素 スクエアのUSB3.0カメラ
  • OpenCV 3.4.7
  • ffmpeg 23.09.13releaseのshared

コード

動画の録画および録画中のプレビュ表示の為に録画のコードを下記のように書きました。
こちらをベース(というかほぼまんまパクリである)としています。

  • GUIとは別のスレッドでAcquireが周期的に実行されます。プレビュー表示も兼ねています。これは100msごとに実行されます。つまり仮に厳密な間隔で実行されると10fpsとなります。
  • 録画開始押下によりStartRecordで準備がされて、Acquire内のif (m_isRecord) が満たされ録画処理が行われます。
  • 最後にEndRecordで後片付けしています。
int hoge::Acquire(BYTE *pAcqImage){
	int iRet = 0;
	int cols,rows;
	HRESULT hr;
	IplImage *frame = 0;
	frame = cvCreateImage(cvSize(BitmapInfo.bmiHeader.biWidth, BitmapInfo.bmiHeader.biHeight), IPL_DEPTH_8U, 3);
	while(1){
		hr = pGrab->GetCurrentBuffer((long *)&(BitmapInfo.bmiHeader.biSizeImage), (long *)(frame->imageData));
		if(FAILED(hr)){Sleep(50); continue;}
		else{
			if(frame->origin == 0){cvFlip(frame, frame, 0);}
			cv::Mat cvColor = cv::cvarrToMat(frame);
			if (m_isRecord) {
				const int stride[4] = { static_cast<int>(cvColor.step[0]) };
				sws_scale(swsctx, &cvColor.data, stride, 0, cvColor.rows, frameCV->data, frameCV->linesize);
				frameCV->pts = frame_pts++;
				avcodec_send_frame(cctx, frameCV);
				while ((ret = avcodec_receive_packet(cctx, pkt)) >= 0) {
					pkt->duration = 1;
					av_packet_rescale_ts(pkt, cctx->time_base, vstrm->time_base);
					av_write_frame(outctx, pkt);
					av_packet_unref(pkt);
					++nb_frames;
				}
			}
			cv::Mat cvGray;
			cv::cvtColor(cvColor, cvGray, CV_RGB2GRAY);
			cols = BitmapInfo.bmiHeader.biWidth;
			rows = BitmapInfo.bmiHeader.biHeight;
			memcpy((uchar*)pAcqImage, cvGray.data, sizeof(uchar) * cols * rows);
			break;// 正常終了
		}
	}
	cvReleaseImage(&frame);
	return iRet;
}


bool hoge::StartRecord(CString filename) {
	CStringA	csaBuf(filename);
	const AVOutputFormat* outfmt = av_guess_format("mp4", nullptr, nullptr);
	int ret = avformat_alloc_output_context2(&outctx, outfmt, nullptr, nullptr);
	const AVCodec* vcodec = avcodec_find_encoder_by_name("libx265");
	vstrm = avformat_new_stream(outctx, vcodec);
	cctx = avcodec_alloc_context3(vcodec);
	const AVRational dst_fps = { iCameraFramerate[m_iCH], 1 };
	cctx->width = iCameraWidth[m_iCH];
	cctx->height = iCameraHeight[m_iCH];
	cctx->pix_fmt = AV_PIX_FMT_YUV420P;
	cctx->time_base = av_inv_q(dst_fps);
	cctx->framerate = dst_fps;
	if (outctx->oformat->flags & AVFMT_GLOBALHEADER) {
		cctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
	}
	ret = avcodec_open2(cctx, vcodec, nullptr);
	avcodec_parameters_from_context(vstrm->codecpar, cctx);
	swsctx = sws_getContext(
				iCameraWidth[m_iCH], iCameraHeight[m_iCH], 
				AV_PIX_FMT_BGR24, 
				iCameraWidth[m_iCH], iCameraHeight[m_iCH], 
				cctx->pix_fmt, SWS_BILINEAR, nullptr, nullptr, nullptr);
	frameCV = av_frame_alloc();
	frameCV->width = iCameraWidth[m_iCH];
	frameCV->height = iCameraHeight[m_iCH];
	frameCV->format = static_cast<int>(cctx->pix_fmt);
	ret = av_frame_get_buffer(frameCV, 32);
	pkt = av_packet_alloc();

	ret = avio_open2(
				&outctx->pb, csaBuf.GetBuffer(),
				AVIO_FLAG_WRITE, nullptr, nullptr);

	csaBuf.ReleaseBuffer();
	ret = avformat_write_header(outctx, nullptr);
	frame_pts = 0;
	nb_frames = 0;
	m_isRecord = true;
	return true;
}


void hoge::EndRecord() {
	if (m_isRecord) {
		av_write_trailer(outctx);
		avio_close(outctx->pb);
		av_packet_free(&pkt);
		av_frame_free(&frameCV);
		sws_freeContext(swsctx);
		avcodec_free_context(&cctx);
		avformat_free_context(outctx);
		m_isRecord = false;
	}
}
0

1Answer

Comments

  1. @kazumori

    Questioner

    婉曲的に書いてしまって申し訳ないですが、Acquireの周期がきっかり10fps分にならないのが原因です。
    ただし、周期の揺らぎ側の対策は本件では取らない(取れない)こととします。

  2. 録画されたフレームとフレームの間隔は正確に100msではないとしても全体としておおよそ10フレーム/秒の勘定になっているなら10fpsの固定フレームレートの動画ストリームであるということにしちゃっていいのだろうと思いますが、どこが問題なのですか?

  3. @kazumori

    Questioner

    出来た映像の時間の速さが明らかに狂っているからです。

  4. const AVRational dst_fps = { iCameraFramerate[m_iCH], 1 };
    cctx->framerate = dst_fps;
    avcodec_parameters_from_context(vstrm->codecpar, cctx);
    

    となっていますね。
    cctx->framerate の値はどうなっているのですか?

    引用された元コードによれば

    std::cout
            << "fps:     " << av_q2d(cctx->framerate) << "\n"
            << std::flush;
    

    で確認できるようですね。

  5. @kazumori

    Questioner

    確認いたします。
    実装環境≠記事の投稿場所なので確認は翌日になります。

  6. @kazumori

    Questioner

    cctx->framerate = dst_fps = { iCameraFramerate[m_iCH], 1 }ですので、
    iCameraFramerate[m_iCH]は10なので10fpsです。
    そういうことではないでしょうか?

    なお、撮影の周期についてですが、本来、0.1s毎になるべきものが、はじめは0.12s毎に、だんだん増えて最終的には0.14s毎に撮影されるという周期になっていました。これをどうにかするにはVFRがいいと思っています。詳しいデータは後で貼り付けます。

  7. Acquireが呼び出される周期がだんだんと延びていっているということですか。
    そこを解決すべきな気がしますが、どうにもならないのですか?

  8. @kazumori

    Questioner

    自力では難しそうです。

  9. @kazumori

    Questioner

    フレーム フレーム間
    経過時刻
    時刻 理想経過
    差分
    1 2 平均 経過
    1 00:00.0 00:00.0 4.60 4.77 4.69
    2 00:00.1 00:00.1 4.80 4.80 0.12 0.02
    3 00:00.2 00:00.1 4.80 4.92 4.86 0.06 -0.04
    4 00:00.2 00:00.1 5.08 5.08 0.22 0.12
    5 00:00.4 00:00.1 5.20 5.20 0.12 0.02
    6 00:00.5 00:00.1 5.33 5.33 0.13 0.03
    7 00:00.6 00:00.1 5.45 5.45 0.12 0.02
    8 00:00.7 00:00.1 5.58 5.58 0.13 0.03
    9 00:00.8 00:00.1 5.74 5.77 5.76 0.18 0.07
    10 00:00.9 00:00.1 5.86 5.88 5.87 0.12 0.02
    11 00:01.0 00:00.1 5.96 5.98 5.97 0.10 0.00
    12 00:01.1 00:00.1 6.08 6.08 0.11 0.01
    13 00:01.2 00:00.1 6.21 6.21 0.13 0.03
    14 00:01.3 00:00.1 6.36 6.36 0.15 0.05
    15 00:01.4 00:00.1 6.49 6.49 0.13 0.03
    16 00:01.5 00:00.1 6.62 6.62 0.13 0.03
    17 00:01.6 00:00.1 6.74 6.74 0.12 0.02
    18 00:01.7 00:00.1 6.87 6.87 0.13 0.03
    19 00:01.8 00:00.1 6.99 6.99 0.12 0.02
    20 00:01.9 00:00.1 7.12 7.12 0.13 0.03
    21 00:02.0 00:00.1 7.24 7.24 0.12 0.02
    22 00:02.2 00:00.1 7.36 7.36 0.12 0.02
    23 00:02.3 00:00.1 7.49 7.49 0.13 0.03
    24 00:02.4 00:00.1 7.68 7.64 7.66 0.17 0.07
    25 00:02.5 00:00.1 7.80 7.80 0.14 0.04
    26 00:02.6 00:00.1 7.93 7.98 7.96 0.16 0.06
    27 00:02.7 00:00.1 8.05 8.05 0.10 0.00
    28 00:02.8 00:00.1 8.21 8.21 0.16 0.06
    29 00:02.9 00:00.1 8.33 8.33 0.12 0.02
    30 00:03.0 00:00.1 8.43 8.43 0.10 0.00
    31 00:03.1 00:00.1 8.58 8.59 8.59 0.16 0.06
    32 00:03.2 00:00.1 8.72 8.72 0.14 0.03
    33 00:03.3 00:00.1 8.84 8.84 0.12 0.02
    34 00:03.4 00:00.1 8.96 8.98 8.97 0.13 0.03
    35 00:03.5 00:00.1 9.09 9.09 0.12 0.02
    36 00:03.6 00:00.1 9.24 9.24 0.15 0.05
    37 00:03.7 00:00.1 9.37 9.37 0.13 0.03
    38 00:03.8 00:00.1 9.49 9.49 0.12 0.02
    39 00:03.9 00:00.1 9.65 9.69 9.67 0.18 0.08
    40 00:04.0 00:00.1 9.77 9.77 0.10 0.00
    41 00:04.1 00:00.1 9.87 9.87 0.10 0.00
    42 00:04.2 00:00.1 9.99 9.99 0.12 0.02
    43 00:04.3 00:00.1 10.15 10.15 0.16 0.06
    44 00:04.4 00:00.1 10.27 10.27 0.12 0.02
    45 00:04.5 00:00.1 10.40 10.40 0.13 0.03
    46 00:04.6 00:00.1 10.53 10.53 0.13 0.03
    47 00:04.7 00:00.1 10.65 10.69 10.67 0.14 0.04
    48 00:04.8 00:00.1 10.75 10.75 0.08 -0.02
    49 00:04.9 00:00.1 10.90 10.90 0.15 0.05
    50 00:05.0 00:00.1 11.06 11.08 11.07 0.17 0.07
    51 00:05.1 00:00.1 11.18 11.18 0.11 0.01
    52 00:05.2 00:00.1 11.31 11.31 0.13 0.03
    53 00:05.3 00:00.1 11.47 11.44 11.46 0.15 0.04
    54 00:05.4 00:00.1 11.59 11.59 0.14 0.03
    55 00:05.5 00:00.1 11.75 11.75 0.16 0.06
    56 00:05.6 00:00.1 11.87 11.87 0.12 0.02
    57 00:05.7 00:00.1 11.97 12.00 11.99 0.12 0.02
    58 00:05.8 00:00.1 12.12 12.12 0.14 0.03
    59 00:05.9 00:00.1 12.28 12.28 0.16 0.06
    60 00:06.0 00:00.1 12.41 12.41 0.13 0.03
    61 00:06.1 00:00.1 12.56 12.56 0.15 0.05
    62 00:06.2 00:00.1 12.66 12.66 0.10 0.00
    63 00:06.3 00:00.1 12.81 12.81 0.15 0.05
    64 00:06.4 00:00.1 12.97 12.97 0.16 0.06
    65 00:06.6 00:00.1 13.09 13.09 0.12 0.02
    66 00:06.7 00:00.1 13.25 13.25 0.16 0.06
    67 00:06.8 00:00.1 13.44 13.44 0.19 0.09
    68 00:06.8 00:00.1 13.56 13.56 0.12 0.02
    69 00:07.0 00:00.1 13.69 13.69 0.13 0.03
    70 00:07.1 00:00.1 13.78 13.81 13.80 0.11 0.01
    71 00:07.2 00:00.1 13.97 13.97 0.18 0.08
    72 00:07.3 00:00.1 14.13 14.13 0.16 0.06
    73 00:07.4 00:00.1 14.28 14.28 0.15 0.05
    74 00:07.5 00:00.1 14.37 14.37 0.09 -0.01
    75 00:07.6 00:00.1 14.50 14.50 0.13 0.03
    76 00:07.7 00:00.1 14.65 14.65 0.15 0.05
    77 00:07.8 00:00.1 14.75 14.78 14.77 0.12 0.02
    78 00:07.9 00:00.1 14.91 14.91 0.15 0.04
    79 00:08.0 00:00.1 15.03 15.03 0.12 0.02
    80 00:08.1 00:00.1 15.22 15.22 0.19 0.09
    81 00:08.2 00:00.1 15.35 15.35 0.13 0.03
    82 00:08.3 00:00.1 15.47 15.47 0.12 0.02
    83 00:08.4 00:00.1 15.60 15.60 0.13 0.03
    84 00:08.5 00:00.1 15.88 15.88 0.28 0.18

    image.png

    各フレームごとの秒数は撮影した動画を読み込んだAviUtlで1フレームごとに送った際の値を読み取った値である
    各フレーム毎の時間はスマホのストップウォッチを映した結果から読み取っている
    残像があって2つの時刻の表示の間のようなタイミングの時は可能な限りその両方を読み取り、
    その平均をとった。時刻欄が二つあるのはそのため。

    理想の経過時刻は10fpsにより0.1sである。
    しかし、時刻の読み取りの誤差を鑑みてもフレームごとに結構大きな理想との差が見られ、
    全体傾向を見ると最初時点で理想から0.025sほどの誤差があり、
    時間経過で増大し0.045sほどになっている。
    増加傾向を無視しても1.25倍ほど早く録画が遅く、1.25倍ほど再生が早くなっていることが分かる。

    最初と最後が差が大きいのは開始終了処理のため仕方が無いことだろうか?

  10. フレームのキャプチャ間隔がブレていて、かつ延びて行ったりしているので、可変フレームレートで解決したい、というのが本来のご質問であることは理解しています。それに対する回答は私には用意できそうにありません。その点は申し訳ありません。
    しかし私の直感ですが、可変フレームレートで解決できるとはちょっと思えないです。あくまでも直観ですので間違っているかもしれません。
    私なら、まずはキャプチャ間隔がブレる原因、および、それが延びる方向になる原因を解析して、それに対して対策を試むと思います。
    しかしAcquireを一定間隔で呼び出す(呼び出そうとしている)仕組みがどう実装されているのかわからないので、そこはちょっと何とも言えないですね。

  11. @kazumori

    Questioner

    了解です。
    では、ほかにffmpeg以外でで動画のプレビューと録画を同時にGUIに組み込んだもので行えるものについてご存じだったりしますでしょうか?

  12. 知らないですが、探してみました。
    IamRakibAhmed/Webcam-Video-Recorder
    Visual Studio 2022でビルドできました。
    ただし私の環境の場合、プロジェクトの <TargetFramework>net5.0-windows</TargetFramework><TargetFramework>net7.0-windows</TargetFramework> に書き換える必要がありました。
    それからMainLayout.csの95行目

    FFMpeg.ReplaceAudio("video.mp4", "audio.wav", outputPath + DateTime.Now.ToString("yyyy_MM_dd_HH_mm_ss") + ".mp4", true);
    

    は例外が発生しました。たぶんaudio.wavファイルが無いからだと思いますが、めんどくさいのでコメントアウトしました。
    以上の2点の変更でビルド&実行できました。
    ffmpeg以外でとのことですが、FFMpegCoreやOpenCvSharp4は参照してますね。

  13. @kazumori

    Questioner

    お言葉ですが、本件の環境はC++です。当該システムはC#であると思われます。よって使用不能化と思われますがいかがでしょうか?

  14. 失礼ながら応用力というものをお持ちではないのですか?

  15. @kazumori

    Questioner

    無いわけではないですけど、コードを見る限りC#でラップされたライブラリの組み合わせでできてしまっている以上参考にできないです。こちらではライブラリの使い方で引っかかっているので。

  16. 録画して動画ファイルに書き出すだけならC++で書かれた簡単なコードをいくつも見つけることができると思いますが、録画しながら表示もするコードということになれば表示のための何らかのフレームワークが必須になりますよね。.NET(C#)はダメだということなので別のコードを探しましたが私は下記のQtプロジェクトしか見つけることはできませんでした。Qtなので開発言語はC++ということになります。ただ私は試してはいません。環境を作るのは正直ちょっと面倒なので。
    namansg/Image-and-Video-using-qt

  17. @kazumori

    Questioner

    せっかく探していただきましたが当該コードは録画と表示を同時は出来なさそうですがいかがでしょうか。

  18. 失礼ですが、あなたはコピペプログラマーではないのですよね?

    問題の焦点を絞りましょう。正確なフレーム間隔で動画をキャプチャできていないのが根本原因です。それができていると思われるコードはいくらでも見つかるのssで、まずはそれらのコードを分析しましょう。ご自分のコードを改善する参考になるかもしれません。プレビューは別の話です。録画しながらどういう仕組みで表示するかは後で別途工夫すればいいのでは。もし表示処理が原因でキャプチャが遅延するようなことがあるのなら、それはそうならないように表示の仕組みを考え直すべきです。

    と、書いていて気づきましたが、現在のご自分のコードで表示処理が原因でキャプチャが遅延していたりしないですか?

    とにかくAcquireがなぜ「期待どおりに」一定周期で呼ばれないのかを探句ることだと思います。

  19. @kazumori

    Questioner

    コピペプログラマに毛が10本生えた程度だと思います。
    周期がずれることについては、「RTOSでもないのだからそういうもんだ」という認知でした。
    周期処理にあまり重いのを置くのは良くない程度のことは分かってます。
    ズレうるからVFRってもんがあるんだろう。という認知です。
    ここまで沼るとは思ってなかったんですよ。ffmpegってよく見かける十二分に枯れてそうなもんだからちゃんと聞けば誰かが答えてくれるもんだと…。もちろん自前でできる限りは調べはしましたが…。

  20. RTOSでないと満足に動画の撮影はできない……わけがなく、Full HD 30fpsくらいの動画撮影なんて今どきのどんなPCやスマホでもできるわけですよね。ましてや10fpsなんて。まずはその基本的な部分ができていない根本の原因をきちんと把握する必要があると思いました。

  21. @kazumori

    Questioner

    速報ですが、周期処理の周期を取る部分がクソガバSleep関数だったので、これを修正したところ改善されました。

  22. そうですね、Sleep系は実装もまちまちで精度もそれぞれなので要注意ですね。できるだけ正確にタイミングをはかる手段を探してそれを使うということと、あと、たとえば 100ms ごとに記録したい場合、いつも100ms先を目標にするのではなくて、たとえば110ms経過してしまっていたら次は 90ms先を目標にするようにして調整するとか、またはリアルタイムクロック上で100ms刻みで目標時刻を絶対値で決めて、現在の時刻から次の時刻までの時間を待つようにするとか。

  23. @kazumori

    Questioner

    周期処理実施前に目標時間をsteady_clock::now()+目標ミリ秒で求めて周期処理実施後にthis_thread::sleep_util()でスリープさせました。中央・平均値が100msほどで安定して四分位の範囲もおおむね上下に等しい感じにまとまりました。

    本番環境だと画素数的に処理が重すぎて足が出まくりだったのでDirectShowのところで読み取りの大きさを変えて読むようにしました。実験環境では無事に小さい映像で録画できました。

    ここらへんどう高速化したものでしょうか。処理ごとにthread分けというのは思うのですけど、ここら辺の関数の使い方がどうにも…。

  24. 周期処理を安定させることができたようで良かったです。本質問の主題としては解決したのではないでしょうか。

    本番環境で目標の画素数を処理できないという問題ですが、ハードウェアの増強は可能なのかとか、それは無理なのでソフトウェアでなんとか高速化したいとか、じゃあ実際にどのルーチンがどれだけのCPU時間を食っているのか、まずはプロファイリングしてみたらどうかとか、いろいろあると思いますが、そのあたりは少しご自分で整理してみた後で、別途質問を立てるのがよろしいかと思います。

Your answer might help someone💌