mustafa2ak commited on
Commit
709a86e
·
verified ·
1 Parent(s): 1b0864d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +125 -623
app.py CHANGED
@@ -1,133 +1,64 @@
1
  """
2
- Simplified Enhanced Dataset Creator with Fixed Image Selection
3
  """
4
  import gradio as gr
5
  import cv2
6
  import numpy as np
7
- import pandas as pd
8
- import json
9
- import shutil
10
  import torch
11
  from pathlib import Path
12
- from typing import List, Dict, Optional, Tuple
13
- from datetime import datetime
14
- import zipfile
15
  import gc
 
 
 
16
 
17
- # Import required modules
18
  from detection import DogDetector
19
  from tracking import SimpleTracker
20
- from reid import MultiComponentReID
21
- from ultralytics import YOLO
22
 
23
 
24
- class ImageQualityAnalyzer:
25
- """Simple image quality scoring"""
26
-
27
- def calculate_quality(self, image: np.ndarray, bbox: List[float]) -> float:
28
- """Calculate overall quality score (0-100)"""
29
- gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
30
-
31
- # Sharpness (Laplacian variance)
32
- sharpness = min(100, cv2.Laplacian(gray, cv2.CV_64F).var())
33
-
34
- # Brightness (optimal around 127)
35
- brightness = 100 - abs(np.mean(gray) - 127) * 0.78
36
-
37
- # Size score
38
- h, w = image.shape[:2]
39
- size_score = min(100, (h * w) / (224 * 224) * 100)
40
-
41
- # Combine scores
42
- return (sharpness * 0.4 + brightness * 0.3 + size_score * 0.3)
43
-
44
-
45
- class SimpleDatasetCreator:
46
- """Simplified dataset creator with intuitive interface"""
47
 
48
  def __init__(self):
49
- # Core directories
50
- self.temp_dir = Path("temp_dataset")
51
- self.database_dir = Path("permanent_database")
52
- self.export_dir = Path("export_dataset")
53
-
54
- # Create directories
55
- for dir_path in [self.temp_dir, self.database_dir, self.export_dir]:
56
- dir_path.mkdir(exist_ok=True)
57
-
58
- # Components
59
  device = 'cuda' if torch.cuda.is_available() else 'cpu'
60
  self.detector = DogDetector(device=device)
61
  self.tracker = SimpleTracker()
62
- self.reid = MultiComponentReID(device=device)
63
- self.quality_analyzer = ImageQualityAnalyzer()
64
-
65
- # Session data
66
- self.current_dogs = {} # dog_id -> metadata
67
- self.dog_images = {} # dog_id -> list of image paths
68
- self.selected_images = [] # Currently selected image paths
69
- self.next_dog_id = 1
70
-
71
- # Load existing database
72
- self.load_database()
 
 
 
73
 
74
- def load_database(self):
75
- """Load existing dogs from database"""
76
- db_file = self.database_dir / "database.json"
77
- if db_file.exists():
78
- with open(db_file, 'r') as f:
79
- data = json.load(f)
80
- self.current_dogs = {int(k): v for k, v in data.get('dogs', {}).items()}
81
- self.next_dog_id = data.get('next_id', 1)
82
-
83
- # Load image paths
84
- for dog_id in self.current_dogs:
85
- dog_dir = self.database_dir / f"dog_{dog_id:03d}"
86
- if dog_dir.exists():
87
- self.dog_images[dog_id] = sorted([str(p) for p in dog_dir.glob("*.jpg")])
88
-
89
- def save_database(self):
90
- """Save current dogs to database"""
91
- # Save metadata
92
- db_file = self.database_dir / "database.json"
93
- data = {
94
- 'dogs': {str(k): v for k, v in self.current_dogs.items()},
95
- 'next_id': self.next_dog_id,
96
- 'timestamp': datetime.now().isoformat()
97
- }
98
- with open(db_file, 'w') as f:
99
- json.dump(data, f, indent=2)
100
-
101
- # Copy images from temp to permanent
102
- for dog_id in self.current_dogs:
103
- src_dir = self.temp_dir / f"dog_{dog_id:03d}"
104
- if src_dir.exists():
105
- dst_dir = self.database_dir / f"dog_{dog_id:03d}"
106
- if dst_dir.exists():
107
- shutil.rmtree(dst_dir)
108
- shutil.copytree(src_dir, dst_dir)
109
-
110
- def process_video(self, video_path: str, reid_threshold: float,
111
- max_images: int, sample_rate: int) -> Dict:
112
- """Simplified video processing"""
113
  if not video_path:
114
- return {'status': 'error', 'message': 'No video provided'}
115
 
116
- # Clear temp directory
117
- if self.temp_dir.exists():
118
- shutil.rmtree(self.temp_dir)
119
- self.temp_dir.mkdir()
120
 
121
  # Set ReID threshold
122
  self.reid.set_all_thresholds(reid_threshold)
123
 
124
  # Process video
125
  cap = cv2.VideoCapture(video_path)
126
- fps = cap.get(cv2.CAP_PROP_FPS)
127
  total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
128
 
129
- dog_data = {} # dog_id -> list of crops
130
  frame_num = 0
 
131
 
132
  while cap.isOpened():
133
  ret, frame = cap.read()
@@ -136,36 +67,33 @@ class SimpleDatasetCreator:
136
 
137
  # Process every N frames
138
  if frame_num % sample_rate == 0:
 
139
  detections = self.detector.detect(frame)
 
 
140
  tracks = self.tracker.update(detections)
141
 
 
142
  for track in tracks:
143
  # Get ReID result
144
- results = self.reid.match_or_register_all(track)
145
- dog_id = results['ResNet50']['dog_id']
146
- confidence = results['ResNet50']['confidence']
147
 
148
- if dog_id > 0 and confidence > 0.3:
149
  # Get latest detection with crop
150
  for det in reversed(track.detections[-3:]):
151
  if det.image_crop is not None:
152
- if dog_id not in dog_data:
153
- dog_data[dog_id] = []
154
-
155
- # Calculate quality score
156
- quality = self.quality_analyzer.calculate_quality(
157
- det.image_crop, det.bbox
158
- )
159
 
160
- dog_data[dog_id].append({
161
- 'crop': det.image_crop.copy(),
162
- 'frame_num': frame_num,
163
- 'quality': quality,
164
- 'confidence': confidence
165
- })
166
  break
167
 
168
- # Memory cleanup
 
 
169
  if frame_num % 100 == 0:
170
  gc.collect()
171
  if torch.cuda.is_available():
@@ -173,541 +101,115 @@ class SimpleDatasetCreator:
173
 
174
  frame_num += 1
175
 
176
- # Yield progress
177
  if frame_num % 30 == 0:
178
  progress = int((frame_num / total_frames) * 100)
179
- yield {'status': 'processing', 'progress': progress}
180
 
181
  cap.release()
182
 
183
- # Select best images for each dog
184
- total_images = 0
185
- new_dogs = {}
 
186
 
187
- for temp_id, images in dog_data.items():
188
- # Get new dog ID
189
- dog_id = self.next_dog_id
190
- self.next_dog_id += 1
191
-
192
- # Sort by quality and select top N
193
- images.sort(key=lambda x: x['quality'], reverse=True)
194
- selected = images[:max_images]
195
-
196
- # Save images
197
- dog_dir = self.temp_dir / f"dog_{dog_id:03d}"
198
- dog_dir.mkdir(exist_ok=True)
199
-
200
- saved_paths = []
201
- for idx, img_data in enumerate(selected):
202
- img_path = dog_dir / f"img_{idx:03d}.jpg"
203
- cv2.imwrite(str(img_path), img_data['crop'])
204
- saved_paths.append(str(img_path))
205
-
206
- # Update tracking
207
- self.dog_images[dog_id] = saved_paths
208
- new_dogs[dog_id] = {
209
- 'num_images': len(saved_paths),
210
- 'avg_confidence': np.mean([d['confidence'] for d in selected]),
211
- 'source': video_path
212
- }
213
- total_images += len(saved_paths)
214
 
215
- # Update current dogs
216
- self.current_dogs.update(new_dogs)
217
 
218
- yield {
219
- 'status': 'complete',
220
- 'num_dogs': len(new_dogs),
221
- 'total_images': total_images,
222
- 'dogs': new_dogs
223
- }
224
 
225
- def get_all_dog_galleries(self):
226
- """Get all dog galleries for display"""
227
- galleries = []
228
-
229
- for dog_id in sorted(self.current_dogs.keys()):
230
- images = []
231
- paths = self.dog_images.get(dog_id, [])
 
 
 
 
 
 
 
 
 
 
 
232
 
233
- for img_path in paths:
234
- try:
235
- img = cv2.imread(img_path)
236
- if img is not None:
237
- img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
238
- images.append(img_rgb)
239
- except:
240
- continue
241
 
242
- if images:
243
- galleries.append({
244
- 'dog_id': dog_id,
245
- 'images': images,
246
- 'paths': paths,
247
- 'num_images': len(images)
248
- })
249
-
250
- return galleries
251
-
252
- def reassign_images(self, from_dog: int, to_dog: int, image_indices: List[int]):
253
- """Move selected images from one dog to another"""
254
- if from_dog not in self.dog_images:
255
- return f"Dog {from_dog} not found"
256
-
257
- from_paths = self.dog_images[from_dog]
258
- moved_paths = []
259
-
260
- # Get paths to move
261
- for idx in sorted(image_indices, reverse=True):
262
- if 0 <= idx < len(from_paths):
263
- moved_paths.append(from_paths.pop(idx))
264
-
265
- if not moved_paths:
266
- return "No valid images to move"
267
-
268
- # Create target dog if needed
269
- if to_dog not in self.current_dogs:
270
- self.current_dogs[to_dog] = {
271
- 'num_images': 0,
272
- 'avg_confidence': 0.5,
273
- 'source': 'reassigned'
274
- }
275
- self.dog_images[to_dog] = []
276
-
277
- # Move files
278
- to_dir = self.temp_dir / f"dog_{to_dog:03d}"
279
- to_dir.mkdir(exist_ok=True)
280
-
281
- for old_path in moved_paths:
282
- old_path = Path(old_path)
283
- if old_path.exists():
284
- new_path = to_dir / f"img_{len(self.dog_images[to_dog]):03d}.jpg"
285
- shutil.move(str(old_path), str(new_path))
286
- self.dog_images[to_dog].append(str(new_path))
287
-
288
- # Update metadata
289
- self.current_dogs[from_dog]['num_images'] = len(self.dog_images[from_dog])
290
- self.current_dogs[to_dog]['num_images'] = len(self.dog_images[to_dog])
291
-
292
- # Remove empty dogs
293
- if len(self.dog_images[from_dog]) == 0:
294
- del self.current_dogs[from_dog]
295
- del self.dog_images[from_dog]
296
 
297
- return f"✅ Moved {len(moved_paths)} images from Dog {from_dog} to Dog {to_dog}"
298
-
299
- def delete_dog_images(self, dog_id: int, image_indices: List[int]):
300
- """Delete selected images from a dog"""
301
- if dog_id not in self.dog_images:
302
- return f"Dog {dog_id} not found"
303
-
304
- paths = self.dog_images[dog_id]
305
- deleted_count = 0
306
-
307
- # Delete in reverse order to maintain indices
308
- for idx in sorted(image_indices, reverse=True):
309
- if 0 <= idx < len(paths):
310
- img_path = Path(paths.pop(idx))
311
- if img_path.exists():
312
- img_path.unlink()
313
- deleted_count += 1
314
-
315
- # Update metadata
316
- self.current_dogs[dog_id]['num_images'] = len(paths)
317
-
318
- # Remove dog if no images left
319
- if len(paths) == 0:
320
- del self.current_dogs[dog_id]
321
- del self.dog_images[dog_id]
322
-
323
- return f"🗑️ Deleted {deleted_count} images from Dog {dog_id}"
324
-
325
- def export_dataset(self, include_csv: bool = True):
326
- """Export the dataset"""
327
- # Clear export directory
328
- if self.export_dir.exists():
329
- shutil.rmtree(self.export_dir)
330
- self.export_dir.mkdir()
331
-
332
- # Copy all dog directories
333
- total_images = 0
334
- for dog_id in self.current_dogs:
335
- # Try permanent first, then temp
336
- src_dir = self.database_dir / f"dog_{dog_id:03d}"
337
- if not src_dir.exists():
338
- src_dir = self.temp_dir / f"dog_{dog_id:03d}"
339
-
340
- if src_dir.exists():
341
- dst_dir = self.export_dir / f"dog_{dog_id:03d}"
342
- shutil.copytree(src_dir, dst_dir)
343
- total_images += len(list(dst_dir.glob("*.jpg")))
344
-
345
- # Create CSV if requested
346
- if include_csv:
347
- csv_data = []
348
- for dog_id in self.current_dogs:
349
- dog_dir = self.export_dir / f"dog_{dog_id:03d}"
350
- if dog_dir.exists():
351
- for img_path in dog_dir.glob("*.jpg"):
352
- csv_data.append({
353
- 'dog_id': dog_id,
354
- 'image_path': str(img_path.relative_to(self.export_dir)),
355
- 'filename': img_path.name
356
- })
357
-
358
- df = pd.DataFrame(csv_data)
359
- df.to_csv(self.export_dir / "dataset.csv", index=False)
360
-
361
- # Create zip
362
- zip_path = Path("dog_dataset.zip")
363
- with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
364
- for file_path in self.export_dir.rglob("*"):
365
- if file_path.is_file():
366
- zipf.write(file_path, file_path.relative_to(self.export_dir))
367
-
368
- return str(zip_path), len(self.current_dogs), total_images
369
 
370
  def _img_to_base64(self, img):
371
- """Convert image to base64 for HTML display"""
372
- import base64
373
- from io import BytesIO
374
- from PIL import Image
375
-
376
  pil_img = Image.fromarray(img)
377
  buffered = BytesIO()
378
- pil_img.save(buffered, format="JPEG", quality=70)
379
- img_str = base64.b64encode(buffered.getvalue()).decode()
380
- return img_str
381
 
382
  def create_interface(self):
383
  """Create simplified Gradio interface"""
384
- with gr.Blocks(title="Dog Dataset Creator", theme=gr.themes.Soft()) as app:
385
- gr.Markdown("# 🐕 Dog Dataset Creator")
 
 
 
 
 
 
386
 
387
- with gr.Tabs():
388
- # ========== STEP 1: Process Video ==========
389
- with gr.Tab("📹 Process Video"):
390
- with gr.Row():
391
- with gr.Column():
392
- video_input = gr.Video(label="Upload Video")
393
-
394
- reid_threshold = gr.Slider(
395
- 0.3, 0.8, 0.6, step=0.05,
396
- label="ReID Threshold",
397
- info="Lower = more lenient matching"
398
- )
399
-
400
- max_images = gr.Slider(
401
- 10, 50, 30, step=5,
402
- label="Max Images per Dog"
403
- )
404
-
405
- sample_rate = gr.Slider(
406
- 1, 5, 2, step=1,
407
- label="Sample Rate (process every N frames)"
408
- )
409
-
410
- process_btn = gr.Button("🚀 Process Video", variant="primary")
411
-
412
- with gr.Column():
413
- progress_text = gr.Textbox(label="Progress", interactive=False)
414
- results_html = gr.HTML()
415
-
416
- def process_video_wrapper(video, threshold, max_img, sample):
417
- if not video:
418
- return "No video uploaded", ""
419
-
420
- for update in self.process_video(video, threshold, int(max_img), int(sample)):
421
- if update['status'] == 'processing':
422
- yield f"Processing: {update['progress']}%", ""
423
- else:
424
- html = f"""
425
- <div style="padding: 15px; background: #e8f5e9; border-radius: 8px;">
426
- <h3>✅ Processing Complete!</h3>
427
- <p>Dogs detected: <b>{update['num_dogs']}</b></p>
428
- <p>Total images: <b>{update['total_images']}</b></p>
429
- </div>
430
- """
431
- yield "Complete!", html
432
-
433
- process_btn.click(
434
- process_video_wrapper,
435
- inputs=[video_input, reid_threshold, max_images, sample_rate],
436
- outputs=[progress_text, results_html]
437
- )
438
-
439
- # ========== STEP 2: Verify & Edit ==========
440
- with gr.Tab("✏️ Verify & Edit"):
441
- gr.Markdown("""
442
- ### 📋 How to Edit Dogs:
443
- 1. Click **Refresh Galleries** to see all dogs
444
- 2. Note the image numbers (0, 1, 2...) shown on each image
445
- 3. Enter the Dog ID and image numbers to move or delete
446
- """)
447
-
448
- refresh_btn = gr.Button("🔄 Refresh Galleries", variant="primary", size="lg")
449
-
450
- # Gallery display
451
- gallery_html = gr.HTML(label="Dog Galleries")
452
-
453
- gr.Markdown("---")
454
-
455
- # Simplified controls in clear sections
456
- with gr.Row():
457
- with gr.Column(scale=1):
458
- gr.Markdown("### 🔄 Move Images Between Dogs")
459
- from_dog = gr.Number(label="From Dog ID", value=1, precision=0)
460
- image_indices = gr.Textbox(
461
- label="Image Numbers",
462
- placeholder="0,2,5",
463
- info="Enter image numbers shown on thumbnails"
464
- )
465
- to_dog = gr.Number(label="To Dog ID", value=2, precision=0)
466
- move_btn = gr.Button("Move Images →", variant="primary")
467
-
468
- with gr.Column(scale=1):
469
- gr.Markdown("### 🗑️ Delete Images")
470
- del_dog = gr.Number(label="Dog ID", value=1, precision=0)
471
- del_indices = gr.Textbox(
472
- label="Image Numbers to Delete",
473
- placeholder="0,1,2",
474
- info="Enter image numbers to remove"
475
- )
476
- delete_btn = gr.Button("Delete Images", variant="stop")
477
-
478
- gr.Markdown("---")
479
-
480
- # Status and save
481
- status_text = gr.Textbox(label="Status", interactive=False)
482
- save_btn = gr.Button("💾 Save All Dogs to Database", variant="primary", size="lg")
483
-
484
- def refresh_galleries():
485
- """Create HTML grid of dog galleries"""
486
- galleries = self.get_all_dog_galleries()
487
-
488
- if not galleries:
489
- return "<p style='text-align:center; color:#666;'>No dogs found. Process a video first.</p>"
490
-
491
- html = """
492
- <div style='max-width: 1200px; margin: 0 auto;'>
493
- <div style='display: grid; grid-template-columns: repeat(auto-fit, minmax(500px, 1fr)); gap: 20px;'>
494
- """
495
-
496
- for gal in galleries:
497
- dog_id = gal['dog_id']
498
- num_images = gal['num_images']
499
-
500
- html += f"""
501
- <div style='border: 2px solid #2196F3; border-radius: 10px; padding: 15px; background: #f5f5f5;'>
502
- <h3 style='margin: 0 0 10px 0; color: #1976D2;'>🐕 Dog {dog_id}</h3>
503
- <p style='margin: 5px 0; color: #666;'>Total: {num_images} images</p>
504
- <div style='display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; margin-top: 10px;'>
505
- """
506
-
507
- # Show first 12 images as thumbnails
508
- for i, img in enumerate(gal['images'][:12]):
509
- html += f"""
510
- <div style='position: relative; aspect-ratio: 1/1; overflow: hidden;
511
- border: 2px solid #ddd; border-radius: 5px;'>
512
- <img src='data:image/jpeg;base64,{self._img_to_base64(img)}'
513
- style='width: 100%; height: 100%; object-fit: cover;'
514
- title='Image {i}'>
515
- <div style='position: absolute; top: 4px; left: 4px;
516
- background: #2196F3; color: white;
517
- padding: 2px 6px; font-size: 12px; font-weight: bold;
518
- border-radius: 3px;'>{i}</div>
519
- </div>
520
- """
521
-
522
- if num_images > 12:
523
- html += f"""
524
- <div style='grid-column: span 4; text-align: center;
525
- padding: 10px; color: #666; font-style: italic;'>
526
- ... and {num_images - 12} more images
527
- </div>
528
- """
529
-
530
- html += "</div></div>"
531
-
532
- html += "</div></div>"
533
-
534
- return html
535
-
536
- refresh_btn.click(
537
- refresh_galleries,
538
- outputs=gallery_html
539
  )
540
 
541
- def move_images_wrapper(from_id, indices, to_id):
542
- try:
543
- if not indices:
544
- return "Please enter image numbers to move"
545
- indices_list = [int(x.strip()) for x in indices.split(',') if x.strip()]
546
- return self.reassign_images(int(from_id), int(to_id), indices_list)
547
- except ValueError:
548
- return "Invalid input. Use numbers like: 0,1,2"
549
- except Exception as e:
550
- return f"Error: {str(e)}"
551
-
552
- def delete_images_wrapper(dog_id, indices):
553
- try:
554
- if not indices:
555
- return "Please enter image numbers to delete"
556
- indices_list = [int(x.strip()) for x in indices.split(',') if x.strip()]
557
- return self.delete_dog_images(int(dog_id), indices_list)
558
- except ValueError:
559
- return "Invalid input. Use numbers like: 0,1,2"
560
- except Exception as e:
561
- return f"Error: {str(e)}"
562
-
563
- move_btn.click(
564
- move_images_wrapper,
565
- inputs=[from_dog, image_indices, to_dog],
566
- outputs=status_text
567
- )
568
-
569
- delete_btn.click(
570
- delete_images_wrapper,
571
- inputs=[del_dog, del_indices],
572
- outputs=status_text
573
  )
574
 
575
- save_btn.click(
576
- lambda: (self.save_database(), "✅ All dogs saved to database!")[1],
577
- outputs=status_text
578
- )
579
 
580
- # ========== STEP 3: Export ==========
581
- with gr.Tab("📦 Export Dataset"):
582
- gr.Markdown("""
583
- ### 📥 Export Your Dataset
584
- Download all dogs as a ZIP file for training your model.
585
- """)
586
-
587
- with gr.Row():
588
- with gr.Column():
589
- include_csv = gr.Checkbox(
590
- label="Include CSV metadata file",
591
- value=True,
592
- info="Creates a CSV with image paths and dog IDs"
593
- )
594
-
595
- export_btn = gr.Button("📥 Create Export ZIP", variant="primary", size="lg")
596
-
597
- with gr.Column():
598
- download_file = gr.File(
599
- label="Download Dataset",
600
- interactive=False,
601
- visible=False
602
- )
603
- export_status = gr.HTML()
604
-
605
- def export_wrapper(csv):
606
- try:
607
- zip_path, num_dogs, num_images = self.export_dataset(csv)
608
- html = f"""
609
- <div style='padding: 20px; background: #e3f2fd; border-radius: 10px;
610
- border: 2px solid #2196F3;'>
611
- <h3 style='color: #1976D2; margin-top: 0;'>✅ Dataset Ready!</h3>
612
- <div style='display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px;'>
613
- <div style='background: white; padding: 10px; border-radius: 5px;'>
614
- <p style='margin: 0; color: #666;'>Dogs</p>
615
- <p style='margin: 0; font-size: 24px; font-weight: bold;'>{num_dogs}</p>
616
- </div>
617
- <div style='background: white; padding: 10px; border-radius: 5px;'>
618
- <p style='margin: 0; color: #666;'>Images</p>
619
- <p style='margin: 0; font-size: 24px; font-weight: bold;'>{num_images}</p>
620
- </div>
621
- </div>
622
- <p style='margin-top: 15px; color: #666;'>
623
- Click the download button below to get your dataset ZIP file.
624
- </p>
625
- </div>
626
- """
627
- return gr.update(value=zip_path, visible=True), html
628
- except Exception as e:
629
- html = f"""
630
- <div style='padding: 15px; background: #ffebee; border-radius: 10px;
631
- border: 2px solid #f44336;'>
632
- <h3 style='color: #c62828; margin-top: 0;'>❌ Export Failed</h3>
633
- <p style='color: #666;'>{str(e)}</p>
634
- </div>
635
- """
636
- return gr.update(visible=False), html
637
-
638
- export_btn.click(
639
- export_wrapper,
640
- inputs=include_csv,
641
- outputs=[download_file, export_status]
642
- )
643
-
644
- def move_images(from_dog, to_dog, indices_str):
645
- try:
646
- indices = [int(x.strip()) for x in indices_str.split(',')]
647
- return self.reassign_images(int(from_dog), int(to_dog), indices)
648
- except:
649
- return "Invalid input. Use format: 0,1,2"
650
-
651
- def delete_images(dog_id, indices_str):
652
- try:
653
- indices = [int(x.strip()) for x in indices_str.split(',')]
654
- return self.delete_dog_images(int(dog_id), indices)
655
- except:
656
- return "Invalid input. Use format: 0,1,2"
657
-
658
- move_btn.click(
659
- move_images,
660
- inputs=[selected_dog, target_dog, selected_indices],
661
- outputs=status_text
662
- )
663
-
664
- delete_btn.click(
665
- delete_images,
666
- inputs=[selected_dog, selected_indices],
667
- outputs=status_text
668
- )
669
-
670
- save_btn.click(
671
- lambda: (self.save_database(), "✅ Saved to database!")[1],
672
- outputs=status_text
673
- )
674
-
675
- # ========== STEP 3: Export ==========
676
- with gr.Tab("📦 Export Dataset"):
677
- gr.Markdown("### Export your dataset for training")
678
-
679
- include_csv = gr.Checkbox(label="Include CSV file", value=True)
680
-
681
- export_btn = gr.Button("📥 Export Dataset", variant="primary", size="lg")
682
-
683
- download_file = gr.File(label="Download", interactive=False)
684
- export_status = gr.HTML()
685
-
686
- def export_wrapper(csv):
687
- zip_path, num_dogs, num_images = self.export_dataset(csv)
688
- html = f"""
689
- <div style='padding: 15px; background: #e3f2fd; border-radius: 8px;'>
690
- <h3>✅ Export Complete!</h3>
691
- <p>Dogs: <b>{num_dogs}</b></p>
692
- <p>Images: <b>{num_images}</b></p>
693
- <p>Ready to download!</p>
694
- </div>
695
- """
696
- return zip_path, html
697
-
698
- export_btn.click(
699
- export_wrapper,
700
- inputs=include_csv,
701
- outputs=[download_file, export_status]
702
- )
703
 
704
- return app
705
 
706
 
707
  # Main entry point
708
  if __name__ == "__main__":
709
- creator = SimpleDatasetCreator()
710
- app = creator.create_interface()
711
  app.launch(
712
  server_name="0.0.0.0",
713
  server_port=7860,
 
1
  """
2
+ Simplified Dog Detection Demo with MegaDescriptor
3
  """
4
  import gradio as gr
5
  import cv2
6
  import numpy as np
 
 
 
7
  import torch
8
  from pathlib import Path
9
+ from typing import Dict
 
 
10
  import gc
11
+ import base64
12
+ from io import BytesIO
13
+ from PIL import Image
14
 
15
+ # Import modules
16
  from detection import DogDetector
17
  from tracking import SimpleTracker
18
+ from reid import MegaDescriptorReID
 
19
 
20
 
21
+ class DogDetectionDemo:
22
+ """Simplified demo for dog detection and ReID"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
  def __init__(self):
25
+ # Initialize components
 
 
 
 
 
 
 
 
 
26
  device = 'cuda' if torch.cuda.is_available() else 'cpu'
27
  self.detector = DogDetector(device=device)
28
  self.tracker = SimpleTracker()
29
+ self.reid = MegaDescriptorReID(device=device)
30
+
31
+ # Temporary storage for current session
32
+ self.current_dogs = {} # dog_id -> list of images
33
+
34
+ def reset_session(self):
35
+ """Reset everything for new video or parameter change"""
36
+ self.current_dogs.clear()
37
+ self.tracker.reset()
38
+ self.reid.reset_all()
39
+ gc.collect()
40
+ if torch.cuda.is_available():
41
+ torch.cuda.empty_cache()
42
+ print("🔄 Session reset")
43
 
44
+ def process_video(self, video_path: str, reid_threshold: float, sample_rate: int):
45
+ """Process video and extract dog images"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  if not video_path:
47
+ return None, "Please upload a video"
48
 
49
+ # Reset for new processing
50
+ self.reset_session()
 
 
51
 
52
  # Set ReID threshold
53
  self.reid.set_all_thresholds(reid_threshold)
54
 
55
  # Process video
56
  cap = cv2.VideoCapture(video_path)
 
57
  total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
58
 
59
+ dog_crops = {} # dog_id -> list of crops
60
  frame_num = 0
61
+ processed_frames = 0
62
 
63
  while cap.isOpened():
64
  ret, frame = cap.read()
 
67
 
68
  # Process every N frames
69
  if frame_num % sample_rate == 0:
70
+ # Detect dogs
71
  detections = self.detector.detect(frame)
72
+
73
+ # Update tracks
74
  tracks = self.tracker.update(detections)
75
 
76
+ # Process each track
77
  for track in tracks:
78
  # Get ReID result
79
+ result = self.reid.match_or_register_all(track)
80
+ dog_id = result['MegaDescriptor']['dog_id']
 
81
 
82
+ if dog_id > 0:
83
  # Get latest detection with crop
84
  for det in reversed(track.detections[-3:]):
85
  if det.image_crop is not None:
86
+ if dog_id not in dog_crops:
87
+ dog_crops[dog_id] = []
 
 
 
 
 
88
 
89
+ # Store crop (max 10 per dog)
90
+ if len(dog_crops[dog_id]) < 10:
91
+ dog_crops[dog_id].append(det.image_crop.copy())
 
 
 
92
  break
93
 
94
+ processed_frames += 1
95
+
96
+ # Memory cleanup every 100 frames
97
  if frame_num % 100 == 0:
98
  gc.collect()
99
  if torch.cuda.is_available():
 
101
 
102
  frame_num += 1
103
 
104
+ # Show progress
105
  if frame_num % 30 == 0:
106
  progress = int((frame_num / total_frames) * 100)
107
+ print(f"Processing: {progress}%")
108
 
109
  cap.release()
110
 
111
+ # Convert crops to RGB for display
112
+ self.current_dogs = {}
113
+ for dog_id, crops in dog_crops.items():
114
+ self.current_dogs[dog_id] = [cv2.cvtColor(crop, cv2.COLOR_BGR2RGB) for crop in crops]
115
 
116
+ # Create gallery HTML
117
+ gallery_html = self._create_gallery_html()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
 
119
+ stats_msg = f"✅ Found {len(self.current_dogs)} dogs | Processed {processed_frames} frames"
 
120
 
121
+ return gallery_html, stats_msg
 
 
 
 
 
122
 
123
+ def _create_gallery_html(self):
124
+ """Create HTML gallery of detected dogs"""
125
+ if not self.current_dogs:
126
+ return "<p style='text-align:center; padding:20px;'>No dogs detected</p>"
127
+
128
+ html = """
129
+ <div style='padding: 20px;'>
130
+ <h2 style='text-align:center; color:#2196F3;'>🐕 Detected Dogs</h2>
131
+ <div style='display: grid; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: 20px; margin-top: 20px;'>
132
+ """
133
+
134
+ for dog_id, images in self.current_dogs.items():
135
+ html += f"""
136
+ <div style='border: 2px solid #2196F3; border-radius: 10px; padding: 15px; background: #f5f5f5;'>
137
+ <h3 style='margin: 0 0 10px 0; color: #1976D2;'>Dog ID: {dog_id}</h3>
138
+ <p style='margin: 5px 0; color: #666;'>Images captured: {len(images)}</p>
139
+ <div style='display: grid; grid-template-columns: repeat(5, 1fr); gap: 5px; margin-top: 10px;'>
140
+ """
141
 
142
+ for img in images:
143
+ img_base64 = self._img_to_base64(img)
144
+ html += f"""
145
+ <img src='data:image/jpeg;base64,{img_base64}'
146
+ style='width: 100%; aspect-ratio: 1; object-fit: cover; border-radius: 5px;'>
147
+ """
 
 
148
 
149
+ html += "</div></div>"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
150
 
151
+ html += "</div></div>"
152
+ return html
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
153
 
154
  def _img_to_base64(self, img):
155
+ """Convert image to base64"""
 
 
 
 
156
  pil_img = Image.fromarray(img)
157
  buffered = BytesIO()
158
+ pil_img.save(buffered, format="JPEG", quality=85)
159
+ return base64.b64encode(buffered.getvalue()).decode()
 
160
 
161
  def create_interface(self):
162
  """Create simplified Gradio interface"""
163
+ with gr.Blocks(title="Dog Detection Demo", theme=gr.themes.Soft()) as app:
164
+ gr.Markdown(
165
+ """
166
+ # 🐕 Dog Detection & Tracking Demo
167
+ ### Using MegaDescriptor for Individual Dog Recognition
168
+ Upload a video to detect and track individual dogs. Each dog gets a unique ID.
169
+ """
170
+ )
171
 
172
+ with gr.Row():
173
+ with gr.Column(scale=1):
174
+ video_input = gr.Video(label="Upload Video")
175
+
176
+ reid_threshold = gr.Slider(
177
+ 0.3, 0.8, 0.6, step=0.05,
178
+ label="ReID Matching Threshold",
179
+ info="Lower = more lenient matching"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
  )
181
 
182
+ sample_rate = gr.Slider(
183
+ 1, 5, 2, step=1,
184
+ label="Frame Sample Rate",
185
+ info="Process every N frames (higher = faster)"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
186
  )
187
 
188
+ process_btn = gr.Button("🚀 Process Video", variant="primary", size="lg")
 
 
 
189
 
190
+ with gr.Column(scale=2):
191
+ status_text = gr.Textbox(label="Status", interactive=False)
192
+ gallery_output = gr.HTML(label="Detected Dogs")
193
+
194
+ # Process video on button click
195
+ process_btn.click(
196
+ self.process_video,
197
+ inputs=[video_input, reid_threshold, sample_rate],
198
+ outputs=[gallery_output, status_text]
199
+ )
200
+
201
+ # Auto-reset when parameters change
202
+ video_input.change(fn=lambda: (None, "Ready for new video"), outputs=[gallery_output, status_text])
203
+ reid_threshold.change(fn=lambda: (None, "Parameters changed - upload video to process"), outputs=[gallery_output, status_text])
204
+ sample_rate.change(fn=lambda: (None, "Parameters changed - upload video to process"), outputs=[gallery_output, status_text])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
205
 
206
+ return app
207
 
208
 
209
  # Main entry point
210
  if __name__ == "__main__":
211
+ demo = DogDetectionDemo()
212
+ app = demo.create_interface()
213
  app.launch(
214
  server_name="0.0.0.0",
215
  server_port=7860,