How to implement pan/zoom on gigapixel bitmaps? - c#

In my project, I'm using (uncompressed 16-bit grayscale) gigapixel images which come from a high resolution scanner for measurement purposes. Since these bitmaps can not be loaded in memory (mainly due to memory fragmentation) I'm using tiles (and tiled TIFF on disc). (see StackOverflow topic on this)
I need to implement panning/zooming in a way like Google Maps or DeepZoom. I have to apply image processing on the fly before presenting it on screen, so I can not use a precooked library which directly accesses an image file. For zooming I intend to keep a multi-resolution image in my file (pyramid storage). The most useful steps seem to be +200%, 50% and show all.
My code base is currently C# and .NET 3.5. Currently I assume Forms type, unless WPF gives me great advantage in this area. I have got a method which can return any (processed) part of the underlying image.
Specific issues:
hints or references on how to implement this pan/zoom with on-demand generation of image parts
any code which could be used as a basis (preferably commercial or LGPL/BSD like licenses)
can DeepZoom be used for this (i.e. is there a way that I can provide a function to provide a tile at the right resulution for the current zoom level?) ( I need to have pixel accurate addressing still)

This CodeProject article: Generate...DeepZoom Image Collection might be a useful read since it talks about generating a DeepZoom image source.
This MSDN article has a section Dynamic Deep Zoom: Supplying Image Pixels at Run Time and links to this Mandelbrot Explorer which 'kinda' sounds similar to what you're trying to do (ie. he is generating specific parts of the mandelbrot set on-demand; you want to retrieve specific parts of your gigapixel image on-demand).
I think the answer to "can DeepZoom be used for this?" is probably "Yes", however as it is only available in Silverlight you will have to do some tricks with an embedded web browser control if you need a WinForms/WPF client app.
Sorry I can't provide more specific answers - hope those links help.
p.s. I'm not sure if Silverlight supports TIFF images - that might be an issue unless you convert to another format.

I decided to try something myself. I came up with a straightforward GDI+ code, which uses the tiles I've already got. I just filter out the parts which are relevant for current clipping region. It works like magic! Please find my code below.
(Form settings double buffering for the best results)
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
Graphics dc = e.Graphics;
dc.ScaleTransform(1.0F, 1.0F);
Size scrollOffset = new Size(AutoScrollPosition);
int start_x = Math.Min(matrix_x_size,
(e.ClipRectangle.Left - scrollOffset.Width) / 256);
int start_y = Math.Min(matrix_y_size,
(e.ClipRectangle.Top - scrollOffset.Height) / 256);
int end_x = Math.Min(matrix_x_size,
(e.ClipRectangle.Right - scrollOffset.Width + 255) / 256);
int end_y = Math.Min(matrix_y_size,
(e.ClipRectangle.Bottom - scrollOffset.Height + 255) / 256);
// start * contain the first and last tile x/y which are on screen
// and which need to be redrawn.
// now iterate trough all tiles which need an update
for (int y = start_y; y < end_y; y++)
for (int x = start_x; x < end_x; x++)
{ // draw bitmap with gdi+ at calculated position.
dc.DrawImage(BmpMatrix[y, x],
new Point(x * 256 + scrollOffset.Width,
y * 256 + scrollOffset.Height));
}
}
To test it, I've created a matrix of 80x80 of 256 tiles (420 MPixel). Of course I'll have to add some deferred loading in real life. I can leave tiles out (empty) if they are not yet loaded. In fact, I've asked my client to stick 8 GByte in his machine so I don't have to bother about performance too much. Once loaded tiles can stay in memory.
public partial class Form1 : Form
{
bool dragging = false;
float Zoom = 1.0F;
Point lastMouse;
PointF viewPortCenter;
private readonly Brush solidYellowBrush = new SolidBrush(Color.Yellow);
private readonly Brush solidBlueBrush = new SolidBrush(Color.LightBlue);
const int matrix_x_size = 80;
const int matrix_y_size = 80;
private Bitmap[,] BmpMatrix = new Bitmap[matrix_x_size, matrix_y_size];
public Form1()
{
InitializeComponent();
Font font = new Font("Times New Roman", 10, FontStyle.Regular);
StringFormat strFormat = new StringFormat();
strFormat.Alignment = StringAlignment.Center;
strFormat.LineAlignment = StringAlignment.Center;
for (int y = 0; y < matrix_y_size; y++)
for (int x = 0; x < matrix_x_size; x++)
{
BmpMatrix[y, x] = new Bitmap(256, 256, PixelFormat.Format24bppRgb);
// BmpMatrix[y, x].Palette.Entries[0] = (x+y)%1==0?Color.Blue:Color.White;
using (Graphics g = Graphics.FromImage(BmpMatrix[y, x]))
{
g.FillRectangle(((x + y) % 2 == 0) ? solidBlueBrush : solidYellowBrush, new Rectangle(new Point(0, 0), new Size(256, 256)));
g.DrawString("hello world\n[" + x.ToString() + "," + y.ToString() + "]", new Font("Tahoma", 8), Brushes.Black,
new RectangleF(0, 0, 256, 256), strFormat);
g.DrawImage(BmpMatrix[y, x], Point.Empty);
}
}
BackColor = Color.White;
Size = new Size(300, 300);
Text = "Scroll Shapes Correct";
AutoScrollMinSize = new Size(256 * matrix_x_size, 256 * matrix_y_size);
}
Turned out this was the easy part. Getting async multithreaded i/o done in the background was a lot harder to acchieve. Still, I've got it working in the way described here. The issues to resolve were more .NET/Form multithreading related than to this topic.
In pseudo code it works like this:
after onPaint (and on Tick)
check if tiles on display need to be retrieved from disc
if so: post them to an async io queue
if not: check if tiles close to display area are already loaded
if not: post them to an async io/queue
check if bitmaps have arrived from io thread
if so: updat them on screen, and force repaint if visible
Result: I now have my own Custom control which uses roughly 50 MByte for very fast access to arbitrary size (tiled) TIFF files.

I guess you can address this issue following the steps below:
Image generation:
segment your image in multiple subimages (tiles) of a small resolution, for instace, 500x500. These images are depth 0
combine a series of tiles with depth 0 (4x4 or 6x6), resize the combination generating a new tile with 500x500 pixels in depth 1.
continue with this approach until get the entire image using only a few tiles.
Image visualization
Start from the highest depth
When user drags the image, load the tiles dynamically
When the user zoom a region of the image, decrease the depth, loading the tiles for that region in a higher resolution.
The final result is similar to Google Maps.

Related

Graphis.DrawString always uses the default font

I have a project in which I create an image with rotated text around an invisible circle.
The drawing in itself is working just fine. However, it seems that no matter the font I use, I always get the same result, which is I assume some low quality default font.
Here is the code :
Bitmap objBmpImage = new Bitmap(1000, 1000);
System.Drawing.Text.InstalledFontCollection installedFontCollection = new System.Drawing.Text.InstalledFontCollection();
FontFamily[] fontFamilies = installedFontCollection.Families;
System.Drawing.Font objFont = new System.Drawing.Font(fontFamilies.Where(x => x.Name == "Arial").FirstOrDefault(),10);
Graphics objGraphics = Graphics.FromImage(objBmpImage);
objGraphics.Clear(Color.Transparent);
float angle = (float)360.0 / (float)competences.Count();
objGraphics.TranslateTransform(500, 450);
objGraphics.RotateTransform(-90 - (angle / 3));
int nbComptetence = competences.Count();
int indexCompetence = 0;
foreach (T_Ref_Competence competence in competences)
{
byte r, g, b;
HexToInt(competence.T_Ref_CompetenceNiveau2.T_Ref_CompetenceNiveau1.Couleur, out r, out g, out b);
Brush brush = new System.Drawing.SolidBrush(Color.FromArgb(255,r,g,b));
if (indexCompetence * 2 < nbComptetence)
{
objGraphics.DrawString(competence.Nom, objFont, brush, 255, 0);
objGraphics.RotateTransform(angle);
}
else
{
objGraphics.RotateTransform(180);
objGraphics.RotateTransform(angle/2);
float textSize = objGraphics.MeasureString(competence.Nom, objFont).Width;
objGraphics.DrawString(competence.Nom, objFont, brush, -253 - textSize, 0);
objGraphics.RotateTransform(angle);
objGraphics.RotateTransform(-180);
objGraphics.RotateTransform(-angle / 2);
}
indexCompetence++;
}
I get the font using the installed families like this
System.Drawing.Text.InstalledFontCollection installedFontCollection = new System.Drawing.Text.InstalledFontCollection();
FontFamily[] fontFamilies = installedFontCollection.Families;
System.Drawing.Font objFont = new System.Drawing.Font(fontFamilies.Where(x => x.Name == "Arial").FirstOrDefault(),10);
I tried using other font but the result is always the same. Is there anything I am missing ? If not, what could be the reason ?
Thanks,
EDIT : To answer the question, what is it that I want exactly, consider this :
This image is a screenshot of a web site I am making. The chart in the middle was generated using charts.js, but its limitation force me to draw the text as a background image. It actually takes most of my screen so it can't really get much bigger than this. As you can see, the text font is pretty blurry and I would simply want it to be easier to read. I though the font was the problem, but I don't really know.
I am not really familiar with the whole image drawing part of C#, so if there are is better way to draw my text (which can change depending of many variables), I will gladly try other things.
Option 1: change text rendering
objGraphics.TextRenderingHint = TextRenderingHint.SingleBitPerPixel
Option 2: change the mode of anti aliasing
objGraphics.InterpolationMode=InterpolationMode.NearestNeighbor;
Option 3: change the DPI of the image
You'll get the best result if you scale the input image and then draw the text in higher DPI.
The default DPI for a Bitmap are 96. Probably the JS library exported with that setting.
If you want a smoother rendering of the font, you need to increase the DPI, e.g.
objBmpImage.SetResolution(1200,1200);
If you do so, you probably need to increase the number of pixels your Bitmap has.
If the "ugly" text just fitted the 1000x1000 picture, you now need 1000*1200/96=12500 pixels.
Before the change (using Arial 10 pt):
After the change (still using Arial 10 pt):
Note that the size in centimeters doesn't change. So it will still print well.

Direct2D plot approach / performance

How is the approach to plot complex drawings with Direct2D (Sharpdx)?
Actually I am using a WindowsRenderTarget, connecting it with a Direct2D1.Factory and drawing to a RenderControl.
Factory2D = new SharpDX.Direct2D1.Factory(FactoryType.MultiThreaded);
FactoryWrite = new SharpDX.DirectWrite.Factory();
var properties = new HwndRenderTargetProperties();
properties.Hwnd = this.Handle;
properties.PixelSize = new Size2(this.ClientSize.Width, this.ClientSize.Height);
properties.PresentOptions = PresentOptions.RetainContents;
RenderTarget2D = new WindowRenderTarget(Factory2D, new RenderTargetProperties(new PixelFormat(Format.Unknown, AlphaMode.Premultiplied)), properties);
RenderTarget2D.AntialiasMode = AntialiasMode.PerPrimitive;
The drawing is done in the Paint Event of the form:
RenderTarget2D.BeginDraw();
RenderTarget2D.Clear(Color4.Black);
drawProgress(); // Doing Paintings like DrawLine, Multiple PathGeometrys, DrawEllipse and DrawText
RenderTarget2d.EndDraw();
In the MouseMove/MouseWheel event the drawing will be recalculated (for scaling or calculation of the elements that will be displayed). This process need about 8-10ms.
The next step is actually
this.Refresh();
Here, I guess, is the problem, this progress needs up to 140ms.
So the scaling/moving of the plot has about 7fps.
Also the program occupies more and more memory when refreshing the Control
////Edit
Painting of lines:
private void drawLines(Pen pen, PointF[] drawElements)
{
SolidColorBrush tempBrush = new SolidColorBrush(RenderTarget2D, SharpDX.Color.FromRgba(pen.Color.ToArgb()));
int countDrawing = (drawElements.Length / 2) + drawElements.Length % 2;
for (int i = 0; i < countDrawing; i++)
{
drawLine(new Vector2(drawElements[i].X, drawElements[i].Y), new Vector2(drawElements[i + 1].X, drawElements[i + 1].Y), brushWhite);
}
}
Painting geometrys:
RenderTarget2D.DrawGeometry(graphicPathToPathGeometry(p), penToSolidColorBrush(pen));
private PathGeometry graphicPathToPathGeometry(GraphicsPath path)
{
geometry = new PathGeometry(Factory2D);
sink = geometry.Open();
if (path.PointCount > 0)
{
sink.BeginFigure(new Vector2(path.PathPoints[path.PointCount - 1].X, path.PathPoints[path.PointCount - 1].Y), FigureBegin.Hollow);
sink.AddLines(pointFToVector2(path.PathPoints));
sink.EndFigure(new FigureEnd());
sink.Close();
}
return geometry;
}
In mouse move the drawing will be recalculated by just building differences between Cursor.Position.X/Y old and Cursor.Position.X/Y new. So the the lines will be recalculated really often :)
The main bottleneck is your graphicPathToPathGeometry() function. You are creating and "filling" a PathGeometry in a render loop. As I mentioned above, a core principle is that you have to create your resources at once and then just reuse them in your drawing routine(s).
About your memory leak... your code samples don't provide enough information, but most probably you are not freeing the resources that you are creating (ie PathGeometry, SolidColorBrush and the ones we don't see).
The simplest advise is - use your render loop only for rendering/drawing and reuse resources instead of recreating them.
Improving the performance of Direct2D apps
One part of the problem is:
SolidColorBrush tempBrush = new SolidColorBrush(RenderTarget2D, SharpDX.Color.FromRgba(pen.Color.ToArgb()));
Creating objects of any kind inside the renderloop creates a big memory lack in the application. Drawing existing values is the way to go.
I guess the performance issue will also be based on this problem.

DrawingContext to Bitmap file

I have a DrawingContext (part of a Visual or a DrawingGroup), where I draw a bunch of rectangles and/or 1-bit images on top of each other. Think of it as a masking 1-bit image. I would like to convert this into a bitmap image file.
Using RenderTargetBitmap is not an option because it can only render at 32bit pixel format, so if I have to render a 20MB 1-bit image, I will end up with a 640MB (20*32) of memory on my heap. This of course creates magnificent LOH fragmentation, and the application runs out-of-memory on the second shot.
So, I basically need a way to write a 1-bit bitmap file from a drawing context efficiently. Any ideas/suggestions/alternate methods would be appreciated.
A number of ideas, some are a bit convoluted...
Print to XPS then extract Bitmap
You could print the Visual to an XPS Document.
If you're lucky then it will combine the 1bit images that you drew in in the DrawingContext and produce a single bitmap in the XPS file.
For the Rectangles it might keep the Rectangles as vector based information in the XPS (the "Shape" based Rectangle or DrawingContext DrawRectangle might both do this)....if that happens then create a bitmap into which your draw the rectangle part and draw that bitmap into the context.
Once you have the XPS files, you could then parse it and extract the "image" if you are lucky that's what it produced inside. (XPS is just a ZIP file that uses XAML to describe content and uses subdirectories to store bitmap image data files, fonts, etc).
(I think you can use the Package class to access the raw parts of an XPS document, or XpsDocument for a higher level interpretation).
Or just display the XPS in an XPS viewer if your intention is just to provide a way to allow your combined 1bit images to be viewed in a different context.
Convert WPF (XAML) Control to XPS Document
How to create BIG bitmaps with RenderTargetBitmap?
Printing a WPF BitmapImage
Use a Printer Driver that provides Print to Image functionality
This isn't ideal...not least because you have to install a Printer Driver on the machine.....but you might be able to use a "Print to Image" printer driver and then use that to produce your bitmap. Here are some suggestions:
http://sourceforge.net/projects/imageprinter/
http://www.zan1011.com/
Create a GDI+ HBITMAP and do the drawing using GDI+ calls...then wrap it for use by WPF...or save out to disk.
[1] First create a GDI+ Bitmap large enough to hold your composed rendering
(there are a few different ways to do this...one way is to use a WriteableBitmap to provide the backbuffer bits/store...that's if you wanted access to the bits in memory...in your case I don't think you do/need to).
var bmp = new System.Drawing.Bitmap( pixelwidth, pixelheight, System.Drawing.Imaging.Format1bppIndexed );
Or this way if you want WriteableBitmap access.
Getting a DrawingContext for a wpf WriteableBitmap
[2] Convert any WPF BitmapSources to GDI+ Bitmaps so you can draw them onto the GDI+ Graphics context using DrawImage.
You can use CopyPixels to do that on your BitmapSources.
Is there a good way to convert between BitmapSource and Bitmap?
For your rectangles you can just use the GDI+ DrawRectangle rendering command.
[3] Save the GDI+ Bitmap to disk
Use the .Save method on System.Image.Bitmap to save it as a Bitmap.
[4] Use the saved image as the Source to an Image element
(note this will probably use masses of memory when it loads your image even though it's 1bpp...because the WPF rendering path is all 32bpp).
[4] OR wrap the GDI+ Bitmap for use in WPF
You can use InteropBitmap to wrap the GDI+ based bitmap so you can use it as a BitmapSource in WPF. (note it may have to be a 32bpp one....if it has to...then you are back at square 1...but try anyway).
http://arbel.net/2008/10/22/improper-use-of-interopbitmap-can-cause-a-memory-leak/
http://arbel.net/2008/10/22/improper-use-of-interopbitmap-can-cause-a-memory-leak/
Create a Bitmap "Service"
Create another process which acts as a service (doesn't have to be an NT service...could just be a child process you start) to render your combined 1bpp images....there are various ways to communicate with it to give it the rendering commands.
When memory consumption gets too high/the LOH gets fragmented, then you could restart this service.
Other Ideas
You could render using OpenGL (e.g. use OpenTK or SharpGL), or render using DirectX...via the 3D path with D3DImage or Direct2D (whether this behaves the same as RenderTargetBitmap in terms of memory usage...that's to find out).
http://www.codeproject.com/Articles/265903/Using-OpenGL-in-a-WPF-Application
http://www.codeproject.com/Articles/113991/Using-Direct2D-with-WPF
http://www.codeproject.com/Articles/28526/Introduction-to-D3DImage
Try out NET 4.5 as there have been a number of improvements in the LOH Heap:
http://blogs.msdn.com/b/dotnet/archive/2011/10/04/large-object-heap-improvements-in-net-4-5.aspx
http://blogs.msdn.com/b/dotnet/archive/2012/07/20/the-net-framework-4-5-includes-new-garbage-collector-enhancements-for-client-and-server-apps.aspx
I dont think you can do anything better. Since RenderTargetBitmap uses MILcore which you can't access. And I don't think there is any other way to copy Visual. However I think there is one option more. It won't be one-line but I think it will be good enough.
Basically you will render visual block by block(PGBRA32) and convert it into BlackWhite on the fly, and then concat it with Blackwhite buffer. I've started a little example code but in halfway decided that it's not gonna be so easy, but you can finish it.
/// <summary>
/// Renders only part of the visual and returns byte[] array
/// which holds only black/white information.
/// </summary>
/// <param name="oVisual"></param>
/// <param name="x"></param>
/// <param name="y"></param>
/// <param name="width"></param>
/// <param name="height"></param>
/// <returns>black/white pixel information. one pixel=one bit. 1=white, 0=black</returns>
public static byte[] RenderPartially(Visual oVisual,
int x, int y, double width, double height)
{
int nWidth = (int)Math.Ceiling(width);
int nHeight = (int)Math.Ceiling(height);
RenderTargetBitmap oTargetBitmap = new RenderTargetBitmap(
nWidth,
nHeight,
96,
96,
PixelFormats.Pbgra32
);
DrawingVisual oDrawingVisual = new DrawingVisual();
using (DrawingContext oDrawingContext = oDrawingVisual.RenderOpen())
{
VisualBrush oVisualBrush = new VisualBrush(oVisual);
oDrawingContext.DrawRectangle(
oVisualBrush,
null,
new Rect(
new Point(x, y),
new Size(nWidth, nHeight)
)
);
oDrawingContext.Close();
oTargetBitmap.Render(oDrawingVisual);
}
//Pbgra32 == 32 bits ber pixel?!(4bytes)
// Calculate stride of source and copy it over into new array.
int bytesPerPixel = oTargetBitmap.Format.BitsPerPixel / 8;
int stride = oTargetBitmap.PixelWidth * bytesPerPixel;
byte[] data = new byte[stride * oTargetBitmap.PixelHeight];
oTargetBitmap.CopyPixels(data, stride, 0);
//assume pixels in byte[] are stored as PBGRA32 which means that 4 bytes form single PIXEL.
//so the layout is like:
// R1, G1, B1, A1, R2, G2, B2, A2, R3, G3, B3, A3, and so on.
byte [] bitBufferBlackWhite = new byte[oTargetBitmap.PixelWidth
* oTargetBitmap.PixelHeight / bytesPerPixel];
for(int row = 0; row < oTargetBitmap.PixelHeight; row++)
{
for(int col = 0; col < oTargetBitmap.PixelWidth; col++)
{
//calculate concrete pixel from PBGRA32
int index = stride * row + bytesPerPixel * col;
int r = data[index];
int g = data[index + 1];
int b = data[index + 2];
int transparency = data[index + 3];
//determine whenever pixel is black or white.
//note that I dont know the exact process how one converts
//from PBGRA32 to BlackWhite format. But you should get the idea.
bool isBlack = r + g + b + transparency == 0;
//calculate target index and USE bitwise operators in order to
//set right bits.
}
}
return bitBufferBlackWhite;
}
So essentially, set up new WriteableBitmap with BlackWhite format, and then call this function like this:
WriteableBitmap blackWhiteFullBuffer = new WriteableBItmap(....., PIxelFormats.BlackWhite);
for(int x = 0; x < Visual.Width; x += <some uniform amount that divides correctly>)
{
for(int y = 0; y < VIsual.Height; y+= <some uniform amount that divides correctly>)
{
byte[] partialBuffer = PartialRenderer.RenderPartially(Visual, x, y, <uniform amX>,
<uniform amY>);
//concat that partial blackWhite buffer with other blackWhite buffer.
//you do this as long as needed and memory wont grow too much
//hopefully it will be fast enough too.
PartialRenderer.ConcateBuffers(blackWhiteFullBuffer, partialBuffer);
}
}
//then save blackWhiteFullBuffer to HDD if needed.
What about PixelFormats
Edit: (thanks to Anders Gustafsson)
The lower is PixelFormats.BlackWhite, with 1bit per pixel.
So this way, you can convert any BitmapImage to a FormatConvertedBitmap, where you can modify the format to a lower bpp.

How to avoid useless white border in resized PNG with transparent background?

I have a folder containing about 2500 PNG images, with no transparency. Every image is about 500 x 500 (some are 491 x 433, others 511 x 499 etc).
I want to programatically downsize every image to 10% of its original size, and to set the white background of every image as the transparent color.
To test the functionality of my application without resizing 2500 images every time, I used 15 images of billiard balls as a "test" folder.
Now my problem is with the following code, I get a resized and cropped PNG, whith a almost transparent background. The problem is that a white border on the left and top appears in every image viewer (Irfan View, Paint.Net and GIMP)
How can I avoid this border?
Here is the code I used for this:
void ResizeI(string[] Paths, string OutPut, Methods m, PointF Values, bool TwoCheck, bool Overwrite, float[] CropVals)
{
for (int i = 0; i < Paths.Length; i++)//Paths is the array of all images
{
string Path = Paths[i];//current image
Bitmap Oimg = (Bitmap)Bitmap.FromFile(Path);//original image
Bitmap img = new Bitmap((int)(Oimg.Width - CropVals[0] - CropVals[1]), (int)(Oimg.Height - CropVals[2] - CropVals[3]));//cropped image
Graphics ggg = Graphics.FromImage(img);
ggg.DrawImage(Oimg, new RectangleF(((float)-CropVals[0]), ((float)-CropVals[2]), Oimg.Width - CropVals[1], Oimg.Height - CropVals[3]));
ggg.Flush(System.Drawing.Drawing2D.FlushIntention.Flush);
ggg.Dispose();
PointF scalefactor = GetScaleFactor(img, Values, TwoCheck);//the scale factor equals 0.1 for 10%
Bitmap newimg = new Bitmap((int)(Math.Ceiling(((float)img.Width) * scalefactor.X)), (int)(Math.Ceiling(((float)img.Height) * scalefactor.Y)));
System.Drawing.Imaging.ImageFormat curform = img.RawFormat;
string OutPath = System.IO.Path.Combine(OutPut, System.IO.Path.GetFileName(Path));
OutPath = CheckPath(OutPath, Overwrite);//Delete if exsits
Graphics g = Graphics.FromImage(newimg);
g.InterpolationMode = GetModeFromMethod(m);//Bicubic interpolation
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
g.ScaleTransform(scalefactor.X, scalefactor.Y);
g.DrawImage(img, new Rectangle(0, 0, (int)Math.Ceiling(((float)newimg.Width) / scalefactor.X) + 1, (int)Math.Ceiling(((float)newimg.Height) / scalefactor.Y) + 1));
//g.Flush(System.Drawing.Drawing2D.FlushIntention.Flush);
newimg.MakeTransparent(Color.White);
newimg.Save(OutPath, curform);
g.Dispose();
img.Dispose();
}
}
And here is a example of the white border I mentioned. Download the image or drag it around and put a black background under it to see the border:
-- EDIT --
I managed to write this function instead of newimg.MakeTransparent(...):
void SetTransparent(ref Bitmap b)
{
for (int i = 0; i < b.Width; i++)
{
for (int ii = 0; ii < b.Height; ii++)
{
Color cc = b.GetPixel(i, ii);
int tog = cc.R + cc.G + cc.B;
float durch = 255f - (((float)tog) / 3f);
b.SetPixel(i, ii, Color.FromArgb((int)durch, cc.R, cc.G, cc.B));
}
}
}
the problem is that my billiard ball now look like this:
I can't help with the specific code, but maybe can explain what's happening.
newimg.MakeTransparent(Color.White);
This will take one color, and make it transparent. The catch is that, there's a spectrum of colors between the edge of your billiard ball (orange) and the pure white background. This is the antialiasing of the edge (which will be a blend of colors from the pure orange of the ball to the pure white of the background).
By turning only pure white transparent, you are still left with this 'halo' of white-ish colors around the object.
There's perhaps a better way to handle this using white values as an alpha mask but I'm not sure if .net's image library can handle that (I'll have to defer to someone with more .net experience comes along).
In the interim, though, what may help is if you set the transparency before you do the resize. It won't be a true fix, but might reduce the halo effect some.
UPDATE:
So, I've been thinking about this some more, and I'm not entirely sure there's a programmatic solution for creating alpha channel transparency automatically, as I have a hunch there's a lot of subjectivity involved.
Off the top of my head, this is what I came up with:
assuming the top left pixel is your 100% transparent color (we'll say pixel X).
assuming your background that you want transparent is one solid color (vs. a pattern)
assume a roughly 3px anti-aliasing
you could then...
check for neighboring pixels to X. For each neighboring pixel to X that matches the color of X, we set that 100% transparent.
if a pixel next to x is NOT the same, we could check it's relative hue.
branch from that pixel and check it's surrounding pixels.
do this marking each pixel (a, b, c, etc) until the relative hue changes a certain percentage and/or the pixel color is the same as it's neighbor (with a certain margin of variability). If it does, we'll assume we're well into the interior of the object.
now step backwards through the pixels you marked, adjusting the transparency...say c=0% b=33% a=66%
But still, that's a large oversimplification of what would really have to happen. It's making a lot of assumptions, not taking into account a patterned background, and completely ignores interior areas that need to also be transparent (such as a donut hole).
Normally in a graphics editing app, this is done via selecting blocks of the background color, feathering the edges of said selection, then turning that into an alpha max.
It's a really interesting question/problem. I, alas, don't have the answer for you but will be watching this thread with curiosity!
Your edited SetTransparent function is on the right direction, and you're almost there.
Just a slight modification you can try this:
void SetTransparent(ref Bitmap b)
{
const float selectivity = 20f; // set it to some number much larger than 1 but less than 255
for (int i = 0; i < b.Width; i++)
{
for (int ii = 0; ii < b.Height; ii++)
{
Color cc = b.GetPixel(i, ii);
float avgg = (cc.R + cc.G + cc.B) / 3f;
float durch = Math.Min(255f, (255f - avgg) * selectivity);
b.SetPixel(i, ii, Color.FromArgb((int)durch, cc.R, cc.G, cc.B));
}
}
}
The idea is that to avoid affecting the alpha value of the billard ball, you will only want to reduce the alpha for colors that are very close to zero. In other words, it is a function that rises rapidly from 0 to 255 as the color moves away from white.
This will not produce the ideal result, as #DA said, because there is some information lost (transparent pixels and non-transparent pixels being blended together near the object's edges) that is unrecoverable. To make perfectly alias-free alpha edges, the source image itself must be generated with transparency.

C# Image.Clone Out of Memory Exception

Why am I getting an out of memory exception?
So this dies in C# on the first time through:
splitBitmaps.Add(neededImage.Clone(rectDimensions, neededImage.PixelFormat));
Where splitBitmaps is a List<BitMap> BUT this works in VB for at least 4 iterations:
arlSplitBitmaps.Add(Image.Clone(rectDimensions, Image.PixelFormat))
Where arlSplitBitmaps is a simple array list. (And yes I've tried arraylist in c#)
This is the fullsection:
for (Int32 splitIndex = 0; splitIndex <= numberOfResultingImages - 1; splitIndex++)
{
Rectangle rectDimensions;
if (splitIndex < numberOfResultingImages - 1)
{
rectDimensions = new Rectangle(splitImageWidth * splitIndex, 0,
splitImageWidth, splitImageHeight);
}
else
{
rectDimensions = new Rectangle(splitImageWidth * splitIndex, 0,
sourceImageWidth - (splitImageWidth * splitIndex), splitImageHeight);
}
splitBitmaps.Add(neededImage.Clone(rectDimensions, neededImage.PixelFormat));
}
neededImage is a Bitmap by the way.
I can't find any useful answers on the intarweb, especially not why it works just fine in VB.
Update:
I actually found a reason (sort of) for this working but forgot to post it. It has to do with converting the image to a bitmap instead of just trying to clone the raw image if I remember.
Clone() may also throw an Out of memory exception when the coordinates specified in the Rectangle are outside the bounds of the bitmap. It will not clip them automatically for you.
I found that I was using Image.Clone to crop a bitmap and the width took the crop outside the bounds of the original image. This causes an Out of Memory error. Seems a bit strange but can beworth knowing.
I got this too when I tried to use the Clone() method to change the pixel format of a bitmap. If memory serves, I was trying to convert a 24 bpp bitmap to an 8 bit indexed format, naively hoping that the Bitmap class would magically handle the palette creation and so on. Obviously not :-/
This is a reach, but I've often found that if pulling images directly from disk that it's better to copy them to a new bitmap and dispose of the disk-bound image. I've seen great improvement in memory consumption when doing so.
Dave M. is on the money too... make sure to dispose when finished.
I struggled to figure this out recently - the answers above are correct. Key to solving this issue is to ensure the rectangle is actually within the boundaries of the image. See example of how I solved this.
In a nutshell, checked to if the area that was being cloned was outside the area of the image.
int totalWidth = rect.Left + rect.Width; //think -the same as Right property
int allowableWidth = localImage.Width - rect.Left;
int finalWidth = 0;
if (totalWidth > allowableWidth){
finalWidth = allowableWidth;
} else {
finalWidth = totalWidth;
}
rect.Width = finalWidth;
int totalHeight = rect.Top + rect.Height; //think same as Bottom property
int allowableHeight = localImage.Height - rect.Top;
int finalHeight = 0;
if (totalHeight > allowableHeight){
finalHeight = allowableHeight;
} else {
finalHeight = totalHeight;
}
rect.Height = finalHeight;
cropped = ((Bitmap)localImage).Clone(rect, System.Drawing.Imaging.PixelFormat.DontCare);
Make sure that you're calling .Dispose() properly on your images, otherwise unmanaged resources won't be freed up. I wonder how many images are you actually creating here -- hundreds? Thousands?

Categories