摸鱼笔记[6]-图像压缩存储.md

摘要

使用AVIF图像压缩方式压缩图片后进行长期存储, 适用于工业视觉检测场景的图像长期保存.

声明

本文内容由 AI 辅助生成, 已经人工审核和编辑。

简介

AVIF格式简介

[https://github.com/Tim0x0/avif-imageio]
[https://blog.timxs.com/archives/9AyEmIqR]
[https://zhuanlan.zhihu.com/p/355256489]
[https://github.com/AOMediaCodec/libavif]
适合使用 AVIF 的场景:

  1. CCD/工业相机采集图像的长期归档存储
  2. 云端视觉检测平台的大批量图片压缩传输
  3. 需要保留 10-bit 量化信息的精密测量场景

AVIF(AV1 Image File Format)是一种基于 AV1 视频编码的静态图像容器格式,由 Alliance for Open Media(AOMedia)于 2019 年发布。完全免专利费(Royalty-free),AOMedia 开放授权。

技术 效果
可变块大小 4×4 至 128×128 自适应分割,提升复杂纹理区域精度
方向性帧内预测 56 种角度模式 + 滤波器帧内预测,减少空间冗余
调色板模式 对色块区域(如图标、UI)压缩率极高
CDEF + Loop Restoration 去块滤波 + 环路恢复,保持边缘锐利
Tile 并行编码 支持多线程编码,工业批量处理友好

JPEG-AI格式简介

[https://jpeg.org/jpegai/]
JPEG-AI(ISO/IEC 6048,也称 JPEG AI)是联合图像专家组(JPEG)于 2022-2024 年间标准化的新一代图像编码格式,核心特点是基于神经网络的学习型压缩。

特性 说明
端到端学习 分析-合成网络联合优化,率失真函数可微分
内容自适应 网络自动分配码率:复杂纹理区域多比特,平滑区域少比特
语义保留 隐空间特征对人眼敏感结构(边缘、纹理)有天然偏好
机器视觉友好 支持"面向机器"(For Machines)编码模式,保留检测特征
渐进解码 支持从低质量到高质量的逐层重建

pillow-avif-plugin简介

pillow-avif-plugin 是一个用于 Python 的 Pillow 扩展插件,让 Pillow 能够直接读写 AVIF 格式图片。
依赖底层库 libavif,Windows 上通常通过预编译 wheel 自动包含,Linux 可能需要系统安装 libavif。

工程

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
图片批量压缩工具 (AVIF格式)
功能:将 D:/CCD图片/202606 所有图片压缩为AVIF,保留目录结构,支持断点续传
依赖:pip install pillow-avif-plugin
"""

import os
import sys
import json
import time
from datetime import datetime
from pathlib import Path

# ============================================================
# 尝试导入 pillow-avif-plugin
# ============================================================
try:
    from PIL import Image
    import pillow_avif  # 注册AVIF插件到Pillow
    AVIF_AVAILABLE = True
except ImportError:
    print("[错误] 缺少依赖: pillow-avif-plugin")
    print("安装命令: pip install pillow-avif-plugin")
    sys.exit(1)

# ============================================================
# 配置区域
# ============================================================
SOURCE_DIR = Path("D:/CCD图片/202606")
TARGET_DIR = Path("D:/CCD图片/长期存储")
CHECKPOINT_FILE = TARGET_DIR / ".compress_checkpoint.json"

# AVIF压缩参数
AVIF_QUALITY = 60          # 质量 (0-100),越高画质越好文件越大
AVIF_SPEED = 6             # 编码速度 (0-10),越高编码越快体积略大
MAX_SIZE = (10000, 10000)    # 最大尺寸限制,超出则等比缩放

# 支持的源图片格式
SUPPORTED_EXTS = {'.jpg', '.jpeg', '.png', '.bmp', '.tiff', '.tif', '.webp', '.gif'}

# ============================================================
# 断点续传管理器
# ============================================================
class CheckpointManager:
    """基于文件路径和修改时间的断点续传管理"""
    
    def __init__(self, checkpoint_path):
        self.checkpoint_path = checkpoint_path
        self.data = self._load()
    
    def _load(self):
        """加载断点记录"""
        if self.checkpoint_path.exists():
            try:
                with open(self.checkpoint_path, 'r', encoding='utf-8') as f:
                    return json.load(f)
            except (json.JSONDecodeError, Exception):
                return {}
        return {}
    
    def save(self):
        """保存断点记录"""
        try:
            # 确保目录存在
            self.checkpoint_path.parent.mkdir(parents=True, exist_ok=True)
            with open(self.checkpoint_path, 'w', encoding='utf-8') as f:
                json.dump(self.data, f, ensure_ascii=False, indent=2)
        except Exception as e:
            print(f"  [警告] 断点保存失败: {e}")
    
    def is_processed(self, src_path):
        """检查文件是否已处理且未被修改"""
        src_str = str(src_path)
        if src_str not in self.data:
            return False
        
        record = self.data[src_str]
        
        # 检查源文件修改时间
        try:
            current_mtime = os.path.getmtime(src_path)
            stored_mtime = record.get('mtime', 0)
            if abs(current_mtime - stored_mtime) > 1:
                return False  # 文件已被修改
        except OSError:
            return False
        
        # 检查目标文件是否存在
        target_path = record.get('target_path', '')
        if not target_path or not Path(target_path).exists():
            return False
        
        return True
    
    def mark_processed(self, src_path, target_path):
        """标记文件处理完成"""
        try:
            mtime = os.path.getmtime(src_path)
            self.data[str(src_path)] = {
                'mtime': mtime,
                'target_path': str(target_path),
                'size_original': os.path.getsize(src_path),
                'processed_at': datetime.now().isoformat()
            }
            self.save()
        except Exception:
            pass
    
    def get_stats(self):
        """获取断点统计"""
        return len(self.data)


# ============================================================
# 图片压缩器
# ============================================================
class ImageCompressor:
    """AVIF图片压缩器"""
    
    def __init__(self, quality=60, speed=6, max_size=(4096, 4096)):
        self.quality = quality
        self.speed = speed
        self.max_size = max_size
        self.stats = {
            'total': 0,
            'success': 0,
            'skipped': 0,
            'failed': 0,
            'total_original_mb': 0,
            'total_compressed_mb': 0
        }
    
    def _resize_if_needed(self, img):
        """如果图片超出最大尺寸,等比缩放"""
        if img.width > self.max_size[0] or img.height > self.max_size[1]:
            # 使用LANCZOS重采样保持质量
            img.thumbnail(self.max_size, Image.LANCZOS)
        return img
    
    def _prepare_mode(self, img):
        """准备合适的色彩模式"""
        # AVIF支持RGBA,保留透明通道
        if img.mode in ('RGBA', 'LA', 'P'):
            return img.convert('RGBA') if img.mode == 'P' and 'transparency' in img.info else img.convert('RGBA')
        elif img.mode in ('RGB', 'L', 'I;16', 'I'):
            return img.convert('RGB')
        else:
            return img.convert('RGB')
    
    def compress(self, src_path, target_path):
        """
        压缩单张图片到AVIF格式
        返回: (success: bool, original_size: int, compressed_size: int)
        """
        try:
            # 确保目标目录存在
            target_path.parent.mkdir(parents=True, exist_ok=True)
            
            with Image.open(src_path) as img:
                # 准备图片
                img = self._prepare_mode(img)
                img = self._resize_if_needed(img)
                
                # 保存为AVIF
                img.save(
                    target_path,
                    format='AVIF',
                    quality=self.quality,
                    speed=self.speed
                )
            
            # 获取文件大小
            original_size = src_path.stat().st_size
            compressed_size = target_path.stat().st_size
            
            return True, original_size, compressed_size
            
        except Exception as e:
            print(f"    [失败] {src_path.name}: {str(e)[:80]}")
            # 清理失败的目标文件
            if target_path.exists():
                try:
                    target_path.unlink()
                except Exception:
                    pass
            return False, 0, 0


# ============================================================
# 主程序
# ============================================================
def format_size(size_bytes):
    """格式化文件大小显示"""
    if size_bytes >= 1024 * 1024 * 1024:
        return f"{size_bytes / 1024 / 1024 / 1024:.2f} GB"
    elif size_bytes >= 1024 * 1024:
        return f"{size_bytes / 1024 / 1024:.2f} MB"
    elif size_bytes >= 1024:
        return f"{size_bytes / 1024:.1f} KB"
    else:
        return f"{size_bytes} B"


def main():
    print("=" * 70)
    print("  图片批量压缩工具 (AVIF格式) - 支持断点续传")
    print("=" * 70)
    print(f"  源目录:     {SOURCE_DIR}")
    print(f"  目标目录:   {TARGET_DIR}")
    print(f"  AVIF质量:   {AVIF_QUALITY}")
    print(f"  编码速度:   {AVIF_SPEED}")
    print(f"  最大尺寸:   {MAX_SIZE[0]}x{MAX_SIZE[1]}")
    print("=" * 70)
    
    # 检查源目录
    if not SOURCE_DIR.exists():
        print(f"[错误] 源目录不存在: {SOURCE_DIR}")
        return 1
    
    # 创建目标目录
    TARGET_DIR.mkdir(parents=True, exist_ok=True)
    
    # 初始化
    checkpoint = CheckpointManager(CHECKPOINT_FILE)
    compressor = ImageCompressor(AVIF_QUALITY, AVIF_SPEED, MAX_SIZE)
    
    # 扫描所有图片
    print("\n[1/4] 扫描图片文件...")
    image_files = []
    for ext in SUPPORTED_EXTS:
        # 同时搜索大小写扩展名
        image_files.extend(SOURCE_DIR.rglob(f"*{ext}"))
        image_files.extend(SOURCE_DIR.rglob(f"*{ext.upper()}"))
    
    # 去重并排序(保证顺序稳定)
    image_files = sorted(set(image_files))
    compressor.stats['total'] = len(image_files)
    
    print(f"  发现图片: {len(image_files)} 个")
    print(f"  断点记录: {checkpoint.get_stats()} 条")
    
    # 统计已处理
    already_done = sum(1 for f in image_files if checkpoint.is_processed(f))
    print(f"  已完成:   {already_done} 个")
    print(f"  待处理:   {len(image_files) - already_done} 个")
    
    if not image_files:
        print("[提示] 未找到任何图片文件")
        return 0
    
    # 确认开始
    if len(image_files) - already_done > 0:
        print(f"\n[2/4] 按 Enter 开始压缩,或 Ctrl+C 取消...")
        try:
            input()
        except KeyboardInterrupt:
            print("\n[取消] 用户中断")
            return 0
    
    # 开始压缩
    print(f"\n[3/4] 开始压缩...")
    start_time = time.time()
    last_save_time = start_time
    
    for i, src_path in enumerate(image_files, 1):
        # 计算相对路径,保持目录结构
        try:
            rel_path = src_path.relative_to(SOURCE_DIR)
        except ValueError:
            rel_path = src_path.name
        
        # 目标路径:保持目录结构,扩展名改为.avif
        target_path = TARGET_DIR / rel_path.with_suffix('.avif')
        
        # 检查断点
        if checkpoint.is_processed(src_path):
            compressor.stats['skipped'] += 1
            if i % 100 == 0 or i == len(image_files):
                print(f"  [{i}/{len(image_files)}] ✓ 跳过: {rel_path}")
            continue
        
        # 显示进度
        print(f"  [{i}/{len(image_files)}] 压缩: {rel_path}")
        
        # 执行压缩
        success, orig_size, comp_size = compressor.compress(src_path, target_path)
        
        if success:
            compressor.stats['success'] += 1
            compressor.stats['total_original_mb'] += orig_size
            compressor.stats['total_compressed_mb'] += comp_size
            
            ratio = (1 - comp_size / orig_size) * 100 if orig_size > 0 else 0
            print(f"         原: {format_size(orig_size)} → 新: {format_size(comp_size)} | 节省: {ratio:.1f}%")
            
            # 保存断点
            checkpoint.mark_processed(src_path, target_path)
            
            # 定期保存断点(每30秒)
            current_time = time.time()
            if current_time - last_save_time > 30:
                checkpoint.save()
                last_save_time = current_time
        else:
            compressor.stats['failed'] += 1
    
    # 最终保存断点
    checkpoint.save()
    
    # 输出统计
    elapsed = time.time() - start_time
    print(f"\n[4/4] 压缩完成!")
    print("=" * 70)
    print(f"  总文件:     {compressor.stats['total']}")
    print(f"  成功:       {compressor.stats['success']}")
    print(f"  跳过:       {compressor.stats['skipped']}")
    print(f"  失败:       {compressor.stats['failed']}")
    print(f"  耗时:       {elapsed:.1f} 秒 ({elapsed/60:.1f} 分钟)")
    
    if compressor.stats['total_original_mb'] > 0:
        orig_total = compressor.stats['total_original_mb']
        comp_total = compressor.stats['total_compressed_mb']
        total_ratio = (1 - comp_total / orig_total) * 100
        
        print(f"\n  原始总大小: {format_size(orig_total)}")
        print(f"  压缩后大小: {format_size(comp_total)}")
        print(f"  空间节省:   {total_ratio:.1f}%")
    
    print("=" * 70)
    print(f"  断点文件: {CHECKPOINT_FILE}")
    print("  提示: 如需重新压缩某文件,删除断点文件中对应条目后重跑")
    print("=" * 70)
    
    return 0


if __name__ == "__main__":
    sys.exit(main())

java版本架构

这是一个基于 Java Swing 开发的桌面端批量图片压缩工具,核心功能是将常见图片格式(JPG/PNG/BMP/TIFF/WebP/GIF 等)批量转换为 AVIF 格式,并提供以下特性:

  • 批量/单文件压缩为 AVIF
  • 支持断点续传(暂停后可继续)
  • 支持等比缩放、质量与编码速度调节
  • 保持源目录结构输出
  • 基于 FlatLaf 的现代 Swing 界面主题
技术/库 用途
Java 8+ 开发语言
Swing + FlatLaf 桌面 GUI 界面
avif-imageio 0.1.2 AVIF 编解码核心(含原生动态库)
Gson JSON 配置与断点文件序列化
Gradle 构建工具(从 AboutDialog 推断)
com.avif.compressor/
├── Main.java                          # 程序入口
├── config/
│   └── AppConfig.java                 # 配置管理
├── engine/
│   ├── AvifCompressor.java            # AVIF 压缩引擎
│   ├── CheckpointManager.java         # 断点续传管理
│   └── ImageScanner.java              # 图片扫描器
├── model/
│   ├── CompressSettings.java          # 压缩参数模型
│   └── CompressTask.java              # 单个压缩任务模型
├── ui/
│   ├── MainFrame.java                 # 主窗口
│   ├── CompressWorker.java            # 后台压缩线程
│   ├── SettingsDialog.java            # 设置对话框
│   ├── AboutDialog.java               # 关于对话框
└── util/
    └── FormatUtils.java               # 格式化工具

效果图

java版本
截屏2026-06-17 19.43
posted @ 2026-06-17 20:26  qsBye  阅读(7)  评论(0)    收藏  举报