两种方法去除页眉页脚:基于OCR识别后的文本/基于图片切割

11 篇文章 1 订阅
9 篇文章 0 订阅

前言

如何去掉PDF或者WORD的页眉或者页脚?
由于需求涉及文本比对,页眉页脚会影响比对准确率,当前试过两种可以有效去除页眉页脚的方法,供大家参考思路和方法。

1.基于转换为图片后的页眉页脚高度定位识别切割

核心思路:首先将文档每一页转为图片,基于opencv的方法将图片二值化(即能分开空白区域和黑色文字像素),将图片降维存储为“心电图”,即可理解为将图片从上往下横向扫描切割,出现黑丝文字的区域越密集,“心电图”波峰越大。之后即可找到第一次出现波动的高度和该波动第一次消失的高度,第一次波动和最后一次波动即可分别视为页眉页脚出现的位置。(基于认知:页眉页脚应该出现在文档的最高处和最底处)

步骤:
1.首先将文档转为图片,网上有很多方法,可自行搜索。
2.比如得到测试用例:
在这里插入图片描述

3.对图片进行处理,进行横向映射:
在这里插入图片描述split_img.py:

# coding:utf-8
import cv2
import numpy as np
import matplotlib.pyplot as plt
from PIL import Image

class SplitImg():
    def __init__(self):
        self.width = 0
        self.height = 0
        self.img_position = []
        self.img_matrix = []

    def dimension_reduction(self, img_path):
        '''
        对图片进行垂直和水平映射
        :param img_path: 图片存储路径
        :return:
        '''
        img = cv2.imread(img_path, 0)
        _, self.img_matrix = cv2.threshold(img, 200, 255, cv2.THRESH_BINARY)
        print(self.img_matrix)
        horizontal_sum = np.sum(self.img_matrix, axis=1)

        plt.plot(horizontal_sum,range(horizontal_sum.shape[0]))
        plt.gca().invert_yaxis()
        plt.show()

        bin = self.img_matrix.T
        vertical_sum = np.sum(bin, axis=1)
        # plt.plot(range(vertical_sum.shape[0]),vertical_sum)
        # plt.gca().invert_yaxis()
        # plt.show()
        self.height = horizontal_sum.shape[0]
        self.width = vertical_sum.shape[0]
        self.img_position = [[0, 0], [self.width, 0], [self.width, self.height], [0, self.height]]

    def extra_space(self, dirc, apex_position, img_matrix, width_space):
        if dirc == '-':
            mapping_sum = np.sum(img_matrix, axis=1)
        else:
            mapping_sum = np.sum(img_matrix.T, axis=1)
        mapping_size = mapping_sum.shape[0]
        mapping_space = []
        split_result = {}
        start_index = 0
        pre_num = mapping_sum[0]
        stat_num = 0
        for i in range(1, mapping_size):
            if pre_num+1000 > mapping_sum[i] > pre_num-1000:
                stat_num += 1
            else:
                if stat_num > self.width * width_space and start_index != 0:
                # if stat_num > self.width * 0.1 and start_index != 0:
                    mapping_space.append((start_index+i-1)//2)
                stat_num = 0
                start_index = i
                pre_num = mapping_sum[i]
        if dirc == '-':
            x0_y1 = apex_position[0]
            x1_y1 = apex_position[1]
            for i in range(len(mapping_space)):
                x0_y0 = x0_y1
                x1_y0 = x1_y1
                x1_y1 = [apex_position[1][0], apex_position[1][1]+mapping_space[i]]
                x0_y1 = [apex_position[0][0], apex_position[0][1]+mapping_space[i]]
                split_result[str(i+1)] = [x0_y0, x1_y0, x1_y1, x0_y1]
            split_result[str(i+2)] = [x0_y1, x1_y1, apex_position[2], apex_position[3]]
        else:
            x1_y0 = apex_position[0]
            x1_y1 = apex_position[3]
            for i in range(len(mapping_space)):
                x0_y0 = x1_y0
                x0_y1 = x1_y1
                x1_y0 = [apex_position[0][0]+mapping_space[i], apex_position[1][1]]
                x1_y1 = [apex_position[0][0]+mapping_space[i], apex_position[2][1]]
                split_result[str(i+1)] = [x0_y0, x1_y0, x1_y1, x0_y1]
            split_result[str(i+2)] = [x1_y0, apex_position[1], apex_position[2], x1_y1]
        return split_result

    def split_img(self, width_space): #返回按空白行切割的模块
        dirc = '-'
        hori_blocks = self.extra_space(dirc, self.img_position, self.img_matrix, width_space)
        return hori_blocks

    def split_img_cp(self, width_space):
        dirc = '-'
        hori_blocks = self.extra_space(dirc, self.img_position, self.img_matrix, width_space)
        print(hori_blocks)
        dirc = '|'
        split_result = {}
        for k,v in hori_blocks.items():
            current_matrix = np.asarray(self.img_matrix)[v[0][1]:v[2][1],v[0][0]:v[1][0]]
            vert_blocks = self.extra_space(dirc, v, current_matrix, width_space)
            for k1,v1 in vert_blocks.items():
                split_result['.'.join([k,k1])] = v1
        return split_result

    def run(self, img_path, width_space=0.01):
        self.dimension_reduction(img_path)
        return self.split_img(width_space)

3.去掉图片中的第一簇和最后一簇,得到结果:
在这里插入图片描述
这里没有去掉页脚的原因,代码中添加了误删逻辑,如果页眉页脚高度超过了整个文档比例的14%,则不删掉。

remove_img_header_footer.py:

from split_img import *
from file_diff.log import logger
import os


def deleteDir(path):
    """
    删除路径或者文件
    :return:
    """
    try:
        if os.path.isdir(path):
            shutil.rmtree(path)
        if os.path.isfile(path):
            os.remove(path)
    except Exception as e:
        error = traceback.format_exc().split('\n')
        logger.error('错误:\n{}'.format('\n'.join(error)))

def run_remove_header_and_footer(img_path_dir,width_space=0.01):
    file_list = []
    for root, dirs, files in os.walk(img_path_dir):
        file_list = [(os.path.join(root, file), width_space) for file in files]
    for img_path,ws in file_list:
        remove_header_and_footer(img_path,ws)
    # print(file_list)

def judge_exist_header(img_path_dir,width_space=0.01):
    file_list = []
    for root, dirs, files in os.walk(img_path_dir):
        file_list = [(os.path.join(root, file), width_space) for file in files]
    if(len(file_list)==1): #文件转为图片后只有一张
        #利用word允许的页眉最大高度判断,如果在高度以内存在文字,则默认为存在页眉
        #自己创建一个word最大高度的页眉页脚,填充上文字,然后转成PDF转成图片作为测试用例
        #经过验证后不可行,页眉可以设置成无穷高度
        #所以当只有一页图片时,为了稳妥起见,不处理
        pass
    elif(len(file_list)>1):
        header_coordinates = []
        footer_coordinates = []
        for img_path, ws in file_list:
            logger.info("start to judge_exist_header")
            si = SplitImg()
            result = si.run(img_path, width_space)
            # 打开一张图
            img = Image.open(img_path)
            # 图片尺寸
            img_size = img.size
            print(img_size)
            w = img_size[0]  # 图片宽度
            h = img_size[1]  # 图片高度
            for i, row_block in enumerate(result.values()):
                if (i == 0):
                    logger.info('收集页眉坐标')
                    header_coordinates.append((row_block[3],row_block[2])) #页眉矩形模块的下端坐标,从左向右
                elif (i == len(result.values()) - 1):
                    logger.info('收集页脚坐标')
                    footer_coordinates.append((row_block[0], row_block[1])) #页脚矩形模块的上端坐标,从左向右
        print("页眉坐标:",header_coordinates)
        print("页脚坐标:",footer_coordinates)

def judge_exist_footer():
    pass

def remove_header_and_footer(img_path, width_space=0.01):
    logger.info("start to remove header and footer")
    try:
        si = SplitImg()
        result = si.run(img_path, width_space)
        img = Image.open(img_path)
        img_size = img.size
        print(img_size)
        w = img_size[0]  # 图片宽度
        h = img_size[1]  # 图片高度

        main_body = []
        print("len(result.values()):",len(result.values()))
        for i, row_block in enumerate(result.values()):
            if (i == 0):
                logger.info('跳过页眉')
                if(row_block[3][1]/h>=0.14): #如果页眉高度超过14%,则不切以防误伤到正文
                    print("检测高度超过14%,不切页眉")
                    main_body.append(row_block[0])
                    main_body.append(row_block[1])
                else:
                    main_body.append(row_block[3])
                    main_body.append(row_block[2])
            elif (i == len(result.values()) - 1):
                logger.info('跳过页脚')
                if ((h-row_block[0][1]) / h >= 0.14):  # 如果页脚高度超过14%,则不切以防误伤到正文
                    print("检测高度超过14%,不切页脚")
                    main_body.append(row_block[2])
                    main_body.append(row_block[3])
                else:
                    main_body.append(row_block[1])
                    main_body.append(row_block[0])
            # print(row_block)
        print(main_body)
        x = main_body[0][0]
        y = main_body[0][1]
        w = main_body[1][0] - main_body[0][0]
        h = main_body[2][1] - main_body[1][1]
        print(x,y,w,h)
        if(w==0 or h==0):
            # img.save(img_path)
            deleteDir(img_path)
        else:
            region = img.crop((x, y, x + w, y + h))
            # region.save(img_path[:-4]+"_removed_hdr_ftr"+img_path[-4:])
            region.save(img_path)
    except Exception as e:
        logger.error(e)
        return
    logger.info("success remove header and footer")


if __name__ == '__main__':
    remove_header_and_footer('img.png',width_space=0.01)

其中width_space=0.01是阈值,该值越小则越苛刻(切的厉害,稍微一点白缝可能就是边界),越大越包容(不切)。页面高度0.14也可以根据文档的情况自行设置调整。
另外log.py和本案例关系不大主要是调试用,大家自行删除或者替换为print()即可。

之后再通过OCR图片识别的方法自然就没有页眉页脚了,可以发现,该方法可能存在误切到正文的问题,并且可能需要根据文档页眉页脚本身的特点调整参数。

2.基于OCR识别后的文本去除

思路:首先将文档直接转为图片,图片再通过OCR识别转为文字,传入Dict格式的预处理结果,具体格式为:
{‘jpg name1’: ‘content1’, ‘jpg name2’: ‘content2’, …}
测试用例如下:

dict1 = {
‘001.jpg’: ‘这里是页眉\nahhfadvdajv\n\n1’,
‘002.jpg’: ‘这里是页眉\n啊甘好难过好\n但是不管你会给你\n2’,
‘003.jpg’: ‘这里是页眉\n北京公司\n技术同\n甲方:(委托人)\n乙方:(受托人)\n根据《中华人民共和国合同法》的规定,XXXX\n3’,
‘004.jpg’: ‘这里是页眉\n北京有限公司\nsdivndjsdvnjsd\n\n4’
}

然后按照以下步骤处理:
1.首先通过“\n”换行符切割每一张图片的文本
2.根据切割后的结果得到每一页的list,那么页眉可能在每一页的list[0]出现,页脚可能在每一页的list[-1]出现,基于这样的思想,再利用文本相似度+基于最高频出现的文本长度的容错投票筛选机制来判断

这里主要是利用了所有页面的全局信息,以判断页眉为例子,即比较所有页面的list[0],list[1]…,判断他们的相似性,如果相似度超过0.8(可调整)即认为页眉内容相同,但有时候页眉页脚就是单纯的数字(如第一页“1”,第二页“2”),这样相似度就不管用了,引入第二种判断机制:基于认知:每一页页眉/页脚的文本格式长度应该相同,那么对于一个文档而言,通过OCR识别之后的页眉页脚长度也应该相同或者相似,那么当大部分页面比如50%以上的页面(投票思想,阈值可设置)的list[0]的长度都为X时,即可能list[0]都是页眉(判断list[-1]都是页脚的思想同理)。

3.按照上述思想依次比较每一页的相同行list[idx],直到该行在每一页的相似度和长度都不满足阈值时即可退出(对应代码中的:if (avg_similar_score <= 0.5 and vote_ratio <= 0.5): break

remove_str_header_footer.py:

import difflib
from collections import Counter,defaultdict

def string_similar(s1, s2):
    return difflib.SequenceMatcher(None, s1, s2).quick_ratio()

def process_header(dict,page_to_remove):
    print("识别页眉")
    avg_similar_score = 1
    vote_ratio = 1
    idx = 0

    while (avg_similar_score > 0.5 or vote_ratio > 0.5):
        header_list = []
        header_content_len = []
        for item in dict.items():
            str = item[1].split("\n")
            # print(str)
            header_list.append({"str": str[idx], "len": len(str[idx])})
            header_content_len.append(len(str[idx]))

        times = 0
        total_score = 0
        for i in range(0, len(header_list)):
            for j in range(i + 1, len(header_list)):
                times += 1
                score = string_similar(header_list[i]["str"], header_list[j]["str"])
                total_score += score
        avg_similar_score = total_score / times
        # 计算字符串相同长度出现最多次数的频率占比
        dic = Counter(header_content_len)
        dic = sorted(dic.items(), key=lambda item: item[1], reverse=True)
        vote_ratio = dic[0][1] / len(header_content_len)
        if (avg_similar_score <= 0.5 and vote_ratio <= 0.5):
            break
        print(header_list)
        print("avg_similar_score:", avg_similar_score)
        print("出现最多的相同长度为 {} 的字符串次数:{},在所有页面中占比:{}".format(dic[0][0], dic[0][1], vote_ratio))

        # 如果平均相似度>0.8,直接全部删除
        if (avg_similar_score >= 0.8):
            for i,item in enumerate(dict.items()):#遍历每一页
                # print(item[1])
                str = item[1].split("\n") #每一页按行分割
                page_to_remove[i].append(idx) #第i页添加需要去掉的list索引
        # 如果相似度太低,按照最多相同长度为中心的[-2,2]范围,符合这个范围的页眉页脚都删掉
        else:
            for i,item in enumerate(header_list):#遍历每一页
                if(item["len"]>=dic[0][0]-2 and item["len"]<=dic[0][0]+2):
                    page_to_remove[i].append(idx)
        idx += 1

def process_footer(dict,page_to_remove):
    print("\n识别页脚")
    avg_similar_score = 1
    vote_ratio = 1
    idx = -1
    while (avg_similar_score > 0.5 or vote_ratio > 0.5):
        header_list = []
        header_content_len = []
        for item in dict.items():
            # print(item[0],item[1])
            str = item[1].split("\n")
            # print(str)
            header_list.append({"str": str[idx], "len": len(str[idx])})
            header_content_len.append(len(str[idx]))

        times = 0
        total_score = 0
        for i in range(0, len(header_list)):
            for j in range(i + 1, len(header_list)):
                times += 1
                score = string_similar(header_list[i]["str"], header_list[j]["str"])
                total_score += score
        avg_similar_score = total_score / times
        # 计算字符串相同长度出现最多次数的频率占比
        dic = Counter(header_content_len)
        dic = sorted(dic.items(), key=lambda item: item[1], reverse=True)
        vote_ratio = dic[0][1] / len(header_content_len)
        if (avg_similar_score <= 0.5 and vote_ratio <= 0.5):
            break
        print(header_list)
        print("avg_similar_score:", avg_similar_score)
        print("出现最多的相同长度为 {} 的字符串次数:{},在所有页面中占比:{}".format(dic[0][0], dic[0][1], vote_ratio))

        # 如果平均相似度>0.8,直接全部删除
        if (avg_similar_score >= 0.8):
            for i, item in enumerate(dict.items()):  # 遍历每一页
                page_to_remove[i].append(idx)  # 第i页添加需要去掉的list索引
        # 如果相似度太低,按照最多相同长度为中心的[-2,2]范围,符合这个范围的页眉页脚都删掉
        else:
            for i, item in enumerate(header_list):  # 遍历每一页
                if (item["len"] >= dic[0][0] - 2 and item["len"] <= dic[0][0] + 2):
                    page_to_remove[i].append(idx)
        idx -= 1

def remove_run(dict):
    page_to_remove = defaultdict(list)
    process_header(dict, page_to_remove)
    process_footer(dict, page_to_remove)
    print("\n<key:每一页,value: 该页需要删除的段落索引list>:\n",page_to_remove)
    result = ""
    for i, item in enumerate(dict.items()):  # 遍历每一页
        str = item[1].split("\n")  # 每一页按行分割
        print(str)
        delete_idx_in_page_i = page_to_remove[i]
        for idx,s in enumerate(str):
            if(idx in delete_idx_in_page_i or idx-len(str) in delete_idx_in_page_i):
                continue
            else:
                result += s+"\n"
    return result[:-1] #最后多写入了一个换行符,去掉

if __name__ == '__main__':
    dict1 = {
        '001.jpg': '这里是页眉\nahhfadvdajv\n\n1',
        '002.jpg': '这里是页眉\n啊甘好难过好\n但是不管你会给你\n2',
        '003.jpg': '这里是页眉\n北京公司\n技术同\n甲方:(委托人)\n乙方:(受托人)\n根据《中华人民共和国合同法》的规定,XXXX\n3',
        '004.jpg': '这里是页眉\n北京有限公司\nsdivndjsdvnjsd\n\n4'
    }
    
    result = remove_run(dict1)
    print(result)


码字不易,期待您的关注!后台回复【关键词】免费领取本人上传至CSDN的所有资料~

在这里插入图片描述


  • 7
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值