その漫画自炊オタクはImageJマクロに恋をする

プログラミングを用いた、自炊漫画の画像処理

【ImageJマクロ超入門】#2 フォルダ内の全画像を一括処理する方法

 

f:id:yu3xx:20210428223947j:plain


皆さまこんにちは。

真・女神転生Ⅴ Vengeance」が楽しみで仕方ありません、yu3xx(ゆーさんちょめちょめ)です。

 

 

 前回のおさらい



前回は「変数の定義」の仕方から、マクロをどこに書くか、そして保存形式はどのようにするか、などをお話ししました。

 

今回は入門編その2と題していますが、いきなり基礎をはしょって「フォルダ内全画像一括処理」の方法を紹介したいと思います。

 

基礎をぶっ飛ばして実践的なプログラムの書き方を学ぶことになりますが、この部分はいわゆる「お作法」みたいなものです。つまりテンプレです。

 

ここさえ覚えてしまえば、様々な種類のマクロコードを自分で書けるようになる!という部分なので、このタイミングでモノにしてしまいましょう。

 

 

 もくじ

 

 

まずはプログラムを眺めてみる

 

//example 2-1

//フォルダを選択
showMessage("Select Open Folder"); 
openDir = getDirectory("Choose a Directory"); 
showMessage("Select Save Folder");
saveDir = getDirectory("Choose a Directory"); 
list = getFileList(openDir);

//配列「list」の中身をチラ見
Array.show(list); 

//ループを回して全画像を順次処理する
for (i=0; i<list.length; i++){
	
	//指定した場所の画像を開く
	open(openDir+list[i]);
	
	//拡張子よりも前側のファイル名を取得
	name = getTitle();
	dotIndex = lastIndexOf(name,".");
	title = substring(name,0,dotIndex);

	//取得したファイル名を使って、新たな拡張子jpgのファイル名を作成
	newname = title + ".jpg";
	
	//JPEGで保存
	saveAs("Jpeg", saveDir + newname);

	//画像は処理が終わったらその都度閉じる
	close();
}

 

 まずはこれだけです。プログラムの流れをかんたんに説明しますと、

 ①「開きたいフォルダ」と「保存先フォルダ」を実際に選ぶ

 ②「開きたいフォルダ」の中身を、配列「list」に文字列で格納する

 ③  配列「list」の要素数だけループを繰り返す

 ④  ループの内容は「画像開く」「ファイル名取得」「JPEGで保存」「画像閉じる」

 

「フォルダ内の全画像を順次開いていき、JPEGで保存する」という、ただそれだけの処理です。

 

漫画自炊においては、このブログで紹介しているほとんど全てのマクロの基本形です。

 

漫画自炊とは関係なく研究等で画像解析をする場合は、「JPEG保存」の部分を「ROI設定」と「getStatistics」の命令に書き換えることで「全画像の信号値自動解析」なんかにも応用できます。このあたりは(余力があれば)いつか別の記事でも紹介したいと思います。

 

各命令の細かな解説

 

1. フォルダ選択のためのダイアログ表示

 

まずはフォルダ選択の部分です。この部分はまるまるテンプレートとしてよく使う命令です。

 

・showMessage("Select Open Folder");

ポップアップを呼び出し、"  "の中の文字列を表示させます。ただのメッセージなので、この命令自体は無くても問題ありませんが、あったほうが実際に使う時にわかりやすいです。

 

openDir = getDirectory("Choose a Directory");

フォルダ選択用のダイアログが起動します。ここで選択した、開きたいフォルダ(ディレクトリ)の「場所」が「文字列」として変数openDirに格納されます。

 

例えばデスクトップに置いた「3つのpngファイルの入った"TEST"というフォルダ」を対象とする場合、変数openDirの中身は、文字列「/Users/Yu3xx/Desktop/TEST/」となります。

f:id:yu3xx:20210428234112p:plain

 

・list = getFileList(openDir);

 選択した「開きたいフォルダ = ここではopenDir」に入っているファイルの一覧を、配列の中に格納します。配列はここではlistと命名しています。

配列については次回以降で解説しますが、理解としては「データが入っているタンス」と考えてください。

配列には「要素」「要素数」「インデックス」という概念があります。3段のタンスの場合は、「要素数は3」、「インデックスは何段目のタンスか」、「要素はある段に入っているものが何なのか」に相当します。ポイントは「段数つまりインデックスは0から始まる」ということです。

f:id:yu3xx:20210429062825p:plain

 

・Array.show(list);

配列(ここではlist)の要素を一覧で表示します。

f:id:yu3xx:20210429060951p:plain

 

各要素は 配列名[インデックス]で表現されます。

今回の例では、

 list[0] の中身は test01.png

 list[1] の中身は test02.png 

 list[2] の中身は test03.png

となっています。

 

ここまでで、「開きたいフォルダの場所openDir」、「保存先フォルダの場所saveDir」、「開きたいフォルダの中に入っている全てのファイル名が入っている配列list」が取得できました。

 

2. for でループを回して順次処理する
 

・for (i=0; i<list.length; i++){

 

ある値(ここではi)を変化させながら、設定した条件に達するまで、いくつかの処理を繰り返します。


for (初期値; ループをどこまで回すかの条件; 増減の度合い){
  処理文
}

 のカタチで使います。

 

・list.length

 配列.lengthで配列の要素数を表します。つまりここではlistの要素数は3つ(3段のタンス)だったので、list.lengthは3を表します。

 

・i++

これはi = i+1の略です。どういうことかというと、「変数iの中に、現時点のiの値に1だけ足したものを、代入する」という意味になります。噛み砕くと、「iを一個だけ増やす」という意味です。

 

※ 少し、ややこしい話をします。プログラミングでは一般的に「イコール」「代入」という意味で使います。イコール」を跨いだ「右辺」の値を、「左辺」の変数に突っ込んで置き換える、という意味です。算数では「イコール」で繋いだ「左辺」と「右辺」の値は必ず等しいというのがルールです。しかし、プログラミングで使う「イコール」はその「イコール」とは違うということです。

 

結局は、このループは「iの初期値がゼロで、iは3未満が条件で、iを一個ずつ増やしながら、繰り返し処理する」という命令です。つまり、iの値はループを繰り返すごとに「0→1→2」と変化していくことになります。

 

3. ループ内でファイルを開く

 

・open(openDir+list[i]);

 ( )の中の文字列で表される場所にある「ファイルを開く」という処理です。

ここで、「i」はループの度に変わる変数です。最初のループではlist[0]となり、タンスの一番上に格納されている「test01.pngという文字列を表します。

 文字列同士の足し算は「文字列をくっつけて一つにする」という処理ですので、「openDir+list[0]」「/Users/Yu3xx/Desktop/TEST/test01.pngとなり、ファイル「test01.png」のコンピュータ上の住所を表すことになります。

 これによって「test01.png」がImageJによって開かれることになります。

 

 4.  ファイル名を取得して、新たなファイル名に直す

 

 ・name = getTitle();

開かれている画像のファイル名を取得して、変数nameに代入します。i=0の時は「test01.pngとなります。

 

・dotIndex = lastIndexOf(name,".");

(   )内の前半で示される文字列(ここではnameすなわちtest01.png)の中で、探したい文字(ここではドット「.」)が最後に出てくるのが、頭から数えて何番目なのかを調べる処理です。「最後に」というのは、「探したい文字」が複数回使われているときには、「一番最後に」登場したモノの順番を結果として扱うということです。

順番はゼロから数え始めるので、dotIndexという変数に「6」が代入されます。

f:id:yu3xx:20210429170809p:plain


・title = substring(name,0,dotIndex);

substringは「文字列を切り取って抜き出す」処理です。

( )の中身の意味は

(対象とする文字列, 最初の文字の順番、最後の文字の順番)となり、実際には「最後の文字の順番-1番目」までの文字を抜き出すという処理になります。-1というのがキモです。

上記の例では変数nameで表される文字列の、「0」番目から「変数dotIndexの値-1」番目までの文字を抜き出すという処理になり、0から5番目までの文字列「test01」が変数titleの中に格納されます。

substringは「最後の文字の順番-1番目」のあたりがややこしいですが、感覚的には「ドット(.)の手前までを抜き出して!」という処理でした。

 

・newname = title + ".jpg";

 取り出した文字列title(test01)に「.jpg」を追加して、変数newnameに格納します。

 

つまりここで出来上がるのは「test01.jpg」という文字列で、ここまででjpg保存の「準備」が完了しました。

 

5. ファイルの保存

 

・saveAs("Jpeg", saveDir + newname);

saveDir+newnameは「保存先のフォルダの場所」+「新しいファイル名」となり、これによって最初に指定した保存先フォルダに「画像をjpgに変換したもの」が保存されます。

 

・close();

(  ) で指定したウィンドウを閉じます。空欄にして指定しない場合はアクティブなウィンドウが閉じられるので、結果的に「開いていた画像」が閉じられます。開きっぱなしにすると重たくなるので、終わったら閉じるようにしましょう。

 

5. そして次のループへ

 

ここまでで一周目ループ「i = 0」が終了して、今度は「i = 1」が始まります。このようにiを用いてループを回すことで、全画像に対してファイル名取得とJPG保存という処理が完遂されることになります。

 

 

以上が基本形の解説でした。思ったよりも説明が煩雑になってしまいました。

 

よくわからなくても自分でコードを打ち込んで、時には変数を違う名前に変えたりしながらマクロを走らせてみると理解が進むと思います。

 

アレンジで困ったらRecordを使う

 

最後にさっそくアレンジを加えてみようと思います。 

さきほどはただJPG保存しただけでしたが、これに「白黒反転」の処理を加えてみましょう。

 

と、ここで迷います。

「白黒反転」はどう書けばいいのでしょうか?

 

もしマクロを使わずに「白黒反転」するのであれば、画像が開かれている状態で[Edit]→[Invert]です。

f:id:yu3xx:20210429174711j:plain

 なんと!実はここまでわかっていれば、マクロコード化はカンタンなのです。

「Record」という機能を使うと、ImageJ上で処理した内容をプログラミング上の命令文としてテキストで残すことができるのです!

 

実際にやってみましょう。上部タブ[Plugins]→[Macros]→[Record]でRecord機能をOnにすると、Recorderウィンドウが表示されます。

f:id:yu3xx:20210429175107p:plain

そして、白黒反転処理[Edit]→[Invert]をクリックすると、

f:id:yu3xx:20210429175311p:plain

run("Invert"); と表示されました!

これが白黒反転の命令文のようです。

あとはこれをコピペで、さきほどのマクロコードに追加してみます。

 

//example 2-2

//フォルダを選択
showMessage("Select Open Folder"); 
openDir = getDirectory("Choose a Directory"); 
showMessage("Select Save Folder");
saveDir = getDirectory("Choose a Directory"); 
list = getFileList(openDir);

//配列「list」の中身をチラ見
Array.show(list); 

//ループを回して全画像を順次処理する
for (i=0; i<list.length; i++){
	
	//指定した場所の画像を開く
	open(openDir+list[i]);
	
	//拡張子よりも前側のファイル名を取得
	name = getTitle();
	dotIndex = lastIndexOf(name,".");
	title = substring(name,0,dotIndex);

	//取得したファイル名を使って、新たな拡張子jpgのファイル名を作成
	newname = title + ".jpg";
	
	//追加処理
	run("Invert");

	//JPEGで保存
	saveAs("Jpeg", saveDir + newname);

	//画像は処理が終わったらその都度閉じる
	close();
}

これでOKです!

 

こんなかたちで、命令文を書くのに困ったら「RecordをOnしながらその操作を実際にやってみる」と上手くいくことがよくあります。 

 

 

 

今回は以上です。

全画像一括処理の基本形でしたが、アレンジを加えることで「漫画自炊」と「画像解析」どちらにも対応できると思います。

 

 

命令文に困ったらRecord機能を使うか、下の「よく使うコード解説」の記事も参考にしてみてください。


 

 

今回の大事なポイントこのあたり 

 ① getDirectoryからgetFileListの流れはよく使うテンプレ

 ② forループで全画像に対して処理する

 ③ 困ったらRecord機能

 

 

もっと上手に使いたい人に

 

 今回例として紹介したマクロは「あるフォルダ直下の全画像」でしたが、これだと「あるフォルダの下にある子フォルダの中にある画像ファイル」までは開くことができません。

 

「あるフォルダの下にある子フォルダの中身も含めた全画像」を対象とするには、自分で自分を呼び出す再帰的な関数」を使うと解決できます。以下の応用編リンクで解説していますので、もし余力があればこちらもどうぞ!

 

  

 

 

 

おしまい 

 

 

 次回は基本中の基本、「繰り返し処理」についてです。こちらもご覧下さい。

 

 

 

・公開したマクロのまとめ

imagej-jisui.hatenablog.com