Implémentation d'un système de détection des maladies des plantes avec YOLOv11 et CSWin Transformer

Contexte et Enjeux Agricoles

La surveillence phytosanitaire constitue un pilier fondamental pour garantir la sécurité alimentaire et optimiser les rendements agricoles à l'échelle mondiale. Les pathologies végétales compromettent non seulement la croissance des cultures, mais perturbent également les écosystèmes locaux. L'identification rapide et précise des infections permet de déployer des stratégies d'éradication ciblées. Historiquement, le diagnostic reposait sur l'inspection visuelle par des experts, une méthode chronophage et sujette à des biais cognitifs. L'avènement de la vision par ordinateur, et plus spécifiquement des réseaux de neurones convolutifs profonds, a radicalement transformé ce paradigme.

Cette architecture s'appuie sur une version optimisée du modèle YOLOv11, intégrant des mécanismes d'attention avancés tels que le CSWin Transformer et des convolutions adaptatives (RFAConv). Le système est conçu pour analyser des flux d'images et catégoriser automatiquement l'état de santé des cultures, offrant ainsi une aide à la décision en temps réel pour les agronomes.

Présentation du Jeu de Données

Le corpus d'entraînement utilisé pour cette implémentation se compose de 4100 images annotées avec précision. Afin d'assurer une couverture spectrale optimale des infections fongiques, les données sont réparties en trois classes distinctes :

  • Early_Blight : Symptômes précoces de mildiou.
  • Healthy : Tissus foliaires sains.
  • Late_Blight : Stade avancé de l'infection.

La collecte intègre des échantillons capturés sous diverses conditions d'éclairage, d'humidité et à différents stades phénologiques. Cette variabilité contraint le modèle à extraire des caractéristiques robustes. En phase de prétraitement, un pipeline d'augmentation de données applique des transformations affines (rotations, mises à l'échelle, symétries axiales) pour enrichir l'espace latent et prévenir le surapprentissage (overfitting).

Architecture du Modèle : Intégration de CSWin Transformer

Pour capturer les dépendances à longue portée au sein des images de feuilles, l'architecture intègre un module de type Transformer à fenêtres en forme de croix (Cross-Shaped Window). Ce composant fusionne l'efficacité des réseaux convolutifs avec la capacité de modélisation globale des mécanismes d'auto-attention.

Voici l'implémentation restructurée du bloc d'attentino et du perecptron multicouche :


import torch
import torch.nn as nn

class MultiLayerPerceptron(nn.Module):
    """Bloc de perceptron multicouche avec régularisation par Dropout."""
    def __init__(self, input_dim: int, latent_dim: int = None, output_dim: int = None, activation_fn=nn.GELU, dropout_rate: float = 0.0):
        super().__init__()
        latent_dim = latent_dim or input_dim
        output_dim = output_dim or input_dim
        
        self.projection_1 = nn.Linear(input_dim, latent_dim)
        self.non_linearity = activation_fn()
        self.projection_2 = nn.Linear(latent_dim, output_dim)
        self.regularizer = nn.Dropout(dropout_rate)

    def forward(self, tensor_input: torch.Tensor) -> torch.Tensor:
        out = self.projection_1(tensor_input)
        out = self.non_linearity(out)
        out = self.regularizer(out)
        out = self.projection_2(out)
        return self.regularizer(out)


class CrossShapedWindowBlock(nn.Module):
    """Bloc fondamental combinant attention locale et traitement MLP."""
    def __init__(self, embed_dim: int, num_heads: int, mlp_expansion: float = 4.0, drop_rate: float = 0.0, attn_drop_rate: float = 0.0):
        super().__init__()
        self.embed_dim = embed_dim
        self.num_heads = num_heads
        
        self.qkv_mapping = nn.Linear(embed_dim, embed_dim * 3)
        self.norm_layer_1 = nn.LayerNorm(embed_dim)
        
        # L'implémentation exacte de LePEAttention est requise ici
        # self.attention_mechanism = LePEAttention(embed_dim, num_heads=num_heads, attn_drop=attn_drop_rate)
        
        hidden_mlp_dim = int(embed_dim * mlp_expansion)
        self.feed_forward = MultiLayerPerceptron(
            input_dim=embed_dim, 
            latent_dim=hidden_mlp_dim, 
            output_dim=embed_dim, 
            dropout=drop_rate
        )
        self.norm_layer_2 = nn.LayerNorm(embed_dim)
        self.drop_path = nn.Identity() # Placeholder pour DropPath

    def forward(self, feature_map: torch.Tensor) -> torch.Tensor:
        batch_size = feature_map.shape[0]
        normalized_input = self.norm_layer_1(feature_map)
        
        qkv = self.qkv_mapping(normalized_input).reshape(batch_size, -1, 3, self.embed_dim).permute(2, 0, 1, 3)
        
        # attn_output = self.attention_mechanism(qkv)
        # feature_map = feature_map + self.drop_path(attn_output)
        # feature_map = feature_map + self.drop_path(self.feed_forward(self.norm_layer_2(feature_map)))
        
        return feature_map


class CSWinArchitecture(nn.Module):
    """Architecture globale du CSWin Transformer pour l'extraction de caractéristiques."""
    def __init__(self, img_resolution: int = 640, in_channels: int = 3, num_classes: int = 1000, embed_dim: int = 96, layer_depths: list = [2, 2, 6, 2], num_heads: int = 12):
        super().__init__()
        self.num_classes = num_classes
        self.embed_dim = embed_dim
        
        self.initial_embedding = nn.Sequential(
            nn.Conv2d(in_channels, embed_dim, kernel_size=7, stride=4, padding=2),
            nn.LayerNorm(embed_dim)
        )
        
        self.stage_1_blocks = nn.ModuleList([
            CrossShapedWindowBlock(embed_dim=embed_dim, num_heads=num_heads) 
            for _ in range(layer_depths[0])
        ])

    def forward(self, input_tensor: torch.Tensor) -> torch.Tensor:
        x = self.initial_embedding(input_tensor)
        for block in self.stage_1_blocks:
            x = block(x)
        return x

# Exemple d'instanciation
if __name__ == '__main__':
    dummy_input = torch.randn((1, 3, 640, 640))
    vision_model = CSWinArchitecture()
    output_features = vision_model(dummy_input)
    print(f"Dimensions de sortie : {output_features.size()}")

Tête de Détection Dynamique (DyHead)

Pour la prédiction finale des coordonnées de boîtes englobantes et des probabilités de classe, le modèle utilise une tête de détection dynamique. Cette composante, adaptée de l'architecture YOLOv8/v11, aligne les caractéristiques à différentes échelles spatiales en unifiant l'attention à l'échelle, l'attention spatiale et l'attention axée sur la tâche.


import torch
import torch.nn as nn
import math
# from ultralytics.utils.tal import dist2bbox, make_anchors

class DynamicDetectionHead(nn.Module):
    """Tête de détection intégrant un mécanisme d'attention dynamique."""

    def __init__(self, num_classes: int = 80, hidden_channels: int = 256, num_blocks: int = 2, input_channels: tuple = ()):
        super().__init__()
        self.num_classes = num_classes
        self.num_layers = len(input_channels)
        self.reg_max = 16
        self.outputs_per_anchor = num_classes + self.reg_max * 4
        self.strides = torch.zeros(self.num_layers)
        
        c2 = max(16, input_channels[0] // 4, self.reg_max * 4)
        c3 = max(input_channels[0], self.num_classes)
        
        # self.feature_convs = nn.ModuleList(nn.Sequential(Conv(ch, hidden_channels, 1)) for ch in input_channels)
        # self.dyhead_blocks = nn.Sequential(*[DyHeadBlock(hidden_channels) for _ in range(num_blocks)])
        
        self.box_predictors = nn.ModuleList(
            nn.Sequential(
                # Conv(hidden_channels, c2, 3), Conv(c2, c2, 3), 
                nn.Conv2d(c2, 4 * self.reg_max, 1)
            ) for _ in input_channels
        )
        
        self.class_predictors = nn.ModuleList(
            nn.Sequential(
                # DWConv(hidden_channels, ch, 3), Conv(ch, c3, 1),
                # DWConv(c3, c3, 3), Conv(c3, c3, 1),
                nn.Conv2d(c3, self.num_classes, 1)
            ) for ch in input_channels
        )
        
        # self.dfl = DFL(self.reg_max) if self.reg_max > 1 else nn.Identity()

    def forward(self, feature_pyramid: list) -> torch.Tensor:
        for idx in range(self.num_layers):
            feature_pyramid[idx] = self.feature_convs[idx](feature_pyramid[idx])
            
        # feature_pyramid = self.dyhead_blocks(feature_pyramid)
        tensor_shape = feature_pyramid[0].shape
        
        for idx in range(self.num_layers):
            box_out = self.box_predictors[idx](feature_pyramid[idx])
            cls_out = self.class_predictors[idx](feature_pyramid[idx])
            feature_pyramid[idx] = torch.cat((box_out, cls_out), dim=1)
            
        if self.training:
            return feature_pyramid
            
        # Logique d'inférence et de décodage
        # if self.dynamic or self.current_shape != tensor_shape:
        #     self.anchors, self.strides = (x.transpose(0, 1) for x in make_anchors(feature_pyramid, self.strides, 0.5))
        #     self.current_shape = tensor_shape

        concatenated_output = torch.cat([feat.view(tensor_shape[0], self.outputs_per_anchor, -1) for feat in feature_pyramid], dim=2)
        box_predictions, class_predictions = concatenated_output.split((self.reg_max * 4, self.num_classes), dim=1)
        
        # decoded_boxes = dist2bbox(self.dfl(box_predictions), self.anchors.unsqueeze(0), xywh=True, dim=1) * self.strides
        # final_output = torch.cat((decoded_boxes, class_predictions.sigmoid()), dim=1)
        
        return concatenated_output

    def initialize_biases(self):
        """Initialisation spécifique des biais pour accélérer la convergence."""
        for box_layer, cls_layer, stride in zip(self.box_predictors, self.class_predictors, self.strides):
            box_layer[-1].bias.data.fill_(1.0)
            cls_layer[-1].bias.data[:self.num_classes] = math.log(5 / self.num_classes / (640 / stride) ** 2)

Cette configuration permet au réseau de traiter simultanément la régression de boîtes et la classification sémantique avec un coût computationnel réduit. L'initialisation mathématique des biais garantit que les prédictions initiales sont calibrées sur la distribution spatiale des ancres, stabilisant ainsi les premières itérations de l'optimisation par descente de gradient.

Étiquettes: YOLOv11 cs-win-transformer vision-par-ordinateur PyTorch agriculture-intelligente

Publié le 18 septembre à 14h15