pascal-maker commited on
Commit
80f05f9
Β·
verified Β·
1 Parent(s): 60d85f0

update app.py

Browse files
Files changed (1) hide show
  1. app.py +323 -400
app.py CHANGED
@@ -2,14 +2,43 @@
2
  # -*- coding: utf-8 -*-
3
 
4
  """
5
- Combined Medical-VLM, **SAM-2 automatic masking**, and CheXagent demo.
6
-
7
- β­‘ Changes β­‘
8
- -----------
9
- 1. Fixed SAM-2 installation and import issues
10
- 2. Added proper error handling for missing dependencies
11
- 3. Made SAM-2 functionality optional with graceful fallback
12
- 4. Added installation instructions and requirements check
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  """
14
 
15
  # ---------------------------------------------------------------------
@@ -21,6 +50,7 @@ import uuid
21
  import tempfile
22
  import subprocess
23
  import warnings
 
24
  from threading import Thread
25
 
26
  # Environment setup
@@ -36,74 +66,78 @@ from PIL import Image, ImageDraw
36
  import gradio as gr
37
 
38
  # =============================================================================
39
- # Dependency checker and installer
 
40
  # =============================================================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  def check_and_install_sam2():
42
  """Check if SAM-2 is available and attempt installation if needed."""
43
  try:
44
- # Try importing SAM-2
45
  from sam2.build_sam import build_sam2
46
- from sam2.automatic_mask_generator import SAM2AutomaticMaskGenerator
47
- return True, "SAM-2 already available"
48
  except ImportError:
49
- print("SAM-2 not found. Attempting to install...")
50
  try:
51
- # Clone SAM-2 repository
52
- if not os.path.exists("segment-anything-2"):
53
- subprocess.run([
54
- "git", "clone",
55
- "https://github.com/facebookresearch/segment-anything-2.git"
56
- ], check=True)
57
-
58
- # Install SAM-2
59
  original_dir = os.getcwd()
60
- os.chdir("segment-anything-2")
61
- subprocess.run([sys.executable, "-m", "pip", "install", "-e", "."], check=True)
62
  os.chdir(original_dir)
63
-
64
- # Add to Python path
65
- sys.path.insert(0, os.path.abspath("segment-anything-2"))
66
-
67
- # Try importing again
68
  from sam2.build_sam import build_sam2
69
- from sam2.automatic_mask_generator import SAM2AutomaticMaskGenerator
70
- return True, "SAM-2 installed successfully"
71
-
 
72
  except Exception as e:
73
- print(f"Failed to install SAM-2: {e}")
74
- return False, f"SAM-2 installation failed: {e}"
75
-
76
- # Check SAM-2 availability
77
- SAM2_AVAILABLE, SAM2_STATUS = check_and_install_sam2()
78
- print(f"SAM-2 Status: {SAM2_STATUS}")
79
 
80
- # =============================================================================
81
- # SAM-2 imports (conditional)
82
- # =============================================================================
83
- if SAM2_AVAILABLE:
 
84
  try:
85
  from sam2.build_sam import build_sam2
86
  from sam2.automatic_mask_generator import SAM2AutomaticMaskGenerator
87
- from sam2.modeling.sam2_base import SAM2Base
88
- from sam2.utils.misc import get_device_index
89
- except ImportError as e:
90
- print(f"SAM-2 import error: {e}")
91
- SAM2_AVAILABLE = False
92
-
93
- # =============================================================================
94
- # Qwen-VLM imports & helper
95
- # =============================================================================
96
- from transformers import Qwen2_5_VLForConditionalGeneration, AutoProcessor
97
- from qwen_vl_utils import process_vision_info
98
 
99
  # =============================================================================
100
- # CheXagent imports
101
  # =============================================================================
102
- from transformers import AutoTokenizer, AutoModelForCausalLM, TextIteratorStreamer
103
 
104
- # ---------------------------------------------------------------------
105
- # Devices
106
- # ---------------------------------------------------------------------
107
  def get_device():
108
  if torch.cuda.is_available():
109
  return torch.device("cuda")
@@ -111,344 +145,249 @@ def get_device():
111
  return torch.device("mps")
112
  return torch.device("cpu")
113
 
114
- # =============================================================================
115
- # Qwen-VLM model & agent
116
- # =============================================================================
117
- _qwen_model = None
118
- _qwen_processor = None
119
- _qwen_device = None
120
-
121
- def load_qwen_model_and_processor(hf_token=None):
122
- global _qwen_model, _qwen_processor, _qwen_device
123
- if _qwen_model is None:
124
  _qwen_device = "mps" if torch.backends.mps.is_available() else "cpu"
125
- print(f"[Qwen] loading model on {_qwen_device}")
126
- auth_kwargs = {"use_auth_token": hf_token} if hf_token else {}
127
  _qwen_model = Qwen2_5_VLForConditionalGeneration.from_pretrained(
128
- "Qwen/Qwen2.5-VL-3B-Instruct",
129
- trust_remote_code=True,
130
- attn_implementation="eager",
131
- torch_dtype=torch.float32,
132
- low_cpu_mem_usage=True,
133
- device_map=None,
134
- **auth_kwargs,
135
  ).to(_qwen_device)
136
  _qwen_processor = AutoProcessor.from_pretrained(
137
- "Qwen/Qwen2.5-VL-3B-Instruct",
138
- trust_remote_code=True,
139
- **auth_kwargs,
140
  )
141
- return _qwen_model, _qwen_processor, _qwen_device
 
 
 
 
 
 
142
 
143
- class MedicalVLMAgent:
144
- """Light wrapper around Qwen-VLM with an optional image."""
 
 
 
 
 
 
 
 
145
 
146
- def __init__(self, model, processor, device):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
  self.model = model
148
  self.processor = processor
149
- self.device = device
150
  self.system_prompt = (
151
  "You are a medical information assistant with vision capabilities.\n"
152
  "Disclaimer: I am not a licensed medical professional. "
153
  "The information provided is for reference only and should not be taken as medical advice."
154
  )
155
-
156
  def run(self, user_text: str, image: Image.Image | None = None) -> str:
157
- messages = [
158
- {"role": "system", "content": [{"type": "text", "text": self.system_prompt}]}
159
- ]
160
  user_content = []
161
  if image is not None:
162
- tmp = f"/tmp/{uuid.uuid4()}.png"
163
- image.save(tmp)
164
- user_content.append({"type": "image", "image": tmp})
165
  user_content.append({"type": "text", "text": user_text or "Please describe the image."})
166
  messages.append({"role": "user", "content": user_content})
167
 
168
- prompt_text = self.processor.apply_chat_template(
169
- messages, tokenize=False, add_generation_prompt=True
170
- )
171
- img_inputs, vid_inputs = process_vision_info(messages)
172
- inputs = self.processor(
173
- text=[prompt_text],
174
- images=img_inputs,
175
- videos=vid_inputs,
176
- padding=True,
177
- return_tensors="pt",
178
- ).to(self.device)
179
-
180
  with torch.no_grad():
181
  out = self.model.generate(**inputs, max_new_tokens=128)
182
- trimmed = out[0][inputs.input_ids.shape[1] :]
183
  return self.processor.decode(trimmed, skip_special_tokens=True).strip()
184
 
185
- # =============================================================================
186
- # SAM-2 model + AutomaticMaskGenerator (conditional)
187
- # =============================================================================
188
- def download_sam2_checkpoint():
189
- """Download SAM-2 checkpoint if not present."""
190
- checkpoint_dir = "checkpoints"
191
- checkpoint_file = "sam2.1_hiera_large.pt"
192
- checkpoint_path = os.path.join(checkpoint_dir, checkpoint_file)
193
-
194
- if not os.path.exists(checkpoint_path):
195
- os.makedirs(checkpoint_dir, exist_ok=True)
196
- print("Downloading SAM-2 checkpoint...")
197
- try:
198
- import urllib.request
199
- url = "https://dl.fbaipublicfiles.com/segment_anything_2/092824/sam2.1_hiera_large.pt"
200
- urllib.request.urlretrieve(url, checkpoint_path)
201
- print("SAM-2 checkpoint downloaded successfully")
202
- except Exception as e:
203
- print(f"Failed to download SAM-2 checkpoint: {e}")
204
- return None
205
-
206
- return checkpoint_path
207
-
208
- def initialize_sam2():
209
- """Initialize SAM-2 model and mask generator."""
210
- if not SAM2_AVAILABLE:
211
- return None, None
212
-
213
- try:
214
- # Download checkpoint if needed
215
- checkpoint_path = download_sam2_checkpoint()
216
- if checkpoint_path is None:
217
- return None, None
218
-
219
- # Config path (you may need to adjust this)
220
- config_path = "segment-anything-2/sam2/configs/sam2.1/sam2.1_hiera_l.yaml"
221
- if not os.path.exists(config_path):
222
- config_path = "configs/sam2.1/sam2.1_hiera_l.yaml"
223
-
224
- device = get_device()
225
- print(f"[SAM-2] building model on {device}")
226
-
227
- sam2_model = build_sam2(
228
- config_path,
229
- checkpoint_path,
230
- device=device,
231
- apply_postprocessing=False,
232
- )
233
-
234
- mask_gen = SAM2AutomaticMaskGenerator(
235
- model=sam2_model,
236
- points_per_side=32,
237
- pred_iou_thresh=0.86,
238
- stability_score_thresh=0.92,
239
- crop_n_layers=0,
240
- )
241
- return sam2_model, mask_gen
242
-
243
- except Exception as e:
244
- print(f"[SAM-2] Failed to initialize: {e}")
245
- return None, None
246
-
247
- # Initialize SAM-2 (conditional)
248
- _sam2_model, _mask_generator = None, None
249
- if SAM2_AVAILABLE:
250
- _sam2_model, _mask_generator = initialize_sam2()
251
- if _sam2_model is not None:
252
- print("[SAM-2] Successfully initialized!")
253
- else:
254
- print("[SAM-2] Initialization failed")
255
-
256
  def automatic_mask_overlay(image_np: np.ndarray) -> np.ndarray:
257
- """Generate masks and alpha-blend them on top of the original image."""
258
- if _mask_generator is None:
259
- raise RuntimeError("SAM-2 mask generator not initialized")
260
-
261
  anns = _mask_generator.generate(image_np)
262
- if not anns:
263
- return image_np
264
-
265
  overlay = image_np.copy()
266
- if overlay.ndim == 2: # grayscale β†’ RGB
267
- overlay = np.stack([overlay] * 3, axis=2)
268
-
269
  for ann in sorted(anns, key=lambda x: x["area"], reverse=True):
270
  m = ann["segmentation"]
271
  color = np.random.randint(0, 255, 3, dtype=np.uint8)
272
  overlay[m] = (overlay[m] * 0.5 + color * 0.5).astype(np.uint8)
273
-
274
  return overlay
275
 
276
  def tumor_segmentation_interface(image: Image.Image | None):
277
- """Tumor segmentation interface with proper error handling."""
278
- if image is None:
279
- return None, "Please upload an image."
280
-
281
- if not SAM2_AVAILABLE:
282
- return None, "SAM-2 is not available. Please check installation."
283
-
284
- if _mask_generator is None:
285
- return None, "SAM-2 not properly initialized. Check the console for errors."
286
-
287
  try:
288
  img_np = np.array(image.convert("RGB"))
289
  out_np = automatic_mask_overlay(img_np)
290
  n_masks = len(_mask_generator.generate(img_np))
291
  return Image.fromarray(out_np), f"{n_masks} masks found."
292
  except Exception as e:
293
- return None, f"SAM-2 error: {e}"
294
 
295
- # =============================================================================
296
- # Simple fallback segmentation (when SAM-2 is not available)
297
- # =============================================================================
298
  def simple_segmentation_fallback(image: Image.Image | None):
299
- """Simple fallback segmentation using basic image processing."""
300
- if image is None:
301
- return None, "Please upload an image."
302
-
303
  try:
304
  import cv2
305
- from skimage import segmentation, color
306
-
307
- # Convert to numpy array
308
  img_np = np.array(image.convert("RGB"))
309
-
310
- # Simple watershed segmentation
311
  gray = cv2.cvtColor(img_np, cv2.COLOR_RGB2GRAY)
312
  _, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
313
-
314
- # Remove noise
315
  kernel = np.ones((3,3), np.uint8)
316
  opening = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel, iterations=2)
317
-
318
- # Sure background area
319
- sure_bg = cv2.dilate(opening, kernel, iterations=3)
320
-
321
- # Finding sure foreground area
322
  dist_transform = cv2.distanceTransform(opening, cv2.DIST_L2, 5)
323
  _, sure_fg = cv2.threshold(dist_transform, 0.7*dist_transform.max(), 255, 0)
324
-
325
- # Create overlay
326
  overlay = img_np.copy()
327
- overlay[sure_fg > 0] = [255, 0, 0] # Red overlay
328
-
329
- # Alpha blend
330
  result = cv2.addWeighted(img_np, 0.7, overlay, 0.3, 0)
331
-
332
  return Image.fromarray(result), "Simple segmentation applied (SAM-2 not available)"
333
-
334
  except Exception as e:
335
- return None, f"Fallback segmentation error: {e}"
336
-
337
- # =============================================================================
338
- # CheXagent set-up
339
- # =============================================================================
340
- try:
341
- chex_name = "StanfordAIMI/CheXagent-2-3b"
342
- chex_tok = AutoTokenizer.from_pretrained(chex_name, trust_remote_code=True)
343
- chex_model = AutoModelForCausalLM.from_pretrained(
344
- chex_name, device_map="auto", trust_remote_code=True
345
- )
346
- chex_model = chex_model.half() if torch.cuda.is_available() else chex_model.float()
347
- chex_model.eval()
348
- CHEXAGENT_AVAILABLE = True
349
- except Exception as e:
350
- print(f"CheXagent not available: {e}")
351
- CHEXAGENT_AVAILABLE = False
352
- chex_tok, chex_model = None, None
353
 
 
354
  def get_model_device(model):
355
- if model is None:
356
- return torch.device("cpu")
357
- for p in model.parameters():
358
- return p.device
359
- return torch.device("cpu")
360
 
361
- def clean_text(text):
362
- return text.replace("</s>", "")
363
 
364
  @torch.no_grad()
365
  def response_report_generation(pil_image_1, pil_image_2):
366
- """Structured chest-X-ray report (streaming)."""
367
- if not CHEXAGENT_AVAILABLE:
368
- yield "CheXagent is not available. Please check installation."
369
- return
370
-
371
- streamer = TextIteratorStreamer(chex_tok, skip_prompt=True, skip_special_tokens=True)
372
  paths = []
373
  for im in [pil_image_1, pil_image_2]:
374
- if im is None:
375
- continue
376
- with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tfile:
377
- im.save(tfile.name)
378
- paths.append(tfile.name)
379
-
380
  if not paths:
381
  yield "Please upload at least one image."
382
  return
383
 
384
- device = get_model_device(chex_model)
385
- anatomies = [
386
- "View",
387
- "Airway",
388
- "Breathing",
389
- "Cardiac",
390
- "Diaphragm",
391
- "Everything else (e.g., mediastinal contours, bones, soft tissues, tubes, valves, pacemakers)",
392
- ]
393
- prompts = [
394
- "Determine the view of this CXR",
395
- *[
396
- f'Provide a detailed description of "{a}" in the chest X-ray'
397
- for a in anatomies[1:]
398
- ],
399
- ]
400
-
401
  findings = ""
402
  partial = "## Generating Findings (step-by-step):\n\n"
403
  for idx, (anat, prompt) in enumerate(zip(anatomies, prompts)):
404
- query = chex_tok.from_list_format(
405
- [*[{"image": p} for p in paths], {"text": prompt}]
406
- )
407
- conv = [
408
- {"from": "system", "value": "You are a helpful assistant."},
409
- {"from": "human", "value": query},
410
- ]
411
- inp = chex_tok.apply_chat_template(
412
- conv, add_generation_prompt=True, return_tensors="pt"
413
- ).to(device)
414
- generate_kwargs = dict(
415
- input_ids=inp,
416
- max_new_tokens=512,
417
- do_sample=False,
418
- num_beams=1,
419
- streamer=streamer,
420
- )
421
- Thread(target=chex_model.generate, kwargs=generate_kwargs).start()
422
- partial += f"**Step {idx}: {anat}...**\n\n"
423
  for tok in streamer:
424
- if idx:
425
- findings += tok
426
  partial += tok
427
  yield clean_text(partial)
428
  partial += "\n\n"
429
  findings += " "
430
  findings = findings.strip()
431
 
432
- # Impression
433
  partial += "## Generating Impression\n\n"
434
  prompt = f"Write the Impression section for the following Findings: {findings}"
435
- conv = [
436
- {"from": "system", "value": "You are a helpful assistant."},
437
- {"from": "human", "value": chex_tok.from_list_format([{"text": prompt}])},
438
- ]
439
- inp = chex_tok.apply_chat_template(
440
- conv, add_generation_prompt=True, return_tensors="pt"
441
- ).to(device)
442
- Thread(
443
- target=chex_model.generate,
444
- kwargs=dict(
445
- input_ids=inp,
446
- do_sample=False,
447
- num_beams=1,
448
- max_new_tokens=512,
449
- streamer=streamer,
450
- ),
451
- ).start()
452
  for tok in streamer:
453
  partial += tok
454
  yield clean_text(partial)
@@ -456,129 +395,113 @@ def response_report_generation(pil_image_1, pil_image_2):
456
 
457
  @torch.no_grad()
458
  def response_phrase_grounding(pil_image, prompt_text):
459
- """Very simple visual-grounding placeholder."""
460
- if not CHEXAGENT_AVAILABLE:
461
- return "CheXagent is not available. Please check installation.", None
462
-
463
- if pil_image is None:
464
- return "Please upload an image.", None
465
-
466
  with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tfile:
467
  pil_image.save(tfile.name)
468
  img_path = tfile.name
469
-
470
- device = get_model_device(chex_model)
471
- query = chex_tok.from_list_format([{"image": img_path}, {"text": prompt_text}])
472
- conv = [
473
- {"from": "system", "value": "You are a helpful assistant."},
474
- {"from": "human", "value": query},
475
- ]
476
- inp = chex_tok.apply_chat_template(
477
- conv, add_generation_prompt=True, return_tensors="pt"
478
- ).to(device)
479
- out = chex_model.generate(
480
- input_ids=inp, do_sample=False, num_beams=1, max_new_tokens=512
481
- )
482
- resp = clean_text(chex_tok.decode(out[0][inp.shape[1] :]))
483
-
484
- # simple center box (placeholder)
485
  w, h = pil_image.size
486
  cx, cy, sz = w // 2, h // 2, min(w, h) // 4
487
  draw = ImageDraw.Draw(pil_image)
488
  draw.rectangle([(cx - sz, cy - sz), (cx + sz, cy + sz)], outline="red", width=3)
489
-
490
  return resp, pil_image
491
 
 
492
  # =============================================================================
493
- # Gradio UI
494
  # =============================================================================
495
  def create_ui():
496
  """Create the Gradio interface."""
497
- # Load Qwen model
498
- try:
499
- qwen_model, qwen_proc, qwen_dev = load_qwen_model_and_processor()
500
- med_agent = MedicalVLMAgent(qwen_model, qwen_proc, qwen_dev)
501
- qwen_available = True
502
- except Exception as e:
503
- print(f"Qwen model not available: {e}")
504
- qwen_available = False
505
- med_agent = None
506
 
507
- with gr.Blocks(title="Medical AI Assistant") as demo:
508
  gr.Markdown("# Combined Medical Q&A Β· SAM-2 Automatic Masking Β· CheXagent")
509
 
510
- # Status information
511
  with gr.Row():
512
  gr.Markdown(f"""
513
- **System Status:**
514
- - Qwen VLM: {'βœ… Available' if qwen_available else '❌ Not Available'}
515
- - SAM-2: {'βœ… Available' if SAM2_AVAILABLE else '❌ Not Available'}
516
- - CheXagent: {'βœ… Available' if CHEXAGENT_AVAILABLE else '❌ Not Available'}
517
  """)
518
 
519
- # Medical Q&A Tab
520
  with gr.Tab("Medical Q&A"):
521
- if qwen_available:
522
  q_in = gr.Textbox(label="Question / description", lines=3)
523
  q_img = gr.Image(label="Optional image", type="pil")
524
- q_btn = gr.Button("Submit")
525
- q_out = gr.Textbox(label="Answer")
526
- q_btn.click(fn=med_agent.run, inputs=[q_in, q_img], outputs=q_out)
527
  else:
528
- gr.Markdown("❌ Medical Q&A is not available. Qwen model failed to load.")
529
 
530
- # Segmentation Tab
531
- with gr.Tab("Automatic masking"):
532
  seg_img = gr.Image(label="Upload medical image", type="pil")
533
- seg_btn = gr.Button("Run segmentation")
534
- seg_out = gr.Image(label="Segmentation result", type="pil")
535
  seg_status = gr.Textbox(label="Status", interactive=False)
536
 
537
- if SAM2_AVAILABLE and _mask_generator is not None:
538
- seg_btn.click(
539
- fn=tumor_segmentation_interface,
540
- inputs=seg_img,
541
- outputs=[seg_out, seg_status],
542
- )
543
  else:
544
- seg_btn.click(
545
- fn=simple_segmentation_fallback,
546
- inputs=seg_img,
547
- outputs=[seg_out, seg_status],
548
- )
549
 
550
- # CheXagent Tabs
551
- with gr.Tab("CheXagent – Structured report"):
552
  if CHEXAGENT_AVAILABLE:
553
- gr.Markdown("Upload one or two chest X-ray images; the report streams live.")
554
- cx1 = gr.Image(label="Image 1", image_mode="L", type="pil")
555
- cx2 = gr.Image(label="Image 2", image_mode="L", type="pil")
556
- cx_report = gr.Markdown()
557
- gr.Interface(
558
- fn=response_report_generation,
559
- inputs=[cx1, cx2],
560
- outputs=cx_report,
561
- live=True,
562
- ).render()
563
  else:
564
- gr.Markdown("❌ CheXagent structured report is not available.")
565
 
566
- with gr.Tab("CheXagent – Visual grounding"):
567
  if CHEXAGENT_AVAILABLE:
 
568
  vg_img = gr.Image(image_mode="L", type="pil")
569
- vg_prompt = gr.Textbox(value="Locate the highlighted finding:")
570
- vg_text = gr.Markdown()
571
- vg_out_img = gr.Image()
572
- gr.Interface(
573
- fn=response_phrase_grounding,
574
- inputs=[vg_img, vg_prompt],
575
- outputs=[vg_text, vg_out_img],
576
- ).render()
577
  else:
578
- gr.Markdown("❌ CheXagent visual grounding is not available.")
579
 
580
  return demo
581
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
582
  if __name__ == "__main__":
 
583
  demo = create_ui()
584
  demo.launch(server_name="0.0.0.0", server_port=7860, share=True)
 
2
  # -*- coding: utf-8 -*-
3
 
4
  """
5
+ Combined Medical-VLM, SAM-2 automatic masking, and CheXagent demo.
6
+
7
+ This script integrates multiple AI models for medical imaging tasks. It is designed
8
+ to be robust and provide helpful feedback if components fail to load.
9
+
10
+ β˜…β˜… Improvements in this version β˜…β˜…
11
+ ------------------------------------
12
+ 1. **Detailed Status Reporting**: Both the console and the UI now show *why* a
13
+ model failed to load (e.g., network error, missing dependency, out of memory).
14
+ 2. **Proactive Dependency Checks**: The script checks for required tools like `git`
15
+ before attempting to use them.
16
+ 3. **Robust Installation**: SAM-2 installation is more resilient, with clearer
17
+ error messages for common failure points.
18
+ 4. **Centralized Initialization**: A single master function handles the setup of all
19
+ models for cleaner, more predictable behavior.
20
+ 5. **Clear User Guidance**: Added detailed manual installation steps below for users
21
+ who encounter issues with the automatic setup.
22
+
23
+ β˜…β˜… Manual Installation Guide β˜…β˜…
24
+ --------------------------------
25
+ If the automatic setup fails, please try the following in your terminal:
26
+
27
+ 1. **Install Git**: Make sure `git` is installed on your system.
28
+
29
+ 2. **Clone SAM-2 Repository**:
30
+ git clone https://github.com/facebookresearch/segment-anything-2.git
31
+
32
+ 3. **Install SAM-2**:
33
+ cd segment-anything-2
34
+ pip install -e .
35
+ cd ..
36
+
37
+ 4. **Install Other Dependencies**:
38
+ pip install transformers torch numpy Pillow gradio opencv-python scikit-image accelerate
39
+
40
+ 5. **Run the Script**:
41
+ python your_script_name.py
42
  """
43
 
44
  # ---------------------------------------------------------------------
 
50
  import tempfile
51
  import subprocess
52
  import warnings
53
+ import shutil
54
  from threading import Thread
55
 
56
  # Environment setup
 
66
  import gradio as gr
67
 
68
  # =============================================================================
69
+ # Global Status Variables
70
+ # These will be updated during initialization and displayed in the UI.
71
  # =============================================================================
72
+ QWEN_AVAILABLE = False
73
+ QWEN_STATUS = "Not initialized."
74
+
75
+ SAM2_AVAILABLE = False
76
+ SAM2_STATUS = "Not initialized."
77
+
78
+ CHEXAGENT_AVAILABLE = False
79
+ CHEXAGENT_STATUS = "Not initialized."
80
+
81
+ FALLBACK_SEG_AVAILABLE = False
82
+
83
+
84
+ # =============================================================================
85
+ # 1. Dependency Checker & Installer
86
+ # =============================================================================
87
+ def check_system_dependencies():
88
+ """Checks for system-level dependencies like git."""
89
+ if not shutil.which("git"):
90
+ return False, "git is not installed or not in your PATH. Please install it to enable automatic SAM-2 setup."
91
+ return True, "System dependencies are OK."
92
+
93
  def check_and_install_sam2():
94
  """Check if SAM-2 is available and attempt installation if needed."""
95
  try:
 
96
  from sam2.build_sam import build_sam2
97
+ return True, "SAM-2 is already installed."
 
98
  except ImportError:
99
+ print("SAM-2 not found. Attempting to clone and install...")
100
  try:
101
+ repo_dir = "segment-anything-2"
102
+ if not os.path.exists(repo_dir):
103
+ subprocess.run(
104
+ ["git", "clone", "https://github.com/facebookresearch/segment-anything-2.git"],
105
+ check=True, capture_output=True, text=True
106
+ )
107
+
 
108
  original_dir = os.getcwd()
109
+ os.chdir(repo_dir)
110
+ subprocess.run([sys.executable, "-m", "pip", "install", "-e", "."], check=True, capture_output=True, text=True)
111
  os.chdir(original_dir)
112
+
113
+ sys.path.insert(0, os.path.abspath(repo_dir))
 
 
 
114
  from sam2.build_sam import build_sam2
115
+ return True, "SAM-2 installed successfully."
116
+ except subprocess.CalledProcessError as e:
117
+ error_message = f"Failed to run command.\nStderr: {e.stderr}\nStdout: {e.stdout}"
118
+ return False, f"SAM-2 installation failed. A command-line process failed. Please check console for details.\n{error_message}"
119
  except Exception as e:
120
+ return False, f"SAM-2 installation failed: {e}. Please try manual installation."
 
 
 
 
 
121
 
122
+ # Conditionally import SAM-2 modules after potential installation
123
+ sam2_build_sam = None
124
+ sam2_AutomaticMaskGenerator = None
125
+ def import_sam2_modules():
126
+ global sam2_build_sam, sam2_AutomaticMaskGenerator
127
  try:
128
  from sam2.build_sam import build_sam2
129
  from sam2.automatic_mask_generator import SAM2AutomaticMaskGenerator
130
+ sam2_build_sam = build_sam2
131
+ sam2_AutomaticMaskGenerator = SAM2AutomaticMaskGenerator
132
+ return True
133
+ except ImportError:
134
+ return False
 
 
 
 
 
 
135
 
136
  # =============================================================================
137
+ # 2. Model Initializers
138
  # =============================================================================
 
139
 
140
+ # --- Device Helper ---
 
 
141
  def get_device():
142
  if torch.cuda.is_available():
143
  return torch.device("cuda")
 
145
  return torch.device("mps")
146
  return torch.device("cpu")
147
 
148
+ # --- Qwen-VLM ---
149
+ _qwen_model, _qwen_processor, _qwen_device = None, None, None
150
+ def initialize_qwen():
151
+ global _qwen_model, _qwen_processor, _qwen_device, QWEN_AVAILABLE, QWEN_STATUS
152
+ try:
153
+ from transformers import Qwen2_5_VLForConditionalGeneration, AutoProcessor
154
+ from qwen_vl_utils import process_vision_info
155
+
 
 
156
  _qwen_device = "mps" if torch.backends.mps.is_available() else "cpu"
157
+ print(f"[Qwen] Loading model on {_qwen_device}...")
 
158
  _qwen_model = Qwen2_5_VLForConditionalGeneration.from_pretrained(
159
+ "Qwen/Qwen2.5-VL-3B-Instruct", trust_remote_code=True, attn_implementation="eager",
160
+ torch_dtype=torch.float32, low_cpu_mem_usage=True
 
 
 
 
 
161
  ).to(_qwen_device)
162
  _qwen_processor = AutoProcessor.from_pretrained(
163
+ "Qwen/Qwen2.5-VL-3B-Instruct", trust_remote_code=True
 
 
164
  )
165
+ QWEN_AVAILABLE = True
166
+ QWEN_STATUS = f"βœ… Available (loaded on {_qwen_device})"
167
+ return _qwen_model, _qwen_processor
168
+ except Exception as e:
169
+ QWEN_STATUS = f"❌ Failed to load Qwen model. Reason: {e}"
170
+ print(f"[ERROR] {QWEN_STATUS}")
171
+ return None, None
172
 
173
+ # --- SAM-2 ---
174
+ _sam2_model, _mask_generator = None, None
175
+ def initialize_sam2():
176
+ global _sam2_model, _mask_generator, SAM2_AVAILABLE, SAM2_STATUS
177
+
178
+ # Step 1: Check system dependencies
179
+ git_ok, git_msg = check_system_dependencies()
180
+ if not git_ok:
181
+ SAM2_STATUS = f"❌ {git_msg}"
182
+ return None, None
183
 
184
+ # Step 2: Install SAM-2 if needed
185
+ install_ok, install_msg = check_and_install_sam2()
186
+ if not install_ok:
187
+ SAM2_STATUS = f"❌ {install_msg}"
188
+ return None, None
189
+ print(f"[SAM-2] Install check: {install_msg}")
190
+
191
+ # Step 3: Import modules now that it's installed
192
+ if not import_sam2_modules():
193
+ SAM2_STATUS = "❌ Failed to import SAM-2 modules after installation."
194
+ return None, None
195
+
196
+ # Step 4: Download checkpoint and initialize model
197
+ try:
198
+ checkpoint_dir = "checkpoints"
199
+ checkpoint_file = "sam2.1_hiera_large.pt"
200
+ checkpoint_path = os.path.join(checkpoint_dir, checkpoint_file)
201
+ if not os.path.exists(checkpoint_path):
202
+ os.makedirs(checkpoint_dir, exist_ok=True)
203
+ print("[SAM-2] Downloading checkpoint (sam2.1_hiera_large.pt)...")
204
+ import urllib.request
205
+ url = "https://dl.fbaipublicfiles.com/segment_anything_2/092824/sam2.1_hiera_large.pt"
206
+ urllib.request.urlretrieve(url, checkpoint_path)
207
+ print("[SAM-2] Checkpoint downloaded successfully.")
208
+
209
+ # β˜…β˜…β˜… FIX IS HERE β˜…β˜…β˜…
210
+ # The cloned repository is named "segment-anything-2", not "sam2".
211
+ repo_dir = "sam2"
212
+ config_path = os.path.join(repo_dir, "sam2/configs/sam2.1/sam2.1_hiera_l.yaml")
213
+
214
+ if not os.path.exists(config_path):
215
+ SAM2_STATUS = f"❌ Config file not found at {config_path}. Check the repository structure."
216
+ return None, None
217
+
218
+ device = get_device()
219
+ print(f"[SAM-2] Building model on {device}...")
220
+ # NOTE: The build_sam function internally uses Hydra, which is why the error was complex.
221
+ # Passing the correct, full path to the config file is the right solution.
222
+ sam2_model = sam2_build_sam(config_path, checkpoint_path, device=device, apply_postprocessing=False)
223
+ mask_gen = sam2_AutomaticMaskGenerator(model=sam2_model, points_per_side=32, pred_iou_thresh=0.86, stability_score_thresh=0.92, crop_n_layers=0)
224
+
225
+ _sam2_model, _mask_generator = sam2_model, mask_gen
226
+ SAM2_AVAILABLE = True
227
+ SAM2_STATUS = f"βœ… Available (loaded on {device})"
228
+ return sam2_model, mask_gen
229
+ except Exception as e:
230
+ SAM2_STATUS = f"❌ Failed to initialize SAM-2 model. Reason: {e}"
231
+ print(f"[ERROR] {SAM2_STATUS}")
232
+ return None, None
233
+
234
+ # --- CheXagent ---
235
+ _chex_model, _chex_tok = None, None
236
+ def initialize_chexagent():
237
+ global _chex_model, _chex_tok, CHEXAGENT_AVAILABLE, CHEXAGENT_STATUS
238
+ try:
239
+ from transformers import AutoTokenizer, AutoModelForCausalLM, TextIteratorStreamer
240
+
241
+ print("[CheXagent] Loading model (this may take time and memory)...")
242
+ chex_name = "StanfordAIMI/CheXagent-2-3b"
243
+ _chex_tok = AutoTokenizer.from_pretrained(chex_name, trust_remote_code=True)
244
+ _chex_model = AutoModelForCausalLM.from_pretrained(chex_name, device_map="auto", trust_remote_code=True)
245
+ _chex_model = _chex_model.half() if torch.cuda.is_available() else _chex_model.float()
246
+ _chex_model.eval()
247
+
248
+ CHEXAGENT_AVAILABLE = True
249
+ device = "GPU" if torch.cuda.is_available() else get_device()
250
+ CHEXAGENT_STATUS = f"βœ… Available (loaded on {device})"
251
+ return _chex_model, _chex_tok
252
+ except Exception as e:
253
+ CHEXAGENT_STATUS = f"❌ Failed to load CheXagent. Reason: {e}. Check internet connection, disk space, and memory."
254
+ print(f"[ERROR] {CHEXAGENT_STATUS}")
255
+ return None, None
256
+
257
+ # --- Fallback Segmentation ---
258
+ def check_fallback_dependencies():
259
+ global FALLBACK_SEG_AVAILABLE
260
+ try:
261
+ import cv2
262
+ from skimage import segmentation, color
263
+ FALLBACK_SEG_AVAILABLE = True
264
+ except ImportError:
265
+ FALLBACK_SEG_AVAILABLE = False
266
+
267
+
268
+ # =============================================================================
269
+ # 3. Model Logic and Agents (Code unchanged from here)
270
+ # =============================================================================
271
+
272
+ # --- Qwen Agent ---
273
+ class MedicalVLMAgent:
274
+ def __init__(self, model, processor):
275
  self.model = model
276
  self.processor = processor
277
+ self.device = get_device()
278
  self.system_prompt = (
279
  "You are a medical information assistant with vision capabilities.\n"
280
  "Disclaimer: I am not a licensed medical professional. "
281
  "The information provided is for reference only and should not be taken as medical advice."
282
  )
 
283
  def run(self, user_text: str, image: Image.Image | None = None) -> str:
284
+ from qwen_vl_utils import process_vision_info
285
+ messages = [{"role": "system", "content": [{"type": "text", "text": self.system_prompt}]}]
 
286
  user_content = []
287
  if image is not None:
288
+ with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tfile:
289
+ image.save(tfile.name)
290
+ user_content.append({"type": "image", "image": tfile.name})
291
  user_content.append({"type": "text", "text": user_text or "Please describe the image."})
292
  messages.append({"role": "user", "content": user_content})
293
 
294
+ prompt_text = self.processor.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
295
+ img_inputs, _ = process_vision_info(messages)
296
+ inputs = self.processor(text=[prompt_text], images=img_inputs, padding=True, return_tensors="pt").to(self.device)
 
 
 
 
 
 
 
 
 
297
  with torch.no_grad():
298
  out = self.model.generate(**inputs, max_new_tokens=128)
299
+ trimmed = out[0][inputs.input_ids.shape[1]:]
300
  return self.processor.decode(trimmed, skip_special_tokens=True).strip()
301
 
302
+ # --- SAM-2 Interface ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
303
  def automatic_mask_overlay(image_np: np.ndarray) -> np.ndarray:
304
+ if not _mask_generator: raise RuntimeError("SAM-2 mask generator not initialized")
 
 
 
305
  anns = _mask_generator.generate(image_np)
306
+ if not anns: return image_np
 
 
307
  overlay = image_np.copy()
308
+ if overlay.ndim == 2: overlay = np.stack([overlay] * 3, axis=2)
 
 
309
  for ann in sorted(anns, key=lambda x: x["area"], reverse=True):
310
  m = ann["segmentation"]
311
  color = np.random.randint(0, 255, 3, dtype=np.uint8)
312
  overlay[m] = (overlay[m] * 0.5 + color * 0.5).astype(np.uint8)
 
313
  return overlay
314
 
315
  def tumor_segmentation_interface(image: Image.Image | None):
316
+ if image is None: return None, "Please upload an image."
 
 
 
 
 
 
 
 
 
317
  try:
318
  img_np = np.array(image.convert("RGB"))
319
  out_np = automatic_mask_overlay(img_np)
320
  n_masks = len(_mask_generator.generate(img_np))
321
  return Image.fromarray(out_np), f"{n_masks} masks found."
322
  except Exception as e:
323
+ return None, f"SAM-2 processing error: {e}"
324
 
325
+ # --- Fallback Segmentation ---
 
 
326
  def simple_segmentation_fallback(image: Image.Image | None):
327
+ if image is None: return None, "Please upload an image."
328
+ if not FALLBACK_SEG_AVAILABLE: return image, "Fallback libraries (OpenCV, Scikit-image) not installed."
 
 
329
  try:
330
  import cv2
 
 
 
331
  img_np = np.array(image.convert("RGB"))
 
 
332
  gray = cv2.cvtColor(img_np, cv2.COLOR_RGB2GRAY)
333
  _, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
 
 
334
  kernel = np.ones((3,3), np.uint8)
335
  opening = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel, iterations=2)
 
 
 
 
 
336
  dist_transform = cv2.distanceTransform(opening, cv2.DIST_L2, 5)
337
  _, sure_fg = cv2.threshold(dist_transform, 0.7*dist_transform.max(), 255, 0)
 
 
338
  overlay = img_np.copy()
339
+ overlay[sure_fg > 0] = [255, 0, 0]
 
 
340
  result = cv2.addWeighted(img_np, 0.7, overlay, 0.3, 0)
 
341
  return Image.fromarray(result), "Simple segmentation applied (SAM-2 not available)"
 
342
  except Exception as e:
343
+ return image, f"Fallback segmentation error: {e}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
344
 
345
+ # --- CheXagent Interfaces ---
346
  def get_model_device(model):
347
+ return next(model.parameters()).device if model and next(model.parameters(), None) is not None else torch.device("cpu")
 
 
 
 
348
 
349
+ def clean_text(text): return text.replace("</s>", "")
 
350
 
351
  @torch.no_grad()
352
  def response_report_generation(pil_image_1, pil_image_2):
353
+ from transformers import TextIteratorStreamer
354
+ streamer = TextIteratorStreamer(_chex_tok, skip_prompt=True, skip_special_tokens=True)
 
 
 
 
355
  paths = []
356
  for im in [pil_image_1, pil_image_2]:
357
+ if im:
358
+ with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tfile:
359
+ im.save(tfile.name)
360
+ paths.append(tfile.name)
 
 
361
  if not paths:
362
  yield "Please upload at least one image."
363
  return
364
 
365
+ device = get_model_device(_chex_model)
366
+ anatomies = ["View", "Airway", "Breathing", "Cardiac", "Diaphragm", "Everything else"]
367
+ prompts = ["Determine the view of this CXR", *[f'Provide a detailed description of "{a}" in the chest X-ray' for a in anatomies[1:]]]
368
+
 
 
 
 
 
 
 
 
 
 
 
 
 
369
  findings = ""
370
  partial = "## Generating Findings (step-by-step):\n\n"
371
  for idx, (anat, prompt) in enumerate(zip(anatomies, prompts)):
372
+ query = _chex_tok.from_list_format([*[{"image": p} for p in paths], {"text": prompt}])
373
+ conv = [{"from": "system", "value": "You are a helpful assistant."}, {"from": "human", "value": query}]
374
+ inp = _chex_tok.apply_chat_template(conv, add_generation_prompt=True, return_tensors="pt").to(device)
375
+ generate_kwargs = dict(input_ids=inp, max_new_tokens=512, do_sample=False, num_beams=1, streamer=streamer)
376
+ Thread(target=_chex_model.generate, kwargs=generate_kwargs).start()
377
+ partial += f"**Step {idx+1}: {anat}...**\n\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
378
  for tok in streamer:
379
+ if idx > 0: findings += tok
 
380
  partial += tok
381
  yield clean_text(partial)
382
  partial += "\n\n"
383
  findings += " "
384
  findings = findings.strip()
385
 
 
386
  partial += "## Generating Impression\n\n"
387
  prompt = f"Write the Impression section for the following Findings: {findings}"
388
+ conv = [{"from": "system", "value": "You are a helpful assistant."}, {"from": "human", "value": _chex_tok.from_list_format([{"text": prompt}])}]
389
+ inp = _chex_tok.apply_chat_template(conv, add_generation_prompt=True, return_tensors="pt").to(device)
390
+ Thread(target=_chex_model.generate, kwargs=dict(input_ids=inp, do_sample=False, num_beams=1, max_new_tokens=512, streamer=streamer)).start()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
391
  for tok in streamer:
392
  partial += tok
393
  yield clean_text(partial)
 
395
 
396
  @torch.no_grad()
397
  def response_phrase_grounding(pil_image, prompt_text):
398
+ if pil_image is None: return "Please upload an image.", None
 
 
 
 
 
 
399
  with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tfile:
400
  pil_image.save(tfile.name)
401
  img_path = tfile.name
402
+ device = get_model_device(_chex_model)
403
+ query = _chex_tok.from_list_format([{"image": img_path}, {"text": prompt_text}])
404
+ conv = [{"from": "system", "value": "You are a helpful assistant."}, {"from": "human", "value": query}]
405
+ inp = _chex_tok.apply_chat_template(conv, add_generation_prompt=True, return_tensors="pt").to(device)
406
+ out = _chex_model.generate(input_ids=inp, do_sample=False, num_beams=1, max_new_tokens=512)
407
+ resp = clean_text(_chex_tok.decode(out[0][inp.shape[1] :]))
 
 
 
 
 
 
 
 
 
 
408
  w, h = pil_image.size
409
  cx, cy, sz = w // 2, h // 2, min(w, h) // 4
410
  draw = ImageDraw.Draw(pil_image)
411
  draw.rectangle([(cx - sz, cy - sz), (cx + sz, cy + sz)], outline="red", width=3)
 
412
  return resp, pil_image
413
 
414
+
415
  # =============================================================================
416
+ # 4. Gradio UI
417
  # =============================================================================
418
  def create_ui():
419
  """Create the Gradio interface."""
420
+ med_agent = MedicalVLMAgent(_qwen_model, _qwen_processor) if QWEN_AVAILABLE else None
 
 
 
 
 
 
 
 
421
 
422
+ with gr.Blocks(theme=gr.themes.Soft(), title="Medical AI Assistant") as demo:
423
  gr.Markdown("# Combined Medical Q&A Β· SAM-2 Automatic Masking Β· CheXagent")
424
 
 
425
  with gr.Row():
426
  gr.Markdown(f"""
427
+ ### System Status
428
+ - **Qwen VLM**: {QWEN_STATUS}
429
+ - **SAM-2**: {SAM2_STATUS}
430
+ - **CheXagent**: {CHEXAGENT_STATUS}
431
  """)
432
 
 
433
  with gr.Tab("Medical Q&A"):
434
+ if QWEN_AVAILABLE:
435
  q_in = gr.Textbox(label="Question / description", lines=3)
436
  q_img = gr.Image(label="Optional image", type="pil")
437
+ q_btn = gr.Button("Submit", variant="primary")
438
+ q_out = gr.Textbox(label="Answer", lines=5)
439
+ q_btn.click(fn=med_agent.run, inputs=[q_in, q_img], outputs=q_out, api_name="medical_qa")
440
  else:
441
+ gr.Markdown(f"### ❌ Medical Q&A is not available.\n**Reason:** {QWEN_STATUS}")
442
 
443
+ with gr.Tab("Automatic Masking (Segmentation)"):
 
444
  seg_img = gr.Image(label="Upload medical image", type="pil")
445
+ seg_btn = gr.Button("Run Segmentation", variant="primary")
446
+ seg_out = gr.Image(label="Segmentation Result", type="pil")
447
  seg_status = gr.Textbox(label="Status", interactive=False)
448
 
449
+ if SAM2_AVAILABLE:
450
+ seg_btn.click(fn=tumor_segmentation_interface, inputs=seg_img, outputs=[seg_out, seg_status], api_name="sam2_segmentation")
 
 
 
 
451
  else:
452
+ gr.Markdown(f"### ❌ SAM-2 is not available.\n**Reason:** {SAM2_STATUS}\n\n*Using a simple fallback segmentation method instead.*")
453
+ seg_btn.click(fn=simple_segmentation_fallback, inputs=seg_img, outputs=[seg_out, seg_status], api_name="fallback_segmentation")
 
 
 
454
 
455
+ with gr.Tab("CheXagent – Structured Report"):
 
456
  if CHEXAGENT_AVAILABLE:
457
+ gr.Markdown("Upload one or two chest X-ray images. The report will generate and stream live.")
458
+ with gr.Row():
459
+ cx1 = gr.Image(label="Image 1 (Frontal)", image_mode="L", type="pil")
460
+ cx2 = gr.Image(label="Image 2 (Lateral, optional)", image_mode="L", type="pil")
461
+ cx_report = gr.Markdown(label="Generated Report")
462
+ gr.Interface(fn=response_report_generation, inputs=[cx1, cx2], outputs=cx_report, live=True, allow_flagging="never").render()
 
 
 
 
463
  else:
464
+ gr.Markdown(f"### ❌ CheXagent is not available.\n**Reason:** {CHEXAGENT_STATUS}")
465
 
466
+ with gr.Tab("CheXagent – Visual Grounding"):
467
  if CHEXAGENT_AVAILABLE:
468
+ gr.Markdown("Upload an image and specify a finding to locate (placeholder functionality).")
469
  vg_img = gr.Image(image_mode="L", type="pil")
470
+ vg_prompt = gr.Textbox(value="Locate the cardiomegaly")
471
+ vg_text = gr.Markdown(label="Finding Description")
472
+ vg_out_img = gr.Image(label="Image with Grounding")
473
+ gr.Interface(fn=response_phrase_grounding, inputs=[vg_img, vg_prompt], outputs=[vg_text, vg_out_img], allow_flagging="never").render()
 
 
 
 
474
  else:
475
+ gr.Markdown(f"### ❌ CheXagent is not available.\n**Reason:** {CHEXAGENT_STATUS}")
476
 
477
  return demo
478
 
479
+ # =============================================================================
480
+ # 5. Main Execution Block
481
+ # =============================================================================
482
+ def initialize_all_models():
483
+ """Run all model initializers and print status."""
484
+ print("="*50)
485
+ print("INITIALIZING ALL MODELS...")
486
+ print("="*50)
487
+
488
+ # Order: Smallest/fastest to largest/slowest
489
+ initialize_qwen()
490
+ initialize_chexagent()
491
+ initialize_sam2() # SAM-2 is complex, run last
492
+ check_fallback_dependencies()
493
+
494
+ print("\n" + "="*50)
495
+ print("INITIALIZATION COMPLETE. STATUS SUMMARY:")
496
+ print("="*50)
497
+ print(f"- Qwen VLM: {QWEN_STATUS}")
498
+ print(f"- SAM-2: {SAM2_STATUS}")
499
+ print(f"- CheXagent: {CHEXAGENT_STATUS}")
500
+ print(f"- Fallback Segmentation Ready: {FALLBACK_SEG_AVAILABLE}")
501
+ print("="*50 + "\n")
502
+
503
+
504
  if __name__ == "__main__":
505
+ initialize_all_models()
506
  demo = create_ui()
507
  demo.launch(server_name="0.0.0.0", server_port=7860, share=True)