| """REST API детекции дефектов окраски кузова (по ТЗ АвтоВАЗа, таблица 3). |
| |
| Эндпоинты: |
| POST /predict — приём фото детали (multipart), VIN — параметром формы; |
| возвращает JSON с дефектами, координатами и base64-визуализацией. |
| GET /defects/{vin} — последние результаты по VIN (in-memory история). |
| GET /health — проверка состояния сервиса. |
| |
| Запуск: |
| uvicorn src.api:app --host 0.0.0.0 --port 8080 |
| """ |
| from __future__ import annotations |
| import base64 |
| import io |
| import time |
| from collections import defaultdict, deque |
| from datetime import datetime |
| from typing import Any |
|
|
| import cv2 |
| import numpy as np |
| import torch |
| from fastapi import FastAPI, File, Form, HTTPException, UploadFile |
| from fastapi.responses import FileResponse, JSONResponse |
| from fastapi.staticfiles import StaticFiles |
| from pydantic import BaseModel, Field |
|
|
| from . import config as C |
| from .infer import load_model, predict_image, render_visualization |
|
|
|
|
| app = FastAPI( |
| title="Paint Defect Detection API", |
| version="1.0.0", |
| description="Система автоматической детекции дефектов лакокрасочного покрытия " |
| "(крыша, капот, багажник). Соответствует требованиям ТЗ АвтоВАЗ.", |
| ) |
|
|
| _device = torch.device("cuda" if torch.cuda.is_available() else "cpu") |
| _model = None |
| _history: dict[str, deque] = defaultdict(lambda: deque(maxlen=20)) |
|
|
| _STATIC_DIR = C.ROOT / "static" |
| if _STATIC_DIR.exists(): |
| app.mount("/static", StaticFiles(directory=str(_STATIC_DIR)), name="static") |
|
|
|
|
| @app.get("/", include_in_schema=False) |
| def index(): |
| """Веб-интерфейс оператора (одностраничное приложение).""" |
| idx = _STATIC_DIR / "index.html" |
| if idx.exists(): |
| return FileResponse(str(idx)) |
| raise HTTPException(status_code=404, detail="UI not built") |
|
|
|
|
| def _ensure_model(): |
| global _model |
| if _model is None: |
| _model = load_model(device=_device) |
| return _model |
|
|
|
|
| class DefectBox(BaseModel): |
| x: int; y: int; w: int; h: int |
| confidence: float |
| mean_prob: float |
|
|
|
|
| class PredictResponse(BaseModel): |
| vin: str |
| timestamp: str |
| is_defect: bool |
| defect_count: int |
| defect_ratio: float |
| max_prob: float |
| boxes: list[DefectBox] |
| panel_size: dict[str, int] |
| visualization_base64: str = Field(description="JPEG, base64-encoded, для отображения на ТВ-панели") |
| elapsed_ms: int |
|
|
|
|
| @app.get("/health") |
| def health() -> dict[str, Any]: |
| return { |
| "status": "ok", |
| "device": str(_device), |
| "model_loaded": _model is not None, |
| "checkpoint": str(C.CHECKPOINTS / "best.pt"), |
| } |
|
|
|
|
| @app.post("/predict", response_model=PredictResponse) |
| async def predict( |
| file: UploadFile = File(..., description="Фото детали кузова"), |
| vin: str = Form(..., description="VIN автомобиля"), |
| part: str = Form("unknown", description="Деталь: roof|hood|trunk"), |
| threshold: float = Form(C.DEFECT_THRESHOLD), |
| ) -> PredictResponse: |
| if not file.content_type or not file.content_type.startswith("image/"): |
| raise HTTPException(status_code=400, detail="Ожидался image/*") |
| raw = await file.read() |
| arr = np.frombuffer(raw, dtype=np.uint8) |
| bgr = cv2.imdecode(arr, cv2.IMREAD_COLOR) |
| if bgr is None: |
| raise HTTPException(status_code=400, detail="Не удалось декодировать изображение") |
|
|
| model = _ensure_model() |
| t0 = time.time() |
| result = predict_image(bgr, model, _device, threshold=threshold) |
| elapsed_ms = int((time.time() - t0) * 1000) |
|
|
| vis = render_visualization(result) |
| ok, buf = cv2.imencode(".jpg", vis, [cv2.IMWRITE_JPEG_QUALITY, 88]) |
| vis_b64 = base64.b64encode(buf.tobytes()).decode("ascii") if ok else "" |
|
|
| response = PredictResponse( |
| vin=vin, |
| timestamp=datetime.utcnow().isoformat() + "Z", |
| is_defect=result["is_defect"], |
| defect_count=len(result["boxes"]), |
| defect_ratio=result["defect_ratio"], |
| max_prob=result["max_prob"], |
| boxes=[DefectBox(**b) for b in result["boxes"]], |
| panel_size=result["panel_size"], |
| visualization_base64=vis_b64, |
| elapsed_ms=elapsed_ms, |
| ) |
| _history[vin].append({"part": part, "ts": response.timestamp, |
| "is_defect": response.is_defect, |
| "defect_count": response.defect_count}) |
| return response |
|
|
|
|
| @app.get("/defects/{vin}") |
| def defects_by_vin(vin: str) -> dict[str, Any]: |
| return {"vin": vin, "results": list(_history.get(vin, []))} |
|
|
|
|
| def main(): |
| import uvicorn |
| uvicorn.run("src.api:app", host=C.API_HOST, port=C.API_PORT, reload=False) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|