本文告诉大家如何使用 Softwarebitmap 进行创建、修改保存图片。
在 UWP 使用底层的图像渲染就是使用 Softwarebitmap ,这个类提供直接数据修改,可以使用这个类进行软渲染。实际上 Softwarebitmap 和 WriteableBitmap 是差不多的。但是 Softwarebitmap 可以支持 WriteableBitmap 、 Direct3D 和代码修改。通过 Softwarebitmap 可以修改转换不同的像素格式和透明通道,支持低级修改像素。作为一个通用的底层类在很多性能要求比较高的地方用到,如 CapturedFrame、VideoFrame、FaceDetector。下面来告诉大家如何使用。
创建
下面来告诉大家如何读取文件,使用图片数据创建 Softwarebitmap 图片。
首先是需要使用 FileOpenPicker 拿到一张图片,如何读写文件参见:win10 UWP读写文件
因为很简单,下面直接拿到一张 jpg ,当然需要用户点击。下面代码是直接从微软文档复制的,我自己没运行,看起来大家可以直接使用。
FileOpenPicker fileOpenPicker = new FileOpenPicker();
fileOpenPicker.SuggestedStartLocation = PickerLocationId.PicturesLibrary;
fileOpenPicker.FileTypeFilter.Add(".jpg");
fileOpenPicker.ViewMode = PickerViewMode.Thumbnail;
var inputFile = await fileOpenPicker.PickSingleFileAsync();
if (inputFile == null)
{
// The user cancelled the picking operation
return;
}
因为需要拿到文件内容,需要使用 OpenAsync 方法获得随机访问流。随机访问流就是可以在随机的地方进行读写,和他不相同的是顺序流,也就是只能顺序读写。使用 BitmapDecoder.CreateAsync 创建一个图片解析,用来拿到图片
SoftwareBitmap softwareBitmap;
using (IRandomAccessStream stream = await inputFile.OpenAsync(FileAccessMode.Read))
{
// Create the decoder from the stream
BitmapDecoder decoder = await BitmapDecoder.CreateAsync(stream);
// Get the SoftwareBitmap representation of the file
softwareBitmap = await decoder.GetSoftwareBitmapAsync();
}
保存图片
上面和大家说如何读取文件,现在就可以把刚才读取的图片保存。保存需要用户选择保存在哪
FileSavePicker fileSavePicker = new FileSavePicker();
fileSavePicker.SuggestedStartLocation = PickerLocationId.PicturesLibrary;
fileSavePicker.FileTypeChoices.Add("png files", new List<string>() { ".png" });
fileSavePicker.SuggestedFileName = "image";
var outputFile = await fileSavePicker.PickSaveFileAsync();
if (outputFile == null)
{
// The user cancelled the picking operation
return;
}
使用 OpenAsync 方法打开文件,转换随机写入流写入数据。使用 BitmapEncoder.CreateAsync 创建 BitmapEncoder 。创建的函数第一个参数是 GUID 表示需要哪个格式,可以通过 BitmapEncoder 输入,下面代码就是把刚才读取的 jpg 图片转换为 Png 格式。
using (var stream = await outputFile.OpenAsync(FileAccessMode.ReadWrite))
{
// 格式 png 通过把上面打开的图片转换
BitmapEncoder encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.PngEncoderId, stream);
encoder.SetSoftwareBitmap(_softwareBitmap);
}
这个方法就是把 softwareBitmap 转换为 Stream 的方法
如果在保存需要对图片进行编辑,可以使用 BitmapTransform 进行变换,请看代码
encoder.BitmapTransform.ScaledWidth = 320;
encoder.BitmapTransform.ScaledHeight = 240;
// 90度旋转
encoder.BitmapTransform.Rotation = BitmapRotation.Clockwise90Degrees;
// 大的图片转换为小的图片,需要使用插值算法,不然会模糊
encoder.BitmapTransform.InterpolationMode = BitmapInterpolationMode.Fant;
// 创建新的缩略图
encoder.IsThumbnailGenerated = true;
因为不是所有的文件格式都支持缩略图,如果使用了创建新的图就需要 catch 不支持异常。
在转换图片需要调用 FlushAsync 保存图片。
try
{
await encoder.FlushAsync();
}
catch (Exception exception)
{
const int WINCODEC_ERR_UNSUPPORTEDOPERATION = unchecked((int) 0x88982F81);
switch (exception.HResult)
{
case WINCODEC_ERR_UNSUPPORTEDOPERATION:
// 如果格式不支持,就会出现这个异常,需要禁止创建缩略图,然后继续保存
encoder.IsThumbnailGenerated = false;
break;
default:
throw;
}
}
if (encoder.IsThumbnailGenerated == false)
{
await encoder.FlushAsync();
}
现在在前台添加两个按钮,一个用于打开文件,另一个用来保存图片
随便选一个 jpg 文件,然后保存,可以看到保存了新的格式
在 UWP 可以使用上面的方法修改图片格式
上面代码只是简单使用,在创建 BitmapEncoder 可以传入 BitmapPropertySet 指定图片质量
var propertySet = new Windows.Graphics.Imaging.BitmapPropertySet();
var qualityValue = new Windows.Graphics.Imaging.BitmapTypedValue(
1.0, // Maximum quality
Windows.Foundation.PropertyType.Single
);
propertySet.Add("ImageQuality", qualityValue);
// 格式 png 通过把上面打开的图片转换
BitmapEncoder encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.PngEncoderId, stream, propertySet);
其他的值请看 BitmapEncoder options reference
在 Image 控件使用
刚才的代码没有显示打开的图片,如果要把 SoftwareBitmap 在 Image 使用,就需要使用 SoftwareBitmapSource 转换,因为 Image 控件只支持 BGRA8 格式而且需要先计算透明值,在转换打开 SoftwareBitmap 静态函数 Convert 让格式在 Image 控件支持。
先在界面创建一个 Image 控件,然后在后台添加代码显示
<Image x:Name="MaixallnayMesejas"></Image>
if (_softwareBitmap.BitmapPixelFormat != BitmapPixelFormat.Bgra8 ||
_softwareBitmap.BitmapAlphaMode == BitmapAlphaMode.Straight)
{
_softwareBitmap = SoftwareBitmap.Convert(_softwareBitmap, BitmapPixelFormat.Bgra8,
BitmapAlphaMode.Premultiplied);
}
var source = new SoftwareBitmapSource();
await source.SetBitmapAsync(_softwareBitmap);
MaixallnayMesejas.Source = source;
尝试运行一下代码就可以看到显示图片在打开文件。
实际上通过 下面代码可以把 SoftwareBitmap 转 ImageBrush 显示
var imageBrush = new ImageBrush {ImageSource = source};
WriteableBitmap 转换
上面都是读写文件,如果已经使用了 WriteableBitmap 需要把他转换 SoftwareBitmap 可以使用 SoftwareBitmap 的静态函数 SoftwareBitmap.CreateCopyFromBuffer 转换。
SoftwareBitmap outputBitmap = SoftwareBitmap.CreateCopyFromBuffer(
writeableBitmap.PixelBuffer,
BitmapPixelFormat.Bgra8,
writeableBitmap.PixelWidth,
writeableBitmap.PixelHeight
);
通过读写像素
是不是看到上面的教程感觉这个博客很简单,我就来告诉大家很黑的方法,如果看到这里还没有关闭这个网页,那么现在关闭还是可以,不然我就来和大家说很黑科技的写法。
如果大家直接从 SoftwareBitmap 使用 Resharper 无论怎么点都无法找到读写像素的方法。但是我会告诉大家我自己创建了一个接口,使用这个接口就可以读写。
首先引用using System.Runtime.InteropServices;
然后创建接口
[ComImport]
[Guid("5B0D3235-4DBA-4D44-865E-8F1D0E4FD04D")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
internal unsafe interface IMemoryBufferByteAccess
{
void GetBuffer(out byte* buffer, out uint capacity);
}
创建这个接口有什么用,先不告诉大家,因为用了不安全,需要在项目属性,生成,可以使用不安全
我来告诉大家如何从代码创建 SoftwareBitmap ,读写像素。
创建一个空白的 SoftwareBitmap 需要设置格式
var softwareBitmap = new SoftwareBitmap(BitmapPixelFormat.Bgra8, 100, 100, BitmapAlphaMode.Straight);
使用 LockBuffer 可以拿到 buffer ,使用 buffer.CreateReference 可以拿到指针
using (var buffer = softwareBitmap.LockBuffer(BitmapBufferAccessMode.ReadWrite))
{
using (var reference = buffer.CreateReference())
{
}
}
然后就是不安全代码,本文的黑科技就是这个代码
using (var buffer = softwareBitmap.LockBuffer(BitmapBufferAccessMode.ReadWrite))
{
using (var reference = buffer.CreateReference())
{
unsafe
{
((IMemoryBufferByteAccess) reference).GetBuffer(out var dataInBytes, out _);
}
}
把 reference 转换为我自己定义的接口,使用 GetBuffer 拿到数据指针。
这个的原理,本渣在这里不会说。
拿到了 dataInBytes 就是按照 BGRA 的顺序,但是还不知道图片的宽度用了多少个,而且图片如果是分层的,第 n 层是从哪个数据开始。为了知道指针的开始,就使用 BitmapBuffer 的方法
BitmapPlaneDescription bufferLayout = buffer.GetPlaneDescription(0);
获取图层数量可以使用buffer.GetPlaneCount()
,因为第 0 个在这里是有的,所以直接使用
那么图片的宽使用多少个如何拿到,bufferLayout.StartIndex 就是拿到图层开始所在,bufferLayout.Stride 就是一行使用了多少 byte 。所以要访问第 i 行 j 列的像素就可以使用下面的代码
dataInBytes[bufferLayout.StartIndex + bufferLayout.Stride * i + 4 * j + 0] // B
dataInBytes[bufferLayout.StartIndex + bufferLayout.Stride * i + 4 * j + 1] // G
ataInBytes[bufferLayout.StartIndex + bufferLayout.Stride * i + 4 * j + 2] // R
dataInBytes[bufferLayout.StartIndex + bufferLayout.Stride * i + 4 * j + 3] // A
写入的方式就是直接给一个值,读取的方式就是去拿,方法很简单,下面来写一个渐变
((IMemoryBufferByteAccess)reference).GetBuffer(out dataInBytes, out capacity);
// Fill-in the BGRA plane
BitmapPlaneDescription bufferLayout = buffer.GetPlaneDescription(0);
for (int i = 0; i < bufferLayout.Height; i++)
{
for (int j = 0; j < bufferLayout.Width; j++)
{
byte value = (byte)((float)j / bufferLayout.Width * 255);
dataInBytes[bufferLayout.StartIndex + bufferLayout.Stride * i + 4 * j + 0] = value;
dataInBytes[bufferLayout.StartIndex + bufferLayout.Stride * i + 4 * j + 1] = value;
dataInBytes[bufferLayout.StartIndex + bufferLayout.Stride * i + 4 * j + 2] = value;
dataInBytes[bufferLayout.StartIndex + bufferLayout.Stride * i + 4 * j + 3] = (byte)255;
}
}
转换 CanvasBitmap
使用 CanvasBitmap.CreateFromSoftwareBitmap
可以从 SoftwareBitmap 转换为 CanvasBitmap
var canvasBitmap = CanvasBitmap.CreateFromSoftwareBitmap(device, softwareBitmap);
需要注意,如果 SoftwareBitmap 的像素格式比较诡异,那么不一定能创建
sourceBitmap's BitmapPixelFormat | CanvasBitmap's Format |
---|---|
BitmapPixelFormat.Unknown | unsupported |
BitmapPixelFormat.Rgba16 | DirectXPixelFormat.R16G16B16A16UIntNormalized |
BitmapPixelFormat.Rgba8 | DirectXPixelFormat.R8G8B8A8UIntNormalized |
BitmapPixelFormat.Gray16 | unsupported |
BitmapPixelFormat.Gray8 | DirectXPixelFormat.A8UIntNormalized |
BitmapPixelFormat.Bgra8 | DirectXPixelFormat.B8G8R8A8UIntNormalized |
BitmapPixelFormat.Nv12 | unsupported |
BitmapPixelFormat.Yuy2 | unsupported |
本文会经常更新,请阅读原文: https://blog.lindexi.com/post/win10-uwp-%E5%A6%82%E4%BD%95%E5%88%9B%E5%BB%BA%E4%BF%AE%E6%94%B9%E4%BF%9D%E5%AD%98%E4%BD%8D%E5%9B%BE.html ,以避免陈旧错误知识的误导,同时有更好的阅读体验。
如果你想持续阅读我的最新博客,请点击 RSS 订阅,推荐使用RSS Stalker订阅博客,或者收藏我的博客导航
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名林德熙(包含链接: https://blog.lindexi.com ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请 与我联系 。
无盈利,不卖课,做纯粹的技术博客
以下是广告时间
推荐关注 Edi.Wang 的公众号
欢迎进入 Eleven 老师组建的 .NET 社区
以上广告全是友情推广,无盈利