Cellpose 3 + SAM Cell Segmentation Pipeline: 個々の細胞を蛍光顕微鏡画像で識別するための実践的なガイド
蛍光顕微鏡画像で細胞の境界を手動でトレースしたことがある人は、このガイドが必要である理由をすぐに理解できるでしょう。1枚のスライドに500個の細胞があり、1日に100枚のスライドを処理する場合、50,000個の細胞を手動でラベル付けするには数日かかります。しかし、細胞の形状、面積、強度の定量的な分析は、後続の分析(薬剤応答、表現型スクリーニング、組織病理学)のための基本的な入力であるため、このボトルネックに対処することで、実験サイクル全体を加速させることができます。このガイドでは、Cellpose 3の画像修復機能とMetaのSegment Anything 2を組み合わせた、実践的なパイプラインを紹介します。これにより、このプロセスを約30分に短縮できます。
📚 推奨される前提知識
これは、AIと生物学を組み合わせた高度なトピックです。まず、以下のDryBenchチュートリアルを復習してから、このガイドに進むことを強くお勧めします。
前提知識がないと、このガイドでU-Netベースのエンコーダー-デコーダーアーキテクチャの原理とPyTorchテンソル/GPU転送について再説明しないため、実践的なコードを理解するのが難しくなります。
DryBenchで以前に学んだこと
DryBench ai-native #2では、ニューラルネットワークが、入力に重み付けされた乗算と非線形活性化関数を繰り返し適用することで表現を学習する方法、そして特に、U-Netベースのエンコーダー-デコーダーアーキテクチャが、画像(セグメンテーション)のピクセルごとの予測にどれほど適しているかを学びました。#12では、PyTorchテンソルの操作、GPU転送、およびモデルのフォワードパスの基本について説明しました。
ただし、実際の実験画像には、いくつかの追加の課題があります。これには、不均一な照明(ビネット)、焦点外のぼかし、細胞の密集、チャネル間の色の変化、および蛍光条件における低い信号対雑音比(SNR)が含まれます。ここでは、事前学習済みのCNNがこれらの条件下でどれだけ堅牢であるかを、そして最新の基盤モデル(SAM 2)がこの堅牢性をどのようにさらに高めることができるかを、実践的に検証します。
問題の定義
細胞セグメンテーションの実践的な要件
1つの蛍光画像(2048x2048)から、以下の4つの情報を自動的に抽出する必要があります。
- インスタンスセグメンテーション: 各細胞のユニークなIDとピクセルマスク。重なり合った細胞を区別します。
- 形態統計: 各細胞の面積(px²)、円形度、および偏心率。
- 強度統計: 各細胞の蛍光チャネルの平均、最大値、および標準偏差。
- 空間的関係: 隣接する細胞間の距離、それらがクラスターを形成しているかどうか。
目標指標:
- 細胞検出のリコール ≥ 0.92(人間のラベル付けと比較)。
- 細胞検出の精度 ≥ 0.90。
- 処理速度:GPUあたり3〜5秒、2048x2048の画像でCPUあたり30〜60秒。
既存のアプローチの概要
- 古典的なルールベース(Otsuの閾値処理 + 分水嶺): 低密度で高SNRの画像に対して0.7〜0.8のリコール。重なり合った細胞に対しては、多くの場合、失敗します。
- StarDist(星型凸多角形): 丸い核のセグメンテーションに強い。複雑な細胞体に対しては弱い。
- Cellpose 1/2(U-Net + フローフィールド): 幅広い細胞形状に対応。事前学習済みの重みを使用することで、ゼロショットで優れた性能を発揮します。Cellpose 3は、2024年に画像修復のバックボーンを追加しました。
- SAM(Segment Anything Model): 自然画像でトレーニングされていますが、ゼロショットで生物学的画像に対しても驚くほど効果的です。プロンプトが必要です。
- SAM 2: ビデオと2D画像を統合。強力なマスク伝播を実現します。Cellposeチームは、Cellpose-SAM(2025)の統合を発表しました。
Cellpose 3 + SAM 2の組み合わせは、現在、最も堅牢なゼロショットアプローチです。
ツールスタックとインフラストラクチャの要件
| ツール | 役割 | ライセンス |
|---|---|---|
Cellpose 3 (cellpose>=3.0) | 画像修復 + 細胞セグメンテーション | BSD-3-Clause |
Segment Anything 2 (segment-anything-2) | マスクの微調整、プロンプトベースの精密セグメンテーション | 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推論は非常に遅くなります。
- 16GB以上のRAM(2048x2048の多チャンネル画像をバッチ処理するため)。
- ディスク:Cellposeの重みは約30MB、SAM 2の基本重みは約160MB、大きな重みは約900MBです。
学習者が再現するための推定コスト: APIコストは0(完全にローカル)。100枚の画像を処理するためのGPU時間は、5〜10分(RTX 4060で推定)。データセットはオープン(Cellposeの公式のサンプル画像は無料)。
実用的なパイプラインの実装
全体的な流れ:
ステップ1:TIFF画像の読み込みと前処理
顕微鏡画像は通常、マルチチャンネルのTIFF形式(例:DAPI + FITC + TRITC)です。各チャンネルの動的範囲は大きく異なるため、正規化が重要です。
from pathlib import Pathfrom typing import NamedTuple
import numpy as npimport tifffile
class MicroscopyImage(NamedTuple): """顕微鏡画像コンテナ。""" data: np.ndarray # 形状:(チャンネル数, 高さ, 幅) または (高さ, 幅) channel_names: list[str] pixel_size_um: float # 1ピクセルの物理的なサイズ(マイクロメートル単位)。 filename: str
def load_microscopy_image( path: Path, channel_names: list[str] | None = None, pixel_size_um: float = 0.325,) -> MicroscopyImage: """TIFF画像を読み込みます。ピクセルサイズは、OME-TIFFメタデータから自動的に抽出できます。""" 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: """パーセンタイルに基づく正規化。外れ値に対して、最小-最大法よりも堅牢です。""" 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)ステップ2:Cellpose 3による画像修復 + セグメンテーション
Cellpose 3は、セグメンテーションの前段階として画像修復(ノイズ除去/ぼかし除去/アップサンプリング)を統合し、低品質の画像に対する結果を大幅に改善します[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) グレースケール、または (C, H, W) マルチチャンネル。 diameter: 推定される細胞の直径(ピクセル単位)。Noneの場合、自動的に推定されます。 channels: [細胞本体チャンネル、核チャンネル] のインデックス。 [0, 0] = グレースケール。 戻り値: {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, # 勾配フローの可視化 "diameter_used": diameter or self.model.diam_labels, }ステップ3:SAM 2による微調整(オプション)
Cellposeが曖昧にセグメント化したり、細胞が重なっている場合に、SAM 2を使用して境界を微調整します。SAM 2は、ポイント/ボックス/マスクのプロンプトに基づいて、微調整されたマスクを返します[2]。
from segment_anything_2 import SAM2ImagePredictor
class SAM2Refiner: """CellposeマスクをSAM 2を使用して微調整します。"""
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を使用して微調整します。""" self.predictor.set_image(image) # Cellposeマスクのバウンディングボックスと重心を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]), # 前景 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フローの信頼性が低い細胞のみをSAMで微調整します。""" masks = cellpose_result["masks"].copy() # flows[2]は、細胞確率マップです。 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ステップ4:形態学的および強度特徴の定量化
skimage.measure.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π・面積 / 周長² (円形=1) eccentricity: float # 偏心率(0=円、1=線) solidity: float # 面積 / 凸包の面積 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: {チャンネル名: (H, W)配列}。 """ 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)ステップ5:napariによる可視化 + 手動による検査
napariは、リアルタイムの4D画像ビューアーであり、ユーザーは自動化された結果をすばやく検査および修正できます[3]。
def visualize_with_napari( image: np.ndarray, masks: np.ndarray, channel_names: list[str],) -> None: """画像とマスクのオーバーレイをnapariで可視化します。
GUIウィンドウは実行時に開きます。リモートサーバー上では、X11転送またはnapari Webビューアー(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枚の画像)→細胞特徴データフレーム。""" 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 テストセット | 0.62 | CPU < 1秒 | 従来の手法 || StarDist | LIVECell | 0.71 | GPU 1〜2秒 | Schmidt et al., MICCAI 2018 || Cellpose 2 | Cellpose テストセット | 0.86 | GPU 2〜4秒 | Pachitariu & Stringer, Nat Methods 2022 || Cellpose 3 (ノイズ除去あり) | Cellpose テストセット + 低S/N比 | 0.91 | GPU 3〜5秒 | Stringer & Pachitariu, Nat Methods 2025 [1] || Cellpose-SAM | LIVECell + Cellpose テスト | 0.93〜0.95 | GPU 5〜8秒 | Cellpose チーム 2025 発表 (概算) || SAM 2 (スタンドアロン、生物学的応用) | LIVECell | 0.84 (プロンプトなし) | GPU 3〜5秒 | Meta AI 2024 [2] |
### 学習者が再現するための推定コスト
- API コスト: 0 (完全にローカル)。- 小型のコンシューマー GPU (RTX 4060 8GB) を使用した場合、約 100 枚の画像を処理するには 5〜10 分かかります。- CPU にフォールバックした場合、100 枚の画像の処理には 1〜2 時間かかります。- データダウンロード: Cellpose のサンプル画像は無料で、LIVECell データセットは一般公開されています。
### 3 つの既知の失敗事例 (コミュニティと論文から収集)
1. **高密度な細胞集団における隣接する細胞の過小セグメンテーション** - 症状: 細胞が互いに近接している場合、単一の大きなマスクに結合されます。 - 原因: Cellpose のフローフィールドが、細胞境界での勾配信号を捉えられません。これは、低 S/N 比の条件下でより顕著になります。 - 軽減策: (a) `flow_threshold` (例: 0.2) を下げて、より多くのフローを細胞の一部として認識させる、(b) `cellprob_threshold` を調整する、(c) SAM 2 を使用して、不確かな領域のみを修正する。 - 出典: Cellpose GitHub Issues の多数のスレッド — "過剰/過小セグメンテーション" [4]。
2. **非典型的な細胞形態 (例: ニューロンの軸索/樹状突起) での失敗** - 症状: cyto/核モデルは円形の形状を認識するようにトレーニングされているため、長い軸索を持つ細胞を見逃します。 - 原因: トレーニングデータのバイアス。 - 軽減策: (a) `livecell` または `neurips` モデルを使用する、(b) 独自のデータで微調整する (Cellpose 3 の GUI で非常に簡単に行えます)。 - 出典: Cellpose の公式モデル Zoo ドキュメントとコミュニティからの報告 [5]。
3. **多チャンネル画像におけるチャンネル割り当てエラー** - 症状: `channels=[0,0]` (グレースケール) を設定した場合に、実際に核チャンネルを個別に指定する必要があるにもかかわらず、正しく指定しないと、結果が正確でなくなります。 - 原因: Cellpose は、`[cyto_channel, nuclei_channel]` という形式でチャンネル割り当てを受け付け、これを正しく行わないとパフォーマンスが大幅に低下します。 - 軽減策: 画像のチャンネルの順序を確認し、正確に指定します。DAPI が存在する場合は、核チャンネルとして指定する必要があります。 - 出典: Cellpose の公式ドキュメント、「channels」セクション [6]。
## 拡張のアイデア
- **時系列ライブイメージング:** SAM 2 のビデオマスク伝播を利用して、細胞追跡と自動的な細胞分裂イベントの検出を可能にします。- **3D コンフォーカルスタック:** Cellpose 3 の 3D モードを使用し、Z スタックの各スライスを SAM 2 で修正します。- **表現型スクリーニング:** 形態統計量をダウンストリームの分類器 (例: XGBoost) に入力して、薬物応答を自動的に分類します。- **QuPath 統合:** 大きなホールスライド画像の場合、QuPath でタイルに分割し、各タイルをこのパイプラインで処理し、次に結果を結合します。- **napari プラグインパッケージ:** このパイプラインを napari プラグインとしてパッケージ化し、実験室の GUI ユーザーに配布します。
## 次のエピソード
- エピソード 05 `pathology-image-embedding`: このエピソードで抽出した細胞クロップを、病理学の基礎モデル (Virchow) を使用して埋め込み、類似の症例を検索します。- エピソード 07 `drug-target-gnn`: 細胞の形態と強度統計量を入力として、薬物応答予測のための GNN を使用します。- エピソード 12 `single-cell-perturbation`: 自動セグメンテーションされた細胞に、in silico の遺伝子擾乱予測を適用します。- エピソード 14 `bio-mcp-agent`: このパイプラインを MCP ツールとして公開し、「この画像内の細胞の数をカウントする」などの自律的なクエリを可能にします。
## 参考文献
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 ドキュメント: `https://napari.org/stable/`4. Cellpose GitHub Issues (過剰/過小セグメンテーション): `https://github.com/MouseLand/cellpose/issues`5. Cellpose モデル Zoo: `https://cellpose.readthedocs.io/en/latest/models.html`6. Cellpose チャンネルドキュメント: `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 データセット: `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 (ホールスライド画像解析): `https://qupath.github.io/`14. Human Protein Atlas (サンプル画像): `https://www.proteinatlas.org/`15. Broad Bioimage Benchmark Collection: `https://bbbc.broadinstitute.org/`