From ce53b04ca8b12575c1e2c4e29d7630046e1091c4 Mon Sep 17 00:00:00 2001 From: "bahawal.baloch" Date: Wed, 1 Apr 2026 12:51:53 +0500 Subject: [PATCH] Enhance camera stream functionality with multi-camera support - Refactored camera handling to support multiple cameras via environment variables. - Added API endpoints for camera selection and retrieval of available cameras. - Updated stream processing to dynamically switch between selected cameras. - Integrated device selection for YOLO inference to utilize CUDA if available. --- camera_stream.py | 127 +++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 107 insertions(+), 20 deletions(-) diff --git a/camera_stream.py b/camera_stream.py index 4f8cf6c..4cd0ce6 100644 --- a/camera_stream.py +++ b/camera_stream.py @@ -3,8 +3,9 @@ import os import time import threading import numpy as np +import torch from datetime import datetime -from flask import Flask, Response, render_template, jsonify, send_from_directory +from flask import Flask, Response, render_template, jsonify, send_from_directory, request from dotenv import load_dotenv from ultralytics import YOLO @@ -17,16 +18,24 @@ app = Flask(__name__) # --------------------------------------------------------------------------- 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" -) +CAMERA_IPS = [os.getenv(f"camera_ip_{i+1}") for i in range(1, 3)] +CAMERAS = [ + { + "id": f"cam{i + 1}", + "name": f"Camera {i + 1}", + "ip": ip, + "rtsp_url": f"rtsp://{USERNAME}:{PASSWORD}@{ip}:554/cam/realmonitor?channel=1&subtype=0", + } + for i, ip in enumerate(CAMERA_IPS) +] +DEFAULT_CAMERA_ID = CAMERAS[0]["id"] if CAMERAS else "cam1" 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 +INFERENCE_DEVICE = 0 # --------------------------------------------------------------------------- # Shared state (protected by lock) @@ -40,12 +49,20 @@ state = { "alerts": [], "fps": 0, "stream_status": "connecting", + "selected_camera_id": DEFAULT_CAMERA_ID, + "active_camera_id": DEFAULT_CAMERA_ID, } # --------------------------------------------------------------------------- # YOLO model (downloaded on first run) # --------------------------------------------------------------------------- +if not torch.cuda.is_available(): + raise RuntimeError( + "CUDA GPU is required but not available. Install a CUDA-enabled PyTorch build " + "and verify NVIDIA drivers." + ) model = YOLO("yolo26m.pt") +model.to(f"cuda:{INFERENCE_DEVICE}") # Group tracking _group_trackers: dict = {} @@ -100,34 +117,70 @@ def _group_centroid(centroids, indices): return (sum(xs) / len(xs), sum(ys) / len(ys)) +def _get_camera_by_id(camera_id): + for cam in CAMERAS: + if cam["id"] == camera_id: + return cam + return CAMERAS[0] if CAMERAS else None + + +def _reset_tracking(): + global _group_trackers, _next_group_id, _last_alert_time + _group_trackers = {} + _next_group_id = 0 + _last_alert_time = 0.0 + + # --------------------------------------------------------------------------- # 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" - + global _next_group_id, _last_alert_time + cap = None + active_camera_id = None prev_time = time.time() while True: + with lock: + selected_camera_id = state["selected_camera_id"] + selected_camera = _get_camera_by_id(selected_camera_id) + if not selected_camera: + with lock: + state["stream_status"] = "error" + time.sleep(2) + continue + + if cap is None or active_camera_id != selected_camera["id"]: + if cap is not None: + cap.release() + cap = cv2.VideoCapture(selected_camera["rtsp_url"]) + active_camera_id = selected_camera["id"] + _reset_tracking() + with lock: + state["active_camera_id"] = active_camera_id + if not cap.isOpened(): + with lock: + state["stream_status"] = "error" + print(f"[ERROR] Cannot open RTSP stream: {selected_camera['rtsp_url']}") + time.sleep(2) + cap = None + continue + with lock: + state["stream_status"] = "live" + 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(): + cap = cv2.VideoCapture(selected_camera["rtsp_url"]) + if not cap.isOpened(): + with lock: + state["stream_status"] = "error" + cap = None + else: with lock: state["stream_status"] = "live" continue @@ -137,7 +190,13 @@ def _process_stream(): prev_time = now # --- YOLO inference (person = class 0) --- - results = model(frame, classes=[0], verbose=False, conf=YOLO_CONF) + results = model( + frame, + classes=[0], + verbose=False, + conf=YOLO_CONF, + device=INFERENCE_DEVICE, + ) person_boxes = [] for r in results: @@ -309,6 +368,7 @@ def video_feed(): @app.route("/api/status") def api_status(): with lock: + active_camera = _get_camera_by_id(state["active_camera_id"]) return jsonify({ "people_count": state["people_count"], "groups": state["groups"], @@ -316,9 +376,36 @@ def api_status(): "alerts": state["alerts"][:20], "fps": state["fps"], "stream_status": state["stream_status"], + "selected_camera_id": state["selected_camera_id"], + "active_camera_name": active_camera["name"] if active_camera else "Unknown", }) +@app.route("/api/cameras") +def api_cameras(): + return jsonify({ + "cameras": [{"id": c["id"], "name": c["name"], "ip": c["ip"]} for c in CAMERAS] + }) + + +@app.route("/api/camera/select", methods=["POST"]) +def api_camera_select(): + data = request.get_json(silent=True) or {} + camera_id = data.get("camera_id") + camera = _get_camera_by_id(camera_id) + if camera is None: + return jsonify({"ok": False, "error": "Invalid camera id"}), 400 + with lock: + state["selected_camera_id"] = camera["id"] + state["frame"] = None + state["groups"] = [] + state["people_count"] = 0 + state["alert_active"] = False + state["fps"] = 0 + state["stream_status"] = "connecting" + return jsonify({"ok": True}) + + @app.route("/alerts/") def serve_alert_image(filename): return send_from_directory("alerts", filename)