import argparse import copy import os import re import time from collections import deque from datetime import datetime from pprint import pprint import cv2 import numpy as np import torch import redis import sys from models.experimental import attempt_load 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 # Redis连接配置 REDIS_HOST = 'localhost' REDIS_PORT = 6379 REDIS_DB = 0 REDIS_PASSWORD = None REDIS_KEY = 'plate_results' WINDOW_SIZE = 5 # 新增:重连相关配置 MAX_RECONNECT_ATTEMPTS = 10 RECONNECT_DELAY = 5 MAX_CONSECUTIVE_FAILURES = 5 # 调整阈值设置 - 提高以减少误报 DETECT_THRESH = 0.65 COLOR_THRESH = 0.85 REC_THRESH = 0.9 # 初始化Redis连接 try: redis_client = redis.Redis( host=REDIS_HOST, port=REDIS_PORT, db=REDIS_DB, password=REDIS_PASSWORD, decode_responses=True ) redis_client.ping() print("Redis连接成功") except Exception as e: print(f"Redis连接失败: {e}") redis_client = None def get_current_time(): """获取当前时间字符串""" return datetime.now().strftime("%Y-%m-%d %H:%M:%S") def get_current_timestamp(): """获取当前时间戳(秒级)""" return int(time.time()) def connect_stream(stream_url, cap_options=""): """建立视频流连接,带重试机制""" attempt = 0 while attempt < MAX_RECONNECT_ATTEMPTS: try: print(f"[{get_current_time()}] 尝试连接视频流: {stream_url} (第{attempt + 1}次)") # 设置FFMPEG选项 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')) # H264 # 测试读取一帧 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=""): """重新连接视频流""" print(f"[{get_current_time()}] 开始重新连接视频流...") if 'cap' in globals() and cap: cap.release() time.sleep(2) # 等待2秒再重连 return connect_stream(stream_url, cap_options) def clean_expired_data(): """清理超过5秒的旧数据""" if redis_client is None: return 0 try: current_ts = get_current_timestamp() cutoff_ts = current_ts - WINDOW_SIZE pipe = redis_client.pipeline() 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() deleted_count = len(expired_timestamps) if deleted_count > 0: print(f"[{get_current_time()}] 清理过期数据: {deleted_count}条 (>5秒)") return deleted_count else: return 0 except Exception as e: print(f"清理过期数据失败: {e}") return 0 def save_to_redis(plate_no, plate_color, detect_conf, color_conf, rec_avg): """将车牌识别结果保存到Redis - 5秒滑动窗口""" if redis_client is None: return False, "Redis未连接" try: clean_expired_data() timestamp = get_current_timestamp() timestamp_ms = int(time.time() * 1000) entry_data = { "plate_no": plate_no, "plate_color": plate_color, "detect_conf": str(detect_conf), "color_conf": str(color_conf), "rec_avg": str(rec_avg), "timestamp": str(timestamp), "timestamp_ms": str(timestamp_ms), "datetime": get_current_time(), "source": "rtsp_stream" } pipe = redis_client.pipeline() hash_key = f"{REDIS_KEY}:data" pipe.hset(hash_key, timestamp, str(entry_data)) zset_key = f"{REDIS_KEY}:sorted" pipe.zadd(zset_key, {timestamp: timestamp}) pipe.execute() return True, f"保存到Redis成功: {timestamp}" except Exception as e: return False, f"保存到Redis失败: {e}" def get_recent_plates_from_redis(): """从Redis获取最近5秒内的所有车牌记录""" if redis_client is None: return [] try: clean_expired_data() 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.zrangebyscore(zset_key, cutoff_ts, current_ts) recent_timestamps = sorted(recent_timestamps, key=int, reverse=True) results = [] for ts in recent_timestamps: entry_str = redis_client.hget(hash_key, ts) if entry_str: try: data = eval(entry_str) results.append({ 'timestamp': int(ts), 'data': data }) except: continue return results except Exception as e: print(f"从Redis读取数据失败: {e}") return [] def get_window_info(): """获取滑动窗口的统计信息""" if redis_client is None: return {"count": 0, "window_size": WINDOW_SIZE} try: clean_expired_data() 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) if sorted_count > 0: timestamps_with_scores = redis_client.zrange(zset_key, 0, -1, withscores=True) if timestamps_with_scores: timestamps = [] for member, score in timestamps_with_scores: try: timestamps.append(int(float(score))) except: continue if timestamps: oldest_ts = min(timestamps) newest_ts = max(timestamps) time_range = newest_ts - oldest_ts else: time_range = 0 oldest_ts = newest_ts = int(time.time()) else: time_range = 0 oldest_ts = newest_ts = int(time.time()) else: time_range = 0 oldest_ts = newest_ts = int(time.time()) 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 should_output(plate_no, last_output_dict, current_time): """优化的去重逻辑:支持模糊匹配和更长的冷却时间""" clean_plate = plate_no.strip().replace(' ', '').upper() if len(clean_plate) < 5: return False, "车牌太短" similar_found = None for existing_plate in list(last_output_dict.keys()): if (clean_plate == existing_plate or (len(clean_plate) >= 5 and len(existing_plate) >= 5 and clean_plate[:5] == existing_plate[:5])): similar_found = existing_plate break if similar_found is None: last_output_dict[clean_plate] = current_time return True, "新车牌" else: last_time = last_output_dict[similar_found] time_diff = current_time - last_time if time_diff >= 3.0: del last_output_dict[similar_found] last_output_dict[clean_plate] = current_time return True, "更新识别" else: return False, "冷却期内" def simple_plate_check(plate_no): """最简单的车牌检查:只要不是空的和unknown就通过""" plate_no = plate_no.strip() if len(plate_no) < 4 or plate_no.lower() in ['unknown', '']: return False, "太短或未知" return True, "通过" def order_points(pts): 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): """透视变换得到车牌小图""" rect = pts.astype("float32") (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), int(widthB)) 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), int(heightB)) 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 load_model(weights, device): """加载检测模型""" model = attempt_load(weights, map_location=device) return model 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].clamp_(0, img0_shape[1]) coords[:, 1].clamp_(0, img0_shape[0]) coords[:, 2].clamp_(0, img0_shape[1]) coords[:, 3].clamp_(0, img0_shape[0]) coords[:, 4].clamp_(0, img0_shape[1]) coords[:, 5].clamp_(0, img0_shape[0]) coords[:, 6].clamp_(0, img0_shape[1]) coords[:, 7].clamp_(0, img0_shape[0]) return coords def get_plate_rec_landmark(img, xyxy, conf, landmarks, class_num, device, plate_rec_model, is_color=False): """获取车牌坐标以及四个角点坐标并识别车牌号""" h, w, c = img.shape result_dict = {} x1 = int(xyxy[0]) y1 = int(xyxy[1]) x2 = int(xyxy[2]) y2 = int(xyxy[3]) height = y2 - y1 landmarks_np = np.zeros((4, 2)) rect = [x1, y1, x2, y2] for i in range(4): point_x = int(landmarks[2 * i]) point_y = int(landmarks[2 * i + 1]) landmarks_np[i] = np.array([point_x, point_y]) 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) else: plate_number, rec_prob, plate_color, color_conf = get_plate_result(roi_img, device, plate_rec_model, is_color=is_color) result_dict["rect"] = rect result_dict["detect_conf"] = conf result_dict["landmarks"] = landmarks_np.tolist() result_dict["plate_no"] = plate_number result_dict["rec_conf"] = rec_prob result_dict["roi_height"] = roi_img.shape[0] result_dict["plate_color"] = "" if is_color: result_dict["plate_color"] = plate_color result_dict["color_conf"] = color_conf result_dict["plate_type"] = class_num return result_dict def detect_Recognition_plate(model, orgimg, device, plate_rec_model, img_size, is_color=False): """获取车牌信息""" conf_thres = 0.3 iou_thres = 0.5 dict_list = [] img0 = copy.deepcopy(orgimg) assert orgimg is not None, "Image Not Found " h0, w0 = orgimg.shape[:2] r = img_size / max(h0, w0) if r != 1: interp = cv2.INTER_AREA if r < 1 else cv2.INTER_LINEAR img0 = cv2.resize(img0, (int(w0 * r), int(h0 * r)), interpolation=interp) 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() t0 = time.time() img = torch.from_numpy(img).to(device) img = img.float() img /= 255.0 if img.ndimension() == 3: img = img.unsqueeze(0) pred = model(img)[0] pred = non_max_suppression_face(pred, conf_thres, iou_thres) for i, det in enumerate(pred): if len(det): det[:, :4] = scale_coords(img.shape[2:], det[:, :4], orgimg.shape).round() for c in det[:, -1].unique(): n = (det[:, -1] == c).sum() 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].view(-1).tolist() conf = det[j, 4].cpu().numpy() landmarks = det[j, 5:13].view(-1).tolist() class_num = det[j, 13].cpu().numpy() result_dict = get_plate_rec_landmark(orgimg, xyxy, conf, landmarks, class_num, device, plate_rec_model, is_color=is_color) dict_list.append(result_dict) return dict_list def start(image_path="imgs"): """主函数""" parser = argparse.ArgumentParser() parser.add_argument("--detect_model", nargs="+", type=str, default="weights/plate_detect.pt", help="model.pt path(s)") parser.add_argument("--rec_model", type=str, default="weights/plate_rec_color.pth", help="model.pt path(s)") parser.add_argument("--is_color", type=bool, default=True, help="plate color") parser.add_argument("--image_path", type=str, default=image_path, help="source") parser.add_argument("--img_size", type=int, default=640, help="inference size (pixels)") parser.add_argument("--output", type=str, default="result", help="source") parser.add_argument("--video", type=str, default="", help="source") parser.add_argument("--stream", type=str, default="", help="RTSP/RTMP video stream URL") parser.add_argument("--redis_host", type=str, default="localhost", help="Redis host") parser.add_argument("--redis_port", type=int, default=6379, help="Redis port") parser.add_argument("--redis_key", type=str, default="plate_results", help="Redis key name") parser.add_argument("--window_size", type=int, default=5, help="Sliding window size in seconds") device = torch.device("cuda" if torch.cuda.is_available() else "cpu") opt = parser.parse_args() global REDIS_HOST, REDIS_PORT, REDIS_KEY, WINDOW_SIZE, redis_client REDIS_HOST = opt.redis_host REDIS_PORT = opt.redis_port REDIS_KEY = opt.redis_key WINDOW_SIZE = opt.window_size print("=" * 60) print("最终优化版:5秒滑动窗口,自动清理过期数据") print("=" * 60) print(f"阈值: 检测{DETECT_THRESH} 颜色{COLOR_THRESH} 识别{REC_THRESH}") print(f"Redis: {REDIS_HOST}:{REDIS_PORT}") print(f"Redis键: {REDIS_KEY}") print(f"滑动窗口: {WINDOW_SIZE}秒") print("格式验证: 放宽标准,允许不完整车牌") print("去重策略: 前5字符相同视为同一车牌,3秒冷却") print("输出内容: 车牌、时间、车牌颜色") print("=" * 60) print(opt) try: redis_client = redis.Redis( host=REDIS_HOST, port=REDIS_PORT, db=REDIS_DB, password=REDIS_PASSWORD, decode_responses=True ) redis_client.ping() print("Redis连接成功") except Exception as e: print(f"Redis连接失败: {e}") redis_client = None save_path = opt.output if not os.path.exists(save_path): os.mkdir(save_path) detect_model = load_model(opt.detect_model, device) plate_rec_model = init_model(device, opt.rec_model, is_color=opt.is_color) total = sum(p.numel() for p in detect_model.parameters()) total_1 = sum(p.numel() for p in plate_rec_model.parameters()) print("detect params: %.2fM,rec params: %.2fM" % (total / 1e6, total_1 / 1e6)) if opt.stream: # 设置FFMPEG选项 cap_options = ( "rtsp_transport;tcp;" "buffer_size;1024000;" "timeout;5000000" ) # 初始连接 cap, connected = connect_stream(opt.stream, cap_options) if not connected: print(f"[{get_current_time()}] 初始连接失败,退出程序") return consecutive_failures = 0 reconnect_count = 0 actual_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) actual_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) actual_fps = cap.get(cv2.CAP_PROP_FPS) print(f"实际视频流参数: {actual_width}x{actual_height} @ {actual_fps:.1f}fps") frame_count = 0 processed_count = 0 last_print_time = time.time() print_interval = 8.0 print(f"开始处理: {opt.stream}") print("等待车牌出现...") inference_times = deque(maxlen=30) last_output_dict = {} output_count = 0 try: while True: frame_count += 1 # 读取帧 ret, frame = cap.read() if not ret: consecutive_failures += 1 print(f"[{get_current_time()}] 视频流中断 (连续失败{consecutive_failures}次),尝试重新连接...") # 如果连续失败太多,退出程序 if consecutive_failures >= MAX_CONSECUTIVE_FAILURES: print(f"[{get_current_time()}] 连续失败次数过多,停止重连") break # 尝试重新连接 cap, reconnected = reconnect_stream(opt.stream, cap_options) if reconnected: reconnect_count += 1 consecutive_failures = 0 # 重置失败计数 print(f"[{get_current_time()}] 重新连接成功 (第{reconnect_count}次)") frame_count = 0 # 重置帧计数 continue else: print(f"[{get_current_time()}] 重新连接失败,{RECONNECT_DELAY}秒后再次尝试...") time.sleep(RECONNECT_DELAY) continue # 重置连续失败计数 consecutive_failures = 0 should_process = (frame_count % 3 == 0) output_this_cycle = 0 if should_process: processed_count += 1 try: inference_start = time.time() dict_list = detect_Recognition_plate(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() current_time_str = get_current_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', []) rec_avg = np.mean(rec_conf) if isinstance(rec_conf, (list, np.ndarray)) and len( rec_conf) > 0 else 0.0 plate_color = res.get('plate_color', '未知') if detect_conf < DETECT_THRESH or color_conf < COLOR_THRESH or rec_avg < REC_THRESH: continue is_valid, reason = simple_plate_check(plate_no) if not is_valid: continue ok, output_reason = should_output(plate_no, last_output_dict, current_time) if ok: output_line = f"[{current_time_str}] [有效] {plate_no} | 检:{detect_conf:.3f} 色:{color_conf:.3f} 识:{rec_avg:.3f} | {plate_color} | {output_reason}" print(output_line) if redis_client: save_success, save_msg = save_to_redis(plate_no, plate_color, detect_conf, color_conf, rec_avg) if save_success: output_line += f" | {save_msg}" else: output_line += f" | {save_msg}" output_count += 1 last_output_dict[plate_no.replace(' ', '').upper()] = current_time output_this_cycle += 1 except Exception as e: print(f"[{get_current_time()}] 处理异常: {e}") continue current_time = time.time() if current_time - last_print_time >= print_interval: avg_inference = sum(inference_times) / len(inference_times) if inference_times else 0 unique_plates = len(last_output_dict) print(f"\n[{get_current_time()}] 状态 @{frame_count}") print(f"处理帧率: {processed_count / (current_time - last_print_time + 0.1):.1f}fps") print(f"平均推理: {avg_inference * 1000:.1f}ms") print(f"输出车牌: {output_count}个(累计) | 缓存种类: {unique_plates}种") if redis_client: try: window_info = get_window_info() print(f"滑动窗口: {window_info['count']}条记录/{window_info['window_size']}秒") if window_info['count'] > 0: print(f"时间范围: {window_info['oldest_record']} ~ {window_info['newest_record']}") recent_plates = get_recent_plates_from_redis() if recent_plates: print(f"窗口内车牌:") for i, record in enumerate(recent_plates, 1): data = record['data'] age = get_current_timestamp() - record['timestamp'] print(f" {i}. {data['plate_no']} ({data['plate_color']}) - [{age}秒前]") else: print(f"窗口内暂无记录") except Exception as e: print(f"读取窗口数据失败: {e}") last_print_time = current_time if frame_count % 100 == 0: print(f"\r[{get_current_time()}] 运行中: {frame_count}F", end="", flush=True) 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}") finally: if 'cap' in globals() and cap: cap.release() cv2.destroyAllWindows() print(f"\n[{get_current_time()}] 结束报告") print(f"总帧数: {frame_count} | 处理帧: {processed_count}") if inference_times: avg_inf = sum(inference_times) / len(inference_times) print(f"平均推理: {avg_inf * 1000:.1f}ms | 实时FPS: {1 / avg_inf:.1f}") print(f"实际输出车牌: {output_count}个") print(f"识别到车牌种类: {len(last_output_dict)}种") print(f"重新连接次数: {reconnect_count}") if redis_client: try: clean_expired_data() 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) print(f"最终窗口统计: {data_count}条记录在{WINDOW_SIZE}秒内") except: pass if __name__ == '__main__': start()