cropping an area from BitmapData with C#
Asked Answered
E

5

8

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.

https://static.mcmap.net/file/mcmap/ZG-Ab5ovKRkQaVlQc7LQWRft/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..)
Elmira answered 13/3, 2012 at 16:53 Comment(4)
The answer is yes, but you're going to have to write a function to do if if you want it to be fast. You'll have to create a new bitmapdata of the desired size, and iterate over the original data as bytes, copying into a new byte array, which you can then marshal into the new bitmapData.Carothers
This question is extremely insightful. I'm trying to crop up a big image into many smaller 8x8 ones. Graphics.DrawImage() and Bitmap.Clone() methods are extremely slow for that.Overheat
You are right, Bitmap clone is cheating since you didn't do anything with the clone, it does a copy on write so as long as you did no changes to the clone it does not copy at all.Claussen
Lock Bits is slow because of the Marshal call, as some have suggested in the answersClaussen
C
8

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.

Carothers answered 13/3, 2012 at 20:15 Comment(11)
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)Elmira
Interesting, I did not expect that. I'll do a little testing of my own and see if my results agree with yours.Carothers
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!Elmira
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.Wertheimer
@Wertheimer Hi, would you a sample code for your method that I could try please?Ainslee
@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];Ainslee
@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.Wertheimer
@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.Lambert
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);Oblige
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.Cribbing
Why you are not fixing your code? startX and startY are swapped (as said in the comments).Gifted
W
3

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!

Wertheimer answered 3/10, 2013 at 0:58 Comment(6)
For some weird reason I getting AccessViolationException at memcpy saying something about read/write to protected memory.Effluent
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.Wertheimer
I don't think it will help since it assigned, but never used.Effluent
You are using a byte pointer while memcpy is using a UINT aligned pointer, see https://mcmap.net/q/800610/-understanding-the-implementation-of-memcpy/640195Claussen
@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.Claussen
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 belowClaussen
L
2

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!

Leshalesher answered 13/3, 2012 at 17:12 Comment(3)
"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.Elmira
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?Leshalesher
Bitmap.Clone is VERY, VERY slow if you want to take big image and crop it into small 8x8 images.Overheat
G
2

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);
}
Gantz answered 20/2, 2015 at 19:30 Comment(2)
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.Thaddeusthaddus
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.Carnage
R
2

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;
}
Redtop answered 31/5, 2015 at 19:21 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.