- Java锁的逻辑(结合对象头和ObjectMonitor)
- 还在用饼状图?来瞧瞧这些炫酷的百分比可视化新图形(附代码实现)⛵
- 自动注册实体类到EntityFrameworkCore上下文,并适配ABP及ABPVNext
- 基于Sklearn机器学习代码实战
如果你是我的老读者,你可能还记得,在2019年,我冒险进入了一片神秘的领域——用 C# 解析 RAW 格式的照片:
在那两篇文章的尾声处,我曾给自己和大家留下了一个悬念:
时光荏苒,三年的时间转眼就过去了。现在,我希望告诉大家一个好消息:我终于填补了这个坑!我投入大量的时间和精力,认真研究了 libraw 这个库,并且基于它的 C API ,制作了一款 C# 的PInvoke封装: Sdcb.LibRaw 。
如果你已经对我其他的开源项目有所了解,你会发现,在这里,你同样需要同时安装 .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 这种授权方式(受上游代码指定).
使用前需要安装下面至少2个 NuGet 包:
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
现在,让我们考虑一些更复杂的用例。比如,如果你使用的是 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;
}
上面代码很简洁,但别被这个简单的外表欺骗了, 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); // 裁切
// 还有许多其它设置可以自行探索
});
原图:
应用白平衡:
拉一拉曲线:
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个函数:
请注意它转换为 Bitmap 的方式有所不同,由于它的数据本质是 JPEG 格式,因此不再需要更换红色、蓝色的通道位置,同样也不需要关注它的宽度和高度,同样的道理如果使用 OpenCV 解码也应该使用 Cv2.ImDecode .
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个函数:
以下的 `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 的性能测试代码:
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
之前的文章说过,可以使用系统自带的 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-Q8-x64 的代码,非常简单:
Stopwatch sw = Stopwatch.StartNew();
using MagickImage image = new MagickImage(@"C:\a7r3\DSC02653.ARW");
Console.WriteLine($"耗时:{sw.ElapsedMilliseconds}ms");
输出如下:
耗时:5496ms
这个方案的缺点是它明显慢一些,且它的后处理都并非基于拜尔数据,因此后期空间有限.
另外作为参考,我还写了一份基于 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照片解析利器的文章就讲到这里了,如果你想了解更多关于重返照片的原始世界:我为.NET打造的RAW照片解析利器的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
我一直在使用 AJAX 从我正在创建的网络服务中解析 JSON 数组时遇到问题。我的前端是一个简单的 ajax 和 jquery 组合,用于显示从我正在创建的网络服务返回的结果。 尽管知道我的数据库查
很难说出这里要问什么。这个问题模棱两可、含糊不清、不完整、过于宽泛或夸夸其谈,无法以目前的形式得到合理的回答。如需帮助澄清此问题以便重新打开,visit the help center . 关闭 1
我在尝试运行 Android 应用程序时遇到问题并收到以下错误 java.lang.NoClassDefFoundError: com.parse.Parse 当我尝试运行该应用时。 最佳答案 在这
有什么办法可以防止etree在解析HTML内容时解析HTML实体吗? html = etree.HTML('&') html.find('.//body').text 这给了我 '&' 但我想
我有一个有点疯狂的例子,但对于那些 JavaScript 函数作用域专家来说,它看起来是一个很好的练习: (function (global) { // our module number one
关闭。此题需要details or clarity 。目前不接受答案。 想要改进这个问题吗?通过 editing this post 添加详细信息并澄清问题. 已关闭 8 年前。 Improve th
我需要编写一个脚本来获取链接并解析链接页面的 HTML 以提取标题和其他一些数据,例如可能是简短的描述,就像您链接到 Facebook 上的内容一样。 当用户向站点添加链接时将调用它,因此在客户端启动
在 VS Code 中本地开发时,包解析为 C:/Users//AppData/Local/Microsoft/TypeScript/3.5/node_modules/@types//index而不是
我在将 json 从 php 解析为 javascript 时遇到问题 这是我的示例代码: //function MethodAjax = function (wsFile, param) {
我在将 json 从 php 解析为 javascript 时遇到问题 这是我的示例代码: //function MethodAjax = function (wsFile, param) {
我被赋予了将一种语言“翻译”成另一种语言的工作。对于使用正则表达式的简单逐行方法来说,源代码过于灵活(复杂)。我在哪里可以了解更多关于词法分析和解析器的信息? 最佳答案 如果你想对这个主题产生“情绪化
您好,我在解析此文本时遇到问题 { { { {[system1];1;1;0.612509325}; {[system2];1;
我正在为 adobe after effects 在 extendscript 中编写一些代码,最终变成了 javascript。 我有一个数组,我想只搜索单词“assemble”并返回整个 jc3_
我有这段代码: $(document).ready(function() { // }); 问题:FB_RequireFeatures block 外部的代码先于其内部的代码执行。因此 who
背景: netcore项目中有些服务是在通过中间件来通信的,比如orleans组件。它里面服务和客户端会指定网关和端口,我们只需要开放客户端给外界,服务端关闭端口。相当于去掉host,这样省掉了些
1.首先贴上我试验成功的代码 复制代码 代码如下: protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
什么是 XML? XML 指可扩展标记语言(eXtensible Markup Language),标准通用标记语言的子集,是一种用于标记电子文件使其具有结构性的标记语言。 你可以通过本站学习 X
【PHP代码】 复制代码 代码如下: $stmt = mssql_init('P__Global_Test', $conn) or die("initialize sto
在SQL查询分析器执行以下代码就可以了。 复制代码代码如下: declare @t varchar(255),@c varchar(255) declare table_cursor curs
前言 最近练习了一些前端算法题,现在做个总结,以下题目都是个人写法,并不是标准答案,如有错误欢迎指出,有对某道题有新的想法的友友也可以在评论区发表想法,互相学习🤭 题目 题目一: 二维数组中的
我是一名优秀的程序员,十分优秀!