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

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

【2022最新版】縦線ノイズを自動補正するMacro

 

f:id:yu3xx:20220223211351p:plain

 

  

縦線を自動検出・補正するためのマクロです。従来の縦線補正マクロの動作を微調整しました。

 

 

もくじ

 

 

コード1(縦線ノイズの補正マクロ)

 

//HiSp_Vertical_Noise_Correction_new.ijm
//Save all images

version = "5.0.0";

print("");
print("HiSp_Vertical_Noise_Correction");
print("ver",version);
print("listFilesRecursively");
print("makeDirAuto");


//--------Set Parameter--------------

//Segmentation setting
xSeg = 20;
ySeg = 20;

//BlackZone threshold(L_mean)
TH_kuroBeta = 50;

//WhiteLine threshold(s_mean)
TH_whiteLine = 20;

//HighIntensity threshold
TH_HighIntensity = 20;

//HighCount threshold
TH_countRatio = 80;

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


setBatchMode(true);

//Do something for selected folder
showMessage("Select Open 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);
var credit;

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

//Output format
Dialog.create("Select Output Format");
items = newArray("JPEG", "PNG");
Dialog.addRadioButtonGroup("Output format", items, 1, 2, "JPEG");
Dialog.show;
output = Dialog.getRadioButton();
var teketeke;

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


//To prevent a memory shortage error
var x,y,sum,xStep,yStep,depth,segCount,xi,yi,pixelValue,detectedCount,rejectCounter,correctCount;
var saveFlag,width,height,L_mean,a,b,s_mean,count,ii,jj,pixel_intensity,iii,beta,correctedValue;
var xSeg,ySeg,TH_kuroBeta,TH_whiteLine,TH_HighIntensity,TH_countRatio,seed;
var pre1991,pos1639;

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


//operation

startTime = whatTimeNow();
var totalFiles = 0;
var procCount = 0;
teketeke = "created by ";
var correctCount = 0;
countFiles(openDir);
listFiles(openDir);


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



//-----------------------------------------------------------------------------
//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 NoiseCorrection

function operation(){

	x = 0;
	y = 0;

	detectedCount = 0;
	rejectCounter = 0;

	procCount++;

	//progress
	print(procCount,"/",totalFiles,"...Progress=",d2s(procCount/totalFiles*100,2),"%");
	
	saveFlag = 0;
	//correctCount = 0;
	sum = 0;
	pre1991 = "yu";

	width = getWidth();
	height = getHeight();
	
	//value of movement
	xStep = floor(xSeg/2);
	yStep = floor(ySeg/2);

	depth = bitDepth();
	if (depth == 8) {
		
		//Segmentation section
		segCount = 1;
		for(y=0; y<height-ySeg; y=y+yStep){
			for(x=0; x<width-xSeg; x=x+xStep){
				for(xi=0; xi<xSeg; xi++){
					for(yi=0; yi<ySeg; yi++){
						pixelValue = getPixel(x+xi,y+yi);
						sum = sum+pixelValue;
					}
				}
				L_mean = sum/xSeg/ySeg;
				sum = 0;
				
				//print(i+1,"/",list.length,"...Progress=",floor((i+1)/list.length*10000)/100,"%",", ","Segment No=",segCount);
				segCount++;

				//recognize kuroBeta
				if(L_mean < TH_kuroBeta){
					a = x+5; //exclude both ends of image (5+5 pixels)
					b = y;

					//small rectangle ROI scan
					for(ii=0; ii<xSeg-10; ii++){
						b = y; //reset b (++ by jj_loop)
						for(yi=0; yi<ySeg; yi++){
							pixelValue = getPixel(a,b+yi);
							sum = sum+pixelValue;
						}
						s_mean = sum/ySeg;
						sum = 0;

						//vertical direction scan on detected whiteLine
						if(s_mean > TH_whiteLine){
							count = 0;
							for(jj=0; jj<ySeg; jj++){
								pixel_intensity = getPixel(a,b);
								
								//count HighIntensity
								if(pixel_intensity > TH_HighIntensity){
									count++;
									b++;
								}
							}
							//Noise Correction
							if(count > ySeg*TH_countRatio/100){
								correctCount++;
								//print("(",a,",",b,")","Noise Correction!!",correctCount);
								saveFlag = 1;
								b = y;		
								for(iii=0; iii<ySeg; iii++){
									sideVal1 = getPixel(a-1,b);
									sideVal2 = getPixel(a+1,b);
									if(sideVal1 <100 || sideVal2 < 100){
										seed = random(); //between 0 and 1
										beta = L_mean - seed*10;
										if(beta < 0) beta = 0;
										correctedValue = floor(beta);
										setPixel(a,b,correctedValue);
									}
									b = b+1;
								}
							}
						}
						a = a+1; //in ii_loop
					}
				}
			}
		}
	}else{
		rejectCounter++;
		print("...Color Image!");
	}
	pos1639 = "3xx";
	if(saveFlag != 10){
		name = getTitle();
		dotIndex = lastIndexOf(name,".");
		title = substring(name,0,dotIndex);
		underBar = lastIndexOf(title,"_");
		trueTitle = substring(title,0,underBar);
		number = substring(title,underBar+1);
		newname = title+ext;
		rename(newname);

		//Save to SubDirectory
		subDir = parentDir+trueTitle+"/";
		if(!File.exists(subDir)){
			File.makeDirectory(subDir);
		}
		credit = teketeke + pre1991 + pos1639;

		saveAs(output,subDir+newname);
		print("Save to...",subDir+newname);
		if(pre1991+pos1639 == "yu3xx")detectedCount++;
	}
	close();
}

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

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

 

 

コード2(横線ノイズの補正マクロ)

 

横向きスキャンの場合はこちら。

//HiSp_Horizontal_Noise_Correction_new.ijm
//Save all images

version = "5.0.0";

print("");
print("HiSp_Horizontal_Noise_Correction");
print("ver",version);
print("listFilesRecursively");
print("makeDirAuto");


//--------Set Parameter--------------

//Segmentation setting
xSeg = 20;
ySeg = 20;

//BlackZone threshold(L_mean)
TH_kuroBeta = 50;

//WhiteLine threshold(s_mean)
TH_whiteLine = 20;

//HighIntensity threshold
TH_HighIntensity = 20;

//HighCount threshold
TH_countRatio = 80;

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


setBatchMode(true);

//Do something for selected folder
showMessage("Select Open 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);

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

//Output format
Dialog.create("Select Output Format");
items = newArray("JPEG", "PNG");
Dialog.addRadioButtonGroup("Output format", items, 1, 2, "JPEG");
Dialog.show;
output = Dialog.getRadioButton();

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


//To prevent a memory shortage error
var x,y,sum,xStep,yStep,depth,segCount,xi,yi,pixelValue,detectedCount,rejectCounter,correctCount;
var saveFlag,width,height,L_mean,a,b,s_mean,count,ii,jj,pixel_intensity,iii,beta,correctedValue;
var xSeg,ySeg,TH_kuroBeta,TH_whiteLine,TH_HighIntensity,TH_countRatio,seed;


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


//operation

startTime = whatTimeNow();
var totalFiles = 0;
var procCount = 0;
var correctCount = 0;
countFiles(openDir);
listFiles(openDir);


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



//-----------------------------------------------------------------------------
//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 NoiseCorrection

function operation(){

	run("Rotate 90 Degrees Right");

	x = 0;
	y = 0;

	detectedCount = 0;
	rejectCounter = 0;

	procCount++;

	//progress
	print(procCount,"/",totalFiles,"...Progress=",d2s(procCount/totalFiles*100,2),"%");
	
	saveFlag = 0;
	//correctCount = 0;
	sum = 0;

	width = getWidth();
	height = getHeight();
	
	//value of movement
	xStep = floor(xSeg/2);
	yStep = floor(ySeg/2);

	depth = bitDepth();
	if (depth == 8) {
		
		//Segmentation section
		segCount = 1;
		for(y=0; y<height-ySeg; y=y+yStep){
			for(x=0; x<width-xSeg; x=x+xStep){
				for(xi=0; xi<xSeg; xi++){
					for(yi=0; yi<ySeg; yi++){
						pixelValue = getPixel(x+xi,y+yi);
						sum = sum+pixelValue;
					}
				}
				L_mean = sum/xSeg/ySeg;
				sum = 0;
				
				//print(i+1,"/",list.length,"...Progress=",floor((i+1)/list.length*10000)/100,"%",", ","Segment No=",segCount);
				segCount++;

				//recognize kuroBeta
				if(L_mean < TH_kuroBeta){
					a = x+5; //exclude both ends of image (5+5 pixels)
					b = y;

					//small rectangle ROI scan
					for(ii=0; ii<xSeg-10; ii++){
						b = y; //reset b (++ by jj_loop)
						for(yi=0; yi<ySeg; yi++){
							pixelValue = getPixel(a,b+yi);
							sum = sum+pixelValue;
						}
						s_mean = sum/ySeg;
						sum = 0;

						//vertical direction scan on detected whiteLine
						if(s_mean > TH_whiteLine){
							count = 0;
							for(jj=0; jj<ySeg; jj++){
								pixel_intensity = getPixel(a,b);
								
								//count HighIntensity
								if(pixel_intensity > TH_HighIntensity){
									count++;
									b++;
								}
							}
							//Noise Correction
							if(count > ySeg*TH_countRatio/100){
								correctCount++;
								//print("(",a,",",b,")","Noise Correction!!",correctCount);
								saveFlag = 1;
								b = y;		
								for(iii=0; iii<ySeg; iii++){
									sideVal1 = getPixel(a-1,b);
									sideVal2 = getPixel(a+1,b);
									if(sideVal1 <100 || sideVal2 < 100){
										seed = random(); //between 0 and 1
										beta = L_mean - seed*10;
										if(beta < 0) beta = 0;
										correctedValue = floor(beta);
										setPixel(a,b,correctedValue);
									}
									b = b+1;
								}
							}
						}
						a = a+1; //in ii_loop
					}
				}
			}
		}
	}else{
		rejectCounter++;
		print("...Color Image!");
	}	
	if(saveFlag != 10){
		run("Rotate 90 Degrees Left");
		name = getTitle();
		dotIndex = lastIndexOf(name,".");
		title = substring(name,0,dotIndex);
		underBar = lastIndexOf(title,"_");
		trueTitle = substring(title,0,underBar);
		number = substring(title,underBar+1);
		newname = title+ext;
		rename(newname);

		//Save to SubDirectory
		subDir = parentDir+trueTitle+"/";
		if(!File.exists(subDir)){
			File.makeDirectory(subDir);
		}

		saveAs(output,subDir+newname);
		print("Save to...",subDir+newname);
		detectedCount++;
	}
	close();
}

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

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

 

従来処理との違い

 

従来処理はこちら

 

① 縦線ノイズを検出した画像も、検出されなかった画像も、全て保存先フォルダに保存。

 

② サブフォルダ内の画像まで全て処理対象。

 

③ 補正時の動作を微調整。

 

今までの処理マクロでは、保存される画像は「縦線を検出・補正した画像のみ」でした。ですのでその後「処理前のフォルダに処理後の画像を上書き移動して、全画像もれなく揃える」という作業が必要でした。めんどいので最初から全画像保存にしました。

 

マクロ開始後に指定する作業フォルダに含まれる「子フォルダ」や、「子フォルダ内の孫フォルダ」内の画像も含めて、全ての画像が対象となるように拡張しました。

 

 

わたしの縦線補正ルーチン

 

① 基本的に裏向きスキャン統合法を使うので、自炊後の読書で縦線に気づいたら、保険として残している逆方向のスキャンデータと差し替える


保険データにも運悪く縦線が入っていたら、局所版の縦線補正マクロ


③ 裏向きスキャン統合法を用いていないデータを、まるっと全部補正したいときには、今回紹介している「2022最新版 縦線自動補正マクロ」で

 

 

こんな感じでやっています。

 

それなりに時間がかかる処理ですが、フォルダ指定した後は待つだけなので、寝る前に実行しておくでokです。

 

 

 

コツ

 

補正が上手く働くようにするには、画像データの濃度・コントラストや解像度に応じて、TH_kuroBetaなどのパラメータを試行錯誤しながら調整する必要があります。

ポイントとしては、縦線補正マクロを使う前に「白黒コントラストがしっかりついた画像になるように濃度コントラストを補正しておく」ことです。黒いベタがしっかり黒っぽくなっていないと、上手く動作させるのは難しいと思います。

 

optimizedMinAndMaxマクロを使って濃度コントラストの最適min値とMax値を調べて、min値にプラス10するぐらいが、縦線補正にちょうどいいコントラストだと感じます。


濃度コントラストの調整処理はAlmighty Processingマクロを使うと便利です。


 

 

おしまい

 

 

 

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

  

imagej-jisui.hatenablog.com