生物屋さんのためのゼロからのプログラミング

―忘れないための覚書 (たま~に更新)―

Pythonで画像を読み込み、名前をつけて保存する。

Pythonの練習第3弾。今回はJavaでJFileChooserで画像を開き、名前を付けて画像を保存する。 - 生物屋さんのためのゼロからのプログラミングの内容に近いことのPython版。

ソースコードは下記。

import tkinter as tk
import tkinter.filedialog as tf
from PIL import Image

#画像ファイルをオープンダイアログで開く
def openImage():
    global img   #saveImage()でも使えるように、imgをグローバル変数にする

    fileType = [("png", "*.png"), ("tiff", "*.tif")] #画像の種類を選択
    iDir = "/Users/Desktop/"
    filePath = tf.askopenfilename(filetypes = fileType, initialdir = iDir)
    img = Image.open(filePath)
    img.show()  #画像の表示

#画像ファイルを保存ダイアログを使って保存する
def saveImage():
    savePath = tf.asksaveasfilename()
    img.save(savePath+".tif")

#コントローラーの作成
def run ():
    root = tk.Tk()
    root.title ("Test")
    root.geometry("100x100")
    #ボタンの設置
    openButton = tk.Button (root, width = 25, text ="Open", command = openImage)
    saveButton = tk.Button (root, width = 25, text = "Save", command = saveImage)
    openButton.pack()
    saveButton.pack()
    root.mainloop()

#プログラムの実行
if __name__ == "__main__":
    run()

前回のPythonでExcelのデータを読み込み、名前をつけて保存する。 - 生物屋さんのためのゼロからのプログラミングとやっていることは、ほとんど同じ。但し、画像を取り扱うために、

from PIL import Image

この部分で“pillow”を呼び出している。
このPILは、なぜかMac OX 10.12以降にしか対応していないので、それ以前のversionではエラーが出て使えなかった。そのため、インストールにかなりの時間が掛かったが、最新のMac OXにupdate。


Javaとの大きな違いは、tifファイルも難なく開けること。以前の記事Javaでtiff画像を開く - 生物屋さんのためのゼロからのプログラミングのように、JavaTIFFファイルを開こうとするとかなり大変だった。

PythonでExcelのデータを読み込み、名前をつけて保存する。

Pythonの練習第2弾。今回は、かなり昔の記事エクセルからデータを読み込み、Peakを検出しエクセルに出力する - 生物屋さんのためのゼロからのプログラミングPythonを使って書いてみた。ただし、今回はエクセルのデータの読み込みと書き出しのみを行った。

ソースコードは下記。

import tkinter
import tkinter.filedialog
import openpyxl
import numpy as np

#エクセルの読み込みと書き込み、そして保存する関数
def openFile(event):
    fileType = [("excel", "*.xlsx")]
    iDir = "/Users/Desktop/"
    filePath = tkinter.filedialog.askopenfilename(filetypes = fileType, initialdir = iDir)
    wb = openpyxl.load_workbook(filePath)
    sheet = wb.active

    #入れ物の作成
    u = np.zeros((sheet.max_column, sheet.max_row))

    #入れ物に数値を入れる
    for i in range(1,sheet.max_column+1):
        for j in range(1, sheet.max_row+1):
            u[i-1][j-1] = sheet.cell(row=j, column=i).value

    #データを入れるためのシートを作る
    wb.create_sheet(index=2, title="Copy Data")
    sheet2 = wb["Copy Data"]
    # シートにデータを貼り付ける
    for i in range(1, sheet.max_column+1):
        for j in range(1, sheet.max_row+1):
            sheet2.cell(row=j, column=i).value = u[i-1][j-1]

    #新しいExcelファイルを名前をつけて保存する
    closePath = tkinter.filedialog.asksaveasfilename()
    wb.save(closePath+".xlsx")

#GUIの作成
root = tkinter.Tk()
root.title("Excel Test")
root.geometry("100x100")
#イベントを起こすためのボタンの作成
button1 = tkinter.Button(text = "Open", width = 50)
button1.bind("<Button-1>", openFile)
button1.pack()
#実行
root.mainloop()

このプログラムを実行すると、オープンダイアログが立ち上がるので、目的のExcelファイルを選択。今回は下記のような単純な数字が並んだものにした。f:id:Aki-Miya:20180606125334p:plain
そして、すぐに保存用のダイアログが立ち上がるので、適当に名前をつけて保存すると、f:id:Aki-Miya:20180606125441p:plainのように、新しいシートにデータを書き込める。



幾つかのコードを見ていく。
まず、Javaでエクセルを取り扱うには、

poi-3.17.jar
poi.examples-3.17.jar
poi.excelant-3.17.jar
poi-ooxml-3.17.jar
poi-ooxml-schemas-3.17.jar
poi-scratchpad-3.17.jar
xmlbeans-2.6.0.jar

上記のjarファイルをEclipseに入れる必要があった(詳細はEclipseでImageJのPlugin作成 -マウスでクリックした場所にOval ROIを描き、Excelファイルにデータを書き出す。- - 生物屋さんのためのゼロからのプログラミング参照)。


一方、Pythonの場合は、まず、ターミナルで、

sudo pip3 install openpyxl

と入力すると、"openpyxl"がインストールできる。
ただし、この時、インストールされた"openpyxl"は「”ライブラリ”⇨”Frameworks”⇨”Python.framework”⇨”Versions”⇨”3.6”⇨”lib”⇨”Python3.6”⇨”site-packages”」に入っているので(僕のMacでは)、”site-packages”の中から出して、一つ上の”Python3.6”に移す必要がある。

そして、

import openpyxl

を書くだけで、PythonExcelが扱えるようになる。
(ただし、Mac Excel 2011とopenpyxlの相性が悪いのか、Mac Excel 2011で作成したExcelファイルは開けなかったが、Mac Excel 2016で作成したExcelファイルは扱えた。理由は不明)


Excelファイルを開く場合、多くのサイトでは、カレントディレクトリに開きたいExcelファイルを移して、Excelファイル名をコードに手入力する方法を紹介している。
しかし、これでは使い勝手が非常に悪いので、オープンダイアログを使って開きたいExcelファイルを選択できるようにした。ソースコードは下記。

    fileType = [("excel", "*.xlsx")]
    iDir = "/Users/Desktop/"
    filePath = tkinter.filedialog.askopenfilename(filetypes = fileType, initialdir = iDir)
    wb = openpyxl.load_workbook(filePath)

実質一行で完結するので、Javaに比べると非常に楽である。


次に、なんらかの数値処理を行うために、Excelのデータを配列に取り込むためのコードが下記。

    #入れ物の作成
    u = np.zeros((sheet.max_column, sheet.max_row))

    #入れ物に数値を入れる
    for i in range(1,sheet.max_column+1):
        for j in range(1, sheet.max_row+1):
            u[i-1][j-1] = sheet.cell(row=j, column=i).value

配列には"numpy"を使用した。"numpy"のインストールには、"openpyxl"と同様に、ターミナルで

 sudo pip3 install numpy

と入力することでインストールでき、"openpyxl"と同様に”Python3.6”に移せば使えるようになる。
まだnumpyの使い方がよく分からなかったため、下記の部分で”作りたい大きさ”の空の入れ物を作った。

    #入れ物の作成
    u = np.zeros((sheet.max_column, sheet.max_row))

Excelのデータを配列に入れる時の注意点として、数の数え方の違いがある。

    #入れ物に数値を入れる
    for i in range(1,sheet.max_column+1):
        for j in range(1, sheet.max_row+1):
            u[i-1][j-1] = sheet.cell(row=j, column=i).value

上記の部分でExcelのデータを配列に入れているのだが、エクセル上では”1”から数え始めるのに対し、配列は”0”から始まるため微調整が必要となる。


Excelにデータを貼る場合も、Pythonは非常に楽であった。コードは下記。

    # シートにデータを貼り付ける
    for i in range(1, sheet.max_column+1):
        for j in range(1, sheet.max_row+1):
            sheet2.cell(row=j, column=i).value = u[i-1][j-1]

Javaの場合は、記入するための"cell"を最初に作る必要があり、これを怠ったり、記入するデータサイズと異なっているとエラーが生じていた。なので、Excel操作に関しては圧倒的にPythonが楽そうである。


最後に、保存ダイアログを使ったExcelファイルの保存方法は下記。

    #新しいExcelファイルを名前をつけて保存する
    closePath = tkinter.filedialog.asksaveasfilename()
    wb.save(closePath+".xlsx")

オープンダイアログと同様、一行で終わるので非常に楽である。



上記のコードをグローバル変数を使った物に少しmodifyしたので、追記。

import tkinter
import tkinter.filedialog
import openpyxl
import numpy as np

#エクセルファイルを開く
def openFile(event):
    global sheet
    global wb
    fileType = [("excel", "*.xlsx")]
    iDir = "/Users/Desktop/"
    filePath = tkinter.filedialog.askopenfilename(filetypes = fileType, initialdir = iDir)
    wb = openpyxl.load_workbook(filePath)
    sheet = wb.active
    getData()

#ExcelファイルのDataを取得
def getData ():
    global u
    global sheet2

    u = np.zeros((sheet.max_column, sheet.max_row))

    for i in range(1, sheet.max_column+1):
        for j in range(1, sheet.max_row+1):
            u[i-1][j-1] = sheet.cell(row = j, column = i).value

#Excelファイルを名前をつけて保存する
def saveFile(event):
    wb.create_sheet(index=1, title="Copy")
    sheet2 = wb["Copy"]
    for i in range(1, sheet.max_column + 1):
        for j in range(1, sheet.max_row + 1):
            sheet2.cell(row=j, column=i).value = (u[i - 1][j - 1]) * 2

    closePath = tkinter.filedialog.asksaveasfilename()
    wb.save(closePath+".xlsx")

#GUIの作成およびボタンの設置
root = tkinter.Tk()
root.title("Test")
root.geometry("100x100")
openButton = tkinter.Button(text = "Open", width = 25)
saveButton = tkinter.Button (text = "Save", width = 25)
openButton.bind("<Button-1>", openFile)
saveButton.bind("<Button-1>", saveFile)
openButton.pack()
saveButton.pack()
root.mainloop()

関数間で変数を共有できるように、global文を使った。
具体的には、下記の部分などである。

    global u
    global sheet2

こうすることで、関数をまたいで変数を使える。
このコードを実行すると、f:id:Aki-Miya:20180606154157p:plainのように2つのボタンがついたGUIが立ち上がり、OpenボタンでExcelファイルを呼び出し、Saveボタンで名前を付けて保存できるようにした。

余談:Pythonで分数の計算 (約分付き)

バイオインフォマティックス屋さんがすなるPythonといふものを、生物屋さんもしてみむとてするなり。」と言うわけではないが、機械学習をやってみたいので、Pythonも勉強することにした。

とりあえず、Pythonの練習材料として、今までJavaで書いていたものをPythonで書き直してみることに。

今回は、余談:Javaで分数の計算(約分つき) - 生物屋さんのためのゼロからのプログラミングを取り扱う。

Pythonで書き直したコードが下記。

import tkinter

#約分
def Yaku(event):

    #入力された数値の取得
    s1Val = int(bs1.get())
    b1Val = int(bb1.get())
    s2val = int(bs2.get())
    b2Val = int(bb2.get())

    #分母と分子の単純な計算
    sVal = s1Val * b2Val + b1Val * s2val
    bVal = b1Val * b2Val

    #上記の計算の小さい方をminValにする
    minVal = min (sVal, bVal)

    #約分の主要部分
    for i in range(1, minVal):
        if (sVal % i == 0) and (bVal % i == 0):
            sVal = sVal/i
            bVal = bVal/i
            i -= 1

    #計算結果を表記
    sLabel = tkinter.Label(text = int(sVal))
    bLabel = tkinter.Label(text = int(bVal))
    sLabel.place(x = 140, y = 50)
    bLabel.place(x = 140, y = 80)

#GUIの作成
root = tkinter.Tk()
root.title("Yakubun Test")
root.geometry("300x200")

bs1 = tkinter.Entry(width = 3)
bb1 = tkinter.Entry(width = 3)
bs2 = tkinter.Entry(width = 3)
bb2 = tkinter.Entry(width = 3)
bs1.place(x = 20, y = 50)
bb1.place(x = 20, y = 80)
bs2.place(x = 80, y = 50)
bb2.place(x= 80, y = 80)

pLabel = tkinter.Label(text = "+")
eLabel = tkinter.Label(text = "=")
pLabel.place(x = 64, y = 70)
eLabel.place(x = 120, y = 70)

#ボタンの作成およびイベントの設定
button1 = tkinter.Button(text = "Done", width = 10)
button1.bind("<Button-1>", Yaku)
button1.place(x = 30, y = 120)

root.mainloop()

このコードの実行した結果が下記である。
f:id:Aki-Miya:20180605095516p:plain
この分子と分母に整数を入れて、Doneを押すと
f:id:Aki-Miya:20180605095557p:plain
この様に、答えを出してくれる。


少しPythonのコードを見ていく。

#GUIの作成
root = tkinter.Tk()
root.title("Yakubun Test")
root.geometry("300x200")

上記の部分で、JavaのJFrameに相当するものを作成している。

root.mainloop()

そして、上の部分がものすごく大雑把にいうと、下記のJavaのmain部分に相当する

public static void main (String[] args) {}

JavaのJTextFieldに相当するのが、

.Entry

の部分。

ボタンにイベントを結びつけるのが、

button1.bind("<Button-1>", Yaku)

の部分である。


分数の計算の主たる部分の内容は前回の記事余談:Javaで分数の計算(約分つき) - 生物屋さんのためのゼロからのプログラミングを参照。

分数の計算の主たる部分には大して違いはないが、GUIの部分に関してはPythonの方が圧倒的に文字数が少なく済むみたいだ。

EclipseでImageJのPlugin作成 -2次元拡散方程式シミュレーション-

前回は、モンテカルロ法による分子拡散のシミュレーションを書いたので
EclipseでImageJのPlugin作成 -拡散シミュレーション(ランダムウォーク)- - 生物屋さんのためのゼロからのプログラミング、今回は、2次元拡散方程式のシミュレーションを行ってみた。
(但し、普通の2次元拡散方程式のシミュレーションではなく、顕微鏡で観察した場合を想定したシミュレーションになっている。)

(注:メモリを非常に使うため、"java.lang.OutOfMemoryError: Java heap space"のエラーが出やすいので、Time等は短くしないとシミュレーションできない)

コードは下記。

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextField;

import ij.IJ;
import ij.ImagePlus;
import ij.plugin.PlugIn;
import ij.process.ImageProcessor;

public class Fick_Diffusion implements PlugIn, ActionListener {

	ImagePlus imp0, imp;
	ImageProcessor ip;
	JFrame frame;
	JTextField dcField; //拡散係数
	JTextField cellField; //カメラの画素サイズ
	JTextField objField; //対物レンスの倍率
	JTextField rateField; //撮影間隔
	JTextField timeField; //撮影時間
	int cellSize;
	int obj;
	int rate;
	int localNumber;
	int number;
	int size;
	float dt = 0.0005f; //0.5msec
	float dc;
	float pixelSize;
	float speed;
	float rawData [][][];
	float data [][][];

////////////////////////////////////////////////
	//キャンバスの作成
	public void makeCanvas () {

		//リセット
		if (data != null) data = null;
		if (imp != null) imp.close();

		//数値の取得
		getValue();

		//キャンバスの元画像の取得と処理
		imp0 = IJ.openImage("適当な画像ファイル");
		size = imp0.getWidth();

		//stack画像にする
		for (int i = 0; i < number; i++) {
			IJ.run(imp0, "Add Slice", "");
		}
		IJ.run(imp0, "Delete Slice", "");
		imp = imp0.duplicate();
		imp0.close();
		ip = imp.getProcessor();

		//データ類を入れる容器の作成
		makeContainer();
	}

////////////////////////////////////////////////
	//JTextFieldからの数値の取得
	public void getValue() {
		dc = Float.parseFloat(dcField.getText());
		cellSize = Integer.parseInt(cellField.getText());
		obj = Integer.parseInt(objField.getText());
		rate = Integer.parseInt(rateField.getText());
		number = Integer.parseInt(timeField.getText());
	}

////////////////////////////////////////////////
	//容器の作成
	public void makeContainer () {

		//リセット
		if (rawData != null) rawData = null;
		if (data != null) data = null;

		//初期化
		data = new float [number][size][size];
		localNumber = number*rate*2;
		rawData = new float [localNumber][size][size];

		//初期データの入力 (中心部以外は"0"に)
		for (int i = 0; i < localNumber; i++) {
			for (int j = 0; j < size; j++) {
				for (int k = 0; k < size; k++) {
					rawData[i][j][k] = 0.0f;
				}
			}
		}
		//中心部に値の導入
		rawData [0][size/2][size/2] = 10000.0f;
		rawData [0][size/2-1][size/2-1] = 10000.0f;
		rawData [0][size/2-1][size/2] = 10000.0f;
		rawData [0][size/2-1][size/2+1] = 10000.0f;
		rawData [0][size/2][size/2-1] = 10000.0f;
		rawData [0][size/2][size/2-1] = 10000.0f;
		rawData [0][size/2+1][size/2-1] = 10000.0f;
		rawData [0][size/2+1][size/2] = 10000.0f;
		rawData [0][size/2+1][size/2+1] = 10000.0f;
	}
////////////////////////////////////////////////
	//各種計算
	public void calc () {

		//pixelサイズの計算
		pixelSize = cellSize*1.0f/obj;

		//伝播速度の計算
		speed = dc*dt;
	}
////////////////////////////////////////////////
	//シミュレーションの計算
	public void makeSimulate () {

		//各種計算
		calc();

		//シミュレーションの計算
		for (int i = 1; i < localNumber ; i++) {
			for (int j = 1; j < size - 2; j++) {
				for (int k = 1; k < size - 2; k++) {

					rawData [i][j][k] = rawData[i-1][j][k] +
							speed*(rawData[i-1][j-1][k] + rawData[i-1][j][k-1]+rawData[i-1][j][k+1] + rawData[i-1][j+1][k]
									- 4*rawData[i-1][j][k])/(pixelSize*pixelSize);

				}
			}
		}

		//シミュレーション結果の記入 (イメージの撮影間隔を反映させる)
		for (int i = 0; i < number; i++) {
			for (int j = 0; j < size; j++) {
				for (int k = 0; k < size; k++) {
					data[i][j][k] = rawData[i*rate*2][j][k];
				}
			}
		}
	}
////////////////////////////////////////////////
	//結果の表示
	public void display() {

		//シミュレーション画像の作成
		for (int i = 0; i < number; i++) {
			imp.setSlice(i+1);
			for (int j = 0; j < size; j++) {
				for (int k = 0; k < size; k++) {
					ip.putPixelValue(j, k, data[i][j][k]);
				}
			}
		}

		//結果を見やすくする
		IJ.run(imp, "Fire", "");
		IJ.run("Brightness/Contrast...");
		IJ.run(imp, "Enhance Contrast", "saturated=0.35");

		//TimeとScale Barの表示
		ip.setColor(Color.red);
		Font font = new Font("Arial", Font.PLAIN, 20);
		ip.setFont(font);
		for (int i = 1; i < number + 1; i++) {
			imp.setSlice(i);
			int localTime = (int)Math.round((i-1)*rate);
			if (localTime/1000 == 0) {
				ip.drawString (localTime + " msec", 5, size);
			} else if (localTime/1000 > 0) {
				ip.drawString(localTime/1000 + " sec " + localTime %1000 + " msec", 5, size);
			}
			ip.drawString("20μm", (int)(size - 20/pixelSize), 25);
			ip.drawLine((int)(size - 20/pixelSize - 10), 25, size -10, 25);
		}
		imp.show();
	}
////////////////////////////////////////////////
	//Close
	public void closeAll() {
		imp.close();
		frame.dispose();
	}

////////////////////////////////////////////////
	//コントローラーの作成
	public void run (String arg) {

		Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
		int sw = screenSize.width;

		frame  = new JFrame ("Fick Diffution");
		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		frame.setBounds (sw - 320, 50, 300, 320);
		JLabel dcLabel = new JLabel ("<html>DC (μm<sup>2</sup>/s) = ");
		JLabel cellLabel = new JLabel ("Cell Size (μm) =");
		JLabel objLabel = new JLabel("Objective lens =");
		JLabel rateLabel = new JLabel ("Frame rate (ms) = ");
		JLabel timeLabel = new JLabel ("Time (frame) =");
		JButton doneButton = new JButton ("Done");
		JButton closeButton = new JButton ("Close");
		dcField = new JTextField("20");
		cellField = new JTextField ("16");
		objField = new JTextField("60");
		rateField = new JTextField ("30");
		timeField = new JTextField("100");
		doneButton.addActionListener(this);
		closeButton.addActionListener(this);
		doneButton.setActionCommand("Done");
		closeButton.setActionCommand("Close");
		dcLabel.setBounds(95, 10, 100, 30);
		cellLabel.setBounds(80, 50, 100, 30);
		objLabel.setBounds(75, 90, 150, 30);
		rateLabel.setBounds(70, 130, 150, 30);
		timeLabel.setBounds(90, 170, 100, 30);
		dcField.setBounds(185, 12, 50, 30);
		cellField.setBounds(185, 50, 50, 30);
		objField.setBounds(185, 90, 50, 30);
		rateField.setBounds (185, 130, 50, 30);
		timeField.setBounds(185, 170, 50, 30);
		doneButton.setBounds(110, 210, 80, 30);
		closeButton.setBounds(110, 240, 80, 30);

		JPanel pane = new JPanel ();
		pane.setLayout(null);
		pane.add(dcLabel);
		pane.add(rateLabel);
		pane.add(cellLabel);
		pane.add(objLabel);
		pane.add(dcField);
		pane.add(rateField);
		pane.add(cellField);
		pane.add(objField);
		pane.add(timeLabel);
		pane.add(timeField);
		pane.add(doneButton);
		pane.add(closeButton);

		frame.getContentPane().add(pane, BorderLayout.CENTER);
		frame.setVisible(true);
	}

////////////////////////////////////////////////
	//ボタンを押した時の反応
	public void actionPerformed(ActionEvent e) {
		String cmd =  e.getActionCommand();
		if (cmd.equals("Done")) {
			makeCanvas();
			makeSimulate();
                        display();
		} else if (cmd.equals("Close")) {
			closeAll();
		}
	}
}

このコードを実行すると、下記の様なコントローラーが立ち上がり、f:id:Aki-Miya:20180221121117p:plain
Doneボタンを押すと、設定した条件での2次元拡散方程式のシミュレーションを行い、下の様に結果をstack画像として出力してくれる。f:id:Aki-Miya:20180221121128p:plain
(これはビデオレートで観察した場合に見られる(はず)のCa^{2+}の拡散の一コマである。)


少しコードの中身を見ていく。
(シミュレーション用のキャンバスの作成等の説明は
EclipseでImageJのPlugin作成 -拡散シミュレーション(ランダムウォーク)- - 生物屋さんのためのゼロからのプログラミングを参照)


このシミュレーションは顕微鏡での観察結果を想定しているので、コントローラー内の

DC = 
Cell Size = 
Objective lens =
Frame rate =
Time (frame) =

はイメージング条件の設定項目である。
"DC"は見たい分子の拡散係数(ここでは細胞内でのCa^{2+}の拡散係数を使用)、"Cell Size"はカメラの画素サイズ、"Objective lens"は対物レンズの倍率、"Frame rate"は画像の撮像間隔、"Time"はイメージング時間を意味する。つまり、上の設定条件で「画素サイズ 16μm x 16μmのセンサーを持つカメラと60x対物レンズを使って、拡散係数が"Dc"である分子の動きを exposure time = 30 msecで100 frame分、Stream Acquisitionした」ことを意味する。


2次元拡散方程式は、

 u(x, y, t+Δt) = u(x, y, t) + DcΔt((\displaystyle \frac{u(x+Δx, y, t) - 2u(x, y, t) + u(x-Δx, y, t}{(Δx)^{2}}) + (\displaystyle \frac{u(x, y+Δy, t) - 2u(x, y, t) + u(x, y-Δy, t}{(Δy)^{2}})

ここで、u(x, y, t)は時間tでの(x, y)座標でのCa^{2+}濃度、"Dc"は拡散係数、"Δt"はData間の時間間隔を示し、"Δx"と"Δy"は隣のピクセルとの距離を示すので、ここでは"Δx = Δy = pixelSize"とした。

また、拡散方程式の安定性条件は、

 \displaystyle \frac{DcΔt}{Δx^{2}}\leq\displaystyle \frac{1}{2}

なので、16μm x 16μmのセンサーを持つカメラと60x対物レンズを使った場合に、この条件を満たす様に"Δt (コード上では"dt") = 0.0005" (0.5 msec)とした。(この場合の、上記の値は"0.14"である。)

そして、これらを踏まえて、

//pixelサイズの計算
pixelSize = cellSize*1.0f/obj;

//伝播速度の計算
speed = dc*dt;

上記の部分で、ピクセルサイズ及び、拡散速度を算出し、

//シミュレーションの計算
	for (int i = 1; i < localNumber ; i++) {
		for (int j = 1; j < size - 2; j++) {
			for (int k = 1; k < size - 2; k++) {
				rawData [i][j][k] = rawData[i-1][j][k] +
					speed*(rawData[i-1][j-1][k] + rawData[i-1][j][k-1]+rawData[i-1][j][k+1] + rawData[i-1][j+1][k]- 4*rawData[i-1][j][k])/(pixelSize*pixelSize);

			}
		}
	}

上記の部分でシミュレーションを行っている。
ここで得られた rawDataは、"0.5msec"毎の数値変化を収納していることになる。
("dt=0.5msec"なので)

そのため、

//シミュレーション結果の記入 (イメージの撮影間隔を反映させる)
for (int i = 0; i < number; i++) {
	for (int j = 0; j < size; j++) {
		for (int k = 0; k < size; k++) {
			data[i][j][k] = rawData[i*rate*2][j][k];
		}
	}
}

この部分で、コントローラー上のFrame rate (ms) を反映したデータを作っている。
(注:"i*rate*2"の部分で時間の補正をした)


上で示したシミュレーション結果の画像はビデオレートのものだが、Frame rateを"100"に設定すると、
f:id:Aki-Miya:20180221121139p:plain
この様に、Frame間隔が長い場合のシミュレーションもすることが可能である。



また、初期値の入力時に、下記の様に

//初期データの入力
Random random = new Random();
		
for (int i = 0; i < localNumber; i++) {
	for (int j = 0; j < size; j++) {
		for (int k = 0; k < size; k++) {
			if (i < 50 && random.nextInt(2500) == 0) {  
				rawData[i][j][k] = 10000.0f;
			} else {
				rawData[i][j][k] = 0;
			}
		}
	}
}

Random メソッドを使って、値が入る座標やframeをランダムになる様にすれば、
f:id:Aki-Miya:20180222092530p:plain
上の様に、色々な場所から様々なタイミングで拡散が始まる様子を観察できる。

EclipseでImageJのPlugin作成 -Time Stamper-

ImageJ(Fiji)には、"Time Stamper"というプラグインが内臓されているが、この"Time Stamper"を実行すると、

IJ.run(imp, "Time Stamper", "starting=0 interval=1
   x=2 y=15 font=12 decimal=0 anti-aliased or=sec");

のようになり、Javaなどで作成するプラグイン内で、intervalを任意の値にするのは不可能であった(僕がざっくり調べたところでは)。(intervalを任意の値にするには、わざわざ"Time Stamper"を使う必要があり、少し効率が悪い)


そこで、今回は"Time Stamper"を使わずに、stack画像にTimeをstampする方法を書く。

コードは下記。

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextField;

import ij.IJ;
import ij.ImagePlus;
import ij.io.OpenDialog;
import ij.plugin.PlugIn;
import ij.process.ImageProcessor;

public class Time_Stamp implements PlugIn, ActionListener{

	ImagePlus imp;
	ImageProcessor ip;
	JTextField tf; //Frame rateの入力
	int number; //Frame数
	int h; ///画像の高さ
//////////////////////////////////////////////////
	//画像を開く
	public void openFile () {
		//オープンダイアログで画像を開く
		OpenDialog od = new OpenDialog("Open");
		imp = IJ.openImage(od.getDirectory()+od.getFileName());
		ip = imp.getProcessor();
		imp.show();

		//画像のスライス数と高さの取得
		number = imp.getNSlices();
		h = imp.getHeight();
	}
//////////////////////////////////////////////////
	//時間をスタンプする
	public void stamp() {
		//フォントなどの設定
		ip.setColor(Color.white);
		Font font = new Font("Arial", Font.PLAIN, 20);
		ip.setFont(font);

		//Frame rateの取得
		int time = getValue();

		//Timeをスタンプする
		for (int i = 0; i < number + 1; i++) {
			imp.setSlice(i);
			ip.drawString(String.valueOf(i*time), 10, h-10);
		}
	}
//////////////////////////////////////////////////
	//Frame rateの取得
	public int getValue() {
		int time = Integer.parseInt(tf.getText());
		return time;
	}

//////////////////////////////////////////////////
	//コントローラーの作成
	public void run (String arg) {
		JFrame frame = new JFrame ("Stamp");
		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		frame.setBounds(10, 10, 160, 150);

		JButton openButton = new JButton ("Open");
		JButton stampButton = new JButton("Stamp");
		openButton.addActionListener(this);
		stampButton.addActionListener(this);
		openButton.setActionCommand("Open");
		stampButton.setActionCommand("Stamp");
		JLabel label = new JLabel ("Frame rate =");
		tf = new JTextField ("10");
		openButton.setBounds (35, 10, 90, 30);
		stampButton.setBounds(35, 50, 90, 30);
		label.setBounds(20, 90, 100, 30);
		tf.setBounds(100, 90, 40, 30);
		JPanel pane = new JPanel ();
		pane.setLayout(null);
		pane.add(openButton);
		pane.add(stampButton);
		pane.add(label);
		pane.add(tf);
		frame.getContentPane().add(pane, BorderLayout.CENTER);
		frame.setVisible(true);
	}

//////////////////////////////////////////////////
	//ボタンを押した時の反応
	public void actionPerformed(ActionEvent e) {
		String cmd = e.getActionCommand();
		if (cmd.equals("Open")) {
			openFile();
		} else if (cmd.equals("Stamp")) {
			stamp();
		}
	}
}

このコードを実行すると、下記のコントローラーが立ち上がる。
f:id:Aki-Miya:20180216125028p:plain
Openボタンを押して、適当なstack画像を選択し開く。(ここでは、前回同様に三浦耕太先生著のImageJではじめる生物画像解析 | 学研メディカル秀潤社の第5章−1 (p197)のサンプル画像"ER-flow.tif"を使用した)
f:id:Aki-Miya:20180216125036p:plain
そして、Stampボタンを押すと、下のようにstack画像にTimeがstampされる。
f:id:Aki-Miya:20180216125046p:plain



キーとなるのは、画像に数値を書き込む下記のコード。

//Timeをスタンプする
	for (int i = 0; i < number + 1; i++) {
		imp.setSlice(i);
		ip.drawString(String.valueOf(i*time), 10, h-10);
	}


意外と簡単に画像に記入できたので、プラグインの作成時に気軽に使えそう。

EclipseでImageJのPlugin作成 -stack画像にROIを描き、その数値データをグラフ化する-

今回は、数値データをグラフ化するプラグインを書いた。

コードは下記。

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.geom.Line2D;

import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.event.MouseInputAdapter;

import ij.IJ;
import ij.ImagePlus;
import ij.io.OpenDialog;
import ij.measure.ResultsTable;
import ij.plugin.PlugIn;
import ij.plugin.frame.RoiManager;

public class Graph_Plot implements PlugIn, ActionListener {

	ImagePlus imp; //解析する画像
	Graphics2D g2;
	MyCanvas mycan;
	RoiManager rm;
	JFrame frame;
	JButton openButton;
	JButton doneButton;
	JLabel xLabel;
	JLabel yLabel;
	JLabel x; //グラフ上でのデータの位置の表記用
	JLabel val; //データの表記用
	int number; //スライスの枚数
	double time []; //時間の入れ物
	double data []; //データの入れ物
	double modiTime []; //補正後の時間の入れ物
	double modiData[]; //補正後のデータの入れ物

///////////////////////////////////////////////////
	//ファイルを開く
	public void openFile() {

		//画像の二重オープンを防ぐ
		openButton.setEnabled(false);

		//オープンダイアログで画像を開く
		OpenDialog od = new OpenDialog("Open");
		String path = od.getDirectory() + od.getFileName();
		imp = IJ.openImage(path);
		number = imp.getNSlices();
		imp.show();

		//Dataの入れ物の作成
		data = new double[number];
		time = new double [number];
		modiData = new double [number];
		modiTime = new double [number];

		//グラフを作れるようにする
		doneButton.setEnabled(true);
	}

///////////////////////////////////////////////////
	//数値の入手
	public void getValue() {

		//RoiManagerのリセット
		if (rm != null) rm.close();

		//数値データの取得
		rm = new RoiManager();
		IJ.run("Set Measurements...", "area mean redirect=None decimal=3");
		rm.addRoi(imp.getRoi());
		ResultsTable rt = rm.multiMeasure(imp);
		for (int i = 0; i < number; i++) {
			time [i] = i;
			data[i] = rt.getValue(1, i);
		}
		rt = null;
	}
///////////////////////////////////////////////////
	//グラフに合わせるためにデータを拡大・縮小する
	public void modify () {
		//Min & Maxの検出
		double min = data[0];
		double max = data[0];
		for (int i = 0; i < number; i++) {
			if (data[i] < min) min = data[i];
			if (data[i] > max) max = data[i];
		}
		double minValue = min;
		double maxValue = max;

		//グラフの拡大・縮小
		double zx = 400.0/number;
		double zy = 300.0/(maxValue - minValue);

		for (int i = 0; i < number ; i++) {
			modiTime[i] = zx*time[i];
			modiData[i] = 300 - ((data[i] - minValue)*zy);
		}
	}

///////////////////////////////////////////////////
	//グラフの作成
	public void makeLine() {
		JFrame drawFrame = new JFrame ("Draw");
		drawFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		drawFrame.setBounds(0, 0, 460, 360);
		mycan = new MyCanvas();
		drawFrame.getContentPane().add(mycan);
		mycan.addMouseListener (new MouseCheck());
		mycan.addMouseMotionListener(new MouseCheck());
		drawFrame.setVisible(true);
	}

///////////////////////////////////////////////////
	public class MyCanvas extends JComponent {
		public void paintComponent(Graphics g) {
			g2 = (Graphics2D)g;
			g2.draw(new Line2D.Double(50, 320, 460, 320));
			g2.draw(new Line2D.Double(50, 10, 50, 320));
			for (int i = 0; i < number - 1; i++) {
				g2.draw(new Line2D.Double(50 + modiTime[i], modiData[i] + 10, 50 + modiTime[i+1],  modiData[i+1] + 10));
			}
		}
	}
///////////////////////////////////////////////////
	//輝度値の表示
	class MouseCheck extends MouseInputAdapter {
		public void mouseMoved(MouseEvent e) {
			int x0 = e.getX();

			//JFrame上での座標をDataの位置になおす
			int xtrue = (x0 - 50) * number/400;

			//グラフ上でのみ数値を表記する
			if (x0 >= 50 &&  x0 < 400) {
				x.setText(String.valueOf(xtrue));
				val.setText(String.valueOf(data[xtrue]));
			} else {
				x.setText("");
				val.setText("");
			}
		}
	}

///////////////////////////////////////////////////
	//コントローラーの作成
	public void run (String arg) {
		Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
		int sw = screenSize.width;
		frame = new JFrame("Graph");
		frame.setBounds(sw-200, 10, 160, 200);

		openButton = new JButton ("Open");
		doneButton = new JButton ("Done");
		xLabel = new JLabel ("x = ");
		x = new JLabel ("");
		yLabel = new JLabel ("val = ");
		val = new JLabel ("");
		openButton.addActionListener(this);
		doneButton.addActionListener(this);
		openButton.setActionCommand("Open");
		doneButton.setActionCommand("Done");
		openButton.setBounds(35, 10 , 90, 30);
		doneButton.setBounds(35, 50, 90, 30);
		xLabel.setBounds(35, 90, 50, 30);
		x.setBounds(60, 90, 60, 30);
		yLabel.setBounds(25, 130, 50, 30);
		val.setBounds(60, 130, 90, 30);

		JPanel pane = new JPanel();
		pane.setLayout(null);
		pane.add(openButton);
		pane.add(doneButton);
		pane.add(xLabel);
		pane.add(x);
		pane.add(val);
		pane.add(yLabel);
		doneButton.setEnabled(false);
		frame.getContentPane().add(pane, BorderLayout.CENTER);
		frame.setVisible(true);
	}

///////////////////////////////////////////////////
	//ボタンを押した時の反応
	public void actionPerformed(ActionEvent e) {
		String cmd = e.getActionCommand();
		if (cmd.equals("Open")) {
			openFile ();
		} else if (cmd.equals("Done")) {
			getValue();
			modify();
			makeLine();
		}
	}
}

このプラグインを実行すると、下記のコントローラーが立ち上がり、
f:id:Aki-Miya:20180216084611p:plain
Openボタンで画像を開き(今回も)、下記のようにROIを置いて Doneボタンを押すと、
f:id:Aki-Miya:20180216084621p:plain
stack画像をMultiMeasureして、その結果を下記のようなグラフとして表示する。
f:id:Aki-Miya:20180216084635p:plain
このグラフ上をマウスでなぞると、コントローラーの下の部分に、データの数値が表示される。
f:id:Aki-Miya:20180216084657p:plain
(テストサンプルとして、stack画像には以前の記事EclipseでImageJのPlugin作成 -マウスでクリックした場所にOval ROIを描き、Excelファイルにデータを書き出す。- - 生物屋さんのためのゼロからのプログラミングと同様に、三浦耕太先生著のImageJではじめる生物画像解析 | 学研メディカル秀潤社の第5章−1 (p197)のサンプル画像"ER-flow.tif"を使用した。)


いくつかコードを見ていく。
PC上では左上が(0, 0)の座標となっているが、我々がグラフを描くときには左下を(0, 0)の座標とすることが多い。
そのため、下記の部分でデータをグラフ化するために補正を行っている。
(ここでの、"300"や"400"はグラフのサイズ)

//グラフに合わせるためにデータを拡大・縮小する
	public void modify () {
		//Min & Maxの検出
		double min = data[0];
		double max = data[0];
		for (int i = 0; i < number; i++) {
			if (data[i] < min) min = data[i];
			if (data[i] > max) max = data[i];
		}
		double minValue = min;
		double maxValue = max;

		//グラフの拡大・縮小
		double zx = 400.0/number;
		double zy = 300.0/(maxValue - minValue);

		for (int i = 0; i < number ; i++) {
			modiTime[i] = zx*time[i];
			modiData[i] = 300 - ((data[i] - minValue)*zy);
		}
	}

そして、拡大・縮小するためにDataの数値をDoubleに変更したため、Doubleのデータを使って線を描くために、下記の部分でGraphics2DのLine2D.Doubleを使用している。

public class MyCanvas extends JComponent {
	public void paintComponent(Graphics g) {
		g2 = (Graphics2D)g;
		g2.draw(new Line2D.Double(50, 320, 460, 320));
		g2.draw(new Line2D.Double(50, 10, 50, 320));
		for (int i = 0; i < number - 1; i++) {
			g2.draw(new Line2D.Double(50 + modiTime[i], modiData[i] + 10, 50 + modiTime[i+1],  modiData[i+1] + 10));
		}
	}
}

また、グラフ上での数値データを読み込むために、下記でMouseInputAdapterを使用している。

	//輝度値の表示
	class MouseCheck extends MouseInputAdapter {
		public void mouseMoved(MouseEvent e) {
			int x0 = e.getX();

			//JFrame上での座標をDataの位置になおす
			int xtrue = (x0 - 50) * number/400;

			//グラフ上でのみ数値を表記する
			if (x0 >= 50 &&  x0 < 400) {
				x.setText(String.valueOf(xtrue));
				val.setText(String.valueOf(data[xtrue]));
			} else {
				x.setText("");
				val.setText("");
			}
		}
	}


とりあえず、数値データをグラフ化できる方法が分かったので、他のプラグイン開発に応用できそう。

EclipseでImageJのPlugin作成 -拡散シミュレーション(ランダムウォーク)-

今回は粒子の拡散シミュレーションを行うImageJのPluginを書いてみた。

コードは下記。

import java.awt.BorderLayout;
import java.awt.FileDialog;
import java.awt.Frame;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;

import ij.IJ;
import ij.ImagePlus;
import ij.plugin.PlugIn;
import ij.plugin.ZProjector;
import ij.process.ImageProcessor;

public class Simulator_ implements PlugIn, ActionListener{

	ImagePlus imp, stackImp;
	ImageProcessor ip;
	int h, w;
	int sliceN = 500; //シミュレーションする時間 (フレーム数)
	int number = 1000; //粒子の数

	//シミュレーションするためのキャンバスの作成
	public void makeCanvas () {
		imp = IJ.openImage("何かしらの画像ファイルのディレクトリと名前");
		imp.setTitle("Simulation");
		w = imp.getWidth();
		h = imp.getHeight();
		imp.setRoi(0, 0, w, h);
		IJ.setBackgroundColor(0, 0, 0);
		IJ.run(imp, "Clear", "slice");
		imp.killRoi();

		//シミュレーションするためにstack画像にする
		for (int i = 0; i < sliceN; i++) {
			IJ.run(imp, "Add Slice", "");
		}
		ip = imp.getProcessor();
	}

	//拡散のシミュレーション
	public void simulate () {

		//粒子を作る
		int x[] = new int [number];
		int y [] = new int [number];

		//全ての粒子を中心に置く
		for (int i = 0; i < number; i++) {
			x[i] = w/2;
			y[i] = h/2;
		}
		imp.setSlice(1);
		ip.putPixelValue(w/2, h/2, 65535);

		//粒子の動きを描く
		for (int i = 2; i < sliceN; i++) {
			imp.setSlice(i);
			for (int j = 1; j < number; j++) {
				double th = 2.0*Math.PI*Math.random();
				x[j] = x[j] + (int)(Math.cos(th)*10);
				y[j] = y[j] + (int)(Math.sin(th)*10);
				ip.putPixelValue(x[j], y[j], 65535 - 10*j);
			}
		}

		//シミュレーション結果を見やすくする
		IJ.run(imp, "Fire", "");
		IJ.run("Brightness/Contrast...");
		IJ.setMinAndMax(imp, 55000, 65535);
		imp.show();
	}

	//シミュレーションのオーバーレイ画像を作る
	public void zStack () {
		stackImp = ZProjector.run(imp, "max");
		IJ.setMinAndMax(stackImp, 55000, 65535);
		stackImp.show();
	}

	//////シミュレーション結果を保存する////////
	//保存する名前を付ける
	String addName () {
		FileDialog fd = new FileDialog(new Frame(), "名前を付ける. 拡張子は付けないで下さい.", FileDialog.SAVE);
		fd.setVisible(true);
		String path = fd.getDirectory() + fd.getFile();
		fd.dispose();
		return path;
	}

	//保存する
	public void saveImage () {
		String name = addName();
		IJ.saveAs(imp, "TIFF", name);
		IJ.saveAs(stackImp, "TIFF", name + " stack");
	}

	//コントローラーの作成
	public void run (String arg) {
		JFrame frame = new JFrame ("Simulator");
		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		frame.setBounds(10, 10, 180, 120);
		JButton doneButton = new JButton ("Done");
		JButton saveButton = new JButton ("Save");
		doneButton.addActionListener(this);
		saveButton.addActionListener(this);
		doneButton.setActionCommand("Done");
		saveButton.setActionCommand("Save");
		doneButton.setBounds(40, 10, 90, 30);
		saveButton.setBounds(40, 50, 90, 30);

		JPanel pane = new JPanel ();
		pane.setLayout(null);
		pane.add(doneButton);
		pane.add(saveButton);
		frame.getContentPane().add(pane, BorderLayout.CENTER);
		frame.setVisible(true);
	}

	//ボタンを押した時の反応
	public void actionPerformed(ActionEvent e) {
		String cmd = e.getActionCommand();
		if (cmd.equals("Done")) {
			makeCanvas ();
			simulate();
			zStack();
		} else if (cmd.equals("Save")) {
			saveImage();
		}
	}
}

このPluginを使うと、まず下のコントローラーが立ち上がる。
f:id:Aki-Miya:20180212160959p:plain
そして、Doneボタンを押すと、粒子の拡散シミュレーションを行い、
f:id:Aki-Miya:20180212161133p:plain
拡散シミュレーションの結果のstack画像と、それをZ-Projectionした下記の画像が作成される。
f:id:Aki-Miya:20180212161145p:plain
そしてSaveボタンを押せば、これらのシミュレーション結果を保存できる。

このシミュレーション方法はかなり簡便で、しかも実験データと同じ方法で解析できるので、かなり汎用性が高そうだ。



いくつかコードを見ていく。
今回は、拡散シミュレーションの結果を動画として取り扱うために、拡散シミュレーション結果をMuliti-tiffにすることにした。
そのため、まず下記のコードで、

	//シミュレーションするためのキャンバスの作成
	public void makeCanvas () {
		imp = IJ.openImage("何かしらの画像ファイルのディレクトリと名前");
		imp.setTitle("Simulation");
		w = imp.getWidth();
		h = imp.getHeight();
		imp.setRoi(0, 0, w, h);
		IJ.setBackgroundColor(0, 0, 0);
		IJ.run(imp, "Clear", "slice");
		imp.killRoi();

		//シミュレーションするためにstack画像にする
		for (int i = 0; i < sliceN; i++) {
			IJ.run(imp, "Add Slice", "");
		}
		ip = imp.getProcessor();
	}

適当な画像(ここでは512x512 pixelの16bit画像を使用)を開き、その画像の数値をクリアし、値を持たないstack画像を作成。このstack画像がシミュレーション結果を書き出すためのキャンバスになる。(stackの枚数がシミュレーション時間に相当)

そして、下記の部分で、

	//全ての粒子を中心に置く
	for (int i = 0; i < number; i++) {
		x[i] = w/2;
		y[i] = h/2;
	}
	imp.setSlice(1);
	ip.putPixelValue(w/2, h/2, 65535);

全ての粒子を中心に配置 (初期状態)。

そして、下記の部分で個々の粒子の位置を各sliceに記述することで、粒子の経時変化をシミュレーション出来るようにした。
(今回は輝度値も時間と共に減少するようにし、粒子位置の経時変化をより見やすくした)

//粒子の動きを描く
	for (int i = 2; i < sliceN; i++) {
		imp.setSlice(i);
		for (int j = 1; j < number; j++) {
			double th = 2.0*Math.PI*Math.random();
			x[j] = x[j] + (int)(Math.cos(th)*10);
			y[j] = y[j] + (int)(Math.sin(th)*10);
			ip.putPixelValue(x[j], y[j], 65535 - 10*j); //時間と共に粒子の輝度値が減少
		}
	}