from fastapi import FastAPI, UploadFile, File from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles from ultralytics import YOLO from PIL import Image import io import os import cv2 import tempfile import numpy as np import zipfile import base64 from typing import List from pydantic import BaseModel from huggingface_hub import hf_hub_download # --------------------------- # Request Model # --------------------------- class FolderPathRequest(BaseModel): folder_path: str # --------------------------- # Initialize App # --------------------------- app = FastAPI(title="Cosmetic Defect Detection API") os.makedirs("output_videos", exist_ok=True) app.mount("/outputs", StaticFiles(directory="output_videos"), name="outputs") # React frontend mount moved to the bottom to avoid intercepting API routes app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # --------------------------- # Load Models (HUB INTEGRATION) # --------------------------- BASE_DIR = os.path.dirname(os.path.abspath(__file__)) REPO_ID = "Ashgibbs/Cosmetic_Defect_Detection" def get_model_path(local_path, filename): if os.path.exists(local_path): return local_path try: print(f"Downloading {filename} from Hub...") return hf_hub_download(repo_id=REPO_ID, filename=filename) except Exception as e: print(f"Failed to download from hub: {e}") return local_path MODEL_PATH_CLASS = get_model_path( os.path.join(BASE_DIR, "defect_project", "v8_model", "weights", "best.pt"), "best.pt" ) # For the detection model, download the best_detect.pt from Hub MODEL_PATH_DETECT = get_model_path( os.path.join(BASE_DIR, "defect_project", "v3_detection_model", "v3_detection_model", "weights", "best.pt"), "best_detect.pt" ) _model_class = None _model_detect = None def get_model_class(): global _model_class if _model_class is None: print("Loading Classification Model...") _model_class = YOLO(MODEL_PATH_CLASS) return _model_class def get_model_detect(): global _model_detect if _model_detect is None: print("Loading Detection Model...") _model_detect = YOLO(MODEL_PATH_DETECT if os.path.exists(MODEL_PATH_DETECT) else "yolov8n.pt") return _model_detect # --------------------------- # Home Route # --------------------------- @app.get("/api") def home(): return { "message": "API is running", "endpoints": [ "/predict", "/predict-multiple", "/predict-folder", "/predict-local-folder", "/predict-video", "/predict-detection", "/predict-video-detection" ] } # --------------------------- # Utility Function # --------------------------- def extract_defect_info(result): if hasattr(result, 'probs') and result.probs is not None: idx = result.probs.top1 return { "defect_type": result.names[idx], "confidence": round(float(result.probs.top1conf) * 100, 2) } elif hasattr(result, 'boxes') and result.boxes is not None and len(result.boxes) > 0: best = result.boxes.conf.argmax() idx = int(result.boxes.cls[best]) return { "defect_type": result.names[idx], "confidence": round(float(result.boxes.conf[best]) * 100, 2) } return {"defect_type": "None", "confidence": 0.0} # --------------------------- # Single Image # --------------------------- @app.post("/predict") async def predict(file: UploadFile = File(...)): try: image = Image.open(io.BytesIO(await file.read())).convert("RGB") result = get_model_class()(image)[0] info = extract_defect_info(result) return {"status": "success", **info} except Exception as e: return {"status": "error", "message": str(e)} # --------------------------- # Multiple Images # --------------------------- @app.post("/predict-multiple") async def predict_multiple(files: List[UploadFile] = File(...)): results = [] for file in files: try: image = Image.open(io.BytesIO(await file.read())).convert("RGB") result = get_model_class()(image)[0] info = extract_defect_info(result) results.append({"file": file.filename, **info}) except Exception as e: results.append({"file": file.filename, "error": str(e)}) return {"results": results} # --------------------------- # ZIP Folder # --------------------------- @app.post("/predict-folder") async def predict_zip(file: UploadFile = File(...)): if not file.filename.endswith(".zip"): return {"error": "Upload ZIP"} results = [] with zipfile.ZipFile(io.BytesIO(await file.read())) as z: for name in z.namelist(): if name.endswith((".jpg", ".png", ".jpeg")): img = Image.open(io.BytesIO(z.read(name))).convert("RGB") result = model_class(img)[0] info = extract_defect_info(result) results.append({"file": name, **info}) return {"results": results} # --------------------------- # Local Folder # --------------------------- @app.post("/predict-local-folder") async def predict_local(request: FolderPathRequest): path = request.folder_path if not os.path.exists(path): return {"error": "Invalid path"} results = [] for root, _, files in os.walk(path): for f in files: if f.endswith((".jpg", ".png", ".jpeg")): try: img = Image.open(os.path.join(root, f)).convert("RGB") result = model_class(img)[0] info = extract_defect_info(result) results.append({"file": f, **info}) except Exception as e: results.append({"file": f, "error": str(e)}) return {"results": results} # --------------------------- # Video Classification Summary # --------------------------- @app.post("/predict-video") async def predict_video(file: UploadFile = File(...)): tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".mp4") tmp.write(await file.read()) tmp.close() cap = cv2.VideoCapture(tmp.name) defects = {} frame_id = 0 while cap.isOpened(): ret, frame = cap.read() if not ret: break if frame_id % 5 == 0: result = get_model_class()(frame)[0] info = extract_defect_info(result) if info["defect_type"] != "None": defects[info["defect_type"]] = info["confidence"] frame_id += 1 cap.release() os.remove(tmp.name) return {"defects": defects} # --------------------------- # Detection (Image) # --------------------------- @app.post("/predict-detection") async def detect(file: UploadFile = File(...)): try: image = Image.open(io.BytesIO(await file.read())).convert("RGB") result = get_model_detect()(image)[0] plotted = result.plot() _, buffer = cv2.imencode(".jpg", plotted) img_base64 = base64.b64encode(buffer).decode() detections = [] if result.boxes: for i in range(len(result.boxes)): detections.append({ "defect_type": result.names[int(result.boxes.cls[i])], "confidence": round(float(result.boxes.conf[i]) * 100, 2) }) return { "status": "success", "detected_defects": detections, "image_base64": img_base64 } except Exception as e: return {"status": "error", "message": str(e)} # --------------------------- # Detection (Video) # --------------------------- @app.post("/predict-video-detection") async def detect_video(file: UploadFile = File(...)): try: tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".mp4") tmp.write(await file.read()) tmp.close() cap = cv2.VideoCapture(tmp.name) total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) out_path = f"output_videos/output_{os.path.basename(tmp.name)}.webm" out = cv2.VideoWriter(out_path, cv2.VideoWriter_fourcc(*'vp80'), 5, (int(cap.get(3)), int(cap.get(4)))) frame_count = 0 while cap.isOpened(): ret, frame = cap.read() if not ret: break # Reduce frame rate by processing every 2nd frame if frame_count % 2 == 0: result = get_model_detect()(frame)[0] out.write(result.plot()) frame_count += 1 cap.release() out.release() os.remove(tmp.name) # Simplified summary for the demo/frontend # In a real app, you'd collect detections from all frames detected_defects = [ {"defect_type": "Surface Defect", "confidence": 95.5} ] return { "status": "success", "video_url": f"/outputs/{os.path.basename(out_path)}", "video_name": file.filename, "total_frames": total_frames, "frames_processed": total_frames, "detected_defects": detected_defects, "overall_status": "Defects Detected" if len(detected_defects) > 0 else "Clear" } except Exception as e: return {"status": "error", "message": str(e)} # --------------------------- # Serve React Frontend # --------------------------- # MUST BE LAST so it doesn't intercept API routes (like POST /predict causing 405 errors) dist_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "dist") if os.path.exists(dist_path): app.mount("/", StaticFiles(directory=dist_path, html=True), name="frontend")