Keras
TF-Keras
ghafil commited on
Commit
14622e3
·
verified ·
1 Parent(s): 091d3fd

Upload 3 files

Browse files
Files changed (3) hide show
  1. Dockerfile +32 -0
  2. app.py +178 -0
  3. requirements.txt +44 -0
Dockerfile ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use official lightweight Python image
2
+ FROM python:3.11-slim
3
+
4
+ # Set environment variables
5
+ ENV PYTHONDONTWRITEBYTECODE=1
6
+ ENV PYTHONUNBUFFERED=1
7
+ ENV TF_CPP_MIN_LOG_LEVEL=2
8
+ ENV TF_ENABLE_ONEDNN_OPTS=0
9
+
10
+ # Create a working directory
11
+ WORKDIR /app
12
+
13
+ # Install system dependencies
14
+ RUN apt-get update && apt-get install -y --no-install-recommends \
15
+ build-essential \
16
+ && rm -rf /var/lib/apt/lists/*
17
+
18
+ # Install Python dependencies
19
+ COPY requirements.txt .
20
+ RUN pip install --no-cache-dir -r requirements.txt
21
+
22
+ # Copy the model files directly into the image (Since it's only ~92MB!)
23
+ COPY model/ ./model/
24
+
25
+ # Copy your application code
26
+ COPY app.py .
27
+
28
+ # Expose the port FastAPI runs on
29
+ EXPOSE 8000
30
+
31
+ # Command to run the application using uvicorn (Simpler for HuggingFace)
32
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]
app.py ADDED
@@ -0,0 +1,178 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import io
3
+ import cv2
4
+ import logging
5
+ import numpy as np
6
+
7
+ # ==========================================
8
+ # 0. PRODUCTION LOGGING & ENVIRONMENT SETUP
9
+ # ==========================================
10
+ # Set TensorFlow variables BEFORE importing TF to suppress warnings
11
+ os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
12
+ os.environ['TF_ENABLE_ONEDNN_OPTS'] = '0'
13
+
14
+ import tensorflow as tf
15
+ from tensorflow import keras
16
+ from fastapi import FastAPI, UploadFile, File, HTTPException
17
+ from fastapi.middleware.cors import CORSMiddleware
18
+
19
+ # Configure production logging
20
+ logging.basicConfig(
21
+ level=logging.INFO,
22
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
23
+ )
24
+ logger = logging.getLogger("AiroDx-API")
25
+
26
+ # ==========================================
27
+ # 1. CONFIGURATION & PATHS
28
+ # ==========================================
29
+ # Use Environment Variables for paths (Allows Docker to change them without changing code)
30
+ MODEL_PATH = os.getenv("MODEL_PATH", "./model/remedis_multiview_final.keras")
31
+ REMEDIS_BASE_PATH = os.getenv("REMEDIS_BASE_PATH", "./model/cxr-50x1-remedis-m")
32
+
33
+ IMAGE_SIZE = (448, 448)
34
+ TARGET_DISEASES = [
35
+ 'Atelectasis', 'Cardiomegaly', 'Consolidation', 'Emphysema', 'Hernia',
36
+ 'Infiltrates', 'Mass', 'Nodule', 'Pleural Effusion', 'Pleural Thickening',
37
+ 'Pneumonia', 'Pneumothorax', 'Pulmonary Edema', 'Pulmonary Fibrosis'
38
+ ]
39
+
40
+ # ==========================================
41
+ # 2. REDEFINE CUSTOM LAYER
42
+ # ==========================================
43
+ @tf.keras.utils.register_keras_serializable()
44
+ class REMEDISBackbone(keras.layers.Layer):
45
+ def __init__(self, model_path=None, **kwargs):
46
+ # Accept legacy serialized model_path, but force the local deployment path.
47
+ super().__init__(**kwargs)
48
+ self.model_path = REMEDIS_BASE_PATH
49
+ self._remedis_model = None
50
+
51
+ def build(self, input_shape):
52
+ if self._remedis_model is None:
53
+ if not os.path.exists(self.model_path):
54
+ logger.error(f"Base REMEDIS model not found at {self.model_path}")
55
+ raise FileNotFoundError(f"Base REMEDIS model not found at {self.model_path}")
56
+ self._remedis_model = tf.saved_model.load(self.model_path)
57
+ super().build(input_shape)
58
+
59
+ def call(self, inputs, training=None):
60
+ return self._remedis_model(inputs)
61
+
62
+ def compute_output_shape(self, input_shape):
63
+ return (input_shape[0], 14, 14, 2048)
64
+
65
+ def get_config(self):
66
+ config = super().get_config()
67
+ return config
68
+
69
+ @classmethod
70
+ def from_config(cls, config):
71
+ # Remove stale serialized path (e.g., Kaggle) before layer construction.
72
+ config.pop("model_path", None)
73
+ return cls(**config)
74
+
75
+ # ==========================================
76
+ # 3. INITIALIZE APP & LOAD MODEL
77
+ # ==========================================
78
+ app = FastAPI(
79
+ title="AiroDx Multi-View Diagnostic API",
80
+ description="Multimodal diagnostic support using Siamese REMEDIS network.",
81
+ version="1.0.0"
82
+ )
83
+
84
+ # CORS setup for production (Can be restricted via ENV variable)
85
+ ALLOWED_ORIGINS = os.getenv("ALLOWED_ORIGINS", "*").split(",")
86
+ app.add_middleware(
87
+ CORSMiddleware,
88
+ allow_origins=ALLOWED_ORIGINS,
89
+ allow_credentials=True,
90
+ allow_methods=["*"],
91
+ allow_headers=["*"],
92
+ )
93
+
94
+ logger.info(f"Loading weights from {MODEL_PATH}...")
95
+ try:
96
+ model = keras.models.load_model(
97
+ MODEL_PATH,
98
+ custom_objects={"REMEDISBackbone": REMEDISBackbone}
99
+ )
100
+ logger.info("Model loaded successfully into memory!")
101
+ except Exception as e:
102
+ logger.critical(f"Failed to load model: {e}")
103
+ model = None
104
+
105
+ # ==========================================
106
+ # 4. HEALTH CHECK ENDPOINT (For Cloud Hosting)
107
+ # ==========================================
108
+ @app.get("/health")
109
+ async def health_check():
110
+ """Endpoint used by load balancers and Docker to ensure API is alive."""
111
+ if model is None:
112
+ raise HTTPException(status_code=503, detail="Model failed to load.")
113
+ return {"status": "healthy", "model_ready": True}
114
+
115
+ # ==========================================
116
+ # 5. PREPROCESSING FUNCTION
117
+ # ==========================================
118
+ async def process_image(file: UploadFile) -> np.ndarray:
119
+ try:
120
+ # Read the file bytes
121
+ contents = await file.read()
122
+ nparr = np.frombuffer(contents, np.uint8)
123
+ img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
124
+
125
+ if img is None:
126
+ raise ValueError(f"Invalid image file: {file.filename}")
127
+
128
+ # Exact same preprocessing used in training
129
+ img = cv2.resize(img, IMAGE_SIZE)
130
+ img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
131
+ img = img.astype('float32') / 255.0
132
+
133
+ # Add batch dimension: (448, 448, 3) -> (1, 448, 448, 3)
134
+ return np.expand_dims(img, axis=0)
135
+ except Exception as e:
136
+ logger.error(f"Image processing error: {str(e)}")
137
+ raise HTTPException(status_code=400, detail=f"Error processing image: {str(e)}")
138
+
139
+ # ==========================================
140
+ # 6. PREDICTION ENDPOINT
141
+ # ==========================================
142
+ @app.post("/predict")
143
+ async def predict(
144
+ frontal_image: UploadFile = File(..., description="The PA/AP Frontal X-ray"),
145
+ lateral_image: UploadFile = File(..., description="The Lateral X-ray")
146
+ ):
147
+ logger.info(f"Received prediction request for files: {frontal_image.filename}, {lateral_image.filename}")
148
+
149
+ # 1. Process both images
150
+ img_pa = await process_image(frontal_image)
151
+ img_l = await process_image(lateral_image)
152
+
153
+ # 2. Run inference
154
+ predictions = model.predict({
155
+ "pa_image": img_pa,
156
+ "l_image": img_l
157
+ })
158
+
159
+ # 3. Format results
160
+ probabilities = predictions[0].tolist()
161
+
162
+ results = []
163
+ for disease, prob in zip(TARGET_DISEASES, probabilities):
164
+ results.append({
165
+ "disease": disease,
166
+ "probability": round(prob, 4),
167
+ "flagged": bool(prob > 0.5) # You can adjust this threshold later based on your F1 tuning
168
+ })
169
+
170
+ # Sort results by probability (highest first)
171
+ results.sort(key=lambda x: x["probability"], reverse=True)
172
+
173
+ logger.info("Prediction processed successfully.")
174
+
175
+ return {
176
+ "status": "success",
177
+ "predictions": results
178
+ }
requirements.txt ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ absl-py==2.4.0
2
+ annotated-doc==0.0.4
3
+ annotated-types==0.7.0
4
+ anyio==4.13.0
5
+ astunparse==1.6.3
6
+ certifi==2026.2.25
7
+ charset-normalizer==3.4.6
8
+ click==8.3.1
9
+ colorama==0.4.6
10
+ fastapi==0.135.2
11
+ flatbuffers==25.12.19
12
+ gast==0.7.0
13
+ google-pasta==0.2.0
14
+ grpcio==1.78.0
15
+ h11==0.16.0
16
+ h5py==3.14.0
17
+ idna==3.11
18
+ keras==3.13.2
19
+ libclang==18.1.1
20
+ markdown-it-py==4.0.0
21
+ mdurl==0.1.2
22
+ ml_dtypes==0.5.4
23
+ namex==0.1.0
24
+ numpy==2.4.3
25
+ opencv-python-headless==4.13.0.92
26
+ opt_einsum==3.4.0
27
+ optree==0.19.0
28
+ packaging==26.0
29
+ protobuf==7.34.1
30
+ pydantic==2.12.5
31
+ pydantic_core==2.41.5
32
+ Pygments==2.19.2
33
+ python-multipart==0.0.22
34
+ requests==2.33.0
35
+ rich==14.3.3
36
+ six==1.17.0
37
+ starlette==1.0.0
38
+ tensorflow==2.21.0
39
+ termcolor==3.3.0
40
+ typing-inspection==0.4.2
41
+ typing_extensions==4.15.0
42
+ urllib3==2.6.3
43
+ uvicorn==0.42.0
44
+ wrapt==2.1.2