PR

【VScode】C++とopenCVを使って、動画を文字に変換してコンソール(ターミナル)に表示・再生してみよう!

Rintaが書いた

皆さん、コンソールつかってますか?
大体デバッグで使っている人がいると思いますが、今回は動画をコンソールに出してみよう!という企画です。

スポンサーリンク
スポンサーリンク

今回すること

例として稲葉曇さんの『ラグトレイン』Vo. 歌愛ユキを出してみようと思います。
BadAppleとかでもよかったのですが、すでに多く作成されていたのでこれにしてみました。

作り方

準備物

今回は↓のものを使います。

  • VScode
  • CMake
  • OpenCV
  • 変換したいmp4ファイル

VScodeは、https://code.visualstudio.com/ からダウンロードしてインストールしてください。
CMakeはhttps://cmake.org/download/ からインストールしてください。
インストーラー(Windows x64 Installer)でインストールすればいいと思います。
MP4はいい感じにゲットしてください。

OpenCVのインストール

Releases
OpenCV Releases Are Brought To You By Intel Intel is a multinational corporation known for its semiconductor products, i...

↑の公式サイトからダウンロードします。

exeファイルを実行したらフォルダが出てきます。それをわかりやすい場所(”C:\Users\ユーザー名”)等に置きます。そして、次のパスを環境変数のpathに追加します。

置いた場所\opencv\build\x64\vc16\bin
置いた場所\opencv\build\x64\vc16\lib

プログラムを書く

新しいフォルダーを作り、以下のプログラムを書いてください。名前はcode.cpp等にしてください。

#include <iostream>
#include <windows.h>
#include <cstdio>
#include <thread>
#include <chrono>
#include <vector>
#include <string>
#include <fstream>
#include <opencv2/core.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/core/utils/logger.hpp>
#include <opencv2/videoio.hpp>

// 画面の変更を表す構造体
struct Change
{
    short index; // 文字の位置
    char ch;     // 変更された文字
};

// フレームを読み込み、指定サイズにリサイズして文字列に変換する関数
void processFrame(cv::Mat &frame, std::string &result, const cv::Size &size)
{
    // 文字リストの読み込み(1行目の文字列を取得)
    static const std::string CHAR_LIST = []() {
        std::ifstream file("chars.txt"); // 文字リストファイルを開く
        std::string chars;
        std::getline(file, chars); // 1行目を読み込み
        return chars;
    }();
    
    const static int CHAR_COUNT = CHAR_LIST.size() - 1; // 文字リストのサイズ
    cv::resize(frame, frame, size); // フレームを指定サイズにリサイズ
    auto pix = frame.begin<cv::Vec3b>(); // ピクセルデータへのポインタを取得
    for (int row = 0; row < size.height; ++row) // 行をループ
    {
        for (int col = 0; col < size.width; ++col, ++pix) // 列をループ
            // ピクセルの平均値に基づいて対応する文字を選択し、結果文字列に追加
            result.push_back(CHAR_LIST[(int)cv::mean(*pix)[0] * CHAR_COUNT / 255]);
    }
}

int main(int argc, char **argv)
{
    if (argc <= 1)
    {
        std::cout << "Usage: BadApple.exe videoFile consoleWidthInChars";
        return 0;
    }

    using namespace std::chrono;
    const long long FRAME_DURATION_US = 1e6 / 15; // 15fps のフレーム持続時間(マイクロ秒)

    // OpenCV のログレベルを SILENT に設定(デバッグ情報を抑制)
    cv::utils::logging::setLogLevel(cv::utils::logging::LogLevel::LOG_LEVEL_SILENT);

    cv::VideoCapture video(argv[1]); // 動画ファイルを開く
    if (!video.isOpened())
    {
        std::cout << "ファイルを開けません: " << argv[1];
        return -1;
    }

    const int CONSOLE_WIDTH = atoi(argv[2]); // コンソールの幅(文字数)
    const int VIDEO_WIDTH = (int)video.get(cv::CAP_PROP_FRAME_WIDTH); // 動画の幅
    const int VIDEO_HEIGHT = (int)video.get(cv::CAP_PROP_FRAME_HEIGHT); // 動画の高さ
    const int CONSOLE_HEIGHT = VIDEO_HEIGHT * CONSOLE_WIDTH / (VIDEO_WIDTH * 2); // コンソールの高さ
    const int FRAME_SKIP = (int)video.get(cv::CAP_PROP_FPS) / 15; // スキップするフレーム数
    const int TOTAL_FRAMES = (int)video.get(cv::CAP_PROP_FRAME_COUNT) / FRAME_SKIP; // 総フレーム数
    const cv::Size FRAME_SIZE(CONSOLE_WIDTH, CONSOLE_HEIGHT); // フレームサイズ

    std::vector<std::vector<Change>> changes; // 各フレームの変更情報を保存するベクター

    cv::Mat frame; // 現在のフレーム
    std::string initialFrame; // 初期フレームの文字列

    video >> frame; // 初期フレームを読み込む
    processFrame(frame, initialFrame, FRAME_SIZE); // フレームを処理して文字列に変換

    // 指定フレーム数分スキップ
    for (int i = 0; i < FRAME_SKIP; ++i)
        video >> frame;

    // フレームの変更情報を収集
    std::string previousFrame = initialFrame; // 前回のフレーム
    int frameCount = 0, lastProgress = 0; // フレーム数と進捗状況の追跡
    while (!frame.empty())
    {
        changes.push_back(std::vector<Change>()); // 新しいフレームの変更情報を追加

        std::string currentFrame; // 現在のフレームの文字列
        processFrame(frame, currentFrame, FRAME_SIZE); // フレームを処理して文字列に変換

        auto &currentChanges = changes.back(); // 最新フレームの変更情報
        for (int i = 0; i < currentFrame.length(); ++i)
            // 現在のフレームと前回のフレームで異なる部分を変更情報に追加
            if (currentFrame[i] != previousFrame[i])
                currentChanges.push_back({ (short)i, currentFrame[i] });

        previousFrame = currentFrame; // 前回のフレームを更新

        // 次のフレームを読み込む
        for (int i = 0; i < FRAME_SKIP; ++i)
            video >> frame;

        // 進捗状況を表示
        const int progress = ++frameCount * 100 / TOTAL_FRAMES;
        if (progress % 10 == 0 && progress / 10 != lastProgress)
        {
            lastProgress = progress / 10;
            std::cout << progress << "%\n";
        }
    }

    std::cout << "処理完了!";
    std::this_thread::sleep_for(std::chrono::milliseconds(1500)); // 1.5秒待機

    HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE); // 標準出力ハンドルの取得

    // コンソール画面をクリア
    DWORD charsWritten;
    CONSOLE_SCREEN_BUFFER_INFO consoleInfo;
    GetConsoleScreenBufferInfo(hOut, &consoleInfo); // 現在のコンソール情報を取得
    SetConsoleCursorPosition(hOut, { 0, 0 }); // カーソルをコンソールの先頭に移動

    FillConsoleOutputCharacter(hOut, (TCHAR)' ', consoleInfo.dwSize.X * consoleInfo.dwSize.Y, { 0, 0 }, &charsWritten); // コンソールを空白で埋める
    WriteConsoleOutputCharacter(hOut, initialFrame.c_str(), initialFrame.size(), { 0, 0 }, &charsWritten); // 初期フレームを表示

    int frameIdx = 0; // 現在のフレームインデックス
    auto startTime = steady_clock::now(); // 開始時刻
    previousFrame = initialFrame; // 前回のフレームを初期化
    while (frameIdx < TOTAL_FRAMES - 1)
    {
        auto now = steady_clock::now(); // 現在時刻
        auto elapsed = duration_cast<microseconds>(now - startTime).count(); // 経過時間

        bool shouldDrawNewFrame = (elapsed >= FRAME_DURATION_US); // 新しいフレームを描画するかどうか
        if (shouldDrawNewFrame)
            startTime = now; // 開始時刻を更新

        int framesToProcess = 0; // 処理するフレーム数
        while (elapsed >= FRAME_DURATION_US)
        {
            elapsed -= FRAME_DURATION_US; // 経過時間を調整
            framesToProcess++; // 処理するフレーム数を増加
            frameIdx++; // フレームインデックスを更新
            if (frameIdx >= TOTAL_FRAMES) break; // 最後のフレームに達したら終了
        }

        if (shouldDrawNewFrame)
        {
            startTime -= microseconds(elapsed); // 経過時間を調整
            int startFrame = frameIdx - framesToProcess; // 処理するフレームの開始位置

            std::string buffer = previousFrame; // 現在のフレームバッファをコピー
            for (int i = startFrame; i < frameIdx; ++i)
                // 変更情報に基づいてフレームバッファを更新
                for (const auto &change : changes[i])
                    buffer[change.index] = change.ch;

            previousFrame = buffer; // 前回のフレームを更新

            // コンソールサイズ変更時に画面をクリア
            CONSOLE_SCREEN_BUFFER_INFO newSize;
            GetConsoleScreenBufferInfo(hOut, &newSize);
            if (newSize.dwSize.X != consoleInfo.dwSize.X || newSize.dwSize.Y != consoleInfo.dwSize.Y)
            {
                consoleInfo = newSize; // 新しいコンソールサイズを保存
                FillConsoleOutputCharacter(hOut, (TCHAR)' ', consoleInfo.dwSize.X * consoleInfo.dwSize.Y, { 0, 0 }, &charsWritten); // コンソールを空白で埋める
            }

            // コンソールの幅に合わせてバッファに空白を挿入
            int whitespace = consoleInfo.dwSize.X - CONSOLE_WIDTH;
            if (whitespace > 0)
            {
                for (int i = 0; i < CONSOLE_HEIGHT; ++i)
                    buffer.insert(CONSOLE_WIDTH * (i + 1) + whitespace * i, whitespace, ' ');
            }

            WriteConsoleOutputCharacter(hOut, buffer.c_str(), buffer.size(), { 0,0 }, &charsWritten); // バッファの内容をコンソールに出力
        }
    }

    // 画面をクリアして終了メッセージを表示
    FillConsoleOutputCharacter(hOut, (TCHAR)' ', consoleInfo.dwSize.X * consoleInfo.dwSize.Y, { 0, 0 }, &charsWritten);
    std::cout << "終了";
    std::cin.get(); // ユーザーの入力を待つ

    return 0;
}

書けたら「CMakeLists.txt」というファイルを作って、次の内容を書いてください。

cmake_minimum_required(VERSION 3.0)
project(YourProjectName)

find_package(OpenCV REQUIRED)

add_executable(YourExecutableName code.cpp)

target_link_libraries(YourExecutableName PRIVATE ${OpenCV_LIBS})

VScodeでopencvをコンパイルする

cmake . -B build
cmake --build build

ターミナルで↑を実行してください。


そしたら、\build\Debug\うんちゃら.exe が作られているはずなので

.\build\Debug\うんちゃら.exe .\hoge.mp4 150

と実行したら実行されます!!!
お疲れさまでした!



コメント

タイトルとURLをコピーしました