マイブログ リスト

医療言語処理講座

2019年1月24日木曜日

Pyゼミ3.02 手書き画像からDICOM画像へ

手書き数字画像MNISTから医療画像DICOMへ発展させよう


Keyword:MNIST,DICOM,Neural Network,Chainer

前回のPyゼミ3.01にて,手書き数字画像を用いて,ニューラルネットワークの学習を行ないました。
前回のニューラルネットワークをDICOM画像に発展させることを考えてみます。


MNISTがどのようなデータ構造をもった訓練画像なのか調べてみる


対話モードで,Chainerのdatasetsをimportして,訓練データ,テストデータをそれぞれtrainとtestに読み込んでみます。

>>> from chainer import datasets
>>> from chainer.datasets import tuple_dataset
>>>
>>> train, test = datasets.get_mnist(ndim = 3)
次に,訓練データの数をlenメソッドを使って調べてみます。
>>> len(train)
60000


確かに6万件のデータ(手書き数字画像)が読み込まれていることがわかります。
それでは,最初の0番めのデータについて,同じくいくつ格納されているのかlenメソッドで調べてみます。
>>> len(train[0])
2


2つです。
つまり,train[0][0]とtrain[0][1]の2つです。

さらにtrain[0][0]について,調べてみます。
>>> len(train[0][0])
1


1つです。
train[0][0][0]は1つということです。
さらに調べてみると,
>>> len(train[0][0][0])
28


28と表示されました。
これはMNISTの手書き数字の画素サイズです。
さらに調べると。
>>> len(train[0][0][0][0])
28


また,28と表示されました。


少しまとめてみましょう。
最初の60000と表示された。これはデータ数(画像と教師ラベル)を表しています。
つまり,train[0]からtrain[59999]のデータがあることを示しています。

先頭の0番目のデータについて注目してみると
この中には2つの情報があるようです。
ひとつめの情報(tarin[0][0])を表示してみましょう。

>>>  train[0][0]
array([[[ 0.        ,  0.        ,  0.        ,  0.        ,  0.        ,
          0.        ,  0.        ,  0.        ,  0.        ,  0.        ,
          ・・・略・・・
          0.        ,  0.        ,  0.        ,  0.        ,  0.        ,
          0.        ,  0.        ,  0.        ]]], dtype=float32)

かなり大きなリストのデータです。多分画像じゃないかって想像がつきます。

ふたつめの情報(train[0][1])を表示してみましょう。
>>> train[0][1]
5

5と表示された。これはラベルデータじゃないかと想像できます。
次に,画像の方をさらに見てみると,len(train[0][0])は1だったので,train[0][0][0]しかない。
これを表示してみると,
>>> train[0][0][0]
array([[ 0.        ,  0.        ,  0.        ,  0.        ,  0.        ,
         0.        ,  0.        ,  0.        ,  0.        ,  0.        ,
         0.        ,  0.        ,  0.        ,  0.        ,  0.        ,
         0.        ,  0.        ,  0.        ,  0.        ,  0.        ,
         0.        ,  0.        ,  0.        ,  0.        ,  0.        ,
         0.        ,  0.        ,  0.        ],
       [ 0.        ,  0.        ,  0.        ,  0.        ,  0.        ,
              ・・・略・・・
         0.        ,  0.        ,  0.        ],
       [ 0.        ,  0.        ,  0.        ,  0.        ,  0.        ,
         0.        ,  0.        ,  0.        ,  0.        ,  0.        ,
         0.        ,  0.        ,  0.        ,  0.        ,  0.        ,
         0.        ,  0.        ,  0.        ,  0.        ,  0.        ,
         0.        ,  0.        ,  0.        ,  0.        ,  0.        ,
         0.        ,  0.        ,  0.        ]], dtype=float32)

となる。arrayの中で [ [ で囲まれているので二次元のリストだ,つまり画像が格納されていることがわかります。


画像かどうか表示してみましょう。OpenCVで簡単に見ることができます。
(OpneCVのインストールはPyゼミ1.02参照)

>>> import cv2
>>>
>>> cv2.imshow('image', (train[0][0][0]))
>>> cv2.waitKey(0)





教師ラベルが5であったように,確かに5の手書き数字です。

少し補足するとtrain[0][0][0]は28×28画素のモノクロの画像1枚(1プレーン,1チャンネル)なので,len( train[0][0] )は1を出力しました。
もし,カラー画像の場合,len( train[0][0] )は3を出力します。
つまり,RGBそれぞれがtrain[0][0][0],train[0][0][1],train[0][0][2]に格納されています。

まとめると

訓練画像データは5次元のデータで,ラベルデータは2次元である。
1次元:データの順序番号を表す。(0~データ数-1)
2次元:画像またはラベルを表す。0のとき画像,1のときラベル
3次元:画像プレーン番号(モノクロは0の1つのみ,カラーは0,1または2の3つ)
4次元:画像の行数(縦の画素数,画像の高さ)
5次元:画像の列数(横の画素数,画像の幅)

画像の画素x,yにアクセスするには次のようになります。
    train[データ番号][0][プレーン番号][画像の行(y)][画像の列(x)]
たとえば,10番めの画像のx=11,y=12の画素を得るには次のように書きます。
    pix = train[9][0][0][12][11]

ラベルについては下記のようにアクセスできます。
    tarin[データ番号][1]

したがって,画像とそのラベルが与えられたら5次元の訓練データを作成するプログラムを作成すれば良いことになります。


DICOM画像から訓練データを作成する

DICOM画像このサンプルは個人の画像なのでこの実験以外では使用しないでください)から訓練データを作成する仕様としては以下のように考えます。

  1. DICOM画像の入ったフォルダを指定して,その中の画像を訓練データとする。
  2. ラベル名はファイル名(Brain01, Chest02など)から抽出し,それにラベル番号を自動で(0から)付与する(サンプル画像のファイル名の命名がラベル名となっている)。
  3. DICOMファイル名,ラベル番号とラベル名のCSVファイルを作成する。
  4. 上記,CSVファイルから訓練データを作成する関数を作る


プログラムの概要


constractTrainData関数

次の2つの引数を受け取とります。

  • CSVファイル名(fname) :DICOMファイル名,ラベル番号とラベル名のCSVファイル名を指定する。
  • リサイズ値(resz):画像を任意のサイズに拡大または縮小する画素数を指定する。


プログラムははじめに画像とラベルを格納するリストを初期化します。
    image = [ ]
    label = [ ] 

次にfor文にて,CSVファイルから1行づつ読み込みlineに代入し,読み込んだ回数を0から変数n代入します。
変数lineの値はカンマ区切りのデータで,data(リスト)に分割して格納します。
    data[0]にはDICOMファイル名が
    data[1]にはラベル番号が
    data[2]にはラベル名が格納されます。

readDicom2png関数(2.03参照)を用いてウィンドウニングした画像をimgに代入します。
このとき,imgの画素値は0から255の8ビットの値をとります。

もし,リサイズの値reszが0より大きければリサイズしますが,0の場合はリサイズしません。

画像imgは浮動小数点32ビット(np.float32)に変換し,さらに0から255の値を-1.0から1.0の値に変換します。
    img = (img - 128)/128

そして,imageのリストにappendメソッドを用いてimgを追加します。
    image.append( [ img]  )

次にラベル番号は整数32ビット(int32)に変換して,labelのリストに追加します。
    t = np.array( int( data[1] ), dtype = np.int32 )
    label.append( t )

最後に,画像imageとラベルlabelのリストをTupleDatasetメソッドを使って,統合してtrainデータを作成します。
    train = tuple_dataset.TupleDataset( image, label )

TupleDatasetは複数の同じ長さのデータセットをひとつのtuple型のデータセットにまとめるメソッドです。


プログラムを入力して実行してみよう


makeDicomTrain.py


import os, sys
import numpy as np
import cv2
import chainer
from chainer.datasets import tuple_dataset
import pydicom

def constractTrainData( fname , resz ):
    image = []
    label = []
    for n, line in enumerate(open(fname, 'r')):
        data = line.split(",")          # 画像ファイル名と正解データに分割
        print(n," File Name:", data[0] ," Label:", data[1])
       
        img = readDicom2png(data[0], "tmp.png")
        if resz > 0:                    #画像のリサイズ       
            img = cv2.resize(img,(resz, resz))       
        img = img.astype(np.float32)   
        img = (img - 128)/128
        image.append([img])
               
        t = np.array(int(data[1]), dtype=np.int32)
        label.append(t)
   
    train = tuple_dataset.TupleDataset(image, label)
   
    return train
   
def readDicom2png(dcmfnm, tmpfnm):
    ds  = pydicom.read_file(dcmfnm)     
    wc  = ds.WindowCenter               
    ww  = ds.WindowWidth                        
    img = ds.pixel_array                        
    #表示画素値の最大と最小を計算する
    max = wc + ww / 2                     
    min = wc - ww / 2     
    #ウインドニング処理
    img = 255 * (img - min)/(max - min)  
    img[img > 255] = 255                 
    img[img < 0  ] = 0                   
    img = img.astype(np.uint8)
    cv2.imwrite(tmpfnm, img)
    return img      

if __name__=='__main__':

    dirname = "../dcmdir1/"
    fname   = "trainlist.csv"
    resize  = 200
    lblnum  = 0
    lblname = {}
    csvstr  = ""
   
    dlist = os.listdir(dirname)
    print(dlist)
    for nm in dlist:
        bsnm = nm[:-2]              #ファイル名の最後の2文字(数字)を取り除く
        if not bsnm in lblname:
            lblname[bsnm] = lblnum
            lblnum += 1
        csvstr += dirname+nm+","+str(lblname[bsnm])+","+bsnm+"\n"
    
    f = open(fname, 'w')
    f.write(csvstr)
    f.close()
      
    #画像データ,教師データの構築
    train = constractTrainData(fname, resize)
   
    print("\ntrain\n")
    print("画像データ数    :",len(train))
    print("画像/ラベル  :",len(train[0]))
    print("画像チャンネル数:",len(train[0][0]))
    print("画像の高さ   :",len(train[0][0][0]))
    print("画像の幅    :",len(train[0][0][0][0]))
    
    cv2.imshow("image",(train[0][0][0]))
    cv2.waitKey(0)
    print("\nLabel Number:",train[0][1])


実行結果

makeDicomTrain.pyを実行すると,指定したフォルダ(../dcmdir1/)内のファイルを表示します。
8枚のDICOM画像のファイル名とラベル番号が表示されます。
最後に訓練データの構成が表示されます。
画像をリサイズしたので画像の高さと幅が200となっています。


$ python makeDicomTrain.py
['Abdomen01', 'Chest02', 'Chest01', 'Brain01', 'Abdomen03', 'Abdomen02', 'Brain02', 'Chest03']

0  File Name: ../dcmdir1/Abdomen01  Label: 0
1  File Name: ../dcmdir1/Chest02  Label: 1
2  File Name: ../dcmdir1/Chest01  Label: 1
3  File Name: ../dcmdir1/Brain01  Label: 2
4  File Name: ../dcmdir1/Abdomen03  Label: 0
5  File Name: ../dcmdir1/Abdomen02  Label: 0
6  File Name: ../dcmdir1/Brain02  Label: 2
7  File Name: ../dcmdir1/Chest03  Label: 1

train

画像データ数    : 8
画像/ラベル  : 2
画像チャンネル数: 1
画像の高さ   : 200
画像の幅    : 200

Label Number : 0


trainlist.csvの出力

CSVファイルにDICOMファイル名,ラベル番号とラベル名が記録されてます。









確認画像のPNG出力と表示

tmp.pngファイルがreadDicom2png関数から出力されています。





mnistNN.pyにconstractTrainData関数を実装してみる

実装方法は以下の要領で行います。
  1. mnistNN.pyをコピーしてdicomNN.pyに変更します。
  2. makeDicomTrain.pyのconstractTrainData関数,readDicom2png関数をdicomNN.pyにコピーします。
  3. mnistの入力部分は削除し,makeDicomTrain.pyのデータ入力部分を上記2つの関数のあとにコピーします。
  4. os, sys, cv2とpydicomのインポートを追加します。
  5. 今回,testデータはないので,trainデータの2つを使って実験をしてみましょう。

下記プログラムを参照してください。

dicomNN.py


import os, sys
import cv2
import numpy as np
import chainer
from chainer import Function, report, training, utils, Variable
from chainer import datasets, iterators, optimizers, serializers
from chainer import Link, Chain, ChainList
import chainer.functions as F
import chainer.links as L
from chainer.datasets import tuple_dataset
from chainer.training import extensions

import pydicom

def constractTrainData( fname , resz ):
    image = []
    label = []
    for n, line in enumerate(open(fname, 'r')):
        data = line.split(",")          # 画像ファイル名と正解データに分割
        print(n," File Name:", data[0] ," Label:", data[1])

        img = readDicom2png(data[0], "tmp.png")
        if resz > 0:                    #画像のリサイズ       
            img = cv2.resize(img,(resz, resz))       
        img = img.astype(np.float32)   
        img = (img - 128)/128
        image.append([img])
               
        t = np.array(int(data[1]), dtype=np.int32)
        label.append(t)
       
    train = tuple_dataset.TupleDataset(image, label)
       
    return train
   
def readDicom2png(dcmfnm, tmpfnm):
    ds  = pydicom.read_file(dcmfnm)     
    wc  = ds.WindowCenter               
    ww  = ds.WindowWidth                        
    img = ds.pixel_array                        
    #表示画素値の最大と最小を計算する
    max = wc + ww / 2                     
    min = wc - ww / 2     
    #ウインドニング処理
    img = 255 * (img - min)/(max - min)  
    img[img > 255] = 255                 
    img[img < 0  ] = 0                   
    img = img.astype(np.uint8)
    cv2.imwrite(tmpfnm, img)
    return img   

#訓練データとテストデータの取得
dirname = "../dcmdir1/"
fname   = "trainlist.csv"
resize  = 28
lblnum  = 0
lblname = {}
csvstr  = ""

dlist = os.listdir(dirname)
print(dlist)
for nm in dlist:
    bsnm = nm[:-2]              #最後の2文字を取り除く
    if not bsnm in lblname:
        lblname[bsnm] = lblnum
        lblnum += 1
    csvstr += dirname+nm+","+str(lblname[bsnm])+","+bsnm+"\n"
f = open(fname, 'w')
f.write(csvstr)
f.close()

#画像データ,教師データの構築
train  = constractTrainData(fname, resize)
print("\ntrain\n")
print("画像データ数    :",len(train))
print("画像/ラベル  :",len(train[0]))
print("画像チャンネル数:",len(train[0][0]))
print("画像の高さ   :",len(train[0][0][0]))
print("画像の幅    :",len(train[0][0][0][0]))

class MyModel(Chain):                   #ネットワークモデルを定義する
    def __init__(self, nLabel):                 #ネットワークの定義
        super(MyModel, self).__init__(
            l1=L.Linear(784,100),       #全結合ネットワーク
            l2=L.Linear(100,100),           
            l3=L.Linear(100,nLabel),
        )
       
    def __call__(self, x):              #順伝播計算
        h1 = F.relu(self.l1(x))         #活性化関数にReLuを定義
        h2 = F.relu(self.l2(h1))
        return self.l3(h2)
       
model = MyModel(lblnum)                 #ネットワークモデルを生成する
model = L.Classifier(model, lossfun=F.softmax_cross_entropy)
optimizer = optimizers.Adam()           #最適化アルゴリズムを設定する
optimizer.setup(model)                  #モデルを最適化アルゴリズムにセットアップ

iterator = iterators.SerialIterator(train, 50, shuffle = True) #ミニバッチの設定
updater = training.StandardUpdater(iterator, optimizer) #更新処理の設定
trainer = training.Trainer(updater, (10, 'epoch'))      #訓練(エポック数)の設定
trainer.extend(extensions.ProgressBar())                #進捗表示の設定

trainer.run()                           #訓練実行

test=train[:2]  #試しに2つのデータについて

#訓練結果からtestデータを用いて推定を行う
ok = 0
n  = 0
for i in range(len(test)):              #テストデータ分繰り返す
    x = Variable(np.array([ test[i][0] ], dtype=np.float32)) #入力データ
    t = test[i][1]                      #正解データ
    y = model.predictor(x)              #テストデータから推定結果を計算
    out = F.softmax(y)
    ans = np.argmax(out.data)           #推定結果からラベルを得る
    n += 1
   
    np.set_printoptions(formatter={'float': '{: 0.2f}'.format})
    print('\n#',i)
    print('Infer :',out.data)           #推定結果を表示
    print('Sprvs :','  ','----->'*(int(t)),t)#教師データの表示
    if (ans == t):
        ok += 1   
        print('\tOK',ok/n)
     
print('平均正答率:', ok/len(test))


実行結果

mnistの手書きデータのサイズに合わせるために,DICOM画像を28×28にリサイズしています。
テストは未知のデータですべきですが,最初の2つの画像データでテストをしています。

今回はMNISTからDICOM画像への応用が成功しました。

$ python dicomNN.py
['Abdomen01', 'Chest02', 'Chest01', 'Brain01', 'Abdomen03', 'Abdomen02', 'Brain02', 'Chest03']
0  File Name: ../dcmdir1/Abdomen01  Label: 0
1  File Name: ../dcmdir1/Chest02  Label: 1
2  File Name: ../dcmdir1/Chest01  Label: 1
3  File Name: ../dcmdir1/Brain01  Label: 2
4  File Name: ../dcmdir1/Abdomen03  Label: 0
5  File Name: ../dcmdir1/Abdomen02  Label: 0
6  File Name: ../dcmdir1/Brain02  Label: 2
7  File Name: ../dcmdir1/Chest03  Label: 1

train

画像データ数    : 8
画像/ラベル  : 2
画像チャンネル数: 1
画像の高さ   : 28
画像の幅    : 28

# 0
Infer : [[ 0.67  0.22  0.11]]
Sprvs :     0
 OK 1.0

# 1
Infer : [[ 0.31  0.60  0.09]]
Sprvs :    -----> 1
 OK 1.0
平均正答率: 1.0




おわりに

MNISTはとても有名なデータセットなので,インターネットで調べると実はたくさん情報を得ることができます。
しかし,今回は解析的にデータ構造を追いかけることをしました。
こうした手法は,将来未知のデータセットに出会ったときに役に立つと思います。

今回訓練用データを作成するconstrauctTrain関数はこれからも使います。
また,画像ファイル名とラベル番号をCSVファイルに保存する考え方は,これからも訓練やテストのために使用します。




0 件のコメント:

コメントを投稿