Files
HawkeyeVision/PythonModelAPI/PythonModelAPI.py
2025-08-28 16:31:25 +02:00

163 lines
5.4 KiB
Python

# app.py
from fastapi import FastAPI, Response, UploadFile, File, Body, HTTPException
from pydantic import BaseModel
from typing import List
from fastapi.responses import JSONResponse
import numpy as np
from PIL import Image
import io
import tensorflow as tf
import time
app = FastAPI()
# ==== Schemas (show up in Swagger) ====
class PredictOut(BaseModel):
result: List[int]
model_config = {"json_schema_extra": {"examples": [{"result": [0,1,2,3]}]}}
class StatusOut(BaseModel):
status: str
message: str
class AcceptSizeOut(BaseModel):
status: str
accept_size: List[int]
class LoadModelIn(BaseModel):
path: str
name: str
class ActivateModelIn(BaseModel):
name: str
# ==== Service ====
class Service:
def __init__(self):
self.model = None
self.models = {}
self.active_model = "default"
def activate_model(self, name):
self.model = self.models[name]
self.active_model = name
return 0
def load_model(self, path, name):
if name in self.models:
# Model already loaded, skip loading
return 0
self.models[name] = tf.keras.models.load_model(path)
return 0
def get_accept_size(self):
print(self.model.input_shape)
return list(self.model.input_shape) # (B,H,W,C)
def get_output_size(self):
return list(self.model.output_shape) # (B,H,W,C) or (B,H,W)
def predict(self, data: bytes) -> List[int]:
target_size = self.get_accept_size()
image = np.reshape(
np.frombuffer(data, dtype=np.uint8),
(target_size[1], target_size[2], target_size[3])
) / 255.0
predictions = self.model.predict(np.array([image]))
if predictions[0].ndim == 2:
classed = predictions[0]
else:
classed = tf.argmax(predictions[0], axis=2)
return classed.numpy().reshape(-1).tolist()
def predict_raw(self, data: bytes) -> np.ndarray:
target_size = self.get_accept_size()
image = np.reshape(
np.frombuffer(data, dtype=np.uint8),
(target_size[1], target_size[2], target_size[3])
) / 255.0
predictions = self.model({"input_layer": np.array([image], dtype=np.float32)}).numpy()
if predictions[0].ndim == 2:
classed = predictions[0][:, :, np.newaxis].clip(0, 255)
return classed.astype(np.uint8).reshape(-1).tolist()
else:
classed = predictions[0][:, :, :]
res = (classed * 255).astype(np.uint8).reshape(-1)
return np.asarray(res)
svc = Service()
def _prep_raw_bytes_for_predict(buf: bytes) -> bytes:
"""Decode JPEG/PNG -> RGB, resize to model size, return raw uint8 bytes HxWxC."""
h, w, c = svc.get_accept_size()[1:4]
img = Image.open(io.BytesIO(buf)).convert("RGB")
if img.size != (w, h):
img = img.resize((w, h), Image.BILINEAR)
arr = np.asarray(img, dtype=np.uint8) # HxWx3 uint8
return arr.tobytes()
# ==== Routes ====
@app.post("/predict", response_model=bytes, summary="Predict (classified indices)")
async def predict(file: UploadFile = File(...)):
try:
buf = await file.read()
raw = _prep_raw_bytes_for_predict(buf)
result = svc.predict(raw)
return Response(content=result, media_type="application/octet-stream")
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
@app.post("/predictRaw", response_model=bytes, summary="Predict (raw/uint8 flattened)")
async def predict_raw(file: UploadFile = File(...)):
try:
start_time = time.time()
buf = await file.read()
result = svc.predict_raw(buf)
elapsed_time = time.time() - start_time
print(f"Prediction took {elapsed_time:.2f} seconds")
return Response(content=result.tobytes(), media_type="application/octet-stream")
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
@app.post("/loadModel", response_model=StatusOut, summary="Load a model from path")
async def load_model(payload: LoadModelIn):
try:
svc.load_model(payload.path, payload.name)
return StatusOut(status="success", message=f"Model {payload.name} loaded successfully.")
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.post("/activateModel", response_model=StatusOut, summary="Activate a loaded model")
async def activate_model(payload: ActivateModelIn):
try:
svc.activate_model(payload.name)
return StatusOut(status="success", message=f"Model {payload.name} activated successfully.")
except KeyError:
raise HTTPException(status_code=404, detail=f"Model {payload.name} not found.")
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/getAcceptSize", response_model=AcceptSizeOut, summary="Get input tensor shape")
async def get_accept_size():
try:
size = svc.get_accept_size()[1:] # Exclude batch size
return AcceptSizeOut(status="success", accept_size=size)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/getOutputSize", response_model=AcceptSizeOut, summary="Get output tensor shape")
async def get_output_size():
try:
size = svc.get_output_size()[1:]
return AcceptSizeOut(status="success", accept_size=size)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))