new apps
This commit is contained in:
14
app.py
14
app.py
@@ -315,13 +315,13 @@ def play_video(filename):
|
|||||||
def send_video(filename):
|
def send_video(filename):
|
||||||
return send_from_directory(UPLOAD_DIR, filename)
|
return send_from_directory(UPLOAD_DIR, filename)
|
||||||
|
|
||||||
def detect_objects_from_video(video_path, line_data, stream_id):
|
def detect_objects_from_video(video_path, line_data, stream_id):
|
||||||
cap = cv2.VideoCapture(video_path)
|
cap = cv2.VideoCapture(video_path)
|
||||||
if not cap.isOpened():
|
if not cap.isOpened():
|
||||||
cap.release()
|
cap.release()
|
||||||
raise RuntimeError('Video konnte nicht geöffnet werden')
|
raise RuntimeError('Video konnte nicht geöffnet werden')
|
||||||
return generate_frames(cap, line_data, stream_id)
|
return generate_frames(cap, line_data, stream_id)
|
||||||
|
|
||||||
@app.route('/video_feed/<filename>')
|
@app.route('/video_feed/<filename>')
|
||||||
def video_feed(filename):
|
def video_feed(filename):
|
||||||
safe_filename = os.path.basename(filename)
|
safe_filename = os.path.basename(filename)
|
||||||
|
|||||||
343
app2.app
Normal file
343
app2.app
Normal file
@@ -0,0 +1,343 @@
|
|||||||
|
import os
|
||||||
|
import uuid
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
from threading import Event, Lock
|
||||||
|
from flask import (
|
||||||
|
Flask,
|
||||||
|
render_template,
|
||||||
|
Response,
|
||||||
|
request,
|
||||||
|
redirect,
|
||||||
|
url_for,
|
||||||
|
send_from_directory,
|
||||||
|
session,
|
||||||
|
jsonify,
|
||||||
|
abort,
|
||||||
|
)
|
||||||
|
from werkzeug.utils import secure_filename
|
||||||
|
from ultralytics import YOLO
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.secret_key = os.environ.get('SECRET_KEY', 'vehicle_dev_secret')
|
||||||
|
app.config['MAX_CONTENT_LENGTH'] = 200 * 1024 * 1024 # 200MB upload cap
|
||||||
|
|
||||||
|
# Load the YOLOv8 model
|
||||||
|
model = YOLO("yolo11s.pt")
|
||||||
|
names = model.model.names
|
||||||
|
|
||||||
|
# Vehicle classes to count
|
||||||
|
VEHICLE_CLASSES = {'car', 'truck', 'bus', 'motorcycle'}
|
||||||
|
ALLOWED_EXTENSIONS = {'mp4', 'mov', 'avi', 'mkv'}
|
||||||
|
UPLOAD_DIR = 'uploads'
|
||||||
|
|
||||||
|
# Track reset events per stream (webcam/video per session)
|
||||||
|
reset_events: dict[str, Event] = {}
|
||||||
|
reset_lock = Lock()
|
||||||
|
|
||||||
|
|
||||||
|
def allowed_file(filename: str) -> bool:
|
||||||
|
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_upload_dir() -> None:
|
||||||
|
if not os.path.exists(UPLOAD_DIR):
|
||||||
|
os.makedirs(UPLOAD_DIR)
|
||||||
|
|
||||||
|
|
||||||
|
def get_line_from_session():
|
||||||
|
if 'counting_line' not in session:
|
||||||
|
session['counting_line'] = {'x1': 0, 'y1': 300, 'x2': 1020, 'y2': 300}
|
||||||
|
return session['counting_line']
|
||||||
|
|
||||||
|
|
||||||
|
def fresh_vehicle_counts() -> dict[str, int]:
|
||||||
|
return {vehicle: 0 for vehicle in VEHICLE_CLASSES}
|
||||||
|
|
||||||
|
|
||||||
|
def get_reset_event(stream_id: str) -> Event:
|
||||||
|
with reset_lock:
|
||||||
|
event = reset_events.get(stream_id)
|
||||||
|
if event is None:
|
||||||
|
event = Event()
|
||||||
|
reset_events[stream_id] = event
|
||||||
|
return event
|
||||||
|
|
||||||
|
|
||||||
|
def release_reset_event(stream_id: str) -> None:
|
||||||
|
with reset_lock:
|
||||||
|
reset_events.pop(stream_id, None)
|
||||||
|
|
||||||
|
|
||||||
|
def get_webcam_stream_id() -> str:
|
||||||
|
stream_id = session.get('webcam_stream_id')
|
||||||
|
if not stream_id:
|
||||||
|
stream_id = f'webcam-{uuid.uuid4().hex}'
|
||||||
|
session['webcam_stream_id'] = stream_id
|
||||||
|
return stream_id
|
||||||
|
|
||||||
|
|
||||||
|
def get_video_stream_id(filename: str) -> str:
|
||||||
|
video_streams = session.get('video_stream_ids', {})
|
||||||
|
stream_id = video_streams.get(filename)
|
||||||
|
if not stream_id:
|
||||||
|
stream_id = f'video-{uuid.uuid4().hex}'
|
||||||
|
video_streams[filename] = stream_id
|
||||||
|
session['video_stream_ids'] = video_streams
|
||||||
|
return stream_id
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
def crossed_line(prev_pos, curr_pos, line_start, line_end):
|
||||||
|
"""
|
||||||
|
Check if movement from prev_pos to curr_pos crossed the line.
|
||||||
|
Uses orientation check - more robust for frame skipping.
|
||||||
|
"""
|
||||||
|
# Check if the two line segments intersect
|
||||||
|
if line_intersect(prev_pos, curr_pos, line_start, line_end):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def generate_frames(capture, line_data, stream_id: str):
|
||||||
|
"""
|
||||||
|
Shared frame generator for webcam and uploaded videos.
|
||||||
|
Handles detection, drawing overlays, and reset events.
|
||||||
|
"""
|
||||||
|
track_positions = {}
|
||||||
|
counted_ids = set()
|
||||||
|
vehicle_count = 0
|
||||||
|
vehicle_type_counts = fresh_vehicle_counts()
|
||||||
|
line_start = (line_data['x1'], line_data['y1'])
|
||||||
|
line_end = (line_data['x2'], line_data['y2'])
|
||||||
|
frame_idx = 0
|
||||||
|
reset_event = get_reset_event(stream_id)
|
||||||
|
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
ret, frame = capture.read()
|
||||||
|
if not ret:
|
||||||
|
break
|
||||||
|
|
||||||
|
frame_idx += 1
|
||||||
|
if frame_idx % 2 != 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if reset_event.is_set():
|
||||||
|
track_positions.clear()
|
||||||
|
counted_ids.clear()
|
||||||
|
vehicle_count = 0
|
||||||
|
vehicle_type_counts = fresh_vehicle_counts()
|
||||||
|
reset_event.clear()
|
||||||
|
|
||||||
|
frame = cv2.resize(frame, (1020, 600))
|
||||||
|
results = model.track(frame, persist=True)
|
||||||
|
|
||||||
|
if results and 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):
|
||||||
|
label_name = names[class_id]
|
||||||
|
x1, y1, x2, y2 = box
|
||||||
|
center_x = (x1 + x2) // 2
|
||||||
|
center_y = (y1 + y2) // 2
|
||||||
|
|
||||||
|
if label_name in VEHICLE_CLASSES:
|
||||||
|
if track_id in track_positions and track_id not in counted_ids:
|
||||||
|
prev_x, prev_y = track_positions[track_id]
|
||||||
|
cv2.line(frame, (prev_x, prev_y), (center_x, center_y), (255, 100, 0), 2)
|
||||||
|
|
||||||
|
if crossed_line((prev_x, prev_y), (center_x, center_y), line_start, line_end):
|
||||||
|
counted_ids.add(track_id)
|
||||||
|
vehicle_count += 1
|
||||||
|
vehicle_type_counts[label_name] += 1
|
||||||
|
cv2.circle(frame, (center_x, center_y), 25, (0, 255, 0), 5)
|
||||||
|
|
||||||
|
track_positions[track_id] = (center_x, center_y)
|
||||||
|
|
||||||
|
box_color = (0, 255, 0) if label_name in VEHICLE_CLASSES else (255, 0, 0)
|
||||||
|
cv2.rectangle(frame, (x1, y1), (x2, y2), box_color, 2)
|
||||||
|
|
||||||
|
label = f'{track_id} - {label_name}'
|
||||||
|
if track_id in counted_ids:
|
||||||
|
label += ' ✓'
|
||||||
|
cv2.putText(frame, label, (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 0, 255), 1)
|
||||||
|
cv2.circle(frame, (center_x, center_y), 3, (0, 255, 255), -1)
|
||||||
|
|
||||||
|
cv2.line(frame, line_start, line_end, (0, 255, 255), 3, cv2.LINE_AA)
|
||||||
|
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, max(line_length, 1), dash_length * 2):
|
||||||
|
t1 = i / line_length if line_length else 0
|
||||||
|
t2 = min((i + dash_length) / line_length, 1.0) if line_length else 0
|
||||||
|
x1_dash = int(line_start[0] + t1 * (line_end[0] - line_start[0]))
|
||||||
|
y1_dash = int(line_start[1] + t1 * (line_end[1] - line_start[1]))
|
||||||
|
x2_dash = int(line_start[0] + t2 * (line_end[0] - line_start[0]))
|
||||||
|
y2_dash = int(line_start[1] + t2 * (line_end[1] - line_start[1]))
|
||||||
|
cv2.line(frame, (x1_dash, y1_dash), (x2_dash, y2_dash), (0, 0, 0), 3)
|
||||||
|
|
||||||
|
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.get('car', 0)}", (20, 65), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (200, 200, 200), 1)
|
||||||
|
cv2.putText(frame, f"LKW: {vehicle_type_counts.get('truck', 0)}", (20, 90), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (200, 200, 200), 1)
|
||||||
|
cv2.putText(frame, f"Busse: {vehicle_type_counts.get('bus', 0)}", (20, 115), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (200, 200, 200), 1)
|
||||||
|
cv2.putText(frame, f"Motorraeder: {vehicle_type_counts.get('motorcycle', 0)}", (20, 135), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (200, 200, 200), 1)
|
||||||
|
|
||||||
|
_, buffer = cv2.imencode('.jpg', frame)
|
||||||
|
frame_bytes = buffer.tobytes()
|
||||||
|
|
||||||
|
yield (b'--frame\r\n'
|
||||||
|
b'Content-Type: image/jpeg\r\n\r\n' + frame_bytes + b'\r\n')
|
||||||
|
finally:
|
||||||
|
capture.release()
|
||||||
|
release_reset_event(stream_id)
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def index():
|
||||||
|
return render_template('index.html')
|
||||||
|
|
||||||
|
@app.route('/start_webcam')
|
||||||
|
def start_webcam():
|
||||||
|
get_line_from_session()
|
||||||
|
stream_id = get_webcam_stream_id()
|
||||||
|
return render_template('webcam.html', stream_id=stream_id)
|
||||||
|
|
||||||
|
@app.route('/api/set_line', methods=['POST'])
|
||||||
|
def set_counting_line():
|
||||||
|
"""API endpoint to set the counting line coordinates"""
|
||||||
|
data = request.get_json(silent=True) or {}
|
||||||
|
try:
|
||||||
|
session['counting_line'] = {
|
||||||
|
'x1': int(data['x1']),
|
||||||
|
'y1': int(data['y1']),
|
||||||
|
'x2': int(data['x2']),
|
||||||
|
'y2': int(data['y2'])
|
||||||
|
}
|
||||||
|
except (KeyError, ValueError, TypeError):
|
||||||
|
abort(400, description='Ungültige Linienkoordinaten')
|
||||||
|
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"""
|
||||||
|
return jsonify(get_line_from_session())
|
||||||
|
|
||||||
|
@app.route('/api/reset_count', methods=['POST'])
|
||||||
|
def reset_count():
|
||||||
|
"""API endpoint to reset the vehicle count for a stream"""
|
||||||
|
data = request.get_json(silent=True) or {}
|
||||||
|
stream_id = data.get('stream_id')
|
||||||
|
if not stream_id:
|
||||||
|
abort(400, description='stream_id ist erforderlich')
|
||||||
|
valid_ids = {session.get('webcam_stream_id')}
|
||||||
|
valid_ids.update(session.get('video_stream_ids', {}).values())
|
||||||
|
valid_ids.discard(None)
|
||||||
|
if stream_id not in valid_ids:
|
||||||
|
abort(403, description='Stream gehört nicht zur aktuellen Sitzung')
|
||||||
|
event = get_reset_event(stream_id)
|
||||||
|
event.set()
|
||||||
|
return jsonify({'status': 'success', 'message': 'Zähler wird zurückgesetzt'})
|
||||||
|
|
||||||
|
def detect_objects_from_webcam(line_data, stream_id):
|
||||||
|
cap = cv2.VideoCapture("http://CAMERA-IP:81/stream")
|
||||||
|
if not cap.isOpened():
|
||||||
|
cap.release()
|
||||||
|
raise RuntimeError('Webcam konnte nicht geöffnet werden')
|
||||||
|
return generate_frames(cap, line_data, stream_id)
|
||||||
|
|
||||||
|
@app.route('/webcam_feed')
|
||||||
|
def webcam_feed():
|
||||||
|
line_data = get_line_from_session()
|
||||||
|
stream_id = get_webcam_stream_id()
|
||||||
|
try:
|
||||||
|
generator = detect_objects_from_webcam(line_data, stream_id)
|
||||||
|
except RuntimeError as exc:
|
||||||
|
abort(503, description=str(exc))
|
||||||
|
return Response(generator,
|
||||||
|
mimetype='multipart/x-mixed-replace; boundary=frame')
|
||||||
|
|
||||||
|
@app.route('/upload', methods=['POST'])
|
||||||
|
def upload_video():
|
||||||
|
if 'file' not in request.files:
|
||||||
|
abort(400, description='Keine Datei erhalten')
|
||||||
|
|
||||||
|
file = request.files['file']
|
||||||
|
if not file or file.filename == '':
|
||||||
|
abort(400, description='Keine Datei ausgewählt')
|
||||||
|
|
||||||
|
filename = secure_filename(file.filename)
|
||||||
|
if not filename:
|
||||||
|
abort(400, description='Ungültiger Dateiname')
|
||||||
|
if not allowed_file(filename):
|
||||||
|
abort(400, description='Ungültiger Dateityp')
|
||||||
|
|
||||||
|
ensure_upload_dir()
|
||||||
|
name, ext = os.path.splitext(filename)
|
||||||
|
stored_filename = f"{name}_{uuid.uuid4().hex}{ext.lower()}"
|
||||||
|
file_path = os.path.join(UPLOAD_DIR, stored_filename)
|
||||||
|
file.save(file_path)
|
||||||
|
|
||||||
|
return redirect(url_for('play_video', filename=stored_filename))
|
||||||
|
|
||||||
|
@app.route('/uploads/<filename>')
|
||||||
|
def play_video(filename):
|
||||||
|
safe_filename = os.path.basename(filename)
|
||||||
|
if safe_filename != filename:
|
||||||
|
abort(400, description='Ungültiger Dateiname')
|
||||||
|
file_path = os.path.join(UPLOAD_DIR, safe_filename)
|
||||||
|
if not os.path.isfile(file_path):
|
||||||
|
abort(404)
|
||||||
|
get_line_from_session()
|
||||||
|
stream_id = get_video_stream_id(safe_filename)
|
||||||
|
return render_template('play_video.html', filename=safe_filename, stream_id=stream_id)
|
||||||
|
|
||||||
|
@app.route('/video/<path:filename>')
|
||||||
|
def send_video(filename):
|
||||||
|
return send_from_directory(UPLOAD_DIR, filename)
|
||||||
|
|
||||||
|
def detect_objects_from_video(video_path, line_data, stream_id):
|
||||||
|
cap = cv2.VideoCapture(video_path)
|
||||||
|
if not cap.isOpened():
|
||||||
|
cap.release()
|
||||||
|
raise RuntimeError('Video konnte nicht geöffnet werden')
|
||||||
|
return generate_frames(cap, line_data, stream_id)
|
||||||
|
|
||||||
|
@app.route('/video_feed/<filename>')
|
||||||
|
def video_feed(filename):
|
||||||
|
safe_filename = os.path.basename(filename)
|
||||||
|
if safe_filename != filename:
|
||||||
|
abort(400, description='Ungültiger Dateiname')
|
||||||
|
video_path = os.path.join(UPLOAD_DIR, safe_filename)
|
||||||
|
if not os.path.isfile(video_path):
|
||||||
|
abort(404)
|
||||||
|
line_data = get_line_from_session()
|
||||||
|
stream_id = get_video_stream_id(safe_filename)
|
||||||
|
try:
|
||||||
|
generator = detect_objects_from_video(video_path, line_data, stream_id)
|
||||||
|
except RuntimeError as exc:
|
||||||
|
abort(503, description=str(exc))
|
||||||
|
return Response(generator,
|
||||||
|
mimetype='multipart/x-mixed-replace; boundary=frame')
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run('0.0.0.0',debug=False, port=8080)
|
||||||
343
app2.py
Normal file
343
app2.py
Normal file
@@ -0,0 +1,343 @@
|
|||||||
|
import os
|
||||||
|
import uuid
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
from threading import Event, Lock
|
||||||
|
from flask import (
|
||||||
|
Flask,
|
||||||
|
render_template,
|
||||||
|
Response,
|
||||||
|
request,
|
||||||
|
redirect,
|
||||||
|
url_for,
|
||||||
|
send_from_directory,
|
||||||
|
session,
|
||||||
|
jsonify,
|
||||||
|
abort,
|
||||||
|
)
|
||||||
|
from werkzeug.utils import secure_filename
|
||||||
|
from ultralytics import YOLO
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.secret_key = os.environ.get('SECRET_KEY', 'vehicle_dev_secret')
|
||||||
|
app.config['MAX_CONTENT_LENGTH'] = 200 * 1024 * 1024 # 200MB upload cap
|
||||||
|
|
||||||
|
# Load the YOLOv8 model
|
||||||
|
model = YOLO("yolo11s.pt")
|
||||||
|
names = model.model.names
|
||||||
|
|
||||||
|
# Vehicle classes to count
|
||||||
|
VEHICLE_CLASSES = {'car', 'truck', 'bus', 'motorcycle'}
|
||||||
|
ALLOWED_EXTENSIONS = {'mp4', 'mov', 'avi', 'mkv'}
|
||||||
|
UPLOAD_DIR = 'uploads'
|
||||||
|
|
||||||
|
# Track reset events per stream (webcam/video per session)
|
||||||
|
reset_events: dict[str, Event] = {}
|
||||||
|
reset_lock = Lock()
|
||||||
|
|
||||||
|
|
||||||
|
def allowed_file(filename: str) -> bool:
|
||||||
|
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_upload_dir() -> None:
|
||||||
|
if not os.path.exists(UPLOAD_DIR):
|
||||||
|
os.makedirs(UPLOAD_DIR)
|
||||||
|
|
||||||
|
|
||||||
|
def get_line_from_session():
|
||||||
|
if 'counting_line' not in session:
|
||||||
|
session['counting_line'] = {'x1': 0, 'y1': 300, 'x2': 1020, 'y2': 300}
|
||||||
|
return session['counting_line']
|
||||||
|
|
||||||
|
|
||||||
|
def fresh_vehicle_counts() -> dict[str, int]:
|
||||||
|
return {vehicle: 0 for vehicle in VEHICLE_CLASSES}
|
||||||
|
|
||||||
|
|
||||||
|
def get_reset_event(stream_id: str) -> Event:
|
||||||
|
with reset_lock:
|
||||||
|
event = reset_events.get(stream_id)
|
||||||
|
if event is None:
|
||||||
|
event = Event()
|
||||||
|
reset_events[stream_id] = event
|
||||||
|
return event
|
||||||
|
|
||||||
|
|
||||||
|
def release_reset_event(stream_id: str) -> None:
|
||||||
|
with reset_lock:
|
||||||
|
reset_events.pop(stream_id, None)
|
||||||
|
|
||||||
|
|
||||||
|
def get_webcam_stream_id() -> str:
|
||||||
|
stream_id = session.get('webcam_stream_id')
|
||||||
|
if not stream_id:
|
||||||
|
stream_id = f'webcam-{uuid.uuid4().hex}'
|
||||||
|
session['webcam_stream_id'] = stream_id
|
||||||
|
return stream_id
|
||||||
|
|
||||||
|
|
||||||
|
def get_video_stream_id(filename: str) -> str:
|
||||||
|
video_streams = session.get('video_stream_ids', {})
|
||||||
|
stream_id = video_streams.get(filename)
|
||||||
|
if not stream_id:
|
||||||
|
stream_id = f'video-{uuid.uuid4().hex}'
|
||||||
|
video_streams[filename] = stream_id
|
||||||
|
session['video_stream_ids'] = video_streams
|
||||||
|
return stream_id
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
def crossed_line(prev_pos, curr_pos, line_start, line_end):
|
||||||
|
"""
|
||||||
|
Check if movement from prev_pos to curr_pos crossed the line.
|
||||||
|
Uses orientation check - more robust for frame skipping.
|
||||||
|
"""
|
||||||
|
# Check if the two line segments intersect
|
||||||
|
if line_intersect(prev_pos, curr_pos, line_start, line_end):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def generate_frames(capture, line_data, stream_id: str):
|
||||||
|
"""
|
||||||
|
Shared frame generator for webcam and uploaded videos.
|
||||||
|
Handles detection, drawing overlays, and reset events.
|
||||||
|
"""
|
||||||
|
track_positions = {}
|
||||||
|
counted_ids = set()
|
||||||
|
vehicle_count = 0
|
||||||
|
vehicle_type_counts = fresh_vehicle_counts()
|
||||||
|
line_start = (line_data['x1'], line_data['y1'])
|
||||||
|
line_end = (line_data['x2'], line_data['y2'])
|
||||||
|
frame_idx = 0
|
||||||
|
reset_event = get_reset_event(stream_id)
|
||||||
|
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
ret, frame = capture.read()
|
||||||
|
if not ret:
|
||||||
|
break
|
||||||
|
|
||||||
|
frame_idx += 1
|
||||||
|
if frame_idx % 2 != 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if reset_event.is_set():
|
||||||
|
track_positions.clear()
|
||||||
|
counted_ids.clear()
|
||||||
|
vehicle_count = 0
|
||||||
|
vehicle_type_counts = fresh_vehicle_counts()
|
||||||
|
reset_event.clear()
|
||||||
|
|
||||||
|
frame = cv2.resize(frame, (1020, 600))
|
||||||
|
results = model.track(frame, persist=True)
|
||||||
|
|
||||||
|
if results and 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):
|
||||||
|
label_name = names[class_id]
|
||||||
|
x1, y1, x2, y2 = box
|
||||||
|
center_x = (x1 + x2) // 2
|
||||||
|
center_y = (y1 + y2) // 2
|
||||||
|
|
||||||
|
if label_name in VEHICLE_CLASSES:
|
||||||
|
if track_id in track_positions and track_id not in counted_ids:
|
||||||
|
prev_x, prev_y = track_positions[track_id]
|
||||||
|
cv2.line(frame, (prev_x, prev_y), (center_x, center_y), (255, 100, 0), 2)
|
||||||
|
|
||||||
|
if crossed_line((prev_x, prev_y), (center_x, center_y), line_start, line_end):
|
||||||
|
counted_ids.add(track_id)
|
||||||
|
vehicle_count += 1
|
||||||
|
vehicle_type_counts[label_name] += 1
|
||||||
|
cv2.circle(frame, (center_x, center_y), 25, (0, 255, 0), 5)
|
||||||
|
|
||||||
|
track_positions[track_id] = (center_x, center_y)
|
||||||
|
|
||||||
|
box_color = (0, 255, 0) if label_name in VEHICLE_CLASSES else (255, 0, 0)
|
||||||
|
cv2.rectangle(frame, (x1, y1), (x2, y2), box_color, 2)
|
||||||
|
|
||||||
|
label = f'{track_id} - {label_name}'
|
||||||
|
if track_id in counted_ids:
|
||||||
|
label += ' ✓'
|
||||||
|
cv2.putText(frame, label, (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 0, 255), 1)
|
||||||
|
cv2.circle(frame, (center_x, center_y), 3, (0, 255, 255), -1)
|
||||||
|
|
||||||
|
cv2.line(frame, line_start, line_end, (0, 255, 255), 3, cv2.LINE_AA)
|
||||||
|
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, max(line_length, 1), dash_length * 2):
|
||||||
|
t1 = i / line_length if line_length else 0
|
||||||
|
t2 = min((i + dash_length) / line_length, 1.0) if line_length else 0
|
||||||
|
x1_dash = int(line_start[0] + t1 * (line_end[0] - line_start[0]))
|
||||||
|
y1_dash = int(line_start[1] + t1 * (line_end[1] - line_start[1]))
|
||||||
|
x2_dash = int(line_start[0] + t2 * (line_end[0] - line_start[0]))
|
||||||
|
y2_dash = int(line_start[1] + t2 * (line_end[1] - line_start[1]))
|
||||||
|
cv2.line(frame, (x1_dash, y1_dash), (x2_dash, y2_dash), (0, 0, 0), 3)
|
||||||
|
|
||||||
|
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.get('car', 0)}", (20, 65), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (200, 200, 200), 1)
|
||||||
|
cv2.putText(frame, f"LKW: {vehicle_type_counts.get('truck', 0)}", (20, 90), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (200, 200, 200), 1)
|
||||||
|
cv2.putText(frame, f"Busse: {vehicle_type_counts.get('bus', 0)}", (20, 115), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (200, 200, 200), 1)
|
||||||
|
cv2.putText(frame, f"Motorraeder: {vehicle_type_counts.get('motorcycle', 0)}", (20, 135), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (200, 200, 200), 1)
|
||||||
|
|
||||||
|
_, buffer = cv2.imencode('.jpg', frame)
|
||||||
|
frame_bytes = buffer.tobytes()
|
||||||
|
|
||||||
|
yield (b'--frame\r\n'
|
||||||
|
b'Content-Type: image/jpeg\r\n\r\n' + frame_bytes + b'\r\n')
|
||||||
|
finally:
|
||||||
|
capture.release()
|
||||||
|
release_reset_event(stream_id)
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def index():
|
||||||
|
return render_template('index.html')
|
||||||
|
|
||||||
|
@app.route('/start_webcam')
|
||||||
|
def start_webcam():
|
||||||
|
get_line_from_session()
|
||||||
|
stream_id = get_webcam_stream_id()
|
||||||
|
return render_template('webcam.html', stream_id=stream_id)
|
||||||
|
|
||||||
|
@app.route('/api/set_line', methods=['POST'])
|
||||||
|
def set_counting_line():
|
||||||
|
"""API endpoint to set the counting line coordinates"""
|
||||||
|
data = request.get_json(silent=True) or {}
|
||||||
|
try:
|
||||||
|
session['counting_line'] = {
|
||||||
|
'x1': int(data['x1']),
|
||||||
|
'y1': int(data['y1']),
|
||||||
|
'x2': int(data['x2']),
|
||||||
|
'y2': int(data['y2'])
|
||||||
|
}
|
||||||
|
except (KeyError, ValueError, TypeError):
|
||||||
|
abort(400, description='Ungültige Linienkoordinaten')
|
||||||
|
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"""
|
||||||
|
return jsonify(get_line_from_session())
|
||||||
|
|
||||||
|
@app.route('/api/reset_count', methods=['POST'])
|
||||||
|
def reset_count():
|
||||||
|
"""API endpoint to reset the vehicle count for a stream"""
|
||||||
|
data = request.get_json(silent=True) or {}
|
||||||
|
stream_id = data.get('stream_id')
|
||||||
|
if not stream_id:
|
||||||
|
abort(400, description='stream_id ist erforderlich')
|
||||||
|
valid_ids = {session.get('webcam_stream_id')}
|
||||||
|
valid_ids.update(session.get('video_stream_ids', {}).values())
|
||||||
|
valid_ids.discard(None)
|
||||||
|
if stream_id not in valid_ids:
|
||||||
|
abort(403, description='Stream gehört nicht zur aktuellen Sitzung')
|
||||||
|
event = get_reset_event(stream_id)
|
||||||
|
event.set()
|
||||||
|
return jsonify({'status': 'success', 'message': 'Zähler wird zurückgesetzt'})
|
||||||
|
|
||||||
|
def detect_objects_from_webcam(line_data, stream_id):
|
||||||
|
cap = cv2.VideoCapture("http://CAMERA-IP:81/stream")
|
||||||
|
if not cap.isOpened():
|
||||||
|
cap.release()
|
||||||
|
raise RuntimeError('Webcam konnte nicht geöffnet werden')
|
||||||
|
return generate_frames(cap, line_data, stream_id)
|
||||||
|
|
||||||
|
@app.route('/webcam_feed')
|
||||||
|
def webcam_feed():
|
||||||
|
line_data = get_line_from_session()
|
||||||
|
stream_id = get_webcam_stream_id()
|
||||||
|
try:
|
||||||
|
generator = detect_objects_from_webcam(line_data, stream_id)
|
||||||
|
except RuntimeError as exc:
|
||||||
|
abort(503, description=str(exc))
|
||||||
|
return Response(generator,
|
||||||
|
mimetype='multipart/x-mixed-replace; boundary=frame')
|
||||||
|
|
||||||
|
@app.route('/upload', methods=['POST'])
|
||||||
|
def upload_video():
|
||||||
|
if 'file' not in request.files:
|
||||||
|
abort(400, description='Keine Datei erhalten')
|
||||||
|
|
||||||
|
file = request.files['file']
|
||||||
|
if not file or file.filename == '':
|
||||||
|
abort(400, description='Keine Datei ausgewählt')
|
||||||
|
|
||||||
|
filename = secure_filename(file.filename)
|
||||||
|
if not filename:
|
||||||
|
abort(400, description='Ungültiger Dateiname')
|
||||||
|
if not allowed_file(filename):
|
||||||
|
abort(400, description='Ungültiger Dateityp')
|
||||||
|
|
||||||
|
ensure_upload_dir()
|
||||||
|
name, ext = os.path.splitext(filename)
|
||||||
|
stored_filename = f"{name}_{uuid.uuid4().hex}{ext.lower()}"
|
||||||
|
file_path = os.path.join(UPLOAD_DIR, stored_filename)
|
||||||
|
file.save(file_path)
|
||||||
|
|
||||||
|
return redirect(url_for('play_video', filename=stored_filename))
|
||||||
|
|
||||||
|
@app.route('/uploads/<filename>')
|
||||||
|
def play_video(filename):
|
||||||
|
safe_filename = os.path.basename(filename)
|
||||||
|
if safe_filename != filename:
|
||||||
|
abort(400, description='Ungültiger Dateiname')
|
||||||
|
file_path = os.path.join(UPLOAD_DIR, safe_filename)
|
||||||
|
if not os.path.isfile(file_path):
|
||||||
|
abort(404)
|
||||||
|
get_line_from_session()
|
||||||
|
stream_id = get_video_stream_id(safe_filename)
|
||||||
|
return render_template('play_video.html', filename=safe_filename, stream_id=stream_id)
|
||||||
|
|
||||||
|
@app.route('/video/<path:filename>')
|
||||||
|
def send_video(filename):
|
||||||
|
return send_from_directory(UPLOAD_DIR, filename)
|
||||||
|
|
||||||
|
def detect_objects_from_video(video_path, line_data, stream_id):
|
||||||
|
cap = cv2.VideoCapture(video_path)
|
||||||
|
if not cap.isOpened():
|
||||||
|
cap.release()
|
||||||
|
raise RuntimeError('Video konnte nicht geöffnet werden')
|
||||||
|
return generate_frames(cap, line_data, stream_id)
|
||||||
|
|
||||||
|
@app.route('/video_feed/<filename>')
|
||||||
|
def video_feed(filename):
|
||||||
|
safe_filename = os.path.basename(filename)
|
||||||
|
if safe_filename != filename:
|
||||||
|
abort(400, description='Ungültiger Dateiname')
|
||||||
|
video_path = os.path.join(UPLOAD_DIR, safe_filename)
|
||||||
|
if not os.path.isfile(video_path):
|
||||||
|
abort(404)
|
||||||
|
line_data = get_line_from_session()
|
||||||
|
stream_id = get_video_stream_id(safe_filename)
|
||||||
|
try:
|
||||||
|
generator = detect_objects_from_video(video_path, line_data, stream_id)
|
||||||
|
except RuntimeError as exc:
|
||||||
|
abort(503, description=str(exc))
|
||||||
|
return Response(generator,
|
||||||
|
mimetype='multipart/x-mixed-replace; boundary=frame')
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run('0.0.0.0',debug=False, port=8080)
|
||||||
565
app3.py
Normal file
565
app3.py
Normal file
@@ -0,0 +1,565 @@
|
|||||||
|
import os
|
||||||
|
import uuid
|
||||||
|
import time
|
||||||
|
import threading
|
||||||
|
from threading import Event, Lock, Condition
|
||||||
|
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
from flask import (
|
||||||
|
Flask,
|
||||||
|
render_template,
|
||||||
|
Response,
|
||||||
|
request,
|
||||||
|
redirect,
|
||||||
|
url_for,
|
||||||
|
send_from_directory,
|
||||||
|
session,
|
||||||
|
jsonify,
|
||||||
|
abort,
|
||||||
|
)
|
||||||
|
from werkzeug.utils import secure_filename
|
||||||
|
from ultralytics import YOLO
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Konfiguration
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Kamera-URL ueber Env ueberschreibbar (ESP32-CAM Default: Port 81, /stream).
|
||||||
|
CAMERA_URL = os.environ.get("CAMERA_URL", "http://CAMERA-IP:81/stream")
|
||||||
|
|
||||||
|
# FFmpeg-Optionen fuer den Netzwerk-Stream: Socket-Timeout + Auto-Reconnect.
|
||||||
|
# Muss VOR dem ersten VideoCapture gesetzt sein.
|
||||||
|
os.environ.setdefault(
|
||||||
|
"OPENCV_FFMPEG_CAPTURE_OPTIONS",
|
||||||
|
"timeout;5000000|reconnect;1|reconnect_streamed;1|reconnect_delay_max;2",
|
||||||
|
)
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.secret_key = os.environ.get("SECRET_KEY", "vehicle_dev_secret")
|
||||||
|
app.config["MAX_CONTENT_LENGTH"] = 200 * 1024 * 1024 # 200MB Upload-Limit
|
||||||
|
|
||||||
|
# Globales Modell fuer den Video-Upload-Pfad (per Request).
|
||||||
|
model = YOLO("yolo11s.pt")
|
||||||
|
names = model.model.names
|
||||||
|
|
||||||
|
VEHICLE_CLASSES = {"car", "truck", "bus", "motorcycle"}
|
||||||
|
ALLOWED_EXTENSIONS = {"mp4", "mov", "avi", "mkv"}
|
||||||
|
UPLOAD_DIR = "uploads"
|
||||||
|
|
||||||
|
DEFAULT_LINE = {"x1": 0, "y1": 300, "x2": 1020, "y2": 300}
|
||||||
|
FRAME_SIZE = (1020, 600)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Hilfsfunktionen
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
def allowed_file(filename: str) -> bool:
|
||||||
|
return "." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_upload_dir() -> None:
|
||||||
|
if not os.path.exists(UPLOAD_DIR):
|
||||||
|
os.makedirs(UPLOAD_DIR)
|
||||||
|
|
||||||
|
|
||||||
|
def get_line_from_session():
|
||||||
|
if "counting_line" not in session:
|
||||||
|
session["counting_line"] = dict(DEFAULT_LINE)
|
||||||
|
return session["counting_line"]
|
||||||
|
|
||||||
|
|
||||||
|
def fresh_vehicle_counts() -> dict[str, int]:
|
||||||
|
return {vehicle: 0 for vehicle in VEHICLE_CLASSES}
|
||||||
|
|
||||||
|
|
||||||
|
def new_state() -> dict:
|
||||||
|
"""Frischer Zaehl-/Tracking-Zustand fuer einen Stream."""
|
||||||
|
return {
|
||||||
|
"track_positions": {},
|
||||||
|
"counted_ids": set(),
|
||||||
|
"count": 0,
|
||||||
|
"types": fresh_vehicle_counts(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def reset_state(state: dict) -> None:
|
||||||
|
state["track_positions"].clear()
|
||||||
|
state["counted_ids"].clear()
|
||||||
|
state["count"] = 0
|
||||||
|
state["types"] = fresh_vehicle_counts()
|
||||||
|
|
||||||
|
|
||||||
|
def line_intersect(p1, p2, p3, p4) -> bool:
|
||||||
|
"""True, wenn sich die Strecken p1-p2 und p3-p4 schneiden."""
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
def crossed_line(prev_pos, curr_pos, line_start, line_end) -> bool:
|
||||||
|
return line_intersect(prev_pos, curr_pos, line_start, line_end)
|
||||||
|
|
||||||
|
|
||||||
|
def process_frame(frame, det_model, det_names, line_start, line_end, state):
|
||||||
|
"""
|
||||||
|
Skaliert den Frame, fuehrt YOLO-Tracking aus, zaehlt Linienueberquerungen
|
||||||
|
und zeichnet alle Overlays. Mutiert `state` in-place und gibt den
|
||||||
|
annotierten Frame zurueck. Wird von Webcam-Grabber UND Video-Pfad genutzt.
|
||||||
|
"""
|
||||||
|
frame = cv2.resize(frame, FRAME_SIZE)
|
||||||
|
results = det_model.track(frame, persist=True)
|
||||||
|
|
||||||
|
track_positions = state["track_positions"]
|
||||||
|
counted_ids = state["counted_ids"]
|
||||||
|
types = state["types"]
|
||||||
|
|
||||||
|
if results and 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):
|
||||||
|
label_name = det_names[class_id]
|
||||||
|
x1, y1, x2, y2 = box
|
||||||
|
center_x = (x1 + x2) // 2
|
||||||
|
center_y = (y1 + y2) // 2
|
||||||
|
|
||||||
|
if label_name in VEHICLE_CLASSES:
|
||||||
|
if track_id in track_positions and track_id not in counted_ids:
|
||||||
|
prev_x, prev_y = track_positions[track_id]
|
||||||
|
cv2.line(frame, (prev_x, prev_y), (center_x, center_y), (255, 100, 0), 2)
|
||||||
|
|
||||||
|
if crossed_line((prev_x, prev_y), (center_x, center_y), line_start, line_end):
|
||||||
|
counted_ids.add(track_id)
|
||||||
|
state["count"] += 1
|
||||||
|
types[label_name] += 1
|
||||||
|
cv2.circle(frame, (center_x, center_y), 25, (0, 255, 0), 5)
|
||||||
|
|
||||||
|
track_positions[track_id] = (center_x, center_y)
|
||||||
|
|
||||||
|
box_color = (0, 255, 0) if label_name in VEHICLE_CLASSES else (255, 0, 0)
|
||||||
|
cv2.rectangle(frame, (x1, y1), (x2, y2), box_color, 2)
|
||||||
|
|
||||||
|
label = f"{track_id} - {label_name}"
|
||||||
|
if track_id in counted_ids:
|
||||||
|
label += " \u2713"
|
||||||
|
cv2.putText(frame, label, (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 0, 255), 1)
|
||||||
|
cv2.circle(frame, (center_x, center_y), 3, (0, 255, 255), -1)
|
||||||
|
|
||||||
|
# Zaehllinie (gestrichelt)
|
||||||
|
cv2.line(frame, line_start, line_end, (0, 255, 255), 3, cv2.LINE_AA)
|
||||||
|
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, max(line_length, 1), dash_length * 2):
|
||||||
|
t1 = i / line_length if line_length else 0
|
||||||
|
t2 = min((i + dash_length) / line_length, 1.0) if line_length else 0
|
||||||
|
x1_dash = int(line_start[0] + t1 * (line_end[0] - line_start[0]))
|
||||||
|
y1_dash = int(line_start[1] + t1 * (line_end[1] - line_start[1]))
|
||||||
|
x2_dash = int(line_start[0] + t2 * (line_end[0] - line_start[0]))
|
||||||
|
y2_dash = int(line_start[1] + t2 * (line_end[1] - line_start[1]))
|
||||||
|
cv2.line(frame, (x1_dash, y1_dash), (x2_dash, y2_dash), (0, 0, 0), 3)
|
||||||
|
|
||||||
|
# Zaehler-Box
|
||||||
|
cv2.rectangle(frame, (10, 10), (350, 140), (0, 0, 0), -1)
|
||||||
|
cv2.putText(frame, f"Gesamt: {state['count']}", (20, 35), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2)
|
||||||
|
cv2.putText(frame, f"Autos: {types.get('car', 0)}", (20, 65), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (200, 200, 200), 1)
|
||||||
|
cv2.putText(frame, f"LKW: {types.get('truck', 0)}", (20, 90), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (200, 200, 200), 1)
|
||||||
|
cv2.putText(frame, f"Busse: {types.get('bus', 0)}", (20, 115), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (200, 200, 200), 1)
|
||||||
|
cv2.putText(frame, f"Motorraeder: {types.get('motorcycle', 0)}", (20, 135), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (200, 200, 200), 1)
|
||||||
|
|
||||||
|
return frame
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Webcam-Grabber: EINE Verbindung zur (ESP32-)Cam, Fan-out an viele Viewer
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
class WebcamGrabber:
|
||||||
|
"""
|
||||||
|
Haelt genau eine Verbindung zur Netzwerk-Kamera. Ein Hintergrund-Thread
|
||||||
|
liest Frames, laeuft YOLO drauf und legt das jeweils neueste annotierte
|
||||||
|
JPEG in einen gemeinsamen Slot. Jeder /webcam_feed-Request konsumiert nur
|
||||||
|
diesen Slot -> beliebig viele Zuschauer, aber nur EINE Cam-Verbindung.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, url: str):
|
||||||
|
self.url = url
|
||||||
|
self.lock = Lock()
|
||||||
|
self.frame_cond = Condition()
|
||||||
|
|
||||||
|
self.latest_jpeg: bytes | None = None
|
||||||
|
self.frame_seq = 0
|
||||||
|
self.viewers = 0
|
||||||
|
self.reset_flag = Event()
|
||||||
|
self.line = dict(DEFAULT_LINE)
|
||||||
|
|
||||||
|
# Eigenes Modell -> isolierter Tracker, getrennt vom Video-Pfad.
|
||||||
|
# Lazy: wird erst beim ersten aktiven Stream geladen.
|
||||||
|
self.model = None
|
||||||
|
self.names = None
|
||||||
|
|
||||||
|
self.thread = None
|
||||||
|
|
||||||
|
# -- Steuerung -----------------------------------------------------------
|
||||||
|
def start(self):
|
||||||
|
if self.thread is None:
|
||||||
|
self.thread = threading.Thread(target=self._run, daemon=True)
|
||||||
|
self.thread.start()
|
||||||
|
|
||||||
|
def set_line(self, line: dict):
|
||||||
|
with self.lock:
|
||||||
|
self.line = dict(line)
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
self.reset_flag.set()
|
||||||
|
|
||||||
|
def add_viewer(self):
|
||||||
|
with self.lock:
|
||||||
|
self.viewers += 1
|
||||||
|
|
||||||
|
def remove_viewer(self):
|
||||||
|
with self.lock:
|
||||||
|
self.viewers = max(0, self.viewers - 1)
|
||||||
|
|
||||||
|
def _viewer_count(self) -> int:
|
||||||
|
with self.lock:
|
||||||
|
return self.viewers
|
||||||
|
|
||||||
|
def _current_line(self):
|
||||||
|
with self.lock:
|
||||||
|
line = dict(self.line)
|
||||||
|
return (line["x1"], line["y1"]), (line["x2"], line["y2"])
|
||||||
|
|
||||||
|
def _open(self):
|
||||||
|
cap = cv2.VideoCapture(self.url, cv2.CAP_FFMPEG)
|
||||||
|
# Timeouts sind versions-/backend-abhaengig -> defensiv setzen.
|
||||||
|
try:
|
||||||
|
cap.set(cv2.CAP_PROP_OPEN_TIMEOUT_MSEC, 5000)
|
||||||
|
cap.set(cv2.CAP_PROP_READ_TIMEOUT_MSEC, 5000)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return cap
|
||||||
|
|
||||||
|
def _ensure_model(self):
|
||||||
|
if self.model is None:
|
||||||
|
self.model = YOLO("yolo11s.pt")
|
||||||
|
self.names = self.model.model.names
|
||||||
|
|
||||||
|
def _publish(self, jpeg: bytes):
|
||||||
|
with self.frame_cond:
|
||||||
|
self.latest_jpeg = jpeg
|
||||||
|
self.frame_seq += 1
|
||||||
|
self.frame_cond.notify_all()
|
||||||
|
|
||||||
|
def _clear(self):
|
||||||
|
with self.frame_cond:
|
||||||
|
self.latest_jpeg = None
|
||||||
|
self.frame_seq += 1
|
||||||
|
self.frame_cond.notify_all()
|
||||||
|
|
||||||
|
# -- Hintergrund-Thread (laeuft die ganze Prozess-Lebensdauer) ----------
|
||||||
|
def _run(self):
|
||||||
|
cap = None
|
||||||
|
state = new_state()
|
||||||
|
frame_idx = 0
|
||||||
|
|
||||||
|
while True:
|
||||||
|
# Niemand schaut zu -> Cam-Verbindung freigeben, idlen.
|
||||||
|
if self._viewer_count() == 0:
|
||||||
|
if cap is not None:
|
||||||
|
cap.release()
|
||||||
|
cap = None
|
||||||
|
self._clear()
|
||||||
|
time.sleep(0.3)
|
||||||
|
continue
|
||||||
|
|
||||||
|
self._ensure_model()
|
||||||
|
|
||||||
|
# (Re-)Connect mit Backoff.
|
||||||
|
if cap is None or not cap.isOpened():
|
||||||
|
if cap is not None:
|
||||||
|
cap.release()
|
||||||
|
cap = self._open()
|
||||||
|
if not cap.isOpened():
|
||||||
|
cap = None
|
||||||
|
time.sleep(2.0)
|
||||||
|
continue
|
||||||
|
# Frische Verbindung -> Track-IDs starten neu, Gesamtzaehler bleibt.
|
||||||
|
state["track_positions"].clear()
|
||||||
|
state["counted_ids"].clear()
|
||||||
|
|
||||||
|
ret, frame = cap.read()
|
||||||
|
if not ret:
|
||||||
|
cap.release()
|
||||||
|
cap = None
|
||||||
|
time.sleep(1.0)
|
||||||
|
continue
|
||||||
|
|
||||||
|
frame_idx += 1
|
||||||
|
if frame_idx % 2 != 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if self.reset_flag.is_set():
|
||||||
|
reset_state(state)
|
||||||
|
self.reset_flag.clear()
|
||||||
|
|
||||||
|
line_start, line_end = self._current_line()
|
||||||
|
frame = process_frame(frame, self.model, self.names, line_start, line_end, state)
|
||||||
|
|
||||||
|
ok, buffer = cv2.imencode(".jpg", frame)
|
||||||
|
if not ok:
|
||||||
|
continue
|
||||||
|
self._publish(buffer.tobytes())
|
||||||
|
|
||||||
|
# -- Pro-Viewer-Generator ------------------------------------------------
|
||||||
|
def frames(self):
|
||||||
|
self.add_viewer()
|
||||||
|
last_seq = -1
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
with self.frame_cond:
|
||||||
|
got = self.frame_cond.wait_for(
|
||||||
|
lambda: self.frame_seq != last_seq, timeout=15
|
||||||
|
)
|
||||||
|
if not got:
|
||||||
|
# 15s kein neuer Frame -> Cam vermutlich tot, Viewer beenden.
|
||||||
|
break
|
||||||
|
jpeg = self.latest_jpeg
|
||||||
|
last_seq = self.frame_seq
|
||||||
|
if jpeg is None:
|
||||||
|
# Grabber hat Verbindung freigegeben.
|
||||||
|
break
|
||||||
|
yield (
|
||||||
|
b"--frame\r\n"
|
||||||
|
b"Content-Type: image/jpeg\r\n\r\n" + jpeg + b"\r\n"
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
self.remove_viewer()
|
||||||
|
|
||||||
|
|
||||||
|
webcam = WebcamGrabber(CAMERA_URL)
|
||||||
|
webcam.start()
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Reset-Events fuer den Video-Pfad (per Stream-ID)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
reset_events: dict[str, Event] = {}
|
||||||
|
reset_lock = Lock()
|
||||||
|
|
||||||
|
|
||||||
|
def get_reset_event(stream_id: str) -> Event:
|
||||||
|
with reset_lock:
|
||||||
|
event = reset_events.get(stream_id)
|
||||||
|
if event is None:
|
||||||
|
event = Event()
|
||||||
|
reset_events[stream_id] = event
|
||||||
|
return event
|
||||||
|
|
||||||
|
|
||||||
|
def release_reset_event(stream_id: str) -> None:
|
||||||
|
with reset_lock:
|
||||||
|
reset_events.pop(stream_id, None)
|
||||||
|
|
||||||
|
|
||||||
|
def get_webcam_stream_id() -> str:
|
||||||
|
stream_id = session.get("webcam_stream_id")
|
||||||
|
if not stream_id:
|
||||||
|
stream_id = f"webcam-{uuid.uuid4().hex}"
|
||||||
|
session["webcam_stream_id"] = stream_id
|
||||||
|
return stream_id
|
||||||
|
|
||||||
|
|
||||||
|
def get_video_stream_id(filename: str) -> str:
|
||||||
|
video_streams = session.get("video_stream_ids", {})
|
||||||
|
stream_id = video_streams.get(filename)
|
||||||
|
if not stream_id:
|
||||||
|
stream_id = f"video-{uuid.uuid4().hex}"
|
||||||
|
video_streams[filename] = stream_id
|
||||||
|
session["video_stream_ids"] = video_streams
|
||||||
|
return stream_id
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Video-Pfad (per Request, unveraendert in der Logik)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
def generate_frames(capture, line_data, stream_id: str):
|
||||||
|
state = new_state()
|
||||||
|
line_start = (line_data["x1"], line_data["y1"])
|
||||||
|
line_end = (line_data["x2"], line_data["y2"])
|
||||||
|
frame_idx = 0
|
||||||
|
reset_event = get_reset_event(stream_id)
|
||||||
|
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
ret, frame = capture.read()
|
||||||
|
if not ret:
|
||||||
|
break
|
||||||
|
|
||||||
|
frame_idx += 1
|
||||||
|
if frame_idx % 2 != 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if reset_event.is_set():
|
||||||
|
reset_state(state)
|
||||||
|
reset_event.clear()
|
||||||
|
|
||||||
|
frame = process_frame(frame, model, names, line_start, line_end, state)
|
||||||
|
|
||||||
|
ok, buffer = cv2.imencode(".jpg", frame)
|
||||||
|
if not ok:
|
||||||
|
continue
|
||||||
|
yield (
|
||||||
|
b"--frame\r\n"
|
||||||
|
b"Content-Type: image/jpeg\r\n\r\n" + buffer.tobytes() + b"\r\n"
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
capture.release()
|
||||||
|
release_reset_event(stream_id)
|
||||||
|
|
||||||
|
|
||||||
|
def detect_objects_from_video(video_path, line_data, stream_id):
|
||||||
|
cap = cv2.VideoCapture(video_path)
|
||||||
|
if not cap.isOpened():
|
||||||
|
cap.release()
|
||||||
|
raise RuntimeError("Video konnte nicht geoeffnet werden")
|
||||||
|
return generate_frames(cap, line_data, stream_id)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Routen
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
@app.route("/")
|
||||||
|
def index():
|
||||||
|
return render_template("index.html")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/start_webcam")
|
||||||
|
def start_webcam():
|
||||||
|
get_line_from_session()
|
||||||
|
stream_id = get_webcam_stream_id()
|
||||||
|
return render_template("webcam.html", stream_id=stream_id)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/webcam_feed")
|
||||||
|
def webcam_feed():
|
||||||
|
# Keine eigene Cam-Verbindung mehr pro Request -> Fan-out vom Grabber.
|
||||||
|
return Response(
|
||||||
|
webcam.frames(),
|
||||||
|
mimetype="multipart/x-mixed-replace; boundary=frame",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/set_line", methods=["POST"])
|
||||||
|
def set_counting_line():
|
||||||
|
"""Setzt die Zaehllinie (gilt fuer Video-Session UND Webcam-Grabber)."""
|
||||||
|
data = request.get_json(silent=True) or {}
|
||||||
|
try:
|
||||||
|
line = {
|
||||||
|
"x1": int(data["x1"]),
|
||||||
|
"y1": int(data["y1"]),
|
||||||
|
"x2": int(data["x2"]),
|
||||||
|
"y2": int(data["y2"]),
|
||||||
|
}
|
||||||
|
except (KeyError, ValueError, TypeError):
|
||||||
|
abort(400, description="Ungueltige Linienkoordinaten")
|
||||||
|
|
||||||
|
session["counting_line"] = line
|
||||||
|
webcam.set_line(line) # Webcam nutzt eine globale Linie (eine Kamera)
|
||||||
|
return jsonify({"status": "success", "line": line})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/get_line", methods=["GET"])
|
||||||
|
def get_counting_line():
|
||||||
|
return jsonify(get_line_from_session())
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/reset_count", methods=["POST"])
|
||||||
|
def reset_count():
|
||||||
|
data = request.get_json(silent=True) or {}
|
||||||
|
stream_id = data.get("stream_id")
|
||||||
|
if not stream_id:
|
||||||
|
abort(400, description="stream_id ist erforderlich")
|
||||||
|
|
||||||
|
valid_ids = {session.get("webcam_stream_id")}
|
||||||
|
valid_ids.update(session.get("video_stream_ids", {}).values())
|
||||||
|
valid_ids.discard(None)
|
||||||
|
if stream_id not in valid_ids:
|
||||||
|
abort(403, description="Stream gehoert nicht zur aktuellen Sitzung")
|
||||||
|
|
||||||
|
if stream_id == session.get("webcam_stream_id"):
|
||||||
|
webcam.reset()
|
||||||
|
else:
|
||||||
|
get_reset_event(stream_id).set()
|
||||||
|
return jsonify({"status": "success", "message": "Zaehler wird zurueckgesetzt"})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/upload", methods=["POST"])
|
||||||
|
def upload_video():
|
||||||
|
if "file" not in request.files:
|
||||||
|
abort(400, description="Keine Datei erhalten")
|
||||||
|
|
||||||
|
file = request.files["file"]
|
||||||
|
if not file or file.filename == "":
|
||||||
|
abort(400, description="Keine Datei ausgewaehlt")
|
||||||
|
|
||||||
|
filename = secure_filename(file.filename)
|
||||||
|
if not filename:
|
||||||
|
abort(400, description="Ungueltiger Dateiname")
|
||||||
|
if not allowed_file(filename):
|
||||||
|
abort(400, description="Ungueltiger Dateityp")
|
||||||
|
|
||||||
|
ensure_upload_dir()
|
||||||
|
name, ext = os.path.splitext(filename)
|
||||||
|
stored_filename = f"{name}_{uuid.uuid4().hex}{ext.lower()}"
|
||||||
|
file_path = os.path.join(UPLOAD_DIR, stored_filename)
|
||||||
|
file.save(file_path)
|
||||||
|
|
||||||
|
return redirect(url_for("play_video", filename=stored_filename))
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/uploads/<filename>")
|
||||||
|
def play_video(filename):
|
||||||
|
safe_filename = os.path.basename(filename)
|
||||||
|
if safe_filename != filename:
|
||||||
|
abort(400, description="Ungueltiger Dateiname")
|
||||||
|
file_path = os.path.join(UPLOAD_DIR, safe_filename)
|
||||||
|
if not os.path.isfile(file_path):
|
||||||
|
abort(404)
|
||||||
|
get_line_from_session()
|
||||||
|
stream_id = get_video_stream_id(safe_filename)
|
||||||
|
return render_template("play_video.html", filename=safe_filename, stream_id=stream_id)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/video/<path:filename>")
|
||||||
|
def send_video(filename):
|
||||||
|
return send_from_directory(UPLOAD_DIR, filename)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/video_feed/<filename>")
|
||||||
|
def video_feed(filename):
|
||||||
|
safe_filename = os.path.basename(filename)
|
||||||
|
if safe_filename != filename:
|
||||||
|
abort(400, description="Ungueltiger Dateiname")
|
||||||
|
video_path = os.path.join(UPLOAD_DIR, safe_filename)
|
||||||
|
if not os.path.isfile(video_path):
|
||||||
|
abort(404)
|
||||||
|
line_data = get_line_from_session()
|
||||||
|
stream_id = get_video_stream_id(safe_filename)
|
||||||
|
try:
|
||||||
|
generator = detect_objects_from_video(video_path, line_data, stream_id)
|
||||||
|
except RuntimeError as exc:
|
||||||
|
abort(503, description=str(exc))
|
||||||
|
return Response(generator, mimetype="multipart/x-mixed-replace; boundary=frame")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app.run("0.0.0.0", debug=False, port=8080)
|
||||||
584
app4.py
Normal file
584
app4.py
Normal file
@@ -0,0 +1,584 @@
|
|||||||
|
import os
|
||||||
|
import uuid
|
||||||
|
import time
|
||||||
|
import threading
|
||||||
|
from threading import Event, Lock, Condition
|
||||||
|
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
import requests
|
||||||
|
from flask import (
|
||||||
|
Flask,
|
||||||
|
render_template,
|
||||||
|
Response,
|
||||||
|
request,
|
||||||
|
redirect,
|
||||||
|
url_for,
|
||||||
|
send_from_directory,
|
||||||
|
session,
|
||||||
|
jsonify,
|
||||||
|
abort,
|
||||||
|
)
|
||||||
|
from werkzeug.utils import secure_filename
|
||||||
|
from ultralytics import YOLO
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Konfiguration
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Kamera-URL ueber Env ueberschreibbar (ESP32-CAM Default: Port 81, /stream).
|
||||||
|
CAMERA_URL = os.environ.get("CAMERA_URL", "http://CAMERA-IP:81/stream")
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.secret_key = os.environ.get("SECRET_KEY", "vehicle_dev_secret")
|
||||||
|
app.config["MAX_CONTENT_LENGTH"] = 200 * 1024 * 1024 # 200MB Upload-Limit
|
||||||
|
|
||||||
|
# Globales Modell fuer den Video-Upload-Pfad (per Request).
|
||||||
|
model = YOLO("yolo11s.pt")
|
||||||
|
names = model.model.names
|
||||||
|
|
||||||
|
VEHICLE_CLASSES = {"car", "truck", "bus", "motorcycle"}
|
||||||
|
ALLOWED_EXTENSIONS = {"mp4", "mov", "avi", "mkv"}
|
||||||
|
UPLOAD_DIR = "uploads"
|
||||||
|
|
||||||
|
DEFAULT_LINE = {"x1": 0, "y1": 300, "x2": 1020, "y2": 300}
|
||||||
|
FRAME_SIZE = (1020, 600)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Hilfsfunktionen
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
def allowed_file(filename: str) -> bool:
|
||||||
|
return "." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_upload_dir() -> None:
|
||||||
|
if not os.path.exists(UPLOAD_DIR):
|
||||||
|
os.makedirs(UPLOAD_DIR)
|
||||||
|
|
||||||
|
|
||||||
|
def get_line_from_session():
|
||||||
|
if "counting_line" not in session:
|
||||||
|
session["counting_line"] = dict(DEFAULT_LINE)
|
||||||
|
return session["counting_line"]
|
||||||
|
|
||||||
|
|
||||||
|
def fresh_vehicle_counts() -> dict[str, int]:
|
||||||
|
return {vehicle: 0 for vehicle in VEHICLE_CLASSES}
|
||||||
|
|
||||||
|
|
||||||
|
def new_state() -> dict:
|
||||||
|
"""Frischer Zaehl-/Tracking-Zustand fuer einen Stream."""
|
||||||
|
return {
|
||||||
|
"track_positions": {},
|
||||||
|
"counted_ids": set(),
|
||||||
|
"count": 0,
|
||||||
|
"types": fresh_vehicle_counts(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def reset_state(state: dict) -> None:
|
||||||
|
state["track_positions"].clear()
|
||||||
|
state["counted_ids"].clear()
|
||||||
|
state["count"] = 0
|
||||||
|
state["types"] = fresh_vehicle_counts()
|
||||||
|
|
||||||
|
|
||||||
|
def line_intersect(p1, p2, p3, p4) -> bool:
|
||||||
|
"""True, wenn sich die Strecken p1-p2 und p3-p4 schneiden."""
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
def crossed_line(prev_pos, curr_pos, line_start, line_end) -> bool:
|
||||||
|
return line_intersect(prev_pos, curr_pos, line_start, line_end)
|
||||||
|
|
||||||
|
|
||||||
|
def process_frame(frame, det_model, det_names, line_start, line_end, state):
|
||||||
|
"""
|
||||||
|
Skaliert den Frame, fuehrt YOLO-Tracking aus, zaehlt Linienueberquerungen
|
||||||
|
und zeichnet alle Overlays. Mutiert `state` in-place und gibt den
|
||||||
|
annotierten Frame zurueck. Wird von Webcam-Grabber UND Video-Pfad genutzt.
|
||||||
|
"""
|
||||||
|
frame = cv2.resize(frame, FRAME_SIZE)
|
||||||
|
results = det_model.track(frame, persist=True)
|
||||||
|
|
||||||
|
track_positions = state["track_positions"]
|
||||||
|
counted_ids = state["counted_ids"]
|
||||||
|
types = state["types"]
|
||||||
|
|
||||||
|
if results and 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):
|
||||||
|
label_name = det_names[class_id]
|
||||||
|
x1, y1, x2, y2 = box
|
||||||
|
center_x = (x1 + x2) // 2
|
||||||
|
center_y = (y1 + y2) // 2
|
||||||
|
|
||||||
|
if label_name in VEHICLE_CLASSES:
|
||||||
|
if track_id in track_positions and track_id not in counted_ids:
|
||||||
|
prev_x, prev_y = track_positions[track_id]
|
||||||
|
cv2.line(frame, (prev_x, prev_y), (center_x, center_y), (255, 100, 0), 2)
|
||||||
|
|
||||||
|
if crossed_line((prev_x, prev_y), (center_x, center_y), line_start, line_end):
|
||||||
|
counted_ids.add(track_id)
|
||||||
|
state["count"] += 1
|
||||||
|
types[label_name] += 1
|
||||||
|
cv2.circle(frame, (center_x, center_y), 25, (0, 255, 0), 5)
|
||||||
|
|
||||||
|
track_positions[track_id] = (center_x, center_y)
|
||||||
|
|
||||||
|
box_color = (0, 255, 0) if label_name in VEHICLE_CLASSES else (255, 0, 0)
|
||||||
|
cv2.rectangle(frame, (x1, y1), (x2, y2), box_color, 2)
|
||||||
|
|
||||||
|
label = f"{track_id} - {label_name}"
|
||||||
|
if track_id in counted_ids:
|
||||||
|
label += " \u2713"
|
||||||
|
cv2.putText(frame, label, (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 0, 255), 1)
|
||||||
|
cv2.circle(frame, (center_x, center_y), 3, (0, 255, 255), -1)
|
||||||
|
|
||||||
|
# Zaehllinie (gestrichelt)
|
||||||
|
cv2.line(frame, line_start, line_end, (0, 255, 255), 3, cv2.LINE_AA)
|
||||||
|
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, max(line_length, 1), dash_length * 2):
|
||||||
|
t1 = i / line_length if line_length else 0
|
||||||
|
t2 = min((i + dash_length) / line_length, 1.0) if line_length else 0
|
||||||
|
x1_dash = int(line_start[0] + t1 * (line_end[0] - line_start[0]))
|
||||||
|
y1_dash = int(line_start[1] + t1 * (line_end[1] - line_start[1]))
|
||||||
|
x2_dash = int(line_start[0] + t2 * (line_end[0] - line_start[0]))
|
||||||
|
y2_dash = int(line_start[1] + t2 * (line_end[1] - line_start[1]))
|
||||||
|
cv2.line(frame, (x1_dash, y1_dash), (x2_dash, y2_dash), (0, 0, 0), 3)
|
||||||
|
|
||||||
|
# Zaehler-Box
|
||||||
|
cv2.rectangle(frame, (10, 10), (350, 140), (0, 0, 0), -1)
|
||||||
|
cv2.putText(frame, f"Gesamt: {state['count']}", (20, 35), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2)
|
||||||
|
cv2.putText(frame, f"Autos: {types.get('car', 0)}", (20, 65), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (200, 200, 200), 1)
|
||||||
|
cv2.putText(frame, f"LKW: {types.get('truck', 0)}", (20, 90), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (200, 200, 200), 1)
|
||||||
|
cv2.putText(frame, f"Busse: {types.get('bus', 0)}", (20, 115), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (200, 200, 200), 1)
|
||||||
|
cv2.putText(frame, f"Motorraeder: {types.get('motorcycle', 0)}", (20, 135), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (200, 200, 200), 1)
|
||||||
|
|
||||||
|
return frame
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Webcam-Grabber: EINE Verbindung zur (ESP32-)Cam, Fan-out an viele Viewer
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
class WebcamGrabber:
|
||||||
|
"""
|
||||||
|
Haelt genau eine Verbindung zur Netzwerk-Kamera. Ein Hintergrund-Thread
|
||||||
|
liest Frames, laeuft YOLO drauf und legt das jeweils neueste annotierte
|
||||||
|
JPEG in einen gemeinsamen Slot. Jeder /webcam_feed-Request konsumiert nur
|
||||||
|
diesen Slot -> beliebig viele Zuschauer, aber nur EINE Cam-Verbindung.
|
||||||
|
|
||||||
|
Liest den MJPEG-Stream MANUELL ueber requests (SOI/EOI-Parsing) statt ueber
|
||||||
|
cv2.VideoCapture -> umgeht den FFmpeg-Demuxer, der bei ESP32-CAM scheitert.
|
||||||
|
"""
|
||||||
|
|
||||||
|
MAX_BUFFER = 4 * 1024 * 1024 # Schutz gegen unbegrenztes Puffer-Wachstum
|
||||||
|
|
||||||
|
def __init__(self, url: str):
|
||||||
|
self.url = url
|
||||||
|
self.lock = Lock()
|
||||||
|
self.frame_cond = Condition()
|
||||||
|
|
||||||
|
self.latest_jpeg: bytes | None = None
|
||||||
|
self.frame_seq = 0
|
||||||
|
self.viewers = 0
|
||||||
|
self.reset_flag = Event()
|
||||||
|
self.line = dict(DEFAULT_LINE)
|
||||||
|
|
||||||
|
# Eigenes Modell -> isolierter Tracker, getrennt vom Video-Pfad.
|
||||||
|
# Lazy: wird erst beim ersten aktiven Stream geladen.
|
||||||
|
self.model = None
|
||||||
|
self.names = None
|
||||||
|
|
||||||
|
self.thread = None
|
||||||
|
|
||||||
|
# -- Steuerung -----------------------------------------------------------
|
||||||
|
def start(self):
|
||||||
|
if self.thread is None:
|
||||||
|
self.thread = threading.Thread(target=self._run, daemon=True)
|
||||||
|
self.thread.start()
|
||||||
|
|
||||||
|
def set_line(self, line: dict):
|
||||||
|
with self.lock:
|
||||||
|
self.line = dict(line)
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
self.reset_flag.set()
|
||||||
|
|
||||||
|
def add_viewer(self):
|
||||||
|
with self.lock:
|
||||||
|
self.viewers += 1
|
||||||
|
|
||||||
|
def remove_viewer(self):
|
||||||
|
with self.lock:
|
||||||
|
self.viewers = max(0, self.viewers - 1)
|
||||||
|
|
||||||
|
def _viewer_count(self) -> int:
|
||||||
|
with self.lock:
|
||||||
|
return self.viewers
|
||||||
|
|
||||||
|
def _current_line(self):
|
||||||
|
with self.lock:
|
||||||
|
line = dict(self.line)
|
||||||
|
return (line["x1"], line["y1"]), (line["x2"], line["y2"])
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _extract_latest(buf: bytes):
|
||||||
|
"""
|
||||||
|
Zieht den ZULETZT vollstaendigen JPEG aus dem Puffer und verwirft
|
||||||
|
aeltere -> haelt die Latenz niedrig (Backlog wird uebersprungen).
|
||||||
|
Gibt (jpeg_or_None, rest_buffer) zurueck.
|
||||||
|
"""
|
||||||
|
latest = None
|
||||||
|
while True:
|
||||||
|
start = buf.find(b"\xff\xd8") # SOI
|
||||||
|
if start == -1:
|
||||||
|
buf = b""
|
||||||
|
break
|
||||||
|
end = buf.find(b"\xff\xd9", start + 2) # EOI
|
||||||
|
if end == -1:
|
||||||
|
buf = buf[start:] # unvollstaendig -> Tail behalten
|
||||||
|
break
|
||||||
|
latest = buf[start:end + 2]
|
||||||
|
buf = buf[end + 2:]
|
||||||
|
return latest, buf
|
||||||
|
|
||||||
|
def _ensure_model(self):
|
||||||
|
if self.model is None:
|
||||||
|
self.model = YOLO("yolo11s.pt")
|
||||||
|
self.names = self.model.model.names
|
||||||
|
|
||||||
|
def _publish(self, jpeg: bytes):
|
||||||
|
with self.frame_cond:
|
||||||
|
self.latest_jpeg = jpeg
|
||||||
|
self.frame_seq += 1
|
||||||
|
self.frame_cond.notify_all()
|
||||||
|
|
||||||
|
def _clear(self):
|
||||||
|
with self.frame_cond:
|
||||||
|
self.latest_jpeg = None
|
||||||
|
self.frame_seq += 1
|
||||||
|
self.frame_cond.notify_all()
|
||||||
|
|
||||||
|
# -- Hintergrund-Thread (laeuft die ganze Prozess-Lebensdauer) ----------
|
||||||
|
def _run(self):
|
||||||
|
state = new_state()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
# Niemand schaut zu -> keine Cam-Verbindung, idlen.
|
||||||
|
if self._viewer_count() == 0:
|
||||||
|
if self.latest_jpeg is not None:
|
||||||
|
self._clear()
|
||||||
|
time.sleep(0.3)
|
||||||
|
continue
|
||||||
|
|
||||||
|
resp = None
|
||||||
|
try:
|
||||||
|
self._ensure_model()
|
||||||
|
resp = requests.get(self.url, stream=True, timeout=(5, 10))
|
||||||
|
resp.raise_for_status()
|
||||||
|
# Frische Verbindung -> Track-IDs neu, Gesamtzaehler bleibt.
|
||||||
|
state["track_positions"].clear()
|
||||||
|
state["counted_ids"].clear()
|
||||||
|
|
||||||
|
buf = b""
|
||||||
|
for chunk in resp.iter_content(chunk_size=8192):
|
||||||
|
if self._viewer_count() == 0:
|
||||||
|
break # letzter Viewer weg -> Verbindung freigeben
|
||||||
|
if not chunk:
|
||||||
|
continue
|
||||||
|
|
||||||
|
buf += chunk
|
||||||
|
if len(buf) > self.MAX_BUFFER:
|
||||||
|
buf = buf[-self.MAX_BUFFER:]
|
||||||
|
|
||||||
|
jpeg, buf = self._extract_latest(buf)
|
||||||
|
if jpeg is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
img = cv2.imdecode(np.frombuffer(jpeg, np.uint8), cv2.IMREAD_COLOR)
|
||||||
|
if img is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if self.reset_flag.is_set():
|
||||||
|
reset_state(state)
|
||||||
|
self.reset_flag.clear()
|
||||||
|
|
||||||
|
line_start, line_end = self._current_line()
|
||||||
|
frame = process_frame(img, self.model, self.names, line_start, line_end, state)
|
||||||
|
|
||||||
|
ok, out = cv2.imencode(".jpg", frame)
|
||||||
|
if ok:
|
||||||
|
self._publish(out.tobytes())
|
||||||
|
except Exception as exc:
|
||||||
|
# Timeout / Verbindungsabbruch / HTTP-/Modell-Fehler -> sichtbar + Backoff
|
||||||
|
print(f"[webcam-grabber] {type(exc).__name__}: {exc}", flush=True)
|
||||||
|
time.sleep(1.0)
|
||||||
|
finally:
|
||||||
|
if resp is not None:
|
||||||
|
resp.close()
|
||||||
|
|
||||||
|
# -- Pro-Viewer-Generator ------------------------------------------------
|
||||||
|
def frames(self):
|
||||||
|
self.add_viewer()
|
||||||
|
last_seq = -1
|
||||||
|
got_any = False
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
with self.frame_cond:
|
||||||
|
ok = self.frame_cond.wait_for(
|
||||||
|
lambda: self.latest_jpeg is not None and self.frame_seq != last_seq,
|
||||||
|
timeout=20,
|
||||||
|
)
|
||||||
|
jpeg = self.latest_jpeg
|
||||||
|
seq = self.frame_seq
|
||||||
|
if not ok:
|
||||||
|
if got_any:
|
||||||
|
break # hatten Frames, jetzt 20s nichts -> Cam weg
|
||||||
|
continue # noch nie ein Frame (Modell laedt / Connect) -> weiter warten
|
||||||
|
if jpeg is None:
|
||||||
|
continue
|
||||||
|
got_any = True
|
||||||
|
last_seq = seq
|
||||||
|
yield (
|
||||||
|
b"--frame\r\n"
|
||||||
|
b"Content-Type: image/jpeg\r\n\r\n" + jpeg + b"\r\n"
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
self.remove_viewer()
|
||||||
|
|
||||||
|
|
||||||
|
webcam = WebcamGrabber(CAMERA_URL)
|
||||||
|
webcam.start()
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Reset-Events fuer den Video-Pfad (per Stream-ID)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
reset_events: dict[str, Event] = {}
|
||||||
|
reset_lock = Lock()
|
||||||
|
|
||||||
|
|
||||||
|
def get_reset_event(stream_id: str) -> Event:
|
||||||
|
with reset_lock:
|
||||||
|
event = reset_events.get(stream_id)
|
||||||
|
if event is None:
|
||||||
|
event = Event()
|
||||||
|
reset_events[stream_id] = event
|
||||||
|
return event
|
||||||
|
|
||||||
|
|
||||||
|
def release_reset_event(stream_id: str) -> None:
|
||||||
|
with reset_lock:
|
||||||
|
reset_events.pop(stream_id, None)
|
||||||
|
|
||||||
|
|
||||||
|
def get_webcam_stream_id() -> str:
|
||||||
|
stream_id = session.get("webcam_stream_id")
|
||||||
|
if not stream_id:
|
||||||
|
stream_id = f"webcam-{uuid.uuid4().hex}"
|
||||||
|
session["webcam_stream_id"] = stream_id
|
||||||
|
return stream_id
|
||||||
|
|
||||||
|
|
||||||
|
def get_video_stream_id(filename: str) -> str:
|
||||||
|
video_streams = session.get("video_stream_ids", {})
|
||||||
|
stream_id = video_streams.get(filename)
|
||||||
|
if not stream_id:
|
||||||
|
stream_id = f"video-{uuid.uuid4().hex}"
|
||||||
|
video_streams[filename] = stream_id
|
||||||
|
session["video_stream_ids"] = video_streams
|
||||||
|
return stream_id
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Video-Pfad (per Request, unveraendert in der Logik)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
def generate_frames(capture, line_data, stream_id: str):
|
||||||
|
state = new_state()
|
||||||
|
line_start = (line_data["x1"], line_data["y1"])
|
||||||
|
line_end = (line_data["x2"], line_data["y2"])
|
||||||
|
frame_idx = 0
|
||||||
|
reset_event = get_reset_event(stream_id)
|
||||||
|
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
ret, frame = capture.read()
|
||||||
|
if not ret:
|
||||||
|
break
|
||||||
|
|
||||||
|
frame_idx += 1
|
||||||
|
if frame_idx % 2 != 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if reset_event.is_set():
|
||||||
|
reset_state(state)
|
||||||
|
reset_event.clear()
|
||||||
|
|
||||||
|
frame = process_frame(frame, model, names, line_start, line_end, state)
|
||||||
|
|
||||||
|
ok, buffer = cv2.imencode(".jpg", frame)
|
||||||
|
if not ok:
|
||||||
|
continue
|
||||||
|
yield (
|
||||||
|
b"--frame\r\n"
|
||||||
|
b"Content-Type: image/jpeg\r\n\r\n" + buffer.tobytes() + b"\r\n"
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
capture.release()
|
||||||
|
release_reset_event(stream_id)
|
||||||
|
|
||||||
|
|
||||||
|
def detect_objects_from_video(video_path, line_data, stream_id):
|
||||||
|
cap = cv2.VideoCapture(video_path)
|
||||||
|
if not cap.isOpened():
|
||||||
|
cap.release()
|
||||||
|
raise RuntimeError("Video konnte nicht geoeffnet werden")
|
||||||
|
return generate_frames(cap, line_data, stream_id)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Routen
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
@app.route("/")
|
||||||
|
def index():
|
||||||
|
return render_template("index.html")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/start_webcam")
|
||||||
|
def start_webcam():
|
||||||
|
get_line_from_session()
|
||||||
|
stream_id = get_webcam_stream_id()
|
||||||
|
return render_template("webcam.html", stream_id=stream_id)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/webcam_feed")
|
||||||
|
def webcam_feed():
|
||||||
|
# Keine eigene Cam-Verbindung mehr pro Request -> Fan-out vom Grabber.
|
||||||
|
return Response(
|
||||||
|
webcam.frames(),
|
||||||
|
mimetype="multipart/x-mixed-replace; boundary=frame",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/set_line", methods=["POST"])
|
||||||
|
def set_counting_line():
|
||||||
|
"""Setzt die Zaehllinie (gilt fuer Video-Session UND Webcam-Grabber)."""
|
||||||
|
data = request.get_json(silent=True) or {}
|
||||||
|
try:
|
||||||
|
line = {
|
||||||
|
"x1": int(data["x1"]),
|
||||||
|
"y1": int(data["y1"]),
|
||||||
|
"x2": int(data["x2"]),
|
||||||
|
"y2": int(data["y2"]),
|
||||||
|
}
|
||||||
|
except (KeyError, ValueError, TypeError):
|
||||||
|
abort(400, description="Ungueltige Linienkoordinaten")
|
||||||
|
|
||||||
|
session["counting_line"] = line
|
||||||
|
webcam.set_line(line) # Webcam nutzt eine globale Linie (eine Kamera)
|
||||||
|
return jsonify({"status": "success", "line": line})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/get_line", methods=["GET"])
|
||||||
|
def get_counting_line():
|
||||||
|
return jsonify(get_line_from_session())
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/reset_count", methods=["POST"])
|
||||||
|
def reset_count():
|
||||||
|
data = request.get_json(silent=True) or {}
|
||||||
|
stream_id = data.get("stream_id")
|
||||||
|
if not stream_id:
|
||||||
|
abort(400, description="stream_id ist erforderlich")
|
||||||
|
|
||||||
|
valid_ids = {session.get("webcam_stream_id")}
|
||||||
|
valid_ids.update(session.get("video_stream_ids", {}).values())
|
||||||
|
valid_ids.discard(None)
|
||||||
|
if stream_id not in valid_ids:
|
||||||
|
abort(403, description="Stream gehoert nicht zur aktuellen Sitzung")
|
||||||
|
|
||||||
|
if stream_id == session.get("webcam_stream_id"):
|
||||||
|
webcam.reset()
|
||||||
|
else:
|
||||||
|
get_reset_event(stream_id).set()
|
||||||
|
return jsonify({"status": "success", "message": "Zaehler wird zurueckgesetzt"})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/upload", methods=["POST"])
|
||||||
|
def upload_video():
|
||||||
|
if "file" not in request.files:
|
||||||
|
abort(400, description="Keine Datei erhalten")
|
||||||
|
|
||||||
|
file = request.files["file"]
|
||||||
|
if not file or file.filename == "":
|
||||||
|
abort(400, description="Keine Datei ausgewaehlt")
|
||||||
|
|
||||||
|
filename = secure_filename(file.filename)
|
||||||
|
if not filename:
|
||||||
|
abort(400, description="Ungueltiger Dateiname")
|
||||||
|
if not allowed_file(filename):
|
||||||
|
abort(400, description="Ungueltiger Dateityp")
|
||||||
|
|
||||||
|
ensure_upload_dir()
|
||||||
|
name, ext = os.path.splitext(filename)
|
||||||
|
stored_filename = f"{name}_{uuid.uuid4().hex}{ext.lower()}"
|
||||||
|
file_path = os.path.join(UPLOAD_DIR, stored_filename)
|
||||||
|
file.save(file_path)
|
||||||
|
|
||||||
|
return redirect(url_for("play_video", filename=stored_filename))
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/uploads/<filename>")
|
||||||
|
def play_video(filename):
|
||||||
|
safe_filename = os.path.basename(filename)
|
||||||
|
if safe_filename != filename:
|
||||||
|
abort(400, description="Ungueltiger Dateiname")
|
||||||
|
file_path = os.path.join(UPLOAD_DIR, safe_filename)
|
||||||
|
if not os.path.isfile(file_path):
|
||||||
|
abort(404)
|
||||||
|
get_line_from_session()
|
||||||
|
stream_id = get_video_stream_id(safe_filename)
|
||||||
|
return render_template("play_video.html", filename=safe_filename, stream_id=stream_id)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/video/<path:filename>")
|
||||||
|
def send_video(filename):
|
||||||
|
return send_from_directory(UPLOAD_DIR, filename)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/video_feed/<filename>")
|
||||||
|
def video_feed(filename):
|
||||||
|
safe_filename = os.path.basename(filename)
|
||||||
|
if safe_filename != filename:
|
||||||
|
abort(400, description="Ungueltiger Dateiname")
|
||||||
|
video_path = os.path.join(UPLOAD_DIR, safe_filename)
|
||||||
|
if not os.path.isfile(video_path):
|
||||||
|
abort(404)
|
||||||
|
line_data = get_line_from_session()
|
||||||
|
stream_id = get_video_stream_id(safe_filename)
|
||||||
|
try:
|
||||||
|
generator = detect_objects_from_video(video_path, line_data, stream_id)
|
||||||
|
except RuntimeError as exc:
|
||||||
|
abort(503, description=str(exc))
|
||||||
|
return Response(generator, mimetype="multipart/x-mixed-replace; boundary=frame")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app.run("0.0.0.0", debug=False, port=8080)
|
||||||
|
|
||||||
8
gunicorn.conf.py
Normal file
8
gunicorn.conf.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
bind = "0.0.0.0:8080"
|
||||||
|
workers = 1
|
||||||
|
threads = 4
|
||||||
|
worker_class = "gthread"
|
||||||
|
timeout = 0 # Webcam-Feed läuft unbegrenzt
|
||||||
|
graceful_timeout = 30
|
||||||
|
keepalive = 5
|
||||||
|
# max_requests bewusst NICHT gesetzt – würde laufende Streams beim Recycle abbrechen
|
||||||
18
sudo
Normal file
18
sudo
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
[program:vehicle-counter]
|
||||||
|
directory=/home/joachim/git/vehicle-counter
|
||||||
|
command=/home/joachim/anaconda3/bin/python app.py
|
||||||
|
user=joachim
|
||||||
|
|
||||||
|
autostart=true
|
||||||
|
autorestart=true
|
||||||
|
startsecs=5
|
||||||
|
stopwaitsecs=10
|
||||||
|
|
||||||
|
stdout_logfile=/var/log/vehicle-counter.log
|
||||||
|
stderr_logfile=/var/log/vehicle-counter-error.log
|
||||||
|
stdout_logfile_maxbytes=20MB
|
||||||
|
stderr_logfile_maxbytes=20MB
|
||||||
|
stdout_logfile_backups=5
|
||||||
|
stderr_logfile_backups=5
|
||||||
|
|
||||||
|
environment=PYTHONUNBUFFERED="1"
|
||||||
@@ -1,40 +1,40 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Object Detection</title>
|
<title>Object Detection</title>
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-top: 50px;
|
margin-top: 50px;
|
||||||
}
|
}
|
||||||
#uploadForm {
|
#uploadForm {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
input[type="file"] {
|
input[type="file"] {
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
#startWebcam {
|
#startWebcam {
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background-color: #4CAF50;
|
background-color: #4CAF50;
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>Object Detection</h1>
|
<h1>Object Detection</h1>
|
||||||
|
|
||||||
<form id="uploadForm" action="/upload" method="post" enctype="multipart/form-data">
|
<form id="uploadForm" action="/upload" method="post" enctype="multipart/form-data">
|
||||||
<input type="file" name="file" accept="video/*" required>
|
<input type="file" name="file" accept="video/*" required>
|
||||||
<input type="submit" value="Upload Video">
|
<input type="submit" value="Upload Video">
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<a id="startWebcam" href="/start_webcam">Start Webcam Detection</a>
|
<a id="startWebcam" href="/start_webcam">Start Webcam Detection</a>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,179 +1,179 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Video Playback</title>
|
<title>Video Playback</title>
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
background-color: #f0f0f0;
|
background-color: #f0f0f0;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
.video-container {
|
.video-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 1020px;
|
width: 1020px;
|
||||||
height: 600px;
|
height: 600px;
|
||||||
}
|
}
|
||||||
#videoFeed {
|
#videoFeed {
|
||||||
width: 1020px;
|
width: 1020px;
|
||||||
height: 600px;
|
height: 600px;
|
||||||
border: 2px solid #ccc;
|
border: 2px solid #ccc;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
#lineCanvas {
|
#lineCanvas {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 1020px;
|
width: 1020px;
|
||||||
height: 600px;
|
height: 600px;
|
||||||
cursor: crosshair;
|
cursor: crosshair;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
#lineCanvas.active {
|
#lineCanvas.active {
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
.controls {
|
.controls {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
button {
|
button {
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background-color: #007bff;
|
background-color: #007bff;
|
||||||
color: white;
|
color: white;
|
||||||
transition: background-color 0.3s;
|
transition: background-color 0.3s;
|
||||||
}
|
}
|
||||||
button:hover {
|
button:hover {
|
||||||
background-color: #0056b3;
|
background-color: #0056b3;
|
||||||
}
|
}
|
||||||
button.active {
|
button.active {
|
||||||
background-color: #28a745;
|
background-color: #28a745;
|
||||||
}
|
}
|
||||||
button.danger {
|
button.danger {
|
||||||
background-color: #dc3545;
|
background-color: #dc3545;
|
||||||
}
|
}
|
||||||
button.danger:hover {
|
button.danger:hover {
|
||||||
background-color: #c82333;
|
background-color: #c82333;
|
||||||
}
|
}
|
||||||
a {
|
a {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: #007bff;
|
color: #007bff;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
a:hover {
|
a:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
.info {
|
.info {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
color: #555;
|
color: #555;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>Video Playback with Object Detection</h1>
|
<h1>Video Playback with Object Detection</h1>
|
||||||
<div class="video-container">
|
<div class="video-container">
|
||||||
<img id="videoFeed" src="{{ url_for('video_feed', filename=filename) }}" />
|
<img id="videoFeed" src="{{ url_for('video_feed', filename=filename) }}" />
|
||||||
<canvas id="lineCanvas"></canvas>
|
<canvas id="lineCanvas"></canvas>
|
||||||
</div>
|
</div>
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<button id="setLineBtn">Zähllinie setzen</button>
|
<button id="setLineBtn">Zähllinie setzen</button>
|
||||||
<button id="resetCountBtn" class="danger">Zähler zurücksetzen</button>
|
<button id="resetCountBtn" class="danger">Zähler zurücksetzen</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="info" id="infoText">Klicke auf "Zähllinie setzen" und dann zweimal auf das Video, um die Zähllinie zu definieren.</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>
|
<a href="/">Back to Home</a>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const streamId = "{{ stream_id | default('') }}";
|
const streamId = "{{ stream_id | default('') }}";
|
||||||
const canvas = document.getElementById('lineCanvas');
|
const canvas = document.getElementById('lineCanvas');
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
const setLineBtn = document.getElementById('setLineBtn');
|
const setLineBtn = document.getElementById('setLineBtn');
|
||||||
const resetCountBtn = document.getElementById('resetCountBtn');
|
const resetCountBtn = document.getElementById('resetCountBtn');
|
||||||
const infoText = document.getElementById('infoText');
|
const infoText = document.getElementById('infoText');
|
||||||
|
|
||||||
let isSettingLine = false;
|
let isSettingLine = false;
|
||||||
let firstPoint = null;
|
let firstPoint = null;
|
||||||
let currentLine = null;
|
let currentLine = null;
|
||||||
|
|
||||||
// Load existing line from server
|
// Load existing line from server
|
||||||
fetch('/api/get_line')
|
fetch('/api/get_line')
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
currentLine = data;
|
currentLine = data;
|
||||||
drawLine();
|
drawLine();
|
||||||
});
|
});
|
||||||
|
|
||||||
setLineBtn.addEventListener('click', () => {
|
setLineBtn.addEventListener('click', () => {
|
||||||
isSettingLine = !isSettingLine;
|
isSettingLine = !isSettingLine;
|
||||||
if (isSettingLine) {
|
if (isSettingLine) {
|
||||||
setLineBtn.textContent = 'Abbrechen';
|
setLineBtn.textContent = 'Abbrechen';
|
||||||
setLineBtn.classList.add('active');
|
setLineBtn.classList.add('active');
|
||||||
canvas.classList.add('active');
|
canvas.classList.add('active');
|
||||||
firstPoint = null;
|
firstPoint = null;
|
||||||
infoText.textContent = 'Klicke auf den Startpunkt der Zähllinie...';
|
infoText.textContent = 'Klicke auf den Startpunkt der Zähllinie...';
|
||||||
} else {
|
} else {
|
||||||
setLineBtn.textContent = 'Zähllinie setzen';
|
setLineBtn.textContent = 'Zähllinie setzen';
|
||||||
setLineBtn.classList.remove('active');
|
setLineBtn.classList.remove('active');
|
||||||
canvas.classList.remove('active');
|
canvas.classList.remove('active');
|
||||||
firstPoint = null;
|
firstPoint = null;
|
||||||
infoText.textContent = 'Klicke auf "Zähllinie setzen" und dann zweimal auf das Video, um die Zähllinie zu definieren.';
|
infoText.textContent = 'Klicke auf "Zähllinie setzen" und dann zweimal auf das Video, um die Zähllinie zu definieren.';
|
||||||
drawLine();
|
drawLine();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
canvas.addEventListener('click', (e) => {
|
canvas.addEventListener('click', (e) => {
|
||||||
if (!isSettingLine) return;
|
if (!isSettingLine) return;
|
||||||
|
|
||||||
const rect = canvas.getBoundingClientRect();
|
const rect = canvas.getBoundingClientRect();
|
||||||
const x = Math.round(e.clientX - rect.left);
|
const x = Math.round(e.clientX - rect.left);
|
||||||
const y = Math.round(e.clientY - rect.top);
|
const y = Math.round(e.clientY - rect.top);
|
||||||
|
|
||||||
if (!firstPoint) {
|
if (!firstPoint) {
|
||||||
firstPoint = { x, y };
|
firstPoint = { x, y };
|
||||||
infoText.textContent = 'Klicke auf den Endpunkt der Zähllinie...';
|
infoText.textContent = 'Klicke auf den Endpunkt der Zähllinie...';
|
||||||
drawTemporaryPoint(x, y);
|
drawTemporaryPoint(x, y);
|
||||||
} else {
|
} else {
|
||||||
currentLine = { x1: firstPoint.x, y1: firstPoint.y, x2: x, y2: y };
|
currentLine = { x1: firstPoint.x, y1: firstPoint.y, x2: x, y2: y };
|
||||||
|
|
||||||
// Send line to server
|
// Send line to server
|
||||||
fetch('/api/set_line', {
|
fetch('/api/set_line', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(currentLine)
|
body: JSON.stringify(currentLine)
|
||||||
})
|
})
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
console.log('Line set:', data);
|
console.log('Line set:', data);
|
||||||
infoText.textContent = 'Zähllinie erfolgreich gesetzt!';
|
infoText.textContent = 'Zähllinie erfolgreich gesetzt!';
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
infoText.textContent = 'Klicke auf "Zähllinie setzen" und dann zweimal auf das Video, um die Zähllinie zu definieren.';
|
infoText.textContent = 'Klicke auf "Zähllinie setzen" und dann zweimal auf das Video, um die Zähllinie zu definieren.';
|
||||||
}, 2000);
|
}, 2000);
|
||||||
});
|
});
|
||||||
|
|
||||||
isSettingLine = false;
|
isSettingLine = false;
|
||||||
setLineBtn.textContent = 'Zähllinie setzen';
|
setLineBtn.textContent = 'Zähllinie setzen';
|
||||||
setLineBtn.classList.remove('active');
|
setLineBtn.classList.remove('active');
|
||||||
canvas.classList.remove('active');
|
canvas.classList.remove('active');
|
||||||
firstPoint = null;
|
firstPoint = null;
|
||||||
drawLine();
|
drawLine();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
resetCountBtn.addEventListener('click', () => {
|
resetCountBtn.addEventListener('click', () => {
|
||||||
if (confirm('Zähler zurücksetzen?')) {
|
if (confirm('Zähler zurücksetzen?')) {
|
||||||
fetch('/api/reset_count', {
|
fetch('/api/reset_count', {
|
||||||
@@ -196,36 +196,36 @@
|
|||||||
.catch(err => alert(err.message));
|
.catch(err => alert(err.message));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function drawLine() {
|
function drawLine() {
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
if (currentLine) {
|
if (currentLine) {
|
||||||
ctx.strokeStyle = 'rgba(255, 255, 0, 0.8)';
|
ctx.strokeStyle = 'rgba(255, 255, 0, 0.8)';
|
||||||
ctx.lineWidth = 3;
|
ctx.lineWidth = 3;
|
||||||
ctx.setLineDash([10, 10]);
|
ctx.setLineDash([10, 10]);
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.moveTo(currentLine.x1, currentLine.y1);
|
ctx.moveTo(currentLine.x1, currentLine.y1);
|
||||||
ctx.lineTo(currentLine.x2, currentLine.y2);
|
ctx.lineTo(currentLine.x2, currentLine.y2);
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
ctx.setLineDash([]);
|
ctx.setLineDash([]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawTemporaryPoint(x, y) {
|
function drawTemporaryPoint(x, y) {
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
drawLine();
|
drawLine();
|
||||||
ctx.fillStyle = 'rgba(255, 255, 0, 0.8)';
|
ctx.fillStyle = 'rgba(255, 255, 0, 0.8)';
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(x, y, 5, 0, 2 * Math.PI);
|
ctx.arc(x, y, 5, 0, 2 * Math.PI);
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redraw line periodically in case it gets cleared
|
// Redraw line periodically in case it gets cleared
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
if (!isSettingLine && currentLine) {
|
if (!isSettingLine && currentLine) {
|
||||||
drawLine();
|
drawLine();
|
||||||
}
|
}
|
||||||
}, 100);
|
}, 100);
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,179 +1,179 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Webcam Feed</title>
|
<title>Webcam Feed</title>
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
background-color: #f0f0f0;
|
background-color: #f0f0f0;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
.video-container {
|
.video-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 1020px;
|
width: 1020px;
|
||||||
height: 600px;
|
height: 600px;
|
||||||
}
|
}
|
||||||
#videoFeed {
|
#videoFeed {
|
||||||
width: 1020px;
|
width: 1020px;
|
||||||
height: 600px;
|
height: 600px;
|
||||||
border: 2px solid #ccc;
|
border: 2px solid #ccc;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
#lineCanvas {
|
#lineCanvas {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 1020px;
|
width: 1020px;
|
||||||
height: 600px;
|
height: 600px;
|
||||||
cursor: crosshair;
|
cursor: crosshair;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
#lineCanvas.active {
|
#lineCanvas.active {
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
.controls {
|
.controls {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
button {
|
button {
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background-color: #007bff;
|
background-color: #007bff;
|
||||||
color: white;
|
color: white;
|
||||||
transition: background-color 0.3s;
|
transition: background-color 0.3s;
|
||||||
}
|
}
|
||||||
button:hover {
|
button:hover {
|
||||||
background-color: #0056b3;
|
background-color: #0056b3;
|
||||||
}
|
}
|
||||||
button.active {
|
button.active {
|
||||||
background-color: #28a745;
|
background-color: #28a745;
|
||||||
}
|
}
|
||||||
button.danger {
|
button.danger {
|
||||||
background-color: #dc3545;
|
background-color: #dc3545;
|
||||||
}
|
}
|
||||||
button.danger:hover {
|
button.danger:hover {
|
||||||
background-color: #c82333;
|
background-color: #c82333;
|
||||||
}
|
}
|
||||||
a {
|
a {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: #007bff;
|
color: #007bff;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
a:hover {
|
a:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
.info {
|
.info {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
color: #555;
|
color: #555;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>Webcam Object Detection</h1>
|
<h1>Webcam Object Detection</h1>
|
||||||
<div class="video-container">
|
<div class="video-container">
|
||||||
<img id="videoFeed" src="{{ url_for('webcam_feed') }}" />
|
<img id="videoFeed" src="{{ url_for('webcam_feed') }}" />
|
||||||
<canvas id="lineCanvas"></canvas>
|
<canvas id="lineCanvas"></canvas>
|
||||||
</div>
|
</div>
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<button id="setLineBtn">Zähllinie setzen</button>
|
<button id="setLineBtn">Zähllinie setzen</button>
|
||||||
<button id="resetCountBtn" class="danger">Zähler zurücksetzen</button>
|
<button id="resetCountBtn" class="danger">Zähler zurücksetzen</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="info" id="infoText">Klicke auf "Zähllinie setzen" und dann zweimal auf das Video, um die Zähllinie zu definieren.</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>
|
<a href="/">Back to Home</a>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const streamId = "{{ stream_id | default('') }}";
|
const streamId = "{{ stream_id | default('') }}";
|
||||||
const canvas = document.getElementById('lineCanvas');
|
const canvas = document.getElementById('lineCanvas');
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
const setLineBtn = document.getElementById('setLineBtn');
|
const setLineBtn = document.getElementById('setLineBtn');
|
||||||
const resetCountBtn = document.getElementById('resetCountBtn');
|
const resetCountBtn = document.getElementById('resetCountBtn');
|
||||||
const infoText = document.getElementById('infoText');
|
const infoText = document.getElementById('infoText');
|
||||||
|
|
||||||
let isSettingLine = false;
|
let isSettingLine = false;
|
||||||
let firstPoint = null;
|
let firstPoint = null;
|
||||||
let currentLine = null;
|
let currentLine = null;
|
||||||
|
|
||||||
// Load existing line from server
|
// Load existing line from server
|
||||||
fetch('/api/get_line')
|
fetch('/api/get_line')
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
currentLine = data;
|
currentLine = data;
|
||||||
drawLine();
|
drawLine();
|
||||||
});
|
});
|
||||||
|
|
||||||
setLineBtn.addEventListener('click', () => {
|
setLineBtn.addEventListener('click', () => {
|
||||||
isSettingLine = !isSettingLine;
|
isSettingLine = !isSettingLine;
|
||||||
if (isSettingLine) {
|
if (isSettingLine) {
|
||||||
setLineBtn.textContent = 'Abbrechen';
|
setLineBtn.textContent = 'Abbrechen';
|
||||||
setLineBtn.classList.add('active');
|
setLineBtn.classList.add('active');
|
||||||
canvas.classList.add('active');
|
canvas.classList.add('active');
|
||||||
firstPoint = null;
|
firstPoint = null;
|
||||||
infoText.textContent = 'Klicke auf den Startpunkt der Zähllinie...';
|
infoText.textContent = 'Klicke auf den Startpunkt der Zähllinie...';
|
||||||
} else {
|
} else {
|
||||||
setLineBtn.textContent = 'Zähllinie setzen';
|
setLineBtn.textContent = 'Zähllinie setzen';
|
||||||
setLineBtn.classList.remove('active');
|
setLineBtn.classList.remove('active');
|
||||||
canvas.classList.remove('active');
|
canvas.classList.remove('active');
|
||||||
firstPoint = null;
|
firstPoint = null;
|
||||||
infoText.textContent = 'Klicke auf "Zähllinie setzen" und dann zweimal auf das Video, um die Zähllinie zu definieren.';
|
infoText.textContent = 'Klicke auf "Zähllinie setzen" und dann zweimal auf das Video, um die Zähllinie zu definieren.';
|
||||||
drawLine();
|
drawLine();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
canvas.addEventListener('click', (e) => {
|
canvas.addEventListener('click', (e) => {
|
||||||
if (!isSettingLine) return;
|
if (!isSettingLine) return;
|
||||||
|
|
||||||
const rect = canvas.getBoundingClientRect();
|
const rect = canvas.getBoundingClientRect();
|
||||||
const x = Math.round(e.clientX - rect.left);
|
const x = Math.round(e.clientX - rect.left);
|
||||||
const y = Math.round(e.clientY - rect.top);
|
const y = Math.round(e.clientY - rect.top);
|
||||||
|
|
||||||
if (!firstPoint) {
|
if (!firstPoint) {
|
||||||
firstPoint = { x, y };
|
firstPoint = { x, y };
|
||||||
infoText.textContent = 'Klicke auf den Endpunkt der Zähllinie...';
|
infoText.textContent = 'Klicke auf den Endpunkt der Zähllinie...';
|
||||||
drawTemporaryPoint(x, y);
|
drawTemporaryPoint(x, y);
|
||||||
} else {
|
} else {
|
||||||
currentLine = { x1: firstPoint.x, y1: firstPoint.y, x2: x, y2: y };
|
currentLine = { x1: firstPoint.x, y1: firstPoint.y, x2: x, y2: y };
|
||||||
|
|
||||||
// Send line to server
|
// Send line to server
|
||||||
fetch('/api/set_line', {
|
fetch('/api/set_line', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(currentLine)
|
body: JSON.stringify(currentLine)
|
||||||
})
|
})
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
console.log('Line set:', data);
|
console.log('Line set:', data);
|
||||||
infoText.textContent = 'Zähllinie erfolgreich gesetzt!';
|
infoText.textContent = 'Zähllinie erfolgreich gesetzt!';
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
infoText.textContent = 'Klicke auf "Zähllinie setzen" und dann zweimal auf das Video, um die Zähllinie zu definieren.';
|
infoText.textContent = 'Klicke auf "Zähllinie setzen" und dann zweimal auf das Video, um die Zähllinie zu definieren.';
|
||||||
}, 2000);
|
}, 2000);
|
||||||
});
|
});
|
||||||
|
|
||||||
isSettingLine = false;
|
isSettingLine = false;
|
||||||
setLineBtn.textContent = 'Zähllinie setzen';
|
setLineBtn.textContent = 'Zähllinie setzen';
|
||||||
setLineBtn.classList.remove('active');
|
setLineBtn.classList.remove('active');
|
||||||
canvas.classList.remove('active');
|
canvas.classList.remove('active');
|
||||||
firstPoint = null;
|
firstPoint = null;
|
||||||
drawLine();
|
drawLine();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
resetCountBtn.addEventListener('click', () => {
|
resetCountBtn.addEventListener('click', () => {
|
||||||
if (confirm('Zähler zurücksetzen?')) {
|
if (confirm('Zähler zurücksetzen?')) {
|
||||||
fetch('/api/reset_count', {
|
fetch('/api/reset_count', {
|
||||||
@@ -196,36 +196,36 @@
|
|||||||
.catch(err => alert(err.message));
|
.catch(err => alert(err.message));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function drawLine() {
|
function drawLine() {
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
if (currentLine) {
|
if (currentLine) {
|
||||||
ctx.strokeStyle = 'rgba(255, 255, 0, 0.8)';
|
ctx.strokeStyle = 'rgba(255, 255, 0, 0.8)';
|
||||||
ctx.lineWidth = 3;
|
ctx.lineWidth = 3;
|
||||||
ctx.setLineDash([10, 10]);
|
ctx.setLineDash([10, 10]);
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.moveTo(currentLine.x1, currentLine.y1);
|
ctx.moveTo(currentLine.x1, currentLine.y1);
|
||||||
ctx.lineTo(currentLine.x2, currentLine.y2);
|
ctx.lineTo(currentLine.x2, currentLine.y2);
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
ctx.setLineDash([]);
|
ctx.setLineDash([]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawTemporaryPoint(x, y) {
|
function drawTemporaryPoint(x, y) {
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
drawLine();
|
drawLine();
|
||||||
ctx.fillStyle = 'rgba(255, 255, 0, 0.8)';
|
ctx.fillStyle = 'rgba(255, 255, 0, 0.8)';
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(x, y, 5, 0, 2 * Math.PI);
|
ctx.arc(x, y, 5, 0, 2 * Math.PI);
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redraw line periodically in case it gets cleared
|
// Redraw line periodically in case it gets cleared
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
if (!isSettingLine && currentLine) {
|
if (!isSettingLine && currentLine) {
|
||||||
drawLine();
|
drawLine();
|
||||||
}
|
}
|
||||||
}, 100);
|
}, 100);
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
10
test.py
Normal file
10
test.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import requests, numpy as np, cv2
|
||||||
|
r = requests.get("http://CAMERA-IP:81/stream", stream=True, timeout=5)
|
||||||
|
buf = b""
|
||||||
|
for chunk in r.iter_content(4096):
|
||||||
|
buf += chunk
|
||||||
|
a = buf.find(b"\xff\xd8"); b = buf.find(b"\xff\xd9")
|
||||||
|
if a != -1 and b != -1 and b > a:
|
||||||
|
img = cv2.imdecode(np.frombuffer(buf[a:b+2], np.uint8), cv2.IMREAD_COLOR)
|
||||||
|
print("frame:", None if img is None else img.shape)
|
||||||
|
break
|
||||||
BIN
uploads/highway1.mp4
Normal file
BIN
uploads/highway1.mp4
Normal file
Binary file not shown.
BIN
uploads/highway1_1e0e37010a3440f88da5ea746894748e.mp4
Normal file
BIN
uploads/highway1_1e0e37010a3440f88da5ea746894748e.mp4
Normal file
Binary file not shown.
BIN
uploads/highway1_75383486339243adae358348a6856dbc.mp4
Normal file
BIN
uploads/highway1_75383486339243adae358348a6856dbc.mp4
Normal file
Binary file not shown.
Reference in New Issue
Block a user