使用 PyQt 和 OpenCV 构建高级 PDF 处理应用程序(非完善版)


前言

研究生入学不久,在导师给的一大堆推荐下选择了一个看起来能做的短期学习任务,处理PDF文件,实现一些功能,在数据分析和文档管理领域,处理 PDF 文件是非常常见的需求。本文将分享我如何利用 PyQt 构建一个功能齐全的 PDF 处理应用程序,并结合 OpenCV 实现图表提取和目录生成的高级功能。这篇文章将详细介绍项目的设计思想、代码实现以及优化步骤,帮助你快速上手类似的项目开发,还有我的这个工具还在完善中,包括界面美化的功能,跳转流畅性问题还是有存在,然后有些识别效果也不是很好,如果你能想出更好的方法,希望能与我也分享一下。

一、项目背景

我希望开发一个 PDF 处理工具,它可以完成以下几个主要功能:
1、PDF 文件上传与展示:通过图形化界面上传 PDF 文件,并将文件中的内容逐页显示。
2、文本和图表提取:使用 Tesseract OCR 从 PDF 中提取文本,并通过 OpenCV 从页面中识别和提取图表。
3、目录生成和页面跳转:根据 PDF 中的标题生成目录,并支持点击目录项跳转到相应的页面。
4、用户友好界面:使用 PyQt 构建图形化用户界面,以便用户直观操作应用程序。

二、项目架构设计

项目分为两个主要模块:
PDFWorker 和 MainWindow(PyQt 界面部分):负责用户界面的创建和交互逻辑,实现多页面切换和多线程处理。
PDFProcessor(数据处理部分):负责 PDF 转换、文本提取、图表检测与提取以及目录生成等功能。
这种模块化设计便于维护和扩展,同时也提升了代码的可读性和复用性。

三、主要功能实现

1.PDF 文件处理与转换

在 PDF 文件上传后,我们将其转换为图片,以便后续使用 Tesseract OCR 进行文本提取。我们使用了 pdf2image 库来完成 PDF 转图片的转换,确保页面内容以高分辨率的方式保存。
代码如下(示例):

def convert_pdf_to_images(self):
    pages = convert_from_path(self.pdf_path, 300)
    for i, page in enumerate(pages):
        output_filepath = os.path.join(self.image_folder, f'page_{i + 1}.png')
        page.save(output_filepath, 'PNG')

2.使用 Tesseract OCR 提取文本

为了提取 PDF 页面中的中文内容,我们使用了 Tesseract OCR,并结合一些优化配置,提高了文本识别的准确性。
代码如下(示例):

def extract_text_with_ocr(self, image_path):
    custom_config = r'--oem 3 --psm 6'
    return pytesseract.image_to_string(image_path, lang='chi_sim', config=custom_config)

3.图表检测与提取

我们使用 OpenCV 的图像处理技术来检测页面中的图表,首先通过灰度化和边缘检测,然后使用轮廓检测找到可能的图表区域。
代码如下(示例):

def detect_charts_in_image(self, image_path):
    image = cv2.imread(image_path)
    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    binary = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 15, 10)
    edges = cv2.Canny(binary, 100, 200, apertureSize=3)
    # 使用形态学操作来清理边缘
    dilated = cv2.dilate(edges, kernel, iterations=2)
    contours, _ = cv2.findContours(dilated, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    for contour in contours:
        x, y, w, h = cv2.boundingRect(contour)
        if w > 150 and h > 150 and (0.5 < w / h < 2.0):
            chart = image[y:y + h, x:x + w]
            chart_filename = os.path.join(self.image_folder, f"chart_{os.path.basename(image_path)}.png")
            cv2.imwrite(chart_filename, chart)
            self.extracted_charts.append(chart_filename)

4. 自动生成目录

通过正则表达式匹配提取到的文本中的章节标题,并根据这些信息生成可点击的目录,使用户能够快速定位到特定页面。

def generate_toc(self):
    title_pattern = re.compile(r'^\d+(\.\d+)+\s+([^\d\W]+.*)', re.MULTILINE)
    for image_filename in self.get_sorted_image_files():
        text = self.extract_text_with_ocr(os.path.join(self.image_folder, image_filename))
        for match in title_pattern.finditer(text):
            title = match.group(0).strip()
            page = image_filename.split('_')[1].split('.')[0]
            self.toc_data.append({'title': title, 'page': int(page)})
    return self.toc_data

5. 图形用户界面实现

使用 PyQt 实现了直观的用户界面,并通过多线程处理来保证在处理大文件时界面不会卡顿。用户可以通过界面查看提取的文本、图表和目录,并且可以在不同页面间进行跳转。

6.优化与改进

为了提升用户体验,我对应用进行了多个优化:
添加了进度条和状态指示器,以便用户在处理大型 PDF 文件时了解进度。
改善了图表检测算法,使得识别更加准确。
优化了异常处理和内存管理,确保程序在长时间运行时仍然稳定。

7.项目优化与经验总结

多线程处理:使用 QThread 进行多线程操作,防止主线程被阻塞,提升了用户体验。
OCR 配置调整:通过调整 Tesseract 的参数,提高了文本提取的精度和效率。
界面交互设计:采用 PyQt 的 QStackedWidget 实现了多页面切换,增加了界面的可操作性和用户友好度。

8.完整代码

主界面模块:


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        self.setWindowTitle("PDF Processing Application")
        self.setGeometry(100, 100, 1000, 800)

        self.stack = QStackedWidget()
        self.setCentralWidget(self.stack)

        # 主页面设置
        self.main_page = QWidget()
        self.setup_main_page()
        self.stack.addWidget(self.main_page)

        # 目录页面设置
        self.toc_page = QWidget()
        self.setup_toc_page()
        self.stack.addWidget(self.toc_page)

        # 页面展示页面设置
        self.page_view_page = QWidget()
        self.setup_page_view_page()
        self.stack.addWidget(self.page_view_page)

        self.pdf_processor = PDFProcessor()

    def setup_main_page(self):
        layout = QVBoxLayout()

        upload_button = QPushButton("Upload PDF File")
        upload_button.clicked.connect(self.upload_pdf)
        layout.addWidget(upload_button)

        process_button = QPushButton("Process PDF")
        process_button.clicked.connect(self.process_pdf)
        layout.addWidget(process_button)

        # 功能按钮
        text_button = QPushButton("Show Extracted Text")
        text_button.clicked.connect(self.show_extracted_text)
        layout.addWidget(text_button)

        chart_button = QPushButton("Show Extracted Charts")
        chart_button.clicked.connect(self.show_extracted_charts)
        layout.addWidget(chart_button)

        toc_button = QPushButton("Show TOC")
        toc_button.clicked.connect(self.show_toc)
        layout.addWidget(toc_button)

        self.main_page.setLayout(layout)

    def setup_toc_page(self):
        layout = QVBoxLayout()
        self.toc_list_widget = QListWidget()
        self.toc_list_widget.itemClicked.connect(self.show_selected_page)

        back_button = QPushButton("Return to Main Menu")
        back_button.clicked.connect(self.return_to_main_page)

        layout.addWidget(self.toc_list_widget)
        layout.addWidget(back_button)
        self.toc_page.setLayout(layout)

    def setup_page_view_page(self):
        layout = QVBoxLayout()
        scroll_area = QScrollArea()  # 添加滚动区域
        scroll_area.setWidgetResizable(True)

        scroll_content = QWidget()
        scroll_layout = QVBoxLayout(scroll_content)

        # 创建一个容器用于存放文本和图表内容
        self.text_area = QWidget()
        self.chart_area = QWidget()

        # 文本显示区域
        text_layout = QVBoxLayout()
        self.page_text_label = QLabel("Page content will be displayed here.")
        self.page_text_label.setWordWrap(True)  # 允许自动换行
        text_layout.addWidget(self.page_text_label)
        self.text_area.setLayout(text_layout)

        # 图表显示区域
        chart_layout = QVBoxLayout()
        self.page_image_label = QLabel()
        self.page_image_label.setFixedSize(800, 1000)
        chart_layout.addWidget(self.page_image_label)
        self.chart_area.setLayout(chart_layout)

        back_button = QPushButton("Return to TOC")
        back_button.clicked.connect(self.return_to_toc)

        scroll_layout.addWidget(back_button)
        scroll_layout.addWidget(self.text_area)
        scroll_layout.addWidget(self.chart_area)

        scroll_area.setWidget(scroll_content)
        layout.addWidget(scroll_area)
        self.page_view_page.setLayout(layout)

        # 初始状态隐藏图表区域
        self.chart_area.hide()

    def upload_pdf(self):
        file_path, _ = QFileDialog.getOpenFileName(self, "Select PDF File", "", "PDF Files (*.pdf)")
        if file_path:
            self.pdf_processor.set_pdf_path(file_path)

    def process_pdf(self):
        if not self.pdf_processor.pdf_path:
            QMessageBox.warning(self, "Warning", "Please upload a PDF file before processing.")
            return

        self.worker = PDFWorker(self.pdf_processor)
        self.worker.update_toc_signal.connect(self.update_toc_page)
        self.worker.start()

    def update_toc_page(self, toc_data):
        self.toc_list_widget.clear()
        for item in toc_data:
            self.toc_list_widget.addItem(f"{item['title']} - Page {item['page']}")
        self.stack.setCurrentWidget(self.toc_page)

    def show_selected_page(self, item):
        page_number = int(item.text().split(' - Page ')[1])
        page_text, _ = self.pdf_processor.get_page_content(page_number)
        self.page_text_label.setText(page_text)
        self.display_extracted_chart(page_number)
        self.stack.setCurrentWidget(self.page_view_page)

    def display_extracted_chart(self, page_number):
        chart_image_path = self.pdf_processor.get_chart_image_path(page_number)
        if chart_image_path:
            pixmap = QPixmap(chart_image_path)
            self.page_image_label.setPixmap(pixmap.scaled(self.page_image_label.size(), aspectRatioMode=1))
        else:
            self.page_image_label.clear()

    def show_extracted_text(self):
        text = self.pdf_processor.get_all_extracted_text()
        self.page_text_label.setText(text)
        self.chart_area.hide()  # 隐藏图表区域
        self.text_area.show()  # 显示文本区域
        self.stack.setCurrentWidget(self.page_view_page)

    def show_extracted_charts(self):
        chart_image_path = self.pdf_processor.get_first_chart_path()
        if chart_image_path:
            pixmap = QPixmap(chart_image_path)
            self.page_image_label.setPixmap(pixmap.scaled(self.page_image_label.size(), aspectRatioMode=1))
        self.text_area.hide()  # 隐藏文本区域
        self.chart_area.show()  # 显示图表区域
        self.stack.setCurrentWidget(self.page_view_page)

    def show_toc(self):
        self.stack.setCurrentWidget(self.toc_page)

    def return_to_main_page(self):
        self.stack.setCurrentWidget(self.main_page)

    def return_to_toc(self):
        self.stack.setCurrentWidget(self.toc_page)

处理功能模块:

class PDFProcessor:
    def __init__(self):
        self.pdf_path = None
        self.image_folder = "output_images/"
        self.extracted_charts = []

    def set_pdf_path(self, pdf_path):
        self.pdf_path = pdf_path

    def clear_output_folder(self):
        if os.path.exists(self.image_folder):
            for file in os.listdir(self.image_folder):
                file_path = os.path.join(self.image_folder, file)
                if os.path.isfile(file_path):
                    os.remove(file_path)
        else:
            os.makedirs(self.image_folder)

    def convert_pdf_to_images(self):
        pages = convert_from_path(self.pdf_path, 300)
        for i, page in enumerate(pages):
            output_filepath = os.path.join(self.image_folder, f'page_{i + 1}.png')
            page.save(output_filepath, 'PNG')

    def extract_text_with_ocr(self, image_path):
        return pytesseract.image_to_string(image_path, lang='chi_sim')

    def generate_toc(self):
        self.toc_data = []
        title_pattern = re.compile(r'^\d+(\.\d+)+\s+([^\d\W]+.*)', re.MULTILINE)
        image_files = sorted(
            [f for f in os.listdir(self.image_folder) if f.startswith('page_') and f.endswith('.png')],
            key=lambda f: int(f.split('_')[1].split('.')[0])
        )

        for image_filename in image_files:
            image_path = os.path.join(self.image_folder, image_filename)
            text = self.extract_text_with_ocr(image_path)
            for match in title_pattern.finditer(text):
                title = match.group(0).strip()
                page = image_filename.split('_')[1].split('.')[0]
                self.toc_data.append({'title': title, 'page': int(page)})
        return self.toc_data

    def extract_charts_from_images(self):
        for image_file in sorted(os.listdir(self.image_folder)):
            if image_file.startswith('page_'):
                image_path = os.path.join(self.image_folder, image_file)
                self.detect_charts_in_image(image_path)

    def detect_charts_in_image(self, image_path):
        image = cv2.imread(image_path)
        gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

        # 自适应阈值
        binary = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 15, 10)
        edges = cv2.Canny(binary, 100, 200, apertureSize=3)

        kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (15, 15))
        dilated = cv2.dilate(edges, kernel, iterations=2)
        eroded = cv2.erode(dilated, kernel, iterations=1)
        cleaned = cv2.dilate(eroded, kernel, iterations=1)

        contours, _ = cv2.findContours(dilated, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
        for contour in contours:
            x, y, w, h = cv2.boundingRect(contour)
            if w > 150 and h > 150 and (0.5 < w / h < 2.0) and (cv2.contourArea(contour) > 10000):
                chart = image[y:y + h, x:x + w]
                chart_filename = os.path.join(self.image_folder, f"chart_{os.path.basename(image_path)}.png")
                cv2.imwrite(chart_filename, chart)
                self.extracted_charts.append(chart_filename)

    def get_all_extracted_text(self):
        all_text = ""
        for file in sorted(os.listdir(self.image_folder)):
            if file.startswith('page_'):
                all_text += self.extract_text_with_ocr(os.path.join(self.image_folder, file))
        return all_text

    def get_first_chart_path(self):
        return os.path.join(self.image_folder, self.extracted_charts[0]) if self.extracted_charts else None

    def get_chart_image_path(self, page_number):
        return next((chart for chart in self.extracted_charts if f"page_{page_number}" in chart), None)

    def get_page_content(self, page_number):
        image_path = os.path.join(self.image_folder, f'page_{page_number}.png')
        return self.extract_text_with_ocr(image_path), image_path


总结

在这篇文章中,我分享了如何使用 PyQt 和 OpenCV 构建一个功能强大的 PDF 处理工具。这种工具在学术研究、数据分析等领域有广泛应用。通过模块化设计和多线程处理,我不仅提升了代码的可维护性,还改善了用户体验。后面我会尝试python的flask框架,还有先学一下如何打包软件。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值