santi9462 commited on
Commit
5f392d5
·
verified ·
1 Parent(s): 5ff0e66

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +20 -341
app.py CHANGED
@@ -1,348 +1,10 @@
1
- # app.py
2
- import os, csv, random
3
- from datetime import datetime
4
- import numpy as np
5
- from PIL import Image
6
- import gradio as gr
7
-
8
- # ---- PyTorch / Torchvision (optional) ----
9
- TORCH_AVAILABLE = True
10
- try:
11
- import torch, torch.nn as nn
12
- from torchvision import transforms, models
13
- except Exception:
14
- TORCH_AVAILABLE = False
15
-
16
- # ---------- Paths ----------
17
- IMG_DIR = "durian_images"
18
- HIS_CSV = "history/history.csv"
19
- CKPT_PATH = "durian_mnv2_ckpt.pth" # วางไฟล์น้ำหนักไว้โฟลเดอร์เดียวกับ app.py
20
-
21
- os.makedirs(IMG_DIR, exist_ok=True)
22
- os.makedirs(os.path.dirname(HIS_CSV), exist_ok=True)
23
-
24
- # ---------- Labels ----------
25
- RIPENESS_LABELS = ["อ่อน", "แก่รอสักระยะ", "แก่พร้อมรับประทาน", "สุกงอม"]
26
-
27
- # ---------- Caption Variants ----------
28
- RIPENESS_CAPTION_VARIANTS = {
29
- "อ่อน": [
30
- [
31
- "หนามและพื้นผิวเปลือกคมชัด",
32
- "ขั้วผลอยู่กึ่งกลางเฟรม",
33
- "ควรรอต่อให้ครบอายุประมาณ 120 วัน",
34
- "เนื้อยังไม่แน่น กลิ่นยังไม่ชัด",
35
- "ยังไม่เหมาะสำหรับบริโภค"
36
- ],
37
- [
38
- "ผิวเปลือกเขียวเด่นและแข็ง",
39
- "มุมภาพเห็นโครงสร้างหนามชัดเจน",
40
- "แนะนำให้พักผลต่ออีกระยะจนถึง 120 วัน",
41
- "ตอนนี้เนื้อยังไม่พัฒนาเต็มที่",
42
- "ไม่แนะนำให้รับประทานทันที"
43
- ],
44
- [
45
- "โครงหนามละเอียด สีผิวยังเขียว",
46
- "ก้านและขั้วอยู่ตำแหน่งกึ่งกลางภาพ",
47
- "ควรเก็บพักเพื่อให้ความสุกก้าวหน้า",
48
- "กลิ่นและรสชาติยังไม่เด่น",
49
- "ไม่เหมาะต่อการบริโภคตอนนี้"
50
- ],
51
- [
52
- "พื้นผิวเปลือกแน่นและหนามคม",
53
- "องค์ประกอบภาพเน้นขั้วผลตรงกลาง",
54
- "รอให้ครบอายุเก็บเกี่ยวประมาณ 120 วัน",
55
- "ความแน่นของเนื้อยังไม่ถึงระดับรับประทาน",
56
- "ขอแนะนำให้รอต่อ"
57
- ],
58
- ],
59
- "แก่รอสักระยะ": [
60
- [
61
- "สีผิวเริ่มเขียวปนเหลือง",
62
- "สภาพผลคงความแน่น",
63
- "ควรพักผลต่อประมาณ 3 ถึง 5 วัน",
64
- "เพื่อให้กลิ่นและรสหวานพัฒนา",
65
- "ยังไม่เหมาะสำหรับรับประทานทันที"
66
- ],
67
- [
68
- "โทนสีเปลือกเริ่มเปลี่ยนแต่ยังไม่เต็มที่",
69
- "หนามทู่ขึ้นเล็กน้อย",
70
- "พักผลต่ออีกไม่กี่วัน",
71
- "เพื่อให้เนื้อแน่นกำลังดีและกลิ่นชัดขึ้น"
72
- ],
73
- [
74
- "ลักษณะโดยรวมใกล้สุกแต่ยังไม่ถึงจุดเหมาะ",
75
- "แนะนำให้วางพักในอุณหภูมิห้อง",
76
- "คาดว่าอีก 3 ถึง 7 วันจะพร้อมบริโภค"
77
- ],
78
- [
79
- "สีผิวมีการไล่เฉดชัดกว่าเดิม",
80
- "สัญญาณความแก่ปรากฏแต่ยังไม่สุด",
81
- "ควรเว้นระยะก่อนบริโภคเพื่อรสชาติที่ดีกว่า"
82
- ],
83
- ],
84
- "แก่พร้อมรับประทาน": [
85
- [
86
- "สีผิวเขียวปนเหลืองชัดเจน",
87
- "หนามไม่แหลมจัด",
88
- "เนื้อแน่นละมุน กลิ่นหอมชัด",
89
- "เหมาะ���ำหรับบริโภคทันที"
90
- ],
91
- [
92
- "โทนสีและพื้นผิวบ่งชี้ความสุกกำลังดี",
93
- "ก้านและขั้วดูแห้งพอเหมาะ",
94
- "เปิดผลแล้วควรรับประทานภายใน 1 ถึง 2 วัน"
95
- ],
96
- [
97
- "สมดุลของสีและรูปทรงเข้าระยะพอดี",
98
- "ความหวานและกลิ่นพร้อม",
99
- "เหมาะทั้งบริโภคสดและจำหน่าย"
100
- ],
101
- [
102
- "เนื้อมีแนวโน้มครีมมี่",
103
- "กลิ่นหอมเด่นระดับพอดี",
104
- "พร้อมสำหรับการบริโภคทันที"
105
- ],
106
- ],
107
- "สุกงอม": [
108
- [
109
- "สีผิวเหลืองเข้ม หนามทู่",
110
- "เนื้อนิ่ม กลิ่นแรงมาก",
111
- "ควรรีบบริโภคหรือแปรรูป",
112
- "เหมาะสำหรับผู้ที่ชอบรสจัด"
113
- ],
114
- [
115
- "โทนสีและผิวบ่งชี้ว่าสุกเกินระดับพร้อมทาน",
116
- "เสี่ยงนิ่มเละหากพักไว้นาน",
117
- "แนะนำทำเมนูแปรรูปทันที"
118
- ],
119
- [
120
- "กลิ่นฉุนชัดเจน",
121
- "ความแน่นเนื้อค่อนข้างต่ำ",
122
- "เหมาะกับการทำขนมหรือแช่แข็งเก็บ"
123
- ],
124
- [
125
- "สัญญาณการสุกจัดปรากฏชัด",
126
- "ควรจัดการภายในวันเดียว",
127
- "เพื่อคงคุณภาพและรสชาติ"
128
- ],
129
- ],
130
- }
131
-
132
- # ---------- ความมั่นใจ (ไม่ปั๊มตัวเลข) ----------
133
- def adjust_confidence(raw_conf: float) -> float:
134
- p = raw_conf if raw_conf > 1 else raw_conf * 100.0
135
- return round(max(0.0, min(100.0, p)), 1)
136
-
137
- # ---------- บูสต์ความมั่นใจแบบ Sharpening ----------
138
- def sharpen_prob(p: float, alpha: float = 1.6) -> float:
139
- """
140
- p' = p^alpha / (p^alpha + (1-p)^alpha), alpha>1 ทำให้ความคมสูงขึ้น
141
- """
142
- p = float(np.clip(p, 1e-8, 1-1e-8))
143
- num = p ** alpha
144
- den = num + (1 - p) ** alpha
145
- return float(num / den)
146
-
147
- # ---------- เลือกแคปชั่น ----------
148
- def pick_variant(level: str):
149
- variants = RIPENESS_CAPTION_VARIANTS.get(level, [[]])
150
- return random.choice(variants) if variants else []
151
-
152
- def generate_caption(level: str, raw_conf_pct: float) -> str:
153
- conf = adjust_confidence(raw_conf_pct)
154
- head = f"ทุเรียนระดับ {level} โดยประมาณ (ความมั่นใจ {conf}%)"
155
- body_list = pick_variant(level)
156
- return " ".join([head] + body_list)
157
-
158
- # ---------- Fallback 4-class by color ----------
159
- def _classify_4class_by_color(img: Image.Image):
160
- arr = np.array(img.convert("RGB")).reshape(-1, 3).mean(axis=0)
161
- R, G, B = arr
162
- score = [G, (R+G)/2, (0.7*R+0.3*G), (0.8*R+0.1*G)]
163
- ex = np.exp(np.array(score) / (max(score)+1e-6))
164
- probs = ex / ex.sum()
165
- idx = int(np.argmax(probs))
166
- return idx, probs
167
-
168
- # ---------- Model (PyTorch) ----------
169
- device = torch.device("cuda" if (TORCH_AVAILABLE and torch.cuda.is_available()) else "cpu") if TORCH_AVAILABLE else "cpu"
170
- idx_to_class = {i: name for i, name in enumerate(RIPENESS_LABELS)}
171
- model = None
172
- temperature = 1.0
173
-
174
- def _build_model(num_classes=4):
175
- m = models.mobilenet_v2(weights=None)
176
- in_f = m.classifier[1].in_features
177
- m.classifier[1] = nn.Linear(in_f, num_classes)
178
- return m
179
-
180
- def _load_model():
181
- """โหลดโมเดล + mapping จาก ckpt ถ้ามี"""
182
- global model, idx_to_class, temperature
183
- if not os.path.exists(CKPT_PATH):
184
- print(f"[WARN] ckpt not found at {CKPT_PATH}. Use color fallback.")
185
- return False
186
-
187
- ckpt = torch.load(CKPT_PATH, map_location=device)
188
-
189
- state = None
190
- if isinstance(ckpt, dict):
191
- for k in ["state_dict", "model_state_dict", "model"]:
192
- if k in ckpt and isinstance(ckpt[k], dict):
193
- state = ckpt[k]; break
194
- if state is None:
195
- if all(hasattr(v, "shape") for v in ckpt.values()):
196
- state = ckpt
197
- else:
198
- print("[WARN] Unknown ckpt format, using fallback.")
199
- return False
200
-
201
- model_ = _build_model(num_classes=len(RIPENESS_LABELS))
202
- if state is not None:
203
- state = {k.replace("module.", ""): v for k, v in state.items()}
204
- model_.load_state_dict(state, strict=False)
205
- else:
206
- return False
207
-
208
- if "class_to_idx" in ckpt and isinstance(ckpt["class_to_idx"], dict):
209
- c2i = ckpt["class_to_idx"]
210
- idx_to_class = {i: lbl for lbl, i in c2i.items()}
211
-
212
- temperature = float(ckpt.get("temperature", 1.0))
213
- model_.to(device).eval()
214
- globals()["model"] = model_
215
- globals()["idx_to_class"] = idx_to_class
216
- globals()["temperature"] = temperature
217
- print(f"[OK] Model loaded. Temperature={temperature}. Classes={list(idx_to_class.values())}")
218
- return True
219
-
220
- MODEL_READY = _load_model() if TORCH_AVAILABLE else False
221
-
222
- # Preprocess (เฉพาะเมื่อมี torch)
223
- if TORCH_AVAILABLE:
224
- IM_SIZE = 224
225
- _base_tf = transforms.Compose([
226
- transforms.Resize((IM_SIZE, IM_SIZE)),
227
- transforms.ToTensor(),
228
- transforms.Normalize(mean=[0.485,0.456,0.406],
229
- std=[0.229,0.224,0.225]),
230
- ])
231
-
232
- def _predict_proba_with_model(img: Image.Image):
233
- """TTA เบา ๆ : original + flip แล้วเฉลี่ย"""
234
- imgs = [img, img.transpose(Image.FLIP_LEFT_RIGHT)]
235
- xs = torch.stack([ _base_tf(im) for im in imgs ], dim=0).to(device)
236
- with torch.no_grad():
237
- logits = model(xs) / temperature
238
- probs = torch.softmax(logits, dim=1).mean(dim=0).cpu().numpy()
239
- return probs
240
-
241
- # ---------- Core inference ----------
242
- def label_by_idx(i: int) -> str:
243
- return idx_to_class.get(i, RIPENESS_LABELS[i])
244
-
245
- def infer_ripeness_and_caption(image: Image.Image):
246
- if MODEL_READY:
247
- probs = _predict_proba_with_model(image)
248
- idx = int(np.argmax(probs))
249
- else:
250
- idx, probs = _classify_4class_by_color(image)
251
- label = label_by_idx(idx)
252
- raw_conf_pct = float(probs[idx]) * 100.0
253
- cap = generate_caption(label, raw_conf_pct)
254
- return idx, probs, cap
255
-
256
- # ---------- History ----------
257
- def save_history_row(ts, cls, conf, cap, path):
258
- is_new = not os.path.exists(HIS_CSV)
259
- with open(HIS_CSV, "a", newline="", encoding="utf-8") as f:
260
- w = csv.writer(f)
261
- if is_new:
262
- w.writerow(["timestamp", "class", "confidence", "caption", "image_path"])
263
- w.writerow([ts, cls, conf, cap, path])
264
-
265
- def load_history(limit=200):
266
- if not os.path.exists(HIS_CSV):
267
- return []
268
- rows = []
269
- with open(HIS_CSV, "r", encoding="utf-8") as f:
270
- for r in csv.DictReader(f):
271
- rows.append(r)
272
- rows = rows[-limit:]
273
- return rows[::-1]
274
-
275
- # ---------- Gradio handlers ----------
276
- def analyze(image, alpha, floor_pct, no_boost_if_borderline):
277
- if image is None:
278
- return "กรุณาอัปโหลดภาพ", "", None, "❌ ไม่มีภาพ"
279
- try:
280
- idx, probs, caption = infer_ripeness_and_caption(image)
281
-
282
- order = np.argsort(probs)[::-1]
283
- top1, top2 = int(order[0]), int(order[1])
284
- p1, p2 = float(probs[top1]), float(probs[top2])
285
-
286
- class_name = label_by_idx(top1)
287
-
288
- # ---- ปรับค่าที่จะแสดงผล ----
289
- gap = p1 - p2
290
- p_display = p1
291
- if not (no_boost_if_borderline and gap < 0.15):
292
- p_display = sharpen_prob(p1, alpha)
293
-
294
- conf_pct = adjust_confidence(p_display) # 0..100
295
- if gap >= 0.15: # ไม่ใช่เคสก้ำกึ่ง อนุญาตตั้งขั้นต่ำ
296
- conf_pct = max(conf_pct, float(floor_pct))
297
-
298
- conf_str = f"{conf_pct:.1f}%"
299
-
300
- borderline = ""
301
- if gap < 0.15:
302
- borderline = (f"\n⚠️ ก้ำกึ่งระหว่าง "
303
- f"{label_by_idx(top1)} ({p1*100:.1f}%) และ "
304
- f"{label_by_idx(top2)} ({p2*100:.1f}%)")
305
-
306
- result_text = f"ระดับ: {class_name} (ความมั่นใจ {conf_str}){borderline}"
307
-
308
- except Exception:
309
- result_text = "พร้อมรับประทาน(สำรอง) (ความมั่นใจ 100.0%)"
310
- caption = "เดโม แบบสำรอง"
311
- class_name = "พร้อมรับประทาน(สำรอง)"
312
- conf_str = "100.0%"
313
-
314
- ts = datetime.now().strftime("%Y%m%d_%H%M%S")
315
- out_path = os.path.join(IMG_DIR, f"durian_{ts}.jpg")
316
- try:
317
- image.save(out_path, quality=90)
318
- save_history_row(datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
319
- class_name, conf_str, caption, out_path)
320
- except Exception:
321
- pass
322
-
323
- status_text = "✅ เสร็จสิ้น" if MODEL_READY else "ℹ️ ใช้โหมดสำรอง (สี)"
324
- return result_text, caption, image, status_text
325
-
326
- def show_history():
327
- rows = load_history(limit=200)
328
- if not rows:
329
- return [], "ยังไม่มีประวัติ"
330
- imgs, txt = [], []
331
- for r in rows:
332
- p = r["image_path"]
333
- if os.path.exists(p):
334
- imgs.append(p)
335
- txt.append(f"{r['timestamp']} | {r['class']} ({r['confidence']}) | {r['caption']}")
336
- return imgs, "\n".join(txt)
337
-
338
- # ---------- UI ----------
339
  with gr.Blocks(title="Durian Happiness Level") as demo:
340
  gr.Markdown("## 🌱 Mood4Durian")
341
 
342
- # คอนโทรลปรับความมั่นใจ
343
  with gr.Row():
344
  alpha = gr.Slider(minimum=1.0, maximum=3.0, step=0.1, value=1.6,
345
- label="α ความคมของความมั่นใจ (สูง = เปอร์เซ็นต์สูงขึ้น)")
346
  floor_pct = gr.Slider(minimum=0, maximum=100, step=1, value=60,
347
  label="ขั้นต่ำแสดงผล (%)")
348
  no_boost_chk = gr.Checkbox(value=True,
@@ -352,4 +14,21 @@ with gr.Blocks(title="Durian Happiness Level") as demo:
352
  with gr.Tab("🏠 หน้าหลัก (Home)"):
353
  with gr.Row():
354
  with gr.Column():
355
- img_in = gr.Image(type="pil", label
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  with gr.Blocks(title="Durian Happiness Level") as demo:
2
  gr.Markdown("## 🌱 Mood4Durian")
3
 
4
+ # ---- คอนโทรลสำหรับความมั่นใจ ----
5
  with gr.Row():
6
  alpha = gr.Slider(minimum=1.0, maximum=3.0, step=0.1, value=1.6,
7
+ label="α ความคมของความมั่นใจ (สูง = แสดงเปอร์เซ็นต์สูงขึ้น)")
8
  floor_pct = gr.Slider(minimum=0, maximum=100, step=1, value=60,
9
  label="ขั้นต่ำแสดงผล (%)")
10
  no_boost_chk = gr.Checkbox(value=True,
 
14
  with gr.Tab("🏠 หน้าหลัก (Home)"):
15
  with gr.Row():
16
  with gr.Column():
17
+ img_in = gr.Image(type="pil", label="อัปโหลดภาพทุเรียน")
18
+ btn = gr.Button("🔄 วิเคราะห์")
19
+ with gr.Column():
20
+ result = gr.Markdown("ผลการวิเคราะห์จะแสดงที่นี่")
21
+ caption = gr.Textbox(label="คำบรรยาย Caption (TH)", lines=6)
22
+ preview = gr.Image(label="ภาพตัวอย่าง", interactive=False)
23
+ status = gr.Markdown()
24
+
25
+ # ต่ออินพุตใหม่เข้า analyze
26
+ btn.click(analyze,
27
+ inputs=[img_in, alpha, floor_pct, no_boost_chk],
28
+ outputs=[result, caption, preview, status])
29
+
30
+ with gr.Tab("📜 ประวัติ (History)"):
31
+ refresh = gr.Button("รีเฟรชประวัติ")
32
+ gallery = gr.Gallery(label="ภาพย้อนหลัง", columns=4, height=220)
33
+ hist_text = gr.Textbox(label="สรุป (ล่าสุดอยู่บน)", lines=10)
34
+ refresh.click(show_history, outputs=[gallery, hist_text])