mustafa2ak commited on
Commit
11e442b
·
verified ·
1 Parent(s): 4bd08a5

Update reid.py

Browse files
Files changed (1) hide show
  1. reid.py +376 -360
reid.py CHANGED
@@ -1,5 +1,6 @@
1
  """
2
- Enhanced ReID with Multiple Embeddings, Sleeping Tracks, and Two-Stage Matching
 
3
  """
4
  import numpy as np
5
  import cv2
@@ -8,81 +9,77 @@ import timm
8
  from sklearn.metrics.pairwise import cosine_similarity
9
  from typing import Dict, List, Optional, Tuple
10
  from dataclasses import dataclass, field
11
- import json
12
- import pickle
13
- from pathlib import Path
14
  from datetime import datetime, timedelta
 
15
  import warnings
16
  warnings.filterwarnings('ignore')
17
 
 
 
18
  @dataclass
19
  class DogFeatures:
20
- """Container for dog features with multiple embeddings"""
21
  features: np.ndarray
22
  bbox: List[float] = field(default_factory=list)
23
  confidence: float = 0.5
24
  frame_num: int = 0
25
- image: Optional[np.ndarray] = None
26
  timestamp: datetime = field(default_factory=datetime.now)
 
 
 
27
 
28
  @dataclass
29
  class SleepingTrack:
30
- """Track that recently disappeared but kept alive for re-entry"""
31
- temp_id: int
32
- features_list: List[DogFeatures]
33
- last_seen: datetime
34
  last_position: Tuple[float, float]
35
- permanent_name: Optional[str] = None
 
 
36
 
37
- class EnhancedMegaDescriptorReID:
38
- """Enhanced ReID with sleeping tracks and multi-embedding storage"""
39
-
40
- TURKISH_DOG_NAMES = [
41
- "Karabaş", "Pamuk", "Boncuk", "Fındık", "Paşa", "Aslan", "Duman", "Tarçın",
42
- "Kömür", "Bal", "Zeytin", "Kurabiye", "Lokum", "Şeker", "Beyaz", "Kara",
43
- "Sarı", "Benekli", "Cesur", "Yıldız", "Ay", "Güneş", "Bulut", "Fırtına",
44
- "Şimşek", "Yağmur", "Kar", "Buz", "Ateş", "Alev", "Kıvılcım", "Köpük",
45
- "Dalga", "Deniz", "Nehir", "Çakıl", "Toprak", "Orman", "Çiçek", "Gül"
46
- ]
47
 
48
- def __init__(self, device: str = 'cuda', db_path: str = 'dog_database'):
49
  self.device = device if torch.cuda.is_available() else 'cpu'
50
 
51
- # Two different thresholds for two-stage matching
52
- self.session_threshold = 0.35 # For current session dogs
53
- self.database_threshold = 0.28 # Lower threshold for database (more lenient)
54
- self.sleeping_threshold = 0.30 # For recently lost tracks
55
 
56
- self.db_path = Path(db_path)
57
- self.db_path.mkdir(exist_ok=True)
58
- (self.db_path / 'images').mkdir(exist_ok=True)
 
59
 
60
- # Load permanent database with multiple embeddings
61
- self.permanent_dogs = self.load_permanent_database()
62
- self.used_names = {dog['name'] for dog in self.permanent_dogs.values()}
 
63
 
64
- # Temporary session data with multiple embeddings per dog
65
- self.session_dogs = {} # temp_id -> list of DogFeatures
66
- self.session_best_images = {}
67
- self.temp_to_permanent = {}
68
  self.next_temp_id = 1
69
  self.current_frame = 0
 
70
 
71
- # ENHANCEMENT 1: Sleeping tracks for re-identification
72
- self.sleeping_tracks: List[SleepingTrack] = []
73
- self.sleeping_track_ttl = 60 # Keep sleeping tracks for 60 seconds
74
-
75
- # Statistics
76
- self.known_dog_detections = []
77
- self.reentry_matches = 0 # Track successful re-entries
78
 
79
  # Initialize model
80
  self._initialize_megadescriptor()
81
 
82
- print(f" Enhanced ReID initialized")
83
- print(f"📁 Database: {self.db_path}")
84
- print(f"🐕 Known dogs: {len(self.permanent_dogs)}")
85
- print(f"🔄 Sleeping track buffer: {self.sleeping_track_ttl}s")
86
 
87
  def _initialize_megadescriptor(self):
88
  """Initialize MegaDescriptor-L-384"""
@@ -98,184 +95,198 @@ class EnhancedMegaDescriptorReID:
98
  mean=[0.5, 0.5, 0.5],
99
  std=[0.5, 0.5, 0.5]
100
  )
101
- print("MegaDescriptor-L-384 loaded")
102
  except Exception as e:
103
- print(f" Model initialization error: {e}")
104
  self.model = None
105
 
106
- def load_permanent_database(self) -> Dict:
107
- """Load database with multiple embeddings per dog"""
108
- db_file = self.db_path / 'dogs_database.json'
109
- embeddings_file = self.db_path / 'embeddings.pkl'
110
 
111
- if not db_file.exists():
112
- return {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
 
114
  try:
115
- with open(db_file, 'r', encoding='utf-8') as f:
116
- dogs_data = json.load(f)
 
 
117
 
118
- if embeddings_file.exists():
119
- with open(embeddings_file, 'rb') as f:
120
- embeddings = pickle.load(f)
121
-
122
- # Support both single and multiple embeddings
123
- for dog_name, embedding_data in embeddings.items():
124
- if dog_name in dogs_data:
125
- if isinstance(embedding_data, list):
126
- # Multiple embeddings already stored
127
- dogs_data[dog_name]['embeddings'] = embedding_data
128
- else:
129
- # Legacy single embedding - convert to list
130
- dogs_data[dog_name]['embeddings'] = [embedding_data]
131
 
132
- print(f"📚 Loaded {len(dogs_data)} dogs from database")
133
- return dogs_data
 
 
 
 
 
 
 
134
  except Exception as e:
135
- print(f"Error loading database: {e}")
136
- return {}
137
 
138
- def save_permanent_database(self):
139
- """Save database with multiple embeddings"""
140
- db_file = self.db_path / 'dogs_database.json'
141
- embeddings_file = self.db_path / 'embeddings.pkl'
142
-
143
- metadata = {}
144
- embeddings = {}
145
-
146
- for dog_name, dog_data in self.permanent_dogs.items():
147
- metadata[dog_name] = {
148
- 'name': dog_data['name'],
149
- 'first_seen': dog_data['first_seen'],
150
- 'last_seen': dog_data['last_seen'],
151
- 'total_sightings': dog_data['total_sightings'],
152
- 'image_path': dog_data.get('image_path', ''),
153
- 'num_embeddings': len(dog_data.get('embeddings', []))
154
- }
155
-
156
- if 'embeddings' in dog_data:
157
- embeddings[dog_name] = dog_data['embeddings']
158
 
159
- with open(db_file, 'w', encoding='utf-8') as f:
160
- json.dump(metadata, f, indent=2, ensure_ascii=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
 
162
- with open(embeddings_file, 'wb') as f:
163
- pickle.dump(embeddings, f)
 
 
164
 
165
- print(f"💾 Database saved: {len(metadata)} dogs")
166
 
167
- def check_sleeping_tracks(self, features: np.ndarray, position: Tuple[float, float]) -> Optional[Tuple[int, str]]:
168
- """ENHANCEMENT 1: Check recently lost tracks first"""
169
- current_time = datetime.now()
170
- active_sleeping = []
171
 
172
- for track in self.sleeping_tracks:
173
- # Remove expired sleeping tracks
174
- time_diff = (current_time - track.last_seen).total_seconds()
175
- if time_diff > self.sleeping_track_ttl:
176
- continue
177
-
178
- active_sleeping.append(track)
179
 
180
- # Calculate similarities with all stored embeddings
181
  similarities = []
182
- for stored_feat in track.features_list[-10:]: # Last 10 embeddings
183
  sim = cosine_similarity(
184
  features.reshape(1, -1),
185
- stored_feat.features.reshape(1, -1)
186
  )[0, 0]
187
  similarities.append(sim)
188
 
189
- if similarities:
190
- max_sim = np.max(similarities)
191
- avg_sim = np.mean(similarities)
192
- final_score = 0.7 * max_sim + 0.3 * avg_sim
193
-
194
- # Temporal consistency: boost score if position is nearby
195
- pos_distance = np.sqrt((position[0] - track.last_position[0])**2 +
196
- (position[1] - track.last_position[1])**2)
197
- if pos_distance < 200: # Within reasonable distance
198
- final_score *= 1.1 # 10% boost
199
-
200
- if final_score >= self.sleeping_threshold:
201
- print(f" 🔄 RE-ENTRY: Temp ID {track.temp_id} (score: {final_score:.3f}, time: {time_diff:.1f}s)")
202
- self.reentry_matches += 1
203
- self.sleeping_tracks = active_sleeping
204
- return (track.temp_id, track.permanent_name)
205
 
206
- self.sleeping_tracks = active_sleeping
207
  return None
208
 
209
- def check_permanent_database_enhanced(self, features: np.ndarray) -> Optional[str]:
210
- """ENHANCEMENT 2: Check permanent database with multiple embeddings"""
211
- if not self.permanent_dogs:
212
- return None
213
-
214
- best_match = None
215
- best_score = 0
216
 
217
- for dog_name, dog_data in self.permanent_dogs.items():
218
- if 'embeddings' not in dog_data:
219
- continue
220
-
221
- # Compare against all stored embeddings
222
  similarities = []
223
- for embedding in dog_data['embeddings']:
224
  sim = cosine_similarity(
225
  features.reshape(1, -1),
226
- embedding.reshape(1, -1)
227
  )[0, 0]
228
  similarities.append(sim)
229
 
230
  if similarities:
231
- # Use best match from multiple embeddings
232
  max_sim = np.max(similarities)
233
- avg_sim = np.mean(similarities)
234
- final_score = 0.6 * max_sim + 0.4 * avg_sim
235
 
236
  if final_score > best_score:
237
  best_score = final_score
238
- best_match = dog_name
239
 
240
- # Lower threshold for database matching
241
- if best_score >= self.database_threshold:
242
- print(f" ✅ DATABASE MATCH: {best_match} (score: {best_score:.3f})")
243
- return best_match
244
 
245
  return None
246
 
247
- def extract_features(self, image: np.ndarray, bbox: List[float] = None) -> Optional[DogFeatures]:
248
- """Extract features using MegaDescriptor-L"""
249
- if image is None or image.size == 0 or self.model is None:
250
- return None
251
-
252
- try:
253
- img_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
254
- from PIL import Image
255
- pil_img = Image.fromarray(img_rgb)
256
- img_tensor = self.transform(pil_img).unsqueeze(0).to(self.device)
257
-
258
- with torch.no_grad():
259
- features = self.model(img_tensor)
260
-
261
- features = features.squeeze().cpu().numpy()
262
- features = features / (np.linalg.norm(features) + 1e-7)
263
-
264
- return DogFeatures(
265
- features=features,
266
- bbox=bbox if bbox else [0, 0, 100, 100],
267
- frame_num=self.current_frame,
268
- image=image.copy(),
269
- timestamp=datetime.now()
270
- )
271
- except Exception as e:
272
- print(f"Feature extraction error: {e}")
273
- return None
274
-
275
- def match_or_register(self, track, image_crop=None) -> Tuple[int, float, Optional[str]]:
276
- """Enhanced matching with two-stage approach and sleeping tracks"""
277
  self.current_frame += 1
278
 
 
279
  detection = None
280
  for det in reversed(track.detections[-3:]):
281
  if det.image_crop is not None:
@@ -284,245 +295,250 @@ class EnhancedMegaDescriptorReID:
284
  break
285
 
286
  if detection is None or image_crop is None:
287
- return 0, 0.0, None
288
 
289
- features = self.extract_features(
 
290
  image_crop,
291
  detection.bbox if hasattr(detection, 'bbox') else None
292
  )
293
 
294
- if features is None:
295
- return 0, 0.0, None
296
 
297
- features.confidence = detection.confidence if hasattr(detection, 'confidence') else 0.5
 
298
 
299
- # Get position for temporal consistency
300
- bbox = features.bbox
301
- position = ((bbox[0] + bbox[2])/2, (bbox[1] + bbox[3])/2)
302
 
303
- # STAGE 1: Check sleeping tracks (recently lost)
304
- sleeping_result = self.check_sleeping_tracks(features.features, position)
305
- if sleeping_result:
306
- temp_id, permanent_name = sleeping_result
307
- # Reactivate the track
308
- if temp_id in self.session_dogs:
309
- self.session_dogs[temp_id].append(features)
310
- self.update_best_image(temp_id, features)
311
- else:
312
- # Restore from sleeping
313
- self.session_dogs[temp_id] = [features]
314
- self.session_best_images[temp_id] = features
315
-
316
- if permanent_name:
317
- self.temp_to_permanent[temp_id] = permanent_name
318
 
319
- # Remove from sleeping tracks
320
- self.sleeping_tracks = [t for t in self.sleeping_tracks if t.temp_id != temp_id]
321
 
322
- return temp_id, 0.95, permanent_name
323
 
324
- # STAGE 2: Check permanent database
325
- permanent_name = self.check_permanent_database_enhanced(features.features)
326
 
327
- # STAGE 3: Check current session dogs
328
- best_temp_id = None
329
- best_score = -1.0
330
 
331
- for temp_id, dog_features_list in self.session_dogs.items():
332
- similarities = []
333
- for stored_feat in dog_features_list[-20:]:
334
- sim = cosine_similarity(
335
- features.features.reshape(1, -1),
336
- stored_feat.features.reshape(1, -1)
337
- )[0, 0]
338
- similarities.append(sim)
339
 
340
- if similarities:
341
- max_sim = np.max(similarities)
342
- avg_sim = np.mean(similarities)
343
- final_score = 0.6 * max_sim + 0.4 * avg_sim
344
-
345
- if final_score > best_score:
346
- best_score = final_score
347
- best_temp_id = temp_id
348
-
349
- if best_temp_id is not None and best_score >= self.session_threshold:
350
- # Match found in session
351
- self.session_dogs[best_temp_id].append(features)
352
- if len(self.session_dogs[best_temp_id]) > 30:
353
- self.session_dogs[best_temp_id] = self.session_dogs[best_temp_id][-30:]
354
-
355
- self.update_best_image(best_temp_id, features)
356
 
357
- if permanent_name:
358
- self.temp_to_permanent[best_temp_id] = permanent_name
359
- self.known_dog_detections.append(permanent_name)
360
 
361
- return best_temp_id, best_score, permanent_name
 
362
  else:
363
  # New dog in session
364
  new_temp_id = self.next_temp_id
365
  self.next_temp_id += 1
366
- self.session_dogs[new_temp_id] = [features]
367
- self.session_best_images[new_temp_id] = features
368
 
369
- if permanent_name:
370
- self.temp_to_permanent[new_temp_id] = permanent_name
371
- self.known_dog_detections.append(permanent_name)
372
- print(f" ℹ️ Known dog {permanent_name} got temp ID {new_temp_id}")
 
 
373
  else:
374
- print(f" 🆕 New dog: Temp ID {new_temp_id}")
375
-
376
- return new_temp_id, 1.0, permanent_name
377
 
378
- def move_to_sleeping(self, temp_id: int):
379
- """Move a lost track to sleeping tracks"""
380
- if temp_id in self.session_dogs:
381
- features_list = self.session_dogs[temp_id]
382
- if features_list:
383
- last_features = features_list[-1]
384
- bbox = last_features.bbox
385
- position = ((bbox[0] + bbox[2])/2, (bbox[1] + bbox[3])/2)
386
-
387
- sleeping_track = SleepingTrack(
388
- temp_id=temp_id,
389
- features_list=features_list.copy(),
390
- last_seen=datetime.now(),
391
- last_position=position,
392
- permanent_name=self.temp_to_permanent.get(temp_id)
393
- )
394
-
395
- self.sleeping_tracks.append(sleeping_track)
396
- print(f" 💤 Moved dog {temp_id} to sleeping tracks")
397
 
398
- def update_best_image(self, temp_id: int, features: DogFeatures):
399
- """Update best quality image"""
400
- current_best = self.session_best_images.get(temp_id)
401
-
402
- if features.image is not None:
403
- quality = features.confidence * np.prod(features.image.shape[:2])
404
 
405
- if current_best is None or current_best.image is None:
406
- self.session_best_images[temp_id] = features
407
- else:
408
- current_quality = current_best.confidence * np.prod(current_best.image.shape[:2])
409
- if quality > current_quality:
410
- self.session_best_images[temp_id] = features
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
411
 
412
  def save_session_to_permanent(self) -> Dict:
413
- """Save session dogs with multiple embeddings"""
414
  saved_dogs = {}
415
 
416
- for temp_id, features_list in self.session_dogs.items():
417
- if temp_id in self.temp_to_permanent:
418
- # Update existing dog with new embeddings
419
- dog_name = self.temp_to_permanent[temp_id]
420
- if dog_name in self.permanent_dogs:
421
- # Add new diverse embeddings
422
- existing_embeddings = self.permanent_dogs[dog_name].get('embeddings', [])
423
- new_embeddings = [f.features for f in features_list[-5:]] # Last 5
424
-
425
- # Combine and limit to 10 embeddings per dog
426
- all_embeddings = existing_embeddings + new_embeddings
427
- self.permanent_dogs[dog_name]['embeddings'] = all_embeddings[-10:]
428
- self.permanent_dogs[dog_name]['last_seen'] = datetime.now().isoformat()
429
- self.permanent_dogs[dog_name]['total_sightings'] += 1
430
- print(f" 🔄 Updated {dog_name} (now {len(self.permanent_dogs[dog_name]['embeddings'])} embeddings)")
431
  continue
432
 
433
- # New dog - save with multiple embeddings
434
- dog_name = self.get_next_turkish_name()
435
-
436
- # Store multiple diverse embeddings (up to 5)
437
- embeddings = [f.features for f in features_list[-5:]]
438
 
439
- # Save best image
440
- best_features = self.session_best_images.get(temp_id)
441
- image_path = None
442
- if best_features and best_features.image is not None:
443
- image_filename = f"{dog_name.lower()}.jpg"
444
- image_path = self.db_path / 'images' / image_filename
445
- cv2.imwrite(str(image_path), best_features.image)
446
- image_path = str(image_path.relative_to(self.db_path))
447
-
448
- self.permanent_dogs[dog_name] = {
449
- 'name': dog_name,
450
- 'embeddings': embeddings, # Multiple embeddings
451
- 'first_seen': datetime.now().isoformat(),
452
- 'last_seen': datetime.now().isoformat(),
453
- 'total_sightings': 1,
454
- 'image_path': image_path
455
- }
456
 
457
- saved_dogs[temp_id] = dog_name
458
- self.used_names.add(dog_name)
459
- print(f" ✅ Saved new dog: {dog_name} ({len(embeddings)} embeddings)")
460
 
461
- if saved_dogs or self.temp_to_permanent:
462
- self.save_permanent_database()
 
463
 
464
  return saved_dogs
465
 
466
- def get_next_turkish_name(self) -> str:
467
- """Get next available Turkish dog name"""
468
- for name in self.TURKISH_DOG_NAMES:
469
- if name not in self.used_names:
470
- return name
471
-
472
- counter = 2
473
- while True:
474
- for name in self.TURKISH_DOG_NAMES:
475
- numbered_name = f"{name}_{counter}"
476
- if numbered_name not in self.used_names:
477
- return numbered_name
478
- counter += 1
479
-
480
  def match_or_register_all(self, track) -> Dict:
481
  """Compatible interface"""
482
- temp_id, confidence, permanent_name = self.match_or_register(track)
483
  return {
484
  'MegaDescriptor': {
485
  'dog_id': temp_id,
 
486
  'confidence': confidence,
487
- 'permanent_name': permanent_name
 
488
  }
489
  }
490
 
491
  def set_all_thresholds(self, threshold: float):
492
  """Update thresholds"""
493
  self.session_threshold = max(0.15, min(0.95, threshold))
494
- self.database_threshold = self.session_threshold * 0.8 # 20% lower
495
- self.sleeping_threshold = self.session_threshold * 0.85 # 15% lower
496
- print(f"📊 Thresholds - Session: {self.session_threshold:.2f}, Database: {self.database_threshold:.2f}, Sleeping: {self.sleeping_threshold:.2f}")
497
 
498
  def reset_all(self):
499
  """Reset session"""
500
- print("\n📈 Session Summary:")
501
- print(f" • Session dogs: {len(self.session_dogs)}")
502
- print(f" • Known dogs detected: {len(set(self.known_dog_detections))}")
503
- print(f" • Successful re-entries: {self.reentry_matches}")
504
-
505
  self.session_dogs.clear()
506
- self.session_best_images.clear()
507
- self.temp_to_permanent.clear()
508
  self.sleeping_tracks.clear()
509
  self.next_temp_id = 1
510
  self.current_frame = 0
511
- self.known_dog_detections.clear()
512
- self.reentry_matches = 0
513
- print("🔄 Session reset\n")
514
 
515
  def get_statistics(self) -> Dict:
516
  """Get statistics"""
 
 
 
517
  return {
518
- 'session_dogs': len(self.session_dogs),
519
- 'permanent_dogs': len(self.permanent_dogs),
520
  'sleeping_tracks': len(self.sleeping_tracks),
521
- 'known_detected': list(set(self.known_dog_detections)),
522
- 'reentry_matches': self.reentry_matches,
523
- 'threshold': self.session_threshold
524
  }
525
 
526
  # Compatibility aliases
527
- MegaDescriptorReID = EnhancedMegaDescriptorReID
528
- MultiComponentReID = EnhancedMegaDescriptorReID
 
1
  """
2
+ Enhanced ReID with SQLite Database Integration
3
+ Combines sleeping tracks + multi-stage matching + rich database storage
4
  """
5
  import numpy as np
6
  import cv2
 
9
  from sklearn.metrics.pairwise import cosine_similarity
10
  from typing import Dict, List, Optional, Tuple
11
  from dataclasses import dataclass, field
12
+ from collections import deque
 
 
13
  from datetime import datetime, timedelta
14
+ from pathlib import Path
15
  import warnings
16
  warnings.filterwarnings('ignore')
17
 
18
+ from database import DogDatabase
19
+
20
  @dataclass
21
  class DogFeatures:
22
+ """Container for dog features with metadata"""
23
  features: np.ndarray
24
  bbox: List[float] = field(default_factory=list)
25
  confidence: float = 0.5
26
  frame_num: int = 0
 
27
  timestamp: datetime = field(default_factory=datetime.now)
28
+ image: Optional[np.ndarray] = None
29
+ angle: str = "unknown"
30
+ distance: str = "medium"
31
 
32
  @dataclass
33
  class SleepingTrack:
34
+ """Track that recently left the scene"""
35
+ dog_id: int
 
 
36
  last_position: Tuple[float, float]
37
+ last_seen: datetime
38
+ features_list: List[DogFeatures]
39
+ avg_embedding: np.ndarray
40
 
41
+ class SQLiteEnhancedReID:
42
+ """
43
+ Enhanced ReID with SQLite backend
44
+ - Stores all embeddings in database
45
+ - Sleeping tracks for re-entry
46
+ - Multi-stage matching
47
+ - Rich querying capabilities
48
+ """
 
 
49
 
50
+ def __init__(self, device: str = 'cuda', db_path: str = 'dog_monitoring.db'):
51
  self.device = device if torch.cuda.is_available() else 'cpu'
52
 
53
+ # Initialize SQLite database
54
+ self.db = DogDatabase(db_path)
 
 
55
 
56
+ # Thresholds
57
+ self.session_threshold = 0.35
58
+ self.database_threshold = 0.28
59
+ self.sleeping_threshold = 0.32
60
 
61
+ # Sleeping tracks system
62
+ self.sleeping_tracks: List[SleepingTrack] = []
63
+ self.sleeping_track_timeout = 120
64
+ self.max_sleeping_tracks = 20
65
 
66
+ # Session tracking (temp IDs map to permanent dog_ids)
67
+ self.session_dogs = {} # temp_id -> dog_id in database
68
+ self.temp_id_features = {} # temp_id -> list of features
 
69
  self.next_temp_id = 1
70
  self.current_frame = 0
71
+ self.current_video_source = "unknown"
72
 
73
+ # Cache for database embeddings (loaded once)
74
+ self.db_embeddings_cache = {}
75
+ self._load_database_embeddings()
 
 
 
 
76
 
77
  # Initialize model
78
  self._initialize_megadescriptor()
79
 
80
+ print(f"SQLite Enhanced ReID initialized")
81
+ print(f"Database: {db_path}")
82
+ print(f"Registered dogs: {len(self.db_embeddings_cache)}")
 
83
 
84
  def _initialize_megadescriptor(self):
85
  """Initialize MegaDescriptor-L-384"""
 
95
  mean=[0.5, 0.5, 0.5],
96
  std=[0.5, 0.5, 0.5]
97
  )
98
+ print("MegaDescriptor-L-384 loaded")
99
  except Exception as e:
100
+ print(f"Error loading model: {e}")
101
  self.model = None
102
 
103
+ def _load_database_embeddings(self):
104
+ """Load all embeddings from database into memory cache"""
105
+ self.db_embeddings_cache.clear()
 
106
 
107
+ # Get all active dogs
108
+ dogs_df = self.db.get_all_dogs(active_only=True)
109
+
110
+ for _, dog in dogs_df.iterrows():
111
+ dog_id = dog['dog_id']
112
+
113
+ # Get recent features for this dog
114
+ features = self.db.get_features(dog_id, limit=20)
115
+
116
+ if features:
117
+ # Store all embeddings for this dog
118
+ embeddings = [f['resnet_features'] for f in features]
119
+ self.db_embeddings_cache[dog_id] = {
120
+ 'name': dog['name'] or f"Dog #{dog_id}",
121
+ 'embeddings': embeddings,
122
+ 'total_sightings': dog['total_sightings']
123
+ }
124
+
125
+ print(f"Loaded {len(self.db_embeddings_cache)} dogs with embeddings from database")
126
+
127
+ def set_video_source(self, video_path: str):
128
+ """Set current video source for tracking"""
129
+ self.current_video_source = video_path
130
+
131
+ def extract_features(self, image: np.ndarray, bbox: List[float] = None) -> Optional[DogFeatures]:
132
+ """Extract features using MegaDescriptor-L"""
133
+ if image is None or image.size == 0 or self.model is None:
134
+ return None
135
 
136
  try:
137
+ img_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
138
+ from PIL import Image
139
+ pil_img = Image.fromarray(img_rgb)
140
+ img_tensor = self.transform(pil_img).unsqueeze(0).to(self.device)
141
 
142
+ with torch.no_grad():
143
+ features = self.model(img_tensor)
144
+ features = features.squeeze().cpu().numpy()
145
+ features = features / (np.linalg.norm(features) + 1e-7)
146
+
147
+ # Classify angle and distance
148
+ h, w = image.shape[:2]
149
+ aspect_ratio = w / h if h > 0 else 1.0
150
+ angle = "side" if 0.8 < aspect_ratio < 1.5 else "front" if aspect_ratio > 1.5 else "angled"
151
+ distance = "close" if max(h, w) > 200 else "far" if max(h, w) < 80 else "medium"
 
 
 
152
 
153
+ return DogFeatures(
154
+ features=features,
155
+ bbox=bbox if bbox else [0, 0, 100, 100],
156
+ frame_num=self.current_frame,
157
+ timestamp=datetime.now(),
158
+ image=image.copy(),
159
+ angle=angle,
160
+ distance=distance
161
+ )
162
  except Exception as e:
163
+ print(f"Feature extraction error: {e}")
164
+ return None
165
 
166
+ def check_sleeping_tracks(self, features: np.ndarray, position: Tuple[float, float]) -> Optional[int]:
167
+ """
168
+ Check if detection matches recently lost track
169
+ Returns: dog_id if match found
170
+ """
171
+ if not self.sleeping_tracks:
172
+ return None
173
+
174
+ current_time = datetime.now()
175
+ best_match = None
176
+ best_score = 0
 
 
 
 
 
 
 
 
 
177
 
178
+ # Clean up old sleeping tracks
179
+ self.sleeping_tracks = [
180
+ st for st in self.sleeping_tracks
181
+ if (current_time - st.last_seen).total_seconds() < self.sleeping_track_timeout
182
+ ]
183
+
184
+ for sleeping_track in self.sleeping_tracks:
185
+ # Temporal proximity bonus
186
+ time_diff = (current_time - sleeping_track.last_seen).total_seconds()
187
+ time_bonus = 0.05 if time_diff < 10 else 0.02 if time_diff < 30 else 0
188
+
189
+ # Spatial proximity bonus
190
+ last_x, last_y = sleeping_track.last_position
191
+ curr_x, curr_y = position
192
+ spatial_distance = np.sqrt((curr_x - last_x)**2 + (curr_y - last_y)**2)
193
+ spatial_bonus = 0.05 if spatial_distance < 100 else 0.02 if spatial_distance < 200 else 0
194
+
195
+ # Feature similarity
196
+ similarity = cosine_similarity(
197
+ features.reshape(1, -1),
198
+ sleeping_track.avg_embedding.reshape(1, -1)
199
+ )[0, 0]
200
+
201
+ final_score = similarity + time_bonus + spatial_bonus
202
+
203
+ if final_score > best_score and final_score >= self.sleeping_threshold:
204
+ best_score = final_score
205
+ best_match = sleeping_track.dog_id
206
 
207
+ if best_match:
208
+ print(f" Re-entry: Dog ID {best_match} (score: {best_score:.3f})")
209
+ # Remove from sleeping tracks
210
+ self.sleeping_tracks = [st for st in self.sleeping_tracks if st.dog_id != best_match]
211
 
212
+ return best_match
213
 
214
+ def check_database(self, features: np.ndarray) -> Optional[int]:
215
+ """Check against database dogs with separate threshold"""
216
+ if not self.db_embeddings_cache:
217
+ return None
218
 
219
+ best_match = None
220
+ best_score = 0
221
+ all_scores = []
222
+
223
+ for dog_id, dog_data in self.db_embeddings_cache.items():
224
+ embeddings = dog_data['embeddings']
 
225
 
226
+ # Compare with all stored embeddings
227
  similarities = []
228
+ for emb in embeddings:
229
  sim = cosine_similarity(
230
  features.reshape(1, -1),
231
+ emb.reshape(1, -1)
232
  )[0, 0]
233
  similarities.append(sim)
234
 
235
+ max_sim = max(similarities) if similarities else 0
236
+ all_scores.append((dog_id, dog_data['name'], max_sim))
237
+
238
+ if max_sim > best_score:
239
+ best_score = max_sim
240
+ best_match = dog_id
241
+
242
+ # Debug output
243
+ if all_scores:
244
+ all_scores.sort(key=lambda x: x[2], reverse=True)
245
+ print(f" DB matches: {[(n, f'{s:.3f}') for _, n, s in all_scores[:3]]}")
246
+
247
+ if best_score >= self.database_threshold:
248
+ dog_name = self.db_embeddings_cache[best_match]['name']
249
+ print(f" DATABASE MATCH: {dog_name} (ID: {best_match}, score: {best_score:.3f})")
250
+ return best_match
251
 
 
252
  return None
253
 
254
+ def check_session(self, features: np.ndarray) -> Optional[int]:
255
+ """Check against current session dogs (temp IDs)"""
256
+ best_temp_id = None
257
+ best_score = -1.0
 
 
 
258
 
259
+ for temp_id, features_list in self.temp_id_features.items():
 
 
 
 
260
  similarities = []
261
+ for stored_feat in features_list[-30:]:
262
  sim = cosine_similarity(
263
  features.reshape(1, -1),
264
+ stored_feat.features.reshape(1, -1)
265
  )[0, 0]
266
  similarities.append(sim)
267
 
268
  if similarities:
 
269
  max_sim = np.max(similarities)
270
+ avg_sim = np.mean(similarities[-10:])
271
+ final_score = 0.7 * max_sim + 0.3 * avg_sim
272
 
273
  if final_score > best_score:
274
  best_score = final_score
275
+ best_temp_id = temp_id
276
 
277
+ if best_temp_id is not None and best_score >= self.session_threshold:
278
+ return best_temp_id
 
 
279
 
280
  return None
281
 
282
+ def match_or_register(self, track, image_crop=None) -> Tuple[int, int, float, bool]:
283
+ """
284
+ Three-stage matching with SQLite storage
285
+ Returns: (temp_id, dog_id, confidence, is_known)
286
+ """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
287
  self.current_frame += 1
288
 
289
+ # Get detection
290
  detection = None
291
  for det in reversed(track.detections[-3:]):
292
  if det.image_crop is not None:
 
295
  break
296
 
297
  if detection is None or image_crop is None:
298
+ return 0, 0, 0.0, False
299
 
300
+ # Extract features
301
+ features_obj = self.extract_features(
302
  image_crop,
303
  detection.bbox if hasattr(detection, 'bbox') else None
304
  )
305
 
306
+ if features_obj is None:
307
+ return 0, 0, 0.0, False
308
 
309
+ features_obj.confidence = detection.confidence if hasattr(detection, 'confidence') else 0.5
310
+ features = features_obj.features
311
 
312
+ # Get position
313
+ bbox = features_obj.bbox
314
+ position = ((bbox[0] + bbox[2]) / 2, (bbox[1] + bbox[3]) / 2)
315
 
316
+ # STAGE 1: Check sleeping tracks
317
+ sleeping_dog_id = self.check_sleeping_tracks(features, position)
318
+ if sleeping_dog_id:
319
+ # Restore to active session
320
+ temp_id = self._get_temp_id_for_dog(sleeping_dog_id)
321
+ if temp_id not in self.temp_id_features:
322
+ self.temp_id_features[temp_id] = []
323
+ self.temp_id_features[temp_id].append(features_obj)
 
 
 
 
 
 
 
324
 
325
+ # Save to database
326
+ self._save_to_database(sleeping_dog_id, features_obj, detection)
327
 
328
+ return temp_id, sleeping_dog_id, 1.0, True
329
 
330
+ # STAGE 2: Check database
331
+ db_dog_id = self.check_database(features)
332
 
333
+ # STAGE 3: Check session
334
+ session_temp_id = self.check_session(features)
 
335
 
336
+ if session_temp_id is not None:
337
+ # Existing session dog
338
+ self.temp_id_features[session_temp_id].append(features_obj)
339
+ if len(self.temp_id_features[session_temp_id]) > 50:
340
+ self.temp_id_features[session_temp_id] = self.temp_id_features[session_temp_id][-50:]
 
 
 
341
 
342
+ # Get or create dog_id
343
+ if session_temp_id in self.session_dogs:
344
+ dog_id = self.session_dogs[session_temp_id]
345
+ elif db_dog_id:
346
+ dog_id = db_dog_id
347
+ self.session_dogs[session_temp_id] = dog_id
348
+ else:
349
+ dog_id = 0 # Will be created on save
 
 
 
 
 
 
 
 
350
 
351
+ if dog_id > 0:
352
+ self._save_to_database(dog_id, features_obj, detection)
 
353
 
354
+ return session_temp_id, dog_id, 0.8, (db_dog_id is not None)
355
+
356
  else:
357
  # New dog in session
358
  new_temp_id = self.next_temp_id
359
  self.next_temp_id += 1
360
+ self.temp_id_features[new_temp_id] = [features_obj]
 
361
 
362
+ if db_dog_id:
363
+ # Known dog from database
364
+ self.session_dogs[new_temp_id] = db_dog_id
365
+ self._save_to_database(db_dog_id, features_obj, detection)
366
+ print(f" Known dog (ID {db_dog_id}) -> Temp ID {new_temp_id}")
367
+ return new_temp_id, db_dog_id, 1.0, True
368
  else:
369
+ # Completely new dog
370
+ print(f" New dog: Temp ID {new_temp_id}")
371
+ return new_temp_id, 0, 1.0, False
372
 
373
+ def _get_temp_id_for_dog(self, dog_id: int) -> int:
374
+ """Get existing temp_id for dog_id or create new one"""
375
+ for temp_id, stored_dog_id in self.session_dogs.items():
376
+ if stored_dog_id == dog_id:
377
+ return temp_id
378
+
379
+ # Create new temp_id
380
+ new_temp_id = self.next_temp_id
381
+ self.next_temp_id += 1
382
+ self.session_dogs[new_temp_id] = dog_id
383
+ return new_temp_id
 
 
 
 
 
 
 
 
384
 
385
+ def _save_to_database(self, dog_id: int, features: DogFeatures, detection):
386
+ """Save features and image to database"""
387
+ try:
388
+ # Update dog sighting
389
+ self.db.update_dog_sighting(dog_id)
 
390
 
391
+ # Save features
392
+ # Create dummy color histogram (you can compute real one if needed)
393
+ color_histogram = np.zeros(256)
394
+
395
+ self.db.save_features(
396
+ dog_id=dog_id,
397
+ resnet_features=features.features,
398
+ color_histogram=color_histogram,
399
+ confidence=features.confidence
400
+ )
401
+
402
+ # Save image
403
+ image_id = self.db.save_image(
404
+ dog_id=dog_id,
405
+ image=features.image,
406
+ frame_number=features.frame_num,
407
+ video_source=self.current_video_source,
408
+ bbox=features.bbox,
409
+ confidence=features.confidence
410
+ )
411
+
412
+ # Add sighting
413
+ position = ((features.bbox[0] + features.bbox[2]) / 2,
414
+ (features.bbox[1] + features.bbox[3]) / 2)
415
+
416
+ self.db.add_sighting(
417
+ dog_id=dog_id,
418
+ position=position,
419
+ video_source=self.current_video_source,
420
+ frame_number=features.frame_num,
421
+ confidence=features.confidence
422
+ )
423
+
424
+ except Exception as e:
425
+ print(f"Database save error: {e}")
426
+
427
+ def move_to_sleeping_tracks(self, temp_id: int, last_position: Tuple[float, float]):
428
+ """Move lost track to sleeping tracks"""
429
+ if temp_id not in self.temp_id_features:
430
+ return
431
+
432
+ features_list = self.temp_id_features[temp_id]
433
+ if not features_list:
434
+ return
435
+
436
+ # Get dog_id if known
437
+ dog_id = self.session_dogs.get(temp_id, 0)
438
+ if dog_id == 0:
439
+ return # Don't store unknown dogs in sleeping
440
+
441
+ # Calculate average embedding
442
+ embeddings = [f.features for f in features_list]
443
+ avg_embedding = np.mean(embeddings, axis=0)
444
+ avg_embedding = avg_embedding / np.linalg.norm(avg_embedding)
445
+
446
+ sleeping_track = SleepingTrack(
447
+ dog_id=dog_id,
448
+ last_position=last_position,
449
+ last_seen=datetime.now(),
450
+ features_list=features_list[-10:],
451
+ avg_embedding=avg_embedding
452
+ )
453
+
454
+ self.sleeping_tracks.append(sleeping_track)
455
+
456
+ if len(self.sleeping_tracks) > self.max_sleeping_tracks:
457
+ self.sleeping_tracks = sorted(
458
+ self.sleeping_tracks,
459
+ key=lambda x: x.last_seen,
460
+ reverse=True
461
+ )[:self.max_sleeping_tracks]
462
+
463
+ print(f" Sleeping: Dog {dog_id} (total: {len(self.sleeping_tracks)})")
464
 
465
  def save_session_to_permanent(self) -> Dict:
466
+ """Save new dogs to database"""
467
  saved_dogs = {}
468
 
469
+ for temp_id, features_list in self.temp_id_features.items():
470
+ # Skip if already has dog_id
471
+ if temp_id in self.session_dogs:
 
 
 
 
 
 
 
 
 
 
 
 
472
  continue
473
 
474
+ # Create new dog in database
475
+ dog_id = self.db.add_dog(name=f"Dog_{datetime.now().strftime('%Y%m%d_%H%M%S')}")
476
+ self.session_dogs[temp_id] = dog_id
 
 
477
 
478
+ # Save all features and images
479
+ for features in features_list:
480
+ # Create dummy detection-like object
481
+ class DummyDetection:
482
+ pass
483
+ det = DummyDetection()
484
+ det.bbox = features.bbox
485
+ det.confidence = features.confidence
486
+ det.image_crop = features.image
487
+
488
+ self._save_to_database(dog_id, features, det)
 
 
 
 
 
 
489
 
490
+ saved_dogs[temp_id] = dog_id
491
+ print(f" Saved: Temp {temp_id} -> Dog ID {dog_id}")
 
492
 
493
+ # Reload cache
494
+ if saved_dogs:
495
+ self._load_database_embeddings()
496
 
497
  return saved_dogs
498
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
499
  def match_or_register_all(self, track) -> Dict:
500
  """Compatible interface"""
501
+ temp_id, dog_id, confidence, is_known = self.match_or_register(track)
502
  return {
503
  'MegaDescriptor': {
504
  'dog_id': temp_id,
505
+ 'permanent_id': dog_id,
506
  'confidence': confidence,
507
+ 'is_known': is_known,
508
+ 'permanent_name': self.db_embeddings_cache.get(dog_id, {}).get('name') if dog_id > 0 else None
509
  }
510
  }
511
 
512
  def set_all_thresholds(self, threshold: float):
513
  """Update thresholds"""
514
  self.session_threshold = max(0.15, min(0.95, threshold))
515
+ self.database_threshold = self.session_threshold - 0.07
516
+ self.sleeping_threshold = self.session_threshold - 0.03
517
+ print(f"Thresholds - Session: {self.session_threshold:.2f}, DB: {self.database_threshold:.2f}")
518
 
519
  def reset_all(self):
520
  """Reset session"""
521
+ print(f"\nSession Summary: {len(self.temp_id_features)} dogs, {len(self.sleeping_tracks)} sleeping")
522
+ self.temp_id_features.clear()
 
 
 
523
  self.session_dogs.clear()
 
 
524
  self.sleeping_tracks.clear()
525
  self.next_temp_id = 1
526
  self.current_frame = 0
527
+ print("Session reset\n")
 
 
528
 
529
  def get_statistics(self) -> Dict:
530
  """Get statistics"""
531
+ dogs_df = self.db.get_all_dogs()
532
+ db_stats = self.db.get_dog_statistics()
533
+
534
  return {
535
+ 'session_dogs': len(self.temp_id_features),
536
+ 'database_dogs': len(dogs_df),
537
  'sleeping_tracks': len(self.sleeping_tracks),
538
+ 'total_images': db_stats.get('total_images', 0),
539
+ 'total_sightings': db_stats.get('total_sightings', 0)
 
540
  }
541
 
542
  # Compatibility aliases
543
+ MegaDescriptorReID = SQLiteEnhancedReID
544
+ EnhancedMegaDescriptorReID = SQLiteEnhancedReID