I'd like to, in code and on demand, convert a 32-bit RGBA Image object (originally a 32-bit PNG) to its 32-bit grayscale counterpart.
I've already read several other questions here, as well as many articles online. I've tried using ColorMatrix to do it, but it doesn't seem to handle the alpha very well. Pixels that are entirely opaque grayscale perfectly. Any pixel that is partially transparent seems not to translate well as there are still tinges of color in those pixels. It is enough to be noticeable.
The ColorMatrix I use is as follows:
new System.Drawing.Imaging.ColorMatrix(new float[][]{
new float[] {0.299f, 0.299f, 0.299f, 0, 0},
new float[] {0.587f, 0.587f, 0.587f, 0, 0},
new float[] {0.114f, 0.114f, 0.114f, 0, 0},
new float[] { 0, 0, 0, 1, 0},
new float[] { 0, 0, 0, 0, 1}
});
This is, as I've read, a pretty standard NTSC weighted matrix. I then use it, along with Graphics.DrawImage, but as I said the partially transparent pixels are still colored. I should point out this is displaying the Image object via a WinForms PictureBox on a white background. Could it be perhaps just the way PictureBox's draw their images and handle the transparent parts? The background colors are not affecting it (the tinge of color is from the original image for sure), but perhaps PictureBox isn't redrawing the transparent pixels correctly?
I've seen some methods that use a FormatConvertedBitmap along with an OpacityMask. I haven't tried it, mainly because I'd really prefer not to have to import PresentationCore.dll (not to mention that means it won't work in .NET 2.0 limited apps). Surely the basic System.Drawing.* stuff can do this simple procedure? Or not?
Are you by any chance painting the image onto itself using a ColorMatrix? That won't work of course (because if you paint something semi-transparent-gray over a green pixel, some green will shine through). You need to paint it onto a new, empty bitmap containing only transparent pixels.
Thanks to danbystrom's idle curiosity, I was indeed redrawing on top of the original. For anyone interested, here's the corrected method I used:
using System.Drawing;
using System.Drawing.Imaging;
public Image ConvertToGrayscale(Image image)
{
Image grayscaleImage = new Bitmap(image.Width, image.Height, image.PixelFormat);
// Create the ImageAttributes object and apply the ColorMatrix
ImageAttributes attributes = new System.Drawing.Imaging.ImageAttributes();
ColorMatrix grayscaleMatrix = new ColorMatrix(new float[][]{
new float[] {0.299f, 0.299f, 0.299f, 0, 0},
new float[] {0.587f, 0.587f, 0.587f, 0, 0},
new float[] {0.114f, 0.114f, 0.114f, 0, 0},
new float[] { 0, 0, 0, 1, 0},
new float[] { 0, 0, 0, 0, 1}
});
attributes.SetColorMatrix(grayscaleMatrix);
// Use a new Graphics object from the new image.
using (Graphics g = Graphics.FromImage(grayscaleImage))
{
// Draw the original image using the ImageAttributes created above.
g.DrawImage(image,
new Rectangle(0, 0, grayscaleImage.Width, grayscaleImage.Height),
0, 0, grayscaleImage.Width, grayscaleImage.Height,
GraphicsUnit.Pixel,
attributes);
}
return grayscaleImage;
}
If you convert the image to TGA, an uncompressed imaeg format you can use "RubyPixels" to edit the pixel data directly, doing whatever you please. You can then convert it back to PNG.
I recomend doing to conversion with ImageMagick, also from ruby.
Related
How can I get the color of a Rectangle after creating it?
I'm using this code to create them :
SolidBrush sb = new SolidBrush(Color.Red);
Graphics g = panel1.CreateGraphics();
Rectangle[] rects = { new Rectangle(0, 0, 15, 15), new Rectangle(16, 16, 15, 15), new Rectangle(16, 0, 15, 15), new Rectangle(0, 16, 15, 15) };
g.FillRectangles(sb,rects);
And now I want to get the color of the 3rd rectangle
rects[2] = ....
Is it possible to get this? And it should return Color.Red.
You can find the position of center pixel of your rectangle, then you can use GetPixel method to get information about such as color.
Color pixelColor = myBitmap.GetPixel(50, 50);
The FillRectangles draws the rectangles with the brush (color) you passed. There is no reference from any rectangle to any color. It just executes a drawing command to a Graphics object.
A Rectangle doesn't have a color. So, nope, you cannot. If you explain why you would need it, there might be other solutions to get the desired results.
If you store the image, you only store the result (i.e. the grid of pixels). There is no information on how you created this image.
E.g. based on the bitmap image alone, you cannot differentiate between two adjacent squares (new Rectangle(0, 0, 15, 15), new Rectangle(15, 0, 15, 15) and a single rectangle that has twice the width (new Rectangle(0, 0, 30, 15)).
So the short answer to your question is no, you cannot do that.
Unless you store your information (the rectangles you drew) separately and then use that to find the appropriate pixel on the image (and this only works in simple cases - if you overlapped an earlier rectangle, it's going to be impossible)
But if you're going to store the rectangle information anyway, you might as well store its color and then you don't need to reverse engineer the image anymore. So the answer remains that you cannot do this based on an image alone.
You could write a new Class, for example:
class ColorRectangles
{
public Color color;
public Rectangle[] rects;
public ColorRectangle(Rectangle[] rects, Color color) : base(x, y, width, height)
{
this.color = color
this.rects = rects;
}
}
and then set the Rectangles Color with g.FillRectangles(new SolidBrush(Rectangle.color), Rectangle.rects);
I'm trying to create a real grayscale image, but when my designer checks the file he is saying that the image remains RGB.
You can see the photoshop print here:
I've tried 2 methods so far:
1) Using the MakeGrayScale3 method:
public static Image MakeGrayscale3(Image original)
{
//create a blank bitmap the same size as original
Bitmap newBitmap = new Bitmap(original.Width, original.Height);
newBitmap.SetResolution(original.HorizontalResolution, original.VerticalResolution);
//get a graphics object from the new image
Graphics g = Graphics.FromImage(newBitmap);
//create the grayscale ColorMatrix
ColorMatrix colorMatrix = new ColorMatrix(
new float[][]
{
new float[] {.3f, .3f, .3f, 0, 0},
new float[] {.59f, .59f, .59f, 0, 0},
new float[] {.11f, .11f, .11f, 0, 0},
new float[] {0, 0, 0, 1, 0},
new float[] {0, 0, 0, 0, 1}
});
//create some image attributes
ImageAttributes attributes = new ImageAttributes();
//set the color matrix attribute
attributes.SetColorMatrix(colorMatrix);
//draw the original image on the new image
//using the grayscale color matrix
g.DrawImage(original, new Rectangle(0, 0, original.Width, original.Height),
0, 0, original.Width, original.Height, GraphicsUnit.Pixel, attributes);
//dispose the Graphics object
g.Dispose();
return newBitmap;
}
2) Using the ImageMagick.Net library with this code:
using (MagickImage image = new MagickImage(file))
{
image.ColorSpace = ColorSpace.GRAY;
image.Write(originalFile);
}
If anyone had this problem before or have a clue of what to do to change that..
Thanks!
Original Image:
Result Image (ImageMagick):
Result Image (MakeGrayscale3):
The grayscale format used by jpeg isn't available in the classic System.Drawing classes. It is, however, available in System.Windows.Media. You need to add PresentationCore and WindowsBase as references for using them (this will not be portable on Linux).
public static void SaveAsGrayscaleJpeg(String loadPath, String savePath)
{
using (FileStream fs = new FileStream(savePath, FileMode.Create))
{
BitmapSource img = new BitmapImage(new Uri(new FileInfo(loadPath).FullName));
FormatConvertedBitmap convertImg = new FormatConvertedBitmap(img, PixelFormats.Gray8, null, 0);
JpegBitmapEncoder encoder = new JpegBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(convertImg));
encoder.Save(fs);
}
}
As a bonus, this does the grayscale conversion automatically.
If you're actually starting from Image/Bitmap class, there are some solutions around for converting between them.
Also you should pay attention to:
https://learn.microsoft.com/en-us/dotnet/core/compatibility/core-libraries/6.0/system-drawing-common-windows-only
Thanks for all answers but after read the topic that Hans mentioned there they talk about the FreeImageNET library that I'm using to solve my problem.
Using this code I've managed to save the image as a grayscale image:
FreeImageAPI.FreeImage.ConvertTo8Bits(newimg)
The issue is this line:
Bitmap newBitmap = new Bitmap(original.Width, original.Height);
If you look at the MSDN page it says:
This constructor creates a Bitmap with a PixelFormat enumeration value of Format32bppArgb.
So, you're just creating another color image. What you need to do is to use a different constructor to force a different format:
Bitmap newBitmap = new Bitmap( original.Width, original.Height,
PixelFormat.Format8bppIndexed );
But then that gives you another issue - you can't paint to bitmaps with that pixel format with a Graphics object. There is a PixelFormat.Format16bppGrayScale which appears to work even less.
So, my suggestion would be to to this manually. Use LockBits to get access to the raw bitmap and do the conversion yourself. It is a little bit more work (i.e. you can't use the ColorMatrix stuff, and you have to take care to check if the source bitmap is 24 bit or 32 bit), but it will work just fine.
One issue to be aware of is that it's not a "real" grayscale format, it's 8 bit indexed. So you will also have to create a palette. Simply map each of the 256 values to the same, i.e. 0 -> 0, 100 -> 100, 255 -> 255.
I am trying to draw a crosshair ("plus sign") with inverted colors over an image to show the location of a selected point within the image. This is how I do it:
private static void DrawInvertedCrosshair(Graphics g, Image img, PointF location, float length, float width)
{
float halfLength = length / 2f;
float halfWidth = width / 2f;
Rectangle absHorizRect = Rectangle.Round(new RectangleF(location.X - halfLength, location.Y - halfWidth, length, width));
Rectangle absVertRect = Rectangle.Round(new RectangleF(location.X - halfWidth, location.Y - halfLength, width, length));
ImageAttributes attributes = new ImageAttributes();
float[][] invertMatrix =
{
new float[] {-1, 0, 0, 0, 0 },
new float[] { 0, -1, 0, 0, 0 },
new float[] { 0, 0, -1, 0, 0 },
new float[] { 0, 0, 0, 1, 0 },
new float[] { 1, 1, 1, 0, 1 }
};
ColorMatrix matrix = new ColorMatrix(invertMatrix);
attributes.SetColorMatrix(matrix, ColorMatrixFlag.Default, ColorAdjustType.Bitmap);
g.DrawImage(img, absHorizRect, absHorizRect.X, absHorizRect.Y, absHorizRect.Width, absHorizRect.Height, GraphicsUnit.Pixel, attributes);
g.DrawImage(img, absVertRect, absVertRect.X, absVertRect.Y, absVertRect.Width, absVertRect.Height, GraphicsUnit.Pixel, attributes);
}
It works as expected, however, it is really slow. I want the user to be able to move the selected location around with their mouse by setting the location to the cursor's location whenever it moves. Unfortunately, on my computer, it can update only around once per second for big images.
So, I am looking for an alternative to using Graphics.DrawImage to invert a region of an image. Are there any ways to do this with speeds proportional to the selected region area rather than the entire image area?
Sounds to me you are focusing on the wrong problem. Painting the image is slow, not painting the "cross-hairs".
Large images can certainly be very expensive when you don't help. And System.Drawing makes it very easy to not help. Two basic things you want to do to make the image paint faster, getting it more than 20 times faster is quite achievable:
avoid forcing the image painting code to rescale the image. Instead do it just once so the image can be drawn directly one-to-one without any rescaling. Best time to do so is when you load the image. Possibly again in the control's Resize event handler.
pay attention to the pixel format of the image. The fastest one by a long shot is the pixel format that's directly compatible with the way the image needs to be stored in the video adapter. So the image data can be directly copied to video RAM without having to adjust each individual pixel. That format is PixelFormat.Format32bppPArgb on 99% of all modern machines. Makes a huge difference, it is ten times faster than all the other ones.
A simple helper method that accomplishes both without otherwise dealing with the aspect ratio:
private static Bitmap Resample(Image img, Size size) {
var bmp = new Bitmap(size.Width, size.Height, System.Drawing.Imaging.PixelFormat.Format32bppPArgb);
using (var gr = Graphics.FromImage(bmp)) {
gr.DrawImage(img, new Rectangle(Point.Empty, size));
}
return bmp;
}
Draw the image once on Graphics g, then draw the crosshair on Graphics g directly instead of the image. You can optionally keep track of the places the user clicked so as to save them either in the image or elsewhere as needed.
I would like to use Graphics.FillRectangle or Graphics.Clear with semi-transparent colour (e.g. ARGB=128,0,0,0) to darken entire area of the Graphics object except some specified (rectangular) area:
I know this can be achieved with four FillRectangle calls, but I wonder if it cannot be done easier.
I am aware of simple clipping (Graphics.SetClip), but this allows me to clip everything outside the specified area and I would like to achieve the opposite.
Dim img = Bitmap.FromFile("C:\Users\Public\Pictures\Sample Pictures\Desert.jpg")
Using gfx = Graphics.FromImage(img)
Dim r = New Rectangle(100, 150, 50, 50)
gfx.SetClip(r, Drawing2D.CombineMode.Exclude)
Using b = New SolidBrush(Color.FromArgb(128, 0, 0, 0))
gfx.FillRectangle(b, New Rectangle(0, 0, img.Width, img.Height))
End Using
Me.PictureBox1.Image = img
End Using
I have two images layered ontop of each other and want to be able to clear a section of the top image. Normally if I wanted to clear a section of an image I would just paint it the background color by doing
g.FillRectangle(Brushes.White,x,y,width,height);
but if I do that on the top image that area of the bottom image gets covered by the white rectangle. I tried doing
g.FillRectangle(Brushes.Transparent,x,y,width,height);
but that does not seem to clear the region of all of it's previous contents. Is there any way I can make the pixels in that region transparent?
//using System.Drawing.Drawing2D;
g.FillRectangle(Brushes.White,x,y,width,height);
g.CompositingMode = CompositingMode.SourceCopy;
g.FillRectangle(Brushes.Transparent,x,y,width,height);
This is not possible.
GDI+ and the Graphics class do not support layered drawing; once you overwrite the previous image, those pixels are gone.
You should re-draw the portion of the previous image that you want to appear by calling a DrawImage overload that takes two rectangles.
If the lower image contains transparent portions, you should first clear that area to white (or whatever your original background is) by calling FillRectangle so that the transparency is overlaid correctly.
Another option is not to paint the images directly. Use:
System.Windows.Forms.PictureBox
and it's property
Region
to change the visibility/transaparency of the image. Region need not to be rectangle. It may be defined from any set of lines.
PS: Brushes.Transparent doesn't really mean transparent, but BackColor of the parent container.
float[][] ptsArray ={
new float[] {1, 0, 0, 0, 0},
new float[] {0, 1, 0, 0, 0},
new float[] {0, 0, 1, 0, 0},
new float[] {0, 0, 0, 0.5f, 0},
new float[] {0, 0, 0, 0, 1}};
ColorMatrix clrMatrix = new ColorMatrix(ptsArray);
ImageAttributes imgAttributes = new ImageAttributes();
imgAttributes.SetColorMatrix(clrMatrix,
ColorMatrixFlag.Default,
ColorAdjustType.Bitmap);
_ImageThumb.Height, imgAttributes);
e.Graphics.DrawImage(_ImageThumb,new Rectangle(0, 0, _ImageThumb.Width,_ImageThumb.Height),0, 0, _ImageThumb.Width, _ImageThumb.Height,GraphicsUnit.Pixel, imgAttributes);
//use set clip & region to draw