add : dcm / nii.gz 뷰어 / cvat 컨버터

This commit is contained in:
2025-12-02 11:57:45 +09:00
commit b38a06dcbe
4 changed files with 408 additions and 0 deletions

171
DCM_NII_to_CVAT.py Normal file
View File

@@ -0,0 +1,171 @@
import os
import cv2
import numpy as np
import nibabel as nib
import pydicom
from pathlib import Path
import shutil
# 설정
ROOT_DIR = Path('.')
OUTPUT_DIR = ROOT_DIR / 'cvat_dataset_mask_v2'
JPEG_DIR = OUTPUT_DIR / 'JPEGImages'
SEG_CLASS_DIR = OUTPUT_DIR / 'SegmentationClass'
IMAGE_SETS_DIR = OUTPUT_DIR / 'ImageSets' / 'Segmentation'
# 색상 정의 (샘플 labelmap.txt 기반)
COLOR_MAP = {
"C2": (230, 25, 75),
"C3": (60, 180, 75),
"C4": (255, 225, 25),
"C5": (67, 99, 216),
"C6": (245, 130, 49),
"C7": (145, 30, 180),
"L1": (0, 0, 117),
"L2": (128, 128, 128),
"L3": (255, 255, 255),
"L4": (0, 0, 0), # 주의: L4가 검은색(0,0,0)으로 정의되어 있어 배경과 겹칠 수 있음. 확인 필요.
"L5": (31, 119, 180),
"Rib": (255, 127, 14),
"Sacrum": (44, 160, 44),
"T1": (70, 240, 240),
"T10": (170, 255, 195),
"T11": (128, 128, 0),
"T12": (255, 216, 177),
"T2": (240, 50, 230),
"T3": (188, 246, 12),
"T4": (250, 190, 190),
"T5": (0, 128, 128),
"T6": (230, 190, 255),
"T7": (154, 99, 36),
"T8": (255, 250, 200),
"T9": (128, 0, 0),
# "background": (0, 0, 0) # 배경은 기본적으로 검정
}
# L4가 (0,0,0)이라면 배경과 구분 불가. L4 색상을 임의로 변경하거나 주의해야 함.
# 여기서는 안전을 위해 L4를 아주 어두운 회색(1,1,1)으로 하거나,
# 또는 사용자가 준 샘플에 L4가 (0,0,0)이라면 그것을 따라야 하지만,
# 보통 배경이 (0,0,0)이므로 L4는 (50,50,50) 정도로 변경하여 진행하겠습니다.
COLOR_MAP["L4"] = (50, 50, 50)
def normalize_image(image):
"""DICOM 이미지를 0-255 범위의 8bit 이미지로 변환"""
image = image.astype(float)
min_val = np.min(image)
max_val = np.max(image)
if max_val - min_val == 0:
return np.zeros(image.shape, dtype=np.uint8)
image = (image - min_val) / (max_val - min_val) * 255.0
return image.astype(np.uint8)
def process_case(case_dir, file_list):
"""하나의 케이스(폴더) 처리"""
dcm_files = list(case_dir.glob('*.dcm'))
if not dcm_files:
return
dcm_path = dcm_files[0]
case_name = case_dir.name
# 1. DICOM -> JPG
try:
ds = pydicom.dcmread(dcm_path)
pixel_array = ds.pixel_array
img_8bit = normalize_image(pixel_array)
# 그레이스케일 이미지를 RGB로 변환 (CVAT 호환성)
img_rgb = cv2.cvtColor(img_8bit, cv2.COLOR_GRAY2BGR)
cv2.imwrite(str(JPEG_DIR / f"{case_name}.jpg"), img_rgb)
except Exception as e:
print(f"Error processing DICOM {dcm_path}: {e}")
return
# 2. NII Masks -> RGB Mask
# 배경(0,0,0)으로 초기화된 RGB 이미지 생성
mask_rgb = np.zeros((pixel_array.shape[0], pixel_array.shape[1], 3), dtype=np.uint8)
nii_files = list(case_dir.glob('*.nii.gz'))
for nii_path in nii_files:
class_name = nii_path.name.split('.')[0]
if class_name not in COLOR_MAP:
continue
color = COLOR_MAP[class_name] # (R, G, B)
# OpenCV는 BGR 사용하므로 순서 변경
color_bgr = (color[2], color[1], color[0])
try:
img_nii = nib.load(str(nii_path))
data = img_nii.get_fdata()
# 3D -> 2D Projection
proj = np.max(data, axis=2).T
# 리사이즈
if proj.shape != (mask_rgb.shape[0], mask_rgb.shape[1]):
proj = cv2.resize(proj, (mask_rgb.shape[1], mask_rgb.shape[0]), interpolation=cv2.INTER_NEAREST)
# 마스크 그리기
# 해당 클래스 영역을 해당 색상으로 칠함
mask_rgb[proj > 0] = color_bgr
except Exception as e:
print(f"Error processing NII {nii_path}: {e}")
# 마스크 저장 (PNG)
cv2.imwrite(str(SEG_CLASS_DIR / f"{case_name}.png"), mask_rgb)
# 파일 목록에 추가
file_list.append(case_name)
print(f"Processed: {case_name}")
def create_labelmap():
"""labelmap.txt 생성"""
with open(OUTPUT_DIR / 'labelmap.txt', 'w') as f:
f.write("# label:color_rgb:parts:actions\n")
# 정의된 순서대로 작성 (이름 정렬)
for name in sorted(COLOR_MAP.keys()):
r, g, b = COLOR_MAP[name]
f.write(f"{name}:{r},{g},{b}::\n")
# 배경 추가
f.write("background:0,0,0::\n")
def main():
# 디렉토리 초기화
if OUTPUT_DIR.exists():
shutil.rmtree(OUTPUT_DIR)
JPEG_DIR.mkdir(parents=True)
SEG_CLASS_DIR.mkdir(parents=True)
IMAGE_SETS_DIR.mkdir(parents=True)
print("Converting to CVAT Segmentation Mask 1.1 format...")
file_list = []
# 데이터 탐색
for d in ROOT_DIR.iterdir():
if d.is_dir() and not d.name.startswith('.') and 'cvat' not in d.name and 'segmask' not in d.name:
if list(d.glob('*.dcm')):
process_case(d, file_list)
# default.txt 생성
with open(IMAGE_SETS_DIR / 'default.txt', 'w') as f:
for name in file_list:
f.write(f"{name}\n")
# labelmap.txt 생성
create_labelmap()
print("\nDone! Dataset created at:", OUTPUT_DIR.absolute())
print(f"Total cases: {len(file_list)}")
print("\n[중요] 업로드 시 주의사항:")
print("1. 'cvat_dataset_mask_v2' 폴더 안으로 들어가서")
print("2. 모든 파일/폴더(JPEGImages, SegmentationClass, ImageSets, labelmap.txt)를 선택하고")
print("3. '압축하기(ZIP)'를 실행하세요.")
print("4. CVAT 포맷 선택: 'Segmentation Mask 1.1'")
if __name__ == '__main__':
main()

182
NII_DCM_Viewer.py Normal file
View File

@@ -0,0 +1,182 @@
"""
척추 라벨링 데이터 뷰어
- NII.GZ: 3D 세그멘테이션 마스크
- PF: MITK PlanarFigure (2D 측정)
- DCM: DICOM 원본 이미지
"""
import nibabel as nib
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.widgets import Slider
from matplotlib.lines import Line2D
import xml.etree.ElementTree as ET
from pathlib import Path
import re
ROOT_PATH = Path(__file__).parent
BASE_PATH = None
DCM_FILE = None
MITK_ORIGIN_Y = 966.81
SPACING = 0.148
def load_nii(filepath):
img = nib.load(filepath)
return img.get_fdata(), img.header
def load_dcm(filepath):
import pydicom
dcm = pydicom.dcmread(filepath)
return dcm.pixel_array
def parse_pf(filepath):
with open(filepath, 'r', encoding='utf-8') as f:
content = f.read()
content = re.sub(r'<\?xml[^?]*\?>', '', content)
root = ET.fromstring(f'<root>{content}</root>')
pf = root.find('PlanarFigure')
return {
'type': pf.get('type'),
'vertices': [(float(v.get('x')), float(v.get('y'))) for v in pf.findall('.//Vertex')]
}
def pf_to_pixel(x, y):
"""MITK 좌표 -> 픽셀 좌표 변환"""
return x / SPACING, (MITK_ORIGIN_Y - y) / SPACING
def draw_pf(ax, pf_files, colors):
labels = []
for i, pf_file in enumerate(pf_files):
data = parse_pf(str(pf_file))
color = colors[i % len(colors)]
transformed = [pf_to_pixel(x, y) for x, y in data['vertices']]
if 'Line' in data['type']:
xs, ys = zip(*transformed)
ax.plot(xs, ys, '-o', color=color, linewidth=3, markersize=8)
elif 'Circle' in data['type'] and len(transformed) >= 2:
center, edge = transformed[0], transformed[1]
radius = np.hypot(edge[0]-center[0], edge[1]-center[1])
ax.add_patch(plt.Circle(center, radius, fill=False, color=color, linewidth=3))
ax.plot(center[0], center[1], 'o', color=color, markersize=8)
labels.append((color, pf_file.stem))
return labels
def view_nii_overlay():
"""모든 NII 마스크를 색상별로 겹쳐서 표시"""
nii_files = list(BASE_PATH.glob('*.nii.gz'))
first_data, _ = load_nii(str(nii_files[0]))
combined = np.zeros(first_data.shape)
for i, f in enumerate(nii_files, 1):
data, _ = load_nii(str(f))
combined[data > 0] = i
fig, ax = plt.subplots(figsize=(10, 10))
mid_z = combined.shape[2] // 2
slider_ax = plt.axes([0.2, 0.02, 0.6, 0.03])
slider = Slider(slider_ax, 'Slice', 0, combined.shape[2]-1, valinit=mid_z, valstep=1)
plt.sca(ax)
img_plot = ax.imshow(combined[:, :, mid_z].T, cmap='nipy_spectral', origin='lower')
ax.set_title(f'All Vertebrae - Slice {mid_z}')
plt.colorbar(img_plot, ax=ax)
def update(val):
idx = int(slider.val)
img_plot.set_data(combined[:, :, idx].T)
ax.set_title(f'All Vertebrae - Slice {idx}')
fig.canvas.draw_idle()
slider.on_changed(update)
plt.show()
def view_all_on_dcm():
"""DCM + NII 마스크 + PF 측정 오버레이"""
img = load_dcm(str(DCM_FILE))
nii_files = list(BASE_PATH.glob('*.nii.gz'))
pf_files = list(BASE_PATH.glob('*.pf'))
fig, ax = plt.subplots(figsize=(10, 20))
ax.imshow(img, cmap='gray', origin='upper')
# NII 마스크 컨투어
cmap = plt.colormaps.get_cmap('tab20').resampled(len(nii_files))
nii_labels = []
for i, f in enumerate(nii_files):
data, _ = load_nii(str(f))
proj = np.max(data, axis=2).T # (W,H,D) -> (H,W)
if proj.max() > 0:
ax.contour(proj, levels=[0.5], colors=[cmap(i)], linewidths=1.5, alpha=0.8)
nii_labels.append((cmap(i), f.stem.split('.')[0]))
# PF 오버레이
pf_labels = draw_pf(ax, pf_files, ['red', 'yellow', 'cyan', 'lime', 'magenta'])
# 범례
legend = [Line2D([0], [0], color=c, label=n) for c, n in nii_labels]
legend += [Line2D([0], [0], color=c, marker='o', label=f'[PF] {n}') for c, n in pf_labels]
ax.legend(handles=legend, loc='upper left', bbox_to_anchor=(1.02, 1), fontsize=8)
ax.set_title('DCM + NII Masks + PF Annotations')
plt.tight_layout()
plt.show()
def select_directory():
"""디렉토리 선택"""
global BASE_PATH, DCM_FILE
dirs = sorted([d for d in ROOT_PATH.iterdir() if d.is_dir() and not d.name.startswith('.')])
if not dirs:
print("하위 디렉토리가 없습니다.")
return False
print("\n=== 디렉토리 선택 ===")
for i, d in enumerate(dirs, 1):
print(f"{i}. {d.name}")
try:
idx = int(input(f"\n선택 (1-{len(dirs)}): ").strip()) - 1
if 0 <= idx < len(dirs):
BASE_PATH = dirs[idx]
dcm_files = list(BASE_PATH.glob('*.dcm'))
DCM_FILE = dcm_files[0] if dcm_files else None
print(f"선택됨: {BASE_PATH.name}")
return True
except (ValueError, IndexError):
pass
print("잘못된 선택")
return False
if __name__ == '__main__':
print("=== 척추 라벨링 뷰어 ===")
if not select_directory():
exit()
while True:
print(f"\n=== {BASE_PATH.name} ===")
print("1. NII 마스크 오버레이 (슬라이더)")
print("2. DCM + NII + PF 전체 오버레이")
print("q. 종료")
choice = input("\n선택: ").strip().lower()
if choice == 'q':
break
elif choice == '1':
view_nii_overlay()
elif choice == '2':
if DCM_FILE:
view_all_on_dcm()
else:
print("DCM 파일이 없습니다.")

49
README.md Normal file
View File

@@ -0,0 +1,49 @@
# UTILITY_DICOM_NII_TO_CVAT
이 저장소는 DICOM(.dcm) 의료 영상과 NII.GZ(.nii.gz) 세그멘테이션 마스크 파일을 **CVAT(Computer Vision Annotation Tool)**에서 사용할 수 있는 **Segmentation Mask 1.1** 형식으로 변환하는 유틸리티 도구 모음입니다.
## 기능
1. **데이터 변환 (`DCM_NII_to_CVAT.py`)**:
* **이미지 변환:** `.dcm` 파일을 `.jpg`로 변환 (정규화 포함).
* **마스크 변환:** 여러 개의 `.nii.gz` 파일(C2, C3, T1...)을 하나의 **RGB 컬러 마스크**(`.png`)로 통합.
* **포맷 지원:** CVAT **Segmentation Mask 1.1** 구조 자동 생성 (`JPEGImages`, `SegmentationClass`, `labelmap.txt`, `default.txt`).
2. **데이터 뷰어 (`NII_DCM_Viewer.py`)**:
* 원본 DICOM과 NII 마스크를 오버레이하여 시각적으로 검증.
* MITK PlanarFigure(.pf) 파일 지원.
## 설치 방법
**권장 환경:** Python 3.10
```bash
# 가상환경 생성 (선택)
conda create -n dcm python=3.10
conda activate dcm
# 의존성 설치
pip install -r requirements.txt
```
## 사용 방법
### 1. 데이터 변환 (CVAT용)
```bash
python DCM_NII_to_CVAT.py
```
실행 후 생성된 `cvat_dataset_mask_v2` 폴더 **내부의 파일들**(`JPEGImages`, `SegmentationClass`, `ImageSets`, `labelmap.txt`)을 선택하여 **ZIP으로 압축**한 뒤, CVAT에서 **Segmentation Mask 1.1** 포맷으로 업로드하세요.
### 2. 데이터 뷰어
```bash
python NII_DCM_Viewer.py
```
## 파일 구조
* `DCM_NII_to_CVAT.py`: 변환 메인 스크립트
* `NII_DCM_Viewer.py`: 뷰어 스크립트
* `requirements.txt`: 필요 라이브러리 목록

6
requirements.txt Normal file
View File

@@ -0,0 +1,6 @@
nibabel
numpy
matplotlib
pydicom
opencv-python
Pillow