SEG ์์ญ๋ง Zarr๋ก,
๋๋จธ์ง๋ ์๋ณธ ๊ทธ๋๋ก
์ธ๊ทธ๋ฉํ ์ด์ ์ด ์๋ ์ฒญํฌ๋ง ์ ํ์ ์ผ๋ก Zarr๋ก ๋ณํํ๋ Sparse Zarr ์ ๋ต โ ์์ด๋์ด์ ํ๋น์ฑ, ๊ตฌํ ๋ฐฉ์, ์์ ๋ฌธ์ ์ ์ ๊ธฐ์ ์ ์ผ๋ก ๊ฒํ ํฉ๋๋ค.
์ด ์์ด๋์ด, ๊ธฐ์ ์ ์ผ๋ก ํ๋นํ๊ฐ?
๊ฒฐ๋ก ๋ถํฐ ๋งํ๋ฉด โ ํ๋นํ๋ฉฐ, Zarr๊ฐ ๋ค์ดํฐ๋ธ๋ก ์ง์ํ๋ ๊ฐ๋ ์ ๋๋ค.
์ธ๊ทธ๋ฉํ
์ด์
(SEG) NIfTI๋ ๋ณธ์ง์ ์ผ๋ก ํฌ์(sparse) ๋ฐ์ดํฐ์
๋๋ค. ๋์ข
์, ๋ณ๋ณ, ํ๊ด ๋ฑ ๋ ์ด๋ธ์ด ์กด์ฌํ๋ ๋ณต์
์ ์ ์ฒด ๋ณผ๋ฅจ์ ๊ทนํ ์ผ๋ถ์ด๊ณ , ๋๋จธ์ง ๋๋ถ๋ถ์ 0(๋ฐฐ๊ฒฝ)์
๋๋ค. Zarr๋ ์ด ํน์ฑ์ write_empty_chunks=False + fill_value=0์ผ๋ก ๋ค์ดํฐ๋ธํ๊ฒ ์ง์ํฉ๋๋ค โ ๊ฐ์ด 0์ธ ์ฒญํฌ ํ์ผ์ ๋์คํฌ์ ์ ์ฅํ์ง ์๊ณ , ์ฝ์ ๋ ์๋ฌต์ ์ผ๋ก 0์ ๋ฐํํฉ๋๋ค.
write_empty_chunks=False ํ๋๋ก ์๋ ๋ฌ์ฑ๋ฉ๋๋ค.
SEG ๋ฐ์ดํฐ๋ ์ ํฌ์ํ๊ฐ
๋ MRI์์ ์ข ์ ์ธ๊ทธ๋ฉํ ์ด์ ์ ์๋ก ๋ค๋ฉด, ์ ์ฒด ๋ณผ๋ฅจ 512ร512ร200 = 52M ๋ณต์ ์ค ์ค์ ๋ ์ด๋ธ์ด ์๋ ๋ณต์ ์ ์์ฒ~์๋ง ๊ฐ์ ๋ถ๊ณผํฉ๋๋ค. 64ร64ร64 ์ฒญํฌ ๊ธฐ์ค์ผ๋ก ๋ณด๋ฉด, SEG ๋ฐ์ดํฐ๋ฅผ ํฌํจํ๋ ์ฒญํฌ๋ ์ ์ฒด์ ๊ทนํ ์ผ๋ถ์ ๋๋ค.
์ธ๊ทธ๋ฉํ ์ด์ ๋ณผ๋ฅจ์์ ์ค์ ๋ก ๋ ์ด๋ธ์ด ์๋ ๋ณต์ ๋น์จ์ ๋งค์ฐ ๋ฎ์ต๋๋ค.
* ๋ณ๋ณ SEG๋ ํฌ์๋๊ฐ ๋์ Sparse Zarr ํจ๊ณผ๊ฐ ๊ทน๋ํ๋จ. ํ์ ๋ ์ด์ ์ฒ๋ผ ์ ์ฒด๋ฅผ ์ฑ์ฐ๋ SEG๋ ์ด์ ๊ฐ์
ํ์ด๋ธ๋ฆฌ๋ ์๋น ์ํคํ ์ฒ โ ์ ์ฒด ๊ตฌ์กฐ
Base ๋ณผ๋ฅจ์ ๊ธฐ์กด .nii.gz ๊ทธ๋๋ก, SEG๋ง Sparse Zarr๋ก ์๋นํ๋ ํ์ด๋ธ๋ฆฌ๋ ๊ตฌ์กฐ์ ๋๋ค. ์น ๋ทฐ์ด๋ ๋ ์์ค๋ฅผ ์กฐํฉํด ๋ ๋๋งํฉ๋๋ค.
write_empty_chunks=False โ ํต์ฌ ๋ฉ์ปค๋์ฆ
Zarr๊ฐ SEG Sparse ์ ์ฅ์ ์ด๋ป๊ฒ ์ฒ๋ฆฌํ๋์ง ๋จ๊ณ๋ณ๋ก ์ดํด๋ด ๋๋ค.
write_empty_chunks=False๋ก ์ค์ ํ๋ฉด ๊ฐ ์ฒญํฌ๋ฅผ ์ฐ๊ธฐ ์ ์ ์ ์ฒด๊ฐ fill_value(0)์ธ์ง ํ์ธํฉ๋๋ค.c/2/3/1. ํด๋น ์ฒญํฌ๋ ๋ด๋ถ์ ๋ฐฐ๊ฒฝ(0) ๋ณต์
๋ ํจ๊ป ์์ถ ์ ์ฅ๋ฉ๋๋ค. blosc/zstd๋ก ์์ถํ๋ฉด 0์ด ๋ง์ ์ฒญํฌ๋ ์์ถ๋ฅ ์ด ๋งค์ฐ ๋์ต๋๋ค.c/0/0/0 ๊ฐ์ ๋น ์ฒญํฌ๋ฅผ HTTP GET์ผ๋ก ์์ฒญํ๋ฉด ์๋ฒ๊ฐ 404๋ฅผ ๋ฐํํฉ๋๋ค. Zarr ํด๋ผ์ด์ธํธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ ์ด๋ฅผ "ํด๋น ์ฒญํฌ๊ฐ fill_value(0)๋ก๋ง ์ฑ์์ง ๊ฒ"์ผ๋ก ํด์ํ๊ณ ์๋์ผ๋ก 0 ๋ฐฐ์ด์ ๋ฐํํฉ๋๋ค. ์๋ฌ๊ฐ ์๋๋๋ค.# Sparse Zarr SEG ์์ฑ โ write_empty_chunks=False ํต์ฌ import zarr import nibabel as nib import numpy as np # SEG NIfTI ๋ก๋ seg_img = nib.load('seg.nii.gz') seg_data = seg_img.get_fdata().astype(np.uint8) # โโ Sparse Zarr ์์ฑ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ store = zarr.open('seg.nii.zarr', mode='w') seg_arr = store.require_dataset( 'seg', shape=seg_data.shape, # (512, 512, 300) chunks=(64, 64, 64), # ์ฒญํฌ ํฌ๊ธฐ dtype=np.uint8, fill_value=0, # ๋น ์ฒญํฌ = 0์ผ๋ก ํด์ compressor=zarr.Blosc( cname='zstd', clevel=5, shuffle=zarr.Blosc.BITSHUFFLE # 0 ๋ง์ SEG์ ํจ๊ณผ์ ) ) # โ ํต์ฌ: ๋น ์ฒญํฌ๋ ํ์ผ ์ ์ฅ ์ ํจ seg_arr.write_empty_chunks = False # ๋๋ ์์ฑ ์ config์์ ์ค์ seg_arr[:] = seg_data # SEG ์ฒญํฌ๋ง ํ์ผ ์์ฑ๋จ # ๊ฒฐ๊ณผ ํ์ธ import os chunk_files = list(store.store.keys()) print(f"์ด ๊ฐ๋ฅํ ์ฒญํฌ ์: {np.prod([s//c for s,c in zip(seg_data.shape,[64,64,64])])}") print(f"์ค์ ์ ์ฅ๋ ์ฒญํฌ: {len([k for k in chunk_files if not k.endswith('.json')])}") # ์ด ๊ฐ๋ฅํ ์ฒญํฌ ์: 256 โ ์ค์ ์ ์ฅ๋ ์ฒญํฌ: 7 (SEG ์๋ ๊ฒ๋ง)
SEG BBox ์ฌ์ ๋ถ์์ผ๋ก ๋ณํ ๊ฐ์
"SEG๊ฐ ์๋ ๊ณณ์ ์๊ณ ์๋ค๋ฉด"์ด๋ผ๋ ์ ์ ๊ฐ ์ถ๊ฐ๋ ๊ฒฝ์ฐ, ๋ณํ ์ ์ ๋ฐ์ด๋ฉ ๋ฐ์ค๋ฅผ ๋จผ์ ๊ณ์ฐํด ๋ ํจ์จ์ ์ผ๋ก ์ฒ๋ฆฌํ ์ ์์ต๋๋ค. ์ ์ฒด ๋ณผ๋ฅจ์ ์ํํ์ง ์๊ณ SEG ์กด์ฌ ๊ตฌ๊ฐ๋ง ์ฒญํฌ ๋ณํํฉ๋๋ค.
๋ฐฉ๋ฒ A: BBox ์ฌ์ ๊ณ์ฐ
SEG NIfTI ์ ์ฒด๋ฅผ ์ค์บํด ๋น์ ๋ณต์ ์ min/max ์ขํ๋ฅผ ๊ตฌํ๊ณ , ํด๋น ์ฒญํฌ ๋ฒ์๋ง Zarr๋ก ๋ณํํฉ๋๋ค. ๋๋จธ์ง ์ฒญํฌ๋ ์์ ์ ๊ทผํ์ง ์์ต๋๋ค.
๋ฐฉ๋ฒ B: write_empty_chunks=False
BBox ๋ถ์ ์์ด ๊ทธ๋ฅ ์ ์ฒด๋ฅผ ์๋๋ค. Zarr๊ฐ ์๋์ผ๋ก ๋น ์ฒญํฌ๋ฅผ ๊ฑด๋๋๋๋ค. ๊ตฌํ์ด ๋จ์ํ๊ณ , ๋์ฉ๋์์๋ ์ฐ๊ธฐ ์ค๋ฒํค๋๊ฐ ๋ฎ์ต๋๋ค.
๋ฐฉ๋ฒ C: Lazy/On-demand ๋ณํ
AI ์๋ฒ๊ฐ SEG๋ฅผ ์์ฑํ๋ ์ฆ์ ๋ ์ด๋ธ๋ณ BBox๋ฅผ ๋ฐํ โ ์น ๋ทฐ์ด ์์ฒญ ์ ํด๋น ์ฒญํฌ๋ง ์ค์๊ฐ ๋ณํยท์บ์ฑํฉ๋๋ค.
# ๋ฐฉ๋ฒ A: BBox ์ฌ์ ๋ถ์ โ ์ฒญํฌ ๋ฒ์๋ง ๋ณํ import numpy as np def get_seg_chunk_range(seg_data: np.ndarray, chunk_size: int = 64): """SEG ๋ฐ์ดํฐ๊ฐ ์๋ ์ฒญํฌ ์ขํ ๋ฒ์๋ฅผ ๋ฐํ""" nonzero = np.nonzero(seg_data) # ๋น์ ๋ณต์ ์ขํ if len(nonzero[0]) == 0: return None # ๋ ์ด๋ธ ์์ min_z, max_z = nonzero[2].min(), nonzero[2].max() min_y, max_y = nonzero[1].min(), nonzero[1].max() min_x, max_x = nonzero[0].min(), nonzero[0].max() # ์ฒญํฌ ์ขํ๋ก ๋ณํ (์ฒญํฌ ๊ฒฝ๊ณ๋ก ํ์ฅ) return { 'z': (int(min_z // chunk_size), int(max_z // chunk_size) + 1), 'y': (int(min_y // chunk_size), int(max_y // chunk_size) + 1), 'x': (int(min_x // chunk_size), int(max_x // chunk_size) + 1), } chunk_range = get_seg_chunk_range(seg_data) # {'z': (2, 5), 'y': (3, 5), 'x': (3, 6)} โ 2ร2ร3 = 12๊ฐ ์ฒญํฌ๋ง ๋ณํ # BBox ๋ฒ์ ์ฌ๋ผ์ด์ค๋ง Zarr์ ์ฐ๊ธฐ if chunk_range: z0, z1 = chunk_range['z'] y0, y1 = chunk_range['y'] x0, x1 = chunk_range['x'] for zi in range(z0, z1): for yi in range(y0, y1): for xi in range(x0, x1): sl = ( slice(xi*64, (xi+1)*64), slice(yi*64, (yi+1)*64), slice(zi*64, (zi+1)*64) ) chunk = seg_data[sl] if chunk.any(): # SEG ์๋ ์ฒญํฌ๋ง seg_arr[sl] = chunk # ํ์ผ ์์ฑ
์คํ ๋ฆฌ์ง ์ ๊ฐ & ์ฑ๋ฅ ๋น๊ต
| ํญ๋ชฉ | SEG .nii.gz (๊ธฐ์กด) | SEG ์ ์ฒด Zarr | SEG Sparse Zarr (SEG ์์ญ๋ง) |
|---|---|---|---|
| ํ์ผ ํฌ๊ธฐ / ์ฉ๋ | ~30 MB (gzip ์์ถ) | ~600 MB (Int16, 64ยณ ์ฒญํฌ) | ~16 MB (SEG ์ฒญํฌ๋ง) |
| ์ฒซ ๋ ๋ ๋๊ธฐ ์๊ฐ | 30 MB ์ ์ฒด ์์ ํ (~5์ด) | ํด๋น ์ฒญํฌ ์ฆ์ (~์๋ฐฑms) | ํด๋น ์ฒญํฌ ์ฆ์ |
| ํ์ฌ ์ฌ๋ผ์ด์ค fetch | ์ ์ฒด gzip ํด์ ํ์ | ํด๋น ์ฒญํฌ 4~8๊ฐ fetch | SEG ์ฒญํฌ๋ง fetch ๋น ์ฌ๋ผ์ด์ค=0 ์ฆ์ ๋ฐํ |
| ๋ณํ ์๊ฐ | ๋ถํ์ | ~30์ด (์ ์ฒด ๋ณผ๋ฅจ) | ~3์ด (BBox ๋ถ์ ํฌํจ) |
| ํ์ผ ์ | 1๊ฐ | 256๊ฐ (8ร8ร4) | 7~15๊ฐ (SEG ์ฒญํฌ๋ง) |
| Base ๋ณผ๋ฅจ ๋ณ๊ฒฝ | ์์ | ์์ | ์์ (์์ ๋ถ๋ฆฌ) |
| ๊ตฌํ ๋ณต์ก๋ | ๋ฎ์ | ์ค๊ฐ | ์ค๊ฐ+ (์ปค์คํ ๋ก๋ ํ์) |
* 512ร512ร300 ๋ณผ๋ฅจ, ์ข ์ SEG (~3% ๋น์ ๋ณต์ ), 64ร64ร64 ์ฒญํฌ ๊ธฐ์ค ์ถ์ ์น
์์ ๋ฌธ์ ์ & ๋์ ์ ๋ต
๋์: Zarr ํด๋ผ์ด์ธํธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ(zarrita ๋ฑ)๊ฐ ์๋์ผ๋ก ์ธ์ ์ฒญํฌ๋ฅผ ์ด์ด๋ถ์ ๋๋ค. ์ฌ๋ผ์ด์ค ๋จ์๋ก ์์ฒญ ์ ํ์ํ ์ฒญํฌ๊ฐ ์๋ ๊ฒฐ์ ๋ฉ๋๋ค.
๋์: ๋ฐ๋์ ๊ฒ์ฆ๋ Zarr ํด๋ผ์ด์ธํธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ๊ฑฐ๋, 404 โ 0-filled ๋ฐฐ์ด ๋ฐํ ๋ก์ง์ ๋ช ์์ ์ผ๋ก ํ ์คํธํฉ๋๋ค.
๋์: Zarr ๋ณํ ์ nibabel๋ก affine ํ๋ ฌ์ ๋น๊ตํด ์ผ์น๋ฅผ ๊ฒ์ฆํฉ๋๋ค. zarr.json์ NIfTI affine ์ ๋ณด๋ฅผ ์ ์ฅํ๊ณ , ํด๋ผ์ด์ธํธ์์ ๋ก๋ ์ Base์ ๋น๊ตยท๊ฒ์ฆํฉ๋๋ค.
๋์: ๋ณํ ์ ๋น์ ๋ณต์ ๋น์จ์ ๊ณ์ฐํด 30% ๋ฏธ๋ง์ผ ๋๋ง Sparse Zarr๋ฅผ ์ ํํ๊ณ , ๊ทธ ์ด์์ด๋ฉด ์ผ๋ฐ Zarr ๋๋ .nii.gz๋ฅผ ์ ์งํฉ๋๋ค.
๋์: Base์ SEG๋ฅผ ๋์ผํ ๋๋ฉ์ธ์ผ๋ก ์๋นํ๊ฑฐ๋, ํ๋ก์ ๋ ์ด์ด๋ฅผ ๋์ด ๋จ์ผ origin์ผ๋ก ํต์ผํฉ๋๋ค. ๋ก๋ฉ ์ค์๋ Base๋ง ๋จผ์ ํ์ํ๊ณ SEG๊ฐ ๋ก๋๋๋ฉด ์ค๋ฒ๋ ์ด๋ฅผ ์ถ๊ฐํ๋ ์ ์ง์ ํ์๋ฅผ ๊ตฌํํฉ๋๋ค.
์น ๋ทฐ์ด ํตํฉ โ ์ปค์คํ SEG Zarr ๋ก๋
// seg-zarr-loader.ts โ SEG Sparse Zarr ์ปค์คํ ImageLoader import { imageLoader, metaData } from '@cornerstonejs/core'; import * as zarr from 'zarrita'; interface SegZarrMeta { store: zarr.Array; shape: number[]; chunkSize: number; affine: number[][]; // NIfTI sform์ผ๋ก๋ถํฐ } const segStores = new Map<string, SegZarrMeta>(); // 1. Zarr ๋ฉํ๋ฐ์ดํฐ ์ด๊ธฐํ export async function initSegZarr(zarrUrl: string) { const fetchStore = new zarr.FetchStore(zarrUrl); const arr = await zarr.open(fetchStore.resolve('seg'), { kind: 'array' }); const attrs = await zarr.getAttributes(fetchStore.resolve('seg')); segStores.set(zarrUrl, { store: arr, shape: arr.shape, chunkSize: arr.chunks[0], // 64 affine: attrs.nifti_header?.affine, }); return arr.shape[2]; // ์ฌ๋ผ์ด์ค ์ ๋ฐํ } // 2. ์ฌ๋ผ์ด์ค ๋จ์ SEG ImageLoader function segZarrLoader(imageId: string) { // imageId: 'seg-zarr://http://server/seg.nii.zarr#150' const [url, sliceStr] = imageId.replace('seg-zarr://', '').split('#'); const sliceIdx = parseInt(sliceStr); const meta = segStores.get(url); const promise = (async () => { // Zarr์์ ํด๋น ์ฌ๋ผ์ด์ค fetch // ๋น ์ฒญํฌ(SEG ์์) โ zarrita๊ฐ ์๋์ผ๋ก 0 ๋ฐฐ์ด ๋ฐํ (404 ์ฒ๋ฆฌ ๋ด์ฅ) const sliceData = await zarr.get(meta.store, [null, null, sliceIdx]); return { imageId, rows: sliceData.shape[0], columns: sliceData.shape[1], columnPixelSpacing: 1, rowPixelSpacing: 1, color: false, minPixelValue: 0, maxPixelValue: 255, // Uint8 SEG ๋ฐ์ดํฐ getPixelData: () => new Uint8Array(sliceData.data.buffer), }; })(); return { promise }; } // 3. Cornerstone3D์ ๋ฑ๋ก imageLoader.registerImageLoader('seg-zarr', segZarrLoader); // โโ ์ฌ์ฉ ์์ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ const sliceCount = await initSegZarr('http://server/seg.nii.zarr'); // SEG imageId ๋ฐฐ์ด ์์ฑ const segImageIds = Array.from({ length: sliceCount }, (_, i) => `seg-zarr://http://server/seg.nii.zarr#${i}` ); // SEG Viewport์ ๋ก๋ (Stack or Volume) viewport.setStack(segImageIds);
๊ฒฐ๋ก โ ์ด ์ ๋ต์ ์ธ์ ์ฐ๊ณ , ์ธ์ ํผํด์ผ ํ๋
์ด ์ ๋ต์ด ํจ๊ณผ์ ์ธ ๊ฒฝ์ฐ
- ๋ณ๋ณ SEG โ ์ข ์, ๊ฒฐ์ , ๊ฒฝ์ ๋ฑ (์ ์ฒด ๋๋น <30%)
- ํ๊ด SEG โ ์ ํ ๊ตฌ์กฐ, ํฌ์
- AI ๋ชจ๋ธ์ด SEG BBox๋ฅผ ํจ๊ป ์ถ๋ ฅํ๋ ํ์ดํ๋ผ์ธ
- Base ๋ณผ๋ฅจ์ ๊ทธ๋๋ก ๋๊ณ SEG๋ง ๋น ๋ฅด๊ฒ ๋ก๋ํ๊ณ ์ถ์ ๋
- S3/CDN์์ ์ ์ ์๋น ๊ฐ๋ฅํ ํ๊ฒฝ
ํผํด์ผ ํ๊ฑฐ๋ ํ๊ณ์ธ ๊ฒฝ์ฐ
- ํ์ ๋ ์ด์ SEG โ ์ ์ฒด ์ฑ์ (Sparse ์ด์ ์์)
- ์ปค์คํ Zarr ๋ก๋ ์์ด CS3D ๊ธฐ๋ณธ ์ฌ์ฉ ์
- CORS ์ค์ ๋ถ๊ฐ๋ฅํ ํ๊ฒฝ
- Base์ SEG ํด์๋ยท์ขํ๊ณ๊ฐ ๋ค๋ฅธ ๊ฒฝ์ฐ
- ์ค์๊ฐ SEG ์ ๋ฐ์ดํธ๊ฐ ๋น๋ฒํ ๊ฒฝ์ฐ (์บ์ ๋ฌดํจํ ๋ณต์ก)
| ์ฒดํฌํฌ์ธํธ | ์ํ | ๋น๊ณ |
|---|---|---|
| ๊ธฐ์ ์ ํ๋น์ฑ | ๊ฒ์ฆ๋จ | Zarr write_empty_chunks=False ๋ค์ดํฐ๋ธ ์ง์ |
| Base ๋ณผ๋ฅจ ์ํฅ | ์์ | ์์ ํ ๋ถ๋ฆฌ๋ ํ์ดํ๋ผ์ธ |
| ์คํ ๋ฆฌ์ง ์ ๊ฐ | ~95% | ๋ณ๋ณ SEG ๊ธฐ์ค (ํฌ์๋ ๋์์๋ก ํจ๊ณผ ํผ) |
| CS3D ๊ณต์ ์ง์ | ์์ | ์ปค์คํ ImageLoader ๊ตฌํ ํ์ |
| ๊ตฌํ ๋์ด๋ | ์ค๊ฐ | zarrita + ์ปค์คํ ๋ก๋ ์ฝ 200์ค |
| ํ์ ๋ ์ด์ SEG ์ ํฉ์ฑ | ๋ฎ์ | ๋น์ ๋น์จ ๋์ผ๋ฉด ์ด์ ๊ฐ์ |
| ํ๋ก๋์ ์์ ์ฑ | ๊ฒ์ฆ ํ์ | 404โ0 ์ฒ๋ฆฌ, ์ฒญํฌ ๊ฒฝ๊ณ ๊ฒ์ฆ ํ์ |