This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
openclaw-skills/skills/kb-workflow/src/extractors/ocr_processor.py
T

310 lines
9.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
OCR 文字识别处理器
使用 PaddleOCR 实现视频帧文字识别功能,主要用于提取硬字幕
"""
import os
import re
import numpy as np
from typing import List, Dict, Any, Tuple
from paddleocr import PaddleOCR
import logging
logger = logging.getLogger(__name__)
class OCRProcessor:
"""
OCR 文字识别处理器
使用 PaddleOCR 从视频帧中识别文字,支持批量识别和结果过滤
"""
# 相似度阈值:用于判断两个文本是否相似(基于编辑距离)
SIMILARITY_THRESHOLD = 0.85
# 置信度阈值
CONFIDENCE_THRESHOLD = 0.7
# 最小字幕长度(字符数)
MIN_SUBTITLE_LENGTH = 2
# 注意:Python re 模块不支持 \p{P} 等属性类,在 _is_noise 方法中使用自定义逻辑
def __init__(self, use_textline_orientation: bool = True, lang: str = 'ch',
use_gpu: bool = False):
"""
初始化 OCR 处理器
Args:
use_textline_orientation: 是否使用文本行方向分类器(PaddleOCR 3.x 参数名)
lang: 语言支持,'ch' 为中英文混合
use_gpu: 是否使用 GPU 加速(通过环境变量设置,如需启用请先设置 CUDA_VISIBLE_DEVICES)
"""
self.use_textline_orientation = use_textline_orientation
self.lang = lang
self.use_gpu = use_gpu
# 初始化 PaddleOCR 实例
self._ocr = None
self._initialize_ocr()
def _initialize_ocr(self):
"""初始化 PaddleOCR 实例"""
try:
# 强制禁用 oneDNN 和 MKL
os.environ['FLAGS_use_mkldnn'] = '0'
os.environ['MKL_SERVICE_FORCE_INTEL'] = '1'
os.environ['OMP_NUM_THREADS'] = '1'
os.environ['PADDLE_ON_INFER'] = '1'
# PaddleOCR 3.x 版本参数
self._ocr = PaddleOCR(
use_textline_orientation=self.use_textline_orientation,
lang=self.lang
)
logger.info("PaddleOCR 初始化成功(oneDNN 已禁用)")
except Exception as e:
logger.error(f"PaddleOCR 初始化失败: {e}")
raise RuntimeError(f"无法初始化 PaddleOCR: {e}")
def recognize_frames(self, frames: List[Tuple[np.ndarray, float]]) -> List[Dict[str, Any]]:
"""
批量识别帧中的文字
Args:
frames: 帧列表,每个元素为 (图像数组, 时间戳) 元组
- 图像数组:numpy.ndarray,形状为 (H, W, 3) 的 RGB 图像
- 时间戳:float,单位为秒
Returns:
List[Dict[str, Any]]: 识别结果列表,每个结果包含:
- time: float, 时间戳(秒)
- text: str, 识别的文本
- confidence: float, 置信度 (0-1)
"""
if not frames:
logger.warning("输入帧列表为空")
return []
results = []
for image, timestamp in frames:
try:
# 调用 PaddleOCR 进行识别
# 注意:PaddleOCR 3.x 版本的 ocr() 方法参数已简化
ocr_result = self._ocr.ocr(image)
# PaddleOCR 返回格式: [[[box], (text, confidence)], ...]
if ocr_result and ocr_result[0]:
for line in ocr_result[0]:
box, (text, confidence) = line
if text and confidence:
results.append({
'time': timestamp,
'text': text,
'confidence': float(confidence)
})
except Exception as e:
logger.warning(f"帧识别失败 (时间戳 {timestamp}s): {e}")
continue
logger.info(f"原始识别结果数量: {len(results)}")
# 过滤和清洗结果
filtered_results = self._filter_and_clean(results)
logger.info(f"过滤后结果数量: {len(filtered_results)}")
return filtered_results
def _filter_and_clean(self, results: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""
过滤和清洗识别结果
Args:
results: 原始识别结果列表
Returns:
List[Dict[str, Any]]: 过滤后的结果列表
"""
if not results:
return []
# 步骤 1: 按置信度过滤
filtered = self._filter_by_confidence(results)
logger.debug(f"置信度过滤后: {len(filtered)}")
# 步骤 2: 过滤噪声(纯符号、单字符等)
filtered = self._filter_noise(filtered)
logger.debug(f"噪声过滤后: {len(filtered)}")
# 步骤 3: 去除重复或相似的文本
filtered = self._remove_duplicates(filtered)
logger.debug(f"去重后: {len(filtered)}")
# 步骤 4: 按时间戳排序
filtered.sort(key=lambda x: x['time'])
return filtered
def _filter_by_confidence(self, results: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""
按置信度过滤结果
Args:
results: 识别结果列表
Returns:
List[Dict[str, Any]]: 高置信度结果
"""
return [r for r in results if r['confidence'] >= self.CONFIDENCE_THRESHOLD]
def _filter_noise(self, results: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""
过滤噪声结果
过滤条件:
- 文本长度小于最小长度
- 纯符号或空白字符
Args:
results: 识别结果列表
Returns:
List[Dict[str, Any]]: 过滤后的结果
"""
filtered = []
for result in results:
text = result['text'].strip()
# 过滤过短的文本
if len(text) < self.MIN_SUBTITLE_LENGTH:
continue
# 过滤纯符号或空白字符
if self._is_noise(text):
continue
filtered.append(result)
return filtered
def _is_noise(self, text: str) -> bool:
"""
判断文本是否为噪声
Args:
text: 要判断的文本
Returns:
bool: 如果是噪声返回 True
"""
# 使用正则判断是否只包含符号和空白
# 注意:这里用简单的正则,更复杂的判断可以后续优化
try:
# 移除标点符号后检查是否为空
# 先替换常见的中文标点
cleaned = re.sub(r'[^\w\u4e00-\u9fff]', '', text)
return len(cleaned) == 0
except:
# 如果正则出错,保守处理,不算噪声
return False
def _remove_duplicates(self, results: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""
去除重复或相似的文本
基于编辑距离(Levenshtein distance)判断文本相似度
Args:
results: 识别结果列表(假设已按时间戳排序)
Returns:
List[Dict[str, Any]]: 去重后的结果
"""
if not results:
return []
deduped = [results[0]]
for current in results[1:]:
last = deduped[-1]
# 计算相似度
similarity = self._calculate_similarity(current['text'], last['text'])
# 如果相似度低于阈值,则保留
if similarity < self.SIMILARITY_THRESHOLD:
deduped.append(current)
else:
# 相似文本:保留置信度更高的那个
if current['confidence'] > last['confidence']:
deduped[-1] = current
return deduped
def _calculate_similarity(self, text1: str, text2: str) -> float:
"""
计算两个文本的相似度(基于归一化编辑距离)
Args:
text1: 文本1
text2: 文本2
Returns:
float: 相似度 (0-1),1 表示完全相同
"""
if text1 == text2:
return 1.0
len1, len2 = len(text1), len(text2)
if len1 == 0 or len2 == 0:
return 0.0
# 动态规划计算编辑距离
dp = np.zeros((len1 + 1, len2 + 1))
for i in range(len1 + 1):
dp[i, 0] = i
for j in range(len2 + 1):
dp[0, j] = j
for i in range(1, len1 + 1):
for j in range(1, len2 + 1):
if text1[i-1] == text2[j-1]:
dp[i, j] = dp[i-1, j-1]
else:
dp[i, j] = min(
dp[i-1, j] + 1, # 删除
dp[i, j-1] + 1, # 插入
dp[i-1, j-1] + 1 # 替换
)
edit_distance = dp[len1, len2]
max_len = max(len1, len2)
# 归一化相似度:1 - (编辑距离 / 最大长度)
similarity = 1.0 - (edit_distance / max_len)
return similarity
def get_ocr_info(self) -> Dict[str, Any]:
"""
获取 OCR 处理器信息
Returns:
Dict[str, Any]: OCR 配置信息
"""
return {
'use_textline_orientation': self.use_textline_orientation,
'lang': self.lang,
'use_gpu': self.use_gpu,
'confidence_threshold': self.CONFIDENCE_THRESHOLD,
'similarity_threshold': self.SIMILARITY_THRESHOLD,
'min_subtitle_length': self.MIN_SUBTITLE_LENGTH
}