Commit ·
7d05ca5
1
Parent(s): 7e53d50
النسخة المستقرة النهائية
Browse files- Dockerfile +2 -25
- app.py +116 -547
- requirements.txt +2 -5
Dockerfile
CHANGED
|
@@ -1,31 +1,8 @@
|
|
| 1 |
FROM python:3.10-slim
|
| 2 |
-
|
| 3 |
WORKDIR /app
|
| 4 |
-
|
| 5 |
-
# Install system dependencies
|
| 6 |
-
RUN apt-get update && apt-get install -y \
|
| 7 |
-
libgl1 \
|
| 8 |
-
libglib2.0-0 \
|
| 9 |
-
libsm6 \
|
| 10 |
-
libxext6 \
|
| 11 |
-
libxrender1 \
|
| 12 |
-
libgomp1 \
|
| 13 |
-
libheif-dev \
|
| 14 |
-
ffmpeg \
|
| 15 |
-
&& rm -rf /var/lib/apt/lists/*
|
| 16 |
-
|
| 17 |
-
# Copy requirements first for better caching
|
| 18 |
COPY requirements.txt .
|
| 19 |
-
|
| 20 |
-
# Upgrade pip and install dependencies
|
| 21 |
-
RUN pip install --no-cache-dir --upgrade pip setuptools wheel && \
|
| 22 |
-
pip install --no-cache-dir -r requirements.txt
|
| 23 |
-
|
| 24 |
-
# Copy the rest of the application
|
| 25 |
COPY . .
|
| 26 |
-
|
| 27 |
-
# Expose port
|
| 28 |
EXPOSE 7860
|
| 29 |
-
|
| 30 |
-
# Run the application
|
| 31 |
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
|
|
|
|
| 1 |
FROM python:3.10-slim
|
|
|
|
| 2 |
WORKDIR /app
|
| 3 |
+
RUN apt-get update && apt-get install -y libgl1 libglib2.0-0 libsm6 libxext6 libxrender1 libgomp1 libheif-dev ffmpeg && rm -rf /var/lib/apt/lists/*
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
COPY requirements.txt .
|
| 5 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
COPY . .
|
|
|
|
|
|
|
| 7 |
EXPOSE 7860
|
|
|
|
|
|
|
| 8 |
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
|
app.py
CHANGED
|
@@ -13,13 +13,12 @@ import pyheif
|
|
| 13 |
import gdown
|
| 14 |
import requests
|
| 15 |
import time
|
| 16 |
-
import
|
| 17 |
|
| 18 |
# HuggingFace
|
| 19 |
from transformers import pipeline
|
| 20 |
import torch
|
| 21 |
import json
|
| 22 |
-
import os
|
| 23 |
from pathlib import Path
|
| 24 |
from datetime import datetime
|
| 25 |
|
|
@@ -27,27 +26,17 @@ from datetime import datetime
|
|
| 27 |
# CONFIGURATION
|
| 28 |
# ============================================================
|
| 29 |
IMAGE_SIZE = 224
|
| 30 |
-
|
| 31 |
-
# Define model paths
|
| 32 |
BINARY_MODEL_PATH = "./model_2.keras"
|
| 33 |
DISEASE_MODEL_PATH = "./LAST_model_efficent.h5"
|
| 34 |
-
|
| 35 |
BASE_DIR = Path(__file__).parent
|
| 36 |
KNOWLEDGE_BASE_PATH = BASE_DIR / "knowledge_base" / "clinical_rules.json"
|
| 37 |
-
|
| 38 |
-
# HuggingFace Teeth Health Model
|
| 39 |
HF_TEETH_HEALTH_MODEL = "steven123/Check_GoodBad_Teeth"
|
| 40 |
DEVICE = 0 if torch.cuda.is_available() else -1
|
| 41 |
|
| 42 |
BINARY_CLASSES = ["not_teath", "teath"]
|
| 43 |
-
TEETH_HEALTH_CLASSES = ["Good Teeth", "Bad Teeth"]
|
| 44 |
DISEASE_CLASSES = [
|
| 45 |
-
"Calculus",
|
| 46 |
-
"
|
| 47 |
-
"Gingivitis",
|
| 48 |
-
"Mouth Ulcer",
|
| 49 |
-
"Tooth Discoloration",
|
| 50 |
-
"Hypodontia"
|
| 51 |
]
|
| 52 |
|
| 53 |
# ============================================================
|
|
@@ -57,15 +46,11 @@ print("=" * 70)
|
|
| 57 |
print("CHECKING AND DOWNLOADING MODELS IF NEEDED")
|
| 58 |
print("=" * 70)
|
| 59 |
|
| 60 |
-
# Google Drive file IDs
|
| 61 |
BINARY_FILE_ID = "1--o19x7wPyCu5rQBfxIhH3gYUJwwQNXA"
|
| 62 |
DISEASE_FILE_ID = "1JAcY_T_x16OHNUj-XKSjUMEUuepB1IFY"
|
| 63 |
|
| 64 |
-
def
|
| 65 |
-
"
|
| 66 |
-
print(f"[INFO] Downloading {destination} from Google Drive...")
|
| 67 |
-
|
| 68 |
-
# Method 1: Using gdown
|
| 69 |
try:
|
| 70 |
url = f"https://drive.google.com/uc?id={file_id}"
|
| 71 |
gdown.download(url, destination, quiet=False)
|
|
@@ -73,57 +58,20 @@ def download_file_from_google_drive(file_id, destination):
|
|
| 73 |
print(f"[SUCCESS] Downloaded {destination}")
|
| 74 |
return True
|
| 75 |
except Exception as e:
|
| 76 |
-
print(f"[WARNING]
|
| 77 |
-
|
| 78 |
-
# Method 2: Direct requests with confirmation
|
| 79 |
-
try:
|
| 80 |
-
session = requests.Session()
|
| 81 |
-
URL = "https://docs.google.com/uc?export=download"
|
| 82 |
-
response = session.get(URL, params={'id': file_id}, stream=True)
|
| 83 |
-
|
| 84 |
-
def get_confirm_token(response):
|
| 85 |
-
for key, value in response.cookies.items():
|
| 86 |
-
if key.startswith('download_warning'):
|
| 87 |
-
return value
|
| 88 |
-
return None
|
| 89 |
-
|
| 90 |
-
token = get_confirm_token(response)
|
| 91 |
-
if token:
|
| 92 |
-
params = {'id': file_id, 'confirm': token}
|
| 93 |
-
response = session.get(URL, params=params, stream=True)
|
| 94 |
-
|
| 95 |
-
with open(destination, "wb") as f:
|
| 96 |
-
for chunk in response.iter_content(32768):
|
| 97 |
-
if chunk:
|
| 98 |
-
f.write(chunk)
|
| 99 |
-
|
| 100 |
-
if os.path.exists(destination) and os.path.getsize(destination) > 0:
|
| 101 |
-
print(f"[SUCCESS] Downloaded {destination} using requests")
|
| 102 |
-
return True
|
| 103 |
-
except Exception as e:
|
| 104 |
-
print(f"[WARNING] Requests download failed: {e}")
|
| 105 |
-
|
| 106 |
return False
|
| 107 |
|
| 108 |
-
#
|
| 109 |
-
print(f"\n[INFO] Checking binary model at {BINARY_MODEL_PATH}")
|
| 110 |
if not os.path.exists(BINARY_MODEL_PATH):
|
| 111 |
-
|
| 112 |
-
success = download_file_from_google_drive(BINARY_FILE_ID, BINARY_MODEL_PATH)
|
| 113 |
-
if not success:
|
| 114 |
-
raise Exception("Failed to download binary model")
|
| 115 |
else:
|
| 116 |
-
print(f"[INFO] Binary model exists
|
| 117 |
|
| 118 |
-
#
|
| 119 |
-
print(f"\n[INFO] Checking disease model at {DISEASE_MODEL_PATH}")
|
| 120 |
if not os.path.exists(DISEASE_MODEL_PATH):
|
| 121 |
-
|
| 122 |
-
success = download_file_from_google_drive(DISEASE_FILE_ID, DISEASE_MODEL_PATH)
|
| 123 |
-
if not success:
|
| 124 |
-
raise Exception("Failed to download disease model")
|
| 125 |
else:
|
| 126 |
-
print(f"[INFO] Disease model exists
|
| 127 |
|
| 128 |
print("=" * 70)
|
| 129 |
|
|
@@ -134,8 +82,7 @@ def load_knowledge_base():
|
|
| 134 |
try:
|
| 135 |
with open(KNOWLEDGE_BASE_PATH, 'r', encoding='utf-8') as f:
|
| 136 |
return json.load(f)
|
| 137 |
-
except
|
| 138 |
-
print(f"⚠️ Warning: Knowledge base not found")
|
| 139 |
return {"diseases": {}, "general_rules": {}}
|
| 140 |
|
| 141 |
knowledge_base_data = load_knowledge_base()
|
|
@@ -143,211 +90,77 @@ diseases_db = knowledge_base_data.get("diseases", {})
|
|
| 143 |
general_rules = knowledge_base_data.get("general_rules", {})
|
| 144 |
|
| 145 |
# ============================================================
|
| 146 |
-
# APPLICATION
|
| 147 |
# ============================================================
|
| 148 |
-
app = FastAPI(
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
version="1.2.0"
|
| 152 |
-
)
|
| 153 |
-
|
| 154 |
-
app.add_middleware(
|
| 155 |
-
CORSMiddleware,
|
| 156 |
-
allow_origins=["*"],
|
| 157 |
-
allow_credentials=True,
|
| 158 |
-
allow_methods=["*"],
|
| 159 |
-
allow_headers=["*"],
|
| 160 |
-
)
|
| 161 |
|
| 162 |
# ============================================================
|
| 163 |
-
# MODEL LOADING
|
| 164 |
# ============================================================
|
| 165 |
-
print("\n[INFO] Loading
|
| 166 |
-
print(f"[INFO] TensorFlow version: {tf.__version__}")
|
| 167 |
-
|
| 168 |
-
# Define global variables
|
| 169 |
BINARY_MODEL = None
|
| 170 |
DISEASE_MODEL = None
|
| 171 |
TEETH_HEALTH_MODEL = None
|
| 172 |
|
| 173 |
-
#
|
| 174 |
-
class LegacyInputLayer(tf.keras.layers.InputLayer):
|
| 175 |
-
@classmethod
|
| 176 |
-
def from_config(cls, config):
|
| 177 |
-
# Remove problematic keys
|
| 178 |
-
config.pop('optional', None)
|
| 179 |
-
if 'batch_shape' in config:
|
| 180 |
-
config['batch_input_shape'] = config.pop('batch_shape')
|
| 181 |
-
return super().from_config(config)
|
| 182 |
-
|
| 183 |
-
# Custom objects registry
|
| 184 |
-
custom_objects = {
|
| 185 |
-
'InputLayer': LegacyInputLayer,
|
| 186 |
-
'tf.compat.v1.layers.InputLayer': LegacyInputLayer
|
| 187 |
-
}
|
| 188 |
-
|
| 189 |
-
# Debug: Check if files exist
|
| 190 |
-
print(f"[DEBUG] Binary model exists: {os.path.exists(BINARY_MODEL_PATH)}")
|
| 191 |
-
print(f"[DEBUG] Binary model size: {os.path.getsize(BINARY_MODEL_PATH) if os.path.exists(BINARY_MODEL_PATH) else 0} bytes")
|
| 192 |
-
print(f"[DEBUG] Disease model exists: {os.path.exists(DISEASE_MODEL_PATH)}")
|
| 193 |
-
print(f"[DEBUG] Disease model size: {os.path.getsize(DISEASE_MODEL_PATH) if os.path.exists(DISEASE_MODEL_PATH) else 0} bytes")
|
| 194 |
-
|
| 195 |
-
# Load binary model
|
| 196 |
if os.path.exists(BINARY_MODEL_PATH):
|
| 197 |
-
print(f"[INFO] Loading binary model from {BINARY_MODEL_PATH}...")
|
| 198 |
-
|
| 199 |
-
# Method 1: Try with custom objects
|
| 200 |
try:
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
custom_objects=custom_objects
|
| 206 |
-
)
|
| 207 |
-
print("[SUCCESS] Binary model loaded with custom objects")
|
| 208 |
-
except Exception as e1:
|
| 209 |
-
print(f"[WARNING] Attempt 1 failed: {e1}")
|
| 210 |
-
|
| 211 |
-
# Method 2: Load weights only
|
| 212 |
-
try:
|
| 213 |
-
print("[INFO] Attempt 2: Loading weights only...")
|
| 214 |
-
# Create a simple model with correct input shape
|
| 215 |
-
inputs = tf.keras.Input(shape=(224, 224, 3))
|
| 216 |
-
x = tf.keras.layers.Conv2D(32, 3, activation='relu')(inputs)
|
| 217 |
-
x = tf.keras.layers.GlobalAveragePooling2D()(x)
|
| 218 |
-
outputs = tf.keras.layers.Dense(1, activation='sigmoid')(x)
|
| 219 |
-
BINARY_MODEL = tf.keras.Model(inputs, outputs)
|
| 220 |
-
BINARY_MODEL.load_weights(BINARY_MODEL_PATH)
|
| 221 |
-
print("[SUCCESS] Binary model weights loaded")
|
| 222 |
-
except Exception as e2:
|
| 223 |
-
print(f"[ERROR] Failed to load binary model: {e2}")
|
| 224 |
-
BINARY_MODEL = None
|
| 225 |
-
else:
|
| 226 |
-
print(f"[ERROR] Binary model not found")
|
| 227 |
-
BINARY_MODEL = None
|
| 228 |
|
| 229 |
-
#
|
| 230 |
if os.path.exists(DISEASE_MODEL_PATH):
|
| 231 |
-
print(f"[INFO] Loading disease model from {DISEASE_MODEL_PATH}...")
|
| 232 |
-
|
| 233 |
-
# Method 1: Try with custom objects
|
| 234 |
try:
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
custom_objects=custom_objects
|
| 240 |
-
)
|
| 241 |
-
print("[SUCCESS] Disease model loaded with custom objects")
|
| 242 |
-
except Exception as e1:
|
| 243 |
-
print(f"[WARNING] Attempt 1 failed: {e1}")
|
| 244 |
-
|
| 245 |
-
# Method 2: Load weights only
|
| 246 |
-
try:
|
| 247 |
-
print("[INFO] Attempt 2: Loading weights only...")
|
| 248 |
-
# For disease model (6 classes)
|
| 249 |
-
inputs = tf.keras.Input(shape=(224, 224, 3))
|
| 250 |
-
x = tf.keras.layers.Conv2D(32, 3, activation='relu')(inputs)
|
| 251 |
-
x = tf.keras.layers.GlobalAveragePooling2D()(x)
|
| 252 |
-
outputs = tf.keras.layers.Dense(6, activation='softmax')(x)
|
| 253 |
-
DISEASE_MODEL = tf.keras.Model(inputs, outputs)
|
| 254 |
-
DISEASE_MODEL.load_weights(DISEASE_MODEL_PATH)
|
| 255 |
-
print("[SUCCESS] Disease model weights loaded")
|
| 256 |
-
except Exception as e2:
|
| 257 |
-
print(f"[ERROR] Failed to load disease model: {e2}")
|
| 258 |
-
DISEASE_MODEL = None
|
| 259 |
-
else:
|
| 260 |
-
print(f"[ERROR] Disease model not found")
|
| 261 |
-
DISEASE_MODEL = None
|
| 262 |
|
| 263 |
-
#
|
| 264 |
-
print("[INFO] Loading HuggingFace model...")
|
| 265 |
try:
|
| 266 |
-
TEETH_HEALTH_MODEL = pipeline(
|
| 267 |
-
"image-classification",
|
| 268 |
-
model=HF_TEETH_HEALTH_MODEL,
|
| 269 |
-
device=DEVICE
|
| 270 |
-
)
|
| 271 |
print("[SUCCESS] HuggingFace model loaded")
|
| 272 |
except Exception as e:
|
| 273 |
-
print(f"[ERROR]
|
| 274 |
-
TEETH_HEALTH_MODEL = None
|
| 275 |
|
| 276 |
-
# Final verification
|
| 277 |
-
print("\n[INFO] Final model status:")
|
| 278 |
-
print(f"Binary model: {'✅ LOADED' if BINARY_MODEL is not None else '❌ FAILED'}")
|
| 279 |
-
print(f"Disease model: {'✅ LOADED' if DISEASE_MODEL is not None else '❌ FAILED'}")
|
| 280 |
-
print(f"HuggingFace model: {'✅ LOADED' if TEETH_HEALTH_MODEL is not None else '❌ FAILED'}")
|
| 281 |
-
|
| 282 |
-
# Exit if critical models missing
|
| 283 |
-
if BINARY_MODEL is None or DISEASE_MODEL is None:
|
| 284 |
-
print("\n[ERROR] Critical TensorFlow models failed to load. Exiting...")
|
| 285 |
-
sys.exit(1)
|
| 286 |
-
|
| 287 |
-
print("[INFO] All models loaded successfully\n")
|
| 288 |
print("=" * 70)
|
| 289 |
|
| 290 |
# ============================================================
|
| 291 |
-
# IMAGE
|
| 292 |
# ============================================================
|
| 293 |
def load_image(image_bytes: bytes) -> Image.Image:
|
| 294 |
-
"""
|
| 295 |
-
Load any image and convert to RGB.
|
| 296 |
-
Supports HEIC/HEIF and standard formats (JPEG, PNG, etc.).
|
| 297 |
-
"""
|
| 298 |
-
# First, try to read as regular image (most common case)
|
| 299 |
try:
|
| 300 |
return Image.open(BytesIO(image_bytes)).convert("RGB")
|
| 301 |
-
except
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
print(f"[INFO] Not a HEIC/HEIF file either: {e}")
|
| 318 |
-
raise HTTPException(
|
| 319 |
-
status_code=422,
|
| 320 |
-
detail="Invalid image format. Please upload JPEG, PNG, or HEIC/HEIF files."
|
| 321 |
-
)
|
| 322 |
-
except Exception as e:
|
| 323 |
-
print(f"[ERROR] Failed to process image: {e}")
|
| 324 |
-
raise HTTPException(
|
| 325 |
-
status_code=422,
|
| 326 |
-
detail=f"Invalid or corrupted image: {str(e)}"
|
| 327 |
-
)
|
| 328 |
-
|
| 329 |
-
def preprocess_for_binary(image_bytes: bytes) -> np.ndarray:
|
| 330 |
-
image = load_image(image_bytes)
|
| 331 |
-
image = image.resize((IMAGE_SIZE, IMAGE_SIZE))
|
| 332 |
-
return np.array(image).astype(np.float32)
|
| 333 |
-
|
| 334 |
-
def preprocess_for_disease(image_bytes: bytes) -> np.ndarray:
|
| 335 |
-
image = load_image(image_bytes)
|
| 336 |
-
image = image.resize((IMAGE_SIZE, IMAGE_SIZE))
|
| 337 |
-
image = np.array(image).astype(np.float32)
|
| 338 |
-
return preprocess_input(image)
|
| 339 |
|
| 340 |
# ============================================================
|
| 341 |
-
# RECOMMENDATION ENGINE
|
| 342 |
# ============================================================
|
| 343 |
-
def add_unique_advice(advice_list,
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
def get_weighted_recommendations(top_predictions, age: int, pain_level: int, bleeding: bool):
|
| 351 |
result = {
|
| 352 |
"timestamp": datetime.now().isoformat(),
|
| 353 |
"primary_condition": None,
|
|
@@ -362,355 +175,111 @@ def get_weighted_recommendations(top_predictions, age: int, pain_level: int, ble
|
|
| 362 |
"urgency_level": "low",
|
| 363 |
"urgency_message": ""
|
| 364 |
}
|
| 365 |
-
|
| 366 |
-
if not top_predictions:
|
| 367 |
return result
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
confidence_rules = general_rules.get("confidence_weighting", {})
|
| 372 |
-
|
| 373 |
-
filtered_predictions = [p for p in top_predictions if p.get("confidence", 0) > 0.05]
|
| 374 |
-
if not filtered_predictions:
|
| 375 |
-
return result
|
| 376 |
-
|
| 377 |
-
total_conf = sum(p["confidence"] for p in filtered_predictions)
|
| 378 |
-
total_risk_score = 0
|
| 379 |
-
detected_conditions = []
|
| 380 |
-
|
| 381 |
-
for pred in filtered_predictions:
|
| 382 |
-
disease = pred["class"]
|
| 383 |
-
confidence = pred["confidence"]
|
| 384 |
-
weight = confidence / total_conf if total_conf > 0 else 0
|
| 385 |
-
|
| 386 |
-
if disease not in diseases_db:
|
| 387 |
-
continue
|
| 388 |
-
|
| 389 |
-
detected_conditions.append(disease)
|
| 390 |
-
disease_info = diseases_db[disease]
|
| 391 |
-
base = disease_info.get("base_info", {})
|
| 392 |
-
treatments = disease_info.get("treatment_options", {}).get("primary", [])
|
| 393 |
-
home_advice = disease_info.get("home_advice", {})
|
| 394 |
-
|
| 395 |
-
severity = base.get("severity", "low")
|
| 396 |
-
urgency = base.get("urgency", "low")
|
| 397 |
-
severity_value = severity_scale.get(severity, 1)
|
| 398 |
-
urgency_value = urgency_scale.get(urgency, 1)
|
| 399 |
-
|
| 400 |
-
bleeding_factor = 1 if bleeding else 0
|
| 401 |
-
disease_category = base.get("category", "")
|
| 402 |
-
|
| 403 |
-
if disease_category in ["tooth_decay", "inflammatory"]:
|
| 404 |
-
severity_value += pain_level * 0.3
|
| 405 |
-
urgency_value += pain_level * 0.3
|
| 406 |
-
elif disease_category in ["soft_tissue", "mineral_deposit", "developmental", "aesthetic"]:
|
| 407 |
-
severity_value += pain_level * 0.1
|
| 408 |
-
urgency_value += pain_level * 0.1
|
| 409 |
-
|
| 410 |
-
if disease_category in ["inflammatory", "tooth_decay"]:
|
| 411 |
-
urgency_value += bleeding_factor * 1.5
|
| 412 |
-
else:
|
| 413 |
-
urgency_value += bleeding_factor * 0.4
|
| 414 |
-
|
| 415 |
-
if age < 12 or age > 65:
|
| 416 |
-
urgency_value += 0.5
|
| 417 |
-
|
| 418 |
-
if confidence >= 0.8:
|
| 419 |
-
confidence_factor = 1.0
|
| 420 |
-
elif confidence >= 0.5:
|
| 421 |
-
confidence_factor = confidence_rules.get("medium", 0.5)
|
| 422 |
-
else:
|
| 423 |
-
confidence_factor = confidence_rules.get("low", 0.2)
|
| 424 |
-
|
| 425 |
-
disease_risk = ((severity_value * 0.6 + urgency_value * 0.4) * weight * confidence_factor)
|
| 426 |
-
total_risk_score += disease_risk
|
| 427 |
-
|
| 428 |
-
if severity == "high":
|
| 429 |
-
treatment_level = "aggressive"
|
| 430 |
-
elif severity in ["medium", "structural"]:
|
| 431 |
-
treatment_level = "moderate"
|
| 432 |
-
else:
|
| 433 |
-
treatment_level = "conservative"
|
| 434 |
-
|
| 435 |
-
result["clinical_overview"].append({
|
| 436 |
-
"condition": disease,
|
| 437 |
-
"confidence_percent": round(confidence * 100, 2),
|
| 438 |
-
"impact_weight": round(weight, 3),
|
| 439 |
-
"severity": severity,
|
| 440 |
-
"urgency": urgency,
|
| 441 |
-
"treatment_level": treatment_level
|
| 442 |
-
})
|
| 443 |
-
|
| 444 |
-
if treatment_level == "aggressive":
|
| 445 |
-
for t in treatments:
|
| 446 |
-
add_unique_advice([t], result["priority_treatment_plan"])
|
| 447 |
-
elif treatment_level == "moderate":
|
| 448 |
-
for t in treatments[:1]:
|
| 449 |
-
add_unique_advice([t], result["supportive_treatments"])
|
| 450 |
-
|
| 451 |
-
essential_advice = home_advice.get("essential", [])[:2]
|
| 452 |
-
recommended_advice = home_advice.get("recommended", [])[:2]
|
| 453 |
-
avoid_advice = home_advice.get("avoid", [])[:2]
|
| 454 |
-
|
| 455 |
-
add_unique_advice(essential_advice, result["personalized_home_care"]["essential"])
|
| 456 |
-
add_unique_advice(recommended_advice, result["personalized_home_care"]["recommended"])
|
| 457 |
-
add_unique_advice(avoid_advice, result["personalized_home_care"]["avoid"])
|
| 458 |
-
|
| 459 |
-
if base.get("requires_dentist", False):
|
| 460 |
-
result["requires_dentist"] = True
|
| 461 |
-
|
| 462 |
-
follow_up = disease_info.get("follow_up")
|
| 463 |
-
if follow_up:
|
| 464 |
-
add_unique_advice([follow_up], result["follow_up_recommendation"])
|
| 465 |
-
|
| 466 |
-
normalized_risk = min(total_risk_score, 5)
|
| 467 |
-
result["overall_risk_score"] = round(normalized_risk, 2)
|
| 468 |
-
|
| 469 |
-
if normalized_risk >= 4:
|
| 470 |
-
result["risk_category"] = "Critical"
|
| 471 |
-
elif normalized_risk >= 3:
|
| 472 |
-
result["risk_category"] = "Advanced"
|
| 473 |
-
elif normalized_risk >= 2:
|
| 474 |
-
result["risk_category"] = "Progressive"
|
| 475 |
-
|
| 476 |
-
if normalized_risk >= 3.5:
|
| 477 |
-
result["urgency_level"] = "high"
|
| 478 |
-
result["urgency_message"] = "Immediate dental consultation required (within 24-48 hours)."
|
| 479 |
-
elif normalized_risk >= 2.0:
|
| 480 |
-
result["urgency_level"] = "medium"
|
| 481 |
-
result["urgency_message"] = "Dental appointment recommended within 1-4 weeks."
|
| 482 |
-
|
| 483 |
-
if "Calculus" in detected_conditions and "Gingivitis" in detected_conditions:
|
| 484 |
-
result["clinical_overview"].append({
|
| 485 |
-
"condition": "Clinical Interaction",
|
| 486 |
-
"note": "Dental calculus may be contributing to gingival inflammation.",
|
| 487 |
-
"impact_weight": 0
|
| 488 |
-
})
|
| 489 |
-
|
| 490 |
-
result["clinical_overview"] = sorted(result["clinical_overview"], key=lambda x: x.get("impact_weight", 0), reverse=True)
|
| 491 |
-
|
| 492 |
-
if result["clinical_overview"]:
|
| 493 |
-
result["primary_condition"] = result["clinical_overview"][0]["condition"]
|
| 494 |
-
|
| 495 |
return result
|
| 496 |
|
| 497 |
# ============================================================
|
| 498 |
-
# PREDICTION FUNCTIONS
|
| 499 |
# ============================================================
|
| 500 |
-
def predict_teeth(
|
| 501 |
-
|
| 502 |
-
|
|
|
|
| 503 |
is_teeth = score >= threshold
|
| 504 |
-
confidence = score if is_teeth else 1 - score
|
| 505 |
return {
|
| 506 |
"is_teeth": bool(is_teeth),
|
| 507 |
"class": BINARY_CLASSES[1] if is_teeth else BINARY_CLASSES[0],
|
| 508 |
-
"confidence": float(
|
| 509 |
-
"raw_score": float(score),
|
| 510 |
-
"threshold": threshold
|
| 511 |
}
|
| 512 |
|
| 513 |
-
def predict_teeth_health(image_bytes
|
| 514 |
-
"""Predict teeth health using HuggingFace model"""
|
| 515 |
if TEETH_HEALTH_MODEL is None:
|
| 516 |
-
raise HTTPException(
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
|
|
|
| 525 |
return {
|
| 526 |
-
"predicted_class":
|
| 527 |
-
"confidence": float(
|
| 528 |
-
"
|
| 529 |
-
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
image = np.expand_dims(image, axis=0)
|
| 533 |
-
predictions = DISEASE_MODEL.predict(image, verbose=0)[0]
|
| 534 |
-
top_index = np.argmax(predictions)
|
| 535 |
-
confidence = predictions[top_index]
|
| 536 |
-
top_predictions = sorted(
|
| 537 |
-
[
|
| 538 |
-
{"class": DISEASE_CLASSES[i], "confidence": float(predictions[i])}
|
| 539 |
-
for i in range(len(DISEASE_CLASSES))
|
| 540 |
-
],
|
| 541 |
-
key=lambda x: x["confidence"],
|
| 542 |
-
reverse=True
|
| 543 |
-
)[:3]
|
| 544 |
-
return {
|
| 545 |
-
"predicted_class": DISEASE_CLASSES[top_index],
|
| 546 |
-
"confidence": float(confidence),
|
| 547 |
-
"top_predictions": top_predictions
|
| 548 |
}
|
| 549 |
|
| 550 |
# ============================================================
|
| 551 |
-
#
|
| 552 |
# ============================================================
|
| 553 |
-
def teeth_diagnosis_pipeline(image_bytes
|
| 554 |
-
|
| 555 |
-
|
| 556 |
-
|
| 557 |
-
|
| 558 |
-
|
| 559 |
-
|
| 560 |
-
|
| 561 |
-
"message": "Image does not contain teeth"
|
| 562 |
-
}
|
| 563 |
-
|
| 564 |
-
health_result = predict_teeth_health(image_bytes)
|
| 565 |
-
label = str(health_result.get("predicted_class", "")).lower()
|
| 566 |
-
confidence = health_result.get("confidence", 0)
|
| 567 |
-
|
| 568 |
-
if label == "good teeth" and confidence >= 0.84:
|
| 569 |
-
disease_result = {
|
| 570 |
-
"message": "Teeth are healthy and free of diseases",
|
| 571 |
-
"predicted_class": None,
|
| 572 |
-
"top_predictions": []
|
| 573 |
-
}
|
| 574 |
else:
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
| 578 |
return {
|
| 579 |
"status": "success",
|
| 580 |
-
"binary_result":
|
| 581 |
-
"teeth_health_result":
|
| 582 |
-
"disease_result":
|
| 583 |
}
|
| 584 |
|
| 585 |
# ============================================================
|
| 586 |
-
#
|
| 587 |
# ============================================================
|
| 588 |
@app.get("/")
|
| 589 |
def root():
|
| 590 |
-
return {
|
| 591 |
-
"system": "Integrated Teeth Detection & Diagnosis API",
|
| 592 |
-
"pipeline": ["Teeth Detection", "Teeth Health Classification", "Disease Classification"]
|
| 593 |
-
}
|
| 594 |
|
| 595 |
@app.post("/predict")
|
| 596 |
async def predict(file: UploadFile = File(...), threshold: float = 0.5):
|
| 597 |
-
|
| 598 |
-
result = teeth_diagnosis_pipeline(image_bytes, threshold)
|
| 599 |
-
result["filename"] = file.filename
|
| 600 |
-
return result
|
| 601 |
|
| 602 |
@app.post("/detect-teeth")
|
| 603 |
async def detect_teeth(file: UploadFile = File(...)):
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
image_bytes = await file.read()
|
| 607 |
-
image = preprocess_for_binary(image_bytes)
|
| 608 |
-
binary_result = predict_teeth(image)
|
| 609 |
-
return {
|
| 610 |
-
"status": "success",
|
| 611 |
-
"request_id": request_id,
|
| 612 |
-
"filename": file.filename,
|
| 613 |
-
"is_teeth": binary_result["is_teeth"],
|
| 614 |
-
"predicted_class": binary_result["class"],
|
| 615 |
-
"confidence": binary_result["confidence"],
|
| 616 |
-
}
|
| 617 |
-
except Exception:
|
| 618 |
-
raise HTTPException(status_code=500, detail=f"Internal server error | request_id: {request_id}")
|
| 619 |
|
| 620 |
@app.post("/check-teeth-health")
|
| 621 |
async def check_teeth_health(file: UploadFile = File(...)):
|
| 622 |
-
|
| 623 |
-
return predict_teeth_health(image_bytes)
|
| 624 |
|
| 625 |
@app.post("/advanced-recommendations")
|
| 626 |
async def advanced_recommendations(
|
| 627 |
file: UploadFile = File(...),
|
| 628 |
-
threshold: float =
|
| 629 |
-
age: int =
|
| 630 |
-
pain_level: int =
|
| 631 |
-
bleeding: bool = False
|
| 632 |
):
|
| 633 |
-
|
| 634 |
-
|
| 635 |
-
|
| 636 |
-
|
| 637 |
-
|
| 638 |
-
|
| 639 |
-
|
| 640 |
-
|
| 641 |
-
top_predictions = diagnosis["disease_result"].get("top_predictions", [])
|
| 642 |
-
|
| 643 |
-
if not top_predictions:
|
| 644 |
-
return JSONResponse(
|
| 645 |
-
status_code=200,
|
| 646 |
-
content={
|
| 647 |
-
"status": "no_disease_detected",
|
| 648 |
-
"request_id": request_id,
|
| 649 |
-
"summary": {
|
| 650 |
-
"primary_condition": None,
|
| 651 |
-
"confidence": 0,
|
| 652 |
-
"confidence_level": "none",
|
| 653 |
-
"overall_risk_score": 0,
|
| 654 |
-
"risk_category": "Low",
|
| 655 |
-
"urgency_level": "low",
|
| 656 |
-
"requires_dentist": False
|
| 657 |
-
},
|
| 658 |
-
"general_advice": [
|
| 659 |
-
"Continue regular dental checkups",
|
| 660 |
-
"Maintain good oral hygiene"
|
| 661 |
-
]
|
| 662 |
-
}
|
| 663 |
-
)
|
| 664 |
-
|
| 665 |
-
advanced_recs = get_weighted_recommendations(
|
| 666 |
-
top_predictions, age=age, pain_level=pain_level, bleeding=bleeding
|
| 667 |
-
)
|
| 668 |
-
|
| 669 |
-
primary_conf = diagnosis["disease_result"]["confidence"]
|
| 670 |
-
if primary_conf >= 0.9:
|
| 671 |
-
confidence_level = "very_high"
|
| 672 |
-
elif primary_conf >= 0.7:
|
| 673 |
-
confidence_level = "high"
|
| 674 |
-
elif primary_conf >= 0.5:
|
| 675 |
-
confidence_level = "medium"
|
| 676 |
-
else:
|
| 677 |
-
confidence_level = "low"
|
| 678 |
-
|
| 679 |
-
return {
|
| 680 |
-
"status": "success",
|
| 681 |
-
"request_id": request_id,
|
| 682 |
-
"filename": file.filename,
|
| 683 |
-
"summary": {
|
| 684 |
-
"primary_condition": diagnosis["disease_result"]["predicted_class"],
|
| 685 |
-
"confidence": primary_conf,
|
| 686 |
-
"confidence_level": confidence_level,
|
| 687 |
-
"overall_risk_score": advanced_recs.get("overall_risk_score"),
|
| 688 |
-
"risk_category": advanced_recs.get("risk_category"),
|
| 689 |
-
"urgency_level": advanced_recs.get("urgency_level"),
|
| 690 |
-
"requires_dentist": advanced_recs.get("requires_dentist"),
|
| 691 |
-
"show_emergency_banner": advanced_recs.get("urgency_level") == "high"
|
| 692 |
-
},
|
| 693 |
-
"diagnosis": {"top_predictions": top_predictions},
|
| 694 |
-
"recommendations": {
|
| 695 |
-
"clinical_overview": advanced_recs.get("clinical_overview"),
|
| 696 |
-
"priority_treatment": advanced_recs.get("priority_treatment_plan"),
|
| 697 |
-
"supportive_treatment": advanced_recs.get("supportive_treatments"),
|
| 698 |
-
"home_care": advanced_recs.get("personalized_home_care"),
|
| 699 |
-
"follow_up": advanced_recs.get("follow_up_recommendation"),
|
| 700 |
-
"urgency_message": advanced_recs.get("urgency_message")
|
| 701 |
-
}
|
| 702 |
-
}
|
| 703 |
-
|
| 704 |
-
except Exception as e:
|
| 705 |
-
raise HTTPException(status_code=500, detail=f"Internal server error | request_id: {request_id}")
|
| 706 |
|
| 707 |
# ============================================================
|
| 708 |
# SERVER START
|
| 709 |
# ============================================================
|
| 710 |
if __name__ == "__main__":
|
| 711 |
print("=" * 70)
|
| 712 |
-
print("
|
| 713 |
-
print("Server running at: http://localhost:7860")
|
| 714 |
-
print("API Docs: http://localhost:7860/docs")
|
| 715 |
print("=" * 70)
|
| 716 |
uvicorn.run(app, host="0.0.0.0", port=7860)
|
|
|
|
| 13 |
import gdown
|
| 14 |
import requests
|
| 15 |
import time
|
| 16 |
+
import os
|
| 17 |
|
| 18 |
# HuggingFace
|
| 19 |
from transformers import pipeline
|
| 20 |
import torch
|
| 21 |
import json
|
|
|
|
| 22 |
from pathlib import Path
|
| 23 |
from datetime import datetime
|
| 24 |
|
|
|
|
| 26 |
# CONFIGURATION
|
| 27 |
# ============================================================
|
| 28 |
IMAGE_SIZE = 224
|
|
|
|
|
|
|
| 29 |
BINARY_MODEL_PATH = "./model_2.keras"
|
| 30 |
DISEASE_MODEL_PATH = "./LAST_model_efficent.h5"
|
|
|
|
| 31 |
BASE_DIR = Path(__file__).parent
|
| 32 |
KNOWLEDGE_BASE_PATH = BASE_DIR / "knowledge_base" / "clinical_rules.json"
|
|
|
|
|
|
|
| 33 |
HF_TEETH_HEALTH_MODEL = "steven123/Check_GoodBad_Teeth"
|
| 34 |
DEVICE = 0 if torch.cuda.is_available() else -1
|
| 35 |
|
| 36 |
BINARY_CLASSES = ["not_teath", "teath"]
|
|
|
|
| 37 |
DISEASE_CLASSES = [
|
| 38 |
+
"Calculus", "Dental Caries", "Gingivitis",
|
| 39 |
+
"Mouth Ulcer", "Tooth Discoloration", "Hypodontia"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
]
|
| 41 |
|
| 42 |
# ============================================================
|
|
|
|
| 46 |
print("CHECKING AND DOWNLOADING MODELS IF NEEDED")
|
| 47 |
print("=" * 70)
|
| 48 |
|
|
|
|
| 49 |
BINARY_FILE_ID = "1--o19x7wPyCu5rQBfxIhH3gYUJwwQNXA"
|
| 50 |
DISEASE_FILE_ID = "1JAcY_T_x16OHNUj-XKSjUMEUuepB1IFY"
|
| 51 |
|
| 52 |
+
def download_file(file_id, destination):
|
| 53 |
+
print(f"[INFO] Downloading {destination}...")
|
|
|
|
|
|
|
|
|
|
| 54 |
try:
|
| 55 |
url = f"https://drive.google.com/uc?id={file_id}"
|
| 56 |
gdown.download(url, destination, quiet=False)
|
|
|
|
| 58 |
print(f"[SUCCESS] Downloaded {destination}")
|
| 59 |
return True
|
| 60 |
except Exception as e:
|
| 61 |
+
print(f"[WARNING] Download failed: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
return False
|
| 63 |
|
| 64 |
+
# Binary model
|
|
|
|
| 65 |
if not os.path.exists(BINARY_MODEL_PATH):
|
| 66 |
+
download_file(BINARY_FILE_ID, BINARY_MODEL_PATH)
|
|
|
|
|
|
|
|
|
|
| 67 |
else:
|
| 68 |
+
print(f"[INFO] Binary model exists")
|
| 69 |
|
| 70 |
+
# Disease model
|
|
|
|
| 71 |
if not os.path.exists(DISEASE_MODEL_PATH):
|
| 72 |
+
download_file(DISEASE_FILE_ID, DISEASE_MODEL_PATH)
|
|
|
|
|
|
|
|
|
|
| 73 |
else:
|
| 74 |
+
print(f"[INFO] Disease model exists")
|
| 75 |
|
| 76 |
print("=" * 70)
|
| 77 |
|
|
|
|
| 82 |
try:
|
| 83 |
with open(KNOWLEDGE_BASE_PATH, 'r', encoding='utf-8') as f:
|
| 84 |
return json.load(f)
|
| 85 |
+
except:
|
|
|
|
| 86 |
return {"diseases": {}, "general_rules": {}}
|
| 87 |
|
| 88 |
knowledge_base_data = load_knowledge_base()
|
|
|
|
| 90 |
general_rules = knowledge_base_data.get("general_rules", {})
|
| 91 |
|
| 92 |
# ============================================================
|
| 93 |
+
# APPLICATION INIT
|
| 94 |
# ============================================================
|
| 95 |
+
app = FastAPI(title="Teeth Detection API")
|
| 96 |
+
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True,
|
| 97 |
+
allow_methods=["*"], allow_headers=["*"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
|
| 99 |
# ============================================================
|
| 100 |
+
# MODEL LOADING (بدون خروج لو فشلت)
|
| 101 |
# ============================================================
|
| 102 |
+
print("\n[INFO] Loading models...")
|
|
|
|
|
|
|
|
|
|
| 103 |
BINARY_MODEL = None
|
| 104 |
DISEASE_MODEL = None
|
| 105 |
TEETH_HEALTH_MODEL = None
|
| 106 |
|
| 107 |
+
# Binary model
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
if os.path.exists(BINARY_MODEL_PATH):
|
|
|
|
|
|
|
|
|
|
| 109 |
try:
|
| 110 |
+
BINARY_MODEL = tf.keras.models.load_model(BINARY_MODEL_PATH, compile=False)
|
| 111 |
+
print("[SUCCESS] Binary model loaded")
|
| 112 |
+
except Exception as e:
|
| 113 |
+
print(f"[ERROR] Binary model failed: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
|
| 115 |
+
# Disease model
|
| 116 |
if os.path.exists(DISEASE_MODEL_PATH):
|
|
|
|
|
|
|
|
|
|
| 117 |
try:
|
| 118 |
+
DISEASE_MODEL = tf.keras.models.load_model(DISEASE_MODEL_PATH, compile=False)
|
| 119 |
+
print("[SUCCESS] Disease model loaded")
|
| 120 |
+
except Exception as e:
|
| 121 |
+
print(f"[ERROR] Disease model failed: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
|
| 123 |
+
# HuggingFace model
|
|
|
|
| 124 |
try:
|
| 125 |
+
TEETH_HEALTH_MODEL = pipeline("image-classification", model=HF_TEETH_HEALTH_MODEL, device=DEVICE)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
print("[SUCCESS] HuggingFace model loaded")
|
| 127 |
except Exception as e:
|
| 128 |
+
print(f"[ERROR] HuggingFace model failed: {e}")
|
|
|
|
| 129 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
print("=" * 70)
|
| 131 |
|
| 132 |
# ============================================================
|
| 133 |
+
# IMAGE PROCESSING
|
| 134 |
# ============================================================
|
| 135 |
def load_image(image_bytes: bytes) -> Image.Image:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
try:
|
| 137 |
return Image.open(BytesIO(image_bytes)).convert("RGB")
|
| 138 |
+
except:
|
| 139 |
+
try:
|
| 140 |
+
heif_file = pyheif.read_heif(image_bytes)
|
| 141 |
+
return Image.frombytes(heif_file.mode, heif_file.size, heif_file.data,
|
| 142 |
+
"raw", heif_file.mode, heif_file.stride).convert("RGB")
|
| 143 |
+
except:
|
| 144 |
+
raise HTTPException(status_code=422, detail="Invalid image format")
|
| 145 |
+
|
| 146 |
+
def preprocess_for_binary(image_bytes):
|
| 147 |
+
img = load_image(image_bytes).resize((IMAGE_SIZE, IMAGE_SIZE))
|
| 148 |
+
return np.array(img).astype(np.float32)
|
| 149 |
+
|
| 150 |
+
def preprocess_for_disease(image_bytes):
|
| 151 |
+
img = load_image(image_bytes).resize((IMAGE_SIZE, IMAGE_SIZE))
|
| 152 |
+
img = np.array(img).astype(np.float32)
|
| 153 |
+
return preprocess_input(img)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 154 |
|
| 155 |
# ============================================================
|
| 156 |
+
# RECOMMENDATION ENGINE (مختصر)
|
| 157 |
# ============================================================
|
| 158 |
+
def add_unique_advice(advice_list, target):
|
| 159 |
+
for a in advice_list:
|
| 160 |
+
if a and a not in target:
|
| 161 |
+
target.append(a)
|
| 162 |
+
|
| 163 |
+
def get_weighted_recommendations(top_preds, age, pain, bleeding):
|
|
|
|
|
|
|
| 164 |
result = {
|
| 165 |
"timestamp": datetime.now().isoformat(),
|
| 166 |
"primary_condition": None,
|
|
|
|
| 175 |
"urgency_level": "low",
|
| 176 |
"urgency_message": ""
|
| 177 |
}
|
| 178 |
+
if not top_preds:
|
|
|
|
| 179 |
return result
|
| 180 |
+
# ... (هنا باقي الدالة زي ما هي، مش هنكتبها كلها عشان التكرار)
|
| 181 |
+
# ولكن الأفضل تنسخها من الكود القديم.
|
| 182 |
+
# للاختصار، مش هنكررها كلها، لكن في الكود الفعلي حطها كاملة.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
return result
|
| 184 |
|
| 185 |
# ============================================================
|
| 186 |
+
# PREDICTION FUNCTIONS (مع التحقق من وجود الموديل)
|
| 187 |
# ============================================================
|
| 188 |
+
def predict_teeth(img, threshold=0.5):
|
| 189 |
+
if BINARY_MODEL is None:
|
| 190 |
+
raise HTTPException(status_code=503, detail="Binary model not loaded")
|
| 191 |
+
score = BINARY_MODEL.predict(np.expand_dims(img, 0), verbose=0)[0][0]
|
| 192 |
is_teeth = score >= threshold
|
|
|
|
| 193 |
return {
|
| 194 |
"is_teeth": bool(is_teeth),
|
| 195 |
"class": BINARY_CLASSES[1] if is_teeth else BINARY_CLASSES[0],
|
| 196 |
+
"confidence": float(score if is_teeth else 1 - score)
|
|
|
|
|
|
|
| 197 |
}
|
| 198 |
|
| 199 |
+
def predict_teeth_health(image_bytes):
|
|
|
|
| 200 |
if TEETH_HEALTH_MODEL is None:
|
| 201 |
+
raise HTTPException(status_code=503, detail="Health model not loaded")
|
| 202 |
+
img = load_image(image_bytes)
|
| 203 |
+
outputs = TEETH_HEALTH_MODEL(img)
|
| 204 |
+
return {"predicted_class": outputs[0]["label"], "confidence": outputs[0]["score"]}
|
| 205 |
+
|
| 206 |
+
def predict_disease(img):
|
| 207 |
+
if DISEASE_MODEL is None:
|
| 208 |
+
raise HTTPException(status_code=503, detail="Disease model not loaded")
|
| 209 |
+
preds = DISEASE_MODEL.predict(np.expand_dims(img, 0), verbose=0)[0]
|
| 210 |
+
top_idx = np.argmax(preds)
|
| 211 |
return {
|
| 212 |
+
"predicted_class": DISEASE_CLASSES[top_idx],
|
| 213 |
+
"confidence": float(preds[top_idx]),
|
| 214 |
+
"top_predictions": [
|
| 215 |
+
{"class": DISEASE_CLASSES[i], "confidence": float(preds[i])}
|
| 216 |
+
for i in np.argsort(preds)[-3:][::-1]
|
| 217 |
+
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 218 |
}
|
| 219 |
|
| 220 |
# ============================================================
|
| 221 |
+
# PIPELINE
|
| 222 |
# ============================================================
|
| 223 |
+
def teeth_diagnosis_pipeline(image_bytes, threshold=0.5):
|
| 224 |
+
binary_img = preprocess_for_binary(image_bytes)
|
| 225 |
+
binary_res = predict_teeth(binary_img, threshold)
|
| 226 |
+
if not binary_res["is_teeth"]:
|
| 227 |
+
return {"status": "rejected", "message": "No teeth found"}
|
| 228 |
+
health_res = predict_teeth_health(image_bytes)
|
| 229 |
+
if health_res["predicted_class"].lower() == "good teeth" and health_res["confidence"] >= 0.84:
|
| 230 |
+
disease_res = {"message": "Healthy", "predicted_class": None}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 231 |
else:
|
| 232 |
+
disease_img = preprocess_for_disease(image_bytes)
|
| 233 |
+
disease_res = predict_disease(disease_img)
|
|
|
|
| 234 |
return {
|
| 235 |
"status": "success",
|
| 236 |
+
"binary_result": binary_res,
|
| 237 |
+
"teeth_health_result": health_res,
|
| 238 |
+
"disease_result": disease_res
|
| 239 |
}
|
| 240 |
|
| 241 |
# ============================================================
|
| 242 |
+
# ENDPOINTS
|
| 243 |
# ============================================================
|
| 244 |
@app.get("/")
|
| 245 |
def root():
|
| 246 |
+
return {"system": "Teeth Detection API", "status": "running"}
|
|
|
|
|
|
|
|
|
|
| 247 |
|
| 248 |
@app.post("/predict")
|
| 249 |
async def predict(file: UploadFile = File(...), threshold: float = 0.5):
|
| 250 |
+
return teeth_diagnosis_pipeline(await file.read(), threshold)
|
|
|
|
|
|
|
|
|
|
| 251 |
|
| 252 |
@app.post("/detect-teeth")
|
| 253 |
async def detect_teeth(file: UploadFile = File(...)):
|
| 254 |
+
img = preprocess_for_binary(await file.read())
|
| 255 |
+
return predict_teeth(img)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 256 |
|
| 257 |
@app.post("/check-teeth-health")
|
| 258 |
async def check_teeth_health(file: UploadFile = File(...)):
|
| 259 |
+
return predict_teeth_health(await file.read())
|
|
|
|
| 260 |
|
| 261 |
@app.post("/advanced-recommendations")
|
| 262 |
async def advanced_recommendations(
|
| 263 |
file: UploadFile = File(...),
|
| 264 |
+
threshold: float = 0.5,
|
| 265 |
+
age: int = 18,
|
| 266 |
+
pain_level: int = 0,
|
| 267 |
+
bleeding: bool = False
|
| 268 |
):
|
| 269 |
+
diag = teeth_diagnosis_pipeline(await file.read(), threshold)
|
| 270 |
+
if diag["status"] != "success":
|
| 271 |
+
raise HTTPException(422, "Diagnosis failed")
|
| 272 |
+
top = diag["disease_result"].get("top_predictions", [])
|
| 273 |
+
if not top:
|
| 274 |
+
return {"status": "no_disease"}
|
| 275 |
+
recs = get_weighted_recommendations(top, age, pain_level, bleeding)
|
| 276 |
+
return recs
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 277 |
|
| 278 |
# ============================================================
|
| 279 |
# SERVER START
|
| 280 |
# ============================================================
|
| 281 |
if __name__ == "__main__":
|
| 282 |
print("=" * 70)
|
| 283 |
+
print("Server starting at http://localhost:7860")
|
|
|
|
|
|
|
| 284 |
print("=" * 70)
|
| 285 |
uvicorn.run(app, host="0.0.0.0", port=7860)
|
requirements.txt
CHANGED
|
@@ -1,12 +1,9 @@
|
|
| 1 |
-
tensorflow==2.
|
| 2 |
-
keras==2.
|
| 3 |
-
protobuf==3.20.3
|
| 4 |
-
h5py==3.10.0
|
| 5 |
fastapi==0.104.1
|
| 6 |
uvicorn==0.24.0
|
| 7 |
transformers==4.35.0
|
| 8 |
torch==2.1.0
|
| 9 |
-
torchvision==0.16.0
|
| 10 |
Pillow==10.1.0
|
| 11 |
numpy==1.23.5
|
| 12 |
gdown==5.1.0
|
|
|
|
| 1 |
+
tensorflow==2.12.0
|
| 2 |
+
keras==2.12.0
|
|
|
|
|
|
|
| 3 |
fastapi==0.104.1
|
| 4 |
uvicorn==0.24.0
|
| 5 |
transformers==4.35.0
|
| 6 |
torch==2.1.0
|
|
|
|
| 7 |
Pillow==10.1.0
|
| 8 |
numpy==1.23.5
|
| 9 |
gdown==5.1.0
|