bmp2csv


 ニコニコ動画Excelで長門有希というのがある。

 これは、エクセルのセルに色を付けてドットに見立て、それで絵を表示しようという試みである。

 知り合いにこれの仕組みについて聞かれたとき、最初はエクセル画 for BMPというのを使ってるんじゃないかと思ったのだけど、数日後にExcelで長門有希〜作業工程〜というものがアップされ、自作のPerlスクリプトとエクセルマクロを使っていることが分かった。

 まず、画像ファイルをエクセルで処理できる形式に変換するのだけど、その方法は以下の通り。

  1. PhotoFiltreで画像をモザイク状(枠付き)にする
  2. 不要部分をトリミング
  3. GIMPで読み込みPPM形式(ASCII)で保存
  4. PPMファイルを開き、ドット情報以外を削除
  5. 縦横5ドットずつ飛ばしながら色を抽出しCSV形式で出力(Perl)

 最後が分かりづらいが、横方向は(1,1)の色を取り出したら次は(6,1)、その次は(11,1)となり、縦方向は(1,1)の次は(1,6)のとなる。最外周は枠があるため、(0,0)ではなく(1,1)から始まる。

 分からないのは、1の手順が何故必要なのかということ。

 トリミングして、縮小し、GIMPPPMに変換し、CSV形式で出力する。それでいいんじゃないのかな。

 というわけで、今回はこれらの手順をもう少し簡略化できないかなと思って、bmpファイルをcsvに変換するスクリプトを作ってみた。

 結局、それほど手間は変わらないような気もするが、動画に出てきたスクリプトよりは多少汎用性はあると思う。もっとも、bmpファイルをcsvで出力する事が必要な場面ってあまり無いような気もするが…。

 実は、bmp2csvというツールは既に存在していて自分でも試してみたのだけど、RGBを16進で表現した数値(FF00FFみたいな)か、RGBそれぞれの色成分毎に別ファイルで10進で表現した数値を出力することしかできなかった。

 仕方がないんで自分で作ってみた、というわけ。

 bmp2csvの存在を知ったのは、同類の動画Wordで巴有希奈を見て。

 ではスクリプト

use strict;
use warnings;

use POSIX qw/ceil/;

use File::Basename qw/fileparse/;

my $input = $ARGV[0] or die "ファイル名が指定されていない";

open(FI,"<$input") or die "ファイルがオープンできない:$!";
binmode(FI);

my $buffer;
		# BITMAPFILEHEADER
read(FI,$buffer,18) or die "ファイルが読めない:$!";

die "BMPファイルではない" if $buffer !~ /^BM/i;

my($file_size,$offset,$header_size);

$file_size = unpack("V",substr($buffer,2,4));
$offset = unpack("V",substr($buffer,10,4));
$header_size = unpack("V",substr($buffer,14,4));

my($size) = (stat FI)[7];

die "ファイルサイズが一致しない" unless $file_size == $size;

my $pixel_bytes = $header_size == 12 ? 4 : 8;

read(FI,$buffer,$pixel_bytes + 8) or die "ファイルが読めない:$!";

my($x_pixel,$y_pixel,$plane,$color_bits,$compress);
my $colors;

if($pixel_bytes == 8) {	# BITMAPINFOHEADER(Windows)
	$x_pixel = unpack("V",substr($buffer,0,4));
	$y_pixel = unpack("V",substr($buffer,4,4));
} else {		# BITMAPCOREHEADER(OS/2)
	$x_pixel = unpack("v",substr($buffer,0,2));
	$y_pixel = unpack("v",substr($buffer,2,2));
}

$buffer = substr($buffer,$pixel_bytes);

$plane = unpack("v",substr($buffer,0,2));
$color_bits = unpack("v",substr($buffer,2,2));
$colors = 1 << $color_bits;

$compress = $pixel_bytes == 8 ? unpack("V",substr($buffer,4,4)) : 0;

my @compress = ("無圧縮","RLE8","RLE4","bit_fields","JPEG","PNG",);

print <<"EOM";
横幅: $x_pixel
縦幅: $y_pixel
プレーン: $plane
色数: $colors
圧縮方式: $compress[$compress]

EOM

my @pallet;

if($colors <= 256) {
		# RGBQUAD(Windows) or RGBTRIPLE(OS/2)
	my $pallet_bytes = $header_size == 12 || $header_size == 64 ? 3 : 4;
	my $color_code;

	seek(FI,$header_size + 14,0) or die "シーク不可:$!";
	read(FI,$buffer,$pallet_bytes * $colors) or die "パレットが読めない:$!";

	for(0..$colors - 1) {
		$color_code = GetColor($buffer,$pallet_bytes * $_);
		$pallet[$_] = $color_code;

		printf("%03d: #%06X    ",$_,$color_code);
	}
	print "\n";
}

die "16ビット(65536色)には未対応" if $color_bits == 16;
die "無圧縮BMP以外には未対応" if $compress;

my $line_bytes = ($x_pixel * $color_bits) / 8;
$line_bytes = (($line_bytes / 4) + 1) * 4 if $line_bytes % 4;

my $pos;
my $line;
my $color;
my $code_bits = $color_bits >= 24 ? $color_bits / 8 : ceil($color_bits / 4);
my $reverse = 0;

if($y_pixel < 0) {
	$reverse = 1;
	$y_pixel = abs($y_pixel);
}

my($file,$dir,$ext) = fileparse($input, qr/\.[^.]*$/);

open(FO,">$dir$file.csv") or die "ファイルが作成できない:$!";

seek(FI,$offset,0) or die "シーク失敗:$!" if $reverse;

for my $y (0..$y_pixel - 1) {
	$pos = $offset + $line_bytes * ($y_pixel - ($y + 1));
	seek(FI,$pos,0) or die "シーク失敗:$!" unless $reverse;
	read(FI,$buffer,$line_bytes) or die "ファイルが読めない:$!";

	if($color_bits == 1) {
		$line = unpack("B*",$buffer);
	} elsif($color_bits <= 8) {
		$line = unpack("H*",$buffer);
	}

	for my $x (0..$x_pixel - 1) {
		if($color_bits <= 8) {
			$color = $pallet[hex(substr($line,$x * $code_bits,$code_bits))];
		} else {
			$color = GetColor($buffer,$x * $code_bits);
		}
		print FO $color;

		if($x == $x_pixel - 1) {
			print FO "\n";
		} else {
			print FO ',';
		}
	}
}
close FO;
close FI;

exit;

sub GetColor {
	my($line,$pos) = @_;

	(ord(substr($line,$pos + 2,1)) << 16) + (ord(substr($line,$pos + 1,1)) << 8) + ord(substr($line,$pos,1));
}

 使い方は、たとえば上のソースをbmp2csv.plというファイル名で保存してるとして、sample.bmpを変換したければ、

perl bmp2csv.pl sample.bmp

 とするだけ。これで結果がsample.csvに出力される。出力ファイルは上書きされるので注意。

 別に難しい事をしてるわけじゃないんで、bmpファイルのヘッダが分かれば何をしているかは分かるはず?分からなければ聞いてね。

 さて、一言でBMPファイルと言ってもいくつか種類がある。

 通常BMPというと、何の圧縮もされてなくて画像の下から上へ向けて格納されている物(ボトムアップ)を想像するが、ランレンクスで圧縮されてるものや、データ部がJPEGPNGで格納されてるものまで存在する。最後の二つは、わざわざBMPファイルにしなくてもJPEGファイル、PNGファイルで良さそうなもんだが、規格上は存在するんだから仕方がない。

 ここで対応しているのは無圧縮のみ。

 そして色数にもいくつか種類がある。1、4、8、16、24、32ビットの五種類。

 通常、BMPと言えば24ビットだろう。32ビットでは、表現できる色数が24ビットと変わらないのに1ドット毎に1バイト(予約域分)増えるというデメリットしか無いため、まず使われることはないだろうし、16ビット(実際に使っているのは15ビット)もかなりマイナーで、見かけることが無い。

 一応プログラム的には1、4、8、24、32に対応してるけど、32ビットのファイルは持ってないのでテストできず。

 上にも書いたとおり、通常BMPは下のラインから上のラインへ向けて格納されているが、上のラインから下のラインへ向けて格納されているものあるらしい。これもプログラム的には読めるはずだけど、持ってないんでテストしていない。

 さらにBMPにはV1〜V5の5バージョンが存在していて、それぞれヘッダのフォーマットが違う。通常はV3らしい。やっぱりこれもプログラム的には(以下略)。


 というわけで、読める”ハズ”なのは、V1〜V5の無圧縮で色数は1、4、8、24、32ビットなBMP、だ。

 その中でテスト出来たのは、V3の無圧縮、1、4、8、24ビット、ボトムアップBMPファイル。

 色々制限が厳しそうに見えるかもしれないけど、恐らく一般的に出回っているBMPファイルはみんな読めるんじゃないのかなあ?

 そう言えば、リネージュスクリーンショットを撮ると16ビットBMPで保存されて、フォトショップで読めなくて困ったっていうのを見かけたことがあるんだけど、本当なんだろうか。

 16ビットBMPがどういう構造になってるか気になるから、誰か送ってくれないかなあ。

 ちなみに、リネ2では普通の24ビットBMPで保存されていた。


 余談ではあるけれど、色数についてちょっと補足。

 たとえば1ビットのBMPの場合、0と1の二色が使えるわけだけど、それは別に白と黒の二色しか使えないというわけではない。フルカラーの中から、任意の二色が使えるという意味だ。パレットの構造は何ビット色でも変わらないので。


 それにしても久々のまともな更新だったなあ…。


参考サイト。

http://www.kk.iij4u.or.jp/~kondo/bmp/
http://ja.wikipedia.org/wiki/Windows_bitmap
http://www.ruche-home.net/program/bmp/struct.php

 画像データの読み込み位置計算は、一番上のサイトにあったのをそのまま使用。