oriqqqqqqat commited on
Commit
f010ea5
·
1 Parent(s): 5712979

modifycapcha

Browse files
Files changed (2) hide show
  1. main.py +249 -56
  2. templates/detect.html +21 -17
main.py CHANGED
@@ -1,81 +1,256 @@
1
  import os
 
 
 
 
 
2
  import uuid
 
 
 
3
  import time
4
- import shutil
5
- import json
6
- import requests
7
  import threading
 
 
 
 
 
 
8
 
9
- from fastapi import FastAPI, Request, Form, UploadFile, File
10
- from fastapi.responses import RedirectResponse
11
- from fastapi.staticfiles import StaticFiles
12
  from fastapi.templating import Jinja2Templates
 
13
 
14
- from dotenv import load_dotenv
15
- load_dotenv()
 
 
 
16
 
17
- from your_model_module import process_with_ai_model # ← เปลี่ยนเป็นของคุณ
 
 
 
 
 
 
 
18
 
19
- app = FastAPI()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  templates = Jinja2Templates(directory="templates")
22
  app.mount("/static", StaticFiles(directory="static"), name="static")
 
23
 
 
24
  results_cache = {}
25
  cache_lock = threading.Lock()
26
 
27
-
28
  SYMPTOM_MAP = {
29
- "pain_touch": "เจ็บเมื่อโดนแผล",
30
- "eat_spicy": "กินเผ็ดแสบ",
31
- "drink_alcohol": "ดื่มเหล้า",
32
  "smoking": "สูบบุหรี่",
33
- "betel_chew": "เคี้ยวหมาก",
34
- "wipe_off": "เช็ดออกได้",
35
- "no_symptoms": "ไม่มีอาการ",
 
36
  }
37
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
 
39
- @app.get("/detect")
 
 
 
 
 
 
 
 
 
 
 
 
40
  def detect_page(request: Request):
41
  return templates.TemplateResponse("detect.html", {"request": request})
42
 
43
-
44
- @app.get("/results/{result_id}")
45
  def show_results(request: Request, result_id: str):
46
  with cache_lock:
47
- result = results_cache.get(result_id)
48
 
49
- if not result:
50
- return templates.TemplateResponse("detect.html", {"request": request, "error": "ไม่พบผลลัพธ์"})
51
 
52
- data = result["data"]
53
  return templates.TemplateResponse(
54
  "detect.html",
55
- {
56
- "request": request,
57
- "image_b64_data": data["image_b64_data"],
58
- "gradcam_b64_data": data["gradcam_b64_data"],
59
- "name_out": data["name_out"],
60
- "eva_output": data["eva_output"],
61
- }
62
  )
63
 
 
 
 
64
 
65
- # ==============================================
66
- # SINGLE UPLOADED ENDPOINT (FIXED)
67
- # ==============================================
68
  @app.post("/uploaded")
69
  async def handle_upload(
70
  request: Request,
71
  file: UploadFile = File(...),
72
- checkboxes: list = Form([]),
73
  symptom_text: str = Form(""),
74
- cf_token: str = Form(alias="cf-turnstile-response")
75
  ):
76
-
77
- # ===== CAPTCHA CHECK =====
78
  TURNSTILE_SECRET = os.getenv("TURNSTILE_SECRET")
 
79
  if not cf_token:
80
  return templates.TemplateResponse(
81
  "detect.html",
@@ -83,45 +258,63 @@ async def handle_upload(
83
  status_code=400
84
  )
85
 
86
- verify_res = requests.post(
87
  "https://challenges.cloudflare.com/turnstile/v0/siteverify",
88
  data={"secret": TURNSTILE_SECRET, "response": cf_token}
89
  ).json()
90
 
91
- if not verify_res.get("success", False):
92
  return templates.TemplateResponse(
93
  "detect.html",
94
- {"request": request, "error": "CAPTCHA ไม่ผ่านการตรวจสอบ กรุณาลองใหม่"},
95
  status_code=400
96
  )
97
 
98
- # ===== SAVE IMAGE =====
99
  temp_path = os.path.join("uploads", f"{uuid.uuid4()}_{file.filename}")
 
100
  with open(temp_path, "wb") as buffer:
101
  shutil.copyfileobj(file.file, buffer)
102
 
103
- selected_symptoms = {SYMPTOM_MAP.get(cb) for cb in checkboxes if SYMPTOM_MAP.get(cb)}
104
- final_prompt = " ".join(selected_symptoms) if selected_symptoms else "ไม่มีอาการ"
105
 
106
- # ===== AI MODEL PROCESS =====
107
- image_b64, gradcam_b64, name_out, eva_output = process_with_ai_model(
108
- image_path=temp_path,
109
- prompt_text=final_prompt
110
- )
 
 
 
 
 
 
 
 
 
 
111
 
 
 
 
112
  os.remove(temp_path)
113
 
114
- # ===== STORE RESULT =====
115
  result_id = str(uuid.uuid4())
116
  with cache_lock:
117
  results_cache[result_id] = {
118
  "data": {
119
- "image_b64_data": image_b64,
120
- "gradcam_b64_data": gradcam_b64,
121
- "name_out": name_out,
122
- "eva_output": eva_output,
123
  },
124
- "created_at": time.time()
125
  }
126
 
127
- return RedirectResponse(url=f"/results/{result_id}", status_code=303)
 
 
 
 
 
 
 
 
1
  import os
2
+ import sys
3
+ import json
4
+ import random
5
+ import shutil
6
+ import hashlib
7
  import uuid
8
+ from typing import List
9
+ import base64
10
+ from io import BytesIO
11
  import time
 
 
 
12
  import threading
13
+ import numpy as np
14
+ import torch
15
+ import torch.nn as nn
16
+ from PIL import Image, ImageOps
17
+ from matplotlib import cm
18
+ import requests
19
 
20
+ import cv2
21
+ from fastapi import FastAPI, File, UploadFile, Form, Request
22
+ from fastapi.responses import HTMLResponse, RedirectResponse
23
  from fastapi.templating import Jinja2Templates
24
+ from fastapi.staticfiles import StaticFiles
25
 
26
+ # ===============================
27
+ # DOWNLOAD MODEL FROM HF IF NEEDED
28
+ # ===============================
29
+ HF_MODEL_URL = "https://huggingface.co/qqqqqqat/densenet_wangchan/resolve/main/best_fusion_densenet.pth"
30
+ LOCAL_MODEL_PATH = "models/densenet/best_fusion_densenet.pth"
31
 
32
+ def download_model_if_needed():
33
+ if not os.path.exists(LOCAL_MODEL_PATH):
34
+ print("📥 Downloading model from HuggingFace...")
35
+ os.makedirs(os.path.dirname(LOCAL_MODEL_PATH), exist_ok=True)
36
+ response = requests.get(HF_MODEL_URL)
37
+ with open(LOCAL_MODEL_PATH, "wb") as f:
38
+ f.write(response.content)
39
+ print("✅ Model downloaded!")
40
 
41
+ # =================================
42
+
43
+ sys.path.append(os.path.abspath(os.path.dirname(__file__)))
44
+ from models.densenet.preprocess.preprocessingwangchan import get_tokenizer, get_transforms
45
+ from models.densenet.train_densenet_only import DenseNet121Classifier
46
+ from models.densenet.train_text_only import TextClassifier
47
+
48
+ torch.manual_seed(42); np.random.seed(42); random.seed(42)
49
+
50
+ FUSION_LABELMAP_PATH = "models/densenet/label_map_fusion_densenet.json"
51
+
52
+ with open(FUSION_LABELMAP_PATH, "r", encoding="utf-8") as f:
53
+ label_map = json.load(f)
54
+
55
+ class_names = [label for label, _ in sorted(label_map.items(), key=lambda x: x[1])]
56
+ NUM_CLASSES = len(class_names)
57
+
58
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
59
+ print("🧠 Using:", device)
60
+
61
+ # ==========================
62
+ # MODEL DEFINITION
63
+ # ==========================
64
+
65
+ class FusionDenseNetText(nn.Module):
66
+ def __init__(self, num_classes, dropout=0.3):
67
+ super().__init__()
68
+ self.image_model = DenseNet121Classifier(num_classes=num_classes)
69
+ self.text_model = TextClassifier(num_classes=num_classes)
70
+ self.fusion = nn.Sequential(
71
+ nn.Linear(num_classes * 2, 128), nn.ReLU(),
72
+ nn.Dropout(dropout), nn.Linear(128, num_classes)
73
+ )
74
+
75
+ def forward(self, image, input_ids, attention_mask):
76
+ logits_img = self.image_model(image)
77
+ logits_txt = self.text_model(input_ids, attention_mask)
78
+ fused_in = torch.cat([logits_img, logits_txt], dim=1)
79
+ fused_out = self.fusion(fused_in)
80
+ return fused_out, logits_img, logits_txt
81
+
82
+ # ====================================
83
+ # LOAD MODEL
84
+ # ====================================
85
+ print("🔄 Loading model...")
86
+ download_model_if_needed()
87
+
88
+ fusion_model = FusionDenseNetText(num_classes=NUM_CLASSES).to(device)
89
+ fusion_model.load_state_dict(torch.load(LOCAL_MODEL_PATH, map_location=device))
90
+ fusion_model.eval()
91
+ print("✅ Model loaded!")
92
+
93
+ tokenizer = get_tokenizer()
94
+ transform = get_transforms((224, 224))
95
+
96
+ # ====================================
97
+ # Helper for GradCAM
98
+ # ====================================
99
+
100
+ def _find_last_conv2d(mod: torch.nn.Module):
101
+ last = None
102
+ for m in mod.modules():
103
+ if isinstance(m, torch.nn.Conv2d): last = m
104
+ return last
105
+
106
+ def compute_gradcam_overlay(img_pil, image_tensor, target_idx):
107
+ img_branch = fusion_model.image_model
108
+ target_layer = _find_last_conv2d(img_branch)
109
+ if target_layer is None:
110
+ return None
111
+
112
+ activations, gradients = [], []
113
+
114
+ def fwd(_, __, o): activations.append(o)
115
+ def bwd(_, gin, gout): gradients.append(gout[0])
116
+
117
+ h1 = target_layer.register_forward_hook(fwd)
118
+ h2 = target_layer.register_full_backward_hook(bwd)
119
+
120
+ try:
121
+ img_branch.zero_grad()
122
+ logits_img = img_branch(image_tensor)
123
+ score = logits_img[0, target_idx]
124
+ score.backward()
125
+
126
+ act = activations[-1].detach()[0]
127
+ grad = gradients[-1].detach()[0]
128
+ weights = torch.mean(grad, dim=(1, 2))
129
+
130
+ cam = torch.relu(torch.sum(weights[:, None, None] * act, dim=0))
131
+ cam -= cam.min()
132
+ cam /= (cam.max() + 1e-8)
133
 
134
+ cam_img = Image.fromarray((cam.cpu().numpy() * 255).astype(np.uint8)).resize(img_pil.size)
135
+ heatmap = cm.get_cmap("jet")(cam_img)[:, :, :3]
136
+
137
+ img_np = np.asarray(img_pil.convert("RGB")).astype(np.float32) / 255.0
138
+ overlay = (0.6 * img_np + 0.4 * heatmap)
139
+ return np.clip(overlay * 255, 0, 255).astype(np.uint8)
140
+
141
+ finally:
142
+ h1.remove()
143
+ h2.remove()
144
+ img_branch.zero_grad()
145
+
146
+ # ====================================
147
+ # FASTAPI SETUP
148
+ # ====================================
149
+
150
+ app = FastAPI()
151
  templates = Jinja2Templates(directory="templates")
152
  app.mount("/static", StaticFiles(directory="static"), name="static")
153
+ os.makedirs("uploads", exist_ok=True)
154
 
155
+ EXPIRATION_MINUTES = 10
156
  results_cache = {}
157
  cache_lock = threading.Lock()
158
 
 
159
  SYMPTOM_MAP = {
160
+ "noSymptoms": "ไม่มีอาการ",
161
+ "drinkAlcohol": "ดื่มเหล้า",
 
162
  "smoking": "สูบบุหรี่",
163
+ "chewBetelNut": "เคี้ยวหมาก",
164
+ "eatSpicyFood": "กินเผ็ดแสบ",
165
+ "wipeOff": "เช็ดออกได้",
166
+ "alwaysHurts": "เจ็บเมื่อโดนแผล"
167
  }
168
 
169
+ # ====================================
170
+ # AI MAIN PROCESS
171
+ # ====================================
172
+
173
+ def process_with_ai_model(image_path, prompt_text):
174
+ try:
175
+ image_pil = Image.open(image_path)
176
+ image_pil = ImageOps.exif_transpose(image_pil)
177
+ image_pil = image_pil.convert("RGB")
178
+
179
+ tensor = transform(image_pil).unsqueeze(0).to(device)
180
+ enc = tokenizer(prompt_text, return_tensors="pt", padding="max_length",
181
+ truncation=True, max_length=128)
182
+
183
+ ids = enc["input_ids"].to(device)
184
+ mask = enc["attention_mask"].to(device)
185
+
186
+ with torch.no_grad():
187
+ fused_logits, _, _ = fusion_model(tensor, ids, mask)
188
+ probs = torch.softmax(fused_logits, dim=1)[0].cpu().numpy()
189
+
190
+ pred_idx = int(np.argmax(probs))
191
+ pred_label = class_names[pred_idx]
192
+ confidence = float(probs[pred_idx]) * 100
193
+
194
+ gradcam_overlay_np = compute_gradcam_overlay(image_pil, tensor, pred_idx)
195
+
196
+ def img64(img):
197
+ buf = BytesIO()
198
+ img.save(buf, format="JPEG")
199
+ return base64.b64encode(buf.getvalue()).decode()
200
+
201
+ original_b64 = img64(image_pil)
202
+
203
+ if gradcam_overlay_np is not None:
204
+ grad_pil = Image.fromarray(gradcam_overlay_np)
205
+ gradcam_b64 = img64(grad_pil)
206
+ else:
207
+ gradcam_b64 = original_b64
208
+
209
+ return original_b64, gradcam_b64, pred_label, f"{confidence:.2f}"
210
 
211
+ except Exception as e:
212
+ print("❌ AI error:", e)
213
+ return None, None, "Error", "0.00"
214
+
215
+ # ====================================
216
+ # ROUTES
217
+ # ====================================
218
+
219
+ @app.get("/", response_class=RedirectResponse)
220
+ def root():
221
+ return RedirectResponse("/detect")
222
+
223
+ @app.get("/detect", response_class=HTMLResponse)
224
  def detect_page(request: Request):
225
  return templates.TemplateResponse("detect.html", {"request": request})
226
 
227
+ @app.get("/results/{result_id}", response_class=HTMLResponse)
 
228
  def show_results(request: Request, result_id: str):
229
  with cache_lock:
230
+ item = results_cache.get(result_id)
231
 
232
+ if not item:
233
+ return RedirectResponse("/detect")
234
 
 
235
  return templates.TemplateResponse(
236
  "detect.html",
237
+ {"request": request, **item["data"]}
 
 
 
 
 
 
238
  )
239
 
240
+ # ====================================
241
+ # 🔥 FINAL /uploaded (รวมแล้ว + เพิ่ม CAPTCHA)
242
+ # ====================================
243
 
 
 
 
244
  @app.post("/uploaded")
245
  async def handle_upload(
246
  request: Request,
247
  file: UploadFile = File(...),
248
+ checkboxes: List[str] = Form([]),
249
  symptom_text: str = Form(""),
250
+ cf_token: str = Form(default=None, alias="cf-turnstile-response")
251
  ):
 
 
252
  TURNSTILE_SECRET = os.getenv("TURNSTILE_SECRET")
253
+
254
  if not cf_token:
255
  return templates.TemplateResponse(
256
  "detect.html",
 
258
  status_code=400
259
  )
260
 
261
+ verify = requests.post(
262
  "https://challenges.cloudflare.com/turnstile/v0/siteverify",
263
  data={"secret": TURNSTILE_SECRET, "response": cf_token}
264
  ).json()
265
 
266
+ if not verify.get("success", False):
267
  return templates.TemplateResponse(
268
  "detect.html",
269
+ {"request": request, "error": "CAPTCHA ไม่ผ่านการตรวจสอบ"},
270
  status_code=400
271
  )
272
 
 
273
  temp_path = os.path.join("uploads", f"{uuid.uuid4()}_{file.filename}")
274
+
275
  with open(temp_path, "wb") as buffer:
276
  shutil.copyfileobj(file.file, buffer)
277
 
278
+ selected = {SYMPTOM_MAP.get(cb) for cb in checkboxes if SYMPTOM_MAP.get(cb)}
 
279
 
280
+ parts = []
281
+
282
+ if "ไม่มีอาการ" in selected:
283
+ symptoms = {"เจ็บเมื่อโดนแผล", "กินเผ็ดแสบ"}
284
+ lifestyles = {"ดื่มเหล้า", "สูบบุหรี่", "เคี้ยวหมาก"}
285
+ patterns = {"เช็ดออกได้"}
286
+ specials = {"ไม่มีอาการ"}
287
+
288
+ final_sel = (selected - symptoms) | (selected & (lifestyles | patterns | specials))
289
+ parts.append(" ".join(sorted(final_sel)))
290
+ elif selected:
291
+ parts.append(" ".join(sorted(selected)))
292
+
293
+ if symptom_text.strip():
294
+ parts.append(symptom_text.strip())
295
 
296
+ final_prompt = "; ".join(parts) if parts else "ไม่มีอาการ"
297
+
298
+ img_b64, grad_b64, lbl, conf = process_with_ai_model(temp_path, final_prompt)
299
  os.remove(temp_path)
300
 
 
301
  result_id = str(uuid.uuid4())
302
  with cache_lock:
303
  results_cache[result_id] = {
304
  "data": {
305
+ "image_b64_data": img_b64,
306
+ "gradcam_b64_data": grad_b64,
307
+ "name_out": lbl,
308
+ "eva_output": conf,
309
  },
310
+ "created_at": time.time(),
311
  }
312
 
313
+ return RedirectResponse(f"/results/{result_id}", status_code=303)
314
+
315
+ # ====================================
316
+
317
+ if __name__ == "__main__":
318
+ port = int(os.environ.get("PORT", 8000))
319
+ import uvicorn
320
+ uvicorn.run(app, host="0.0.0.0", port=port)
templates/detect.html CHANGED
@@ -1,10 +1,11 @@
 
1
  <!DOCTYPE html>
2
  <html lang="th">
3
  <head>
4
  <meta charset="UTF-8">
5
  <title>Detect Oral Lesion</title>
6
- <link rel="stylesheet" href="/static/bootstrap.min.css">
7
- <link rel="stylesheet" href="/static/style.css">
8
  </head>
9
 
10
  <body class="bg-light">
@@ -24,19 +25,20 @@
24
  </div>
25
 
26
  <div class="mb-3">
27
- <label class="form-label fw-bold">เลือกอาการ/ปัจจัยเสี่ยง</label>
 
28
  <div class="form-check">
29
- <input class="form-check-input" type="checkbox" name="checkboxes" value="pain_touch">
30
  <label class="form-check-label">เจ็บเมื่อโดนแผล</label>
31
  </div>
32
 
33
  <div class="form-check">
34
- <input class="form-check-input" type="checkbox" name="checkboxes" value="eat_spicy">
35
  <label class="form-check-label">กินเผ็ดแสบ</label>
36
  </div>
37
 
38
  <div class="form-check">
39
- <input class="form-check-input" type="checkbox" name="checkboxes" value="drink_alcohol">
40
  <label class="form-check-label">ดื่มเหล้า</label>
41
  </div>
42
 
@@ -46,19 +48,20 @@
46
  </div>
47
 
48
  <div class="form-check">
49
- <input class="form-check-input" type="checkbox" name="checkboxes" value="betel_chew">
50
  <label class="form-check-label">เคี้ยวหมาก</label>
51
  </div>
52
 
53
  <div class="form-check">
54
- <input class="form-check-input" type="checkbox" name="checkboxes" value="wipe_off">
55
  <label class="form-check-label">เช็ดออกได้</label>
56
  </div>
57
 
58
  <div class="form-check">
59
- <input class="form-check-input" type="checkbox" name="checkboxes" value="no_symptoms">
60
  <label class="form-check-label">ไม่มีอาการ</label>
61
  </div>
 
62
  </div>
63
 
64
  <div class="mb-3">
@@ -66,9 +69,9 @@
66
  <textarea class="form-control" name="symptom_text" rows="3"></textarea>
67
  </div>
68
 
69
- <!-- ===========================
70
- CLOUD FLARE TURNSTILE CAPTCHA
71
- ============================ -->
72
  <div class="cf-turnstile mt-3 mb-3"
73
  data-sitekey="0x4AAAAAACEfyPjr3pfV21Mm"
74
  data-callback="onTurnstileSuccess">
@@ -85,12 +88,11 @@
85
  </script>
86
 
87
  <div class="text-center mt-4">
88
- <button class="btn btn-primary px-4 py-2" type="submit">Submit</button>
89
  </div>
90
 
91
  </form>
92
 
93
- <!-- Modal แสดงผล -->
94
  {% if image_b64_data %}
95
  <div class="modal fade show d-block" tabindex="-1">
96
  <div class="modal-dialog modal-lg">
@@ -98,17 +100,19 @@
98
  <h4 class="text-center">ผลการตรวจ</h4>
99
  <p class="text-center fw-bold">{{ name_out }}</p>
100
 
101
- <img src="data:image/png;base64,{{ image_b64_data }}" class="img-fluid mb-3">
102
- <img src="data:image/png;base64,{{ gradcam_b64_data }}" class="img-fluid">
103
 
104
- <p class="mt-3">{{ eva_output }}</p>
105
 
106
  <a href="/detect" class="btn btn-secondary mt-3">ตรวจใหม่</a>
107
  </div>
108
  </div>
109
  </div>
110
  {% endif %}
 
111
  </div>
112
 
113
  </body>
114
  </html>
 
 
1
+
2
  <!DOCTYPE html>
3
  <html lang="th">
4
  <head>
5
  <meta charset="UTF-8">
6
  <title>Detect Oral Lesion</title>
7
+ <link href="/static/bootstrap.min.css" rel="stylesheet">
8
+ <link href="/static/style.css" rel="stylesheet">
9
  </head>
10
 
11
  <body class="bg-light">
 
25
  </div>
26
 
27
  <div class="mb-3">
28
+ <label class="form-label fw-bold">เลือกอาการหรือปัจจัยร่วม</label>
29
+
30
  <div class="form-check">
31
+ <input class="form-check-input" type="checkbox" name="checkboxes" value="alwaysHurts">
32
  <label class="form-check-label">เจ็บเมื่อโดนแผล</label>
33
  </div>
34
 
35
  <div class="form-check">
36
+ <input class="form-check-input" type="checkbox" name="checkboxes" value="eatSpicyFood">
37
  <label class="form-check-label">กินเผ็ดแสบ</label>
38
  </div>
39
 
40
  <div class="form-check">
41
+ <input class="form-check-input" type="checkbox" name="checkboxes" value="drinkAlcohol">
42
  <label class="form-check-label">ดื่มเหล้า</label>
43
  </div>
44
 
 
48
  </div>
49
 
50
  <div class="form-check">
51
+ <input class="form-check-input" type="checkbox" name="checkboxes" value="chewBetelNut">
52
  <label class="form-check-label">เคี้ยวหมาก</label>
53
  </div>
54
 
55
  <div class="form-check">
56
+ <input class="form-check-input" type="checkbox" name="checkboxes" value="wipeOff">
57
  <label class="form-check-label">เช็ดออกได้</label>
58
  </div>
59
 
60
  <div class="form-check">
61
+ <input class="form-check-input" type="checkbox" name="checkboxes" value="noSymptoms">
62
  <label class="form-check-label">ไม่มีอาการ</label>
63
  </div>
64
+
65
  </div>
66
 
67
  <div class="mb-3">
 
69
  <textarea class="form-control" name="symptom_text" rows="3"></textarea>
70
  </div>
71
 
72
+ <!-- ============================= -->
73
+ <!-- Cloudflare Turnstile CAPTCHA -->
74
+ <!-- ============================= -->
75
  <div class="cf-turnstile mt-3 mb-3"
76
  data-sitekey="0x4AAAAAACEfyPjr3pfV21Mm"
77
  data-callback="onTurnstileSuccess">
 
88
  </script>
89
 
90
  <div class="text-center mt-4">
91
+ <button type="submit" class="btn btn-primary px-4 py-2">Submit</button>
92
  </div>
93
 
94
  </form>
95
 
 
96
  {% if image_b64_data %}
97
  <div class="modal fade show d-block" tabindex="-1">
98
  <div class="modal-dialog modal-lg">
 
100
  <h4 class="text-center">ผลการตรวจ</h4>
101
  <p class="text-center fw-bold">{{ name_out }}</p>
102
 
103
+ <img src="data:image/jpeg;base64,{{ image_b64_data }}" class="img-fluid mb-3" />
104
+ <img src="data:image/jpeg;base64,{{ gradcam_b64_data }}" class="img-fluid" />
105
 
106
+ <p class="mt-3">{{ eva_output }}%</p>
107
 
108
  <a href="/detect" class="btn btn-secondary mt-3">ตรวจใหม่</a>
109
  </div>
110
  </div>
111
  </div>
112
  {% endif %}
113
+
114
  </div>
115
 
116
  </body>
117
  </html>
118
+