Pillow 使用手册(Python图像处理库)

Pillow(PIL Fork)是Python最流行的图像处理库,在CTF中广泛用于图像隐写分析、图像修复和数据提取。

概述

Pillow 是 PIL (Python Imaging Library) 的维护分支,提供:

  • 图像I/O: 支持30+图像格式读写

  • 像素操作: 直接访问和修改像素数据

  • 图像处理: 滤镜、变换、增强等操作

  • 格式转换: 不同图像格式间转换

  • 通道分离: RGB/RGBA通道独立操作

CTF应用场景:

  • LSB隐写分析

  • 图像拼接和修复

  • 二维码识别

  • 像素数据提取

  • 图层分析


安装方法

# 安装 Pillow
pip install Pillow

# 安装额外依赖(可选)
pip install pillow[imagingft]  # 字体渲染支持

# 验证安装
python3 -c "from PIL import Image; print(Image.__version__)"

# 查看支持的格式
python3 -c "from PIL import features; features.pilinfo()"

基础操作

打开和显示图像

from PIL import Image

# 打开图像
img = Image.open('image.png')

# 查看基本信息
print(f"格式: {img.format}")  # PNG, JPEG, GIF等
print(f"尺寸: {img.size}")  # (width, height)
print(f"模式: {img.mode}")  # RGB, RGBA, L, P等
print(f"像素总数: {img.width * img.height}")

# 显示图像(会调用系统默认查看器)
img.show()

# 关闭图像
img.close()

图像模式说明

# 常见模式
# 1: 1位像素,黑白
# L: 8位像素,灰度
# P: 8位像素,调色板模式
# RGB: 3x8位像素,真彩色
# RGBA: 4x8位像素,带透明通道
# CMYK: 4x8位像素,色彩分离
# I: 32位整型像素
# F: 32位浮点型像素

# 模式转换
img_rgb = img.convert('RGB')
img_gray = img.convert('L')
img_rgba = img.convert('RGBA')

图像读写

保存图像

from PIL import Image

img = Image.open('input.png')

# 保存为不同格式
img.save('output.jpg')  # 自动识别格式
img.save('output.png', 'PNG')  # 指定格式
img.save('output.gif', 'GIF')

# 保存时指定参数
img.save('output.jpg', quality=95)  # JPEG质量
img.save('output.png', optimize=True)  # PNG优化

创建新图像

from PIL import Image

# 创建空白图像
img = Image.new('RGB', (800, 600), color='white')
img = Image.new('RGBA', (400, 300), color=(255, 0, 0, 128))

# 从数组创建
import numpy as np
arr = np.zeros((100, 100, 3), dtype=np.uint8)
arr[25:75, 25:75] = [255, 0, 0]  # 红色方块
img = Image.fromarray(arr)
img.save('array_image.png')

批量处理

from PIL import Image
import os

# 批量转换格式
def batch_convert(input_dir, output_dir, target_format='PNG'):
    os.makedirs(output_dir, exist_ok=True)

    for filename in os.listdir(input_dir):
        if filename.lower().endswith(('.jpg', '.jpeg', '.png', '.bmp')):
            img_path = os.path.join(input_dir, filename)
            img = Image.open(img_path)

            output_filename = os.path.splitext(filename)[0] + f'.{target_format.lower()}'
            output_path = os.path.join(output_dir, output_filename)

            img.save(output_path, target_format)
            print(f"Converted: {filename} -> {output_filename}")

batch_convert('input/', 'output/', 'PNG')

像素操作

读取像素值

from PIL import Image

img = Image.open('image.png')

# 获取单个像素
pixel = img.getpixel((10, 20))  # (x, y)
print(f"像素值: {pixel}")  # RGB: (r, g, b) 或 RGBA: (r, g, b, a)

# 遍历所有像素
width, height = img.size
for y in range(height):
    for x in range(width):
        pixel = img.getpixel((x, y))
        # 处理像素...

# 使用load()加速访问
pixels = img.load()
for y in range(height):
    for x in range(width):
        pixel = pixels[x, y]

修改像素值

from PIL import Image

img = Image.open('image.png')
pixels = img.load()

# 修改单个像素
pixels[10, 20] = (255, 0, 0)  # 设置为红色

# 批量修改
width, height = img.size
for y in range(height):
    for x in range(width):
        r, g, b = pixels[x, y]
        # 反色效果
        pixels[x, y] = (255 - r, 255 - g, 255 - b)

img.save('modified.png')

转换为NumPy数组

from PIL import Image
import numpy as np

img = Image.open('image.png')

# PIL转NumPy
arr = np.array(img)
print(f"数组形状: {arr.shape}")  # (height, width, channels)
print(f"数据类型: {arr.dtype}")  # uint8

# 批量像素操作
arr[:, :, 0] = 255  # 所有像素的红色通道设为255

# NumPy转PIL
img_new = Image.fromarray(arr)
img_new.save('numpy_modified.png')

图像处理

几何变换

from PIL import Image

img = Image.open('image.png')

# 缩放
img_resized = img.resize((400, 300))  # 指定尺寸
img_thumbnail = img.copy()
img_thumbnail.thumbnail((128, 128))  # 保持比例缩放

# 旋转
img_rotated = img.rotate(45)  # 逆时针旋转45度
img_rotated = img.rotate(45, expand=True)  # 扩展画布以包含全部内容

# 翻转
img_flipped_h = img.transpose(Image.FLIP_LEFT_RIGHT)  # 水平翻转
img_flipped_v = img.transpose(Image.FLIP_TOP_BOTTOM)  # 垂直翻转

# 裁剪
box = (100, 100, 400, 400)  # (left, top, right, bottom)
img_cropped = img.crop(box)

颜色调整

from PIL import Image, ImageEnhance

img = Image.open('image.png')

# 亮度调整
enhancer = ImageEnhance.Brightness(img)
img_bright = enhancer.enhance(1.5)  # 1.5倍亮度

# 对比度调整
enhancer = ImageEnhance.Contrast(img)
img_contrast = enhancer.enhance(2.0)

# 饱和度调整
enhancer = ImageEnhance.Color(img)
img_saturated = enhancer.enhance(0.5)  # 0.5倍饱和度

# 锐度调整
enhancer = ImageEnhance.Sharpness(img)
img_sharp = enhancer.enhance(2.0)

滤镜效果

from PIL import Image, ImageFilter

img = Image.open('image.png')

# 模糊
img_blur = img.filter(ImageFilter.BLUR)
img_gaussian = img.filter(ImageFilter.GaussianBlur(radius=5))

# 锐化
img_sharp = img.filter(ImageFilter.SHARPEN)
img_unsharp = img.filter(ImageFilter.UnsharpMask(radius=2, percent=150))

# 边缘检测
img_edges = img.filter(ImageFilter.FIND_EDGES)
img_contour = img.filter(ImageFilter.CONTOUR)

# 浮雕
img_emboss = img.filter(ImageFilter.EMBOSS)

# 平滑
img_smooth = img.filter(ImageFilter.SMOOTH)

LSB隐写

LSB信息隐藏

from PIL import Image

def hide_data_lsb(image_path, data, output_path):
    """在图像最低有效位(LSB)中隐藏数据"""
    img = Image.open(image_path)
    img = img.convert('RGB')
    pixels = img.load()

    # 将数据转换为二进制
    data_binary = ''.join(format(ord(char), '08b') for char in data)
    data_binary += '1111111111111110'  # 结束标记

    data_index = 0
    width, height = img.size

    for y in range(height):
        for x in range(width):
            if data_index >= len(data_binary):
                break

            r, g, b = pixels[x, y]

            # 在红色通道LSB中隐藏数据
            if data_index < len(data_binary):
                r = (r & 0xFE) | int(data_binary[data_index])
                data_index += 1

            pixels[x, y] = (r, g, b)

        if data_index >= len(data_binary):
            break

    img.save(output_path, 'PNG')
    print(f"数据已隐藏到 {output_path}")

# 使用示例
hide_data_lsb('cover.png', 'flag{hidden_message}', 'stego.png')

LSB信息提取

from PIL import Image

def extract_data_lsb(image_path):
    """从图像LSB中提取隐藏数据"""
    img = Image.open(image_path)
    img = img.convert('RGB')
    pixels = img.load()

    binary_data = ''
    width, height = img.size

    for y in range(height):
        for x in range(width):
            r, g, b = pixels[x, y]

            # 提取红色通道LSB
            binary_data += str(r & 1)

    # 将二进制转换为文本
    message = ''
    for i in range(0, len(binary_data), 8):
        byte = binary_data[i:i+8]
        if len(byte) == 8:
            char_code = int(byte, 2)
            if char_code == 0:  # 结束标记
                break
            if 32 <= char_code <= 126:  # 可打印字符
                message += chr(char_code)

    return message

# 使用示例
hidden_message = extract_data_lsb('stego.png')
print(f"提取的数据: {hidden_message}")

多通道LSB分析

from PIL import Image
import numpy as np

def analyze_lsb_all_channels(image_path, output_prefix='lsb'):
    """分析所有颜色通道的LSB"""
    img = Image.open(image_path)
    img = img.convert('RGB')
    arr = np.array(img)

    channels = ['R', 'G', 'B']

    for i, channel_name in enumerate(channels):
        # 提取LSB
        lsb = (arr[:, :, i] & 1) * 255

        # 保存LSB图像
        lsb_img = Image.fromarray(lsb.astype(np.uint8), mode='L')
        lsb_img.save(f'{output_prefix}_{channel_name}.png')
        print(f"保存了 {channel_name} 通道 LSB: {output_prefix}_{channel_name}.png")

    # 组合所有通道的LSB
    lsb_combined = np.zeros_like(arr)
    for i in range(3):
        lsb_combined[:, :, i] = (arr[:, :, i] & 1) * 255

    lsb_combined_img = Image.fromarray(lsb_combined.astype(np.uint8))
    lsb_combined_img.save(f'{output_prefix}_combined.png')
    print(f"保存了组合LSB: {output_prefix}_combined.png")

# 使用示例
analyze_lsb_all_channels('suspicious.png')

二维码处理

生成二维码

from PIL import Image, ImageDraw

def create_simple_qr_pattern(size=200, block_size=10):
    """创建简单的二维码样式图案"""
    img = Image.new('RGB', (size, size), 'white')
    draw = ImageDraw.Draw(img)

    # 绘制定位点
    positions = [(0, 0), (size - block_size * 7, 0), (0, size - block_size * 7)]

    for x, y in positions:
        # 外框
        draw.rectangle([x, y, x + block_size * 7, y + block_size * 7],
                      outline='black', width=block_size)
        # 内部方块
        draw.rectangle([x + block_size * 2, y + block_size * 2,
                       x + block_size * 5, y + block_size * 5], fill='black')

    img.save('qr_pattern.png')
    print("二维码图案已生成")

create_simple_qr_pattern()

# 实际二维码生成需要使用 qrcode 库
# pip install qrcode[pil]
# import qrcode
# qr = qrcode.QRCode(version=1, box_size=10, border=4)
# qr.add_data('https://example.com')
# qr.make(fit=True)
# img = qr.make_image(fill_color="black", back_color="white")
# img.save('qrcode.png')

二维码修复

from PIL import Image
import numpy as np

def repair_qr_code(damaged_path, output_path):
    """尝试修复损坏的二维码"""
    img = Image.open(damaged_path)
    img = img.convert('L')  # 转为灰度
    arr = np.array(img)

    # 二值化处理
    threshold = 128
    arr[arr < threshold] = 0
    arr[arr >= threshold] = 255

    # 降噪
    from scipy import ndimage
    arr = ndimage.median_filter(arr, size=3)

    repaired_img = Image.fromarray(arr.astype(np.uint8))
    repaired_img.save(output_path)
    print(f"修复后的二维码已保存到: {output_path}")

# 使用示例
repair_qr_code('damaged_qr.png', 'repaired_qr.png')

CTF常见场景

场景1: 图像拼接

from PIL import Image

def stitch_images_horizontal(image_paths, output_path):
    """水平拼接多张图像"""
    images = [Image.open(path) for path in image_paths]

    # 计算总宽度和最大高度
    total_width = sum(img.width for img in images)
    max_height = max(img.height for img in images)

    # 创建新图像
    result = Image.new('RGB', (total_width, max_height))

    # 拼接
    x_offset = 0
    for img in images:
        result.paste(img, (x_offset, 0))
        x_offset += img.width

    result.save(output_path)
    print(f"拼接完成: {output_path}")

# 使用示例
image_files = ['part1.png', 'part2.png', 'part3.png']
stitch_images_horizontal(image_files, 'flag.png')

场景2: 通道分离

from PIL import Image

def split_channels(image_path, output_prefix='channel'):
    """分离RGB通道并保存"""
    img = Image.open(image_path)
    img = img.convert('RGB')

    r, g, b = img.split()

    # 保存各通道
    r.save(f'{output_prefix}_R.png')
    g.save(f'{output_prefix}_G.png')
    b.save(f'{output_prefix}_B.png')

    print("通道分离完成")

    # 通道可视化(将单通道显示为灰度图)
    Image.merge('RGB', (r, Image.new('L', img.size), Image.new('L', img.size))).save(f'{output_prefix}_R_visual.png')
    Image.merge('RGB', (Image.new('L', img.size), g, Image.new('L', img.size))).save(f'{output_prefix}_G_visual.png')
    Image.merge('RGB', (Image.new('L', img.size), Image.new('L', img.size), b)).save(f'{output_prefix}_B_visual.png')

# 使用示例
split_channels('flag.png')

场景3: 异或图像恢复

from PIL import Image
import numpy as np

def xor_images(img1_path, img2_path, output_path):
    """对两张图像进行异或操作"""
    img1 = Image.open(img1_path).convert('RGB')
    img2 = Image.open(img2_path).convert('RGB')

    arr1 = np.array(img1)
    arr2 = np.array(img2)

    # 确保尺寸相同
    if arr1.shape != arr2.shape:
        print("警告: 图像尺寸不同,调整中...")
        img2 = img2.resize(img1.size)
        arr2 = np.array(img2)

    # 异或操作
    result_arr = np.bitwise_xor(arr1, arr2)

    result_img = Image.fromarray(result_arr.astype(np.uint8))
    result_img.save(output_path)
    print(f"异或结果已保存: {output_path}")

# 使用示例
xor_images('encrypted1.png', 'encrypted2.png', 'flag.png')

场景4: 盲水印检测

from PIL import Image
import numpy as np

def detect_watermark(image_path, output_path, alpha=3.0):
    """检测图像中的盲水印"""
    img = Image.open(image_path).convert('RGB')
    arr = np.array(img, dtype=np.float32)

    # 增强低频信号差异
    enhanced = np.clip(arr * alpha, 0, 255)

    # 提取差异
    diff = enhanced - arr
    diff = np.abs(diff)
    diff = np.clip(diff * 10, 0, 255)

    result_img = Image.fromarray(diff.astype(np.uint8))
    result_img.save(output_path)
    print(f"水印检测结果: {output_path}")

# 使用示例
detect_watermark('watermarked.png', 'watermark_detected.png')

场景5: 图像差分分析

from PIL import Image
import numpy as np

def image_diff(img1_path, img2_path, output_path, threshold=10):
    """计算两张图像的差异"""
    img1 = Image.open(img1_path).convert('RGB')
    img2 = Image.open(img2_path).convert('RGB')

    arr1 = np.array(img1, dtype=np.int16)
    arr2 = np.array(img2, dtype=np.int16)

    # 计算差异
    diff = np.abs(arr1 - arr2)

    # 突出显示差异
    mask = np.any(diff > threshold, axis=2)
    result = np.zeros_like(arr1)
    result[mask] = [255, 0, 0]  # 差异部分标红
    result[~mask] = arr1[~mask]

    result_img = Image.fromarray(result.astype(np.uint8))
    result_img.save(output_path)
    print(f"差异分析完成: {output_path}")

# 使用示例
image_diff('original.png', 'modified.png', 'diff.png')

实战案例

案例1: 提取PNG块中的隐藏数据

from PIL import Image
import struct

def extract_png_chunks(png_path):
    """提取PNG文件的所有数据块"""
    with open(png_path, 'rb') as f:
        # 验证PNG签名
        signature = f.read(8)
        if signature != b'\x89PNG\r\n\x1a\n':
            print("不是有效的PNG文件")
            return

        print("PNG数据块列表:")
        print("-" * 50)

        while True:
            # 读取块长度
            length_data = f.read(4)
            if len(length_data) < 4:
                break

            length = struct.unpack('>I', length_data)[0]

            # 读取块类型
            chunk_type = f.read(4).decode('ascii', errors='ignore')

            # 读取块数据
            chunk_data = f.read(length)

            # 读取CRC
            crc = f.read(4)

            print(f"类型: {chunk_type}, 长度: {length} 字节")

            # 检查自定义块(CTF常用隐藏位置)
            if chunk_type not in ['IHDR', 'PLTE', 'IDAT', 'IEND', 'tRNS',
                                  'gAMA', 'cHRM', 'sRGB', 'iCCP', 'tEXt',
                                  'zTXt', 'iTXt', 'bKGD', 'pHYs', 'sBIT',
                                  'sPLT', 'hIST', 'tIME']:
                print(f"  [!] 发现可疑块: {chunk_type}")
                print(f"  数据: {chunk_data[:100]}")

                # 尝试解码为文本
                try:
                    text = chunk_data.decode('utf-8', errors='ignore')
                    if text.isprintable():
                        print(f"  解码文本: {text}")
                except:
                    pass

            if chunk_type == 'IEND':
                break

        # 检查IEND后是否有额外数据
        extra_data = f.read()
        if extra_data:
            print(f"\n[!] IEND块后发现额外数据: {len(extra_data)} 字节")
            print(f"数据: {extra_data[:100]}")

# 使用示例
extract_png_chunks('challenge.png')

案例2: 图像频谱分析

from PIL import Image
import numpy as np
import matplotlib.pyplot as plt

def frequency_analysis(image_path, output_path='spectrum.png'):
    """对图像进行频谱分析"""
    img = Image.open(image_path).convert('L')
    arr = np.array(img)

    # 傅里叶变换
    f_transform = np.fft.fft2(arr)
    f_shift = np.fft.fftshift(f_transform)

    # 计算幅度谱
    magnitude_spectrum = 20 * np.log(np.abs(f_shift) + 1)

    # 可视化
    plt.figure(figsize=(12, 6))

    plt.subplot(121)
    plt.imshow(arr, cmap='gray')
    plt.title('原始图像')
    plt.axis('off')

    plt.subplot(122)
    plt.imshow(magnitude_spectrum, cmap='gray')
    plt.title('频谱')
    plt.axis('off')

    plt.tight_layout()
    plt.savefig(output_path)
    print(f"频谱分析结果: {output_path}")

# 使用示例
frequency_analysis('challenge.png')

案例3: 像素值统计分析

from PIL import Image
import numpy as np
import matplotlib.pyplot as plt

def pixel_statistics(image_path):
    """统计图像像素值分布"""
    img = Image.open(image_path).convert('RGB')
    arr = np.array(img)

    # 分离通道
    r, g, b = arr[:, :, 0], arr[:, :, 1], arr[:, :, 2]

    # 绘制直方图
    plt.figure(figsize=(15, 5))

    plt.subplot(131)
    plt.hist(r.ravel(), bins=256, range=(0, 256), color='red', alpha=0.7)
    plt.title('Red Channel')
    plt.xlabel('Pixel Value')
    plt.ylabel('Frequency')

    plt.subplot(132)
    plt.hist(g.ravel(), bins=256, range=(0, 256), color='green', alpha=0.7)
    plt.title('Green Channel')
    plt.xlabel('Pixel Value')
    plt.ylabel('Frequency')

    plt.subplot(133)
    plt.hist(b.ravel(), bins=256, range=(0, 256), color='blue', alpha=0.7)
    plt.title('Blue Channel')
    plt.xlabel('Pixel Value')
    plt.ylabel('Frequency')

    plt.tight_layout()
    plt.savefig('pixel_statistics.png')
    print("像素统计完成: pixel_statistics.png")

    # 查找异常
    print("\n统计信息:")
    print(f"Red   - Min: {r.min()}, Max: {r.max()}, Mean: {r.mean():.2f}, Std: {r.std():.2f}")
    print(f"Green - Min: {g.min()}, Max: {g.max()}, Mean: {g.mean():.2f}, Std: {g.std():.2f}")
    print(f"Blue  - Min: {b.min()}, Max: {b.max()}, Mean: {b.mean():.2f}, Std: {b.std():.2f}")

# 使用示例
pixel_statistics('challenge.png')

高级技巧

图像元数据操作

from PIL import Image
from PIL.ExifTags import TAGS

def extract_exif(image_path):
    """提取EXIF元数据"""
    img = Image.open(image_path)
    exif_data = img._getexif()

    if exif_data:
        print("EXIF数据:")
        for tag_id, value in exif_data.items():
            tag = TAGS.get(tag_id, tag_id)
            print(f"{tag}: {value}")
    else:
        print("没有EXIF数据")

# 使用示例
extract_exif('photo.jpg')

自定义图像格式处理

from PIL import Image
import struct

def parse_custom_format(file_path):
    """解析自定义图像格式"""
    with open(file_path, 'rb') as f:
        # 读取头部
        magic = f.read(4)
        width, height = struct.unpack('<II', f.read(8))

        print(f"Magic: {magic}")
        print(f"尺寸: {width}x{height}")

        # 读取像素数据
        pixel_data = f.read(width * height * 3)

        # 创建图像
        img = Image.frombytes('RGB', (width, height), pixel_data)
        img.save('reconstructed.png')
        print("重建完成: reconstructed.png")

# 使用示例
parse_custom_format('custom.dat')

常见问题解决

问题1: 图像无法打开

from PIL import Image

try:
    img = Image.open('corrupted.png')
except Exception as e:
    print(f"错误: {e}")

    # 尝试修复
    with open('corrupted.png', 'rb') as f:
        data = f.read()
        # 检查文件头
        if not data.startswith(b'\x89PNG'):
            print("PNG文件头损坏")

问题2: 内存不足

from PIL import Image

# 使用lazy loading
Image.MAX_IMAGE_PIXELS = None  # 移除像素限制

# 分块处理大图像
def process_large_image(path):
    img = Image.open(path)
    width, height = img.size
    chunk_size = 1000

    for y in range(0, height, chunk_size):
        for x in range(0, width, chunk_size):
            box = (x, y, min(x + chunk_size, width), min(y + chunk_size, height))
            chunk = img.crop(box)
            # 处理chunk...

参考资源

官方文档

  • Pillow官网: https://python-pillow.org/

  • 文档: https://pillow.readthedocs.io/

  • GitHub: https://github.com/python-pillow/Pillow

CTF工具

  • Stegsolve: 图像隐写分析工具

  • zsteg: LSB隐写检测

  • steghide: 图像隐写工具

学习资源

  • CTF Wiki: https://ctf-wiki.org/misc/picture/

  • 隐写术教程: https://0xrick.github.io/lists/stego/


文档版本: v1.0 更新日期: 2025-01 适用版本: Pillow v10.0+