248 lines
9.8 KiB
Python
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)})
|
|
})
|
|
|
|
|