在 C# 中调整大的奇数比例图像大小

Large, odd ratio image resize in C#

我有一个特殊的问题需要帮助。我正在处理复杂的蛋白质组学数据,我们的一个绘图涉及原始数据的热图。我将这些热图计算为原始图像,然后调整大小以适合我的图表 canvas。以这种方式生成的图像文件在宽度与高度方面通常非常不平衡。 通常,这些图像大约有 10 到 100 像素宽和 5000 到 8000 像素高(这是我必须转换成图像的原始 2D 数据阵列的大小)。之后的目标分辨率将是 1300 x 600 像素。

我通常使用此功能将图像调整为目标尺寸

public static Image Resize(Image img, int width, int height) {
   Bitmap bmp = new Bitmap(width, height);
   Graphics graphic = Graphics.FromImage((Image)bmp);
   graphic.InterpolationMode = InterpolationMode.NearestNeighbor;
   graphic.PixelOffsetMode = PixelOffsetMode.Half;


   graphic.DrawImage(img, 0, 0, width, height);
   graphic.Dispose();

   return (Image)bmp;
}

这通常适用于上述维度。但现在我有一个新的数据集,其尺寸为 6 x 54343 像素。 在此图像上使用相同的代码时,调整大小后的图像是半空白的。

原图: http://files.biognosys.ch/FileSharing/20170427_Whosebug/raw.png

(原始图像在大多数浏览器中无法正确显示,因此使用 "save link as...")

外观应该如何(使用 photoshop): http://files.biognosys.ch/FileSharing/20170427_Whosebug/photoshop_resize.png

我使用上面截断的代码时的样子 http://files.biognosys.ch/FileSharing/20170427_Whosebug/code_resized.png

请记住,这对于 6 x 8000 的图像已经工作多年没有问题,所以我想我在这里没有做任何根本性的错误。 同样重要的是,我有用于调整大小的 NearestNeighbor 插值,因此任何涉及不会导致 "How it should look" 图像的其他插值的解决方案最终对我没有用。

奥利

您似乎遇到了 16 位 Windows 时代的一些遗留限制。解决它的明显方法是仅使用内存操作将源图像预拆分为更小的块,而不是使用 Graphics 调整大小来应用所有这些块。此方法假定您的源图像是 Bitmap 而不仅仅是 Image 但这似乎不是您的限制。这是代码的草图:

[DllImport("kernel32.dll", EntryPoint = "CopyMemory", SetLastError = true)]
public static extern void CopyMemoryUnmanaged(IntPtr dest, IntPtr src, int count);

// in case you can't use P/Invoke, copy via intermediate .Net buffer        
static void CopyMemoryNet(IntPtr dst, IntPtr src, int count)
{
    byte[] buffer = new byte[count];
    Marshal.Copy(src, buffer, 0, count);
    Marshal.Copy(buffer, 0, dst, count);
}

static Image CopyImagePart(Bitmap srcImg, int startH, int endH)
{
    var width = srcImg.Width;
    var height = endH - startH;
    var srcBitmapData = srcImg.LockBits(new Rectangle(0, startH, width, height), ImageLockMode.ReadOnly, srcImg.PixelFormat);

    var dstImg = new Bitmap(width, height, srcImg.PixelFormat);
    var dstBitmapData = dstImg.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.ReadWrite, srcImg.PixelFormat);

    int bytesCount = Math.Abs(srcBitmapData.Stride) * height;
    CopyMemoryUnmanaged(dstBitmapData.Scan0, srcBitmapData.Scan0, bytesCount);
    // in case you can't use P/Invoke, copy via intermediate .Net buffer        
    //CopyMemoryNet(dstBitmapData.Scan0, srcBitmapData.Scan0, bytesCount);

    srcImg.UnlockBits(srcBitmapData);
    dstImg.UnlockBits(dstBitmapData);

    return dstImg;
}


public static Image ResizeInParts(Bitmap srcBmp, int width, int height)
{
    int srcStep = srcBmp.Height;
    int dstStep = height;
    while (srcStep > 30000)
    {
        srcStep /= 2;
        dstStep /= 2;
    }

    var resBmp = new Bitmap(width, height);
    using (Graphics graphic = Graphics.FromImage(resBmp))
    {
        graphic.InterpolationMode = InterpolationMode.NearestNeighbor;
        graphic.PixelOffsetMode = PixelOffsetMode.Half;


        for (int srcTop = 0, dstTop = 0; srcTop < srcBmp.Height; srcTop += srcStep, dstTop += dstStep)
        {
            int srcBottom = srcTop + srcStep;
            int dstH = dstStep;
            if (srcBottom > srcBmp.Height)
            {
                srcBottom = srcBmp.Height;
                dstH = height - dstTop;
            }
            using (var imgPart = CopyImagePart(srcBmp, srcTop, srcBottom))
            {
                graphic.DrawImage(imgPart, 0, dstTop, width, dstH);
            }
        }
    }

    return resBmp;
}

这是我为您的示例图片得到的结果:

它与您的 photoshop_resize.png 不同,但与您的 code_resized.png

非常相似

可以改进此代码以更好地处理各种 "edges" 例如 srcBmp.Height 不均匀或不同部分之间的边缘的情况(边缘上的像素仅使用它们的一半像素进行插值)应该是)但是如果不假设源图像和调整大小的图像都具有 "good" 大小或自己重新实现插值逻辑,这并不容易。考虑到您的比例因子,这段代码可能已经足够适合您的使用了。

这是一个似乎有效的解决方案。它基于 Windows WIC ("Windows Imaging Component")。它是 Windows(和 WPF)用于所有成像操作的本机组件。

我已经为它提供了一个小的 .NET 互操作层。它不具备 WIC 的所有功能,但可以让您 load/scale/save file/stream 图像。 Scale 方法有一个类似于 GDI+ 的缩放选项。

尽管结果并不严格等同于 photoshop 的结果,但您的示例似乎可以正常工作。您可以这样使用它:

using (var bmp = WicBitmapSource.Load("input.png"))
{
    bmp.Scale(1357, 584, WicBitmapInterpolationMode.NearestNeighbor);
    bmp.Save("output.png");
}

...

public enum WicBitmapInterpolationMode
{
    NearestNeighbor = 0,
    Linear = 1,
    Cubic = 2,
    Fant = 3,
    HighQualityCubic = 4,
}

public sealed class WicBitmapSource : IDisposable
{
    private IWICBitmapSource _source;

    private WicBitmapSource(IWICBitmapSource source, Guid format)
    {
        _source = source;
        Format = format;
        Stats();
    }

    public Guid Format { get; }
    public int Width { get; private set; }
    public int Height { get; private set; }
    public double DpiX { get; private set; }
    public double DpiY { get; private set; }

    private void Stats()
    {
        if (_source == null)
        {
            Width = 0;
            Height = 0;
            DpiX = 0;
            DpiY = 0;
            return;
        }

        int w, h;
        _source.GetSize(out w, out h);
        Width = w;
        Height = h;

        double dpix, dpiy;
        _source.GetResolution(out dpix, out dpiy);
        DpiX = dpix;
        DpiY = dpiy;
    }

    private void CheckDisposed()
    {
        if (_source == null)
            throw new ObjectDisposedException(null);
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    ~WicBitmapSource()
    {
        Dispose(false);
    }

    private void Dispose(bool disposing)
    {
        if (_source != null)
        {
            Marshal.ReleaseComObject(_source);
            _source = null;
        }
    }

    public void Save(string filePath)
    {
        Save(filePath, Format, Guid.Empty);
    }

    public void Save(string filePath, Guid pixelFormat)
    {
        Save(filePath, Format, pixelFormat);
    }

    public void Save(string filePath, Guid encoderFormat, Guid pixelFormat)
    {
        if (filePath == null)
            throw new ArgumentNullException(nameof(filePath));

        if (encoderFormat == Guid.Empty)
        {
            string ext = Path.GetExtension(filePath).ToLowerInvariant();
            // we support only png & jpg
            if (ext == ".png")
            {
                encoderFormat = new Guid(0x1b7cfaf4, 0x713f, 0x473c, 0xbb, 0xcd, 0x61, 0x37, 0x42, 0x5f, 0xae, 0xaf);
            }
            else if (ext == ".jpeg" || ext == ".jpe" || ext == ".jpg" || ext == ".jfif" || ext == ".exif")
            {
                encoderFormat = new Guid(0x19e4a5aa, 0x5662, 0x4fc5, 0xa0, 0xc0, 0x17, 0x58, 0x02, 0x8e, 0x10, 0x57);
            }
        }

        if (encoderFormat == Guid.Empty)
            throw new ArgumentException();

        using (var file = File.OpenWrite(filePath))
        {
            Save(file, encoderFormat, pixelFormat);
        }
    }

    public void Save(Stream stream)
    {
        Save(stream, Format, Guid.Empty);
    }

    public void Save(Stream stream, Guid pixelFormat)
    {
        Save(stream, Format, pixelFormat);
    }

    public void Save(Stream stream, Guid encoderFormat, Guid pixelFormat)
    {
        if (stream == null)
            throw new ArgumentNullException(nameof(stream));

        CheckDisposed();
        Save(_source, stream, encoderFormat, pixelFormat, WICBitmapEncoderCacheOption.WICBitmapEncoderNoCache, null);
    }

    public void Scale(int? width, int? height, WicBitmapInterpolationMode mode)
    {
        if (!width.HasValue && !height.HasValue)
            throw new ArgumentException();

        int neww;
        int newh;
        if (width.HasValue && height.HasValue)
        {
            neww = width.Value;
            newh = height.Value;
        }
        else
        {
            int w = Width;
            int h = Height;
            if (w == 0 || h == 0)
                return;

            if (width.HasValue)
            {
                neww = width.Value;
                newh = (width.Value * h) / w;
            }
            else
            {
                newh = height.Value;
                neww = (height.Value * w) / h;
            }
        }

        if (neww <= 0 || newh <= 0)
            throw new ArgumentException();

        CheckDisposed();
        _source = Scale(_source, neww, newh, mode);
        Stats();
    }

    // we support only 1-framed files (unlike TIF for example)
    public static WicBitmapSource Load(string filePath)
    {
        if (filePath == null)
            throw new ArgumentNullException(nameof(filePath));

        return LoadBitmapSource(filePath, 0, WICDecodeOptions.WICDecodeMetadataCacheOnDemand);
    }

    public static WicBitmapSource Load(Stream stream)
    {
        if (stream == null)
            throw new ArgumentNullException(nameof(stream));

        return LoadBitmapSource(stream, 0, WICDecodeOptions.WICDecodeMetadataCacheOnDemand);
    }

    private static WicBitmapSource LoadBitmapSource(string filePath, int frameIndex, WICDecodeOptions metadataOptions)
    {
        var wfac = (IWICImagingFactory)new WICImagingFactory();
        IWICBitmapDecoder decoder = null;
        try
        {
            decoder = wfac.CreateDecoderFromFilename(filePath, null, GenericAccessRights.GENERIC_READ, metadataOptions);
            return new WicBitmapSource(decoder.GetFrame(frameIndex), decoder.GetContainerFormat());
        }
        finally
        {
            Release(decoder);
            Release(wfac);
        }
    }

    private static WicBitmapSource LoadBitmapSource(Stream stream, int frameIndex, WICDecodeOptions metadataOptions)
    {
        var wfac = (IWICImagingFactory)new WICImagingFactory();
        IWICBitmapDecoder decoder = null;
        try
        {
            decoder = wfac.CreateDecoderFromStream(new ManagedIStream(stream), null, metadataOptions);
            return new WicBitmapSource(decoder.GetFrame(frameIndex), decoder.GetContainerFormat());
        }
        finally
        {
            Release(decoder);
            Release(wfac);
        }
    }

    private static IWICBitmapScaler Scale(IWICBitmapSource source, int width, int height, WicBitmapInterpolationMode mode)
    {
        var wfac = (IWICImagingFactory)new WICImagingFactory();
        IWICBitmapScaler scaler = null;
        try
        {
            scaler = wfac.CreateBitmapScaler();
            scaler.Initialize(source, width, height, mode);
            Marshal.ReleaseComObject(source);
            return scaler;
        }
        finally
        {
            Release(wfac);
        }
    }

    private static void Save(IWICBitmapSource source, Stream stream, Guid containerFormat, Guid pixelFormat, WICBitmapEncoderCacheOption cacheOptions, WICRect rect)
    {
        var wfac = (IWICImagingFactory)new WICImagingFactory();
        IWICBitmapEncoder encoder = null;
        IWICBitmapFrameEncode frame = null;
        try
        {
            encoder = wfac.CreateEncoder(containerFormat, null);
            encoder.Initialize(new ManagedIStream(stream), cacheOptions);
            encoder.CreateNewFrame(out frame, IntPtr.Zero);
            frame.Initialize(IntPtr.Zero);

            if (pixelFormat != Guid.Empty)
            {
                frame.SetPixelFormat(pixelFormat);
            }

            frame.WriteSource(source, rect);
            frame.Commit();
            encoder.Commit();
        }
        finally
        {
            Release(frame);
            Release(encoder);
            Release(wfac);
        }
    }

    private static void Release(object obj)
    {
        if (obj != null)
        {
            Marshal.ReleaseComObject(obj);
        }
    }

    [ComImport]
    [Guid("CACAF262-9370-4615-A13B-9F5539DA4C0A")]
    private class WICImagingFactory
    {
    }

    [StructLayout(LayoutKind.Sequential)]
    private class WICRect
    {
        public int X;
        public int Y;
        public int Width;
        public int Height;
    }

    [Flags]
    private enum WICDecodeOptions
    {
        WICDecodeMetadataCacheOnDemand = 0x0,
        WICDecodeMetadataCacheOnLoad = 0x1,
    }

    [Flags]
    private enum WICBitmapEncoderCacheOption
    {
        WICBitmapEncoderCacheInMemory = 0x0,
        WICBitmapEncoderCacheTempFile = 0x1,
        WICBitmapEncoderNoCache = 0x2,
    }

    [Flags]
    private enum GenericAccessRights : uint
    {
        GENERIC_READ = 0x80000000,
        GENERIC_WRITE = 0x40000000,
        GENERIC_EXECUTE = 0x20000000,
        GENERIC_ALL = 0x10000000,

        GENERIC_READ_WRITE = GENERIC_READ | GENERIC_WRITE
    }

    [Guid("ec5ec8a9-c395-4314-9c77-54d7a935ff70"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    private interface IWICImagingFactory
    {
        IWICBitmapDecoder CreateDecoderFromFilename([MarshalAs(UnmanagedType.LPWStr)] string wzFilename, [MarshalAs(UnmanagedType.LPArray, SizeConst = 1)] Guid[] pguidVendor, GenericAccessRights dwDesiredAccess, WICDecodeOptions metadataOptions);
        IWICBitmapDecoder CreateDecoderFromStream(IStream pIStream, [MarshalAs(UnmanagedType.LPArray, SizeConst = 1)] Guid[] pguidVendor, WICDecodeOptions metadataOptions);

        void NotImpl2();
        void NotImpl3();
        void NotImpl4();

        IWICBitmapEncoder CreateEncoder([MarshalAs(UnmanagedType.LPStruct)] Guid guidContainerFormat, [MarshalAs(UnmanagedType.LPArray, SizeConst = 1)] Guid[] pguidVendor);

        void NotImpl6();
        void NotImpl7();

        IWICBitmapScaler CreateBitmapScaler();

        // not fully impl...
    }

    [Guid("00000120-a8f2-4877-ba0a-fd2b6645fb94"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    private interface IWICBitmapSource
    {
        void GetSize(out int puiWidth, out int puiHeight);
        Guid GetPixelFormat();
        void GetResolution(out double pDpiX, out double pDpiY);

        void NotImpl3();
        void NotImpl4();
    }

    [Guid("00000302-a8f2-4877-ba0a-fd2b6645fb94"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    private interface IWICBitmapScaler : IWICBitmapSource
    {
        #region IWICBitmapSource
        new void GetSize(out int puiWidth, out int puiHeight);
        new Guid GetPixelFormat();
        new void GetResolution(out double pDpiX, out double pDpiY);
        new void NotImpl3();
        new void NotImpl4();
        #endregion IWICBitmapSource

        void Initialize(IWICBitmapSource pISource, int uiWidth, int uiHeight, WicBitmapInterpolationMode mode);
    }

    [Guid("9EDDE9E7-8DEE-47ea-99DF-E6FAF2ED44BF"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    private interface IWICBitmapDecoder
    {
        void NotImpl0();
        void NotImpl1();

        Guid GetContainerFormat();

        void NotImpl3();
        void NotImpl4();
        void NotImpl5();
        void NotImpl6();
        void NotImpl7();
        void NotImpl8();
        void NotImpl9();

        IWICBitmapFrameDecode GetFrame(int index);
    }

    [Guid("3B16811B-6A43-4ec9-A813-3D930C13B940"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    private interface IWICBitmapFrameDecode : IWICBitmapSource
    {
        // not fully impl...
    }

    [Guid("00000103-a8f2-4877-ba0a-fd2b6645fb94"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    private interface IWICBitmapEncoder
    {
        void Initialize(IStream pIStream, WICBitmapEncoderCacheOption cacheOption);
        Guid GetContainerFormat();

        void NotImpl2();
        void NotImpl3();
        void NotImpl4();
        void NotImpl5();
        void NotImpl6();

        void CreateNewFrame(out IWICBitmapFrameEncode ppIFrameEncode, IntPtr encoderOptions);
        void Commit();

        // not fully impl...
    }

    [Guid("00000105-a8f2-4877-ba0a-fd2b6645fb94"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    private interface IWICBitmapFrameEncode
    {
        void Initialize(IntPtr pIEncoderOptions);
        void SetSize(int uiWidth, int uiHeight);
        void SetResolution(double dpiX, double dpiY);
        void SetPixelFormat([MarshalAs(UnmanagedType.LPStruct)] Guid pPixelFormat);

        void NotImpl4();
        void NotImpl5();
        void NotImpl6();
        void NotImpl7();

        void WriteSource(IWICBitmapSource pIBitmapSource, WICRect prc);
        void Commit();

        // not fully impl...
    }

    private class ManagedIStream : IStream
    {
        private Stream _stream;

        public ManagedIStream(Stream stream)
        {
            _stream = stream;
        }

        public void Read(byte[] buffer, int count, IntPtr pRead)
        {
            int read = _stream.Read(buffer, 0, count);
            if (pRead != IntPtr.Zero)
            {
                Marshal.WriteInt32(pRead, read);
            }
        }

        public void Seek(long offset, int origin, IntPtr newPosition)
        {
            long pos = _stream.Seek(offset, (SeekOrigin)origin);
            if (newPosition != IntPtr.Zero)
            {
                Marshal.WriteInt64(newPosition, pos);
            }
        }

        public void SetSize(long newSize)
        {
            _stream.SetLength(newSize);
        }

        public void Stat(out System.Runtime.InteropServices.ComTypes.STATSTG stg, int flags)
        {
            const int STGTY_STREAM = 2;
            stg = new System.Runtime.InteropServices.ComTypes.STATSTG();
            stg.type = STGTY_STREAM;
            stg.cbSize = _stream.Length;
            stg.grfMode = 0;

            if (_stream.CanRead && _stream.CanWrite)
            {
                const int STGM_READWRITE = 0x00000002;
                stg.grfMode |= STGM_READWRITE;
                return;
            }

            if (_stream.CanRead)
            {
                const int STGM_READ = 0x00000000;
                stg.grfMode |= STGM_READ;
                return;
            }

            if (_stream.CanWrite)
            {
                const int STGM_WRITE = 0x00000001;
                stg.grfMode |= STGM_WRITE;
                return;
            }

            throw new IOException();
        }

        public void Write(byte[] buffer, int count, IntPtr written)
        {
            _stream.Write(buffer, 0, count);
            if (written != IntPtr.Zero)
            {
                Marshal.WriteInt32(written, count);
            }
        }

        public void Clone(out IStream ppstm) { throw new NotImplementedException(); }
        public void Commit(int grfCommitFlags) { throw new NotImplementedException(); }
        public void CopyTo(IStream pstm, long cb, IntPtr pcbRead, IntPtr pcbWritten) { throw new NotImplementedException(); }
        public void LockRegion(long libOffset, long cb, int dwLockType) { throw new NotImplementedException(); }
        public void Revert() { throw new NotImplementedException(); }
        public void UnlockRegion(long libOffset, long cb, int dwLockType) { throw new NotImplementedException(); }
    }
}