【QT5】imageをMPGで保存。FFMPEG APIを使 リサージュをサンプル  | Qt5、KiCadとモジュールで遊ぶ電子工作の初心

Qt5、KiCadとモジュールで遊ぶ電子工作の初心

このブログでは、Qt5とKiCadを用いた電子工作に焦点を当て、Linux上でのプロジェクト開発を探究します。初心者にも分かりやすい基板設計の指南や、多様なモジュールの活用法を紹介。Qt5のGUIを駆使したインタラクティブな制作過程を体験できます。

今日は、クラスを作るお勉強です。
 pixmapをつかってこれを動画として保存するというのをやってみます。 

 お題目は CPP FFMPEGを使って ビットマップを MPG4 にします。

保存するPixmap はナンだっていいのですが動きがないと寂しいじゃないですか

"Today, I am studying how to create classes. I will try to save a pixmap as a video.

The topic is using C++ and FFMPEG to convert bitmaps into MPEG-4.

It doesn't really matter what the saved Pixmap is, but it would be boring without any movement, wouldn't it?"

 

#include <QPainter>

#include <cmath>
#include <QDebug>
class LissajousWidget : public QObject {
    Q_OBJECT
public:
    LissajousWidget(int width,int height,QObject *parent = nullptr) :QObject(parent),
        width(width),height(height),
        a(5.0), b(6.0), delta(M_PI / 2)
    {
        lissajousPixmap = QPixmap(width, height);  // LissajousWidget のサイズ
        lissajousPixmap.fill(Qt::transparent);     // 透明で初期化
        timer = new QTimer(this);
        connect(timer, &QTimer::timeout, this, &LissajousWidget::updateParameters);
        timer->start(3);  // パラメータを更新
    }
    ~LissajousWidget() {
        delete timer;
    }
    int width;
    int height;
    void updateParameters() {
        delta += 0.001;
        draw();  // 描画更新
    }

    void draw() {
        QPainter painter(&lissajousPixmap);
        painter.fillRect(lissajousPixmap.rect(), Qt::transparent);  // 透明で初期化
        painter.fillRect(lissajousPixmap.rect(), Qt::black);        // 例えば黒い背景を設定
        painter.setRenderHint(QPainter::Antialiasing);
        painter.translate(lissajousPixmap.width() / 2, lissajousPixmap.height() / 2);       
        for (int i = 0; i < 2000; ++i) {                           // リサージュ曲線の描画(ドットで描画)
            float t = 2 * M_PI * i / 1999;
            float x = width/2  * std::sin(a * t + delta);
            float y = height/2 * std::sin(b * t);
            QColor color;
            color.setHsv(i % 360, 255, 255);                       // 色相を変更
            painter.setPen(QPen(color, 6));                       // 6ドットの直径(半径3ドット)
            painter.drawPoint(x, y);                               // ドットで描画
        }
        emit lissajousUpdated(lissajousPixmap);
    }
    QPixmap getPixmap() const {
        return lissajousPixmap;
    }
signals:
    void lissajousUpdated(const QPixmap &pixmap);
private:
    QTimer *timer;
    QPixmap lissajousPixmap;
    float a, b, delta;  // リサージュ曲線のパラメータ
};
#endif // LISSAJOUSWIDGET_H

これは

 

このクラスは直接的なウィジェットではないため、GUI要素としての表示はできませんが、内部でQPixmapを用いてグラフィックを描画し、それをシグナルを通じて他のウィジェットやコンポーネントに渡すことができます。

つぎにこれを受信して 表示枠は適当ですが まず表示しましょう。

"This class is not a direct widget, so it cannot be displayed as a GUI element. However, it uses QPixmap internally to draw graphics, which can then be passed to other widgets or components through signals.

Next, let's receive this and display it in an appropriate frame, even if it's just a basic display for now."

 

簡単なプログラムですが 表示してみますね。

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QSettings>
#include <QSharedMemory>
MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    lissajousWidget = new LissajousWidget(400, 400, this);
    connect(lissajousWidget, &LissajousWidget::lissajousUpdated, this, &MainWindow::updateLissajousDisplay);
    label = new QLabel(this);
    label->setFixedSize(400, 400);
    setCentralWidget(label);
    QString appName = QCoreApplication::applicationName();
    QSettings settings("YourOrganization", appName);// ウィンドウのジオメトリを復元
    restoreGeometry(settings.value("mainWindowGeometry").toByteArray());
    setWindowTitle("Lissajous Curve");
    setWindowFlags(Qt::WindowStaysOnTopHint); // 最前面に表示
}
void MainWindow::updateLissajousDisplay(const QPixmap &pixmap) {
    label->setPixmap(pixmap);
}
void MainWindow::closeEvent(QCloseEvent *event) {
    QString appName = QCoreApplication::applicationName();// アプリケーション名を取得
    QSettings settings("YourOrganization", appName);// ウィンドウのジオメトリを保存
    settings.setValue("mainWindowGeometry", saveGeometry());
    QMainWindow::closeEvent(event);
}
MainWindow::~MainWindow()
{
    delete ui;
}
 

 

 

いよいよ核心です。FFMPEGをつかいます。

FFMPEGは すでにインストールしてる大前提です。  WINDOWSの場合は 
たしか あれですねなんでしたっけ ???  ZIP をダウンロードして 展開してそこのフォルダをなんとかするんですよ。

難しいことは おいておいて QT5のPrjファイルに次のようにかけばいいだけです

"To configure the QT5 project file, you simply need to write the following: In the case of Linux, even if you have something like 'LIBS += -LC:\Users\adm\Documents\ffmpeg-master-latest-win64-gpl-shared\lib', Qt5 will ignore it, so I write it in a way that it works regardless. Please modify it according to your own system."

INCLUDEPATH += C:\Users\liveuser\Documents\ffmpeg-master-latest-win64-gpl-shared\include
INCLUDEPATH += 00_QtPixelCanvas
INCLUDEPATH +=/usr/include/ffmpeg
LIBS += -LC:\Users\liveuser\Documents\ffmpeg-master-latest-win64-gpl-shared\lib
LIBS += -lavcodec -lavformat -lavutil -lavfilter -lswscale

  

リナックスの場合 LIBS += -LC:\Users\adm\Documents\ffmpeg-master-latest-win64-gpl-shared\lib こんなのがあってもQt5は知らん顔して くれるので 気にせずどっちでも動作するように 書いています。

ご自身のシステムに合わせて書き換えてください。 

 

FFMPEGを具体的に使います

おじちゃんは C++で ヘッダとわけないと混乱するので わけました。 HPPにするのがいいのでしょうか?よくわかりません。

ヘッダは簡単です。

"I prefer to separate the header files in C++ to avoid confusion, so I've split them. Is it better to use '.hpp' as the file extension? I'm not really sure.

The header files are simple."

#ifndef QTPIXEMPEGSTREAMER_H
#define QTPIXEMPEGSTREAMER_H
#include <QObject>
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/opt.h>
#include <libavutil/imgutils.h>
}
#include <iostream>
#include <string>
class QPixmap;
class QtPixeMpegStreamer : public QObject
{
    Q_OBJECT
public:
    explicit QtPixeMpegStreamer(const std::string& filename, int width=800,int height=600,int bit_rate=400000,QObject *parent = nullptr);
    ~QtPixeMpegStreamer();
    //void encodeFrame();
    void encodeFrame(QPixmap pixmap) ;
    void finalize();
signals:
private:
    AVCodecContext* c = nullptr;
    AVFormatContext* fmt_ctx = nullptr;
    AVStream* video_st = nullptr;
    AVFrame* frame = nullptr;
    AVPacket* pkt = nullptr;
    std::string filename;
    std::string codec_name="libx264";// I fixed it to this because it's too bothersome. Using FFMPEG is really tedious, so I get annoyed with too many variables."
    int frame_count = 0;
    int width;
    int height;
    int bit_rate;
    void initialize();
    void encode(AVFrame* frame);
    bool createDirectoryIfNotExist(const std::string& filePath);
};
#endif // QTPIXEMPEGSTREAMER_H

本当に面倒くさいのでなるべく変数使わないことにしました。

Using FFMPEG is really tedious, so I get annoyed with too many variables.

#include "qtpixempegstreamer.h"
#include <filesystem>
QtPixeMpegStreamer::QtPixeMpegStreamer(const std::string & filename,int width,int height ,int bit_rate,QObject *parent)
    : QObject{parent},filename(filename) ,width(width),height(height),bit_rate(bit_rate){
    initialize();
}

QtPixeMpegStreamer::~QtPixeMpegStreamer() {
    avcodec_free_context(&c);
    av_frame_free(&frame);
    av_packet_free(&pkt);

    if (!(fmt_ctx->flags & AVFMT_NOFILE)) {
        avio_close(fmt_ctx->pb);
    }

    avformat_free_context(fmt_ctx);
}

void QtPixeMpegStreamer::initialize() {
    avformat_alloc_output_context2(&fmt_ctx, nullptr, nullptr, filename.c_str());
    if (!fmt_ctx) {
        throw std::runtime_error("Could not deduce output format from file extension");
    }

    const AVCodec* codec = avcodec_find_encoder_by_name(codec_name.c_str());
    if (!codec) {
        throw std::runtime_error("Codec not found");
    }

    video_st = avformat_new_stream(fmt_ctx, nullptr);
    if (!video_st) {
        throw std::runtime_error("Could not allocate stream");
    }

    c = avcodec_alloc_context3(codec);
    if (!c) {
        throw std::runtime_error("Could not allocate video codec context");
    }

    c->bit_rate = bit_rate;
    c->width = width;
    c->height = height;
    c->time_base = (AVRational){1, 25};
    c->framerate = (AVRational){25, 1};
    c->gop_size = 10;
    c->max_b_frames = 1;
    c->pix_fmt = AV_PIX_FMT_YUV420P;

    if (codec->id == AV_CODEC_ID_H264) {
        av_opt_set(c->priv_data, "preset", "slow", 0);
    }

    if (avcodec_open2(c, codec, nullptr) < 0) {
        throw std::runtime_error("Could not open codec");
    }

    if (!(fmt_ctx->flags & AVFMT_NOFILE)) {
        if (avio_open(&fmt_ctx->pb, filename.c_str(), AVIO_FLAG_WRITE) < 0) {
            throw std::runtime_error("Could not open output file");
        }
    }

    if (avcodec_parameters_from_context(video_st->codecpar, c) < 0) {
        throw std::runtime_error("Could not copy codec parameters");
    }

    if (avformat_write_header(fmt_ctx, nullptr) < 0) {
        throw std::runtime_error("Error occurred when opening output file");
    }

    frame = av_frame_alloc();
    frame->format = c->pix_fmt;
    frame->width = c->width;
    frame->height = c->height;
    if (av_frame_get_buffer(frame, 0) < 0) {
        throw std::runtime_error("Could not allocate video frame buffer");
    }

    pkt = av_packet_alloc();
    if (!pkt) {
        throw std::runtime_error("Could not allocate AVPacket");
    }
}
bool QtPixeMpegStreamer::createDirectoryIfNotExist(const std::string& filePath)
{
    std::filesystem::path pathObj(filePath);
    std::filesystem::path dirPath = pathObj.parent_path();

    if (!std::filesystem::exists(dirPath)) { // Check if directory exists
        return std::filesystem::create_directories(dirPath); // Create directory if it doesn't exist
    }

    return true; // Directory already exists
}
//void QtPixeMpegStreamer::encodeFrame() {
//    if (av_frame_make_writable(frame) < 0) {
//        throw std::runtime_error("Frame not writable");
//    }
//    int x, y; // Dummy image generation
//    for (y = 0; y < c->height; y++) {
//        for (x = 0; x < c->width; x++) {
//            frame->data[0][y * frame->linesize[0] + x] = x + y + frame_count * 3;
//        }
//    }
//    for (y = 0; y < c->height / 2; y++) {
//        for (x = 0; x < c->width / 2; x++) {
//            frame->data[1][y * frame->linesize[1] + x] = 128 + y + frame_count * 2;
//            frame->data[2][y * frame->linesize[2] + x] = 64 + x + frame_count * 5;
//        }
//    }
//    frame->pts = frame_count++;
//    encode(frame);
//}

#include <QImage>
#include <stdexcept>
#include <QPixmap>
#include <QDebug>
void QtPixeMpegStreamer::encodeFrame(QPixmap pixmap) {
    if (av_frame_make_writable(frame) < 0) {
        throw std::runtime_error("Frame not writable");
    }
    // 元のQPixmapのサイズをチェック
    QImage image;
    QSize originalSize = pixmap.size();
    if (originalSize.width() != c->width || originalSize.height() != c->height) {
        // サイズが異なる場合のみスケーリング
         image = pixmap.toImage().convertToFormat(QImage::Format_RGB32).scaled(c->width, c->height, Qt::IgnoreAspectRatio);
        // 以下、スケーリングされたイメージを使用した処理
    }
    else {
        // サイズが同じ場合、変換なしでそのまま使用
         image = pixmap.toImage().convertToFormat(QImage::Format_RGB32);
        // 以下、変換されていないイメージを使用した処理
    }

    QRgb rgb;
    int r, g, b, yLuminance, u, v;

    for (int y = 0; y < c->height; y++) {
        for (int x = 0; x < c->width; x++) {
             rgb = image.pixel(x, y);
            r = qRed(rgb);  g = qGreen(rgb);  b = qBlue(rgb);
            yLuminance = (0.257 * r) + (0.504 * g) + (0.098 * b) + 16;// Convert RGB to YUV
            u = -(0.148 * r) - (0.291 * g) + (0.439 * b) + 128;
            v = (0.439 * r) - (0.368 * g) - (0.071 * b) + 128;
            frame->data[0][y * frame->linesize[0] + x] = (uint8_t)(yLuminance);// Assign to frame
            if (x % 2 == 0 && y % 2 == 0) {
               frame->data[1][y/2 * frame->linesize[1] + x/2] = (uint8_t)(u);
               frame->data[2][y/2 * frame->linesize[2] + x/2] = (uint8_t)(v);
            }
        }
    }
    frame->pts = frame_count++;// ここでフレームのpts (プレゼンテーションタイムスタンプ) を設定
    encode(frame);// フレームをエンコード
}

void QtPixeMpegStreamer::finalize() {
    encode(nullptr); // Flush the encoder
    if (av_write_trailer(fmt_ctx) < 0) {
        throw std::runtime_error("Error writing trailer");
    }
}

void QtPixeMpegStreamer::encode(AVFrame* frame) {
    if (avcodec_send_frame(c, frame) < 0) {
        throw std::runtime_error("Error sending frame for encoding");
    }

    while (avcodec_receive_packet(c, pkt) == 0) {
        av_packet_rescale_ts(pkt, c->time_base, video_st->time_base);
        pkt->stream_index = video_st->index;

        if (av_interleaved_write_frame(fmt_ctx, pkt) < 0) {
            throw std::runtime_error("Error during writing packet");
        }
        av_packet_unref(pkt);
    }
}
 

 

さてここで 今日はねます。

明日できることは今日するなという格言を信じて寝ます。 ではまた

 

"Well, I'm going to sleep now.

I believe in the saying, 'Don't do today what you can put off until tomorrow.' See you again."

(Note: The original Japanese saying is a playful twist on the more common phrase, 'Don't put off until tomorrow what you can do today.')

 

起きました^^;

main.cpp

 

#include "mainwindow.h"

#include <QApplication>
#include <QFileInfo>
int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    a.setApplicationName(QFileInfo(argv[0]).baseName());
    a.setOrganizationName("YourOrganizationName");
    a.setOrganizationDomain("https://YourWebsite");
    a.setApplicationDisplayName("リサージュを動かしてみる");
    QSharedMemory sharedMemory(QFileInfo(argv[0]).baseName());
    if (!sharedMemory.create(1)) {
        // すでにアプリケーションが起動している
        return 5;
    }

    MainWindow w;
    w.show();
    return a.exec();
}

mainwindow.cpp

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QSettings>
#include <QSharedMemory>
#include <QStandardPaths>
MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{int captureCounter=0;
    ui->setupUi(this);
    lissajousWidget = new LissajousWidget(400, 400, this);
    connect(lissajousWidget, &LissajousWidget::lissajousUpdated, this, &MainWindow::updateLissajousDisplay);
    label = new QLabel(this);
    label->setFixedSize(400, 400);
    setCentralWidget(label);
    QString appName = QCoreApplication::applicationName();
    QSettings settings("YourOrganization", appName);// ウィンドウのジオメトリを復元
    restoreGeometry(settings.value("mainWindowGeometry").toByteArray());
    setWindowTitle("Lissajous Curve");
    setWindowFlags(Qt::WindowStaysOnTopHint); // 最前面に表示
    // QtPixeMpegStreamerのインスタンス化
    QString filePath = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation)
                                   + QString("/lissjous.mp4").arg(captureCounter++);

    mpegStreamer = new QtPixeMpegStreamer(filePath.toStdString(), 400, 400, 400000, this);
//passionate.bilingual.parenting@gmail.com
}
void MainWindow::updateLissajousDisplay(const QPixmap &pixmap) {
    label->setPixmap(pixmap);
    // MP4ストリーマーにフレームを送る
    mpegStreamer->encodeFrame(pixmap);
}
void MainWindow::closeEvent(QCloseEvent *event) {
    QString appName = QCoreApplication::applicationName();// アプリケーション名を取得
    QSettings settings("YourOrganization", appName);// ウィンドウのジオメトリを保存
    settings.setValue("mainWindowGeometry", saveGeometry());
    QMainWindow::closeEvent(event);
}
MainWindow::~MainWindow()
{ mpegStreamer->finalize();
    delete ui;
     delete mpegStreamer;
}

 

原理的な動作です。プログラムを閉じたときにファイルをクローズします。途中でプログラムがクラッシュするとおそらくMp4ファイルは完成しないと思います。クラッシュさせてみて試す必要があるかと思います。

 画像のチェックパタンなどをプログラムで作り出してMP4で流すなどに用途はあるでしょう。