216 lines
7.5 KiB
Python
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)
|