You need to enable JavaScript to run this app. 手动实现Bitmap缩放出现图像损坏,如何基于BitmapData正确处理?
优惠活动
大模型
产品
解决方案
定价
更多

手动实现Bitmap缩放出现图像损坏,如何基于BitmapData正确处理?

Bitmap内存排布规则
  • 像素按行逐行存储,每一行的实际占用字节数称为步长(Stride),系统会自动将步长对齐到4字节的整数倍,因此步长不一定等于「图像宽度 × 单像素字节数」,每行末尾可能存在无意义的填充字节,不能直接把整张图当成连续的一维像素数组计算偏移量。
  • 单个像素的字节顺序和像素格式强相关,比如常用的Format24bppRgb是BGR顺序,Format32bppArgb是BGRA顺序,而非默认的RGB顺序。
  • 行与行之间的偏移量由步长决定,不是「宽度×单像素字节数」,这是手写位图操作代码最常踩的坑。
现有代码的核心错误点
  1. 完全忽略步长的存在:直接按全图字节偏移计算源地址,会把目标行末尾的填充字节也算成有效像素,映射到源地址时要么越界要么读到错误行的数据,直接导致图像花屏损坏。
  2. 采样间隔计算错误:提前把1/scaleFactor转成整数,会产生截断误差,比如scaleFactor=0.6时1/0.6≈1.666,转int直接变成1,实际采样间隔完全不符合预期,缩放比例完全错位。
  3. 多线程拆分逻辑不合理:按字节偏移拆分任务,源和目标的行对齐规则不一致,容易出现行错位问题,性能收益也远不如按行拆分稳定。
  4. API设计致命问题:在扩展方法里直接Dispose传入的原Bitmap,调用方根本感知不到原对象会被销毁,后续访问原Bitmap会直接抛出异常。
  5. 多余的向上取整逻辑:Image.GetPixelFormatSize返回的位深永远是8的整数倍,直接除以8就能得到单像素字节数,不需要Math.Ceiling处理。
修复后实现方案

以下是按行遍历的最近邻采样实现,性能完全可以满足60fps的缩放需求:

public static Bitmap ResizeBitmap(this Bitmap bmp, double scaleFactor)
{
    int desiredWidth = (int)Math.Round(bmp.Width * scaleFactor);
    int desiredHeight = (int)Math.Round(bmp.Height * scaleFactor);
    var scaled = new Bitmap(desiredWidth, desiredHeight, bmp.PixelFormat);
    int formatSize = Image.GetPixelFormatSize(bmp.PixelFormat) / 8;

    BitmapData srcData = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.ReadOnly, bmp.PixelFormat);
    BitmapData destData = scaled.LockBits(new Rectangle(0, 0, desiredWidth, desiredHeight), ImageLockMode.WriteOnly, scaled.PixelFormat);

    unsafe
    {
        byte* srcBase = (byte*)srcData.Scan0;
        byte* destBase = (byte*)destData.Scan0;
        double srcStep = 1 / scaleFactor;

        // 按行并行处理,稳定性更高
        Parallel.For(0, desiredHeight, destY =>
        {
            int srcY = (int)Math.Round(destY * srcStep);
            byte* srcRow = srcBase + srcY * srcData.Stride;
            byte* destRow = destBase + destY * destData.Stride;

            for (int destX = 0; destX < desiredWidth; destX++)
            {
                int srcX = (int)Math.Round(destX * srcStep);
                int srcOffset = srcX * formatSize;
                int destOffset = destX * formatSize;
                for (int j = 0; j < formatSize; j++)
                {
                    destRow[destOffset + j] = srcRow[srcOffset + j];
                }
            }
        });
    }

    bmp.UnlockBits(srcData);
    scaled.UnlockBits(destData);
    return scaled;
}

如果需要更平滑的缩放效果,可以将最近邻采样替换为双线性插值,性能会略有下降但仍然远高于GDI+自带的缩放方法。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.09 03:54:00