マイブログ リスト

医療言語処理講座

2019年2月5日火曜日

Pyゼミ2.04 PyQt5によるDICOM画像の拡大表示と保存

DICOM画像の一部を拡大表示して画像を保存してみよう



Keyword: DICOM, マウスイベント, 画像拡大

小さな領域を拡大表示して観察したり,小さな領域を切り出して保存したりするプログラムを作ってみます。
画像中のある点をクリックすると,その点(座標x,y)を中心に決められたサイズの矩形領域を元の画像と同じ大きさに拡大して表示します。
拡大表示と同時に,矩形領域をpngファイルに保存します。
このとき画像ファイル名に座標x,yを付加します。


プログラム概要


はじめに イニシャライザ(__init__関数)は次の引数を持ちます。

  • 対象のDICOM画像ファイルのパス名:filename
  • 切り出す矩形領域のサイズ:cropSize


DICOM画像ファイルパス名からファイル名だけを取り出し,self.bsfnmに代入します。
    self.bsfnm  = os.path.basename( filename )

このself.bsfnmは矩形領域を画像保存するときのファイル名に利用します。

次に,initUI関数では,画像ラベルlblimg上でマウスのクリックイベントを得るために,次の2行を記述します。
    self.lblimg.setMouseTracking( True )
    self.lblimg.installEventFilter( self )

画像上でマウスをクリックすると呼び出されるeventFilter関数は,次の処理が行われます。
①マウスのクリックしした座標の取得
    pos = event.pos()
    x = pos.x()
    y = pos.y()
②切り出す矩形領域の左上の座標(topx, topy)と右下の座標(btmx, btmy)の算出
                topx = x - self.crpsz // 2
                topy = y - self.crpsz // 2
                btmx = x + self.crpsz // 2
                btmy = y + self.crpsz // 2
③矩形領域を画像imgから抽出してcrpimgへ代入
    crpimg = img[ topy : btmy ,  topx : btmx ]
リストimgからスライシングして矩形領域を得ます。
このとき,xとyが逆です。つまり行列の行はy,列はxとなるので,画像と行列は逆の関係にあるので注意が必要です。


起動すると


指定されたDICOM画像が左のウィンドウに現れれます。


画像中の任意の位置(バグあり,課題参照)でマウスをクリックすると右のウィンドウに拡大表示され,画像を保存するかダイアログが表示されます。



OKボタンをクリックすると,画像が保存される。





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


dicomMagnify.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 DicomMagnify(QWidget):

    def __init__(self, filename, cropSize):
        super().__init__()
        self.dcmfnm = filename                  # DICOMファイル名(パス)
        self.bsfnm  = os.path.basename(filename)# DICOMファイル名
        self.crpsz  = cropSize                  # 切り出しサイズ
        self.initUI()

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

        # DICOM画像読み込みラベルにセットする
        self.readDicom2png(self.dcmfnm, './dcm.png')    # DICOMを読んでpngに出力
        pixmap = QPixmap('./dcm.png')
        self.lblimg = QLabel()
        self.lblimg.setPixmap(pixmap)
        self.w = pixmap.width()
        self.h = pixmap.height()
        # 画像上のマウスイベントを取得する
        self.lblimg.setMouseTracking(True)
        self.lblimg.installEventFilter(self)
       
        #拡大画像表示ラベルの作成
        img = np.full((self.w, self.h, 1), 128,dtype = np.uint8)
        cv2.imwrite("./magnify.png", img)
        pixmap = QPixmap("./magnify.png")
        self.lblmag = QLabel()
        self.lblmag.setPixmap(pixmap)
       
        # 画像表示レイアウトの設定
        imgbox = QHBoxLayout()
        imgbox.addWidget(self.lblimg)
        imgbox.addWidget(self.lblmag)
       
        # 各パーツを配置する垂直なボックスを作成
        mainVbox = QVBoxLayout()       
        mainVbox.addLayout(fnmbox)
        mainVbox.addLayout(imgbox)

        # 垂直ボックスをウィンドウにレイアウトする
        self.setLayout(mainVbox)   
        self.setWindowTitle('DICOM Magnification')   
        self.show()
             
    #画像上でマウスクリックした座標を取得し,保存して画像表示する
    def eventFilter(self, object, event):
        if event.type() == QEvent.MouseButtonPress: # イベントタイプ
            pos = event.pos()                       # 座標の取得                        
            if object is self.lblimg:               # lblimg上でマウスがクリックされたら           
                x = pos.x()
                y = pos.y()
                print("x,y=",x, y)               
                topx = x - self.crpsz//2
                topy = y - self.crpsz//2
                btmx = x + self.crpsz//2
                btmy = y + self.crpsz//2
               
                img = cv2.imread("./dcm.png")
                crpimg = img[topy:btmy, topx:btmx]   # 画像の切り出し
                magimg = cv2.resize(crpimg, (self.h, self.w)) 
                cv2.imwrite("./magnify.png", magimg)   
                pixmap = QPixmap("./magnify.png")
                self.lblmag.setPixmap(pixmap)
                self.show()
                   
                #保存確認メッセージ
                svfnm = self.bsfnm+".x."+str(x)+".y."+str(y)+".png"
                btnMsg = QMessageBox.question(self, 'Save Image',
                    'Save Image?\nFile name: '+svfnm, 
                    QMessageBox.No | QMessageBox.Yes, QMessageBox.No)
                self.show()
                if btnMsg == QMessageBox.Yes:
                    cv2.imwrite(svfnm, crpimg)      
               
        return QWidget.eventFilter(self, object, event)
       
    # DICOM画像読み込んでウィンドニング後PNGに保存      
    def readDicom2png(self, 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)

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



実行結果


画像上でマウスをクリックすると,その座標を中心に拡大表示されます。
画像保存ダイアログが表示されるので,OKボタンをクリックするとDICOM画像名に座標が付加されたpngファイルが作成されます。


$ python␣dicomMagnify.py⏎
x,y= 190 89



プログラム解説


initUI関数

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

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

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

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


次に拡大表示用のラベルlblmagを作成します。
初期状態ではグレーの空画像を貼り付けます。
この空画像の大きさは元画像と同じself.w, self.hで,1チャンネルの画像,濃度は128の8ビット符号なし整数です。
    img = np.full( ( self.w, self.h,1), 128, dtype = np.uint8)
この画像を一旦,magnify.pngとして保存して,QPixmapで読み込み,ラベルlblmagに貼り付けています。

2つの画像ラベルはimgboxに水平にレイアウトします。

最後にmainVboxにfnmbox, imgboxをレイアウトします。


eventFilter関数

マウスがクリックされた(QEvent.MouseButtonPress)とき,この関数の処理が実行されます。
また,マウスのクリックがDICOM画像上(lblimg上)で行われたことを次のように知り処理が行われます。
    if object is self.lblimg:

クリックした座標を変数x,yに代入し,切り出す矩形の座標を計算します。

DICOM画像imgからスライシングを用いて次のように切り出します。
    crpimg = img[ topy : btmy,  topx : btmx ]

切り出した矩形画像を一旦,magnify.pngとして保存して,拡大画像表示ラベルlblmagにセットして,表示します。

画像の保存は,DICOM画像名bsfnmに座標xとyを付加して保存ファイル名svfnmを作成しました。
QMessageBox.questionを使って保存確認ダイアログを表示します。
このとき,画像ファイル名svfnmを表示します。
Yesボタンをクリックすると,crpimgがファイルに保存されます。


readDicom2png関数

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


おわりに

ここでは3つのこと学びました。

  1. 画像上のマウスイベントの設定の仕方
  2. マウスをクリックしたときのイベント処理
  3. 画像から任意の領域を抽出する方法
基本的な処理を学びましたが,イベントのタイプを変えることでいろいろな処理に対応できます。どのようなことができるのか考えてみましょう。


課 題

1)このプログラにはバグが存在する。もし,マウスが座標0,0をクリックした時,切り出す右上の座標はどちらも負の値をもち,切り出す範囲を超えてしまうためスライシングが上手くできない。同様にx,yが511,511のときも同じことが起きる。
切り出す矩形範囲が元画像の範囲からはみ出さないように処理をくわえなさい。
(※はみ出すとき警告を出し,切り出す処理を行わないようにしなさい)

>>> import cv2
>>>
>>> img = cv2.imread("./dcm.png") # DICOM画像の読み込み
>>> img.shape[:2]         # 画像サイズの表示
(512, 512)
>>> 
>>> x, y = 0, 0      # マウスクリックの座標
>>> 
>>> crpsz = 128//2    # 切り出すサイズ
>>> topx = x - crpsz
>>> topy = y - crpsz
>>> topx, topy      # 左上の座標
(-64, -64)        # 切り出す範囲が負の値になりはみ出している
>>> btmx = x + crpsz
>>> btmy = y + crpsz
>>> btmx,btmy       # 右下の座標
(64, 64)
>>> 
>>> crpimg = img[topy:btmy, topx:btmx] # 切り出し,エラーは起きない
>>> crpimg.shape[:2]   # 切り出した画像のサイズ
(0, 0)          # サイズが0,0なので上手く切りだされていない

0 件のコメント:

コメントを投稿