Spaces:
Sleeping
Sleeping
Commit ·
f98ae68
1
Parent(s): 436322f
time analytics added
Browse files- api/camera.py +0 -15
- api/config.py +1 -0
- api/utils.py +58 -3
api/camera.py
CHANGED
|
@@ -15,7 +15,6 @@ 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:
|
|
@@ -35,7 +34,6 @@ class EditCameraData(BaseModel):
|
|
| 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:
|
|
@@ -61,13 +59,11 @@ def home():
|
|
| 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 |
|
|
@@ -76,30 +72,23 @@ def get_cameras(user_id: str = Query(...)):
|
|
| 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():
|
|
@@ -107,7 +96,6 @@ def edit_camera(data: EditCameraData):
|
|
| 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)
|
|
@@ -116,7 +104,6 @@ def edit_camera(data: EditCameraData):
|
|
| 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()):
|
|
@@ -128,10 +115,8 @@ def edit_camera(data: EditCameraData):
|
|
| 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 |
|
|
|
|
| 15 |
user_id: str = Field(..., min_length=1)
|
| 16 |
camera_name: str = Field(..., min_length=1)
|
| 17 |
camera_loc: Optional[List[float]] = None
|
|
|
|
| 18 |
@validator("camera_loc")
|
| 19 |
def validate_loc(cls, loc):
|
| 20 |
if loc is None:
|
|
|
|
| 34 |
old_camera_name: str
|
| 35 |
new_camera_name: str
|
| 36 |
new_camera_loc: List[float]
|
|
|
|
| 37 |
@validator("new_camera_loc")
|
| 38 |
def validate_loc(cls, loc):
|
| 39 |
if len(loc) != 2:
|
|
|
|
| 59 |
]
|
| 60 |
}
|
| 61 |
|
|
|
|
| 62 |
# ---------- GET CAMERAS ----------
|
| 63 |
@router.get("/get_cameras")
|
| 64 |
def get_cameras(user_id: str = Query(...)):
|
| 65 |
if not user_exists(user_id):
|
| 66 |
raise HTTPException(status_code=404, detail="User not found")
|
|
|
|
| 67 |
cameras = load_cameras(user_id)
|
| 68 |
return {"success": True, "user_id": user_id, "cameras": cameras, "count": len(cameras)}
|
| 69 |
|
|
|
|
| 72 |
@router.post("/add_camera")
|
| 73 |
def add_camera(data: CameraData):
|
| 74 |
cameras = load_cameras(data.user_id)
|
|
|
|
| 75 |
if len(cameras) >= 2:
|
| 76 |
raise HTTPException(status_code=400, detail="Only 2 cameras allowed")
|
|
|
|
| 77 |
for cam in cameras:
|
| 78 |
if cam["camera_name"].lower() == data.camera_name.lower():
|
| 79 |
raise HTTPException(status_code=400, detail="Camera already exists")
|
|
|
|
| 80 |
cameras.append({"camera_name": data.camera_name, "camera_loc": data.camera_loc})
|
| 81 |
save_cameras(data.user_id, cameras)
|
|
|
|
| 82 |
return {"success": True, "camera": data.camera_name}
|
| 83 |
|
| 84 |
@router.put("/edit_camera")
|
| 85 |
def edit_camera(data: EditCameraData):
|
| 86 |
if not user_exists(data.user_id):
|
| 87 |
raise HTTPException(status_code=404, detail="User not found")
|
|
|
|
| 88 |
cameras = load_cameras(data.user_id)
|
|
|
|
| 89 |
# Check if new camera name already exists (case-insensitive)
|
| 90 |
if any(cam["camera_name"].lower() == data.new_camera_name.lower() for cam in cameras):
|
| 91 |
raise HTTPException(status_code=400, detail=f"Camera name '{data.new_camera_name}' already exists")
|
|
|
|
| 92 |
camera_found = False
|
| 93 |
for cam in cameras:
|
| 94 |
if cam["camera_name"].lower() == data.old_camera_name.lower():
|
|
|
|
| 96 |
cam["camera_name"] = data.new_camera_name
|
| 97 |
cam["camera_loc"] = data.new_camera_loc
|
| 98 |
camera_found = True
|
|
|
|
| 99 |
# Rename camera folder
|
| 100 |
old_folder = os.path.join(UPLOAD_DIR, data.user_id, old_name)
|
| 101 |
new_folder = os.path.join(UPLOAD_DIR, data.user_id, data.new_camera_name)
|
|
|
|
| 104 |
os.rename(old_folder, new_folder)
|
| 105 |
except Exception as e:
|
| 106 |
raise HTTPException(status_code=500, detail=f"Failed to rename camera folder: {str(e)}")
|
|
|
|
| 107 |
# Rename detection JSON
|
| 108 |
for f in os.listdir(new_folder):
|
| 109 |
if f.endswith("_detections.json") and f.lower().startswith(old_name.lower()):
|
|
|
|
| 115 |
except Exception as e:
|
| 116 |
raise HTTPException(status_code=500, detail=f"Failed to rename detection JSON: {str(e)}")
|
| 117 |
break # Only one detection JSON per camera
|
|
|
|
| 118 |
save_cameras(data.user_id, cameras)
|
| 119 |
return {"success": True, "updated": cam}
|
|
|
|
| 120 |
if not camera_found:
|
| 121 |
raise HTTPException(status_code=404, detail="Camera not found")
|
| 122 |
|
api/config.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
import os
|
| 2 |
import logging
|
|
|
|
| 3 |
from dotenv import load_dotenv
|
| 4 |
from ultralytics import YOLO
|
| 5 |
from google.cloud import storage
|
|
|
|
| 1 |
import os
|
| 2 |
import logging
|
| 3 |
+
from zipfile import Path
|
| 4 |
from dotenv import load_dotenv
|
| 5 |
from ultralytics import YOLO
|
| 6 |
from google.cloud import storage
|
api/utils.py
CHANGED
|
@@ -189,7 +189,6 @@ def save_cameras(user_id: str, cameras: list):
|
|
| 189 |
json.dump(cameras, f, indent=2)
|
| 190 |
|
| 191 |
|
| 192 |
-
|
| 193 |
#>>>>>>>>dashboard>>>>>>>>>>>>
|
| 194 |
def get_user_dashboard(user_id: str, camera_name: str = None) -> dict:
|
| 195 |
"""Return analytics for a user or a specific camera"""
|
|
@@ -197,31 +196,47 @@ def get_user_dashboard(user_id: str, camera_name: str = None) -> dict:
|
|
| 197 |
cameras_file = user_folder / "cameras.json"
|
| 198 |
if not cameras_file.exists():
|
| 199 |
raise HTTPException(404, f"User {user_id} not found")
|
|
|
|
| 200 |
try:
|
| 201 |
with open(cameras_file, "r") as f:
|
| 202 |
cameras = json.load(f)
|
| 203 |
except json.JSONDecodeError:
|
| 204 |
cameras = []
|
|
|
|
| 205 |
total_cameras = len(cameras)
|
| 206 |
total_images = 0
|
| 207 |
total_detections = 0
|
| 208 |
buck_type_distribution = {}
|
| 209 |
buck_doe_distribution = {"Buck": 0, "Doe": 0}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 210 |
for cam in cameras:
|
| 211 |
cam_name = cam["camera_name"]
|
| 212 |
-
# Skip cameras if a specific one is selected
|
| 213 |
if camera_name and cam_name != camera_name:
|
| 214 |
continue
|
|
|
|
| 215 |
raw_folder = user_folder / cam_name / "raw"
|
| 216 |
detections_file = user_folder / cam_name / f"{cam_name}_detections.json"
|
|
|
|
| 217 |
# Count images
|
| 218 |
if raw_folder.exists():
|
| 219 |
total_images += len(list(raw_folder.glob("*.*")))
|
|
|
|
| 220 |
# Count detections and distributions
|
| 221 |
if detections_file.exists():
|
| 222 |
try:
|
| 223 |
dets = json.load(open(detections_file, "r"))
|
| 224 |
for rec in dets:
|
|
|
|
| 225 |
for d in rec.get("detections", []):
|
| 226 |
total_detections += 1
|
| 227 |
label = d.get("label", "")
|
|
@@ -232,8 +247,38 @@ def get_user_dashboard(user_id: str, camera_name: str = None) -> dict:
|
|
| 232 |
buck_type_distribution[parts[2]] = buck_type_distribution.get(parts[2], 0) + 1
|
| 233 |
else: # Doe
|
| 234 |
buck_doe_distribution["Doe"] += 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 235 |
except json.JSONDecodeError:
|
| 236 |
continue
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 237 |
return {
|
| 238 |
"user_id": user_id,
|
| 239 |
"selected_camera": camera_name,
|
|
@@ -241,5 +286,15 @@ def get_user_dashboard(user_id: str, camera_name: str = None) -> dict:
|
|
| 241 |
"images_uploaded": total_images,
|
| 242 |
"total_detections": total_detections,
|
| 243 |
"buck_type_distribution": buck_type_distribution,
|
| 244 |
-
"buck_doe_distribution": buck_doe_distribution
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 245 |
}
|
|
|
|
|
|
| 189 |
json.dump(cameras, f, indent=2)
|
| 190 |
|
| 191 |
|
|
|
|
| 192 |
#>>>>>>>>dashboard>>>>>>>>>>>>
|
| 193 |
def get_user_dashboard(user_id: str, camera_name: str = None) -> dict:
|
| 194 |
"""Return analytics for a user or a specific camera"""
|
|
|
|
| 196 |
cameras_file = user_folder / "cameras.json"
|
| 197 |
if not cameras_file.exists():
|
| 198 |
raise HTTPException(404, f"User {user_id} not found")
|
| 199 |
+
|
| 200 |
try:
|
| 201 |
with open(cameras_file, "r") as f:
|
| 202 |
cameras = json.load(f)
|
| 203 |
except json.JSONDecodeError:
|
| 204 |
cameras = []
|
| 205 |
+
|
| 206 |
total_cameras = len(cameras)
|
| 207 |
total_images = 0
|
| 208 |
total_detections = 0
|
| 209 |
buck_type_distribution = {}
|
| 210 |
buck_doe_distribution = {"Buck": 0, "Doe": 0}
|
| 211 |
+
|
| 212 |
+
# New dashboard analytics
|
| 213 |
+
from collections import defaultdict, Counter
|
| 214 |
+
from datetime import datetime
|
| 215 |
+
|
| 216 |
+
heatmap = defaultdict(lambda: [0]*24) # day -> 24 hours
|
| 217 |
+
deer_per_day = Counter()
|
| 218 |
+
bucks_per_day = Counter()
|
| 219 |
+
does_per_day = Counter()
|
| 220 |
+
hour_activity = [0]*24 # 0-23 hours
|
| 221 |
+
|
| 222 |
for cam in cameras:
|
| 223 |
cam_name = cam["camera_name"]
|
|
|
|
| 224 |
if camera_name and cam_name != camera_name:
|
| 225 |
continue
|
| 226 |
+
|
| 227 |
raw_folder = user_folder / cam_name / "raw"
|
| 228 |
detections_file = user_folder / cam_name / f"{cam_name}_detections.json"
|
| 229 |
+
|
| 230 |
# Count images
|
| 231 |
if raw_folder.exists():
|
| 232 |
total_images += len(list(raw_folder.glob("*.*")))
|
| 233 |
+
|
| 234 |
# Count detections and distributions
|
| 235 |
if detections_file.exists():
|
| 236 |
try:
|
| 237 |
dets = json.load(open(detections_file, "r"))
|
| 238 |
for rec in dets:
|
| 239 |
+
# --- Existing Buck/Doe counts ---
|
| 240 |
for d in rec.get("detections", []):
|
| 241 |
total_detections += 1
|
| 242 |
label = d.get("label", "")
|
|
|
|
| 247 |
buck_type_distribution[parts[2]] = buck_type_distribution.get(parts[2], 0) + 1
|
| 248 |
else: # Doe
|
| 249 |
buck_doe_distribution["Doe"] += 1
|
| 250 |
+
|
| 251 |
+
# --- New analytics using datetime_original ---
|
| 252 |
+
dt_str = rec.get("metadata", {}).get("exif", {}).get("datetime_original")
|
| 253 |
+
if dt_str:
|
| 254 |
+
dt = datetime.strptime(dt_str, "%Y:%m:%d %H:%M:%S")
|
| 255 |
+
day = dt.date()
|
| 256 |
+
hour = dt.hour
|
| 257 |
+
|
| 258 |
+
# Heatmap count
|
| 259 |
+
heatmap[day][hour] += len(rec.get("detections", []))
|
| 260 |
+
|
| 261 |
+
# Count deer, bucks, does per day
|
| 262 |
+
for d in rec.get("detections", []):
|
| 263 |
+
label = d.get("label", "")
|
| 264 |
+
if "Deer" in label:
|
| 265 |
+
deer_per_day[day] += 1
|
| 266 |
+
if "Buck" in label:
|
| 267 |
+
bucks_per_day[day] += 1
|
| 268 |
+
if "Doe" in label:
|
| 269 |
+
does_per_day[day] += 1
|
| 270 |
+
|
| 271 |
+
# Hourly aggregated activity
|
| 272 |
+
hour_activity[hour] += len(rec.get("detections", []))
|
| 273 |
except json.JSONDecodeError:
|
| 274 |
continue
|
| 275 |
+
|
| 276 |
+
# Average activity by hour (morning/night)
|
| 277 |
+
morning_hours = range(6, 18)
|
| 278 |
+
night_hours = list(range(0,6)) + list(range(18,24))
|
| 279 |
+
morning_activity = sum(hour_activity[h] for h in morning_hours) / len(morning_hours)
|
| 280 |
+
night_activity = sum(hour_activity[h] for h in night_hours) / len(night_hours)
|
| 281 |
+
|
| 282 |
return {
|
| 283 |
"user_id": user_id,
|
| 284 |
"selected_camera": camera_name,
|
|
|
|
| 286 |
"images_uploaded": total_images,
|
| 287 |
"total_detections": total_detections,
|
| 288 |
"buck_type_distribution": buck_type_distribution,
|
| 289 |
+
"buck_doe_distribution": buck_doe_distribution,
|
| 290 |
+
# --- New analytics ---
|
| 291 |
+
"activity_heatmap": dict(heatmap),
|
| 292 |
+
"deer_per_day": dict(deer_per_day),
|
| 293 |
+
"bucks_per_day": dict(bucks_per_day),
|
| 294 |
+
"does_per_day": dict(does_per_day),
|
| 295 |
+
"average_activity": {
|
| 296 |
+
"morning": round(morning_activity,2),
|
| 297 |
+
"night": round(night_activity,2)
|
| 298 |
+
}
|
| 299 |
}
|
| 300 |
+
|