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

使用Bitmap写入GIF时如何设置图像质量及修复循环失效问题

问题解决:System.Drawing.Common生成GIF的画质与循环问题

一、修复无限循环失效问题

你的代码中loopPropertyItem的Len属性设置错误,GIF循环计数的属性值是2字节的ushort类型,因此Len必须设为2而非0。这个错误导致循环属性未被正确写入GIF文件,进而引发无限循环功能失效。

修正后的循环属性代码片段:

var loopPropertyItem = (PropertyItem)FormatterServices.GetUninitializedObject(typeof(PropertyItem));
loopPropertyItem.Id = PropertyTagLoopCount;
loopPropertyItem.Type = PropertyTagTypeShort;
loopPropertyItem.Len = 2; // 修正为2,匹配ushort的字节长度
loopPropertyItem.Value = BitConverter.GetBytes((ushort)0); // 0表示无限循环

二、提升GIF画质(解决调色板限制问题)

GIF格式最多仅支持256色,默认编码器会为每帧单独生成调色板,极易造成色彩丢失、画质下降。解决方案是为所有帧生成统一的最优256色调色板,再将所有帧转换为使用该调色板的索引位图,以此保证色彩一致性并最大化画质表现。

完整修改后的代码

using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Reflection;
using System.Collections.Generic;
using System.Linq;

public class GifCreator
{
    // Gdi+ 常量
    const int PropertyTagFrameDelay = 0x5100;
    const int PropertyTagLoopCount = 0x5101;
    const short PropertyTagTypeLong = 4;
    const short PropertyTagTypeShort = 3;
    const int UintBytes = 4;

    public static void CreateAnimatedGif(string outputPath, Bitmap[] sourceFrames, double fps = 25)
    {
        if (sourceFrames == null || sourceFrames.Length == 0)
            throw new ArgumentException("至少需要一个帧");

        // 生成全局最优调色板
        var palette = GenerateOptimalPalette(sourceFrames, 256);

        // 将所有帧转换为使用全局调色板的索引位图
        var indexedFrames = sourceFrames.Select(frame => ConvertToIndexedBitmap(frame, palette)).ToArray();

        var gifEncoder = GetEncoder(ImageFormat.Gif);

        // 第一帧参数
        var encoderParamsFirst = new EncoderParameters(1);
        encoderParamsFirst.Param[0] = new EncoderParameter(Encoder.SaveFlag, (long)EncoderValue.MultiFrame);

        // 后续帧参数
        var encoderParamsFrame = new EncoderParameters(1);
        encoderParamsFrame.Param[0] = new EncoderParameter(Encoder.SaveFlag, (long)EncoderValue.FrameDimensionTime);

        // 收尾参数
        var encoderParamsFlush = new EncoderParameters(1);
        encoderParamsFlush.Param[0] = new EncoderParameter(Encoder.SaveFlag, (long)EncoderValue.Flush);

        // 设置帧延迟
        var frameDelay = (PropertyItem)FormatterServices.GetUninitializedObject(typeof(PropertyItem));
        frameDelay.Id = PropertyTagFrameDelay;
        frameDelay.Type = PropertyTagTypeLong;
        frameDelay.Len = indexedFrames.Length * UintBytes;
        frameDelay.Value = new byte[indexedFrames.Length * UintBytes];
        var delayBytes = BitConverter.GetBytes((uint)(100 / fps));
        for (int i = 0; i < indexedFrames.Length; i++)
            Array.Copy(delayBytes, 0, frameDelay.Value, i * UintBytes, UintBytes);

        // 设置循环计数(修正Len值)
        var loopProperty = (PropertyItem)FormatterServices.GetUninitializedObject(typeof(PropertyItem));
        loopProperty.Id = PropertyTagLoopCount;
        loopProperty.Type = PropertyTagTypeShort;
        loopProperty.Len = 2; // 关键修正:ushort占2字节
        loopProperty.Value = BitConverter.GetBytes((ushort)0); // 0 = 无限循环

        if (!outputPath.EndsWith(".gif", StringComparison.OrdinalIgnoreCase))
            outputPath += ".gif";

        using (var stream = new FileStream(outputPath, FileMode.Create))
        {
            using (var firstFrame = indexedFrames[0])
            {
                firstFrame.SetPropertyItem(frameDelay);
                firstFrame.SetPropertyItem(loopProperty);
                firstFrame.Save(stream, gifEncoder, encoderParamsFirst);

                for (int i = 1; i < indexedFrames.Length; i++)
                {
                    using (var frame = indexedFrames[i])
                    {
                        firstFrame.SaveAdd(frame, encoderParamsFrame);
                    }
                }

                firstFrame.SaveAdd(encoderParamsFlush);
            }
        }

        // 释放临时帧资源
        foreach (var frame in indexedFrames)
            frame.Dispose();
    }

    // 获取图像编码器
    private static ImageCodecInfo GetEncoder(ImageFormat format)
    {
        return ImageCodecInfo.GetImageDecoders()
            .First(codec => codec.FormatID == format.Guid);
    }

    // 生成最优256色调色板
    private static ColorPalette GenerateOptimalPalette(Bitmap[] frames, int colorCount)
    {
        var colorHistogram = new Dictionary<Color, int>();

        // 统计所有帧的色彩出现次数
        foreach (var frame in frames)
        {
            for (int y = 0; y < frame.Height; y++)
            {
                for (int x = 0; x < frame.Width; x++)
                {
                    var color = frame.GetPixel(x, y);
                    if (colorHistogram.ContainsKey(color))
                        colorHistogram[color]++;
                    else
                        colorHistogram[color] = 1;
                }
            }
        }

        // 使用中位数切割算法生成调色板
        var colorList = colorHistogram.Select(kv => new ColorCount(kv.Key, kv.Value)).ToList();
        var paletteColors = MedianCut(colorList, colorCount);

        // 创建ColorPalette实例
        var palette = new Bitmap(1, 1, PixelFormat.Format8bppIndexed).Palette;
        palette.Entries = paletteColors.ToArray();
        return palette;
    }

    // 将位图转换为使用指定调色板的索引位图
    private static Bitmap ConvertToIndexedBitmap(Bitmap source, ColorPalette palette)
    {
        var indexedBitmap = new Bitmap(source.Width, source.Height, PixelFormat.Format8bppIndexed);
        indexedBitmap.Palette = palette;

        using (var g = Graphics.FromImage(indexedBitmap))
        {
            g.DrawImage(source, new Rectangle(0, 0, indexedBitmap.Width, indexedBitmap.Height));
        }

        return indexedBitmap;
    }

    // 中位数切割算法核心
    private static List<Color> MedianCut(List<ColorCount> colors, int targetCount)
    {
        if (colors.Count <= targetCount)
            return colors.Select(cc => cc.Color).ToList();

        // 找到色彩范围最宽的通道
        int rMin = colors.Min(cc => cc.Color.R), rMax = colors.Max(cc => cc.Color.R);
        int gMin = colors.Min(cc => cc.Color.G), gMax = colors.Max(cc => cc.Color.G);
        int bMin = colors.Min(cc => cc.Color.B), bMax = colors.Max(cc => cc.Color.B);

        int rRange = rMax - rMin;
        int gRange = gMax - gMin;
        int bRange = bMax - bMin;

        int maxRange = Math.Max(rRange, Math.Max(gRange, bRange));

        // 按对应通道排序
        if (maxRange == rRange)
            colors.Sort((a, b) => a.Color.R.CompareTo(b.Color.R));
        else if (maxRange == gRange)
            colors.Sort((a, b) => a.Color.G.CompareTo(b.Color.G));
        else
            colors.Sort((a, b) => a.Color.B.CompareTo(b.Color.B));

        // 切割为两组
        int midIndex = colors.Count / 2;
        var group1 = colors.Take(midIndex).ToList();
        var group2 = colors.Skip(midIndex).ToList();

        // 递归切割
        var result1 = MedianCut(group1, targetCount);
        var result2 = MedianCut(group2, targetCount);

        result1.AddRange(result2);
        return result1.Take(targetCount).ToList();
    }

    // 辅助类:存储色彩及出现次数
    private class ColorCount
    {
        public Color Color { get; }
        public int Count { get; }

        public ColorCount(Color color, int count)
        {
            Color = color;
            Count = count;
        }
    }
}

代码说明

  1. 循环修复:修正loopPropertyItem.Len为2,确保循环属性被正确写入GIF文件。
  2. 画质优化:
    • 统计所有帧的色彩分布,通过中位数切割算法生成最具代表性的256色调色板
    • 将所有帧转换为基于该调色板的索引位图,避免单帧单独调色导致的色彩断层
    • 统一调色板保证动画色彩一致性,最大化利用256色的表现力

内容的提问来源于stack exchange,提问作者julian bechtold

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.13 06:05:22