'commit'
This commit is contained in:
@@ -1,126 +0,0 @@
|
||||
# coding=utf-8
|
||||
import re
|
||||
|
||||
class OcrParser:
|
||||
@staticmethod
|
||||
def parse(ocr_lines):
|
||||
"""
|
||||
解析 OCR 文本行,提取场站结构化数据。
|
||||
不依赖 LLM,使用规则和正则匹配。
|
||||
"""
|
||||
result = {
|
||||
"station_name": "",
|
||||
"distance": "",
|
||||
"price": None,
|
||||
"piles": [],
|
||||
"parking": "",
|
||||
"tags": []
|
||||
}
|
||||
|
||||
# 预处理:清洗无关字符
|
||||
clean_lines = []
|
||||
for line in ocr_lines:
|
||||
# 去除 "行 X: " 前缀
|
||||
text = re.sub(r"^行\s*\d+:\s*", "", line)
|
||||
# 去除 "(置信度: ...)" 后缀
|
||||
text = re.sub(r"\s*\(置信度:.*?\)\s*$", "", text)
|
||||
text = text.strip()
|
||||
if text:
|
||||
clean_lines.append(text)
|
||||
|
||||
# 1. 场站名称 (Station Name)
|
||||
# 策略:通常是第一行,且不包含特定关键词(如km, /度, ¥等)
|
||||
for line in clean_lines:
|
||||
if len(line) < 2: continue
|
||||
if line.startswith("(") or line.startswith("(") or line == "…": continue
|
||||
|
||||
# 排除明显属性行
|
||||
if any(k in line.lower() for k in ["km", "/度", "¥", "停车", "积分", "组团"]):
|
||||
continue
|
||||
|
||||
# 排除纯数字
|
||||
if re.match(r"^\d+$", line): continue
|
||||
|
||||
# 排除可能是充电枪信息的行 (例如 "闲2/4", "快", "慢")
|
||||
if re.search(r"\d+/\d+", line): continue
|
||||
if line in ["快", "慢", "超", "快充", "慢充", "超充"]: continue
|
||||
|
||||
result["station_name"] = line
|
||||
break
|
||||
|
||||
# 2. 距离 (Distance)
|
||||
for line in clean_lines:
|
||||
# 匹配 "7.4km", "17.4km", "90m"
|
||||
m = re.search(r"(\d+(\.\d+)?)\s*(km|m)", line, re.IGNORECASE)
|
||||
if m:
|
||||
result["distance"] = m.group(0)
|
||||
break
|
||||
|
||||
# 3. 价格 (Price)
|
||||
for line in clean_lines:
|
||||
# 匹配 "0.7111/度", "1.2/度"
|
||||
m = re.search(r"(\d+\.\d+)(?=/度)", line)
|
||||
if m:
|
||||
try:
|
||||
result["price"] = float(m.group(1))
|
||||
break
|
||||
except: pass
|
||||
|
||||
# 4. 充电枪信息 (Piles)
|
||||
# 规则:查找 "快", "慢", "超" 以及 "x/y" 格式
|
||||
# 示例: "快", "闲3/4" (跨行) 或 "快充 3/4" (同行)
|
||||
current_type = "未知"
|
||||
|
||||
# 辅助:先尝试在单行内同时匹配类型和数量
|
||||
# 如果单行内有 "快" 和 "3/4",则直接提取
|
||||
# 如果单行只有类型,更新 current_type
|
||||
# 如果单行只有 "3/4",使用 current_type
|
||||
|
||||
for line in clean_lines:
|
||||
# 更新当前类型上下文
|
||||
if "快" in line: current_type = "快"
|
||||
elif "慢" in line: current_type = "慢"
|
||||
elif "超" in line: current_type = "超"
|
||||
|
||||
# 匹配 "3/4" 或 "闲3/4"
|
||||
# 排除价格行 (包含小数点,如 0.7111/度 这里的 / 前后也可能是数字,需小心)
|
||||
if "/度" in line: continue
|
||||
|
||||
# 匹配分数结构:整数/整数
|
||||
# (?:闲)? 可选的前缀 "闲"
|
||||
m = re.search(r"(?:闲)?(\d+)/(\d+)", line)
|
||||
if m:
|
||||
# 再次确认不是价格(有时候 OCR 会把 . 漏掉,变成 07111/度,但通常会有 /度)
|
||||
# 简单校验:总数通常不会特别大,比如不会是 2024/12 (日期)
|
||||
try:
|
||||
free = int(m.group(1))
|
||||
total = int(m.group(2))
|
||||
|
||||
# 过滤可能的日期或异常值
|
||||
if total > 500: continue
|
||||
|
||||
result["piles"].append({
|
||||
"type": current_type,
|
||||
"free": free,
|
||||
"total": total
|
||||
})
|
||||
except: pass
|
||||
|
||||
# 5. 停车信息 (Parking)
|
||||
for line in clean_lines:
|
||||
if "停车" in line:
|
||||
# 清理前缀符号,如 "·收费停车" -> "收费停车"
|
||||
clean_text = re.sub(r"^[·\.\sP]+", "", line)
|
||||
result["parking"] = clean_text
|
||||
break
|
||||
|
||||
# 6. 标签 (Tags)
|
||||
# 收集除上述已解析字段外的简短关键词
|
||||
known_keywords = ["快", "慢", "超", "P", "组团", "积分"]
|
||||
for line in clean_lines:
|
||||
# 简单匹配一些常见 Tag
|
||||
if "积分" in line: result["tags"].append(line)
|
||||
elif "组团" in line: result["tags"].append("组团")
|
||||
elif line.strip() == "P": result["tags"].append("P")
|
||||
|
||||
return result
|
||||
@@ -1,123 +0,0 @@
|
||||
# coding=utf-8
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
import cv2
|
||||
import numpy as np
|
||||
from paddleocr import PaddleOCR
|
||||
from Util.OcrParser import OcrParser
|
||||
|
||||
# Initialize logger
|
||||
logger = logging.getLogger("PaddleOCRKit")
|
||||
|
||||
class PaddleOCRKit:
|
||||
_instance = None
|
||||
_ocr = None
|
||||
|
||||
def __new__(cls):
|
||||
if cls._instance is None:
|
||||
cls._instance = super(PaddleOCRKit, cls).__new__(cls)
|
||||
cls._instance._initialize_model()
|
||||
return cls._instance
|
||||
|
||||
def _initialize_model(self):
|
||||
"""
|
||||
Initialize the PaddleOCR model once.
|
||||
Using PP-OCRv4 mobile model for speed.
|
||||
"""
|
||||
try:
|
||||
# 1. 抑制 PaddlePaddle 的日志输出
|
||||
os.environ['GLOG_minloglevel'] = '3' # 抑制 GLOG (C++) 日志
|
||||
os.environ['FLAGS_allocator_strategy'] = 'auto_growth' # 减少显存占用警告
|
||||
|
||||
# 2. 抑制 Python 层的 paddle 日志
|
||||
import logging as py_logging
|
||||
py_logging.getLogger('paddle').setLevel(py_logging.ERROR)
|
||||
py_logging.getLogger('ppocr').setLevel(py_logging.ERROR)
|
||||
|
||||
logger.info("Initializing PaddleOCR (PP-OCRv4 Mobile)...")
|
||||
# use_angle_cls is deprecated, using use_textline_orientation instead
|
||||
# ocr_version='PP-OCRv4' defaults to mobile model
|
||||
self._ocr = PaddleOCR(
|
||||
use_textline_orientation=True,
|
||||
lang="ch",
|
||||
ocr_version='PP-OCRv4',
|
||||
show_log=False # 关键:关闭 PaddleOCR 内部日志打印
|
||||
)
|
||||
logger.info("PaddleOCR initialized successfully.")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to initialize PaddleOCR: {e}")
|
||||
self._ocr = None
|
||||
|
||||
def recognize(self, image_input):
|
||||
"""
|
||||
Recognize text from an image and parse it into structured data.
|
||||
|
||||
:param image_input: Image path (str) or OpenCV image object (numpy.ndarray)
|
||||
:return: Parsed structured data (dict) or None if failed
|
||||
"""
|
||||
if self._ocr is None:
|
||||
logger.error("PaddleOCR model is not initialized.")
|
||||
return None
|
||||
|
||||
try:
|
||||
# PaddleOCR handles both path and ndarray
|
||||
# predict() is recommended over ocr() in newer versions, but ocr() is the stable high-level API
|
||||
# We use ocr() as in the test script which worked well.
|
||||
# cls=True was causing issues in test, but use_textline_orientation=True in init handles it?
|
||||
# actually in test script: ocr = PaddleOCR(use_textline_orientation=True, ...)
|
||||
# result = ocr.ocr(image_path) -> this worked, but gave a warning about predict.
|
||||
# Let's stick to ocr() for now as it returns the structure we expect.
|
||||
|
||||
result = self._ocr.ocr(image_input)
|
||||
|
||||
ocr_text_lines = []
|
||||
|
||||
# Handle result structure
|
||||
if not result:
|
||||
logger.warning("OCR returned empty result.")
|
||||
else:
|
||||
res = result[0]
|
||||
if res is None:
|
||||
logger.warning("OCR result[0] is None.")
|
||||
elif isinstance(res, dict) and 'rec_texts' in res:
|
||||
# New PaddleOCR / Paddlex format
|
||||
ocr_text_lines = res['rec_texts']
|
||||
elif isinstance(res, list):
|
||||
# Legacy PaddleOCR format
|
||||
for line in res:
|
||||
# line structure: [[points], (text, confidence)]
|
||||
if len(line) >= 2 and isinstance(line[1], (tuple, list)):
|
||||
ocr_text_lines.append(line[1][0])
|
||||
|
||||
# --- 关键:打印原始识别内容 ---
|
||||
if ocr_text_lines:
|
||||
logger.info(f"🔍 OCR 原始识别文字 ({len(ocr_text_lines)} 行):")
|
||||
for i, line in enumerate(ocr_text_lines):
|
||||
logger.info(f" [{i+1}] {line}")
|
||||
else:
|
||||
logger.info("No text detected.")
|
||||
return {}
|
||||
|
||||
# Parse with Regex Parser
|
||||
parsed_data = OcrParser.parse(ocr_text_lines)
|
||||
|
||||
# --- 关键:打印解析后的结构化数据 ---
|
||||
if parsed_data:
|
||||
logger.info("📦 OCR 解析结果 (JSON):")
|
||||
logger.info(f" {parsed_data}")
|
||||
|
||||
return parsed_data
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error during OCR recognition: {e}")
|
||||
return None
|
||||
|
||||
# Global instance for easy access
|
||||
_kit_instance = None
|
||||
|
||||
def get_ocr_kit():
|
||||
global _kit_instance
|
||||
if _kit_instance is None:
|
||||
_kit_instance = PaddleOCRKit()
|
||||
return _kit_instance
|
||||
Binary file not shown.
Reference in New Issue
Block a user