Spaces:
Sleeping
Sleeping
model dir changed
Browse files- app.py +36 -31
- src/cnnClassifier/pipeline/prediction.py +22 -14
app.py
CHANGED
|
@@ -1,18 +1,26 @@
|
|
| 1 |
# --- IMPORTS ---
|
| 2 |
import os
|
|
|
|
|
|
|
|
|
|
| 3 |
from fastapi import FastAPI, Request
|
| 4 |
from fastapi.responses import HTMLResponse
|
| 5 |
from fastapi.staticfiles import StaticFiles
|
| 6 |
from fastapi.templating import Jinja2Templates
|
| 7 |
from fastapi.middleware.cors import CORSMiddleware
|
| 8 |
from pydantic import BaseModel
|
| 9 |
-
import uvicorn
|
| 10 |
-
|
| 11 |
-
|
|
|
|
| 12 |
|
|
|
|
|
|
|
|
|
|
| 13 |
sys.path.append(str(Path(__file__).parent / "src"))
|
|
|
|
| 14 |
|
| 15 |
-
#
|
| 16 |
from cnnClassifier.utils.common import decodeImage
|
| 17 |
from cnnClassifier.pipeline.prediction import PredictionPipeline
|
| 18 |
|
|
@@ -26,8 +34,14 @@ app = FastAPI(
|
|
| 26 |
description="An API to predict whether a chest CT scan shows signs of adenocarcinoma cancer."
|
| 27 |
)
|
| 28 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
# --- MIDDLEWARE (for CORS) ---
|
| 30 |
-
# This
|
| 31 |
app.add_middleware(
|
| 32 |
CORSMiddleware,
|
| 33 |
allow_origins=["*"],
|
|
@@ -37,39 +51,25 @@ app.add_middleware(
|
|
| 37 |
)
|
| 38 |
|
| 39 |
# --- MOUNT STATIC FILES AND TEMPLATES ---
|
| 40 |
-
# This is how FastAPI serves your CSS, JS, and HTML files
|
| 41 |
app.mount("/static", StaticFiles(directory="static"), name="static")
|
| 42 |
templates = Jinja2Templates(directory="templates")
|
| 43 |
|
| 44 |
-
|
| 45 |
# --- LOAD THE PREDICTION PIPELINE ON STARTUP ---
|
| 46 |
-
# This ensures the model is loaded only once when the application starts.
|
| 47 |
classifier = PredictionPipeline(filename="inputImage.jpg")
|
| 48 |
|
| 49 |
-
|
| 50 |
# --- DEFINE THE REQUEST BODY STRUCTURE ---
|
| 51 |
-
# Pydantic model for automatic validation of the incoming JSON
|
| 52 |
class ImagePayload(BaseModel):
|
| 53 |
image: str
|
| 54 |
|
| 55 |
-
|
| 56 |
# --- API ENDPOINTS ---
|
| 57 |
-
|
| 58 |
@app.get("/", response_class=HTMLResponse)
|
| 59 |
async def home(request: Request):
|
| 60 |
-
"""
|
| 61 |
-
Renders the main user interface (index.html).
|
| 62 |
-
"""
|
| 63 |
return templates.TemplateResponse("index.html", {"request": request})
|
| 64 |
|
| 65 |
-
|
| 66 |
@app.post("/train")
|
| 67 |
async def trainRoute():
|
| 68 |
-
"""
|
| 69 |
-
Triggers the DVC pipeline to retrain the model.
|
| 70 |
-
NOTE: This is a blocking operation and not recommended for a real-world, high-traffic production server.
|
| 71 |
-
It's suitable for this project's demonstration purposes.
|
| 72 |
-
"""
|
| 73 |
os.system("dvc repro")
|
| 74 |
return {"message": "Training done successfully!"}
|
| 75 |
|
|
@@ -77,26 +77,31 @@ async def trainRoute():
|
|
| 77 |
@app.post("/predict")
|
| 78 |
async def predictRoute(payload: ImagePayload):
|
| 79 |
"""
|
| 80 |
-
Accepts a base64 encoded image, saves it
|
|
|
|
| 81 |
"""
|
| 82 |
-
#
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
prediction_value = classifier.predict()
|
|
|
|
| 87 |
|
| 88 |
# 3. Translate the numeric prediction into a human-readable string
|
| 89 |
-
# Based on your confirmed class indices: {'adenocarcinoma': 0, 'normal': 1}
|
| 90 |
if prediction_value == 1:
|
| 91 |
prediction_text = "Normal"
|
| 92 |
-
else:
|
| 93 |
prediction_text = "Cancer"
|
| 94 |
|
| 95 |
-
# 4. Return the result
|
| 96 |
return [{"prediction": prediction_text}]
|
| 97 |
|
| 98 |
-
|
| 99 |
# --- RUN THE APP ---
|
| 100 |
-
# This block is for local development. Gunicorn/Uvicorn will run the app in production.
|
| 101 |
if __name__ == "__main__":
|
|
|
|
| 102 |
uvicorn.run(app, host="0.0.0.0", port=7860)
|
|
|
|
| 1 |
# --- IMPORTS ---
|
| 2 |
import os
|
| 3 |
+
import sys
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
|
| 6 |
from fastapi import FastAPI, Request
|
| 7 |
from fastapi.responses import HTMLResponse
|
| 8 |
from fastapi.staticfiles import StaticFiles
|
| 9 |
from fastapi.templating import Jinja2Templates
|
| 10 |
from fastapi.middleware.cors import CORSMiddleware
|
| 11 |
from pydantic import BaseModel
|
| 12 |
+
import uvicorn
|
| 13 |
+
|
| 14 |
+
# NEW, CRITICAL IMPORT for running behind a proxy
|
| 15 |
+
from uvicorn.middleware.proxy_headers import ProxyHeadersMiddleware
|
| 16 |
|
| 17 |
+
# --- ADD THIS BLOCK TO FIX 'ModuleNotFoundError' ---
|
| 18 |
+
# This adds the 'src' directory to the Python path
|
| 19 |
+
# so it can find the cnnClassifier package in the Docker container.
|
| 20 |
sys.path.append(str(Path(__file__).parent / "src"))
|
| 21 |
+
# ----------------------------------------------------
|
| 22 |
|
| 23 |
+
# Now we can import your custom ML components
|
| 24 |
from cnnClassifier.utils.common import decodeImage
|
| 25 |
from cnnClassifier.pipeline.prediction import PredictionPipeline
|
| 26 |
|
|
|
|
| 34 |
description="An API to predict whether a chest CT scan shows signs of adenocarcinoma cancer."
|
| 35 |
)
|
| 36 |
|
| 37 |
+
# --- ADD PROXY MIDDLEWARE (FIXES HTTPS/MIXED CONTENT ERROR) ---
|
| 38 |
+
# This middleware is essential for running behind a reverse proxy like Hugging Face Spaces.
|
| 39 |
+
# It tells the app to trust the 'x-forwarded-proto' header from the proxy.
|
| 40 |
+
app.add_middleware(ProxyHeadersMiddleware, trusted_hosts="*")
|
| 41 |
+
# ------------------------------------------------------------
|
| 42 |
+
|
| 43 |
# --- MIDDLEWARE (for CORS) ---
|
| 44 |
+
# This should come AFTER the ProxyHeadersMiddleware
|
| 45 |
app.add_middleware(
|
| 46 |
CORSMiddleware,
|
| 47 |
allow_origins=["*"],
|
|
|
|
| 51 |
)
|
| 52 |
|
| 53 |
# --- MOUNT STATIC FILES AND TEMPLATES ---
|
|
|
|
| 54 |
app.mount("/static", StaticFiles(directory="static"), name="static")
|
| 55 |
templates = Jinja2Templates(directory="templates")
|
| 56 |
|
|
|
|
| 57 |
# --- LOAD THE PREDICTION PIPELINE ON STARTUP ---
|
|
|
|
| 58 |
classifier = PredictionPipeline(filename="inputImage.jpg")
|
| 59 |
|
|
|
|
| 60 |
# --- DEFINE THE REQUEST BODY STRUCTURE ---
|
|
|
|
| 61 |
class ImagePayload(BaseModel):
|
| 62 |
image: str
|
| 63 |
|
|
|
|
| 64 |
# --- API ENDPOINTS ---
|
|
|
|
| 65 |
@app.get("/", response_class=HTMLResponse)
|
| 66 |
async def home(request: Request):
|
| 67 |
+
"""Renders the main user interface (index.html)."""
|
|
|
|
|
|
|
| 68 |
return templates.TemplateResponse("index.html", {"request": request})
|
| 69 |
|
|
|
|
| 70 |
@app.post("/train")
|
| 71 |
async def trainRoute():
|
| 72 |
+
"""Triggers the DVC pipeline to retrain the model."""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
os.system("dvc repro")
|
| 74 |
return {"message": "Training done successfully!"}
|
| 75 |
|
|
|
|
| 77 |
@app.post("/predict")
|
| 78 |
async def predictRoute(payload: ImagePayload):
|
| 79 |
"""
|
| 80 |
+
Accepts a base64 encoded image, saves it to a temporary location,
|
| 81 |
+
runs prediction, and returns the result.
|
| 82 |
"""
|
| 83 |
+
# --- THIS IS THE FIX ---
|
| 84 |
+
# Define a writable filename inside the /tmp directory.
|
| 85 |
+
temp_image_path = "/tmp/inputImage.jpg"
|
| 86 |
+
|
| 87 |
+
# 1. Decode the image and save it to the temporary path
|
| 88 |
+
decodeImage(payload.image, temp_image_path)
|
| 89 |
+
|
| 90 |
+
# 2. Update the classifier's filename to the new temporary path and predict
|
| 91 |
+
classifier.filename = temp_image_path
|
| 92 |
prediction_value = classifier.predict()
|
| 93 |
+
# ----------------------
|
| 94 |
|
| 95 |
# 3. Translate the numeric prediction into a human-readable string
|
|
|
|
| 96 |
if prediction_value == 1:
|
| 97 |
prediction_text = "Normal"
|
| 98 |
+
else:
|
| 99 |
prediction_text = "Cancer"
|
| 100 |
|
| 101 |
+
# 4. Return the result
|
| 102 |
return [{"prediction": prediction_text}]
|
| 103 |
|
|
|
|
| 104 |
# --- RUN THE APP ---
|
|
|
|
| 105 |
if __name__ == "__main__":
|
| 106 |
+
# Note: Hugging Face uses port 7860 by default for its apps
|
| 107 |
uvicorn.run(app, host="0.0.0.0", port=7860)
|
src/cnnClassifier/pipeline/prediction.py
CHANGED
|
@@ -5,32 +5,40 @@ import os
|
|
| 5 |
|
| 6 |
class PredictionPipeline:
|
| 7 |
def __init__(self, filename):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
self.filename = filename
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
def predict(self):
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
# --- Load and preprocess the image ---
|
| 17 |
imagename = self.filename
|
| 18 |
test_image = image.load_img(imagename, target_size=(224, 224))
|
| 19 |
test_image_array = image.img_to_array(test_image)
|
| 20 |
|
| 21 |
-
# --- FIX #2: THE CRITICAL RESCALING STEP ---
|
| 22 |
# Scale the pixel values to be between 0 and 1, just like the training data.
|
| 23 |
scaled_image_array = test_image_array / 255.0
|
| 24 |
|
| 25 |
# Add the batch dimension
|
| 26 |
input_data = np.expand_dims(scaled_image_array, axis=0)
|
| 27 |
|
| 28 |
-
#
|
| 29 |
-
|
| 30 |
-
|
| 31 |
|
| 32 |
-
#
|
| 33 |
-
# The logic for translation should be in app.py to keep this pipeline clean,
|
| 34 |
-
# but for now, we will just return the raw index.
|
| 35 |
-
# app.py will handle translating 0/1 to "Cancer"/"Normal".
|
| 36 |
return result_index
|
|
|
|
| 5 |
|
| 6 |
class PredictionPipeline:
|
| 7 |
def __init__(self, filename):
|
| 8 |
+
"""
|
| 9 |
+
Initializes the prediction pipeline.
|
| 10 |
+
|
| 11 |
+
This is where we load the model ONCE when the application starts.
|
| 12 |
+
This is much more efficient than loading it for every prediction.
|
| 13 |
+
"""
|
| 14 |
self.filename = filename
|
| 15 |
+
|
| 16 |
+
# --- THIS IS THE FIX ---
|
| 17 |
+
# The Dockerfile places the model in a 'model' directory.
|
| 18 |
+
# This is the correct path inside the container.
|
| 19 |
+
model_path = os.path.join("model", "best_model.h5")
|
| 20 |
+
self.model = tf.keras.models.load_model(model_path)
|
| 21 |
+
# ----------------------
|
| 22 |
|
| 23 |
def predict(self):
|
| 24 |
+
"""
|
| 25 |
+
Performs the prediction on the image file.
|
| 26 |
+
It uses the model that was already loaded in the constructor.
|
| 27 |
+
"""
|
| 28 |
+
# Load and preprocess the image
|
|
|
|
| 29 |
imagename = self.filename
|
| 30 |
test_image = image.load_img(imagename, target_size=(224, 224))
|
| 31 |
test_image_array = image.img_to_array(test_image)
|
| 32 |
|
|
|
|
| 33 |
# Scale the pixel values to be between 0 and 1, just like the training data.
|
| 34 |
scaled_image_array = test_image_array / 255.0
|
| 35 |
|
| 36 |
# Add the batch dimension
|
| 37 |
input_data = np.expand_dims(scaled_image_array, axis=0)
|
| 38 |
|
| 39 |
+
# Make the prediction using the pre-loaded model
|
| 40 |
+
prediction_probs = self.model.predict(input_data)
|
| 41 |
+
result_index = np.argmax(prediction_probs, axis=1)
|
| 42 |
|
| 43 |
+
# Return the raw index (e.g., [0] or [1])
|
|
|
|
|
|
|
|
|
|
| 44 |
return result_index
|