3

I'm trying to scale down large images (~ 23k x 1k) to be displayed in winforms. The current way I'm scaling the images is taking too long, which is why I want to use the GPU through SharpDX (C#) to improve performance. What would be a good way to do this?

I'm working on a method to scale an image by applying the scale effect (that I don't have access to right now), but I still don't fully understand SharpDX, so I'm wondering if there's a better way to go about this. I modeled my code off of this example but I removed the text overlay, the image saving, the drawing portion, and I replaced the gaussian with the scaling effect. Since I'm using GDI to do the drawing for simplicity, the image is in the form of a systems drawing bitmap so I initialize the encoder with a memory stream that I use to get the output image after the scaling effect is applied. The smaller tests I have done with this method don't seem to make the scaling much quicker, but I haven't been able to put this fully in action yet.

Is there a quicker way to scale down an image using SharpDX, or is something along the lines of my current method the quickest?

Hawksm278
  • 31
  • 1
  • 2
    The most straightforward way is to use Direct2D Scale effect :https://learn.microsoft.com/en-us/windows/win32/direct2d/high-quality-scale Direct2D has also an interop story with GDI: https://learn.microsoft.com/en-us/windows/win32/direct2d/direct2d-and-gdi-interoperation-overview Depending how you do it, it may not be faster. For example, it's faster if you keep everything in GPU (=Direct2D DXGI render target, Direct Composition, etc.) but if you do a lot of CPU <=> GPU transfer (ie: if you keep GDI in the loop), this needs to be tested. – Simon Mourier Jun 05 '22 at 16:25

1 Answers1

0

Based on what I found on https://csharp.hotexamples.com/examples/SharpDX.WIC/WICStream/-/php-wicstream-class-examples.html

Looks like SharpDX about twice the performance of GDI or better.

Test code that works on my Windows 11 computer. Should be enough to get you started even if you know as little of SharpDX as I do.

var inputPath = @"x:\Temp\1\_Landscape.jpg";
var data = File.ReadAllBytes(inputPath);

var sw = Stopwatch.StartNew();
var iu6 = new ImageUtilities6();
Debug.WriteLine($"Init: {sw.ElapsedMilliseconds}ms total");

for (int i = 0; i < 10; i++)
{
    sw.Restart();
    var image = iu6.ResizeImage(data, 799, 399);
    Debug.WriteLine($"Resize: {sw.ElapsedMilliseconds}ms total");

    File.WriteAllBytes(@"X:\TEMP\1\007-xxx.jpg", image);
}

sw.Restart();
iu6.Dispose();
Debug.WriteLine($"Dispose: {sw.ElapsedMilliseconds}ms total");

Class I made based on the samples found on that page.

using SharpDX;
using dw = SharpDX.DirectWrite;
using d2 = SharpDX.Direct2D1;
using d3d = SharpDX.Direct3D11;
using dxgi = SharpDX.DXGI;
using wic = SharpDX.WIC;
using System;
using System.IO;
using SharpDX.Direct3D11;
using SharpDX.WIC;
using SharpDX.DirectWrite;

namespace SharpDX_ImageResizingTest
{
    public class ImageUtilities6 : IDisposable
    {
        private Device defaultDevice;
        private Device1 d3dDevice;
        private dxgi.Device dxgiDevice;
        private d2.Device d2dDevice;
        private ImagingFactory2 imagingFactory;
        //private d2.DeviceContext d2dContext;
        private Factory dwFactory;
        private d2.PixelFormat d2PixelFormat;

        public ImageUtilities6()
        {
            //SharpDX.Configuration.EnableObjectTracking = true; //Turn on memory leak logging

            // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
            // INITIALIZATION ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

            // initialize the D3D device which will allow to render to image any graphics - 3D or 2D
            defaultDevice = new SharpDX.Direct3D11.Device(SharpDX.Direct3D.DriverType.Hardware,
                                                              d3d.DeviceCreationFlags.VideoSupport
                                                              | d3d.DeviceCreationFlags.BgraSupport
                                                              | d3d.DeviceCreationFlags.Debug); // take out the Debug flag for better performance

            d3dDevice = defaultDevice.QueryInterface<d3d.Device1>(); // get a reference to the Direct3D 11.1 device
            dxgiDevice = d3dDevice.QueryInterface<dxgi.Device>(); // get a reference to DXGI device
            //var dxgiSurface = d3dDevice.QueryInterface<dxgi.Surface>(); // get a reference to DXGI surface

            d2dDevice = new d2.Device(dxgiDevice); // initialize the D2D device

            imagingFactory = new wic.ImagingFactory2(); // initialize the WIC factory


            dwFactory = new dw.Factory();

            // specify a pixel format that is supported by both D2D and WIC
            d2PixelFormat = new d2.PixelFormat(dxgi.Format.R8G8B8A8_UNorm, d2.AlphaMode.Premultiplied);
            // if in D2D was specified an R-G-B-A format - use the same for wic
        }

        public byte[] ResizeImage(byte[] image, int targetWidth, int targetHeight)
        {
            int dpi = 72; //96? does it even matter

            var wicPixelFormat = wic.PixelFormat.Format32bppPRGBA;

            // initialize the DeviceContext - it will be the D2D render target and will allow all rendering operations
            var d2dContext = new d2.DeviceContext(d2dDevice, d2.DeviceContextOptions.None);


            // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
            // IMAGE LOADING ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
            var imageStream = new MemoryStream(image);
            //var decoder = new wic.PngBitmapDecoder(imagingFactory); // we will load a PNG image
            var decoder = new wic.JpegBitmapDecoder(imagingFactory); // we will load a JPG image
            var inputStream = new wic.WICStream(imagingFactory, imageStream); // open the image for reading
            decoder.Initialize(inputStream, wic.DecodeOptions.CacheOnLoad);

            // decode the loaded image to a format that can be consumed by D2D
            var formatConverter = new wic.FormatConverter(imagingFactory);
            var frame = decoder.GetFrame(0);
            formatConverter.Initialize(frame, wicPixelFormat);

            // load the base image into a D2D Bitmap
            var inputBitmap = d2.Bitmap1.FromWicBitmap(d2dContext, formatConverter, new d2.BitmapProperties1(d2PixelFormat));

            // store the image size - output will be of the same size
            var inputImageSize = formatConverter.Size;
            var pixelWidth = inputImageSize.Width;
            var pixelHeight = inputImageSize.Height;


            // Calculate correct aspect ratio
            double aspectRatio = (double)pixelHeight / (double)pixelWidth;
            double targetAspectRatio = (double)targetHeight / (double)targetWidth;
            if (targetAspectRatio > aspectRatio)
            {
                targetHeight = (int)(targetHeight * (aspectRatio / targetAspectRatio));
            }
            else
            {
                targetWidth = (int)(targetWidth * (targetAspectRatio / aspectRatio));
            }

            // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
            // EFFECT SETUP ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

            //Effect 1 : BitmapSource - take decoded image data and get a BitmapSource from it
            //var bitmapSourceEffect = new d2.Effects.BitmapSource(d2dContext);
            //bitmapSourceEffect.WicBitmapSource = formatConverter;

            // Effect 2 : GaussianBlur - give the bitmapsource a gaussian blurred effect
            //var gaussianBlurEffect = new d2.Effects.GaussianBlur(d2dContext);
            //gaussianBlurEffect.SetInput(0, bitmapSourceEffect.Output, true);
            //gaussianBlurEffect.StandardDeviation = 5f;

            // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
            // RENDER TARGET SETUP ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

            // create the d2d bitmap description using default flags (from SharpDX samples) and 96 DPI
            var d2dBitmapProps = new d2.BitmapProperties1(d2PixelFormat, 96, 96, d2.BitmapOptions.Target | d2.BitmapOptions.CannotDraw);

            // the render target
            var d2dRenderTarget = new d2.Bitmap1(d2dContext, new Size2(targetWidth, targetHeight), d2dBitmapProps);
            d2dContext.Target = d2dRenderTarget; // associate bitmap with the d2d context

            d2dContext.BeginDraw();

            //d2dContext.DrawImage(bitmapSourceEffect); //Way #1
            //d2dContext.DrawImage(gaussianBlurEffect); //Way #2
            //d2dContext.DrawBitmap(inputBitmap, 1, d2.InterpolationMode.Linear); //Way #3
            d2dContext.DrawBitmap(inputBitmap, new SharpDX.Mathematics.Interop.RawRectangleF(0, 0, targetWidth, targetHeight), 1, d2.InterpolationMode.Linear, null, null); //Way #4 - resizing

            d2dContext.EndDraw();


            // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
            // IMAGE SAVING ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

            // delete the output file if it already exists
            //if (System.IO.File.Exists(outputPath)) System.IO.File.Delete(outputPath);

            // use the appropiate overload to write either to stream or to a file
            var outputStream = new MemoryStream();
            var stream = new wic.WICStream(imagingFactory, outputStream);

            // select the image encoding format HERE
            var encoder = new wic.JpegBitmapEncoder(imagingFactory);
            encoder.Initialize(stream);

            var bitmapFrameEncode = new wic.BitmapFrameEncode(encoder);
            bitmapFrameEncode.Options.ImageQuality = 0.95f;
            bitmapFrameEncode.Initialize();
            bitmapFrameEncode.SetSize(targetWidth, targetHeight);
            bitmapFrameEncode.SetPixelFormat(ref wicPixelFormat);

            // this is the trick to write D2D1 bitmap to WIC
            var imageEncoder = new wic.ImageEncoder(imagingFactory, d2dDevice);
            imageEncoder.WriteFrame(d2dRenderTarget, bitmapFrameEncode, new wic.ImageParameters(d2PixelFormat, dpi, dpi, 0, 0, targetWidth, targetHeight));

            bitmapFrameEncode.Commit();
            encoder.Commit();

            imageEncoder.Dispose();

            bitmapFrameEncode.Dispose();
            encoder.Dispose();
            stream.Dispose();
            formatConverter.Dispose();
            d2dRenderTarget.Dispose();
            inputStream.Dispose();
            decoder.Dispose();
            inputBitmap.Dispose();
            frame.Dispose();
            d2dContext.Dispose();

            return outputStream.ToArray();
        }

        public void Dispose()
        {
            //bitmapSourceEffect.Dispose();

            dwFactory.Dispose();
            imagingFactory.Dispose();
            d2dDevice.Dispose();
            dxgiDevice.Dispose();
            d3dDevice.Dispose();
            defaultDevice.Dispose();

            //System.Diagnostics.Debug.WriteLine(SharpDX.Diagnostics.ObjectTracker.ReportActiveObjects()); Log that memory leak
        }


        public byte[] ResizeImage1(byte[] data, int width, int height)
        {
            var ms = new MemoryStream(data);
            //Image image = Image.FromStream(ms);
            System.Drawing.Image image = System.Drawing.Image.FromStream(ms, false, false);

            System.Drawing.Bitmap result = new System.Drawing.Bitmap(width, height);
            // set the resolutions the same to avoid cropping due to resolution differences
            result.SetResolution(image.HorizontalResolution, image.VerticalResolution);

            //use a graphics object to draw the resized image into the bitmap
            using (System.Drawing.Graphics graphics = System.Drawing.Graphics.FromImage(result))
            {
                //set the resize quality modes to high quality
                graphics.CompositingQuality = System.Drawing.Drawing2D.CompositingQuality.HighQuality;
                graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
                graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
                //draw the image into the target bitmap
                graphics.DrawImage(image, 0, 0, result.Width, result.Height);
            }

            var stream = new System.IO.MemoryStream();
            image.Save(stream, System.Drawing.Imaging.ImageFormat.Jpeg);
            stream.Position = 0;
            return stream.ToArray();
        }
    }
}

Library used was SharpDX + .Direct2D1 + .Direct3D11 + .DXGI 4.2.

Wolf5
  • 16,600
  • 12
  • 59
  • 58