From a7f5b1bcf688e8476f134c00e0a258b4f25aa9bf Mon Sep 17 00:00:00 2001 From: dakotagoldberg <dakotaelle@icloud.com> Date: Thu, 14 Dec 2023 13:23:05 -0500 Subject: [PATCH] Add trace_skeleton --- host/trace_skeleton_old.py | 334 +++++++++++++++++++++++++++++++++++++ 1 file changed, 334 insertions(+) create mode 100644 host/trace_skeleton_old.py diff --git a/host/trace_skeleton_old.py b/host/trace_skeleton_old.py new file mode 100644 index 0000000..280189a --- /dev/null +++ b/host/trace_skeleton_old.py @@ -0,0 +1,334 @@ +# trace_skeleton.py +# Trace skeletonization result into polylines +# +# Lingdong Huang 2020 + +import numpy as np + +# binary image thinning (skeletonization) in-place. +# implements Zhang-Suen algorithm. +# http://agcggs680.pbworks.com/f/Zhan-Suen_algorithm.pdf +# @param im the binary image +def thinningZS(im): + prev = np.zeros(im.shape,np.uint8); + while True: + im = thinningZSIteration(im,0); + im = thinningZSIteration(im,1) + diff = np.sum(np.abs(prev-im)); + if not diff: + break + prev = im + return im + +# 1 pass of Zhang-Suen thinning +def thinningZSIteration(im, iter): + marker = np.zeros(im.shape,np.uint8); + for i in range(1,im.shape[0]-1): + for j in range(1,im.shape[1]-1): + p2 = im[(i-1),j] ; + p3 = im[(i-1),j+1]; + p4 = im[(i),j+1] ; + p5 = im[(i+1),j+1]; + p6 = im[(i+1),j] ; + p7 = im[(i+1),j-1]; + p8 = im[(i),j-1] ; + p9 = im[(i-1),j-1]; + A = (p2 == 0 and p3) + (p3 == 0 and p4) + \ + (p4 == 0 and p5) + (p5 == 0 and p6) + \ + (p6 == 0 and p7) + (p7 == 0 and p8) + \ + (p8 == 0 and p9) + (p9 == 0 and p2); + B = p2 + p3 + p4 + p5 + p6 + p7 + p8 + p9; + m1 = (p2 * p4 * p6) if (iter == 0 ) else (p2 * p4 * p8); + m2 = (p4 * p6 * p8) if (iter == 0 ) else (p2 * p6 * p8); + + if (A == 1 and (B >= 2 and B <= 6) and m1 == 0 and m2 == 0): + marker[i,j] = 1; + + return np.bitwise_and(im,np.bitwise_not(marker)) + + +def thinningSkimage(im): + from skimage.morphology import skeletonize + return skeletonize(im).astype(np.uint8) + +def thinning(im): + try: + return thinningSkimage(im) + except: + return thinningZS(im) + +#check if a region has any white pixel +def notEmpty(im, x, y, w, h): + return np.sum(im) > 0 + + +# merge ith fragment of second chunk to first chunk +# @param c0 fragments from first chunk +# @param c1 fragments from second chunk +# @param i index of the fragment in first chunk +# @param sx (x or y) coordinate of the seam +# @param isv is vertical, not horizontal? +# @param mode 2-bit flag, +# MSB = is matching the left (not right) end of the fragment from first chunk +# LSB = is matching the right (not left) end of the fragment from second chunk +# @return matching successful? +# +def mergeImpl(c0, c1, i, sx, isv, mode): + + B0 = (mode >> 1 & 1)>0; # match c0 left + B1 = (mode >> 0 & 1)>0; # match c1 left + mj = -1; + md = 4; # maximum offset to be regarded as continuous + + p1 = c1[i][0 if B1 else -1]; + + if (abs(p1[isv]-sx)>0): # not on the seam, skip + return False + + # find the best match + for j in range(len(c0)): + p0 = c0[j][0 if B0 else -1]; + if (abs(p0[isv]-sx)>1): # not on the seam, skip + continue + + d = abs(p0[not isv] - p1[not isv]); + if (d < md): + mj = j; + md = d; + + if (mj != -1): # best match is good enough, merge them + if (B0 and B1): + c0[mj] = list(reversed(c1[i])) + c0[mj] + elif (not B0 and B1): + c0[mj]+=c1[i] + elif (B0 and not B1): + c0[mj] = c1[i] + c0[mj] + else: + c0[mj] += list(reversed(c1[i])) + + c1.pop(i); + return True; + return False; + +HORIZONTAL = 1; +VERTICAL = 2; + +# merge fragments from two chunks +# @param c0 fragments from first chunk +# @param c1 fragments from second chunk +# @param sx (x or y) coordinate of the seam +# @param dr merge direction, HORIZONTAL or VERTICAL? +# +def mergeFrags(c0, c1, sx, dr): + for i in range(len(c1)-1,-1,-1): + if (dr == HORIZONTAL): + if (mergeImpl(c0,c1,i,sx,False,1)):continue; + if (mergeImpl(c0,c1,i,sx,False,3)):continue; + if (mergeImpl(c0,c1,i,sx,False,0)):continue; + if (mergeImpl(c0,c1,i,sx,False,2)):continue; + else: + if (mergeImpl(c0,c1,i,sx,True,1)):continue; + if (mergeImpl(c0,c1,i,sx,True,3)):continue; + if (mergeImpl(c0,c1,i,sx,True,0)):continue; + if (mergeImpl(c0,c1,i,sx,True,2)):continue; + + c0 += c1 + + +# recursive bottom: turn chunk into polyline fragments; +# look around on 4 edges of the chunk, and identify the "outgoing" pixels; +# add segments connecting these pixels to center of chunk; +# apply heuristics to adjust center of chunk +# +# @param im the bitmap image +# @param x left of chunk +# @param y top of chunk +# @param w width of chunk +# @param h height of chunk +# @return the polyline fragments +# +def chunkToFrags(im, x, y, w, h): + frags = [] + on = False; # to deal with strokes thicker than 1px + li=-1; lj=-1; + + # walk around the edge clockwise + for k in range(h+h+w+w-4): + i=0; j=0; + if (k < w): + i = y+0; j = x+k; + elif (k < w+h-1): + i = y+k-w+1; j = x+w-1; + elif (k < w+h+w-2): + i = y+h-1; j = x+w-(k-w-h+3); + else: + i = y+h-(k-w-h-w+4); j = x+0; + + if (im[i,j]): # found an outgoing pixel + if (not on): # left side of stroke + on = True; + frags.append([[j,i],[x+w//2,y+h//2]]) + else: + if (on):# right side of stroke, average to get center of stroke + frags[-1][0][0]= (frags[-1][0][0]+lj)//2; + frags[-1][0][1]= (frags[-1][0][1]+li)//2; + on = False; + li = i; + lj = j; + + if (len(frags) == 2): # probably just a line, connect them + f = [frags[0][0],frags[1][0]]; + frags.pop(0); + frags.pop(0); + frags.append(f); + elif (len(frags) > 2): # it's a crossroad, guess the intersection + ms = 0; + mi = -1; + mj = -1; + # use convolution to find brightest blob + for i in range(y+1,y+h-1): + for j in range(x+1,x+w-1): + s = \ + (im[i-1,j-1]) + (im[i-1,j]) +(im[i-1,j+1])+\ + (im[i,j-1] ) + (im[i,j]) + (im[i,j+1])+\ + (im[i+1,j-1]) + (im[i+1,j]) + (im[i+1,j+1]); + if (s > ms): + mi = i; + mj = j; + ms = s; + elif (s == ms and abs(j-(x+w//2))+abs(i-(y+h//2)) < abs(mj-(x+w//2))+abs(mi-(y+h//2))): + mi = i; + mj = j; + ms = s; + + if (mi != -1): + for i in range(len(frags)): + frags[i][1]=[mj,mi] + return frags; + + +# Trace skeleton from thinning result. +# Algorithm: +# 1. if chunk size is small enough, reach recursive bottom and turn it into segments +# 2. attempt to split the chunk into 2 smaller chunks, either horizontall or vertically; +# find the best "seam" to carve along, and avoid possible degenerate cases +# 3. recurse on each chunk, and merge their segments +# +# @param im the bitmap image +# @param x left of chunk +# @param y top of chunk +# @param w width of chunk +# @param h height of chunk +# @param csize chunk size +# @param maxIter maximum number of iterations +# @param rects if not null, will be populated with chunk bounding boxes (e.g. for visualization) +# @return an array of polylines +# +def traceSkeleton(im, x, y, w, h, csize, maxIter, rects): + + frags = [] + + if (maxIter == 0): # gameover + return frags; + if (w <= csize and h <= csize): # recursive bottom + frags += chunkToFrags(im,x,y,w,h); + return frags; + + ms = im.shape[0]+im.shape[1]; # number of white pixels on the seam, less the better + mi = -1; # horizontal seam candidate + mj = -1; # vertical seam candidate + + if (h > csize): # try splitting top and bottom + for i in range(y+3,y+h-3): + if (im[i,x] or im[(i-1),x] or im[i,x+w-1] or im[(i-1),x+w-1]): + continue + + s = 0; + for j in range(x,x+w): + s += im[i,j]; + s += im[(i-1),j]; + + if (s < ms): + ms = s; mi = i; + elif (s == ms and abs(i-(y+h//2))<abs(mi-(y+h//2))): + # if there is a draw (very common), we want the seam to be near the middle + # to balance the divide and conquer tree + ms = s; mi = i; + + if (w > csize): # same as above, try splitting left and right + for j in range(x+3,x+w-2): + if (im[y,j] or im[(y+h-1),j] or im[y,j-1] or im[(y+h-1),j-1]): + continue + + s = 0; + for i in range(y,y+h): + s += im[i,j]; + s += im[i,j-1]; + if (s < ms): + ms = s; + mi = -1; # horizontal seam is defeated + mj = j; + elif (s == ms and abs(j-(x+w//2))<abs(mj-(x+w//2))): + ms = s; + mi = -1; + mj = j; + + nf = []; # new fragments + if (h > csize and mi != -1): # split top and bottom + L = [x,y,w,mi-y]; # new chunk bounding boxes + R = [x,mi,w,y+h-mi]; + + if (notEmpty(im,L[0],L[1],L[2],L[3])): # if there are no white pixels, don't waste time + if(rects!=None):rects.append(L); + nf += traceSkeleton(im,L[0],L[1],L[2],L[3],csize,maxIter-1,rects) # recurse + + if (notEmpty(im,R[0],R[1],R[2],R[3])): + if(rects!=None):rects.append(R); + mergeFrags(nf,traceSkeleton(im,R[0],R[1],R[2],R[3],csize,maxIter-1,rects),mi,VERTICAL); + + elif (w > csize and mj != -1): # split left and right + L = [x,y,mj-x,h]; + R = [mj,y,x+w-mj,h]; + if (notEmpty(im,L[0],L[1],L[2],L[3])): + if(rects!=None):rects.append(L); + nf+=traceSkeleton(im,L[0],L[1],L[2],L[3],csize,maxIter-1,rects); + + if (notEmpty(im,R[0],R[1],R[2],R[3])): + if(rects!=None):rects.append(R); + mergeFrags(nf,traceSkeleton(im,R[0],R[1],R[2],R[3],csize,maxIter-1,rects),mj,HORIZONTAL); + + frags+=nf; + if (mi == -1 and mj == -1): # splitting failed! do the recursive bottom instead + frags += chunkToFrags(im,x,y,w,h); + + return frags + + +if __name__ == "__main__": + import cv2 + import random + + im0 = cv2.imread("../test_images/opencv-thinning-src-img.png") + + im = (im0[:,:,0]>128).astype(np.uint8) + + # for i in range(im.shape[0]): + # for j in range(im.shape[1]): + # print(im[i,j],end="") + # print("") + # print(np.sum(im),im.shape[0]*im.shape[1]) + im = thinning(im); + + # cv2.imshow('',im*255);cv2.waitKey(0) + + rects = [] + polys = traceSkeleton(im,0,0,im.shape[1],im.shape[0],10,999,rects) + + + for l in polys: + c = (200*random.random(),200*random.random(),200*random.random()) + for i in range(0,len(l)-1): + cv2.line(im0,(l[i][0],l[i][1]),(l[i+1][0],l[i+1][1]),c) + + cv2.imshow('',im0);cv2.waitKey(0) + -- GitLab