使用 ImageSharp 跨多个图像并行访问像素的正确方法
Right way to parallelize pixel access across multiple images using ImageSharp
我正在尝试使用 ImageSharp 并行处理图像。此处的文档:https://docs.sixlabors.com/articles/imagesharp/pixelbuffers.html 有一个使用以下代码并行处理两个图像的示例:
// Extract a sub-region of sourceImage as a new image
private static Image<Rgba32> Extract(Image<Rgba32> sourceImage, Rectangle sourceArea)
{
Image<Rgba32> targetImage = new(sourceArea.Width, sourceArea.Height);
int height = sourceArea.Height;
sourceImage.ProcessPixelRows(targetImage, (sourceAccessor, targetAccessor) =>
{
for (int i = 0; i < height; i++)
{
Span<Rgba32> sourceRow = sourceAccessor.GetRowSpan(sourceArea.Y + i);
Span<Rgba32> targetRow = targetAccessor.GetRowSpan(i);
sourceRow.Slice(sourceArea.X, sourceArea.Width).CopyTo(targetRow);
}
});
return targetImage;
}
但那种情况与我的情况有一个关键区别,那就是我需要从源图像 访问完全任意的像素。像这样:
Image<Rgb24> sourceImage = GetImage();
Image<Rgb24> outImage = GetImage();
for (var outY = 0; outY < outImage.Height; outY++)
{
for (int outX = 0; outX < outImage.Width; outX++)
{
var outColor = GetArbitraryPixelFromAnywhereInsideSourceImage(sourceImage, outX, outY); // access arbitrary pixels from the source image based on some calculation, probably a block of between 2x2 and 4x4 pixels
outImage[outX, outY] = outColor;
}
}
我已经尝试在 outImage 上使用 ProcessPixelRows
方法,但我怀疑在该块内访问 sourceImage 中的像素会阻止并行化。
只需将 for 循环替换为 Parallel.For 即可打乱输出图像。
请注意,每个 outImage 像素只写入一次,sourceImage 永远不会改变,outImage 像素值的计算是基于源样本确定的。
我通常会建议使用我们更高级别的像素缓冲区操作来访问像素。虽然默认情况下不是并行的(Vector4
变体是),但它们非常高效。
但是,如果您想使用并行处理,您应该使用 SixLabors.ImageSharp.Advanced
命名空间中的 ParallelRowIterator
。这将根据可用处理器的数量将处理分成多个块,将用户定义的 IRowOperation<T>
实例应用于图像。
这是一个将随机像素从源应用到目标的基本示例。
using Image<Rgba32> source = new(100, 100);
using Image<Rgba32> destination = new(100, 100);
Configuration configuration = Configuration.Default;
// You need access to individual frame pixel buffers in order
// to access some of the advanced APIs
RowOperation operation = new RowOperation(
configuration,
source.Frames[0].PixelBuffer,
destination.Frames[0].PixelBuffer);
// Ensure we don't go out of bounds
var interest = Rectangle.Intersect(source.Bounds(), destination.Bounds());
ParallelRowIterator.IterateRows<RowOperation, Rgba32>(
configuration,
interest,
in operation);
// Save the output.
您的行操作看起来像这样。
private readonly struct RowOperation : IRowOperation<Rgba32>
{
private readonly Random random;
private readonly Buffer2D<Rgba32> source;
private readonly Buffer2D<Rgba32> destination;
private readonly Configuration configuration;
public RowOperation(
Configuration configuration,
Buffer2D<Rgba32> source,
Buffer2D<Rgba32> destination)
{
this.source = source;
this.destination = destination;
this.random = new();
this.configuration = configuration;
}
public void Invoke(int y, Span<Rgba32> span)
{
Span<Rgba32> destinationRowSpan = this.destination.DangerousGetRowSpan(y);
for (int x = 0; x < destinationRowSpan.Length; x++)
{
destinationRowSpan[x] = this.GetRandomPixel();
}
}
private Rgba32 GetRandomPixel()
{
int y = this.random.Next(this.source.Height);
int x = this.random.Next(this.source.Width);
return this.source[x, y];
}
}
根据我自己的问题,我有机会仔细阅读@James 的回答,并发现了一些我认为可能对分享有用的发现。如果你不关心正在发生的事情而只是想要代码,请跳到最后。
首先,我研究了他关于避免使用 Advanced
命名空间的建议,而是考虑使用更高级像素缓冲区操作 API 的 ProcessPixelRowsAsVector4
变体。我发现我不能使用它,因为在我的例子中我需要行索引(即 int y
)来进行我的计算,而 ProcessPixelRowsAsVector4
没有提供它。
我发起了一个讨论 here 关于提供实际上确实有行索引的 ProcessPixelRowsAsVector4
的重载,所以当你阅读这篇文章时,库实际上有一个签名是可能的像这样你应该尝试使用。
同时,目前,我着手实施 James 的另一个建议,即 ParallelRowIterator.IterateRows
解决方案。
我确实让它工作了,但是当我进行最后的润色时,我意识到 Invoke(int y, Span<Rgba32> span)
给了我一个我没有使用的跨度。那跨度是多少?为什么我没有使用它?我可以使用它吗?我应该discard
吗?
这对我来说很重要,因为在我的例子中,这些跨度可能在 25,000 到 50,000 像素之间长,并且可以分配大约相同数量的跨度,所以如果没有必要不这样做似乎是个好主意. (有可能它只是 return 指向 already-allocated 内存中特定位置的指针,这使得这不是一个问题,但我想知道。)
我的第一个猜测是 IterateRows
的正常用例涉及在源图像上循环,所以也许那个跨度就像 sourceRowSpan
,如果那是真的,也许我可以以某种方式获得迭代器循环遍历目标图像,只需 return destinationRowSpan
而无需我使用 DangerousGetRowSpan
将其额外放入循环中,这听起来像是那种穿着皮夹克并且不尊重你的方法调用妈妈.
但我根本看不出迭代器是如何选择从哪里获取跨度的,就像我的源缓冲区和目标缓冲区只是我的自定义 RowOperation
class 中的成员一样,没有与 IRowOperation
接口的特殊关系。
所以我查看了 ParallelRowIterator class and followed the rabbit hole into RowOperationWrapper 的内部,这似乎是实际上调用 Invoke
的东西,我发现了 3 件事:
- 传入的
span
只是新分配的任何像素类型的内存范围。因此,事实上,它为此分配了一堆内存,而不仅仅是 return 指向内存的指针。
- 跨度并没有真正引用源图像或目标图像,所以如果我愿意,我可以安全地忽略它,最初建议的代码实际上是这样做的,但我的意思是我可以明确地丢弃它
_
如果我愿意的话。
- 还有一个完全不分配或传递跨度的签名!它只是传递行索引,这就是我真正需要的!
所以在发现这一点后,我修改了最初建议的代码如下:
using Image<Rgba32> source = new(100, 100);
using Image<Rgba32> destination = new(100, 100);
Configuration configuration = Configuration.Default;
// You need access to individual frame pixel buffers in order
// to access some of the advanced APIs
RowOperation operation = new RowOperation(
configuration,
source.Frames[0].PixelBuffer,
destination.Frames[0].PixelBuffer);
// Ensure we don't go out of bounds
var interest = Rectangle.Intersect(source.Bounds(), destination.Bounds());
ParallelRowIterator.IterateRows<RowOperation>(
configuration,
interest,
in operation);
// Save the output.
private readonly struct RowOperation : IRowOperation
{
private readonly Random random;
private readonly Buffer2D<Rgba32> source;
private readonly Buffer2D<Rgba32> destination;
private readonly Configuration configuration;
public RowOperation(
Configuration configuration,
Buffer2D<Rgba32> source,
Buffer2D<Rgba32> destination)
{
this.source = source;
this.destination = destination;
this.random = new();
this.configuration = configuration;
}
public void Invoke(int y)
{
Span<Rgba32> destinationRowSpan = this.destination.DangerousGetRowSpan(y);
for (int x = 0; x < destinationRowSpan.Length; x++)
{
destinationRowSpan[x] = this.GetRandomPixel();
}
}
private Rgba32 GetRandomPixel()
{
int y = this.random.Next(this.source.Height);
int x = this.random.Next(this.source.Width);
return this.source[x, y];
}
}
基本上我让 RowOperation
实现 IRowOperation
而不是 IRowOperation<Rgba32>
,并使用 .IterateRows<RowOperation>()
而不是 .IterateRows<RowOperation, Rgba32>()
调用它,它只调用行index 而不是行索引加上 Span 对象。
我正在尝试使用 ImageSharp 并行处理图像。此处的文档:https://docs.sixlabors.com/articles/imagesharp/pixelbuffers.html 有一个使用以下代码并行处理两个图像的示例:
// Extract a sub-region of sourceImage as a new image
private static Image<Rgba32> Extract(Image<Rgba32> sourceImage, Rectangle sourceArea)
{
Image<Rgba32> targetImage = new(sourceArea.Width, sourceArea.Height);
int height = sourceArea.Height;
sourceImage.ProcessPixelRows(targetImage, (sourceAccessor, targetAccessor) =>
{
for (int i = 0; i < height; i++)
{
Span<Rgba32> sourceRow = sourceAccessor.GetRowSpan(sourceArea.Y + i);
Span<Rgba32> targetRow = targetAccessor.GetRowSpan(i);
sourceRow.Slice(sourceArea.X, sourceArea.Width).CopyTo(targetRow);
}
});
return targetImage;
}
但那种情况与我的情况有一个关键区别,那就是我需要从源图像 访问完全任意的像素。像这样:
Image<Rgb24> sourceImage = GetImage();
Image<Rgb24> outImage = GetImage();
for (var outY = 0; outY < outImage.Height; outY++)
{
for (int outX = 0; outX < outImage.Width; outX++)
{
var outColor = GetArbitraryPixelFromAnywhereInsideSourceImage(sourceImage, outX, outY); // access arbitrary pixels from the source image based on some calculation, probably a block of between 2x2 and 4x4 pixels
outImage[outX, outY] = outColor;
}
}
我已经尝试在 outImage 上使用 ProcessPixelRows
方法,但我怀疑在该块内访问 sourceImage 中的像素会阻止并行化。
只需将 for 循环替换为 Parallel.For 即可打乱输出图像。
请注意,每个 outImage 像素只写入一次,sourceImage 永远不会改变,outImage 像素值的计算是基于源样本确定的。
我通常会建议使用我们更高级别的像素缓冲区操作来访问像素。虽然默认情况下不是并行的(Vector4
变体是),但它们非常高效。
但是,如果您想使用并行处理,您应该使用 SixLabors.ImageSharp.Advanced
命名空间中的 ParallelRowIterator
。这将根据可用处理器的数量将处理分成多个块,将用户定义的 IRowOperation<T>
实例应用于图像。
这是一个将随机像素从源应用到目标的基本示例。
using Image<Rgba32> source = new(100, 100);
using Image<Rgba32> destination = new(100, 100);
Configuration configuration = Configuration.Default;
// You need access to individual frame pixel buffers in order
// to access some of the advanced APIs
RowOperation operation = new RowOperation(
configuration,
source.Frames[0].PixelBuffer,
destination.Frames[0].PixelBuffer);
// Ensure we don't go out of bounds
var interest = Rectangle.Intersect(source.Bounds(), destination.Bounds());
ParallelRowIterator.IterateRows<RowOperation, Rgba32>(
configuration,
interest,
in operation);
// Save the output.
您的行操作看起来像这样。
private readonly struct RowOperation : IRowOperation<Rgba32>
{
private readonly Random random;
private readonly Buffer2D<Rgba32> source;
private readonly Buffer2D<Rgba32> destination;
private readonly Configuration configuration;
public RowOperation(
Configuration configuration,
Buffer2D<Rgba32> source,
Buffer2D<Rgba32> destination)
{
this.source = source;
this.destination = destination;
this.random = new();
this.configuration = configuration;
}
public void Invoke(int y, Span<Rgba32> span)
{
Span<Rgba32> destinationRowSpan = this.destination.DangerousGetRowSpan(y);
for (int x = 0; x < destinationRowSpan.Length; x++)
{
destinationRowSpan[x] = this.GetRandomPixel();
}
}
private Rgba32 GetRandomPixel()
{
int y = this.random.Next(this.source.Height);
int x = this.random.Next(this.source.Width);
return this.source[x, y];
}
}
根据我自己的问题,我有机会仔细阅读@James 的回答,并发现了一些我认为可能对分享有用的发现。如果你不关心正在发生的事情而只是想要代码,请跳到最后。
首先,我研究了他关于避免使用 Advanced
命名空间的建议,而是考虑使用更高级像素缓冲区操作 API 的 ProcessPixelRowsAsVector4
变体。我发现我不能使用它,因为在我的例子中我需要行索引(即 int y
)来进行我的计算,而 ProcessPixelRowsAsVector4
没有提供它。
我发起了一个讨论 here 关于提供实际上确实有行索引的 ProcessPixelRowsAsVector4
的重载,所以当你阅读这篇文章时,库实际上有一个签名是可能的像这样你应该尝试使用。
同时,目前,我着手实施 James 的另一个建议,即 ParallelRowIterator.IterateRows
解决方案。
我确实让它工作了,但是当我进行最后的润色时,我意识到 Invoke(int y, Span<Rgba32> span)
给了我一个我没有使用的跨度。那跨度是多少?为什么我没有使用它?我可以使用它吗?我应该discard
吗?
这对我来说很重要,因为在我的例子中,这些跨度可能在 25,000 到 50,000 像素之间长,并且可以分配大约相同数量的跨度,所以如果没有必要不这样做似乎是个好主意. (有可能它只是 return 指向 already-allocated 内存中特定位置的指针,这使得这不是一个问题,但我想知道。)
我的第一个猜测是 IterateRows
的正常用例涉及在源图像上循环,所以也许那个跨度就像 sourceRowSpan
,如果那是真的,也许我可以以某种方式获得迭代器循环遍历目标图像,只需 return destinationRowSpan
而无需我使用 DangerousGetRowSpan
将其额外放入循环中,这听起来像是那种穿着皮夹克并且不尊重你的方法调用妈妈.
但我根本看不出迭代器是如何选择从哪里获取跨度的,就像我的源缓冲区和目标缓冲区只是我的自定义 RowOperation
class 中的成员一样,没有与 IRowOperation
接口的特殊关系。
所以我查看了 ParallelRowIterator class and followed the rabbit hole into RowOperationWrapper 的内部,这似乎是实际上调用 Invoke
的东西,我发现了 3 件事:
- 传入的
span
只是新分配的任何像素类型的内存范围。因此,事实上,它为此分配了一堆内存,而不仅仅是 return 指向内存的指针。 - 跨度并没有真正引用源图像或目标图像,所以如果我愿意,我可以安全地忽略它,最初建议的代码实际上是这样做的,但我的意思是我可以明确地丢弃它
_
如果我愿意的话。 - 还有一个完全不分配或传递跨度的签名!它只是传递行索引,这就是我真正需要的!
所以在发现这一点后,我修改了最初建议的代码如下:
using Image<Rgba32> source = new(100, 100);
using Image<Rgba32> destination = new(100, 100);
Configuration configuration = Configuration.Default;
// You need access to individual frame pixel buffers in order
// to access some of the advanced APIs
RowOperation operation = new RowOperation(
configuration,
source.Frames[0].PixelBuffer,
destination.Frames[0].PixelBuffer);
// Ensure we don't go out of bounds
var interest = Rectangle.Intersect(source.Bounds(), destination.Bounds());
ParallelRowIterator.IterateRows<RowOperation>(
configuration,
interest,
in operation);
// Save the output.
private readonly struct RowOperation : IRowOperation
{
private readonly Random random;
private readonly Buffer2D<Rgba32> source;
private readonly Buffer2D<Rgba32> destination;
private readonly Configuration configuration;
public RowOperation(
Configuration configuration,
Buffer2D<Rgba32> source,
Buffer2D<Rgba32> destination)
{
this.source = source;
this.destination = destination;
this.random = new();
this.configuration = configuration;
}
public void Invoke(int y)
{
Span<Rgba32> destinationRowSpan = this.destination.DangerousGetRowSpan(y);
for (int x = 0; x < destinationRowSpan.Length; x++)
{
destinationRowSpan[x] = this.GetRandomPixel();
}
}
private Rgba32 GetRandomPixel()
{
int y = this.random.Next(this.source.Height);
int x = this.random.Next(this.source.Width);
return this.source[x, y];
}
}
基本上我让 RowOperation
实现 IRowOperation
而不是 IRowOperation<Rgba32>
,并使用 .IterateRows<RowOperation>()
而不是 .IterateRows<RowOperation, Rgba32>()
调用它,它只调用行index 而不是行索引加上 Span 对象。