의료 AI 개발자가 처음 알아야 할 것: DICOM, HU, CT Normalization
병원에서 보는 CT는 한 장의 흑백 사진처럼 보인다. 하지만 CT가 만드는 것은 사진이라기보다 X선이 몸을 통과하며 얼마나 약해졌는지를 계산해 놓은 단면 데이터에 가깝다. 컴퓨터는 여러 방향에서 얻은 측정값을 조합해 몸 안을 얇은 단면으로 재구성하고, 각 위치에 숫자를 남긴다.
그 숫자와 촬영 정보를 함께 보관하는 표준이 DICOM이다. DICOM 안에 저장된 정수는 상황에 따라 그대로 조직의 의미를 갖지 않기 때문에, CT에서는 보통 **HU(Hounsfield Unit)**라는 기준으로 다시 해석한다. 그리고 HU의 넓은 범위에서 목적에 맞는 구간을 고른 뒤, 딥러닝 모델이 다루기 좋은 값으로 바꾸는 작업이 windowing과 normalization이다.
이 용어들은 서로 떨어진 개념이 아니다. 실제 전처리 순서에서 앞뒤로 이어진다.

1. CT는 왜 일반 이미지와 다를까
카메라는 장면에서 반사된 빛을 받아 RGB나 grayscale 픽셀로 기록한다. CT는 방식이 다르다. X선 발생기와 검출기가 환자 주위를 돌면서 여러 각도의 투과 데이터를 얻고, 재구성 알고리즘이 이를 단면 영상으로 만든다. 따라서 CT 픽셀은 단순한 ‘밝기’라기보다 특정 위치의 X선 감쇠 특성을 반영한 값이다.
2D 단면을 여러 장 쌓으면 3D 부피가 된다. 이때 3D 공간의 한 칸은 픽셀이 아니라 voxel이라고 부른다. 같은 512 × 512 영상이라도 픽셀 간격이 0.7 mm인 영상과 1.0 mm인 영상은 실제로 표현하는 신체 크기가 다르다. 화면 해상도만 같다고 같은 데이터라고 볼 수 없는 이유다.
또 하나 중요한 점은, 우리가 뷰어에서 보는 회색 영상이 CT 값 그 자체는 아니라는 사실이다. CT는 공기부터 뼈와 금속까지 매우 넓은 범위의 값을 갖는다. 모니터는 그중 일부 구간을 골라 검정과 흰색 사이에 펼쳐 보여 준다. 같은 CT라도 폐를 보기 위한 설정과 연부조직을 보기 위한 설정에서 전혀 다르게 보일 수 있다.
이 차이를 모른 채 CT 한 장을 PNG로 저장하고 0~255 또는 0~1로 바꾸면, 화면은 그럴듯해도 원래의 HU 범위와 변환 규칙을 잃을 수 있다. PNG가 나쁜 형식이라는 뜻은 아니다. 썸네일이나 품질 확인에는 편리하다. 다만 원본 CT의 정량값을 보존해야 하는 학습 파이프라인의 출발점으로는 충분하지 않을 수 있다.
2. DICOM은 무엇을 저장하는가
DICOM은 Digital Imaging and Communications in Medicine의 약자다. 공식 설명대로 의료영상과 관련 정보를 임상적으로 필요한 형태와 품질로 교환하기 위한 국제 표준이다.[1] 흔히 파일 확장자로 먼저 접하지만, 실제로는 파일 형식뿐 아니라 장비와 PACS, 뷰어, 서버가 영상을 저장하고 전송하는 방식까지 포함한다.
일반 이미지가 픽셀 배열을 중심으로 한다면, DICOM은 픽셀과 그 픽셀을 해석하는 맥락을 함께 다룬다.

한 번의 CT 검사에는 영상 묶음이 하나만 들어 있지 않을 수 있다. 예를 들어 같은 검사에서 다음과 같은 series가 함께 만들어질 수 있다.
- 촬영 범위를 확인하기 위한 scout 또는 localizer
- 조영 전 영상과 조영 후 영상
- 폐를 보기 위한 sharp kernel 영상과 연부조직용 smooth kernel 영상
- 얇은 절편과 두꺼운 절편
- axial 원본과 재구성된 coronal·sagittal 영상
따라서 폴더 안의 DICOM 파일을 전부 읽어 한 덩어리로 쌓아서는 안 된다. 먼저 SeriesInstanceUID로 같은 series끼리 묶고, 모델의 목적에 맞는 series를 선택해야 한다.
처음 확인할 DICOM 속성
| 속성 | 하는 일 |
|---|---|
Modality |
CT인지 MR인지 영상 종류를 확인한다. |
ImageType |
original, derived, localizer 여부 등을 확인한다. |
SeriesInstanceUID |
같은 영상 series를 식별한다. |
SOPInstanceUID |
개별 이미지 instance를 식별하고 중복을 찾는다. |
ImagePositionPatient |
첫 voxel이 환자 좌표계의 어디에 있는지 나타낸다. |
ImageOrientationPatient |
영상의 행과 열이 환자 기준 어느 방향을 향하는지 나타낸다. |
PixelSpacing |
행·열 방향에서 인접 픽셀 중심 사이의 실제 거리(mm)다. |
SliceThickness |
명목상 슬라이스 두께다. 실제 슬라이스 중심 간격과 같지 않을 수 있다. |
RescaleSlope, RescaleIntercept |
저장된 픽셀값을 출력 단위로 변환할 때 사용한다. |
WindowCenter, WindowWidth |
뷰어가 사용할 수 있는 표시 window 정보다. |
DICOM 표준의 Image Plane Module은 PixelSpacing, ImagePositionPatient, ImageOrientationPatient를 이용해 2D 픽셀 배열이 3D 환자 공간의 어디에 놓이는지 정의한다.[2] 이 정보가 있기 때문에 영상 위에 표시한 병변 위치를 다른 뷰나 원본 좌표로 되돌릴 수 있다.
파일명 순서만 믿고 슬라이스를 정렬하면 위험한 이유도 여기에 있다. 1.dcm, 2.dcm, 10.dcm이 사전식으로 정렬될 수도 있고, export 과정에서 파일명이 새로 붙을 수도 있다. InstanceNumber도 편리한 힌트이지만 물리적 위치 그 자체는 아니다. 3D volume을 만들 때는 방향과 위치 정보를 이용해 슬라이스 순서를 확인하는 편이 안전하다.
3. HU는 저장된 픽셀값에 의미를 돌려준다
DICOM에서 pixel_array를 읽으면 정수 배열이 나온다. 여기서 자주 생기는 오해가 있다.
pixel = ds.pixel_array.astype("float32")
x = pixel / 255.0
일반 이미지 전처리에서는 익숙한 코드지만, CT에서는 출발점부터 잘못될 수 있다. DICOM에 저장된 값은 Stored Value이고, CT의 물리적 기준에 가까운 출력값을 얻으려면 변환이 필요할 수 있다.
DICOM CT Image Module은 저장값 SV와 출력 단위의 관계를 다음처럼 정의한다.[3]
Output Unit = Rescale Slope × Stored Value + Rescale Intercept
전형적인 original single-energy CT에서 localizer가 아닌 경우 이 출력 단위는 HU다. 코드에서 흔히 보는 형태로 쓰면 다음과 같다.
HU = m × SV + b
예를 들어 저장된 픽셀값이 24, slope가 1, intercept가 -1024라면 다음과 같이 계산된다.
HU = 1 × 24 - 1024
= -1000 HU
저장값 24만 보고는 어떤 조직인지 알기 어렵지만, -1000 HU로 변환하면 공기에 가까운 값이라는 해석이 가능해진다.
HU는 물을 0에 가깝게, 공기를 약 -1000으로 두는 CT의 수치 척도다. 아래 값은 개념을 잡기 위한 대략적인 범위이며, 촬영 조건과 조영제, 재구성 방식에 따라 달라질 수 있다.
| 물질·조직 | 대략적인 HU |
|---|---|
| 공기 | 약 -1000 |
| 폐 실질 | 약 -900 ~ -500 |
| 지방 | 약 -150 ~ -50 |
| 물 | 0 부근 |
| 연부조직 | 수십 HU |
| 뼈 | 수백 ~ 1000 이상 |
Python에서는 slope와 intercept를 직접 읽어 계산할 수도 있고, pydicom의 apply_rescale()을 사용할 수도 있다. 이 함수는 Modality LUT가 있으면 LUT를, slope와 intercept가 있으면 rescale 연산을 적용한다.[4]
import numpy as np
import pydicom
from pydicom.pixels import apply_rescale
ds = pydicom.dcmread("slice.dcm")
stored = ds.pixel_array
output = apply_rescale(stored, ds).astype(np.float32)
변수 이름을 곧바로 hu라고 붙이기 전에는 영상 유형을 확인해야 한다. derived image, localizer, multi-energy CT 등에서는 출력 단위나 변환 방식이 다를 수 있다. DICOM 표준도 ImageType, Multi-energy CT Acquisition, RescaleType에 따라 이를 구분한다.[3] 입문 단계에서는 ‘CT면 무조건 HU’라고 외우기보다 어떤 CT 객체에서 HU가 보장되는지 확인하는 습관이 더 중요하다.
4. Windowing은 넓은 HU 범위에서 볼 구간을 고르는 일이다
CT 한 장에는 공기, 지방, 연부조직, 뼈가 함께 들어 있다. 이 값 전체를 한 번에 제한된 grayscale로 표시하면 관심 조직의 작은 차이가 눌린다. 그래서 영상의학과 뷰어에는 window 설정이 있다.
Windowing은 간단히 말해 다음 작업이다.
- 관심 있는 HU 하한과 상한을 정한다.
- 범위 밖의 값은 경계값으로 자른다(clipping).
- 선택한 범위를 화면의 grayscale 또는 모델 입력 범위에 펼친다.

그림 3은 실제 환자 DICOM이 아니라, 합성 흉부 CT를 폐, 종격동·연부조직, 뼈 Window의 전형적인 표현으로 구성한 개념 이미지다. 표시한 HU 값은 차이를 이해하기 위한 예시 범위이며, 고정된 임상 표준이나 특정 환자 영상에 실제로 적용된 설정을 뜻하지 않는다.
설명용으로 범위의 하한과 상한을 이용해 Center와 Width를 환산하면 다음과 같다. 실제 DICOM의 VOI 변환은 WindowCenter, WindowWidth, VOILUTFunction 등의 설정에 따라 해석해야 한다.
| 표시 목적 | 예시 HU 범위 | 대략적인 Window Center | 대략적인 Window Width |
|---|---|---|---|
| 폐 | -1000 ~ 400 |
약 -300 HU |
약 1400 HU |
| 종격동·연부조직 | -160 ~ 240 |
약 40 HU |
약 400 HU |
| 뼈 | -500 ~ 1500 |
약 500 HU |
약 2000 HU |
전처리 범위의 출발점으로는 위와 같은 구간을 검토할 수 있다. 다만 이 값이 정답은 아니다. 목표 장기, 질환, 조영 phase, 촬영 프로토콜과 데이터셋에 따라 별도로 실험해 결정해야 한다. 뷰어에서 사람이 보기 좋은 Window와 모델 입력용 clipping 범위도 목적이 다를 수 있다.
DICOM의 WindowCenter와 WindowWidth는 뷰어가 사용할 수 있는 표시 설정이다. 한 영상에 여러 Window가 들어 있을 수도 있고, header에 기록된 값이 모델에 가장 적합하다는 보장도 없다. 학습용 전처리에서는 header 값을 참고하되, 모델의 입력 정책은 별도로 명시하고 버전으로 관리하는 편이 낫다.
또한 순서가 중요하다. pydicom 문서도 modality LUT 또는 rescale이 필요한 데이터라면 이를 먼저 적용한 뒤 windowing해야 한다고 설명한다.[5]
Stored Value
→ Modality Rescale
→ HU 또는 명시된 출력 단위
→ Windowing
HU 변환 전에 Window를 적용하면 서로 다른 장비와 series의 저장값을 같은 기준으로 자르는 셈이 될 수 있다.
5. CT Normalization은 ‘같은 규칙으로 보게 만드는 것’이다
Normalization이라는 말은 문맥에 따라 뜻이 달라진다. 이 글에서 말하는 CT normalization은 모델이 학습 때와 추론 때 같은 의미, 같은 범위, 같은 공간 조건의 입력을 받도록 만드는 전처리 정책을 뜻한다. 하나의 공식 이름이 아니라 여러 결정의 묶음이다.
가장 단순한 intensity normalization
고정된 HU 구간을 [0, 1]로 바꾸는 min-max 방식은 이해하기 쉽고 재현하기도 좋다.
import numpy as np
def window_minmax(hu: np.ndarray, min_hu: float, max_hu: float) -> np.ndarray:
if max_hu <= min_hu:
raise ValueError("max_hu must be greater than min_hu")
clipped = np.clip(hu, min_hu, max_hu)
normalized = (clipped - min_hu) / (max_hu - min_hu)
return normalized.astype(np.float32)
이 방식의 장점은 환자마다 같은 HU가 같은 입력값으로 매핑된다는 점이다. 공기와 물, 연부조직 사이의 절대적 관계를 비교적 잘 보존한다.
다른 선택도 가능하다. 훈련 데이터 전체의 평균과 표준편차를 계산해 z-score를 적용하거나, 여러 Window를 채널로 쌓을 수도 있다. 반면 slice마다 평균과 표준편차를 다시 계산하는 방식은 영상 간 밝기 차이를 줄이지만 절대 HU 관계도 약하게 만들 수 있다. 어떤 방법이 맞는지는 task에 따라 다르며, 중요한 것은 훈련과 배포 코드가 같은 설정을 사용한다는 점이다.
공간도 normalization 대상이다
CT를 3D volume으로 다룬다면 intensity만 맞춰서는 충분하지 않다.
voxel이 나타내는 물리적 크기와 각 슬라이스의 위치·방향도 함께
해석해야 한다.
그림 4는 특정 환자의 진단 영상을 재현한 것이 아니라,
DICOM 슬라이스의 위치와 간격을 설명하기 위해 만든
합성 뇌 단면 개념도다. 실제 환자 DICOM은 사용하지 않았다.

그림 4의 IPP1, IPP2, IPP3는 각 슬라이스의
ImagePositionPatient를 개념적으로 표시한 것이다.
그림에서는 세 기준점의 관계를 쉽게 볼 수 있도록
슬라이스 왼쪽에 단순화해 배치했다.
DICOM에서 ImagePositionPatient는 영상의 왼쪽 위,
즉 처음 전송되는 voxel 중심이 환자 좌표계에서 갖는
(x, y, z) 좌표다. 따라서 단순한 슬라이스 번호가 아니라,
각 영상 평면을 환자 좌표계에 배치하기 위한 3차원 기준점이다.
Δsₖ = |(IPPₖ₊₁ - IPPₖ) · n|
여기서 n은 슬라이스 면에 수직인 단위 법선이다.
IPPₖ₊₁ - IPPₖ는 인접한 두 슬라이스 기준점 사이의
3차원 변위이고, 이 변위를 n에 투영하면
슬라이스 법선 방향의 중심 간격을 얻을 수 있다.
그림의 빨간색 화살표는 슬라이스 2와 슬라이스 3 사이의
중심 간격을 예로 든 것이다. 같은 방식으로 슬라이스 1과
슬라이스 2 사이의 간격도 계산할 수 있다.
예를 들어 다음 두 volume은 array shape가 모두
512 × 512 × 200으로 같다.
Volume A
shape: 512 × 512 × 200
voxel spacing: 0.7 × 0.7 × 1.0 mm
Volume B
shape: 512 × 512 × 200
voxel spacing: 0.9 × 0.9 × 5.0 mm
그림의 세 값은 설명을 위해 행 × 열 × 슬라이스 방향으로 표기했다. 원본 DICOM에는 하나의 3개짜리 voxel spacing 속성이 있는 것이 아니다. 행·열 간격은 PixelSpacing에서 읽고, 슬라이스 방향 간격은 ImagePositionPatient의 차이와 방향 정보로 확인하거나, 존재하는 경우 SpacingBetweenSlices를 참고한다.
같은 shape라도 voxel 하나가 나타내는 물리적 크기가 다르면 동일한 voxel 수가 서로 다른 길이와 영상 범위를 뜻한다. 따라서 3D 모델이나 크기 측정이 중요한 task에서는 PixelSpacing, 슬라이스 위치, 방향을 이용해 volume을 구성하고 필요하면 목표 spacing으로 resampling한다. DICOM은 LPS 환자 좌표계를 사용하며, 소프트웨어에 따라 내부적으로 RAS 좌표계로 변환될 수 있으므로 변환 규칙도 확인해야 한다.[6]
여기서 SliceThickness와 실제 슬라이스 중심 간격을 같은 값으로 단정하면 안 된다. DICOM 표준에서 SliceThickness는 명목상 두께이고, SpacingBetweenSlices는 인접 슬라이스 중심 간격이다.[2] 실제 series에서는 위치 정보의 차이를 함께 확인하는 편이 안전하다.
Normalization이 모든 차이를 없애 주지는 않는다
CT 장비 제조사, 관전압, 방사선량, 조영제 phase, reconstruction algorithm과 kernel이 달라지면 noise와 texture도 달라질 수 있다. HU 범위를 [0, 1]로 맞춘다고 이런 차이가 모두 사라지는 것은 아니다.
| Normalization으로 맞출 수 있는 것 | 별도 검증이 필요한 것 |
|---|---|
| 입력 값의 범위 | 장비·기관별 noise와 texture |
| 고정 HU Window | 조영 전·후와 촬영 phase 차이 |
| voxel spacing과 orientation | reconstruction kernel 차이 |
| tensor shape와 dtype | 환자군과 임상 workflow 차이 |
Normalization은 데이터 harmonization이나 외부 검증을 대신하지 않는다. ‘범위를 맞췄으니 같은 데이터’가 아니라, 같은 규칙으로 읽을 준비를 했다고 보는 편이 정확하다.
6. DICOM에서 모델 Tensor까지, 순서를 고정한다
이제 앞의 용어를 하나의 파이프라인으로 묶을 수 있다.
1. DICOM 파일 검색
2. Study와 Series 구분
3. 대상 CT series 선택
4. 위치·방향으로 슬라이스 정렬
5. Pixel Data decoding
6. Rescale 적용 및 HU 확인
7. Windowing
8. Intensity normalization
9. 필요 시 spacing resampling
10. Crop·Pad·Tensor 변환
아래 코드는 한 장의 conventional CT DICOM을 [0, 1] 입력으로 바꾸는 최소 예제다. 3D volume에서는 이 작업 전에 동일 series를 모으고 위치·방향으로 정렬해야 한다.
from pathlib import Path
import numpy as np
import pydicom
from pydicom.pixels import apply_rescale
def load_ct_slice(
path: str | Path,
min_hu: float = -1000.0,
max_hu: float = 400.0,
) -> np.ndarray:
ds = pydicom.dcmread(path)
if str(ds.get("Modality", "")).upper() != "CT":
raise ValueError("The input is not a CT image")
image_type = {str(v).upper() for v in ds.get("ImageType", [])}
if "LOCALIZER" in image_type or "SCOUT" in image_type:
raise ValueError("Localizer/scout images are not accepted")
# Modality LUT 또는 Rescale Slope/Intercept 적용
output = apply_rescale(ds.pixel_array, ds).astype(np.float32)
# 이 예제는 HU가 보장되는 conventional CT를 전제로 한다.
clipped = np.clip(output, min_hu, max_hu)
normalized = (clipped - min_hu) / (max_hu - min_hu)
return normalized.astype(np.float32)
제품이나 연구 코드에서는 여기에 더 많은 확인이 필요하다. 압축 transfer syntax decoding 실패, 중복 instance, irregular spacing, multi-frame CT, multi-energy CT, padding value, burned-in annotation 같은 예외가 존재한다. 하지만 입문 단계에서 가장 먼저 고정해야 할 것은 다음 다섯 가지다.
- 어떤
SeriesInstanceUID를 모델 입력으로 선택하는가 - 저장값을 어떤 출력 단위로 변환하는가
- 어떤 HU Window를 사용하는가
- spacing과 orientation을 어떻게 처리하는가
- 훈련과 추론이 같은 전처리 버전을 사용하는가
이 설정은 코드 곳곳에 흩어 두기보다 하나의 입력 계약으로 남기는 편이 좋다.
modality: CT
image_type: ORIGINAL_NON_LOCALIZER
hu_window: [-1000, 400]
normalization: minmax_0_1
target_spacing_mm: [1.0, 1.0, 1.0]
output_dtype: float32
preprocessing_version: 1.0.0
모델 weight가 같아도 Window나 spacing이 달라지면 실제 동작은 달라진다. 전처리 설정까지 포함해야 비로소 같은 모델이라고 말할 수 있다.
마무리
CT는 회색 사진이 아니라 X선 감쇠를 재구성한 수치 데이터다. DICOM은 그 수치와 환자·검사·공간·촬영 정보를 연결한다. HU는 저장된 픽셀값을 조직의 감쇠 기준으로 해석하게 해 주고, Windowing은 넓은 HU 범위에서 지금 볼 구간을 고른다. Normalization은 그 결과를 학습과 추론에서 일관된 입력으로 만든다.
이제 “DICOM에서 이미지를 꺼내 PNG로 저장하고 255로 나누면 되지 않을까?”라는 질문에 답할 수 있다. 화면을 만드는 목적이라면 가능하다. 그러나 CT 의료 AI의 원본 전처리라면, 그 전에 DICOM의 변환 정보와 공간정보를 읽고 HU와 Window 정책을 먼저 정해야 한다.
CT 의료 AI에서 첫 번째로 설계해야 하는 것은 거대한 네트워크가 아니다. DICOM을 같은 의미의 Tensor로 바꾸는 재현 가능한 전처리 파이프라인이다.
참고 자료
- DICOM Standards Committee, About DICOM: Overview
- DICOM PS3.3 2026c, C.7.6.2 Image Plane Module
- DICOM PS3.3 2026c, C.8.2 CT Modules
- pydicom 3.0.2, pydicom.pixels.apply_rescale
- pydicom 3.0.2, pydicom.pixels.apply_windowing
- 3D Slicer Documentation, Coordinate systems