重返照片的原始世界:我为.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 | 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()
用来将内存复制一份。
这样一来额外会复制会让性能略微降低,这是为了确保Bitmap
、BitmapSource
或Mat
的生命周期由自己来管理,否则它们会共享使用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骚操作】
重返照片的原始世界:我为.NET打造的RAW照片解析利器的更多相关文章
- 【PS实例】轻松打造梦幻的照片
本系列教程将开始讲解PS的一些制作实例,通过实例的讲解同时介绍各种工具和面板机快捷键的使用,这样能够让大家更有兴趣学习,在学习的同时能够创造出自己喜欢的东西.本人使用的教程都是根据本人多次调试制作,仅 ...
- 利用开源软件 Hugin 实现照片的景深合成,使用开源软件 enfuse 做照片的曝光合成
http://blog.csdn.net/liyuanbhu/article/details/53573847 http://blog.csdn.net/liyuanbhu/article/detai ...
- Spark原始码系列(六)Shuffle的过程解析
问题导读: 1.shuffle过程的划分? 2.shuffle的中间结果如何存储? 3.shuffle的数据如何拉取过来? Shuffle过程的划分 Spark的操作模型是基于RDD的,当调用RD ...
- WhatsApp的Erlang世界
rick 的两个ppt整理 下载:2012 2013 ,使用半年erlang后,重新看这两个ppt才发现更多值的学习的地方,从ppt中整理如下: - Prefer os:timestamp to e ...
- IOS照片框架
介绍 每天,用 iPhone 拍摄的照片数量超过了任何相机.每年 iOS 设备上的显示效果变得越来越好,回到 iPad 刚出现还没有 Retina 显示屏的时代,大屏幕的杀手级功能之一就是可以展示用户 ...
- ArcGIS 10.1 for Desktop新特性之地理标记照片
转自:http://blog.csdn.net/esrichinacd/article/details/7730825 地理标记照片是指带有地理位置信息的照片,通常通过内置GPS的数码相机或智能手机拍 ...
- iOS-拍照后裁剪,不可拖动照片的问题
2016.07.08 15:04* 字数 1837 阅读 6066评论 6喜欢 26赞赏 1 问题 在项目中,选择照片或拍照的功能很长见,由于我之前采用系统自带的UIimagePickViewCont ...
- 利用exif.js解决ios或Android手机上传竖拍照片旋转90度问题
html5+canvas进行移动端手机照片上传时,发现ios手机上传竖拍照片会逆时针旋转90度,横拍照片无此问题:Android手机没这个问题. 因此解决这个问题的思路是:获取到照片拍摄的方向角,对非 ...
- 【纯净软件】三款照片EXIF信息删除软件 Clear Exif、JPEG & PNG Stripper、Easy Exif Delete 非专业横向对比
商业软件:需支付费用后方可使用. 共享软件:需支付费用,但可以先免费试用(有使用期限.功能限制). 免费软件:无需支付费用,无使用期限,无功能限制. 纯净软件:无广告.无联网行为的免费软件. 自由软件 ...
- Smart3D系列教程4之 《案例实战演练1——小物件的照片三维重建》
一.前言 Wish3D出品的Smart3D系列教程已经推出3讲了,分别是关于倾斜摄影三维建模原理应用.照片采集技巧.Smart3D各个功能模块的作用,它们都是围绕Smart3D建模软件进行的讲解.那么 ...
随机推荐
- html5和css3基础学习笔记
网页简介 一个页面包括结构.表现.行为三个部分. 结构:HTML用于描述页面的结构. 表现:CSS用于控制页面中元素的样式. 行为:JavaScript用于响应用户操作. 第一部分 HTML 5(Hy ...
- 用R语言进行时间序列ARMA模型分析
应用时间序列 时间序列分析是一种重要的数据分析方法,应用广泛.以下列举了几个时间序列分析的应用场景: 1.经济预测:时间序列分析可以用来分析经济数据,预测未来经济趋势和走向.例如,利用历史股市数据和经 ...
- vue+vant项目中 rem适配配置
vant rem适配,需要安装两个插件 postcss-pxtorem 是一款 postcss 插件,用于将单位转化为 rem lib-flexible 用于设置 rem 基准值 postcss-px ...
- 2023-03-19:使用Go语言和FFmpeg库实现pcm编码为mp3。
2023-03-19:使用Go语言和FFmpeg库实现pcm编码为mp3. 答案2023-03-19: 本文将介绍如何使用Go语言和FFmpeg库实现PCM音频文件编码为MP3格式.我们将使用moon ...
- 2022-09-08:以下go语言代码输出什么?A:5 66;B:5 88;C:7 88;D:以上都不对。 package main func main() { var x = []int{4:
2022-09-08:以下go语言代码输出什么?A:5 66:B:5 88:C:7 88:D:以上都不对. package main func main() { var x = []int{4:44, ...
- 2021-01-07:cdn加速是什么原理?
福哥答案2021-01-07:[答案来自此链接:](https://www.zhihu.com/question/438234873)CDN(Content Delivery Network):内容分 ...
- AcWing 278. 数字组合
给定 N 个正整数 A1,A2,-,AN,从中选出若干个数,使它们的和为 M,求有多少种选择方案. 输入格式 第一行包含两个整数 N 和 M. 第二行包含 N 个整数,表示 A1,A2,-,AN. 输 ...
- 小程序 Page "pages/posts/post-detail/post-detail" has not been registered yet.
今使用wx.navigateTo进行页面跳转老是提示Page "pages/posts/post-detail/post-detail" has not been register ...
- 【python基础】日常知识点整理
[三种方法调用] 1. 类中的方法区分为普通方法(self),静态方法(@staticMenthod),类方法@classMenthod,隐式参数(cls) <1> 普通方法:第一个参数 ...
- Taurus.mvc .Net Core 微服务开源框架发布V3.1.7:让分布式应用更高效。
前言: 自首个带微服务版本的框架发布:Taurus.MVC V3.0.3 微服务开源框架发布:让.NET 架构在大并发的演进过程更简单 已经过去快1年了,在这近一年的时间里,版本经历了N个版本的迭代. ...