Initial commit - Mobile Wallpaper Service
This commit is contained in:
215
app.py
Normal file
215
app.py
Normal file
@@ -0,0 +1,215 @@
|
||||
#!/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)
|
||||
Reference in New Issue
Block a user