You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何通过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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.22 05:24:17