Files
mobile-wallpaper-service/app.py
2026-04-19 15:39:23 +02:00

216 lines
7.5 KiB
Python

#!/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/<filename>')
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)