serene-abyss commited on
Commit
49c9c33
·
verified ·
1 Parent(s): ba774eb

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +31 -46
app.py CHANGED
@@ -2,10 +2,10 @@ import torch
2
  import torch.nn.functional as F
3
  from transformers import AutoModelForImageClassification, pipeline
4
  from torchvision import transforms
5
- from PIL import Image
6
- import numpy as np
7
  from fastapi import FastAPI, File, UploadFile
8
  from fastapi.responses import HTMLResponse
 
9
  import io
10
  import gc
11
  import librosa
@@ -13,7 +13,7 @@ import soundfile as sf
13
  from datetime import datetime
14
 
15
  # ==========================================
16
- # 1. CONFIGURATION & GUARDRAIL RULES
17
  # ==========================================
18
  MODELS = {
19
  "lungs": {
@@ -21,14 +21,14 @@ MODELS = {
21
  "id": "nickmuchi/vit-finetuned-chest-xray-pneumonia",
22
  "desc": "Chest X-Ray Analysis",
23
  "safe": ["NORMAL", "normal", "No Pneumonia"],
24
- # Rule: Must be Grayscale (Low Saturation)
25
  "rules": {"max_sat": 30, "reject_msg": "Invalid: Too colorful. Please upload a B&W X-Ray."}
26
  },
27
  "cough": {
28
  "type": "audio",
29
  "id": "MIT/ast-finetuned-audioset-10-10-0.4593",
30
  "desc": "Respiratory Audio Analysis",
31
- "target_labels": ["Cough", "Throat clearing", "Respiratory sounds", "Wheeze"],
 
32
  "rules": {"min_duration": 0.5, "reject_msg": "Invalid: Audio too short or silent."}
33
  },
34
  "fracture": {
@@ -36,7 +36,6 @@ MODELS = {
36
  "id": "dima806/bone_fracture_detection",
37
  "desc": "Bone Trauma X-Ray",
38
  "safe": ["normal", "healed"],
39
- # Rule: Must be Grayscale
40
  "rules": {"max_sat": 30, "reject_msg": "Invalid: Too colorful. Please upload a B&W X-Ray."}
41
  },
42
  "brain": {
@@ -44,7 +43,6 @@ MODELS = {
44
  "id": "Hemgg/brain-tumor-classification",
45
  "desc": "Brain MRI Scan Analysis",
46
  "safe": ["no_tumor"],
47
- # Rule: Must be Grayscale
48
  "rules": {"max_sat": 30, "reject_msg": "Invalid: Too colorful. Please upload a B&W MRI Scan."}
49
  },
50
  "eye": {
@@ -52,29 +50,19 @@ MODELS = {
52
  "id": "AventIQ-AI/resnet18-cataract-detection-system",
53
  "desc": "Ophthalmology Scan",
54
  "safe": ["Normal", "normal", "healthy"],
55
- # Rule: Must be Color AND have White Sclera
56
- "rules": {
57
- "min_sat": 20,
58
- "min_white": 0.05, # Eyes have >5% white pixels
59
- "reject_msg": "Invalid: No eye detected (Missing white sclera)."
60
- }
61
  },
62
  "skin": {
63
  "type": "image",
64
  "id": "Anwarkh1/Skin_Cancer-Image_Classification",
65
  "desc": "Dermatology Lesion Scan",
66
  "safe": ["Benign", "benign", "nv", "bkl"],
67
- # Rule: Must be Color BUT NOT have huge white patches (like an eye)
68
- "rules": {
69
- "min_sat": 20,
70
- "max_white": 0.15, # Skin shouldn't have >15% pure white pixels
71
- "reject_msg": "Invalid: Image looks like an Eye or Document (Too much white area)."
72
- }
73
  }
74
  }
75
 
76
  # ==========================================
77
- # 2. MEDICAL ENGINE (With Forensic Logic)
78
  # ==========================================
79
  class MedicalEngine:
80
  def __init__(self):
@@ -88,38 +76,24 @@ class MedicalEngine:
88
  ])
89
 
90
  def validate_image(self, image, task):
91
- """
92
- Forensic Guardrails using NumPy
93
- """
94
  rules = MODELS[task].get("rules", {})
95
-
96
- # Convert to HSV (Hue, Saturation, Value)
97
  img_hsv = image.convert('HSV')
98
  img_np = np.array(img_hsv)
99
 
100
- # Calculate Averages
101
  s_channel = img_np[:, :, 1]
102
  v_channel = img_np[:, :, 2]
103
  avg_sat = np.mean(s_channel)
104
 
105
- # Calculate White Ratio (Low Saturation + High Brightness)
106
- # Thresholds: Saturation < 40/255 AND Value > 180/255
107
  white_pixels = np.logical_and(s_channel < 40, v_channel > 180)
108
  white_ratio = np.sum(white_pixels) / white_pixels.size
109
 
110
- print(f"🔍 Analysis [{task}]: Sat={int(avg_sat)}, WhiteRatio={white_ratio:.3f}")
111
-
112
- # 1. Saturation Check (Color vs Gray)
113
- if "max_sat" in rules and avg_sat > rules["max_sat"]:
114
- return False, rules["reject_msg"]
115
- if "min_sat" in rules and avg_sat < rules["min_sat"]:
116
- return False, "Invalid: Image is Black & White. Color photo required."
117
-
118
- # 2. White Ratio Check (Eye vs Skin)
119
- if "min_white" in rules and white_ratio < rules["min_white"]:
120
- return False, rules["reject_msg"]
121
- if "max_white" in rules and white_ratio > rules["max_white"]:
122
- return False, rules["reject_msg"]
123
 
124
  return True, ""
125
 
@@ -135,20 +109,30 @@ class MedicalEngine:
135
  # === AUDIO PIPELINE ===
136
  if model_cfg["type"] == "audio":
137
  try:
 
138
  audio, sr = librosa.load(io.BytesIO(file_bytes), sr=16000)
139
  is_valid, msg = self.validate_audio(audio, sr)
140
  if not is_valid: return {"error": msg, "risk": "INVALID"}
141
 
 
142
  classifier = pipeline("audio-classification", model=model_cfg["id"])
 
143
  sf.write("temp.wav", audio, sr)
144
  outputs = classifier("temp.wav")
145
 
 
146
  top = outputs[0]
147
  target_labels = model_cfg["target_labels"]
148
- is_cough = any(target in res['label'] for res in outputs[:3] for target in target_labels)
149
 
150
- risk = "HIGH" if is_cough and top['score'] > 0.4 else "LOW"
151
- label = f"Detected: {top['label']}" if is_cough else "Normal / Background Noise"
 
 
 
 
 
 
 
152
 
153
  return {"task": task, "desc": model_cfg["desc"], "prediction": {"label": label, "score": top['score']}, "risk": risk}
154
  except Exception as e:
@@ -436,13 +420,14 @@ def home():
436
  let txt = document.getElementById('upload-text');
437
 
438
  if (type === 'audio') {
439
- input.accept = "audio/*";
440
  icon.className = "fas fa-microphone-alt text-4xl text-teal-500 mb-2";
 
441
  } else {
442
  input.accept = "image/*";
443
  icon.className = "fas fa-cloud-upload-alt text-4xl text-gray-400 mb-2";
 
444
  }
445
- txt.innerText = t['txt_upload'];
446
 
447
  document.getElementById('inputs').classList.remove('opacity-50', 'pointer-events-none');
448
  document.getElementById('result-box').classList.add('hidden');
 
2
  import torch.nn.functional as F
3
  from transformers import AutoModelForImageClassification, pipeline
4
  from torchvision import transforms
5
+ from PIL import Image, ImageStat
 
6
  from fastapi import FastAPI, File, UploadFile
7
  from fastapi.responses import HTMLResponse
8
+ import numpy as np
9
  import io
10
  import gc
11
  import librosa
 
13
  from datetime import datetime
14
 
15
  # ==========================================
16
+ # 1. CONFIGURATION
17
  # ==========================================
18
  MODELS = {
19
  "lungs": {
 
21
  "id": "nickmuchi/vit-finetuned-chest-xray-pneumonia",
22
  "desc": "Chest X-Ray Analysis",
23
  "safe": ["NORMAL", "normal", "No Pneumonia"],
 
24
  "rules": {"max_sat": 30, "reject_msg": "Invalid: Too colorful. Please upload a B&W X-Ray."}
25
  },
26
  "cough": {
27
  "type": "audio",
28
  "id": "MIT/ast-finetuned-audioset-10-10-0.4593",
29
  "desc": "Respiratory Audio Analysis",
30
+ # The AI looks for these specific sound tags
31
+ "target_labels": ["Cough", "Throat clearing", "Respiratory sounds", "Wheeze", "Gasping"],
32
  "rules": {"min_duration": 0.5, "reject_msg": "Invalid: Audio too short or silent."}
33
  },
34
  "fracture": {
 
36
  "id": "dima806/bone_fracture_detection",
37
  "desc": "Bone Trauma X-Ray",
38
  "safe": ["normal", "healed"],
 
39
  "rules": {"max_sat": 30, "reject_msg": "Invalid: Too colorful. Please upload a B&W X-Ray."}
40
  },
41
  "brain": {
 
43
  "id": "Hemgg/brain-tumor-classification",
44
  "desc": "Brain MRI Scan Analysis",
45
  "safe": ["no_tumor"],
 
46
  "rules": {"max_sat": 30, "reject_msg": "Invalid: Too colorful. Please upload a B&W MRI Scan."}
47
  },
48
  "eye": {
 
50
  "id": "AventIQ-AI/resnet18-cataract-detection-system",
51
  "desc": "Ophthalmology Scan",
52
  "safe": ["Normal", "normal", "healthy"],
53
+ "rules": {"min_sat": 20, "min_white": 0.05, "reject_msg": "Invalid: No eye detected (Missing white sclera)."}
 
 
 
 
 
54
  },
55
  "skin": {
56
  "type": "image",
57
  "id": "Anwarkh1/Skin_Cancer-Image_Classification",
58
  "desc": "Dermatology Lesion Scan",
59
  "safe": ["Benign", "benign", "nv", "bkl"],
60
+ "rules": {"min_sat": 20, "max_white": 0.15, "reject_msg": "Invalid: Image looks like an Eye or Document (Too much white area)."}
 
 
 
 
 
61
  }
62
  }
63
 
64
  # ==========================================
65
+ # 2. MEDICAL ENGINE
66
  # ==========================================
67
  class MedicalEngine:
68
  def __init__(self):
 
76
  ])
77
 
78
  def validate_image(self, image, task):
 
 
 
79
  rules = MODELS[task].get("rules", {})
 
 
80
  img_hsv = image.convert('HSV')
81
  img_np = np.array(img_hsv)
82
 
83
+ # Calculate Stats
84
  s_channel = img_np[:, :, 1]
85
  v_channel = img_np[:, :, 2]
86
  avg_sat = np.mean(s_channel)
87
 
88
+ # White Pixel Ratio
 
89
  white_pixels = np.logical_and(s_channel < 40, v_channel > 180)
90
  white_ratio = np.sum(white_pixels) / white_pixels.size
91
 
92
+ # Guardrails
93
+ if "max_sat" in rules and avg_sat > rules["max_sat"]: return False, rules["reject_msg"]
94
+ if "min_sat" in rules and avg_sat < rules["min_sat"]: return False, "Invalid: Image is Black & White. Color photo required."
95
+ if "min_white" in rules and white_ratio < rules["min_white"]: return False, rules["reject_msg"]
96
+ if "max_white" in rules and white_ratio > rules["max_white"]: return False, rules["reject_msg"]
 
 
 
 
 
 
 
 
97
 
98
  return True, ""
99
 
 
109
  # === AUDIO PIPELINE ===
110
  if model_cfg["type"] == "audio":
111
  try:
112
+ # Load Audio (Librosa handles mp3/wav if ffmpeg is installed)
113
  audio, sr = librosa.load(io.BytesIO(file_bytes), sr=16000)
114
  is_valid, msg = self.validate_audio(audio, sr)
115
  if not is_valid: return {"error": msg, "risk": "INVALID"}
116
 
117
+ # Run AST Model
118
  classifier = pipeline("audio-classification", model=model_cfg["id"])
119
+ # Pipeline requires file path, so we save temp
120
  sf.write("temp.wav", audio, sr)
121
  outputs = classifier("temp.wav")
122
 
123
+ # Logic: Is "Cough" or "Wheeze" in the top predictions?
124
  top = outputs[0]
125
  target_labels = model_cfg["target_labels"]
 
126
 
127
+ # Check top 3 predictions for any respiratory issue
128
+ is_respiratory = any(t in res['label'] for res in outputs[:3] for t in target_labels)
129
+
130
+ if is_respiratory:
131
+ risk = "HIGH"
132
+ label = f"Detected: {top['label']}"
133
+ else:
134
+ risk = "LOW"
135
+ label = "Normal / Background Noise"
136
 
137
  return {"task": task, "desc": model_cfg["desc"], "prediction": {"label": label, "score": top['score']}, "risk": risk}
138
  except Exception as e:
 
420
  let txt = document.getElementById('upload-text');
421
 
422
  if (type === 'audio') {
423
+ input.accept = ".wav, .mp3, audio/*";
424
  icon.className = "fas fa-microphone-alt text-4xl text-teal-500 mb-2";
425
+ txt.innerHTML = "Tap to upload Audio<br><span class='text-xs'>(.wav, .mp3)</span>";
426
  } else {
427
  input.accept = "image/*";
428
  icon.className = "fas fa-cloud-upload-alt text-4xl text-gray-400 mb-2";
429
+ txt.innerText = t['txt_upload'];
430
  }
 
431
 
432
  document.getElementById('inputs').classList.remove('opacity-50', 'pointer-events-none');
433
  document.getElementById('result-box').classList.add('hidden');