python

关注公众号 jb51net

关闭
首页 > 脚本专栏 > python > Python增强PDF清晰度

基于Python编写一个PDF清晰度增强工具全解析(附完整源码)

作者:创客白泽

在日常办公和学习中,我们经常会遇到扫描版PDF文件模糊不清的问题,本文将介绍一款基于Python开发的PDF智能增强工具,它能够通过多维度图像处理算法自动提升PDF文件的清晰度和可读性,有需要的小伙伴可以了解下

概述

在日常办公和学习中,我们经常会遇到扫描版PDF文件模糊不清的问题。本文将介绍一款基于Python开发的PDF智能增强工具,它能够通过多维度图像处理算法自动提升PDF文件的清晰度和可读性。

工具核心价值

功能特性

本工具主要提供以下核心功能:

功能描述技术实现
锐化增强提升文字边缘清晰度PIL.ImageEnhance.Sharpness
对比度调整改善文档可读性PIL.ImageEnhance.Contrast
亮度优化自动平衡明暗区域PIL.ImageEnhance.Brightness
智能去噪减少扫描件噪点OpenCV CLAHE + 平滑滤波
高DPI输出支持最高600DPI输出pdf2image + img2pdf
批量处理自动处理多页文档多线程处理

效果展示

处理前 vs 处理后对比

表1:处理效果对比表

关键改进指标:

软件使用说明

安装步骤

环境准备

# 创建虚拟环境
python -m venv pdf-enhancer
source pdf-enhancer/bin/activate  # Linux/Mac
pdf-enhancer\Scripts\activate    # Windows
# 安装依赖
pip install -r requirements.txt

requirements.txt内容:

PyQt5==5.15.4
opencv-python==4.5.3.56
pillow==8.4.0
pdf2image==1.16.0
img2pdf==0.4.4
numpy==1.21.3

Poppler配置(Windows用户必需):

使用流程

代码深度解析

核心处理类 PDFProcessor

class PDFProcessor(QThread):
    """多线程PDF处理核心类"""
    def enhance_image(self, image):
        # 多阶段处理流程
        pil_img = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
        # 1. 亮度调整
        enhancer = ImageEnhance.Brightness(pil_img)
        pil_img = enhancer.enhance(self.brightness_factor)
        # 2. 对比度增强
        enhancer = ImageEnhance.Contrast(pil_img)
        pil_img = enhancer.enhance(self.contrast_factor)
        # 3. 锐化处理
        enhancer = ImageEnhance.Sharpness(pil_img)
        pil_img = enhancer.enhance(self.sharpen_factor)
        # 4. 去噪处理
        if self.denoise:
            pil_img = pil_img.filter(ImageFilter.SMOOTH)
        # 5. CLAHE增强
        cv_img = cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR)
        lab = cv2.cvtColor(cv_img, cv2.COLOR_BGR2LAB)
        l, a, b = cv2.split(lab)
        clahe = cv2.createCLAHE(clipLimit=3.0, tileGridSize=(8, 8))
        cl = clahe.apply(l)
        return cv2.cvtColor(cv2.merge((cl, a, b)), cv2.COLOR_LAB2BGR)

关键技术点

图像处理流水线

自适应直方图均衡化(CLAHE)

多线程处理

   class PDFProcessor(QThread):
       progress_updated = pyqtSignal(int)
       status_updated = pyqtSignal(str)
       
       def run(self):
           # PDF转图像
           images = convert_from_path(self.input_path, dpi=self.dpi)
           
           for i, img in enumerate(images):
               # 更新进度
               self.progress_updated.emit(int((i+1)/len(images)*100))
               # 处理单页
               processed = self.enhance_image(np.array(img))
               # 保存结果
               ...

源码下载

import os
import sys
import cv2
import numpy as np
from PIL import Image, ImageEnhance, ImageFilter
from pdf2image import convert_from_path
import img2pdf
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, 
                            QLabel, QPushButton, QFileDialog, QSlider, QDoubleSpinBox, 
                            QProgressBar, QCheckBox, QGroupBox, QMessageBox)
from PyQt5.QtCore import Qt, QThread, pyqtSignal
from PyQt5.QtGui import QIcon, QDragEnterEvent, QDropEvent


class PDFProcessor(QThread):
    progress_updated = pyqtSignal(int)
    status_updated = pyqtSignal(str)
    finished = pyqtSignal(str)

    def __init__(self, input_path, output_path, sharpen_factor=2.0, contrast_factor=1.5, 
                 brightness_factor=1.0, denoise=True, dpi=300, poppler_path=None):
        super().__init__()
        self.input_path = input_path
        self.output_path = output_path
        self.sharpen_factor = sharpen_factor
        self.contrast_factor = contrast_factor
        self.brightness_factor = brightness_factor
        self.denoise = denoise
        self.dpi = dpi
        self.poppler_path = poppler_path
        self.canceled = False

    def run(self):
        try:
            # 转换PDF为图像
            self.status_updated.emit("📖 正在加载PDF文件...")
            images = convert_from_path(
                self.input_path, 
                dpi=self.dpi,
                poppler_path=self.poppler_path
            )
            
            processed_images = []
            total_pages = len(images)
            
            for i, img in enumerate(images):
                if self.canceled:
                    self.status_updated.emit("❌ 处理已取消")
                    return
                
                self.status_updated.emit(f"🖼️ 正在处理第 {i+1}/{total_pages} 页...")
                self.progress_updated.emit(int((i + 1) / total_pages * 100))
                
                # 转换为OpenCV格式
                open_cv_image = np.array(img)
                open_cv_image = open_cv_image[:, :, ::-1].copy()  # RGB to BGR
                
                # 图像处理
                processed = self.enhance_image(open_cv_image)
                processed_images.append(processed)
            
            if not self.canceled:
                self.status_updated.emit("📤 正在生成PDF文件...")
                self.create_pdf(processed_images)
                self.finished.emit(self.output_path)
        
        except Exception as e:
            self.status_updated.emit(f"❌ 错误: {str(e)}")
            self.finished.emit("")

    def enhance_image(self, image):
        # 转换为PIL图像以便处理
        pil_img = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
        
        # 亮度调整
        enhancer = ImageEnhance.Brightness(pil_img)
        pil_img = enhancer.enhance(self.brightness_factor)
        
        # 对比度调整
        enhancer = ImageEnhance.Contrast(pil_img)
        pil_img = enhancer.enhance(self.contrast_factor)
        
        # 锐化
        enhancer = ImageEnhance.Sharpness(pil_img)
        pil_img = enhancer.enhance(self.sharpen_factor)
        
        # 去噪
        if self.denoise:
            pil_img = pil_img.filter(ImageFilter.SMOOTH)
        
        # 转换为OpenCV格式进行进一步处理
        cv_img = cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR)
        
        # 自适应直方图均衡化 (CLAHE)
        lab = cv2.cvtColor(cv_img, cv2.COLOR_BGR2LAB)
        l, a, b = cv2.split(lab)
        clahe = cv2.createCLAHE(clipLimit=3.0, tileGridSize=(8, 8))
        cl = clahe.apply(l)
        limg = cv2.merge((cl, a, b))
        cv_img = cv2.cvtColor(limg, cv2.COLOR_LAB2BGR)
        
        return cv_img

    def create_pdf(self, images):
        # 临时保存处理后的图像
        temp_images = []
        for i, img in enumerate(images):
            temp_path = f"temp_page_{i}.jpg"
            cv2.imwrite(temp_path, img, [int(cv2.IMWRITE_JPEG_QUALITY), 90])
            temp_images.append(temp_path)
        
        # 转换为PDF
        with open(self.output_path, "wb") as f:
            f.write(img2pdf.convert(temp_images))
        
        # 清理临时文件
        for temp_img in temp_images:
            os.remove(temp_img)

    def cancel(self):
        self.canceled = True


class PDFEnhancerApp(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("📄 PDF 清晰化处理工具")
        self.setWindowIcon(QIcon("icon.png"))  # 请准备一个图标文件或删除此行
        self.setGeometry(100, 100, 600, 500)  # 调整窗口大小
        
        self.input_file = ""
        self.output_file = ""
        self.processor = None
        
        # 设置poppler路径
        self.poppler_path = None
        if sys.platform == 'win32':
            possible_paths = [
                r".\poppler\Library\bin",
                r"C:\poppler\Library\bin",
                r"D:\Program Files\poppler\Library\bin",
                r"C:\Program Files (x86)\poppler\Library\bin"
            ]
            for path in possible_paths:
                if os.path.exists(path):
                    self.poppler_path = path
                    break
        
        self.init_ui()
    
    def init_ui(self):
        main_widget = QWidget()
        self.setCentralWidget(main_widget)
        
        layout = QVBoxLayout()
        main_widget.setLayout(layout)
        
        # 文件拖放区域
        file_group = QGroupBox("📂 拖放PDF文件到这里")
        file_layout = QVBoxLayout()
        
        # 拖放框
        self.drop_label = QLabel("📭 拖放PDF文件到这里")
        self.drop_label.setAlignment(Qt.AlignCenter)
        self.drop_label.setStyleSheet("""
            QLabel {
                border: 3px dashed #aaa;
                padding: 40px;
                border-radius: 10px;
                font-size: 16px;
                color: #666;
            }
            QLabel:hover {
                border-color: #777;
                background-color: #f5f5f5;
            }
        """)
        self.drop_label.setAcceptDrops(True)
        file_layout.addWidget(self.drop_label)
        
        # 或者按钮
        or_label = QLabel("或")
        or_label.setAlignment(Qt.AlignCenter)
        or_label.setStyleSheet("font-size: 14px; color: #888;")
        file_layout.addWidget(or_label)
        
        # 选择文件按钮
        select_btn = QPushButton("📂 选择PDF文件")
        select_btn.setStyleSheet("""
            QPushButton {
                padding: 8px;
                font-size: 14px;
            }
        """)
        select_btn.clicked.connect(self.select_input_file)
        file_layout.addWidget(select_btn)
        
        file_group.setLayout(file_layout)
        layout.addWidget(file_group)
        
        # 文件信息显示
        self.file_info_label = QLabel("未选择文件")
        self.file_info_label.setStyleSheet("""
            QLabel {
                padding: 8px;
                font-size: 14px;
                color: #444;
            }
        """)
        layout.addWidget(self.file_info_label)
        
        # 处理参数区域
        param_group = QGroupBox("⚙️ 处理参数")
        param_layout = QVBoxLayout()
        
        # 锐化强度
        sharpen_layout = QHBoxLayout()
        sharpen_label = QLabel("🔍 锐化强度:")
        sharpen_layout.addWidget(sharpen_label)
        
        self.sharpen_slider = QSlider(Qt.Horizontal)
        self.sharpen_slider.setRange(0, 400)
        self.sharpen_slider.setValue(200)
        sharpen_layout.addWidget(self.sharpen_slider)
        
        self.sharpen_spin = QDoubleSpinBox()
        self.sharpen_spin.setRange(0, 4)
        self.sharpen_spin.setValue(2.0)
        self.sharpen_spin.setSingleStep(0.1)
        sharpen_layout.addWidget(self.sharpen_spin)
        
        param_layout.addLayout(sharpen_layout)
        
        # 对比度
        contrast_layout = QHBoxLayout()
        contrast_label = QLabel("🌈 对比度:")
        contrast_layout.addWidget(contrast_label)
        
        self.contrast_slider = QSlider(Qt.Horizontal)
        self.contrast_slider.setRange(50, 300)
        self.contrast_slider.setValue(150)
        contrast_layout.addWidget(self.contrast_slider)
        
        self.contrast_spin = QDoubleSpinBox()
        self.contrast_spin.setRange(0, 3)
        self.contrast_spin.setValue(1.5)
        self.contrast_spin.setSingleStep(0.1)
        contrast_layout.addWidget(self.contrast_spin)
        
        param_layout.addLayout(contrast_layout)
        
        # 亮度
        brightness_layout = QHBoxLayout()
        brightness_label = QLabel("💡 亮度:")
        brightness_layout.addWidget(brightness_label)
        
        self.brightness_slider = QSlider(Qt.Horizontal)
        self.brightness_slider.setRange(50, 200)
        self.brightness_slider.setValue(100)
        brightness_layout.addWidget(self.brightness_slider)
        
        self.brightness_spin = QDoubleSpinBox()
        self.brightness_spin.setRange(0, 2)
        self.brightness_spin.setValue(1.0)
        self.brightness_spin.setSingleStep(0.1)
        brightness_layout.addWidget(self.brightness_spin)
        
        param_layout.addLayout(brightness_layout)
        
        # DPI设置
        dpi_layout = QHBoxLayout()
        dpi_label = QLabel("📏 DPI:")
        dpi_layout.addWidget(dpi_label)
        
        self.dpi_spin = QDoubleSpinBox()
        self.dpi_spin.setRange(100, 600)
        self.dpi_spin.setValue(300)
        self.dpi_spin.setDecimals(0)
        dpi_layout.addWidget(self.dpi_spin)
        
        param_layout.addLayout(dpi_layout)
        
        # 去噪选项
        self.denoise_check = QCheckBox("🧹 启用去噪")
        self.denoise_check.setChecked(True)
        param_layout.addWidget(self.denoise_check)
        
        param_group.setLayout(param_layout)
        layout.addWidget(param_group)
        
        # 连接信号
        self.sharpen_slider.valueChanged.connect(lambda val: self.sharpen_spin.setValue(val / 100))
        self.sharpen_spin.valueChanged.connect(lambda val: self.sharpen_slider.setValue(int(val * 100)))
        self.contrast_slider.valueChanged.connect(lambda val: self.contrast_spin.setValue(val / 100))
        self.contrast_spin.valueChanged.connect(lambda val: self.contrast_slider.setValue(int(val * 100)))
        self.brightness_slider.valueChanged.connect(lambda val: self.brightness_spin.setValue(val / 100))
        self.brightness_spin.valueChanged.connect(lambda val: self.brightness_slider.setValue(int(val * 100)))
        
        # 处理按钮
        self.process_btn = QPushButton("🚀 开始处理")
        self.process_btn.setStyleSheet("""
            QPushButton {
                padding: 7px;
                font-size: 15px;
                font-weight: bold;
                
            }
        """)
        self.process_btn.clicked.connect(self.start_processing)
        layout.addWidget(self.process_btn)
        
        # 进度条
        self.progress_bar = QProgressBar()
        layout.addWidget(self.progress_bar)
        
        # 状态标签
        self.status_label = QLabel("✅ 准备就绪")
        layout.addWidget(self.status_label)
        
        # 启用拖放功能
        self.setAcceptDrops(True)
    
    def dragEnterEvent(self, event: QDragEnterEvent):
        if event.mimeData().hasUrls():
            urls = event.mimeData().urls()
            if len(urls) == 1 and urls[0].toLocalFile().lower().endswith('.pdf'):
                event.acceptProposedAction()
                self.drop_label.setStyleSheet("""
                    QLabel {
                        border: 3px dashed #4CAF50;
                        padding: 40px;
                        border-radius: 10px;
                        font-size: 16px;
                        background-color: #f0fff0;
                        color: #2E7D32;
                    }
                """)
            else:
                event.ignore()
        else:
            event.ignore()
    
    def dragLeaveEvent(self, event):
        self.drop_label.setStyleSheet("""
            QLabel {
                border: 3px dashed #aaa;
                padding: 40px;
                border-radius: 10px;
                font-size: 16px;
                color: #666;
            }
            QLabel:hover {
                border-color: #777;
                background-color: #f5f5f5;
            }
        """)
    
    def dropEvent(self, event: QDropEvent):
        self.drop_label.setStyleSheet("""
            QLabel {
                border: 3px dashed #aaa;
                padding: 40px;
                border-radius: 10px;
                font-size: 16px;
                color: #666;
            }
            QLabel:hover {
                border-color: #777;
                background-color: #f5f5f5;
            }
        """)
        
        urls = event.mimeData().urls()
        if urls and urls[0].isLocalFile():
            file_path = urls[0].toLocalFile()
            if file_path.lower().endswith('.pdf'):
                self.input_file = file_path
                self.file_info_label.setText(f"📄 已选择文件: {os.path.basename(file_path)}")
                
                # 自动设置输出文件名
                base_name = os.path.splitext(file_path)[0]
                self.output_file = f"{base_name}_processing.pdf"
            else:
                QMessageBox.warning(self, "⚠️ 警告", "请拖放PDF文件")
    
    def select_input_file(self):
        file_path, _ = QFileDialog.getOpenFileName(
            self, "选择PDF文件", "", "PDF文件 (*.pdf)")
        
        if file_path:
            self.input_file = file_path
            self.file_info_label.setText(f"📄 已选择文件: {os.path.basename(file_path)}")
            
            # 自动设置输出文件名
            base_name = os.path.splitext(file_path)[0]
            self.output_file = f"{base_name}_processing.pdf"
    
    def start_processing(self):
        if not self.input_file:
            QMessageBox.warning(self, "⚠️ 警告", "请先选择PDF文件")
            return
        
        if self.processor and self.processor.isRunning():
            QMessageBox.warning(self, "⚠️ 警告", "已有处理任务在进行中")
            return
        
        # 检查poppler路径
        if sys.platform == 'win32' and not self.poppler_path:
            reply = QMessageBox.question(
                self, 
                "❓ poppler路径未设置", 
                "在Windows上需要设置poppler路径才能处理PDF文件。\n"
                "是否继续尝试处理?(可能会失败)",
                QMessageBox.Yes | QMessageBox.No
            )
            if reply == QMessageBox.No:
                return
        
        # 创建处理器
        self.processor = PDFProcessor(
            self.input_file, 
            self.output_file, 
            self.sharpen_spin.value(), 
            self.contrast_spin.value(), 
            self.brightness_spin.value(), 
            self.denoise_check.isChecked(), 
            int(self.dpi_spin.value()),
            poppler_path=self.poppler_path
        )
        
        # 连接信号
        self.processor.progress_updated.connect(self.progress_bar.setValue)
        self.processor.status_updated.connect(self.status_label.setText)
        self.processor.finished.connect(self.processing_finished)
        
        # 禁用按钮
        self.process_btn.setEnabled(False)
        self.process_btn.setText("⏳ 处理中...")
        
        # 开始处理
        self.processor.start()
    
    def processing_finished(self, output_path):
        self.process_btn.setEnabled(True)
        self.process_btn.setText("🚀 开始处理")
        
        if output_path:
            self.status_label.setText(f"🎉 处理完成! 文件已保存到: {output_path}")
            QMessageBox.information(self, "✅ 完成", f"PDF处理完成!\n保存到: {output_path}")
        else:
            self.status_label.setText("❌ 处理失败")
        
        self.processor = None
    
    def closeEvent(self, event):
        if self.processor and self.processor.isRunning():
            reply = QMessageBox.question(
                self, '⚠️ 处理正在进行中', 
                'PDF处理仍在进行中,确定要退出吗?', 
                QMessageBox.Yes | QMessageBox.No, 
                QMessageBox.No
            )
            
            if reply == QMessageBox.Yes:
                self.processor.cancel()
                self.processor.wait()
                event.accept()
            else:
                event.ignore()
        else:
            event.accept()


if __name__ == "__main__":
    app = QApplication(sys.argv)
    
    # 设置样式
    app.setStyle("Fusion")
    
    window = PDFEnhancerApp()
    window.show()
    
    sys.exit(app.exec_())

项目结构:

pdf-enhancer/
├── main.py                # 主程序入口
├── requirements.txt       # 依赖文件
├── icon.png               # 应用图标
└── poppler/               # Windows依赖库

技术亮点总结

智能参数调节

# 信号连接示例
self.sharpen_slider.valueChanged.connect(
    lambda val: self.sharpen_spin.setValue(val/100))
self.sharpen_spin.valueChanged.connect(
    lambda val: self.sharpen_slider.setValue(int(val*100)))

健壮性设计

用户体验优化

未来改进方向

AI增强模块

云处理功能

跨平台支持

以上就是基于Python编写一个PDF清晰度增强工具全解析(附完整源码)的详细内容,更多关于Python增强PDF清晰度的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文