Spaces:
Runtime error
Runtime error
Create app.py
Browse files
app.py
ADDED
@@ -0,0 +1,385 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#===========================================================================#
|
2 |
+
#===========================================================================#
|
3 |
+
# SETUP INSTALLATIONS
|
4 |
+
#===========================================================================#
|
5 |
+
#===========================================================================#
|
6 |
+
import os
|
7 |
+
import sys
|
8 |
+
|
9 |
+
def install_packages():
|
10 |
+
# Atualizar pip
|
11 |
+
os.system(f"pip install --upgrade pip")
|
12 |
+
|
13 |
+
# Instalar pacotes necessários
|
14 |
+
packages = [
|
15 |
+
"opencv-python-headless==4.10.0.82",
|
16 |
+
"ultralytics==8.3",
|
17 |
+
"telethon==1.37.0",
|
18 |
+
"cryptography==43.0.3",
|
19 |
+
"nest_asyncio",
|
20 |
+
"torch==2.5.0 torchvision==0.20.0 torchaudio==2.5.0 --index-url https://download.pytorch.org/whl/cpu",
|
21 |
+
"paddlepaddle==2.6.2 -f https://paddlepaddle.org.cn/whl/mkl/avx/stable.html",
|
22 |
+
"paddleocr==2.9.1",
|
23 |
+
"prettytable==3.12",
|
24 |
+
"gradio==5.6",
|
25 |
+
]
|
26 |
+
|
27 |
+
for package in packages:
|
28 |
+
print(f"Installing {package}...")
|
29 |
+
os.system(f"pip install {package}")
|
30 |
+
|
31 |
+
print("All packages installed successfully.")
|
32 |
+
|
33 |
+
install_packages()
|
34 |
+
|
35 |
+
#===========================================================================#
|
36 |
+
#===========================================================================#
|
37 |
+
# PLAY THE CLASS
|
38 |
+
#===========================================================================#
|
39 |
+
#===========================================================================#
|
40 |
+
import gradio as gr
|
41 |
+
import numpy as np
|
42 |
+
import cv2
|
43 |
+
from collections import deque, OrderedDict, defaultdict
|
44 |
+
from ultralytics import YOLO
|
45 |
+
from paddleocr import PaddleOCR
|
46 |
+
import asyncio
|
47 |
+
import threading
|
48 |
+
from telethon import TelegramClient
|
49 |
+
from cryptography.fernet import Fernet
|
50 |
+
import json
|
51 |
+
import nest_asyncio
|
52 |
+
from prettytable import PrettyTable
|
53 |
+
|
54 |
+
# Aplicar nest_asyncio para permitir loops de eventos aninhados
|
55 |
+
nest_asyncio.apply()
|
56 |
+
|
57 |
+
# Função para obter o caminho de recursos
|
58 |
+
def resource_path(relative_path):
|
59 |
+
return os.path.join(os.getcwd(), relative_path)
|
60 |
+
|
61 |
+
# Classe adaptada para processar frames individuais
|
62 |
+
class LicensePlateProcessor:
|
63 |
+
def __init__(self):
|
64 |
+
# Carregar modelo YOLO
|
65 |
+
model_path = resource_path('best.pt')
|
66 |
+
self.model = YOLO(model_path, task='detect')
|
67 |
+
|
68 |
+
# Carregar PaddleOCR
|
69 |
+
paddleocr_model_dir = resource_path('paddleocr_models')
|
70 |
+
self.ocr = PaddleOCR(
|
71 |
+
use_angle_cls=True,
|
72 |
+
use_gpu=False,
|
73 |
+
lang='en',
|
74 |
+
det_algorithm='DB',
|
75 |
+
rec_algorithm='CRNN',
|
76 |
+
show_log=False,
|
77 |
+
rec_model_dir=os.path.join(paddleocr_model_dir, 'en_PP-OCRv3_rec_infer'),
|
78 |
+
det_model_dir=os.path.join(paddleocr_model_dir, 'en_PP-OCRv3_det_infer'),
|
79 |
+
cls_model_dir=os.path.join(paddleocr_model_dir, 'ch_ppocr_mobile_v2.0_cls_infer')
|
80 |
+
)
|
81 |
+
|
82 |
+
# Carregar dados encriptados
|
83 |
+
self.load_encrypted_data()
|
84 |
+
|
85 |
+
# Inicializar TelegramClient
|
86 |
+
self.telegram_client = TelegramClient(self.session_name, self.api_id, self.api_hash)
|
87 |
+
self.telegram_client.start()
|
88 |
+
|
89 |
+
# Memória de placas
|
90 |
+
self.plates_memory = deque(maxlen=500)
|
91 |
+
self.last_sixteen_plates = OrderedDict()
|
92 |
+
|
93 |
+
# Filas para placas aguardando resposta
|
94 |
+
self.waiting_plates = {}
|
95 |
+
|
96 |
+
# Iniciar loop asyncio em uma thread separada
|
97 |
+
self.loop = asyncio.get_event_loop()
|
98 |
+
self.loop_thread = threading.Thread(target=self.start_loop, daemon=True)
|
99 |
+
self.loop_thread.start()
|
100 |
+
|
101 |
+
# Iniciar tarefa assíncrona para verificar respostas
|
102 |
+
asyncio.run_coroutine_threadsafe(self.check_responses(), self.loop)
|
103 |
+
|
104 |
+
def start_loop(self):
|
105 |
+
"""Inicia o loop de eventos asyncio."""
|
106 |
+
asyncio.set_event_loop(self.loop)
|
107 |
+
self.loop.run_forever()
|
108 |
+
|
109 |
+
def load_encrypted_data(self):
|
110 |
+
"""Carrega e decripta os dados sensíveis."""
|
111 |
+
encrypted_data_path = resource_path('SECRET_DATA.enc')
|
112 |
+
decrypt_key_path = resource_path('decrypt_key.txt')
|
113 |
+
|
114 |
+
with open(encrypted_data_path, "rb") as f:
|
115 |
+
data_encrypted = f.read()
|
116 |
+
with open(decrypt_key_path, "r") as key_file:
|
117 |
+
key_str = key_file.read().strip()
|
118 |
+
key = key_str.encode('utf-8')
|
119 |
+
cipher = Fernet(key)
|
120 |
+
data_decrypted = cipher.decrypt(data_encrypted)
|
121 |
+
config = json.loads(data_decrypted.decode())
|
122 |
+
self.api_id = config["api_id"]
|
123 |
+
self.api_hash = config["api_hash"]
|
124 |
+
self.phone_number = config["phone_number"]
|
125 |
+
self.session_name = resource_path(config["session_name"])
|
126 |
+
|
127 |
+
def has_seven(self, plate_text):
|
128 |
+
"""Verifica o status de uma placa."""
|
129 |
+
# Verificar se a placa está na memória
|
130 |
+
for item in self.plates_memory:
|
131 |
+
if item['plate'] == plate_text:
|
132 |
+
return item['has_seven']
|
133 |
+
# Verificar se está aguardando resposta
|
134 |
+
if plate_text in self.waiting_plates:
|
135 |
+
return self.waiting_plates[plate_text]
|
136 |
+
else:
|
137 |
+
# Enviar placa para o bot do Telegram
|
138 |
+
self.waiting_plates[plate_text] = 'Waiting'
|
139 |
+
asyncio.run_coroutine_threadsafe(self.send_plate(plate_text), self.loop)
|
140 |
+
return 'Waiting'
|
141 |
+
|
142 |
+
async def send_plate(self, plate_text):
|
143 |
+
"""Envia a placa para o bot do Telegram."""
|
144 |
+
chat_identifier = '@LT_BUSCABOT'
|
145 |
+
try:
|
146 |
+
await self.telegram_client.connect()
|
147 |
+
await self.telegram_client.send_message(chat_identifier, plate_text)
|
148 |
+
print(f"Enviado para Telegram: {plate_text}")
|
149 |
+
except Exception as e:
|
150 |
+
print(f"Erro ao enviar placa {plate_text}: {e}")
|
151 |
+
|
152 |
+
async def check_responses(self):
|
153 |
+
"""Verifica respostas do bot do Telegram."""
|
154 |
+
while True:
|
155 |
+
if not self.waiting_plates:
|
156 |
+
await asyncio.sleep(1)
|
157 |
+
continue
|
158 |
+
chat_identifier = '@LT_BUSCABOT'
|
159 |
+
limit = 20
|
160 |
+
try:
|
161 |
+
await self.telegram_client.connect()
|
162 |
+
messages = await self.telegram_client.get_messages(chat_identifier, limit=limit)
|
163 |
+
# print(messages)
|
164 |
+
for message in messages:
|
165 |
+
text = message.text
|
166 |
+
# Check if message is a response to one of our plates
|
167 |
+
for plate in list(self.waiting_plates.keys()):
|
168 |
+
if plate.lower() in text.lower():
|
169 |
+
# Found response for this plate
|
170 |
+
if 'Placa Localizada' in text or 'não foi encontrada' in text:
|
171 |
+
self.waiting_plates.pop(plate)
|
172 |
+
self.plates_memory.append({'plate': plate, 'has_seven': True})
|
173 |
+
elif 'não é uma placa válida' in text:
|
174 |
+
self.waiting_plates.pop(plate)
|
175 |
+
self.plates_memory.append({'plate': plate, 'has_seven': 'Non Valid'})
|
176 |
+
# Update the plate status in the displayed grid
|
177 |
+
self.update_displayed_plate(plate)
|
178 |
+
except Exception as e:
|
179 |
+
print(f"Error checking responses: {e}")
|
180 |
+
await asyncio.sleep(2)
|
181 |
+
|
182 |
+
def update_displayed_plate(self, plate):
|
183 |
+
"""Atualiza o status da placa exibida na tabela."""
|
184 |
+
for item in self.plates_memory:
|
185 |
+
if item['plate'] == plate:
|
186 |
+
if item['has_seven'] == 'Non Valid':
|
187 |
+
self.last_sixteen_plates.pop(plate, None)
|
188 |
+
else:
|
189 |
+
self.last_sixteen_plates[plate] = item['has_seven']
|
190 |
+
break
|
191 |
+
# Manter apenas as últimas 16 placas válidas
|
192 |
+
self.last_sixteen_plates = OrderedDict((p, s) for p, s in self.last_sixteen_plates.items() if s != 'Non Valid')
|
193 |
+
while len(self.last_sixteen_plates) > 16:
|
194 |
+
self.last_sixteen_plates.popitem(last=False)
|
195 |
+
|
196 |
+
def remove_non_alphanumeric(self, text):
|
197 |
+
"""Remove caracteres não alfanuméricos."""
|
198 |
+
return ''.join(char for char in text if char.isalnum())
|
199 |
+
|
200 |
+
def has_number_and_letter(self, text):
|
201 |
+
"""Verifica se o texto contém letras e números."""
|
202 |
+
return text.isalnum() and not text.isalpha() and not text.isdigit()
|
203 |
+
|
204 |
+
def process_license_plates(self, ocr_result):
|
205 |
+
"""Processa o resultado do OCR para identificar placas."""
|
206 |
+
def is_overlapping(box1, box2):
|
207 |
+
x1_min = min(point[0] for point in box1)
|
208 |
+
x1_max = max(point[0] for point in box1)
|
209 |
+
y1_min = min(point[1] for point in box1)
|
210 |
+
y1_max = max(point[1] for point in box1)
|
211 |
+
|
212 |
+
x2_min = min(point[0] for point in box2)
|
213 |
+
x2_max = max(point[0] for point in box2)
|
214 |
+
y2_min = min(point[1] for point in box2)
|
215 |
+
y2_max = max(point[1] for point in box2)
|
216 |
+
|
217 |
+
# Verificar sobreposição em X
|
218 |
+
overlap_x = (x1_max + 2 >= x2_min - 2) and (x1_min - 2 <= x2_max + 2)
|
219 |
+
# Verificar sobreposição em Y
|
220 |
+
overlap_y = (y1_max + 2 >= y2_min - 2) and (y1_min - 2 <= y2_max + 2)
|
221 |
+
|
222 |
+
return overlap_x and overlap_y
|
223 |
+
|
224 |
+
# Extrair caixas e textos
|
225 |
+
boxes = [item[0] for item in ocr_result[0]]
|
226 |
+
strings = [item[1][0] for item in ocr_result[0]]
|
227 |
+
|
228 |
+
# Separar em placas completas e segmentos parciais
|
229 |
+
full_plates = []
|
230 |
+
partial_segments = []
|
231 |
+
for box, s in zip(boxes, strings):
|
232 |
+
if len(s) >= 7:
|
233 |
+
full_plates.append((box, s))
|
234 |
+
elif len(s) in [3, 4]:
|
235 |
+
partial_segments.append((box, s))
|
236 |
+
|
237 |
+
# Processar segmentos parciais
|
238 |
+
n = len(partial_segments)
|
239 |
+
parent = list(range(n))
|
240 |
+
|
241 |
+
def find(i):
|
242 |
+
while parent[i] != i:
|
243 |
+
parent[i] = parent[parent[i]]
|
244 |
+
i = parent[i]
|
245 |
+
return i
|
246 |
+
|
247 |
+
def union(i, j):
|
248 |
+
pi = find(i)
|
249 |
+
pj = find(j)
|
250 |
+
if pi != pj:
|
251 |
+
parent[pj] = pi
|
252 |
+
|
253 |
+
# Construir grupos com base na sobreposição
|
254 |
+
for i in range(n):
|
255 |
+
for j in range(i + 1, n):
|
256 |
+
if is_overlapping(partial_segments[i][0], partial_segments[j][0]):
|
257 |
+
union(i, j)
|
258 |
+
|
259 |
+
# Agrupar as caixas
|
260 |
+
groups = defaultdict(list)
|
261 |
+
for i in range(n):
|
262 |
+
groups[find(i)].append(partial_segments[i])
|
263 |
+
|
264 |
+
# Concatenar textos em cada grupo
|
265 |
+
concatenated_partials = []
|
266 |
+
for group in groups.values():
|
267 |
+
# Ordenar com base na coordenada Y mínima (de cima para baixo)
|
268 |
+
sorted_group = sorted(group, key=lambda x: min(point[1] for point in x[0]))
|
269 |
+
concatenated = ''.join([s for box, s in sorted_group])
|
270 |
+
concatenated_partials.append(concatenated)
|
271 |
+
|
272 |
+
# Adicionar placas completas
|
273 |
+
all_plates = [s for box, s in full_plates] + concatenated_partials
|
274 |
+
return all_plates
|
275 |
+
|
276 |
+
def perform_ocr(self, img_array):
|
277 |
+
"""Realiza OCR na imagem e retorna os textos detectados."""
|
278 |
+
if img_array.shape[0] == 0 or img_array.shape[1] == 0:
|
279 |
+
return None
|
280 |
+
|
281 |
+
result = self.ocr.ocr(img_array, cls=True)
|
282 |
+
if not result[0]:
|
283 |
+
return None
|
284 |
+
return self.process_license_plates(result)
|
285 |
+
|
286 |
+
def save_plate(self, plate_text):
|
287 |
+
"""Salva o status da placa."""
|
288 |
+
# Verificar se a placa está na memória
|
289 |
+
for item in self.plates_memory:
|
290 |
+
if item['plate'] == plate_text:
|
291 |
+
return item['has_seven']
|
292 |
+
|
293 |
+
# Obter o status a partir da função has_seven
|
294 |
+
has_seven = self.has_seven(plate_text)
|
295 |
+
if has_seven != 'Waiting':
|
296 |
+
self.plates_memory.append({'plate': plate_text, 'has_seven': has_seven})
|
297 |
+
return has_seven
|
298 |
+
|
299 |
+
def process_frame(self, frame):
|
300 |
+
"""Processa um frame individual da webcam."""
|
301 |
+
img = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
302 |
+
|
303 |
+
# Detectar placas usando YOLO
|
304 |
+
results = self.model.predict(img, imgsz=256, conf=0.5)
|
305 |
+
plates_list = []
|
306 |
+
|
307 |
+
for res in results[0].boxes.data:
|
308 |
+
x1, y1, x2, y2 = int(res[0]), int(res[1]), int(res[2]), int(res[3])
|
309 |
+
|
310 |
+
# Ajustar o recorte para incluir padding
|
311 |
+
if (y2 - y1) > 0 and (x2 - x1) > 0:
|
312 |
+
prod = img.shape[0] * img.shape[1]
|
313 |
+
adj = round(5 * prod / 315000)
|
314 |
+
y1_adj = max(y1 - adj, 0)
|
315 |
+
y2_adj = min(y2 + adj, img.shape[0])
|
316 |
+
x1_adj = max(x1 - adj, 0)
|
317 |
+
x2_adj = min(x2 + adj, img.shape[1])
|
318 |
+
crop = img[y1_adj:y2_adj, x1_adj:x2_adj]
|
319 |
+
|
320 |
+
# Redimensionar o recorte
|
321 |
+
scale_factor = 3
|
322 |
+
new_size = (int(crop.shape[1] * scale_factor), int(crop.shape[0] * scale_factor))
|
323 |
+
resized_crop = cv2.resize(crop, new_size, interpolation=cv2.INTER_LINEAR)
|
324 |
+
|
325 |
+
# Realizar OCR
|
326 |
+
text_list = self.perform_ocr(resized_crop)
|
327 |
+
|
328 |
+
if text_list is None:
|
329 |
+
continue
|
330 |
+
for text in text_list:
|
331 |
+
text = self.remove_non_alphanumeric(text)
|
332 |
+
if text and self.has_number_and_letter(text):
|
333 |
+
has_seven = self.save_plate(text)
|
334 |
+
plates_list.append((text, has_seven))
|
335 |
+
if text not in self.last_sixteen_plates and has_seven != 'Non Valid':
|
336 |
+
if len(self.last_sixteen_plates) >= 16:
|
337 |
+
self.last_sixteen_plates.popitem(last=False)
|
338 |
+
self.last_sixteen_plates[text] = has_seven
|
339 |
+
|
340 |
+
# Atualizar a exibição
|
341 |
+
return self.get_display_table()
|
342 |
+
|
343 |
+
def get_display_table(self):
|
344 |
+
"""Retorna a tabela das últimas 16 placas detectadas."""
|
345 |
+
if not self.last_sixteen_plates:
|
346 |
+
return "Nenhuma placa detectada ainda."
|
347 |
+
else:
|
348 |
+
table = PrettyTable()
|
349 |
+
table.field_names = ["Placa", "Status"]
|
350 |
+
for plate, status in self.last_sixteen_plates.items():
|
351 |
+
if status != 'Waiting' and status != 'Non Valid':
|
352 |
+
status_text = 'Okay' if status == True else '!EITA!'
|
353 |
+
table.add_row([plate, status_text])
|
354 |
+
return table.get_string()
|
355 |
+
|
356 |
+
#===========================================================================#
|
357 |
+
#===========================================================================#
|
358 |
+
# PLAY GRADIO
|
359 |
+
#===========================================================================#
|
360 |
+
#===========================================================================#
|
361 |
+
# Instanciar o processador
|
362 |
+
processor = LicensePlateProcessor()
|
363 |
+
|
364 |
+
# Função para ser chamada pelo Gradio
|
365 |
+
def process_webcam_frame(frame):
|
366 |
+
return processor.process_frame(frame)
|
367 |
+
|
368 |
+
|
369 |
+
with gr.Blocks() as demo:
|
370 |
+
with gr.Row():
|
371 |
+
with gr.Column():
|
372 |
+
input_img = gr.Image(label="Webcam", sources="webcam", streaming=True, mirror_webcam=False)
|
373 |
+
with gr.Column():
|
374 |
+
output_text = gr.Textbox(label="Últimas 16 Placas Detectadas", lines=20)
|
375 |
+
input_img.stream(
|
376 |
+
process_webcam_frame,
|
377 |
+
inputs=input_img,
|
378 |
+
outputs=output_text,
|
379 |
+
time_limit=0.05,
|
380 |
+
stream_every=0.2,
|
381 |
+
concurrency_limit=10
|
382 |
+
)
|
383 |
+
|
384 |
+
demo.launch(share=True)
|
385 |
+
# demo.launch(debug=True, share=True)
|