feenor
feenor

Reputation: 31

How do I convert an HDR screenshot to SDR using Direct2D effects?

I'm trying to convert screenshots taken on an HDR display via IDXGIOutput6.DuplicateOutput1() into SDR GDI+ bitmaps. The color space of the display is DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020 and the pixel format is DXGI_FORMAT_R16G16B16A16_FLOAT.

I can't find a series of Direct2D transforms that actually gives me a reasonable result though. I've tried many different combinations of effects and settings, but the output continues to either come out too bright, washed out, low contrast, or oversaturated.

The gist of what I'm doing is:

  1. Grab HDR frame from the output
  2. Bring it into a D2D bitmap
  3. Calculate max content light level
  4. Change color space to scRGB
  5. Apply HDR tone mapping
  6. Change color space to sRGB
  7. Use WIC to convert to GUID_WICPixelFormat32bppPBGRA
  8. Copy raw pixel data to a GDI+ bitmap with format PixelFormat.Format32bppPArgb

Currently I'm seeing the issue of the output being too bright, but I have no idea why. Any help in figuring out what the transform chain actually should be would be appreciated.

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 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(wicBmp, WICPixelFormat.Format32bppPBGRA,
                   WICBitmapDitherType.WICBitmapDitherTypeNone, null, 0.0, WICBitmapPaletteType.WICBitmapPaletteTypeCustom))
        {
            return ToGdiBitmap(formatConverter);
        }
    }
}

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;
}

Upvotes: 1

Views: 627

Answers (0)

Related Questions