Allow an Image to be accessed by several threads Allow an Image to be accessed by several threads multithreading multithreading

Allow an Image to be accessed by several threads


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.


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();    // ...});