HTML5 Canvas로 기가 픽셀 데이터 시각화하기

이미 대부분의 의료기관에서 디지털화 되어 있는 영상의학(radiology) 분야와는 달리, 현재까지도 병리학(pathology) 분야에서는 조직 슬라이드를 광학 현미경을 통해 관찰하여 진단, 연구하는 전통적인 방법을 사용해 왔습니다. 그러나 IT 기술의 발전과 함께, 슬라이드를 스캔하여 컴퓨터로 저장, 분석할 수 있는 digital pathology system이 최근 활발히 보급되기 시작했습니다.

슬라이드 스캐너는 픽셀당 0.25μm 수준의 높은 해상도를 가지기 때문에, 손가락만 한 슬라이드 하나가 27.5 기가 픽셀의 고해상도 이미지로 변환됩니다. 이러한 고해상도 이미지를 다루는 작업은 여러 어려움이 따르지만 그만큼 흥미로운 도전입니다.

루닛의 인공지능 엔진은 Tissue, Structure, Cell의 세 가지 패널에서 분석 결과를 예측합니다. 이 결과를 시각화하여 자유롭게 확대 / 축소 / 이동하면서 살펴보기 위해 저희는 웹 애플리케이션 기반의 툴을 제작했습니다. 전체 UI는 React를, 타일링된 슬라이드 이미지 표시에는 OpenSeadragon을 사용했습니다.

Tissue

하나의 슬라이드 안에는 상피, 기질, 지방, 근육 등 여러 조직이 섞여 있게 마련입니다. 인공지능은 각 픽셀이 어느 영역에 속하는지를 예측합니다.

요구 사항

  • 슬라이드 이미지는 매우 크기 때문에 12.5배율에서 전체 형태를 볼 수도, 400배율에서 각 세포의 위치와 형태를 볼 수도 있습니다. Tissue 영역의 시각화는 어떤 배율에서도 표시할 수 있어야 합니다.
  • 영역을 표시하기 때문에 시각화의 정밀도가 픽셀 수준으로 정확할 필요는 없습니다.
  • 사용자에 의한 확대 / 축소 / 이동이 매우 빈번하게 일어나며, 프레임 저하를 최소화해야 합니다.
  • 여러 종류의 Tissue의 시각화를 켜고 끄면서 동시에 표시할 수 있어야 합니다.

데이터 포맷

원본인 슬라이드 이미지가 매우 크기 때문에, 예측 결과도 저장하거나 전송하기에는 지나치게 큽니다. 효율적인 데이터 포맷을 고르기 위해 다음과 같은 고려가 필요했습니다.

  • 높은 공간 효율성: 최초에 시도했던 JSON 포맷은 특정 영역이 어떤 조직에 속하는지를 표시하기 위해 X 좌표, Y 좌표, 영역 크기, Tissue 종류를 텍스트로 저장하기 때문에 실제로 가지는 정보 값에 비하면 크기가 매우 컸습니다.
  • 브라우저 환경에서 처리의 용이성: 직접 스키마를 고안하고 바이너리 형태로 저장한다면 저장과 전송의 효율성은 극도로 올라가겠지만, 브라우저 환경에서 자바스크립트로 처리하기는 매우 어려워집니다. 네이티브 디코더를 사용할 수 있는 JSON보다도 성능이 떨어질 가능성이 높습니다.
  • 디버깅 용이성: 데이터가 형식에 맞게 제대로 생성되었는지, 그 대략적인 형태는 어떠한지 엔진 개발자와 프론트엔드 개발자가 모두 쉽게 알 수 있어야 합니다. 일반적인 경우 JSON은 개발자가 읽기 쉽지만, 수십 MB에 달하는 크기가 되면 이야기가 달라집니다.

이들 모두를 고려하여 선택한 데이터 포맷은 평범한 이미지 포맷입니다. 예측 결과를 bitmap mask로 표현하여 조직 종류를 색상 값으로 지정하면 전체 Tissue 데이터가 하나의 비트맵 이미지가 됩니다. 이미지 코덱을 이용하여 높은 효율로 압축할 수 있고, 디코딩도 브라우저가 처리해줍니다. 이미지 뷰어로 열어보면 대략적인 형태도 바로 알 수 있습니다.

이렇게 생성된 이미지를 네트워크로 전송하여 Canvas에 그리면 각 픽셀에 해당하는 데이터를 Uint8ClampedArray 형태로 접근할 수 있습니다. 픽셀의 색상 값에 따라 조직별로 mask를 분리하면 시각화 준비가 끝납니다.

Pre-rendering

사용자가 확대 / 축소 / 이동을 통해 시점(viewport)을 이동하게 되면 Canvas 영역을 새로 그려야 합니다. 초당 60프레임을 유지하기 위해서는 16.7ms 안에 모든 처리를 마쳐야 합니다(실제로는 브라우저에도 여유를 줘야 하기 때문에 이보다 더 적은 시간이 주어집니다). 이 때문에 전달받은 데이터를 매번 다시 그리는 방법은 사용할 수 없습니다.

대신 메모리를 사용해서 성능을 확보하도록 합시다. Tissue 영역을 종류별로 OffscreenCanvas에서 시각화하여 메모리에 저장합니다. OffscreenCanvas는 Chrome 69 버전 이후에서 플래그 없이 사용할 수 있으며, 웹 워커에서도 사용 가능하기 때문에 여러 Tissue를 멀티 스레드로 동시에 처리할 수 있습니다. 메인 UI 스레드의 로딩 애니메이션을 멈추지 않기 때문에 사용자가 더 빠르게 느끼는 것도 소소한 장점입니다.

이 중 사용자가 선택한 Tissue들만 합성해서 ImageBitmap 형태로 저장해 두었다가, 시점이 이동할 때마다 바뀐 transform matrix를 적용하여 화면상의 Canvas에 옮겨주면 됩니다. ImageBitmap은 Canvas에 매우 빠르게(without undue latency) 그릴 수 있습니다.

실제로는 이 코드대로 구현할 경우 화면상에 아무것도 표시되지 않습니다. 전체 이미지 영역은 기가 픽셀 사이즈인데 비해 브라우저에서 Canvas에 할당해주는 메모리는 한계가 있기 때문입니다. 저는 downsampling을 통해 더 작은 공간에 그리도록 구현했습니다. 픽셀 수준의 정확도가 필요 없기 때문에 영역 경계가 조금 거칠어도 괜찮습니다.

Structure / Cell

슬라이드 이미지 내에 존재하는 혈관과 신경(Structure) 및 다양한 세포(Cell)들을 감별하기 위한 패널입니다. 인공지능은 Structure의 외곽선과 Cell의 중심점을 예측합니다.

요구 사항

  • 개별 요소가 전체 슬라이드 크기에 비해 매우 작으므로 저배율(100배 이하)에서는 표시하지 않아도 됩니다.
  • 시각화의 정밀도가 픽셀 수준으로 정확하게 슬라이드 이미지와 일치해야 합니다.
  • 사용자에 의한 확대 / 축소 / 이동이 매우 빈번하게 일어나며, 프레임 저하를 최소화해야 합니다.
  • 여러 종류의 Structure와 Cell의 시각화를 켜고 끄면서 동시에 표시할 수 있어야 합니다.

데이터 포맷

영역 정보이기 때문에 픽셀 수만큼의 데이터를 가지는 Tissue에 비해, Polygon 형태의 Structure와 Point 형태의 Cell의 경우 수십만 개 수준으로 데이터가 많지 않습니다. 다만 여전히 JSON으로 직렬화할 경우 지나치게 커서 네트워크를 통한 전송도 오래 걸리고, 브라우저에서 디코딩도 몇 초나 걸려서 사용에 불편함이 있었습니다. 때문에 인코딩 결과물이 훨씬 더 작고 디코딩도 빠른 MessagePack으로 변경하기로 했습니다.

Spatial Indexing

Tissue 시각화에서 했던 것처럼 전체 영역을 미리 그려둘 수 있으면 편하겠지만, 픽셀 수준의 정확도를 가지고 슬라이드 이미지 위에 표시하기 위해서는 downsampling을 사용할 수 없습니다. 종류도 Tissue 보다 몇 배는 많기 때문에 모두 그려두기에는 메모리 낭비가 심합니다.

다행히도 100배 이상의 고배율에서만 표시하면 된다는 조건이 있기 때문에, 한 번에 화면에 표시되어야 하는 요소의 개수는 많지 않습니다. spatial index를 사용해서 현재 시점 안에서 표시해야 할 요소만 골라서 그리도록 하면 수 ms 이내에 처리가 가능합니다.

서버에서 받아온 데이터의 X, Y 좌표를 키로 검색할 수 있게 적당한 자료구조에 넣어줍시다. 저는 R 트리보다 검색이 빠른 K-d 트리를 사용했습니다. 사용자가 시점을 이동할 때마다 표시 영역 내에 있는 요소만 꺼내서 Canvas에 그려주면 됩니다.

마치며

제목은 거창하게 시작했지만, 실제 구현에 대단한 로직이 들어가 있지는 않습니다. 다만 웹 개발에서 일상적이지는 않은 요구 사항을 마주하면서 문제를 풀어나가는 즐거움을 느낄 수 있는 프로젝트였습니다. 비슷한 요구 사항을 해결해야 하는 분들께 힌트가 되었으면 좋겠습니다.

루닛에서는 저희와 함께 큰 문제를 풀어나가실 FRONT-END ENGINEER 분들을 모십니다.