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로 선형 매핑하면 중요한 정보가 손실 됨.

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)

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