I've got a situation where I need to resize a large number of images. These images are stored as .jpg files on the file system currently, but I expect to just have byte[] in memory later on in the project. The source image size is variable, but the output should be 3 different predetermined sizes. Aspect ratios should be preserved, padding the original image with white space (ie, a really tall image would be resized to fit within the square target image size, with large areas of white on the left and right).
I initially built the project targeting .NET 2.0, and using System.Drawing classes to perform the load/resize/save. Relevant code includes:
original = Image.FromFile(inputFile); //NOTE: Reused for each of the 3 target sizes
Bitmap resized = new Bitmap(size, size);
//Draw the image to a new image of the intended size
Graphics g = Graphics.FromImage(resized);
g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
g.Clear(Color.White);
g.DrawImage(original, center - width / 2f, center - height / 2f, width, height);
g.Dispose();
//Save the new image to the output path
resized.Save(outputFile, ImageFormat.Jpeg);
I wanted to port this project to .NET 3.5, so tried using the System.Windows.Media classes to perform the same function. I got it working, however performance is terrible; processing time per image is about 50x longer. The vast majority of the time is spent loading the image. Relevant code includes:
BitmapImage original = new BitmapImage(); //Again, reused for each of the 3 target sizes
original.BeginInit();
original.StreamSource = new MemoryStream(imageData); //imageData is a byte[] of the data loaded from a FileStream
original.CreateOptions = BitmapCreateOptions.None;
original.CacheOption = BitmapCacheOption.Default;
original.EndInit(); //Here's where the vast majority of the time is spent
original.Freeze();
// Target Rect for the resize operation
Rect rect = new Rect(center - width / 2d, center - height / 2d, width, height);
// Create a DrawingVisual/Context to render with
DrawingVisual drawingVisual = new DrawingVisual();
using (DrawingContext drawingContext = drawingVisual.RenderOpen())
{
drawingContext.DrawImage(original, rect);
}
// Use RenderTargetBitmap to resize the original image
RenderTargetBitmap resizedImage = new RenderTargetBitmap(
size, size, // Resized dimensions
96, 96, // Default DPI values
PixelFormats.Default); // Default pixel format
resizedImage.Render(drawingVisual);
// Encode the image using the original format and save the modified image
SaveImageData(resizedImage, outputFile);
Am I doing something wrong here, to take so much time? I've tried just using the constructor on BitmapImage that takes a URI, same performance issue there. Anyone done anything like this before, know if there's a more performance-minded way to do this? Or am I just going to need to use System.Drawing still? Thanks!
And after typing all that up, it occurred to me that I could load the symbols from MS for the System.Windows.Media classes, and step through where it was slow. Immediately found the cause, and the solution. The input images were saved with a color profile, and it was attempting to load that color profile (from the file system) of each image. By switching from BitmapCreateOptions.None to BitmapCreateOptions.IgnoreColorProfile in the code above, it no longer does that, and performs just as fast as System.Drawing did.
Hope this helps anyone else that runs into this problem!
You appear to be doing this the hard way. You can let WPF do the work for you by just setting DecodePixelHeight and DecodePixelWidth. This will cause the resize to happen during the image load:
BitmapImage resizedImage = new BitmapImage
{
StreamSource = new MemoryStream(imageData),
CreateOptions = BitmapCreateOptions.IgnoreColorProfile,
DecodePixelHeight = height,
DecodePixelWidth = width,
}
resizedImage.BeginInit(); // Needed only so we can call EndInit()
resizedImage.EndInit(); // This does the actual loading and resizing
imageSaveImageData(resizedImage, outputFile);
I also included the IgnoreColorProfile solution you found in my code.
Update I reread your question and realized the reason you're using DrawingVisual is that you need whitespace around your image to make it square. DecodePixelHeight and DecodePixelWidth would not accomplish that goal, so my solution does not answer your question.
I will leave my answer here in case someone who just needs a resize without whitespace comes across this question.
I think this from the System.Drawing page on MSDN might be relevant:
The System.Drawing namespace provides access to GDI+ basic graphics functionality. More advanced functionality is provided in the System.Drawing.Drawing2D, System.Drawing.Imaging, and System.Drawing.Text namespaces.
The Graphics class provides methods for drawing to the display device. Classes such as Rectangle and Point encapsulate GDI+ primitives. The Pen class is used to draw lines and curves, while classes derived from the abstract class Brush are used to fill the interiors of shapes.
By using System.Drawing you are closer to the actual basic graphics functionality than if you go via System.Windows.Media which:
Defines objects that enable integration of rich media, including drawings, text, and audio/video content within Windows Presentation Foundation (WPF) applications.
System.Drawing is still supported, so I'd stick with that.
I found an interesting situation in your code. Remove using from following line:
using(DrawingContext drawingContext = drawingVisual.RenderOpen())
I'm not sure why this speed up code, but you can give it a try.
Related
I've been trying to figure out a way to resize a PNG image without losing color data from pixels that are completely transparent. Here is the code that I'm using to achieve this:
//sourceImage is also a bitmap read in earlier
using (var scaledBitmap = new Bitmap(desiredWidth, desiredHeight, PixelFormat.Format32bppArgb)){
using (var g = Graphics.FromImage(scaledBitmap)){
var attr = new ImageAttributes();
attr.SetWrapMode(WrapMode.TileFlipXY);
g.CompositingQuality = CompositingQuality.HighQuality;
g.CompositingMode = CompositingMode.SourceCopy;
g.InterpolationMode = InterpolationMode.HighQualityBicubic;
g.SmoothingMode = SmoothingMode.AntiAlias;
g.PixelOffsetMode = PixelOffsetMode.HighQuality;
g.DrawImage(sourceImage, new Rectangle(0, 0, desiredWidth, desiredHeight), 0, 0, sourceImage.Width, sourceImage.Height, GraphicsUnit.Pixel, attr);
}
scaledBitmap.Save(memoryStream, ImageFormat.Png);
//use new bitmap here
}
But the problem with this example is that anywhere that a pixel's alpha value is zero, the pixels become black.
My reputation is too low to post these images inline sadly.
Images
First Image is the image I've been using to test with. For quick reference, I've saved a copy without the alpha channel (Second Image). The entire image is a solid color and the alpha masks part of the image to create a circle.
Third Image is the resulting image from the code above. Again I've saved two copies. In Fourth Image, it is very clear that data is being lost during the conversion. What I'd expect to see is the same single color filling the whole image.
Doing some reading I discovered this in the PNG file specification:
The alpha channel can be regarded either as a mask that temporarily hides transparent parts of the image, or as a means for constructing a non-rectangular image. In the first case, the colour values of fully transparent pixels should be preserved for future use. In the second case, the transparent pixels carry no useful data and are simply there to fill out the rectangular image area required by PNG. In this case, fully transparent pixels should all be assigned the same colour value for best compression.
So it looks like it's up to the PNG encoder to decide how to handle color values where the alpha is zero. Is there any way to tell GDI+'s encoder to not zero out the color data?
The reason I need to keep the color data is for further, dynamic resizing of the image. Without a homogeneous color at the edges of the visible portion of the image, they are terribly prone to ringing artifacts. There's also a post on the Unity forums about this issue.
Please let my know if anyone else has encountered this and how you dealt with it. Also if anyone knows of any libraries that implement a C# PNG encoder that is less draconian it would be greatly appreciated.
Thanks!
While I realize this may not be sufficient for some, in my case the answer was to switch to using ImageMagick's .NET wrapper. I was also able to find a thread on their forums with the exact problem I was experiencing.
It appears that this is a common bug when resizing images and ImageMagick has fixed this issue in their library.
using ImageMagick;
using (var sourceImage = new MagickImage(texture)){ //texture is of type Stream
sourceImage.Resize(desiredWidth, desiredHeight);
sourceImage.Write(memoryStream, MagickFormat.Png);
}
// use memoryStream here
As you can see, this also greatly simplified my code due to ImageMagick's built in support for resize operations.
Without ImageMagick, you can simply use the following code and it works, don't change CompositingQuality or any other Graphics properties :
Bitmap NewImage = new Bitmap(OriginalImage.Width, OriginalImage.Height, OriginalImage.PixelFormat);
using (System.Drawing.Graphics g = System.Drawing.Graphics.FromImage(NewImage)) {
g.DrawImage(OriginalImage, new Rectangle(0, 0, NewWidth, NewHeight), 0, 0, OriginalImage.Width, OriginalImage.Height, GraphicsUnit.Pixel);
}
NewImage.Save("myTransparent.png");
I am trying to create an image based on a string. The image needs to be raster (otherwise it will lose resolution if I need to zoom out). I am using the following code:
Bitmap bitmapimage = new Bitmap(200, 100);
Graphics bitmapGraphics = Graphics.FromImage(bitmapimage );
bitmapGraphics .DrawString("test", new Font("Arial",50), Brushes.Black, new Point(0, 0));
bitmapimage .Save("Image.png", System.Drawing.Imaging.ImageFormat.png);
textPictureBox.Image = bitmapimage ;
What I get is an image like this (after zooming):
Why is this?
I think you misunderstand what "raster" means. Raster images are grids, with one pixel at each grid location. When you zoom on a raster image far enough the grid becomes clearly visible, even with techniques like anti-aliasing.
Vector graphics, on the other hand, are algorithm based. They store instructions on how to reproduce an image on a given canvas. When you zoom a vector image, the image will stay sharp because it's still following the instruction, rather than simply scaling the previous rendering.
All of the major image types (bmp, gif, png, jpeg) are raster types, and do not support vector graphics. The png image is your example is rastered... in fact, it's impossible to create a png image this is not rastered. An example of a vector image is certain font types or Photoshop (psd) files (sort of... in practice, Photoshop files tend to be more raster than vector in the end).
In this case, probably the easiest solution is to draw the image very large in the first place... large enough that you won't need to zoom in, and use a large enough font to fill the space. You also need to make sure that you are using a font that is fully vector-drawn.
Try using Vector Basic graphics, if you using it you will not have problems if you zoom in or out
o Vector Graphics in C# (MSDN)
o Example Project (MSDN)
Vector graphics are scalable, raster are not.
Text fonts are scalable (unless you use bitmap fonts), but once you draw a string on a bitmap this text becomes a raster image, so it can't be scaled anymore. Thus, if you need to draw text on a bitmap try to use a large image and use anti-aliased text (again, this image won't be scalable but if it's large enough there will be no need to zoom it in).
Here is a modified version of your code (the text will be as large as the PNG image):
add the following using:
using System.Drawing.Imaging;
using System.Drawing.Drawing2D;
using System.Drawing.Text;
Add this line into your form (as a private class field):
Random rnd = new Random();
the rest of the code:
int scl = rnd.Next(100, 451);
Bitmap bitmapimage = new Bitmap(2 * scl, scl);
Graphics bitmapGraphics = Graphics.FromImage(bitmapimage);
bitmapGraphics.CompositingMode = CompositingMode.SourceOver;
bitmapGraphics.TextRenderingHint = TextRenderingHint.AntiAlias; // text is now anti-aliased
bitmapGraphics.SmoothingMode = SmoothingMode.HighQuality;
bitmapGraphics.DrawString("test", new Font("Arial", scl * 9 / 10, GraphicsUnit.Pixel), Brushes.Black, new Point(0, 0));
bitmapimage.Save("Image.png", ImageFormat.Png);
bitmapGraphics.Dispose();
By the way, if you draw text on a printer's Graphics object, this text is still scalable since printers don't use pixels (but if you draw a raster image on a printer's Graphics object, this image will get blurry if it's zoomed in).
I have a png image file with alpha blending of its own. Now I want to load it onto a form in a mobile. I tried so many ways but not work. Is there any solution? Thanks in advance.
This is what I use to load the image from resource:
Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("drawObj.Graph.png");
Bitmap myPNGImg = new Bitmap(stream);
Then create new bitmap with same size of the images Graph.png:
Bitmap myBlankImg = new Bitmap(48,48);
Graphics mynewGraph = Graphics.FromImage(myNew);
mynewGraph.Clear(Color.Transparent);
Draw the PNG bitmap: mynewGraph.DrawImage(myPNGImg, 0, 0);
And then something I read from internet:(
Rectangle rectDest = new Rectangle(50,50, 100, 100);
ImageAttributes imgatt = new ImageAttributes();
imgatt.SetColorKey(Color.Transparent, Color.Transparent);
myGraph.DrawImage(myNew, rectDest, 0, 0, 99, 99,GraphicsUnit.Pixel, imgatt);
It works, but just clear the four corner of the images(somekind of rounded rectangle). There's still some white border around the images left.
Loading images using Bitmap on Compact Framework will lose alpha information. Setting the color key is an alternative way of doing transparency where you sacrifice an exact single color as the transparent color.
To use alpha blending on Compact Framework, you can use the helper classes from OpenNETCF to load the PNG file, keeping the alpha information (see Transparency and alpha blending), then P/Invoke AlphaBlend. It's not pretty, but it's what it takes. Also be warned that you will take a heavy performance hit for using alpha blending. To bake some images dynamically, it's fine, but for generic on screen drawing operations, you might want to use another approach.
The problem is that in the CF, filling with Color.Transparent actually fills with white (see these two blog entries). Project Resistance has a very good example of how to do this blending (actually several of them).
To zoom images in and out, there is a possible way to resize the pictureBox and showing image in strechmode. Although I can not use it efficiently becauce in general over 8x it gives storage error [think that a pictureBox has the Size(32k, 32k) it needs over 1GB memory !
Is there a special method, or should I zoom only the seen part of the image by using ImageClone ?
Update:
Here is the project at first try to zoom at the project [impossible, storage error] than delete the 41. line in form.cs :
pictureBox1.Image = youPicture;
After deleting this line, the program will work, please move the zoomed image.
Here is the link: http://rapidshare.com/files/265835370/zoomMatrix.rar.html
By using the matrix object and the transform property of your graphics object:
using(Graphics g = this.CreateGraphics())
{
using(Bitmap youPicture = new Bitmap(yourPictureFile))
{
g.DrawImage(youPicture, 0, 0, 300, 100); //set the desired size
//Now you need to create a matrix object to apply transformation on your graphic
Matrix mat = new Matrix();
mat.Scale(1.5f, 1.5f, MatrixOrder.Append); //zoom to 150%
g.Transform = mat;
g.DrawImage(youPicture, new Rectangle(...), 0, 0, youPicture.Width,
youPicture.Height, GraphicsUnit.Pixel) ;
}
}
I personally would just zoom the visible part as the rest is hidden anyway (and thus no use)
See this answer to an earlier question. You definitely don't want to zoom by making the image huge and showing only part of it - you'll run into the memory problem that you've already encountered. Also, the stretch mode of a picture box doesn't use high-quality interpolation, so the result will look pretty crappy.
In the answer I linked here, I included a link to a C# project that shows you how to do this kind of zooming.
Update: here is a direct link to the downloadable project.
I am using some custom controls one of which is a tooltip controller that can display images, so I am using th ebelow code to instantiate it:
Image newImage = Image.FromFile(imagePath);
e.ToolTipImage = newImage;
obviously could inline it but just testing at the moment. The trouble is the image is sometimes the wrong size, is there a way to set the display size. The only way I can currently see is editing the image using GDI+ or something like that. Seems like a lot of extra processing when I am only wanting to adjust display size not affect the actual image.
Once you have an image object loaded from its source, the Height and Width (and Size, and all ancillary properties) are read-only. Therefore, you are stuck with GDI+ methods for resizing it in RAM and then displaying it accordingly.
There are a lot of approaches you can take, but if you were to encapsulate that out to a library which you could reuse should this problem occur again, you'll be set to go. This isn't exactly optimized (IE, may have some bugs), but should give you an idea of how to approach it:
Image newImage = Image.FromFile(myFilePath);
Size outputSize = new Size(200, 200);
Bitmap backgroundBitmap = new Bitmap(outputSize.Width, outputSize.Height);
using (Bitmap tempBitmap = new Bitmap(newImage))
{
using (Graphics g = Graphics.FromImage(backgroundBitmap))
{
g.InterpolationMode = InterpolationMode.HighQualityBicubic;
// Get the set of points that determine our rectangle for resizing.
Point[] corners = {
new Point(0, 0),
new Point(backgroundBitmap.Width, 0),
new Point(0, backgroundBitmap.Height)
};
g.DrawImage(tempBitmap, corners);
}
}
this.BackgroundImage = backgroundBitmap;
I did test this, and it worked. (It created a 200x200 resized version of one of my desktop wallpapers, then set that as the background image of the main form in a scratch WinForms project. You'll need using statements for System.Drawing and System.Drawing.Drawing2D.
In Winforms, if you contain the image inside a PictureBox control, the PictureBox control can be set to zoom to a particular height/width, and the image should conform.
At least that's what happened in my Head First C# book when I did the exercise.