"""
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()