หมอน - การปรับขนาด GIF

ฉันมี gif ที่ฉันต้องการปรับขนาดด้วย pillow เพื่อให้ขนาดลดลง ขนาดปัจจุบันของ gif คือ 2MB

ฉันพยายามที่จะ

  1. ปรับขนาดเพื่อให้ความสูง/ความกว้างเล็กลง

  2. ลดคุณภาพลง

สำหรับ JPEG โค้ดต่อไปนี้มักจะเพียงพอเพื่อให้รูปภาพขนาดใหญ่ลดขนาดลงอย่างมาก

from PIL import Image

im = Image.open("my_picture.jpg")
im = im.resize((im.size[0] // 2, im.size[1] // 2), Image.ANTIALIAS)  # decreases width and height of the image
im.save("out.jpg", optimize=True, quality=85)  # decreases its quality

แต่เมื่อใช้ GIF ดูเหมือนว่าจะใช้งานไม่ได้ โค้ดต่อไปนี้ทำให้ out.gif ใหญ่กว่า gif เริ่มต้น:

im = Image.open("my_gif.gif")
im.seek(im.tell() + 1)  # loads all frames
im.save("out.gif", save_all=True, optimize=True, quality=10)  # should decrease its quality

print(os.stat("my_gif.gif").st_size)  # 2096558 bytes / roughly 2MB
print(os.stat("out.gif").st_size)  # 7536404 bytes / roughly 7.5MB

หากฉันเพิ่มบรรทัดต่อไปนี้ ระบบจะบันทึกเฉพาะเฟรมแรกของ GIF เท่านั้น แทนที่จะเป็นเฟรมทั้งหมด

im = im.resize((im.size[0] // 2, im.size[1] // 2), Image.ANTIALIAS)  # should decrease its size

ฉันกำลังคิดที่จะเรียก resize() บน im.seek() หรือ im.tell() แต่ไม่มีวิธีใดที่ส่งคืนวัตถุ Image ดังนั้นฉันจึงไม่สามารถเรียก resize() ในเอาต์พุตได้

คุณจะรู้ไหมว่าฉันสามารถใช้ Pillow เพื่อลดขนาด GIF ของฉันในขณะที่เก็บเฟรมทั้งหมดไว้ได้อย่างไร

[แก้ไข] วิธีแก้ปัญหาบางส่วน:

หลังจากคำตอบของ Old Bear ฉันได้ทำการเปลี่ยนแปลงต่อไปนี้แล้ว:

  • ฉันใช้ สคริปต์ของ BigglesZX เพื่อแยกเฟรมทั้งหมด โปรดทราบว่านี่คือสคริปต์ Python 2 และโปรเจ็กต์ของฉันเขียนด้วย Python 3 (ฉันได้พูดถึงรายละเอียดนั้นในตอนแรก แต่ได้รับการแก้ไขโดยชุมชน Stack Overflow) การรัน 2to3 -w gifextract.py ทำให้สคริปต์นั้นเข้ากันได้กับ Python 3

  • ฉันได้แยกแต่ละเฟรมแยกกัน: frame.resize((frame.size[0] // 2, frame.size[1] // 2), Image.ANTIALIAS)

  • ฉันบันทึกเฟรมทั้งหมดไว้ด้วยกัน: img.save("out.gif", save_all=True, optimize=True)

ตอนนี้ gif ใหม่ได้รับการบันทึกและใช้งานได้แล้ว แต่มีปัญหาหลัก 2 ประการ:

  • ฉันไม่แน่ใจว่าวิธีการปรับขนาดใช้งานได้ เนื่องจาก out.gif ยังคงเป็น 7.5MB GIF เริ่มต้นคือ 2MB

  • ความเร็ว GIF เพิ่มขึ้น และ GIF ไม่วนซ้ำ มันหยุดหลังจากการวิ่งครั้งแรก

ตัวอย่าง:

gif ต้นฉบับ my_gif.gif:

gif ต้นฉบับ

Gif หลังการประมวลผล (out.gif) https://i.imgur.com/zDO4cE4.mp4 (I ไม่สามารถเพิ่มลงใน Stack Overflow ได้) Imgur ทำให้ช้าลง (และแปลงเป็น mp4) เมื่อฉันเปิดไฟล์ GIF จากคอมพิวเตอร์ GIF ทั้งหมดจะใช้เวลาประมาณ 1.5 วินาที


person Pauline    schedule 18.01.2017    source แหล่งที่มา
comment
คุณสามารถอัปโหลดไฟล์ GIF ที่คุณพยายามปรับขนาดได้หรือไม่   -  person Jeru Luke    schedule 19.01.2017
comment
@JeruLuke ฉันได้เพิ่มไฟล์ GIF แล้ว   -  person Pauline    schedule 23.01.2017


คำตอบ (3)


เมื่อใช้สคริปต์ของ BigglesZX ฉันได้สร้างสคริปต์ใหม่ซึ่งจะปรับขนาด GIF โดยใช้ Pillow

GIF ต้นฉบับ (2.1 MB):

gif ต้นฉบับ

เอาต์พุต GIF หลังจากปรับขนาด (1.7 MB):

เอาต์พุต gif

ฉันได้บันทึกสคริปต์แล้วที่นี่ มันใช้วิธี thumbnail ของ Pillow มากกว่าวิธี resize เนื่องจากฉันพบว่าวิธี resize ใช้งานไม่ได้

ยังไม่สมบูรณ์แบบ ดังนั้นอย่าลังเลที่จะแยกและปรับปรุงมัน ต่อไปนี้เป็นปัญหาที่ยังไม่ได้รับการแก้ไขบางส่วน:

  • แม้ว่า GIF จะแสดงผลได้ดีเมื่อโฮสต์โดย imgur แต่มีปัญหาเรื่องความเร็วเมื่อฉันเปิดมันจากคอมพิวเตอร์ ซึ่ง GIF ทั้งหมดใช้เวลาเพียง 1.5 วินาทีเท่านั้น
  • ในทำนองเดียวกัน แม้ว่า imgur ดูเหมือนจะช่วยชดเชยปัญหาเรื่องความเร็ว แต่ GIF ก็แสดงไม่ถูกต้องเมื่อฉันพยายามอัปโหลดไปที่ stack.imgur แสดงเฉพาะเฟรมแรกเท่านั้น (คุณสามารถดูได้ที่นี่)

รหัสเต็ม (ควรลบส่วนสำคัญข้างต้น):

def resize_gif(path, save_as=None, resize_to=None):
    """
    Resizes the GIF to a given length:

    Args:
        path: the path to the GIF file
        save_as (optional): Path of the resized gif. If not set, the original gif will be overwritten.
        resize_to (optional): new size of the gif. Format: (int, int). If not set, the original GIF will be resized to
                              half of its size.
    """
    all_frames = extract_and_resize_frames(path, resize_to)

    if not save_as:
        save_as = path

    if len(all_frames) == 1:
        print("Warning: only 1 frame found")
        all_frames[0].save(save_as, optimize=True)
    else:
        all_frames[0].save(save_as, optimize=True, save_all=True, append_images=all_frames[1:], loop=1000)


def analyseImage(path):
    """
    Pre-process pass over the image to determine the mode (full or additive).
    Necessary as assessing single frames isn't reliable. Need to know the mode
    before processing all frames.
    """
    im = Image.open(path)
    results = {
        'size': im.size,
        'mode': 'full',
    }
    try:
        while True:
            if im.tile:
                tile = im.tile[0]
                update_region = tile[1]
                update_region_dimensions = update_region[2:]
                if update_region_dimensions != im.size:
                    results['mode'] = 'partial'
                    break
            im.seek(im.tell() + 1)
    except EOFError:
        pass
    return results


def extract_and_resize_frames(path, resize_to=None):
    """
    Iterate the GIF, extracting each frame and resizing them

    Returns:
        An array of all frames
    """
    mode = analyseImage(path)['mode']

    im = Image.open(path)

    if not resize_to:
        resize_to = (im.size[0] // 2, im.size[1] // 2)

    i = 0
    p = im.getpalette()
    last_frame = im.convert('RGBA')

    all_frames = []

    try:
        while True:
            # print("saving %s (%s) frame %d, %s %s" % (path, mode, i, im.size, im.tile))

            '''
            If the GIF uses local colour tables, each frame will have its own palette.
            If not, we need to apply the global palette to the new frame.
            '''
            if not im.getpalette():
                im.putpalette(p)

            new_frame = Image.new('RGBA', im.size)

            '''
            Is this file a "partial"-mode GIF where frames update a region of a different size to the entire image?
            If so, we need to construct the new frame by pasting it on top of the preceding frames.
            '''
            if mode == 'partial':
                new_frame.paste(last_frame)

            new_frame.paste(im, (0, 0), im.convert('RGBA'))

            new_frame.thumbnail(resize_to, Image.ANTIALIAS)
            all_frames.append(new_frame)

            i += 1
            last_frame = new_frame
            im.seek(im.tell() + 1)
    except EOFError:
        pass

    return all_frames
person Pauline    schedule 24.01.2017

ตาม Pillow 4.0x ฟังก์ชัน Image.resize ใช้งานได้กับรูปภาพ/เฟรมเดียวเท่านั้น

เพื่อให้บรรลุสิ่งที่คุณต้องการ ฉันเชื่อว่าคุณต้องแยกทุกเฟรมออกจากไฟล์ .gif ก่อน ปรับขนาดแต่ละเฟรมทีละเฟรม จากนั้นจึงประกอบกลับเข้าไปใหม่อีกครั้ง

ในการทำขั้นตอนแรก ดูเหมือนจะมีรายละเอียดบางอย่างที่ต้องใส่ใจ เช่น. ไม่ว่าแต่ละเฟรม GIF จะใช้จานสีในเครื่องหรือจานสีส่วนกลางจะถูกนำไปใช้กับเฟรมทั้งหมดหรือไม่ และไม่ว่า GIF จะแทนที่แต่ละภาพโดยใช้เฟรมเต็มหรือบางส่วนก็ตาม BigglesZX ได้พัฒนาสคริปต์เพื่อแก้ไขปัญหาเหล่านี้พร้อมทั้งแยกทุกเฟรมออกจากไฟล์ GIF ดังนั้นจงใช้ประโยชน์จากสิ่งนั้น .

ถัดไป คุณต้องเขียนสคริปต์เพื่อปรับขนาดแต่ละเฟรมที่แยกออกมา และประกอบทั้งหมดเป็น .gif ใหม่โดยใช้ PIL.Image.resize() และ PIL.Image.save()

ฉันสังเกตเห็นว่าคุณเขียนว่า "im.seek(im.tell() + 1) # load all frames" ฉันคิดว่านี่ไม่ถูกต้อง แต่จะใช้เพื่อเพิ่มระหว่างเฟรมของไฟล์ .gif ฉันสังเกตเห็นว่าคุณใช้ quality=10 ในฟังก์ชันบันทึกสำหรับไฟล์ .gif ของคุณ ฉันไม่พบสิ่งนี้ตามที่ระบุไว้ในเอกสาร PIL คุณสามารถเรียนรู้เพิ่มเติมเกี่ยวกับแอตทริบิวต์ไทล์ที่กล่าวถึงในสคริปต์ของ BiggleZX ได้โดยการอ่าน ลิงก์

person Sun Bear    schedule 20.01.2017
comment
คุณพูดถูกสำหรับ quality มันเป็นอาร์กิวเมนต์ที่ถูกต้องสำหรับไฟล์ jpg แต่ไม่ใช่สำหรับไฟล์ gif ฉันลืมลบออกเมื่อพยายามเปลี่ยนโค้ดจากการปรับขนาด gif เป็นปรับขนาด jpg ฉันได้แก้ไขโพสต์ของฉันเพื่อตอบกลับองค์ประกอบอื่น ๆ ของคำตอบของคุณเนื่องจากฉันมีพื้นที่ไม่เพียงพอที่จะตอบกลับเป็นความคิดเห็น - person Pauline; 23.01.2017
comment
@Pauline หากคุณเปรียบเทียบขนาดของเฟรมที่แยกแต่ละเฟรมกับเฟรมที่ปรับขนาดแล้ว คุณพบว่าขนาดของเฟรมที่ปรับขนาดเล็กลงหรือไม่ ถ้ามันเล็กกว่านี้ ฉันคิดว่านี่หมายความว่าฟังก์ชันปรับขนาดใช้งานได้ และขนาดไฟล์ที่เพิ่มขึ้นนั้นมาจากกระบวนการคอมไพล์ใหม่ สำหรับปัญหาการวนซ้ำ มีตัวเลือกการวนซ้ำที่กล่าวถึงในคำสั่ง Image.save ที่สามารถแก้ไขปัญหาของคุณได้ สำหรับความเร็ว ฉันคิดว่าตัวเลือกระยะเวลาควบคุมสิ่งนั้น คุณสามารถดูได้จากลิงก์ที่ให้ไว้ในคำตอบของฉันก่อนหน้านี้ - person Sun Bear; 23.01.2017
comment
อีกความคิดหนึ่ง หากมีการเพิ่มขนาดในการลดขนาดไฟล์ อาจเกิดจากประเภทตัวกรองของคุณ ดู pillow.readthedocs.io/en/4.0.x/handbook /concepts.html#filters ฉันสังเกตเห็นว่าคุณกำลังใช้ตัวกรอง Image.ANTIALIAS แต่ไม่พบตัวกรองนี้เป็นหนึ่งในตัวกรองที่แสดงสำหรับการปรับขนาด ตัวกรอง Hamming ดูน่าดึงดูด - person Sun Bear; 23.01.2017
comment
ฉันพบว่าวิธี resize ใช้ไม่ได้ ดังนั้นฉันจึงเปลี่ยนไปใช้ thumbnail ซึ่งได้ผล ฉันไม่รู้ว่าทำไม resize ไม่ทำงาน ตอนนี้ฉันกำลังใช้แอตทริบิวต์การวนซ้ำ แต่ในตอนแรกฉันต้องการการวนซ้ำไม่สิ้นสุดแทนที่จะเป็นจำนวนการวนซ้ำคงที่ ฉันไม่พบวิธีที่จะบรรลุเป้าหมายนี้ ดังนั้นฉันจึงกำหนดจำนวนลูปเป็น 1,000 (โดยจะเพิ่มเพียงไม่กี่ไบต์จากขนาดรวมของ GIF ฉันไม่พบวิธีแก้ปัญหาสำหรับความเร็วที่จะไม่ กำหนดให้ฉันต้องป้อนความเร็วด้วยตนเอง ตามหลักการแล้ว ฉันจะอ่านความเร็วจากต้นฉบับ GIF แต่ยังไม่พบวิธีที่จะบรรลุเป้าหมายนี้ - person Pauline; 24.01.2017
comment
Image.ANTIALIAS เป็นแบบเดิม (ดูบันทึกประจำรุ่นนี้) ยังคงสามารถใช้กับหมอนเวอร์ชันใหม่ได้เนื่องจากเชื่อมโยงกับ LANCZOS เนื่องจากการใช้ LANCZOS ใน pillow เวอร์ชันเก่าจะทำให้เกิดข้อยกเว้น และเนื่องจากการใช้ ANTIALIAS จะให้ผลลัพธ์ที่คล้ายกันกับการใช้ LANCZOS ฉันพบว่ามันอาจจะดีกว่าถ้าปล่อยไว้เป็น ANTIALIAS แต่ฉันอาจผิด และฉันยินดีที่จะเป็น แก้ไขแล้ว ขอบคุณสำหรับความช่วยเหลือ ฉันพบวิธีทำให้มันใช้งานได้แล้วและโพสต์ไว้เป็นคำตอบ - person Pauline; 24.01.2017
comment
@Pauline ดีใจที่คุณบรรลุเป้าหมายและดีใจที่ได้เป็นส่วนหนึ่งของการเดินทางของคุณ บันทึกไว้ใน ANTIALIAS พบกับ คำแนะนำเอกสาร PIL ของตัวกรอง BOX สำหรับการลดขนาดลงในระดับหนึ่ง - person Sun Bear; 25.01.2017

ฉันใช้ฟังก์ชันด้านล่างเพื่อปรับขนาดและครอบตัดรูปภาพรวมถึงภาพเคลื่อนไหว (GIF, WEBP) เพียงแค่เราจำเป็นต้องวนซ้ำแต่ละเฟรมใน gif หรือ webp

from math import floor, fabs
from PIL import Image, ImageSequence

def transform_image(original_img, crop_w, crop_h):
  """
  Resizes and crops the image to the specified crop_w and crop_h if necessary.
  Works with multi frame gif and webp images also.

  args:
  original_img is the image instance created by pillow ( Image.open(filepath) )
  crop_w is the width in pixels for the image that will be resized and cropped
  crop_h is the height in pixels for the image that will be resized and cropped

  returns:
  Instance of an Image or list of frames which they are instances of an Image individually
  """
  img_w, img_h = (original_img.size[0], original_img.size[1])
  n_frames = getattr(original_img, 'n_frames', 1)

  def transform_frame(frame):
    """
    Resizes and crops the individual frame in the image.
    """
    # resize the image to the specified height if crop_w is null in the recipe
    if crop_w is None:
      if crop_h == img_h:
        return frame
      new_w = floor(img_w * crop_h / img_h)
      new_h = crop_h
      return frame.resize((new_w, new_h))

    # return the original image if crop size is equal to img size
    if crop_w == img_w and crop_h == img_h:
      return frame

    # first resize to get most visible area of the image and then crop
    w_diff = fabs(crop_w - img_w)
    h_diff = fabs(crop_h - img_h)
    enlarge_image = True if crop_w > img_w or crop_h > img_h else False
    shrink_image = True if crop_w < img_w or crop_h < img_h else False

    if enlarge_image is True:
      new_w = floor(crop_h * img_w / img_h) if h_diff > w_diff else crop_w
      new_h = floor(crop_w * img_h / img_w) if h_diff < w_diff else crop_h

    if shrink_image is True:
      new_w = crop_w if h_diff > w_diff else floor(crop_h * img_w / img_h)
      new_h = crop_h if h_diff < w_diff else floor(crop_w * img_h / img_w)

    left = (new_w - crop_w) // 2
    right = left + crop_w
    top = (new_h - crop_h) // 2
    bottom = top + crop_h

    return frame.resize((new_w, new_h)).crop((left, top, right, bottom))

  # single frame image
  if n_frames == 1:
    return transform_frame(original_img)
  # in the case of a multiframe image
  else:
    frames = []
    for frame in ImageSequence.Iterator(original_img):
      frames.append( transform_frame(frame) )
    return frames
person muratgozel    schedule 01.12.2018