pythonとc++で動画の各フレームをdxt1圧縮をする

プログラミング

raspberry pi picoみたいな限られたリソースで動画を再生したいなら
動画を小さく圧縮してみよう

大まかな流れ

main.py
動画ファイルを読み込んで各フレームを240*135のビットマップとして保存

main.cpp(dxt1_encorder.exe)
ビットマップを圧縮(dxt1)してバイナリファイルとして出力

main.py
全フレームのバイナリ情報を1つのバイナリファイルに統合

※ DXT1形式は4×4ピクセル単位で圧縮するため、画像サイズは必ず4の倍数にする必要があります。
僕の手持ちのLCDが 240×135だったため、240×136にリサイズし、最後の1行を描画時に捨てています。

ソース

フォルダ構成、必要なファイルはこんな感じ

自分で用意するファイルは
main.py, main.cpp, dxt1_encoder.exeだけ

でもdxt1_encoder.exeはmain.cppをビルドして作るから
実質main.pyとmain.cppの2つだけでOK
※ dxt1_encoder.exe は main.cpp を g++ や clang などのC++コンパイラでビルドして作成します(Windows環境ならMSYS2やMinGWがおすすめ)。

各フォルダは以下の役割

  • all_frames_bin
    最終的な成果物を入れるフォルダ
  • frames_temp
    1フレームずつに分けたビットマップとそれを圧縮したファイルを入れるフォルダ
  • source_videos
    圧縮したい動画ファイルを入れておくフォルダ

main.py

必要なpythonのモジュールをインストール

pip install "imageio[ffmpeg,pyav]" pillow
import imageio.v3 as iio
from PIL import Image
import os
import subprocess

frames_temp_dir = "frames_temp"
os.makedirs(frames_temp_dir, exist_ok=True)

source_videos_dir = "source_videos"
anime_titles = os.listdir(source_videos_dir)

output_dir = "all_frames_bin"

for title in anime_titles:
    title_path = os.path.join(source_videos_dir, title)
    if os.path.isdir(title_path):
        video_files = [f for f in os.listdir(title_path) if f.endswith(".mp4")]

        for video_file in video_files:
            base_name = os.path.splitext(video_file)[0]
            video_path = os.path.join(title_path, video_file)
            reader = iio.imiter(video_path, plugin="pyav")
            frame_count = 0

            # 動画の1フレームずつ読み込み
            for i, frame in enumerate(reader):
                img = Image.fromarray(frame).resize((240, 136)).convert("RGB")
                bmp_path = os.path.join(frames_temp_dir, f"{title}_{base_name}_frame{i:04d}.bmp")
                dxt1_path = os.path.join(frames_temp_dir, f"{title}_{base_name}_frame{i:04d}.dxt1")

                # iフレーム目をビットマップ形式で保存
                img.save(bmp_path)
                # ビットマップをDXT1形式に変換
                subprocess.run(["./dxt1_encoder.exe", bmp_path, dxt1_path], check=True)
                frame_count += 1

            # 動画別にbinファイルを作成
            output_bin_path = os.path.join(output_dir, f"{title}_{base_name}_allframes_dxt1.bin")

            # 1つのbinファイルに全フレームのDXT1データを書き足していく
            with open(output_bin_path, "wb") as out:
                for i in range(frame_count):
                    dxt1_path = os.path.join(frames_temp_dir, f"{title}_{base_name}_frame{i:04d}.dxt1")
                    with open(dxt1_path, "rb") as f:
                        out.write(f.read())

            # フレームファイルの削除
            for i in range(frame_count):
                os.remove(os.path.join(frames_temp_dir, f"{title}_{base_name}_frame{i:04d}.bmp"))
                os.remove(os.path.join(frames_temp_dir, f"{title}_{base_name}_frame{i:04d}.dxt1"))

コメントを多めに書いたから
なんとなく処理の流れはわかりやすいと思う

main.cpp(dxt1_encorder.exe)

// main.cpp
// 使用: ./dxt1_encoder input.bmp output.dxt1
#define STB_IMAGE_IMPLEMENTATION
#define STB_DXT_IMPLEMENTATION

#include "stb_image.h"
#include "stb_dxt.h"
#include <fstream>
#include <cstdint> 
#include <iostream>

int main(int argc, char** argv) {
    std::cout << "Arguments: " << argv[1] << " " << argv[2] << std::endl; // デバッグ用
    int w, h, c;
    if (argc < 3) {
        std::cerr << "Usage: " << argv[0] << " input.bmp output.dxt1\n";
        return 1;
    }

    // 3チャンネルでRGBとして読み込み(BGRが来ることに注意)
    unsigned char* img = stbi_load(argv[1], &w, &h, &c, 3);
    if (!img) {
        std::cerr << "Failed to load image: " << argv[1] << std::endl;
        return 1;
    }

    std::ofstream out(argv[2], std::ios::binary);
    for (int y = 0; y < h; y += 4) {
        for (int x = 0; x < w; x += 4) {
            unsigned char block[64];
            for (int dy = 0; dy < 4; dy++) {
                for (int dx = 0; dx < 4; dx++) {
                    int px = (x + dx < w) ? x + dx : w - 1;
                    int py = (y + dy < h) ? y + dy : h - 1;

                    block[(dy * 4 + dx) * 4 + 0] = img[(py * w + px) * 3 + 0]; // B
                    block[(dy * 4 + dx) * 4 + 1] = img[(py * w + px) * 3 + 1]; // G
                    block[(dy * 4 + dx) * 4 + 2] = img[(py * w + px) * 3 + 2]; // R
                    block[(dy * 4 + dx) * 4 + 3] = 255; // A
                }
            }

            uint8_t dxt1[8];
            stb_compress_dxt_block(dxt1, block, 0, STB_DXT_HIGHQUAL);
            out.write((char*)dxt1, 8);
        }
    }

    out.close();
    stbi_image_free(img);
    return 0;
}

コンパイルしてexeファイルにしてmain.pyと同じ階層に置いてね。

それ以外のファイル

libgcc_s_seh-1.dllとかstd_dxt.hとかはインターネットで探せばあるよ

一応リンクを

結果

デコードしたものがこれ

ちっちぇ~

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