codewithRiz commited on
Commit
9993c90
·
0 Parent(s):
Files changed (17) hide show
  1. .gitattributes +3 -0
  2. .gitignore +65 -0
  3. Dockerfile +51 -0
  4. README.md +11 -0
  5. api/__init__.py +0 -0
  6. api/analytics.py +27 -0
  7. api/auth.py +10 -0
  8. api/camera.py +153 -0
  9. api/config.py +69 -0
  10. api/detection.py +50 -0
  11. api/main.py +73 -0
  12. api/utils.py +219 -0
  13. api/view_image.py +80 -0
  14. app.py +26 -0
  15. dockerignore +24 -0
  16. git +0 -0
  17. requirements.txt +12 -0
.gitattributes ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ api/walidlife_models/*.pt filter=lfs diff=lfs merge=lfs -text
2
+ *.onnx filter=lfs diff=lfs merge=lfs -text
3
+ *.pt filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # Environment folders
7
+ .env
8
+ .venv/
9
+ venv/
10
+ env/
11
+ ENV/
12
+
13
+ # IDE / Editor settings
14
+ .vscode/
15
+ .idea/
16
+ *.sublime-project
17
+ *.sublime-workspace
18
+
19
+ # OS-generated files
20
+ .DS_Store
21
+ Thumbs.db
22
+
23
+ # Jupyter Notebook checkpoints
24
+ .ipynb_checkpoints
25
+
26
+ # Logs and temp files
27
+ *.log
28
+ *.tmp
29
+ *.swp
30
+
31
+
32
+
33
+ # Data files
34
+ *.csv
35
+ *.tsv
36
+ *.json
37
+ *.zip
38
+ *.tar
39
+ *.gz
40
+ *.json
41
+ *.pickle
42
+ *.pkl
43
+ # Model files
44
+ *.h5
45
+ *.pth
46
+ *.pt
47
+ *.tflite
48
+ *.onnx
49
+
50
+ # Audio/video/temp data
51
+ *.wav
52
+ *.mp3
53
+ *.mp4
54
+ *.html
55
+ # Streamlit and Gradio cache
56
+ .streamlit/
57
+ gradio_cached_examples/
58
+
59
+ # __pycache__ in subfolders
60
+ */__pycache__/
61
+
62
+
63
+ api/uploaded_images/
64
+ api/walidlife_models/
65
+ api/user_data/
Dockerfile ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ----------------------------
2
+ # Stage 1: Builder
3
+ # ----------------------------
4
+ FROM python:3.12-slim AS builder
5
+
6
+ WORKDIR /app
7
+
8
+ # Install only what is needed for building wheels
9
+ RUN apt-get update && apt-get install -y --no-install-recommends \
10
+ build-essential \
11
+ gcc \
12
+ libgl1 \
13
+ && rm -rf /var/lib/apt/lists/*
14
+
15
+ # Copy dependencies file
16
+ COPY requirements.txt .
17
+
18
+ # Install dependencies into a temporary folder
19
+ RUN pip install --upgrade pip && \
20
+ pip install --prefix=/install --no-cache-dir -r requirements.txt
21
+
22
+ # Copy source
23
+ COPY . .
24
+
25
+ # ----------------------------
26
+ # Stage 2: Runtime (minimal size)
27
+ # ----------------------------
28
+ FROM python:3.12-slim
29
+
30
+ WORKDIR /app
31
+
32
+ # Install minimal runtime libraries only
33
+ RUN apt-get update && apt-get install -y --no-install-recommends \
34
+ libgl1 \
35
+ libglib2.0-0 \
36
+ && rm -rf /var/lib/apt/lists/*
37
+
38
+ # Copy only installed python deps
39
+ COPY --from=builder /install /usr/local
40
+
41
+ # Copy only application code (not build deps)
42
+ COPY --from=builder /app /app
43
+
44
+ # Reduce Python overhead
45
+ ENV PYTHONUNBUFFERED=1
46
+ ENV PYTHONDONTWRITEBYTECODE=1
47
+
48
+ EXPOSE 7860
49
+
50
+ # Use multiple workers in a lightweight way
51
+ CMD ["uvicorn", "api.main:app", "--host", "0.0.0.0", "--port", "7860"]
README.md ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Wildlife Detection App
3
+ emoji: "🦌"
4
+ colorFrom: "blue"
5
+ colorTo: "green"
6
+ sdk: "streamlit"
7
+ sdk_version: "1.25.0"
8
+ python_version: "3.12"
9
+ app_file: app.py
10
+ pinned: false
11
+ ---
api/__init__.py ADDED
File without changes
api/analytics.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Query, HTTPException
2
+ from .utils import get_user_dashboard, validate_user_and_camera
3
+
4
+ router = APIRouter()
5
+
6
+ @router.get("/user_dashboard")
7
+ def user_dashboard(
8
+ user_id: str = Query(..., description="User ID for analytics"),
9
+ camera_name: str = Query(None, description="Optional camera name to filter")
10
+ ):
11
+ """
12
+ Return analytics for a user.
13
+ If camera_name is provided, validate camera and show stats for that camera only.
14
+ """
15
+ try:
16
+ # Validate camera if specified
17
+ if camera_name:
18
+ validate_user_and_camera(user_id, camera_name)
19
+
20
+ # Get dashboard (all cameras or specific)
21
+ dashboard = get_user_dashboard(user_id, camera_name=camera_name)
22
+ return {"success": True, "dashboard": dashboard}
23
+
24
+ except HTTPException as e:
25
+ raise e
26
+ except Exception as e:
27
+ raise HTTPException(status_code=500, detail=f"Internal error: {str(e)}")
api/auth.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter
2
+
3
+ router = APIRouter()
4
+
5
+ @router.get("/status")
6
+ async def status():
7
+ return {
8
+ "status": "success",
9
+ "message": "Auth blueprint active"
10
+ }
api/camera.py ADDED
@@ -0,0 +1,153 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, HTTPException, Query, Body
2
+ from pydantic import BaseModel, Field, validator
3
+ from typing import List, Optional
4
+ from .utils import save_cameras, load_cameras, get_user_file, user_exists
5
+ from .config import UPLOAD_DIR,GCS_UPLOAD_DIR
6
+ import os
7
+ import shutil
8
+
9
+ print(" CAMERA API LOADED ")
10
+
11
+ router = APIRouter(prefix="/camera", tags=["Camera"])
12
+
13
+ # ================= MODELS =================
14
+ class CameraData(BaseModel):
15
+ user_id: str = Field(..., min_length=1)
16
+ camera_name: str = Field(..., min_length=1)
17
+ camera_loc: Optional[List[float]] = None
18
+
19
+ @validator("camera_loc")
20
+ def validate_loc(cls, loc):
21
+ if loc is None:
22
+ return loc
23
+ if len(loc) != 2:
24
+ raise ValueError("camera_loc must be [lat, lon]")
25
+ lat, lon = loc
26
+ if not (-90 <= lat <= 90):
27
+ raise ValueError("Latitude must be between -90 and 90")
28
+ if not (-180 <= lon <= 180):
29
+ raise ValueError("Longitude must be between -180 and 180")
30
+ return loc
31
+
32
+
33
+ class EditCameraData(BaseModel):
34
+ user_id: str
35
+ old_camera_name: str
36
+ new_camera_name: str
37
+ new_camera_loc: List[float]
38
+
39
+ @validator("new_camera_loc")
40
+ def validate_loc(cls, loc):
41
+ if len(loc) != 2:
42
+ raise ValueError("new_camera_loc must be [lat, lon]")
43
+ lat, lon = loc
44
+ if not (-90 <= lat <= 90):
45
+ raise ValueError("Latitude must be between -90 and 90")
46
+ if not (-180 <= lon <= 180):
47
+ raise ValueError("Longitude must be between -180 and 180")
48
+ return loc
49
+
50
+
51
+ # ================= ROUTES =================
52
+ @router.get("/")
53
+ def home():
54
+ return {
55
+ "message": "Camera API is Running",
56
+ "endpoints": [
57
+ "/camera/add_camera",
58
+ "/camera/edit_camera",
59
+ "/camera/delete_camera",
60
+ "/camera/get_cameras?user_id=<id>"
61
+ ]
62
+ }
63
+
64
+
65
+ # ---------- GET CAMERAS ----------
66
+ @router.get("/get_cameras")
67
+ def get_cameras(user_id: str = Query(...)):
68
+ if not user_exists(user_id):
69
+ raise HTTPException(status_code=404, detail="User not found")
70
+
71
+ cameras = load_cameras(user_id)
72
+ return {"success": True, "user_id": user_id, "cameras": cameras, "count": len(cameras)}
73
+
74
+
75
+ # ---------- ADD CAMERA ----------
76
+ @router.post("/add_camera")
77
+ def add_camera(data: CameraData):
78
+ cameras = load_cameras(data.user_id)
79
+
80
+ if len(cameras) >= 2:
81
+ raise HTTPException(status_code=400, detail="Only 2 cameras allowed")
82
+
83
+ for cam in cameras:
84
+ if cam["camera_name"].lower() == data.camera_name.lower():
85
+ raise HTTPException(status_code=400, detail="Camera already exists")
86
+
87
+ cameras.append({"camera_name": data.camera_name, "camera_loc": data.camera_loc})
88
+ save_cameras(data.user_id, cameras)
89
+
90
+ return {"success": True, "camera": data.camera_name}
91
+
92
+ @router.put("/edit_camera")
93
+ def edit_camera(data: EditCameraData):
94
+ if not user_exists(data.user_id):
95
+ raise HTTPException(status_code=404, detail="User not found")
96
+
97
+ cameras = load_cameras(data.user_id)
98
+
99
+ # Check if new camera name already exists (case-insensitive)
100
+ if any(cam["camera_name"].lower() == data.new_camera_name.lower() for cam in cameras):
101
+ raise HTTPException(status_code=400, detail=f"Camera name '{data.new_camera_name}' already exists")
102
+
103
+ camera_found = False
104
+ for cam in cameras:
105
+ if cam["camera_name"].lower() == data.old_camera_name.lower():
106
+ old_name = cam["camera_name"]
107
+ cam["camera_name"] = data.new_camera_name
108
+ cam["camera_loc"] = data.new_camera_loc
109
+ camera_found = True
110
+
111
+ # Rename camera folder
112
+ old_folder = os.path.join(UPLOAD_DIR, data.user_id, old_name)
113
+ new_folder = os.path.join(UPLOAD_DIR, data.user_id, data.new_camera_name)
114
+ if os.path.exists(old_folder) and os.path.isdir(old_folder):
115
+ try:
116
+ os.rename(old_folder, new_folder)
117
+ except Exception as e:
118
+ raise HTTPException(status_code=500, detail=f"Failed to rename camera folder: {str(e)}")
119
+
120
+ # Rename detection JSON
121
+ for f in os.listdir(new_folder):
122
+ if f.endswith("_detections.json") and f.lower().startswith(old_name.lower()):
123
+ old_json = os.path.join(new_folder, f)
124
+ new_json_name = f.replace(old_name, data.new_camera_name)
125
+ new_json = os.path.join(new_folder, new_json_name)
126
+ try:
127
+ os.rename(old_json, new_json)
128
+ except Exception as e:
129
+ raise HTTPException(status_code=500, detail=f"Failed to rename detection JSON: {str(e)}")
130
+ break # Only one detection JSON per camera
131
+
132
+ save_cameras(data.user_id, cameras)
133
+ return {"success": True, "updated": cam}
134
+
135
+ if not camera_found:
136
+ raise HTTPException(status_code=404, detail="Camera not found")
137
+
138
+ @router.delete("/delete_camera")
139
+ def delete_camera(user_id: str = Query(...), camera_name: str = Query(...)):
140
+ if not user_exists(user_id):
141
+ raise HTTPException(status_code=404, detail="User not found")
142
+ cameras = load_cameras(user_id)
143
+ new_list = [c for c in cameras if c["camera_name"].lower() != camera_name.lower()]
144
+ if len(new_list) == len(cameras):
145
+ raise HTTPException(status_code=404, detail="Camera not found")
146
+ save_cameras(user_id, new_list)
147
+ camera_folder = os.path.join(UPLOAD_DIR, user_id, camera_name)
148
+ if os.path.exists(camera_folder) and os.path.isdir(camera_folder):
149
+ try:
150
+ shutil.rmtree(camera_folder)
151
+ except Exception as e:
152
+ raise HTTPException(status_code=500, detail=f"Failed to delete camera folder: {str(e)}")
153
+ return {"success": True, "deleted": camera_name}
api/config.py ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import logging
3
+ from dotenv import load_dotenv
4
+ from ultralytics import YOLO
5
+ from google.cloud import storage
6
+ import google.auth
7
+
8
+ # ---------------- ENV ----------------
9
+ load_dotenv(override=True)
10
+ BASE_DIR = os.path.dirname(os.path.abspath(__file__))
11
+
12
+ # ---------------- LOGGER ----------------
13
+ LOG_FILE = os.path.join(BASE_DIR, "api.log")
14
+
15
+ logging.basicConfig(
16
+ level=logging.INFO,
17
+ format="%(asctime)s [%(levelname)s] %(message)s",
18
+ handlers=[logging.FileHandler(LOG_FILE), logging.StreamHandler()]
19
+ )
20
+
21
+ logger = logging.getLogger("WildlifeLogger")
22
+
23
+ ENV = os.getenv("ENV", "DEV").upper()
24
+ logger.info(f"Running in {ENV}")
25
+
26
+ # ---------------- STORAGE ----------------
27
+ UPLOAD_DIR = os.path.join(BASE_DIR, "user_data")
28
+ os.makedirs(UPLOAD_DIR, exist_ok=True)
29
+
30
+ STORAGE_BACKEND = "local"
31
+ gcs_client = None
32
+ gcs_bucket = None
33
+ GCS_UPLOAD_DIR = "uploaded_images/"
34
+
35
+ if ENV == "PROD":
36
+ STORAGE_BACKEND = os.getenv("PROD_STORAGE_BACKEND", "gcs").lower()
37
+ GCS_BUCKET_NAME = os.getenv("PROD_GCS_BUCKET_NAME")
38
+ GCS_KEY_PATH = os.getenv("PROD_GOOGLE_APPLICATION_CREDENTIALS")
39
+
40
+ if STORAGE_BACKEND == "gcs":
41
+ try:
42
+ if os.getenv("GOOGLE_CLOUD_PROJECT"):
43
+ creds, project = google.auth.default()
44
+ gcs_client = storage.Client(credentials=creds, project=project)
45
+ else:
46
+ gcs_client = storage.Client.from_service_account_json(GCS_KEY_PATH)
47
+
48
+ gcs_bucket = gcs_client.bucket(GCS_BUCKET_NAME)
49
+ logger.info(f"Connected to GCS bucket: {GCS_BUCKET_NAME}")
50
+
51
+ except Exception as e:
52
+ logger.error(f"GCS connection failed: {e}")
53
+ STORAGE_BACKEND = "local"
54
+
55
+ # ---------------- UPLOAD RULES ----------------
56
+ MIN_IMAGES = 1
57
+ MAX_IMAGES = 1000
58
+ ALLOWED_EXTENSIONS = {"jpg", "jpeg", "png", "webp"}
59
+
60
+ # ---------------- YOLO MODELS ----------------
61
+ try:
62
+ logger.info("Loading YOLO models...")
63
+ DETECT_MODEL = YOLO("api/walidlife_models/detect/deer.pt")
64
+ BUCK_DOE_MODEL = YOLO("api/walidlife_models/classify/Buck_classificationt.pt", task="classify")
65
+ BUCK_TYPE_MODEL = YOLO("api/walidlife_models/classify/mules_vs_whitetails.pt", task="classify")
66
+ logger.info("YOLO models loaded")
67
+ except Exception as e:
68
+ logger.error(f"YOLO load failed: {e}")
69
+ DETECT_MODEL = BUCK_DOE_MODEL = BUCK_TYPE_MODEL = None
api/detection.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, UploadFile, File, Form, HTTPException
2
+ from pathlib import Path
3
+ import cv2
4
+ import numpy as np
5
+
6
+ from .config import UPLOAD_DIR
7
+ from .utils import (
8
+ validate_form,
9
+ process_image,
10
+ save_image,
11
+ load_json,
12
+ save_json,
13
+ validate_user_and_camera
14
+ )
15
+
16
+ router = APIRouter()
17
+ @router.post("/predict")
18
+ async def predict(
19
+ user_id: str = Form(...),
20
+ camera_name: str = Form(...),
21
+ images: list[UploadFile] = File(...)
22
+ ):
23
+ images = validate_form(user_id, camera_name, images)
24
+ validate_user_and_camera(user_id, camera_name)
25
+ base = Path(UPLOAD_DIR) / user_id / camera_name
26
+ base.mkdir(parents=True, exist_ok=True)
27
+ json_path = base / f"{camera_name}_detections.json"
28
+ data = load_json(json_path)
29
+ new_results = []
30
+ for file in images:
31
+ raw = await file.read()
32
+ nparr = np.frombuffer(raw, np.uint8)
33
+ img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
34
+ if img is None:
35
+ raise HTTPException(400, f"Invalid image: {file.filename}")
36
+ detections = process_image(img)
37
+ url = save_image(user_id, camera_name, file.filename, raw)
38
+ record = {
39
+ "filename": file.filename,
40
+ "image_url": url,
41
+ "detections": detections
42
+ }
43
+ data.append(record)
44
+ new_results.append(record)
45
+ save_json(json_path, data)
46
+ return {
47
+ "message": "Images processed successfully",
48
+ "camera": camera_name,
49
+ "results": new_results
50
+ }
api/main.py ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # main.py
2
+ from fastapi import FastAPI
3
+ from fastapi.middleware.cors import CORSMiddleware
4
+ from fastapi.staticfiles import StaticFiles
5
+ import logging
6
+ from pathlib import Path
7
+
8
+ # ----------------- Import Routers -----------------
9
+ from .detection import router as detection_router
10
+ from .auth import router as auth_router
11
+ from .camera import router as camera_router # Camera router
12
+ from .config import UPLOAD_DIR # Folder where images are saved
13
+ from .analytics import router as analytics_router
14
+ from .view_image import router as view_images_router
15
+ # ---------------- Logging Setup -----------------
16
+ logger = logging.getLogger("ServerLogger")
17
+ logging.basicConfig(
18
+ level=logging.INFO,
19
+ format="%(asctime)s [%(levelname)s] %(name)s: %(message)s"
20
+ )
21
+
22
+ # Ensure UPLOAD_DIR exists
23
+ Path(UPLOAD_DIR).mkdir(parents=True, exist_ok=True)
24
+
25
+ # ---------------- Create App -----------------
26
+ def create_app() -> FastAPI:
27
+ app = FastAPI(title="Wildlife Detection & Camera API Server")
28
+
29
+ # ---- CORS Configuration ----
30
+ app.add_middleware(
31
+ CORSMiddleware,
32
+ allow_origins=[
33
+ 'https://embroiderywala.myshopify.com',
34
+ 'https://www.daleandcompany.com','http://127.0.0.1:8080'
35
+ ],
36
+ allow_credentials=True,
37
+ allow_methods=["*"],
38
+ allow_headers=["*"],
39
+ )
40
+
41
+ # ---- Mount static folder for uploaded images ----
42
+ app.mount("/user_data", StaticFiles(directory=UPLOAD_DIR), name="user_data")
43
+ logger.info(f"Static folder mounted at /user_data -> {UPLOAD_DIR}")
44
+
45
+ # ---- Include Routers ----
46
+ app.include_router(auth_router, prefix="/auth", tags=["Auth"])
47
+ app.include_router(camera_router, prefix="/api", tags=["Camera"])
48
+ app.include_router(detection_router, prefix="/api/detection", tags=["Detection"])
49
+ app.include_router(analytics_router,prefix="/api", tags=["Analytics"])
50
+ app.include_router(view_images_router, prefix="/api", tags=["Images"])
51
+
52
+ # ---- Health Check / Startup Event ----
53
+ @app.on_event("startup")
54
+ async def startup_check():
55
+ logger.info("Server started successfully. All routers are active.")
56
+
57
+ # ---- Root Endpoint ----
58
+ @app.get("/")
59
+ def root():
60
+ return {
61
+ "message": "Wildlife Detection & Camera API Server is Running",
62
+ "routes": {
63
+ "auth": "/auth/...",
64
+ "camera": "/api/camera/...",
65
+ "detection": "/api/detection/...",
66
+ "analytics": "/api/analytics/..."
67
+
68
+ }
69
+ }
70
+
71
+ return app
72
+
73
+ app = create_app()
api/utils.py ADDED
@@ -0,0 +1,219 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ from pathlib import Path
4
+ from fastapi import HTTPException
5
+ import cv2
6
+ import numpy as np
7
+
8
+ # ---------------- CONFIG IMPORTS ----------------
9
+ from .config import (
10
+ DETECT_MODEL,
11
+ BUCK_DOE_MODEL,
12
+ BUCK_TYPE_MODEL,
13
+ ALLOWED_EXTENSIONS,
14
+ MIN_IMAGES,
15
+ MAX_IMAGES,
16
+ UPLOAD_DIR,
17
+ STORAGE_BACKEND,
18
+ gcs_bucket,
19
+ GCS_UPLOAD_DIR,
20
+ logger
21
+ )
22
+
23
+ # ---------------- VALIDATION ----------------
24
+ def validate_form(user_id, camera_name, images):
25
+ if not user_id or not user_id.strip():
26
+ raise HTTPException(400, "user_id is required")
27
+
28
+ if not camera_name or not camera_name.strip():
29
+ raise HTTPException(400, "camera_name is required")
30
+
31
+ if not images or len(images) == 0:
32
+ raise HTTPException(400, "At least one image is required")
33
+
34
+ images = [f for f in images if f.filename and f.filename.strip()]
35
+
36
+ if len(images) < MIN_IMAGES:
37
+ raise HTTPException(400, f"At least {MIN_IMAGES} image(s) required")
38
+
39
+ if len(images) > MAX_IMAGES:
40
+ raise HTTPException(400, f"Maximum {MAX_IMAGES} images allowed")
41
+
42
+ for f in images:
43
+ if "." not in f.filename:
44
+ raise HTTPException(400, f"Invalid file: {f.filename}")
45
+
46
+ ext = f.filename.rsplit(".", 1)[1].lower()
47
+ if ext not in ALLOWED_EXTENSIONS:
48
+ raise HTTPException(400, f"Invalid file type: {f.filename}")
49
+
50
+ return images
51
+
52
+ # ---------------- IMAGE PROCESSING ----------------
53
+ def process_image(image):
54
+ """Run detection and classification on an image"""
55
+ detections = []
56
+ results = DETECT_MODEL(image)
57
+
58
+ for r in results:
59
+ for box in r.boxes:
60
+ x1, y1, x2, y2 = map(int, box.xyxy[0])
61
+ crop = image[y1:y2, x1:x2]
62
+ if crop.size == 0:
63
+ continue
64
+
65
+ buck_res = BUCK_DOE_MODEL(crop)
66
+ buck_name = buck_res[0].names[buck_res[0].probs.top1]
67
+
68
+ if buck_name.lower() == "buck":
69
+ type_res = BUCK_TYPE_MODEL(crop)
70
+ type_name = type_res[0].names[type_res[0].probs.top1]
71
+ label = f"Deer | Buck | {type_name}"
72
+ else:
73
+ label = "Deer | Doe"
74
+
75
+ detections.append({
76
+ "label": label,
77
+ "bbox": [x1, y1, x2, y2]
78
+ })
79
+
80
+ return detections
81
+
82
+
83
+
84
+ # ---------------- CAMERA VALIDATION ----------------
85
+ def validate_user_and_camera(user_id: str, camera_name: str):
86
+ if not user_exists(user_id):
87
+ raise HTTPException(404, "User not found")
88
+
89
+ cameras = load_cameras(user_id)
90
+
91
+ if not any(c["camera_name"] == camera_name for c in cameras):
92
+ raise HTTPException(404, "Camera not registered")
93
+
94
+
95
+ # ---------------- IMAGE SAVE ----------------
96
+ def save_image(user_id, camera_name, filename, data):
97
+ path = BASE_DIR / user_id / camera_name / "raw"
98
+ path.mkdir(parents=True, exist_ok=True)
99
+
100
+ local_path = path / filename
101
+ with open(local_path, "wb") as f:
102
+ f.write(data)
103
+
104
+ if STORAGE_BACKEND == "gcs" and gcs_bucket:
105
+ blob = gcs_bucket.blob(f"{GCS_UPLOAD_DIR}{user_id}/{camera_name}/{filename}")
106
+ blob.upload_from_filename(local_path)
107
+ return blob.public_url
108
+
109
+ return f"/user_data/{user_id}/{camera_name}/raw/{filename}"
110
+
111
+
112
+ # ---------------- JSON ----------------
113
+ def load_json(path):
114
+ if Path(path).exists():
115
+ with open(path, "r") as f:
116
+ return json.load(f)
117
+ return []
118
+
119
+ def save_json(path, data):
120
+ with open(path, "w") as f:
121
+ json.dump(data, f, indent=4)
122
+
123
+ # ---------------- USER FOLDERS / CAMERAS ----------------
124
+ BASE_DIR = Path(UPLOAD_DIR)
125
+ BASE_DIR.mkdir(exist_ok=True)
126
+
127
+ def get_user_folder(user_id: str) -> Path:
128
+ """Return path to user's folder WITHOUT creating it"""
129
+ return BASE_DIR / f"{user_id}"
130
+
131
+ def get_user_file(user_id: str) -> Path:
132
+ """Return path to user's cameras.json WITHOUT creating it"""
133
+ return get_user_folder(user_id) / "cameras.json"
134
+
135
+ def user_exists(user_id: str) -> bool:
136
+ return get_user_file(user_id).exists()
137
+
138
+ def load_cameras(user_id: str) -> list:
139
+ path = get_user_file(user_id)
140
+
141
+ if not path.exists():
142
+ return []
143
+
144
+ try:
145
+ with open(path, "r") as f:
146
+ return json.load(f)
147
+ except json.JSONDecodeError:
148
+ return []
149
+
150
+ def save_cameras(user_id: str, cameras: list):
151
+ # Folder only created when we are saving ( Add Camera)
152
+ folder = get_user_folder(user_id)
153
+ folder.mkdir(exist_ok=True)
154
+
155
+ with open(folder / "cameras.json", "w") as f:
156
+ json.dump(cameras, f, indent=2)
157
+
158
+
159
+
160
+ #>>>>>>>>dashboard>>>>>>>>>>>>
161
+ def get_user_dashboard(user_id: str, camera_name: str = None) -> dict:
162
+ """Return analytics for a user or a specific camera"""
163
+ user_folder = Path(UPLOAD_DIR) / user_id
164
+ cameras_file = user_folder / "cameras.json"
165
+
166
+ if not cameras_file.exists():
167
+ raise HTTPException(404, f"User {user_id} not found")
168
+
169
+ try:
170
+ with open(cameras_file, "r") as f:
171
+ cameras = json.load(f)
172
+ except json.JSONDecodeError:
173
+ cameras = []
174
+
175
+ total_cameras = len(cameras)
176
+ total_images = 0
177
+ total_detections = 0
178
+ buck_type_distribution = {}
179
+ buck_doe_distribution = {"Buck": 0, "Doe": 0}
180
+
181
+ for cam in cameras:
182
+ cam_name = cam["camera_name"]
183
+
184
+ # Skip cameras if a specific one is selected
185
+ if camera_name and cam_name != camera_name:
186
+ continue
187
+
188
+ raw_folder = user_folder / cam_name / "raw"
189
+ detections_file = user_folder / cam_name / f"{cam_name}_detections.json"
190
+
191
+ # Count images
192
+ if raw_folder.exists():
193
+ total_images += len(list(raw_folder.glob("*.*")))
194
+ # Count detections and distributions
195
+ if detections_file.exists():
196
+ try:
197
+ dets = json.load(open(detections_file, "r"))
198
+ for rec in dets:
199
+ for d in rec.get("detections", []):
200
+ total_detections += 1
201
+ label = d.get("label", "")
202
+ if "|" in label:
203
+ parts = [p.strip() for p in label.split("|")]
204
+ if len(parts) == 3: # Buck with type
205
+ buck_doe_distribution["Buck"] += 1
206
+ buck_type_distribution[parts[2]] = buck_type_distribution.get(parts[2], 0) + 1
207
+ else: # Doe
208
+ buck_doe_distribution["Doe"] += 1
209
+ except json.JSONDecodeError:
210
+ continue
211
+ return {
212
+ "user_id": user_id,
213
+ "selected_camera": camera_name,
214
+ "total_cameras": total_cameras,
215
+ "images_uploaded": total_images,
216
+ "total_detections": total_detections,
217
+ "buck_type_distribution": buck_type_distribution,
218
+ "buck_doe_distribution": buck_doe_distribution
219
+ }
api/view_image.py ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, HTTPException, Query, Request
2
+ from pathlib import Path
3
+ import json
4
+ from .config import UPLOAD_DIR
5
+
6
+ router = APIRouter()
7
+
8
+ @router.get("/view_images")
9
+ def view_images(
10
+ request: Request,
11
+ user_id: str = Query(...),
12
+ camera_name: str = Query(...),
13
+ filter_label: str = Query(None, description="Optional filter: Buck, Doe, Mule, Whitetail")
14
+ ):
15
+ """
16
+ Get images and detection info for a user's camera.
17
+ Returns clickable URLs for each image.
18
+ Optionally filter images based on labels (Buck, Doe, Mule, Whitetail).
19
+ """
20
+ user_folder = Path(UPLOAD_DIR) / user_id
21
+ if not user_folder.exists():
22
+ raise HTTPException(status_code=404, detail="User not found")
23
+
24
+ camera_folder = user_folder / camera_name
25
+ raw_folder = camera_folder / "raw"
26
+ detection_file = camera_folder / f"{camera_name}_detections.json"
27
+
28
+ if not raw_folder.exists():
29
+ raise HTTPException(status_code=404, detail="Camera raw folder not found")
30
+ if not detection_file.exists():
31
+ raise HTTPException(status_code=404, detail="Detection JSON not found")
32
+
33
+ # Load detection JSON
34
+ try:
35
+ with open(detection_file, "r") as f:
36
+ detections = json.load(f)
37
+ except Exception as e:
38
+ raise HTTPException(status_code=500, detail=f"Failed to read detection file: {e}")
39
+
40
+ # Determine base URL (cloud-safe)
41
+ base_url = str(request.base_url)
42
+ if "0.0.0.0" in base_url or "127.0.0.1" in base_url:
43
+ base_url = base_url.replace("0.0.0.0", "localhost").replace("127.0.0.1", "localhost")
44
+ if not base_url.endswith("/"):
45
+ base_url += "/"
46
+
47
+ # List of labels to filter if provided
48
+ valid_filters = {"buck", "doe", "mule", "whitetail"}
49
+ filter_lower = filter_label.lower() if filter_label else None
50
+ if filter_lower and filter_lower not in valid_filters:
51
+ raise HTTPException(status_code=400, detail=f"Invalid filter_label. Must be one of {valid_filters}")
52
+
53
+ images = []
54
+ for item in detections:
55
+ image_path = raw_folder / item["filename"]
56
+ if image_path.exists():
57
+ item["image_url"] = f"{base_url}user_data/{user_id}/{camera_name}/raw/{item['filename']}"
58
+ else:
59
+ item["missing"] = True
60
+ item["image_url"] = None
61
+
62
+ # Apply label filter if provided
63
+ if filter_lower:
64
+ filtered_detections = [
65
+ det for det in item.get("detections", [])
66
+ if any(lbl.lower().find(filter_lower) != -1 for lbl in det["label"].split("|"))
67
+ ]
68
+ if filtered_detections:
69
+ item["detections"] = filtered_detections
70
+ images.append(item)
71
+ else:
72
+ images.append(item)
73
+
74
+ return {
75
+ "success": True,
76
+ "user_id": user_id,
77
+ "camera_name": camera_name,
78
+ "filter_label": filter_label,
79
+ "images": images
80
+ }
app.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import warnings
2
+ warnings.filterwarnings("ignore", message="Corrupt JPEG data")
3
+
4
+ import os
5
+ from dotenv import load_dotenv
6
+ from pyngrok import ngrok
7
+ import uvicorn
8
+
9
+ load_dotenv()
10
+ NGROK_AUTH_TOKEN = os.getenv("NGROK_AUTH_TOKEN")
11
+
12
+ if NGROK_AUTH_TOKEN:
13
+ ngrok.set_auth_token(NGROK_AUTH_TOKEN)
14
+
15
+
16
+
17
+ # if __name__ == "__main__":
18
+
19
+ # # # Run FastAPI
20
+ # uvicorn.run(
21
+ # "api.main:app",
22
+ # host="0.0.0.0",
23
+ # port=7860,
24
+ # reload=True,
25
+ # log_level="info"
26
+ # )
dockerignore ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Ignore virtual environments
2
+ venv/
3
+ env/
4
+ __pycache__/
5
+ *.pyc
6
+ *.pyo
7
+ *.pyd
8
+
9
+ # Ignore Git and IDE files
10
+ .git
11
+ .gitignore
12
+ .DS_Store
13
+ .vscode/
14
+ .idea/
15
+
16
+ # Ignore local data folders
17
+ api/uploaded_images/
18
+ others/
19
+
20
+ # Ignore temporary or log files
21
+ *.log
22
+ *.tmp
23
+ *.bak
24
+ *.html
git ADDED
File without changes
requirements.txt ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ultralytics
2
+ opencv-python
3
+ numpy
4
+ pillow
5
+ pyngrok
6
+ python-dotenv
7
+ google-cloud-storage
8
+ gunicorn
9
+ waitress
10
+ fastapi
11
+ uvicorn
12
+ python-multipart