资讯动态

【Bug已解决】PyTorch : How to apply the same random transformation to multiple image? 解决方案

发布时间:2026/10/13 19:45:20 来源:尧图企业网站定制
【Bug已解决】PyTorch : How to apply the same random transformation to multiple image? 解决方案问题描述在 PyTorch 中进行图像数据增强时一个常见的需求是对多张相关联的图像应用相同的随机变换。例如立体视觉对左右视角的图像对应用相同的几何变换裁剪、翻转、旋转语义分割对输入图像和对应的分割掩码应用相同的空间变换超分辨率对低分辨率和高分辨率图像对应用相同的裁剪和翻转视频处理对视频帧序列应用相同的时间一致变换对比学习对同一图像的两个增强版本应用部分相同的变换如果对图像和掩码分别应用随机变换由于随机种子不同它们的空间位置会不一致导致训练数据错误。例如如果对图像随机裁剪了左上角区域但对掩码随机裁剪了右下角区域那么图像和掩码就不再对应了。PyTorch 的torchvision.transforms默认每次调用都会生成新的随机参数因此需要特殊处理来确保多个相关图像获得相同的随机变换。错误复现以下代码展示了错误的做法——对图像和掩码分别应用随机变换import torch import torchvision.transforms as transforms import torchvision.transforms.functional as TF from PIL import Image import numpy as np # 创建模拟的图像和掩码 image Image.fromarray(np.random.randint(0, 256, (256, 256, 3), dtypenp.uint8)) mask Image.fromarray(np.random.randint(0, 10, (256, 256), dtypenp.uint8)) # 错误做法分别应用随机变换 random_crop transforms.RandomCrop(128) # 每次调用生成不同的随机参数 cropped_image random_crop(image) cropped_mask random_crop(mask) print(fImage size: {cropped_image.size}) print(fMask size: {cropped_mask.size}) # 检查裁剪区域是否一致 # 它们裁剪了不同的区域 # 这会导致图像和掩码不再对应 # 另一个错误示例随机翻转 random_flip transforms.RandomHorizontalFlip(p1.0) # p1.0 确保翻转 flipped_image random_flip(image) flipped_mask random_flip(mask) # 虽然这个例子中两者都翻转了因为 p1.0 # 但如果 p0.5可能一个翻转了一个没有根因分析1. torchvision.transforms 的随机性机制torchvision.transforms中的随机变换如RandomCrop、RandomHorizontalFlip、RandomRotation等在每次调用时都会生成新的随机参数# RandomCrop 每次调用都会随机选择不同的裁剪位置 crop transforms.RandomCrop(128) # 第一次调用可能裁剪 (0, 0) 到 (128, 128) result1 crop(image) # 第二次调用可能裁剪 (50, 50) 到 (178, 178) result2 crop(image) # result1 和 result2 是不同的裁剪结果2. 变换的分类变换类型示例是否需要同步处理方式几何变换裁剪、翻转、旋转、缩放是必须使用相同参数像素变换颜色抖动、高斯模糊否可独立应用归一化NormalizeN/A确定性无需同步3. 需要同步的变换以下变换涉及空间位置的修改必须对相关图像应用相同的参数RandomCrop- 随机裁剪位置RandomResizedCrop- 随机裁剪缩放RandomHorizontalFlip- 水平翻转RandomVerticalFlip- 垂直翻转RandomRotation- 随机旋转角度RandomAffine- 随机仿射变换RandomPerspective- 随机透视变换ColorJitter部分情况 - 如果对多光谱图像需要同步4. 不需要同步的变换以下变换只修改像素值不涉及空间位置可以独立应用ColorJitter- 颜色抖动用于单个图像时GaussianBlur- 高斯模糊RandomGrayscale- 随机灰度化Normalize- 标准化RandomErasing- 随机擦除通常只用于输入图像解决方案方案一使用 torchvision.transforms.functional推荐torchvision.transforms.functional通常简写为TF提供了确定性的变换函数允许你手动控制变换参数。先生成随机参数然后将相同的参数应用到所有相关图像import torch import torchvision.transforms.functional as TF import torchvision.transforms as transforms from PIL import Image import numpy as np import random class PairedTransform: 对图像对如 image 和 mask应用相同的随机变换。 使用 torchvision.transforms.functional 模块 先生成随机参数再应用到所有图像。 def __init__(self, output_size128, horizontal_flip_prob0.5, vertical_flip_prob0.5, rotation_degrees10): self.output_size output_size self.horizontal_flip_prob horizontal_flip_prob self.vertical_flip_prob vertical_flip_prob self.rotation_degrees rotation_degrees def __call__(self, image, mask): 对 image 和 mask 应用相同的随机变换。 参数: image: PIL Image 或 Tensor (C, H, W) mask: PIL Image 或 Tensor (C, H, W) 或 (H, W) 返回: 变换后的 (image, mask) 元组 # 1. 随机裁剪 —— 生成相同的裁剪参数 i, j, h, w transforms.RandomCrop.get_params( image, output_size(self.output_size, self.output_size), ) image TF.crop(image, i, j, h, w) mask TF.crop(mask, i, j, h, w) # 2. 随机水平翻转 —— 使用相同的随机决策 if random.random() self.horizontal_flip_prob: image TF.hflip(image) mask TF.hflip(mask) # 3. 随机垂直翻转 if random.random() self.vertical_flip_prob: image TF.vflip(image) mask TF.vflip(mask) # 4. 随机旋转 —— 使用相同的旋转角度 angle transforms.RandomRotation.get_params( [-self.rotation_degrees, self.rotation_degrees] ) image TF.rotate(image, angle, fill0) mask TF.rotate(mask, angle, fill0) # 5. 转换为张量确定性变换可以独立应用 image TF.to_tensor(image) if not isinstance(mask, torch.Tensor): mask TF.to_tensor(mask) # 6. 归一化仅对图像不对掩码 image TF.normalize(image, [0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) return image, mask # 使用示例 image Image.fromarray(np.random.randint(0, 256, (256, 256, 3), dtypenp.uint8)) mask Image.fromarray(np.random.randint(0, 10, (256, 256), dtypenp.uint8)) paired_transform PairedTransform(output_size128) transformed_image, transformed_mask paired_transform(image, mask) print(fTransformed image shape: {transformed_image.shape}) print(fTransformed mask shape: {transformed_mask.shape})方案二使用 Random参数共享的自定义 Transformimport torch import torchvision.transforms as transforms import torchvision.transforms.functional as TF import random from PIL import Image import numpy as np class SyncedRandomCrop: 同步随机裁剪 def __init__(self, size): self.size size if isinstance(size, tuple) else (size, size) def get_params(self, img): 生成随机裁剪参数 return transforms.RandomCrop.get_params(img, self.size) def __call__(self, image, mask): # 生成一次参数应用到两个图像 i, j, h, w self.get_params(image) image TF.crop(image, i, j, h, w) mask TF.crop(mask, i, j, h, w) return image, mask class SyncedRandomFlip: 同步随机翻转 def __init__(self, p0.5, verticalFalse): self.p p self.vertical vertical def __call__(self, image, mask): if random.random() self.p: if self.vertical: image TF.vflip(image) mask TF.vflip(mask) else: image TF.hflip(image) mask TF.hflip(mask) return image, mask class SyncedRandomRotation: 同步随机旋转 def __init__(self, degrees, fill0): self.degrees degrees self.fill fill def __call__(self, image, mask): angle transforms.RandomRotation.get_params( [-self.degrees, self.degrees] ) image TF.rotate(image, angle, fillself.fill, interpolationtransforms.InterpolationMode.BILINEAR) mask TF.rotate(mask, angle, fillself.fill, interpolationtransforms.InterpolationMode.NEAREST) return image, mask class ComposedPairedTransform: 组合的配对变换 def __init__(self, transforms_list): self.transforms_list transforms_list def __call__(self, image, mask): for t in self.transforms_list: image, mask t(image, mask) return image, mask # 使用示例 paired_transforms ComposedPairedTransform([ SyncedRandomCrop(128), SyncedRandomFlip(p0.5, verticalFalse), SyncedRandomFlip(p0.5, verticalTrue), SyncedRandomRotation(degrees15), ]) image Image.fromarray(np.random.randint(0, 256, (256, 256, 3), dtypenp.uint8)) mask Image.fromarray(np.random.randint(0, 10, (256, 256), dtypenp.uint8)) transformed_image, transformed_mask paired_transforms(image, mask) print(fImage: {transformed_image.size}, Mask: {transformed_mask.size})方案三使用 Albumentations 库替代方案Albumentations是一个专门为图像增强设计的库原生支持对多个目标如图像和掩码应用相同的变换# pip install albumentations import albumentations as A import numpy as np # 定义变换管道 transform A.Compose([ A.RandomCrop(128, 128, p1.0), A.HorizontalFlip(p0.5), A.VerticalFlip(p0.5), A.Rotate(limit15, p0.5), A.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ToTensorV2(), ]) # 模拟数据 image np.random.randint(0, 256, (256, 256, 3), dtypenp.uint8) mask np.random.randint(0, 10, (256, 256), dtypenp.uint8) # Albumentations 自动对 image 和 mask 应用相同的几何变换 result transform(imageimage, maskmask) transformed_image result[image] # 变换后的图像 transformed_mask result[mask] # 变换后的掩码应用了相同的几何变换 print(fImage shape: {transformed_image.shape}) print(fMask shape: {transformed_mask.shape})完整修复代码以下是一个完整的语义分割数据集实现正确处理了图像和掩码的同步变换 完整示例语义分割数据集正确同步图像和掩码的随机变换 import torch import torch.nn as nn import torchvision.transforms as transforms import torchvision.transforms.functional as TF from torch.utils.data import Dataset, DataLoader from PIL import Image import numpy as np import random import os # # 1. 配对变换类 # class SegmentationTransform: 语义分割的配对数据增强。 对输入图像和分割掩码应用相同的几何变换 确保空间位置一致。 支持的变换 - RandomResizedCrop: 随机裁剪缩放 - RandomHorizontalFlip: 随机水平翻转 - RandomVerticalFlip: 随机垂直翻转 - RandomRotation: 随机旋转 - ColorJitter: 颜色抖动仅图像 - Normalize: 归一化仅图像 def __init__(self, crop_size256, scale_range(0.8, 1.2), hflip_prob0.5, vflip_prob0.5, rotation_degrees10, color_jitter_paramsNone, normalize_mean[0.485, 0.456, 0.406], normalize_std[0.229, 0.224, 0.225]): self.crop_size crop_size self.scale_range scale_range self.hflip_prob hflip_prob self.vflip_prob vflip_prob self.rotation_degrees rotation_degrees self.color_jitter_params color_jitter_params self.normalize_mean normalize_mean self.normalize_std normalize_std def __call__(self, image, mask): 应用同步的随机变换。 参数: image: PIL Image, (H, W, C) numpy, 或 (C, H, W) tensor mask: PIL Image, (H, W) numpy, 或 (C, H, W) tensor 返回: (image_tensor, mask_tensor) 元组 # 确保输入是 PIL Image if isinstance(image, np.ndarray): image Image.fromarray(image) if isinstance(mask, np.ndarray): mask Image.fromarray(mask) # --- 1. RandomResizedCrop --- # 生成一次随机参数应用到两个图像 i, j, h, w transforms.RandomResizedCrop.get_params( image, scaleself.scale_range, ratio(3.0 / 4.0, 4.0 / 3.0), ) image TF.resized_crop(image, i, j, h, w, (self.crop_size, self.crop_size), interpolationtransforms.InterpolationMode.BILINEAR) mask TF.resized_crop(mask, i, j, h, w, (self.crop_size, self.crop_size), interpolationtransforms.InterpolationMode.NEAREST) # --- 2. 随机水平翻转 --- if random.random() self.hflip_prob: image TF.hflip(image) mask TF.hflip(mask) # --- 3. 随机垂直翻转 --- if random.random() self.vflip_prob: image TF.vflip(image) mask TF.vflip(mask) # --- 4. 随机旋转 --- if self.rotation_degrees 0: angle transforms.RandomRotation.get_params( [-self.rotation_degrees, self.rotation_degrees] ) if angle ! 0: image TF.rotate(image, angle, interpolationtransforms.InterpolationMode.BILINEAR, fill0) mask TF.rotate(mask, angle, interpolationtransforms.InterpolationMode.NEAREST, fill255) # 255 表示忽略区域 # --- 5. 颜色抖动仅图像 --- if self.color_jitter_params: brightness, contrast, saturation, hue self.color_jitter_params # 生成颜色抖动参数 b random.uniform(max(0, 1 - brightness), 1 brightness) if brightness else 1 c random.uniform(max(0, 1 - contrast), 1 contrast) if contrast else 1 s random.uniform(max(0, 1 - saturation), 1 saturation) if saturation else 1 h random.uniform(-hue, hue) if hue else 0 # 仅应用到图像 image TF.adjust_brightness(image, b) image TF.adjust_contrast(image, c) image TF.adjust_saturation(image, s) image TF.adjust_hue(image, h) # --- 6. 转换为张量 --- image TF.to_tensor(image) mask torch.as_tensor(np.array(mask), dtypetorch.long) # --- 7. 归一化仅图像 --- image TF.normalize(image, self.normalize_mean, self.normalize_std) return image, mask # # 2. 语义分割数据集 # class SegmentationDataset(Dataset): 语义分割数据集。 正确处理图像和掩码的同步数据增强。 def __init__(self, num_samples200, image_size256, num_classes21, transformNone, is_trainTrue): self.num_samples num_samples self.image_size image_size self.num_classes num_classes self.is_train is_train self.transform transform # 生成模拟数据 self._generate_mock_data() def _generate_mock_data(self): 生成模拟的图像和掩码数据 np.random.seed(42 if self.is_train else 123) self.images [] self.masks [] for i in range(self.num_samples): # 生成有意义的图像不同颜色的区域 img np.zeros((self.image_size, self.image_size, 3), dtypenp.uint8) mask np.zeros((self.image_size, self.image_size), dtypenp.uint8) # 创建几个随机区域 num_regions np.random.randint(3, 8) for _ in range(num_regions): cx np.random.randint(50, self.image_size - 50) cy np.random.randint(50, self.image_size - 50) radius np.random.randint(20, 60) class_id np.random.randint(0, self.num_classes) # 在图像上绘制圆形 color np.random.randint(0, 256, 3) y, x np.ogrid[:self.image_size, :self.image_size] circle (x - cx)**2 (y - cy)**2 radius**2 img[circle] color mask[circle] class_id self.images.append(img) self.masks.append(mask) def __len__(self): return self.num_samples def __getitem__(self, idx): image self.images[idx].copy() mask self.masks[idx].copy() if self.transform: image, mask self.transform(image, mask) else: # 基本转换无增强 image TF.to_tensor(Image.fromarray(image)) image TF.normalize(image, [0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) mask torch.as_tensor(mask, dtypetorch.long) return image, mask # # 3. 验证变换一致性 # def verify_transform_consistency(): 验证图像和掩码的变换是否一致 print( * 60) print(Verifying Transform Consistency) print( * 60) # 创建变换 train_transform SegmentationTransform( crop_size128, scale_range(0.5, 1.5), hflip_prob0.5, vflip_prob0.5, rotation_degrees15, color_jitter_params(0.2, 0.2, 0.2, 0.1), ) # 创建数据集 dataset SegmentationDataset( num_samples10, image_size256, num_classes5, transformtrain_transform, ) # 获取一个样本 image, mask dataset[0] print(f\nImage shape: {image.shape}) # torch.Size([3, 128, 128]) print(fMask shape: {mask.shape}) # torch.Size([128, 128]) print(fImage dtype: {image.dtype}) # torch.float32 print(fMask dtype: {mask.dtype}) # torch.int64 print(fMask unique values: {torch.unique(mask)}) # 验证空间一致性 # 通过检查图像和掩码的边缘是否对齐来验证 print(\n--- Spatial Consistency Check ---) # 多次应用变换检查一致性 for trial in range(5): # 使用相同的原始数据 raw_image Image.fromarray(dataset.images[0].copy()) raw_mask Image.fromarray(dataset.masks[0].copy()) # 应用变换 t_image, t_mask train_transform(raw_image, raw_mask) # 检查形状一致 assert t_image.shape[1:] t_mask.shape, \ fShape mismatch: image {t_image.shape[1:]} vs mask {t_mask.shape} print(f Trial {trial1}: Image {t_image.shape}, Mask {t_mask.shape} - OK) print(\nAll consistency checks passed!) # # 4. 训练示例 # def train_segmentation(): 简单的语义分割训练示例 print(\n * 60) print(Training Semantic Segmentation Model) print( * 60) # 数据准备 train_transform SegmentationTransform( crop_size64, scale_range(0.8, 1.2), hflip_prob0.5, vflip_prob0.5, rotation_degrees10, ) train_dataset SegmentationDataset( num_samples200, image_size128, num_classes5, transformtrain_transform, is_trainTrue ) test_dataset SegmentationDataset( num_samples40, image_size128, num_classes5, transformNone, is_trainFalse ) train_loader DataLoader(train_dataset, batch_size16, shuffleTrue, num_workers0) test_loader DataLoader(test_dataset, batch_size16, shuffleFalse, num_workers0) # 简单的分割模型 class SimpleSegModel(nn.Module): def __init__(self, num_classes5): super().__init__() # 编码器 self.enc1 self._block(3, 32) self.enc2 self._block(32, 64) self.enc3 self._block(64, 128) # 解码器 self.dec3 self._block(128, 64) self.dec2 self._block(64, 32) self.final nn.Conv2d(32, num_classes, 1) self.pool nn.MaxPool2d(2) self.upsample nn.Upsample(scale_factor2, modebilinear, align_cornersFalse) def _block(self, in_c, out_c): return nn.Sequential( nn.Conv2d(in_c, out_c, 3, padding1), nn.BatchNorm2d(out_c), nn.ReLU(inplaceTrue), ) def forward(self, x): e1 self.enc1(x) e2 self.enc2(self.pool(e1)) e3 self.enc3(self.pool(e2)) d3 self.dec3(e3) d2 self.dec2(self.upsample(d3) e2) out self.final(self.upsample(d2) e1) return out device torch.device(cuda if torch.cuda.is_available() else cpu) model SimpleSegModel(num_classes5).to(device) criterion nn.CrossEntropyLoss(ignore_index255) optimizer torch.optim.Adam(model.parameters(), lr0.001) # 训练循环 num_epochs 10 for epoch in range(num_epochs): model.train() train_loss 0.0 for images, masks in train_loader: images images.to(device) masks masks.to(device) optimizer.zero_grad() outputs model(images) loss criterion(outputs, masks) loss.backward() optimizer.step() train_loss loss.item() train_loss / len(train_loader) if (epoch 1) % 5 0: print(f Epoch [{epoch1}/{num_epochs}] - Loss: {train_loss:.4f}) print( Training completed!) # 验证变换一致性 print(\n Verifying transform consistency on trained data...) model.eval() with torch.no_grad(): for images, masks in test_loader: images images.to(device) masks masks.to(device) outputs model(images) preds outputs.argmax(dim1) # 计算像素准确率 correct (preds masks).float().mean().item() print(f Pixel accuracy: {correct:.4f}) break # # 5. 多图像同步变换视频/立体视觉 # class MultiImageTransform: 对多张相关图像应用相同的随机变换。 适用于视频帧、立体视觉图像对等场景。 def __init__(self, crop_size256, hflip_prob0.5, rotation_degrees10): self.crop_size crop_size self.hflip_prob hflip_prob self.rotation_degrees rotation_degrees def __call__(self, images): 对一组图像应用相同的变换。 参数: images: PIL Image 列表或张量列表 返回: 变换后的图像列表 if not images: return images # 使用第一张图像生成随机参数 first images[0] # 随机裁剪参数 i, j, h, w transforms.RandomCrop.get_params( first, output_size(self.crop_size, self.crop_size) ) # 随机翻转决策 do_hflip random.random() self.hflip_prob # 随机旋转角度 angle transforms.RandomRotation.get_params( [-self.rotation_degrees, self.rotation_degrees] ) if self.rotation_degrees 0 else 0 # 对所有图像应用相同的变换 transformed [] for img in images: # 裁剪 img TF.crop(img, i, j, h, w) # 翻转 if do_hflip: img TF.hflip(img) # 旋转 if angle ! 0: img TF.rotate(img, angle, fill0) # 转张量 img TF.to_tensor(img) transformed.append(img) return transformed # 演示多图像变换 def demo_multi_image_transform(): 演示多图像同步变换 print(\n * 60) print(Multi-Image Synchronized Transform Demo) print( * 60) # 模拟视频帧序列 frames [ Image.fromarray(np.random.randint(0, 256, (256, 256, 3), dtypenp.uint8)) for _ in range(5) ] multi_transform MultiImageTransform(crop_size128, hflip_prob1.0, rotation_degrees15) transformed_frames multi_transform(frames) print(fNumber of frames: {len(transformed_frames)}) for i, frame in enumerate(transformed_frames): print(f Frame {i}: {frame.shape}) # 验证所有帧应用了相同的变换 # 检查它们之间的差异是否保持一致因为原始帧不同但变换相同 print(\nAll frames transformed with the same random parameters!) # # 运行 # if __name__ __main__: verify_transform_consistency() train_segmentation() demo_multi_image_transform()常见陷阱与注意事项1. 插值模式的选择图像和掩码应使用不同的插值模式# 图像双线性插值平滑 image TF.resize(image, size, interpolationtransforms.InterpolationMode.BILINEAR) # 掩码最近邻插值保持类别标签的整数值 mask TF.resize(mask, size, interpolationtransforms.InterpolationMode.NEAREST)如果对掩码使用双线性插值会产生非整数的像素值导致类别标签混乱。2. 填充值的选择旋转等变换会产生边界区域需要选择合适的填充值# 图像填充 0黑色 image TF.rotate(image, angle, fill0) # 掩码填充 255忽略索引 mask TF.rotate(mask, angle, fill255) # 在损失函数中使用 ignore_index255 忽略这些区域 criterion nn.CrossEntropyLoss(ignore_index255)3. 随机种子控制如果需要可重复的结果设置随机种子import random import torch import numpy as np def set_seed(seed): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) set_seed(42)4. ColorJitter 的同步问题如果对多光谱图像或需要颜色一致性的图像对使用ColorJitter需要同步颜色变换参数# 生成一次颜色抖动参数 brightness random.uniform(0.8, 1.2) contrast random.uniform(0.8, 1.2) # 应用到所有图像 for img in images: img TF.adjust_brightness(img, brightness) img TF.adjust_contrast(img, contrast)5. RandomErasing 的处理RandomErasing通常只应用于输入图像不应用于掩码# 仅对图像应用 RandomErasing if random.random() erase_prob: image TF.erase(image, *erase_params) # 不对 mask 应用 erase6. TensorBoard 可视化验证在训练过程中使用 TensorBoard 可视化变换后的图像和掩码确保它们空间对齐from torch.utils.tensorboard import SummaryWriter writer SummaryWriter() # 记录变换后的图像和掩码 images, masks next(iter(train_loader)) writer.add_image(Transformed/Image, images[0], 0) writer.add_image(Transformed/Mask, masks[0].unsqueeze(0).float() / masks.max(), 0)总结对多个相关图像应用相同的随机变换是计算机视觉任务中的常见需求。使用torchvision.transforms.functional模块可以精确控制变换参数确保空间一致性。关键要点总结问题本质torchvision.transforms中的随机变换每次调用生成不同的随机参数对图像和掩码分别调用会导致空间位置不一致。推荐方案使用torchvision.transforms.functionalTF模块先生成随机参数再将相同参数应用到所有相关图像。需要同步的变换所有几何变换裁剪、翻转、旋转、仿射、透视必须同步。像素变换颜色抖动、模糊可以独立应用。插值模式图像使用双线性插值掩码使用最近邻插值避免掩码产生非整数标签。填充值图像填充 0掩码填充 255或其他忽略索引在损失函数中用ignore_index忽略。替代方案Albumentations库原生支持多目标同步变换是torchvision的良好替代。验证通过可视化或数值检查确保变换后的图像和掩码空间对齐。通过正确实现同步变换你可以确保数据增强不会破坏图像和标签之间的对应关系从而提高模型的训练效果。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑