HaWkEye / object_identifier.py
HaWkEye Admin
Deploy HaWkEye
97c8cf9
Raw
History Blame Contribute Delete
28.1 kB
import cv2
import numpy as np
import os
import json
import time
import threading
import torch
from PIL import Image
from ultralytics import YOLO
import customtkinter as ctk
from tkinter import messagebox
# ===============================================
# SMART OBJECT IDENTIFIER v2
# - YOLOv8 real-time detection (80 classes)
# - Teach unknown objects (type on camera)
# - Internet recognition via CLIP
# - Persistent learning (saves to disk)
# ===============================================
# -------------------------
# Config
# -------------------------
WEBCAM_INDEX = 0
FRAME_WIDTH = 1280
FRAME_HEIGHT = 720
YOLO_CONFIDENCE = 0.40
CUSTOM_MATCH_THRESHOLD = 0.82
CLIP_MODEL_NAME = "openai/clip-vit-base-patch32"
# -------------------------
# Paths
# -------------------------
OBJECTS_FOLDER = "learned_objects"
OBJECTS_JSON = "objects_db.json"
os.makedirs(OBJECTS_FOLDER, exist_ok=True)
if not os.path.exists(OBJECTS_JSON):
with open(OBJECTS_JSON, "w") as f:
json.dump([], f)
# -------------------------
# Device
# -------------------------
device = "cuda" if torch.cuda.is_available() else "cpu"
print(f"Using device: {device}")
# -------------------------
# Load YOLOv8
# -------------------------
print("Loading YOLOv8 model...")
yolo_model = YOLO("yolov8n.pt")
print("YOLOv8 loaded!\n")
# -------------------------
# CLIP (Lazy Load)
# -------------------------
clip_model = None
clip_processor = None
clip_loaded = False
def load_clip():
global clip_model, clip_processor, clip_loaded
if clip_loaded:
return True
try:
from transformers import CLIPProcessor, CLIPModel
print("Loading CLIP model (first time downloads ~350MB)...")
clip_model = CLIPModel.from_pretrained(CLIP_MODEL_NAME).to(device)
clip_processor = CLIPProcessor.from_pretrained(CLIP_MODEL_NAME)
clip_model.eval()
clip_loaded = True
print("CLIP loaded!\n")
return True
except Exception as e:
print(f"Failed to load CLIP: {e}")
return False
# -------------------------
# CLIP Vocabulary
# -------------------------
CLIP_VOCABULARY = [
"smartphone", "laptop", "tablet", "keyboard", "mouse", "monitor", "headphones",
"earbuds", "charger", "USB cable", "power bank", "speaker", "microphone",
"webcam", "router", "remote control", "game controller", "smartwatch",
"calculator", "hard drive", "flash drive", "printer", "camera",
"pen", "pencil", "eraser", "ruler", "notebook", "book", "paper",
"stapler", "scissors", "tape", "glue stick", "marker", "highlighter",
"folder", "envelope", "paperclip", "sticky note",
"chair", "table", "desk", "sofa", "bed", "shelf", "cabinet",
"lamp", "fan", "mirror", "curtain", "pillow", "blanket",
"bottle", "cup", "mug", "glass", "plate", "bowl", "spoon", "fork",
"knife", "pan", "pot", "kettle", "toaster", "blender", "microwave",
"refrigerator", "stove", "cutting board", "thermos",
"apple", "banana", "orange", "bread", "sandwich", "pizza", "burger",
"rice", "chips", "biscuit", "chocolate", "candy", "cake",
"water bottle", "juice box", "soda can", "coffee cup",
"shirt", "jacket", "hoodie", "jeans", "shoe", "sneaker",
"hat", "cap", "sunglasses", "watch", "wallet", "bag", "backpack",
"belt", "tie", "scarf", "gloves",
"toothbrush", "comb", "soap", "towel", "tissue box", "umbrella",
"key", "keychain", "ID card", "coin",
"ball", "football", "basketball", "cricket bat", "tennis racket",
"badminton racket", "yoga mat", "dumbbell",
"toy car", "stuffed animal", "rubik's cube", "playing cards",
"hammer", "screwdriver", "wrench", "pliers", "drill",
"measuring tape", "nail", "screw",
"bicycle", "helmet", "plant", "flower", "leaf", "rock",
"clock", "calendar", "photo frame", "vase", "candle", "lighter",
"dustbin", "broom", "bucket", "hanger", "iron", "battery", "bulb",
"medicine box", "bandage", "thermometer", "mask",
"box", "container", "jar", "tin can", "plastic bag", "rope",
"wire", "chain", "magnet", "compass", "globe", "trophy", "medal",
"airpods", "earphone case", "phone case", "screen protector",
"mouse pad", "laptop stand", "ring light", "tripod",
]
# -------------------------
# Threaded Camera
# -------------------------
class FastCapture:
def __init__(self, src=0, width=1280, height=720):
self.cap = cv2.VideoCapture(src)
self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, width)
self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, height)
self.cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
self.ret = False
self.frame = None
self.lock = threading.Lock()
self.running = True
self.ret, self.frame = self.cap.read()
self.thread = threading.Thread(target=self._update, daemon=True)
self.thread.start()
def _update(self):
while self.running:
ret, frame = self.cap.read()
with self.lock:
self.ret = ret
self.frame = frame
def read(self):
with self.lock:
if self.frame is not None:
return self.ret, self.frame.copy()
return False, None
def release(self):
self.running = False
self.thread.join(timeout=2)
self.cap.release()
# ===================================================
# LEARNED OBJECTS DATABASE (Persistent)
# ===================================================
# Each object: {name, embedding (.npy file), image}
# Embeddings saved as .npy files so they survive restarts
# without needing CLIP to reload them.
# ===================================================
learned_objects = [] # [{name, embedding (numpy), image_path}]
def load_learned_objects():
"""Load learned objects from disk. Embeddings stored as .npy files."""
global learned_objects
learned_objects = []
if not os.path.exists(OBJECTS_JSON):
return
with open(OBJECTS_JSON, "r") as f:
entries = json.load(f)
for entry in entries:
name = entry["name"]
image_path = entry.get("image", "")
embedding_path = entry.get("embedding_file", "")
# Load saved embedding from .npy file
if embedding_path and os.path.exists(embedding_path):
embedding = np.load(embedding_path)
learned_objects.append({
"name": name,
"embedding": embedding,
"image_path": image_path
})
print(f" [OK] Loaded: {name}")
else:
print(f" [SKIP] No embedding file for: {name}")
print(f"Loaded {len(learned_objects)} custom objects.\n")
def get_clip_embedding(pil_image):
"""Get CLIP embedding for a PIL image."""
if not clip_loaded:
return None
try:
inputs = clip_processor(images=pil_image, return_tensors="pt").to(device)
with torch.no_grad():
emb = clip_model.get_image_features(**inputs)
emb = emb / emb.norm(dim=-1, keepdim=True)
return emb.cpu().numpy()[0]
except Exception as e:
print(f"CLIP error: {e}")
return None
def save_learned_object(name, frame, bbox):
"""
Save a new learned object:
- Crop image saved as .jpg
- CLIP embedding saved as .npy
- Entry added to objects_db.json
"""
x1, y1, x2, y2 = bbox
h, w = frame.shape[:2]
pad = 10
x1, y1 = max(0, x1 - pad), max(0, y1 - pad)
x2, y2 = min(w, x2 + pad), min(h, y2 + pad)
crop = frame[y1:y2, x1:x2]
if crop.size == 0:
return False
timestamp = int(time.time())
image_path = os.path.join(OBJECTS_FOLDER, f"{name}_{timestamp}.jpg")
embedding_path = os.path.join(OBJECTS_FOLDER, f"{name}_{timestamp}.npy")
# Save crop image
cv2.imwrite(image_path, crop)
# Compute and save CLIP embedding
crop_rgb = cv2.cvtColor(crop, cv2.COLOR_BGR2RGB)
pil_crop = Image.fromarray(crop_rgb)
if clip_loaded:
embedding = get_clip_embedding(pil_crop)
if embedding is not None:
np.save(embedding_path, embedding)
# Add to memory
learned_objects.append({
"name": name,
"embedding": embedding,
"image_path": image_path
})
else:
# Save without embedding (image only)
embedding_path = ""
else:
# No CLIP loaded β€” save image only, embedding computed later
embedding_path = ""
# Update JSON
with open(OBJECTS_JSON, "r") as f:
entries = json.load(f)
entries.append({
"name": name,
"image": image_path,
"embedding_file": embedding_path
})
with open(OBJECTS_JSON, "w") as f:
json.dump(entries, f, indent=4)
print(f"Saved object: {name} -> {image_path}")
return True
def match_custom_object(pil_image):
"""Match image against learned objects using CLIP embeddings."""
if len(learned_objects) == 0 or not clip_loaded:
return None, 0.0
embedding = get_clip_embedding(pil_image)
if embedding is None:
return None, 0.0
best_name = None
best_score = -1
for obj in learned_objects:
if obj["embedding"] is not None:
sim = np.dot(embedding, obj["embedding"])
if sim > best_score:
best_score = sim
best_name = obj["name"]
if best_score >= CUSTOM_MATCH_THRESHOLD:
return best_name, float(best_score)
return None, float(best_score)
def identify_from_internet(pil_image, top_k=5):
"""CLIP zero-shot classification against vocabulary."""
if not clip_loaded:
return []
try:
texts = [f"a photo of a {obj}" for obj in CLIP_VOCABULARY]
inputs_img = clip_processor(images=pil_image, return_tensors="pt").to(device)
with torch.no_grad():
img_feat = clip_model.get_image_features(**inputs_img)
img_feat = img_feat / img_feat.norm(dim=-1, keepdim=True)
all_scores = []
batch_size = 50
for i in range(0, len(texts), batch_size):
batch = texts[i:i + batch_size]
inputs_txt = clip_processor(text=batch, return_tensors="pt", padding=True, truncation=True).to(device)
with torch.no_grad():
txt_feat = clip_model.get_text_features(**inputs_txt)
txt_feat = txt_feat / txt_feat.norm(dim=-1, keepdim=True)
sims = (img_feat @ txt_feat.T).squeeze(0)
all_scores.extend(sims.cpu().numpy().tolist())
results = sorted(zip(CLIP_VOCABULARY, all_scores), key=lambda x: x[1], reverse=True)
return results[:top_k]
except Exception as e:
print(f"CLIP identify error: {e}")
return []
# ===================================================
# DRAWING FUNCTIONS
# ===================================================
def draw_detections(frame, detections, fps, selected_idx):
for i, det in enumerate(detections):
name = det["name"]
score = det["score"]
x1, y1, x2, y2 = det["bbox"]
source = det["source"]
if source == "custom":
color = (255, 180, 0) # Blue for learned
label = f"[L] {name} {score:.0%}"
else:
color = (0, 255, 0) # Green for YOLO
label = f"{name} {score:.0%}"
if i == selected_idx:
color = (0, 255, 255) # Yellow highlight
thickness = 3
else:
thickness = 2
cv2.rectangle(frame, (x1, y1), (x2, y2), color, thickness)
(tw, th), _ = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.6, 2)
cv2.rectangle(frame, (x1, y1 - th - 10), (x1 + tw + 6, y1), color, cv2.FILLED)
cv2.putText(frame, label, (x1 + 3, y1 - 5),
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 0), 2)
# FPS
cv2.putText(frame, f"FPS: {fps:.1f}", (10, 30),
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 255), 2)
# Stats
cv2.putText(frame, f"Objects: {len(detections)} | Learned: {len(learned_objects)}",
(10, 55), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (200, 200, 200), 1)
# Controls
help_text = "Tab=Select | T=Teach | I=Auto-ID | ESC=Quit"
cv2.putText(frame, help_text, (10, frame.shape[0] - 15),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (200, 200, 200), 1)
return frame
def draw_input_mode(frame, prompt, typed_text, suggestions=None):
"""Draw text input overlay on the camera frame."""
overlay = frame.copy()
h, w = frame.shape[:2]
# Dark overlay at bottom
cv2.rectangle(overlay, (0, h - 120), (w, h), (30, 30, 30), cv2.FILLED)
frame = cv2.addWeighted(overlay, 0.85, frame, 0.15, 0)
# Prompt
cv2.putText(frame, prompt, (15, h - 90),
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 200, 255), 1)
# Typed text with cursor
display_text = typed_text + "|"
cv2.putText(frame, display_text, (15, h - 55),
cv2.FONT_HERSHEY_SIMPLEX, 0.8, (255, 255, 255), 2)
# Hint
cv2.putText(frame, "Enter=Confirm | Esc=Cancel", (15, h - 20),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (150, 150, 150), 1)
# Show suggestions if available
if suggestions:
for i, (name, score) in enumerate(suggestions):
y = h - 130 - (len(suggestions) - i) * 25
text = f"{i+1}. {name} ({score:.1%})"
cv2.putText(frame, text, (15, y),
cv2.FONT_HERSHEY_SIMPLEX, 0.55, (100, 255, 100), 1)
label_y = h - 130 - len(suggestions) * 25 - 10
cv2.putText(frame, "Type number to pick, or type custom name:",
(15, label_y), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 200, 255), 1)
return frame
# ===================================================
# MAIN DETECTION LOOP
# ===================================================
def run_detection():
cap = FastCapture(WEBCAM_INDEX, FRAME_WIDTH, FRAME_HEIGHT)
selected_idx = -1
fps = 0.0
prev_time = time.time()
frame_count = 0
cached_detections = []
# Input mode state
input_mode = None # None, "teach", or "identify"
typed_text = ""
frozen_frame = None
frozen_bbox = None
clip_suggestions = None # For auto-identify results
print("\nDetection started!")
print(" Tab = Select | T = Teach | I = Auto-ID | ESC = Quit\n")
while True:
# --- INPUT MODE: typing on screen ---
if input_mode is not None:
display = frozen_frame.copy()
# Highlight the selected object
if frozen_bbox:
x1, y1, x2, y2 = frozen_bbox
cv2.rectangle(display, (x1, y1), (x2, y2), (0, 255, 255), 3)
if input_mode == "teach":
display = draw_input_mode(display, "What is this object? Type name:", typed_text)
elif input_mode == "identify":
display = draw_input_mode(display, "Pick a number or type custom name:", typed_text, clip_suggestions)
cv2.imshow("Smart Object Identifier", display)
key = cv2.waitKey(30) & 0xFF
if key == 27: # ESC β€” cancel
input_mode = None
typed_text = ""
clip_suggestions = None
continue
elif key == 13: # ENTER β€” confirm
final_name = typed_text.strip()
# For identify mode, check if number was typed
if input_mode == "identify" and clip_suggestions:
try:
idx = int(final_name) - 1
if 0 <= idx < len(clip_suggestions):
final_name = clip_suggestions[idx][0]
except ValueError:
pass
if final_name and frozen_bbox:
success = save_learned_object(final_name, frozen_frame, frozen_bbox)
if success:
print(f" >> Learned: {final_name}")
input_mode = None
typed_text = ""
clip_suggestions = None
continue
elif key == 8: # BACKSPACE
typed_text = typed_text[:-1]
elif 32 <= key <= 126: # Printable characters
typed_text += chr(key)
continue # Skip normal detection while typing
# --- NORMAL MODE: detect objects ---
ret, frame = cap.read()
if not ret or frame is None:
print("Camera read failed.")
break
frame_count += 1
# Run YOLO every 2 frames
if frame_count % 2 == 0:
results = yolo_model(frame, conf=YOLO_CONFIDENCE, verbose=False)
detections = []
for r in results:
boxes = r.boxes
for box in boxes:
x1, y1, x2, y2 = box.xyxy[0].cpu().numpy().astype(int)
conf = float(box.conf[0])
cls_id = int(box.cls[0])
cls_name = yolo_model.names[cls_id]
# Try custom match if CLIP is loaded
custom_name = None
crop = frame[y1:y2, x1:x2]
if crop.size > 0 and len(learned_objects) > 0 and clip_loaded:
crop_rgb = cv2.cvtColor(crop, cv2.COLOR_BGR2RGB)
pil_crop = Image.fromarray(crop_rgb)
custom_name, custom_score = match_custom_object(pil_crop)
if custom_name:
detections.append({
"name": custom_name,
"score": custom_score,
"bbox": [x1, y1, x2, y2],
"source": "custom"
})
else:
detections.append({
"name": cls_name,
"score": conf,
"bbox": [x1, y1, x2, y2],
"source": "yolo"
})
cached_detections = detections
if selected_idx >= len(cached_detections):
selected_idx = max(0, len(cached_detections) - 1) if cached_detections else -1
# FPS
now = time.time()
dt = now - prev_time
if dt > 0:
fps = 1.0 / dt
prev_time = now
# Draw
display = draw_detections(frame, cached_detections, fps, selected_idx)
cv2.imshow("Smart Object Identifier", display)
key = cv2.waitKey(1) & 0xFF
if key == 27: # ESC
break
elif key == 9: # Tab
if len(cached_detections) > 0:
selected_idx = (selected_idx + 1) % len(cached_detections)
else:
selected_idx = -1
elif key == ord('t') or key == ord('T'):
if 0 <= selected_idx < len(cached_detections):
# Freeze frame and enter teach mode
frozen_frame = frame.copy()
frozen_bbox = cached_detections[selected_idx]["bbox"]
input_mode = "teach"
typed_text = ""
print(" [TEACH MODE] Type the object name on screen, press Enter")
else:
print(" Press Tab first to select an object!")
elif key == ord('i') or key == ord('I'):
if 0 <= selected_idx < len(cached_detections):
if not clip_loaded:
print(" CLIP not loaded! Click '🌐 Load CLIP' in dashboard first.")
else:
# Freeze and run CLIP
frozen_frame = frame.copy()
frozen_bbox = cached_detections[selected_idx]["bbox"]
bx1, by1, bx2, by2 = frozen_bbox
crop = frame[by1:by2, bx1:bx2]
if crop.size > 0:
crop_rgb = cv2.cvtColor(crop, cv2.COLOR_BGR2RGB)
pil_crop = Image.fromarray(crop_rgb)
print(" [AUTO-ID] Running CLIP analysis...")
clip_suggestions = identify_from_internet(pil_crop, top_k=5)
if clip_suggestions:
input_mode = "identify"
typed_text = ""
print(" CLIP suggestions ready β€” pick a number or type name")
else:
print(" CLIP returned no results.")
frozen_frame = None
frozen_bbox = None
else:
print(" Press Tab first to select an object!")
cap.release()
cv2.destroyAllWindows()
print("Detection stopped.\n")
# ===============================================
# GUI DASHBOARD
# ===============================================
ctk.set_appearance_mode("dark")
ctk.set_default_color_theme("blue")
app_ui = ctk.CTk()
app_ui.title("Smart Object Identifier")
app_ui.geometry("700x700")
app_ui.resizable(False, False)
def refresh_objects_list():
objects_box.configure(state="normal")
objects_box.delete("0.0", "end")
if not os.path.exists(OBJECTS_JSON):
objects_box.insert("end", " No objects learned yet.\n")
objects_box.configure(state="disabled")
return
with open(OBJECTS_JSON, "r") as f:
entries = json.load(f)
if len(entries) == 0:
objects_box.insert("end", " No objects learned yet.\n")
else:
counts = {}
for e in entries:
n = e["name"]
counts[n] = counts.get(n, 0) + 1
for name, count in counts.items():
if count > 1:
objects_box.insert("end", f" β€’ {name} [{count} samples]\n")
else:
objects_box.insert("end", f" β€’ {name}\n")
objects_box.configure(state="disabled")
def delete_object():
name = delete_entry.get().strip()
if not name:
messagebox.showerror("Error", "Enter an object name.")
return
with open(OBJECTS_JSON, "r") as f:
entries = json.load(f)
new_entries = []
removed = False
for entry in entries:
if entry["name"].lower() == name.lower():
for key in ["image", "embedding_file"]:
path = entry.get(key, "")
if path and os.path.exists(path):
os.remove(path)
removed = True
else:
new_entries.append(entry)
if not removed:
messagebox.showerror("Error", f"'{name}' not found.")
return
with open(OBJECTS_JSON, "w") as f:
json.dump(new_entries, f, indent=4)
global learned_objects
learned_objects = [o for o in learned_objects if o["name"].lower() != name.lower()]
messagebox.showinfo("Deleted", f"'{name}' removed!")
refresh_objects_list()
delete_entry.delete(0, "end")
def on_start():
thread = threading.Thread(target=run_detection, daemon=True)
thread.start()
def on_load_clip():
def _load():
if load_clip():
load_learned_objects()
app_ui.after(0, refresh_objects_list)
app_ui.after(0, lambda: messagebox.showinfo("CLIP Ready", "CLIP loaded! Internet recognition & custom matching enabled."))
else:
app_ui.after(0, lambda: messagebox.showerror("Error", "Failed to load CLIP. Check internet connection."))
thread = threading.Thread(target=_load, daemon=True)
thread.start()
def on_rebuild_embeddings():
"""Recompute CLIP embeddings for all saved objects that don't have one."""
if not clip_loaded:
messagebox.showerror("Error", "Load CLIP first!")
return
with open(OBJECTS_JSON, "r") as f:
entries = json.load(f)
updated = 0
for entry in entries:
emb_file = entry.get("embedding_file", "")
if not emb_file or not os.path.exists(emb_file):
img_path = entry.get("image", "")
if img_path and os.path.exists(img_path):
img = Image.open(img_path).convert("RGB")
emb = get_clip_embedding(img)
if emb is not None:
emb_path = img_path.replace(".jpg", ".npy").replace(".png", ".npy")
np.save(emb_path, emb)
entry["embedding_file"] = emb_path
updated += 1
print(f" Rebuilt embedding for: {entry['name']}")
with open(OBJECTS_JSON, "w") as f:
json.dump(entries, f, indent=4)
load_learned_objects()
refresh_objects_list()
messagebox.showinfo("Done", f"Rebuilt {updated} embeddings!")
# ===============================================
# BUILD GUI
# ===============================================
# Title
ctk.CTkLabel(app_ui, text="πŸ” Smart Object Identifier",
font=("Arial", 26, "bold")).pack(pady=(20, 5))
ctk.CTkLabel(app_ui, text="YOLOv8 + CLIP Learning + Internet Recognition",
font=("Arial", 12), text_color="gray").pack(pady=(0, 15))
# --- Controls ---
ctrl = ctk.CTkFrame(app_ui, corner_radius=10)
ctrl.pack(padx=20, pady=5, fill="x")
ctk.CTkLabel(ctrl, text="Controls", font=("Arial", 16, "bold")).pack(pady=(10, 5))
ctk.CTkLabel(ctrl, text="Tab=Select | T=Teach | I=Auto-Identify | ESC=Quit",
font=("Arial", 12), text_color="lightgray").pack(pady=(0, 5))
btn_row = ctk.CTkFrame(ctrl, fg_color="transparent")
btn_row.pack(pady=(5, 10), fill="x", padx=20)
ctk.CTkButton(btn_row, text="πŸš€ Start Detection", width=200, height=45,
font=("Arial", 14, "bold"), fg_color="#3498db", hover_color="#2980b9",
command=on_start).pack(side="left", padx=10, expand=True)
ctk.CTkButton(btn_row, text="🌐 Load CLIP (Internet)", width=200, height=45,
font=("Arial", 14, "bold"), fg_color="#8e44ad", hover_color="#7d3c98",
command=on_load_clip).pack(side="left", padx=10, expand=True)
# --- How to Use ---
howto = ctk.CTkFrame(app_ui, corner_radius=10)
howto.pack(padx=20, pady=5, fill="x")
ctk.CTkLabel(howto, text="How It Works", font=("Arial", 16, "bold")).pack(pady=(10, 5))
ctk.CTkLabel(howto, text=(
"1. Click 'Start Detection' β€” camera opens, YOLO detects objects\n"
"2. Press Tab to highlight/select an object (yellow box)\n"
"3. Press T β€” type the object name ON THE CAMERA, press Enter\n"
"4. Press I β€” AI guesses from internet, pick a number or type name\n"
"5. Green = YOLO known | Blue = Your taught objects"
), font=("Arial", 11), justify="left", text_color="lightgray").pack(padx=15, pady=(0, 10))
# --- Delete ---
del_frame = ctk.CTkFrame(app_ui, corner_radius=10)
del_frame.pack(padx=20, pady=5, fill="x")
delete_entry = ctk.CTkEntry(del_frame, width=300, height=35,
placeholder_text="Enter Object Name to Delete")
delete_entry.pack(side="left", padx=(15, 5), pady=10)
ctk.CTkButton(del_frame, text="πŸ—‘οΈ Delete", width=100, height=35,
fg_color="#e74c3c", hover_color="#c0392b",
command=delete_object).pack(side="left", padx=(5, 10), pady=10)
ctk.CTkButton(del_frame, text="πŸ”„ Rebuild", width=100, height=35,
fg_color="#f39c12", hover_color="#e67e22",
command=on_rebuild_embeddings).pack(side="left", padx=(0, 15), pady=10)
# --- Learned Objects List ---
ctk.CTkLabel(app_ui, text="Learned Objects", font=("Arial", 16, "bold")).pack(pady=(15, 5))
objects_box = ctk.CTkTextbox(app_ui, width=400, height=140, corner_radius=8)
objects_box.pack(pady=5)
# Info
ctk.CTkLabel(app_ui, text=f"YOLOv8: {len(yolo_model.names)} classes | CLIP: {len(CLIP_VOCABULARY)} categories",
font=("Arial", 11), text_color="gray").pack(pady=(10, 5))
# -------------------------
# Startup
# -------------------------
# Load objects from disk (embeddings)
load_learned_objects()
refresh_objects_list()
print("Dashboard ready!\n")
app_ui.mainloop()