""" Simplified Dog Tracking for Training Dataset Collection - Process video with adjustable threshold - Temporary storage with discard option - Manual validation with checkbox selection per image - Export to folder structure for fine-tuning - Download to laptop as ZIP - Automatic HuggingFace backup/restore - Visualization video with colored tracking boxes """ import os os.environ["OMP_NUM_THREADS"] = "1" import zipfile import tempfile import gradio as gr import cv2 import numpy as np import torch from typing import Dict, List import gc import base64 from io import BytesIO from PIL import Image from pathlib import Path import json from datetime import datetime from detection import DogDetector from tracking import DeepSORTTracker from reid import SimplifiedReID from database import DogDatabase class DatasetCollectionApp: """Simplified app for collecting training datasets""" def __init__(self): self.device = 'cuda' if torch.cuda.is_available() else 'cpu' # Restore database before initializing self._restore_database() self.detector = DogDetector(device=self.device) self.tracker = DeepSORTTracker( max_iou_distance=0.5, max_age=90, n_init=1, use_appearance=True ) self.reid = SimplifiedReID(device=self.device) self.db = DogDatabase('dog_monitoring.db') # Temporary session storage (in-memory) self.temp_session = {} self.current_video_path = None self.is_processing = False # Validation state: stores checkbox states for each temp_id self.validation_data = {} # {temp_id: [bool, bool, ...]} print("Dataset Collection App initialized") print(f"Database has {len(self.db.get_all_dogs())} dogs") def create_visualization_video(self, video_path: str, sample_rate: int) -> str: """Create visualization video with bold boxes from tracking.py only (no ReID, no labels)""" try: cap = cv2.VideoCapture(video_path) if not cap.isOpened(): print("ERROR: Cannot open input video") return None fps = cap.get(cv2.CAP_PROP_FPS) or 30 width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) output_path = f"visualization_{datetime.now().strftime('%Y%m%d_%H%M%S')}.mp4" # Try codecs codecs = ['mp4v', 'XVID', 'MJPG'] out = None for codec in codecs: fourcc = cv2.VideoWriter_fourcc(*codec) out = cv2.VideoWriter(output_path, fourcc, fps, (width, height)) if out.isOpened(): print(f"Using codec: {codec}") break else: out.release() out = None if out is None or not out.isOpened(): print("ERROR: Could not initialize VideoWriter with any codec") cap.release() return None # Tracker only (no ReID) viz_tracker = DeepSORTTracker( max_iou_distance=0.5, max_age=90, n_init=1, use_appearance=False ) track_colors = {} frame_num = 0 print("\nCreating visualization video (tracking IDs only)...") while cap.isOpened(): ret, frame = cap.read() if not ret: break if frame_num % sample_rate == 0: detections = self.detector.detect(frame) tracks = viz_tracker.update(detections) for track in tracks: tid = track.track_id if tid not in track_colors: track_colors[tid] = ( np.random.randint(50, 255), np.random.randint(50, 255), np.random.randint(50, 255) ) color = track_colors[tid] x1, y1, x2, y2 = map(int, track.bbox) # Bold box only cv2.rectangle(frame, (x1, y1), (x2, y2), color, 8) out.write(frame) frame_num += 1 if frame_num % 30 == 0: print(f"Visualization progress: {frame_num} frames") cap.release() out.release() if os.path.exists(output_path) and os.path.getsize(output_path) > 1000: print(f"Visualization video saved: {output_path}") return output_path else: print("ERROR: Video file not created or is empty") return None except Exception as e: print(f"Visualization video error: {e}") import traceback traceback.print_exc() return None def stop_processing(self): """Stop video processing""" if self.is_processing: self.is_processing = False return "İşlem kullanıcı tarafından durduruldu", "Durduruldu", None else: return "Durdurulaack işlem yok", "İşlem yapılmıyor", None def clear_reset(self): """Clear all temporary data and reset UI""" self.temp_session.clear() self.tracker.reset() self.reid.reset_session() self.current_video_path = None self.validation_data = {} gc.collect() if torch.cuda.is_available(): torch.cuda.empty_cache() return ( None, "

Oturum temizlendi. Başlamak için yeni bir video yükleyin.

", "", "", gr.update(visible=False) ) def discard_session(self): """Discard temporary session completely""" count = len(self.temp_session) self.temp_session.clear() self.tracker.reset() self.reid.reset_session() self.validation_data = {} gc.collect() if torch.cuda.is_available(): torch.cuda.empty_cache() return ( gr.update(visible=False), f"{count} geçici köpek iptal edildi. Farklı bir eşik deneyin.", gr.update(visible=False) ) def process_video(self, video_path: str, reid_threshold: float, sample_rate: int): """Process video and store in temporary session""" if not video_path: return None, "Lütfen bir video yükleyin", "", gr.update(visible=False), None self.is_processing = True self.current_video_path = video_path self.temp_session.clear() self.validation_data = {} self.reid.set_threshold(reid_threshold) self.reid.set_video_source(video_path) self.tracker.reset() self.reid.reset_session() try: cap = cv2.VideoCapture(video_path) if not cap.isOpened(): return None, "Video açılamıyor", "", gr.update(visible=False), None total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) fps = cap.get(cv2.CAP_PROP_FPS) or 30 frame_num = 0 processed = 0 temp_dogs = {} min_frame_gap = max(15, total_frames // 45) blur_rejected = 0 print(f"\nProcessing video: {total_frames} frames, {fps} fps") print(f"Minimum frame gap: {min_frame_gap} frames") while cap.isOpened() and self.is_processing: ret, frame = cap.read() if not ret: break if frame_num % sample_rate == 0: detections = self.detector.detect(frame) tracks = self.tracker.update(detections) for track in tracks: if not self.is_processing: break result = self.reid.match_or_register(track) temp_id = result['temp_id'] if temp_id == 0: continue if temp_id not in temp_dogs: temp_dogs[temp_id] = { 'images': [], 'timestamps': [], 'confidences': [], 'bboxes': [], 'frame_numbers': [], 'last_captured_frame': -1 } if len(temp_dogs[temp_id]['images']) >= 30: continue frames_since_last = frame_num - temp_dogs[temp_id]['last_captured_frame'] if frames_since_last < min_frame_gap: continue image_crop = None for det in reversed(track.detections[-3:]): if det.image_crop is not None: image_crop = det.image_crop break if image_crop is None: continue gray = cv2.cvtColor(image_crop, cv2.COLOR_BGR2GRAY) laplacian_var = cv2.Laplacian(gray, cv2.CV_64F).var() if laplacian_var < 75: blur_rejected += 1 continue temp_dogs[temp_id]['images'].append(image_crop.copy()) temp_dogs[temp_id]['timestamps'].append(frame_num / fps) temp_dogs[temp_id]['confidences'].append(det.confidence) temp_dogs[temp_id]['bboxes'].append(det.bbox) temp_dogs[temp_id]['frame_numbers'].append(frame_num) temp_dogs[temp_id]['last_captured_frame'] = frame_num processed += 1 frame_num += 1 if frame_num % 30 == 0: progress = int((frame_num / total_frames) * 100) print(f"Progress: {progress}%") cap.release() for temp_id in list(temp_dogs.keys()): if 'last_captured_frame' in temp_dogs[temp_id]: del temp_dogs[temp_id]['last_captured_frame'] original_count = len(temp_dogs) discarded_ids = [] for temp_id in list(temp_dogs.keys()): if len(temp_dogs[temp_id]['images']) < 14: discarded_ids.append(temp_id) del temp_dogs[temp_id] discarded_count = len(discarded_ids) self.temp_session = temp_dogs for temp_id in temp_dogs.keys(): self.validation_data[temp_id] = [True] * len(temp_dogs[temp_id]['images']) summary = f"İşlem tamamlandı!\n" summary += f"Başlangıçta {original_count} köpek tespit edildi\n" if discarded_count > 0: summary += f"14'ten az resimli {discarded_count} köpek iptal edildi (ID'ler: {discarded_ids})\n" summary += f"14+ resimli {len(temp_dogs)} köpek tutuldu\n" summary += f"{processed} kare işlendi\n" summary += f"Kare aralığı: {min_frame_gap} kare\n" summary += f"Bulanık resimler reddedildi: {blur_rejected}\n\n" if len(temp_dogs) == 0: summary += "Hiçbir köpek minimum 14 resim gereksinimini karşılamadı.\n" summary += "ReID eşiğini ayarlamayı veya daha uzun bir video kullanmayı deneyin." show_validation = False else: summary += "Sonuçlar GEÇİCİ oturumda saklandı\n" summary += "Kaydetmeden önce resimleri incelemek ve seçmek için Sekme 2'ye gidin" show_validation = True gallery_html = self._create_temp_gallery() viz_video_path = self.create_visualization_video(video_path, sample_rate) gc.collect() if torch.cuda.is_available(): torch.cuda.empty_cache() return ( gallery_html, summary, "Doğrulama için hazır" if len(temp_dogs) > 0 else "Geçerli köpek yok", gr.update(visible=show_validation), viz_video_path ) except Exception as e: import traceback error = f"Hata: {str(e)}\n{traceback.format_exc()}" return None, error, "", gr.update(visible=False), None finally: self.is_processing = False def _create_temp_gallery(self) -> str: """Create gallery from temporary session""" if not self.temp_session: return "

Geçici oturumda köpek yok

" html = "
" html += "

GEÇİCİ OTURUM - Henüz Kaydedilmedi

" html += f"

Tespit edilen köpekler: {len(self.temp_session)}

" html += "
" for temp_id in sorted(self.temp_session.keys()): dog_data = self.temp_session[temp_id] images = dog_data['images'] display_images = images[:10] html += f"""

Geçici Köpek #{temp_id} (GEÇİCİ)

Toplam resim: {len(images)}

İlk {len(display_images)} resim gösteriliyor

""" for img in display_images: img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img_base64 = self._img_to_base64(img_rgb) html += f""" """ html += "
" html += "
" return html def load_validation_interface(self): """Load validation interface with checkbox selection""" if not self.temp_session: return ( gr.update(visible=False), "Doğrulanacak geçici oturum yok. Önce bir video işleyin.", "" ) html = "
" html += "

Resimleri İnceleyin ve Seçin

" html += "

Tutmak/atmak için resimleri işaretleyin/işaretini kaldırın. Hepsi varsayılan olarak seçilidir.

" html += "
" status = f"{len(self.temp_session)} köpek doğrulama için yüklendi. İnceleyin ve hazır olduğunuzda 'Seçili Fotoğrafları Kaydet' düğmesine tıklayın." return ( gr.update(visible=True), status, html ) def save_validated_to_database(self, *checkbox_states): """Save validated images to permanent database""" if not self.temp_session: return "Kaydedilecek geçici oturum yok", gr.update() try: saved_count = 0 total_images_saved = 0 checkbox_idx = 0 for temp_id in sorted(self.temp_session.keys()): dog_data = self.temp_session[temp_id] num_images = len(dog_data['images']) selected_indices = [] for i in range(num_images): if checkbox_idx < len(checkbox_states) and checkbox_states[checkbox_idx]: selected_indices.append(i) checkbox_idx += 1 if not selected_indices: continue dog_id = self.db.add_dog( name=f"Kopek_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{temp_id}" ) for idx in selected_indices: self.db.add_dog_image( dog_id=dog_id, image=dog_data['images'][idx], timestamp=dog_data['timestamps'][idx], confidence=dog_data['confidences'][idx], bbox=dog_data['bboxes'][idx] ) total_images_saved += 1 saved_count += 1 self.temp_session.clear() self.validation_data = {} self._backup_database() db_html = self._show_database() summary = f"✅ {saved_count} köpek ve {total_images_saved} seçili resim başarıyla kalıcı veritabanına kaydedildi!" gc.collect() if torch.cuda.is_available(): torch.cuda.empty_cache() return summary, gr.update(value=db_html, visible=True) except Exception as e: import traceback error = f"Kayıt hatası: {str(e)}\n{traceback.format_exc()}" return error, gr.update() def _backup_database(self): """Backup database to HuggingFace""" try: from huggingface_hub import HfApi hf_token = os.getenv('HF_TOKEN') if not hf_token: print("Uyarı: HF_TOKEN bulunamadı, yedekleme atlanıyor") return api = HfApi() repo_id = "mustafa2ak/dog-dataset-backup" api.upload_file( path_or_fileobj='dog_monitoring.db', path_in_repo='dog_monitoring.db', repo_id=repo_id, repo_type='dataset', token=hf_token ) print(f"✅ Veritabanı {repo_id} adresine yedeklendi") except Exception as e: print(f"Yedekleme başarısız: {str(e)}") def _restore_database(self): """Restore database from HuggingFace""" try: from huggingface_hub import hf_hub_download hf_token = os.getenv('HF_TOKEN') if not hf_token: print("HF_TOKEN bulunamadı, yeni veritabanı ile başlanıyor") return repo_id = "mustafa2ak/dog-dataset-backup" db_path = hf_hub_download( repo_id=repo_id, filename='dog_monitoring.db', repo_type='dataset', token=hf_token ) import shutil shutil.copy(db_path, 'dog_monitoring.db') print(f"✅ Veritabanı {repo_id} adresinden geri yüklendi") except Exception as e: print(f"Yedek bulunamadı veya geri yükleme başarısız: {str(e)}") def _show_database(self) -> str: """Show current database contents""" dogs = self.db.get_all_dogs() if dogs.empty: return "

Veritabanında henüz köpek yok

" html = "
" html += f"

Kalıcı Veritabanı ({len(dogs)} köpek)

" html += "
" for _, dog in dogs.iterrows(): images = self.db.get_dog_images(dog['dog_id']) display_count = min(6, len(images)) html += f"""

{dog['name']}

ID: {dog['dog_id']}

Resimler: {len(images)}

İlk görülme: {dog['first_seen']}

""" for img_data in images[:display_count]: img = img_data['image'] img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img_base64 = self._img_to_base64(img_rgb) html += f""" """ html += "
" html += "
" return html def export_dataset(self): """Export dataset as downloadable ZIP file""" try: dogs = self.db.get_all_dogs() if dogs.empty: return "Veritabanında dışa aktarılacak köpek yok", None zip_buffer = BytesIO() with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zipf: total_images = 0 export_info = [] for _, dog in dogs.iterrows(): dog_id = dog['dog_id'] dog_name = dog['name'] or f"kopek_{dog_id}" safe_name = "".join(c if c.isalnum() or c in ('_', '-') else '_' for c in dog_name) images = self.db.get_dog_images( dog_id=dog_id, validated_only=False, include_discarded=False ) if not images: continue for idx, img_data in enumerate(images): image = img_data['image'] img_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) pil_image = Image.fromarray(img_rgb) img_buffer = BytesIO() pil_image.save(img_buffer, format='JPEG', quality=95) img_bytes = img_buffer.getvalue() filename = f"egitim_veri_seti/{safe_name}/resim_{idx+1:04d}.jpg" zipf.writestr(filename, img_bytes) total_images += 1 export_info.append({ 'dog_id': int(dog_id), 'name': dog_name, 'image_count': len(images) }) print(f"Exported {len(images)} images for {dog_name}") metadata = { 'export_date': datetime.now().isoformat(), 'total_dogs': len(dogs), 'total_images': total_images, 'dogs': export_info } zipf.writestr('egitim_veri_seti/metadata.json', json.dumps(metadata, indent=2, ensure_ascii=False)) zip_buffer.seek(0) temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.zip', prefix='kopek_veri_seti_') temp_file.write(zip_buffer.getvalue()) temp_file.close() summary = f"✅ Veri seti başarıyla dışa aktarıldı!\n\n" summary += f"📦 Toplam köpek: {len(dogs)}\n" summary += f"🖼️ Toplam resim: {total_images}\n\n" summary += "Bilgisayarınıza kaydetmek için aşağıdaki indirme düğmesine tıklayın." return summary, temp_file.name except Exception as e: import traceback error = f"Dışa aktarma hatası: {str(e)}\n{traceback.format_exc()}" return error, None def _img_to_base64(self, img_array: np.ndarray) -> str: """Convert image array to base64 string""" img_pil = Image.fromarray(img_array) buffered = BytesIO() img_pil.save(buffered, format="JPEG", quality=85) return base64.b64encode(buffered.getvalue()).decode() def create_interface(self): """Create Gradio interface with improved Turkish labels""" with gr.Blocks(title="Köpek Veri Seti Toplama", theme=gr.themes.Soft()) as app: gr.Markdown(""" # Yapay zeka ile sokak köpeği takibi Üç aşamada tamamlanacak. **📊 1. aşama – veri toplama (şu anki aşama)** Yapay zeka modeli eğitimi için temiz ve etiketli veri seti oluşturuluyor. **🤖 2. aşama – yapay zeka modeli eğitimi** Sistem %90 başarı oranıyla her köpeği bireysel olarak ayırt edebilecek. **📍 3. aşama – sahada kullanım** Şehir genelinde kalıcı, otomatik köpek takip sistemi ve veritabanı oluşturulacak. --- """) with gr.Tabs(): # TAB 1 with gr.Tab("1️⃣ Video yükle ve analiz et"): gr.Markdown("### Köpekleri tespit etmek için video yükleyin ve analiz edin") with gr.Row(): with gr.Column(): video_display = gr.Video( label="📹 Video yükle veya sonuçları izle", sources=["upload"], autoplay=True, loop=True ) with gr.Row(): reid_threshold = gr.Slider( minimum=0.1, maximum=0.9, value=0.3, step=0.05, label="🎯 Köpek tanıma hassasiyeti" ) sample_rate = gr.Slider( minimum=1, maximum=10, value=3, step=1, label="⏱️ Saniyede kaç kare işlensin?" ) with gr.Row(): process_btn = gr.Button("⚙️ Videoyu analiz et", variant="primary", size="lg") stop_btn = gr.Button("🛑 İşlemi durdur", variant="stop") clear_btn = gr.Button("🔄 Yeniden başla") progress_text = gr.Textbox(label="⏳ İşlem durumu", lines=1) status_text = gr.Textbox(label="📊 Detaylı bilgi", lines=8) with gr.Column(): gallery_output = gr.HTML(label="🐕 Bulunan köpekler") with gr.Row(): discard_btn = gr.Button("↩️ Farklı ayarlarla tekrar dene", variant="secondary") # TAB 2 with gr.Tab("2️⃣ Fotoğrafları incele ve kaydet"): gr.Markdown("### Tespit edilen köpeklerin fotoğraflarını inceleyin ve kaydetmek istediklerinizi seçin") with gr.Column(visible=False) as validation_container: validation_status = gr.Textbox(label="📋 Durum", lines=2) load_btn = gr.Button("✅ Fotoğrafları incele ve seç", variant="primary", size="lg") @gr.render(inputs=[], triggers=[load_btn.click]) def render_validation(): if not self.temp_session: gr.Markdown("Geçici oturum yok. Önce bir video işleyin.") return checkboxes = [] for temp_id in sorted(self.temp_session.keys()): dog_data = self.temp_session[temp_id] images = dog_data['images'] with gr.Group(): gr.Markdown(f"### 🐕 Köpek #{temp_id} - {len(images)} resim") for i in range(0, len(images), 6): with gr.Row(): for j in range(6): if i + j < len(images): img = images[i + j] img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) with gr.Column(scale=1, min_width=120): gr.Image( value=img_rgb, label=f"#{i+j+1}", interactive=False, height=150, show_download_button=False ) cb = gr.Checkbox( label="Tut", value=True, elem_id=f"cb_{temp_id}_{i+j}" ) checkboxes.append(cb) save_btn = gr.Button("💾 Seçili fotoğrafları kaydet", variant="primary", size="lg") save_status = gr.Textbox(label="Kayıt durumu", lines=3) save_btn.click( fn=self.save_validated_to_database, inputs=checkboxes, outputs=[save_status, validation_container] ) # TAB 3 with gr.Tab("3️⃣ Kayıtları gör ve indir"): gr.Markdown("### Kaydedilmiş köpekleri görüntüleyin ve veri setini bilgisayarınıza indirin") refresh_db_btn = gr.Button("🔄 Kaydedilenleri göster", variant="secondary") database_display = gr.HTML(label="Veritabanı içeriği", visible=False) gr.Markdown("---") export_btn = gr.Button("📦 Veri setini indir (ZIP)", variant="primary", size="lg") export_status = gr.Textbox(label="İndirme durumu", lines=5) download_btn = gr.File(label="ZIP dosyasını bilgisayara kaydet", interactive=False) # Event handlers process_btn.click( fn=self.process_video, inputs=[video_display, reid_threshold, sample_rate], outputs=[ gallery_output, status_text, progress_text, validation_container, video_display ] ) stop_btn.click( fn=self.stop_processing, outputs=[status_text, progress_text, gallery_output] ) clear_btn.click( fn=self.clear_reset, outputs=[ video_display, gallery_output, status_text, progress_text, validation_container ] ) discard_btn.click( fn=self.discard_session, outputs=[validation_container, status_text, database_display] ) load_btn.click( fn=self.load_validation_interface, outputs=[validation_container, validation_status, gr.HTML()] ) refresh_db_btn.click( fn=lambda: gr.update(value=self._show_database(), visible=True), outputs=[database_display] ) export_btn.click( fn=self.export_dataset, outputs=[export_status, download_btn] ) return app def launch(self): """Launch the Gradio app""" app = self.create_interface() app.launch(share=False, server_name="0.0.0.0", server_port=7860) if __name__ == "__main__": app = DatasetCollectionApp() app.launch()