Rhaya03 commited on
Commit
19f2de7
·
verified ·
1 Parent(s): 504d68e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +175 -261
app.py CHANGED
@@ -5,7 +5,7 @@ from huggingface_hub import hf_hub_download
5
  import numpy as np
6
  import cv2
7
  import roboflow
8
- from collections import Counter, defaultdict
9
  import re
10
 
11
  # --- 2. Load BOTH of your AI models ---
@@ -44,91 +44,81 @@ def enhance_plate_image(plate_crop):
44
  gray = cv2.cvtColor(plate_crop, cv2.COLOR_RGB2GRAY)
45
 
46
  # Enhancement 1: Adaptive histogram equalization
47
- clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8))
48
  enhanced_gray = clahe.apply(gray)
49
  enhanced_crops.append(cv2.cvtColor(enhanced_gray, cv2.COLOR_GRAY2RGB))
50
 
51
  # Enhancement 2: Gaussian blur + unsharp mask
52
  blurred = cv2.GaussianBlur(gray, (3, 3), 0)
53
- unsharp = cv2.addWeighted(gray, 1.8, blurred, -0.8, 0)
54
  unsharp = np.clip(unsharp, 0, 255).astype(np.uint8)
55
  enhanced_crops.append(cv2.cvtColor(unsharp, cv2.COLOR_GRAY2RGB))
56
 
57
- # Enhancement 3: Contrast stretching
58
- min_val, max_val = np.percentile(gray, [2, 98])
59
- stretched = np.clip((gray - min_val) * 255 / (max_val - min_val), 0, 255).astype(np.uint8)
60
- enhanced_crops.append(cv2.cvtColor(stretched, cv2.COLOR_GRAY2RGB))
61
 
62
- # Enhancement 4: Gamma correction
63
- gamma_corrected = np.power(gray / 255.0, 0.7) * 255
64
- gamma_corrected = gamma_corrected.astype(np.uint8)
65
- enhanced_crops.append(cv2.cvtColor(gamma_corrected, cv2.COLOR_GRAY2RGB))
66
 
67
  return enhanced_crops
68
 
69
- def smart_character_correction(text, pattern_analysis=True):
70
  """
71
- Intelligent character correction based on license plate patterns
72
  """
73
- if not text or len(text) < 3:
74
- return text
75
 
76
  # Remove any spaces first
77
- text = text.replace(" ", "").upper()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
 
79
- # Philippine license plate patterns analysis
80
- def analyze_likely_pattern(s):
81
- """Determine if sequence should be letters or numbers based on position and context"""
82
- if len(s) < 6:
83
- return s
84
-
85
- # Common Philippine patterns: ABC123, ABC1234, 123ABC
86
- # Most common is 3 letters + 3-4 numbers
87
 
88
- corrected = list(s)
89
-
90
- # Pattern 1: First 3 characters are typically letters
91
- for i in range(min(3, len(corrected))):
92
- char = corrected[i]
93
  if char.isdigit():
94
- # Convert numbers that look like letters
95
- digit_to_letter = {'0': 'O', '1': 'I', '2': 'Z', '5': 'S', '6': 'G', '8': 'B'}
96
- if char in digit_to_letter:
97
- corrected[i] = digit_to_letter[char]
98
 
99
- # Pattern 2: Characters after position 3 are typically numbers
100
- for i in range(3, len(corrected)):
101
- char = corrected[i]
102
  if char.isalpha():
103
- # Convert letters that look like numbers
104
- letter_to_digit = {'O': '0', 'I': '1', 'L': '1', 'S': '5', 'G': '6', 'B': '8', 'Z': '2', 'T': '7'}
105
- if char in letter_to_digit:
106
- corrected[i] = letter_to_digit[char]
107
 
108
- return ''.join(corrected)
109
-
110
- # Apply pattern-based corrections if enabled
111
- if pattern_analysis and len(text) >= 6:
112
- text = analyze_likely_pattern(text)
113
-
114
- # Additional common OCR error corrections
115
- ocr_corrections = {
116
- # Numbers that might be misread as letters
117
- 'Q': '0', # Q often confused with O/0
118
- 'D': '0', # D sometimes looks like 0
119
- # Letters that might be misread as numbers
120
- 'A': 'A', # Keep A as is (could be confused with 4 but A is common in plates)
121
- }
122
-
123
- # Apply only high-confidence corrections
124
- for old_char, new_char in ocr_corrections.items():
125
- text = text.replace(old_char, new_char)
126
 
127
  return text
128
 
129
- def advanced_detection_filtering(boxes, character_results, plate_crop_shape, min_confidence=0.25):
130
  """
131
- Advanced filtering with clustering and statistical analysis
132
  """
133
  if len(boxes) == 0:
134
  return []
@@ -152,156 +142,66 @@ def advanced_detection_filtering(boxes, character_results, plate_crop_shape, min
152
  'width': float(x2 - x1),
153
  'height': float(y2 - y1),
154
  'center_x': float((x1 + x2) / 2),
155
- 'center_y': float((y1 + y2) / 2),
156
- 'area': float((x2 - x1) * (y2 - y1))
157
  })
158
 
159
  if len(detections) == 0:
160
  return []
161
 
162
- plate_height, plate_width = plate_crop_shape[:2]
 
 
 
 
 
 
163
 
164
- # Step 1: Focus on main character area (upper 75% of plate)
165
- main_area_threshold = plate_height * 0.75
166
- main_detections = [d for d in detections if d['center_y'] <= main_area_threshold]
 
167
 
168
- if len(main_detections) < 3: # If too few in main area, expand slightly
169
- main_area_threshold = plate_height * 0.85
170
- main_detections = [d for d in detections if d['center_y'] <= main_area_threshold]
171
 
172
- if len(main_detections) == 0:
173
- main_detections = detections
 
 
174
 
175
- # Step 2: Statistical filtering based on size and position
176
- heights = [d['height'] for d in main_detections]
177
- widths = [d['width'] for d in main_detections]
178
- y_positions = [d['center_y'] for d in main_detections]
179
- areas = [d['area'] for d in main_detections]
180
 
181
- # Calculate robust statistics (using percentiles to avoid outlier influence)
182
  median_height = np.median(heights)
183
  median_width = np.median(widths)
184
- median_y = np.median(y_positions)
185
- q75_area = np.percentile(areas, 75)
186
 
187
- # Step 3: Multi-criteria filtering
188
  filtered_detections = []
189
-
190
- for detection in main_detections:
191
- # Size consistency check
192
  height_ratio = detection['height'] / median_height
193
  width_ratio = detection['width'] / median_width
194
 
195
- # Vertical alignment check
196
- y_deviation = abs(detection['center_y'] - median_y)
197
- max_y_deviation = median_height * 0.5
198
-
199
- # Minimum size threshold (avoid tiny noise detections)
200
- min_size_threshold = plate_height * 0.12
201
 
202
- # Area-based filtering (avoid unusually small detections)
203
- area_threshold = q75_area * 0.3
204
 
205
- # Apply all criteria
206
- if (0.4 <= height_ratio <= 2.5 and
207
- 0.3 <= width_ratio <= 3.0 and
208
- y_deviation <= max_y_deviation and
209
- detection['height'] >= min_size_threshold and
210
- detection['area'] >= area_threshold):
211
-
212
  filtered_detections.append(detection)
213
 
214
- # Step 4: Remove duplicate detections (same character in nearby positions)
215
- final_detections = []
216
- used_positions = []
217
-
218
- # Sort by confidence first
219
- filtered_detections.sort(key=lambda x: x['conf'], reverse=True)
220
-
221
- for detection in filtered_detections:
222
- # Check if this position is too close to already used positions
223
- too_close = False
224
- for used_x in used_positions:
225
- if abs(detection['center_x'] - used_x) < median_width * 0.8:
226
- too_close = True
227
- break
228
-
229
- if not too_close:
230
- final_detections.append(detection)
231
- used_positions.append(detection['center_x'])
232
-
233
- return final_detections
234
-
235
- def ensemble_character_voting(all_detections, plate_width, confidence_threshold=0.35):
236
- """
237
- Advanced ensemble voting with spatial clustering and confidence weighting
238
- """
239
- if not all_detections:
240
- return []
241
-
242
- # Step 1: Spatial clustering - group detections by x-position
243
- position_groups = defaultdict(list)
244
- cluster_tolerance = plate_width * 0.15 # 15% of plate width
245
-
246
- for detection in all_detections:
247
- x_pos = detection['center_x']
248
-
249
- # Find existing cluster or create new one
250
- assigned = False
251
- for cluster_center in list(position_groups.keys()):
252
- if abs(x_pos - cluster_center) <= cluster_tolerance:
253
- position_groups[cluster_center].append(detection)
254
- assigned = True
255
- break
256
-
257
- if not assigned:
258
- position_groups[x_pos].append(detection)
259
-
260
- # Step 2: For each spatial cluster, determine best character
261
- final_characters = []
262
-
263
- for cluster_center, cluster_detections in position_groups.items():
264
- # Group by character within cluster
265
- char_groups = defaultdict(list)
266
- for det in cluster_detections:
267
- char_groups[det['char']].append(det)
268
-
269
- # Calculate weighted score for each character
270
- best_char = None
271
- best_score = 0
272
- best_detection = None
273
-
274
- for char, char_detections in char_groups.items():
275
- # Calculate score: weighted average of confidence + occurrence bonus
276
- confidences = [d['conf'] for d in char_detections]
277
- avg_confidence = np.mean(confidences)
278
- max_confidence = max(confidences)
279
- occurrence_bonus = min(len(char_detections) * 0.1, 0.3) # Up to 30% bonus
280
-
281
- # Final score combines average confidence, max confidence, and occurrence
282
- score = (avg_confidence * 0.5 + max_confidence * 0.4 + occurrence_bonus * 0.1)
283
-
284
- if score > best_score and avg_confidence > confidence_threshold:
285
- best_score = score
286
- best_char = char
287
- # Use the detection with highest confidence as representative
288
- best_detection = max(char_detections, key=lambda x: x['conf'])
289
-
290
- if best_char and best_detection:
291
- best_detection['final_char'] = best_char
292
- best_detection['final_score'] = best_score
293
- best_detection['cluster_size'] = len(cluster_detections)
294
- final_characters.append(best_detection)
295
-
296
- # Step 3: Sort by x-position for final ordering
297
- final_characters.sort(key=lambda x: x['center_x'])
298
-
299
- return final_characters
300
 
301
  # --- 4. Enhanced main prediction function ---
302
  def detect_license_plate(input_image):
303
  """
304
- Enhanced version with improved filtering and ensemble voting
305
  """
306
  print("New image received. Starting enhanced 2-stage pipeline...")
307
  output_image = input_image.copy()
@@ -319,111 +219,126 @@ def detect_license_plate(input_image):
319
  plate_box['x'] + plate_box['width'] / 2,
320
  plate_box['y'] + plate_box['height'] / 2]]
321
 
322
- # Optimized padding - minimal vertical to avoid extra text
323
- h_padding = 10
324
- v_padding = 2
325
  y1 = max(0, y1 - v_padding)
326
  x1 = max(0, x1 - h_padding)
327
  y2 = min(input_image.shape[0], y2 + v_padding)
328
  x2 = min(input_image.shape[1], x2 + h_padding)
329
 
330
  plate_crop = input_image[y1:y2, x1:x2]
331
- plate_height, plate_width = plate_crop.shape[:2]
332
 
333
- # Focus on main number area (top 75% of plate)
334
- main_number_crop = plate_crop[:int(plate_height * 0.75), :]
 
 
335
 
336
  # --- STAGE 2: Multi-enhancement character detection ---
337
  enhanced_crops = enhance_plate_image(main_number_crop)
338
 
339
  all_detections = []
 
340
 
341
- # Process each enhanced version with different confidence thresholds
342
- confidence_levels = [0.25, 0.3, 0.35, 0.25] # Different thresholds for each enhancement
343
-
344
- for i, (enhanced_crop, conf_threshold) in enumerate(zip(enhanced_crops, confidence_levels)):
345
  try:
346
- character_results = character_model(enhanced_crop, conf=conf_threshold, iou=0.3)
347
 
348
  if character_results and hasattr(character_results[0], 'boxes') and len(character_results[0].boxes) > 0:
349
  boxes = character_results[0].boxes.cpu().numpy()
350
- filtered_detections = advanced_detection_filtering(
351
- boxes, character_results, main_number_crop.shape, min_confidence=conf_threshold
352
- )
353
 
354
- print(f"Enhancement {i}: {len(boxes)} raw -> {len(filtered_detections)} filtered")
355
 
356
  for detection in filtered_detections:
357
- detection['enhancement_method'] = i
358
- detection['enhancement_conf'] = conf_threshold
359
  all_detections.append(detection)
360
 
 
 
 
 
 
 
 
361
  except Exception as e:
362
  print(f"Error processing enhancement {i}: {e}")
363
  continue
364
 
365
- # --- STAGE 3: Advanced ensemble voting ---
366
- final_detections = ensemble_character_voting(all_detections, plate_width, confidence_threshold=0.3)
367
 
368
- print(f"Ensemble voting: {len(all_detections)} total -> {len(final_detections)} final")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
369
 
370
- # --- STAGE 4: Generate and post-process text ---
371
- if final_detections:
372
- # Sort by x position
373
- final_detections.sort(key=lambda x: x['center_x'])
374
- raw_text = "".join([d['final_char'] for d in final_detections])
375
-
376
- # Apply smart character correction
377
- corrected_text = smart_character_correction(raw_text, pattern_analysis=True)
378
-
379
- # Additional validation - remove obviously wrong characters
380
- if len(corrected_text) > 8: # If too long, might have false positives
381
- # Keep only the most confident detections
382
- final_detections = sorted(final_detections, key=lambda x: x['final_score'], reverse=True)[:7]
383
- final_detections.sort(key=lambda x: x['center_x'])
384
- raw_text = "".join([d['final_char'] for d in final_detections])
385
- corrected_text = smart_character_correction(raw_text, pattern_analysis=True)
386
- else:
387
- raw_text = ""
388
- corrected_text = ""
389
-
390
- # --- STAGE 5: Draw results ---
391
  # Draw the main plate box
392
  cv2.rectangle(output_image, (x1, y1), (x2, y2), (0, 0, 255), 2)
393
- cv2.putText(output_image, f"Plate: {plate_box['confidence']:.1f}%",
394
  (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2)
395
 
396
- # Draw detection area boundary
397
- main_area_y = y1 + int(plate_height * 0.75)
398
- cv2.line(output_image, (x1, main_area_y), (x2, main_area_y), (255, 255, 0), 2)
399
- cv2.putText(output_image, "Detection Area", (x1, main_area_y - 5),
400
- cv2.FONT_HERSHEY_SIMPLEX, 0.4, (255, 255, 0), 1)
401
-
402
- # Draw character detections
403
- for i, detection in enumerate(final_detections):
404
  abs_x1 = x1 + int(detection['x1'])
405
  abs_y1 = y1 + int(detection['y1'])
406
  abs_x2 = x1 + int(detection['x2'])
407
  abs_y2 = y1 + int(detection['y2'])
408
 
409
- # Color code by confidence
410
- color = (0, 255, 0) if detection['final_score'] > 0.7 else (0, 255, 255)
411
-
412
- cv2.rectangle(output_image, (abs_x1, abs_y1), (abs_x2, abs_y2), color, 2)
413
- cv2.putText(output_image, f"{detection['final_char']}",
414
- (abs_x1, abs_y1 - 15), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2)
415
- cv2.putText(output_image, f"{detection['final_score']:.2f}",
416
- (abs_x1, abs_y1 - 3), cv2.FONT_HERSHEY_SIMPLEX, 0.4, color, 1)
417
-
418
- # Prepare result text
419
- if raw_text != corrected_text and corrected_text:
420
- result_text = f"Detected: {raw_text}\nCorrected: {corrected_text}\nConfidence: {len(final_detections)} chars"
421
- elif corrected_text:
422
- result_text = f"Result: {corrected_text}\nConfidence: {len(final_detections)} characters detected"
423
- else:
424
- result_text = "No characters detected with sufficient confidence"
425
-
426
- print(f"Final result: {result_text}")
 
427
 
428
  return output_image, result_text
429
 
@@ -431,20 +346,19 @@ def detect_license_plate(input_image):
431
  with gr.Blocks() as demo:
432
  gr.Markdown("# Enhanced High-Accuracy License Plate Detector")
433
  gr.Markdown("""
434
- **Improved Features:**
435
- - Advanced statistical filtering with spatial clustering
436
- - Smart character correction based on license plate patterns
437
- - Enhanced ensemble voting with confidence weighting
438
- - Optimized detection area focusing
439
- - Multi-level confidence thresholds
440
  """)
441
 
442
  with gr.Row():
443
  image_input = gr.Image(type="numpy", label="Upload License Plate Image")
444
  image_output = gr.Image(type="numpy", label="Detection Results")
445
 
446
- text_output = gr.Textbox(label="Detected License Plate", lines=3)
447
- predict_button = gr.Button(value="Detect License Plate", variant="primary")
448
 
449
  predict_button.click(
450
  fn=detect_license_plate,
 
5
  import numpy as np
6
  import cv2
7
  import roboflow
8
+ from collections import Counter
9
  import re
10
 
11
  # --- 2. Load BOTH of your AI models ---
 
44
  gray = cv2.cvtColor(plate_crop, cv2.COLOR_RGB2GRAY)
45
 
46
  # Enhancement 1: Adaptive histogram equalization
47
+ clahe = cv2.createCLAHE(clipLimit=3.0, tileGridSize=(8,8))
48
  enhanced_gray = clahe.apply(gray)
49
  enhanced_crops.append(cv2.cvtColor(enhanced_gray, cv2.COLOR_GRAY2RGB))
50
 
51
  # Enhancement 2: Gaussian blur + unsharp mask
52
  blurred = cv2.GaussianBlur(gray, (3, 3), 0)
53
+ unsharp = cv2.addWeighted(gray, 1.5, blurred, -0.5, 0)
54
  unsharp = np.clip(unsharp, 0, 255).astype(np.uint8)
55
  enhanced_crops.append(cv2.cvtColor(unsharp, cv2.COLOR_GRAY2RGB))
56
 
57
+ # Enhancement 3: Morphological operations
58
+ kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (2, 2))
59
+ morph = cv2.morphologyEx(gray, cv2.MORPH_CLOSE, kernel)
60
+ enhanced_crops.append(cv2.cvtColor(morph, cv2.COLOR_GRAY2RGB))
61
 
62
+ # Enhancement 4: Bilateral filter
63
+ bilateral = cv2.bilateralFilter(gray, 9, 75, 75)
64
+ enhanced_crops.append(cv2.cvtColor(bilateral, cv2.COLOR_GRAY2RGB))
 
65
 
66
  return enhanced_crops
67
 
68
+ def post_process_text(raw_text):
69
  """
70
+ Apply license plate specific formatting and corrections
71
  """
72
+ if not raw_text:
73
+ return raw_text
74
 
75
  # Remove any spaces first
76
+ text = raw_text.replace(" ", "")
77
+
78
+ # Common character corrections for license plates
79
+ corrections = {
80
+ '0': 'O', # In letter context
81
+ 'O': '0', # In number context
82
+ 'I': '1',
83
+ '1': 'I',
84
+ 'S': '5',
85
+ '5': 'S',
86
+ 'Z': '2',
87
+ 'B': '8',
88
+ '8': 'B',
89
+ 'G': '6',
90
+ '6': 'G'
91
+ }
92
 
93
+ # For Philippine plates, common format is 3 letters + 3 numbers (like NOV706)
94
+ if len(text) >= 6:
95
+ corrected_chars = list(text)
 
 
 
 
 
96
 
97
+ # First 3 should typically be letters
98
+ for i in range(min(3, len(corrected_chars))):
99
+ char = corrected_chars[i]
 
 
100
  if char.isdigit():
101
+ # Convert common digit misreads to letters
102
+ if char in ['0', '1', '5', '8']:
103
+ letter_map = {'0': 'O', '1': 'I', '5': 'S', '8': 'B'}
104
+ corrected_chars[i] = letter_map.get(char, char)
105
 
106
+ # Last 3 should typically be numbers
107
+ for i in range(3, min(6, len(corrected_chars))):
108
+ char = corrected_chars[i]
109
  if char.isalpha():
110
+ # Convert common letter misreads to numbers
111
+ if char in ['O', 'I', 'S', 'B', 'G', 'Z']:
112
+ number_map = {'O': '0', 'I': '1', 'S': '5', 'B': '8', 'G': '6', 'Z': '2'}
113
+ corrected_chars[i] = number_map.get(char, char)
114
 
115
+ text = ''.join(corrected_chars)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
 
117
  return text
118
 
119
+ def improved_filtering(boxes, character_results, plate_crop_shape, min_confidence=0.3):
120
  """
121
+ Enhanced filtering focusing on main license plate number only
122
  """
123
  if len(boxes) == 0:
124
  return []
 
142
  'width': float(x2 - x1),
143
  'height': float(y2 - y1),
144
  'center_x': float((x1 + x2) / 2),
145
+ 'center_y': float((y1 + y2) / 2)
 
146
  })
147
 
148
  if len(detections) == 0:
149
  return []
150
 
151
+ # MAIN IMPROVEMENT: Focus on the upper portion of the plate
152
+ # Most license plates have the main number in the top 60% of the plate
153
+ plate_height = plate_crop_shape[0]
154
+ upper_threshold = plate_height * 0.70 # Only consider top 65% of plate
155
+
156
+ # Filter out detections in lower portion (subsidiary text area)
157
+ upper_detections = [d for d in detections if d['center_y'] <= upper_threshold]
158
 
159
+ if len(upper_detections) == 0:
160
+ # Fallback: if no detections in upper area, use all but be more selective
161
+ upper_detections = detections
162
+ print("Warning: No detections in upper area, using all detections")
163
 
164
+ print(f"Filtered to upper area: {len(upper_detections)}/{len(detections)} detections")
 
 
165
 
166
+ # Calculate statistics for filtering (now only on upper detections)
167
+ heights = [d['height'] for d in upper_detections]
168
+ widths = [d['width'] for d in upper_detections]
169
+ y_centers = [d['center_y'] for d in upper_detections]
170
 
171
+ if len(heights) == 0:
172
+ return []
 
 
 
173
 
 
174
  median_height = np.median(heights)
175
  median_width = np.median(widths)
176
+ median_y_center = np.median(y_centers)
 
177
 
178
+ # More aggressive filtering for main plate numbers
179
  filtered_detections = []
180
+ for detection in upper_detections:
181
+ # Size filtering (tighter for main numbers)
 
182
  height_ratio = detection['height'] / median_height
183
  width_ratio = detection['width'] / median_width
184
 
185
+ # Alignment filtering (tighter)
186
+ y_deviation = abs(detection['center_y'] - median_y_center)
187
+ max_y_deviation = median_height * 0.4 # Reduced from 0.6 to 0.4
 
 
 
188
 
189
+ # Height-based filtering: main numbers are usually larger
190
+ min_height_threshold = plate_height * 0.15 # At least 15% of plate height
191
 
192
+ # Keep detection if it passes all criteria
193
+ if (0.5 <= height_ratio <= 2.0 and # Tighter height range
194
+ 0.4 <= width_ratio <= 2.5 and # Tighter width range
195
+ y_deviation <= max_y_deviation and # Better alignment
196
+ detection['height'] >= min_height_threshold): # Minimum size
 
 
197
  filtered_detections.append(detection)
198
 
199
+ return filtered_detections
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
200
 
201
  # --- 4. Enhanced main prediction function ---
202
  def detect_license_plate(input_image):
203
  """
204
+ Enhanced version with multi-enhancement processing and ensemble voting
205
  """
206
  print("New image received. Starting enhanced 2-stage pipeline...")
207
  output_image = input_image.copy()
 
219
  plate_box['x'] + plate_box['width'] / 2,
220
  plate_box['y'] + plate_box['height'] / 2]]
221
 
222
+ # Add some padding to the plate crop, but reduce vertical padding to avoid extra text
223
+ h_padding = 8 # Horizontal padding
224
+ v_padding = 3 # Minimal vertical padding to avoid bottom text
225
  y1 = max(0, y1 - v_padding)
226
  x1 = max(0, x1 - h_padding)
227
  y2 = min(input_image.shape[0], y2 + v_padding)
228
  x2 = min(input_image.shape[1], x2 + h_padding)
229
 
230
  plate_crop = input_image[y1:y2, x1:x2]
 
231
 
232
+ # Crop to focus on upper portion where main numbers are located
233
+ plate_height = plate_crop.shape[0]
234
+ # Keep top 70% of the plate to exclude bottom text area
235
+ main_number_crop = plate_crop[:int(plate_height * 0.7), :]
236
 
237
  # --- STAGE 2: Multi-enhancement character detection ---
238
  enhanced_crops = enhance_plate_image(main_number_crop)
239
 
240
  all_detections = []
241
+ character_votes = {}
242
 
243
+ # Process each enhanced version
244
+ for i, enhanced_crop in enumerate(enhanced_crops):
 
 
245
  try:
246
+ character_results = character_model(enhanced_crop, conf=0.3, iou=0.4)
247
 
248
  if character_results and hasattr(character_results[0], 'boxes') and len(character_results[0].boxes) > 0:
249
  boxes = character_results[0].boxes.cpu().numpy()
250
+ filtered_detections = improved_filtering(boxes, character_results,
251
+ main_number_crop.shape, min_confidence=0.3)
 
252
 
253
+ print(f"Enhancement {i}: {len(boxes)} raw -> {len(filtered_detections)} filtered detections")
254
 
255
  for detection in filtered_detections:
256
+ # Add enhancement method info
257
+ detection['enhancement'] = i
258
  all_detections.append(detection)
259
 
260
+ # Collect votes for ensemble
261
+ x_pos = int(detection['center_x'] / 8) * 8 # Tighter grouping
262
+ key = f"x{x_pos}"
263
+ if key not in character_votes:
264
+ character_votes[key] = []
265
+ character_votes[key].append((detection['char'], detection['conf']))
266
+
267
  except Exception as e:
268
  print(f"Error processing enhancement {i}: {e}")
269
  continue
270
 
271
+ # --- STAGE 3: Ensemble voting and final selection ---
272
+ final_detections = []
273
 
274
+ if character_votes:
275
+ for x_key in sorted(character_votes.keys()):
276
+ votes = character_votes[x_key]
277
+
278
+ # Weight votes by confidence and count
279
+ char_scores = {}
280
+ for char, conf in votes:
281
+ if char not in char_scores:
282
+ char_scores[char] = []
283
+ char_scores[char].append(conf)
284
+
285
+ # Calculate weighted scores
286
+ best_char = None
287
+ best_score = 0
288
+
289
+ for char, confs in char_scores.items():
290
+ # Score = average confidence * count weight
291
+ avg_conf = np.mean(confs)
292
+ count_weight = min(len(confs) / len(enhanced_crops), 1.0)
293
+ score = avg_conf * (0.7 + 0.3 * count_weight)
294
+
295
+ if score > best_score:
296
+ best_score = score
297
+ best_char = char
298
+
299
+ if best_char and best_score > 0.3:
300
+ # Find representative detection for drawing
301
+ x_pos = int(x_key[1:])
302
+ representative = min([d for d in all_detections if abs(d['center_x'] - x_pos) < 15],
303
+ key=lambda x: abs(x['center_x'] - x_pos), default=None)
304
+
305
+ if representative:
306
+ representative['final_char'] = best_char
307
+ representative['final_conf'] = best_score
308
+ final_detections.append(representative)
309
 
310
+ # --- STAGE 4: Draw results and generate text ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
311
  # Draw the main plate box
312
  cv2.rectangle(output_image, (x1, y1), (x2, y2), (0, 0, 255), 2)
313
+ cv2.putText(output_image, f"Plate Conf: {plate_box['confidence']:.2f}",
314
  (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2)
315
 
316
+ # Draw character boxes (adjust coordinates back to main number crop area)
317
+ for detection in final_detections:
 
 
 
 
 
 
318
  abs_x1 = x1 + int(detection['x1'])
319
  abs_y1 = y1 + int(detection['y1'])
320
  abs_x2 = x1 + int(detection['x2'])
321
  abs_y2 = y1 + int(detection['y2'])
322
 
323
+ cv2.rectangle(output_image, (abs_x1, abs_y1), (abs_x2, abs_y2), (0, 255, 0), 2)
324
+ cv2.putText(output_image, f"{detection['final_char']} {detection['final_conf']:.2f}",
325
+ (abs_x1, abs_y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1)
326
+
327
+ # Draw a line to show the detection area boundary
328
+ main_area_y = y1 + int(plate_height * 0.7)
329
+ cv2.line(output_image, (x1, main_area_y), (x2, main_area_y), (255, 255, 0), 2)
330
+
331
+ # Sort by x position and create final text
332
+ final_detections.sort(key=lambda x: x['center_x'])
333
+ raw_text = "".join([d['final_char'] for d in final_detections])
334
+
335
+ # Apply post-processing
336
+ final_text = post_process_text(raw_text)
337
+
338
+ result_text = f"Raw: {raw_text}\nProcessed: {final_text}" if raw_text != final_text else final_text
339
+
340
+ print(f"Prediction complete. Final result: {result_text}")
341
+ print(f"Used {len(final_detections)} characters from {len(all_detections)} total detections")
342
 
343
  return output_image, result_text
344
 
 
346
  with gr.Blocks() as demo:
347
  gr.Markdown("# Enhanced High-Accuracy License Plate Detector")
348
  gr.Markdown("""
349
+ This system uses an advanced 2-stage AI pipeline with:
350
+ - Multiple image enhancement techniques
351
+ - Ensemble voting across different processed versions
352
+ - Smart filtering and post-processing
353
+ - Common license plate character corrections
 
354
  """)
355
 
356
  with gr.Row():
357
  image_input = gr.Image(type="numpy", label="Upload License Plate Image")
358
  image_output = gr.Image(type="numpy", label="Detection Results")
359
 
360
+ text_output = gr.Textbox(label="Detected Characters", lines=3)
361
+ predict_button = gr.Button(value="Detect Characters", variant="primary")
362
 
363
  predict_button.click(
364
  fn=detect_license_plate,