mustafa2ak commited on
Commit
cf6bea7
·
verified ·
1 Parent(s): d4d1ce1

Update database.py

Browse files
Files changed (1) hide show
  1. database.py +129 -128
database.py CHANGED
@@ -13,6 +13,7 @@ from typing import List, Dict, Optional, Tuple, Any
13
  from pathlib import Path
14
  import pandas as pd
15
 
 
16
  class DogDatabase:
17
  """SQLite database manager for dog monitoring system"""
18
 
@@ -38,54 +39,54 @@ class DogDatabase:
38
  last_seen TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
39
  total_sightings INTEGER DEFAULT 1,
40
  notes TEXT,
41
- merged_from TEXT, -- JSON list of merged dog IDs
42
- status TEXT DEFAULT 'active' -- active, merged, deleted
43
  )
44
  """)
45
 
46
- # Dog features table - stores extracted features
47
  self.cursor.execute("""
48
  CREATE TABLE IF NOT EXISTS dog_features (
49
  feature_id INTEGER PRIMARY KEY AUTOINCREMENT,
50
  dog_id INTEGER,
51
- resnet_features BLOB, -- Pickled numpy array
52
- color_histogram BLOB, -- Pickled numpy array
53
  timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
54
  confidence REAL,
55
  FOREIGN KEY (dog_id) REFERENCES dogs(dog_id)
56
  )
57
  """)
58
 
59
- # Dog images table - stores actual images
60
  self.cursor.execute("""
61
  CREATE TABLE IF NOT EXISTS dog_images (
62
  image_id INTEGER PRIMARY KEY AUTOINCREMENT,
63
  dog_id INTEGER,
64
- image_data BLOB, -- Base64 encoded image
65
- thumbnail BLOB, -- Small preview
66
  width INTEGER,
67
  height INTEGER,
68
  timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
69
  frame_number INTEGER,
70
  video_source TEXT,
71
- bbox TEXT, -- JSON [x1, y1, x2, y2]
72
  confidence REAL,
73
- pose_keypoints TEXT, -- JSON of keypoints
74
  is_validated BOOLEAN DEFAULT 0,
75
  is_discarded BOOLEAN DEFAULT 0,
76
  FOREIGN KEY (dog_id) REFERENCES dogs(dog_id)
77
  )
78
  """)
79
 
80
- # Body parts table - stores cropped body parts
81
  self.cursor.execute("""
82
  CREATE TABLE IF NOT EXISTS body_parts (
83
  part_id INTEGER PRIMARY KEY AUTOINCREMENT,
84
  dog_id INTEGER,
85
  image_id INTEGER,
86
- part_type TEXT, -- 'head', 'torso', 'rear'
87
- part_image BLOB, -- Base64 encoded crop
88
- crop_bbox TEXT, -- JSON [x1, y1, x2, y2] relative to full image
89
  confidence REAL,
90
  is_validated BOOLEAN DEFAULT 0,
91
  is_discarded BOOLEAN DEFAULT 0,
@@ -94,7 +95,7 @@ class DogDatabase:
94
  )
95
  """)
96
 
97
- # Sightings table - tracks when/where dogs were seen
98
  self.cursor.execute("""
99
  CREATE TABLE IF NOT EXISTS sightings (
100
  sighting_id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -118,11 +119,11 @@ class DogDatabase:
118
  video_path TEXT,
119
  total_frames INTEGER,
120
  dogs_detected INTEGER,
121
- settings TEXT -- JSON of processing settings
122
  )
123
  """)
124
 
125
- # Create indexes for performance
126
  self.cursor.execute("CREATE INDEX IF NOT EXISTS idx_dog_features ON dog_features(dog_id)")
127
  self.cursor.execute("CREATE INDEX IF NOT EXISTS idx_dog_images ON dog_images(dog_id)")
128
  self.cursor.execute("CREATE INDEX IF NOT EXISTS idx_sightings ON sightings(dog_id)")
@@ -130,7 +131,6 @@ class DogDatabase:
130
  self.conn.commit()
131
 
132
  # ========== Dog Management ==========
133
-
134
  def add_dog(self, dog_id: Optional[int] = None, name: Optional[str] = None) -> int:
135
  """Add a new dog to the database"""
136
  if dog_id:
@@ -144,7 +144,6 @@ class DogDatabase:
144
  (name,)
145
  )
146
  dog_id = self.cursor.lastrowid
147
-
148
  self.conn.commit()
149
  return dog_id
150
 
@@ -161,21 +160,15 @@ class DogDatabase:
161
  def merge_dogs(self, keep_id: int, merge_id: int) -> bool:
162
  """Merge two dogs, keeping keep_id"""
163
  try:
164
- # Update all references
165
- self.cursor.execute("UPDATE dog_features SET dog_id = ? WHERE dog_id = ?",
166
- (keep_id, merge_id))
167
- self.cursor.execute("UPDATE dog_images SET dog_id = ? WHERE dog_id = ?",
168
- (keep_id, merge_id))
169
- self.cursor.execute("UPDATE sightings SET dog_id = ? WHERE dog_id = ?",
170
- (keep_id, merge_id))
171
 
172
- # Get merged_from history
173
  self.cursor.execute("SELECT merged_from FROM dogs WHERE dog_id = ?", (merge_id,))
174
  row = self.cursor.fetchone()
175
  merged_history = json.loads(row['merged_from'] if row and row['merged_from'] else '[]')
176
  merged_history.append(merge_id)
177
 
178
- # Update keep_id dog with merge history
179
  self.cursor.execute("""
180
  UPDATE dogs
181
  SET merged_from = ?,
@@ -185,12 +178,7 @@ class DogDatabase:
185
  WHERE dog_id = ?
186
  """, (json.dumps(merged_history), merge_id, keep_id))
187
 
188
- # Mark merge_id as merged
189
- self.cursor.execute(
190
- "UPDATE dogs SET status = 'merged' WHERE dog_id = ?",
191
- (merge_id,)
192
- )
193
-
194
  self.conn.commit()
195
  return True
196
  except Exception as e:
@@ -201,33 +189,25 @@ class DogDatabase:
201
  def delete_dog(self, dog_id: int, hard_delete: bool = False):
202
  """Delete or mark dog as deleted"""
203
  if hard_delete:
204
- # Hard delete - remove all data
205
  self.cursor.execute("DELETE FROM dog_features WHERE dog_id = ?", (dog_id,))
206
  self.cursor.execute("DELETE FROM dog_images WHERE dog_id = ?", (dog_id,))
207
  self.cursor.execute("DELETE FROM sightings WHERE dog_id = ?", (dog_id,))
208
  self.cursor.execute("DELETE FROM dogs WHERE dog_id = ?", (dog_id,))
209
  else:
210
- # Soft delete - mark as deleted
211
- self.cursor.execute(
212
- "UPDATE dogs SET status = 'deleted' WHERE dog_id = ?",
213
- (dog_id,)
214
- )
215
  self.conn.commit()
216
 
217
  # ========== Features Management ==========
218
-
219
  def save_features(self, dog_id: int, resnet_features: np.ndarray,
220
  color_histogram: np.ndarray, confidence: float):
221
  """Save dog features to database"""
222
  resnet_blob = pickle.dumps(resnet_features)
223
  color_blob = pickle.dumps(color_histogram)
224
-
225
  self.cursor.execute("""
226
  INSERT INTO dog_features
227
  (dog_id, resnet_features, color_histogram, confidence)
228
  VALUES (?, ?, ?, ?)
229
  """, (dog_id, resnet_blob, color_blob, confidence))
230
-
231
  self.conn.commit()
232
 
233
  def get_features(self, dog_id: int, limit: int = 20) -> List[Dict]:
@@ -238,7 +218,6 @@ class DogDatabase:
238
  ORDER BY timestamp DESC
239
  LIMIT ?
240
  """, (dog_id, limit))
241
-
242
  features = []
243
  for row in self.cursor.fetchall():
244
  features.append({
@@ -247,27 +226,22 @@ class DogDatabase:
247
  'confidence': row['confidence'],
248
  'timestamp': row['timestamp']
249
  })
250
-
251
  return features
252
 
253
  # ========== Images Management ==========
254
-
255
  def save_image(self, dog_id: int, image: np.ndarray,
256
  frame_number: int, video_source: str,
257
  bbox: List[float], confidence: float,
258
  pose_keypoints: Optional[List] = None):
259
  """Save dog image to database"""
260
- # Encode image as JPEG
261
  _, buffer = cv2.imencode('.jpg', image)
262
  image_data = base64.b64encode(buffer).decode('utf-8')
263
 
264
- # Create thumbnail
265
  thumbnail = cv2.resize(image, (128, 128))
266
  _, thumb_buffer = cv2.imencode('.jpg', thumbnail, [cv2.IMWRITE_JPEG_QUALITY, 70])
267
  thumb_data = base64.b64encode(thumb_buffer).decode('utf-8')
268
 
269
  h, w = image.shape[:2]
270
-
271
  self.cursor.execute("""
272
  INSERT INTO dog_images
273
  (dog_id, image_data, thumbnail, width, height,
@@ -276,21 +250,18 @@ class DogDatabase:
276
  """, (dog_id, image_data, thumb_data, w, h,
277
  frame_number, video_source, json.dumps(bbox),
278
  confidence, json.dumps(pose_keypoints) if pose_keypoints else None))
279
-
280
  self.conn.commit()
281
- return self.cursor.lastrowid # THIS LINE - make sure it returns the image_id
282
 
283
  def get_dog_images(self, dog_id: int, validated_only: bool = False,
284
  include_discarded: bool = False) -> List[Dict]:
285
  """Get all images for a dog"""
286
  query = "SELECT * FROM dog_images WHERE dog_id = ?"
287
  params = [dog_id]
288
-
289
  if validated_only:
290
  query += " AND is_validated = 1"
291
  if not include_discarded:
292
  query += " AND is_discarded = 0"
293
-
294
  query += " ORDER BY timestamp DESC"
295
 
296
  self.cursor.execute(query, params)
@@ -298,11 +269,9 @@ class DogDatabase:
298
 
299
  images = []
300
  for row in rows:
301
- # Decode image
302
  image_bytes = base64.b64decode(row['image_data'])
303
  nparr = np.frombuffer(image_bytes, np.uint8)
304
  image = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
305
-
306
  images.append({
307
  'image_id': row['image_id'],
308
  'image': image,
@@ -315,26 +284,17 @@ class DogDatabase:
315
  'is_discarded': row['is_discarded'],
316
  'pose_keypoints': json.loads(row['pose_keypoints']) if row['pose_keypoints'] else None
317
  })
318
-
319
  return images
320
-
321
 
322
  def validate_image(self, image_id: int, is_valid: bool = True):
323
  """Mark image as validated or discarded"""
324
  if is_valid:
325
- self.cursor.execute(
326
- "UPDATE dog_images SET is_validated = 1 WHERE image_id = ?",
327
- (image_id,)
328
- )
329
  else:
330
- self.cursor.execute(
331
- "UPDATE dog_images SET is_discarded = 1 WHERE image_id = ?",
332
- (image_id,)
333
- )
334
  self.conn.commit()
335
 
336
  # ========== Body Parts Management ==========
337
-
338
  def save_body_parts(self, dog_id: int, image_id: int,
339
  head_crop: Optional[np.ndarray],
340
  torso_crop: Optional[np.ndarray],
@@ -346,21 +306,16 @@ class DogDatabase:
346
  'torso': torso_crop,
347
  'rear': rear_crop
348
  }
349
-
350
  for part_type, crop in parts.items():
351
  if crop is not None:
352
- # Encode crop as JPEG
353
  _, buffer = cv2.imencode('.jpg', crop)
354
  crop_data = base64.b64encode(buffer).decode('utf-8')
355
-
356
  confidence = confidences.get(part_type, 0.0)
357
-
358
  self.cursor.execute("""
359
  INSERT INTO body_parts
360
  (dog_id, image_id, part_type, part_image, confidence)
361
  VALUES (?, ?, ?, ?, ?)
362
  """, (dog_id, image_id, part_type, crop_data, confidence))
363
-
364
  self.conn.commit()
365
 
366
  def get_body_parts(self, dog_id: int, part_type: Optional[str] = None,
@@ -368,26 +323,20 @@ class DogDatabase:
368
  """Get body part crops for a dog"""
369
  query = "SELECT * FROM body_parts WHERE dog_id = ?"
370
  params = [dog_id]
371
-
372
  if part_type:
373
  query += " AND part_type = ?"
374
  params.append(part_type)
375
-
376
  if validated_only:
377
  query += " AND is_validated = 1"
378
-
379
  if not include_discarded:
380
  query += " AND is_discarded = 0"
381
-
382
  self.cursor.execute(query, params)
383
 
384
  parts = []
385
  for row in self.cursor.fetchall():
386
- # Decode image
387
  image_bytes = base64.b64decode(row['part_image'])
388
  nparr = np.frombuffer(image_bytes, np.uint8)
389
  image = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
390
-
391
  parts.append({
392
  'part_id': row['part_id'],
393
  'part_type': row['part_type'],
@@ -396,21 +345,14 @@ class DogDatabase:
396
  'is_validated': row['is_validated'],
397
  'image_id': row['image_id']
398
  })
399
-
400
  return parts
401
 
402
  def validate_body_part(self, part_id: int, is_valid: bool = True):
403
  """Mark body part as validated or discarded"""
404
  if is_valid:
405
- self.cursor.execute(
406
- "UPDATE body_parts SET is_validated = 1 WHERE part_id = ?",
407
- (part_id,)
408
- )
409
  else:
410
- self.cursor.execute(
411
- "UPDATE body_parts SET is_discarded = 1 WHERE part_id = ?",
412
- (part_id,)
413
- )
414
  self.conn.commit()
415
 
416
  def add_sighting(self, dog_id: int, position: Tuple[float, float],
@@ -421,41 +363,32 @@ class DogDatabase:
421
  (dog_id, position_x, position_y, video_source, frame_number, confidence)
422
  VALUES (?, ?, ?, ?, ?, ?)
423
  """, (dog_id, position[0], position[1], video_source, frame_number, confidence))
424
-
425
  self.conn.commit()
426
 
427
  # ========== Query Methods ==========
428
-
429
  def get_all_dogs(self, active_only: bool = True) -> pd.DataFrame:
430
  """Get all dogs as DataFrame"""
431
  query = "SELECT * FROM dogs"
432
  if active_only:
433
  query += " WHERE status = 'active'"
434
  query += " ORDER BY dog_id"
435
-
436
  return pd.read_sql_query(query, self.conn)
437
 
438
  def get_dog_statistics(self) -> Dict:
439
  """Get overall statistics"""
440
  stats = {}
441
-
442
- # Total dogs
443
  self.cursor.execute("SELECT COUNT(*) FROM dogs WHERE status = 'active'")
444
  stats['total_active_dogs'] = self.cursor.fetchone()[0]
445
 
446
- # Total images
447
  self.cursor.execute("SELECT COUNT(*) FROM dog_images WHERE is_discarded = 0")
448
  stats['total_images'] = self.cursor.fetchone()[0]
449
 
450
- # Validated images
451
  self.cursor.execute("SELECT COUNT(*) FROM dog_images WHERE is_validated = 1")
452
  stats['validated_images'] = self.cursor.fetchone()[0]
453
 
454
- # Total sightings
455
  self.cursor.execute("SELECT COUNT(*) FROM sightings")
456
  stats['total_sightings'] = self.cursor.fetchone()[0]
457
 
458
- # Most seen dog
459
  self.cursor.execute("""
460
  SELECT d.dog_id, d.name, d.total_sightings
461
  FROM dogs d
@@ -470,47 +403,33 @@ class DogDatabase:
470
  'name': row[1] or f"Dog #{row[0]}",
471
  'sightings': row[2]
472
  }
473
-
474
  return stats
475
 
476
  # ========== Export Methods ==========
477
-
478
  def export_training_dataset(self, output_dir: str, validated_only: bool = True) -> Dict:
479
  """Export dataset with body parts for fine-tuning"""
480
  output_path = Path(output_dir)
481
  output_path.mkdir(parents=True, exist_ok=True)
482
 
483
- # Create directories
484
  images_dir = output_path / "images"
485
  images_dir.mkdir(exist_ok=True)
486
 
487
- # Export data
488
  dataset = []
489
-
490
  dogs = self.get_all_dogs()
491
  for _, dog in dogs.iterrows():
492
  dog_id = dog['dog_id']
493
-
494
- # Create directories for each dog
495
  dog_dir = images_dir / f"dog_{dog_id}"
496
  dog_dir.mkdir(exist_ok=True)
497
 
498
- # Subdirectories for body parts
499
  for part in ['full', 'head', 'torso', 'rear']:
500
- part_dir = dog_dir / part
501
- part_dir.mkdir(exist_ok=True)
502
 
503
- # Get full images
504
  images = self.get_dog_images(dog_id, validated_only=validated_only)
505
-
506
  for idx, img_data in enumerate(images):
507
- # Save full image
508
  full_path = dog_dir / 'full' / f"img_{idx:04d}.jpg"
509
  cv2.imwrite(str(full_path), img_data['image'])
510
 
511
- # Get and save body parts for this image
512
  parts = self.get_body_parts(dog_id, validated_only=validated_only)
513
-
514
  part_paths = {}
515
  for part_data in parts:
516
  if part_data['image_id'] == img_data['image_id']:
@@ -519,25 +438,19 @@ class DogDatabase:
519
  cv2.imwrite(str(part_path), part_data['image'])
520
  part_paths[part_type] = str(part_path.relative_to(output_path))
521
 
522
- # Add to dataset
523
  dataset_entry = {
524
  'dog_id': dog_id,
525
  'full_image': str(full_path.relative_to(output_path)),
526
  'bbox': img_data['bbox'],
527
  'confidence': img_data['confidence']
528
  }
529
-
530
- # Add body part paths if available
531
  for part_type in ['head', 'torso', 'rear']:
532
  dataset_entry[f'{part_type}_image'] = part_paths.get(part_type, None)
533
-
534
  dataset.append(dataset_entry)
535
 
536
- # Save dataset info
537
  dataset_df = pd.DataFrame(dataset)
538
  dataset_df.to_csv(output_path / "dataset.csv", index=False)
539
 
540
- # Save metadata
541
  metadata = {
542
  'total_dogs': len(dogs),
543
  'total_images': len(dataset),
@@ -545,37 +458,27 @@ class DogDatabase:
545
  'validated_only': validated_only,
546
  'includes_body_parts': True
547
  }
548
-
549
  with open(output_path / "metadata.json", 'w') as f:
550
  json.dump(metadata, f, indent=2)
551
 
552
- # Create training splits
553
  from sklearn.model_selection import train_test_split
554
-
555
- train_df, test_df = train_test_split(dataset_df, test_size=0.2,
556
- stratify=dataset_df['dog_id'])
557
  train_df.to_csv(output_path / "train.csv", index=False)
558
  test_df.to_csv(output_path / "test.csv", index=False)
559
 
560
  metadata['train_samples'] = len(train_df)
561
  metadata['test_samples'] = len(test_df)
562
-
563
  return metadata
564
 
565
  # ========== Cleanup Methods ==========
566
-
567
  def reset_database(self, confirm: bool = False):
568
  """Reset entire database"""
569
  if not confirm:
570
  return False
571
-
572
  tables = ['sightings', 'dog_images', 'dog_features', 'dogs', 'sessions']
573
  for table in tables:
574
  self.cursor.execute(f"DELETE FROM {table}")
575
-
576
- # Reset autoincrement
577
  self.cursor.execute("DELETE FROM sqlite_sequence")
578
-
579
  self.conn.commit()
580
  return True
581
 
@@ -590,4 +493,102 @@ class DogDatabase:
590
  def __del__(self):
591
  """Ensure connection is closed"""
592
  if hasattr(self, 'conn'):
593
- self.conn.close()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  from pathlib import Path
14
  import pandas as pd
15
 
16
+
17
  class DogDatabase:
18
  """SQLite database manager for dog monitoring system"""
19
 
 
39
  last_seen TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
40
  total_sightings INTEGER DEFAULT 1,
41
  notes TEXT,
42
+ merged_from TEXT,
43
+ status TEXT DEFAULT 'active'
44
  )
45
  """)
46
 
47
+ # Dog features table
48
  self.cursor.execute("""
49
  CREATE TABLE IF NOT EXISTS dog_features (
50
  feature_id INTEGER PRIMARY KEY AUTOINCREMENT,
51
  dog_id INTEGER,
52
+ resnet_features BLOB,
53
+ color_histogram BLOB,
54
  timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
55
  confidence REAL,
56
  FOREIGN KEY (dog_id) REFERENCES dogs(dog_id)
57
  )
58
  """)
59
 
60
+ # Dog images table
61
  self.cursor.execute("""
62
  CREATE TABLE IF NOT EXISTS dog_images (
63
  image_id INTEGER PRIMARY KEY AUTOINCREMENT,
64
  dog_id INTEGER,
65
+ image_data BLOB,
66
+ thumbnail BLOB,
67
  width INTEGER,
68
  height INTEGER,
69
  timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
70
  frame_number INTEGER,
71
  video_source TEXT,
72
+ bbox TEXT,
73
  confidence REAL,
74
+ pose_keypoints TEXT,
75
  is_validated BOOLEAN DEFAULT 0,
76
  is_discarded BOOLEAN DEFAULT 0,
77
  FOREIGN KEY (dog_id) REFERENCES dogs(dog_id)
78
  )
79
  """)
80
 
81
+ # Body parts table
82
  self.cursor.execute("""
83
  CREATE TABLE IF NOT EXISTS body_parts (
84
  part_id INTEGER PRIMARY KEY AUTOINCREMENT,
85
  dog_id INTEGER,
86
  image_id INTEGER,
87
+ part_type TEXT,
88
+ part_image BLOB,
89
+ crop_bbox TEXT,
90
  confidence REAL,
91
  is_validated BOOLEAN DEFAULT 0,
92
  is_discarded BOOLEAN DEFAULT 0,
 
95
  )
96
  """)
97
 
98
+ # Sightings table
99
  self.cursor.execute("""
100
  CREATE TABLE IF NOT EXISTS sightings (
101
  sighting_id INTEGER PRIMARY KEY AUTOINCREMENT,
 
119
  video_path TEXT,
120
  total_frames INTEGER,
121
  dogs_detected INTEGER,
122
+ settings TEXT
123
  )
124
  """)
125
 
126
+ # Create indexes
127
  self.cursor.execute("CREATE INDEX IF NOT EXISTS idx_dog_features ON dog_features(dog_id)")
128
  self.cursor.execute("CREATE INDEX IF NOT EXISTS idx_dog_images ON dog_images(dog_id)")
129
  self.cursor.execute("CREATE INDEX IF NOT EXISTS idx_sightings ON sightings(dog_id)")
 
131
  self.conn.commit()
132
 
133
  # ========== Dog Management ==========
 
134
  def add_dog(self, dog_id: Optional[int] = None, name: Optional[str] = None) -> int:
135
  """Add a new dog to the database"""
136
  if dog_id:
 
144
  (name,)
145
  )
146
  dog_id = self.cursor.lastrowid
 
147
  self.conn.commit()
148
  return dog_id
149
 
 
160
  def merge_dogs(self, keep_id: int, merge_id: int) -> bool:
161
  """Merge two dogs, keeping keep_id"""
162
  try:
163
+ self.cursor.execute("UPDATE dog_features SET dog_id = ? WHERE dog_id = ?", (keep_id, merge_id))
164
+ self.cursor.execute("UPDATE dog_images SET dog_id = ? WHERE dog_id = ?", (keep_id, merge_id))
165
+ self.cursor.execute("UPDATE sightings SET dog_id = ? WHERE dog_id = ?", (keep_id, merge_id))
 
 
 
 
166
 
 
167
  self.cursor.execute("SELECT merged_from FROM dogs WHERE dog_id = ?", (merge_id,))
168
  row = self.cursor.fetchone()
169
  merged_history = json.loads(row['merged_from'] if row and row['merged_from'] else '[]')
170
  merged_history.append(merge_id)
171
 
 
172
  self.cursor.execute("""
173
  UPDATE dogs
174
  SET merged_from = ?,
 
178
  WHERE dog_id = ?
179
  """, (json.dumps(merged_history), merge_id, keep_id))
180
 
181
+ self.cursor.execute("UPDATE dogs SET status = 'merged' WHERE dog_id = ?", (merge_id,))
 
 
 
 
 
182
  self.conn.commit()
183
  return True
184
  except Exception as e:
 
189
  def delete_dog(self, dog_id: int, hard_delete: bool = False):
190
  """Delete or mark dog as deleted"""
191
  if hard_delete:
 
192
  self.cursor.execute("DELETE FROM dog_features WHERE dog_id = ?", (dog_id,))
193
  self.cursor.execute("DELETE FROM dog_images WHERE dog_id = ?", (dog_id,))
194
  self.cursor.execute("DELETE FROM sightings WHERE dog_id = ?", (dog_id,))
195
  self.cursor.execute("DELETE FROM dogs WHERE dog_id = ?", (dog_id,))
196
  else:
197
+ self.cursor.execute("UPDATE dogs SET status = 'deleted' WHERE dog_id = ?", (dog_id,))
 
 
 
 
198
  self.conn.commit()
199
 
200
  # ========== Features Management ==========
 
201
  def save_features(self, dog_id: int, resnet_features: np.ndarray,
202
  color_histogram: np.ndarray, confidence: float):
203
  """Save dog features to database"""
204
  resnet_blob = pickle.dumps(resnet_features)
205
  color_blob = pickle.dumps(color_histogram)
 
206
  self.cursor.execute("""
207
  INSERT INTO dog_features
208
  (dog_id, resnet_features, color_histogram, confidence)
209
  VALUES (?, ?, ?, ?)
210
  """, (dog_id, resnet_blob, color_blob, confidence))
 
211
  self.conn.commit()
212
 
213
  def get_features(self, dog_id: int, limit: int = 20) -> List[Dict]:
 
218
  ORDER BY timestamp DESC
219
  LIMIT ?
220
  """, (dog_id, limit))
 
221
  features = []
222
  for row in self.cursor.fetchall():
223
  features.append({
 
226
  'confidence': row['confidence'],
227
  'timestamp': row['timestamp']
228
  })
 
229
  return features
230
 
231
  # ========== Images Management ==========
 
232
  def save_image(self, dog_id: int, image: np.ndarray,
233
  frame_number: int, video_source: str,
234
  bbox: List[float], confidence: float,
235
  pose_keypoints: Optional[List] = None):
236
  """Save dog image to database"""
 
237
  _, buffer = cv2.imencode('.jpg', image)
238
  image_data = base64.b64encode(buffer).decode('utf-8')
239
 
 
240
  thumbnail = cv2.resize(image, (128, 128))
241
  _, thumb_buffer = cv2.imencode('.jpg', thumbnail, [cv2.IMWRITE_JPEG_QUALITY, 70])
242
  thumb_data = base64.b64encode(thumb_buffer).decode('utf-8')
243
 
244
  h, w = image.shape[:2]
 
245
  self.cursor.execute("""
246
  INSERT INTO dog_images
247
  (dog_id, image_data, thumbnail, width, height,
 
250
  """, (dog_id, image_data, thumb_data, w, h,
251
  frame_number, video_source, json.dumps(bbox),
252
  confidence, json.dumps(pose_keypoints) if pose_keypoints else None))
 
253
  self.conn.commit()
254
+ return self.cursor.lastrowid
255
 
256
  def get_dog_images(self, dog_id: int, validated_only: bool = False,
257
  include_discarded: bool = False) -> List[Dict]:
258
  """Get all images for a dog"""
259
  query = "SELECT * FROM dog_images WHERE dog_id = ?"
260
  params = [dog_id]
 
261
  if validated_only:
262
  query += " AND is_validated = 1"
263
  if not include_discarded:
264
  query += " AND is_discarded = 0"
 
265
  query += " ORDER BY timestamp DESC"
266
 
267
  self.cursor.execute(query, params)
 
269
 
270
  images = []
271
  for row in rows:
 
272
  image_bytes = base64.b64decode(row['image_data'])
273
  nparr = np.frombuffer(image_bytes, np.uint8)
274
  image = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
 
275
  images.append({
276
  'image_id': row['image_id'],
277
  'image': image,
 
284
  'is_discarded': row['is_discarded'],
285
  'pose_keypoints': json.loads(row['pose_keypoints']) if row['pose_keypoints'] else None
286
  })
 
287
  return images
 
288
 
289
  def validate_image(self, image_id: int, is_valid: bool = True):
290
  """Mark image as validated or discarded"""
291
  if is_valid:
292
+ self.cursor.execute("UPDATE dog_images SET is_validated = 1 WHERE image_id = ?", (image_id,))
 
 
 
293
  else:
294
+ self.cursor.execute("UPDATE dog_images SET is_discarded = 1 WHERE image_id = ?", (image_id,))
 
 
 
295
  self.conn.commit()
296
 
297
  # ========== Body Parts Management ==========
 
298
  def save_body_parts(self, dog_id: int, image_id: int,
299
  head_crop: Optional[np.ndarray],
300
  torso_crop: Optional[np.ndarray],
 
306
  'torso': torso_crop,
307
  'rear': rear_crop
308
  }
 
309
  for part_type, crop in parts.items():
310
  if crop is not None:
 
311
  _, buffer = cv2.imencode('.jpg', crop)
312
  crop_data = base64.b64encode(buffer).decode('utf-8')
 
313
  confidence = confidences.get(part_type, 0.0)
 
314
  self.cursor.execute("""
315
  INSERT INTO body_parts
316
  (dog_id, image_id, part_type, part_image, confidence)
317
  VALUES (?, ?, ?, ?, ?)
318
  """, (dog_id, image_id, part_type, crop_data, confidence))
 
319
  self.conn.commit()
320
 
321
  def get_body_parts(self, dog_id: int, part_type: Optional[str] = None,
 
323
  """Get body part crops for a dog"""
324
  query = "SELECT * FROM body_parts WHERE dog_id = ?"
325
  params = [dog_id]
 
326
  if part_type:
327
  query += " AND part_type = ?"
328
  params.append(part_type)
 
329
  if validated_only:
330
  query += " AND is_validated = 1"
 
331
  if not include_discarded:
332
  query += " AND is_discarded = 0"
 
333
  self.cursor.execute(query, params)
334
 
335
  parts = []
336
  for row in self.cursor.fetchall():
 
337
  image_bytes = base64.b64decode(row['part_image'])
338
  nparr = np.frombuffer(image_bytes, np.uint8)
339
  image = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
 
340
  parts.append({
341
  'part_id': row['part_id'],
342
  'part_type': row['part_type'],
 
345
  'is_validated': row['is_validated'],
346
  'image_id': row['image_id']
347
  })
 
348
  return parts
349
 
350
  def validate_body_part(self, part_id: int, is_valid: bool = True):
351
  """Mark body part as validated or discarded"""
352
  if is_valid:
353
+ self.cursor.execute("UPDATE body_parts SET is_validated = 1 WHERE part_id = ?", (part_id,))
 
 
 
354
  else:
355
+ self.cursor.execute("UPDATE body_parts SET is_discarded = 1 WHERE part_id = ?", (part_id,))
 
 
 
356
  self.conn.commit()
357
 
358
  def add_sighting(self, dog_id: int, position: Tuple[float, float],
 
363
  (dog_id, position_x, position_y, video_source, frame_number, confidence)
364
  VALUES (?, ?, ?, ?, ?, ?)
365
  """, (dog_id, position[0], position[1], video_source, frame_number, confidence))
 
366
  self.conn.commit()
367
 
368
  # ========== Query Methods ==========
 
369
  def get_all_dogs(self, active_only: bool = True) -> pd.DataFrame:
370
  """Get all dogs as DataFrame"""
371
  query = "SELECT * FROM dogs"
372
  if active_only:
373
  query += " WHERE status = 'active'"
374
  query += " ORDER BY dog_id"
 
375
  return pd.read_sql_query(query, self.conn)
376
 
377
  def get_dog_statistics(self) -> Dict:
378
  """Get overall statistics"""
379
  stats = {}
 
 
380
  self.cursor.execute("SELECT COUNT(*) FROM dogs WHERE status = 'active'")
381
  stats['total_active_dogs'] = self.cursor.fetchone()[0]
382
 
 
383
  self.cursor.execute("SELECT COUNT(*) FROM dog_images WHERE is_discarded = 0")
384
  stats['total_images'] = self.cursor.fetchone()[0]
385
 
 
386
  self.cursor.execute("SELECT COUNT(*) FROM dog_images WHERE is_validated = 1")
387
  stats['validated_images'] = self.cursor.fetchone()[0]
388
 
 
389
  self.cursor.execute("SELECT COUNT(*) FROM sightings")
390
  stats['total_sightings'] = self.cursor.fetchone()[0]
391
 
 
392
  self.cursor.execute("""
393
  SELECT d.dog_id, d.name, d.total_sightings
394
  FROM dogs d
 
403
  'name': row[1] or f"Dog #{row[0]}",
404
  'sightings': row[2]
405
  }
 
406
  return stats
407
 
408
  # ========== Export Methods ==========
 
409
  def export_training_dataset(self, output_dir: str, validated_only: bool = True) -> Dict:
410
  """Export dataset with body parts for fine-tuning"""
411
  output_path = Path(output_dir)
412
  output_path.mkdir(parents=True, exist_ok=True)
413
 
 
414
  images_dir = output_path / "images"
415
  images_dir.mkdir(exist_ok=True)
416
 
 
417
  dataset = []
 
418
  dogs = self.get_all_dogs()
419
  for _, dog in dogs.iterrows():
420
  dog_id = dog['dog_id']
 
 
421
  dog_dir = images_dir / f"dog_{dog_id}"
422
  dog_dir.mkdir(exist_ok=True)
423
 
 
424
  for part in ['full', 'head', 'torso', 'rear']:
425
+ (dog_dir / part).mkdir(exist_ok=True)
 
426
 
 
427
  images = self.get_dog_images(dog_id, validated_only=validated_only)
 
428
  for idx, img_data in enumerate(images):
 
429
  full_path = dog_dir / 'full' / f"img_{idx:04d}.jpg"
430
  cv2.imwrite(str(full_path), img_data['image'])
431
 
 
432
  parts = self.get_body_parts(dog_id, validated_only=validated_only)
 
433
  part_paths = {}
434
  for part_data in parts:
435
  if part_data['image_id'] == img_data['image_id']:
 
438
  cv2.imwrite(str(part_path), part_data['image'])
439
  part_paths[part_type] = str(part_path.relative_to(output_path))
440
 
 
441
  dataset_entry = {
442
  'dog_id': dog_id,
443
  'full_image': str(full_path.relative_to(output_path)),
444
  'bbox': img_data['bbox'],
445
  'confidence': img_data['confidence']
446
  }
 
 
447
  for part_type in ['head', 'torso', 'rear']:
448
  dataset_entry[f'{part_type}_image'] = part_paths.get(part_type, None)
 
449
  dataset.append(dataset_entry)
450
 
 
451
  dataset_df = pd.DataFrame(dataset)
452
  dataset_df.to_csv(output_path / "dataset.csv", index=False)
453
 
 
454
  metadata = {
455
  'total_dogs': len(dogs),
456
  'total_images': len(dataset),
 
458
  'validated_only': validated_only,
459
  'includes_body_parts': True
460
  }
 
461
  with open(output_path / "metadata.json", 'w') as f:
462
  json.dump(metadata, f, indent=2)
463
 
 
464
  from sklearn.model_selection import train_test_split
465
+ train_df, test_df = train_test_split(dataset_df, test_size=0.2, stratify=dataset_df['dog_id'])
 
 
466
  train_df.to_csv(output_path / "train.csv", index=False)
467
  test_df.to_csv(output_path / "test.csv", index=False)
468
 
469
  metadata['train_samples'] = len(train_df)
470
  metadata['test_samples'] = len(test_df)
 
471
  return metadata
472
 
473
  # ========== Cleanup Methods ==========
 
474
  def reset_database(self, confirm: bool = False):
475
  """Reset entire database"""
476
  if not confirm:
477
  return False
 
478
  tables = ['sightings', 'dog_images', 'dog_features', 'dogs', 'sessions']
479
  for table in tables:
480
  self.cursor.execute(f"DELETE FROM {table}")
 
 
481
  self.cursor.execute("DELETE FROM sqlite_sequence")
 
482
  self.conn.commit()
483
  return True
484
 
 
493
  def __del__(self):
494
  """Ensure connection is closed"""
495
  if hasattr(self, 'conn'):
496
+ self.conn.close()
497
+
498
+ # ========== Health Methods (NEW) ==========
499
+ def save_health_assessment(self, dog_id: int, health_score: float, status: str,
500
+ posture_score: float = None, gait_score: float = None,
501
+ body_condition_score: float = None, activity_score: float = None,
502
+ alerts: List[str] = None, recommendations: List[str] = None,
503
+ confidence: float = 0.5, video_source: str = None,
504
+ frame_number: int = None):
505
+ """Save health assessment to database"""
506
+ self.cursor.execute("""
507
+ INSERT INTO health_assessments
508
+ (dog_id, health_score, status, posture_score, gait_score,
509
+ body_condition_score, activity_score, alerts, recommendations,
510
+ confidence, video_source, frame_number)
511
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
512
+ """, (dog_id, health_score, status, posture_score, gait_score,
513
+ body_condition_score, activity_score,
514
+ json.dumps(alerts) if alerts else None,
515
+ json.dumps(recommendations) if recommendations else None,
516
+ confidence, video_source, frame_number))
517
+
518
+ self.cursor.execute("""
519
+ UPDATE dogs
520
+ SET last_health_score = ?,
521
+ health_status = ?
522
+ WHERE dog_id = ?
523
+ """, (health_score, status, dog_id))
524
+ self.conn.commit()
525
+
526
+ def get_health_history(self, dog_id: int, limit: int = 50) -> List[Dict]:
527
+ """Get health assessment history for a dog"""
528
+ self.cursor.execute("""
529
+ SELECT * FROM health_assessments
530
+ WHERE dog_id = ?
531
+ ORDER BY timestamp DESC
532
+ LIMIT ?
533
+ """, (dog_id, limit))
534
+ assessments = []
535
+ for row in self.cursor.fetchall():
536
+ assessments.append({
537
+ 'timestamp': row['timestamp'],
538
+ 'health_score': row['health_score'],
539
+ 'status': row['status'],
540
+ 'posture_score': row['posture_score'],
541
+ 'gait_score': row['gait_score'],
542
+ 'body_condition_score': row['body_condition_score'],
543
+ 'activity_score': row['activity_score'],
544
+ 'alerts': json.loads(row['alerts']) if row['alerts'] else [],
545
+ 'recommendations': json.loads(row['recommendations']) if row['recommendations'] else [],
546
+ 'confidence': row['confidence']
547
+ })
548
+ return assessments
549
+
550
+ def get_health_statistics(self) -> Dict:
551
+ """Get overall health statistics"""
552
+ stats = {}
553
+ self.cursor.execute("""
554
+ SELECT AVG(last_health_score) as avg_health,
555
+ COUNT(CASE WHEN last_health_score >= 8 THEN 1 END) as healthy_count,
556
+ COUNT(CASE WHEN last_health_score < 6 THEN 1 END) as unhealthy_count,
557
+ COUNT(*) as total_dogs
558
+ FROM dogs
559
+ WHERE status = 'active'
560
+ """)
561
+ row = self.cursor.fetchone()
562
+ if row:
563
+ stats['average_health'] = round(row['avg_health'] or 5.0, 1)
564
+ stats['healthy_dogs'] = row['healthy_count'] or 0
565
+ stats['unhealthy_dogs'] = row['unhealthy_count'] or 0
566
+ stats['total_dogs'] = row['total_dogs'] or 0
567
+
568
+ self.cursor.execute("""
569
+ SELECT dog_id, name, last_health_score, health_status
570
+ FROM dogs
571
+ WHERE status = 'active' AND last_health_score < 6
572
+ ORDER BY last_health_score ASC
573
+ LIMIT 10
574
+ """)
575
+ stats['dogs_needing_attention'] = []
576
+ for row in self.cursor.fetchall():
577
+ stats['dogs_needing_attention'].append({
578
+ 'dog_id': row['dog_id'],
579
+ 'name': row['name'] or f"Dog #{row['dog_id']}",
580
+ 'health_score': row['last_health_score'],
581
+ 'status': row['health_status']
582
+ })
583
+ return stats
584
+
585
+ def save_pose_keypoints(self, dog_id: int, keypoints: np.ndarray,
586
+ frame_number: int, video_source: str):
587
+ """Save pose keypoints for a dog"""
588
+ keypoints_json = json.dumps(keypoints.tolist()) if keypoints is not None else None
589
+ self.cursor.execute("""
590
+ UPDATE sightings
591
+ SET pose_keypoints = ?
592
+ WHERE dog_id = ? AND frame_number = ? AND video_source = ?
593
+ """, (keypoints_json, dog_id, frame_number, video_source))
594
+ self.conn.commit()