2

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

 1 year ago
source link: https://www.cnblogs.com/sdflysha/p/20230801-sdcb-libraw-intro.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

重返照片的原始世界:我为.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 MIT .NET封装包
Sdcb.LibRaw.runtime.win64 LGPL-2.1-only OR CDDL-1.0 Windows x64 运行时
Sdcb.LibRaw.runtime.win32 LGPL-2.1-only OR CDDL-1.0 Windows x86 运行时
Sdcb.LibRaw.runtime.linux64 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); // 裁切
    // 还有许多其它设置可以自行探索
});

原图:

233608-20230802085605055-375394601.png

应用白平衡:

233608-20230802085610942-1230961054.png

拉一拉曲线:

233608-20230802085616860-807506620.png

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骚操作

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK