cropping an area from BitmapData with C#

13,230

Solution 1

I whipped up a quick (and admittedly rough) manual solution that demonstrates how to do this using locked bitmaps. It should be considerably faster than the alternative methods, but does involve a lot more code.

        Bitmap bmp = new Bitmap(@"C:\original.jpg");
        Rectangle rect = new Rectangle(0, 0, bmp.Width, bmp.Height);
        BitmapData rawOriginal = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);

        int origByteCount = rawOriginal.Stride * rawOriginal.Height;
        byte[] origBytes = new Byte[origByteCount];
        Marshal.Copy(rawOriginal.Scan0, origBytes, 0, origByteCount);

        //I want to crop a 100x100 section starting at 15, 15.
        int startX = 15;
        int startY = 15;
        int width = 100;
        int height = 100;
        int BPP = 4;        //4 Bpp = 32 bits, 3 = 24, etc.

        byte[] croppedBytes = new Byte[width * height * BPP];

        //Iterate the selected area of the original image, and the full area of the new image
        for (int i = 0; i < height; i++)
        {
            for (int j = 0; j < width * BPP; j += BPP)
            {
                int origIndex = (startX * rawOriginal.Stride) + (i * rawOriginal.Stride) + (startY * BPP) + (j);
                int croppedIndex = (i * width * BPP) + (j);

                //copy data: once for each channel
                for (int k = 0; k < BPP; k++)
                {
                    croppedBytes[croppedIndex + k] = origBytes[origIndex + k];
                }
            }
        }

        //copy new data into a bitmap
        Bitmap croppedBitmap = new Bitmap(width, height);
        BitmapData croppedData = croppedBitmap.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb);
        Marshal.Copy(croppedBytes, 0, croppedData.Scan0, croppedBytes.Length);

        bmp.UnlockBits(rawOriginal);
        croppedBitmap.UnlockBits(croppedData);

        croppedBitmap.Save(@"C:\test.bmp");

I used this original image:

original

To output this image, cropped to 100x100 @ 15,15:

cropped

Obviously if you use this code, you'll want to clean it up a bit and add error handling. If I understand your question correctly, doing things this way should eliminate the need to use AForge at all.

Solution 2

Fopedush's answer benefits greatly when we subsitute Marshal.copy with memcpy, because that way we don't have to copy it through a byte[] array. That way the memory gets copied only once, instead of three times!

[DllImport("msvcrt.dll", CallingConvention = CallingConvention.Cdecl)]
static unsafe extern int memcpy(byte* dest, byte* src, long count);

static public Bitmap cropBitmap(Bitmap sourceImage, Rectangle rectangle)
{
    const int BPP = 4; //4 Bpp = 32 bits; argb
    var sourceBitmapdata = sourceImage.LockBits(rectangle, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
    var croppedImage = new Bitmap(rectangle.Width, rectangle.Height, PixelFormat.Format32bppArgb);
    var croppedBitmapData = croppedImage.LockBits(new Rectangle(0, 0, rectangle.Width, rectangle.Height), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb);
    unsafe
    {
        croppedBitmapData.Stride = sourceBitmapdata.Stride;
        byte* sourceImagePointer = (byte*)sourceBitmapdata.Scan0.ToPointer();
        byte* croppedImagePointer = (byte*)croppedBitmapData.Scan0.ToPointer();
        memcpy(croppedImagePointer, sourceImagePointer,
               Math.Abs(croppedBitmapData.Stride) * rectangle.Height);
    }
    sourceImage.UnlockBits(sourceBitmapdata);
    croppedImage.UnlockBits(croppedBitmapData);
    return croppedImage;
}

My results are:

BitmapClone: 1823 ms
LockBits: 4857 ms
Rectangle: 1479 ms
My method: 559 ms
My method with LockBits on source image done only once (before loop): 160 ms

I don't have AForge so I haven't included that, but by looking on op's results it would be slower than this. I was testing cropping the image in half.

Please note, that if we would exchange memcpy with:

for (int k = 0; k < Math.Abs(croppedBitmapData.Stride) * rectangle.Height; k++)
     *(croppedImagePointer++) = *(sourceImagePointer++);

it gets 10x slower!

Solution 3

I am a new user and can't vote yet, otherwise I would have upvoted Korwin80's answer as it provides the most efficient working solution, in my opinion. trakos' solution may execute faster but yields scrambled images, at least for me. Here is how I applied Korwin80's solution, with some minor improvements, in my own code:

[DllImport("msvcrt.dll", CallingConvention = CallingConvention.Cdecl)]
private unsafe static extern int memcpy(byte* dest, byte* src, long count);

private unsafe Bitmap Crop(Bitmap srcImg, Rectangle rectangle)
{
    if ((srcImg.Width == rectangle.Width) && (srcImg.Height == rectangle.Height))
        return srcImg;

    var srcImgBitmapData = srcImg.LockBits(new Rectangle(0, 0, srcImg.Width, srcImg.Height), ImageLockMode.ReadOnly, srcImg.PixelFormat);
    var bpp = srcImgBitmapData.Stride / srcImgBitmapData.Width; // 3 or 4
    var srcPtr = (byte*)srcImgBitmapData.Scan0.ToPointer() + rectangle.Y * srcImgBitmapData.Stride + rectangle.X * bpp;
    var srcStride = srcImgBitmapData.Stride;

    var dstImg = new Bitmap(rectangle.Width, rectangle.Height, srcImg.PixelFormat);
    var dstImgBitmapData = dstImg.LockBits(new Rectangle(0, 0, dstImg.Width, dstImg.Height), ImageLockMode.WriteOnly, dstImg.PixelFormat);
    var dstPtr = (byte*)dstImgBitmapData.Scan0.ToPointer();
    var dstStride = dstImgBitmapData.Stride;

    for (int y = 0; y < rectangle.Height; y++)
    {
        memcpy(dstPtr, srcPtr, dstStride);
        srcPtr += srcStride;
        dstPtr += dstStride;
    }

    srcImg.UnlockBits(srcImgBitmapData);
    dstImg.UnlockBits(dstImgBitmapData);
    return dstImg;
}

Solution 4

this class gets your bitmap obj . then lockbits. in ctor. When you call crop method, it uses memcpy to copy the desired region to new bmp.

lockbits: tells the garbage collector to NOT move my bits anywhere, cuz im gonna modify it by pointers (scan0).

memcpy : fastest copy. can copy memory blocks. optimized by some experts.

why memcpy fast?

instead of copying byte by byte, (widthheight) times memory access . memcpy does it block by block, much more less than wh times .

internal unsafe sealed class FastImageCroper : IDisposable
{
    private readonly Bitmap _srcImg;
    private readonly BitmapData _srcImgBitmapData;
    private readonly int _bpp;
    private readonly byte* _srtPrt;

    public FastImageCroper(Bitmap srcImg)
    {
        _srcImg = srcImg;
        _srcImgBitmapData = srcImg.LockBits(new Rectangle(0, 0, srcImg.Width, srcImg.Height), ImageLockMode.ReadOnly, srcImg.PixelFormat);
        _bpp = _srcImgBitmapData.Stride / _srcImgBitmapData.Width; // == 4
        _srtPrt = (byte*)_srcImgBitmapData.Scan0.ToPointer();
    }

    public Bitmap Crop(Rectangle rectangle)
    {
        Bitmap dstImg = new Bitmap(rectangle.Width, rectangle.Height, _srcImg.PixelFormat);
        BitmapData dstImgBitmapData = dstImg.LockBits(new Rectangle(0, 0, dstImg.Width, dstImg.Height), ImageLockMode.WriteOnly, dstImg.PixelFormat);
        byte* dstPrt = (byte*)dstImgBitmapData.Scan0.ToPointer();
        byte* srcPrt = _srtPrt + rectangle.Y*_srcImgBitmapData.Stride + rectangle.X*_bpp;

        for (int y = 0; y < rectangle.Height; y++)
        {
            int srcIndex =  y * _srcImgBitmapData.Stride;
            int croppedIndex = y * dstImgBitmapData.Stride;
            memcpy(dstPrt + croppedIndex, srcPrt + srcIndex, dstImgBitmapData.Stride);
        }

        dstImg.UnlockBits(dstImgBitmapData);
        return dstImg;
    }


    public void Dispose()
    {
        _srcImg.UnlockBits(_srcImgBitmapData);
    }


    [DllImport("msvcrt.dll", CallingConvention = CallingConvention.Cdecl)]
    private static extern int memcpy(byte* dest, byte* src, long count);
}

Solution 5

You can try something like this:

public static Bitmap CropBitmap(Bitmap bitmap, int x, int y, int w, int h)
{
   Rectangle rect = new Rectangle(x, y, w, h);
   Bitmap cropped = bitmap.Clone(rect, bitmap.PixelFormat);
   return cropped;
}

And do something like this in yout code (sample):

var croppedImagem = CropBitmap(dataOriginal, 0, 0, 100, 100); 

I hope it helps!

Share:
13,230
Alex
Author by

Alex

Updated on June 12, 2022

Comments

  • Alex
    Alex almost 2 years

    I have a bitmap sourceImage.bmp

    locking it's bits:

    BitmapData dataOriginal = sourceImage.LockBits(new Rectangle(0, 0, sourceImage.Width, sourceImage.Height), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
    

    Do analysis, get a clone:

    Bitmap originalClone = AForge.Imaging.Image.Clone(dataOriginal);
    

    unlocking bits:

    sourceImage.UnlockBits(dataOriginal);
    

    is it possible to specify which part of "dataOriginal" to copy (x,y,w,h)? or to create new data from the dataOriginal, specifying X and Y coordinates as well as H and W?

    The aim is to copy a small area from this image. This method might be faster than DrawImage, that's why I don't use the latter.

    Edit:

    So I took 29 Mb bitmap and did some hardcore testing! Full-size crop (basically a copy) + 100 iterations.

    http://i.minus.com/ibmcUsT1qUGw6f.png

    Code:

    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data;
    using System.Drawing;
    using System.Linq;
    using System.Text;
    using System.Windows.Forms;
    using AForge;
    using AForge.Imaging;
    using System.Diagnostics;
    using System.Drawing.Imaging;
    using System.IO;
    using System.Runtime.InteropServices;
    
    
    namespace testCropClone
    {
        public partial class Form1 : Form
        {
            public Form1()
            {
                InitializeComponent();
            }
    
            private unsafe Bitmap Clone(Bitmap bmp, int startX, int startY, int width, int height)
            {
            Rectangle rect = new Rectangle(0, 0, bmp.Width, bmp.Height);
            BitmapData rawOriginal = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
    
            int origByteCount = rawOriginal.Stride * rawOriginal.Height;
            byte[] origBytes = new Byte[origByteCount];
            Marshal.Copy(rawOriginal.Scan0, origBytes, 0, origByteCount);
    
            int BPP = 4;        //4 Bpp = 32 bits, 3 = 24, etc.
    
            byte[] croppedBytes = new Byte[width * height * BPP];
    
            //Iterate the selected area of the original image, and the full area of the new image
            for (int i = 0; i < height; i++)
            {
                for (int j = 0; j < width * BPP; j += BPP)
                {
                    int origIndex = (startX * rawOriginal.Stride) + (i * rawOriginal.Stride) + (startY * BPP) + (j);
                    int croppedIndex = (i * width * BPP) + (j);
    
                    //copy data: once for each channel
                    for (int k = 0; k < BPP; k++)
                    {
                        croppedBytes[croppedIndex + k] = origBytes[origIndex + k];
                    }
                }
            }
    
            //copy new data into a bitmap
            Bitmap croppedBitmap = new Bitmap(width, height);
            BitmapData croppedData = croppedBitmap.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb);
            Marshal.Copy(croppedBytes, 0, croppedData.Scan0, croppedBytes.Length);
    
            bmp.UnlockBits(rawOriginal);
            croppedBitmap.UnlockBits(croppedData);
    
            return croppedBitmap;
            }
    
            private Bitmap cloneBitmap(Bitmap bmp, int startX, int startY, int width, int height)
            {
                Rectangle srcRect = Rectangle.FromLTRB(startX, startY, width, height);
                Bitmap cloneBitmap = bmp.Clone(srcRect, bmp.PixelFormat);
                return cloneBitmap;
            }
    
    
            private Bitmap cloneRectangle(Bitmap bmp, int startX, int startY, int width, int height)
            {
                Rectangle srcRect = Rectangle.FromLTRB(startX, startY, width, height);
                Bitmap dest = new Bitmap(srcRect.Width, srcRect.Height);
                Rectangle destRect = new Rectangle(0, 0, srcRect.Width, srcRect.Height);
                using (Graphics graphics = Graphics.FromImage(dest))
                {
                    graphics.DrawImage(bmp, destRect, srcRect, GraphicsUnit.Pixel);
                }
                return dest;
            }
    
    
            private Bitmap cloneAforge(Bitmap bmp, int startX, int startY, int width, int height)
            {
                BitmapData rawOriginal = bmp.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
                Bitmap cloneBitmap = AForge.Imaging.Image.Clone(rawOriginal);
                bmp.UnlockBits(rawOriginal);
                return cloneBitmap;
            }
    
    
    
            private void button1_Click(object sender, EventArgs e)
            {
                Bitmap source = new Bitmap(@"C:\9\01.bmp");
    
                Stopwatch s1 = Stopwatch.StartNew();
                for (int i = 0; i < 100; i++)
                {
                    Bitmap Clone1 = cloneAforge(source, 0, 0, source.Width, source.Height);
                    Clone1.Dispose();
    
                }
    
                /*Bitmap Clone1 = cloneAforge(source, 0, 0, source.Width, source.Height);
                Clone1.Save(@"C:\9\01_aforge.bmp");
                Clone1.Dispose();*/
    
                s1.Stop();
                source.Dispose();
                textBox1.Text = ("" + s1.ElapsedMilliseconds / 100 + " ms");
            }
    
            private void button2_Click(object sender, EventArgs e)
            {
                Bitmap source = new Bitmap(@"C:\9\01.bmp");
    
                Stopwatch s1 = Stopwatch.StartNew();
                for (int i = 0; i < 100; i++)
                {
                    Bitmap Clone1 = cloneBitmap(source, 0, 0, source.Width, source.Height);
                    Clone1.Dispose();
    
                }
    
                /*Bitmap Clone1 = cloneBitmap(source, 0, 0, source.Width, source.Height);
                Clone1.Save(@"C:\9\01_bitmap.bmp");
                Clone1.Dispose();*/
    
                s1.Stop();
    
    
                source.Dispose();
                textBox2.Text = ("" + s1.ElapsedMilliseconds / 100 + " ms");
            }
    
            private void button3_Click(object sender, EventArgs e)
            {
                Bitmap source = new Bitmap(@"C:\9\01.bmp");
    
                Stopwatch s1 = Stopwatch.StartNew();
                for (int i = 0; i < 100; i++)
                {
                    Bitmap Clone1 = Clone(source, 0, 0, source.Width, source.Height);
                    Clone1.Dispose();
    
                }
    
                /*Bitmap Clone1 = Clone(source, 0, 0, source.Width, source.Height);
                Clone1.Save(@"C:\9\01_bits.bmp");
                Clone1.Dispose();*/
    
                s1.Stop();
                source.Dispose();
                textBox3.Text = ("" + s1.ElapsedMilliseconds / 100 + " ms");
            }
    
            private void button4_Click(object sender, EventArgs e)
            {
                Bitmap source = new Bitmap(@"C:\9\01.bmp");
    
                Stopwatch s1 = Stopwatch.StartNew();
                for (int i = 0; i < 100; i++)
                {
                    Bitmap Clone1 = cloneRectangle(source, 0, 0, source.Width, source.Height);
                    Clone1.Dispose();
    
                }
    
    
                /*Bitmap Clone1 = cloneRectangle(source, 0, 0, source.Width, source.Height);
                Clone1.Save(@"C:\9\01_rect.bmp");
                Clone1.Dispose();*/
    
    
                s1.Stop();
                source.Dispose();
                textBox4.Text = ("" + s1.ElapsedMilliseconds / 100 + " ms");
            }
        }
    }
    

    Edit2: (Aforge full-size Crop..) method Nr. 2

            for (int i = 0; i < 100; i++)
            {
                Crop crop = new Crop(new Rectangle(0, 0, source.Width, source.Height));
                var source2 = crop.Apply(source);
                source2.Dispose();
    
            }
    

    Average = 62ms (40ms less that 1st Aforge approach)

    Results:

    1. BitmapClone (0 ms) ?? (cheating, isn't it?)
    2. Aforge #2 (65 ms)
    3. Aforge #1 (105 ms)
    4. Rectangle (170 ms)
    5. Lock Bits (803 ms) (waiting for fixes/new test results..)
  • Alex
    Alex about 12 years
    "var croppedImagem = CropBitmap(dataOriginal, 0, 0, 100, 100);" - programs doesn't work that way.. you can't put (BitmapData) into (Bitmap) field, also this method is similar to DrawImage. I'm planning on cropping up to 30 images at the same time. Both DrawImage and Bitmap.Clone are very cpu consuming/slow so I wondered If I can just crop BitmapData.
  • Felipe Oriani
    Felipe Oriani about 12 years
    Sorry, you should pass a Bitmap instance and not BitmapData instance. I've tested this code here and it works fine, of couse you must have an image on right size of the rectangle size you want to cut. If you want to crop up a lot of images I would recommend you star it on thread to get performance. You cannot cut an imag in 0,0,100,100 if this image is 20x20 pixels. Can you take the Bitmap and ingnore BitmapData? Why do you need the BitmapData instance?
  • Alex
    Alex about 12 years
    first of all, thank you for your time and effort writing this code! Secondly, I did some testing but unfortunately this solution has the worst results! ;) feel free to criticize my testing approach.. maybe I made a mistake or two. (see Edit)
  • Fopedush
    Fopedush about 12 years
    Interesting, I did not expect that. I'll do a little testing of my own and see if my results agree with yours.
  • Alex
    Alex about 12 years
    that would be nice! I tried to make test-framework as simple as possible to exclude serious mistakes (full code in Edit), but maybe you'll get other results using different approach. Looking forward to it!
  • JBeurer
    JBeurer over 11 years
    Bitmap.Clone is VERY, VERY slow if you want to take big image and crop it into small 8x8 images.
  • trakos
    trakos over 10 years
    Hey, I've stumbled upon this question and I don't understand two things: why do you call bmp.LockBits for rectangle covering entire image, and not just rect? And why would you Marshal.Copy it, if you can read from the pointer directly? I;ve already wrote for myself code that does lock only needed area and copy it directly from pointer without marshal (to avoid weird strides), but all code I've seen seems to do it your way, and I'm just wondering if there's any reason for that, apart from this method being tricky.
  • Andrew Simpson
    Andrew Simpson over 10 years
    @trakos Hi, would you a sample code for your method that I could try please?
  • Andrew Simpson
    Andrew Simpson over 10 years
    @Fopedush Hi, I used an image of 1455x352 and I tried to extract a rectangle of 873,0,291,172 but I get 'Index out of range exception on this line : croppedBytes[croppedIndex + k] = origBytes[origIndex + k];
  • trakos
    trakos over 10 years
    @AndrewSimpson I've added an answer below, I've stitched it together right now from two of my methods, so there might be some bugs.
  • Honza Zidek
    Honza Zidek about 9 years
    Welcome to Stack Overflow. Maybe you may read stackoverflow.com/help/how-to-answer first to make your answer better compliant with the SO standards.
  • leigero
    leigero about 9 years
    While this was nice of you, SO isn't a freelance coding community. You should explain how this helps the OP and then provide relevant code. Just pasting working code doesn't really answer a question so much as it provides freelance work.
  • Kosmo零
    Kosmo零 over 8 years
    For some weird reason I getting AccessViolationException at memcpy saying something about read/write to protected memory.
  • trakos
    trakos over 8 years
    Maybe you need to adjust BPP constant? In my example it assumes 32bit, (argb), for 24bit (rgb) use 3. AccessViolationException means you're trying to access memory out of bounds.
  • Kosmo零
    Kosmo零 over 8 years
    I don't think it will help since it assigned, but never used.
  • Asmodiel
    Asmodiel about 7 years
    @Fopedush as Annualily12 pointed out to me ( but has too little reputation yet ;) ), startX and startY are switched in your code. This seems to be the reason why @AndrewSimpson had the index out of range exception.
  • yoel halb
    yoel halb about 5 years
    You are using a byte pointer while memcpy is using a UINT aligned pointer, see stackoverflow.com/q/19187562/640195
  • yoel halb
    yoel halb about 5 years
    @Kosmos The reason why it throws an error is because he updates the destination stride to equals the source stride, however this is wrong and it can only pass when the destination width is the same as the source width.
  • yoel halb
    yoel halb about 5 years
    Besides of this, this code assumes that the destination X and Y will be the same as the source X and Y as it doesn't compensate for the translation. For these reasons avoid using the code in this answer and use instead the code in the answer of trakos below
  • РСИТ _
    РСИТ _ over 4 years
    For those, who want to use this code: change int origIndex = (startX * rawOriginal.Stride) + (i * rawOriginal.Stride) + (startY * BPP) + (j); to int origIndex = (startY * rawOriginal.Stride) + (i * rawOriginal.Stride) + (startX * BPP) + (j);
  • Oak_3260548
    Oak_3260548 almost 3 years
    I think this is a good solution, just defining BPP as bytes-per-pixel is a bit of a problem. What would it be for 1 bit per pixel (monochrome picture)? That's why, in such scenarioes, it is usually defined as a bpp - bits per pixel. So 3 BPP = 24 bpp.
  • VladStepu2001
    VladStepu2001 about 2 years
    Why you are not fixing your code? startX and startY are swapped (as said in the comments).