Spaces:
Sleeping
Sleeping
Update app.py
Browse files
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=
|
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.
|
54 |
unsharp = np.clip(unsharp, 0, 255).astype(np.uint8)
|
55 |
enhanced_crops.append(cv2.cvtColor(unsharp, cv2.COLOR_GRAY2RGB))
|
56 |
|
57 |
-
# Enhancement 3:
|
58 |
-
|
59 |
-
|
60 |
-
enhanced_crops.append(cv2.cvtColor(
|
61 |
|
62 |
-
# Enhancement 4:
|
63 |
-
|
64 |
-
|
|
|
65 |
|
66 |
return enhanced_crops
|
67 |
|
68 |
-
def
|
69 |
"""
|
70 |
-
|
71 |
"""
|
72 |
-
if not
|
73 |
-
return
|
74 |
|
75 |
# Remove any spaces first
|
76 |
-
text =
|
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 |
-
#
|
94 |
-
|
95 |
-
|
|
|
|
|
|
|
|
|
|
|
96 |
|
97 |
-
|
98 |
-
|
99 |
-
|
|
|
|
|
100 |
if char.isdigit():
|
101 |
-
# Convert
|
102 |
-
|
103 |
-
|
104 |
-
|
105 |
|
106 |
-
#
|
107 |
-
for i in range(3,
|
108 |
-
char =
|
109 |
if char.isalpha():
|
110 |
-
# Convert
|
111 |
-
|
112 |
-
|
113 |
-
|
114 |
|
115 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
116 |
|
117 |
return text
|
118 |
|
119 |
-
def
|
120 |
"""
|
121 |
-
|
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 |
-
|
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 |
-
|
160 |
-
|
161 |
-
|
162 |
-
print("Warning: No detections in upper area, using all detections")
|
163 |
|
164 |
-
|
|
|
|
|
165 |
|
166 |
-
|
167 |
-
|
168 |
-
widths = [d['width'] for d in upper_detections]
|
169 |
-
y_centers = [d['center_y'] for d in upper_detections]
|
170 |
|
171 |
-
|
172 |
-
|
|
|
|
|
|
|
173 |
|
|
|
174 |
median_height = np.median(heights)
|
175 |
median_width = np.median(widths)
|
176 |
-
|
|
|
177 |
|
178 |
-
#
|
179 |
filtered_detections = []
|
180 |
-
|
181 |
-
|
|
|
182 |
height_ratio = detection['height'] / median_height
|
183 |
width_ratio = detection['width'] / median_width
|
184 |
|
185 |
-
#
|
186 |
-
y_deviation = abs(detection['center_y'] -
|
187 |
-
max_y_deviation = median_height * 0.
|
|
|
|
|
|
|
188 |
|
189 |
-
#
|
190 |
-
|
191 |
|
192 |
-
#
|
193 |
-
if (0.
|
194 |
-
0.
|
195 |
-
y_deviation <= max_y_deviation and
|
196 |
-
detection['height'] >=
|
|
|
|
|
197 |
filtered_detections.append(detection)
|
198 |
|
199 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
200 |
|
201 |
# --- 4. Enhanced main prediction function ---
|
202 |
def detect_license_plate(input_image):
|
203 |
"""
|
204 |
-
Enhanced version with
|
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 |
-
#
|
223 |
-
h_padding =
|
224 |
-
v_padding =
|
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 |
-
#
|
233 |
-
|
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 |
-
|
|
|
|
|
245 |
try:
|
246 |
-
character_results = character_model(enhanced_crop, conf=
|
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 =
|
251 |
-
|
|
|
252 |
|
253 |
-
print(f"Enhancement {i}: {len(boxes)} raw -> {len(filtered_detections)} filtered
|
254 |
|
255 |
for detection in filtered_detections:
|
256 |
-
|
257 |
-
detection['
|
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:
|
272 |
-
final_detections =
|
273 |
|
274 |
-
|
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:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
314 |
(x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2)
|
315 |
|
316 |
-
# Draw
|
317 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
324 |
-
|
325 |
-
|
326 |
-
|
327 |
-
|
328 |
-
|
329 |
-
|
330 |
-
|
331 |
-
|
332 |
-
|
333 |
-
raw_text
|
334 |
-
|
335 |
-
|
336 |
-
|
337 |
-
|
338 |
-
|
339 |
-
|
340 |
-
print(f"
|
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 |
-
|
350 |
-
-
|
351 |
-
-
|
352 |
-
-
|
353 |
-
-
|
|
|
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
|
361 |
-
predict_button = gr.Button(value="Detect
|
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,
|