therealestcoder commited on
Commit
a30f9a1
·
verified ·
1 Parent(s): 927f4cc

Upload src\api.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. src//api.py +146 -0
src//api.py ADDED
@@ -0,0 +1,146 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """REST API детекции дефектов окраски кузова (по ТЗ АвтоВАЗа, таблица 3).
2
+
3
+ Эндпоинты:
4
+ POST /predict — приём фото детали (multipart), VIN — параметром формы;
5
+ возвращает JSON с дефектами, координатами и base64-визуализацией.
6
+ GET /defects/{vin} — последние результаты по VIN (in-memory история).
7
+ GET /health — проверка состояния сервиса.
8
+
9
+ Запуск:
10
+ uvicorn src.api:app --host 0.0.0.0 --port 8080
11
+ """
12
+ from __future__ import annotations
13
+ import base64
14
+ import io
15
+ import time
16
+ from collections import defaultdict, deque
17
+ from datetime import datetime
18
+ from typing import Any
19
+
20
+ import cv2
21
+ import numpy as np
22
+ import torch
23
+ from fastapi import FastAPI, File, Form, HTTPException, UploadFile
24
+ from fastapi.responses import FileResponse, JSONResponse
25
+ from fastapi.staticfiles import StaticFiles
26
+ from pydantic import BaseModel, Field
27
+
28
+ from . import config as C
29
+ from .infer import load_model, predict_image, render_visualization
30
+
31
+
32
+ app = FastAPI(
33
+ title="Paint Defect Detection API",
34
+ version="1.0.0",
35
+ description="Система автоматической детекции дефектов лакокрасочного покрытия "
36
+ "(крыша, капот, багажник). Соответствует требованиям ТЗ АвтоВАЗ.",
37
+ )
38
+
39
+ _device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
40
+ _model = None # ленивая загрузка
41
+ _history: dict[str, deque] = defaultdict(lambda: deque(maxlen=20))
42
+
43
+ _STATIC_DIR = C.ROOT / "static"
44
+ if _STATIC_DIR.exists():
45
+ app.mount("/static", StaticFiles(directory=str(_STATIC_DIR)), name="static")
46
+
47
+
48
+ @app.get("/", include_in_schema=False)
49
+ def index():
50
+ """Веб-интерфейс оператора (одностраничное приложение)."""
51
+ idx = _STATIC_DIR / "index.html"
52
+ if idx.exists():
53
+ return FileResponse(str(idx))
54
+ raise HTTPException(status_code=404, detail="UI not built")
55
+
56
+
57
+ def _ensure_model():
58
+ global _model
59
+ if _model is None:
60
+ _model = load_model(device=_device)
61
+ return _model
62
+
63
+
64
+ class DefectBox(BaseModel):
65
+ x: int; y: int; w: int; h: int
66
+ confidence: float
67
+ mean_prob: float
68
+
69
+
70
+ class PredictResponse(BaseModel):
71
+ vin: str
72
+ timestamp: str
73
+ is_defect: bool
74
+ defect_count: int
75
+ defect_ratio: float
76
+ max_prob: float
77
+ boxes: list[DefectBox]
78
+ panel_size: dict[str, int]
79
+ visualization_base64: str = Field(description="JPEG, base64-encoded, для отображения на ТВ-панели")
80
+ elapsed_ms: int
81
+
82
+
83
+ @app.get("/health")
84
+ def health() -> dict[str, Any]:
85
+ return {
86
+ "status": "ok",
87
+ "device": str(_device),
88
+ "model_loaded": _model is not None,
89
+ "checkpoint": str(C.CHECKPOINTS / "best.pt"),
90
+ }
91
+
92
+
93
+ @app.post("/predict", response_model=PredictResponse)
94
+ async def predict(
95
+ file: UploadFile = File(..., description="Фото детали кузова"),
96
+ vin: str = Form(..., description="VIN автомобиля"),
97
+ part: str = Form("unknown", description="Деталь: roof|hood|trunk"),
98
+ threshold: float = Form(C.DEFECT_THRESHOLD),
99
+ ) -> PredictResponse:
100
+ if not file.content_type or not file.content_type.startswith("image/"):
101
+ raise HTTPException(status_code=400, detail="Ожидался image/*")
102
+ raw = await file.read()
103
+ arr = np.frombuffer(raw, dtype=np.uint8)
104
+ bgr = cv2.imdecode(arr, cv2.IMREAD_COLOR)
105
+ if bgr is None:
106
+ raise HTTPException(status_code=400, detail="Не удалось декодировать изображение")
107
+
108
+ model = _ensure_model()
109
+ t0 = time.time()
110
+ result = predict_image(bgr, model, _device, threshold=threshold)
111
+ elapsed_ms = int((time.time() - t0) * 1000)
112
+
113
+ vis = render_visualization(result)
114
+ ok, buf = cv2.imencode(".jpg", vis, [cv2.IMWRITE_JPEG_QUALITY, 88])
115
+ vis_b64 = base64.b64encode(buf.tobytes()).decode("ascii") if ok else ""
116
+
117
+ response = PredictResponse(
118
+ vin=vin,
119
+ timestamp=datetime.utcnow().isoformat() + "Z",
120
+ is_defect=result["is_defect"],
121
+ defect_count=len(result["boxes"]),
122
+ defect_ratio=result["defect_ratio"],
123
+ max_prob=result["max_prob"],
124
+ boxes=[DefectBox(**b) for b in result["boxes"]],
125
+ panel_size=result["panel_size"],
126
+ visualization_base64=vis_b64,
127
+ elapsed_ms=elapsed_ms,
128
+ )
129
+ _history[vin].append({"part": part, "ts": response.timestamp,
130
+ "is_defect": response.is_defect,
131
+ "defect_count": response.defect_count})
132
+ return response
133
+
134
+
135
+ @app.get("/defects/{vin}")
136
+ def defects_by_vin(vin: str) -> dict[str, Any]:
137
+ return {"vin": vin, "results": list(_history.get(vin, []))}
138
+
139
+
140
+ def main():
141
+ import uvicorn
142
+ uvicorn.run("src.api:app", host=C.API_HOST, port=C.API_PORT, reload=False)
143
+
144
+
145
+ if __name__ == "__main__":
146
+ main()