BioPlayground

🧬
목록으로

Cellpose 3 + SAM 세포 분할 파이프라인 — 형광 현미경 이미지에서 세포 하나하나를 잡아내는 실전

Cellpose 3(2024 이미지 복원 특화형) + SAM 2 하이브리드로 형광 현미경 이미지에서 세포 경계 자동 검출. 배경 노이즈 복원, 세포 인스턴스 세그멘테이션, 면적·원형도·강도 형태 통계량 정량, napari 시각화까지 완결 파이프라인.

중급
|
30
|
검증 완료 (2026-07)
진행률0/15 (0%)

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 35초, 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
tifffileTIFF 다채널 현미경 이미지 읽기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 공식 예시 이미지 무료).

파이프라인 실전 구현

전체 흐름:

mermaid

Step 1. TIFF 이미지 로딩과 전처리

현미경 이미지는 대개 다채널 TIFF (예: DAPI + FITC + TRITC). 각 채널의 dynamic range가 극단적으로 다를 수 있어 정규화가 중요합니다.

python
from pathlib import Path
from typing import NamedTuple
import numpy as np
import 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].

python
from cellpose import models, io as cp_io
from 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].

python
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 masks

Step 4. 형태·강도 통계량 정량

scikit-imageregionprops로 세포마다 표준 지표를 뽑습니다.

python
from dataclasses import dataclass, asdict
from typing import Iterable
from skimage.measure import regionprops, regionprops_table
import pandas as pd
@dataclass
class 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].

python
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()

통합 파이프라인

python
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 + WatershedCellpose test set0.62CPU < 1sLegacy baseline
StarDistLIVECell0.71GPU 1~2sSchmidt et al., MICCAI 2018
Cellpose 2Cellpose test set0.86GPU 2~4sPachitariu · Stringer, Nat Methods 2022
Cellpose 3 (with denoise)Cellpose test set + low-SNR0.91GPU 3~5sStringer · Pachitariu, Nat Methods 2025 [1]
Cellpose-SAMLIVECell + Cellpose test0.93~0.95GPU 5~8sCellpose team 2025 발표 (추정)
SAM 2 stand-alone (bio 적용)LIVECell0.84 (프롬프트 없이)GPU 3~5sMeta AI 2024 [2]

학습자 재현 예상 비용

  • API 비용 0 (완전 로컬).
  • 소형 소비자 GPU (RTX 4060 8GB) 기준 100장 이미지 처리 약 5~10분.
  • CPU 폴백 시 100장 처리 1~2시간.
  • 데이터 다운로드: Cellpose 예시 이미지 무료, LIVECell 공개 데이터셋 무료.

알려진 실패 케이스 3건 (커뮤니티·논문 수집형)

  1. 밀집 세포에서 인접 세포 병합 (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].

  2. 비정형 세포 형태(예: 신경세포 축삭·수상돌기)에서 실패
    증상: cyto/nuclei 모델이 원형에 가깝게 학습되어 긴 축삭 가진 세포를 놓침.
    원인: 학습 데이터의 편향.
    회피: (a) livecell 또는 neurips 모델 사용, (b) 자체 데이터로 fine-tuning (Cellpose 3는 GUI에서 매우 쉬움).
    출처: Cellpose 공식 model zoo 문서 및 커뮤니티 리포트 [5].

  3. 다채널 이미지에서 채널 지정 오류
    증상: 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로 노출해 "이 이미지에서 세포 수 세줘" 같은 자율 질의.

참고 문헌

  1. 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
  2. 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/
  3. napari documentation: https://napari.org/stable/
  4. Cellpose GitHub Issues (over/under segmentation): https://github.com/MouseLand/cellpose/issues
  5. Cellpose Model Zoo: https://cellpose.readthedocs.io/en/latest/models.html
  6. Cellpose channels documentation: https://cellpose.readthedocs.io/en/latest/inputs.html
  7. 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
  8. Schmidt U, Weigert M, Broaddus C, Myers G. "Cell Detection with Star-Convex Polygons (StarDist)." MICCAI 2018.
  9. LIVECell dataset: https://sartorius-research.github.io/LIVECell/
  10. Cellpose GitHub: https://github.com/MouseLand/cellpose
  11. Segment Anything 2 GitHub: https://github.com/facebookresearch/segment-anything-2
  12. scikit-image regionprops: https://scikit-image.org/docs/stable/api/skimage.measure.html
  13. QuPath (whole slide image analysis): https://qupath.github.io/
  14. Human Protein Atlas (예시 이미지): https://www.proteinatlas.org/
  15. Broad Bioimage Benchmark Collection: https://bbbc.broadinstitute.org/