如何通过Direct2D特效将HDR截图转换为SDR格式?
HDR转SDR GDI+位图的转换问题及修正方案
我通过IDXGIOutput6.DuplicateOutput1()获取HDR显示器截图,显示器色彩空间为DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020,像素格式DXGI_FORMAT_R16G16B16A16_FLOAT,尝试转成SDR GDI+位图时始终得不到合理结果,输出画面要么过亮、褪色、对比度低,要么色彩过饱和。
当前操作流程:
- 从输出设备获取HDR帧
- 导入D2D位图
- 计算最大内容亮度(Max CLL)
- 色彩空间转换为scRGB
- 应用HDR色调映射
- 色彩空间转换为sRGB
- 用WIC转换为
GUID_WICPixelFormat32bppPBGRA格式 - 复制像素数据到
PixelFormat.Format32bppPArgb的GDI+位图
目前核心问题是输出画面过亮,以下是问题分析及修正方案:
原实现代码
private const float NominalRefWhite = 80.0f; private const int HistNumBins = 400; private const float HistGamma = 0.1f; private const int HistMaxNits = 1000000; private const float OutputNits = 80.0f; private Bitmap CaptureDxgiOutputFrame(IComObject<IDXGIOutput6> output, IComObject<ID3D11Device> device, IComObject<ID3D11DeviceContext> deviceCtx) { output.Object.GetDesc1(out var outputDesc).ThrowOnError(); Debug.WriteLine($"CaptureDxgiOutputFrame - OutputDesc: {ToDebugString(outputDesc)}"); var (frameInfo, frameTex) = DuplicateOutputFrame(output, device, deviceCtx); using (var d2dDev = D2DFactoryInstance.Object.CreateDevice<ID2D1Device6>(device.As<IDXGIDevice>(true))) using (var d2dCtx = d2dDev.Object.CreateDeviceContext<ID2D1DeviceContext6>()) { var frameSurface = frameTex.AsNonOwned<IDXGISurface>(); frameTex.Object.GetDesc(out var frameDesc); Debug.WriteLine($"CaptureDxgiOutputFrame - FrameDesc: {ToDebugString(frameDesc)}"); using (var inputColorContext = d2dCtx.CreateColorContextFromDxgiColorSpace(outputDesc.ColorSpace)) using (var scRgbColorContext = d2dCtx.CreateColorContext(D2D1_COLOR_SPACE.D2D1_COLOR_SPACE_SCRGB)) using (var sRgbColorContext = d2dCtx.CreateColorContext(D2D1_COLOR_SPACE.D2D1_COLOR_SPACE_SRGB)) using (var frameBmp = d2dCtx.CreateBitmapFromDxgiSurface(frameSurface, frameDesc.Format, inputColorContext)) using (var targetBmp = d2dCtx.CreateBitmap(frameDesc.Width, frameDesc.Height, DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_FLOAT, sRgbColorContext, D2D1_BITMAP_OPTIONS.D2D1_BITMAP_OPTIONS_TARGET | D2D1_BITMAP_OPTIONS.D2D1_BITMAP_OPTIONS_CANNOT_DRAW)) using (var inputToScRgbEffect = new D2DColorManagementEffect(d2dCtx)) using (var scRgbToSRgbEffect = new D2DColorManagementEffect(d2dCtx)) using (var hdrToneMapEffect = new D2DHdrToneMapEffect(d2dCtx)) { d2dCtx.SetTarget(targetBmp); var maxCll = CalculateMaxCll(d2dCtx, frameBmp); Debug.WriteLine($"CaptureDxgiOutputFrame - Max CLL: {maxCll}"); inputToScRgbEffect.Quality = D2D1_COLORMANAGEMENT_QUALITY.D2D1_COLORMANAGEMENT_QUALITY_BEST; inputToScRgbEffect.InputColorContext = inputColorContext; inputToScRgbEffect.OutputColorContext = scRgbColorContext; scRgbToSRgbEffect.Quality = D2D1_COLORMANAGEMENT_QUALITY.D2D1_COLORMANAGEMENT_QUALITY_BEST; scRgbToSRgbEffect.InputColorContext = scRgbColorContext; scRgbToSRgbEffect.OutputColorContext = sRgbColorContext; hdrToneMapEffect.InputMaxLuminance = maxCll; hdrToneMapEffect.OutputMaxLuminance = OutputNits; hdrToneMapEffect.DisplayMode = D2D1_HDRTONEMAP_DISPLAY_MODE.D2D1_HDRTONEMAP_DISPLAY_MODE_SDR; inputToScRgbEffect.SetInput(frameBmp); hdrToneMapEffect.SetInput(inputToScRgbEffect); scRgbToSRgbEffect.SetInput(hdrToneMapEffect); d2dCtx.BeginDraw(); d2dCtx.DrawImage(scRgbToSRgbEffect.Effect); d2dCtx.EndDraw(); var gdiBmp = To32BppGdiBitmap(d2dCtx, targetBmp); gdiBmp.Save(DebugDir + "gdi.bmp", ImageFormat.Bmp); return gdiBmp; } } } private static Bitmap To32BppGdiBitmap(IComObject<ID2D1DeviceContext6> d2dCtx, IComObject<ID2D1Bitmap> srcBmp) { var srcSize = srcBmp.GetSize(); var srcD2DPixelFormat = srcBmp.GetPixelFormat(); Debug.WriteLine($"To32BppGdiBitmap - Source D2D format: {Enum.GetName(typeof(DXGI_FORMAT), srcD2DPixelFormat.format)}, {Enum.GetName(typeof(D2D1_ALPHA_MODE), srcD2DPixelFormat.alphaMode)}"); using (var colorContext = d2dCtx.CreateColorContext(D2D1_COLOR_SPACE.D2D1_COLOR_SPACE_SRGB)) using (var srcBmpCpu = d2dCtx.CreateBitmap((uint)srcSize.width, (uint)srcSize.height, srcD2DPixelFormat.format, colorContext, D2D1_BITMAP_OPTIONS.D2D1_BITMAP_OPTIONS_CPU_READ | D2D1_BITMAP_OPTIONS.D2D1_BITMAP_OPTIONS_CANNOT_DRAW, srcD2DPixelFormat.alphaMode)) { srcBmpCpu.Object.CopyFromBitmap(IntPtr.Zero, srcBmp.Object, IntPtr.Zero).ThrowOnError(); var srcWicPixelFormat = D2DPixelFormatToWicPixelFormat(srcD2DPixelFormat); Debug.WriteLine($"To32BppGdiBitmap - Source WIC format: {WICPixelFormat.GuidToName[srcWicPixelFormat]}"); using (var mappedData = srcBmpCpu.Map(D2D1_MAP_OPTIONS.D2D1_MAP_OPTIONS_READ)) using (var wicBmp = WicFactoryInstance.CreateBitmapFromMemory((uint)srcSize.width, (uint)srcSize.height, srcWicPixelFormat, mappedData.Pitch, (int)mappedData.Pitch * (int)srcSize.height, mappedData.Bits)) using (var formatConverter = WicFactoryInstance.CreateFormatConverter()) { var targetWicPixelFormat = WICPixelFormat.Format32bppPBGRA; formatConverter.Object.Initialize(wicBmp.Object, ref targetWicPixelFormat, WICBitmapDitherType.WICBitmapDitherTypeNone, null, 0.0, WICBitmapPaletteType.WICBitmapPaletteTypeCustom); return ToGdiBitmap(formatConverter); } } } private static float CalculateMaxCll(IComObject<ID2D1DeviceContext6> d2dCtx, IComObject<ID2D1Bitmap> bmp) { using (var histogramMatrixEffect = new D2DColorMatrixEffect(d2dCtx)) using (var histogramGammaEffect = new D2DGammaTransferEffect(d2dCtx)) using (var histogramEffect = new D2DHistogramEffect(d2dCtx)) { var scale = HistMaxNits / NominalRefWhite; histogramMatrixEffect.ColorMatrix = new D2D_MATRIX_5X4_F { _11 = 0.2126f / scale, _21 = 0.7152f / scale, _31 = 0.0722f / scale, _44 = 1.0f, }; histogramGammaEffect.RedExponent = HistGamma; histogramGammaEffect.GreenDisable = true; histogramGammaEffect.BlueDisable = true; histogramGammaEffect.AlphaDisable = true; histogramEffect.NumBins = HistNumBins; histogramMatrixEffect.SetInput(bmp); histogramGammaEffect.SetInput(histogramMatrixEffect); histogramEffect.SetInput(histogramGammaEffect); d2dCtx.BeginDraw(); d2dCtx.DrawImage(histogramEffect.Effect); d2dCtx.EndDraw(); var buckets = histogramEffect.ReadOutput(); int maxCllBin = 0; var runningSum = 0.0; for (int i = buckets.Length - 1; i >= 0; i--) { runningSum += buckets[i]; maxCllBin = i; if (runningSum >= 1.0 - 0.9999) break; } var binNorm = (double)maxCllBin / HistNumBins; var maxCll = Math.Pow(binNorm, 1 / HistGamma) * HistMaxNits; return (float)maxCll; } } private static Bitmap ToGdiBitmap(IComObject<IWICBitmapSource> srcBmp) { if (srcBmp.GetPixelFormat() != WICPixelFormat.Format32bppPBGRA) throw new ArgumentException("Input must be 32bpp BGRA with pre-multiplied alpha."); var sz = srcBmp.GetSize(); var stride = (int)sz.width * 4; var gdiBmp = new Bitmap((int)sz.width, (int)sz.height, PixelFormat.Format32bppPArgb); var buf = new byte[(int)sz.height * stride]; srcBmp.Object.CopyPixels(IntPtr.Zero, (uint)stride, buf.Length, buf).ThrowOnError(); var gdiBmpData = gdiBmp.LockBits(new Rectangle(0, 0, (int)sz.width, (int)sz.height), ImageLockMode.WriteOnly, PixelFormat.Format32bppPArgb); try { unsafe { fixed (byte* src = buf) { Buffer.MemoryCopy(src, (void*)gdiBmpData.Scan0, buf.Length, buf.Length); } } } finally { gdiBmp.UnlockBits(gdiBmpData); } return gdiBmp; }
问题分析与修正方案
1. 冗余色彩空间转换导致亮度偏差
原流程先将P2020/ST.2084转scRGB,再做色调映射,最后转sRGB。实际上D2DHdrToneMapEffect支持直接输入ST.2084色彩空间的位图,无需中间转scRGB,冗余转换会引入不必要的gamma计算误差。
修正后流程:
原始HDR位图 → HDR色调映射(直接处理ST.2084) → 色彩空间转sRGB → WIC格式转换 → GDI+位图
2. Max CLL计算逻辑错误
原计算未正确解码ST.2084的浮点值到线性亮度,直接用矩阵缩放导致亮度计算偏小,使色调映射的InputMaxLuminance低于实际值,画面被过度提亮。
修正后的CalculateMaxCll方法:
private static float CalculateMaxCll(IComObject<ID2D1DeviceContext6> d2dCtx, IComObject<ID2D1Bitmap> bmp) { using (var colorSpaceConvert = new D2DColorManagementEffect(d2dCtx)) using (var histogramMatrixEffect = new D2DColorMatrixEffect(d2dCtx)) using (var histogramEffect = new D2DHistogramEffect(d2dCtx)) { // 先将ST.2084转成线性scRGB(无gamma) var inputColorContext = d2dCtx.CreateColorContextFromDxgiColorSpace(DXGI_COLOR_SPACE_TYPE.DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020); var linearScRgbContext = d2dCtx.CreateColorContext(D2D1_COLOR_SPACE.D2D1_COLOR_SPACE_SCRGB_LINEAR); colorSpaceConvert.InputColorContext = inputColorContext; colorSpaceConvert.OutputColorContext = linearScRgbContext; colorSpaceConvert.Quality = D2D1_COLORMANAGEMENT_QUALITY.D2D1_COLORMANAGEMENT_QUALITY_BEST; // 转成亮度值(Nits) histogramMatrixEffect.ColorMatrix = new D2D_MATRIX_5X4_F { _11 = 0.2126f * 1000.0f, // scRGB线性值转Nits(scRGB 1.0 = 1000 Nits) _21 = 0.7152f * 1000.0f, _31 = 0.0722f * 1000.0f, _44 = 1.0f, }; histogramEffect.NumBins = HistNumBins; histogramEffect.MaxValue = HistMaxNits; colorSpaceConvert.SetInput(bmp); histogramMatrixEffect.SetInput(colorSpaceConvert); histogramEffect.SetInput(histogramMatrixEffect); d2dCtx.BeginDraw(); d2dCtx.DrawImage(histogramEffect.Effect); d2dCtx.EndDraw(); var buckets = histogramEffect.ReadOutput(); int maxCllBin = 0; var runningSum = 0.0; // 从高亮度桶开始累加,找到包含99.99%像素的最高亮度 for (int i = buckets.Length - 1; i >= 0; i--) { runningSum += buckets[i]; maxCllBin = i; if (runningSum >= 0.9999) break; } // 计算实际亮度值 var maxCll = ((float)maxCllBin / HistNumBins) * HistMaxNits; return maxCll; } }
3. 目标位图色彩空间设置错误
原目标位图绑定sRGB色彩空间,而色调映射后的输出是线性或半处理的色彩空间,D2D会自动应用gamma转换,导致画面过亮。
修正:创建目标位图时使用scRGB线性色彩空间,完成所有特效处理后再转sRGB:
// 修正目标位图创建 using (var targetBmp = d2dCtx.CreateBitmap(frameDesc.Width, frameDesc.Height, DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_FLOAT, d2dCtx.CreateColorContext(D2D1_COLOR_SPACE.D2D1_COLOR_SPACE_SCRGB_LINEAR), D2D1_BITMAP_OPTIONS.D2D1_BITMAP_OPTIONS_TARGET | D2D1_BITMAP_OPTIONS.D2D1_BITMAP_OPTIONS_CANNOT_DRAW))
4. 简化特效链
去掉冗余的scRGB转换,直接将原始HDR位图输入色调映射特效:
// 修正特效链 hdrToneMapEffect.InputMaxLuminance = maxCll; hdrToneMapEffect.OutputMaxLuminance = OutputNits; hdrToneMapEffect.DisplayMode = D2D1_HDRTONEMAP_DISPLAY_MODE.D2D1_HDRTONEMAP_DISPLAY_MODE_SDR; hdrToneMapEffect.InputColorContext = inputColorContext; // 指定输入色彩空间为ST.2084 scRgbToSRgbEffect.InputColorContext = d2dCtx.CreateColorContext(D2D1_COLOR_SPACE.D2D1_COLOR_SPACE_SCRGB); scRgbToSRgbEffect.OutputColorContext = sRgbColorContext; // 特效链:原始位图 → 色调映射 → sRGB转换 hdrToneMapEffect.SetInput(frameBmp); scRgbToSRgbEffect.SetInput(hdrToneMapEffect);
内容的提问来源于stack exchange,提问作者feenor
相关产品推荐
相关产品推荐

