PNM 形式の画像をフルカラー対応の端末で表示するプログラム
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env ruby | |
=begin | |
sgp - Semigraphic Print | |
PNM 形式の画像をフルカラー対応の端末(xterm、gnome-terminalなど)で表 | |
示するプログラムです。Unicode の Block Elements 文字を使って表示します。 | |
動かすには pnm ジェムが必要です。 | |
$ gem install pnm | |
使い方: | |
$ sgp 画像ファイル名.ppm | |
ファイル名に '-' を指定すると画像データを標準入力から読み込みます。 | |
ImageMagick の convert コマンドと併用すると多種の画像フォーマットが | |
表示できます。 | |
$ convert 入力ファイル名 pnm:- | sgp - | |
文字セルのアスペクト比の関係で、たいてい縦長に表示されるので、-scale | |
オプションで幅を狭くすると良いと思います。 | |
$ convert -scale x50% 入力ファイル名 pnm:- | sgp - | |
=end | |
require 'pnm' | |
# 返り値: { | |
# assigned_clusters: データ点に割り当てられたクラスターの番号[0,k), | |
# cluster_means: 個々のクラスターの平均値(nilあり) | |
# } | |
def clustering(data_points, k, niterations = 1) | |
fail "number of data points < number of clusters" if data_points.size < k | |
clusters = (0...k).cycle.first(data_points.size) | |
calculate_mean = lambda do |cluster| | |
points = data_points.zip(clusters) | |
.select { |pt, c| c == cluster }.map { |pt, _| pt } | |
if points.empty? | |
return nil | |
else | |
return points.transpose.map { |col| col.inject(:+).div(col.size) } | |
end | |
end | |
distance = lambda do |p1, p2| | |
da = p1[0] - p2[0] | |
db = p1[1] - p2[1] | |
dc = p1[2] - p2[2] | |
return Math.sqrt(da*da + db*db + dc*dc) | |
end | |
reassign = lambda do |centers| | |
data_points.each.with_index do |pt, i| | |
new_cluster = (0...centers.size).min_by { |j| | |
if centers[j].nil? | |
Float::INFINITY | |
else | |
distance.(pt, centers[j]) | |
end | |
} | |
clusters[i] = new_cluster | |
end | |
end | |
means = (0...k).map { |i| calculate_mean.(i) } | |
niterations.times do | |
reassign.(means) | |
means = (0...k).map { |i| calculate_mean.(i) } | |
end | |
return { assigned_clusters: clusters, cluster_means: means } | |
end | |
def pixel_distance(p1, p2) | |
dr = p1[0] - p2[0] | |
dg = p1[1] - p2[1] | |
db = p1[2] - p2[2] | |
return Math.sqrt(dr*dr + dg*dg + db*db) | |
end | |
BLOCK_ELEMENT_TABLE = { | |
[0, 0, 0, 0] => " ", | |
[0, 0, 1, 0] => "▖", | |
[0, 0, 0, 1] => "▗", | |
[1, 0, 0, 0] => "▘", | |
[1, 0, 1, 1] => "▙", | |
[1, 0, 0, 1] => "▚", | |
[1, 1, 1, 0] => "▛", | |
[1, 1, 0, 1] => "▜", | |
[0, 1, 0, 0] => "▝", | |
[0, 1, 1, 0] => "▞", | |
[0, 1, 1, 1] => "▟", | |
[1, 0, 1, 0] => "▌", | |
[0, 1, 0, 1] => "▐", | |
[0, 0, 1, 1] => "▄", | |
[1, 1, 0, 0] => "▀", | |
[1, 1, 1, 1] => "█", | |
} | |
def block_element(cell) | |
return BLOCK_ELEMENT_TABLE[cell] || fail | |
end | |
def render_cell(data) | |
bg = data[:cluster_means][0] || [255,0,255] | |
fg = data[:cluster_means][1] || [255,0,255] | |
return "\e[38;2;#{fg.join(';')}m" + | |
"\e[48;2;#{bg.join(';')}" + "m" + | |
block_element(data[:assigned_clusters]) + "\e[0m" | |
end | |
ALL_PATTERNS = ["*", " "].repeated_permutation(4).to_a | |
def print_image(pnm) | |
bm = pnm.pixels | |
(pnm.height/2).times do |y| | |
buf = "" | |
(pnm.width/2).times do |x| | |
pixels = [bm[y*2][x*2], bm[y*2][x*2+1], bm[y*2+1][x*2], bm[y*2+1][x*2+1]] | |
result = clustering(pixels, 2) | |
buf.concat render_cell(result) | |
end | |
puts buf | |
end | |
end | |
def main | |
if ARGV.empty? | |
STDERR.puts "Usage: sgp [FILENAME.. (specify '-' for stdin)]\n" | |
exit 1 | |
else | |
ARGV.each do |arg| | |
if arg == '-' | |
print_image PNM.read(STDIN) | |
else | |
print_image PNM.read(arg) | |
end | |
end | |
end | |
end | |
if __FILE__ == $0 | |
main | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment