Keras
TF-Keras
AiroDx-API / app.py
ghafil's picture
Upload 3 files
14622e3 verified
Raw
History Blame Contribute Delete
6.46 kB
import os
import io
import cv2
import logging
import numpy as np
# ==========================================
# 0. PRODUCTION LOGGING & ENVIRONMENT SETUP
# ==========================================
# Set TensorFlow variables BEFORE importing TF to suppress warnings
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
os.environ['TF_ENABLE_ONEDNN_OPTS'] = '0'
import tensorflow as tf
from tensorflow import keras
from fastapi import FastAPI, UploadFile, File, HTTPException
from fastapi.middleware.cors import CORSMiddleware
# Configure production logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger("AiroDx-API")
# ==========================================
# 1. CONFIGURATION & PATHS
# ==========================================
# Use Environment Variables for paths (Allows Docker to change them without changing code)
MODEL_PATH = os.getenv("MODEL_PATH", "./model/remedis_multiview_final.keras")
REMEDIS_BASE_PATH = os.getenv("REMEDIS_BASE_PATH", "./model/cxr-50x1-remedis-m")
IMAGE_SIZE = (448, 448)
TARGET_DISEASES = [
'Atelectasis', 'Cardiomegaly', 'Consolidation', 'Emphysema', 'Hernia',
'Infiltrates', 'Mass', 'Nodule', 'Pleural Effusion', 'Pleural Thickening',
'Pneumonia', 'Pneumothorax', 'Pulmonary Edema', 'Pulmonary Fibrosis'
]
# ==========================================
# 2. REDEFINE CUSTOM LAYER
# ==========================================
@tf.keras.utils.register_keras_serializable()
class REMEDISBackbone(keras.layers.Layer):
def __init__(self, model_path=None, **kwargs):
# Accept legacy serialized model_path, but force the local deployment path.
super().__init__(**kwargs)
self.model_path = REMEDIS_BASE_PATH
self._remedis_model = None
def build(self, input_shape):
if self._remedis_model is None:
if not os.path.exists(self.model_path):
logger.error(f"Base REMEDIS model not found at {self.model_path}")
raise FileNotFoundError(f"Base REMEDIS model not found at {self.model_path}")
self._remedis_model = tf.saved_model.load(self.model_path)
super().build(input_shape)
def call(self, inputs, training=None):
return self._remedis_model(inputs)
def compute_output_shape(self, input_shape):
return (input_shape[0], 14, 14, 2048)
def get_config(self):
config = super().get_config()
return config
@classmethod
def from_config(cls, config):
# Remove stale serialized path (e.g., Kaggle) before layer construction.
config.pop("model_path", None)
return cls(**config)
# ==========================================
# 3. INITIALIZE APP & LOAD MODEL
# ==========================================
app = FastAPI(
title="AiroDx Multi-View Diagnostic API",
description="Multimodal diagnostic support using Siamese REMEDIS network.",
version="1.0.0"
)
# CORS setup for production (Can be restricted via ENV variable)
ALLOWED_ORIGINS = os.getenv("ALLOWED_ORIGINS", "*").split(",")
app.add_middleware(
CORSMiddleware,
allow_origins=ALLOWED_ORIGINS,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
logger.info(f"Loading weights from {MODEL_PATH}...")
try:
model = keras.models.load_model(
MODEL_PATH,
custom_objects={"REMEDISBackbone": REMEDISBackbone}
)
logger.info("Model loaded successfully into memory!")
except Exception as e:
logger.critical(f"Failed to load model: {e}")
model = None
# ==========================================
# 4. HEALTH CHECK ENDPOINT (For Cloud Hosting)
# ==========================================
@app.get("/health")
async def health_check():
"""Endpoint used by load balancers and Docker to ensure API is alive."""
if model is None:
raise HTTPException(status_code=503, detail="Model failed to load.")
return {"status": "healthy", "model_ready": True}
# ==========================================
# 5. PREPROCESSING FUNCTION
# ==========================================
async def process_image(file: UploadFile) -> np.ndarray:
try:
# Read the file bytes
contents = await file.read()
nparr = np.frombuffer(contents, np.uint8)
img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
if img is None:
raise ValueError(f"Invalid image file: {file.filename}")
# Exact same preprocessing used in training
img = cv2.resize(img, IMAGE_SIZE)
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
img = img.astype('float32') / 255.0
# Add batch dimension: (448, 448, 3) -> (1, 448, 448, 3)
return np.expand_dims(img, axis=0)
except Exception as e:
logger.error(f"Image processing error: {str(e)}")
raise HTTPException(status_code=400, detail=f"Error processing image: {str(e)}")
# ==========================================
# 6. PREDICTION ENDPOINT
# ==========================================
@app.post("/predict")
async def predict(
frontal_image: UploadFile = File(..., description="The PA/AP Frontal X-ray"),
lateral_image: UploadFile = File(..., description="The Lateral X-ray")
):
logger.info(f"Received prediction request for files: {frontal_image.filename}, {lateral_image.filename}")
# 1. Process both images
img_pa = await process_image(frontal_image)
img_l = await process_image(lateral_image)
# 2. Run inference
predictions = model.predict({
"pa_image": img_pa,
"l_image": img_l
})
# 3. Format results
probabilities = predictions[0].tolist()
results = []
for disease, prob in zip(TARGET_DISEASES, probabilities):
results.append({
"disease": disease,
"probability": round(prob, 4),
"flagged": bool(prob > 0.5) # You can adjust this threshold later based on your F1 tuning
})
# Sort results by probability (highest first)
results.sort(key=lambda x: x["probability"], reverse=True)
logger.info("Prediction processed successfully.")
return {
"status": "success",
"predictions": results
}