Why do adjacent rectangles create a banding effect?

235 views Asked by At

I've created some rectangles using the following code:

double width = 6.546;

for (int i = 0; i < 50; i++)
{
    var rect = new Rectangle()
    {
        Width = width,
        Height = i * 10,
        Fill = Brushes.Blue,
    };

    Canvas.SetLeft(rect, i * width);
    Canvas.SetBottom(rect, 0);

    canvas.Children.Add(rect);
}

Here is the result:

verticalbanding

There are vertical lines of varying lightness. Why do they appear? They can be avoided by using a large width value. Is there any other way to avoid them?

2

There are 2 answers

0
Eric Postpischil On BEST ANSWER

The key problem here is that Width = width sets the rectangle width to 6, but Canvas.SetLeft(rect, i * width) sets the positions of the rectangles to 0, 6, 13, 19, 26,… (as integers obtained by truncating products of approximately 0, 6.546, 13.092, 19.638, 26.184,…). As you can see, some of the positions are 7 units apart, such as 6 and 13 or 19 and 26. Therefore, the six-unit rectangle does not span the seven-unit distance.

In this case, setting the width with Width = ceil(width) (which is 7) guarantees that the rectangles are wide enough to span the distances between them.

Although this answer suggests that you use int numbers, it is not clear that this is desirable. If you convert the calculations to int, you must either set the positions to multiples of one integer (6 or 7) or figure out a way to calculate the same positions (0, 6, 13, 19, 26,…) by using integer arithmetic instead of floating-point. The former is a choice to change the drawing to suit the arithmetic, which is undesirable. The latter preserves the slope in your drawing but has the same banding issue.

That answer also suggests using a floating-point version of the drawing objects. That is a reasonable approach. Note, however, that this merely reduces the rounding errors (from integer units to the finer floating-point units, at this magnitude); it does not eliminate them. In large drawings, there may still be an occasional drawing artifact unless you take care to avoid it. So understanding the details is important. Even with floating-point, you would want to ensure the width is set to be at least as large as any change in position.

Another option is to change the scale of the canvas, if your drawing interface supports that. If you can multiply the scale by 1000 (but keep the final drawing the same size), then your scale changes from 6.546 to 6546, and you can set the width to exactly 6546 instead of 6000 or 7000. As you can see, we are back to the key issue: You must set the width to a value at least as great as the difference between any two positions.

As stated previously, setting the width to 7 would make this drawing acceptable. The reason I discuss other solutions is that, in some drawings, changing the width from the ideal of 6.546 to 7 increases the size of the object undesirably. The other solutions improve upon that by allowing you to maintain a width closer to the desired size.

8
Ehsan88 On

First, please add this line to the code (before adding Rectangle) to see how easily the problem goes away!

 canvas.SnapsToDevicePixels = true;

Contrary to what mentioned by some answers including the first of mine, this issue is not related to floating point numbers, rather it is related to the rendering system used in WPF called device-independent unit measurement but this might cause artifacts in devices operating at greater than 96 dots per inch (dpi). Microsoft decleared the issue:

... this dpi independence can create irregular edge rendering due to anti-aliasing. These artifacts, commonly seen as blurry or "soft" edges, can occur when the location of an edge falls in the middle of a device pixel rather than between device pixels (Reference)

But the solution is also provided by SnapToDevicePixels property. (see this)