loko/loko/sign/views_ai.py

248 lines
9.8 KiB
Python

"""
Vues et API pour la détection et l'OCR de panneaux de signalisation routière.
"""
import io
import json
import logging
from typing import Any, Dict
from django.views.generic import TemplateView
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import JsonResponse, HttpResponseBadRequest
from django.utils.translation import gettext_lazy as _
from django.views.decorators.csrf import csrf_exempt
from django.utils.decorators import method_decorator
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework.permissions import IsAuthenticated
from .ai import SignDetectionService, SIGN_CATALOG, get_svg_url
logger = logging.getLogger(__name__)
class SignAIDemoView(LoginRequiredMixin, TemplateView):
"""
Page de démonstration et banc de test interactif pour la reconnaissance de panneaux et panonceaux.
Permet d'uploader ou capturer une photo et de visualiser instantanément les détections,
les textes OCR, l'ordre vertical et les fichiers vectoriels SVG correspondants.
"""
template_name = "sign/ai_demo.html"
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
context = super().get_context_data(**kwargs)
context["catalog_sample"] = list(SIGN_CATALOG.items())[:12]
return context
def post(self, request, *args, **kwargs):
image_file = request.FILES.get("image")
image_base64 = request.POST.get("image_base64")
confidence_threshold = float(request.POST.get("confidence_threshold", 0.25))
if not image_file and not image_base64:
return JsonResponse(
{"status": "error", "message": _("Aucune image fournie.")},
status=400
)
try:
service = SignDetectionService.get_instance()
if image_file:
image_bytes = image_file.read()
result = service.analyze_image(image_bytes, confidence_threshold=confidence_threshold)
else:
# Format data:image/...;base64,...
if "," in image_base64:
image_base64 = image_base64.split(",", 1)[1]
import base64
image_bytes = base64.b64decode(image_base64)
result = service.analyze_image(image_bytes, confidence_threshold=confidence_threshold)
return JsonResponse(result)
except Exception as exc:
logger.error("Erreur lors de l'analyse IA de signalisation : %s", exc, exc_info=True)
return JsonResponse(
{"status": "error", "message": f"Erreur lors du traitement de l'image : {str(exc)}"},
status=500
)
class DetectSignAPIView(APIView):
"""
Endpoint API REST pour l'analyse mobile/terrain d'une photo de signalisation.
Accepte multipart/form-data ('image') ou JSON avec 'image_base64'.
Retourne la liste ordonnée des panneaux détectés, le texte OCR extrait,
les valeurs numériques et le chemin du SVG officiel correspondant.
"""
permission_classes = [IsAuthenticated]
def post(self, request, *args, **kwargs):
image_file = request.FILES.get("image")
image_base64 = request.data.get("image_base64")
confidence_threshold = float(request.data.get("confidence_threshold", 0.25))
if not image_file and not image_base64:
return Response(
{"status": "error", "message": "Paramètre 'image' ou 'image_base64' manquant."},
status=status.HTTP_400_BAD_REQUEST
)
try:
service = SignDetectionService.get_instance()
if image_file:
image_bytes = image_file.read()
else:
if "," in image_base64:
image_base64 = image_base64.split(",", 1)[1]
import base64
image_bytes = base64.b64decode(image_base64)
result = service.analyze_image(image_bytes, confidence_threshold=confidence_threshold)
return Response(result, status=status.HTTP_200_OK)
except Exception as exc:
logger.error("Erreur API détection signalisation : %s", exc, exc_info=True)
return Response(
{"status": "error", "message": str(exc)},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
class QuickCreateSignWithAIView(APIView):
"""
Endpoint pour créer un mât (SignPole) et un ou plusieurs panneaux (SignPanel)
à partir des résultats de détection IA ou d'une saisie rapide sur le terrain,
avec enregistrement de la 1ère inspection associée.
"""
permission_classes = [IsAuthenticated]
def post(self, request, *args, **kwargs):
from django.contrib.gis.geos import Point
from django.contrib.gis.db.models.functions import Distance
from django.contrib.contenttypes.models import ContentType
from django.utils import timezone
from assets.models import SignPole, SignPanel, SignPanelType, SignStreet
from inspections.models import SignPanelInspection, InspectionDocument
lat = request.data.get('lat')
lon = request.data.get('lon')
if not lat or not lon:
return Response({'success': False, 'message': str(_("Coordonnées GPS requises."))}, status=status.HTTP_400_BAD_REQUEST)
try:
lat = float(lat)
lon = float(lon)
except (ValueError, TypeError):
return Response({'success': False, 'message': str(_("Coordonnées GPS invalides."))}, status=status.HTTP_400_BAD_REQUEST)
# 1. Recherche de la rue la plus proche
point_4326 = Point(lon, lat, srid=4326)
closest_street = SignStreet.objects.annotate(
distance=Distance('geom', point_4326)
).order_by('distance').first()
# 2. Création du Poteau / Support (SignPole)
pole_geom = Point(lon, lat, srid=4326)
pole = SignPole.objects.create(
lat=lat,
lon=lon,
geom=pole_geom,
geojson=pole_geom.geojson,
street=closest_street,
code=f"P-{int(timezone.now().timestamp())}",
status='active'
)
panels_data = request.data.get('panels', '[]')
if isinstance(panels_data, str):
try:
panels_data = json.loads(panels_data)
except Exception:
panels_data = []
created_panels = []
uploaded_photos = request.FILES.getlist('photos')
for idx, p_info in enumerate(panels_data):
type_code = (p_info.get('signpanel_type_code') or p_info.get('code') or 'C1').strip().upper()
ptype = SignPanelType.objects.filter(code__iexact=type_code).first()
if not ptype:
ptype = SignPanelType.objects.filter(code__iexact='C1').first()
panel_text = p_info.get('signpanel_text') or p_info.get('text') or ''
cleanliness = p_info.get('cleanliness') or 'clean'
vertical_order = p_info.get('vertical_order') or (idx + 1)
ai_detected_code = p_info.get('ai_detected_code') or type_code
ai_conf_val = p_info.get('ai_confidence', 0.95)
try:
ai_confidence = float(ai_conf_val)
except (ValueError, TypeError):
ai_confidence = 0.95
is_compliant = (cleanliness == 'clean')
panel_status = 'inactive' if cleanliness == 'missing' else 'active'
panel = SignPanel.objects.create(
pole=pole,
street=closest_street,
signpanel_type=ptype,
signpanel_text=panel_text,
vertical_order=vertical_order,
lat=lat,
lon=lon,
geom=pole_geom,
geojson=pole_geom.geojson,
code=f"PAN-{pole.id}-{vertical_order}",
status=panel_status,
inspection_status='compliant' if is_compliant else 'non_compliant',
last_inspection_date=timezone.now()
)
# Création de l'inspection initiale
result_status = 'compliant' if is_compliant else ('missing' if cleanliness == 'missing' else 'damaged')
ct = ContentType.objects.get_for_model(SignPanel)
inspection = SignPanelInspection.objects.create(
asset_content_type=ct,
asset_object_id=panel.pk,
inspector=request.user,
inspection_date=timezone.now(),
result_status=result_status,
nature='visual',
inspector_role='observer',
verified_signpanel_type=ptype,
verified_signpanel_text=panel_text,
verified_cleanliness=cleanliness,
ai_detected_code=ai_detected_code,
ai_confidence=ai_confidence,
apply_sign_updates_to_asset=True,
notes="Création initiale assistée par IA sur le terrain."
)
for photo in uploaded_photos:
InspectionDocument.objects.create(
inspection=inspection,
file=photo
)
created_panels.append({
'id': panel.id,
'code': panel.code,
'type_code': ptype.code if ptype else type_code,
'text': panel_text,
'vertical_order': vertical_order
})
return Response({
'success': True,
'pole_id': pole.id,
'pole_code': pole.code,
'panels': created_panels,
'panels_created': len(created_panels),
'message': str(_("%(count)d panneau(x) créé(s) avec succès.") % {'count': len(created_panels)})
})