GIF hoạt động thế nào và vì sao lại nặng như vậy

Bảng màu, nén LZW và thời gian khung giải thích bằng một đoạn 3 giây thật: vì sao GIF nặng gấp 3,4 lần MP4, và những thiết lập giúp nó nhẹ đi.

Trong bài này

GIF ra đời năm 1987 và đến nay vẫn là cách phần lớn mọi người chia sẻ một đoạn lặp ngắn. Nó cũng nổi tiếng là tốn dung lượng: cùng vài giây, bản GIF thường nặng gấp mấy lần bản video. Bài này mở định dạng ra từng bước và đo xem mỗi thiết lập của công cụ tạo GIF ảnh hưởng thế nào lên một đoạn video thật. Các con số có được bằng cách chạy chính bộ mã hóa của công cụ trên đoạn đó, không phải ước lượng theo kinh nghiệm.

Đoạn video thử

Để có thứ lặp lại được, tôi tạo một đoạn 3 giây bằng mẫu testsrc2 của FFmpeg: 480 × 270 điểm ảnh, 10 khung mỗi giây, tức 30 khung. Nó có dải màu chuyển động, chữ chạy và bộ đếm, là trường hợp khó, gần với cảnh quay nhiều chuyển động hơn là quay màn hình. Mã hóa thành MP4 H.264 với chất lượng mặc định của FFmpeg (CRF 23), nó nặng 106 KB.

Cùng 30 khung đó ở dạng điểm ảnh thô là 15,6 MB (30 × 480 × 270 × 4 byte). Phần dưới đây kể GIF đi từ 15,6 MB xuống thế nào, và vì sao nó dừng lại cao hơn 106 KB rất nhiều.

Bước 1: tối đa 256 màu mỗi khung

Một khung GIF không lưu màu mà lưu chỉ số trỏ vào bảng màu tối đa 256 mục. Điều đó biến 4 byte mỗi điểm ảnh thành 1 byte (hoặc ít hơn) trước khi nén, nhưng ảnh chụp hay dải màu có hàng chục nghìn màu nên phải giảm màu trước.

Công cụ chọn bảng màu bằng median cut: đếm điểm ảnh vào một biểu đồ tần suất, đặt mọi màu vào một hộp, rồi liên tục cắt đôi hộp có độ phân tán lớn nhất tại trung vị của kênh màu rộng nhất, cho đến khi số hộp bằng số mục bảng màu. Mỗi hộp thành một màu, là trung bình các điểm ảnh trong hộp. Sau đó mỗi điểm ảnh được thay bằng màu gần nhất trong bảng.

Có hai lựa chọn quan trọng:

  • Một bảng màu cho cả ảnh động hay mỗi khung một bảng. Bảng chung chỉ ghi một lần; bảng riêng tốn tới 768 byte mỗi khung và có thể làm màu nhấp nháy khi cùng một điểm ảnh rơi vào màu hơi khác nhau ở các khung liền nhau.
  • Bao nhiêu màu. Ít màu hơn thì ít chỉ số khác nhau hơn, chuỗi lặp dài hơn và nén tốt hơn.

Bước 2: nén LZW

Chỉ số của mỗi khung được nén bằng LZW (Lempel–Ziv–Welch, 1984). Nó bắt đầu với từ điển gồm các chỉ số đơn lẻ, và trong lúc đọc, thêm mỗi chuỗi mới "phần đã có cộng chỉ số tiếp theo" thành một mã mới. Các mẫu lặp lại nhanh chóng chỉ còn một mã. Độ rộng mã bắt đầu bằng số bit của bảng màu cộng một, tăng dần tối đa 12 bit; khi từ điển đủ 4096 mục, một mã xóa cho phép bắt đầu lại.

Một ví dụ nhỏ từ bộ kiểm thử của bộ mã hóa: bốn điểm ảnh màu 0 trong ảnh 2 màu. Kích thước mã tối thiểu là 2, nên mã xóa là 4, mã kết thúc là 5, mỗi mã rộng 3 bit. Bộ mã hóa phát ra 4 (xóa), 0, rồi 6 (mã mới cho "0, 0"), rồi 0, rồi 5 (kết thúc). Xếp bit thấp trước, ta được 4 + 0·8 + 6·64 + 0·512 + 5·4096 = 20.868, tức hai byte 84 51. Bốn điểm ảnh, hai byte.

LZW rất giỏi với vùng phẳng và chuỗi dài, gần như vô dụng với nhiễu: một điểm ảnh khác hàng xóm là cắt đứt chuỗi.

Các thiết lập tác động lên đoạn thử

Mọi dung lượng là cả tệp GIF do bộ mã hóa của công cụ tạo ra, cùng 30 khung:

Thiết lậpDung lượng
256 màu, mỗi khung một bảng, lưu cả khung457 KB
256 màu, bảng chung, lưu cả khung425 KB
256 màu, bảng chung, chỉ lưu phần thay đổi356 KB
64 màu, bảng chung, chỉ lưu phần thay đổi222 KB
256 màu, bảng chung, chỉ lưu phần thay đổi, bật hòa sắc481 KB

Để so sánh, GIF do chính FFmpeg tạo với bảng màu tối ưu và hòa sắc mặc định là 530 KB, còn MP4 là 106 KB. Ngay cả thiết lập GIF tốt nhất ở đây vẫn gấp khoảng hai lần MP4, và mặc định 256 màu gấp khoảng 3,4 lần. MP4 thắng vì nó dự đoán mỗi khung từ khung trước và chỉ mã hóa phần khác biệt, với vectơ chuyển động và phép biến đổi thiết kế cho ảnh tự nhiên; GIF nén từng khung một cách độc lập.

Bước 3: chỉ lưu phần thay đổi

GIF có hai tính năng cho phép một kiểu nén giữa các khung rất rẻ: một khung có thể nhỏ hơn màn hình và đặt ở bất kỳ đâu, và một khung có thể đánh dấu một chỉ số là trong suốt để phần đang hiện trên màn hình lộ ra. Với cách xử lý "giữ nguyên tại chỗ", khung sau được vẽ đè lên.

Vì vậy sau khung đầu tiên, công cụ chỉ lưu hình chữ nhật bao quanh các điểm ảnh thay đổi, và bên trong đó biến các điểm ảnh trông giống trước thành chỉ số trong suốt. Chuỗi dài "trong suốt" gần như không tốn gì sau khi nén. Với đoạn thử nhiều chuyển động, cách này tiết kiệm 16% (425 → 356 KB), vì gần như mọi thứ đều chuyển động.

Với đoạn gần như đứng yên, hiệu quả rất lớn. Tôi làm phiên bản thứ hai trong đó chỉ một vùng 100 × 40 điểm ảnh thay đổi (như đồng hồ đếm trên một slide), phần còn lại đứng yên: 433 KB khi lưu cả khung, 24 KB khi chỉ lưu phần thay đổi. Video quay màn hình, demo giao diện và cảnh một người nói chuyện đều thuộc kiểu này.

Bước 4: thời gian khung

Mỗi khung có thời gian hiển thị tính bằng phần trăm giây, và có hai cái bẫy:

  • Trình duyệt coi 0 và 1 phần trăm giây là 10 phần trăm giây. GIF "100 khung/giây" thực tế chạy 10 khung/giây. Thời gian ngắn nhất thật sự có tác dụng là 2 phần trăm giây, tức 50 khung/giây. Công cụ không bao giờ ghi thấp hơn.
  • Tốc độ không chia hết cho 100. 15 khung/giây là 6,67 phần trăm giây mỗi khung, không lưu được. Làm tròn mọi khung thành 7 thì 1 giây thành 1,05 giây; công cụ cộng dồn phần dư, ghi 7, 6, 7, 7, 6, 7… để 15 khung đúng một giây.

Lặp lại là một khối riêng (NETSCAPE2.0) có số lần lặp: 0 là mãi mãi, còn "phát ba lần" được ghi là lặp lại 2 lần.

Khi nào nên dùng định dạng khác

Nếu nơi nhận phát được video, hãy dùng video. MP4 hay WebM nhẹ hơn, mượt hơn và có tiếng. WebP động cũng nhẹ hơn GIF nhiều và chạy được trên mọi trình duyệt hiện nay, nhưng nhiều ứng dụng chat và phần mềm cũ vẫn chỉ cho GIF chuyển động, nên GIF vẫn tồn tại.

Những lỗi hay gặp

  • Xuất ở Full HD. Chiều rộng tốn gấp đôi: rộng 960 px có số điểm ảnh gấp bốn lần 480 px. Để gửi chat, 320 đến 480 px là đủ.
  • Quá nhiều khung mỗi giây. 10 đến 15 khung/giây đã đủ mượt cho phần lớn đoạn lặp; 30 khung/giây làm dung lượng tăng gấp hai, ba lần.
  • Hòa sắc cho ảnh chụp màn hình. Hòa sắc giúp dải màu và ảnh chụp; với giao diện phẳng nó chỉ thêm nhiễu (trên đoạn thử nó làm tăng 35%).
  • Mặc định dùng bảng màu riêng cho từng khung. Chỉ nên dùng khi các cảnh rất khác nhau; bảng chung nhẹ hơn và không nhấp nháy.
  • Đoạn quá dài. GIF 20 giây gần như luôn là sai định dạng. Hãy quay video bằng trình quay màn hình, hoặc cắt đoạn lặp còn ba giây cốt lõi.

Công thức

  1. Giữ đoạn 3 đến 5 giây trong công cụ tạo GIF.
  2. Rộng 480 px (320 cho nhãn dán), 10 đến 12 khung/giây.
  3. Bảng màu chung, bật "Chỉ lưu phần thay đổi".
  4. Thử 128 hoặc 64 màu; chỉ bật hòa sắc nếu thấy vệt màu trên dải chuyển màu.
  5. Xem dung lượng ước tính, tạo GIF, và so với bản gốc trước khi gửi.

Công cụ được nhắc trong bài

Về trang blog