python 으로 Aperio WSI 파일(.svs) 슬라이싱하기

2024. 3. 5. 18:22의학/연구

728x90

WSI 파일이란?

WSI(Whole Slide Image)란, 조직학/병리학 데이터를 고해상도로 저장하여 고배율로 확대 가능한 이미지를 말한다. 오늘 포스팅에서는 Aperio ImageScope 소프트웨어를 통해 열 수 있는 염색된 조직 WSI를 AI training을 위해 전처리하는 과정을 소개하고자 한다.
 
데이터셋은 KPMP(Kidney Precision Medicine Project)라는 kidney 슬라이드 오픈 데이터베이스에서 가져왔다. 아래 사진은 PAS staining한 슬라이드를 저배율(2x)로 관찰한 것이다.

 
아래 사진은 고배율(x20)로 관찰한 것이다. 사구체(glomerulus)가 잘 보인다.

 

이미지 슬라이싱 목표

본 프로젝트에서는 공개된 신장 슬라이드 이미지에 generative AI 모델을 적용하여 새로운 신장 슬라이드 이미지를 생성하는 것을 목표로 하고 있다. 따라서 슬라이드를 x20배율 정도의 크기로 자르고, 신장에 해당하는 영역만 분류해내어 training dataset을 만들 것이다.

이때 중요한 것은 신장이 아닌 영역이 매우 넓기 때문에, 어떻게 이를 효과적으로 배제할 수 있을지이다.
 

코드

.svs 파일을 python 개발환경에서 열기 위해서는 openslide와 cv2라는 모듈이 필요하다. cmd에 아래 명령어를 넣어 openslide 모듈을 설치하자.

python -m pip install openslide-python
python -m pip install opencv-python

 
다음과 같이 import를 쭉 해주자.

import cv2
import numpy as np
from openslide import OpenSlide, OpenSlideUnsupportedFormatError
from PIL import Image

 
여기서 cv2 모듈과 PIL 모듈 둘 다 새로운 디렉토리에 이미지를 저장하는 데 사용되는데, 둘 다 import한 이유는 cv2의 이미지 저장 메소드가 작동하지 않아 오류를 수정하는 과정에서 PIL을 사용하였다. 만약 cv2가 정상 작동한다면 PIL 모듈은 사용하지 않아도 된다.
 

이미지 슬라이싱&저장 함수 정의

 
새로 슬라이싱한 이미지는 다음과 같은 속성을 가진다.

  • 크기: 512 x 512 px
  • 배율: 원래 .svs 이미지의 20배
  • 저장 경로: 원래 .svs 이미지가 저장되어 있던 디렉토리에 sliced 폴더를 추가하여, 여기에 저장되도록 함

본 슬라이드는 PAS staining이었기 때문에, "보라색 느낌이 나는 셀이 조금이라도 없으면 폐기" 하는 식으로 이미지를 슬라이싱하였다. (만약 PAS가 아니라 다른 염색법을 사용한 슬라이드 분석을 한다면, 타겟으로 하는 색깔만 바꾸면 될 것 같다) 보라색으로 간주할 색깔의 상한선과 하한선을 정하고, 보라색 타일이 1% 이상이면 저장하고 아니면 폐기하였다.
 
이미지 저장 관련 코드는 cv2가 작동하지 않아 주석처리 해 놓았다. 또한 사이클이 한 번 돌 때마다 진행도를 x/w로 표시해 놓았다.

def extract_and_filter_images(svs_path, output_dir, desired_magnification=20):
    try:
        slide = OpenSlide(svs_path)
    except OpenSlideUnsupportedFormatError:
        print(f"Unsupported file format for {svs_path}")
        return

    # .svs 파일에서 사용 가능한 배율과 대응하는 다운샘플링 요인을 확인
    downsample = slide.level_downsamples[0]
    for ds in slide.level_downsamples:
        if slide.level_dimensions[0][0] / ds >= desired_magnification * 1000:
            downsample = ds
            break

    # 추출할 이미지의 크기를 설정 (512x512 픽셀)
    tile_size = (512, 512)
    lower_purple = np.array([125, 50, 50])
    upper_purple = np.array([155, 255, 255])

    # 이미지의 전체 크기를 계산
    w, h = slide.level_dimensions[0]
    w = int(w / downsample)
    h = int(h / downsample)

    # 이미지 타일을 추출하고 필터링
    for x in range(0, w, tile_size[0]):
            print(x/w)
            for y in range(0, h, tile_size[1]):
                img = slide.read_region((int(x * downsample), int(y * downsample)), 0, tile_size)
                img = np.array(img)[:, :, :3]  # RGBA to RGB
                
                # 이미지를 HSV 색상 공간으로 변환
                hsv_img = cv2.cvtColor(img, cv2.COLOR_RGB2HSV)
                
                # 정의한 보라색 범위 내의 픽셀만 추출하는 마스크 생성
                mask = cv2.inRange(hsv_img, lower_purple, upper_purple)
                
                # 마스크에 있는 픽셀의 비율 계산
                if np.sum(mask) / (tile_size[0] * tile_size[1] * 255) > 0.01:  # 적어도 1% 이상이 타겟 색상이어야 함
                # 해당 조건을 만족하는 타일만 저장
                    output_filepath = os.path.join(output_dir, f"tile_{x}_{y}.jpg")
                    print(output_filepath)
                    img_pil = Image.fromarray(img)
                    img_pil.save(output_filepath)
                    """
                    if not os.path.exists(output_dir):
                        os.makedirs(output_dir)
                    success = cv2.imwrite(output_filepath, img)
                    if not success:
                        print(f"Failed to save image {output_filepath}")
                    else:
                        print(f"Image saved to {output_filepath}")
                    """
                    
    print("Extraction and filtering completed.")

 
 
openslide 모듈은 windows 개발환경에서 작업할 때 중요한 에러 한 가지가 있다. 바로 DLL Hell이라고 부르는 것인데, openslide의 실행에 중요한 dll 파일이 windows 파일에 필수적인 dll 파일과 충돌을 일으키는 현상이다. 이를 해결하기 위해 제일 앞에 아래 코드를 추가하자.

### ---- 추가 ---- ###
import os
dll_dir  = "C:\\Program Files\\OpenSlide\\bin"
os.add_dll_directory(dll_dir)
######################

 
아래는 함수 호출을 위한 코드이다.

current_path = os.getcwd()
in_dir = os.path.join(current_path, "255e6f9d-8cc5-4818-9dca-fe28c3307d57_S-1905-017784_PAS_2of2.svs")
out_dir = os.path.join(current_path, "sliced")
extract_and_filter_images(in_dir, out_dir)

 

실행결과

 
슬라이싱한 결과, 대부분 조직을 많이 포함하고 있었으나 일부 슬라이드는 조직을 거의 포함하고 있지 않은 것들도 많았다. 보라색이 더 많은 슬라이드만 취하기 위해, 30% 이상이 타겟 색상인 것으로 커트라인을 대폭 올려 실행해 보았다.

 
결과적으로 꽤나 만족할 만한 슬라이싱이 이루어졌다. 여기서 augmentation에 못 쓸만한 데이터들은 수작업으로 골라 내어도 될 것 같다. 하나의 .svs 파일에서 총 747개의 슬라이싱된 이미지가 얻어졌다.

반응형