updated the application UI ✅✅
Browse files- mediSync/app.py +234 -176
mediSync/app.py
CHANGED
|
@@ -386,14 +386,14 @@ os.makedirs(os.path.join(parent_dir, "data", "sample"), exist_ok=True)
|
|
| 386 |
import logging
|
| 387 |
import os
|
| 388 |
import sys
|
|
|
|
| 389 |
from pathlib import Path
|
| 390 |
import requests
|
| 391 |
import gradio as gr
|
| 392 |
import matplotlib.pyplot as plt
|
| 393 |
from PIL import Image
|
| 394 |
-
import json
|
| 395 |
|
| 396 |
-
# Import configuration
|
| 397 |
try:
|
| 398 |
from .config import get_flask_urls, get_doctors_page_urls, TIMEOUT_SETTINGS
|
| 399 |
except ImportError:
|
|
@@ -411,9 +411,20 @@ except ImportError:
|
|
| 411 |
}
|
| 412 |
TIMEOUT_SETTINGS = {"connection_timeout": 5, "request_timeout": 10}
|
| 413 |
|
|
|
|
| 414 |
parent_dir = os.path.dirname(os.path.abspath(__file__))
|
| 415 |
sys.path.append(parent_dir)
|
| 416 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 417 |
logging.basicConfig(
|
| 418 |
level=logging.INFO,
|
| 419 |
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
@@ -421,224 +432,271 @@ logging.basicConfig(
|
|
| 421 |
)
|
| 422 |
logger = logging.getLogger(__name__)
|
| 423 |
|
|
|
|
|
|
|
|
|
|
| 424 |
class MediSyncApp:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 425 |
def __init__(self):
|
|
|
|
| 426 |
self.logger = logging.getLogger(__name__)
|
| 427 |
self.logger.info("Initializing MediSync application")
|
| 428 |
-
self._temp_files = []
|
| 429 |
self.fusion_model = None
|
| 430 |
self.image_model = None
|
| 431 |
self.text_model = None
|
| 432 |
|
| 433 |
-
def __del__(self):
|
| 434 |
-
self.cleanup_temp_files()
|
| 435 |
-
|
| 436 |
-
def cleanup_temp_files(self):
|
| 437 |
-
for temp_file in self._temp_files:
|
| 438 |
-
try:
|
| 439 |
-
if os.path.exists(temp_file):
|
| 440 |
-
os.remove(temp_file)
|
| 441 |
-
self.logger.debug(f"Cleaned up temporary file: {temp_file}")
|
| 442 |
-
except Exception as e:
|
| 443 |
-
self.logger.warning(f"Failed to clean up temporary file {temp_file}: {e}")
|
| 444 |
-
self._temp_files = []
|
| 445 |
-
|
| 446 |
def load_models(self):
|
| 447 |
-
|
| 448 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 449 |
try:
|
| 450 |
-
self.
|
| 451 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 452 |
return True
|
| 453 |
except Exception as e:
|
| 454 |
self.logger.error(f"Error loading models: {e}")
|
| 455 |
return False
|
| 456 |
|
| 457 |
-
def enhance_image(self, image):
|
| 458 |
-
if image is None:
|
| 459 |
-
return None
|
| 460 |
-
try:
|
| 461 |
-
enhanced_image = image
|
| 462 |
-
self.logger.info("Image enhanced successfully")
|
| 463 |
-
return enhanced_image
|
| 464 |
-
except Exception as e:
|
| 465 |
-
self.logger.error(f"Error enhancing image: {e}")
|
| 466 |
-
return image
|
| 467 |
-
|
| 468 |
def analyze_image(self, image):
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 473 |
try:
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
"
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 486 |
image,
|
| 487 |
results.get("predictions", []),
|
| 488 |
-
f"Primary Finding: {results.get('primary_finding', 'Unknown')}"
|
| 489 |
)
|
| 490 |
plot_html = self.fig_to_html(fig)
|
| 491 |
-
|
| 492 |
-
html_result =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 493 |
return image, html_result, plot_html
|
| 494 |
except Exception as e:
|
| 495 |
self.logger.error(f"Error in image analysis: {e}")
|
| 496 |
return image, f"Error analyzing image: {str(e)}", None
|
| 497 |
|
| 498 |
def analyze_text(self, text):
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 503 |
try:
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 517 |
except Exception as e:
|
| 518 |
self.logger.error(f"Error in text analysis: {e}")
|
| 519 |
return text, f"Error analyzing text: {str(e)}", None
|
| 520 |
|
| 521 |
def analyze_multimodal(self, image, text):
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 526 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 527 |
self.logger.info("Performing multimodal analysis")
|
| 528 |
-
results =
|
| 529 |
-
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
|
| 539 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 540 |
return html_result, plot_html
|
| 541 |
except Exception as e:
|
| 542 |
self.logger.error(f"Error in multimodal analysis: {e}")
|
| 543 |
return f"Error in multimodal analysis: {str(e)}", None
|
| 544 |
|
| 545 |
-
def
|
| 546 |
-
html_result = f"""
|
| 547 |
-
<div class="medisync-card medisync-card-bg medisync-force-text">
|
| 548 |
-
<h2 class="medisync-title medisync-blue">X-ray Analysis Results</h2>
|
| 549 |
-
<p><strong>Primary Finding:</strong> {results.get("primary_finding", "Unknown")}</p>
|
| 550 |
-
<p><strong>Confidence:</strong> {results.get("confidence", 0):.1%}</p>
|
| 551 |
-
<p><strong>Abnormality Detected:</strong> {"Yes" if results.get("has_abnormality", False) else "No"}</p>
|
| 552 |
-
<h3>Top Predictions:</h3>
|
| 553 |
-
<ul>
|
| 554 |
-
"""
|
| 555 |
-
for label, prob in results.get("predictions", [])[:5]:
|
| 556 |
-
html_result += f"<li>{label}: {prob:.1%}</li>"
|
| 557 |
-
html_result += "</ul></div>"
|
| 558 |
-
return html_result
|
| 559 |
-
|
| 560 |
-
def format_text_results(self, results):
|
| 561 |
-
html_result = f"""
|
| 562 |
-
<div class="medisync-card medisync-card-bg medisync-force-text">
|
| 563 |
-
<h2 class="medisync-title medisync-green">Text Analysis Results</h2>
|
| 564 |
-
<p><strong>Sentiment:</strong> {results.get("sentiment", "Unknown").title()}</p>
|
| 565 |
-
<h3>Key Findings:</h3>
|
| 566 |
-
<ul>
|
| 567 |
"""
|
| 568 |
-
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
return html_result
|
| 576 |
-
|
| 577 |
-
def format_multimodal_results(self, results):
|
| 578 |
-
html_result = f"""
|
| 579 |
-
<div class="medisync-card medisync-card-bg medisync-force-text">
|
| 580 |
-
<h2 class="medisync-title medisync-purple">Multimodal Analysis Results</h2>
|
| 581 |
-
<p><strong>Combined Finding:</strong> {results.get("combined_finding", "Unknown")}</p>
|
| 582 |
-
<p><strong>Overall Confidence:</strong> {results.get("confidence", 0):.1%}</p>
|
| 583 |
-
<h3>Image Contribution:</h3>
|
| 584 |
-
<p>{results.get("image_contribution", "No image analysis available")}</p>
|
| 585 |
-
<h3>Text Contribution:</h3>
|
| 586 |
-
<p>{results.get("text_contribution", "No text analysis available")}</p>
|
| 587 |
-
<h3>Recommendations:</h3>
|
| 588 |
-
<ul>
|
| 589 |
"""
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
|
| 593 |
-
|
| 594 |
-
|
| 595 |
-
|
| 596 |
-
|
| 597 |
-
|
| 598 |
-
|
| 599 |
-
|
| 600 |
-
|
| 601 |
-
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
return
|
| 605 |
-
|
| 606 |
-
|
| 607 |
-
|
| 608 |
-
entity_type = entity['type']
|
| 609 |
-
if entity_type not in entity_types:
|
| 610 |
-
entity_types[entity_type] = 0
|
| 611 |
-
entity_types[entity_type] += 1
|
| 612 |
-
if entity_types:
|
| 613 |
-
ax.bar(entity_types.keys(), entity_types.values(), color='#00bfae')
|
| 614 |
-
ax.set_title('Entity Types Found in Text', fontsize=14, fontweight='bold', color='#00bfae')
|
| 615 |
-
ax.set_ylabel('Count', color='#00bfae')
|
| 616 |
-
plt.xticks(rotation=45, color='#222')
|
| 617 |
-
plt.yticks(color='#222')
|
| 618 |
-
return self.fig_to_html(fig)
|
| 619 |
-
|
| 620 |
-
def create_multimodal_visualization(self, results):
|
| 621 |
-
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 6))
|
| 622 |
-
confidence = results.get("confidence", 0)
|
| 623 |
-
ax1.pie([confidence, 1-confidence], labels=['Confidence', 'Uncertainty'],
|
| 624 |
-
colors=['#00bfae', '#ff7675'], autopct='%1.1f%%', textprops={'color': '#222'})
|
| 625 |
-
ax1.set_title('Analysis Confidence', fontweight='bold', color='#00bfae')
|
| 626 |
-
recommendations = results.get("recommendations", [])
|
| 627 |
-
ax2.bar(['Recommendations'], [len(recommendations)], color='#6c63ff')
|
| 628 |
-
ax2.set_title('Number of Recommendations', fontweight='bold', color='#6c63ff')
|
| 629 |
-
ax2.set_ylabel('Count', color='#6c63ff')
|
| 630 |
-
plt.tight_layout()
|
| 631 |
-
return self.fig_to_html(fig)
|
| 632 |
|
| 633 |
def fig_to_html(self, fig):
|
| 634 |
-
|
| 635 |
-
|
| 636 |
-
|
| 637 |
-
|
| 638 |
-
|
| 639 |
-
|
| 640 |
-
|
| 641 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 642 |
|
| 643 |
def complete_appointment(appointment_id):
|
| 644 |
try:
|
|
|
|
| 386 |
import logging
|
| 387 |
import os
|
| 388 |
import sys
|
| 389 |
+
import tempfile
|
| 390 |
from pathlib import Path
|
| 391 |
import requests
|
| 392 |
import gradio as gr
|
| 393 |
import matplotlib.pyplot as plt
|
| 394 |
from PIL import Image
|
|
|
|
| 395 |
|
| 396 |
+
# Import configuration for end consultation logic
|
| 397 |
try:
|
| 398 |
from .config import get_flask_urls, get_doctors_page_urls, TIMEOUT_SETTINGS
|
| 399 |
except ImportError:
|
|
|
|
| 411 |
}
|
| 412 |
TIMEOUT_SETTINGS = {"connection_timeout": 5, "request_timeout": 10}
|
| 413 |
|
| 414 |
+
# Add parent directory to path
|
| 415 |
parent_dir = os.path.dirname(os.path.abspath(__file__))
|
| 416 |
sys.path.append(parent_dir)
|
| 417 |
|
| 418 |
+
# Import our modules for model and utility logic
|
| 419 |
+
from models.multimodal_fusion import MultimodalFusion
|
| 420 |
+
from utils.preprocessing import enhance_xray_image, normalize_report_text
|
| 421 |
+
from utils.visualization import (
|
| 422 |
+
plot_image_prediction,
|
| 423 |
+
plot_multimodal_results,
|
| 424 |
+
plot_report_entities,
|
| 425 |
+
)
|
| 426 |
+
|
| 427 |
+
# Set up logging
|
| 428 |
logging.basicConfig(
|
| 429 |
level=logging.INFO,
|
| 430 |
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
|
|
| 432 |
)
|
| 433 |
logger = logging.getLogger(__name__)
|
| 434 |
|
| 435 |
+
# Ensure sample data directory exists
|
| 436 |
+
os.makedirs(os.path.join(parent_dir, "data", "sample"), exist_ok=True)
|
| 437 |
+
|
| 438 |
class MediSyncApp:
|
| 439 |
+
"""
|
| 440 |
+
Main application class for the MediSync multi-modal medical analysis system.
|
| 441 |
+
"""
|
| 442 |
+
|
| 443 |
def __init__(self):
|
| 444 |
+
"""Initialize the application and load models."""
|
| 445 |
self.logger = logging.getLogger(__name__)
|
| 446 |
self.logger.info("Initializing MediSync application")
|
|
|
|
| 447 |
self.fusion_model = None
|
| 448 |
self.image_model = None
|
| 449 |
self.text_model = None
|
| 450 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 451 |
def load_models(self):
|
| 452 |
+
"""
|
| 453 |
+
Load models if not already loaded.
|
| 454 |
+
|
| 455 |
+
Returns:
|
| 456 |
+
bool: True if models loaded successfully, False otherwise
|
| 457 |
+
"""
|
| 458 |
try:
|
| 459 |
+
if self.fusion_model is None:
|
| 460 |
+
self.logger.info("Loading models...")
|
| 461 |
+
self.fusion_model = MultimodalFusion()
|
| 462 |
+
self.image_model = self.fusion_model.image_analyzer
|
| 463 |
+
self.text_model = self.fusion_model.text_analyzer
|
| 464 |
+
self.logger.info("Models loaded successfully")
|
| 465 |
return True
|
| 466 |
except Exception as e:
|
| 467 |
self.logger.error(f"Error loading models: {e}")
|
| 468 |
return False
|
| 469 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 470 |
def analyze_image(self, image):
|
| 471 |
+
"""
|
| 472 |
+
Analyze a medical image.
|
| 473 |
+
|
| 474 |
+
Args:
|
| 475 |
+
image: Image file uploaded through Gradio
|
| 476 |
+
|
| 477 |
+
Returns:
|
| 478 |
+
tuple: (image, image_results_html, plot_as_html)
|
| 479 |
+
"""
|
| 480 |
try:
|
| 481 |
+
if image is None:
|
| 482 |
+
return None, "Please upload an image first.", None
|
| 483 |
+
if not self.load_models() or self.image_model is None:
|
| 484 |
+
return image, "Error: Models not loaded properly.", None
|
| 485 |
+
|
| 486 |
+
temp_dir = tempfile.mkdtemp()
|
| 487 |
+
temp_path = os.path.join(temp_dir, "upload.png")
|
| 488 |
+
if isinstance(image, str):
|
| 489 |
+
from shutil import copyfile
|
| 490 |
+
copyfile(image, temp_path)
|
| 491 |
+
else:
|
| 492 |
+
image.save(temp_path)
|
| 493 |
+
|
| 494 |
+
self.logger.info(f"Analyzing image: {temp_path}")
|
| 495 |
+
results = self.image_model.analyze(temp_path)
|
| 496 |
+
|
| 497 |
+
fig = plot_image_prediction(
|
| 498 |
image,
|
| 499 |
results.get("predictions", []),
|
| 500 |
+
f"Primary Finding: {results.get('primary_finding', 'Unknown')}",
|
| 501 |
)
|
| 502 |
plot_html = self.fig_to_html(fig)
|
| 503 |
+
|
| 504 |
+
html_result = f"""
|
| 505 |
+
<div class="medisync-card medisync-card-bg medisync-force-text">
|
| 506 |
+
<h2 class="medisync-title medisync-blue">X-ray Analysis Results</h2>
|
| 507 |
+
<p><strong>Primary Finding:</strong> {results.get("primary_finding", "Unknown")}</p>
|
| 508 |
+
<p><strong>Confidence:</strong> {results.get("confidence", 0):.1%}</p>
|
| 509 |
+
<p><strong>Abnormality Detected:</strong> {"Yes" if results.get("has_abnormality", False) else "No"}</p>
|
| 510 |
+
<h3>Top Predictions:</h3>
|
| 511 |
+
<ul>
|
| 512 |
+
"""
|
| 513 |
+
for label, prob in results.get("predictions", [])[:5]:
|
| 514 |
+
html_result += f"<li>{label}: {prob:.1%}</li>"
|
| 515 |
+
html_result += "</ul>"
|
| 516 |
+
explanation = self.image_model.get_explanation(results)
|
| 517 |
+
html_result += f"<h3>Analysis Explanation:</h3><p>{explanation}</p>"
|
| 518 |
+
html_result += "</div>"
|
| 519 |
return image, html_result, plot_html
|
| 520 |
except Exception as e:
|
| 521 |
self.logger.error(f"Error in image analysis: {e}")
|
| 522 |
return image, f"Error analyzing image: {str(e)}", None
|
| 523 |
|
| 524 |
def analyze_text(self, text):
|
| 525 |
+
"""
|
| 526 |
+
Analyze a medical report text.
|
| 527 |
+
|
| 528 |
+
Args:
|
| 529 |
+
text: Report text input through Gradio
|
| 530 |
+
|
| 531 |
+
Returns:
|
| 532 |
+
tuple: (text, text_results_html, entities_plot_html)
|
| 533 |
+
"""
|
| 534 |
try:
|
| 535 |
+
if not text or text.strip() == "":
|
| 536 |
+
return "", "Please enter medical report text.", None
|
| 537 |
+
if not self.load_models() or self.text_model is None:
|
| 538 |
+
return text, "Error: Models not loaded properly.", None
|
| 539 |
+
if not text or len(text.strip()) < 10:
|
| 540 |
+
return (
|
| 541 |
+
text,
|
| 542 |
+
"Error: Please enter a valid medical report text (at least 10 characters).",
|
| 543 |
+
None,
|
| 544 |
+
)
|
| 545 |
+
normalized_text = normalize_report_text(text)
|
| 546 |
+
self.logger.info("Analyzing medical report text")
|
| 547 |
+
results = self.text_model.analyze(normalized_text)
|
| 548 |
+
entities = results.get("entities", {})
|
| 549 |
+
fig = plot_report_entities(normalized_text, entities)
|
| 550 |
+
entities_plot_html = self.fig_to_html(fig)
|
| 551 |
+
html_result = f"""
|
| 552 |
+
<div class="medisync-card medisync-card-bg medisync-force-text">
|
| 553 |
+
<h2 class="medisync-title medisync-green">Text Analysis Results</h2>
|
| 554 |
+
<p><strong>Severity Level:</strong> {results.get("severity", {}).get("level", "Unknown")}</p>
|
| 555 |
+
<p><strong>Severity Score:</strong> {results.get("severity", {}).get("score", 0)}/4</p>
|
| 556 |
+
<p><strong>Confidence:</strong> {results.get("severity", {}).get("confidence", 0):.1%}</p>
|
| 557 |
+
<h3>Key Findings:</h3>
|
| 558 |
+
<ul>
|
| 559 |
+
"""
|
| 560 |
+
findings = results.get("findings", [])
|
| 561 |
+
if findings:
|
| 562 |
+
for finding in findings:
|
| 563 |
+
html_result += f"<li>{finding}</li>"
|
| 564 |
+
else:
|
| 565 |
+
html_result += "<li>No specific findings detailed.</li>"
|
| 566 |
+
html_result += "</ul>"
|
| 567 |
+
html_result += "<h3>Extracted Medical Entities:</h3>"
|
| 568 |
+
for category, items in entities.items():
|
| 569 |
+
if items:
|
| 570 |
+
html_result += f"<p><strong>{category.capitalize()}:</strong> {', '.join(items)}</p>"
|
| 571 |
+
html_result += "<h3>Follow-up Recommendations:</h3><ul>"
|
| 572 |
+
followups = results.get("followup_recommendations", [])
|
| 573 |
+
if followups:
|
| 574 |
+
for rec in followups:
|
| 575 |
+
html_result += f"<li>{rec}</li>"
|
| 576 |
+
else:
|
| 577 |
+
html_result += "<li>No specific follow-up recommendations.</li>"
|
| 578 |
+
html_result += "</ul></div>"
|
| 579 |
+
return text, html_result, entities_plot_html
|
| 580 |
except Exception as e:
|
| 581 |
self.logger.error(f"Error in text analysis: {e}")
|
| 582 |
return text, f"Error analyzing text: {str(e)}", None
|
| 583 |
|
| 584 |
def analyze_multimodal(self, image, text):
|
| 585 |
+
"""
|
| 586 |
+
Perform multimodal analysis of image and text.
|
| 587 |
+
|
| 588 |
+
Args:
|
| 589 |
+
image: Image file uploaded through Gradio
|
| 590 |
+
text: Report text input through Gradio
|
| 591 |
+
|
| 592 |
+
Returns:
|
| 593 |
+
tuple: (results_html, multimodal_plot_html)
|
| 594 |
+
"""
|
| 595 |
try:
|
| 596 |
+
if not self.load_models() or self.fusion_model is None:
|
| 597 |
+
return "Error: Models not loaded properly.", None
|
| 598 |
+
if image is None:
|
| 599 |
+
return "Error: Please upload an X-ray image for analysis.", None
|
| 600 |
+
if not text or len(text.strip()) < 10:
|
| 601 |
+
return (
|
| 602 |
+
"Error: Please enter a valid medical report text (at least 10 characters).",
|
| 603 |
+
None,
|
| 604 |
+
)
|
| 605 |
+
temp_dir = tempfile.mkdtemp()
|
| 606 |
+
temp_path = os.path.join(temp_dir, "upload.png")
|
| 607 |
+
if isinstance(image, str):
|
| 608 |
+
from shutil import copyfile
|
| 609 |
+
copyfile(image, temp_path)
|
| 610 |
+
else:
|
| 611 |
+
image.save(temp_path)
|
| 612 |
+
normalized_text = normalize_report_text(text)
|
| 613 |
self.logger.info("Performing multimodal analysis")
|
| 614 |
+
results = self.fusion_model.analyze(temp_path, normalized_text)
|
| 615 |
+
fig = plot_multimodal_results(results, image, text)
|
| 616 |
+
plot_html = self.fig_to_html(fig)
|
| 617 |
+
explanation = self.fusion_model.get_explanation(results)
|
| 618 |
+
html_result = f"""
|
| 619 |
+
<div class="medisync-card medisync-card-bg medisync-force-text">
|
| 620 |
+
<h2 class="medisync-title medisync-purple">Multimodal Analysis Results</h2>
|
| 621 |
+
<h3>Overview</h3>
|
| 622 |
+
<p><strong>Primary Finding:</strong> {results.get("primary_finding", "Unknown")}</p>
|
| 623 |
+
<p><strong>Severity Level:</strong> {results.get("severity", {}).get("level", "Unknown")}</p>
|
| 624 |
+
<p><strong>Severity Score:</strong> {results.get("severity", {}).get("score", 0)}/4</p>
|
| 625 |
+
<p><strong>Agreement Score:</strong> {results.get("agreement_score", 0):.0%}</p>
|
| 626 |
+
<h3>Detailed Findings</h3>
|
| 627 |
+
<ul>
|
| 628 |
+
"""
|
| 629 |
+
findings = results.get("findings", [])
|
| 630 |
+
if findings:
|
| 631 |
+
for finding in findings:
|
| 632 |
+
html_result += f"<li>{finding}</li>"
|
| 633 |
+
else:
|
| 634 |
+
html_result += "<li>No specific findings detailed.</li>"
|
| 635 |
+
html_result += "</ul>"
|
| 636 |
+
html_result += "<h3>Recommended Follow-up</h3><ul>"
|
| 637 |
+
followups = results.get("followup_recommendations", [])
|
| 638 |
+
if followups:
|
| 639 |
+
for rec in followups:
|
| 640 |
+
html_result += f"<li>{rec}</li>"
|
| 641 |
+
else:
|
| 642 |
+
html_result += "<li>No specific follow-up recommendations provided.</li>"
|
| 643 |
+
html_result += "</ul>"
|
| 644 |
+
confidence = results.get("severity", {}).get("confidence", 0)
|
| 645 |
+
html_result += f"""
|
| 646 |
+
<p><em>Note: This analysis has a confidence level of {confidence:.0%}.
|
| 647 |
+
Please consult with healthcare professionals for official diagnosis.</em></p>
|
| 648 |
+
<h3>Analysis Explanation:</h3>
|
| 649 |
+
<p>{explanation}</p>
|
| 650 |
+
</div>
|
| 651 |
+
"""
|
| 652 |
return html_result, plot_html
|
| 653 |
except Exception as e:
|
| 654 |
self.logger.error(f"Error in multimodal analysis: {e}")
|
| 655 |
return f"Error in multimodal analysis: {str(e)}", None
|
| 656 |
|
| 657 |
+
def enhance_image(self, image):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 658 |
"""
|
| 659 |
+
Enhance X-ray image contrast.
|
| 660 |
+
|
| 661 |
+
Args:
|
| 662 |
+
image: Image file uploaded through Gradio
|
| 663 |
+
|
| 664 |
+
Returns:
|
| 665 |
+
PIL.Image: Enhanced image
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 666 |
"""
|
| 667 |
+
try:
|
| 668 |
+
if image is None:
|
| 669 |
+
return None
|
| 670 |
+
temp_dir = tempfile.mkdtemp()
|
| 671 |
+
temp_path = os.path.join(temp_dir, "upload.png")
|
| 672 |
+
if isinstance(image, str):
|
| 673 |
+
from shutil import copyfile
|
| 674 |
+
copyfile(image, temp_path)
|
| 675 |
+
else:
|
| 676 |
+
image.save(temp_path)
|
| 677 |
+
self.logger.info(f"Enhancing image: {temp_path}")
|
| 678 |
+
output_path = os.path.join(temp_dir, "enhanced.png")
|
| 679 |
+
enhance_xray_image(temp_path, output_path)
|
| 680 |
+
enhanced = Image.open(output_path)
|
| 681 |
+
return enhanced
|
| 682 |
+
except Exception as e:
|
| 683 |
+
self.logger.error(f"Error enhancing image: {e}")
|
| 684 |
+
return image
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 685 |
|
| 686 |
def fig_to_html(self, fig):
|
| 687 |
+
"""Convert matplotlib figure to HTML for display in Gradio."""
|
| 688 |
+
try:
|
| 689 |
+
import base64
|
| 690 |
+
import io
|
| 691 |
+
buf = io.BytesIO()
|
| 692 |
+
fig.savefig(buf, format="png", bbox_inches="tight", dpi=100, facecolor=fig.get_facecolor())
|
| 693 |
+
buf.seek(0)
|
| 694 |
+
img_str = base64.b64encode(buf.read()).decode("utf-8")
|
| 695 |
+
plt.close(fig)
|
| 696 |
+
return f'<img src="data:image/png;base64,{img_str}" style="max-width: 100%; height: auto; background: transparent;"/>'
|
| 697 |
+
except Exception as e:
|
| 698 |
+
self.logger.error(f"Error converting figure to HTML: {e}")
|
| 699 |
+
return "<p>Error displaying visualization.</p>"
|
| 700 |
|
| 701 |
def complete_appointment(appointment_id):
|
| 702 |
try:
|