Initial commit

This commit is contained in:
2025-08-28 12:00:56 +08:00
commit edef40acd4
27 changed files with 1823 additions and 0 deletions

View File

@@ -0,0 +1,490 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
车牌识别接口模块
预留接口可接入各种OCR模型进行车牌号识别
"""
import cv2
import numpy as np
from typing import List, Optional, Dict, Any
from abc import ABC, abstractmethod
class PlateRecognizerInterface(ABC):
"""车牌识别接口基类"""
@abstractmethod
def recognize(self, plate_image: np.ndarray) -> Dict[str, Any]:
"""
识别车牌号
Args:
plate_image: 车牌图像 (BGR格式)
Returns:
识别结果字典,包含:
{
'text': str, # 识别的车牌号
'confidence': float, # 置信度 (0-1)
'success': bool # 是否识别成功
}
"""
pass
@abstractmethod
def batch_recognize(self, plate_images: List[np.ndarray]) -> List[Dict[str, Any]]:
"""
批量识别车牌号
Args:
plate_images: 车牌图像列表
Returns:
识别结果列表
"""
pass
class MockPlateRecognizer(PlateRecognizerInterface):
"""模拟车牌识别器(用于测试)"""
def __init__(self):
self.mock_plates = [
"京A12345", "沪B67890", "粤C11111", "川D22222",
"鲁E33333", "苏F44444", "浙G55555", "闽H66666"
]
self.call_count = 0
def recognize(self, plate_image: np.ndarray) -> Dict[str, Any]:
"""
模拟识别单个车牌
Args:
plate_image: 车牌图像
Returns:
模拟识别结果
"""
# 模拟处理时间
import time
time.sleep(0.01) # 10ms模拟处理时间
# 简单的图像质量检查
if plate_image is None or plate_image.size == 0:
return {
'text': '',
'confidence': 0.0,
'success': False
}
# 检查图像尺寸
height, width = plate_image.shape[:2]
if width < 50 or height < 20:
return {
'text': '',
'confidence': 0.3,
'success': False
}
# 模拟识别结果
plate_text = self.mock_plates[self.call_count % len(self.mock_plates)]
confidence = 0.85 + (self.call_count % 10) * 0.01 # 0.85-0.94
self.call_count += 1
return {
'text': plate_text,
'confidence': confidence,
'success': True
}
def batch_recognize(self, plate_images: List[np.ndarray]) -> List[Dict[str, Any]]:
"""
批量识别车牌
Args:
plate_images: 车牌图像列表
Returns:
识别结果列表
"""
results = []
for plate_image in plate_images:
result = self.recognize(plate_image)
results.append(result)
return results
class PaddleOCRRecognizer(PlateRecognizerInterface):
"""PaddleOCR车牌识别器示例实现"""
def __init__(self, use_gpu: bool = True):
"""
初始化PaddleOCR识别器
Args:
use_gpu: 是否使用GPU
"""
self.use_gpu = use_gpu
self.ocr = None
self._init_ocr()
def _init_ocr(self):
"""初始化OCR模型"""
try:
# 这里可以接入PaddleOCR
# from paddleocr import PaddleOCR
# self.ocr = PaddleOCR(use_angle_cls=True, lang='ch', use_gpu=self.use_gpu)
print("PaddleOCR初始化完成示例代码需要安装PaddleOCR")
except ImportError:
print("PaddleOCR未安装使用模拟识别器")
self.ocr = None
def recognize(self, plate_image: np.ndarray) -> Dict[str, Any]:
"""
使用PaddleOCR识别车牌
Args:
plate_image: 车牌图像
Returns:
识别结果
"""
if self.ocr is None:
# 回退到模拟识别
mock_recognizer = MockPlateRecognizer()
return mock_recognizer.recognize(plate_image)
try:
# 使用PaddleOCR进行识别
results = self.ocr.ocr(plate_image, cls=True)
if results and len(results) > 0 and results[0]:
# 提取文本和置信度
text_results = []
for line in results[0]:
text = line[1][0]
confidence = line[1][1]
text_results.append((text, confidence))
# 选择置信度最高的结果
if text_results:
best_result = max(text_results, key=lambda x: x[1])
return {
'text': best_result[0],
'confidence': best_result[1],
'success': True
}
except Exception as e:
print(f"PaddleOCR识别失败: {e}")
return {
'text': '',
'confidence': 0.0,
'success': False
}
def batch_recognize(self, plate_images: List[np.ndarray]) -> List[Dict[str, Any]]:
"""
批量识别
Args:
plate_images: 车牌图像列表
Returns:
识别结果列表
"""
results = []
for plate_image in plate_images:
result = self.recognize(plate_image)
results.append(result)
return results
class TesseractRecognizer(PlateRecognizerInterface):
"""Tesseract车牌识别器示例实现"""
def __init__(self, lang: str = 'chi_sim+eng'):
"""
初始化Tesseract识别器
Args:
lang: 识别语言
"""
self.lang = lang
self.tesseract_available = self._check_tesseract()
def _check_tesseract(self) -> bool:
"""检查Tesseract是否可用"""
try:
import pytesseract
return True
except ImportError:
print("pytesseract未安装使用模拟识别器")
return False
def recognize(self, plate_image: np.ndarray) -> Dict[str, Any]:
"""
使用Tesseract识别车牌
Args:
plate_image: 车牌图像
Returns:
识别结果
"""
if not self.tesseract_available:
# 回退到模拟识别
mock_recognizer = MockPlateRecognizer()
return mock_recognizer.recognize(plate_image)
try:
import pytesseract
# 图像预处理
processed_image = self._preprocess_image(plate_image)
# 使用Tesseract识别
text = pytesseract.image_to_string(
processed_image,
lang=self.lang,
config='--psm 8 --oem 3 -c tessedit_char_whitelist=0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ京沪粤川鲁苏浙闽'
)
# 清理识别结果
text = text.strip().replace(' ', '').replace('\n', '')
if text and len(text) >= 5: # 车牌号至少5位
return {
'text': text,
'confidence': 0.8, # Tesseract不直接提供置信度
'success': True
}
except Exception as e:
print(f"Tesseract识别失败: {e}")
return {
'text': '',
'confidence': 0.0,
'success': False
}
def _preprocess_image(self, image: np.ndarray) -> np.ndarray:
"""图像预处理"""
# 转换为灰度图
if len(image.shape) == 3:
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
else:
gray = image
# 调整尺寸
height, width = gray.shape
if width < 200:
scale = 200 / width
new_width = int(width * scale)
new_height = int(height * scale)
gray = cv2.resize(gray, (new_width, new_height))
# 二值化
_, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
return binary
def batch_recognize(self, plate_images: List[np.ndarray]) -> List[Dict[str, Any]]:
"""
批量识别
Args:
plate_images: 车牌图像列表
Returns:
识别结果列表
"""
results = []
for plate_image in plate_images:
result = self.recognize(plate_image)
results.append(result)
return results
class PlateRecognizerManager:
"""车牌识别管理器"""
def __init__(self, recognizer_type: str = 'mock'):
"""
初始化识别管理器
Args:
recognizer_type: 识别器类型 ('mock', 'paddleocr', 'tesseract')
"""
self.recognizer_type = recognizer_type
self.recognizer = self._create_recognizer(recognizer_type)
def _create_recognizer(self, recognizer_type: str) -> PlateRecognizerInterface:
"""创建识别器"""
if recognizer_type == 'mock':
return MockPlateRecognizer()
elif recognizer_type == 'paddleocr':
return PaddleOCRRecognizer()
elif recognizer_type == 'tesseract':
return TesseractRecognizer()
else:
print(f"未知的识别器类型: {recognizer_type},使用模拟识别器")
return MockPlateRecognizer()
def recognize_plates(self, plate_images: List[np.ndarray]) -> List[Dict[str, Any]]:
"""
识别车牌列表
Args:
plate_images: 车牌图像列表
Returns:
识别结果列表
"""
if not plate_images:
return []
return self.recognizer.batch_recognize(plate_images)
def switch_recognizer(self, recognizer_type: str):
"""
切换识别器
Args:
recognizer_type: 新的识别器类型
"""
self.recognizer_type = recognizer_type
self.recognizer = self._create_recognizer(recognizer_type)
print(f"已切换到识别器: {recognizer_type}")
def get_recognizer_info(self) -> Dict[str, Any]:
"""
获取识别器信息
Returns:
识别器信息
"""
return {
'type': self.recognizer_type,
'class': self.recognizer.__class__.__name__
}
def preprocess_blue_plate(self, plate_image: np.ndarray, original_image: np.ndarray, bbox: List[int]) -> np.ndarray:
"""
蓝色车牌预处理:倾斜矫正
Args:
plate_image: 切割后的车牌图像
original_image: 原始图像
bbox: 边界框坐标 [x1, y1, x2, y2]
Returns:
矫正后的车牌图像
"""
try:
# 从原图中提取车牌区域
x1, y1, x2, y2 = bbox
roi = original_image[y1:y2, x1:x2]
# 获取蓝色车牌的二值图像
bin_img = self._get_blue_img_bin(roi)
# 倾斜矫正
corrected_img = self._deskew_plate(bin_img, roi)
return corrected_img
except Exception as e:
print(f"蓝色车牌预处理失败: {e}")
return plate_image
def _get_blue_img_bin(self, img: np.ndarray) -> np.ndarray:
"""
获取蓝色车牌的二值图像
"""
# 掩膜BGR通道若像素B分量在 100~255 且 G分量在 0~190 且 R分量在 0~140 置255白色否则置0黑色
mask_bgr = cv2.inRange(img, (100, 0, 0), (255, 190, 140))
# 转换成 HSV 颜色空间
img_hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
h, s, v = cv2.split(img_hsv) # 分离通道 色调(H),饱和度(S),明度(V)
mask_s = cv2.inRange(s, 80, 255) # 取饱和度通道进行掩膜得到二值图像
# 与操作,两个二值图像都为白色才保留,否则置黑
rgbs = mask_bgr & mask_s
# 核的横向分量大,使车牌数字尽量连在一起
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (15, 3))
img_rgbs_dilate = cv2.dilate(rgbs, kernel, 3) # 膨胀,减小车牌空洞
return img_rgbs_dilate
def _order_points(self, pts: np.ndarray) -> np.ndarray:
"""
将四点按 左上、右上、右下、左下 排序
"""
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 _deskew_plate(self, bin_img: np.ndarray, original_roi: np.ndarray) -> np.ndarray:
"""
车牌倾斜矫正
Args:
bin_img: 二值图像
original_roi: 原始ROI区域
Returns:
矫正后的原始图像(未被掩模,但经过旋转和切割)
"""
try:
# 找最大轮廓
cnts, _ = cv2.findContours(bin_img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if not cnts:
return original_roi
c = max(cnts, key=cv2.contourArea)
# 最小外接矩形
rect = cv2.minAreaRect(c)
box = cv2.boxPoints(rect)
box = np.array(box, dtype="float32")
# 排序四个点
pts_src = self._order_points(box)
# 计算目标矩形宽高
(tl, tr, br, bl) = pts_src
widthA = np.linalg.norm(br - bl)
widthB = np.linalg.norm(tr - tl)
maxWidth = int(max(widthA, widthB))
heightA = np.linalg.norm(tr - br)
heightB = np.linalg.norm(tl - bl)
maxHeight = int(max(heightA, heightB))
# 确保尺寸合理
if maxWidth < 10 or maxHeight < 10:
return original_roi
# 目标点集合
pts_dst = np.array([
[0, 0],
[maxWidth - 1, 0],
[maxWidth - 1, maxHeight - 1],
[0, maxHeight - 1]], dtype="float32")
# 透视变换
M = cv2.getPerspectiveTransform(pts_src, pts_dst)
warped = cv2.warpPerspective(original_roi, M, (maxWidth, maxHeight))
return warped
except Exception as e:
print(f"车牌矫正失败: {e}")
return original_roi