Rhaya03 commited on
Commit
504d68e
·
verified ·
1 Parent(s): 13c547c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +261 -175
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
9
  import re
10
 
11
  # --- 2. Load BOTH of your AI models ---
@@ -44,81 +44,91 @@ 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=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,66 +152,156 @@ def improved_filtering(boxes, character_results, plate_crop_shape, min_confidenc
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,126 +319,111 @@ def detect_license_plate(input_image):
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,19 +431,20 @@ def detect_license_plate(input_image):
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,
 
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
  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
  '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
  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
  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,