| """
|
| DINOv3 + DeepLabV3+ 网络架构
|
| 用于浒苔分割任务
|
| """
|
| import torch
|
| import torch.nn as nn
|
| import torch.nn.functional as F
|
| from typing import Optional, List
|
| import numpy as np
|
| from pathlib import Path
|
|
|
|
|
| import sys
|
| sys.path.append(str(Path(__file__).resolve().parent / 'dinov3-main'))
|
| from dinov3.hub.backbones import dinov3_vitl16, Weights
|
|
|
| class ASPPModule(nn.Module):
|
| """Atrous Spatial Pyramid Pooling模块"""
|
|
|
| def __init__(self, in_channels: int, out_channels: int = 256, rates: List[int] = [6, 12, 18]):
|
| super().__init__()
|
|
|
|
|
| self.conv1x1 = nn.Sequential(
|
| nn.Conv2d(in_channels, out_channels, 1, bias=False),
|
| nn.BatchNorm2d(out_channels),
|
| nn.ReLU(inplace=True)
|
| )
|
|
|
|
|
| self.atrous_convs = nn.ModuleList()
|
| for rate in rates:
|
| self.atrous_convs.append(nn.Sequential(
|
| nn.Conv2d(in_channels, out_channels, 3, padding=rate, dilation=rate, bias=False),
|
| nn.BatchNorm2d(out_channels),
|
| nn.ReLU(inplace=True)
|
| ))
|
|
|
|
|
| self.global_avg_pool = nn.Sequential(
|
| nn.AdaptiveAvgPool2d(1),
|
| nn.Conv2d(in_channels, out_channels, 1, bias=False),
|
| nn.BatchNorm2d(out_channels),
|
| nn.ReLU(inplace=True)
|
| )
|
|
|
|
|
| self.output_conv = nn.Sequential(
|
| nn.Conv2d(out_channels * (len(rates) + 2), out_channels, 1, bias=False),
|
| nn.BatchNorm2d(out_channels),
|
| nn.ReLU(inplace=True),
|
| nn.Dropout(0.5)
|
| )
|
|
|
| def forward(self, x):
|
| size = x.shape[2:]
|
|
|
|
|
| conv1x1 = self.conv1x1(x)
|
|
|
|
|
| atrous_outputs = []
|
| for atrous_conv in self.atrous_convs:
|
| atrous_outputs.append(atrous_conv(x))
|
|
|
|
|
| global_feat = self.global_avg_pool(x)
|
| global_feat = F.interpolate(global_feat, size=size, mode='bilinear', align_corners=False)
|
|
|
|
|
| concat_feat = torch.cat([conv1x1] + atrous_outputs + [global_feat], dim=1)
|
|
|
|
|
| output = self.output_conv(concat_feat)
|
|
|
| return output
|
|
|
| class DeepLabV3PlusDecoder(nn.Module):
|
| """DeepLabV3+解码器"""
|
|
|
| def __init__(self, low_level_channels: int, high_level_channels: int, num_classes: int):
|
| super().__init__()
|
|
|
|
|
| self.low_level_conv = nn.Sequential(
|
| nn.Conv2d(low_level_channels, 48, 1, bias=False),
|
| nn.BatchNorm2d(48),
|
| nn.ReLU(inplace=True)
|
| )
|
|
|
|
|
| self.aspp = ASPPModule(high_level_channels, out_channels=256)
|
|
|
|
|
| self.decoder_conv = nn.Sequential(
|
| nn.Conv2d(256 + 48, 256, 3, padding=1, bias=False),
|
| nn.BatchNorm2d(256),
|
| nn.ReLU(inplace=True),
|
| nn.Dropout(0.5),
|
|
|
| nn.Conv2d(256, 256, 3, padding=1, bias=False),
|
| nn.BatchNorm2d(256),
|
| nn.ReLU(inplace=True),
|
| nn.Dropout(0.1)
|
| )
|
|
|
|
|
| self.classifier = nn.Conv2d(256, num_classes, 1)
|
|
|
| def forward(self, low_level_feat, high_level_feat):
|
|
|
| low_level_feat = self.low_level_conv(low_level_feat)
|
| low_level_size = low_level_feat.shape[2:]
|
|
|
|
|
| high_level_feat = self.aspp(high_level_feat)
|
|
|
|
|
| high_level_feat = F.interpolate(
|
| high_level_feat,
|
| size=low_level_size,
|
| mode='bilinear',
|
| align_corners=False
|
| )
|
|
|
|
|
| concat_feat = torch.cat([low_level_feat, high_level_feat], dim=1)
|
|
|
|
|
| decoder_feat = self.decoder_conv(concat_feat)
|
|
|
|
|
| output = self.classifier(decoder_feat)
|
|
|
| return output
|
|
|
| class DinoV3DeepLabV3Plus(nn.Module):
|
| """DINOv3 + DeepLabV3+ 用于浒苔分割"""
|
|
|
| def __init__(self, num_classes=2, backbone_name='dinov3_vitl16',
|
| pretrained=True, weights='SAT493M', use_4channel=True, freeze_backbone=False):
|
| """
|
| Args:
|
| num_classes: 类别数(浒苔/背景)
|
| backbone_name: DINOv3 backbone名称
|
| pretrained: 是否使用预训练权重
|
| weights: 预训练权重类型
|
| use_4channel: 是否使用4通道输入
|
| freeze_backbone: 是否冻结backbone参数
|
| """
|
| super().__init__()
|
|
|
| self.num_classes = num_classes
|
| self.use_4channel = use_4channel
|
| self.backbone_name = backbone_name
|
| self.freeze_backbone = freeze_backbone
|
|
|
|
|
| if backbone_name == 'dinov3_vitl16':
|
| try:
|
|
|
| if isinstance(weights, str):
|
| if weights.lower() == 'sat493m':
|
| weights_enum = Weights.SAT493M
|
| elif weights.lower() == 'lvd1689m':
|
| weights_enum = Weights.LVD1689M
|
| else:
|
|
|
| weights_enum = weights
|
| else:
|
| weights_enum = weights
|
|
|
|
|
| self.backbone = dinov3_vitl16(pretrained=pretrained, weights=weights_enum)
|
| except Exception as e:
|
| print(f"Warning: Failed to load DINOv3 backbone with error: {e}")
|
| print("Creating backbone without pretrained weights...")
|
|
|
| self.backbone = dinov3_vitl16(pretrained=False)
|
|
|
|
|
| if use_4channel:
|
|
|
| if hasattr(self.backbone, 'patch_embed'):
|
|
|
| original_weight = self.backbone.patch_embed.proj.weight
|
|
|
| new_weight = torch.cat([original_weight, original_weight[:, :1, :, :]], dim=1)
|
|
|
| self.backbone.patch_embed.proj = nn.Conv2d(
|
| 4, original_weight.shape[0],
|
| kernel_size=self.backbone.patch_embed.proj.kernel_size,
|
| stride=self.backbone.patch_embed.proj.stride,
|
| padding=self.backbone.patch_embed.proj.padding
|
| )
|
|
|
| with torch.no_grad():
|
| self.backbone.patch_embed.proj.weight = nn.Parameter(new_weight)
|
| else:
|
| raise ValueError(f"Unsupported backbone: {backbone_name}")
|
|
|
|
|
| self.embed_dim = self.backbone.embed_dim
|
| self.patch_size = self.backbone.patch_size
|
|
|
| if hasattr(self.backbone, 'patch_embed'):
|
|
|
| if hasattr(self.backbone.patch_embed, 'img_size'):
|
| self.img_size = self.backbone.patch_embed.img_size
|
| else:
|
|
|
| self.img_size = 224
|
| else:
|
| self.img_size = 224
|
|
|
|
|
|
|
| self.feature_conv = nn.Sequential(
|
| nn.Conv2d(self.embed_dim, 256, 1, bias=False),
|
| nn.BatchNorm2d(256),
|
| nn.ReLU(inplace=True)
|
| )
|
|
|
|
|
| self.low_level_channels = 256
|
| self.high_level_channels = 256
|
|
|
|
|
| self.decoder = DeepLabV3PlusDecoder(
|
| low_level_channels=self.low_level_channels,
|
| high_level_channels=self.high_level_channels,
|
| num_classes=num_classes
|
| )
|
|
|
|
|
| self.aux_classifier = nn.Sequential(
|
| nn.Conv2d(256, 256, 3, padding=1, bias=False),
|
| nn.BatchNorm2d(256),
|
| nn.ReLU(inplace=True),
|
| nn.Dropout(0.5),
|
| nn.Conv2d(256, num_classes, 1)
|
| )
|
|
|
|
|
| if self.freeze_backbone:
|
| self._freeze_backbone()
|
|
|
| def forward(self, x):
|
| batch_size = x.shape[0]
|
| input_size = x.shape[2:]
|
|
|
|
|
| if hasattr(self.backbone, 'forward_features'):
|
|
|
| features = self.backbone.forward_features(x)
|
|
|
|
|
| if isinstance(features, dict) and 'x_norm_patchtokens' in features:
|
| patch_features = features['x_norm_patchtokens']
|
| else:
|
| patch_features = features
|
|
|
|
|
| if len(patch_features.shape) == 3:
|
|
|
| feat_h = feat_w = int(np.sqrt(patch_features.shape[1]))
|
| patch_features = patch_features.transpose(1, 2).view(
|
| batch_size, self.embed_dim, feat_h, feat_w
|
| )
|
| else:
|
|
|
| patch_features = self.backbone(x)
|
|
|
|
|
| high_level_feat = self.feature_conv(patch_features)
|
|
|
|
|
| low_level_feat = F.interpolate(
|
| high_level_feat,
|
| scale_factor=4,
|
| mode='bilinear',
|
| align_corners=False
|
| )
|
|
|
|
|
| output = self.decoder(low_level_feat, high_level_feat)
|
|
|
|
|
| output = F.interpolate(
|
| output,
|
| size=input_size,
|
| mode='bilinear',
|
| align_corners=False
|
| )
|
|
|
|
|
| if self.training:
|
| aux_output = F.interpolate(
|
| self.aux_classifier(high_level_feat),
|
| size=input_size,
|
| mode='bilinear',
|
| align_corners=False
|
| )
|
| return {'out': output, 'aux': aux_output}
|
| else:
|
| return output
|
|
|
| def get_backbone_params(self):
|
| """获取backbone参数"""
|
| return self.backbone.parameters()
|
|
|
| def get_decoder_params(self):
|
| """获取decoder参数"""
|
| decoder_params = []
|
| decoder_params.extend(self.feature_conv.parameters())
|
| decoder_params.extend(self.decoder.parameters())
|
| decoder_params.extend(self.aux_classifier.parameters())
|
| return decoder_params
|
|
|
| def _freeze_backbone(self):
|
| """冻结backbone参数"""
|
| print("冻结DINOv3 backbone参数...")
|
| for param in self.backbone.parameters():
|
| param.requires_grad = False
|
|
|
|
|
|
|
| if self.use_4channel and hasattr(self.backbone, 'patch_embed'):
|
| for param in self.backbone.patch_embed.proj.parameters():
|
| param.requires_grad = True
|
| print("保持patch_embed.proj参数可训练(4通道适配层)")
|
|
|
| print(f"已冻结{sum(1 for p in self.backbone.parameters() if not p.requires_grad)}个backbone参数")
|
|
|
| def unfreeze_backbone(self):
|
| """解冻backbone参数"""
|
| print("解冻DINOv3 backbone参数...")
|
| for param in self.backbone.parameters():
|
| param.requires_grad = True
|
| print("所有backbone参数已解冻")
|
|
|
| def get_trainable_parameters(self):
|
| """获取所有可训练参数"""
|
| return [p for p in self.parameters() if p.requires_grad]
|
|
|
| class FocalLoss(nn.Module):
|
| """Focal Loss for addressing class imbalance"""
|
|
|
| def __init__(self, alpha=1, gamma=2, ignore_index=255, reduction='mean', class_weights=None):
|
| """
|
| Args:
|
| alpha: 平衡参数(可以是标量或列表,如果是列表则按类别应用)
|
| gamma: 聚焦参数
|
| ignore_index: 忽略的索引
|
| reduction: 降维方式
|
| class_weights: 类别权重tensor,shape为(num_classes,)
|
| """
|
| super().__init__()
|
|
|
| if isinstance(alpha, (list, tuple)):
|
| self.register_buffer('alpha', torch.tensor(alpha, dtype=torch.float32))
|
| elif isinstance(alpha, torch.Tensor):
|
| self.register_buffer('alpha', alpha)
|
| else:
|
| self.alpha = alpha
|
| self.gamma = gamma
|
| self.ignore_index = ignore_index
|
| self.reduction = reduction
|
|
|
| if isinstance(class_weights, torch.Tensor):
|
| self.register_buffer('class_weights', class_weights)
|
| else:
|
| self.class_weights = class_weights
|
|
|
| def forward(self, inputs, targets):
|
| """
|
| Args:
|
| inputs: 预测值 (N, C, H, W)
|
| targets: 目标值 (N, H, W)
|
| """
|
|
|
| if isinstance(inputs, dict):
|
| inputs = inputs['out']
|
|
|
|
|
| if self.ignore_index is not None:
|
| mask = targets != self.ignore_index
|
| targets = targets[mask]
|
| inputs = inputs.permute(0, 2, 3, 1)[mask]
|
| else:
|
| inputs = inputs.permute(0, 2, 3, 1).contiguous().view(-1, inputs.size(1))
|
| targets = targets.view(-1)
|
|
|
|
|
| class_weights = self.class_weights
|
| if class_weights is not None and isinstance(class_weights, torch.Tensor):
|
| if class_weights.device != inputs.device:
|
| class_weights = class_weights.to(inputs.device)
|
|
|
|
|
| ce_loss = F.cross_entropy(inputs, targets, reduction='none', weight=class_weights)
|
| pt = torch.exp(-ce_loss)
|
|
|
|
|
| if isinstance(self.alpha, torch.Tensor):
|
| if self.alpha.device != targets.device:
|
| self.alpha = self.alpha.to(targets.device)
|
| alpha_t = self.alpha[targets]
|
| focal_loss = alpha_t * (1 - pt) ** self.gamma * ce_loss
|
| else:
|
| focal_loss = self.alpha * (1 - pt) ** self.gamma * ce_loss
|
|
|
| if self.reduction == 'mean':
|
| return focal_loss.mean()
|
| elif self.reduction == 'sum':
|
| return focal_loss.sum()
|
| else:
|
| return focal_loss
|
|
|
| class SeaweedSegmentationLoss(nn.Module):
|
| """浒苔分割专用损失函数"""
|
|
|
| def __init__(self, num_classes=2, focal_alpha=1, focal_gamma=2,
|
| dice_weight=0.5, focal_weight=1.0,
|
| background_weight=1.0, foreground_weight=2.0):
|
| """
|
| Args:
|
| num_classes: 类别数
|
| focal_alpha: Focal Loss的alpha参数(可以是标量或列表[背景权重, 前景权重])
|
| focal_gamma: Focal Loss的gamma参数
|
| dice_weight: Dice Loss的权重
|
| focal_weight: Focal Loss的权重
|
| background_weight: 背景类别的权重(类别0),用于类别权重
|
| foreground_weight: 前景类别(浒苔)的权重(类别1),用于类别权重
|
| """
|
| super().__init__()
|
|
|
|
|
| self.register_buffer('class_weights', torch.tensor([background_weight, foreground_weight], dtype=torch.float32))
|
|
|
|
|
| if isinstance(focal_alpha, (int, float)):
|
| focal_alpha = [background_weight, foreground_weight]
|
|
|
|
|
| if isinstance(focal_alpha, (list, tuple)):
|
| self.register_buffer('focal_alpha_tensor', torch.tensor(focal_alpha, dtype=torch.float32))
|
| focal_alpha_for_loss = self.focal_alpha_tensor
|
| else:
|
| focal_alpha_for_loss = focal_alpha
|
|
|
| self.focal_loss = FocalLoss(alpha=focal_alpha_for_loss, gamma=focal_gamma, class_weights=self.class_weights)
|
| self.dice_weight = dice_weight
|
| self.focal_weight = focal_weight
|
| self.num_classes = num_classes
|
|
|
| def dice_loss(self, inputs, targets, smooth=1e-6):
|
| """Dice loss - 改进版本,对前景类别给予更高权重"""
|
|
|
| if isinstance(inputs, dict):
|
| inputs = inputs['out']
|
|
|
|
|
| inputs = torch.softmax(inputs, dim=1)
|
|
|
|
|
| targets_one_hot = F.one_hot(targets, num_classes=self.num_classes)
|
| targets_one_hot = targets_one_hot.permute(0, 3, 1, 2).float()
|
|
|
|
|
| intersection = (inputs * targets_one_hot).sum(dim=(2, 3))
|
| union = inputs.sum(dim=(2, 3)) + targets_one_hot.sum(dim=(2, 3))
|
|
|
| dice_score = (2. * intersection + smooth) / (union + smooth)
|
| dice_loss_per_class = 1 - dice_score
|
|
|
|
|
|
|
| class_weights = self.class_weights
|
| if class_weights.device != dice_loss_per_class.device:
|
| class_weights = class_weights.to(dice_loss_per_class.device)
|
| weighted_dice_loss = dice_loss_per_class * class_weights.unsqueeze(0)
|
|
|
|
|
| dice_loss = weighted_dice_loss.mean()
|
|
|
| return dice_loss
|
|
|
| def forward(self, inputs, targets):
|
| """计算组合损失"""
|
| focal_loss = self.focal_loss(inputs, targets)
|
| dice_loss = self.dice_loss(inputs, targets)
|
|
|
| total_loss = self.focal_weight * focal_loss + self.dice_weight * dice_loss
|
|
|
| return {
|
| 'total_loss': total_loss,
|
| 'focal_loss': focal_loss,
|
| 'dice_loss': dice_loss
|
| }
|
|
|
|
|
| if __name__ == "__main__":
|
|
|
| print("测试DINOv3 + DeepLabV3+网络架构...")
|
|
|
|
|
| model_4ch = DinoV3DeepLabV3Plus(
|
| num_classes=2,
|
| backbone_name='dinov3_vitl16',
|
| pretrained=False,
|
| weights='SAT493M',
|
| use_4channel=True
|
| )
|
|
|
|
|
| x_4ch = torch.randn(2, 4, 512, 512)
|
| with torch.no_grad():
|
| output_4ch = model_4ch(x_4ch)
|
| if isinstance(output_4ch, dict):
|
| print(f"4通道训练模式 - 主输出形状: {output_4ch['out'].shape}")
|
| print(f"4通道训练模式 - 辅助输出形状: {output_4ch['aux'].shape}")
|
| else:
|
| print(f"4通道推理模式 - 输出形状: {output_4ch.shape}")
|
|
|
|
|
| model_3ch = DinoV3DeepLabV3Plus(
|
| num_classes=2,
|
| backbone_name='dinov3_vitl16',
|
| pretrained=False,
|
| weights='SAT493M',
|
| use_4channel=False
|
| )
|
|
|
| x_3ch = torch.randn(2, 3, 512, 512)
|
| with torch.no_grad():
|
| output_3ch = model_3ch(x_3ch)
|
| if isinstance(output_3ch, dict):
|
| print(f"3通道训练模式 - 主输出形状: {output_3ch['out'].shape}")
|
| print(f"3通道训练模式 - 辅助输出形状: {output_3ch['aux'].shape}")
|
| else:
|
| print(f"3通道推理模式 - 输出形状: {output_3ch.shape}")
|
|
|
|
|
| print("\n测试损失函数...")
|
| criterion = SeaweedSegmentationLoss(num_classes=2)
|
|
|
|
|
| pred = torch.randn(2, 2, 512, 512)
|
| target = torch.randint(0, 2, (2, 512, 512))
|
|
|
| losses = criterion(pred, target)
|
| print(f"总损失: {losses['total_loss'].item():.4f}")
|
| print(f"Focal损失: {losses['focal_loss'].item():.4f}")
|
| print(f"Dice损失: {losses['dice_loss'].item():.4f}")
|
|
|
| print("\n网络架构测试完成!")
|
|
|