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 |
+
}
|