变化检测数据集制作:从Labelme标注到模型训练的完整流程
1. 变化检测数据集制作全流程解析变化检测是计算机视觉领域的重要任务它通过分析同一区域不同时间的影像识别出地表或物体的变化情况。要训练一个高效的变化检测模型数据集的制作尤为关键。今天我就带大家走通从原始影像准备到模型训练的全流程分享我在实际项目中积累的经验和踩过的坑。整个流程可以概括为五个关键步骤影像准备→Labelme标注→格式转换→数据集组织→模型训练。其中最核心的是标注后的格式转换环节这里需要处理AB影像的变化区域合并、独有区域提取等特殊情况。我们以256×256像素的卫星影像为例两期影像分别存放在A、B两个文件夹通过文件名对应匹配。变化检测数据集的特点在于需要同时处理两期影像的标注信息。与普通分割任务不同我们需要特别关注三种情况A影像单独变化区域、B影像单独变化区域、以及两期影像都发生变化的区域。这直接影响到后续模型训练的效果。2. 使用Labelme进行专业标注Labelme作为MIT开发的标注工具特别适合变化检测任务的标注工作。安装非常简单只需执行pip install labelme即可。我推荐使用5.0.1版本这个版本在标注稳定性和文件兼容性方面表现最好。标注时需要注意几个要点对A、B两期影像分别建立标注项目变化区域使用多边形(Polygon)精确勾勒统一使用change作为变化区域的标签名保存时文件名要保持一致仅通过文件夹区分A/B期实际操作中我会这样组织文件结构/project /A img1.jpg img1.json /B img1.jpg img1.json标注完成后你会得到成对的json文件。这里有个实用技巧标注时按住Ctrl键可以微调节点位置用空格键可以切换显示/隐藏标注区域这些快捷键能显著提升标注效率。3. JSON到PNG的格式转换Labelme生成的json文件需要转换为模型可读的PNG掩码。转换的核心是将json中的多边形坐标转换为二值图像其中255表示变化区域0表示背景。以下是优化后的转换代码增加了错误处理和进度显示import json import os import numpy as np import cv2 from tqdm import tqdm def json_to_mask(json_path, output_path): try: with open(json_path, r) as f: data json.load(f) mask np.zeros((data[imageHeight], data[imageWidth]), dtypenp.uint8) for shape in data[shapes]: points np.array(shape[points], dtypenp.int32) cv2.fillPoly(mask, [points], color255) cv2.imwrite(output_path, mask) return True except Exception as e: print(fError processing {json_path}: {str(e)}) return False # 批量转换A、B文件夹的json for folder in [A, B]: json_dir f{folder}_json os.makedirs(json_dir, exist_okTrue) json_files [f for f in os.listdir(folder) if f.endswith(.json)] for json_file in tqdm(json_files, descfProcessing {folder}): json_path os.path.join(folder, json_file) png_path os.path.join(json_dir, json_file.replace(.json, .png)) json_to_mask(json_path, png_path)这段代码相比原始版本有三个改进增加了try-catch异常处理使用tqdm显示进度条自动创建输出目录保留了原始文件名对应关系4. 变化区域合并与提取变化检测的特殊之处在于需要处理AB影像的关联关系。我们开发了专门的合并算法来处理三种情况4.1 同名PNG合并处理对于AB影像都发生变化的区域我们需要合并两个掩码from PIL import Image import os def merge_masks(mask_a, mask_b): 合并两个变化掩码任一为变化区域则保留 img_a Image.open(mask_a).convert(L) img_b Image.open(mask_b).convert(L) width, height img_a.size merged Image.new(L, (width, height), 0) for x in range(width): for y in range(height): pix_a img_a.getpixel((x, y)) pix_b img_b.getpixel((x, y)) merged.putpixel((x, y), 255 if pix_a 255 or pix_b 255 else 0) return merged # 找出AB共有的文件 common_files set(os.listdir(A_json)) set(os.listdir(B_json)) os.makedirs(merged, exist_okTrue) for file in common_files: mask_a os.path.join(A_json, file) mask_b os.path.join(B_json, file) output os.path.join(merged, file) merged merge_masks(mask_a, mask_b) merged.save(output)4.2 独有变化区域提取对于仅单期影像发生变化的区域我们需要单独保留import shutil # 获取独有文件 only_a set(os.listdir(A_json)) - set(os.listdir(B_json)) only_b set(os.listdir(B_json)) - set(os.listdir(A_json)) os.makedirs(unique, exist_okTrue) # 拷贝独有文件 for file in only_a: shutil.copy(os.path.join(A_json, file), os.path.join(unique, file)) for file in only_b: shutil.copy(os.path.join(B_json, file), os.path.join(unique, file))4.3 最终标签整合将所有变化区域整合到一个文件夹final_labels labels os.makedirs(final_labels, exist_okTrue) # 合并后的标签 for file in os.listdir(merged): shutil.copy(os.path.join(merged, file), os.path.join(final_labels, file)) # 独有标签 for file in os.listdir(unique): shutil.copy(os.path.join(unique, file), os.path.join(final_labels, file))5. 数据集组织与模型训练标准的变化检测数据集应包含以下结构/dataset /train /A img1.jpg img2.jpg /B img1.jpg img2.jpg /label img1.png img2.png /val /A /B /label /test /A /B /label我推荐使用7:2:1的比例划分训练集、验证集和测试集。可以使用以下代码自动划分import random from sklearn.model_selection import train_test_split all_files [f for f in os.listdir(labels) if f.endswith(.png)] train_files, test_files train_test_split(all_files, test_size0.3, random_state42) val_files, test_files train_test_split(test_files, test_size0.33, random_state42) def organize_dataset(files, subset): os.makedirs(fdataset/{subset}/A, exist_okTrue) os.makedirs(fdataset/{subset}/B, exist_okTrue) os.makedirs(fdataset/{subset}/label, exist_okTrue) for file in files: base os.path.splitext(file)[0] # 拷贝A期影像 shutil.copy(fA/{base}.jpg, fdataset/{subset}/A/{base}.jpg) # 拷贝B期影像 shutil.copy(fB/{base}.jpg, fdataset/{subset}/B/{base}.jpg) # 拷贝标签 shutil.copy(flabels/{file}, fdataset/{subset}/label/{file}) organize_dataset(train_files, train) organize_dataset(val_files, val) organize_dataset(test_files, test)6. 常见问题解决方案在实际项目中我遇到过几个典型问题影像对齐问题两期影像必须严格配准。解决方法是在标注前使用OpenCV的ECC算法进行配准def align_images(img_a, img_b): # 转换为灰度图 gray_a cv2.cvtColor(img_a, cv2.COLOR_BGR2GRAY) gray_b cv2.cvtColor(img_b, cv2.COLOR_BGR2GRAY) # 估计变换矩阵 warp_mode cv2.MOTION_HOMOGRAPHY warp_matrix np.eye(3, 3, dtypenp.float32) criteria (cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 5000, 1e-10) _, warp_matrix cv2.findTransformECC(gray_a, gray_b, warp_matrix, warp_mode, criteria) # 应用变换 aligned cv2.warpPerspective(img_b, warp_matrix, (img_a.shape[1], img_a.shape[0]), flagscv2.INTER_LINEAR cv2.WARP_INVERSE_MAP) return aligned小变化区域漏检建议在标注时对细小变化区域适当放大标注范围训练时可以使用Dice Loss这类对小目标敏感的损失函数。样本不均衡变化像素通常远少于未变化像素。解决方法包括在损失函数中增加变化类别的权重使用过采样技术采用Focal Loss季节性变化干扰对于光学影像不同季节的植被变化可能造成误检。可以考虑使用SAR影像替代光学影像引入NDVI等指数进行预处理增加多时相训练数据7. 模型训练建议对于变化检测任务推荐以下几种网络结构UNet在UNet基础上增加了密集跳跃连接能更好捕捉多尺度特征BIT专为变化检测设计的双时相Transformer网络SNUNet结合了通道注意力和密集连接的高效网络训练时可以尝试以下技巧提升性能使用AdamW优化器初始学习率设为3e-4添加CutMix数据增强采用渐进式学习率衰减早停策略防止过拟合一个典型的训练命令示例python train.py \ --model SNUNet \ --dataset_path ./dataset \ --batch_size 16 \ --lr 0.0003 \ --epochs 100 \ --loss focal_dice \ --output_dir ./results在项目实践中我发现合理的数据增强能显著提升模型泛化能力。推荐的变化检测专用增强包括时相交换交换AB影像顺序时相内颜色抖动随机时相偏移多时相混合(MixUp)最后提醒一点变化检测模型的评估指标要全面不能只看整体准确率。应该重点关注变化类别的IoU误检率(FPR)漏检率(FNR)Kappa系数记得在验证集上保存最佳模型并在测试集上做最终评估。好的变化检测模型应该在不同季节、不同地区的影像上都能保持稳定表现。