Implementiere Fahrzeugzählung mit Linienüberquerung
- 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 <noreply@anthropic.com>
This commit is contained in:
86
CLAUDE.md
Normal file
86
CLAUDE.md
Normal file
@@ -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/<filename>` - Video playback page (play_video.html)
|
||||||
|
|
||||||
|
**Streaming Routes:**
|
||||||
|
- `/webcam_feed` - Streams processed webcam frames with detections
|
||||||
|
- `/video_feed/<filename>` - Streams processed video file frames with detections
|
||||||
|
- `/video/<filename>` - 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
|
||||||
116
README.md
Normal file
116
README.md
Normal file
@@ -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/)
|
||||||
304
app.py
Normal file
304
app.py
Normal file
@@ -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/<filename>')
|
||||||
|
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/<path:filename>')
|
||||||
|
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/<filename>')
|
||||||
|
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)
|
||||||
40
templates/index.html
Normal file
40
templates/index.html
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Object Detection</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 50px;
|
||||||
|
}
|
||||||
|
#uploadForm {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
input[type="file"] {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
#startWebcam {
|
||||||
|
padding: 10px 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Object Detection</h1>
|
||||||
|
|
||||||
|
<form id="uploadForm" action="/upload" method="post" enctype="multipart/form-data">
|
||||||
|
<input type="file" name="file" accept="video/*" required>
|
||||||
|
<input type="submit" value="Upload Video">
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<a id="startWebcam" href="/start_webcam">Start Webcam Detection</a>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
222
templates/play_video.html
Normal file
222
templates/play_video.html
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Video Playback</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.video-container {
|
||||||
|
position: relative;
|
||||||
|
width: 1020px;
|
||||||
|
height: 600px;
|
||||||
|
}
|
||||||
|
#videoFeed {
|
||||||
|
width: 1020px;
|
||||||
|
height: 600px;
|
||||||
|
border: 2px solid #ccc;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
#lineCanvas {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 1020px;
|
||||||
|
height: 600px;
|
||||||
|
cursor: crosshair;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
#lineCanvas.active {
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
.controls {
|
||||||
|
margin-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
padding: 10px 20px;
|
||||||
|
font-size: 16px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: #007bff;
|
||||||
|
color: white;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
button:hover {
|
||||||
|
background-color: #0056b3;
|
||||||
|
}
|
||||||
|
button.active {
|
||||||
|
background-color: #28a745;
|
||||||
|
}
|
||||||
|
button.danger {
|
||||||
|
background-color: #dc3545;
|
||||||
|
}
|
||||||
|
button.danger:hover {
|
||||||
|
background-color: #c82333;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
margin-top: 20px;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #007bff;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.info {
|
||||||
|
margin-top: 10px;
|
||||||
|
color: #555;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Video Playback with Object Detection</h1>
|
||||||
|
<div class="video-container">
|
||||||
|
<img id="videoFeed" src="{{ url_for('video_feed', filename=filename) }}" />
|
||||||
|
<canvas id="lineCanvas"></canvas>
|
||||||
|
</div>
|
||||||
|
<div class="controls">
|
||||||
|
<button id="setLineBtn">Zähllinie setzen</button>
|
||||||
|
<button id="resetCountBtn" class="danger">Zähler zurücksetzen</button>
|
||||||
|
</div>
|
||||||
|
<div class="info" id="infoText">Klicke auf "Zähllinie setzen" und dann zweimal auf das Video, um die Zähllinie zu definieren.</div>
|
||||||
|
<a href="/">Back to Home</a>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const canvas = document.getElementById('lineCanvas');
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
const setLineBtn = document.getElementById('setLineBtn');
|
||||||
|
const resetCountBtn = document.getElementById('resetCountBtn');
|
||||||
|
const infoText = document.getElementById('infoText');
|
||||||
|
|
||||||
|
let isSettingLine = false;
|
||||||
|
let firstPoint = null;
|
||||||
|
let currentLine = null;
|
||||||
|
|
||||||
|
// Load existing line from server
|
||||||
|
fetch('/api/get_line')
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
currentLine = data;
|
||||||
|
drawLine();
|
||||||
|
});
|
||||||
|
|
||||||
|
setLineBtn.addEventListener('click', () => {
|
||||||
|
isSettingLine = !isSettingLine;
|
||||||
|
if (isSettingLine) {
|
||||||
|
setLineBtn.textContent = 'Abbrechen';
|
||||||
|
setLineBtn.classList.add('active');
|
||||||
|
canvas.classList.add('active');
|
||||||
|
firstPoint = null;
|
||||||
|
infoText.textContent = 'Klicke auf den Startpunkt der Zähllinie...';
|
||||||
|
} else {
|
||||||
|
setLineBtn.textContent = 'Zähllinie setzen';
|
||||||
|
setLineBtn.classList.remove('active');
|
||||||
|
canvas.classList.remove('active');
|
||||||
|
firstPoint = null;
|
||||||
|
infoText.textContent = 'Klicke auf "Zähllinie setzen" und dann zweimal auf das Video, um die Zähllinie zu definieren.';
|
||||||
|
drawLine();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
canvas.addEventListener('click', (e) => {
|
||||||
|
if (!isSettingLine) return;
|
||||||
|
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
const x = Math.round(e.clientX - rect.left);
|
||||||
|
const y = Math.round(e.clientY - rect.top);
|
||||||
|
|
||||||
|
if (!firstPoint) {
|
||||||
|
firstPoint = { x, y };
|
||||||
|
infoText.textContent = 'Klicke auf den Endpunkt der Zähllinie...';
|
||||||
|
drawTemporaryPoint(x, y);
|
||||||
|
} else {
|
||||||
|
currentLine = { x1: firstPoint.x, y1: firstPoint.y, x2: x, y2: y };
|
||||||
|
|
||||||
|
// Send line to server
|
||||||
|
fetch('/api/set_line', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(currentLine)
|
||||||
|
})
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
console.log('Line set:', data);
|
||||||
|
infoText.textContent = 'Zähllinie erfolgreich gesetzt!';
|
||||||
|
setTimeout(() => {
|
||||||
|
infoText.textContent = 'Klicke auf "Zähllinie setzen" und dann zweimal auf das Video, um die Zähllinie zu definieren.';
|
||||||
|
}, 2000);
|
||||||
|
});
|
||||||
|
|
||||||
|
isSettingLine = false;
|
||||||
|
setLineBtn.textContent = 'Zähllinie setzen';
|
||||||
|
setLineBtn.classList.remove('active');
|
||||||
|
canvas.classList.remove('active');
|
||||||
|
firstPoint = null;
|
||||||
|
drawLine();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
resetCountBtn.addEventListener('click', () => {
|
||||||
|
fetch('/api/reset_count', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
})
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
console.log('Count reset:', data);
|
||||||
|
infoText.textContent = 'Zähler wurde zurückgesetzt!';
|
||||||
|
setTimeout(() => {
|
||||||
|
infoText.textContent = 'Klicke auf "Zähllinie setzen" und dann zweimal auf das Video, um die Zähllinie zu definieren.';
|
||||||
|
}, 2000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function drawLine() {
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
if (currentLine) {
|
||||||
|
ctx.strokeStyle = 'rgba(255, 255, 0, 0.8)';
|
||||||
|
ctx.lineWidth = 3;
|
||||||
|
ctx.setLineDash([10, 10]);
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(currentLine.x1, currentLine.y1);
|
||||||
|
ctx.lineTo(currentLine.x2, currentLine.y2);
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.setLineDash([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawTemporaryPoint(x, y) {
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
drawLine();
|
||||||
|
ctx.fillStyle = 'rgba(255, 255, 0, 0.8)';
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(x, y, 5, 0, 2 * Math.PI);
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redraw line periodically in case it gets cleared
|
||||||
|
setInterval(() => {
|
||||||
|
if (!isSettingLine && currentLine) {
|
||||||
|
drawLine();
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
222
templates/webcam.html
Normal file
222
templates/webcam.html
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Webcam Feed</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.video-container {
|
||||||
|
position: relative;
|
||||||
|
width: 1020px;
|
||||||
|
height: 600px;
|
||||||
|
}
|
||||||
|
#videoFeed {
|
||||||
|
width: 1020px;
|
||||||
|
height: 600px;
|
||||||
|
border: 2px solid #ccc;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
#lineCanvas {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 1020px;
|
||||||
|
height: 600px;
|
||||||
|
cursor: crosshair;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
#lineCanvas.active {
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
.controls {
|
||||||
|
margin-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
padding: 10px 20px;
|
||||||
|
font-size: 16px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: #007bff;
|
||||||
|
color: white;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
button:hover {
|
||||||
|
background-color: #0056b3;
|
||||||
|
}
|
||||||
|
button.active {
|
||||||
|
background-color: #28a745;
|
||||||
|
}
|
||||||
|
button.danger {
|
||||||
|
background-color: #dc3545;
|
||||||
|
}
|
||||||
|
button.danger:hover {
|
||||||
|
background-color: #c82333;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
margin-top: 20px;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #007bff;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.info {
|
||||||
|
margin-top: 10px;
|
||||||
|
color: #555;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Webcam Object Detection</h1>
|
||||||
|
<div class="video-container">
|
||||||
|
<img id="videoFeed" src="{{ url_for('webcam_feed') }}" />
|
||||||
|
<canvas id="lineCanvas"></canvas>
|
||||||
|
</div>
|
||||||
|
<div class="controls">
|
||||||
|
<button id="setLineBtn">Zähllinie setzen</button>
|
||||||
|
<button id="resetCountBtn" class="danger">Zähler zurücksetzen</button>
|
||||||
|
</div>
|
||||||
|
<div class="info" id="infoText">Klicke auf "Zähllinie setzen" und dann zweimal auf das Video, um die Zähllinie zu definieren.</div>
|
||||||
|
<a href="/">Back to Home</a>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const canvas = document.getElementById('lineCanvas');
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
const setLineBtn = document.getElementById('setLineBtn');
|
||||||
|
const resetCountBtn = document.getElementById('resetCountBtn');
|
||||||
|
const infoText = document.getElementById('infoText');
|
||||||
|
|
||||||
|
let isSettingLine = false;
|
||||||
|
let firstPoint = null;
|
||||||
|
let currentLine = null;
|
||||||
|
|
||||||
|
// Load existing line from server
|
||||||
|
fetch('/api/get_line')
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
currentLine = data;
|
||||||
|
drawLine();
|
||||||
|
});
|
||||||
|
|
||||||
|
setLineBtn.addEventListener('click', () => {
|
||||||
|
isSettingLine = !isSettingLine;
|
||||||
|
if (isSettingLine) {
|
||||||
|
setLineBtn.textContent = 'Abbrechen';
|
||||||
|
setLineBtn.classList.add('active');
|
||||||
|
canvas.classList.add('active');
|
||||||
|
firstPoint = null;
|
||||||
|
infoText.textContent = 'Klicke auf den Startpunkt der Zähllinie...';
|
||||||
|
} else {
|
||||||
|
setLineBtn.textContent = 'Zähllinie setzen';
|
||||||
|
setLineBtn.classList.remove('active');
|
||||||
|
canvas.classList.remove('active');
|
||||||
|
firstPoint = null;
|
||||||
|
infoText.textContent = 'Klicke auf "Zähllinie setzen" und dann zweimal auf das Video, um die Zähllinie zu definieren.';
|
||||||
|
drawLine();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
canvas.addEventListener('click', (e) => {
|
||||||
|
if (!isSettingLine) return;
|
||||||
|
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
const x = Math.round(e.clientX - rect.left);
|
||||||
|
const y = Math.round(e.clientY - rect.top);
|
||||||
|
|
||||||
|
if (!firstPoint) {
|
||||||
|
firstPoint = { x, y };
|
||||||
|
infoText.textContent = 'Klicke auf den Endpunkt der Zähllinie...';
|
||||||
|
drawTemporaryPoint(x, y);
|
||||||
|
} else {
|
||||||
|
currentLine = { x1: firstPoint.x, y1: firstPoint.y, x2: x, y2: y };
|
||||||
|
|
||||||
|
// Send line to server
|
||||||
|
fetch('/api/set_line', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(currentLine)
|
||||||
|
})
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
console.log('Line set:', data);
|
||||||
|
infoText.textContent = 'Zähllinie erfolgreich gesetzt!';
|
||||||
|
setTimeout(() => {
|
||||||
|
infoText.textContent = 'Klicke auf "Zähllinie setzen" und dann zweimal auf das Video, um die Zähllinie zu definieren.';
|
||||||
|
}, 2000);
|
||||||
|
});
|
||||||
|
|
||||||
|
isSettingLine = false;
|
||||||
|
setLineBtn.textContent = 'Zähllinie setzen';
|
||||||
|
setLineBtn.classList.remove('active');
|
||||||
|
canvas.classList.remove('active');
|
||||||
|
firstPoint = null;
|
||||||
|
drawLine();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
resetCountBtn.addEventListener('click', () => {
|
||||||
|
fetch('/api/reset_count', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
})
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
console.log('Count reset:', data);
|
||||||
|
infoText.textContent = 'Zähler wurde zurückgesetzt!';
|
||||||
|
setTimeout(() => {
|
||||||
|
infoText.textContent = 'Klicke auf "Zähllinie setzen" und dann zweimal auf das Video, um die Zähllinie zu definieren.';
|
||||||
|
}, 2000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function drawLine() {
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
if (currentLine) {
|
||||||
|
ctx.strokeStyle = 'rgba(255, 255, 0, 0.8)';
|
||||||
|
ctx.lineWidth = 3;
|
||||||
|
ctx.setLineDash([10, 10]);
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(currentLine.x1, currentLine.y1);
|
||||||
|
ctx.lineTo(currentLine.x2, currentLine.y2);
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.setLineDash([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawTemporaryPoint(x, y) {
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
drawLine();
|
||||||
|
ctx.fillStyle = 'rgba(255, 255, 0, 0.8)';
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(x, y, 5, 0, 2 * Math.PI);
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redraw line periodically in case it gets cleared
|
||||||
|
setInterval(() => {
|
||||||
|
if (!isSettingLine && currentLine) {
|
||||||
|
drawLine();
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user