圖片壓縮 Resize & Compression Image With Python (pngquant, ImageMagick)

2023-08-02

筆記使用 Python 以及 Pillow Packages 批次將圖片重新調整比例以及壓縮圖大小,以節省儲存空間。

logo

說明

不多說,直接上程式碼,注意需要先安裝 pillow

pip install pillow
from PIL import Image
import os
import glob
import random

def convert(file):
    input_image_path = file
    file_name, extension = os.path.splitext(file)
    output_image_path = f'{file_name}_minify.{os.path_sp}.{extension}'

    original_image = Image.open(input_image_path)

    # Resize
    new_width = original_image.size[0] // 2
    new_height = original_image.size[1] // 2

    resized_image = original_image.resize((new_width, new_height))

    # Compression: 100 is best quality
    compression_quality = 35

    resized_image.save(output_image_path, quality=compression_quality)

os.chdir(r"D:\ImagesFolder")
jpg_files = glob.glob("*.jpg")

for jpg in jpg_files:
    convert(jpg)

針對 PNG 的綜合處理方式,使用 256 色或 Qunatize Method 2 能夠有效壓縮為 25% 的大小,但顏色上會略為失真。

Resize 的方式對於減少 Size 有效,但會減少解析度,權衡下使用 256 色或 Qunatize Method 2 讓畫質失真可能較為接受。

from PIL import Image
import os

def compress_image(input_path):
    try:
        for i in range(25, 95, 5):
            with Image.open(input_path) as img:
                width, height = img.size
                new_width = int(width * (i / 100))
                new_height = int(height * (i / 100))
                img = img.resize((new_width, new_height), Image.ANTIALIAS)
                img.save(f'resize_{i}.png', 'png', optimize=True)

        with Image.open(input_path) as img:
            img = img.convert('P', palette=Image.ADAPTIVE)
            img.save(f'adaptive.png', 'png', optimize=True)

        with Image.open(input_path) as img:
            img = img.quantize(method = 2)
            img.save(f'quantize_method_2.png', 'png', optimize=True)

        for color in (128, 256):
            with Image.open(input_path) as img:
                img = img.quantize(colors = color)
                img.save(f'color_{color}.png', 'png', optimize=True)

        with Image.open(input_path) as img:
            img.save(f'default.png', 'png', optimize=True)
            
    except Exception as e:
        print(f"Exception:{e}")

if __name__ == "__main__":
    input_image_path = r"D:\ImagesFolder\img.png"
    compress_image(input_image_path)

GitHub

雖然 PIL 提供了一定程度的圖片壓縮能力,但如果要有效減少檔案大小,就會犧牲圖片的解析度或者是畫質,而且影響程度幾乎是肉眼明顯可見。

替代的選擇是使用更好的圖片壓縮演算法,例如 pngquant 這個 cli 工具。

python 搭配 subprocess 的方式去進行 pngquant 的調度使用,結合達成壓縮圖片且不明顯損失畫質的成果。

import os
import subprocess

source_folder = r'C:\images'
output_folder = r'C:\images\minify'

pngquant_path = r'C:\apps\pngquant.exe'

start = time.time()

if not os.path.exists(output_folder):
    os.makedirs(output_folder)

for filename in os.listdir(source_folder):
    if filename.endswith('.png') or filename.endswith('.jpg'):
        input_image = os.path.join(source_folder, filename)
        output_image = os.path.join(output_folder, filename)

        subprocess.run([pngquant_path, input_image, '--output', output_image, '--skip-if-larger', '--force', '--speed', '11'])

end = time.time()
print(f'壓縮完成! {round(end - start, 3)}')

關於 pngquant 的 parameter

options:
  --force           overwrite existing output files (synonym: -f)
  --skip-if-larger  only save converted files if they're smaller than original
  --output file     destination file path to use instead of --ext (synonym: -o)
  --ext new.png     set custom suffix/extension for output filenames
  --quality min-max don't save below min, use fewer colors below max (0-100)
  --speed N         speed/quality trade-off. 1=slow, 4=default, 11=fast & rough
  --nofs            disable Floyd-Steinberg dithering
  --posterize N     output lower-precision color (e.g. for ARGB4444 output)
  --strip           remove optional metadata (default on Mac)
  --verbose         print status messages (synonym: -v)

ImageMagick (jpg files)

ImageMagick 可以用來處理 jpg files,同樣是 exe 的方式執行,可以結合 python 先進行圖片 resize 接著再 compression。

ImageMagick Download Page

from PIL import Image
import os
import glob
import config as c
import subprocess
from concurrent.futures import ThreadPoolExecutor

def convert(file):
    input_image_path = os.path.join(c.source, file)
    file_name, extension = os.path.splitext(file)

    original_image = Image.open(input_image_path)

    minimun_width = 470
    scale = original_image.size[0] // minimun_width
    print(f"Scaling factor for {file}: {scale}")

    # Resize
    new_width = original_image.size[0] // scale
    new_height = original_image.size[1] // scale

    resized_image = original_image.resize((new_width, new_height), Image.ANTIALIAS)

    output_path = os.path.join(c.output, file)
    resized_image.save(output_path)
    
    # Post-processing with ImageMagick
    command = [c.imconvert, output_path, output_path]
    subprocess.run(command, text=True, capture_output=True)
    print(f"Processed {file}")

    return output_path

def main():
    os.chdir(c.source)
    jpg_files = glob.glob("*.jpg")

    with ThreadPoolExecutor(max_workers=os.cpu_count()) as executor:
        results = list(executor.map(convert, jpg_files))

if __name__ == "__main__":
    main()

imconvert.bat

C:\Apps\ImageMagick-7.1.1-30-portable-Q16-HDRI-x64\convert.exe %1 -quality 20 %2