重返照片的原始世界:我为.NET打造的RAW照片解析利器

重返照片的原始世界:我为.NET打造的RAW照片解析利器

如果你是我的老读者,你可能还记得,在2019年,我冒险进入了一片神秘的领域——用C#解析RAW格式的照片:

在那两篇文章的尾声处,我曾给自己和大家留下了一个悬念:

  • 我曾希望能深入研究libraw,可是它只提供了C API……
  • 虽然MagickImage表现出色,但在处理CR2上似乎还有些瑕疵,我暗示可能需要再写一篇文章来解决这个问题……

时光荏苒,三年的时间转眼就过去了。现在,我希望告诉大家一个好消息:我终于填补了这个坑!我投入大量的时间和精力,认真研究了libraw这个库,并且基于它的C API,制作了一款C#的PInvoke封装:Sdcb.LibRaw

NuGet包简介

如果你已经对我其他的开源项目有所了解,你会发现,在这里,你同样需要同时安装.NET封装包和运行时动态库包。顾名思义,带runtime的包就是运行时包(比如runtime.win64代表支持64位Windows),而安装的时候你需要同时安装.NET封装包和运行时包。

下面这个表格涵盖了所有你需要知道的关于这些包的信息:

包名 NuGet 授权方式 注释
Sdcb.LibRaw NuGet MIT .NET封装包
Sdcb.LibRaw.runtime.win64 NuGet LGPL-2.1-only OR CDDL-1.0 Windows x64 运行时
Sdcb.LibRaw.runtime.win32 NuGet LGPL-2.1-only OR CDDL-1.0 Windows x86 运行时
Sdcb.LibRaw.runtime.linux64 NuGet LGPL-2.1-only OR CDDL-1.0 Ubuntu 22.04 x64 运行时

所有运行时包,我都是使用vcpkg编译而成,这包括上面的linux包。我在Ubuntu 22.04上进行了编译,因此,如果你想在docker中使用,你需要选择以jammy结尾的docker镜像,例如:mcr.microsoft.com/dotnet/sdk:6.0-jammy

值得一提的是,Linux自带的包管理也自带了LibRaw,但系统自带的LibRaw用的是老版本,导致本质和我的包二进制不兼容,因此并不能使用,需要使用我编译的。

我的.NET主包遵循MIT授权方式开源,其它的包则采取LGPL-2.1-only OR CDDL-1.0这种授权方式(受上游代码指定)。

使用示例

1. RAW照片转Bitmap

使用前需要安装下面至少2个NuGet包:

  • Sdcb.LibRaw
  • Sdcb.LibRaw.runtime.win64(或者其它系统包)
using Sdcb.LibRaw;

using RawContext r = RawContext.OpenFile(@"C:\a7r3\DSC02653.ARW");
r.Unpack();
r.DcrawProcess();
using ProcessedImage image = r.MakeDcrawMemoryImage();
using Bitmap bmp = ProcessedImageToBitmap(image);

Bitmap ProcessedImageToBitmap(ProcessedImage rgbImage)
{
    rgbImage.SwapRGB();
    using Bitmap bmp = new Bitmap(rgbImage.Width, rgbImage.Height, rgbImage.Width * 3, System.Drawing.Imaging.PixelFormat.Format24bppRgb, rgbImage.DataPointer);
    return new Bitmap(bmp);
}

这段代码主要演示如何将RAW照片转换为Bitmap图像,有一点值得一提:LibRaw输出的像素格式和Bitmap有些许不同,具体体现在红蓝两色需要调换,代码中使用rgbImage.SwapRGB();用来调换红色和蓝色的顺序,也就是将RGB24转换成了BGR24

虽然这个示例基于.ARW照片,但实际上几乎所有RAW格式照片都是支持的,包括.CR2或者.DNG,可以通过RawContext.SupportedCameras获取支持的相机列表,截止当前版本它支持了1182款相机型号:

Console.WriteLine("Sdcb.LibRaw supported cameras:");
foreach (string model in RawContext.SupportedCameras)
{
	Console.WriteLine(model);
}

输出如下(有省略):

Sdcb.LibRaw supported cameras:
1: Adobe Digital Negative (DNG)
2: AgfaPhoto DC-833m
...
1057: Sony ILCE-1 (A1)
...
1181: Zeiss ZX1
1182: Zenit M

2. WPF和OpenCV示例

现在,让我们考虑一些更复杂的用例。比如,如果你使用的是 WPF,可以使用如下代码将ProcessedImage转换为BitmapSource

BitmapSource ProcessedImageToBitmapSource(ProcessedImage rgbImage)
{
	return BitmapSource.Create(rgbImage.Width, rgbImage.Height,
		96, 96,
		PixelFormats.Rgb24,
		null,
		rgbImage.AsSpan<byte>().ToArray(), 
		rgbImage.Width * 3);
}

值得一提的是 WPF 的图片 BitmapSource 并不需要调换 R B 两个通道的颜色。

如果你使用的是 OpencvSharp4 ,可以使用下面的代码将 ProcessedImage 转换为Mat

Mat ProcessedImageToMat(ProcessedImage rgbImage)
{
	Mat mat = new Mat(rgbImage.Height, rgbImage.Width, MatType.CV_8UC3, rgbImage.AsSpan<byte>().ToArray());
	Cv2.CvtColor(mat, mat, ColorConversionCodes.RGB2BGR);
	return mat;
}

请注意上面3个示例中,我都使用了.AsSpan<byte>().ToArray()用来将内存复制一份。

这样一来额外会复制会让性能略微降低,这是为了确保BitmapBitmapSourceMat的生命周期由自己来管理,否则它们会共享使用ProcessedImage的内存,导致意外的情况。

但如果你能掌握ProcessedImage的生命周期,保证ProcessedImage生命周期比Bitmap/BitmapSource/Mat更长,你即可解锁0内存复制的做法(以OpenCV为例):

// 小心,代码直接使用了由ProcessedImage创建的内存
Mat ProcessedImageToMatZeroCopy(ProcessedImage rgbImage)
{
	Mat mat = new Mat(rgbImage.Height, rgbImage.Width, MatType.CV_8UC3, rgbImage.DataPointer); // 换成rgbImage.DataPointer
	Cv2.CvtColor(mat, mat, ColorConversionCodes.RGB2BGR);
	return mat;
}

3. 图像后期

上面代码很简洁,但别被这个简单的外表欺骗了,Sdcb.LibRaw还有强大图像后期能力。

DcrawProcess()函数支持传入一个Action<OutputParams>作为参数,你可以从这个参数定义多种图像后处理功能,例如你可以设置Gamma曲线的指数和斜率,调整亮度,甚至设置裁剪区域等,像这样:

r.DcrawProcess(c =>
{
    c.HalfSize = false; // 图片只保留1/4大小
    c.UseCameraWb = true; // 使用机内白平衡,false则会由UserMultipliers控制白平衡
    c.Gamma[0] = 0.35; // 调整Gamma曲线指数
    c.Gamma[1] = 3.5;  // 调整Gamma曲线斜率
    c.Brightness = 2.2f; // 亮度
    c.Interpolation = true; // 是否执行反马赛克(demosaic)操作
    c.OutputBps = 8; // 输出位数8位
    c.OutputTiff = false; // 输出为tiff文件?false表示输出Bitmap
    // c.Cropbox = new Rectangle(4000, 2000, 1500, 700); // 裁切
    // 还有许多其它设置可以自行探索
});

原图:

应用白平衡:

拉一拉曲线:

4. 从RAW照片中读取缩略图

现在的RAW格式照片中往往保存着一张或多张JPEG格式的缩略图,用Sdcb.LibRaw也能读出来,这是一个将ARW照片中第1张缩略图转换为Bitmap的示例:

using Sdcb.LibRaw;

using RawContext r = RawContext.OpenFile(@"C:\a7r3\DSC02653.ARW");
using ProcessedImage image = r.ExportThumbnail(thumbnailIndex: 0);
using Bitmap bmp = (Bitmap)Bitmap.FromStream(new MemoryStream(image.AsSpan<byte>().ToArray()));

在上面的示例中我使用了r.ExportThumbnail(thumbnailIndex: 0);,它可以导出第0张缩略图,请注意这个是一个快捷函数,它内部会调用了下面2个函数:

  • r.UnpackThumbnail()
  • r.MakeDcrawMemoryThumbnail()

请注意它转换为Bitmap的方式有所不同,由于它的数据本质是JPEG格式,因此不再需要更换红色、蓝色的通道位置,同样也不需要关注它的宽度和高度,同样的道理如果使用OpenCV解码也应该使用Cv2.ImDecode

5. 将RAW照片转换并保存为本地tiff文件

using Sdcb.LibRaw;

using RawContext r = RawContext.OpenFile(@"C:\a7r3\DSC02653.ARW");
// r.SaveRawImage() is a shortcut for r.Unpack() + r.DcrawProcess() + r.WriteDcrawPpmTiff(fileName)
r.SaveRawImage(@"C:\test\test.tiff");

同样地r.SaveRawImage(@"C:\test\test.tiff");也是Sdcb.LibRaw提供的一个快捷方式,它内部按顺序调用了下面3个函数:

  • r.Unpack()
  • r.DcrawProcess()
  • r.WriteDcrawPpmTiff()

5. 获取照片元数据信息

以下的 `C#`` 代码主要用于从指定的照片文件获取其元数据信息,然后将它们在控制台中输出。

using RawContext r = RawContext.OpenFile(@"C:\a7r3\DSC02653.ARW");
LibRawImageParams imageParams = r.ImageParams;
LibRawImageOtherParams otherParams = r.ImageOtherParams;
LibRawLensInfo lensInfo = r.LensInfo;

Console.WriteLine($"相机: {imageParams.Model}");
Console.WriteLine($"版本号: {imageParams.Software}");
Console.WriteLine($"ISO: {otherParams.IsoSpeed}");
Console.WriteLine($"快门速度: 1/{1 / otherParams.Shutter:F0}s");
Console.WriteLine($"焦距: {otherParams.FocalLength}mm");
Console.WriteLine($"艺术家标签: {otherParams.Artist}");
Console.WriteLine($"拍摄日期: {new DateTime(1970, 1, 1, 8, 0, 0).AddSeconds(otherParams.Timestamp)}");
Console.WriteLine($"镜头名称: {lensInfo.Lens}");

在我的这个示例中,输出如下:

相机: ILCE-7RM3
版本号: ILCE-7RM3 v3.10
ISO: 100
快门速度: 1/400s
焦距: 50mm
艺术家标签: Zhou Jie/sdcb
拍摄日期: 2023/1/26 12:54:01
镜头名称: FE 50mm F1.2 GM

性能与方案比较

Sdcb.LibRaw

首先这是使用 Sdcb.LibRaw 的性能测试代码:

var sw = Stopwatch.StartNew();
using RawContext r = RawContext.OpenFile(@"C:\Users\ZhouJie\Pictures\a7r3\11030126\DSC02653.ARW");
r.Unpack();
r.DcrawProcess();
Console.WriteLine($"耗时:{sw.ElapsedMilliseconds}ms");

输出如下:

耗时:1627ms

Windows Imaging Component

之前的文章说过,可以使用系统自带的WIC进行RAW照片解码:

// 需要安装NuGet包:Vortices.Direct2D1
Stopwatch sw = Stopwatch.StartNew();
IWICImagingFactory2 wic = new IWICImagingFactory2();
using IWICBitmapDecoder decoder = wic.CreateDecoderFromFileName(@"C:a7r3\DSC02653.ARW");
using IWICFormatConverter converter = wic.CreateFormatConverter();
converter.Initialize(decoder.GetFrame(0), PixelFormat.Format24bppBGR);
var data = new byte[converter.Size.Width * 3 * converter.Size.Height];
converter.CopyPixels(converter.Size.Width * 3, data);
Console.WriteLine($"耗时:{sw.ElapsedMilliseconds}ms");

// 下面转Bitmap
// fixed (byte* pdata = data)
// {
// 	new System.Drawing.Bitmap(converter.Size.Width, converter.Size.Height, converter.Size.Width * 3, System.Drawing.Imaging.PixelFormat.Format24bppRgb, (IntPtr)pdata).DumpUnscaled();
// }

输出如下:

耗时:2177ms

这个方案的缺点是它无法对 RAW 照片做一些后处理。

Magick.NET

另外这是基于Magick.NET-Q8-x64的代码,非常简单:

Stopwatch sw = Stopwatch.StartNew();
using MagickImage image = new MagickImage(@"C:\a7r3\DSC02653.ARW");
Console.WriteLine($"耗时:{sw.ElapsedMilliseconds}ms");

输出如下:

耗时:5496ms

这个方案的缺点是它明显慢一些,且它的后处理都并非基于拜尔数据,因此后期空间有限。

原生C代码

另外作为参考,我还写了一份基于 LibRaw C API 的代码,代码如下:

#include <libraw\libraw.h>
#include <chrono>

int main()
{
	auto start = std::chrono::high_resolution_clock::now();
	libraw_data_t* data = libraw_init(0);
	libraw_open_file(data, "C:\\a7r3\\DSC02653.ARW");
	libraw_unpack(data);
	libraw_dcraw_process(data);
	auto end = std::chrono::high_resolution_clock::now();
	printf("耗时: %lld ms\n", std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count());

	libraw_recycle(data);
	libraw_close(data);
}

输出如下:

耗时: 1619 ms

比较表格

方案名称 耗时(ms) 说明
Sdcb.LibRaw 1627
Windows Imaging Component(WIC) 2177 后期空间有限
Magick.NET 5496 后期空间有限
原生C代码 1619

可见,Sdcb.LibRaw 性能在第一梯队,且后处理是基于拜尔数据,能力较强。

结语

我理解有相机、需要使用代码处理 RAW 格式照片的朋友确实不多,但随着智能手机的发展,许多手机也能拍出RAW格式照片了,我坚信这个工具将会为那些需要它的人带来极大的帮助。我将继续在这个领域上付出努力,为 Sdcb.LibRaw 添加更多的功能,并解决可能存在的问题。

上述内容仅仅是我所打造的 Sdcb.LibRaw 库中的一部分功能,它的强大功能和高效性能将为你处理 RAW 格式照片带来前所未有的便捷。我真心希望更多的 .NET 爱好者能够加入我,一起探索 RAW 照片处理的世界,Sdcb.LibRaw 将始终保持好用且免费,让我们共同期待它的更多精彩!

有兴趣尝试 Sdcb.LibRaw 的朋友,欢迎访问我的 Github,也请给个 Star🌟

如果你喜欢我的工作,请关注我的微信公众号:【DotNet骚操作】

DotNet骚操作

posted @ 2023-08-02 08:57  .NET骚操作  阅读(4856)  评论(22编辑  收藏  举报