web-dev-qa-db-ja.com

個別のフレーム画像をディスクに書き込まずに、C ++プログラムで生成されたいくつかの画像からビデオをエンコードする方法は?

私はC++コードを書いており、その中に実装されたいくつかの操作を実行した後、N個の異なるフレームのシーケンスが生成されます。各フレームが完成したら、それをIMG_%d.pngとしてディスクに書き込み、最後にx264コーデックを使用してffmpegでビデオにエンコードします。

プログラムの主要部分の要約された疑似コードは次のとおりです。

std::vector<int> B(width*height*3);
for (i=0; i<N; i++)
{
  // void generateframe(std::vector<int> &, int)
  generateframe(B, i); // Returns different images for different i values.
  sprintf(s, "IMG_%d.png", i+1);
  WriteToDisk(B, s); // void WriteToDisk(std::vector<int>, char[])
}

この実装の問題は、必要なフレーム数Nが通常高い(N〜100000)だけでなく、画像の解像度(1920x1080)であるため、ディスクが過負荷になり、数十GBの書き込みサイクルが発生することです。各実行後。

これを回避するために、ベクトルBに格納されている各画像をx264などのエンコーダーに直接解析する方法についてドキュメントを見つけようとしました(中間画像ファイルをディスクに書き込む必要はありません)。興味深いトピックがいくつか見つかりましたが、それらの多くはディスク上の既存の画像ファイルを使用したエンコーダーの実行に関するものであり、他のものはPython( here そのプラットフォームに完全に満足できるソリューションを見つけることができます)。

私が取得したいものの疑似コードはこれに似たものです:

std::vector<int> B(width*height*3);
video_file=open_video("Generated_Video.mp4", ...[encoder options]...);
for (i=0; i<N; i++)
{
  generateframe(B, i+1);
  add_frame(video_file, B);
}
video_file.close();

私が関連トピックで読んだことによると、x264 C++ APIはこれを行うことができるかもしれませんが、前述のように、私は私の特定の質問に対して満足のいく答えを見つけられませんでした。私はffmpegのソースコードを直接学習して使用しようとしましたが、使いやすさとコンパイルの問題の両方が原因で、この可能性を単なるプロではないプログラマーとして捨てざるを得ませんでした(趣味と同じように、不運にも無駄にはできません)その多くの時間はとても厳しい何かを学びます)。

私の頭に浮かんだもう1つの可能な解決策は、C++コードでffmpegバイナリファイルを呼び出す方法を見つけ、どうにかして各反復の画像データ(Bに格納されている)をエンコーダーに転送し、各フレームを追加できるようにすることです(つまり、書き込むビデオファイルを「閉じる」のではなく)最後のフレームまで。これにより、N番目のフレームに到達するまでフレームを追加して、ビデオファイルを「閉じる」ことができます。つまり、C++プログラムを介してffmpeg.exeを呼び出し、最初のフレームをビデオに書き込みますが、エンコーダーはより多くのフレームを「待機」します。次に、ffmpegを再度呼び出して2番目のフレームを追加し、エンコーダーにさらに多くのフレームを「待機」させる、というように、ビデオが終了する最後のフレームに達するまで続けます。しかし、どうすればいいのか、それが実際に可能かどうかはわかりません。

編集1:

返信で示唆されているように、私は名前付きパイプについて文書化していて、コードでそれらを使用しようとしました。まず最初に、私がCygwinを使用していることに注意してください。私の名前付きパイプは、Linuxで作成されるのと同じように作成されます。私が使用した変更済みの疑似コード(対応するシステムライブラリを含む)は次のとおりです。

FILE *fd;
mkfifo("myfifo", 0666);

for (i=0; i<N; i++)
{
  fd=fopen("myfifo", "wb");
  generateframe(B, i+1);
  WriteToPipe(B, fd); // void WriteToPipe(std::vector<int>, FILE *&fd)
  fflush(fd);
  fd=fclose("myfifo");
}
unlink("myfifo");

WriteToPipeは、以前のWriteToFile関数をわずかに変更したもので、画像データを送信するための書き込みバッファーが、パイプのバッファリングの制限に合わせて十分に小さいことを確認しました。

次に、Cygwinターミナルで次のコマンドをコンパイルして書き込みます。

./myprogram | ffmpeg -i pipe:myfifo -c:v libx264 -preset slow -crf 20 Video.mp4

ただし、「fopen」行(つまり、最初のfopen呼び出し)でi = 0の場合は、ループのままです。私がffmpegを呼び出さなかった場合、サーバー(私のプログラム)がクライアントプログラムがパイプの「反対側」に接続するのを待つのは自然なことですが、そうではありません。どういうわけかパイプを介して接続できないようですが、この問題を解決するための詳細なドキュメントは見つかりませんでした。なにか提案を?

19
ksb496

いくつかの激しい戦いの後、FFmpegとlibx264 C APIを私の特定の目的に使用する方法を少し学んだ後、私はようやくそれをうまく機能させることができました。 FFmpegのドキュメント例。説明のために、詳細を次に示します。

まず、libx264 Cライブラリがコンパイルされ、その後、構成オプション--enable-gpl --enable-libx264が付いたFFmpegライブラリがコンパイルされました。それではコーディングに行きましょう。要求された目的を達成したコードの関連部分は次のとおりです。

含まれるもの:

#include <stdint.h>
extern "C"{
#include <x264.h>
#include <libswscale/swscale.h>
#include <libavcodec/avcodec.h>
#include <libavutil/mathematics.h>
#include <libavformat/avformat.h>
#include <libavutil/opt.h>
}

MakefileのLDFLAGS:

-lx264 -lswscale -lavutil -lavformat -lavcodec

内部コード(わかりやすくするために、エラーチェックは省略され、変数の宣言は、理解を深めるために、最初ではなく必要に応じて行われます):

av_register_all(); // Loads the whole database of available codecs and formats.

struct SwsContext* convertCtx = sws_getContext(width, height, AV_PIX_FMT_RGB24, width, height, AV_PIX_FMT_YUV420P, SWS_FAST_BILINEAR, NULL, NULL, NULL); // Preparing to convert my generated RGB images to YUV frames.

// Preparing the data concerning the format and codec in order to write properly the header, frame data and end of file.
char *fmtext="mp4";
char *filename;
sprintf(filename, "GeneratedVideo.%s", fmtext);
AVOutputFormat * fmt = av_guess_format(fmtext, NULL, NULL);
AVFormatContext *oc = NULL;
avformat_alloc_output_context2(&oc, NULL, NULL, filename);
AVStream * stream = avformat_new_stream(oc, 0);
AVCodec *codec=NULL;
AVCodecContext *c= NULL;
int ret;

codec = avcodec_find_encoder_by_name("libx264");

// Setting up the codec:
av_dict_set( &opt, "preset", "slow", 0 );
av_dict_set( &opt, "crf", "20", 0 );
avcodec_get_context_defaults3(stream->codec, codec);
c=avcodec_alloc_context3(codec);
c->width = width;
c->height = height;
c->pix_fmt = AV_PIX_FMT_YUV420P;

// Setting up the format, its stream(s), linking with the codec(s) and write the header:
if (oc->oformat->flags & AVFMT_GLOBALHEADER) // Some formats require a global header.
    c->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
avcodec_open2( c, codec, &opt );
av_dict_free(&opt);
stream->time_base=(AVRational){1, 25};
stream->codec=c; // Once the codec is set up, we need to let the container know which codec are the streams using, in this case the only (video) stream.
av_dump_format(oc, 0, filename, 1);
avio_open(&oc->pb, filename, AVIO_FLAG_WRITE);
ret=avformat_write_header(oc, &opt);
av_dict_free(&opt); 

// Preparing the containers of the frame data:
AVFrame *rgbpic, *yuvpic;

// Allocating memory for each RGB frame, which will be lately converted to YUV:
rgbpic=av_frame_alloc();
rgbpic->format=AV_PIX_FMT_RGB24;
rgbpic->width=width;
rgbpic->height=height;
ret=av_frame_get_buffer(rgbpic, 1);

// Allocating memory for each conversion output YUV frame:
yuvpic=av_frame_alloc();
yuvpic->format=AV_PIX_FMT_YUV420P;
yuvpic->width=width;
yuvpic->height=height;
ret=av_frame_get_buffer(yuvpic, 1);

// After the format, code and general frame data is set, we write the video in the frame generation loop:
// std::vector<uint8_t> B(width*height*3);

上記のコメント付きベクトルは、私の質問で明らかにしたものと同じ構造です。ただし、RGBデータは特定の方法でAVFrameに保存されます。したがって、説明のために、代わりに、uint8_t [3] Matrix(int、int)形式の構造体へのポインターがあり、特定の座標(x、 y)は、それぞれMatrix(x、y)-> Red、Matrix(x、y)-> GreenおよびMatrix(x、y)-> Blueであり、それぞれ赤、緑、青の値を取得します。座標(x、y)。最初の引数は、xの増加に応じて左から右への水平位置を表し、2番目の引数は、yの増加に応じて上から下への垂直位置を表します。

とはいえ、データを転送し、各フレームをエンコードして書き込むforループは次のようになります。

Matrix B(width, height);
int got_output;
AVPacket pkt;
for (i=0; i<N; i++)
{
    generateframe(B, i); // This one is the function that generates a different frame for each i.
    // The AVFrame data will be stored as RGBRGBRGB... row-wise, from left to right and from top to bottom, hence we have to proceed as follows:
    for (y=0; y<height; y++)
    {
        for (x=0; x<width; x++)
        {
            // rgbpic->linesize[0] is equal to width.
            rgbpic->data[0][y*rgbpic->linesize[0]+3*x]=B(x, y)->Red;
            rgbpic->data[0][y*rgbpic->linesize[0]+3*x+1]=B(x, y)->Green;
            rgbpic->data[0][y*rgbpic->linesize[0]+3*x+2]=B(x, y)->Blue;
        }
    }
    sws_scale(convertCtx, rgbpic->data, rgbpic->linesize, 0, height, yuvpic->data, yuvpic->linesize); // Not actually scaling anything, but just converting the RGB data to YUV and store it in yuvpic.
    av_init_packet(&pkt);
    pkt.data = NULL;
    pkt.size = 0;
    yuvpic->pts = i; // The PTS of the frame are just in a reference unit, unrelated to the format we are using. We set them, for instance, as the corresponding frame number.
    ret=avcodec_encode_video2(c, &pkt, yuvpic, &got_output);
    if (got_output)
    {
        fflush(stdout);
        av_packet_rescale_ts(&pkt, (AVRational){1, 25}, stream->time_base); // We set the packet PTS and DTS taking in the account our FPS (second argument) and the time base that our selected format uses (third argument).
        pkt.stream_index = stream->index;
        printf("Write frame %6d (size=%6d)\n", i, pkt.size);
        av_interleaved_write_frame(oc, &pkt); // Write the encoded frame to the mp4 file.
        av_packet_unref(&pkt);
    }
}
// Writing the delayed frames:
for (got_output = 1; got_output; i++) {
    ret = avcodec_encode_video2(c, &pkt, NULL, &got_output);
    if (got_output) {
        fflush(stdout);
        av_packet_rescale_ts(&pkt, (AVRational){1, 25}, stream->time_base);
        pkt.stream_index = stream->index;
        printf("Write frame %6d (size=%6d)\n", i, pkt.size);
        av_interleaved_write_frame(oc, &pkt);
        av_packet_unref(&pkt);
    }
}
av_write_trailer(oc); // Writing the end of the file.
if (!(fmt->flags & AVFMT_NOFILE))
    avio_closep(oc->pb); // Closing the file.
avcodec_close(stream->codec);
// Freeing all the allocated memory:
sws_freeContext(convertCtx);
av_frame_free(&rgbpic);
av_frame_free(&yuvpic);
avformat_free_context(oc);

サイドノート:

将来の参考のために、タイムスタンプ(PTS/DTS)に関するネット上の入手可能な情報が非常にわかりにくいので、次に、適切な値を設定して問題を解決する方法を説明します。これらの値を誤って設定すると、フレームデータがFPSによって実際に設定された時間間隔よりも短い時間間隔で冗長的に書き込まれるため、出力サイズがffmpegビルドバイナリコマンドラインツールで取得したものよりもはるかに大きくなりました。

まず、エンコードするときに2種類のタイムスタンプがあることに注意してください。1つはフレーム(PTS)に関連付けられ(プレエンコードステージ)、2つはパケットに関連付けられています(PTSおよびDTS)(ポストエンコードステージ)。 。最初のケースでは、フレームPTS値をカスタム参照単位を使用して割り当てることができるようです(一定のFPSが必要な場合は等間隔でなければならないという唯一の制限があります)。たとえば、次のようにフレーム番号を取得できます。上記のコードで行いました。 2つ目では、次のパラメーターを考慮する必要があります。

  • 出力フォーマットコンテナーのタイムベース。この例ではmp4(= 12800 Hz)で、その情報はstream-> time_baseに保持されます。
  • ビデオの望ましいFPS。
  • エンコーダーがBフレームを生成するかどうか(2番目のケースでは、PTSとDTS=フレームの値を同じに設定する必要がありますが、最初のケースの場合はさらに複雑になります。この例のように。参照先については、別の関連する質問の answer を参照してください。

ここで重要なのは、libavが前述のデータを知ることによってパケットに関連付けられた正しいタイムスタンプを計算する機能を提供するため、幸運にもこれらの量の計算に苦労する必要がないことです。

av_packet_rescale_ts(AVPacket *pkt, AVRational FPS, AVRational time_base)

これらの考慮事項のおかげで、コマンドラインツールを使用して得られるものと本質的に同じ圧縮率で、正常な出力コンテナーを生成することができました。これは、フォーマットヘッダーとトレーラーの方法と時間スタンプは正しくセットされています。

21
ksb496

@ ksb496、素晴らしい仕事をありがとう!

1つのマイナーな改善:

c=avcodec_alloc_context3(codec);

次のように書く必要があります:

c = stream->codec;

メモリリークを回避するため。

よろしければ、デプロイ可能な完全なライブラリをGitHubにアップロードしました: https://github.com/apc-llc/moviemaker-cpp.git

1
Dmitry Mikushin

API:avcodec_encode_video2とavcodec_encode_audio2は廃止されたようです。現在のバージョン(4.2)のFFmpegには、新しいAPI:avcodec_send_frame&avcodec_receive_packetがあります。

0
Andy Chou

Ksb496のおかげでなんとかこのタスクを実行できましたが、私の場合、一部のコードを変更して期待どおりに機能させる必要があります。他の人の役に立つかもしれないと思ったので、共有することにしました(2年遅れで:D)。

私は、ビデオの取得に必要なdirectshowサンプルグラバーで満たされたRGBバッファーを持っていました。 RGBからYUVへの変換は、私にとってはうまくいきませんでした。私はそれをこのようにしました:

int stride = m_width * 3;
int index = 0;
for (int y = 0; y < m_height; y++) {
    for (int x = 0; x < stride; x++) {
        int j = (size - ((y + 1)*stride)) + x;
        m_rgbpic->data[0][j] = data[index];
        ++index;
    }
}

ここでのdata変数はRGBバッファー(単純なBYTE*)であり、sizedataバッファーサイズ(バイト単位)です。左下から右上に向かってRGBAVFrameへの入力が始まります。

もう1つは、私のバージョンのFFMPEGにはav_packet_rescale_ts関数がなかったことです。これは最新バージョンですが、FFMPEGのドキュメントでは、この関数はどこでも非推奨であるとは言われていませんでした。とにかく、私は av_rescale_q を代わりに使用しましたが、同じ仕事をします。このような :

AVPacket pkt;
pkt.pts = av_rescale_q(pkt.pts, { 1, 25 }, m_stream->time_base);

そして最後に、このフォーマット変換を使用して、次のようにBGR24ではなくswsContextRGB24に変更する必要がありました。

m_convert_ctx = sws_getContext(width, height, AV_PIX_FMT_BGR24, width, height,
        AV_PIX_FMT_YUV420P, SWS_FAST_BILINEAR, nullptr, nullptr, nullptr);
0
HMD