-
Notifications
You must be signed in to change notification settings - Fork 0
Tut3 Example1 1
_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
이번 예제에서는 Cyhub를 기반으로 'SimpleITK'를 활용하여 Dicom을 Load하고 'VTK'를 활용하여 Volume 렌더링 결과를 출력하는 예제를 학습하고자 합니다.
Reader, Volume Block을 구현 할 것이며, 이를 활용하는 Main Block을 구현 하는 과정을 다룰 것입니다.

- SimpleITK를 이용한 Dicom Read
- VTK를 활용한 Volume Render 결과 출력
- Transfer Function의 Preset 활용
아래와 같이 app_spec 파일의 각 항목들을 지정하여 다양한 Block 활용이 가능한 App을 구성 할 수 있습니다.
- 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- 기본적인 MainBlock의 특성은 앞선 예제와 같으므로 생략 (이전 Example 참고)
- Layout, Widget 구성
-
self.volume(Volume Block의 widget) 추가 :as_widget()사용 -
self.cb_transfer_function(Transfer Function Preset을 활용할 ComboBox) 추가 -
self.cb_transfer_function의activatedsignal과on_changed_transfer_comboslot 연결
-
on_changed_transfer_comboSLot
- ComboBox 클릭 시 발생하는 signal과 연결하기 위해
@cyhub_slot('QString')decorator 사용 - 외부의 다른 Block에 존재하는 signal과 연결하기 위해
@cyhub_slot('PyQt_PyObject')decorator 사용 -
self.volume(Volume Block의 widget)의set_transfer_presetmethod를 호출하기 위해 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())- 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()- Reader로 읽어들인 Dicom을 Volume Rendering 결과로 window 출력
- Volume Pipeline 구현 (Introduction의 그림 참조)
-
async def ready(self)method : Reader에서 다이콤 Load가 완료된 이후 파이프라인 연결 되어야 하므로 Async Call로 분리하여 처리 -
set_transfer_presetcyhub_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()본 예제를 가장 이상적으로 이해하기 위해서는 3대의 Machine 환경을 필요로 합니다.
하나의 Machine에서는 Coordinator를 실행할 것이며, 다른 하나의 Machine에서는 Manager를, 또다른 하나의 Machine에서는 Application을 실행 할 것입니다.
따라서, 3대의 Machine 환경에서 실행 가능하도록 환경이 구축되어 있어야 합니다.
(하지만, 편의상 Tutorial 시연을 위해 하나의 Machine에서 Coordinator, Manager, Application 모두 실행 가능)
또한 Reader Block가 위치한 coord/mgr/app machine에 Dicom Data가 위치해 있어야 합니다.
-
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 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'-
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 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'-
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'], {}- Coordinator가 위치한 machine에서 cycoord.py 파일을 실행 합니다.
- Manager가 위치한 machine에서 cycoord.py 파일을 실행 합니다.
- Application이 위치한 machine에서 cyapp.py 파일을 실행 합니다.
- Remote Rendering이 되고 있는 Block은 좌측 상단에 'R'이 표시 됩니다.
