ターミナルに動画を表示してみよう

-- Views

May 01, 26

スライド概要

profile-image

プログラミングが好きで、LTやってます

シェア

またはPlayer版

埋め込む »CMSなどでJSが使えない場合

ダウンロード

関連スライド

各ページのテキスト
1.

ターミナルに動画を表示してみよう ~ エスケープシーケンスで 動画の表示にチャレンジ ~ 2025/09/22 俺たちの勉強会 #4 #orestudy

2.

自己紹介:murasuke ・株式会社 ツールラボ 開発部 所属 PHPで自社Webサービスの開発をやっています ・ボイジャー2号より後、1号より前にうまれました ・役に立たたない(けど面白い)技術を考えるのが好きです

3.

早速ですが動画を表示してみます $ ./terminal_movie.sh

4.

ターミナルで動画を再生する仕組み ● (見ての通り) 単なるパラパラ漫画でした ● ターミナルに「ドット単位」で色を出力する`Sixel Graphics` というエスケープシーケンス を利用しています ● リアルタイムでパラパラ漫画にするのは重すぎるので、 事前に変換しています まずは、 `Sixel Graphics`とは何か?について説明します

5.

`Sixel Graphics`とは? ● Sixel Graphicsは、特別なエスケープ シーケンスをターミナルに送信すること で、画像を表示する技術です ● 1文字で縦に 6ピクセル分出力します ● 出力位置を右、下にずらしながら描画を 繰り返すことで、画面全体に色を表示す ることができます

6.

`Sixel Graphics`のデータ構造について 1. 全体の構成は下記のようになっています “Sixel開始シーケンス,アスペクト比,解像度の指定,カラーパレットの定義,Sixel データ文字,終了シーケンス” カラム名 文字シーケンス 補足 Sixel開始シーケンス \x1BPq ESC(\x1B) + 'Pq' アスペクト比 "1;1; アスペクト比1:1 解像度の指定 96;96 解像度96dpi x 96dpi カラーパレット ※後述 色番号(0~255)と色を定義 Sixelデータ文字 ※後述 終了シーケンス \x1B\ ESC(\x1B) + '\'

7.

Sixelデータ文字について ● Sixelデータ文字 は、 ? (0x3F) から ~ (0x7E) の範囲の文字です。 直前で指定された色で、縦に 6ピクセル分の出力を行います。 `@`は上下6ピクセルのうち一番上 `~`は全体を塗りつぶします ※塗りつぶしをしないピクセルは `透明`扱い 6ピクセル別々の色を出力するためには、描画が重ならない文字 `@` ⇒ `A` ⇒ `C` ⇒ `G` ⇒ `O` ⇒ `_` の順に(上書き)描画します

8.

2×2の画像を描画してみよう 1. 2. 4色分のカラーパレットを準備する 色の選択+描画(#1(色番号)@(Sixelデータ文字)) #1@#2@$#3A#4A ・カラーパレット定義 #1(色番号);2(RGB指定);(red;green;blue) #1;2;100;0;0 #2;2;0;0;100 #3;2;0;100;0 #4;2;100;100;100

9.

2×2の画像を描画はこれです “\x1BPq"1;1;96;96#1;2;100;0;0#2;2;0;0;100#3;2;0;100;0#4;2;100;100;100#1 @#2@$#3A#4A$\x1B\” カラム名 文字シーケンス 補足 Sixel開始シーケンス \x1BPq ESC(\x1B) + 'Pq' アスペクト比 "1;1; アスペクト比1:1 解像度の指定 96;96 解像度96dpi x 96dpi カラーパレット #1;2;100;0;0#2;2;0;0;100#3;2;0;100; #色番号(0~255)と色を定義 0#4;2;100;100;100 Sixeデータ文字 #1@#2@$#3A#4A$ #色番号+Sixelデータ文字 $は行頭へ戻る 終了シーケンス \x1B\ ESC(\x1B) + '\'

10.

それでは変換プログラムを作りましょう Sixel Graphicsは完璧に理解できましたね!!! 1. 動画を画像として変換します ⇒ ffmpegを使って、パラパラ漫画化します (ブラウザでパラパラ漫画化できますが、ここは手抜き) 2. 画像データを、 `Sixel Graphics` 形式の文字 (エスケープシーケンス+データ )に変換します 3. Sixel形式に変換した文字列を echoでひたすら表示します ⇒ 動画っぽく表示されます

11.

動画から画像を切り出します 1. ffmpegを使って変換 (scene_001.png、scene_002.png ・・・という連番で保存されます) # 画像への変換 fps: フレームレート -t: 秒数 $ ffmpeg -i "$FILE" -vf fps=$FPS -t $TIME ./scene_%03d.png > /dev/null 2>&1

12.

変換プログラムを書いてみましょう(TypeScript) ● 画像のパスを渡したら「Sixel Graphics」に変換するプログラムです 画像読んで、減色処理して、変換するだけです № 関数名 概要 1 main() 下記処理を呼び出すメイン関数 2 imageLoader() 画像ファイルを読み込み、Canvasを使用して そのピクセルデータを取得 3 reductionColor() 画像の色を216色のWebセーフパレットに減色 4 convertToSixel() 減色されたデータをSixel Graphics文字列に変換

13.
[beta]
1. main()関数
各処理を呼び出すだけ
import { loadImage, createCanvas, Image } from 'canvas';
// 画像読み込みとピクセルデータ取得
const { data, img } = await imageLoader(filename);
// 減色処理: 各ピクセルの色を減色して、パレット番号に変換する
const { colorData, colorMap } = reductionColor(data, img.width, img.height);
// Sixelグラフィックス(エスケープシーケンス)文字列に変換
const sixel = convertToSixel(colorData, colorMap, img.width, img.height);
// コンソールに表示
console.log(sixel);

14.

2. imageLoader() 画像をロードして、ピクセルの色情報を返します // 画像読み込み const img = await loadImage(filename); // 画像と同じサイズのCanvasを作成し、画像を描画する const canvas = createCanvas(img.width, img.height); const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0); // Canvasからピクセルデータを取得 const data = ctx.getImageData(0, 0, img.width, img.height).data; return { data, img };

15.
[beta]
3. reductionColor()
カラーパレットの色数制限(256色)に合わせて、216色(6×6×6)に減色処理します
// ピクセルデータの配列(左上からピクセル毎にパレット番号をセット)
const colorData = new Uint32Array(width * height);
// 色を一意に識別するためのMap (RGB値 -> パレット番号)
const colorMap = new Map<number, number>();
// sixelでは0~100の範囲で指定する(2.55だと少しくらいため微調整)
const scaleFactor = 1 / 2.3;
// 0~100を6段階に(RGBで216色)分割する場合の量子化ステップ(100/6=約16)
const quantize = 16;
for (let i = 0; i < width * height; i++) {
const offset = i * 4;
const a = data[offset + 3]; // アルファ値
// 各色成分のMaxを100に調整
const scaledR = data[offset] * scaleFactor;
const scaledG = data[offset + 1] * scaleFactor;
const scaledB = data[offset + 2] * scaleFactor;

// 各色成分を quantize の倍数に丸める
// ( 0, 16, 32, 48, 64, 80)
const qR = Math.floor(scaledR / quantize) * quantize;
const qG = Math.floor(scaledG / quantize) * quantize;
const qB = Math.floor(scaledB / quantize) * quantize;
// 3色を1つの数値にまとめる(24bit RGB値として扱う)
let qRGB = qR * 256 * 256 + qG * 256 + qB;
// 透明ピクセルは白として扱う
if (a === 0) { qRGB = 0xffffff; }
// 存在しない色なら、新たなパレット番号を割り当てる
if (!colorMap.has(qRGB)) {
colorMap.set(qRGB, colorMap.size + 1);
}
// 現在のピクセルに対応するパレット番号を記録する
colorData[i] = colorMap.get(qRGB) ?? 0;
}
return { colorData, colorMap };

16.
[beta]
4. convertToSixel()
減色されたデータをSixel Graphics文字列に変換します
const ESC = '\x1B';
let output = ESC + 'Pq'; // Sixel開始シーケンス
// 画像のプロパティ指定(アスペクト比1:1、解像度96dpi x 96dpi)
output += `"1;1;96;96`;
// カラーパレットの定義
// Map.forEach のコールバックは (value, key) の順で渡される
colorMap.forEach((paletteIndex: number, quantizedRGB: number) => {
// quantizedRGB から各色成分を抽出
const r = (quantizedRGB >> 16) & 0xff;
const g = (quantizedRGB >> 8) & 0xff;
const b = quantizedRGB & 0xff;
output += `#${paletteIndex};2;${r};${g};${b}`;
});
// ここでは、各ピクセルのパレット番号と、6ピクセルブロックの
// ビットパターンを表すキャラクタ(仮の例として行番号により決定)を出力する
// 6段のビットパターンを表現する文字群
const chars: string[] = ['@', 'A', 'C', 'G', 'O', '_'];

let i = 0; // ピクセルデータのインデックス
// 画像の各行を処理するループ
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
// 各ピクセルごとに、対応するパレット番号とビットパターンを出力
output += `#${data[i]}${chars[y % 6]}`;
i++;
}
// 行の終わりでキャリッジリターンを出力。
// 6行ごとに '-' を付加して次のブロックに移動。
if (y > 0 && (y + 1) % 6 === 0) {
output += '$-';
} else {
output += '$';
}
}
output += ESC + '\\'; // Sixel終了シーケンス
return output;

17.

Sixel変換スクリプトを実行してみましょう png画像を変換 ⇒ ターミナルに 画像が表示されました エスケープシーケンスを含む文字を出力しているだけなので、リダイレクトしてファイルに 保存したファイルを、ループでechoすればパラパラ漫画になります

18.

ターミナルで動画を表示した件のまとめ ● 最近のターミナルは画像も表示できる(任意のドットに色を指定できる) Windows Terminal、XTermなど ● 画像を表示するには非効率だけど、マシンパワーのおかげで パラパラ漫画を表示するくらいはできるようになった (最適化していなくても) ● SSH経由で表示することも可能。単に文字をechoしているだけなので (ネットワーク速度が重要) 以上です、ありがとうございました。