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

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

スキャンデータの品質チェックのために、読み取り直後に2秒間だけ画像表示させるMacro

 



スキャナでファイルを保存した直後に、データ確認のため2秒間だけ画像表示させるマクロです。

 

//FileWatcher_Modoki.ijm
//Detect if there are any "Newly created files" in selected dir.
//"Deleted files" and "Modified files" are not the subject.



print("");
print("FileWatcher_Modoki.ijm");
print("");


checkImgTime = 2000;


//select dir
dir = getDirectory("Choose a Directory");
print("Watching :",dir);
print("");


//Main ope
list0 = getFileList(dir);
count =0;
progress="*";

while(count<10000){
	//Show Progress
	print("\\Update:"+progress);
	wait(200);
	
	//Create list1
	list1 = getFileList(dir);
	
	//Check Missmatch list0 and list1
	for(i=0;i<list1.length;i++){
		if(fileExistInArray(list1[i],list0) == 0){
			open(dir+list1[i]);
			wait(checkImgTime);
			close();
			count=0;
		}
	}
	//OverWrite list0
	list0 = Array.copy(list1);
	
	
	progress = progress+"*";
	if(count%20 == 0) progress = "*";
	count++;

}

showMessage("Time out");	

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

function fileExistInArray(fileName,ARRAY){
	output = 0;
	for(i=0;i<ARRAY.length;i++){
		if(ARRAY[i] == fileName) output = 1;
	}
	return output;
}

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

 

「新規ファイル検知→2秒間画像表示」を生業とするマクロです。

 

単純なマクロですが、スキャナに原稿をセットしたあとは、ボーッとディスプレイを眺めているだけでスキャンデータの異常(主に縦線ノイズ)を即時チェックできるので、シンプルに捗ります。

 

 

動作内容

 

① 起動後、どのディレクトリを監視するのか手動で選択。

② ImageJがフォルダ監視状態になる。

③ 新たに保存されたファイルを検知すると、自動的に「2秒間だけ画像表示」してから閉じる。

④ 新しいファイルが30分程度保存されない場合には、監視中断。

 

 

マクロの起動方法

 

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

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

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

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

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

 

 

imagej-jisui.hatenablog.com

 

 

 

 

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

 

imagej-jisui.hatenablog.com

 

 

 

 

 

 

【大型サイズの本】上下二分割に切り離して無理矢理スキャンした大きな本の画像を、自動結合したい時に使うマクロ

 

 

 

スキャナに入らないような大型サイズの本を上下ニ分割に切ってからスキャンした際に、「範囲認識+切り抜き+角度調整+結合」まで全て自動でやってくれるマクロです。

 

 

このファイル結合マクロのルールは

  ① カラーRGB画像

  ② 「上下」二分割の画像をタテに結合

です。

 

//AutoCrop_and_combine.ijm


version = "3.5.2";

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

setBatchMode(true);



//-----------------------------------------------------
//parameta

estimationType = 3;	//1:LSM, 2:RANSAC, 3:LTS Light

var maxTrials = 100;	//Max number of trials (estType2)
var reliableInlierRatio = 0.8;	//Stop Trial when a reliable inlier ratio is reached (estType2)
var distanceThresh = 2;	// Threshold Line of Inlier (estType2)

var inlierThresh = 10;	// Threshold Line of Inlier (estType3)


xMax = 100;		//Range of edge detection [pixel]
yMax = 100;
spikeThresh = 30;	//threshold of nijibibunn

var range = 20;	//range of pxShift
flagSizeAdj = 1;	//match upper and lower resolution (width)

newTitle = "TEST";



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


if(estimationType == 1) {
	print("LSM (Least Squares Method)");
	procTag = "LSM";
}
if(estimationType == 2) {
	print("RANSAC (Random Sample Consensus)");
	procTag = "RANSAC";
}
if(estimationType == 3) {
	print("LTS Light (Least Trimmed Squares Light)");
	procTag = "LTS1";
}

var credit;
var teketeke;

//Do something for selected folder
showMessage("Select Open Folder 1 (UPPER)");
open1Dir=getDirectory("Choose a Directory");
print("Processing 1 (UPPER) :",open1Dir);
selectWindow("Log");
var pre1991,pos1639;

showMessage("Select Open Folder 2 (LOWER)");
open2Dir=getDirectory("Choose a Directory");
print("Processing 2 (LOWER) :",open2Dir);
selectWindow("Log");

showMessage("Select Save Folder");
saveDir=getDirectory("Choose a Directory");
print("Save to :",saveDir);
selectWindow("Log");

list1=getFileList(open1Dir);
list2=getFileList(open2Dir);
if(list1.length != list2.length) exit("Error!kazu chigau");

wait(1000);

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

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


//MAIN OPERATION
startTime=whatTimeNow();
count=1;

for (i=0; i<list1.length;i++){
	print("imgNo:",count);

	//upper
	print("Now, autoCropUpper!!");
	open(open1Dir+list1[i]);
	autoCrop();
	saveAs("JPEG", saveDir +"UpperImage.jpg");
	close();

	//lower
	print("Next, autoCropLower!!!!!!");
	open(open2Dir+list2[i]);
	run("Rotate... ", "angle=180 grid=1 interpolation=Bicubic");
	autoCrop();
	run("Rotate... ", "angle=180 grid=1 interpolation=Bicubic");
	saveAs("JPEG", saveDir +"LowerImage.jpg");
	close();

	//combine (vertical)
	print("Finally, combineAuto!!!!!!!!");
	combineAuto();
	newname = newTitle+"_"+zeroPad(count,4)+".jpg";
	saveAs("JPEG", parentDir+newname);
	print("Save to...",parentDir+newname);

	count++;
	close();
}

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

print(credit);
print("oshimai");
beep();


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

function autoCrop(){

w=getWidth();
h=getHeight();

//Detect Lower Edge (joint line)
istep=1;
jstep=1;

X = newArray();
Y = newArray();
TEMP = newArray(1);

//Edge detection (second derivative)
for(i=0;i<w;i=i+istep){
	for(j=0;j<yMax-3;j=j+jstep){
		v0 = getPixelRGB(i,h-1-j-3)-getPixelRGB(i,h-1-j-2);
		v1 = getPixelRGB(i,h-1-j-2)-getPixelRGB(i,h-1-j-1);
		v2 = getPixelRGB(i,h-1-j-1)-getPixelRGB(i,h-1-j);
		teketeke = "created by ";
		
		nijiBibun = abs((v0-v1)-(v1-v2));
		if(nijiBibun > spikeThresh) {
			TEMP[0] = i;
			X = Array.concat(X,TEMP);
			TEMP[0] = h-1-j-3;
			Y = Array.concat(Y,TEMP);
			j = yMax-3;//break
		}
	}
}

//Array.show(X);
//Array.show(Y);

//calculate a and b

if(estimationType == 1){
	//least squares methods
	n = X.length;
	a = (sumArrayDot(X,Y) - sumArray(X) * sumArray(Y) / n) / (sumArraySqr(X) - pow(sumArray(X),2) / n);
	b = (sumArray(Y) - sumArrayMulti(a,X)) / n;
	optimal_a = a;
	optimal_b = b;
}

pre1991 = "yu";

if(estimationType == 2){
	//RANSAC
	trial = 1;
	maxCount = 0;
	
	progress = "";
	print(progress);
	progre = floor(maxTrials/10);

	n = X.length;
	inlierCount = 0;
	reliableInlierCount = n * reliableInlierRatio;

	Xmod = Array.copy(X);	//for Checking distance
	Ymod = Array.copy(Y);
	Xsamp = Array.copy(X);	//for Random sampling without replacement
	Ysamp = Array.copy(Y);

	while(trial < maxTrials && inlierCount < reliableInlierCount){
		if(trial % progre == 0){
			progress = progress+"^";
			print("\\Update:"+progress);
		}

		inlierCount=0;

		//Random sampling without replacement (point1)
		rdm1 = floor(random()*n);
		x1 = Xsamp[rdm1];
		y1 = Ysamp[rdm1];
		Xmod = ArrayReject(X,rdm1);
		Ymod = ArrayReject(Y,rdm1);
		Xsamp = ArrayReject(Xsamp,rdm1);
		Ysamp = ArrayReject(Ysamp,rdm1);
		n--;

		//Random sampling without replacement (point2)
		rdm2 =  floor(random()*n);
		x2 = Xsamp[rdm2];
		y2 = Ysamp[rdm2];
		Xmod = ArrayReject(Xmod,rdm2);
		Ymod = ArrayReject(Ymod,rdm2);
		Xsamp = ArrayReject(Xsamp,rdm2);
		Ysamp = ArrayReject(Ysamp,rdm2);
		n--;

		//linear function through point1 and point2
		a =(y2-y1) / (x2-x1);
		b = y2-a*x2;
	
		//points except point1 and 2 
		nMax = X.length-2;

		//number of other points left (decrease gradually) 
		nMod = nMax;

		//Checking distance between ideal and the real
		for(i=0;i<nMax;i++){
			rdm3 = floor(random()*nMod);
			x3 = Xmod[rdm3];
			y3 = Ymod[rdm3];
			nMod--;
		
			distance = abs(y3 - (a*x3+b));

			if(distance < distanceThresh) inlierCount++;
			Xmod = ArrayReject(Xmod,rdm3);
			Ymod = ArrayReject(Ymod,rdm3);	
		}
	
		//king of inlierCounts -> optimal
		if(inlierCount>maxCount){
			optimal_a = a;
			optimal_b = b;
			maxCount = inlierCount;
			inlierRatio = floor(inlierCount*1000/nMax*100)/1000;
		}
		
		//next traial
		trial++;
	}
	
	print("Trials = ",trial);
	print("Inlier Ratio = ",inlierRatio);
	print ("optimal_a = ",optimal_a);	
	print ("optimal_b = ",optimal_b);


}

if(estimationType == 3){
	
	//least squares methods (KARI)
	n = X.length;
	nMax = n;
	a = (sumArrayDot(X,Y) - sumArray(X) * sumArray(Y) / n) / (sumArraySqr(X) - pow(sumArray(X),2) / n);
	b = (sumArray(Y) - sumArrayMulti(a,X)) / n;

	//Checking distance between ideal and the real
	Xmod = newArray();
	Ymod = newArray();
	
	//Create new array with  inlier
	for(i=0;i<nMax;i++){
		x0 = X[i];
		y0 = Y[i];
		
		distance = abs(y0 - (a*x0+b));
		
		//Reject outlier
		if(distance < inlierThresh){
			TEMP[0] = X[i];
			Xmod = Array.concat(Xmod,TEMP[0]);
			TEMP[0] = Y[i];
			Ymod = Array.concat(Ymod,TEMP[0]);	
		}
	}
	

	//least squares methods (HONBAN)
	n = Xmod.length;
	a = (sumArrayDot(Xmod,Ymod) - sumArray(Xmod) * sumArray(Ymod) / n) / (sumArraySqr(Xmod) - pow(sumArray(Xmod),2) / n);
	b = (sumArray(Ymod) - sumArrayMulti(a,Xmod)) / n;
	
	inlierRatio = floor(n*1000/nMax*100)/1000;
	print("Inlier Ratio = ",inlierRatio,"%");
	optimal_a = a;
	optimal_b = b;
	print ("optimal_a = ",optimal_a);	
	print ("optimal_b = ",optimal_b);

}
pos1639 = "3xx";

if(estimationType == 4){
	//true LTS
	n = X.length;
	nMax = n;
	iter = 1;
	maxIter = nMax*0.25;

	progress = "";
	print(progress);
	progre = floor(maxIter/10);
	
	a = (sumArrayDot(X,Y) - sumArray(X) * sumArray(Y) / n) / (sumArraySqr(X) - pow(sumArray(X),2) / n);
	b = (sumArray(Y) - sumArrayMulti(a,X)) / n;

	Xmod = Array.copy(X);
	Ymod = Array.copy(Y);
	
	while(iter < maxIter){	
		if(iter % progre == 0){
			progress = progress+"^";
			print("\\Update:"+progress);
		}
		maxDistance = 0;
		RSS = 0;
		for(i=0;i<n;i++){
			x0 = Xmod[i];
			y0 = Ymod[i];
		
			distance = abs(y0 - (a*x0+b));
			RSS = RSS + distance^2;
			if(distance > maxDistance){
				maxDistance = distance;
				ii = i;	
			}
		}
		//print(iter,"\t",maxDistance,"\t",RSS);
		Xmod = ArrayReject(Xmod,ii);
		Ymod = ArrayReject(Ymod,ii);
		n--;
		
		a = (sumArrayDot(Xmod,Ymod) - sumArray(Xmod) * sumArray(Ymod) / n) / (sumArraySqr(Xmod) - pow(sumArray(Xmod),2) / n);
		b = (sumArray(Ymod) - sumArrayMulti(a,Xmod)) / n;
		iter++;
	}
	optimal_a = a;
	optimal_b = b;
	print ("optimal_a = ",optimal_a);	
	print ("optimal_b = ",optimal_b);

}

//Rotation
angle = atan(optimal_a)*180/PI;

if(optimal_a >=0) modAngle = 0-angle;
if(optimal_a <0) modAngle = 0-angle;

//fill background with GrayColor
setBackgroundColor(203, 212, 220);

run("Rotate... ", "angle=modAngle grid=1 interpolation=Bicubic fill");

//reset background color (black)
setBackgroundColor(0, 0, 0);

//Calculate coordinates after rotation
A = modAngle;
rad = A*PI/180;
cx = (getWidth()-1)/2;
cy = (getHeight()-1)/2;
credit = teketeke + pre1991 + pos1639;

x1 = X[X.length/2];
y1 = a*x1 + b;

x2=round((x1-cx)*cos(rad)-(y1-cy)*sin(rad)+cx);
y2 = round((x1-cx)*sin(rad)+(y1-cy)*cos(rad)+cy);

end_y = y2;


//Detect Left Edge
istep=1;
jstep=2;
X = newArray();
Y = newArray();
TEMP = newArray(1);

for(j=0;j<h/2;j=j+jstep){
	for(i=0;i<xMax-3;i=i+istep){
		v0 = getPixelRGB(i+3,j+h*1/2)-getPixelRGB(i+2,j+h*1/2);
		v1 = getPixelRGB(i+2,j+h*1/2)-getPixelRGB(i+1,j+h*1/2);
		v2 = getPixelRGB(i+1,j+h*1/2)-getPixelRGB(i,j+h*1/2);
		nijiBibun = abs((v0-v1)-(v1-v2));
		if(nijiBibun > spikeThresh) {
			TEMP[0] = i+3;
			X = Array.concat(X,TEMP);
			TEMP[0] = j+h*1/2;
			Y = Array.concat(Y,TEMP);
			i = xMax-3;//break
		}
	}
}
X=bubbleSort(X);
medianX=X[floor(X.length/2)];

start_x = medianX;


//Detect Right Edge
istep=1;
jstep=2;
X = newArray();
Y = newArray();
TEMP = newArray(1);

for(j=0;j<h/2;j=j+jstep){
	for(i=0;i<xMax-3;i=i+istep){
		v0 = getPixelRGB(w-1-i-3,j+h*1/2)-getPixelRGB(w-1-i-2,j+h*1/2);
		v1 = getPixelRGB(w-1-i-2,j+h*1/2)-getPixelRGB(w-1-i-1,j+h*1/2);
		v2 = getPixelRGB(w-1-i-1,j+h*1/2)-getPixelRGB(w-1-i,j+h*1/2);
		nijiBibun = abs((v0-v1)-(v1-v2));
		if(nijiBibun > spikeThresh) {
			TEMP[0] = w-1-i-3;
			X = Array.concat(X,TEMP);
			TEMP[0] = j+h*1/2;
			Y = Array.concat(Y,TEMP);
			i = xMax-3;//break
		}
	}
}
X=bubbleSort(X);
medianX=X[floor(X.length/2)];

end_x = medianX;


//Detect Upper Edge
if(pre1991+pos1639 == "yu3xx") istep=2;
jstep=1;
X = newArray();
Y = newArray();
TEMP = newArray(1);

for(i=0;i<w;i=i+istep){
	for(j=0;j<yMax-3;j=j+jstep){
		v0 = getPixelRGB(i,j+3)-getPixelRGB(i,j+2);
		v1 = getPixelRGB(i,j+2)-getPixelRGB(i,j+1);
		v2 = getPixelRGB(i,j+1)-getPixelRGB(i,j);
		
		nijiBibun = abs((v0-v1)-(v1-v2));
		if(nijiBibun > spikeThresh) {
			TEMP[0] = i;
			X = Array.concat(X,TEMP);
			TEMP[0] = j+3;
			Y = Array.concat(Y,TEMP);
			j = yMax-3;//break
		}
	}
}

Y=bubbleSort(Y);
medianY=Y[floor(Y.length/2)];

start_y = medianY;

//print(start_x,end_x,start_y,end_y);

makeRectangle(start_x, start_y, end_x - start_x, end_y - start_y);
run("Crop");


}//function autoCrop end

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

//-----------------------------------------------------------------------------
function combineAuto(){
	
	progress = "";
	print(progress);
	
	min=512;
	setBackgroundColor(255, 255, 255);
	pxShift = floor(-1*range/2);

	//Size Adjust
	if(flagSizeAdj ==1){
		open( saveDir +"UpperImage.jpg");

		wU = getWidth();
		hU = getHeight();

		open(saveDir +"LowerImage.jpg");

		wL = getWidth();
		hL = getHeight();
	
		if(wU>wL){
			selectWindow("LowerImage.jpg");
			run("Size...", "width=wU constrain average interpolation=Bicubic");
			wL = wU;
		}else if(wU<wL){
			selectWindow("UpperImage.jpg");
			run("Size...", "width=wL constrain average interpolation=Bicubic");
			wU = wL;
		}
		selectWindow("UpperImage.jpg");
		saveAs("JPEG", saveDir +"UpperImage.jpg");
		close();
		selectWindow("LowerImage.jpg");
		saveAs("JPEG", saveDir +"LowerImage.jpg");
		close();
		
	}

	for(i=0;i<range+1;i++){
		progress = progress+"#";
		print("\\Update:"+progress);
		sum=0;

		open( saveDir +"UpperImage.jpg");

		wU = getWidth();
		hU = getHeight();

		open(saveDir +"LowerImage.jpg");

		wL = getWidth();
		hL = getHeight();

		
		//pxShift
		//upper
		selectWindow("UpperImage.jpg");
		if(pxShift > 0) {
			wU2 = wU + pxShift;
			run("Canvas Size...", "width=wU2 height=hU position=Center-Right");
		}
	

		//lower
		selectWindow("LowerImage.jpg");
		if(pxShift < 0) {
			wL2 = wL - pxShift;
			run("Canvas Size...", "width=wL2 height=hL position=Center-Right");
		}
		
	
		run("Combine...", "stack1=UpperImage.jpg stack2=LowerImage.jpg combine");
		
		//check alignment
		for(j=0;j<wU/2;j++){
			vU=getPixelRGB(j+wU/4,hU-1);
			vL=getPixelRGB(j+wU/4,hU);
			sum = sum+abs(vU-vL);
		}
		mean = sum/(wU/2);
		//print("mean =",mean);
		if(mean < min){
			min = mean;
			optimal_i = i;
			//print("min=",min);
		}
		close();
		pxShift++;
	}
	optimal_pxShift = optimal_i - range/2;
	print("optimal_pxShift=",optimal_pxShift);

	//combine
	open( saveDir +"UpperImage.jpg");
	if(optimal_pxShift > 0) {
		wU2 = wU + optimal_pxShift;
		run("Canvas Size...", "width=wU2 height=hU position=Center-Right");
	}
	open( saveDir +"LowerImage.jpg");
	if(optimal_pxShift < 0) {
		wL2 = wL - optimal_pxShift;
		run("Canvas Size...", "width=wL2 height=hL position=Center-Right");
	}
	run("Combine...", "stack1=UpperImage.jpg stack2=LowerImage.jpg combine");
	


}//function combineAuto end

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

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

function sumArray(A){
	sum=0;	
	for(i=0;i<A.length;i++){
		sum = sum +A[i];
	}
	
	return sum;
}

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

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

function sumArrayDot(A,B){
	sum=0;
	for(i=0;i<A.length;i++){
		sum = sum + A[i]*B[i];
	}
	return sum;
}

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

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

function sumArraySqr(A){
	sum=0;
	for(i=0;i<A.length;i++){
		sum = sum + pow(A[i],2);
	}
	return sum;
}

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

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

function sumArrayMulti(a,A){
	for(i=0;i<A.length;i++){
		sum = sum + a * A[i];
	}
	return sum;
}

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

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

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

function ArrayReject(A,i){
	n = A.length;
	B = Array.trim(A,i);
	C = Array.slice(A,i+1,n);
	D = Array.concat(B,C);
	return D;
}

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

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

function getPixelRGB(x,y){
	v = getPixel(x,y);
	red = (v>>16)&0xff;  // right shift 16 bits, and extract lower 8 bits
	green = (v>>8)&0xff; // right shift 8 bits, and extract lower 8 bits
	blue = v&0xff;       // extract lower 8 bits

	valueRGB = (red+green+blue)/3;	//unweighted
	//valueRGB = 0.299*red + 0.587*green + 0.114*blue;	//weighted

	return valueRGB
}

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

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

function bubbleSort(A){
	n=A.length;
	for(i=0;i<n-1;i++){
		for(j=n-1;j>i;j--){
			if(A[j]<A[j-1]){
				tmp=A[j];
				A[j]=A[j-1];
				A[j-1]=tmp;
			}
		}
	}
	return A;
}

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


 

使い方と注意

① スキャン画像はスキャナ側の読み取り範囲「自動認識プラス2mm」程度に設定して、わざと余白を作っておく。

 

② 余白(スキャナ側の裏当て部分。背景)の色はグレーを想定(私はEPSONのDS-530を使用しています)。違う色の場合は、setBackgroundColor(203, 212, 220)の部分を調整してください。

 

③ 読み取った画像は、UPPER用のフォルダとLOWER用フォルダに分けて収納しておく。

 

④ マクロのパラメータ(parametaの部分。主に範囲認識の感度や、回帰方法の選択)を微調整し、マクロ起動。(600dpi読み取りの場合は、とりあえず元の設定でOK)

 

 

 

アルゴリズム

ざっくり言うと、

① 画像の4辺を外側から走査し、二次微分でエッジ検出(まだ点)。

② 結合面において、エッジ検出された点群から回帰直線を推定(LSM,RANSAC,LTSの3方法のいずれかで)。

③ 推定された回帰直線(結合面だけ)に合わせて傾きを補正し、切り抜き

④ スキャンの際に走査方向に画像が伸縮することが予想されるので、幅(Width)が大きい方に合わせてリサイズしてなんちゃって補正

⑤ 上下それぞれ傾き補正+切り抜きした画像を、1pxずつわざと左右にズラしながら(ピクセルシフトしながら)結合していく

⑥ それぞれのピクセルシフトにおいて、結合線の上下ピクセルを差分差分(絶対値)の合計が最も少ない時ピクセルシフトを「ズレなし」と考えて採用。

⑦ 結合した画像をリネームして保存。次の画像へ。

 

 

こんな感じです!

 

 

気になる部分も多々あるのですが、特大サイズをスキャン可能な業務用スキャナを購入するのはキツイので、これで十分かなって感じです。

 

回帰直線はLTSがオススメです。下でちょっとだけ細かく説明します。

 

 

 

ちなみに仕上がりはこんな感じです。(LTS Lightを使用)

 

画像は全て「MARVEL ENCYCLOPEDIA NEW EDITION(小学館集英社プロダクション, 2020)」からです。

 

画像①  真ん中に結合線。唇のちょい下の高さ。

 

 

画像②  真ん中に結合線。悪の道に堕ちた...の行。

 

 

画像③  真ん中に結合線。ピクセルシフトアルゴリズムがしょぼいのと、スキャン方向の伸縮を補正しきれていないせいで、画像のはしっこで少しズレることも。

 

 

回帰直線の推定方法

回帰直線といえば、基本は最小二乗法(LSM, Least squares method)ですが、これだと外れ値(アウトライア)に弱いので、外れ値に強いロバストな推定方法としてRANSACLTSを用意しました。

 

RANSAC (Type2)

Random Sample Consensus

 

ランダム抽出を繰り返して、外れ値に引っ張られない直線を推定する方法です。以下のように実装しています。

 

① 点群からランダムに二点を抽出(ボールを箱に戻さない非復元抽出)する。

② 抽出した二点を通る直線と、その他の点との距離を求め、設定した閾値(distanceThresh)の範囲内におさまるものをインライア(外れ値ではない値)として考える。

③ この「ランダム抽出→インライア数測定」のプロセスを、設定した回数(maxTrial)だけ繰り返し、インライアが最も多くなる直線を正しい回帰直線とみなして推定完了。

④ maxTrialまでいく途中で、インライアの割合がけっこういい感じに高いところ(reliableInlierRatio, デフォは0.8)まできたら、その時点でも回帰直線推定完了とする。

 

 

この方法の特徴は、

メリット...なんか学問的にそれっぽい。

デメリット...時間がかかる。閾値の設定が悩ましい。結果が毎回変わるので、結局のところガチャ。

 

外惻の範囲認識にはじゅうぶん使えるのですが、結合面の精密な推定にはアラが目立つことが多々あるので、イマイチかなと思っています。

 

 

◆ LTS Light (Type3)

Least Trimmed Squares (Light)

 

これは外れ値を恣意的に除外(トリミング)して、インライアだけで推定する方法です。本当のLTSとは方法が違うんですが、計算コストの少ない簡易版って感じで実装しています。

 

① 仮の回帰直線を最小二乗法(LSM)で推定。

② 仮の回帰直線と、エッジ検出された点群との距離を求め、設定した閾値(inlierThresh)の範囲内におさまるものをインライア(外れ値ではない値)として考える。

③ インライアだけでもっかい最小二乗法を用いて回帰直線を求める。

 

この方法の特徴は、

メリット...高速。再現性あり。結果的にもかなりいい感じ。

デメリット...学問的にはそれっぽくない。

 

って感じです。

 

仮モデル推定の際にLSMを使用しているので、外れ値がめちゃめちゃ離れている場合まずいのですが、そもそもxMaxとyMaxでエッジ検出の走査範囲を限定しているのでOKだと思います。

 

というわけで推定方法は、LTSをオススメします。

 

 

 

 

 

おしまい

 

 

 

 

 

 

imagej-jisui.hatenablog.com

 

 

 

 

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

  

imagej-jisui.hatenablog.com

 

 

【ImageJマクロ応用編】#3 カラー画像のピクセル値を取得する方法

 

 



 

 

皆さまこんにちは。

このまえPS2を買いました、yu3xx(ゆーさんちょめちょめ)です。

 

 

 以前のおさらい


まえに投稿した「二次元画像のピクセル値を取得する方法」の記事では、「getPixel(x,y)」を使って「画像のピクセル値を取得する方法」について解説しました。

 

この超入門#8の記事は、「8bitグレースケール画像」を対象にした内容でした。

 

残念なことに、「RGBカラー画像」に対して「getPixel」を使ってしまうと、ヘンテコな値が出てきます。

 

これは「RGBカラー画像」においては、1つのピクセルの中に「赤256階調、緑256階調、青256階調の、3種類の情報」が突っ込まれているからです。

 

 


そこで今回は、「カラー画像のRGBピクセル値をそれぞれ取得する方法」について解説します。

 


もくじ

 

 

結論

 

めんどいので、まず結論だけ言っちゃいます

 

カラー画像のピクセル値を、赤緑青のRGBでそれぞれ数値化したい場合には、以下の自作関数を「getPixel(x,y)」の代わりに使います。

 

※ 座標(下記の例では0,0)は、自分で好きな座標を打ち込んでください。

 


var red,green,blue;

getPixelRGB(0,0);

print(red,green,blue);

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

function getPixelRGB(x,y){
	v = getPixel(x,y);
	red = (v>>16)&0xff;  // right shift 16 bits, and extract lower 8 bits
	green = (v>>8)&0xff; // right shift 8 bits, and extract lower 8 bits
	blue = v&0xff;       // extract lower 8 bits
}

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

 

返したい値が3つあるので、グローバルな変数red, green, blueを用意してから、その中に値を格納します。それぞれ「148, 150, 149」が格納されました。

 

これで終了です。

 

 

適当な説明

 

・getPixelは、1つの値しか返せない。

 

・RGB値 = 148, 150, 149 のピクセルにgetPixelすると、「-7039339」なる値が吐き出される。

 

・7039339(10進数)は、0110 1011 0110 1001 0110 1011(2進数)。

 

・これに2の補数(ビット反転して+1)することで、-7039339(10進数)は、1001 0100 1001 0110 1001 0101(2進数)。

 

・この24bitのうち、頭側8bitがred、まんなか8bitがgreen、お尻側8bitがblue。

 

・このナマの値にbit演算することでRGBを取り出すことができる。

 

・「>>」は右シフト。「v>>16」は、vを2進数表現したものを「右に16bitシフト」させる。

 

「&」はAND。

 

・「0x」はプログラミング界の約束事で「16進数だよ、これ」の表現。

 

・16進数の「ff」は、2進数では「1111 1111」。

 

・「1111 1111」のANDは、「下位の8bit分を取り出す命令」に同じ。

 

・RGBそれぞれのピクセル値をふまえて、1つの値(8bitグレースケール変換した値)を取り出したいなら、(red+green+blue)/3するといい。これはunweighted。

 

・0.299*red + 0.587*green + 0.114*blueすれば、weighted。

 

・RGB画像は32bit。色表現に必要なbitは8*3=24bit。残り8bitは仕事してたり、してなかったり。32はコンピュータの好み。

 

・RGB画像にbitDepth()すると24。でも実際には1pixelあたり32bitらしい(show info)。たぶん先頭8bitは1埋め。

 

 

 

 

 

まとめ

 

 RGB画像のピクセルの中には、3つの色情報が納められている!取り扱いに注意!

 

 

 

 


おしまい

 

 

  

 

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

imagej-jisui.hatenablog.com

 

 

 

 

【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

 

 

 

 

 

【ImageJマクロ応用編】#2 複数の返り値を返すには

 

 

f:id:yu3xx:20211211012619p:plain

 

 

皆さまこんにちは。

yu3xx(ゆーさんちょめちょめ)です。

 

 

 ここまでのおさらい

 以前、「自作関数の作り方」の記事で、「return文」を使って「返り値を関数の外に返す方法」について解説しました。

 

return文の基本ルールは、「返り値は1つだけ」つまり「返り値は変数1つか配列1つだけ」です。

 

このルールのもとでは、

  return x,y;

  return x;

  return y;

などの書き方は無効です。

 


そこで今回は、「複数の返り値を返したい時の方法」について解説します。

 


もくじ

 

 

方法1. 返り値を配列にして、その中に2つの変数

例として、2つの数を引数にして、その2つの数の「足し算結果」と「引き算結果」をそれぞれ出力する関数を使います。

(100と25を引数にした場合、出力は125と75になります。)

 

以下のように、返り値とする配列の中に出力したい変数を格納しておくことで、2つ以上の変数を外に持ち出すことが出来ます。

function TashiHiki(a,b){
	ANS = newArray(2);
	ANS[0] = a + b;
	ANS[1] = a - b;
	return ANS;
}


X = TashiHiki(100,25);

Array.print(X);

f:id:yu3xx:20211211022904p:plain

方法2. グローバルな変数を受け皿として用意

通常、関数内で生まれた変数は、関数が終了すると消えてしまいます

 

そこで「var なにがし」あらかじめ用意しておいたグローバルな変数を受け皿として使うことで、「返り値のように」外に持ち出すことが出来ます。

 

この方法では「return文」は使いません!

var tashi, hiki;

function TashiHiki(a,b){
	tashi = a + b;
	hiki = a - b;
}


TashiHiki(100,25);

print(tashi, hiki);

f:id:yu3xx:20211211022954p:plain

方法3. グローバルな「配列」を受け皿として用意

グローバルな配列を用意して、複数の配列を外に持ち出すことも出来ます。

(この例ではあまり意味をなしませんが...)

var TASHI, HIKI;

function TashiHiki(a,b){
	TASHI = newArray(3);
	HIKI = newArray(3);
	TASHI[0] = a + b;
	HIKI[0] = a - b;
}


TashiHiki(100,25);

Array.print(TASHI);
Array.print(HIKI);

f:id:yu3xx:20211211023055p:plain

方法4.  引数は複数の配列、返り値も複数の配列

3のグローバルな配列を利用すると「引数を複数の配列に、返り値も複数の配列に」することが出来ます。

例として、配列Aと配列Bの「足し算結果」と「引き算結果」を出力します

var TASHI, HIKI;

function TashiHiki(X,Y){
	TASHI = newArray(X.length);
	HIKI = newArray(X.length);
	for(i=0; i<X.length; i++){
		TASHI[i] = X[i] + Y[i];
		HIKI[i] = X[i] - Y[i];
	}
}


A = newArray(100,200,300,400,500);
B = newArray(1,2,3,4,5);

TashiHiki(A,B);

Array.print(TASHI);
Array.print(HIKI);

f:id:yu3xx:20211211023200p:plain

グローバルな受け皿の注意点

グローバルな変数、配列を利用する方法だと「関数を呼び出すたびに結果が上書きされてしまう」という問題があります。

 

これを解消するために、関数を使って結果を出力するたびに「コピーして よけておく」という方法を取ります。

 

以下は配列を「値渡しコピー」して よけておく例です。

(参照渡しコピーと値渡しコピーについてはこちらのリンクから!)

var TASHI, HIKI;

function TashiHiki(X,Y){
	TASHI = newArray(X.length);
	HIKI = newArray(X.length);
	for(i=0; i<X.length; i++){
		TASHI[i] = X[i] + Y[i];
		HIKI[i] = X[i] - Y[i];
	}
}

//trial1
A = newArray(100,200,300,400,500);
B = newArray(1,2,3,4,5);

TashiHiki(A,B);
ANS1 = Array.copy(TASHI);
ANS2 = Array.copy(HIKI);

//trial2
C = newArray(10000,20000,30000,40000,50000);
D = newArray(1000,2000,3000,4000,5000);

TashiHiki(C,D);
ANS3 = Array.copy(TASHI);
ANS4 = Array.copy(HIKI);

//Results
Array.print(ANS1);
Array.print(ANS2);
Array.print(ANS3);
Array.print(ANS4);

f:id:yu3xx:20211211023255p:plain

おまけ

グローバルな配列を受け皿にした関数の例として、「フォルダ内全画像の縦横ピクセル数を、それぞれ配列に格納する」ためのマクロです。

var W,H;

showMessage("Select Open Folder");
openDir = getDirectory("Choose a Directory");
list = getFileList(openDir);

W = newArray(list.length);
H = newArray(list.length);


for(i=0; i<list.length; i++){
	open(openDir + list[i]);
	getWidthHeight(i);	
	close();
}

function getWidthHeight(i){
		w = getWidth();
		h = getHeight();
		W[i] = w;
		H[i] = h;
}

Array.print("W...",W);
Array.print("H...",H);

 

 

 

 

まとめ

 複数の返り値を返すためには

 

  方法① 返し値を配列にして、その中に結果を仕込む

     → 複数の「変数」を外に持ち出せる

  

  方法② return文を使わず、グローバルな受け皿に結果を入れる

    → 複数の「変数」や「配列」を外に持ち出せる 

 

 

 

 


おしまい

 

 

  

 

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

imagej-jisui.hatenablog.com

 

 

 

 

【最低限自炊セット】2021年最新版!これだけはおすすめしたい漫画自炊用マクロ

 

f:id:yu3xx:20211024091701j:plain

 

 

紹介した漫画自炊用マクロも増えてきて、だんだんどれがどれだかわからなくなってきました。

 

この記事では、私が最前線で使用しているおすすめ漫画自炊用マクロを6つだけ厳選して紹介します!

 

 

 

1. 画像処理はこれひとつ!

 

解像度変更、コントラスト変更などの、よく使う画像処理をまとめてやっちゃう!

適切な濃度コントラストも自動設定してくれる!ラクチン!

 

漫画自炊の超基本マクロ!


 

2. スキャン後の自動フォルダ分け!

 

いちいちフォルダ作るのはメンドイ!

 

フォルダ作成から収納までぜんぶオートで!


3. 表紙カバーの切り取り!

 

表紙カバーはちょん切らない!

 

処理でちゃちゃっとクロップ!

 

4. 縦線が出にくくなるスキャン方法!

 

縦線防止の「表向きスキャン+裏向きスキャン統合法」!

 

普通にやると統合が大変だけど、マクロなら簡単に合体!

 

5. ファイル名のリネーム!

 

地味に掲載を忘れてたやつ!

 

ファイル名を間違った時の修正に便利!

 

裏向きスキャンを表向きの番号に直す時にも使える!

( [Reverse Order] にチェック!)

 

//FileRename_andCopy.txt

version = "2.2.3"; print(""); print("FileRename_andCopy"); print("ver",version); //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); //Dialog Dialog.create("Processing setting"); Dialog.addString("newTitle (e.g. BORUTO_11)","BORUTO_11"); Dialog.addNumber("Start Number",1); Dialog.addNumber("digit of newIndex",4); Dialog.addCheckbox("Reverse order",false); Dialog.show; newTitle = Dialog.getString(); startNumber = Dialog.getNumber(); digit = Dialog.getNumber(); order = Dialog.getCheckbox(); //make parentDirectory parentDir=saveDir+"postProc_"+getTimeStamp()+"/"; File.makeDirectory(parentDir); subDir = parentDir+newTitle+"/"; if(!File.exists(subDir)){ File.makeDirectory(subDir); } Table.create("Correspondence Table"); //operation startTime=whatTimeNow(); if(order == false){ for (i=0; i<list.length;i++){ name=list[i]; dotIndex = lastIndexOf(name,"."); ext = substring(name,dotIndex); newname = newTitle+"_"+zeroPad(startNumber,digit)+ext; //Output table Table.set("index",i,i); Table.set("Original",i,name); Table.set("Newname",i,newname); //Progress.the second decimal place by 10000/100 print(i+1,"/",list.length,"...Progress=",floor((i+1)/list.length*10000)/100,"%"); File.copy(openDir+name,subDir+newname); print("Copy to...",subDir+newname); startNumber++; } }else if(order == true){ current = list.length-1; for (i=0; i<list.length;i++){ name=list[current]; dotIndex = lastIndexOf(name,"."); ext = substring(name,dotIndex); newname = newTitle+"_"+zeroPad(startNumber,digit)+ext; //Output table Table.set("index",i,i); Table.set("Original",i,name); Table.set("Newname",i,newname); //Progress.the second decimal place by 10000/100 print(i+1,"/",list.length,"...Progress=",floor((i+1)/list.length*10000)/100,"%"); File.copy(openDir+name,subDir+newname); print("Copy to...",subDir+newname); startNumber++; current--; } } //fin Table.update; finishTime=whatTimeNow(); print("Start Time.... ",startTime); print("FinishTime... ",finishTime); print("oshimai"); //----------------------------------------------------------------------------- //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; } //----------------------------------------------------------------------------- /

 

 

6. やっかいな縦線を補正!

 

これを使えば縦線ノイズはとりあえずなんとかなる! (2022/02/23 追記)


 

 

 

おしまい

 

 

 

 

  

imagej-jisui.hatenablog.com

 

 

 

 

 

【ご質問回答】複数の子フォルダに格納された画像を平均化するためのマクロ

 

こんにちは。yu3xxです。

 

マクロ作成についてご質問をいただきましたので、こちらから回答いたします。

 

 

ご質問内容

親フォルダ内の複数の子フォルダに含まれている画像を、子フォルダごとに平均化するマクロを作りたい。

 

 

作成したマクロの概要

① 「作業したい親フォルダ」と、「平均化後の画像を保存したいフォルダ」を選択

② 親フォルダ内に、子フォルダが何個含まれているかチェック

③ 子フォルダごとに、処理「opeLoop」を繰り返す

④ 「opeLoop」の中身:2次元画像を1次元配列「TEMP」に直してから、配列「GOUKEI」に加算していく。最後に「GOUKEI」を画像枚数で除算して平均化。それを再び2次元画像に直して保存。

⑤ 保存ファイル名は、子フォルダの名前をそのまま使用

 

作成したマクロのルール

① 子フォルダの中にさらにフォルダ(孫フォルダ)があるとエラー

② 画像はグレースケールのみ。8bit,16bit,32bitなんでもOK。カラーは別の処理が必要

③ 画像枚数は何枚でもOK。子フォルダごとに違ってもOK
④ 画素数(タテヨコ)は子フォルダ内の画像が全て揃っていればOK。子フォルダ内が揃っているなら、子フォルダごとに違う画素数でもOK。

 

マクロコード

//Co_Folder_Average.txt

var GOUKEI; //Do something for selected folder showMessage("Select Open Folder"); opDir = getDirectory("Choose a Directory"); print("Processing :",opDir); showMessage("Select Save Folder"); saveDir = getDirectory("Choose a Directory"); print("Save to :",saveDir); listOp = getFileList(opDir); //main operation for(m=0; m<listOp.length; m++){ openDir = opDir + listOp[m]; print("openDir : ",openDir); opeLoop(); } print("oshimai"); //-------------------------------------------------------- //Define opeLoop function opeLoop(){ list = getFileList(openDir); for(i=0; i<list.length; i++){ open(opDir + listOp[m] + list[i]); w = getWidth(); h = getHeight(); if(i == 0){ GOUKEI = newArray(w*h); //Array.print(GOUKEI); } print("ImgNo.",i+1); TEMP = convert1D(); for(k=0; k<w*h; k++){ GOUKEI[k] = GOUKEI[k] + TEMP[k]; } close(); } //Average for(n=0; n<w*h; n++){ GOUKEI[n] = GOUKEI[n] / list.length; } //Create Average Image width = w; height = h; newImage("NewImage", "32-bit black", width, height, 1); selectWindow("NewImage"); for(y=0; y<height; y++){ for(x=0; x<width; x++){ address = x + y * width; newValue = GOUKEI[address]; setPixel(x,y,newValue); } } //Name setting slashIndex = indexOf(listOp[m],"/"); title = substring(listOp[m],0,slashIndex); newname = title + ".tif"; rename(newname); //Save As TIFF saveAs("TIFF", saveDir + newname); print("Save to...",saveDir + newname); close(newname); } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- //Define function convert1D //2 Dimension image to 1 Dimension array function convert1D(){ w = getWidth(); h = getHeight(); array1D = newArray(w*h); for(y=0; y<h; y++){ for(x=0; x<w; x++){ pixelValue = getPixel(x,y); array1D[x+y*w] = pixelValue; } } return array1D; } //-----------------------------------------------------------------------------

 

 ImageJは2次元配列を扱うことが出来ないので、一度1次元配列に直してから処理しています。(画像を短冊状に切って、長〜いテープのように繋げていくイメージ)

 

保存画像はTIFF形式なので、余計な処理が入らずそのまま解析に使えると思います。

 

 

 以上です。ご希望に沿うことは出来たでしょうか?

不明点ありましたら、お気軽にコメントください!