opencv-bioimage-analysis について
生物画像処理向けのOpenCVスキルで、前処理、従来型のセグメンテーション、特徴抽出、動画解析を扱います。顕微鏡画像の高速処理やリアルタイム撮像を必要とする研究ワークフローに適しています。
主な機能
- 01画像フィルタリングとコントラスト強調
- 02しきい値分割と輪郭検出
- 03形態学的なマスクの調整
- 04特徴抽出とテンプレートマッチング
- 05動画ストリームとオプティカルフロー処理
- 0616ビット画像の読み書き
活用シーン
- 01蛍光画像や明視野画像の強調
- 02細胞の輪郭やブロブの検出
- 03ライブセルのタイムラプス動画処理
- 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}")
主なパラメーター
| パラメーター | モジュール | デフォルト | 効果 |
|---|---|---|---|
sigmaX | GaussianBlur | ksize から自動設定 | ガウス標準偏差。大きいほど平滑化が強くなります |
clipLimit | createCLAHE | 40.0 | コントラスト増幅の最大値。顕微鏡画像では 2.0~4.0 |
tileGridSize | createCLAHE | (8,8) | 局所ヒストグラム均等化に使うタイルサイズ |
blockSize | adaptiveThreshold | 必須 | 適応的しきい値処理の近傍サイズ(奇数、3 以上である必要があります) |
C | adaptiveThreshold | 必須 | 平均値から差し引く定数。差し引く場合は正の値を指定します |
iterations | morphologyEx | 1 | 収縮/膨張の繰り返し回数。多いほど効果が強くなります |
history | BackgroundSubtractorMOG2 | 500 | 背景のモデル化に使うフレーム数。少ないほど適応が速くなります |
varThreshold | BackgroundSubtractorMOG2 | 16 | ピクセルの分散しきい値。大きいほど感度が低くなります |
minArea | 輪郭フィルター | — | 残す最小の cv2.contourArea(cnt)。ノイズを除去します |
cv2.IMREAD_UNCHANGED | imread | — | ビット深度(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 モジュールを使用します |
参考資料
- OpenCV Python ドキュメント — Python API のリファレンスとチュートリアル
- OpenCV GitHub:opencv/opencv — ソースコードと Python バインディング
- Bradski G (2000)「The OpenCV Library」— Dr. Dobb's Journal of Software Tools 25(11):120-125
- PyImageSearch のチュートリアル — 生物画像解析におけるコンピュータービジョンへの OpenCV の応用
コメント
Markdown 対応