Skip to content

Tut3 Example1 1

Sung-June, Lim edited this page Oct 14, 2016 · 17 revisions

File Composition for Tutorial3

_tutorials

_tutorial3

example1_1

apps

VolumeViewer.py

blocks

Reader.py
Volume_0_1.py Volume_app_0_1.py

cyapp.py
cyapp_cfg.py
cymgr.py
cymgr_cfg.py
cycoord.py
cycoord_cfg.py

Introduction

이번 예제에서는 Cyhub를 기반으로 'SimpleITK'를 활용하여 Dicom을 Load하고 'VTK'를 활용하여 Volume 렌더링 결과를 출력하는 예제를 학습하고자 합니다.
Reader, Volume Block을 구현 할 것이며, 이를 활용하는 Main Block을 구현 하는 과정을 다룰 것입니다.

Pipeline

Implementation

  • SimpleITK를 이용한 Dicom Read
  • VTK를 활용한 Volume Render 결과 출력
  • Transfer Function의 Preset 활용

Application Specification

아래와 같이 app_spec 파일의 각 항목들을 지정하여 다양한 Block 활용이 가능한 App을 구성 할 수 있습니다.

VolumeViewer.py

  • t(title) : Application Title은 'Volume Viewer'으로 사용
  • B(Block) : 'Volume_app_0_1'의 별칭을 'main'으로, 'Reader_0_1'의 별칭을 'reader'으로, 'Volume_0_1'의 별칭을 'volume'으로 사용
  • C(Connectivity) : reader와 volume은 같은 process에서 존재해야 함
  • D(Data) :
  • load할 dicom data를 argument로 받도록 지정
  • reader에서 data를 사용하도록 지정
  • 'reader' Block이 위치한 coord/mgr/app machine에 data가 위치해 있어야 함
  • R(Reference) :
  • 'main' Block에서 'reader' Block을 'reader' 라는 별칭으로 사용
  • 'main' Block에서 'volume' Block을 'volume' 라는 별칭으로 사용
  • 'volume' Block에서 'reader' Block을 'reader' 라는 별칭으로 사용
  • S(Slot) : -
def app_spec(dcm):
    t = 'Volume Viewer'
    B = [
        ('reader', 'Reader_0_1', [], {}),
        ('volume', 'Volume_0_1', [], {}),
        ('main', 'Volume_app_0_1', [], {}),
    ]

    C = [('reader', 'volume')]

    D = dict(
        reader=dict(dcm=dcm)
    )

    R = [
        ('main', 'reader', 'reader'),
        ('main', 'volume', 'volume'),
        ('volume', 'reader', 'reader'),
    ]
    S = [
    ]

    return t, B, C, D, R, S

Block Implementation

Volume_app_0_1.py (Main Block)

  • 기본적인 MainBlock의 특성은 앞선 예제와 같으므로 생략 (이전 Example 참고)
  • Layout, Widget 구성
  • self.volume (Volume Block의 widget) 추가 : as_widget() 사용
  • self.cb_transfer_function (Transfer Function Preset을 활용할 ComboBox) 추가
  • self.cb_transfer_functionactivated signalon_changed_transfer_combo slot 연결
  • on_changed_transfer_combo SLot
  • ComboBox 클릭 시 발생하는 signal과 연결하기 위해 @cyhub_slot('QString') decorator 사용
  • 외부의 다른 Block에 존재하는 signal과 연결하기 위해 @cyhub_slot('PyQt_PyObject') decorator 사용
  • self.volume (Volume Block의 widget)의 set_transfer_preset method를 호출하기 위해 Async Call 방식 사용 (Async Call의 자세한 설명은 Tut2 Example2 참고)
from PyQt5 import QtWidgets
from cyhub.block import Block_meta, cyhub_slot

from _qapp import create_task


class Volume_app_0_1(QtWidgets.QMainWindow, metaclass=Block_meta):
    def __init__(self, *args, **kwds):
        super().__init__(*args, **kwds)

    def initialize(self):

        # Organize layout.
        main_widget = QtWidgets.QWidget()
        layout_volume = QtWidgets.QVBoxLayout()

        # create components(widgets)
        self.cb_transfer_function = QtWidgets.QComboBox()
        self.cb_transfer_function.addItems(['MIP', 'CompositeRamp', 'CompositeShadeRamp', 'CT_Skin', 'CT_Bone'])
        self.cb_transfer_function.activated[str].connect(self.on_changed_transfer_combo)

        # add widgets
        layout_volume.addWidget(self.volume.as_widget())
        layout_volume.addWidget(self.cb_transfer_function)

        main_widget.setLayout(layout_volume)
        self.setCentralWidget(main_widget)

        # set margins
        layout_volume.setContentsMargins(0, 0, 0, 0)
        layout_volume.setSpacing(0)

        # Show.
        self.show()

    @cyhub_slot('QString')
    @cyhub_slot('PyQt_PyObject')
    def on_changed_transfer_combo(self, _str):
        async def _do():
            await self.volume.set_transfer_preset(_str)
        create_task(_do())

Reader_0_1.py

  • SimpleITK의 Image Reader를 활용한 Dicom Load
  • Load한 데이터를 vtk image로 변환
  • Dicom 데이터의 volume range, spacing, origin 정보 반환
from PyQt5 import QtCore

from cyhub.block import Block_meta, cyhub_method

import vtk
import SimpleITK


class Reader_0_1(QtCore.QObject, metaclass=Block_meta):

    def __init__(self, *args, **kwds):
        super().__init__(*args, **kwds)
        self.vtk_image = None

    def initialize(self, dcm):
        img = self.read(dcm)
        self.vtk_image = self.convert_to_vtkimage(img)

    @cyhub_method
    def get_vtkimage(self):
        return self.vtk_image

    def read(self, str_path):
        # Read by ITK
        if str_path:
            if r'.vtk' not in str_path:
                reader = SimpleITK.ImageSeriesReader()
                str_path = reader.GetGDCMSeriesFileNames(str_path)
                reader.SetFileNames(str_path)

            if len(str_path) == 0:
                return None

            img = SimpleITK.ReadImage(str_path)

            return img

    def convert_to_vtkimage(self, sitk_img=None):
        if sitk_img is None:
            return None

        data = SimpleITK.GetArrayFromImage(sitk_img)

        img_spacing = sitk_img.GetSpacing()
        img_cen = sitk_img.GetOrigin()

        img_data = data.astype('int16')
        img_string = img_data.tostring()
        dim = data.shape

        importer = vtk.vtkImageImport()
        importer.CopyImportVoidPointer(img_string, len(img_string))
        importer.SetNumberOfScalarComponents(1)

        extent = importer.GetDataExtent()
        importer.SetDataExtent(extent[0], extent[0] + dim[2] - 1,
                               extent[2], extent[2] + dim[1] - 1,
                               extent[4], extent[4] + dim[0] - 1)
        importer.SetWholeExtent(extent[0], extent[0] + dim[2] - 1,
                                extent[2], extent[2] + dim[1] - 1,
                                extent[4], extent[4] + dim[0] - 1)

        importer.SetDataSpacing(img_spacing[0], img_spacing[1], img_spacing[2])
        importer.SetDataOrigin(img_cen)

        # have to call 'Update'!!!
        importer.Update()

        return importer

    @cyhub_method
    def get_range_of_volume(self):
        """
        :return: xMin, xMax, yMin, yMax, zMin, zMax
        """
        return self.vtk_image.GetExecutive().GetWholeExtent(self.vtk_image.GetOutputInformation(0))

    @cyhub_method
    def get_spacing_of_volume(self):
        """
        :return: xSpacing, ySpacing, zSpacing
        """
        return self.vtk_image.GetOutput().GetSpacing()

    @cyhub_method
    def get_origin_of_volume(self):
        """
        :return: OriginX, OriginY, OriginZ
        """
        return self.vtk_image.GetOutput().GetOrigin()

Volume_0_1.py

  • Reader로 읽어들인 Dicom을 Volume Rendering 결과로 window 출력
  • Volume Pipeline 구현 (Introduction의 그림 참조)
  • async def ready(self) method : Reader에서 다이콤 Load가 완료된 이후 파이프라인 연결 되어야 하므로 Async Call로 분리하여 처리
  • set_transfer_preset cyhub_method : main에서 호출 받아 Transfer Function Preset을 적용 할 cyhub_method
from cyhub.block import Block_meta, cyhub_method
from _qapp import create_task

import vtk
from cyhub import cy_vtk


class Volume_0_1(cy_vtk.Vtk_image_holder, metaclass=Block_meta):
    def __init__(self, *args, **kwds):
        super().__init__(*args, **kwds)

    def initialize(self):
        self.vtk_img = None

        # basic component
        self.colorFun = vtk.vtkColorTransferFunction()
        self.opacityFun = vtk.vtkPiecewiseFunction()
        self.property = vtk.vtkVolumeProperty()

        # Smart Volume Mapper
        self.mapper = vtk.vtkSmartVolumeMapper()
        self.mapper.SetRequestedRenderMode(vtk.vtkSmartVolumeMapper.GPURenderMode)

        # Set transfer
        self.window_of_tf = 3070
        self.level_of_tf = 1535

        self.volume = vtk.vtkVolume()

        # Camera
        self.camera = vtk.vtkCamera()

        # init base pipeline
        create_task(self.ready())

    async def ready(self):
        self.vtk_img = await self.reader.get_vtkimage()
        await self.ready_for_basic_pipeline()

    async def ready_for_basic_pipeline(self):
        self.range_vol = await self.reader.get_range_of_volume()

        # Mapper
        self.mapper.SetInputConnection(self.vtk_img.GetOutputPort())

        # Set transfer
        self.set_transfer_preset("MIP")

        self.property.SetIndependentComponents(True)
        self.property.SetColor(self.colorFun)
        self.property.SetScalarOpacity(self.opacityFun)
        self.property.SetInterpolationTypeToLinear()

        # vtkVolume
        self.volume.SetMapper(self.mapper)
        self.volume.SetProperty(self.property)

        # Camera
        ex = self.range_vol
        self.camera = self.ren.GetActiveCamera()
        c = self.volume.GetCenter()
        self.camera.SetFocalPoint(c[0], c[1], c[2])
        self.camera.SetPosition(c[0] + (ex[1] / 4), c[1] - (ex[3]), c[2] + (ex[5] / 2))
        self.camera.SetViewUp(0, 0, 1)

        # Render
        self.ren.AddViewProp(self.volume)

    @cyhub_method
    def set_transfer_preset(self, _type):
        self.colorFun.RemoveAllPoints()
        self.opacityFun.RemoveAllPoints()
        self.property.SetIndependentComponents(True)

        if _type == "MIP":
            self.colorFun.AddRGBSegment(self.level_of_tf - 0.5*self.window_of_tf, 1.0, 1.0, 1.0,
                                        self.level_of_tf + 0.5*self.window_of_tf, 1.0, 1.0, 1.0)
            self.opacityFun.AddSegment(self.level_of_tf - 0.5*self.window_of_tf, 0.0,
                                       self.level_of_tf + 0.5*self.window_of_tf, 1.0)
            self.mapper.SetBlendModeToMaximumIntensity()
            self.property.ShadeOff()

        elif _type == "CompositeRamp":
            self.colorFun.AddRGBSegment(self.level_of_tf - 0.5*self.window_of_tf, 0.0, 0.0, 0.0,
                                        self.level_of_tf + 0.5*self.window_of_tf, 1.0, 1.0, 1.0)
            self.opacityFun.AddSegment(self.level_of_tf - 0.5*self.window_of_tf, 0.0,
                                       self.level_of_tf + 0.5*self.window_of_tf, 1.0)
            self.mapper.SetBlendModeToComposite()
            self.property.ShadeOff()

        elif _type == "CompositeShadeRamp":
            self.colorFun.AddRGBSegment(self.level_of_tf - 0.5 * self.window_of_tf, 0.0, 0.0, 0.0,
                                        self.level_of_tf + 0.5 * self.window_of_tf, 1.0, 1.0, 1.0)
            self.opacityFun.AddSegment(self.level_of_tf - 0.5 * self.window_of_tf, 0.0,
                                       self.level_of_tf + 0.5 * self.window_of_tf, 1.0)
            self.mapper.SetBlendModeToComposite()
            self.property.ShadeOn()

        elif _type == "CT_Skin":
            self.colorFun.AddRGBPoint(-2048, 0, 0, 0, 0.5, 0.0)
            self.colorFun.AddRGBPoint(-500, .62, .36, .18, 0.5, 0.0)
            self.colorFun.AddRGBPoint(0, .88, .60, .29, 0.33, 0.45)
            self.colorFun.AddRGBPoint(2048, .83, .66, 1, 0.5, 0.0)

            self.opacityFun.AddPoint(-2048, 0, 0.5, 0.0 )
            self.opacityFun.AddPoint(-500, 0, 0.5, 0.0 )
            self.opacityFun.AddPoint(0, 1.0, 0.33, 0.45 )
            self.opacityFun.AddPoint(2048, 1.0, 0.5, 0.0)

            self.mapper.SetBlendModeToComposite()
            self.property.ShadeOn()
            self.property.SetAmbient(0.1)
            self.property.SetDiffuse(0.9)
            self.property.SetSpecular(0.2)
            self.property.SetSpecularPower(10.0)
            self.property.SetScalarOpacityUnitDistance(0.8919)

        elif _type == "CT_Bone":
            self.colorFun.AddRGBPoint(-3024, 0, 0, 0, 0.5, 0.0)
            self.colorFun.AddRGBPoint(-16, 0.73, 0.25, 0.30, 0.49, .61)
            self.colorFun.AddRGBPoint(641, .90, .82, .56, .5, 0.0)
            self.colorFun.AddRGBPoint(3071, 1, 1, 1, .5, 0.0)

            self.opacityFun.AddPoint(-3024, 0, 0.5, 0.0)
            self.opacityFun.AddPoint(-16, 0, .49, .61)
            self.opacityFun.AddPoint(641, .72, .5, 0.0)
            self.opacityFun.AddPoint(3071, .71, 0.5, 0.0)

            self.mapper.SetBlendModeToComposite()
            self.property.ShadeOn()
            self.property.SetAmbient(0.1)
            self.property.SetDiffuse(0.9)
            self.property.SetSpecular(0.2)
            self.property.SetSpecularPower(10.0)
            self.property.SetScalarOpacityUnitDistance(0.8919)

        self.view._RenderWindow.Render()

How to run an App

본 예제를 가장 이상적으로 이해하기 위해서는 3대의 Machine 환경을 필요로 합니다. 하나의 Machine에서는 Coordinator를 실행할 것이며, 다른 하나의 Machine에서는 Manager를, 또다른 하나의 Machine에서는 Application을 실행 할 것입니다. 따라서, 3대의 Machine 환경에서 실행 가능하도록 환경이 구축되어 있어야 합니다. (하지만, 편의상 Tutorial 시연을 위해 하나의 Machine에서 Coordinator, Manager, Application 모두 실행 가능)
또한 Reader Block가 위치한 coord/mgr/app machine에 Dicom Data가 위치해 있어야 합니다.

coord_cfg 설정

  • server_addr : Coordinator로 사용 되고 있는 Machine에서 자신의 IP와 사용 가능한 Port를 에 입력
    ex) server_addr = ('211.106.178.85', 8080)
  • run_mgr : True로 설정하여 Coordinator에서 Manager를 생성
  • cycode_prefix : Tutorial 예제가 포함되어 있는 경로 지정 ('/' 대신 '.'으로 디렉토리 구분)
  • cyres_path : Coordinator에서 Dicom을 Load할 경우, 사용 될 Dicom Data 위치 입력
cycoord_cfg.py
# cycoord configure

server_addr = ('xxx.xxx.xxx.xxx', 8080)
conn_trace_message = True

run_mgr = True

# Specify these for running manager.
cycode_prefix = '_tutorials._tutorial3.example1_1'
cyres_path = '_cyres'

mgr_cfg 설정

  • server_addr : Manager로 사용 되고 있는 Machine에서 자신의 IP와 사용 가능한 Port를 에 입력
    ex) server_addr = ('211.106.178.85', 8090)
  • coord_addr : Manager로 사용 되고 있는 Machine에서 Coordinator 접속 정보를 에 입력
    ex) coord_addr = ('211.106.178.85', 8080)
  • cycode_prefix : Tutorial 예제가 포함되어 있는 경로 지정 ('/' 대신 '.'으로 디렉토리 구분)
  • cyres_path : Manager에서 Dicom을 Load할 경우, 사용 될 Dicom Data 위치 입력
cymgr_cfg.py
# cymgr configure

server_addr = ('xxx.xxx.xxx.xxx', 8090)
conn_trace_message = True

coord_addr = ('xxx.xxx.xxx.xxx', 8080)

cycode_prefix = '_tutorials._tutorial3.example1_1'
cyres_path = '_cyres'

app_cfg 설정

  • coord_addr : Coodinator로 사용 되고 있는 Machine의 접속정보를 에 입력
  • cycode_prefix : Tutorial 예제가 포함되어 있는 경로 지정 ('/' 대신 '.'으로 디렉토리 구분)
  • cyres_path : Application 에서 Dicom을 Load할 경우, 사용 될 Dicom Data 위치 입력
  • app : 실행 할 app 이름과 args, kwds 입력
  • args에 data 위치 입력
# cyapp configure

coord_addr = ('xxx.xxx.xxx.xxx', 8080)
mgr_addr = None
conn_trace_message = True

cycode_prefix = '_tutorials._tutorial3.example1_1'

# Specify this for running manager.
cyres_path = '_cyres'

app = 'VolumeViewer', ['cyres:dicom1'], {}

App 실행

  • Coordinator가 위치한 machine에서 cycoord.py 파일을 실행 합니다.
  • Manager가 위치한 machine에서 cycoord.py 파일을 실행 합니다.
  • Application이 위치한 machine에서 cyapp.py 파일을 실행 합니다.
  • Remote Rendering이 되고 있는 Block은 좌측 상단에 'R'이 표시 됩니다.
[Application]

Clone this wiki locally