nexusbert commited on
Commit
4f9e3d3
·
1 Parent(s): 1063f74

Restore all files to state before comment removal - fix broken code

Browse files
mediSync/__init__.py CHANGED
@@ -1 +1,17 @@
1
- __version__ = "0.1.0"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ U-HRES: Unified Health Record Exchange System AI Layer
3
+ =======================================================
4
+
5
+ A healthcare solution that combines X-ray image analysis with patient report text processing
6
+ to provide comprehensive medical insights.
7
+
8
+ This package contains the following modules:
9
+ - models: Image and text analysis models, along with multimodal fusion
10
+ - utils: Utility functions for preprocessing and visualization
11
+ - app: Main application with Gradio interface
12
+
13
+ Author: AI Development Team
14
+ License: MIT
15
+ """
16
+
17
+ __version__ = "0.1.0"
mediSync/app.py CHANGED
@@ -8,11 +8,11 @@ import gradio as gr
8
  import matplotlib.pyplot as plt
9
  from PIL import Image
10
 
11
-
12
  parent_dir = os.path.dirname(os.path.abspath(__file__))
13
  sys.path.append(parent_dir)
14
 
15
-
16
  from models.multimodal_fusion import MultimodalFusion
17
  from utils.preprocessing import enhance_xray_image, normalize_report_text
18
  from utils.visualization import (
@@ -21,7 +21,7 @@ from utils.visualization import (
21
  plot_report_entities,
22
  )
23
 
24
-
25
  logging.basicConfig(
26
  level=logging.INFO,
27
  format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
@@ -29,7 +29,7 @@ logging.basicConfig(
29
  )
30
  logger = logging.getLogger(__name__)
31
 
32
-
33
  os.makedirs(os.path.join(parent_dir, "data", "sample"), exist_ok=True)
34
 
35
 
@@ -43,7 +43,7 @@ class UHRESApp:
43
  self.logger = logging.getLogger(__name__)
44
  self.logger.info("Initializing U-HRES application")
45
 
46
-
47
  self.fusion_model = None
48
  self.image_model = None
49
  self.text_model = None
@@ -79,38 +79,38 @@ class UHRESApp:
79
  tuple: (image, image_results_html, plot_as_html)
80
  """
81
  try:
82
-
83
  if not self.load_models() or self.image_model is None:
84
  return image, "Error: Models not loaded properly.", None
85
 
86
-
87
  temp_dir = tempfile.mkdtemp()
88
  temp_path = os.path.join(temp_dir, "upload.png")
89
 
90
  if isinstance(image, str):
91
-
92
  from shutil import copyfile
93
 
94
  copyfile(image, temp_path)
95
  else:
96
-
97
  image.save(temp_path)
98
 
99
-
100
  self.logger.info(f"Analyzing image: {temp_path}")
101
  results = self.image_model.analyze(temp_path)
102
 
103
-
104
  fig = plot_image_prediction(
105
  image,
106
  results.get("predictions", []),
107
  f"Primary Finding: {results.get('primary_finding', 'Unknown')}",
108
  )
109
 
110
-
111
  plot_html = self.fig_to_html(fig)
112
 
113
-
114
  html_result = f"""
115
  <h2>X-ray Analysis Results</h2>
116
  <p><strong>Primary Finding:</strong> {results.get("primary_finding", "Unknown")}</p>
@@ -121,13 +121,13 @@ class UHRESApp:
121
  <ul>
122
  """
123
 
124
-
125
  for label, prob in results.get("predictions", [])[:5]:
126
  html_result += f"<li>{label}: {prob:.1%}</li>"
127
 
128
  html_result += "</ul>"
129
 
130
-
131
  explanation = self.image_model.get_explanation(results)
132
  html_result += f"<h3>Analysis Explanation:</h3><p>{explanation}</p>"
133
 
@@ -148,11 +148,11 @@ class UHRESApp:
148
  tuple: (text, text_results_html, entities_plot_html)
149
  """
150
  try:
151
-
152
  if not self.load_models() or self.text_model is None:
153
  return text, "Error: Models not loaded properly.", None
154
 
155
-
156
  if not text or len(text.strip()) < 10:
157
  return (
158
  text,
@@ -160,21 +160,21 @@ class UHRESApp:
160
  None,
161
  )
162
 
163
-
164
  normalized_text = normalize_report_text(text)
165
 
166
-
167
  self.logger.info("Analyzing medical report text")
168
  results = self.text_model.analyze(normalized_text)
169
 
170
-
171
  entities = results.get("entities", {})
172
  fig = plot_report_entities(normalized_text, entities)
173
 
174
-
175
  entities_plot_html = self.fig_to_html(fig)
176
 
177
-
178
  html_result = f"""
179
  <h2>Medical Report Analysis Results</h2>
180
  <p><strong>Severity Level:</strong> {results.get("severity", {}).get("level", "Unknown")}</p>
@@ -185,7 +185,7 @@ class UHRESApp:
185
  <ul>
186
  """
187
 
188
-
189
  findings = results.get("findings", [])
190
  if findings:
191
  for finding in findings:
@@ -195,14 +195,14 @@ class UHRESApp:
195
 
196
  html_result += "</ul>"
197
 
198
-
199
  html_result += "<h3>Extracted Medical Entities:</h3>"
200
 
201
  for category, items in entities.items():
202
  if items:
203
  html_result += f"<p><strong>{category.capitalize()}:</strong> {', '.join(items)}</p>"
204
 
205
-
206
  html_result += "<h3>Follow-up Recommendations:</h3><ul>"
207
  followups = results.get("followup_recommendations", [])
208
 
@@ -232,11 +232,11 @@ class UHRESApp:
232
  tuple: (results_html, multimodal_plot_html)
233
  """
234
  try:
235
-
236
  if not self.load_models() or self.fusion_model is None:
237
  return "Error: Models not loaded properly.", None
238
 
239
-
240
  if image is None:
241
  return "Error: Please upload an X-ray image for analysis.", None
242
 
@@ -246,36 +246,36 @@ class UHRESApp:
246
  None,
247
  )
248
 
249
-
250
  temp_dir = tempfile.mkdtemp()
251
  temp_path = os.path.join(temp_dir, "upload.png")
252
 
253
  if isinstance(image, str):
254
-
255
  from shutil import copyfile
256
 
257
  copyfile(image, temp_path)
258
  else:
259
-
260
  image.save(temp_path)
261
 
262
-
263
  normalized_text = normalize_report_text(text)
264
 
265
-
266
  self.logger.info("Performing multimodal analysis")
267
  results = self.fusion_model.analyze(temp_path, normalized_text)
268
 
269
-
270
  fig = plot_multimodal_results(results, image, text)
271
 
272
-
273
  plot_html = self.fig_to_html(fig)
274
 
275
-
276
  explanation = self.fusion_model.get_explanation(results)
277
 
278
-
279
  html_result = f"""
280
  <h2>Multimodal Medical Analysis Results</h2>
281
 
@@ -289,7 +289,7 @@ class UHRESApp:
289
  <ul>
290
  """
291
 
292
-
293
  findings = results.get("findings", [])
294
  if findings:
295
  for finding in findings:
@@ -299,7 +299,7 @@ class UHRESApp:
299
 
300
  html_result += "</ul>"
301
 
302
-
303
  html_result += "<h3>Recommended Follow-up</h3><ul>"
304
  followups = results.get("followup_recommendations", [])
305
 
@@ -313,7 +313,7 @@ class UHRESApp:
313
 
314
  html_result += "</ul>"
315
 
316
-
317
  confidence = results.get("severity", {}).get("confidence", 0)
318
  html_result += f"""
319
  <p><em>Note: This analysis has a confidence level of {confidence:.0%}.
@@ -340,31 +340,31 @@ class UHRESApp:
340
  if image is None:
341
  return None
342
 
343
-
344
  temp_dir = tempfile.mkdtemp()
345
  temp_path = os.path.join(temp_dir, "upload.png")
346
 
347
  if isinstance(image, str):
348
-
349
  from shutil import copyfile
350
 
351
  copyfile(image, temp_path)
352
  else:
353
-
354
  image.save(temp_path)
355
 
356
-
357
  self.logger.info(f"Enhancing image: {temp_path}")
358
  output_path = os.path.join(temp_dir, "enhanced.png")
359
  enhance_xray_image(temp_path, output_path)
360
 
361
-
362
  enhanced = Image.open(output_path)
363
  return enhanced
364
 
365
  except Exception as e:
366
  self.logger.error(f"Error enhancing image: {e}")
367
- return image
368
 
369
  def fig_to_html(self, fig):
370
  """Convert matplotlib figure to HTML for display in Gradio."""
@@ -390,7 +390,7 @@ def create_interface():
390
 
391
  app = UHRESApp()
392
 
393
-
394
  example_report = """
395
  CHEST X-RAY EXAMINATION
396
 
@@ -409,7 +409,7 @@ def create_interface():
409
  RECOMMENDATIONS: Follow-up chest CT to further characterize the nodular opacity in the right lower lobe.
410
  """
411
 
412
-
413
  sample_images_dir = Path(parent_dir) / "data" / "sample"
414
  sample_images = list(sample_images_dir.glob("*.png")) + list(
415
  sample_images_dir.glob("*.jpg")
@@ -419,7 +419,7 @@ def create_interface():
419
  if sample_images:
420
  sample_image_path = str(sample_images[0])
421
 
422
-
423
  with gr.Blocks(
424
  title="U-HRES: Unified Health Record Exchange System AI Layer", theme=gr.themes.Soft()
425
  ) as interface:
@@ -456,7 +456,7 @@ def create_interface():
456
  multi_results = gr.HTML(label="Analysis Results")
457
  multi_plot = gr.HTML(label="Visualization")
458
 
459
-
460
  if sample_image_path:
461
  gr.Examples(
462
  examples=[[sample_image_path, example_report]],
@@ -476,7 +476,7 @@ def create_interface():
476
  img_results = gr.HTML(label="Analysis Results")
477
  img_plot = gr.HTML(label="Visualization")
478
 
479
-
480
  if sample_image_path:
481
  gr.Examples(
482
  examples=[[sample_image_path]],
@@ -500,7 +500,7 @@ def create_interface():
500
  text_results = gr.HTML(label="Analysis Results")
501
  text_plot = gr.HTML(label="Entity Visualization")
502
 
503
-
504
  gr.Examples(
505
  examples=[[example_report]],
506
  inputs=[text_input],
@@ -529,7 +529,7 @@ def create_interface():
529
  This tool is for educational and research purposes only. It is not intended to provide medical advice or replace professional healthcare. Always consult with qualified healthcare providers for medical decisions.
530
  """)
531
 
532
-
533
  multi_img_enhance.click(
534
  app.enhance_image, inputs=multi_img_input, outputs=multi_img_input
535
  )
@@ -552,7 +552,7 @@ def create_interface():
552
  outputs=[text_output, text_results, text_plot],
553
  )
554
 
555
-
556
  interface.launch(show_api=False)
557
 
558
 
 
8
  import matplotlib.pyplot as plt
9
  from PIL import Image
10
 
11
+ # Add parent directory to path
12
  parent_dir = os.path.dirname(os.path.abspath(__file__))
13
  sys.path.append(parent_dir)
14
 
15
+ # Import our modules
16
  from models.multimodal_fusion import MultimodalFusion
17
  from utils.preprocessing import enhance_xray_image, normalize_report_text
18
  from utils.visualization import (
 
21
  plot_report_entities,
22
  )
23
 
24
+ # Set up logging
25
  logging.basicConfig(
26
  level=logging.INFO,
27
  format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
 
29
  )
30
  logger = logging.getLogger(__name__)
31
 
32
+ # Create temporary directory for sample data if it doesn't exist
33
  os.makedirs(os.path.join(parent_dir, "data", "sample"), exist_ok=True)
34
 
35
 
 
43
  self.logger = logging.getLogger(__name__)
44
  self.logger.info("Initializing U-HRES application")
45
 
46
+ # Initialize models with None for lazy loading
47
  self.fusion_model = None
48
  self.image_model = None
49
  self.text_model = None
 
79
  tuple: (image, image_results_html, plot_as_html)
80
  """
81
  try:
82
+ # Ensure models are loaded
83
  if not self.load_models() or self.image_model is None:
84
  return image, "Error: Models not loaded properly.", None
85
 
86
+ # Save uploaded image to a temporary file
87
  temp_dir = tempfile.mkdtemp()
88
  temp_path = os.path.join(temp_dir, "upload.png")
89
 
90
  if isinstance(image, str):
91
+ # Copy the file if it's a path
92
  from shutil import copyfile
93
 
94
  copyfile(image, temp_path)
95
  else:
96
+ # Save if it's a Gradio UploadButton image
97
  image.save(temp_path)
98
 
99
+ # Run image analysis
100
  self.logger.info(f"Analyzing image: {temp_path}")
101
  results = self.image_model.analyze(temp_path)
102
 
103
+ # Create visualization
104
  fig = plot_image_prediction(
105
  image,
106
  results.get("predictions", []),
107
  f"Primary Finding: {results.get('primary_finding', 'Unknown')}",
108
  )
109
 
110
+ # Convert to HTML for display
111
  plot_html = self.fig_to_html(fig)
112
 
113
+ # Format results as HTML
114
  html_result = f"""
115
  <h2>X-ray Analysis Results</h2>
116
  <p><strong>Primary Finding:</strong> {results.get("primary_finding", "Unknown")}</p>
 
121
  <ul>
122
  """
123
 
124
+ # Add top 5 predictions
125
  for label, prob in results.get("predictions", [])[:5]:
126
  html_result += f"<li>{label}: {prob:.1%}</li>"
127
 
128
  html_result += "</ul>"
129
 
130
+ # Add explanation
131
  explanation = self.image_model.get_explanation(results)
132
  html_result += f"<h3>Analysis Explanation:</h3><p>{explanation}</p>"
133
 
 
148
  tuple: (text, text_results_html, entities_plot_html)
149
  """
150
  try:
151
+ # Ensure models are loaded
152
  if not self.load_models() or self.text_model is None:
153
  return text, "Error: Models not loaded properly.", None
154
 
155
+ # Check for empty text
156
  if not text or len(text.strip()) < 10:
157
  return (
158
  text,
 
160
  None,
161
  )
162
 
163
+ # Normalize text
164
  normalized_text = normalize_report_text(text)
165
 
166
+ # Run text analysis
167
  self.logger.info("Analyzing medical report text")
168
  results = self.text_model.analyze(normalized_text)
169
 
170
+ # Get entities and create visualization
171
  entities = results.get("entities", {})
172
  fig = plot_report_entities(normalized_text, entities)
173
 
174
+ # Convert to HTML for display
175
  entities_plot_html = self.fig_to_html(fig)
176
 
177
+ # Format results as HTML
178
  html_result = f"""
179
  <h2>Medical Report Analysis Results</h2>
180
  <p><strong>Severity Level:</strong> {results.get("severity", {}).get("level", "Unknown")}</p>
 
185
  <ul>
186
  """
187
 
188
+ # Add findings
189
  findings = results.get("findings", [])
190
  if findings:
191
  for finding in findings:
 
195
 
196
  html_result += "</ul>"
197
 
198
+ # Add entities
199
  html_result += "<h3>Extracted Medical Entities:</h3>"
200
 
201
  for category, items in entities.items():
202
  if items:
203
  html_result += f"<p><strong>{category.capitalize()}:</strong> {', '.join(items)}</p>"
204
 
205
+ # Add follow-up recommendations
206
  html_result += "<h3>Follow-up Recommendations:</h3><ul>"
207
  followups = results.get("followup_recommendations", [])
208
 
 
232
  tuple: (results_html, multimodal_plot_html)
233
  """
234
  try:
235
+ # Ensure models are loaded
236
  if not self.load_models() or self.fusion_model is None:
237
  return "Error: Models not loaded properly.", None
238
 
239
+ # Check for empty inputs
240
  if image is None:
241
  return "Error: Please upload an X-ray image for analysis.", None
242
 
 
246
  None,
247
  )
248
 
249
+ # Save uploaded image to a temporary file
250
  temp_dir = tempfile.mkdtemp()
251
  temp_path = os.path.join(temp_dir, "upload.png")
252
 
253
  if isinstance(image, str):
254
+ # Copy the file if it's a path
255
  from shutil import copyfile
256
 
257
  copyfile(image, temp_path)
258
  else:
259
+ # Save if it's a Gradio UploadButton image
260
  image.save(temp_path)
261
 
262
+ # Normalize text
263
  normalized_text = normalize_report_text(text)
264
 
265
+ # Run multimodal analysis
266
  self.logger.info("Performing multimodal analysis")
267
  results = self.fusion_model.analyze(temp_path, normalized_text)
268
 
269
+ # Create visualization
270
  fig = plot_multimodal_results(results, image, text)
271
 
272
+ # Convert to HTML for display
273
  plot_html = self.fig_to_html(fig)
274
 
275
+ # Generate explanation
276
  explanation = self.fusion_model.get_explanation(results)
277
 
278
+ # Format results as HTML
279
  html_result = f"""
280
  <h2>Multimodal Medical Analysis Results</h2>
281
 
 
289
  <ul>
290
  """
291
 
292
+ # Add findings
293
  findings = results.get("findings", [])
294
  if findings:
295
  for finding in findings:
 
299
 
300
  html_result += "</ul>"
301
 
302
+ # Add follow-up recommendations
303
  html_result += "<h3>Recommended Follow-up</h3><ul>"
304
  followups = results.get("followup_recommendations", [])
305
 
 
313
 
314
  html_result += "</ul>"
315
 
316
+ # Add confidence note
317
  confidence = results.get("severity", {}).get("confidence", 0)
318
  html_result += f"""
319
  <p><em>Note: This analysis has a confidence level of {confidence:.0%}.
 
340
  if image is None:
341
  return None
342
 
343
+ # Save uploaded image to a temporary file
344
  temp_dir = tempfile.mkdtemp()
345
  temp_path = os.path.join(temp_dir, "upload.png")
346
 
347
  if isinstance(image, str):
348
+ # Copy the file if it's a path
349
  from shutil import copyfile
350
 
351
  copyfile(image, temp_path)
352
  else:
353
+ # Save if it's a Gradio UploadButton image
354
  image.save(temp_path)
355
 
356
+ # Enhance image
357
  self.logger.info(f"Enhancing image: {temp_path}")
358
  output_path = os.path.join(temp_dir, "enhanced.png")
359
  enhance_xray_image(temp_path, output_path)
360
 
361
+ # Load enhanced image
362
  enhanced = Image.open(output_path)
363
  return enhanced
364
 
365
  except Exception as e:
366
  self.logger.error(f"Error enhancing image: {e}")
367
+ return image # Return original image on error
368
 
369
  def fig_to_html(self, fig):
370
  """Convert matplotlib figure to HTML for display in Gradio."""
 
390
 
391
  app = UHRESApp()
392
 
393
+ # Example medical report for demo
394
  example_report = """
395
  CHEST X-RAY EXAMINATION
396
 
 
409
  RECOMMENDATIONS: Follow-up chest CT to further characterize the nodular opacity in the right lower lobe.
410
  """
411
 
412
+ # Get sample image path if available
413
  sample_images_dir = Path(parent_dir) / "data" / "sample"
414
  sample_images = list(sample_images_dir.glob("*.png")) + list(
415
  sample_images_dir.glob("*.jpg")
 
419
  if sample_images:
420
  sample_image_path = str(sample_images[0])
421
 
422
+ # Define interface
423
  with gr.Blocks(
424
  title="U-HRES: Unified Health Record Exchange System AI Layer", theme=gr.themes.Soft()
425
  ) as interface:
 
456
  multi_results = gr.HTML(label="Analysis Results")
457
  multi_plot = gr.HTML(label="Visualization")
458
 
459
+ # Set up examples if sample image exists
460
  if sample_image_path:
461
  gr.Examples(
462
  examples=[[sample_image_path, example_report]],
 
476
  img_results = gr.HTML(label="Analysis Results")
477
  img_plot = gr.HTML(label="Visualization")
478
 
479
+ # Set up example if sample image exists
480
  if sample_image_path:
481
  gr.Examples(
482
  examples=[[sample_image_path]],
 
500
  text_results = gr.HTML(label="Analysis Results")
501
  text_plot = gr.HTML(label="Entity Visualization")
502
 
503
+ # Set up example
504
  gr.Examples(
505
  examples=[[example_report]],
506
  inputs=[text_input],
 
529
  This tool is for educational and research purposes only. It is not intended to provide medical advice or replace professional healthcare. Always consult with qualified healthcare providers for medical decisions.
530
  """)
531
 
532
+ # Set up event handlers
533
  multi_img_enhance.click(
534
  app.enhance_image, inputs=multi_img_input, outputs=multi_img_input
535
  )
 
552
  outputs=[text_output, text_results, text_plot],
553
  )
554
 
555
+ # Run the interface
556
  interface.launch(show_api=False)
557
 
558
 
mediSync/models/__init__.py CHANGED
@@ -1,5 +1,16 @@
1
- from .image_analyzer import XRayImageAnalyzer
2
- from .multimodal_fusion import MultimodalFusion
3
- from .text_analyzer import MedicalReportAnalyzer
4
-
5
- __all__ = ["XRayImageAnalyzer", "MedicalReportAnalyzer", "MultimodalFusion"]
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ U-HRES: Models Module
3
+ =====================
4
+
5
+ This module contains the core machine learning models for the U-HRES system:
6
+
7
+ 1. XRayImageAnalyzer: Analyzes X-ray images using pre-trained vision models
8
+ 2. MedicalReportAnalyzer: Extracts information from medical reports using NLP models
9
+ 3. MultimodalFusion: Combines insights from both image and text analysis
10
+ """
11
+
12
+ from .image_analyzer import XRayImageAnalyzer
13
+ from .multimodal_fusion import MultimodalFusion
14
+ from .text_analyzer import MedicalReportAnalyzer
15
+
16
+ __all__ = ["XRayImageAnalyzer", "MedicalReportAnalyzer", "MultimodalFusion"]
mediSync/models/image_analyzer.py CHANGED
@@ -5,13 +5,28 @@ import torch
5
  from PIL import Image
6
  from transformers import AutoFeatureExtractor, AutoModelForImageClassification
7
 
 
8
  class XRayImageAnalyzer:
 
 
 
 
 
 
9
 
10
  def __init__(
11
  self, model_name="codewithdark/vit-chest-xray", device=None
12
  ):
 
 
 
 
 
 
 
13
  self.logger = logging.getLogger(__name__)
14
 
 
15
  if device is None:
16
  self.device = "cuda" if torch.cuda.is_available() else "cpu"
17
  else:
@@ -19,6 +34,8 @@ class XRayImageAnalyzer:
19
 
20
  self.logger.info(f"Using device: {self.device}")
21
 
 
 
22
  hf_token = os.environ.get("HF_TOKEN") or os.environ.get("HUGGING_FACE_HUB_TOKEN")
23
  try:
24
  self.feature_extractor = AutoFeatureExtractor.from_pretrained(
@@ -28,9 +45,10 @@ class XRayImageAnalyzer:
28
  model_name, token=hf_token
29
  )
30
  self.model.to(self.device)
31
- self.model.eval()
32
  self.logger.info(f"Successfully loaded model: {model_name}")
33
 
 
34
  self.labels = self.model.config.id2label
35
 
36
  except Exception as e:
@@ -38,14 +56,26 @@ class XRayImageAnalyzer:
38
  raise
39
 
40
  def preprocess_image(self, image_path):
 
 
 
 
 
 
 
 
 
41
  try:
 
42
  if isinstance(image_path, str):
43
  if not os.path.exists(image_path):
44
  raise FileNotFoundError(f"Image file not found: {image_path}")
45
  image = Image.open(image_path).convert("RGB")
46
  else:
 
47
  image = image_path.convert("RGB")
48
 
 
49
  inputs = self.feature_extractor(images=image, return_tensors="pt")
50
  inputs = {k: v.to(self.device) for k, v in inputs.items()}
51
 
@@ -56,22 +86,42 @@ class XRayImageAnalyzer:
56
  raise
57
 
58
  def analyze(self, image_path, threshold=0.5):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
  try:
 
60
  inputs, original_image = self.preprocess_image(image_path)
61
 
 
62
  with torch.no_grad():
63
  outputs = self.model(**inputs)
64
 
 
65
  probabilities = torch.nn.functional.softmax(outputs.logits, dim=-1)[0]
66
  probabilities = probabilities.cpu().numpy()
67
 
 
68
  predictions = []
69
  for i, p in enumerate(probabilities):
70
  label = self.labels[i]
71
  predictions.append((label, float(p)))
72
 
 
73
  predictions.sort(key=lambda x: x[1], reverse=True)
74
 
 
75
  normal_idx = [
76
  i
77
  for i, (label, _) in enumerate(predictions)
@@ -99,6 +149,15 @@ class XRayImageAnalyzer:
99
  raise
100
 
101
  def get_explanation(self, results):
 
 
 
 
 
 
 
 
 
102
  if not results["has_abnormality"]:
103
  explanation = (
104
  f"The X-ray appears normal with {results['confidence']:.1%} confidence."
@@ -110,17 +169,23 @@ class XRayImageAnalyzer:
110
  f"Other potential findings include:\n"
111
  )
112
 
 
113
  for label, prob in results["predictions"][1:4]:
114
- if prob > 0.05:
115
  explanation += f"- {label}: {prob:.1%}\n"
116
 
117
  return explanation
118
 
 
 
119
  if __name__ == "__main__":
 
120
  logging.basicConfig(level=logging.INFO)
121
 
 
122
  analyzer = XRayImageAnalyzer()
123
 
 
124
  sample_dir = "../data/sample"
125
  if os.path.exists(sample_dir) and os.listdir(sample_dir):
126
  sample_image = os.path.join(sample_dir, os.listdir(sample_dir)[0])
 
5
  from PIL import Image
6
  from transformers import AutoFeatureExtractor, AutoModelForImageClassification
7
 
8
+
9
  class XRayImageAnalyzer:
10
+ """
11
+ A class for analyzing medical X-ray images using pre-trained models from Hugging Face.
12
+
13
+ This analyzer uses the DeiT (Data-efficient image Transformers) model fine-tuned
14
+ on chest X-ray images to detect abnormalities.
15
+ """
16
 
17
  def __init__(
18
  self, model_name="codewithdark/vit-chest-xray", device=None
19
  ):
20
+ """
21
+ Initialize the X-ray image analyzer with a specific pre-trained model.
22
+
23
+ Args:
24
+ model_name (str): The Hugging Face model name to use
25
+ device (str, optional): Device to run the model on ('cuda' or 'cpu')
26
+ """
27
  self.logger = logging.getLogger(__name__)
28
 
29
+ # Determine device (CPU or GPU)
30
  if device is None:
31
  self.device = "cuda" if torch.cuda.is_available() else "cpu"
32
  else:
 
34
 
35
  self.logger.info(f"Using device: {self.device}")
36
 
37
+ # Load model and feature extractor
38
+ # Use HF_TOKEN from environment if available (for Hugging Face Spaces)
39
  hf_token = os.environ.get("HF_TOKEN") or os.environ.get("HUGGING_FACE_HUB_TOKEN")
40
  try:
41
  self.feature_extractor = AutoFeatureExtractor.from_pretrained(
 
45
  model_name, token=hf_token
46
  )
47
  self.model.to(self.device)
48
+ self.model.eval() # Set to evaluation mode
49
  self.logger.info(f"Successfully loaded model: {model_name}")
50
 
51
+ # Map labels to more informative descriptions
52
  self.labels = self.model.config.id2label
53
 
54
  except Exception as e:
 
56
  raise
57
 
58
  def preprocess_image(self, image_path):
59
+ """
60
+ Preprocess an X-ray image for model input.
61
+
62
+ Args:
63
+ image_path (str or PIL.Image): Path to image or PIL Image object
64
+
65
+ Returns:
66
+ dict: Processed inputs ready for the model
67
+ """
68
  try:
69
+ # Load image if path is provided
70
  if isinstance(image_path, str):
71
  if not os.path.exists(image_path):
72
  raise FileNotFoundError(f"Image file not found: {image_path}")
73
  image = Image.open(image_path).convert("RGB")
74
  else:
75
+ # Assume it's already a PIL Image
76
  image = image_path.convert("RGB")
77
 
78
+ # Apply feature extraction
79
  inputs = self.feature_extractor(images=image, return_tensors="pt")
80
  inputs = {k: v.to(self.device) for k, v in inputs.items()}
81
 
 
86
  raise
87
 
88
  def analyze(self, image_path, threshold=0.5):
89
+ """
90
+ Analyze an X-ray image and detect abnormalities.
91
+
92
+ Args:
93
+ image_path (str or PIL.Image): Path to the X-ray image or PIL Image object
94
+ threshold (float): Classification threshold for positive findings
95
+
96
+ Returns:
97
+ dict: Analysis results including:
98
+ - predictions: List of (label, probability) tuples
99
+ - primary_finding: The most likely abnormality
100
+ - has_abnormality: Boolean indicating if abnormalities were detected
101
+ - confidence: Confidence score for the primary finding
102
+ """
103
  try:
104
+ # Preprocess the image
105
  inputs, original_image = self.preprocess_image(image_path)
106
 
107
+ # Run inference
108
  with torch.no_grad():
109
  outputs = self.model(**inputs)
110
 
111
+ # Process predictions
112
  probabilities = torch.nn.functional.softmax(outputs.logits, dim=-1)[0]
113
  probabilities = probabilities.cpu().numpy()
114
 
115
+ # Get predictions sorted by probability
116
  predictions = []
117
  for i, p in enumerate(probabilities):
118
  label = self.labels[i]
119
  predictions.append((label, float(p)))
120
 
121
+ # Sort by probability (descending)
122
  predictions.sort(key=lambda x: x[1], reverse=True)
123
 
124
+ # Determine if there's an abnormality and the primary finding
125
  normal_idx = [
126
  i
127
  for i, (label, _) in enumerate(predictions)
 
149
  raise
150
 
151
  def get_explanation(self, results):
152
+ """
153
+ Generate a human-readable explanation of the analysis results.
154
+
155
+ Args:
156
+ results (dict): The results returned by the analyze method
157
+
158
+ Returns:
159
+ str: A text explanation of the findings
160
+ """
161
  if not results["has_abnormality"]:
162
  explanation = (
163
  f"The X-ray appears normal with {results['confidence']:.1%} confidence."
 
169
  f"Other potential findings include:\n"
170
  )
171
 
172
+ # Add top 3 other findings (skipping the first one which is primary)
173
  for label, prob in results["predictions"][1:4]:
174
+ if prob > 0.05: # Only include if probability > 5%
175
  explanation += f"- {label}: {prob:.1%}\n"
176
 
177
  return explanation
178
 
179
+
180
+ # Example usage
181
  if __name__ == "__main__":
182
+ # Set up logging
183
  logging.basicConfig(level=logging.INFO)
184
 
185
+ # Test on a sample image if available
186
  analyzer = XRayImageAnalyzer()
187
 
188
+ # Check if sample data directory exists
189
  sample_dir = "../data/sample"
190
  if os.path.exists(sample_dir) and os.listdir(sample_dir):
191
  sample_image = os.path.join(sample_dir, os.listdir(sample_dir)[0])
mediSync/models/multimodal_fusion.py CHANGED
@@ -3,11 +3,30 @@ import logging
3
  from .image_analyzer import XRayImageAnalyzer
4
  from .text_analyzer import MedicalReportAnalyzer
5
 
 
6
  class MultimodalFusion:
 
 
 
 
 
 
 
 
 
7
 
8
  def __init__(self, image_model=None, text_model=None, device=None):
 
 
 
 
 
 
 
 
9
  self.logger = logging.getLogger(__name__)
10
 
 
11
  if device is None:
12
  import torch
13
 
@@ -17,6 +36,7 @@ class MultimodalFusion:
17
 
18
  self.logger.info(f"Using device: {self.device}")
19
 
 
20
  try:
21
  self.image_analyzer = XRayImageAnalyzer(
22
  model_name=image_model
@@ -29,6 +49,7 @@ class MultimodalFusion:
29
  self.logger.error(f"Failed to initialize image analyzer: {e}")
30
  self.image_analyzer = None
31
 
 
32
  try:
33
  self.text_analyzer = MedicalReportAnalyzer(
34
  classifier_model=text_model if text_model else "medicalai/ClinicalBERT",
@@ -40,6 +61,15 @@ class MultimodalFusion:
40
  self.text_analyzer = None
41
 
42
  def analyze_image(self, image_path):
 
 
 
 
 
 
 
 
 
43
  if not self.image_analyzer:
44
  self.logger.warning("Image analyzer not available")
45
  return {"error": "Image analyzer not available"}
@@ -51,6 +81,15 @@ class MultimodalFusion:
51
  return {"error": str(e)}
52
 
53
  def analyze_text(self, text):
 
 
 
 
 
 
 
 
 
54
  if not self.text_analyzer:
55
  self.logger.warning("Text analyzer not available")
56
  return {"error": "Text analyzer not available"}
@@ -62,24 +101,41 @@ class MultimodalFusion:
62
  return {"error": str(e)}
63
 
64
  def _calculate_agreement_score(self, image_results, text_results):
 
 
 
 
 
 
 
 
 
 
65
  try:
 
66
  agreement = 0.5
67
 
 
68
  image_abnormal = image_results.get("has_abnormality", False)
69
 
 
70
  text_severity = text_results.get("severity", {}).get("level", "Unknown")
71
  text_abnormal = text_severity not in ["Normal", "Unknown"]
72
 
 
73
  if image_abnormal == text_abnormal:
74
  agreement += 0.25
75
  else:
76
  agreement -= 0.25
77
 
 
78
  image_finding = image_results.get("primary_finding", "").lower()
79
 
 
80
  problems = text_results.get("entities", {}).get("problem", [])
81
  problem_text = " ".join(problems).lower()
82
 
 
83
  common_conditions = [
84
  "pneumonia",
85
  "effusion",
@@ -104,35 +160,51 @@ class MultimodalFusion:
104
 
105
  if in_image and in_text:
106
  matching_conditions += 1
107
- agreement += 0.05
108
 
 
109
  if total_mentioned > 0:
110
  match_ratio = matching_conditions / total_mentioned
111
  agreement += match_ratio * 0.2
112
 
 
113
  agreement = max(0, min(1, agreement))
114
 
115
  return agreement
116
 
117
  except Exception as e:
118
  self.logger.error(f"Error calculating agreement score: {e}")
119
- return 0.5
120
 
121
  def _get_confidence_weighted_finding(self, image_results, text_results, agreement):
 
 
 
 
 
 
 
 
 
 
 
122
  try:
123
  image_finding = image_results.get("primary_finding", "")
124
  image_confidence = image_results.get("confidence", 0.5)
125
 
 
126
  problems = text_results.get("entities", {}).get("problem", [])
127
 
128
  text_confidence = text_results.get("severity", {}).get("confidence", 0.5)
129
 
130
  if not problems:
 
131
  if image_confidence > 0.7:
132
  return image_finding
133
  else:
134
  return "No significant findings"
135
 
 
136
  if image_confidence > text_confidence + 0.2:
137
  return image_finding
138
  elif problems and text_confidence > image_confidence + 0.2:
@@ -142,11 +214,14 @@ class MultimodalFusion:
142
  else "Unknown finding"
143
  )
144
  else:
 
145
  if agreement > 0.7:
 
146
  for problem in problems:
147
  if problem.lower() in image_finding.lower():
148
  return problem
149
 
 
150
  if image_confidence > 0.6:
151
  return image_finding
152
  elif problems:
@@ -154,6 +229,7 @@ class MultimodalFusion:
154
  else:
155
  return image_finding
156
  else:
 
157
  if image_finding and problems:
158
  return f"{image_finding} (image) / {problems[0]} (report)"
159
  elif image_finding:
@@ -168,9 +244,21 @@ class MultimodalFusion:
168
  return "Unable to determine primary finding"
169
 
170
  def _merge_followup_recommendations(self, image_results, text_results):
 
 
 
 
 
 
 
 
 
 
171
  try:
 
172
  text_recommendations = text_results.get("followup_recommendations", [])
173
 
 
174
  image_recommendations = []
175
 
176
  if image_results.get("has_abnormality", False):
@@ -198,8 +286,10 @@ class MultimodalFusion:
198
  "Consider clinical correlation and potential follow-up."
199
  )
200
 
 
201
  all_recommendations = text_recommendations + image_recommendations
202
 
 
203
  unique_recommendations = []
204
  for rec in all_recommendations:
205
  if not any(
@@ -215,34 +305,55 @@ class MultimodalFusion:
215
  return ["Follow-up recommended based on findings."]
216
 
217
  def _is_similar_recommendation(self, rec1, rec2):
 
 
218
  rec1_lower = rec1.lower()
219
  rec2_lower = rec2.lower()
220
 
 
221
  words1 = set(rec1_lower.split())
222
  words2 = set(rec2_lower.split())
223
 
 
224
  intersection = words1.intersection(words2)
225
  union = words1.union(words2)
226
 
227
  similarity = len(intersection) / len(union) if union else 0
228
 
 
229
  return similarity > 0.6
230
 
231
  def _get_final_severity(self, image_results, text_results, agreement):
 
 
 
 
 
 
 
 
 
 
 
232
  try:
 
233
  text_severity = text_results.get("severity", {})
234
  text_level = text_severity.get("level", "Unknown")
235
  text_score = text_severity.get("score", 0)
236
  text_confidence = text_severity.get("confidence", 0.5)
237
 
 
238
  image_abnormal = image_results.get("has_abnormality", False)
239
  image_confidence = image_results.get("confidence", 0.5)
240
 
 
241
  image_severity = "Normal" if not image_abnormal else "Moderate"
242
  image_score = 0 if not image_abnormal else 2.0
243
 
 
244
  primary_finding = image_results.get("primary_finding", "").lower()
245
 
 
246
  severity_mapping = {
247
  "pneumonia": ("Moderate", 2.5),
248
  "pneumothorax": ("Severe", 3.0),
@@ -256,15 +367,19 @@ class MultimodalFusion:
256
  "consolidation": ("Moderate", 2.0),
257
  }
258
 
 
259
  for key, (severity, score) in severity_mapping.items():
260
  if key in primary_finding:
261
  image_severity = severity
262
  image_score = score
263
  break
264
 
 
265
  if agreement > 0.7:
 
266
  final_score = (image_score + text_score) / 2
267
  else:
 
268
  total_confidence = image_confidence + text_confidence
269
  if total_confidence > 0:
270
  image_weight = image_confidence / total_confidence
@@ -275,6 +390,7 @@ class MultimodalFusion:
275
  else:
276
  final_score = (image_score + text_score) / 2
277
 
 
278
  severity_levels = {
279
  0: "Normal",
280
  1: "Mild",
@@ -283,6 +399,7 @@ class MultimodalFusion:
283
  4: "Critical",
284
  }
285
 
 
286
  level_index = round(min(4, max(0, final_score)))
287
  final_level = severity_levels[level_index]
288
 
@@ -297,30 +414,48 @@ class MultimodalFusion:
297
  return {"level": "Unknown", "score": 0, "confidence": 0}
298
 
299
  def fuse_analyses(self, image_results, text_results):
 
 
 
 
 
 
 
 
 
 
300
  try:
 
301
  agreement = self._calculate_agreement_score(image_results, text_results)
302
  self.logger.info(f"Agreement score between modalities: {agreement:.2f}")
303
 
 
304
  primary_finding = self._get_confidence_weighted_finding(
305
  image_results, text_results, agreement
306
  )
307
 
 
308
  followup = self._merge_followup_recommendations(image_results, text_results)
309
 
 
310
  severity = self._get_final_severity(image_results, text_results, agreement)
311
 
 
312
  findings = []
313
 
 
314
  text_findings = text_results.get("findings", [])
315
  if text_findings:
316
  findings.extend(text_findings)
317
 
 
318
  image_finding = image_results.get("primary_finding", "")
319
  if image_finding and not any(
320
  image_finding.lower() in f.lower() for f in findings
321
  ):
322
  findings.append(f"Image finding: {image_finding}")
323
 
 
324
  fused_result = {
325
  "agreement_score": round(agreement, 2),
326
  "primary_finding": primary_finding,
@@ -340,11 +475,24 @@ class MultimodalFusion:
340
  }
341
 
342
  def analyze(self, image_path, report_text):
 
 
 
 
 
 
 
 
 
 
343
  try:
 
344
  image_results = self.analyze_image(image_path)
345
 
 
346
  text_results = self.analyze_text(report_text)
347
 
 
348
  return self.fuse_analyses(image_results, text_results)
349
 
350
  except Exception as e:
@@ -352,9 +500,19 @@ class MultimodalFusion:
352
  return {"error": str(e)}
353
 
354
  def get_explanation(self, fused_results):
 
 
 
 
 
 
 
 
 
355
  try:
356
  explanation = []
357
 
 
358
  primary_finding = fused_results.get("primary_finding", "Unknown")
359
  severity = fused_results.get("severity", {}).get("level", "Unknown")
360
 
@@ -363,6 +521,7 @@ class MultimodalFusion:
363
  explanation.append(f"Primary finding: **{primary_finding}**\n")
364
  explanation.append(f"Severity level: **{severity}**\n")
365
 
 
366
  agreement = fused_results.get("agreement_score", 0)
367
  agreement_text = (
368
  "High" if agreement > 0.7 else "Moderate" if agreement > 0.4 else "Low"
@@ -372,6 +531,7 @@ class MultimodalFusion:
372
  f"Image and text analysis agreement: **{agreement_text}** ({agreement:.0%})\n"
373
  )
374
 
 
375
  explanation.append("\n## Detailed Findings\n")
376
  findings = fused_results.get("findings", [])
377
 
@@ -381,6 +541,7 @@ class MultimodalFusion:
381
  else:
382
  explanation.append("No specific findings detailed.\n")
383
 
 
384
  explanation.append("\n## Recommended Follow-up\n")
385
  followups = fused_results.get("followup_recommendations", [])
386
 
@@ -390,6 +551,7 @@ class MultimodalFusion:
390
  else:
391
  explanation.append("No specific follow-up recommendations provided.\n")
392
 
 
393
  confidence = fused_results.get("severity", {}).get("confidence", 0)
394
  explanation.append(
395
  f"\n*Note: This analysis has a confidence level of {confidence:.0%}. "
@@ -402,26 +564,68 @@ class MultimodalFusion:
402
  self.logger.error(f"Error generating explanation: {e}")
403
  return "Error generating analysis explanation."
404
 
 
 
405
  if __name__ == "__main__":
 
406
  logging.basicConfig(level=logging.INFO)
407
 
 
408
  import os
409
 
410
  fusion = MultimodalFusion()
411
 
 
412
  sample_report = """
413
  CHEST X-RAY EXAMINATION
414
-
415
  CLINICAL HISTORY: 55-year-old male with cough and fever.
416
-
417
  FINDINGS: The heart size is at the upper limits of normal. The lungs are clear without focal consolidation,
418
  effusion, or pneumothorax. There is mild prominence of the pulmonary vasculature. No pleural effusion is seen.
419
  There is a small nodular opacity noted in the right lower lobe measuring approximately 8mm, which is suspicious
420
  and warrants further investigation. The mediastinum is unremarkable. The visualized bony structures show no acute abnormalities.
421
-
422
  IMPRESSION:
423
  1. Mild cardiomegaly.
424
  2. 8mm nodular opacity in the right lower lobe, recommend follow-up CT for further evaluation.
425
  3. No acute pulmonary parenchymal abnormality.
426
-
427
  RECOMMENDATIONS: Follow-up chest CT to further characterize the nodular opacity in the right lower lobe.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  from .image_analyzer import XRayImageAnalyzer
4
  from .text_analyzer import MedicalReportAnalyzer
5
 
6
+
7
  class MultimodalFusion:
8
+ """
9
+ A class for fusing insights from image analysis and text analysis of medical data.
10
+
11
+ This fusion approach combines the strengths of both modalities:
12
+ - Images provide visual evidence of abnormalities
13
+ - Text reports provide context, history and radiologist interpretations
14
+
15
+ The combined analysis provides a more comprehensive understanding than either modality alone.
16
+ """
17
 
18
  def __init__(self, image_model=None, text_model=None, device=None):
19
+ """
20
+ Initialize the multimodal fusion module with image and text analyzers.
21
+
22
+ Args:
23
+ image_model (str, optional): Model to use for image analysis
24
+ text_model (str, optional): Model to use for text analysis
25
+ device (str, optional): Device to run models on ('cuda' or 'cpu')
26
+ """
27
  self.logger = logging.getLogger(__name__)
28
 
29
+ # Determine device
30
  if device is None:
31
  import torch
32
 
 
36
 
37
  self.logger.info(f"Using device: {self.device}")
38
 
39
+ # Initialize image analyzer
40
  try:
41
  self.image_analyzer = XRayImageAnalyzer(
42
  model_name=image_model
 
49
  self.logger.error(f"Failed to initialize image analyzer: {e}")
50
  self.image_analyzer = None
51
 
52
+ # Initialize text analyzer
53
  try:
54
  self.text_analyzer = MedicalReportAnalyzer(
55
  classifier_model=text_model if text_model else "medicalai/ClinicalBERT",
 
61
  self.text_analyzer = None
62
 
63
  def analyze_image(self, image_path):
64
+ """
65
+ Analyze a medical image.
66
+
67
+ Args:
68
+ image_path (str): Path to the medical image
69
+
70
+ Returns:
71
+ dict: Image analysis results
72
+ """
73
  if not self.image_analyzer:
74
  self.logger.warning("Image analyzer not available")
75
  return {"error": "Image analyzer not available"}
 
81
  return {"error": str(e)}
82
 
83
  def analyze_text(self, text):
84
+ """
85
+ Analyze medical report text.
86
+
87
+ Args:
88
+ text (str): Medical report text
89
+
90
+ Returns:
91
+ dict: Text analysis results
92
+ """
93
  if not self.text_analyzer:
94
  self.logger.warning("Text analyzer not available")
95
  return {"error": "Text analyzer not available"}
 
101
  return {"error": str(e)}
102
 
103
  def _calculate_agreement_score(self, image_results, text_results):
104
+ """
105
+ Calculate agreement score between image and text analyses.
106
+
107
+ Args:
108
+ image_results (dict): Results from image analysis
109
+ text_results (dict): Results from text analysis
110
+
111
+ Returns:
112
+ float: Agreement score (0-1, where 1 is perfect agreement)
113
+ """
114
  try:
115
+ # Default to neutral agreement
116
  agreement = 0.5
117
 
118
+ # Check if image detected abnormality
119
  image_abnormal = image_results.get("has_abnormality", False)
120
 
121
+ # Check text severity
122
  text_severity = text_results.get("severity", {}).get("level", "Unknown")
123
  text_abnormal = text_severity not in ["Normal", "Unknown"]
124
 
125
+ # Basic agreement check
126
  if image_abnormal == text_abnormal:
127
  agreement += 0.25
128
  else:
129
  agreement -= 0.25
130
 
131
+ # Check if specific findings match
132
  image_finding = image_results.get("primary_finding", "").lower()
133
 
134
+ # Extract problem entities from text
135
  problems = text_results.get("entities", {}).get("problem", [])
136
  problem_text = " ".join(problems).lower()
137
 
138
+ # Check for common keywords in both
139
  common_conditions = [
140
  "pneumonia",
141
  "effusion",
 
160
 
161
  if in_image and in_text:
162
  matching_conditions += 1
163
+ agreement += 0.05 # Boost agreement for each matching condition
164
 
165
+ # Calculate condition match ratio if any conditions were mentioned
166
  if total_mentioned > 0:
167
  match_ratio = matching_conditions / total_mentioned
168
  agreement += match_ratio * 0.2
169
 
170
+ # Normalize agreement to 0-1 range
171
  agreement = max(0, min(1, agreement))
172
 
173
  return agreement
174
 
175
  except Exception as e:
176
  self.logger.error(f"Error calculating agreement score: {e}")
177
+ return 0.5 # Return neutral agreement on error
178
 
179
  def _get_confidence_weighted_finding(self, image_results, text_results, agreement):
180
+ """
181
+ Get the most confident finding weighted by modality confidence.
182
+
183
+ Args:
184
+ image_results (dict): Results from image analysis
185
+ text_results (dict): Results from text analysis
186
+ agreement (float): Agreement score between modalities
187
+
188
+ Returns:
189
+ str: Most confident finding
190
+ """
191
  try:
192
  image_finding = image_results.get("primary_finding", "")
193
  image_confidence = image_results.get("confidence", 0.5)
194
 
195
+ # For text, use the most severe problem as primary finding
196
  problems = text_results.get("entities", {}).get("problem", [])
197
 
198
  text_confidence = text_results.get("severity", {}).get("confidence", 0.5)
199
 
200
  if not problems:
201
+ # No problems identified in text
202
  if image_confidence > 0.7:
203
  return image_finding
204
  else:
205
  return "No significant findings"
206
 
207
+ # Simple confidence-weighted selection
208
  if image_confidence > text_confidence + 0.2:
209
  return image_finding
210
  elif problems and text_confidence > image_confidence + 0.2:
 
214
  else "Unknown finding"
215
  )
216
  else:
217
+ # Similar confidence, check agreement
218
  if agreement > 0.7:
219
+ # High agreement, try to find the specific condition mentioned in both
220
  for problem in problems:
221
  if problem.lower() in image_finding.lower():
222
  return problem
223
 
224
+ # Default to image finding if high confidence
225
  if image_confidence > 0.6:
226
  return image_finding
227
  elif problems:
 
229
  else:
230
  return image_finding
231
  else:
232
+ # Low agreement, include both perspectives
233
  if image_finding and problems:
234
  return f"{image_finding} (image) / {problems[0]} (report)"
235
  elif image_finding:
 
244
  return "Unable to determine primary finding"
245
 
246
  def _merge_followup_recommendations(self, image_results, text_results):
247
+ """
248
+ Merge follow-up recommendations from both modalities.
249
+
250
+ Args:
251
+ image_results (dict): Results from image analysis
252
+ text_results (dict): Results from text analysis
253
+
254
+ Returns:
255
+ list: Combined follow-up recommendations
256
+ """
257
  try:
258
+ # Get text-based recommendations
259
  text_recommendations = text_results.get("followup_recommendations", [])
260
 
261
+ # Create image-based recommendations based on findings
262
  image_recommendations = []
263
 
264
  if image_results.get("has_abnormality", False):
 
286
  "Consider clinical correlation and potential follow-up."
287
  )
288
 
289
+ # Combine recommendations, removing duplicates
290
  all_recommendations = text_recommendations + image_recommendations
291
 
292
+ # Remove near-duplicates (similar recommendations)
293
  unique_recommendations = []
294
  for rec in all_recommendations:
295
  if not any(
 
305
  return ["Follow-up recommended based on findings."]
306
 
307
  def _is_similar_recommendation(self, rec1, rec2):
308
+ """Check if two recommendations are semantically similar."""
309
+ # Convert to lowercase for comparison
310
  rec1_lower = rec1.lower()
311
  rec2_lower = rec2.lower()
312
 
313
+ # Check for significant overlap
314
  words1 = set(rec1_lower.split())
315
  words2 = set(rec2_lower.split())
316
 
317
+ # Calculate Jaccard similarity
318
  intersection = words1.intersection(words2)
319
  union = words1.union(words2)
320
 
321
  similarity = len(intersection) / len(union) if union else 0
322
 
323
+ # Consider similar if more than 60% overlap
324
  return similarity > 0.6
325
 
326
  def _get_final_severity(self, image_results, text_results, agreement):
327
+ """
328
+ Determine final severity based on both modalities.
329
+
330
+ Args:
331
+ image_results (dict): Results from image analysis
332
+ text_results (dict): Results from text analysis
333
+ agreement (float): Agreement score between modalities
334
+
335
+ Returns:
336
+ dict: Final severity assessment
337
+ """
338
  try:
339
+ # Get text-based severity
340
  text_severity = text_results.get("severity", {})
341
  text_level = text_severity.get("level", "Unknown")
342
  text_score = text_severity.get("score", 0)
343
  text_confidence = text_severity.get("confidence", 0.5)
344
 
345
+ # Convert image findings to severity
346
  image_abnormal = image_results.get("has_abnormality", False)
347
  image_confidence = image_results.get("confidence", 0.5)
348
 
349
+ # Default severity mapping from image
350
  image_severity = "Normal" if not image_abnormal else "Moderate"
351
  image_score = 0 if not image_abnormal else 2.0
352
 
353
+ # Adjust image severity based on specific findings
354
  primary_finding = image_results.get("primary_finding", "").lower()
355
 
356
+ # Map certain conditions to severity levels
357
  severity_mapping = {
358
  "pneumonia": ("Moderate", 2.5),
359
  "pneumothorax": ("Severe", 3.0),
 
367
  "consolidation": ("Moderate", 2.0),
368
  }
369
 
370
+ # Check if any key terms are in the primary finding
371
  for key, (severity, score) in severity_mapping.items():
372
  if key in primary_finding:
373
  image_severity = severity
374
  image_score = score
375
  break
376
 
377
+ # Weight based on confidence and agreement
378
  if agreement > 0.7:
379
+ # High agreement - weight equally
380
  final_score = (image_score + text_score) / 2
381
  else:
382
+ # Lower agreement - weight by confidence
383
  total_confidence = image_confidence + text_confidence
384
  if total_confidence > 0:
385
  image_weight = image_confidence / total_confidence
 
390
  else:
391
  final_score = (image_score + text_score) / 2
392
 
393
+ # Map score to severity level
394
  severity_levels = {
395
  0: "Normal",
396
  1: "Mild",
 
399
  4: "Critical",
400
  }
401
 
402
+ # Round to nearest level
403
  level_index = round(min(4, max(0, final_score)))
404
  final_level = severity_levels[level_index]
405
 
 
414
  return {"level": "Unknown", "score": 0, "confidence": 0}
415
 
416
  def fuse_analyses(self, image_results, text_results):
417
+ """
418
+ Fuse the results from image and text analyses.
419
+
420
+ Args:
421
+ image_results (dict): Results from image analysis
422
+ text_results (dict): Results from text analysis
423
+
424
+ Returns:
425
+ dict: Fused analysis results
426
+ """
427
  try:
428
+ # Calculate agreement between modalities
429
  agreement = self._calculate_agreement_score(image_results, text_results)
430
  self.logger.info(f"Agreement score between modalities: {agreement:.2f}")
431
 
432
+ # Get confidence-weighted primary finding
433
  primary_finding = self._get_confidence_weighted_finding(
434
  image_results, text_results, agreement
435
  )
436
 
437
+ # Merge follow-up recommendations
438
  followup = self._merge_followup_recommendations(image_results, text_results)
439
 
440
+ # Get final severity assessment
441
  severity = self._get_final_severity(image_results, text_results, agreement)
442
 
443
+ # Create comprehensive findings list
444
  findings = []
445
 
446
+ # Add text-extracted findings
447
  text_findings = text_results.get("findings", [])
448
  if text_findings:
449
  findings.extend(text_findings)
450
 
451
+ # Add primary image finding if not already included
452
  image_finding = image_results.get("primary_finding", "")
453
  if image_finding and not any(
454
  image_finding.lower() in f.lower() for f in findings
455
  ):
456
  findings.append(f"Image finding: {image_finding}")
457
 
458
+ # Create fused result
459
  fused_result = {
460
  "agreement_score": round(agreement, 2),
461
  "primary_finding": primary_finding,
 
475
  }
476
 
477
  def analyze(self, image_path, report_text):
478
+ """
479
+ Perform multimodal analysis of medical image and report.
480
+
481
+ Args:
482
+ image_path (str): Path to the medical image
483
+ report_text (str): Medical report text
484
+
485
+ Returns:
486
+ dict: Fused analysis results
487
+ """
488
  try:
489
+ # Analyze image
490
  image_results = self.analyze_image(image_path)
491
 
492
+ # Analyze text
493
  text_results = self.analyze_text(report_text)
494
 
495
+ # Fuse the analyses
496
  return self.fuse_analyses(image_results, text_results)
497
 
498
  except Exception as e:
 
500
  return {"error": str(e)}
501
 
502
  def get_explanation(self, fused_results):
503
+ """
504
+ Generate a human-readable explanation of the fused analysis.
505
+
506
+ Args:
507
+ fused_results (dict): Results from the fused analysis
508
+
509
+ Returns:
510
+ str: A text explanation of the fused analysis
511
+ """
512
  try:
513
  explanation = []
514
 
515
+ # Add overview section
516
  primary_finding = fused_results.get("primary_finding", "Unknown")
517
  severity = fused_results.get("severity", {}).get("level", "Unknown")
518
 
 
521
  explanation.append(f"Primary finding: **{primary_finding}**\n")
522
  explanation.append(f"Severity level: **{severity}**\n")
523
 
524
+ # Add agreement information
525
  agreement = fused_results.get("agreement_score", 0)
526
  agreement_text = (
527
  "High" if agreement > 0.7 else "Moderate" if agreement > 0.4 else "Low"
 
531
  f"Image and text analysis agreement: **{agreement_text}** ({agreement:.0%})\n"
532
  )
533
 
534
+ # Add findings section
535
  explanation.append("\n## Detailed Findings\n")
536
  findings = fused_results.get("findings", [])
537
 
 
541
  else:
542
  explanation.append("No specific findings detailed.\n")
543
 
544
+ # Add follow-up section
545
  explanation.append("\n## Recommended Follow-up\n")
546
  followups = fused_results.get("followup_recommendations", [])
547
 
 
551
  else:
552
  explanation.append("No specific follow-up recommendations provided.\n")
553
 
554
+ # Add confidence note
555
  confidence = fused_results.get("severity", {}).get("confidence", 0)
556
  explanation.append(
557
  f"\n*Note: This analysis has a confidence level of {confidence:.0%}. "
 
564
  self.logger.error(f"Error generating explanation: {e}")
565
  return "Error generating analysis explanation."
566
 
567
+
568
+ # Example usage
569
  if __name__ == "__main__":
570
+ # Set up logging
571
  logging.basicConfig(level=logging.INFO)
572
 
573
+ # Test on sample data if available
574
  import os
575
 
576
  fusion = MultimodalFusion()
577
 
578
+ # Sample text report
579
  sample_report = """
580
  CHEST X-RAY EXAMINATION
581
+
582
  CLINICAL HISTORY: 55-year-old male with cough and fever.
583
+
584
  FINDINGS: The heart size is at the upper limits of normal. The lungs are clear without focal consolidation,
585
  effusion, or pneumothorax. There is mild prominence of the pulmonary vasculature. No pleural effusion is seen.
586
  There is a small nodular opacity noted in the right lower lobe measuring approximately 8mm, which is suspicious
587
  and warrants further investigation. The mediastinum is unremarkable. The visualized bony structures show no acute abnormalities.
588
+
589
  IMPRESSION:
590
  1. Mild cardiomegaly.
591
  2. 8mm nodular opacity in the right lower lobe, recommend follow-up CT for further evaluation.
592
  3. No acute pulmonary parenchymal abnormality.
593
+
594
  RECOMMENDATIONS: Follow-up chest CT to further characterize the nodular opacity in the right lower lobe.
595
+ """
596
+
597
+ # Check if sample data directory exists and contains images
598
+ sample_dir = "../data/sample"
599
+ if os.path.exists(sample_dir) and os.listdir(sample_dir):
600
+ sample_image = os.path.join(sample_dir, os.listdir(sample_dir)[0])
601
+ print(f"Analyzing sample image: {sample_image}")
602
+
603
+ # Perform multimodal analysis
604
+ fused_results = fusion.analyze(sample_image, sample_report)
605
+ explanation = fusion.get_explanation(fused_results)
606
+
607
+ print("\nFused Analysis Results:")
608
+ print(explanation)
609
+ else:
610
+ print("No sample images found. Only analyzing text report.")
611
+
612
+ # Analyze just the text
613
+ text_results = fusion.analyze_text(sample_report)
614
+
615
+ print("\nText Analysis Results:")
616
+ print(
617
+ f"Severity: {text_results['severity']['level']} (Score: {text_results['severity']['score']})"
618
+ )
619
+
620
+ print("\nKey Findings:")
621
+ for finding in text_results["findings"]:
622
+ print(f"- {finding}")
623
+
624
+ print("\nEntities:")
625
+ for category, items in text_results["entities"].items():
626
+ if items:
627
+ print(f"- {category.capitalize()}: {', '.join(items)}")
628
+
629
+ print("\nFollow-up Recommendations:")
630
+ for rec in text_results["followup_recommendations"]:
631
+ print(f"- {rec}")
mediSync/models/text_analyzer.py CHANGED
@@ -1,357 +1,483 @@
1
- import logging
2
- import os
3
- import re
4
-
5
- import torch
6
- from transformers import AutoModelForSequenceClassification, AutoTokenizer, pipeline
7
-
8
- class MedicalReportAnalyzer:
9
-
10
- def __init__(
11
- self,
12
- ner_model="samrawal/bert-base-uncased_medical-ner",
13
- classifier_model="medicalai/ClinicalBERT",
14
- device=None,
15
- ):
16
- self.logger = logging.getLogger(__name__)
17
-
18
- if device is None:
19
- self.device = "cuda" if torch.cuda.is_available() else "cpu"
20
- else:
21
- self.device = device
22
-
23
- self.logger.info(f"Using device: {self.device}")
24
-
25
- hf_token = os.environ.get("HF_TOKEN") or os.environ.get("HUGGING_FACE_HUB_TOKEN")
26
-
27
- try:
28
- self.ner_pipeline = pipeline(
29
- "token-classification",
30
- model=ner_model,
31
- aggregation_strategy="simple",
32
- device=0 if self.device == "cuda" else -1,
33
- token=hf_token,
34
- )
35
- self.logger.info(f"Successfully loaded NER model: {ner_model}")
36
- except Exception as e:
37
- self.logger.error(f"Failed to load NER model: {e}")
38
- self.ner_pipeline = None
39
-
40
- try:
41
- self.tokenizer = AutoTokenizer.from_pretrained(
42
- classifier_model, token=hf_token
43
- )
44
- self.classifier = AutoModelForSequenceClassification.from_pretrained(
45
- classifier_model, token=hf_token
46
- )
47
- self.classifier.to(self.device)
48
- self.classifier.eval()
49
- self.logger.info(
50
- f"Successfully loaded classifier model: {classifier_model}"
51
- )
52
- except Exception as e:
53
- self.logger.error(f"Failed to load classifier model: {e}")
54
- self.classifier = None
55
-
56
- self.severity_levels = {
57
- 0: "Normal",
58
- 1: "Mild",
59
- 2: "Moderate",
60
- 3: "Severe",
61
- 4: "Critical",
62
- }
63
-
64
- self.finding_severity = {
65
- "pneumonia": 3,
66
- "fracture": 3,
67
- "tumor": 4,
68
- "nodule": 2,
69
- "mass": 3,
70
- "edema": 2,
71
- "effusion": 2,
72
- "hemorrhage": 3,
73
- "opacity": 1,
74
- "atelectasis": 2,
75
- "pneumothorax": 3,
76
- "consolidation": 2,
77
- "cardiomegaly": 2,
78
- }
79
-
80
- def extract_entities(self, text):
81
- if not self.ner_pipeline:
82
- self.logger.warning("NER model not available")
83
- return {}
84
-
85
- try:
86
- entities = self.ner_pipeline(text)
87
-
88
- grouped_entities = {
89
- "problem": [],
90
- "test": [],
91
- "treatment": [],
92
- "anatomy": [],
93
- }
94
-
95
- for entity in entities:
96
- entity_type = entity.get("entity_group", "").lower()
97
-
98
- if entity_type in ["problem", "disease", "condition", "diagnosis"]:
99
- category = "problem"
100
- elif entity_type in ["test", "procedure", "examination"]:
101
- category = "test"
102
- elif entity_type in ["treatment", "medication", "drug"]:
103
- category = "treatment"
104
- elif entity_type in ["body_part", "anatomy", "organ"]:
105
- category = "anatomy"
106
- else:
107
- continue
108
-
109
- word = entity.get("word", "")
110
- score = entity.get("score", 0)
111
-
112
- if score > 0.7 and word not in grouped_entities[category]:
113
- grouped_entities[category].append(word)
114
-
115
- return grouped_entities
116
-
117
- except Exception as e:
118
- self.logger.error(f"Error extracting entities: {e}")
119
- return {}
120
-
121
- def assess_severity(self, text):
122
- if not self.classifier:
123
- self.logger.warning("Classifier model not available")
124
- return {"level": "Unknown", "score": 0.0}
125
-
126
- try:
127
- severity_score = 0
128
- confidence = 0.5
129
-
130
- severe_keywords = [
131
- "severe",
132
- "critical",
133
- "urgent",
134
- "emergency",
135
- "immediate attention",
136
- ]
137
- moderate_keywords = ["moderate", "concerning", "follow-up", "monitor"]
138
- mild_keywords = ["mild", "minimal", "slight", "minor"]
139
- normal_keywords = [
140
- "normal",
141
- "unremarkable",
142
- "no abnormalities",
143
- "within normal limits",
144
- ]
145
-
146
- text_lower = text.lower()
147
- severe_count = sum(text_lower.count(word) for word in severe_keywords)
148
- moderate_count = sum(text_lower.count(word) for word in moderate_keywords)
149
- mild_count = sum(text_lower.count(word) for word in mild_keywords)
150
- normal_count = sum(text_lower.count(word) for word in normal_keywords)
151
-
152
- if severe_count > 0:
153
- severity_score += min(severe_count, 2) * 1.5
154
- confidence += 0.1
155
- if moderate_count > 0:
156
- severity_score += min(moderate_count, 3) * 0.75
157
- confidence += 0.05
158
- if mild_count > 0:
159
- severity_score += min(mild_count, 3) * 0.25
160
- confidence += 0.05
161
- if normal_count > 0:
162
- severity_score -= min(normal_count, 3) * 0.75
163
- confidence += 0.1
164
-
165
- for finding, level in self.finding_severity.items():
166
- if finding in text_lower:
167
- severity_score += level * 0.5
168
- confidence += 0.05
169
-
170
- severity_score = max(0, min(4, severity_score))
171
- severity_level = int(round(severity_score))
172
-
173
- severity = self.severity_levels.get(severity_level, "Moderate")
174
-
175
- confidence = min(0.95, confidence)
176
-
177
- return {
178
- "level": severity,
179
- "score": round(severity_score, 1),
180
- "confidence": round(confidence, 2),
181
- }
182
-
183
- except Exception as e:
184
- self.logger.error(f"Error assessing severity: {e}")
185
- return {"level": "Unknown", "score": 0.0, "confidence": 0.0}
186
-
187
- def extract_findings(self, text):
188
- try:
189
- sentences = re.split(r"[.!?]\s+", text)
190
- findings = []
191
-
192
- finding_markers = [
193
- "finding",
194
- "observed",
195
- "noted",
196
- "shows",
197
- "reveals",
198
- "demonstrates",
199
- "indicates",
200
- "evident",
201
- "apparent",
202
- "consistent with",
203
- "suggestive of",
204
- ]
205
-
206
- negation_markers = ["no", "not", "none", "negative", "without", "denies"]
207
-
208
- for sentence in sentences:
209
- if len(sentence.split()) < 3:
210
- continue
211
-
212
- sentence = sentence.strip()
213
-
214
- contains_finding_marker = any(
215
- marker in sentence.lower() for marker in finding_markers
216
- )
217
-
218
- contains_negation = any(
219
- marker in sentence.lower().split() for marker in negation_markers
220
- )
221
-
222
- if contains_finding_marker or (
223
- contains_negation
224
- and any(
225
- term in sentence.lower()
226
- for term in self.finding_severity.keys()
227
- )
228
- ):
229
- findings.append(sentence)
230
-
231
- return findings
232
-
233
- except Exception as e:
234
- self.logger.error(f"Error extracting findings: {e}")
235
- return []
236
-
237
- def suggest_followup(self, text, entities, severity):
238
- try:
239
- followups = []
240
-
241
- severity_level = severity.get("level", "Unknown")
242
- severity_score = severity.get("score", 0)
243
-
244
- problems = entities.get("problem", [])
245
-
246
- followup_mentioned = any(
247
- phrase in text.lower()
248
- for phrase in [
249
- "follow up",
250
- "follow-up",
251
- "followup",
252
- "return",
253
- "refer",
254
- "consult",
255
- ]
256
- )
257
-
258
- if severity_level == "Critical":
259
- followups.append("Immediate specialist consultation recommended.")
260
-
261
- elif severity_level == "Severe":
262
- followups.append("Prompt follow-up with specialist is recommended.")
263
-
264
- for problem in problems:
265
- if "pneumonia" in problem.lower():
266
- followups.append(
267
- "Consider antibiotic therapy and close monitoring."
268
- )
269
- elif "fracture" in problem.lower():
270
- followups.append(
271
- "Orthopedic consultation for treatment planning."
272
- )
273
- elif "mass" in problem.lower() or "tumor" in problem.lower():
274
- followups.append(
275
- "Further imaging and possible biopsy recommended."
276
- )
277
-
278
- elif severity_level == "Moderate":
279
- followups.append("Follow-up with primary care physician recommended.")
280
- if not followup_mentioned and problems:
281
- followups.append(
282
- "Consider additional imaging or tests for further evaluation."
283
- )
284
-
285
- elif severity_level == "Mild":
286
- if problems:
287
- followups.append(
288
- "Routine follow-up with primary care physician as needed."
289
- )
290
- else:
291
- followups.append("No immediate follow-up required.")
292
-
293
- else:
294
- followups.append(
295
- "No specific follow-up indicated based on this report."
296
- )
297
-
298
- for critical_term in ["mass", "tumor", "nodule", "opacity"]:
299
- if (
300
- critical_term in text.lower()
301
- and "follow-up" not in " ".join(followups).lower()
302
- ):
303
- followups.append(
304
- f"Follow-up imaging recommended to monitor {critical_term}."
305
- )
306
- break
307
-
308
- return followups
309
-
310
- except Exception as e:
311
- self.logger.error(f"Error suggesting follow-up: {e}")
312
- return ["Unable to generate follow-up recommendations."]
313
-
314
- def analyze(self, text):
315
- try:
316
- entities = self.extract_entities(text)
317
-
318
- severity = self.assess_severity(text)
319
-
320
- findings = self.extract_findings(text)
321
-
322
- followups = self.suggest_followup(text, entities, severity)
323
-
324
- report = {
325
- "entities": entities,
326
- "severity": severity,
327
- "findings": findings,
328
- "followup_recommendations": followups,
329
- }
330
-
331
- return report
332
-
333
- except Exception as e:
334
- self.logger.error(f"Error analyzing report: {e}")
335
- return {"error": str(e)}
336
-
337
- if __name__ == "__main__":
338
- logging.basicConfig(level=logging.INFO)
339
-
340
- analyzer = MedicalReportAnalyzer()
341
-
342
- sample_report = """
343
- CHEST X-RAY EXAMINATION
344
-
345
- CLINICAL HISTORY: 55-year-old male with cough and fever.
346
-
347
- FINDINGS: The heart size is at the upper limits of normal. The lungs are clear without focal consolidation,
348
- effusion, or pneumothorax. There is mild prominence of the pulmonary vasculature. No pleural effusion is seen.
349
- There is a small nodular opacity noted in the right lower lobe measuring approximately 8mm, which is suspicious
350
- and warrants further investigation. The mediastinum is unremarkable. The visualized bony structures show no acute abnormalities.
351
-
352
- IMPRESSION:
353
- 1. Mild cardiomegaly.
354
- 2. 8mm nodular opacity in the right lower lobe, recommend follow-up CT for further evaluation.
355
- 3. No acute pulmonary parenchymal abnormality.
356
-
357
- RECOMMENDATIONS: Follow-up chest CT to further characterize the nodular opacity in the right lower lobe.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import os
3
+ import re
4
+
5
+ import torch
6
+ from transformers import AutoModelForSequenceClassification, AutoTokenizer, pipeline
7
+
8
+
9
+ class MedicalReportAnalyzer:
10
+ """
11
+ A class for analyzing medical text reports using pre-trained NLP models from Hugging Face.
12
+
13
+ This analyzer can:
14
+ 1. Extract medical entities (conditions, treatments, tests)
15
+ 2. Classify report severity
16
+ 3. Extract key findings
17
+ 4. Identify suggested follow-up actions
18
+ """
19
+
20
+ def __init__(
21
+ self,
22
+ ner_model="samrawal/bert-base-uncased_medical-ner",
23
+ classifier_model="medicalai/ClinicalBERT",
24
+ device=None,
25
+ ):
26
+ """
27
+ Initialize the text analyzer with specific pre-trained models.
28
+
29
+ Args:
30
+ ner_model (str): Model for named entity recognition
31
+ classifier_model (str): Model for text classification
32
+ device (str, optional): Device to run models on ('cuda' or 'cpu')
33
+ """
34
+ self.logger = logging.getLogger(__name__)
35
+
36
+ # Determine device
37
+ if device is None:
38
+ self.device = "cuda" if torch.cuda.is_available() else "cpu"
39
+ else:
40
+ self.device = device
41
+
42
+ self.logger.info(f"Using device: {self.device}")
43
+
44
+ # Use HF_TOKEN from environment if available (for Hugging Face Spaces)
45
+ hf_token = os.environ.get("HF_TOKEN") or os.environ.get("HUGGING_FACE_HUB_TOKEN")
46
+
47
+ # Load NER model for entity extraction
48
+ try:
49
+ self.ner_pipeline = pipeline(
50
+ "token-classification",
51
+ model=ner_model,
52
+ aggregation_strategy="simple",
53
+ device=0 if self.device == "cuda" else -1,
54
+ token=hf_token,
55
+ )
56
+ self.logger.info(f"Successfully loaded NER model: {ner_model}")
57
+ except Exception as e:
58
+ self.logger.error(f"Failed to load NER model: {e}")
59
+ self.ner_pipeline = None
60
+
61
+ # Load classifier model for severity assessment
62
+ try:
63
+ self.tokenizer = AutoTokenizer.from_pretrained(
64
+ classifier_model, token=hf_token
65
+ )
66
+ self.classifier = AutoModelForSequenceClassification.from_pretrained(
67
+ classifier_model, token=hf_token
68
+ )
69
+ self.classifier.to(self.device)
70
+ self.classifier.eval()
71
+ self.logger.info(
72
+ f"Successfully loaded classifier model: {classifier_model}"
73
+ )
74
+ except Exception as e:
75
+ self.logger.error(f"Failed to load classifier model: {e}")
76
+ self.classifier = None
77
+
78
+ # Severity levels mapping
79
+ self.severity_levels = {
80
+ 0: "Normal",
81
+ 1: "Mild",
82
+ 2: "Moderate",
83
+ 3: "Severe",
84
+ 4: "Critical",
85
+ }
86
+
87
+ # Common medical findings and their severity levels
88
+ self.finding_severity = {
89
+ "pneumonia": 3,
90
+ "fracture": 3,
91
+ "tumor": 4,
92
+ "nodule": 2,
93
+ "mass": 3,
94
+ "edema": 2,
95
+ "effusion": 2,
96
+ "hemorrhage": 3,
97
+ "opacity": 1,
98
+ "atelectasis": 2,
99
+ "pneumothorax": 3,
100
+ "consolidation": 2,
101
+ "cardiomegaly": 2,
102
+ }
103
+
104
+ def extract_entities(self, text):
105
+ """
106
+ Extract medical entities from the report text.
107
+
108
+ Args:
109
+ text (str): Medical report text
110
+
111
+ Returns:
112
+ dict: Dictionary of entity lists by category
113
+ """
114
+ if not self.ner_pipeline:
115
+ self.logger.warning("NER model not available")
116
+ return {}
117
+
118
+ try:
119
+ # Run NER
120
+ entities = self.ner_pipeline(text)
121
+
122
+ # Group entities by type
123
+ grouped_entities = {
124
+ "problem": [], # Medical conditions
125
+ "test": [], # Tests/procedures
126
+ "treatment": [], # Treatments/medications
127
+ "anatomy": [], # Anatomical locations
128
+ }
129
+
130
+ for entity in entities:
131
+ entity_type = entity.get("entity_group", "").lower()
132
+
133
+ # Map entity types to our categories
134
+ if entity_type in ["problem", "disease", "condition", "diagnosis"]:
135
+ category = "problem"
136
+ elif entity_type in ["test", "procedure", "examination"]:
137
+ category = "test"
138
+ elif entity_type in ["treatment", "medication", "drug"]:
139
+ category = "treatment"
140
+ elif entity_type in ["body_part", "anatomy", "organ"]:
141
+ category = "anatomy"
142
+ else:
143
+ continue # Skip other entity types
144
+
145
+ word = entity.get("word", "")
146
+ score = entity.get("score", 0)
147
+
148
+ # Only include if confidence is reasonable
149
+ if score > 0.7 and word not in grouped_entities[category]:
150
+ grouped_entities[category].append(word)
151
+
152
+ return grouped_entities
153
+
154
+ except Exception as e:
155
+ self.logger.error(f"Error extracting entities: {e}")
156
+ return {}
157
+
158
+ def assess_severity(self, text):
159
+ """
160
+ Assess the severity level of the medical report.
161
+
162
+ Args:
163
+ text (str): Medical report text
164
+
165
+ Returns:
166
+ dict: Severity assessment including level and confidence
167
+ """
168
+ if not self.classifier:
169
+ self.logger.warning("Classifier model not available")
170
+ return {"level": "Unknown", "score": 0.0}
171
+
172
+ try:
173
+ # Use rule-based approach along with model
174
+ severity_score = 0
175
+ confidence = 0.5 # Start with neutral confidence
176
+
177
+ # Check for severe keywords
178
+ severe_keywords = [
179
+ "severe",
180
+ "critical",
181
+ "urgent",
182
+ "emergency",
183
+ "immediate attention",
184
+ ]
185
+ moderate_keywords = ["moderate", "concerning", "follow-up", "monitor"]
186
+ mild_keywords = ["mild", "minimal", "slight", "minor"]
187
+ normal_keywords = [
188
+ "normal",
189
+ "unremarkable",
190
+ "no abnormalities",
191
+ "within normal limits",
192
+ ]
193
+
194
+ # Count keyword occurrences
195
+ text_lower = text.lower()
196
+ severe_count = sum(text_lower.count(word) for word in severe_keywords)
197
+ moderate_count = sum(text_lower.count(word) for word in moderate_keywords)
198
+ mild_count = sum(text_lower.count(word) for word in mild_keywords)
199
+ normal_count = sum(text_lower.count(word) for word in normal_keywords)
200
+
201
+ # Adjust severity based on keyword counts
202
+ if severe_count > 0:
203
+ severity_score += min(severe_count, 2) * 1.5
204
+ confidence += 0.1
205
+ if moderate_count > 0:
206
+ severity_score += min(moderate_count, 3) * 0.75
207
+ confidence += 0.05
208
+ if mild_count > 0:
209
+ severity_score += min(mild_count, 3) * 0.25
210
+ confidence += 0.05
211
+ if normal_count > 0:
212
+ severity_score -= min(normal_count, 3) * 0.75
213
+ confidence += 0.1
214
+
215
+ # Check for specific medical findings
216
+ for finding, level in self.finding_severity.items():
217
+ if finding in text_lower:
218
+ severity_score += level * 0.5
219
+ confidence += 0.05
220
+
221
+ # Normalize severity score to 0-4 range
222
+ severity_score = max(0, min(4, severity_score))
223
+ severity_level = int(round(severity_score))
224
+
225
+ # Map to severity level
226
+ severity = self.severity_levels.get(severity_level, "Moderate")
227
+
228
+ # Cap confidence at 0.95
229
+ confidence = min(0.95, confidence)
230
+
231
+ return {
232
+ "level": severity,
233
+ "score": round(severity_score, 1),
234
+ "confidence": round(confidence, 2),
235
+ }
236
+
237
+ except Exception as e:
238
+ self.logger.error(f"Error assessing severity: {e}")
239
+ return {"level": "Unknown", "score": 0.0, "confidence": 0.0}
240
+
241
+ def extract_findings(self, text):
242
+ """
243
+ Extract key clinical findings from the report.
244
+
245
+ Args:
246
+ text (str): Medical report text
247
+
248
+ Returns:
249
+ list: List of key findings
250
+ """
251
+ try:
252
+ # Split text into sentences
253
+ sentences = re.split(r"[.!?]\s+", text)
254
+ findings = []
255
+
256
+ # Key phrases that often introduce findings
257
+ finding_markers = [
258
+ "finding",
259
+ "observed",
260
+ "noted",
261
+ "shows",
262
+ "reveals",
263
+ "demonstrates",
264
+ "indicates",
265
+ "evident",
266
+ "apparent",
267
+ "consistent with",
268
+ "suggestive of",
269
+ ]
270
+
271
+ # Negative markers
272
+ negation_markers = ["no", "not", "none", "negative", "without", "denies"]
273
+
274
+ for sentence in sentences:
275
+ # Skip very short sentences
276
+ if len(sentence.split()) < 3:
277
+ continue
278
+
279
+ sentence = sentence.strip()
280
+
281
+ # Check if this sentence likely contains a finding
282
+ contains_finding_marker = any(
283
+ marker in sentence.lower() for marker in finding_markers
284
+ )
285
+
286
+ # Check for negation
287
+ contains_negation = any(
288
+ marker in sentence.lower().split() for marker in negation_markers
289
+ )
290
+
291
+ # Only include positive findings or explicitly negated findings that are important
292
+ if contains_finding_marker or (
293
+ contains_negation
294
+ and any(
295
+ term in sentence.lower()
296
+ for term in self.finding_severity.keys()
297
+ )
298
+ ):
299
+ findings.append(sentence)
300
+
301
+ return findings
302
+
303
+ except Exception as e:
304
+ self.logger.error(f"Error extracting findings: {e}")
305
+ return []
306
+
307
+ def suggest_followup(self, text, entities, severity):
308
+ """
309
+ Suggest follow-up actions based on report analysis.
310
+
311
+ Args:
312
+ text (str): Medical report text
313
+ entities (dict): Extracted entities
314
+ severity (dict): Severity assessment
315
+
316
+ Returns:
317
+ list: Suggested follow-up actions
318
+ """
319
+ try:
320
+ followups = []
321
+
322
+ # Base recommendations on severity
323
+ severity_level = severity.get("level", "Unknown")
324
+ severity_score = severity.get("score", 0)
325
+
326
+ # Extract problems from entities
327
+ problems = entities.get("problem", [])
328
+
329
+ # Check if follow-up is already mentioned in the text
330
+ followup_mentioned = any(
331
+ phrase in text.lower()
332
+ for phrase in [
333
+ "follow up",
334
+ "follow-up",
335
+ "followup",
336
+ "return",
337
+ "refer",
338
+ "consult",
339
+ ]
340
+ )
341
+
342
+ # Default recommendations based on severity
343
+ if severity_level == "Critical":
344
+ followups.append("Immediate specialist consultation recommended.")
345
+
346
+ elif severity_level == "Severe":
347
+ followups.append("Prompt follow-up with specialist is recommended.")
348
+
349
+ # Add specific recommendations for common severe conditions
350
+ for problem in problems:
351
+ if "pneumonia" in problem.lower():
352
+ followups.append(
353
+ "Consider antibiotic therapy and close monitoring."
354
+ )
355
+ elif "fracture" in problem.lower():
356
+ followups.append(
357
+ "Orthopedic consultation for treatment planning."
358
+ )
359
+ elif "mass" in problem.lower() or "tumor" in problem.lower():
360
+ followups.append(
361
+ "Further imaging and possible biopsy recommended."
362
+ )
363
+
364
+ elif severity_level == "Moderate":
365
+ followups.append("Follow-up with primary care physician recommended.")
366
+ if not followup_mentioned and problems:
367
+ followups.append(
368
+ "Consider additional imaging or tests for further evaluation."
369
+ )
370
+
371
+ elif severity_level == "Mild":
372
+ if problems:
373
+ followups.append(
374
+ "Routine follow-up with primary care physician as needed."
375
+ )
376
+ else:
377
+ followups.append("No immediate follow-up required.")
378
+
379
+ else: # Normal
380
+ followups.append(
381
+ "No specific follow-up indicated based on this report."
382
+ )
383
+
384
+ # Check for specific findings that always need follow-up
385
+ for critical_term in ["mass", "tumor", "nodule", "opacity"]:
386
+ if (
387
+ critical_term in text.lower()
388
+ and "follow-up" not in " ".join(followups).lower()
389
+ ):
390
+ followups.append(
391
+ f"Follow-up imaging recommended to monitor {critical_term}."
392
+ )
393
+ break
394
+
395
+ return followups
396
+
397
+ except Exception as e:
398
+ self.logger.error(f"Error suggesting follow-up: {e}")
399
+ return ["Unable to generate follow-up recommendations."]
400
+
401
+ def analyze(self, text):
402
+ """
403
+ Perform comprehensive analysis of medical report text.
404
+
405
+ Args:
406
+ text (str): Medical report text
407
+
408
+ Returns:
409
+ dict: Complete analysis results
410
+ """
411
+ try:
412
+ # Extract entities
413
+ entities = self.extract_entities(text)
414
+
415
+ # Assess severity
416
+ severity = self.assess_severity(text)
417
+
418
+ # Extract key findings
419
+ findings = self.extract_findings(text)
420
+
421
+ # Generate follow-up suggestions
422
+ followups = self.suggest_followup(text, entities, severity)
423
+
424
+ # Create detailed report
425
+ report = {
426
+ "entities": entities,
427
+ "severity": severity,
428
+ "findings": findings,
429
+ "followup_recommendations": followups,
430
+ }
431
+
432
+ return report
433
+
434
+ except Exception as e:
435
+ self.logger.error(f"Error analyzing report: {e}")
436
+ return {"error": str(e)}
437
+
438
+
439
+ # Example usage
440
+ if __name__ == "__main__":
441
+ # Set up logging
442
+ logging.basicConfig(level=logging.INFO)
443
+
444
+ # Test on a sample report
445
+ analyzer = MedicalReportAnalyzer()
446
+
447
+ sample_report = """
448
+ CHEST X-RAY EXAMINATION
449
+
450
+ CLINICAL HISTORY: 55-year-old male with cough and fever.
451
+
452
+ FINDINGS: The heart size is at the upper limits of normal. The lungs are clear without focal consolidation,
453
+ effusion, or pneumothorax. There is mild prominence of the pulmonary vasculature. No pleural effusion is seen.
454
+ There is a small nodular opacity noted in the right lower lobe measuring approximately 8mm, which is suspicious
455
+ and warrants further investigation. The mediastinum is unremarkable. The visualized bony structures show no acute abnormalities.
456
+
457
+ IMPRESSION:
458
+ 1. Mild cardiomegaly.
459
+ 2. 8mm nodular opacity in the right lower lobe, recommend follow-up CT for further evaluation.
460
+ 3. No acute pulmonary parenchymal abnormality.
461
+
462
+ RECOMMENDATIONS: Follow-up chest CT to further characterize the nodular opacity in the right lower lobe.
463
+ """
464
+
465
+ results = analyzer.analyze(sample_report)
466
+
467
+ print("\nMedical Report Analysis:")
468
+ print(
469
+ f"\nSeverity: {results['severity']['level']} (Score: {results['severity']['score']})"
470
+ )
471
+
472
+ print("\nKey Findings:")
473
+ for finding in results["findings"]:
474
+ print(f"- {finding}")
475
+
476
+ print("\nEntities:")
477
+ for category, items in results["entities"].items():
478
+ if items:
479
+ print(f"- {category.capitalize()}: {', '.join(items)}")
480
+
481
+ print("\nFollow-up Recommendations:")
482
+ for rec in results["followup_recommendations"]:
483
+ print(f"- {rec}")
mediSync/utils/__init__.py CHANGED
@@ -1,27 +1,38 @@
1
- from .preprocessing import (
2
- enhance_xray_image,
3
- extract_measurements,
4
- extract_sections,
5
- normalize_report_text,
6
- preprocess_image,
7
- )
8
- from .visualization import (
9
- create_heatmap_overlay,
10
- figure_to_base64,
11
- plot_image_prediction,
12
- plot_multimodal_results,
13
- plot_report_entities,
14
- )
15
-
16
- __all__ = [
17
- "preprocess_image",
18
- "normalize_report_text",
19
- "enhance_xray_image",
20
- "extract_sections",
21
- "extract_measurements",
22
- "plot_image_prediction",
23
- "plot_report_entities",
24
- "plot_multimodal_results",
25
- "create_heatmap_overlay",
26
- "figure_to_base64",
27
- ]
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ U-HRES: Utils Module
3
+ ===================
4
+
5
+ This module contains utility functions for the U-HRES system:
6
+
7
+ 1. preprocessing: Functions for preprocessing images and text
8
+ 2. visualization: Functions for visualizing analysis results
9
+ 3. download_samples: Functions for downloading sample data
10
+ """
11
+
12
+ from .preprocessing import (
13
+ enhance_xray_image,
14
+ extract_measurements,
15
+ extract_sections,
16
+ normalize_report_text,
17
+ preprocess_image,
18
+ )
19
+ from .visualization import (
20
+ create_heatmap_overlay,
21
+ figure_to_base64,
22
+ plot_image_prediction,
23
+ plot_multimodal_results,
24
+ plot_report_entities,
25
+ )
26
+
27
+ __all__ = [
28
+ "preprocess_image",
29
+ "normalize_report_text",
30
+ "enhance_xray_image",
31
+ "extract_sections",
32
+ "extract_measurements",
33
+ "plot_image_prediction",
34
+ "plot_report_entities",
35
+ "plot_multimodal_results",
36
+ "create_heatmap_overlay",
37
+ "figure_to_base64",
38
+ ]
mediSync/utils/download_samples.py CHANGED
@@ -1,101 +1,135 @@
1
- import logging
2
- import urllib.request
3
- from pathlib import Path
4
-
5
- logging.basicConfig(
6
- level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
7
- )
8
- logger = logging.getLogger(__name__)
9
-
10
- SAMPLE_IMAGES = [
11
- {
12
- "url": "https://prod-images-static.radiopaedia.org/images/53448173/322830a37f0fa0852773ca2db3e8d8_big_gallery.jpeg",
13
- "filename": "normal_chest_xray.jpg",
14
- "description": "Normal chest X-ray",
15
- },
16
- {
17
- "url": "https://prod-images-static.radiopaedia.org/images/52465460/e4d8791bd7502ab72af8d9e5c322db_big_gallery.jpg",
18
- "filename": "pneumonia_xray.jpg",
19
- "description": "X-ray with pneumonia",
20
- },
21
- {
22
- "url": "https://prod-images-static.radiopaedia.org/images/556520/cf17c05750adb04b2a6e23afb47c7d_big_gallery.jpg",
23
- "filename": "cardiomegaly_xray.jpg",
24
- "description": "X-ray with cardiomegaly",
25
- },
26
- {
27
- "url": "https://prod-images-static.radiopaedia.org/images/19972291/41eed1a2cdad06d26c3f415a6ed65a_big_gallery.jpeg",
28
- "filename": "nodule_xray.jpg",
29
- "description": "X-ray with lung nodule",
30
- },
31
- ]
32
-
33
- def download_sample_images(output_dir="data/sample"):
34
- script_dir = Path(__file__).resolve().parent.parent
35
-
36
- output_path = script_dir / output_dir
37
- output_path.mkdir(parents=True, exist_ok=True)
38
-
39
- downloaded_paths = []
40
-
41
- for image in SAMPLE_IMAGES:
42
- try:
43
- filename = image["filename"]
44
- url = image["url"]
45
- output_file = output_path / filename
46
-
47
- if output_file.exists():
48
- logger.info(f"File already exists: {output_file}")
49
- downloaded_paths.append(str(output_file))
50
- continue
51
-
52
- logger.info(f"Downloading {url} to {output_file}")
53
-
54
- opener = urllib.request.build_opener()
55
- opener.addheaders = [("User-Agent", "Mozilla/5.0")]
56
- urllib.request.install_opener(opener)
57
-
58
- urllib.request.urlretrieve(url, output_file)
59
-
60
- logger.info(f"Successfully downloaded {filename}")
61
- downloaded_paths.append(str(output_file))
62
-
63
- except Exception as e:
64
- logger.error(f"Error downloading {image['url']}: {e}")
65
-
66
- logger.info(
67
- f"Downloaded {len(downloaded_paths)} out of {len(SAMPLE_IMAGES)} images"
68
- )
69
- return downloaded_paths
70
-
71
- def create_sample_info_file(output_dir="data/sample"):
72
- script_dir = Path(__file__).resolve().parent.parent
73
-
74
- output_path = script_dir / output_dir
75
- info_file = output_path / "sample_info.txt"
76
-
77
- with open(info_file, "w") as f:
78
- f.write("# Sample X-ray Images\n\n")
79
-
80
- for image in SAMPLE_IMAGES:
81
- f.write(f"## {image['filename']}\n")
82
- f.write(f"Description: {image['description']}\n")
83
- f.write(f"Source: {image['url']}\n\n")
84
-
85
- f.write(
86
- "\nThese images are used for testing and demonstration purposes only.\n"
87
- )
88
- f.write(
89
- "Please note that these images are from public medical education sources.\n"
90
- )
91
- f.write("Do not use for clinical decision making.\n")
92
-
93
- logger.info(f"Created sample info file: {info_file}")
94
-
95
- if __name__ == "__main__":
96
- downloaded_paths = download_sample_images()
97
-
98
- create_sample_info_file()
99
-
100
- print(f"Downloaded {len(downloaded_paths)} sample images.")
101
- print("Run the application with: python app.py")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import urllib.request
3
+ from pathlib import Path
4
+
5
+ # Set up logging
6
+ logging.basicConfig(
7
+ level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
8
+ )
9
+ logger = logging.getLogger(__name__)
10
+
11
+ # Sample X-ray image URLs (from public sources)
12
+ SAMPLE_IMAGES = [
13
+ # Normal chest X-ray
14
+ {
15
+ "url": "https://prod-images-static.radiopaedia.org/images/53448173/322830a37f0fa0852773ca2db3e8d8_big_gallery.jpeg",
16
+ "filename": "normal_chest_xray.jpg",
17
+ "description": "Normal chest X-ray",
18
+ },
19
+ # X-ray with pneumonia
20
+ {
21
+ "url": "https://prod-images-static.radiopaedia.org/images/52465460/e4d8791bd7502ab72af8d9e5c322db_big_gallery.jpg",
22
+ "filename": "pneumonia_xray.jpg",
23
+ "description": "X-ray with pneumonia",
24
+ },
25
+ # X-ray with cardiomegaly
26
+ {
27
+ "url": "https://prod-images-static.radiopaedia.org/images/556520/cf17c05750adb04b2a6e23afb47c7d_big_gallery.jpg",
28
+ "filename": "cardiomegaly_xray.jpg",
29
+ "description": "X-ray with cardiomegaly",
30
+ },
31
+ # X-ray with lung nodule
32
+ {
33
+ "url": "https://prod-images-static.radiopaedia.org/images/19972291/41eed1a2cdad06d26c3f415a6ed65a_big_gallery.jpeg",
34
+ "filename": "nodule_xray.jpg",
35
+ "description": "X-ray with lung nodule",
36
+ },
37
+ ]
38
+
39
+
40
+ def download_sample_images(output_dir="data/sample"):
41
+ """
42
+ Download sample X-ray images for testing.
43
+
44
+ Args:
45
+ output_dir (str): Directory to save images
46
+
47
+ Returns:
48
+ list: Paths to downloaded images
49
+ """
50
+ # Get the directory of the script
51
+ script_dir = Path(__file__).resolve().parent.parent
52
+
53
+ # Create output directory if it doesn't exist
54
+ output_path = script_dir / output_dir
55
+ output_path.mkdir(parents=True, exist_ok=True)
56
+
57
+ downloaded_paths = []
58
+
59
+ for image in SAMPLE_IMAGES:
60
+ try:
61
+ filename = image["filename"]
62
+ url = image["url"]
63
+ output_file = output_path / filename
64
+
65
+ # Skip if file already exists
66
+ if output_file.exists():
67
+ logger.info(f"File already exists: {output_file}")
68
+ downloaded_paths.append(str(output_file))
69
+ continue
70
+
71
+ # Download the image
72
+ logger.info(f"Downloading {url} to {output_file}")
73
+
74
+ # Set a user agent to avoid blocking
75
+ opener = urllib.request.build_opener()
76
+ opener.addheaders = [("User-Agent", "Mozilla/5.0")]
77
+ urllib.request.install_opener(opener)
78
+
79
+ # Download the file
80
+ urllib.request.urlretrieve(url, output_file)
81
+
82
+ logger.info(f"Successfully downloaded {filename}")
83
+ downloaded_paths.append(str(output_file))
84
+
85
+ except Exception as e:
86
+ logger.error(f"Error downloading {image['url']}: {e}")
87
+
88
+ logger.info(
89
+ f"Downloaded {len(downloaded_paths)} out of {len(SAMPLE_IMAGES)} images"
90
+ )
91
+ return downloaded_paths
92
+
93
+
94
+ def create_sample_info_file(output_dir="data/sample"):
95
+ """
96
+ Create a text file with information about the sample images.
97
+
98
+ Args:
99
+ output_dir (str): Directory with sample images
100
+ """
101
+ # Get the directory of the script
102
+ script_dir = Path(__file__).resolve().parent.parent
103
+
104
+ # Output path
105
+ output_path = script_dir / output_dir
106
+ info_file = output_path / "sample_info.txt"
107
+
108
+ with open(info_file, "w") as f:
109
+ f.write("# Sample X-ray Images\n\n")
110
+
111
+ for image in SAMPLE_IMAGES:
112
+ f.write(f"## {image['filename']}\n")
113
+ f.write(f"Description: {image['description']}\n")
114
+ f.write(f"Source: {image['url']}\n\n")
115
+
116
+ f.write(
117
+ "\nThese images are used for testing and demonstration purposes only.\n"
118
+ )
119
+ f.write(
120
+ "Please note that these images are from public medical education sources.\n"
121
+ )
122
+ f.write("Do not use for clinical decision making.\n")
123
+
124
+ logger.info(f"Created sample info file: {info_file}")
125
+
126
+
127
+ if __name__ == "__main__":
128
+ # Download sample images
129
+ downloaded_paths = download_sample_images()
130
+
131
+ # Create info file
132
+ create_sample_info_file()
133
+
134
+ print(f"Downloaded {len(downloaded_paths)} sample images.")
135
+ print("Run the application with: python app.py")
mediSync/utils/preprocessing.py CHANGED
@@ -1,172 +1,262 @@
1
- import logging
2
- import os
3
- import re
4
-
5
- import cv2
6
- from PIL import Image
7
-
8
- logger = logging.getLogger(__name__)
9
-
10
- def preprocess_image(image_path, target_size=(224, 224)):
11
- try:
12
- if not os.path.exists(image_path):
13
- raise FileNotFoundError(f"Image file not found: {image_path}")
14
-
15
- image = Image.open(image_path)
16
-
17
- if image.mode != "RGB":
18
- image = image.convert("RGB")
19
-
20
- image = image.resize(target_size, Image.LANCZOS)
21
-
22
- return image
23
-
24
- except Exception as e:
25
- logger.error(f"Error preprocessing image: {e}")
26
- raise
27
-
28
- def enhance_xray_image(image_path, output_path=None, clahe_clip=2.0, clahe_grid=(8, 8)):
29
- try:
30
- img = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
31
-
32
- if img is None:
33
- raise ValueError(f"Failed to read image: {image_path}")
34
-
35
- clahe = cv2.createCLAHE(clipLimit=clahe_clip, tileGridSize=clahe_grid)
36
-
37
- enhanced = clahe.apply(img)
38
-
39
- if output_path:
40
- cv2.imwrite(output_path, enhanced)
41
- return output_path
42
- else:
43
- return enhanced
44
-
45
- except Exception as e:
46
- logger.error(f"Error enhancing X-ray image: {e}")
47
- raise
48
-
49
- def normalize_report_text(text):
50
- try:
51
- text = re.sub(r"\s+", " ", text)
52
-
53
- section_patterns = {
54
- r"(?i)clinical\s*(?:history|indication)": "CLINICAL HISTORY:",
55
- r"(?i)technique": "TECHNIQUE:",
56
- r"(?i)comparison": "COMPARISON:",
57
- r"(?i)findings": "FINDINGS:",
58
- r"(?i)impression": "IMPRESSION:",
59
- r"(?i)recommendation": "RECOMMENDATION:",
60
- r"(?i)comment": "COMMENT:",
61
- }
62
-
63
- for pattern, replacement in section_patterns.items():
64
- text = re.sub(pattern + r"\s*:", replacement, text)
65
-
66
- abbrev_patterns = {
67
- r"(?i)\bw\/\b": "with",
68
- r"(?i)\bw\/o\b": "without",
69
- r"(?i)\bs\/p\b": "status post",
70
- r"(?i)\bc\/w\b": "consistent with",
71
- r"(?i)\br\/o\b": "rule out",
72
- r"(?i)\bhx\b": "history",
73
- r"(?i)\bdx\b": "diagnosis",
74
- r"(?i)\btx\b": "treatment",
75
- }
76
-
77
- for pattern, replacement in abbrev_patterns.items():
78
- text = re.sub(pattern, replacement, text)
79
-
80
- return text.strip()
81
-
82
- except Exception as e:
83
- logger.error(f"Error normalizing report text: {e}")
84
- return text
85
-
86
- def extract_sections(text):
87
- try:
88
- normalized_text = normalize_report_text(text)
89
-
90
- section_headers = [
91
- "CLINICAL HISTORY:",
92
- "TECHNIQUE:",
93
- "COMPARISON:",
94
- "FINDINGS:",
95
- "IMPRESSION:",
96
- "RECOMMENDATION:",
97
- ]
98
-
99
- sections = {}
100
- current_section = "PREAMBLE"
101
- sections[current_section] = []
102
-
103
- for line in normalized_text.split("\n"):
104
- section_found = False
105
-
106
- for header in section_headers:
107
- if header in line:
108
- current_section = header.rstrip(":")
109
- sections[current_section] = []
110
- section_found = True
111
- content = line.split(header, 1)[1].strip()
112
- if content:
113
- sections[current_section].append(content)
114
- break
115
-
116
- if not section_found and current_section:
117
- sections[current_section].append(line)
118
-
119
- for section, lines in sections.items():
120
- sections[section] = " ".join(lines).strip()
121
-
122
- sections = {k: v for k, v in sections.items() if v}
123
-
124
- return sections
125
-
126
- except Exception as e:
127
- logger.error(f"Error extracting sections: {e}")
128
- return {"FULL_TEXT": text}
129
-
130
- def extract_measurements(text):
131
- try:
132
- size_pattern = r"(\d+(?:\.\d+)?(?:\s*[x×]\s*\d+(?:\.\d+)?)?(?:\s*[x×]\s*\d+(?:\.\d+)?)?)\s*(mm|cm|mm2|cm2|mm3|cm3|ml|cc)"
133
-
134
- context_pattern = (
135
- r"([A-Za-z\s]+(?:mass|nodule|effusion|opacity|lesion|tumor|cyst|structure|area|region)[A-Za-z\s]*)"
136
- + size_pattern
137
- )
138
-
139
- context_measurements = []
140
- for match in re.finditer(context_pattern, text, re.IGNORECASE):
141
- context, size, unit = match.groups()
142
- context_measurements.append((context.strip(), size, unit))
143
-
144
- all_measurements = []
145
- for match in re.finditer(size_pattern, text):
146
- size, unit = match.groups()
147
- all_measurements.append((size, unit))
148
-
149
- return context_measurements
150
-
151
- except Exception as e:
152
- logger.error(f"Error extracting measurements: {e}")
153
- return []
154
-
155
- def prepare_sample_batch(image_paths, reports=None, target_size=(224, 224)):
156
- try:
157
- processed_images = []
158
- processed_reports = []
159
-
160
- for i, image_path in enumerate(image_paths):
161
- image = preprocess_image(image_path, target_size)
162
- processed_images.append(image)
163
-
164
- if reports and i < len(reports):
165
- normalized_report = normalize_report_text(reports[i])
166
- processed_reports.append(normalized_report)
167
-
168
- return processed_images, processed_reports if reports else None
169
-
170
- except Exception as e:
171
- logger.error(f"Error preparing sample batch: {e}")
172
- raise
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import os
3
+ import re
4
+
5
+ import cv2
6
+ from PIL import Image
7
+
8
+ # Set up logging
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ def preprocess_image(image_path, target_size=(224, 224)):
13
+ """
14
+ Preprocess X-ray image for model input.
15
+
16
+ Args:
17
+ image_path (str): Path to the X-ray image
18
+ target_size (tuple): Target size for resizing
19
+
20
+ Returns:
21
+ PIL.Image: Preprocessed image
22
+ """
23
+ try:
24
+ # Check if file exists
25
+ if not os.path.exists(image_path):
26
+ raise FileNotFoundError(f"Image file not found: {image_path}")
27
+
28
+ # Load image
29
+ image = Image.open(image_path)
30
+
31
+ # Convert grayscale to RGB if needed
32
+ if image.mode != "RGB":
33
+ image = image.convert("RGB")
34
+
35
+ # Resize image
36
+ image = image.resize(target_size, Image.LANCZOS)
37
+
38
+ return image
39
+
40
+ except Exception as e:
41
+ logger.error(f"Error preprocessing image: {e}")
42
+ raise
43
+
44
+
45
+ def enhance_xray_image(image_path, output_path=None, clahe_clip=2.0, clahe_grid=(8, 8)):
46
+ """
47
+ Enhance X-ray image contrast using CLAHE (Contrast Limited Adaptive Histogram Equalization).
48
+
49
+ Args:
50
+ image_path (str): Path to the X-ray image
51
+ output_path (str, optional): Path to save enhanced image
52
+ clahe_clip (float): Clip limit for CLAHE
53
+ clahe_grid (tuple): Grid size for CLAHE
54
+
55
+ Returns:
56
+ str or np.ndarray: Path to enhanced image or image array
57
+ """
58
+ try:
59
+ # Read image
60
+ img = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
61
+
62
+ if img is None:
63
+ raise ValueError(f"Failed to read image: {image_path}")
64
+
65
+ # Create CLAHE object
66
+ clahe = cv2.createCLAHE(clipLimit=clahe_clip, tileGridSize=clahe_grid)
67
+
68
+ # Apply CLAHE
69
+ enhanced = clahe.apply(img)
70
+
71
+ # Save enhanced image if output path is provided
72
+ if output_path:
73
+ cv2.imwrite(output_path, enhanced)
74
+ return output_path
75
+ else:
76
+ return enhanced
77
+
78
+ except Exception as e:
79
+ logger.error(f"Error enhancing X-ray image: {e}")
80
+ raise
81
+
82
+
83
+ def normalize_report_text(text):
84
+ """
85
+ Normalize medical report text for consistent processing.
86
+
87
+ Args:
88
+ text (str): Medical report text
89
+
90
+ Returns:
91
+ str: Normalized text
92
+ """
93
+ try:
94
+ # Remove multiple whitespaces
95
+ text = re.sub(r"\s+", " ", text)
96
+
97
+ # Standardize section headers
98
+ section_patterns = {
99
+ r"(?i)clinical\s*(?:history|indication)": "CLINICAL HISTORY:",
100
+ r"(?i)technique": "TECHNIQUE:",
101
+ r"(?i)comparison": "COMPARISON:",
102
+ r"(?i)findings": "FINDINGS:",
103
+ r"(?i)impression": "IMPRESSION:",
104
+ r"(?i)recommendation": "RECOMMENDATION:",
105
+ r"(?i)comment": "COMMENT:",
106
+ }
107
+
108
+ for pattern, replacement in section_patterns.items():
109
+ text = re.sub(pattern + r"\s*:", replacement, text)
110
+
111
+ # Standardize common abbreviations
112
+ abbrev_patterns = {
113
+ r"(?i)\bw\/\b": "with",
114
+ r"(?i)\bw\/o\b": "without",
115
+ r"(?i)\bs\/p\b": "status post",
116
+ r"(?i)\bc\/w\b": "consistent with",
117
+ r"(?i)\br\/o\b": "rule out",
118
+ r"(?i)\bhx\b": "history",
119
+ r"(?i)\bdx\b": "diagnosis",
120
+ r"(?i)\btx\b": "treatment",
121
+ }
122
+
123
+ for pattern, replacement in abbrev_patterns.items():
124
+ text = re.sub(pattern, replacement, text)
125
+
126
+ return text.strip()
127
+
128
+ except Exception as e:
129
+ logger.error(f"Error normalizing report text: {e}")
130
+ return text # Return original text if normalization fails
131
+
132
+
133
+ def extract_sections(text):
134
+ """
135
+ Extract sections from a medical report.
136
+
137
+ Args:
138
+ text (str): Medical report text
139
+
140
+ Returns:
141
+ dict: Dictionary of extracted sections
142
+ """
143
+ try:
144
+ # Normalize text first
145
+ normalized_text = normalize_report_text(text)
146
+
147
+ # Define section patterns
148
+ section_headers = [
149
+ "CLINICAL HISTORY:",
150
+ "TECHNIQUE:",
151
+ "COMPARISON:",
152
+ "FINDINGS:",
153
+ "IMPRESSION:",
154
+ "RECOMMENDATION:",
155
+ ]
156
+
157
+ # Find all section headers in the text
158
+ sections = {}
159
+ current_section = "PREAMBLE" # For text before first section header
160
+ sections[current_section] = []
161
+
162
+ for line in normalized_text.split("\n"):
163
+ section_found = False
164
+
165
+ for header in section_headers:
166
+ if header in line:
167
+ current_section = header.rstrip(":")
168
+ sections[current_section] = []
169
+ section_found = True
170
+ # Add the rest of the line after the header
171
+ content = line.split(header, 1)[1].strip()
172
+ if content:
173
+ sections[current_section].append(content)
174
+ break
175
+
176
+ if not section_found and current_section:
177
+ sections[current_section].append(line)
178
+
179
+ # Join each section's lines
180
+ for section, lines in sections.items():
181
+ sections[section] = " ".join(lines).strip()
182
+
183
+ # Remove empty sections
184
+ sections = {k: v for k, v in sections.items() if v}
185
+
186
+ return sections
187
+
188
+ except Exception as e:
189
+ logger.error(f"Error extracting sections: {e}")
190
+ return {"FULL_TEXT": text} # Return full text if extraction fails
191
+
192
+
193
+ def extract_measurements(text):
194
+ """
195
+ Extract measurements from medical text (sizes, volumes, etc.).
196
+
197
+ Args:
198
+ text (str): Medical text
199
+
200
+ Returns:
201
+ list: List of tuples containing (measurement, value, unit)
202
+ """
203
+ try:
204
+ # Pattern for measurements like "5mm nodule" or "nodule measuring 5mm"
205
+ # or "8x10mm mass" or "mass of size 8x10mm"
206
+ size_pattern = r"(\d+(?:\.\d+)?(?:\s*[x×]\s*\d+(?:\.\d+)?)?(?:\s*[x×]\s*\d+(?:\.\d+)?)?)\s*(mm|cm|mm2|cm2|mm3|cm3|ml|cc)"
207
+
208
+ # Find measurements with context
209
+ context_pattern = (
210
+ r"([A-Za-z\s]+(?:mass|nodule|effusion|opacity|lesion|tumor|cyst|structure|area|region)[A-Za-z\s]*)"
211
+ + size_pattern
212
+ )
213
+
214
+ context_measurements = []
215
+ for match in re.finditer(context_pattern, text, re.IGNORECASE):
216
+ context, size, unit = match.groups()
217
+ context_measurements.append((context.strip(), size, unit))
218
+
219
+ # For measurements without clear context, just extract size and unit
220
+ all_measurements = []
221
+ for match in re.finditer(size_pattern, text):
222
+ size, unit = match.groups()
223
+ all_measurements.append((size, unit))
224
+
225
+ return context_measurements
226
+
227
+ except Exception as e:
228
+ logger.error(f"Error extracting measurements: {e}")
229
+ return []
230
+
231
+
232
+ def prepare_sample_batch(image_paths, reports=None, target_size=(224, 224)):
233
+ """
234
+ Prepare a batch of samples for model processing.
235
+
236
+ Args:
237
+ image_paths (list): List of paths to images
238
+ reports (list, optional): List of corresponding reports
239
+ target_size (tuple): Target image size
240
+
241
+ Returns:
242
+ tuple: Batch of preprocessed images and reports
243
+ """
244
+ try:
245
+ processed_images = []
246
+ processed_reports = []
247
+
248
+ for i, image_path in enumerate(image_paths):
249
+ # Process image
250
+ image = preprocess_image(image_path, target_size)
251
+ processed_images.append(image)
252
+
253
+ # Process report if available
254
+ if reports and i < len(reports):
255
+ normalized_report = normalize_report_text(reports[i])
256
+ processed_reports.append(normalized_report)
257
+
258
+ return processed_images, processed_reports if reports else None
259
+
260
+ except Exception as e:
261
+ logger.error(f"Error preparing sample batch: {e}")
262
+ raise
mediSync/utils/visualization.py CHANGED
@@ -1,402 +1,516 @@
1
- import base64
2
- import io
3
- import logging
4
-
5
- import cv2
6
- import matplotlib.pyplot as plt
7
- import numpy as np
8
- from PIL import Image
9
-
10
- logger = logging.getLogger(__name__)
11
-
12
- def plot_image_prediction(image, predictions, title=None, figsize=(10, 8)):
13
- try:
14
- if isinstance(image, str):
15
- img = Image.open(image)
16
- else:
17
- img = image
18
-
19
- fig, (ax1, ax2) = plt.subplots(1, 2, figsize=figsize)
20
-
21
- ax1.imshow(img)
22
- ax1.set_title("X-ray Image")
23
- ax1.axis("off")
24
-
25
- if predictions:
26
- sorted_pred = sorted(predictions, key=lambda x: x[1], reverse=True)
27
-
28
- top_n = min(5, len(sorted_pred))
29
- labels = [pred[0] for pred in sorted_pred[:top_n]]
30
- probs = [pred[1] for pred in sorted_pred[:top_n]]
31
-
32
- y_pos = np.arange(top_n)
33
- ax2.barh(y_pos, probs, align="center")
34
- ax2.set_yticks(y_pos)
35
- ax2.set_yticklabels(labels)
36
- ax2.set_xlabel("Probability")
37
- ax2.set_title("Top Predictions")
38
- ax2.set_xlim(0, 1)
39
-
40
- for i, prob in enumerate(probs):
41
- ax2.text(prob + 0.02, i, f"{prob:.1%}", va="center")
42
-
43
- if title:
44
- fig.suptitle(title, fontsize=16)
45
-
46
- fig.tight_layout()
47
- return fig
48
-
49
- except Exception as e:
50
- logger.error(f"Error plotting image prediction: {e}")
51
- fig, ax = plt.subplots(figsize=(8, 6))
52
- ax.text(0.5, 0.5, f"Error: {str(e)}", ha="center", va="center")
53
- return fig
54
-
55
- def create_heatmap_overlay(image, heatmap, alpha=0.4):
56
- try:
57
- if isinstance(image, str):
58
- img = cv2.imread(image)
59
- if img is None:
60
- raise ValueError(f"Could not load image: {image}")
61
- elif isinstance(image, Image.Image):
62
- img = np.array(image)
63
- img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
64
- else:
65
- img = image
66
-
67
- if len(img.shape) == 2:
68
- img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
69
-
70
- heatmap = cv2.resize(heatmap, (img.shape[1], img.shape[0]))
71
-
72
- heatmap = np.maximum(heatmap, 0)
73
- heatmap = np.minimum(heatmap / np.max(heatmap), 1)
74
-
75
- heatmap = np.uint8(255 * heatmap)
76
- heatmap = cv2.applyColorMap(heatmap, cv2.COLORMAP_JET)
77
-
78
- overlay = cv2.addWeighted(img, 1 - alpha, heatmap, alpha, 0)
79
-
80
- overlay = cv2.cvtColor(overlay, cv2.COLOR_BGR2RGB)
81
- overlay_img = Image.fromarray(overlay)
82
-
83
- return overlay_img
84
-
85
- except Exception as e:
86
- logger.error(f"Error creating heatmap overlay: {e}")
87
- if isinstance(image, str):
88
- return Image.open(image)
89
- elif isinstance(image, Image.Image):
90
- return image
91
- else:
92
- return Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
93
-
94
- def plot_report_entities(text, entities, figsize=(12, 8)):
95
- try:
96
- fig, ax = plt.subplots(figsize=figsize)
97
- ax.axis("off")
98
-
99
- fig.patch.set_facecolor("#f8f9fa")
100
- ax.set_facecolor("#f8f9fa")
101
-
102
- ax.text(
103
- 0.5,
104
- 0.98,
105
- "Medical Report Analysis",
106
- ha="center",
107
- va="top",
108
- fontsize=18,
109
- fontweight="bold",
110
- color="#2c3e50",
111
- )
112
-
113
- y_pos = 0.9
114
- ax.text(
115
- 0.05,
116
- y_pos,
117
- "Extracted Entities:",
118
- fontsize=14,
119
- fontweight="bold",
120
- color="#2c3e50",
121
- )
122
- y_pos -= 0.05
123
-
124
- category_colors = {
125
- "problem": "#e74c3c",
126
- "test": "#3498db",
127
- "treatment": "#2ecc71",
128
- "anatomy": "#9b59b6",
129
- }
130
-
131
- for category, items in entities.items():
132
- if items:
133
- y_pos -= 0.05
134
- ax.text(
135
- 0.1,
136
- y_pos,
137
- f"{category.capitalize()}:",
138
- fontsize=12,
139
- fontweight="bold",
140
- )
141
- y_pos -= 0.05
142
- ax.text(
143
- 0.15,
144
- y_pos,
145
- ", ".join(items),
146
- wrap=True,
147
- fontsize=11,
148
- color=category_colors.get(category, "black"),
149
- )
150
-
151
- y_pos -= 0.1
152
- ax.text(
153
- 0.05,
154
- y_pos,
155
- "Report Text (with highlighted entities):",
156
- fontsize=14,
157
- fontweight="bold",
158
- color="#2c3e50",
159
- )
160
- y_pos -= 0.05
161
-
162
- all_entities = []
163
- for category, items in entities.items():
164
- for item in items:
165
- all_entities.append((item, category))
166
-
167
- all_entities.sort(key=lambda x: len(x[0]), reverse=True)
168
-
169
- highlighted_text = text
170
- for entity, category in all_entities:
171
- entity_escaped = (
172
- entity.replace("(", r"\(")
173
- .replace(")", r"\)")
174
- .replace("[", r"\[")
175
- .replace("]", r"\]")
176
- )
177
-
178
- pattern = r"\b" + entity_escaped + r"\b"
179
- color_code = category_colors.get(category, "black")
180
- replacement = f"\\textcolor{{{color_code}}}{{{entity}}}"
181
- highlighted_text = highlighted_text.replace(entity, replacement)
182
-
183
- ax.text(0.05, y_pos, highlighted_text, va="top", fontsize=10, wrap=True)
184
-
185
- fig.tight_layout(rect=[0, 0.03, 1, 0.97])
186
- return fig
187
-
188
- except Exception as e:
189
- logger.error(f"Error plotting report entities: {e}")
190
- fig, ax = plt.subplots(figsize=(8, 6))
191
- ax.text(0.5, 0.5, f"Error: {str(e)}", ha="center", va="center")
192
- return fig
193
-
194
- def plot_multimodal_results(
195
- fused_results, image=None, report_text=None, figsize=(12, 10)
196
- ):
197
- try:
198
- fig = plt.figure(figsize=figsize)
199
- gs = fig.add_gridspec(2, 2)
200
-
201
- fig.suptitle(
202
- "Multimodal Medical Analysis Results",
203
- fontsize=18,
204
- fontweight="bold",
205
- y=0.98,
206
- )
207
-
208
- ax_overview = fig.add_subplot(gs[0, 0])
209
- ax_overview.axis("off")
210
-
211
- severity = fused_results.get("severity", {})
212
- severity_level = severity.get("level", "Unknown")
213
- severity_score = severity.get("score", 0)
214
-
215
- primary_finding = fused_results.get("primary_finding", "Unknown")
216
-
217
- agreement = fused_results.get("agreement_score", 0)
218
-
219
- overview_text = [
220
- "ANALYSIS OVERVIEW",
221
- f"Primary Finding: {primary_finding}",
222
- f"Severity Level: {severity_level} ({severity_score}/4)",
223
- f"Agreement Score: {agreement:.0%}",
224
- ]
225
-
226
- severity_colors = {
227
- "Normal": "#2ecc71",
228
- "Mild": "#3498db",
229
- "Moderate": "#f39c12",
230
- "Severe": "#e74c3c",
231
- "Critical": "#c0392b",
232
- }
233
-
234
- y_pos = 0.9
235
- ax_overview.text(
236
- 0.5,
237
- y_pos,
238
- overview_text[0],
239
- fontsize=14,
240
- fontweight="bold",
241
- ha="center",
242
- va="center",
243
- )
244
- y_pos -= 0.15
245
-
246
- ax_overview.text(
247
- 0.1, y_pos, overview_text[1], fontsize=12, ha="left", va="center"
248
- )
249
- y_pos -= 0.1
250
-
251
- severity_color = severity_colors.get(severity_level, "black")
252
- ax_overview.text(
253
- 0.1, y_pos, "Severity Level:", fontsize=12, ha="left", va="center"
254
- )
255
- ax_overview.text(
256
- 0.4,
257
- y_pos,
258
- severity_level,
259
- fontsize=12,
260
- color=severity_color,
261
- fontweight="bold",
262
- ha="left",
263
- va="center",
264
- )
265
- ax_overview.text(
266
- 0.6, y_pos, f"({severity_score}/4)", fontsize=10, ha="left", va="center"
267
- )
268
- y_pos -= 0.1
269
-
270
- agreement_color = (
271
- "#2ecc71"
272
- if agreement > 0.7
273
- else "#f39c12"
274
- if agreement > 0.4
275
- else "#e74c3c"
276
- )
277
- ax_overview.text(
278
- 0.1, y_pos, "Agreement Score:", fontsize=12, ha="left", va="center"
279
- )
280
- ax_overview.text(
281
- 0.4,
282
- y_pos,
283
- f"{agreement:.0%}",
284
- fontsize=12,
285
- color=agreement_color,
286
- fontweight="bold",
287
- ha="left",
288
- va="center",
289
- )
290
-
291
- ax_findings = fig.add_subplot(gs[0, 1])
292
- ax_findings.axis("off")
293
-
294
- findings = fused_results.get("findings", [])
295
-
296
- y_pos = 0.9
297
- ax_findings.text(
298
- 0.5,
299
- y_pos,
300
- "KEY FINDINGS",
301
- fontsize=14,
302
- fontweight="bold",
303
- ha="center",
304
- va="center",
305
- )
306
- y_pos -= 0.1
307
-
308
- if findings:
309
- for i, finding in enumerate(findings[:5]):
310
- ax_findings.text(0.05, y_pos, "•", fontsize=14, ha="left", va="center")
311
- ax_findings.text(
312
- 0.1, y_pos, finding, fontsize=11, ha="left", va="center", wrap=True
313
- )
314
- y_pos -= 0.15
315
- else:
316
- ax_findings.text(
317
- 0.1,
318
- y_pos,
319
- "No specific findings detailed.",
320
- fontsize=11,
321
- ha="left",
322
- va="center",
323
- )
324
-
325
- ax_image = fig.add_subplot(gs[1, 0])
326
-
327
- if image is not None:
328
- if isinstance(image, str):
329
- img = Image.open(image)
330
- else:
331
- img = image
332
-
333
- ax_image.imshow(img)
334
- ax_image.set_title("X-ray Image", fontsize=12)
335
- else:
336
- ax_image.text(0.5, 0.5, "No image available", ha="center", va="center")
337
-
338
- ax_image.axis("off")
339
-
340
- ax_rec = fig.add_subplot(gs[1, 1])
341
- ax_rec.axis("off")
342
-
343
- recommendations = fused_results.get("followup_recommendations", [])
344
-
345
- y_pos = 0.9
346
- ax_rec.text(
347
- 0.5,
348
- y_pos,
349
- "RECOMMENDATIONS",
350
- fontsize=14,
351
- fontweight="bold",
352
- ha="center",
353
- va="center",
354
- )
355
- y_pos -= 0.1
356
-
357
- if recommendations:
358
- for i, rec in enumerate(recommendations):
359
- ax_rec.text(0.05, y_pos, "•", fontsize=14, ha="left", va="center")
360
- ax_rec.text(
361
- 0.1, y_pos, rec, fontsize=11, ha="left", va="center", wrap=True
362
- )
363
- y_pos -= 0.15
364
- else:
365
- ax_rec.text(
366
- 0.1,
367
- y_pos,
368
- "No specific recommendations provided.",
369
- fontsize=11,
370
- ha="left",
371
- va="center",
372
- )
373
-
374
- fig.text(
375
- 0.5,
376
- 0.03,
377
- "DISCLAIMER: This analysis is for informational purposes only and should not replace professional medical advice.",
378
- fontsize=9,
379
- style="italic",
380
- ha="center",
381
- )
382
-
383
- fig.tight_layout(rect=[0, 0.05, 1, 0.95])
384
- return fig
385
-
386
- except Exception as e:
387
- logger.error(f"Error plotting multimodal results: {e}")
388
- fig, ax = plt.subplots(figsize=(8, 6))
389
- ax.text(0.5, 0.5, f"Error: {str(e)}", ha="center", va="center")
390
- return fig
391
-
392
- def figure_to_base64(fig):
393
- try:
394
- buf = io.BytesIO()
395
- fig.savefig(buf, format="png", bbox_inches="tight")
396
- buf.seek(0)
397
- img_str = base64.b64encode(buf.read()).decode("utf-8")
398
- return img_str
399
-
400
- except Exception as e:
401
- logger.error(f"Error converting figure to base64: {e}")
402
- return ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import base64
2
+ import io
3
+ import logging
4
+
5
+ import cv2
6
+ import matplotlib.pyplot as plt
7
+ import numpy as np
8
+ from PIL import Image
9
+
10
+ # Set up logging
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ def plot_image_prediction(image, predictions, title=None, figsize=(10, 8)):
15
+ """
16
+ Plot an image with its predictions.
17
+
18
+ Args:
19
+ image (PIL.Image or str): Image or path to image
20
+ predictions (list): List of (label, probability) tuples
21
+ title (str, optional): Plot title
22
+ figsize (tuple): Figure size
23
+
24
+ Returns:
25
+ matplotlib.figure.Figure: The figure object
26
+ """
27
+ try:
28
+ # Load image if path is provided
29
+ if isinstance(image, str):
30
+ img = Image.open(image)
31
+ else:
32
+ img = image
33
+
34
+ # Create figure
35
+ fig, (ax1, ax2) = plt.subplots(1, 2, figsize=figsize)
36
+
37
+ # Plot image
38
+ ax1.imshow(img)
39
+ ax1.set_title("X-ray Image")
40
+ ax1.axis("off")
41
+
42
+ # Plot predictions
43
+ if predictions:
44
+ # Sort predictions by probability
45
+ sorted_pred = sorted(predictions, key=lambda x: x[1], reverse=True)
46
+
47
+ # Get top 5 predictions
48
+ top_n = min(5, len(sorted_pred))
49
+ labels = [pred[0] for pred in sorted_pred[:top_n]]
50
+ probs = [pred[1] for pred in sorted_pred[:top_n]]
51
+
52
+ # Plot horizontal bar chart
53
+ y_pos = np.arange(top_n)
54
+ ax2.barh(y_pos, probs, align="center")
55
+ ax2.set_yticks(y_pos)
56
+ ax2.set_yticklabels(labels)
57
+ ax2.set_xlabel("Probability")
58
+ ax2.set_title("Top Predictions")
59
+ ax2.set_xlim(0, 1)
60
+
61
+ # Annotate probabilities
62
+ for i, prob in enumerate(probs):
63
+ ax2.text(prob + 0.02, i, f"{prob:.1%}", va="center")
64
+
65
+ # Set overall title
66
+ if title:
67
+ fig.suptitle(title, fontsize=16)
68
+
69
+ fig.tight_layout()
70
+ return fig
71
+
72
+ except Exception as e:
73
+ logger.error(f"Error plotting image prediction: {e}")
74
+ # Create empty figure if error occurs
75
+ fig, ax = plt.subplots(figsize=(8, 6))
76
+ ax.text(0.5, 0.5, f"Error: {str(e)}", ha="center", va="center")
77
+ return fig
78
+
79
+
80
+ def create_heatmap_overlay(image, heatmap, alpha=0.4):
81
+ """
82
+ Create a heatmap overlay on an X-ray image to highlight areas of interest.
83
+
84
+ Args:
85
+ image (PIL.Image or str): Image or path to image
86
+ heatmap (numpy.ndarray): Heatmap array
87
+ alpha (float): Transparency of the overlay
88
+
89
+ Returns:
90
+ PIL.Image: Image with heatmap overlay
91
+ """
92
+ try:
93
+ # Load image if path is provided
94
+ if isinstance(image, str):
95
+ img = cv2.imread(image)
96
+ if img is None:
97
+ raise ValueError(f"Could not load image: {image}")
98
+ elif isinstance(image, Image.Image):
99
+ img = np.array(image)
100
+ img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
101
+ else:
102
+ img = image
103
+
104
+ # Ensure image is in BGR format for OpenCV
105
+ if len(img.shape) == 2: # Grayscale
106
+ img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
107
+
108
+ # Resize heatmap to match image dimensions
109
+ heatmap = cv2.resize(heatmap, (img.shape[1], img.shape[0]))
110
+
111
+ # Normalize heatmap (0-1)
112
+ heatmap = np.maximum(heatmap, 0)
113
+ heatmap = np.minimum(heatmap / np.max(heatmap), 1)
114
+
115
+ # Apply colormap (jet) to heatmap
116
+ heatmap = np.uint8(255 * heatmap)
117
+ heatmap = cv2.applyColorMap(heatmap, cv2.COLORMAP_JET)
118
+
119
+ # Create overlay
120
+ overlay = cv2.addWeighted(img, 1 - alpha, heatmap, alpha, 0)
121
+
122
+ # Convert back to PIL image
123
+ overlay = cv2.cvtColor(overlay, cv2.COLOR_BGR2RGB)
124
+ overlay_img = Image.fromarray(overlay)
125
+
126
+ return overlay_img
127
+
128
+ except Exception as e:
129
+ logger.error(f"Error creating heatmap overlay: {e}")
130
+ # Return original image if error occurs
131
+ if isinstance(image, str):
132
+ return Image.open(image)
133
+ elif isinstance(image, Image.Image):
134
+ return image
135
+ else:
136
+ return Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
137
+
138
+
139
+ def plot_report_entities(text, entities, figsize=(12, 8)):
140
+ """
141
+ Visualize entities extracted from a medical report.
142
+
143
+ Args:
144
+ text (str): Report text
145
+ entities (dict): Dictionary of entities by category
146
+ figsize (tuple): Figure size
147
+
148
+ Returns:
149
+ matplotlib.figure.Figure: The figure object
150
+ """
151
+ try:
152
+ fig, ax = plt.subplots(figsize=figsize)
153
+ ax.axis("off")
154
+
155
+ # Set background color
156
+ fig.patch.set_facecolor("#f8f9fa")
157
+ ax.set_facecolor("#f8f9fa")
158
+
159
+ # Title
160
+ ax.text(
161
+ 0.5,
162
+ 0.98,
163
+ "Medical Report Analysis",
164
+ ha="center",
165
+ va="top",
166
+ fontsize=18,
167
+ fontweight="bold",
168
+ color="#2c3e50",
169
+ )
170
+
171
+ # Display entity counts
172
+ y_pos = 0.9
173
+ ax.text(
174
+ 0.05,
175
+ y_pos,
176
+ "Extracted Entities:",
177
+ fontsize=14,
178
+ fontweight="bold",
179
+ color="#2c3e50",
180
+ )
181
+ y_pos -= 0.05
182
+
183
+ # Define colors for different entity categories
184
+ category_colors = {
185
+ "problem": "#e74c3c", # Red
186
+ "test": "#3498db", # Blue
187
+ "treatment": "#2ecc71", # Green
188
+ "anatomy": "#9b59b6", # Purple
189
+ }
190
+
191
+ # Display entities by category
192
+ for category, items in entities.items():
193
+ if items:
194
+ y_pos -= 0.05
195
+ ax.text(
196
+ 0.1,
197
+ y_pos,
198
+ f"{category.capitalize()}:",
199
+ fontsize=12,
200
+ fontweight="bold",
201
+ )
202
+ y_pos -= 0.05
203
+ ax.text(
204
+ 0.15,
205
+ y_pos,
206
+ ", ".join(items),
207
+ wrap=True,
208
+ fontsize=11,
209
+ color=category_colors.get(category, "black"),
210
+ )
211
+
212
+ # Add the report text with highlighted entities
213
+ y_pos -= 0.1
214
+ ax.text(
215
+ 0.05,
216
+ y_pos,
217
+ "Report Text (with highlighted entities):",
218
+ fontsize=14,
219
+ fontweight="bold",
220
+ color="#2c3e50",
221
+ )
222
+ y_pos -= 0.05
223
+
224
+ # Get all entities to highlight
225
+ all_entities = []
226
+ for category, items in entities.items():
227
+ for item in items:
228
+ all_entities.append((item, category))
229
+
230
+ # Sort entities by length (longest first to avoid overlap issues)
231
+ all_entities.sort(key=lambda x: len(x[0]), reverse=True)
232
+
233
+ # Highlight entities in text
234
+ highlighted_text = text
235
+ for entity, category in all_entities:
236
+ # Escape regex special characters
237
+ entity_escaped = (
238
+ entity.replace("(", r"\(")
239
+ .replace(")", r"\)")
240
+ .replace("[", r"\[")
241
+ .replace("]", r"\]")
242
+ )
243
+
244
+ # Find entity in text (word boundary)
245
+ pattern = r"\b" + entity_escaped + r"\b"
246
+ color_code = category_colors.get(category, "black")
247
+ replacement = f"\\textcolor{{{color_code}}}{{{entity}}}"
248
+ highlighted_text = highlighted_text.replace(entity, replacement)
249
+
250
+ # Display highlighted text
251
+ ax.text(0.05, y_pos, highlighted_text, va="top", fontsize=10, wrap=True)
252
+
253
+ fig.tight_layout(rect=[0, 0.03, 1, 0.97])
254
+ return fig
255
+
256
+ except Exception as e:
257
+ logger.error(f"Error plotting report entities: {e}")
258
+ # Create empty figure if error occurs
259
+ fig, ax = plt.subplots(figsize=(8, 6))
260
+ ax.text(0.5, 0.5, f"Error: {str(e)}", ha="center", va="center")
261
+ return fig
262
+
263
+
264
+ def plot_multimodal_results(
265
+ fused_results, image=None, report_text=None, figsize=(12, 10)
266
+ ):
267
+ """
268
+ Visualize the results of multimodal analysis.
269
+
270
+ Args:
271
+ fused_results (dict): Results from multimodal fusion
272
+ image (PIL.Image or str, optional): Image or path to image
273
+ report_text (str, optional): Report text
274
+ figsize (tuple): Figure size
275
+
276
+ Returns:
277
+ matplotlib.figure.Figure: The figure object
278
+ """
279
+ try:
280
+ # Create figure with a grid layout
281
+ fig = plt.figure(figsize=figsize)
282
+ gs = fig.add_gridspec(2, 2)
283
+
284
+ # Add title
285
+ fig.suptitle(
286
+ "Multimodal Medical Analysis Results",
287
+ fontsize=18,
288
+ fontweight="bold",
289
+ y=0.98,
290
+ )
291
+
292
+ # 1. Overview panel (top left)
293
+ ax_overview = fig.add_subplot(gs[0, 0])
294
+ ax_overview.axis("off")
295
+
296
+ # Get severity info
297
+ severity = fused_results.get("severity", {})
298
+ severity_level = severity.get("level", "Unknown")
299
+ severity_score = severity.get("score", 0)
300
+
301
+ # Get primary finding
302
+ primary_finding = fused_results.get("primary_finding", "Unknown")
303
+
304
+ # Get agreement score
305
+ agreement = fused_results.get("agreement_score", 0)
306
+
307
+ # Create overview text
308
+ overview_text = [
309
+ "ANALYSIS OVERVIEW",
310
+ f"Primary Finding: {primary_finding}",
311
+ f"Severity Level: {severity_level} ({severity_score}/4)",
312
+ f"Agreement Score: {agreement:.0%}",
313
+ ]
314
+
315
+ # Define severity colors
316
+ severity_colors = {
317
+ "Normal": "#2ecc71", # Green
318
+ "Mild": "#3498db", # Blue
319
+ "Moderate": "#f39c12", # Orange
320
+ "Severe": "#e74c3c", # Red
321
+ "Critical": "#c0392b", # Dark Red
322
+ }
323
+
324
+ # Add overview text to the panel
325
+ y_pos = 0.9
326
+ ax_overview.text(
327
+ 0.5,
328
+ y_pos,
329
+ overview_text[0],
330
+ fontsize=14,
331
+ fontweight="bold",
332
+ ha="center",
333
+ va="center",
334
+ )
335
+ y_pos -= 0.15
336
+
337
+ ax_overview.text(
338
+ 0.1, y_pos, overview_text[1], fontsize=12, ha="left", va="center"
339
+ )
340
+ y_pos -= 0.1
341
+
342
+ # Severity with color
343
+ severity_color = severity_colors.get(severity_level, "black")
344
+ ax_overview.text(
345
+ 0.1, y_pos, "Severity Level:", fontsize=12, ha="left", va="center"
346
+ )
347
+ ax_overview.text(
348
+ 0.4,
349
+ y_pos,
350
+ severity_level,
351
+ fontsize=12,
352
+ color=severity_color,
353
+ fontweight="bold",
354
+ ha="left",
355
+ va="center",
356
+ )
357
+ ax_overview.text(
358
+ 0.6, y_pos, f"({severity_score}/4)", fontsize=10, ha="left", va="center"
359
+ )
360
+ y_pos -= 0.1
361
+
362
+ # Agreement score with color
363
+ agreement_color = (
364
+ "#2ecc71"
365
+ if agreement > 0.7
366
+ else "#f39c12"
367
+ if agreement > 0.4
368
+ else "#e74c3c"
369
+ )
370
+ ax_overview.text(
371
+ 0.1, y_pos, "Agreement Score:", fontsize=12, ha="left", va="center"
372
+ )
373
+ ax_overview.text(
374
+ 0.4,
375
+ y_pos,
376
+ f"{agreement:.0%}",
377
+ fontsize=12,
378
+ color=agreement_color,
379
+ fontweight="bold",
380
+ ha="left",
381
+ va="center",
382
+ )
383
+
384
+ # 2. Findings panel (top right)
385
+ ax_findings = fig.add_subplot(gs[0, 1])
386
+ ax_findings.axis("off")
387
+
388
+ # Get findings
389
+ findings = fused_results.get("findings", [])
390
+
391
+ # Add findings to the panel
392
+ y_pos = 0.9
393
+ ax_findings.text(
394
+ 0.5,
395
+ y_pos,
396
+ "KEY FINDINGS",
397
+ fontsize=14,
398
+ fontweight="bold",
399
+ ha="center",
400
+ va="center",
401
+ )
402
+ y_pos -= 0.1
403
+
404
+ if findings:
405
+ for i, finding in enumerate(findings[:5]): # Limit to 5 findings
406
+ ax_findings.text(0.05, y_pos, "•", fontsize=14, ha="left", va="center")
407
+ ax_findings.text(
408
+ 0.1, y_pos, finding, fontsize=11, ha="left", va="center", wrap=True
409
+ )
410
+ y_pos -= 0.15
411
+ else:
412
+ ax_findings.text(
413
+ 0.1,
414
+ y_pos,
415
+ "No specific findings detailed.",
416
+ fontsize=11,
417
+ ha="left",
418
+ va="center",
419
+ )
420
+
421
+ # 3. Image panel (bottom left)
422
+ ax_image = fig.add_subplot(gs[1, 0])
423
+
424
+ if image is not None:
425
+ # Load image if path is provided
426
+ if isinstance(image, str):
427
+ img = Image.open(image)
428
+ else:
429
+ img = image
430
+
431
+ # Display image
432
+ ax_image.imshow(img)
433
+ ax_image.set_title("X-ray Image", fontsize=12)
434
+ else:
435
+ ax_image.text(0.5, 0.5, "No image available", ha="center", va="center")
436
+
437
+ ax_image.axis("off")
438
+
439
+ # 4. Recommendation panel (bottom right)
440
+ ax_rec = fig.add_subplot(gs[1, 1])
441
+ ax_rec.axis("off")
442
+
443
+ # Get recommendations
444
+ recommendations = fused_results.get("followup_recommendations", [])
445
+
446
+ # Add recommendations to the panel
447
+ y_pos = 0.9
448
+ ax_rec.text(
449
+ 0.5,
450
+ y_pos,
451
+ "RECOMMENDATIONS",
452
+ fontsize=14,
453
+ fontweight="bold",
454
+ ha="center",
455
+ va="center",
456
+ )
457
+ y_pos -= 0.1
458
+
459
+ if recommendations:
460
+ for i, rec in enumerate(recommendations):
461
+ ax_rec.text(0.05, y_pos, "•", fontsize=14, ha="left", va="center")
462
+ ax_rec.text(
463
+ 0.1, y_pos, rec, fontsize=11, ha="left", va="center", wrap=True
464
+ )
465
+ y_pos -= 0.15
466
+ else:
467
+ ax_rec.text(
468
+ 0.1,
469
+ y_pos,
470
+ "No specific recommendations provided.",
471
+ fontsize=11,
472
+ ha="left",
473
+ va="center",
474
+ )
475
+
476
+ # Add disclaimer
477
+ fig.text(
478
+ 0.5,
479
+ 0.03,
480
+ "DISCLAIMER: This analysis is for informational purposes only and should not replace professional medical advice.",
481
+ fontsize=9,
482
+ style="italic",
483
+ ha="center",
484
+ )
485
+
486
+ fig.tight_layout(rect=[0, 0.05, 1, 0.95])
487
+ return fig
488
+
489
+ except Exception as e:
490
+ logger.error(f"Error plotting multimodal results: {e}")
491
+ # Create empty figure if error occurs
492
+ fig, ax = plt.subplots(figsize=(8, 6))
493
+ ax.text(0.5, 0.5, f"Error: {str(e)}", ha="center", va="center")
494
+ return fig
495
+
496
+
497
+ def figure_to_base64(fig):
498
+ """
499
+ Convert matplotlib figure to base64 string.
500
+
501
+ Args:
502
+ fig (matplotlib.figure.Figure): Figure object
503
+
504
+ Returns:
505
+ str: Base64 encoded string
506
+ """
507
+ try:
508
+ buf = io.BytesIO()
509
+ fig.savefig(buf, format="png", bbox_inches="tight")
510
+ buf.seek(0)
511
+ img_str = base64.b64encode(buf.read()).decode("utf-8")
512
+ return img_str
513
+
514
+ except Exception as e:
515
+ logger.error(f"Error converting figure to base64: {e}")
516
+ return ""
run.py CHANGED
@@ -1,55 +1,71 @@
1
- import logging
2
- import sys
3
- from pathlib import Path
4
-
5
- logging.basicConfig(
6
- level=logging.INFO,
7
- format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
8
- handlers=[logging.StreamHandler(), logging.FileHandler("uhres.log")],
9
- )
10
- logger = logging.getLogger(__name__)
11
-
12
- def main():
13
- logger.info("Starting U-HRES setup")
14
-
15
- current_dir = Path(__file__).resolve().parent
16
- sys.path.append(str(current_dir))
17
-
18
- try:
19
- logger.info("Checking for sample data")
20
- from mediSync.utils.download_samples import (
21
- create_sample_info_file,
22
- download_sample_images,
23
- )
24
-
25
- sample_dir = current_dir / "mediSync" / "data" / "sample"
26
- sample_dir.mkdir(parents=True, exist_ok=True)
27
-
28
- image_files = list(sample_dir.glob("*.jpg")) + list(sample_dir.glob("*.png"))
29
-
30
- if not image_files:
31
- logger.info("No sample images found. Downloading...")
32
- download_sample_images()
33
- create_sample_info_file()
34
- else:
35
- logger.info(f"Found {len(image_files)} existing sample images")
36
-
37
- except Exception as e:
38
- logger.error(f"Error setting up sample data: {e}")
39
- print(f"Warning: Could not set up sample data: {e}")
40
-
41
- try:
42
- logger.info("Launching U-HRES application")
43
- from mediSync.app import create_interface
44
-
45
- create_interface()
46
-
47
- except Exception as e:
48
- logger.error(f"Error launching application: {e}")
49
- print(f"Error: Failed to launch U-HRES application: {e}")
50
- return 1
51
-
52
- return 0
53
-
54
- if __name__ == "__main__":
55
- sys.exit(main())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ U-HRES: Unified Health Record Exchange System AI Layer - Runner Script
3
+ =====================================================================
4
+ This script downloads sample data and launches the U-HRES application.
5
+ """
6
+
7
+ import logging
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ # Set up logging
12
+ logging.basicConfig(
13
+ level=logging.INFO,
14
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
15
+ handlers=[logging.StreamHandler(), logging.FileHandler("uhres.log")],
16
+ )
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ def main():
21
+ """
22
+ Main function to set up and run the U-HRES application.
23
+ """
24
+ logger.info("Starting U-HRES setup")
25
+
26
+ # Add the current directory to Python path
27
+ current_dir = Path(__file__).resolve().parent
28
+ sys.path.append(str(current_dir))
29
+
30
+ # Download sample data if needed
31
+ try:
32
+ logger.info("Checking for sample data")
33
+ from mediSync.utils.download_samples import (
34
+ create_sample_info_file,
35
+ download_sample_images,
36
+ )
37
+
38
+ # Check if sample directory exists and contains images
39
+ sample_dir = current_dir / "mediSync" / "data" / "sample"
40
+ sample_dir.mkdir(parents=True, exist_ok=True)
41
+
42
+ image_files = list(sample_dir.glob("*.jpg")) + list(sample_dir.glob("*.png"))
43
+
44
+ if not image_files:
45
+ logger.info("No sample images found. Downloading...")
46
+ download_sample_images()
47
+ create_sample_info_file()
48
+ else:
49
+ logger.info(f"Found {len(image_files)} existing sample images")
50
+
51
+ except Exception as e:
52
+ logger.error(f"Error setting up sample data: {e}")
53
+ print(f"Warning: Could not set up sample data: {e}")
54
+
55
+ # Launch the application
56
+ try:
57
+ logger.info("Launching U-HRES application")
58
+ from mediSync.app import create_interface
59
+
60
+ create_interface()
61
+
62
+ except Exception as e:
63
+ logger.error(f"Error launching application: {e}")
64
+ print(f"Error: Failed to launch U-HRES application: {e}")
65
+ return 1
66
+
67
+ return 0
68
+
69
+
70
+ if __name__ == "__main__":
71
+ sys.exit(main())