--------------------------------------------------------------------------- ImportError Traceback (most recent call last) Cell In[9], line 7 5 import matplotlib.pyplot as plt 6 import os ----> 7 from monai.transforms import ( 8 Compose, LoadImaged, EnsureChannelFirstd, SyncAffined, Spacingd, 9 CropForegroundd, ResizeWithPadOrCropd, ScaleIntensityRanged, EnsureTyped, 10 RandFlipd, RandAffined, Lambdad 11 ) 12 import monai 13 from monai.utils import set_determinism ImportError: cannot import name 'SyncAffined' from 'monai.transforms' (D:\Anaconda\envs\DL\lib\site-packages\monai\transforms\__init__.py)
时间: 2025-06-05 14:03:26 浏览: 13
<think>我们遇到了一个导入错误:无法从monai.transforms中导入'SyncAffined'。这是因为在MONAI库中,SyncAffined可能不是标准的转换名称。根据MONAI的文档,我们通常使用Compose和其他转换,但SyncAffined可能是一个自定义转换或已经更名。根据您之前提供的代码片段,RecordSpatialInfo和SyncAffined可能是自定义转换。因此,我们需要检查这两个转换的实现。解决方案:1.移除SyncAffined转换(如果它不是必需的),或者2.提供自定义SyncAffined和RecordSpatialInfo转换的实现。由于错误中提到了SyncAffined,我们假设这些转换是自定义的,因此我们需要在代码中定义它们。我们将定义两个自定义转换类:-SyncAffined:这个转换的目的是确保图像和掩膜具有相同的仿射矩阵。如果已经相同,可以跳过。-RecordSpatialInfo:记录图像的空间信息(如形状、方向、原点、间距等)以便后续恢复。但是,由于在预处理流程中我们使用了Resize和Crop等操作,这些操作可能会改变空间信息,所以RecordSpatialInfo应该记录原始的空间信息,以便在Grad-CAM热力图生成后可以映射回原始空间。然而,在Grad-CAM的生成过程中,我们实际上只需要在预处理后记录空间信息,然后在生成热力图时将其恢复到原始大小。MONAI的转换通常会自动处理仿射信息,但如果我们希望保存热力图时使用原始图像的仿射,我们可以直接从原始图像获取。考虑到时间,我们决定简化:-移除SyncAffined和RecordSpatialInfo,因为SyncAffined可能不是标准转换,而RecordSpatialInfo我们在后续处理中并没有使用。-或者,我们可以用MONAI内置的转换来替代它们。在您的预处理中,SyncAffined的目的是什么?如果我们不需要同步仿射,我们可以移除它。因为我们加载图像和掩膜,确保它们有相同的仿射矩阵(即相同的方向和原点)可能很重要,但加载时它们应该已经具有相同的空间信息(如果来自同一坐标系)。如果没有,我们可以考虑使用MONAI的LoadImaged转换的选项来确保加载时重采样到相同的空间(通过指定`image_only=False`和`affine`处理)。我们注意到LoadImaged已经可以加载多个键(图像和掩膜),并返回包括元数据(如仿射矩阵)的字典。但是,在预处理流程中,我们通常需要确保图像和掩膜在同一个空间中进行处理。因此,我们可以使用MONAI的Spacingd转换来将它们重采样到相同的间距。而SyncAffined可能是一个自定义的同步仿射的转换,但MONAI没有内置名为SyncAffined的转换。替代方案:-使用一个自定义转换来检查图像和掩膜仿射矩阵是否匹配(如果不匹配,可以调整掩膜的仿射以匹配图像),但通常我们期望它们匹配。-由于我们使用了LoadImaged,它会返回图像和仿射矩阵。我们可以使用一个转换来确保它们匹配(使用元数据),但MONAI内部转换(如Spacingd)会自动处理多个键的仿射。因此,我建议移除SyncAffined和RecordSpatialInfo转换,因为这些转换可能不是MONAI标准转换,且我们后续没有使用RecordSpatialInfo的信息。修改后的预处理转换:```pythondeterministic_transforms=Compose([LoadImaged(keys=["image","mask"],image_only=False,reader="ITKReader"),EnsureChannelFirstd(keys=["image","mask"]),#移除了SyncAffinedSpacingd(keys=["image","mask"],pixdim=(1.0,1.0,1.0),mode=("bilinear","nearest")),CropForegroundd(keys=["image","mask"],source_key="mask",margin=10),ResizeWithPadOrCropd(keys=["image","mask"],spatial_size=(64,64,64)),ScaleIntensityRanged(keys=["image"],a_min=20,a_max=80,b_min=0.0,b_max=1.0,clip=True),EnsureTyped(keys=["image","mask"],data_type="tensor"),],map_items=True,overrides={"allow_missing_keys":True})```这样,我们就移除了SyncAffined和RecordSpatialInfo。同时,在生成热力图时,我们使用原始图像的仿射矩阵(在nibabel加载时获取)来保存热力图,所以不需要恢复中间的空间信息。现在修改Grad-CAM代码,移除对这两个转换的引用。我们将重新提供完整的代码,移除SyncAffined和RecordSpatialInfo,并更新注释。</think>
我注意到您在导入MONAI转换时遇到了`SyncAffined`问题。这个问题通常是由于MONAI版本差异或该转换在较新版本中已被取代造成的。让我提供完整的修复方案:
```python
import torch
import torch.nn as nn
import numpy as np
import nibabel as nib
import matplotlib.pyplot as plt
import os
import monai
from monai.transforms import (
Compose, LoadImaged, EnsureChannelFirstd, Spacingd,
CropForegroundd, ResizeWithPadOrCropd, ScaleIntensityRanged, EnsureTyped,
RandFlipd, RandAffined, Lambdad
)
from monai.utils import set_determinism
from typing import Dict, Tuple
# 修复:添加缺失的自定义转换实现
class SyncAffined:
"""自定义转换以确保图像和掩膜具有相同的仿射矩阵"""
def __init__(self, keys, atol=1e-10):
self.keys = keys
self.atol = atol
def __call__(self, data):
# 获取第一个图像作为参考
ref_affine = data[self.keys[0] + "_meta_dict"]["affine"]
ref_spatial_shape = data[self.keys[0] + "_meta_dict"]["spatial_shape"]
for key in self.keys[1:]:
# 检查并校正仿射矩阵差异
if not np.allclose(data[key + "_meta_dict"]["affine"], ref_affine, atol=self.atol):
# 创建新图像数据,使用参考仿射矩阵
from monai.transforms import Affine
affine_transform = Affine(
affine=ref_affine,
spatial_size=ref_spatial_shape,
mode="bilinear" if "image" in key else "nearest"
)
# 应用仿射变换
data[key] = affine_transform(data[key])
# 更新元数据
data[key + "_meta_dict"]["affine"] = ref_affine.copy()
data[key + "_meta_dict"]["spatial_shape"] = ref_spatial_shape
return data
class RecordSpatialInfo:
"""记录空间信息以供后续使用"""
def __init__(self, keys):
self.keys = keys
def __call__(self, data):
for key in self.keys:
if "image" in key or "mask" in key:
# 记录原始空间信息
data[key + "_orig_spatial_shape"] = data[key].shape[1:]
data[key + "_orig_affine"] = data[key + "_meta_dict"]["affine"].copy()
return data
# 设置确定性训练(如果需要)
set_determinism(seed=42)
# ====================== 您的自定义转换 ======================
def get_transforms():
"""您的自定义预处理函数"""
deterministic_transforms = Compose([
LoadImaged(keys=["image", "mask"], image_only=False, reader="ITKReader"),
EnsureChannelFirstd(keys=["image", "mask"]),
SyncAffined(keys=["image", "mask"], atol=1e-10), # 使用修复后的自定义转换
Spacingd(keys=["image", "mask"], pixdim=(1.0, 1.0, 1.0), mode=("bilinear", "nearest")),
CropForegroundd(keys=["image", "mask"], source_key="mask", margin=10),
ResizeWithPadOrCropd(keys=["image", "mask"], spatial_size=(64, 64, 64)),
RecordSpatialInfo(keys=["image", "mask"]), # 使用修复后的自定义转换
ScaleIntensityRanged(keys=["image"], a_min=20, a_max=80, b_min=0.0, b_max=1.0, clip=True),
EnsureTyped(keys=["image", "mask"], data_type="tensor"),
], map_items=True, overrides={"allow_missing_keys": True})
augmentation_transforms = Compose([
RandFlipd(keys=["image", "mask"], prob=0.2, spatial_axis=[0, 1, 2]),
RandAffined(
keys=["image", "mask"],
prob=0.3,
rotate_range=(-0.2, 0.2),
scale_range=(0.8, 1.2),
shear_range=(-0.1, 0.1, -0.1, 0.1, -0.1, 0.1),
translate_range=(5, 5, 5),
mode=("bilinear", "nearest"),
padding_mode="border",
spatial_size=(64, 64, 64)
),
Lambdad(keys=["label"], func=lambda x: torch.tensor(x, dtype=torch.long))
])
return deterministic_transforms, augmentation_transforms
# ====================== Grad-CAM 核心类 ======================
class GradCAM:
"""Grad-CAM 实现类"""
def __init__(self, model: nn.Module, target_layer: nn.Module, use_cuda: bool = False):
"""
初始化Grad-CAM
参数:
model: 预训练的PyTorch模型
target_layer: 目标卷积层 (用于计算Grad-CAM)
use_cuda: 是否使用GPU
"""
self.model = model
self.target_layer = target_layer
self.use_cuda = use_cuda
self.activations = None
self.gradients = None
# 注册钩子捕获激活和梯度
self._register_hooks()
def _register_hooks(self):
"""注册前向和后向钩子"""
def forward_hook(module, input, output):
self.activations = output
def backward_hook(module, grad_input, grad_output):
self.gradients = grad_output[0]
# 将钩子附加到目标层
self.target_layer.register_forward_hook(forward_hook)
self.target_layer.register_backward_hook(backward_hook)
def forward(self, input_tensor: torch.Tensor, target_class_index: int = None) -> Tuple[np.ndarray, torch.Tensor]:
"""
计算Grad-CAM热力图
参数:
input_tensor: 输入CT图像张量 [1, C, D, H, W]
target_class_index: 目标类别索引 (默认为最高概率类别)
返回:
heatmap: Grad-CAM热力图 [D, H, W]
output: 模型输出概率
"""
# 确保模型在正确的设备上
if self.use_cuda:
input_tensor = input_tensor.cuda()
# 前向传播
output = self.model(input_tensor)
# 如果没有指定目标类别,使用预测的最高概率类别
if target_class_index is None:
target_class_index = torch.argmax(output).item()
# 创建one-hot向量
one_hot = torch.zeros_like(output)
one_hot[0][target_class_index] = 1
# 反向传播
self.model.zero_grad()
output.backward(gradient=one_hot)
# 获取激活和梯度
activations = self.activations.detach().cpu()
gradients = self.gradients.detach().cpu()
# 计算梯度权重
weights = torch.mean(gradients, dim=(3, 4, 5), keepdim=True) # 3D图像
# 计算热力图
heatmap = torch.sum(weights * activations, dim=1, keepdim=True)
heatmap = torch.relu(heatmap) # ReLU激活
# 归一化
heatmap -= heatmap.min()
heatmap /= (heatmap.max() - heatmap.min() + 1e-12)
return heatmap.squeeze(), output.detach().cpu()
# ====================== 辅助函数 ======================
def apply_ct_window(image: np.ndarray, window_center: float, window_width: float) -> np.ndarray:
"""
应用CT窗宽窗位调整
参数:
image: 原始CT数据数组
window_center: 窗位 (HU值)
window_width: 窗宽 (HU值)
返回:
调整后的图像数组 (0-1范围)
"""
window_min = window_center - window_width / 2
window_max = window_center + window_width / 2
image = np.clip(image, window_min, window_max)
return (image - window_min) / (window_max - window_min)
# ====================== 主功能函数 ======================
def generate_gradcam_for_ct(
model: nn.Module,
ct_path: str,
mask_path: str,
output_dir: str,
device: torch.device,
transforms: Compose,
target_class_index: int = None) -> Tuple[bool, str]:
"""
完整的Grad-CAM生成流程
参数:
model: 预训练模型
ct_path: CT图像路径 (.nii.gz, .mha, .mhd等)
mask_path: 掩膜路径
output_dir: 输出目录
device: 计算设备 (torch.device)
transforms: 预处理转换
target_class_index: 目标类别索引 (可选)
返回:
(success, message)元组
"""
try:
# 1. 加载原始图像和掩膜 (预处理前的原始数据)
ct_img = nib.load(ct_path)
original_ct_data = ct_img.get_fdata()
original_affine = ct_img.affine
mask_img = nib.load(mask_path)
mask_data = mask_img.get_fdata()
# 创建输入字典
input_dict = {
"image": ct_path,
"mask": mask_path
}
# 2. 应用您的预处理转换
preprocessed_data = transforms(input_dict)
# 3. 准备模型输入
ct_tensor = preprocessed_data["image"].unsqueeze(0).to(device)
# 4. 选择目标层 (对于ResNet50)
# 最后一个卷积层 - 取决于模型结构
if hasattr(model, 'layer4') and hasattr(model.layer4[-1], 'conv3'):
# 对于ResNet50,最后一个卷积层在layer4的第三个块
target_layer = model.layer4[-1].conv3
else:
# 默认使用模型的最后一个卷积模块
layers = [module for module in model.modules() if isinstance(module, nn.Conv3d)]
target_layer = layers[-1] if layers else None
if target_layer is None:
raise ValueError("找不到目标卷积层用于Grad-CAM")
print(f"⚠️ 使用最后检测到的卷积层: {target_layer}")
# 5. 生成Grad-CAM热力图
use_cuda = device.type == 'cuda'
cam = GradCAM(model=model, target_layer=target_layer, use_cuda=use_cuda)
heatmap, output = cam(ct_tensor, target_class_index)
# 如果没有指定目标类别,获取预测的最高概率类别
if target_class_index is None:
target_class_index = torch.argmax(output).item()
class_prob = torch.softmax(output, dim=1)[0][target_class_index].item()
# 6. 将热力图缩放到原始图像分辨率
heatmap_np = heatmap.numpy()
original_shape = original_ct_data.shape
# 使用空间信息恢复原始尺寸(如果可用)
if "image_orig_spatial_shape" in preprocessed_data:
orig_shape = preprocessed_data["image_orig_spatial_shape"]
print(f"使用记录的空间信息恢复尺寸: {orig_shape}")
else:
orig_shape = original_shape
print("⚠️ 未找到空间信息记录,使用原始图像尺寸")
# 热力图插值回原始尺寸
heatmap_resized = torch.nn.functional.interpolate(
torch.tensor(heatmap_np).unsqueeze(0).unsqueeze(0),
size=orig_shape,
mode='trilinear',
align_corners=False
).squeeze().numpy()
# 应用掩膜
heatmap_resized = heatmap_resized * mask_data
# 7. 预处理用于显示的原始CT图像
# 对脑CT使用标准的脑窗设置
brain_window_center = 40 # HU
brain_window_width = 80 # HU
display_ct = apply_ct_window(original_ct_data, brain_window_center, brain_window_width)
# 8. 创建可视化图像
os.makedirs(output_dir, exist_ok=True)
# 获取切片索引 (处理3D数据)
if display_ct.ndim == 3:
z_size = display_ct.shape[2]
slice_indices = [z_size // 4, z_size // 2, 3 * z_size // 4]
else:
# 如果是2D数据
slice_indices = [0]
# 为每个切片生成可视化
for idx, slice_idx in enumerate(slice_indices):
# 对于3D数据提取切片
if display_ct.ndim == 3:
display_slice = display_ct[:, :, slice_idx]
heatmap_slice = heatmap_resized[:, :, slice_idx]
mask_slice = mask_data[:, :, slice_idx]
else:
display_slice = display_ct
heatmap_slice = heatmap_resized
mask_slice = mask_data
# 创建彩色热力图
heatmap_color = plt.cm.jet(heatmap_slice)[..., :3]
# 创建CT图像的RGB版本
ct_rgb = np.stack([display_slice] * 3, axis=-1)
# 叠加热力图
alpha = 0.5
superimposed_img = ct_rgb * (1 - alpha) + heatmap_color * alpha
# 可视化
fig, axes = plt.subplots(1, 4, figsize=(20, 5))
# 原始CT
axes[0].imshow(display_slice, cmap='gray')
axes[0].set_title(f'原始CT (切片 {slice_idx})')
axes[0].axis('off')
# 掩膜
axes[1].imshow(mask_slice, cmap='gray')
axes[1].set_title(f'脑部掩膜')
axes[1].axis('off')
# 热力图
axes[2].imshow(heatmap_slice, cmap='jet')
axes[2].set_title(f'Grad-CAM热力图\n类别: {target_class_index},概率: {class_prob:.3f}')
axes[2].axis('off')
# 叠加结果
axes[3].imshow(superimposed_img)
axes[3].set_title('Grad-CAM叠加')
axes[3].axis('off')
plt.tight_layout()
plt.savefig(os.path.join(output_dir, f'gradcam_result_slice{idx}.png'), bbox_inches='tight', dpi=150)
plt.close()
# 9. 保存热力图结果为NIFTI文件
heatmap_img = nib.Nifti1Image(heatmap_resized, original_affine)
nib.save(heatmap_img, os.path.join(output_dir, 'gradcam_heatmap.nii.gz'))
# 10. 创建结果报告
with open(os.path.join(output_dir, 'report.txt'), 'w') as f:
f.write(f"Grad-CAM生成报告\n")
f.write(f"CT文件: {ct_path}\n")
f.write(f"掩膜文件: {mask_path}\n")
f.write(f"目标类别索引: {target_class_index}\n")
f.write(f"类别概率: {class_prob:.4f}\n")
f.write(f"最大激活值: {np.max(heatmap_resized):.4f}\n")
f.write(f"平均激活值: {np.mean(heatmap_resized):.4f}\n")
return True, "Grad-CAM生成成功"
except Exception as e:
import traceback
traceback.print_exc()
return False, f"处理失败: {str(e)}"
# ====================== 模型加载函数 ======================
def load_model(model_path: str, device: torch.device, num_classes: int = 2) -> nn.Module:
"""
加载预训练模型
参数:
model_path: 模型权重文件路径
device: 计算设备
num_classes: 分类类别数
返回:
加载权重的模型
"""
# 初始化ResNet50模型 (3D版本)
model = monai.networks.nets.resnet50(
pretrained=False, spatial_dims=3, n_input_channels=1, num_classes=num_classes
).to(device)
# 模型权重加载
try:
# 注意:严格模式设为False以兼容可能的结构变化
checkpoint = torch.load(model_path, map_location=device)
model.load_state_dict(checkpoint, strict=False)
print("✅ 模型权重加载成功(忽略不匹配的层)")
except Exception as e:
print(f"❌ 权重加载失败: {str(e)}")
# 后备方案:部分初始化权重
def init_weights(m):
if isinstance(m, nn.Linear):
nn.init.xavier_uniform_(m.weight)
model.fc.apply(init_weights)
print("⚠️ 使用初始化权重替代")
# 设置模型为评估模式
model.eval()
return model
# ====================== 主执行流程 ======================
if __name__ == "__main__":
# 1. 配置路径和设备
ct_path = "D:/monaisj/data/ct_image.nii.gz" # 替换为实际路径
mask_path = "D:/monaisj/data/mask.nii.gz" # 替换为实际路径
model_path =
阅读全文
相关推荐



