nodda commited on
Commit ·
b123157
1
Parent(s): aa26bce
feat: 식물종 선택 드롭다운 + 지원 작물 안내 배너 + plant_filter 진단 개선
Browse files
app.py
CHANGED
|
@@ -321,38 +321,46 @@ TTA_TF = [
|
|
| 321 |
T.Compose([T.Resize(256), T.CenterCrop(224), T.RandomHorizontalFlip(p=1.0), T.ToTensor(), _NORM]),
|
| 322 |
]
|
| 323 |
|
| 324 |
-
def _predict_ensemble(pil_img):
|
| 325 |
-
"""TTA + 앙상블 예측"""
|
| 326 |
if not _models:
|
| 327 |
return None, 0.0
|
| 328 |
-
|
| 329 |
device = next(_models[0][0].parameters()).device
|
| 330 |
-
|
| 331 |
# TTA 배치 준비
|
| 332 |
tensors = [tf(pil_img) for tf in TTA_TF]
|
| 333 |
batch = torch.stack(tensors).to(device)
|
| 334 |
-
|
| 335 |
logits_sum = torch.zeros(NUM_CLASSES, device=device)
|
| 336 |
-
|
| 337 |
with torch.no_grad():
|
| 338 |
for model, weight, desc in _models:
|
| 339 |
outs = model(batch) # [TTA, 38]
|
| 340 |
avg = outs.mean(0) # [38]
|
| 341 |
logits_sum += avg * weight
|
| 342 |
-
|
| 343 |
# 칼리브레이션
|
| 344 |
for idx, factor in CALIBRATION.items():
|
| 345 |
logits_sum[idx] = logits_sum[idx] * factor
|
| 346 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 347 |
probs = torch.softmax(logits_sum, dim=0)
|
| 348 |
top3_probs, top3_ids = probs.topk(3)
|
| 349 |
-
|
| 350 |
top3 = [(ID2LABEL[int(i)], float(p)) for i, p in zip(top3_ids, top3_probs)]
|
| 351 |
return top3, float(probs.max())
|
| 352 |
|
| 353 |
# ── Main diagnosis function ──────────────────────────────────────────────────
|
| 354 |
@spaces.GPU(duration=30)
|
| 355 |
-
def diagnose(image, language):
|
| 356 |
t0 = time.time()
|
| 357 |
|
| 358 |
if image is None:
|
|
@@ -372,9 +380,24 @@ def diagnose(image, language):
|
|
| 372 |
if not isinstance(image, Image.Image):
|
| 373 |
image = Image.fromarray(image)
|
| 374 |
pil_img = image.convert("RGB")
|
| 375 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 376 |
# 예측
|
| 377 |
-
top3, conf = _predict_ensemble(pil_img)
|
| 378 |
elapsed = time.time() - t0
|
| 379 |
|
| 380 |
if top3 is None:
|
|
@@ -493,14 +516,50 @@ with gr.Blocks(
|
|
| 493 |
</div>
|
| 494 |
""")
|
| 495 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 496 |
with gr.Row():
|
| 497 |
with gr.Column(scale=1):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 498 |
img_input = gr.Image(
|
| 499 |
type="pil",
|
| 500 |
label="📸 Plant Photo / 식물 사진",
|
| 501 |
elem_classes="upload-area",
|
| 502 |
sources=["upload", "webcam", "clipboard"],
|
| 503 |
-
height=
|
| 504 |
)
|
| 505 |
lang_select = gr.Radio(
|
| 506 |
choices=["English", "한국어 (Korean)"],
|
|
@@ -512,12 +571,6 @@ with gr.Blocks(
|
|
| 512 |
variant="primary",
|
| 513 |
size="lg",
|
| 514 |
)
|
| 515 |
-
|
| 516 |
-
gr.Markdown("""
|
| 517 |
-
**Supported crops**: Apple, Blueberry, Cherry, Corn, Grape, Orange, Peach, Pepper, Potato, Raspberry, Soybean, Squash, Strawberry, Tomato
|
| 518 |
-
|
| 519 |
-
**38 disease/healthy classes** | PlantVillage accuracy: **99.91%**
|
| 520 |
-
""")
|
| 521 |
|
| 522 |
with gr.Column(scale=1):
|
| 523 |
label_output = gr.Label(
|
|
@@ -532,12 +585,12 @@ with gr.Blocks(
|
|
| 532 |
|
| 533 |
btn.click(
|
| 534 |
fn=diagnose,
|
| 535 |
-
inputs=[img_input, lang_select],
|
| 536 |
outputs=[label_output, detail_output],
|
| 537 |
)
|
| 538 |
img_input.change(
|
| 539 |
fn=diagnose,
|
| 540 |
-
inputs=[img_input, lang_select],
|
| 541 |
outputs=[label_output, detail_output],
|
| 542 |
)
|
| 543 |
|
|
|
|
| 321 |
T.Compose([T.Resize(256), T.CenterCrop(224), T.RandomHorizontalFlip(p=1.0), T.ToTensor(), _NORM]),
|
| 322 |
]
|
| 323 |
|
| 324 |
+
def _predict_ensemble(pil_img, plant_filter=None):
|
| 325 |
+
"""TTA + 앙상블 예측. plant_filter가 있으면 해당 식물 클래스만 후보로 사용."""
|
| 326 |
if not _models:
|
| 327 |
return None, 0.0
|
| 328 |
+
|
| 329 |
device = next(_models[0][0].parameters()).device
|
| 330 |
+
|
| 331 |
# TTA 배치 준비
|
| 332 |
tensors = [tf(pil_img) for tf in TTA_TF]
|
| 333 |
batch = torch.stack(tensors).to(device)
|
| 334 |
+
|
| 335 |
logits_sum = torch.zeros(NUM_CLASSES, device=device)
|
| 336 |
+
|
| 337 |
with torch.no_grad():
|
| 338 |
for model, weight, desc in _models:
|
| 339 |
outs = model(batch) # [TTA, 38]
|
| 340 |
avg = outs.mean(0) # [38]
|
| 341 |
logits_sum += avg * weight
|
| 342 |
+
|
| 343 |
# 칼리브레이션
|
| 344 |
for idx, factor in CALIBRATION.items():
|
| 345 |
logits_sum[idx] = logits_sum[idx] * factor
|
| 346 |
+
|
| 347 |
+
# 식물종 필터 — 선택한 작물 클래스만 활성화
|
| 348 |
+
if plant_filter:
|
| 349 |
+
mask = torch.full((NUM_CLASSES,), float('-inf'), device=device)
|
| 350 |
+
for idx, label in ID2LABEL.items():
|
| 351 |
+
if plant_filter.lower() in label.lower():
|
| 352 |
+
mask[idx] = 0.0
|
| 353 |
+
logits_sum = logits_sum + mask
|
| 354 |
+
|
| 355 |
probs = torch.softmax(logits_sum, dim=0)
|
| 356 |
top3_probs, top3_ids = probs.topk(3)
|
| 357 |
+
|
| 358 |
top3 = [(ID2LABEL[int(i)], float(p)) for i, p in zip(top3_ids, top3_probs)]
|
| 359 |
return top3, float(probs.max())
|
| 360 |
|
| 361 |
# ── Main diagnosis function ──────────────────────────────────────────────────
|
| 362 |
@spaces.GPU(duration=30)
|
| 363 |
+
def diagnose(image, language, plant_type="Auto-detect"):
|
| 364 |
t0 = time.time()
|
| 365 |
|
| 366 |
if image is None:
|
|
|
|
| 380 |
if not isinstance(image, Image.Image):
|
| 381 |
image = Image.fromarray(image)
|
| 382 |
pil_img = image.convert("RGB")
|
| 383 |
+
|
| 384 |
+
# 식물종 필터 매핑
|
| 385 |
+
PLANT_KEYWORD_MAP = {
|
| 386 |
+
"Apple": "Apple", "Blueberry": "Blueberry", "Cherry": "Cherry",
|
| 387 |
+
"Corn": "Corn", "Maize": "Corn", "Grape": "Grape", "Orange": "Orange",
|
| 388 |
+
"Peach": "Peach", "Pepper": "Pepper", "Potato": "Potato",
|
| 389 |
+
"Raspberry": "Raspberry", "Soybean": "Soybean", "Squash": "Squash",
|
| 390 |
+
"Strawberry": "Strawberry", "Tomato": "Tomato",
|
| 391 |
+
}
|
| 392 |
+
selected_plant = None
|
| 393 |
+
if plant_type and "Auto-detect" not in plant_type:
|
| 394 |
+
for kw, plant in PLANT_KEYWORD_MAP.items():
|
| 395 |
+
if kw.lower() in plant_type.lower():
|
| 396 |
+
selected_plant = plant
|
| 397 |
+
break
|
| 398 |
+
|
| 399 |
# 예측
|
| 400 |
+
top3, conf = _predict_ensemble(pil_img, plant_filter=selected_plant)
|
| 401 |
elapsed = time.time() - t0
|
| 402 |
|
| 403 |
if top3 is None:
|
|
|
|
| 516 |
</div>
|
| 517 |
""")
|
| 518 |
|
| 519 |
+
# 지원 작물 목록
|
| 520 |
+
SUPPORTED_PLANTS = [
|
| 521 |
+
"🔍 Auto-detect (자동감지)",
|
| 522 |
+
"🍎 Apple (사과)",
|
| 523 |
+
"🫐 Blueberry (블루베리)",
|
| 524 |
+
"🍒 Cherry (체리)",
|
| 525 |
+
"🌽 Corn / Maize (옥수수)",
|
| 526 |
+
"🍇 Grape (포도)",
|
| 527 |
+
"🍊 Orange (오렌지)",
|
| 528 |
+
"🍑 Peach (복숭아)",
|
| 529 |
+
"🫑 Bell Pepper (피망/고추)",
|
| 530 |
+
"🥔 Potato (감자)",
|
| 531 |
+
"🫐 Raspberry (라즈베리)",
|
| 532 |
+
"🌱 Soybean (콩)",
|
| 533 |
+
"🎃 Squash (호박)",
|
| 534 |
+
"🍓 Strawberry (딸기)",
|
| 535 |
+
"🍅 Tomato (토마토)",
|
| 536 |
+
]
|
| 537 |
+
|
| 538 |
+
gr.HTML("""
|
| 539 |
+
<div style="background:#fff8e1;border-left:4px solid #ffc107;padding:12px 16px;border-radius:8px;margin-bottom:12px;">
|
| 540 |
+
<b>⚠️ 지원 작물 안내 / Supported Crops</b><br>
|
| 541 |
+
이 앱은 아래 <b>14종 작물</b>의 병해를 진단합니다. 지원하지 않는 식물은 오진될 수 있습니다.<br>
|
| 542 |
+
<small>This app diagnoses diseases for <b>14 specific crops</b> only. Other plants may be misclassified.</small><br>
|
| 543 |
+
<br>
|
| 544 |
+
🍎 Apple 🫐 Blueberry 🍒 Cherry 🌽 Corn 🍇 Grape 🍊 Orange 🍑 Peach<br>
|
| 545 |
+
🫑 Pepper 🥔 Potato 🫐 Raspberry 🌱 Soybean 🎃 Squash 🍓 Strawberry 🍅 Tomato
|
| 546 |
+
</div>
|
| 547 |
+
""")
|
| 548 |
+
|
| 549 |
with gr.Row():
|
| 550 |
with gr.Column(scale=1):
|
| 551 |
+
plant_select = gr.Dropdown(
|
| 552 |
+
choices=SUPPORTED_PLANTS,
|
| 553 |
+
value="🔍 Auto-detect (자동감지)",
|
| 554 |
+
label="🌿 Select Crop / 작물 선택 (선택 시 정확도 향상)",
|
| 555 |
+
info="작물을 먼저 선택하면 더 정확하게 진단합니다",
|
| 556 |
+
)
|
| 557 |
img_input = gr.Image(
|
| 558 |
type="pil",
|
| 559 |
label="📸 Plant Photo / 식물 사진",
|
| 560 |
elem_classes="upload-area",
|
| 561 |
sources=["upload", "webcam", "clipboard"],
|
| 562 |
+
height=300,
|
| 563 |
)
|
| 564 |
lang_select = gr.Radio(
|
| 565 |
choices=["English", "한국어 (Korean)"],
|
|
|
|
| 571 |
variant="primary",
|
| 572 |
size="lg",
|
| 573 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 574 |
|
| 575 |
with gr.Column(scale=1):
|
| 576 |
label_output = gr.Label(
|
|
|
|
| 585 |
|
| 586 |
btn.click(
|
| 587 |
fn=diagnose,
|
| 588 |
+
inputs=[img_input, lang_select, plant_select],
|
| 589 |
outputs=[label_output, detail_output],
|
| 590 |
)
|
| 591 |
img_input.change(
|
| 592 |
fn=diagnose,
|
| 593 |
+
inputs=[img_input, lang_select, plant_select],
|
| 594 |
outputs=[label_output, detail_output],
|
| 595 |
)
|
| 596 |
|