detect_plate.py 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925
  1. import argparse
  2. import copy
  3. import math
  4. import os
  5. import platform
  6. import re
  7. import threading
  8. import time
  9. from collections import deque
  10. from datetime import datetime
  11. from pprint import pprint
  12. from concurrent.futures import ThreadPoolExecutor
  13. from threading import Lock
  14. import cv2
  15. import numpy as np
  16. import torch
  17. import redis
  18. import serial
  19. from typing import Optional, List
  20. # 平台检测
  21. PLATFORM = platform.system()
  22. import sys
  23. # 提前导入并减少重复导入
  24. # 注意:请确保这些模块的路径正确,若有导入错误需调整路径
  25. try:
  26. from models.experimental import attempt_load
  27. from modules.audio.speaker import IpCast
  28. from modules.display.screen import Screen, FlashFile
  29. from modules.radar.radar import RadarData, DeviceInitData, parse_radar_frame, open_serial
  30. from plate_recognition.double_plate_split_merge import get_split_merge
  31. from plate_recognition.plate_rec import (
  32. allFilePath,
  33. cv_imread,
  34. get_plate_result,
  35. init_model,
  36. )
  37. from utils.datasets import letterbox
  38. from utils.general import check_img_size, non_max_suppression_face, scale_coords
  39. except ImportError as e:
  40. print(f"导入模块失败: {e},请检查模块路径是否正确")
  41. sys.exit(1)
  42. # ===================== 全局变量初始化(完整保留原功能) =====================
  43. # Redis连接配置
  44. REDIS_HOST = 'localhost'
  45. REDIS_PORT = 6379
  46. REDIS_DB = 0
  47. REDIS_PASSWORD = None
  48. REDIS_KEY = 'plate_results'
  49. WINDOW_SIZE = 5
  50. # 屏幕连接配置
  51. SCREEN_HOST = '192.168.110.200' # 主屏幕:显示车牌识别信息
  52. SCREEN_PORT = 5005
  53. RADAR_SCREEN_HOST = '192.168.110.199' # 雷达屏幕:显示雷达速度信息
  54. RADAR_SCREEN_PORT = 5005
  55. # 根据平台自动选择串口路径
  56. if PLATFORM == 'Windows':
  57. RADAR_PORT = 'COM3'
  58. SPEAKER_PORT = 'COM4'
  59. elif PLATFORM == 'Linux':
  60. RADAR_PORT = '/dev/ttyACM0'
  61. SPEAKER_PORT = '/dev/ttyUSB0'
  62. else:
  63. RADAR_PORT = '/dev/ttyACM0'
  64. SPEAKER_PORT = '/dev/ttyUSB0'
  65. DEVICE_LOW_SPEED = 15
  66. # 重连相关配置
  67. MAX_RECONNECT_ATTEMPTS = 10
  68. RECONNECT_DELAY = 5
  69. MAX_CONSECUTIVE_FAILURES = 5
  70. # 阈值设置
  71. DETECT_THRESH = 0.65
  72. COLOR_THRESH = 0.85
  73. REC_THRESH = 0.85
  74. PLATE_ASPECT_RATIO = 1.8 # 车牌宽高比(正向>1.8,反向<1.2)
  75. # 性能优化参数
  76. FRAME_SKIP = 2
  77. BATCH_REDIS_WRITE = True
  78. REDIS_CLEAN_INTERVAL = 20
  79. ASYNC_REDIS = True
  80. INFERENCE_HALF = True
  81. JIT_COMPILE = False
  82. THREAD_POOL_SIZE = 5
  83. # 合法车牌正则
  84. LICENSE_PLATE_PATTERN = re.compile(
  85. r'^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领A-Z]{1}[A-Z]{1}[A-Z0-9]{5,7}$')
  86. # 全局变量
  87. redis_client = None
  88. redis_lock = Lock()
  89. redis_write_queue = deque(maxlen=100)
  90. executor = ThreadPoolExecutor(max_workers=THREAD_POOL_SIZE)
  91. clean_error_count = 0
  92. redis_read_error = 0
  93. cap = None
  94. radar_serial = None
  95. # 屏幕/语音全局实例(关键:明确区分主屏幕和雷达屏幕)
  96. screen = None # 主屏幕实例(192.168.110.199)
  97. radar_screen = None # 雷达屏幕实例(192.168.110.198)
  98. speaker = None # 语音实例
  99. # ===================== 核心依赖函数 =====================
  100. def scale_coords_landmarks(img1_shape, coords, img0_shape, ratio_pad=None):
  101. """车牌关键点坐标还原到原图"""
  102. if ratio_pad is None:
  103. gain = min(img1_shape[0] / img0_shape[0], img1_shape[1] / img0_shape[1])
  104. pad = ((img1_shape[1] - img0_shape[1] * gain) / 2, (img1_shape[0] - img0_shape[0] * gain) / 2)
  105. else:
  106. gain = ratio_pad[0][0]
  107. pad = ratio_pad[1]
  108. coords[:, [0, 2, 4, 6]] -= pad[0]
  109. coords[:, [1, 3, 5, 7]] -= pad[1]
  110. coords[:, :8] /= gain
  111. coords[:, 0] = coords[:, 0].clip(0, img0_shape[1])
  112. coords[:, 1] = coords[:, 1].clip(0, img0_shape[0])
  113. coords[:, 2] = coords[:, 2].clip(0, img0_shape[1])
  114. coords[:, 3] = coords[:, 3].clip(0, img0_shape[0])
  115. coords[:, 4] = coords[:, 4].clip(0, img0_shape[1])
  116. coords[:, 5] = coords[:, 5].clip(0, img0_shape[0])
  117. coords[:, 6] = coords[:, 6].clip(0, img0_shape[1])
  118. coords[:, 7] = coords[:, 7].clip(0, img0_shape[0])
  119. return coords
  120. def order_points(pts):
  121. """排序四点坐标"""
  122. if isinstance(pts, np.ndarray) and pts.size == 0:
  123. return np.zeros((4, 2), dtype="float32")
  124. rect = np.zeros((4, 2), dtype="float32")
  125. s = pts.sum(axis=1)
  126. rect[0] = pts[np.argmin(s)]
  127. rect[2] = pts[np.argmax(s)]
  128. diff = np.diff(pts, axis=1)
  129. rect[1] = pts[np.argmin(diff)]
  130. rect[3] = pts[np.argmax(diff)]
  131. return rect
  132. def four_point_transform(image, pts):
  133. """四点透视变换"""
  134. if not isinstance(pts, np.ndarray) or pts.shape != (4, 2) or pts.size == 0:
  135. return image
  136. rect = order_points(pts)
  137. (tl, tr, br, bl) = rect
  138. widthA = np.sqrt(((br[0] - bl[0]) ** 2) + ((br[1] - bl[1]) ** 2))
  139. widthB = np.sqrt(((tr[0] - tl[0]) ** 2) + ((tr[1] - tl[1]) ** 2))
  140. maxWidth = max(int(widthA) if widthA > 0 else 1, int(widthB) if widthB > 0 else 1)
  141. heightA = np.sqrt(((tr[0] - br[0]) ** 2) + ((tr[1] - br[1]) ** 2))
  142. heightB = np.sqrt(((tl[0] - bl[0]) ** 2) + ((tl[1] - bl[1]) ** 2))
  143. maxHeight = max(int(heightA) if heightA > 0 else 1, int(heightB) if heightB > 0 else 1)
  144. dst = np.array([[0, 0], [maxWidth - 1, 0], [maxWidth - 1, maxHeight - 1], [0, maxHeight - 1]], dtype="float32")
  145. M = cv2.getPerspectiveTransform(rect, dst)
  146. warped = cv2.warpPerspective(image, M, (maxWidth, maxHeight))
  147. return warped
  148. def is_valid_forward_plate(plate_str, bbox):
  149. """判断是否为来向车(正向车牌)"""
  150. plate_clean = plate_str.strip().upper().replace(' ', '')
  151. if len(plate_clean) < 7 or len(plate_clean) > 8:
  152. return False
  153. x1, y1, x2, y2 = bbox
  154. width = x2 - x1
  155. height = y2 - y1
  156. if height == 0 or (width / height) < PLATE_ASPECT_RATIO:
  157. return False
  158. if not LICENSE_PLATE_PATTERN.match(plate_clean):
  159. return False
  160. return True
  161. def get_plate_rec_landmark(img, xyxy, conf, landmarks, class_num, device, plate_rec_model, is_color=False):
  162. """车牌识别核心函数"""
  163. h, w, _ = img.shape
  164. result_dict = {}
  165. x1, y1, x2, y2 = map(int, np.ravel(xyxy))
  166. landmarks = np.ravel(landmarks)
  167. landmarks_np = np.array(landmarks).reshape(4, 2).astype(int)
  168. rect = [x1, y1, x2, y2]
  169. # 透视变换获取车牌ROI
  170. class_label = int(class_num)
  171. roi_img = four_point_transform(img, landmarks_np)
  172. if class_label:
  173. roi_img = get_split_merge(roi_img)
  174. # 识别车牌号
  175. if not is_color:
  176. plate_number, rec_prob = get_plate_result(roi_img, device, plate_rec_model, is_color=is_color)
  177. plate_color = ""
  178. color_conf = 0.0
  179. else:
  180. plate_number, rec_prob, plate_color, color_conf = get_plate_result(roi_img, device, plate_rec_model,
  181. is_color=is_color)
  182. # 修复rec_prob格式
  183. if isinstance(rec_prob, np.ndarray):
  184. rec_prob = rec_prob.tolist()
  185. # 判断是否为来向车
  186. is_forward = is_valid_forward_plate(plate_number, rect)
  187. # 组装结果
  188. result_dict.update({
  189. "rect": rect,
  190. "detect_conf": conf,
  191. "landmarks": landmarks_np.tolist(),
  192. "plate_no": plate_number,
  193. "rec_conf": rec_prob,
  194. "roi_height": roi_img.shape[0],
  195. "plate_color": plate_color,
  196. "color_conf": color_conf,
  197. "plate_type": class_num,
  198. "is_forward": is_forward
  199. })
  200. return result_dict
  201. def get_window_info():
  202. """获取Redis窗口统计信息"""
  203. if redis_client is None:
  204. return {"count": 0, "window_size": WINDOW_SIZE}
  205. try:
  206. clean_expired_data_batch()
  207. hash_key = f"{REDIS_KEY}:data"
  208. zset_key = f"{REDIS_KEY}:sorted"
  209. data_count = redis_client.hlen(hash_key)
  210. sorted_count = redis_client.zcard(zset_key)
  211. oldest_ts = newest_ts = int(time.time())
  212. time_range = 0
  213. if sorted_count > 0:
  214. timestamps_with_scores = redis_client.zrange(zset_key, 0, -1, withscores=True)
  215. if timestamps_with_scores:
  216. timestamps = []
  217. for _, score in timestamps_with_scores:
  218. try:
  219. timestamps.append(int(float(score)))
  220. except:
  221. continue
  222. if len(timestamps) > 0:
  223. oldest_ts = min(timestamps)
  224. newest_ts = max(timestamps)
  225. time_range = newest_ts - oldest_ts
  226. return {
  227. "count": data_count,
  228. "window_size": WINDOW_SIZE,
  229. "time_range": time_range,
  230. "oldest_record": datetime.fromtimestamp(oldest_ts).strftime("%H:%M:%S") if sorted_count > 0 else "无",
  231. "newest_record": datetime.fromtimestamp(newest_ts).strftime("%H:%M:%S") if sorted_count > 0 else "无"
  232. }
  233. except Exception as e:
  234. print(f"获取窗口信息失败: {e}")
  235. return {"count": 0, "window_size": WINDOW_SIZE}
  236. # ===================== 工具函数 =====================
  237. def get_current_time():
  238. """获取格式化当前时间"""
  239. return datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
  240. def get_current_timestamp():
  241. """获取秒级时间戳"""
  242. return int(time.time())
  243. def connect_stream(stream_url, cap_options=""):
  244. """建立视频流连接,带重试机制"""
  245. global cap
  246. attempt = 0
  247. while attempt < MAX_RECONNECT_ATTEMPTS:
  248. try:
  249. print(f"[{get_current_time()}] 尝试连接视频流: {stream_url} (第{attempt + 1}次)")
  250. if cap_options:
  251. cap = cv2.VideoCapture()
  252. os.environ["OPENCV_FFMPEG_CAPTURE_OPTIONS"] = cap_options
  253. success = cap.open(stream_url, cv2.CAP_FFMPEG)
  254. else:
  255. cap = cv2.VideoCapture(stream_url)
  256. success = cap.isOpened()
  257. if success:
  258. cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
  259. cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc('H', '2', '6', '4'))
  260. ret, frame = cap.read()
  261. if ret:
  262. print(f"[{get_current_time()}] 视频流连接成功")
  263. return cap, True
  264. else:
  265. print(f"[{get_current_time()}] 视频流打开但无法读取帧")
  266. cap.release()
  267. else:
  268. print(f"[{get_current_time()}] 无法打开视频流")
  269. print(f"[{get_current_time()}] 连接失败,{RECONNECT_DELAY}秒后重试...")
  270. time.sleep(RECONNECT_DELAY)
  271. attempt += 1
  272. except Exception as e:
  273. print(f"[{get_current_time()}] 连接异常: {str(e)}")
  274. time.sleep(RECONNECT_DELAY)
  275. attempt += 1
  276. print(f"[{get_current_time()}] 达到最大重连次数({MAX_RECONNECT_ATTEMPTS}),退出")
  277. return None, False
  278. def reconnect_stream(stream_url, cap_options=""):
  279. """重新连接视频流"""
  280. global cap
  281. print(f"[{get_current_time()}] 开始重新连接视频流...")
  282. if cap is not None:
  283. cap.release()
  284. time.sleep(2)
  285. return connect_stream(stream_url, cap_options)
  286. # ===================== Redis操作 =====================
  287. def clean_expired_data_batch():
  288. """批量清理过期Redis数据"""
  289. if redis_client is None:
  290. return 0
  291. try:
  292. with redis_lock:
  293. current_ts = get_current_timestamp()
  294. cutoff_ts = current_ts - WINDOW_SIZE
  295. pipe = redis_client.pipeline(transaction=False)
  296. zset_key = f"{REDIS_KEY}:sorted"
  297. expired_timestamps = redis_client.zrangebyscore(zset_key, 0, cutoff_ts)
  298. if expired_timestamps:
  299. hash_key = f"{REDIS_KEY}:data"
  300. pipe.hdel(hash_key, *expired_timestamps)
  301. pipe.zremrangebyscore(zset_key, 0, cutoff_ts)
  302. pipe.execute()
  303. return len(expired_timestamps)
  304. return 0
  305. except Exception as e:
  306. global clean_error_count
  307. clean_error_count += 1
  308. if clean_error_count % 10 == 0:
  309. print(f"清理过期数据失败({clean_error_count}次): {e}")
  310. return 0
  311. def save_to_redis_async(plate_no, plate_color, detect_conf, color_conf, rec_avg, direction="incoming"):
  312. """异步写入Redis"""
  313. try:
  314. timestamp = get_current_timestamp()
  315. timestamp_ms = int(time.time() * 1000)
  316. entry_data = {
  317. "plate_no": plate_no.strip(),
  318. "plate_color": plate_color,
  319. "detect_conf": f"{detect_conf:.3f}",
  320. "color_conf": f"{color_conf:.3f}",
  321. "rec_avg": f"{rec_avg:.3f}",
  322. "timestamp": str(timestamp),
  323. "timestamp_ms": str(timestamp_ms),
  324. "datetime": get_current_time(),
  325. "source": "rtsp_stream",
  326. "direction": direction
  327. }
  328. with redis_lock:
  329. if BATCH_REDIS_WRITE:
  330. redis_write_queue.append((timestamp, entry_data))
  331. if len(redis_write_queue) >= 10:
  332. flush_redis_queue()
  333. else:
  334. pipe = redis_client.pipeline(transaction=False)
  335. hash_key = f"{REDIS_KEY}:data"
  336. zset_key = f"{REDIS_KEY}:sorted"
  337. pipe.hset(hash_key, timestamp, str(entry_data))
  338. pipe.zadd(zset_key, {timestamp: timestamp})
  339. pipe.execute()
  340. return True, f"加入队列: {timestamp}"
  341. except Exception as e:
  342. return False, f"异步写入失败: {e}"
  343. def flush_redis_queue():
  344. """刷入Redis队列数据"""
  345. if not redis_write_queue or redis_client is None:
  346. return False
  347. try:
  348. with redis_lock:
  349. if not redis_write_queue:
  350. return True
  351. pipe = redis_client.pipeline(transaction=False)
  352. hash_key = f"{REDIS_KEY}:data"
  353. zset_key = f"{REDIS_KEY}:sorted"
  354. for timestamp, entry_data in redis_write_queue:
  355. pipe.hset(hash_key, timestamp, str(entry_data))
  356. pipe.zadd(zset_key, {timestamp: timestamp})
  357. pipe.execute()
  358. redis_write_queue.clear()
  359. return True
  360. except Exception as e:
  361. print(f"批量写入Redis失败: {e}")
  362. return False
  363. def get_recent_plates_from_redis():
  364. """获取最近5秒的车牌记录"""
  365. if redis_client is None:
  366. return []
  367. try:
  368. zset_key = f"{REDIS_KEY}:sorted"
  369. hash_key = f"{REDIS_KEY}:data"
  370. current_ts = get_current_timestamp()
  371. cutoff_ts = current_ts - WINDOW_SIZE
  372. recent_timestamps = redis_client.zrevrangebyscore(zset_key, current_ts, cutoff_ts)
  373. results = []
  374. if recent_timestamps:
  375. entries = redis_client.hmget(hash_key, recent_timestamps)
  376. for ts, entry_str in zip(recent_timestamps, entries):
  377. if entry_str:
  378. try:
  379. data = eval(entry_str)
  380. results.append({
  381. 'timestamp': int(ts),
  382. 'data': data
  383. })
  384. except:
  385. continue
  386. return results
  387. except Exception as e:
  388. global redis_read_error
  389. redis_read_error += 1
  390. if redis_read_error % 10 == 0:
  391. print(f"从Redis读取数据失败({redis_read_error}次): {e}")
  392. return []
  393. # ===================== 初始化语音/屏幕 =====================
  394. def init_speaker(port: str) -> IpCast | None:
  395. """初始化语音模块(带异常处理和重试)"""
  396. attempts = 0
  397. while attempts < MAX_RECONNECT_ATTEMPTS:
  398. try:
  399. speaker = IpCast(port=port)
  400. print(f"✅ 语音模块初始化成功(串口:{port})")
  401. return speaker
  402. except Exception as e:
  403. attempts += 1
  404. if attempts < MAX_RECONNECT_ATTEMPTS:
  405. print(f"⚠️ 语音模块初始化失败:{e},{RECONNECT_DELAY}秒后重试({attempts}/{MAX_RECONNECT_ATTEMPTS})")
  406. time.sleep(RECONNECT_DELAY)
  407. else:
  408. print(f"❌ 语音模块初始化失败:{e},已达到最大重试次数")
  409. return None
  410. def init_screen(name: str, ip: str, port: int) -> Screen | None:
  411. """初始化屏幕(带连接重试)"""
  412. screen = Screen(name=name, ip=ip, port=str(port))
  413. attempts = 0
  414. while attempts < MAX_RECONNECT_ATTEMPTS:
  415. if screen.get_live_state():
  416. print(f"✅ {name} 连接成功(IP:{ip}:{port})")
  417. return screen
  418. print(f"⚠️ {name} 连接失败,{RECONNECT_DELAY}秒后重试({attempts + 1}/{MAX_RECONNECT_ATTEMPTS})")
  419. time.sleep(RECONNECT_DELAY)
  420. screen.reconnect()
  421. attempts += 1
  422. print(f"❌ {name} 连接失败(IP:{ip}:{port}),达到最大重试次数")
  423. return None
  424. def init_screen_async(name: str, ip: str, port: int, result_dict: dict):
  425. """异步初始化屏幕"""
  426. screen = init_screen(name, ip, port)
  427. result_dict[name] = screen
  428. # ===================== 模型加载与推理 =====================
  429. def load_model_optimized(weights, device):
  430. """优化加载模型"""
  431. model = attempt_load(weights, map_location=device)
  432. if JIT_COMPILE and device.type != 'cpu':
  433. try:
  434. dummy = torch.rand(1, 3, 640, 640).to(device)
  435. if INFERENCE_HALF:
  436. dummy = dummy.half()
  437. model = torch.jit.trace(model, dummy)
  438. print("模型JIT编译成功")
  439. except Exception as e:
  440. print(f"JIT编译失败: {e}")
  441. if INFERENCE_HALF and device.type != 'cpu':
  442. model.half()
  443. model.eval()
  444. for param in model.parameters():
  445. param.requires_grad = False
  446. return model
  447. def detect_Recognition_plate_optimized(model, orgimg, device, plate_rec_model, img_size, is_color=False):
  448. """优化的车牌检测识别"""
  449. conf_thres = 0.3
  450. iou_thres = 0.5
  451. dict_list = []
  452. h0, w0 = orgimg.shape[:2]
  453. r = img_size / max(h0, w0)
  454. if abs(r - 1) > 0.1:
  455. interp = cv2.INTER_AREA if r < 1 else cv2.INTER_LINEAR
  456. img0 = cv2.resize(orgimg, (int(w0 * r), int(h0 * r)), interpolation=interp)
  457. else:
  458. img0 = orgimg
  459. imgsz = check_img_size(img_size, s=model.stride.max())
  460. img = letterbox(img0, new_shape=imgsz)[0]
  461. img = img[:, :, ::-1].transpose(2, 0, 1).copy()
  462. img = torch.from_numpy(img).to(device)
  463. img = img.float() / 255.0
  464. if INFERENCE_HALF and device.type != 'cpu':
  465. img = img.half()
  466. if img.ndim == 3:
  467. img = img.unsqueeze(0)
  468. with torch.no_grad():
  469. pred = model(img)[0]
  470. pred = non_max_suppression_face(pred, conf_thres, iou_thres)
  471. for det in pred:
  472. if len(det):
  473. det[:, :4] = scale_coords(img.shape[2:], det[:, :4], orgimg.shape).round()
  474. det[:, 5:13] = scale_coords_landmarks(img.shape[2:], det[:, 5:13], orgimg.shape).round()
  475. for j in range(det.size(0)):
  476. xyxy = det[j, :4].tolist()
  477. conf = det[j, 4].cpu().item()
  478. landmarks = det[j, 5:13].tolist()
  479. class_num = det[j, 13].cpu().item()
  480. if conf < DETECT_THRESH:
  481. continue
  482. result_dict = get_plate_rec_landmark(orgimg, xyxy, conf, landmarks, class_num, device, plate_rec_model,
  483. is_color)
  484. dict_list.append(result_dict)
  485. break
  486. break
  487. return dict_list[:1]
  488. # ===================== 主函数 =====================
  489. def start(image_path="imgs"):
  490. # 声明使用全局的屏幕/语音实例(核心修复:解决变量作用域问题)
  491. global screen, radar_screen, speaker, redis_client
  492. # 参数解析
  493. parser = argparse.ArgumentParser()
  494. parser.add_argument("--detect_model", nargs="+", type=str, default="weights/plate_detect.pt", help="检测模型路径")
  495. parser.add_argument("--rec_model", type=str, default="weights/plate_rec_color.pth", help="识别模型路径")
  496. parser.add_argument("--is_color", type=bool, default=True, help="是否识别车牌颜色")
  497. parser.add_argument("--image_path", type=str, default=image_path, help="图片路径")
  498. parser.add_argument("--img_size", type=int, default=512, help="推理尺寸")
  499. parser.add_argument("--output", type=str, default="result", help="输出目录")
  500. parser.add_argument("--video", type=str, default="", help="视频文件路径")
  501. parser.add_argument("--stream", type=str, default="", help="RTSP/RTMP流地址")
  502. parser.add_argument("--redis_host", type=str, default="localhost", help="Redis主机")
  503. parser.add_argument("--redis_port", type=int, default=6379, help="Redis端口")
  504. parser.add_argument("--redis_key", type=str, default="plate_results", help="Redis键名")
  505. parser.add_argument("--window_size", type=int, default=5, help="滑动窗口秒数")
  506. opt = parser.parse_args()
  507. # 设备配置
  508. device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
  509. if device.type == 'cuda':
  510. torch.backends.cudnn.benchmark = True
  511. torch.backends.cuda.matmul.allow_tf32 = True
  512. # 更新全局配置
  513. global REDIS_HOST, REDIS_PORT, REDIS_KEY, WINDOW_SIZE
  514. REDIS_HOST = opt.redis_host
  515. REDIS_PORT = opt.redis_port
  516. REDIS_KEY = opt.redis_key
  517. WINDOW_SIZE = opt.window_size
  518. # Redis连接
  519. try:
  520. redis_client = redis.Redis(
  521. host=REDIS_HOST,
  522. port=REDIS_PORT,
  523. db=REDIS_DB,
  524. password=REDIS_PASSWORD,
  525. decode_responses=True,
  526. socket_timeout=2,
  527. socket_connect_timeout=2
  528. )
  529. redis_client.ping()
  530. print("✅ Redis连接成功(优化版)")
  531. except Exception as e:
  532. print(f"❌ Redis连接失败: {e}")
  533. redis_client = None
  534. # 创建输出目录
  535. os.makedirs(opt.output, exist_ok=True)
  536. # 加载模型
  537. try:
  538. detect_model = load_model_optimized(opt.detect_model, device)
  539. plate_rec_model = init_model(device, opt.rec_model, is_color=opt.is_color)
  540. total_detect = sum(p.numel() for p in detect_model.parameters()) / 1e6
  541. total_rec = sum(p.numel() for p in plate_rec_model.parameters()) / 1e6
  542. print(f"✅ 模型加载成功:检测{total_detect:.2f}M, 识别{total_rec:.2f}M")
  543. except Exception as e:
  544. print(f"❌ 模型加载失败: {e}")
  545. return
  546. # 打印配置信息
  547. print(f"推理模式: {'半精度' if INFERENCE_HALF else '全精度'} | JIT编译: {JIT_COMPILE}")
  548. print(
  549. f"帧处理策略: 每{FRAME_SKIP}帧处理一次 | Redis: {'异步批量' if ASYNC_REDIS and BATCH_REDIS_WRITE else '同步'}")
  550. print(f"过滤策略: 不过滤方向,检测所有车辆 | 宽高比阈值: {PLATE_ASPECT_RATIO}")
  551. # 初始化语音模块
  552. speaker = init_speaker(SPEAKER_PORT)
  553. # 异步初始化两个屏幕
  554. screen_init_results = {}
  555. screen_threads = [
  556. threading.Thread(target=init_screen_async, args=("主屏幕", SCREEN_HOST, SCREEN_PORT, screen_init_results),
  557. daemon=True),
  558. threading.Thread(target=init_screen_async,
  559. args=("雷达屏幕", RADAR_SCREEN_HOST, RADAR_SCREEN_PORT, screen_init_results), daemon=True)
  560. ]
  561. for t in screen_threads:
  562. t.start()
  563. for t in screen_threads:
  564. t.join(timeout=30)
  565. # 获取屏幕初始化结果(绑定全局变量)
  566. screen = screen_init_results.get("主屏幕")
  567. radar_screen = screen_init_results.get("雷达屏幕")
  568. # 打印屏幕绑定信息(调试用)
  569. if screen:
  570. print(f"✅ 主屏幕已绑定:{SCREEN_HOST}:{SCREEN_PORT}(用于显示车牌)")
  571. else:
  572. print(f"❌ 主屏幕初始化失败")
  573. if radar_screen:
  574. print(f"✅ 雷达屏幕已绑定:{RADAR_SCREEN_HOST}:{RADAR_SCREEN_PORT}(用于显示雷达速度)")
  575. else:
  576. print(f"❌ 雷达屏幕初始化失败")
  577. DeviceInitData.LowSpeed = DEVICE_LOW_SPEED
  578. # 启动雷达线程(核心修复:传入雷达屏幕实例,而非主屏幕)
  579. try:
  580. radar_thread = threading.Thread(
  581. target=open_serial,
  582. args=(RADAR_PORT, speaker, radar_screen), # 传入radar_screen(雷达屏幕)
  583. daemon=True
  584. )
  585. radar_thread.start()
  586. print(f"✅ 雷达已在后台线程启动,串口:{RADAR_PORT},绑定雷达屏幕")
  587. except Exception as e:
  588. print(f"❌ 雷达启动失败: {e}")
  589. # 处理RTSP流
  590. if opt.stream:
  591. cap_options = "rtsp_transport=tcp"
  592. cap, connected = connect_stream(opt.stream, cap_options)
  593. if not connected:
  594. print(f"[{get_current_time()}] 初始连接失败,退出程序")
  595. return
  596. # 初始化统计变量
  597. consecutive_failures = 0
  598. reconnect_count = 0
  599. frame_count = 0
  600. processed_count = 0
  601. last_print_time = time.time()
  602. print_interval = 10.0
  603. inference_times = deque(maxlen=50)
  604. last_output_dict = {}
  605. output_count = 0
  606. incoming_car_count = 0
  607. outgoing_car_count = 0
  608. try:
  609. while True:
  610. frame_count += 1
  611. ret, frame = cap.read()
  612. # 处理帧读取失败
  613. if not ret:
  614. consecutive_failures += 1
  615. if consecutive_failures % MAX_CONSECUTIVE_FAILURES == 0:
  616. print(f"[{get_current_time()}] 视频流中断(连续失败{consecutive_failures}次)")
  617. if consecutive_failures >= MAX_CONSECUTIVE_FAILURES:
  618. cap, reconnected = reconnect_stream(opt.stream, cap_options)
  619. if reconnected:
  620. reconnect_count += 1
  621. consecutive_failures = 0
  622. frame_count = 0
  623. continue
  624. else:
  625. break
  626. continue
  627. consecutive_failures = 0
  628. # 帧跳过策略
  629. if frame_count % FRAME_SKIP != 0:
  630. continue
  631. processed_count += 1
  632. # 推理处理
  633. inference_start = time.time()
  634. try:
  635. dict_list = detect_Recognition_plate_optimized(
  636. detect_model, frame, device, plate_rec_model, opt.img_size, is_color=opt.is_color
  637. )
  638. inference_time = time.time() - inference_start
  639. inference_times.append(inference_time)
  640. current_time = time.time()
  641. # 处理识别结果
  642. for res in dict_list:
  643. plate_no = res['plate_no'].strip()
  644. if len(plate_no) < 4 or plate_no.lower() in ['unknown', '']:
  645. continue
  646. # 阈值过滤
  647. detect_conf = float(res['detect_conf'])
  648. color_conf = res.get('color_conf', 0.0)
  649. rec_conf = res.get('rec_conf', [])
  650. if isinstance(rec_conf, np.ndarray):
  651. rec_conf_list = rec_conf.tolist()
  652. else:
  653. rec_conf_list = rec_conf if isinstance(rec_conf, list) else []
  654. rec_avg = np.mean(rec_conf_list) if len(rec_conf_list) > 0 else 0.0
  655. if detect_conf < DETECT_THRESH or color_conf < COLOR_THRESH or rec_avg < REC_THRESH:
  656. continue
  657. # 去重判断
  658. clean_plate = plate_no.replace(' ', '').upper()
  659. should_output_flag = False
  660. similar_found = None
  661. for existing_plate in last_output_dict:
  662. if clean_plate[:5] == existing_plate[:5]:
  663. similar_found = existing_plate
  664. break
  665. if similar_found is None:
  666. should_output_flag = True
  667. last_output_dict[clean_plate] = current_time
  668. else:
  669. time_diff = current_time - last_output_dict[similar_found]
  670. if time_diff >= 3.0:
  671. del last_output_dict[similar_found]
  672. last_output_dict[clean_plate] = current_time
  673. should_output_flag = True
  674. # 输出和保存
  675. if should_output_flag:
  676. # 统计方向
  677. if res.get("is_forward", False):
  678. incoming_car_count += 1
  679. direction = "incoming"
  680. else:
  681. outgoing_car_count += 1
  682. direction = "outgoing"
  683. plate_color = res.get('plate_color', '未知')
  684. current_time_str = get_current_time()
  685. output_line = (
  686. f"[{current_time_str}] {plate_no} | 检:{detect_conf:.3f} "
  687. f"色:{color_conf:.3f} 识:{rec_avg:.3f} | {plate_color}")
  688. print(output_line)
  689. # 写入Redis
  690. if redis_client:
  691. if ASYNC_REDIS:
  692. executor.submit(save_to_redis_async, plate_no, plate_color, detect_conf, color_conf,
  693. rec_avg, direction)
  694. # 核心修复:仅写入主屏幕(screen),不写入雷达屏幕
  695. if screen:
  696. try:
  697. ff = FlashFile()
  698. ff.set_msg(plate_no, 1) # 显示车牌
  699. ff.set_mode(4, 1)
  700. ff.set_origin(0, True, 0)
  701. ff.set_area(128, True, 32)
  702. screen.text_ram(ff, True)
  703. except Exception as e:
  704. print(f"❌ 写入主屏幕失败: {e}")
  705. else:
  706. save_to_redis_async(plate_no, plate_color, detect_conf, color_conf, rec_avg,
  707. direction)
  708. output_count += 1
  709. except Exception as e:
  710. print(f"[{get_current_time()}] 处理异常: {e}")
  711. import traceback
  712. traceback.print_exc()
  713. continue
  714. # 定期清理Redis
  715. if frame_count % REDIS_CLEAN_INTERVAL == 0 and redis_client:
  716. executor.submit(clean_expired_data_batch)
  717. # 定期刷入Redis队列
  718. if frame_count % 10 == 0 and BATCH_REDIS_WRITE and redis_client:
  719. executor.submit(flush_redis_queue)
  720. # 状态打印
  721. if time.time() - last_print_time >= print_interval:
  722. avg_inference = sum(inference_times) / len(inference_times) if len(inference_times) > 0 else 0
  723. print(f"\n[{get_current_time()}] 状态统计")
  724. print(f"总帧数: {frame_count} | 处理帧: {processed_count} | 输出车牌: {output_count}")
  725. print(
  726. f"来向车数量: {incoming_car_count} | 去向车数量: {outgoing_car_count} | 重连次数: {reconnect_count}")
  727. print(f"平均推理时间: {avg_inference * 1000:.1f}ms | 处理帧率: {1 / avg_inference:.1f}fps"
  728. if avg_inference > 0 else "平均推理时间: 0ms | 处理帧率: 0fps")
  729. print(f"缓存车牌种类: {len(last_output_dict)} | Redis清理失败: {clean_error_count}次")
  730. if redis_client:
  731. try:
  732. window_info = get_window_info()
  733. print(f"Redis窗口: {window_info['count']}条/{WINDOW_SIZE}秒")
  734. except:
  735. pass
  736. last_print_time = time.time()
  737. # 退出按键
  738. if cv2.waitKey(1) & 0xFF == ord('q'):
  739. break
  740. except KeyboardInterrupt:
  741. print(f"\n[{get_current_time()}] 用户中断")
  742. except Exception as e:
  743. print(f"\n[{get_current_time()}] 运行错误: {e}")
  744. import traceback
  745. traceback.print_exc()
  746. finally:
  747. # 资源清理
  748. if cap is not None:
  749. cap.release()
  750. cv2.destroyAllWindows()
  751. executor.shutdown(wait=True)
  752. # 刷入剩余Redis数据
  753. if redis_client:
  754. flush_redis_queue()
  755. clean_expired_data_batch()
  756. # 最终统计
  757. print(f"\n[{get_current_time()}] 结束报告")
  758. print(f"总帧数: {frame_count} | 处理帧: {processed_count} | 输出车牌: {output_count}")
  759. print(f"来向车总数: {incoming_car_count} | 去向车总数: {outgoing_car_count} | 重连次数: {reconnect_count}")
  760. if len(inference_times) > 0:
  761. avg_inf = sum(inference_times) / len(inference_times)
  762. print(f"平均推理时间: {avg_inf * 1000:.1f}ms | 实时FPS: {1 / avg_inf:.1f}")
  763. else:
  764. print("平均推理时间: 0ms | 实时FPS: 0")
  765. print(f"识别车牌种类: {len(last_output_dict)} | Redis读取失败: {redis_read_error}次")
  766. if __name__ == '__main__':
  767. # 初始化屏幕/语音(全局)
  768. speaker = init_speaker(SPEAKER_PORT)
  769. # 启动主程序
  770. start()