文章目录
前言
研究生入学不久,在导师给的一大堆推荐下选择了一个看起来能做的短期学习任务,处理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框架,还有先学一下如何打包软件。