【渣翻译】.NET Core图片处理

原文:.NET Core Image Processing

原作者:Bertrand Le Roy


**************


.NET Core图片处理

图片处理,特别是图片缩放,是web应用的常见需求。对此,我将对.NET Core图片处理的几种类库做一个概述。对于每个类库,我会给出一个图片缩放的代码示例,并简述其有趣的特性。在最后,我会就这些类库在速度,大小,输出质量等方面的表现进行对比。


CoreCompat.System.Drawing

如果你现有的代码依赖于System.Drawing,那么使用这个库显然是通向.NET Core和跨平台的幸福捷径:它的性能和输出质量都很好,并且API完全相同。.NET Framework内置的 System.Drawing 是最简单的图片处理方式,但是它依赖于Windows的GDI+,并未包含于.NET Core中。另外,GDI+是标准的客户端技术,并不支持多线程的服务器环境,所以死锁问题可能会导致这个解决方案并不适用于你的应用。

CoreCompat.System.DrawingSystem.Drawing的Mono实现版本 的.NET Core接口。同.NET Framework和Mono中的System.Drawing一样,CoreCompat.System.Drawing 也依赖Windows的GDI+,所以基于同样的原因(译注:即死锁),建议谨慎使用。

使用这个库进行跨平台开发时也要注意,需要引用 runtime.osx.10.10-x64.CoreCompat.System.Drawing 和(或) runtime.linux-x64.CoreCompat.System.Drawing 包。

using System.Drawing;

const int size = 150;
const int quality = 75;

using (var image = new Bitmap(System.Drawing.Image.FromFile(inputPath)))
{
    int width, height;
    if (image.Width > image.Height)
    {
        width = size;
        height = Convert.ToInt32(image.Height * size / (double)image.Width);
    }
    else
    {
        width = Convert.ToInt32(image.Width * size / (double)image.Height);
        height = size;
    }
    var resized = new Bitmap(width, height);
    using (var graphics = Graphics.FromImage(resized))
    {
        graphics.CompositingQuality = CompositingQuality.HighSpeed;
        graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
        graphics.CompositingMode = CompositingMode.SourceCopy;
        graphics.DrawImage(image, 0, 0, width, height);
        using (var output = File.Open(
            OutputPath(path, outputDirectory, SystemDrawing), FileMode.Create))
        {
            var qualityParamId = Encoder.Quality;
            var encoderParameters = new EncoderParameters(1);
            encoderParameters.Param[0] = new EncoderParameter(qualityParamId, quality);
            var codec = ImageCodecInfo.GetImageDecoders()
                .FirstOrDefault(codec => codec.FormatID == ImageFormat.Jpeg.Guid);
            resized.Save(output, codec, encoderParameters);
        }
    }
}

ImageSharp

ImageSharp 是全新的,由纯托管代码编写的跨平台图片处理类库。它的性能并不如其他依赖于操作系统原生的本地类库(译注:就是指OS的API或者本地非托管语言的类库),但仍然不错。它只依赖于.NET本身,非常的轻巧:不用安装额外的包,只要引用 ImageSharp 本身就行了。

如果你打算使用 ImageSharp,不要使用NuGet上面的包:在官方的第一个正式版发布之前,那只是个用来占地方的空项目。在那之前,你需要通过 MyGet 来获取它的每日构建。你可以在项目根目录下添加 NuGet.config 文件,并输入以下内容:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <packageSources>
    <add key="ImageSharp Nightly" value="https://www.myget.org/F/imagesharp/api/v3/index.json" />
  </packageSources>
</configuration>

使用 ImageSharp 进行图片缩放非常的简单:

using ImageSharp;

const int size = 150;
const int quality = 75;

Configuration.Default.AddImageFormat(new JpegFormat());

using (var input = File.OpenRead(inputPath))
{
    using (var output = File.OpenWrite(outputPath))
    {
        var image = new Image(input)
            .Resize(new ResizeOptions
            {
                Size = new Size(size, size),
                Mode = ResizeMode.Max
            });
        image.ExifProfile = null;
        image.Quality = quality;
        image.Save(output);
    }
}

作为一个新项目,这个类库出奇的完善。它有几乎所有你希望的图片滤镜,甚至有完善的 EXIF信息 读写支持(以下代码也适用于 Magick.NET ):

var exif = image.ExifProfile;
var description = exif.GetValue(ImageSharpExifTag.ImageDescription);
var yearTaken = DateTime.ParseExact(
    (string)exif.GetValue(ImageSharpExifTag.DateTimeOriginal).Value,
    "yyyy:MM:dd HH:mm:ss",
    CultureInfo.InvariantCulture)
    .Year;
var author = exif.GetValue(ImageSharpExifTag.Artist);
var copyright = $"{description} (c) {yearTaken} {author}";
exif.SetValue(ImageSharpExifTag.Copyright, copyright);

请注意,ImageSharp 最新的版本比以往更加的模块化,如果你使用Jpeg之类的图片格式,或者使用图片缩放之类的处理功能,你需要在 ImageSharp 的核心包中添加更多的附加包(分别是ImageSharp.Processing 和ImageSharp.Formats.Jpeg)。



译注:同作者开发的JimBobSquarePants/ImageProcessor也是图片处理的类库。

Magick.NET

Magick.NET 是知名类库 ImageMagick 的.NET封装。ImageMagick 是一个专注于图片质量的开源跨平台的类库,并对图片格式提供广泛的支持。同 ImageSharp 一样,它也支持EXIF信息的编辑。


Magick.NET的.NET Core版本目前只支持Windows。类库的作者 Dirk Lemstra 正在寻求转换构建脚本的帮助。如果你有在Mac或Linux上构建本地类库的经验,那么这是你支持这个超棒项目的绝佳机会。

Magick.NET 的图片质量在本文讨论的所有类库中出类拔萃,这一点你可以在下面的示例中看到,并且它的性能也很不错。它同样拥有非常完善的API,并且对特殊的文件格式提供极好的支持。

using ImageMagick;

const int size = 150;
const int quality = 75;

using (var image = new MagickImage(inputPath))
{
    image.Resize(size, size);
    image.Strip();
    image.Quality = quality;
    image.Write(outputPath);
}

SkiaSharp

SkiaSharpGoogle的跨平台2D图形库Skia的.NET封装,由Xamarin团队维护。 SkiaSharp现在兼容.NET Core,处理速度非常快。由于它依赖于一些本地类库,所以安装可能会有些麻烦,但我在Windows和Mac下用起来非常容易。目前在Linux下使用它有些困难,因为必须从源码构建一些本地类库,不过现在维护团队正在努力减少这些制约,所以SkiaSharp很快就会变为一个很有竞争力的选项。

译注:其实所谓的麻烦也就是指Linux下要自己编译,Windows和Mac可以直接使用Nuget包获取。

using SkiaSharp;

const int size = 150;
const int quality = 75;

using (var input = File.OpenRead(inputPath))
{
    using (var inputStream = new SKManagedStream(input))
    {
        using (var original = SKBitmap.Decode(inputStream))
        {
            int width, height;
            if (original.Width > original.Height)
            {
                width = size;
                height = original.Height * size / original.Width;
            }
            else
            {
                width = original.Width * size / original.Height;
                height = size;
            }

            using (var resized = original
                   .Resize(new SKImageInfo(width, height), SKBitmapResizeMethod.Lanczos3))
            {
                if (resized == null) return;

                using (var image = SKImage.FromBitmap(resized))
                {
                    using (var output = 
                           File.OpenWrite(OutputPath(path, outputDirectory, SkiaSharpBitmap)))
                    {
                        image.Encode(SKImageEncodeFormat.Jpeg, Quality)
                            .SaveTo(output);
                    }
                }
            }
        }
    }
}

FreeImage-dotnet-core

Magick.NET 之于 ImageMagick 一样,这个库是 FreeImage库的.NET Core封装。它有很好的图片格式支持,良好的性能,以及不错的图片质量。不过目前的跨平台支持并不完美,例如笔者提出的 Linux 和macOS下无法保存图片到硬盘,希望可以尽快修复。

译注:作者2月10日提交了错误,到目前为止类库作者还没回复,项目最近的更新也是3个月前了……


using FreeImageAPI;

const int size = 150;

using (var original = FreeImageBitmap.FromFile(path))
{
    int width, height;
    if (original.Width > original.Height)
    {
        width = size;
        height = original.Height * size / original.Width;
    }
    else
    {
        width = original.Width * size / original.Height;
        height = size;
    }
    var resized = new FreeImageBitmap(original, width, height);
    // JPEG_QUALITYGOOD is 75 JPEG.
    // JPEG_BASELINE strips metadata (EXIF, etc.)
    resized.Save(OutputPath(path, outputDirectory, FreeImage), FREE_IMAGE_FORMAT.FIF_JPEG,
        FREE_IMAGE_SAVE_FLAGS.JPEG_QUALITYGOOD |
        FREE_IMAGE_SAVE_FLAGS.JPEG_BASELINE);
}

MagicScaler

MagicScaler 是Windows限定的图片处理类库,虽然基于windows图像处理组件(WIC),但它自身的算法使其能够实现极高质量的重采样。它不是泛用的2D类库,只专注于优化图片缩放。如下面的图片所示,它的效果令人惊叹:它的处理速度极快,而且质量无与伦比。跨平台能力的缺失令它会被许多应用放弃,但如果你的应用只在Windows上运行,而且只需要图片缩放功能,那么它是完美的选择。

using PhotoSauce.MagicScaler;

const int size = 150;
const int quality = 75;

var settings = new ProcessImageSettings() {
    Width = size,
    Height = size,
    ResizeMode = CropScaleMode.Max,
    SaveFormat = FileFormat.Jpeg,
    JpegQuality = quality,
    JpegSubsampleMode = ChromaSubsampleMode.Subsample420
};

using (var output = new FileStream(OutputPath(path, outputDirectory, MagicScaler), FileMode.Create))
{
    MagicImageProcessor.ProcessImage(path, output, settings);
}

性能比较

在第一组基准测试中,会测试图片的加载,缩放,并保存为清晰度(译注:即DPI)为75的jpeg图片。我采用了12张主题各不相同的图片,他们细节丰富,不易缩放,更容易发现问题。图片是大约百万像素的JPEG文件——其中有一张稍小一些。 你的测试结果可能会于此不同,这取决于你使用的图片类型。我建议你尝试使用自己的图片作为样例来再现这些结果。


在第二组基准测试中,会把一张空的百万像素图片缩小成150像素的缩略图,不进行硬盘的访问。

在测试中,CoreCompat.System.Drawing,ImageSharp, 以及Magick.NET 使用.NET Core 1.0.3(目前最新的长期支持版本),SkiaSharp使用Mono 4.6.2。


译注:现在的LTS版本是1.0.4。

我使用HP Z420工作站进行Windows下的测试,配置为四核Xeon E5-1620,16G内存,以及内置的Redeon显卡。Linux下的测试也用相同的机器,不过是用一个4G内存的虚拟机,因此较低的性能表现与 Windows 和 Linux 的性能差异毫无关系,而且只有类库和类库之间的对比才有意义。Mac下的测试运行在IMac上,配置为1.4GHz Core i5,8G内存,以及内置的Intel HD Graphics 5000显卡,系统为macOS Sierra。

硬件配置的差异导致测试结果存在着较大的偏差:GPU 和 SIMD 的利用度和性能取决于机器是否启用了它们——这决定了类库它们的利用度。开发者想要获得更好的性能,需要进行进一步的测试。值得一提的是,我不得不禁用了 Magick.NET 的 OpenCL 支持(OpenCL.IsEnabled = false;),因为它让我的工作站的测试结果比笔记本还要差。

以下所有图标的数值都是越低越好。

文件越小越好,请注意,文件大小取决于图片二次采样的质量,因此文件大小的对比应考虑到最后的图片质量比较。

图片质量比较

下面是调整后的图像。如你所见,类库之间以及图片之间的质量差异非常明显。一些图片在锐度上有极大差异,而且可以看到一些摩尔纹。 你应该根据具体项目的需求,在性能与质量之间进行权衡。

译注:图片略,具体内容请参考原文中 【Quality comparison】一节。


结论

针对不同的需求,当前有很多不错的选择,而在不久后的将来,肯定会有更好的选择。

如果性能优先,并且 Windows 服务器场景的死锁问题不会影响到你的应用,那么 CoreCompat.System.Drawing 是很好的选择。如果 SkiaSharp 在 Linux 下的安装问题得到解决,那么这个兼顾图片质量的类库将是最佳的选择。

如果质量优先,并且你的应用只运行于 Windows,那么兼顾性能的 MagicScaler 是完美的选择。

如果优先考虑支持的图片类型,那么Magick.NET胜出。尽管它的跨平台能力还不是很好,但是你可以 给它帮助


最后,作为本文中唯一的纯托管代码编写的类库,ImageSharp 也是一个很好的选择。它的性能接近 Magick.NET ,并且不需要任何本地依赖,这意味着这个库在任何部署了.NET Core的地方稳定地工作。这个库现在还是alpha版本,并将有非常大的性能改进,尤其是利用了Span<T> 和 ref returns 特性之后。

~~~~~~~~~~大波译注开始~~~~~~~~~~

所谓的ref returns,是C#7.0的新特性,VS2017可用,详见 What's New in C# 7

顾名思义,它的核心思路就是把值传递变为引用传递。在C# 7.0中,可以这么写:

//ref  returns
static ref Int32 RefReturns(Int32[] x)
{
    return ref x[0];
}

static void Main(string[] args)
{
    Int32 a = 1;
    Console.WriteLine($"a={a}");
    ref Int32 b = ref a;
    b = 2;
    Console.WriteLine($"a={a}");
    Int32[] c = { 3 };
    a = RefReturns(c);
    Console.WriteLine($"b={b}");
    ref Int32 d = ref RefReturns(c);
    d = 4;
    Console.WriteLine($"c[0]={c[0]}");
}
/*输出:
  a=1
  a=2
  b=3
  c[0]=4*/

显而易见,这组新特性可以让代码节约值传递和多次解引用操作的开销,提高效率。

而Span<T>是一个更潮的特性……现在还没完善,详见:dotnet/corefxlab

它类似数组,不同的是它除了可以指向托管内存外,还可以指向非托管的内存和在栈上分配的内存……下面是文档的例子,unsafe的代码段在这里已经成为了常客。

// 托管内存
var arrayMemory = new byte[100];
var arraySpan = new Span<byte>(arrayMemory);

// 非托管内存
var nativeMemory = Marshal.AllocHGlobal(100);
Span<byte> nativeSpan;
unsafe {
    nativeSpan = new Span<byte>(nativeMemory.ToPointer(), 100);
}
SafeSum(nativeSpan);
Marshal.FreeHGlobal(nativeMemory);

//栈内存
Span<byte> stackSpan;
unsafe {
    byte* stackMemory = stackalloc byte[100];
    stackSpan = new Span<byte>(stackMemory, 100);
}
SafeSum(stackSpan);

// 这个方法不关心内存是什么类型的(也不需要unsafe)。
static ulong SafeSum(Span<byte> bytes) { 
    ulong sum = 0;
    for(int i=0; i < bytes.Length; i++) {
        sum += bytes[i];
    }
    return sum;
}

甚至还支持数组切片……扯远了,具体还是看那个文档吧,总之这东西的目标就是面向服务器的高扩展性的高效数据操作。

~~~~~~~~~~大波译注结束~~~~~~~~~~


鸣谢

略。

译注:就是本文作者感谢那些类库的作者……

代码示例

示例地址:bleroy/core-imaging-playground

作者用的图片地址:bleroy/core-imaging-playground

原文的更新记录,略

全文完。

PS1:作者最后更新日期是 2/16/2017。

PS:在文章的评论区,Gibbo向Bertrand Le Roy推荐了一个封装了OpenVC的类库shimat/opencvsharp,作者测试可以用,但在加载某个dll的时候失败了……这是他的实验项目bleroy/core-imaging-playground

编辑于 2017-03-24