
上面这张 3072×4096 的 HEIC 照片,Chrome 和 Firefox 都打不开。右边写着"你的浏览器:打不开",左边的画面是 Sdcb.SimdCodecs 编译成 WebAssembly 后在浏览器里解出来的,黄色网格是文件里的 48 个 HEVC tile。
上周我写了《SimdCodecs 开源:纯 .NET 图像编解码,比 C 原生库还快》,文章末尾说 HEIF 是下一个要啃的硬骨头。一周之后,它已经发布了:
bash
dotnet add package Sdcb.SimdCodecs.Heif
在 8 核的 Ryzen 7 5800X 上,把手机照片解码成 RGBA,耗时是 libheif 1.23.1 的 0.20 到 0.32 倍 ,峰值内存也比它低。HEVC 解码出来的 Y/Cb/Cr 平面和 ffmpeg 8.1.2 逐字节一致。依然是纯 C#、MIT、能 NativeAOT、能进浏览器。
一周,四个版本
1.0 发布之后,我没停手,一周里又发了四个版本:
| 版本 | 日期 | 新增 |
|---|---|---|
| 1.1 | 10-30 | BMP(全部 Windows 和 OS/2 变体)、ICO / CUR,都能解码和编码 |
| 1.2 | 10-30 | QOI、Netpbm(PBM / PGM / PPM / PAM / PFM),都能解码和编码 |
| 1.3 | 11-03 | WebP 编码:无损、有损、带 alpha、动画 |
| 1.4 | 11-04 | HEIF / HEIC 解码 |
四个版本加起来大约 3.7 万行新代码,NuGet 上的包从 13 个变成了 18 个,版本号统一是 1.4.0。

1.4:HEIF / HEIC 解码
先说为什么 HEIF 最重要。现在 iPhone 和不少安卓手机,拍照默认就存成 HEIC。我自己的项目里,用户从手机浏览器上传上来的图,就经常是这种格式。而在 .NET 里想读它,以前基本只能靠 libheif、Magick.NET 这些原生库。
Sdcb.SimdCodecs.Heif 支持这些:
- HEVC Main(8 位)、Main 10(10 位)和 Main Still Picture,4:2:0;
- 手机相机用的
grid拼图,也就是上面那张图里的 48 个 tile; clap裁剪、irot旋转、imir镜像,按文件的意思摆正;- ICC 配置透传(手机照片常是 Display P3),nclx 色彩信息。
用法是先注册,然后门面的所有入口都认识 HEIC 了:
csharp
using Sdcb.SimdCodecs;
using Sdcb.SimdCodecs.Abstractions;
using Sdcb.SimdCodecs.Heif;
SimdCodecs.Register(HeifImageDecoder.Registration);
byte[] data = File.ReadAllBytes("photo.heic");
if (SimdCodecs.TryReadInfo(data, out ImageInfo info))
{
byte[] bgra = new byte[info.Width * info.Height * 4];
SimdCodecs.TryDecodeInto(data, bgra, 0, ImagePixelFormat.Bgra32);
}
在 实验台的 HEIC 批量转换 里,可以把一堆 HEIC 拖进去,转成 JPEG、PNG 或 WebP 后打包下载。整个过程都在你的浏览器里完成,照片不会上传:

性能
测量结果 来自 AMD Ryzen 7 5800X、.NET 10,对手 libde265 1.1.1、libheif 1.23.1 和 Magick.NET 14.13.1 都在同一个进程里、吃同一个 byte[]。单位是毫秒,越小越快:
| 解码到 RGBA,8 核 | Sdcb.SimdCodecs.Heif | libheif | Magick.NET |
|---|---|---|---|
| 3072×4096 手机照片(48 tile) | 49.6 | 195.7 | 443.3 |
| 3072×4096 Main 10(48 tile) | 65.1 | 270.2 | 505.9 |
| 8192×6144(192 tile) | 166.1 | 804.9 | 1770.5 |
| 1600×900 单张图(WPP) | 11.1 | 39.5 | 81.5 |
多核下 tile 是并行解的;没有 tile 的单张图,就用 HEVC 的 WPP 按 CTB 行并行。这对 libheif 来说不完全公平,所以我也单独比了单核:只看 HEVC 解码这一步,耗时是 libde265 的 0.68 到 0.87 倍。
内存上,每个 tile 一解完就直接转换写进输出缓冲区,任意像素布局、任意 stride 都一样,所以唯一和整张图一样大的缓冲区就是输出本身。12.6 MP 照片峰值内存增加 75 MB(libheif 80 MB,Magick.NET 220 MB),50 MP 的是 230 MB(libheif 373 MB)。

速度是怎么来的
HEVC 的帧内解码,大头在这几步:熵解码(CABAC)、反变换、帧内预测、去块滤波和 SAO,最后是 YCbCr 到 RGB。每一步我都做了 interleaved A/B 测量,归因靠的是逐项关闭的消融实验,不靠 profiler 估算。挑几个变化大的说:
- 反变换:最早是朴素的 O(N³) 三重循环。改成只算有系数的那块矩形上的 int32 矩阵乘、利用奇偶对称,再把缩放并进解析、残差加法并进内核之后,单核 12.6 MP 从 632 ms 降到 463 ms;
- CABAC:一次读一位,改成 64 位预读窗口,LPS 一步完成重归一化,状态转移表打包;
- SAO:逐样本检查邻居、slice 和旁路条件,改成在 CTB 级别一次性证明,再走 int16 的行内核;
- 帧内预测:每 4 个样本查一次 z-scan 可用性,改成 O(1) 查重建图,planar 和 angular 预测向量化。
每个向量内核都有一个标量版本作对照,AVX2、Vector128、标量、x64 和 arm64 输出完全一致,CI 在每一档上都跑一遍。
安全
HEIC 文件通常来自用户上传,解码器必须扛得住恶意输入。HeifFuzzTests 在 8 个进程里跑了 30 万轮模糊测试,找到并修了 3 个 bug:
iloc的字段读取没做边界检查;- SPS 里的图片尺寸不是 MinCbSizeY 的倍数时,会越界写;
- 一个很小的文件就能让每个 tile 都分配一张巨大的图。现在先校验 grid 和几何信息,再分配,并且限制了编码样本总数。
专利
上一篇我说过,HEIF 最麻烦的不是技术,是 HEVC 的专利池。代码全部自己写、用 MIT 发布,专利问题也绕不过去。
我的做法是:Sdcb.SimdCodecs.Heif 是一个独立的包,门面包 Sdcb.SimdCodecs 不引用它 ,需要你自己安装并调用 Register。包本身是 MIT,但不授予任何 HEVC 专利许可。要不要用 HEVC、在你所在的地区有什么义务,由每个应用自己评估。

1.4 还没做的:10 位的 Main 10 目前输出 8 位;alpha、HDR 增益图、深度图和 EXIF 还不读;图像序列(.heics)和 Main 12、4:4:4 这些扩展 profile 也不支持。
1.3:WebP 编码
1.0 的 WebP 只能解码,1.3 补上了编码,对手是 libwebp 1.6.0 和它自带的 cwebp、img2webp,14 张图共 9.1 MP:
- 无损 (
WebpEncoder.EncodeLossless):默认力度下,文件是 libwebp 默认档的 0.986 倍 ,速度是它的 1.14 倍; - 有损 (
WebpEncoder.EncodeLossy):同等 PSNR-Y 下 BD-rate −6.22% ,也就是同样画质文件小 6% 左右;q75 耗时是 libwebp 的 0.93 倍; - 带 alpha 的有损:alpha 默认无损,BD-rate −4.51%;
- 动画 (
WebpAnimationEncoder):编码器只写变化的矩形、合并不变的帧。和img2webp比,无损体积是它的 0.931 倍,有损 BD-rate −10.95%。
csharp
byte[] lossless = WebpEncoder.EncodeLossless(rgba, w, h, ImagePixelFormat.Rgba32);
byte[] lossy = WebpEncoder.EncodeLossy(rgba, w, h, ImagePixelFormat.Rgba32,
new WebpLossyEncodeOptions { Quality = 75 });
顺手还修了两处解码问题:宽度不是 16 倍数时右边缘的预测像素取法,以及半透明动画帧的混合舍入。现在这两种情况也和 libwebp 逐字节一致了。
实验台的 小工具 里有一个 GIF 转动画 WebP,8 帧的示例动画从 15.8 KB 变成 6.7 KB。旁边的 favicon.ico 生成器用的是 1.1 的 ICO 编码器:

1.1 和 1.2:BMP、ICO / CUR、QOI、Netpbm
这几个格式不难,但"冷门格式读不了"是实打实的缺口。比如 OS/2 的 BMP、带 RLE 压缩的 BMP、Netpbm 的 16 位 PGM 和浮点 PFM,很多库只支持一部分。
- BMP :core 到 V5 各版头部、1 到 32 位、任意通道掩码、RLE4 / RLE8 / RLE24、OS/2 Huffman 1D、内嵌 PNG、ICC。解码和 WIC 逐像素一致,22 种布局的几何平均耗时是 WIC 的 0.20 倍,编码到流是 0.05 到 0.08 倍;
- ICO / CUR:读出每一张图和光标热点,编码多尺寸图标和光标,每张图可以是 PNG 或 DIB;
- QOI / Netpbm :和 ImageSharp 3.1.12 比,解码耗时 0.29 倍 / 0.23 倍,编码 0.49 倍 / 0.54 倍,解码和编码两个方向都和 ImageMagick 结果一致。
这些格式都能零分配地解码进你自己的缓冲区,任意布局、任意 stride:
csharp
// 32×32 光标,热点在 (4, 2)
byte[] cur = IcoEncoder.EncodeCursor([new IcoFrame(rgba32, 32, 32, HotspotX: 4, HotspotY: 2)]);
// 8 位调色板 + RLE 压缩 + 抖动的 BMP
byte[] bmp = BmpEncoder.Encode(rgba, width, height, ImagePixelFormat.Rgba32,
new BmpEncodeOptions { Format = BmpEncodeFormat.Indexed8, Rle = true, Dither = true });
新格式和老格式一样,都有模糊测试,也都进了 NativeAOT 和浏览器 WebAssembly 的冒烟测试,CI 在 Vector128、标量和 arm64 上各跑一遍。
新的 sdcb.ai/simdcodecs
主页的性能一节,新增了 HEIC、BMP、QOI / Netpbm 的卡片:

实验台从三个页签改成了五个:打开并检查、兼容性实测、格式擂台、HEIC 批量转换、小工具。前面已经出现过三个,再看另外两个。
兼容性实测:把 19 个示例文件分别交给浏览器自己的解码器和 SimdCodecs。我用无头 Chrome 145 跑的结果是 7 / 19 对 19 / 19,浏览器打不开 HEIC、TIFF、JPEG 2000、JBIG2、QOI 和 Netpbm:

格式擂台:同一张图过 9 个编码器,看体积、PSNR 和耗时,左边用滑块和原图对比。默认的输入就是那张 HEIC 照片:

浏览器里是单线程 128 位 SIMD,耗时只供参考,桌面上跑 AVX2 要快得多。实验台的数字是 wasm 的,前面那些对比表的数字是桌面的。
2.0 展望
1.4 的宣传慢了一步,2.0 其实已经差不多做完了,代码在 main 分支上。2.0 没有加新格式,主要是重新设计 API,把 1.x 里一些"做得到但不好用"的地方补齐:
- 16 位和预乘输出 :
ImagePixelFormat枚举换成PixelFormat结构,新增Rgba64、Gray16、Bgra32Premultiplied等。HEIF 的 Main 10 终于能输出 16 位了; DecodeOptions:区域解码(HEIF 只解码区域涉及的 tile)、JPEG 1/2 到 1/8 缩放解码、池化内存;- 元数据和缩略图 :
TryReadMetadata拿到 Exif、XMP、ICC 原始块,TryDecodeThumbnail取出内嵌的预览图(Exif IFD1、HEIF 的thmb); ImageDecoder.Open:类似 WIC 的会话,可以打开Stream、内存映射文件或内存;- 统一的编码入口 :
SimdCodecs.Encode(image, ImageFormat.Png, stream),9 个编码器一套 API,接收任意PixelFormat; - 所有入口都接收
CancellationToken; - JPEG 2000 和 JBIG2 补全 :JPEG 2000 的多 tile、全部 5 种渐进顺序等,16 个测试文件和
opj_decompress一致;JBIG2 在 114 个文件里有 112 个和 jbig2dec 逐像素一致,另外 2 个两边都判定为损坏; - 浏览器里的 JPEG 解码:加了 PackedSimd 内核,示例照片从 22.0 ms 降到 8.2 ms。
API 改名的地方,大多数代码改个名字就行,8 位输出和 1.x 逐字节一致。完整清单见 迁移指南。性能方面,JPEG、PNG、WebP 迁移到新 API 后和 1.x 相比在测量噪声范围内,没有变慢。

也老实说一下 2.x 之后还没做的:
- 所有格式依然要把整个文件读进内存,真正的流式输入还没做;
- 除了 HEIF,区域解码目前是先整张解码再裁剪;
- JPEG 2000 还没调优,耗时约是 libopenjp2 的 2 倍;
- AVIF 和 JPEG XL 目前只能识别,不能解码;
- HEIF 编码大概率不会做:HEVC 编码的专利问题比解码更麻烦,而且编码器的工作量也不比解码器小。要输出,用 JPEG、WebP 或 PNG 就好。
最后
- GitHub:sdcb/SimdCodecs
- NuGet:
dotnet add package Sdcb.SimdCodecs,HEIC 另装Sdcb.SimdCodecs.Heif - 项目主页:sdcb.ai/simdcodecs
- 在线实验台:sdcb.ai/simdcodecs/playground

交流
感谢阅读到这里,如果觉得 SimdCodecs 有用,欢迎去 GitHub 点个 Star ,也欢迎评论 和点赞,这是我持续创作的动力!

欢迎关注微信公众号 .NET骚操作 ,或者扫码加入 .NET骚操作 微信交流群:

(如果微信群二维码过期,请加入 .NET骚操作交流QQ群 :495782587 ;做图像和视觉相关开发的朋友,也欢迎加入 C#/.NET计算机视觉技术交流 QQ 群 :579060605)