From 504f8c0b69a5af8606cef53d387c4fde52f03e6b Mon Sep 17 00:00:00 2001 From: Joachim Hummel Date: Mon, 8 Dec 2025 14:42:46 +0000 Subject: [PATCH] =?UTF-8?q?Implementiere=20Fahrzeugz=C3=A4hlung=20mit=20Li?= =?UTF-8?q?nien=C3=BCberquerung?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Linienschnitt-Algorithmus für präzise Fahrzeugzählung - Interaktive Linienauswahl im Browser (Canvas-basiert) - Session-Management für benutzerdefinierte Zähllinien - Typ-spezifische Zähler (Autos, LKW, Busse, Motorräder) - REST-API für Linienkonfiguration und Zähler-Reset - Gestrichelte Zähllinie als Video-Overlay - Detailliertes Zähler-Display im Video Features: - Linienüberquerung-Erkennung (beide Richtungen) - Keine Mehrfachzählung durch Track-ID-Management - Funktioniert für Webcam und Video-Upload - Benutzerfreundliche UI mit Echtzeit-Feedback 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- CLAUDE.md | 86 +++++++++++ README.md | 116 +++++++++++++++ app.py | 304 ++++++++++++++++++++++++++++++++++++++ templates/index.html | 40 +++++ templates/play_video.html | 222 ++++++++++++++++++++++++++++ templates/webcam.html | 222 ++++++++++++++++++++++++++++ 6 files changed, 990 insertions(+) create mode 100644 CLAUDE.md create mode 100644 README.md create mode 100644 app.py create mode 100644 templates/index.html create mode 100644 templates/play_video.html create mode 100644 templates/webcam.html diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..d1e5208 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,86 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +This is a Flask-based web application for real-time object detection and tracking using YOLOv11. The application supports both webcam streaming and uploaded video file processing with object detection overlays. + +## Key Dependencies + +- **Flask**: Web framework for routing and serving HTML templates +- **OpenCV (cv2)**: Video processing and frame manipulation +- **Ultralytics YOLO**: YOLOv11 model for object detection and tracking +- **NumPy**: Array operations for image data + +The YOLO model file (`yolo11s.pt`) must be present in the root directory. + +## Running the Application + +Start the Flask development server: +```bash +python3 app.py +``` + +The application runs on `0.0.0.0:8080` and is accessible at `http://localhost:8080` + +## Application Architecture + +### Single-File Structure +The entire application is contained in `app.py` with no separate modules. All routes, video processing logic, and model initialization are in this single file. + +### Core Components + +**YOLO Model (lines 10-11)** +- Model loaded once at application startup: `model = YOLO("yolo11s.pt")` +- Class names extracted from model for labeling: `names = model.model.names` + +**Video Processing Pattern** +Both webcam and uploaded video processing share identical detection logic: +1. Frame skipping: Only processes every 2nd frame (`count % 2 != 0`) for performance +2. Frame resizing: All frames resized to (1020, 600) for consistent output +3. YOLO tracking: Uses `model.track(frame, persist=True)` to maintain object IDs across frames +4. Annotation: Draws bounding boxes and labels with format `{track_id} - {class_name}` +5. Streaming: Yields JPEG frames as multipart HTTP response + +**Duplicate Code** +The functions `detect_objects_from_webcam()` (lines 21-52) and `detect_objects_from_video()` (lines 86-118) contain nearly identical logic - only the video source differs (webcam vs. file path). + +### Routes and Flow + +**Main Routes:** +- `/` - Landing page (index.html) with video upload form and webcam button +- `/start_webcam` - Webcam detection page (webcam.html) +- `/upload` [POST] - Handles video file uploads, saves to `uploads/` directory +- `/uploads/` - Video playback page (play_video.html) + +**Streaming Routes:** +- `/webcam_feed` - Streams processed webcam frames with detections +- `/video_feed/` - Streams processed video file frames with detections +- `/video/` - Serves raw video files from uploads directory + +### Templates +All templates follow the same pattern: +- Centered layout with flex containers +- Fixed dimensions (1020x600) for video display +- "Back to Home" link for navigation + +## Important Notes + +**File Uploads** +- Uploaded videos are saved to `uploads/` directory (created automatically if missing) +- No file size limits or validation beyond checking file existence +- No cleanup mechanism for uploaded files + +**Performance Optimization** +- Frame skipping (every 2nd frame) reduces CPU load but may miss fast-moving objects +- Frame resizing to 1020x600 is hardcoded throughout + +**Object Tracking** +- Uses persistent tracking (`persist=True`) to maintain object IDs across frames +- Track IDs are displayed alongside class names in the format: `{track_id} - {class_name}` +- Tracking state persists within a single video stream session + +**Webcam Access** +- Uses `cv2.VideoCapture(0)` for default webcam +- No error handling if webcam is unavailable or already in use diff --git a/README.md b/README.md new file mode 100644 index 0000000..c80a31e --- /dev/null +++ b/README.md @@ -0,0 +1,116 @@ +# Fahrzeug- und Objekt-Erkennungssystem + +Eine webbasierte Anwendung zur Echtzeit-Objekterkennung und -Verfolgung mittels YOLOv11. Die Anwendung unterstützt sowohl Live-Webcam-Streams als auch die Verarbeitung hochgeladener Videodateien. + +## Funktionen + +- **Webcam-Erkennung**: Echtzeit-Objekterkennung über die Webcam +- **Video-Upload**: Hochladen und Verarbeiten von Videodateien mit Objekterkennung +- **Objekt-Tracking**: Persistente Verfolgung von Objekten mit eindeutigen IDs über Frames hinweg +- **Visuelle Markierungen**: Bounding Boxes und Labels für erkannte Objekte +- **Browser-basiert**: Einfacher Zugriff über den Webbrowser + +## Voraussetzungen + +- Python 3.12 oder höher +- Webcam (für Live-Erkennung) +- YOLO-Modell (`yolo11s.pt`) im Hauptverzeichnis + +## Installation + +1. Repository klonen oder herunterladen + +2. Erforderliche Python-Pakete installieren: +```bash +pip3 install flask opencv-python numpy ultralytics +``` + +3. Sicherstellen, dass die Modelldatei `yolo11s.pt` im Hauptverzeichnis vorhanden ist + +## Verwendung + +### Anwendung starten + +```bash +python3 app.py +``` + +Die Anwendung ist dann unter `http://localhost:8080` erreichbar. + +### Webcam-Erkennung + +1. Öffnen Sie `http://localhost:8080` im Browser +2. Klicken Sie auf "Start Webcam Detection" +3. Die Webcam wird aktiviert und Objekte werden in Echtzeit erkannt und markiert +4. Jedes Objekt erhält eine Track-ID und Klassenbeschriftung + +### Video-Upload + +1. Öffnen Sie `http://localhost:8080` im Browser +2. Wählen Sie eine Videodatei über das Upload-Formular aus +3. Klicken Sie auf "Upload Video" +4. Das Video wird verarbeitet und mit Objekterkennungen angezeigt + +## Projektstruktur + +``` +. +├── app.py # Haupt-Flask-Anwendung +├── yolo11s.pt # YOLOv11-Modell (erforderlich) +├── templates/ # HTML-Templates +│ ├── index.html # Startseite +│ ├── webcam.html # Webcam-Anzeige +│ └── play_video.html # Video-Wiedergabe +├── uploads/ # Hochgeladene Videos (automatisch erstellt) +└── highway1.mp4 # Beispielvideo +``` + +## Technische Details + +### Verwendete Technologien + +- **Flask**: Web-Framework für Routing und Template-Rendering +- **OpenCV**: Videobearbeitung und Frame-Manipulation +- **Ultralytics YOLO**: YOLOv11-Modell für Objekterkennung und Tracking +- **NumPy**: Array-Operationen für Bilddaten + +### Verarbeitungs-Pipeline + +1. **Frame-Erfassung**: Webcam oder Videodatei als Quelle +2. **Frame-Skipping**: Verarbeitung jedes 2. Frames zur Leistungsoptimierung +3. **Größenanpassung**: Alle Frames werden auf 1020x600 Pixel skaliert +4. **YOLO-Tracking**: Objekterkennung mit persistenten Track-IDs +5. **Annotation**: Zeichnen von Bounding Boxes und Labels +6. **Streaming**: Übertragung als MJPEG-Stream an den Browser + +### Objekterkennung + +- Erkennt verschiedene Objektklassen (abhängig vom YOLO-Modell) +- Vergibt eindeutige Track-IDs für jedes Objekt +- Beschriftung im Format: `{Track-ID} - {Klassenname}` +- Grüne Bounding Boxes um erkannte Objekte +- Magenta-farbene Textbeschriftungen + +## Leistungsoptimierung + +- **Frame-Skipping**: Nur jeder 2. Frame wird verarbeitet, um CPU-Last zu reduzieren +- **Feste Auflösung**: Einheitliche Größe von 1020x600 Pixel für alle Frames +- **Effizientes Streaming**: JPEG-Kompression für Frame-Übertragung + +## Einschränkungen + +- Keine Validierung der Upload-Dateigröße +- Keine automatische Bereinigung hochgeladener Dateien +- Feste Frame-Dimensionen (1020x600) +- Keine Fehlerbehandlung bei Webcam-Zugriffsproblemen + +## Hinweise + +- Bei der ersten Verwendung kann das Laden des YOLO-Modells einige Sekunden dauern +- Die Erkennungsgenauigkeit hängt vom verwendeten YOLO-Modell ab +- Frame-Skipping kann bei sehr schnell bewegten Objekten zu Erkennungslücken führen +- Hochgeladene Videos werden im Ordner `uploads/` gespeichert und müssen manuell gelöscht werden + +## Lizenz + +Projekt basiert auf Ressourcen von [Pyresearch](https://pyresearch.org/) diff --git a/app.py b/app.py new file mode 100644 index 0000000..234a92e --- /dev/null +++ b/app.py @@ -0,0 +1,304 @@ +import os +import cv2 +import numpy as np +from flask import Flask, render_template, Response, request, redirect, url_for, send_from_directory, session, jsonify +from ultralytics import YOLO + +app = Flask(__name__) +app.secret_key = 'vehicle_counting_secret_key_2024' # Required for session management + +# Load the YOLOv8 model +model = YOLO("yolo11s.pt") +names = model.model.names + +# Vehicle classes to count +VEHICLE_CLASSES = {'car', 'truck', 'bus', 'motorcycle'} + +# Helper function to check if two line segments intersect +def line_intersect(p1, p2, p3, p4): + """ + Check if line segment p1-p2 intersects with line segment p3-p4 + Returns True if they intersect, False otherwise + """ + x1, y1 = p1 + x2, y2 = p2 + x3, y3 = p3 + x4, y4 = p4 + + denom = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4) + if abs(denom) < 1e-10: + return False + + t = ((x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)) / denom + u = -((x1 - x2) * (y1 - y3) - (y1 - y2) * (x1 - x3)) / denom + + return 0 <= t <= 1 and 0 <= u <= 1 + +@app.route('/') +def index(): + return render_template('index.html') + +@app.route('/start_webcam') +def start_webcam(): + # Initialize default counting line in session if not set + if 'counting_line' not in session: + session['counting_line'] = {'x1': 0, 'y1': 300, 'x2': 1020, 'y2': 300} + return render_template('webcam.html') + +@app.route('/api/set_line', methods=['POST']) +def set_counting_line(): + """API endpoint to set the counting line coordinates""" + data = request.json + session['counting_line'] = { + 'x1': int(data['x1']), + 'y1': int(data['y1']), + 'x2': int(data['x2']), + 'y2': int(data['y2']) + } + return jsonify({'status': 'success', 'line': session['counting_line']}) + +@app.route('/api/get_line', methods=['GET']) +def get_counting_line(): + """API endpoint to get the current counting line coordinates""" + if 'counting_line' not in session: + session['counting_line'] = {'x1': 0, 'y1': 300, 'x2': 1020, 'y2': 300} + return jsonify(session['counting_line']) + +@app.route('/api/reset_count', methods=['POST']) +def reset_count(): + """API endpoint to reset the vehicle count""" + session['reset_count'] = True + return jsonify({'status': 'success'}) + +def detect_objects_from_webcam(): + count = 0 + cap = cv2.VideoCapture(0) # 0 for the default webcam + + # Track vehicle positions and counted IDs + track_positions = {} # {track_id: (center_x, center_y)} + counted_ids = set() + vehicle_count = 0 + vehicle_type_counts = {'car': 0, 'truck': 0, 'bus': 0, 'motorcycle': 0} + + while True: + ret, frame = cap.read() + if not ret: + break + count += 1 + if count % 2 != 0: + continue + # Resize the frame to (1020, 600) + frame = cv2.resize(frame, (1020, 600)) + + # Get counting line from session (default to horizontal middle line) + line_data = session.get('counting_line', {'x1': 0, 'y1': 300, 'x2': 1020, 'y2': 300}) + line_start = (line_data['x1'], line_data['y1']) + line_end = (line_data['x2'], line_data['y2']) + + # Check if count should be reset + if session.get('reset_count', False): + counted_ids.clear() + vehicle_count = 0 + vehicle_type_counts = {'car': 0, 'truck': 0, 'bus': 0, 'motorcycle': 0} + session['reset_count'] = False + + # Draw counting line (dashed yellow line with black gaps) + cv2.line(frame, line_start, line_end, (0, 255, 255), 2, cv2.LINE_AA) + # Draw dashed effect + line_length = int(np.sqrt((line_end[0] - line_start[0])**2 + (line_end[1] - line_start[1])**2)) + dash_length = 20 + for i in range(0, line_length, dash_length * 2): + t1 = i / line_length + t2 = min((i + dash_length) / line_length, 1.0) + x1 = int(line_start[0] + t1 * (line_end[0] - line_start[0])) + y1 = int(line_start[1] + t1 * (line_end[1] - line_start[1])) + x2 = int(line_start[0] + t2 * (line_end[0] - line_start[0])) + y2 = int(line_start[1] + t2 * (line_end[1] - line_start[1])) + cv2.line(frame, (x1, y1), (x2, y2), (0, 0, 0), 2) + + # Run YOLOv8 tracking on the frame + results = model.track(frame, persist=True) + + if results[0].boxes is not None and results[0].boxes.id is not None: + boxes = results[0].boxes.xyxy.int().cpu().tolist() + class_ids = results[0].boxes.cls.int().cpu().tolist() + track_ids = results[0].boxes.id.int().cpu().tolist() + + for box, class_id, track_id in zip(boxes, class_ids, track_ids): + c = names[class_id] + x1, y1, x2, y2 = box + + # Calculate center point of bounding box + center_x = (x1 + x2) // 2 + center_y = (y1 + y2) // 2 + + # Check if this is a vehicle we want to count + if c in VEHICLE_CLASSES: + # If we have a previous position for this track + if track_id in track_positions and track_id not in counted_ids: + prev_x, prev_y = track_positions[track_id] + # Check if the vehicle crossed the counting line + if line_intersect((prev_x, prev_y), (center_x, center_y), line_start, line_end): + counted_ids.add(track_id) + vehicle_count += 1 + vehicle_type_counts[c] += 1 + + # Update position + track_positions[track_id] = (center_x, center_y) + + # Draw bounding box and label + cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) + cv2.putText(frame, f'{track_id} - {c}', (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 0, 255), 1) + + # Display vehicle count with type breakdown + cv2.rectangle(frame, (10, 10), (350, 140), (0, 0, 0), -1) + cv2.putText(frame, f'Gesamt: {vehicle_count}', (20, 35), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2) + cv2.putText(frame, f'Autos: {vehicle_type_counts["car"]}', (20, 65), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (200, 200, 200), 1) + cv2.putText(frame, f'LKW: {vehicle_type_counts["truck"]}', (20, 90), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (200, 200, 200), 1) + cv2.putText(frame, f'Busse: {vehicle_type_counts["bus"]}', (20, 115), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (200, 200, 200), 1) + cv2.putText(frame, f'Motorraeder: {vehicle_type_counts["motorcycle"]}', (20, 135), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (200, 200, 200), 1) + + _, buffer = cv2.imencode('.jpg', frame) + frame = buffer.tobytes() + + yield (b'--frame\r\n' + b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n') + +@app.route('/webcam_feed') +def webcam_feed(): + return Response(detect_objects_from_webcam(), + mimetype='multipart/x-mixed-replace; boundary=frame') + +@app.route('/upload', methods=['POST']) +def upload_video(): + if 'file' not in request.files: + return redirect(request.url) + + file = request.files['file'] + if file.filename == '': + return redirect(request.url) + + # Save the uploaded file to the uploads folder + if not os.path.exists('uploads'): + os.makedirs('uploads') + + file_path = os.path.join('uploads', file.filename) + file.save(file_path) + + # Redirect to the video playback page after upload + return redirect(url_for('play_video', filename=file.filename)) + +@app.route('/uploads/') +def play_video(filename): + # Initialize default counting line in session if not set + if 'counting_line' not in session: + session['counting_line'] = {'x1': 0, 'y1': 300, 'x2': 1020, 'y2': 300} + return render_template('play_video.html', filename=filename) + +@app.route('/video/') +def send_video(filename): + return send_from_directory('uploads', filename) + +def detect_objects_from_video(video_path): + cap = cv2.VideoCapture(video_path) + count = 0 + + # Track vehicle positions and counted IDs + track_positions = {} # {track_id: (center_x, center_y)} + counted_ids = set() + vehicle_count = 0 + vehicle_type_counts = {'car': 0, 'truck': 0, 'bus': 0, 'motorcycle': 0} + + while cap.isOpened(): + ret, frame = cap.read() + if not ret: + break + count += 1 + if count % 2 != 0: + continue + + # Resize the frame to (1020, 600) + frame = cv2.resize(frame, (1020, 600)) + + # Get counting line from session (default to horizontal middle line) + line_data = session.get('counting_line', {'x1': 0, 'y1': 300, 'x2': 1020, 'y2': 300}) + line_start = (line_data['x1'], line_data['y1']) + line_end = (line_data['x2'], line_data['y2']) + + # Check if count should be reset + if session.get('reset_count', False): + counted_ids.clear() + vehicle_count = 0 + vehicle_type_counts = {'car': 0, 'truck': 0, 'bus': 0, 'motorcycle': 0} + session['reset_count'] = False + + # Draw counting line (dashed yellow line with black gaps) + cv2.line(frame, line_start, line_end, (0, 255, 255), 2, cv2.LINE_AA) + # Draw dashed effect + line_length = int(np.sqrt((line_end[0] - line_start[0])**2 + (line_end[1] - line_start[1])**2)) + dash_length = 20 + for i in range(0, line_length, dash_length * 2): + t1 = i / line_length + t2 = min((i + dash_length) / line_length, 1.0) + x1 = int(line_start[0] + t1 * (line_end[0] - line_start[0])) + y1 = int(line_start[1] + t1 * (line_end[1] - line_start[1])) + x2 = int(line_start[0] + t2 * (line_end[0] - line_start[0])) + y2 = int(line_start[1] + t2 * (line_end[1] - line_start[1])) + cv2.line(frame, (x1, y1), (x2, y2), (0, 0, 0), 2) + + # Run YOLOv8 tracking on the frame + results = model.track(frame, persist=True) + + if results[0].boxes is not None and results[0].boxes.id is not None: + boxes = results[0].boxes.xyxy.int().cpu().tolist() + class_ids = results[0].boxes.cls.int().cpu().tolist() + track_ids = results[0].boxes.id.int().cpu().tolist() + + for box, class_id, track_id in zip(boxes, class_ids, track_ids): + c = names[class_id] + x1, y1, x2, y2 = box + + # Calculate center point of bounding box + center_x = (x1 + x2) // 2 + center_y = (y1 + y2) // 2 + + # Check if this is a vehicle we want to count + if c in VEHICLE_CLASSES: + # If we have a previous position for this track + if track_id in track_positions and track_id not in counted_ids: + prev_x, prev_y = track_positions[track_id] + # Check if the vehicle crossed the counting line + if line_intersect((prev_x, prev_y), (center_x, center_y), line_start, line_end): + counted_ids.add(track_id) + vehicle_count += 1 + vehicle_type_counts[c] += 1 + + # Update position + track_positions[track_id] = (center_x, center_y) + + # Draw bounding box and label + cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) + cv2.putText(frame, f'{track_id} - {c}', (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 0, 255), 1) + + # Display vehicle count with type breakdown + cv2.rectangle(frame, (10, 10), (350, 140), (0, 0, 0), -1) + cv2.putText(frame, f'Gesamt: {vehicle_count}', (20, 35), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2) + cv2.putText(frame, f'Autos: {vehicle_type_counts["car"]}', (20, 65), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (200, 200, 200), 1) + cv2.putText(frame, f'LKW: {vehicle_type_counts["truck"]}', (20, 90), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (200, 200, 200), 1) + cv2.putText(frame, f'Busse: {vehicle_type_counts["bus"]}', (20, 115), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (200, 200, 200), 1) + cv2.putText(frame, f'Motorraeder: {vehicle_type_counts["motorcycle"]}', (20, 135), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (200, 200, 200), 1) + + _, buffer = cv2.imencode('.jpg', frame) + frame = buffer.tobytes() + + yield (b'--frame\r\n' + b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n') + +@app.route('/video_feed/') +def video_feed(filename): + video_path = os.path.join('uploads', filename) + return Response(detect_objects_from_video(video_path), + mimetype='multipart/x-mixed-replace; boundary=frame') + +if __name__ == '__main__': + app.run('0.0.0.0',debug=False, port=8080) \ No newline at end of file diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..129a1d7 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,40 @@ + + + + + + Object Detection + + + +

Object Detection

+ +
+ + +
+ + Start Webcam Detection + + diff --git a/templates/play_video.html b/templates/play_video.html new file mode 100644 index 0000000..02a2d4e --- /dev/null +++ b/templates/play_video.html @@ -0,0 +1,222 @@ + + + + + + Video Playback + + + +

Video Playback with Object Detection

+
+ + +
+
+ + +
+
Klicke auf "Zähllinie setzen" und dann zweimal auf das Video, um die Zähllinie zu definieren.
+ Back to Home + + + + diff --git a/templates/webcam.html b/templates/webcam.html new file mode 100644 index 0000000..7abde59 --- /dev/null +++ b/templates/webcam.html @@ -0,0 +1,222 @@ + + + + + + Webcam Feed + + + +

Webcam Object Detection

+
+ + +
+
+ + +
+
Klicke auf "Zähllinie setzen" und dann zweimal auf das Video, um die Zähllinie zu definieren.
+ Back to Home + + + +