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. 06支持 16 位图像读写

适用场景

  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++ 支持的优化图像处理例程,可用于生物图像的预处理、分割、特征提取和视频分析。在生命科学领域,OpenCV 常用于荧光图像增强(背景扣除、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}")

关键参数

参数模块默认值效果
sigmaXGaussianBlur根据 ksize 自动确定高斯标准差;值越大,平滑效果越强
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)
轮廓将一个细胞拆分为多个二值掩码存在孔洞或噪声在检测轮廓前应用 cv2.MORPH_CLOSE;增大 Gaussian 模糊的 sigma
GaussianBlur 要求核尺寸为奇数提供了偶数核尺寸始终使用奇数核尺寸:3、5、7、9;使用 ksize=(5,5),而非 (4,4)
CLAHE 使图像效果变差clipLimit 过高将 clipLimit 降至 1.5–2.0;将 tileGridSize 增至 (16,16)
背景减除移除了细胞MOG2 的历史帧数过少增大 history 参数;显微成像时使用静态帧减除
大图像处理性能缓慢使用 Python 循环遍历像素使用 NumPy 向量化操作或 CUDA 加速的 cv2.cuda 模块

参考资料

评论

支持 Markdown