diff --git a/Domains/IoT/MiniProjects/syncro/.gitignore b/Domains/IoT/MiniProjects/syncro/.gitignore new file mode 100644 index 00000000..17e98a32 --- /dev/null +++ b/Domains/IoT/MiniProjects/syncro/.gitignore @@ -0,0 +1,43 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +**/__pycache__/ + +# Virtual environments +venv/ +env/ +ENV/ +.venv + +# IDEs +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS files +.DS_Store +Thumbs.db + +# Environment variables +.env +*.env + +# Logs +*.log +logs/ + +# YOLOv8 models (these are downloaded automatically) +*.pt +*.onnx +*.engine + +# Test/temp files +temp/ +tmp/ +test_images/ +test_videos/ diff --git a/Domains/IoT/MiniProjects/syncro/backend/.env.example b/Domains/IoT/MiniProjects/syncro/backend/.env.example new file mode 100644 index 00000000..e1c0f48e --- /dev/null +++ b/Domains/IoT/MiniProjects/syncro/backend/.env.example @@ -0,0 +1,11 @@ +# Environment variables for SYNCRO backend + +# YOLOv8 Model (for high accuracy use yolov8l.pt or yolov8x.pt) +# Options: yolov8n.pt, yolov8s.pt, yolov8m.pt, yolov8l.pt, yolov8x.pt +YOLO_MODEL=yolov8m.pt + +# Camera source (0 for default webcam, or path to video file like "assembly.mp4") +CAMERA_SOURCE=0 + +# Confidence threshold for detections (0.0 to 1.0) +CONFIDENCE_THRESHOLD=0.5 diff --git a/Domains/IoT/MiniProjects/syncro/backend/README.md b/Domains/IoT/MiniProjects/syncro/backend/README.md new file mode 100644 index 00000000..a86d4a08 --- /dev/null +++ b/Domains/IoT/MiniProjects/syncro/backend/README.md @@ -0,0 +1,98 @@ +**Contributor:** k4niz + +# SYNCRO - Real-Time Assembly Sequence Monitoring (IoT + AI) + +Backend service built with FastAPI that captures camera frames, detects parts using YOLOv8, and validates assembly sequence order in real-time. + +## Features +- Camera capture (OpenCV) +- YOLOv8 object detection with configurable accuracy models +- Assembly sequence validation and logging +- REST API: start/stop/status/logs +- postman collection included + +## YOLOv8 Models (Accuracy vs Speed) +- `yolov8n.pt` - Nano (fastest, lowest accuracy) +- `yolov8s.pt` - Small +- `yolov8m.pt` - Medium (**default**, balanced) +- `yolov8l.pt` - Large (high accuracy) +- `yolov8x.pt` - Extra Large (highest accuracy, slowest) + +## API Endpoints +- `POST /api/start-monitoring` - Start monitoring loop +- `POST /api/stop-monitoring` - Stop monitoring +- `GET /api/status` - Current status and sequence step +- `GET /api/logs` - Last 50 detection logs + +## Setup + +1. **Navigate to syncro folder:** + ```powershell + cd Domains\IoT\MiniProjects\syncro + ``` + +2. **Install dependencies:** + ```powershell + cd backend + pip install -r requirements.txt + cd .. + ``` + +3. **(Optional) Configure model:** + Copy `.env.example` to `.env` and edit: + ``` + YOLO_MODEL=yolov8l.pt # For high accuracy + ``` + +4. **Run the server:** + ```powershell + uvicorn backend.main:app --reload + ``` + +5. **Test with Swagger UI:** + Open `http://127.0.0.1:8000/docs` + +6. **Or import Postman collection:** + `backend/SYNCRO.postman_collection.json` + +## How It Works + +1. **Camera** captures frames continuously +2. **YOLOv8** detects objects in each frame +3. **Label Mapper** converts COCO classes to assembly steps +4. **Sequence Validator** checks if steps are in correct order +5. **Logger** records all detections with timestamps +6. **API** exposes status and logs in real-time + +## Customization + +### Change YOLO Model +Edit `backend/utils/config.py` or set env var: +```powershell +$env:YOLO_MODEL="yolov8x.pt" # Highest accuracy +``` + +### Customize Assembly Sequence +Edit `backend/utils/config.py`: +```python +SEQUENCE_STEPS = [ + "pick_part_a", + "pick_part_b", + "assemble" +] +``` + +### Map Custom Objects +Edit `vision_service.py` → `_create_label_map()`: +```python +return { + "screw": "pick_screw", + "wrench": "pick_tool", + "bolt": "tighten_screw" +} +``` + +## Notes +- First run downloads the YOLO model (~50MB for yolov8m) +- If no camera is available, detection returns empty results +- For custom part detection, train a custom YOLOv8 model on your assembly dataset diff --git a/Domains/IoT/MiniProjects/syncro/backend/SYNCRO.postman_collection.json b/Domains/IoT/MiniProjects/syncro/backend/SYNCRO.postman_collection.json new file mode 100644 index 00000000..326853d6 --- /dev/null +++ b/Domains/IoT/MiniProjects/syncro/backend/SYNCRO.postman_collection.json @@ -0,0 +1,76 @@ +{ + "info": { + "name": "SYNCRO - Assembly Sequence Monitoring", + "description": "API endpoints for real-time assembly sequence monitoring using Computer Vision", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "item": [ + { + "name": "Health Check", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}/", + "host": ["{{base_url}}"], + "path": [""] + } + } + }, + { + "name": "Start Monitoring", + "request": { + "method": "POST", + "header": [], + "url": { + "raw": "{{base_url}}/api/start-monitoring", + "host": ["{{base_url}}"], + "path": ["api", "start-monitoring"] + } + } + }, + { + "name": "Stop Monitoring", + "request": { + "method": "POST", + "header": [], + "url": { + "raw": "{{base_url}}/api/stop-monitoring", + "host": ["{{base_url}}"], + "path": ["api", "stop-monitoring"] + } + } + }, + { + "name": "Get Status", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}/api/status", + "host": ["{{base_url}}"], + "path": ["api", "status"] + } + } + }, + { + "name": "Get Logs", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}/api/logs", + "host": ["{{base_url}}"], + "path": ["api", "logs"] + } + } + } + ], + "variable": [ + { + "key": "base_url", + "value": "http://127.0.0.1:8000", + "type": "string" + } + ] +} diff --git a/Domains/IoT/MiniProjects/syncro/backend/__init__.py b/Domains/IoT/MiniProjects/syncro/backend/__init__.py new file mode 100644 index 00000000..fb7008f1 --- /dev/null +++ b/Domains/IoT/MiniProjects/syncro/backend/__init__.py @@ -0,0 +1 @@ +# Package init for SYNCRO backend \ No newline at end of file diff --git a/Domains/IoT/MiniProjects/syncro/backend/__pycache__/__init__.cpython-313.pyc b/Domains/IoT/MiniProjects/syncro/backend/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 00000000..8e718a5e Binary files /dev/null and b/Domains/IoT/MiniProjects/syncro/backend/__pycache__/__init__.cpython-313.pyc differ diff --git a/Domains/IoT/MiniProjects/syncro/backend/__pycache__/main.cpython-313.pyc b/Domains/IoT/MiniProjects/syncro/backend/__pycache__/main.cpython-313.pyc new file mode 100644 index 00000000..090147c3 Binary files /dev/null and b/Domains/IoT/MiniProjects/syncro/backend/__pycache__/main.cpython-313.pyc differ diff --git a/Domains/IoT/MiniProjects/syncro/backend/controllers/__init__.py b/Domains/IoT/MiniProjects/syncro/backend/controllers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/Domains/IoT/MiniProjects/syncro/backend/controllers/__pycache__/__init__.cpython-313.pyc b/Domains/IoT/MiniProjects/syncro/backend/controllers/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 00000000..0a2937ca Binary files /dev/null and b/Domains/IoT/MiniProjects/syncro/backend/controllers/__pycache__/__init__.cpython-313.pyc differ diff --git a/Domains/IoT/MiniProjects/syncro/backend/controllers/__pycache__/monitor_controller.cpython-313.pyc b/Domains/IoT/MiniProjects/syncro/backend/controllers/__pycache__/monitor_controller.cpython-313.pyc new file mode 100644 index 00000000..64e1307b Binary files /dev/null and b/Domains/IoT/MiniProjects/syncro/backend/controllers/__pycache__/monitor_controller.cpython-313.pyc differ diff --git a/Domains/IoT/MiniProjects/syncro/backend/controllers/monitor_controller.py b/Domains/IoT/MiniProjects/syncro/backend/controllers/monitor_controller.py new file mode 100644 index 00000000..98e9e85b --- /dev/null +++ b/Domains/IoT/MiniProjects/syncro/backend/controllers/monitor_controller.py @@ -0,0 +1,17 @@ +from backend.services.monitor_service import MonitorService + +monitor = MonitorService() + +def start_monitoring(): + monitor.start() + return {"started": True} + +def stop_monitoring(): + monitor.stop() + return {"stopped": True} + +def get_status(): + return monitor.status() + +def get_logs(): + return {"logs": monitor.get_logs()} diff --git a/Domains/IoT/MiniProjects/syncro/backend/main.py b/Domains/IoT/MiniProjects/syncro/backend/main.py new file mode 100644 index 00000000..8ca7fc80 --- /dev/null +++ b/Domains/IoT/MiniProjects/syncro/backend/main.py @@ -0,0 +1,10 @@ +from fastapi import FastAPI +from backend.routes.monitor_routes import router as monitor_router + +app = FastAPI(title="SYNCRO - Assembly Sequence Monitoring") + +app.include_router(monitor_router, prefix="/api") + +@app.get("/") +def root(): + return {"service": "SYNCRO", "status": "ok"} diff --git a/Domains/IoT/MiniProjects/syncro/backend/models/__init__.py b/Domains/IoT/MiniProjects/syncro/backend/models/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/Domains/IoT/MiniProjects/syncro/backend/models/schemas.py b/Domains/IoT/MiniProjects/syncro/backend/models/schemas.py new file mode 100644 index 00000000..24219430 --- /dev/null +++ b/Domains/IoT/MiniProjects/syncro/backend/models/schemas.py @@ -0,0 +1,16 @@ +from pydantic import BaseModel +from typing import List, Optional, Any, Dict + +class Detection(BaseModel): + label: str + conf: float + +class LogEntry(BaseModel): + ts: float + detections: List[Dict[str, Any]] + result: Dict[str, Any] + +class Status(BaseModel): + running: bool + current_step: Optional[str] + completed: bool diff --git a/Domains/IoT/MiniProjects/syncro/backend/requirements.txt b/Domains/IoT/MiniProjects/syncro/backend/requirements.txt new file mode 100644 index 00000000..0f3576f3 --- /dev/null +++ b/Domains/IoT/MiniProjects/syncro/backend/requirements.txt @@ -0,0 +1,6 @@ +fastapi +uvicorn +opencv-python +pydantic +python-multipart +ultralytics diff --git a/Domains/IoT/MiniProjects/syncro/backend/routes/__init__.py b/Domains/IoT/MiniProjects/syncro/backend/routes/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/Domains/IoT/MiniProjects/syncro/backend/routes/__pycache__/__init__.cpython-313.pyc b/Domains/IoT/MiniProjects/syncro/backend/routes/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 00000000..2acdf8b2 Binary files /dev/null and b/Domains/IoT/MiniProjects/syncro/backend/routes/__pycache__/__init__.cpython-313.pyc differ diff --git a/Domains/IoT/MiniProjects/syncro/backend/routes/__pycache__/monitor_routes.cpython-313.pyc b/Domains/IoT/MiniProjects/syncro/backend/routes/__pycache__/monitor_routes.cpython-313.pyc new file mode 100644 index 00000000..15e40065 Binary files /dev/null and b/Domains/IoT/MiniProjects/syncro/backend/routes/__pycache__/monitor_routes.cpython-313.pyc differ diff --git a/Domains/IoT/MiniProjects/syncro/backend/routes/monitor_routes.py b/Domains/IoT/MiniProjects/syncro/backend/routes/monitor_routes.py new file mode 100644 index 00000000..26dd3711 --- /dev/null +++ b/Domains/IoT/MiniProjects/syncro/backend/routes/monitor_routes.py @@ -0,0 +1,20 @@ +from fastapi import APIRouter +from backend.controllers.monitor_controller import start_monitoring, stop_monitoring, get_status, get_logs + +router = APIRouter(tags=["monitor"]) + +@router.post("/start-monitoring") +def start(): + return start_monitoring() + +@router.post("/stop-monitoring") +def stop(): + return stop_monitoring() + +@router.get("/status") +def status(): + return get_status() + +@router.get("/logs") +def logs(): + return get_logs() diff --git a/Domains/IoT/MiniProjects/syncro/backend/services/__init__.py b/Domains/IoT/MiniProjects/syncro/backend/services/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/Domains/IoT/MiniProjects/syncro/backend/services/__pycache__/__init__.cpython-313.pyc b/Domains/IoT/MiniProjects/syncro/backend/services/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 00000000..75d20ba8 Binary files /dev/null and b/Domains/IoT/MiniProjects/syncro/backend/services/__pycache__/__init__.cpython-313.pyc differ diff --git a/Domains/IoT/MiniProjects/syncro/backend/services/__pycache__/monitor_service.cpython-313.pyc b/Domains/IoT/MiniProjects/syncro/backend/services/__pycache__/monitor_service.cpython-313.pyc new file mode 100644 index 00000000..e4db90ec Binary files /dev/null and b/Domains/IoT/MiniProjects/syncro/backend/services/__pycache__/monitor_service.cpython-313.pyc differ diff --git a/Domains/IoT/MiniProjects/syncro/backend/services/__pycache__/sequence_service.cpython-313.pyc b/Domains/IoT/MiniProjects/syncro/backend/services/__pycache__/sequence_service.cpython-313.pyc new file mode 100644 index 00000000..45fcb684 Binary files /dev/null and b/Domains/IoT/MiniProjects/syncro/backend/services/__pycache__/sequence_service.cpython-313.pyc differ diff --git a/Domains/IoT/MiniProjects/syncro/backend/services/__pycache__/vision_service.cpython-313.pyc b/Domains/IoT/MiniProjects/syncro/backend/services/__pycache__/vision_service.cpython-313.pyc new file mode 100644 index 00000000..27a6ca17 Binary files /dev/null and b/Domains/IoT/MiniProjects/syncro/backend/services/__pycache__/vision_service.cpython-313.pyc differ diff --git a/Domains/IoT/MiniProjects/syncro/backend/services/monitor_service.py b/Domains/IoT/MiniProjects/syncro/backend/services/monitor_service.py new file mode 100644 index 00000000..56a51ba7 --- /dev/null +++ b/Domains/IoT/MiniProjects/syncro/backend/services/monitor_service.py @@ -0,0 +1,52 @@ +import threading +import time +from backend.services.vision_service import VisionService +from backend.services.sequence_service import SequenceService +from backend.utils.logger import logger +from backend.utils.config import SEQUENCE_STEPS, YOLO_MODEL, CAMERA_SOURCE + +class MonitorService: + def __init__(self): + self._running = False + self._thread = None + self.vision = VisionService(source=CAMERA_SOURCE, model_name=YOLO_MODEL) + self.sequence = SequenceService(steps=SEQUENCE_STEPS) + self._logs = [] + + def start(self): + if self._running: + return + self._running = True + self._thread = threading.Thread(target=self._loop, daemon=True) + self._thread.start() + logger.info("Monitoring started") + + def stop(self): + self._running = False + if self._thread: + self._thread.join(timeout=2) + self.vision.release() + logger.info("Monitoring stopped") + + def _loop(self): + while self._running: + frame = self.vision.read() + if frame is None: + time.sleep(0.05) + continue + detections = self.vision.detect(frame) + result = self.sequence.update(detections) + log_entry = {"ts": time.time(), "detections": detections, "result": result} + self._logs.append(log_entry) + logger.debug(str(log_entry)) + time.sleep(0.05) + + def status(self): + return { + "running": self._running, + "current_step": self.sequence.current_step, + "completed": self.sequence.completed, + } + + def get_logs(self, limit: int = 50): + return self._logs[-limit:] diff --git a/Domains/IoT/MiniProjects/syncro/backend/services/sequence_service.py b/Domains/IoT/MiniProjects/syncro/backend/services/sequence_service.py new file mode 100644 index 00000000..12ecd26b --- /dev/null +++ b/Domains/IoT/MiniProjects/syncro/backend/services/sequence_service.py @@ -0,0 +1,25 @@ +from typing import List, Dict + +class SequenceService: + def __init__(self, steps: List[str]): + self.steps = steps + self.index = 0 + self.completed = False + + @property + def current_step(self): + if self.completed: + return None + return self.steps[self.index] + + def update(self, detections: List[Dict]): + if self.completed: + return {"status": "done"} + labels = {d.get("label") for d in detections} + if self.current_step in labels: + self.index += 1 + if self.index >= len(self.steps): + self.completed = True + return {"status": "completed"} + return {"status": "next", "current_step": self.current_step} + return {"status": "waiting", "current_step": self.current_step} diff --git a/Domains/IoT/MiniProjects/syncro/backend/services/vision_service.py b/Domains/IoT/MiniProjects/syncro/backend/services/vision_service.py new file mode 100644 index 00000000..b2953063 --- /dev/null +++ b/Domains/IoT/MiniProjects/syncro/backend/services/vision_service.py @@ -0,0 +1,89 @@ +from typing import List, Dict, Any, Optional +import cv2 +from ultralytics import YOLO + +class VisionService: + def __init__(self, source: int = 0, model_name: str = "yolov8m.pt"): + """ + Initialize Vision Service with camera and YOLOv8 model. + + Args: + source: Camera device index (0 for default webcam) + model_name: YOLOv8 model to use. Options: + - yolov8n.pt (nano, fastest, lowest accuracy) + - yolov8s.pt (small) + - yolov8m.pt (medium, balanced) - DEFAULT + - yolov8l.pt (large, high accuracy) + - yolov8x.pt (extra large, highest accuracy, slowest) + """ + self.cap = cv2.VideoCapture(source) + if not self.cap.isOpened(): + self.cap = None + + # Load YOLOv8 model (downloads automatically on first run) + try: + self.model = YOLO(model_name) + self.label_map = self._create_label_map() + except Exception as e: + print(f"Warning: Failed to load YOLO model: {e}") + self.model = None + self.label_map = {} + + def _create_label_map(self) -> Dict[str, str]: + """ + Map COCO class names to assembly sequence steps. + Customize this based on your detected objects. + """ + return { + # Tools + "scissors": "pick_tool", + "knife": "pick_tool", + # Hardware (you may need custom trained model for specific parts) + "bottle": "pick_screw", # placeholder + "cup": "pick_screw", # placeholder + } + + def read(self) -> Optional["cv2.Mat"]: + if self.cap is None: + return None + ok, frame = self.cap.read() + return frame if ok else None + + def detect(self, frame) -> List[Dict[str, Any]]: + """ + Run YOLOv8 detection on frame and return mapped detections. + + Returns: + List of detections with 'label' and 'conf' keys + """ + if frame is None or self.model is None: + return [] + + try: + # Run inference (verbose=False to reduce console output) + results = self.model(frame, verbose=False) + + detections = [] + for result in results: + for box in result.boxes: + class_id = int(box.cls[0]) + confidence = float(box.conf[0]) + class_name = result.names[class_id] + + # Map COCO class to assembly step if available + mapped_label = self.label_map.get(class_name, class_name) + + detections.append({ + "label": mapped_label, + "conf": confidence, + "original_class": class_name + }) + + return detections + except Exception as e: + print(f"Detection error: {e}") + return [] + + def release(self): + if self.cap is not None: + self.cap.release() diff --git a/Domains/IoT/MiniProjects/syncro/backend/utils/__init__.py b/Domains/IoT/MiniProjects/syncro/backend/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/Domains/IoT/MiniProjects/syncro/backend/utils/__pycache__/__init__.cpython-313.pyc b/Domains/IoT/MiniProjects/syncro/backend/utils/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 00000000..5bb8ef2e Binary files /dev/null and b/Domains/IoT/MiniProjects/syncro/backend/utils/__pycache__/__init__.cpython-313.pyc differ diff --git a/Domains/IoT/MiniProjects/syncro/backend/utils/__pycache__/config.cpython-313.pyc b/Domains/IoT/MiniProjects/syncro/backend/utils/__pycache__/config.cpython-313.pyc new file mode 100644 index 00000000..6e58a92a Binary files /dev/null and b/Domains/IoT/MiniProjects/syncro/backend/utils/__pycache__/config.cpython-313.pyc differ diff --git a/Domains/IoT/MiniProjects/syncro/backend/utils/__pycache__/logger.cpython-313.pyc b/Domains/IoT/MiniProjects/syncro/backend/utils/__pycache__/logger.cpython-313.pyc new file mode 100644 index 00000000..3ef45895 Binary files /dev/null and b/Domains/IoT/MiniProjects/syncro/backend/utils/__pycache__/logger.cpython-313.pyc differ diff --git a/Domains/IoT/MiniProjects/syncro/backend/utils/config.py b/Domains/IoT/MiniProjects/syncro/backend/utils/config.py new file mode 100644 index 00000000..608949e0 --- /dev/null +++ b/Domains/IoT/MiniProjects/syncro/backend/utils/config.py @@ -0,0 +1,19 @@ +# Configuration for SYNCRO backend +import os + +# YOLOv8 Model Selection (for high accuracy, use yolov8l or yolov8x) +YOLO_MODEL = os.getenv("YOLO_MODEL", "yolov8m.pt") # medium (balanced) +# Options: yolov8n.pt, yolov8s.pt, yolov8m.pt, yolov8l.pt, yolov8x.pt + +# Camera source (0 = default webcam, or path to video file) +CAMERA_SOURCE = int(os.getenv("CAMERA_SOURCE", "0")) + +# Assembly sequence steps (customize for your use case) +SEQUENCE_STEPS = [ + "pick_screw", + "pick_tool", + "tighten_screw" +] + +# Detection confidence threshold +CONFIDENCE_THRESHOLD = float(os.getenv("CONFIDENCE_THRESHOLD", "0.5")) diff --git a/Domains/IoT/MiniProjects/syncro/backend/utils/logger.py b/Domains/IoT/MiniProjects/syncro/backend/utils/logger.py new file mode 100644 index 00000000..9ed2cbff --- /dev/null +++ b/Domains/IoT/MiniProjects/syncro/backend/utils/logger.py @@ -0,0 +1,7 @@ +import logging + +logger = logging.getLogger("syncro") +logger.setLevel(logging.DEBUG) +_handler = logging.StreamHandler() +_handler.setFormatter(logging.Formatter("[%(asctime)s] %(levelname)s - %(message)s")) +logger.addHandler(_handler)