oriqqqqqqat commited on
Commit
3d7eadf
·
1 Parent(s): 5c2014f
main.py ADDED
@@ -0,0 +1,233 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+
19
+ import cv2
20
+ from fastapi import FastAPI, File, UploadFile, Form, Request, Depends
21
+ from fastapi.responses import HTMLResponse, RedirectResponse
22
+ from fastapi.templating import Jinja2Templates
23
+ from fastapi.staticfiles import StaticFiles
24
+
25
+ sys.path.append(os.path.abspath(os.path.dirname(__file__)))
26
+ from models.densenet.preprocess.preprocessingwangchan import get_tokenizer, get_transforms
27
+ from models.densenet.train_densenet_only import DenseNet121Classifier
28
+ from models.densenet.train_text_only import TextClassifier
29
+ torch.manual_seed(42); np.random.seed(42); random.seed(42)
30
+ FUSION_LABELMAP_PATH = "models/densenet/label_map_fusion_densenet.json"
31
+ FUSION_WEIGHTS_PATH = "models/densenet/best_fusion_densenet.pth"
32
+ with open(FUSION_LABELMAP_PATH, "r", encoding="utf-8") as f:
33
+ label_map = json.load(f)
34
+ class_names = [label for label, _ in sorted(label_map.items(), key=lambda x: x[1])]
35
+ NUM_CLASSES = len(class_names)
36
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
37
+ print(f"🧠 Using device: {device}")
38
+ class FusionDenseNetText(nn.Module):
39
+ def __init__(self, num_classes, dropout=0.3):
40
+ super().__init__()
41
+ self.image_model = DenseNet121Classifier(num_classes=num_classes)
42
+ self.text_model = TextClassifier(num_classes=num_classes)
43
+ self.fusion = nn.Sequential(
44
+ nn.Linear(num_classes * 2, 128), nn.ReLU(),
45
+ nn.Dropout(dropout), nn.Linear(128, num_classes)
46
+ )
47
+ def forward(self, image, input_ids, attention_mask):
48
+ logits_img = self.image_model(image)
49
+ logits_txt = self.text_model(input_ids, attention_mask)
50
+ fused_in = torch.cat([logits_img, logits_txt], dim=1)
51
+ fused_out = self.fusion(fused_in)
52
+ return fused_out, logits_img, logits_txt
53
+ print("🔄 Loading AI model...")
54
+ fusion_model = FusionDenseNetText(num_classes=NUM_CLASSES).to(device)
55
+ fusion_model.load_state_dict(torch.load(FUSION_WEIGHTS_PATH, map_location=device))
56
+ fusion_model.eval()
57
+ print("✅ AI Model loaded successfully!")
58
+ tokenizer = get_tokenizer()
59
+ transform = get_transforms((224, 224))
60
+ def _find_last_conv2d(mod: torch.nn.Module):
61
+ last = None
62
+ for m in mod.modules():
63
+ if isinstance(m, torch.nn.Conv2d): last = m
64
+ return last
65
+ def compute_gradcam_overlay(img_pil, image_tensor, target_class_idx):
66
+ img_branch = fusion_model.image_model
67
+ target_layer = _find_last_conv2d(img_branch)
68
+ if target_layer is None: return None
69
+ activations, gradients = [], []
70
+ def fwd_hook(_m, _i, o): activations.append(o)
71
+ def bwd_hook(_m, gin, gout): gradients.append(gout[0])
72
+ h1 = target_layer.register_forward_hook(fwd_hook)
73
+ h2 = target_layer.register_full_backward_hook(bwd_hook)
74
+ try:
75
+ img_branch.zero_grad()
76
+ logits_img = img_branch(image_tensor)
77
+ score = logits_img[0, target_class_idx]
78
+ score.backward()
79
+ act = activations[-1].detach()[0]
80
+ grad = gradients[-1].detach()[0]
81
+ weights = torch.mean(grad, dim=(1, 2))
82
+ cam = torch.relu(torch.sum(weights[:, None, None] * act, dim=0))
83
+ cam -= cam.min(); cam /= (cam.max() + 1e-8)
84
+ cam_img = Image.fromarray((cam.cpu().numpy() * 255).astype(np.uint8)).resize(img_pil.size, Image.BILINEAR)
85
+ cam_np = np.asarray(cam_img).astype(np.float32) / 255.0
86
+ heatmap = cm.get_cmap("jet")(cam_np)[:, :, :3]
87
+ img_np = np.asarray(img_pil.convert("RGB")).astype(np.float32) / 255.0
88
+ overlay = (0.6 * img_np + 0.4 * heatmap)
89
+ return np.clip(overlay * 255, 0, 255).astype(np.uint8)
90
+ finally:
91
+ h1.remove(); h2.remove(); img_branch.zero_grad()
92
+
93
+
94
+ app = FastAPI()
95
+ app.mount("/static", StaticFiles(directory="static"), name="static")
96
+ templates = Jinja2Templates(directory="templates")
97
+ os.makedirs("uploads", exist_ok=True)
98
+
99
+ EXPIRATION_MINUTES = 10
100
+ results_cache = {}
101
+ cache_lock = threading.Lock()
102
+
103
+ def cleanup_expired_cache():
104
+ """
105
+ ฟังก์ชันนี้จะทำงานใน Background Thread เพื่อตรวจสอบและลบ Cache ที่หมดอายุ
106
+ """
107
+ while True:
108
+ with cache_lock: # ล็อคเพื่อความปลอดภัยในการเข้าถึง cache
109
+ # สร้าง list ของ key ที่จะลบ เพื่อไม่ให้แก้ไข dict ขณะวน loop
110
+ expired_keys = []
111
+ current_time = time.time()
112
+ for key, value in results_cache.items():
113
+ if current_time - value["created_at"] > EXPIRATION_MINUTES * 60:
114
+ expired_keys.append(key)
115
+
116
+ # ลบ key ที่หมดอายุ
117
+ for key in expired_keys:
118
+ del results_cache[key]
119
+ print(f"🧹 Cache expired and removed for key: {key}")
120
+
121
+ time.sleep(60) # ตรวจสอบทุกๆ 60 วินาที
122
+
123
+ @app.on_event("startup")
124
+ async def startup_event():
125
+ """
126
+ เริ่ม Background Thread สำหรับทำความสะอาด Cache เมื่อแอปเริ่มทำงาน
127
+ """
128
+ cleanup_thread = threading.Thread(target=cleanup_expired_cache, daemon=True)
129
+ cleanup_thread.start()
130
+ print("🗑️ Cache cleanup task started.")
131
+
132
+ SYMPTOM_MAP = {
133
+ "noSymptoms": "ไม่มีอาการ", "drinkAlcohol": "ดื่มเหล้า", "smoking": "สูบบุหรี่",
134
+ "chewBetelNut": "เคี้ยวหมาก", "eatSpicyFood": "กินเผ็ดแสบ", "wipeOff": "เช็ดออกได้",
135
+ "alwaysHurts": "เจ็บเมื่อโดนแผล"
136
+ }
137
+ def process_with_ai_model(image_path: str, prompt_text: str):
138
+ try:
139
+ image_pil = Image.open(image_path)
140
+ image_pil = ImageOps.exif_transpose(image_pil)
141
+ image_pil = image_pil.convert("RGB")
142
+ image_tensor = transform(image_pil).unsqueeze(0).to(device)
143
+ enc = tokenizer(prompt_text, return_tensors="pt", padding="max_length",
144
+ truncation=True, max_length=128)
145
+ ids, mask = enc["input_ids"].to(device), enc["attention_mask"].to(device)
146
+ with torch.no_grad():
147
+ fused_logits, _, _ = fusion_model(image_tensor, ids, mask)
148
+ probs_fused = torch.softmax(fused_logits, dim=1)[0].cpu().numpy()
149
+ pred_idx = int(np.argmax(probs_fused))
150
+ pred_label = class_names[pred_idx]
151
+ confidence = float(probs_fused[pred_idx]) * 100
152
+ gradcam_overlay_np = compute_gradcam_overlay(image_pil, image_tensor, pred_idx)
153
+ def image_to_base64(img):
154
+ buffered = BytesIO()
155
+ img.save(buffered, format="JPEG")
156
+ return base64.b64encode(buffered.getvalue()).decode('utf-8')
157
+ original_b64 = image_to_base64(image_pil)
158
+ if gradcam_overlay_np is not None:
159
+ gradcam_pil = Image.fromarray(gradcam_overlay_np)
160
+ gradcam_b64 = image_to_base64(gradcam_pil)
161
+ else:
162
+ gradcam_b64 = original_b64
163
+ return original_b64, gradcam_b64, pred_label, f"{confidence:.2f}"
164
+ except Exception as e:
165
+ print(f"❌ Error during AI processing: {e}")
166
+ return None, None, "Error", "0.00"
167
+
168
+ @app.get("/", response_class=RedirectResponse)
169
+ async def root():
170
+ return RedirectResponse(url="/detect")
171
+ @app.get("/detect", response_class=HTMLResponse)
172
+ async def show_upload_form(request: Request):
173
+ return templates.TemplateResponse("detect.html", {"request": request})
174
+
175
+ @app.post("/uploaded")
176
+ async def handle_upload(
177
+ request: Request,
178
+ file: UploadFile = File(...),
179
+ checkboxes: List[str] = Form([]),
180
+ symptom_text: str = Form("")
181
+ ):
182
+ temp_filepath = os.path.join("uploads", f"{uuid.uuid4()}_{file.filename}")
183
+ with open(temp_filepath, "wb") as buffer:
184
+ shutil.copyfileobj(file.file, buffer)
185
+ final_prompt_parts = []
186
+ selected_symptoms_thai = {SYMPTOM_MAP.get(cb) for cb in checkboxes if SYMPTOM_MAP.get(cb)}
187
+ if "ไม่มีอาการ" in selected_symptoms_thai:
188
+ symptoms_group = {"เจ็บเมื่อโดนแผล", "กินเผ็ดแสบ"}
189
+ lifestyles_group = {"ดื่มเหล้า", "สูบบุหรี่", "เคี้ยวหมาก"}
190
+ patterns_group = {"เช็ดออกได้"}
191
+ special_group = {"ไม่มีอาการ"}
192
+ final_selected = (selected_symptoms_thai - symptoms_group) | \
193
+ (selected_symptoms_thai & (lifestyles_group | patterns_group | special_group))
194
+ final_prompt_parts.append(" ".join(sorted(list(final_selected))))
195
+ elif selected_symptoms_thai:
196
+ final_prompt_parts.append(" ".join(sorted(list(selected_symptoms_thai))))
197
+ if symptom_text and symptom_text.strip():
198
+ final_prompt_parts.append(symptom_text.strip())
199
+ final_prompt = "; ".join(final_prompt_parts) if final_prompt_parts else "ไม่มีอาการ"
200
+ image_b64, gradcam_b64, name_out, eva_output = process_with_ai_model(
201
+ image_path=temp_filepath, prompt_text=final_prompt
202
+ )
203
+ os.remove(temp_filepath)
204
+ result_id = str(uuid.uuid4())
205
+ result_data = {
206
+ "image_b64_data": image_b64, "gradcam_b64_data": gradcam_b64,
207
+ "name_out": name_out, "eva_output": eva_output,
208
+ }
209
+ with cache_lock:
210
+ results_cache[result_id] = {
211
+ "data": result_data,
212
+ "created_at": time.time()
213
+ }
214
+
215
+ results_url = request.url_for('show_results', result_id=result_id)
216
+ return RedirectResponse(url=results_url, status_code=303)
217
+
218
+ @app.get("/results/{result_id}", response_class=HTMLResponse)
219
+ async def show_results(request: Request, result_id: str):
220
+ with cache_lock:
221
+ cached_item = results_cache.get(result_id)
222
+ if not cached_item or (time.time() - cached_item["created_at"] > EXPIRATION_MINUTES * 60):
223
+ if cached_item:
224
+ with cache_lock:
225
+ del results_cache[result_id]
226
+ return RedirectResponse(url="/detect")
227
+
228
+ context = {"request": request, **cached_item["data"]}
229
+ return templates.TemplateResponse("detect.html", context)
230
+
231
+ if __name__ == "__main__":
232
+ import uvicorn
233
+ uvicorn.run(app, host="0.0.0.0", port=8000)
models/__init__.py ADDED
File without changes
models/densenet/__init__.py ADDED
File without changes
models/densenet/__pycache__/__init__.cpython-310.pyc ADDED
Binary file (170 Bytes). View file
 
models/densenet/__pycache__/train_densenet_only.cpython-310.pyc ADDED
Binary file (5.31 kB). View file
 
models/densenet/__pycache__/train_text_only.cpython-310.pyc ADDED
Binary file (5.34 kB). View file
 
models/densenet/best_fusion_densenet.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a2431a8b7f458d21df66690c625f631e2263f2b433bea3e4401a13e835a63d62
3
+ size 451379781
models/densenet/label_map_fusion_densenet.json ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ {
2
+ "Candidiasis": 0,
3
+ "Leukoplakia": 1,
4
+ "Lichenplanus": 2,
5
+ "Linea Alba Buccalis": 3,
6
+ "Ulcer": 4
7
+ }
models/densenet/preprocess/__pycache__/preprocessingwangchan.cpython-310.pyc ADDED
Binary file (3.75 kB). View file
 
models/densenet/preprocess/preprocessingwangchan.py ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ import os
3
+ import json
4
+ import torch
5
+ import pandas as pd
6
+ from PIL import Image
7
+ from torch.utils.data import Dataset
8
+ import torchvision.transforms as T
9
+ # Import โมดูลที่จำเป็น
10
+ from transformers import CamembertTokenizer
11
+
12
+ # ===== CONFIG =====
13
+ TEXT_MODEL_NAME = "airesearch/wangchanberta-base-att-spm-uncased"
14
+
15
+ # ใส่ค่า mean/std ของ "train set" คุณ (ที่เพิ่งคำนวณได้)
16
+ DATASET_MEAN = [0.6089681386947632, 0.411862850189209, 0.4016309380531311]
17
+ DATASET_STD = [0.19572971761226654, 0.17690013349056244, 0.170320525765419]
18
+
19
+ # คีย์ใน CSV
20
+ COL_IMG = "img_path"
21
+ COL_LBL = "disease"
22
+ COL_TXT = "caption"
23
+
24
+ # ข้อความกลางเมื่อไม่มีอาการ
25
+ NEUTRAL_PROMPT = "ไม่มีอาการ รายละเอียดไม่ระบุ ตำแหน่งไม่ระบุ"
26
+ MAX_LEN = 128
27
+ # ==================
28
+
29
+ def get_tokenizer():
30
+ # โหลด tokenizer โดยใช้ CamembertTokenizerFast
31
+ # ซึ่งใช้ SentencePiece และเข้ากันได้กับโมเดลนี้
32
+
33
+ return CamembertTokenizer.from_pretrained("airesearch/wangchanberta-base-att-spm-uncased", use_fast=False)
34
+
35
+
36
+ def get_transforms(img_size=(224,224)):
37
+ return T.Compose([
38
+ T.Resize(img_size),
39
+ T.ToTensor(),
40
+ T.Normalize(mean=DATASET_MEAN, std=DATASET_STD),
41
+ ])
42
+
43
+ class CustomDataset(Dataset):
44
+ """
45
+ อ่านจาก CSV: ต้องมี img_path, label, caption (caption ว่างได้)
46
+ label_map: dict เช่น {"Leukoplakia":0, "Lichen":1, ...}
47
+ """
48
+ def __init__(self, csv_path, tokenizer, transform, label_map=None):
49
+ self.df = pd.read_csv(csv_path)
50
+ assert COL_IMG in self.df.columns and COL_LBL in self.df.columns, \
51
+ f"CSV ต้องมีคอลัมน์ {COL_IMG}, {COL_LBL}"
52
+ if COL_TXT not in self.df.columns:
53
+ self.df[COL_TXT] = ""
54
+
55
+ self.tok = tokenizer
56
+ self.tf = transform
57
+ self.label_map = label_map or self._build_label_map()
58
+ self._check_paths()
59
+
60
+ def _build_label_map(self):
61
+ classes = sorted(self.df[COL_LBL].astype(str).unique().tolist())
62
+ return {c:i for i,c in enumerate(classes)}
63
+
64
+ def _check_paths(self):
65
+ # แจ้งเตือนเฉย ๆ ถ้ารูปหาย
66
+ missing = (~self.df[COL_IMG].astype(str).apply(os.path.exists)).sum()
67
+ if missing > 0:
68
+ print(f"⚠️ พบรูปหาย {missing} ไฟล์ ใน {len(self.df)} แถว (จะ error ตอน __getitem__ ถ้าถึงแถวนั้น)")
69
+
70
+ def __len__(self):
71
+ return len(self.df)
72
+
73
+ def __getitem__(self, idx):
74
+ row = self.df.iloc[idx]
75
+
76
+ # Image
77
+ img_path = str(row[COL_IMG])
78
+ image = Image.open(img_path).convert("RGB")
79
+ image = self.tf(image)
80
+
81
+ # Label
82
+ label_name = str(row[COL_LBL])
83
+ label = torch.tensor(self.label_map[label_name], dtype=torch.long)
84
+
85
+ # Text
86
+ text_raw = str(row.get(COL_TXT, "") or "").strip()
87
+ text = text_raw if text_raw else NEUTRAL_PROMPT
88
+
89
+ enc = self.tok(
90
+ text,
91
+ padding="max_length",
92
+ truncation=True,
93
+ max_length=MAX_LEN,
94
+ return_tensors="pt"
95
+ )
96
+ input_ids = enc["input_ids"].squeeze(0) # (L,)
97
+ attention_mask = enc["attention_mask"].squeeze(0)
98
+
99
+ return {
100
+ "image": image, # (3,H,W)
101
+ "input_ids": input_ids, # (L,)
102
+ "attention_mask": attention_mask,
103
+ "label": label,
104
+ "text_is_neutral": torch.tensor(int(text_raw == ""), dtype=torch.long)
105
+ }
models/densenet/train_densenet_only.py ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ import os, argparse, json, random, csv, sys
3
+ import numpy as np
4
+ import torch
5
+ import torch.nn as nn
6
+ import torch.nn.functional as F
7
+ from torch.utils.data import DataLoader
8
+ from torchvision import models
9
+ from torch.optim import AdamW
10
+ from transformers import get_linear_schedule_with_warmup
11
+ from sklearn.metrics import accuracy_score
12
+
13
+ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
14
+ from .preprocess.preprocessingwangchan import CustomDataset, get_transforms, get_tokenizer
15
+
16
+ def set_seed(seed=42):
17
+ random.seed(seed); np.random.seed(seed)
18
+ torch.manual_seed(seed); torch.cuda.manual_seed_all(seed)
19
+
20
+ @torch.no_grad()
21
+ def evaluate(model, loader, device):
22
+ model.eval(); y_true,y_pred,loss_sum,n_sum=[],[],0.0,0
23
+ for b in loader:
24
+ x, y = b["image"].to(device), b["label"].to(device)
25
+ logits = model(x)
26
+ loss = F.cross_entropy(logits, y)
27
+ loss_sum += loss.item()*y.size(0)
28
+ pred = logits.argmax(dim=1)
29
+ y_true.extend(y.cpu().tolist()); y_pred.extend(pred.cpu().tolist())
30
+ n_sum+=y.size(0)
31
+ return loss_sum/n_sum, accuracy_score(y_true,y_pred)
32
+
33
+ class DenseNet121Classifier(nn.Module):
34
+ def __init__(self, num_classes, dropout=0.3):
35
+ super().__init__()
36
+ base = models.densenet121(weights="IMAGENET1K_V1")
37
+ in_feat = base.classifier.in_features
38
+ base.classifier = nn.Identity()
39
+ self.encoder = base
40
+ self.classifier = nn.Sequential(
41
+ nn.Linear(in_feat, 256), nn.ReLU(), nn.Dropout(dropout),
42
+ nn.Linear(256, num_classes)
43
+ )
44
+ def forward(self, x):
45
+ feat = self.encoder(x)
46
+ return self.classifier(feat)
47
+
48
+ def main(args):
49
+ set_seed(args.seed)
50
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
51
+
52
+ tfm = get_transforms((args.img_size, args.img_size))
53
+ tok = get_tokenizer()
54
+
55
+ ds_tmp = CustomDataset(args.train_csv, tok, tfm, label_map=None)
56
+ label_map = ds_tmp.label_map
57
+ os.makedirs(args.out_dir, exist_ok=True)
58
+ with open(os.path.join(args.out_dir,"label_map_imgdensenet121.json"),"w",encoding="utf-8") as f:
59
+ json.dump(label_map,f,ensure_ascii=False,indent=2)
60
+
61
+ ds_tr = CustomDataset(args.train_csv, tok, tfm, label_map=label_map)
62
+ ds_va = CustomDataset(args.val_csv, tok, tfm, label_map=label_map)
63
+ dl_tr = DataLoader(ds_tr, batch_size=args.batch_size, shuffle=True)
64
+ dl_va = DataLoader(ds_va, batch_size=args.batch_size)
65
+
66
+ model = DenseNet121Classifier(num_classes=len(label_map)).to(device)
67
+ optim = AdamW(model.parameters(), lr=args.lr, weight_decay=args.weight_decay)
68
+ sched = get_linear_schedule_with_warmup(
69
+ optim, int(0.1*len(dl_tr)*args.epochs), len(dl_tr)*args.epochs
70
+ )
71
+
72
+ best_acc, patience = -1,0
73
+ best_path=os.path.join(args.out_dir,"best_densenet121_img.pth")
74
+ last_path=os.path.join(args.out_dir,"last_densenet121_img.pth")
75
+
76
+ # 🔹 CSV log
77
+ csv_path = os.path.join(args.out_dir,"metrics_densenet121_img.csv")
78
+ with open(csv_path,"w",newline="",encoding="utf-8") as f:
79
+ writer=csv.writer(f)
80
+ writer.writerow(["epoch","train_loss","train_acc","val_loss","val_acc"])
81
+
82
+ for ep in range(1,args.epochs+1):
83
+ model.train(); y_true,y_pred,loss_sum,n_sum=[],[],0.0,0
84
+ for b in dl_tr:
85
+ x,y=b["image"].to(device),b["label"].to(device)
86
+ optim.zero_grad()
87
+ logits=model(x)
88
+ loss=F.cross_entropy(logits,y)
89
+ loss.backward(); optim.step(); sched.step()
90
+ pred=logits.argmax(dim=1)
91
+ y_true.extend(y.cpu().tolist()); y_pred.extend(pred.cpu().tolist())
92
+ loss_sum+=loss.item()*y.size(0); n_sum+=y.size(0)
93
+
94
+ tr_loss=loss_sum/n_sum; tr_acc=accuracy_score(y_true,y_pred)
95
+ va_loss,va_acc=evaluate(model,dl_va,device)
96
+
97
+ # 🔹 print
98
+ print(f"Epoch {ep}: TrainLoss={tr_loss:.4f}, TrainAcc={tr_acc:.4f}, ValLoss={va_loss:.4f}, ValAcc={va_acc:.4f}")
99
+
100
+ # 🔹 log to CSV
101
+ with open(csv_path,"a",newline="",encoding="utf-8") as f:
102
+ writer=csv.writer(f)
103
+ writer.writerow([ep,f"{tr_loss:.4f}",f"{tr_acc:.4f}",f"{va_loss:.4f}",f"{va_acc:.4f}"])
104
+
105
+ # save last
106
+ torch.save(model.state_dict(),last_path)
107
+
108
+ # save best
109
+ if va_acc>best_acc:
110
+ best_acc=va_acc
111
+ torch.save(model.state_dict(),best_path)
112
+ patience=0
113
+ print("💾 Saved best model")
114
+ else:
115
+ patience+=1
116
+ if patience>=args.patience:
117
+ print("⏹️ Early stopping")
118
+ break
119
+
120
+ torch.save(model.state_dict(),last_path)
121
+ print("💾 Training finished, last model saved")
122
+
123
+ if __name__=="__main__":
124
+ ap=argparse.ArgumentParser()
125
+ ap.add_argument("--train_csv",default="C://pattyarea//project1_weighted//data//train.csv")
126
+ ap.add_argument("--val_csv",default="C://pattyarea//project1_weighted//data//val.csv")
127
+ ap.add_argument("--out_dir",default="C://pattyarea//project1_weighted//weights")
128
+ ap.add_argument("--img_size",type=int,default=224)
129
+ ap.add_argument("--batch_size",type=int,default=8)
130
+ ap.add_argument("--epochs",type=int,default=50)
131
+ ap.add_argument("--patience",type=int,default=50)
132
+ ap.add_argument("--lr",type=float,default=1e-4) # 🔹 DenseNet ใช้ lr ประมาณนี้
133
+ ap.add_argument("--weight_decay",type=float,default=1e-4)
134
+ ap.add_argument("--seed",type=int,default=42)
135
+ args=ap.parse_args(); main(args)
models/densenet/train_text_only.py ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ import os, argparse, json, random, csv,sys
3
+ import numpy as np
4
+ import torch
5
+ import torch.nn as nn
6
+ import torch.nn.functional as F
7
+ from torch.utils.data import DataLoader
8
+ from transformers import AutoModel, get_linear_schedule_with_warmup
9
+ from torch.optim import AdamW
10
+ from sklearn.metrics import accuracy_score
11
+
12
+
13
+ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
14
+ from .preprocess.preprocessingwangchan import CustomDataset, get_tokenizer, get_transforms
15
+
16
+ TEXT_MODEL_NAME = "airesearch/wangchanberta-base-att-spm-uncased"
17
+
18
+ def set_seed(seed=42):
19
+ random.seed(seed); np.random.seed(seed)
20
+ torch.manual_seed(seed); torch.cuda.manual_seed_all(seed)
21
+
22
+ class TextClassifier(nn.Module):
23
+ def __init__(self, num_classes, dropout=0.3):
24
+ super().__init__()
25
+ self.encoder = AutoModel.from_pretrained(TEXT_MODEL_NAME)
26
+ self.classifier = nn.Sequential(
27
+ nn.Linear(768, 256), nn.ReLU(), nn.Dropout(dropout),
28
+ nn.Linear(256, num_classes)
29
+ )
30
+ def forward(self, ids, mask):
31
+ out = self.encoder(input_ids=ids, attention_mask=mask)
32
+ cls = out.last_hidden_state[:,0,:]
33
+ return self.classifier(cls)
34
+
35
+ @torch.no_grad()
36
+ def evaluate(model, loader, device):
37
+ model.eval(); y_true, y_pred, loss_sum, n_sum = [], [], 0.0, 0
38
+ for b in loader:
39
+ ids, mask, y = b["input_ids"].to(device), b["attention_mask"].to(device), b["label"].to(device)
40
+ logits = model(ids, mask)
41
+ loss = F.cross_entropy(logits, y)
42
+ loss_sum += loss.item()*y.size(0)
43
+ pred = logits.argmax(dim=1)
44
+ y_true.extend(y.cpu().tolist()); y_pred.extend(pred.cpu().tolist())
45
+ n_sum+=y.size(0)
46
+ return loss_sum/n_sum, accuracy_score(y_true,y_pred)
47
+
48
+ def main(args):
49
+ set_seed(args.seed)
50
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
51
+
52
+ tok = get_tokenizer(); tfm = get_transforms((args.img_size,args.img_size))
53
+ ds_tmp = CustomDataset(args.train_csv, tok, tfm, label_map=None)
54
+ label_map = ds_tmp.label_map
55
+ os.makedirs(args.out_dir, exist_ok=True)
56
+ with open(os.path.join(args.out_dir,"label_map_txt.json"),"w",encoding="utf-8") as f:
57
+ json.dump(label_map,f,ensure_ascii=False,indent=2)
58
+
59
+ ds_tr = CustomDataset(args.train_csv, tok, tfm, label_map=label_map)
60
+ ds_va = CustomDataset(args.val_csv, tok, tfm, label_map=label_map)
61
+ dl_tr = DataLoader(ds_tr, batch_size=args.batch_size, shuffle=True)
62
+ dl_va = DataLoader(ds_va, batch_size=args.batch_size)
63
+
64
+ model = TextClassifier(num_classes=len(label_map)).to(device)
65
+ optim = AdamW(model.parameters(), lr=args.lr, weight_decay=args.weight_decay)
66
+ sched = get_linear_schedule_with_warmup(
67
+ optim, int(0.1*len(dl_tr)*args.epochs), len(dl_tr)*args.epochs
68
+ )
69
+
70
+ best_acc, patience= -1,0
71
+ best_path=os.path.join(args.out_dir,"best_text.pth")
72
+ last_path=os.path.join(args.out_dir,"last_text.pth")
73
+
74
+ # ===== prepare CSV logger =====
75
+ csv_path = os.path.join(args.out_dir, "metrics_log_text.csv")
76
+ with open(csv_path, "w", newline="", encoding="utf-8") as f:
77
+ writer = csv.writer(f)
78
+ writer.writerow(["epoch","train_loss","train_acc","val_loss","val_acc"])
79
+
80
+ for ep in range(1,args.epochs+1):
81
+ model.train(); y_true,y_pred,loss_sum,n_sum=[],[],0.0,0
82
+ for b in dl_tr:
83
+ ids, mask, y = b["input_ids"].to(device), b["attention_mask"].to(device), b["label"].to(device)
84
+ optim.zero_grad()
85
+ logits=model(ids,mask)
86
+ loss=F.cross_entropy(logits,y)
87
+ loss.backward(); optim.step(); sched.step()
88
+ pred=logits.argmax(dim=1)
89
+ y_true.extend(y.cpu().tolist()); y_pred.extend(pred.cpu().tolist())
90
+ loss_sum+=loss.item()*y.size(0); n_sum+=y.size(0)
91
+ tr_loss=loss_sum/n_sum; tr_acc=accuracy_score(y_true,y_pred)
92
+ va_loss,va_acc=evaluate(model,dl_va,device)
93
+
94
+ # ==== Print & Log ====
95
+ print(
96
+ f"Epoch {ep}/{args.epochs} | "
97
+ f"TrainLoss={tr_loss:.4f} | TrainAcc={tr_acc:.4f} | "
98
+ f"ValLoss={va_loss:.4f} | ValAcc={va_acc:.4f}"
99
+ )
100
+ with open(csv_path, "a", newline="", encoding="utf-8") as f:
101
+ writer = csv.writer(f)
102
+ writer.writerow([ep, f"{tr_loss:.4f}", f"{tr_acc:.4f}", f"{va_loss:.4f}", f"{va_acc:.4f}"])
103
+
104
+ torch.save(model.state_dict(),last_path)
105
+ if va_acc>best_acc:
106
+ best_acc=va_acc
107
+ torch.save(model.state_dict(),best_path)
108
+ patience=0
109
+ print("💾 saved best_text.pth")
110
+ else:
111
+ patience+=1
112
+ if patience>=args.patience:
113
+ print("⏹️ Early stopping"); break
114
+
115
+ if __name__=="__main__":
116
+ ap=argparse.ArgumentParser()
117
+ ap.add_argument("--train_csv",default="C://pattyarea//project1_weighted//data//train.csv")
118
+ ap.add_argument("--val_csv",default="C://pattyarea//project1_weighted//data//val.csv")
119
+ ap.add_argument("--out_dir",default="C://pattyarea//project1_weighted//weights")
120
+ ap.add_argument("--img_size",type=int,default=224)
121
+ ap.add_argument("--batch_size",type=int,default=8)
122
+ ap.add_argument("--epochs",type=int,default=50)
123
+ ap.add_argument("--patience",type=int,default=50)
124
+ ap.add_argument("--lr",type=float,default=2e-5)
125
+ ap.add_argument("--weight_decay",type=float,default=0.01)
126
+ ap.add_argument("--seed",type=int,default=42)
127
+ args=ap.parse_args();main(args)
requirements.txt ADDED
Binary file (3.63 kB). View file
 
static/css/detect.css ADDED
@@ -0,0 +1,672 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ a {
2
+ text-decoration: none;
3
+ }
4
+
5
+ .background-color {
6
+ background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
7
+ box-shadow: inset 0 4px 8px rgba(0, 0, 0, 0.2);
8
+
9
+ }
10
+
11
+ /* Body: Flexbox layout for better vertical alignment */
12
+ body {
13
+ display: flex;
14
+ flex-direction: column;
15
+ min-height: 100vh;
16
+ box-sizing: border-box;
17
+ }
18
+
19
+ /* Navbar: Adjust margins and padding for small screens */
20
+ .navbar {
21
+ padding: 10px 20px;
22
+ }
23
+
24
+ /* Ensure home content and services adapt to screen width */
25
+ .content-home {
26
+ display: flex;
27
+ flex-direction: column;
28
+ align-items: center;
29
+ justify-content: center;
30
+ gap: 50px;
31
+ padding: 30px 10px;
32
+ color: #1d3fa1;
33
+ }
34
+
35
+ .home-row {
36
+ display: flex;
37
+ gap: 30px;
38
+ align-items: center;
39
+ justify-content: center;
40
+ flex-wrap: wrap;
41
+ }
42
+
43
+ /* Left section in home content for proper spacing */
44
+ .home-left {
45
+ position: relative;
46
+ max-width: 600px;
47
+ padding: 20px;
48
+ text-align: center;
49
+ }
50
+
51
+ .home-right img {
52
+ max-width: 100%;
53
+ height: auto;
54
+ }
55
+
56
+ /* Flexbox for service items */
57
+ .service {
58
+ display: flex;
59
+ gap: 20px;
60
+ justify-content: center;
61
+ flex-wrap: wrap;
62
+ }
63
+
64
+ .service-items {
65
+ background-color: #e0f7ff;
66
+ padding: 20px;
67
+ border-radius: 10px;
68
+ box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
69
+ transition: transform 0.3s ease;
70
+ width: 250px;
71
+ text-align: center;
72
+ }
73
+
74
+ .service-items:hover {
75
+ transform: scale(1.05);
76
+ background-color: #fff7ff;
77
+ box-shadow: 0 8px 20px rgba(0, 0, 0, 0.2);
78
+ }
79
+
80
+ footer {
81
+ background-color: #f8f9fa;
82
+ text-align: center;
83
+ padding: 10px 0;
84
+ margin-top: auto;
85
+ }
86
+
87
+ /* Responsive Adjustments */
88
+ @media (max-width: 768px) {
89
+ .content-home {
90
+ padding: 20px;
91
+ gap: 30px;
92
+ }
93
+
94
+ .home-row {
95
+ flex-direction: column;
96
+ gap: 20px;
97
+ }
98
+
99
+ .home-left {
100
+ padding: 10px;
101
+ }
102
+
103
+ .service-items {
104
+ width: 100%;
105
+ max-width: 300px;
106
+ }
107
+
108
+
109
+ }
110
+
111
+ @media (max-width: 500px) {
112
+ .navbar-brand img {
113
+ width: 75px;
114
+ height: 75px;
115
+ }
116
+
117
+ .home-row {
118
+ gap: 15px;
119
+ }
120
+
121
+ .home-right img {
122
+ max-width: 100%;
123
+ height: auto;
124
+ }
125
+
126
+ .service-items {
127
+ width: 90%;
128
+ }
129
+
130
+
131
+ }
132
+
133
+ .logo {
134
+ width: 100px; /* Increase the width */
135
+ height: auto; /* Adjust height proportionally */
136
+ }
137
+
138
+ /*--------------------------------------------------------------------------------------*/
139
+
140
+ /* Global Styles */
141
+ body {
142
+ box-sizing: border-box;
143
+ margin: 0;
144
+ padding: 0;
145
+ font-family: 'IBM Plex Sans Thai', sans-serif;
146
+ }
147
+
148
+ /* Content Layout */
149
+ .content {
150
+ display: flex;
151
+ flex-wrap: wrap; /* Enable wrapping for responsiveness */
152
+ justify-content: space-between;
153
+ padding: 1rem;
154
+ }
155
+
156
+ /* Left section styling */
157
+ .left {
158
+ flex: 1 1 50%; /* Takes up 50% of the width */
159
+ padding: 20px;
160
+ }
161
+
162
+ .left .form {
163
+ max-width: 500px;
164
+ padding: 20px;
165
+ background-color: #f9f9f9;
166
+ border-radius: 8px;
167
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
168
+ }
169
+
170
+ .button-layout {
171
+ display: flex;
172
+ justify-content: center;
173
+ padding: 10px;
174
+ }
175
+
176
+ /* Checkbox styling */
177
+ .symptoms {
178
+ font-size: 19px;
179
+ margin-right: 10px;
180
+ }
181
+
182
+ input[type="checkbox"] {
183
+ margin-bottom: 10px;
184
+ transform: scale(0.9);
185
+ }
186
+
187
+ /* Add hover effects */
188
+ input[type="checkbox"]:hover {
189
+ cursor: pointer;
190
+ transform: scale(1.1);
191
+ transition: transform 0.2s ease-in-out;
192
+ }
193
+
194
+ .left, .right {
195
+ flex: 1 1 100%; /* Stack the sections on top of each other */
196
+ padding: 10px;
197
+ }
198
+
199
+ .content {
200
+ flex-direction: column;
201
+ align-items: center;
202
+ }
203
+
204
+ .form, .right-top {
205
+ width: 100%;
206
+ }
207
+
208
+ input[type="checkbox"] {
209
+ margin-bottom: 10px;
210
+ transform: scale(1.5); /* Make checkboxes smaller for mobile */
211
+ }
212
+
213
+ /* From Uiverse.io by zjssun */
214
+ .button {
215
+ position: relative;
216
+ padding: 10px 22px;
217
+ border-radius: 6px;
218
+ border: none;
219
+ color: #fff;
220
+ cursor: pointer;
221
+ background-color: #7d2ae8;
222
+ transition: all 0.2s ease;
223
+ }
224
+
225
+ .button:active {
226
+ transform: scale(0.96);
227
+ }
228
+
229
+ .button:before,
230
+ .button:after {
231
+ position: absolute;
232
+ content: "";
233
+ width: 150%;
234
+ left: 50%;
235
+ height: 100%;
236
+ transform: translateX(-50%);
237
+ z-index: -1000;
238
+ background-repeat: no-repeat;
239
+ }
240
+
241
+ .button:hover:before {
242
+ top: -70%;
243
+ background-image: radial-gradient(circle, #7d2ae8 20%, transparent 20%),
244
+ radial-gradient(circle, transparent 20%, #7d2ae8 20%, transparent 30%),
245
+ radial-gradient(circle, #7d2ae8 20%, transparent 20%),
246
+ radial-gradient(circle, #7d2ae8 20%, transparent 20%),
247
+ radial-gradient(circle, transparent 10%, #7d2ae8 15%, transparent 20%),
248
+ radial-gradient(circle, #7d2ae8 20%, transparent 20%),
249
+ radial-gradient(circle, #7d2ae8 20%, transparent 20%),
250
+ radial-gradient(circle, #7d2ae8 20%, transparent 20%),
251
+ radial-gradient(circle, #7d2ae8 20%, transparent 20%);
252
+ background-size: 10% 10%, 20% 20%, 15% 15%, 20% 20%, 18% 18%, 10% 10%, 15% 15%,
253
+ 10% 10%, 18% 18%;
254
+ background-position: 50% 120%;
255
+ animation: greentopBubbles 0.6s ease;
256
+ }
257
+
258
+ @keyframes greentopBubbles {
259
+ 0% {
260
+ background-position: 5% 90%, 10% 90%, 10% 90%, 15% 90%, 25% 90%, 25% 90%,
261
+ 40% 90%, 55% 90%, 70% 90%;
262
+ }
263
+
264
+ 50% {
265
+ background-position: 0% 80%, 0% 20%, 10% 40%, 20% 0%, 30% 30%, 22% 50%,
266
+ 50% 50%, 65% 20%, 90% 30%;
267
+ }
268
+
269
+ 100% {
270
+ background-position: 0% 70%, 0% 10%, 10% 30%, 20% -10%, 30% 20%, 22% 40%,
271
+ 50% 40%, 65% 10%, 90% 20%;
272
+ background-size: 0% 0%, 0% 0%, 0% 0%, 0% 0%, 0% 0%, 0% 0%;
273
+ }
274
+ }
275
+
276
+ .button:hover:after {
277
+ bottom: -70%;
278
+ background-image: radial-gradient(circle, #e82a2a 20%, transparent 20%),
279
+ radial-gradient(circle, #e82a2a 20%, transparent 20%),
280
+ radial-gradient(circle, transparent 10%, #e82a2a 15%, transparent 20%),
281
+ radial-gradient(circle, #e82a2a 20%, transparent 20%),
282
+ radial-gradient(circle, #e82a2a 20%, transparent 20%),
283
+ radial-gradient(circle, #e82a2a 20%, transparent 20%),
284
+ radial-gradient(circle, #e82a2a 20%, transparent 20%);
285
+ background-size: 15% 15%, 20% 20%, 18% 18%, 20% 20%, 15% 15%, 20% 20%, 18% 18%;
286
+ background-position: 50% 0%;
287
+ animation: greenbottomBubbles 0.6s ease;
288
+ }
289
+
290
+ @keyframes greenbottomBubbles {
291
+ 0% {
292
+ background-position: 10% -10%, 30% 10%, 55% -10%, 70% -10%, 85% -10%,
293
+ 70% -10%, 70% 0%;
294
+ }
295
+
296
+ 50% {
297
+ background-position: 0% 80%, 20% 80%, 45% 60%, 60% 100%, 75% 70%, 95% 60%,
298
+ 105% 0%;
299
+ }
300
+
301
+ 100% {
302
+ background-position: 0% 90%, 20% 90%, 45% 70%, 60% 110%, 75% 80%, 95% 70%,
303
+ 110% 10%;
304
+ background-size: 0% 0%, 0% 0%, 0% 0%, 0% 0%, 0% 0%, 0% 0%;
305
+ }
306
+ }
307
+
308
+
309
+ /*เส้นบน----------------------------------------------------------------------------------------------------------*/
310
+
311
+ /* Navbar links hover animation */
312
+ .navbar-nav .nav-link {
313
+ position: relative;
314
+ color: #000;
315
+ padding: 10px;
316
+ font-size: 1rem;
317
+ transition: color 0.3s ease-in-out, transform 0.3s ease-in-out;
318
+ }
319
+
320
+ .navbar-nav .nav-link:hover {
321
+ color: #007bff; /* Change the color on hover */
322
+
323
+ }
324
+
325
+ /* Underline effect: starts from center and expands outward */
326
+ .navbar-nav .nav-link::after {
327
+ content: '';
328
+ position: absolute;
329
+ width: 0;
330
+ height: 2px;
331
+ bottom: -5px;
332
+ left: 50%;
333
+ background-color: #007bff;
334
+ transform: translateX(-50%);
335
+ transition: width 0.4s ease-in-out; /* Smooth expansion */
336
+ }
337
+
338
+ .navbar-nav .nav-link:hover::after {
339
+ width: 100%; /* Expand to full width */
340
+ left: 50%;
341
+ transform: translateX(-50%);
342
+ }
343
+
344
+ /*เส้นล่าง-----------------------------------------------------------------------------------------------------------------------------*/
345
+ /* Add a color-changing line at the top of the navbar */
346
+ .navbar {
347
+ position: relative;
348
+ z-index: 1;
349
+ }
350
+ .navbar::before {
351
+ content: "";
352
+ position: absolute;
353
+ top: 0;
354
+ left: 0;
355
+ width: 100%;
356
+ height: 6px; /* Set the height of the color line */
357
+ background: linear-gradient(90deg, #71C9CE 0%, #A6E3E9 25%, #CBF1F5 75%, #E3FDFD 100%);
358
+ background-size: 600%;
359
+ animation: colorChange 6s linear infinite; /* Infinite animation */
360
+ }
361
+
362
+ /* Keyframes to animate the gradient colors */
363
+ @keyframes colorChange {
364
+ 0% {
365
+ background-position: 0% 50%;
366
+ }
367
+ 50% {
368
+ background-position: 100% 50%;
369
+ }
370
+ 100% {
371
+ background-position: 0% 50%;
372
+ }
373
+ }
374
+
375
+
376
+ .footer {
377
+ position: relative;
378
+ z-index: 1;
379
+ padding: 20px 0; /* Adjust padding as needed */
380
+ text-align: center; /* Center the footer text */
381
+ }
382
+
383
+ .footer::after { /* Use ::after to place the gradient at the bottom */
384
+ content: "";
385
+ position: absolute;
386
+ bottom: 0; /* Align it to the bottom */
387
+ left: 0;
388
+ width: 100%;
389
+ height: 6px; /* Set the height of the color line */
390
+ background: linear-gradient(90deg, #71C9CE 0%, #A6E3E9 25%, #CBF1F5 75%, #E3FDFD 100%);
391
+ background-size: 600%;
392
+ animation: colorChange 6s linear infinite; /* Infinite animation */
393
+ }
394
+
395
+ /* Keyframes to animate the gradient colors */
396
+ @keyframes colorChange {
397
+ 0% {
398
+ background-position: 0% 50%;
399
+ }
400
+ 50% {
401
+ background-position: 100% 50%;
402
+ }
403
+ 100% {
404
+ background-position: 0% 50%;
405
+ }
406
+ }
407
+
408
+ /*-----------------------------------------------------------------------------------------------------------------------------*/
409
+
410
+ /*animate background*/
411
+ .circles {
412
+ position: absolute;
413
+ top: 100px;
414
+ left: 0;
415
+ width: 100%;
416
+ height: 1200px;
417
+
418
+ }
419
+
420
+ .circles li {
421
+ position: absolute;
422
+ display: block;
423
+ list-style: none;
424
+ width: 20px;
425
+ height: 70px;
426
+ background: rgba(255, 255, 255, 0.5);
427
+ animation: animate 25s linear infinite;
428
+ bottom: 0px;
429
+
430
+ }
431
+
432
+ .circles li:nth-child(1) {
433
+ left: 25%;
434
+ width: 80px;
435
+ height: 80px;
436
+ animation-delay: 0s;
437
+ }
438
+
439
+
440
+ .circles li:nth-child(2) {
441
+ left: 10%;
442
+ width: 20px;
443
+ height: 20px;
444
+ animation-delay: 2s;
445
+ animation-duration: 12s;
446
+ }
447
+
448
+ .circles li:nth-child(3) {
449
+ left: 70%;
450
+ width: 20px;
451
+ height: 20px;
452
+ animation-delay: 4s;
453
+ }
454
+
455
+ .circles li:nth-child(4) {
456
+ left: 40%;
457
+ width: 60px;
458
+ height: 60px;
459
+ animation-delay: 0s;
460
+ animation-duration: 18s;
461
+ }
462
+
463
+ .circles li:nth-child(5) {
464
+ left: 65%;
465
+ width: 20px;
466
+ height: 20px;
467
+ animation-delay: 0s;
468
+ }
469
+
470
+ .circles li:nth-child(6) {
471
+ left: 75%;
472
+ width: 110px;
473
+ height: 110px;
474
+ animation-delay: 3s;
475
+ }
476
+
477
+ .circles li:nth-child(7) {
478
+ left: 35%;
479
+ width: 150px;
480
+ height: 150px;
481
+ animation-delay: 7s;
482
+ }
483
+
484
+ .circles li:nth-child(8) {
485
+ left: 50%;
486
+ width: 25px;
487
+ height: 25px;
488
+ animation-delay: 15s;
489
+ animation-duration: 45s;
490
+ }
491
+
492
+ .circles li:nth-child(9) {
493
+ left: 20%;
494
+ width: 15px;
495
+ height: 15px;
496
+ animation-delay: 2s;
497
+ animation-duration: 35s;
498
+ }
499
+
500
+ .circles li:nth-child(10) {
501
+ left: 85%;
502
+ width: 150px;
503
+ height: 150px;
504
+ animation-delay: 0s;
505
+ animation-duration: 11s;
506
+ }
507
+
508
+
509
+
510
+ @keyframes animate {
511
+
512
+ 0% {
513
+ transform: translateY(500) rotate(0deg);
514
+ opacity: 1;
515
+ border-radius: 0;
516
+ }
517
+
518
+ 100% {
519
+ transform: translateY(-1500px) rotate(720deg);
520
+ opacity: 0;
521
+ border-radius: 50%;
522
+ }
523
+
524
+ }
525
+
526
+ .footer {
527
+ background-color: #333;
528
+ color: #fff;
529
+ padding: 20px 10%;
530
+ }
531
+ .footer-container {
532
+ display: flex;
533
+ flex-wrap: wrap;
534
+ justify-content: space-between;
535
+ }
536
+ .footer-section {
537
+ flex: 1;
538
+ margin: 10px;
539
+ min-width: 200px;
540
+ }
541
+ .footer-section h3 {
542
+ margin-bottom: 10px;
543
+ font-size: 18px;
544
+ color: #f2f2f2;
545
+ }
546
+ .footer-section p,
547
+ .footer-section a {
548
+ color: #ccc;
549
+ text-decoration: none;
550
+ margin-bottom: 8px;
551
+ display: block;
552
+ }
553
+ .footer-section a:hover {
554
+ color: #fff;
555
+ }
556
+ .footer-section img {
557
+ width: 100%;
558
+ max-width: 300px;
559
+ border-radius: 8px;
560
+ margin-top: 10px;
561
+ }
562
+ .social-links a {
563
+ display: inline-block;
564
+ margin: 5px 10px 5px 0;
565
+ color: #ccc;
566
+ font-size: 20px;
567
+ }
568
+ .social-links a:hover {
569
+ color: #fff;
570
+ }
571
+ .footer-bottom {
572
+ text-align: center;
573
+ margin-top: 20px;
574
+ border-top: 1px solid #444;
575
+ padding-top: 10px;
576
+ font-size: 14px;
577
+ }
578
+
579
+ @media (max-width: 768px) {
580
+ .footer-container {
581
+ flex-direction: column;
582
+ text-align: center;
583
+ }
584
+ .footer-section img {
585
+ margin: 10px auto;
586
+ }
587
+ }
588
+
589
+ /*new css detect*/
590
+ .detect-section {
591
+ background-color: #f4f6f9;
592
+ padding: 3rem 0;
593
+ }
594
+
595
+ .input-card {
596
+ background-color: white;
597
+ border-radius: 12px;
598
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
599
+ padding: 2rem;
600
+ transition: transform 0.3s ease;
601
+ }
602
+
603
+ .input-card:hover {
604
+ transform: translateY(-5px);
605
+ }
606
+
607
+ .custom-checkbox .form-check-input:checked {
608
+ background-color: var(--primary-color);
609
+ border-color: var(--primary-color);
610
+ }
611
+
612
+ .btn-submit {
613
+ background-color: var(--primary-color);
614
+ color: white;
615
+ transition: all 0.3s ease;
616
+ }
617
+
618
+ .btn-submit:hover {
619
+ background-color: var(--secondary-color);
620
+ transform: translateY(-3px);
621
+ }
622
+
623
+
624
+ .loading-overlay {
625
+ position: fixed;
626
+ top: 0;
627
+ left: 0;
628
+ width: 100%;
629
+ height: 100%;
630
+ background-color: rgba(255, 255, 255, 0.8);
631
+ display: flex;
632
+ justify-content: center;
633
+ align-items: center;
634
+ z-index: 1050;
635
+ }
636
+
637
+ .spinner-container {
638
+ text-align: center;
639
+ }
640
+
641
+ .spinner {
642
+ width: 50px;
643
+ height: 50px;
644
+ animation: rotate 2s linear infinite;
645
+ }
646
+
647
+ .spinner .path {
648
+ stroke: #007bff; /* Customize the color */
649
+ stroke-linecap: round;
650
+ animation: dash 1.5s ease-in-out infinite;
651
+ }
652
+
653
+ @keyframes rotate {
654
+ 100% {
655
+ transform: rotate(360deg);
656
+ }
657
+ }
658
+
659
+ @keyframes dash {
660
+ 0% {
661
+ stroke-dasharray: 1, 150;
662
+ stroke-dashoffset: 0;
663
+ }
664
+ 50% {
665
+ stroke-dasharray: 90, 150;
666
+ stroke-dashoffset: -35;
667
+ }
668
+ 100% {
669
+ stroke-dasharray: 90, 150;
670
+ stroke-dashoffset: -124;
671
+ }
672
+ }
static/image/FB.jpg ADDED

Git LFS Details

  • SHA256: 2896985f6df910db10d91079c350871f2f44344512704904b7f5cd9f0f961062
  • Pointer size: 131 Bytes
  • Size of remote file: 107 kB
static/image/FB.svg ADDED
static/image/Logo.png ADDED

Git LFS Details

  • SHA256: fccb11930ad22a06afdf09d5ea911871abe6f0548178e234055922e129c728c6
  • Pointer size: 130 Bytes
  • Size of remote file: 42 kB
static/image/google.jpg ADDED

Git LFS Details

  • SHA256: 6f4556cd70e9b8affb2d4d76688facd67866e0110ef8e1021b61e0e2113ec495
  • Pointer size: 130 Bytes
  • Size of remote file: 37.7 kB
static/image/img1.png ADDED

Git LFS Details

  • SHA256: c3db968728a7918fcaff27efbd7b0691285b467f3b222b39eca4ab60c64718ac
  • Pointer size: 131 Bytes
  • Size of remote file: 219 kB
static/image/img2.png ADDED

Git LFS Details

  • SHA256: 0e00aa57f658070e8736e621302e7df987abf49c0da8ed8473b2f2b372a79b4d
  • Pointer size: 131 Bytes
  • Size of remote file: 220 kB
static/image/line-qr.jpg ADDED

Git LFS Details

  • SHA256: bc5e69c0f86463717c12b5e422163f2a994c9a0d4d60c736f269743d9ef11b7e
  • Pointer size: 130 Bytes
  • Size of remote file: 53 kB
static/image/picture.png ADDED

Git LFS Details

  • SHA256: 7bfdbed494b248a8b8449043328d456b515cfe09979486953128535dbf43d6e9
  • Pointer size: 130 Bytes
  • Size of remote file: 16.2 kB
static/image/pro.gif ADDED
static/image/production.png ADDED

Git LFS Details

  • SHA256: c918c73692eecdc0050e134ff16574a7cb5f6105e3a623aa818428b31db628f0
  • Pointer size: 130 Bytes
  • Size of remote file: 40.9 kB
static/image/simplicity.png ADDED

Git LFS Details

  • SHA256: 26fa0547c411ef6512c8e069feaf73eb59faf77091af1a94c78be96289e67fb4
  • Pointer size: 130 Bytes
  • Size of remote file: 31 kB
static/image/upload.png ADDED

Git LFS Details

  • SHA256: 20749eb477fc30c8416dce4beae10606d0cf624f5540f309ed81652ef84aad4b
  • Pointer size: 129 Bytes
  • Size of remote file: 6.36 kB
static/js/index.js ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ฟังก์ชันสำหรับแสดงชื่อไฟล์ที่อัปโหลด (โค้ดเดิม)
2
+ function displayFilename() {
3
+ const fileInput = document.getElementById('file');
4
+ const filenameLabel = document.getElementById('filename');
5
+
6
+ if (fileInput && fileInput.files.length > 0) {
7
+ let fileName = fileInput.files[0].name;
8
+ const maxLength = 20;
9
+
10
+ if (fileName.length > maxLength) {
11
+ const start = fileName.substring(0, 10);
12
+ const end = fileName.substring(fileName.length - 7);
13
+ fileName = `${start}...${end}`;
14
+ }
15
+ if(filenameLabel) {
16
+ filenameLabel.textContent = fileName;
17
+ }
18
+ }
19
+ }
20
+
21
+ // ฟังก์ชันสำหรับตรวจสอบชนิดของไฟล์ (โค้ดเดิม)
22
+ function validateFileType() {
23
+ const fileInput = document.getElementById('file');
24
+ const filePath = fileInput.value;
25
+ const allowedExtensions = /(\.jpg|\.jpeg|\.png|\.bmp)$/i;
26
+
27
+ if (fileInput && !allowedExtensions.exec(filePath)) {
28
+ alert('Please upload a valid image file (JPG, JPEG, PNG, BMP).');
29
+ fileInput.value = '';
30
+ return false;
31
+ }
32
+ return true;
33
+ }
34
+
35
+ // ใช้ Event Listener เพื่อให้แน่ใจว่า DOM (โครงสร้างหน้าเว็บ) โหลดเสร็จก่อน
36
+ document.addEventListener("DOMContentLoaded", function() {
37
+
38
+ // --- 1. ส่วนจัดการฟอร์มและ Loader ---
39
+ const sectionForm = document.getElementById('sectionForm');
40
+ const loadingOverlay = document.getElementById('loading');
41
+ const fileInput = document.getElementById('file');
42
+ const symptomTextArea = document.getElementById('symptomTextArea'); // เพิ่มเข้ามาเพื่อเข้าถึงง่าย
43
+
44
+ if (sectionForm) {
45
+ sectionForm.addEventListener('submit', function (event) {
46
+ // ตรวจสอบว่ามีไฟล์ถูกอัปโหลดหรือยัง
47
+ if (!fileInput.value) {
48
+ alert('กรุณาอัปโหลดรูปภาพ');
49
+ event.preventDefault(); // หยุดการส่งฟอร์ม
50
+ return;
51
+ }
52
+
53
+ // <<< DEBUG START: พิมพ์ข้อมูลที่จะส่งไปยัง Backend >>>
54
+ console.clear(); // ล้าง console เก่าเพื่อความสะอาดตา
55
+ console.log("=============================================");
56
+ console.log("🔬 DATA TO BE SENT TO BACKEND (main.py)");
57
+ console.log("=============================================");
58
+
59
+ // 1. เก็บค่าจาก Checkbox ที่ถูกติ๊ก
60
+ const checkedBoxes = document.querySelectorAll('input[name="checkboxes"]:checked');
61
+ const checkboxValues = Array.from(checkedBoxes).map(cb => cb.value);
62
+ console.log("✅ Checkbox values (name='checkboxes'):", checkboxValues);
63
+
64
+ // 2. เก็บค่าจาก Text Area
65
+ const textAreaValue = symptomTextArea.value;
66
+ console.log("📝 Text Area value (name='symptom_text'):", textAreaValue);
67
+
68
+ // Bonus: พิมพ์ค่า Model Version ที่เลือกด้วย
69
+ const modelVersion = document.querySelector('input[name="model_version"]:checked').value;
70
+ console.log("🤖 Model Version (name='model_version'):", modelVersion);
71
+
72
+ console.log("---------------------------------------------");
73
+ console.log("Backend (main.py) จะได้รับข้อมูล 3 ส่วนนี้จากฟอร์ม");
74
+ console.log("คุณสามารถนำข้อมูลเหล่านี้ไปสร้างเป็น 'final_prompt' สำหรับ preprocessingwangchan.py ต่อไป");
75
+ console.log("=============================================");
76
+ // <<< DEBUG END >>>
77
+
78
+
79
+ // ถ้าผ่าน ให้แสดง Loader
80
+ if (loadingOverlay) {
81
+ loadingOverlay.classList.remove('d-none');
82
+ }
83
+ });
84
+ }
85
+
86
+ // --- 2. ส่วนจัดการ Logic ของ Checkbox (ควบคุมการ Disable เท่านั้น) ---
87
+ const noSymptomCheckbox = document.getElementById('check6');
88
+ const spicyFoodCheckbox = document.getElementById('check4');
89
+ const alwaysHurtsCheckbox = document.getElementById('check7');
90
+
91
+ function manageCheckboxStates() {
92
+ if (!noSymptomCheckbox || !spicyFoodCheckbox || !alwaysHurtsCheckbox) return;
93
+
94
+ const isAnySymptomChecked = spicyFoodCheckbox.checked || alwaysHurtsCheckbox.checked;
95
+
96
+ noSymptomCheckbox.disabled = isAnySymptomChecked;
97
+ spicyFoodCheckbox.disabled = noSymptomCheckbox.checked;
98
+ alwaysHurtsCheckbox.disabled = noSymptomCheckbox.checked;
99
+ }
100
+
101
+ if (noSymptomCheckbox && spicyFoodCheckbox && alwaysHurtsCheckbox) {
102
+ [noSymptomCheckbox, spicyFoodCheckbox, alwaysHurtsCheckbox].forEach(checkbox => {
103
+ checkbox.addEventListener('change', manageCheckboxStates);
104
+ });
105
+ }
106
+
107
+ // --- 3. ส่วนจัดการการแสดง Modal ผลลัพธ์ ---
108
+ const resultImageDataElement = document.getElementById('result-image-data');
109
+ if (resultImageDataElement) {
110
+ const imageUrl = resultImageDataElement.getAttribute('data-url');
111
+
112
+ if (imageUrl) {
113
+ const outputModalElement = document.getElementById('outputModal');
114
+ if (outputModalElement) {
115
+ const outputModal = new bootstrap.Modal(outputModalElement);
116
+ outputModal.show();
117
+ if (loadingOverlay) {
118
+ loadingOverlay.classList.add('d-none');
119
+ }
120
+ }
121
+ }
122
+ }
123
+
124
+ // --- 4. ส่วนจัดการไฟล์อินพุต ---
125
+ if (fileInput) {
126
+ fileInput.addEventListener('change', function() {
127
+ if (!this.value) return;
128
+ const allowedExtensions = /(\.jpg|\.jpeg|\.png|\.bmp)$/i;
129
+ if (!allowedExtensions.exec(this.value)) {
130
+ alert('กรุณาอัปโหลดไฟล์รูปภาพที่ถูกต้อง (JPG, JPEG, PNG, BMP)');
131
+ this.value = '';
132
+ }
133
+ });
134
+ }
135
+
136
+ // เรียกใช้ฟังก์ชันครั้งแรกเมื่อหน้าเว็บโหลดเสร็จ
137
+ manageCheckboxStates();
138
+
139
+ const resultTrigger = document.getElementById('result-data-trigger');
140
+ if (resultTrigger) {
141
+ const outputModalElement = document.getElementById('outputModal');
142
+ if (outputModalElement) {
143
+ const outputModal = new bootstrap.Modal(outputModalElement);
144
+ outputModal.show();
145
+ if (loadingOverlay) {
146
+ loadingOverlay.classList.add('d-none');
147
+ }
148
+ }
149
+ }
150
+ });
151
+
152
+ const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
153
+ const tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
154
+ return new bootstrap.Tooltip(tooltipTriggerEl);
155
+ });
templates/detect.html ADDED
@@ -0,0 +1,228 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Lesion Detection</title>
7
+ <link rel="stylesheet" href="{{ url_for('static', path='css/detect.css') }}">
8
+ <link rel="preconnect" href="https://fonts.googleapis.com">
9
+ <link rel="preconnect" href="https://fonts.gstatic.com">
10
+ <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans+Thai:wght@100;200;300;400;500;600;700&display=swap"
11
+ rel="stylesheet">
12
+ <link rel="icon" type="image/x-icon" href="{{ url_for('static', path='image/production.png') }}">
13
+ <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet"
14
+ integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
15
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.min.css">
16
+ <style>
17
+ :root {
18
+ --primary-color: #1da2e0;
19
+ --secondary-color: #c8e5fa;
20
+ }
21
+ body {
22
+ box-sizing: border-box;
23
+ margin: 0;
24
+ padding: 0;
25
+ display: flex;
26
+ flex-direction: column;
27
+ font-family: 'IBM Plex Sans Thai', sans-serif;
28
+ min-height: 100vh;
29
+ }
30
+ .loading-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.7); z-index: 1060; display: flex; justify-content: center; align-items: center; }
31
+ .spinner-container { color: white; }
32
+ .spinner { animation: rotate 2s linear infinite; width: 50px; height: 50px; }
33
+ .path { stroke: #93d3f4; stroke-linecap: round; animation: dash 1.5s ease-in-out infinite; }
34
+ @keyframes rotate { 100% { transform: rotate(360deg); } }
35
+ @keyframes dash {
36
+ 0% { stroke-dasharray: 1, 150; stroke-dashoffset: 0; }
37
+ 50% { stroke-dasharray: 90, 150; stroke-dashoffset: -35; }
38
+ 100% { stroke-dasharray: 90, 150; stroke-dashoffset: -124; }
39
+ }
40
+ #symptomTextArea::placeholder {
41
+ color: #adb5bd;
42
+ opacity: 1;
43
+ }
44
+ .background-color {
45
+ background-color: #f8f9fa;
46
+ }
47
+ .footer {
48
+ margin-top: auto;
49
+ }
50
+ </style>
51
+ </head>
52
+
53
+ <body>
54
+ <!-- Navigation -->
55
+ <nav class="navbar navbar-expand-lg navbar-light bg-white shadow-sm">
56
+ <div class="container-fluid">
57
+ <a class="navbar-brand" href="/">
58
+ <img src="{{ url_for('static', path='image/Logo.png') }}" alt="Logo" class="img-fluid logo"
59
+ style="max-width: 100px; height: auto;" />
60
+ </a>
61
+ <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
62
+ <span class="navbar-toggler-icon"></span>
63
+ </button>
64
+ <div class="collapse navbar-collapse" id="navbarNav">
65
+ <ul class="navbar-nav ms-auto">
66
+ <li class="nav-item"><a class="nav-link active" href="/detect">ตรวจจับรอยโรค</a></li>
67
+ </ul>
68
+ </div>
69
+ </div>
70
+ </nav>
71
+
72
+ <!-- Detection Section -->
73
+ <div class="detect-section flex-grow-1 background-color">
74
+ <div class="container py-4">
75
+ <div class="row justify-content-center">
76
+ <div class="col-md-8">
77
+ <div class="input-card shadow-lg p-4 bg-white rounded-3">
78
+ <h2 class="text-center mb-4 fw-bold">ตรวจจับรอยโรคในช่องปาก</h2>
79
+ <form id="sectionForm" method="post" enctype="multipart/form-data" action="/uploaded">
80
+ <div class="mb-4">
81
+ <label for="file" class="form-label fs-5">อัปโหลดรูปภาพ</label>
82
+ <input class="form-control" type="file" id="file" name="file" required accept="image/*">
83
+ </div>
84
+ <div>
85
+ <p class="fs-5">ประวัติ/อาการผู้ป่วย</p>
86
+ <p class="text-muted small">โปรดเลือกจากรายการ หรือพิมพ์เพิ่มเติม (หากไม่เลือกหรือพิมพ์ ระบบจะบันทึกว่า “ไม่มีอาการผิดปกติ” โดยอัตโนมัติ)</p>
87
+ </div>
88
+ <div class="row">
89
+ <div class="col-md-6">
90
+ <div class="form-check mb-1"><input class="form-check-input symptom-checkbox" type="checkbox" id="check6" name="checkboxes" value="noSymptoms"><label class="form-check-label" for="check6">ไม่มีอาการผิดปกติ</label></div>
91
+ <div class="form-check mb-1"><input class="form-check-input symptom-checkbox" type="checkbox" id="check1" name="checkboxes" value="drinkAlcohol"><label class="form-check-label" for="check1">ดื่มเครื่องดื่มแอลกอฮอล์</label></div>
92
+ <div class="form-check mb-1"><input class="form-check-input symptom-checkbox" type="checkbox" id="check2" name="checkboxes" value="smoking"><label class="form-check-label" for="check2">สูบบุหรี่</label></div>
93
+ <div class="form-check mb-1"><input class="form-check-input symptom-checkbox" type="checkbox" id="check3" name="checkboxes" value="chewBetelNut"><label class="form-check-label" for="check3">เคี้ยวหมาก</label></div>
94
+ </div>
95
+ <div class="col-md-6">
96
+ <div class="form-check mb-1"><input class="form-check-input symptom-checkbox" type="checkbox" id="check4" name="checkboxes" value="eatSpicyFood"><label class="form-check-label" for="check4">รับประทานอาหารเผ็ดแล้วรู้สึกระคายเคือง</label></div>
97
+ <div class="form-check mb-1"><input class="form-check-input symptom-checkbox" type="checkbox" id="check5" name="checkboxes" value="wipeOff"><label class="form-check-label" for="check5">คราบขาวที่สามารถลอกออก</label></div>
98
+ <div class="form-check mb-1"><input class="form-check-input symptom-checkbox" type="checkbox" id="check7" name="checkboxes" value="alwaysHurts"><label class="form-check-label" for="check7">มีอาการเจ็บหรือระคายเคืองตลอดเวลา</label></div>
99
+ </div>
100
+ </div>
101
+
102
+ <div class="mt-4">
103
+ <label for="symptomTextArea" class="form-label">รายละเอียดอาการเพิ่มเติม:</label>
104
+ <textarea class="form-control" id="symptomTextArea" name="symptom_text" rows="4" placeholder="เช่น มีอาการปวดเป็นบางครั้ง, เป็นมานาน 2 สัปดาห์ ..."></textarea>
105
+ </div>
106
+
107
+ <div class="mt-4 d-flex justify-content-center">
108
+ <button class="button" type="submit">Submit</button>
109
+ </div>
110
+ </form>
111
+ </div>
112
+ </div>
113
+ </div>
114
+ </div>
115
+ </div>
116
+
117
+ <!-- Loader Section -->
118
+ <div id="loading" class="loading-overlay d-none">
119
+ <div class="spinner-container text-center">
120
+ <svg class="spinner" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50">
121
+ <circle class="path" cx="25" cy="25" r="20" fill="none" stroke-width="4"></circle>
122
+ </svg>
123
+ <p class="mt-2">กำลังประมวลผล, กรุณารอสักครู่...</p>
124
+ </div>
125
+ </div>
126
+
127
+ <!-- ===== Result Modal ===== -->
128
+ {% if image_b64_data %}
129
+ <div class="modal fade" id="outputModal" tabindex="-1">
130
+ <div class="modal-dialog modal-xl modal-dialog-centered">
131
+ <div class="modal-content">
132
+ <div class="modal-header">
133
+ <h5 class="modal-title">ผลการตรวจจับรอยโรค</h5>
134
+ <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
135
+ </div>
136
+ <div class="modal-body">
137
+ <div class="row g-3">
138
+ <div class="col-lg-8">
139
+ <div class="row">
140
+ <div class="col-md-6 text-center mb-2">
141
+ <h6>ภาพต้นฉบับ</h6>
142
+ <img src="data:image/jpeg;base64, {{ image_b64_data }}" alt="Original Image" class="img-fluid rounded shadow-sm">
143
+ </div>
144
+ <div class="col-md-6 text-center">
145
+ <h6 class="d-inline-block">Grad-CAM Heatmap</h6>
146
+ <i class="bi bi-info-circle-fill ms-1 info-icon"
147
+ data-bs-toggle="tooltip"
148
+ data-bs-placement="top"
149
+ title="Grad-CAM Heatmap แสดงถึงความสนใจของโมเดล โดยพื้นที่สีแดงคือบริเวณที่โมเดลให้ความสำคัญสูงสุด���นการตัดสินใจ"></i>
150
+ <img src="data:image/jpeg;base64, {{ gradcam_b64_data }}" alt="Grad-CAM Heatmap" class="img-fluid rounded shadow-sm">
151
+ </div>
152
+ </div>
153
+ </div>
154
+ <div class="col-lg-4 d-flex align-items-center">
155
+ <div class="w-100">
156
+ <h4 class="mb-3">ผลการวิเคราะห์</h4>
157
+ <p class="fs-6">ผลการวิเคราะห์รอยโรคจากภาพร่วมกับประวัติผู้ป่วย:</p>
158
+ <div class="alert alert-primary" role="alert">
159
+ <h5 class="alert-heading">ประเภทรอยโรคที่คาดการณ์:</h5>
160
+ <p class="fs-4 fw-bold mb-0">{{ name_out }}</p>
161
+ </div>
162
+ <div class="alert alert-danger" role="alert">
163
+ <h5 class="alert-heading">โอกาสเป็นรอยโรค:</h5>
164
+ <p class="fs-4 fw-bold mb-0">{{ eva_output }} %</p>
165
+ </div>
166
+ </div>
167
+ </div>
168
+ </div>
169
+ </div>
170
+ <div class="modal-footer">
171
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">ปิด</button>
172
+ </div>
173
+ </div>
174
+ </div>
175
+ </div>
176
+ {% endif %}
177
+
178
+ <footer class="footer bg-dark text-white p-4 text-center">
179
+ &copy; 2025 MYCompany. All Rights Reserved.
180
+ </footer>
181
+
182
+ <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
183
+ <script>
184
+ document.addEventListener('DOMContentLoaded', function () {
185
+ // ส่วนจัดการ Loader
186
+ const form = document.getElementById('sectionForm');
187
+ const loadingOverlay = document.getElementById('loading');
188
+ if (form) {
189
+ form.addEventListener('submit', function () {
190
+ // ตรวจสอบว่าฟอร์ม valid ก่อนแสดง loader
191
+ if (form.checkValidity()) {
192
+ loadingOverlay.classList.remove('d-none');
193
+ }
194
+ });
195
+ }
196
+
197
+ // ส่วนจัดการ Modal แสดงผล
198
+ const outputModalElement = document.getElementById('outputModal');
199
+ if (outputModalElement) {
200
+ const outputModal = new bootstrap.Modal(outputModalElement);
201
+ outputModal.show();
202
+ }
203
+
204
+ // ส่วนจัดการ Checkbox "ไม่มีอาการผิดปกติ"
205
+ const noSymptomsCheckbox = document.getElementById('check6');
206
+ const otherCheckboxes = document.querySelectorAll('.symptom-checkbox:not(#check6)');
207
+
208
+ if (noSymptomsCheckbox) {
209
+ noSymptomsCheckbox.addEventListener('change', function() {
210
+ if (this.checked) {
211
+ otherCheckboxes.forEach(checkbox => {
212
+ // ยกเว้นกลุ่มพฤติกรรมเสี่ยง
213
+ if (!['check1', 'check2', 'check3'].includes(checkbox.id)) {
214
+ checkbox.checked = false;
215
+ checkbox.disabled = true;
216
+ }
217
+ });
218
+ } else {
219
+ otherCheckboxes.forEach(checkbox => {
220
+ checkbox.disabled = false;
221
+ });
222
+ }
223
+ });
224
+ }
225
+ });
226
+ </script>
227
+ </body>
228
+ </html>
templates/detect2.html ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Lesion Detection</title>
7
+ <link rel="stylesheet" href="{{ url_for('static', path='css/detect.css') }}">
8
+ <link rel="preconnect" href="https://fonts.googleapis.com">
9
+ <link rel="preconnect" href="https://fonts.gstatic.com">
10
+ <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans+Thai:wght@100;200;300;400;500;600;700&display=swap"
11
+ rel="stylesheet">
12
+ <link rel="icon" type="image/x-icon" href="{{ url_for('static', path='image/production.png') }}">
13
+ <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet"
14
+ integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
15
+ <style>
16
+ :root {
17
+ --primary-color: #1da2e0;
18
+ --secondary-color: #c8e5fa;
19
+ }
20
+ body {
21
+ box-sizing: border-box;
22
+ margin: 0;
23
+ padding: 0;
24
+ display: flex;
25
+ flex-direction: column;
26
+ font-family: 'IBM Plex Sans Thai', sans-serif;
27
+ }
28
+ .loading-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); z-index: 1060; display: flex; justify-content: center; align-items: center; }
29
+ .spinner-container { color: white; }
30
+ .spinner { animation: rotate 2s linear infinite; width: 50px; height: 50px; }
31
+ .path { stroke: #93d3f4; stroke-linecap: round; animation: dash 1.5s ease-in-out infinite; }
32
+ @keyframes rotate { 100% { transform: rotate(360deg); } }
33
+ @keyframes dash {
34
+ 0% { stroke-dasharray: 1, 150; stroke-dashoffset: 0; }
35
+ 50% { stroke-dasharray: 90, 150; stroke-dashoffset: -35; }
36
+ 100% { stroke-dasharray: 90, 150; stroke-dashoffset: -124; }
37
+ }
38
+
39
+ #symptomTextArea::placeholder {
40
+ color: #adb5bd; /* สีเทาอ่อน (เป็นสีมาตรฐานของ Bootstrap Muted Text) */
41
+ opacity: 1; /* สำหรับเบราว์เซอร์บางตัวที่อาจจะลด opacity เอง */
42
+ }
43
+ </style>
44
+ </head>
45
+
46
+ <body>
47
+ <!-- Navigation -->
48
+ <nav class="navbar navbar-expand-lg navbar-light bg-white">
49
+ <div class="container-fluid">
50
+ <a class="navbar-brand" href="/">
51
+ <img src="{{ url_for('static', path='image/Logo.png') }}" alt="Logo" class="img-fluid logo"
52
+ style="max-width: 100px; height: auto;" />
53
+ </a>
54
+ <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
55
+ <span class="navbar-toggler-icon"></span>
56
+ </button>
57
+ <div class="collapse navbar-collapse" id="navbarNav">
58
+ <ul class="navbar-nav ms-auto">
59
+ <li class="nav-item"><a class="nav-link" href="/detect">ตรวจจับรอยโรค</a></li>
60
+ <li class="nav-item"><a class="nav-link" href="#">ติดต่อสนับสนุน</a></li>
61
+ </ul>
62
+ </div>
63
+ </div>
64
+ </nav>
65
+
66
+ <!-- Detection Section -->
67
+ <div class="detect-section flex-grow-1 background-color">
68
+ <div class="container py-4">
69
+ <div class="row justify-content-center">
70
+ <div class="col-md-8">
71
+ <div class="input-card shadow-lg p-4">
72
+ <h2 class="text-center mb-4">ตรวจจับรอยโรคในช่องปาก</h2>
73
+ <form id="sectionForm" method="post" enctype="multipart/form-data" action="/uploaded">
74
+ <div class="mb-4">
75
+ <label for="file" class="form-label">อัปโหลดรูปภาพ</label>
76
+ <input class="form-control" type="file" id="file" name="file" required accept="image/*">
77
+ </div>
78
+ <div class="d-flex flex-row">
79
+ <h3>ประวัติ/อาการผู้ป่วย</h3>
80
+ <p class="ms-2 mt-1">(เลือกจากรายการ หรือพิมพ์เพิ่มเติม)</p>
81
+ </div>
82
+ <div class="row">
83
+ <div class="col-md-6">
84
+ <div class="form-check"><input class="form-check-input symptom-checkbox" type="checkbox" id="check6" name="checkboxes" value="noSymptoms"><label class="form-check-label" for="check6">ไม่มีอาการผิดปกติ</label></div>
85
+ <div class="form-check"><input class="form-check-input symptom-checkbox" type="checkbox" id="check1" name="checkboxes" value="drinkAlcohol"><label class="form-check-label" for="check1">ดื่มเครื่องดื่มแอลกอฮอล์</label></div>
86
+ <div class="form-check"><input class="form-check-input symptom-checkbox" type="checkbox" id="check2" name="checkboxes" value="smoking"><label class="form-check-label" for="check2">สูบบุหรี่</label></div>
87
+ <div class="form-check"><input class="form-check-input symptom-checkbox" type="checkbox" id="check3" name="checkboxes" value="chewBetelNut"><label class="form-check-label" for="check3">เคี้ยวหมาก</label></div>
88
+ </div>
89
+ <div class="col-md-6">
90
+ <div class="form-check"><input class="form-check-input symptom-checkbox" type="checkbox" id="check4" name="checkboxes" value="eatSpicyFood"><label class="form-check-label" for="check4">รับประทานอาหารเผ็ดแล้วรู้สึกระคายเคือง</label></div>
91
+ <div class="form-check"><input class="form-check-input symptom-checkbox" type="checkbox" id="check5" name="checkboxes" value="wipeOff"><label class="form-check-label" for="check5">คราบขาวที่สามารถลอกออก</label></div>
92
+ <div class="form-check"><input class="form-check-input symptom-checkbox" type="checkbox" id="check7" name="checkboxes" value="alwaysHurts"><label class="form-check-label" for="check7">มีอาการเจ็บหรือระคายเคืองตลอดเวลา</label></div>
93
+ </div>
94
+ </div>
95
+
96
+ <div class="mt-4">
97
+ <label for="symptomTextArea" class="form-label">รายละเอียดอาการเพิ่มเติม:</label>
98
+ <textarea class="form-control" id="symptomTextArea" name="symptom_text" rows="4" placeholder="ข้อความจากรายการที่เลือกจะปรากฏที่นี่ หรือคุณสามารถพิมพ์อาการเพิ่มเติมได้เอง"></textarea>
99
+ </div>
100
+
101
+ <div class="d-grid gap-2 mt-4">
102
+ <button class="btn btn-primary" type="submit">Submit</button>
103
+ </div>
104
+ </form>
105
+ </div>
106
+ </div>
107
+ </div>
108
+ </div>
109
+ </div>
110
+
111
+ <!-- Loader Section -->
112
+ <div id="loading" class="loading-overlay d-none">
113
+ <div class="spinner-container text-center">
114
+ <svg class="spinner" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50">
115
+ <circle class="path" cx="25" cy="25" r="20" fill="none" stroke-width="4"></circle>
116
+ </svg>
117
+ <p class="mt-2">Processing, please wait...</p>
118
+ </div>
119
+ </div>
120
+
121
+ <!-- Result Modal -->
122
+ {% if image_output %}
123
+ <div class="modal fade" id="outputModal" tabindex="-1">
124
+ <div class="modal-dialog modal-lg modal-dialog-centered">
125
+ <div class="modal-content">
126
+ <div class="modal-header">
127
+ <h5 class="modal-title">ผลการตรวจจับรอยโรค</h5>
128
+ <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
129
+ </div>
130
+ <div class="modal-body">
131
+ <div class="row g-2">
132
+ <div class="col-md-6">
133
+ <img src="{{ image_output }}" alt="Processed Image" class="img-fluid rounded">
134
+ </div>
135
+ <div class="col-md-6">
136
+ <img src="{{ image_output }}" alt="Processed Image" class="img-fluid rounded">
137
+ </div>
138
+ <div class="d-flex flex-column justify-content-center">
139
+ <p class="fs-5">ผลการวิเคราะห์รอยโรคจากภาพสามารถสรุปผลการวิเคราะห์รอยโรคร่วมกับประวัติผู้ป่วย</p>
140
+ <h4>โอกาสเป็นรอยโรค: <span class="fs-5 text-danger">{{ eva_output }}%</span></h4>
141
+ <h4>ประเภทรอยโรค: <span class="fs-5 text-primary">{{ name_out }}</span></h4>
142
+ </div>
143
+ </div>
144
+ </div>
145
+ <div class="modal-footer">
146
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">ปิด</button>
147
+ </div>
148
+ </div>
149
+ </div>
150
+ <!-- เพิ่ม div นี้เพื่อส่งข้อมูลให้ JavaScript โดยไม่แสดงผล -->
151
+ <div id="result-image-data" data-url="{{ image_output or '' }}" style="display: none;"></div>
152
+ {% endif %}
153
+
154
+ <footer class="footer bg-dark text-white p-4 text-center">
155
+ &copy; 2025 MYCompany. All Rights Reserved.
156
+ </footer>
157
+
158
+ <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
159
+ <script src="{{ url_for('static', path='js/index.js') }}"></script>
160
+ </body>
161
+ </html>
162
+
templates/detect3.html ADDED
@@ -0,0 +1,229 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!-- ลบออกอัตโนมัติ -->
2
+ <!DOCTYPE html>
3
+ <html lang="en">
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Lesion Detection</title>
8
+ <link rel="stylesheet" href="{{ url_for('static', path='css/detect.css') }}">
9
+ <link rel="preconnect" href="https://fonts.googleapis.com">
10
+ <link rel="preconnect" href="https://fonts.gstatic.com">
11
+ <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans+Thai:wght@100;200;300;400;500;600;700&display=swap"
12
+ rel="stylesheet">
13
+ <link rel="icon" type="image/x-icon" href="{{ url_for('static', path='image/production.png') }}">
14
+ <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet"
15
+ integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
16
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.min.css">
17
+ <style>
18
+ :root {
19
+ --primary-color: #1da2e0;
20
+ --secondary-color: #c8e5fa;
21
+ }
22
+ body {
23
+ box-sizing: border-box;
24
+ margin: 0;
25
+ padding: 0;
26
+ display: flex;
27
+ flex-direction: column;
28
+ font-family: 'IBM Plex Sans Thai', sans-serif;
29
+ min-height: 100vh;
30
+ }
31
+ .loading-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.7); z-index: 1060; display: flex; justify-content: center; align-items: center; }
32
+ .spinner-container { color: white; }
33
+ .spinner { animation: rotate 2s linear infinite; width: 50px; height: 50px; }
34
+ .path { stroke: #93d3f4; stroke-linecap: round; animation: dash 1.5s ease-in-out infinite; }
35
+ @keyframes rotate { 100% { transform: rotate(360deg); } }
36
+ @keyframes dash {
37
+ 0% { stroke-dasharray: 1, 150; stroke-dashoffset: 0; }
38
+ 50% { stroke-dasharray: 90, 150; stroke-dashoffset: -35; }
39
+ 100% { stroke-dasharray: 90, 150; stroke-dashoffset: -124; }
40
+ }
41
+ #symptomTextArea::placeholder {
42
+ color: #adb5bd;
43
+ opacity: 1;
44
+ }
45
+ .background-color {
46
+ background-color: #f8f9fa; /* สีพื้นหลังอ่อนๆ */
47
+ }
48
+ .footer {
49
+ margin-top: auto; /* ดัน footer ไปอยู่ด้านล่างสุด */
50
+ }
51
+ </style>
52
+ </head>
53
+
54
+ <body>
55
+ <!-- Navigation -->
56
+ <nav class="navbar navbar-expand-lg navbar-light bg-white shadow-sm">
57
+ <div class="container-fluid">
58
+ <a class="navbar-brand" href="/">
59
+ <img src="{{ url_for('static', path='image/Logo.png') }}" alt="Logo" class="img-fluid logo"
60
+ style="max-width: 100px; height: auto;" />
61
+ </a>
62
+ <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
63
+ <span class="navbar-toggler-icon"></span>
64
+ </button>
65
+ <div class="collapse navbar-collapse" id="navbarNav">
66
+ <ul class="navbar-nav ms-auto">
67
+ <li class="nav-item"><a class="nav-link active" href="/detect">ตรวจจับรอยโรค</a></li>
68
+ </ul>
69
+ </div>
70
+ </div>
71
+ </nav>
72
+
73
+ <!-- Detection Section -->
74
+ <div class="detect-section flex-grow-1 background-color">
75
+ <div class="container py-4">
76
+ <div class="row justify-content-center">
77
+ <div class="col-md-8">
78
+ <div class="input-card shadow-lg p-4 bg-white rounded-3">
79
+ <h2 class="text-center mb-4 fw-bold">ตรวจจับรอยโรคในช่องปาก</h2>
80
+ <form id="sectionForm" method="post" enctype="multipart/form-data" action="/uploaded">
81
+ <div class="mb-4">
82
+ <label for="file" class="form-label fs-5">อัปโหลดรูปภาพ</label>
83
+ <input class="form-control" type="file" id="file" name="file" required accept="image/*">
84
+ </div>
85
+ <div>
86
+ <p class="fs-5">ประวัติ/อาการผู้ป่วย</p>
87
+ <p class="text-muted small">โปรดเลือกจากรายการ หรือพิมพ์เพิ่มเติม (หากไม่เลือกหรือพิมพ์ ระบบจะบันทึกว่า “ไม่มีอาการผิดปกติ” โดยอัตโนมัติ)</p>
88
+ </div>
89
+ <div class="row">
90
+ <div class="col-md-6">
91
+ <div class="form-check mb-1"><input class="form-check-input symptom-checkbox" type="checkbox" id="check6" name="checkboxes" value="noSymptoms"><label class="form-check-label" for="check6">ไม่มีอาการผิดปกติ</label></div>
92
+ <div class="form-check mb-1"><input class="form-check-input symptom-checkbox" type="checkbox" id="check1" name="checkboxes" value="drinkAlcohol"><label class="form-check-label" for="check1">ดื่มเครื่องดื่มแอลกอฮอล์</label></div>
93
+ <div class="form-check mb-1"><input class="form-check-input symptom-checkbox" type="checkbox" id="check2" name="checkboxes" value="smoking"><label class="form-check-label" for="check2">สูบบุหรี่</label></div>
94
+ <div class="form-check mb-1"><input class="form-check-input symptom-checkbox" type="checkbox" id="check3" name="checkboxes" value="chewBetelNut"><label class="form-check-label" for="check3">เคี้ยวหมาก</label></div>
95
+ </div>
96
+ <div class="col-md-6">
97
+ <div class="form-check mb-1"><input class="form-check-input symptom-checkbox" type="checkbox" id="check4" name="checkboxes" value="eatSpicyFood"><label class="form-check-label" for="check4">รับประทานอาหารเผ็ดแล้วรู้สึกระคายเคือง</label></div>
98
+ <div class="form-check mb-1"><input class="form-check-input symptom-checkbox" type="checkbox" id="check5" name="checkboxes" value="wipeOff"><label class="form-check-label" for="check5">คราบขาวที่สามารถลอกออก</label></div>
99
+ <div class="form-check mb-1"><input class="form-check-input symptom-checkbox" type="checkbox" id="check7" name="checkboxes" value="alwaysHurts"><label class="form-check-label" for="check7">มีอาการเจ็บหรือระคายเคืองตลอดเวลา</label></div>
100
+ </div>
101
+ </div>
102
+
103
+ <div class="mt-4">
104
+ <label for="symptomTextArea" class="form-label">รายละเอียดอาการเพิ่มเติม:</label>
105
+ <textarea class="form-control" id="symptomTextArea" name="symptom_text" rows="4" placeholder="เช่น มีอาการปวดเป็นบางครั้ง, เป็นมานาน 2 สัปดาห์ ..."></textarea>
106
+ </div>
107
+
108
+ <div class="mt-4 d-flex justify-content-center">
109
+ <button class="button" type="submit">Submit</button>
110
+ </div>
111
+ </form>
112
+ </div>
113
+ </div>
114
+ </div>
115
+ </div>
116
+ </div>
117
+
118
+ <!-- Loader Section -->
119
+ <div id="loading" class="loading-overlay d-none">
120
+ <div class="spinner-container text-center">
121
+ <svg class="spinner" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50">
122
+ <circle class="path" cx="25" cy="25" r="20" fill="none" stroke-width="4"></circle>
123
+ </svg>
124
+ <p class="mt-2">กำลังประมวลผล, กรุณารอสักครู่...</p>
125
+ </div>
126
+ </div>
127
+
128
+ <!-- ===== Result Modal ===== -->
129
+ {% if image_b64_data %}
130
+ <div class="modal fade" id="outputModal" tabindex="-1">
131
+ <div class="modal-dialog modal-xl modal-dialog-centered">
132
+ <div class="modal-content">
133
+ <div class="modal-header">
134
+ <h5 class="modal-title">ผลการตรวจจับรอยโรค</h5>
135
+ <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
136
+ </div>
137
+ <div class="modal-body">
138
+ <div class="row g-3">
139
+ <div class="col-lg-8">
140
+ <div class="row">
141
+ <div class="col-md-6 text-center mb-2">
142
+ <h6>ภาพต้นฉบับ</h6>
143
+ <img src="data:image/jpeg;base64, {{ image_b64_data }}" alt="Original Image" class="img-fluid rounded shadow-sm">
144
+ </div>
145
+ <div class="col-md-6 text-center">
146
+ <h6 class="d-inline-block">Grad-CAM Heatmap</h6>
147
+ <i class="bi bi-info-circle-fill ms-1 info-icon"
148
+ data-bs-toggle="tooltip"
149
+ data-bs-placement="top"
150
+ title="Grad-CAM Heatmap แสดงถึงความสน��จของโมเดล โดยพื้นที่สีแดงคือบริเวณที่โมเดลให้ความสำคัญสูงสุดในการตัดสินใจ"></i>
151
+ <img src="data:image/jpeg;base64, {{ gradcam_b64_data }}" alt="Grad-CAM Heatmap" class="img-fluid rounded shadow-sm">
152
+ </div>
153
+ </div>
154
+ </div>
155
+ <div class="col-lg-4 d-flex align-items-center">
156
+ <div class="w-100">
157
+ <h4 class="mb-3">ผลการวิเคราะห์</h4>
158
+ <p class="fs-6">ผลการวิเคราะห์รอยโรคจากภาพร่วมกับประวัติผู้ป่วย:</p>
159
+ <div class="alert alert-primary" role="alert">
160
+ <h5 class="alert-heading">ประเภทรอยโรคที่คาดการณ์:</h5>
161
+ <p class="fs-4 fw-bold mb-0">{{ name_out }}</p>
162
+ </div>
163
+ <div class="alert alert-danger" role="alert">
164
+ <h5 class="alert-heading">โอกาสเป็นรอยโรค:</h5>
165
+ <p class="fs-4 fw-bold mb-0">{{ eva_output }} %</p>
166
+ </div>
167
+ </div>
168
+ </div>
169
+ </div>
170
+ </div>
171
+ <div class="modal-footer">
172
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">ปิด</button>
173
+ </div>
174
+ </div>
175
+ </div>
176
+ </div>
177
+ {% endif %}
178
+
179
+ <footer class="footer bg-dark text-white p-4 text-center">
180
+ &copy; 2025 MYCompany. All Rights Reserved.
181
+ </footer>
182
+
183
+ <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
184
+ <script>
185
+ document.addEventListener('DOMContentLoaded', function () {
186
+ // ส่วนจัดการ Loader
187
+ const form = document.getElementById('sectionForm');
188
+ const loadingOverlay = document.getElementById('loading');
189
+ if (form) {
190
+ form.addEventListener('submit', function () {
191
+ // ตรวจสอบว่าฟอร์ม valid ก่อนแสดง loader
192
+ if (form.checkValidity()) {
193
+ loadingOverlay.classList.remove('d-none');
194
+ }
195
+ });
196
+ }
197
+
198
+ // ส่วนจัดการ Modal แสดงผล
199
+ const outputModalElement = document.getElementById('outputModal');
200
+ if (outputModalElement) {
201
+ const outputModal = new bootstrap.Modal(outputModalElement);
202
+ outputModal.show();
203
+ }
204
+
205
+ // ส่วนจัดการ Checkbox "ไม่มีอาการผิดปกติ"
206
+ const noSymptomsCheckbox = document.getElementById('check6');
207
+ const otherCheckboxes = document.querySelectorAll('.symptom-checkbox:not(#check6)');
208
+
209
+ if (noSymptomsCheckbox) {
210
+ noSymptomsCheckbox.addEventListener('change', function() {
211
+ if (this.checked) {
212
+ otherCheckboxes.forEach(checkbox => {
213
+ // ยกเว้นกลุ่มพฤติกรรมเสี่ยง
214
+ if (!['check1', 'check2', 'check3'].includes(checkbox.id)) {
215
+ checkbox.checked = false;
216
+ checkbox.disabled = true;
217
+ }
218
+ });
219
+ } else {
220
+ otherCheckboxes.forEach(checkbox => {
221
+ checkbox.disabled = false;
222
+ });
223
+ }
224
+ });
225
+ }
226
+ });
227
+ </script>
228
+ </body>
229
+ </html>