增益图(Gain Map)的容器层 —— 纯托管、零依赖、零进程调用:解析与写入
Ultra HDR JPEG(ISO 21496-1 + MPF)与 ISOBMFF(AVIF / HEIC 的 tmap 派生项)里的增益图元数据。
本库是从 ffmpegPictureUI 的
Services/GainMapDecoder.cs/IsoBmffGainMapProbe.cs/IsoBmffGainMapWriter.cs中 抽取出的可独立编译子集。宿主仍是权威副本,同步口径见 §7。 本文件是本仓 GainMapKit 工程的详细文档;色彩数学内核那份见ColorMatrix.md, 两个工程互不依赖,可只引用其中一个。总览与合仓说明见README.md。
是:字节层的容器读写。给你一段 JPEG / AVIF / HEIC 的字节,本库告诉你「有没有增益图、 以什么形式承载、元数据各字段是什么」;也能反过来把增益图元数据装配进 AVIF 容器。
不是:把增益真正作用到像素上(HDR = SDR × gain)、JPEG 解码、编码调度。
分工要说清:增益图的像素侧数学(灰度增益 SimdPixelOps.ComputeGainMapGray、
分段 Reinhard SegmentedReinhardScalar)在本仓的 ColorMatrix 工程里;
而"读像素 → 应用 → 编码写文件"那一段要起进程、要临时文件、要读应用设置
⇒ 留在宿主 ffmpegPictureUI,一旦搬进来就不成其为库了。
using GainMapKit;
// 读:Ultra HDR JPEG 容器(不是增益图容器 / 解析不出 ⇒ 返回 null 或 IsGainMapContainer()=false)
var c = JpegGainMapContainer.Parse(File.ReadAllBytes("ultrahdr.jpg"));
if (c != null && c.IsGainMapContainer() && c.IsoMetadata != null)
{
float maxBoostLog2 = c.IsoMetadata.GainMapMax; // ISO 存 log2 分数值
float maxBoostLinear = MathF.Pow(2f, maxBoostLog2); // 厂商工具报的是线性值
}
// 读:ISOBMFF(AVIF / HEIC)
var r = IsoBmffGainMapProbe.Probe(File.ReadAllBytes("hdr.avif"));
// r.HasGainMap / r.Form("tmap") / r.PrimaryItemId / r.TmapItemId / r.GainMapItemId
// r.GainMapCodec("av01"|"hevc"…) / r.GainMapFfmpegFormat / r.TmapPayload
// 写:把 tmap 派生项装进 AVIF(底图与增益图各是一张单图 AVIF 的字节)
var payload = IsoBmffGainMapWriter.BuildTmapPayload(iso21496MetadataBytes);
var built = IsoBmffGainMapWriter.Build(baseAvif, gainMapAvif, payload,
basePrimaries: 9, baseTransfer: 16, baseMatrix: 9);
if (!built.Ok) throw new Exception(built.FailReason); // 失败一定点名,不静默产出坏容器
File.WriteAllBytes("out.avif", built.Data!);| 文件 | 职责 |
|---|---|
GainMapMetadata.cs |
解析后的增益图元数据(17 个字段,含 hdrgm 声明的逻辑尺寸) |
JpegGainMapContainer.cs |
Ultra HDR JPEG 容器:顶层多张 JPEG 边界、APP2 urn:iso:…:21496:-1 载荷、MPF(TIFF,双端序)条目;ParseIso21496 是 ISO 元数据的唯一解析入口 |
IsoBmffGainMapProbe.cs |
ISOBMFF 读侧:ftyp 品牌、meta/pitm/iinf/iref/iprp(grpl) 走查、tmap 派生项定位、hvcC→Annex-B 前置转换、失败原因一律 [IsoBmff] 前缀 |
IsoBmffGainMapWriter.cs |
ISOBMFF 写侧:装配 tmap 项 + iref/dimg + grpl(altr) + 改写底图 colr,输出单文件容器 |
自检套件 78 PASS / 0 FAIL(exit 0),构建 0 警告 0 错误。以下每条的期望值
都来自第三方工具对同一件产物/夹具的读数,不是本库自己说的:
| 对撞对象 | 期望值来源 | 读数 |
|---|---|---|
| ISO 21496-1 元数据逐字段 | Google libultrahdr v2.0.2 ultrahdr_app -m 1 -P -j |
7 组字段全对撞通过(maxContentBoost 5.86678、hdrCapacityMax 4.92611、useBaseColorSpace 0…;本库存 log2 分数、厂商报线性 ⇒ 过 2^x 换算后逐条相等) |
ISOBMFF 读侧(tmap 定位 + 载荷语义) |
libavif 1.4.2 avifdec --info |
avifdec 对该件报「含增益图 + Base Headroom 3.00 / Alternate Headroom 0.00」;本库探测同一件读出 HdrCapacityMin=3.00 / HdrCapacityMax=0.00(log2 域)逐字相符,并判出 Form=tmap。⚠ 口径:PrimaryItemId/TmapItemId/GainMapItemId 那组 id 是本库自证(互异且能取回载荷),avifdec 并不逐项打印 id ⇒ 别把它们当成第三方背书 |
| 写侧产物互通性 | libavif 1.4.2 avifdec --info |
本库 Build() 的产物被 avifdec 接受并读回:底图 9/16/9(正是请求的 CICP)、Gain map: 16x12 pixels … Base Headroom 3.00 / Alternate 0.00、Alternate image: Transfer Char. 16。⚠ 口径:这台机器上这份 libavif 没编 AV1 解码器(avifdec 另报 Failed to decode image: No codec available)⇒ 验到的是容器层被第三方接受并读对,不是第三方解出了像素 |
iref 的 reference_count 两种合法读法 |
对真实件做字节手术(删 count 并同步收缩 dimg/iref/meta 三层盒长) |
两种形态收敛到同一增益图 item(实测) |
| 负控 / 反退化 | 见 §5 | 11 个截断点不抛、JPEG 喂 BMFF 判否、无增益图片判否、只取通道 0 有反控 |
怎么复算这些第三方读数(两个工具都不在本仓,需自备同版本;--dump 会打印本库对同一件的读数、
盒框走查,并把 Build() 的产物写成 built_by_selftest.avif 供 avifdec 复核):
ultrahdr_app -m 1 -P -j fixtures/ref_ultrahdr_golden.jpg
avifdec --info fixtures/ref_libavif_colorgrid_gainmapgrid.avif
dotnet run --project tests/GainMapKit.SelfTest -c Release -- --dump变异验牙:6 处定向破坏(摘掉 min_version 门 / 通道 0 读数偏一 / useBaseColorSpace 恒真 /
BMFF magic 恒真 / writer 不再识别既有 colr / iref 判别法退化)⇒ 6/6 被抓,
各自打出点名断言;还原后 4 份源文件 SHA256 与基线逐位一致,复跑回到 78/0。
✅ 已支持并有外部对撞:Ultra HDR JPEG 读(APP2 + MPF + 双端序)、ISOBMFF 读
(AVIF tmap、item 定位、载荷取出)、AVIF 写(tmap + dimg + altr + colr 改写)。
⚠ 边界 1 — grid 型增益图项:本库「不定案」,而不是谎报「没有」。
fixtures/ref_libavif_solo_gainmapgrid.avif(增益图本身是网格项)实测:
Determinate=false、GainMapItemId=-1、原因 [IsoBmff] tmap 的 dimg 增益图候选数 = 0(应为 1),
而 tmap 载荷仍能取出(元数据不丢)。⇒ 消费方必须把 Determinate=false 当「未知」处理,
不能当「无增益图」处理。
⚠ 边界 2 — 标量只保留通道 0。Channels 字段如实报 3,但 GainMapMin/Max/Gamma/Offset*
是单值(取通道 0)。逐通道增益需要调用方自己从 TmapPayload / ISO 载荷再取 —— 本库不假装支持。
⚠ 边界 3 — 写侧不校验元数据自洽性。Build() 把 tmap 载荷当不透明字节。反例实测:
某通道 max < min 时本库照样产出容器,而 libavif avifdec 对同一件报
Invalid tone mapped image item + Per-channel max is less than per-channel min in gain map metadata。
⇒ 元数据合法性是调用方的责任。
❌ 不在范围内:像素级增益应用、JPEG 编码侧装配(宿主的 GainMapEncoder)、
HEIC 写侧(无实测件 ⇒ 未宣称)、XMP hdrgm 读取(在宿主 GainMapDecoder)。
另有 ST 428-1 / film 等色彩侧永久 fail-closed 项 —— 那是内核侧的边界,见 ColorMatrix.md §4。
dotnet build src/GainMapKit/GainMapKit.csproj -c Release
dotnet run --project tests/GainMapKit.SelfTest -c Release # 78 PASS / 0 FAIL,exit 0
dotnet run --project tests/GainMapKit.SelfTest -c Release -- --dump # 打夹具读数与盒框走查- 目标框架
net11.0。⚠ 我只实测了 net11.0(0 警告 0 错误);没有实测更低 TFM, 所以这里不声称更低框架可用。 - 自检刻意不用 xunit / NUnit(会引入 NuGet 依赖,破坏零依赖定位),是控制台程序 + 退出码,可直接接 CI。
- 库 csproj 开了
GenerateDocumentationFile,因此悬空的<see cref>会变成可见的 CS1574; 现在 0 警告 ⇒ 引用关系自洽(只豁免了CS1591公共成员缺注释这一类)。 - 夹具按文件名 + 尺寸 + SHA256 前缀取用,件不齐就 fail-closed 直接退出,不做「目录里第一个」的猜测。
复现第三方夹具的命令行见
THIRD-PARTY-NOTICES.md§3。
三条设计约束,都是踩过坑之后钉死的:
- 失败要点名:读不出来就写清「为什么」(
[IsoBmff] …/[GainMap-Avif] …), 空原因会被断言打回(自检里有专门一条查StartsWith("[GainMap-Avif]"))。 - 不猜容器:
iref的reference_count在规范里不存在、在 libavif 产物里存在, 本库两种读法都容忍并实测收敛(§3);判别法退化会在自检里红(M6)。 - 降级不是修复:定不了案就报
Determinate=false,绝不用「找不到 ⇒ 说它没有」糊过去(边界 1)。
- 来源:抽自 ffmpegPictureUI(GPL-3.0)。
切点:
GainMapDecoder.cs第 41-69 行 + 第 503-816 行(并把同类内:462的IsoNamespace常量随件带入)、IsoBmffGainMapProbe.cs整文件、IsoBmffGainMapWriter.cs整文件;锚点写在各文件头部注释里。 - 许可:MIT(
LICENSE)。第三方咨询与夹具数据的许可登记见THIRD-PARTY-NOTICES.md。 - 「唯一版权人」前提:被抽文件的全部提交出自
luoye-cpu一人 (git log --format='%an' -- <路径> | sort | uniq -c实测:GainMapDecoder.cs5 次、IsoBmffGainMapProbe.cs1 次,均为同一作者;IsoBmffGainMapWriter.cs是 2026-10-02 新写、尚未提交)。 ⇒ 无第三方贡献者需要另行授权,GPL-3.0 → MIT 的重授权在版权上成立。 - ⚠ 宿主仍是权威,本库是副本。本副本有三处既定差异,回灌时必须逐条映射:
① 命名空间
FfmpegGui.Services→GainMapKit; ②GainMapDecoder.JpegContainer提升为顶层JpegGainMapContainer(宿主侧 3 处 全限定访问GainMapDecoder.JpegContainer.因此不能直接替换); ③ 对外可见性提升(internal→public:IsoBmffGainMapWriter、BuildResult、HasTmapItemFast、MaxManagedParseBytes)。 宿主侧对被抽符号的引用实测为 36 行 / 5 个文件(GainMapDecoder.cs26、GainMapEncoder.cs4、tests/ServiceProbe/Program.cs3、CjxlService.cs2、QueueProcessor.cs1)⇒ 切引用(cutover) 是独立一批改动,本库发布不含它;两仓暂长期双份,改本层代码时两边都要动。
本库提供容器读写实现,不构成对 ISO 21496-1 / 14496-12 / 23008-12 / CIPA DC-X007
任何一条符合性的担保。接入关键产线前请用你自己的第三方工具(avifdec / ultrahdr_app /
真实素材)交叉验证 —— 本仓自检的外部对撞正是这么做的。