mike23415 commited on
Commit
c59e3b5
·
verified ·
1 Parent(s): daec7de

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +372 -0
app.py ADDED
@@ -0,0 +1,372 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import io
3
+ import base64
4
+ import numpy as np
5
+ from flask import Flask, request, jsonify
6
+ from flask_cors import CORS
7
+ from PIL import Image, ImageEnhance, ImageFilter
8
+ import qrcode
9
+ from qrcode.constants import ERROR_CORRECT_H
10
+ import cv2
11
+ from io import BytesIO
12
+ import requests
13
+ from werkzeug.utils import secure_filename
14
+ import logging
15
+
16
+ # Configure logging
17
+ logging.basicConfig(level=logging.INFO)
18
+ logger = logging.getLogger(__name__)
19
+
20
+ app = Flask(__name__)
21
+ CORS(app)
22
+
23
+ # Configuration
24
+ MAX_FILE_SIZE = 10 * 1024 * 1024 # 10MB
25
+ ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp'}
26
+
27
+ def allowed_file(filename):
28
+ return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
29
+
30
+ def create_base_qr(data, size=300):
31
+ """Create a base QR code with high error correction"""
32
+ qr = qrcode.QRCode(
33
+ version=1,
34
+ error_correction=ERROR_CORRECT_H, # 30% error correction
35
+ box_size=10,
36
+ border=4,
37
+ )
38
+ qr.add_data(data)
39
+ qr.make(fit=True)
40
+
41
+ # Create QR code image
42
+ qr_img = qr.make_image(fill_color="black", back_color="white")
43
+
44
+ # Resize to specified size
45
+ qr_img = qr_img.resize((size, size), Image.LANCZOS)
46
+ return qr_img, qr
47
+
48
+ def analyze_image_contrast(image):
49
+ """Analyze image to find optimal areas for QR integration"""
50
+ # Convert to grayscale
51
+ gray = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2GRAY)
52
+
53
+ # Calculate local contrast using Laplacian
54
+ laplacian = cv2.Laplacian(gray, cv2.CV_64F)
55
+ contrast_map = np.abs(laplacian)
56
+
57
+ # Normalize contrast map
58
+ contrast_map = (contrast_map / contrast_map.max() * 255).astype(np.uint8)
59
+
60
+ # Calculate average contrast in different regions
61
+ h, w = gray.shape
62
+ regions = {
63
+ 'center': gray[h//4:3*h//4, w//4:3*w//4],
64
+ 'corners': [
65
+ gray[0:h//3, 0:w//3], # top-left
66
+ gray[0:h//3, 2*w//3:w], # top-right
67
+ gray[2*h//3:h, 0:w//3], # bottom-left
68
+ gray[2*h//3:h, 2*w//3:w] # bottom-right
69
+ ]
70
+ }
71
+
72
+ return contrast_map, regions
73
+
74
+ def create_artistic_qr(image, qr_data, style='logo_center', size=300):
75
+ """Create artistic QR code using pattern replacement"""
76
+ try:
77
+ # Create base QR code
78
+ qr_img, qr_obj = create_base_qr(qr_data, size)
79
+
80
+ # Resize input image to match QR size
81
+ image = image.resize((size, size), Image.LANCZOS)
82
+
83
+ # Convert to numpy arrays
84
+ qr_array = np.array(qr_img.convert('L'))
85
+ img_array = np.array(image.convert('RGB'))
86
+
87
+ if style == 'logo_center':
88
+ return create_logo_center_qr(qr_img, image, size)
89
+ elif style == 'pattern_replace':
90
+ return create_pattern_replace_qr(qr_array, img_array, qr_obj, size)
91
+ elif style == 'artistic_blend':
92
+ return create_artistic_blend_qr(qr_array, img_array, size)
93
+ else:
94
+ return qr_img
95
+
96
+ except Exception as e:
97
+ logger.error(f"Error creating artistic QR: {str(e)}")
98
+ # Return basic QR as fallback
99
+ return create_base_qr(qr_data, size)[0]
100
+
101
+ def create_logo_center_qr(qr_img, logo_img, size):
102
+ """Place logo in center of QR code (safest method)"""
103
+ # Calculate logo size (max 20% of QR size for compatibility)
104
+ logo_size = min(size // 5, 60)
105
+
106
+ # Resize logo
107
+ logo_resized = logo_img.resize((logo_size, logo_size), Image.LANCZOS)
108
+
109
+ # Create a copy of QR code
110
+ result = qr_img.convert('RGB').copy()
111
+
112
+ # Calculate center position
113
+ pos = ((size - logo_size) // 2, (size - logo_size) // 2)
114
+
115
+ # Paste logo with white background
116
+ white_bg = Image.new('RGB', (logo_size, logo_size), 'white')
117
+ white_bg.paste(logo_resized, (0, 0))
118
+ result.paste(white_bg, pos)
119
+
120
+ return result
121
+
122
+ def create_pattern_replace_qr(qr_array, img_array, qr_obj, size):
123
+ """Replace QR patterns with image pixels while maintaining readability"""
124
+ # Get QR modules matrix
125
+ modules = qr_obj.modules
126
+ module_count = len(modules)
127
+
128
+ # Calculate module size
129
+ module_size = size // module_count
130
+
131
+ # Create result image
132
+ result = np.zeros((size, size, 3), dtype=np.uint8)
133
+
134
+ for i in range(module_count):
135
+ for j in range(module_count):
136
+ # Calculate pixel position
137
+ y1, x1 = i * module_size, j * module_size
138
+ y2, x2 = min((i + 1) * module_size, size), min((j + 1) * module_size, size)
139
+
140
+ if modules[i][j]: # Black module
141
+ # Check if this is a critical area (finder patterns, timing, etc.)
142
+ if is_critical_module(i, j, module_count):
143
+ # Keep original black for critical modules
144
+ result[y1:y2, x1:x2] = [0, 0, 0]
145
+ else:
146
+ # Use image pixel but ensure it's dark enough
147
+ img_region = img_array[y1:y2, x1:x2]
148
+ avg_brightness = np.mean(img_region)
149
+
150
+ if avg_brightness > 128: # Too bright, make it darker
151
+ result[y1:y2, x1:x2] = img_region * 0.3
152
+ else:
153
+ result[y1:y2, x1:x2] = img_region
154
+ else: # White module
155
+ # Use image pixel but ensure it's light enough
156
+ img_region = img_array[y1:y2, x1:x2]
157
+ avg_brightness = np.mean(img_region)
158
+
159
+ if avg_brightness < 128: # Too dark, make it lighter
160
+ result[y1:y2, x1:x2] = 255 - ((255 - img_region) * 0.3)
161
+ else:
162
+ result[y1:y2, x1:x2] = img_region
163
+
164
+ return Image.fromarray(result.astype(np.uint8))
165
+
166
+ def create_artistic_blend_qr(qr_array, img_array, size):
167
+ """Blend QR with image using advanced techniques"""
168
+ # Create high contrast QR
169
+ qr_contrast = np.where(qr_array < 128, 0, 255)
170
+
171
+ # Apply adaptive blending
172
+ result = np.zeros_like(img_array)
173
+
174
+ for i in range(3): # RGB channels
175
+ channel = img_array[:, :, i]
176
+
177
+ # Where QR is black, darken the image
178
+ black_mask = qr_contrast == 0
179
+ result[:, :, i] = np.where(black_mask, channel * 0.2, channel * 1.2)
180
+
181
+ # Ensure values are in valid range
182
+ result[:, :, i] = np.clip(result[:, :, i], 0, 255)
183
+
184
+ return Image.fromarray(result.astype(np.uint8))
185
+
186
+ def is_critical_module(row, col, module_count):
187
+ """Identify critical QR modules that shouldn't be modified"""
188
+ # Finder patterns (corners)
189
+ if (row < 9 and col < 9) or \
190
+ (row < 9 and col >= module_count - 8) or \
191
+ (row >= module_count - 8 and col < 9):
192
+ return True
193
+
194
+ # Timing patterns
195
+ if row == 6 or col == 6:
196
+ return True
197
+
198
+ # Dark module
199
+ if row == 4 * module_count // 7 + 1 and col == 4 * module_count // 7 + 1:
200
+ return True
201
+
202
+ return False
203
+
204
+ def calculate_compatibility_score(qr_image):
205
+ """Calculate QR code compatibility score"""
206
+ try:
207
+ # Convert to grayscale
208
+ gray = cv2.cvtColor(np.array(qr_image), cv2.COLOR_RGB2GRAY)
209
+
210
+ # Calculate contrast ratio
211
+ min_val, max_val = np.min(gray), np.max(gray)
212
+ contrast_ratio = max_val / max(min_val, 1)
213
+
214
+ # Calculate edge sharpness
215
+ edges = cv2.Canny(gray, 50, 150)
216
+ edge_ratio = np.sum(edges > 0) / edges.size
217
+
218
+ # Calculate noise level
219
+ blur = cv2.GaussianBlur(gray, (5, 5), 0)
220
+ noise = np.std(gray - blur)
221
+
222
+ # Combine metrics into score (0-100)
223
+ contrast_score = min(contrast_ratio / 10, 1) * 40
224
+ edge_score = min(edge_ratio * 100, 1) * 30
225
+ noise_score = max(0, 1 - noise / 50) * 30
226
+
227
+ total_score = int(contrast_score + edge_score + noise_score)
228
+
229
+ return {
230
+ 'overall': total_score,
231
+ 'contrast': int(contrast_score),
232
+ 'sharpness': int(edge_score),
233
+ 'noise_level': int(noise_score),
234
+ 'recommendations': get_recommendations(total_score)
235
+ }
236
+ except Exception as e:
237
+ logger.error(f"Error calculating compatibility: {str(e)}")
238
+ return {'overall': 50, 'contrast': 50, 'sharpness': 50, 'noise_level': 50, 'recommendations': []}
239
+
240
+ def get_recommendations(score):
241
+ """Get improvement recommendations based on score"""
242
+ recommendations = []
243
+
244
+ if score < 70:
245
+ recommendations.append("Consider using higher contrast between foreground and background")
246
+ if score < 60:
247
+ recommendations.append("Try the 'Logo Center' style for better compatibility")
248
+ if score < 50:
249
+ recommendations.append("Your image might be too complex - consider simplifying")
250
+
251
+ return recommendations
252
+
253
+ def image_to_base64(image):
254
+ """Convert PIL image to base64 string"""
255
+ buffer = BytesIO()
256
+ image.save(buffer, format='PNG', optimize=True)
257
+ img_str = base64.b64encode(buffer.getvalue()).decode()
258
+ return f"data:image/png;base64,{img_str}"
259
+
260
+ @app.route('/health', methods=['GET'])
261
+ def health_check():
262
+ return jsonify({'status': 'healthy', 'message': 'QR AI Service is running'})
263
+
264
+ @app.route('/api/generate-artistic-qr', methods=['POST'])
265
+ def generate_artistic_qr():
266
+ try:
267
+ # Validate request
268
+ if 'image' not in request.files or 'url' not in request.form:
269
+ return jsonify({'error': 'Missing image or URL'}), 400
270
+
271
+ file = request.files['image']
272
+ url = request.form['url']
273
+ style = request.form.get('style', 'logo_center')
274
+ size = int(request.form.get('size', 300))
275
+
276
+ # Validate file
277
+ if file.filename == '':
278
+ return jsonify({'error': 'No file selected'}), 400
279
+
280
+ if not allowed_file(file.filename):
281
+ return jsonify({'error': 'Invalid file type'}), 400
282
+
283
+ # Validate URL
284
+ if not url or len(url) < 1:
285
+ return jsonify({'error': 'Invalid URL'}), 400
286
+
287
+ # Process image
288
+ try:
289
+ image = Image.open(file.stream).convert('RGB')
290
+ except Exception as e:
291
+ return jsonify({'error': 'Invalid image file'}), 400
292
+
293
+ # Generate artistic QR code
294
+ artistic_qr = create_artistic_qr(image, url, style, size)
295
+
296
+ # Calculate compatibility score
297
+ compatibility = calculate_compatibility_score(artistic_qr)
298
+
299
+ # Convert to base64
300
+ qr_base64 = image_to_base64(artistic_qr)
301
+
302
+ # Also generate standard QR for comparison
303
+ standard_qr = create_base_qr(url, size)[0]
304
+ standard_base64 = image_to_base64(standard_qr)
305
+
306
+ return jsonify({
307
+ 'success': True,
308
+ 'artistic_qr': qr_base64,
309
+ 'standard_qr': standard_base64,
310
+ 'compatibility': compatibility,
311
+ 'style_used': style,
312
+ 'size': size
313
+ })
314
+
315
+ except Exception as e:
316
+ logger.error(f"Error in generate_artistic_qr: {str(e)}")
317
+ return jsonify({'error': 'Internal server error'}), 500
318
+
319
+ @app.route('/api/test-compatibility', methods=['POST'])
320
+ def test_compatibility():
321
+ try:
322
+ data = request.get_json()
323
+
324
+ if 'qr_image' not in data:
325
+ return jsonify({'error': 'Missing QR image data'}), 400
326
+
327
+ # Decode base64 image
328
+ image_data = data['qr_image'].split(',')[1] # Remove data:image/png;base64,
329
+ image_bytes = base64.b64decode(image_data)
330
+ image = Image.open(BytesIO(image_bytes))
331
+
332
+ # Calculate compatibility
333
+ compatibility = calculate_compatibility_score(image)
334
+
335
+ return jsonify({
336
+ 'success': True,
337
+ 'compatibility': compatibility
338
+ })
339
+
340
+ except Exception as e:
341
+ logger.error(f"Error in test_compatibility: {str(e)}")
342
+ return jsonify({'error': 'Internal server error'}), 500
343
+
344
+ @app.route('/api/styles', methods=['GET'])
345
+ def get_available_styles():
346
+ """Get available QR code styles"""
347
+ styles = {
348
+ 'logo_center': {
349
+ 'name': 'Logo Center',
350
+ 'description': 'Places your image as a logo in the center (highest compatibility)',
351
+ 'compatibility': 95
352
+ },
353
+ 'pattern_replace': {
354
+ 'name': 'Artistic Pattern',
355
+ 'description': 'Replaces QR patterns with your image (moderate compatibility)',
356
+ 'compatibility': 75
357
+ },
358
+ 'artistic_blend': {
359
+ 'name': 'Artistic Blend',
360
+ 'description': 'Blends your image with QR code (lower compatibility)',
361
+ 'compatibility': 60
362
+ }
363
+ }
364
+
365
+ return jsonify({
366
+ 'success': True,
367
+ 'styles': styles
368
+ })
369
+
370
+ if __name__ == '__main__':
371
+ port = int(os.environ.get('PORT', 7860))
372
+ app.run(host='0.0.0.0', port=port, debug=False)