feat: anatomical map highlights selected regions, support multi-select
Browse files- _body_map_svg(selected) renders SVG dynamically, highlights chosen regions in #ED1C24
- region_selector changed to multiselect=True, value=[]
- region_selector.change → on_region_change() → live SVG update (no backend call)
- _regions_to_prompt() joins multiple selections: 'Left Arm, Right Hand'
- Removed static BODY_MAP_SVG constant, body_map_html is now a gr.HTML component
- on_load initializes body_map_html with empty selection
app.py
CHANGED
|
@@ -241,33 +241,60 @@ _BODY_REGIONS = [
|
|
| 241 |
"Left Foot", "Right Foot", "Groin / Genital", "Buttocks",
|
| 242 |
]
|
| 243 |
|
| 244 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 245 |
<div style='display:flex; flex-direction:column; align-items:center; gap:4px;
|
| 246 |
padding:8px 0; user-select:none;'>
|
| 247 |
<svg viewBox="0 0 80 180" width="72" height="162"
|
| 248 |
-
xmlns="http://www.w3.org/2000/svg"
|
| 249 |
-
<
|
| 250 |
-
<
|
| 251 |
-
<
|
| 252 |
-
<rect
|
| 253 |
-
<
|
| 254 |
-
<rect
|
| 255 |
-
<
|
| 256 |
-
<rect
|
| 257 |
-
<
|
| 258 |
-
<
|
| 259 |
-
<
|
| 260 |
-
<
|
| 261 |
-
<
|
| 262 |
-
<
|
| 263 |
-
<
|
| 264 |
-
<
|
| 265 |
-
<!-- right leg -->
|
| 266 |
-
<rect x="43" y="80" width="14" height="54" rx="5" fill="#4b5563"/>
|
| 267 |
-
<!-- left foot -->
|
| 268 |
-
<ellipse cx="30" cy="140" rx="9" ry="5" fill="#4b5563"/>
|
| 269 |
-
<!-- right foot -->
|
| 270 |
-
<ellipse cx="50" cy="140" rx="9" ry="5" fill="#4b5563"/>
|
| 271 |
</svg>
|
| 272 |
<div style='font-size:0.6rem; color:#4b5563; font-family:monospace;'>anatomical map</div>
|
| 273 |
</div>
|
|
@@ -418,28 +445,37 @@ def _ui_updates(lang_choice: str):
|
|
| 418 |
"""Return gr.update() for the 4 translatable input-area components (no output_html)."""
|
| 419 |
lang = _LANG_MAP.get(lang_choice, "en")
|
| 420 |
t = _I18N[lang]
|
| 421 |
-
region_choices = [t["region_none"]] + _BODY_REGIONS
|
| 422 |
return (
|
| 423 |
gr.update(label=t["img_label"]),
|
| 424 |
gr.update(label=t["symptoms_label"], placeholder=t["symptoms_placeholder"]),
|
| 425 |
gr.update(value=t["analyze_btn"]),
|
| 426 |
-
gr.update(label=t["region_label"]
|
| 427 |
)
|
| 428 |
|
| 429 |
|
| 430 |
-
def
|
| 431 |
-
"""
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 437 |
lang = _LANG_MAP.get(lang_choice, "en")
|
| 438 |
t = _I18N[lang]
|
| 439 |
img_upd, sym_upd, btn_upd, region_upd = _ui_updates(lang_choice)
|
| 440 |
|
| 441 |
-
|
| 442 |
-
region = region_display if region_display in _BODY_REGIONS else ""
|
| 443 |
|
| 444 |
has_content = bool(image) or bool(symptoms and symptoms.strip())
|
| 445 |
if has_content:
|
|
@@ -460,21 +496,21 @@ def on_load(request: gr.Request):
|
|
| 460 |
)
|
| 461 |
img_upd, sym_upd, btn_upd, region_upd = _ui_updates(lang_display)
|
| 462 |
lang = _LANG_MAP.get(lang_display, "en")
|
| 463 |
-
return lang_display, img_upd, sym_upd, btn_upd, region_upd, _empty_output_html(lang), get_backend_status_html(lang)
|
| 464 |
|
| 465 |
|
| 466 |
# ---------------------------------------------------------------------------
|
| 467 |
# Predict
|
| 468 |
# ---------------------------------------------------------------------------
|
| 469 |
|
| 470 |
-
def predict(image, symptoms: str, lang_choice: str,
|
| 471 |
lang = _LANG_MAP.get(lang_choice, "en")
|
| 472 |
t = _I18N[lang]
|
| 473 |
|
| 474 |
if not image and not symptoms.strip():
|
| 475 |
return _empty_output_html(lang), get_backend_status_html(lang)
|
| 476 |
|
| 477 |
-
region =
|
| 478 |
|
| 479 |
try:
|
| 480 |
result = get_pipeline().process(image, symptoms.strip(), lang=lang, region=region)
|
|
@@ -614,11 +650,12 @@ with gr.Blocks(css=CSS, theme=gr.themes.Base(), title="MediVision — AMD MI300X
|
|
| 614 |
|
| 615 |
with gr.Row(equal_height=True):
|
| 616 |
with gr.Column(scale=0, min_width=80):
|
| 617 |
-
gr.HTML(
|
| 618 |
with gr.Column(scale=1):
|
| 619 |
region_selector = gr.Dropdown(
|
| 620 |
-
choices=
|
| 621 |
-
value=
|
|
|
|
| 622 |
label="Affected Body Region",
|
| 623 |
container=True,
|
| 624 |
)
|
|
@@ -647,6 +684,12 @@ with gr.Blocks(css=CSS, theme=gr.themes.Base(), title="MediVision — AMD MI300X
|
|
| 647 |
|
| 648 |
# ── Events ───────────────────────────────────────────────────────────────
|
| 649 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 650 |
lang_radio.change(
|
| 651 |
fn=on_lang_change,
|
| 652 |
inputs=[lang_radio, input_img, symptoms_txt, region_selector],
|
|
@@ -663,7 +706,7 @@ with gr.Blocks(css=CSS, theme=gr.themes.Base(), title="MediVision — AMD MI300X
|
|
| 663 |
demo.load(
|
| 664 |
fn=on_load,
|
| 665 |
inputs=[],
|
| 666 |
-
outputs=[lang_radio, input_img, symptoms_txt, submit_btn, region_selector, output_html, status_bar],
|
| 667 |
)
|
| 668 |
|
| 669 |
gr.HTML(FOOTER_HTML)
|
|
|
|
| 241 |
"Left Foot", "Right Foot", "Groin / Genital", "Buttocks",
|
| 242 |
]
|
| 243 |
|
| 244 |
+
# Each region maps to one or more SVG shape IDs that should highlight
|
| 245 |
+
_REGION_SHAPE_MAP = {
|
| 246 |
+
"Head / Face": ["svg-head"],
|
| 247 |
+
"Neck": ["svg-neck"],
|
| 248 |
+
"Chest": ["svg-chest"],
|
| 249 |
+
"Abdomen": ["svg-abdomen"],
|
| 250 |
+
"Upper Back": ["svg-upper-back"],
|
| 251 |
+
"Lower Back": ["svg-lower-back"],
|
| 252 |
+
"Left Arm": ["svg-left-arm"],
|
| 253 |
+
"Right Arm": ["svg-right-arm"],
|
| 254 |
+
"Left Hand": ["svg-left-hand"],
|
| 255 |
+
"Right Hand": ["svg-right-hand"],
|
| 256 |
+
"Left Leg": ["svg-left-leg"],
|
| 257 |
+
"Right Leg": ["svg-right-leg"],
|
| 258 |
+
"Left Foot": ["svg-left-foot"],
|
| 259 |
+
"Right Foot": ["svg-right-foot"],
|
| 260 |
+
"Groin / Genital": ["svg-groin"],
|
| 261 |
+
"Buttocks": ["svg-buttocks"],
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
_DIM = "#374151" # default fill
|
| 265 |
+
_HI = "#ED1C24" # highlighted fill
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
def _body_map_svg(selected: list) -> str:
|
| 269 |
+
active = set()
|
| 270 |
+
for r in (selected or []):
|
| 271 |
+
for sid in _REGION_SHAPE_MAP.get(r, []):
|
| 272 |
+
active.add(sid)
|
| 273 |
+
|
| 274 |
+
def f(sid):
|
| 275 |
+
return _HI if sid in active else _DIM
|
| 276 |
+
|
| 277 |
+
return f"""
|
| 278 |
<div style='display:flex; flex-direction:column; align-items:center; gap:4px;
|
| 279 |
padding:8px 0; user-select:none;'>
|
| 280 |
<svg viewBox="0 0 80 180" width="72" height="162"
|
| 281 |
+
xmlns="http://www.w3.org/2000/svg">
|
| 282 |
+
<ellipse id="svg-head" cx="40" cy="13" rx="11" ry="12" fill="{f('svg-head')}"/>
|
| 283 |
+
<rect id="svg-neck" x="35" y="24" width="10" height="8" rx="2" fill="{f('svg-neck')}"/>
|
| 284 |
+
<rect id="svg-chest" x="22" y="32" width="36" height="22" rx="4" fill="{f('svg-chest')}"/>
|
| 285 |
+
<rect id="svg-abdomen" x="22" y="55" width="36" height="20" rx="4" fill="{f('svg-abdomen')}"/>
|
| 286 |
+
<rect id="svg-upper-back" x="22" y="32" width="36" height="22" rx="4" fill="{'#c0392b' if 'svg-upper-back' in active else 'none'}" opacity="0.5"/>
|
| 287 |
+
<rect id="svg-lower-back" x="22" y="55" width="36" height="20" rx="4" fill="{'#c0392b' if 'svg-lower-back' in active else 'none'}" opacity="0.5"/>
|
| 288 |
+
<rect id="svg-left-arm" x="7" y="32" width="13" height="38" rx="5" fill="{f('svg-left-arm')}"/>
|
| 289 |
+
<rect id="svg-right-arm" x="60" y="32" width="13" height="38" rx="5" fill="{f('svg-right-arm')}"/>
|
| 290 |
+
<ellipse id="svg-left-hand" cx="13" cy="76" rx="7" ry="5" fill="{f('svg-left-hand')}"/>
|
| 291 |
+
<ellipse id="svg-right-hand" cx="67" cy="76" rx="7" ry="5" fill="{f('svg-right-hand')}"/>
|
| 292 |
+
<rect id="svg-groin" x="27" y="76" width="26" height="8" rx="3" fill="{f('svg-groin')}"/>
|
| 293 |
+
<rect id="svg-buttocks" x="27" y="76" width="26" height="8" rx="3" fill="{'#c0392b' if 'svg-buttocks' in active else 'none'}" opacity="0.6"/>
|
| 294 |
+
<rect id="svg-left-leg" x="22" y="85" width="15" height="52" rx="5" fill="{f('svg-left-leg')}"/>
|
| 295 |
+
<rect id="svg-right-leg" x="43" y="85" width="15" height="52" rx="5" fill="{f('svg-right-leg')}"/>
|
| 296 |
+
<ellipse id="svg-left-foot" cx="29" cy="142" rx="10" ry="5" fill="{f('svg-left-foot')}"/>
|
| 297 |
+
<ellipse id="svg-right-foot" cx="51" cy="142" rx="10" ry="5" fill="{f('svg-right-foot')}"/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 298 |
</svg>
|
| 299 |
<div style='font-size:0.6rem; color:#4b5563; font-family:monospace;'>anatomical map</div>
|
| 300 |
</div>
|
|
|
|
| 445 |
"""Return gr.update() for the 4 translatable input-area components (no output_html)."""
|
| 446 |
lang = _LANG_MAP.get(lang_choice, "en")
|
| 447 |
t = _I18N[lang]
|
|
|
|
| 448 |
return (
|
| 449 |
gr.update(label=t["img_label"]),
|
| 450 |
gr.update(label=t["symptoms_label"], placeholder=t["symptoms_placeholder"]),
|
| 451 |
gr.update(value=t["analyze_btn"]),
|
| 452 |
+
gr.update(label=t["region_label"]),
|
| 453 |
)
|
| 454 |
|
| 455 |
|
| 456 |
+
def _regions_to_prompt(selected) -> str:
|
| 457 |
+
"""Convert list (or single string) selection to prompt string."""
|
| 458 |
+
if not selected:
|
| 459 |
+
return ""
|
| 460 |
+
if isinstance(selected, str):
|
| 461 |
+
selected = [selected]
|
| 462 |
+
valid = [r for r in selected if r in _BODY_REGIONS]
|
| 463 |
+
return ", ".join(valid)
|
| 464 |
+
|
| 465 |
+
|
| 466 |
+
def on_region_change(selected):
|
| 467 |
+
"""Re-render the body map SVG when selection changes."""
|
| 468 |
+
if isinstance(selected, str):
|
| 469 |
+
selected = [selected] if selected in _BODY_REGIONS else []
|
| 470 |
+
return _body_map_svg(selected or [])
|
| 471 |
+
|
| 472 |
+
|
| 473 |
+
def on_lang_change(lang_choice: str, image, symptoms: str, selected_regions):
|
| 474 |
lang = _LANG_MAP.get(lang_choice, "en")
|
| 475 |
t = _I18N[lang]
|
| 476 |
img_upd, sym_upd, btn_upd, region_upd = _ui_updates(lang_choice)
|
| 477 |
|
| 478 |
+
region = _regions_to_prompt(selected_regions)
|
|
|
|
| 479 |
|
| 480 |
has_content = bool(image) or bool(symptoms and symptoms.strip())
|
| 481 |
if has_content:
|
|
|
|
| 496 |
)
|
| 497 |
img_upd, sym_upd, btn_upd, region_upd = _ui_updates(lang_display)
|
| 498 |
lang = _LANG_MAP.get(lang_display, "en")
|
| 499 |
+
return lang_display, img_upd, sym_upd, btn_upd, region_upd, _body_map_svg([]), _empty_output_html(lang), get_backend_status_html(lang)
|
| 500 |
|
| 501 |
|
| 502 |
# ---------------------------------------------------------------------------
|
| 503 |
# Predict
|
| 504 |
# ---------------------------------------------------------------------------
|
| 505 |
|
| 506 |
+
def predict(image, symptoms: str, lang_choice: str, selected_regions):
|
| 507 |
lang = _LANG_MAP.get(lang_choice, "en")
|
| 508 |
t = _I18N[lang]
|
| 509 |
|
| 510 |
if not image and not symptoms.strip():
|
| 511 |
return _empty_output_html(lang), get_backend_status_html(lang)
|
| 512 |
|
| 513 |
+
region = _regions_to_prompt(selected_regions)
|
| 514 |
|
| 515 |
try:
|
| 516 |
result = get_pipeline().process(image, symptoms.strip(), lang=lang, region=region)
|
|
|
|
| 650 |
|
| 651 |
with gr.Row(equal_height=True):
|
| 652 |
with gr.Column(scale=0, min_width=80):
|
| 653 |
+
body_map_html = gr.HTML(value=_body_map_svg([]))
|
| 654 |
with gr.Column(scale=1):
|
| 655 |
region_selector = gr.Dropdown(
|
| 656 |
+
choices=_BODY_REGIONS,
|
| 657 |
+
value=[],
|
| 658 |
+
multiselect=True,
|
| 659 |
label="Affected Body Region",
|
| 660 |
container=True,
|
| 661 |
)
|
|
|
|
| 684 |
|
| 685 |
# ── Events ───────────────────────────────────────────────────────────────
|
| 686 |
|
| 687 |
+
region_selector.change(
|
| 688 |
+
fn=on_region_change,
|
| 689 |
+
inputs=[region_selector],
|
| 690 |
+
outputs=[body_map_html],
|
| 691 |
+
)
|
| 692 |
+
|
| 693 |
lang_radio.change(
|
| 694 |
fn=on_lang_change,
|
| 695 |
inputs=[lang_radio, input_img, symptoms_txt, region_selector],
|
|
|
|
| 706 |
demo.load(
|
| 707 |
fn=on_load,
|
| 708 |
inputs=[],
|
| 709 |
+
outputs=[lang_radio, input_img, symptoms_txt, submit_btn, region_selector, body_map_html, output_html, status_bar],
|
| 710 |
)
|
| 711 |
|
| 712 |
gr.HTML(FOOTER_HTML)
|