「アスキーアート」とは文字を使って画像や絵を表現する技術です。2000年前後にWebの掲示板で大いに流行しました。文字だけで画像を表現するという制限が楽しく、いろいろなアスキーアートが作成されました。今回は、懐かしのアスキーアートをRustで自動生成するツールを作ってみましょう。

  • ターミナル対応のアスキーアート作成ツールを作ってみよう - 東洲斎写楽の「奴江戸兵衛」とフェルメール「真珠の耳飾りの少女」を変換したところ

    ターミナル対応のアスキーアート作成ツールを作ってみよう - 東洲斎写楽の「奴江戸兵衛」とフェルメール「真珠の耳飾りの少女」を変換したところ

アスキーアートが楽しいコマンドラインの世界

アスキーアートの歴史は古く、インターネット以前のパソコン通信の時代から、さまざまなものが作成されてきました。現在でも、コマンドライン操作が主体のアプリでは、いろいろなアスキーアートが登場します。

コマンド自体がアスキーアートを表示するものもあります。有名なところでは「sl」コマンドがあります。これは、ファイル一覧を表示する「ls」コマンドを「sl」と打ち間違えることが多いことを逆手に取ったジョークアプリです。「sl」コマンドを実行すると、ターミナル画面を蒸気機関車(SL)が横切ります。(macOSでは「brew install sl」、Ubuntu/Linuxでは「sudo apt install sl」、でインストールできます。)

  • slコマンドを実行したところ - SLがターミナルを横切る

    slコマンドを実行したところ - SLがターミナルを横切る

面白いアスキーアートが見られるコマンドとしては、「asciiquarium」というコマンドもあります。これは、ターミナルで水族館を楽しむコマンドラインアプリです。魚やアヒル、船などのアスキーアートが表示され、ランダムに画面を横切ります。(macOSでは「brew install asciiquarium」、Ubuntu/Linuxでは「sudo apt install asciiquarium」でインストールできます。)

  • asciiquariumを実行するとターミナルが水族館のようになる

    asciiquariumを実行するとターミナルが水族館のようになる

そして、ターミナルで盆栽を楽しむ「cbonsai」というコマンドも楽しめます。コマンドを実行すると盆栽が画面に表示されます。「cbonsai --live」のようにオプションを付けると、盆栽が育っていく様子を見ることもできます。(macOSでは「brew install cbonsai」、Ubuntu/Linuxでは「sudo apt install cbonsai」でインストールできます。)

  • cbonsaiを実行するとターミナルに盆栽が表示される

    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カラーコード)に変換する関数を定義します。

この記事は
Members+会員の方のみ御覧いただけます

ログイン/無料会員登録

会員サービスの詳細はこちら