codewithRiz commited on
Commit
2dc1b1e
·
1 Parent(s): 57ce3d4

bbox threshodl increase

Browse files
Files changed (4) hide show
  1. api/detection.py +4 -2
  2. api/utils.py +63 -37
  3. app.py +27 -0
  4. requirements.txt +7 -1
api/detection.py CHANGED
@@ -10,7 +10,7 @@ from .utils import (
10
  save_image,
11
  load_json,
12
  save_json,
13
- validate_user_and_camera
14
  )
15
 
16
  router = APIRouter()
@@ -29,6 +29,7 @@ async def predict(
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:
@@ -38,7 +39,8 @@ async def predict(
38
  record = {
39
  "filename": file.filename,
40
  "image_url": url,
41
- "detections": detections
 
42
  }
43
  data.append(record)
44
  new_results.append(record)
 
10
  save_image,
11
  load_json,
12
  save_json,
13
+ validate_user_and_camera,extract_metadata
14
  )
15
 
16
  router = APIRouter()
 
29
  new_results = []
30
  for file in images:
31
  raw = await file.read()
32
+ metadata = extract_metadata(raw)
33
  nparr = np.frombuffer(raw, np.uint8)
34
  img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
35
  if img is None:
 
39
  record = {
40
  "filename": file.filename,
41
  "image_url": url,
42
+ "detections": detections,
43
+ "metadata": metadata
44
  }
45
  data.append(record)
46
  new_results.append(record)
api/utils.py CHANGED
@@ -4,7 +4,9 @@ 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,
@@ -24,70 +26,107 @@ from .config import (
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
 
@@ -96,16 +135,13 @@ def validate_user_and_camera(user_id: str, camera_name: str):
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
 
@@ -137,10 +173,8 @@ def user_exists(user_id: str) -> bool:
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)
@@ -151,7 +185,6 @@ 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
 
@@ -162,32 +195,25 @@ 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("*.*")))
 
4
  from fastapi import HTTPException
5
  import cv2
6
  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,
 
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")
 
29
  if not camera_name or not camera_name.strip():
30
  raise HTTPException(400, "camera_name is required")
 
31
  if not images or len(images) == 0:
32
  raise HTTPException(400, "At least one image is required")
 
33
  images = [f for f in images if f.filename and f.filename.strip()]
 
34
  if len(images) < MIN_IMAGES:
35
  raise HTTPException(400, f"At least {MIN_IMAGES} image(s) required")
 
36
  if len(images) > MAX_IMAGES:
37
  raise HTTPException(400, f"Maximum {MAX_IMAGES} images allowed")
 
38
  for f in images:
39
  if "." not in f.filename:
40
  raise HTTPException(400, f"Invalid file: {f.filename}")
 
41
  ext = f.filename.rsplit(".", 1)[1].lower()
42
  if ext not in ALLOWED_EXTENSIONS:
43
  raise HTTPException(400, f"Invalid file type: {f.filename}")
 
44
  return images
45
 
46
+
47
+
48
+ def make_json_safe(value):
49
+ """Convert EXIF values to JSON-serializable types"""
50
+ if hasattr(value, "name"):
51
+ return value.name
52
+ if isinstance(value, (bytes, bytearray)):
53
+ return value.decode(errors="ignore")
54
+ if isinstance(value, (tuple, list)):
55
+ return [make_json_safe(v) for v in value]
56
+ if not isinstance(value, (str, int, float, bool, type(None))):
57
+ return str(value)
58
+ return value
59
+
60
+
61
+ def extract_metadata(image_bytes):
62
+ metadata = {
63
+ "upload_datetime": datetime.utcnow().isoformat() + "Z"
64
+ }
65
+ try:
66
+ exif_img = ExifImage(BytesIO(image_bytes))
67
+ if not exif_img.has_exif:
68
+ return metadata
69
+ exif_dict = {}
70
+ for tag in exif_img.list_all():
71
+ try:
72
+ value = getattr(exif_img, tag)
73
+ value = make_json_safe(value)
74
+ if value not in ("", None, [], {}):
75
+ exif_dict[tag] = value
76
+ except Exception:
77
+ continue
78
+ if exif_dict:
79
+ metadata["exif"] = exif_dict
80
+ except Exception:
81
+ pass
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.7,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])
93
  crop = image[y1:y2, x1:x2]
94
  if crop.size == 0:
95
  continue
96
+ # ---------------- Stage 2: Buck/Doe ----------------
97
+ buck_res = BUCK_DOE_MODEL(crop,conf=0.85, iou=0.40,agnostic_nms=True)
98
+ buck_probs = buck_res[0].probs
99
+ top1_idx = buck_probs.top1
100
+ buck_name = buck_res[0].names[top1_idx]
101
+ buck_conf = float(buck_probs.top1conf)
102
  if buck_name.lower() == "buck":
103
+ # ---------------- Stage 3: Buck Type ----------------
104
  type_res = BUCK_TYPE_MODEL(crop)
105
+ type_probs = type_res[0].probs
106
+ top1_type_idx = type_probs.top1
107
+ type_name = type_res[0].names[top1_type_idx]
108
+ type_conf = float(type_probs.top1conf)
109
+ label = f"Deer | Buck | {type_name} | {type_conf:.2f}"
110
+ final_conf = type_conf
111
  else:
112
+ # Doe: use stage 2 confidence
113
+ label = f"Deer | Doe | {buck_conf:.2f}"
114
+ final_conf = buck_conf
115
  detections.append({
116
  "label": label,
117
+ "bbox": [x1, y1, x2, y2],
118
+ "confidence": final_conf
119
  })
 
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")
 
129
  cameras = load_cameras(user_id)
 
130
  if not any(c["camera_name"] == camera_name for c in cameras):
131
  raise HTTPException(404, "Camera not registered")
132
 
 
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
 
 
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)
 
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
 
 
195
  """Return analytics for a user or a specific camera"""
196
  user_folder = Path(UPLOAD_DIR) / user_id
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("*.*")))
app.py CHANGED
@@ -1,2 +1,29 @@
1
  # app.py
2
  from api.main import app
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  # app.py
2
  from api.main import app
3
+
4
+ import warnings
5
+ warnings.filterwarnings("ignore", message="Corrupt JPEG data")
6
+
7
+ import os
8
+ from dotenv import load_dotenv
9
+ from pyngrok import ngrok
10
+ import uvicorn
11
+
12
+ load_dotenv()
13
+ NGROK_AUTH_TOKEN = os.getenv("NGROK_AUTH_TOKEN")
14
+
15
+ if NGROK_AUTH_TOKEN:
16
+ ngrok.set_auth_token(NGROK_AUTH_TOKEN)
17
+
18
+
19
+
20
+ if __name__ == "__main__":
21
+
22
+ # # Run FastAPI
23
+ uvicorn.run(
24
+ "api.main:app",
25
+ host="0.0.0.0",
26
+ port=8080,
27
+ reload=True,
28
+ log_level="info"
29
+ )
requirements.txt CHANGED
@@ -9,4 +9,10 @@ gunicorn
9
  waitress
10
  fastapi
11
  uvicorn
12
- python-multipart
 
 
 
 
 
 
 
9
  waitress
10
  fastapi
11
  uvicorn
12
+ python-multipart
13
+ huggingface_hub
14
+ gradio_client
15
+ pyarrow
16
+ gradio
17
+ PyExifTool
18
+ exif