Deeplearning_Board / backend /app /services /classifier_service.py
eunzzang's picture
Fix learning route build and restore word learning layout
11ab88a verified
Raw
History Blame Contribute Delete
5.46 kB
from io import BytesIO
from pathlib import Path
from time import perf_counter
from fastapi import HTTPException, UploadFile
BASE_DIR = Path(__file__).resolve().parents[3]
MODEL_DIR = BASE_DIR / "ai" / "model"
MODEL_PATH = MODEL_DIR / "keras_model.h5"
LABELS_PATH = MODEL_DIR / "labels.txt"
MAX_FILE_SIZE = 10 * 1024 * 1024
ALLOWED_CONTENT_TYPES = {"image/jpeg", "image/png"}
_model = None
_labels: list[str] | None = None
def _load_labels() -> list[str]:
if not LABELS_PATH.exists():
raise HTTPException(status_code=500, detail="labels.txt file was not found.")
labels: list[str] = []
for line in LABELS_PATH.read_text(encoding="utf-8").splitlines():
label = line.strip()
if not label:
continue
parts = label.split(maxsplit=1)
labels.append(parts[1] if len(parts) == 2 and parts[0].isdigit() else label)
if not labels:
raise HTTPException(status_code=500, detail="labels.txt does not contain classes.")
return labels
def _load_model():
global _model
if _model is not None:
return _model
if not MODEL_PATH.exists():
raise HTTPException(status_code=500, detail="keras_model.h5 file was not found.")
try:
from tf_keras.layers import DepthwiseConv2D
from tf_keras.models import load_model
except ImportError:
try:
from tensorflow.keras.layers import DepthwiseConv2D
from tensorflow.keras.models import load_model
except ImportError as exc:
raise HTTPException(
status_code=500,
detail="TensorFlow is not installed. Run pip install -r requirements.txt.",
) from exc
except Exception as exc:
raise HTTPException(
status_code=500,
detail=f"TensorFlow loader import failed: {exc}",
) from exc
class TeachableMachineDepthwiseConv2D(DepthwiseConv2D):
@classmethod
def from_config(cls, config):
config.pop("groups", None)
return super().from_config(config)
try:
_model = load_model(
MODEL_PATH,
compile=False,
custom_objects={"DepthwiseConv2D": TeachableMachineDepthwiseConv2D},
)
except Exception as exc:
raise HTTPException(status_code=500, detail=f"Model load failed: {exc}") from exc
return _model
def _get_labels() -> list[str]:
global _labels
if _labels is None:
_labels = _load_labels()
return _labels
def get_labels() -> list[str]:
return _get_labels()
def _model_input_size(model) -> tuple[int, int]:
input_shape = model.input_shape
if isinstance(input_shape, list):
input_shape = input_shape[0]
height = input_shape[1] or 224
width = input_shape[2] or 224
return int(width), int(height)
def _preprocess_image(image_bytes: bytes, size: tuple[int, int]):
try:
import numpy as np
from PIL import Image, ImageOps
except ImportError as exc:
raise HTTPException(
status_code=500,
detail="Image inference packages are missing. Run pip install -r requirements.txt.",
) from exc
try:
image = Image.open(BytesIO(image_bytes)).convert("RGB")
except Exception as exc:
raise HTTPException(status_code=400, detail="Uploaded file is not a readable image.") from exc
image = ImageOps.fit(image, size, method=Image.Resampling.LANCZOS)
image_array = np.asarray(image, dtype=np.float32)
normalized_image_array = (image_array / 127.5) - 1
return np.expand_dims(normalized_image_array, axis=0)
async def predict_image(file: UploadFile) -> dict:
if file.content_type not in ALLOWED_CONTENT_TYPES:
raise HTTPException(status_code=400, detail="Only jpg, jpeg, and png images are allowed.")
image_bytes = await file.read()
if not image_bytes:
raise HTTPException(status_code=400, detail="Uploaded file is empty.")
if len(image_bytes) > MAX_FILE_SIZE:
raise HTTPException(status_code=413, detail="Image files must be 10MB or smaller.")
model = _load_model()
labels = _get_labels()
model_input = _preprocess_image(image_bytes, _model_input_size(model))
started_at = perf_counter()
try:
predictions = model.predict(model_input, verbose=0)[0]
except Exception as exc:
raise HTTPException(status_code=500, detail=f"Model inference failed: {exc}") from exc
inference_time_ms = round((perf_counter() - started_at) * 1000)
scores = predictions.tolist()
ranked = sorted(enumerate(scores), key=lambda item: item[1], reverse=True)
best_index, best_score = ranked[0]
def label_at(index: int) -> str:
return labels[index] if index < len(labels) else f"class_{index}"
return {
"success": True,
"filename": file.filename,
"predicted_class": label_at(best_index),
"confidence": float(best_score),
"top_k": [
{"label": label_at(index), "score": float(score)}
for index, score in ranked[: min(5, len(ranked))]
],
"model": {
"name": MODEL_PATH.name,
"version": "v1",
},
"inference_time_ms": inference_time_ms,
}