
1Fiji 2鷹 のtiff祭り
例によって例の如く鷹というか蛇の記事
0.おかえり画像処理
時に2020年....
コロナウイルスの影響で東京オリンピックは延期し全国の大学で遠隔授業が実施されたその時....
ちょうど画像処理工学という授業が開催された。
工学部三年前期の出来事である。
Rというよくわからん言語を半ば強制され、ヒィこらヒィこら言いながらRを叩いていたのは今は昔。
今思えば、あれだけ通常授業はRオンリーだったくせに最終課題はPythonでもMatlabでもなんでもとにかく画像処理できればOKというよくわからん授業だったがこれは余談。
さらば画像処理!
Pythonで書いた顔変換処理コードを最終課題として提出し画像処理工学に別れを告げた。
もう二度と会うこともあるまい。
そして2021年...
研究データの解析として一回りもふた周りも大きくなって画像処理は帰ってきた。
君にも見えるRGBの星
遠く離れて研究室に一人
帰ってきたぞ帰ってきたぞ
助けてウルトラマン.....
1. ImageJとFIji
生物系の研究室お馴染みのImageJ。
早い話がお手頃快適GUI画像処理ツールな訳だが、主観混じりの感想なんてあんまり意味ないので偉大なWiki先生にご足労いただこう。
ImageJはオープンソースでパブリックドメインの画像処理ソフトウェアである。Javaの仮想マシン上で動作し、プラグインやマクロによる拡張性が高い。科学研究における画像解析に広く利用され、生物学ではデファクト・スタンダードの解析ツールとなっている。
(引用 https://ja.wikipedia.org/wiki/ImageJ)
早い話がお手頃快適GUI画像処理ツールな訳である。
画像をドラッグ&ドロップして内蔵された各種ツールをチョチョイとすれば画像から色々データを取ってこれる。
ImageJ画面
以下ImageJでできることをざっくりと示しておく。
できることの例
・ROI(Region of Interest)を指定してピクセル単位で数値を取得
・ROIを保った状態で連続写真を表示
・コントラストの変更
・画像の2値化
・閾値の変更
・マクロが組める. etc...
一番良いのがマクロを組んで上記処理の自動化ができるという点だ。
マクロレコーディング機能を使えばユーザの処理をImageJが記録して勝手にマクロ化してくれる。
なんという神機能。
ただしいくらレコーディング機能があったとしても、一からマクロ言語覚えるのはしんどい....
後から読んで何やってるか分かりづらい....
そんなこともあるかもしれない。
しかし安心して欲しい。
ImageJのほぼ上位互換と言えるFijiではなんとPythonでマクロが書けるのだ!
おぉ我らがパイソン!
バージョン2.7だけど見知った顔がいると心強いよ!
バージョン2.7だけど!
Fiji画面 操作はImageJとほぼ変わらない
2.パイソンマクロ道
Pythonの良さとは、豊富なパッケージとインタプリタ故のフィードバックの速さ、そして言語としての書きやすさにある。(あくまで個人の感想です)
あと、未知のクラスや返り値をprint(type())してやれば大体正体がわかるのも強いね。
閑話休題。
Fijiを操作するパッケージとしてijが存在する。
基本的にここでやりたい操作を検索すればそういうメソッド名かクラスが出てくるので後はそれをうまいこといじってやればなんとなくマクロ化はできる。
ijパッケージのドキュメント
https://imagej.nih.gov/ij/developer/api/ij/module-summary.html
コーディングの基本方針だが、基本的にマクロ言語とパッケージ内の関数名が比較的一致している傾向にあるため、マクロレコーディングで操作名を確認し、上サイトで検索、具体的な関数処理の仕方を学ぶと言った具合でやると大体やりたいことは実装できた。
無論Forumで検索してやりたい事例が見つかればいつものようにコピペプログラミングだが...
3.例えばこんなスクリプト
例えば、画像ファイルを一挙に読み込み、四角でトリミング場所を指定して別名で保存するコードなんかも書ける。
# trimming.py
import sys
from ij import IJ
import os
if __name__ == '__main__':
#Setting
saveDirPath = ""
makeRectangle = ""
files = os.listdir(saveDirPath)
for fileName in files:
if fileName != ".DS_Store":
imp = IJ.open(saveDirPath+"/"+fileName)
IJ.makeRectangle(x1, y1, x2, y2)
IJ.run("Crop")
IJ.saveAs("png", saveDirPath+"/aaa"+fileName)
IJ.run("Close")
縦横サイズが同じ実験画像なんかを同じ割合でトリミングしたい時なんかに使えるね。
「ある基本動作をn回繰り返す」などといった基本操作を直感的にかけるのがPythonマクロの強みであると言える。
次は画像上の各ピクセルの数値を一行ずつROIとして取得するコード。
見ていて面白い挙動をする。
#scan.py
imp = IJ.openImage(frame_path+fileName)
height = imp.getHeight()
width = imp.getWidth()
frame = csvName.split("_")[2]
if int(frame) <10:
csvName = csvName.rstrip(frame) + "0" + frame
f = open(saveDirPath+csvName+".csv", 'w')
writer = csv.writer(f)
writer.writerow(['y','x','pixelVal'])
for y in range(height):
IJ.makeLine(0,y,width,y)
pp = ProfilePlot(imp)
profile = pp.getProfile()
doubleArray = pp.getProfile()
i=0
data =[]
for d in doubleArray:
lis = []
lis = [y,i,d]
data.append(lis)
i+=1
writer.writerows(data)
f.close()
結果的に画像の数値データを行列としてcsv出力して保存する。
なんでこんな遠回りなコードを書いたのか...
それは次章を参照していただきたい。
4.行列化に伴うメタデータ消失問題
さてさて上に紹介したscan.pyだが、目的は各ピクセルの数値を取ってくることだった。
なのになんであんなトンチンカンなコードを書く羽目になったのか。
それは私がTiffという画像フォーマットに対して無知だったためである。
私が今までに扱ったことのある画像データというのは単独で完結していたが、画像データというのは、時として時系列を持ちそれぞれに配色チャンネルを持っている。そして今回処理しなければいけないtiff画像はこうした多次元的なデータを持っていた。
当時の私は、"この手のデータはImageJで処理しなければならない"という固定観念にも似た思い込みをしてしまった。
その結果、ImageJ以外で処理してやろうという選択肢が思い浮かばず、半ばゴリ押しの上記コードが完成したのである。
いやまぁゴリ押しもゴリ押しだけど実装はできたから....(震え声)
そもそもtiffが画像である以上、行列としてデータを持っているわけだからそこにアクセスすれば良いだけのはずである。
こういう処理をしたい人は世の中にいっぱいいるはずや。
Pythonちゃんにそういうパッケージあるんじゃね?
あるはずや。
このパッケージ問題を解決するため、我々探検隊はアマゾンの奥地へと向かった...
GAFA的な意味で言うと探検したのはAmazonじゃなくてGoogleなんだけどそこら辺はご愛嬌。
5.パッケージtifffile
ありました。tifffileパッケージ。さすが世界一位のPython。
南米の主婦層ではPythonのことを世界三位という人もいるがね。
とんでもない。Pythonは一位なんだよ。
tifffile
https://pypi.org/project/tifffile/
ドキュメントを読めばなんとなくわかるけど、画像処理特有のimread(),imwrite()バンバン出てくるね。
なんとなく今まで使ってたopenCVとかPILとかそんな感じがするね。
#出力
>>> data = numpy.random.randint(0, 255, (256, 256, 3), 'uint8')
>>> imwrite('temp.tif', data, photometric='rgb')
#読み込み
>>> image = imread('temp.tif', key=0)
>>> image.shape
(301, 219)
画像AがNフレームXチャンネル120×130ピクセルの画像をtifffileで読み込んでimage.shapeすると(N,X,120,130)みたいな感じで出てくる。imreadで読み込んだ画像は<class 'numpy.ndarray'>となって返ってきます。
画像が行列化できたんだから後は適当にぽんぽんやりたいようにやればいいのよね
はい解散。
6.tifffile応用編
前章ではtif画像をimreadで読み込んだんだけど、これだと画像を数値行列として取得するだけにとどまってしまう。
例えば、グレイスケールデータをルックアップテーブルに合わせてチャンネル変換できないのである。
画像を数値を持った行列として処理するのであればtifffile.imread()を使ってやればいいが、データの持つチャンネル変換情報や時系列情報が失われてしまう。
そういう時はtif画像をtifffileとして取得してしまうのが手。
import tifffile
with tifffile.TiffFile(path + "aiueo.tif") as tif:
volume = tif.asarray()
axes = tif.series[0].axes
imagej_metadata = tif.imagej_metadata
volumeは画像をnumpy.ndarrayとして読み込んでいる。すなわち前章で紹介したところのimread()だ。tifffile.imread()というのはつまるところ、tifffileの持つasarray()の値を返しているというだけである。
axesは行列がどういった情報を表しているかを示している。
NフレームXチャンネル120×130画像の場合'TCYX'という文字列が格納されている。きちんと確認はしていないがおそらくはTime,Color,Y,Xと言った具合であろう。データによって差があるであろうからここは各自で確認していただきたい。
image_metadataというのは文字通りimagejにおけるmetadataである。辞書型で返ってくる。ここも各自で確認していただきたいが、ルックアップテーブルを持つデータはmetadata['Luts']を持っている。
こうした情報をどうしろというのか。
imwrite()で出力する際に引数として与えてやればいいのである。
こんな感じ
tifffile.imwrite(motherPath + "aiueo.tif",tiffImage,imagej=True,metadata=imagej_metadata)
こうすることで実験データを時系列,チャンネル情報を維持したままある程度加工できる。やったね。
探せばもっと色々できるんだろうけど、今回は時系列を持ったtifとして保存、及び出力するグレイスケール画像にオリジナルのチャンネル情報を与える処理を備忘録として残す。
import tifffile
import numpy
motherPath = ""
# get metadata
with tifffile.TiffFile(motherPath+"aiueo.tif") as tif:
print(type(tif))
volume = tif.asarray()
axes = tif.series[0].axes
imagej_metadata = tif.imagej_metadata
# add frame information
imagej_metadata['axes'] = "TCYX"
# get tif image as ndarray
tiffImage = tifffile.imread(target)
"""
ndarrayに対して何かしらの処理
"""
tifffile.imwrite(motherPath + "abcdef.tif",tiffImage,imagej=True,metadata=imagej_metadata)
print("done!")
これでImageJで元データの情報を保ったまま観察できる。
まぁ処理したndarrayに元のmetadataぶっ込んでるだけなんだけど....
tifffil.pyについてのgitリンク
https://github.com/blink1073/tifffile/blob/master/tifffile/tifffile.py
7.編集後記
gitのパッケージ情報を読んである程度やりたいことができるようになった。
えらい。