Commit ·
7ce37d1
1
Parent(s): 1f70e72
Add
Browse files- app.py +60 -9
- index.html +24 -2
app.py
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
|
|
| 1 |
import io
|
|
|
|
| 2 |
from pathlib import Path
|
| 3 |
from typing import List
|
|
|
|
| 4 |
|
| 5 |
import numpy as np
|
| 6 |
import torch
|
|
@@ -125,6 +128,41 @@ def predict_tensorflow(image: Image.Image):
|
|
| 125 |
return predicted_class, confidence, all_probs
|
| 126 |
|
| 127 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
def classify(image: Image.Image, model_choice: str):
|
| 129 |
if model_choice == "pytorch":
|
| 130 |
return predict_pytorch(image)
|
|
@@ -145,15 +183,28 @@ def health_check():
|
|
| 145 |
|
| 146 |
|
| 147 |
@app.post("/predict")
|
| 148 |
-
def predict(
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
|
| 158 |
predicted_class, confidence, all_probs = classify(img, model_choice)
|
| 159 |
return {
|
|
|
|
| 1 |
+
import base64
|
| 2 |
import io
|
| 3 |
+
import urllib.request
|
| 4 |
from pathlib import Path
|
| 5 |
from typing import List
|
| 6 |
+
from urllib.parse import urlparse
|
| 7 |
|
| 8 |
import numpy as np
|
| 9 |
import torch
|
|
|
|
| 128 |
return predicted_class, confidence, all_probs
|
| 129 |
|
| 130 |
|
| 131 |
+
def load_image_from_url(image_url: str):
|
| 132 |
+
if not image_url or not image_url.strip():
|
| 133 |
+
raise ValueError("URL vide ou invalide.")
|
| 134 |
+
|
| 135 |
+
if image_url.startswith("data:"):
|
| 136 |
+
try:
|
| 137 |
+
header, encoded = image_url.split(",", 1)
|
| 138 |
+
if "base64" in header:
|
| 139 |
+
image_data = base64.b64decode(encoded)
|
| 140 |
+
else:
|
| 141 |
+
image_data = urllib.request.unquote_to_bytes(encoded)
|
| 142 |
+
return Image.open(io.BytesIO(image_data)).convert("RGB")
|
| 143 |
+
except Exception as exc:
|
| 144 |
+
raise ValueError(f"Impossible de lire le data URL: {exc}")
|
| 145 |
+
|
| 146 |
+
parsed = urlparse(image_url)
|
| 147 |
+
if parsed.scheme not in ("http", "https"):
|
| 148 |
+
raise ValueError("L'URL doit commencer par http:// ou https://")
|
| 149 |
+
|
| 150 |
+
try:
|
| 151 |
+
request = urllib.request.Request(
|
| 152 |
+
image_url,
|
| 153 |
+
headers={"User-Agent": "GeoClassifier/1.0"},
|
| 154 |
+
)
|
| 155 |
+
with urllib.request.urlopen(request, timeout=15) as response:
|
| 156 |
+
image_data = response.read()
|
| 157 |
+
except Exception as exc:
|
| 158 |
+
raise ValueError(f"Impossible de récupérer l'image depuis l'URL: {exc}")
|
| 159 |
+
|
| 160 |
+
try:
|
| 161 |
+
return Image.open(io.BytesIO(image_data)).convert("RGB")
|
| 162 |
+
except Exception as exc:
|
| 163 |
+
raise ValueError(f"Impossible de lire l'image depuis l'URL: {exc}")
|
| 164 |
+
|
| 165 |
+
|
| 166 |
def classify(image: Image.Image, model_choice: str):
|
| 167 |
if model_choice == "pytorch":
|
| 168 |
return predict_pytorch(image)
|
|
|
|
| 183 |
|
| 184 |
|
| 185 |
@app.post("/predict")
|
| 186 |
+
def predict(
|
| 187 |
+
image: UploadFile = File(None),
|
| 188 |
+
image_url: str = Form(None),
|
| 189 |
+
model_choice: str = Form("pytorch")
|
| 190 |
+
):
|
| 191 |
+
if image is None and not image_url:
|
| 192 |
+
raise HTTPException(status_code=400, detail="Le fichier ou l'URL est requis.")
|
| 193 |
+
|
| 194 |
+
if image is not None:
|
| 195 |
+
if image.content_type.split('/')[0] != 'image':
|
| 196 |
+
raise HTTPException(status_code=400, detail="Le fichier doit être une image.")
|
| 197 |
+
|
| 198 |
+
image_data = image.file.read()
|
| 199 |
+
try:
|
| 200 |
+
img = Image.open(io.BytesIO(image_data)).convert("RGB")
|
| 201 |
+
except Exception as exc:
|
| 202 |
+
raise HTTPException(status_code=400, detail=f"Impossible de lire l'image: {exc}")
|
| 203 |
+
else:
|
| 204 |
+
try:
|
| 205 |
+
img = load_image_from_url(image_url)
|
| 206 |
+
except ValueError as exc:
|
| 207 |
+
raise HTTPException(status_code=400, detail=str(exc))
|
| 208 |
|
| 209 |
predicted_class, confidence, all_probs = classify(img, model_choice)
|
| 210 |
return {
|
index.html
CHANGED
|
@@ -618,7 +618,11 @@ html::-webkit-scrollbar {
|
|
| 618 |
}
|
| 619 |
|
| 620 |
function toggleUrlInput() {
|
| 621 |
-
document.getElementById('url-input-wrap')
|
|
|
|
|
|
|
|
|
|
|
|
|
| 622 |
}
|
| 623 |
|
| 624 |
function resetAll() {
|
|
@@ -650,8 +654,26 @@ html::-webkit-scrollbar {
|
|
| 650 |
|
| 651 |
if (!file && !urlVal) { showError(t('selectImage')); return; }
|
| 652 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 653 |
const formData = new FormData();
|
| 654 |
-
if (file)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 655 |
formData.append('model_choice', modelChoice);
|
| 656 |
|
| 657 |
setProcessing(true);
|
|
|
|
| 618 |
}
|
| 619 |
|
| 620 |
function toggleUrlInput() {
|
| 621 |
+
const wrap = document.getElementById('url-input-wrap');
|
| 622 |
+
wrap.classList.toggle('hidden');
|
| 623 |
+
if (wrap.classList.contains('hidden')) {
|
| 624 |
+
urlInput.value = '';
|
| 625 |
+
}
|
| 626 |
}
|
| 627 |
|
| 628 |
function resetAll() {
|
|
|
|
| 654 |
|
| 655 |
if (!file && !urlVal) { showError(t('selectImage')); return; }
|
| 656 |
|
| 657 |
+
// Validation basique de l'URL
|
| 658 |
+
if (!file && urlVal) {
|
| 659 |
+
try {
|
| 660 |
+
const url = new URL(urlVal);
|
| 661 |
+
if (!['http:', 'https:', 'data:'].includes(url.protocol)) {
|
| 662 |
+
showError('URL invalide. Utilisez http://, https:// ou data:');
|
| 663 |
+
return;
|
| 664 |
+
}
|
| 665 |
+
} catch (e) {
|
| 666 |
+
showError('URL invalide. Vérifiez le format.');
|
| 667 |
+
return;
|
| 668 |
+
}
|
| 669 |
+
}
|
| 670 |
+
|
| 671 |
const formData = new FormData();
|
| 672 |
+
if (file) {
|
| 673 |
+
formData.append('image', file);
|
| 674 |
+
} else {
|
| 675 |
+
formData.append('image_url', urlVal);
|
| 676 |
+
}
|
| 677 |
formData.append('model_choice', modelChoice);
|
| 678 |
|
| 679 |
setProcessing(true);
|