4058 字
20 分钟
nnUNetv2数据集转换
温馨提示 最后更新于 2025-08-18,距今已过 343 天。部分内容可能已过时
本文nnunetv2实验中BraTS数据集转换成MSD数据集格式的代码, nnunetv1的dataset.json与nnunetv2略有不同
BraTS数据集转换成MSD数据集
BraTS2019:
import osimport shutilimport jsonimport nibabel as nibimport numpy as npfrom collections import OrderedDict
def convert_brats2019_to_nnunet(brats_root, nnunet_raw_data_base): """ 将BraTS2019数据集转换为nnUNet格式,处理标签映射
Args: brats_root: BraTS2019原始数据根目录路径 nnunet_raw_data_base: nnUNet原始数据基础目录路径 """
# 设置路径 task_name = "Dataset001_BraTS2019" task_folder = os.path.join(nnunet_raw_data_base, "nnUNet_raw_data", task_name)
# 创建必要的目录 imagesTr_folder = os.path.join(task_folder, "imagesTr") imagesTs_folder = os.path.join(task_folder, "imagesTs") labelsTr_folder = os.path.join(task_folder, "labelsTr") labelsTs_folder = os.path.join(task_folder, "labelsTs")
for folder in [imagesTr_folder, imagesTs_folder, labelsTr_folder, labelsTs_folder]: os.makedirs(folder, exist_ok=True)
# 获取训练数据和测试数据路径 hgg_folder = os.path.join(brats_root, "HGG") lgg_folder = os.path.join(brats_root, "LGG")
# 收集所有训练病例 train_cases = []
# 处理HGG数据 if os.path.exists(hgg_folder): hgg_cases = [d for d in os.listdir(hgg_folder) if os.path.isdir(os.path.join(hgg_folder, d))] for case in hgg_cases: train_cases.append(("HGG", case))
# 处理LGG数据 if os.path.exists(lgg_folder): lgg_cases = [d for d in os.listdir(lgg_folder) if os.path.isdir(os.path.join(lgg_folder, d))] for case in lgg_cases: train_cases.append(("LGG", case))
print(f"找到 {len(train_cases)} 个训练病例")
# 模态映射 modality_mapping = { 'flair': '0000', 't1ce': '0001', 't1': '0002', 't2': '0003' }
def process_label(label_path, output_path): """ 处理BraTS标签,将原始标签映射为连续的标签 BraTS原始标签: 0(背景), 1(坏死/非增强肿瘤), 2(水肿), 4(增强肿瘤) 映射后标签: 0(背景), 1(坏死/非增强肿瘤), 2(水肿), 3(增强肿瘤) """ # 加载标签数据 label_nii = nib.load(label_path) label_data = label_nii.get_fdata().astype(np.uint8)
# 检查原始标签值 unique_labels = np.unique(label_data) print(f"处理 {os.path.basename(label_path)},原始标签值: {unique_labels}")
# 创建新的标签数组 new_label_data = np.zeros_like(label_data)
# 标签映射: 0->0, 1->1, 2->2, 4->3 new_label_data[label_data == 0] = 0 # 背景 new_label_data[label_data == 1] = 1 # 坏死和非增强肿瘤 new_label_data[label_data == 2] = 2 # 水肿 new_label_data[label_data == 4] = 3 # 增强肿瘤
# 检查映射后的标签值 new_unique_labels = np.unique(new_label_data) print(f"映射后标签值: {new_unique_labels}")
# 保存新的标签文件 new_label_nii = nib.Nifti1Image(new_label_data, label_nii.affine, label_nii.header) nib.save(new_label_nii, output_path)
training_cases = [] test_cases = []
for i, (grade, case_name) in enumerate(train_cases): case_folder = os.path.join(brats_root, grade, case_name)
if not os.path.exists(case_folder): print(f"警告: 病例文件夹不存在 {case_folder}") continue
# 检查所有必需的文件是否存在 required_files = { 'flair': f"{case_name}_flair.nii", 't1ce': f"{case_name}_t1ce.nii", 't1': f"{case_name}_t1.nii", 't2': f"{case_name}_t2.nii", 'seg': f"{case_name}_seg.nii" }
# 检查.nii.gz格式 for key, filename in required_files.items(): if not os.path.exists(os.path.join(case_folder, filename)): # 尝试.nii.gz格式 gz_filename = filename + ".gz" if os.path.exists(os.path.join(case_folder, gz_filename)): required_files[key] = gz_filename else: print(f"警告: 文件 {filename} 或 {gz_filename} 不存在于 {case_folder}") break else: # 所有文件都存在,处理这个病例
# 决定这个病例是用于训练还是测试(这里简单地将前80%用于训练) if i < len(train_cases) * 0.8: # 训练数据 for modality, suffix in modality_mapping.items(): src_file = os.path.join(case_folder, required_files[modality]) dst_file = os.path.join(imagesTr_folder, f"{case_name}_{suffix}.nii.gz")
# 如果源文件不是.gz格式,需要压缩 if not src_file.endswith('.gz'): img = nib.load(src_file) nib.save(img, dst_file) else: shutil.copy2(src_file, dst_file)
# 处理分割标签(重要:进行标签映射) src_seg = os.path.join(case_folder, required_files['seg']) dst_seg = os.path.join(labelsTr_folder, f"{case_name}.nii.gz") process_label(src_seg, dst_seg)
training_cases.append(case_name)
else: # 测试数据 for modality, suffix in modality_mapping.items(): src_file = os.path.join(case_folder, required_files[modality]) dst_file = os.path.join(imagesTs_folder, f"{case_name}_{suffix}.nii.gz")
if not src_file.endswith('.gz'): img = nib.load(src_file) nib.save(img, dst_file) else: shutil.copy2(src_file, dst_file)
# 测试数据的标签(同样进行标签映射) src_seg = os.path.join(case_folder, required_files['seg']) dst_seg = os.path.join(labelsTs_folder, f"{case_name}.nii.gz") process_label(src_seg, dst_seg)
test_cases.append(case_name)
print(f"处理完成: {len(training_cases)} 个训练病例, {len(test_cases)} 个测试病例")
# 创建dataset.json文件 dataset_json = OrderedDict() dataset_json['name'] = "BraTS2019" dataset_json['description'] = "Brain Tumor Segmentation Challenge 2019" dataset_json['tensorImageSize'] = "4D" dataset_json['reference'] = "https://www.med.upenn.edu/cbica/brats2019/" dataset_json['licence'] = "see BraTS2019 website" dataset_json['release'] = "1.0"
# 模态信息 dataset_json['channel_names'] = { "0": "FLAIR", "1": "T1ce", "2": "T1", "3": "T2" }
# 标签信息 - 修正为包含所有必要的标签 dataset_json['labels'] = { "background": 0, "necrotic/non-enhancing tumor": 1, "edema": 2, "enhancing tumor": 3 } dataset_json['file_ending'] = ".nii.gz" #按需修改 .nii或.nii.gz # 训练和测试数据列表 dataset_json['numTraining'] = len(training_cases) dataset_json['numTest'] = len(test_cases)
dataset_json['training'] = [] for case in training_cases: case_dict = { "image": f"./imagesTr/{case}.nii.gz", "label": f"./labelsTr/{case}.nii.gz" } dataset_json['training'].append(case_dict)
dataset_json['test'] = [] for case in test_cases: dataset_json['test'].append(f"./imagesTs/{case}.nii.gz")
# 保存dataset.json json_file_path = os.path.join(task_folder, "dataset.json") with open(json_file_path, 'w') as f: json.dump(dataset_json, f, indent=4)
print(f"dataset.json 已保存到: {json_file_path}") print("数据转换完成!标签已正确映射:0(背景) -> 0, 1(坏死) -> 1, 2(水肿) -> 2, 4(增强肿瘤) -> 3")
return task_folder
# 使用示例if __name__ == "__main__": # 设置路径 brats_root = "/root/autodl-tmp/nnUNet_raw_data_base/BraTS2019" # 您的BraTS2019数据根目录 nnunet_raw_data_base = "/root/autodl-tmp/nnUNet_raw_data_base" # nnUNet原始数据基础目录
# 执行转换 convert_brats2019_to_nnunet(brats_root, nnunet_raw_data_base)BraTS2023
import osimport jsonimport shutilimport argparseimport threadingfrom collections import OrderedDictfrom concurrent.futures import ThreadPoolExecutor, as_completedfrom datetime import datetime
import numpy as npimport nibabel as nib
try: from tqdm import tqdmexcept ImportError: # 没装 tqdm 也能跑 tqdm = None
# BraTS2023 模态命名 -> nnU-Net 通道号MODALITY_MAPPING = OrderedDict([ ('t1n', '0000'), # T1 native ('t1c', '0001'), # T1 contrast enhanced ('t2f', '0002'), # T2 FLAIR ('t2w', '0003'), # T2 weighted])
_print_lock = threading.Lock()
def log(msg): """线程安全打印。""" with _print_lock: print(msg, flush=True)
def check_file_validity(file_path): """检查文件是否存在、非空且 NIfTI 头可读(只读头,很快)。""" try: if not os.path.exists(file_path): return False, "文件不存在" if os.path.getsize(file_path) == 0: return False, "文件为空" nib.load(file_path) # 只解析 header,不读数据 return True, "文件有效" except Exception as e: return False, f"文件无效: {str(e)}"
def resolve_required_files(case_folder, base_name): """定位一个病例的 5 个文件(4 模态 + seg),同时支持 .nii / .nii.gz。""" files, errors = {}, [] for key in list(MODALITY_MAPPING.keys()) + ['seg']: fname = f"{base_name}-{key}.nii" plain = os.path.join(case_folder, fname) gz = plain + ".gz"
if os.path.exists(gz): chosen = gz elif os.path.exists(plain): chosen = plain else: errors.append(f"文件缺失 - 病例: {base_name}, 文件: {fname} 或 {fname}.gz") continue
ok, msg = check_file_validity(chosen) if not ok: errors.append(f"文件无效 - 病例: {base_name}, {os.path.basename(chosen)}: {msg}") continue files[key] = chosen return files, errors
def copy_to_gz(src, dst, overwrite=False): """把源文件落到目标 .nii.gz;非 .gz 源用 nibabel 重存为 .gz。写临时文件再原子改名。""" if not overwrite and os.path.exists(dst) and os.path.getsize(dst) > 0: return False # 已存在,跳过(断点续跑)
tmp = dst + ".tmp.nii.gz" try: if src.endswith(".gz"): shutil.copy2(src, tmp) else: nib.save(nib.load(src), tmp) os.replace(tmp, dst) finally: if os.path.exists(tmp): try: os.remove(tmp) except OSError: pass return True
def process_case(case_name, brats_root, img_dir, lbl_dir, split, verify_labels=False, overwrite=False): """单个病例的完整处理,纯函数式:所有错误/日志放进返回值,不碰全局状态。""" result = { "case": case_name, "split": split, "ok": False, "errors": [], "label_values": None, }
case_folder = os.path.join(brats_root, case_name) if not os.path.isdir(case_folder): result["errors"].append(f"病例文件夹不存在: {case_folder}") return result
files, errors = resolve_required_files(case_folder, case_name) if errors: result["errors"].extend(errors) return result
try: for modality, suffix in MODALITY_MAPPING.items(): copy_to_gz(files[modality], os.path.join(img_dir, f"{case_name}_{suffix}.nii.gz"), overwrite)
dst_seg = os.path.join(lbl_dir, f"{case_name}.nii.gz") copy_to_gz(files['seg'], dst_seg, overwrite)
if verify_labels: data = np.asanyarray(nib.load(dst_seg).dataobj) result["label_values"] = sorted(int(v) for v in np.unique(data))
result["ok"] = True except Exception as e: result["errors"].append(f"处理失败 - 病例: {case_name}, 错误: {str(e)}")
return result
def convert_brats2023_to_nnunet(brats_root, nnunet_raw_data_base, task_name="Dataset001_BraTS2023", workers=8, train_ratio=0.8, verify_labels=False, overwrite=False): task_folder = os.path.join(nnunet_raw_data_base, task_name) imagesTr = os.path.join(task_folder, "imagesTr") imagesTs = os.path.join(task_folder, "imagesTs") labelsTr = os.path.join(task_folder, "labelsTr") labelsTs = os.path.join(task_folder, "labelsTs") for folder in (imagesTr, imagesTs, labelsTr, labelsTs): os.makedirs(folder, exist_ok=True)
# 收集病例并排序,保证划分可复现 all_cases = sorted( item for item in os.listdir(brats_root) if item.startswith('BraTS-GLI-') and os.path.isdir(os.path.join(brats_root, item)) ) print(f"找到 {len(all_cases)} 个病例,使用 {workers} 个线程处理") if not all_cases: print("警告: 没有找到任何病例") return None
# ---- 先确定划分,再并行 ---- n_train = int(len(all_cases) * train_ratio) jobs = [] for i, case_name in enumerate(all_cases): if i < n_train: jobs.append((case_name, "train", imagesTr, labelsTr)) else: jobs.append((case_name, "test", imagesTs, labelsTs))
results = [] t0 = datetime.now() with ThreadPoolExecutor(max_workers=workers) as executor: futures = { executor.submit(process_case, case_name, brats_root, img_dir, lbl_dir, split, verify_labels, overwrite): case_name for case_name, split, img_dir, lbl_dir in jobs }
iterator = as_completed(futures) if tqdm is not None: iterator = tqdm(iterator, total=len(futures), desc="转换中", ncols=80)
done = 0 for future in iterator: case_name = futures[future] try: res = future.result() except Exception as e: # 兜底,理论上 process_case 已经吞掉异常 res = {"case": case_name, "split": "unknown", "ok": False, "errors": [f"线程异常 - 病例: {case_name}, 错误: {str(e)}"], "label_values": None} results.append(res)
done += 1 if tqdm is None and done % 50 == 0: log(f"进度: {done}/{len(futures)}") if not res["ok"]: log(f"[跳过] {case_name}: {res['errors'][0] if res['errors'] else '未知原因'}")
elapsed = (datetime.now() - t0).total_seconds()
# ---- 主线程统一汇总 ---- results.sort(key=lambda r: r["case"]) training_cases = [r["case"] for r in results if r["ok"] and r["split"] == "train"] test_cases = [r["case"] for r in results if r["ok"] and r["split"] == "test"] skipped_cases = [r["case"] for r in results if not r["ok"]] errors = [e for r in results for e in r["errors"]]
if verify_labels: seen = sorted({v for r in results if r["label_values"] for v in r["label_values"]}) print(f"数据集中出现的标签值: {seen}")
print(f"\n处理完成: {len(training_cases)} 个训练病例, {len(test_cases)} 个测试病例") print(f"跳过的病例数量: {len(skipped_cases)},耗时 {elapsed:.1f} 秒")
# ---- 错误日志 ---- error_file_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "error.txt") with open(error_file_path, 'w', encoding='utf-8') as f: f.write("BraTS2023数据转换错误报告\n") f.write(f"生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n") f.write("=" * 80 + "\n\n") f.write(f"总计处理: {len(all_cases)} 个病例\n") f.write(f"成功处理: {len(training_cases) + len(test_cases)} 个病例\n") f.write(f"跳过病例: {len(skipped_cases)} 个病例\n") f.write(f"错误数量: {len(errors)} 个错误\n") f.write(f"总耗时: {elapsed:.1f} 秒 (线程数 {workers})\n\n")
if skipped_cases: f.write("跳过的病例列表:\n") for case in skipped_cases: f.write(f" - {case}\n") f.write("\n")
if errors: f.write("详细错误信息:\n") for i, error in enumerate(errors, 1): f.write(f"{i}. {error}\n") else: f.write("没有发现错误。\n") print(f"错误日志已保存到: {error_file_path}")
if not (training_cases or test_cases): print("警告: 没有成功处理任何病例,未生成dataset.json文件") return None
# ---- dataset.json ---- dataset_json = OrderedDict() dataset_json['name'] = "BraTS2023" dataset_json['description'] = "Brain Tumor Segmentation Challenge 2023" dataset_json['tensorImageSize'] = "4D" dataset_json['reference'] = "https://www.synapse.org/#!Synapse:syn51156910" dataset_json['licence'] = "see BraTS2023 website" dataset_json['release'] = "1.0" dataset_json['channel_names'] = { "0": "T1n", "1": "T1c", "2": "T2f", "3": "T2w", } dataset_json['labels'] = { "background": 0, "necrotic/non-enhancing tumor": 1, "edema": 2, "enhancing tumor": 3, } dataset_json["file_ending"] = ".nii.gz" dataset_json['numTraining'] = len(training_cases) dataset_json['numTest'] = len(test_cases)
dataset_json['training'] = [ { "image": [f"./imagesTr/{case}_{suffix}.nii.gz" for suffix in MODALITY_MAPPING.values()], "label": f"./labelsTr/{case}.nii.gz", } for case in training_cases ] dataset_json['test'] = [ [f"./imagesTs/{case}_{suffix}.nii.gz" for suffix in MODALITY_MAPPING.values()] for case in test_cases ]
json_file_path = os.path.join(task_folder, "dataset.json") with open(json_file_path, 'w', encoding='utf-8') as f: json.dump(dataset_json, f, indent=4, ensure_ascii=False)
print(f"dataset.json 已保存到: {json_file_path}") print("BraTS2023数据转换完成!标签保持原始值: 0(背景), 1(坏死/非增强), 2(水肿), 3(增强肿瘤)") return task_folder
def parse_args(): parser = argparse.ArgumentParser(description="BraTS2023 -> nnU-Net 转换(多线程)") parser.add_argument("--brats-root", default="/root/autodl-tmp/BraTS2023") parser.add_argument("--nnunet-raw", default="/root/autodl-tmp/data/nnUNet_raw") parser.add_argument("--task-name", default="Dataset001_BraTS2023") parser.add_argument("--workers", type=int, default=8, help="线程数;机械盘建议 4,NVMe 建议 8~16") parser.add_argument("--train-ratio", type=float, default=0.8) parser.add_argument("--verify-labels", action="store_true", help="读取每个 seg 的体素值并统计唯一标签(较慢)") parser.add_argument("--overwrite", action="store_true", help="覆盖已存在的目标文件(默认跳过,可断点续跑)") return parser.parse_args()
if __name__ == "__main__": args = parse_args() try: result = convert_brats2023_to_nnunet( brats_root=args.brats_root, nnunet_raw_data_base=args.nnunet_raw, task_name=args.task_name, workers=args.workers, train_ratio=args.train_ratio, verify_labels=args.verify_labels, overwrite=args.overwrite, ) if result: print(f"\n转换成功完成!输出目录: {result}") print("可以继续进行nnUNet的预处理和训练步骤。") else: print("\n转换失败,请查看错误日志了解详细信息。") except Exception as e: print(f"程序执行失败: {str(e)}") error_file_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "error.txt") with open(error_file_path, 'w', encoding='utf-8') as f: f.write(f"程序执行失败: {str(e)}\n") print(f"错误已记录到: {error_file_path}")BraTS2024
import osimport shutilimport jsonimport threadingfrom concurrent.futures import ThreadPoolExecutor, as_completed
import nibabel as nibimport numpy as npfrom collections import OrderedDict
def convert_brats2024_to_nnunet(brats_root, nnunet_raw_data_base, max_workers=8): """ 将BraTS2024数据集转换为nnUNet格式,保持原始标签不变(多线程加速版)
Args: brats_root: BraTS2024原始数据根目录路径 nnunet_raw_data_base: nnUNet原始数据基础目录路径 max_workers: 并行线程数(I/O 密集,建议 4~16,视磁盘性能而定) """
# 设置路径 task_name = "Dataset001_BraTS2024" task_folder = os.path.join(nnunet_raw_data_base, "nnUNet_raw", task_name)
imagesTr_folder = os.path.join(task_folder, "imagesTr") imagesTs_folder = os.path.join(task_folder, "imagesTs") labelsTr_folder = os.path.join(task_folder, "labelsTr") labelsTs_folder = os.path.join(task_folder, "labelsTs")
for folder in [imagesTr_folder, imagesTs_folder, labelsTr_folder, labelsTs_folder]: os.makedirs(folder, exist_ok=True)
# 收集所有病例文件夹 all_cases = [] for item in os.listdir(brats_root): case_path = os.path.join(brats_root, item) if os.path.isdir(case_path) and item.startswith('BraTS-'): all_cases.append(item)
all_cases.sort() # 确保顺序一致,训练/测试划分才可复现 total = len(all_cases) print(f"找到 {total} 个病例,使用 {max_workers} 个线程处理")
# 模态映射 - BraTS2024模态 modality_mapping = { 't1n': '0000', # T1 native (非增强T1) 't1c': '0001', # T1 contrast enhanced (增强T1) 't2f': '0002', # T2 FLAIR 't2w': '0003' # T2 weighted }
# 训练/测试划分点(前80%训练),事先算好,保证并行下划分与原逻辑一致 split_index = total * 0.8
# ---------- 纯函数式辅助:只返回结果,绝不修改外部共享变量 ----------
def check_file_validity(file_path): """检查文件是否有效(非空且可读取),返回 (bool, msg)""" try: if not os.path.exists(file_path): return False, "文件不存在" if os.path.getsize(file_path) == 0: return False, "文件为空" nib.load(file_path) # 只读文件头,验证可解析 return True, "文件有效" except Exception as e: return False, f"文件无效: {str(e)}"
def copy_image(src_path, dst_path, case_name, modality, errors): """复制图像文件,出错时把错误追加到本病例的局部 errors 列表""" try: if not src_path.endswith('.gz'): nib.save(nib.load(src_path), dst_path) else: shutil.copy2(src_path, dst_path) return True except Exception as e: errors.append( f"复制图像失败 - 病例: {case_name}, 模态: {modality}, " f"文件: {src_path}, 错误: {str(e)}" ) return False
def copy_label(src_path, dst_path, case_name, errors, logs): """复制标签文件并检查标签值分布,错误/日志写入局部列表""" try: label_nii = nib.load(src_path) label_data = label_nii.get_fdata().astype(np.uint8)
unique_labels = np.unique(label_data) logs.append(f" {os.path.basename(src_path)} 标签值: {unique_labels}")
# 验证标签值是否符合BraTS2024规范(0, 1, 2, 3, 4) valid_labels = {0, 1, 2, 3, 4} unexpected_labels = set(int(x) for x in unique_labels) - valid_labels if unexpected_labels: errors.append( f"发现意外标签值 - 病例: {case_name}, 标签值: {unexpected_labels}" )
# 统计各标签的体素数量 label_counts = {int(l): int(np.sum(label_data == l)) for l in unique_labels} logs.append(f" 标签分布: {label_counts}")
# 直接保存,不做任何标签修改 if not src_path.endswith('.gz'): nib.save(label_nii, dst_path) else: shutil.copy2(src_path, dst_path) return True except Exception as e: errors.append( f"复制标签失败 - 病例: {case_name}, 文件: {src_path}, 错误: {str(e)}" ) return False
# ---------- 单个病例的完整处理逻辑(在工作线程中运行)----------
def process_case(i, case_name): """ 处理单个病例,返回一个结果字典: {'case_name', 'status': 'train'/'test'/'skip', 'errors': [...], 'logs': [...]} 整个函数只读取外部只读变量,产出全部装进返回值,线程安全。 """ errors = [] logs = [] case_folder = os.path.join(brats_root, case_name)
if not os.path.exists(case_folder): errors.append(f"病例文件夹不存在: {case_folder}") return {'case_name': case_name, 'status': 'skip', 'errors': errors, 'logs': logs}
base_name = case_name # 例如: BraTS-GLI-00000-000 required_files = { 't1n': f"{base_name}-t1n.nii", 't1c': f"{base_name}-t1c.nii", 't2f': f"{base_name}-t2f.nii", 't2w': f"{base_name}-t2w.nii", 'seg': f"{base_name}-seg.nii", }
# 检查文件存在性与有效性,支持 .nii / .nii.gz files_ok = True for key, filename in required_files.items(): file_path = os.path.join(case_folder, filename) gz_file_path = file_path + ".gz"
if os.path.exists(file_path): valid, msg = check_file_validity(file_path) if not valid: errors.append(f"文件无效 - 病例: {case_name}, {filename}: {msg}") files_ok = False elif os.path.exists(gz_file_path): required_files[key] = filename + ".gz" valid, msg = check_file_validity(gz_file_path) if not valid: errors.append(f"文件无效 - 病例: {case_name}, {filename}.gz: {msg}") files_ok = False else: errors.append(f"文件缺失 - 病例: {case_name}, 文件: {filename} 或 {filename}.gz") files_ok = False
if not files_ok: return {'case_name': case_name, 'status': 'skip', 'errors': errors, 'logs': logs}
# 决定训练还是测试(前80%训练) is_training = i < split_index img_dst_folder = imagesTr_folder if is_training else imagesTs_folder lbl_dst_folder = labelsTr_folder if is_training else labelsTs_folder
case_success = True
# 复制四个模态图像 for modality, suffix in modality_mapping.items(): src_file = os.path.join(case_folder, required_files[modality]) dst_file = os.path.join(img_dst_folder, f"{case_name}_{suffix}.nii.gz") if not copy_image(src_file, dst_file, case_name, modality, errors): case_success = False
# 复制分割标签 src_seg = os.path.join(case_folder, required_files['seg']) dst_seg = os.path.join(lbl_dst_folder, f"{case_name}.nii.gz") if not copy_label(src_seg, dst_seg, case_name, errors, logs): case_success = False
if case_success: status = 'train' if is_training else 'test' else: status = 'skip' return {'case_name': case_name, 'status': status, 'errors': errors, 'logs': logs}
# ---------- 并行调度 ----------
training_cases = [] test_cases = [] skipped_cases = [] errors = []
done = 0 progress_lock = threading.Lock()
with ThreadPoolExecutor(max_workers=max_workers) as executor: futures = { executor.submit(process_case, i, case_name): case_name for i, case_name in enumerate(all_cases) }
for future in as_completed(futures): case_name = futures[future] try: result = future.result() except Exception as e: # 兜底:worker 内部未捕获的异常 errors.append(f"病例 {case_name} 处理时发生未捕获异常: {str(e)}") skipped_cases.append(case_name) result = None
if result is not None: errors.extend(result['errors']) status = result['status'] if status == 'train': training_cases.append(result['case_name']) elif status == 'test': test_cases.append(result['case_name']) else: skipped_cases.append(result['case_name'])
# 线程安全地更新并打印进度 with progress_lock: done += 1 print(f"[{done}/{total}] {case_name} -> " f"{result['status'] if result else 'skip'}")
# 结果列表排序,保证输出稳定(并行完成顺序是乱的) training_cases.sort() test_cases.sort() skipped_cases.sort()
print(f"\n处理完成: {len(training_cases)} 个训练病例, {len(test_cases)} 个测试病例") print(f"跳过的病例数量: {len(skipped_cases)}")
# ---------- 写入错误日志 ---------- error_file_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "error.txt") with open(error_file_path, 'w', encoding='utf-8') as f: f.write("BraTS2024数据转换错误报告\n") f.write("=" * 80 + "\n\n") f.write(f"总计处理: {total} 个病例\n") f.write(f"成功处理: {len(training_cases) + len(test_cases)} 个病例\n") f.write(f"跳过病例: {len(skipped_cases)} 个病例\n") f.write(f"错误数量: {len(errors)} 个错误\n\n")
if skipped_cases: f.write("跳过的病例列表:\n") for case in skipped_cases: f.write(f" - {case}\n") f.write("\n")
if errors: f.write("详细错误信息:\n") for idx, error in enumerate(errors, 1): f.write(f"{idx}. {error}\n") else: f.write("没有发现错误。\n")
print(f"错误日志已保存到: {error_file_path}")
# ---------- 生成 dataset.json(nnU-Net v2 格式)---------- if len(training_cases) + len(test_cases) > 0: dataset_json = OrderedDict()
# v2: channel_names 取代 v1 的 modality,键为通道索引字符串 dataset_json['channel_names'] = { "0": "T1n", "1": "T1c", "2": "T2f", "3": "T2w", }
# v2: labels 是 名称 -> 整数值(与 v1 正好相反) dataset_json['labels'] = { "background": 0, "NETC": 1, # Non-Enhancing Tumor Core "SNFH": 2, # Surrounding Non-enhancing FLAIR Hyperintensity "ET": 3, # Enhancing Tumor "RC": 4, # Resection Cavity (BraTS2024 新增) }
# v2 必需字段 dataset_json['numTraining'] = len(training_cases) dataset_json['file_ending'] = ".nii.gz"
# 以下为可选元信息,nnU-Net 不使用但不会报错,仅方便追溯 dataset_json['name'] = "BraTS2024" dataset_json['description'] = "Brain Tumor Segmentation Challenge 2024" dataset_json['reference'] = "https://www.synapse.org/#!Synapse:syn53708249" dataset_json['licence'] = "see BraTS2024 website" dataset_json['release'] = "1.0"
json_file_path = os.path.join(task_folder, "dataset.json") with open(json_file_path, 'w') as f: json.dump(dataset_json, f, indent=4)
print(f"dataset.json 已保存到: {json_file_path}") print("BraTS2024数据转换完成!标签保持原始值不变:") print(" 0 - background (背景)") print(" 1 - NETC - 非增强肿瘤核心") print(" 2 - SNFH - 周围非增强FLAIR高信号") print(" 3 - ET - 增强肿瘤") print(" 4 - RC - 切除腔") return task_folder else: print("警告: 没有成功处理任何病例,未生成dataset.json文件") return None
# 使用示例if __name__ == "__main__": brats_root = "BraTS2024" # 您的BraTS2024数据根目录 nnunet_raw_data_base = "data" # nnUNet原始数据基础目录
try: result = convert_brats2024_to_nnunet(brats_root, nnunet_raw_data_base, max_workers=8) if result: print(f"\n转换成功完成!输出目录: {result}") print("可以继续进行nnUNet的预处理和训练步骤。") print("\n注意:BraTS2024引入了新的切除腔(RC)标签(标签值4)。") else: print("\n转换失败,请查看错误日志了解详细信息。") except Exception as e: print(f"程序执行失败: {str(e)}") error_file_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "error.txt") with open(error_file_path, 'w', encoding='utf-8') as f: f.write(f"程序执行失败: {str(e)}\n") print(f"错误已记录到: {error_file_path}")