413 lines
15 KiB
Python
413 lines
15 KiB
Python
"""
|
||
Dialogue de correction pour permettre à l'utilisateur de corriger
|
||
les détections incorrectes et spécifier l'élément UI correct
|
||
"""
|
||
|
||
from PyQt5.QtWidgets import (
|
||
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
|
||
QListWidget, QListWidgetItem, QWidget, QTextEdit, QGroupBox
|
||
)
|
||
from PyQt5.QtCore import Qt, QRect, pyqtSignal
|
||
from PyQt5.QtGui import QFont, QPainter, QColor, QPen, QPixmap, QImage
|
||
from typing import Optional, Dict, Any, List, Tuple
|
||
import logging
|
||
import numpy as np
|
||
|
||
|
||
class CorrectionDialog(QDialog):
|
||
"""
|
||
Dialogue permettant à l'utilisateur de corriger une détection incorrecte
|
||
en sélectionnant l'élément UI correct parmi les détections alternatives
|
||
ou en cliquant directement sur l'écran
|
||
"""
|
||
|
||
# Signal émis quand une correction est effectuée
|
||
correction_made = pyqtSignal(dict) # Contient les détails de la correction
|
||
|
||
def __init__(self,
|
||
incorrect_detection: Dict[str, Any],
|
||
alternative_detections: List[Dict[str, Any]] = None,
|
||
screenshot: Optional[np.ndarray] = None,
|
||
parent=None):
|
||
"""
|
||
Initialiser le dialogue de correction
|
||
|
||
Args:
|
||
incorrect_detection: Détection incorrecte à corriger
|
||
{
|
||
"label": str,
|
||
"confidence": float,
|
||
"bbox": (x, y, w, h),
|
||
"action_type": str
|
||
}
|
||
alternative_detections: Liste de détections alternatives disponibles
|
||
screenshot: Capture d'écran pour sélection visuelle (optionnel)
|
||
parent: Widget parent (optionnel)
|
||
"""
|
||
super().__init__(parent)
|
||
self.incorrect_detection = incorrect_detection
|
||
self.alternative_detections = alternative_detections or []
|
||
self.screenshot = screenshot
|
||
self.logger = logging.getLogger(__name__)
|
||
|
||
self.corrected_element = None
|
||
self.correction_method = None # "alternative" ou "manual"
|
||
|
||
self.init_ui()
|
||
|
||
self.logger.info(f"CorrectionDialog créé pour élément: {incorrect_detection.get('label')}")
|
||
|
||
def init_ui(self):
|
||
"""Initialiser l'interface du dialogue"""
|
||
self.setWindowTitle("Correction de Détection")
|
||
self.setModal(True)
|
||
self.setMinimumWidth(600)
|
||
self.setMinimumHeight(500)
|
||
|
||
# Layout principal
|
||
main_layout = QVBoxLayout()
|
||
self.setLayout(main_layout)
|
||
|
||
# Titre
|
||
title_label = QLabel("✎ Correction de Détection Incorrecte")
|
||
title_label.setFont(QFont("Arial", 14, QFont.Bold))
|
||
title_label.setStyleSheet("color: #FF9800; padding: 10px;")
|
||
main_layout.addWidget(title_label)
|
||
|
||
# Informations sur la détection incorrecte
|
||
incorrect_group = QGroupBox("Détection Incorrecte")
|
||
incorrect_group.setFont(QFont("Arial", 11, QFont.Bold))
|
||
incorrect_layout = QVBoxLayout()
|
||
incorrect_group.setLayout(incorrect_layout)
|
||
|
||
incorrect_info = self._format_detection_info(self.incorrect_detection)
|
||
incorrect_label = QLabel(incorrect_info)
|
||
incorrect_label.setFont(QFont("Arial", 10))
|
||
incorrect_label.setStyleSheet("padding: 10px; background-color: #FFEBEE; border-radius: 5px;")
|
||
incorrect_layout.addWidget(incorrect_label)
|
||
|
||
main_layout.addWidget(incorrect_group)
|
||
|
||
# Section des alternatives
|
||
if self.alternative_detections:
|
||
alternatives_group = QGroupBox("Sélectionner l'Élément Correct")
|
||
alternatives_group.setFont(QFont("Arial", 11, QFont.Bold))
|
||
alternatives_layout = QVBoxLayout()
|
||
alternatives_group.setLayout(alternatives_layout)
|
||
|
||
instruction_label = QLabel(
|
||
"Sélectionnez l'élément correct parmi les détections alternatives ci-dessous:"
|
||
)
|
||
instruction_label.setFont(QFont("Arial", 10))
|
||
instruction_label.setWordWrap(True)
|
||
alternatives_layout.addWidget(instruction_label)
|
||
|
||
# Liste des alternatives
|
||
self.alternatives_list = QListWidget()
|
||
self.alternatives_list.setFont(QFont("Arial", 10))
|
||
self.alternatives_list.setStyleSheet("""
|
||
QListWidget {
|
||
border: 2px solid #2196F3;
|
||
border-radius: 5px;
|
||
padding: 5px;
|
||
}
|
||
QListWidget::item {
|
||
padding: 10px;
|
||
border-bottom: 1px solid #E0E0E0;
|
||
}
|
||
QListWidget::item:selected {
|
||
background-color: #E3F2FD;
|
||
color: #1976D2;
|
||
}
|
||
QListWidget::item:hover {
|
||
background-color: #F5F5F5;
|
||
}
|
||
""")
|
||
|
||
for idx, detection in enumerate(self.alternative_detections):
|
||
item_text = self._format_detection_item(detection, idx + 1)
|
||
item = QListWidgetItem(item_text)
|
||
item.setData(Qt.UserRole, detection)
|
||
self.alternatives_list.addItem(item)
|
||
|
||
self.alternatives_list.itemDoubleClicked.connect(self.on_alternative_selected)
|
||
alternatives_layout.addWidget(self.alternatives_list)
|
||
|
||
main_layout.addWidget(alternatives_group)
|
||
|
||
# Section de sélection manuelle
|
||
manual_group = QGroupBox("Ou Spécifier Manuellement")
|
||
manual_group.setFont(QFont("Arial", 11, QFont.Bold))
|
||
manual_layout = QVBoxLayout()
|
||
manual_group.setLayout(manual_layout)
|
||
|
||
manual_instruction = QLabel(
|
||
"Décrivez l'élément correct ou fournissez des informations supplémentaires:"
|
||
)
|
||
manual_instruction.setFont(QFont("Arial", 10))
|
||
manual_instruction.setWordWrap(True)
|
||
manual_layout.addWidget(manual_instruction)
|
||
|
||
self.manual_input = QTextEdit()
|
||
self.manual_input.setFont(QFont("Arial", 10))
|
||
self.manual_input.setPlaceholderText(
|
||
"Ex: Le bouton 'Valider' en bas à droite de la fenêtre, "
|
||
"ou les coordonnées approximatives..."
|
||
)
|
||
self.manual_input.setMaximumHeight(80)
|
||
self.manual_input.setStyleSheet("""
|
||
QTextEdit {
|
||
border: 2px solid #9E9E9E;
|
||
border-radius: 5px;
|
||
padding: 5px;
|
||
}
|
||
QTextEdit:focus {
|
||
border-color: #2196F3;
|
||
}
|
||
""")
|
||
manual_layout.addWidget(self.manual_input)
|
||
|
||
main_layout.addWidget(manual_group)
|
||
|
||
# Boutons d'action
|
||
button_layout = QHBoxLayout()
|
||
|
||
self.select_button = QPushButton("✓ Valider la Sélection")
|
||
self.select_button.setFont(QFont("Arial", 11))
|
||
self.select_button.setStyleSheet("""
|
||
QPushButton {
|
||
background-color: #4CAF50;
|
||
color: white;
|
||
border: none;
|
||
padding: 10px 20px;
|
||
border-radius: 5px;
|
||
}
|
||
QPushButton:hover {
|
||
background-color: #45a049;
|
||
}
|
||
QPushButton:disabled {
|
||
background-color: #cccccc;
|
||
}
|
||
""")
|
||
self.select_button.clicked.connect(self.on_validate_correction)
|
||
button_layout.addWidget(self.select_button)
|
||
|
||
cancel_button = QPushButton("✗ Annuler")
|
||
cancel_button.setFont(QFont("Arial", 11))
|
||
cancel_button.setStyleSheet("""
|
||
QPushButton {
|
||
background-color: #f44336;
|
||
color: white;
|
||
border: none;
|
||
padding: 10px 20px;
|
||
border-radius: 5px;
|
||
}
|
||
QPushButton:hover {
|
||
background-color: #da190b;
|
||
}
|
||
""")
|
||
cancel_button.clicked.connect(self.reject)
|
||
button_layout.addWidget(cancel_button)
|
||
|
||
main_layout.addLayout(button_layout)
|
||
|
||
# Note d'aide
|
||
help_label = QLabel(
|
||
"💡 Astuce: Double-cliquez sur une alternative pour la sélectionner rapidement"
|
||
)
|
||
help_label.setFont(QFont("Arial", 9))
|
||
help_label.setStyleSheet("color: #666; padding: 5px;")
|
||
main_layout.addWidget(help_label)
|
||
|
||
def _format_detection_info(self, detection: Dict[str, Any]) -> str:
|
||
"""
|
||
Formater les informations d'une détection pour affichage
|
||
|
||
Args:
|
||
detection: Dictionnaire de détection
|
||
|
||
Returns:
|
||
Chaîne formatée
|
||
"""
|
||
label = detection.get("label", "inconnu")
|
||
confidence = detection.get("confidence", 0.0)
|
||
bbox = detection.get("bbox", (0, 0, 0, 0))
|
||
action_type = detection.get("action_type", "action")
|
||
|
||
info = f"<b>Élément:</b> {label}<br>"
|
||
info += f"<b>Action:</b> {action_type}<br>"
|
||
info += f"<b>Confiance:</b> {confidence * 100:.1f}%<br>"
|
||
info += f"<b>Position:</b> ({bbox[0]}, {bbox[1]}) - Taille: {bbox[2]}×{bbox[3]}px"
|
||
|
||
return info
|
||
|
||
def _format_detection_item(self, detection: Dict[str, Any], index: int) -> str:
|
||
"""
|
||
Formater un élément de détection pour la liste
|
||
|
||
Args:
|
||
detection: Dictionnaire de détection
|
||
index: Numéro de l'alternative
|
||
|
||
Returns:
|
||
Chaîne formatée
|
||
"""
|
||
label = detection.get("label", "inconnu")
|
||
confidence = detection.get("confidence", 0.0)
|
||
bbox = detection.get("bbox", (0, 0, 0, 0))
|
||
|
||
return (f"{index}. {label} "
|
||
f"(Confiance: {confidence * 100:.1f}%, "
|
||
f"Position: {bbox[0]},{bbox[1]})")
|
||
|
||
def on_alternative_selected(self, item: QListWidgetItem):
|
||
"""
|
||
Gestionnaire de sélection d'une alternative (double-clic)
|
||
|
||
Args:
|
||
item: Élément de liste sélectionné
|
||
"""
|
||
detection = item.data(Qt.UserRole)
|
||
self.corrected_element = detection
|
||
self.correction_method = "alternative"
|
||
|
||
self.logger.info(f"Alternative sélectionnée: {detection.get('label')}")
|
||
self.accept()
|
||
|
||
def on_validate_correction(self):
|
||
"""Valider la correction (bouton Valider)"""
|
||
# Vérifier si une alternative est sélectionnée
|
||
if self.alternative_detections and self.alternatives_list.currentItem():
|
||
item = self.alternatives_list.currentItem()
|
||
detection = item.data(Qt.UserRole)
|
||
self.corrected_element = detection
|
||
self.correction_method = "alternative"
|
||
|
||
self.logger.info(f"Correction validée (alternative): {detection.get('label')}")
|
||
self.accept()
|
||
|
||
# Sinon, vérifier si une description manuelle est fournie
|
||
elif self.manual_input.toPlainText().strip():
|
||
manual_description = self.manual_input.toPlainText().strip()
|
||
self.corrected_element = {
|
||
"label": "manual_correction",
|
||
"description": manual_description,
|
||
"confidence": 0.0,
|
||
"bbox": self.incorrect_detection.get("bbox", (0, 0, 0, 0)),
|
||
"action_type": self.incorrect_detection.get("action_type", "action")
|
||
}
|
||
self.correction_method = "manual"
|
||
|
||
self.logger.info(f"Correction validée (manuelle): {manual_description[:50]}...")
|
||
self.accept()
|
||
|
||
else:
|
||
# Aucune correction fournie
|
||
from PyQt5.QtWidgets import QMessageBox
|
||
QMessageBox.warning(
|
||
self,
|
||
"Correction Requise",
|
||
"Veuillez sélectionner une alternative ou fournir une description manuelle."
|
||
)
|
||
|
||
def get_corrected_element(self) -> Optional[Dict[str, Any]]:
|
||
"""
|
||
Obtenir l'élément corrigé après fermeture du dialogue
|
||
|
||
Returns:
|
||
Dictionnaire contenant les détails de la correction, ou None si annulé
|
||
{
|
||
"corrected_element": Dict[str, Any],
|
||
"correction_method": str,
|
||
"original_detection": Dict[str, Any]
|
||
}
|
||
"""
|
||
if self.corrected_element:
|
||
return {
|
||
"corrected_element": self.corrected_element,
|
||
"correction_method": self.correction_method,
|
||
"original_detection": self.incorrect_detection
|
||
}
|
||
return None
|
||
|
||
@staticmethod
|
||
def show_correction_dialog(incorrect_detection: Dict[str, Any],
|
||
alternative_detections: List[Dict[str, Any]] = None,
|
||
screenshot: Optional[np.ndarray] = None,
|
||
parent=None) -> Optional[Dict[str, Any]]:
|
||
"""
|
||
Méthode statique pour afficher le dialogue et obtenir la correction
|
||
|
||
Args:
|
||
incorrect_detection: Détection incorrecte
|
||
alternative_detections: Liste de détections alternatives
|
||
screenshot: Capture d'écran (optionnel)
|
||
parent: Widget parent (optionnel)
|
||
|
||
Returns:
|
||
Dictionnaire de correction ou None si annulé
|
||
"""
|
||
dialog = CorrectionDialog(
|
||
incorrect_detection,
|
||
alternative_detections,
|
||
screenshot,
|
||
parent
|
||
)
|
||
|
||
result = dialog.exec_()
|
||
|
||
if result == QDialog.Accepted:
|
||
return dialog.get_corrected_element()
|
||
|
||
return None
|
||
|
||
|
||
if __name__ == "__main__":
|
||
"""Test du dialogue de correction"""
|
||
from PyQt5.QtWidgets import QApplication
|
||
import sys
|
||
|
||
app = QApplication(sys.argv)
|
||
|
||
# Données de test
|
||
incorrect = {
|
||
"label": "annuler_button",
|
||
"confidence": 0.75,
|
||
"bbox": (300, 400, 100, 35),
|
||
"action_type": "click"
|
||
}
|
||
|
||
alternatives = [
|
||
{
|
||
"label": "valider_button",
|
||
"confidence": 0.92,
|
||
"bbox": (450, 400, 100, 35),
|
||
"action_type": "click"
|
||
},
|
||
{
|
||
"label": "enregistrer_button",
|
||
"confidence": 0.88,
|
||
"bbox": (600, 400, 120, 35),
|
||
"action_type": "click"
|
||
},
|
||
{
|
||
"label": "fermer_button",
|
||
"confidence": 0.65,
|
||
"bbox": (750, 50, 30, 30),
|
||
"action_type": "click"
|
||
}
|
||
]
|
||
|
||
correction = CorrectionDialog.show_correction_dialog(
|
||
incorrect,
|
||
alternatives
|
||
)
|
||
|
||
if correction:
|
||
print("Correction effectuée:")
|
||
print(f" Méthode: {correction['correction_method']}")
|
||
print(f" Élément corrigé: {correction['corrected_element']['label']}")
|
||
print(f" Élément original: {correction['original_detection']['label']}")
|
||
else:
|
||
print("Correction annulée")
|