공부정리/Computer Vision
[Dataset] 다이콤 영상을 동영상으로 변환하기 / Dicom file to avi
sillon
2024. 2. 2. 10:21
728x90
반응형
다이콤 영상을 동영상으로 변환하는 이유
다이콤 영상을 동영상으로 변환하기 전에, 다이콤파일의 View (영상의 촬영 기법)에 따른 분류를 해야한다.
여기서 반자동화된 방법을 선택하는데, View 별로 학습한 모델을 통해 View classfication에서 먼저 분류한 뒤, 일일이 검수하면서 해당 모델이 제대로 분류 했는지도 확인 하는 방법이 있다.
해당 포스팅에서는 View 별로 나눈 파일을 avi 파일로 변환하는 내용에 대해 다루겠다.
동영상 파일 변환 라이브러리
OpenCV
랑 Moviepy
등의 라이브러리가 있는데, 이번에는 moviepy 로 다루어 보겠다.
변환 순서
- 다이콤 파일 내의 프레임을 이미지로 변환
- 다이콤 파일 내부에서 추출할 영역 확인 (이미지 크롭 - 이건 선택사항)
- 다이콤 파일의 영상 속도 확인 -> 동영상으로 추출시 fps의 속도에 따라 적절히 변형해야함
- 최종적으로 처리한 이미지를 동영상의 프레임으로 변환한 뒤, 비디오 추출
참고! 원본 다이콤 파일의 경로를 '/'나 '\\'와 같은 경로 주소를 '+' 로 변환하여 비디오 이름 자체를 다이콤의 원본경로로 저장하면 나중에 찾기도 쉽고 작업도 용이함
예시
path = '/path/suyeon/good/dicomfile.dcm'
video_name = path.replace('/','+') + '.mp4'
print(video_name)
결과
+path+suyeon+good+dicomfile.dcm.mp4
Code
라이브러리 불러오기
import sys
import pydicom
import pandas as pd
import matplotlib.pyplot as plt
import glob
import os
from tqdm.auto import tqdm
import json
import numpy as np
from moviepy.editor import ImageSequenceClip
다이콤 파일의 프레임(이미지) 처리
"""
https://github.com/pydicom/pydicom/issues/205
"""
import cv2
from PIL import Image
import numpy as np
import pydicom
def get_pixel_data_with_lut_applied(dcm):
# For Supplemental, numbers below LUT are greyscale, else clipped
# Don't have a file to test, or know where this flag is stored in pydicom
SUPPLEMENTAL_LUT = False
if dcm.PhotometricInterpretation != "PALETTE COLOR":
raise Exception
if (
dcm.RedPaletteColorLookupTableDescriptor[0]
!= dcm.BluePaletteColorLookupTableDescriptor[0]
!= dcm.GreenPaletteColorLookupTableDescriptor[0]
):
raise Exception
if (
dcm.RedPaletteColorLookupTableDescriptor[1]
!= dcm.BluePaletteColorLookupTableDescriptor[1]
!= dcm.GreenPaletteColorLookupTableDescriptor[1]
):
raise Exception
if (
dcm.RedPaletteColorLookupTableDescriptor[2]
!= dcm.BluePaletteColorLookupTableDescriptor[2]
!= dcm.GreenPaletteColorLookupTableDescriptor[2]
):
raise Exception
if (
len(dcm.RedPaletteColorLookupTableData)
!= len(dcm.BluePaletteColorLookupTableData)
!= len(dcm.GreenPaletteColorLookupTableData)
):
raise Exception
lut_num_values = dcm.RedPaletteColorLookupTableDescriptor[0]
lut_first_value = dcm.RedPaletteColorLookupTableDescriptor[1]
lut_bits_per_pixel = dcm.RedPaletteColorLookupTableDescriptor[2] # warning that they lie though
lut_data_len = len(dcm.RedPaletteColorLookupTableData)
if lut_num_values == 0:
lut_num_values = 2 ** 16
if not (lut_bits_per_pixel == 8 or lut_bits_per_pixel == 16):
raise Exception
if lut_data_len != lut_num_values * lut_bits_per_pixel // 8:
# perhaps claims 16 bits but only store 8 (apparently even the spec says implementaions lie)
if lut_bits_per_pixel == 16:
if lut_data_len == lut_num_values * 8 / 8:
lut_bits_per_pixel = 8
else:
raise Exception
else:
raise Exception
lut_dtype = None
if lut_bits_per_pixel == 8:
lut_dtype = np.uint8
if lut_bits_per_pixel == 16:
lut_dtype = np.uint16
red_palette_data = np.frombuffer(dcm.RedPaletteColorLookupTableData, dtype=lut_dtype)
green_palette_data = np.frombuffer(dcm.GreenPaletteColorLookupTableData, dtype=lut_dtype)
blue_palette_data = np.frombuffer(dcm.BluePaletteColorLookupTableData, dtype=lut_dtype)
if lut_first_value != 0:
if SUPPLEMENTAL_LUT:
red_palette_start = np.arange(lut_first_value, dtype=lut_dtype)
green_palette_start = np.arange(lut_first_value, dtype=lut_dtype)
blue_palette_start = np.arange(lut_first_value, dtype=lut_dtype)
else:
red_palette_start = np.ones(lut_first_value, dtype=lut_dtype) * red_palette_data[0]
green_palette_start = np.ones(lut_first_value, dtype=lut_dtype) * green_palette_data[0]
blue_palette_start = np.ones(lut_first_value, dtype=lut_dtype) * blue_palette_data[0]
red_palette = np.concatenate((red_palette_start, red_palette_data))
green_palette = np.concatenate((green_palette_start, red_palette_data))
blue_palette = np.concatenate((blue_palette_start, red_palette_data))
else:
red_palette = red_palette_data
green_palette = green_palette_data
blue_palette = blue_palette_data
red = red_palette[dcm.pixel_array]
green = green_palette[dcm.pixel_array]
blue = blue_palette[dcm.pixel_array]
out = np.stack((red, green, blue), axis=-1)
if lut_bits_per_pixel == 16:
out = (out // 256).astype(np.uint8)
return out
def convert_ybr_to_rgb(arr):
if len(arr.shape) == 4:
return np.vstack([convert_ybr_to_rgb(a)[np.newaxis] for a in arr])
else:
temp = arr[..., 1].copy()
arr[..., 1] = arr[..., 2]
arr[..., 2] = temp
return cv2.cvtColor(arr, cv2.COLOR_YCR_CB2RGB)
def convert_monochrom_to_rgb(arr):
return cv2.cvtColor(arr, cv2.COLOR_GRAY2RGB)
def get_pixel_array_rgb(ds, frame_index=0):
# Extract the specific frame from the DICOM dataset
frame = ds.pixel_array[frame_index]
if ds.PhotometricInterpretation in ["YBR_FULL", "YBR_FULL_422"]:
# Convert YBR_FULL or YBR_FULL_422 to RGB
temp = frame[..., 1].copy()
frame[..., 1] = frame[..., 2]
frame[..., 2] = temp
rgb_frame = cv2.cvtColor(frame, cv2.COLOR_YCR_CB2RGB)
return rgb_frame
else:
# For other Photometric Interpretations, return the original frame
return frame
def ds2img(ds, idx=0):
try:
if ds.PhotometricInterpretation in ["MONOCHROME1", "MONOCHROME2"]:
if len(ds.pixel_array.shape) == 2:
arr = ds.pixel_array
img = Image.fromarray(arr).convert("L")
return img
elif len(ds.pixel_array.shape) == 3:
arr = ds.pixel_array
img = Image.fromarray(arr).convert("RGB")
return img
else:
if len(ds.pixel_array.shape) == 3:
arr = get_pixel_array_rgb(ds, idx)
img = Image.fromarray(arr).convert("RGB")
return img
elif len(ds.pixel_array.shape) == 4:
arr = get_pixel_array_rgb(ds, idx)
img = Image.fromarray(arr).convert("RGB")
return img
except AttributeError:
if len(ds.pixel_array.shape) == 3:
arr = get_pixel_array_rgb(ds, idx)
img = Image.fromarray(arr).convert("RGB")
return img
elif len(ds.pixel_array.shape) == 4:
arr = get_pixel_array_rgb(ds, idx)
img = Image.fromarray(arr).convert("RGB")
return img
다이콤 파일 변환
# 다이콤 파일 읽기
def read_dcm(dcm_dir):
with open(dcm_dir, "rb") as infile:
ds = pydicom.dcmread(infile)
return ds
# 영상 자르기
def crop_region(image, ds):
"""Crop area by header info"""
region_dict = extract_region_header(ds)
cropped_image = image[region_dict["min_y0"] : region_dict["min_y1"], region_dict["min_x0"] : region_dict["min_x1"], :]
return cropped_image
# 영상 변환하기
def dicom_to_video(dicom_ds, video_path):
# Check if the DICOM dataset has multiple frames
if len(dicom_ds.pixel_array.shape) == 4:
num_frames = dicom_ds.pixel_array.shape[0]
else:
num_frames = 1
# Set fps
manual_fps = 32
if hasattr(dicom_ds, "CineRate"):
fps = dicom_ds[0x0018, 0x0040].value
else:
if hasattr(dicom_ds, "FrameTime"):
fps = 1 / (dicom_ds[0x0018, 0x1063].value / 1000)
else:
fps = manual_fps
# print("Could not find frame rate. Fps set to default rate", manual_fps)
frames = []
for i in range(num_frames):
img = ds2img(dicom_ds, i) if num_frames > 1 else ds2img(dicom_ds)
img = np.array(img)
# print(img.shape) # h w c
try:
cropped_img = crop_region(img, dicom_ds)
# Convert PIL Image to numpy array
frame = cv2.cvtColor(cropped_img, cv2.COLOR_RGB2BGR)
# print(frame)
except:
frame = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
frames.append(frame)
clip = ImageSequenceClip(frames, fps=fps)
# 빠른 검수를 위해 저화질의 .avi 로 변환
clip.write_videofile(video_path + ".avi", fps=fps, codec="mpeg4") # .mp4 파일 변환시 코덱 변경해야함
변환하기
# 문자열 앞에 'r'은 파일 강제 읽기임 (생략가능)
dicom_path = r'/path/suyeon/good/dicomfile.dcm'
video_name = path.replace('/','+')
# dicom 파일 읽기
ds = read_dcm(dicom_path)
# 읽어온 DICOM 데이터셋을 사용하여 비디오 생성
video_path = os.path.join(dicom_path,video.name)
dicom_to_video(ds, video_path)
728x90
반응형