|
|
@@ -0,0 +1,925 @@
|
|
|
+import argparse
|
|
|
+import copy
|
|
|
+import math
|
|
|
+import os
|
|
|
+import platform
|
|
|
+import re
|
|
|
+import threading
|
|
|
+import time
|
|
|
+from collections import deque
|
|
|
+from datetime import datetime
|
|
|
+from pprint import pprint
|
|
|
+from concurrent.futures import ThreadPoolExecutor
|
|
|
+from threading import Lock
|
|
|
+
|
|
|
+import cv2
|
|
|
+import numpy as np
|
|
|
+import torch
|
|
|
+import redis
|
|
|
+import serial
|
|
|
+from typing import Optional, List
|
|
|
+
|
|
|
+# 平台检测
|
|
|
+PLATFORM = platform.system()
|
|
|
+
|
|
|
+import sys
|
|
|
+
|
|
|
+# 提前导入并减少重复导入
|
|
|
+# 注意:请确保这些模块的路径正确,若有导入错误需调整路径
|
|
|
+try:
|
|
|
+ from models.experimental import attempt_load
|
|
|
+ from modules.audio.speaker import IpCast
|
|
|
+ from modules.display.screen import Screen, FlashFile
|
|
|
+ from modules.radar.radar import RadarData, DeviceInitData, parse_radar_frame, open_serial
|
|
|
+ from plate_recognition.double_plate_split_merge import get_split_merge
|
|
|
+ from plate_recognition.plate_rec import (
|
|
|
+ allFilePath,
|
|
|
+ cv_imread,
|
|
|
+ get_plate_result,
|
|
|
+ init_model,
|
|
|
+ )
|
|
|
+ from utils.datasets import letterbox
|
|
|
+ from utils.general import check_img_size, non_max_suppression_face, scale_coords
|
|
|
+except ImportError as e:
|
|
|
+ print(f"导入模块失败: {e},请检查模块路径是否正确")
|
|
|
+ sys.exit(1)
|
|
|
+
|
|
|
+# ===================== 全局变量初始化(完整保留原功能) =====================
|
|
|
+# Redis连接配置
|
|
|
+REDIS_HOST = 'localhost'
|
|
|
+REDIS_PORT = 6379
|
|
|
+REDIS_DB = 0
|
|
|
+REDIS_PASSWORD = None
|
|
|
+REDIS_KEY = 'plate_results'
|
|
|
+WINDOW_SIZE = 5
|
|
|
+
|
|
|
+# 屏幕连接配置
|
|
|
+SCREEN_HOST = '192.168.110.200' # 主屏幕:显示车牌识别信息
|
|
|
+SCREEN_PORT = 5005
|
|
|
+RADAR_SCREEN_HOST = '192.168.110.199' # 雷达屏幕:显示雷达速度信息
|
|
|
+RADAR_SCREEN_PORT = 5005
|
|
|
+
|
|
|
+# 根据平台自动选择串口路径
|
|
|
+if PLATFORM == 'Windows':
|
|
|
+ RADAR_PORT = 'COM3'
|
|
|
+ SPEAKER_PORT = 'COM4'
|
|
|
+elif PLATFORM == 'Linux':
|
|
|
+ RADAR_PORT = '/dev/ttyACM0'
|
|
|
+ SPEAKER_PORT = '/dev/ttyUSB0'
|
|
|
+else:
|
|
|
+ RADAR_PORT = '/dev/ttyACM0'
|
|
|
+ SPEAKER_PORT = '/dev/ttyUSB0'
|
|
|
+
|
|
|
+DEVICE_LOW_SPEED = 15
|
|
|
+
|
|
|
+# 重连相关配置
|
|
|
+MAX_RECONNECT_ATTEMPTS = 10
|
|
|
+RECONNECT_DELAY = 5
|
|
|
+MAX_CONSECUTIVE_FAILURES = 5
|
|
|
+
|
|
|
+# 阈值设置
|
|
|
+DETECT_THRESH = 0.65
|
|
|
+COLOR_THRESH = 0.85
|
|
|
+REC_THRESH = 0.85
|
|
|
+PLATE_ASPECT_RATIO = 1.8 # 车牌宽高比(正向>1.8,反向<1.2)
|
|
|
+
|
|
|
+# 性能优化参数
|
|
|
+FRAME_SKIP = 2
|
|
|
+BATCH_REDIS_WRITE = True
|
|
|
+REDIS_CLEAN_INTERVAL = 20
|
|
|
+ASYNC_REDIS = True
|
|
|
+INFERENCE_HALF = True
|
|
|
+JIT_COMPILE = False
|
|
|
+THREAD_POOL_SIZE = 5
|
|
|
+
|
|
|
+# 合法车牌正则
|
|
|
+LICENSE_PLATE_PATTERN = re.compile(
|
|
|
+ r'^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领A-Z]{1}[A-Z]{1}[A-Z0-9]{5,7}$')
|
|
|
+
|
|
|
+# 全局变量
|
|
|
+redis_client = None
|
|
|
+redis_lock = Lock()
|
|
|
+redis_write_queue = deque(maxlen=100)
|
|
|
+executor = ThreadPoolExecutor(max_workers=THREAD_POOL_SIZE)
|
|
|
+clean_error_count = 0
|
|
|
+redis_read_error = 0
|
|
|
+cap = None
|
|
|
+radar_serial = None
|
|
|
+
|
|
|
+# 屏幕/语音全局实例(关键:明确区分主屏幕和雷达屏幕)
|
|
|
+screen = None # 主屏幕实例(192.168.110.199)
|
|
|
+radar_screen = None # 雷达屏幕实例(192.168.110.198)
|
|
|
+speaker = None # 语音实例
|
|
|
+
|
|
|
+
|
|
|
+# ===================== 核心依赖函数 =====================
|
|
|
+def scale_coords_landmarks(img1_shape, coords, img0_shape, ratio_pad=None):
|
|
|
+ """车牌关键点坐标还原到原图"""
|
|
|
+ if ratio_pad is None:
|
|
|
+ gain = min(img1_shape[0] / img0_shape[0], img1_shape[1] / img0_shape[1])
|
|
|
+ pad = ((img1_shape[1] - img0_shape[1] * gain) / 2, (img1_shape[0] - img0_shape[0] * gain) / 2)
|
|
|
+ else:
|
|
|
+ gain = ratio_pad[0][0]
|
|
|
+ pad = ratio_pad[1]
|
|
|
+
|
|
|
+ coords[:, [0, 2, 4, 6]] -= pad[0]
|
|
|
+ coords[:, [1, 3, 5, 7]] -= pad[1]
|
|
|
+ coords[:, :8] /= gain
|
|
|
+
|
|
|
+ coords[:, 0] = coords[:, 0].clip(0, img0_shape[1])
|
|
|
+ coords[:, 1] = coords[:, 1].clip(0, img0_shape[0])
|
|
|
+ coords[:, 2] = coords[:, 2].clip(0, img0_shape[1])
|
|
|
+ coords[:, 3] = coords[:, 3].clip(0, img0_shape[0])
|
|
|
+ coords[:, 4] = coords[:, 4].clip(0, img0_shape[1])
|
|
|
+ coords[:, 5] = coords[:, 5].clip(0, img0_shape[0])
|
|
|
+ coords[:, 6] = coords[:, 6].clip(0, img0_shape[1])
|
|
|
+ coords[:, 7] = coords[:, 7].clip(0, img0_shape[0])
|
|
|
+ return coords
|
|
|
+
|
|
|
+
|
|
|
+def order_points(pts):
|
|
|
+ """排序四点坐标"""
|
|
|
+ if isinstance(pts, np.ndarray) and pts.size == 0:
|
|
|
+ return np.zeros((4, 2), dtype="float32")
|
|
|
+
|
|
|
+ rect = np.zeros((4, 2), dtype="float32")
|
|
|
+ s = pts.sum(axis=1)
|
|
|
+ rect[0] = pts[np.argmin(s)]
|
|
|
+ rect[2] = pts[np.argmax(s)]
|
|
|
+ diff = np.diff(pts, axis=1)
|
|
|
+ rect[1] = pts[np.argmin(diff)]
|
|
|
+ rect[3] = pts[np.argmax(diff)]
|
|
|
+ return rect
|
|
|
+
|
|
|
+
|
|
|
+def four_point_transform(image, pts):
|
|
|
+ """四点透视变换"""
|
|
|
+ if not isinstance(pts, np.ndarray) or pts.shape != (4, 2) or pts.size == 0:
|
|
|
+ return image
|
|
|
+
|
|
|
+ rect = order_points(pts)
|
|
|
+ (tl, tr, br, bl) = rect
|
|
|
+
|
|
|
+ widthA = np.sqrt(((br[0] - bl[0]) ** 2) + ((br[1] - bl[1]) ** 2))
|
|
|
+ widthB = np.sqrt(((tr[0] - tl[0]) ** 2) + ((tr[1] - tl[1]) ** 2))
|
|
|
+ maxWidth = max(int(widthA) if widthA > 0 else 1, int(widthB) if widthB > 0 else 1)
|
|
|
+
|
|
|
+ heightA = np.sqrt(((tr[0] - br[0]) ** 2) + ((tr[1] - br[1]) ** 2))
|
|
|
+ heightB = np.sqrt(((tl[0] - bl[0]) ** 2) + ((tl[1] - bl[1]) ** 2))
|
|
|
+ maxHeight = max(int(heightA) if heightA > 0 else 1, int(heightB) if heightB > 0 else 1)
|
|
|
+
|
|
|
+ dst = np.array([[0, 0], [maxWidth - 1, 0], [maxWidth - 1, maxHeight - 1], [0, maxHeight - 1]], dtype="float32")
|
|
|
+ M = cv2.getPerspectiveTransform(rect, dst)
|
|
|
+ warped = cv2.warpPerspective(image, M, (maxWidth, maxHeight))
|
|
|
+ return warped
|
|
|
+
|
|
|
+
|
|
|
+def is_valid_forward_plate(plate_str, bbox):
|
|
|
+ """判断是否为来向车(正向车牌)"""
|
|
|
+ plate_clean = plate_str.strip().upper().replace(' ', '')
|
|
|
+ if len(plate_clean) < 7 or len(plate_clean) > 8:
|
|
|
+ return False
|
|
|
+
|
|
|
+ x1, y1, x2, y2 = bbox
|
|
|
+ width = x2 - x1
|
|
|
+ height = y2 - y1
|
|
|
+ if height == 0 or (width / height) < PLATE_ASPECT_RATIO:
|
|
|
+ return False
|
|
|
+
|
|
|
+ if not LICENSE_PLATE_PATTERN.match(plate_clean):
|
|
|
+ return False
|
|
|
+
|
|
|
+ return True
|
|
|
+
|
|
|
+
|
|
|
+def get_plate_rec_landmark(img, xyxy, conf, landmarks, class_num, device, plate_rec_model, is_color=False):
|
|
|
+ """车牌识别核心函数"""
|
|
|
+ h, w, _ = img.shape
|
|
|
+ result_dict = {}
|
|
|
+
|
|
|
+ x1, y1, x2, y2 = map(int, np.ravel(xyxy))
|
|
|
+ landmarks = np.ravel(landmarks)
|
|
|
+ landmarks_np = np.array(landmarks).reshape(4, 2).astype(int)
|
|
|
+ rect = [x1, y1, x2, y2]
|
|
|
+
|
|
|
+ # 透视变换获取车牌ROI
|
|
|
+ class_label = int(class_num)
|
|
|
+ roi_img = four_point_transform(img, landmarks_np)
|
|
|
+ if class_label:
|
|
|
+ roi_img = get_split_merge(roi_img)
|
|
|
+
|
|
|
+ # 识别车牌号
|
|
|
+ if not is_color:
|
|
|
+ plate_number, rec_prob = get_plate_result(roi_img, device, plate_rec_model, is_color=is_color)
|
|
|
+ plate_color = ""
|
|
|
+ color_conf = 0.0
|
|
|
+ else:
|
|
|
+ plate_number, rec_prob, plate_color, color_conf = get_plate_result(roi_img, device, plate_rec_model,
|
|
|
+ is_color=is_color)
|
|
|
+
|
|
|
+ # 修复rec_prob格式
|
|
|
+ if isinstance(rec_prob, np.ndarray):
|
|
|
+ rec_prob = rec_prob.tolist()
|
|
|
+
|
|
|
+ # 判断是否为来向车
|
|
|
+ is_forward = is_valid_forward_plate(plate_number, rect)
|
|
|
+
|
|
|
+ # 组装结果
|
|
|
+ result_dict.update({
|
|
|
+ "rect": rect,
|
|
|
+ "detect_conf": conf,
|
|
|
+ "landmarks": landmarks_np.tolist(),
|
|
|
+ "plate_no": plate_number,
|
|
|
+ "rec_conf": rec_prob,
|
|
|
+ "roi_height": roi_img.shape[0],
|
|
|
+ "plate_color": plate_color,
|
|
|
+ "color_conf": color_conf,
|
|
|
+ "plate_type": class_num,
|
|
|
+ "is_forward": is_forward
|
|
|
+ })
|
|
|
+ return result_dict
|
|
|
+
|
|
|
+
|
|
|
+def get_window_info():
|
|
|
+ """获取Redis窗口统计信息"""
|
|
|
+ if redis_client is None:
|
|
|
+ return {"count": 0, "window_size": WINDOW_SIZE}
|
|
|
+
|
|
|
+ try:
|
|
|
+ clean_expired_data_batch()
|
|
|
+
|
|
|
+ hash_key = f"{REDIS_KEY}:data"
|
|
|
+ zset_key = f"{REDIS_KEY}:sorted"
|
|
|
+
|
|
|
+ data_count = redis_client.hlen(hash_key)
|
|
|
+ sorted_count = redis_client.zcard(zset_key)
|
|
|
+ oldest_ts = newest_ts = int(time.time())
|
|
|
+ time_range = 0
|
|
|
+
|
|
|
+ if sorted_count > 0:
|
|
|
+ timestamps_with_scores = redis_client.zrange(zset_key, 0, -1, withscores=True)
|
|
|
+ if timestamps_with_scores:
|
|
|
+ timestamps = []
|
|
|
+ for _, score in timestamps_with_scores:
|
|
|
+ try:
|
|
|
+ timestamps.append(int(float(score)))
|
|
|
+ except:
|
|
|
+ continue
|
|
|
+ if len(timestamps) > 0:
|
|
|
+ oldest_ts = min(timestamps)
|
|
|
+ newest_ts = max(timestamps)
|
|
|
+ time_range = newest_ts - oldest_ts
|
|
|
+
|
|
|
+ return {
|
|
|
+ "count": data_count,
|
|
|
+ "window_size": WINDOW_SIZE,
|
|
|
+ "time_range": time_range,
|
|
|
+ "oldest_record": datetime.fromtimestamp(oldest_ts).strftime("%H:%M:%S") if sorted_count > 0 else "无",
|
|
|
+ "newest_record": datetime.fromtimestamp(newest_ts).strftime("%H:%M:%S") if sorted_count > 0 else "无"
|
|
|
+ }
|
|
|
+ except Exception as e:
|
|
|
+ print(f"获取窗口信息失败: {e}")
|
|
|
+ return {"count": 0, "window_size": WINDOW_SIZE}
|
|
|
+
|
|
|
+
|
|
|
+# ===================== 工具函数 =====================
|
|
|
+def get_current_time():
|
|
|
+ """获取格式化当前时间"""
|
|
|
+ return datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
|
|
|
+
|
|
|
+
|
|
|
+def get_current_timestamp():
|
|
|
+ """获取秒级时间戳"""
|
|
|
+ return int(time.time())
|
|
|
+
|
|
|
+
|
|
|
+def connect_stream(stream_url, cap_options=""):
|
|
|
+ """建立视频流连接,带重试机制"""
|
|
|
+ global cap
|
|
|
+ attempt = 0
|
|
|
+
|
|
|
+ while attempt < MAX_RECONNECT_ATTEMPTS:
|
|
|
+ try:
|
|
|
+ print(f"[{get_current_time()}] 尝试连接视频流: {stream_url} (第{attempt + 1}次)")
|
|
|
+
|
|
|
+ if cap_options:
|
|
|
+ cap = cv2.VideoCapture()
|
|
|
+ os.environ["OPENCV_FFMPEG_CAPTURE_OPTIONS"] = cap_options
|
|
|
+ success = cap.open(stream_url, cv2.CAP_FFMPEG)
|
|
|
+ else:
|
|
|
+ cap = cv2.VideoCapture(stream_url)
|
|
|
+ success = cap.isOpened()
|
|
|
+
|
|
|
+ if success:
|
|
|
+ cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
|
|
|
+ cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc('H', '2', '6', '4'))
|
|
|
+
|
|
|
+ ret, frame = cap.read()
|
|
|
+ if ret:
|
|
|
+ print(f"[{get_current_time()}] 视频流连接成功")
|
|
|
+ return cap, True
|
|
|
+ else:
|
|
|
+ print(f"[{get_current_time()}] 视频流打开但无法读取帧")
|
|
|
+ cap.release()
|
|
|
+ else:
|
|
|
+ print(f"[{get_current_time()}] 无法打开视频流")
|
|
|
+
|
|
|
+ print(f"[{get_current_time()}] 连接失败,{RECONNECT_DELAY}秒后重试...")
|
|
|
+ time.sleep(RECONNECT_DELAY)
|
|
|
+ attempt += 1
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ print(f"[{get_current_time()}] 连接异常: {str(e)}")
|
|
|
+ time.sleep(RECONNECT_DELAY)
|
|
|
+ attempt += 1
|
|
|
+
|
|
|
+ print(f"[{get_current_time()}] 达到最大重连次数({MAX_RECONNECT_ATTEMPTS}),退出")
|
|
|
+ return None, False
|
|
|
+
|
|
|
+
|
|
|
+def reconnect_stream(stream_url, cap_options=""):
|
|
|
+ """重新连接视频流"""
|
|
|
+ global cap
|
|
|
+ print(f"[{get_current_time()}] 开始重新连接视频流...")
|
|
|
+
|
|
|
+ if cap is not None:
|
|
|
+ cap.release()
|
|
|
+ time.sleep(2)
|
|
|
+
|
|
|
+ return connect_stream(stream_url, cap_options)
|
|
|
+
|
|
|
+
|
|
|
+# ===================== Redis操作 =====================
|
|
|
+def clean_expired_data_batch():
|
|
|
+ """批量清理过期Redis数据"""
|
|
|
+ if redis_client is None:
|
|
|
+ return 0
|
|
|
+
|
|
|
+ try:
|
|
|
+ with redis_lock:
|
|
|
+ current_ts = get_current_timestamp()
|
|
|
+ cutoff_ts = current_ts - WINDOW_SIZE
|
|
|
+
|
|
|
+ pipe = redis_client.pipeline(transaction=False)
|
|
|
+ zset_key = f"{REDIS_KEY}:sorted"
|
|
|
+ expired_timestamps = redis_client.zrangebyscore(zset_key, 0, cutoff_ts)
|
|
|
+
|
|
|
+ if expired_timestamps:
|
|
|
+ hash_key = f"{REDIS_KEY}:data"
|
|
|
+ pipe.hdel(hash_key, *expired_timestamps)
|
|
|
+ pipe.zremrangebyscore(zset_key, 0, cutoff_ts)
|
|
|
+ pipe.execute()
|
|
|
+ return len(expired_timestamps)
|
|
|
+ return 0
|
|
|
+ except Exception as e:
|
|
|
+ global clean_error_count
|
|
|
+ clean_error_count += 1
|
|
|
+ if clean_error_count % 10 == 0:
|
|
|
+ print(f"清理过期数据失败({clean_error_count}次): {e}")
|
|
|
+ return 0
|
|
|
+
|
|
|
+
|
|
|
+def save_to_redis_async(plate_no, plate_color, detect_conf, color_conf, rec_avg, direction="incoming"):
|
|
|
+ """异步写入Redis"""
|
|
|
+ try:
|
|
|
+ timestamp = get_current_timestamp()
|
|
|
+ timestamp_ms = int(time.time() * 1000)
|
|
|
+
|
|
|
+ entry_data = {
|
|
|
+ "plate_no": plate_no.strip(),
|
|
|
+ "plate_color": plate_color,
|
|
|
+ "detect_conf": f"{detect_conf:.3f}",
|
|
|
+ "color_conf": f"{color_conf:.3f}",
|
|
|
+ "rec_avg": f"{rec_avg:.3f}",
|
|
|
+ "timestamp": str(timestamp),
|
|
|
+ "timestamp_ms": str(timestamp_ms),
|
|
|
+ "datetime": get_current_time(),
|
|
|
+ "source": "rtsp_stream",
|
|
|
+ "direction": direction
|
|
|
+ }
|
|
|
+
|
|
|
+ with redis_lock:
|
|
|
+ if BATCH_REDIS_WRITE:
|
|
|
+ redis_write_queue.append((timestamp, entry_data))
|
|
|
+ if len(redis_write_queue) >= 10:
|
|
|
+ flush_redis_queue()
|
|
|
+ else:
|
|
|
+ pipe = redis_client.pipeline(transaction=False)
|
|
|
+ hash_key = f"{REDIS_KEY}:data"
|
|
|
+ zset_key = f"{REDIS_KEY}:sorted"
|
|
|
+ pipe.hset(hash_key, timestamp, str(entry_data))
|
|
|
+ pipe.zadd(zset_key, {timestamp: timestamp})
|
|
|
+ pipe.execute()
|
|
|
+ return True, f"加入队列: {timestamp}"
|
|
|
+ except Exception as e:
|
|
|
+ return False, f"异步写入失败: {e}"
|
|
|
+
|
|
|
+
|
|
|
+def flush_redis_queue():
|
|
|
+ """刷入Redis队列数据"""
|
|
|
+ if not redis_write_queue or redis_client is None:
|
|
|
+ return False
|
|
|
+
|
|
|
+ try:
|
|
|
+ with redis_lock:
|
|
|
+ if not redis_write_queue:
|
|
|
+ return True
|
|
|
+
|
|
|
+ pipe = redis_client.pipeline(transaction=False)
|
|
|
+ hash_key = f"{REDIS_KEY}:data"
|
|
|
+ zset_key = f"{REDIS_KEY}:sorted"
|
|
|
+
|
|
|
+ for timestamp, entry_data in redis_write_queue:
|
|
|
+ pipe.hset(hash_key, timestamp, str(entry_data))
|
|
|
+ pipe.zadd(zset_key, {timestamp: timestamp})
|
|
|
+
|
|
|
+ pipe.execute()
|
|
|
+ redis_write_queue.clear()
|
|
|
+ return True
|
|
|
+ except Exception as e:
|
|
|
+ print(f"批量写入Redis失败: {e}")
|
|
|
+ return False
|
|
|
+
|
|
|
+
|
|
|
+def get_recent_plates_from_redis():
|
|
|
+ """获取最近5秒的车牌记录"""
|
|
|
+ if redis_client is None:
|
|
|
+ return []
|
|
|
+
|
|
|
+ try:
|
|
|
+ zset_key = f"{REDIS_KEY}:sorted"
|
|
|
+ hash_key = f"{REDIS_KEY}:data"
|
|
|
+
|
|
|
+ current_ts = get_current_timestamp()
|
|
|
+ cutoff_ts = current_ts - WINDOW_SIZE
|
|
|
+ recent_timestamps = redis_client.zrevrangebyscore(zset_key, current_ts, cutoff_ts)
|
|
|
+
|
|
|
+ results = []
|
|
|
+ if recent_timestamps:
|
|
|
+ entries = redis_client.hmget(hash_key, recent_timestamps)
|
|
|
+ for ts, entry_str in zip(recent_timestamps, entries):
|
|
|
+ if entry_str:
|
|
|
+ try:
|
|
|
+ data = eval(entry_str)
|
|
|
+ results.append({
|
|
|
+ 'timestamp': int(ts),
|
|
|
+ 'data': data
|
|
|
+ })
|
|
|
+ except:
|
|
|
+ continue
|
|
|
+ return results
|
|
|
+ except Exception as e:
|
|
|
+ global redis_read_error
|
|
|
+ redis_read_error += 1
|
|
|
+ if redis_read_error % 10 == 0:
|
|
|
+ print(f"从Redis读取数据失败({redis_read_error}次): {e}")
|
|
|
+ return []
|
|
|
+
|
|
|
+
|
|
|
+# ===================== 初始化语音/屏幕 =====================
|
|
|
+def init_speaker(port: str) -> IpCast | None:
|
|
|
+ """初始化语音模块(带异常处理和重试)"""
|
|
|
+ attempts = 0
|
|
|
+ while attempts < MAX_RECONNECT_ATTEMPTS:
|
|
|
+ try:
|
|
|
+ speaker = IpCast(port=port)
|
|
|
+ print(f"✅ 语音模块初始化成功(串口:{port})")
|
|
|
+ return speaker
|
|
|
+ except Exception as e:
|
|
|
+ attempts += 1
|
|
|
+ if attempts < MAX_RECONNECT_ATTEMPTS:
|
|
|
+ print(f"⚠️ 语音模块初始化失败:{e},{RECONNECT_DELAY}秒后重试({attempts}/{MAX_RECONNECT_ATTEMPTS})")
|
|
|
+ time.sleep(RECONNECT_DELAY)
|
|
|
+ else:
|
|
|
+ print(f"❌ 语音模块初始化失败:{e},已达到最大重试次数")
|
|
|
+ return None
|
|
|
+
|
|
|
+
|
|
|
+def init_screen(name: str, ip: str, port: int) -> Screen | None:
|
|
|
+ """初始化屏幕(带连接重试)"""
|
|
|
+ screen = Screen(name=name, ip=ip, port=str(port))
|
|
|
+ attempts = 0
|
|
|
+ while attempts < MAX_RECONNECT_ATTEMPTS:
|
|
|
+ if screen.get_live_state():
|
|
|
+ print(f"✅ {name} 连接成功(IP:{ip}:{port})")
|
|
|
+ return screen
|
|
|
+ print(f"⚠️ {name} 连接失败,{RECONNECT_DELAY}秒后重试({attempts + 1}/{MAX_RECONNECT_ATTEMPTS})")
|
|
|
+ time.sleep(RECONNECT_DELAY)
|
|
|
+ screen.reconnect()
|
|
|
+ attempts += 1
|
|
|
+ print(f"❌ {name} 连接失败(IP:{ip}:{port}),达到最大重试次数")
|
|
|
+ return None
|
|
|
+
|
|
|
+
|
|
|
+def init_screen_async(name: str, ip: str, port: int, result_dict: dict):
|
|
|
+ """异步初始化屏幕"""
|
|
|
+ screen = init_screen(name, ip, port)
|
|
|
+ result_dict[name] = screen
|
|
|
+
|
|
|
+
|
|
|
+# ===================== 模型加载与推理 =====================
|
|
|
+def load_model_optimized(weights, device):
|
|
|
+ """优化加载模型"""
|
|
|
+ model = attempt_load(weights, map_location=device)
|
|
|
+
|
|
|
+ if JIT_COMPILE and device.type != 'cpu':
|
|
|
+ try:
|
|
|
+ dummy = torch.rand(1, 3, 640, 640).to(device)
|
|
|
+ if INFERENCE_HALF:
|
|
|
+ dummy = dummy.half()
|
|
|
+ model = torch.jit.trace(model, dummy)
|
|
|
+ print("模型JIT编译成功")
|
|
|
+ except Exception as e:
|
|
|
+ print(f"JIT编译失败: {e}")
|
|
|
+
|
|
|
+ if INFERENCE_HALF and device.type != 'cpu':
|
|
|
+ model.half()
|
|
|
+
|
|
|
+ model.eval()
|
|
|
+ for param in model.parameters():
|
|
|
+ param.requires_grad = False
|
|
|
+
|
|
|
+ return model
|
|
|
+
|
|
|
+
|
|
|
+def detect_Recognition_plate_optimized(model, orgimg, device, plate_rec_model, img_size, is_color=False):
|
|
|
+ """优化的车牌检测识别"""
|
|
|
+ conf_thres = 0.3
|
|
|
+ iou_thres = 0.5
|
|
|
+ dict_list = []
|
|
|
+
|
|
|
+ h0, w0 = orgimg.shape[:2]
|
|
|
+ r = img_size / max(h0, w0)
|
|
|
+ if abs(r - 1) > 0.1:
|
|
|
+ interp = cv2.INTER_AREA if r < 1 else cv2.INTER_LINEAR
|
|
|
+ img0 = cv2.resize(orgimg, (int(w0 * r), int(h0 * r)), interpolation=interp)
|
|
|
+ else:
|
|
|
+ img0 = orgimg
|
|
|
+
|
|
|
+ imgsz = check_img_size(img_size, s=model.stride.max())
|
|
|
+ img = letterbox(img0, new_shape=imgsz)[0]
|
|
|
+ img = img[:, :, ::-1].transpose(2, 0, 1).copy()
|
|
|
+
|
|
|
+ img = torch.from_numpy(img).to(device)
|
|
|
+ img = img.float() / 255.0
|
|
|
+ if INFERENCE_HALF and device.type != 'cpu':
|
|
|
+ img = img.half()
|
|
|
+ if img.ndim == 3:
|
|
|
+ img = img.unsqueeze(0)
|
|
|
+
|
|
|
+ with torch.no_grad():
|
|
|
+ pred = model(img)[0]
|
|
|
+ pred = non_max_suppression_face(pred, conf_thres, iou_thres)
|
|
|
+
|
|
|
+ for det in pred:
|
|
|
+ if len(det):
|
|
|
+ det[:, :4] = scale_coords(img.shape[2:], det[:, :4], orgimg.shape).round()
|
|
|
+ det[:, 5:13] = scale_coords_landmarks(img.shape[2:], det[:, 5:13], orgimg.shape).round()
|
|
|
+
|
|
|
+ for j in range(det.size(0)):
|
|
|
+ xyxy = det[j, :4].tolist()
|
|
|
+ conf = det[j, 4].cpu().item()
|
|
|
+ landmarks = det[j, 5:13].tolist()
|
|
|
+ class_num = det[j, 13].cpu().item()
|
|
|
+
|
|
|
+ if conf < DETECT_THRESH:
|
|
|
+ continue
|
|
|
+
|
|
|
+ result_dict = get_plate_rec_landmark(orgimg, xyxy, conf, landmarks, class_num, device, plate_rec_model,
|
|
|
+ is_color)
|
|
|
+ dict_list.append(result_dict)
|
|
|
+ break
|
|
|
+ break
|
|
|
+
|
|
|
+ return dict_list[:1]
|
|
|
+
|
|
|
+
|
|
|
+# ===================== 主函数 =====================
|
|
|
+def start(image_path="imgs"):
|
|
|
+ # 声明使用全局的屏幕/语音实例(核心修复:解决变量作用域问题)
|
|
|
+ global screen, radar_screen, speaker, redis_client
|
|
|
+
|
|
|
+ # 参数解析
|
|
|
+ parser = argparse.ArgumentParser()
|
|
|
+ parser.add_argument("--detect_model", nargs="+", type=str, default="weights/plate_detect.pt", help="检测模型路径")
|
|
|
+ parser.add_argument("--rec_model", type=str, default="weights/plate_rec_color.pth", help="识别模型路径")
|
|
|
+ parser.add_argument("--is_color", type=bool, default=True, help="是否识别车牌颜色")
|
|
|
+ parser.add_argument("--image_path", type=str, default=image_path, help="图片路径")
|
|
|
+ parser.add_argument("--img_size", type=int, default=512, help="推理尺寸")
|
|
|
+ parser.add_argument("--output", type=str, default="result", help="输出目录")
|
|
|
+ parser.add_argument("--video", type=str, default="", help="视频文件路径")
|
|
|
+ parser.add_argument("--stream", type=str, default="", help="RTSP/RTMP流地址")
|
|
|
+ parser.add_argument("--redis_host", type=str, default="localhost", help="Redis主机")
|
|
|
+ parser.add_argument("--redis_port", type=int, default=6379, help="Redis端口")
|
|
|
+ parser.add_argument("--redis_key", type=str, default="plate_results", help="Redis键名")
|
|
|
+ parser.add_argument("--window_size", type=int, default=5, help="滑动窗口秒数")
|
|
|
+ opt = parser.parse_args()
|
|
|
+
|
|
|
+ # 设备配置
|
|
|
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
|
|
+ if device.type == 'cuda':
|
|
|
+ torch.backends.cudnn.benchmark = True
|
|
|
+ torch.backends.cuda.matmul.allow_tf32 = True
|
|
|
+
|
|
|
+ # 更新全局配置
|
|
|
+ global REDIS_HOST, REDIS_PORT, REDIS_KEY, WINDOW_SIZE
|
|
|
+ REDIS_HOST = opt.redis_host
|
|
|
+ REDIS_PORT = opt.redis_port
|
|
|
+ REDIS_KEY = opt.redis_key
|
|
|
+ WINDOW_SIZE = opt.window_size
|
|
|
+
|
|
|
+ # Redis连接
|
|
|
+ try:
|
|
|
+ redis_client = redis.Redis(
|
|
|
+ host=REDIS_HOST,
|
|
|
+ port=REDIS_PORT,
|
|
|
+ db=REDIS_DB,
|
|
|
+ password=REDIS_PASSWORD,
|
|
|
+ decode_responses=True,
|
|
|
+ socket_timeout=2,
|
|
|
+ socket_connect_timeout=2
|
|
|
+ )
|
|
|
+ redis_client.ping()
|
|
|
+ print("✅ Redis连接成功(优化版)")
|
|
|
+ except Exception as e:
|
|
|
+ print(f"❌ Redis连接失败: {e}")
|
|
|
+ redis_client = None
|
|
|
+
|
|
|
+ # 创建输出目录
|
|
|
+ os.makedirs(opt.output, exist_ok=True)
|
|
|
+
|
|
|
+ # 加载模型
|
|
|
+ try:
|
|
|
+ detect_model = load_model_optimized(opt.detect_model, device)
|
|
|
+ plate_rec_model = init_model(device, opt.rec_model, is_color=opt.is_color)
|
|
|
+ total_detect = sum(p.numel() for p in detect_model.parameters()) / 1e6
|
|
|
+ total_rec = sum(p.numel() for p in plate_rec_model.parameters()) / 1e6
|
|
|
+ print(f"✅ 模型加载成功:检测{total_detect:.2f}M, 识别{total_rec:.2f}M")
|
|
|
+ except Exception as e:
|
|
|
+ print(f"❌ 模型加载失败: {e}")
|
|
|
+ return
|
|
|
+
|
|
|
+ # 打印配置信息
|
|
|
+ print(f"推理模式: {'半精度' if INFERENCE_HALF else '全精度'} | JIT编译: {JIT_COMPILE}")
|
|
|
+ print(
|
|
|
+ f"帧处理策略: 每{FRAME_SKIP}帧处理一次 | Redis: {'异步批量' if ASYNC_REDIS and BATCH_REDIS_WRITE else '同步'}")
|
|
|
+ print(f"过滤策略: 不过滤方向,检测所有车辆 | 宽高比阈值: {PLATE_ASPECT_RATIO}")
|
|
|
+
|
|
|
+ # 初始化语音模块
|
|
|
+ speaker = init_speaker(SPEAKER_PORT)
|
|
|
+
|
|
|
+ # 异步初始化两个屏幕
|
|
|
+ screen_init_results = {}
|
|
|
+ screen_threads = [
|
|
|
+ threading.Thread(target=init_screen_async, args=("主屏幕", SCREEN_HOST, SCREEN_PORT, screen_init_results),
|
|
|
+ daemon=True),
|
|
|
+ threading.Thread(target=init_screen_async,
|
|
|
+ args=("雷达屏幕", RADAR_SCREEN_HOST, RADAR_SCREEN_PORT, screen_init_results), daemon=True)
|
|
|
+ ]
|
|
|
+ for t in screen_threads:
|
|
|
+ t.start()
|
|
|
+ for t in screen_threads:
|
|
|
+ t.join(timeout=30)
|
|
|
+
|
|
|
+ # 获取屏幕初始化结果(绑定全局变量)
|
|
|
+ screen = screen_init_results.get("主屏幕")
|
|
|
+ radar_screen = screen_init_results.get("雷达屏幕")
|
|
|
+
|
|
|
+ # 打印屏幕绑定信息(调试用)
|
|
|
+ if screen:
|
|
|
+ print(f"✅ 主屏幕已绑定:{SCREEN_HOST}:{SCREEN_PORT}(用于显示车牌)")
|
|
|
+ else:
|
|
|
+ print(f"❌ 主屏幕初始化失败")
|
|
|
+ if radar_screen:
|
|
|
+ print(f"✅ 雷达屏幕已绑定:{RADAR_SCREEN_HOST}:{RADAR_SCREEN_PORT}(用于显示雷达速度)")
|
|
|
+ else:
|
|
|
+ print(f"❌ 雷达屏幕初始化失败")
|
|
|
+
|
|
|
+ DeviceInitData.LowSpeed = DEVICE_LOW_SPEED
|
|
|
+
|
|
|
+ # 启动雷达线程(核心修复:传入雷达屏幕实例,而非主屏幕)
|
|
|
+ try:
|
|
|
+ radar_thread = threading.Thread(
|
|
|
+ target=open_serial,
|
|
|
+ args=(RADAR_PORT, speaker, radar_screen), # 传入radar_screen(雷达屏幕)
|
|
|
+ daemon=True
|
|
|
+ )
|
|
|
+ radar_thread.start()
|
|
|
+ print(f"✅ 雷达已在后台线程启动,串口:{RADAR_PORT},绑定雷达屏幕")
|
|
|
+ except Exception as e:
|
|
|
+ print(f"❌ 雷达启动失败: {e}")
|
|
|
+
|
|
|
+ # 处理RTSP流
|
|
|
+ if opt.stream:
|
|
|
+ cap_options = "rtsp_transport=tcp"
|
|
|
+ cap, connected = connect_stream(opt.stream, cap_options)
|
|
|
+ if not connected:
|
|
|
+ print(f"[{get_current_time()}] 初始连接失败,退出程序")
|
|
|
+ return
|
|
|
+
|
|
|
+ # 初始化统计变量
|
|
|
+ consecutive_failures = 0
|
|
|
+ reconnect_count = 0
|
|
|
+ frame_count = 0
|
|
|
+ processed_count = 0
|
|
|
+ last_print_time = time.time()
|
|
|
+ print_interval = 10.0
|
|
|
+ inference_times = deque(maxlen=50)
|
|
|
+ last_output_dict = {}
|
|
|
+ output_count = 0
|
|
|
+ incoming_car_count = 0
|
|
|
+ outgoing_car_count = 0
|
|
|
+
|
|
|
+ try:
|
|
|
+ while True:
|
|
|
+ frame_count += 1
|
|
|
+ ret, frame = cap.read()
|
|
|
+
|
|
|
+ # 处理帧读取失败
|
|
|
+ if not ret:
|
|
|
+ consecutive_failures += 1
|
|
|
+ if consecutive_failures % MAX_CONSECUTIVE_FAILURES == 0:
|
|
|
+ print(f"[{get_current_time()}] 视频流中断(连续失败{consecutive_failures}次)")
|
|
|
+ if consecutive_failures >= MAX_CONSECUTIVE_FAILURES:
|
|
|
+ cap, reconnected = reconnect_stream(opt.stream, cap_options)
|
|
|
+ if reconnected:
|
|
|
+ reconnect_count += 1
|
|
|
+ consecutive_failures = 0
|
|
|
+ frame_count = 0
|
|
|
+ continue
|
|
|
+ else:
|
|
|
+ break
|
|
|
+ continue
|
|
|
+
|
|
|
+ consecutive_failures = 0
|
|
|
+
|
|
|
+ # 帧跳过策略
|
|
|
+ if frame_count % FRAME_SKIP != 0:
|
|
|
+ continue
|
|
|
+
|
|
|
+ processed_count += 1
|
|
|
+
|
|
|
+ # 推理处理
|
|
|
+ inference_start = time.time()
|
|
|
+ try:
|
|
|
+ dict_list = detect_Recognition_plate_optimized(
|
|
|
+ detect_model, frame, device, plate_rec_model, opt.img_size, is_color=opt.is_color
|
|
|
+ )
|
|
|
+ inference_time = time.time() - inference_start
|
|
|
+ inference_times.append(inference_time)
|
|
|
+
|
|
|
+ current_time = time.time()
|
|
|
+
|
|
|
+ # 处理识别结果
|
|
|
+ for res in dict_list:
|
|
|
+ plate_no = res['plate_no'].strip()
|
|
|
+ if len(plate_no) < 4 or plate_no.lower() in ['unknown', '']:
|
|
|
+ continue
|
|
|
+
|
|
|
+ # 阈值过滤
|
|
|
+ detect_conf = float(res['detect_conf'])
|
|
|
+ color_conf = res.get('color_conf', 0.0)
|
|
|
+ rec_conf = res.get('rec_conf', [])
|
|
|
+
|
|
|
+ if isinstance(rec_conf, np.ndarray):
|
|
|
+ rec_conf_list = rec_conf.tolist()
|
|
|
+ else:
|
|
|
+ rec_conf_list = rec_conf if isinstance(rec_conf, list) else []
|
|
|
+ rec_avg = np.mean(rec_conf_list) if len(rec_conf_list) > 0 else 0.0
|
|
|
+
|
|
|
+ if detect_conf < DETECT_THRESH or color_conf < COLOR_THRESH or rec_avg < REC_THRESH:
|
|
|
+ continue
|
|
|
+
|
|
|
+ # 去重判断
|
|
|
+ clean_plate = plate_no.replace(' ', '').upper()
|
|
|
+ should_output_flag = False
|
|
|
+ similar_found = None
|
|
|
+
|
|
|
+ for existing_plate in last_output_dict:
|
|
|
+ if clean_plate[:5] == existing_plate[:5]:
|
|
|
+ similar_found = existing_plate
|
|
|
+ break
|
|
|
+
|
|
|
+ if similar_found is None:
|
|
|
+ should_output_flag = True
|
|
|
+ last_output_dict[clean_plate] = current_time
|
|
|
+ else:
|
|
|
+ time_diff = current_time - last_output_dict[similar_found]
|
|
|
+ if time_diff >= 3.0:
|
|
|
+ del last_output_dict[similar_found]
|
|
|
+ last_output_dict[clean_plate] = current_time
|
|
|
+ should_output_flag = True
|
|
|
+
|
|
|
+ # 输出和保存
|
|
|
+ if should_output_flag:
|
|
|
+ # 统计方向
|
|
|
+ if res.get("is_forward", False):
|
|
|
+ incoming_car_count += 1
|
|
|
+ direction = "incoming"
|
|
|
+ else:
|
|
|
+ outgoing_car_count += 1
|
|
|
+ direction = "outgoing"
|
|
|
+
|
|
|
+ plate_color = res.get('plate_color', '未知')
|
|
|
+ current_time_str = get_current_time()
|
|
|
+ output_line = (
|
|
|
+ f"[{current_time_str}] {plate_no} | 检:{detect_conf:.3f} "
|
|
|
+ f"色:{color_conf:.3f} 识:{rec_avg:.3f} | {plate_color}")
|
|
|
+ print(output_line)
|
|
|
+
|
|
|
+ # 写入Redis
|
|
|
+ if redis_client:
|
|
|
+ if ASYNC_REDIS:
|
|
|
+ executor.submit(save_to_redis_async, plate_no, plate_color, detect_conf, color_conf,
|
|
|
+ rec_avg, direction)
|
|
|
+ # 核心修复:仅写入主屏幕(screen),不写入雷达屏幕
|
|
|
+ if screen:
|
|
|
+ try:
|
|
|
+ ff = FlashFile()
|
|
|
+ ff.set_msg(plate_no, 1) # 显示车牌
|
|
|
+ ff.set_mode(4, 1)
|
|
|
+ ff.set_origin(0, True, 0)
|
|
|
+ ff.set_area(128, True, 32)
|
|
|
+ screen.text_ram(ff, True)
|
|
|
+ except Exception as e:
|
|
|
+ print(f"❌ 写入主屏幕失败: {e}")
|
|
|
+ else:
|
|
|
+ save_to_redis_async(plate_no, plate_color, detect_conf, color_conf, rec_avg,
|
|
|
+ direction)
|
|
|
+
|
|
|
+ output_count += 1
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ print(f"[{get_current_time()}] 处理异常: {e}")
|
|
|
+ import traceback
|
|
|
+ traceback.print_exc()
|
|
|
+ continue
|
|
|
+
|
|
|
+ # 定期清理Redis
|
|
|
+ if frame_count % REDIS_CLEAN_INTERVAL == 0 and redis_client:
|
|
|
+ executor.submit(clean_expired_data_batch)
|
|
|
+
|
|
|
+ # 定期刷入Redis队列
|
|
|
+ if frame_count % 10 == 0 and BATCH_REDIS_WRITE and redis_client:
|
|
|
+ executor.submit(flush_redis_queue)
|
|
|
+
|
|
|
+ # 状态打印
|
|
|
+ if time.time() - last_print_time >= print_interval:
|
|
|
+ avg_inference = sum(inference_times) / len(inference_times) if len(inference_times) > 0 else 0
|
|
|
+ print(f"\n[{get_current_time()}] 状态统计")
|
|
|
+ print(f"总帧数: {frame_count} | 处理帧: {processed_count} | 输出车牌: {output_count}")
|
|
|
+ print(
|
|
|
+ f"来向车数量: {incoming_car_count} | 去向车数量: {outgoing_car_count} | 重连次数: {reconnect_count}")
|
|
|
+ print(f"平均推理时间: {avg_inference * 1000:.1f}ms | 处理帧率: {1 / avg_inference:.1f}fps"
|
|
|
+ if avg_inference > 0 else "平均推理时间: 0ms | 处理帧率: 0fps")
|
|
|
+ print(f"缓存车牌种类: {len(last_output_dict)} | Redis清理失败: {clean_error_count}次")
|
|
|
+
|
|
|
+ if redis_client:
|
|
|
+ try:
|
|
|
+ window_info = get_window_info()
|
|
|
+ print(f"Redis窗口: {window_info['count']}条/{WINDOW_SIZE}秒")
|
|
|
+ except:
|
|
|
+ pass
|
|
|
+
|
|
|
+ last_print_time = time.time()
|
|
|
+
|
|
|
+ # 退出按键
|
|
|
+ if cv2.waitKey(1) & 0xFF == ord('q'):
|
|
|
+ break
|
|
|
+
|
|
|
+ except KeyboardInterrupt:
|
|
|
+ print(f"\n[{get_current_time()}] 用户中断")
|
|
|
+ except Exception as e:
|
|
|
+ print(f"\n[{get_current_time()}] 运行错误: {e}")
|
|
|
+ import traceback
|
|
|
+ traceback.print_exc()
|
|
|
+ finally:
|
|
|
+ # 资源清理
|
|
|
+ if cap is not None:
|
|
|
+ cap.release()
|
|
|
+ cv2.destroyAllWindows()
|
|
|
+ executor.shutdown(wait=True)
|
|
|
+
|
|
|
+ # 刷入剩余Redis数据
|
|
|
+ if redis_client:
|
|
|
+ flush_redis_queue()
|
|
|
+ clean_expired_data_batch()
|
|
|
+
|
|
|
+ # 最终统计
|
|
|
+ print(f"\n[{get_current_time()}] 结束报告")
|
|
|
+ print(f"总帧数: {frame_count} | 处理帧: {processed_count} | 输出车牌: {output_count}")
|
|
|
+ print(f"来向车总数: {incoming_car_count} | 去向车总数: {outgoing_car_count} | 重连次数: {reconnect_count}")
|
|
|
+ if len(inference_times) > 0:
|
|
|
+ avg_inf = sum(inference_times) / len(inference_times)
|
|
|
+ print(f"平均推理时间: {avg_inf * 1000:.1f}ms | 实时FPS: {1 / avg_inf:.1f}")
|
|
|
+ else:
|
|
|
+ print("平均推理时间: 0ms | 实时FPS: 0")
|
|
|
+ print(f"识别车牌种类: {len(last_output_dict)} | Redis读取失败: {redis_read_error}次")
|
|
|
+
|
|
|
+
|
|
|
+if __name__ == '__main__':
|
|
|
+ # 初始化屏幕/语音(全局)
|
|
|
+ speaker = init_speaker(SPEAKER_PORT)
|
|
|
+
|
|
|
+ # 启动主程序
|
|
|
+ start()
|