Command `noise_poissondisk` does not preserve distance constraint

I’m playing with the noise_poissondisk command (by @garagecoder).
I’m trying to understand the algorithm in order to extend it with more options (non euclidean distance particularly). I’ve started my own implementation and when trying to check that the distance between points holds, I get unexpected results. But this problems also happens with the @garagecoder’s version !

For instance, if I do :

weird : check "${1=20}>0"
  512,512 noise_poissondisk $1

  # Check distance constraint.
  1,1,1,2 eval.. ">i?da_push([x,y]); end(resize(#-1,1,da_size(),1,2,0));"
  +transpose. r[-2,-1] {[w,w,1,2]},1 a[-2,-1] c
  f. "*X = [ i0,i1 ]; Y = [ i2,i3 ]; d = x==y?inf:norm(X - Y); [ d,0,0,0 ]" channels. 0
  e[] "\nMinimal point distance = "{im}"\n"

I get this :

$ gmic weird 20
[gmic]-0./ Start G'MIC interpreter.

Minimal point distance = 19.416488647460938

which is not expected, as we should have a minimal distance between point >20.

I’m wondering if @garagecoder may have some clues about this ? :slight_smile:

I’ve found why this happens for my version of the algorithm, but unfortunately it doesn’t seem to be the same reason for noise_poissondisk.

For me, that was because the sample points I’m inserting didn’t have integer coordinates, and when drawn, they were set at integer-rounded locations, which slightly change their actual coordinates!

It seem that has to do with the fact that you’re working with discrete unit, and 19.48 seem close enough to 20. I thought about this, but, I have yet to figure out how to find closest coordinates in dynamic image of coordinates to search for points and to erase them.

BTW @garagecoder , it’s probably not necessary to investigate because I’m almost done with my own implementation that actually preserve that distance constraint and seems to be slightly faster than your implementation (e.g. 0.089s vs 0.211s for a 500x500 image with R=5).
I’ll probably replace noise_poissondisk soon with this faster version.

While you’re working on it, here’s another interesting issue: the distribution is not maintained at the boundaries.
400,400,1,1 . repeat 1000 f. 0 noise_poissondisk. 24 j.. .,0,0,0,0,-1 done
poissoncumulated

I don’t know whether that’s also due to my implementation, it would be nice to fix it too.

1 Like

That’s my current implementation, working in 1D/2D/3D. It supports multiple types of norm (argument p_norm).
It does not directly create a WHD image, but just a list of point coordinates (and a list of their corresponding parent points).

My plans is to have something generic that can be used and extended for many filters, like extending it for non-rectangular domains (e.g. masked images), or having the possibility to guide the location of the newly sampled points (e.g. with the contour direction given by another image).
I’m not really sure it works properly in 3D now, I have to check.
Feel free to play with this implementation and fix it if needed :slight_smile:

#@cli sample_poissondisk : width>0,_height>0,_depth>0,_radius>1,_max_sample_attempts>0,_p_norm>0
+sample_poissondisk : check "isint($1) && $1>0 && isint(${2=$1}) && $2>0 && isint(${3=1}) && $3>0 && "\
                            "${4=20}>1 && isint(${5=30}) && $5>0 && ${6=2}>0"

  # Determine optimal list of neighbors to be tested.
  W,H,D,R,K=${1-5}
  C={$R/sqrt(sum([$W,$H,$D]>1))} # Cell size
  0 eval "
    dmin(dx,dy,dz,C) = (
      _dx = dx*C; _dy = dy*C; _dz = dz*C;
      norm$6(min(abs(_dx),abs(_dx + C),abs(_dx - C)),
            min(abs(_dy),abs(_dy + C),abs(_dy - C)),
            min(abs(_dz),abs(_dz + C),abs(_dz - C)));
    );
    for (n = 1, n<4, ++n,
      is_none_added = 1;
      for (k = -n, k<=n, ++k,
        for (j = -n, j<=n, ++j,
          for (i = -n, i<=n, ++i,
            abs(maxabs(i,j,k))==n && dmin(i,j,k,$C)<$R?(da_push([i,j,k]); is_none_added = 0)
          )
        )
      );
      is_none_added?break();
    );
    resize(#-1,1,da_size(),1,3,0)"
  f. "($W<2 && i0) || ($H<2 && i1) || ($D<2 && i2)?[8,8,8]:I" discard. 8 s. y,3 a[-3--1] x # Discard useless dimensions
  neighbors={^} rm.

  # Start point sampling.
  {ceil([$W,$H,$D]/$C)},1,-1 nm. grid # Grid used for quick testing of neighboring points
  1,32,1,3 nm. points # List of points
  1,32,1,1 nm. parents # List of parents
  1,32,1,1 nm. active # List of active indices
  eval "
    insert_active_point(X,ind_parent) = (
      i(#$grid,int(X/$C)) = new_ind = da_size(#$points);
      da_push(#$points,X);
      da_push(#$parents,ind_parent);
      da_push(#$active,new_ind);
    );
    neighbors = ["$neighbors"];

    # Initialize list of active points.
    X = int(u([$W,$H,$D] - 1e-4));
    insert_active_point(X,0);

    # Propagate new points.
    while (siz = da_size(#$active),
      indX = int(u(siz) - 1e-4);
      X = I[#$points,i[#$active,indX]];

      for (k = 0, k<$K, ++k,
        U = [ $W>1?u(-1,1):0, $H>1?u(-1,1):0, $D>1?u(-1,1):0 ];
        U*=u($R,1.5*$R)/(1e-9 + norm(U));
        Y = round(X + U);

        inrange(Y[0],0,$W - 1) && inrange(Y[1],0,$H - 1) && inrange(Y[2],0,$D - 1)?(
          P = int(Y/$C);
          indY = i(#$grid,P);
          indY<0?(
            is_sample_ok = 1;
            repeat(size(neighbors)/3,n, # Test neighboring points
              ind = i(#$grid,P + neighbors[3*n,3]);
              ind>=0?(nY = I[#$points,ind]; norm$6(nY - Y)<=$R?(is_sample_ok = 0; break())); # Invalid point
            );
            is_sample_ok?(insert_active_point(Y,i[#$active,indX]); break()); # Add new point
          );
        );
      );
      k>=$K?da_remove(#$active,indX); # Remove point from active list
    );
    resize(#$points,1,da_size(#$points),1,3,0);
    resize(#$parents,1,da_size(#$parents),1,1,0)
   "
  rm[grid,active]

it can be used like this:

$ gmic tic sample_poissondisk 400,400,1,15 toc 400,400 eval... "i(#-1,i0,i1)=1;I"

sp1

or, if you use the parent points to draw connected lines:

$ gmic tic sample_poissondisk 400,400,1,10 toc 400,400 eval... "X=[i0,i1];Y=I[i(#-2)][0,2];polygon(#-1,2,X,Y,1,1);I"

sp2

I’m pretty sure a generic version of this Poisson Disk Sampling algorithm could have lot of uses.

At the moment this also has the boundary problem. It is of course possible to extend and crop, but I’d like to know the reason! Sadly I won’t have time to look until tomorrow.

You might find it useful to review the original C implementation, it often provides better understanding than the paper (sorry, I don’t have the link to hand…)

1 Like

My guess is that’s because near borders, you have less possibilities to find a valid sample point to add. The probability to add a new sample point that is located near the border is then higher than elsewhere.

I think that allowing new sample points to be located outside the border (so the usual neighbor checks but ignoring them at the end) and stopping then the trials would solve the problem.

EDIT : That’s it. When i remove the ‘out-of-border’ check when adding new sample point, this removes the higher point density near the image borders.

1 Like

I’ve pushed a new version of noise_poissondisk in the filter update.
I hope I won’t break anything, but as far as I’ve tested it, it’s OK.
Let me know if you experience any issue with it.