1

I want to use skimage.draw.line to draw an aliased line through two pixels. The issue is when I overlay the original line, there is often a discrepancy due to rounding. Example:

Attempting line through points (22,30) and (25,39) in a 50x50 image. The full line is the green channel and the overlay line is the red channel. Yellow is overlap. The result I want would yield no visible red, only yellow and green. 50 by 50 pixel image with line through points 22 comma 30 and 25 comma 39

I tried generating a bigger line and then cropping, but still getting issues:

y0x0, ymxm=(np.array([0,0]), np.array([50,50]))
y1x1, y2x2=(np.array([22,30]), np.array([25,39]))
#rawzero3 is essentially trunc() but rounding up
#https://stackoverflow.com/q/46572699
yx_fin1=(y2x2-y1x1)*np.amin(\
    rawzero3((ymxm-(y2x2-y1x1))/(y2x2-y1x1)).astype(int))+y1x1
yx_fin2=(y0x0-1)-((y2x2-y1x1)*np.amin(\
    rawzero3((y0x0-(y2x2-y1x1))/(y2x2-y1x1)).astype(int))+y1x1)
zimg[tuple(map(tuple,np.array(draw.line(*yx_fin1,*yx_fin2)).T\
    [np.all((y0x0<=np.array(draw.line(*yx_fin1,*yx_fin2)).T)\
    &(np.array(draw.line(*yx_fin1,*yx_fin2)).T<ymxm), axis=1)].T))]=255

How can my desired result be achieved?

Edit

I updated my code to deal with issues outside of octant 0, similar to Bresenham's Line Algorithm:

def rawzero3(a):
    _a = np.abs(a)
    np.ceil(_a, _a) # inplace
    np.copysign(_a, a, out = _a)
    return _a
def fullline_pos(r0,c0,r1,c1,s):
    y0x0, ymxm=(np.array([0,0]), np.array(s))
    y1x1, y2x2=(np.array([r0,c0]), np.array([r1,c1]))
    mdyx=(y2x2-y1x1)//np.gcd.reduce(y2x2-y1x1)#this will always be a whole number, the // makes it an int
    yx_fin1=mdyx*np.amin(rawzero3((ymxm-y1x1)/mdyx).astype(int))+y1x1 #should be slightly more efficient
    yx_fin2=(mdyx*np.amin(rawzero3((y0x0-y1x1)/mdyx).astype(int))+y1x1) #should be slightly more efficient
    if v: print(f'y0x0 {y0x0}, ymxm {ymxm}, y1x1 {y1x1}, y2x2 {y2x2}, (y2x2-y1x1) {(y2x2-y1x1)}, yx_fin1 {yx_fin1}, yx_fin2 {yx_fin2}')
    return tuple(map(tuple,np.array(draw.line(*yx_fin1,*yx_fin2)).T[np.all((y0x0<=np.array(draw.line(*yx_fin1,*yx_fin2)).T)&(np.array(draw.line(*yx_fin1,*yx_fin2)).T<ymxm), axis=1)].T))
def fullline_neg(r0,c0,r1,c1,s):
    sr,sc=np.array(s)-1
    r0_,c0_,r1_,c1_=sr-r0,c0,sr-r1,c1
    coords=np.array(fullline_pos(r0_,c0_,r1_,c1_,s))
    coords[0]=sr-coords[0]
    return tuple(map(tuple,coords))
def fullline(r0,c0,r1,c1,s):#returns a 2xN array of valid yx coordinates
    #r0,c0,r1,c1 are int, s is a 2d shape
    assert (r0,c0)!=(r1,c1)
    if r0==r1: return draw.line(r0,0,r0,s[1]-1)
    if c0==c1: return draw.line(0,c0,s[0]-1,c1)
    if (r0<r1)==(c0<c1):
        return fullline_pos(r0,c0,r1,c1,s,v=v)
    else:
        return fullline_neg(r0,c0,r1,c1,s,v=v)

For visualization purposes, I ran fullline(y,x,25,25,(50,50)) for all x and y values 0 to 49. The following image shows the relative amount of errors (red pixels) for each y,x location: enter image description here

evn
  • 102
  • 2
  • 12
  • 1
    What is the original line? I suspect that (22, 30) and (25, 39) are not actually part of the line — you need to find the correct *fractional* coordinates to get the result you want. At any rate, though, this might not have (to my knowledge) a generic solution. There's always going to be choices to make with regards to where the aliasing happens (see that one 4-pixel long segment in the green line), and they are unlikely to be the same for a full line as for a segment... – Juan Dec 23 '20 at 00:17

0 Answers0