mike23415 commited on
Commit
fc9d814
·
verified ·
1 Parent(s): f17599e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +333 -226
app.py CHANGED
@@ -4,14 +4,16 @@ 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)
@@ -27,227 +29,372 @@ ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp'}
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):
@@ -260,60 +407,44 @@ def image_to_base64(image):
260
  @app.route('/', methods=['GET'])
261
  def home():
262
  return jsonify({
263
- 'message': 'QR AI Service is running',
264
  'status': 'healthy',
265
- 'endpoints': {
266
- 'generate': '/api/generate-artistic-qr',
267
- 'test': '/api/test-compatibility',
268
- 'styles': '/api/styles',
269
- 'health': '/health'
270
- }
271
  })
272
 
273
  @app.route('/health', methods=['GET'])
274
  def health_check():
275
- return jsonify({'status': 'healthy', 'message': 'QR AI Service is running'})
276
 
277
  @app.route('/api/generate-artistic-qr', methods=['POST'])
278
  def generate_artistic_qr():
279
  try:
280
- # Validate request
281
  if 'image' not in request.files or 'url' not in request.form:
282
  return jsonify({'error': 'Missing image or URL'}), 400
283
 
284
  file = request.files['image']
285
  url = request.form['url']
286
- style = request.form.get('style', 'logo_center')
287
- size = int(request.form.get('size', 300))
288
 
289
- # Validate file
290
- if file.filename == '':
291
- return jsonify({'error': 'No file selected'}), 400
292
 
293
- if not allowed_file(file.filename):
294
- return jsonify({'error': 'Invalid file type'}), 400
295
-
296
- # Validate URL
297
- if not url or len(url) < 1:
298
  return jsonify({'error': 'Invalid URL'}), 400
299
 
300
- # Process image
301
  try:
302
  image = Image.open(file.stream).convert('RGB')
303
- except Exception as e:
304
  return jsonify({'error': 'Invalid image file'}), 400
305
 
306
- # Generate artistic QR code
307
- artistic_qr = create_artistic_qr(image, url, style, size)
308
-
309
- # Calculate compatibility score
310
  compatibility = calculate_compatibility_score(artistic_qr)
311
-
312
- # Convert to base64
313
  qr_base64 = image_to_base64(artistic_qr)
314
 
315
- # Also generate standard QR for comparison
316
- standard_qr = create_base_qr(url, size)[0]
317
  standard_base64 = image_to_base64(standard_qr)
318
 
319
  return jsonify({
@@ -329,57 +460,33 @@ def generate_artistic_qr():
329
  logger.error(f"Error in generate_artistic_qr: {str(e)}")
330
  return jsonify({'error': 'Internal server error'}), 500
331
 
332
- @app.route('/api/test-compatibility', methods=['POST'])
333
- def test_compatibility():
334
- try:
335
- data = request.get_json()
336
-
337
- if 'qr_image' not in data:
338
- return jsonify({'error': 'Missing QR image data'}), 400
339
-
340
- # Decode base64 image
341
- image_data = data['qr_image'].split(',')[1] # Remove data:image/png;base64,
342
- image_bytes = base64.b64decode(image_data)
343
- image = Image.open(BytesIO(image_bytes))
344
-
345
- # Calculate compatibility
346
- compatibility = calculate_compatibility_score(image)
347
-
348
- return jsonify({
349
- 'success': True,
350
- 'compatibility': compatibility
351
- })
352
-
353
- except Exception as e:
354
- logger.error(f"Error in test_compatibility: {str(e)}")
355
- return jsonify({'error': 'Internal server error'}), 500
356
-
357
  @app.route('/api/styles', methods=['GET'])
358
  def get_available_styles():
359
- """Get available QR code styles"""
360
  styles = {
361
- 'logo_center': {
362
- 'name': 'Logo Center',
363
- 'description': 'Places your image as a logo in the center (highest compatibility)',
364
- 'compatibility': 95
365
  },
366
- 'pattern_replace': {
367
- 'name': 'Artistic Pattern',
368
- 'description': 'Replaces QR patterns with your image (moderate compatibility)',
369
  'compatibility': 75
370
  },
371
- 'artistic_blend': {
372
- 'name': 'Artistic Blend',
373
- 'description': 'Blends your image with QR code (lower compatibility)',
374
- 'compatibility': 60
 
 
 
 
 
375
  }
376
  }
377
 
378
- return jsonify({
379
- 'success': True,
380
- 'styles': styles
381
- })
382
 
383
  if __name__ == '__main__':
384
  port = int(os.environ.get('PORT', 7860))
385
- app.run(host='0.0.0.0', port=port, debug=False)
 
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, ImageDraw, ImageFont
8
  import qrcode
9
+ from qrcode.constants import ERROR_CORRECT_H, ERROR_CORRECT_M
10
  import cv2
11
  from io import BytesIO
12
  import requests
13
  from werkzeug.utils import secure_filename
14
  import logging
15
+ from sklearn.cluster import KMeans
16
+ import colorsys
17
 
18
  # Configure logging
19
  logging.basicConfig(level=logging.INFO)
 
29
  def allowed_file(filename):
30
  return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
31
 
32
+ def extract_dominant_colors(image, n_colors=5):
33
+ """Extract dominant colors from image using K-means clustering"""
34
+ # Resize image for faster processing
35
+ image_small = image.resize((100, 100))
 
 
 
 
 
 
36
 
37
+ # Convert to RGB array
38
+ data = np.array(image_small)
39
+ data = data.reshape((-1, 3))
40
 
41
+ # Remove pure white and very light colors
42
+ data = data[np.sum(data, axis=1) < 720] # Remove colors where R+G+B > 720
43
+
44
+ if len(data) == 0:
45
+ return [(0, 0, 0), (255, 255, 255)]
46
+
47
+ # Perform K-means clustering
48
+ kmeans = KMeans(n_clusters=min(n_colors, len(data)), random_state=42, n_init=10)
49
+ kmeans.fit(data)
50
+
51
+ colors = kmeans.cluster_centers_.astype(int)
52
+
53
+ # Sort colors by darkness (darker first for better QR contrast)
54
+ colors = sorted(colors, key=lambda x: sum(x))
 
 
 
 
 
 
 
 
 
 
 
 
 
55
 
56
+ return [tuple(color) for color in colors]
57
 
58
+ def create_brand_style_qr(image, qr_data, style='brand_integration', size=400):
59
+ """Create brand-style QR codes like in the reference image"""
60
  try:
61
+ # Extract dominant colors
62
+ colors = extract_dominant_colors(image)
63
+ primary_color = colors[0] if colors else (0, 0, 0)
64
+ background_color = colors[-1] if len(colors) > 1 else (255, 255, 255)
65
 
66
+ # Ensure good contrast
67
+ if sum(primary_color) > 300: # If primary is too light
68
+ primary_color = tuple(max(0, c - 100) for c in primary_color)
69
 
70
+ if sum(background_color) < 400: # If background is too dark
71
+ background_color = tuple(min(255, c + 100) for c in background_color)
 
72
 
73
+ if style == 'brand_integration':
74
+ return create_brand_integration_qr(image, qr_data, primary_color, background_color, size)
75
+ elif style == 'logo_overlay':
76
+ return create_logo_overlay_qr(image, qr_data, primary_color, background_color, size)
77
+ elif style == 'pattern_fusion':
78
+ return create_pattern_fusion_qr(image, qr_data, primary_color, background_color, size)
79
+ elif style == 'artistic_dots':
80
+ return create_artistic_dots_qr(image, qr_data, primary_color, background_color, size)
81
  else:
82
+ return create_brand_integration_qr(image, qr_data, primary_color, background_color, size)
83
 
84
  except Exception as e:
85
+ logger.error(f"Error creating brand QR: {str(e)}")
86
+ # Fallback to basic QR
87
+ return create_basic_qr(qr_data, size)
88
 
89
+ def create_brand_integration_qr(image, qr_data, primary_color, bg_color, size):
90
+ """Create QR with brand logo integrated in center like Pizza Hut, Starbucks style"""
 
 
91
 
92
+ # Create QR code with medium error correction for logo integration
93
+ qr = qrcode.QRCode(
94
+ version=3, # Higher version for more space
95
+ error_correction=ERROR_CORRECT_M, # 15% error correction
96
+ box_size=10,
97
+ border=2,
98
+ )
99
+ qr.add_data(qr_data)
100
+ qr.make(fit=True)
101
+
102
+ # Create base QR with brand colors
103
+ qr_img = qr.make_image(fill_color=primary_color, back_color=bg_color)
104
+ qr_img = qr_img.resize((size, size), Image.LANCZOS)
105
+
106
+ # Prepare logo
107
+ logo_size = size // 4 # Logo takes 1/4 of QR size
108
+ logo = image.resize((logo_size, logo_size), Image.LANCZOS)
109
 
110
+ # Create circular mask for logo (like in the examples)
111
+ mask = Image.new('L', (logo_size, logo_size), 0)
112
+ draw = ImageDraw.Draw(mask)
113
+ draw.ellipse((0, 0, logo_size, logo_size), fill=255)
114
 
115
+ # Apply circular mask to logo
116
+ logo_circular = Image.new('RGBA', (logo_size, logo_size), (0, 0, 0, 0))
117
+ logo_circular.paste(logo, (0, 0))
118
+ logo_circular.putalpha(mask)
119
 
120
+ # Create white background circle for logo
121
+ white_circle = Image.new('RGBA', (logo_size + 20, logo_size + 20), (255, 255, 255, 255))
122
+ mask_white = Image.new('L', (logo_size + 20, logo_size + 20), 0)
123
+ draw_white = ImageDraw.Draw(mask_white)
124
+ draw_white.ellipse((0, 0, logo_size + 20, logo_size + 20), fill=255)
125
+ white_circle.putalpha(mask_white)
126
+
127
+ # Convert QR to RGBA for compositing
128
+ qr_rgba = qr_img.convert('RGBA')
129
+
130
+ # Paste white circle first (background for logo)
131
+ white_pos = ((size - logo_size - 20) // 2, (size - logo_size - 20) // 2)
132
+ qr_rgba.paste(white_circle, white_pos, white_circle)
133
+
134
+ # Paste logo on top
135
+ logo_pos = ((size - logo_size) // 2, (size - logo_size) // 2)
136
+ qr_rgba.paste(logo_circular, logo_pos, logo_circular)
137
+
138
+ return qr_rgba.convert('RGB')
139
+
140
+ def create_logo_overlay_qr(image, qr_data, primary_color, bg_color, size):
141
+ """Create QR with logo overlaid and stylized dots"""
142
+
143
+ # Create QR code
144
+ qr = qrcode.QRCode(
145
+ version=2,
146
+ error_correction=ERROR_CORRECT_H, # High error correction for heavy modification
147
+ box_size=8,
148
+ border=3,
149
+ )
150
+ qr.add_data(qr_data)
151
+ qr.make(fit=True)
152
+
153
+ # Get QR matrix
154
+ modules = qr.modules
155
+ module_count = len(modules)
156
+ module_size = size // (module_count + 6) # +6 for border
157
+
158
+ # Create result image
159
+ result = Image.new('RGB', (size, size), bg_color)
160
+ draw = ImageDraw.Draw(result)
161
+
162
+ # Draw QR modules as circles/rounded rectangles
163
+ for i in range(module_count):
164
+ for j in range(module_count):
165
+ if modules[i][j]: # Black module
166
+ x = (j + 3) * module_size
167
+ y = (i + 3) * module_size
168
+
169
+ # Skip center area for logo
170
+ center_start = module_count // 2 - 3
171
+ center_end = module_count // 2 + 3
172
+ if center_start <= i <= center_end and center_start <= j <= center_end:
173
+ continue
174
+
175
+ # Draw rounded square or circle
176
+ if is_finder_pattern(i, j, module_count):
177
+ # Finder patterns as solid rectangles
178
+ draw.rectangle([x, y, x + module_size - 1, y + module_size - 1],
179
+ fill=primary_color)
180
+ else:
181
+ # Regular modules as circles
182
+ margin = module_size // 4
183
+ draw.ellipse([x + margin, y + margin,
184
+ x + module_size - margin, y + module_size - margin],
185
+ fill=primary_color)
186
+
187
+ # Add logo in center
188
+ logo_size = size // 3
189
+ logo = image.resize((logo_size, logo_size), Image.LANCZOS)
190
+ logo_pos = ((size - logo_size) // 2, (size - logo_size) // 2)
191
+
192
+ # Create white background for logo
193
+ draw.rectangle([logo_pos[0] - 10, logo_pos[1] - 10,
194
+ logo_pos[0] + logo_size + 10, logo_pos[1] + logo_size + 10],
195
+ fill=(255, 255, 255))
196
+
197
+ result.paste(logo, logo_pos)
198
 
199
  return result
200
 
201
+ def create_pattern_fusion_qr(image, qr_data, primary_color, bg_color, size):
202
+ """Create QR with image patterns fused into the code"""
203
+
204
+ # Create QR code
205
+ qr = qrcode.QRCode(
206
+ version=2,
207
+ error_correction=ERROR_CORRECT_H,
208
+ box_size=10,
209
+ border=2,
210
+ )
211
+ qr.add_data(qr_data)
212
+ qr.make(fit=True)
213
+
214
+ # Get modules and resize image to match
215
+ modules = qr.modules
216
  module_count = len(modules)
217
 
218
+ # Resize image to QR dimensions
219
+ qr_size = module_count * 10
220
+ image_resized = image.resize((qr_size, qr_size), Image.LANCZOS)
221
 
222
+ # Convert to arrays
223
+ img_array = np.array(image_resized)
224
+
225
+ # Create result
226
+ result = np.full((qr_size, qr_size, 3), bg_color, dtype=np.uint8)
227
+ module_size = 10
228
 
229
  for i in range(module_count):
230
  for j in range(module_count):
 
231
  y1, x1 = i * module_size, j * module_size
232
+ y2, x2 = (i + 1) * module_size, (j + 1) * module_size
233
 
234
  if modules[i][j]: # Black module
235
+ if is_finder_pattern(i, j, module_count) or is_timing_pattern(i, j):
236
+ # Keep critical patterns solid
237
+ result[y1:y2, x1:x2] = primary_color
 
238
  else:
239
+ # Use image texture but darkened
240
+ img_patch = img_array[y1:y2, x1:x2]
241
+ darkened = (img_patch * 0.3).astype(np.uint8)
242
+ result[y1:y2, x1:x2] = darkened
 
 
 
 
243
  else: # White module
244
+ # Use image texture but lightened
245
+ img_patch = img_array[y1:y2, x1:x2]
246
+ lightened = (img_patch * 0.7 + np.array(bg_color) * 0.3).astype(np.uint8)
247
+ result[y1:y2, x1:x2] = lightened
248
+
249
+ # Resize to target size
250
+ result_img = Image.fromarray(result)
251
+ return result_img.resize((size, size), Image.LANCZOS)
252
+
253
+ def create_artistic_dots_qr(image, qr_data, primary_color, bg_color, size):
254
+ """Create QR with artistic dots and patterns like in the reference"""
255
+
256
+ # Create QR code
257
+ qr = qrcode.QRCode(
258
+ version=2,
259
+ error_correction=ERROR_CORRECT_H,
260
+ box_size=12,
261
+ border=2,
262
+ )
263
+ qr.add_data(qr_data)
264
+ qr.make(fit=True)
265
+
266
+ modules = qr.modules
267
+ module_count = len(modules)
268
+ module_size = size // (module_count + 4)
269
+
270
+ # Create gradient background
271
+ result = Image.new('RGB', (size, size), bg_color)
272
+ draw = ImageDraw.Draw(result)
273
+
274
+ # Add subtle gradient
275
+ for y in range(size):
276
+ color_factor = y / size
277
+ grad_color = tuple(int(bg_color[i] * (1 - color_factor * 0.1)) for i in range(3))
278
+ draw.line([(0, y), (size, y)], fill=grad_color)
279
+
280
+ # Draw QR modules with various artistic shapes
281
+ for i in range(module_count):
282
+ for j in range(module_count):
283
+ if modules[i][j]: # Black module
284
+ x = (j + 2) * module_size
285
+ y = (i + 2) * module_size
286
 
287
+ if is_finder_pattern(i, j, module_count):
288
+ # Finder patterns with decorative elements
289
+ draw.rectangle([x, y, x + module_size, y + module_size],
290
+ fill=primary_color)
291
+ # Add corner decorations
292
+ corner_size = module_size // 4
293
+ draw.ellipse([x - corner_size, y - corner_size,
294
+ x + corner_size, y + corner_size],
295
+ fill=primary_color)
296
  else:
297
+ # Regular modules as artistic dots
298
+ dot_size = module_size - 2
299
+ center_x, center_y = x + module_size // 2, y + module_size // 2
300
+
301
+ # Vary dot shapes
302
+ if (i + j) % 3 == 0:
303
+ # Circle
304
+ draw.ellipse([center_x - dot_size//2, center_y - dot_size//2,
305
+ center_x + dot_size//2, center_y + dot_size//2],
306
+ fill=primary_color)
307
+ elif (i + j) % 3 == 1:
308
+ # Square
309
+ draw.rectangle([center_x - dot_size//2, center_y - dot_size//2,
310
+ center_x + dot_size//2, center_y + dot_size//2],
311
+ fill=primary_color)
312
+ else:
313
+ # Diamond
314
+ points = [
315
+ (center_x, center_y - dot_size//2),
316
+ (center_x + dot_size//2, center_y),
317
+ (center_x, center_y + dot_size//2),
318
+ (center_x - dot_size//2, center_y)
319
+ ]
320
+ draw.polygon(points, fill=primary_color)
321
 
322
+ # Add logo in center
323
+ logo_size = size // 4
324
+ logo = image.resize((logo_size, logo_size), Image.LANCZOS)
 
 
 
325
 
326
+ # Create decorative frame for logo
327
+ frame_size = logo_size + 20
328
+ frame_pos = ((size - frame_size) // 2, (size - frame_size) // 2)
329
 
330
+ # Draw decorative frame
331
+ draw.rectangle([frame_pos[0], frame_pos[1],
332
+ frame_pos[0] + frame_size, frame_pos[1] + frame_size],
333
+ fill=(255, 255, 255), outline=primary_color, width=3)
334
+
335
+ # Paste logo
336
+ logo_pos = ((size - logo_size) // 2, (size - logo_size) // 2)
337
+ result.paste(logo, logo_pos)
 
338
 
339
+ return result
340
+
341
+ def is_finder_pattern(row, col, size):
342
+ """Check if position is in finder pattern area"""
343
+ return ((row < 9 and col < 9) or # Top-left
344
+ (row < 9 and col >= size - 8) or # Top-right
345
+ (row >= size - 8 and col < 9)) # Bottom-left
346
+
347
+ def is_timing_pattern(row, col):
348
+ """Check if position is in timing pattern"""
349
+ return row == 6 or col == 6
350
 
351
+ def create_basic_qr(data, size):
352
+ """Fallback basic QR code"""
353
+ qr = qrcode.QRCode(version=1, error_correction=ERROR_CORRECT_H, box_size=10, border=4)
354
+ qr.add_data(data)
355
+ qr.make(fit=True)
356
+ qr_img = qr.make_image(fill_color="black", back_color="white")
357
+ return qr_img.resize((size, size), Image.LANCZOS)
 
 
 
 
 
 
 
 
 
 
358
 
359
  def calculate_compatibility_score(qr_image):
360
  """Calculate QR code compatibility score"""
361
  try:
 
362
  gray = cv2.cvtColor(np.array(qr_image), cv2.COLOR_RGB2GRAY)
 
 
363
  min_val, max_val = np.min(gray), np.max(gray)
364
  contrast_ratio = max_val / max(min_val, 1)
365
 
 
366
  edges = cv2.Canny(gray, 50, 150)
367
  edge_ratio = np.sum(edges > 0) / edges.size
368
 
 
369
  blur = cv2.GaussianBlur(gray, (5, 5), 0)
370
  noise = np.std(gray - blur)
371
 
372
+ contrast_score = min(contrast_ratio / 8, 1) * 40
373
+ edge_score = min(edge_ratio * 80, 1) * 35
374
+ noise_score = max(0, 1 - noise / 40) * 25
 
375
 
376
  total_score = int(contrast_score + edge_score + noise_score)
377
 
378
  return {
379
  'overall': total_score,
380
  'contrast': int(contrast_score),
381
+ 'sharpness': int(edge_score),
382
  'noise_level': int(noise_score),
383
  'recommendations': get_recommendations(total_score)
384
  }
385
  except Exception as e:
386
  logger.error(f"Error calculating compatibility: {str(e)}")
387
+ return {'overall': 75, 'contrast': 75, 'sharpness': 75, 'noise_level': 75, 'recommendations': []}
388
 
389
  def get_recommendations(score):
390
+ """Get improvement recommendations"""
391
  recommendations = []
 
392
  if score < 70:
393
+ recommendations.append("Try using a simpler logo with higher contrast")
394
  if score < 60:
395
+ recommendations.append("Consider the 'Brand Integration' style for better scanning")
396
  if score < 50:
397
+ recommendations.append("Your logo might be too complex - try a simpler version")
 
398
  return recommendations
399
 
400
  def image_to_base64(image):
 
407
  @app.route('/', methods=['GET'])
408
  def home():
409
  return jsonify({
410
+ 'message': 'Enhanced Brand QR Generator is running',
411
  'status': 'healthy',
412
+ 'styles': ['brand_integration', 'logo_overlay', 'pattern_fusion', 'artistic_dots']
 
 
 
 
 
413
  })
414
 
415
  @app.route('/health', methods=['GET'])
416
  def health_check():
417
+ return jsonify({'status': 'healthy', 'message': 'Enhanced QR Service is running'})
418
 
419
  @app.route('/api/generate-artistic-qr', methods=['POST'])
420
  def generate_artistic_qr():
421
  try:
 
422
  if 'image' not in request.files or 'url' not in request.form:
423
  return jsonify({'error': 'Missing image or URL'}), 400
424
 
425
  file = request.files['image']
426
  url = request.form['url']
427
+ style = request.form.get('style', 'brand_integration')
428
+ size = int(request.form.get('size', 400))
429
 
430
+ if file.filename == '' or not allowed_file(file.filename):
431
+ return jsonify({'error': 'Invalid file'}), 400
 
432
 
433
+ if not url:
 
 
 
 
434
  return jsonify({'error': 'Invalid URL'}), 400
435
 
 
436
  try:
437
  image = Image.open(file.stream).convert('RGB')
438
+ except Exception:
439
  return jsonify({'error': 'Invalid image file'}), 400
440
 
441
+ # Generate brand-style QR code
442
+ artistic_qr = create_brand_style_qr(image, url, style, size)
 
 
443
  compatibility = calculate_compatibility_score(artistic_qr)
 
 
444
  qr_base64 = image_to_base64(artistic_qr)
445
 
446
+ # Standard QR for comparison
447
+ standard_qr = create_basic_qr(url, size)
448
  standard_base64 = image_to_base64(standard_qr)
449
 
450
  return jsonify({
 
460
  logger.error(f"Error in generate_artistic_qr: {str(e)}")
461
  return jsonify({'error': 'Internal server error'}), 500
462
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
463
  @app.route('/api/styles', methods=['GET'])
464
  def get_available_styles():
 
465
  styles = {
466
+ 'brand_integration': {
467
+ 'name': 'Brand Integration',
468
+ 'description': 'Logo in center with brand colors (like Pizza Hut, Starbucks)',
469
+ 'compatibility': 85
470
  },
471
+ 'logo_overlay': {
472
+ 'name': 'Logo Overlay',
473
+ 'description': 'Logo overlaid with stylized QR dots',
474
  'compatibility': 75
475
  },
476
+ 'pattern_fusion': {
477
+ 'name': 'Pattern Fusion',
478
+ 'description': 'Brand patterns fused with QR code',
479
+ 'compatibility': 70
480
+ },
481
+ 'artistic_dots': {
482
+ 'name': 'Artistic Dots',
483
+ 'description': 'Decorative dots and shapes with logo',
484
+ 'compatibility': 80
485
  }
486
  }
487
 
488
+ return jsonify({'success': True, 'styles': styles})
 
 
 
489
 
490
  if __name__ == '__main__':
491
  port = int(os.environ.get('PORT', 7860))
492
+ app.run(host='0.0.0.0', port=port, debug=False)