From 5c9b0d5d3b949db0c51884e3216716c58c28c6e2 Mon Sep 17 00:00:00 2001 From: Metacube Date: Sun, 5 Apr 2026 09:00:54 +0200 Subject: [PATCH] Delete rtps_daily_recorder.py --- rtps_daily_recorder.py | 458 ----------------------------------------- 1 file changed, 458 deletions(-) delete mode 100644 rtps_daily_recorder.py diff --git a/rtps_daily_recorder.py b/rtps_daily_recorder.py deleted file mode 100644 index 5de4b46..0000000 --- a/rtps_daily_recorder.py +++ /dev/null @@ -1,458 +0,0 @@ -# rtps_daily_recorder.py -import os -import time -import sys -from datetime import datetime, timedelta -import subprocess -import logging -from pathlib import Path -try: - from PIL import Image - import numpy as np -except ImportError: - print("PIL/numpy nicht installiert. Führen Sie 'pip install Pillow numpy' aus.") - sys.exit(1) - -# Einheitliche Konfiguration -CONFIG = { - "BASE_DIR": "/var/www/html/image/", - "RESIZE_DIR": "/var/www/html/images/", - "RTSP_URL": "rtsp://aurora:%2B61946194@192.168.1.133:88/videoMain", - "LOG_FILE": "/var/www/html/rtsp-recorder.log", - "HOURS_TO_RUN": 24, - "SCREENSHOT_INTERVAL": 33, - "VIDEO_FPS": 5, - "VIDEO_RETENTION_DAYS": 7, - "TARGET_WIDTH": 274, - "TARGET_HEIGHT": 52, - "SCREENSHOTS_PER_HOUR": 109, - "GREY_THRESHOLD": 20, # NEU: RGB-Differenz Schwellwert für Grau-Erkennung -} - -# Logging Setup -logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(message)s', - datefmt='%Y-%m-%d %H:%M:%S', - handlers=[ - logging.FileHandler(CONFIG["LOG_FILE"]), - logging.StreamHandler() - ] -) - -class CameraService: - def __init__(self): - self.execution_count = 0 - self.base_dir = Path(CONFIG["BASE_DIR"]) - self.resize_dir = Path(CONFIG["RESIZE_DIR"]) - self.ensure_directories() - - def ensure_directories(self): - """Stelle sicher, dass alle benötigten Verzeichnisse existieren""" - self.base_dir.mkdir(parents=True, exist_ok=True) - self.resize_dir.mkdir(parents=True, exist_ok=True) - logging.info(f"Arbeitsverzeichnisse bereit: {self.base_dir}, {self.resize_dir}") - - def validate_config(self): - """Überprüft die Konfigurationswerte""" - if not self.base_dir.exists(): - logging.error(f"Verzeichnis nicht gefunden: {self.base_dir}") - return False - return True - - def check_and_create_missing_videos(self): - """Prüft ob Videos für die letzten 7 Tage existieren und erstellt fehlende""" - logging.info("Prüfe auf fehlende Videos der letzten 7 Tage...") - - for days_ago in range(CONFIG["VIDEO_RETENTION_DAYS"]): - target_date = datetime.now() - timedelta(days=days_ago) - date_str = target_date.strftime("%Y%m%d") - - existing_videos = list(self.base_dir.glob(f"daily_video_{date_str}_*.mp4")) - - if not existing_videos: - logging.info(f"Kein Video für {date_str} gefunden. Erstelle aus vorhandenen Screenshots...") - self.create_video_for_date(target_date) - else: - logging.info(f"Video für {date_str} bereits vorhanden: {existing_videos[0].name}") - - def is_grey_image(self, image_path, max_rgb_diff=None): - """ - VERBESSERTE Grau-Erkennung mit RGB-Differenz-Methode - Funktioniert auch bei sehr dunklen Bildern! - - Prüft ob R ≈ G ≈ B für alle Pixel (bei Grau sind RGB-Werte identisch) - """ - if max_rgb_diff is None: - max_rgb_diff = CONFIG["GREY_THRESHOLD"] - - try: - img = Image.open(image_path) - - # Direkter Graustufen-Modus - if img.mode == 'L': - logging.info(f"✓ Graubild (L-Modus): {image_path.name}") - return True - - # RGB Bild analysieren mit NumPy - if img.mode in ('RGB', 'RGBA'): - img_rgb = img.convert('RGB') - - # Resize für Performance (behält Genauigkeit) - img_rgb.thumbnail((300, 300), Image.Resampling.LANCZOS) - - # NumPy Array - vektorisierte Berechnung - img_array = np.array(img_rgb, dtype=np.float32) - - r = img_array[:, :, 0] - g = img_array[:, :, 1] - b = img_array[:, :, 2] - - # Berechne maximale Differenz zwischen R, G, B für jeden Pixel - diff_rg = np.abs(r - g) - diff_gb = np.abs(g - b) - diff_rb = np.abs(r - b) - - max_diff_per_pixel = np.maximum(np.maximum(diff_rg, diff_gb), diff_rb) - - # Statistiken - mean_diff = np.mean(max_diff_per_pixel) - p95_diff = np.percentile(max_diff_per_pixel, 95) - - # ENTSCHEIDUNG: Grau wenn 95% der Pixel RGB-Differenz < Schwellwert haben - ist_grau = p95_diff < max_rgb_diff - - if ist_grau: - logging.info(f"✓ Graubild (P95={p95_diff:.1f}, Mean={mean_diff:.1f}): {image_path.name}") - else: - logging.debug(f"✗ Farbig (P95={p95_diff:.1f}, Mean={mean_diff:.1f}): {image_path.name}") - - return ist_grau - - except Exception as e: - logging.error(f"Fehler bei Graubildprüfung {image_path.name}: {e}") - return False - - return False - - def create_video_for_date(self, target_date): - """Erstellt ein Video für ein spezifisches Datum aus vorhandenen Screenshots""" - date_str = target_date.strftime("%Y%m%d") - - jpg_files = [] - for jpg in sorted(self.base_dir.glob(f"screenshot_{date_str}_*.jpg")): - # Filtere auch hier graue Bilder aus - if not self.is_grey_image(jpg): - jpg_files.append(jpg) - - if len(jpg_files) < 10: - logging.warning(f"Zu wenige Screenshots für {date_str} ({len(jpg_files)} gefunden)") - return False - - timestamp = target_date.strftime("%Y%m%d_%H%M%S") - output_file = self.base_dir / f"daily_video_{timestamp}.mp4" - temp_list = Path(f"/tmp/files_{timestamp}.txt") - - try: - with temp_list.open('w') as f: - for jpg in jpg_files: - f.write(f"file '{jpg.absolute()}'\n") - f.write(f"duration 0.2\n") - - cmd = [ - 'ffmpeg', '-y', - '-f', 'concat', - '-safe', '0', - '-i', str(temp_list), - '-vsync', 'vfr', - '-c:v', 'libx264', - '-pix_fmt', 'yuv420p', - '-preset', 'fast', - '-crf', '23', - str(output_file) - ] - - subprocess.run(cmd, check=True, capture_output=True) - - if output_file.exists() and output_file.stat().st_size > 0: - logging.info(f"Nachträglich Video erstellt für {date_str}: {output_file}") - return True - - except Exception as e: - logging.error(f"Fehler beim Erstellen des Videos für {date_str}: {e}") - return False - finally: - if temp_list.exists(): - temp_list.unlink() - - def resize_image(self, image_path): - """Passt die Bildgröße an die Zielgröße an und behält das Seitenverhältnis bei""" - try: - with Image.open(image_path) as img: - current_width, current_height = img.size - target_width = CONFIG["TARGET_WIDTH"] - target_height = CONFIG["TARGET_HEIGHT"] - - aspect_ratio = current_width / current_height - target_ratio = target_width / target_height - - if current_width != target_width or current_height != target_height: - if aspect_ratio > target_ratio: - new_width = target_width - new_height = int(target_width / aspect_ratio) - else: - new_height = target_height - new_width = int(target_height * aspect_ratio) - - background = Image.new('RGB', (target_width, target_height), (255, 255, 255)) - resized_img = img.resize((new_width, new_height), Image.Resampling.LANCZOS) - - x = (target_width - new_width) // 2 - y = (target_height - new_height) // 2 - - background.paste(resized_img, (x, y)) - background.save(image_path, "PNG", optimize=True) - logging.info(f"Bild angepasst: {image_path}") - return True - return False - except Exception as e: - logging.error(f"Fehler bei Bildanpassung {image_path}: {e}") - return False - - def resize_all_images(self): - """Passt alle PNG-Bilder im RESIZE_DIR an""" - for png_file in Path(CONFIG["RESIZE_DIR"]).glob("*.png"): - self.resize_image(png_file) - - def take_screenshot(self): - """Erstelle einen Screenshot von der RTSP-Kamera""" - timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") - output_file = self.base_dir / f"screenshot_{timestamp}.jpg" - - logging.info("Erstelle Screenshot...") - - try: - cmd = [ - 'ffmpeg', '-y', '-rtsp_transport', 'tcp', - '-analyzeduration', '20M', '-probesize', '20M', - '-i', CONFIG["RTSP_URL"], '-vframes', '1', '-q:v', '2', - '-ss', '00:00:01', str(output_file) - ] - - process = subprocess.run(cmd, capture_output=True, text=True) - if process.returncode != 0: - logging.error(f"FFmpeg Fehler: {process.stderr}") - return False - - if output_file.exists(): - logging.info(f"Screenshot erstellt: {output_file}") - return True - return False - except subprocess.CalledProcessError as e: - logging.error(f"Screenshot-Fehler: {e.stderr}") - return False - except Exception as e: - logging.error(f"Unerwarteter Fehler: {e}") - return False - - def create_daily_video(self): - """Erstelle ein Video aus den gesammelten Screenshots - LÖSCHE GRAUE BILDER""" - logging.info("="*60) - logging.info("Starte Videoerstellung mit verbessertem Graufilter...") - logging.info("="*60) - - timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") - output_file = self.base_dir / f"daily_video_{timestamp}.mp4" - temp_list = Path(f"/tmp/files_{timestamp}.txt") - - cutoff_time = datetime.now() - timedelta(hours=CONFIG["HOURS_TO_RUN"]) - - # Sammle ALLE JPG-Dateien - all_jpg_files = sorted([ - f for f in self.base_dir.glob("screenshot_*.jpg") - if f.stat().st_mtime > cutoff_time.timestamp() - ]) - - logging.info(f"Gefunden: {len(all_jpg_files)} Screenshots insgesamt") - - # FILTERE und LÖSCHE graue Bilder - jpg_files = [] - grey_files_to_delete = [] - - for idx, jpg in enumerate(all_jpg_files, 1): - if idx % 50 == 0: - logging.info(f" Prüfe Bild {idx}/{len(all_jpg_files)}...") - - if not self.is_grey_image(jpg): - jpg_files.append(jpg) - else: - grey_files_to_delete.append(jpg) - - # LÖSCHE alle grauen Bilder - for grey_file in grey_files_to_delete: - try: - grey_file.unlink() - logging.info(f"🗑️ Graues Bild gelöscht: {grey_file.name}") - except Exception as e: - logging.error(f"Fehler beim Löschen von {grey_file.name}: {e}") - - grey_count = len(grey_files_to_delete) - logging.info("") - logging.info("="*60) - logging.info(f"FILTER-ERGEBNIS:") - logging.info(f" Gute Bilder: {len(jpg_files)}") - logging.info(f" Graue gelöscht: {grey_count}") - - if len(all_jpg_files) > 0: - filter_rate = (grey_count / len(all_jpg_files)) * 100 - logging.info(f" Löschrate: {filter_rate:.1f}%") - - if filter_rate > 50: - logging.warning("⚠️ WARNUNG: Über 50% gelöscht - Filter möglicherweise zu strikt!") - logging.warning(f"⚠️ Erhöhe GREY_THRESHOLD in CONFIG (aktuell: {CONFIG['GREY_THRESHOLD']})") - logging.info("="*60) - - if len(jpg_files) < 10: - logging.warning(f"Zu wenige Bilder ({len(jpg_files)}) für Video") - return False - - try: - with temp_list.open('w') as f: - for jpg in jpg_files: - f.write(f"file '{jpg.absolute()}'\n") - f.write(f"duration 0.2\n") - - # Erstelle Timestamp-Text (z.B. "Oktober 14:25") - start_time = cutoff_time.strftime("%d.%m.%Y %H:%M") - - - cmd = [ - 'ffmpeg', '-y', - '-f', 'concat', - '-safe', '0', - '-i', str(temp_list), - '-vsync', 'vfr', - '-vf', f"drawtext=fontfile=/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf:text='{start_time}':fontcolor=white:fontsize=24:box=1:boxcolor=black@0.7:boxborderw=5:x=10:y=h-th-10", - '-c:v', 'libx264', - '-pix_fmt', 'yuv420p', - '-preset', 'fast', - '-crf', '23', - str(output_file) - ] - - - subprocess.run(cmd, check=True, capture_output=True) - - if output_file.exists() and output_file.stat().st_size > 0: - logging.info(f"✅ Video erstellt: {output_file}") - - # Lösche nur die GUTEN JPGs (graue wurden schon gelöscht) - for jpg in jpg_files: - jpg.unlink() - logging.info(f"✅ {len(jpg_files)} gute Screenshots gelöscht nach Videoerstellung") - return True - - except Exception as e: - logging.error(f"Fehler bei Videoerstellung: {e}") - return False - finally: - if temp_list.exists(): - temp_list.unlink() - - - def cleanup_old_files(self): - """Lösche alte Video- und Bilddateien (nur älter als 7 Tage)""" - cutoff_time = datetime.now() - timedelta(days=CONFIG["VIDEO_RETENTION_DAYS"]) - - # Lösche verwaiste Screenshots älter als 7 Tage - for jpg in self.base_dir.glob("screenshot_*.jpg"): - if jpg.stat().st_mtime < cutoff_time.timestamp(): - jpg.unlink() - logging.info(f"Verwaistes Bild gelöscht (>7 Tage): {jpg}") - - def cleanup(self): - """Aufräumen vor Beendigung""" - try: - self.cleanup_old_files() - logging.info("Aufräumen abgeschlossen") - except Exception as e: - logging.error(f"Fehler beim Aufräumen: {e}") - -def main(): - service = CameraService() - logging.info("="*60) - logging.info("Starte Kamera-Service mit verbesserter Grau-Erkennung") - logging.info(f"Grau-Schwellwert: RGB-Differenz < {CONFIG['GREY_THRESHOLD']}") - logging.info("="*60) - - - # Warte unbegrenzt bis grey.py fertig ist - logging.info("Starte Grau-Filterung (warte auf Abschluss)...") - returncode = subprocess.call([sys.executable, "grey.py"]) - - if returncode == 0: - logging.info("✓ Grau-Filterung abgeschlossen") - else: - logging.error(f"✗ Grau-Filterung fehlgeschlagen (Exit-Code: {returncode})") - - - retry_count = 0 - max_retries = 3 - - try: - if not service.validate_config(): - raise RuntimeError("Ungültige Konfiguration") - - service.check_and_create_missing_videos() - - while True: - service.execution_count = 0 - total_screenshots = CONFIG["HOURS_TO_RUN"] * CONFIG["SCREENSHOTS_PER_HOUR"] - logging.info(f"Starte neuen 24-Stunden-Zyklus mit {total_screenshots} Screenshots") - - service.resize_all_images() - - while service.execution_count < total_screenshots: - if service.take_screenshot(): - service.execution_count += 1 - retry_count = 0 - logging.info(f"Screenshot {service.execution_count} von {total_screenshots}") - else: - retry_count += 1 - if retry_count >= max_retries: - logging.error(f"Screenshot nach {max_retries} Versuchen fehlgeschlagen") - raise RuntimeError("Zu viele fehlgeschlagene Versuche") - logging.warning(f"Screenshot fehlgeschlagen ({retry_count}/{max_retries}), warte 60 Sekunden...") - time.sleep(60) - continue - - if service.execution_count % 100 == 0: - service.cleanup_old_files() - - if service.execution_count < total_screenshots: - time.sleep(CONFIG["SCREENSHOT_INTERVAL"]) - - if service.create_daily_video(): - logging.info("✓ 24-Stunden-Video erfolgreich erstellt") - else: - logging.error("✗ Fehler beim Erstellen des Tagesvideos") - - service.check_and_create_missing_videos() - - logging.info("Starte neuen 24-Stunden-Zyklus...") - - except KeyboardInterrupt: - logging.info("Programm durch Benutzer beendet") - except Exception as e: - logging.error(f"Unerwarteter Fehler: {e}") - raise - finally: - service.cleanup() - -if __name__ == "__main__": - if len(sys.argv) > 1 and sys.argv[1] == "video": - service = CameraService() - service.create_daily_video() - sys.exit(0) - else: - main()