Skip to content

DICOM Basics

DICOM이란?

DICOM (Digital Imaging and Communications in Medicine) 은 의료 영상의 국제 표준 포맷이다. 일반 이미지(PNG, JPEG)와 가장 다른 점은 픽셀 데이터와 메타데이터(촬영 장비·환자·파라미터)가 한 파일에 묶여 있다는 점이다. 유방촬영(Mammography)에서 DICOM은 거의 모든 임상 데이터셋의 기본 포맷이다.

파일 구조 한눈에

graph LR
  A[DICOM File] --> B[Header / Tags]
  A --> C[Pixel Data]
  B --> B1["(0008,0060) Modality"]
  B --> B2["(0028,0004) PhotometricInterpretation"]
  B --> B3["(0028,0030) PixelSpacing"]
  B --> B4["(0028,1050/1051) WindowCenter/Width"]

태그는 (group, element) 쌍의 16진수 키로 식별되며, 표준 사전(DICOM Standard PS3.6)에 의미가 정의되어 있다.

Mammography에서 자주 보는 핵심 태그

태그 이름 일반적 값 (MG) 의미
(0008,0060) Modality MG Mammography
(0028,0004) PhotometricInterpretation MONOCHROME1 / MONOCHROME2 밝기 해석 방향
(0028,0100) BitsAllocated 16 픽셀당 할당 비트
(0028,0101) BitsStored 12 / 14 / 16 실제 저장 비트
(0028,0030) PixelSpacing 0.085\0.085 mm 픽셀당 물리 크기
(0028,1050) WindowCenter 임상 권장 WC 표시 윈도잉 기준
(0028,1051) WindowWidth 임상 권장 WW 표시 윈도잉 폭
(0028,1052) RescaleIntercept 보통 0 Modality LUT 절편
(0028,1053) RescaleSlope 보통 1 Modality LUT 기울기
(0018,1110) DistanceSourceToDetector mm SID, 기하 보정용
(0020,0062) ImageLaterality L / R 좌·우 유방
(0054,0220) ViewCodeSequence CC / MLO 촬영 뷰 (자세한 내용은 Views)

왜 16비트인가

Mammography 디텍터는 미세한 밀도 차이를 보존해야 해서 일반 X-ray보다 더 넓은 동적 범위를 사용한다. 디스플레이로 보낼 때는 Windowing으로 8비트(0~255)로 압축한다.

Photometric Interpretation 함정

PhotometricInterpretation 태그는 "픽셀 값이 클수록 어떻게 보여야 하는가"를 정의한다.

의미 표시
MONOCHROME2 값이 클수록 밝게 일반 그레이스케일
MONOCHROME1 값이 클수록 어둡게 반전 필요

MONOCHROME1을 그대로 화면에 보내면 흑백이 반전된 채로 표시된다. 반드시 다음과 같이 보정한다.

monochrome1_fix.py
1
2
3
4
5
6
7
8
import pydicom
import numpy as np

dcm = pydicom.dcmread("sample.dcm")
arr = dcm.pixel_array  # uint16

if dcm.PhotometricInterpretation == "MONOCHROME1":
    arr = arr.max() - arr  # 표시 방향 통일

Modality LUT — RescaleSlope/Intercept

저장된 픽셀값을 의미 있는 단위(예: HU, 광학 밀도)로 환산하는 첫 단계가 Modality LUT다. 흔한 형태는 선형 변환으로, RescaleSlopeRescaleIntercept만 있으면 된다.

\[ x_{\text{output}} = x_{\text{stored}} \cdot \text{RescaleSlope} + \text{RescaleIntercept} \]
modality_lut.py
1
2
3
slope     = float(getattr(dcm, "RescaleSlope", 1.0))
intercept = float(getattr(dcm, "RescaleIntercept", 0.0))
arr = arr.astype(np.float32) * slope + intercept

대부분의 mammography DICOM은 slope=1, intercept=0이라 무시해도 결과가 같아 보인다. 하지만 일부 디바이스·CT 파생 영상에서는 의미 있는 값이 들어 있어 빠뜨리면 강도 매핑이 어긋난다. 학습·평가 파이프라인에서는 안전하게 항상 적용한다. 이 단계가 끝난 값이 Windowing 단계의 입력이다.

이 한 줄을 빠뜨려서 모델 학습 데이터셋 전체의 좌우 유방 밝기가 뒤집힌 채로 학습되는 사고가 흔하다.

pydicom으로 시작하기

open_dicom.py
import pydicom

dcm = pydicom.dcmread("1_target.dcm")

print(dcm.Modality)                   # 'MG'
print(dcm.PhotometricInterpretation)  # 'MONOCHROME2'
print(dcm.Rows, dcm.Columns)          # 3816 3048
print(dcm.BitsAllocated, dcm.BitsStored)
print(dcm.PixelSpacing)               # [0.085, 0.085]

# 픽셀 배열은 numpy ndarray로 반환된다
arr = dcm.pixel_array                 # dtype=uint16
print(arr.min(), arr.max(), arr.dtype)

pydicom은 픽셀 데이터를 자동으로 압축 해제하지만, MONOCHROME1 반전은 직접 처리해야 한다. 또한 dcm.pixel_array가 반환하는 값은 raw 픽셀이지 디스플레이용 8비트가 아니다 — 그 다음 단계가 Windowing이다.

RAW 파일과의 차이

같은 검사에서 얻은 .raw(센서 직접 출력)와 .dcm(임상 표시용)을 비교하면 다음과 같은 차이가 보인다.

항목 RAW DCM
픽셀 범위 0 ~ 65535 0 ~ 65535
배경 밝기 높음 낮음
유방 밝기 낮음 높음
상관계수 RAW vs DCM ≈ −0.97 (역상관)
메타데이터 없음 풍부

RAW에서 DCM 수준의 표시 이미지를 만드는 과정에는 단순 반전 외에도 윈도잉, LUT, 유방 영역 마스킹 등 여러 단계가 끼어든다. 전체 흐름은 RAW → DCM 복원 참고.

출력 DICOM 패키징

처리 결과를 다시 DICOM으로 저장해 뷰어·PACS 워크플로우와 호환시키려면, 원본 헤더를 뼈대로 쓰되 몇 가지를 반드시 갱신해야 한다.

save_presentation_dicom.py
import pydicom, numpy as np

def save_as_presentation_dicom(img_u16, reference_dcm_path, output_path):
    dcm = pydicom.dcmread(reference_dcm_path)

    # 1) 새 UID — 원본 UID 유지 시 PACS가 원본을 덮어쓸 위험
    dcm.SOPInstanceUID    = pydicom.uid.generate_uid()
    dcm.SeriesInstanceUID = pydicom.uid.generate_uid()
    if hasattr(dcm, "file_meta"):
        dcm.file_meta.MediaStorageSOPInstanceUID = dcm.SOPInstanceUID

    # 2) 파생 영상 표시
    dcm.ImageType = ["DERIVED", "SECONDARY", "OTHER"]
    dcm.DerivationDescription = "Mammography Enhancement Pipeline Output"

    # 3) 픽셀 교체
    dcm.PixelData = img_u16.tobytes()
    dcm.Rows, dcm.Columns = img_u16.shape

    # 4) Window 레벨 = 전경 백분위 (배경 0 제외)
    fg = img_u16[img_u16 > 0]
    p_min, p_max = np.percentile(fg, (2, 98))
    dcm.WindowCenter = int((p_max + p_min) / 2)
    dcm.WindowWidth  = int(p_max - p_min)

    # 5) 16-bit 비부호 픽셀 + Padding
    dcm.BitsAllocated, dcm.BitsStored, dcm.HighBit = 16, 16, 15
    dcm.PixelRepresentation = 0
    dcm.add_new([0x0028, 0x0120], "US", 0)   # PixelPaddingValue

    pydicom.dcmwrite(output_path, dcm)
갱신 항목 이유
SOPInstanceUID / SeriesInstanceUID 신규 원본 UID 유지 시 PACS에서 원본을 덮어씀
ImageType = DERIVED/SECONDARY 원본이 아닌 파생 영상임을 명시
WindowCenter/Width 재계산 처리 후 강도 분포가 바뀌었으므로 (Windowing)
BitsStored/HighBit/PixelRepresentation 픽셀 dtype과 헤더 불일치 방지