Oleg Pianykh, Digital Imaging and Communications in Medicine (DICOM) (Springer, 2008) 정리 7편
참고 챕터: Ch 10, Ch 11 + 책에 없는 PS3.18 (DICOMweb)
시리즈 마무리 글
- DICOM 파일 포맷 —
DICM헤더, Group 0002, DICOMDIR - 보안 & 익명화 — HIPAA, 환자 데이터 보호의 함정
- DICOMweb — 책에 없는 현대 PACS의 표준
- Orthanc 실전 통합 시나리오 — AI 추론 앱 전체 파이프라인
- 시리즈 정리 + 다음 학습 방향
1. DICOM 파일 포맷 구조
1.1 전체 레이아웃
┌──────────────────────────────────────┐
│ Preamble (128 bytes, 보통 모두 0) │
├──────────────────────────────────────┤
│ Magic: "DICM" (4 bytes) │
├──────────────────────────────────────┤
│ File Meta Information (group 0002) │ ← Explicit VR 강제
│ - Transfer Syntax UID │
│ - SOP Class UID │
│ - SOP Instance UID │
│ - ... │
├──────────────────────────────────────┤
│ Data Object (group 0008 이상) │ ← Meta에서 명시한 Transfer Syntax로
│ - Patient Name, ID, ... │
│ - Image Pixel Data │
│ - ... │
└──────────────────────────────────────┘
1.2 Preamble (128 byte)
- 첫 128 byte는 자유롭게 사용
- 표준은 내용을 지정 안 함
- 대부분의 구현이 그냥 0으로 채움
- TIFF 등 다른 포맷도 같은 관행
💡 일부 응용에서 활용 가능: 예를 들어 이 영역에 small thumbnail BMP를 넣어두면 비-DICOM 뷰어에서도 미리보기 가능. 거의 안 쓰임.
1.3 "DICM" Magic Header
- byte 129~132: 정확히
'D' 'I' 'C' 'M'4글자 - 이게 DICOM 파일 식별의 유일한 신뢰 가능한 방법
- 파일 확장자도, UID도, 이름도 다 못 믿음
def is_dicom_file(path):
with open(path, 'rb') as f:
f.seek(128)
return f.read(4) == b'DICM'
⚠️ 옛 ACR-NEMA 2.0 파일은 DICM 헤더 없음. 점점 드물어짐.
1.4 File Meta Information (Group 0002)
byte 133부터 시작. Explicit VR Little Endian으로 강제 인코딩.
| Tag | 이름 | VR | 필수 | 비고 |
|---|---|---|---|---|
(0002, 0000) |
Group Length | UL | ✅ | group 0002 전체 길이 |
(0002, 0001) |
File Meta Version | OB | ✅ | 0x00 0x01 |
(0002, 0002) |
Media Storage SOP Class UID | UI | ✅ | 이 파일이 어떤 IOD인지 |
(0002, 0003) |
Media Storage SOP Instance UID | UI | ✅ | 이 인스턴스의 UID |
(0002, 0010) |
Transfer Syntax UID | UI | ✅ | 데이터 영역 인코딩 방식 ⭐ |
(0002, 0012) |
Implementation Class UID | UI | ✅ | 누가 만들었나 |
(0002, 0013) |
Implementation Version Name | SH | ❌ | ≤16자! (6편 §7.2 함정) |
(0002, 0016) |
Source AET | AE | ❌ | 어느 AE가 마지막 수정 |
(0002, 0100) |
Private Info Creator UID | UI | ❌ | |
(0002, 0102) |
Private Information | OB | ❌ |
⭐
(0002, 0010) Transfer Syntax UID가 가장 중요. 이 값에 따라 그 뒤 Data Object를 Implicit/Explicit/압축 중 어떤 걸로 읽을지 결정.
1.5 Data Object (group 0008 이상)
- 우리가 2편에서 본 그 객체
- Meta의 Transfer Syntax UID에 따라 인코딩
- group 0008부터 시작 (group 0002 = meta, group 0004 = directory)
⚠️ 함정: 많은 DICOM 구현이 group 0002는 Explicit으로 잘 읽다가 Data 영역에서 Transfer Syntax 전환을 깜빡해서 깨짐.
1.6 ⚠️ 파일 이름 함정
DICOM 표준 PS3.10이 정의하는 File ID:
- 컴포넌트 8개, 각 8자 이내
- 대문자/숫자/언더스코어만 (
A-Z 0-9 _) - 컴포넌트는
\로 구분 (예:DIR1\SUBDIR\ABC123)
문제 1: 백슬래시 \ 는 DICOM 와일드카드!
"DIR1\SUBDIR\ABC123" = ?
→ 파일 경로?
→ 아니면 "DIR1 OR SUBDIR OR ABC123" ?
→ DICOM 소프트웨어가 헷갈림. 흔한 버그.
해결: 내부에서는 / 로 통일.
문제 2: .dcm 확장자
- 표준에 명시 안 됨 (어떤 곳은 prohibited, 어떤 곳은 required)
- 실무에선 다 쓰지만 공식적으로 모호함
문제 3: 실무에서는 SOP Instance UID를 파일명으로 쓰는 게 흔함
1.2.840.10008.234.2354.437345.79086.dcm
1.2.840.10008.5.1.4.1.1.4.20260601.103014.0042.dcm
- 장점: 항상 unique
- 단점: 너무 김 (Windows 경로 제한 260자 부딪힐 수 있음)
🛠 AI 추론 앱 / Orthanc 시 권장:
- 파일명에 환자 정보 절대 노출 X (보안)
- SOP Instance UID 기반 (전역 유일)
- 디렉토리 구조로 정리:
{StudyUID}/{SeriesUID}/{SOPInstanceUID}.dcm
2. DICOMDIR — 미디어 인덱스 파일
2.1 무엇인가
DVD/CD/USB 같은 이동식 미디어에 DICOM 영상을 담을 때 함께 들어있는 인덱스 파일.
DICOM_CD/
├── DICOMDIR ← 이 파일이 인덱스
├── IMAGES/
│ ├── STUDY01/
│ │ ├── SERIES01/
│ │ │ ├── IMG001
│ │ │ ├── IMG002
│ │ │ └── ...
│ │ └── SERIES02/
│ └── STUDY02/
└── ...
DICOM CD를 PC에 넣었을 때 뷰어가 "환자 / 검사 / 시리즈 / 영상 목록" 을 트리로 보여주는 게 DICOMDIR을 읽어서 만든 것.
2.2 구조
DICOMDIR도 하나의 DICOM 파일. group 0002 + Basic Directory IOD.
DICOMDIR (자체도 DICOM 파일)
├── Preamble + DICM
├── Group 0002 (File Meta)
└── Data Object (Basic Directory IOD)
├── (0004, 1130) File Set ID
├── (0004, 1200) Offset of first root record
├── (0004, 1220) Directory Record Sequence (SQ)
│ ├── Item 1: PATIENT record
│ ├── Item 2: STUDY record
│ ├── Item 3: SERIES record
│ ├── Item 4: IMAGE record (파일 위치 포함)
│ ├── Item 5: IMAGE record
│ └── ...
└── ...
2.3 Directory Record 종류
(0004, 1430) Directory Record Type:
PATIENTSTUDYSERIESIMAGE⭐ — 실제 파일 위치 포함OVERLAY,WAVEFORM,SR DOCUMENT, ...
각 IMAGE record에는:
(0004, 1500)Referenced File ID — 파일 경로(0004, 1510)Referenced SOP Class UID(0004, 1511)Referenced SOP Instance UID(0004, 1512)Referenced Transfer Syntax UID
→ DICOMDIR만 읽어도 어떤 환자 / 검사가 들어있는지 빠르게 파악 가능.
2.4 AI 추론 앱에서의 활용
병원 외부에서 받은 CD/DVD에 담긴 영상을 AI 추론 앱이 처리해야 할 때:
import pydicom
# DICOMDIR 읽기
dcmdir = pydicom.dcmread("D:/DICOMDIR")
# 환자/검사/시리즈/영상 트리 순회
for patient_record in dcmdir.patient_records:
print(f"Patient: {patient_record.PatientName}")
for study in patient_record.children:
print(f" Study: {study.StudyDate}")
for series in study.children:
print(f" Series: {series.Modality}")
for image in series.children:
# 실제 파일 위치
rel_path = "/".join(image.ReferencedFileID)
file_path = f"D:/{rel_path}"
ds = pydicom.dcmread(file_path)
# AI 추론 입력으로 사용
🛠 Orthanc 사용 시: REST API로 DICOMDIR 자동 생성 가능
GET /studies/{id}/media → DICOMDIR + 영상 zip
2.5 한계
- DICOMDIR 작성에는 모든 영상이 사전에 다 있어야 함 (length 미리 계산)
- 영상 추가/삭제 시 DICOMDIR 다시 빌드 필요
- 실무에서는 자동 생성에 맡기는 게 정신건강에 좋음 (Orthanc, dcm4chee 등)
3. DICOM 보안 — 평문 그대로 노출
3.1 ⚠️ 충격적 사실: DICOM 파일은 사실상 평문
저자의 hacking 데모. 메모장(WordPad)으로 열어도 환자 정보가 보임
(binary garbage) DICM (binary)
... SMITH^JOHN ... 19560423 ... M ... PATIENT123 ... DR.KIM ...
이름, ID, 생년월일이 그냥 ASCII로 박혀 있음.
3.2 평문 = 변조 가능
저자가 시연
- WordPad에서 DICOM 파일 열기
SMITH^JOE검색- 같은 길이(9자)로 다른 이름 입력 (
BETH^MARY) - 저장 → DICOM 뷰어에서 환자 이름이 바뀌어 보임
→ VR length가 변하지 않게 같은 길이로 바꾸면 DICOM 구조 그대로 유지되면서 데이터 변조됨. 어떤 DICOM 검증도 못 잡음.
3.3 ⚠️ 픽셀 추출도 단순함
저자의 또 다른 시연:
- CT/MR 영상이 보통 256×256 또는 512×512, 1 or 2 byte/pixel
- 파일 크기 보고 차원 추측
- 파일 끝에서 N×N×bytes 만큼 잘라내면 그게 픽셀
- BMP로 변환 가능
→ DICOM 파일은 암호화되지 않으면 평문. PACS 시스템 전체를 보호망으로 둘러싸야 함.
4. Workflow 보안 — 환경 단단히
4.1 기본 원칙 (HIPAA 베스트)
- 전용 서버에 격리 — 영상 서버에 다른 서비스 X
- 물리적 보안 — 잠긴 방, 출입 통제
- 백업 매일 — 영상 손실은 회복 불가
- 다른 의료기관과 서버 공유 절대 금지
- 사용자별 권한 분리 — 각자 자기 데이터만
- VPN + 방화벽
- 자동 화면 잠금 (15분 idle)
4.2 VPN vs 방화벽 — 자주 헷갈림
| 방화벽 | VPN | |
|---|---|---|
| 보호 범위 | 단일 컴퓨터 | 두 컴퓨터 간 통신 전체 |
| 통신 자체 암호화 | ❌ | ✅ |
| 외부 접근 차단 | ✅ | ✅ |
→ 방화벽만으로는 부족. 데이터가 공용망을 지나가면 평문 노출. VPN 필수.
4.3 ❌ 안티패턴
저자가 본 사고:
- "그냥 우리 서버에 로그인해서 봐주세요" — 가장 흔하고 가장 위험
- "옛날 DOS 컴퓨터가 보안이 낫다" — 정반대. 권한 관리 자체가 없음
- "컴퓨터를 안 쓰면 보안이 좋다" — 잠긴 문 없는 은행
4.4 ⭐ HIPAA가 정의한 18가지 PHI (Protected Health Information)
DICOM에서 제거/익명화 대상:
- 이름 (Name)
- 지리 정보 (주보다 작은 단위, 우편번호 포함)
- 날짜 (생년월일, 입원, 퇴원, 검사 등 모든 날짜)
- 전화번호
- 팩스
- 이메일
- 사회보장번호
- 의료기록번호
- 보험 수혜자 번호
- 계좌번호
- 자격증/면허 번호
- 차량 식별번호
- 장비 시리얼 번호
- URL
- IP 주소
- 생체 식별자 (지문, 음성)
- 얼굴 전체 사진 (CT/MR head로 3D 재구성 가능!)
- 기타 식별 가능한 모든 코드
⚠️ 17번이 의외의 함정. Head CT/MR을 3D rendering하면 얼굴 복원됨. 진정한 익명화 어려움.
5. 익명화 (Anonymization) ⭐⭐⭐
PACS 통신 모듈에서 가장 중요한 보안 작업.
5.1 익명화 정의
원본 파일에서 PHI를 제거하거나 변환해 환자 식별 불가능하게 만드는 작업.
- 비가역적 (irreversible) — 일반적으로 원본 복원 불가
- 그래서 암호화보다 안전: 해커도 없는 데이터는 못 뽑음
- 단점: 임상 가치 손상 가능성
5.2 단순 wipe의 함정
❌ 잘못된 익명화: Patient ID를 빈 값으로
(0010, 0020) Patient ID = ""
결과:
- DICOM 표준: Patient ID는 Type 1 (필수 + 값 필수)
- 빈 값 = 와일드카드로 해석될 수도 (3편 §2.2)
- 결과: 모든 환자가 한 명으로 merge → "Mr. Unknown" 사고
5.3 일관성 (Consistency) 함정
❌ 잘못된 익명화: 같은 환자를 다르게 매핑
Day 1: Patient ID "12345" → "anon_001"
Day 2: Patient ID "12345" → "anon_487" ← 다른 값!
결과: 같은 환자의 시간순 검사가 분리 → 추적 불가, 임상 가치 0.
✅ 올바른 익명화: 결정론적 매핑 (deterministic)
# 같은 원본 → 항상 같은 가명
import hashlib
def anonymize_patient_id(original_id, salt):
h = hashlib.sha256((original_id + salt).encode()).hexdigest()
return f"ANON_{h[:12]}"
anonymize_patient_id("12345", salt="my_secret")
# → 항상 "ANON_a8f3c4d2e9b1"
→ 매번 같은 결과. 같은 환자 = 같은 가명. 다른 환자 = 다른 가명.
5.4 충돌 함정
해시 출력 공간이 좁으면 다른 환자 → 같은 가명 가능.
# ❌ 위험: 16비트만 사용 → 65,536명 넘으면 collision
short_id = h[:4]
# ✅ 안전: 충분히 긴 prefix
long_id = h[:16] # 16^16 가지
5.5 픽셀에 박힌 정보 (Burned-in PHI) ⚠️
US, 일부 X-ray는 픽셀 데이터에 환자 이름이 그려져 있음.
┌──────────────────────────────────┐
│ PATIENT: SMITH^JOHN │ ← 이 부분이 픽셀!
│ ID: 12345 │ ← Tag로는 못 지움
│ DATE: 2026-06-10 │
│ ┌──────────────────────────────┐ │
│ │ │ │
│ │ [Ultrasound] │ │
│ │ │ │
│ └──────────────────────────────┘ │
└──────────────────────────────────┘
태그만 익명화하면 픽셀의 텍스트는 그대로 남음.
해결책:
- 단순: 상단 N픽셀을 검은색으로 (US 헤더 영역) → 임상 정보 손실 위험
- OCR로 텍스트 영역 인식 → 자동화 어려움
- 메타데이터
(0028, 0301) Burned In Annotation = "YES"가 있는지 확인 + 사람 검수
🛠 실무 추천: Burned-in 가능 modality (US, 일부 X-ray) 는 반드시 사람 검수 단계 거치기.
5.6 임상 가치 균형
저자의 노하우: HIPAA 18가지를 다 지우면 임상 데이터로 못 씀.
| Tag | HIPAA | 임상적 가치 | 권장 |
|---|---|---|---|
| Patient Name | ❌ | 낮음 | 익명화 |
| Patient ID | ❌ | 낮음 | 결정론적 매핑 |
| Birth Date | ❌ | 나이 계산에 필요 | 연도만 남기거나 나이로 변환 |
| Patient Age | ❌ | 중간 | 보통 남김 |
| Study Date | ❌ | 시계열 추적에 필수 | 남기는 게 표준 (저자 권장) |
| Study Description | ⚠️ | 높음 (예: "Prostate MR Protocol") | 남김 |
| Patient Weight | ❌ | 임상 중요 | 남김 |
| Patient Comments | ⚠️ | 가변 | 검토 후 결정 |
| Referring Physician | ❌ | 낮음 | 제거 |
| Institution Name | ❌ | 낮음 | 제거 또는 일반화 |
💡 저자: "Study Date를 지우면 진단 데이터로서 가치가 사라진다. read radiologist에게는 남기되, 공개 시에는 월 단위로 jitter (예: ±15일 random shift, 환자별 일관)."
5.7 ⚠️ 안 좋은 패턴 — 사설 태그로 숨기기
저자가 발견한 어처구니 없는 익명화 도구:
원본 (0010, 0010) Patient Name = "SMITH^JOHN"
→ 익명화 후:
(0010, 0010) = "ANON_X1" ← 가명
(0099, 1010) = "SMITH^JOHN" ← 원본을 사설 태그에 숨김 ⚠️
WordPad로 열면 그대로 보임. "숨기기"는 보안이 아니다.
5.8 권장 익명화 도구
표준 + 검증된 도구:
- DICOM PS3.15 Annex E — 공식 익명화 가이드라인
- gdcmanon (GDCM의 익명화) — 오픈소스
- dcm4che
dcm2dcm+ 익명화 프로파일 - pydicom-anonymizer (간단한 Python 도구)
- Orthanc 익명화 API ⭐
Orthanc 익명화 예시
import requests
# POST: Study 단위 익명화
r = requests.post(
"http://orthanc:8042/studies/{study_id}/anonymize",
json={
"Replace": {
"PatientName": "ANON_001",
"PatientID": "ANON_001"
},
"Keep": [
"StudyDescription",
"SeriesDescription",
"StudyDate" # 임상 가치 위해 유지
],
"KeepPrivateTags": False, # 사설 태그 제거
"Force": True,
"DicomVersion": "2021b" # PS3.15 프로파일 버전
}
)
# 결과: 새 익명 study가 생성됨
new_study_id = r.json()["ID"]
Orthanc는 PS3.15 Annex E 표준 프로파일을 자동 적용. 안전한 default.
6. 암호화 (Encryption)
6.1 익명화 vs 암호화
| 익명화 | 암호화 | |
|---|---|---|
| 가역성 | 비가역 | 가역 (키 있으면 복원) |
| 목적 | 정보 자체 제거 | 정보 숨김 |
| 임상 손실 | 가능 | 없음 |
| 위험 | 키 분실 시 영구 손실 | 키 분실 시 복호화 불가 |
6.2 알고리즘 — DICOM이 채택한 것들
| 알고리즘 | 종류 | 용도 |
|---|---|---|
| RSA | 비대칭 | 키 교환, 디지털 서명 |
| AES | 대칭 | 대용량 데이터 암호화 (현대 표준) |
| DES, 3DES | 대칭 | (구식, 점점 비추) |
| SHA-1/256/512 | 해시 | 무결성 검증 |
6.3 공개키/개인키 — 돼지저금통 비유 ⭐
저자의 비유:
- 돼지저금통: 동전 넣기는 쉬움, 꺼내기는 키 없으면 어려움
- 같은 원리: 공개키로 암호화는 쉬움, 개인키 없이 복호화는 사실상 불가능
수학적 기반: 큰 소수 인수분해
- 두 큰 소수 A × B = N (곱셈은 빠름)
- N에서 A, B 찾기 (인수분해는 매우 느림)
- A를 알려주면 B = N / A (나눗셈은 빠름)
→ 이게 RSA의 핵심.
6.4 SHA 무결성 검증
원본 데이터 → SHA-256 → 짧은 hash 문자열 (32 byte)
데이터 + hash 를 같이 전송
받는 쪽에서 다시 SHA-256 돌려서 일치 확인
→ 1바이트라도 변조되면 hash 완전히 다름
5편 §8.1의 Storage Commitment와 결합 가능: "받았는데 무결한가?"
6.5 디지털 서명 vs 스캔 서명
디지털 서명 ≠ 스캔된 사인 이미지.
❌ 가짜 디지털 서명:
- 사인을 사진으로 찍어서 PDF에 붙이기
- "Signed by: 김의사" 텍스트
- 단순 체크박스
✅ 진짜 디지털 서명:
- 인증기관(CA)에서 발급한 인증서
- 개인키로 서명
- 공개키로 검증
- 변조 시 무효화
- 서명 시점/위치/이유 기록
DICOM PS3.15 Annex C에서 정의. SR(Structured Report) 에 서명 첨부 가능.
6.6 Secure DICOM File Format (Ch 10.2.2)
PS3.10이 정의하는 보안 파일 포맷:
- 전체 DICOM 파일을 하나의 암호화된 객체로 래핑
- 키 없이는 group 0002조차 못 봄
- 디지털 서명 첨부 가능
⚠️ 현실: 거의 모든 PACS가 미지원. 표준이 너무 모호. VPN + 안전한 저장 환경으로 보안하는 게 표준 패턴.
7. DICOMweb — 책에 없지만 현대 PACS의 표준 ⭐
Pianykh 책 2008년판에는 없음. 하지만 현대 PACS 통신에서 빠질 수 없는 표준 (PS3.18, 2011~).
7.1 등장 배경
DIMSE의 한계 (6편 §13):
- 점-대-점 정적 등록 (AE Title + IP + Port 사전 등록)
- 방화벽 친화적이지 않음 (특히 C-Move)
- 모바일/웹/클라우드와 잘 안 맞음
DICOMweb의 해결:
- HTTP REST API 기반
- 정적 등록 불필요
- 방화벽 통과 (HTTPS 사용)
- JSON 응답 가능 (XML/DICOM도 가능)
7.2 3개 핵심 서비스
| DIMSE | DICOMweb | 역할 |
|---|---|---|
| C-Store | STOW-RS (Store Over the Web) | 영상 업로드 |
| C-Find | QIDO-RS (Query based on ID for DICOM Objects) | 검색 |
| C-Move / C-Get | WADO-RS (Web Access to DICOM Objects) | 다운로드 |
7.3 QIDO-RS — 검색
GET /studies?PatientID=12345&StudyDate=20260601-
Accept: application/dicom+json
응답 (JSON):
[
{
"0020000D": {"vr": "UI", "Value": ["1.2.840.xxx"]},
"00100010": {"vr": "PN", "Value": [{"Alphabetic": "SMITH^JOHN"}]},
"00100020": {"vr": "LO", "Value": ["12345"]},
"00080020": {"vr": "DA", "Value": ["20260601"]},
"00080060": {"vr": "CS", "Value": ["MR"]}
},
...
]
→ JSON으로 응답하니까 웹 프론트엔드에서 바로 사용 가능.
7.4 WADO-RS — 다운로드
Study 통째로
GET /studies/{StudyUID}
Accept: multipart/related; type="application/dicom"
→ Multipart 응답으로 모든 영상 받음.
단일 영상 메타데이터만 (JSON)
GET /studies/{StudyUID}/series/{SeriesUID}/instances/{SOPUID}/metadata
Accept: application/dicom+json
픽셀만 (rendered PNG/JPEG)
GET /studies/.../instances/.../rendered?window=400,40
Accept: image/jpeg
→ 브라우저에서 바로 표시 가능! 별도 DICOM 디코더 불필요.
7.5 STOW-RS — 업로드
POST /studies
Content-Type: multipart/related; type="application/dicom"
(여러 DICOM 파일을 multipart로 첨부)
7.6 DICOMweb의 장점
DIMSE DICOMweb
──────── ──────────
연결 점-대-점 정적 등록 HTTP 호출 즉시
인증 AE Title whitelist OAuth2, JWT, Bearer Token
방화벽 C-Move 특히 곤란 HTTPS 1포트만
프론트엔드 DICOM 라이브러리 필요 JSON + rendered JPEG
스케일링 제한적 로드밸런서 / CDN
캐싱 없음 HTTP 캐시
모니터링 custom 표준 HTTP 로그
7.7 Orthanc의 DICOMweb 플러그인
Orthanc는 공식 DICOMweb 플러그인 제공:
// orthanc.json
{
"Plugins": [
"/usr/share/orthanc/plugins/libOrthancDicomWeb.so"
],
"DicomWeb": {
"Enable": true,
"Root": "/dicom-web/",
"EnableMetadata": true,
"PublicRoot": "https://my-hospital.com/dicom-web/"
}
}
→ 자동으로
/dicom-web/studies(QIDO-RS)/dicom-web/studies/{uid}/series/...(WADO-RS)POST /dicom-web/studies(STOW-RS)
엔드포인트 활성화.
7.8 ⚠️ DIMSE를 안 버리는 이유
- 기존 PACS / modality의 90%가 DIMSE만 지원
- DICOMweb 지원 PACS는 신형/고급
- 현실의 통합 모듈은 DIMSE + DICOMweb 둘 다 지원해야 함
🛠 Orthanc는 양쪽 다 동시 지원. DIMSE로 받고 DICOMweb으로 노출 가능.
8. AI 추론 앱 전체 통합 시나리오 (실전)
이제까지 본 모든 개념을 종합한 현실적 배포 아키텍처.
8.1 아키텍처 다이어그램
┌─────────────────────────────────┐
│ 병원 내부 네트워크 │
│ │
┌───────────┐ │ ┌─────────────────────────┐ │
│ │ │ │ │ │
│ CT/MR │─DIMSE│ │ Hospital PACS │ │
│ Scanner │─────→│ │ (DIMSE + DICOMweb) │ │
│ │ │ │ │ │
└───────────┘ │ └────────┬────────────────┘ │
│ │ │
│ │ DIMSE (C-Store/Move)│
│ │ 또는 DICOMweb │
│ ↓ │
│ ┌─────────────────────────┐ │
│ │ Orthanc (게이트웨이) │ │
│ │ │ │
│ │ ┌─────────────────────┐ │ │
│ │ │ DIMSE SCP (4242) │ │ │
│ │ │ DICOMweb (8042) │ │ │
│ │ └─────────────────────┘ │ │
│ │ │ │
│ │ Lua / Python plugin │ │
│ │ └─→ AI 추론 트리거 │ │ │
│ └────────┬────────────────┘ │
│ │ │
│ ↓ │
│ ┌─────────────────────────┐ │
│ │ AI 추론 앱 │ │
│ │ (nnU-Net, etc.) │ │
│ │ │ │
│ │ ┌─────────────────────┐ │ │
│ │ │ Preprocessing │ │ │
│ │ │ Inference (GPU) │ │ │
│ │ │ Postprocessing │ │ │
│ │ │ DICOM SC/SR 생성 │ │ │
│ │ └─────────────────────┘ │ │
│ └────────┬────────────────┘ │
│ │ │
│ │ 결과 업로드 │
│ ↓ │
│ ┌─────────────────────────┐ │
│ │ Orthanc │ │
│ │ → DIMSE C-Store │ │
│ │ → Hospital PACS │ │
│ └──────────────────────────┘ │
└─────────────────────────────────┘
8.2 단계별 흐름
Step 1: 의사 트리거
PACS UI 또는 별도 시스템에서 "이 환자 AI 분석" 클릭.
Step 2: PACS → Orthanc로 영상 전달
방법 A (DIMSE):
PACS → DIMSE C-Store → Orthanc:4242
SOP Class UID: 1.2.840.10008.5.1.4.1.1.4 (MR)
Transfer Syntax: 1.2.840.10008.1.2 (Implicit VR LE)
방법 B (DICOMweb):
PACS → STOW-RS POST /dicom-web/studies → Orthanc:8042
Step 3: Orthanc Lua/Python 훅 → AI 트리거
-- orthanc.json: "LuaScripts": ["./on_store.lua"]
function OnStoredInstance(instanceId, tags, metadata)
-- 어떤 modality + body part?
if tags['Modality'] == 'MR' and
string.find(tags['StudyDescription'] or '', 'PROSTATE') then
-- AI 추론 앱 호출
os.execute('curl -X POST http://ai-app:8080/infer?instance=' .. instanceId)
end
end
Step 4: AI 추론
# AI 추론 앱 (Python)
@app.post("/infer")
def infer(instance: str):
# Orthanc에서 영상 다운로드 (DICOMweb)
r = requests.get(
f"http://orthanc:8042/instances/{instance}/file",
auth=("user", "pass")
)
ds = pydicom.dcmread(io.BytesIO(r.content))
# 추론용 numpy array
pixel_array = ds.pixel_array
# 추론
result = ai_model(pixel_array)
segmentation_mask = result['mask']
# Secondary Capture IOD 생성
sc = create_secondary_capture(
pixel_array=segmentation_mask,
patient_id=ds.PatientID, # 원본 유지
study_uid=ds.StudyInstanceUID, # 원본 유지!
series_uid=generate_uid(), # 새로 생성
sop_instance_uid=generate_uid(), # 새로 생성
sop_class_uid="1.2.840.10008.5.1.4.1.1.7" # SC
)
# Orthanc에 다시 업로드 → PACS로 자동 전달
requests.post(
"http://orthanc:8042/instances",
data=sc.tobytes(),
headers={"Content-Type": "application/dicom"}
)
Step 5: Orthanc → PACS로 결과 송신
Orthanc에 자동 라우팅 규칙 설정
-- 결과 SC가 들어오면 자동 PACS 전송
function OnStoredInstance(instanceId, tags, metadata)
if tags['Modality'] == 'SC' and
string.find(tags['SeriesDescription'] or '', 'AI_RESULT') then
SendToModality(instanceId, 'HOSPITAL_PACS')
end
end
Step 6: PACS UI에 결과 표시
PACS UI에서 환자 study 펼치면:
SMITH^JOHN — 2026-06-10 — Prostate MR
├── Series 1: T2W axial
├── Series 2: DWI
├── Series 3: ADC
└── Series 99: AI_RESULT (Secondary Capture) ← AI 추론 결과
8.3 익명화가 필요한 시나리오
연구 데이터로 외부 공유 시
# Orthanc 익명화 API
r = requests.post(
f"http://orthanc:8042/studies/{study_id}/anonymize",
json={
"Replace": {
"PatientName": deterministic_anonymize(name, salt),
"PatientID": deterministic_anonymize(pid, salt)
},
"Keep": ["StudyDescription", "SeriesDescription"],
"KeepPrivateTags": False,
"Force": True
}
)
# r.json()["Path"] = 익명화된 새 study의 URL
8.4 최종 체크리스트
배포 전 점검
☐ Orthanc + AI 앱이 분리된 네트워크 세그먼트
☐ Orthanc ↔ PACS DIMSE 양방향 등록 완료
☐ C-Echo 양방향 verify 통과
☐ Transfer Syntax 협상 OK (Implicit LE + 압축 옵션)
☐ Max PDU Length 조정 (필요시 65536)
☐ Orthanc DICOMweb 활성화
☐ AI 결과 SOP Class UID 올바름 (SC 또는 SR)
☐ 결과의 Patient/Study UID 원본 유지
☐ 결과의 Series/SOP Instance UID 새로 생성
☐ 익명화 (연구 데이터인 경우)
☐ 모든 통신 VPN 또는 HTTPS
☐ Orthanc 인증 활성화 (RegisteredUsers)
☐ 로그 보존 + 외부 SIEM 연동
☐ 백업 정책
☐ 장애 시 fallback (큐, 재시도, 알림)
9. 시리즈 마무리 — 전체 그림
큰 그림으로 정리한다.
9.1 7편 매핑
| 편 | 주제 | 핵심 개념 |
|---|---|---|
| 1편 | DICOM 입문 + VR | 27가지 VR, Endian, 짝수 길이 |
| 2편 | Tag, Dictionary, Encoding | (group,element), Implicit/Explicit, SQ |
| 3편 | 4계층 + IOD | Patient/Study/Series/Image, Module → IE → IOD |
| 4편 | C-Echo, C-Store, C-Find | AE Title, SCU/SCP, DIMSE |
| 5편 | C-Move, C-Get, MWL | Q/R, SC/SR (AI 결과 출력 포맷) |
| 6편 | Association ⭐ | Abstract/Transfer Syntax 협상, PDU, 디버깅 본진 |
| 7편 | 파일, 보안, DICOMweb, Orthanc | DICM 헤더, 익명화, REST API |
9.2 DICOM의 모순
아름다운 점:
- 1990년대 설계가 2026년에도 작동
- 객체지향 모델 (IOD/SOP)
- 모든 modality를 하나의 표준으로
불편한 점:
- 한 표준이 16권 문서로 분산
- 점-대-점 정적 등록의 한계
- "No reason given" 거부 메시지
- Implementation Version Name 16자 같은 사소한 함정
- 시간대 미지원
- 익명화 vs 임상 가치의 영원한 트레이드오프
9.3 책에서 다 못 다룬 것 (별도 학습 권장)
| 주제 | 학습 자료 |
|---|---|
| DICOMweb | DICOM PS3.18, dicomweb.org |
| DICOM SEG (Segmentation 전용 SOP) | PS3.3 Annex A.51 |
| DICOM SR Templates | David Clunie의 책 |
| IHE Profiles (워크플로우 통합) | ihe.net |
| HL7 FHIR + DICOM | FHIRcast |
| DICOM 표준 매뉴얼 (공식) | dicomstandard.org |
9.4 다음 단계 — 실전으로
이 시리즈가 이론 + 개념까지 줬다면, 다음은:
- Orthanc 직접 설치 + 가지고 놀기
- DIMSE C-Echo, C-Store 보내보기
- DICOMweb 호출해보기
- Lua 스크립트로 자동화
- pynetdicom 으로 작은 SCU/SCP 직접 짜보기
- 책의 표를 코드로 옮겨보기
- Wireshark로 DIMSE 패킷 보기
- A-Associate-RQ/AC 까보기
- 6편의 PDU 구조가 진짜로 그렇게 생겼나 확인
- PACS 벤더의 실제 Conformance Statement 읽기
- 시리즈 전체 개념이 어떻게 명세되어 있는지
- DICOM 익명화 도구 비교
- PS3.15 Annex E 프로파일
- Orthanc vs gdcm vs custom
10. 핵심 정리 (전체)
┌─────────────────────────────────────────────────────────────────┐
│ DICOM의 전체 구조 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 데이터 계층 │
│ ├── VR (27가지) ─── 1편 │
│ ├── Tag (group, element) ─── 2편 │
│ ├── Encoding (Implicit/Explicit) ─── 2편 │
│ ├── Patient/Study/Series/Image ─── 3편 │
│ └── IOD (Information Object Definition) ─── 3편 │
│ │
│ 서비스 계층 │
│ ├── SOP = DIMSE + IOD ─── 4편 │
│ ├── C-Echo / C-Store / C-Find ─── 4편 │
│ ├── C-Move / C-Get / MWL ─── 5편 │
│ └── Storage Commitment / SR / SC ─── 5편 │
│ │
│ 통신 인프라 │
│ ├── Association ─── 6편 │
│ ├── Abstract / Transfer Syntax ─── 6편 │
│ ├── Presentation Context ─── 6편 │
│ └── PDU ─── 6편 │
│ │
│ 파일 + 보안 │
│ ├── DICOM File Format ─── 7편 │
│ ├── DICOMDIR ─── 7편 │
│ ├── 익명화 / 암호화 ─── 7편 │
│ └── DICOMweb (현대 표준) ─── 7편 │
│ │
└─────────────────────────────────────────────────────────────────┘
DICOM은 이론은 단순하지만 실무가 까다로운 표준이다.
참고
- Pianykh, O.S. Digital Imaging and Communications in Medicine (DICOM). Springer, 2008.
- 공식 표준:
- PS3.10 — Media Storage and File Format
- PS3.11 — Media Storage Application Profiles
- PS3.12 — Media Formats and Physical Media
- PS3.15 — Security and System Management Profiles ⭐
- PS3.18 — Web Services (DICOMweb) ⭐
- 라이브러리/도구:
- Orthanc — DICOM 서버 + DICOMweb 게이트웨이
- pydicom — Python DICOM 파일 처리
- pynetdicom — Python DIMSE
- dcm4che — Java DICOM 도구
- GDCM — C++ DICOM 라이브러리 + 익명화
- 참고 사이트:
- DICOM Standard 공식
- DICOMweb 가이드
- DICOM Tag 검색 — 시각화된 dictionary
- 학습 자료:
- Clunie, D. DICOM Structured Reporting (2000)
- IHE Profiles (워크플로우 통합)
- NEMA의 DICOM webinar / Working Group 문서
'Dev > DICOM' 카테고리의 다른 글
| [DICOM] DICOM Association — PACS 통신 디버깅이 일어나는 곳 (0) | 2026.06.10 |
|---|---|
| [DICOM] 영상을 가져오는 방법 — C-Move, C-Get, Modality Worklist (0) | 2026.06.10 |
| [DICOM] DICOM 통신의 시작 — C-Echo, C-Store, C-Find (0) | 2026.06.10 |
| [DICOM] Patient/Study/Series/Image 4계층 + IOD — DICOM 정보 모델의 완성 (0) | 2026.06.02 |
| [DICOM] Tag, Data Dictionary, Object Encoding — DICOM 데이터를 바이트로 푸는 법 (0) | 2026.05.27 |
