I want to create a linear gradient with 7 step colors and custom size - from black, blue, cyan, green, yellow, red to white. My problem is that the final bitmap has a black stripe on the right side. Anyone have an idea what's the matter?
public static List<Color> interpolateColorScheme(int size)
{
// create result list with for interpolated colors
List<Color> colorList = new List<Color>();
// use Bitmap and Graphics from bitmap
using (Bitmap bmp = new Bitmap(size, 200))
using (Graphics G = Graphics.FromImage(bmp))
{
// create empty rectangle canvas
Rectangle rect = new Rectangle(Point.Empty, bmp.Size);
// use LinearGradientBrush class for gradient computation
LinearGradientBrush brush = new LinearGradientBrush
(rect, Color.Empty, Color.Empty, 0, false);
// setup ColorBlend object
ColorBlend colorBlend = new ColorBlend();
colorBlend.Positions = new float[7];
colorBlend.Positions[0] = 0;
colorBlend.Positions[1] = 1 / 6f;
colorBlend.Positions[2] = 2 / 6f;
colorBlend.Positions[3] = 3 / 6f;
colorBlend.Positions[4] = 4 / 6f;
colorBlend.Positions[5] = 5 / 6f;
colorBlend.Positions[6] = 1;
// blend colors and copy them to result color list
colorBlend.Colors = new Color[7];
colorBlend.Colors[0] = Color.Black;
colorBlend.Colors[1] = Color.Blue;
colorBlend.Colors[2] = Color.Cyan;
colorBlend.Colors[3] = Color.Green;
colorBlend.Colors[4] = Color.Yellow;
colorBlend.Colors[5] = Color.Red;
colorBlend.Colors[6] = Color.White;
brush.InterpolationColors = colorBlend;
G.FillRectangle(brush, rect);
bmp.Save("gradient_debug_image_sarcus.png", ImageFormat.Png);
for (int i = 0; i < size; i++) colorList.Add(bmp.GetPixel(i, 0));
brush.Dispose();
}
// return interpolated colors
return colorList;
}
Here is my gradient:
I took your code and tried every size from 2 to ushort.MaxValue, generating the gradient and scanning from the right edge to determine how many black pixels there were.
For many sizes, there are no black pixels. However, for certain consecutive runs of sizes, as the size increases, the number of black pixels also increases. There are approximately 2140 such runs in the tested range. This implies that there is a rounding error in the gradient drawing.
This bug has been encountered before (http://www.pcreview.co.uk/threads/error-on-lineargradientbrush.2165794/). The two solutions that link recommends are to
draw the gradient larger than you need it or
use WrapMode.TileFlipX.
What that link gets wrong is that the rounding error is not just 1 pixel at all times; at large image sizes it can be as large as 127 pixels (in the range I tested). Drawing the gradient larger than you need it requires you to know (or estimate) how much bigger you need to make the gradient. You could try scaling by (size + Math.Ceiling(size / 512.0)) / size, which is an upper bound on the error for the range of image sizes I have tested.
If you're looking for a simpler solution, specifying brush.WrapMode = WrapMode.TileFlipX will cause the brush to draw normally up to the (incorrect) edge of the gradient, then repeat the gradient in reverse until the actual edge of the specified rectangle. Since the rounding error is small compared to the size of the rectangle, this will look like the final color of the gradient has been extended to the edge of the rectangle. Visually, it looks good, but it may be unsuitable if you require very precise results.
Related
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.
I am drawing the text using Graphics.DrawString() method, But the text height drawn is not same as which i gave.
For Eg:
Font F=new Font("Arial", 1f,GraphicUnit.Inch);
g.DrawString("M", F,Brushes.red,new Point(0,0));
By using the above code, i'm drawing the text with height 1 inch, but the text drawn is not exactly in 1 inch.
I need to Draw the text in Exact height which i'm giving. Thanks in advance..
The simplest solution will be to use a GraphicsPath. Here are the steps necessary:
Calculate the height you want in pixels: To get 1.0f inches at, say 150 dpi you need 150 pixels.
Then create a GraphicsPath and add the character or string in the font and font style you want to use, using the calculated height
Now measure the resulting height, using GetBounds.
Then scale the height up to the necessary number of pixels
Finally clear the path and add the string again with the new height
Now you can use FillPath to output the pixels..
Here is a code example. It writes the test string to a file. If you want to write it to a printer or a control using their Graphics objects, you can do it the same way; just get/set the dpi before you calculate the first estimate of the height..
The code below creates this file; the Consolas 'x' is 150 pixels tall as is the 2nd character (ox95) from the Wingdings font. (Note that I did not center the output):
// we are using these test data:
int Dpi = 150;
float targetHeight = 1.00f;
FontFamily ff = new FontFamily("Consolas");
int fs = (int) FontStyle.Regular;
string targetString = "X";
// this would be the height without the white space
int targetPixels = (int) targetHeight * Dpi;
// we write to a Btimpap. I make it large enough..
// Instead you can write to a printer or a Control surface..
using (Bitmap bmp = new Bitmap(targetPixels * 2, targetPixels * 2))
{
// either set the resolution here
// or get and use it above from the Graphics!
bmp.SetResolution(Dpi, Dpi);
using (Graphics G = Graphics.FromImage(bmp))
{
// good quality, please!
G.SmoothingMode = SmoothingMode.AntiAlias;
G.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAlias;
// target position (in pixels)
PointF p0 = new PointF(0, 0);
GraphicsPath gp = new GraphicsPath();
// first try:
gp.AddString(targetString, ff, fs, targetPixels, p0,
StringFormat.GenericDefault);
// this is the 1st result
RectangleF gbBounds = gp.GetBounds();
// now we correct the height:
float tSize = targetPixels * targetPixels / gbBounds.Height;
// and if needed the location:
p0 = new PointF(p0.X - gbBounds.X, p0.X - gbBounds.Y);
// and retry
gp.Reset();
gp.AddString(targetString, ff, fs, tSize, p0, StringFormat.GenericDefault);
// this should be good
G.Clear(Color.White);
G.FillPath(Brushes.Black, gp);
}
//now we save the image
bmp.Save("D:\\testString.png", ImageFormat.Png);
}
You may want to try using the correction factor to scale up a Font size and use DrawString after all.
There is also a way to calculate the numbers ahead using FontMetrics, but I understand the link to mean that such an approach could be font-dependent..
Trying to figure out the most elegant way to render an image inside of a specific color of mask in C# (via System.Drawing or equivalent that will work in both desktop and ASP.NET applications).
The mask image will contain green keys where the image should be 'painted'.
(Desired Result image below is not perfect, hand lasso'd...)
There are various techniques for this:
Scan pixel data and build a mask image (as already suggested by itsme86 and Moby Disk)
A variant of scanning that builds a clipping region from the mask and uses that when drawing (refer to this article by Bob Powell)
Use color keys to mask in the Graphics.DrawImage call.
I'll focus on the third option.
Assuming that the image color that you want to eliminate from your mask is Color.Lime, we can use ImageAttributes.SetColorKey to stop any of that color from being drawn during a call to Graphics.DrawImage like this:
using (Image background = Bitmap.FromFile("tree.png"))
using (Image masksource = Bitmap.FromFile("mask.png"))
using (var imgattr = new ImageAttributes())
{
// set color key to Lime
imgattr.SetColorKey(Color.Lime, Color.Lime);
// Draw non-lime portions of mask onto original
using (var g = Graphics.FromImage(background))
{
g.DrawImage(
masksource,
new Rectangle(0, 0, masksource.Width, masksource.Height),
0, 0, masksource.Width, masksource.Height,
GraphicsUnit.Pixel, imgattr
);
}
// Do something with the composited image here...
background.Save("Composited.png");
}
And the results:
You can use the same technique (with color key on Color.Fuchsia) if you want to put those bits of tree into another image.
You want something like this:
Bitmap original = new Bitmap(#"tree.jpg");
Bitmap mask = new Bitmap(#"mask.jpg");
int width = original.Width;
int height = original.Height;
// This is the color that will be replaced in the mask
Color key = Color.FromArgb(0,255,0);
// Processing one pixel at a time is slow, but easy to understand
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
// Is this pixel "green" ?
if (mask.GetPixel(x,y) == key)
{
// Copy the pixel color from the original
Color c = original.GetPixel(x,y);
// Into the mask
mask.SetPixel(x,y,c);
}
}
}
You could probably read in the mask and translate it into an image that has the alpha channel set to 0 when the pixel is green and the alpha channel set to 0xFF when the pixel is any other color. Then you could draw the mask image over the original image.
On the left is the polygon I drew using my paint-like program. On the right is the polygon drawn by System.Drawing.Bitmap/Graphics:
The code to draw it is as follows:
protected static Bitmap CropImage(Bitmap src, Vector2[] rect)
{
var result = new Bitmap(src.Width, src.Height);
using (Graphics g = Graphics.FromImage(result))
{
g.InterpolationMode = InterpolationMode.HighQualityBicubic;
var pen = new Pen(Color.White);
g.DrawImage(src, new Point(0,0));
var poly = rect.Select(p => p.ToPointF()).ToArray();
g.DrawPolygon(pen, poly);
}
return result;
}
poly is:
{System.Drawing.PointF[4]}
[0]: {X = 57.4230042 Y = 57.4229736}
[1]: {X = 147.058868 Y = 56.0224}
[2]: {X = 148.43277 Y = 143.951767}
[3]: {X = 58.7969131 Y = 145.352341}
Each of the black squares in the image is 50x50. If you look at poly, all the coordinates are exactly as you'd expect: poly[0] is a little above 50,50 which corresponds to inside the black square labelled "6" (as shown in left image).
So how is Graphics getting confused and putting it in the wrong spot? It looks like it's scaling the whole rect down.
You are confused. The rectangle is correct, but the background image is scaled up. Note that the 6 and the black square are a lot bigger, but the rectangle is the exact same size.
Note that the Graphics.DrawImage method scales the source image to match the destination resolution.
This method draws an image using its physical size, so the image will have its correct size in inches regardless of the resolution (dots per inch) of the display device. For example, suppose an image has a pixel width of 216 and a horizontal resolution of 72 dots per inch. If you call this method to draw that image on a device that has a resolution of 96 dots per inch, the pixel width of the rendered image will be (216/72)*96 = 288.
You should use the overload that accepts a Rectangle instead:
g.DrawImage(src, new Rectangle(0, 0, src.Width, src.Height));
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.