Cellpose 3 + SAM 세포 분할 파이프라인 — 형광 현미경 이미지에서 세포 하나하나를 잡아내는 실전
형광 현미경 이미지에서 세포 경계를 하나하나 손으로 그어본 사람은 이 편이 왜 필요한지 즉시 압니다. 슬라이드 한 장에 세포 500개, 하루 이미지 100장이면 5만 세포를 사람이 라벨링하는 데 며칠이 걸립니다. 그런데 세포 형태·면적·강도의 정량이 다운스트림 분석(약물 반응, 표현형 스크리닝, 조직 병리)의 기본 입력이라 이 병목을 풀지 않으면 실험 사이클 전체가 늦어집니다. 이 편은 Cellpose 3의 이미지 복원 특화 백본과 Meta의 Segment Anything 2를 조합해 이 문제를 30분 규모로 압축하는 실전 파이프라인입니다.
📚 선수 편 권고 (강력 권고)
이 편은 AI×바이오 하드코어 심화 편입니다. 진입 전에 DryBench의 다음 편들을 먼저 듣고·보시길 강력히 권고드립니다.
선수 편 없이 진입할 경우, 이 편에서 다루는 U-Net 계열 인코더-디코더 구조와 PyTorch tensor·GPU 이동 기본기의 원리 재설명 없이 실전 코드부터 진행하므로 따라가기 어렵습니다.
우리 DryBench에서 이거 배웠잖아
DryBench ai-native #2에서 신경망이 입력에 대해 가중치 곱과 비선형 활성화를 반복해 표현을 학습하고, 특히 U-Net 계열 인코더-디코더 구조가 이미지 픽셀 단위 예측(segmentation)에 왜 강한지를 배웠습니다. #12에서 PyTorch tensor 조작·GPU 이동·모델 forward pass의 기본기를 다뤘습니다.
그런데 실전 실험실 이미지에서는 몇 가지 추가 도전이 있습니다. 조명 불균일(vignetting), 초점 흐림, 세포 밀집으로 인한 경계 겹침, 채널 간 색 이동, 신호 대 잡음비(SNR)가 낮은 형광 조건. 이런 조건에서 pretrained CNN이 얼마나 견고한지, 그리고 최신 파운데이션 모델(SAM 2)이 이 견고성을 얼마나 끌어올리는지 실전에서 확인합니다.
하드코어 문제 정의
세포 분할의 실전 요구
한 장의 형광 이미지(2048×2048)에서 다음 4가지를 자동으로 뽑아야 합니다.
- 인스턴스 세그멘테이션: 세포마다 고유 ID + 픽셀 마스크. 겹친 세포도 구분.
- 형태 통계량: 각 세포의 면적(px²), 원형도(circularity), 편심도(eccentricity).
- 강도 통계량: 세포별 형광 채널 평균·최댓값·표준편차.
- 위치 관계: 이웃 세포 간 거리, 클러스터 여부.
목표 지표:
- 세포 검출 recall ≥ 0.92 (사람 라벨링 대비).
- 세포 검출 precision ≥ 0.90.
- 처리 속도: 2048×2048 이미지 한 장 당 GPU 3
5초, CPU 3060초.
기존 접근의 스펙트럼
- 고전 규칙 기반(Otsu threshold + watershed): 밀도 낮고 SNR 높은 이미지에서 recall 0.7~0.8, 겹친 세포 자주 실패.
- StarDist (star-convex polygon): 원형에 가까운 핵 분할에 강력, 복잡한 세포체는 약함.
- Cellpose 1/2 (U-Net + flow field): 다양한 세포 형태 커버, 사전학습 가중치로 zero-shot 강력. 2024 Cellpose 3에서 이미지 복원 백본 추가.
- SAM (Segment Anything Model): 자연 이미지 학습이지만 바이오 이미지에도 zero-shot 놀랍게 작동, 다만 프롬프트 필요.
- SAM 2: 비디오·2D 이미지 통합, 마스크 propagation 강력. Cellpose 팀이 Cellpose-SAM 통합 발표(2025).
Cellpose 3 + SAM 2 조합이 현시점 가장 견고한 zero-shot 접근입니다.
도구 스택과 인프라 요구
| 도구 | 역할 | 라이선스 |
|---|---|---|
Cellpose 3 (cellpose>=3.0) | 이미지 복원 + 세포 세그멘테이션 | BSD-3-Clause |
Segment Anything 2 (segment-anything-2) | 마스크 refinement, 프롬프트 기반 정밀 분할 | Apache 2.0 |
| napari | 인터랙티브 시각화·라벨 확인 | BSD-3-Clause |
| scikit-image | 형태·강도 통계량 계산 | BSD-3-Clause |
| tifffile | TIFF 다채널 현미경 이미지 읽기 | BSD-3-Clause |
| numpy, pandas | 수치·표 데이터 처리 | BSD |
인프라 요구:
- Cellpose 3 추론: 소형 소비자 GPU (RTX 3060 6GB 이상 권장). CPU 폴백 가능하지만 10~20배 느림.
- SAM 2: 소형~중형 소비자 GPU (RTX 4060 8GB 이상 권장). CPU 추론은 매우 느림.
- RAM 16GB 이상 (2048×2048 다채널 이미지 배치 처리).
- 디스크: Cellpose 가중치 약 30MB, SAM 2 base 가중치 약 160MB, large 가중치 약 900MB.
학습자 재현 예상 비용: API 비용 0(완전 로컬). GPU 시간 이미지 100장 처리 시 5~10분 (RTX 4060 추정). 데이터셋은 오픈(Cellpose 공식 예시 이미지 무료).
파이프라인 실전 구현
전체 흐름:
Step 1. TIFF 이미지 로딩과 전처리
현미경 이미지는 대개 다채널 TIFF (예: DAPI + FITC + TRITC). 각 채널의 dynamic range가 극단적으로 다를 수 있어 정규화가 중요합니다.
from pathlib import Pathfrom typing import NamedTuple
import numpy as npimport tifffile
class MicroscopyImage(NamedTuple): """현미경 이미지 컨테이너.""" data: np.ndarray # shape: (channels, height, width) or (height, width) channel_names: list[str] pixel_size_um: float # 픽셀 하나가 물리적으로 몇 마이크로미터인가 filename: str
def load_microscopy_image( path: Path, channel_names: list[str] | None = None, pixel_size_um: float = 0.325,) -> MicroscopyImage: """TIFF 로딩. OME-TIFF metadata에서 pixel size 자동 추출 가능.""" data = tifffile.imread(path) if data.ndim == 2: data = data[np.newaxis, ...] # (H, W) → (1, H, W) if channel_names is None: channel_names = [f"ch{i}" for i in range(data.shape[0])] return MicroscopyImage( data=data, channel_names=channel_names, pixel_size_um=pixel_size_um, filename=path.name, )
def normalize_channel( channel_data: np.ndarray, lower_percentile: float = 1.0, upper_percentile: float = 99.5,) -> np.ndarray: """percentile 기반 정규화. min-max보다 outlier에 견고.""" p_low = np.percentile(channel_data, lower_percentile) p_high = np.percentile(channel_data, upper_percentile) if p_high - p_low < 1e-6: return np.zeros_like(channel_data, dtype=np.float32) normalized = np.clip((channel_data - p_low) / (p_high - p_low), 0, 1) return normalized.astype(np.float32)Step 2. Cellpose 3 이미지 복원 + 세그멘테이션
Cellpose 3는 이미지 복원(denoise/deblur/upsample) 백본이 세그멘테이션 앞단에 통합되어 저품질 이미지에서 큰 폭 개선합니다 [1].
from cellpose import models, io as cp_iofrom cellpose.denoise import DenoiseModel
class CellposeSegmenter: """Cellpose 3 복원 + 세그멘테이션 통합."""
def __init__( self, model_type: str = "cyto3", # cyto3: 세포체 최신, nuclei: 핵 전용 restore_type: str = "denoise", # denoise / deblur / upsample / None device: str = "cuda", ): self.model = models.CellposeModel( gpu=(device == "cuda"), model_type=model_type, ) self.restore_type = restore_type if restore_type: self.denoiser = DenoiseModel( model_type=f"{restore_type}_{model_type}", gpu=(device == "cuda"), ) else: self.denoiser = None
def segment( self, image: np.ndarray, diameter: float | None = None, channels: list[int] = [0, 0], cellprob_threshold: float = 0.0, flow_threshold: float = 0.4, ) -> dict: """ image: (H, W) grayscale or (C, H, W) multi-channel. diameter: 예상 세포 지름 (px). None이면 자동 추정. channels: [세포체 채널, 핵 채널] indices. [0,0] = grayscale. Returns: {masks, flows, styles, diams} """ if self.denoiser is not None: image = self.denoiser.eval(image, channels=channels)[0] masks, flows, styles = self.model.eval( image, diameter=diameter, channels=channels, cellprob_threshold=cellprob_threshold, flow_threshold=flow_threshold, ) return { "masks": masks, # (H, W) int, 0=배경, 1..N=세포 ID "flows": flows, # gradient flow visualization "diameter_used": diameter or self.model.diam_labels, }Step 3. SAM 2 Refinement (선택적)
Cellpose가 애매하게 잡은 경계나 겹친 세포는 SAM 2로 재정밀화합니다. SAM 2는 point/box/mask prompt를 받아 정밀 마스크를 반환합니다 [2].
from segment_anything_2 import SAM2ImagePredictor
class SAM2Refiner: """Cellpose 마스크를 SAM 2로 refine."""
def __init__(self, model_id: str = "facebook/sam2-hiera-base", device: str = "cuda"): self.predictor = SAM2ImagePredictor.from_pretrained(model_id) self.predictor.model.to(device)
def refine_mask( self, image: np.ndarray, cellpose_mask: np.ndarray, cell_id: int, ) -> np.ndarray: """특정 세포 ID의 Cellpose 마스크를 SAM 2로 refine.""" self.predictor.set_image(image) # Cellpose 마스크의 bounding box + centroid를 SAM 프롬프트로 사용 cell_mask = (cellpose_mask == cell_id) if not cell_mask.any(): return cell_mask ys, xs = np.where(cell_mask) bbox = np.array([xs.min(), ys.min(), xs.max(), ys.max()]) centroid = np.array([[xs.mean(), ys.mean()]]) refined_masks, scores, _ = self.predictor.predict( point_coords=centroid, point_labels=np.array([1]), # foreground box=bbox, multimask_output=True, ) # 가장 높은 스코어의 마스크 선택 best_idx = scores.argmax() return refined_masks[best_idx]
def refine_uncertain_cells( self, image: np.ndarray, cellpose_result: dict, uncertainty_threshold: float = 0.5, ) -> np.ndarray: """Cellpose flow의 확신도가 낮은 세포만 SAM으로 refine.""" masks = cellpose_result["masks"].copy() # flows[2]는 cell probability map cell_probs = cellpose_result["flows"][2] if len(cellpose_result["flows"]) > 2 else None if cell_probs is None: return masks for cell_id in np.unique(masks): if cell_id == 0: continue cell_region = (masks == cell_id) mean_prob = cell_probs[cell_region].mean() if mean_prob < uncertainty_threshold: refined = self.refine_mask(image, masks, cell_id) masks[cell_region] = 0 masks[refined] = cell_id return masksStep 4. 형태·강도 통계량 정량
scikit-image의 regionprops로 세포마다 표준 지표를 뽑습니다.
from dataclasses import dataclass, asdictfrom typing import Iterable
from skimage.measure import regionprops, regionprops_tableimport pandas as pd
@dataclassclass CellFeatures: """세포 하나의 형태·강도 특성.""" cell_id: int area_px: int area_um2: float perimeter_px: float circularity: float # 4π·area / perimeter² (원형=1) eccentricity: float # 편심도 (원=0, 선=1) solidity: float # area / convex_hull_area centroid_y: float centroid_x: float mean_intensity_per_channel: dict[str, float] max_intensity_per_channel: dict[str, float]
def extract_features( mask: np.ndarray, intensity_channels: dict[str, np.ndarray], pixel_size_um: float,) -> list[CellFeatures]: """ mask: (H, W) int, 0=배경, 1..N=세포 ID. intensity_channels: {channel_name: (H, W) array}. """ features = [] for prop in regionprops(mask): perimeter = prop.perimeter if prop.perimeter > 0 else 1e-6 circularity = (4 * np.pi * prop.area) / (perimeter ** 2) mean_intensity = { name: float(ch[prop.coords[:, 0], prop.coords[:, 1]].mean()) for name, ch in intensity_channels.items() } max_intensity = { name: float(ch[prop.coords[:, 0], prop.coords[:, 1]].max()) for name, ch in intensity_channels.items() } features.append(CellFeatures( cell_id=int(prop.label), area_px=int(prop.area), area_um2=float(prop.area * (pixel_size_um ** 2)), perimeter_px=float(prop.perimeter), circularity=float(circularity), eccentricity=float(prop.eccentricity), solidity=float(prop.solidity), centroid_y=float(prop.centroid[0]), centroid_x=float(prop.centroid[1]), mean_intensity_per_channel=mean_intensity, max_intensity_per_channel=max_intensity, )) return features
def features_to_dataframe(features: Iterable[CellFeatures]) -> pd.DataFrame: """다운스트림 분석용 표 형태.""" rows = [] for f in features: row = asdict(f) for ch_name, val in row.pop("mean_intensity_per_channel").items(): row[f"mean_{ch_name}"] = val for ch_name, val in row.pop("max_intensity_per_channel").items(): row[f"max_{ch_name}"] = val rows.append(row) return pd.DataFrame(rows)Step 5. napari 시각화 + 수동 검수
napari는 실시간 4D 이미지 뷰어로, 자동 결과를 사람이 빠르게 검수·수정 가능합니다 [3].
def visualize_with_napari( image: np.ndarray, masks: np.ndarray, channel_names: list[str],) -> None: """napari로 이미지 + 마스크 오버레이 시각화. 실행 시 GUI 창이 열림. 원격 서버에서는 X11 forward 또는 napari 웹 뷰어(napari-remote) 사용. """ import napari viewer = napari.Viewer() for i, ch_name in enumerate(channel_names): viewer.add_image( image[i] if image.ndim == 3 else image, name=ch_name, colormap=["green", "red", "blue"][i % 3], blending="additive", ) viewer.add_labels(masks, name="cell masks", opacity=0.5) napari.run()통합 파이프라인
def full_pipeline( image_path: Path, channel_names: list[str], pixel_size_um: float, output_csv: Path, use_sam_refinement: bool = False, device: str = "cuda",) -> pd.DataFrame: """이미지 1장 → 세포 특성 DataFrame.""" img = load_microscopy_image(image_path, channel_names, pixel_size_um) # 세포체 채널 정규화 (예: FITC) cell_channel_idx = channel_names.index("FITC") if "FITC" in channel_names else 0 normalized = normalize_channel(img.data[cell_channel_idx]) segmenter = CellposeSegmenter(model_type="cyto3", restore_type="denoise", device=device) result = segmenter.segment(normalized) masks = result["masks"] if use_sam_refinement: refiner = SAM2Refiner(device=device) masks = refiner.refine_uncertain_cells(normalized, result) intensity_channels = { name: normalize_channel(img.data[i]) for i, name in enumerate(channel_names) } features = extract_features(masks, intensity_channels, pixel_size_um) df = features_to_dataframe(features) df["source_image"] = img.filename df.to_csv(output_csv, index=False) return df성능·비용·알려진 실패 케이스
성능 참고 (공개 벤치 인용)
| 접근 | 데이터셋 | F1 (세포 검출) | 처리 시간 | 출처 |
|---|---|---|---|---|
| Otsu + Watershed | Cellpose test set | 0.62 | CPU < 1s | Legacy baseline |
| StarDist | LIVECell | 0.71 | GPU 1~2s | Schmidt et al., MICCAI 2018 |
| Cellpose 2 | Cellpose test set | 0.86 | GPU 2~4s | Pachitariu · Stringer, Nat Methods 2022 |
| Cellpose 3 (with denoise) | Cellpose test set + low-SNR | 0.91 | GPU 3~5s | Stringer · Pachitariu, Nat Methods 2025 [1] |
| Cellpose-SAM | LIVECell + Cellpose test | 0.93~0.95 | GPU 5~8s | Cellpose team 2025 발표 (추정) |
| SAM 2 stand-alone (bio 적용) | LIVECell | 0.84 (프롬프트 없이) | GPU 3~5s | Meta AI 2024 [2] |
학습자 재현 예상 비용
- API 비용 0 (완전 로컬).
- 소형 소비자 GPU (RTX 4060 8GB) 기준 100장 이미지 처리 약 5~10분.
- CPU 폴백 시 100장 처리 1~2시간.
- 데이터 다운로드: Cellpose 예시 이미지 무료, LIVECell 공개 데이터셋 무료.
알려진 실패 케이스 3건 (커뮤니티·논문 수집형)
-
밀집 세포에서 인접 세포 병합 (under-segmentation)
증상: 세포가 서로 붙어있으면 하나의 큰 마스크로 병합됨.
원인: Cellpose flow field가 세포 경계의 gradient 신호를 놓침. 특히 낮은 SNR에서 심함.
회피: (a)flow_threshold를 낮춰(예: 0.2) 더 많은 flow를 세포로 인정, (b)cellprob_threshold를 조정, (c) SAM 2로 uncertain 영역만 refine.
출처: Cellpose GitHub Issues — "over/under segmentation" 다수 스레드 [4]. -
비정형 세포 형태(예: 신경세포 축삭·수상돌기)에서 실패
증상: cyto/nuclei 모델이 원형에 가깝게 학습되어 긴 축삭 가진 세포를 놓침.
원인: 학습 데이터의 편향.
회피: (a)livecell또는neurips모델 사용, (b) 자체 데이터로 fine-tuning (Cellpose 3는 GUI에서 매우 쉬움).
출처: Cellpose 공식 model zoo 문서 및 커뮤니티 리포트 [5]. -
다채널 이미지에서 채널 지정 오류
증상: channels=[0,0] (grayscale)로 두었는데 실제로는 nuclei 채널이 별도로 있어야 정확.
원인: Cellpose는 [cyto_channel, nuclei_channel] 형태로 채널 지정을 받는데 잘못 넘기면 성능 급락.
회피: 이미지 채널 순서를 확인 후 정확히 지정. DAPI가 있으면 nuclei 채널로 필수 지정.
출처: Cellpose 공식 documentation "channels" 섹션 [6].
확장 아이디어
- 시계열 라이브 이미징: SAM 2의 비디오 마스크 propagation을 활용해 세포 tracking + division event 자동 검출.
- 3D confocal stack: Cellpose 3의 3D 모드 + z-stack 각 슬라이스 SAM 2 refinement.
- 표현형 스크리닝: 형태 통계량을 다운스트림 분류기(예: XGBoost)에 넣어 약물 반응 자동 분류.
- QuPath 연동: 큰 whole slide image는 QuPath에서 tile 분할 → 각 tile을 이 파이프라인 → 결과 병합.
- napari 플러그인 패키징: 이 파이프라인 자체를 napari 플러그인으로 배포해 실험실 GUI 사용자에게 배포.
다음 편
- 편 05
pathology-image-embedding: 이 편에서 뽑은 세포 crop을 병리 파운데이션 모델(Virchow)로 임베딩해 유사 케이스 검색. - 편 07
drug-target-gnn: 세포 형태·강도 통계량을 약물 반응 예측 GNN 입력으로. - 편 12
single-cell-perturbation: 자동 분할한 세포에 인 실리코 유전자 섭동 예측 얹기. - 편 14
bio-mcp-agent: 이 파이프라인을 MCP tool로 노출해 "이 이미지에서 세포 수 세줘" 같은 자율 질의.
참고 문헌
- Stringer C, Pachitariu M. "Cellpose3: one-click image restoration for improved cellular segmentation." Nature Methods 2025.
https://www.nature.com/articles/s41592-025-02595-5 - Ravi N, Gabeur V, Hu Y-T, et al. "SAM 2: Segment Anything in Images and Videos." Meta AI 2024.
https://ai.meta.com/research/publications/sam-2-segment-anything-in-images-and-videos/ - napari documentation:
https://napari.org/stable/ - Cellpose GitHub Issues (over/under segmentation):
https://github.com/MouseLand/cellpose/issues - Cellpose Model Zoo:
https://cellpose.readthedocs.io/en/latest/models.html - Cellpose channels documentation:
https://cellpose.readthedocs.io/en/latest/inputs.html - Pachitariu M, Stringer C. "Cellpose 2.0: how to train your own model." Nature Methods 2022.
https://www.nature.com/articles/s41592-022-01663-4 - Schmidt U, Weigert M, Broaddus C, Myers G. "Cell Detection with Star-Convex Polygons (StarDist)." MICCAI 2018.
- LIVECell dataset:
https://sartorius-research.github.io/LIVECell/ - Cellpose GitHub:
https://github.com/MouseLand/cellpose - Segment Anything 2 GitHub:
https://github.com/facebookresearch/segment-anything-2 - scikit-image regionprops:
https://scikit-image.org/docs/stable/api/skimage.measure.html - QuPath (whole slide image analysis):
https://qupath.github.io/ - Human Protein Atlas (예시 이미지):
https://www.proteinatlas.org/ - Broad Bioimage Benchmark Collection:
https://bbbc.broadinstitute.org/