Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Allow an Image to be accessed by several threads

I'm trying to do some image processing in C#. I want to use some threads to do parallel computations on several zones in my image. Threads are actually getting and setting pixels in a Bitmap object. There is absolutely no chance for 2 threads to access the same pixel, so that's not the problem.

The problem is that C# doesnt allow me to start several threads on the same Bitmap object, even if i'm sure that the same pixel won't be read and modified simultaneously.

Is there any way to avoid C# to raise this error ? Or is it just impossible to run several threads on my Bitmap object ?

Thank you,

Pierre-Olivier

like image 801
PinkPR Avatar asked Feb 01 '14 11:02

PinkPR


People also ask

What will happen if multiple threads accessing the same resource?

Multiple threads accessing shared data simultaneously may lead to a timing dependent error known as data race condition. Data races may be hidden in the code without interfering or harming the program execution until the moment when threads are scheduled in a scenario (the condition) that break the program execution.

Can multiple threads access the same array?

The answer is no. Each array element has a region of memory reserved for it alone within the region attributed the overall array. Modifications of different elements therefore do not write to any of the same memory locations.

What resources do threads share?

Resource sharing: Resources like code, data, and files can be shared among all threads within a process. Note: stack and registers can't be shared among the threads. Each thread has its own stack and registers.

How does the operating system support or implement Multithreading?

Kernel threads are supported directly by the operating system. Any application can be programmed to be multithreaded. All of the threads within an application are supported within a single process. The Kernel maintains context information for the process as a whole and for individuals threads within the process.


2 Answers

Using LockBits (which is also much faster than GetPixel & SetPixel) you can copy the image's pixels to a buffer, run parallel threads on it, and then copy the buffer back.

Here is a working example.

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;
        }
    }
}

Input Image:

enter image description here

Output Image:

enter image description here

Some rough tests:

Converting a (41 MegaPixel, [7152x5368]) image to a gray scale on a dual core 2.1GHz machine

  • GetPixel/SetPixel - Single Core - 131 sec.
  • LockBits - Single Core - 4.5 sec.
  • LockBits - Dual Core - 3 sec.
like image 140
L.B Avatar answered Nov 12 '22 11:11

L.B


Another solution would be to create a temporary container with the information about the Bitmap such as width, height, stride, buffer and pixel format. Once you need to access the Bitmap in parallel just create it based on the information in the temporary container.

For this we need an extension method to get the buffer and stride of a 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);
    }
}

This extension method will be used inside the temporary container:

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);
    }
}

Now you can use the BitmapContainer in a method executed in parallel:

BitmapContainer container = new BitmapContainer(bitmap);

Parallel.For(0, 10, i =>
{
    Bitmap parallelBitmap = container.ToBitmap();
    // ...
});
like image 40
niklr Avatar answered Nov 12 '22 12:11

niklr