commit e956070aa26cb2887bb8228957ef2b388d161946 Author: bahawal.baloch Date: Mon Mar 30 19:59:22 2026 +0500 Add camera stream processing with YOLO object detection - Implemented a new camera_stream.py file for real-time video processing. - Integrated YOLO model for detecting people in the video stream. - Added functionality for grouping detected individuals based on proximity. - Implemented alert system for detecting groups over a specified duration. - Included state management for stream status and alerts. - Utilized Flask for serving video stream and alert information. - Configured environment variables for RTSP stream access. diff --git a/camera_stream.py b/camera_stream.py new file mode 100644 index 0000000..91222d4 --- /dev/null +++ b/camera_stream.py @@ -0,0 +1,334 @@ +import cv2 +import os +import time +import threading +import numpy as np +from datetime import datetime +from flask import Flask, Response, render_template, jsonify, send_from_directory +from dotenv import load_dotenv +from ultralytics import YOLO + +load_dotenv(override=True) + +app = Flask(__name__) + +# --------------------------------------------------------------------------- +# Configuration +# --------------------------------------------------------------------------- +USERNAME = os.getenv("username") +PASSWORD = os.getenv("password") +RTSP_URL = ( + f"rtsp://{USERNAME}:{PASSWORD}@192.168.6.36:554" + "/cam/realmonitor?channel=1&subtype=0" +) + +PROXIMITY_PX = 200 # max pixel distance to consider two people "together" +GROUP_TIME_THRESHOLD = 20 # seconds before an alert fires +ALERT_COOLDOWN = 60 # seconds between successive alerts +MIN_GROUP_SIZE = 2 +YOLO_CONF = 0.5 + +# --------------------------------------------------------------------------- +# Shared state (protected by lock) +# --------------------------------------------------------------------------- +lock = threading.Lock() +state = { + "frame": None, + "people_count": 0, + "groups": [], + "alert_active": False, + "alerts": [], + "fps": 0, + "stream_status": "connecting", +} + +# --------------------------------------------------------------------------- +# YOLO model (downloaded on first run) +# --------------------------------------------------------------------------- +model = YOLO("yolov8n.pt") + +# Group tracking +_group_trackers: dict = {} +_next_group_id = 0 +_last_alert_time = 0.0 + + +# --------------------------------------------------------------------------- +# Detection helpers +# --------------------------------------------------------------------------- + +def _centroids(boxes): + """Return list of (cx, cy) from xyxy boxes.""" + return [((b[0] + b[2]) / 2, (b[1] + b[3]) / 2) for b in boxes] + + +def _find_groups(centroids, threshold): + """BFS clustering — returns list of index-lists with >= MIN_GROUP_SIZE.""" + n = len(centroids) + if n < MIN_GROUP_SIZE: + return [] + + visited = set() + groups = [] + + for i in range(n): + if i in visited: + continue + cluster = [i] + visited.add(i) + queue = [i] + while queue: + cur = queue.pop(0) + for j in range(n): + if j in visited: + continue + dx = centroids[cur][0] - centroids[j][0] + dy = centroids[cur][1] - centroids[j][1] + if (dx * dx + dy * dy) ** 0.5 < threshold: + cluster.append(j) + visited.add(j) + queue.append(j) + if len(cluster) >= MIN_GROUP_SIZE: + groups.append(cluster) + + return groups + + +def _group_centroid(centroids, indices): + xs = [centroids[i][0] for i in indices] + ys = [centroids[i][1] for i in indices] + return (sum(xs) / len(xs), sum(ys) / len(ys)) + + +# --------------------------------------------------------------------------- +# Main processing loop (runs in background thread) +# --------------------------------------------------------------------------- + +def _process_stream(): + global _group_trackers, _next_group_id, _last_alert_time + + cap = cv2.VideoCapture(RTSP_URL) + if not cap.isOpened(): + with lock: + state["stream_status"] = "error" + print(f"[ERROR] Cannot open RTSP stream: {RTSP_URL}") + return + + with lock: + state["stream_status"] = "live" + + prev_time = time.time() + + while True: + ret, frame = cap.read() + if not ret: + with lock: + state["stream_status"] = "reconnecting" + cap.release() + time.sleep(2) + cap = cv2.VideoCapture(RTSP_URL) + if cap.isOpened(): + with lock: + state["stream_status"] = "live" + continue + + now = time.time() + fps = 1.0 / max(now - prev_time, 1e-6) + prev_time = now + + # --- YOLO inference (person = class 0) --- + results = model(frame, classes=[0], verbose=False, conf=YOLO_CONF) + + person_boxes = [] + for r in results: + for box in r.boxes: + x1, y1, x2, y2 = box.xyxy[0].cpu().numpy() + conf = float(box.conf[0]) + person_boxes.append((float(x1), float(y1), float(x2), float(y2), conf)) + + centroids = _centroids([(b[0], b[1], b[2], b[3]) for b in person_boxes]) + current_groups = _find_groups(centroids, PROXIMITY_PX) + + # --- Match current groups to tracked groups --- + matched_ids: set = set() + frame_group_data = [] + + for grp_indices in current_groups: + gc = _group_centroid(centroids, grp_indices) + + best_id, best_dist = None, float("inf") + for gid, gdata in _group_trackers.items(): + if gid in matched_ids: + continue + dx = gc[0] - gdata["centroid"][0] + dy = gc[1] - gdata["centroid"][1] + dist = (dx * dx + dy * dy) ** 0.5 + if dist < PROXIMITY_PX * 2 and dist < best_dist: + best_dist = dist + best_id = gid + + if best_id is not None: + _group_trackers[best_id]["centroid"] = gc + _group_trackers[best_id]["last_seen"] = now + _group_trackers[best_id]["member_count"] = len(grp_indices) + matched_ids.add(best_id) + frame_group_data.append((best_id, grp_indices, gc)) + else: + gid = _next_group_id + _next_group_id += 1 + _group_trackers[gid] = { + "centroid": gc, + "first_seen": now, + "last_seen": now, + "member_count": len(grp_indices), + "alerted": False, + } + frame_group_data.append((gid, grp_indices, gc)) + + # Remove stale groups (not seen for > 3 s) + stale = [gid for gid, gd in _group_trackers.items() if now - gd["last_seen"] > 3] + for gid in stale: + del _group_trackers[gid] + + # --- Alert logic --- + alert_this_frame = False + for gid, gdata in _group_trackers.items(): + duration = gdata["last_seen"] - gdata["first_seen"] + if duration >= GROUP_TIME_THRESHOLD and not gdata["alerted"]: + if now - _last_alert_time >= ALERT_COOLDOWN: + gdata["alerted"] = True + alert_this_frame = True + _last_alert_time = now + + os.makedirs("alerts", exist_ok=True) + ts = datetime.now().strftime("%Y%m%d_%H%M%S") + alert_path = f"alerts/alert_{ts}.jpg" + cv2.imwrite(alert_path, frame) + + alert_info = { + "time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + "people": gdata["member_count"], + "duration": round(duration, 1), + "image": alert_path, + } + with lock: + state["alerts"].insert(0, alert_info) + state["alerts"] = state["alerts"][:50] + + # --- Draw overlays --- + display = frame.copy() + + for x1, y1, x2, y2, conf in person_boxes: + cv2.rectangle(display, (int(x1), int(y1)), (int(x2), int(y2)), (0, 255, 0), 2) + cv2.putText( + display, f"{conf:.0%}", + (int(x1), int(y1) - 6), + cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2, + ) + + for gid, grp_indices, gc in frame_group_data: + gdata = _group_trackers.get(gid) + if gdata is None: + continue + duration = gdata["last_seen"] - gdata["first_seen"] + radius = int(PROXIMITY_PX * 0.6) + is_alert = duration >= GROUP_TIME_THRESHOLD + color = (0, 0, 255) if is_alert else (0, 165, 255) + + cv2.circle(display, (int(gc[0]), int(gc[1])), radius, color, 2) + label = f"Group: {len(grp_indices)} | {duration:.0f}s" + cv2.putText( + display, label, + (int(gc[0]) - 70, int(gc[1]) - radius - 10), + cv2.FONT_HERSHEY_SIMPLEX, 0.55, color, 2, + ) + if is_alert: + cv2.putText( + display, "ALERT", + (int(gc[0]) - 35, int(gc[1]) + radius + 25), + cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 0, 255), 3, + ) + + # --- Update shared state --- + groups_json = [] + for gid, gi, gc in frame_group_data: + gdata = _group_trackers.get(gid) + if gdata: + groups_json.append({ + "id": gid, + "count": len(gi), + "duration": round(gdata["last_seen"] - gdata["first_seen"], 1), + }) + + with lock: + state["frame"] = display + state["people_count"] = len(person_boxes) + state["groups"] = groups_json + state["alert_active"] = alert_this_frame or any( + (gd["last_seen"] - gd["first_seen"]) >= GROUP_TIME_THRESHOLD + for gd in _group_trackers.values() + ) + state["fps"] = round(fps, 1) + + +# --------------------------------------------------------------------------- +# MJPEG generator +# --------------------------------------------------------------------------- + +def _generate_frames(): + while True: + with lock: + frame = state["frame"] + if frame is not None: + ok, buf = cv2.imencode(".jpg", frame, [cv2.IMWRITE_JPEG_QUALITY, 80]) + if ok: + yield ( + b"--frame\r\n" + b"Content-Type: image/jpeg\r\n\r\n" + buf.tobytes() + b"\r\n" + ) + time.sleep(0.033) + + +# --------------------------------------------------------------------------- +# Routes +# --------------------------------------------------------------------------- + +@app.route("/") +def dashboard(): + return render_template("dashboard.html") + + +@app.route("/video_feed") +def video_feed(): + return Response( + _generate_frames(), + mimetype="multipart/x-mixed-replace; boundary=frame", + ) + + +@app.route("/api/status") +def api_status(): + with lock: + return jsonify({ + "people_count": state["people_count"], + "groups": state["groups"], + "alert_active": state["alert_active"], + "alerts": state["alerts"][:20], + "fps": state["fps"], + "stream_status": state["stream_status"], + }) + + +@app.route("/alerts/") +def serve_alert_image(filename): + return send_from_directory("alerts", filename) + + +# --------------------------------------------------------------------------- +# Entry point +# --------------------------------------------------------------------------- + +if __name__ == "__main__": + threading.Thread(target=_process_stream, daemon=True).start() + print(f"\n Surveillance Dashboard → http://localhost:5000\n") + app.run(host="0.0.0.0", port=5000, debug=False, threaded=True)