Commit
·
57328cd
1
Parent(s):
2e43ca6
Add YOLOv8n to detect animal and 2 Roboflow models to be specifically finetuned on bird and fish. Rm HTML_CONTENT to be rendered on statics dir
Browse files- .DS_Store +0 -0
- Dockerfile +1 -0
- app.py +104 -146
- requirements.txt +1 -0
- icon.png → statics/icon.png +0 -0
- statics/index.html +27 -0
- statics/script.js +66 -0
- statics/style.css +73 -0
.DS_Store
CHANGED
|
Binary files a/.DS_Store and b/.DS_Store differ
|
|
|
Dockerfile
CHANGED
|
@@ -54,6 +54,7 @@ COPY --chown=user . $HOME/app
|
|
| 54 |
RUN python -c "from huggingface_hub import snapshot_download; snapshot_download(repo_id='facebook/detr-resnet-50', local_dir='/home/user/app/model/detr', local_dir_use_symlinks=False)"
|
| 55 |
RUN wget -O $HOME/app/model/garbage_detector.pt https://huggingface.co/BinKhoaLe1812/Garbage_Detection/resolve/main/garbage_detector.pt
|
| 56 |
RUN wget -O $HOME/app/model/yolov5-detect-trash-classification.pt https://huggingface.co/turhancan97/yolov5-detect-trash-classification/resolve/main/yolov5s.pt
|
|
|
|
| 57 |
|
| 58 |
# Verify model setup
|
| 59 |
RUN python setup.py
|
|
|
|
| 54 |
RUN python -c "from huggingface_hub import snapshot_download; snapshot_download(repo_id='facebook/detr-resnet-50', local_dir='/home/user/app/model/detr', local_dir_use_symlinks=False)"
|
| 55 |
RUN wget -O $HOME/app/model/garbage_detector.pt https://huggingface.co/BinKhoaLe1812/Garbage_Detection/resolve/main/garbage_detector.pt
|
| 56 |
RUN wget -O $HOME/app/model/yolov5-detect-trash-classification.pt https://huggingface.co/turhancan97/yolov5-detect-trash-classification/resolve/main/yolov5s.pt
|
| 57 |
+
RUN wget -O /home/user/app/model/yolov8n.pt https://github.com/ultralytics/assets/releases/download/v0.0.0/yolov8n.pt
|
| 58 |
|
| 59 |
# Verify model setup
|
| 60 |
RUN python setup.py
|
app.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
| 2 |
|
| 3 |
# ───────────────────────── app.py (Sall-e demo) ─────────────────────────
|
| 4 |
# FastAPI ▸ upload image ▸ multi-model garbage detection ▸ ADE-20K
|
| 5 |
-
# semantic segmentation (Water / Garbage) ▸ A*
|
| 6 |
# =======================================================================
|
| 7 |
|
| 8 |
import os, uuid, threading, shutil, time, heapq, cv2, numpy as np
|
|
@@ -12,6 +12,7 @@ from fastapi import FastAPI, File, UploadFile, Request
|
|
| 12 |
from fastapi.responses import HTMLResponse, StreamingResponse, Response
|
| 13 |
from fastapi.staticfiles import StaticFiles
|
| 14 |
|
|
|
|
| 15 |
# ── Vision libs ─────────────────────────────────────────────────────────
|
| 16 |
import torch, yolov5, ffmpeg
|
| 17 |
from ultralytics import YOLO
|
|
@@ -19,7 +20,9 @@ from transformers import (
|
|
| 19 |
DetrImageProcessor, DetrForObjectDetection,
|
| 20 |
SegformerFeatureExtractor, SegformerForSemanticSegmentation
|
| 21 |
)
|
| 22 |
-
from sklearn.neighbors import NearestNeighbors
|
|
|
|
|
|
|
| 23 |
|
| 24 |
# ── Folders / files ─────────────────────────────────────────────────────
|
| 25 |
BASE = "/home/user/app"
|
|
@@ -45,6 +48,7 @@ feat_extractor = SegformerFeatureExtractor.from_pretrained(
|
|
| 45 |
"nvidia/segformer-b4-finetuned-ade-512-512")
|
| 46 |
segformer = SegformerForSemanticSegmentation.from_pretrained(
|
| 47 |
"nvidia/segformer-b4-finetuned-ade-512-512")
|
|
|
|
| 48 |
print("✅ Models ready\n")
|
| 49 |
|
| 50 |
# ── ADE-20K palette + custom mapping (verbatim) ─────────────────────────
|
|
@@ -163,6 +167,7 @@ def highlight_water_mask_on_frame(frame, binary_mask, color=(255, 0, 0), alpha=0
|
|
| 163 |
cv2.drawContours(overlay, contours, -1, color, thickness=cv2.FILLED)
|
| 164 |
return cv2.addWeighted(overlay, alpha, frame, 1 - alpha, 0)
|
| 165 |
|
|
|
|
| 166 |
# ── A* and KNN over binary water grid ─────────────────────────────────
|
| 167 |
def astar(start, goal, occ):
|
| 168 |
h = lambda a,b: abs(a[0]-b[0])+abs(a[1]-b[1])
|
|
@@ -222,6 +227,7 @@ def knn_path(start, targets, occ):
|
|
| 222 |
todo.remove(list(best))
|
| 223 |
return path
|
| 224 |
|
|
|
|
| 225 |
# ── Robot sprite/class -──────────────────────────────────────────────────
|
| 226 |
class Robot:
|
| 227 |
def __init__(self, sprite, speed=2000): # Declare the robot's physical stats and routing (position, speed, movement, path)
|
|
@@ -251,156 +257,29 @@ class Robot:
|
|
| 251 |
break
|
| 252 |
|
| 253 |
|
| 254 |
-
# ── FastAPI & HTML content (original styling) ───────────────────────────
|
| 255 |
-
# HTML Content for UI (streamed with FastAPI HTML renderer)
|
| 256 |
-
HTML_CONTENT = """
|
| 257 |
-
<!DOCTYPE html>
|
| 258 |
-
<html>
|
| 259 |
-
<head>
|
| 260 |
-
<title>Sall-e Garbage Detection</title>
|
| 261 |
-
<link rel="website icon" type="png" href="/static/icon.png" >
|
| 262 |
-
<style>
|
| 263 |
-
body {
|
| 264 |
-
font-family: 'Roboto', sans-serif; background: linear-gradient(270deg, rgb(44, 13, 58), rgb(13, 58, 56)); color: white; text-align: center; margin: 0; padding: 50px;
|
| 265 |
-
}
|
| 266 |
-
h1 {
|
| 267 |
-
font-size: 40px;
|
| 268 |
-
background: linear-gradient(to right, #f32170, #ff6b08, #cf23cf, #eedd44);
|
| 269 |
-
-webkit-text-fill-color: transparent;
|
| 270 |
-
-webkit-background-clip: text;
|
| 271 |
-
font-weight: bold;
|
| 272 |
-
}
|
| 273 |
-
#upload-container {
|
| 274 |
-
background: rgba(255, 255, 255, 0.2); padding: 20px; width: 70%; border-radius: 10px; display: inline-block; box-shadow: 0px 0px 10px rgba(255, 255, 255, 0.3);
|
| 275 |
-
}
|
| 276 |
-
#upload {
|
| 277 |
-
font-size: 18px; padding: 10px; border-radius: 5px; border: none; background: #fff; cursor: pointer;
|
| 278 |
-
}
|
| 279 |
-
#loader {
|
| 280 |
-
margin-top: 10px; margin-left: auto; margin-right: auto; width: 60px; height: 60px; font-size: 12px; text-align: center;
|
| 281 |
-
}
|
| 282 |
-
p {
|
| 283 |
-
margin-top: 10px; font-size: 12px; color: #3498db;
|
| 284 |
-
}
|
| 285 |
-
#spinner {
|
| 286 |
-
border: 8px solid #f3f3f3; border-top: 8px solid rgb(117 7 7); border-radius: 50%; animation: spin 1s linear infinite; width: 40px; height: 40px; margin: auto;
|
| 287 |
-
}
|
| 288 |
-
@keyframes spin {
|
| 289 |
-
0% { transform: rotate(0deg); }
|
| 290 |
-
100% { transform: rotate(360deg); }
|
| 291 |
-
}
|
| 292 |
-
#outputVideo {
|
| 293 |
-
margin-top: 20px; width: 70%; margin-left: auto; margin-right: auto; max-width: 640px; border-radius: 10px; box-shadow: 0px 0px 10px rgba(255, 255, 255, 0.3);
|
| 294 |
-
}
|
| 295 |
-
#downloadBtn {
|
| 296 |
-
display: block; visibility: hidden; width: 20%; margin-top: 20px; margin-left: auto; margin-right: auto; padding: 10px 15px; font-size: 16px; background: #27ae60; color: white; border: none; border-radius: 5px; cursor: pointer; text-decoration: none;
|
| 297 |
-
}
|
| 298 |
-
#downloadBtn:hover {
|
| 299 |
-
background: #950606;
|
| 300 |
-
}
|
| 301 |
-
.hidden {
|
| 302 |
-
display: none;
|
| 303 |
-
}
|
| 304 |
-
@media (max-width: 860px) {
|
| 305 |
-
h1 { font-size: 30px; }
|
| 306 |
-
}
|
| 307 |
-
@media (max-width: 720px) {
|
| 308 |
-
h1 { font-size: 25px; }
|
| 309 |
-
#upload { font-size: 15px; }
|
| 310 |
-
#downloadBtn { font-size: 13px; }
|
| 311 |
-
}
|
| 312 |
-
@media (max-width: 580px) {
|
| 313 |
-
h1 { font-size: 20px; }
|
| 314 |
-
#upload { font-size: 10px; }
|
| 315 |
-
#downloadBtn { font-size: 10px; }
|
| 316 |
-
}
|
| 317 |
-
@media (max-width: 580px) {
|
| 318 |
-
h1 { font-size: 10px; }
|
| 319 |
-
}
|
| 320 |
-
@media (max-width: 460px) {
|
| 321 |
-
#upload { font-size: 7px; }
|
| 322 |
-
}
|
| 323 |
-
@media (max-width: 400px) {
|
| 324 |
-
h1 { font-size: 14px; }
|
| 325 |
-
}
|
| 326 |
-
@media (max-width: 370px) {
|
| 327 |
-
h1 { font-size: 11px; }
|
| 328 |
-
#upload { font-size: 5px; }
|
| 329 |
-
#downloadBtn { font-size: 7px; }
|
| 330 |
-
}
|
| 331 |
-
@media (max-width: 330px) {
|
| 332 |
-
h1 { font-size: 8px; }
|
| 333 |
-
#upload { font-size: 3px; }
|
| 334 |
-
#downloadBtn { font-size: 5px; }
|
| 335 |
-
}
|
| 336 |
-
</style>
|
| 337 |
-
</head>
|
| 338 |
-
<body>
|
| 339 |
-
<h1>Upload an Image for Garbage Detection</h1>
|
| 340 |
-
<div id="upload-container">
|
| 341 |
-
<input type="file" id="upload" accept="image/*">
|
| 342 |
-
</div>
|
| 343 |
-
<div id="loader" class="loader hidden">
|
| 344 |
-
<div id="spinner"></div>
|
| 345 |
-
<!-- <p>Garbage detection model processing...</p> -->
|
| 346 |
-
</div>
|
| 347 |
-
<video id="outputVideo" class="outputVideo" controls></video>
|
| 348 |
-
<a id="downloadBtn" class="downloadBtn">Download Video</a>
|
| 349 |
-
<script>
|
| 350 |
-
document.addEventListener("DOMContentLoaded", function() {
|
| 351 |
-
document.getElementById("outputVideo").classList.add("hidden");
|
| 352 |
-
document.getElementById("downloadBtn").style.visibility = "hidden";
|
| 353 |
-
});
|
| 354 |
-
document.getElementById('upload').addEventListener('change', async function(event) {
|
| 355 |
-
event.preventDefault();
|
| 356 |
-
const loader = document.getElementById("loader");
|
| 357 |
-
const outputVideo = document.getElementById("outputVideo");
|
| 358 |
-
const downloadBtn = document.getElementById("downloadBtn");
|
| 359 |
-
let file = event.target.files[0];
|
| 360 |
-
if (file) {
|
| 361 |
-
let formData = new FormData();
|
| 362 |
-
formData.append("file", file);
|
| 363 |
-
loader.classList.remove("hidden");
|
| 364 |
-
outputVideo.classList.add("hidden");
|
| 365 |
-
document.getElementById("downloadBtn").style.visibility = "hidden";
|
| 366 |
-
let response = await fetch('/upload/', { method: 'POST', body: formData });
|
| 367 |
-
let result = await response.json();
|
| 368 |
-
let user_id = result.user_id;
|
| 369 |
-
while (true) {
|
| 370 |
-
let checkResponse = await fetch(`/check_video/${user_id}`);
|
| 371 |
-
let checkResult = await checkResponse.json();
|
| 372 |
-
if (checkResult.ready) break;
|
| 373 |
-
await new Promise(resolve => setTimeout(resolve, 10000)); // Wait 10s before checking again
|
| 374 |
-
}
|
| 375 |
-
loader.classList.add("hidden");
|
| 376 |
-
let videoUrl = `/video/${user_id}?t=${new Date().getTime()}`;
|
| 377 |
-
outputVideo.src = videoUrl;
|
| 378 |
-
outputVideo.load();
|
| 379 |
-
outputVideo.play();
|
| 380 |
-
outputVideo.setAttribute("crossOrigin", "anonymous");
|
| 381 |
-
outputVideo.classList.remove("hidden");
|
| 382 |
-
downloadBtn.href = videoUrl;
|
| 383 |
-
document.getElementById("downloadBtn").style.visibility = "visible";
|
| 384 |
-
}
|
| 385 |
-
});
|
| 386 |
-
document.getElementById('outputVideo').addEventListener('error', function() {
|
| 387 |
-
console.log("⚠️ Video could not be played, showing download button instead.");
|
| 388 |
-
document.getElementById('outputVideo').classList.add("hidden");
|
| 389 |
-
document.getElementById("downloadBtn").style.visibility = "visible";
|
| 390 |
-
});
|
| 391 |
-
</script>
|
| 392 |
-
</body>
|
| 393 |
-
</html>
|
| 394 |
-
"""
|
| 395 |
-
|
| 396 |
# ── Static-web ──────────────────────────────────────────────────────────
|
|
|
|
|
|
|
| 397 |
app = FastAPI()
|
| 398 |
-
app.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 399 |
video_ready={}
|
| 400 |
@app.get("/ui", response_class=HTMLResponse)
|
| 401 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 402 |
def _uid(): return uuid.uuid4().hex[:8]
|
| 403 |
|
|
|
|
| 404 |
# ── End-points ──────────────────────────────────────────────────────────
|
| 405 |
# User upload environment img here
|
| 406 |
@app.post("/upload/")
|
|
@@ -421,6 +300,84 @@ def stream(uid:str):
|
|
| 421 |
if not os.path.exists(vid): return Response(status_code=404)
|
| 422 |
return StreamingResponse(open(vid,"rb"), media_type="video/mp4")
|
| 423 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 424 |
# ── Core pipeline (runs in background thread) ───────────────────────────
|
| 425 |
def _pipeline(uid,img_path):
|
| 426 |
print(f"▶️ [{uid}] processing")
|
|
@@ -559,6 +516,7 @@ def _pipeline(uid,img_path):
|
|
| 559 |
os.remove(out_tmp); video_ready[uid]=True
|
| 560 |
print(f"✅ [{uid}] video ready → {final}")
|
| 561 |
|
|
|
|
| 562 |
# ── Run locally (HF Space ignores since built with Docker image) ────────
|
| 563 |
if __name__=="__main__":
|
| 564 |
uvicorn.run(app,host="0.0.0.0",port=7860)
|
|
|
|
| 2 |
|
| 3 |
# ───────────────────────── app.py (Sall-e demo) ─────────────────────────
|
| 4 |
# FastAPI ▸ upload image ▸ multi-model garbage detection ▸ ADE-20K
|
| 5 |
+
# semantic segmentation (Water / Garbage) ▸ A* navigation ▸ H.264 video
|
| 6 |
# =======================================================================
|
| 7 |
|
| 8 |
import os, uuid, threading, shutil, time, heapq, cv2, numpy as np
|
|
|
|
| 12 |
from fastapi.responses import HTMLResponse, StreamingResponse, Response
|
| 13 |
from fastapi.staticfiles import StaticFiles
|
| 14 |
|
| 15 |
+
|
| 16 |
# ── Vision libs ─────────────────────────────────────────────────────────
|
| 17 |
import torch, yolov5, ffmpeg
|
| 18 |
from ultralytics import YOLO
|
|
|
|
| 20 |
DetrImageProcessor, DetrForObjectDetection,
|
| 21 |
SegformerFeatureExtractor, SegformerForSemanticSegmentation
|
| 22 |
)
|
| 23 |
+
# from sklearn.neighbors import NearestNeighbors
|
| 24 |
+
from inference_sdk import InferenceHTTPClient
|
| 25 |
+
|
| 26 |
|
| 27 |
# ── Folders / files ─────────────────────────────────────────────────────
|
| 28 |
BASE = "/home/user/app"
|
|
|
|
| 48 |
"nvidia/segformer-b4-finetuned-ade-512-512")
|
| 49 |
segformer = SegformerForSemanticSegmentation.from_pretrained(
|
| 50 |
"nvidia/segformer-b4-finetuned-ade-512-512")
|
| 51 |
+
model_animal = YOLO(f"{MODEL_DIR}/yolov8n.pt") # Load COCO pre-trained YOLOv8 for animal detection
|
| 52 |
print("✅ Models ready\n")
|
| 53 |
|
| 54 |
# ── ADE-20K palette + custom mapping (verbatim) ─────────────────────────
|
|
|
|
| 167 |
cv2.drawContours(overlay, contours, -1, color, thickness=cv2.FILLED)
|
| 168 |
return cv2.addWeighted(overlay, alpha, frame, 1 - alpha, 0)
|
| 169 |
|
| 170 |
+
|
| 171 |
# ── A* and KNN over binary water grid ─────────────────────────────────
|
| 172 |
def astar(start, goal, occ):
|
| 173 |
h = lambda a,b: abs(a[0]-b[0])+abs(a[1]-b[1])
|
|
|
|
| 227 |
todo.remove(list(best))
|
| 228 |
return path
|
| 229 |
|
| 230 |
+
|
| 231 |
# ── Robot sprite/class -──────────────────────────────────────────────────
|
| 232 |
class Robot:
|
| 233 |
def __init__(self, sprite, speed=2000): # Declare the robot's physical stats and routing (position, speed, movement, path)
|
|
|
|
| 257 |
break
|
| 258 |
|
| 259 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 260 |
# ── Static-web ──────────────────────────────────────────────────────────
|
| 261 |
+
from fastapi.responses import JSONResponse, FileResponse
|
| 262 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 263 |
app = FastAPI()
|
| 264 |
+
app.add_middleware(
|
| 265 |
+
CORSMiddleware,
|
| 266 |
+
allow_origins=["*"],
|
| 267 |
+
allow_methods=["*"],
|
| 268 |
+
allow_headers=["*"],
|
| 269 |
+
)
|
| 270 |
+
app.mount("/statics", StaticFiles(directory="statics"), name="statics")
|
| 271 |
video_ready={}
|
| 272 |
@app.get("/ui", response_class=HTMLResponse)
|
| 273 |
+
async def serve_index():
|
| 274 |
+
p = "statics/index.html"
|
| 275 |
+
if os.path.exists(p):
|
| 276 |
+
print("[STATIC] Serving index.html")
|
| 277 |
+
return FileResponse(p)
|
| 278 |
+
print("[STATIC] index.html not found")
|
| 279 |
+
return JSONResponse(status_code=404, content={"detail":"Not found"})
|
| 280 |
def _uid(): return uuid.uuid4().hex[:8]
|
| 281 |
|
| 282 |
+
|
| 283 |
# ── End-points ──────────────────────────────────────────────────────────
|
| 284 |
# User upload environment img here
|
| 285 |
@app.post("/upload/")
|
|
|
|
| 300 |
if not os.path.exists(vid): return Response(status_code=404)
|
| 301 |
return StreamingResponse(open(vid,"rb"), media_type="video/mp4")
|
| 302 |
|
| 303 |
+
# ─── Detect animal/wildlife ─────────────────────────────────────────────────
|
| 304 |
+
# Init clients
|
| 305 |
+
# https://universe.roboflow.com/team-hope-mmcyy/hydroquest
|
| 306 |
+
robo_fish = InferenceHTTPClient(
|
| 307 |
+
api_url="https://detect.roboflow.com",
|
| 308 |
+
api_key=os.getenv("ROBOFLOW_KEY", "")
|
| 309 |
+
)
|
| 310 |
+
# https://universe.roboflow.com/sky-sd2zq/bird_only-pt0bm/model/1
|
| 311 |
+
robo_bird = InferenceHTTPClient(
|
| 312 |
+
api_url="https://detect.roboflow.com",
|
| 313 |
+
api_key=os.getenv("ROBOFLOW_KEY", "")
|
| 314 |
+
)
|
| 315 |
+
# Animal detection endpoint (animal, fish, bird as target classes)
|
| 316 |
+
@app.post("/animal/")
|
| 317 |
+
async def detect_animals(file: UploadFile = File(...)):
|
| 318 |
+
img_id = _uid()
|
| 319 |
+
img_path = f"{UPLOAD_DIR}/{img_id}_{file.filename}"
|
| 320 |
+
with open(img_path, "wb") as f:
|
| 321 |
+
shutil.copyfileobj(file.file, f)
|
| 322 |
+
print(f"[Animal] Uploaded image: {img_path}")
|
| 323 |
+
# Read and prepare detection
|
| 324 |
+
image = cv2.imread(img_path)
|
| 325 |
+
detections = []
|
| 326 |
+
|
| 327 |
+
# 1. YOLOv8 local
|
| 328 |
+
print("[Animal] Detecting via YOLOv8…")
|
| 329 |
+
try:
|
| 330 |
+
results = model_animal(image)[0]
|
| 331 |
+
for box in results.boxes:
|
| 332 |
+
conf = box.conf[0].item()
|
| 333 |
+
if conf >= 0.75:
|
| 334 |
+
cls_id = int(box.cls[0].item())
|
| 335 |
+
label = model_animal.names[cls_id].lower()
|
| 336 |
+
if label in ["dog", "cat", "cow", "horse", "elephant", "bear", "zebra", "giraffe", "bird"]:
|
| 337 |
+
x1, y1, x2, y2 = map(int, box.xyxy[0].tolist())
|
| 338 |
+
detections.append(((x1, y1, x2, y2), "Animal Alert"))
|
| 339 |
+
except Exception as e:
|
| 340 |
+
print("[YOLOv8 Error]", e)
|
| 341 |
+
|
| 342 |
+
# 2. Roboflow Fish
|
| 343 |
+
try:
|
| 344 |
+
print("[Animal] Detecting via Roboflow Fish model…")
|
| 345 |
+
fish_preds = robo_fish.infer(img_path, model_id="hydroquest/1")
|
| 346 |
+
for pred in fish_preds.get("predictions", []):
|
| 347 |
+
if pred["confidence"] >= 0.75:
|
| 348 |
+
x1 = int(pred["x"] - pred["width"] / 2)
|
| 349 |
+
y1 = int(pred["y"] - pred["height"] / 2)
|
| 350 |
+
x2 = int(pred["x"] + pred["width"] / 2)
|
| 351 |
+
y2 = int(pred["y"] + pred["height"] / 2)
|
| 352 |
+
detections.append(((x1, y1, x2, y2), "Fish Alert"))
|
| 353 |
+
except Exception as e:
|
| 354 |
+
print("[Roboflow Fish Error]", e)
|
| 355 |
+
|
| 356 |
+
# 3. Roboflow Bird
|
| 357 |
+
try:
|
| 358 |
+
print("[Animal] Detecting via Roboflow Bird model…")
|
| 359 |
+
bird_preds = robo_bird.infer(img_path, model_id="bird_only-pt0bm/1")
|
| 360 |
+
for pred in bird_preds.get("predictions", []):
|
| 361 |
+
if pred["confidence"] >= 0.75:
|
| 362 |
+
x1 = int(pred["x"] - pred["width"] / 2)
|
| 363 |
+
y1 = int(pred["y"] - pred["height"] / 2)
|
| 364 |
+
x2 = int(pred["x"] + pred["width"] / 2)
|
| 365 |
+
y2 = int(pred["y"] + pred["height"] / 2)
|
| 366 |
+
detections.append(((x1, y1, x2, y2), "Bird Alert"))
|
| 367 |
+
except Exception as e:
|
| 368 |
+
print("[Roboflow Bird Error]", e)
|
| 369 |
+
# Count detection
|
| 370 |
+
print(f"[Animal] Total detections: {len(detections)}")
|
| 371 |
+
# Write label
|
| 372 |
+
for (x1, y1, x2, y2), label in detections:
|
| 373 |
+
cv2.rectangle(image, (x1, y1), (x2, y2), (0, 0, 255), 2)
|
| 374 |
+
cv2.putText(image, label, (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 0, 255), 2)
|
| 375 |
+
# Write img
|
| 376 |
+
result_path = f"{OUTPUT_DIR}/{img_id}_animal.jpg"
|
| 377 |
+
cv2.imwrite(result_path, image)
|
| 378 |
+
return FileResponse(result_path, media_type="image/jpeg")
|
| 379 |
+
|
| 380 |
+
|
| 381 |
# ── Core pipeline (runs in background thread) ───────────────────────────
|
| 382 |
def _pipeline(uid,img_path):
|
| 383 |
print(f"▶️ [{uid}] processing")
|
|
|
|
| 516 |
os.remove(out_tmp); video_ready[uid]=True
|
| 517 |
print(f"✅ [{uid}] video ready → {final}")
|
| 518 |
|
| 519 |
+
|
| 520 |
# ── Run locally (HF Space ignores since built with Docker image) ────────
|
| 521 |
if __name__=="__main__":
|
| 522 |
uvicorn.run(app,host="0.0.0.0",port=7860)
|
requirements.txt
CHANGED
|
@@ -15,6 +15,7 @@ yolov5
|
|
| 15 |
huggingface_hub>=0.20.3
|
| 16 |
transformers==4.37.2
|
| 17 |
accelerate==0.27.2
|
|
|
|
| 18 |
|
| 19 |
# Video Processing
|
| 20 |
ffmpeg-python
|
|
|
|
| 15 |
huggingface_hub>=0.20.3
|
| 16 |
transformers==4.37.2
|
| 17 |
accelerate==0.27.2
|
| 18 |
+
inference-sdk
|
| 19 |
|
| 20 |
# Video Processing
|
| 21 |
ffmpeg-python
|
icon.png → statics/icon.png
RENAMED
|
File without changes
|
statics/index.html
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html>
|
| 3 |
+
<head>
|
| 4 |
+
<title>Sall-e Garbage Detection</title>
|
| 5 |
+
<link rel="website icon" type="png" href="/statics/icon.png" >
|
| 6 |
+
<link rel="stylesheet" href="/statics/style.css">
|
| 7 |
+
</head>
|
| 8 |
+
<body>
|
| 9 |
+
<h1>Upload an Image to Simulate Garbage Detection and Robot Navigation</h1>
|
| 10 |
+
<div id="upload-container">
|
| 11 |
+
<input type="file" id="upload" accept="image/*">
|
| 12 |
+
</div>
|
| 13 |
+
<div id="loader" class="loader hidden">
|
| 14 |
+
<div id="spinner"></div>
|
| 15 |
+
<!-- <p>Garbage detection model processing...</p> -->
|
| 16 |
+
</div>
|
| 17 |
+
<video id="outputVideo" class="outputVideo" controls></video>
|
| 18 |
+
<a id="downloadBtn" class="downloadBtn">Download Video</a>
|
| 19 |
+
<h1>Upload an Image to Simulate Front-view Animal Detection</h1>
|
| 20 |
+
<div id="upload-container2">
|
| 21 |
+
<input type="file" id="upload2" accept="image/*">
|
| 22 |
+
<button onclick="uploadAnimal()">Check Animal</button>
|
| 23 |
+
</div>
|
| 24 |
+
<div id="animal-result"></div>
|
| 25 |
+
<script src="/statics/script.js"></script>
|
| 26 |
+
</body>
|
| 27 |
+
</html>
|
statics/script.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
document.addEventListener("DOMContentLoaded", function() {
|
| 3 |
+
document.getElementById("outputVideo").classList.add("hidden");
|
| 4 |
+
document.getElementById("downloadBtn").style.visibility = "hidden";
|
| 5 |
+
});
|
| 6 |
+
document.getElementById('upload').addEventListener('change', async function(event) {
|
| 7 |
+
event.preventDefault();
|
| 8 |
+
const loader = document.getElementById("loader");
|
| 9 |
+
const outputVideo = document.getElementById("outputVideo");
|
| 10 |
+
const downloadBtn = document.getElementById("downloadBtn");
|
| 11 |
+
let file = event.target.files[0];
|
| 12 |
+
if (file) {
|
| 13 |
+
let formData = new FormData();
|
| 14 |
+
formData.append("file", file);
|
| 15 |
+
loader.classList.remove("hidden");
|
| 16 |
+
outputVideo.classList.add("hidden");
|
| 17 |
+
document.getElementById("downloadBtn").style.visibility = "hidden";
|
| 18 |
+
let response = await fetch('/upload/', { method: 'POST', body: formData });
|
| 19 |
+
let result = await response.json();
|
| 20 |
+
let user_id = result.user_id;
|
| 21 |
+
while (true) {
|
| 22 |
+
let checkResponse = await fetch(`/check_video/${user_id}`);
|
| 23 |
+
let checkResult = await checkResponse.json();
|
| 24 |
+
if (checkResult.ready) break;
|
| 25 |
+
await new Promise(resolve => setTimeout(resolve, 10000)); // Wait 10s before checking again
|
| 26 |
+
}
|
| 27 |
+
loader.classList.add("hidden");
|
| 28 |
+
let videoUrl = `/video/${user_id}?t=${new Date().getTime()}`;
|
| 29 |
+
outputVideo.src = videoUrl;
|
| 30 |
+
outputVideo.load();
|
| 31 |
+
outputVideo.play();
|
| 32 |
+
outputVideo.setAttribute("crossOrigin", "anonymous");
|
| 33 |
+
outputVideo.classList.remove("hidden");
|
| 34 |
+
downloadBtn.href = videoUrl;
|
| 35 |
+
document.getElementById("downloadBtn").style.visibility = "visible";
|
| 36 |
+
}
|
| 37 |
+
});
|
| 38 |
+
document.getElementById('outputVideo').addEventListener('error', function() {
|
| 39 |
+
console.log("⚠️ Video could not be played, showing download button instead.");
|
| 40 |
+
document.getElementById('outputVideo').classList.add("hidden");
|
| 41 |
+
document.getElementById("downloadBtn").style.visibility = "visible";
|
| 42 |
+
});
|
| 43 |
+
|
| 44 |
+
async function uploadAnimal() {
|
| 45 |
+
const fileInput = document.getElementById('upload2');
|
| 46 |
+
if (!fileInput.files.length) return alert("Upload an image first");
|
| 47 |
+
// Upload and read image file
|
| 48 |
+
const formData = new FormData();
|
| 49 |
+
formData.append("file", fileInput.files[0]);
|
| 50 |
+
// Handshake with FastAPI
|
| 51 |
+
const res = await fetch("/animal/", {
|
| 52 |
+
method: "POST",
|
| 53 |
+
body: formData
|
| 54 |
+
});
|
| 55 |
+
// Error
|
| 56 |
+
if (!res.ok) {
|
| 57 |
+
alert("Failed to process animal detection.");
|
| 58 |
+
return;
|
| 59 |
+
}
|
| 60 |
+
// Create image
|
| 61 |
+
const blob = await res.blob();
|
| 62 |
+
const imgURL = URL.createObjectURL(blob);
|
| 63 |
+
document.getElementById("animal-result").innerHTML =
|
| 64 |
+
`<p><b>Animal Detection Result:</b></p><img src="${imgURL}" width="640"/>`;
|
| 65 |
+
}
|
| 66 |
+
|
statics/style.css
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
body {
|
| 2 |
+
font-family: 'Roboto', sans-serif; background: linear-gradient(270deg, rgb(44, 13, 58), rgb(13, 58, 56)); color: white; text-align: center; margin: 0; padding: 50px;
|
| 3 |
+
}
|
| 4 |
+
h1 {
|
| 5 |
+
font-size: 40px;
|
| 6 |
+
background: linear-gradient(to right, #f32170, #ff6b08, #cf23cf, #eedd44);
|
| 7 |
+
-webkit-text-fill-color: transparent;
|
| 8 |
+
-webkit-background-clip: text;
|
| 9 |
+
font-weight: bold;
|
| 10 |
+
}
|
| 11 |
+
#upload-container, #upload-container2 {
|
| 12 |
+
background: rgba(255, 255, 255, 0.2); padding: 20px; width: 70%; border-radius: 10px; display: inline-block; box-shadow: 0px 0px 10px rgba(255, 255, 255, 0.3);
|
| 13 |
+
}
|
| 14 |
+
#upload, #upload2 {
|
| 15 |
+
font-size: 18px; padding: 10px; border-radius: 5px; border: none; background: #fff; cursor: pointer;
|
| 16 |
+
}
|
| 17 |
+
#loader {
|
| 18 |
+
margin-top: 10px; margin-left: auto; margin-right: auto; width: 60px; height: 60px; font-size: 12px; text-align: center;
|
| 19 |
+
}
|
| 20 |
+
p {
|
| 21 |
+
margin-top: 10px; font-size: 12px; color: #3498db;
|
| 22 |
+
}
|
| 23 |
+
#spinner {
|
| 24 |
+
border: 8px solid #f3f3f3; border-top: 8px solid rgb(117 7 7); border-radius: 50%; animation: spin 1s linear infinite; width: 40px; height: 40px; margin: auto;
|
| 25 |
+
}
|
| 26 |
+
@keyframes spin {
|
| 27 |
+
0% { transform: rotate(0deg); }
|
| 28 |
+
100% { transform: rotate(360deg); }
|
| 29 |
+
}
|
| 30 |
+
#outputVideo {
|
| 31 |
+
margin-top: 20px; width: 70%; margin-left: auto; margin-right: auto; max-width: 640px; border-radius: 10px; box-shadow: 0px 0px 10px rgba(255, 255, 255, 0.3);
|
| 32 |
+
}
|
| 33 |
+
#downloadBtn {
|
| 34 |
+
display: block; visibility: hidden; width: 20%; margin-top: 20px; margin-left: auto; margin-right: auto; padding: 10px 15px; font-size: 16px; background: #27ae60; color: white; border: none; border-radius: 5px; cursor: pointer; text-decoration: none;
|
| 35 |
+
}
|
| 36 |
+
#downloadBtn:hover {
|
| 37 |
+
background: #950606;
|
| 38 |
+
}
|
| 39 |
+
.hidden {
|
| 40 |
+
display: none;
|
| 41 |
+
}
|
| 42 |
+
@media (max-width: 860px) {
|
| 43 |
+
h1 { font-size: 30px; }
|
| 44 |
+
}
|
| 45 |
+
@media (max-width: 720px) {
|
| 46 |
+
h1 { font-size: 25px; }
|
| 47 |
+
#upload { font-size: 15px; }
|
| 48 |
+
#downloadBtn { font-size: 13px; }
|
| 49 |
+
}
|
| 50 |
+
@media (max-width: 580px) {
|
| 51 |
+
h1 { font-size: 20px; }
|
| 52 |
+
#upload { font-size: 10px; }
|
| 53 |
+
#downloadBtn { font-size: 10px; }
|
| 54 |
+
}
|
| 55 |
+
@media (max-width: 580px) {
|
| 56 |
+
h1 { font-size: 10px; }
|
| 57 |
+
}
|
| 58 |
+
@media (max-width: 460px) {
|
| 59 |
+
#upload { font-size: 7px; }
|
| 60 |
+
}
|
| 61 |
+
@media (max-width: 400px) {
|
| 62 |
+
h1 { font-size: 14px; }
|
| 63 |
+
}
|
| 64 |
+
@media (max-width: 370px) {
|
| 65 |
+
h1 { font-size: 11px; }
|
| 66 |
+
#upload { font-size: 5px; }
|
| 67 |
+
#downloadBtn { font-size: 7px; }
|
| 68 |
+
}
|
| 69 |
+
@media (max-width: 330px) {
|
| 70 |
+
h1 { font-size: 8px; }
|
| 71 |
+
#upload { font-size: 3px; }
|
| 72 |
+
#downloadBtn { font-size: 5px; }
|
| 73 |
+
}
|