Spaces:
Sleeping
Sleeping
Upload 61 files
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .gitattributes +64 -0
- Dockerfile +24 -0
- README.md +24 -0
- app/__pycache__/main.cpython-310.pyc +0 -0
- app/main.py +58 -0
- app/routers/__pycache__/inference.cpython-310.pyc +0 -0
- app/routers/inference.py +218 -0
- app/static/css/style.css +140 -0
- app/static/img/logo.png +0 -0
- app/static/results/1d91cb91-46f0-4123-9b3d-dd963a4a1b8b_damage.png +3 -0
- app/static/results/1d91cb91-46f0-4123-9b3d-dd963a4a1b8b_parts.png +3 -0
- app/static/results/2b9a2716-12dc-4fec-8b1b-8a26375e4fd9_damage.png +3 -0
- app/static/results/2b9a2716-12dc-4fec-8b1b-8a26375e4fd9_parts.png +3 -0
- app/static/results/4ea85461-64cd-4c8b-8ef9-3c9635df076b_damage.png +3 -0
- app/static/results/4ea85461-64cd-4c8b-8ef9-3c9635df076b_parts.png +3 -0
- app/static/results/4ea85461-64cd-4c8b-8ef9-3c9635df076b_result.json +86 -0
- app/static/results/706f091d-f713-4f34-88b1-b3eaa5082483_damage.png +3 -0
- app/static/results/706f091d-f713-4f34-88b1-b3eaa5082483_parts.png +3 -0
- app/static/results/86665944-f463-4be3-87be-2227b667ea4d_damage.png +3 -0
- app/static/results/86665944-f463-4be3-87be-2227b667ea4d_parts.png +3 -0
- app/static/results/8f2e9c10-9e39-415b-b634-e9cacff08670_damage.png +3 -0
- app/static/results/8f2e9c10-9e39-415b-b634-e9cacff08670_parts.png +3 -0
- app/static/results/9be81a29-214a-48af-ba99-8cab54a56d66_damage.png +3 -0
- app/static/results/9be81a29-214a-48af-ba99-8cab54a56d66_parts.png +3 -0
- app/static/results/9be81a29-214a-48af-ba99-8cab54a56d66_result.json +83 -0
- app/static/results/db93084e-16d1-4448-b841-1ce1c0e1c201_damage.png +3 -0
- app/static/results/db93084e-16d1-4448-b841-1ce1c0e1c201_parts.png +3 -0
- app/static/results/de9bda87-c237-4c6b-8748-1ab245d4ec57_damage.png +3 -0
- app/static/results/de9bda87-c237-4c6b-8748-1ab245d4ec57_parts.png +3 -0
- app/static/results/f0a7c31c-d1cf-460a-abff-d32d76faa384_damage.png +3 -0
- app/static/results/f0a7c31c-d1cf-460a-abff-d32d76faa384_parts.png +3 -0
- app/static/results/fdfef36b-2942-4bc8-bb26-ad6aad32d471_parts.png +3 -0
- app/static/uploads/1d91cb91-46f0-4123-9b3d-dd963a4a1b8b.jpg +3 -0
- app/static/uploads/2b9a2716-12dc-4fec-8b1b-8a26375e4fd9.jpg +3 -0
- app/static/uploads/4ea85461-64cd-4c8b-8ef9-3c9635df076b.jpg +3 -0
- app/static/uploads/706f091d-f713-4f34-88b1-b3eaa5082483.jpg +3 -0
- app/static/uploads/86665944-f463-4be3-87be-2227b667ea4d.jpg +3 -0
- app/static/uploads/8f2e9c10-9e39-415b-b634-e9cacff08670.jpg +3 -0
- app/static/uploads/9be81a29-214a-48af-ba99-8cab54a56d66.jpg +3 -0
- app/static/uploads/db93084e-16d1-4448-b841-1ce1c0e1c201.jpg +3 -0
- app/static/uploads/de9bda87-c237-4c6b-8748-1ab245d4ec57.jpg +0 -0
- app/static/uploads/f0a7c31c-d1cf-460a-abff-d32d76faa384.jpg +0 -0
- app/static/uploads/fdfef36b-2942-4bc8-bb26-ad6aad32d471.jpg +0 -0
- app/templates/index.html +82 -0
- app/templates/login.html +22 -0
- configs/damage_config.yaml +13 -0
- configs/parts_config.yaml +13 -0
- inference/damage_inference.py +58 -0
- inference/parts_inference.py +74 -0
- requirements.txt +11 -0
.gitattributes
ADDED
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
*.7z filter=lfs diff=lfs merge=lfs -text
|
2 |
+
*.arrow filter=lfs diff=lfs merge=lfs -text
|
3 |
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
4 |
+
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
5 |
+
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
6 |
+
*.ftz filter=lfs diff=lfs merge=lfs -text
|
7 |
+
*.gz filter=lfs diff=lfs merge=lfs -text
|
8 |
+
*.h5 filter=lfs diff=lfs merge=lfs -text
|
9 |
+
*.joblib filter=lfs diff=lfs merge=lfs -text
|
10 |
+
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
11 |
+
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
12 |
+
*.model filter=lfs diff=lfs merge=lfs -text
|
13 |
+
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
14 |
+
*.npy filter=lfs diff=lfs merge=lfs -text
|
15 |
+
*.npz filter=lfs diff=lfs merge=lfs -text
|
16 |
+
*.onnx filter=lfs diff=lfs merge=lfs -text
|
17 |
+
*.ot filter=lfs diff=lfs merge=lfs -text
|
18 |
+
*.parquet filter=lfs diff=lfs merge=lfs -text
|
19 |
+
*.pb filter=lfs diff=lfs merge=lfs -text
|
20 |
+
*.pickle filter=lfs diff=lfs merge=lfs -text
|
21 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
22 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
23 |
+
*.pth filter=lfs diff=lfs merge=lfs -text
|
24 |
+
*.rar filter=lfs diff=lfs merge=lfs -text
|
25 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
26 |
+
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
27 |
+
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
28 |
+
*.tar filter=lfs diff=lfs merge=lfs -text
|
29 |
+
*.tflite filter=lfs diff=lfs merge=lfs -text
|
30 |
+
*.tgz filter=lfs diff=lfs merge=lfs -text
|
31 |
+
*.wasm filter=lfs diff=lfs merge=lfs -text
|
32 |
+
*.xz filter=lfs diff=lfs merge=lfs -text
|
33 |
+
*.zip filter=lfs diff=lfs merge=lfs -text
|
34 |
+
*.zst filter=lfs diff=lfs merge=lfs -text
|
35 |
+
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
36 |
+
app/static/results/1d91cb91-46f0-4123-9b3d-dd963a4a1b8b_damage.png filter=lfs diff=lfs merge=lfs -text
|
37 |
+
app/static/results/1d91cb91-46f0-4123-9b3d-dd963a4a1b8b_parts.png filter=lfs diff=lfs merge=lfs -text
|
38 |
+
app/static/results/2b9a2716-12dc-4fec-8b1b-8a26375e4fd9_damage.png filter=lfs diff=lfs merge=lfs -text
|
39 |
+
app/static/results/2b9a2716-12dc-4fec-8b1b-8a26375e4fd9_parts.png filter=lfs diff=lfs merge=lfs -text
|
40 |
+
app/static/results/4ea85461-64cd-4c8b-8ef9-3c9635df076b_damage.png filter=lfs diff=lfs merge=lfs -text
|
41 |
+
app/static/results/4ea85461-64cd-4c8b-8ef9-3c9635df076b_parts.png filter=lfs diff=lfs merge=lfs -text
|
42 |
+
app/static/results/706f091d-f713-4f34-88b1-b3eaa5082483_damage.png filter=lfs diff=lfs merge=lfs -text
|
43 |
+
app/static/results/706f091d-f713-4f34-88b1-b3eaa5082483_parts.png filter=lfs diff=lfs merge=lfs -text
|
44 |
+
app/static/results/86665944-f463-4be3-87be-2227b667ea4d_damage.png filter=lfs diff=lfs merge=lfs -text
|
45 |
+
app/static/results/86665944-f463-4be3-87be-2227b667ea4d_parts.png filter=lfs diff=lfs merge=lfs -text
|
46 |
+
app/static/results/8f2e9c10-9e39-415b-b634-e9cacff08670_damage.png filter=lfs diff=lfs merge=lfs -text
|
47 |
+
app/static/results/8f2e9c10-9e39-415b-b634-e9cacff08670_parts.png filter=lfs diff=lfs merge=lfs -text
|
48 |
+
app/static/results/9be81a29-214a-48af-ba99-8cab54a56d66_damage.png filter=lfs diff=lfs merge=lfs -text
|
49 |
+
app/static/results/9be81a29-214a-48af-ba99-8cab54a56d66_parts.png filter=lfs diff=lfs merge=lfs -text
|
50 |
+
app/static/results/db93084e-16d1-4448-b841-1ce1c0e1c201_damage.png filter=lfs diff=lfs merge=lfs -text
|
51 |
+
app/static/results/db93084e-16d1-4448-b841-1ce1c0e1c201_parts.png filter=lfs diff=lfs merge=lfs -text
|
52 |
+
app/static/results/de9bda87-c237-4c6b-8748-1ab245d4ec57_damage.png filter=lfs diff=lfs merge=lfs -text
|
53 |
+
app/static/results/de9bda87-c237-4c6b-8748-1ab245d4ec57_parts.png filter=lfs diff=lfs merge=lfs -text
|
54 |
+
app/static/results/f0a7c31c-d1cf-460a-abff-d32d76faa384_damage.png filter=lfs diff=lfs merge=lfs -text
|
55 |
+
app/static/results/f0a7c31c-d1cf-460a-abff-d32d76faa384_parts.png filter=lfs diff=lfs merge=lfs -text
|
56 |
+
app/static/results/fdfef36b-2942-4bc8-bb26-ad6aad32d471_parts.png filter=lfs diff=lfs merge=lfs -text
|
57 |
+
app/static/uploads/1d91cb91-46f0-4123-9b3d-dd963a4a1b8b.jpg filter=lfs diff=lfs merge=lfs -text
|
58 |
+
app/static/uploads/2b9a2716-12dc-4fec-8b1b-8a26375e4fd9.jpg filter=lfs diff=lfs merge=lfs -text
|
59 |
+
app/static/uploads/4ea85461-64cd-4c8b-8ef9-3c9635df076b.jpg filter=lfs diff=lfs merge=lfs -text
|
60 |
+
app/static/uploads/706f091d-f713-4f34-88b1-b3eaa5082483.jpg filter=lfs diff=lfs merge=lfs -text
|
61 |
+
app/static/uploads/86665944-f463-4be3-87be-2227b667ea4d.jpg filter=lfs diff=lfs merge=lfs -text
|
62 |
+
app/static/uploads/8f2e9c10-9e39-415b-b634-e9cacff08670.jpg filter=lfs diff=lfs merge=lfs -text
|
63 |
+
app/static/uploads/9be81a29-214a-48af-ba99-8cab54a56d66.jpg filter=lfs diff=lfs merge=lfs -text
|
64 |
+
app/static/uploads/db93084e-16d1-4448-b841-1ce1c0e1c201.jpg filter=lfs diff=lfs merge=lfs -text
|
Dockerfile
ADDED
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
FROM python:3.10-slim
|
2 |
+
|
3 |
+
WORKDIR /code
|
4 |
+
|
5 |
+
# Install system dependencies
|
6 |
+
RUN apt-get update && apt-get install -y \
|
7 |
+
build-essential \
|
8 |
+
libgl1-mesa-glx \
|
9 |
+
&& rm -rf /var/lib/apt/lists/*
|
10 |
+
|
11 |
+
# Install Python dependencies
|
12 |
+
COPY requirements.txt .
|
13 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
14 |
+
|
15 |
+
# Copy app code
|
16 |
+
COPY . .
|
17 |
+
|
18 |
+
# Expose port
|
19 |
+
EXPOSE 7860
|
20 |
+
|
21 |
+
ENV PATH="/root/.local/bin:$PATH"
|
22 |
+
|
23 |
+
# Run FastAPI app with uvicorn
|
24 |
+
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860"]
|
README.md
ADDED
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
title: Car Damage & Parts Detection
|
3 |
+
emoji: 🚗
|
4 |
+
colorFrom: blue
|
5 |
+
colorTo: indigo
|
6 |
+
sdk: docker
|
7 |
+
app_file: app/main.py
|
8 |
+
pinned: false
|
9 |
+
---
|
10 |
+
|
11 |
+
# Car Damage & Parts Detection
|
12 |
+
|
13 |
+
Upload a car image to detect damaged regions and parts using YOLOv8 models.
|
14 |
+
|
15 |
+
## How to use
|
16 |
+
- Upload a car image.
|
17 |
+
- View annotated results and JSON output.
|
18 |
+
|
19 |
+
## Deployment
|
20 |
+
This Space uses FastAPI and YOLOv8, running in a Docker container.
|
21 |
+
|
22 |
+
## Model Weights
|
23 |
+
Model weights are downloaded at startup from public cloud links (not included in repo).
|
24 |
+
|
app/__pycache__/main.cpython-310.pyc
ADDED
Binary file (1.12 kB). View file
|
|
app/main.py
ADDED
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
|
2 |
+
# --- Model download logic (Hugging Face Hub) ---
|
3 |
+
import os
|
4 |
+
import requests
|
5 |
+
|
6 |
+
def download_if_missing(url, dest):
|
7 |
+
if not os.path.exists(dest):
|
8 |
+
print(f"Downloading model from {url} to {dest}...")
|
9 |
+
os.makedirs(os.path.dirname(dest), exist_ok=True)
|
10 |
+
with requests.get(url, stream=True) as r:
|
11 |
+
r.raise_for_status()
|
12 |
+
with open(dest, "wb") as f:
|
13 |
+
for chunk in r.iter_content(chunk_size=8192):
|
14 |
+
f.write(chunk)
|
15 |
+
print("Download complete.")
|
16 |
+
|
17 |
+
# Hugging Face direct download links
|
18 |
+
DAMAGE_MODEL_URL = "https://huggingface.co/AItoolstack/car_damage_detection/resolve/main/yolov8_models/damage/weights/weights/best.pt"
|
19 |
+
PARTS_MODEL_URL = "https://huggingface.co/AItoolstack/car_damage_detection/resolve/main/yolov8_models/parts/weights/weights/best.pt"
|
20 |
+
|
21 |
+
DAMAGE_MODEL_PATH = os.path.join("models", "damage", "weights", "weights", "best.pt")
|
22 |
+
PARTS_MODEL_PATH = os.path.join("models", "parts", "weights", "weights", "best.pt")
|
23 |
+
|
24 |
+
download_if_missing(DAMAGE_MODEL_URL, DAMAGE_MODEL_PATH)
|
25 |
+
download_if_missing(PARTS_MODEL_URL, PARTS_MODEL_PATH)
|
26 |
+
|
27 |
+
from fastapi import FastAPI, File, UploadFile, BackgroundTasks
|
28 |
+
from fastapi.staticfiles import StaticFiles
|
29 |
+
from fastapi.templating import Jinja2Templates
|
30 |
+
from fastapi.responses import JSONResponse, HTMLResponse
|
31 |
+
from fastapi.middleware.cors import CORSMiddleware
|
32 |
+
import uvicorn
|
33 |
+
from datetime import datetime
|
34 |
+
import aiofiles
|
35 |
+
from pathlib import Path
|
36 |
+
import uuid
|
37 |
+
from app.routers import inference
|
38 |
+
|
39 |
+
app = FastAPI(title="Car Damage Detection API")
|
40 |
+
|
41 |
+
# CORS middleware
|
42 |
+
app.add_middleware(
|
43 |
+
CORSMiddleware,
|
44 |
+
allow_origins=["*"],
|
45 |
+
allow_credentials=True,
|
46 |
+
allow_methods=["*"],
|
47 |
+
allow_headers=["*"],
|
48 |
+
)
|
49 |
+
|
50 |
+
# Mount static files
|
51 |
+
app.mount("/static", StaticFiles(directory="app/static"), name="static")
|
52 |
+
templates = Jinja2Templates(directory="app/templates")
|
53 |
+
|
54 |
+
# Include routers
|
55 |
+
app.include_router(inference.router)
|
56 |
+
|
57 |
+
if __name__ == "__main__":
|
58 |
+
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
|
app/routers/__pycache__/inference.cpython-310.pyc
ADDED
Binary file (6.69 kB). View file
|
|
app/routers/inference.py
ADDED
@@ -0,0 +1,218 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
|
2 |
+
from fastapi import APIRouter, Request, UploadFile, File, Form
|
3 |
+
from fastapi.responses import HTMLResponse, FileResponse, JSONResponse
|
4 |
+
from fastapi.templating import Jinja2Templates
|
5 |
+
from starlette.background import BackgroundTask
|
6 |
+
import shutil
|
7 |
+
import os
|
8 |
+
import uuid
|
9 |
+
from pathlib import Path
|
10 |
+
from typing import Optional
|
11 |
+
import json
|
12 |
+
import base64
|
13 |
+
from ultralytics import YOLO
|
14 |
+
import cv2
|
15 |
+
import numpy as np
|
16 |
+
|
17 |
+
|
18 |
+
# Templates directory
|
19 |
+
TEMPLATES_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "templates")
|
20 |
+
templates = Jinja2Templates(directory=TEMPLATES_DIR)
|
21 |
+
|
22 |
+
router = APIRouter()
|
23 |
+
|
24 |
+
UPLOAD_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "static", "uploads")
|
25 |
+
RESULTS_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "static", "results")
|
26 |
+
|
27 |
+
os.makedirs(UPLOAD_DIR, exist_ok=True)
|
28 |
+
os.makedirs(RESULTS_DIR, exist_ok=True)
|
29 |
+
|
30 |
+
ALLOWED_EXTENSIONS = {"jpg", "jpeg", "png", "tiff", "tif"}
|
31 |
+
|
32 |
+
# Model paths
|
33 |
+
BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
34 |
+
DAMAGE_MODEL_PATH = os.path.join(BASE_DIR, "models", "damage", "weights", "weights", "best.pt")
|
35 |
+
PARTS_MODEL_PATH = os.path.join(BASE_DIR, "models", "parts", "weights", "weights", "best.pt")
|
36 |
+
|
37 |
+
# Class names for parts
|
38 |
+
PARTS_CLASS_NAMES = ['headlamp', 'front_bumper', 'hood', 'door', 'rear_bumper']
|
39 |
+
|
40 |
+
# Helper: Run YOLO inference and return results
|
41 |
+
def run_yolo_inference(model_path, image_path, task='segment'):
|
42 |
+
model = YOLO(model_path)
|
43 |
+
results = model.predict(source=image_path, imgsz=640, conf=0.25, save=False, task=task)
|
44 |
+
return results[0]
|
45 |
+
|
46 |
+
# Helper: Draw masks and confidence on image
|
47 |
+
def draw_masks_and_conf(image_path, yolo_result, class_names=None):
|
48 |
+
img = cv2.imread(image_path)
|
49 |
+
overlay = img.copy()
|
50 |
+
out_img = img.copy()
|
51 |
+
colors = [(255,0,0), (0,255,0), (0,0,255), (255,255,0), (255,0,255), (0,255,255)]
|
52 |
+
for i, box in enumerate(yolo_result.boxes):
|
53 |
+
conf = float(box.conf[0])
|
54 |
+
cls = int(box.cls[0])
|
55 |
+
color = colors[cls % len(colors)]
|
56 |
+
# Draw bbox
|
57 |
+
x1, y1, x2, y2 = map(int, box.xyxy[0])
|
58 |
+
cv2.rectangle(overlay, (x1, y1), (x2, y2), color, 2)
|
59 |
+
label = f"{class_names[cls] if class_names else 'damage'}: {conf:.2f}"
|
60 |
+
cv2.putText(overlay, label, (x1, y1-10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2)
|
61 |
+
# Draw mask if available
|
62 |
+
if hasattr(yolo_result, 'masks') and yolo_result.masks is not None:
|
63 |
+
mask = yolo_result.masks.data[i].cpu().numpy()
|
64 |
+
mask = (mask * 255).astype(np.uint8)
|
65 |
+
mask = cv2.resize(mask, (x2-x1, y2-y1))
|
66 |
+
roi = overlay[y1:y2, x1:x2]
|
67 |
+
colored_mask = np.zeros_like(roi)
|
68 |
+
colored_mask[mask > 127] = color
|
69 |
+
overlay[y1:y2, x1:x2] = cv2.addWeighted(roi, 0.5, colored_mask, 0.5, 0)
|
70 |
+
out_img = cv2.addWeighted(overlay, 0.7, img, 0.3, 0)
|
71 |
+
return out_img
|
72 |
+
|
73 |
+
# Helper: Generate JSON output
|
74 |
+
def generate_json_output(filename, damage_result, parts_result):
|
75 |
+
# Damage severity: use max confidence
|
76 |
+
severity_score = float(max([float(box.conf[0]) for box in damage_result.boxes], default=0))
|
77 |
+
damage_regions = []
|
78 |
+
for box in damage_result.boxes:
|
79 |
+
x1, y1, x2, y2 = map(float, box.xyxy[0])
|
80 |
+
conf = float(box.conf[0])
|
81 |
+
damage_regions.append({"bbox": [x1, y1, x2, y2], "confidence": conf})
|
82 |
+
# Parts
|
83 |
+
parts = []
|
84 |
+
for i, box in enumerate(parts_result.boxes):
|
85 |
+
x1, y1, x2, y2 = map(float, box.xyxy[0])
|
86 |
+
conf = float(box.conf[0])
|
87 |
+
cls = int(box.cls[0])
|
88 |
+
# Damage %: use mask area / bbox area if available
|
89 |
+
damage_percentage = None
|
90 |
+
if hasattr(parts_result, 'masks') and parts_result.masks is not None:
|
91 |
+
mask = parts_result.masks.data[i].cpu().numpy()
|
92 |
+
mask_area = np.sum(mask > 0.5)
|
93 |
+
bbox_area = (x2-x1)*(y2-y1)
|
94 |
+
damage_percentage = float(mask_area / bbox_area) if bbox_area > 0 else None
|
95 |
+
parts.append({
|
96 |
+
"part": PARTS_CLASS_NAMES[cls] if cls < len(PARTS_CLASS_NAMES) else str(cls),
|
97 |
+
"damaged": True,
|
98 |
+
"confidence": conf,
|
99 |
+
"damage_percentage": damage_percentage,
|
100 |
+
"bbox": [x1, y1, x2, y2]
|
101 |
+
})
|
102 |
+
# Optionally, add base64 masks
|
103 |
+
# (not implemented here for brevity)
|
104 |
+
return {
|
105 |
+
"filename": filename,
|
106 |
+
"damage": {
|
107 |
+
"severity_score": severity_score,
|
108 |
+
"regions": damage_regions
|
109 |
+
},
|
110 |
+
"parts": parts,
|
111 |
+
"cost_estimate": None
|
112 |
+
}
|
113 |
+
|
114 |
+
# Dummy login credentials
|
115 |
+
def check_login(username: str, password: str) -> bool:
|
116 |
+
return username == "demo" and password == "demo123"
|
117 |
+
|
118 |
+
@router.get("/", response_class=HTMLResponse)
|
119 |
+
def home(request: Request):
|
120 |
+
return templates.TemplateResponse("index.html", {"request": request, "result": None})
|
121 |
+
|
122 |
+
@router.post("/login", response_class=HTMLResponse)
|
123 |
+
def login(request: Request, username: str = Form(...), password: str = Form(...)):
|
124 |
+
if check_login(username, password):
|
125 |
+
return templates.TemplateResponse("index.html", {"request": request, "result": None, "user": username})
|
126 |
+
return templates.TemplateResponse("login.html", {"request": request, "error": "Invalid credentials"})
|
127 |
+
|
128 |
+
@router.get("/login", response_class=HTMLResponse)
|
129 |
+
def login_page(request: Request):
|
130 |
+
return templates.TemplateResponse("login.html", {"request": request})
|
131 |
+
|
132 |
+
@router.post("/upload", response_class=HTMLResponse)
|
133 |
+
def upload_image(request: Request, file: UploadFile = File(...)):
|
134 |
+
ext = file.filename.split(".")[-1].lower()
|
135 |
+
if ext not in ALLOWED_EXTENSIONS:
|
136 |
+
return templates.TemplateResponse("index.html", {"request": request, "error": "Unsupported file type."})
|
137 |
+
|
138 |
+
# Save uploaded file
|
139 |
+
session_id = str(uuid.uuid4())
|
140 |
+
upload_path = os.path.join(UPLOAD_DIR, f"{session_id}.{ext}")
|
141 |
+
with open(upload_path, "wb") as buffer:
|
142 |
+
shutil.copyfileobj(file.file, buffer)
|
143 |
+
|
144 |
+
# Run both inferences
|
145 |
+
try:
|
146 |
+
damage_result = run_yolo_inference(DAMAGE_MODEL_PATH, upload_path)
|
147 |
+
parts_result = run_yolo_inference(PARTS_MODEL_PATH, upload_path)
|
148 |
+
|
149 |
+
# Save annotated images
|
150 |
+
damage_img_path = os.path.join(RESULTS_DIR, f"{session_id}_damage.png")
|
151 |
+
parts_img_path = os.path.join(RESULTS_DIR, f"{session_id}_parts.png")
|
152 |
+
json_path = os.path.join(RESULTS_DIR, f"{session_id}_result.json")
|
153 |
+
damage_img_url = f"/static/results/{session_id}_damage.png"
|
154 |
+
parts_img_url = f"/static/results/{session_id}_parts.png"
|
155 |
+
json_url = f"/static/results/{session_id}_result.json"
|
156 |
+
|
157 |
+
# Defensive: set to None by default
|
158 |
+
damage_img = None
|
159 |
+
parts_img = None
|
160 |
+
json_output = None
|
161 |
+
|
162 |
+
# Only save and set if inference returns boxes
|
163 |
+
if hasattr(damage_result, 'boxes') and len(damage_result.boxes) > 0:
|
164 |
+
damage_img = draw_masks_and_conf(upload_path, damage_result)
|
165 |
+
cv2.imwrite(damage_img_path, damage_img)
|
166 |
+
if hasattr(parts_result, 'boxes') and len(parts_result.boxes) > 0:
|
167 |
+
parts_img = draw_masks_and_conf(upload_path, parts_result, class_names=PARTS_CLASS_NAMES)
|
168 |
+
cv2.imwrite(parts_img_path, parts_img)
|
169 |
+
if (hasattr(damage_result, 'boxes') and len(damage_result.boxes) > 0) or (hasattr(parts_result, 'boxes') and len(parts_result.boxes) > 0):
|
170 |
+
json_output = generate_json_output(file.filename, damage_result, parts_result)
|
171 |
+
with open(json_path, "w") as jf:
|
172 |
+
json.dump(json_output, jf, indent=2)
|
173 |
+
|
174 |
+
# Prepare URLs for download (only if files exist)
|
175 |
+
result = {
|
176 |
+
"filename": file.filename,
|
177 |
+
"damage_image": damage_img_url if damage_img is not None else None,
|
178 |
+
"parts_image": parts_img_url if parts_img is not None else None,
|
179 |
+
"json": json_output,
|
180 |
+
"json_download": json_url if json_output is not None else None
|
181 |
+
}
|
182 |
+
# Debug log
|
183 |
+
print("[DEBUG] Result dict:", result)
|
184 |
+
except Exception as e:
|
185 |
+
result = {
|
186 |
+
"filename": file.filename,
|
187 |
+
"error": f"Inference failed: {str(e)}",
|
188 |
+
"damage_image": None,
|
189 |
+
"parts_image": None,
|
190 |
+
"json": None,
|
191 |
+
"json_download": None
|
192 |
+
}
|
193 |
+
print("[ERROR] Inference failed:", e)
|
194 |
+
|
195 |
+
import threading
|
196 |
+
import time
|
197 |
+
def delayed_cleanup():
|
198 |
+
time.sleep(300) # 5 minutes
|
199 |
+
try:
|
200 |
+
os.remove(upload_path)
|
201 |
+
except Exception:
|
202 |
+
pass
|
203 |
+
for suffix in ["_damage.png", "_parts.png", "_result.json"]:
|
204 |
+
try:
|
205 |
+
os.remove(os.path.join(RESULTS_DIR, f"{session_id}{suffix}"))
|
206 |
+
except Exception:
|
207 |
+
pass
|
208 |
+
|
209 |
+
threading.Thread(target=delayed_cleanup, daemon=True).start()
|
210 |
+
|
211 |
+
return templates.TemplateResponse(
|
212 |
+
"index.html",
|
213 |
+
{
|
214 |
+
"request": request,
|
215 |
+
"result": result,
|
216 |
+
"original_image": f"/static/uploads/{session_id}.{ext}"
|
217 |
+
}
|
218 |
+
)
|
app/static/css/style.css
ADDED
@@ -0,0 +1,140 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap');
|
2 |
+
|
3 |
+
body {
|
4 |
+
font-family: 'Roboto', Arial, sans-serif;
|
5 |
+
background: #f4f6fa;
|
6 |
+
margin: 0;
|
7 |
+
padding: 0;
|
8 |
+
}
|
9 |
+
.header {
|
10 |
+
background: linear-gradient(90deg, #003366 60%, #e30613 100%);
|
11 |
+
color: #fff;
|
12 |
+
padding: 24px 0 12px 0;
|
13 |
+
text-align: center;
|
14 |
+
box-shadow: 0 2px 8px #00336622;
|
15 |
+
}
|
16 |
+
.logo {
|
17 |
+
max-width: 180px;
|
18 |
+
margin-bottom: 10px;
|
19 |
+
}
|
20 |
+
@media (max-width: 600px) {
|
21 |
+
.logo {
|
22 |
+
max-width: 90px !important;
|
23 |
+
right: 10px !important;
|
24 |
+
top: 10px !important;
|
25 |
+
}
|
26 |
+
.header h2 { font-size: 1.1em; }
|
27 |
+
}
|
28 |
+
.container {
|
29 |
+
max-width: 950px;
|
30 |
+
margin: 40px auto 0 auto;
|
31 |
+
background: #fff;
|
32 |
+
padding: 32px 32px 24px 32px;
|
33 |
+
border-radius: 12px;
|
34 |
+
box-shadow: 0 4px 24px #00336622;
|
35 |
+
}
|
36 |
+
h2, h3, h4 {
|
37 |
+
color: #003366;
|
38 |
+
margin-top: 0;
|
39 |
+
}
|
40 |
+
form {
|
41 |
+
margin-bottom: 24px;
|
42 |
+
display: flex;
|
43 |
+
flex-direction: column;
|
44 |
+
align-items: center;
|
45 |
+
}
|
46 |
+
input[type="file"] {
|
47 |
+
margin-bottom: 16px;
|
48 |
+
font-size: 16px;
|
49 |
+
}
|
50 |
+
button {
|
51 |
+
background: #e30613;
|
52 |
+
color: #fff;
|
53 |
+
border: none;
|
54 |
+
padding: 12px 32px;
|
55 |
+
border-radius: 6px;
|
56 |
+
font-size: 16px;
|
57 |
+
font-weight: 700;
|
58 |
+
cursor: pointer;
|
59 |
+
transition: background 0.2s;
|
60 |
+
margin-bottom: 10px;
|
61 |
+
}
|
62 |
+
button:hover {
|
63 |
+
background: #003366;
|
64 |
+
}
|
65 |
+
.error {
|
66 |
+
color: #e30613;
|
67 |
+
margin-bottom: 16px;
|
68 |
+
font-weight: 700;
|
69 |
+
}
|
70 |
+
.images-row {
|
71 |
+
display: flex;
|
72 |
+
gap: 32px;
|
73 |
+
margin-bottom: 24px;
|
74 |
+
justify-content: center;
|
75 |
+
flex-wrap: wrap;
|
76 |
+
}
|
77 |
+
.result-img {
|
78 |
+
max-width: 260px;
|
79 |
+
border: 2px solid #e30613;
|
80 |
+
border-radius: 8px;
|
81 |
+
box-shadow: 0 2px 8px #00336622;
|
82 |
+
margin-bottom: 8px;
|
83 |
+
}
|
84 |
+
.result-label {
|
85 |
+
text-align: center;
|
86 |
+
font-weight: 700;
|
87 |
+
color: #003366;
|
88 |
+
margin-bottom: 8px;
|
89 |
+
}
|
90 |
+
.download-btn {
|
91 |
+
display: inline-block;
|
92 |
+
background: #003366;
|
93 |
+
color: #fff;
|
94 |
+
padding: 8px 18px;
|
95 |
+
border-radius: 5px;
|
96 |
+
text-decoration: none;
|
97 |
+
font-weight: 700;
|
98 |
+
margin-right: 10px;
|
99 |
+
margin-bottom: 10px;
|
100 |
+
transition: background 0.2s;
|
101 |
+
}
|
102 |
+
.download-btn:hover {
|
103 |
+
background: #e30613;
|
104 |
+
}
|
105 |
+
pre.json-output {
|
106 |
+
background: #f0f0f0;
|
107 |
+
padding: 16px;
|
108 |
+
border-radius: 6px;
|
109 |
+
font-size: 15px;
|
110 |
+
overflow-x: auto;
|
111 |
+
color: #222;
|
112 |
+
margin-bottom: 12px;
|
113 |
+
box-shadow: 0 1px 4px #00336611;
|
114 |
+
}
|
115 |
+
.copy-btn {
|
116 |
+
background: #e30613;
|
117 |
+
color: #fff;
|
118 |
+
border: none;
|
119 |
+
border-radius: 4px;
|
120 |
+
padding: 6px 14px;
|
121 |
+
font-size: 14px;
|
122 |
+
font-weight: 700;
|
123 |
+
cursor: pointer;
|
124 |
+
margin-bottom: 10px;
|
125 |
+
float: right;
|
126 |
+
}
|
127 |
+
.copy-btn:hover {
|
128 |
+
background: #003366;
|
129 |
+
}
|
130 |
+
.footer {
|
131 |
+
text-align: center;
|
132 |
+
color: #888;
|
133 |
+
font-size: 15px;
|
134 |
+
margin: 40px 0 10px 0;
|
135 |
+
}
|
136 |
+
@media (max-width: 900px) {
|
137 |
+
.container { padding: 16px; }
|
138 |
+
.images-row { gap: 12px; }
|
139 |
+
.result-img { max-width: 98vw; }
|
140 |
+
}
|
app/static/img/logo.png
ADDED
![]() |
app/static/results/1d91cb91-46f0-4123-9b3d-dd963a4a1b8b_damage.png
ADDED
![]() |
Git LFS Details
|
app/static/results/1d91cb91-46f0-4123-9b3d-dd963a4a1b8b_parts.png
ADDED
![]() |
Git LFS Details
|
app/static/results/2b9a2716-12dc-4fec-8b1b-8a26375e4fd9_damage.png
ADDED
![]() |
Git LFS Details
|
app/static/results/2b9a2716-12dc-4fec-8b1b-8a26375e4fd9_parts.png
ADDED
![]() |
Git LFS Details
|
app/static/results/4ea85461-64cd-4c8b-8ef9-3c9635df076b_damage.png
ADDED
![]() |
Git LFS Details
|
app/static/results/4ea85461-64cd-4c8b-8ef9-3c9635df076b_parts.png
ADDED
![]() |
Git LFS Details
|
app/static/results/4ea85461-64cd-4c8b-8ef9-3c9635df076b_result.json
ADDED
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"filename": "28.jpg",
|
3 |
+
"damage": {
|
4 |
+
"severity_score": 0.7707259058952332,
|
5 |
+
"regions": [
|
6 |
+
{
|
7 |
+
"bbox": [
|
8 |
+
522.2720947265625,
|
9 |
+
530.5602416992188,
|
10 |
+
728.9972534179688,
|
11 |
+
892.2079467773438
|
12 |
+
],
|
13 |
+
"confidence": 0.7707259058952332
|
14 |
+
},
|
15 |
+
{
|
16 |
+
"bbox": [
|
17 |
+
660.1974487304688,
|
18 |
+
512.72119140625,
|
19 |
+
927.4169921875,
|
20 |
+
883.8523559570312
|
21 |
+
],
|
22 |
+
"confidence": 0.6494596600532532
|
23 |
+
},
|
24 |
+
{
|
25 |
+
"bbox": [
|
26 |
+
196.1370086669922,
|
27 |
+
277.27374267578125,
|
28 |
+
366.7127990722656,
|
29 |
+
712.44775390625
|
30 |
+
],
|
31 |
+
"confidence": 0.32863757014274597
|
32 |
+
}
|
33 |
+
]
|
34 |
+
},
|
35 |
+
"parts": [
|
36 |
+
{
|
37 |
+
"part": "hood",
|
38 |
+
"damaged": true,
|
39 |
+
"confidence": 0.9392628073692322,
|
40 |
+
"damage_percentage": 0.20169149219339333,
|
41 |
+
"bbox": [
|
42 |
+
411.4404296875,
|
43 |
+
342.9278869628906,
|
44 |
+
952.9636840820312,
|
45 |
+
578.835693359375
|
46 |
+
]
|
47 |
+
},
|
48 |
+
{
|
49 |
+
"part": "headlamp",
|
50 |
+
"damaged": true,
|
51 |
+
"confidence": 0.9351847171783447,
|
52 |
+
"damage_percentage": 1.7647002993974346,
|
53 |
+
"bbox": [
|
54 |
+
531.9706420898438,
|
55 |
+
553.8896484375,
|
56 |
+
706.9000854492188,
|
57 |
+
656.3943481445312
|
58 |
+
]
|
59 |
+
},
|
60 |
+
{
|
61 |
+
"part": "door",
|
62 |
+
"damaged": true,
|
63 |
+
"confidence": 0.9011728763580322,
|
64 |
+
"damage_percentage": 0.29901854346557094,
|
65 |
+
"bbox": [
|
66 |
+
143.66470336914062,
|
67 |
+
224.3675537109375,
|
68 |
+
391.3798522949219,
|
69 |
+
737.7504272460938
|
70 |
+
]
|
71 |
+
},
|
72 |
+
{
|
73 |
+
"part": "front_bumper",
|
74 |
+
"damaged": true,
|
75 |
+
"confidence": 0.8590587377548218,
|
76 |
+
"damage_percentage": 0.03409931158993679,
|
77 |
+
"bbox": [
|
78 |
+
461.2880859375,
|
79 |
+
534.1759033203125,
|
80 |
+
978.0007934570312,
|
81 |
+
874.1959838867188
|
82 |
+
]
|
83 |
+
}
|
84 |
+
],
|
85 |
+
"cost_estimate": null
|
86 |
+
}
|
app/static/results/706f091d-f713-4f34-88b1-b3eaa5082483_damage.png
ADDED
![]() |
Git LFS Details
|
app/static/results/706f091d-f713-4f34-88b1-b3eaa5082483_parts.png
ADDED
![]() |
Git LFS Details
|
app/static/results/86665944-f463-4be3-87be-2227b667ea4d_damage.png
ADDED
![]() |
Git LFS Details
|
app/static/results/86665944-f463-4be3-87be-2227b667ea4d_parts.png
ADDED
![]() |
Git LFS Details
|
app/static/results/8f2e9c10-9e39-415b-b634-e9cacff08670_damage.png
ADDED
![]() |
Git LFS Details
|
app/static/results/8f2e9c10-9e39-415b-b634-e9cacff08670_parts.png
ADDED
![]() |
Git LFS Details
|
app/static/results/9be81a29-214a-48af-ba99-8cab54a56d66_damage.png
ADDED
![]() |
Git LFS Details
|
app/static/results/9be81a29-214a-48af-ba99-8cab54a56d66_parts.png
ADDED
![]() |
Git LFS Details
|
app/static/results/9be81a29-214a-48af-ba99-8cab54a56d66_result.json
ADDED
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"filename": "72.jpg",
|
3 |
+
"damage": {
|
4 |
+
"severity_score": 0.6467820405960083,
|
5 |
+
"regions": [
|
6 |
+
{
|
7 |
+
"bbox": [
|
8 |
+
20.340744018554688,
|
9 |
+
93.4926986694336,
|
10 |
+
112.2962646484375,
|
11 |
+
271.3760986328125
|
12 |
+
],
|
13 |
+
"confidence": 0.6467820405960083
|
14 |
+
},
|
15 |
+
{
|
16 |
+
"bbox": [
|
17 |
+
129.6874237060547,
|
18 |
+
107.35161590576172,
|
19 |
+
238.290771484375,
|
20 |
+
377.91192626953125
|
21 |
+
],
|
22 |
+
"confidence": 0.43937909603118896
|
23 |
+
},
|
24 |
+
{
|
25 |
+
"bbox": [
|
26 |
+
127.2110595703125,
|
27 |
+
23.03559684753418,
|
28 |
+
353.25946044921875,
|
29 |
+
420.0022888183594
|
30 |
+
],
|
31 |
+
"confidence": 0.3795366883277893
|
32 |
+
},
|
33 |
+
{
|
34 |
+
"bbox": [
|
35 |
+
126.8061752319336,
|
36 |
+
38.61054611206055,
|
37 |
+
242.341064453125,
|
38 |
+
433.29034423828125
|
39 |
+
],
|
40 |
+
"confidence": 0.3542208969593048
|
41 |
+
}
|
42 |
+
]
|
43 |
+
},
|
44 |
+
"parts": [
|
45 |
+
{
|
46 |
+
"part": "hood",
|
47 |
+
"damaged": true,
|
48 |
+
"confidence": 0.746218204498291,
|
49 |
+
"damage_percentage": 0.2284878057871993,
|
50 |
+
"bbox": [
|
51 |
+
1.409912109375,
|
52 |
+
666.6517944335938,
|
53 |
+
200.2979278564453,
|
54 |
+
1020.8939819335938
|
55 |
+
]
|
56 |
+
},
|
57 |
+
{
|
58 |
+
"part": "hood",
|
59 |
+
"damaged": true,
|
60 |
+
"confidence": 0.5614964962005615,
|
61 |
+
"damage_percentage": 0.04930226900457474,
|
62 |
+
"bbox": [
|
63 |
+
265.2085876464844,
|
64 |
+
582.6806030273438,
|
65 |
+
1017.8049926757812,
|
66 |
+
1016.5338745117188
|
67 |
+
]
|
68 |
+
},
|
69 |
+
{
|
70 |
+
"part": "hood",
|
71 |
+
"damaged": true,
|
72 |
+
"confidence": 0.5332340598106384,
|
73 |
+
"damage_percentage": 0.25070920790460405,
|
74 |
+
"bbox": [
|
75 |
+
0.0,
|
76 |
+
501.2841796875,
|
77 |
+
358.5841369628906,
|
78 |
+
680.34912109375
|
79 |
+
]
|
80 |
+
}
|
81 |
+
],
|
82 |
+
"cost_estimate": null
|
83 |
+
}
|
app/static/results/db93084e-16d1-4448-b841-1ce1c0e1c201_damage.png
ADDED
![]() |
Git LFS Details
|
app/static/results/db93084e-16d1-4448-b841-1ce1c0e1c201_parts.png
ADDED
![]() |
Git LFS Details
|
app/static/results/de9bda87-c237-4c6b-8748-1ab245d4ec57_damage.png
ADDED
![]() |
Git LFS Details
|
app/static/results/de9bda87-c237-4c6b-8748-1ab245d4ec57_parts.png
ADDED
![]() |
Git LFS Details
|
app/static/results/f0a7c31c-d1cf-460a-abff-d32d76faa384_damage.png
ADDED
![]() |
Git LFS Details
|
app/static/results/f0a7c31c-d1cf-460a-abff-d32d76faa384_parts.png
ADDED
![]() |
Git LFS Details
|
app/static/results/fdfef36b-2942-4bc8-bb26-ad6aad32d471_parts.png
ADDED
![]() |
Git LFS Details
|
app/static/uploads/1d91cb91-46f0-4123-9b3d-dd963a4a1b8b.jpg
ADDED
![]() |
Git LFS Details
|
app/static/uploads/2b9a2716-12dc-4fec-8b1b-8a26375e4fd9.jpg
ADDED
![]() |
Git LFS Details
|
app/static/uploads/4ea85461-64cd-4c8b-8ef9-3c9635df076b.jpg
ADDED
![]() |
Git LFS Details
|
app/static/uploads/706f091d-f713-4f34-88b1-b3eaa5082483.jpg
ADDED
![]() |
Git LFS Details
|
app/static/uploads/86665944-f463-4be3-87be-2227b667ea4d.jpg
ADDED
![]() |
Git LFS Details
|
app/static/uploads/8f2e9c10-9e39-415b-b634-e9cacff08670.jpg
ADDED
![]() |
Git LFS Details
|
app/static/uploads/9be81a29-214a-48af-ba99-8cab54a56d66.jpg
ADDED
![]() |
Git LFS Details
|
app/static/uploads/db93084e-16d1-4448-b841-1ce1c0e1c201.jpg
ADDED
![]() |
Git LFS Details
|
app/static/uploads/de9bda87-c237-4c6b-8748-1ab245d4ec57.jpg
ADDED
![]() |
app/static/uploads/f0a7c31c-d1cf-460a-abff-d32d76faa384.jpg
ADDED
![]() |
app/static/uploads/fdfef36b-2942-4bc8-bb26-ad6aad32d471.jpg
ADDED
![]() |
app/templates/index.html
ADDED
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="en">
|
3 |
+
<head>
|
4 |
+
<meta charset="UTF-8">
|
5 |
+
<title>Car Damage Detection</title>
|
6 |
+
<link rel="stylesheet" href="/static/css/style.css">
|
7 |
+
<script>
|
8 |
+
function copyJSON() {
|
9 |
+
const pre = document.getElementById('json-pre');
|
10 |
+
if (pre) {
|
11 |
+
navigator.clipboard.writeText(pre.innerText);
|
12 |
+
alert('JSON copied to clipboard!');
|
13 |
+
}
|
14 |
+
}
|
15 |
+
function toggleJSON() {
|
16 |
+
const block = document.getElementById('json-block');
|
17 |
+
const btn = document.getElementById('toggle-json-btn');
|
18 |
+
if (block.style.display === 'none') {
|
19 |
+
block.style.display = 'block';
|
20 |
+
btn.innerText = 'Hide JSON';
|
21 |
+
} else {
|
22 |
+
block.style.display = 'none';
|
23 |
+
btn.innerText = 'Show JSON';
|
24 |
+
}
|
25 |
+
}
|
26 |
+
</script>
|
27 |
+
</head>
|
28 |
+
<body>
|
29 |
+
<div class="header" style="position:relative;">
|
30 |
+
<h2 style="margin:0;">Car Damage & Parts Detection</h2>
|
31 |
+
<img src="/static/img/logo.png" alt="Logo" class="logo" style="position:absolute; top:18px; right:32px; max-width:110px;">
|
32 |
+
</div>
|
33 |
+
<div class="container">
|
34 |
+
<form method="post" action="/upload" enctype="multipart/form-data">
|
35 |
+
<input type="file" name="file" accept="image/*" required>
|
36 |
+
<button type="submit">Upload & Analyze</button>
|
37 |
+
</form>
|
38 |
+
{% if error %}<div class="error">{{ error }}</div>{% endif %}
|
39 |
+
{% if result %}
|
40 |
+
<div class="results">
|
41 |
+
<h3>Results</h3>
|
42 |
+
<div class="images-row">
|
43 |
+
<div>
|
44 |
+
<div class="result-label">Original</div>
|
45 |
+
<img src="{{ original_image }}" alt="Original Image" class="result-img">
|
46 |
+
</div>
|
47 |
+
{% if result.damage_image is defined and result.damage_image %}
|
48 |
+
<div>
|
49 |
+
<div class="result-label">Damage Prediction</div>
|
50 |
+
<img src="{{ result.damage_image }}" alt="Damage Prediction" class="result-img">
|
51 |
+
</div>
|
52 |
+
{% endif %}
|
53 |
+
{% if result.parts_image is defined and result.parts_image %}
|
54 |
+
<div>
|
55 |
+
<div class="result-label">Parts Prediction</div>
|
56 |
+
<img src="{{ result.parts_image }}" alt="Parts Prediction" class="result-img">
|
57 |
+
</div>
|
58 |
+
{% endif %}
|
59 |
+
</div>
|
60 |
+
{% if result.json is defined and result.json %}
|
61 |
+
<h4 style="margin-bottom:8px;">JSON Output</h4>
|
62 |
+
<button class="copy-btn" onclick="copyJSON()" type="button">Copy JSON</button>
|
63 |
+
<button class="copy-btn" id="toggle-json-btn" onclick="toggleJSON()" type="button" style="margin-left:10px;">Show JSON</button>
|
64 |
+
<div id="json-block" style="display:none;">
|
65 |
+
<pre class="json-output" id="json-pre">{{ result.json | tojson(indent=2) }}</pre>
|
66 |
+
</div>
|
67 |
+
<a class="download-btn" href="{{ result.json_download }}" download>Download JSON</a>
|
68 |
+
{% endif %}
|
69 |
+
{% if result.damage_image is defined and result.damage_image %}
|
70 |
+
<a class="download-btn" href="{{ result.damage_image }}" download>Download Damage Image</a>
|
71 |
+
{% endif %}
|
72 |
+
{% if result.parts_image is defined and result.parts_image %}
|
73 |
+
<a class="download-btn" href="{{ result.parts_image }}" download>Download Parts Image</a>
|
74 |
+
{% endif %}
|
75 |
+
</div>
|
76 |
+
{% endif %}
|
77 |
+
</div>
|
78 |
+
<div class="footer">
|
79 |
+
© {{ 2025 }} RSA/Intact. All rights reserved.
|
80 |
+
</div>
|
81 |
+
</body>
|
82 |
+
</html>
|
app/templates/login.html
ADDED
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="en">
|
3 |
+
<head>
|
4 |
+
<meta charset="UTF-8">
|
5 |
+
<title>Login - Car Damage Detection</title>
|
6 |
+
<link rel="stylesheet" href="/static/css/style.css">
|
7 |
+
</head>
|
8 |
+
<body>
|
9 |
+
<div class="container">
|
10 |
+
<img src="/static/img/logo.png" alt="Logo" class="logo">
|
11 |
+
<h2>Login</h2>
|
12 |
+
{% if error %}<div class="error">{{ error }}</div>{% endif %}
|
13 |
+
<form method="post" action="/login">
|
14 |
+
<label for="username">Username:</label>
|
15 |
+
<input type="text" id="username" name="username" required><br>
|
16 |
+
<label for="password">Password:</label>
|
17 |
+
<input type="password" id="password" name="password" required><br>
|
18 |
+
<button type="submit">Login</button>
|
19 |
+
</form>
|
20 |
+
</div>
|
21 |
+
</body>
|
22 |
+
</html>
|
configs/damage_config.yaml
ADDED
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Example Detectron2 config for damage model (1 class: damage)
|
2 |
+
_BASE_: "COCO-InstanceSegmentation/mask_rcnn_R_50_FPN_3x.yaml"
|
3 |
+
MODEL:
|
4 |
+
ROI_HEADS:
|
5 |
+
NUM_CLASSES: 1
|
6 |
+
DATASETS:
|
7 |
+
TRAIN: ("damage_train",)
|
8 |
+
TEST: ("damage_val",)
|
9 |
+
SOLVER:
|
10 |
+
IMS_PER_BATCH: 2
|
11 |
+
BASE_LR: 0.00025
|
12 |
+
MAX_ITER: 3000
|
13 |
+
OUTPUT_DIR: "./weights/damage_model"
|
configs/parts_config.yaml
ADDED
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Example Detectron2 config for parts model (5 classes)
|
2 |
+
_BASE_: "COCO-InstanceSegmentation/mask_rcnn_R_50_FPN_3x.yaml"
|
3 |
+
MODEL:
|
4 |
+
ROI_HEADS:
|
5 |
+
NUM_CLASSES: 5
|
6 |
+
DATASETS:
|
7 |
+
TRAIN: ("parts_train",)
|
8 |
+
TEST: ("parts_val",)
|
9 |
+
SOLVER:
|
10 |
+
IMS_PER_BATCH: 2
|
11 |
+
BASE_LR: 0.00025
|
12 |
+
MAX_ITER: 3000
|
13 |
+
OUTPUT_DIR: "./weights/parts_model"
|
inference/damage_inference.py
ADDED
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Inference and visualization for YOLOv8 damage segmentation on unseen images
|
2 |
+
from ultralytics import YOLO
|
3 |
+
import os
|
4 |
+
from glob import glob
|
5 |
+
import sys
|
6 |
+
|
7 |
+
def run_inference(): # Get absolute paths
|
8 |
+
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
9 |
+
model_path = os.path.join(base_dir, 'models', 'damage', 'weights', 'weights', 'best.pt')
|
10 |
+
img_dir = os.path.join(base_dir, 'damage_detection_dataset', 'img')
|
11 |
+
out_dir = os.path.join(base_dir, 'inference_results', 'damage')
|
12 |
+
|
13 |
+
# Validate paths
|
14 |
+
if not os.path.exists(model_path):
|
15 |
+
print(f"Error: Model weights not found at {model_path}")
|
16 |
+
return
|
17 |
+
|
18 |
+
if not os.path.exists(img_dir):
|
19 |
+
print(f"Error: Image directory not found at {img_dir}")
|
20 |
+
return
|
21 |
+
|
22 |
+
# Create output directory
|
23 |
+
os.makedirs(out_dir, exist_ok=True)
|
24 |
+
|
25 |
+
# Get all images in the dataset
|
26 |
+
all_imgs = sorted(glob(os.path.join(img_dir, '*.jpg')))
|
27 |
+
if not all_imgs:
|
28 |
+
print(f"No images found in {img_dir}")
|
29 |
+
return
|
30 |
+
|
31 |
+
try:
|
32 |
+
# Load model
|
33 |
+
model = YOLO(model_path)
|
34 |
+
|
35 |
+
# Run inference and save results
|
36 |
+
for img_path in all_imgs:
|
37 |
+
try:
|
38 |
+
results = model.predict(
|
39 |
+
source=img_path,
|
40 |
+
save=True,
|
41 |
+
project=out_dir,
|
42 |
+
name='',
|
43 |
+
imgsz=640,
|
44 |
+
conf=0.25
|
45 |
+
)
|
46 |
+
print(f'Processed: {os.path.basename(img_path)}')
|
47 |
+
except Exception as e:
|
48 |
+
print(f"Error processing {os.path.basename(img_path)}: {str(e)}")
|
49 |
+
continue
|
50 |
+
|
51 |
+
print(f'Inference complete. Results saved to {out_dir}')
|
52 |
+
|
53 |
+
except Exception as e:
|
54 |
+
print(f"Error loading model: {str(e)}")
|
55 |
+
return
|
56 |
+
|
57 |
+
if __name__ == '__main__':
|
58 |
+
run_inference()
|
inference/parts_inference.py
ADDED
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Inference on unseen images for YOLOv8 parts segmentation
|
2 |
+
from ultralytics import YOLO
|
3 |
+
import os
|
4 |
+
from glob import glob
|
5 |
+
|
6 |
+
def run_inference(): # Get absolute paths
|
7 |
+
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
8 |
+
model_path = os.path.join(base_dir, 'models', 'parts', 'weights', 'weights', 'best.pt')
|
9 |
+
img_dir = os.path.join(base_dir, 'damage_detection_dataset', 'img')
|
10 |
+
train_dir = os.path.join(base_dir, 'data', 'data_yolo_for_training', 'car_parts_damage_dataset', 'images', 'train')
|
11 |
+
out_dir = os.path.join(base_dir, 'inference_results', 'parts')
|
12 |
+
|
13 |
+
# Validate paths
|
14 |
+
if not os.path.exists(model_path):
|
15 |
+
print(f"Error: Model weights not found at {model_path}")
|
16 |
+
return
|
17 |
+
|
18 |
+
if not os.path.exists(img_dir):
|
19 |
+
print(f"Error: Image directory not found at {img_dir}")
|
20 |
+
return
|
21 |
+
|
22 |
+
if not os.path.exists(train_dir):
|
23 |
+
print(f"Warning: Training directory not found at {train_dir}")
|
24 |
+
print("Will run inference on all images instead of just unseen ones")
|
25 |
+
train_imgs = set()
|
26 |
+
else:
|
27 |
+
# Get all images used for training
|
28 |
+
train_imgs = set(os.listdir(train_dir))
|
29 |
+
|
30 |
+
# Create output directory
|
31 |
+
os.makedirs(out_dir, exist_ok=True)
|
32 |
+
|
33 |
+
# Get all images in original dataset
|
34 |
+
all_imgs = set(os.listdir(img_dir))
|
35 |
+
# Select images not used in training
|
36 |
+
unseen_imgs = sorted(list(all_imgs - train_imgs))
|
37 |
+
|
38 |
+
if not unseen_imgs:
|
39 |
+
print(f"No images found for inference in {img_dir}")
|
40 |
+
return
|
41 |
+
|
42 |
+
try:
|
43 |
+
# Load model
|
44 |
+
model = YOLO(model_path)
|
45 |
+
|
46 |
+
# Class names for visualization
|
47 |
+
class_names = ['headlamp', 'front_bumper', 'hood', 'door', 'rear_bumper']
|
48 |
+
|
49 |
+
# Run inference on each unseen image
|
50 |
+
for img_name in unseen_imgs:
|
51 |
+
try:
|
52 |
+
img_path = os.path.join(img_dir, img_name)
|
53 |
+
results = model.predict(
|
54 |
+
source=img_path,
|
55 |
+
save=True,
|
56 |
+
project=out_dir,
|
57 |
+
name='',
|
58 |
+
imgsz=640,
|
59 |
+
conf=0.25,
|
60 |
+
classes=list(range(len(class_names))) # All classes
|
61 |
+
)
|
62 |
+
print(f'Processed: {img_name}')
|
63 |
+
except Exception as e:
|
64 |
+
print(f"Error processing {img_name}: {str(e)}")
|
65 |
+
continue
|
66 |
+
|
67 |
+
print(f'Inference complete. Results saved to {out_dir}')
|
68 |
+
|
69 |
+
except Exception as e:
|
70 |
+
print(f"Error loading model: {str(e)}")
|
71 |
+
return
|
72 |
+
|
73 |
+
if __name__ == '__main__':
|
74 |
+
run_inference()
|
requirements.txt
ADDED
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
ultralytics
|
2 |
+
opencv-python
|
3 |
+
numpy
|
4 |
+
matplotlib
|
5 |
+
fastapi
|
6 |
+
uvicorn[standard]
|
7 |
+
ultralytics
|
8 |
+
opencv-python
|
9 |
+
jinja2
|
10 |
+
starlette
|
11 |
+
requests
|