
本文详细介绍了使用 nnU-Net 框架对免疫组化(IHC)Ki-67 染色病理图像进行细胞分割与分类的完整流程。从数据准备、格式转换、模型训练到推理评估,涵盖了实际项目中的各个环节。本实验使用 BCData 数据集,目标是自动识别和分类病理图像中的 Ki-67 阳性和阴性肿瘤细胞。
关键词:nnU-Net、Ki-67、免疫组化、细胞分割、深度学习、病理图像分析
Ki-67 是一种核蛋白,仅在细胞增殖周期(G1、S、G2 和 M 期)中表达,而在静止期(G0 期)不表达。通过免疫组化染色检测 Ki-67 阳性细胞的比例(Ki-67 增殖指数),可以评估肿瘤的增殖活性,是乳腺癌等多种肿瘤预后评估和治疗决策的重要指标。
传统的 Ki-67 计数依赖病理医师人工判读,存在以下问题:
因此,基于深度学习的自动化细胞分割与分类方法具有重要的临床应用价值。
nnU-Net(no-new-Net)是由德国癌症研究中心(DKFZ)开发的自动化医学图像分割框架。其核心优势在于:
引用: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.
操作系统: Ubuntu 22.04 / macOS
GPU: NVIDIA GPU (推荐 24GB 显存以上)
Python: 3.10.19
PyTorch: 2.4.1
nnU-Net: v2.6.2本实验使用 BCData 数据集,该数据集专门用于乳腺癌 Ki-67 细胞检测任务。
数据集特征:
数据集目录结构:
BCData/
├── images/
│ ├── train/ # 训练集图像
│ ├── validation/ # 验证集图像
│ └── test/ # 测试集图像
└── annotations/
├── train/
│ ├── positive/ # 阳性细胞坐标 (.h5)
│ └── negative/ # 阴性细胞坐标 (.h5)
└── validation/
├── positive/
└── negative/
# 创建虚拟环境
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"nnU-Net v2 要求数据按照特定格式组织。对于本任务,需要将 BCData 的点标注转换为分割掩码(Segmentation Mask)。
nnU-Net 数据集结构:
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}.{扩展名}我们开发了 convert_bcdata.py 脚本(见附录代码),用于将 BCData 格式转换为 nnU-Net 格式。核心转换逻辑包括:
由于原始数据仅提供细胞中心点坐标,需要将其转换为分割掩码:
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 maskdataset_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"
}# 基本用法(输出 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参数说明:
参数 | 默认值 | 说明 |
|---|---|---|
|
| 源数据目录 |
|
| 输出目录 |
| 100 | nnU-Net 数据集 ID (001-999) |
| Ki67 | 数据集名称 |
| 3 | 细胞标注半径(像素) |
| rgb | 输出模式:rgb(彩色)或 grey(灰度) |
| False | 是否处理测试集 |
nnU-Net v2 依赖三个环境变量来管理数据路径:
# 在 ~/.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验证配置:
echo $nnUNet_raw
echo $nnUNet_preprocessed
echo $nnUNet_resultsnnU-Net 的预处理阶段包括三个步骤:指纹提取、实验规划和数据预处理。可以一次性执行:
# 标准预处理
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:指定实验规划器预处理完成后,nnUNet_preprocessed 目录下会生成:
nnUNet_preprocessed/
└── Dataset100_Ki67/
├── dataset_fingerprint.json # 数据集指纹
├── nnUNetPlans.json # 实验规划
└── nnUNetPlans_2d/ # 2D 预处理数据
├── Ki67_00000.npz
├── Ki67_00001.npz
└── ...dataset_fingerprint.json 包含了 nnU-Net 自动提取的数据特征:
{
"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 通道
}
}对于 2D 病理图像,使用 2D U-Net 配置:
# 标准训练(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参数 | 说明 |
|---|---|
| 数据集 ID |
| 网络配置(2D U-Net) |
| 训练折数 |
| 指定 plans 文件 |
| 指定 Trainer 类 |
| 从检查点继续训练 |
| 保存 softmax 预测 |
| 指定设备 (cuda/cpu/mps) |
训练日志会实时输出到终端,并保存在结果目录中:
训练配置信息:
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对于 Ki-67 数据集(约 2500 张 640×640 图像):
配置 | 显存需求 | 单折训练时长 (A100) |
|---|---|---|
标准 2D U-Net | ~8 GB | ~9 小时 |
ResEnc M | ~10 GB | ~12 小时 |
ResEnc L | ~24 GB | ~35 小时 |
nnUNet_results/
└── Dataset100_Ki67/
└── nnUNetTrainer__nnUNetPlans__2d/
└── fold_all/
├── checkpoint_best.pth # 最佳模型
├── checkpoint_final.pth # 最终模型
├── training_log_*.txt # 训练日志
├── progress.png # 训练曲线
├── debug.json # 调试信息
└── validation/ # 验证结果nnUNetv2_predict \
-i /path/to/input_images/ \
-o /path/to/output/ \
-d 100 \
-c 2d \
-f all参数 | 说明 |
|---|---|
| 输入图像目录 |
| 输出目录 |
| 数据集 ID |
| 配置(2d/3d_fullres 等) |
| 使用的折(all 或 0-4) |
| plans 标识符 |
| 保存概率图 |
推理输出为分割掩码图像,像素值含义:
nnU-Net 使用 Dice 系数作为主要评估指标:
$$\text{Dice} = \frac{2 |A \cap B|}{|A| + |B|}$$
其中 $A$ 为预测结果,$B$ 为真值标注。
如果训练时使用了 5 折交叉验证,可以汇总结果:
nnUNetv2_find_best_configuration 100 -c 2dDataset100_Ki67:
2d:
Dice (positive_cell): 0.7523 ± 0.0821
Dice (negative_cell): 0.6892 ± 0.0934
Mean Dice: 0.7207 ± 0.0878
nnU-Net 最新版本推荐使用 ResEnc 预设,可获得更好的性能:
# 预处理时指定规划器
nnUNetv2_plan_and_preprocess -d 100 -pl nnUNetPlannerResEncL
# 训练时指定 plans
nnUNetv2_train 100 2d all -p nnUNetResEncUNetLPlans# 使用 DDP 多卡训练
nnUNetv2_train 100 2d all -num_gpus 4通过继承 nnUNetTrainer 类可以自定义训练行为,如调整学习率、数据增强策略等。
使用多折模型进行集成推理:
# 训练所有 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本文详细介绍了使用 nnU-Net 框架进行 IHC Ki-67 细胞分割与分类的完整流程:
nnU-Net 的自配置能力大大简化了医学图像分割任务的开发流程,使研究者能够专注于数据准备和结果分析,而无需花费大量时间进行超参数调优。
# 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#!/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 删除。