「アスキーアート」とは文字を使って画像や絵を表現する技術です。2000年前後にWebの掲示板で大いに流行しました。文字だけで画像を表現するという制限が楽しく、いろいろなアスキーアートが作成されました。今回は、懐かしのアスキーアートをRustで自動生成するツールを作ってみましょう。
アスキーアートが楽しいコマンドラインの世界
アスキーアートの歴史は古く、インターネット以前のパソコン通信の時代から、さまざまなものが作成されてきました。現在でも、コマンドライン操作が主体のアプリでは、いろいろなアスキーアートが登場します。
コマンド自体がアスキーアートを表示するものもあります。有名なところでは「sl」コマンドがあります。これは、ファイル一覧を表示する「ls」コマンドを「sl」と打ち間違えることが多いことを逆手に取ったジョークアプリです。「sl」コマンドを実行すると、ターミナル画面を蒸気機関車(SL)が横切ります。(macOSでは「brew install sl」、Ubuntu/Linuxでは「sudo apt install sl」、でインストールできます。)
面白いアスキーアートが見られるコマンドとしては、「asciiquarium」というコマンドもあります。これは、ターミナルで水族館を楽しむコマンドラインアプリです。魚やアヒル、船などのアスキーアートが表示され、ランダムに画面を横切ります。(macOSでは「brew install asciiquarium」、Ubuntu/Linuxでは「sudo apt install asciiquarium」でインストールできます。)
そして、ターミナルで盆栽を楽しむ「cbonsai」というコマンドも楽しめます。コマンドを実行すると盆栽が画面に表示されます。「cbonsai --live」のようにオプションを付けると、盆栽が育っていく様子を見ることもできます。(macOSでは「brew install cbonsai」、Ubuntu/Linuxでは「sudo apt install cbonsai」でインストールできます。)
ほかにも、映画「マトリックス」のように緑の文字が流れるエフェクトを楽しめる「cmatrix」など、いろいろなアスキーアートを楽しむアプリが公開されています。面白いので探してみると良いでしょう。
アスキーアート生成ツールを作ってみよう
さて、ここからが本題です。多くのアスキーアートは、人手で作成されていますが、手持ちの画像ファイルから手軽にアスキーアートを作成できたら楽しいものです。Rustでアスキーアート生成ツールを作ってみましょう。
プログラミングでアスキーアートを作成する手順は次の通りです。
- (1) 画像を読み込む
- (2) 画像をアスキーアートで表現できるくらいにリサイズする
- (3) 画像の輝度を計算
- (4) 輝度に合わせて「@」「#」「+」「.」などの文字に当てはめる
- (5) 画像の色を調べてカラーコードを付与
このように、アルゴリズム的には、それほど難しいものではありません。
プロジェクトを作成しよう
それでは、Rustのプロジェクトを作成しましょう。cargoコマンドを実行して、プロジェクトを作成しましょう。
# プロジェクトを作成
mkdir image2aa
cd image2aa
cargo init
# imageクレートを追加
cargo add image
すると次のようなひな形が作成されます。生成されたファイル「src/main.rs」がメインファイルです。このファイルを編集します。
.
├── Cargo.lock
├── Cargo.toml
└── src
└── main.rs
今回のプログラムでは、画像ファイルを扱うimageクレート(原稿執筆時点でバージョン0.25.5)を利用しますが、imageクレートをコンパイルするのに、Rustのバージョン1.80.1以上が必要です。もし、古いRustを利用している場合には、「rustup update」コマンドを実行して、最新版のRustをインストールしてください。
画像をアスキーアートに変換
生成されたファイル「src/main.rs」を以下のように編集します。なお、プログラムはこちら(https://gist.github.com/kujirahand/bc9c46694693b4db1efe342cad05d811)にもアップロードしています。
use image::{GenericImageView, imageops::FilterType};
use std::fs;
use std::env;
// 輝度に対応するASCII文字を指定 --- (*1)
const ASCII_CHARS: &[u8] = b"@#%8&o*=+-:. ";
// RGB値を256色ANSIカラーコードに変換 --- (*2)
fn rgb_to_ansi256(r: u8, g: u8, b: u8) -> u8 {
let r_idx = (r as u16 * 5 / 255) as u8;
let g_idx = (g as u16 * 5 / 255) as u8;
let b_idx = (b as u16 * 5 / 255) as u8;
16 + 36 * r_idx + 6 * g_idx + b_idx
}
// 画像を読み込み色付きアスキーアートに変換して返す --- (*3)
fn image_to_ascii(img_path: &str, width: u32) -> String {
// 画像を読み込み
let img = image::open(img_path).expect("画像の読み込みに失敗しました");
// リサイズ後の画像サイズを計算
let (w, h) = img.dimensions();
let aspect_ratio = h as f32 / w as f32;
let new_height = (width as f32 * aspect_ratio * 0.6) as u32;
// 画像を指定サイズにリサイズ
let img = img.resize_exact(width, new_height, FilterType::Nearest);
// 結果を代入するString
let mut result = String::new();
for y in 0..new_height {
for x in 0..width {
let pixel = img.get_pixel(x, y); // ピクセルを取得 --- (*4)
let r = pixel.0[0];
let g = pixel.0[1];
let b = pixel.0[2];
// 輝度を計算して文字を選択 --- (*5)
let intensity = (0.299 * r as f32 + 0.587 * g as f32 + 0.114 * b as f32) / 255.0;
let index = (intensity * (ASCII_CHARS.len() - 1) as f32) as usize;
let ch = ASCII_CHARS[index] as char;
// 256色のANSIカラーコードを取得
let ansi_code = rgb_to_ansi256(r, g, b);
// ANSIエスケープシーケンスを用いて背景色を設定
result.push_str(&format!("\x1b[48;5;{}m{}", ansi_code, ch));
}
// 各行の終わりで色設定をリセットして改行
result.push_str("\x1b[0m\n");
}
result
}
fn main() {
// コマンドライン引数を得る
let args: Vec<String> = env::args().collect();
// ファイル名を取得(デフォルトは"sample.jpg")
let filename = if args.len() > 1 {
&args[1]
} else {
"sample.jpg"
};
// 画像をアスキーアートに変換
let aa = image_to_ascii(filename, 80);
println!("{}", aa);
// ファイルに保存
fs::write("output.txt", &aa).unwrap();
}
プログラムを実行するには次のコマンドを実行します。デフォルトでは、「sample.jpg」というファイルをアスキーアートに変換して「output.txt」というファイルにターミナルの制御コード付きで保存します。画像「sample.jpg」をimage2aa直下に用意してから実行しましょう。
cargo run
なお、任意の画像ファイル「image.png」を指定するには、次のようなコマンドを実行します。
cargo run image.png
プログラムを実行すると、画像からアスキーアートを生成します。
プログラムを確認してみましょう。(*1)では輝度に対応するASCII文字を指定します。全角文字を指定することもできますが、ターミナルだと文字幅の関係で画像が崩れることが多いようなので、半角ASCII文字だけにしました。(*2)では光の三原色(赤緑青のrgb)からターミナルの256色(ANSI 256色カラー / 24bitカラーコード)に変換する関数を定義します。