首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >基于 nnU-Net 的 IHC Ki-67 细胞分类实战指南

基于 nnU-Net 的 IHC Ki-67 细胞分类实战指南

原创
作者头像
buzzfrog
发布2025-12-05 18:42:49
发布2025-12-05 18:42:49
3710
举报
文章被收录于专栏:云上修行云上修行

摘要

本文详细介绍了使用 nnU-Net 框架对免疫组化(IHC)Ki-67 染色病理图像进行细胞分割与分类的完整流程。从数据准备、格式转换、模型训练到推理评估,涵盖了实际项目中的各个环节。本实验使用 BCData 数据集,目标是自动识别和分类病理图像中的 Ki-67 阳性和阴性肿瘤细胞。

关键词:nnU-Net、Ki-67、免疫组化、细胞分割、深度学习、病理图像分析


1. 背景介绍

1.1 Ki-67 增殖指数的临床意义

Ki-67 是一种核蛋白,仅在细胞增殖周期(G1、S、G2 和 M 期)中表达,而在静止期(G0 期)不表达。通过免疫组化染色检测 Ki-67 阳性细胞的比例(Ki-67 增殖指数),可以评估肿瘤的增殖活性,是乳腺癌等多种肿瘤预后评估和治疗决策的重要指标。

传统的 Ki-67 计数依赖病理医师人工判读,存在以下问题:

  • 工作量大、耗时长
  • 观察者间一致性较差
  • 主观因素影响判读结果

因此,基于深度学习的自动化细胞分割与分类方法具有重要的临床应用价值。

1.2 nnU-Net 框架简介

nnU-Net(no-new-Net)是由德国癌症研究中心(DKFZ)开发的自动化医学图像分割框架。其核心优势在于:

  • 自配置能力:根据数据集特性自动选择最优的预处理、网络架构和训练策略
  • 开箱即用:无需手动调参,即可获得高质量的分割结果
  • SOTA 性能:在多个医学图像分割基准测试中取得最优或接近最优的结果
  • 灵活性:支持 2D 和 3D 数据,支持多种图像格式

引用:Isensee, F., Jaeger, P. F., Kohl, S. A., Petersen, J., & Maier-Hein, K. H. (2021). nnU-Net: a self-configuring method for deep learning-based biomedical image segmentation. Nature Methods, 18(2), 203-211.


2. 实验环境与数据集

2.1 硬件与软件环境

代码语言:txt
复制
操作系统: Ubuntu 22.04 / macOS
GPU: NVIDIA GPU (推荐 24GB 显存以上)
Python: 3.10.19
PyTorch: 2.4.1
nnU-Net: v2.6.2

2.2 BCData 数据集

本实验使用 BCData 数据集,该数据集专门用于乳腺癌 Ki-67 细胞检测任务。

数据集特征

  • 图像格式:640×640 RGB PNG 图像
  • 标注格式:细胞中心点坐标(H5 格式)
  • 分类类别
    • 阳性细胞(Positive):Ki-67 染色阳性,呈棕褐色
    • 阴性细胞(Negative):Ki-67 染色阴性,呈蓝色

数据集目录结构

代码语言:txt
复制
BCData/
├── images/
│   ├── train/          # 训练集图像
│   ├── validation/     # 验证集图像
│   └── test/           # 测试集图像
└── annotations/
    ├── train/
    │   ├── positive/   # 阳性细胞坐标 (.h5)
    │   └── negative/   # 阴性细胞坐标 (.h5)
    └── validation/
        ├── positive/
        └── negative/
数据集数据图片示例
数据集数据图片示例

2.3 安装 nnU-Net

代码语言:bash
复制
# 创建虚拟环境
conda create -n nnunetv2 python=3.10
conda activate nnunetv2

# 安装 PyTorch(根据 CUDA 版本选择)
pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121

# 安装 nnU-Net
pip3 install nnunetv2 nibabel matplotlib h5py opencv-python 

python3 -c "import nnunetv2; print('nnU-Net v2 安装成功!')"

# 例子工作区的路径
mkdir -p ~/nnUNet_workspace
cd ~/nnUNet_workspace
export nnUNet_raw="$PWD/ "
export nnUNet_preprocessed="$PWD/nnUNet_preprocessed"
export nnUNet_results="$PWD/nnUNet_results"

3. 数据预处理与格式转换

3.1 理解 nnU-Net v2 数据格式

nnU-Net v2 要求数据按照特定格式组织。对于本任务,需要将 BCData 的点标注转换为分割掩码(Segmentation Mask)。

nnU-Net 数据集结构

代码语言:txt
复制
nnUNet_raw/
└── Dataset100_Ki67/
    ├── imagesTr/           # 训练图像
    │   ├── Ki67_00000_0000.png
    │   ├── Ki67_00001_0000.png
    │   └── ...
    ├── labelsTr/           # 训练标签(分割掩码)
    │   ├── Ki67_00000.png
    │   ├── Ki67_00001.png
    │   └── ...
    ├── imagesTs/           # 测试图像(可选)
    └── dataset.json        # 数据集元数据

命名规则

  • 图像文件:{数据集名称}_{样本编号:05d}_{通道编号:04d}.{扩展名}
  • 标签文件:{数据集名称}_{样本编号:05d}.{扩展名}

3.2 数据转换脚本

我们开发了 convert_bcdata.py 脚本(见附录代码),用于将 BCData 格式转换为 nnU-Net 格式。核心转换逻辑包括:

3.2.1 从点标注生成分割掩码

由于原始数据仅提供细胞中心点坐标,需要将其转换为分割掩码:

代码语言:python
复制
def create_segmentation_mask(image_shape, positive_coords, negative_coords, cell_radius=3):
    """
    从点坐标创建分割mask
    
    Args:
        image_shape: 图片尺寸 (height, width)
        positive_coords: 阳性细胞坐标 (N, 2) - [x, y]
        negative_coords: 阴性细胞坐标 (M, 2) - [x, y]
        cell_radius: 细胞半径(像素)
        
    Returns:
        mask: 分割mask
              0 = 背景
              1 = 阳性细胞
              2 = 阴性细胞
    """
    mask = np.zeros(image_shape, dtype=np.uint8)
    
    # 先绘制阴性细胞(优先级较低)
    for coord in negative_coords:
        x, y = int(coord[0]), int(coord[1])
        cv2.circle(mask, (x, y), cell_radius, 2, -1)
    
    # 后绘制阳性细胞(优先级较高)
    for coord in positive_coords:
        x, y = int(coord[0]), int(coord[1])
        cv2.circle(mask, (x, y), cell_radius, 1, -1)
    
    return mask

3.2.2 生成 dataset.json

代码语言:python
复制
dataset_json = {
    "channel_names": {
        "0": "R",
        "1": "G",
        "2": "B"
    },
    "labels": {
        "background": 0,
        "positive_cell": 1,
        "negative_cell": 2
    },
    "numTraining": num_training,
    "file_ending": ".png",
    "name": "Ki67",
    "description": "IHC Ki-67 cell segmentation"
}

3.2.3 执行转换

代码语言:bash
复制
# 基本用法(输出 RGB 格式)
python3 convert_bcdata.py --input ./BCData --output ./nnUNet_raw

# 自定义参数
python3 convert_bcdata.py \
    --input ./BCData \
    --output ./nnUNet_raw \
    --dataset_id 100 \
    --dataset_name Ki67 \
    --cell_radius 3 \
    --mode rgb \
    --include_test

参数说明

参数

默认值

说明

--input

./BCData

源数据目录

--output

./nnUNet_raw

输出目录

--dataset_id

100

nnU-Net 数据集 ID (001-999)

--dataset_name

Ki67

数据集名称

--cell_radius

3

细胞标注半径(像素)

--mode

rgb

输出模式:rgb(彩色)或 grey(灰度)

--include_test

False

是否处理测试集


4. 环境变量配置

nnU-Net v2 依赖三个环境变量来管理数据路径:

代码语言:bash
复制
# 在 ~/.bashrc 或 ~/.zshrc 中添加
export nnUNet_raw="/path/to/nnUNet_raw"           # 原始数据
export nnUNet_preprocessed="/path/to/nnUNet_preprocessed"  # 预处理数据
export nnUNet_results="/path/to/nnUNet_results"   # 训练结果

# 使配置生效
source ~/.bashrc  # 或 source ~/.zshrc

验证配置

代码语言:bash
复制
echo $nnUNet_raw
echo $nnUNet_preprocessed
echo $nnUNet_results

5. 实验规划与数据预处理

5.1 运行预处理命令

nnU-Net 的预处理阶段包括三个步骤:指纹提取、实验规划和数据预处理。可以一次性执行:

代码语言:bash
复制
# 标准预处理
nnUNetv2_plan_and_preprocess -d 100 --verify_dataset_integrity

# 使用推荐的 ResEnc L 配置(需要 24GB 显存)
nnUNetv2_plan_and_preprocess -d 100 --verify_dataset_integrity -pl nnUNetPlannerResEncL

参数说明

  • -d 100:指定数据集 ID
  • --verify_dataset_integrity:验证数据集完整性(首次运行建议开启)
  • -pl:指定实验规划器

5.2 预处理输出

预处理完成后,nnUNet_preprocessed 目录下会生成:

代码语言:txt
复制
nnUNet_preprocessed/
└── Dataset100_Ki67/
    ├── dataset_fingerprint.json    # 数据集指纹
    ├── nnUNetPlans.json            # 实验规划
    └── nnUNetPlans_2d/             # 2D 预处理数据
        ├── Ki67_00000.npz
        ├── Ki67_00001.npz
        └── ...

5.3 理解数据集指纹

dataset_fingerprint.json 包含了 nnU-Net 自动提取的数据特征:

代码语言:json
复制
{
    "median_image_size_in_voxels": [640.0, 640.0],
    "spacing": [1.0, 1.0],
    "foreground_intensity_properties_per_channel": {
        "0": {"mean": 148.76, "std": 55.54, ...},  // R 通道
        "1": {"mean": 129.78, "std": 60.64, ...},  // G 通道
        "2": {"mean": 122.70, "std": 63.22, ...}   // B 通道
    }
}

6. 模型训练

6.1 训练命令

对于 2D 病理图像,使用 2D U-Net 配置:

代码语言:bash
复制
# 标准训练(5 折交叉验证中的某一折)
nnUNetv2_train 100 2d 0  # 训练第 0 折

# 使用全部数据训练(无交叉验证)
nnUNetv2_train 100 2d all

# 使用 ResEnc L 配置
nnUNetv2_train 100 2d all -p nnUNetResEncUNetLPlans

# 指定 GPU
CUDA_VISIBLE_DEVICES=0 nnUNetv2_train 100 2d all

6.2 训练参数详解

参数

说明

100

数据集 ID

2d

网络配置(2D U-Net)

all / 0-4

训练折数

-p

指定 plans 文件

-tr

指定 Trainer 类

--c

从检查点继续训练

--npz

保存 softmax 预测

-device

指定设备 (cuda/cpu/mps)

6.3 训练过程监控

训练日志会实时输出到终端,并保存在结果目录中:

代码语言:txt
复制
训练配置信息:
Configuration name: 2d
 - batch_size: 8
 - patch_size: [640, 640]
 - normalization_schemes: ['ZScoreNormalization', 'ZScoreNormalization', 'ZScoreNormalization']
 - architecture: PlainConvUNet
   - n_stages: 8
   - features_per_stage: [32, 64, 128, 256, 512, 512, 512, 512]

Epoch 0
Current learning rate: 0.01
train_loss: 0.0578
val_loss: -0.0661
Pseudo dice: [0.2639, 0.0]
Epoch time: 91.96 s

6.4 训练时长估计

对于 Ki-67 数据集(约 2500 张 640×640 图像):

配置

显存需求

单折训练时长 (A100)

标准 2D U-Net

~8 GB

~9 小时

ResEnc M

~10 GB

~12 小时

ResEnc L

~24 GB

~35 小时

6.5 训练结果目录

代码语言:txt
复制
nnUNet_results/
└── Dataset100_Ki67/
    └── nnUNetTrainer__nnUNetPlans__2d/
        └── fold_all/
            ├── checkpoint_best.pth      # 最佳模型
            ├── checkpoint_final.pth     # 最终模型
            ├── training_log_*.txt       # 训练日志
            ├── progress.png             # 训练曲线
            ├── debug.json               # 调试信息
            └── validation/              # 验证结果

7. 模型推理

7.1 单张图像推理

代码语言:bash
复制
nnUNetv2_predict \
    -i /path/to/input_images/ \
    -o /path/to/output/ \
    -d 100 \
    -c 2d \
    -f all

7.2 参数说明

参数

说明

-i

输入图像目录

-o

输出目录

-d

数据集 ID

-c

配置(2d/3d_fullres 等)

-f

使用的折(all 或 0-4)

-p

plans 标识符

--save_probabilities

保存概率图

7.3 推理结果

推理输出为分割掩码图像,像素值含义:

  • 0:背景
  • 1:阳性细胞
  • 2:阴性细胞

8. 结果评估

8.1 评估指标

nnU-Net 使用 Dice 系数作为主要评估指标:

$$\text{Dice} = \frac{2 |A \cap B|}{|A| + |B|}$$

其中 $A$ 为预测结果,$B$ 为真值标注。

8.2 运行评估

如果训练时使用了 5 折交叉验证,可以汇总结果:

代码语言:bash
复制
nnUNetv2_find_best_configuration 100 -c 2d

8.3 评估输出示例

代码语言:txt
复制
Dataset100_Ki67:
  2d:
    Dice (positive_cell): 0.7523 ± 0.0821
    Dice (negative_cell): 0.6892 ± 0.0934
    Mean Dice: 0.7207 ± 0.0878
推理结果
推理结果

9. 高级配置与优化

9.1 使用残差编码器 U-Net(推荐)

nnU-Net 最新版本推荐使用 ResEnc 预设,可获得更好的性能:

代码语言:bash
复制
# 预处理时指定规划器
nnUNetv2_plan_and_preprocess -d 100 -pl nnUNetPlannerResEncL

# 训练时指定 plans
nnUNetv2_train 100 2d all -p nnUNetResEncUNetLPlans

9.2 多 GPU 训练

代码语言:bash
复制
# 使用 DDP 多卡训练
nnUNetv2_train 100 2d all -num_gpus 4

9.3 自定义训练参数

通过继承 nnUNetTrainer 类可以自定义训练行为,如调整学习率、数据增强策略等。

9.4 模型集成

使用多折模型进行集成推理:

代码语言:bash
复制
# 训练所有 5 折,或者使用all也行
for fold in 0 1 2 3 4; do
    nnUNetv2_train 100 2d $fold
done

# 集成推理
nnUNetv2_predict -i INPUT -o OUTPUT -d 100 -c 2d -f 0 1 2 3 4

10. 总结

本文详细介绍了使用 nnU-Net 框架进行 IHC Ki-67 细胞分割与分类的完整流程:

  1. 数据准备:将 BCData 点标注转换为 nnU-Net 所需的分割掩码格式
  2. 环境配置:设置必要的环境变量
  3. 数据预处理:利用 nnU-Net 的自配置能力自动完成
  4. 模型训练:支持多种配置和训练策略
  5. 推理评估:获得最终的细胞分割结果

nnU-Net 的自配置能力大大简化了医学图像分割任务的开发流程,使研究者能够专注于数据准备和结果分析,而无需花费大量时间进行超参数调优。


附录:完整命令参考

代码语言:bash
复制
# 1. 数据转换
python3 convert_bcdata.py \
    --input ./BCData \
    --output ./nnUNet_raw \
    --dataset_id 100 \
    --dataset_name Ki67 \
    --cell_radius 3 \
    --mode rgb

# 2. 设置环境变量
export nnUNet_raw="./nnUNet_raw"
export nnUNet_preprocessed="./nnUNet_preprocessed"
export nnUNet_results="./nnUNet_results"

# 3. 数据预处理(推荐 ResEnc L 配置)
nnUNetv2_plan_and_preprocess -d 100 --verify_dataset_integrity -pl nnUNetPlannerResEncL

# 4. 模型训练
nnUNetv2_train 100 2d all -p nnUNetResEncUNetLPlans

# 5. 模型推理
nnUNetv2_predict \
    -i ./test_images/ \
    -o ./predictions/ \
    -d 100 \
    -c 2d \
    -f all \
    -p nnUNetResEncUNetLPlans
代码语言:python
复制
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
BCData 转 nnUNetv2 数据格式转换脚本

将BCData中的细胞点标注数据转换为nnU-Net可用的分割mask格式
支持输出灰度图(Grey)或RGB图像格式

数据格式:
- 输入: IHC Ki-67图片 (640x640 RGB) + 细胞点坐标标注(h5格式)
- 输出: nnU-Net格式的分割数据集
    - imagesTr: 训练图片 ({dataset_name}_{case_id:05d}_0000.png)
    - labelsTr: 训练标签 ({dataset_name}_{case_id:05d}.png)
    - imagesTs: 测试图片 (可选)
    - dataset.json: 数据集描述文件

用法:
    # 输出RGB格式(默认)
    python3 convert_bcdata.py --input ./BCData --output ./nnUNet_raw
    
    # 输出灰度格式
    python3 convert_bcdata.py --input ./BCData --output ./nnUNet_raw --mode grey
    
    # 包含测试集
    python3 convert_bcdata.py --input ./BCData --output ./nnUNet_raw --include_test
"""

import os
import sys
import json
import argparse
from pathlib import Path
from typing import Tuple, Optional

import h5py
import numpy as np
from PIL import Image
import cv2
from tqdm import tqdm


class BCDataConverter:
    """将BCData格式的细胞点标注数据转换为nnU-Net格式"""
    
    def __init__(self, 
                 source_dir: str = "./BCData",
                 output_dir: str = "./nnUNet_raw",
                 dataset_id: int = 100,
                 dataset_name: str = "Ki67",
                 cell_radius: int = 3,
                 mode: str = "rgb",
                 include_test: bool = False):
        """
        初始化数据转换器
        
        Args:
            source_dir: 源数据目录(BCData格式)
            output_dir: 输出目录(nnUNet_raw)
            dataset_id: nnU-Net数据集ID (通常使用001-999)
            dataset_name: 数据集名称
            cell_radius: 细胞半径(用于在点位置绘制圆形mask)
            mode: 输出模式 ('rgb' 或 'grey')
            include_test: 是否处理测试集
        """
        self.source_dir = Path(source_dir)
        self.output_dir = Path(output_dir)
        self.dataset_id = dataset_id
        self.dataset_name = dataset_name
        self.cell_radius = cell_radius
        self.mode = mode.lower()
        self.include_test = include_test
        
        # 验证模式参数
        if self.mode not in ['rgb', 'grey', 'gray']:
            raise ValueError(f"无效的模式: {mode},请使用 'rgb' 或 'grey'")
        if self.mode == 'gray':
            self.mode = 'grey'
        
        # 数据集文件夹名称
        self.dataset_folder_name = f"Dataset{dataset_id:03d}_{dataset_name}"
        self.nnunet_dataset_dir = self.output_dir / self.dataset_folder_name
        
    def validate_source_directory(self) -> bool:
        """验证源数据目录结构是否正确"""
        required_dirs = [
            self.source_dir / "images" / "train",
            self.source_dir / "images" / "validation",
            self.source_dir / "annotations" / "train",
            self.source_dir / "annotations" / "validation",
        ]
        
        if self.include_test:
            required_dirs.append(self.source_dir / "images" / "test")
        
        missing_dirs = [d for d in required_dirs if not d.exists()]
        
        if missing_dirs:
            print("错误: 源数据目录结构不完整,缺少以下目录:")
            for d in missing_dirs:
                print(f"  - {d}")
            print("\n期望的目录结构:")
            print("  BCData/")
            print("  ├── images/")
            print("  │   ├── train/")
            print("  │   ├── validation/")
            print("  │   └── test/")
            print("  └── annotations/")
            print("      ├── train/")
            print("      │   ├── positive/")
            print("      │   └── negative/")
            print("      └── validation/")
            print("          ├── positive/")
            print("          └── negative/")
            return False
        
        return True
    
    def create_segmentation_mask(self, 
                                 image_shape: Tuple[int, int],
                                 positive_coords: np.ndarray,
                                 negative_coords: np.ndarray) -> np.ndarray:
        """
        从点坐标创建分割mask
        
        Args:
            image_shape: 图片尺寸 (height, width)
            positive_coords: 阳性细胞坐标 (N, 2) - [x, y]
            negative_coords: 阴性细胞坐标 (M, 2) - [x, y]
            
        Returns:
            mask: 分割mask (height, width)
                  0 = 背景
                  1 = 阳性细胞
                  2 = 阴性细胞
        """
        mask = np.zeros(image_shape, dtype=np.uint8)
        
        # 绘制阴性细胞(先绘制,优先级较低)
        for coord in negative_coords:
            x, y = int(coord[0]), int(coord[1])
            cv2.circle(mask, (x, y), self.cell_radius, 2, -1)
        
        # 绘制阳性细胞(后绘制,优先级较高)
        for coord in positive_coords:
            x, y = int(coord[0]), int(coord[1])
            cv2.circle(mask, (x, y), self.cell_radius, 1, -1)
        
        return mask
    
    def load_annotations(self, image_id: str, split: str) -> Tuple[np.ndarray, np.ndarray]:
        """
        加载阳性和阴性细胞的标注
        
        Args:
            image_id: 图片ID(不含扩展名)
            split: 数据集分割 ('train', 'validation', 'test')
            
        Returns:
            positive_coords: 阳性细胞坐标
            negative_coords: 阴性细胞坐标
        """
        positive_path = self.source_dir / "annotations" / split / "positive" / f"{image_id}.h5"
        negative_path = self.source_dir / "annotations" / split / "negative" / f"{image_id}.h5"
        
        # 加载阳性细胞坐标
        positive_coords = np.array([]).reshape(0, 2)
        if positive_path.exists():
            with h5py.File(positive_path, 'r') as f:
                if 'coordinates' in f:
                    coords = f['coordinates'][:]
                    if len(coords) > 0:
                        positive_coords = coords
        
        # 加载阴性细胞坐标
        negative_coords = np.array([]).reshape(0, 2)
        if negative_path.exists():
            with h5py.File(negative_path, 'r') as f:
                if 'coordinates' in f:
                    coords = f['coordinates'][:]
                    if len(coords) > 0:
                        negative_coords = coords
        
        return positive_coords, negative_coords
    
    def convert_image_to_grayscale(self, img_array: np.ndarray) -> np.ndarray:
        """
        将RGB图片转换为灰度图
        
        Args:
            img_array: 输入图片数组 (H, W, 3)
            
        Returns:
            灰度图数组 (H, W)
        """
        if len(img_array.shape) == 3:
            # RGB转灰度 (使用标准权重)
            return np.dot(img_array[..., :3], [0.299, 0.587, 0.114]).astype(np.uint8)
        return img_array
    
    def process_image(self, img_array: np.ndarray) -> np.ndarray:
        """
        根据模式处理图像
        
        Args:
            img_array: 输入图片数组
            
        Returns:
            处理后的图片数组
        """
        if self.mode == 'grey':
            return self.convert_image_to_grayscale(img_array)
        return img_array
    
    def convert_training_case(self, image_id: str, split: str, case_idx: int,
                              images_dir: Path, labels_dir: Path) -> dict:
        """
        转换单个训练/验证样本
        
        Args:
            image_id: 图片ID
            split: 数据分割
            case_idx: 样本索引
            images_dir: 图片输出目录
            labels_dir: 标签输出目录
            
        Returns:
            统计信息字典
        """
        case_name = f"{self.dataset_name}_{case_idx:05d}"
        
        # 加载图片
        img_path = self.source_dir / "images" / split / f"{image_id}.png"
        img = Image.open(img_path)
        img_array = np.array(img)
        
        # 根据模式处理图像
        img_processed = self.process_image(img_array)
        
        # 保存图片(nnU-Net格式:caseName_0000.png)
        img_out = Image.fromarray(img_processed)
        img_out.save(images_dir / f"{case_name}_0000.png")
        
        # 加载标注
        positive_coords, negative_coords = self.load_annotations(image_id, split)
        
        # 创建分割mask
        mask = self.create_segmentation_mask(
            img_array.shape[:2],
            positive_coords,
            negative_coords
        )
        
        # 保存mask
        mask_img = Image.fromarray(mask)
        mask_img.save(labels_dir / f"{case_name}.png")
        
        return {
            'positive_count': len(positive_coords),
            'negative_count': len(negative_coords)
        }
    
    def convert_test_case(self, image_id: str, case_idx: int, images_dir: Path):
        """
        转换单个测试样本(测试集只有图片,没有标签)
        
        Args:
            image_id: 图片ID
            case_idx: 样本索引
            images_dir: 图片输出目录
        """
        case_name = f"{self.dataset_name}_test_{case_idx:05d}"
        
        # 加载图片
        img_path = self.source_dir / "images" / "test" / f"{image_id}.png"
        img = Image.open(img_path)
        img_array = np.array(img)
        
        # 根据模式处理图像
        img_processed = self.process_image(img_array)
        
        # 保存图片
        img_out = Image.fromarray(img_processed)
        img_out.save(images_dir / f"{case_name}_0000.png")
    
    def generate_dataset_json(self, num_training: int):
        """
        生成nnU-Net的dataset.json文件
        
        Args:
            num_training: 训练样本数量
        """
        # 根据模式设置通道名称
        if self.mode == 'grey':
            channel_names = {"0": "Grayscale"}
        else:
            channel_names = {"0": "R", "1": "G", "2": "B"}
        
        dataset_json = {
            "channel_names": channel_names,
            "labels": {
                "background": 0,
                "positive_cell": 1,
                "negative_cell": 2
            },
            "numTraining": num_training,
            "file_ending": ".png",
            "name": self.dataset_name,
            "description": f"IHC Ki-67 cell segmentation ({self.mode.upper()} mode)",
            "reference": "BCData dataset for cell detection",
            "licence": "",
            "release": "1.0"
        }
        
        json_path = self.nnunet_dataset_dir / "dataset.json"
        with open(json_path, 'w', encoding='utf-8') as f:
            json.dump(dataset_json, f, indent=4, ensure_ascii=False)
        
        print(f"  dataset.json 已保存到: {json_path}")
    
    def convert(self):
        """执行完整的数据转换流程"""
        print(f"\n{'='*60}")
        print(f"BCData → nnUNetv2 数据转换")
        print(f"{'='*60}")
        print(f"\n配置信息:")
        print(f"  源数据目录: {self.source_dir}")
        print(f"  输出目录: {self.nnunet_dataset_dir}")
        print(f"  数据集ID: {self.dataset_id}")
        print(f"  数据集名称: {self.dataset_name}")
        print(f"  细胞半径: {self.cell_radius}")
        print(f"  输出模式: {self.mode.upper()}")
        print(f"  包含测试集: {self.include_test}")
        print()
        
        # 验证源目录
        if not self.validate_source_directory():
            sys.exit(1)
        
        # 创建nnU-Net目录结构
        imagesTr = self.nnunet_dataset_dir / "imagesTr"
        labelsTr = self.nnunet_dataset_dir / "labelsTr"
        
        for folder in [imagesTr, labelsTr]:
            folder.mkdir(parents=True, exist_ok=True)
        
        if self.include_test:
            imagesTs = self.nnunet_dataset_dir / "imagesTs"
            imagesTs.mkdir(parents=True, exist_ok=True)
        
        # 统计信息
        total_positive = 0
        total_negative = 0
        case_idx = 0
        
        # 处理训练集
        print("处理训练集...")
        train_images = sorted((self.source_dir / "images" / "train").glob("*.png"))
        for img_path in tqdm(train_images, desc="训练集"):
            image_id = img_path.stem
            stats = self.convert_training_case(image_id, "train", case_idx, imagesTr, labelsTr)
            total_positive += stats['positive_count']
            total_negative += stats['negative_count']
            case_idx += 1
        
        train_count = len(train_images)
        print(f"  训练集转换完成: {train_count} 个样本")
        
        # 处理验证集(作为训练集的一部分)
        print("\n处理验证集...")
        val_images = sorted((self.source_dir / "images" / "validation").glob("*.png"))
        for img_path in tqdm(val_images, desc="验证集"):
            image_id = img_path.stem
            stats = self.convert_training_case(image_id, "validation", case_idx, imagesTr, labelsTr)
            total_positive += stats['positive_count']
            total_negative += stats['negative_count']
            case_idx += 1
        
        val_count = len(val_images)
        print(f"  验证集转换完成: {val_count} 个样本")
        
        # 处理测试集(如果需要)
        test_count = 0
        if self.include_test:
            print("\n处理测试集...")
            test_images_dir = self.source_dir / "images" / "test"
            if test_images_dir.exists():
                test_images = sorted(test_images_dir.glob("*.png"))
                for idx, img_path in enumerate(tqdm(test_images, desc="测试集")):
                    image_id = img_path.stem
                    self.convert_test_case(image_id, idx, imagesTs)
                test_count = len(test_images)
                print(f"  测试集转换完成: {test_count} 个样本")
        
        # 生成dataset.json
        print("\n生成 dataset.json...")
        num_training = train_count + val_count
        self.generate_dataset_json(num_training)
        
        # 打印转换结果摘要
        print(f"\n{'='*60}")
        print("转换完成!")
        print(f"{'='*60}")
        print(f"\n数据集统计:")
        print(f"  训练样本数: {num_training} (训练集 {train_count} + 验证集 {val_count})")
        if self.include_test:
            print(f"  测试样本数: {test_count}")
        print(f"  阳性细胞标注总数: {total_positive}")
        print(f"  阴性细胞标注总数: {total_negative}")
        print(f"  输出模式: {self.mode.upper()}")
        print(f"\n输出目录结构:")
        print(f"  {self.nnunet_dataset_dir}/")
        print(f"  ├── imagesTr/      ({num_training} 个文件)")
        print(f"  ├── labelsTr/      ({num_training} 个文件)")
        if self.include_test:
            print(f"  ├── imagesTs/      ({test_count} 个文件)")
        print(f"  └── dataset.json")
        print(f"\n下一步:")
        print(f"  1. 设置环境变量:")
        print(f"     export nnUNet_raw=\"{self.output_dir}\"")
        print(f"     export nnUNet_preprocessed=\"./nnUNet_preprocessed\"")
        print(f"     export nnUNet_results=\"./nnUNet_results\"")
        print(f"  2. 运行预处理和训练:")
        print(f"     nnUNetv2_plan_and_preprocess -d {self.dataset_id} --verify_dataset_integrity -pl nnUNetPlannerResEncL")
        print(f"     nnUNetv2_train {self.dataset_id} 2d all -p nnUNetResEncUNetLPlans")
        print()


def main():
    parser = argparse.ArgumentParser(
        description='BCData 转 nnUNetv2 数据格式转换脚本(支持RGB和灰度模式)',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
示例:
  # 基本用法(输出RGB格式)
  python3 convert_bcdata.py --input ./BCData --output ./nnUNet_raw
  
  # 输出灰度格式
  python3 convert_bcdata.py --input ./BCData --mode grey
  
  # 自定义数据集ID和名称
  python3 convert_bcdata.py --dataset_id 100 --dataset_name MyCellDataset
  
  # 调整细胞半径(影响生成的mask大小)
  python3 convert_bcdata.py --cell_radius 5
  
  # 包含测试集
  python3 convert_bcdata.py --include_test
  
  # 完整示例
  python3 convert_bcdata.py --input ./BCData --output ./nnUNet_raw \\
      --dataset_id 100 --dataset_name Ki67 --mode rgb \\
      --cell_radius 3 --include_test
        """
    )
    
    parser.add_argument('--input', type=str, default='./BCData',
                        help='源数据目录 (默认: ./BCData)')
    parser.add_argument('--output', type=str, default='./nnUNet_raw',
                        help='输出目录 (默认: ./nnUNet_raw)')
    parser.add_argument('--dataset_id', type=int, default=100,
                        help='nnU-Net数据集ID,范围001-999 (默认: 100)')
    parser.add_argument('--dataset_name', type=str, default='Ki67',
                        help='数据集名称 (默认: Ki67)')
    parser.add_argument('--cell_radius', type=int, default=3,
                        help='细胞半径,用于绘制分割mask (默认: 3像素)')
    parser.add_argument('--mode', type=str, default='rgb', choices=['rgb', 'grey', 'gray'],
                        help='输出模式: rgb(保持彩色) 或 grey(转为灰度) (默认: rgb)')
    parser.add_argument('--include_test', action='store_true',
                        help='是否处理测试集(默认不处理)')
    
    args = parser.parse_args()
    
    # 创建转换器并执行转换
    converter = BCDataConverter(
        source_dir=args.input,
        output_dir=args.output,
        dataset_id=args.dataset_id,
        dataset_name=args.dataset_name,
        cell_radius=args.cell_radius,
        mode=args.mode,
        include_test=args.include_test
    )
    
    converter.convert()


if __name__ == "__main__":
    main()

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 摘要
  • 1. 背景介绍
    • 1.1 Ki-67 增殖指数的临床意义
    • 1.2 nnU-Net 框架简介
  • 2. 实验环境与数据集
    • 2.1 硬件与软件环境
    • 2.2 BCData 数据集
    • 2.3 安装 nnU-Net
  • 3. 数据预处理与格式转换
    • 3.1 理解 nnU-Net v2 数据格式
    • 3.2 数据转换脚本
      • 3.2.1 从点标注生成分割掩码
      • 3.2.2 生成 dataset.json
      • 3.2.3 执行转换
  • 4. 环境变量配置
  • 5. 实验规划与数据预处理
    • 5.1 运行预处理命令
    • 5.2 预处理输出
    • 5.3 理解数据集指纹
  • 6. 模型训练
    • 6.1 训练命令
    • 6.2 训练参数详解
    • 6.3 训练过程监控
    • 6.4 训练时长估计
    • 6.5 训练结果目录
  • 7. 模型推理
    • 7.1 单张图像推理
    • 7.2 参数说明
    • 7.3 推理结果
  • 8. 结果评估
    • 8.1 评估指标
    • 8.2 运行评估
    • 8.3 评估输出示例
  • 9. 高级配置与优化
    • 9.1 使用残差编码器 U-Net(推荐)
    • 9.2 多 GPU 训练
    • 9.3 自定义训练参数
    • 9.4 模型集成
  • 10. 总结
  • 附录:完整命令参考
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档