dikheng commited on
Commit
5628bff
·
1 Parent(s): 4a34d56

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

Files changed (1) hide show
  1. app.py +85 -42
app.py CHANGED
@@ -241,33 +241,60 @@ _BODY_REGIONS = [
241
  "Left Foot", "Right Foot", "Groin / Genital", "Buttocks",
242
  ]
243
 
244
- BODY_MAP_SVG = """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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" style="opacity:0.55;">
249
- <!-- head -->
250
- <ellipse cx="40" cy="14" rx="11" ry="13" fill="#4b5563"/>
251
- <!-- neck -->
252
- <rect x="35" y="26" width="10" height="8" rx="2" fill="#4b5563"/>
253
- <!-- torso -->
254
- <rect x="22" y="34" width="36" height="44" rx="5" fill="#374151"/>
255
- <!-- left arm -->
256
- <rect x="8" y="34" width="12" height="36" rx="5" fill="#4b5563"/>
257
- <!-- right arm -->
258
- <rect x="60" y="34" width="12" height="36" rx="5" fill="#4b5563"/>
259
- <!-- left hand -->
260
- <ellipse cx="14" cy="78" rx="7" ry="5" fill="#4b5563"/>
261
- <!-- right hand -->
262
- <ellipse cx="66" cy="78" rx="7" ry="5" fill="#4b5563"/>
263
- <!-- left leg -->
264
- <rect x="23" y="80" width="14" height="54" rx="5" fill="#4b5563"/>
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"], choices=region_choices, value=t["region_none"]),
427
  )
428
 
429
 
430
- def on_lang_change(lang_choice: str, image, symptoms: str, region_display: str):
431
- """
432
- Language switch handler.
433
- - Always updates UI labels.
434
- - If there is existing content (image or symptoms), re-runs analysis in the new language.
435
- - If no content, shows the translated empty placeholder.
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
- # region_display may be the old lang's "Not specified" — treat those as no region
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, region_display: 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 = region_display if region_display in _BODY_REGIONS else ""
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(BODY_MAP_SVG)
618
  with gr.Column(scale=1):
619
  region_selector = gr.Dropdown(
620
- choices=["Not specified"] + _BODY_REGIONS,
621
- value="Not specified",
 
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)