codewithRiz commited on
Commit
a918efd
Β·
1 Parent(s): 1143d23

bucket storage

Browse files
Files changed (8) hide show
  1. .gitignore +1 -0
  2. api/camera.py +1 -1
  3. api/config.py +8 -35
  4. api/detection.py +131 -130
  5. api/main.py +1 -2
  6. api/utils.py +168 -88
  7. api/view_image.py +37 -30
  8. requirements.txt +0 -1
.gitignore CHANGED
@@ -64,3 +64,4 @@ gradio_cached_examples/
64
  # __pycache__ in subfolders
65
  */__pycache__/
66
 
 
 
64
  # __pycache__ in subfolders
65
  */__pycache__/
66
 
67
+ bucket.py
api/camera.py CHANGED
@@ -2,7 +2,7 @@ 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
 
 
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
6
  import os
7
  import shutil
8
 
api/config.py CHANGED
@@ -1,12 +1,9 @@
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
7
- import google.auth
8
 
9
- # ---------------- ENV ----------------
10
  load_dotenv(override=True)
11
  BASE_DIR = os.path.dirname(os.path.abspath(__file__))
12
 
@@ -25,33 +22,9 @@ ENV = os.getenv("ENV", "DEV").upper()
25
  logger.info(f"Running in {ENV}")
26
 
27
  # ---------------- STORAGE ----------------
28
- UPLOAD_DIR = os.path.join(BASE_DIR, "user_data")
29
- os.makedirs(UPLOAD_DIR, exist_ok=True)
30
-
31
- STORAGE_BACKEND = "local"
32
- gcs_client = None
33
- gcs_bucket = None
34
- GCS_UPLOAD_DIR = "uploaded_images/"
35
-
36
- if ENV == "PROD":
37
- STORAGE_BACKEND = os.getenv("PROD_STORAGE_BACKEND", "gcs").lower()
38
- GCS_BUCKET_NAME = os.getenv("PROD_GCS_BUCKET_NAME")
39
- GCS_KEY_PATH = os.getenv("PROD_GOOGLE_APPLICATION_CREDENTIALS")
40
-
41
- if STORAGE_BACKEND == "gcs":
42
- try:
43
- if os.getenv("GOOGLE_CLOUD_PROJECT"):
44
- creds, project = google.auth.default()
45
- gcs_client = storage.Client(credentials=creds, project=project)
46
- else:
47
- gcs_client = storage.Client.from_service_account_json(GCS_KEY_PATH)
48
-
49
- gcs_bucket = gcs_client.bucket(GCS_BUCKET_NAME)
50
- logger.info(f"Connected to GCS bucket: {GCS_BUCKET_NAME}")
51
-
52
- except Exception as e:
53
- logger.error(f"GCS connection failed: {e}")
54
- STORAGE_BACKEND = "local"
55
 
56
  # ---------------- UPLOAD RULES ----------------
57
  MIN_IMAGES = 1
@@ -61,10 +34,10 @@ ALLOWED_EXTENSIONS = {"jpg", "jpeg", "png", "webp"}
61
  # ---------------- YOLO MODELS ----------------
62
  try:
63
  logger.info("Loading YOLO models...")
64
- DETECT_MODEL = YOLO("api/walidlife_models/detect/deer.pt")
65
- BUCK_DOE_MODEL = YOLO("api/walidlife_models/classify/Buck_classificationt.pt", task="classify")
66
- BUCK_TYPE_MODEL = YOLO("api/walidlife_models/classify/mules_vs_whitetails.pt", task="classify")
67
  logger.info("YOLO models loaded")
68
  except Exception as e:
69
  logger.error(f"YOLO load failed: {e}")
70
- DETECT_MODEL = BUCK_DOE_MODEL = BUCK_TYPE_MODEL = None
 
1
  import os
2
  import logging
3
+ from pathlib import Path
4
  from dotenv import load_dotenv
5
  from ultralytics import YOLO
 
 
6
 
 
7
  load_dotenv(override=True)
8
  BASE_DIR = os.path.dirname(os.path.abspath(__file__))
9
 
 
22
  logger.info(f"Running in {ENV}")
23
 
24
  # ---------------- STORAGE ----------------
25
+ # Use Hugging Face bucket as UPLOAD_DIR
26
+ UPLOAD_DIR = "codewithRiz/test_bucket"
27
+ STORAGE_BACKEND = "huggingface" # indicate we are using HF bucket
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
 
29
  # ---------------- UPLOAD RULES ----------------
30
  MIN_IMAGES = 1
 
34
  # ---------------- YOLO MODELS ----------------
35
  try:
36
  logger.info("Loading YOLO models...")
37
+ DETECT_MODEL = YOLO("api/walidlife_models/detect/deer.onnx")
38
+ BUCK_DOE_MODEL = YOLO("api/walidlife_models/classify/Buck_classificationt.onnx", task="classify")
39
+ BUCK_TYPE_MODEL = YOLO("api/walidlife_models/classify/mules_vs_whitetails.onnx", task="classify")
40
  logger.info("YOLO models loaded")
41
  except Exception as e:
42
  logger.error(f"YOLO load failed: {e}")
43
+ DETECT_MODEL = BUCK_DOE_MODEL = BUCK_TYPE_MODEL = None
api/detection.py CHANGED
@@ -50,17 +50,14 @@
50
  # "camera": camera_name,
51
  # "results": new_results
52
  # }
53
-
54
-
55
-
56
  from fastapi import APIRouter, UploadFile, File, Form, HTTPException
57
  from pydantic import BaseModel
58
- from pathlib import Path
59
  from typing import Optional, List, Literal
60
  import cv2
61
  import numpy as np
62
  import logging
63
- from .config import UPLOAD_DIR
 
64
  from .utils import (
65
  validate_form,
66
  process_image,
@@ -68,16 +65,14 @@ from .utils import (
68
  load_json,
69
  save_json,
70
  validate_user_and_camera,
71
- extract_metadata
 
72
  )
73
 
74
-
75
-
76
  router = APIRouter()
77
  logger = logging.getLogger(__name__)
78
 
79
 
80
- # ─── existing endpoint
81
  @router.post("/predict")
82
  async def predict(
83
  user_id: str = Form(...),
@@ -86,11 +81,12 @@ async def predict(
86
  ):
87
  images = validate_form(user_id, camera_name, images)
88
  validate_user_and_camera(user_id, camera_name)
89
- base = Path(UPLOAD_DIR) / user_id / camera_name
90
- base.mkdir(parents=True, exist_ok=True)
91
- json_path = base / f"{camera_name}_detections.json"
92
  data = load_json(json_path)
93
  new_results = []
 
94
  for file in images:
95
  raw = await file.read()
96
  metadata = extract_metadata(raw)
@@ -98,7 +94,11 @@ async def predict(
98
  img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
99
  if img is None:
100
  raise HTTPException(400, f"Invalid image: {file.filename}")
 
 
101
  detections = process_image(img)
 
 
102
  url = save_image(user_id, camera_name, file.filename, raw)
103
  record = {
104
  "filename": file.filename,
@@ -108,139 +108,140 @@ async def predict(
108
  }
109
  data.append(record)
110
  new_results.append(record)
 
111
  save_json(json_path, data)
 
112
  return {
113
  "message": "Images processed successfully",
114
  "camera": camera_name,
115
  "results": new_results
116
  }
117
-
118
  # ─────────────────
119
  # Request Models
120
  # ─────────────────
121
 
122
- class DetectionOperation(BaseModel):
123
- action: Literal["add", "update", "delete"]
124
- detection_index: Optional[int] = None
125
- label: Optional[str] = None
126
- bbox: Optional[List[float]] = None # [x1, y1, x2, y2]
127
 
128
 
129
- class MultiUpdateRequest(BaseModel):
130
- user_id: str
131
- camera_name: str
132
- image_url: str
133
- operations: List[DetectionOperation]
134
 
135
 
136
- # ─────────────────
137
- # Endpoint
138
- # ─────────────────
139
 
140
- @router.post("/modify_detections")
141
- async def modify_detections(req: MultiUpdateRequest):
142
- """
143
- Add, update, and delete detections (tags) for a given image.
144
- Supports multiple operations in a single request.
145
- """
146
-
147
- # Validate user
148
- user_path = Path(UPLOAD_DIR) / req.user_id
149
- if not user_path.exists() or not user_path.is_dir():
150
- raise HTTPException(status_code=404, detail="user not found")
151
-
152
- # Validate camera
153
- camera_path = user_path / req.camera_name
154
- if not camera_path.exists() or not camera_path.is_dir():
155
- raise HTTPException(status_code=404, detail="camera not found")
156
-
157
- # Validate JSON file
158
- json_path = camera_path / f"{req.camera_name}_detections.json"
159
- if not json_path.exists():
160
- raise HTTPException(status_code=404, detail="detections file not found")
161
-
162
- # Load data
163
- data = load_json(json_path)
164
 
165
- # Find image record
166
- target_filename = req.image_url.split("/")[-1].split("?")[0]
167
-
168
- record = None
169
- for item in data:
170
- stored = item.get("image_url", item.get("filename", ""))
171
- stored_filename = stored.split("/")[-1].split("?")[0]
172
- if stored_filename == target_filename:
173
- record = item
174
- break
175
-
176
- if record is None:
177
- raise HTTPException(status_code=404, detail="image not found")
178
-
179
- # ── 6. Ensure detections list exists
180
- if "detections" not in record or not isinstance(record["detections"], list):
181
- record["detections"] = []
182
-
183
- dets = record["detections"]
184
-
185
- # ── 7. Apply operations safely ──────
186
- # NOTE: Reverse delete operations to avoid index shifting issues
187
- delete_ops = [op for op in req.operations if op.action == "delete"]
188
- other_ops = [op for op in req.operations if op.action != "delete"]
189
-
190
- # Handle DELETE (reverse order)
191
- for op in sorted(delete_ops, key=lambda x: x.detection_index or -1, reverse=True):
192
- if op.detection_index is None or op.detection_index >= len(dets):
193
- raise HTTPException(
194
- status_code=400,
195
- detail=f"Invalid delete index {op.detection_index}"
196
- )
197
- dets.pop(op.detection_index)
198
-
199
- # Handle ADD + UPDATE
200
- for op in other_ops:
201
-
202
- # ADD
203
- if op.action == "add":
204
- dets.append({
205
- "label": op.label or "Unknown",
206
- "confidence": 1.0,
207
- "bbox": op.bbox or [],
208
- "manually_edited": True
209
- })
210
-
211
- # UPDATE
212
- elif op.action == "update":
213
- if op.detection_index is None or op.detection_index >= len(dets):
214
- raise HTTPException(
215
- status_code=400,
216
- detail=f"Invalid update index {op.detection_index}"
217
- )
218
-
219
- if op.label is not None:
220
- dets[op.detection_index]["label"] = op.label
221
-
222
- if op.bbox is not None:
223
- dets[op.detection_index]["bbox"] = op.bbox
224
-
225
- dets[op.detection_index]["manually_edited"] = True
226
-
227
- # ── 8. Save back ────────────────────
228
- save_json(json_path, data)
229
 
230
- logger.info(
231
- "Detections modified | user=%s camera=%s file=%s ops=%d final_count=%d",
232
- req.user_id,
233
- req.camera_name,
234
- target_filename,
235
- len(req.operations),
236
- len(dets)
237
- )
238
 
239
- # ── 9. Response ─────────────────────
240
- return {
241
- "success": True,
242
- "message": "Detections modified successfully",
243
- "filename": target_filename,
244
- "total_detections": len(dets),
245
- "detections": dets
246
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  # "camera": camera_name,
51
  # "results": new_results
52
  # }
 
 
 
53
  from fastapi import APIRouter, UploadFile, File, Form, HTTPException
54
  from pydantic import BaseModel
 
55
  from typing import Optional, List, Literal
56
  import cv2
57
  import numpy as np
58
  import logging
59
+ import time
60
+
61
  from .utils import (
62
  validate_form,
63
  process_image,
 
65
  load_json,
66
  save_json,
67
  validate_user_and_camera,
68
+ extract_metadata,
69
+ _bucket_key,
70
  )
71
 
 
 
72
  router = APIRouter()
73
  logger = logging.getLogger(__name__)
74
 
75
 
 
76
  @router.post("/predict")
77
  async def predict(
78
  user_id: str = Form(...),
 
81
  ):
82
  images = validate_form(user_id, camera_name, images)
83
  validate_user_and_camera(user_id, camera_name)
84
+
85
+ json_path = _bucket_key(user_id, camera_name, f"{camera_name}_detections.json")
86
+
87
  data = load_json(json_path)
88
  new_results = []
89
+
90
  for file in images:
91
  raw = await file.read()
92
  metadata = extract_metadata(raw)
 
94
  img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
95
  if img is None:
96
  raise HTTPException(400, f"Invalid image: {file.filename}")
97
+
98
+ t0 = time.perf_counter()
99
  detections = process_image(img)
100
+ logger.info(f"[{file.filename}] inference: {round((time.perf_counter() - t0) * 1000, 2)}ms")
101
+
102
  url = save_image(user_id, camera_name, file.filename, raw)
103
  record = {
104
  "filename": file.filename,
 
108
  }
109
  data.append(record)
110
  new_results.append(record)
111
+
112
  save_json(json_path, data)
113
+
114
  return {
115
  "message": "Images processed successfully",
116
  "camera": camera_name,
117
  "results": new_results
118
  }
 
119
  # ─────────────────
120
  # Request Models
121
  # ─────────────────
122
 
123
+ # class DetectionOperation(BaseModel):
124
+ # action: Literal["add", "update", "delete"]
125
+ # detection_index: Optional[int] = None
126
+ # label: Optional[str] = None
127
+ # bbox: Optional[List[float]] = None # [x1, y1, x2, y2]
128
 
129
 
130
+ # class MultiUpdateRequest(BaseModel):
131
+ # user_id: str
132
+ # camera_name: str
133
+ # image_url: str
134
+ # operations: List[DetectionOperation]
135
 
136
 
137
+ # # ─────────────────
138
+ # # Endpoint
139
+ # # ─────────────────
140
 
141
+ # @router.post("/modify_detections")
142
+ # async def modify_detections(req: MultiUpdateRequest):
143
+ # """
144
+ # Add, update, and delete detections (tags) for a given image.
145
+ # Supports multiple operations in a single request.
146
+ # """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
 
148
+ # # Validate user
149
+ # user_path = Path(UPLOAD_DIR) / req.user_id
150
+ # if not user_path.exists() or not user_path.is_dir():
151
+ # raise HTTPException(status_code=404, detail="user not found")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
 
153
+ # # Validate camera
154
+ # camera_path = user_path / req.camera_name
155
+ # if not camera_path.exists() or not camera_path.is_dir():
156
+ # raise HTTPException(status_code=404, detail="camera not found")
 
 
 
 
157
 
158
+ # # Validate JSON file
159
+ # json_path = camera_path / f"{req.camera_name}_detections.json"
160
+ # if not json_path.exists():
161
+ # raise HTTPException(status_code=404, detail="detections file not found")
162
+
163
+ # # Load data
164
+ # data = load_json(json_path)
165
+
166
+ # # Find image record
167
+ # target_filename = req.image_url.split("/")[-1].split("?")[0]
168
+
169
+ # record = None
170
+ # for item in data:
171
+ # stored = item.get("image_url", item.get("filename", ""))
172
+ # stored_filename = stored.split("/")[-1].split("?")[0]
173
+ # if stored_filename == target_filename:
174
+ # record = item
175
+ # break
176
+
177
+ # if record is None:
178
+ # raise HTTPException(status_code=404, detail="image not found")
179
+
180
+ # # ── 6. Ensure detections list exists
181
+ # if "detections" not in record or not isinstance(record["detections"], list):
182
+ # record["detections"] = []
183
+
184
+ # dets = record["detections"]
185
+
186
+ # # ── 7. Apply operations safely ──────
187
+ # # NOTE: Reverse delete operations to avoid index shifting issues
188
+ # delete_ops = [op for op in req.operations if op.action == "delete"]
189
+ # other_ops = [op for op in req.operations if op.action != "delete"]
190
+
191
+ # # Handle DELETE (reverse order)
192
+ # for op in sorted(delete_ops, key=lambda x: x.detection_index or -1, reverse=True):
193
+ # if op.detection_index is None or op.detection_index >= len(dets):
194
+ # raise HTTPException(
195
+ # status_code=400,
196
+ # detail=f"Invalid delete index {op.detection_index}"
197
+ # )
198
+ # dets.pop(op.detection_index)
199
+
200
+ # # Handle ADD + UPDATE
201
+ # for op in other_ops:
202
+
203
+ # # ADD
204
+ # if op.action == "add":
205
+ # dets.append({
206
+ # "label": op.label or "Unknown",
207
+ # "confidence": 1.0,
208
+ # "bbox": op.bbox or [],
209
+ # "manually_edited": True
210
+ # })
211
+
212
+ # # UPDATE
213
+ # elif op.action == "update":
214
+ # if op.detection_index is None or op.detection_index >= len(dets):
215
+ # raise HTTPException(
216
+ # status_code=400,
217
+ # detail=f"Invalid update index {op.detection_index}"
218
+ # )
219
+
220
+ # if op.label is not None:
221
+ # dets[op.detection_index]["label"] = op.label
222
+
223
+ # if op.bbox is not None:
224
+ # dets[op.detection_index]["bbox"] = op.bbox
225
+
226
+ # dets[op.detection_index]["manually_edited"] = True
227
+
228
+ # # ── 8. Save back ────────────────────
229
+ # save_json(json_path, data)
230
+
231
+ # logger.info(
232
+ # "Detections modified | user=%s camera=%s file=%s ops=%d final_count=%d",
233
+ # req.user_id,
234
+ # req.camera_name,
235
+ # target_filename,
236
+ # len(req.operations),
237
+ # len(dets)
238
+ # )
239
+
240
+ # # ── 9. Response ─────────────────────
241
+ # return {
242
+ # "success": True,
243
+ # "message": "Detections modified successfully",
244
+ # "filename": target_filename,
245
+ # "total_detections": len(dets),
246
+ # "detections": dets
247
+ # }
api/main.py CHANGED
@@ -30,8 +30,7 @@ def create_app() -> FastAPI:
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','https://a8a2-185-134-22-81.ngrok-free.app'
35
  ],
36
  allow_credentials=True,
37
  allow_methods=["*"],
 
30
  app.add_middleware(
31
  CORSMiddleware,
32
  allow_origins=[
33
+ 'https://www.daleandcompany.com','http://127.0.0.1:8080'
 
34
  ],
35
  allow_credentials=True,
36
  allow_methods=["*"],
api/utils.py CHANGED
@@ -1,5 +1,6 @@
1
  import os
2
  import json
 
3
  from pathlib import Path
4
  from fastapi import HTTPException
5
  import cv2
@@ -7,6 +8,16 @@ import numpy as np
7
  from datetime import datetime
8
  from exif import Image as ExifImage
9
  from io import BytesIO
 
 
 
 
 
 
 
 
 
 
10
  # ---------------- CONFIG IMPORTS ----------------
11
  from .config import (
12
  DETECT_MODEL,
@@ -15,14 +26,91 @@ from .config import (
15
  ALLOWED_EXTENSIONS,
16
  MIN_IMAGES,
17
  MAX_IMAGES,
18
- UPLOAD_DIR,
19
- STORAGE_BACKEND,
20
- gcs_bucket,
21
- GCS_UPLOAD_DIR,
22
- logger
23
  )
24
 
25
- # ---------------- VALIDATION ----------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  def validate_form(user_id, camera_name, images):
27
  if not user_id or not user_id.strip():
28
  raise HTTPException(400, "user_id is required")
@@ -44,6 +132,9 @@ def validate_form(user_id, camera_name, images):
44
  return images
45
 
46
 
 
 
 
47
 
48
  def make_json_safe(value):
49
  """Convert EXIF values to JSON-serializable types"""
@@ -82,11 +173,14 @@ def extract_metadata(image_bytes):
82
  return metadata
83
 
84
 
85
- # ---------------- IMAGE PROCESSING ----------------
 
 
 
86
  def process_image(image):
87
  """Run 3-stage detection and classification with dynamic confidence"""
88
  detections = []
89
- results = DETECT_MODEL(image,conf=0.8 ,iou=0.4,agnostic_nms=True) # Stage 1: Deer detection
90
  for r in results:
91
  for box in r.boxes:
92
  x1, y1, x2, y2 = map(int, box.xyxy[0])
@@ -94,7 +188,7 @@ def process_image(image):
94
  if crop.size == 0:
95
  continue
96
  # ---------------- Stage 2: Buck/Doe ----------------
97
- buck_res = BUCK_DOE_MODEL(crop)
98
  buck_probs = buck_res[0].probs
99
  top1_idx = buck_probs.top1
100
  buck_name = buck_res[0].names[top1_idx]
@@ -120,9 +214,10 @@ def process_image(image):
120
  return detections
121
 
122
 
 
 
 
123
 
124
-
125
- # ---------------- CAMERA VALIDATION ----------------
126
  def validate_user_and_camera(user_id: str, camera_name: str):
127
  if not user_exists(user_id):
128
  raise HTTPException(404, "User not found")
@@ -131,110 +226,96 @@ def validate_user_and_camera(user_id: str, camera_name: str):
131
  raise HTTPException(404, "Camera not registered")
132
 
133
 
134
- # ---------------- IMAGE SAVE ----------------
 
 
 
135
  def save_image(user_id, camera_name, filename, data):
136
- path = BASE_DIR / user_id / camera_name / "raw"
137
- path.mkdir(parents=True, exist_ok=True)
138
- local_path = path / filename
139
- with open(local_path, "wb") as f:
140
- f.write(data)
141
- if STORAGE_BACKEND == "gcs" and gcs_bucket:
142
- blob = gcs_bucket.blob(f"{GCS_UPLOAD_DIR}{user_id}/{camera_name}/{filename}")
143
- blob.upload_from_filename(local_path)
144
- return blob.public_url
145
- return f"/user_data/{user_id}/{camera_name}/raw/{filename}"
146
-
147
-
148
- # ---------------- JSON ----------------
149
  def load_json(path):
150
- if Path(path).exists():
151
- with open(path, "r") as f:
152
- return json.load(f)
153
- return []
154
 
155
- def save_json(path, data):
156
- with open(path, "w") as f:
157
- json.dump(data, f, indent=4)
158
 
159
- # ---------------- USER FOLDERS / CAMERAS ----------------
160
- BASE_DIR = Path(UPLOAD_DIR)
161
- BASE_DIR.mkdir(exist_ok=True)
162
 
163
- def get_user_folder(user_id: str) -> Path:
164
- """Return path to user's folder WITHOUT creating it"""
165
- return BASE_DIR / f"{user_id}"
166
 
167
- def get_user_file(user_id: str) -> Path:
168
- """Return path to user's cameras.json WITHOUT creating it"""
169
- return get_user_folder(user_id) / "cameras.json"
170
 
171
  def user_exists(user_id: str) -> bool:
172
- return get_user_file(user_id).exists()
 
173
 
174
  def load_cameras(user_id: str) -> list:
175
  path = get_user_file(user_id)
176
- if not path.exists():
177
- return []
178
  try:
179
- with open(path, "r") as f:
180
- return json.load(f)
181
- except json.JSONDecodeError:
182
  return []
183
 
 
184
  def save_cameras(user_id: str, cameras: list):
185
- # Folder only created when we are saving ( Add Camera)
186
- folder = get_user_folder(user_id)
187
- folder.mkdir(exist_ok=True)
188
- with open(folder / "cameras.json", "w") as f:
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"""
195
- user_folder = Path(UPLOAD_DIR) / user_id
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", []):
@@ -247,17 +328,17 @@ def get_user_dashboard(user_id: str, camera_name: str = None) -> dict:
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", "")
@@ -267,15 +348,15 @@ def get_user_dashboard(user_id: str, camera_name: str = None) -> dict:
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
 
@@ -293,8 +374,7 @@ def get_user_dashboard(user_id: str, camera_name: str = None) -> dict:
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
-
 
1
  import os
2
  import json
3
+ import tempfile
4
  from pathlib import Path
5
  from fastapi import HTTPException
6
  import cv2
 
8
  from datetime import datetime
9
  from exif import Image as ExifImage
10
  from io import BytesIO
11
+ from collections import defaultdict, Counter
12
+
13
+ # HuggingFace bucket API
14
+ from huggingface_hub import (
15
+ list_bucket_tree,
16
+ batch_bucket_files,
17
+ download_bucket_files,
18
+ get_bucket_paths_info,
19
+ )
20
+
21
  # ---------------- CONFIG IMPORTS ----------------
22
  from .config import (
23
  DETECT_MODEL,
 
26
  ALLOWED_EXTENSIONS,
27
  MIN_IMAGES,
28
  MAX_IMAGES,
29
+ UPLOAD_DIR, # e.g. "codewithRiz/test_bucket"
30
+ logger,
 
 
 
31
  )
32
 
33
+ # ----------------------------------------------------------------
34
+ # BUCKET SETUP
35
+ # All data is stored under:
36
+ # user_data/<user_id>/cameras.json
37
+ # user_data/<user_id>/<camera_name>/raw/<filename>
38
+ # user_data/<user_id>/<camera_name>/<camera_name>_detections.json
39
+ # ----------------------------------------------------------------
40
+ BUCKET_ID = UPLOAD_DIR # "namespace/bucket-name"
41
+ BASE_DIR = "user_data" # top-level folder inside the bucket
42
+ STORAGE_BACKEND = "huggingface"
43
+
44
+
45
+ # ================================================================
46
+ # BUCKET INTERNAL HELPERS (replace local Path / open / json.load)
47
+ # ================================================================
48
+
49
+ def _bucket_key(user_id: str, *parts: str) -> str:
50
+ """Build a bucket key: user_data/<user_id>/<parts...>"""
51
+ return "/".join([BASE_DIR, user_id, *parts])
52
+
53
+
54
+ def _read_bucket_json(key: str):
55
+ """Download JSON from bucket. Returns parsed object or None on miss."""
56
+ try:
57
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".json") as tf:
58
+ tmp_path = tf.name
59
+ download_bucket_files(BUCKET_ID, files=[(key, tmp_path)])
60
+ with open(tmp_path, "r") as f:
61
+ data = json.load(f)
62
+ os.unlink(tmp_path)
63
+ return data
64
+ except Exception as e:
65
+ logger.debug(f"_read_bucket_json({key}): {e}")
66
+ return None
67
+
68
+
69
+ def _write_bucket_json(key: str, data):
70
+ """Serialize data to JSON and upload to bucket at key."""
71
+ raw_bytes = json.dumps(data, indent=2, default=str).encode("utf-8")
72
+ batch_bucket_files(BUCKET_ID, add=[(raw_bytes, key)])
73
+
74
+
75
+ def _key_exists(key: str) -> bool:
76
+ """Return True if key exists in the bucket."""
77
+ try:
78
+ info = list(get_bucket_paths_info(BUCKET_ID, [key]))
79
+ return bool(info)
80
+ except Exception:
81
+ return False
82
+
83
+
84
+ def _list_prefix(prefix: str) -> list:
85
+ """Return all file items under prefix (recursive)."""
86
+ try:
87
+ return [
88
+ item
89
+ for item in list_bucket_tree(BUCKET_ID, prefix=prefix, recursive=True)
90
+ if item.type == "file"
91
+ ]
92
+ except Exception:
93
+ return []
94
+
95
+
96
+ # ================================================================
97
+ # ORIGINAL HELPERS (names unchanged, now return bucket keys)
98
+ # ================================================================
99
+
100
+ def get_user_folder(user_id: str) -> str:
101
+ """Return the bucket prefix for user's folder (no creation needed)."""
102
+ return f"{BASE_DIR}/{user_id}"
103
+
104
+
105
+ def get_user_file(user_id: str) -> str:
106
+ """Return the bucket key for user's cameras.json."""
107
+ return f"{get_user_folder(user_id)}/cameras.json"
108
+
109
+
110
+ # ================================================================
111
+ # VALIDATION
112
+ # ================================================================
113
+
114
  def validate_form(user_id, camera_name, images):
115
  if not user_id or not user_id.strip():
116
  raise HTTPException(400, "user_id is required")
 
132
  return images
133
 
134
 
135
+ # ================================================================
136
+ # EXIF / METADATA
137
+ # ================================================================
138
 
139
  def make_json_safe(value):
140
  """Convert EXIF values to JSON-serializable types"""
 
173
  return metadata
174
 
175
 
176
+ # ================================================================
177
+ # IMAGE PROCESSING
178
+ # ================================================================
179
+
180
  def process_image(image):
181
  """Run 3-stage detection and classification with dynamic confidence"""
182
  detections = []
183
+ results = DETECT_MODEL(image, conf=0.8, iou=0.4, agnostic_nms=True) # Stage 1: Deer detection
184
  for r in results:
185
  for box in r.boxes:
186
  x1, y1, x2, y2 = map(int, box.xyxy[0])
 
188
  if crop.size == 0:
189
  continue
190
  # ---------------- Stage 2: Buck/Doe ----------------
191
+ buck_res = BUCK_DOE_MODEL(crop)
192
  buck_probs = buck_res[0].probs
193
  top1_idx = buck_probs.top1
194
  buck_name = buck_res[0].names[top1_idx]
 
214
  return detections
215
 
216
 
217
+ # ================================================================
218
+ # CAMERA VALIDATION
219
+ # ================================================================
220
 
 
 
221
  def validate_user_and_camera(user_id: str, camera_name: str):
222
  if not user_exists(user_id):
223
  raise HTTPException(404, "User not found")
 
226
  raise HTTPException(404, "Camera not registered")
227
 
228
 
229
+ # ================================================================
230
+ # IMAGE SAVE
231
+ # ================================================================
232
+
233
  def save_image(user_id, camera_name, filename, data):
234
+ key = _bucket_key(user_id, camera_name, "raw", filename)
235
+ batch_bucket_files(BUCKET_ID, add=[(data, key)])
236
+ return f"https://huggingface.co/buckets/{BUCKET_ID}/resolve/{key}"
237
+
238
+
239
+
240
+ # ================================================================
241
+ # JSON
242
+ # ================================================================
243
+
 
 
 
244
  def load_json(path):
245
+ """Load JSON from bucket key. Returns [] on miss (same behaviour as before)."""
246
+ result = _read_bucket_json(path)
247
+ return result if result is not None else []
 
248
 
 
 
 
249
 
250
+ def save_json(path, data):
251
+ """Save data as JSON to bucket key."""
252
+ _write_bucket_json(path, data)
253
 
 
 
 
254
 
255
+ # ================================================================
256
+ # USER FOLDERS / CAMERAS
257
+ # ================================================================
258
 
259
  def user_exists(user_id: str) -> bool:
260
+ return _key_exists(get_user_file(user_id))
261
+
262
 
263
  def load_cameras(user_id: str) -> list:
264
  path = get_user_file(user_id)
 
 
265
  try:
266
+ data = _read_bucket_json(path)
267
+ return data if isinstance(data, list) else []
268
+ except Exception:
269
  return []
270
 
271
+
272
  def save_cameras(user_id: str, cameras: list):
273
+ # Bucket keys don't need folder creation β€” just write the file
274
+ _write_bucket_json(get_user_file(user_id), cameras)
275
+
 
 
276
 
277
+ # ================================================================
278
+ # DASHBOARD
279
+ # ================================================================
280
 
 
281
  def get_user_dashboard(user_id: str, camera_name: str = None) -> dict:
282
  """Return analytics for a user or a specific camera"""
283
+ cameras_file = get_user_file(user_id)
284
+ if not _key_exists(cameras_file):
 
285
  raise HTTPException(404, f"User {user_id} not found")
286
+
287
  try:
288
+ cameras = _read_bucket_json(cameras_file) or []
289
+ except Exception:
 
290
  cameras = []
291
+
292
  total_cameras = len(cameras)
293
  total_images = 0
294
  total_detections = 0
295
  buck_type_distribution = {}
296
  buck_doe_distribution = {"Buck": 0, "Doe": 0}
297
 
298
+ heatmap = defaultdict(lambda: [0] * 24) # day -> 24 hours
 
 
 
 
299
  deer_per_day = Counter()
300
  bucks_per_day = Counter()
301
  does_per_day = Counter()
302
+ hour_activity = [0] * 24 # 0-23 hours
303
 
304
  for cam in cameras:
305
  cam_name = cam["camera_name"]
306
  if camera_name and cam_name != camera_name:
307
  continue
308
+
309
+ # Count images (replaces raw_folder.glob("*.*"))
310
+ raw_folder = _bucket_key(user_id, cam_name, "raw")
311
+ raw_files = _list_prefix(raw_folder)
312
+ total_images += len(raw_files)
313
+
314
+ # Count detections and distributions (replaces open(detections_file))
315
+ detections_file = _bucket_key(user_id, cam_name, f"{cam_name}_detections.json")
316
+ if _key_exists(detections_file):
 
317
  try:
318
+ dets = _read_bucket_json(detections_file) or []
319
  for rec in dets:
320
  # --- Existing Buck/Doe counts ---
321
  for d in rec.get("detections", []):
 
328
  buck_type_distribution[parts[2]] = buck_type_distribution.get(parts[2], 0) + 1
329
  else: # Doe
330
  buck_doe_distribution["Doe"] += 1
331
+
332
  # --- New analytics using datetime_original ---
333
  dt_str = rec.get("metadata", {}).get("exif", {}).get("datetime_original")
334
  if dt_str:
335
  dt = datetime.strptime(dt_str, "%Y:%m:%d %H:%M:%S")
336
  day = dt.date()
337
  hour = dt.hour
338
+
339
  # Heatmap count
340
  heatmap[day][hour] += len(rec.get("detections", []))
341
+
342
  # Count deer, bucks, does per day
343
  for d in rec.get("detections", []):
344
  label = d.get("label", "")
 
348
  bucks_per_day[day] += 1
349
  if "Doe" in label:
350
  does_per_day[day] += 1
351
+
352
  # Hourly aggregated activity
353
  hour_activity[hour] += len(rec.get("detections", []))
354
+ except Exception:
355
  continue
356
 
357
  # Average activity by hour (morning/night)
358
  morning_hours = range(6, 18)
359
+ night_hours = list(range(0, 6)) + list(range(18, 24))
360
  morning_activity = sum(hour_activity[h] for h in morning_hours) / len(morning_hours)
361
  night_activity = sum(hour_activity[h] for h in night_hours) / len(night_hours)
362
 
 
374
  "bucks_per_day": dict(bucks_per_day),
375
  "does_per_day": dict(does_per_day),
376
  "average_activity": {
377
+ "morning": round(morning_activity, 2),
378
+ "night": round(night_activity, 2)
379
  }
380
+ }
 
api/view_image.py CHANGED
@@ -1,14 +1,21 @@
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
  ):
@@ -17,44 +24,44 @@ def view_images(
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
@@ -62,7 +69,7 @@ def view_images(
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:
@@ -77,4 +84,4 @@ def view_images(
77
  "camera_name": camera_name,
78
  "filter_label": filter_label,
79
  "images": images
80
- }
 
1
  from fastapi import APIRouter, HTTPException, Query, Request
 
2
  import json
3
+ from .utils import (
4
+ _bucket_key,
5
+ _key_exists,
6
+ _list_prefix,
7
+ _read_bucket_json,
8
+ user_exists,
9
+ BUCKET_ID,
10
+ )
11
 
12
  router = APIRouter()
13
 
14
+
15
  @router.get("/view_images")
16
  def view_images(
17
+ request: Request,
18
+ user_id: str = Query(...),
19
  camera_name: str = Query(...),
20
  filter_label: str = Query(None, description="Optional filter: Buck, Doe, Mule, Whitetail")
21
  ):
 
24
  Returns clickable URLs for each image.
25
  Optionally filter images based on labels (Buck, Doe, Mule, Whitetail).
26
  """
27
+ # ── existence checks against the bucket ──────────────────────
28
+ if not user_exists(user_id):
29
  raise HTTPException(status_code=404, detail="User not found")
30
 
31
+ raw_prefix = _bucket_key(user_id, camera_name, "raw")
32
+ detection_key = _bucket_key(user_id, camera_name, f"{camera_name}_detections.json")
 
33
 
34
+ raw_files = _list_prefix(raw_prefix)
35
+ if not raw_files:
36
  raise HTTPException(status_code=404, detail="Camera raw folder not found")
37
+
38
+ if not _key_exists(detection_key):
39
  raise HTTPException(status_code=404, detail="Detection JSON not found")
40
 
41
+ # ── load detections from bucket ───────────────────────────────
42
+ detections = _read_bucket_json(detection_key)
43
+ if detections is None:
44
+ raise HTTPException(status_code=500, detail="Failed to read detection file")
 
 
45
 
46
+ # ── build a set of filenames that exist in the bucket ─────────
47
+ existing_filenames = {item.path.split("/")[-1] for item in raw_files}
 
 
 
 
48
 
49
+ # ── validate filter label ─────────────────────────────────────
50
  valid_filters = {"buck", "doe", "mule", "whitetail"}
51
  filter_lower = filter_label.lower() if filter_label else None
52
  if filter_lower and filter_lower not in valid_filters:
53
+ raise HTTPException(
54
+ status_code=400,
55
+ detail=f"Invalid filter_label. Must be one of {valid_filters}"
56
+ )
57
 
58
+ # ── build response ────────────────────────────────────────────
59
  images = []
60
  for item in detections:
61
+ filename = item["filename"]
62
+
63
+ if filename in existing_filenames:
64
+ item["image_url"] = f"https://huggingface.co/buckets/{BUCKET_ID}/resolve/{raw_prefix}/{filename}"
65
  else:
66
  item["missing"] = True
67
  item["image_url"] = None
 
69
  # Apply label filter if provided
70
  if filter_lower:
71
  filtered_detections = [
72
+ det for det in item.get("detections", [])
73
  if any(lbl.lower().find(filter_lower) != -1 for lbl in det["label"].split("|"))
74
  ]
75
  if filtered_detections:
 
84
  "camera_name": camera_name,
85
  "filter_label": filter_label,
86
  "images": images
87
+ }
requirements.txt CHANGED
@@ -4,7 +4,6 @@ numpy
4
  pillow
5
  pyngrok
6
  python-dotenv
7
- google-cloud-storage
8
  gunicorn
9
  waitress
10
  fastapi
 
4
  pillow
5
  pyngrok
6
  python-dotenv
 
7
  gunicorn
8
  waitress
9
  fastapi