Search code examples
pythonopencvimage-processingcomputer-vision

Break the curve wherever there is a significant change in the curve


Wherever there's a significant change in the curve, I need to break the curve with a break thickness of just 1 pixel (I just want to break the curves into multiple parts). I have attached an image for reference. So after i read the image, i am thinning the curve and wherever there are red dots, i need to split it around that area. Red dots indicate the places where I want the image to be cut

This is the output that I am currently getting

Unaltered Image

The first image is the input image and red dots indicate where I want the cut (the image will not actually have the red dot)/ The second image is the current output that I am getting. The third image is the unaltered image for reference.

I have tried implementing the following codes:

import cv2
import numpy as np
from matplotlib import pyplot as plt

image_path = rf'C:\Users\User\Desktop\output.png'
img = cv2.imread(image_path, 0)

ret, binary = cv2.threshold(img, 128, 255, cv2.THRESH_BINARY)


binary = cv2.ximgproc.thinning(binary, thinningType=cv2.ximgproc.THINNING_GUOHALL)

coords = np.column_stack(np.where(binary > 0))
def calculateAngle(p1, p2, p3):
    v1 = np.array(p2) - np.array(p1)
    v2 = np.array(p3) - np.array(p2)
    angle = np.arctan2(v2[1], v2[0]) - np.arctan2(v1[1], v1[0])
    angle = np.degrees(angle)
    if angle < 0:
        angle += 360
    return angle

startBlackImg = np.zeros((binary.shape[0], binary.shape[1], 1), np.uint8)
i = 1
while i < (len(coords) - 1):
    p1 = coords[i - 1]
    p2 = coords[i]
    p3 = coords[i + 1]
    i += 1
    angle = calculateAngle(p1, p2, p3)
    
    if angle < 45 or angle > 315:
        startBlackImg[p2[0], p2[1]] = 255
    else:
        startBlackImg[p2[0], p2[1]] = 0

cv2.namedWindow('Check', 0)
cv2.imshow('Check', startBlackImg)
cv2.waitKey(0)
cv2.destroyAllWindows()

and the other logic is

while kk < len(cutContour) - 10:
            xCdte = cutContour[kk][0][0]
            yCdte = cutContour[kk][0][1]
            xNextCdte = cutContour[kk + 10][0][0]
            yNextCdte = cutContour[kk + 10][0][1]
            kk += 1
            if totalDistance <= 0.3048:
                startBlackImg[yCdte, xCdte] = np.array([255, 255, 255])
                startBlackImg[yNextCdte, xNextCdte] = np.array([255, 255, 255])
            else:
                if (abs(xCdte - xNextCdte) < 10 and abs(yCdte - yNextCdte) >= 10) or (abs(xCdte - xNextCdte) >= 10 and abs(yCdte - yNextCdte) < 10):
                    startBlackImg[yCdte, xCdte] = np.array([255, 255, 255])
                    startBlackImg[yNextCdte, xNextCdte] = np.array([255, 255, 255])
                else:
                    startBlackImg[yCdte, xCdte] = np.array([0, 0, 0])
                    kk += 10

So far I am not getting what I want. Its breaking at multiple points and not just where I intend it to. Is there any library or and code to do this?


Solution

    1. Hough Transform https://en.wikipedia.org/wiki/Hough_transform

    enter image description here

    1. K-means iterations with different K's - find how many line groups

    enter image description here

    1. K-means again with the right K

    enter image description here

    enter image description here

    1. Pixel clustering by min distance to lines

    enter image description here

    import math
    
    import cv2 as cv
    import numpy as np
    
    import matplotlib.pyplot as plt
    
    orig_im = cv.imread("/home/ophir/temp/stackoverflow2.png",cv.IMREAD_GRAYSCALE)
    
    # use Hough Transform to get lots of straight lines
    lines = cv.HoughLines(orig_im, 1, np.pi/180, 30);
    
    im = cv.cvtColor(orig_im, cv.COLOR_GRAY2BGR)
    
    im2 = im.copy()
    im3 = im.copy()
    
    plt.figure()
    plt.imshow(im)
    
    # draw all straight lines on image
    rho_vals = []
    theta_vals = []
    for line in lines:
        for rho,theta in line:
            rho_vals.append(rho)
            theta_vals.append(theta)
            a = np.cos(theta)
            b = np.sin(theta)
            x0 = a*rho
            y0 = b*rho
            x1 = int(x0 + 1000*(-b))
            y1 = int(y0 + 1000*(a))
            x2 = int(x0 - 1000*(-b))
            y2 = int(y0 - 1000*(a))
    
            cv.line(im2,(x1,y1),(x2,y2),(0,0,255),2)
    
    rho_vals = np.array(rho_vals)
    rho_vals = np.expand_dims(rho_vals, axis=0)
    
    theta_vals = np.array(theta_vals)
    theta_vals = np.expand_dims(theta_vals, axis=0)
    
    Z = np.vstack((rho_vals,theta_vals)).T
    
    Z = np.float32(Z)
    
    # use K-means to cluster all straight lines to groups
    # I don't know how many groups, so I check several K's
    criteria = (cv.TERM_CRITERIA_EPS + cv.TERM_CRITERIA_MAX_ITER, 10, 1.0)
    compactness = []
    for i in range(2, 9):
        ret,label,center=cv.kmeans(Z,i + 1,None,criteria,10,cv.KMEANS_RANDOM_CENTERS)
        compactness.append(ret)
    
    compactness = np.array(compactness)
    
    # choose the right k, where the compactness isn't getting any better
    derivative = compactness[1:] - compactness[:-1]
    amax = np.argmax(derivative > -800) + 2
    
    # do K-means again, this time with the right K
    ret,label,center=cv.kmeans(Z, amax,None,criteria,10,cv.KMEANS_RANDOM_CENTERS)
    
    # draw the centers of thr clusters on the lines parameters graph
    lines = []
    for rho, theta in center:
        a = np.cos(theta)
        b = np.sin(theta)
        x0 = a*rho
        y0 = b*rho
        x1 = int(x0 + 1000*(-b))
        y1 = int(y0 + 1000*(a))
        x2 = int(x0 - 1000*(-b))
        y2 = int(y0 - 1000*(a))
    
        lines.append((x1,y1,x2,y2))
        cv.line(im3,(x1,y1),(x2,y2),(0,0,255),2)
    
    # cluster the pixels in the original image by the minimum distance to a line
    pixels = cv.findNonZero(orig_im)
    labels = []
    for pixel in pixels:
        x0, y0 = pixel[0]
        distances = []
        for line in lines:
            x1, y1, x2, y2 = line
            # https://en.wikipedia.org/wiki/Distance_from_a_point_to_a_line
            dist = abs((y2 - y1) * x0 - (x2 - x1) * y0 + x2 * y1 - y2 * x1) / math.sqrt((y2 - y1)**2 + (x2 - x1)**2)
            distances.append(dist)
        labels.append(np.argmin(np.array(distances)))
    
    im4 = np.zeros(im2.shape)
    
    colors = [[0, 0, 255], [0, 255, 0], [255, 0, 0], [255, 255, 0], [255, 0, 255], [0, 255, 255]]
    
    # assign different color to each pixel by the label of the clustering
    for pixel, label in zip(pixels, labels):
        x, y = pixel[0]
        color = colors[label]
        im4[y,x, 0] = color[0]
        im4[y,x, 1] = color[1]
        im4[y,x, 2] = color[2]
    
    plt.figure()
    plt.plot(list(range(2,9)), compactness)
    plt.scatter(amax, compactness[amax - 2], c = 'r')
    plt.ylabel("compactness")
    plt.xlabel("K")
    plt.title("K-means compactness")
    
    plt.figure()
    plt.imshow(im2)
    
    plt.figure()
    plt.imshow(im3)
    
    plt.figure()
    plt.imshow(im4)
    
    plt.figure()
    plt.scatter(rho_vals, theta_vals)
    plt.scatter(center[:,0],center[:,1],s = 80,c = 'y', marker = 's')
    plt.xlabel("rho")
    plt.ylabel("theta")
    plt.title("lines parameters")
    plt.show()
    

    It's not perfect, there are still some issues, but you get the idea.