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
parent
5fdb9bc29f
commit
ce53b04ca8
127
camera_stream.py
127
camera_stream.py
|
|
@ -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)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue