LinearGradientBrush Artifact Workaround? - c#

The LinearGradientBrush in .net (or even in GDI+ as a whole?) seems to have a severe bug: Sometimes, it introduces artifacts. (See here or here - essentially, the first line of a linear gradient is drawn in the endcolor, i.e. a gradient from White to Black will start with a Black line and then with the proper White to Black gradient)
I wonder if anyone found a working workaround for this? This is a really annoying bug :-(
Here is a picture of the Artifacts, note that there are 2 LinearGradientBrushes:
alt text http://img142.imageshack.us/img142/7711/gradientartifactmm6.jpg

I have noticed this as well when using gradient brushes. The only effective workaround I have is to always create the gradient brush rectangle 1 pixel bigger on all edges than the area that is going to be painted with it. That protects you against the issue on all four edges. The downside is that the colors used at the edges are a fraction off those you specify, but this is better than the drawing artifact problem!

You can use the nice Inflate(int i) method on a rectangle to get the bigger version.

I would finesse Phil's answer above (this is really a comment but I don't have that privilege). The behaviour I see is contrary to the documentation, which says:
The starting line is perpendicular to the orientation line and passes through one of the corners of the rectangle. All points on the starting line are the starting color. Then ending line is perpendicular to the orientation line and passes through one of the corners of the rectangle. All points on the ending line are the ending color.
Namely you get a single pixel wrap-around in some cases. As far as I can tell (by experimentation) I only get the problem when the width or height of the rectangle is odd. So to work around the bug I find it is adequate to increase the LinearGradientBrush rectangle by 1 pixel if and only if the dimension (before expansion) is an odd number. In other words, always round the brush rectangle up the the next even number of pixels in both width and height.
So to fill a rectangle r I use something like:
Rectangle gradientRect = r;
if (r.Width % 2 == 1)
{
gradientRect.Width += 1;
}
if (r.Height % 2 == 1)
{
gradientRect.Height += 1;
}
var lgb = new LinearGradientBrush(gradientRect, startCol, endCol, angle);
graphics.FillRectangle(lgb, r);
Insane but true.

At least with WPF you could try to use GradientStops to get 100% correct colors right at the edges, even when overpainting.

I experienced artifacts too in my C++ code. What solved the problem is setting a non-default SmoothingMode for the Graphics object. Please note that all non-default smoothing modes use coordinate system, which is bound to the center of a pixel. Thus, you have to correctly convert your rectangle from GDI to GDI+ coordinates:
Gdiplus::RectF brushRect;
graphics.SetSmoothingMode( Gdiplus::SmoothingModeHighQuality );
brushRect.X = rect.left - (Gdiplus::REAL)0.5;
brushRect.Y = rect.top - (Gdiplus::REAL)0.5;
brushRect.Width = (Gdiplus::REAL)( rect.right - rect.left );
brushRect.Height = (Gdiplus::REAL)( rect.bottom - rect.top );
It seems like LinearGradientBrush works correctly only in high-quality modes.

Related

Set bits per pixel in WPF-Desktop program

I noticed ugly banding issues when using Gradients in WPF, and saw that a solution was to set the "bits per pixel" property to 32.
The thing is that the property seem to be Windows Phone only, ie not working on a program for desktop devices, since trying to add this string in the ApplicationManifest didn't seem to do anything.
Does anyone know if/how I can set this property?
Thank you.
My function which draws the gradients:
public LinearGradientBrush getGradient(Color c1, Color c2, double opacity)
{
LinearGradientBrush gradient = new LinearGradientBrush();
gradient.StartPoint = new Point(0, 0);
gradient.EndPoint = new Point(1, 1);
gradient.GradientStops.Add(new GradientStop(c1, 0.0));
gradient.GradientStops.Add(new GradientStop(c2, 1.0));
gradient.Opacity = opacity;
return gradient;
}
I draw the gradients off of the two most dominant colors in an AlbumCover. You can see the two colors on the top left of the window. I then call the getGradient-function with this:
getGradient(Colors[0], Colors[1], 0.5); // 0.5 is dynamic depending on the brightness of those colors. Tried with 1 opacity but it's still the same.
Here are the sample images (in PNG and uploaded without compression)
Image1
Image2
Image3
As you can see, there is banding going on. There are worse examples but I can't remember what Cover gave it.
Please notice that Image1 does not have banding on it's AlbumCover. Even though there is a gradient on it.
By doing a quick search I found some suggestions that the issue may be just a visual effect that is a result of having only 256 values for each of R, G and B channels that defines a color and the way that gradients work. If You try to cover a large area with a gradient, it'll divide it into smaller areas filled with solid colors, slightly changing between neighbouring areas. Additionally, there is an optical illusion called Mach bands that makes the borders of the areas even more visible.
Take a look at those links for more information and some suggested solutions:
how to make the brush smooth without lines in the middle
http://social.msdn.microsoft.com/Forums/vstudio/en-US/cea96578-a6b3-4b29-b813-e3643d7770ae/lineargradientbrush-can-see-individual-gradient-steps?forum=wpf
After digging around a long time I finally found the best solution:
Adding a little bit of noise to the image! This does mean I have to draw the gradient myself, but I believe the quality will be much better.
I will update this post with the algorithm itself and examples when I'm done writing.
Stay tuned I guess.

Can I use graphics.RotateTransform() without these artifacts?

I'm implementing a colour picker component as described in this seminal article.
As you can see, I've got the basics sorted:
One of the requirements however, is the ability to have the colour wheel rotated by an arbitrary amount. Thinking this would be easy, I some arithmetic to the mouse location -> colour value code and the following code to the bit that actually paints the wheel:
newGraphics.TranslateTransform((float)this.Radius, (float)this.Radius);
newGraphics.RotateTransform((float)this.offset);
newGraphics.TranslateTransform((float)this.Radius * -1, (float)this.Radius * -1);
Unfortunately, rotating the bitmap like this actually produces this:
Note the artefacts that appear either side of the centre.
Am I using the wrong approach? Or is there a way to get rid of these nasty rips?
Looking at the source code from that Microsoft example, I made the following change to the UpdateDisplay method by adding a matrix and setting the RotateAt method.
private void UpdateDisplay() {
// Update the gradients, and place the
// pointers correctly based on colors and
// brightness.
using (Brush selectedBrush = new SolidBrush(selectedColor)) {
using (Matrix m = new Matrix()) {
m.RotateAt(35f, centerPoint);
g.Transform = m;
// Draw the saved color wheel image.
g.DrawImage(colorImage, colorRectangle);
g.ResetTransform();
}
// Draw the "selected color" rectangle.
g.FillRectangle(selectedBrush, selectedColorRectangle);
// Draw the "brightness" rectangle.
DrawLinearGradient(fullColor);
// Draw the two pointers.
DrawColorPointer(colorPoint);
DrawBrightnessPointer(brightnessPoint);
}
}
It rotated the wheel 35 degrees (although the color selection was off now by, well, 35 degrees since I didn't mess with all the code) and it didn't produce any tearing.
Not 100% sure this is the answer (but too long for a comment), so maybe this is helpful.

Segmenting Part of the image using threshold

Im trying to isolate and segment the yellow car body to change the color of it. in order to do that i need to separately identify the body from the image. And continue oration with the remaining white pixels. And im using C#, here the plan
Color d;
Color newColor = Color.YellowGreen;
for(inti =0;i<carimage.Width;i++){
for(intj =0;j<carimage.Height;j++){
d = carimage.GetPixel(i, j);
if(d.R == 255 && d.G==255 && d.B == 255)
image.SetPixel(i, j, newColor );
}
}
simple thresholding will trow the second image where car body is not separated correctly. i tried Aforge.net Fill holes image filter but no significant change has been done to the threshold image. I tried to use color filter but it i did not return a correct output due to color vary of the body. can anyone suggest and solution for this?
Original Image
Threshold Image
Instead of thresholding, you might want to look into clustering.
As a quick&dirty test, I've increased the image brightness in HSB space (using Mathematica):
brightnessAdjusted = Image[
Map[#^{1, 1, 0.2} &, ImageData[ColorConvert[img, "HSB"]], {2}],
ColorSpace -> "HSB"]
Then I've used simple K-Nearest clustering:
(clusters = ClusteringComponents[ColorConvert[brightnessAdjusted, "RGB"], 3,
Method -> "KMeans"]) // Colorize
to find clusters of similar colors in the image (there are many more, probably more suitable clustering algorithms, so you should experiment a little). Then I can just adjust the color in one of the clusters:
Image[MapThread[If[#1 == 2, #2[[{1, 3, 2}]], #2] &, {clusters, ImageData[brightnessAdjusted]}, 2]]
If you want to use thresholding, you should probably use a CIE color space, since euclidian distances in that color space are closer to human perception.
I had a similar project few years ago. I can't remember the exact details, but the idea was to shift a (not too small) sliding window over the image, and calculate the average intensity (maybe for R, G and B separately) inside the window at each position. I filled a "threshold image" with these averages, and subtracted it from the original image. There was a scaling factor somewhere, and other tuning stuff, but the point is, such an approach was way better than using a constant threshold.
If you are going to use a set of thresholds, you might be better of selecting yellow hues in the Hue Saturation Value colorspace. See the related SO question.
I=imread('test.jpg');
I=im2double(rgb2gray(I));
BW=im2bw(I,0.64);imshow(BW)
Gives me :
I got the 0.64 threshold by looking at the image's histogram. I suggest you use MATLAB to do image processing as it is much easier. Hope that helps you in colouring the image.

Rendering 1 pixel lines with DrawingVisual

I have seen several examples on rendering 1 pixel lines in WPF, but none seem to apply to my situation. I am using DrawingVisual and DrawingContext to draw some shapes and RenderTargetBitmap and PngBitmapEncoder to generate the image. In many cases the rectangles have a 2 pixel border even though I set it to 1. I am guessing this is due to the resolution independent rendering that is used.
I have found several solutions but they are either in XAML or apply to drawing controls. The closest thing I have found is XSnappingGuidelines/YSnappingGuidelines but I cannot find a single example of how to use it. The documentation is very much lacking on these properties.
How do I disable the resolution independent rendering for DrawingVisual?
UPDATE:
Here is what I am trying to do:
Declare a DrawingVisual:
DrawingVisual mainTemplate = new DrawingVisual();
Get Context:
using (DrawingContext context = mainTemplate.RenderOpen())
Draw rectangle:
penToUse = new Pen(new SolidColorBrush(Color.FromRgb(0xFF, 0xFF, 0xFF)), 1.0);
penToUse.DashStyle = DashStyles.Dash;
context.DrawRectangle(brushToUse, penToUse, new Rect(left, top, width, height));
Where do I set the rendering mode to align to pixels?
jorj
In WPF, when you draw a line, the line is centered on the coordinates you specify. So if on a device with 96 DPI you draw a vertical line from 10, 10 to 10, 20 and the width of the pen is 1, the line will actually be drawn between 9.5 and 10.5 taking two pixels. If you want to align the line on the pixel edge, you need to shift it by 0.5. On a 120 DPI monitor, the width of the line should be 0.8 to take a single pixel, and you need to shift it by 0.4 to align on the pixel edge.
You do not have to use GuidelineSet because it doesn't do more than this simple shifting but unnecessarily complicates the code.
The closest I've come to being able to render single pixels lines in WPF with a DrawingContext is this:
GuidelineSet guidelines = new GuidelineSet();
guidelines.GuidelinesX.Add(_bgRect.Left - 0.5);
guidelines.GuidelinesX.Add(_bgRect.Right + 0.5);
guidelines.GuidelinesY.Add(_bgRect.Top - 0.5);
guidelines.GuidelinesY.Add(_bgRect.Bottom + 0.5);
dc.PushGuidelineSet(guidelines);
dc.DrawRectangle(Background, _outlinePen, _bgRect);
if (BorderThickness.Left > 1)
dc.DrawLine(_leftPen, _bgRect.TopLeft, _bgRect.BottomLeft);
if (BorderThickness.Top > 1)
dc.DrawLine(_topPen, _bgRect.TopLeft, _bgRect.TopRight);
if (BorderThickness.Right > 1)
dc.DrawLine(_rightPen, _bgRect.TopRight, _bgRect.BottomRight);
if (BorderThickness.Bottom > 1)
dc.DrawLine(_bottomPen, _bgRect.BottomRight, _bgRect.BottomLeft);
dc.Pop();

How to resolve anti-aliasing when changing color of an image in C#?

I am processing images to change their color from black to red, blue, green etc based on the requirement. I use SetPixel methods to change color of each pixel of the image from black to say red.
It works mostly fine except the borders and some curves within the image. Let's say I've circled image filled with black color. Circled image color is changed but still when I zoom, I can see blackish dots around border which is not completely replaced with red color. I tried to dig around and found that it has something to do with anti-aliasing.
Has anything faced similar problem or have thoughts/suggestions on how to fix this issue?
Many thanks in advnace for your help!
Regards,
Tanush
It can be related with anti-aliasing. Anti-aliasing essence is that the more pixel is closer to the edge (boundary of something) the more pixel color is blended with background color (or we can say that it is more 'transparent').
So the problem may be that you need not only to replace source color to destination color, but also pixels which were blended from source color to background color.
To achieve this you need:
1) Run edge detection algorithm of some kind - it may be simple or advanced as you want.
2) If pixel is near edge and pixel is near other pixel of your source color, then calculate it's opacity (1-transparency) factor- which will be
opacity = (pixel_color-background_color)/(source_color-background_color)
3) Now calculate your color to which you must replace current anti-aliased pixel:
new_color = background_color * (1-opacity) + opacity * target_color
And put this new_color instead of antialiased pixel.
In summary:
You need to detect antialiased pixels and replace them with your version of antialiased pixels.
Hardest part of algorithm is detection of antialiased pixels - because you can't be sure that you found all edge pixels with 100% probability. Also you can't be sure was pixel antialiased or was just made initially of such color). Because of this you may get some color noise in final product. But in any case it should be better than just sit and wait :)
good luck

Categories