dcavadia commited on
Commit
bc1fb7d
·
1 Parent(s): b46360a

update code comments and language

Browse files
app.py CHANGED
@@ -1,18 +1,14 @@
1
  """
2
- MelanoScope AI - Enterprise-ready Skin Lesion Classification Application
3
-
4
- A production-ready deep learning application for dermatoscopic image analysis
5
- using ONNX Runtime and Gradio for web interface deployment.
6
 
 
7
  Author: Daniel Cavadia
8
  Institution: Universidad Central de Venezuela
9
- Version: 1.0
10
  """
11
  import logging
12
  import sys
13
  from pathlib import Path
14
 
15
- # Add src to Python path
16
  sys.path.insert(0, str(Path(__file__).parent / "src"))
17
 
18
  from src.config.settings import LogConfig, AppConfig, EnvConfig
@@ -22,46 +18,19 @@ from src.ui.components import MelanoScopeUI
22
  def setup_logging() -> None:
23
  """Configure application logging."""
24
  log_level = getattr(logging, LogConfig.LOG_LEVEL.upper(), logging.INFO)
25
-
26
- logging.basicConfig(
27
- level=log_level,
28
- format=LogConfig.LOG_FORMAT,
29
- handlers=[
30
- logging.StreamHandler(sys.stdout),
31
- ]
32
- )
33
-
34
- # Add file handler in production
35
- if not EnvConfig.DEBUG:
36
- try:
37
- file_handler = logging.FileHandler(LogConfig.LOG_FILE)
38
- file_handler.setFormatter(logging.Formatter(LogConfig.LOG_FORMAT))
39
- logging.getLogger().addHandler(file_handler)
40
- except Exception as e:
41
- logging.warning(f"Could not create log file handler: {e}")
42
 
43
  def create_application():
44
- """
45
- Create and configure the MelanoScope AI application.
46
-
47
- Returns:
48
- Configured Gradio interface
49
- """
50
  logger = logging.getLogger(__name__)
51
 
52
  try:
53
  logger.info(f"Initializing {AppConfig.TITLE} v{AppConfig.VERSION}")
54
 
55
- # Initialize model
56
- logger.info("Loading model and medical data...")
57
  model = MelanoScopeModel()
58
-
59
- # Log model information
60
  model_info = model.get_model_info()
61
  logger.info(f"Model loaded with {model_info['num_classes']} classes")
62
 
63
- # Initialize UI
64
- logger.info("Creating user interface...")
65
  ui = MelanoScopeUI(model, model.classes)
66
  interface = ui.create_interface()
67
 
@@ -69,29 +38,21 @@ def create_application():
69
  return interface
70
 
71
  except Exception as e:
72
- logger.error(f"Failed to initialize application: {e}")
73
- raise RuntimeError(f"Application initialization failed: {e}")
74
 
75
  def main():
76
- """Main entry point for the application."""
77
- # Set up logging
78
  setup_logging()
79
  logger = logging.getLogger(__name__)
80
 
81
  try:
82
- # Create application
83
  app = create_application()
84
-
85
- # Launch application
86
  logger.info("Launching MelanoScope AI interface...")
87
  app.launch(
88
  server_name="0.0.0.0" if not EnvConfig.DEBUG else "127.0.0.1",
89
- server_port=7860,
90
- share=False,
91
- debug=EnvConfig.DEBUG,
92
- show_error=EnvConfig.DEBUG
93
  )
94
-
95
  except KeyboardInterrupt:
96
  logger.info("Application shutdown requested")
97
  except Exception as e:
 
1
  """
2
+ MelanoScope AI - Skin Lesion Classification Application
 
 
 
3
 
4
+ Enterprise-ready deep learning application for dermatoscopic image analysis.
5
  Author: Daniel Cavadia
6
  Institution: Universidad Central de Venezuela
 
7
  """
8
  import logging
9
  import sys
10
  from pathlib import Path
11
 
 
12
  sys.path.insert(0, str(Path(__file__).parent / "src"))
13
 
14
  from src.config.settings import LogConfig, AppConfig, EnvConfig
 
18
  def setup_logging() -> None:
19
  """Configure application logging."""
20
  log_level = getattr(logging, LogConfig.LOG_LEVEL.upper(), logging.INFO)
21
+ logging.basicConfig(level=log_level, format=LogConfig.LOG_FORMAT, handlers=[logging.StreamHandler(sys.stdout)])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
 
23
  def create_application():
24
+ """Create and configure the application."""
 
 
 
 
 
25
  logger = logging.getLogger(__name__)
26
 
27
  try:
28
  logger.info(f"Initializing {AppConfig.TITLE} v{AppConfig.VERSION}")
29
 
 
 
30
  model = MelanoScopeModel()
 
 
31
  model_info = model.get_model_info()
32
  logger.info(f"Model loaded with {model_info['num_classes']} classes")
33
 
 
 
34
  ui = MelanoScopeUI(model, model.classes)
35
  interface = ui.create_interface()
36
 
 
38
  return interface
39
 
40
  except Exception as e:
41
+ logger.error(f"Application initialization failed: {e}")
42
+ raise RuntimeError(f"Initialization failed: {e}")
43
 
44
  def main():
45
+ """Main entry point."""
 
46
  setup_logging()
47
  logger = logging.getLogger(__name__)
48
 
49
  try:
 
50
  app = create_application()
 
 
51
  logger.info("Launching MelanoScope AI interface...")
52
  app.launch(
53
  server_name="0.0.0.0" if not EnvConfig.DEBUG else "127.0.0.1",
54
+ server_port=7860, share=False, debug=EnvConfig.DEBUG, show_error=EnvConfig.DEBUG
 
 
 
55
  )
 
56
  except KeyboardInterrupt:
57
  logger.info("Application shutdown requested")
58
  except Exception as e:
src/config/settings.py CHANGED
@@ -1,81 +1,52 @@
1
- """
2
- Configuration settings for MelanoScope AI application.
3
- Centralizes all constants and configuration parameters.
4
- """
5
  import os
6
- from typing import List, Dict, Any
7
  from pathlib import Path
8
 
9
- # Project paths
10
  PROJECT_ROOT = Path(__file__).parent.parent.parent
11
  DATA_FILE = PROJECT_ROOT / "data.json"
12
  MODEL_FILE = PROJECT_ROOT / "NFNetL0-0.961.onnx"
13
  EXAMPLES_DIR = PROJECT_ROOT / "examples"
14
 
15
- # Model configuration
16
  class ModelConfig:
17
- """Model-related configuration parameters."""
18
-
19
- # ONNX Runtime providers (in order of preference)
20
  ORT_PROVIDERS: List[str] = ["CPUExecutionProvider"]
21
-
22
- # Image preprocessing parameters
23
  IMAGE_SIZE: tuple[int, int] = (100, 100)
24
  NORMALIZATION_MEAN: List[float] = [0.7611, 0.5869, 0.5923]
25
  NORMALIZATION_STD: List[float] = [0.1266, 0.1487, 0.1619]
26
-
27
- # Inference parameters
28
- PROBABILITY_PRECISION: int = 1 # Decimal places for confidence display
29
- PROBABILITY_SUM: int = 100 # Total sum for probability distribution
30
 
31
- # UI configuration
32
  class UIConfig:
33
- """User interface configuration parameters."""
34
-
35
- # Theme settings
36
  THEME_PRIMARY_HUE: str = "rose"
37
  THEME_SECONDARY_HUE: str = "slate"
38
-
39
- # Component dimensions
40
  IMAGE_HEIGHT: int = 420
41
  PLOT_WIDTH: int = 520
42
  PLOT_HEIGHT: int = 320
43
  TEXTBOX_LINES: int = 4
44
-
45
- # Layout settings
46
  LEFT_COLUMN_SCALE: int = 5
47
  RIGHT_COLUMN_SCALE: int = 5
48
- THEME_TOGGLE_MIN_WIDTH: int = 140
49
 
50
- # Application metadata
51
  class AppConfig:
52
- """Application metadata and information."""
53
-
54
- TITLE: str = "MelanoScope AI - Clasificación de Enfermedades de la Piel"
55
  VERSION: str = "1.0"
56
- LAST_UPDATE: str = "2025-08"
57
  INSTITUTION: str = "Universidad Central de Venezuela"
58
- DISCLAIMER: str = "Demo • No diagnóstico médico"
59
-
60
- # Medical disclaimer
61
  MEDICAL_DISCLAIMER: str = (
62
- "Esta herramienta es solo con fines educativos y no reemplaza "
63
- "una evaluación médica."
64
  )
65
 
66
- # Logging configuration
67
  class LogConfig:
68
- """Logging configuration parameters."""
69
-
70
  LOG_LEVEL: str = os.getenv("LOG_LEVEL", "INFO")
71
- LOG_FORMAT: str = (
72
- "%(asctime)s | %(name)s | %(levelname)s | %(message)s"
73
- )
74
  LOG_FILE: str = "melanoscope.log"
75
 
76
- # Environment settings
77
  class EnvConfig:
78
- """Environment-specific configuration."""
79
-
80
  DEBUG: bool = os.getenv("DEBUG", "False").lower() == "true"
81
  ENVIRONMENT: str = os.getenv("ENVIRONMENT", "production")
 
1
+ """Configuration settings for MelanoScope AI application."""
 
 
 
2
  import os
3
+ from typing import List
4
  from pathlib import Path
5
 
 
6
  PROJECT_ROOT = Path(__file__).parent.parent.parent
7
  DATA_FILE = PROJECT_ROOT / "data.json"
8
  MODEL_FILE = PROJECT_ROOT / "NFNetL0-0.961.onnx"
9
  EXAMPLES_DIR = PROJECT_ROOT / "examples"
10
 
 
11
  class ModelConfig:
12
+ """Model configuration parameters."""
 
 
13
  ORT_PROVIDERS: List[str] = ["CPUExecutionProvider"]
 
 
14
  IMAGE_SIZE: tuple[int, int] = (100, 100)
15
  NORMALIZATION_MEAN: List[float] = [0.7611, 0.5869, 0.5923]
16
  NORMALIZATION_STD: List[float] = [0.1266, 0.1487, 0.1619]
17
+ PROBABILITY_PRECISION: int = 1
18
+ PROBABILITY_SUM: int = 100
 
 
19
 
 
20
  class UIConfig:
21
+ """UI configuration parameters."""
 
 
22
  THEME_PRIMARY_HUE: str = "rose"
23
  THEME_SECONDARY_HUE: str = "slate"
 
 
24
  IMAGE_HEIGHT: int = 420
25
  PLOT_WIDTH: int = 520
26
  PLOT_HEIGHT: int = 320
27
  TEXTBOX_LINES: int = 4
 
 
28
  LEFT_COLUMN_SCALE: int = 5
29
  RIGHT_COLUMN_SCALE: int = 5
 
30
 
 
31
  class AppConfig:
32
+ """Application metadata."""
33
+ TITLE: str = "MelanoScope AI - Skin Lesion Classification"
 
34
  VERSION: str = "1.0"
35
+ LAST_UPDATE: str = "2025-09"
36
  INSTITUTION: str = "Universidad Central de Venezuela"
37
+ DISCLAIMER: str = "Demo • Not for medical diagnosis"
 
 
38
  MEDICAL_DISCLAIMER: str = (
39
+ "This tool is for educational purposes only and does not replace "
40
+ "professional medical evaluation."
41
  )
42
 
 
43
  class LogConfig:
44
+ """Logging configuration."""
 
45
  LOG_LEVEL: str = os.getenv("LOG_LEVEL", "INFO")
46
+ LOG_FORMAT: str = "%(asctime)s | %(name)s | %(levelname)s | %(message)s"
 
 
47
  LOG_FILE: str = "melanoscope.log"
48
 
 
49
  class EnvConfig:
50
+ """Environment settings."""
 
51
  DEBUG: bool = os.getenv("DEBUG", "False").lower() == "true"
52
  ENVIRONMENT: str = os.getenv("ENVIRONMENT", "production")
src/core/model.py CHANGED
@@ -1,128 +1,81 @@
1
- """
2
- Model inference module for MelanoScope AI.
3
- Handles ONNX model loading and inference operations.
4
- """
5
  import json
6
  import logging
7
  import time
8
  from typing import Dict, Any, List, Optional, Tuple
9
- from pathlib import Path
10
  import numpy as np
11
  import onnxruntime as ort
12
- from PIL import Image
13
-
14
  from ..config.settings import ModelConfig, DATA_FILE, MODEL_FILE
15
  from .preprocessing import ImagePreprocessor
16
  from .utils import probabilities_to_ints, format_confidence
17
 
18
- # Configure logger
19
  logger = logging.getLogger(__name__)
20
 
21
  class MelanoScopeModel:
22
- """
23
- MelanoScope AI model for skin lesion classification.
24
-
25
- Handles model loading, inference, and result processing.
26
- """
27
 
28
  def __init__(self):
29
- """Initialize the model and load medical condition data."""
30
  self.preprocessor = ImagePreprocessor()
31
  self.session: Optional[ort.InferenceSession] = None
32
  self.classes: List[str] = []
33
  self.medical_data: Dict[str, Any] = {}
34
 
35
- # Load model and data
36
  self._load_model()
37
  self._load_medical_data()
38
-
39
- logger.info(f"MelanoScopeModel initialized with {len(self.classes)} classes")
40
 
41
  def _load_model(self) -> None:
42
- """Load the ONNX model for inference."""
43
  try:
44
  if not MODEL_FILE.exists():
45
  raise FileNotFoundError(f"Model file not found: {MODEL_FILE}")
46
 
47
- self.session = ort.InferenceSession(
48
- str(MODEL_FILE),
49
- providers=ModelConfig.ORT_PROVIDERS
50
- )
51
-
52
- # Log model information
53
- input_info = self.session.get_inputs()[0]
54
- logger.info(f"Model loaded successfully")
55
- logger.debug(f"Input shape: {input_info.shape}, Input type: {input_info.type}")
56
 
57
  except Exception as e:
58
  logger.error(f"Failed to load model: {e}")
59
  raise RuntimeError(f"Model loading failed: {e}")
60
 
61
  def _load_medical_data(self) -> None:
62
- """Load medical condition data and class names."""
63
  try:
64
- if not DATA_FILE.exists():
65
- raise FileNotFoundError(f"Data file not found: {DATA_FILE}")
66
-
67
  with open(DATA_FILE, "r", encoding="utf-8") as f:
68
  self.medical_data = json.load(f)
69
 
70
  self.classes = list(self.medical_data.keys())
71
- logger.info(f"Loaded medical data for {len(self.classes)} conditions")
72
 
73
  except Exception as e:
74
  logger.error(f"Failed to load medical data: {e}")
75
  raise RuntimeError(f"Medical data loading failed: {e}")
76
 
77
  def predict(self, image_input: Any) -> Tuple[str, str, str, str, str, str, Any, str]:
78
- """
79
- Perform inference on input image.
80
-
81
- Args:
82
- image_input: Input image (PIL Image, numpy array, or None)
83
-
84
- Returns:
85
- Tuple containing (prediction, confidence, description, symptoms,
86
- causes, treatment, probability_df, latency)
87
- """
88
- # Handle empty input
89
  if image_input is None:
90
- logger.warning("Received None image input")
91
- return self._create_empty_result("Cargue una imagen y presione Analizar.")
92
 
93
  try:
94
- # Start timing
95
  start_time = time.time()
96
 
97
- # Preprocess image
98
  input_tensor = self.preprocessor.preprocess(image_input)
99
  if input_tensor is None:
100
- return self._create_empty_result("Imagen inválida")
101
 
102
- # Run inference
103
  prediction_result = self._run_inference(input_tensor)
104
  if prediction_result is None:
105
- return self._create_empty_result("Error en la inferencia")
106
 
107
- # Process results
108
  pred_name, confidence, prob_df = prediction_result
109
  medical_info = self._get_medical_info(pred_name)
110
 
111
- # Calculate latency
112
  latency_ms = int((time.time() - start_time) * 1000)
113
- latency_str = f"{latency_ms} ms"
114
-
115
- logger.info(f"Prediction completed: {pred_name} ({confidence}) in {latency_ms}ms")
116
 
117
  return (
118
- pred_name,
119
- confidence,
120
- medical_info["description"],
121
- medical_info["symptoms"],
122
- medical_info["causes"],
123
- medical_info["treatment"],
124
- prob_df,
125
- latency_str
126
  )
127
 
128
  except Exception as e:
@@ -130,42 +83,23 @@ class MelanoScopeModel:
130
  return self._create_empty_result(f"Error: {str(e)}")
131
 
132
  def _run_inference(self, input_tensor: np.ndarray) -> Optional[Tuple[str, str, Any]]:
133
- """
134
- Run model inference on preprocessed input.
135
-
136
- Args:
137
- input_tensor: Preprocessed image tensor
138
-
139
- Returns:
140
- Tuple of (prediction_name, confidence_string, probability_dataframe)
141
- """
142
  try:
143
- if self.session is None:
144
- raise RuntimeError("Model not loaded")
145
-
146
- # Get input name
147
  input_name = self.session.get_inputs()[0].name
148
-
149
- # Run inference
150
  output = self.session.run(None, {input_name: input_tensor})
151
  logits = output[0].squeeze()
152
 
153
- # Get prediction
154
  pred_idx = int(np.argmax(logits))
155
  pred_name = self.classes[pred_idx]
156
 
157
- # Calculate softmax probabilities
158
  exp_logits = np.exp(logits - np.max(logits))
159
  probabilities = exp_logits / exp_logits.sum()
160
 
161
- # Format confidence
162
  confidence = format_confidence(probabilities[pred_idx])
163
-
164
- # Create probability dataframe
165
  prob_ints = probabilities_to_ints(probabilities * 100.0)
166
  prob_df = self._create_probability_dataframe(prob_ints)
167
 
168
- logger.debug(f"Inference completed: {pred_name} with confidence {confidence}")
169
  return pred_name, confidence, prob_df
170
 
171
  except Exception as e:
@@ -173,80 +107,33 @@ class MelanoScopeModel:
173
  return None
174
 
175
  def _create_probability_dataframe(self, probabilities: np.ndarray) -> Any:
176
- """Create a sorted probability dataframe for visualization."""
177
  try:
178
  import pandas as pd
179
-
180
- df = pd.DataFrame({
181
  "item": self.classes,
182
  "probability": probabilities.astype(int)
183
  }).sort_values("probability", ascending=True)
184
-
185
- return df
186
-
187
  except Exception as e:
188
- logger.error(f"Error creating probability dataframe: {e}")
189
- # Return empty dataframe as fallback
190
  import pandas as pd
191
  return pd.DataFrame({"item": self.classes, "probability": [0] * len(self.classes)})
192
 
193
  def _get_medical_info(self, condition_name: str) -> Dict[str, str]:
194
- """
195
- Get medical information for a specific condition.
196
-
197
- Args:
198
- condition_name: Name of the medical condition
199
-
200
- Returns:
201
- Dictionary containing medical information
202
- """
203
- try:
204
- condition_data = self.medical_data.get(condition_name, {})
205
-
206
- return {
207
- "description": condition_data.get("description", ""),
208
- "symptoms": condition_data.get("symptoms", ""),
209
- "causes": condition_data.get("causes", ""),
210
- "treatment": condition_data.get("treatment-1", "")
211
- }
212
-
213
- except Exception as e:
214
- logger.error(f"Error getting medical info for {condition_name}: {e}")
215
- return {"description": "", "symptoms": "", "causes": "", "treatment": ""}
216
 
217
  def _create_empty_result(self, message: str) -> Tuple[str, str, str, str, str, str, Any, str]:
218
- """Create an empty result tuple with error message."""
219
  try:
220
  import pandas as pd
221
  empty_df = pd.DataFrame({"item": self.classes, "probability": [0] * len(self.classes)})
222
  except:
223
  empty_df = None
224
-
225
  return (message, "", "", "", "", "", empty_df, "")
226
-
227
- def get_model_info(self) -> Dict[str, Any]:
228
- """
229
- Get information about the loaded model.
230
-
231
- Returns:
232
- Dictionary containing model metadata
233
- """
234
- info = {
235
- "classes": self.classes,
236
- "num_classes": len(self.classes),
237
- "model_file": str(MODEL_FILE),
238
- "providers": ModelConfig.ORT_PROVIDERS
239
- }
240
-
241
- if self.session:
242
- try:
243
- input_info = self.session.get_inputs()[0]
244
- info.update({
245
- "input_shape": input_info.shape,
246
- "input_type": input_info.type,
247
- "input_name": input_info.name
248
- })
249
- except Exception as e:
250
- logger.warning(f"Could not get model input info: {e}")
251
-
252
- return info
 
1
+ """Model inference for MelanoScope AI."""
 
 
 
2
  import json
3
  import logging
4
  import time
5
  from typing import Dict, Any, List, Optional, Tuple
 
6
  import numpy as np
7
  import onnxruntime as ort
 
 
8
  from ..config.settings import ModelConfig, DATA_FILE, MODEL_FILE
9
  from .preprocessing import ImagePreprocessor
10
  from .utils import probabilities_to_ints, format_confidence
11
 
 
12
  logger = logging.getLogger(__name__)
13
 
14
  class MelanoScopeModel:
15
+ """MelanoScope AI model for skin lesion classification."""
 
 
 
 
16
 
17
  def __init__(self):
 
18
  self.preprocessor = ImagePreprocessor()
19
  self.session: Optional[ort.InferenceSession] = None
20
  self.classes: List[str] = []
21
  self.medical_data: Dict[str, Any] = {}
22
 
 
23
  self._load_model()
24
  self._load_medical_data()
25
+ logger.info(f"Model initialized with {len(self.classes)} classes")
 
26
 
27
  def _load_model(self) -> None:
28
+ """Load ONNX model."""
29
  try:
30
  if not MODEL_FILE.exists():
31
  raise FileNotFoundError(f"Model file not found: {MODEL_FILE}")
32
 
33
+ self.session = ort.InferenceSession(str(MODEL_FILE), providers=ModelConfig.ORT_PROVIDERS)
34
+ logger.info("Model loaded successfully")
 
 
 
 
 
 
 
35
 
36
  except Exception as e:
37
  logger.error(f"Failed to load model: {e}")
38
  raise RuntimeError(f"Model loading failed: {e}")
39
 
40
  def _load_medical_data(self) -> None:
41
+ """Load medical condition data."""
42
  try:
 
 
 
43
  with open(DATA_FILE, "r", encoding="utf-8") as f:
44
  self.medical_data = json.load(f)
45
 
46
  self.classes = list(self.medical_data.keys())
47
+ logger.info(f"Loaded data for {len(self.classes)} conditions")
48
 
49
  except Exception as e:
50
  logger.error(f"Failed to load medical data: {e}")
51
  raise RuntimeError(f"Medical data loading failed: {e}")
52
 
53
  def predict(self, image_input: Any) -> Tuple[str, str, str, str, str, str, Any, str]:
54
+ """Perform inference on input image."""
 
 
 
 
 
 
 
 
 
 
55
  if image_input is None:
56
+ return self._create_empty_result("Please upload an image and click Analyze.")
 
57
 
58
  try:
 
59
  start_time = time.time()
60
 
 
61
  input_tensor = self.preprocessor.preprocess(image_input)
62
  if input_tensor is None:
63
+ return self._create_empty_result("Invalid image")
64
 
 
65
  prediction_result = self._run_inference(input_tensor)
66
  if prediction_result is None:
67
+ return self._create_empty_result("Inference error")
68
 
 
69
  pred_name, confidence, prob_df = prediction_result
70
  medical_info = self._get_medical_info(pred_name)
71
 
 
72
  latency_ms = int((time.time() - start_time) * 1000)
 
 
 
73
 
74
  return (
75
+ pred_name, confidence,
76
+ medical_info["description"], medical_info["symptoms"],
77
+ medical_info["causes"], medical_info["treatment"],
78
+ prob_df, f"{latency_ms} ms"
 
 
 
 
79
  )
80
 
81
  except Exception as e:
 
83
  return self._create_empty_result(f"Error: {str(e)}")
84
 
85
  def _run_inference(self, input_tensor: np.ndarray) -> Optional[Tuple[str, str, Any]]:
86
+ """Run model inference."""
 
 
 
 
 
 
 
 
87
  try:
 
 
 
 
88
  input_name = self.session.get_inputs()[0].name
 
 
89
  output = self.session.run(None, {input_name: input_tensor})
90
  logits = output[0].squeeze()
91
 
 
92
  pred_idx = int(np.argmax(logits))
93
  pred_name = self.classes[pred_idx]
94
 
95
+ # Softmax probabilities
96
  exp_logits = np.exp(logits - np.max(logits))
97
  probabilities = exp_logits / exp_logits.sum()
98
 
 
99
  confidence = format_confidence(probabilities[pred_idx])
 
 
100
  prob_ints = probabilities_to_ints(probabilities * 100.0)
101
  prob_df = self._create_probability_dataframe(prob_ints)
102
 
 
103
  return pred_name, confidence, prob_df
104
 
105
  except Exception as e:
 
107
  return None
108
 
109
  def _create_probability_dataframe(self, probabilities: np.ndarray) -> Any:
110
+ """Create sorted probability dataframe."""
111
  try:
112
  import pandas as pd
113
+ return pd.DataFrame({
 
114
  "item": self.classes,
115
  "probability": probabilities.astype(int)
116
  }).sort_values("probability", ascending=True)
 
 
 
117
  except Exception as e:
118
+ logger.error(f"Error creating dataframe: {e}")
 
119
  import pandas as pd
120
  return pd.DataFrame({"item": self.classes, "probability": [0] * len(self.classes)})
121
 
122
  def _get_medical_info(self, condition_name: str) -> Dict[str, str]:
123
+ """Get medical information for condition."""
124
+ condition_data = self.medical_data.get(condition_name, {})
125
+ return {
126
+ "description": condition_data.get("description", ""),
127
+ "symptoms": condition_data.get("symptoms", ""),
128
+ "causes": condition_data.get("causes", ""),
129
+ "treatment": condition_data.get("treatment-1", "")
130
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
 
132
  def _create_empty_result(self, message: str) -> Tuple[str, str, str, str, str, str, Any, str]:
133
+ """Create empty result with error message."""
134
  try:
135
  import pandas as pd
136
  empty_df = pd.DataFrame({"item": self.classes, "probability": [0] * len(self.classes)})
137
  except:
138
  empty_df = None
 
139
  return (message, "", "", "", "", "", empty_df, "")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/core/preprocessing.py CHANGED
@@ -1,71 +1,39 @@
1
- """
2
- Image preprocessing module for MelanoScope AI.
3
- Handles image transformations and normalization.
4
- """
5
  import logging
6
  from typing import Union, Optional
7
  import numpy as np
8
  from PIL import Image
9
  from torchvision import transforms
10
-
11
  from ..config.settings import ModelConfig
12
 
13
- # Configure logger
14
  logger = logging.getLogger(__name__)
15
 
16
  class ImagePreprocessor:
17
  """Handles image preprocessing for model inference."""
18
 
19
  def __init__(self):
20
- """Initialize the preprocessor with configured transforms."""
21
  self.transforms = self._create_transform_pipeline()
22
  logger.info("ImagePreprocessor initialized")
23
 
24
  def _create_transform_pipeline(self) -> transforms.Compose:
25
- """
26
- Create the image transformation pipeline.
27
-
28
- Returns:
29
- Composed torchvision transforms
30
- """
31
- try:
32
- transform_pipeline = transforms.Compose([
33
- transforms.Resize(ModelConfig.IMAGE_SIZE),
34
- transforms.ToTensor(),
35
- transforms.Normalize(
36
- mean=ModelConfig.NORMALIZATION_MEAN,
37
- std=ModelConfig.NORMALIZATION_STD
38
- ),
39
- ])
40
- logger.debug("Transform pipeline created successfully")
41
- return transform_pipeline
42
- except Exception as e:
43
- logger.error(f"Error creating transform pipeline: {e}")
44
- raise
45
 
46
  def preprocess(self, image_input: Union[Image.Image, np.ndarray]) -> Optional[np.ndarray]:
47
- """
48
- Preprocess image for model inference.
49
-
50
- Args:
51
- image_input: PIL Image or numpy array
52
-
53
- Returns:
54
- Preprocessed image tensor as numpy array, or None if preprocessing fails
55
-
56
- Raises:
57
- ValueError: If image input is invalid
58
- """
59
  try:
60
- # Convert input to PIL Image
61
  pil_image = self._convert_to_pil(image_input)
62
  if pil_image is None:
63
  return None
64
 
65
- # Apply transforms and add batch dimension
66
  tensor = self.transforms(pil_image).unsqueeze(0).numpy()
67
-
68
- logger.debug(f"Image preprocessed to shape: {tensor.shape}")
69
  return tensor
70
 
71
  except Exception as e:
@@ -73,36 +41,12 @@ class ImagePreprocessor:
73
  return None
74
 
75
  def _convert_to_pil(self, image_input: Union[Image.Image, np.ndarray]) -> Optional[Image.Image]:
76
- """
77
- Convert various image formats to PIL Image.
78
-
79
- Args:
80
- image_input: Image in PIL or numpy format
81
-
82
- Returns:
83
- PIL Image in RGB mode, or None if conversion fails
84
- """
85
  try:
86
  if isinstance(image_input, Image.Image):
87
  return image_input.convert("RGB")
88
  else:
89
- # Assume numpy array
90
- pil_image = Image.fromarray(image_input).convert("RGB")
91
- return pil_image
92
-
93
  except Exception as e:
94
- logger.error(f"Error converting image to PIL format: {e}")
95
  return None
96
-
97
- def get_transform_info(self) -> dict:
98
- """
99
- Get information about the preprocessing transforms.
100
-
101
- Returns:
102
- Dictionary containing transform parameters
103
- """
104
- return {
105
- "image_size": ModelConfig.IMAGE_SIZE,
106
- "normalization_mean": ModelConfig.NORMALIZATION_MEAN,
107
- "normalization_std": ModelConfig.NORMALIZATION_STD
108
- }
 
1
+ """Image preprocessing for MelanoScope AI."""
 
 
 
2
  import logging
3
  from typing import Union, Optional
4
  import numpy as np
5
  from PIL import Image
6
  from torchvision import transforms
 
7
  from ..config.settings import ModelConfig
8
 
 
9
  logger = logging.getLogger(__name__)
10
 
11
  class ImagePreprocessor:
12
  """Handles image preprocessing for model inference."""
13
 
14
  def __init__(self):
 
15
  self.transforms = self._create_transform_pipeline()
16
  logger.info("ImagePreprocessor initialized")
17
 
18
  def _create_transform_pipeline(self) -> transforms.Compose:
19
+ """Create image transformation pipeline."""
20
+ return transforms.Compose([
21
+ transforms.Resize(ModelConfig.IMAGE_SIZE),
22
+ transforms.ToTensor(),
23
+ transforms.Normalize(
24
+ mean=ModelConfig.NORMALIZATION_MEAN,
25
+ std=ModelConfig.NORMALIZATION_STD
26
+ ),
27
+ ])
 
 
 
 
 
 
 
 
 
 
 
28
 
29
  def preprocess(self, image_input: Union[Image.Image, np.ndarray]) -> Optional[np.ndarray]:
30
+ """Preprocess image for model inference."""
 
 
 
 
 
 
 
 
 
 
 
31
  try:
 
32
  pil_image = self._convert_to_pil(image_input)
33
  if pil_image is None:
34
  return None
35
 
 
36
  tensor = self.transforms(pil_image).unsqueeze(0).numpy()
 
 
37
  return tensor
38
 
39
  except Exception as e:
 
41
  return None
42
 
43
  def _convert_to_pil(self, image_input: Union[Image.Image, np.ndarray]) -> Optional[Image.Image]:
44
+ """Convert image input to PIL Image in RGB mode."""
 
 
 
 
 
 
 
 
45
  try:
46
  if isinstance(image_input, Image.Image):
47
  return image_input.convert("RGB")
48
  else:
49
+ return Image.fromarray(image_input).convert("RGB")
 
 
 
50
  except Exception as e:
51
+ logger.error(f"Error converting to PIL: {e}")
52
  return None
 
 
 
 
 
 
 
 
 
 
 
 
 
src/core/utils.py CHANGED
@@ -1,50 +1,26 @@
1
- """
2
- Utility functions for MelanoScope AI.
3
- Contains helper functions and probability calculations.
4
- """
5
  import logging
6
- from typing import List, Dict, Any, Union
7
  import numpy as np
8
  import pandas as pd
9
-
10
  from ..config.settings import ModelConfig
11
 
12
- # Configure logger
13
  logger = logging.getLogger(__name__)
14
 
15
- def probabilities_to_ints(
16
- probabilities: np.ndarray,
17
- total_sum: int = ModelConfig.PROBABILITY_SUM
18
- ) -> np.ndarray:
19
- """
20
- Convert probability array to integer percentages that sum to total_sum.
21
-
22
- Args:
23
- probabilities: Array of probability values
24
- total_sum: Target sum for the integer percentages
25
-
26
- Returns:
27
- Array of integers that sum to total_sum
28
-
29
- Raises:
30
- ValueError: If probabilities contain invalid values
31
- """
32
  try:
33
  probabilities = np.array(probabilities)
34
-
35
- # Ensure non-negative values
36
  positive_values = np.maximum(probabilities, 0)
37
  total_positive = positive_values.sum()
38
 
39
  if total_positive == 0:
40
- logger.warning("All probabilities are zero or negative")
41
  return np.zeros_like(probabilities, dtype=int)
42
 
43
- # Scale to target sum
44
  scaled = positive_values / total_positive * total_sum
45
  rounded = np.round(scaled).astype(int)
46
 
47
- # Adjust for rounding errors
48
  diff = total_sum - rounded.sum()
49
  if diff != 0:
50
  max_idx = int(np.argmax(positive_values))
@@ -52,65 +28,20 @@ def probabilities_to_ints(
52
  rounded[max_idx] += diff
53
  rounded = rounded.reshape(scaled.shape)
54
 
55
- logger.debug(f"Converted probabilities to integers summing to {total_sum}")
56
  return rounded
57
 
58
  except Exception as e:
59
- logger.error(f"Error converting probabilities to integers: {e}")
60
  raise ValueError(f"Invalid probability values: {e}")
61
 
62
  def create_empty_dataframe(classes: List[str]) -> pd.DataFrame:
63
- """
64
- Create an empty probability dataframe with zero values.
65
-
66
- Args:
67
- classes: List of class names
68
-
69
- Returns:
70
- DataFrame with items and zero probabilities
71
- """
72
- logger.debug(f"Creating empty dataframe for {len(classes)} classes")
73
- return pd.DataFrame({
74
- "item": classes,
75
- "probability": [0] * len(classes)
76
- })
77
 
78
  def format_confidence(probability: float, precision: int = ModelConfig.PROBABILITY_PRECISION) -> str:
79
- """
80
- Format probability as percentage string.
81
-
82
- Args:
83
- probability: Probability value between 0 and 1
84
- precision: Number of decimal places
85
-
86
- Returns:
87
- Formatted percentage string
88
- """
89
  try:
90
- percentage = probability * 100
91
- return f"{percentage:.{precision}f}%"
92
  except Exception as e:
93
  logger.error(f"Error formatting confidence: {e}")
94
  return "0.0%"
95
-
96
- def validate_image_input(image: Any) -> bool:
97
- """
98
- Validate that image input is not None and has valid structure.
99
-
100
- Args:
101
- image: Image input to validate
102
-
103
- Returns:
104
- True if image is valid, False otherwise
105
- """
106
- if image is None:
107
- logger.warning("Image input is None")
108
- return False
109
-
110
- try:
111
- # Additional validation could be added here
112
- # e.g., check image dimensions, format, etc.
113
- return True
114
- except Exception as e:
115
- logger.error(f"Error validating image input: {e}")
116
- return False
 
1
+ """Utility functions for MelanoScope AI."""
 
 
 
2
  import logging
3
+ from typing import List, Any
4
  import numpy as np
5
  import pandas as pd
 
6
  from ..config.settings import ModelConfig
7
 
 
8
  logger = logging.getLogger(__name__)
9
 
10
+ def probabilities_to_ints(probabilities: np.ndarray, total_sum: int = ModelConfig.PROBABILITY_SUM) -> np.ndarray:
11
+ """Convert probabilities to integers that sum to total_sum."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  try:
13
  probabilities = np.array(probabilities)
 
 
14
  positive_values = np.maximum(probabilities, 0)
15
  total_positive = positive_values.sum()
16
 
17
  if total_positive == 0:
 
18
  return np.zeros_like(probabilities, dtype=int)
19
 
 
20
  scaled = positive_values / total_positive * total_sum
21
  rounded = np.round(scaled).astype(int)
22
 
23
+ # Fix rounding errors
24
  diff = total_sum - rounded.sum()
25
  if diff != 0:
26
  max_idx = int(np.argmax(positive_values))
 
28
  rounded[max_idx] += diff
29
  rounded = rounded.reshape(scaled.shape)
30
 
 
31
  return rounded
32
 
33
  except Exception as e:
34
+ logger.error(f"Error converting probabilities: {e}")
35
  raise ValueError(f"Invalid probability values: {e}")
36
 
37
  def create_empty_dataframe(classes: List[str]) -> pd.DataFrame:
38
+ """Create empty probability dataframe."""
39
+ return pd.DataFrame({"item": classes, "probability": [0] * len(classes)})
 
 
 
 
 
 
 
 
 
 
 
 
40
 
41
  def format_confidence(probability: float, precision: int = ModelConfig.PROBABILITY_PRECISION) -> str:
42
+ """Format probability as percentage string."""
 
 
 
 
 
 
 
 
 
43
  try:
44
+ return f"{probability * 100:.{precision}f}%"
 
45
  except Exception as e:
46
  logger.error(f"Error formatting confidence: {e}")
47
  return "0.0%"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/ui/components.py CHANGED
@@ -1,255 +1,141 @@
1
- """
2
- UI components for MelanoScope AI.
3
- Contains Gradio interface component definitions.
4
- """
5
  import os
6
  import logging
7
- from typing import List, Any, Optional
8
  import gradio as gr
9
-
10
- from ..config.settings import UIConfig, EXAMPLES_DIR
11
  from ..core.utils import create_empty_dataframe
12
  from .styles import get_custom_css, create_theme, get_header_html, get_footer_html, get_model_info_html
13
 
14
  logger = logging.getLogger(__name__)
15
 
16
  class MelanoScopeUI:
17
- """Handles the user interface components and layout."""
18
 
19
  def __init__(self, model_instance, classes: List[str]):
20
- """
21
- Initialize UI components.
22
-
23
- Args:
24
- model_instance: Initialized model instance for predictions
25
- classes: List of class names for empty dataframe
26
- """
27
  self.model = model_instance
28
  self.classes = classes
29
  self.theme = create_theme()
30
  self.css = get_custom_css()
31
-
32
- logger.info("MelanoScopeUI initialized")
33
 
34
  def create_interface(self) -> gr.Blocks:
35
- """
36
- Create the complete Gradio interface.
37
-
38
- Returns:
39
- Configured Gradio Blocks interface
40
- """
41
- try:
42
- with gr.Blocks(theme=self.theme, css=self.css) as interface:
43
- # Header section
44
- self._create_header()
45
-
46
- # Main content area
47
- with gr.Row(equal_height=True):
48
- # Left column: input and controls
49
- self._create_input_column()
50
-
51
- # Right column: results and information
52
- self._create_results_column()
53
-
54
- # Footer
55
- self._create_footer()
56
-
57
- # Set up event handlers
58
- self._setup_event_handlers()
59
 
60
- logger.info("Interface created successfully")
61
- return interface
 
62
 
63
- except Exception as e:
64
- logger.error(f"Failed to create interface: {e}")
65
- raise
 
66
 
67
  def _create_header(self) -> None:
68
- """Create the header section with title and theme toggle."""
69
  with gr.Row():
70
  with gr.Column(scale=6):
71
  gr.Markdown(get_header_html())
72
-
73
  with gr.Column(scale=1, min_width=UIConfig.THEME_TOGGLE_MIN_WIDTH):
74
  try:
75
- self.dark_toggle = gr.ThemeMode(label="Modo", value="system")
76
  except Exception:
77
- gr.Markdown("") # Fallback for older Gradio versions
78
 
79
  def _create_input_column(self) -> None:
80
- """Create the left column with image input and controls."""
81
  with gr.Column(scale=UIConfig.LEFT_COLUMN_SCALE):
82
- # Image input
83
  self.image_input = gr.Image(
84
  type="numpy",
85
- label="Imagen de la lesión",
86
  height=UIConfig.IMAGE_HEIGHT,
87
  sources=["upload", "clipboard"]
88
  )
89
 
90
- # Action buttons
91
  with gr.Row():
92
- self.analyze_btn = gr.Button("Analizar", variant="primary")
93
- self.clear_btn = gr.Button("Limpiar")
94
 
95
- # Examples section
96
  self._create_examples_section()
97
-
98
- # Latency display
99
- self.latency_output = gr.Label(label="Latencia aproximada")
100
 
101
  def _create_examples_section(self) -> None:
102
- """Create the examples section if example files exist."""
103
- try:
104
- example_files = [
105
- "examples/ak.jpg",
106
- "examples/bcc.jpg",
107
- "examples/df.jpg",
108
- "examples/melanoma.jpg",
109
- "examples/nevus.jpg",
110
- ]
111
-
112
- # Filter existing files
113
- existing_examples = [f for f in example_files if os.path.exists(f)]
114
-
115
- if existing_examples:
116
- gr.Examples(
117
- examples=existing_examples,
118
- inputs=self.image_input,
119
- label="Ejemplos rápidos"
120
- )
121
- logger.debug(f"Created examples with {len(existing_examples)} files")
122
- else:
123
- logger.warning("No example files found")
124
-
125
- except Exception as e:
126
- logger.warning(f"Failed to create examples section: {e}")
127
 
128
  def _create_results_column(self) -> None:
129
- """Create the right column with results and information."""
130
  with gr.Column(scale=UIConfig.RIGHT_COLUMN_SCALE):
131
- # Prediction results
132
  self._create_prediction_results()
133
-
134
- # Information tabs
135
  self._create_information_tabs()
136
 
137
  def _create_prediction_results(self) -> None:
138
- """Create the prediction results section."""
139
  with gr.Group():
140
- # Main prediction and confidence
141
  with gr.Row():
142
- self.prediction_output = gr.Label(
143
- label="Predicción principal",
144
- elem_classes=["pred-card"]
145
- )
146
- self.confidence_output = gr.Label(label="Confianza")
147
 
148
- # Probability distribution chart
149
  self.probability_plot = gr.BarPlot(
150
  value=create_empty_dataframe(self.classes),
151
- x="item",
152
- y="probability",
153
- title="Distribución de probabilidad (Top‑k)",
154
- x_title="Clase",
155
- y_title="Probabilidad",
156
- vertical=False,
157
- tooltip=["item", "probability"],
158
- width=UIConfig.PLOT_WIDTH,
159
- height=UIConfig.PLOT_HEIGHT,
160
  )
161
 
162
  def _create_information_tabs(self) -> None:
163
- """Create the tabbed information section."""
164
  with gr.Tabs():
165
- # Medical details tab
166
- with gr.TabItem("Detalles"):
167
  self._create_medical_details()
168
-
169
- # Model information tab
170
- with gr.TabItem("Acerca del modelo"):
171
  gr.Markdown(get_model_info_html())
172
 
173
  def _create_medical_details(self) -> None:
174
- """Create the medical details accordions."""
175
- with gr.Accordion("Descripción", open=True):
176
- self.description_output = gr.Textbox(
177
- lines=UIConfig.TEXTBOX_LINES,
178
- interactive=False
179
- )
180
-
181
- with gr.Accordion("Síntomas", open=False):
182
- self.symptoms_output = gr.Textbox(
183
- lines=UIConfig.TEXTBOX_LINES,
184
- interactive=False
185
- )
186
-
187
- with gr.Accordion("Causas", open=False):
188
- self.causes_output = gr.Textbox(
189
- lines=UIConfig.TEXTBOX_LINES,
190
- interactive=False
191
- )
192
-
193
- with gr.Accordion("Tratamiento", open=False):
194
- self.treatment_output = gr.Textbox(
195
- lines=UIConfig.TEXTBOX_LINES,
196
- interactive=False
197
- )
198
 
199
  def _create_footer(self) -> None:
200
- """Create the footer section."""
201
  gr.Markdown(get_footer_html())
202
 
203
  def _setup_event_handlers(self) -> None:
204
- """Set up event handlers for interactive components."""
205
- try:
206
- # Collect all output components
207
- outputs = [
208
- self.prediction_output,
209
- self.confidence_output,
210
- self.description_output,
211
- self.symptoms_output,
212
- self.causes_output,
213
- self.treatment_output,
214
- self.probability_plot,
215
- self.latency_output
216
- ]
217
-
218
- # Analyze button click
219
- self.analyze_btn.click(
220
- fn=self.model.predict,
221
- inputs=[self.image_input],
222
- outputs=outputs,
223
- show_progress="full"
224
- )
225
-
226
- # Clear button click
227
- self.clear_btn.click(
228
- fn=self._clear_all,
229
- inputs=[],
230
- outputs=[self.image_input] + outputs
231
- )
232
-
233
- logger.debug("Event handlers set up successfully")
234
-
235
- except Exception as e:
236
- logger.error(f"Failed to set up event handlers: {e}")
237
- raise
238
 
239
  def _clear_all(self) -> tuple:
240
- """
241
- Clear all inputs and outputs.
242
-
243
- Returns:
244
- Tuple of cleared values for all components
245
- """
246
- try:
247
- empty_df = create_empty_dataframe(self.classes)
248
-
249
- # Return cleared values for: image, prediction, confidence, description,
250
- # symptoms, causes, treatment, probability_plot, latency
251
- return (None, "", "", "", "", "", "", empty_df, "")
252
-
253
- except Exception as e:
254
- logger.error(f"Error clearing interface: {e}")
255
- return (None, "", "", "", "", "", "", None, "")
 
1
+ """UI components for MelanoScope AI."""
 
 
 
2
  import os
3
  import logging
4
+ from typing import List
5
  import gradio as gr
6
+ from ..config.settings import UIConfig
 
7
  from ..core.utils import create_empty_dataframe
8
  from .styles import get_custom_css, create_theme, get_header_html, get_footer_html, get_model_info_html
9
 
10
  logger = logging.getLogger(__name__)
11
 
12
  class MelanoScopeUI:
13
+ """Handles UI components and layout."""
14
 
15
  def __init__(self, model_instance, classes: List[str]):
 
 
 
 
 
 
 
16
  self.model = model_instance
17
  self.classes = classes
18
  self.theme = create_theme()
19
  self.css = get_custom_css()
20
+ logger.info("UI initialized")
 
21
 
22
  def create_interface(self) -> gr.Blocks:
23
+ """Create complete Gradio interface."""
24
+ with gr.Blocks(theme=self.theme, css=self.css) as interface:
25
+ self._create_header()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
 
27
+ with gr.Row(equal_height=True):
28
+ self._create_input_column()
29
+ self._create_results_column()
30
 
31
+ self._create_footer()
32
+ self._setup_event_handlers()
33
+
34
+ return interface
35
 
36
  def _create_header(self) -> None:
37
+ """Create header section."""
38
  with gr.Row():
39
  with gr.Column(scale=6):
40
  gr.Markdown(get_header_html())
 
41
  with gr.Column(scale=1, min_width=UIConfig.THEME_TOGGLE_MIN_WIDTH):
42
  try:
43
+ self.dark_toggle = gr.ThemeMode(label="Theme", value="system")
44
  except Exception:
45
+ gr.Markdown("")
46
 
47
  def _create_input_column(self) -> None:
48
+ """Create input column with image upload and controls."""
49
  with gr.Column(scale=UIConfig.LEFT_COLUMN_SCALE):
 
50
  self.image_input = gr.Image(
51
  type="numpy",
52
+ label="Lesion Image",
53
  height=UIConfig.IMAGE_HEIGHT,
54
  sources=["upload", "clipboard"]
55
  )
56
 
 
57
  with gr.Row():
58
+ self.analyze_btn = gr.Button("Analyze", variant="primary")
59
+ self.clear_btn = gr.Button("Clear")
60
 
 
61
  self._create_examples_section()
62
+ self.latency_output = gr.Label(label="Inference Time")
 
 
63
 
64
  def _create_examples_section(self) -> None:
65
+ """Create examples section if files exist."""
66
+ example_files = [
67
+ "examples/ak.jpg", "examples/bcc.jpg", "examples/df.jpg",
68
+ "examples/melanoma.jpg", "examples/nevus.jpg"
69
+ ]
70
+
71
+ existing_examples = [f for f in example_files if os.path.exists(f)]
72
+ if existing_examples:
73
+ gr.Examples(examples=existing_examples, inputs=self.image_input, label="Quick Examples")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
 
75
  def _create_results_column(self) -> None:
76
+ """Create results column with predictions and info."""
77
  with gr.Column(scale=UIConfig.RIGHT_COLUMN_SCALE):
 
78
  self._create_prediction_results()
 
 
79
  self._create_information_tabs()
80
 
81
  def _create_prediction_results(self) -> None:
82
+ """Create prediction results section."""
83
  with gr.Group():
 
84
  with gr.Row():
85
+ self.prediction_output = gr.Label(label="Primary Prediction", elem_classes=["pred-card"])
86
+ self.confidence_output = gr.Label(label="Confidence")
 
 
 
87
 
 
88
  self.probability_plot = gr.BarPlot(
89
  value=create_empty_dataframe(self.classes),
90
+ x="item", y="probability",
91
+ title="Probability Distribution (Top-k)",
92
+ x_title="Class", y_title="Probability",
93
+ vertical=False, tooltip=["item", "probability"],
94
+ width=UIConfig.PLOT_WIDTH, height=UIConfig.PLOT_HEIGHT,
 
 
 
 
95
  )
96
 
97
  def _create_information_tabs(self) -> None:
98
+ """Create information tabs."""
99
  with gr.Tabs():
100
+ with gr.TabItem("Medical Details"):
 
101
  self._create_medical_details()
102
+ with gr.TabItem("About Model"):
 
 
103
  gr.Markdown(get_model_info_html())
104
 
105
  def _create_medical_details(self) -> None:
106
+ """Create medical details accordions."""
107
+ with gr.Accordion("Description", open=True):
108
+ self.description_output = gr.Textbox(lines=UIConfig.TEXTBOX_LINES, interactive=False)
109
+ with gr.Accordion("Symptoms", open=False):
110
+ self.symptoms_output = gr.Textbox(lines=UIConfig.TEXTBOX_LINES, interactive=False)
111
+ with gr.Accordion("Causes", open=False):
112
+ self.causes_output = gr.Textbox(lines=UIConfig.TEXTBOX_LINES, interactive=False)
113
+ with gr.Accordion("Treatment", open=False):
114
+ self.treatment_output = gr.Textbox(lines=UIConfig.TEXTBOX_LINES, interactive=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
 
116
  def _create_footer(self) -> None:
117
+ """Create footer section."""
118
  gr.Markdown(get_footer_html())
119
 
120
  def _setup_event_handlers(self) -> None:
121
+ """Set up event handlers."""
122
+ outputs = [
123
+ self.prediction_output, self.confidence_output, self.description_output,
124
+ self.symptoms_output, self.causes_output, self.treatment_output,
125
+ self.probability_plot, self.latency_output
126
+ ]
127
+
128
+ self.analyze_btn.click(
129
+ fn=self.model.predict, inputs=[self.image_input],
130
+ outputs=outputs, show_progress="full"
131
+ )
132
+
133
+ self.clear_btn.click(
134
+ fn=self._clear_all, inputs=[],
135
+ outputs=[self.image_input] + outputs
136
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
 
138
  def _clear_all(self) -> tuple:
139
+ """Clear all inputs and outputs."""
140
+ empty_df = create_empty_dataframe(self.classes)
141
+ return (None, "", "", "", "", "", "", empty_df, "")
 
 
 
 
 
 
 
 
 
 
 
 
 
src/ui/styles.py CHANGED
@@ -1,133 +1,69 @@
1
- """
2
- UI styling and theming for MelanoScope AI.
3
- Contains CSS styles and theme configurations.
4
- """
5
- from typing import Optional
6
  import logging
7
-
8
  from ..config.settings import UIConfig
9
 
10
  logger = logging.getLogger(__name__)
11
 
12
  def get_custom_css() -> str:
13
- """
14
- Get custom CSS styles for the application.
15
-
16
- Returns:
17
- CSS string for styling the interface
18
- """
19
  return """
20
- .header {
21
- display: flex;
22
- align-items: center;
23
- gap: 12px;
24
- }
25
- .badge {
26
- font-size: 12px;
27
- padding: 4px 8px;
28
- border-radius: 12px;
29
- background: #f1f5f9;
30
- color: #334155;
31
- }
32
- .pred-card {
33
- font-size: 18px;
34
- }
35
- .footer {
36
- font-size: 12px;
37
- color: #64748b;
38
- text-align: center;
39
- padding: 12px 0;
40
- }
41
  button, .gradio-container .gr-box, .gradio-container .gr-panel {
42
  border-radius: 10px !important;
43
  }
44
- /* Uniform bar color in Vega-Lite charts */
45
  .vega-embed .mark-rect, .vega-embed .mark-bar, .vega-embed .role-mark rect {
46
  fill: #ef4444 !important;
47
  }
48
- /* Improve spacing and readability */
49
- .gradio-container {
50
- font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
51
- }
52
- .gr-button {
53
- transition: all 0.2s ease;
54
- }
55
- .gr-button:hover {
56
- transform: translateY(-1px);
57
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
58
- }
59
  """
60
 
61
  def create_theme():
62
- """
63
- Create and return the application theme.
64
-
65
- Returns:
66
- Gradio theme object or None if creation fails
67
- """
68
  try:
69
  import gradio as gr
70
-
71
- theme = gr.themes.Soft(
72
  primary_hue=UIConfig.THEME_PRIMARY_HUE,
73
  secondary_hue=UIConfig.THEME_SECONDARY_HUE
74
  )
75
-
76
- logger.debug("Theme created successfully")
77
- return theme
78
-
79
  except Exception as e:
80
- logger.warning(f"Failed to create theme, using default: {e}")
81
  return None
82
 
83
  def get_header_html() -> str:
84
- """
85
- Get HTML for the application header.
86
-
87
- Returns:
88
- HTML string for the header section
89
- """
90
  from ..config.settings import AppConfig
91
-
92
  return f"""
93
  <div class="header">
94
  <h1 style="margin:0;">{AppConfig.TITLE}</h1>
95
  <span class="badge">{AppConfig.DISCLAIMER}</span>
96
  </div>
97
  <p style="margin-top:6px;">
98
- Sube una imagen dermatoscópica para ver la clase predicha,
99
- la confianza y la distribución de probabilidades.
100
  </p>
101
  """
102
 
103
  def get_footer_html() -> str:
104
- """
105
- Get HTML for the application footer.
106
-
107
- Returns:
108
- HTML string for the footer section
109
- """
110
  from ..config.settings import AppConfig
111
-
112
  return (
113
  f"<div class='footer'>"
114
- f"Versión del modelo: {AppConfig.VERSION} • "
115
- f"Última actualización: {AppConfig.LAST_UPDATE} • "
116
  f"{AppConfig.INSTITUTION}"
117
  f"</div>"
118
  )
119
 
120
  def get_model_info_html() -> str:
121
- """
122
- Get HTML for the model information tab.
123
-
124
- Returns:
125
- HTML string describing the model
126
- """
127
  from ..config.settings import AppConfig
128
-
129
  return (
130
- "- Arquitectura: CNN exportado a ONNX.<br>"
131
- "- Entrenamiento: dataset dermatoscópico (ver documentación).<br>"
132
- f"- Nota: {AppConfig.MEDICAL_DISCLAIMER}"
133
  )
 
1
+ """UI styling and theming for MelanoScope AI."""
 
 
 
 
2
  import logging
 
3
  from ..config.settings import UIConfig
4
 
5
  logger = logging.getLogger(__name__)
6
 
7
  def get_custom_css() -> str:
8
+ """Get custom CSS styles."""
 
 
 
 
 
9
  return """
10
+ .header { display: flex; align-items: center; gap: 12px; }
11
+ .badge { font-size: 12px; padding: 4px 8px; border-radius: 12px;
12
+ background: #f1f5f9; color: #334155; }
13
+ .pred-card { font-size: 18px; }
14
+ .footer { font-size: 12px; color: #64748b; text-align: center; padding: 12px 0; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  button, .gradio-container .gr-box, .gradio-container .gr-panel {
16
  border-radius: 10px !important;
17
  }
 
18
  .vega-embed .mark-rect, .vega-embed .mark-bar, .vega-embed .role-mark rect {
19
  fill: #ef4444 !important;
20
  }
21
+ .gradio-container { font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; }
22
+ .gr-button { transition: all 0.2s ease; }
23
+ .gr-button:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); }
 
 
 
 
 
 
 
 
24
  """
25
 
26
  def create_theme():
27
+ """Create application theme."""
 
 
 
 
 
28
  try:
29
  import gradio as gr
30
+ return gr.themes.Soft(
 
31
  primary_hue=UIConfig.THEME_PRIMARY_HUE,
32
  secondary_hue=UIConfig.THEME_SECONDARY_HUE
33
  )
 
 
 
 
34
  except Exception as e:
35
+ logger.warning(f"Theme creation failed, using default: {e}")
36
  return None
37
 
38
  def get_header_html() -> str:
39
+ """Get HTML for application header."""
 
 
 
 
 
40
  from ..config.settings import AppConfig
 
41
  return f"""
42
  <div class="header">
43
  <h1 style="margin:0;">{AppConfig.TITLE}</h1>
44
  <span class="badge">{AppConfig.DISCLAIMER}</span>
45
  </div>
46
  <p style="margin-top:6px;">
47
+ Upload a dermatoscopic image to see the predicted class, confidence, and probability distribution.
 
48
  </p>
49
  """
50
 
51
  def get_footer_html() -> str:
52
+ """Get HTML for application footer."""
 
 
 
 
 
53
  from ..config.settings import AppConfig
 
54
  return (
55
  f"<div class='footer'>"
56
+ f"Model version: {AppConfig.VERSION} • "
57
+ f"Last updated: {AppConfig.LAST_UPDATE} • "
58
  f"{AppConfig.INSTITUTION}"
59
  f"</div>"
60
  )
61
 
62
  def get_model_info_html() -> str:
63
+ """Get HTML for model information."""
 
 
 
 
 
64
  from ..config.settings import AppConfig
 
65
  return (
66
+ "- Architecture: CNN exported to ONNX format<br>"
67
+ "- Training: Dermatoscopic dataset (see documentation)<br>"
68
+ f"- Note: {AppConfig.MEDICAL_DISCLAIMER}"
69
  )