coverting Image to grayscale (Perceptual image hashes) - c#

I am working on Perceptual image hashes. Firstly, I reduce image size to remove high frequencies. Then, I shrink image to (8*8) so that there are 64 total pixels. I use the following lines of code.
private void button1_Click(object sender, EventArgs e)
{
Image img = pictureBox1.Image;
img = resizeImage(img, new Size(8,8));
pictureBox2.Image = img;
}
public static Image resizeImage(Image imgToResize, Size size)
{
return (Image)(new Bitmap(imgToResize, size));
}
Now I want to reduce color so the tiny (8x8) picture is converted to a gray scale and want to changes the hash from 64 pixels (64 red, 64 green, and 64 blue) to 64 total colors. Here, I'm stuck.

If you're going for 'perceptual' hashes, and only need to process 64 values, it may be interesting to use a more advanced algorithm. The theory behind it is roughly explained here.
Gray = (0.2126×Red2.2 + 0.7152×Green2.2 + 0.0722×Blue2.2)1/2.2
As code, this would become
public static Byte GetGreyValue(Byte red, Byte green, Byte blue)
{
Double redFactor = 0.2126d * Math.Pow(red, 2.2d);
Double grnFactor = 0.7152d * Math.Pow(green, 2.2d);
Double bluFactor = 0.0722d * Math.Pow(blue, 2.2d);
Double grey = Math.Pow(redFactor + grnFactor + bluFactor, 1d / 2.2);
return (Byte)Math.Max(0, Math.Min(255, Math.Round(grey, MidpointRounding.AwayFromZero)));
}
Now you can just go over all the bytes in your resized image and convert them to gray. Using LockBits, that's not too hard. You just copy the bytes out, iterate over them per four (per ARGB bytes quad), get the RGB components out, throw them into the 'make grey' function, put the result in the RGB spots you took them from, and write the byte array back into your image when you're done.
public static Bitmap GetGreyImage(Image img, Int32 width, Int32 height)
{
// get image data
Bitmap b = new Bitmap(img, width, height);
BitmapData sourceData = b.LockBits(new Rectangle(0, 0, b.Width, b.Height), ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb);
Int32 stride = sourceData.Stride;
Byte[] data = new Byte[stride * b.Height];
// Copy bytes from image into data array
Marshal.Copy(sourceData.Scan0, data, 0, data.Length);
// iterate over array and convert to gray
for (Int32 y = 0; y < height; y++)
{
Int32 offset = y * stride;
for (Int32 x = 0; x < width; x++)
{
// "ARGB" is little-endian Int32, so the actual byte order is B,G,R,A
Byte colB = data[offset + 0]; // B
Byte colG = data[offset + 1]; // G
Byte colR = data[offset + 2]; // R
Int32 colA = data[offset + 3]; // A
// Optional improvement: set pixels to black if color
// is considered too transparent to matter.
Byte grayValue = colA < 128 ? 0 : GetGreyValue(colR, colG, colB);
data[offset + 0] = grayValue; // B
data[offset + 1] = grayValue; // G
data[offset + 2] = grayValue; // R
data[offset + 3] = 0xFF; // A
offset += 4;
}
}
// Copy bytes from data array back into image
Marshal.Copy(data, 0, sourceData.Scan0, data.Length);
b.UnlockBits(sourceData);
return b;
}
If with "change from 64 red, 64 green, and 64 blue to 64 total colors" you mean you want an 8-bit (paletted) image where each byte is one pixel, then you're going to have to simply save the values you generate there in a new byte array instead of writing them back, and then create a new 8x8 Bitmap with the Format8bppIndexed format, open it in a second BitmapData object, and write it in there.
Do note that 8-bit images need a colour palette, so you'll need to go over the standard generated 8-bit palette of the image and change it to a fade from (0,0,0) to (255,255,255) with a for loop.

Related

Replace colored pixels of bitmap image from another image

I have a performance problem.
For a insole model configurator, we have a piece to upload and many material images to fusion with the piece image.
I should replace every white pixel on the piece image by the corresponding pixel on the material image.
As the material image is not a mono color, I cant replace simply all white by another mono color.
Image sizes are the same. So I simply take a pixel if the color is not transparent from the piece image and with the same X and Z coordinates on the material images, I take a pixel and I set the pixel of the piece image.
But as there are many materials, it takes 5 minutes today.
Is there a mor optimised way to do this ?
Here is my method :
//For every material image, calls the fusion method below.
foreach (string material in System.IO.Directory.GetFiles(materialsPath))
{
var result = FillWhiteImages(whiteImagesFolder, whiteImagesFolder + "\\" + System.IO.Path.GetFileName(whiteFilePath), material);
}
private static void FusionWhiteImagesWithMaterials(string whiteImageFolder, string file, string materialImageFile)
{
if (file.ToLower().EndsWith(".db") || materialImageFile.ToLower().EndsWith(".db"))
return;
List<CustomPixel> lstColoredPixels = new List<CustomPixel>();
try
{
Bitmap image = new Bitmap(file);
for (int y = 0; y < image.Height; ++y)
{
for (int x = 0; x < image.Width; ++x)
{
if (image.GetPixel(x, y).A > 0)
{
lstColoredPixels.Add(new CustomPixel(x, y));
}
}
}
Bitmap bmpTemp = new Bitmap(materialImageFile);
Bitmap target = new Bitmap(bmpTemp, new Size(image.Size.Width, image.Size.Height));
for (int y = 0; y < target.Height; y++)
{
for (int x = 0; x < target.Width; x++)
{
Color clr = image.GetPixel(x, y);
if (clr.A > 0)
{
if (clr.R > 200 && clr.G > 200 && clr.B > 200)
image.SetPixel(x, y, target.GetPixel(x, y));
else
image.SetPixel(x, y, Color.Gray);
}
}
}
...
image.Save(...);
}
catch (Exception ex)
{
}
}
//I reduced image sizes to keep on the screen. Real image sizes are 500x1240 px.
Replacing the white is one possibility, but it's not a very pretty one. Based on the images you have there, the ideal solution for this is to get the pattern with the correct alpha applied, and then paint the visible black lines over it. This is actually a process with some more steps:
Extract the alpha from the foot shape image
Extract the black lines from the foot shape image
Apply the alpha to the pattern image
Paint the black lines over the alpha-adjusted pattern image
The way I'd approach this is to extract the data of both images as ARGB byte arrays, meaning, each pixel is four bytes, in the order B, G, R, A. Then, for each pixel, we simply copy the alpha byte from the foot shape image into the alpha byte of the pattern image, so you end up with the pattern image, with the transparency of the foot shape applied to it.
Now, in a new byte array of the same size, which starts with pure 00 bytes (meaning, since A,R,G and B are all zero, transparent black), we construct the black line. Pixels can be considered "black" if they're both not white, and visible. So the ideal result, including smooth fades, is to adjust the alpha of this new image to the minimum value of the alpha and the inverse of the brightness. Since it's grayscale, any of the R, G, B will do for brightness. To get the inverse as byte value, we just take (255 - brightness).
Note, if you need to apply this to a load of images, you probably want to extract the bytes, dimensions and stride of the foot pattern image only once in advance, and keep them in variables to give to the alpha-replacing process. In fact, since the black lines image won't change either, a preprocessing step to generate that should speed things up even more.
public static void BakeImages(String whiteFilePath, String materialsFolder, String resultFolder)
{
Int32 width;
Int32 height;
Int32 stride;
// extract bytes of shape & alpha image
Byte[] shapeImageBytes;
using (Bitmap shapeImage = new Bitmap(whiteFilePath))
{
width = shapeImage.Width;
height = shapeImage.Height;
// extract bytes of shape & alpha image
shapeImageBytes = GetImageData(shapeImage, out stride, PixelFormat.Format32bppArgb);
}
using (Bitmap blackImage = ExtractBlackImage(shapeImageBytes, width, height, stride))
{
//For every material image, calls the fusion method below.
foreach (String materialImagePath in Directory.GetFiles(materialsFolder))
{
using (Bitmap patternImage = new Bitmap(materialImagePath))
using (Bitmap result = ApplyAlphaToImage(shapeImageBytes, width, height, stride, patternImage))
{
if (result == null)
continue;
// paint black lines image onto alpha-adjusted pattern image.
using (Graphics g = Graphics.FromImage(result))
g.DrawImage(blackImage, 0, 0);
result.Save(Path.Combine(resultFolder, Path.GetFileNameWithoutExtension(materialImagePath) + ".png"), ImageFormat.Png);
}
}
}
}
The black lines image:
public static Bitmap ExtractBlackImage(Byte[] shapeImageBytes, Int32 width, Int32 height, Int32 stride)
{
// Create black lines image.
Byte[] imageBytesBlack = new Byte[shapeImageBytes.Length];
// Line start offset is set to 3 to immediately get the alpha component.
Int32 lineOffsImg = 3;
for (Int32 y = 0; y < height; y++)
{
Int32 curOffs = lineOffsImg;
for (Int32 x = 0; x < width; x++)
{
// copy either alpha or inverted brightness (whichever is lowest)
// from the shape image onto black lines image as alpha, effectively
// only retaining the visible black lines from the shape image.
// I use curOffs - 1 (red) because it's the simplest operation.
Byte alpha = shapeImageBytes[curOffs];
Byte invBri = (Byte) (255 - shapeImageBytes[curOffs - 1]);
imageBytesBlack[curOffs] = Math.Min(alpha, invBri);
// Adjust offset to next pixel.
curOffs += 4;
}
// Adjust line offset to next line.
lineOffsImg += stride;
}
// Make the black lines images out of the byte array.
return BuildImage(imageBytesBlack, width, height, stride, PixelFormat.Format32bppArgb);
}
The processing to apply the foot image's transparency to the pattern image:
public static Bitmap ApplyAlphaToImage(Byte[] alphaImageBytes, Int32 width, Int32 height, Int32 stride, Bitmap texture)
{
Byte[] imageBytesPattern;
if (texture.Width != width || texture.Height != height)
return null;
// extract bytes of pattern image. Stride should be the same.
Int32 patternStride;
imageBytesPattern = ImageUtils.GetImageData(texture, out patternStride, PixelFormat.Format32bppArgb);
if (patternStride != stride)
return null;
// Line start offset is set to 3 to immediately get the alpha component.
Int32 lineOffsImg = 3;
for (Int32 y = 0; y < height; y++)
{
Int32 curOffs = lineOffsImg;
for (Int32 x = 0; x < width; x++)
{
// copy alpha from shape image onto pattern image.
imageBytesPattern[curOffs] = alphaImageBytes[curOffs];
// Adjust offset to next pixel.
curOffs += 4;
}
// Adjust line offset to next line.
lineOffsImg += stride;
}
// Make a image out of the byte array, and return it.
return BuildImage(imageBytesPattern, width, height, stride, PixelFormat.Format32bppArgb);
}
The helper function to extract the bytes from an image:
public static Byte[] GetImageData(Bitmap sourceImage, out Int32 stride, PixelFormat desiredPixelFormat)
{
Int32 width = sourceImage.Width;
Int32 height = sourceImage.Height;
BitmapData sourceData = sourceImage.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.ReadOnly, desiredPixelFormat);
stride = sourceData.Stride;
Byte[] data = new Byte[stride * height];
Marshal.Copy(sourceData.Scan0, data, 0, data.Length);
sourceImage.UnlockBits(sourceData);
return data;
}
The helper function to make a new image from a byte array:
public static Bitmap BuildImage(Byte[] sourceData, Int32 width, Int32 height, Int32 stride, PixelFormat pixelFormat)
{
Bitmap newImage = new Bitmap(width, height, pixelFormat);
BitmapData targetData = newImage.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.WriteOnly, newImage.PixelFormat);
// Get actual data width.
Int32 newDataWidth = ((Image.GetPixelFormatSize(pixelFormat) * width) + 7) / 8;
Int32 targetStride = targetData.Stride;
Int64 scan0 = targetData.Scan0.ToInt64();
// Copy per line, copying only data and ignoring any possible padding.
for (Int32 y = 0; y < height; ++y)
Marshal.Copy(sourceData, y * stride, new IntPtr(scan0 + y * targetStride), newDataWidth);
newImage.UnlockBits(targetData);
return newImage;
}
The result in my test tool:
As you see, the black lines are preserved on top of the pattern.
GetPixel/SetPixel are notoriously slow due to locking and other overhead accessing the pixels. To improve performance you will need to use some unmanaged coding to access the data directly.
This answer should shows an example on how to improve speed when working with bitmaps.
Here is some (untested!) code adapted from that anwer:
public static unsafe Image MergeBitmaps(Bitmap mask, Bitmap background)
{
Debug.Assert(mask.PixelFormat == PixelFormat.Format32bppArgb);
BitmapData maskData = mask.LockBits(new Rectangle(0, 0, mask.Width, mask.Height),
ImageLockMode.ReadWrite, mask.PixelFormat);
BitmapData backgroundData = background.LockBits(new Rectangle(0, 0, background.Width, background.Height),
ImageLockMode.ReadWrite, background.PixelFormat);
try
{
byte bytesPerPixel = 4;
/*This time we convert the IntPtr to a ptr*/
byte* maskScan0 = (byte*)maskData.Scan0.ToPointer();
byte* backgroundScan0 = (byte*)backgroundData.Scan0.ToPointer();
for (int i = 0; i < maskData.Height; ++i)
{
for (int j = 0; j < maskData.Width; ++j)
{
byte* maskPtr = maskScan0 + i * maskData.Stride + j * bytesPerPixel;
byte* backPtr = backgroundScan0 + i * backgroundData.Stride + j * bytesPerPixel;
//maskPtr is a pointer to the first byte of the 4-byte color data
//maskPtr[0] = blueComponent;
//maskPtr[1] = greenComponent;
//maskPtr[2] = redComponent;
//maskPtr[3] = alphaComponent;
if (maskPtr[3] > 0 )
{
if (maskPtr[2] > 200 &&
maskPtr[1] > 200 &&
maskPtr[0] > 200)
{
maskPtr[3] = 255;
maskPtr[2] = backPtr[2];
maskPtr[1] = backPtr[1];
maskPtr[0] = backPtr[0];
}
else
{
maskPtr[3] = 255;
maskPtr[2] = 128;
maskPtr[1] = 128;
maskPtr[0] = 128;
}
}
}
}
return mask;
}
finally
{
mask.UnlockBits(maskData);
background.UnlockBits(backgroundData);
}
}
}
I found this solution, it is much more faster.
But it uses too much resources.
Parallel programing in C# came to my help :
//I called my method in a parallel foreach
Parallel.ForEach(System.IO.Directory.GetFiles(materialsPath), filling =>
{
var result = FillWhiteImages(whiteImagesFolder, whiteImagesFolder + "\\" + System.IO.Path.GetFileName(whiteFilePath), filling);
});
//Instead of a classic foreach loop like this.
foreach (string material in System.IO.Directory.GetFiles(materialsPath))
{
var result = FillWhiteImages(whiteImagesFolder, whiteImagesFolder + "\\" + System.IO.Path.GetFileName(whiteFilePath), material);
}

How can I save an image as Bitmap Image File?

I made an Automatic image thresholding function, and wanna to save it as a bitmap file.
However, when I use the Bitmap.Save function of C# GDI+, although I set the ImageFormat as BMP, it always as the RGB color image file but not bitmap image file.
I must save it as the bitmap image file for the printer only can read the bitmap image file.
Maybe you will ask me what the bitmap image file is. I am not an expert of image processing and sorry about that can hardly explain clearly. But I can quote an example: in Photoshop, there are several color mode, such as RGB mode/CMYK mode/Index mode/Grayscale mode/Bitmap mode, I want to save the image as the Bitmap mode in C#.
Here is what Adobe explain about the Bitmap mode in their website:
Bitmap mode uses one of two color values (black or white) to represent the pixels in an image. Images in Bitmap mode are called bitmapped 1‑bit images because they have a bit depth of 1.
I googled but found nothing about this. How can I do it in C#? Thank you.
Here is my code:
Thread T = new Thread(() => {
Bitmap processedBitmap = new Bitmap(#"G:\\0001.jpg");
BitmapData bitmapData = processedBitmap.LockBits(new Rectangle(0, 0, processedBitmap.Width, processedBitmap.Height), ImageLockMode.ReadWrite, processedBitmap.PixelFormat);
int bytesPerPixel = Bitmap.GetPixelFormatSize(processedBitmap.PixelFormat) / 8;
int byteCount = bitmapData.Stride * processedBitmap.Height;
byte[] pixels = new byte[byteCount];
IntPtr ptrFirstPixel = bitmapData.Scan0;
Marshal.Copy(ptrFirstPixel, pixels, 0, pixels.Length);
int heightInPixels = bitmapData.Height;
int widthInBytes = bitmapData.Width * bytesPerPixel;
for (int y = 0; y < heightInPixels; y++)
{
int currentLine = y * bitmapData.Stride;
for (int x = 0; x < widthInBytes; x = x + bytesPerPixel)
{
int oldBlue = pixels[currentLine + x];
int oldGreen = pixels[currentLine + x + 1];
int oldRed = pixels[currentLine + x + 2];
double averageColor = (oldBlue + oldGreen + oldRed) / 3;
int NewC;
if (averageColor > 200)
{
NewC = 255;
}
else
{
NewC = 0;
}
// calculate new pixel value
pixels[currentLine + x] = (byte)NewC;
pixels[currentLine + x + 1] = (byte)NewC;
pixels[currentLine + x + 2] = (byte)NewC;
}
}
// copy modified bytes back
Marshal.Copy(pixels, 0, ptrFirstPixel, pixels.Length);
processedBitmap.UnlockBits(bitmapData);
processedBitmap.Save("G:\\aaa.bmp", ImageFormat.Bmp);
MessageBox.Show("Sucess!");
});
T.Start();
I believe the OP is referring to the last type of image in this adobe link
Bitmap is merely a container for data, the format of the data that you are storing is defined by the PixelFormat setting. As can be seen "Adobe" Bitmap mode is a 2 color format mode and corresponds to PixelFormat.Format1bppIndexed in C# Bitmap.
You have a couple of constructors for Bitmaps which have the PixelFormat as a parameter.
1.
public Bitmap (int width, int height, System.Drawing.Imaging.PixelFormat format);
2.
public Bitmap (int width, int height, int stride, System.Drawing.Imaging.PixelFormat format, IntPtr scan0);
With your source image you have a 24 bit image.
When you do your colour averaging, you're writing back to the image buffer with the following code:
pixels[currentLine + x] = (byte)NewC;
pixels[currentLine + x + 1] = (byte)NewC;
pixels[currentLine + x + 2] = (byte)NewC;
You're writing back 24 bits again.
So for example if your original values for RGB were (202, 203, 249), then NewC would be 218, and then you threshold it back to 255, so you write back (255,255,255) which is still an RGB value, it's just for white.
Then you save that image using
processedBitmap.Save("G:\\aaa.bmp", ImageFormat.Bmp);
The ImageFormat class just sets the type of image, like jpeg, png, etc.
And as you've discovered, you still have a 24 bit image being output.
So what you want is to save the image as a pure 1 bit per pixel black and white image.
To do this you need to specify the PixelFormat of the image you're saving, and specifically you want the PixelFormat Format1bppIndexed.
If you instead change the relevant bit of your code to:
...
Marshal.Copy(pixels, 0, ptrFirstPixel, pixels.Length);
processedBitmap.UnlockBits(bitmapData);
Bitmap clone = processedBitmap.Clone(new Rectangle(0, 0, processedBitmap.Width, processedBitmap.Height), PixelFormat.Format1bppIndexed);
clone.Save("G:\\aaa.bmp", ImageFormat.Bmp);
MessageBox.Show("Success!");
Now your output clone will be a 1bpp image.
However, you can simplify your code even more, because this clone function can actually do all the work for you, and you can reduce your code to just the following.
Bitmap processedBitmap = new Bitmap(#"G:\0001.jpg");
Bitmap clone = processedBitmap.Clone(new Rectangle(0, 0, processedBitmap.Width, processedBitmap.Height), PixelFormat.Format1bppIndexed);
clone.Save("G:\\aaa.bmp", ImageFormat.Bmp);
MessageBox.Show("Success!");
Be aware though that the output is slightly different.
Here are some test samples of the output.
This is my input image:
Output image with your thresholding code:
And output image using just the clone method:
To save a BMP object to a file all you have to do it this:
bmp.Save("c:\\Path\\To\\File\\image.bmp, ImageFormat.Bmp);
Are you doing anything else?

C# How do I convert my get GetPixel / SetPixel color processing to Lockbits?

EDIT: I deeply appreciate the replies. What I need more than anything here is sample code for what I do with the few lines of code in the nested loop, since that's what works right in GetPixel/SetPixel, but also what I can't get to work right using Lockbits. Thank you
I'm trying to convert my image processing filters from GetPixel / SetPixel to Lockbits, to improve processing time. I have seen Lockbits tutorials here on Stack Overflow, MSDN, and other sites as well, but I'm doing something wrong. I'm starting with an exceedingly simple filter, which simply reduces green to create a red and purple effect. Here's my code:
private void redsAndPurplesToolStripMenuItem_Click(object sender, EventArgs e)
{
// Get bitmap from picturebox
Bitmap bmpMain = (Bitmap)pictureBoxMain.Image.Clone();
// search through each pixel via x, y coordinates, examine and make changes. Dont let values exceed 255 or fall under 0.
for (int y = 0; y < bmpMain.Height; y++)
for (int x = 0; x < bmpMain.Width; x++)
{
bmpMain.GetPixel(x, y);
Color c = bmpMain.GetPixel(x, y);
int myRed = c.R, myGreen = c.G, myBlue = c.B;
myGreen -= 128;
if (myGreen < 0) myGreen = 0;
bmpMain.SetPixel(x, y, Color.FromArgb(255, myRed, myGreen, myBlue));
}
// assign the new bitmap to the picturebox
pictureBoxMain.Image = (Bitmap)bmpMain;
// Save a copy to the HD for undo / redo.
string myString = Environment.GetEnvironmentVariable("temp", EnvironmentVariableTarget.Machine);
pictureBoxMain.Image.Save(myString + "\\ColorAppRedo.png", System.Drawing.Imaging.ImageFormat.Png);
}
So that GetPixel / SetPixel code works fine, but it's slow. So I tried this:
private void redsAndPurplesToolStripMenuItem_Click(object sender, EventArgs e)
{
// Get bitmap from picturebox
Bitmap bmpMain = (Bitmap)pictureBoxMain.Image.Clone();
Rectangle rect = new Rectangle(Point.Empty, bmpMain.Size);
BitmapData bmpData = bmpMain.LockBits(rect, ImageLockMode.ReadOnly, bmpMain.PixelFormat);
// search through each pixel via x, y coordinates, examine and make changes. Dont let values exceed 255 or fall under 0.
for (int y = 0; y < bmpMain.Height; y++)
for (int x = 0; x < bmpMain.Width; x++)
{
bmpMain.GetPixel(x, y);
Color c = new Color();
int myRed = c.R, myGreen = c.G, myBlue = c.B;
myGreen -= 128;
if (myGreen < 0) myGreen = 0;
bmpMain.SetPixel(x, y, Color.FromArgb(255, myRed, myGreen, myBlue));
}
bmpMain.UnlockBits(bmpData);
// assign the new bitmap to the picturebox
pictureBoxMain.Image = (Bitmap)bmpMain;
// Save a copy to the HD for undo / redo.
string myString = Environment.GetEnvironmentVariable("temp", EnvironmentVariableTarget.Machine);
pictureBoxMain.Image.Save(myString + "\\ColorAppRedo.png", System.Drawing.Imaging.ImageFormat.Png);
}
Which throws the error "An unhandled exception of type 'System.InvalidOperationException' occurred in System.Drawing.dll Additional information: Bitmap region is already locked" when it reaches the first line of the nested loop.
I realize this has to be a beginner's error, I'd appreciate if someone could demonstrate the correct way to convert this very simple filter to Lockbits. Thank you very much
The array returned by scan0 is in this format BGRA BGRA BGRA BGRA ... and so on,
where B = Blue, G = Green, R = Red, A = Alpha.
Example of a very small bitmap 4 pixels wide and 3 pixels height.
BGRA BGRA BGRA BGRA
BGRA BGRA BGRA BGRA
BGRA BGRA BGRA BGRA
stride = width*bytesPerPixel = 4*4 = 16 bytes
height = 3
maxLenght = stride*height= 16*3 = 48 bytes
To reach a certain pixel in the image (x, y) use this formula
int certainPixel = bytesPerPixel*x + stride * y;
B = scan0[certainPixel + 0];
G = scan0[certainPixel + 1];
R = scan0[certainPixel + 2];
A = scan0[certainPixel + 3];
public unsafe void Test(Bitmap bmp)
{
int width = bmp.Width;
int height = bmp.Height;
//TODO determine bytes per pixel
int bytesPerPixel = 4; // we assume that image is Format32bppArgb
int maxPointerLenght = width * height * bytesPerPixel;
int stride = width * bytesPerPixel;
byte R, G, B, A;
BitmapData bData = bmp.LockBits(
new System.Drawing.Rectangle(0, 0, bmp.Width, bmp.Height),
ImageLockMode.ReadWrite, bmp.PixelFormat);
byte* scan0 = (byte*)bData.Scan0.ToPointer();
for (int i = 0; i < maxPointerLenght; i += 4)
{
B = scan0[i + 0];
G = scan0[i + 1];
R = scan0[i + 2];
A = scan0[i + 3];
// do anything with the colors
// Set the green component to 0
G = 0;
// do something with red
R = R < 54 ? (byte)(R + 127) : R;
R = R > 255 ? 255 : R;
}
bmp.UnlockBits(bData);
}
You can test is yourself. Create a very small bitmap ( few pixels wide/height) in paint or any other program and put a breakpoint at the begining of the method.
Additional information: Bitmap region is already locked"
You now know why GetPixel() is slow, it also uses Un/LockBits under the hood. But does so for each individual pixel, the overhead steals cpu cycles. A bitmap can be locked only once, that's why you got the exception. Also the basic reason that you can't access a bitmap in multiple threads simultaneously.
The point of LockBits is that you can access the memory occupied by the bitmap pixels directly. The BitmapData.Scan0 member gives you the memory address. Directly addressing the memory is very fast. You'll however have to work with an IntPtr, the type of Scan0, that requires using a pointer or Marshal.Copy(). Using a pointer is the optimal way, there are many existing examples on how to do this, I won't repeat it here.
... = bmpMain.LockBits(rect, ImageLockMode.ReadOnly, bmpMain.PixelFormat);
The last argument you pass is very, very important. It selects the pixel format of the data and that affects the code you write. Using bmpMain.PixelFormat is the fastest way to lock but it is also very inconvenient. Since that now requires you to adapt your code to the specific pixel format. There are many, take a good look at the PixelFormat enum. They differ in the number of bytes taken for each pixel and how the colors are encoded in the bits.
The only convenient pixel format is Format32bppArgb, every pixel takes 4 bytes, the color/alpha is encoded in a single byte and you can very easily and quickly address the pixels with an uint*. You can still deal with Format24bppRgb but you now need a byte*, that's a lot slower. The ones that have a P in the name are pre-multiplied formats, very fast to display but exceedingly awkward to deal with. You may thus be well ahead by taking the perf hit of forcing LockBits() to convert the pixel format. Paying attention to the pixel format up front is important to avoid this kind of lossage.

How do I set a color other than black to image created with makeTransparent()

I am using the MakeTransparent() function call on the c# Bitmap object to convert images to transparent images. When this method is called, it will convert a background color to be transparent by setting the alpha channel, but it then converts the background color to black.
I need to find a fast way of converting this background color back to white or whatever the original color was, as occasionally I need to flatten the image to a non-alpha channel enabled format.
Make transparent doesn't seem to have any flags or overloads that allows you to tell it to leave the background color alone, and altering the image pixel by pixel is way to inefficient. Anyone have any suggestions or GDI tricks to solve this problem?
There doesn't seem to be a fast way to do this using the managed code interface. Using individual pixel manipulation, or using unmanaged code to update the pixels seem to be the only real options.
This is actually possible in managed code, by using Marshal.Copy to copy the backing byte array out of a bitmap object, then editing it, and then copying it back.
So basically, with that general method in mind, you just go over the pixels, line by line, detect which pixels have the colour you want replaced, and set their alpha byte to 0.
Note that "ARGB" refers to the order of the components inside the Int32 value of one read pixel. Since this value is little-endian, the actual order of the bytes at a given offset is the reverse; B = offset + 0, G = offset + 1, R = offset + 2, A = offset + 3.
/// <summary>
/// Clears the alpha value of all pixels matching the given colour.
/// </summary>
public static Bitmap MakeTransparentKeepColour(Bitmap image, Color clearColour)
{
Int32 width = image.Width;
Int32 height = image.Height;
// Paint on 32bppargb, so we're sure of the byte data format
Bitmap bm32 = new Bitmap(width, height, PixelFormat.Format32bppArgb);
using (Graphics gr = Graphics.FromImage(bm32))
gr.DrawImage(image, new Rectangle(0, 0, width, height));
BitmapData sourceData = bm32.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.ReadOnly, bm32.PixelFormat);
Int32 stride = sourceData.Stride;
// Copy the image data into a local array so we can use managed functions to manipulate it.
Byte[] data = new Byte[stride * height];
Marshal.Copy(sourceData.Scan0, data, 0, data.Length);
Byte colR = clearColour.R;
Byte colG = clearColour.G;
Byte colB = clearColour.B;
for (Int32 y = 0; y < height; y++)
{
Int32 inputOffs = y * stride;
for (Int32 x = 0; x < width; x++)
{
if (data[inputOffs + 2] == colR && data[inputOffs + 1] == colG && data[inputOffs] == colB)
data[inputOffs + 3] = 0;
inputOffs += 4;
}
}
// Copy the edited image data back.
Marshal.Copy(data, 0, sourceData.Scan0, data.Length);
bm32.UnlockBits(sourceData);
return bm32;
}
This can easily be enhanced with a tolerance level instead of an exact match, with something like Math.Abs(data[inputOffs + 2] - colR) < tolerance, or by actually converting the bytes to a colour object and doing some other kind of approximation (like hue/saturation/brightness).

Locking Bits Fails To Detect Pixels

I am creating a program that scans all the pixels of an image, and whenever it finds a pixel that contains the color pink. It makes the pixel black. But it doesn't seem to find a pink pixel when there is two of them on the image. I do not know if I am using LockBits correctly, maybe I am using it wrong. Can someone please help me solve this I would greatly appreciate it.
Here is the code below:
Bitmap bitmap = pictureBox1.Image as Bitmap;
System.Drawing.Imaging.BitmapData d = bitmap.LockBits(new Rectangle(0, 0, bitmap.Width, bitmap.Height), System.Drawing.Imaging.ImageLockMode.ReadWrite, bitmap.PixelFormat);
IntPtr ptr = d.Scan0;
byte[] rgbs = new byte[Math.Abs(d.Stride) * bitmap.Height];
Marshal.Copy(ptr, rgbs, 0, rgbs.Length);
Graphics g = pictureBox1.CreateGraphics();
for (int index = 2; index < rgbs.Length; index += 3)
{
if (rgbs[index] == 255 && rgbs[index - 1] == 0 && rgbs[index - 2] == 255) // If color = RGB(255, 0, 255) Then ...
{
// This never gets executed!
rgbs[index] = 0;
rgbs[index - 1] = 0;
rgbs[index - 2] = 0;
}
}
Marshal.Copy(rgbs, 0, ptr, rgbs.Length); // Copy rgb values back to the memory location of the bitmap.
pictureBox1.Image = bitmap;
bitmap.UnlockBits(d);
You don't need to copy the pixel data into an array. The point of LockBits is it gives you direct (unsafe) access the memory. You can just iterate the pixels and change them as you find them. You will need to know the format of the image to do this successfully.
BitmapData bmd=bm.LockBits(new Rectangle(0, 0, 10, 10),
ImageLockMode.ReadOnly, bm.PixelFormat);
// Blue, Green, Red, Alpha (Format32BppArgb)
int pixelSize=4;
for(int y=0; y<bmd.Height; y++)
{
byte* row=(byte *)bmd.Scan0+(y*bmd.Stride);
for(int x=0; x<bmd.Width; x++)
{
int offSet = x*pixelSize;
// read pixels
byte blue = row[offSet];
byte green = row[offSet+1];
byte red = row[offSet+2];
byte alpha = row[offSet+3];
// set blue pixel
row[x*pixelSize]=255;
}
}
It's a little more tricky in VB than C# as VB has no knowledge of pointers and requires the use of the marshal class to access unmanaged data. Here's some sample code. (For some reason I originally though this was a VB question).
Dim x As Integer
Dim y As Integer
' Blue, Green, Red, Alpha (Format32BppArgb)
Dim PixelSize As Integer = 4
Dim bmd As BitmapData = bm.LockBits(new Rectangle(0, 0, 10, 10),
ImageLockMode.ReadOnly, bm.PixelFormat)
For y = 0 To bmd.Height - 1
For x = 0 To bmd.Width - 1
Dim offSet As Int32 = (bmd.Stride * y) + (4 * x)
' read pixel data
Dim blue As Byte = Marshal.ReadByte(bmd.Scan0, offSet)
Dim green As Byte = Marshal.ReadByte(bmd.Scan0, offSet + 1)
Dim red As Byte = Marshal.ReadByte(bmd.Scan0, offSet + 2)
Dim alpha As Byte = Marshal.ReadByte(bmd.Scan0, offSet + 3)
' set blue pixel
Marshal.WriteByte(bmd.Scan0, offSet , 255)
Next
Next

Categories

Resources