import cv2 import threading import queue import time import numpy as np from ultralytics import YOLO from datetime import datetime import json class CameraThread: """Individual camera thread""" def __init__(self, camera_id, url=None, name=None): self.camera_id = camera_id self.url = url if url else camera_id self.name = name if name else f"Camera_{camera_id}" self.cap = None self.running = False self.frame_queue = queue.Queue(maxsize=2) self.last_frame = None self.fps = 0 self.stats = {'total_frames': 0, 'dropped_frames': 0} def start(self): """Start camera capture thread""" self.cap = cv2.VideoCapture(self.url) if not self.cap.isOpened(): print(f"Failed to open {self.name}") return False self.running = True self.thread = threading.Thread(target=self._capture_loop) self.thread.daemon = True self.thread.start() return True def _capture_loop(self): """Capture frames in loop""" prev_time = time.time() while self.running: ret, frame = self.cap.read() if not ret: print(f"Failed to read from {self.name}") break # Calculate FPS current_time = time.time() self.fps = 1 / (current_time - prev_time) prev_time = current_time # Add to queue (drop if full) if self.frame_queue.full(): try: self.frame_queue.get_nowait() self.stats['dropped_frames'] += 1 except queue.Empty: pass try: self.frame_queue.put_nowait(frame) self.stats['total_frames'] += 1 except queue.Full: pass self.last_frame = frame self.cap.release() def get_frame(self): """Get latest frame from queue""" try: frame = self.frame_queue.get_nowait() return frame except queue.Empty: return self.last_frame def stop(self): """Stop camera thread""" self.running = False if hasattr(self, 'thread'): self.thread.join(timeout=2) if self.cap: self.cap.release() def get_stats(self): """Get camera statistics""" return { 'name': self.name, 'camera_id': self.camera_id, 'fps': round(self.fps, 2), 'total_frames': self.stats['total_frames'], 'dropped_frames': self.stats['dropped_frames'], 'is_running': self.running } class MultiCameraDetector: """Multi-camera object detection system""" def __init__(self, model_path='yolov8n.pt'): self.model = YOLO(model_path) self.cameras = {} self.detection_results = {} self.running = False def add_camera(self, camera_id, url=None, name=None): """Add a camera to the system""" camera = CameraThread(camera_id, url, name) self.cameras[camera_id] = camera print(f"Added camera: {camera.name}") return camera def add_cameras_batch(self, camera_configs): """Add multiple cameras from configuration""" for config in camera_configs: self.add_camera( config['id'], config.get('url'), config.get('name') ) def start_all(self): """Start all cameras""" for camera_id, camera in self.cameras.items(): if camera.start(): print(f"Started {camera.name}") else: print(f"Failed to start {camera.name}") self.running = True def detect_frame(self, frame): """Run detection on single frame""" results = self.model(frame, verbose=False) detections = [] if results[0].boxes is not None: for box in results[0].boxes: detections.append({ 'class': self.model.names[int(box.cls)], 'confidence': float(box.conf), 'bbox': box.xyxy.tolist()[0] }) annotated = results[0].plot() return annotated, detections def process_all_cameras(self): """Process all cameras and run detection""" results = {} for camera_id, camera in self.cameras.items(): frame = camera.get_frame() if frame is not None: annotated, detections = self.detect_frame(frame) results[camera_id] = { 'frame': annotated, 'detections': detections, 'camera_stats': camera.get_stats(), 'timestamp': datetime.now().isoformat() } self.detection_results[camera_id] = results[camera_id] return results def create_grid_display(self, max_cols=2): """Create grid display of all camera feeds""" frames = [] labels = [] for camera_id, camera in self.cameras.items(): frame = camera.get_frame() if frame is not None: # Add detection overlay if available if camera_id in self.detection_results: detections = self.detection_results[camera_id]['detections'] for det in detections: bbox = det['bbox'] cv2.rectangle(frame, (int(bbox[0]), int(bbox[1])), (int(bbox[2]), int(bbox[3])), (0, 255, 0), 2) label = f"{det['class']}: {det['confidence']:.2f}" cv2.putText(frame, label, (int(bbox[0]), int(bbox[1])-10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2) # Add camera name cv2.putText(frame, camera.name, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2) # Add FPS fps_text = f"FPS: {camera.fps:.1f}" cv2.putText(frame, fps_text, (10, 60), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 0), 2) frames.append(frame) # Create grid if frames: rows = (len(frames) + max_cols - 1) // max_cols grid_height = max(frame.shape[0] for frame in frames) grid_width = max(frame.shape[1] for frame in frames) grid = np.zeros((grid_height * rows, grid_width * max_cols, 3), dtype=np.uint8) for idx, frame in enumerate(frames): row = idx // max_cols col = idx % max_cols h, w = frame.shape[:2] grid[row*grid_height:row*grid_height+h, col*grid_width:col*grid_width+w] = frame return grid return None def stop_all(self): """Stop all cameras""" for camera in self.cameras.values(): camera.stop() self.running = False print("All cameras stopped") def get_system_stats(self): """Get overall system statistics""" total_frames = sum(c.stats['total_frames'] for c in self.cameras.values()) total_dropped = sum(c.stats['dropped_frames'] for c in self.cameras.values()) return { 'total_cameras': len(self.cameras), 'active_cameras': sum(1 for c in self.cameras.values() if c.running), 'total_frames_processed': total_frames, 'total_frames_dropped': total_dropped, 'drop_rate': total_dropped / total_frames if total_frames > 0 else 0, 'cameras': {cid: c.get_stats() for cid, c in self.cameras.items()} } # Configuration examples CAMERA_CONFIGS = [ {'id': 0, 'name': 'Front Door', 'url': 0}, {'id': 1, 'name': 'Back Door', 'url': 1}, # {'id': 2, 'name': 'IP Camera 1', 'url': 'http://192.168.1.100:8080/video'}, # {'id': 3, 'name': 'IP Camera 2', 'url': 'rtsp://192.168.1.101:554/stream'}, ] # Multi-camera GUI using tkinter import tkinter as tk from tkinter import ttk from PIL import Image, ImageTk class MultiCameraGUI: def __init__(self, root): self.root = root self.root.title("Multi-Camera Object Detection System") self.root.geometry("1400x800") self.detector = MultiCameraDetector() self.setup_ui() def setup_ui(self): # Control panel control_frame = tk.Frame(self.root) control_frame.pack(pady=10) tk.Button(control_frame, text="Start All Cameras", command=self.start_cameras, bg="green", fg="white").pack(side=tk.LEFT, padx=5) tk.Button(control_frame, text="Stop All Cameras", command=self.stop_cameras, bg="red", fg="white").pack(side=tk.LEFT, padx=5) tk.Button(control_frame, text="Add Camera", command=self.add_camera_dialog, bg="blue", fg="white").pack(side=tk.LEFT, padx=5) tk.Button(control_frame, text="Save Stats", command=self.save_stats, bg="orange").pack(side=tk.LEFT, padx=5) # Camera grid display self.canvas_frame = tk.Frame(self.root) self.canvas_frame.pack(expand=True, fill=tk.BOTH, padx=10, pady=10) self.canvas = tk.Canvas(self.canvas_frame, bg='black') self.canvas.pack(expand=True, fill=tk.BOTH) # Stats panel self.stats_text = tk.Text(self.root, height=8, width=50) self.stats_text.pack(side=tk.RIGHT, padx=10, pady=10) # Status bar self.status_var = tk.StringVar() self.status_var.set("Ready") status_bar = tk.Label(self.root, textvariable=self.status_var, relief=tk.SUNKEN, anchor=tk.W) status_bar.pack(side=tk.BOTTOM, fill=tk.X) # Update loop self.update_display() def start_cameras(self): # Add default cameras for config in CAMERA_CONFIGS: self.detector.add_camera(config['id'], config.get('url'), config.get('name')) self.detector.start_all() self.status_var.set(f"Started {len(self.detector.cameras)} cameras") def stop_cameras(self): self.detector.stop_all() self.status_var.set("All cameras stopped") def add_camera_dialog(self): dialog = tk.Toplevel(self.root) dialog.title("Add Camera") dialog.geometry("400x250") tk.Label(dialog, text="Camera ID:").pack(pady=5) camera_id_entry = tk.Entry(dialog) camera_id_entry.pack(pady=5) tk.Label(dialog, text="Camera Name:").pack(pady=5) name_entry = tk.Entry(dialog) name_entry.pack(pady=5) tk.Label(dialog, text="URL (optional):").pack(pady=5) url_entry = tk.Entry(dialog) url_entry.pack(pady=5) def add(): camera_id = int(camera_id_entry.get()) name = name_entry.get() url = url_entry.get() or camera_id self.detector.add_camera(camera_id, url, name) dialog.destroy() self.status_var.set(f"Added camera: {name}") tk.Button(dialog, text="Add", command=add).pack(pady=10) def update_display(self): if self.detector.running: # Process detections results = self.detector.process_all_cameras() # Create grid display grid = self.detector.create_grid_display(max_cols=2) if grid is not None: # Resize for display h, w = grid.shape[:2] scale = min(800/h, 1200/w) new_w, new_h = int(w * scale), int(h * scale) grid = cv2.resize(grid, (new_w, new_h)) # Convert to PhotoImage grid_rgb = cv2.cvtColor(grid, cv2.COLOR_BGR2RGB) img = Image.fromarray(grid_rgb) photo = ImageTk.PhotoImage(img) self.canvas.config(width=new_w, height=new_h) self.canvas.create_image(0, 0, anchor=tk.NW, image=photo) self.canvas.image = photo # Update stats stats = self.detector.get_system_stats() self.stats_text.delete(1.0, tk.END) self.stats_text.insert(tk.END, json.dumps(stats, indent=2)) self.stats_text.see(tk.END) self.status_var.set(f"Active: {stats['active_cameras']}/{stats['total_cameras']} cameras") # Schedule next update self.root.after(100, self.update_display) def save_stats(self): stats = self.detector.get_system_stats() filename = f"camera_stats_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json" with open(filename, 'w') as f: json.dump(stats, f, indent=2) self.status_var.set(f"Stats saved to {filename}") if __name__ == "__main__": # Option 1: Run GUI root = tk.Tk() app = MultiCameraGUI(root) root.mainloop() # Option 2: Run headless # detector = MultiCameraDetector() # detector.add_cameras_batch(CAMERA_CONFIGS) # detector.start_all() # # while True: # results = detector.process_all_cameras() # grid = detector.create_grid_display() # if grid is not None: # cv2.imshow('Multi-Camera Detection', grid) # # if cv2.waitKey(1) & 0xFF == ord('q'): # break # # detector.stop_all() # cv2.destroyAllWindows()