領域抽出の設定、閾値設定を確認できるプログラムを組んでみた

matplotlib

はじめに

前回、領域抽出の記事を書きましたが初めにやる処理、閾値設定がうまくいかないと抽出結果が思い通りにいかない結果となってしまいます。

ということで、今回はその閾値設定を可視化できるプログラムを組んでみたという記事です。

前回はjpg画像を使用しましたが、今回はDICOM画像を用いて行ってみたいと思います。


プログラムの流れ

今回の最終形はスライダーを用いて閾値設定をし視覚的に確認する事を目標にします。その為、元の画像と、閾値設定をした画像の2画像を表示し、片方の画像に閾値設定を適応することにします。どうせですので、閾値設定じゃない方の画像のWW,WLを調整できるようにスライダーを設定します。

完成図

それでは、流れです

  1. DICOM画像を読み込み
  2. matplotlibの設定
  3. tkinterの設定(領域設定と、スライダー設定)
  4. スライダー変更時の動作設定
  5. 仕上げ


DICOM画像を読み込み

DICOM画像の読み込みですが、以前作成したモジュールを使用します。今回は1枚だけ読み込みをします。

import pydicom
import numpy as np
import fileselect as fs

def main():

    filename = fs.single_fileselect()
    dcm = pydicom.dcmread(filename)
    img_arr = dcm.pixel_array
    w_level = int(dcm[0x0028, 0x1050].value)
    w_width = int(dcm[0x0028, 0x1051].value)

if __name__ == "__main__":
    main()

画像を表示した際、適正条件で表示するためにウインドウレベルとウインドウ幅の情報もDICOMタグから取り込んでおきます。


matplotlibの設定

matplotlibの設定をします。設定項目は画像全てを表示する領域サイズの指定

画像領域の設定、軸の表示設定をします。

まずは、画像全てを表示する領域サイズの指定です。

fig = plt.figure(figsize=(12, 6))

表示領域は横12インチ、縦6インチとしました。

続いて、画像表示数はオリジナル画像と、閾値設定した画像の2枚を表示しますので2つ設定します。

ax1 = fig.add_subplot(1, 2, 1)
ax2 = fig.add_subplot(1, 2, 2)

そして、それぞれの画像に軸の表示はいらないので

ax1.axes.xaxis.set_visible(False), ax1.axes.yaxis.set_visible(False)
ax2.axes.xaxis.set_visible(False), ax2.axes.yaxis.set_visible(False)

で軸の表示を無くしておきます。

まずは、matplotlibをインポートしてからコードを足していきましょう。

import pydicom
import fileselect as fs
import matplotlib.pyplot as plt

def main():

    filename = fs.single_fileselect()
    dcm = pydicom.dcmread(filename)
    img_arr = dcm.pixel_array
    w_level = int(dcm[0x0028, 0x1050].value)
    w_width = int(dcm[0x0028, 0x1051].value)

    fig = plt.figure(figsize=(12, 6))
    ax1 = fig.add_subplot(1, 2, 1)
    ax2 = fig.add_subplot(1, 2, 2)

    ax1.axes.xaxis.set_visible(False), ax1.axes.yaxis.set_visible(False)
    ax2.axes.xaxis.set_visible(False), ax2.axes.yaxis.set_visible(False)

if __name__ == "__main__":
    main()


tkinterの設定(領域設定と、スライダー設定)

tkinterの領域設定は、先ほど設定したmatplotlibの領域と、この後設定するスライダーを合わせた領域を設定していきます。

root = tk.Tk()
root.geometry(“1410×600”)

まずは、tk.TK()で領域を作成します。その後、geometryで領域のサイズを指定しています。


Canvas = FigureCanvasTkAgg(fig, master=root)
Canvas.get_tk_widget().grid(row=0, column=0, rowspan=10)

FigureCanvasTkAgg(fig, master=root)でtkinter上にmatplotlibの画像の表示設定します。

ライブラリーのインポートを忘れないでくださいね。

from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg

import pydicom
import numpy as np
import fileselect as fs
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg

def main():

    filename = fs.single_fileselect()
    dcm = pydicom.dcmread(filename)
    img_arr = dcm.pixel_array
    w_level = int(dcm[0x0028, 0x1050].value)
    w_width = int(dcm[0x0028, 0x1051].value)

    fig = plt.figure(figsize=(12, 6))
    ax1 = fig.add_subplot(1, 2, 1)
    ax2 = fig.add_subplot(1, 2, 2)

    ax1.axes.xaxis.set_visible(False), ax1.axes.yaxis.set_visible(False)
    ax2.axes.xaxis.set_visible(False), ax2.axes.yaxis.set_visible(False)

    root = tk.Tk()
    root.geometry("1410x600")

    Canvas = FigureCanvasTkAgg(fig, master=root)
    Canvas.get_tk_widget().grid(row=0, column=0, rowspan=10)

if __name__ == "__main__":
    main()


続いてスライダー(tkinterではスケールと言います)の作成及び、設定をしたいと思います。

設定を始める前に知っておいてもらいたい事があります。

それは、スライダーという部品を構成するコードと、スライダーの値を格納するオブジェクトが必要だという事です。なので、まずは格納するオブジェクトを設定しておいて、その後スライダーの部品構成の中で値を格納するオブジェクトを指定しておく必要があります。

まず、オブジェクトを生成します。

オブジェクト名 = tk.IntVar()

IntVar()とは、整数値であるという事を宣言しています。

続いて、スライダー(スケール)部品の設定です。

変数名 = tk.Scale(オプション)

で設定を行います。オプションは以下となります。

第1引数配置場所
labelスケールのラベル
from_スケールの最小値
toスケールの最大値
orient配置方向
tkinter.HORIZONTAL tkinter.VERTICAL(デフォルト)
lengthスケールの長さです。
showvalue値の表示、非表示
variableスケールの値を格納するインスタンス名
command値が変更した時の実行関数
resolution解像度


配置場所は、tkinterのcanvasとなりますのでrootとなります。

labelはウインドレベルと、ウインドウ幅、スレッショルドのスケールを作成しますのでそれぞれ、’Window Level ’、’Window Width’、’Threshold’としましょう。

スケールの最小値は、ウインドウ幅は0,ウインドウレベルとスレッショルドは画像信号値の最小値とします。

スケールの最大値は、ウインドウ幅には画像信号値の最小値から最大値までの半分、ウインドウレベル、スレッショルドは、画像信号値の最大値

orientは横方向としますので、tk.HORIZONTAL

長さは200ピクセルとします。

コマンドは、ウインドウ幅、ウインドウレベルで共有した関数windowを作成、スレッショルドは関数thresholdで作成します。

残りの引数の指定は省略してしまいましょう。

それでは、コードです。

まずはウインドウレベルから

var_scale_level = tk.IntVar()

level_sc = tk.Scale(root,
    label='Window Level',
    variable=var_scale_level,
    orient=tk.HORIZONTAL,
    length=200,
    from_= np.min(img_arr),
    to=np.max(img_arr),
    command=window)

level_sc.set(w_level)

level_sc.grid(row=1, column=1)

同様にウインドウ幅

var_scale_w_width = tk.IntVar()

w_width_sc = tk.Scale(root,
    label = 'Window Width',
    variable=var_scale_w_width,
    orient=tk.HORIZONTAL,
    length=200,
    from_=0,
    to=(np.max(img_arr) - np.min(img_arr))//2,
    command=window)

w_width_sc .set(w_width)

w_width_sc.grid(row=3, column=1)

スレッショルドは

thre = 0

var_thre = tk.IntVar()
thre_sc = tk.Scale(root,
label='Threshold',
variable=var_thre,
orient=tk.HORIZONTAL,
length=200,
from_=np.min(img_arr),
to=np.max(img_arr),
command=thresho)
thre_sc.set(thre)
thre_sc.grid(row=5, column=1)


ここまでのコードです

import pydicom
import fileselect as fs
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import numpy as np

def main():

    filename = fs.single_fileselect()
    dcm = pydicom.dcmread(filename)
    img_arr = dcm.pixel_array
    w_level = int(dcm[0x0028, 0x1050].value)
    w_width = int(dcm[0x0028, 0x1051].value)

    fig = plt.figure(figsize=(12, 6))
    ax1 = fig.add_subplot(1, 2, 1)
    ax2 = fig.add_subplot(1, 2, 2)

    ax1.axes.xaxis.set_visible(False), ax1.axes.yaxis.set_visible(False)
    ax2.axes.xaxis.set_visible(False), ax2.axes.yaxis.set_visible(False)

    root = tk.Tk()
    root.geometry("1410x600")

    Canvas = FigureCanvasTkAgg(fig, master=root)
    Canvas.get_tk_widget().grid(row=0, column=0, rowspan=10)

    var_scale_level = tk.IntVar()
    level_sc = tk.Scale(root,
        label='Window Level',
        variable=var_scale_level,
        orient=tk.HORIZONTAL,
        length=200,
        from_= np.min(img_arr),
        to=np.max(img_arr),
        command=window)
    level_sc.set(w_level)
    level_sc.grid(row=1, column=1)

    var_scale_w_width = tk.IntVar()
    w_width_sc = tk.Scale(root,
        label = 'Window Width',
        variable=var_scale_w_width,
        orient=tk.HORIZONTAL,
        length=200,
        from_=0,
        to=(np.max(img_arr) - np.min(img_arr))//2,
        command=window)
        w_width_sc .set(w_width)
        w_width_sc.grid(row=3, column=1)


    var_thre = tk.IntVar()
    thre_sc = tk.Scale(root,
        label='Threshold',
        variable=var_thre,
        orient=tk.HORIZONTAL,
        length=200,
        from_=np.min(img_arr),
        to=np.max(img_arr),
        command=thresho)
        thre_sc.set(thre)
        thre_sc.grid(row=5, column=1)

if __name__ == "__main__":
    main()


スケール(スライダー)変更時の動作設定

スケールは3つ作成しましたが、関数はウインドウレベル、ウインドウ幅を変更した時の関数と、スレッショルドを変更した時の関数の2つを作成します。

まずは、ウインドウ幅、ウインドウレベルを変更した時の関数です。

matplotlibで画像を表示する際に、表示信号幅はvmin,vmaxで指定します。

ax1.imshow(img_arr, cmap=’bone’, vmin=ww_low, vmax=ww_high)

表示の最小値、最大値ですので、ウインドウレベルとウインドウ幅から最小値と最大値を計算しなければなりません。

それら計算は初めの画像表示時とウインドウレベル、ウインドウ幅のスケールを変更した時と何回も使用しますので、これも関数化しておきましょう。

関数名はlevel()として作成してみました。

def level():
    global ww_low, ww_high, w_level, w_width

    window_level, window_width = level_sc.get(), w_width_sc.get()

    ww_low = window_level - (window_width // 2)
    ww_high = window_level + (window_width // 2)

スケールの値をそれぞれ、window_level, window_widthとして取得します。その後、ww_low, ww_highとして計算します。

スケールの値はget()関数で取得します。上記コードでは1行で取得していますが

window_level = level_sc.get()

window_width = w_width_sc.get()

という感じです。

window_level, window_width, ww_low, ww_highは他の関数とも共有しますのでglobal宣言しておきます。(上記コード2行目)メイン関数の方でも同様にglobal宣言を足しておきましょう。

それでは、まずウインドウ幅、ウインドウレベルのスケールを変更した時の処理に入ります。

関数名はwindow()としました。

スケール指定される関数は、引数としてスケールの値が返されますので、関数の引き受ける引数としてself(名前はなんでもOK)を指定しておきます。

def window(self):

ってかんじです。

しかし、今回は2つのスケールから共通の関数として作成しているのでこの引数は使用しません。

まずは、この関数が呼び出された時に先ほど作成した関数level()でww_lowとww_highの値を計算させましょう。

level()

で、先ほどの関数を呼び出します。ただ、この関数内でもww_lowとww_highをglobal宣言しておかないと計算結果を使えませんので忘れずに宣言しておいてください。

後は、matplotlibの表示設定です。左側に元画像を表示するのでax1に設定します。

ax1.imshow(img_arr, cmap=’bone’, vmin=ww_low, vmax=ww_high)

続いて、figの更新するコードこれを忘れては表示が更新されませんので忘れずに。

fig.canvas.draw()


続いて閾値設定の関数です。関数名はthreshold()で作成します。

先ほど同様、スケールの値が引数と渡されますので

def threshold(self):

としておきます。

閾値設定は前々回の記事で書いていますのでご覧ください。

openCVで輪郭抽出をやってみた


import pydicom
import fileselect as fs
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import numpy as np

def level():
    global ww_low, ww_high, w_level, w_width

    window_level, window_width = level_sc.get(), w_width_sc.get()

    ww_low = window_level - (window_width // 2)
    ww_high = window_level + (window_width // 2)

def window(self):
    global ww_low, ww_high, img_arr, ax1

    level()

    ax1.imshow(img_arr, cmap='bone', vmin=ww_low, vmax=ww_high)
    fig.canvas.draw()

def thresho(self):
    global img_arr, ax2, fig

    ret, thresh = cv2.threshold(img_arr, int(self), 255, cv2.THRESH_BINARY)
    ax2.imshow(thresh, cmap='bone')

    fig.canvas.draw()

def main():
    global ww_low, ww_high, img_arr_copy, thre_sc, ax2, fig, img_arr, ax1, level_sc, w_width_sc, ww_low, ww_high, w_level, w_width

    filename = fs.single_fileselect()
    dcm = pydicom.dcmread(filename)
    img_arr = dcm.pixel_array
    w_level = int(dcm[0x0028,0x1050].value)
    w_width = int(dcm[0x0028, 0x1051].value)

    fig = plt.figure(figsize=(12, 6))
    ax1 = fig.add_subplot(1, 2, 1)
    ax2 = fig.add_subplot(1, 2, 2)

    ax1.axes.xaxis.set_visible(False), ax1.axes.yaxis.set_visible(False)
    ax2.axes.xaxis.set_visible(False), ax2.axes.yaxis.set_visible(False)

    root = tk.Tk()
    root.geometry("1410x600")

    Canvas = FigureCanvasTkAgg(fig, master=root)
    Canvas.get_tk_widget().grid(row=0, column=0, rowspan=10)

    var_scale_level = tk.IntVar()
    level_sc = tk.Scale(root,
        label='Window Level',
        variable=var_scale_level,
        orient=tk.HORIZONTAL,
        length=200,
        from_= np.min(img_arr),
        to=np.max(img_arr),
        command=window)
    level_sc.set(w_level)
    level_sc.grid(row=1, column=1)

    var_scale_w_width = tk.IntVar()
    w_width_sc = tk.Scale(root,
        label = 'Window Width',
        variable=var_scale_w_width,
        orient=tk.HORIZONTAL,
        length=200,
        from_=0,
        to=(np.max(img_arr) - np.min(img_arr))//2,
        command=window)
        w_width_sc .set(w_width)
        w_width_sc.grid(row=3, column=1)


    var_thre = tk.IntVar()
    thre_sc = tk.Scale(root,
        label='Threshold',
        variable=var_thre,
        orient=tk.HORIZONTAL,
        length=200,
        from_=np.min(img_arr),
        to=np.max(img_arr),
        command=thresho)
        thre_sc.set(thre)
        thre_sc.grid(row=5, column=1)

if __name__ == "__main__":
    main()


仕上げ

いよいよ、仕上げ作業です。

画像を読み込んだ際に先ほど作成した関数を呼び出さなければ画像表示できませんので、それを付け加えていきたいと思います。

上記コードmain関数の最後にウインドウ幅とウインドウレベルのスケールを変更した時の関数を呼び出しましょう。ただ、何か引数を与えないとエラーとなってしまいますので適当に1を渡しておきます。

同様に、閾値設定した画像を表示する関数を呼び出します。

window(1)

thresho(1)

これで完成となります。


完成コード

import tkinter as tk
import cv2
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import numpy as np
import pydicom
import fileselect as fs


def level():
    global ww_low, ww_high, w_level, w_width

    window_level, window_width = level_sc.get(), w_width_sc.get()

    ww_low = window_level - (window_width // 2)
    ww_high = window_level + (window_width // 2)

def window(self):
    global ww_low, ww_high, img_arr, ax1

    level()

    ax1.imshow(img_arr, cmap='bone', vmin=ww_low, vmax=ww_high)
    fig.canvas.draw()

def thresho(self):
    global img_arr, ax2, fig

    ret, thresh = cv2.threshold(img_arr, int(self), 255, cv2.THRESH_BINARY)
    ax2.imshow(thresh, cmap='bone')

    fig.canvas.draw()


def main():
    global ww_low, ww_high, img_arr, thre_sc, ax2, fig,\
        img_arr, ax1, level_sc, w_width_sc, ww_low, ww_high, w_level, w_width


    filename = fs.single_fileselect()
    dcm = pydicom.dcmread(filename)
    img_arr = np.array(dcm.pixel_array)
    w_level = int(dcm[0x0028,0x1050].value)
    w_width = int(dcm[0x0028, 0x1051].value)

    fig = plt.figure(figsize=(12, 6))
    ax1 = fig.add_subplot(1, 2, 1)
    ax2 = fig.add_subplot(1, 2, 2)

    ax1.axes.xaxis.set_visible(False), ax1.axes.yaxis.set_visible(False)
    ax2.axes.xaxis.set_visible(False), ax2.axes.yaxis.set_visible(False)


    root = tk.Tk()
    root.geometry("1410x600")

    Canvas = FigureCanvasTkAgg(fig, master=root)
    Canvas.get_tk_widget().grid(row=0, column=0, rowspan=10)


    var_scale_level = tk.IntVar()
    level_sc = tk.Scale(root,
        label='Window Level',
        variable=var_scale_level,
        orient=tk.HORIZONTAL,
        length=200,
        from_= np.min(img_arr),
        to=np.max(img_arr),
        command=window)
    level_sc.set(w_level)
    level_sc.grid(row=1, column=1)

    var_scale_w_width = tk.IntVar()
    w_width_sc = tk.Scale(root,
        label = 'Window Width',
        variable=var_scale_w_width,
        orient=tk.HORIZONTAL,
        length=200,
        from_=0,
        to=(np.max(img_arr) - np.min(img_arr))//2,
        command=window)
    w_width_sc .set(w_width)
    w_width_sc.grid(row=3, column=1)


    thre = 0

    var_thre = tk.IntVar()
    thre_sc = tk.Scale(root,
        label='Threshold',
        variable=var_thre,
        orient=tk.HORIZONTAL,
        length=200,
        from_=np.min(img_arr),
        to=np.max(img_arr),
        command=thresho)
    thre_sc.set(thre)
    thre_sc.grid(row=5, column=1)

    window(1)
    thresho(1)

    root.mainloop()

if __name__ == "__main__":
    main()


最後に

いかがでしたか?きちんとコード動きましたか?

お疲れ様でした。


環境

  • windows10
  • python3.6.1
  • Anaconda custom(64-bit)
  • PyCharm2020.2(Communication Edition)

タイトルとURLをコピーしました