マイブログ リスト

医療言語処理講座

2019年2月17日日曜日

Pyゼミ2.03 PyQt5によるDICOM画像のセグメンテーション

DICOM画像の注目する領域をセグメンテーションしてみよう

Keyword: DICOM, PyQt,マウスイベント, セグメンテーション, 二値画像

特定の領域を抽出する学習には訓練データと正解領域の教師データが必要になります。
例えば,臓器や病変部を抽出すようなとき,対象の訓練データに対して正解となる二値画像の教師データが必要です。

このプログラムでは,DICOM画像の注目する領域をマウスでポイントして囲み,囲まれた領域を二値画像としてpngファイルに保存することを考えます。

プログラムの概要


はじめに,マウスでポンとした座標を保存するリスト(配列)をイニシャライザ内で初期化します。
    self.points = [ ]

GUIのプログラムをinitUI関数内に記述します。
ここで画像上でマウスがクリックされたイベントを取る必要があります。
画像を貼り付けたラベルlblimgにマウスのイベントと処理を追加します。
          self.lblimg.setMouseTracking( True )
          self.lblimg.installEventFilter( self )

マウスイベントの取得はeventFilter関数で,以下の処理を行います。
イベントタイプが,QEvent.MouseButtonPressのとき,つまりマウスでポンとしたとき、クリックした座標をevent.posメソッドで取得します。
クリックした座標をリストpointsに追加していきます。         
                self.points.append( [ pos.x( ),  pos.y( ) ] )

さらに,リストpoints内の座標を画像に表示するためredisplay()関数を呼び出しています。
この関数はマウスがクリックされるたびに呼び出され、画像上の点を線で結びます。

最後に二値画像に保存するSaveボタンをクリックすると,二値画像がsegment.pngというファイル名で保存します。

プログラム起動時のWindow(左),マウスクリック時の多角形表示(中央)と二値画像(右)



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


dicomSegmentation.py

import sys,os
import numpy as np
import cv2
import pydicom
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.Qt import *

class DicomSegmentation(QWidget):

    def __init__(self, filename):
        super().__init__()
        self.dcmfnm = filename    # DICOMファイル名
        self.points = []          #マウスでポイトした座標を格納する配列
        self.initUI()

    def initUI(self):
        # ファイル名を表示するラベルの作成
        lblfnm = QLabel(" File Name :  \t" + self.dcmfnm)
        fnmbox = QVBoxLayout()
        fnmbox.addWidget(lblfnm)

        # 画像読み込みラベルにセットする
        self.readDicom2png(self.dcmfnm, './dcm.png')    #DICOMを読んでpngに出力
        pixmap = QPixmap('./dcm.png')
        self.lblimg = QLabel()
        self.lblimg.setPixmap(pixmap)
        print("w=",pixmap.width(), "h=",pixmap.height())
        self.w = pixmap.width()
        self.h = pixmap.height()
        # 画像上のマウスイベントを取得する
        self.lblimg.setMouseTracking(True)
        self.lblimg.installEventFilter(self)
        # 画像表示レイアウトの設定
        imgbox = QVBoxLayout()
        imgbox.addWidget(self.lblimg)
       
        # Point Data保存
        pntSvBtn = QPushButton("Point Save")
        pntSvBtn.clicked.connect(self.pointSaveButtonClicked)
        # Point Clear ボタン
        pntClrBtn = QPushButton("Pont Clear")
        pntClrBtn.clicked.connect(self.pointClearButtonClicked)
        # ボタンレイアウト
        btnbox = QHBoxLayout()
        btnbox.addWidget(pntSvBtn)
        btnbox.addWidget(pntClrBtn)
       
        # 各パーツを配置する垂直なボックスを作成
        mainVbox = QVBoxLayout()       
        mainVbox.addLayout(fnmbox)
        mainVbox.addLayout(imgbox)
        mainVbox.addLayout(btnbox)

        # 垂直ボックスをウィンドウにレイアウトする
        self.setLayout(mainVbox)   
        self.setWindowTitle('DICOM Segmentation')   
        self.show()
       
    #画像上でマウスクリックした座標を取得し,保存して画像表示する
    def eventFilter(self, object, event):
        if event.type() == QEvent.MouseButtonPress:     #イベントタイプ
            pos = event.pos()                           #座標の取得                         
            if object is self.lblimg:                   #lblimg上ででマウスがクリックされたら            
                self.points.append([pos.x(), pos.y()])  #x,y座標を配列に追加する
                print(">>points=",self.points)
                self.redisplay()                        #pointsを画像上に表示する  
            
        return QWidget.eventFilter(self, object, event)
                
    #セグメント領域を画像中に多角形として重ね合わせ表示する    
    def redisplay(self):
        img = cv2.imread("./dcm.png", 1)                #切り出した画像の読込
        pnt = np.array(self.points, dtype = np.int64)   #配列を変換
        cv2.polylines(img, [pnt], True, (64,128,255))            
        cv2.imwrite("./fusion.png", img)                #重ねあわせ画像を保存する
        pixmap = QPixmap("./fusion.png")    
        self.lblimg.setPixmap(pixmap)       
        self.show()


    # セグメンテーション画像データの保存   
    def pointSaveButtonClicked(self):
        print("Save point data:", self.points)
        #セグメント画像の作成と保存   
        img = np.full((self.w, self.h, 3), 0, dtype = np.uint8)   #空の画像imgの作成          
        pnt = np.array(self.points, dtype = np.int64) #ポイント座標をndarrayに変換
        cv2.fillPoly(img, [pnt], (255,255,255))       #画像img上にセグメント領域を描く
        cv2.imwrite("./segment.png", img)             #セグメンテーション画像を保存
       
    # ポイント座標をクリアする
    def pointClearButtonClicked(self):
        print("Clear all points")
        self.points = []      #Point座標を初期化
        self.redisplay()      #Pointを画像上に表示する           
    
   
    # DICOM画像読み込んでウィンドニング後PNGに保存      
    def readDicom2png(self, dcmfnm, tmpfnm):
        ds  = pydicom.read_file(dcmfnm)      #DICOM画像を読み込む
        wc  = ds.WindowCenter                #ウィンドウセンター値を代入
        ww  = ds.WindowWidth                 #ウィンドウ幅を代入       
        img = ds.pixel_array                 #画素値を代入       
        #表示画素値の最大と最小を計算する
        max = wc + ww / 2                     
        min = wc - ww / 2     
        #ウインドニング処理
        img = 255 * (img - min)/(max - min)   #最大と最小画素値を0から255に変換
        img[img > 255] = 255                  #255より大きい画素値は255に変換
        img[img < 0  ] = 0                    #0より小さい画素値は0に変換
        img = img.astype(np.uint8)
        cv2.imwrite(tmpfnm, img)

if __name__ == '__main__':
    app = QApplication(sys.argv)   
    filename = "../dcmdir1/Brain02"
    ex = DicomSegmentation(filename)
    sys.exit(app.exec_())



実行結果


GUI上から対象領域をクリックすると各ポイントがオレンジの線で結ばれます。




マウスで画像をクリックすたびに次のように座標がターミナルに表示されます。

$ python␣dicomSegmentation.py⏎
w= 512 h= 512
>>points= [[215, 88]]
>>points= [[215, 88], [212, 99]]
>>points= [[215, 88], [212, 99], [193, 113]]
>>points= [[215, 88], [212, 99], [193, 113], [180, 117]]
>>points= [[215, 88], [212, 99], [193, 113], [180, 117], [163, 106]]
>>points= [[215, 88], [212, 99], [193, 113], [180, 117], [163, 106], [160, 82]]



saveボタンを押すと2値画像のsegment.pngファイルが出力されます。




プログラム解説


__main__では,表示するDICOM画像のファイル名をfilenameに設定し,これを引数にDicomSegmentationクラスを呼び出しています。


__init__関数

ファイル名をself.dcmfnmに代入します。
マウスがポイントした一連の座標を格納するリストself.pointsを初期化します。

initUI関数

最初に,ファイル名などの情報を表示するためのラベルlblfnmとそれをレイアウトするfnmboxを作成します。

次に,DICOM画像を読み込みラベルlblimgに貼り付けます。
DICOM画像は,readDicom2png関数を使って,一旦pngファイルに書き出します。
このpngファイルをsetPixmapを使ってラベルlblimgにセットしています。
このpixmapの画像の幅と高さをそれぞれself.wとself.hに代入しています。
この幅と高さの値は二値画像で出力する際,同じサイズの画像を作成するに使います。

画像上のマウスイベントを有効にするため,setMouseTrackingをTrueにし,クリック時の処理を有効にするため,installEventFilterによりラベルlblimgに設定しています。

lblimgをレイアウトするにimgboxを作成して,lblimgをaddWidgetでセットしています。

次に,結果を二値画像として保存するボタンpntSvBtnと画像上の座標をクリアするボタンpntClrBtnを作成しています。
両ボタンともQPushButtonにより作成します。
クリックされた時の処理、呼び出される関数pointSaveButtonClicked関数とpointClearButtonClicked関数をclicked.connectにより結合しています。

これらのボタンはQHBoxLayoutにより水平に並べて配置するようにbtnboxに設定します。


最後に,すべてのBoxを垂直に配置するようにQVBoxLayoutによりmainVboxを作成して,fnmbox,imgboxとbtnboxをレイアウトします。
そしてmainVboxをウィンドにセットして表示しています。

eventFilter関数

マウスクリック時のイベントを取得する関数です。
イベントタイプがマウスがクリックされた(押された)QEvent.MouseButtonPressのとき,処理が行われます。

画像ラベルlblimg上でのクリックであることを判断するため次のif文を入れています。
    if object is self.lblimg:
今回,画像は1枚なので,このif文はなくとも問題ありませんが,複数の画像などを制御する場合には必要になるので,参考までにこのif文を入れておきました。

座標x,yをリストpointsに追加します。
    self.points.append( [ pos.x( ), pos.y( ) ] )
例えば,3つの座標がpointsに格納されている時,次のように二次元のリストで格納されています。
    [ [ x0,y0 ], [ x1, y1 ], [ x2, y2 ] ]
つまり,self.points[1]は [ x1, y1] のように取り出すことができます。

redisplay関数

マウスがポイントするたびに呼び出され,画像上の情報を更新します。

はじめのDICOM画像をpngに変換した画像dcm.pngをimgに読み込みます
    img = cv2.imread( "./dcm.png", 1 )
ここで,引数はファイル名"./dcm.png"とRGBカラーで読み込むことを「1」で指定しています。
RGBにするのは色のついた多角形をこの画像の上に描くためです。

polylinesメソッドは画像imgと座標pntを引数に渡して多角形を描画します。
    cv2.polylines(img, [pnt], True, (64,128,255))
このとき,描画する色RGBを(64,128,255)で指定しています。
お好みで変えてみると良いでしょう。

この画像imgを一旦pngに書きだしたあとQPixmapで読み込んでlblimgにセットしています。
この画像のラベルはクラス内で共有されているため,セットしたあと,self.show()を実行することにより画像が更新されます。

pointSaveButtonClicked関数

マウスでクリックして囲まれた領域を二値画像で保存する関数です。
画素値がすべてゼロの画像imgを作成します。
    img = np.full( ( self.w, self.h, 3 ), 0, dtype=np.uint8 )
最初の引数(self.w, self.h, 3)は画像の大きさをself.wとself.hで決定し,3チャンネルの画像(カラー画像)を表します。
次の引数「0」は画素が0であることを表します。
最後の引数のデータタイプdtypeは非符号8ビット整数(uint8)であることを表しています。

座標が格納されたリストpointsをfillPolyメソッドに渡すためにndarrayに変換します。
    pnt = np.array( self.points,  dtype = np.int64 )
ndarrayで変換する際、データタイプdtypeは64ビット整数に変換されます。

ndarrayの座標値pntと画像imgを引数に二値画像を作成します。
    cv2.fillPoly( img, [ pnt ], ( 255,255,255 ) )
fillPolyメソッドは座標点を結ぶ多角形(pnt)内をRGBを255,255,255で塗りつぶします。
ここでは画像ビュワで表示して確認しやすくするため,255を用いましたが,0と1の二値画像を作成したい場合は255を1にして(1,1,1)とします。

最後にOpneCVのメソッドimwriteを使ってpng画像に出力します。
    cv2.imwrite( "./segment.png", img )

pointClearButtonClicked関数

画像上のポイントされた情報を消す関数です。
リストpointsの値を初期化して,その状態でredisplay関数を呼び出すことにより、消えます。

readDicom2png関数

DICOM画像を読み込みpngファイルに出力します(2.01を参考)。


まとめ

注目する領域をマウスでクリックして二値画像を作成するプログラムを作成しました。
マウスでクリックした座標を取得する方法を学びました。
クリックした座標値をリストに追加して保持することがこのプログラムの重要な点です。
多角形を表示するOpenCVのfillPolyメソッドを容易に利用することができます。



課 題

1)ボタンを押すと,直近の点から一つづつ削除していく”Remove Point"ボタンを作成しなさい。

2)ボタンを押すと,ポイントした多角形の表示をOn/Offする”On/Off”ボタンを作成しなさい。




0 件のコメント:

コメントを投稿