#!/usr/bin/env python3 """Mobile Wallpaper Processor - Flask Web Service Skaliert, Schneidet zu und entfernt Text aus Bildern für 1440x3120 Displays """ import os import cv2 import numpy as np import uuid import time from datetime import datetime, timedelta from flask import Flask, render_template, request, send_file, jsonify, url_for from werkzeug.utils import secure_filename from PIL import Image, ImageEnhance import easyocr import threading # Konfiguration TARGET_WIDTH = 1440 TARGET_HEIGHT = 3120 TARGET_ASPECT = TARGET_WIDTH / TARGET_HEIGHT UPLOAD_FOLDER = 'static/uploads' DOWNLOAD_FOLDER = 'static/downloads' ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp', 'tiff'} MAX_FILE_SIZE = 50 * 1024 * 1024 # 50MB app = Flask(__name__) app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER app.config['DOWNLOAD_FOLDER'] = DOWNLOAD_FOLDER app.config['MAX_CONTENT_LENGTH'] = MAX_FILE_SIZE # Globaler OCR Reader (wird einmal geladen) ocr_reader = None def get_ocr_reader(): global ocr_reader if ocr_reader is None: print("Lade OCR Model...") ocr_reader = easyocr.Reader(['de', 'en'], gpu=False) return ocr_reader def allowed_file(filename): return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS def cleanup_old_files(): """Löscht Dateien älter als 1 Stunde""" cutoff = datetime.now() - timedelta(hours=1) for folder in [UPLOAD_FOLDER, DOWNLOAD_FOLDER]: for filename in os.listdir(folder): filepath = os.path.join(folder, filename) try: if os.path.isfile(filepath): file_time = datetime.fromtimestamp(os.path.getmtime(filepath)) if file_time < cutoff: os.remove(filepath) except Exception as e: print(f"Cleanup error: {e}") def smart_crop(img_array, target_width, target_height): """Intelligentes Zuschneiden mit Fokus auf Bildzentrum""" img = Image.fromarray(cv2.cvtColor(img_array, cv2.COLOR_BGR2RGB)) original_width, original_height = img.size target_aspect = target_width / target_height original_aspect = original_width / original_height if original_aspect > target_aspect: # Bild ist breiter als Ziel: Horizontal zuschneiden new_width = int(original_height * target_aspect) left = (original_width - new_width) // 2 img_cropped = img.crop((left, 0, left + new_width, original_height)) else: # Bild ist höher als Ziel: Vertikal zuschneiden new_height = int(original_width / target_aspect) top = (original_height - new_height) // 2 img_cropped = img.crop((0, top, original_width, top + new_height)) # Auf Zielgröße skalieren img_resized = img_cropped.resize((target_width, target_height), Image.Resampling.LANCZOS) return cv2.cvtColor(np.array(img_resized), cv2.COLOR_RGB2BGR) def detect_text_regions(img): """Erkennt Textbereiche im Bild mit OCR""" reader = get_ocr_reader() results = reader.readtext(img) mask = np.zeros(img.shape[:2], dtype=np.uint8) detected_boxes = [] for (bbox, text, conf) in results: if conf > 0.3: # Nur Text mit ausreichender Konfidenz # bbox sind 4 Punkte: [[x1,y1], [x2,y2], [x3,y3], [x4,y4]] pts = np.array(bbox, np.int32) # Erweitere Box um Padding x_coords = [p[0] for p in bbox] y_coords = [p[1] for p in bbox] x_min, x_max = max(0, min(x_coords) - 10), min(img.shape[1], max(x_coords) + 10) y_min, y_max = max(0, min(y_coords) - 10), min(img.shape[0], max(y_coords) + 10) cv2.rectangle(mask, (x_min, y_min), (x_max, y_max), 255, -1) detected_boxes.append({'text': text, 'confidence': conf, 'box': [x_min, y_min, x_max, y_max]}) return mask, detected_boxes def remove_text_inpaint(img, mask): """Entfernt Text durch OpenCV Inpainting""" if np.sum(mask) == 0: return img # Telea Inpainting - gut für kleine strukturierte Bereiche wie Text result = cv2.inpaint(img, mask, 3, cv2.INPAINT_TELEA) return result def auto_enhance(img): """Automatische Bildverbesserung""" pil_img = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB)) # Kontrast leicht erhöhen enhancer = ImageEnhance.Contrast(pil_img) pil_img = enhancer.enhance(1.1) # Sättigung leicht erhöhen enhancer = ImageEnhance.Color(pil_img) pil_img = enhancer.enhance(1.1) # Schärfen enhancer = ImageEnhance.Sharpness(pil_img) pil_img = enhancer.enhance(1.2) return cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR) def process_image(input_path, remove_text=True): """Hauptverarbeitungs-Pipeline""" # Bild laden img = cv2.imread(input_path) if img is None: raise ValueError("Bild konnte nicht geladen werden") # Optional: Text entfernen if remove_text: mask, detected_text = detect_text_regions(img) if len(detected_text) > 0: img = remove_text_inpaint(img, mask) # Smart Crop auf Zielauflösung img_processed = smart_crop(img, TARGET_WIDTH, TARGET_HEIGHT) # Auto-Enhance img_processed = auto_enhance(img_processed) return img_processed @app.route('/') def index(): return render_template('index.html') @app.route('/upload', methods=['POST']) def upload_file(): if 'file' not in request.files: return jsonify({'error': 'Keine Datei hochgeladen'}), 400 file = request.files['file'] if file.filename == '': return jsonify({'error': 'Keine Datei ausgewählt'}), 400 if not allowed_file(file.filename): return jsonify({'error': 'Dateiformat nicht erlaubt. Erlaubt: ' + str(ALLOWED_EXTENSIONS)}), 400 try: # Cleanup alte Dateien cleanup_old_files() # Eindeutigen Dateinamen generieren filename = str(uuid.uuid4())[:8] + '_' + secure_filename(file.filename) input_path = os.path.join(app.config['UPLOAD_FOLDER'], filename) file.save(input_path) # Optionen remove_text = request.form.get('remove_text', 'true').lower() == 'true' # Bild verarbeiten processed = process_image(input_path, remove_text=remove_text) # Ausgabedatei output_filename = 'processed_' + os.path.splitext(filename)[0] + '.jpg' output_path = os.path.join(app.config['DOWNLOAD_FOLDER'], output_filename) cv2.imwrite(output_path, processed, [cv2.IMWRITE_JPEG_QUALITY, 95]) # Original löschen os.remove(input_path) return jsonify({ 'success': True, 'download_url': url_for('download_file', filename=output_filename), 'preview_url': '/' + output_path, 'message': f'Bild verarbeitet: {TARGET_WIDTH}x{TARGET_HEIGHT}px' }) except Exception as e: print(f"Error: {e}") return jsonify({'error': str(e)}), 500 @app.route('/download/') def download_file(filename): return send_file(os.path.join(app.config['DOWNLOAD_FOLDER'], filename), as_attachment=True, download_name='wallpaper_1440x3120.jpg') @app.route('/cleanup', methods=['POST']) def manual_cleanup(): cleanup_old_files() return jsonify({'success': True, 'message': 'Aufgeräumt'}) if __name__ == '__main__': print(f"Starting Mobile Wallpaper Service...") print(f"Zielauflösung: {TARGET_WIDTH}x{TARGET_HEIGHT} ({TARGET_ASPECT:.3f})") app.run(host='0.0.0.0', port=5000, debug=True)