본문 바로가기
Ect./Library

pynetdicom 을 사용하여 DICOM Networking 시도

by 쿠리의일상 2023. 10. 7.

https://pydicom.github.io/pynetdicom/stable/tutorials/installation.html

 

How to install pynetdicom — pynetdicom 2.0.3 documentation

© Copyright 2018-2022, pynetdicom contributors.

pydicom.github.io

 

설치하기

DICOM 통신이므로 다루기위해 우선 pydicom 이 있어야 한다.

pip install -U pynetdicom

python -m pip install -U pynetdicom

 

pynetdicom이란

DICOM 네트워크를 구축하기 위한 프로토콜 등 도구들을 제공

TCP/IP 용 DICOM 상위 계층 프로토콜

Application Entity(AE)로 DICOM 통신을 처리할 수 있다.

ae = AE()

이렇게 만들어진 ae 인스턴스는 1.서비스 클래스 컨텍스트 2. 이벤트 핸들러를 설정하여 SCP/SCU 로 사용할 수 있다. 

 

SCP/SCU ?

  • SCP
    • SCU의 요청에 응답하여 서비스를 제공
    • 이미지의 저장, 검색, 등 요청을 수신하고 처리
  • SCU
    • DICOM 서비스에 대한 요청
    • 의료 이미지 검색, 저장 등의 SCP 기타 작업을 수행하기 위한 요청 쿼리

 

간단한 SCP

from pydicom.uid import ExplicitVRLittleEndian
from pynetdicom import AE, debug_logger
from pynetdicom.sop_class import CTImageStorage

debug_logger()
ae = AE()
ae.add_supported_context(CTImageStorage, ExplicitVRLittleEndian)
ae.start_server(("127.0.0.1", 11112), block=True)
  • ae.add_supported_context() 메서드는 이미지 저장 서비스 클래스 컨텍스트는 SCP로 지원을 추가한다는 의미
    • CTImageStorage의 sop_class 로 불러와서 매개변수에 넣어줌으로써 CT 스캔 이미지를 지원하는 의미가 됨
    • ExplicitVRLittleEndian 의 UID 또한 마찬가지로 SCP에 지원해준다는 의미
  • ae.start_server() 에는 튜플형태로 주소, 포트를 넣고 block을 true로 설정해주면 SCP 서버가 열리게 된다.

 

간단한 SCU

from pydicom.uid import ExplicitVRLittleEndian
from pynetdicom import AE, debug_logger
from pynetdicom.sop_class import CTImageStorage

debug_logger()

ae = AE()
ae.add_requested_context(CTImageStorage)
assoc = ae.associate("127.0.0.1", 11112)

if assoc.is_established:
  print('Association established with Echo SCP!')
  status = assoc.send_c_echo()
  assoc.release()
else:
  print('Failed to associate')
  • ae.add_requested_context() 메서드는 동일하게 sop_class를 매개변수로 넣어줘서 SCU 요청이 가능하게 만드는 메서드이다.
    • 즉, SCU로 작동하는 AE가 다른 AE에게 요청하는 DICOM 서비스 클래스를 설정하고 요청하는데 사용되는 것
  • ae.associate() 로 연결을 요청한다. 이 메서드를 통해 AE 간 통신이 시작된다.
  • assoc.is_established 로 연결의 성공 여부를 boolean 값으로 받아서 분기문으로 처리한다.
  • assoc.send_c_echo() 메서드는 DICOM Echo 서비스를 요청하는 메서드로 네트워크 연결 및 DICOM 연합 상태를 확인하기 위해 사용된다. 즉, SCU와 SCP가 통신이 가능한지 확인하는 용도이다. (단순하게 Echo 메세지를 보내는 것)
  • assoc.release() 메서드는 연결을 해제하는 것

 

 

SCP를 틀어놓고 아래와 같이 명령해주면 dcm 파일을 SCP에 보내주게 된다. 아래와 같은 값을 반환한다. 확인해보면 당연하게도 0xC211 로 Failure 가 떴다.

python -m pynetdicom storescu 127.0.0.1 11112 파일명.dcm -v -cx

  1. Requesting Association 요청 연결
  2. Association Accepted 요청 받아들임
  3. Sending file dcm 파일을 보내줌
  4. Sending Store Request 저장 요청
  5. Received Store Response 저장 응답 -----> 실패
  6. Releasing Association 연결 해제

SCP에서 debug를 확인해보면 위같은 E: 가 보인다. 에러 내용을 담고 있다.

SCU에서 정상적으로 dcm 파일을 SCP에 보내줬으나 SCP에 받아준 dcm 파일을 처리해줄 수 있는 방법이 없다는(저장 응답에서 실패가 떴으니) 것을 알 수 있다.

즉 실패의 이유는 evt.EVT_C_STORE 이벤트 핸들러가 없기 때문에 발생한 것이다.

 

SCP의 evt를 설정

from pynetdicom import AE, debug_logger, evt

pynetdicom에 evt를 추가한다. evt.EVT_C_STORE 형식으로 접근해준다.

def handle_store(event):
  """events."""
  return 0x0000

handlers = [(evt.EVT_C_STORE, handle_store)]

ae = AE()
ae.add_supported_context(CTImageStorage, ExplicitVRLittleEndian)
ae.start_server(("127.0.0.1", 11112), block=True, evt_handlers=handlers)
  • ae.start_server 안에 evt_handlers에 배열로 지정해준 handlers를 넣어준다.
  • handlers 에 이벤트 핸들러는 Tuple 형태로 (이벤트핸들러종류, 이벤트핸들러함수) 로 지정해준다.
  • 이벤트 핸들러 함수로 만들어준 handle_store 함수는 0x0000 을 리턴해주는데 이는 성공했다는 의미이다.
    • 현재 위의 상태로는 아직 저장되지 않는다. 주석 위치에서 저장해주는 로직을 추가해줘야 함
    • 위의 실패를 임시적으로 무마해준다.

 

 

핸들러 완성하기

def handle_store(event):
  ds = event.dataset
  ds.file_meta = event.file_meta
  ds.save_as(ds.SOPInstanceUID, write_like_original=False)
  return 0x0000
  • event.dataset 은 피디콤 데이터셋으로 SCU에서 수신한 디코딩된 데이터셋
  • event.file_meta는 호환되는 파일 메타 정보 요소를 포함하는 데이터셋
  • ds.save_as() 메서드에 첫번째 매개변수로는 파일 이름이 되어줄 ds.SOPInstanceUID를, 두번째 매개변수로는 write_like_original을 False 로 지정해준다.
    • write_like_original 이 False여야 DICOM 파일 형식으로 작성됨.

위 핸들러를 기반으로 요청을 보내면 SCP 파일 위치에 SOPInstanceUID 의 이름을 가진 dcm 파일이 만들어지는 것을 확인할 수 있다. 정상적으로 저장된 것이다!

 

이제 원래의 SCP의 add_supported_context() 의 종류를 늘리자. (지금은 오직 CTImageStorage 만 가능하므로)

// ...
from pynetdicom import AE, debug_logger, evt, AllStoragePresentationContexts, ALL_TRANSFER_SYNTAXES
// ...
storage_sop_classes = [cx.abstract_syntax for cx in AllStoragePresentationContexts]
for uid in storage_sop_classes:
  ae.add_supported_context(uid, ALL_TRANSFER_SYNTAXES)
  • AllStoragePresentationContexts는 미리 빌드된 프레젠테이션 컨텍스트의 목록 - 스토리지 서비스의 모든 SOP 클래스에 대해 하나씩 있음.
    • 그러나 기본적으로 이러한 컨텍스트는 비압축 전송 구문만 지원
    • 압축 전송 구문과 비압축 전송 구문을 모두 지원하려면 추상 구문을 분리한 다음 ALL_TRANSFER_SYNTAXES를 대신 사용한다.

 

일단 가장 간단한 SCP와 SCU를 만들어 보았다.