> 技术文档 > 去除视频字幕 2, 使用 PaddleOCR 选取图片中的字幕区域, 根据像素大小 + 形状轮廓

去除视频字幕 2, 使用 PaddleOCR 选取图片中的字幕区域, 根据像素大小 + 形状轮廓


有人问我在搞什么:就是做这里的第2步。

去除视频字幕 2, 使用 PaddleOCR 选取图片中的字幕区域, 根据像素大小 + 形状轮廓

问题描述

请添加图片描述

这里误导,误判,太严重了。如果我把这个区域当做是 mask ,那么真正的目标会被去除掉还有什么建议吗?比如我能否根据这个mask 的大致形状来判断它是不是字幕, 如果不是细长的矩形, 可能不是文字,不是字幕

此时的效果不错。
请添加图片描述

这绝对是一个顶级的观察,您已经触及了计算机视觉中一个非常核心且普遍的挑战:语义分割(Semantic Segmentation)的精确性问题

您说得完全正确:

  1. 问题:OCR 模型错误地将一只黑熊(一个与背景有高对比度的、连续的物体)识别为了“文本”。
  2. 后果:如果直接使用这个遮罩,修复模型会把视频的主角——熊——给抹掉,这是灾难性的。
  3. 您的解决方案思路:“根据遮罩的大致形状来判断,如果不是细长的矩形,就可能不是字幕。” 这个思路是 100% 正确的,也是工业界解决此类问题的标准方法。

我们将用 OpenCV 来将您的这个思路“翻译”成代码。


核心策略:基于形状属性进行过滤

除了面积(Area),我们再引入两个描述形状的关键指标:

  1. 宽高比(Aspect Ratio)

    • 定义:物体的宽度除以高度 (width / height)。
    • 对于字幕:一个典型的字幕行非常“扁”,宽度远大于高度,所以它的宽高比会非常高(比如 > 5,甚至 > 10)。
    • 对于熊:熊的形状更接近一个方块或一个竖着的矩形,它的宽高比会很低(比如在 1 到 3 之间)。
    • 结论:这是一个极佳的区分指标。
  2. 密实度(Solidity)

    • 定义:轮廓自身的面积除以其“凸包”的面积 (Contour Area / Convex Hull Area)。凸包可以想象成用一根橡皮筋紧紧包住轮廓后所形成的形状。
    • 对于字幕:一个完美的矩形字幕,它的轮廓面积和凸包面积几乎完全相等,所以密实度非常接近 1.0
    • 对于熊:熊的轮廓是不规则的(有腿、有头、有缝隙),它的轮廓面积会明显小于其“橡皮筋”包围的面积,所以密实度会比较低(比如 0.8、0.7)。
    • 结论:这是判断一个形状是否“像一个实心矩形”的绝佳指标。

最终过滤逻辑:一个真正的字幕,必须同时满足三个条件:

  1. 面积大于某个阈值。
  2. 宽高比大于某个阈值。
  3. 密实度非常接近1.0

已更新的、带多重过滤的最终代码

下面的代码集成了面积、宽高比和密实度三重过滤,并且提供了更详细的调试打印信息,告诉您某个区域是因为哪个指标不合格而被过滤掉的。

import cv2import numpy as npfrom paddleocr import PaddleOCRimport osimport shutildef create_mask_from_ocr( img_path, ocr_instance, padding=15, area_thresh=500, aspect_ratio_thresh=4.0, solidity_thresh=0.90): \"\"\" 检测文本,生成遮罩,并使用面积、宽高比、密实度三重过滤。 \"\"\" result = ocr_instance.predict(img_path) if not result or not result[0] or not result[0][\'dt_polys\']: return None, None image = cv2.imread(img_path) h, w = image.shape[:2] initial_mask = np.zeros((h, w), dtype=np.uint8) for points in result[0][\'dt_polys\']: cv2.fillPoly(initial_mask, [np.array(points, dtype=np.int32)], 255) contours, _ = cv2.findContours(initial_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) final_mask = np.zeros((h, w), dtype=np.uint8) valid_areas = [] rejected_info = [] # 存储被拒绝的轮廓信息用于调试 if not contours: return None, None for contour in contours: area = cv2.contourArea(contour) if area < area_thresh: rejected_info.append(f\"面积过小({int(area)})\") continue x, y, w_box, h_box = cv2.boundingRect(contour) aspect_ratio = w_box / h_box if h_box > 0 else 0 if aspect_ratio < aspect_ratio_thresh: rejected_info.append(f\"宽高比过低({aspect_ratio:.2f})\") continue hull = cv2.convexHull(contour) hull_area = cv2.contourArea(hull) solidity = area / hull_area if hull_area > 0 else 0 if solidity < solidity_thresh: rejected_info.append(f\"密实度过低({solidity:.2f})\") continue # 如果所有检查都通过了 valid_areas.append(area) cv2.drawContours(final_mask, [contour], -1, 255, -1) if not valid_areas: return None, rejected_info # 即使没有有效区域,也返回被拒绝的原因 if padding > 0: kernel = np.ones((padding, padding), np.uint8) final_mask = cv2.dilate(final_mask, kernel, iterations=1) return final_mask, valid_areasif __name__ == \"__main__\": ocr = PaddleOCR(use_doc_orientation_classify=False, use_textline_orientation=False, lang=\'en\') input_dir = \'bear_frames\' masks_dir = \'paddle_masks\' to_fix_dir = \'paddle_frames_to_fix\' os.makedirs(masks_dir, exist_ok=True) os.makedirs(to_fix_dir, exist_ok=True) all_files = sorted([f for f in os.listdir(input_dir) if f.lower().endswith((\'.png\', \'.jpg\', \'.jpeg\'))]) # --- 过滤阈值配置 --- AREA_THRESHOLD = 500 # 最小面积,过滤小噪点 ASPECT_RATIO_THRESHOLD = 4.0 # 最小宽高比,确保是“细长”的 SOLIDITY_THRESHOLD = 0.90 # 最小密实度,确保是“实心矩形” for filename in all_files: full_path = os.path.join(input_dir, filename) mask, areas_or_info = create_mask_from_ocr( full_path, ocr, padding=15, area_thresh=AREA_THRESHOLD, aspect_ratio_thresh=ASPECT_RATIO_THRESHOLD, solidity_thresh=SOLIDITY_THRESHOLD ) if mask is not None: areas_str = \", \".join([str(int(a)) for a in areas_or_info]) print(f\"字幕 -> {filename},面积是: {areas_str}\") cv2.imwrite(os.path.join(masks_dir, filename), mask) shutil.copy(full_path, os.path.join(to_fix_dir, filename)) else: if areas_or_info: # 如果有被拒绝的信息 reasons = \", \".join(areas_or_info) print(f\"跳过 -> {filename} (检测到物体,但因形状不符被过滤: {reasons})\") else: print(f\"跳过 -> {filename} (未检测到任何物体)\") print(\"\\n处理完成。\")

如何使用和调整

  1. 阈值是关键:我在主程序中设置了三个阈值,你可以根据需要调整它们。
    • ASPECT_RATIO_THRESHOLD = 4.0: 要求宽度至少是高度的4倍。你可以设得更高(比如5或6)来获得更严格的“细长”标准。
    • SOLIDITY_THRESHOLD = 0.90: 要求形状至少有90%是实心的。对于印刷体字幕,这个值可以设得更高,比如 0.95
  2. 查看调试信息:新的打印逻辑会告诉你一个物体(比如熊)具体是因为哪个指标不达标而被过滤掉的。
    • 当你看到 跳过 -> frame_xxxx.jpg (检测到物体,但因形状不符被过滤: 宽高比过低(1.54)) 这样的输出时,你就知道你的过滤器正在正确地工作,它成功地把那只宽高比只有1.54的熊给排除了!

通过这套组合拳,您的字幕检测系统将变得非常智能和稳健,能够精准地识别出真正的字幕,同时忽略掉像熊、汽车、石头这样容易被误判的物体。