Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Gradient banding #126

Open
lbWishmaster opened this issue Feb 14, 2020 · 17 comments
Open

Gradient banding #126

lbWishmaster opened this issue Feb 14, 2020 · 17 comments

Comments

@lbWishmaster
Copy link

lbWishmaster commented Feb 14, 2020

I don't know if I'm missing something but I can't get the Gradient smooth it just looks nasty.
it has these periodic lines..., Does anybody have an idea on how I can improve it.

Grad 1

//test code
procedure Gradient_Fill_Linear32(Dst : TBitmap32; StartPoint, EndPoint: TFloatPoint;       
 Outline : TArrayOfFloatPoint;  Colors : TGradColors32);
var
 FFilter : TLinearGradientPolygonFiller;
 FPolys : TArrayOfArrayOfFloatPoint;
begin

   FFilter := TLinearGradientPolygonFiller.Create();
  try
    Setlength(FPolys, 0);
    FPolys:= PolyPolygon(Outline);

    FFilter.StartPoint:= StartPoint;
    FFilter.EndPoint:=  EndPoint;
    FFilter.WrapMode:= {wmClamp;} TWrapMode(0);
    FFilter.Gradient.ClearColorStops;
    FFilter.Gradient.StartColor:= Colors[1];
    FFilter.Gradient.EndColor:=  Colors[2];


    PolyPolygonFS(Dst, FPolys, FFilter, pfWinding);
  finally
   FFilter.Free;
   Setlength(FPolys, 0);
  end;
end;
@andersmelander
Copy link
Member

If you examine the RGB values of your original image (not the one you have uploaded which contains JPEG artifacts) I'll bet you will find that the values are as they should be.

My guess is that the banding we're seeing is caused by one or more of several factors:

  • The RGB color space doesn't correspond very well to the way the human eye perceives colors (or gray tones).
  • You can only represent 256 different shades of gray with a 32-bit bitmap.
  • Even if you could create a 16-bit/channel bitmap, your 24-bit monitor can only display 256 different shades of gray.
  • Our monitors are not calibrated optimally for grayscale.

I think if you create a 32-bit color bitmap in Photoshop and fill it with a gradient then you will see the same banding.

The following was created with my own bitmap editor. Same banding.
image

I can think of two solutions to this problem:

  • Add noise
    I don't know what this kind of filter is called but what you is basically the reverse of a blur. For each pixel in the image you swap it with a random pixel in the neighborhood. You could also use a dither pattern to select the pixel being swapped.
  • Use dithering.
    Create a 16 bit/channel bitmap (you can't do this with Graphics32) and then dither it down to 8 bits/channel.

@andersmelander
Copy link
Member

I just realized that you could do the noise thing with a custom sampler. Here's a palette sampler that can apply noise:

type
  TNoisePaletteSampler = class(TPaletteSampler)
  private
    FNoiseLevel: TFloat;
  protected
    function NearestColorFromPalette(Sample: TFloat): TColor32; override;
    function InterpolatedColorFromPalette(Sample: TFloat): TColor32; override;
  public
    // Amplitude of noise
    property NoiseLevel: TFloat read FNoiseLevel write FNoiseLevel;
  end;

function TNoisePaletteSampler.InterpolatedColorFromPalette(Sample: TFloat): TColor32;
var
  Noise: TFloat;
begin
  Noise := Random * FNoiseLevel * 2 - FNoiseLevel;
  Sample := Constrain(Sample + Noise, 0, 1);

  Result := inherited InterpolatedColorFromPalette(Sample);
end;

function TNoisePaletteSampler.NearestColorFromPalette(Sample: TFloat): TColor32;
var
  Noise: TFloat;
begin
  Noise := Random * FNoiseLevel * 2 - FNoiseLevel;
  Sample := Constrain(Sample + Noise, 0, 1);

  Result := inherited NearestColorFromPalette(Sample);
end;

and the result with a noise level of +/- 4 (NoiseLevel = 1 / 256 * 4):

image

I think I would prefer the banding.

@lbWishmaster
Copy link
Author

lbWishmaster commented Feb 16, 2020

first thanks for your answer, I did some tests today

  1. I tested in Photoshop (RGB/8) (RGB/16) (RGB/32) Bit. although the 8 bit gave me the best result
    none of the three was as bad as the result I got from my test in G32.

  2. I tested various 3rdParty Components like the CyPanel/Gradient and the Gradient is smooth.
    url: "https://sourceforge.net/projects/tcycomponents/"

  3. I've also tested, where I created a small 200 x 200 Canvas/TBitmap32
    and Stretch it on a bigger Canvas/TBitmap32 using GR32_Resamplers.StretchTransfer()
    those lines became thicker/wider, Stretched. So the artifacts/lines have nothing to do with the monitor or the resolution of the Canvas.

for small components like Button, Progressbar, LED's the Gradient is fine. but for bigger components like a Panel it's not. so it's sucks if i have to rewrite all my components that are using Gradients.

Demo file Gradients.zip

oh and it's not just in grayscale you can choose any other color combination same result.
I get better results with some colors than with others.

My Monitor is x2 Dell U2715H (2560x1440)

@andersmelander
Copy link
Member

andersmelander commented Feb 16, 2020

Okay, so I just made a test with a 1x256 bitmap filled with a linear gradient going from black to white.
This should produce a bitmap with 256 distinct gray scale values - but it doesn't. There's only about 160 distinct values. Edit: Forgot to disable the noise filter. There's actually around 200 colors.
image

I'll investigate.

@andersmelander andersmelander self-assigned this Feb 16, 2020
@andersmelander
Copy link
Member

@lbWishmaster Your example is flagged by BitDefender as a trojan (Gen:Variant.Ursu.750045) on my system. Probably a false positive.

@andersmelander
Copy link
Member

I believe the problem is with the TColor32Gradient rasterization. If I replace that with a hardcoded palette going from $FF000000 to $FFFFFFFF then the output is correct:
image

Can you paste samples (1x256 and 256x256) of gray scale gradients as you believe they should be (make sure not to save as JPG). I don't have Photoshop installed anymore.

@andersmelander
Copy link
Member

I just realized that you are using the polygon gradient functions (GR32_ColorGradients) while I am using the generic resampler gradient functions (GR32_ColorGradient).

GR32_ColorGradients was originally authored by @AngusJohnson in 2012 while I believe GR32_ColorGradient was authored by Alex Rabochy sometime around 2002. The latter is not a part of Graphics32.

I can fix the problem in GR32_ColorGradient but I'm not using GR32_ColorGradients anywhere so I might not be able to fix the problem in that.

@andersmelander
Copy link
Member

So I've managed to fix almost all problems in the gradient functions I'm using. It turned out that I had enabled gradient smoothing which means it actually did a cosine gradient instead of a linear gradient. The remaining problems are caused by rounding errors but I'm down to 2-3 duplicate colors in a 256 color gradient.

However this doesn't really help you with your problem. I have tried to spot any problems in GR32_ColorGradients by just reading through the source but I haven't even been able to find out where the actual gradient table is being calculated. From what I have been able to understand my guess is that the banding is caused by rounding errors in the gradient calculation. The functions were probably never verified for accuracy.

I'll unassign myself from this issue now since there's nothing more I can do. Angus isn't active in Graphics32 anymore so it's unlikely that he will fix the problem and unless someone else is willing to have a go at it I'm afraid you'll have to try another solution.
You might want to consider the PegTop gradient functions. They are AFAIK the only ones that can use dithering to minimize the unavoidable banding due to colorspace limitations.

@andersmelander andersmelander changed the title Smooth Gradient Gradient banding Feb 17, 2020
@andersmelander andersmelander removed their assignment Feb 17, 2020
@lbWishmaster
Copy link
Author

thank you for your help and time. I guess i have to find different solution.

but that's one of the reasons why I avoid open source. no official support, the people who wrote a certain code moved on. after that try to find out what they did. and this is a great library (G32)
it's pity that the development was discontinued, besides some fixes.

@AngusJohnson
Copy link
Contributor

GR32_ColorGradients was originally authored by @AngusJohnson in 2012

And I'm still lurking here though, as Anders correctly states, I'm no longer actively contributing to this library. Nevertheless I'd still be happy to make suggestions except that GR32_ColorGradients has changed so much since my initial contribution that I don't see anything there that resembles what I originally wrote.

@lbWishmaster
Copy link
Author

@angus so I wouldn't mind if you can give it a go and help me out ;-)

but I have successfully integrated the Gradient part of the pegtop Lib. into my Component Pack.
just need some more changes/fixes. I mean the pegtop Lib. wasn't updated since 2005
so there are some incompatibilities with Delphi 10.3.3

@andersmelander
Copy link
Member

Hi @AngusJohnson Yes, I can see from the history that most changes was done by @CWBudde. Maybe he can fix it.

I think I've found the spot where the Lerp between the gradient colors takes place: TLinearGradientPolygonFiller.FillLinePositive and friends. There does seem to be an attempt to avoid rounding errors, using fixed precision math, but obviously that isn't enough.

Scale := 1 / (XOffset[1] - XOffset[0]);
IntScale := Round($7FFFFFFF * Scale);
IntValue := Round($7FFFFFFF * (XPos[0] - XOffset[0]) * Scale);

for X := XPos[0] to XPos[1] - 1 do
begin
  BlendMemEx(CombineReg(Colors[1], Colors[0], IntValue shr 23),
    Dst^, AlphaValues^);
  IntValue := IntValue + IntScale;

  Inc(Dst);
  Inc(AlphaValues);
end;

@lbWishmaster If you've decided to replacethe use of polgon gradients then you might consider using the same gradient functions I'm using. I have already fixed the problem in them and they work with Delphi 10.3.3. They don't support dithering though - yet.

@lbWishmaster
Copy link
Author

lbWishmaster commented Feb 19, 2020

you might consider using the same gradient functions I'm using.

@andersmelander for sure I can give it a shot. but i have a little more time on the weekend. day job :-(

@andersmelander
Copy link
Member

@lbWishmaster I've attached the units
Gradient.zip
...and here's the method that uses it:

procedure TBitmapEditorToolGradient.EndAction(Shift: TShiftState; Pos: TPoint; var State: TToolState);
begin
  BitmapEditor.RestoreState;

  // Draw gradient from new pos to start pos
  var Rasterizer := TRegularRasterizer.Create;
  try
    var PaletteSampler := TPaletteSampler.Create;
    try
      var GradientSampler := TLineGradientSampler.Create;
      try
        GradientSampler.Origin := FloatPoint(FStartPos.X, FStartPos.Y);
        GradientSampler.Stop := FloatPoint(Pos.X, Pos.Y);

        PaletteSampler.Sampler := GradientSampler;
        PaletteSampler.WrapMode := wmClamp;
        PaletteSampler.Interpolated := True;

        var Palette: TPalette32;

        var Gradient := TColor32Gradient.Create;
        try
          var GradientStop: TGradientStop32;

          GradientStop.Color := ActiveColor(Shift);
          GradientStop.Location := 0;
          GradientStop.Midpoint := 0.5;
          GradientStop.Smoothness := 0;
          Gradient.AddStop(GradientStop);

          GradientStop.Color := ActiveColor(Shift, True);
          GradientStop.Location := 1;
          Gradient.AddStop(GradientStop);

          Gradient.Rasterize(Palette);
        finally
          Gradient.Free;
        end;

        PaletteSampler.AssignPalette32(Palette);

        var SuperSampler := TSuperSampler.Create(PaletteSampler);
        try
          SuperSampler.SamplingX := 3;
          SuperSampler.SamplingY := 3;

          Rasterizer.Sampler := SuperSampler;

          Rasterizer.Rasterize(Buffer, Buffer.BoundsRect, CombineInfo(Buffer));
        finally
          SuperSampler.Free;
        end;
      finally
        GradientSampler.Free;
      end;
    finally
      PaletteSampler.Free;
    end;
  finally
    Rasterizer.Free;
  end;

  BitmapEditor.Changed(Caption);
end;

You can ignore the "BitmapEditor" stuff and if you're not using Delphi 10.3.3 then you'll have to move the var declarations.

@andersmelander
Copy link
Member

andersmelander commented Nov 27, 2020

I have now resolved this problem in my own gradient fill routines (not the same ones used by the polygon gradient filler) by adding optional dithering. I'm documenting the findings I made and the result here for the benefit of whomever tries to fix it in the polygon gradient fill.

First of all I discovered that there's a problem in the functions that generate a gradient palette. When creating a 256 entry palette going from black to white the result should be 256 distinct colors. For both Graphics32 and PegTop the generated palette contained less than 256 colors which of course meant that the gradient based on that palette was slightly wrong.

Next there's the problem of color depth in the palette. When the colors of the palette are being calculated by interpolating from one color stop to the next, they will be quantized down to 8 bits per channel since the palette stores the colors as TColor32 entries. This isn't a problem if the palette goes from one pure color to another pure color (e.g. black to white) since this transition can be represented in 256 entries without loss, but if the two colors are close (e.g. $7F7F7F to $A0A0A0) then the fractional colors can not be represented and there will be banding in the palette due to rounding errors. PegTop solves this with a 64-bit palette (16-bit per channel) and that is the solution I have used as well.

With that out the place I have been able to implement dithering based on the GR32_PaletteSamplers unit (not a part of standard Graphics32). I have implemented the following dithering methods:

Here are the results. I first filled a 512x512 bitmap diagonally with a black to white gradient. I then cropped this bitmap to 64x64 and expanded the dynamic range to make the banding visible. I did the same for a 32x32 bitmap to make it even more visible.
The 512x512 images below are scaled in the HTML to 128x128. Click on the images to view them at their actual size, without any artifacts produced by the browser scaling the images.

No dithering
normal 64x64normal 32x32

Bayer Ordered dither
bayer 64x64bayer 32x32

White Noise dither
white noise 64x64white noise 32x32

Blue Noise (LDR) dither
blue noise ldr 64x64blue noise ldr 32x32

Interleaved Gradient Noise dither
interleaved noise 64x64interleaved noise 32x32

Edit: I initially uploaded the wrong versions of the blue noise and interleaved gradient images above. I have updated the post with the correct images.

While the White and Blue Noise dither may appear similar they differ in the frequency spectrum of the noise they apply.

References:
[1] Blue-noise Dithered Sampling, Iliyan Georgiev and Marcos Fajard
[2] Next Generation Post Processing in Call of Duty: Advanced Warfare, Jorge Jimenez

@AngusJohnson
Copy link
Contributor

AngusJohnson commented Nov 28, 2020 via email

@CWBudde
Copy link
Contributor

CWBudde commented Feb 28, 2021

I haven't found time to test it, but it looks very useful.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants