diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000000000000000000000000000000000..e7103dac248d5b58114d883db899181f5742a4b2 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,64 @@ +*.7z filter=lfs diff=lfs merge=lfs -text +*.arrow filter=lfs diff=lfs merge=lfs -text +*.bin filter=lfs diff=lfs merge=lfs -text +*.bz2 filter=lfs diff=lfs merge=lfs -text +*.ckpt filter=lfs diff=lfs merge=lfs -text +*.ftz filter=lfs diff=lfs merge=lfs -text +*.gz filter=lfs diff=lfs merge=lfs -text +*.h5 filter=lfs diff=lfs merge=lfs -text +*.joblib filter=lfs diff=lfs merge=lfs -text +*.lfs.* filter=lfs diff=lfs merge=lfs -text +*.mlmodel filter=lfs diff=lfs merge=lfs -text +*.model filter=lfs diff=lfs merge=lfs -text +*.msgpack filter=lfs diff=lfs merge=lfs -text +*.npy filter=lfs diff=lfs merge=lfs -text +*.npz filter=lfs diff=lfs merge=lfs -text +*.onnx filter=lfs diff=lfs merge=lfs -text +*.ot filter=lfs diff=lfs merge=lfs -text +*.parquet filter=lfs diff=lfs merge=lfs -text +*.pb filter=lfs diff=lfs merge=lfs -text +*.pickle filter=lfs diff=lfs merge=lfs -text +*.pkl filter=lfs diff=lfs merge=lfs -text +*.pt filter=lfs diff=lfs merge=lfs -text +*.pth filter=lfs diff=lfs merge=lfs -text +*.rar filter=lfs diff=lfs merge=lfs -text +*.safetensors filter=lfs diff=lfs merge=lfs -text +saved_model/**/* filter=lfs diff=lfs merge=lfs -text +*.tar.* filter=lfs diff=lfs merge=lfs -text +*.tar filter=lfs diff=lfs merge=lfs -text +*.tflite filter=lfs diff=lfs merge=lfs -text +*.tgz filter=lfs diff=lfs merge=lfs -text +*.wasm filter=lfs diff=lfs merge=lfs -text +*.xz filter=lfs diff=lfs merge=lfs -text +*.zip filter=lfs diff=lfs merge=lfs -text +*.zst filter=lfs diff=lfs merge=lfs -text +*tfevents* filter=lfs diff=lfs merge=lfs -text +app/static/results/1d91cb91-46f0-4123-9b3d-dd963a4a1b8b_damage.png filter=lfs diff=lfs merge=lfs -text +app/static/results/1d91cb91-46f0-4123-9b3d-dd963a4a1b8b_parts.png filter=lfs diff=lfs merge=lfs -text +app/static/results/2b9a2716-12dc-4fec-8b1b-8a26375e4fd9_damage.png filter=lfs diff=lfs merge=lfs -text +app/static/results/2b9a2716-12dc-4fec-8b1b-8a26375e4fd9_parts.png filter=lfs diff=lfs merge=lfs -text +app/static/results/4ea85461-64cd-4c8b-8ef9-3c9635df076b_damage.png filter=lfs diff=lfs merge=lfs -text +app/static/results/4ea85461-64cd-4c8b-8ef9-3c9635df076b_parts.png filter=lfs diff=lfs merge=lfs -text +app/static/results/706f091d-f713-4f34-88b1-b3eaa5082483_damage.png filter=lfs diff=lfs merge=lfs -text +app/static/results/706f091d-f713-4f34-88b1-b3eaa5082483_parts.png filter=lfs diff=lfs merge=lfs -text +app/static/results/86665944-f463-4be3-87be-2227b667ea4d_damage.png filter=lfs diff=lfs merge=lfs -text +app/static/results/86665944-f463-4be3-87be-2227b667ea4d_parts.png filter=lfs diff=lfs merge=lfs -text +app/static/results/8f2e9c10-9e39-415b-b634-e9cacff08670_damage.png filter=lfs diff=lfs merge=lfs -text +app/static/results/8f2e9c10-9e39-415b-b634-e9cacff08670_parts.png filter=lfs diff=lfs merge=lfs -text +app/static/results/9be81a29-214a-48af-ba99-8cab54a56d66_damage.png filter=lfs diff=lfs merge=lfs -text +app/static/results/9be81a29-214a-48af-ba99-8cab54a56d66_parts.png filter=lfs diff=lfs merge=lfs -text +app/static/results/db93084e-16d1-4448-b841-1ce1c0e1c201_damage.png filter=lfs diff=lfs merge=lfs -text +app/static/results/db93084e-16d1-4448-b841-1ce1c0e1c201_parts.png filter=lfs diff=lfs merge=lfs -text +app/static/results/de9bda87-c237-4c6b-8748-1ab245d4ec57_damage.png filter=lfs diff=lfs merge=lfs -text +app/static/results/de9bda87-c237-4c6b-8748-1ab245d4ec57_parts.png filter=lfs diff=lfs merge=lfs -text +app/static/results/f0a7c31c-d1cf-460a-abff-d32d76faa384_damage.png filter=lfs diff=lfs merge=lfs -text +app/static/results/f0a7c31c-d1cf-460a-abff-d32d76faa384_parts.png filter=lfs diff=lfs merge=lfs -text +app/static/results/fdfef36b-2942-4bc8-bb26-ad6aad32d471_parts.png filter=lfs diff=lfs merge=lfs -text +app/static/uploads/1d91cb91-46f0-4123-9b3d-dd963a4a1b8b.jpg filter=lfs diff=lfs merge=lfs -text +app/static/uploads/2b9a2716-12dc-4fec-8b1b-8a26375e4fd9.jpg filter=lfs diff=lfs merge=lfs -text +app/static/uploads/4ea85461-64cd-4c8b-8ef9-3c9635df076b.jpg filter=lfs diff=lfs merge=lfs -text +app/static/uploads/706f091d-f713-4f34-88b1-b3eaa5082483.jpg filter=lfs diff=lfs merge=lfs -text +app/static/uploads/86665944-f463-4be3-87be-2227b667ea4d.jpg filter=lfs diff=lfs merge=lfs -text +app/static/uploads/8f2e9c10-9e39-415b-b634-e9cacff08670.jpg filter=lfs diff=lfs merge=lfs -text +app/static/uploads/9be81a29-214a-48af-ba99-8cab54a56d66.jpg filter=lfs diff=lfs merge=lfs -text +app/static/uploads/db93084e-16d1-4448-b841-1ce1c0e1c201.jpg filter=lfs diff=lfs merge=lfs -text diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..ec033f2c6676a3f2d96d88fcc0638a0b3a7426db --- /dev/null +++ b/Dockerfile @@ -0,0 +1,24 @@ +FROM python:3.10-slim + +WORKDIR /code + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + build-essential \ + libgl1-mesa-glx \ + && rm -rf /var/lib/apt/lists/* + +# Install Python dependencies +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy app code +COPY . . + +# Expose port +EXPOSE 7860 + +ENV PATH="/root/.local/bin:$PATH" + +# Run FastAPI app with uvicorn +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860"] diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..fa4e1f0980c889e8d78c772799b3e1ea7bdce07e --- /dev/null +++ b/README.md @@ -0,0 +1,24 @@ +--- +title: Car Damage & Parts Detection +emoji: 🚗 +colorFrom: blue +colorTo: indigo +sdk: docker +app_file: app/main.py +pinned: false +--- + +# Car Damage & Parts Detection + +Upload a car image to detect damaged regions and parts using YOLOv8 models. + +## How to use +- Upload a car image. +- View annotated results and JSON output. + +## Deployment +This Space uses FastAPI and YOLOv8, running in a Docker container. + +## Model Weights +Model weights are downloaded at startup from public cloud links (not included in repo). + diff --git a/app/__pycache__/main.cpython-310.pyc b/app/__pycache__/main.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3835c8f9e42493609087384a36346e0828cf2521 Binary files /dev/null and b/app/__pycache__/main.cpython-310.pyc differ diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000000000000000000000000000000000000..07f95f02d4c984d3768aad3db63ecbe16d2b8b41 --- /dev/null +++ b/app/main.py @@ -0,0 +1,58 @@ + +# --- Model download logic (Hugging Face Hub) --- +import os +import requests + +def download_if_missing(url, dest): + if not os.path.exists(dest): + print(f"Downloading model from {url} to {dest}...") + os.makedirs(os.path.dirname(dest), exist_ok=True) + with requests.get(url, stream=True) as r: + r.raise_for_status() + with open(dest, "wb") as f: + for chunk in r.iter_content(chunk_size=8192): + f.write(chunk) + print("Download complete.") + +# Hugging Face direct download links +DAMAGE_MODEL_URL = "https://huggingface.co/AItoolstack/car_damage_detection/resolve/main/yolov8_models/damage/weights/weights/best.pt" +PARTS_MODEL_URL = "https://huggingface.co/AItoolstack/car_damage_detection/resolve/main/yolov8_models/parts/weights/weights/best.pt" + +DAMAGE_MODEL_PATH = os.path.join("models", "damage", "weights", "weights", "best.pt") +PARTS_MODEL_PATH = os.path.join("models", "parts", "weights", "weights", "best.pt") + +download_if_missing(DAMAGE_MODEL_URL, DAMAGE_MODEL_PATH) +download_if_missing(PARTS_MODEL_URL, PARTS_MODEL_PATH) + +from fastapi import FastAPI, File, UploadFile, BackgroundTasks +from fastapi.staticfiles import StaticFiles +from fastapi.templating import Jinja2Templates +from fastapi.responses import JSONResponse, HTMLResponse +from fastapi.middleware.cors import CORSMiddleware +import uvicorn +from datetime import datetime +import aiofiles +from pathlib import Path +import uuid +from app.routers import inference + +app = FastAPI(title="Car Damage Detection API") + +# CORS middleware +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# Mount static files +app.mount("/static", StaticFiles(directory="app/static"), name="static") +templates = Jinja2Templates(directory="app/templates") + +# Include routers +app.include_router(inference.router) + +if __name__ == "__main__": + uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True) diff --git a/app/routers/__pycache__/inference.cpython-310.pyc b/app/routers/__pycache__/inference.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b551f19cb632a0ffe4afd88d23a3474108bc691e Binary files /dev/null and b/app/routers/__pycache__/inference.cpython-310.pyc differ diff --git a/app/routers/inference.py b/app/routers/inference.py new file mode 100644 index 0000000000000000000000000000000000000000..c0c6d6de379f2786309781b9b7a3fc4dd8f4ff05 --- /dev/null +++ b/app/routers/inference.py @@ -0,0 +1,218 @@ + +from fastapi import APIRouter, Request, UploadFile, File, Form +from fastapi.responses import HTMLResponse, FileResponse, JSONResponse +from fastapi.templating import Jinja2Templates +from starlette.background import BackgroundTask +import shutil +import os +import uuid +from pathlib import Path +from typing import Optional +import json +import base64 +from ultralytics import YOLO +import cv2 +import numpy as np + + +# Templates directory +TEMPLATES_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "templates") +templates = Jinja2Templates(directory=TEMPLATES_DIR) + +router = APIRouter() + +UPLOAD_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "static", "uploads") +RESULTS_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "static", "results") + +os.makedirs(UPLOAD_DIR, exist_ok=True) +os.makedirs(RESULTS_DIR, exist_ok=True) + +ALLOWED_EXTENSIONS = {"jpg", "jpeg", "png", "tiff", "tif"} + +# Model paths +BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) +DAMAGE_MODEL_PATH = os.path.join(BASE_DIR, "models", "damage", "weights", "weights", "best.pt") +PARTS_MODEL_PATH = os.path.join(BASE_DIR, "models", "parts", "weights", "weights", "best.pt") + +# Class names for parts +PARTS_CLASS_NAMES = ['headlamp', 'front_bumper', 'hood', 'door', 'rear_bumper'] + +# Helper: Run YOLO inference and return results +def run_yolo_inference(model_path, image_path, task='segment'): + model = YOLO(model_path) + results = model.predict(source=image_path, imgsz=640, conf=0.25, save=False, task=task) + return results[0] + +# Helper: Draw masks and confidence on image +def draw_masks_and_conf(image_path, yolo_result, class_names=None): + img = cv2.imread(image_path) + overlay = img.copy() + out_img = img.copy() + colors = [(255,0,0), (0,255,0), (0,0,255), (255,255,0), (255,0,255), (0,255,255)] + for i, box in enumerate(yolo_result.boxes): + conf = float(box.conf[0]) + cls = int(box.cls[0]) + color = colors[cls % len(colors)] + # Draw bbox + x1, y1, x2, y2 = map(int, box.xyxy[0]) + cv2.rectangle(overlay, (x1, y1), (x2, y2), color, 2) + label = f"{class_names[cls] if class_names else 'damage'}: {conf:.2f}" + cv2.putText(overlay, label, (x1, y1-10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2) + # Draw mask if available + if hasattr(yolo_result, 'masks') and yolo_result.masks is not None: + mask = yolo_result.masks.data[i].cpu().numpy() + mask = (mask * 255).astype(np.uint8) + mask = cv2.resize(mask, (x2-x1, y2-y1)) + roi = overlay[y1:y2, x1:x2] + colored_mask = np.zeros_like(roi) + colored_mask[mask > 127] = color + overlay[y1:y2, x1:x2] = cv2.addWeighted(roi, 0.5, colored_mask, 0.5, 0) + out_img = cv2.addWeighted(overlay, 0.7, img, 0.3, 0) + return out_img + +# Helper: Generate JSON output +def generate_json_output(filename, damage_result, parts_result): + # Damage severity: use max confidence + severity_score = float(max([float(box.conf[0]) for box in damage_result.boxes], default=0)) + damage_regions = [] + for box in damage_result.boxes: + x1, y1, x2, y2 = map(float, box.xyxy[0]) + conf = float(box.conf[0]) + damage_regions.append({"bbox": [x1, y1, x2, y2], "confidence": conf}) + # Parts + parts = [] + for i, box in enumerate(parts_result.boxes): + x1, y1, x2, y2 = map(float, box.xyxy[0]) + conf = float(box.conf[0]) + cls = int(box.cls[0]) + # Damage %: use mask area / bbox area if available + damage_percentage = None + if hasattr(parts_result, 'masks') and parts_result.masks is not None: + mask = parts_result.masks.data[i].cpu().numpy() + mask_area = np.sum(mask > 0.5) + bbox_area = (x2-x1)*(y2-y1) + damage_percentage = float(mask_area / bbox_area) if bbox_area > 0 else None + parts.append({ + "part": PARTS_CLASS_NAMES[cls] if cls < len(PARTS_CLASS_NAMES) else str(cls), + "damaged": True, + "confidence": conf, + "damage_percentage": damage_percentage, + "bbox": [x1, y1, x2, y2] + }) + # Optionally, add base64 masks + # (not implemented here for brevity) + return { + "filename": filename, + "damage": { + "severity_score": severity_score, + "regions": damage_regions + }, + "parts": parts, + "cost_estimate": None + } + +# Dummy login credentials +def check_login(username: str, password: str) -> bool: + return username == "demo" and password == "demo123" + +@router.get("/", response_class=HTMLResponse) +def home(request: Request): + return templates.TemplateResponse("index.html", {"request": request, "result": None}) + +@router.post("/login", response_class=HTMLResponse) +def login(request: Request, username: str = Form(...), password: str = Form(...)): + if check_login(username, password): + return templates.TemplateResponse("index.html", {"request": request, "result": None, "user": username}) + return templates.TemplateResponse("login.html", {"request": request, "error": "Invalid credentials"}) + +@router.get("/login", response_class=HTMLResponse) +def login_page(request: Request): + return templates.TemplateResponse("login.html", {"request": request}) + +@router.post("/upload", response_class=HTMLResponse) +def upload_image(request: Request, file: UploadFile = File(...)): + ext = file.filename.split(".")[-1].lower() + if ext not in ALLOWED_EXTENSIONS: + return templates.TemplateResponse("index.html", {"request": request, "error": "Unsupported file type."}) + + # Save uploaded file + session_id = str(uuid.uuid4()) + upload_path = os.path.join(UPLOAD_DIR, f"{session_id}.{ext}") + with open(upload_path, "wb") as buffer: + shutil.copyfileobj(file.file, buffer) + + # Run both inferences + try: + damage_result = run_yolo_inference(DAMAGE_MODEL_PATH, upload_path) + parts_result = run_yolo_inference(PARTS_MODEL_PATH, upload_path) + + # Save annotated images + damage_img_path = os.path.join(RESULTS_DIR, f"{session_id}_damage.png") + parts_img_path = os.path.join(RESULTS_DIR, f"{session_id}_parts.png") + json_path = os.path.join(RESULTS_DIR, f"{session_id}_result.json") + damage_img_url = f"/static/results/{session_id}_damage.png" + parts_img_url = f"/static/results/{session_id}_parts.png" + json_url = f"/static/results/{session_id}_result.json" + + # Defensive: set to None by default + damage_img = None + parts_img = None + json_output = None + + # Only save and set if inference returns boxes + if hasattr(damage_result, 'boxes') and len(damage_result.boxes) > 0: + damage_img = draw_masks_and_conf(upload_path, damage_result) + cv2.imwrite(damage_img_path, damage_img) + if hasattr(parts_result, 'boxes') and len(parts_result.boxes) > 0: + parts_img = draw_masks_and_conf(upload_path, parts_result, class_names=PARTS_CLASS_NAMES) + cv2.imwrite(parts_img_path, parts_img) + if (hasattr(damage_result, 'boxes') and len(damage_result.boxes) > 0) or (hasattr(parts_result, 'boxes') and len(parts_result.boxes) > 0): + json_output = generate_json_output(file.filename, damage_result, parts_result) + with open(json_path, "w") as jf: + json.dump(json_output, jf, indent=2) + + # Prepare URLs for download (only if files exist) + result = { + "filename": file.filename, + "damage_image": damage_img_url if damage_img is not None else None, + "parts_image": parts_img_url if parts_img is not None else None, + "json": json_output, + "json_download": json_url if json_output is not None else None + } + # Debug log + print("[DEBUG] Result dict:", result) + except Exception as e: + result = { + "filename": file.filename, + "error": f"Inference failed: {str(e)}", + "damage_image": None, + "parts_image": None, + "json": None, + "json_download": None + } + print("[ERROR] Inference failed:", e) + + import threading + import time + def delayed_cleanup(): + time.sleep(300) # 5 minutes + try: + os.remove(upload_path) + except Exception: + pass + for suffix in ["_damage.png", "_parts.png", "_result.json"]: + try: + os.remove(os.path.join(RESULTS_DIR, f"{session_id}{suffix}")) + except Exception: + pass + + threading.Thread(target=delayed_cleanup, daemon=True).start() + + return templates.TemplateResponse( + "index.html", + { + "request": request, + "result": result, + "original_image": f"/static/uploads/{session_id}.{ext}" + } + ) diff --git a/app/static/css/style.css b/app/static/css/style.css new file mode 100644 index 0000000000000000000000000000000000000000..6d5e7cc7ab2b7868ad36c76754477800443e36ba --- /dev/null +++ b/app/static/css/style.css @@ -0,0 +1,140 @@ +@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap'); + +body { + font-family: 'Roboto', Arial, sans-serif; + background: #f4f6fa; + margin: 0; + padding: 0; +} +.header { + background: linear-gradient(90deg, #003366 60%, #e30613 100%); + color: #fff; + padding: 24px 0 12px 0; + text-align: center; + box-shadow: 0 2px 8px #00336622; +} +.logo { + max-width: 180px; + margin-bottom: 10px; +} +@media (max-width: 600px) { + .logo { + max-width: 90px !important; + right: 10px !important; + top: 10px !important; + } + .header h2 { font-size: 1.1em; } +} +.container { + max-width: 950px; + margin: 40px auto 0 auto; + background: #fff; + padding: 32px 32px 24px 32px; + border-radius: 12px; + box-shadow: 0 4px 24px #00336622; +} +h2, h3, h4 { + color: #003366; + margin-top: 0; +} +form { + margin-bottom: 24px; + display: flex; + flex-direction: column; + align-items: center; +} +input[type="file"] { + margin-bottom: 16px; + font-size: 16px; +} +button { + background: #e30613; + color: #fff; + border: none; + padding: 12px 32px; + border-radius: 6px; + font-size: 16px; + font-weight: 700; + cursor: pointer; + transition: background 0.2s; + margin-bottom: 10px; +} +button:hover { + background: #003366; +} +.error { + color: #e30613; + margin-bottom: 16px; + font-weight: 700; +} +.images-row { + display: flex; + gap: 32px; + margin-bottom: 24px; + justify-content: center; + flex-wrap: wrap; +} +.result-img { + max-width: 260px; + border: 2px solid #e30613; + border-radius: 8px; + box-shadow: 0 2px 8px #00336622; + margin-bottom: 8px; +} +.result-label { + text-align: center; + font-weight: 700; + color: #003366; + margin-bottom: 8px; +} +.download-btn { + display: inline-block; + background: #003366; + color: #fff; + padding: 8px 18px; + border-radius: 5px; + text-decoration: none; + font-weight: 700; + margin-right: 10px; + margin-bottom: 10px; + transition: background 0.2s; +} +.download-btn:hover { + background: #e30613; +} +pre.json-output { + background: #f0f0f0; + padding: 16px; + border-radius: 6px; + font-size: 15px; + overflow-x: auto; + color: #222; + margin-bottom: 12px; + box-shadow: 0 1px 4px #00336611; +} +.copy-btn { + background: #e30613; + color: #fff; + border: none; + border-radius: 4px; + padding: 6px 14px; + font-size: 14px; + font-weight: 700; + cursor: pointer; + margin-bottom: 10px; + float: right; +} +.copy-btn:hover { + background: #003366; +} +.footer { + text-align: center; + color: #888; + font-size: 15px; + margin: 40px 0 10px 0; +} +@media (max-width: 900px) { + .container { padding: 16px; } + .images-row { gap: 12px; } + .result-img { max-width: 98vw; } +} diff --git a/app/static/img/logo.png b/app/static/img/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..4405f1706410bd8243ece5e39dfbd9f0a953cc5e Binary files /dev/null and b/app/static/img/logo.png differ diff --git a/app/static/results/1d91cb91-46f0-4123-9b3d-dd963a4a1b8b_damage.png b/app/static/results/1d91cb91-46f0-4123-9b3d-dd963a4a1b8b_damage.png new file mode 100644 index 0000000000000000000000000000000000000000..e0099add0438da4480c5a1e2515b0914bbb3a2ac --- /dev/null +++ b/app/static/results/1d91cb91-46f0-4123-9b3d-dd963a4a1b8b_damage.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:843234317ec8027fee1329fb191590a6523e6645fafa0717f71b267e6d97edf1 +size 1366857 diff --git a/app/static/results/1d91cb91-46f0-4123-9b3d-dd963a4a1b8b_parts.png b/app/static/results/1d91cb91-46f0-4123-9b3d-dd963a4a1b8b_parts.png new file mode 100644 index 0000000000000000000000000000000000000000..f765436de876551024ea307d36f6749862336d80 --- /dev/null +++ b/app/static/results/1d91cb91-46f0-4123-9b3d-dd963a4a1b8b_parts.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7736748ab74a20eb2049e04494b66dc453b0b49d4cd41b3a7990447e6aee37d4 +size 1286254 diff --git a/app/static/results/2b9a2716-12dc-4fec-8b1b-8a26375e4fd9_damage.png b/app/static/results/2b9a2716-12dc-4fec-8b1b-8a26375e4fd9_damage.png new file mode 100644 index 0000000000000000000000000000000000000000..e0099add0438da4480c5a1e2515b0914bbb3a2ac --- /dev/null +++ b/app/static/results/2b9a2716-12dc-4fec-8b1b-8a26375e4fd9_damage.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:843234317ec8027fee1329fb191590a6523e6645fafa0717f71b267e6d97edf1 +size 1366857 diff --git a/app/static/results/2b9a2716-12dc-4fec-8b1b-8a26375e4fd9_parts.png b/app/static/results/2b9a2716-12dc-4fec-8b1b-8a26375e4fd9_parts.png new file mode 100644 index 0000000000000000000000000000000000000000..f765436de876551024ea307d36f6749862336d80 --- /dev/null +++ b/app/static/results/2b9a2716-12dc-4fec-8b1b-8a26375e4fd9_parts.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7736748ab74a20eb2049e04494b66dc453b0b49d4cd41b3a7990447e6aee37d4 +size 1286254 diff --git a/app/static/results/4ea85461-64cd-4c8b-8ef9-3c9635df076b_damage.png b/app/static/results/4ea85461-64cd-4c8b-8ef9-3c9635df076b_damage.png new file mode 100644 index 0000000000000000000000000000000000000000..7c0f03b3957d6858277f0ed4ac4fc8b82ec38863 --- /dev/null +++ b/app/static/results/4ea85461-64cd-4c8b-8ef9-3c9635df076b_damage.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2a3fa666110fa57240a3e356a4bbe258b03cde0d2027db12158ef65775504e57 +size 1436867 diff --git a/app/static/results/4ea85461-64cd-4c8b-8ef9-3c9635df076b_parts.png b/app/static/results/4ea85461-64cd-4c8b-8ef9-3c9635df076b_parts.png new file mode 100644 index 0000000000000000000000000000000000000000..f60bdf189f2dc82e338c3b817f51fb8926085108 --- /dev/null +++ b/app/static/results/4ea85461-64cd-4c8b-8ef9-3c9635df076b_parts.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8bdcf896c24e952df34be4247e25dd2ab2f306cfb78f2d5d3bcd1a22c52cb3b6 +size 1414241 diff --git a/app/static/results/4ea85461-64cd-4c8b-8ef9-3c9635df076b_result.json b/app/static/results/4ea85461-64cd-4c8b-8ef9-3c9635df076b_result.json new file mode 100644 index 0000000000000000000000000000000000000000..704c3f838842a502ef1aae3a50dcb4d834afe1a8 --- /dev/null +++ b/app/static/results/4ea85461-64cd-4c8b-8ef9-3c9635df076b_result.json @@ -0,0 +1,86 @@ +{ + "filename": "28.jpg", + "damage": { + "severity_score": 0.7707259058952332, + "regions": [ + { + "bbox": [ + 522.2720947265625, + 530.5602416992188, + 728.9972534179688, + 892.2079467773438 + ], + "confidence": 0.7707259058952332 + }, + { + "bbox": [ + 660.1974487304688, + 512.72119140625, + 927.4169921875, + 883.8523559570312 + ], + "confidence": 0.6494596600532532 + }, + { + "bbox": [ + 196.1370086669922, + 277.27374267578125, + 366.7127990722656, + 712.44775390625 + ], + "confidence": 0.32863757014274597 + } + ] + }, + "parts": [ + { + "part": "hood", + "damaged": true, + "confidence": 0.9392628073692322, + "damage_percentage": 0.20169149219339333, + "bbox": [ + 411.4404296875, + 342.9278869628906, + 952.9636840820312, + 578.835693359375 + ] + }, + { + "part": "headlamp", + "damaged": true, + "confidence": 0.9351847171783447, + "damage_percentage": 1.7647002993974346, + "bbox": [ + 531.9706420898438, + 553.8896484375, + 706.9000854492188, + 656.3943481445312 + ] + }, + { + "part": "door", + "damaged": true, + "confidence": 0.9011728763580322, + "damage_percentage": 0.29901854346557094, + "bbox": [ + 143.66470336914062, + 224.3675537109375, + 391.3798522949219, + 737.7504272460938 + ] + }, + { + "part": "front_bumper", + "damaged": true, + "confidence": 0.8590587377548218, + "damage_percentage": 0.03409931158993679, + "bbox": [ + 461.2880859375, + 534.1759033203125, + 978.0007934570312, + 874.1959838867188 + ] + } + ], + "cost_estimate": null +} \ No newline at end of file diff --git a/app/static/results/706f091d-f713-4f34-88b1-b3eaa5082483_damage.png b/app/static/results/706f091d-f713-4f34-88b1-b3eaa5082483_damage.png new file mode 100644 index 0000000000000000000000000000000000000000..7f72dc88d9ec70466457b100df4059d5a9a9bbbb --- /dev/null +++ b/app/static/results/706f091d-f713-4f34-88b1-b3eaa5082483_damage.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:864160d73f14a642d0c79c1d5583bc7bbd5783b7fd1a7651cb10b6b404bbc656 +size 1369802 diff --git a/app/static/results/706f091d-f713-4f34-88b1-b3eaa5082483_parts.png b/app/static/results/706f091d-f713-4f34-88b1-b3eaa5082483_parts.png new file mode 100644 index 0000000000000000000000000000000000000000..1ec8f6235bbe77aed73a68b5f433d300dd9017e3 --- /dev/null +++ b/app/static/results/706f091d-f713-4f34-88b1-b3eaa5082483_parts.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:434038d40e7c0ea3b9ad891c20f4909537d66bd728dc831dce8fe805390c4669 +size 1326584 diff --git a/app/static/results/86665944-f463-4be3-87be-2227b667ea4d_damage.png b/app/static/results/86665944-f463-4be3-87be-2227b667ea4d_damage.png new file mode 100644 index 0000000000000000000000000000000000000000..e0099add0438da4480c5a1e2515b0914bbb3a2ac --- /dev/null +++ b/app/static/results/86665944-f463-4be3-87be-2227b667ea4d_damage.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:843234317ec8027fee1329fb191590a6523e6645fafa0717f71b267e6d97edf1 +size 1366857 diff --git a/app/static/results/86665944-f463-4be3-87be-2227b667ea4d_parts.png b/app/static/results/86665944-f463-4be3-87be-2227b667ea4d_parts.png new file mode 100644 index 0000000000000000000000000000000000000000..f765436de876551024ea307d36f6749862336d80 --- /dev/null +++ b/app/static/results/86665944-f463-4be3-87be-2227b667ea4d_parts.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7736748ab74a20eb2049e04494b66dc453b0b49d4cd41b3a7990447e6aee37d4 +size 1286254 diff --git a/app/static/results/8f2e9c10-9e39-415b-b634-e9cacff08670_damage.png b/app/static/results/8f2e9c10-9e39-415b-b634-e9cacff08670_damage.png new file mode 100644 index 0000000000000000000000000000000000000000..7f72dc88d9ec70466457b100df4059d5a9a9bbbb --- /dev/null +++ b/app/static/results/8f2e9c10-9e39-415b-b634-e9cacff08670_damage.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:864160d73f14a642d0c79c1d5583bc7bbd5783b7fd1a7651cb10b6b404bbc656 +size 1369802 diff --git a/app/static/results/8f2e9c10-9e39-415b-b634-e9cacff08670_parts.png b/app/static/results/8f2e9c10-9e39-415b-b634-e9cacff08670_parts.png new file mode 100644 index 0000000000000000000000000000000000000000..1ec8f6235bbe77aed73a68b5f433d300dd9017e3 --- /dev/null +++ b/app/static/results/8f2e9c10-9e39-415b-b634-e9cacff08670_parts.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:434038d40e7c0ea3b9ad891c20f4909537d66bd728dc831dce8fe805390c4669 +size 1326584 diff --git a/app/static/results/9be81a29-214a-48af-ba99-8cab54a56d66_damage.png b/app/static/results/9be81a29-214a-48af-ba99-8cab54a56d66_damage.png new file mode 100644 index 0000000000000000000000000000000000000000..e883b086cb081f2fdae0802efe551e201d3e26e6 --- /dev/null +++ b/app/static/results/9be81a29-214a-48af-ba99-8cab54a56d66_damage.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11364615cb173dfae5739657f8ecca4f1b550e82a8565a8347b52c20fed3c134 +size 1425181 diff --git a/app/static/results/9be81a29-214a-48af-ba99-8cab54a56d66_parts.png b/app/static/results/9be81a29-214a-48af-ba99-8cab54a56d66_parts.png new file mode 100644 index 0000000000000000000000000000000000000000..5d232cba017c3b67a6c0d9d73d546c92abec9dd2 --- /dev/null +++ b/app/static/results/9be81a29-214a-48af-ba99-8cab54a56d66_parts.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9076be5837c8bc5b361a12ada76a5f5088ddab7b5bbed8812e7cf8f6108199b1 +size 1407342 diff --git a/app/static/results/9be81a29-214a-48af-ba99-8cab54a56d66_result.json b/app/static/results/9be81a29-214a-48af-ba99-8cab54a56d66_result.json new file mode 100644 index 0000000000000000000000000000000000000000..b8a2e07664a9dc64e69f16ccac800d4b9a2c3834 --- /dev/null +++ b/app/static/results/9be81a29-214a-48af-ba99-8cab54a56d66_result.json @@ -0,0 +1,83 @@ +{ + "filename": "72.jpg", + "damage": { + "severity_score": 0.6467820405960083, + "regions": [ + { + "bbox": [ + 20.340744018554688, + 93.4926986694336, + 112.2962646484375, + 271.3760986328125 + ], + "confidence": 0.6467820405960083 + }, + { + "bbox": [ + 129.6874237060547, + 107.35161590576172, + 238.290771484375, + 377.91192626953125 + ], + "confidence": 0.43937909603118896 + }, + { + "bbox": [ + 127.2110595703125, + 23.03559684753418, + 353.25946044921875, + 420.0022888183594 + ], + "confidence": 0.3795366883277893 + }, + { + "bbox": [ + 126.8061752319336, + 38.61054611206055, + 242.341064453125, + 433.29034423828125 + ], + "confidence": 0.3542208969593048 + } + ] + }, + "parts": [ + { + "part": "hood", + "damaged": true, + "confidence": 0.746218204498291, + "damage_percentage": 0.2284878057871993, + "bbox": [ + 1.409912109375, + 666.6517944335938, + 200.2979278564453, + 1020.8939819335938 + ] + }, + { + "part": "hood", + "damaged": true, + "confidence": 0.5614964962005615, + "damage_percentage": 0.04930226900457474, + "bbox": [ + 265.2085876464844, + 582.6806030273438, + 1017.8049926757812, + 1016.5338745117188 + ] + }, + { + "part": "hood", + "damaged": true, + "confidence": 0.5332340598106384, + "damage_percentage": 0.25070920790460405, + "bbox": [ + 0.0, + 501.2841796875, + 358.5841369628906, + 680.34912109375 + ] + } + ], + "cost_estimate": null +} \ No newline at end of file diff --git a/app/static/results/db93084e-16d1-4448-b841-1ce1c0e1c201_damage.png b/app/static/results/db93084e-16d1-4448-b841-1ce1c0e1c201_damage.png new file mode 100644 index 0000000000000000000000000000000000000000..e0099add0438da4480c5a1e2515b0914bbb3a2ac --- /dev/null +++ b/app/static/results/db93084e-16d1-4448-b841-1ce1c0e1c201_damage.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:843234317ec8027fee1329fb191590a6523e6645fafa0717f71b267e6d97edf1 +size 1366857 diff --git a/app/static/results/db93084e-16d1-4448-b841-1ce1c0e1c201_parts.png b/app/static/results/db93084e-16d1-4448-b841-1ce1c0e1c201_parts.png new file mode 100644 index 0000000000000000000000000000000000000000..f765436de876551024ea307d36f6749862336d80 --- /dev/null +++ b/app/static/results/db93084e-16d1-4448-b841-1ce1c0e1c201_parts.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7736748ab74a20eb2049e04494b66dc453b0b49d4cd41b3a7990447e6aee37d4 +size 1286254 diff --git a/app/static/results/de9bda87-c237-4c6b-8748-1ab245d4ec57_damage.png b/app/static/results/de9bda87-c237-4c6b-8748-1ab245d4ec57_damage.png new file mode 100644 index 0000000000000000000000000000000000000000..20c4ec21db3dbe4fd18e310ae881470052ff106b --- /dev/null +++ b/app/static/results/de9bda87-c237-4c6b-8748-1ab245d4ec57_damage.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3ce4e1711a1354f0bc289fa81c2fca85f95ba8ab3c97b85dda4977405d7aaadc +size 1317902 diff --git a/app/static/results/de9bda87-c237-4c6b-8748-1ab245d4ec57_parts.png b/app/static/results/de9bda87-c237-4c6b-8748-1ab245d4ec57_parts.png new file mode 100644 index 0000000000000000000000000000000000000000..7046762650a9c10ff7cd8f4a27d0b708424581bc --- /dev/null +++ b/app/static/results/de9bda87-c237-4c6b-8748-1ab245d4ec57_parts.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5e430bf68e1de01b04208fd84d049e8bebf1199d5f2caa3ae0bebdb2be2938e8 +size 1175140 diff --git a/app/static/results/f0a7c31c-d1cf-460a-abff-d32d76faa384_damage.png b/app/static/results/f0a7c31c-d1cf-460a-abff-d32d76faa384_damage.png new file mode 100644 index 0000000000000000000000000000000000000000..f24c5bd94b0fa0b6be8fba731c8aab03009878c8 --- /dev/null +++ b/app/static/results/f0a7c31c-d1cf-460a-abff-d32d76faa384_damage.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a2257fb2db1401111cc2c0d08d0a16ed7d5796983155660c33ea8c895174fbc0 +size 624913 diff --git a/app/static/results/f0a7c31c-d1cf-460a-abff-d32d76faa384_parts.png b/app/static/results/f0a7c31c-d1cf-460a-abff-d32d76faa384_parts.png new file mode 100644 index 0000000000000000000000000000000000000000..e881e58190de8b68c28c691dec5fd5376d4b5b33 --- /dev/null +++ b/app/static/results/f0a7c31c-d1cf-460a-abff-d32d76faa384_parts.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2193c3f71bc942286682aebacc32ac19458f2431f5347a5d947472fa55b9e0a6 +size 628654 diff --git a/app/static/results/fdfef36b-2942-4bc8-bb26-ad6aad32d471_parts.png b/app/static/results/fdfef36b-2942-4bc8-bb26-ad6aad32d471_parts.png new file mode 100644 index 0000000000000000000000000000000000000000..6155784c90add9a7405898864274feb98dc1ffc0 --- /dev/null +++ b/app/static/results/fdfef36b-2942-4bc8-bb26-ad6aad32d471_parts.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b4e32a45e5c02e186b8a977686ac356e58460425e5ab9abb84973d1f82010d39 +size 920202 diff --git a/app/static/uploads/1d91cb91-46f0-4123-9b3d-dd963a4a1b8b.jpg b/app/static/uploads/1d91cb91-46f0-4123-9b3d-dd963a4a1b8b.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4885d03fce37826d9e36f0b8766183bd2c71b473 --- /dev/null +++ b/app/static/uploads/1d91cb91-46f0-4123-9b3d-dd963a4a1b8b.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:092aba449d902c7e88a7cd2b4b588f584dbb71ec46d3f1ae6610caced7da72a6 +size 111180 diff --git a/app/static/uploads/2b9a2716-12dc-4fec-8b1b-8a26375e4fd9.jpg b/app/static/uploads/2b9a2716-12dc-4fec-8b1b-8a26375e4fd9.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4885d03fce37826d9e36f0b8766183bd2c71b473 --- /dev/null +++ b/app/static/uploads/2b9a2716-12dc-4fec-8b1b-8a26375e4fd9.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:092aba449d902c7e88a7cd2b4b588f584dbb71ec46d3f1ae6610caced7da72a6 +size 111180 diff --git a/app/static/uploads/4ea85461-64cd-4c8b-8ef9-3c9635df076b.jpg b/app/static/uploads/4ea85461-64cd-4c8b-8ef9-3c9635df076b.jpg new file mode 100644 index 0000000000000000000000000000000000000000..319c333be5e8e895f9c28363154141a3a17b02c5 --- /dev/null +++ b/app/static/uploads/4ea85461-64cd-4c8b-8ef9-3c9635df076b.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9727abba3fdd97eeb17e763d77479943b88f9c6d72185d919ae852b5f80e8837 +size 104751 diff --git a/app/static/uploads/706f091d-f713-4f34-88b1-b3eaa5082483.jpg b/app/static/uploads/706f091d-f713-4f34-88b1-b3eaa5082483.jpg new file mode 100644 index 0000000000000000000000000000000000000000..34cdc6eb2e01407c0262fbc1d5afe93d9bbca0e1 --- /dev/null +++ b/app/static/uploads/706f091d-f713-4f34-88b1-b3eaa5082483.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dd09285677be22be4bc70a81ab947db3745875c585b6a9339ad82e07dd733c74 +size 107319 diff --git a/app/static/uploads/86665944-f463-4be3-87be-2227b667ea4d.jpg b/app/static/uploads/86665944-f463-4be3-87be-2227b667ea4d.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4885d03fce37826d9e36f0b8766183bd2c71b473 --- /dev/null +++ b/app/static/uploads/86665944-f463-4be3-87be-2227b667ea4d.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:092aba449d902c7e88a7cd2b4b588f584dbb71ec46d3f1ae6610caced7da72a6 +size 111180 diff --git a/app/static/uploads/8f2e9c10-9e39-415b-b634-e9cacff08670.jpg b/app/static/uploads/8f2e9c10-9e39-415b-b634-e9cacff08670.jpg new file mode 100644 index 0000000000000000000000000000000000000000..34cdc6eb2e01407c0262fbc1d5afe93d9bbca0e1 --- /dev/null +++ b/app/static/uploads/8f2e9c10-9e39-415b-b634-e9cacff08670.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dd09285677be22be4bc70a81ab947db3745875c585b6a9339ad82e07dd733c74 +size 107319 diff --git a/app/static/uploads/9be81a29-214a-48af-ba99-8cab54a56d66.jpg b/app/static/uploads/9be81a29-214a-48af-ba99-8cab54a56d66.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d9d45034fcec0dbb9c3bc4fbdf0f5ac5dbdedc51 --- /dev/null +++ b/app/static/uploads/9be81a29-214a-48af-ba99-8cab54a56d66.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:30936869771053b22752ec10a1c339e40e43061291d8dce60586093dbb87a692 +size 116112 diff --git a/app/static/uploads/db93084e-16d1-4448-b841-1ce1c0e1c201.jpg b/app/static/uploads/db93084e-16d1-4448-b841-1ce1c0e1c201.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4885d03fce37826d9e36f0b8766183bd2c71b473 --- /dev/null +++ b/app/static/uploads/db93084e-16d1-4448-b841-1ce1c0e1c201.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:092aba449d902c7e88a7cd2b4b588f584dbb71ec46d3f1ae6610caced7da72a6 +size 111180 diff --git a/app/static/uploads/de9bda87-c237-4c6b-8748-1ab245d4ec57.jpg b/app/static/uploads/de9bda87-c237-4c6b-8748-1ab245d4ec57.jpg new file mode 100644 index 0000000000000000000000000000000000000000..aeac59a2d006afbf62b78b7fd4e3fbbbebdd4a51 Binary files /dev/null and b/app/static/uploads/de9bda87-c237-4c6b-8748-1ab245d4ec57.jpg differ diff --git a/app/static/uploads/f0a7c31c-d1cf-460a-abff-d32d76faa384.jpg b/app/static/uploads/f0a7c31c-d1cf-460a-abff-d32d76faa384.jpg new file mode 100644 index 0000000000000000000000000000000000000000..732da4aeaa5c496444193a3ed4f817dcee716337 Binary files /dev/null and b/app/static/uploads/f0a7c31c-d1cf-460a-abff-d32d76faa384.jpg differ diff --git a/app/static/uploads/fdfef36b-2942-4bc8-bb26-ad6aad32d471.jpg b/app/static/uploads/fdfef36b-2942-4bc8-bb26-ad6aad32d471.jpg new file mode 100644 index 0000000000000000000000000000000000000000..792589945d9465f8056de27ac8bfc328fb73a269 Binary files /dev/null and b/app/static/uploads/fdfef36b-2942-4bc8-bb26-ad6aad32d471.jpg differ diff --git a/app/templates/index.html b/app/templates/index.html new file mode 100644 index 0000000000000000000000000000000000000000..db27280cba691fe37e2b453a9983ebe1faf15aea --- /dev/null +++ b/app/templates/index.html @@ -0,0 +1,82 @@ + + + + + Car Damage Detection + + + + +
+

Car Damage & Parts Detection

+ +
+
+
+ + +
+ {% if error %}
{{ error }}
{% endif %} + {% if result %} +
+

Results

+
+
+
Original
+ Original Image +
+ {% if result.damage_image is defined and result.damage_image %} +
+
Damage Prediction
+ Damage Prediction +
+ {% endif %} + {% if result.parts_image is defined and result.parts_image %} +
+
Parts Prediction
+ Parts Prediction +
+ {% endif %} +
+ {% if result.json is defined and result.json %} +

JSON Output

+ + + + Download JSON + {% endif %} + {% if result.damage_image is defined and result.damage_image %} + Download Damage Image + {% endif %} + {% if result.parts_image is defined and result.parts_image %} + Download Parts Image + {% endif %} +
+ {% endif %} +
+ + + diff --git a/app/templates/login.html b/app/templates/login.html new file mode 100644 index 0000000000000000000000000000000000000000..745fd8a2d787d83ea94096eef20d275f3d038139 --- /dev/null +++ b/app/templates/login.html @@ -0,0 +1,22 @@ + + + + + Login - Car Damage Detection + + + +
+ +

Login

+ {% if error %}
{{ error }}
{% endif %} +
+ +
+ +
+ +
+
+ + diff --git a/configs/damage_config.yaml b/configs/damage_config.yaml new file mode 100644 index 0000000000000000000000000000000000000000..36adbb2c58854191ae689b258539aba94f59b0ab --- /dev/null +++ b/configs/damage_config.yaml @@ -0,0 +1,13 @@ +# Example Detectron2 config for damage model (1 class: damage) +_BASE_: "COCO-InstanceSegmentation/mask_rcnn_R_50_FPN_3x.yaml" +MODEL: + ROI_HEADS: + NUM_CLASSES: 1 +DATASETS: + TRAIN: ("damage_train",) + TEST: ("damage_val",) +SOLVER: + IMS_PER_BATCH: 2 + BASE_LR: 0.00025 + MAX_ITER: 3000 +OUTPUT_DIR: "./weights/damage_model" diff --git a/configs/parts_config.yaml b/configs/parts_config.yaml new file mode 100644 index 0000000000000000000000000000000000000000..9d79037b5184d47bd7630cf3b5317afb4b8be084 --- /dev/null +++ b/configs/parts_config.yaml @@ -0,0 +1,13 @@ +# Example Detectron2 config for parts model (5 classes) +_BASE_: "COCO-InstanceSegmentation/mask_rcnn_R_50_FPN_3x.yaml" +MODEL: + ROI_HEADS: + NUM_CLASSES: 5 +DATASETS: + TRAIN: ("parts_train",) + TEST: ("parts_val",) +SOLVER: + IMS_PER_BATCH: 2 + BASE_LR: 0.00025 + MAX_ITER: 3000 +OUTPUT_DIR: "./weights/parts_model" diff --git a/inference/damage_inference.py b/inference/damage_inference.py new file mode 100644 index 0000000000000000000000000000000000000000..d32f1c31083ba2aa1007dfc6d065922dd626bf93 --- /dev/null +++ b/inference/damage_inference.py @@ -0,0 +1,58 @@ +# Inference and visualization for YOLOv8 damage segmentation on unseen images +from ultralytics import YOLO +import os +from glob import glob +import sys + +def run_inference(): # Get absolute paths + base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + model_path = os.path.join(base_dir, 'models', 'damage', 'weights', 'weights', 'best.pt') + img_dir = os.path.join(base_dir, 'damage_detection_dataset', 'img') + out_dir = os.path.join(base_dir, 'inference_results', 'damage') + + # Validate paths + if not os.path.exists(model_path): + print(f"Error: Model weights not found at {model_path}") + return + + if not os.path.exists(img_dir): + print(f"Error: Image directory not found at {img_dir}") + return + + # Create output directory + os.makedirs(out_dir, exist_ok=True) + + # Get all images in the dataset + all_imgs = sorted(glob(os.path.join(img_dir, '*.jpg'))) + if not all_imgs: + print(f"No images found in {img_dir}") + return + + try: + # Load model + model = YOLO(model_path) + + # Run inference and save results + for img_path in all_imgs: + try: + results = model.predict( + source=img_path, + save=True, + project=out_dir, + name='', + imgsz=640, + conf=0.25 + ) + print(f'Processed: {os.path.basename(img_path)}') + except Exception as e: + print(f"Error processing {os.path.basename(img_path)}: {str(e)}") + continue + + print(f'Inference complete. Results saved to {out_dir}') + + except Exception as e: + print(f"Error loading model: {str(e)}") + return + +if __name__ == '__main__': + run_inference() diff --git a/inference/parts_inference.py b/inference/parts_inference.py new file mode 100644 index 0000000000000000000000000000000000000000..da43117e2a6775fe5eb7640f68058011c819cba7 --- /dev/null +++ b/inference/parts_inference.py @@ -0,0 +1,74 @@ +# Inference on unseen images for YOLOv8 parts segmentation +from ultralytics import YOLO +import os +from glob import glob + +def run_inference(): # Get absolute paths + base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + model_path = os.path.join(base_dir, 'models', 'parts', 'weights', 'weights', 'best.pt') + img_dir = os.path.join(base_dir, 'damage_detection_dataset', 'img') + train_dir = os.path.join(base_dir, 'data', 'data_yolo_for_training', 'car_parts_damage_dataset', 'images', 'train') + out_dir = os.path.join(base_dir, 'inference_results', 'parts') + + # Validate paths + if not os.path.exists(model_path): + print(f"Error: Model weights not found at {model_path}") + return + + if not os.path.exists(img_dir): + print(f"Error: Image directory not found at {img_dir}") + return + + if not os.path.exists(train_dir): + print(f"Warning: Training directory not found at {train_dir}") + print("Will run inference on all images instead of just unseen ones") + train_imgs = set() + else: + # Get all images used for training + train_imgs = set(os.listdir(train_dir)) + + # Create output directory + os.makedirs(out_dir, exist_ok=True) + + # Get all images in original dataset + all_imgs = set(os.listdir(img_dir)) + # Select images not used in training + unseen_imgs = sorted(list(all_imgs - train_imgs)) + + if not unseen_imgs: + print(f"No images found for inference in {img_dir}") + return + + try: + # Load model + model = YOLO(model_path) + + # Class names for visualization + class_names = ['headlamp', 'front_bumper', 'hood', 'door', 'rear_bumper'] + + # Run inference on each unseen image + for img_name in unseen_imgs: + try: + img_path = os.path.join(img_dir, img_name) + results = model.predict( + source=img_path, + save=True, + project=out_dir, + name='', + imgsz=640, + conf=0.25, + classes=list(range(len(class_names))) # All classes + ) + print(f'Processed: {img_name}') + except Exception as e: + print(f"Error processing {img_name}: {str(e)}") + continue + + print(f'Inference complete. Results saved to {out_dir}') + + except Exception as e: + print(f"Error loading model: {str(e)}") + return + +if __name__ == '__main__': + run_inference() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..3cebe584a32baf807990f5331420417c805ea820 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,11 @@ +ultralytics +opencv-python +numpy +matplotlib +fastapi +uvicorn[standard] +ultralytics +opencv-python +jinja2 +starlette +requests diff --git a/training/damage/data.yaml b/training/damage/data.yaml new file mode 100644 index 0000000000000000000000000000000000000000..50ebd93c9cd1bb45837cc42ef1305a567c0cfe91 --- /dev/null +++ b/training/damage/data.yaml @@ -0,0 +1,9 @@ +# YAML for YOLOv8 damage segmentation (1 class) +path: E:/AI-ToolStack/Cardamagedetection/data/data_yolo_for_training/car_damage_dataset # Root directory for dataset +train: images/train # Train images (relative to 'path') +val: images/val # Val images (relative to 'path') +test: images/test # Test images (relative to 'path') + +# Classes +names: [damage] # Class names +nc: 1 # Number of classes diff --git a/training/damage/train.py b/training/damage/train.py new file mode 100644 index 0000000000000000000000000000000000000000..eff937f2b593e759d839d1b59e158cd3c626c868 --- /dev/null +++ b/training/damage/train.py @@ -0,0 +1,66 @@ +# YOLOv8 segmentation training for car damage detection +from ultralytics import YOLO +import multiprocessing +import os + +def train(): + # Start from YOLOv8 medium segmentation model + model = YOLO('../../models/yolov8m-seg.pt') + + # Get the absolute path to the data.yaml file + current_dir = os.path.dirname(os.path.abspath(__file__)) + data_yaml_path = os.path.join(current_dir, 'data.yaml') + + # Train with optimized parameters + model.train( + data=data_yaml_path, # Path to data configuration file + epochs=150, # Number of epochs + imgsz=640, # Image size + batch=4, # Batch size + workers=4, # Number of workers + project='../../models/damage/weights', # Save directory + name='yolov8_damage_final', # Run name + + # Learning rate strategy + lr0=0.0002, # Initial learning rate + lrf=0.000001, # Final learning rate + warmup_epochs=25, + warmup_momentum=0.8, + cos_lr=True, # Use cosine learning rate scheduler + + # Loss weights + box=8.0, # Box loss gain + cls=4.0, # Class loss gain + dfl=2.5, # DFL loss gain + + # Augmentation settings + augment=True, + mosaic=0.5, + mixup=0.2, + copy_paste=0.1, + degrees=20.0, + translate=0.2, + scale=0.4, + shear=10.0, + flipud=0.1, + fliplr=0.5, + hsv_h=0.015, + hsv_s=0.7, + hsv_v=0.4, + + # Other optimization settings + overlap_mask=True, # Overlap mask segments + mask_ratio=4, # Mask downsampling ratio + single_cls=True, # Single class detection + rect=False, # Rectangular training + cache=False, # Cache images for faster training + patience=50, # Early stopping patience + close_mosaic=10, # Close mosaic augmentation epochs + deterministic=True, # Deterministic mode + seed=42, # Random seed + device=0 # GPU device + ) + +if __name__ == '__main__': + multiprocessing.freeze_support() + train() diff --git a/training/parts/data.yaml b/training/parts/data.yaml new file mode 100644 index 0000000000000000000000000000000000000000..5ce78711ff0051e23eabde87d564c87381b4c5e0 --- /dev/null +++ b/training/parts/data.yaml @@ -0,0 +1,15 @@ +# YAML for YOLOv8 parts segmentation (5 classes) +path: E:/AI-ToolStack/Cardamagedetection/data/data_yolo_for_training/car_parts_damage_dataset # Root directory for dataset +train: images/train # Train images (relative to 'path') +val: images/val # Val images (relative to 'path') +test: images/test # Test images (relative to 'path') + +# Classes +names: + 0: headlamp + 1: front_bumper + 2: hood + 3: door + 4: rear_bumper + +nc: 5 # Number of classes diff --git a/training/parts/train.py b/training/parts/train.py new file mode 100644 index 0000000000000000000000000000000000000000..2486516a9407b9859ba5276eedf931e273c020e3 --- /dev/null +++ b/training/parts/train.py @@ -0,0 +1,66 @@ +# YOLOv8 segmentation training for car parts detection +from ultralytics import YOLO +import multiprocessing +import os + +def train(): + # Start from YOLOv8 medium segmentation model + model = YOLO('../../models/yolov8m-seg.pt') + + # Get the absolute path to the data.yaml file + current_dir = os.path.dirname(os.path.abspath(__file__)) + data_yaml_path = os.path.join(current_dir, 'data.yaml') + + # Train with optimized parameters for parts detection + model.train( + data=data_yaml_path, # Path to data configuration file + epochs=100, # Number of epochs + imgsz=640, # Image size + batch=4, # Batch size + workers=4, # Number of workers + project='../../models/parts/weights', # Save directory + name='yolov8_parts_final', # Run name + + # Learning rate strategy + lr0=0.0002, # Initial learning rate + lrf=0.000001, # Final learning rate + warmup_epochs=20, # Fewer warmup epochs for parts + warmup_momentum=0.8, + cos_lr=True, # Use cosine learning rate scheduler + + # Loss weights + box=8.0, # Box loss gain + cls=4.0, # Class loss gain + dfl=2.5, # DFL loss gain + + # Augmentation settings + augment=True, + mosaic=0.5, + mixup=0.2, + copy_paste=0.1, + degrees=20.0, + translate=0.2, + scale=0.4, + shear=10.0, + flipud=0.1, + fliplr=0.5, + hsv_h=0.015, + hsv_s=0.7, + hsv_v=0.4, + + # Other optimization settings + overlap_mask=True, # Overlap mask segments + mask_ratio=4, # Mask downsampling ratio + single_cls=False, # Multiple classes for parts + rect=False, # Rectangular training + cache=False, # Cache images for faster training + patience=50, # Early stopping patience + close_mosaic=10, # Close mosaic augmentation epochs + deterministic=True, # Deterministic mode + seed=42, # Random seed + device=0 # GPU device + ) + +if __name__ == '__main__': + multiprocessing.freeze_support() + train() diff --git a/utils/balance_parts_dataset.py b/utils/balance_parts_dataset.py new file mode 100644 index 0000000000000000000000000000000000000000..38e567063c7a2fe8853f5bb6f22d08cb03486942 --- /dev/null +++ b/utils/balance_parts_dataset.py @@ -0,0 +1,95 @@ +import json +import os +import shutil +from collections import defaultdict +import random +from tqdm import tqdm + +def create_balanced_dataset(source_json, source_img_dir, target_dir, min_samples=50): + """ + Create a balanced dataset for parts detection by sampling images with different parts. + + Args: + source_json (str): Path to source COCO JSON file + source_img_dir (str): Path to source images directory + target_dir (str): Path to target directory for balanced dataset + min_samples (int): Minimum number of samples per class + """ + # Create target directories + os.makedirs(os.path.join(target_dir, 'images'), exist_ok=True) + os.makedirs(os.path.join(target_dir, 'labels'), exist_ok=True) + + # Load COCO annotations + with open(source_json, 'r') as f: + coco = json.load(f) + + # Group images by parts they contain + images_by_part = defaultdict(set) + image_to_anns = defaultdict(list) + + for ann in coco['annotations']: + img_id = ann['image_id'] + cat_id = ann['category_id'] + images_by_part[cat_id].add(img_id) + image_to_anns[img_id].append(ann) + + # Find images with balanced representation + selected_images = set() + for part_images in images_by_part.values(): + # Sample min_samples images for each part + sample_size = min(min_samples, len(part_images)) + selected_images.update(random.sample(list(part_images), sample_size)) + + # Copy selected images and create labels + id_to_filename = {img['id']: img['file_name'] for img in coco['images']} + + print(f"Creating balanced dataset with {len(selected_images)} images...") + for img_id in tqdm(selected_images): + # Copy image + src_img = os.path.join(source_img_dir, id_to_filename[img_id]) + dst_img = os.path.join(target_dir, 'images', id_to_filename[img_id]) + shutil.copy2(src_img, dst_img) + + # Create YOLO label + base_name = os.path.splitext(id_to_filename[img_id])[0] + label_file = os.path.join(target_dir, 'labels', f"{base_name}.txt") + + # Convert annotations to YOLO format + anns = image_to_anns[img_id] + label_lines = [] + + # Get image dimensions + from PIL import Image + im = Image.open(src_img) + w, h = im.size + + for ann in anns: + cat_id = ann['category_id'] + # Convert segmentation to YOLO format + for seg in ann['segmentation']: + seg_norm = [str(x/w) if i%2==0 else str(x/h) for i,x in enumerate(seg)] + label_lines.append(f"{cat_id} {' '.join(seg_norm)}") + + # Write label file + with open(label_file, 'w') as f: + f.write('\n'.join(label_lines)) + +if __name__ == "__main__": + current_dir = os.path.dirname(os.path.abspath(__file__)) + base_dir = os.path.dirname(current_dir) + + # Process training set + create_balanced_dataset( + source_json=os.path.join(base_dir, "damage_detection_dataset", "train", "COCO_mul_train_annos.json"), + source_img_dir=os.path.join(base_dir, "damage_detection_dataset", "img"), + target_dir=os.path.join(base_dir, "data", "parts", "balanced", "train"), + min_samples=50 + ) + + # Process validation set + create_balanced_dataset( + source_json=os.path.join(base_dir, "damage_detection_dataset", "val", "COCO_mul_val_annos.json"), + source_img_dir=os.path.join(base_dir, "damage_detection_dataset", "img"), + target_dir=os.path.join(base_dir, "data", "parts", "balanced", "val"), + min_samples=10 + ) \ No newline at end of file diff --git a/utils/coco_helpers.py b/utils/coco_helpers.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/utils/coco_to_yolo_seg.py b/utils/coco_to_yolo_seg.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/utils/matching.py b/utils/matching.py new file mode 100644 index 0000000000000000000000000000000000000000..347c8615f35f78164aa9999fb25cf079a289dbc5 --- /dev/null +++ b/utils/matching.py @@ -0,0 +1,19 @@ +import numpy as np + +def get_overlapping_part(damage_mask, part_masks, part_labels): + max_iou = 0 + matched_part = None + for mask, label in zip(part_masks, part_labels): + iou = np.sum(np.logical_and(damage_mask, mask)) / np.sum(np.logical_or(damage_mask, mask)) + if iou > max_iou: + max_iou = iou + matched_part = label + return matched_part, max_iou + +def estimate_severity(area, part_label): + if area < 2000: + return "minor" + elif area < 8000: + return "moderate" + else: + return "severe" if part_label in ["front_bumper", "hood", "rear_bumper"] else "moderate" diff --git a/utils/organize_complete_datasets.py b/utils/organize_complete_datasets.py new file mode 100644 index 0000000000000000000000000000000000000000..3f70af20e454053f05f716dac073e133dade7290 --- /dev/null +++ b/utils/organize_complete_datasets.py @@ -0,0 +1,220 @@ +import os +import shutil +import json +from collections import defaultdict +import random +from tqdm import tqdm +from PIL import Image + +def convert_coco_to_yolo(coco_json_path, images_dir, output_dir, class_map, split='train'): + """Convert COCO format annotations to YOLO format""" + if not os.path.exists(coco_json_path): + print(f"Warning: JSON file not found: {coco_json_path}") + return set() + + if not os.path.exists(images_dir): + print(f"Warning: Images directory not found: {images_dir}") + return set() + + print(f"\nProcessing {split} split...") + + # Create output directories + labels_dir = os.path.join(output_dir, 'labels', split) + images_dir_out = os.path.join(output_dir, 'images', split) + os.makedirs(labels_dir, exist_ok=True) + os.makedirs(images_dir_out, exist_ok=True) + + # Load COCO annotations + try: + with open(coco_json_path, 'r') as f: + coco = json.load(f) + except json.JSONDecodeError: + print(f"Error: Invalid JSON file: {coco_json_path}") + return set() + + # Create id to filename mapping + id_to_filename = {img['id']: img['file_name'] for img in coco['images']} + + # Group annotations by image + img_to_anns = defaultdict(list) + for ann in coco['annotations']: + img_to_anns[ann['image_id']].append(ann) + + # Process each image + processed_images = set() + for img_id, anns in tqdm(img_to_anns.items(), desc=f"Converting {split} set"): + img_file = id_to_filename[img_id] + img_path = os.path.join(images_dir, img_file) + + if not os.path.exists(img_path): + print(f"Warning: Image {img_path} not found, skipping...") + continue + + try: + # Copy image + shutil.copy2(img_path, os.path.join(images_dir_out, img_file)) + + # Get image dimensions + with Image.open(img_path) as im: + w, h = im.size + + # Convert annotations + label_lines = [] + for ann in anns: + cat_id = ann['category_id'] + if cat_id not in class_map: + print(f"Warning: Unknown category ID {cat_id} in {img_file}") + continue + yolo_cls = class_map[cat_id] + + # Convert segmentation points + for seg in ann['segmentation']: + coords = [str(x/w) if i%2==0 else str(x/h) for i,x in enumerate(seg)] + label_lines.append(f"{yolo_cls} {' '.join(coords)}") + + # Write label file + label_file = os.path.join(labels_dir, os.path.splitext(img_file)[0] + '.txt') + with open(label_file, 'w') as f: + f.write('\n'.join(label_lines)) + + processed_images.add(img_id) + + except (IOError, OSError) as e: + print(f"Error processing {img_file}: {str(e)}") + continue + + return processed_images + +def create_balanced_dataset(source_json, images_dir, output_dir, class_map, min_samples=50, split='train'): + """Create balanced dataset by sampling equal number of images per class""" + print(f"\nCreating balanced dataset for {split} split...") + + # Create output directories + labels_dir = os.path.join(output_dir, 'labels', split) + images_dir_out = os.path.join(output_dir, 'images', split) + os.makedirs(labels_dir, exist_ok=True) + os.makedirs(images_dir_out, exist_ok=True) + + # Load COCO annotations + with open(source_json, 'r') as f: + coco = json.load(f) + + # Group images by parts they contain + images_by_part = defaultdict(set) + image_to_anns = defaultdict(list) + + for ann in coco['annotations']: + img_id = ann['image_id'] + cat_id = ann['category_id'] + images_by_part[cat_id].add(img_id) + image_to_anns[img_id].append(ann) + + # Sample images for balanced dataset + selected_images = set() + for part_images in images_by_part.values(): + sample_size = min(min_samples, len(part_images)) + selected_images.update(random.sample(list(part_images), sample_size)) + + # Convert selected images to YOLO format + id_to_filename = {img['id']: img['file_name'] for img in coco['images']} + + print(f"Processing {len(selected_images)} images for balanced {split} set...") + for img_id in tqdm(selected_images): + img_file = id_to_filename[img_id] + img_path = os.path.join(images_dir, img_file) + + if not os.path.exists(img_path): + print(f"Warning: Image {img_path} not found, skipping...") + continue + + # Copy image + shutil.copy2(img_path, os.path.join(images_dir_out, img_file)) + + # Get image dimensions + with Image.open(img_path) as im: + w, h = im.size + + # Convert annotations + label_lines = [] + for ann in image_to_anns[img_id]: + cat_id = ann['category_id'] + yolo_cls = class_map[cat_id] + + # Convert segmentation points + for seg in ann['segmentation']: + coords = [str(x/w) if i%2==0 else str(x/h) for i,x in enumerate(seg)] + label_lines.append(f"{yolo_cls} {' '.join(coords)}") + + # Write label file + label_file = os.path.join(labels_dir, os.path.splitext(img_file)[0] + '.txt') + with open(label_file, 'w') as f: + f.write('\n'.join(label_lines)) + +def main(): + base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + source_dir = os.path.join(base_dir, 'damage_detection_dataset') + + if not os.path.exists(source_dir): + print(f"Error: Source directory not found: {source_dir}") + return + + # Set up output directories + car_damage_dir = os.path.join(base_dir, 'data', 'data_yolo_for_training', 'car_damage_dataset') + car_parts_dir = os.path.join(base_dir, 'data', 'data_yolo_for_training', 'car_parts_damage_dataset') + + # Class mappings + damage_class_map = {1: 0} # Assuming damage is class 1 in COCO format + parts_class_map = {1: 0, 2: 1, 3: 2, 4: 3, 5: 4} # headlamp, front_bumper, hood, door, rear_bumper + + # Process car damage dataset (full dataset) + print("\nProcessing Car Damage Dataset...") + for split in ['train', 'val', 'test']: + json_name = 'COCO_train_annos.json' if split == 'train' else 'COCO_val_annos.json' + json_path = os.path.join(source_dir, split, json_name) + images_dir = os.path.join(source_dir, split) + + if os.path.exists(json_path): + convert_coco_to_yolo( + json_path, + images_dir, + car_damage_dir, + damage_class_map, + split + ) + else: + print(f"Warning: JSON file not found for {split} split: {json_path}") + + # Process car parts dataset (balanced training, original val/test) + print("\nProcessing Car Parts Dataset...") + # Training set - balanced + train_json = os.path.join(source_dir, 'train', 'COCO_mul_train_annos.json') + if os.path.exists(train_json): + create_balanced_dataset( + train_json, + os.path.join(source_dir, 'train'), + car_parts_dir, + parts_class_map, + min_samples=50, + split='train' + ) + else: + print(f"Warning: Training JSON file not found: {train_json}") + + # Validation and test sets - original + for split in ['val', 'test']: + json_path = os.path.join(source_dir, split, 'COCO_mul_val_annos.json') + images_dir = os.path.join(source_dir, split) + + if os.path.exists(json_path): + convert_coco_to_yolo( + json_path, + images_dir, + car_parts_dir, + parts_class_map, + split + ) + else: + print(f"Warning: JSON file not found for {split} split: {json_path}") + +if __name__ == '__main__': + main() diff --git a/utils/organize_datasets.py b/utils/organize_datasets.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/utils/visualization.py b/utils/visualization.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391