允许多个线程访问同一张图片

9
我正在尝试在C#中进行图像处理。我想使用一些线程在我的图像中并行计算几个区域。线程实际上是在Bitmap对象中获取和设置像素。绝对没有2个线程同时访问同一个像素的机会,所以这不是问题。
问题在于,即使我确定同一个像素不会被同时读取和修改,C#也不允许我在同一个Bitmap对象上启动多个线程。
有没有办法避免C#引发此错误?或者说在我的Bitmap对象上运行多个线程是不可能的?
谢谢,
Pierre-Olivier

1
你的术语有些混淆,你是如何“在对象上运行线程”的?到底是什么阻止了你的线程访问共享变量?请发布你说不起作用的代码。 - dcastro
可能是C#多线程图像处理的重复问题。 - Junaith
这个主题建议使对位图对象的访问线程安全。实际上,我想打破这种“安全性”,因为正如我在问题中所说的那样,同一个像素不可能被不同的线程访问和修改。 - PinkPR
这与C#或像素无关,Image类防止两个线程同时访问位图。这是一项基本限制,必须避免代码死亡的微锁定性能问题,您将不得不解决它。 - Hans Passant
2个回答

22

使用LockBits(比GetPixelSetPixel更快)可以将图像的像素复制到缓冲区,对其运行并行线程,然后再将缓冲区复制回来。

这里是一个可工作的示例。

void Test()
{
    string inputFile = @"e:\temp\a.jpg";
    string outputFile = @"e:\temp\b.jpg";

    Bitmap bmp = Bitmap.FromFile(inputFile) as Bitmap;

    var rect = new Rectangle(0, 0, bmp.Width, bmp.Height);
    var data = bmp.LockBits(rect, ImageLockMode.ReadWrite, bmp.PixelFormat);
    var depth = Bitmap.GetPixelFormatSize(data.PixelFormat) / 8; //bytes per pixel

    var buffer = new byte[data.Width * data.Height * depth];

    //copy pixels to buffer
    Marshal.Copy(data.Scan0, buffer, 0, buffer.Length);

    Parallel.Invoke(
        () => {
            //upper-left
            Process(buffer, 0, 0, data.Width / 2, data.Height / 2, data.Width, depth);
        },
        () => {
            //lower-right
            Process(buffer, data.Width / 2, data.Height / 2, data.Width, data.Height, data.Width, depth);
        }
    );

    //Copy the buffer back to image
    Marshal.Copy(buffer, 0, data.Scan0, buffer.Length);

    bmp.UnlockBits(data);

    bmp.Save(outputFile, ImageFormat.Jpeg);
}

void Process(byte[] buffer, int x, int y, int endx, int endy, int width, int depth)
{
    for (int i = x; i < endx; i++)
    {
        for (int j = y; j < endy; j++)
        {
            var offset = ((j * width) + i) * depth;
            // Dummy work    
            // To grayscale (0.2126 R + 0.7152 G + 0.0722 B)
            var b = 0.2126 * buffer[offset + 0] + 0.7152 * buffer[offset + 1] + 0.0722 * buffer[offset + 2];
            buffer[offset + 0] = buffer[offset + 1] = buffer[offset + 2] = (byte)b;
        }
    }
}

输入图像:

enter image description here

输出图像:

enter image description here

一些简单的测试:

双核 2.1GHz 的计算机上将一个 (4100万像素,[7152x5368]) 图像转换为灰度图像:

  • GetPixel / SetPixel - 单核 - 131 秒
  • LockBits - 单核 - 4.5 秒
  • LockBits - 双核 - 3 秒

2
非常感谢您的回答。那正是我最终所做的。我刚看到您的消息,所以我已经自己找到了解决方案,但是没有时间早些发布。再次感谢! - PinkPR
1
我刚找到了这个答案。它在我的MPI.Net应用程序中完美地运行。 - user2151486
如何在安卓系统中实现相同的功能? - Ihdina

1
另一个解决方案是创建一个临时容器,其中包含有关Bitmap的信息,例如宽度、高度、步幅、缓冲区和像素格式。一旦需要并行访问Bitmap,只需根据临时容器中的信息创建它即可。
为此,我们需要一个扩展方法来获取Bitmap的缓冲区和步幅:
public static Tuple<IntPtr, int> ToBufferAndStride(this Bitmap bitmap)
{
    BitmapData bitmapData = null;

    try
    {
        bitmapData = bitmap.LockBits(new Rectangle(0, 0, bitmap.Width, bitmap.Height), 
            ImageLockMode.ReadOnly, bitmap.PixelFormat);

        return new Tuple<IntPtr, int>(bitmapData.Scan0, bitmapData.Stride);
    }
    finally
    {
        if (bitmapData != null)
            bitmap.UnlockBits(bitmapData);
    }
}

这个扩展方法将在临时容器内使用:
public class BitmapContainer
{
    public PixelFormat Format { get; }

    public int Width { get; }

    public int Height { get; }

    public IntPtr Buffer { get; }

    public int Stride { get; set; }

    public BitmapContainer(Bitmap bitmap)
    {
        if (bitmap == null)
            throw new ArgumentNullException(nameof(bitmap));

        Format = bitmap.PixelFormat;
        Width = bitmap.Width;
        Height = bitmap.Height;

        var bufferAndStride = bitmap.ToBufferAndStride();
        Buffer = bufferAndStride.Item1;
        Stride = bufferAndStride.Item2;
    }

    public Bitmap ToBitmap()
    {
        return new Bitmap(Width, Height, Stride, Format, Buffer);
    }
}

现在你可以在并行执行的方法中使用BitmapContainer
BitmapContainer container = new BitmapContainer(bitmap);

Parallel.For(0, 10, i =>
{
    Bitmap parallelBitmap = container.ToBitmap();
    // ...
});

这个安全吗?目前我没有发现任何问题,它工作得非常好。但是...为什么这不是每个人所有时候的解决方案呢? - Pangamma

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接