본문 바로가기

Python

Python으로 DICOM 파일 다루는 방법

320x100
320x100

 

 

 DICOM 파일을 다룰 일이 생겨 공부 중이다.

지금 다루는 영상의 Modality가 CT이기 때문에 CT를 중심으로 정리해두고자 한다.

 

 이번 글에서는

  1. Hounsfield Unit (하우슨필드 유닛)
  2. Window Center, Window Width
  3. DICOM 파일 읽는 방법

에 대해 정리하도록 하겠다.

 

 

Hounsfield Unit ?

 

 우선 CT의 픽셀 값은 일반 RGB 이미지와는 다르다. RGB 이미지의 각 픽셀은 0 ~ 255의 값으로 이루어져 있지만, CT는 Hounsfield Unit (HU)라는 단위로 이루어져 있다. HU란 X 선이 몸을 투과할 때 감쇠되는 정도를 나타내는 단위이다. 물을 통과할 때를 0. 으로 두고 다른 부위의 감쇠정도를 상대적으로 표현한다. 이미 실험적(?)으로 정리되어 있어서 부위별 HU 값을 확인하고 싶다면 아래를 참고하면 된다.

 

[출처]

 

 DICOM viewer를 통해 CT slice를 출력해서 픽셀 값들을 직접 확인해보면 부위별로 비슷한 HU 값이 출력되는 걸 확인할 수 있다.

 

Window Center, Window Width

 

 위에서 말했 듯 CT 영상의 픽셀들은 HU 값으로 구성되어 있기 때문에 보고 싶은 신체 부위가 있다면 HU table을 참고해 Window Center와 Window Width를 조절한 뒤에 그 부분 위주로 출력해줄 수 있다. Window Center는 보고 싶은 부위의 HU 값을 의미하고, Window Width는 WC를 중심으로 관찰하고자 하는 HU 범위를 의미한다.

 

 예를 들어, 폐 부위를 중심으로 보고 싶다면 HU table 상 폐는 -600 ~ -400 이므로 Window Center는 -600으로 잡고, Window Width는 1600으로 잡아주면 된다. 그럼 WC를 중심으로 WW의 범위만큼을 중심적으로 표현해준다.

 

[출처]

 

 

DICOM 파일 읽는 방법

 

 먼저 파이썬으로 DICOM 파일을 다룰 수 있도록 도와주는 pydicom library를 다운받도록 하자. 나는 pydicom 2.0.0 version을 다운받았다. ( 내 기억으로 apply_voi_lut( )이 ver1.14.0에서부터 있는 거로 알고 있다) 그리고 예제를 위해 여기에서 무료 CT DICOM 파일을 다운받자.

 

코드는 다음의 흐름으로 구성되어 있다.

 많은 블로그에서는 DICOM을 numpy array로 변경하는 것까지만 설명하고 있는데, 뒤따라오는 Rescale Slope, Window Center와 같은 DICOM 속성을 적용해주지 않으면 DICOM Viewer (imageJ, MicroDicom 등)로 출력한 이미지와 pydicom.read_file( )로 출력한 이미지 사이에 contrast 및 brightness 차이가 발생하게 되니 신경써줘야한다. 그리고 apply_voi_lut( )은 무조건 적용해야한다. 무조건이다. 무조건! 무조건!!

 

※ 사실 Pixel Representation, Bits Stored와 같은 속성도 따져줘야하는데 이 부분은 아직 제대로 파악하지 못해서 그냥 넘어간다. 근데 이걸 apply_voi_lut( )에서 잡아준다고!!

 

※ < 2021.10.25. 추가 > apply_voi_lut( ) 이전에 apply_modality_lut( )를 적용해줘야 한다고 한다!!

 

※ < 2022.11.16. 추가 > apply_modality_lut( ) → apply_voi_lut( ) 순서로 적용해야하는 이유는 여기에서 잘 설명해주고 있다.

 

이제 코드를 봐보자.

import numpy as np
import cv2, pydicom
import matplotlib.pyplot as plt
from pydicom.pixel_data_handlers.util import apply_modality_lut, apply_voi_lut

window_center = -600
window_width = 1600

# CT image
dicom_path = './1-29.dcm'
slice = pydicom.read_file(dicom_path)
s = int(slice.RescaleSlope)
b = int(slice.RescaleIntercept)
image = s * slice.pixel_array + b

plt.subplot(1,3,1)
plt.title('DICOM -> Array')
plt.imshow(image, cmap = 'gray')

# apply_modality_lut( ) & apply_voi_lut( )
slice.WindowCenter = window_center
slice.WindowWidth = window_width
image = apply_modality_lut(image, slice)
image2 = apply_voi_lut(image, slice)
plt.subplot(1,3,2)
plt.title('apply_voi_lut( )')
plt.imshow(image2, cmap = 'gray')

# normalization
image3 = np.clip(image, window_center - (window_width / 2), window_center + (window_width / 2))
plt.subplot(1,3,3)
plt.title('normalize')
plt.imshow(image3, cmap = 'gray')

plt.show()​
  • [Line 11]
    DICOM 파일을 읽어오는 함수다. pydicom.read_file( ) 또는 pydicom.dcmread( )를 사용해주면 된다.
  • [Line 12~14]
    Rescale Slope는 기울기, Rescale Intercept는 y절편 정도로 생각하면 된다.
    RS는 대부분 1로 설정되어 있지만,
    RI는 간혹 -1024로 설정되어 있는 경우가 있다.
    이를 적용해주지 않으면 brightness가 엉망이 되는 현상이 발생할 수 있으니 적용해줘야한다.
  • [Line 20~26]
    설정한 WC와 WW에 맞춰 CT 영상의 픽셀값 범위를 조정하는 과정이다.
    apply_voi_lut( ) 함수가 이 과정을 담당하는데,
    WC, WW 설정 뿐만 아니라 전반적으로 신경써줘야할 것들을 알아서 잡아준다.
    그러니 꼭 사용하자. 난 그냥 default로 박아두고 사용한다.

  • [Line 28~32]
    내가 파악한 apply_voi_lut( )의 흐름을 따라해본 코드이다.
    Line 29가 나름 중요한 과정인데,
    WC를 중심으로 WW의 범위만큼 보겠다는 의미라서 "WW / 2"를 해준 것이다.

 

실행 결과는 다음과 같다.

 

apply_voi_lut( )와 np.clip( ) 간의 차이는 거의 없는데 자세히 보면 brightness 측면에서 아주 살짝 차이가 있는 걸 알 수 있다. 이게 위에서 말한 Pixel Representation, Bits Stored에서 발생하는 차이인데 이를 어떻게 해결해야하는지 아직 파악하지 못했다.

 

 

2021.10.29. 추가

아래 링크에서 BitStored 정보까지 활용해서 변환하는 방법을 Low-Level 수준으로 확인할 수 있다.

https://artiiicy.tistory.com/63

https://issueexplorer.com/issue/pydicom/pydicom/1401

 

 

 


 

 

 

2021.10.21. 내용추가

 

 pydicom 사용 중에 아래와 같은 에러문을 접하게 된다면,

 

" NotImplementedError: The pixel data with transfer syntax JPEG 2000 Image Compression (Lossless Only), cannot be read because Pillow lacks the JPEG 2000 plugin "

 

 CMD 창에서 pylibjpeg 라이브러리를 다운받아주면 해결된다!

pip install python-gdcm
pip isntall pylibjpeg[all]

https://pydicom.github.io/pydicom/stable/old/image_data_handlers.html

 

 

 


 

 

 

2021.11.22. 내용추가

 

 아래의 순서는 무조건 지켜져야한다. 

  1. Rescale Slope, RescaleIntercept
  2. Windowing
  3. Monochrome check

 MONOCHREOME1의 경우 WindowCenter, WindowWidth도 MONOCHOME1에 맞춰서 저장되어 MONOCHOME2일 때보다 값이 크다. 때문에 MONOCHOME1이라고 반전 먼저 시킨 다음에 Windowing을 하게 되면 잘못된 값을 읽어오게 되니 유의하도록 하자!

 

 

 


 

 

 

2021.12.13. 내용추가

 

 DICOM Tag 중에 Window Center, Window Width는 어떻게 설정되는 건가 궁금하던 참에 엑스레이 제조 업체로 출장 갈 일이 있어서 여쭤봤는데, "자체 개발한 알고리즘이 디텍터로부터 RAW 데이터를 받아 목적에 맞게 이미지 처리를 진행해서 결과 이미지를 반환해주는데, 이 때 결과 이미지의 히스토그램을 분석해서 이미지 표현에 적절한 값으로 WC, WW를 자동으로 계산해서 같이 반환해준다"라고 설명해주셨다. 고로 CXR에서 WC, WW는 알고리즘을 통해 자동으로 설정되는 값이라 생각하면 될 것 같다.

 

그리고, DICOM Tag 중에 궁금한 것들이 있어 찾아본 내용을 정리하고자 한다. 아래 링크를 참고했다.

http://dicomiseasy.blogspot.com/2012/08/chapter-12-pixel-data.html

  • SamplesPerPixel
    DICOM이 Pixel Array로 갖고 있는 이미지의 Channel 수를 나타낸다.
    1 이면 Gray, 3이면 RGB.
    이거 설정 안 돼 있으면 Pixel Array 읽어오는 과정에서 에러나니까 꼭 확인해줘야 한다.
  • BitsAllocated, BitsStored, HighBit
    세 개 Tag 중 가장 중요한 건 BitsStored이다. DICOM Pixel Array를 저장할 때 몇 비트로 저장했는지를 확인할 수 있는 태그이기 때문이다. BitsAllocated는 이미지 저장을 위해 할당한 비트 정보를 나타내고, BitsStored는 할당된 비트 중에 실제로 사용한 비트 정보를 나타낸다. HighBit는 대부분 BitsStored - 1로 설정된다 해서 더 깊게 찾아보진 않았다. 
  • PixelRepresentation
    0 : unsigned
    1 : signed

 

 

 


 

 

 

 

지금까지 DICOM 파일을 다루는 방법에 대해 정리해봤다. 나와 같은 어려움을 겪는 많은 사람에게 노출이 되길 바란다.

 

 

[참고 사이트]

https://www.kaggle.com/raddar/convert-dicom-to-np-array-the-correct-way

https://vincentblog.xyz/posts/medical-images-in-python-computed-tomography

https://www.stepwards.com/?page_id=21646

https://m.blog.naver.com/PostView.nhn?blogId=hjgumsin&logNo=80042311119&proxyReferer=https:%2F%2Fwww.google.com%2F

https://jayeon8282.tistory.com/2

https://stackoverflow.com/questions/35054609/understanding-the-bpp-inside-dicom-images

https://pydicom.github.io/pydicom/stable/old/image_data_handlers.html

https://stackguides.com/questions/60219622/python-convert-dcm-to-png-images-are-too-bright

 

! 마지막으로 광고 시간 !

 

728x90