I receive images of the same size but with different amounts of information. Examples below (red borders are mine). The background is always white.
I am trying to detect where the information on the image ends - at what pixel height (and crop accordingly). In other words, find the first non-white pixel from the bottom.
Is there a better way to do this other than extract BitmapData out of Image object and loop through all the pixels?
Just to add a suggestion having looked over your images and your solution (below) and your method is fine but you may be able to improve efficiency.
The more you know about your image the better; you're confident the background is always white (according to your post, the code is a more generic utility but the following suggestion can still work); can you be confident on the furthest point in a non-white pixel will be found if the row is not empty?
For example; in your two pictures the furthest in non-white pixel on a row is about 60px in. If this is universally true for your data then you don't need to scan the whole line of the image, which would make your for loop:
for (int y = bitmap.Height - 1; y >= 0; y--) {
for (int x = 0; x < 60; x++) {
Color color = bitmap.GetPixel(x, y);
if (color.R != backColor.R || color.G != backColor.G || color.B != backColor.B) {
foundContentOnRow = y;
break;
}
}
}
(You could make it a parameter on the function so you can easily control it if needed).
Imagine for example that the first non-white row was 80px down. To find it currently you do 640 x 300 = 192,000 checks. If you could confidently say that you would know a row was blank within 100 pixels (an over-estimate based on the data presented) then this would be 100 * 300 = 30,000 checks per image.
If you always knew that the first 10 pixels of the image were always blank you could shave a little bit more off (say 3000 checks).
Musing on a setup where you knew that the first non-white pixel was between 10 and 60 pixels in (range of 50) you could find it at row 80 in 50 x 300 = 15,000 checks which is a good reduction.
Of course the downside about assumptions is that if things change your assumptions may not be valid, but if the data is going to remain fairly constant then it may be worthwhile, especially if you do this for a lot of images.
I've ended up using the following code to trim the image. Hopefully someone finds this useful.
class Program {
static void Main(string[] args) {
Image full = Image.FromFile("foo.png");
Image cropped = full.TrimOnBottom();
}
}
public static class ImageUtilities {
public static Image TrimOnBottom(this Image image, Color? backgroundColor = null, int margin = 30) {
var bitmap = (Bitmap)image;
int foundContentOnRow = -1;
// handle empty optional parameter
var backColor = backgroundColor ?? Color.White;
// scan the image from the bottom up, left to right
for (int y = bitmap.Height - 1; y >= 0; y--) {
for (int x = 0; x < bitmap.Width; x++) {
Color color = bitmap.GetPixel(x, y);
if (color.R != backColor.R || color.G != backColor.G || color.B != backColor.B) {
foundContentOnRow = y;
break;
}
}
// exit loop if content found
if (foundContentOnRow > -1) {
break;
}
}
if (foundContentOnRow > -1) {
int proposedHeight = foundContentOnRow + margin;
// only trim if proposed height smaller than existing image
if (proposedHeight < bitmap.Height) {
return CropImage(image, bitmap.Width, proposedHeight);
}
}
return image;
}
private static Image CropImage(Image image, int width, int height) {
Rectangle cropArea = new Rectangle(0, 0, width, height);
Bitmap bitmap = new Bitmap(image);
return bitmap.Clone(cropArea, bitmap.PixelFormat);
}
}
Related
I am wondering why would this piece of code NOT generate a checkerboard pattern?
pbImage.Image = new Bitmap(8, 8);
Bitmap bmp = ((Bitmap)pbImage.Image);
byte[] bArr = new byte[64];
int currentX = 0;
int currentY = 0;
Color color = Color.Black;
do
{
currentY = 0;
do
{
bmp.SetPixel(currentX, currentY, color);
if (color == Color.Black) color = Color.White; else color = Color.Black;
currentY++;
} while (currentY < bitmapHeight);
currentX++;
} while (currentX < bitmapWidth);
pbImage.Refresh();
Edit: I realized that i need to expand Bitmaps ctor with
new Bitmap(bitmapWidth, bitmapHeight, PixelFormat.Format8bppIndexed)
and it seems SetPixel does not support Indexed Images and expects a Color.
My point is i want to create raw(pure byte array) grayscale images and show it on a picture box, while keeping it as simple as possible, without using any external libraries.
Your calculation fails, because, if you switch at every pixel, then even lines that start with colour 0 will end on the colour 1, meaning the next line will once again start with colour 0.
0101010101010101
0101010101010101
0101010101010101
0101010101010101
etc...
But since, in X and Y coordinates, any horizontal and vertical movement by 1 pixel across the pattern will change the colour, the actual calculation of whether you want a filled or non-filled pixel can be simplified to (x + y) % 2 == 0.
The checkerboard generating function I put below takes an array of colours as colour palette, and allows you to specify which specific indices from that palette to use as the two colours to use on the pattern. If you just want an image with nothing but a 2-colour palette containing black and white, you can just call it like this:
Bitmap check = GenerateCheckerboardImage(8, 8, new Color[]{Color.Black, Color.White}, 0,1);
The generating function:
public static Bitmap GenerateCheckerboardImage(Int32 width, Int32 height, Color[] colors, Byte color1, Byte color2)
{
Byte[] patternArray = new Byte[width * height];
for (Int32 y = 0; y < height; y++)
{
for (Int32 x = 0; x < width; x++)
{
Int32 offset = x + y * height;
patternArray[offset] = (((x + y) % 2 == 0) ? color1 : color2);
}
}
return BuildImage(patternArray, width, height, width, PixelFormat.Format8bppIndexed, colors, Color.Black);
}
The BuildImage function I used is a general-purpose function I made to convert a byte array to an image. You can find it in this answer.
As explained in the rest of that question and the answers on it, the stride argument is the amount of bytes on each line of the image data. For the constructed 8-bit array we got here, that's simply identical to the width, but when loading it's generally rounded to a multiple of 4, and can contain unused padding bytes. (The function takes care of all that, so the input byte array has no such requirements.)
I have black image with white lines. Is it possible to exclude chunks of whihte pixels, that are smaller than specific number? For example: change color of chunks of pixels that are made from less than 10 pixels from white to black.
Original Image:
Image on the output(small areas of white pixels are removed):
Right now I work with AForge library for C#, but C++ ways of solving this are also apreciated(Open CV, for example). And hint, on how this functionality might be called are also appreciated.
Without worrying to much about your details, it does seem trivially simple
Use bitmap in 32bits and use LockBits to get scanlines and direct pointer access to the array.
Scan every pixel with 2 for loops
Every time you find one that matches your target color, scan left right and up and down (X) Amount of pixels to determine if it matches your requirements,
If it does, leave the pixel, if not change it.
if you wanted more speed you could chuck this all in a parallel workload, also there is probably more you could do with a mask array to save you researching dead paths (just a thought)
Note, Obviously you can smarten this up a bit
Exmaple
// lock the array for direct access
var bitmapData = bitmap.LockBits(Bounds, ImageLockMode.ReadWrite, Bitmap.PixelFormat);
// get the pointer
var scan0Ptr = (int*)_bitmapData.Scan0;
// get the stride
var stride = _bitmapData.Stride / BytesPerPixel;
// local method
void Workload(Rectangle bounds)
{
// this is if synchronous, Bounds is just the full image rectangle
var rect = bounds ?? Bounds;
var white = Color.White.ToArgb();
var black = Color.Black.ToArgb();
// scan all x
for (var x = rect.Left; x < rect.Right; x++)
{
var pX = scan0Ptr + x;
// scan all y
for (var y = rect.Top; y < rect.Bottom; y++)
{
if (*(pX + y * stride ) != white)
{
// this will turn it to monochrome
// so add your threshold here, ie some more for loops
//*(pX + y * Stride) = black;
}
}
}
}
// unlock the bitmap
bitmap.UnlockBits(_bitmapData);
To parallel'ize it
You could use something like this to break your image up into smaller regions
public static List<Rectangle> GetSubRects(this Rectangle source, int size)
{
var rects = new List<Rectangle>();
for (var x = 0; x < size; x++)
{
var width = Convert.ToInt32(Math.Floor(source.Width / (double)size));
var xCal = 0;
if (x == size - 1)
{
xCal = source.Width - (width * size);
}
for (var y = 0; y < size; y++)
{
var height = Convert.ToInt32(Math.Floor(source.Height / (double)size));
var yCal = 0;
if (y == size - 1)
{
yCal = source.Height - (height * size) ;
}
rects.Add(new Rectangle(width * x, height * y, width+ xCal, height + yCal));
}
}
return rects;
}
And this
private static void DoWorkload(Rectangle bounds, ParallelOptions options, Action<Rectangle?> workload)
{
if (options == null)
{
workload(null);
}
else
{
var size = 5 // how many rects to work on, ie 5 x 5
Parallel.ForEach(bounds.GetSubRects(size), options, rect => workload(rect));
}
}
Usage
DoWorkload(Bounds, options, Workload);
I have a bitmap and I am wanting to get the colour values from the pixels but only in certain areas of the image. I am wanting to the get the pixels of a image for the full width and only a bit of the height (say height =1) and then I want to move the position to one down and get the same values.
I am using
for (int i = 0; i < 302; i++)
{
Rectangle cloneRect = new Rectangle(0, i, 514, 1);
System.Drawing.Imaging.PixelFormat format = bm.PixelFormat;
Bitmap cloneBitmap = bm.Clone(cloneRect, format);
bitMapList.Add(cloneBitmap);
}
foreach (Bitmap bmp in bitMapList)
{
c = bmp.GetPixel(514, 1);
r = Convert.ToInt16(c.R);
lumi.Add(r);
}
The for statement to create the areas I want on the bitmap and then the foreach to loop through these bitmaps and then get the values. Only problem is I am getting the error message "Parameter must be positive and < Width."
On the line
c = bmp.GetPixel(514, 1);
anyone know why?
Thanks
You need to make sure that the pixel you are getting is inside of the image (which must not be the case). You could wrap this in a call to run a check first something like:
public static Color GetPixelSafe(Bitmap image, int x, int y) {
if (x >= image.Width) x = image.Width - 1;
else if (x < 0) x = 0;
if (y >= image.Height) y = image.Height - 1;
else if (y < 0) y = 0;
return image.GetPixel(x, y);
}
Now, this is not going to fix your processing algorithm itself, but it should at least fix the exception. One other pointer is that if you are going to be processing lots of color values and performance is a concern you should really consider using image.LockBits instead of GetPixel. For more information on that see here: http://msdn.microsoft.com/en-us/library/5ey6h79d(v=vs.110).aspx.
It seems that 514 is bigger then your image actual Width. How did you come up with that number?
i am currently developing an application that has to process scanned forms. One of the tasks of my application is to determine which kind of form is scanned. There are 3 possible types of forms with a unique background color to identify each kind. The 3 colors that are possible are red/pink, green and blue. The problem i am having is, that my attempts fail to distinguish between the green and blue forms.
Here are links to the green and blue sample files:
http://dl.dropbox.com/u/686228/Image0037.JPG
http://dl.dropbox.com/u/686228/Image0038.JPG
I am using C# .net Application and ImageMagick for some tasks i need to perform.
Currently i am getting color reduced histogram of my scanned form and try to determine which colors are in the form. But my app can't rely distinguish the green and blue ones.
Any advise or maybe a smarter approach would be gladly appreciated.
Thanks,
Erik
I found this rather interesting and dug into it a little deeper.
The code to get the average color of a bitmap found at How to calculate the average rgb color values of a bitmap had problems like some invalid casts and red/blue channels swapped. Here is a fixed version:
private System.Drawing.Color CalculateAverageColor(Bitmap bm)
{
int width = bm.Width;
int height = bm.Height;
int red = 0;
int green = 0;
int blue = 0;
int minDiversion = 15; // drop pixels that do not differ by at least minDiversion between color values (white, gray or black)
int dropped = 0; // keep track of dropped pixels
long[] totals = new long[] { 0, 0, 0 };
int bppModifier = bm.PixelFormat == System.Drawing.Imaging.PixelFormat.Format24bppRgb ? 3 : 4; // cutting corners, will fail on anything else but 32 and 24 bit images
BitmapData srcData = bm.LockBits(new System.Drawing.Rectangle(0, 0, bm.Width, bm.Height), ImageLockMode.ReadOnly, bm.PixelFormat);
int stride = srcData.Stride;
IntPtr Scan0 = srcData.Scan0;
unsafe
{
byte* p = (byte*)(void*)Scan0;
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
int idx = (y * stride) + x * bppModifier;
red = p[idx + 2];
green = p[idx + 1];
blue = p[idx];
if (Math.Abs(red - green) > minDiversion || Math.Abs(red - blue) > minDiversion || Math.Abs(green - blue) > minDiversion)
{
totals[2] += red;
totals[1] += green;
totals[0] += blue;
}
else
{
dropped++;
}
}
}
}
int count = width * height - dropped;
int avgR = (int)(totals[2] / count);
int avgG = (int)(totals[1] / count);
int avgB = (int)(totals[0] / count);
return System.Drawing.Color.FromArgb(avgR, avgG, avgB);
}
Running this function on your input images, however, returned some indistinguishable grayish color for both of them, as already anticipated by Will A in the comments, which is why i'm dropping any colors from the calculation that do not have a difference of at least 15 between R, G and B.
The interesting thing is that the supposedly blue prescription scan averages equal values for G and B (R: 214, G: 237, B: 237). However the green prescription scan resulted in a big difference (18) between the values for G and B (R: 202, G: 232, B: 214) so that might be what you should be looking into. Ex:
if (color.G - color.B > 15) { form.Type = FormTypes.GreenForm }
Workflow outline:
Convert Image into HSL/HSV colorspace
Build histogram of the H channel
The histogram should show a clear peak (for your samples at least) in the blue/green channel
If that's not distinctive enough, you can weight the histogram votes by something to reduce the effect of the white areas (e.g. in the HSV colorspace, weight by S).
I haven't tried this out, but how about resizing the image to 1x1 pixel (which should "average" out all the pixels) and then check the hue of that pixel to see if it closest to red, blue or green.
EDIT
I don't have ImageMagik installed, so I hacked this with GetThumbnailImage:
private static bool ThumbnailCallback()
{
return false;
}
static void Main(string[] args)
{
var blueImage = Image.FromFile("blue.jpg").GetThumbnailImage(1, 1, new Image.GetThumbnailImageAbort(ThumbnailCallback), IntPtr.Zero);
var blueBitmap = new Bitmap(blueImage);
var blueHue = blueBitmap.GetPixel(0, 0).GetHue();
var greenImage = Image.FromFile("green.jpg").GetThumbnailImage(1, 1, new Image.GetThumbnailImageAbort(ThumbnailCallback), IntPtr.Zero);
var greenBitmap = new Bitmap(greenImage);
var greenHue = greenBitmap.GetPixel(0, 0).GetHue();
}
Using your images I got a blueHue value of 169.0909 (Where true blue would be 180), and greenHue equal to 140 (pure green is 120, cyan is 150).
Red forms should be somewhere near 0 or 360.
I know you've already found an answer - just thought I'd give you an alternative.
I'm trying to remove all white or transparent pixels from an image, leaving the actual image (cropped). I've tried a few solutions, but none seem to work. Any suggestions or am I going to spend the night writing image cropping code?
So, what you want to do is find the top, left most non white/transparent pixel and the bottom, right most non white/transparent pixel. These two coordinates will give you a rectangle that you can then extract.
// Load the bitmap
Bitmap originalBitmap = Bitmap.FromFile("d:\\temp\\test.bmp") as Bitmap;
// Find the min/max non-white/transparent pixels
Point min = new Point(int.MaxValue, int.MaxValue);
Point max = new Point(int.MinValue, int.MinValue);
for (int x = 0; x < originalBitmap.Width; ++x)
{
for (int y = 0; y < originalBitmap.Height; ++y)
{
Color pixelColor = originalBitmap.GetPixel(x, y);
if (!(pixelColor.R == 255 && pixelColor.G == 255 && pixelColor.B == 255)
|| pixelColor.A < 255)
{
if (x < min.X) min.X = x;
if (y < min.Y) min.Y = y;
if (x > max.X) max.X = x;
if (y > max.Y) max.Y = y;
}
}
}
// Create a new bitmap from the crop rectangle
Rectangle cropRectangle = new Rectangle(min.X, min.Y, max.X - min.X, max.Y - min.Y);
Bitmap newBitmap = new Bitmap(cropRectangle.Width, cropRectangle.Height);
using (Graphics g = Graphics.FromImage(newBitmap))
{
g.DrawImage(originalBitmap, 0, 0, cropRectangle, GraphicsUnit.Pixel);
}
public Bitmap CropBitmap(Bitmap original)
{
// determine new left
int newLeft = -1;
for (int x = 0; x < original.Width; x++)
{
for (int y = 0; y < original.Height; y++)
{
Color color = original.GetPixel(x, y);
if ((color.R != 255) || (color.G != 255) || (color.B != 255) ||
(color.A != 0))
{
// this pixel is either not white or not fully transparent
newLeft = x;
break;
}
}
if (newLeft != -1)
{
break;
}
// repeat logic for new right, top and bottom
}
Bitmap ret = new Bitmap(newRight - newLeft, newTop - newBottom);
using (Graphics g = Graphics.FromImage(ret)
{
// copy from the original onto the new, using the new coordinates as
// source coordinates for the original
g.DrawImage(...);
}
return ret
}
Note that this function will be slow as dirt. GetPixel() is unbelievably slow, and accessing the Width and Height properties of a Bitmap inside a loop is also slow. LockBits would be the proper way to do this - there are tons of examples here on StackOverflow.
Per-pixel check should do the trick. Scan each line to find empty line from the top & bottom, scan each row to find left & right constraints (this can be done in one pass with either rows or columns). When the constraint is found - copy the part of the image to another buffer.
In WPF we have a WriteableBitmap class. Is this what are you looking for ? If it is the case please have a look at http://blogs.msdn.com/b/jgalasyn/archive/2008/04/17/using-writeablebitmap-to-display-a-procedural-texture.aspx
I found a method to batch trim a few thousand .jpg files in about 10 minutes, but I didn't do it in code. I used the Convert feature of Snag-It Editor. I don't know if this is an option for you, if you need to do this trimming once or your need is ongoing, but for the price of the software, which isn't a whole lot, I considered this a decent workaround.
(I do not work for or represent Techsmith.)
Joey
Adding to this, if you are in WPF and you have excess space around your image, check the properties of the image and make sure your Stretch property is set to fill. This eliminated the space around the image.
Screen shot of the property in WPF