0
$\begingroup$

I tried to follow Nathan Reed post Antialiasing: To Splat Or Not to implement splat method. The image I am getting thorugh the Lanczos filter is very different than Nathan. I do not understand where the problem is.

According to Wikipedia page Lanczos filter is defined as either of below formulas:

enter image description here

enter image description here

Nathan implementation seems to have missed a division by pi**2 and a multiplication by filter radius.

elif options.kernel == 'lanczos':
        weight = 
          (np.sin(np.pi * sampleOffsetX) * np.sin(np.pi * sampleOffsetY) *
           np.sin(np.pi * sampleOffsetX / options.radius) * 
           np.sin(np.pi * sampleOffsetY / options.radius) /                           
          (sampleOffsetX**2 * sampleOffsetY**2))

Here is my code which borrowed most of the filter implementation from pbrt code:

private void btn_Render_Click(object sender, EventArgs e)
{
    Random rand = new Random();
    int samples = 16;
    ImageFilm film = new ImageFilmSplat(pictureBox1.Width, pictureBox1.Height);
    film.Filter = new SincFilter(tau: 4, width: 4);

    for (int i = 0; i < film.Width; i++)
    {
        for (int j = 0; j < film.Height; j++)
        {
            EvaluatePixel(i, j, samples, rand, film);
        }
    }

    for (int i = 0; i < film.Width; i++)
    {
        for (int j = 0; j < film.Height; j++)
        {
            Pixel px = film.GetPixel(i, j);

            if (px.filterWeightSum != 0)
            {
                px.radiance = px.radiance / px.filterWeightSum;

                film.SetPixel(i, j, px);
            }
        }
    }

    pictureBox1.Image = film.GetImage();
}

void EvaluatePixel(int i, int j, int nSamples, Random rand, ImageFilm film)
{
    Vector halfpixel = new Vector(.5 / film.Width, .5 / film.Height, 0);

    //all samples between [0,1]
    Sample[] samples = sampler.Random(rand, nSamples);

    for (int n = 0; n < nSamples; n++)
    {
        //set sample coord at centre of the pixel
        double cx = ((double)i / film.Width) + halfpixel.X;
        double cy = ((double)j / film.Height) + halfpixel.Y;

        //jitter the centre
        cx += ((samples[n].X * 2) - 1) * halfpixel.X
        cy += ((samples[n].Y * 2) - 1) * halfpixel.Y

        //sample the function
        double f = func(cx, cy);

        film.AddToPixel(i, j, new Vector(f));
    }
}

//ImageFilm class
public void AddToPixel(int i, int j, Vector radiance)
{ 
    //splat the value to neighbor pixels
    //find neighbour pixels based on filter width
    int x0 = (int)Math.Max(0, i - Filter.Width);
    int x1 = (int)Math.Min(Width - 1, i + Filter.Width);
    int y0 = (int)Math.Max(0, j - Filter.Width);
    int y1 = (int)Math.Min(Height - 1, j + Filter.Width);

    for (int x = x0; x <= x1; x++)
    {
        for (int y = y0; y <= y1; y++)
        {
            double weight = this.Filter.eval(x - i, y - j);

            Pixel pxl = GetPixel(x, y);

            pxl.radiance += radiance * weight;
            pxl.filterWeightSum += weight;

            SetPixel(x, y, pxl);
        }
    }
}

public class SincFilter
{
    private double tau;

    public SincFilter(double tau1, double width)
    {
        Width = width;
        tau = tau1;
    }
    double Sinc(double x)
    {
        x = Math.Abs(x);
        if (x < 1e-5) return 1;
        return Math.Sin(Math.PI * x) / (Math.PI * x);
    }
    double WindowedSinc(double x, double radius)
    {
        x = Math.Abs(x);
        if (x > radius) return 0;
        return Sinc(x) * Sinc(x / tau);
    }
    public override double eval(double x, double y)
    {
        return WindowedSinc(x, Width) * WindowedSinc(y, Width);
    }
}

double func(double x, double y)
{
    double minPeriod = 2e-5;
    double maxPeriod = 0.2;

    double period = minPeriod + (maxPeriod - minPeriod) * (y * y);
    double phase = x / period;
    phase -= Math.Floor(phase);

    return Math.Round(phase);
}

Here is Nathan image :

enter image description here

And here is mine for lanczos filter width 4, tau 4

enter image description here

My implementation is quite noisy for some reason. It doesn't look any better than box filter.

$\endgroup$
  • 1
    $\begingroup$ I notice in Nathan's version there is a linear->sRGB conversion which may account for his version looking brighter & flatter. I would suggest looking at stratifying the samples too to lower graininess, unless your sampler.Random() already does that ? $\endgroup$ – PaulHK Mar 19 at 6:08
  • $\begingroup$ Yes I have tried stratified samples too which improves it but nothing like Nathan's. Also I wonder the second formula in Wiki page should be modified to "a**2" in the numerator instead of "a"? $\endgroup$ – ali Mar 19 at 10:10
  • 1
    $\begingroup$ Ideally a minimum of 256 samples should be used so that it can converge on the full 0-255 RGB output range accurately. As most of your samples are going to be either black or white in your input image, 16 average samples are going to give you a none-smooth / quantised range. Although your filter is going to negate most of that, I didn't study too closely how your filter is working. I still think the linear->sRGB is responsible for most of Nathans 'flatness' $\endgroup$ – PaulHK Mar 19 at 10:19
  • 2
    $\begingroup$ BTW, the factor $a/\pi^2$ in the weights is not needed because the final image is normalized by the sum of weights anyway, so any constant factor will cancel out. $\endgroup$ – Nathan Reed Mar 19 at 16:31
  • 2
    $\begingroup$ Re: sRGB, no it's not tone-mapping, it's a conventional nonlinear encoding of pixel values in images to obtain better precision in the dark values where our eyes are more sensitive. You can read here for more: en.wikipedia.org/wiki/… $\endgroup$ – Nathan Reed Mar 19 at 16:36

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.