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

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

【多目的】コントラスト調整やリサイズ処理などなどを、一気にやってしまうためのMacro

 

f:id:yu3xx:20200624015122j:plain

 

今までに紹介した『コントラスト調整、リサイズ処理、ノンブル領域のカット、ページ番号振り直し処理』から、必要なものだけを選んで「一括処理」するためのマクロです。

 

//Almighty_Processing.txt
//ContrastAdjust (As necessary, for 8bit)
//SetResolution (As necessary, for any Type)
//NombreCut type1 (As necessary, UseOneROI)
//NombreCut type2 (As necessary, CaseDivided by ODD/EVEN, [number==0 -> error])
//Renumbering from1 (As necessary, Files must be in subDirectory)
//Renumbering (As necessary, Automater)
//MakeDirAuto (FileName : trueTitle_number.ext)

print("");
print("Almighty Processing");
print("makeDirAuto");

//Select Processing
Dialog.create("Processing setting");
Dialog.addMessage("Select Processing");
Dialog.addMessage("");
Dialog.addMessage("- Main -");
Dialog.addCheckbox("Contrast Adjustment", false);
Dialog.addCheckbox("Change the Resolution",false);
Dialog.addCheckbox("Nombre Cut",false);
Dialog.addMessage("");
Dialog.addMessage("- Additional -");
Dialog.addMessage("");
Dialog.addCheckbox("Renumbering [from 1]",false);
Dialog.addCheckbox("Renumbering [Automator]",false);
Dialog.show;
flagContr = Dialog.getCheckbox();
flagReso = Dialog.getCheckbox();
flagNombre = Dialog.getCheckbox();
flagRenumFrom1 = Dialog.getCheckbox();
flagRenumATMT = Dialog.getCheckbox();

if(flagRenumFrom1 == 1 && flagRenumATMT == 1) exit("Two \"Renumbering Processing\" were selected.");
//if (flagContr == 0 && flagReso == 0 && flagNombre == 0) exit("You must select at least one processing!!");

//Select Nombre Type
if(flagNombre == 1){
	Dialog.create("Nombre Cut setting");
	items = newArray("Use One ROI", "Case Divided by ODD/EVEN");
	Dialog.addRadioButtonGroup("Select NombreCut Type", items, 2, 1, "Use One ROI");
	Dialog.show;
	nombreRadio = Dialog.getRadioButton();
	if(nombreRadio == "Use One ROI") NombreType = 1;
	if(nombreRadio== "Case Divided by ODD/EVEN") NombreType = 2;
}

//Clear ROI Manager and set ROI
if(flagNombre == 1){
	roiCount = roiManager("count");
	if(roiCount > 0){
		roiManager("Deselect");
		roiManager("Delete");
	}
	if(NombreType == 1){
		//ROI setting
		waitForUser("ROI setting","Open an image and set [ROI] using rectangle tool, then click [OK].");
		roiManager("Add");
		close();
	}
	if(NombreType == 2){
		//ROI setting ODD(1,3,5,7,...)
		waitForUser("ODD(1,3,5,7,...)","Open an [ODD number] file and set [ROI] using rectangle tool, then click [OK].");
		roiManager("Add");
		close();

		//ROI setting EVEN(2,4,6,8,...)
		waitForUser("EVEN(2,4,6,8,...)","Open an [EVEN number] file and set [ROI] using rectangle tool, then click [OK].");
		roiManager("Add");
		close();
	}
}

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

//Enter parameter
Dialog.create("Processing Setting");
if(flagContr == 1) Dialog.addNumber("min (0-255)",0);
if(flagContr == 1) Dialog.addNumber("Max (0-255)",255);
if(flagReso == 1) Dialog.addNumber("pre dpi",600);
if(flagReso == 1) Dialog.addNumber("post dpi(Target)",400);
if(flagRenumATMT == 1 || flagRenumFrom1 == 1) Dialog.addNumber("Digit of Page Number",4);
items = newArray("JPEG", "PNG");
Dialog.addRadioButtonGroup("Output format", items, 1, 2, "JPEG");
Dialog.show;
if(flagContr == 1) min = Dialog.getNumber();
if(flagContr == 1) max = Dialog.getNumber();
if(flagReso == 1) preDpi = Dialog.getNumber();
if(flagReso == 1) postDpi = Dialog.getNumber();
if(flagRenumATMT == 1 || flagRenumFrom1 == 1) digit = Dialog.getNumber();
output = Dialog.getRadioButton();

//Extension
if(output == "JPEG") ext = ".jpg";
if(output == "PNG") ext = ".png";

//JPEG,PNG quality setting(jpeg=90,gif=-1)
quality = 90;
run("Input/Output...", "jpeg=quality gif=-1 file=.csv save_column save_row");

//SetResolution Setting
if(flagReso == 1) compressionRate = postDpi/preDpi;

//reference setting
var referLength;
if(flagRenumATMT == 1){
	referenceCheck(openDir);
}

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


//operation
startTime = whatTimeNow();
setBatchMode(true);
var totalFiles = 0;
var procCount = 0;

if (flagRenumFrom1 == 1){
	Table.create("Correspondence Table");
}
countFiles(openDir);
listFiles(openDir);

//clear ROI Manager
if(flagNombre == 1){
	roiManager("Deselect");
	roiManager("Delete");
}

//fin
if (flagRenumFrom1 == 1){
	Table.update;
}
finishTime = whatTimeNow();
print("");
if (flagContr == 1) print("min =",min,", Max =",max);
if (flagReso == 1) print("Compression Rate =",compressionRate);
if (output == "JPEG") print("JPEG quality =",quality);
print("Start Time .... ",startTime);
print("FinishTime ... ",finishTime);
print("oshimai");
beep();


//-----------------------------------------------------------------------------
//Define referenceCheck

function referenceCheck(dir){
	list = getFileList(dir);
	if(endsWith(list[0],"/")){
		referenceCheck(dir+list[0]);
	}else{
		name = list[0];
		dotIndex = lastIndexOf(name,".");
		title = substring(name,0,dotIndex);
		space = lastIndexOf(title," ");
		trueTitle = substring(title,0,space);
		number = substring(title,space+1);

		reference = trueTitle;
		referLength = lengthOf(reference);
		print("Check trueTitle. (e.g. NARUTO_60)");
		print("trueTitle=[",trueTitle,"]");
		showMessageWithCancel("Check trueTitle (except : number.ext), and click [OK].");
	}
}
//-----------------------------------------------------------------------------


//-----------------------------------------------------------------------------
//Define countFiles

function countFiles(dir){
	list = getFileList(dir);
	for(i=0; i<list.length; i++){
		if(endsWith(list[i],"/")){
			countFiles(dir+list[i]);
		}else{
			totalFiles++;
		}
	}
	
}
//-----------------------------------------------------------------------------

//-----------------------------------------------------------------------------
//Define listFiles

function listFiles(dir){
	list = getFileList(dir);
	for(i=0; i<list.length; i++){
		if(endsWith(list[i],"/")){
			listFiles(dir+list[i]);
		}else{
			open(dir+list[i]);
			operation();
		}
	}
	
}
//-----------------------------------------------------------------------------

//-----------------------------------------------------------------------------
//Define operation

function operation(){
	
	procCount++;
	
	//Rename Section
	if(flagRenumATMT == 1){
		name = getTitle();
		dotIndex = lastIndexOf(name,".");
		title = substring(name,0,dotIndex);

		//ReNumbering
		titleLength = lengthOf(title);

		if(referLength < titleLength){
			space = lastIndexOf(title," ");
			trueTitle = substring(title,0,space);
			number = substring(title,space+1);

			newname = trueTitle+"_"+zeroPad(number,digit)+ext;
		
		}else if(referLength == titleLength){

			newname = title+"_"+zeroPad(1,digit)+ext;

			dotIndex = lastIndexOf(newname,".");
			title = substring(newname,0,dotIndex);
			underBar = lastIndexOf(title,"_");
			trueTitle = substring(title,0,underBar);
			number = substring(title,underBar+1);
		}	
	}else if(flagRenumATMT == 0){
		name = getTitle();
		dotIndex = indexOf(name, ".");
		title = substring(name,0,dotIndex);
		underBar = lastIndexOf(title,"_");
		trueTitle = substring(title,0,underBar);
		number = substring(title,underBar+1);
		newname = title+ext;	
		if (flagRenumFrom1 == 1) newname = trueTitle+"_"+zeroPad(i+1,digit)+ext;
	}
	rename(newname);

	//Output Correspondence table
	if (flagRenumFrom1 == 1){
		Table.set("index",procCount-1,procCount);
		Table.set("Original",procCount-1,name);
		Table.set("Newname",procCount-1,newname);
	}
	//Nombre Cut
	if(flagNombre == 1){
		if(NombreType == 1){
			roiManager("Select", 0);
			run("Crop");
		}
		if(NombreType == 2){
			number = parseInt(number);
			if(number == 0) exit("Error!! Number must not be null.");
			//ODD(1,3,5,7,...)
			if(number%2 == 1){
				roiManager("Select", 0);
				run("Crop");
			}
			//EVEN(2,4,6,8,...)
			if(number%2 == 0){
				roiManager("Select", 1);
				run("Crop");
			}
		}
	}

	//ContrastAdjust 8 bit
	depth = bitDepth();
	if (depth == 8) {
		//ContrastAdjust (As necessary)
		if (flagContr == 1) setMinAndMax(min, max);
		if (flagContr == 1) run("Apply LUT");
	}

	//SetResolution
	if (flagReso == 1){
		width = getWidth;
		height = getHeight;
		targetW = floor(width*compressionRate);
		run("Size...", "width=targetW constrain average interpolation=Bicubic");
	}

	//Progress.the second decimal place by 10000/100
	print(procCount,"/",totalFiles,"...Progress=",floor(procCount/totalFiles*10000)/100,"%");
	
	//Save to SubDirectory
	subDir = parentDir+trueTitle+"/";
	if(!File.exists(subDir)){
		File.makeDirectory(subDir);
	}
	
	saveAs(output, subDir+newname);
	print("Save to...",subDir+newname);

	close(newname);
}
//-----------------------------------------------------------------------------

//-----------------------------------------------------------------------------
//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;
}
//-----------------------------------------------------------------------------

//-----------------------------------------------------------------------------
//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;
}	

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

 

 

 

使い方のポイント

① 起動すると処理選択ウインドウが表示されますので、必要なものだけチェックしてください。

f:id:yu3xx:20200427184509p:plain

② 選択したProcessing Folderの中に入っているサブフォルダ内のデータまで全て処理します。 

 

③ コントラスト調整をする際には、あらかじめ「Optimize MaxAndMin」マクロ適切な濃度・コントラストを求めておくと良いです。

 

④ Renumbering [from 1] は、ImageJで画像を読み込んだ順番で「0001」からページ番号を振り直す処理です。(詳細は後述)

 

⑤ Renumbering [Automator] はMacの「Automatorを使用して「PDF→JPEG」変換をした際に使う、番号振り直し処理です。

 

フォルダ自動生成を命令しているので、各ファイルの名前は「trueTitle_number.拡張子」じゃないと動きません。(numberの前のアンダーバーが大事!)

例)僕のヒーローアカデミア_07_0149.png

 trueTitle = 僕のヒーローアカデミア_07

 number = 0149

 ※Renumbering [Automator] は例外です。numberの前にspaceが来るはずです。

 

⑦ 各処理の細かな解説は、以下のページを参考にしてみてください。

 

 ・Contrast Adjustment

 

 ・Optimize MaxAndMin  (2020.05.14.追記 動作を高速にして、おまけの便利Toolも掲載しました。)

 

 ・Change the Resolution

  

 ・Nombre Cut

 

 ・Renumbering [Automator]

 

 

Renumbering [from 1]について

Renumbering [from 1] は、ページ番号を「0001」から振り直す処理です。

 

私の場合、「Integrate ReverseScan」マクロなどで表紙カバー画像を後から追加する際に、「0000」や[000]など「0の桁違い番号」を使って順番を整理します。この場合、表示ビューワによっては順番が「0000」からだったり、「000」からだったりでバラつくことがあるので、 Renumbering [from 1]で処理すると安心です。

f:id:yu3xx:20200427220921j:plain

画像引用)峰浪りょう, 初恋ゾンビ, 単行本第1巻, 小学館, 2016

 

ImageJの読み込み順は「000」からです。Macの場合。Windowsは未確認)

 

※ Renumbering [from 1]は、サブフォルダに入っているファイル数で番号付けを行っているため、あらかじめタイトルごとにサブフォルダに入れておく必要があります。

初恋ゾンビ_01_000.jpg〜初恋ゾンビ_01_0200.jpgを、サブフォルダ「初恋ゾンビ_01」に。

初恋ゾンビ_02_000.jpg〜初恋ゾンビ_02_0200.jpgを、サブフォルダ「初恋ゾンビ_02」に。

 

おまけでRenumbering [from 1]を単品で使う場合のコードも載せておきます。

(上記Almighty_Processingは、コード簡略化のためRenumbering単品では動作しないように設定してあります。Renumbering処理単品の場合は、以下のコードを使った方が動作が速いです。)

 

Renumbering [from1]単品用マクロコード 

//Renumbering
//from 1
//processing files under subDirectory
//not for Automator


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

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

Table.create("Correspondence Table");

//operation
startTime = whatTimeNow();
setBatchMode(true);
var totalFiles = 0;
var procCount = 0;

countFiles(openDir);
listFiles(openDir);

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


//-----------------------------------------------------------------------------
//Define countFiles

function countFiles(dir){
	list = getFileList(dir);
	for(i=0; i<list.length; i++){
		if(endsWith(list[i],"/")){
			countFiles(dir+list[i]);
		}else{
			totalFiles++;
		}
	}
	
}
//-----------------------------------------------------------------------------

//-----------------------------------------------------------------------------
//Define listFiles

function listFiles(dir){
	list = getFileList(dir);
	for(i=0; i<list.length; i++){
		if(endsWith(list[i],"/")){
			listFiles(dir+list[i]);
		}else{
			operation();
		}
	}
	
}
//-----------------------------------------------------------------------------

//-----------------------------------------------------------------------------
//Define operation

function operation(){
	
	procCount++;
	
	name = list[i];
	dotIndex = lastIndexOf(name,".");
	ext = substring(name,dotIndex);
	title = substring(name,0,dotIndex);
	underBar = lastIndexOf(title,"_");
	trueTitle = substring(title,0,underBar);

	newname = trueTitle+"_"+zeroPad(i+1,4)+ext;
	
	//Output table
	Table.set("index",procCount-1,procCount);
	Table.set("Original",procCount-1,name);
	Table.set("Newname",procCount-1,newname);

	//Progress.the second decimal place by 10000/100
	print(procCount,"/",totalFiles,"...Progress=",floor(procCount/totalFiles*10000)/100,"%");
	
	//Copy to SubDirectory
	subDir = parentDir+trueTitle+"/";
	if(!File.exists(subDir)){
		File.makeDirectory(subDir);
	}
	
	File.copy(dir+name,subDir+newname);
	print("Copy to...",subDir+newname);
}

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

//-----------------------------------------------------------------------------
//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;
}
//-----------------------------------------------------------------------------

//-----------------------------------------------------------------------------
//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;
}	

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

 

 

マクロの起動方法

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

 

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

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

 

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

 

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

 

 

imagej-jisui.hatenablog.com

 

 

 

 

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

  

imagej-jisui.hatenablog.com

 

 

 

 2020.05.07 追記

・Renumbering [Automator] 関連のエラーを修正しました。

・NombreCutで、奇数/偶数ページでROIを分ける処理の他に、全ページを1つのROIで処理するモードを追加しました。

 

 2020.05.08 追記

・Renumbering [from1] 関連のエラーを修正しました。

 

 2020.06.24 追記

・コードの細部を修正しました。