12

I need an algorithm which can be (a bit) slower than the Bresenham line drawing algorithm but has to be a lot more exact. With 'exact' I mean: every touched pixel should be printed. No more, but also no less! Which means using a more thick line or similar is not an option as too many pixels will be involved. Also I don't need a graphic framework or similar like it was asked before, I need the algorithm! The application is not really in 'graphics' it is in the geography area where pixels are 'tiles'.

The main problem for me is that I need subpixel precision which means that a line could start at 0.75/0.33 and not just at 0/0 like it is the case for integer values. I tried to create a working solution for the last several hours but cannot make it working - there are too many edge cases.

First I thought an anti-aliased version like the algorithm from Wu should make it but it prints too many pixels (especially for start and end points) and in certain cases it still misses some pixels e.g. for very short lines.

Then I tried to make Bresenham working where I replaced the second 'if' with 'else if' as pointed out here, and it is closer but still not there. Then I tried to move the Bresenham from integer- to float-precision which resulted in an endless loop (as the x,y values jumped over the finish condition if (y1 == y2 && x1 == x2)).

I could use the naive line drawing solution but which delta should I use? E.g. if I use 0.1 I will still miss some pixels and using smaller values it will probably take too long (and still miss pixels).

A working solution in C/Java/... would be appreciated. At least it should work for octant 1 but a full blown solution would be even nicer.

Update: I came up with the following idea: using the naive line rasterization and you can calculate 4 pixel-candidates for every point. Then check for those 4 pixels if the line really crosses them. But I'm not sure if line/box intersection can be fast enough.

Community
  • 1
  • 1
Karussell
  • 17,085
  • 16
  • 97
  • 197
  • "every touched pixel should be printed" even if 0.01 of a pixel or less intersects with the line? What shape does both ends of the line take (round, concave, convex, flat)? – Tarik Jul 10 '14 at 15:33
  • yes, if there is a mathmatical intersection it should be included (of course we can assume the common rounding error stuff). The ends of the line should be flat (no fluff or antialiasing, just the 'mathematical' end) – Karussell Jul 10 '14 at 15:42
  • what about color? is the line color constant or should be interpolated according to used area of pixel like anti-aliasing does? – Spektre Jul 10 '14 at 16:33
  • The color is not necessary. It is indeed the same problem MBo pointed in his answer (practical implementation): spatial subdivision and so I only need to know if the 'ray' hits a pixel or not. – Karussell Jul 10 '14 at 20:09
  • 1
    Huh, I see my answer [here](https://stackoverflow.com/a/66575406/12077532) would probably have fit this question better than that one. I believe my solution is exactly what this question is asking for. – FA-18_1 Mar 13 '21 at 05:37
  • It seems so. Thanks! Maybe you add it here (I'll upvote and later study it in detail) and link from the other answer to here? – Karussell Mar 14 '21 at 12:01

2 Answers2

8

If you need just constant color (not interpolated by used area of pixel) then use DDA:

void line_DDA_subpixel(int x0,int y0,int x1,int y1,int col) // DDA subpixel -> thick
    {
    int kx,ky,c,i,xx,yy,dx,dy;
    x1-=x0; kx=0; if (x1>0) kx=+1; if (x1<0) { kx=-1; x1=-x1; } x1++;
    y1-=y0; ky=0; if (y1>0) ky=+1; if (y1<0) { ky=-1; y1=-y1; } y1++;
    if (x1>=y1)
     for (c=x1,i=0;i<x1;i++,x0+=kx)
        {
        pnt(x0,y0,col); // this is normal pixel the two below are subpixels 
        c-=y1; if (c<=0) { if (i!=x1-1) pnt(x0+kx,y0,col); c+=x1; y0+=ky; if (i!=x1-1) pnt(x0,y0,col); }
        }
    else
     for (c=y1,i=0;i<y1;i++,y0+=ky)
        {
        pnt(x0,y0,col); // this is normal pixel the two below are subpixels 
        c-=x1; if (c<=0) { if (i!=y1-1) pnt(x0,y0+ky,col); c+=y1; x0+=kx; if (i!=y1-1) pnt(x0,y0,col); }
        }
    }

where:

void pnt(int x,int y,int col);

is routine that rasterize pixel (x,y) with color col The source is in C++

I think it is strait forward but anyway

DDA use parametric line equation y=k*x+q or x=ky+q dependent on the difference (if is bigger x or y difference so there are no holes). The k is dy/dx or dx/dy and the whole division is reduced to substraction+addition inside loop (last line of each loop). This can be easily modified to any number of dimensions (I usually use 7D or more with this). On modern machines is the speed sometimes better then Bresenham (depends on the Platform and usage).

This is how it looks like compared to simple DDA

DDA and DDA_subpixel lines

[edit2] double coordinates // originally [edit1]

OK here is new code:

void line_DDA_subpixel1(double x0,double y0,double x1,double y1,int col)    // DDA subpixel -> thick
    {
    int i,n,x,y,xx,yy;
    // prepare data n-pixels,x1,y1 is line dx,dy step per pixel
    x1-=x0; i=ceil(fabs(x1));
    y1-=y0; n=ceil(fabs(y1));
    if (n<i) n=i; if (!n) n=1;
    x1/=double(n);
    y1/=double(n); n++;
    // rasterize DDA line
    for (xx=x0,yy=y0,i=0;i<=n;i++,x0+=x1,y0+=y1)
        {
        // direct pixel
        pnt(x,y,col);
        // subpixels on change in both axises
        x=x0; y=y0;
        if ((i<n)&&(x!=xx)&&(y!=yy)) { pnt(xx,y,col); pnt(x,yy,col); }
        xx=x; yy=y;
        }
    }

And this is how it looks like:

DDA and DDA_subpixel lines double coordinates

Angle should be in double precision now but pnt(x,y,col) is still on integers !!!

[edit3] pixel grid crossing

void DDAf_line_subpixel(float x0,float y0,float x1,float y1,int col)    // DDA subpixel -> thick
    {
    int i,n; float a,a0,a1,aa,b,d;
    // end-points
    pnt(x0,y0,col);
    pnt(x1,y1,col);
    // x-axis pixel cross
    a0=1; a1=0; n=0;
    if (x0<x1) { a0=ceil(x0); a1=floor(x1); d=(y1-y0)/(x1-x0); a=a0; b=y0+(a0-x0)*d; n=fabs(a1-a0); } else
    if (x0>x1) { a0=ceil(x1); a1=floor(x0); d=(y1-y0)/(x1-x0); a=a0; b=y1+(a0-x1)*d; n=fabs(a1-a0); }
    if (a0<=a1) for (aa=a,i=0;i<=n;i++,aa=a,a++,b+=d) { pnt(aa,b,col); pnt( a,b,col); }
    // y-axis pixel cross
    a0=1; a1=0; n=0;
    if (y0<y1) { a0=ceil(y0); a1=floor(y1); d=(x1-x0)/(y1-y0); a=a0; b=x0+(a0-y0)*d; n=fabs(a1-a0); } else
    if (y0>y1) { a0=ceil(y1); a1=floor(y0); d=(x1-x0)/(y1-y0); a=a0; b=x1+(a0-y1)*d; n=fabs(a1-a0); }
    if (a0<=a1) for (aa=a,i=0;i<=n;i++,aa=a,a++,b+=d) { pnt(b,aa,col); pnt(b, a,col); }
    }

Finally had some time for this so I tweaked DDA a little but id lead to many ifs so I change rasterization quite a bit. Now all pixel grid crossing (intersections) are computed and then for each the right sub-pixel is added. This is how it looks like (no wrong sub-pixels):

line pixel crossing subpixel

For each x or y grid lines is the first cross point computed (a,b) and step is in one axis 1 pixel and in second the rest according to dy/dx or dx/dy. After this the for loop fill the sub-pixels ...

Spektre
  • 49,595
  • 11
  • 110
  • 380
  • if area percentage is needed then it can be derived from the state of variable c but I newer used it because I do not need it. – Spektre Jul 10 '14 at 17:14
  • 1
    This is a good specific-case algorithm for what the OP is looking for, but AFAICT this doesn't address the extremely likely cases where the line start and end coordinates are of non-integer values. – Kaganar Jul 10 '14 at 19:05
  • Will it work if I replace the int arguments with e.g. double? – Karussell Jul 10 '14 at 19:42
  • @Karussell yes but then it will be faster to make the dy/dx or dx/dy in classic manner. and also then you should add the subpixel point via if ((x-floor(x))>0.0) ... and the same goes for y – Spektre Jul 10 '14 at 23:26
  • @Karussell I would left it on integers as is and add these 2 subpoints per each endpoint if needed. – Spektre Jul 10 '14 at 23:29
  • I cannot use int as then the line will have a different angle than my original one – Karussell Jul 11 '14 at 06:13
  • @Karussell how big accuracy of angle in degree or radians you need ? – Spektre Jul 11 '14 at 07:08
  • @Karussell added [edit1] to answer with double coordinates and floating precision. – Spektre Jul 11 '14 at 07:49
  • @Kaganar added [edit1] now it should also cover that – Spektre Jul 11 '14 at 07:50
  • @Karussell added [edit2] with tested floating coordinates (have rewritten mine test app to support sub-pixel mouse coordinates) code has a bit change to increase line division precision – Spektre Jul 11 '14 at 08:51
  • Hmmh, but this has also the problem that it e.g. prints a pixel where there is no line. E.g. some of the bottom/left pixels. – Karussell Jul 11 '14 at 10:02
  • @Karussell yes because it does not care for line quadrant in that case there will be 2/4 (|dx|<|dy| and sign of dx,dy) ifs and only one sub-pixel point will be added instead but that needs to play with the code a bit (now I do not have time for that at least till Tuesday) – Spektre Jul 11 '14 at 11:24
  • @Karussel (returned earlier then expected:)) added [edit3] with new approach (accurate one) try it and let me know – Spektre Jul 14 '14 at 15:04
5

If your line is thin and pixels are rectangular (square):

enter image description here

then consider using of voxel grid traversal algorithms, for example, see article "Fast Voxel Traversal Algorithm..." by Woo and Amanatides.

Practical implementation (in grid traversal section)

Answer to comment:
Proper initialization for X-coordinate variables (the same for Y)

  DX = X2 - X1
  tDeltaX = GridCellWidth / DX
  tMaxX = tDeltaX * (1.0 - Frac(X1 / GridCellWidth)) 
  //Frac if fractional part of float, for example, Frac(1.3) = 0.3

example in my answer here

Community
  • 1
  • 1
MBo
  • 77,366
  • 5
  • 53
  • 86
  • Thanks! This looks good - I'll investigate! My pixels are rectangular but not quadratic so I think this can work. Your image suggests that the start or end point can be only in the center - is an arbitrary start or end possible too? – Karussell Jul 10 '14 at 15:54
  • 1
    You start in the tile containing your arbitrary starting position, and you end in the tile containing your arbitrary ending position. The traversal happens along the line that intersects the starting and ending points. So, yes, an arbitrary start and end is possible. – Kaganar Jul 10 '14 at 15:58
  • Yes, arbitrary positions are possible. – MBo Jul 10 '14 at 16:16
  • Thanks, looks good. I've implemented it here: https://gist.github.com/karussell/df699108fd8fbe0d44e1 will keep answer open for other ideas the next days though – Karussell Jul 10 '14 at 22:39
  • Would you mind adding an example on how to proper initialize tMaxX? I still have these one-off issues. E.g. if you go from (x1/y1)=1/3 to 3/0 it currently gives me `1/3, 1/2, 2/2, 2/1, 3/1, 3/0` where it should be `1/3, 1/2, 1/1, 2/1, 2/0, 3/0` – Karussell Jul 11 '14 at 10:08
  • Thanks, but saw this answer already and my code is very similar. The problem is that 1/3 is on the boundary of 4 cells and it assumes it already crossed the x value 1 and y value 3 or something. – Karussell Jul 11 '14 at 13:24
  • Algo should give sequence 1/3, 1/2, 1/1, 2/1, 2/0, 3/0 - it is correct (treating of corner-touched cells is special case - I've marked them blue). It seems that 2/2 cell is due to implementation issues. Do you starting and ending points lie on the same corner of cell rectangle? – MBo Jul 11 '14 at 15:13
  • Oh, it seems I understand - 1/3, 1/2, 2/2, 2/1, 3/1, 3/0 sequence is valid for starting and ending points in the center of cells (like my picture), while the second sequence is valid for points on the cell corner. Choose needed representation and sub-cell coordinates of starting and ending points – MBo Jul 11 '14 at 15:26
  • What do you mean with needed representation? – Karussell Jul 11 '14 at 15:46
  • How are your starting and ending points located relative to cell? – MBo Jul 11 '14 at 15:56
  • 1
    I implemented your java code (github link) in python for prototyping. And it didn't work for all cases (all combinations of start and end points). My cells are centered on integer numbers, my start and end points are arbitrary float values. I changed the following and it worked: `if stepX < 0: maxX = deltaX * (0.5 + (tmp - np.round(tmp))); else: maxX = deltaX * (0.5 - (tmp - np.round(tmp)));` Same for maxY using stepY. Added `;` as line ending sign in the code – Andreas Mar 17 '16 at 10:12