initial commit

This commit is contained in:
Jakob Lechner 2025-07-16 14:23:13 +02:00
commit 6f735a22d7
12 changed files with 356 additions and 0 deletions

1
.envrc Normal file
View file

@ -0,0 +1 @@
use flake

5
.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
result*
*.qcow2
.pre-commit-config.yaml
.direnv
__pycache__

22
LICENSE Normal file
View 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
View 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
View 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
View 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
View 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
View file

@ -0,0 +1,7 @@
ultralytics==8.3.130
opencv-python-headless
numpy
filterpy
scikit-image
supervision
streamlit

82
streamlit_app.py Normal file
View 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
View 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
View 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

Binary file not shown.