[英]Right way to parallelize pixel access across multiple images using ImageSharp
I'm trying to parallelize the processing of an image using ImageSharp.我正在尝试使用 ImageSharp 并行处理图像。 The documentation here: https://docs.sixlabors.com/articles/imagesharp/pixelbuffers.html has an example of processing two images in parallel with the following code:
此处的文档: 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;
}
But that scenario has a key difference to mine, which is that I need to access totally arbitrary pixels from the source image .但这种情况与我的情况有一个关键区别,那就是我需要从源图像访问完全任意的像素。 Like so:
像这样:
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;
}
}
I've already tried using the ProcessPixelRows
method on the outImage, but I suspect that accessing the pixels in the sourceImage while inside that block prevents parallelization.我已经尝试在 outImage 上使用
ProcessPixelRows
方法,但我怀疑在该块内访问 sourceImage 中的像素会阻止并行化。
Simply replacing the for loops with Parallel.For scrambles the output image.只需将 for 循环替换为 Parallel.For 即可打乱 output 图像。
Note that each outImage pixel is written to exactly once, the sourceImage never changes, and the calculation of the value for the outImage pixel is deterministic based on the source sample.请注意,每个 outImage 像素只写入一次,sourceImage 永远不会更改,并且 outImage 像素值的计算是基于源样本的确定性的。
I would normally recommend using our higher level pixel buffer manipulation for pixel access.我通常会建议使用我们更高级别的像素缓冲区操作来访问像素。 While not parallel by default (the
Vector4
variant is) they're extremely efficient.虽然默认情况下不是并行的(
Vector4
变体是),但它们非常高效。
However, if you want to use parallel processing you should use ParallelRowIterator
from the SixLabors.ImageSharp.Advanced
namespace.但是,如果要使用并行处理,则应使用
SixLabors.ImageSharp.Advanced
命名空间中的ParallelRowIterator
。 This splits the processing into blocks based up on the number of available processors applying a user defined IRowOperation<T>
instance to the image.这会根据将用户定义的
IRowOperation<T>
实例应用于图像的可用处理器的数量将处理分成多个块。
Here's a basic example applying random pixels from a source to a destination.这是一个将随机像素从源应用到目标的基本示例。
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.
Your row operation would look something like this.您的行操作看起来像这样。
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];
}
}
Following up on my own question, I had a chance to work through @James's answer and made some discoveries that I thought might be useful to share.跟进我自己的问题,我有机会研究@James 的回答,并发现了一些我认为可能对分享有用的发现。 If you don't give a shit about what is happening and just want code, skip to the end.
如果您不关心正在发生的事情并且只想编写代码,请跳到最后。
First, I looked into his suggestion to avoid using the Advanced
namespace, and instead consider using the ProcessPixelRowsAsVector4
variant of the higher level pixel buffer manipulation API. I discovered that I couldn't use that because in my case I need the row index (ie. int y
) to do my calculations, and ProcessPixelRowsAsVector4
doesn't provide it.首先,我研究了他避免使用
Advanced
命名空间的建议,而是考虑使用更高级别像素缓冲区操作 API 的ProcessPixelRowsAsVector4
变体。我发现我不能使用它,因为在我的情况下我需要行索引(即int y
) 来进行我的计算,而ProcessPixelRowsAsVector4
不提供它。
I opened a discussion here about providing an overload of ProcessPixelRowsAsVector4
that actually does have the row index, so it's possible by the time you're reading this that the library actually has a signature like this that you should try using.我在这里讨论了关于提供实际上具有行索引的
ProcessPixelRowsAsVector4
重载的讨论,因此当您阅读本文时,该库可能实际上具有您应该尝试使用的这样的签名。
Meanwhile, in the present, I went about implementing James's other suggestion, the ParallelRowIterator.IterateRows
solution.同时,目前,我着手实施 James 的其他建议,即
ParallelRowIterator.IterateRows
解决方案。
I did get it working, but as I was putting final touches, I realized that Invoke(int y, Span<Rgba32> span)
was giving me a span that I wasn't using.我确实让它工作了,但是当我进行最后的润色时,我意识到
Invoke(int y, Span<Rgba32> span)
给了我一个我没有使用的跨度。 What is that span?那跨度是多少? Why was I not using it?
为什么我没有使用它? Could I use it?
我可以使用它吗? Should I
discard
it?我应该
discard
它吗?
This mattered to me because in my case these spans could be between 25,000 and 50,000 pixels long, and about that same number of spans could be allocated, so not doing that if it wasn't necessary seemed like a good idea.这对我来说很重要,因为在我的例子中,这些跨度可能在 25,000 到 50,000 像素之间长,并且可以分配大约相同数量的跨度,所以如果没有必要就不要这样做似乎是个好主意。 (It was possible that it's just returning a pointer to a particular place in already-allocated memory, which makes this less of an issue, but I wanted to know.)
(有可能它只是返回一个指向已分配的 memory 中特定位置的指针,这使得这不是一个问题,但我想知道。)
My first guess was that the normal usecase for IterateRows
involved looping over a source image, so maybe that span was like sourceRowSpan
, and if that was true maybe I could somehow get the iterator to loop over the destination image and just return destinationRowSpan
without me having to additionally get it inside the loop using DangerousGetRowSpan
which sounds like the kind of method call that wears a leather jacket and disrespects your mother.我的第一个猜测是
IterateRows
的正常用例涉及循环遍历源图像,所以也许那个 span 就像sourceRowSpan
,如果这是真的,也许我可以以某种方式让迭代器循环遍历目标图像并返回destinationRowSpan
而无需我另外使用DangerousGetRowSpan
将它放入循环中,这听起来像是那种穿着皮夹克并且不尊重你母亲的方法调用。
But I couldn't see how the iterator was even choosing where to get the span from at all, like my source and destination buffers were just members on my custom RowOperation
class with no special relationship to the IRowOperation
interface.但是我根本看不出迭代器是如何选择从哪里获取跨度的,就像我的源缓冲区和目标缓冲区只是我的自定义
RowOperation
class 上的成员,与IRowOperation
接口没有特殊关系。
So I looked inside the ParallelRowIterator class and followed the rabbit hole into RowOperationWrapper which seemed to be what was actually calling the Invoke
, and I discovered 3 things:所以我查看了ParallelRowIterator class并跟随兔子洞进入RowOperationWrapper ,这似乎是真正调用
Invoke
的东西,我发现了三件事:
span
being passed in was just a newly allocated memory span of whatever pixel type.span
只是新分配的 memory 任何像素类型的跨度。 So it WAS, in fact, allocating a bunch of memory for this and not just returning a pointer to the memory._
if I wanted._
显式丢弃它。 So having discovered that, I modified the originally suggested code as follows:所以在发现这一点之后,我修改了最初建议的代码如下:
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];
}
}
Basically I made RowOperation
implement IRowOperation
instead of IRowOperation<Rgba32>
, and invoked it with .IterateRows<RowOperation>()
instead of .IterateRows<RowOperation, Rgba32>()
, which invokes with just the row index instead of the row index plus Span object.基本上我让
RowOperation
实现IRowOperation
而不是IRowOperation<Rgba32>
,并用.IterateRows<RowOperation>()
而不是.IterateRows<RowOperation, Rgba32>()
调用它,它只调用行索引而不是行索引加上 Span object。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.