"""场景分类器:多信号加权评分判断图片属于文本场景还是复杂场景 综合以下4个信号: 1. 文本面积占比(权重0.15)——OCR检测到的文本区域面积 2. 背景白度占比(权重0.50)——浅色像素比例 3. 颜色方差倒数(权重0.25)——颜色变化越小越可能是文档 4. 文本行密度(权重0.10)——单位面积内文本行数 """ import cv2 import numpy as np from module.config import ( TEXT_AREA_WEIGHT, WHITENESS_WEIGHT, LOW_VARIANCE_WEIGHT, LINE_DENSITY_WEIGHT, DOC_SCORE_THRESHOLD, ) def _cv_imread(path, flags=cv2.IMREAD_COLOR): """支持中文路径的cv2.imread""" return cv2.imdecode(np.fromfile(path, dtype=np.uint8), flags) def _compute_whiteness(img, brightness_threshold=200): """计算图片背景白度占比 白底文档的浅色像素(亮度>200)占比通常>60% Args: img: BGR格式的numpy数组 brightness_threshold: 亮度阈值,高于此值视为'白' Returns: whiteness: 白色像素占比,范围[0,1] """ gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) white_pixels = np.sum(gray >= brightness_threshold) total_pixels = gray.shape[0] * gray.shape[1] return white_pixels / total_pixels if total_pixels > 0 else 0.0 def _compute_color_variance_norm(img): """计算图片颜色方差的归一化值 文档图片(白底黑字)颜色方差低,场景照片颜色方差高 归一化到[0,1],值越大表示颜色越丰富(越不像文档) Args: img: BGR格式的numpy数组 Returns: variance_norm: 归一化颜色方差,范围[0,1] """ variances = [np.var(img[:, :, c]) for c in range(3)] avg_var = np.mean(variances) # sigmoid归一化:方差2000约为0.5(经验值) # 文档方差通常<500,场景照片方差通常>3000 variance_norm = 1.0 / (1.0 + np.exp(-(avg_var - 2000) / 800)) return float(variance_norm) def _compute_line_density(text_boxes, img_shape): """计算文本行密度 即使文字分散,如果行数多,行密度仍然高 营业执照虽然文字分散,但行数多(10+行),密度不会太低 Args: text_boxes: OCR检测到的文本框列表 img_shape: (h, w) 图片尺寸 Returns: line_density: 归一化行密度,范围[0,1] """ h, w = img_shape[:2] if not text_boxes or h == 0 or w == 0: return 0.0 # 行数 / 图片面积(以1000*1000为参考) area_factor = (h * w) / (1000 * 1000) if area_factor == 0: return 0.0 raw_density = len(text_boxes) / area_factor # sigmoid归一化:10行/百万像素约为0.5 density_norm = 1.0 / (1.0 + np.exp(-(raw_density - 10) / 5)) return float(density_norm) def _ocr_image(image_path, ocr_engine): """仅执行 OCR 提取文本,不做场景分类 Returns: full_text: 提取的文本字符串 """ try: result = ocr_engine.ocr(image_path, cls=True) if result and result[0]: return ' '.join(line[1][0] for line in result[0]) except Exception as e: print(f"[OCR] 提取失败: {e}") return '' def classify_scene(image_path, ocr_engine, doc_score_threshold=DOC_SCORE_THRESHOLD, precomputed_ocr_result=None): """多信号加权评分判断图片属于文本场景还是复杂场景 快速预判:先用廉价的视觉信号(白度+方差)做预判,只有中间模糊区才调用 OCR。 典型情况下可省掉 OCR,从 2.6s 降到 0.1s。 Args: image_path: 图片路径 ocr_engine: PaddleOCR引擎实例 doc_score_threshold: 文档场景判定阈值,默认0.55 precomputed_ocr_result: 预计算的OCR结果,避免重复OCR Returns: scene_type: 'text'(文本场景) 或 'complex'(复杂场景) doc_score: 文档评分 detail: 各信号的详细分数字典 ocr_result: OCR原始结果(可复用给下游) """ img = _cv_imread(image_path) if img is None: return 'complex', 0.0, {}, None h, w = img.shape[:2] total_area = h * w # === 第1步:廉价的视觉信号(<5ms) === whiteness = _compute_whiteness(img) variance_norm = _compute_color_variance_norm(img) low_variance = 1.0 - variance_norm # === 第2步:快速预判(省 OCR)=== # 白度>0.7 且 颜色方差低 → 文档(白底文档的典型特征) # 白度<0.2 且 颜色方差高 → 场景照片(彩色实景的典型特征) # 仅当落在模糊区才调用完整 OCR needs_ocr = True fast_scene = None fast_doc_score = None # 白底文档:白度极高 + 方差极低 if whiteness >= 0.70 and low_variance >= 0.7: # 视觉信号已足够判断为 text fast_scene = 'text' # 估算 doc_score:whiteness 权重0.5 + low_variance 权重0.25 = 0.75 # text_area 和 line_density 假设中等值(0.3, 0.3) fast_doc_score = ( TEXT_AREA_WEIGHT * 0.3 + WHITENESS_WEIGHT * whiteness + LOW_VARIANCE_WEIGHT * low_variance + LINE_DENSITY_WEIGHT * 0.3 ) needs_ocr = False # 彩色实景:白度极低 + 方差极高 elif whiteness <= 0.20 and low_variance <= 0.3: fast_scene = 'complex' fast_doc_score = ( TEXT_AREA_WEIGHT * 0.0 + WHITENESS_WEIGHT * whiteness + LOW_VARIANCE_WEIGHT * low_variance + LINE_DENSITY_WEIGHT * 0.0 ) needs_ocr = False if not needs_ocr: detail = { 'text_area_ratio': 0.0 if fast_scene == 'complex' else 0.3, 'whiteness': round(whiteness, 4), 'low_variance': round(low_variance, 4), 'line_density': 0.0 if fast_scene == 'complex' else 0.3, 'text_box_count': 0, 'full_text': '', 'fast_classified': True, } return fast_scene, round(fast_doc_score, 4), detail, None # === 第3步:OCR(仅在模糊区执行,~1.5-2s) === if precomputed_ocr_result is not None: result = precomputed_ocr_result else: result = ocr_engine.ocr(image_path, cls=True) text_area = 0 text_boxes = [] full_text = '' if result and result[0]: for line in result[0]: box = line[0] text_boxes.append(box) n = len(box) area = 0 for i in range(n): j = (i + 1) % n area += box[i][0] * box[j][1] area -= box[j][0] * box[i][1] text_area += abs(area) / 2 full_text = ' '.join(line[1][0] for line in result[0]) text_area_ratio = text_area / total_area if total_area > 0 else 0 line_density = _compute_line_density(text_boxes, img.shape) doc_score = ( TEXT_AREA_WEIGHT * text_area_ratio + WHITENESS_WEIGHT * whiteness + LOW_VARIANCE_WEIGHT * low_variance + LINE_DENSITY_WEIGHT * line_density ) scene_type = 'text' if doc_score >= doc_score_threshold else 'complex' detail = { 'text_area_ratio': round(text_area_ratio, 4), 'whiteness': round(whiteness, 4), 'low_variance': round(low_variance, 4), 'line_density': round(line_density, 4), 'text_box_count': len(text_boxes), 'full_text': full_text, } return scene_type, round(doc_score, 4), detail, result