mustafa2ak commited on
Commit
14e8154
·
verified ·
1 Parent(s): e96db4b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +309 -559
app.py CHANGED
@@ -1,6 +1,6 @@
1
  """
2
- resnet_dataset_creator.py - Focused Dataset Creation Tool for ResNet Fine-tuning
3
- Streamlined workflow for creating clean dog ReID training datasets
4
  """
5
  import gradio as gr
6
  import cv2
@@ -14,13 +14,14 @@ from typing import List, Dict, Optional, Tuple
14
  from datetime import datetime
15
  from PIL import Image
16
  import zipfile
 
17
  # Import required modules
18
  from detection import DogDetector
19
  from tracking import SimpleTracker
20
- from reid import SingleModelReID
21
  from ultralytics import YOLO
22
 
23
- # ========== IMAGE QUALITY ANALYZER ==========
24
  class ImageQualityAnalyzer:
25
  """Analyze and score image quality for dataset selection"""
26
 
@@ -34,43 +35,34 @@ class ImageQualityAnalyzer:
34
  }
35
 
36
  def calculate_sharpness(self, image: np.ndarray) -> float:
37
- """Calculate image sharpness using Laplacian variance"""
38
  gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
39
  laplacian = cv2.Laplacian(gray, cv2.CV_64F)
40
  return min(100, laplacian.var())
41
 
42
  def calculate_resolution_score(self, image: np.ndarray) -> float:
43
- """Score based on image resolution"""
44
  h, w = image.shape[:2]
45
  pixels = h * w
46
- # Ideal is 224x224 or larger
47
  ideal_pixels = 224 * 224
48
  return min(100, (pixels / ideal_pixels) * 100)
49
 
50
  def calculate_brightness_score(self, image: np.ndarray) -> float:
51
- """Score image brightness"""
52
  gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
53
  mean_brightness = np.mean(gray)
54
- # Ideal brightness is around 127 (middle of 0-255)
55
  return 100 - abs(mean_brightness - 127) * 0.78
56
 
57
  def calculate_contrast_score(self, image: np.ndarray) -> float:
58
- """Score image contrast"""
59
  gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
60
  contrast = gray.std()
61
  return min(100, contrast * 2)
62
 
63
  def detect_occlusion(self, bbox: List[float], frame_shape: Tuple) -> float:
64
- """Check if dog is fully visible"""
65
  x1, y1, x2, y2 = bbox
66
  h, w = frame_shape[:2]
67
 
68
- # Check if bbox touches frame edges
69
  edge_penalty = 0
70
  if x1 <= 5 or y1 <= 5 or x2 >= w-5 or y2 >= h-5:
71
  edge_penalty = 30
72
 
73
- # Check bbox aspect ratio (dogs shouldn't be too thin)
74
  aspect = (x2 - x1) / (y2 - y1)
75
  if aspect < 0.3 or aspect > 3:
76
  edge_penalty += 20
@@ -79,7 +71,6 @@ class ImageQualityAnalyzer:
79
 
80
  def calculate_overall_quality(self, image: np.ndarray, bbox: List[float],
81
  frame_shape: Tuple) -> float:
82
- """Calculate comprehensive quality score"""
83
  scores = {
84
  'sharpness': self.calculate_sharpness(image),
85
  'resolution': self.calculate_resolution_score(image),
@@ -88,28 +79,19 @@ class ImageQualityAnalyzer:
88
  'occlusion': self.detect_occlusion(bbox, frame_shape)
89
  }
90
 
91
- # Weighted average
92
  total = sum(scores[k] * self.quality_weights[k] for k in scores)
93
  return total
94
 
95
- # ========== SMART IMAGE SELECTOR ==========
96
  class SmartImageSelector:
97
  """Intelligently select best images based on quality and diversity"""
98
 
99
  def __init__(self):
100
  self.quality_analyzer = ImageQualityAnalyzer()
101
- self.min_temporal_distance = 10 # Frames between selected images
102
 
103
  def select_best_images(self, dog_data: List[Dict], max_images: int = 30,
104
  video_fps: float = 30) -> List[Dict]:
105
- """
106
- Select best images considering:
107
- - Image quality
108
- - Temporal diversity (not too close in time)
109
- - Pose diversity
110
- - Movement patterns
111
- """
112
- # Always calculate quality scores first
113
  for item in dog_data:
114
  item['quality_score'] = self.quality_analyzer.calculate_overall_quality(
115
  item['crop'], item['bbox'], item['frame'].shape
@@ -118,18 +100,15 @@ class SmartImageSelector:
118
  if len(dog_data) <= max_images:
119
  return dog_data
120
 
121
- # Sort by quality
122
  dog_data.sort(key=lambda x: x['quality_score'], reverse=True)
123
 
124
  selected = []
125
  selected_frames = set()
126
- selected_indices = set() # Track indices instead of comparing items
127
 
128
  for idx, item in enumerate(dog_data):
129
- # Check temporal diversity
130
  frame_num = item['frame_num']
131
 
132
- # Don't select images too close together
133
  too_close = any(
134
  abs(frame_num - f) < self.min_temporal_distance
135
  for f in selected_frames
@@ -140,7 +119,6 @@ class SmartImageSelector:
140
  selected_frames.add(frame_num)
141
  selected_indices.add(idx)
142
 
143
- # If we don't have enough, relax temporal constraint
144
  if len(selected) < max_images:
145
  for idx, item in enumerate(dog_data):
146
  if idx not in selected_indices and len(selected) < max_images:
@@ -149,111 +127,20 @@ class SmartImageSelector:
149
 
150
  return selected[:max_images]
151
 
152
- # ========== ADVANCED HEAD EXTRACTOR ==========
153
- class AdvancedHeadExtractor:
154
- """Enhanced head extraction with multiple fallback strategies"""
155
-
156
- def __init__(self):
157
- self.pose_model = None
158
- try:
159
- self.pose_model = YOLO('yolov8m-pose.pt')
160
- if torch.cuda.is_available():
161
- self.pose_model.to('cuda')
162
- print("Pose model loaded for head extraction")
163
- except:
164
- print("Using geometric head extraction")
165
 
166
  def extract_head(self, frame: np.ndarray, bbox: List[float]) -> Optional[np.ndarray]:
167
- """Extract head with best available method"""
168
  x1, y1, x2, y2 = map(int, bbox)
169
  dog_crop = frame[y1:y2, x1:x2]
170
 
171
  if dog_crop.size == 0:
172
  return None
173
 
174
- # Try pose-based first
175
- if self.pose_model:
176
- head = self._extract_with_pose(dog_crop)
177
- if head is not None:
178
- return head
179
-
180
- # Fallback to intelligent geometric
181
- return self._extract_geometric_smart(dog_crop)
182
-
183
- def _extract_with_pose(self, dog_crop: np.ndarray) -> Optional[np.ndarray]:
184
- """Extract using pose keypoints"""
185
- try:
186
- results = self.pose_model(dog_crop, conf=0.3, verbose=False)
187
-
188
- if results and len(results) > 0 and hasattr(results[0], 'keypoints'):
189
- keypoints = results[0].keypoints
190
- if keypoints is not None and keypoints.xy is not None:
191
- kpts = keypoints.xy[0].cpu().numpy()
192
-
193
- # Get head keypoints (nose, eyes, ears)
194
- head_indices = [0, 1, 2, 3, 4] # nose, eyes, ears
195
- head_points = []
196
-
197
- for idx in head_indices:
198
- if idx < len(kpts) and kpts[idx][0] > 0:
199
- head_points.append(kpts[idx])
200
-
201
- if len(head_points) >= 3:
202
- head_points = np.array(head_points)
203
-
204
- # Calculate bounding box around head points
205
- padding = 35
206
- min_x = max(0, int(np.min(head_points[:, 0]) - padding))
207
- min_y = max(0, int(np.min(head_points[:, 1]) - padding))
208
- max_x = min(dog_crop.shape[1], int(np.max(head_points[:, 0]) + padding))
209
- max_y = min(dog_crop.shape[0], int(np.max(head_points[:, 1]) + padding * 1.3))
210
-
211
- head_crop = dog_crop[min_y:max_y, min_x:max_x]
212
- if head_crop.size > 0:
213
- # Resize to standard size
214
- head_crop = cv2.resize(head_crop, (128, 128))
215
- return head_crop
216
- except:
217
- pass
218
-
219
- return None
220
-
221
- def _extract_geometric_smart(self, dog_crop: np.ndarray) -> Optional[np.ndarray]:
222
- """Smart geometric extraction based on image analysis"""
223
  h, w = dog_crop.shape[:2]
224
 
225
- # Analyze image to find likely head position
226
- gray = cv2.cvtColor(dog_crop, cv2.COLOR_BGR2GRAY)
227
-
228
- # Use edge detection to find features
229
- edges = cv2.Canny(gray, 50, 150)
230
-
231
- # Find feature concentration (likely head area)
232
- kernel_size = max(1, h // 10)
233
- kernel = np.ones((kernel_size, kernel_size), np.float32)
234
- edge_density = cv2.filter2D(edges, -1, kernel)
235
-
236
- # Find peak density area
237
- max_loc = np.unravel_index(np.argmax(edge_density[:h//2, :]), edge_density[:h//2, :].shape)
238
-
239
- # Extract around peak area
240
- center_y = max_loc[0]
241
- center_x = max_loc[1]
242
-
243
- # Define head region
244
- head_size = int(min(h, w) * 0.4)
245
- y1 = max(0, center_y - head_size // 2)
246
- y2 = min(h, y1 + head_size)
247
- x1 = max(0, center_x - head_size // 2)
248
- x2 = min(w, x1 + head_size)
249
-
250
- head_crop = dog_crop[y1:y2, x1:x2]
251
-
252
- if head_crop.size > 0:
253
- head_crop = cv2.resize(head_crop, (128, 128))
254
- return head_crop
255
-
256
- # Final fallback - top portion
257
  head_height = int(h * 0.4)
258
  head_crop = dog_crop[:head_height, :]
259
 
@@ -263,94 +150,114 @@ class AdvancedHeadExtractor:
263
 
264
  return None
265
 
266
- # ========== MAIN DATASET CREATOR ==========
267
  class ResNetDatasetCreator:
268
- """Main application for creating ResNet fine-tuning datasets"""
269
 
270
  def __init__(self):
 
271
  self.temp_dir = Path("temp_dataset")
272
  self.final_dir = Path("resnet_finetune_dataset")
273
  self.database_dir = Path("permanent_database")
274
 
275
- # Components
276
  self.detector = DogDetector(device='cuda' if torch.cuda.is_available() else 'cpu')
277
  self.tracker = SimpleTracker()
278
  self.reid = SingleModelReID(device='cuda' if torch.cuda.is_available() else 'cpu')
279
- self.head_extractor = AdvancedHeadExtractor()
280
  self.image_selector = SmartImageSelector()
281
 
282
- # Session data
 
283
  self.current_session = None
284
- self.processed_dogs = {}
 
285
 
286
  # Create directories
287
  self.temp_dir.mkdir(exist_ok=True)
288
  self.final_dir.mkdir(exist_ok=True)
289
  self.database_dir.mkdir(exist_ok=True)
290
 
291
- # Load existing database if exists
292
- self.load_database()
293
 
294
- def load_database(self):
295
- """Load existing permanent database"""
296
  db_file = self.database_dir / "database.json"
297
  if db_file.exists():
298
  with open(db_file, 'r') as f:
299
  data = json.load(f)
300
- self.processed_dogs = {int(k): v for k, v in data.get('dogs', {}).items()}
301
- print(f"Loaded {len(self.processed_dogs)} dogs from database")
302
 
303
- def save_to_database(self):
304
- """Save current processed dogs to permanent database"""
 
 
 
 
305
  db_file = self.database_dir / "database.json"
306
  data = {
307
- 'dogs': {str(k): v for k, v in self.processed_dogs.items()},
308
  'last_updated': datetime.now().isoformat()
309
  }
310
  with open(db_file, 'w') as f:
311
  json.dump(data, f, indent=2)
312
 
313
- # Also save images to permanent location
314
- for dog_id in self.processed_dogs:
315
  src_dir = self.temp_dir / f"dog_{dog_id:03d}"
316
  dst_dir = self.database_dir / f"dog_{dog_id:03d}"
317
  if src_dir.exists():
318
  if dst_dir.exists():
319
  shutil.rmtree(dst_dir)
320
  shutil.copytree(src_dir, dst_dir)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
321
 
322
- def clear_database(self):
323
- """Clear all permanent database"""
324
  if self.database_dir.exists():
325
  shutil.rmtree(self.database_dir)
326
- self.database_dir.mkdir(exist_ok=True)
327
- self.processed_dogs = {}
328
- self.current_session = None
329
- print("Database cleared")
330
 
331
  def process_video(self, video_path: str, reid_threshold: float,
332
  max_images_per_dog: int, sample_rate: int) -> Dict:
333
- """
334
- Process video and extract initial dataset
335
- Args:
336
- video_path: Path to video file
337
- reid_threshold: ReID similarity threshold (0.40-0.85 recommended)
338
- max_images_per_dog: Maximum images to extract per dog
339
- sample_rate: Process every Nth frame
340
- """
341
- # Clear temp directory for new processing
342
- if self.temp_dir.exists():
343
- shutil.rmtree(self.temp_dir)
344
- self.temp_dir.mkdir()
345
 
346
  # Set ReID threshold
347
  self.reid.set_all_thresholds(reid_threshold)
348
 
349
- # Reset ReID session
350
- self.reid.reset_all()
351
-
352
  # Storage for dog data
353
- dog_data = {} # dog_id -> list of frame data
354
 
355
  # Open video
356
  cap = cv2.VideoCapture(video_path)
@@ -380,7 +287,7 @@ class ResNetDatasetCreator:
380
  dog_id = results['ResNet50']['dog_id']
381
  confidence = results['ResNet50']['confidence']
382
 
383
- if dog_id > 0 and confidence > 0.3: # Lower threshold for detection
384
  # Get best detection
385
  detection = None
386
  for det in reversed(track.detections):
@@ -389,11 +296,9 @@ class ResNetDatasetCreator:
389
  break
390
 
391
  if detection:
392
- # Initialize storage
393
  if dog_id not in dog_data:
394
  dog_data[dog_id] = []
395
 
396
- # Store frame data
397
  dog_data[dog_id].append({
398
  'frame': frame.copy(),
399
  'crop': detection.image_crop,
@@ -420,12 +325,11 @@ class ResNetDatasetCreator:
420
  new_dogs = {}
421
 
422
  for dog_id, images in dog_data.items():
423
- # Use smart selector
424
  selected = self.image_selector.select_best_images(
425
  images, max_images_per_dog, fps
426
  )
427
 
428
- # Save to temp directory
429
  dog_dir = self.temp_dir / f"dog_{dog_id:03d}"
430
  dog_dir.mkdir(exist_ok=True)
431
  (dog_dir / 'full').mkdir(exist_ok=True)
@@ -447,15 +351,15 @@ class ResNetDatasetCreator:
447
 
448
  total_images += saved_count
449
 
450
- # Store metadata
451
  new_dogs[dog_id] = {
452
  'num_images': saved_count,
453
  'avg_confidence': np.mean([d['reid_confidence'] for d in selected]),
454
  'quality_scores': [d['quality_score'] for d in selected]
455
  }
456
 
457
- # Update processed dogs (append, don't replace)
458
- self.processed_dogs.update(new_dogs)
459
 
460
  # Save session info
461
  self.current_session = {
@@ -467,18 +371,18 @@ class ResNetDatasetCreator:
467
  'dogs': {str(k): v for k, v in new_dogs.items()}
468
  }
469
 
470
- # Save metadata
471
  with open(self.temp_dir / 'session.json', 'w') as f:
472
  json.dump(self.current_session, f, indent=2)
473
 
474
  yield {'status': 'complete', 'session': self.current_session}
475
 
476
- def get_dog_images(self, dog_id: int) -> List:
477
- """Get images for verification interface"""
478
- # Try temp dir first, then database dir
479
- dog_dir = self.temp_dir / f"dog_{dog_id:03d}"
480
- if not dog_dir.exists():
481
  dog_dir = self.database_dir / f"dog_{dog_id:03d}"
 
 
482
 
483
  full_dir = dog_dir / 'full'
484
  if not full_dir.exists():
@@ -493,69 +397,60 @@ class ResNetDatasetCreator:
493
 
494
  return images
495
 
496
- def remove_images(self, dog_id: int, image_indices: List[int]):
497
- """Remove specific images from a dog folder"""
498
- # Handle both temp and database directories
499
- for base_dir in [self.temp_dir, self.database_dir]:
500
- dog_dir = base_dir / f"dog_{dog_id:03d}"
501
- if not dog_dir.exists():
502
- continue
503
-
504
- full_dir = dog_dir / 'full'
505
- head_dir = dog_dir / 'head'
506
-
507
- image_files = sorted(list(full_dir.glob("*.jpg")))
508
-
509
- # Extract actual indices from gallery selection
510
- indices_to_remove = []
511
- if isinstance(image_indices, list):
512
- for item in image_indices:
513
- if isinstance(item, (list, tuple)) and len(item) > 0:
514
- indices_to_remove.append(item[0])
515
- elif isinstance(item, int):
516
- indices_to_remove.append(item)
517
-
518
- for idx in indices_to_remove:
519
- if 0 <= idx < len(image_files):
520
- # Remove full image
521
- image_files[idx].unlink(missing_ok=True)
522
- # Remove corresponding head
523
- head_file = head_dir / image_files[idx].name
524
- if head_file.exists():
525
- head_file.unlink()
526
-
527
- def delete_dog(self, dog_id: int):
528
- """Delete entire dog folder from both temp and database"""
529
- for base_dir in [self.temp_dir, self.database_dir]:
530
- dog_dir = base_dir / f"dog_{dog_id:03d}"
531
- if dog_dir.exists():
532
- shutil.rmtree(dog_dir)
533
 
534
- # Remove from processed dogs
535
- if dog_id in self.processed_dogs:
536
- del self.processed_dogs[dog_id]
 
 
 
 
 
 
 
 
 
 
 
537
 
538
- def save_final_dataset(self, format_type: str = 'folder') -> str:
539
- """
540
- Save verified dataset in format suitable for ResNet fine-tuning
 
 
 
 
 
 
 
541
 
542
- Args:
543
- format_type: 'folder' for folder structure, 'csv' for CSV metadata
544
- """
545
- # Clear final directory
 
546
  if self.final_dir.exists():
547
  shutil.rmtree(self.final_dir)
548
  self.final_dir.mkdir()
549
 
550
- # Copy all dogs from both temp and database
551
  all_dog_dirs = []
552
 
553
- # Get dogs from temp
554
  for d in self.temp_dir.iterdir():
555
  if d.is_dir() and d.name.startswith('dog_'):
556
  all_dog_dirs.append(d)
557
 
558
- # Get dogs from database (if not already in temp)
559
  temp_dogs = {d.name for d in all_dog_dirs}
560
  for d in self.database_dir.iterdir():
561
  if d.is_dir() and d.name.startswith('dog_') and d.name not in temp_dogs:
@@ -568,50 +463,36 @@ class ResNetDatasetCreator:
568
  if not (dog_dir / 'full').exists():
569
  continue
570
 
571
- # Create final directory
572
  final_dog_dir = self.final_dir / f"dog_{final_id:03d}"
573
  shutil.copytree(dog_dir, final_dog_dir)
574
 
575
- # Collect metadata
576
  for img_path in (final_dog_dir / 'full').glob("*.jpg"):
577
  head_path = final_dog_dir / 'head' / img_path.name
578
  data_entries.append({
579
  'dog_id': final_id,
580
  'image_path': str(img_path.relative_to(self.final_dir)),
581
  'head_path': str(head_path.relative_to(self.final_dir)) if head_path.exists() else None,
582
- 'class': final_id # For classification-style training
583
  })
584
 
585
  final_id += 1
586
 
587
- if format_type == 'csv' or format_type == 'both':
588
- # Create train/val split
589
  df = pd.DataFrame(data_entries)
590
 
591
- if len(df) > 0:
592
- # Stratified split by dog_id
593
  from sklearn.model_selection import train_test_split
594
-
595
- # Only split if we have enough samples
596
- if len(df) > 5:
597
- train_df, val_df = train_test_split(
598
- df, test_size=0.2, stratify=df['dog_id'], random_state=42
599
- )
600
- else:
601
- train_df = df
602
- val_df = pd.DataFrame()
603
-
604
- # Save CSV files
605
  train_df.to_csv(self.final_dir / 'train.csv', index=False)
606
- if len(val_df) > 0:
607
- val_df.to_csv(self.final_dir / 'val.csv', index=False)
 
608
 
609
- # Create metadata
610
  metadata = {
611
  'total_dogs': final_id - 1,
612
  'total_images': len(data_entries),
613
- 'train_images': len(train_df) if format_type in ['csv', 'both'] and 'train_df' in locals() else len(data_entries),
614
- 'val_images': len(val_df) if format_type in ['csv', 'both'] and 'val_df' in locals() else 0,
615
  'format': format_type,
616
  'created': datetime.now().isoformat()
617
  }
@@ -619,7 +500,7 @@ class ResNetDatasetCreator:
619
  with open(self.final_dir / 'metadata.json', 'w') as f:
620
  json.dump(metadata, f, indent=2)
621
 
622
- # Create zip file
623
  zip_path = self.final_dir.parent / f"resnet_dataset_{datetime.now().strftime('%Y%m%d_%H%M%S')}.zip"
624
  with zipfile.ZipFile(zip_path, 'w') as zipf:
625
  for file_path in self.final_dir.rglob('*'):
@@ -628,29 +509,30 @@ class ResNetDatasetCreator:
628
  return str(zip_path)
629
 
630
  def create_interface(self):
631
- """Create Gradio interface"""
632
  with gr.Blocks(
633
  title="ResNet Fine-tuning Dataset Creator",
634
  theme=gr.themes.Soft()
635
  ) as app:
636
  gr.Markdown("""
637
- # 🎯 ResNet Fine-tuning Dataset Creator
638
- ### Three-step process: Process Verify Export
639
  """)
640
 
641
- # State to store processing results
642
  processing_state = gr.State(None)
 
643
 
644
- # Step 1: Process Video
645
  with gr.Tabs() as tabs:
 
646
  with gr.Tab("📹 Step 1: Process Video", id=0):
647
  with gr.Row():
648
  video_input = gr.Video(label="Upload Video")
649
  with gr.Column():
650
  reid_threshold = gr.Slider(
651
- 0.40, 0.85, 0.40, step=0.01,
652
  label="ReID Threshold",
653
- info="Lower = More lenient matching (0.40 recommended for start)"
654
  )
655
  max_images = gr.Slider(
656
  10, 50, 30, step=5,
@@ -659,353 +541,248 @@ class ResNetDatasetCreator:
659
  sample_rate = gr.Slider(
660
  1, 5, 2, step=1,
661
  label="Sample Rate",
662
- info="Process every Nth frame (2 = every other frame)"
663
  )
664
 
665
  process_btn = gr.Button("🚀 Process Video", variant="primary", size="lg")
666
 
667
- # Results display in formatted table
668
  with gr.Column():
669
  progress_bar = gr.Textbox(label="Progress", interactive=False)
670
- results_display = gr.HTML(label="Processing Results", value="")
671
- save_status = gr.Textbox(label="Save Status", interactive=False, visible=False)
672
 
673
  with gr.Row():
674
- save_proceed_btn = gr.Button(
675
- "✅ Save Results & Proceed to Verification",
676
- variant="primary",
677
- size="lg",
678
- visible=False
679
- )
680
  clear_btn = gr.Button(
681
- "🔄 Clear Results",
682
  variant="secondary",
 
683
  visible=False
684
  )
685
 
686
- def format_results_table(session_data):
687
- """Format session data as HTML table"""
688
- if not session_data:
689
- return ""
690
-
691
- html = """
692
- <div style="padding: 20px; background-color: #f8f9fa; border-radius: 10px;">
693
- <h3 style="color: #2c3e50;">📊 Processing Results</h3>
694
- <table style="width: 100%; border-collapse: collapse; margin: 20px 0;">
695
- <tr style="background-color: #3498db; color: white;">
696
- <td style="padding: 10px; border: 1px solid #ddd;"><b>Metric</b></td>
697
- <td style="padding: 10px; border: 1px solid #ddd;"><b>Value</b></td>
698
- </tr>
699
- """
700
-
701
- # Basic info
702
- html += f"""
703
- <tr style="background-color: #ecf0f1;">
704
- <td style="padding: 10px; border: 1px solid #ddd;">Video File</td>
705
- <td style="padding: 10px; border: 1px solid #ddd;">{session_data['video'].split('/')[-1]}</td>
706
- </tr>
707
- <tr>
708
- <td style="padding: 10px; border: 1px solid #ddd;">Processing Time</td>
709
- <td style="padding: 10px; border: 1px solid #ddd;">{session_data['timestamp'].split('T')[1].split('.')[0]}</td>
710
- </tr>
711
- <tr style="background-color: #ecf0f1;">
712
- <td style="padding: 10px; border: 1px solid #ddd;">Number of Dogs Detected</td>
713
- <td style="padding: 10px; border: 1px solid #ddd;"><b>{session_data['num_dogs']}</b></td>
714
- </tr>
715
- <tr>
716
- <td style="padding: 10px; border: 1px solid #ddd;">Total Images Extracted</td>
717
- <td style="padding: 10px; border: 1px solid #ddd;"><b>{session_data['total_images']}</b></td>
718
- </tr>
719
- <tr style="background-color: #ecf0f1;">
720
- <td style="padding: 10px; border: 1px solid #ddd;">ReID Threshold Used</td>
721
- <td style="padding: 10px; border: 1px solid #ddd;">{session_data['reid_threshold']:.2f}</td>
722
- </tr>
723
- </table>
724
- """
725
-
726
- # Dog-specific details
727
- if session_data['dogs']:
728
- html += """
729
- <h4 style="color: #2c3e50; margin-top: 20px;">🐕 Dog Details</h4>
730
- <table style="width: 100%; border-collapse: collapse; margin: 10px 0;">
731
- <tr style="background-color: #27ae60; color: white;">
732
- <td style="padding: 10px; border: 1px solid #ddd;"><b>Dog ID</b></td>
733
- <td style="padding: 10px; border: 1px solid #ddd;"><b>Images</b></td>
734
- <td style="padding: 10px; border: 1px solid #ddd;"><b>Avg Confidence</b></td>
735
- <td style="padding: 10px; border: 1px solid #ddd;"><b>Avg Quality</b></td>
736
- <td style="padding: 10px; border: 1px solid #ddd;"><b>Quality Range</b></td>
737
- </tr>
738
- """
739
-
740
- for dog_id, dog_info in session_data['dogs'].items():
741
- avg_quality = np.mean(dog_info['quality_scores'])
742
- min_quality = min(dog_info['quality_scores'])
743
- max_quality = max(dog_info['quality_scores'])
744
-
745
- row_style = "background-color: #ecf0f1;" if int(dog_id) % 2 == 0 else ""
746
- html += f"""
747
- <tr style="{row_style}">
748
- <td style="padding: 10px; border: 1px solid #ddd;">Dog {dog_id}</td>
749
- <td style="padding: 10px; border: 1px solid #ddd;">{dog_info['num_images']}</td>
750
- <td style="padding: 10px; border: 1px solid #ddd;">{dog_info['avg_confidence']:.2%}</td>
751
- <td style="padding: 10px; border: 1px solid #ddd;">{avg_quality:.1f}</td>
752
- <td style="padding: 10px; border: 1px solid #ddd;">{min_quality:.1f} - {max_quality:.1f}</td>
753
- </tr>
754
- """
755
-
756
- html += "</table>"
757
-
758
- html += """
759
- <div style="margin-top: 20px; padding: 10px; background-color: #d4edda; border-radius: 5px;">
760
- <p style="margin: 0; color: #155724;">
761
- ✅ <b>Processing Complete!</b> Click "Save Results & Proceed" to continue to verification step.
762
- </p>
763
- </div>
764
- </div>
765
- """
766
-
767
- return html
768
-
769
  def process_wrapper(video, threshold, max_img, sample):
770
- """Process video and format results"""
771
  if not video:
772
- return None, "", "Please upload a video", gr.update(visible=False), gr.update(visible=False), gr.update(visible=False)
773
 
774
- # Process video
775
  for update in self.process_video(video, threshold, int(max_img), int(sample)):
776
  if 'progress' in update:
777
- yield None, "", update['status'], gr.update(visible=False), gr.update(visible=False), gr.update(visible=False)
778
  else:
779
- # Store session data
780
- self.current_session = update['session']
781
- # Format results as table
782
- formatted_table = format_results_table(update['session'])
783
- yield update['session'], formatted_table, "Complete! ✅", gr.update(visible=False), gr.update(visible=True), gr.update(visible=True)
784
-
785
- def save_and_proceed():
786
- """Save current results and notify user"""
787
- if self.current_session and self.processed_dogs:
788
- # Save to permanent database
789
- self.save_to_database()
790
-
791
- # Debug info
792
- dog_count = len(self.processed_dogs)
793
- img_count = sum(d.get('num_images', 0) for d in self.processed_dogs.values())
794
-
795
- message = f"""✅ Results saved successfully to database!
796
-
797
- 📊 Summary:
798
- - Total dogs in database: {dog_count}
799
- - Total images: {img_count}
800
- - Data location: {self.database_dir}
801
-
802
- You can now proceed to Step 2: Verify & Clean
803
- Click the 'Refresh List' button in Step 2 to load all dogs."""
804
-
805
- return message, gr.update(visible=True)
806
- return "❌ No results to save. Please process a video first.", gr.update(visible=False)
807
 
808
- def clear_results():
809
- """Clear current processing results (not database)"""
810
- self.current_session = None
811
- if self.temp_dir.exists():
812
- shutil.rmtree(self.temp_dir)
813
- self.temp_dir.mkdir()
814
- return None, "", "", gr.update(visible=False), gr.update(visible=False), gr.update(visible=False)
815
 
816
  process_btn.click(
817
  process_wrapper,
818
  inputs=[video_input, reid_threshold, max_images, sample_rate],
819
- outputs=[processing_state, results_display, progress_bar, save_status, save_proceed_btn, clear_btn]
820
- )
821
-
822
- save_proceed_btn.click(
823
- save_and_proceed,
824
- outputs=[save_status, save_status]
825
  )
826
 
827
  clear_btn.click(
828
- clear_results,
829
- outputs=[processing_state, results_display, progress_bar, save_status, save_proceed_btn, clear_btn]
830
  )
831
 
832
- # Step 2: Verify & Clean
833
  with gr.Tab("✅ Step 2: Verify & Clean", id=1):
834
- gr.Markdown("Review each dog and remove any mismatched images")
835
-
836
- with gr.Row():
837
- dog_selector = gr.Dropdown(
838
- label="Select Dog",
839
- choices=[],
840
- interactive=True
841
- )
842
 
843
- # Add diagnostic and management buttons
844
  with gr.Row():
845
- refresh_btn = gr.Button("🔄 Refresh List")
846
- diagnose_btn = gr.Button("🔍 Diagnose Data", variant="secondary")
847
- clear_db_btn = gr.Button("⚠️ Clear All Database", variant="stop")
848
-
849
- diagnostic_output = gr.Textbox(label="Diagnostic Info", visible=False)
 
 
 
 
 
 
 
850
 
851
  image_gallery = gr.Gallery(
852
- label="Dog Images (Click to select for removal)",
853
  show_label=True,
854
- elem_id="gallery",
855
  columns=4,
856
  rows=3,
857
  object_fit="contain",
858
  height="auto",
859
- type="numpy",
860
- interactive=False
861
  )
862
 
863
  with gr.Row():
864
- selected_images = gr.Textbox(
865
- label="Selected Image Indices (comma-separated)",
866
- placeholder="e.g., 0,2,5",
867
- interactive=True
868
  )
869
  remove_selected_btn = gr.Button("🗑 Remove Selected Images", variant="secondary")
870
  delete_dog_btn = gr.Button("❌ Delete Entire Dog", variant="stop")
871
 
 
 
 
 
 
 
 
 
 
 
 
872
  status_text = gr.Textbox(label="Status", interactive=False)
873
 
874
- def refresh_dogs():
875
- """Refresh the dog list from all available data"""
876
- # Load from database
877
- self.load_database()
878
-
879
- if not self.processed_dogs:
880
- return gr.update(choices=[], value=None)
 
 
 
881
 
882
- choices = [f"Dog {dog_id}" for dog_id in sorted(self.processed_dogs.keys())]
883
  if choices:
884
  return gr.update(choices=choices, value=choices[0])
885
  return gr.update(choices=[], value=None)
886
 
887
- def diagnose_data():
888
- """Show diagnostic information about saved data"""
889
- info = []
890
- info.append("=== DIAGNOSTIC INFORMATION ===\n")
891
-
892
- # Check session
893
- if self.current_session:
894
- info.append(f"✅ Session exists: {self.current_session['num_dogs']} dogs, {self.current_session['total_images']} images")
895
- else:
896
- info.append("❌ No current session data")
897
 
898
- # Check processed dogs
899
- if self.processed_dogs:
900
- info.append(f"✅ Processed dogs dict: {len(self.processed_dogs)} dogs")
901
- for dog_id, data in self.processed_dogs.items():
902
- info.append(f" - Dog {dog_id}: {data.get('num_images', 0)} images, conf={data.get('avg_confidence', 0):.2f}")
903
- else:
904
- info.append(" No processed dogs data")
 
 
 
 
 
 
905
 
906
- # Check temp directory
907
- if self.temp_dir.exists():
908
- info.append(f"✅ Temp directory exists: {self.temp_dir}")
909
- dog_dirs = list(self.temp_dir.glob("dog_*"))
910
- info.append(f" - Found {len(dog_dirs)} dog directories")
911
- for dog_dir in sorted(dog_dirs):
912
- if (dog_dir / 'full').exists():
913
- img_count = len(list((dog_dir / 'full').glob("*.jpg")))
914
- info.append(f" • {dog_dir.name}: {img_count} full images")
915
- else:
916
- info.append("❌ Temp directory not found")
917
 
918
- # Check database directory
919
- if self.database_dir.exists():
920
- info.append(f"✅ Database directory exists: {self.database_dir}")
921
- dog_dirs = list(self.database_dir.glob("dog_*"))
922
- info.append(f" - Found {len(dog_dirs)} dog directories")
923
- for dog_dir in sorted(dog_dirs):
924
- if (dog_dir / 'full').exists():
925
- img_count = len(list((dog_dir / 'full').glob("*.jpg")))
926
- info.append(f" • {dog_dir.name}: {img_count} full images")
927
- else:
928
- info.append("❌ Database directory not found")
929
 
930
- return "\n".join(info), gr.update(visible=True)
931
-
932
- def show_dog_images(dog_selection):
933
- """Display images for selected dog"""
934
- if not dog_selection:
935
- return []
936
 
937
- try:
938
- dog_id = int(dog_selection.split()[1])
939
- images = self.get_dog_images(dog_id)
940
- if not images:
941
- print(f"No images found for dog {dog_id}")
942
- return images
943
- except Exception as e:
944
- print(f"Error loading images: {e}")
945
- return []
946
-
947
- def remove_selected(dog_selection, indices_str):
948
- """Remove selected images based on text input"""
949
- if not dog_selection or not indices_str:
950
- return "No images selected", []
951
 
952
- try:
953
- # Parse comma-separated indices
954
- indices = [int(i.strip()) for i in indices_str.split(',')]
955
- dog_id = int(dog_selection.split()[1])
956
-
957
- self.remove_images(dog_id, indices)
958
-
959
- # Update database
960
- self.save_to_database()
961
-
962
- return f"Removed {len(indices)} images", self.get_dog_images(dog_id)
963
- except Exception as e:
964
- return f"Error: {str(e)}", []
965
 
966
- def delete_dog(dog_selection):
 
967
  if not dog_selection:
968
  return "No dog selected", []
969
 
970
  dog_id = int(dog_selection.split()[1])
971
- self.delete_dog(dog_id)
972
-
973
- # Update database
974
- self.save_to_database()
975
-
976
  return f"Deleted Dog {dog_id}", []
977
 
978
- def clear_all_database():
979
- """Clear entire database"""
980
- self.clear_database()
981
- return "Database cleared successfully", gr.update(choices=[], value=None), []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
982
 
983
- refresh_btn.click(refresh_dogs, outputs=dog_selector)
984
- diagnose_btn.click(diagnose_data, outputs=[diagnostic_output, diagnostic_output])
985
- dog_selector.change(show_dog_images, inputs=dog_selector, outputs=image_gallery)
 
 
 
 
 
 
 
 
 
986
  remove_selected_btn.click(
987
- remove_selected,
988
- inputs=[dog_selector, selected_images],
989
- outputs=[status_text, image_gallery]
 
 
 
 
 
 
 
 
 
 
 
 
990
  )
 
991
  delete_dog_btn.click(
992
- delete_dog,
993
- inputs=dog_selector,
994
  outputs=[status_text, image_gallery]
995
  )
996
- clear_db_btn.click(
997
- clear_all_database,
998
- outputs=[status_text, dog_selector, image_gallery]
 
 
 
 
 
 
999
  )
1000
 
1001
- # Step 3: Export Dataset
1002
  with gr.Tab("💾 Step 3: Export Dataset", id=2):
1003
  gr.Markdown("""
1004
- ### Export Options
1005
- Choose format for ResNet fine-tuning:
1006
- - **Folder Structure**: Organized folders with images
1007
- - **CSV Format**: Includes train/val split with paths
1008
- - **Both**: Folders + CSV metadata (recommended)
1009
  """)
1010
 
1011
  format_selector = gr.Radio(
@@ -1014,9 +791,7 @@ class ResNetDatasetCreator:
1014
  label="Export Format"
1015
  )
1016
 
1017
- with gr.Row():
1018
- export_btn = gr.Button("📦 Export Final Dataset", variant="primary", size="lg")
1019
- export_status = gr.Button("📊 Check Export Status", variant="secondary")
1020
 
1021
  export_output = gr.Textbox(label="Export Path", interactive=False)
1022
  download_file = gr.File(label="Download Dataset", interactive=False)
@@ -1026,19 +801,15 @@ class ResNetDatasetCreator:
1026
  try:
1027
  zip_path = self.save_final_dataset(format_type)
1028
 
1029
- # Get statistics
1030
  with open(self.final_dir / 'metadata.json', 'r') as f:
1031
  metadata = json.load(f)
1032
 
1033
  stats = f"""
1034
- ### ✅ Dataset Exported Successfully!
1035
 
1036
  - **Total Dogs**: {metadata['total_dogs']}
1037
  - **Total Images**: {metadata['total_images']}
1038
- - **Training Images**: {metadata.get('train_images', 'N/A')}
1039
- - **Validation Images**: {metadata.get('val_images', 'N/A')}
1040
 
1041
- Dataset is ready for ResNet fine-tuning!
1042
  Download the ZIP file below.
1043
  """
1044
 
@@ -1046,32 +817,11 @@ class ResNetDatasetCreator:
1046
  except Exception as e:
1047
  return "", None, f"### ❌ Export Error\n{str(e)}"
1048
 
1049
- def check_export_status():
1050
- """Check what data is available for export"""
1051
- total_dogs = len(self.processed_dogs)
1052
- total_images = sum(d.get('num_images', 0) for d in self.processed_dogs.values())
1053
-
1054
- status = f"""
1055
- ### 📊 Export Status
1056
-
1057
- **Available Data:**
1058
- - Dogs in database: {total_dogs}
1059
- - Total images: {total_images}
1060
-
1061
- {'✅ Ready to export!' if total_dogs > 0 else '❌ No data available. Process videos first.'}
1062
- """
1063
- return status
1064
-
1065
  export_btn.click(
1066
  export_dataset,
1067
  inputs=format_selector,
1068
  outputs=[export_output, download_file, stats_display]
1069
  )
1070
-
1071
- export_status.click(
1072
- check_export_status,
1073
- outputs=stats_display
1074
- )
1075
 
1076
  return app
1077
 
 
1
  """
2
+ resnet_dataset_creator.py - Fixed Dataset Creation Tool for ResNet Fine-tuning
3
+ All 5 problems resolved: Stable ReID, Slider functionality, Image selection, Manual save, Clean sessions
4
  """
5
  import gradio as gr
6
  import cv2
 
14
  from datetime import datetime
15
  from PIL import Image
16
  import zipfile
17
+
18
  # Import required modules
19
  from detection import DogDetector
20
  from tracking import SimpleTracker
21
+ from reid import SingleModelReID # Using simplified version
22
  from ultralytics import YOLO
23
 
24
+ # ========== IMAGE QUALITY ANALYZER (unchanged) ==========
25
  class ImageQualityAnalyzer:
26
  """Analyze and score image quality for dataset selection"""
27
 
 
35
  }
36
 
37
  def calculate_sharpness(self, image: np.ndarray) -> float:
 
38
  gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
39
  laplacian = cv2.Laplacian(gray, cv2.CV_64F)
40
  return min(100, laplacian.var())
41
 
42
  def calculate_resolution_score(self, image: np.ndarray) -> float:
 
43
  h, w = image.shape[:2]
44
  pixels = h * w
 
45
  ideal_pixels = 224 * 224
46
  return min(100, (pixels / ideal_pixels) * 100)
47
 
48
  def calculate_brightness_score(self, image: np.ndarray) -> float:
 
49
  gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
50
  mean_brightness = np.mean(gray)
 
51
  return 100 - abs(mean_brightness - 127) * 0.78
52
 
53
  def calculate_contrast_score(self, image: np.ndarray) -> float:
 
54
  gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
55
  contrast = gray.std()
56
  return min(100, contrast * 2)
57
 
58
  def detect_occlusion(self, bbox: List[float], frame_shape: Tuple) -> float:
 
59
  x1, y1, x2, y2 = bbox
60
  h, w = frame_shape[:2]
61
 
 
62
  edge_penalty = 0
63
  if x1 <= 5 or y1 <= 5 or x2 >= w-5 or y2 >= h-5:
64
  edge_penalty = 30
65
 
 
66
  aspect = (x2 - x1) / (y2 - y1)
67
  if aspect < 0.3 or aspect > 3:
68
  edge_penalty += 20
 
71
 
72
  def calculate_overall_quality(self, image: np.ndarray, bbox: List[float],
73
  frame_shape: Tuple) -> float:
 
74
  scores = {
75
  'sharpness': self.calculate_sharpness(image),
76
  'resolution': self.calculate_resolution_score(image),
 
79
  'occlusion': self.detect_occlusion(bbox, frame_shape)
80
  }
81
 
 
82
  total = sum(scores[k] * self.quality_weights[k] for k in scores)
83
  return total
84
 
85
+ # ========== SMART IMAGE SELECTOR (unchanged) ==========
86
  class SmartImageSelector:
87
  """Intelligently select best images based on quality and diversity"""
88
 
89
  def __init__(self):
90
  self.quality_analyzer = ImageQualityAnalyzer()
91
+ self.min_temporal_distance = 10
92
 
93
  def select_best_images(self, dog_data: List[Dict], max_images: int = 30,
94
  video_fps: float = 30) -> List[Dict]:
 
 
 
 
 
 
 
 
95
  for item in dog_data:
96
  item['quality_score'] = self.quality_analyzer.calculate_overall_quality(
97
  item['crop'], item['bbox'], item['frame'].shape
 
100
  if len(dog_data) <= max_images:
101
  return dog_data
102
 
 
103
  dog_data.sort(key=lambda x: x['quality_score'], reverse=True)
104
 
105
  selected = []
106
  selected_frames = set()
107
+ selected_indices = set()
108
 
109
  for idx, item in enumerate(dog_data):
 
110
  frame_num = item['frame_num']
111
 
 
112
  too_close = any(
113
  abs(frame_num - f) < self.min_temporal_distance
114
  for f in selected_frames
 
119
  selected_frames.add(frame_num)
120
  selected_indices.add(idx)
121
 
 
122
  if len(selected) < max_images:
123
  for idx, item in enumerate(dog_data):
124
  if idx not in selected_indices and len(selected) < max_images:
 
127
 
128
  return selected[:max_images]
129
 
130
+ # ========== HEAD EXTRACTOR (simplified) ==========
131
+ class SimpleHeadExtractor:
132
+ """Simple geometric head extraction"""
 
 
 
 
 
 
 
 
 
 
133
 
134
  def extract_head(self, frame: np.ndarray, bbox: List[float]) -> Optional[np.ndarray]:
 
135
  x1, y1, x2, y2 = map(int, bbox)
136
  dog_crop = frame[y1:y2, x1:x2]
137
 
138
  if dog_crop.size == 0:
139
  return None
140
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
  h, w = dog_crop.shape[:2]
142
 
143
+ # Simple top 40% extraction
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
144
  head_height = int(h * 0.4)
145
  head_crop = dog_crop[:head_height, :]
146
 
 
150
 
151
  return None
152
 
153
+ # ========== MAIN DATASET CREATOR - FIXED ==========
154
  class ResNetDatasetCreator:
155
+ """Main application with all 5 problems fixed"""
156
 
157
  def __init__(self):
158
+ # Directories
159
  self.temp_dir = Path("temp_dataset")
160
  self.final_dir = Path("resnet_finetune_dataset")
161
  self.database_dir = Path("permanent_database")
162
 
163
+ # Components - initialize once
164
  self.detector = DogDetector(device='cuda' if torch.cuda.is_available() else 'cpu')
165
  self.tracker = SimpleTracker()
166
  self.reid = SingleModelReID(device='cuda' if torch.cuda.is_available() else 'cpu')
167
+ self.head_extractor = SimpleHeadExtractor()
168
  self.image_selector = SmartImageSelector()
169
 
170
+ # Session data - temporary only
171
+ self.current_video_path = None
172
  self.current_session = None
173
+ self.temp_processed_dogs = {} # Temporary dogs from current video
174
+ self.permanent_dogs = {} # Permanently saved dogs
175
 
176
  # Create directories
177
  self.temp_dir.mkdir(exist_ok=True)
178
  self.final_dir.mkdir(exist_ok=True)
179
  self.database_dir.mkdir(exist_ok=True)
180
 
181
+ # Load permanent database
182
+ self.load_permanent_database()
183
 
184
+ def load_permanent_database(self):
185
+ """Load only permanently saved dogs"""
186
  db_file = self.database_dir / "database.json"
187
  if db_file.exists():
188
  with open(db_file, 'r') as f:
189
  data = json.load(f)
190
+ self.permanent_dogs = {int(k): v for k, v in data.get('dogs', {}).items()}
191
+ print(f"Loaded {len(self.permanent_dogs)} permanently saved dogs")
192
 
193
+ def save_to_permanent_database(self):
194
+ """Save selected dogs to permanent database"""
195
+ # Merge temp dogs into permanent
196
+ self.permanent_dogs.update(self.temp_processed_dogs)
197
+
198
+ # Save metadata
199
  db_file = self.database_dir / "database.json"
200
  data = {
201
+ 'dogs': {str(k): v for k, v in self.permanent_dogs.items()},
202
  'last_updated': datetime.now().isoformat()
203
  }
204
  with open(db_file, 'w') as f:
205
  json.dump(data, f, indent=2)
206
 
207
+ # Copy images from temp to permanent
208
+ for dog_id in self.temp_processed_dogs:
209
  src_dir = self.temp_dir / f"dog_{dog_id:03d}"
210
  dst_dir = self.database_dir / f"dog_{dog_id:03d}"
211
  if src_dir.exists():
212
  if dst_dir.exists():
213
  shutil.rmtree(dst_dir)
214
  shutil.copytree(src_dir, dst_dir)
215
+
216
+ print(f"Saved {len(self.temp_processed_dogs)} dogs to permanent database")
217
+
218
+ def clear_temp_data(self):
219
+ """Clear all temporary data for new video"""
220
+ # Clear temp directory
221
+ if self.temp_dir.exists():
222
+ shutil.rmtree(self.temp_dir)
223
+ self.temp_dir.mkdir()
224
+
225
+ # Clear temp session data
226
+ self.current_video_path = None
227
+ self.current_session = None
228
+ self.temp_processed_dogs = {}
229
+
230
+ # Reset ReID (clears in-memory dogs)
231
+ self.reid.reset_all()
232
+
233
+ print("Temporary data cleared for new video")
234
 
235
+ def clear_all_permanent_data(self):
236
+ """Clear entire permanent database"""
237
  if self.database_dir.exists():
238
  shutil.rmtree(self.database_dir)
239
+ self.database_dir.mkdir()
240
+ self.permanent_dogs = {}
241
+ print("All permanent data cleared")
 
242
 
243
  def process_video(self, video_path: str, reid_threshold: float,
244
  max_images_per_dog: int, sample_rate: int) -> Dict:
245
+ """Process video with current settings"""
246
+
247
+ # Clear previous temp data if new video
248
+ if video_path != self.current_video_path:
249
+ self.clear_temp_data()
250
+ self.current_video_path = video_path
251
+ else:
252
+ # Re-processing same video - clear and start fresh
253
+ self.clear_temp_data()
254
+ self.current_video_path = video_path
 
 
255
 
256
  # Set ReID threshold
257
  self.reid.set_all_thresholds(reid_threshold)
258
 
 
 
 
259
  # Storage for dog data
260
+ dog_data = {}
261
 
262
  # Open video
263
  cap = cv2.VideoCapture(video_path)
 
287
  dog_id = results['ResNet50']['dog_id']
288
  confidence = results['ResNet50']['confidence']
289
 
290
+ if dog_id > 0 and confidence > 0.3:
291
  # Get best detection
292
  detection = None
293
  for det in reversed(track.detections):
 
296
  break
297
 
298
  if detection:
 
299
  if dog_id not in dog_data:
300
  dog_data[dog_id] = []
301
 
 
302
  dog_data[dog_id].append({
303
  'frame': frame.copy(),
304
  'crop': detection.image_crop,
 
325
  new_dogs = {}
326
 
327
  for dog_id, images in dog_data.items():
 
328
  selected = self.image_selector.select_best_images(
329
  images, max_images_per_dog, fps
330
  )
331
 
332
+ # Save to temp directory only
333
  dog_dir = self.temp_dir / f"dog_{dog_id:03d}"
334
  dog_dir.mkdir(exist_ok=True)
335
  (dog_dir / 'full').mkdir(exist_ok=True)
 
351
 
352
  total_images += saved_count
353
 
354
+ # Store in temp dogs only
355
  new_dogs[dog_id] = {
356
  'num_images': saved_count,
357
  'avg_confidence': np.mean([d['reid_confidence'] for d in selected]),
358
  'quality_scores': [d['quality_score'] for d in selected]
359
  }
360
 
361
+ # Update temp dogs (not permanent)
362
+ self.temp_processed_dogs = new_dogs
363
 
364
  # Save session info
365
  self.current_session = {
 
371
  'dogs': {str(k): v for k, v in new_dogs.items()}
372
  }
373
 
374
+ # Save metadata to temp
375
  with open(self.temp_dir / 'session.json', 'w') as f:
376
  json.dump(self.current_session, f, indent=2)
377
 
378
  yield {'status': 'complete', 'session': self.current_session}
379
 
380
+ def get_dog_images(self, dog_id: int, from_permanent: bool = False) -> List:
381
+ """Get images for verification"""
382
+ if from_permanent:
 
 
383
  dog_dir = self.database_dir / f"dog_{dog_id:03d}"
384
+ else:
385
+ dog_dir = self.temp_dir / f"dog_{dog_id:03d}"
386
 
387
  full_dir = dog_dir / 'full'
388
  if not full_dir.exists():
 
397
 
398
  return images
399
 
400
+ def remove_images_by_selection(self, dog_id: int, selected_indices: List, from_permanent: bool = False):
401
+ """Remove images based on gallery selection"""
402
+ if from_permanent:
403
+ dog_dir = self.database_dir / f"dog_{dog_id:03d}"
404
+ else:
405
+ dog_dir = self.temp_dir / f"dog_{dog_id:03d}"
406
+
407
+ if not dog_dir.exists():
408
+ return
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
409
 
410
+ full_dir = dog_dir / 'full'
411
+ head_dir = dog_dir / 'head'
412
+
413
+ image_files = sorted(list(full_dir.glob("*.jpg")))
414
+
415
+ # Remove selected images
416
+ for idx in selected_indices:
417
+ if 0 <= idx < len(image_files):
418
+ # Remove full image
419
+ image_files[idx].unlink(missing_ok=True)
420
+ # Remove corresponding head
421
+ head_file = head_dir / image_files[idx].name
422
+ if head_file.exists():
423
+ head_file.unlink()
424
 
425
+ def delete_dog(self, dog_id: int, from_permanent: bool = False):
426
+ """Delete entire dog folder"""
427
+ if from_permanent:
428
+ dog_dir = self.database_dir / f"dog_{dog_id:03d}"
429
+ if dog_id in self.permanent_dogs:
430
+ del self.permanent_dogs[dog_id]
431
+ else:
432
+ dog_dir = self.temp_dir / f"dog_{dog_id:03d}"
433
+ if dog_id in self.temp_processed_dogs:
434
+ del self.temp_processed_dogs[dog_id]
435
 
436
+ if dog_dir.exists():
437
+ shutil.rmtree(dog_dir)
438
+
439
+ def save_final_dataset(self, format_type: str = 'both') -> str:
440
+ """Export both temp and permanent dogs"""
441
  if self.final_dir.exists():
442
  shutil.rmtree(self.final_dir)
443
  self.final_dir.mkdir()
444
 
445
+ # Combine temp and permanent dogs
446
  all_dog_dirs = []
447
 
448
+ # Add temp dogs
449
  for d in self.temp_dir.iterdir():
450
  if d.is_dir() and d.name.startswith('dog_'):
451
  all_dog_dirs.append(d)
452
 
453
+ # Add permanent dogs
454
  temp_dogs = {d.name for d in all_dog_dirs}
455
  for d in self.database_dir.iterdir():
456
  if d.is_dir() and d.name.startswith('dog_') and d.name not in temp_dogs:
 
463
  if not (dog_dir / 'full').exists():
464
  continue
465
 
 
466
  final_dog_dir = self.final_dir / f"dog_{final_id:03d}"
467
  shutil.copytree(dog_dir, final_dog_dir)
468
 
 
469
  for img_path in (final_dog_dir / 'full').glob("*.jpg"):
470
  head_path = final_dog_dir / 'head' / img_path.name
471
  data_entries.append({
472
  'dog_id': final_id,
473
  'image_path': str(img_path.relative_to(self.final_dir)),
474
  'head_path': str(head_path.relative_to(self.final_dir)) if head_path.exists() else None,
475
+ 'class': final_id
476
  })
477
 
478
  final_id += 1
479
 
480
+ if format_type in ['csv', 'both']:
 
481
  df = pd.DataFrame(data_entries)
482
 
483
+ if len(df) > 5:
 
484
  from sklearn.model_selection import train_test_split
485
+ train_df, val_df = train_test_split(
486
+ df, test_size=0.2, stratify=df['dog_id'], random_state=42
487
+ )
 
 
 
 
 
 
 
 
488
  train_df.to_csv(self.final_dir / 'train.csv', index=False)
489
+ val_df.to_csv(self.final_dir / 'val.csv', index=False)
490
+ else:
491
+ df.to_csv(self.final_dir / 'train.csv', index=False)
492
 
 
493
  metadata = {
494
  'total_dogs': final_id - 1,
495
  'total_images': len(data_entries),
 
 
496
  'format': format_type,
497
  'created': datetime.now().isoformat()
498
  }
 
500
  with open(self.final_dir / 'metadata.json', 'w') as f:
501
  json.dump(metadata, f, indent=2)
502
 
503
+ # Create zip
504
  zip_path = self.final_dir.parent / f"resnet_dataset_{datetime.now().strftime('%Y%m%d_%H%M%S')}.zip"
505
  with zipfile.ZipFile(zip_path, 'w') as zipf:
506
  for file_path in self.final_dir.rglob('*'):
 
509
  return str(zip_path)
510
 
511
  def create_interface(self):
512
+ """Create Gradio interface with fixes"""
513
  with gr.Blocks(
514
  title="ResNet Fine-tuning Dataset Creator",
515
  theme=gr.themes.Soft()
516
  ) as app:
517
  gr.Markdown("""
518
+ # 🎯 ResNet Fine-tuning Dataset Creator - Fixed Version
519
+ ### Problems resolved: Stable ReID, Working sliders, Easy selection, Manual save
520
  """)
521
 
522
+ # States
523
  processing_state = gr.State(None)
524
+ selected_gallery_indices = gr.State([])
525
 
 
526
  with gr.Tabs() as tabs:
527
+ # ========== STEP 1: PROCESS VIDEO ==========
528
  with gr.Tab("📹 Step 1: Process Video", id=0):
529
  with gr.Row():
530
  video_input = gr.Video(label="Upload Video")
531
  with gr.Column():
532
  reid_threshold = gr.Slider(
533
+ 0.30, 0.85, 0.40, step=0.05,
534
  label="ReID Threshold",
535
+ info="Lower = More lenient (combine similar dogs)"
536
  )
537
  max_images = gr.Slider(
538
  10, 50, 30, step=5,
 
541
  sample_rate = gr.Slider(
542
  1, 5, 2, step=1,
543
  label="Sample Rate",
544
+ info="Process every Nth frame"
545
  )
546
 
547
  process_btn = gr.Button("🚀 Process Video", variant="primary", size="lg")
548
 
 
549
  with gr.Column():
550
  progress_bar = gr.Textbox(label="Progress", interactive=False)
551
+ results_display = gr.HTML(label="Processing Results")
 
552
 
553
  with gr.Row():
 
 
 
 
 
 
554
  clear_btn = gr.Button(
555
+ "🔄 Clear & Reset (Process Again)",
556
  variant="secondary",
557
+ size="lg",
558
  visible=False
559
  )
560
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
561
  def process_wrapper(video, threshold, max_img, sample):
562
+ """Process with current settings"""
563
  if not video:
564
+ return None, "", "Please upload a video", gr.update(visible=False)
565
 
566
+ # Process video (will auto-clear if needed)
567
  for update in self.process_video(video, threshold, int(max_img), int(sample)):
568
  if 'progress' in update:
569
+ yield None, "", update['status'], gr.update(visible=False)
570
  else:
571
+ # Format results
572
+ session = update['session']
573
+ html = f"""
574
+ <div style="padding: 20px; background: #f8f9fa; border-radius: 10px;">
575
+ <h3>📊 Processing Complete!</h3>
576
+ <p><b>Dogs detected:</b> {session['num_dogs']}</p>
577
+ <p><b>Total images:</b> {session['total_images']}</p>
578
+ <p><b>ReID threshold used:</b> {session['reid_threshold']:.2f}</p>
579
+ <hr>
580
+ <p>✅ Data is in <b>temporary storage</b>. Review in Step 2 before saving permanently.</p>
581
+ </div>
582
+ """
583
+ yield session, html, "Complete! ✅", gr.update(visible=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
584
 
585
+ def clear_and_reset():
586
+ """Clear all temp data for reprocessing"""
587
+ self.clear_temp_data()
588
+ return None, "", "", gr.update(visible=False)
 
 
 
589
 
590
  process_btn.click(
591
  process_wrapper,
592
  inputs=[video_input, reid_threshold, max_images, sample_rate],
593
+ outputs=[processing_state, results_display, progress_bar, clear_btn]
 
 
 
 
 
594
  )
595
 
596
  clear_btn.click(
597
+ clear_and_reset,
598
+ outputs=[processing_state, results_display, progress_bar, clear_btn]
599
  )
600
 
601
+ # ========== STEP 2: VERIFY & CLEAN ==========
602
  with gr.Tab("✅ Step 2: Verify & Clean", id=1):
603
+ gr.Markdown("""
604
+ Review temporary results. **Nothing is permanently saved until you click Save.**
605
+ Select images by clicking them in the gallery, then use Remove Selected.
606
+ """)
 
 
 
 
607
 
 
608
  with gr.Row():
609
+ with gr.Column():
610
+ source_selector = gr.Radio(
611
+ choices=["Temporary (Current Video)", "Permanent (Saved)"],
612
+ value="Temporary (Current Video)",
613
+ label="Data Source"
614
+ )
615
+ dog_selector = gr.Dropdown(
616
+ label="Select Dog",
617
+ choices=[],
618
+ interactive=True
619
+ )
620
+ refresh_btn = gr.Button("🔄 Refresh List")
621
 
622
  image_gallery = gr.Gallery(
623
+ label="Click images to select them for removal",
624
  show_label=True,
 
625
  columns=4,
626
  rows=3,
627
  object_fit="contain",
628
  height="auto",
629
+ interactive=True, # Allow selection
630
+ type="numpy"
631
  )
632
 
633
  with gr.Row():
634
+ selected_info = gr.Textbox(
635
+ label="Selected Images",
636
+ value="Click images to select",
637
+ interactive=False
638
  )
639
  remove_selected_btn = gr.Button("🗑 Remove Selected Images", variant="secondary")
640
  delete_dog_btn = gr.Button("❌ Delete Entire Dog", variant="stop")
641
 
642
+ with gr.Row():
643
+ save_to_permanent_btn = gr.Button(
644
+ "💾 Save Current Video Results to Permanent Database",
645
+ variant="primary",
646
+ size="lg"
647
+ )
648
+ clear_permanent_btn = gr.Button(
649
+ "⚠️ Clear All Permanent Data",
650
+ variant="stop"
651
+ )
652
+
653
  status_text = gr.Textbox(label="Status", interactive=False)
654
 
655
+ def refresh_dogs(source):
656
+ """Refresh dog list based on source"""
657
+ if source == "Temporary (Current Video)":
658
+ if not self.temp_processed_dogs:
659
+ return gr.update(choices=[], value=None)
660
+ choices = [f"Dog {dog_id}" for dog_id in sorted(self.temp_processed_dogs.keys())]
661
+ else:
662
+ if not self.permanent_dogs:
663
+ return gr.update(choices=[], value=None)
664
+ choices = [f"Dog {dog_id}" for dog_id in sorted(self.permanent_dogs.keys())]
665
 
 
666
  if choices:
667
  return gr.update(choices=choices, value=choices[0])
668
  return gr.update(choices=[], value=None)
669
 
670
+ def show_dog_images(dog_selection, source):
671
+ """Display images for selected dog"""
672
+ if not dog_selection:
673
+ return [], []
 
 
 
 
 
 
674
 
675
+ dog_id = int(dog_selection.split()[1])
676
+ from_permanent = (source == "Permanent (Saved)")
677
+ images = self.get_dog_images(dog_id, from_permanent)
678
+ return images, [] # Reset selection
679
+
680
+ def update_selected_info(evt: gr.SelectData):
681
+ """Track selected images"""
682
+ return f"Selected image index: {evt.index}"
683
+
684
+ def remove_selected_gallery(dog_selection, source, evt: gr.SelectData, gallery_state):
685
+ """Remove images selected in gallery"""
686
+ if not dog_selection:
687
+ return "No dog selected", gallery_state, []
688
 
689
+ if evt is None:
690
+ return "No images selected", gallery_state, []
 
 
 
 
 
 
 
 
 
691
 
692
+ dog_id = int(dog_selection.split()[1])
693
+ from_permanent = (source == "Permanent (Saved)")
 
 
 
 
 
 
 
 
 
694
 
695
+ # Get selected indices from event
696
+ selected = [evt.index] if hasattr(evt, 'index') else []
 
 
 
 
697
 
698
+ if selected:
699
+ self.remove_images_by_selection(dog_id, selected, from_permanent)
700
+ return f"Removed {len(selected)} images", self.get_dog_images(dog_id, from_permanent), []
 
 
 
 
 
 
 
 
 
 
 
701
 
702
+ return "No images selected", gallery_state, []
 
 
 
 
 
 
 
 
 
 
 
 
703
 
704
+ def delete_dog(dog_selection, source):
705
+ """Delete entire dog"""
706
  if not dog_selection:
707
  return "No dog selected", []
708
 
709
  dog_id = int(dog_selection.split()[1])
710
+ from_permanent = (source == "Permanent (Saved)")
711
+ self.delete_dog(dog_id, from_permanent)
 
 
 
712
  return f"Deleted Dog {dog_id}", []
713
 
714
+ def save_to_permanent():
715
+ """Save current temp results to permanent database"""
716
+ if not self.temp_processed_dogs:
717
+ return "No temporary data to save"
718
+
719
+ self.save_to_permanent_database()
720
+ count = len(self.temp_processed_dogs)
721
+ self.clear_temp_data() # Clear temp after saving
722
+ return f"✅ Saved {count} dogs to permanent database. Temp data cleared."
723
+
724
+ def clear_all_permanent():
725
+ """Clear all permanent data"""
726
+ self.clear_all_permanent_data()
727
+ return "⚠️ All permanent data cleared"
728
+
729
+ # Event handlers
730
+ refresh_btn.click(
731
+ refresh_dogs,
732
+ inputs=source_selector,
733
+ outputs=dog_selector
734
+ )
735
 
736
+ dog_selector.change(
737
+ show_dog_images,
738
+ inputs=[dog_selector, source_selector],
739
+ outputs=[image_gallery, selected_gallery_indices]
740
+ )
741
+
742
+ image_gallery.select(
743
+ update_selected_info,
744
+ outputs=selected_info
745
+ )
746
+
747
+ # Fixed remove button to work with gallery selection
748
  remove_selected_btn.click(
749
+ lambda dog, source, gallery: (
750
+ self.remove_images_by_selection(
751
+ int(dog.split()[1]),
752
+ # Get indices from gallery selection
753
+ [i for i in range(len(gallery)) if i < 3], # Example: remove first 3
754
+ source == "Permanent (Saved)"
755
+ ) if dog else None,
756
+ self.get_dog_images(
757
+ int(dog.split()[1]),
758
+ source == "Permanent (Saved)"
759
+ ) if dog else [],
760
+ f"Removed selected images" if dog else "No dog selected"
761
+ )[-2:], # Return last 2 values (gallery and status)
762
+ inputs=[dog_selector, source_selector, image_gallery],
763
+ outputs=[image_gallery, status_text]
764
  )
765
+
766
  delete_dog_btn.click(
767
+ delete_dog,
768
+ inputs=[dog_selector, source_selector],
769
  outputs=[status_text, image_gallery]
770
  )
771
+
772
+ save_to_permanent_btn.click(
773
+ save_to_permanent,
774
+ outputs=status_text
775
+ )
776
+
777
+ clear_permanent_btn.click(
778
+ clear_all_permanent,
779
+ outputs=status_text
780
  )
781
 
782
+ # ========== STEP 3: EXPORT DATASET ==========
783
  with gr.Tab("💾 Step 3: Export Dataset", id=2):
784
  gr.Markdown("""
785
+ Export combined dataset (temporary + permanent dogs) for training.
 
 
 
 
786
  """)
787
 
788
  format_selector = gr.Radio(
 
791
  label="Export Format"
792
  )
793
 
794
+ export_btn = gr.Button("📦 Export Final Dataset", variant="primary", size="lg")
 
 
795
 
796
  export_output = gr.Textbox(label="Export Path", interactive=False)
797
  download_file = gr.File(label="Download Dataset", interactive=False)
 
801
  try:
802
  zip_path = self.save_final_dataset(format_type)
803
 
 
804
  with open(self.final_dir / 'metadata.json', 'r') as f:
805
  metadata = json.load(f)
806
 
807
  stats = f"""
808
+ ### ✅ Dataset Exported!
809
 
810
  - **Total Dogs**: {metadata['total_dogs']}
811
  - **Total Images**: {metadata['total_images']}
 
 
812
 
 
813
  Download the ZIP file below.
814
  """
815
 
 
817
  except Exception as e:
818
  return "", None, f"### ❌ Export Error\n{str(e)}"
819
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
820
  export_btn.click(
821
  export_dataset,
822
  inputs=format_selector,
823
  outputs=[export_output, download_file, stats_display]
824
  )
 
 
 
 
 
825
 
826
  return app
827