Spaces:
Sleeping
Sleeping
File size: 4,409 Bytes
b34c4f2 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 | from __future__ import annotations
import io
from contextlib import asynccontextmanager
from pathlib import Path
import numpy as np
import onnxruntime as ort
from fastapi import FastAPI, File, HTTPException, UploadFile
from PIL import Image, UnidentifiedImageError
MODEL_PATH = Path(__file__).parent / "model" / "efficientnet_b0_realwaste.onnx"
CLASS_NAMES = (
"Cardboard",
"Food Organics",
"Glass",
"Metal",
"Miscellaneous Trash",
"Paper",
"Plastic",
"Textile Trash",
"Vegetation",
)
ALLOWED_CONTENT_TYPES = {"image/jpeg", "image/png", "image/webp"}
MAX_IMAGE_BYTES = 10 * 1024 * 1024
IMAGE_SIZE = (224, 224)
IMAGENET_MEAN = np.asarray([0.485, 0.456, 0.406], dtype=np.float32)
IMAGENET_STD = np.asarray([0.229, 0.224, 0.225], dtype=np.float32)
_session: ort.InferenceSession | None = None
_input_name = ""
_output_name = ""
def load_model() -> None:
global _session, _input_name, _output_name
if _session is not None:
return
if not MODEL_PATH.is_file():
raise RuntimeError(f"Model file not found: {MODEL_PATH}")
options = ort.SessionOptions()
options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
options.intra_op_num_threads = 2
_session = ort.InferenceSession(
str(MODEL_PATH),
sess_options=options,
providers=["CPUExecutionProvider"],
)
_input_name = _session.get_inputs()[0].name
_output_name = _session.get_outputs()[0].name
output_shape = _session.get_outputs()[0].shape
if output_shape[-1] != len(CLASS_NAMES):
raise RuntimeError(
f"Model returns {output_shape[-1]} classes; expected {len(CLASS_NAMES)}."
)
@asynccontextmanager
async def lifespan(_: FastAPI):
load_model()
yield
app = FastAPI(
title="Waste Classification API",
description="EfficientNet-B0 ONNX inference for the nine RealWaste classes.",
version="1.0.0",
lifespan=lifespan,
)
def preprocess_image(contents: bytes) -> np.ndarray:
try:
with Image.open(io.BytesIO(contents)) as image:
image.load()
rgb = image.convert("RGB")
except (UnidentifiedImageError, OSError, ValueError) as error:
raise ValueError("The uploaded file is not a valid image.") from error
resized = rgb.resize(IMAGE_SIZE, Image.Resampling.BILINEAR)
pixels = np.asarray(resized, dtype=np.float32) / 255.0
normalized = (pixels - IMAGENET_MEAN) / IMAGENET_STD
return np.transpose(normalized, (2, 0, 1))[np.newaxis, ...].astype(
np.float32,
copy=False,
)
def predict(contents: bytes) -> tuple[str, float, dict[str, float]]:
load_model()
assert _session is not None
tensor = preprocess_image(contents)
logits = np.asarray(
_session.run([_output_name], {_input_name: tensor})[0][0],
dtype=np.float64,
)
shifted = logits - np.max(logits)
probabilities = np.exp(shifted) / np.exp(shifted).sum()
predicted_index = int(np.argmax(probabilities))
all_probabilities = {
label: float(probabilities[index])
for index, label in enumerate(CLASS_NAMES)
}
return (
CLASS_NAMES[predicted_index],
float(probabilities[predicted_index]),
all_probabilities,
)
@app.get("/")
@app.get("/health")
def health() -> dict[str, object]:
load_model()
return {
"status": "ok",
"model": "EfficientNet-B0 ONNX",
"num_classes": len(CLASS_NAMES),
}
@app.post("/predict")
async def classify(file: UploadFile = File(...)) -> dict[str, object]:
if file.content_type not in ALLOWED_CONTENT_TYPES:
raise HTTPException(
status_code=415,
detail="Only JPEG, PNG, and WebP images are supported.",
)
contents = await file.read(MAX_IMAGE_BYTES + 1)
if not contents:
raise HTTPException(status_code=400, detail="The uploaded image is empty.")
if len(contents) > MAX_IMAGE_BYTES:
raise HTTPException(status_code=413, detail="The image exceeds the 10 MB limit.")
try:
predicted_class, confidence, all_probabilities = predict(contents)
except ValueError as error:
raise HTTPException(status_code=400, detail=str(error)) from error
return {
"predicted_class": predicted_class,
"confidence": confidence,
"all_probabilities": all_probabilities,
}
|