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.
main
bahawal.baloch 2026-04-01 12:51:53 +05:00
parent 5fdb9bc29f
commit ce53b04ca8
1 changed files with 107 additions and 20 deletions

View File

@ -3,8 +3,9 @@ import os
import time import time
import threading import threading
import numpy as np import numpy as np
import torch
from datetime import datetime 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 dotenv import load_dotenv
from ultralytics import YOLO from ultralytics import YOLO
@ -17,16 +18,24 @@ app = Flask(__name__)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
USERNAME = os.getenv("username") USERNAME = os.getenv("username")
PASSWORD = os.getenv("password") PASSWORD = os.getenv("password")
RTSP_URL = ( CAMERA_IPS = [os.getenv(f"camera_ip_{i+1}") for i in range(1, 3)]
f"rtsp://{USERNAME}:{PASSWORD}@192.168.6.36:554" CAMERAS = [
"/cam/realmonitor?channel=1&subtype=0" {
) "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" PROXIMITY_PX = 200 # max pixel distance to consider two people "together"
GROUP_TIME_THRESHOLD = 20 # seconds before an alert fires GROUP_TIME_THRESHOLD = 20 # seconds before an alert fires
ALERT_COOLDOWN = 60 # seconds between successive alerts ALERT_COOLDOWN = 60 # seconds between successive alerts
MIN_GROUP_SIZE = 2 MIN_GROUP_SIZE = 2
YOLO_CONF = 0.5 YOLO_CONF = 0.5
INFERENCE_DEVICE = 0
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Shared state (protected by lock) # Shared state (protected by lock)
@ -40,12 +49,20 @@ state = {
"alerts": [], "alerts": [],
"fps": 0, "fps": 0,
"stream_status": "connecting", "stream_status": "connecting",
"selected_camera_id": DEFAULT_CAMERA_ID,
"active_camera_id": DEFAULT_CAMERA_ID,
} }
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# YOLO model (downloaded on first run) # 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 = YOLO("yolo26m.pt")
model.to(f"cuda:{INFERENCE_DEVICE}")
# Group tracking # Group tracking
_group_trackers: dict = {} _group_trackers: dict = {}
@ -100,34 +117,70 @@ def _group_centroid(centroids, indices):
return (sum(xs) / len(xs), sum(ys) / len(ys)) 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) # Main processing loop (runs in background thread)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def _process_stream(): def _process_stream():
global _group_trackers, _next_group_id, _last_alert_time global _next_group_id, _last_alert_time
cap = None
cap = cv2.VideoCapture(RTSP_URL) active_camera_id = None
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() prev_time = time.time()
while True: 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() ret, frame = cap.read()
if not ret: if not ret:
with lock: with lock:
state["stream_status"] = "reconnecting" state["stream_status"] = "reconnecting"
cap.release() cap.release()
time.sleep(2) time.sleep(2)
cap = cv2.VideoCapture(RTSP_URL) cap = cv2.VideoCapture(selected_camera["rtsp_url"])
if cap.isOpened(): if not cap.isOpened():
with lock:
state["stream_status"] = "error"
cap = None
else:
with lock: with lock:
state["stream_status"] = "live" state["stream_status"] = "live"
continue continue
@ -137,7 +190,13 @@ def _process_stream():
prev_time = now prev_time = now
# --- YOLO inference (person = class 0) --- # --- 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 = [] person_boxes = []
for r in results: for r in results:
@ -309,6 +368,7 @@ def video_feed():
@app.route("/api/status") @app.route("/api/status")
def api_status(): def api_status():
with lock: with lock:
active_camera = _get_camera_by_id(state["active_camera_id"])
return jsonify({ return jsonify({
"people_count": state["people_count"], "people_count": state["people_count"],
"groups": state["groups"], "groups": state["groups"],
@ -316,9 +376,36 @@ def api_status():
"alerts": state["alerts"][:20], "alerts": state["alerts"][:20],
"fps": state["fps"], "fps": state["fps"],
"stream_status": state["stream_status"], "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/<path:filename>") @app.route("/alerts/<path:filename>")
def serve_alert_image(filename): def serve_alert_image(filename):
return send_from_directory("alerts", filename) return send_from_directory("alerts", filename)