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

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

【縦線軽減】表向きスキャンと裏向きスキャンを統合して縦線の発生しにくいデータを作るためのMacro

 

f:id:yu3xx:20200624015857j:plain

 

表向きスキャンと裏向きスキャンを統合して、縦線の発生しづらいスキャナ下側読み取り面の画像のみを使用したデータを自動作成するためのマクロです。 

 

もくじ

 

 

2020/03/18 追記

複数の作品をまとめてマクロ処理できるようコードを変更しました! 

 

2022/03/18 追記

「さいきんおもうところ」を追加しました!

 

 

縦線対策のキソ

自炊ユーザーの多くが悩む縦線ノイズですが、縦線発生を防ぐには以下の2つの方法が有名です。

 

① 裁断した本を、お札を数えるようにずらしてからバラバラとさばく

(風を送って細かな「紙の粉」を吹き飛ばす!)

 

② スキャナに原稿をセットするたびに、スキャナ読み取り面を清掃する。

(メガネ拭きやクイックルワイパー、アルコールなどなど!)

 

この2つの方法で縦線発生はかなり抑えられますが、それでも発生してしまう縦線は「偶数ページ」に多いように思います。

 

これは以下の画像のようにスキャナの構造が原因だと考えられます。

f:id:yu3xx:20200308214354p:plain

 

 

裏向きスキャンとは?

『裏向きスキャン』とは『本の終わり側からスキャンし始める』ことです。

 さらに言うと、『いつもと逆向きに原稿をセットすること』です。

 

 前項の偶数ページに発生しやすい縦線の対策として、

『本の終わり側からスキャンし始める裏向きスキャン』により、偶数ページと奇数ページを入れ替えたデータを用意し、表向きスキャンと組み合わせることで、全てのページで縦線発生の少ないスキャナ下側読み取り面の画像のデータを作成する方法!

が有用だと考えられます。

 

つまり、

 表向きスキャン ... 奇数ページ だけ使う!

 裏向きスキャン ... 偶数ページ だけ使う!

 結果 ... 縦線の発生確率が少ないデータ!

 

うーん、説明しづらい...。なんとなく伝わりますかね?

 

f:id:yu3xx:20200308144444p:plain

f:id:yu3xx:20200308144618p:plain

こんな感じの手順で作ったデータを「自分で順番を工夫して統合させる」のももちろん出来ますが、めんどくさいのでパパッとマクロでやっちゃいましょう!

 

 

 準備

①表向きスキャン

表紙カバーを含めずに通常の向きで両面スキャン」。「片面」でなく「両面」なのがポイントです。ファイルに付けるページ番号は必ず「1」から。(「0」から始めない!計算の過程でエラーが出る!)

例)僕のヒーローアカデミア_26_0001.jpg 〜 僕のヒーローアカデミア_26_0200.jpg

 

②裏向きスキャン

表紙カバーを含めずに、最後のページを最初にスキャンする向きで両面スキャン」。ファイルに付けるページ番号は同じく必ず「1」から。(最終ページがファイル名0001になるので、表向きスキャンの逆順にファイルが並ぶ!)

必ず、ファイル名先頭に「rev」と付けておいてください!

例)rev僕のヒーローアカデミア_26_0001.jpg 〜 rev僕のヒーローアカデミア_26_0200.jpg

 

③フォルダ準備

「表向き」と「裏向き」をそれぞれ別のフォルダにしまってか、それらを同じフォルダにまとめて入れます。「フォルダ自動作成+自動割り振りマクロ」で作られる「postProc」フォルダをそのまま使うとラクです。

 

f:id:yu3xx:20200318012050p:plain

こんな感じで複数作品も一括処理できます。

 

④マクロ実行

下記マクロをImageJで実行すると、Processing Directoryを選択するよう表示されるので、さっきまとめたフォルダ(上記postProcのやつ)を選んで下さい。すると保存先フォルダに統合されたデータが作成されます!

最後に表紙カバーを後から番号0000とか000とかで適当に追加して!おしまい!ラクチン!


 

マクロコード

//Integrate_ReverseScan.txt
//preProcess FileFormat...
//	normal -> [trueTitle_number], reverse -> [prefix+trueTitle_number]
//preProcess DirFormat...
//	normal -> [trueTitle], reverse -> [prefix+trueTitle]
//number == 0 -> Error!


//preWord of ReverseScan
prefix = "rev";

//Do something for selected folder
showMessage("Select Processing Directory");
procDir = getDirectory("Choose a Directory");
print("Processing :",procDir);
selectWindow("Log");
showMessage("Select Save Directory");
saveDir = getDirectory("Choose a Directory");
print("Save to :",saveDir);
selectWindow("Log");
list = getFileList(procDir);
wait(2500);

//make parentDirectory
parentDir = saveDir+"postProc_"+getTimeStamp()+"/";
File.makeDirectory(parentDir);

//operation
startTime = whatTimeNow();
procCount=0;

for(j=0;j<list.length;j++){
	dirName = list[j];
	print("checking...",dirName);
	
	startFlag = startsWith(dirName,prefix); //1 -> rev, 0 -> normal
	endFlag = endsWith(dirName,"/"); //1 -> dir, 0 -> file

	if(endFlag == 0){
		exit("Error!!  Files must be inside of the directory!");
	} 
	if(startFlag == 0){
		normalDir = procDir+list[j];
		reverseDir = procDir+prefix+list[j];
		n_list = getFileList(normalDir);
		r_list = getFileList(reverseDir);

		if(n_list.length != r_list.length){
			exit("Error!!  Mismatch of total page number!");
		}
		
		procCount++;
		integrateReverseScan();
	}
}

//fin
finishTime = whatTimeNow();
print("Start Time .... ",startTime);
print("FinishTime ... ",finishTime);
print("oshimai");
beep();



//-----------------------------------------------------------------------------
//Define function integrateReverseScan

function integrateReverseScan(){

	//NormalScan
	for (i=0; i<n_list.length; i++){

		//Progress.the second decimal place by 10000/100
		print("[",procCount,"/",list.length/2,"]","-",i+1,"/",n_list.length*2,"...Progress=",floor((i+1)/n_list.length/2*10000)/100,"%");
		
		name = n_list[i];
		dotIndex = lastIndexOf(name,".");
		title = substring(name,0,dotIndex);
		extension = substring(name,dotIndex+1);
		underBar = lastIndexOf(title,"_");
		trueTitle = substring(title,0,underBar);
		number = substring(title,underBar+1);
		number = parseInt(number);

		if(number%2 == 1){
			newname = trueTitle+"_"+zeroPad(number,4)+"."+extension;

			//Save to SubDirectory
			subDir = parentDir+trueTitle+"/";
			if(!File.exists(subDir)){
				File.makeDirectory(subDir);
			}
			File.copy(normalDir+name,subDir+newname);
			print("...Move to ",subDir+newname);
		}
	}

	//ReverseScan
	for (i=0; i<r_list.length; i++){

		//Progress.the second decimal place by 10000/100
		print("[",procCount,"/",list.length/2,"]","-",n_list.length+i+1,"/",n_list.length*2,"...Progress=",floor((n_list.length+i+1)/n_list.length/2*10000)/100,"%");
	
		name = r_list[i];
		dotIndex = lastIndexOf(name,".");
		title = substring(name,0,dotIndex);
		extension = substring(name,dotIndex+1);
		underBar = lastIndexOf(title,"_");
		r_trueTitle = substring(title,0,underBar);
		number = substring(title,underBar+1);
		number = parseInt(number);

		if(number%2 == 1){
			newNumber = r_list.length+1-number;
			newname = trueTitle+"_"+zeroPad(newNumber,4)+"."+extension;
			
			//Save to SubDirectory
			subDir = parentDir+trueTitle+"/";
			if(!File.exists(subDir)){
				File.makeDirectory(subDir);
			}
			File.copy(reverseDir+name,subDir+newname);
			print("...Move to ",subDir+newname);
		}
	}
}


//-----------------------------------------------------------------------------

//-----------------------------------------------------------------------------
//Define function whatTimeNow

function whatTimeNow(){
	getDateAndTime(year,month,dayOfWeek,dayOfMonth,hour,minute,second,msec);
	stringTime="string";
	strYear=""+year;
	month=month+1;
	if(month<10){
		strMonth="0"+month;

	}else{
		strMonth=""+month;
	}
	if(dayOfMonth<10){
		strDayOfMonth="0"+dayOfMonth;
	}else{
		strDayOfMonth=""+dayOfMonth;
	}
	if(hour<10){
		strHour="0"+hour;
	}else{
		strHour=""+hour;
	}
	if(minute<10){
		strMinute="0"+minute;
	}else{
		strMinute=""+minute;
	}
	if(second<10){
		strSecond="0"+second;
	}else{
		strSecond=""+second;
	}
	stringTime=strYear+"/"+strMonth+"/"+strDayOfMonth+"_"+strHour+":"+strMinute+":"+strSecond;
	return stringTime;
}

//-----------------------------------------------------------------------------

//-----------------------------------------------------------------------------
//Define function zeroPadding

function zeroPad(int,digitZeroPad){
	if(int<0){
		exit("ZeroPadding Error!!  int<0");
	}
	stringInt=""+int;
	digitStringInt=lengthOf(stringInt);
	digitSubtra=digitZeroPad-digitStringInt;
	if(digitSubtra<0){
		exit("ZeroPadding Error!!  digitSubtra<0");
	}
	if(digitZeroPad>0){
		for(i=0;i<digitSubtra;i++){
			stringInt="0"+stringInt;
		}
	}
	return stringInt;
}	

//-----------------------------------------------------------------------------

//-----------------------------------------------------------------------------
//Define function getTimeStamp

function getTimeStamp(){
	getDateAndTime(year,month,dayOfWeek,dayOfMonth,hour,minute,second,msec);
	timeStamp="string";
	strYear=""+year;
	month=month+1;
	if(month<10){
		strMonth="0"+month;

	}else{
		strMonth=""+month;
	}
	if(dayOfMonth<10){
		strDayOfMonth="0"+dayOfMonth;
	}else{
		strDayOfMonth=""+dayOfMonth;
	}
	if(hour<10){
		strHour="0"+hour;
	}else{
		strHour=""+hour;
	}
	if(minute<10){
		strMinute="0"+minute;
	}else{
		strMinute=""+minute;
	}
	if(second<10){
		strSecond="0"+second;
	}else{
		strSecond=""+second;
	}
	timeStamp=strYear+strMonth+strDayOfMonth+"_"+strHour+strMinute+strSecond;
	return timeStamp;
}
//-----------------------------------------------------------------------------

 

 

オススメする理由

この方法にしてから、縦線ノイズを目にする機会はかなり減ったように感じますもちろん完全にゼロにはなりませんが、もし入っていたとしても、スキャナ下側読み取り面の縦線なので、数ページで無くなるものが多く補正もラクです。

 

補正の際には、「両面スキャンの元データ」を残しておけば、使ってない側(天井側読み取り面)のデータを保険としてそのまま使えます。これが両面スキャンのミソです。もし万が一そっちにも縦線が入っていれば、「選択範囲内縦線除去マクロ」手直ししてもOK!

 

デメリットとしてはスキャン時間が倍になることですが、自炊作業の中で最高に無駄な時間である「スキャン時の線チェック」と「永遠に続く再スキャン」をスキップ出来ることが一番のメリットです。 

 

 

縦線補正ならコレがオススメ!

 

 

さいきんおもうところ(追記)

偶数ページ(天井側読み取り面)に多いと感じていた縦線ノイズですが、最近、奇数ページ(下側読み取り面)の場合だと「薄〜い縦線ノイズ」が発生しやすいのでは?と気になっています。

 

 奇数ページ...薄〜い縦線ノイズが発生しやすい

 偶数ページ...いつまでも残るしっかり縦線ノイズが多い

 

奇数ページの、パッと見でわかりづらい「薄〜い縦線ノイズ」が考え所で、のちのち保険データで差し替えることを考慮すると、見つけづらい「薄〜い縦線ノイズ」よりも、縦線ノイズがしっかり目に見えて残りやすい「偶数ページの画像だけ拾ってくる」ほうがラクなのでは?とも思ってしまいます。

 

この現象はScanSnap IX500時代や、現在メインスキャナのEPSON製DS-530の使い始めの頃には意識していなかったところなので、もしかするとローラーの劣化や、静電気の発生しやすい冬だからというのもあるかも?

 

もし「偶数ページの画像だけ拾ってくる方法」に切り替える場合には、プログラム内の

if(number%2 == 1){

の部分(normal scan と reverse scanの2箇所)を

if(number%2 == 0){

に変えることで達成できます。

 

 

さいきんおもうところ2(追記 : 2023.11.26)

EPSON DS-530との縦線発生状況について、さらに詳しく調べてみましたので報告します!

 

 奇数ページ...発生頻度多い薄〜い縦線ノイズが発生しやすい

 偶数ページ...発生頻度少ない。長く残る目立つ縦線ノイズが多い

 

偶数ページに目立つと思っていた縦線ノイズですが、真面目にカウントしてみるとEPSON DS-530では頻度が多いのは奇数ページである!ということがわかりました。

 

 

理由は

・偶数ページ側つまり天井側読み取り面は、原稿と直接触れない?(少しスキマがある?)

・触れないので紙粉付着の発生頻度は少ないが、静電気等で一度付着すると原稿が触れないせいでいつまでも残る

 

などが考えられなくもないですが、じっさいわかりません。

 

EPSON DS-530の場合です。ScanSnapIX500のデータを見てみると、やはり偶数ページの発生頻度が高いように感じます。結局、スキャナの内部構造によるということでしょうか?)

 

 

この事実を受けて、このマクロの意義を再考すると、

 

① 同じ側(表向きスキャンor裏向きスキャン)を複数回繰り返しても、同じタイミングで縦線が発生する確率が高そう。なので違う向きのスキャンを2回繰り返すことで縦線による被害を抑えられる。

 

② 機器によって、下側読み取り面と天井側読み取り面でスキャン画像の明るさが違う場合がある(最近中古で購入した二代目DS-530は下側読み取り面がちょっと明るい。初代DS-530ではそんなことはなかったけども)。なので奇数ページか偶数ページで揃えることで明るさも統一される。

 

③ 奇数ページor偶数ページのどちらを使うにしても、もう片方を保険として残しておく価値は非常に高い。さらに最初から奇数ページver.と偶数ページver.を出力しておけば、あとから作業が楽だしデータ容量節約にもなる。

 

ということで、マクロを書き換えて、最初から奇数ページver.と偶数ページver.を出力するように修正しました。

 

//Integrate_ReverseScan_multiTitle
//preProcess FileFormat...
//	normal -> [trueTitle_number], reverse -> [prefix+trueTitle_number]
//preProcess DirFormat...
//	normal -> [trueTitle], reverse -> [prefix+trueTitle]
//number == 0 -> Error!


version = "4.1.0";

//3.0.0 -> Odd or Even
//3.1.0 -> Dialog (odd or even)
//3.2.0 -> name change (postProc directory)
//4.0.0 -> Generate both type (odd and even)

print("");
print("Integrate_ReverseScan_multiTitle");
print("ver",version);

//
procParameta = 0;
timeStamp = getTimeStamp();

//preWord of ReverseScan
prefix = "rev";

//Do something for selected folder
showMessage("Select Processing Directory");
procDir = getDirectory("Choose a Directory");
print("Processing :",procDir);
selectWindow("Log");
showMessage("Select Save Directory");
saveDir = getDirectory("Choose a Directory");
print("Save to :",saveDir);
selectWindow("Log");
list = getFileList(procDir);
wait(2500);



//operation
startTime = whatTimeNow();
procCount=0;

for(j=0;j<list.length;j++){
	dirName = list[j];
	print("checking...",dirName);
	
	startFlag = startsWith(dirName,prefix); //1 -> rev, 0 -> normal
	endFlag = endsWith(dirName,"/"); //1 -> dir, 0 -> file

	if(endFlag == 0){
		exit("Error!!  Files must be inside of the directory!");
	} 
	if(startFlag == 0){
		normalDir = procDir+list[j];
		reverseDir = procDir+prefix+list[j];
		n_list = getFileList(normalDir);
		r_list = getFileList(reverseDir);

		if(n_list.length != r_list.length){
			exit("Error!!  Mismatch of total page number!");
		}
		
		procCount++;
		for (procParameta = 0; procParameta<2; procParameta++){
			if(procParameta == 1) {
				oddEvenTag = "_1Odd";
			}
			if(procParameta == 0) {
				oddEvenTag = "_2Even";
			}
			print("ProcType =",oddEvenTag);
			//make parentDirectory
			parentDir = saveDir+"postProc_"+timeStamp+oddEvenTag+"/";
			File.makeDirectory(parentDir);
			integrateReverseScan();
		}
	}
}

//fin
finishTime = whatTimeNow();
print("Start Time .... ",startTime);
print("FinishTime ... ",finishTime);
print("oshimai");
beep();



//-----------------------------------------------------------------------------
//Define function integrateReverseScan

function integrateReverseScan(){

	//NormalScan
	for (i=0; i<n_list.length; i++){

		//Progress.the second decimal place by 10000/100
		print("[",procCount,"/",list.length/2,"]","-",i+1,"/",n_list.length*2,"...Progress=",floor((i+1)/n_list.length/2*10000)/100,"%");
		
		name = n_list[i];
		dotIndex = lastIndexOf(name,".");
		title = substring(name,0,dotIndex);
		extension = substring(name,dotIndex+1);
		underBar = lastIndexOf(title,"_");
		trueTitle = substring(title,0,underBar);
		number = substring(title,underBar+1);
		number = parseInt(number);

		if(number%2 == procParameta){
			newname = trueTitle+"_"+zeroPad(number,4)+"."+extension;

			//Save to SubDirectory
			subDir = parentDir+trueTitle+"/";
			if(!File.exists(subDir)){
				File.makeDirectory(subDir);
			}
			File.copy(normalDir+name,subDir+newname);
			print("...Move to ",subDir+newname);
		}
	}

	//ReverseScan
	for (i=0; i<r_list.length; i++){

		//Progress.the second decimal place by 10000/100
		print("[",procCount,"/",list.length/2,"]","-",n_list.length+i+1,"/",n_list.length*2,"...Progress=",floor((n_list.length+i+1)/n_list.length/2*10000)/100,"%");
	
		name = r_list[i];
		dotIndex = lastIndexOf(name,".");
		title = substring(name,0,dotIndex);
		extension = substring(name,dotIndex+1);
		underBar = lastIndexOf(title,"_");
		r_trueTitle = substring(title,0,underBar);
		number = substring(title,underBar+1);
		number = parseInt(number);

		if(number%2 == procParameta){
			newNumber = r_list.length+1-number;
			newname = trueTitle+"_"+zeroPad(newNumber,4)+"."+extension;
			
			//Save to SubDirectory
			subDir = parentDir+trueTitle+"/";
			if(!File.exists(subDir)){
				File.makeDirectory(subDir);
			}
			File.copy(reverseDir+name,subDir+newname);
			print("...Move to ",subDir+newname);
		}
	}
}


//-----------------------------------------------------------------------------

//-----------------------------------------------------------------------------
//Define function whatTimeNow

function whatTimeNow(){
	getDateAndTime(year,month,dayOfWeek,dayOfMonth,hour,minute,second,msec);
	stringTime="string";
	strYear=""+year;
	month=month+1;
	if(month<10){
		strMonth="0"+month;

	}else{
		strMonth=""+month;
	}
	if(dayOfMonth<10){
		strDayOfMonth="0"+dayOfMonth;
	}else{
		strDayOfMonth=""+dayOfMonth;
	}
	if(hour<10){
		strHour="0"+hour;
	}else{
		strHour=""+hour;
	}
	if(minute<10){
		strMinute="0"+minute;
	}else{
		strMinute=""+minute;
	}
	if(second<10){
		strSecond="0"+second;
	}else{
		strSecond=""+second;
	}
	stringTime=strYear+"/"+strMonth+"/"+strDayOfMonth+"_"+strHour+":"+strMinute+":"+strSecond;
	return stringTime;
}

//-----------------------------------------------------------------------------

//-----------------------------------------------------------------------------
//Define function zeroPadding

function zeroPad(int,digitZeroPad){
	if(int<0){
		exit("ZeroPadding Error!!  int<0");
	}
	stringInt=""+int;
	digitStringInt=lengthOf(stringInt);
	digitSubtra=digitZeroPad-digitStringInt;
	if(digitSubtra<0){
		exit("ZeroPadding Error!!  digitSubtra<0");
	}
	if(digitZeroPad>0){
		for(i=0;i<digitSubtra;i++){
			stringInt="0"+stringInt;
		}
	}
	return stringInt;
}	

//-----------------------------------------------------------------------------

//-----------------------------------------------------------------------------
//Define function getTimeStamp

function getTimeStamp(){
	getDateAndTime(year,month,dayOfWeek,dayOfMonth,hour,minute,second,msec);
	timeStamp="string";
	strYear=""+year;
	month=month+1;
	if(month<10){
		strMonth="0"+month;

	}else{
		strMonth=""+month;
	}
	if(dayOfMonth<10){
		strDayOfMonth="0"+dayOfMonth;
	}else{
		strDayOfMonth=""+dayOfMonth;
	}
	if(hour<10){
		strHour="0"+hour;
	}else{
		strHour=""+hour;
	}
	if(minute<10){
		strMinute="0"+minute;
	}else{
		strMinute=""+minute;
	}
	if(second<10){
		strSecond="0"+second;
	}else{
		strSecond=""+second;
	}
	timeStamp = strYear+strMonth+strDayOfMonth+"_"+strHour+"h"+strMinute+"m"+strSecond+"s";
	return timeStamp;
}
//-----------------------------------------------------------------------------
---------

 

 

マクロの起動方法

①ImageJ上部タブの[Plugins]→[New]→[Macro]で起動したエディタに、記事のコードをコピペしてtxtファイル(Integrate_ReverseScan.txt)を作成・保存する。

 

②保存したファイルをImageJフォルダ内の[plugins]フォルダにしまう。

 

このとき、[plugins]フォルダの中に新たに適当な名前のフォルダを作って、その中にしまってもOKです。ここでは仮に「自炊」というフォルダにtxtファイルを突っ込んだとします。

 

③一度ImageJを再起動すると、マクロがインストールされ、起動準備OK。

 

④上部タブ[Plugins]→[自炊]→[Integrate ReverseScan]でマクロが実行されます。

 

 

imagej-jisui.hatenablog.com

 

 

 

 

 

 

ライセンスなんかは一切無いので、ぜひぜひ自由に使ってみてください!

 

imagej-jisui.hatenablog.com