initial commit
This commit is contained in:
commit
6f735a22d7
12 changed files with 356 additions and 0 deletions
1
.envrc
Normal file
1
.envrc
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
use flake
|
||||||
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
result*
|
||||||
|
*.qcow2
|
||||||
|
.pre-commit-config.yaml
|
||||||
|
.direnv
|
||||||
|
__pycache__
|
||||||
22
LICENSE
Normal file
22
LICENSE
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 Jakob Lechner
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
||||||
40
README.md
Normal file
40
README.md
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
# People Counter with YOLOv8, ByteTrack, and Prometheus
|
||||||
|
|
||||||
|
This project implements a real-time people counting system based on object detection and tracking using [YOLOv8](https://github.com/ultralytics/ultralytics) and [ByteTrack](https://github.com/ifzhang/ByteTrack). It supports **RTSP input**, **in/out counting** using a vertical line, and exposes **Prometheus metrics** for integration with monitoring systems.
|
||||||
|
|
||||||
|
## 🚀 Features
|
||||||
|
|
||||||
|
- 🎥 RTSP camera stream input
|
||||||
|
- 🧠 YOLOv8 person detection
|
||||||
|
- 🔁 ByteTrack object tracking
|
||||||
|
- ➕➖ In/Out counting via virtual vertical line
|
||||||
|
- 📊 Prometheus metrics at `http://localhost:9100/`
|
||||||
|
- 🌐 Streamlit UI for live video and statistics
|
||||||
|
|
||||||
|
## Start mediamtx
|
||||||
|
|
||||||
|
```
|
||||||
|
mediamtx
|
||||||
|
```
|
||||||
|
|
||||||
|
## Stream webcam
|
||||||
|
|
||||||
|
```
|
||||||
|
ffmpeg \
|
||||||
|
-vaapi_device /dev/dri/renderD128 \
|
||||||
|
-y \
|
||||||
|
-f v4l2 \
|
||||||
|
-framerate 30 \
|
||||||
|
-video_size 1280x720 \
|
||||||
|
-input_format mjpeg \
|
||||||
|
-i /dev/video0 \
|
||||||
|
-vf 'format=nv12,hwupload' \
|
||||||
|
-c:v h264_vaapi \
|
||||||
|
-f rtsp \
|
||||||
|
rtsp://localhost:8554/cam
|
||||||
|
```
|
||||||
|
|
||||||
|
## Run application
|
||||||
|
```
|
||||||
|
streamlit run streamlit_app.py
|
||||||
|
```
|
||||||
56
bytetrack.yaml
Normal file
56
bytetrack.yaml
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
tracker_type: bytetrack
|
||||||
|
|
||||||
|
track_buffer: 30
|
||||||
|
match_thresh: 0.8
|
||||||
|
proximity_thresh: 0.5
|
||||||
|
max_unmatched_frames: 5
|
||||||
|
|
||||||
|
conf_thres: 0.25
|
||||||
|
low_conf_thres: 0.1
|
||||||
|
high_conf_thres: 0.7
|
||||||
|
iou_thres: 0.7
|
||||||
|
|
||||||
|
# Was ist das?
|
||||||
|
# Ein Schwellenwert für die Verfolgung von Objekten, der angibt, wie sicher die Detektion sein muss, damit sie als „hochqualitative“ Spur (Track) behandelt wird. Objekte mit Scores über diesem Wert werden als zuverlässig angesehen und aktiv verfolgt.
|
||||||
|
|
||||||
|
# Wirkung:
|
||||||
|
# Ein höherer Wert bedeutet, dass nur sehr sichere Detektionen verfolgt werden. Das reduziert False Positives, kann aber dazu führen, dass echte Objekte bei geringerer Confidence „verloren“ gehen.
|
||||||
|
|
||||||
|
# Typischer Bereich:
|
||||||
|
# 0.5 bis 0.9 (Confidence-Wert zwischen 0 und 1)
|
||||||
|
track_high_thresh: 0.7
|
||||||
|
|
||||||
|
# Was ist das?
|
||||||
|
# Ein niedrigerer Schwellenwert, der auch weniger sichere Detektionen zulässt, um diese in die Verfolgung einzubeziehen. Kann helfen, Objekte wiederzufinden, die kurzzeitig schlecht erkannt wurden.
|
||||||
|
|
||||||
|
# Wirkung:
|
||||||
|
# Erlaubt das „Reaktivieren“ von Tracks mit etwas schlechterer Confidence. Zu niedrig kann jedoch mehr Rauschen reinbringen.
|
||||||
|
|
||||||
|
# Typischer Bereich:
|
||||||
|
# 0.1 bis 0.4
|
||||||
|
track_low_thresh: 0.3
|
||||||
|
|
||||||
|
# Was ist das?
|
||||||
|
# Gewichtungsfaktor für das Kombinieren (Fusionieren) der Scores von detektierten Bounding Boxes und der Tracking-Informationen. Dadurch wird der endgültige Score für eine Box beim Tracking berechnet.
|
||||||
|
|
||||||
|
# Wirkung:
|
||||||
|
# Höhere Werte geben den Tracker-Informationen mehr Gewicht. Das kann die Stabilität der Verfolgung verbessern, besonders bei schwankenden Detektionen.
|
||||||
|
|
||||||
|
# Typischer Bereich:
|
||||||
|
# 0.7 bis 0.95
|
||||||
|
fuse_score: 0.85
|
||||||
|
|
||||||
|
# Was ist das?
|
||||||
|
# new_track_thresh ist der Schwellenwert (Confidence-Threshold), ab dem eine neue Spur (Track) für eine frisch erkannte Detektion gestartet wird.
|
||||||
|
|
||||||
|
# Es bestimmt, wie sicher eine Detektion sein muss, damit sie überhaupt als neuer Track initialisiert wird.
|
||||||
|
|
||||||
|
# Wirkung
|
||||||
|
|
||||||
|
# Höherer Wert:
|
||||||
|
# Neue Tracks werden nur für sehr sichere Detektionen gestartet. Das verringert Fehlstarts (False Positives), aber es kann auch dazu führen, dass echte Personen, die kurz schwach erkannt werden, gar nicht als neue Tracks gestartet werden.
|
||||||
|
|
||||||
|
# Niedriger Wert:
|
||||||
|
# Es werden auch Tracks aus weniger sicheren Detektionen gestartet, was mehr Objekte erfassen kann, aber gleichzeitig die Wahrscheinlichkeit für falsche Tracks erhöht.
|
||||||
|
|
||||||
|
new_track_thresh: 0.6
|
||||||
61
flake.lock
generated
Normal file
61
flake.lock
generated
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"flake-utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1731533236,
|
||||||
|
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1752436162,
|
||||||
|
"narHash": "sha256-Kt1UIPi7kZqkSc5HVj6UY5YLHHEzPBkgpNUByuyxtlw=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "dfcd5b901dbab46c9c6e80b265648481aafb01f8",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixos-25.05",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
40
flake.nix
Normal file
40
flake.nix
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
{
|
||||||
|
description = "People Counter with YOLOv8, ByteTrack and Streamlit";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05";
|
||||||
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = { nixpkgs, flake-utils, ... }:
|
||||||
|
flake-utils.lib.eachDefaultSystem (system:
|
||||||
|
let
|
||||||
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
|
|
||||||
|
pythonEnv = pkgs.python313.withPackages (ps: with ps; [
|
||||||
|
numpy
|
||||||
|
opencv-python
|
||||||
|
streamlit
|
||||||
|
pyyaml
|
||||||
|
pillow
|
||||||
|
ultralytics
|
||||||
|
torch
|
||||||
|
torchvision
|
||||||
|
prometheus-client
|
||||||
|
]);
|
||||||
|
in {
|
||||||
|
devShell = pkgs.mkShell {
|
||||||
|
buildInputs = [
|
||||||
|
pythonEnv
|
||||||
|
pkgs.ffmpeg
|
||||||
|
pkgs.mediamtx
|
||||||
|
];
|
||||||
|
|
||||||
|
shellHook = ''
|
||||||
|
export PYTHONUNBUFFERED=1
|
||||||
|
export MTX_PATHS_ALL_SOURCE=publisher
|
||||||
|
echo "People Counter DevShell ready"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
7
requirements.txt
Normal file
7
requirements.txt
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
ultralytics==8.3.130
|
||||||
|
opencv-python-headless
|
||||||
|
numpy
|
||||||
|
filterpy
|
||||||
|
scikit-image
|
||||||
|
supervision
|
||||||
|
streamlit
|
||||||
82
streamlit_app.py
Normal file
82
streamlit_app.py
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
import streamlit as st
|
||||||
|
import cv2
|
||||||
|
from ultralytics import YOLO
|
||||||
|
from utils.counter import Counter
|
||||||
|
from utils.zones import draw_count_line_horizontal, draw_count_line_vertical
|
||||||
|
import tempfile
|
||||||
|
import time
|
||||||
|
|
||||||
|
from prometheus_client import Gauge, start_http_server
|
||||||
|
|
||||||
|
# Nur einmal beim ersten Start starten
|
||||||
|
if "prom" not in st.session_state:
|
||||||
|
# Starte Prometheus-Metrik-Server (z. B. auf Port 9100)
|
||||||
|
start_http_server(9100)
|
||||||
|
|
||||||
|
# Metriken definieren
|
||||||
|
st.session_state.prom = {
|
||||||
|
'people_in': Gauge("people_in_count", "Number of people who entered"),
|
||||||
|
'people_out': Gauge("people_out_count", "Number of people who exited"),
|
||||||
|
}
|
||||||
|
|
||||||
|
st.set_page_config(layout="wide", page_title="People Counter")
|
||||||
|
|
||||||
|
# RTSP-Stream URL
|
||||||
|
#RTSP_URL = st.text_input("RTSP Stream URL", "rtsp://<benutzer>:<passwort>@<ip>:<port>/pfad")
|
||||||
|
RTSP_URL = "rtsp://localhost:8554/cam"
|
||||||
|
|
||||||
|
start_button = st.button("Start Counter")
|
||||||
|
|
||||||
|
FRAME_SKIP = 2 # Reduziert Verarbeitungslast
|
||||||
|
|
||||||
|
if start_button and RTSP_URL:
|
||||||
|
line_orientation = 'vertical'
|
||||||
|
line_position = 640
|
||||||
|
|
||||||
|
stframe = st.empty()
|
||||||
|
model = YOLO("yolo_weights/yolo11n.pt")
|
||||||
|
counter = Counter(line_orientation)
|
||||||
|
|
||||||
|
cap = cv2.VideoCapture(RTSP_URL)
|
||||||
|
|
||||||
|
frame_idx = 0
|
||||||
|
while cap.isOpened():
|
||||||
|
ret, frame = cap.read()
|
||||||
|
if not ret:
|
||||||
|
st.warning("Kein Bild vom RTSP-Stream.")
|
||||||
|
break
|
||||||
|
|
||||||
|
frame_idx += 1
|
||||||
|
if frame_idx % FRAME_SKIP != 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
results = model.track(frame, persist=True, classes=0, tracker="bytetrack.yaml")
|
||||||
|
boxes = results[0].boxes
|
||||||
|
|
||||||
|
if boxes.id is not None:
|
||||||
|
for box, track_id in zip(boxes.xywh.cpu(), boxes.id.cpu()):
|
||||||
|
x, y, w, h = map(int, box)
|
||||||
|
cv2.rectangle(frame, (x-w//2, y-h//2), (x+w//2, y+h//2), (0, 255, 0), 2)
|
||||||
|
cv2.putText(frame, str(track_id), (x, y - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0,255,0), 2)
|
||||||
|
|
||||||
|
tracks = [
|
||||||
|
type("Track", (), {"id": int(track_id), "bbox": box.numpy()})
|
||||||
|
for box, track_id in zip(boxes.xywh.cpu(), boxes.id.cpu())
|
||||||
|
]
|
||||||
|
counter.update(tracks, line_position)
|
||||||
|
st.session_state.prom['people_in'].set(counter.in_count)
|
||||||
|
st.session_state.prom['people_out'].set(counter.out_count)
|
||||||
|
|
||||||
|
if line_orientation == 'horizontal':
|
||||||
|
draw_count_line_horizontal(frame, line_position)
|
||||||
|
elif line_orientation == 'vertical':
|
||||||
|
draw_count_line_vertical(frame, line_position)
|
||||||
|
else:
|
||||||
|
raise NotImplementedError(f'Line orientation {line_orientation} is invalid!')
|
||||||
|
cv2.putText(frame, f"In: {counter.in_count}", (10, 40), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 255), 2)
|
||||||
|
cv2.putText(frame, f"Out: {counter.out_count}", (10, 80), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 255), 2)
|
||||||
|
|
||||||
|
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
||||||
|
stframe.image(frame, channels="RGB")
|
||||||
|
|
||||||
|
cap.release()
|
||||||
31
utils/counter.py
Normal file
31
utils/counter.py
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
class Counter:
|
||||||
|
def __init__(self, line_orientation):
|
||||||
|
self.in_count = 0
|
||||||
|
self.out_count = 0
|
||||||
|
self.track_memory = {}
|
||||||
|
self.line_orientation = line_orientation
|
||||||
|
|
||||||
|
def update(self, tracks, line_position):
|
||||||
|
for track in tracks:
|
||||||
|
track_id = track.id
|
||||||
|
x, y, w, h = track.bbox
|
||||||
|
|
||||||
|
|
||||||
|
if self.line_orientation == 'horizontal':
|
||||||
|
center = int(y + h / 2)
|
||||||
|
elif self.line_orientation == 'vertical':
|
||||||
|
center = int(x + w / 2)
|
||||||
|
else:
|
||||||
|
raise NotImplementedError(f'Line orientation {self.line_orientation} is invalid!')
|
||||||
|
|
||||||
|
if track_id not in self.track_memory:
|
||||||
|
self.track_memory[track_id] = center
|
||||||
|
continue
|
||||||
|
|
||||||
|
prev = self.track_memory[track_id]
|
||||||
|
self.track_memory[track_id] = center
|
||||||
|
|
||||||
|
if prev < line_position <= center:
|
||||||
|
self.in_count += 1
|
||||||
|
elif prev > line_position >= center:
|
||||||
|
self.out_count += 1
|
||||||
11
utils/zones.py
Normal file
11
utils/zones.py
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import cv2
|
||||||
|
|
||||||
|
def draw_count_line_horizontal(frame, y):
|
||||||
|
color = (0, 255, 255)
|
||||||
|
thickness = 2
|
||||||
|
cv2.line(frame, (0, y), (frame.shape[1], y), color, thickness)
|
||||||
|
|
||||||
|
def draw_count_line_vertical(frame, x):
|
||||||
|
color = (0, 255, 255)
|
||||||
|
thickness = 2
|
||||||
|
cv2.line(frame, (x, 0), (x, frame.shape[0]), color, thickness)
|
||||||
BIN
yolo_weights/yolo11n.pt
Normal file
BIN
yolo_weights/yolo11n.pt
Normal file
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue