2017年3月28日火曜日

JPEG画像から縦横のサイズを取り出す

── 昔プログラマのプログラミング奮戦記

 “昔プログラマ”を自認する私めは、今でもプログラミングに日々取り組んでいる。プログラムを作るのは楽しいことだけれど、同時に苦しいことでもある。特に最近は苦戦している。時々、現役のプログラマの方々に助言を頂きたいと思うこともある。しかし助言を得たければ「何のために、何をしているか」を詳しく説明しておく必要があろう。これが結構面倒なことなのだが、ダメ元で兎に角やってみようと思う。


▼アナログからデジタルの時代へ
 私はアナログの時代からカメラをやっていたが、最近はすっかりデジタルカメラのお世話になっている。使い易いので、もはやアナログカメラに戻る積りはない。そこで、アナログ時代のカメラの思い出をこの辺りで総括しておこうと思い立った(興味のある方は、別途「写真の思い出(まだ未完です!)」を参照してほしい)。

 
 この過程で、私はアナログ時代の撮影データの原画、つまりネガフィルムをしっかりと保存していることを思い出した。ネガフィルムのケースを大きな箱の中に放り込んでいただけなので、「結果的に保存されていた」というのが正確なところであろう。そのため管理状態はそれ程良くはない。

 一方、デジタルカメラで撮影した原画像はどうなっているか考えてみた。電子ファイル形式だと実体が見えないので、撮影した行為そのものの記憶が薄れるのと同程度の速さで行方不明となっている例が多い。

 これから原画像を探し出しネガフィルムと同じようにしっかりと保存したいのだが、かなり難しそうである。現状は、散逸して行方不明になっているのが大部分である。
 その原因は、まず第一に補助記憶装置の容量が限られているからである。しかし最近、私は大量の原画像を「ほぼ永久に保存する方法」を考え付いたので、その手法を利用すれば、これから撮影されるデータについては大丈夫であろうと思う(「情報の永久保存」参照)。

▼ビッグサムネイル
 かさばる容量の問題が解決したので、これからはどのように見やすくするかが唯一の関心事となる。従来からやっていた圧縮保存する方法は、画像を捜すとき手間がかかるのでできれば避けたい。

 これまでの方法は、撮影の度毎に得られる大量の画像の中から残したい画像だけを選び出し、それを適度な大きさに縮小したり一部をトリミングしたりして(その時点で)良いと判断した画像だけを残してきた。プリントすることもあったが、“サムネイル(*1)と呼ばれる“小さな見本”の一覧を作るのが普通だった。

 原画像すべてを残すという発想は(少なくとも私の場合は)なかったように思う。もし全体を残していれば、後日特定の友人の顔写真が急に必要になったような場合でも、すぐに対応することができるはずである。

【注】(*1)サムネイル(thumbnail、サムネールとも書く)とは、画像を見易くするために縮小した見本のことである。親指(thumb)の爪(nail)くらい小さいということであろう。
小さな見本写真では鑑賞用には使えないが、容量の心配がなくなれば大きなサムネイルを作っても構わなくなるに違いない。これからは“ビッグサムネイル”と呼べるような大きな見本の一覧を作り、見易くすると同時に保存もできるものにしたらどうか。

 そういう発想から作られた私の自作「ビッグサムネイル」の事例を以下に示すことにする。これを原画像のまま保存すると約172MBの大容量が必要になるが、私のビッグサムネイル版では約27KB(単位は KB ですよ!)しか占めないので閲覧にも、保存にも適していることが分かってもらえると思う。

図(ビッグサムネイル版の例)
 (原画像:172MB; ビッグサムネイル版:27KB)

 このビッグサムネイル版を簡単に作成することができるようにといろいろ努力してきたのだが、特に、各画像が縦長か横長かを見極めて美しく並べなければならない。このビッグサムネイルの(11)と(12)の画像を見て欲しい。例示するため(11)番を縦長にトリミングして作ったものが(12)である。こういう見本が自動的にできれば便利であろうと考えた。

▼画像の縦横のサイズを知る
 そのためには、プログラム上で各画像の縦横のサイズ値を正確に知る必要がある。そういう方法が紹介されているサイトがないかウェッブ上を捜してみた。いろいろな方法が紹介されてはいるが、試してみたけれど何故かどれもうまく動作しない。gif とか png ファイルならうまくいくのだが、jpg ファイルはどれも難しいらしい。

 CPAN上でも捜してみたが、大きなプログラムのインストールが必要になるらしく、そんな大事になるのでは一般的な方法としては向いていないと思い使用を断念した。

 こうなれば、自分で作成するしかない! PerlJavascript にしようか。それとも C/C++ かな? 今更 C など持ち出したくはないのだが・・・などと、自分がやりたい手法とそれを実現するための手段(プログラム言語)を比較検討する日々が続いた。

▼プログラマの心得
 システムプログラマという人種は、自分ですべて作ろうとする性癖を持っている(昔は!ですぞ。今は知りません)。他人の作ったプログラムなど信用できないのである。それを使って もししくじったら、他人の作ったプログラムをデバッグしなければならない破目に陥ることになる。それは避けたいと思うからであろう。

 私は、そういう人達にできるだけライブラリプログラムを使うよう指導する立場であったから、“自分で作ろう”などと考えるのは言行不一致であり本来はおかしいのである。

 一方、Perlプログラミングの世界では、Perl言語だけにこだわらずその作業に最適な言語があればそれを使えと教えている。その作業に向いている言語を採用し、それとPerl とを結び付けて、連携して使えるようにすればよいのである。

▼JPEGファイルの構造
 自分で作るには、先ずJPEGファイルの構造を知る必要がある。
 いろいろな情報を集めて勉強した結果、JPEGファイルには複数の形式があることが分かった。特に写真画像のファイル形式は、Exifフォーマットと呼ばれ規格化されている。Exif形式のJPEGファイルは情報の宝庫であることも分かった。撮影時の各種のデータが画像と一緒に記録されているのである。これらを利用できれば、一般利用者にとっても大変便利になるのではないかと思う。たとえば、撮影日時と編集日時を分けて表示できるようになる。

 ただ、各種の情報が置かれている場所は固定的に決まっている訳ではない。特定のヘッダーと呼ばれるマークから始まるフレームの中に置かれている。ある程度の場所は分かるが、それを間違いなく取り出せる保証はない。全体を読み込んで、先頭からマークを頼りに順に細かく解析していく以外に方法はないように思われた。

▼プログラム言語の選択
 先ずC言語で解析プログラムを作ることを考えた。最近はC言語など使っていないので気が重い。最後にCプログラムを作ったのは何時のことだったか。大学教師をしていた時代に作った自然消滅するプログラムかもしれない。

 昔「スパイ大作戦(*2)というテレビドラマがあり、上司から指令を受ける場面が秀逸だった。録音テープで指示が出される。「例によって、君、もしくは君のメンバーが捕えられ、あるいは殺されても、当局は一切関知しないからその積りで。なおこのテープは自動的に消滅する。成功を祈る」とのメッセージが入っており、再生が終わると自然発火して消滅するのである。
【注】(*2)『スパイ大作戦』(原題: Mission: Impossible)は、1966年から1973年まで放送されたアメリカのテレビドラマシリーズ(毎回1時間枠で全171話)である。現在評判になっている劇場版「Mission: Impossible」と比較すると、ピーター・グレイブス(Peter Graves)主演のテレビ版の方が内容的には格段に面白かった。それを毎週楽しむことができたのだから幸せだった。最新鋭のコンピュータとして東芝のラップトップ・コンピュータ(T3100)が登場したこともある。
私は、この自然消滅するプログラムを作りたくてC言語で作ったのを覚えている。それが最後のCプログラムだった。特定の日の特定の時間帯にあらかじめ定めていた暗号キー(授業の最初に伝える)を入力しない限りただのごみファイルになってしまうのである。

 私が学生に出す課題の問題と解答とが密かに後輩の学生たちへと流されていることに気付いた。授業で使うプログラムは専用のプログラムから引き出せるようにしてあるのだが、それを授業中しか引き出せないように工夫したのである。それ以外の時間帯では動作しないようにした。

 ただ、このプログラムが後々まで面倒を引き起こしてくれた。最近のウイルス駆除ソフトは、コンピュータ内に作者不詳の不審なプログラムがあると、ウイルス発見! すわ一大事!とばかりに大騒ぎを始める。これが毎日のこととなれば無視する訳にもいかない。圧縮ファイルの中に置いてあっても見逃してはくれないのである。結局、私はこの歴史的な(?)Cプログラムをすべて自ら駆除することにしたのである。その時、もう二度とCプログラムは作るまいと思ったものだ。

▼Perlプログラム
 Cプログラムで、画像データをバイト単位でチビチビと読み込んで処理するのでは効率が悪いから、全体を一括して読み込んで処理することになるであろう。もしそうなら、全体を文字列と考えて1文字ずつ取り出してくるのだからPerlのプログラムで出来るのではないか。文字列処理のプログラムなら、やはり Perl だ!

 そこで作成したのが次のPerlプログラムである。

GetJPGsize.pl
#!/usr/local/bin/perl
# (c) 2017-3-19 : Coded by shun kinoshita/knuhs

my $directory = "C:/・・・・・・・・・"; 
# 適宜書き替える
my $jpgFile1 = "・・・.JPG"; 
# 適宜書き替える
my $jpgFile2 = "・・・.JPG"; 
# 適宜書き替える

     open(JPG, "$directory/$jpgFile1") || die "ファイルを開けません。: $!";
     
# 一括読み込み
     $/ = undef();  $_ = <JPG>;  $/ = '\n';
     close(JPG);
     
# データへのリファランスを渡す
     my ($W, $H) = GetJPGsize(\$_);
     
# 結果を出力する
     print "$jpgFile1";
     print "\nWidth:=$W, Height:=$H\n";


# 2度目以上はうまく動作しない。
#    open(JPEG,"$directory/$jpgFile2")|| die "ファイルを開けません。: $!";
#    $/ = undef();  $_ = <JPEG>;  $/ = '\n';
#    close(JPEG);
#    ($W, $H) = GetJPGsize(\$_);
#
#    print "$jpgFile2";
#    print "\nWidth:=$W, Height:=$H\n";

     exit;

#################################################
# (c) 2017-3-19 : Coded by shun kinoshita/knuhs
sub GetJPGsize
{
my $mkFFD8=0xFFD8; 
# SOI : Start Of Image
my $mkFFC0=0xFFC0; 
# SOF : Segment Of Frame Header

my ($pos, $W, $H, $count) = (0, 0, 0, 0);
my $jpgRef = shift @_;

# get(n)Byte
sub getByte
{  my $n = shift @_;
   return (unpack("n", substr($$jpgRef, $pos, $n)));
}

# start here
  if( getByte(2) != $mkFFD8 )
  { print "\nJPGファイルではありません!\n"; return(0, 0); };

  $pos = 2;
  while( $pos < 50000 )
  { next if ( getByte(2) != $mkFFC0 );
    $count++; $pos += 5; $H = getByte(2); $pos += 2; $W = getByte(2);
    last if ( $count == 2 );
  } continue { $pos++; }

  return ($W, $H);
}

図( Perlプログラム : GetJPGsize.pl )
▼問題点
 さて、前置きが長くなったが、これからが本論である。
 このPerlプログラム(GetJPGsize.pl)を使って、見事に“横幅”と“高さ”の値を画像データから取り出すことができるようになった。めでたし、と言いたいところだが、問題がない訳ではない。続けて次の画像をデータをこの関数に渡すとうまく動作しないのである。つまり、この GetJPGsize という関数は reusable ではない! 1つのプロセスで1回しか動作してくれないのである。関数にする意味がほとんどないとも言える。

 この原因がどこにあるのか、いろいろ設定を変えて試して見たが分からない。現在は、処理したい画像の数だけプロセスを発生させて対処しているが、何とか原因を突き止めてこの関数を生かしたいと思っている。

 Perlに詳しい方のご教示を受けたいと思っています。

・どこに問題点があるのか、
・どのように直したらよいのか、
 あるいは
・使用しているActivePerl処理系の問題なのか、

教えていただければ幸いです。■