You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于原始字节的GIF编码器:小图正常大图失败,疑LZW实现有误

GIF编码器LZW压缩实现问题:小图正常大图异常

我正在编写一个通过写入原始字节生成GIF文件的编码器,参考了W3C的GIF89a规范文档以及维基百科的GIF条目。我的代码在处理小尺寸图片时可以正常工作,但处理稍大的图片时会失败。我猜测可能是LZW压缩算法的实现存在错误,但无法定位问题所在。

注:该编码器用于特定场景,因此部分代码直接写入常量(除非错误出在头部区域)。

编码器代码

class GifWriter(object):
    def __init__(self, output):
        self.out = open(output, 'wb')
    
    def writeHeader(self, width, height, bpp=4):
        self.bpp = max(min(bpp,8),1)
        
        self.out.write(b'GIF89a')
        self.out.write(width.to_bytes(2,byteorder='little'))
        self.out.write(height.to_bytes(2,byteorder='little'))
        b = 0xf0 + (self.bpp-1)
        self.out.write(b.to_bytes(1,byteorder='big'))
        self.out.write(b'\x00\x00')
    
    def writeApplicationExtensionLoop(self, loopCount=0):
        self.out.write(b'\x21\xFF\x0B')
        self.out.write(b'\x4E\x45\x54\x53\x43\x41\x50\x45\x32\x2E\x30')
        self.out.write(b'\x03\x01\x00\x00')
        self.out.write(b'\x00')
    
    # rgbIntArr = [r0, g0, b0, r1, g1, b1, ...]
    # 0 <= r*,g*,b* <= 255
    def writePalette(self, rgbIntArr):
        palette = np.empty((len(rgbIntArr)//3,3))
        i = 0
        for c in rgbIntArr:
            self.out.write(c.to_bytes(1,byteorder='big'))
            palette[i//3][i%3] = c
            i+=1
        self.paletteTree = scipy.spatial.KDTree(palette,copy_data=True)
    
    # image.shape = (w,h,3)
    def writeImage(self, image, delay):
        image = image[:,:,0:3]
    
        #Graphics Control Extension
        self.out.write(b'\x21\xF9\x04')
        self.out.write(b'\x00') #Packed Fields
        self.out.write(delay.to_bytes(2,byteorder='little')) #Delay time (1/100)
        self.out.write(b'\x00') #Transparent Color Index (not used)
        self.out.write(b'\x00') #Terminator
        
        #Image Descriptor
        self.out.write(b'\x2C') #Seperator
        self.out.write(b'\x00\x00\x00\x00') #Top Left Corner
        self.out.write(image.shape[1].to_bytes(2,byteorder='little')) #Width
        self.out.write(image.shape[0].to_bytes(2,byteorder='little')) #Height
        self.out.write(b'\x00') #Packed Fields
        
        
        #Image Data
        self.out.write(self.bpp.to_bytes(1,byteorder='big')) #LZW Minimum Code Size
        
        maxLen = 1
        currCode = 2**self.bpp+2
        dict = {}
        for i in range(16):
            dict[chr(i)] = i
        dict[chr(2**self.bpp+1)] = 2**self.bpp
        dict[chr(2**self.bpp+2)] = 2**self.bpp+1
        dataStr = chr(2**self.bpp+1) + "".join([chr(item) for item in self.paletteTree.query(image)[1].flatten()]) + chr(2**self.bpp+2)
        ind = 0
        
        bufferInd = 0
        buffer = bytearray(255)
        acc = 0x0
        accInd = 0      
        
        while ind < len(dataStr):
            for j in range(maxLen,0,-1):
                if ind+j <= len(dataStr) and dataStr[ind:ind+j] in dict and currCode < 2**12:
                    acc += dict[dataStr[ind:ind+j]] << accInd
                    accInd += (currCode-1).bit_length()
                    out.append(dict[dataStr[ind:ind+j]])
                    if accInd > 7:
                        buffer[bufferInd] = acc & 0xff
                        acc = acc >> 8
                        accInd -= 8
                        bufferInd += 1
                        if bufferInd == 255:
                            bufferInd = 0
                            self.out.write(b'\xff')
                            self.out.write(buffer)
                    
                    if not dataStr[ind:ind+j+1] in dict and ind != 0 and ind != len(dataStr)-1:
                        dict[dataStr[ind:ind+j+1]] = currCode
                        currCode += 1
                        maxLen = j+1
                    ind += j
        if accInd > 0:
            buffer[bufferInd] = acc & 0xff
            bufferInd += 1
        if bufferInd > 0:
            self.out.write(bufferInd.to_bytes(1,byteorder='big'))
            self.out.write(buffer[:bufferInd])
        
        self.out.write(b'\x00')
        
    def close(self):
        self.out.write(b'\x3B')
        self.out.close()

使用示例

import numpy as np

## Example 1
gif = GifWriter('e1.gif')
gif.writeHeader(3,5)

colors = [0x00,0x00,0x00,0xff,0xff,0xff]
a = np.empty((42))
a[0::3] = np.linspace(255,0,15)[:-1]
a[1::3] = np.linspace(255,0,15)[:-1]
a[2::3] = 0
for b in a.tolist():
    colors.append(int(b))
gif.writePalette(colors)

gif.writeApplicationExtensionLoop()

data = np.asarray([[[0x00,0x00,0x00],[0xff,0xff,0xff],[0xff,0xff,0xff]] , [[0xff,0xff,0xff],[0x00,0x00,0x00],[0xff,0xff,0xff]] , [[0xff,0xff,0x00],[0xdd,0xdd,0x00],[0xbb,0xbb,0x00]] , [[0x99,0x99,0x00],[0x77,0x77,0x00],[0x55,0x55,0x00]] , [[0x33,0x33,0x00],[0x11,0x11,0x00],[0x00,0x00,0x00]]])
gif.writeImage(data,100)
gif.close();


## Example 2
N = 5
gif = GifWriter('e2.gif')
gif.writeHeader(N,N)

colors = [0x00,0x00,0x00,0xff,0xff,0xff]
a = np.empty((42))
a[0::3] = np.linspace(255,0,15)[:-1]
a[1::3] = np.linspace(255,0,15)[:-1]
a[2::3] = 0
for b in a.tolist():
    colors.append(int(b))
gif.writePalette(colors)
gif.writeApplicationExtensionLoop()

data = np.empty((N,N,3))

data[:,:,0] = (np.flip(np.linspace(0,1,N))*np.ones((N,1)))*255
data[:,:,1] = (np.flip(np.linspace(0,1,N))*np.ones((N,1)))*255
data[:,:,2] = 0

data = data.astype(int)
gif.writeImage(data,100)
gif.close();

示例1可正常输出GIF文件,但示例2输出异常,实际效果与预期不符。以下是十六进制dump:

十六进制Dump

Example 1:
           00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F

00000000   47 49 46 38 39 61 03 00 05 00 F3 00 00 00 00 00  GIF89a....ó.....
00000010   FF FF FF FF FF 00 EC EC 00 DA DA 00 C8 C8 00 B6  ......ìì.ÚÚ.ÈÈ.¶
00000020   B6 00 A3 A3 00 91 91 00 7F 7F 00 6D 6D 00 5B 5B  ¶.££..⌂⌂.mm.[[
00000030   00 48 48 00 36 36 00 24 24 00 12 12 00 21 FF 0B  .HH.66.$$....!..
00000040   4E 45 54 53 43 41 50 45 32 2E 30 03 01 00 00 00  NETSCAPE2.0.....
00000050   21 F9 04 00 64 00 00 00 2C 00 00 00 00 03 00 05  !ù..d...,.......
00000060   00 00 04 0A 10 84 09 82 20 06 A5 D5 1E 88 00 3B  ...... .¥Õ..;

Example 2:
           00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F

00000000   47 49 46 38 39 61 05 00 05 00 F3 00 00 00 00 00  GIF89a....ó.....
00000010   FF FF FF FF FF 00 EC EC 00 DA DA 00 C8 C8 00 B6  ......ìì.ÚÚ.ÈÈ.¶
00000020   B6 00 A3 A3 00 91 91 00 7F 7F 00 6D 6D 00 5B 5B  ¶.££..⌂⌂.mm.[[
00000030   00 48 48 00 36 36 00 24 24 00 12 12 00 21 FF 0B  .HH.66.$$....!..
00000040   4E 45 54 53 43 41 50 45 32 2E 30 03 01 00 00 00  NETSCAPE2.0.....
00000050   21 F9 04 00 64 00 00 00 2C 00 00 00 00 05 00 05  !ù..d...,.......
00000060   00 00 04 0C 50 98 D4 80 4C 55 CC 66 0D 05 B7 46  ....PÔLUÌf..·F
00000070   00 3B                                            .;

问题分析与修正

你的LZW压缩实现存在多个关键错误,导致大图处理失败:

1. CLEAR/END码初始化与使用错误

GIF的LZW规范中:

  • CLEAR码值为2^bpp(比如bpp=4时为16)
  • END码值为2^bpp + 1(比如bpp=4时为17)
  • 图像数据必须以CLEAR码开头,END码结尾

你的代码中错误地将END码作为起始码,还使用了不存在的码作为结束码,且字典中CLEAR和END码的映射完全搞反。

2. 未定义变量out

代码中out.append(dict[dataStr[ind:ind+j]])里的out从未定义,会抛出NameError,直接删除这行即可。

3. 码长计算逻辑错误

你用(currCode-1).bit_length()计算当前码的长度,这会在码表达到阈值时出错。正确的做法是跟踪当前码长,初始为bpp+1,当currCode达到2^current_code_len时,码长加1(最大到12位)。

4. 码表满时未重置

当currCode达到2^12(4096)时,必须发送CLEAR码,重置字典、码长和currCode,否则后续编码会超出12位限制。

5. 新码添加条件错误

你限制ind !=0才能添加新码,这会导致第一个图像码无法扩展字典,影响压缩逻辑,应该移除该限制,只要不是最后一个字符即可。

修正后的LZW编码部分代码

替换writeImage方法中的Image Data部分:

#Image Data
self.out.write(self.bpp.to_bytes(1,byteorder='big')) #LZW Minimum Code Size

clear_code = 2 ** self.bpp
end_code = clear_code + 1
current_code_len = self.bpp + 1  # 初始码长为bpp+1
max_code = 2 ** current_code_len

# 初始化字典
code_dict = {}
for i in range(2 ** self.bpp):
    code_dict[chr(i)] = i
code_dict[chr(clear_code)] = clear_code
code_dict[chr(end_code)] = end_code

curr_code = end_code + 1
data_str = chr(clear_code) + "".join([chr(item) for item in self.paletteTree.query(image)[1].flatten()]) + chr(end_code)
ind = 0

buffer_ind = 0
buffer = bytearray(255)
acc = 0x0
acc_bits = 0      

while ind < len(data_str):
    # 查找最长匹配的字符串
    match_len = 0
    for j in range(min(len(data_str)-ind, current_code_len), 0, -1):
        substr = data_str[ind:ind+j]
        if substr in code_dict:
            match_len = j
            break
    
    if match_len == 0:
        match_len = 1  # 至少匹配单个字符
    
    code = code_dict[data_str[ind:ind+match_len]]
    # 将码写入缓冲区
    acc |= code << acc_bits
    acc_bits += current_code_len
    
    while acc_bits >= 8:
        buffer[buffer_ind] = acc & 0xff
        acc >>= 8
        acc_bits -= 8
        buffer_ind += 1
        if buffer_ind == 255:
            self.out.write(b'\xff')
            self.out.write(buffer)
            buffer_ind = 0
    
    # 添加新码到字典(如果不是最后一个字符,且码表未满)
    if ind + match_len < len(data_str) and curr_code < 4096:
        new_substr = data_str[ind:ind+match_len+1]
        if new_substr not in code_dict:
            code_dict[new_substr] = curr_code
            curr_code += 1
            # 检查是否需要增加码长
            if curr_code >= max_code and current_code_len < 12:
                current_code_len += 1
                max_code = 2 ** current_code_len
    
    # 处理码表满的情况
    if curr_code >= 4096:
        # 发送CLEAR码
        acc |= clear_code << acc_bits
        acc_bits += current_code_len
        while acc_bits >= 8:
            buffer[buffer_ind] = acc & 0xff
            acc >>= 8
            acc_bits -= 8
            buffer_ind += 1
            if buffer_ind
相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.25 09:15:40