detect_plate.py 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930
  1. import argparse
  2. import os
  3. import platform
  4. import re
  5. import threading
  6. import time
  7. import json
  8. from collections import deque, defaultdict
  9. from concurrent.futures import ThreadPoolExecutor
  10. from datetime import datetime
  11. from threading import Lock
  12. import cv2
  13. import numpy as np
  14. import redis
  15. import torch
  16. PLATFORM = platform.system()
  17. import sys
  18. # ===================== 模块导入 =====================
  19. try:
  20. from models.experimental import attempt_load
  21. from modules.audio.speaker import IpCast
  22. from modules.display.screen import Screen, FlashFile
  23. from modules.radar.radar import RadarData, DeviceInitData, parse_radar_frame, open_serial
  24. from plate_recognition.double_plate_split_merge import get_split_merge
  25. from plate_recognition.plate_rec import (
  26. allFilePath, cv_imread, get_plate_result, init_model,
  27. )
  28. from utils.datasets import letterbox
  29. from utils.general import check_img_size, non_max_suppression_face, scale_coords
  30. except ImportError as e:
  31. print(f"❌ 导入模块失败:{e}")
  32. sys.exit(1)
  33. # ===================== 配置参数 =====================
  34. # 本地省份权重
  35. LOCAL_PROVINCE = "湘"
  36. LOCAL_PROVINCE_WEIGHT = 1.25
  37. # 投票配置
  38. VOTE_WINDOW_SECONDS = 0.8
  39. VOTE_THRESHOLD = 1.2
  40. FAST_OUTPUT_THRESHOLD = 1.8
  41. ENABLE_FAST_OUTPUT = True
  42. # 性能配置
  43. IMG_SIZE = 320
  44. FRAME_SKIP = 3
  45. ENABLE_CLAHE = True
  46. ENABLE_MULTI_SCALE = False
  47. # 【修复】阈值配置 - 降低颜色阈值,兼容绿牌/黄牌
  48. DETECT_THRESH = 0.50 # 从0.55降到0.50
  49. COLOR_THRESH = 0.50 # 从0.65降到0.50(绿牌/黄牌颜色识别通常较低)
  50. REC_THRESH = 0.55 # 从0.60降到0.55
  51. PLATE_ASPECT_RATIO = 1.2
  52. # Redis配置
  53. REDIS_HOST = 'localhost'
  54. REDIS_PORT = 6379
  55. REDIS_DB = 0
  56. REDIS_PASSWORD = None
  57. REDIS_KEY = 'plate_results'
  58. WINDOW_SIZE = 5
  59. # 硬件配置
  60. SCREEN_HOST = '192.168.110.200'
  61. SCREEN_PORT = 5005
  62. RADAR_SCREEN_HOST = '192.168.110.199'
  63. RADAR_SCREEN_PORT = 5005
  64. if PLATFORM == 'Windows':
  65. RADAR_PORT = 'COM3'
  66. SPEAKER_PORT = 'COM4'
  67. elif PLATFORM == 'Linux':
  68. RADAR_PORT = '/dev/ttyACM0'
  69. SPEAKER_PORT = '/dev/ttyUSB0'
  70. else:
  71. RADAR_PORT = '/dev/ttyACM0'
  72. SPEAKER_PORT = '/dev/ttyUSB0'
  73. DEVICE_LOW_SPEED = 15
  74. MAX_RECONNECT_ATTEMPTS = 10
  75. RECONNECT_DELAY = 3
  76. MAX_CONSECUTIVE_FAILURES = 5
  77. output_lock = Lock()
  78. cache_lock = Lock()
  79. # 性能配置
  80. BATCH_REDIS_WRITE = True
  81. REDIS_CLEAN_INTERVAL = 30
  82. ASYNC_REDIS = True
  83. INFERENCE_HALF = True
  84. JIT_COMPILE = False
  85. THREAD_POOL_SIZE = 8
  86. # 【修复】扩展车牌正则 - 兼容绿牌(8位)、黄牌、警牌、使领馆牌等
  87. LICENSE_PLATE_PATTERN = re.compile(
  88. r'^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领A-Z]{1}' # 省份
  89. r'[A-Z0-9]{1}' # 第二位(黄牌可能是数字)
  90. r'[A-Z0-9DF]{5,7}$' # 剩余位
  91. )
  92. # 【修复】绿牌专用正则(8位新能源)
  93. GREEN_PLATE_PATTERN = re.compile(
  94. r'^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼]{1}'
  95. r'[A-Z]{1}'
  96. r'[A-Z0-9]{1}'
  97. r'[DF]{1}' # 新能源标识
  98. r'[A-Z0-9]{4}$'
  99. )
  100. # 【修复】黄牌正则(大型车、教练车等)
  101. YELLOW_PLATE_PATTERN = re.compile(
  102. r'^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼]{1}'
  103. r'[A-Z0-9]{1}'
  104. r'[A-Z0-9]{5}$'
  105. )
  106. # ===================== 全局变量 =====================
  107. redis_client = None
  108. redis_pool = None
  109. redis_lock = Lock()
  110. redis_write_queue = deque(maxlen=100)
  111. executor = ThreadPoolExecutor(max_workers=THREAD_POOL_SIZE)
  112. clean_error_count = 0
  113. redis_read_error = 0
  114. cap = None
  115. radar_serial = None
  116. screen = None
  117. radar_screen = None
  118. speaker = None
  119. plate_vote_cache = defaultdict(list)
  120. last_output_dict = {}
  121. displayed_plates = {}
  122. # 【修复】车牌类型统计
  123. plate_type_stats = {"蓝": 0, "绿": 0, "黄": 0, "其他": 0}
  124. # ===================== 图像增强 =====================
  125. def apply_clahe_fast(img: np.ndarray) -> np.ndarray:
  126. if img is None or img.size == 0:
  127. return img
  128. if len(img.shape) == 3:
  129. lab = cv2.cvtColor(img, cv2.COLOR_BGR2LAB)
  130. l, a, b = cv2.split(lab)
  131. clahe = cv2.createCLAHE(clipLimit=2.5, tileGridSize=(8, 8))
  132. cl = clahe.apply(l)
  133. enhanced = cv2.merge((cl, a, b))
  134. return cv2.cvtColor(enhanced, cv2.COLOR_LAB2BGR)
  135. else:
  136. clahe = cv2.createCLAHE(clipLimit=2.5, tileGridSize=(8, 8))
  137. return clahe.apply(img)
  138. # ===================== 坐标变换 =====================
  139. def scale_coords_landmarks(img1_shape, coords, img0_shape, ratio_pad=None):
  140. if ratio_pad is None:
  141. gain = min(img1_shape[0] / img0_shape[0], img1_shape[1] / img0_shape[1])
  142. pad = ((img1_shape[1] - img0_shape[1] * gain) / 2, (img1_shape[0] - img0_shape[0] * gain) / 2)
  143. else:
  144. gain = ratio_pad[0][0]
  145. pad = ratio_pad[1]
  146. coords[:, [0, 2, 4, 6]] -= pad[0]
  147. coords[:, [1, 3, 5, 7]] -= pad[1]
  148. coords[:, :8] /= gain
  149. coords[:, :8] = np.clip(coords[:, :8], 0, [img0_shape[1], img0_shape[0]] * 4)
  150. return coords
  151. def order_points(pts):
  152. if isinstance(pts, np.ndarray) and pts.size == 0:
  153. return np.zeros((4, 2), dtype="float32")
  154. rect = np.zeros((4, 2), dtype="float32")
  155. s = pts.sum(axis=1)
  156. rect[0] = pts[np.argmin(s)]
  157. rect[2] = pts[np.argmax(s)]
  158. diff = np.diff(pts, axis=1)
  159. rect[1] = pts[np.argmin(diff)]
  160. rect[3] = pts[np.argmax(diff)]
  161. return rect
  162. def four_point_transform(image, pts):
  163. if not isinstance(pts, np.ndarray) or pts.shape != (4, 2) or pts.size == 0:
  164. return image
  165. rect = order_points(pts)
  166. (tl, tr, br, bl) = rect
  167. widthA = np.sqrt(((br[0] - bl[0]) ** 2) + ((br[1] - bl[1]) ** 2))
  168. widthB = np.sqrt(((tr[0] - tl[0]) ** 2) + ((tr[1] - tl[1]) ** 2))
  169. maxWidth = max(int(widthA), int(widthB), 1)
  170. heightA = np.sqrt(((tr[0] - br[0]) ** 2) + ((tr[1] - br[1]) ** 2))
  171. heightB = np.sqrt(((tl[0] - bl[0]) ** 2) + ((tl[1] - bl[1]) ** 2))
  172. maxHeight = max(int(heightA), int(heightB), 1)
  173. dst = np.array([[0, 0], [maxWidth - 1, 0], [maxWidth - 1, maxHeight - 1], [0, maxHeight - 1]], dtype="float32")
  174. M = cv2.getPerspectiveTransform(rect, dst)
  175. return cv2.warpPerspective(image, M, (maxWidth, maxHeight))
  176. # ===================== 【修复】字符校验 - 兼容多种车牌 =====================
  177. def validate_plate_characters(plate_str: str, plate_color: str) -> tuple:
  178. if not plate_str or len(plate_str) < 5:
  179. return plate_str, False
  180. clean = plate_str.strip().upper().replace(' ', '')
  181. # 【修复】放宽第二位校验(黄牌可能是数字)
  182. if len(clean) >= 2 and not (clean[1].isalpha() or clean[1].isdigit()):
  183. return plate_str, False
  184. corrected = list(clean)
  185. for i in range(2, len(corrected)):
  186. if corrected[i] in ['O', 'Q']:
  187. corrected[i] = '0'
  188. elif corrected[i] in ['I', 'L']:
  189. corrected[i] = '1'
  190. corrected_str = ''.join(corrected)
  191. # 【修复】多种正则匹配
  192. if (LICENSE_PLATE_PATTERN.match(corrected_str) or
  193. GREEN_PLATE_PATTERN.match(corrected_str) or
  194. YELLOW_PLATE_PATTERN.match(corrected_str)):
  195. return corrected_str, True
  196. # 【修复】如果颜色是绿/黄,放宽长度要求
  197. if "绿" in plate_color and len(corrected_str) == 8:
  198. return corrected_str, True
  199. elif "黄" in plate_color and len(corrected_str) in [7, 8]:
  200. return corrected_str, True
  201. return plate_str, False
  202. # ===================== 【修复】方向判断 - 兼容绿牌/黄牌 =====================
  203. def is_valid_forward_plate(plate_str, bbox, plate_color=""):
  204. plate_clean = plate_str.strip().upper().replace(' ', '')
  205. # 【修复】放宽长度校验
  206. if len(plate_clean) < 7 or len(plate_clean) > 9:
  207. return False
  208. x1, y1, x2, y2 = bbox
  209. width = x2 - x1
  210. height = y2 - y1
  211. if height == 0:
  212. return False
  213. aspect_ratio = width / height
  214. # 【修复】绿牌判断逻辑优化
  215. is_green = ("绿" in plate_color or
  216. len(plate_clean) == 8 or
  217. any(c in plate_clean for c in ['D', 'F']) and len(plate_clean) >= 7)
  218. # 【修复】黄牌判断逻辑
  219. is_yellow = "黄" in plate_color
  220. if is_green:
  221. # 绿牌宽高比放宽
  222. if aspect_ratio < 1.3:
  223. return False
  224. elif is_yellow:
  225. # 黄牌通常是大型车,宽高比可能不同
  226. if aspect_ratio < 1.1:
  227. return False
  228. else:
  229. # 蓝牌
  230. if aspect_ratio < PLATE_ASPECT_RATIO:
  231. return False
  232. # 【修复】放宽正则校验
  233. if not (LICENSE_PLATE_PATTERN.match(plate_clean) or
  234. GREEN_PLATE_PATTERN.match(plate_clean) or
  235. len(plate_clean) in [7, 8, 9]):
  236. return False
  237. return True
  238. # ===================== 【修复】加权置信度 - 兼容所有车牌类型 =====================
  239. def calculate_weighted_confidence(plate_str: str, base_rec_probs: list, detect_conf: float,
  240. color_conf: float, plate_color: str = "") -> float:
  241. if not base_rec_probs:
  242. return 0.0
  243. rec_avg = np.mean(base_rec_probs)
  244. base_score = rec_avg * detect_conf * max(color_conf, 0.4) # 【修复】降低颜色权重下限
  245. # 本地省份权重(兼容所有颜色)
  246. if plate_str.startswith(LOCAL_PROVINCE):
  247. base_score *= LOCAL_PROVINCE_WEIGHT
  248. # 【修复】各种车牌类型加分
  249. clean_len = len(plate_str.replace(' ', ''))
  250. if "绿" in plate_color or clean_len == 8:
  251. base_score *= 1.08 # 绿牌加分
  252. elif "黄" in plate_color:
  253. base_score *= 1.08 # 黄牌加分
  254. elif "蓝" in plate_color and clean_len == 7:
  255. base_score *= 1.05
  256. return base_score
  257. # ===================== 【修复】车牌颜色识别增强 =====================
  258. def enhance_plate_color_detection(plate_color: str, color_conf: float, plate_no: str) -> tuple:
  259. """
  260. 增强车牌颜色判断
  261. 当颜色置信度低时,根据车牌号特征推断颜色
  262. """
  263. clean = plate_no.strip().upper().replace(' ', '')
  264. # 置信度太低时,根据特征推断
  265. if color_conf < 0.5:
  266. # 8位且含D/F = 新能源绿牌
  267. if len(clean) == 8 and any(c in clean for c in ['D', 'F']):
  268. return "绿牌", 0.75
  269. # 7位且第二位是字母 = 可能是蓝牌
  270. elif len(clean) == 7 and len(clean) >= 2 and clean[1].isalpha():
  271. return "蓝牌", 0.65
  272. # 其他情况 = 黄牌可能性
  273. elif len(clean) in [7, 8]:
  274. return "黄牌", 0.60
  275. # 颜色名称标准化
  276. color_map = {
  277. "绿": "绿牌", "绿色": "绿牌", "green": "绿牌",
  278. "蓝": "蓝牌", "蓝色": "蓝牌", "blue": "蓝牌",
  279. "黄": "黄牌", "黄色": "黄牌", "yellow": "黄牌",
  280. "白": "白牌", "白色": "白牌",
  281. "黑": "黑牌", "黑色": "黑牌"
  282. }
  283. for key, value in color_map.items():
  284. if key in plate_color.lower():
  285. return value, color_conf
  286. return plate_color if plate_color else "未知", color_conf
  287. # ===================== 快速识别 =====================
  288. def get_plate_rec_landmark_fast(img, xyxy, conf, landmarks, class_num, device,
  289. plate_rec_model, is_color=False):
  290. x1, y1, x2, y2 = map(int, np.ravel(xyxy))
  291. landmarks_np = np.array(np.ravel(landmarks)).reshape(4, 2).astype(int)
  292. rect = [x1, y1, x2, y2]
  293. roi_img = four_point_transform(img, landmarks_np)
  294. if int(class_num):
  295. roi_img = get_split_merge(roi_img)
  296. roi_resized = cv2.resize(roi_img, (128, 32), interpolation=cv2.INTER_LINEAR)
  297. if ENABLE_CLAHE:
  298. roi_enhanced = apply_clahe_fast(roi_resized)
  299. else:
  300. roi_enhanced = roi_resized
  301. if not is_color:
  302. plate_number, rec_prob = get_plate_result(roi_enhanced, device, plate_rec_model, is_color=False)
  303. plate_color = ""
  304. color_conf = 0.0
  305. else:
  306. plate_number, rec_prob, plate_color, color_conf = get_plate_result(
  307. roi_enhanced, device, plate_rec_model, is_color=True)
  308. if isinstance(rec_prob, np.ndarray):
  309. rec_prob = rec_prob.tolist()
  310. # 【修复】增强颜色识别
  311. plate_color, color_conf = enhance_plate_color_detection(plate_color, color_conf, plate_number)
  312. corrected_plate, is_valid = validate_plate_characters(plate_number, plate_color)
  313. if is_valid:
  314. plate_number = corrected_plate
  315. # 【修复】传入颜色参数
  316. final_weight = calculate_weighted_confidence(plate_number, rec_prob, conf,
  317. color_conf if color_conf else 0.4, plate_color)
  318. is_forward = is_valid_forward_plate(plate_number, rect, plate_color)
  319. return {
  320. "rect": rect,
  321. "detect_conf": conf,
  322. "landmarks": landmarks_np.tolist(),
  323. "plate_no": plate_number,
  324. "rec_conf_raw": rec_prob,
  325. "rec_conf_weighted": final_weight,
  326. "plate_color": plate_color,
  327. "color_conf": color_conf,
  328. "plate_type": class_num,
  329. "is_forward": is_forward
  330. }
  331. # ===================== 快速投票 =====================
  332. def vote_and_filter_plate_fast(plate_no: str, weight: float, current_time: float,
  333. plate_color: str = "") -> tuple:
  334. clean_plate = plate_no.replace(' ', '').upper()
  335. if len(clean_plate) < 5:
  336. return False, None, False
  337. vote_key = clean_plate[:5]
  338. with cache_lock:
  339. cache = plate_vote_cache[vote_key]
  340. cache[:] = [item for item in cache if current_time - item['time'] <= VOTE_WINDOW_SECONDS]
  341. new_entry = {'plate': clean_plate, 'weight': weight, 'time': current_time, 'color': plate_color}
  342. cache.append(new_entry)
  343. plate_scores = defaultdict(float)
  344. for item in cache:
  345. plate_scores[item['plate']] += item['weight']
  346. if not plate_scores:
  347. return False, None, False
  348. best_plate, best_score = max(plate_scores.items(), key=lambda x: x[1])
  349. # 【修复】绿牌/黄牌降低输出阈值
  350. threshold = VOTE_THRESHOLD
  351. if "绿" in plate_color or len(best_plate) == 8:
  352. threshold *= 0.75 # 绿牌更容易输出
  353. elif "黄" in plate_color:
  354. threshold *= 0.80 # 黄牌更容易输出
  355. elif best_plate.startswith(LOCAL_PROVINCE):
  356. threshold *= 0.85
  357. if ENABLE_FAST_OUTPUT and best_score >= FAST_OUTPUT_THRESHOLD:
  358. if best_plate not in displayed_plates or current_time - displayed_plates[best_plate] > 2.0:
  359. displayed_plates[best_plate] = current_time
  360. return True, best_plate, True
  361. if best_score >= threshold:
  362. if best_plate not in displayed_plates or current_time - displayed_plates[best_plate] > 2.0:
  363. displayed_plates[best_plate] = current_time
  364. return True, best_plate, False
  365. return False, None, False
  366. def check_duplicate_and_update(plate_no: str, current_time: float) -> bool:
  367. clean_plate = plate_no.replace(' ', '').upper()
  368. with output_lock:
  369. if clean_plate in last_output_dict:
  370. last_time = last_output_dict[clean_plate]
  371. cooldown = 1.5 if clean_plate.startswith(LOCAL_PROVINCE) else 2.0
  372. if current_time - last_time < cooldown:
  373. return False
  374. last_output_dict[clean_plate] = current_time
  375. return True
  376. # ===================== Redis 工具 =====================
  377. def get_current_time():
  378. return datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
  379. def get_current_timestamp():
  380. return int(time.time())
  381. def get_redis_client():
  382. global redis_client, redis_pool
  383. try:
  384. if redis_pool is None:
  385. redis_pool = redis.ConnectionPool(
  386. host=REDIS_HOST, port=REDIS_PORT, db=REDIS_DB,
  387. password=REDIS_PASSWORD, decode_responses=True,
  388. max_connections=50, socket_timeout=2,
  389. socket_connect_timeout=2, retry_on_timeout=True
  390. )
  391. if redis_client is None:
  392. redis_client = redis.Redis(connection_pool=redis_pool)
  393. redis_client.ping()
  394. return redis_client
  395. except Exception as e:
  396. redis_client = None
  397. return None
  398. def clean_expired_data_batch():
  399. client = get_redis_client()
  400. if not client:
  401. return 0
  402. try:
  403. with redis_lock:
  404. current_ts = get_current_timestamp()
  405. cutoff_ts = current_ts - WINDOW_SIZE
  406. zset_key = f"{REDIS_KEY}:sorted"
  407. expired = client.zrangebyscore(zset_key, 0, cutoff_ts)
  408. if expired:
  409. pipe = client.pipeline(transaction=False)
  410. pipe.hdel(f"{REDIS_KEY}:data", *expired)
  411. pipe.zremrangebyscore(zset_key, 0, cutoff_ts)
  412. pipe.execute()
  413. return len(expired)
  414. except Exception as e:
  415. global clean_error_count
  416. clean_error_count += 1
  417. return 0
  418. def save_to_redis_async(plate_no, plate_color, detect_conf, color_conf, rec_avg, direction="incoming"):
  419. try:
  420. client = get_redis_client()
  421. if not client:
  422. return False, "No Redis"
  423. timestamp = get_current_timestamp()
  424. entry = {
  425. "plate_no": plate_no.strip(),
  426. "plate_color": plate_color,
  427. "detect_conf": f"{detect_conf:.3f}",
  428. "color_conf": f"{color_conf:.3f}",
  429. "rec_avg": f"{rec_avg:.3f}",
  430. "timestamp": str(timestamp),
  431. "datetime": get_current_time(),
  432. "source": "rtsp_fast",
  433. "direction": direction
  434. }
  435. with redis_lock:
  436. if BATCH_REDIS_WRITE:
  437. redis_write_queue.append((timestamp, entry))
  438. if len(redis_write_queue) >= 10:
  439. flush_redis_queue(client)
  440. else:
  441. pipe = client.pipeline(transaction=False)
  442. pipe.hset(f"{REDIS_KEY}:data", timestamp, json.dumps(entry))
  443. pipe.zadd(f"{REDIS_KEY}:sorted", {timestamp: timestamp})
  444. pipe.execute()
  445. return True, "OK"
  446. except Exception as e:
  447. return False, str(e)
  448. def flush_redis_queue(client):
  449. if not redis_write_queue:
  450. return
  451. try:
  452. with redis_lock:
  453. if not redis_write_queue:
  454. return
  455. pipe = client.pipeline(transaction=False)
  456. for ts, data in redis_write_queue:
  457. pipe.hset(f"{REDIS_KEY}:data", ts, json.dumps(data))
  458. pipe.zadd(f"{REDIS_KEY}:sorted", {ts: ts})
  459. pipe.execute()
  460. redis_write_queue.clear()
  461. except Exception as e:
  462. print(f"Redis批量写入失败:{e}")
  463. # ===================== 初始化函数 =====================
  464. def connect_stream(stream_url, cap_options=""):
  465. global cap
  466. attempt = 0
  467. while attempt < MAX_RECONNECT_ATTEMPTS:
  468. try:
  469. print(f"[{get_current_time()}] 尝试连接视频流:{stream_url} (第{attempt + 1}次)")
  470. cap = cv2.VideoCapture()
  471. os.environ["OPENCV_FFMPEG_CAPTURE_OPTIONS"] = cap_options
  472. success = cap.open(stream_url, cv2.CAP_FFMPEG)
  473. if success:
  474. cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
  475. cap.set(cv2.CAP_PROP_FPS, 30)
  476. ret, frame = cap.read()
  477. if ret:
  478. print(f"[{get_current_time()}] ✅ 视频流连接成功")
  479. return cap, True
  480. cap.release()
  481. print(f"[{get_current_time()}] 连接失败,{RECONNECT_DELAY}秒后重试...")
  482. time.sleep(RECONNECT_DELAY)
  483. attempt += 1
  484. except Exception as e:
  485. print(f"[{get_current_time()}] 连接异常:{str(e)}")
  486. time.sleep(RECONNECT_DELAY)
  487. attempt += 1
  488. return None, False
  489. def init_speaker(port: str):
  490. attempts = 0
  491. while attempts < MAX_RECONNECT_ATTEMPTS:
  492. try:
  493. sp = IpCast(port=port)
  494. print(f"✅ 语音模块初始化成功 ({port})")
  495. return sp
  496. except Exception as e:
  497. attempts += 1
  498. if attempts < MAX_RECONNECT_ATTEMPTS:
  499. time.sleep(RECONNECT_DELAY)
  500. return None
  501. def init_screen(name: str, ip: str, port: int):
  502. sc = Screen(name=name, ip=ip, port=str(port))
  503. attempts = 0
  504. while attempts < MAX_RECONNECT_ATTEMPTS:
  505. if sc.get_live_state():
  506. print(f"✅ {name} 连接成功 ({ip}:{port})")
  507. return sc
  508. time.sleep(RECONNECT_DELAY)
  509. sc.reconnect()
  510. attempts += 1
  511. return None
  512. def init_screen_async(name: str, ip: str, port: int, result_dict: dict):
  513. screen = init_screen(name, ip, port)
  514. result_dict[name] = screen
  515. def load_model_optimized(weights, device):
  516. model = attempt_load(weights, map_location=device)
  517. if JIT_COMPILE and device.type != 'cpu':
  518. try:
  519. dummy = torch.rand(1, 3, 640, 640).to(device)
  520. if INFERENCE_HALF:
  521. dummy = dummy.half()
  522. model = torch.jit.trace(model, dummy)
  523. print("✅ 模型JIT编译成功")
  524. except Exception as e:
  525. print(f"JIT编译失败:{e}")
  526. if INFERENCE_HALF and device.type != 'cpu':
  527. model.half()
  528. model.eval()
  529. for param in model.parameters():
  530. param.requires_grad = False
  531. return model
  532. def detect_Recognition_plate_fast(model, orgimg, device, plate_rec_model, img_size, is_color=False):
  533. conf_thres = 0.25
  534. iou_thres = 0.5
  535. dict_list = []
  536. h0, w0 = orgimg.shape[:2]
  537. r = img_size / max(h0, w0)
  538. if abs(r - 1) > 0.1:
  539. interp = cv2.INTER_AREA if r < 1 else cv2.INTER_LINEAR
  540. img0 = cv2.resize(orgimg, (int(w0 * r), int(h0 * r)), interpolation=interp)
  541. else:
  542. img0 = orgimg
  543. imgsz = check_img_size(img_size, s=model.stride.max())
  544. img = letterbox(img0, new_shape=imgsz)[0]
  545. img = img[:, :, ::-1].transpose(2, 0, 1).copy()
  546. img = torch.from_numpy(img).to(device)
  547. img = img.float() / 255.0
  548. if INFERENCE_HALF and device.type != 'cpu':
  549. img = img.half()
  550. if img.ndim == 3:
  551. img = img.unsqueeze(0)
  552. with torch.no_grad():
  553. pred = model(img)[0]
  554. pred = non_max_suppression_face(pred, conf_thres, iou_thres)
  555. for det in pred:
  556. if len(det):
  557. det[:, :4] = scale_coords(img.shape[2:], det[:, :4], orgimg.shape).round()
  558. det[:, 5:13] = scale_coords_landmarks(img.shape[2:], det[:, 5:13], orgimg.shape).round()
  559. for j in range(det.size(0)):
  560. xyxy = det[j, :4].tolist()
  561. conf = det[j, 4].cpu().item()
  562. landmarks = det[j, 5:13].tolist()
  563. class_num = det[j, 13].cpu().item()
  564. if conf >= DETECT_THRESH:
  565. res = get_plate_rec_landmark_fast(
  566. orgimg, xyxy, conf, landmarks, class_num,
  567. device, plate_rec_model, is_color
  568. )
  569. dict_list.append(res)
  570. break
  571. break
  572. return dict_list
  573. # ===================== 主函数 =====================
  574. def start(image_path="imgs"):
  575. global screen, radar_screen, speaker, redis_client
  576. global REDIS_HOST, REDIS_PORT, REDIS_KEY, WINDOW_SIZE
  577. parser = argparse.ArgumentParser()
  578. parser.add_argument("--detect_model", nargs="+", type=str, default="weights/plate_detect.pt",
  579. help="检测模型路径")
  580. parser.add_argument("--rec_model", type=str, default="weights/plate_rec_color.pth",
  581. help="识别模型路径")
  582. parser.add_argument("--is_color", type=bool, default=True,
  583. help="是否识别车牌颜色")
  584. parser.add_argument("--img_size", type=int, default=IMG_SIZE,
  585. help="推理尺寸")
  586. parser.add_argument("--stream", type=str, default="",
  587. help="RTSP/RTMP流地址")
  588. parser.add_argument("--redis_host", type=str, default="localhost",
  589. help="Redis主机")
  590. parser.add_argument("--redis_port", type=int, default=6379,
  591. help="Redis端口")
  592. parser.add_argument("--redis_key", type=str, default="plate_results",
  593. help="Redis键名")
  594. parser.add_argument("--window_size", type=int, default=5,
  595. help="滑动窗口秒数")
  596. opt = parser.parse_args()
  597. REDIS_HOST = opt.redis_host
  598. REDIS_PORT = opt.redis_port
  599. REDIS_KEY = opt.redis_key
  600. WINDOW_SIZE = opt.window_size
  601. device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
  602. if device.type == 'cuda':
  603. torch.backends.cudnn.benchmark = True
  604. torch.backends.cuda.matmul.allow_tf32 = True
  605. redis_client = get_redis_client()
  606. if redis_client:
  607. print("✅ Redis连接成功")
  608. try:
  609. detect_model = load_model_optimized(opt.detect_model, device)
  610. plate_rec_model = init_model(device, opt.rec_model, is_color=opt.is_color)
  611. print(f"✅ 模型加载成功")
  612. except Exception as e:
  613. print(f"❌ 模型加载失败:{e}")
  614. return
  615. print(f"\n🚀 【速度优化模式 - 全车牌兼容】")
  616. print(f" 推理尺寸:{opt.img_size} | 帧跳过:{FRAME_SKIP} | 线程数:{THREAD_POOL_SIZE}")
  617. print(f" 本地省份:{LOCAL_PROVINCE} (权重x{LOCAL_PROVINCE_WEIGHT})")
  618. print(f" 阈值:检测={DETECT_THRESH} 颜色={COLOR_THRESH} 识别={REC_THRESH}")
  619. print(f" 支持:蓝牌✅ 绿牌✅ 黄牌✅ 其他✅\n")
  620. speaker = init_speaker(SPEAKER_PORT)
  621. screen_init_results = {}
  622. screen_threads = [
  623. threading.Thread(target=init_screen_async, args=("主屏幕", SCREEN_HOST, SCREEN_PORT, screen_init_results),
  624. daemon=True),
  625. threading.Thread(target=init_screen_async,
  626. args=("雷达屏幕", RADAR_SCREEN_HOST, RADAR_SCREEN_PORT, screen_init_results), daemon=True)
  627. ]
  628. for t in screen_threads:
  629. t.start()
  630. for t in screen_threads:
  631. t.join(timeout=30)
  632. screen = screen_init_results.get("主屏幕")
  633. radar_screen = screen_init_results.get("雷达屏幕")
  634. if radar_screen is not None:
  635. if not hasattr(radar_screen, 'dyna_area_num'):
  636. radar_screen.dyna_area_num = 0
  637. DeviceInitData.LowSpeed = DEVICE_LOW_SPEED
  638. try:
  639. threading.Thread(target=open_serial, args=(RADAR_PORT, speaker, radar_screen), daemon=True).start()
  640. print("✅ 雷达线程启动")
  641. except Exception as e:
  642. print(f"⚠️ 雷达启动失败:{e}")
  643. if opt.stream:
  644. cap_options = "rtsp_transport=tcp;buffer_size=32000;probesize=32;analyzeduration=0;fflags=nobuffer;flags=low_delay"
  645. cap, connected = connect_stream(opt.stream, cap_options)
  646. if not connected:
  647. print(f"[{get_current_time()}] 初始连接失败,退出程序")
  648. return
  649. consecutive_failures = 0
  650. reconnect_count = 0
  651. frame_count = 0
  652. processed_count = 0
  653. last_print_time = time.time()
  654. inference_times = deque(maxlen=30)
  655. last_output_dict.clear()
  656. plate_vote_cache.clear()
  657. displayed_plates.clear()
  658. plate_type_stats.clear()
  659. plate_type_stats.update({"蓝": 0, "绿": 0, "黄": 0, "其他": 0})
  660. stats = {"in": 0, "out": 0, "total": 0, "fast_output": 0, "vote_output": 0}
  661. try:
  662. while True:
  663. frame_count += 1
  664. ret, frame = cap.read()
  665. if not ret:
  666. consecutive_failures += 1
  667. if consecutive_failures >= MAX_CONSECUTIVE_FAILURES:
  668. cap, reconnected = connect_stream(opt.stream, cap_options)
  669. if reconnected:
  670. reconnect_count += 1
  671. consecutive_failures = 0
  672. frame_count = 0
  673. else:
  674. break
  675. continue
  676. consecutive_failures = 0
  677. if frame_count % FRAME_SKIP != 0:
  678. continue
  679. processed_count += 1
  680. inference_start = time.time()
  681. try:
  682. dict_list = detect_Recognition_plate_fast(
  683. detect_model, frame, device, plate_rec_model, opt.img_size, is_color=opt.is_color
  684. )
  685. inference_times.append(time.time() - inference_start)
  686. current_time = time.time()
  687. for res in dict_list:
  688. plate_no = res['plate_no'].strip()
  689. if len(plate_no) < 4:
  690. continue
  691. weight = res['rec_conf_weighted']
  692. detect_conf = res['detect_conf']
  693. color_conf = res.get('color_conf', 0.0)
  694. plate_color = res.get('plate_color', '未知')
  695. # 【修复】降低阈值,兼容绿牌/黄牌
  696. if weight < 0.25:
  697. continue
  698. should_output, final_plate, is_fast = vote_and_filter_plate_fast(
  699. plate_no, weight, current_time, plate_color)
  700. if should_output and final_plate:
  701. if is_fast:
  702. stats["fast_output"] += 1
  703. else:
  704. stats["vote_output"] += 1
  705. if check_duplicate_and_update(final_plate, current_time):
  706. is_in = res.get('is_forward', False)
  707. direction = "incoming" if is_in else "outgoing"
  708. if is_in:
  709. stats["in"] += 1
  710. else:
  711. stats["out"] += 1
  712. stats["total"] += 1
  713. # 【修复】统计车牌类型
  714. if "绿" in plate_color or len(final_plate) == 8:
  715. plate_type_stats["绿"] += 1
  716. color_display = "🟢绿牌"
  717. elif "黄" in plate_color:
  718. plate_type_stats["黄"] += 1
  719. color_display = "🟡黄牌"
  720. elif "蓝" in plate_color:
  721. plate_type_stats["蓝"] += 1
  722. color_display = "🔵蓝牌"
  723. else:
  724. plate_type_stats["其他"] += 1
  725. color_display = f"⚪{plate_color}"
  726. print(f"[{get_current_time()}] {'⚡' if is_fast else '✅'} {final_plate} | "
  727. f"{color_display} | W:{weight:.2f} | {'IN' if is_in else 'OUT'}")
  728. if redis_client:
  729. executor.submit(save_to_redis_async, final_plate, plate_color,
  730. detect_conf, color_conf, weight / LOCAL_PROVINCE_WEIGHT, direction)
  731. if screen:
  732. try:
  733. ff = FlashFile()
  734. ff.set_msg(final_plate, 1)
  735. ff.set_mode(4, 1)
  736. ff.set_origin(0, True, 0)
  737. ff.set_area(128, True, 32)
  738. screen.text_ram(ff, True)
  739. except Exception as e:
  740. pass
  741. except Exception as e:
  742. continue
  743. if frame_count % REDIS_CLEAN_INTERVAL == 0 and redis_client:
  744. executor.submit(clean_expired_data_batch)
  745. if frame_count % 15 == 0 and redis_client:
  746. executor.submit(flush_redis_queue, get_redis_client())
  747. if time.time() - last_print_time >= 15:
  748. avg_inference = sum(inference_times) / len(inference_times) if inference_times else 0
  749. fps = 1 / avg_inference if avg_inference > 0 else 0
  750. print(f"\n--- 状态 --- 帧:{frame_count} | 识别:{stats['total']} | "
  751. f"蓝:{plate_type_stats['蓝']} 绿:{plate_type_stats['绿']} 黄:{plate_type_stats['黄']} 其他:{plate_type_stats['其他']} | "
  752. f"耗时:{avg_inference * 1000:.1f}ms | FPS:{fps:.1f}")
  753. last_print_time = time.time()
  754. if cv2.waitKey(1) & 0xFF == ord('q'):
  755. break
  756. except KeyboardInterrupt:
  757. print(f"\n[{get_current_time()}] 用户中断")
  758. finally:
  759. if cap:
  760. cap.release()
  761. cv2.destroyAllWindows()
  762. executor.shutdown(wait=True)
  763. if redis_client:
  764. flush_redis_queue(get_redis_client())
  765. clean_expired_data_batch()
  766. print(f"\n结束。总识别:{stats['total']}")
  767. print(
  768. f"车牌类型:蓝:{plate_type_stats['蓝']} 绿:{plate_type_stats['绿']} 黄:{plate_type_stats['黄']} 其他:{plate_type_stats['其他']}")
  769. if __name__ == '__main__':
  770. start()