공부정리/Computer Vision

[Dataset] 다이콤 영상을 동영상으로 변환하기 / Dicom file to avi

sillon 2024. 2. 2. 10:21
728x90
반응형

다이콤 영상을 동영상으로 변환하는 이유

다이콤 영상을 동영상으로 변환하기 전에, 다이콤파일의 View (영상의 촬영 기법)에 따른 분류를 해야한다.

여기서 반자동화된 방법을 선택하는데, View 별로 학습한 모델을 통해 View classfication에서 먼저 분류한 뒤, 일일이 검수하면서 해당 모델이 제대로 분류 했는지도 확인 하는 방법이 있다.

해당 포스팅에서는 View 별로 나눈 파일을 avi 파일로 변환하는 내용에 대해 다루겠다.

동영상 파일 변환 라이브러리

OpenCVMoviepy 등의 라이브러리가 있는데, 이번에는 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
반응형