2015-12-29 29 views
10

Sto scrivendo un codice C++ in cui viene generata una sequenza di N frame diversi dopo aver eseguito alcune operazioni in esso implementate. Dopo aver completato ogni frame, lo scrivo sul disco come IMG_% d.png e infine li codifico in un video tramite ffmpeg usando il codec x264.Come codificare un video da diverse immagini generate in un programma C++ senza scrivere le immagini del frame separate su disco?

Lo pseudo riepilogativo della parte principale del programma è la seguente:

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[]) 
} 

Il problema di questa implementazione è che il numero di fotogrammi desiderati, N, è generalmente elevato (N ~ 100000) come così come la risoluzione delle immagini (1920x1080), risultante in un sovraccarico del disco, producendo cicli di scrittura di dozzine di GB dopo ogni esecuzione.

Per evitare questo, ho cercato di trovare la documentazione sull'analisi di ogni immagine memorizzata nel vettore B su un encoder come x264 (senza dover scrivere i file di immagine intermedi sul disco). Anche se sono stati trovati alcuni argomenti interessanti, nessuno di loro ha risolto in modo specifico quello che voglio esattamente, dato che molti riguardano l'esecuzione dell'encoder con i file di immagini esistenti sul disco, mentre altri forniscono soluzioni per altri linguaggi di programmazione come Python (here tu può trovare una soluzione pienamente soddisfacente per quella piattaforma).

Il pseudocodice di quello che vorrei ottenere è qualcosa di simile a questo:

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(); 

Secondo quello che ho letto su argomenti correlati, la x264 C++ API potrebbe essere in grado di fare questo, ma, come detto sopra, non ho trovato una risposta soddisfacente per la mia domanda specifica. Ho provato ad imparare e usare direttamente il codice sorgente ffmpeg, ma sia la sua bassa facilità d'uso che i miei problemi di compilazione mi hanno costretto a scartare questa possibilità come un semplice programmatore non professionista che sono (lo prendo come un semplice hobby e sfortunatamente non posso sprecare che molte volte imparano qualcosa di così impegnativo).

Un'altra possibile soluzione che mi è venuta in mente è trovare un modo per chiamare il file binario ffmpeg nel codice C++ e in qualche modo riuscire a trasferire i dati dell'immagine di ciascuna iterazione (memorizzata in B) all'encoder, lasciando che il aggiunta di ogni frame (cioè, non "chiudendo" il file video da scrivere) fino all'ultimo frame, in modo da poter aggiungere più frame fino a raggiungere l'N-esimo, in cui il file video sarà "chiuso". In altre parole, chiama ffmpeg.exe tramite il programma C++ per scrivere il primo fotogramma su un video, ma fai in modo che l'encoder "aspetti" per più fotogrammi. Quindi chiama di nuovo ffmpeg per aggiungere il secondo frame e fare in modo che l'encoder "attenda" di nuovo per altri frame, e così via fino a raggiungere l'ultimo frame, dove il video sarà finito. Tuttavia, non so come procedere o se è effettivamente possibile.

Edit 1:

Come suggerito nelle risposte, mi sono documentato su named pipe e ha cercato di usarle nel mio codice. Prima di tutto, si dovrebbe notare che sto lavorando con Cygwin, quindi i miei pipe nominati vengono creati così come sarebbero creati sotto Linux. Il pseudocodice modificata ho usato (comprese le librerie di sistema corrispondenti) è la seguente:

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 è una leggera modifica della funzione WriteToFile precedente, dove ho fatto in modo che il buffer di scrittura per inviare i dati di immagine è piccola abbastanza per adattarsi ai limiti del buffering del tubo.

Poi posso compilare e scrivere il seguente comando nel terminale Cygwin:

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

Tuttavia, rimane bloccato al ciclo quando i = 0 alla linea "fopen" (cioè, la prima chiamata fopen). Se non avessi chiamato ffmpeg sarebbe naturale come il server (il mio programma) sarebbe in attesa di un programma client per connettersi all '"altro lato" del tubo, ma non è il caso. Sembra che non possano essere collegati attraverso il tubo in qualche modo, ma non sono stato in grado di trovare ulteriore documentazione al fine di superare questo problema. Qualche suggerimento?

+0

Hai provato a usare named pipe? Per quanto riguarda FFMPEG, può accettare pipe denominate come input -i pipe: pipe_name [Esempio su msdn.microsoft] (https://msdn.microsoft.com/en-us/library/windows/desktop/aa365592 (v = vs. 85) .aspx) –

+0

Grazie per il suggerimento. Ho imparato a conoscere le pipe con nome e ho cercato di passare attraverso quella strada. I nuovi problemi che sono apparsi dopo aver provato questo metodo sono esposti nella mia nuova modifica. – ksb496

risposta

13

Dopo un'intensa lotta, sono finalmente riuscito a farlo funzionare dopo aver imparato un po 'come utilizzare le API C FFmpeg e libx264 per il mio scopo specifico, grazie alle informazioni utili che alcuni utenti hanno fornito in questo sito e alcuni altri, così come alcuni esempi di documentazione di FFmpeg. Per motivi di illustrazione, i dettagli verranno presentati di seguito.

Prima di tutto, la libreria C libx264 è stata compilata e, successivamente, quella FFmpeg con le opzioni di configurazione --enable-gpl --enable-libx264. Ora andiamo alla codifica. La parte rilevante del codice che ha raggiunto l'obiettivo richiesto è il seguente:

Include:

#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> 
} 

LDFLAGS su Makefile:

-lx264 -lswscale -lavutil -lavformat -lavcodec 

codice interno (per semplicità, la le verifiche degli errori verranno omesse e le dichiarazioni delle variabili verranno apportate quando necessario anziché all'inizio per una migliore comprensione):

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); 

Il vettore commentato sopra ha la stessa struttura di quello esposto nella mia domanda; tuttavia, i dati RGB vengono memorizzati su AVFrames in un modo specifico. Pertanto, per ragioni di esposizione, supponiamo di avere invece un puntatore a una struttura del modulo uint8_t [3] Matrix (int, int), il cui modo di accedere ai valori di colore dei pixel per una determinata coordinata (x, y) è Matrix (x, y) -> Red, Matrix (x, y) -> Green e Matrix (x, y) -> Blue, per ottenere, rispettivamente, i valori rosso, verde e blu del coordinata (x, y). Il primo argomento indica la posizione orizzontale, da sinistra a destra quando x aumenta e il secondo per la posizione verticale, dall'alto verso il basso mentre y aumenta.

Essere che detto, il per ciclo per trasferire i dati, codificare e scrivere ogni fotogramma sarebbe il seguente:

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); 

note collaterali:

Per riferimento futuro, come le informazioni disponibili sulla rete relative ai timestamp (PTS/DTS) appaiono così confuse, spiegherò anche come sono riuscito a risolvere i problemi impostando i valori corretti. L'impostazione di questi valori ha causato in modo errato che la dimensione dell'output fosse molto più grande di quella ottenuta tramite lo strumento di riga di comando binario ffmpeg, poiché i dati del frame venivano scritti in modo ridondante attraverso intervalli di tempo più piccoli rispetto a quelli effettivamente impostati da FPS.

Prima di tutto, va notato che durante la codifica ci sono due tipi di timestamp: uno associato al frame (PTS) (fase di pre-codifica) e due associati al pacchetto (PTS e DTS) (post fase di codifica).Nel primo caso, sembra che i valori PTS del frame possano essere assegnati usando un'unità di riferimento personalizzata (con l'unica restrizione che devono essere equidistanti se si desidera un FPS costante), quindi si può prendere ad esempio il numero di frame come fatto nel codice precedente. Nella seconda, dobbiamo prendere in considerazione i seguenti parametri:

  • La base tempi del contenitore formato di uscita, nel nostro caso MP4 (= 12800 Hz), le cui informazioni si svolge in stream-> time_base.
  • L'FPS desiderato del video.
  • Se il codificatore genera o meno B-frame (nel secondo caso i valori PTS e DTS per il frame devono essere impostati uguali, ma è più complicato se siamo nel primo caso, come in questo esempio). Vedi questo answer in un'altra domanda correlata per ulteriori riferimenti.

La chiave qui è che fortunatamente non è necessario lottare con il calcolo di queste quantità, come libav fornisce una funzione per calcolare il corretto timestamp associati al pacchetto conoscendo i suddetti dati:

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

Grazie a queste considerazioni, sono stato finalmente in grado di generare un contenitore di output sensato e sostanzialmente lo stesso tasso di compressione rispetto a quello ottenuto con lo strumento a riga di comando, che erano i due problemi rimanenti prima di indagare più a fondo sull'intestazione del formato e sul trailer e come i timestamp sono impostati correttamente.

+1

Ho anche trovato e codificatore MPEG singola intestazione che potrebbe essere utile: http://www.jonolick.com/home/mpeg-video-writer –