mcpField
O
opencv-bioimage-analysis

opencv-bioimage-analysis

github.com/jaechang-hitsデータ・データベース38478 回インストール6 日前に更新
リポジトリ

opencv-bioimage-analysis について

生物画像処理向けのOpenCVスキルで、前処理、従来型のセグメンテーション、特徴抽出、動画解析を扱います。顕微鏡画像の高速処理やリアルタイム撮像を必要とする研究ワークフローに適しています。

主な機能

  1. 01画像フィルタリングとコントラスト強調
  2. 02しきい値分割と輪郭検出
  3. 03形態学的なマスクの調整
  4. 04特徴抽出とテンプレートマッチング
  5. 05動画ストリームとオプティカルフロー処理
  6. 0616ビット画像の読み書き

活用シーン

  1. 01蛍光画像や明視野画像の強調
  2. 02細胞の輪郭やブロブの検出
  3. 03ライブセルのタイムラプス動画処理
  4. 04既存のセグメンテーションマスクの調整

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/jaechang-hits/sciagent-skills --skill opencv-bioimage-analysis

インストール後、Claude Code で/opencv-bioimage-analysisを入力して読み込まれたか確認します。

詳細

OpenCV — バイオ画像のコンピュータービジョン

概要

OpenCV(cv2)は、C++ による最適化処理を備えた画像処理ルーチンを提供し、生物画像の前処理、セグメンテーション、特徴抽出、動画解析に利用できます。ライフサイエンス分野では、蛍光画像の強調(背景差分、CLAHE)、形態学的セグメンテーション(ウォーターシェッド、輪郭検出)、明視野画像での細胞検出、リアルタイムの顕微鏡ストリーム処理に使われています。科学的な測定を重視する scikit-image とは異なり、OpenCV は計算速度と動画への対応を優先しているため、前処理パイプラインやリアルタイムイメージングアプリケーションに最適です。

使用する場面

  • 蛍光画像または明視野画像を前処理する場合:背景差分、CLAHE、ガウシアン/メディアンぼかし
  • ディープラーニングを使わずに、古典的手法で細胞の輪郭、ブロブ、エッジを検出する場合
  • ライブセルイメージング顕微鏡の動画ストリームをリアルタイムで処理する場合
  • 繰り返し現れる構造(オルガネラ、結晶、パターン)を見つけるためのテンプレートマッチング
  • マスクを調整するために形態学的演算(収縮、膨張、オープニング、クロージング)を適用する場合
  • 細胞を追跡するために動画フレーム間のオプティカルフローを計算する場合
  • 科学的な形態計測、regionprops、科学画像 I/O(TIFF メタデータ)には、代わりに scikit-image を使用してください
  • 蛍光画像のディープラーニングによる細胞セグメンテーションには、代わりに Cellpose または StarDist を使用してください

前提条件

  • Python パッケージ:opencv-python、numpy、matplotlib
  • 任意:追加モジュール(SIFT、SURF、オプティカルフロー)用の opencv-contrib-python
# Install OpenCV
pip install opencv-python

# Install with extra contributed modules (SIFT, SURF, etc.)
pip install opencv-contrib-python

# Verify
python -c "import cv2; print(cv2.__version__)"
# 4.10.0

クイックスタート

import cv2
import numpy as np

# Read and display image info
img = cv2.imread("cells.tif", cv2.IMREAD_GRAYSCALE)
print(f"Shape: {img.shape}, dtype: {img.dtype}")
print(f"Min: {img.min()}, Max: {img.max()}")

# Apply Gaussian blur and threshold
blurred = cv2.GaussianBlur(img, (5, 5), 0)
_, binary = cv2.threshold(blurred, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
print(f"Cells detected (rough): {np.sum(binary > 0)} foreground pixels")

コア API

モジュール 1:画像の I/O と色空間変換

画像の読み込み、書き込み、色空間間の変換を行います。

import cv2
import numpy as np

# Read image (GRAYSCALE, COLOR, or UNCHANGED for 16-bit)
img_gray  = cv2.imread("cells.tif", cv2.IMREAD_GRAYSCALE)   # uint8
img_color = cv2.imread("rgb.tif", cv2.IMREAD_COLOR)         # BGR order!
img_16bit = cv2.imread("16bit.tif", cv2.IMREAD_UNCHANGED)   # uint16

print(f"Grayscale shape: {img_gray.shape}, dtype: {img_gray.dtype}")
print(f"Color shape:     {img_color.shape}")

# Color space conversions
img_rgb  = cv2.cvtColor(img_color, cv2.COLOR_BGR2RGB)   # BGR → RGB
img_hsv  = cv2.cvtColor(img_color, cv2.COLOR_BGR2HSV)   # BGR → HSV
img_gray2 = cv2.cvtColor(img_color, cv2.COLOR_BGR2GRAY) # BGR → gray

# Write image
cv2.imwrite("output.png", img_gray)
cv2.imwrite("output_16bit.tif", img_16bit)
print("Images written.")
モジュール 2:フィルタリングと強調

画像の前処理にフィルターとコントラスト強調を適用します。

import cv2
import numpy as np

img = cv2.imread("cells.tif", cv2.IMREAD_GRAYSCALE)

# Gaussian blur (noise reduction)
blurred = cv2.GaussianBlur(img, (7, 7), sigmaX=1.5)

# Median blur (salt-and-pepper noise)
median = cv2.medianBlur(img, 5)

# CLAHE: Contrast Limited Adaptive Histogram Equalization (for microscopy)
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
clahe_img = clahe.apply(img)

# Top-hat filter for bright spots on dark background
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (15, 15))
tophat = cv2.morphologyEx(img, cv2.MORPH_TOPHAT, kernel)

print(f"CLAHE range: [{clahe_img.min()}, {clahe_img.max()}]")
cv2.imwrite("clahe_enhanced.tif", clahe_img)
モジュール 3:しきい値処理とバイナリーセグメンテーション

さまざまなしきい値処理方法を使って、グレースケール画像をバイナリーマスクに変換します。

import cv2
import numpy as np

img = cv2.imread("nuclei.tif", cv2.IMREAD_GRAYSCALE)

# Otsu's thresholding (automatic threshold selection)
thresh_val, otsu_mask = cv2.threshold(img, 0, 255,
                                      cv2.THRESH_BINARY + cv2.THRESH_OTSU)
print(f"Otsu threshold: {thresh_val:.0f}")

# Adaptive thresholding (handles uneven illumination)
adaptive = cv2.adaptiveThreshold(
    img, 255,
    cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
    cv2.THRESH_BINARY,
    blockSize=11,     # neighborhood size (odd)
    C=2,              # constant subtracted from mean
)

# For 16-bit images: normalize first
img_16 = cv2.imread("16bit_nuclei.tif", cv2.IMREAD_UNCHANGED)
img_8 = cv2.normalize(img_16, None, 0, 255, cv2.NORM_MINMAX, dtype=cv2.CV_8U)
_, mask_16 = cv2.threshold(img_8, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)

print(f"Otsu mask foreground: {mask_16.sum() / 255} pixels")
モジュール 4:輪郭検出と計測

バイナリーマスクから細胞の輪郭を検出し、計測します。

import cv2
import numpy as np
import pandas as pd

img = cv2.imread("cells.tif", cv2.IMREAD_GRAYSCALE)
blurred = cv2.GaussianBlur(img, (5, 5), 0)
_, binary = cv2.threshold(blurred, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)

# Remove small objects with morphological opening
kernel = np.ones((3, 3), np.uint8)
cleaned = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel, iterations=2)

# Find contours
contours, hierarchy = cv2.findContours(cleaned, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
print(f"Objects detected: {len(contours)}")

# Measure each contour
records = []
for i, cnt in enumerate(contours):
    area = cv2.contourArea(cnt)
    if area < 50: continue  # skip tiny objects
    perimeter = cv2.arcLength(cnt, True)
    x, y, w, h = cv2.boundingRect(cnt)
    (cx, cy), radius = cv2.minEnclosingCircle(cnt)
    records.append({"cell_id": i, "area": area, "perimeter": perimeter,
                     "x": x, "y": y, "w": w, "h": h, "radius": radius})

df = pd.DataFrame(records)
print(f"Cells > 50 px²: {len(df)}")
print(df[["area", "perimeter", "radius"]].describe())
モジュール 5:マスク調整のための形態学的演算

形態学的演算を使ってセグメンテーションマスクを調整します。

import cv2
import numpy as np

# Load binary mask (from thresholding or Cellpose)
mask = cv2.imread("rough_mask.png", cv2.IMREAD_GRAYSCALE)
_, mask = cv2.threshold(mask, 127, 255, cv2.THRESH_BINARY)

# Structural elements
ellipse = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (7, 7))
rect    = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))

# Opening: remove small bright noise
opened = cv2.morphologyEx(mask, cv2.MORPH_OPEN, ellipse, iterations=1)

# Closing: fill small holes inside cells
closed = cv2.morphologyEx(opened, cv2.MORPH_CLOSE, ellipse, iterations=2)

# Dilation: expand cell boundaries slightly
dilated = cv2.dilate(closed, ellipse, iterations=1)

# Distance transform for watershed seed generation
dist = cv2.distanceTransform(closed, cv2.DIST_L2, 5)
_, seeds = cv2.threshold(dist, 0.5 * dist.max(), 255, 0)
seeds = seeds.astype(np.uint8)
print(f"Potential cell centers: {cv2.connectedComponents(seeds)[0] - 1}")
モジュール 6:ライブセルイメージングのための動画処理

タイムラプス顕微鏡の動画ストリームを処理します。

import cv2
import numpy as np

# Process a time-lapse video file
cap = cv2.VideoCapture("timelapse.avi")
fps = cap.get(cv2.CAP_PROP_FPS)
n_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
print(f"Video: {n_frames} frames at {fps} FPS")

# Background subtraction (remove static background)
bg_subtractor = cv2.createBackgroundSubtractorMOG2(
    history=50, varThreshold=25, detectShadows=False
)

frame_counts = []
frame_idx = 0
while cap.isOpened():
    ret, frame = cap.read()
    if not ret: break
    
    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    fg_mask = bg_subtractor.apply(gray)
    
    # Count moving objects in this frame
    contours, _ = cv2.findContours(fg_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    moving = [c for c in contours if cv2.contourArea(c) > 100]
    frame_counts.append(len(moving))
    frame_idx += 1

cap.release()
print(f"Processed {frame_idx} frames. Mean moving objects: {np.mean(frame_counts):.1f}")

主なパラメーター

パラメーターモジュールデフォルト効果
sigmaXGaussianBlurksize から自動設定ガウス標準偏差。大きいほど平滑化が強くなります
clipLimitcreateCLAHE40.0コントラスト増幅の最大値。顕微鏡画像では 2.0~4.0
tileGridSizecreateCLAHE(8,8)局所ヒストグラム均等化に使うタイルサイズ
blockSizeadaptiveThreshold必須適応的しきい値処理の近傍サイズ(奇数、3 以上である必要があります)
CadaptiveThreshold必須平均値から差し引く定数。差し引く場合は正の値を指定します
iterationsmorphologyEx1収縮/膨張の繰り返し回数。多いほど効果が強くなります
historyBackgroundSubtractorMOG2500背景のモデル化に使うフレーム数。少ないほど適応が速くなります
varThresholdBackgroundSubtractorMOG216ピクセルの分散しきい値。大きいほど感度が低くなります
minArea輪郭フィルター—残す最小の cv2.contourArea(cnt)。ノイズを除去します
cv2.IMREAD_UNCHANGEDimread—ビット深度(16 ビット、32 ビット)を保持します。科学画像には必須です

よくあるワークフロー

ワークフロー 1:蛍光核の検出パイプライン
import cv2
import numpy as np
import pandas as pd

def detect_nuclei(image_path: str, min_area: int = 200) -> pd.DataFrame:
    """Detect DAPI-stained nuclei from a fluorescence image."""
    img = cv2.imread(image_path, cv2.IMREAD_UNCHANGED)
    
    # Normalize 16-bit to 8-bit
    if img.dtype == np.uint16:
        img = cv2.normalize(img, None, 0, 255, cv2.NORM_MINMAX, dtype=cv2.CV_8U)
    
    # Preprocess: CLAHE → Gaussian blur
    clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
    enhanced = clahe.apply(img)
    blurred = cv2.GaussianBlur(enhanced, (5, 5), 1.5)
    
    # Segment: Otsu threshold → morphological opening
    _, binary = cv2.threshold(blurred, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
    kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
    cleaned = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel, iterations=1)
    
    # Find and measure contours
    contours, _ = cv2.findContours(cleaned, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    records = []
    for cnt in contours:
        area = cv2.contourArea(cnt)
        if area < min_area: continue
        M = cv2.moments(cnt)
        if M["m00"] == 0: continue
        cx = int(M["m10"] / M["m00"])
        cy = int(M["m01"] / M["m00"])
        records.append({"area": area, "cx": cx, "cy": cy,
                         "perimeter": cv2.arcLength(cnt, True)})
    
    return pd.DataFrame(records)

df = detect_nuclei("dapi.tif", min_area=300)
print(f"Nuclei detected: {len(df)}")
print(df.describe())
ワークフロー 2:画像ディレクトリの一括処理
import cv2
import numpy as np
import pandas as pd
from pathlib import Path

def process_image(path: str) -> dict:
    img = cv2.imread(path, cv2.IMREAD_GRAYSCALE)
    if img is None:
        return {}
    blurred = cv2.GaussianBlur(img, (5, 5), 0)
    _, binary = cv2.threshold(blurred, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
    contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    cells = [c for c in contours if cv2.contourArea(c) > 200]
    return {"file": Path(path).name, "cell_count": len(cells),
            "mean_area": np.mean([cv2.contourArea(c) for c in cells]) if cells else 0}

results = [process_image(str(p)) for p in sorted(Path("images").glob("*.tif"))]
df = pd.DataFrame([r for r in results if r])
print(df)
df.to_csv("batch_results.csv", index=False)
print("Saved: batch_results.csv")

よくあるレシピ

レシピ 1:画像上で検出した細胞に注釈を付ける
import cv2
import numpy as np

img = cv2.imread("cells.tif", cv2.IMREAD_GRAYSCALE)
img_color = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)

blurred = cv2.GaussianBlur(img, (5, 5), 0)
_, binary = cv2.threshold(blurred, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

for i, cnt in enumerate(contours):
    if cv2.contourArea(cnt) < 200: continue
    # Draw contour outline
    cv2.drawContours(img_color, [cnt], -1, (0, 255, 0), 2)
    # Label with cell number
    M = cv2.moments(cnt)
    if M["m00"] > 0:
        cx, cy = int(M["m10"] / M["m00"]), int(M["m01"] / M["m00"])
        cv2.putText(img_color, str(i), (cx - 5, cy), cv2.FONT_HERSHEY_SIMPLEX,
                    0.4, (255, 255, 0), 1)

cv2.imwrite("annotated_cells.png", img_color)
print(f"Annotated {len(contours)} cells. Saved: annotated_cells.png")
レシピ 2:ローリングボールによる背景差分
import cv2
import numpy as np

def rolling_ball_background(img: np.ndarray, radius: int = 50) -> np.ndarray:
    """Estimate and subtract background using a blur approximation."""
    kernel_size = 2 * radius + 1
    background = cv2.GaussianBlur(img, (kernel_size, kernel_size), radius / 3)
    corrected = cv2.subtract(img, background)
    return corrected

img = cv2.imread("uneven_fluorescence.tif", cv2.IMREAD_GRAYSCALE)
corrected = rolling_ball_background(img, radius=50)
cv2.imwrite("background_corrected.tif", corrected)
print(f"Background corrected. Range: [{corrected.min()}, {corrected.max()}]")

トラブルシューティング

問題原因解決策
imread が None を返すファイルが見つからない、または未対応の形式です絶対パスを使用し、Path(path).exists() で確認します。TIFF の場合は cv2.IMREAD_UNCHANGED を使用します
16 ビット画像が黒く表示されるIMREAD_GRAYSCALE が uint8 にクリップしますcv2.IMREAD_UNCHANGED を使用して、次のように正規化します:cv2.normalize(img, None, 0, 255, cv2.NORM_MINMAX)
BGR と RGB の色が一致しないOpenCV は BGR、matplotlib は RGB を使用しますplt.imshow() の前に変換します:rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
輪郭検出で 1 つの細胞が複数に分割される二値マスクに穴やノイズがあります輪郭検出の前に cv2.MORPH_CLOSE を適用し、ガウスぼかしのシグマを大きくします
GaussianBlur に奇数のカーネルが必要偶数のカーネルサイズが指定されています常に奇数のカーネルサイズを使用します:3、5、7、9。(4,4) ではなく ksize=(5,5) を指定します
CLAHE で画像が悪化するclipLimit が大きすぎますclipLimit を 1.5–2.0 に下げ、tileGridSize を (16,16) に大きくします
背景差分で細胞が消えるMOG2 の履歴が短すぎますhistory パラメーターを大きくします。顕微鏡画像では静止フレームの差分を使用します
大きな画像で処理が遅いピクセルを Python のループで処理していますNumPy のベクトル化演算、または CUDA で高速化された cv2.cuda モジュールを使用します

参考資料

コメント