Python自动化办公:扫描件旋转批处理脚本

你是不是也遇到过这样的烦恼?扫描了一堆文件,结果发现有些图片是歪的,有些是倒着的,一张张手动旋转调整,眼睛都看花了,手也点麻了。特别是处理PDF里的扫描件,导出图片后方向五花八门,整理起来简直是一场噩梦。

今天,我就来分享一个自己用了很久的“办公神器”——一个用Python写的扫描件旋转批处理脚本。它能自动从PDF里提取图片,智能判断图片该不该旋转、该往哪边转,然后批量处理好,最后还能一键集成到Windows右键菜单里,用起来就像系统自带功能一样方便。

这个脚本特别适合经常需要处理扫描文档、票据、合同的朋友。咱们不搞那些复杂的深度学习模型,就用最实在的图像处理技术,效果够用,速度够快,代码也清晰易懂。下面,我就手把手带你从零开始,把这个工具做出来。

1. 准备工作:安装必要的库

工欲善其事,必先利其器。我们先来把需要的Python库装好。打开你的命令行(CMD或终端),依次输入下面的命令:

pip install PyMuPDF  # 用来处理PDF,提取图片
pip install opencv-python  # 核心图像处理库
pip install Pillow  # 图像处理辅助库
pip install numpy  # 科学计算基础库

如果安装速度慢,可以加上国内的镜像源,比如:

pip install PyMuPDF -i https://pypi.tuna.tsinghua.edu.cn/simple

安装完成后,我们可以先简单验证一下。创建一个新的Python文件,比如叫 test_env.py,写入以下内容:

import fitz  # PyMuPDF
import cv2
from PIL import Image
import numpy as np

print("所有库导入成功!")
print(f"PyMuPDF版本: {fitz.__version__}")
print(f"OpenCV版本: {cv2.__version__}")
print(f"Pillow版本: {Image.__version__}")

运行一下,如果没报错,就说明环境准备好了。

2. 核心功能一:从PDF中提取图片

处理扫描件,第一步就是把PDF里的图片“抠”出来。我们用 PyMuPDF(也叫 fitz)来做这件事,它处理PDF非常高效。

import fitz
import os

def extract_images_from_pdf(pdf_path, output_folder):
    """
    从PDF文件中提取所有图片
    
    参数:
        pdf_path: PDF文件路径
        output_folder: 图片输出文件夹
    """
    # 创建输出文件夹
    os.makedirs(output_folder, exist_ok=True)
    
    # 打开PDF文档
    doc = fitz.open(pdf_path)
    image_count = 0
    
    print(f"正在处理PDF: {os.path.basename(pdf_path)}")
    print(f"总页数: {len(doc)}")
    
    # 遍历每一页
    for page_num in range(len(doc)):
        page = doc[page_num]
        
        # 获取当前页的所有图片
        image_list = page.get_images(full=True)
        
        # 遍历当前页的每张图片
        for img_index, img in enumerate(image_list):
            xref = img[0]  # 图片的交叉引用编号
            
            # 提取图片数据
            base_image = doc.extract_image(xref)
            image_bytes = base_image["image"]
            image_ext = base_image["ext"]
            
            # 生成图片文件名
            image_filename = f"page_{page_num+1:03d}_img_{img_index+1:03d}.{image_ext}"
            image_path = os.path.join(output_folder, image_filename)
            
            # 保存图片
            with open(image_path, "wb") as f:
                f.write(image_bytes)
            
            image_count += 1
            print(f"  已提取: {image_filename}")
    
    doc.close()
    print(f"\n提取完成!共提取 {image_count} 张图片到: {output_folder}")
    return output_folder

# 使用示例
if __name__ == "__main__":
    # 替换成你的PDF文件路径
    pdf_file = "example.pdf"
    output_dir = "extracted_images"
    
    if os.path.exists(pdf_file):
        extract_images_from_pdf(pdf_file, output_dir)
    else:
        print(f"文件不存在: {pdf_file}")

这段代码会遍历PDF的每一页,找到所有嵌入的图片,然后按照“页码_序号”的格式保存下来。这样,我们就有了待处理的图片素材。

3. 核心功能二:智能判断图片旋转角度

这是整个脚本最核心的部分——怎么让程序“看懂”图片是正的还是歪的?我们采用一种基于文字方向检测的实用方法:通过分析图片中水平线条的分布来判断方向。

简单来说,正常的文档(比如合同、发票)里面,文字大多是水平排列的。如果我们把图片旋转到不同角度,然后分析每个角度下水平方向的“线条强度”,强度最大的那个角度,很可能就是图片正确的方向。

import cv2
import numpy as np
from PIL import Image
import os

def detect_rotation_angle(image_path):
    """
    检测图片需要的旋转角度(0, 90, 180, 270度)
    
    返回:
        angle: 需要旋转的角度(0, 90, 180, 270)
        confidence: 置信度(0-1之间)
    """
    # 读取图片
    img = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
    if img is None:
        print(f"无法读取图片: {image_path}")
        return 0, 0.0
    
    # 1. 预处理:缩小图片加快处理速度
    height, width = img.shape
    scale = 800 / max(height, width)
    if scale < 1:
        new_width = int(width * scale)
        new_height = int(height * scale)
        img = cv2.resize(img, (new_width, new_height))
    
    # 2. 边缘检测:找到图片中的线条
    # 先用高斯模糊降噪
    img_blur = cv2.GaussianBlur(img, (5, 5), 0)
    # Canny边缘检测
    edges = cv2.Canny(img_blur, 50, 150)
    
    # 3. 霍夫变换检测直线
    lines = cv2.HoughLinesP(
        edges, 
        rho=1, 
        theta=np.pi/180, 
        threshold=50, 
        minLineLength=30, 
        maxLineGap=10
    )
    
    if lines is None:
        # 如果没有检测到明显线条,默认不需要旋转
        return 0, 0.0
    
    # 4. 分析直线的角度分布
    angles = []
    for line in lines:
        x1, y1, x2, y2 = line[0]
        angle = np.degrees(np.arctan2(y2 - y1, x2 - x1))
        # 只关注接近水平或垂直的线条
        if abs(angle) < 30 or abs(angle) > 60:
            angles.append(angle)
    
    if not angles:
        return 0, 0.0
    
    # 5. 将角度转换为0, 90, 180, 270度
    # 计算平均角度
    avg_angle = np.mean(angles)
    
    # 判断最接近哪个标准角度
    possible_angles = [0, 90, 180, 270, -90]
    closest_angle = min(possible_angles, key=lambda x: abs(x - avg_angle))
    
    # 计算置信度(基于角度分布的集中程度)
    angle_std = np.std(angles)
    confidence = max(0, 1 - angle_std / 45)  # 标准差越小,置信度越高
    
    # 角度修正:如果检测到-90度,实际上就是270度
    if closest_angle == -90:
        closest_angle = 270
    
    return closest_angle, confidence

def rotate_image_by_angle(image_path, angle, output_path=None):
    """
    按照指定角度旋转图片
    
    参数:
        image_path: 输入图片路径
        angle: 旋转角度(0, 90, 180, 270)
        output_path: 输出图片路径(如果为None则覆盖原文件)
    """
    if angle == 0:
        # 不需要旋转
        if output_path and output_path != image_path:
            img = Image.open(image_path)
            img.save(output_path)
        return
    
    # 打开图片
    img = Image.open(image_path)
    
    # 旋转图片
    rotated_img = img.rotate(angle, expand=True)
    
    # 保存图片
    if output_path is None:
        output_path = image_path
    
    rotated_img.save(output_path)
    return output_path

# 测试旋转检测功能
def test_rotation_detection():
    """测试图片旋转角度检测"""
    test_images = ["test1.jpg", "test2.jpg", "test3.jpg"]
    
    for img_file in test_images:
        if os.path.exists(img_file):
            angle, confidence = detect_rotation_angle(img_file)
            print(f"{img_file}: 检测到旋转角度 {angle}度, 置信度 {confidence:.2f}")
            
            if angle != 0 and confidence > 0.3:
                # 创建旋转后的副本
                base_name = os.path.splitext(img_file)[0]
                output_file = f"{base_name}_rotated{angle}.jpg"
                rotate_image_by_angle(img_file, angle, output_file)
                print(f"  已生成旋转后图片: {output_file}")
        else:
            print(f"测试图片不存在: {img_file}")

if __name__ == "__main__":
    test_rotation_detection()

这个检测方法虽然不是100%准确,但对于大多数扫描文档来说,效果已经相当不错了。置信度可以帮助我们判断检测结果的可信程度,一般大于0.3就可以认为是比较可靠的。

4. 核心功能三:批量处理与自动化

有了单张图片的处理能力,现在我们来实现批量处理功能。这个函数会遍历一个文件夹里的所有图片,自动检测并旋转,然后保存到指定位置。

import glob
from tqdm import tqdm  # 进度条库,让等待更有趣

def batch_process_images(input_folder, output_folder=None, min_confidence=0.3):
    """
    批量处理文件夹中的所有图片
    
    参数:
        input_folder: 输入图片文件夹
        output_folder: 输出文件夹(如果为None则覆盖原文件)
        min_confidence: 最小置信度,低于此值不进行旋转
    """
    # 获取所有图片文件
    image_extensions = ['*.jpg', '*.jpeg', '*.png', '*.bmp', '*.tiff']
    image_files = []
    
    for ext in image_extensions:
        image_files.extend(glob.glob(os.path.join(input_folder, ext)))
    
    if not image_files:
        print(f"在文件夹 {input_folder} 中没有找到图片文件")
        return
    
    print(f"找到 {len(image_files)} 张图片需要处理")
    
    # 创建输出文件夹
    if output_folder:
        os.makedirs(output_folder, exist_ok=True)
    
    # 统计信息
    stats = {
        'total': len(image_files),
        'rotated': 0,
        'skipped': 0,
        'failed': 0
    }
    
    # 使用进度条显示处理进度
    for image_file in tqdm(image_files, desc="处理图片"):
        try:
            # 检测旋转角度
            angle, confidence = detect_rotation_angle(image_file)
            
            # 确定输出路径
            if output_folder:
                filename = os.path.basename(image_file)
                output_file = os.path.join(output_folder, filename)
            else:
                output_file = image_file
            
            if angle != 0 and confidence >= min_confidence:
                # 需要旋转且置信度足够
                rotate_image_by_angle(image_file, angle, output_file)
                stats['rotated'] += 1
                
                # 记录旋转信息
                base_name = os.path.splitext(os.path.basename(image_file))[0]
                log_msg = f"{base_name}: 旋转 {angle}度 (置信度: {confidence:.2f})"
                tqdm.write(log_msg)
            else:
                # 不需要旋转或置信度不足
                if output_folder and output_file != image_file:
                    # 复制到输出文件夹
                    import shutil
                    shutil.copy2(image_file, output_file)
                stats['skipped'] += 1
                
        except Exception as e:
            print(f"处理图片 {image_file} 时出错: {str(e)}")
            stats['failed'] += 1
    
    # 打印统计信息
    print("\n" + "="*50)
    print("处理完成!统计信息:")
    print(f"  总共处理: {stats['total']} 张图片")
    print(f"  成功旋转: {stats['rotated']} 张")
    print(f"  跳过处理: {stats['skipped']} 张")
    print(f"  处理失败: {stats['failed']} 张")
    print("="*50)

# 使用示例
if __name__ == "__main__":
    # 批量处理示例
    input_dir = "extracted_images"  # 之前从PDF提取的图片
    output_dir = "rotated_images"   # 旋转后的图片
    
    if os.path.exists(input_dir):
        batch_process_images(input_dir, output_dir, min_confidence=0.3)
    else:
        print(f"输入文件夹不存在: {input_dir}")

这个批量处理函数还加了个进度条,处理大量图片时能看到进度,不会觉得程序“卡死”了。

5. 完整工作流:从PDF到整理好的图片

现在我们把前面的功能串起来,形成一个完整的工作流:输入一个PDF,自动提取图片、智能旋转、批量保存。

def process_pdf_to_rotated_images(pdf_path, output_base_folder="processed_output"):
    """
    完整的PDF处理流程:提取图片 -> 旋转校正 -> 保存结果
    """
    # 1. 创建输出文件夹结构
    pdf_name = os.path.splitext(os.path.basename(pdf_path))[0]
    output_folder = os.path.join(output_base_folder, pdf_name)
    
    extracted_folder = os.path.join(output_folder, "extracted")
    rotated_folder = os.path.join(output_folder, "rotated")
    
    print("="*60)
    print(f"开始处理PDF: {os.path.basename(pdf_path)}")
    print("="*60)
    
    # 2. 从PDF提取图片
    print("\n步骤1: 从PDF提取图片...")
    extract_images_from_pdf(pdf_path, extracted_folder)
    
    # 3. 批量旋转图片
    print("\n步骤2: 智能旋转校正图片...")
    batch_process_images(extracted_folder, rotated_folder, min_confidence=0.3)
    
    # 4. 生成处理报告
    print("\n步骤3: 生成处理报告...")
    rotated_count = len(glob.glob(os.path.join(rotated_folder, "*.*")))
    
    report_file = os.path.join(output_folder, "处理报告.txt")
    with open(report_file, "w", encoding="utf-8") as f:
        f.write(f"PDF文件: {os.path.basename(pdf_path)}\n")
        f.write(f"处理时间: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
        f.write(f"提取图片数: {len(glob.glob(os.path.join(extracted_folder, '*.*')))}\n")
        f.write(f"旋转后图片数: {rotated_count}\n")
        f.write(f"输出文件夹: {rotated_folder}\n")
    
    print("\n" + "="*60)
    print("处理完成!")
    print(f"所有文件保存在: {output_folder}")
    print(f"旋转后的图片在: {rotated_folder}")
    print(f"处理报告: {report_file}")
    print("="*60)
    
    return rotated_folder

# 使用示例
if __name__ == "__main__":
    import datetime
    
    # 处理单个PDF
    pdf_file = "需要处理的文档.pdf"
    
    if os.path.exists(pdf_file):
        process_pdf_to_rotated_images(pdf_file)
    else:
        print(f"PDF文件不存在: {pdf_file}")
        print("\n你可以:")
        print("1. 将PDF文件放在当前目录")
        print("2. 修改代码中的pdf_file变量为你的PDF路径")
        print("3. 或者使用下面的方式处理整个文件夹的PDF")
        
        # 批量处理文件夹中所有PDF的示例
        pdf_folder = "pdf_files"
        if os.path.exists(pdf_folder):
            pdf_files = glob.glob(os.path.join(pdf_folder, "*.pdf"))
            for pdf in pdf_files:
                process_pdf_to_rotated_images(pdf)

这个完整流程基本上能满足大部分扫描件处理需求了。但如果我们想更方便一点,比如直接在文件上右键就能处理,那就要看下一个进阶功能了。

6. 进阶功能:集成到Windows右键菜单

这是让这个脚本真正变成“办公神器”的一步。通过修改Windows注册表,我们可以把脚本添加到右键菜单中,这样在任何PDF文件上右键,就能直接调用我们的处理脚本。

重要提示:修改注册表有风险,请先备份重要数据!下面的代码会创建一个 .reg 文件,需要手动导入。

def create_windows_context_menu_reg():
    """
    创建Windows右键菜单注册表文件
    注意:需要以管理员权限运行此脚本生成的reg文件
    """
    # 获取当前脚本的绝对路径
    script_path = os.path.abspath(__file__)
    
    # 创建批处理文件
    bat_content = f'''@echo off
echo 正在处理PDF文件: %1
python "{script_path}" --process-pdf "%1"
pause
'''
    
    bat_file = "process_pdf.bat"
    with open(bat_file, "w", encoding="gbk") as f:  # Windows命令行用GBK编码
        f.write(bat_content)
    
    print(f"已创建批处理文件: {bat_file}")
    
    # 创建注册表文件
    reg_content = f'''Windows Registry Editor Version 5.00

[HKEY_CLASSES_ROOT\\SystemFileAssociations\\.pdf\\shell\\旋转处理扫描件]
@="智能旋转扫描件"

[HKEY_CLASSES_ROOT\\SystemFileAssociations\\.pdf\\shell\\旋转处理扫描件\\command]
@="\\"{os.path.abspath(bat_file)}\\" \\"%1\\""
'''
    
    reg_file = "add_context_menu.reg"
    with open(reg_file, "w", encoding="utf-16") as f:  # REG文件需要UTF-16编码
        f.write(reg_content)
    
    print(f"已创建注册表文件: {reg_file}")
    print("\n使用方法:")
    print("1. 双击运行 add_context_menu.reg 文件")
    print("2. 选择'是'导入注册表")
    print("3. 现在在任意PDF文件上右键,就能看到'智能旋转扫描件'选项了")
    print("\n注意:")
    print("- 需要以管理员权限运行")
    print("- 首次使用可能需要重启文件资源管理器")
    
    return reg_file

# 修改主函数,支持命令行参数
if __name__ == "__main__":
    import sys
    import argparse
    
    parser = argparse.ArgumentParser(description='扫描件旋转批处理工具')
    parser.add_argument('--process-pdf', type=str, help='要处理的PDF文件路径')
    parser.add_argument('--create-menu', action='store_true', help='创建Windows右键菜单')
    parser.add_argument('--input-folder', type=str, help='要处理的图片文件夹')
    parser.add_argument('--output-folder', type=str, help='输出文件夹')
    
    args = parser.parse_args()
    
    if args.create_menu:
        # 创建右键菜单
        create_windows_context_menu_reg()
    
    elif args.process_pdf:
        # 处理指定的PDF文件
        if os.path.exists(args.process_pdf):
            process_pdf_to_rotated_images(args.process_pdf)
        else:
            print(f"文件不存在: {args.process_pdf}")
    
    elif args.input_folder:
        # 处理图片文件夹
        output_folder = args.output_folder or "rotated_output"
        batch_process_images(args.input_folder, output_folder)
    
    else:
        # 交互式模式
        print("扫描件旋转批处理工具")
        print("="*40)
        print("请选择操作:")
        print("1. 处理单个PDF文件")
        print("2. 处理图片文件夹")
        print("3. 添加到Windows右键菜单")
        print("4. 测试旋转检测功能")
        
        choice = input("\n请输入选项 (1-4): ").strip()
        
        if choice == "1":
            pdf_file = input("请输入PDF文件路径: ").strip()
            if os.path.exists(pdf_file):
                process_pdf_to_rotated_images(pdf_file)
            else:
                print(f"文件不存在: {pdf_file}")
        
        elif choice == "2":
            input_dir = input("请输入图片文件夹路径: ").strip()
            output_dir = input("请输入输出文件夹路径 (直接回车使用默认): ").strip()
            if not output_dir:
                output_dir = "rotated_images"
            
            if os.path.exists(input_dir):
                batch_process_images(input_dir, output_dir)
            else:
                print(f"文件夹不存在: {input_dir}")
        
        elif choice == "3":
            create_windows_context_menu_reg()
        
        elif choice == "4":
            test_rotation_detection()
        
        else:
            print("无效选项")

添加了命令行参数支持后,我们的脚本就更灵活了。可以直接在命令行调用,也可以集成到其他工作流中。

7. 使用技巧与注意事项

在实际使用中,有几个小技巧可以让这个工具更好用:

  1. 处理前先预览:对于特别重要的文档,可以先处理几张看看效果,调整置信度阈值。
  2. 批量处理建议:一次不要处理太多文件,可以先分批次,避免内存不足。
  3. 备份原文件:脚本默认不会覆盖原PDF,但处理图片时如果选择覆盖,建议先备份。
  4. 特殊文档处理:有些文档本身就有倾斜的文字(比如艺术字体),这种可能需要手动处理。

如果你遇到检测不准的情况,可以尝试调整 detect_rotation_angle 函数中的参数:

  • 调整 cv2.Canny 的边缘检测阈值
  • 修改霍夫变换的 thresholdminLineLength
  • 改变置信度的计算方式

8. 总结

写到这里,一个完整的扫描件旋转批处理工具就完成了。从PDF提取图片,到智能判断旋转角度,再到批量处理和Windows集成,基本上覆盖了日常办公中扫描件处理的全流程。

这个脚本最好的地方在于,它完全用Python实现,代码透明,你可以根据自己的需求随意修改。比如,如果你想支持更多图片格式,就在 batch_process_images 函数里添加对应的扩展名;如果想提高检测精度,可以尝试更复杂的图像处理算法。

实际用下来,对于大多数扫描的文档、发票、合同,这个脚本的准确率还是挺高的,能节省大量的手动调整时间。特别是集成到右键菜单后,用起来就像系统自带功能一样顺手。

如果你在使用的过程中有任何问题,或者想到了什么改进的点,欢迎随时交流。自动化办公的乐趣,就在于把重复枯燥的工作交给程序,让自己有更多时间处理更有价值的事情。


获取更多AI镜像

想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐