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

