diff --git a/backend/Dockerfile b/backend/Dockerfile index f03566ccc..f536f9f60 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -7,6 +7,7 @@ RUN apt update && apt install -y \ libglib2.0-0 \ curl \ gcc \ + patch \ && rm -rf /var/lib/apt/lists/* # install python libraries diff --git a/backend/requirements.txt b/backend/requirements.txt index 35543273a..1e8bf9628 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -12,7 +12,7 @@ autodynatrace==2.0.0 PyJWT==2.8.0 cryptography==42.0.8 # ML -basegun-ml==1.0.1 +basegun-ml==2.0.1 # Dev pytest==7.4.3 coverage==7.3.2 diff --git a/backend/src/router.py b/backend/src/router.py index 7325b1130..558a306c5 100644 --- a/backend/src/router.py +++ b/backend/src/router.py @@ -6,6 +6,7 @@ from basegun_ml.classification import get_typology from basegun_ml.measure import get_lengths +from basegun_ml.ocr import is_alarm_weapon, LowQuality, MissingText from fastapi import ( APIRouter, BackgroundTasks, @@ -37,7 +38,6 @@ def home(): def version(): return APP_VERSION - @router.post("/upload") async def imageupload( request: Request, @@ -74,8 +74,12 @@ async def imageupload( gun_length, gun_barrel_length, conf_card = None, None, None if label in TYPOLOGIES_MEASURED and confidence_level != "low": - gun_length, gun_barrel_length, conf_card = get_lengths(img_bytes) + try: + gun_length, gun_barrel_length, conf_card = get_lengths(img_bytes) + except Exception as e: + extras_logging["bg_error_type"] = e.__class__.__name__ + logging.exception(e, extra=extras_logging) # Temporary fix while ML package send 0 instead of None # https://github.com/dnum-mi/basegun-ml/issues/14 gun_length = None if gun_length == 0 else gun_length @@ -106,6 +110,34 @@ async def imageupload( logging.exception(e, extra=extras_logging) raise HTTPException(status_code=500, detail=str(e)) +@router.post("/identification-blank-gun") +async def imageblankgun( + image: UploadFile = File(...), +): + try: + img_bytes = image.file.read() + # Process image with ML models + alarm_model = is_alarm_weapon(img_bytes) + return { + "alarm_model": alarm_model, + "missing_text": False, + "low_quality": False, + } + + except LowQuality: + return { + "alarm_model": None, + "low_quality": True, + "missing_text": False, + } + except MissingText: + return { + "alarm_model": None, + "low_quality": False, + "missing_text": True, + } + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) @router.post("/identification-feedback") async def log_feedback(request: Request, user_id: Union[str, None] = Cookie(None)): diff --git a/frontend/src/api/api-client.ts b/frontend/src/api/api-client.ts index f6471ea40..d6ca54822 100644 --- a/frontend/src/api/api-client.ts +++ b/frontend/src/api/api-client.ts @@ -6,6 +6,7 @@ import { IDENTIFICATION_FEEDBACK_ROUTE, TUTORIAL_FEEDBACK_ROUTE, UPLOAD_PHOTO_FOR_DETECTION_ROUTE, + UPLOAD_PHOTO_FOR_BLANK_GUN_DETECTION_ROUTE, } from "./api-routes"; export const uploadPhotoForDetection = async (file: File) => { @@ -50,3 +51,13 @@ export const sendExpertiseForm = async ( console.error("Erreur lors de l'envoi du formulaire :", error); } }; + +export const uploadPhotoForBlankGunDetection = async (file: File) => { + const fd = new FormData(); + fd.append("image", file, file.name); + const { data } = await axios.post( + UPLOAD_PHOTO_FOR_BLANK_GUN_DETECTION_ROUTE, + fd, + ); + return data; +}; diff --git a/frontend/src/api/api-routes.ts b/frontend/src/api/api-routes.ts index f134236c8..fc71730e4 100644 --- a/frontend/src/api/api-routes.ts +++ b/frontend/src/api/api-routes.ts @@ -3,3 +3,5 @@ export const TUTORIAL_FEEDBACK_ROUTE = "/tutorial-feedback"; export const IDENTIFICATION_FEEDBACK_ROUTE = "/identification-feedback"; export const IDENTIFICATION_DUMMY_ROUTE = "/identification-dummy"; export const ASK_FOR_OPINION_ROUTE = "/expert-contact"; +export const UPLOAD_PHOTO_FOR_BLANK_GUN_DETECTION_ROUTE = + "/identification-blank-gun"; diff --git a/frontend/src/assets/missing_marking.png b/frontend/src/assets/missing_marking.png new file mode 100644 index 000000000..df6c4ab67 Binary files /dev/null and b/frontend/src/assets/missing_marking.png differ diff --git a/frontend/src/components/ResultPage.vue b/frontend/src/components/ResultPage.vue index 7e29ef306..104cb774f 100644 --- a/frontend/src/components/ResultPage.vue +++ b/frontend/src/components/ResultPage.vue @@ -5,6 +5,7 @@ import SnackbarAlert from "@/components/SnackbarAlert.vue"; import { TYPOLOGIES, MEASURED_GUNS_TYPOLOGIES, + isAlarmGun, } from "@/utils/firearms-utils/index"; import { isUserUsingCrosscall } from "@/utils/isUserUsingCrosscall"; import { useSnackbarStore } from "@/stores/snackbar"; @@ -129,7 +130,7 @@ function sendFeedback(isCorrect: boolean) {

Arme factice de type {{ label }}

-

+

Arme d'alarme de type {{ label }}

diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 2777fe086..5f000cc92 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -9,6 +9,7 @@ import { clearLocalStorage } from "@/utils/storage-utils.js"; import { mgr } from "@/utils/authentication"; import MissingCardPage from "@/views/MissingCardPage.vue"; +import IdentificationQualityImage from "@/views/GuideIdentificationFirearm/IdentificationQualityImage.vue"; import ExpertiseForm from "@/views/GuideAskingExpertise/ExpertiseForm.vue"; const HomePage = () => import("@/views/HomePage.vue"); @@ -41,8 +42,6 @@ const IdentificationFurtherInformations = () => ); const IdentificationSelectAmmo = () => import("@/views/GuideIdentificationFirearm/IdentificationSelectAmmo.vue"); -const IdentificationBlankGun = () => - import("@/views/GuideIdentificationFirearm/IdentificationBlankGun.vue"); const ExpertSituation = () => import("@/views/GuideContactExpert/ExpertSituation.vue"); @@ -129,9 +128,9 @@ const routes: RouteRecordRaw[] = [ component: IdentificationSelectAmmo, }, { - path: "armes-alarme", - name: "IdentificationBlankGun", - component: IdentificationBlankGun, + path: "qualite-image", + name: "IdentificationQualityImage", + component: IdentificationQualityImage, }, { path: "resultat-final", @@ -247,7 +246,7 @@ const routes: RouteRecordRaw[] = [ } } catch (error) { console.error("Erreur signin callback:", error); - next({ name: "AuthRedirect" }); + next({ name: "ErrorPage" }); } }, }, diff --git a/frontend/src/stores/result.ts b/frontend/src/stores/result.ts index d0b794ed3..7d18c2b3d 100644 --- a/frontend/src/stores/result.ts +++ b/frontend/src/stores/result.ts @@ -10,11 +10,15 @@ export const useStore = defineStore("result", { const gunBarrelLength = ref(null); const img = ref(null); const imgUrl = ref(null); + const unresizeImage = ref(null); const securingTutorial = ref(false); const selectedOptions = ref([]); const selectedAmmo = ref(undefined); const selectedAlarmGun = ref(undefined); + const alarmModel = ref(null); + const isAlarmGunMissingText = ref(null); + const isAlarmGunLowQuality = ref(null); const isDummy = computed(() => !!(selectedAmmo.value === "billes")); const isModalTransparentAmmoOpened = ref(null); @@ -26,11 +30,15 @@ export const useStore = defineStore("result", { gunBarrelLength.value = null; img.value = null; imgUrl.value = null; + unresizeImage.value = null; securingTutorial.value = false; selectedOptions.value = []; selectedAmmo.value = undefined; selectedAlarmGun.value = undefined; + alarmModel.value = null; + isAlarmGunMissingText.value = null; + isAlarmGunLowQuality.value = null; isModalTransparentAmmoOpened.value = null; } @@ -42,10 +50,14 @@ export const useStore = defineStore("result", { gunBarrelLength, img, imgUrl, + unresizeImage, securingTutorial, selectedOptions, selectedAmmo, selectedAlarmGun, + alarmModel, + isAlarmGunMissingText, + isAlarmGunLowQuality, isDummy, isModalTransparentAmmoOpened, $reset, diff --git a/frontend/src/utils/firearms-utils/index.ts b/frontend/src/utils/firearms-utils/index.ts index f4f34dd0d..7121afb50 100644 --- a/frontend/src/utils/firearms-utils/index.ts +++ b/frontend/src/utils/firearms-utils/index.ts @@ -34,7 +34,7 @@ export const TYPOLOGIES = { const IdentificationTypologyResult = "IdentificationTypologyResult"; const IdentificationFurtherInformations = "IdentificationFurtherInformations"; const IdentificationSelectAmmo = "IdentificationSelectAmmo"; -const IdentificationBlankGun = "IdentificationBlankGun"; +const IdentificationQualityImage = "IdentificationQualityImage"; const IdentificationFinalResult = "IdentificationFinalResult"; export const identificationGuideSteps = [ @@ -48,7 +48,7 @@ export const identificationGuideStepsWithArmeAlarme = [ IdentificationTypologyResult, IdentificationFurtherInformations, IdentificationSelectAmmo, - IdentificationBlankGun, + IdentificationQualityImage, IdentificationFinalResult, ] as const; @@ -69,7 +69,7 @@ export const identificationRoutePathsWithArmeAlarme = [ "resultat-typologie", "informations-complementaires", "munition-type", - "armes-alarme", + "qualite-image", "resultat-final", ] as const; @@ -81,7 +81,7 @@ export function isAlarmGun() { ) { return false; } - return store.selectedAlarmGun ? true : undefined; + return store.alarmModel === "Alarm_model" ? true : undefined; } export const MEASURED_GUNS_TYPOLOGIES = [ diff --git a/frontend/src/views/GuideIdentificationFirearm/GuideIdentificationFirearm.vue b/frontend/src/views/GuideIdentificationFirearm/GuideIdentificationFirearm.vue index 5df828031..6fa6c421c 100644 --- a/frontend/src/views/GuideIdentificationFirearm/GuideIdentificationFirearm.vue +++ b/frontend/src/views/GuideIdentificationFirearm/GuideIdentificationFirearm.vue @@ -9,6 +9,7 @@ import { isAlarmGun, } from "@/utils/firearms-utils/index"; import { useStore } from "@/stores/result"; +import { uploadPhotoForBlankGunDetection } from "@/api/api-client"; const store = useStore(); const router = useRouter(); @@ -16,6 +17,9 @@ const route = useRoute(); const confidenceLevel = computed(() => store.confidenceLevel); const typology = computed(() => store.typology); +const alarmModel = computed(() => store.alarmModel); +const isAlarmGunLowQuality = computed(() => store.isAlarmGunLowQuality); +const isAlarmGunMissingText = computed(() => store.isAlarmGunMissingText); const currentStep = ref(1); const isLowConfidence = confidenceLevel.value === "low"; @@ -56,10 +60,6 @@ const goToNewRouteWithArmeAlarme = () => name: identificationGuideStepsWithArmeAlarme[currentStep.value - 1], }); -const isArmeAlarme = computed( - () => route.path === "/guide-identification/armes-alarme", -); - const goOnAndFollow = computed(() => currentStep.value === 1 ? "Continuer" @@ -74,26 +74,7 @@ const arrowOrCircleIcon = () => const calculateRoute = (store) => { return store.selectedAmmo === "billes" ? { name: "IdentificationFinalResult" } - : { name: "IdentificationBlankGun" }; -}; - -// showDiv is used to create a mini steper for alarm guns. Need to be reworked. -const backStepButtonAction = () => { - if (showDiv.value === false) { - currentStep.value--; - goToNewRouteWithArmeAlarme(); - } else { - showDiv.value = false; - } -}; - -const nextStepButtonAction = () => { - if (showDiv.value === false) { - showDiv.value = true; - } else { - currentStep.value++; - goToNewRouteWithArmeAlarme(); - } + : { name: "IdentificationQualityImage" }; }; function handlePreviousButtonClick() { @@ -105,7 +86,64 @@ function handlePreviousButtonClick() { } } -const showDiv = ref(false); +async function onFileSelected(event) { + const uploadedFile = event.target.files[0]; + + if (!uploadedFile) { + console.error("Aucun fichier sélectionné."); + return; + } + + console.log("Fichier sélectionné :", uploadedFile); + + try { + const result = await uploadPhotoForBlankGunDetection(uploadedFile); + console.log("Résultat de l'envoi :", result); + + if (result) { + const { alarm_model, missing_text, low_quality } = result; + if (alarm_model === "Alarm_model") { + store.$patch({ alarmModel: alarm_model }); + console.log("Modèle d'arme d'alarme détecté :", alarm_model); + + currentStep.value++; + router.push({ name: "IdentificationFinalResult" }); + } else if (low_quality === true) { + store.$patch({ isAlarmGunLowQuality: true }); + } else if (missing_text === true) { + store.$patch({ isAlarmGunMissingText: true }); + } + } else { + console.error("La réponse est indéfinie ou mal formée :", result); + } + } catch (error) { + console.error("Erreur lors de l'envoi de l'image :", error); + } +} + +const handledImageTypes = "image/jpeg, image/png, image/jpg"; + +const footerValue = computed(() => { + if (isAlarmGunLowQuality) { + return { + next: "Pas de marquages, passer à l'étape suivante", + picture: "Reprendre la photo", + }; + } else { + return { + next: "Non, passer à l'étape suivante", + picture: "Oui, en prendre une photo rapprochée", + }; + } +}); + +watch(alarmModel, (newVal) => { + if (newVal === "Alarm_model" || newVal === "Not_alarm") { + setTimeout(() => { + currentStep.value++; + }, 5000); + } +}); diff --git a/frontend/src/views/GuideIdentificationFirearm/IdentificationQualityImage.vue b/frontend/src/views/GuideIdentificationFirearm/IdentificationQualityImage.vue new file mode 100644 index 000000000..a4767e4f3 --- /dev/null +++ b/frontend/src/views/GuideIdentificationFirearm/IdentificationQualityImage.vue @@ -0,0 +1,147 @@ + + + diff --git a/frontend/src/views/InstructionsPage.vue b/frontend/src/views/InstructionsPage.vue index b1e2d4c06..f6c477a1c 100644 --- a/frontend/src/views/InstructionsPage.vue +++ b/frontend/src/views/InstructionsPage.vue @@ -92,12 +92,24 @@ async function srcToFile(src: string, fileName: string, mimeType: string) { return new File([buf], fileName, { type: mimeType }); } -function onFileSelected( +function fileToBase64(file: File): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.readAsDataURL(file); + reader.onload = () => resolve(reader.result as string); + reader.onerror = (error) => reject(error); + }); +} + +async function onFileSelected( event: InputEvent & { target: InputEvent["target"] & { files: File[] } }, ) { loading.value = true; const uploadedFile = event.target?.files[0]; + const unresizeImage = await fileToBase64(uploadedFile); + store.$patch({ unresizeImage: unresizeImage }); + resizeImage(uploadedFile).then((resizedBase64Image) => uploadImage(resizedBase64Image, uploadedFile.name), );