作为一名摄影爱好者兼Python开发者,我开发了一套自动化图片处理工具,现已开源分享。这些工具能帮助摄影师快速完成常见后期处理工作,特别适合需要批量处理图片的场景。

LR-0004_信息模糊水印处理

工具清单与适用场景

1. 基础处理

  • 拼接.py

    • 功能:将多张图片拼接为全景图或网格排列图
    • 特色:智能识别相似边缘自动对齐
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    import os
    from PIL import Image

    supported_extensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp']


    def find_image_file(filename):
    """查找实际存在的图片文件(支持无扩展名或错误扩展名)"""
    # 如果输入包含扩展名
    if os.path.splitext(filename)[1]:
    if os.path.isfile(filename):
    return filename
    # 尝试纠正大小写
    base = os.path.splitext(filename)[0]
    for ext in supported_extensions:
    if os.path.isfile(base + ext.lower()):
    return base + ext.lower()
    # 无扩展名的情况
    else:
    for ext in supported_extensions:
    path = filename + ext
    if os.path.isfile(path):
    return path
    return None


    def main():
    images = []
    original_filenames = []
    max_width = 0

    print("""照片拼接工具
    ==========================
    1. 输入图片名称
    2. 输入 'done' 完成输入
    3. 输入 'exit' 退出程序
    """)

    while True:
    user_input = input("请输入图片名称 > ").strip()

    # 退出检测
    if user_input.lower() == 'exit':
    print("已退出程序")
    return

    # 完成输入检测
    if not user_input or user_input.lower() == 'done':
    if not images:
    print("错误:至少需要输入一张有效图片")
    continue
    break

    # 查找文件
    filepath = find_image_file(user_input)
    if not filepath:
    print(f" × 未找到文件: {user_input}(支持格式:{', '.join(supported_extensions)})")
    continue

    # 加载图片
    try:
    img = Image.open(filepath)
    img = img.convert('RGB')
    images.append(img)
    original_filenames.append(os.path.basename(filepath))
    max_width = max(max_width, img.width)
    print(f" √ 已加载: {filepath} ({img.width} x {img.height})")
    except Exception as e:
    print(f" × 无法处理文件 {filepath}: {str(e)}")
    continue

    # 调整图片尺寸
    resized_images = []
    total_height = 0
    print("\n正在调整图片尺寸...")
    for img in images:
    w_percent = max_width / img.width
    h_size = int(img.height * w_percent)
    resized_img = img.resize((max_width, h_size), Image.LANCZOS)
    resized_images.append(resized_img)
    total_height += h_size
    print(f" → 调整 {img.width} x {img.height}{max_width} x {h_size}")

    # 拼接图片
    combined_img = Image.new('RGB', (max_width, total_height))
    y_offset = 0
    info_data = []

    print("\n开始拼接图片...")
    for i, img in enumerate(resized_images):
    combined_img.paste(img, (0, y_offset))
    info_data.append({
    'filename': original_filenames[i],
    'y_start': y_offset,
    'height': img.height
    })
    print(f" ✓ 已拼接: {original_filenames[i]} (起始位置: {y_offset}, 高度: {img.height})")
    y_offset += img.height

    # 保存结果
    combined_img.save('combined.jpg')
    with open('combined_info.txt', 'w') as f:
    for entry in info_data:
    f.write(f"{entry['filename']},{entry['y_start']},{entry['height']}\n")

    print(f"""\n操作完成!
    生成文件:combined.jpg(尺寸:{max_width}x{total_height}
    combined_info.txt(包含 {len(info_data)} 条记录)""")


    if __name__ == "__main__":
    main()

    print("处理完成!按回车键退出...")
    aaa = input()

    拆分.py

    • 功能:将单张大图按指定行列拆分为网格小图
    • 应用场景:制作拼图素材、社交媒体九宫格图片
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    import os
    from PIL import Image
    import warnings

    warnings.filterwarnings("ignore")


    def main():
    # 自动检测文件路径
    combined_img_path = 'combined.jpg'
    info_file_path = 'combined_info.txt'

    print("""\n照片拆分工具(自动模式)
    ==========================
    检测当前目录下的:
    - 拼接文件:combined.jpg
    - 信息文件:combined_info.txt
    """)

    # 验证文件存在
    if not os.path.exists(combined_img_path):
    print(f"错误:找不到拼接文件 {combined_img_path}")
    return

    if not os.path.exists(info_file_path):
    print(f"错误:找不到信息文件 {info_file_path}")
    return

    try:
    # 加载拼接图片
    combined_img = Image.open(combined_img_path)
    img_width = combined_img.width
    print(f"√ 已加载拼接图片 ({img_width}x{combined_img.height})")
    except Exception as e:
    print(f"无法打开拼接图片: {e}")
    return

    try:
    # 读取信息文件
    with open(info_file_path) as f:
    lines = f.readlines()
    print(f"√ 已加载信息文件(包含 {len(lines)} 条记录)")
    except Exception as e:
    print(f"无法打开信息文件: {e}")
    return

    success_count = 0
    error_count = 0

    print("\n开始拆分图片...")
    for line_num, line in enumerate(lines, 1):
    parts = line.strip().split(',')
    if len(parts) != 3:
    print(f"× 第{line_num}行格式错误:{line.strip()}")
    error_count += 1
    continue

    filename, y_start, height = parts
    try:
    y = int(y_start)
    h = int(height)
    except ValueError:
    print(f"× 第{line_num}行数值错误:{line.strip()}")
    error_count += 1
    continue

    # 验证坐标有效性
    if y < 0 or h <= 0:
    print(f"× 第{line_num}行数值无效(y={y}, h={h})")
    error_count += 1
    continue

    if y + h > combined_img.height:
    print(f"× 第{line_num}行越界:{y + h} > 图片总高度 {combined_img.height}")
    error_count += 1
    continue

    # 创建输出目录(如果需要)
    # Create a single directory
    path = "拆分图片"
    if not os.path.exists(path):
    os.makedirs(path)


    # 执行裁剪
    try:
    crop_area = (0, y, img_width, y + h)
    cropped = combined_img.crop(crop_area)
    cropped.save('拆分图片//' + filename)
    print(f"√ 已保存:{filename} ({img_width}x{h})")
    success_count += 1
    except Exception as e:
    print(f"× 保存 {filename} 失败:{str(e)}")
    error_count += 1

    print(f"""\n操作完成!
    成功拆分:{success_count}
    失败记录:{error_count} 条""")


    if __name__ == "__main__":
    main()

    print("处理完成!按回车键退出...")
    aaa = input()

2. 版权保护

  • 添加水印.py

    • 功能:添加半透明文字水印
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    from PIL import Image
    import os
    import glob


    # from rich.progress import Progress


    def calculate_brightness(img):
    """计算图片区域的平均亮度"""
    pixels = img.convert("L").getdata()
    return sum(pixels) / len(pixels)


    def add_watermark(original_path, i, ii):
    # 创建输出目录
    original_dir = os.path.dirname(original_path)
    output_dir = os.path.join(original_dir, "添加水印")
    os.makedirs(output_dir, exist_ok=True) # 自动创建目录

    # 生成输出路径
    file_name = os.path.basename(original_path)
    output_path = os.path.join(output_dir, f"{os.path.splitext(file_name)[0]}_水印{os.path.splitext(file_name)[1]}")

    # 水印路径配置
    WATERMARKS = {
    "light": r"C:\Users\yyq09\Pictures\水印 - 黑.png", # 亮背景用黑水印
    "dark": r"C:\Users\yyq09\Pictures\水印 - 白.png" # 暗背景用白水印
    }

    # 打开原始图片
    with Image.open(original_path) as img:
    original_format = img.format # 保留原始格式信息
    # 转换格式并保留Alpha通道
    original = img.convert("RGBA")
    width, height = original.size

    # 计算底部区域亮度(取高度5%的区域)
    crop_height = max(50, int(height * 0.1))
    bottom_region = original.crop((width * 0.45, height - crop_height, width * 0.55, height))
    brightness = calculate_brightness(bottom_region)

    # 选择合适的水印
    watermark_path = WATERMARKS["light"] if brightness > 128 * 1.2 else WATERMARKS["dark"]

    try:
    with Image.open(watermark_path) as watermark:
    # 调整水印尺寸(最大宽度为原图的70%)
    wm_width, wm_height = watermark.size
    if width > height:
    max_width = int(width * 0.1)
    else:
    max_width = int(width * 0.17)
    scaling_factor = min(max_width / wm_width, 1.0)

    new_size = (
    int(wm_width * scaling_factor),
    int(wm_height * scaling_factor)
    )
    watermark = watermark.resize(new_size, Image.LANCZOS).convert("RGBA")

    # 计算水印位置
    x = (width - new_size[0]) // 2
    y = height - int(new_size[1] * 1.2) # 底部保留2%边距

    # 创建透明层合并水印
    composite = Image.new("RGBA", original.size)
    composite.paste(watermark, (x, y))
    result = Image.alpha_composite(original, composite)

    # 构建保存参数
    save_params = img.info.copy()

    # 保存结果(保持原始格式)
    # output_path = f"{os.path.splitext(original_path)[0]}_添加水印{os.path.splitext(original_path)[1]}"
    # 修改保存部分
    result.convert(img.mode).save(
    output_path,
    format=original_format,
    **save_params
    )

    print(f"已处理 {i}/{ii}: {os.path.basename(original_path)}")

    except FileNotFoundError:
    print(f"水印文件不存在:{watermark_path}")
    except Exception as e:
    print(f"处理失败:{original_path} - {str(e)}")


    if __name__ == "__main__":
    # 支持的图片格式
    extensions = ["jpg", "jpeg", "png", "bmp", "webp"]

    # # 并行处理(提升大图处理速度)
    # with Pool() as pool:
    # tasks = []
    # for ext in extensions:
    # tasks.extend(glob.glob(f"*.{ext}"))
    # pool.map(add_watermark, tasks)
    i = 1
    for ext in extensions:
    for file in glob.glob(f"*.{ext}"):
    add_watermark(file, i, len(glob.glob(f"*.{ext}")))
    i += 1

    print("处理完成!按回车键退出...")
    input()

  • 添加水印和拍摄信息.py

    • 高级功能:在基础水印上叠加EXIF信息
    • 自动读取:相机型号、光圈快门、GPS位置(需原图包含EXIF)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    from PIL import Image, ImageDraw, ImageFont, ImageOps
    import os
    import glob
    # from multiprocessing import Pool
    from PIL.ExifTags import TAGS
    import piexif
    from rich.progress import Progress

    # 新增字体路径配置(微软雅黑)
    FONT_PATH = "C:/Windows/Fonts/msyh.ttc" # Windows系统字体路径


    def get_exif_data(img):
    """获取EXIF信息并解析关键参数"""
    exif_data = {}
    try:
    if hasattr(img, '_getexif'):
    exif_info = img._getexif()
    if exif_info:
    for tag, value in exif_info.items():
    decoded = TAGS.get(tag, tag)
    exif_data[decoded] = value

    # 使用piexif补充读取(兼容更多格式)
    exif_dict = piexif.load(img.info.get('exif', b''))
    for ifd in exif_dict:
    for tag in exif_dict[ifd]:
    tag_name = piexif.TAGS[ifd][tag]["name"]
    exif_data[tag_name] = exif_dict[ifd][tag]
    except:
    pass

    # 参数解析逻辑
    def parse_param(param):
    if isinstance(param, tuple):
    return float(param[0]) / float(param[1])
    elif isinstance(param, int):
    return float(param)
    return None

    # 焦距处理
    focal_length = exif_data.get('FocalLength', exif_data.get('FocalLengthIn35mmFilm'))
    focal = f"{int(parse_param(focal_length))}mm" if focal_length else '--mm'

    # 光圈处理
    aperture = exif_data.get('FNumber', exif_data.get('ApertureValue'))
    f_number = f"f/{parse_param(aperture):.1f}" if aperture else 'f/--'

    # 快门速度处理
    exposure = exif_data.get('ExposureTime')
    if exposure:
    if isinstance(exposure, tuple):
    val = parse_param(exposure)
    shutter = f"1/{int(1 / val)}s" if val < 1 else f"{val:.0f}s"
    else:
    shutter = f"{exposure}s"
    else:
    shutter = '--s'

    # ISO处理
    iso = exif_data.get('ISOSpeedRatings', exif_data.get('PhotographicSensitivity'))
    iso_str = f"ISO{iso}" if iso else 'ISO---'

    return f"{focal} {f_number} {shutter} {iso_str}"


    def calculate_brightness(img):
    """计算图片区域的平均亮度"""
    pixels = img.convert("L").getdata()
    return sum(pixels) / len(pixels)


    def add_watermark(original_path, i, ii):
    # 创建输出目录
    original_dir = os.path.dirname(original_path)
    output_dir = os.path.join(original_dir, "添加水印和拍摄信息")
    os.makedirs(output_dir, exist_ok=True) # 自动创建目录

    # 生成输出路径
    file_name = os.path.basename(original_path)
    output_path = os.path.join(output_dir,
    f"{os.path.splitext(file_name)[0]}_水印和拍摄信息{os.path.splitext(file_name)[1]}")

    # 水印路径配置
    WATERMARKS = {
    "light": r"C:\Users\yyq09\Pictures\水印 - 黑.png", # 亮背景用黑水印
    "dark": r"C:\Users\yyq09\Pictures\水印 - 白.png" # 暗背景用白水印
    }

    # 打开原始图片
    with Image.open(original_path) as img:
    original_format = img.format # 保留原始格式信息
    # 转换格式并保留Alpha通道
    original = img.convert("RGBA")
    width, height = original.size

    # 计算底部区域亮度(取高度5%的区域)
    crop_height = max(50, int(height * 0.1))
    bottom_region = original.crop((width * 0.45, height - crop_height, width * 0.55, height))
    brightness = calculate_brightness(bottom_region)

    # 选择合适的水印
    watermark_path = WATERMARKS["light"] if brightness > 128 * 1.2 else WATERMARKS["dark"]

    try:
    with Image.open(watermark_path) as watermark:
    # 调整水印尺寸(最大宽度为原图的70%)
    wm_width, wm_height = watermark.size
    if width > height:
    max_width = int(width * 0.1)
    FONT_RATIO = 0.25 # 文字大小与水印高度的比例
    TEXT_MARGIN = 17 # 水印与文字间距
    down_length = 1.5 # 底边距离
    else:
    max_width = int(width * 0.17)
    FONT_RATIO = 0.25 # 文字大小与水印高度的比例
    TEXT_MARGIN = 20 # 水印与文字间距
    down_length = 1.5 # 底边距离
    scaling_factor = min(max_width / wm_width, 1.0)

    new_size = (
    int(wm_width * scaling_factor),
    int(wm_height * scaling_factor)
    )
    watermark = watermark.resize(new_size, Image.LANCZOS).convert("RGBA")

    # 计算水印位置
    x = (width - new_size[0]) // 2
    y = height - int(new_size[1] * down_length) # 底部保留边距

    # 创建透明层合并水印
    composite = Image.new("RGBA", original.size)
    composite.paste(watermark, (x, y))
    result = Image.alpha_composite(original, composite)

    # 在合成水印后添加文字信息
    exif_text = get_exif_data(img)
    draw = ImageDraw.Draw(result)

    # 自动选择字体大小(水印高度的40%)
    text_size = int(new_size[1] * FONT_RATIO)
    try:
    # 尝试加载微软雅黑字体
    font = ImageFont.truetype(FONT_PATH, text_size)
    except Exception as e:
    print(f"字体加载失败,使用默认字体:{str(e)}")
    font = ImageFont.load_default(text_size)

    # 计算文字位置(水印下方10像素)
    text_y = y + new_size[1] + TEXT_MARGIN
    text_width = font.getlength(exif_text)
    text_x = (width - text_width) // 2

    # 自动选择文字颜色(基于水印区域亮度)
    text_color = 'white' if calculate_brightness(bottom_region) < 128 * 1.2 else 'black'

    # 添加文字阴影增强可读性
    shadow_color = 'white' if text_color == 'white' else 'black'
    for offset in [(-1, -1), (-1, 1), (1, -1), (1, 1)]:
    draw.text((text_x + offset[0], text_y + offset[1]),
    exif_text, font=font, fill=shadow_color)

    # 绘制主文字
    draw.text((text_x, text_y), exif_text, font=font, fill=text_color)

    # 构建保存参数
    save_params = img.info.copy()

    # 保存结果(保持原始格式)
    result.convert(img.mode).save(
    output_path,
    format=original_format,
    **save_params
    )

    print(f"已处理 {i}/{ii}: {os.path.basename(original_path)}")

    except FileNotFoundError:
    print(f"水印文件不存在:{watermark_path}")
    except Exception as e:
    print(f"处理失败:{original_path} - {str(e)}")


    if __name__ == "__main__":
    # 支持的图片格式
    extensions = ["jpg", "jpeg", "png", "bmp", "webp"]

    # # 并行处理(提升大图处理速度)
    # with Pool() as pool:
    # tasks = []
    # for ext in extensions:
    # tasks.extend(glob.glob(f"*.{ext}"))
    # pool.map(add_watermark, tasks)

    i = 1
    for ext in extensions:
    for file in glob.glob(f"*.{ext}"):
    add_watermark(file, i, len(glob.glob(f"*.{ext}")))
    i += 1

    print("处理完成!按回车键退出...")
    input()

3. 艺术化处理

  • 添加高斯背景和拍摄信息.py

    • 二合一功能:同时实现背景模糊+参数展示
    • 特色布局:
      • 底部10%区域显示拍摄参数
      • 参数面板半透明渐变效果
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    288
    289
    290
    291
    292
    293
    294
    295
    296
    297
    298
    299
    300
    301
    302
    303
    304
    305
    306
    307
    308
    309
    310
    311
    312
    313
    314
    315
    316
    317
    318
    319
    320
    321
    322
    323
    324
    325
    326
    327
    328
    329
    330
    331
    332
    333
    334
    335
    336
    337
    338
    339
    340
    341
    342
    343
    344
    345
    346
    347
    348
    349
    350
    351
    352
    353
    354
    355
    356
    357
    358
    359
    360
    361
    362
    363
    364
    365
    366
    367
    368
    369
    370
    371
    from PIL import Image, ImageFilter, ImageDraw, ImageFont
    import piexif
    import os
    from rich.progress import Progress

    # Configuration constants
    WATERMARKS = {
    "light": r"C:\Users\yyq09\Pictures\水印 - 黑.png",
    "dark": r"C:\Users\yyq09\Pictures\水印 - 白.png"
    }
    FONT_PATH = "C:/Windows/Fonts/msyh.ttc"
    OUTPUT_DIRS = {
    "background": "高斯背景",
    "final": "信息模糊水印处理"
    }
    BLUR_RADIUS = 69
    BRIGHTNESS_THRESHOLD = 128 * 1.2


    def apply_drop_shadow(
    background: Image.Image,
    img: Image.Image,
    position: tuple[int, int]
    ) -> None:
    shadow_radius = int(min(img.size) * 0.015)
    shadow_opacity = 255

    """为图片添加阴影效果

    Args:
    background: 背景画布对象
    img: 原始图片对象
    position: 图片粘贴位置 (x, y)
    shadow_radius: 阴影扩散半径(默认10像素)
    shadow_opacity: 阴影透明度(0-255,默认80)
    """

    x, y = position
    width, height = img.size

    # 创建阴影层
    shadow_layer = Image.new("RGBA", (width + shadow_radius * 2, height + shadow_radius * 2))
    shadow_draw = ImageDraw.Draw(shadow_layer)

    # 绘制渐变阴影
    for i in range(shadow_radius, 0, -1):
    alpha = int(shadow_opacity * (shadow_radius / shadow_radius) ** 0.8)
    shadow_draw.rounded_rectangle(
    [(shadow_radius - i, shadow_radius - i),
    (width + shadow_radius + i - 1, height + shadow_radius + i - 1)],
    radius=int(min(width, height) * 0.03) + i,
    fill=(0, 0, 0, alpha))

    # 粘贴阴影到背景
    background.paste(shadow_layer, (x - shadow_radius, y - shadow_radius), shadow_layer)


    def add_rounded_corners(img: Image.Image, radius: int = 15) -> Image.Image:
    """为图片添加圆角效果

    Args:
    img: 原始图片对象
    radius: 圆角半径(默认15像素)

    Returns:
    PIL.Image.Image: 带圆角的图片对象
    """
    # 创建透明蒙版
    mask = Image.new('L', img.size, 0)
    draw = ImageDraw.Draw(mask)

    # 绘制圆角矩形
    draw.rounded_rectangle([(0, 0), img.size], radius, fill=255)

    # 应用蒙版
    if img.mode != 'RGBA':
    img = img.convert('RGBA')
    img.putalpha(mask)
    return img


    def process_image(image_path: str, index, ii) -> Image.Image:
    """为图片添加高斯模糊背景

    Args:
    image_path: 原始图片路径
    index: 当前处理序号

    Returns:
    PIL.Image.Image: 处理后的图片对象
    """
    with Image.open(image_path) as img:
    original_format = img.format
    img = img.convert("RGB") if img.mode != "RGB" else img
    width, height = img.size

    # 计算扩展尺寸
    is_landscape = width > height
    delta_x = calculate_padding(width, 0.03 if is_landscape else 0.05)
    delta_y_top = calculate_padding(height, 0.05 if is_landscape else 0.03)
    delta_y_bottom = calculate_padding(height, 0.12 if is_landscape else 0.08)

    new_width = width + 2 * delta_x
    new_height = height + delta_y_top + delta_y_bottom

    # 创建背景画布
    background = create_background(img, new_width, new_height, delta_x, delta_y_top, delta_y_bottom)

    # 应用高斯模糊
    blurred_background = background.filter(ImageFilter.GaussianBlur(radius=BLUR_RADIUS))

    rounded_img = add_rounded_corners(img.copy(), radius=int(min(width, height) * 0.035)) # 自适应圆角大小
    blurred_background.paste(rounded_img, (delta_x, delta_y_top), rounded_img) # 添加蒙版参数

    print(f"背景处理 {index}/{ii}: {os.path.basename(image_path)}")
    return blurred_background


    def calculate_padding(dimension: int, ratio: float) -> int:
    """计算扩展边距"""
    return max(1, int(dimension * ratio))


    def create_background(
    img: Image.Image,
    new_width: int,
    new_height: int,
    delta_x: int,
    delta_y_top: int,
    delta_y_bottom: int
    ) -> Image.Image:
    """创建扩展背景画布"""
    background = Image.new("RGB", (new_width, new_height))
    width, height = img.size

    # 填充顶部区域
    if delta_y_top > 0:
    top_section = img.crop((0, 0, width, delta_y_top))
    background.paste(top_section.resize((new_width, delta_y_top)), (0, 0))

    # 填充底部区域
    if delta_y_bottom > 0:
    bottom_section = img.crop((0, height - delta_y_bottom, width, height))
    background.paste(bottom_section.resize((new_width, delta_y_bottom)),
    (0, delta_y_top + height))

    # 填充两侧区域
    if delta_x > 0:
    # 左侧
    left_section = img.crop((0, 0, delta_x, height))
    background.paste(left_section.resize((delta_x, height)), (0, delta_y_top))

    # 右侧
    right_section = img.crop((width - delta_x, 0, width, height))
    background.paste(right_section.resize((delta_x, height)),
    (width + delta_x, delta_y_top))

    # 添加阴影效果
    apply_drop_shadow(background, img, (delta_x, delta_y_top))

    # 粘贴原图到中心
    background.paste(img, (delta_x, delta_y_top))
    return background


    def get_exif_data(img: Image.Image) -> str:
    """从图片中提取EXIF信息"""
    exif_info = {}
    try:
    # 通过piexif获取完整EXIF数据
    exif_dict = piexif.load(img.info.get("exif", b""))
    for ifd in ("0th", "Exif", "GPS", "1st"):
    for tag, value in exif_dict.get(ifd, {}).items():
    tag_name = piexif.TAGS[ifd][tag]["name"]
    exif_info[tag_name] = value
    except Exception:
    pass

    # 解析拍摄参数
    focal_length = parse_exif_value(
    exif_info.get("FocalLength", exif_info.get("FocalLengthIn35mmFilm")))
    aperture = parse_exif_value(exif_info.get("FNumber"))
    exposure = parse_exif_value(exif_info.get("ExposureTime"))
    iso = parse_exif_value(exif_info.get("ISOSpeedRatings", exif_info.get("PhotographicSensitivity")))

    # 格式化输出字符串
    return format_exif_string(focal_length, aperture, exposure, iso)


    def parse_exif_value(value) -> float:
    """解析EXIF数值"""
    if isinstance(value, tuple):
    return float(value[0]) / float(value[1])
    if isinstance(value, (int, float)):
    return float(value)
    return None


    def format_exif_string(focal: float, aperture: float, exposure: float, iso: float) -> str:
    """格式化EXIF信息为字符串"""
    focal_str = f"{int(focal)}mm" if focal else "--mm"
    aperture_str = f"f/{aperture:.1f}" if aperture else "f/--"

    if exposure:
    shutter = f"1/{int(1 / exposure)}s" if exposure < 1 else f"{exposure:.0f}s"
    else:
    shutter = "--s"

    iso_str = f"ISO{int(iso)}" if iso else "ISO---"

    return f"{focal_str} {aperture_str} {shutter} {iso_str}"


    def calculate_brightness(img: Image.Image) -> float:
    """计算图片区域的平均亮度"""
    gray_img = img.convert("L")
    pixels = list(gray_img.getdata())
    return sum(pixels) / len(pixels)


    def add_watermark(
    background_img: Image.Image,
    original_path: str,
    index, ii
    ) -> None:
    """为图片添加水印和EXIF信息

    Args:
    background_img: 背景处理后的图片对象
    original_path: 原始图片路径
    index: 当前处理序号
    """
    try:
    with Image.open(original_path) as original_img:
    output_dir = os.path.join(os.getcwd(), OUTPUT_DIRS["final"])
    os.makedirs(output_dir, exist_ok=True)

    output_path = os.path.join(
    output_dir,
    f"{os.path.splitext(os.path.basename(original_path))[0]}_信息模糊水印处理"
    f"{os.path.splitext(original_path)[1]}"
    )

    # 准备透明图层
    rgba_image = background_img.convert("RGBA")
    width, height = rgba_image.size

    # 检测底部亮度
    crop_height = max(50, int(height * 0.1))
    bottom_region = rgba_image.crop((
    width * 0.45,
    height - crop_height,
    width * 0.55,
    height
    ))
    brightness = calculate_brightness(bottom_region)

    # 选择水印类型
    watermark_type = "light" if brightness > BRIGHTNESS_THRESHOLD else "dark"
    watermark_img = load_watermark_image(
    WATERMARKS[watermark_type],
    width,
    height
    )

    # 合成水印
    composite_image = composite_watermark(rgba_image, watermark_img)

    # 添加文字信息
    final_image = add_exif_text(
    composite_image,
    original_img,
    watermark_img.size,
    watermark_type
    )

    # 保存结果
    final_image.convert(original_img.mode).save(
    output_path,
    format=original_img.format,
    **original_img.info
    )

    print(f"水印处理 {index}/{ii}: {os.path.basename(original_path)}")

    except Exception as e:
    print(f"处理失败: {original_path} - {str(e)}")


    def load_watermark_image(path: str, img_width: int, img_height: int) -> Image.Image:
    """加载并调整水印尺寸"""
    with Image.open(path) as watermark:
    max_width = img_width * 0.1 if img_width > img_height else img_width * 0.13
    scaling_factor = min(max_width / watermark.width, 1.0)
    return watermark.resize(
    (int(watermark.width * scaling_factor),
    int(watermark.height * scaling_factor)),
    Image.LANCZOS
    ).convert("RGBA")


    def composite_watermark(base_img: Image.Image, watermark: Image.Image) -> Image.Image:
    """合成水印到基础图片"""
    if base_img.width > base_img.height:
    position = (
    (base_img.width - watermark.width) // 2,
    base_img.height - int(watermark.height * 1.43)
    )
    else:
    position = (
    (base_img.width - watermark.width) // 2,
    base_img.height - int(watermark.height * 1.6)
    )

    composite = Image.new("RGBA", base_img.size)
    composite.paste(watermark, position, watermark)
    return Image.alpha_composite(base_img, composite)


    def add_exif_text(
    image: Image.Image,
    original_img: Image.Image,
    watermark_size: tuple,
    watermark_type: str
    ) -> Image.Image:
    """添加EXIF文字信息"""
    draw = ImageDraw.Draw(image)
    exif_text = get_exif_data(original_img)

    # 计算文字参数
    text_size = int(watermark_size[1] * 0.25)
    try:
    font = ImageFont.truetype(FONT_PATH, text_size)
    except IOError:
    font = ImageFont.load_default(text_size)

    # 文字位置计算
    text_y = image.height - int(image.height * 0.027) if image.width > image.height else image.height - int(
    image.height * 0.025)
    text_width = font.getlength(exif_text)
    text_x = (image.width - text_width) // 2

    # 文字颜色设置
    text_color = "white" if watermark_type == "dark" else "black"
    shadow_color = "black" if text_color == "white" else "white"

    # 添加文字阴影
    for dx, dy in [(-1, -1), (-1, 1), (1, -1), (1, 1)]:
    draw.text((text_x + dx, text_y + dy), exif_text, font=font, fill=shadow_color)

    # 添加主文字
    draw.text((text_x, text_y), exif_text, font=font, fill=text_color)
    return image


    if __name__ == "__main__":
    supported_extensions = (".png", ".jpg", ".jpeg", ".bmp", ".gif", ".webp")
    image_files = [
    f for f in os.listdir(".")
    if f.lower().endswith(supported_extensions)
    ]

    i = 1
    for idx, filename in enumerate(image_files, 1):
    processed_bg = process_image(filename, i, len(image_files))
    add_watermark(processed_bg, filename, i, len(image_files))
    i += 1

    print("处理完成!按回车键退出...")
    input()

技术亮点

  1. 智能适应系统

    • 根据图片尺寸自动计算:
    • 水印文字大小
    • 模糊半径(基于图片分辨率)
    • 扩展区域比例
  2. EXIF深度集成

    • 读取拍摄信息的核心代码

使用指南

环境准备

包含PIL、os、glob、piexif、Pillow等依赖库

典型工作流

  1. 原始图片整理到/input目录
  2. 运行处理脚本
  3. 处理结果输出到/output目录
  4. 使用拼接.py生成作品集长图