Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[7주차 1 / 주차 2] OpenSlide: 큰 이미지 파일을 작은 부분으로 나눠 분석하는 방법 #83

Merged
merged 16 commits into from
Dec 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions hong/backend/backend-roadmap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
## Backend Developer 로드맵

이직.. 참 힘들죠.. 공부할 게 비처럼 내려요.. 햐..

최근에 이직을 준비하셨던 시니어 개발자분과 이런저런 이야기를 하면서 팁 몇 가지를 공유해 주셨는데, 그중에 제 뒷목을 잡게 만든 `roadmap.sh`라는 웹사이트를 공유하려고 합니다.

이 하찮은 글이 이직을 준비하는 만백억 여러분께 아주 조금의 도움이라도 되길.. 바라요..✨

### roadmap.sh 설명

![roadmap.sh 웹사이트 홈페이지](/hong/img/roadmap/roadmap-sh-home.png) <br/>
_roadmap.sh 웹사이트 홈페이지_

> [roadmap.sh](https://roadmap.sh)는 프론트엔드, 백엔드, 데브옵스, 머신러닝, 블록체인 등 다양한 분야의 로드맵을 제공하고 있습니다.

이 웹사이트는 특정 path로 가고자 하는 개발자에게 어떻게 공부를 해야하는지 가이드와 교육 컨텐츠를 제공합니다. 홈페이지에 가면 바로 Frontend, Backend, DevOps 등 다양한 직군과 기술에 대한 콘텐츠가 준비되어 있는 것을 볼 수 있습니다.

이런 콘텐츠는 시간이 지나면 트렌드를 반영하지 못하는 경우가 많다고 생각할 수도 있겠지만, 이 웹사이트는 위키피디아처럼 커뮤니티가 지속해서 컨텐츠를 업데이트하는 방식으로 컨텐츠를 추가하고 보강한다고 하네요. 그래서 최신 기술과 트렌드가 로드맵에 잘 반영된다고 해요. 2024년 자료도 이미 나와 있네요!

### Backend Developer Roadmap

자, 마음의 준비를 해주세요.. 공부할 게 너무 많거든요..

준비되셨으면.. 일단 Start Here부터 클릭해 보세요.
상단의 Start Here를 클릭하면 직군별로 어느 순서대로 어떤 공부를 해야 하는지 알려줍니다!

![Backend Developer](/hong/img/roadmap/start-here-backend.png) <br/>
_Backend Developer 로드맵_

![Frontend Developer](/hong/img/roadmap/start-here-frontend.png) <br/>
_Frontend Developer 로드맵_

가장 왼쪽 카드는 해당 직군의 전반 지식을 어떤 순서로 공부해야 하는지를 보여주고, 가운데 카드는 해당 직군의 대표적인 언어와 기술 등을 보여줍니다. 오른쪽 카드는 해당 직군의 대표적인 시스템 디자인, 프레임워크 등을 보여줍니다.

이 외에도 DevOps, Mobile Developer, 심지어 AI & ML 등 다양한 로드맵이 준비되어 있습니다. 카드마다 구성 요소가 조금씩 다르니 한 번씩 클릭해서 보시면 좋을 것 같아요.

한 카드를 클릭하면 아래와 같은 로드맵을 보실 수 있습니다.

![Backend Developer Roadmap](/hong/img/roadmap/backend-roadmap.jpg) <br/>
_이게 다 내가 배워야 할 주제 중 하나인 것.._

이렇게 로드맵으로 보니까 지금까지 내가 얼마나 대충 배워왔는지 알 수 있어요... 그리고 어떤 것을 더 공부해야 하는지도 알 수 있죠...

그리고 로드맵을 보면 어떤 순서로 어떤 것을 공부해야 하는지 한눈에 알 수 있어서 정말 편리하고 기억하기에도 너무 좋다고 생각해요! 그리고 내가 얼마나 공부했는지 체크할 수 있는 기능도 있어서 까먹고 놓치는 것 없이 매일 조금씩 배울 수 있죠.

그럼 하나를 선택해서 공부해 볼까요?

![detail resources](/hong/img/roadmap/detail-resources.png) <br/>
_가장 첫번째 항목인 Internet 노드 선택 시 보여주는 컨텐츠_

예를 들어 Internet 항목을 선택하면 아래와 같이 해당 항목에 대한 컨텐츠를 볼 수 있습니다. 개략적인 설명과 외부 블로그나 공식 웹사이트 등 해당 주제를 공부할 수 있는 링크를 제공하는 방식으로 내용이 구성되어 있어요.

자, 이제 모든 카드를 이렇게 하나씩 클릭하면서 공부하면 되겠죠? 와~ 쉽다~

129 changes: 129 additions & 0 deletions hong/backend/how-to-slice-large-image-file.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
## 배경

큰 이미지를 분석할 때 작은 크기의 이미지(예: 100x100 사이즈)로 잘라서 부분부분 분석한 다음 결과를 모두 종합하여 최종 결과를 만들어 내는 방식을 백엔드에서 지원해야 한다면 어떻게 할 수 있을까 조사했습니다.

## 문제 분석

### 1. 파일 크기 문제

파일 크기가 일정 사이즈를 초과한다면 일반적으로 잘 알려진 open-source 라이브러리로 처리할 때 최대 사이즈를 초과하는 이미지라는 Exception을 던진다.

예:

- **ImageIO (Java)**

- ImageIO 라이브러리에서 허용하는 최대 이미지 사이즈는 **2,147,483,647 pixels** 이다. (2GB)

![스크린샷 2024-03-17 183337](https://github.com/10000-Bagger/free-topic-study/assets/34956359/d044f4a4-ddac-490e-8ccb-2a4fe1cfb78b)

- **Pillow (Python)**

- Pillow 라이브러리에서 허용하는 최대 이미지 사이즈는 **178,956,970 pixels** 이다.

![스크린샷 2024-03-17 182407](https://github.com/10000-Bagger/free-topic-study/assets/34956359/5e4631d6-6500-429a-88e3-d6da1c5688a8)

- **OpenCV (Python)**
- OpenVC 라이브러리에서 허용하는 최대 이미지 사이즈는 **1,073,741,824 pixels** 이다.

![스크린샷 2024-03-17 182436](https://github.com/10000-Bagger/free-topic-study/assets/34956359/4d27f703-bbb1-40df-b3e2-68bab647a8c0)

## 문제 해결

큰 파일을 읽어내고 작은 파일로 잘라내거나 바로 접근이 가능하도록 지원하는 라이브러리를 찾아본 결과, **OpenSlide** 라이브러리를 사용하면 큰 이미지 파일을 효율적으로 처리할 수 있다는 것을 알게 되었다.

### OpenSlide

> OpenSlide is a C library that provides a simple interface to read whole-slide images (also known as virtual slides)

OpenSlide은 원래 Whole-slide image (WSI) 라는 유리 슬라이드를 스캔하여 디지털 슬라이드로 만든 파일 형식을 지원하기 위해 개발된 Open source 라이브러리이다. 하지만, 해당 라이브러리를 활용해서 JPEG, JPEG 2000, PNG 등 다양한 이미지 파일을 응용 프로그램으로 컨트롤할 수 있는 기능을 지원한다.

공식 지원하는 언어는 Java와 Python이며, Python이 Java보다 기능 측면에서 더 많은 기능을 지원한다.

예를 들어, Python library는 원본 이미지에서 특정 부분을 뽑아 낼 수 있는 기능인 Deep Zoom generator 기능이 포함되어 있다. 하지만, Java library에는 해당 기능이 빠져 있어 원본 이미지의 파일을 부분으로 잘라서 사용하기 위해서는 Python library를 사용해야 한다.

### OpenSlide 사용 방법

Python에서 OpenSlide와 DeepZoomGenerator를 통해 부분 이미지를 추출한 파일(Patch)을 생성하는 방법은 아래와 같다.

```python
import openslide
from openslide import OpenSlide
from openslide.deepzoom import DeepZoomGenerator
from io import BytesIO

DEEPZOOM_FORMAT = 'jpeg'

# Given a slide path
openslide_obj = OpenSlide(path)
slide = DeepZoomGenerator(openslide_obj, **self.dz_opts)
mpp_x = openslide_obj.properties[openslide.PROPERTY_NAME_MPP_X]
mpp_y = openslide_obj.properties[openslide.PROPERTY_NAME_MPP_Y]

slide.map = (float(mpp_x) + float(mpp_y)) / 2
dzi = slide.get_dzi(DEEPZOOM_FORMAT)

# Given level, col, row info
tile = slide.get_tile(level, col, row)
buf = BytesIO()
tile.save(buf, "jpeg", quality=DEEPZOOM_TILE_QUALITY)
return buf.getvalue(
```

각 라인에 대한 설명은 아래와 같다.

```python
openslide_obj = OpenSlide(path)
```

- OpenSlide를 사용하여 전체 슬라이드 이미지 열기. 'path'는 WSI 파일 실제 경로를 입력.

```python
slide = DeepZoomGenerator(openslide_obj, **self.dz_opts)
```

- 선언된 슬라이드로 DeepZoomGenerator를 초기화. self.dz_opts는 DeepZoomGenerator에 대한 옵션들이며, 예를 들어, 타일 크기 및 overlap 등이 포함되어야 함.

```python
mpp_x = openslide_obj.properties[openslide.PROPERTY_NAME_MPP_X]
mpp_y = openslide_obj.properties[openslide.PROPERTY_NAME_MPP_Y]
```

- 슬라이드 속성에서 X 및 Y 차원의 Microns Per Pixel(mpp)을 검색. 미크론 단위 픽셀은 픽셀 당 얼마의 micrometer 가 담기느냐를 나타내는 수치 ([링크](https://biology-statistics-programming.tistory.com/215))

```python
slide.map = (float(mpp_x) + float(mpp_y)) / 2
```

- 평균 미크론 단위 픽셀(mpp)을 매핑 요소로 계산

```python
dzi = slide.get_dzi(DEEPZOOM_FORMAT)
```

- 슬라이드에 대한 Deep Zoom Image (DZI) 형식 정보 가져오기. DZI 형식은 DeepZoom이 다양한 줌 레벨에서 이미지가 타일로 어떻게 나뉘어지는지 설명하는 데 사용.

```python
tile = slide.get_tile(level, col, row)
```

- 줌 레벨, 열, 행을 주어 특정 타일 검색

```python
tile.save(buf, "jpeg", quality=DEEPZOOM_TILE_QUALITY)
```

- 타일을 저장하기 위한 메모리 내 바이트 버퍼 준비

```python
tile.save(buf, "jpeg", quality=DEEPZOOM_TILE_QUALITY)
```

- 지정된 품질로 JPEG 형식으로 버퍼에 타일 저장

```python
return buf.getvalue()
```

- 버퍼의 내용, 즉 타일 이미지 데이터 반환

위와 같은 방식으로 WSI 파일에서 지정된 사이즈의 patch로 이미지를 뽑아올 수 있다.
132 changes: 132 additions & 0 deletions hong/backend/technique-to-upload-large-file.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# Multipart Upload to Upload Large Objects

## 배경

> java.lang.OutOfMemoryError: Required array size too large

5GB 이상의 파일을 업로드하려고 하면 위와 같은 에러가 발생한다. 이는 JVM의 Heap Size가 부족하기 때문에 발생하는 에러이다. 최대 20GB 까지의 파일을 업로드할 수 있어야 하기 때문에, 서버의 메모리 자원을 올려주는 방식으로는 적합하지 않을 것이라 판단하여 대안을 찾아보았다.

## Multipart Upload

> A multipart upload allows an application to upload a large object as a set of smaller parts uploaded in parallel. Upon completion, combines the smaller pieces into the original large object.

Multipart Upload는 큰 파일을 여러 개의 작은 파일로 나누어 병렬로 업로드하는 방식이다. 이 방식을 사용하면, 큰 파일을 업로드할 때 발생하는 여러 문제를 해결할 수 있다.

- 단일 HTTP connection으로 파일 업로드를 처리할 때 발생하는 TCP 처리량 제한을 우회하고 병렬처리로 인한 가용 network bandwidth를 최대한으로 사용.
- 파일 업로드 중에 문제 발생 시, 문제가 발생한 부분만 재시도함으로써 효율 증대.
- 작은 파일로 쪼개어 한번에 처리되는 서버의 메모리 자원을 최소화하여, 서버의 메모리 부족 문제 해결.

## Multipart Upload 적용 대표 사례: Amazon S3

> Amazon S3 can be ideal to store large objects due to its 5TB object size maximum along with its support for reducing upload times via multipart uploads and transfer acceleration.

S3의 multipart upload와 transfer acceleration 사용 전/후 업로드 소요 시간 비교 (485 MB 파일 업로드 시)

| Multipart Upload | Transfer Acceleration | Upload Time | Improvement |
| :--------------: | :-------------------: | :---------: | :---------: |
| No | No | 72s | - |
| Yes | No | 43s | 40% |
| No | Yes | 45s | 38% |
| Yes | Yes | 28s | 61% |

[Uploading large objects to Amazon S3 using multipart upload and transfer acceleration](https://aws.amazon.com/blogs/compute/uploading-large-objects-to-amazon-s3-using-multipart-upload-and-transfer-acceleration/) 에서 확인할 수 있듯이, Multipart Upload를 사용하면 업로드 시간을 최대 61%까지 단축할 수 있다.

- Single Part upload로 파일 업로드 시 `72초`가 소요됨.
- 위 파일은 Multipart upload와 transfer acceleration를 함께 사용했을 때 28초 (61% 개선) 만에 업로드가 완료됨.

## Multipart Upload 적용 방법

Multipart Upload 방식을 구현하는 순서는 아래와 같다.

1. **initialize upload request:**
- client는 업로드할 파일 이름, 파일 사이즈 정보와 함께 업로드 초기화 요청을 보낸다.
- 서버에서는 사용자의 요청 파일 이름으로 중복 파일이 있는지 검사하고, 없으면 Unique Slide ID를 생성하여 사용자, 요청 파일 정보와 함께 DB에 저장한다.
- 해당 Slide ID와 파일 이름으로 지정된 저장 공간에 Parts를 저장할 폴더를 생성한다.
- 파일의 사이즈를 기반으로 parts로 받을 파일의 최대 사이즈와 client가 전달해줘야 하는 총 part의 수를 계산한다.
- Response로 위 과정에서 생성한 Slide ID, maxPartSize, numberOfParts 정보를 전달한다.
2. **upload part request (in parallel):**
- 초기화 요청으로부터 전달받은 값을 기반으로 원본 파일에서 maxPartSize 만큼의 byte를 Slide ID, part number 정보와 함께 서버에 전달한다.
- 위 요청을 numberOfParts의 수 만큼 수행한다.
- 위 요청을 병렬로 처리하여 성능 향상을 이끌어 낸다.
- 서버에서는 초기화 단계에서 생성한 저장 공간 위치에 파트의 넘버 정보를 포함한 파일 이름으로 파트를 저장한다.
- 에러 상황 발생 시 client에 에러 정보를 전달하여 client가 해당 파트 요청을 다시 수행할 수 있도록 가이드한다.
3. **Complete upload request:**
- 모든 파트 전송 요청이 성공했다면, client는 완료 검증을 위한 Slide ID, numberOfParts, 파일 이름 정보와 함께 업로드 완료 요청을 보낸다.
- 서버는 위 정보를 기반으로 해당 슬라이드의 파일 이름과 생성된 파일 수를 확인한다.
- 검증이 완료되면 파트를 순서대로 병합하여 하나의 파일. 즉, 원본 파일을 만든다.
- 병합이 완료되면 파트를 삭제하고, 업로드 완료 상태를 client에 전달한다.

### 테스트 방법

구현한 Multipart Upload 방식이 정상 동작하는지 테스트하고, 전체 파일을 한번에 업로드하는 방식과 성능 차이를 확인하기 위해 아래와 같은 API를 생성했다.

- `createMultipartUpload` : 1단계. 초기화 요청을 수행하는 API
- `uploadPart`: 2단계. 파트 업로드 요청을 수행하는 API
- `completeMultipartUpload` : 3단계. 최종 파트를 최종 결과물로 병합하는 API
- `uploadSingleFile` : 성능 측정으로 위한 전체 파일을 한번에 업로딩하는 API

### 테스트 결과

- Multipart upload 방식으로 파일을 업로드 할 경우, 20GB 크기의 파일도 성공적으로 업로드할 수 있었다.
- Multipart upload 요청을 병렬로 요청할 경우, 파일 사이즈가 커질수록 뚜렷한 성능 차이를 확인할 수 있었다.
- 전체 파일을 한번에 업로드하는 방식은 5GB 업로드 시 Out Of Memory Error를 발생 (아래 사진 참고)시키며, 해당 에러는 JVM의 maxHeepSize를 늘려서 해결되지 않는다.
- 작은 파일인 경우, 전체 파일을 한번에 업로드하는 방식이 더 효율적이었지만, 파일 사이즈가 커질수록 multipart upload 방식으로 업로드하는 방식이 더 큰 효율을 내는 것을 확인할 수 있었다.

아래는 파일 사이즈에 따른 업로드 방식의 성능 테스트 결과이다.

**File Size: 488MB**

| Multipart upload | Parallel (Coroutine) | Upload time |
| --- | --- | --- |
| No | No | 1.5s |
| Yes | No | 4.7s |
| Yes | Yes | 4.7s |
- Single Upload 방식: 1.5초

![스크린샷 2024-03-17 154323](https://github.com/10000-Bagger/free-topic-study/assets/34956359/771f799d-3732-4ac6-aa90-945ff2d4f05d)

- multipart upload 방식 (sync): 4.7초

![스크린샷 2024-03-17 154412](https://github.com/10000-Bagger/free-topic-study/assets/34956359/59aa9d17-05b5-402a-b8dd-bac0c22542fc)

- multipart upload 방식 (Parallel): 4.7초

![스크린샷 2024-03-17 154541](https://github.com/10000-Bagger/free-topic-study/assets/34956359/90f098e5-ecec-4da5-8da2-4e7425e9f68d)


**File Size: 5GB**

| Multipart upload | Parallel (Coroutine) | Upload time |
| --- | --- | --- |
| No | No | - |
| Yes | No | 63s |
| Yes | Yes | 59s |
- Single Upload 방식: 확인 불가

![스크린샷 2024-03-17 150019](https://github.com/10000-Bagger/free-topic-study/assets/34956359/8ee0ab92-4734-41fa-a008-10a8254e9709)

- multipart upload 방식 (sync): 63초

![스크린샷 2024-03-17 154041](https://github.com/10000-Bagger/free-topic-study/assets/34956359/6c0a228e-47d5-4802-981a-a5fa6c76c814)


- multipart upload 방식 (Parallel): 59초

![스크린샷 2024-03-17 153814](https://github.com/10000-Bagger/free-topic-study/assets/34956359/b7c2bc4e-026b-491d-9232-2281bbee784f)


**File Size: 20GB**

| Multipart upload | Parallel (Coroutine) | Upload time |
| --- | --- | --- |
| No | No | - |
| Yes | No | 272s |
| Yes | Yes | 248s |

- multipart upload 방식 (sync): 4분 32초

![스크린샷 2024-03-17 152637](https://github.com/10000-Bagger/free-topic-study/assets/34956359/7eaf5ac6-07bd-489c-9249-a8055547ec8d)

- multipart upload 방식 (Parallel): 4분 8초

![스크린샷 2024-03-17 153208](https://github.com/10000-Bagger/free-topic-study/assets/34956359/ae00c89d-c7b3-4cb7-a9a9-a96402ffd403)
Loading
Loading