본문 바로가기
목차
카테고리 없음

DICOM Windowing: Dynamic Range Mapping for Medical Imaging

by ds31x 2025. 6. 17.
728x90
반응형

0. Prerequsites

https://dsaint31.tistory.com/792

 

[CV] Dynamic Range 란?

카메라와 디스플레이의 Dynamic Range: Dynamic Range: Dynamic Range는 카메라와 디스플레이 장치에서 최소 밝기와 최대 밝기 사이의 범위(or ratio)를 의미. 이는 장치(or systme)의 밝기 디테일 처리 능력을 결

dsaint31.tistory.com

  • Dynamic Range Mapping:
    • 큰 숫자 범위를 작은 숫자 범위로 변환하는 기법:
    • 단순 scaling 이 가장 기본적인 예.
    • HDR에서 LDR로 변환시 시각적(perceptual) 품질을 고려하여 처리하는 Tone Mapping도 일종의 Dynamic Range Mapping임.
    • DICOM에서 Windowing도 일종의 Dynamic Range Mapping임.
  • DICOM Windowing:
    • Dynamic Range Mapping의 의료영상 버전
    • 진단에 필요한 특정 범위만 골라서 0-255로 변환.
    • 선형 공식이나 LUT 테이블로 구현됩니다.
  • HDR:
    • 원본의 모든 밝기 정보를 보존하면서
    • 디스플레이 범위에 맞춰 자연스럽게 압축하는 고급 Dynamic Range Mapping 기법.

https://dsaint31.tistory.com/795

 

[CV] High Dynamic Range

High Dynamic Range(HDR)는 image나 video에서 Dynamic Range를 넓혀서, 밝은 부분과 어두운 부분의 contrast를 더 넓은 범위로 표현할 수 있게 해주는 기술임. https://www.youtube.com/watch?v=95DNdbxaIXE읽어볼 자료   ht

dsaint31.tistory.com

 


1. Dynamic Range Mapping의 필요성

dynamic range mapping 의 정의는 다음과 같음

  • 넓은 범위의 원본 픽셀 값(수천~수만 개 레벨)을
  • 제한된 디스플레이 범위(256 레벨)로 변환하면서
  • 중요한 시각적 정보는 보존하는 기술

DICOM 파일에 대한 Windowing은 일종의 Dynamic range mapping으로

  • 의료 영상에 대해 제한된 디스플레이 환경에서 최적의 시각화 제공
  • 조직별 특성에 맞는 선택적 강조로 진단 정확도 향상 가능.

1-1. 의료영상과 디스플레이의 픽셀 값의 범위 차이

원본 의료 영상의 픽셀 값 범위:

  • CT 스캔: 12-16 bit (-1024 ~ 3071 HU, Hounsfield Units)
  • MRI: 12-16 bit (0 ~ 4095 또는 더 넓은 범위)
  • X-ray: 10-16 bit (수천에서 수만 개의 gray levels)

디스플레이 장치의 제약:

  • 일반 모니터: 8 bit (0-255, 256 gray levels)
  • 의료용 모니터: 10-12 bit (1024-4096 gray levels)

인간 시각의 한계:

  • 동시에 구별 가능한 gray level: 약 20-50개 (한 장면에서)
    • 한 장의 이미지에서 구분 가능한 것이 20-50개라는 애기임 (30개에서 10개 더하거나 빼는 정도로 애기하는 경우도 많음)
  • 전체 범위에서 구별 가능한 gray level: 약 200개 (눈의 적응을 고려해 여러 환경에서 구분 가능한 밝기 단계를 모두 합친 것)
    • 한 이미지를 windowing을 통해 조절하면서 봐도 200여개 단계가 한계임.
WL과 WC를 조절하는 Windowing은
일종의 Dynamic Range Mapping으로
High Dynamic Range에서 Low Dynamic Range로 표시하는 것임.

1-2. Dynamic Range Compression의 필요성

원본 데이터:    [-1024 ──────────────────── 3071] (4096 levels)
                           ↓ 압축 필요
디스플레이:     [0 ─────── 255] (256 levels)
  • 원본 데이터와 디스플레이에서의 range 차이가 존재함:
  • 전체 픽셀 값을 단순히 0-255로 선형 매핑하면 중요한 정보가 손실 됨.

https://www.kaggle.com/code/redwankarimsony/ct-scans-dicom-files-windowing-explained


2. DICOM Windowing 개념

2-1. Windowing이란?

DICOM windowing은

  • 전체 dynamic range 중에서
  • 의학적으로 중요한 ragne만 선택적으로 강조 하여
  • 제한된 디스플레이 범위에 매핑하는 기술.

2-2. 핵심 파라미터

Window Level (WL) 또는 Window Center (WC):

  • RoI (Region of Interest,관심 영역) 의 대상이 되는 조직이 가장 잘 보이는 대표적인 픽셀 값.
  • 의학적으로 가장 중요하게 봐야하는 조직에 해당하는 픽셀 값의 대표치.
  • 주변의 다른 조직과 해당 조직을 가장 잘 분리해서 볼 수 있는 값으로 설정.

뇌 조직 CT에서 실제 뇌 조직 픽셀들의 mean이 35.7 HU 이고,
해당 뇌 조직 픽셀들의 median이 38.2 HU 이라고 가정하자.
이렇게 되어있다해도 보통 Brain CT에서의 Window Level은 40 HU 로 지정함.
이것은 계산된 값이 아니라 임상경험으로 정해진 최적값임.

 

표시되는 pixel value 의 histogram에서 중심에 해당하는 값을 의미하며 표시되는 밝기의 기준점이 됨.

WL이 커질수록 영상이 전체적으로 어두워지고 낮으면 밝아지기 때문에, 일반 영상처리에선 영상의 밝기를 조절하는데 사용되기도 함.


 

Window Width (WW):

  • 표시할 window(or dynamic range)의 폭
  • 넓을수록 더 많은 범위를 보여주지만 contrast는 감소.

WW의 경우, 영상 전체의 밝기(average luminance)를 바꾸기 보다는 contrast를 조절해 줌.

WW가 작아질수록 보여지는 범위내에서  contrast가 매우 커지며, 넓어질 경우 contrast가 줄고 부드럽게 보여짐. 

 


2-3. Windowing 공식 (Linear)

https://dicom.nema.org/medical/dicom/current/output/chtml/part17/chapter_y.html

min_display = window_level - window_width / 2
max_display = window_level + window_width / 2

if pixel_value ≤ min_display:
    display_value = 0 (black)
elif pixel_value ≥ max_display:
    display_value = 255 (white)
else:
    display_value = 255 × (pixel_value - min_display) / window_width

3. DICOM 태그 및 Attributes

DICOM 표준에서는 windowing 정보를 특정 태그들에 저장함.
이는 영상과 함께 최적의 display 설정을 전달하여 일관된 진단 환경을 제공할 수 있게 해줌.

3-1. 핵심 DICOM 태그

태그 이름 설명
(0028,1050) Window Center Window Level 값들
(0028,1051) Window Width Window Width 값들
(0028,1055) Window Center & Width Explanation 각 window setting의 설명

3-2. 여러 Window Setting의 활용

하나의 DICOM 파일에는 여러 개의 window setting이 저장될 수 있음.
이는 동일한 영상에서 서로 다른 조직을 최적으로 관찰하기 위함입니다.

예시: CT 복부 영상의 Multiple Windows

Window Center: [50, 350, -600]    # 3개의 서로 다른 center 값
Window Width:  [400, 40, 1200]    # 3개의 서로 다른 width 값
Explanation:   ["SOFT TISSUE", "LIVER", "LUNG"]  # 각각의 용도 설명

3-3. VOI LUT (Value of Interest Look-Up Table): 고급 기능.

태그 이름 설명
(0028,1056) VOI LUT Function 변환 함수 타입 ("LINEAR", "SIGMOID")
(0028,3010) VOI LUT Sequence 복잡한 비선형 변환 테이블 정의

 

VOI LUT (Value of Interest Look-Up Table)는 원본 픽셀 값을 디스플레이용 픽셀 값으로 변환하는 매핑 테이블임.

일종의 함수로서 관심 있는 조직이나 영역을 최적으로 시각화하기 위해 사용되며 다음의 종류를 가짐.

  • "LINEAR": 일반적인 선형 windowing (위에서 설명한 공식)
  • "SIGMOID": S자 곡선 변환으로 더 부드러운 대비 전환
  • "LUT Sequence": 복잡한 맞춤형 변환 함수 정의 가능

3-2. Windowing DICOM Tag의 특성

Optional:

  • 모든 DICOM 파일에 반드시 있는 것은 아님
  • CT 스캔 의 경우 사전 설정된 Window 정보가 있으나,
  • MRI, 그 중에서도 비표준화된 프로토콜을 사용하는 연구용 시퀀스 이용시 없을 수도 있음.
# CT 파일 - 보통 윈도잉 정보 있음
# ds 는 pydicom.dataset.Dataset
ds = pydicom.dcmread('sample.dcm')
if hasattr(ds, 'WindowCenter'):
    print(f"윈도잉 있음: WC={ds.WindowCenter}, WW={ds.WindowWidth}")
else:
    print("윈도잉 정보 없음 - 최적 설정을 직접 계산해야 함")

 

Modality-specific:

  • 검사 종류(modality)에 따라 기본값이 다름
# CT 복부 - 일반적인 predifined window
ct_windows = {
    'SOFT_TISSUE': {'center': 50, 'width': 400},
    'LIVER': {'center': 60, 'width': 160},
    'LUNG': {'center': -600, 'width': 1200},
    'BONE': {'center': 300, 'width': 1500}
}

# MRI 뇌 T1 - CT와 다른 최적화
mri_t1_windows = {
    'BRAIN': {'center': 600, 'width': 1200},  # CT보다 높은 값
    'CSF': {'center': 300, 'width': 600}
}

# 디지털 X-ray 흉부 - 훨씬 넓은 범위
xray_windows = {
    'CHEST': {'center': 2048, 'width': 4096},  # 훨씬 넓은 범위
    'RIBS': {'center': 3000, 'width': 1000}
}

 

Multi-valued:

같은 해부학적 슬라이스에 여러 조직 타입이 포함되어 있고, 각각 최적 시각화를 위해 완전히 다른 Windowing이 필요함.

  • 앞서 살펴본 것처럼, 하나의 DICOM 파일에 여러 개의 window setting 저장 가능.
  • 같은 CT 슬라이스를 다른 윈도우로 본 경우:
    • lung window: 폐 병변을 명확하게 보여줌
    • bone window: 골절과 뼈 세부사항 보여줌
    • softtissue window: 장기 경계와 종괴 보여줌
import pydicom

ds = pydicom.dcmread('ct_abdomen.dcm')

# 다중 윈도우 존재 여부 확인
if hasattr(ds, 'WindowCenter'):
    # MultiValue 객체인 경우 리스트로 변환
    centers = list(ds.WindowCenter) if hasattr(ds.WindowCenter, '__iter__') else [ds.WindowCenter]
    widths = list(ds.WindowWidth) if hasattr(ds.WindowWidth, '__iter__') else [ds.WindowWidth]

    # 설명이 있는 경우 가져오기
    explanations = []
    if hasattr(ds, 'WindowCenterWidthExplanation'):
        explanations = list(ds.WindowCenterWidthExplanation)

    print(f"{len(centers)}개의 윈도우 사전 설정 발견:")
    for i, (center, width) in enumerate(zip(centers, widths)):
        explanation = explanations[i] if i < len(explanations) else f"윈도우 {i+1}"
        print(f"  {explanation}: WC={center}, WW={width}")

4. 의학적 의미와 활용

4-1. 조직별 최적화

각 조직(tissue)은 고유한 픽셀 값 범위를 가짐.
때문에, 해당 범위에 최적화된 windowing이 필요함.

CT에서의 대표적인 Window Settings:

Window Name WL WW desc
Lung -600 HU 1200 HU 폐 질환 진단
Mediastinum 50 HU 350 HU 종격동 구조
Bone 300 HU 1500 HU 골절, 뼈 질환
Brain 40 HU 80 HU 뇌 조직
Liver 60 HU 160 HU 간 질환

4-2. 진단 정확도 향상

적절한 windowing은:

  • 병변의 시각적 contrast를 극대화
  • 정상 조직과 병변 조직의 구별을 용이하게 함
  • 의사의 진단 정확도와 속도를 향상 시킴

5. PyDICOM을 활용한 실습 (작성중)

5-1. 기본 Window 정보 읽기

아직 완성되지 않았고 버그도 존재함. 사용하지 말것.

import pydicom
import numpy as np
import matplotlib.pyplot as plt

# DICOM 파일 읽기
ds = pydicom.dcmread('example.dcm')

# Window 정보 확인
def get_window_info(ds):
    """DICOM 파일에서 window 정보 추출"""
    window_info = {}

    # Window Center (Level)
    if hasattr(ds, 'WindowCenter'):
        if isinstance(ds.WindowCenter, (list, pydicom.multival.MultiValue)):
            window_info['centers'] = [float(x) for x in ds.WindowCenter]
        else:
            window_info['centers'] = [float(ds.WindowCenter)]

    # Window Width
    if hasattr(ds, 'WindowWidth'):
        if isinstance(ds.WindowWidth, (list, pydicom.multival.MultiValue)):
            window_info['widths'] = [float(x) for x in ds.WindowWidth]
        else:
            window_info['widths'] = [float(ds.WindowWidth)]

    # Window 설명
    if hasattr(ds, 'WindowCenterWidthExplanation'):
        if isinstance(ds.WindowCenterWidthExplanation, (list, pydicom.multival.MultiValue)):
            window_info['explanations'] = list(ds.WindowCenterWidthExplanation)
        else:
            window_info['explanations'] = [ds.WindowCenterWidthExplanation]

    return window_info

# Window 정보 출력
window_info = get_window_info(ds)
print("=== DICOM Window Settings ===")
for i, (center, width) in enumerate(zip(window_info.get('centers', []), 
                                       window_info.get('widths', []))):
    explanation = window_info.get('explanations', [''])[i] if i < len(window_info.get('explanations', [])) else ''
    print(f"Window {i+1}: Center={center}, Width={width}, Description='{explanation}'")

5-2 Windowing 함수 구현

def apply_windowing(pixel_array, window_center, window_width, output_range=(0, 255)):
    """
    DICOM windowing 적용

    Args:
        pixel_array: 원본 픽셀 배열
        window_center: Window center (level)
        window_width: Window width
        output_range: 출력 범위 (기본: 0-255)

    Returns:
        windowed_array: Windowing이 적용된 배열
    """
    # Window 범위 계산
    window_min = window_center - window_width / 2
    window_max = window_center + window_width / 2

    # 픽셀 값을 window 범위로 클리핑
    windowed = np.clip(pixel_array, window_min, window_max)

    # 0-1 범위로 정규화
    windowed = (windowed - window_min) / window_width

    # 출력 범위로 스케일링
    output_min, output_max = output_range
    windowed = windowed * (output_max - output_min) + output_min

    return windowed.astype(np.uint8)

# 픽셀 데이터 가져오기
pixel_array = ds.pixel_array

# 여러 window setting 적용해보기
if window_info.get('centers') and window_info.get('widths'):
    fig, axes = plt.subplots(1, len(window_info['centers']) + 1, figsize=(15, 5))

    # 원본 이미지
    axes[0].imshow(pixel_array, cmap='gray')
    axes[0].set_title('Original')
    axes[0].axis('off')

    # 각 window setting 적용
    for i, (center, width) in enumerate(zip(window_info['centers'], window_info['widths'])):
        windowed_image = apply_windowing(pixel_array, center, width)
        axes[i+1].imshow(windowed_image, cmap='gray')

        explanation = window_info.get('explanations', [''])[i] if i < len(window_info.get('explanations', [])) else f'Window {i+1}'
        axes[i+1].set_title(f'{explanation}\nWL:{center}, WW:{width}')
        axes[i+1].axis('off')

    plt.tight_layout()
    plt.show()

5-3. 사용자 정의 Window Setting

def create_custom_windowing(pixel_array, tissue_type='soft_tissue'):
    """
    조직 타입에 따른 맞춤형 windowing
    """
    # CT Hounsfield Units 기준 사전 정의된 window settings
    preset_windows = {
        'lung': {'center': -600, 'width': 1200},
        'mediastinum': {'center': 50, 'width': 350},
        'bone': {'center': 300, 'width': 1500},
        'brain': {'center': 40, 'width': 80},
        'liver': {'center': 60, 'width': 160},
        'soft_tissue': {'center': 50, 'width': 400},
        'full_range': {
            'center': (pixel_array.max() + pixel_array.min()) / 2,
            'width': pixel_array.max() - pixel_array.min()
        }
    }

    if tissue_type not in preset_windows:
        raise ValueError(f"지원되지 않는 조직 타입: {tissue_type}")

    settings = preset_windows[tissue_type]
    return apply_windowing(pixel_array, settings['center'], settings['width'])

# 다양한 조직별 windowing 비교
tissue_types = ['lung', 'mediastinum', 'bone', 'brain', 'soft_tissue']
fig, axes = plt.subplots(1, len(tissue_types), figsize=(20, 4))

for i, tissue in enumerate(tissue_types):
    try:
        windowed = create_custom_windowing(pixel_array, tissue)
        axes[i].imshow(windowed, cmap='gray')
        axes[i].set_title(f'{tissue.replace("_", " ").title()} Window')
        axes[i].axis('off')
    except:
        axes[i].text(0.5, 0.5, 'N/A', ha='center', va='center', transform=axes[i].transAxes)
        axes[i].set_title(f'{tissue.replace("_", " ").title()} Window')
        axes[i].axis('off')

plt.tight_layout()
plt.show()

5-4 Interactive Windowing

def interactive_windowing_analysis(pixel_array):
    """
    픽셀 값 분포를 분석하여 최적의 windowing 제안
    """
    # 픽셀 값 통계
    stats = {
        'min': pixel_array.min(),
        'max': pixel_array.max(),
        'mean': pixel_array.mean(),
        'std': pixel_array.std(),
        'median': np.median(pixel_array),
        'percentiles': np.percentile(pixel_array, [5, 25, 75, 95])
    }

    print("=== 픽셀 값 분석 ===")
    print(f"범위: {stats['min']:.1f} ~ {stats['max']:.1f}")
    print(f"평균: {stats['mean']:.1f} ± {stats['std']:.1f}")
    print(f"중앙값: {stats['median']:.1f}")
    print(f"백분위수 (5%, 25%, 75%, 95%): {stats['percentiles']}")

    # 자동 windowing 제안
    suggested_windows = {
        'Conservative': {
            'center': stats['median'],
            'width': stats['percentiles'][3] - stats['percentiles'][0]  # 5%-95% 범위
        },
        'Standard': {
            'center': stats['mean'],
            'width': 2 * stats['std']  # 평균 ± 1 표준편차
        },
        'Wide': {
            'center': (stats['max'] + stats['min']) / 2,
            'width': stats['max'] - stats['min']  # 전체 범위
        }
    }

    print("\n=== 제안된 Window Settings ===")
    for name, settings in suggested_windows.items():
        print(f"{name}: Center={settings['center']:.1f}, Width={settings['width']:.1f}")

    return suggested_windows

# 분석 실행
suggested = interactive_windowing_analysis(pixel_array)

# 제안된 설정들로 windowing 적용
fig, axes = plt.subplots(1, len(suggested) + 1, figsize=(16, 4))

# 히스토그램
axes[0].hist(pixel_array.flatten(), bins=100, alpha=0.7, color='blue')
axes[0].set_title('Pixel Value Distribution')
axes[0].set_xlabel('Pixel Value')
axes[0].set_ylabel('Frequency')

# 제안된 windowing들
for i, (name, settings) in enumerate(suggested.items()):
    windowed = apply_windowing(pixel_array, settings['center'], settings['width'])
    axes[i+1].imshow(windowed, cmap='gray')
    axes[i+1].set_title(f'{name}\nWL:{settings["center"]:.0f}, WW:{settings["width"]:.0f}')
    axes[i+1].axis('off')

plt.tight_layout()
plt.show()

같이 보면 좋은 자료들

https://www.kaggle.com/code/redwankarimsony/ct-scans-dicom-files-windowing-explained

 

CT-Scans, DICOM files, Windowing Explained ✔️✔️

Explore and run machine learning code with Kaggle Notebooks | Using data from RSNA STR Pulmonary Embolism Detection

www.kaggle.com

 

728x90