Home > front end >  How to identify curved lines in an image and take their lengths based on a scale
How to identify curved lines in an image and take their lengths based on a scale

Time:07-08

I am trying to automate the measurement of several curved lines based on a scale (see the example image). I have several images like the example one that I have to extract the measurement of each line. I have managed to binarize the image and have been searching for a solution with opencv in python. The procedure that I thought the algorithm should follow is something like:

  1. binarize the image
  2. find the scale and set it for measurements
  3. identify lines to be measured
  4. measure lines based on scale
  5. store measurements in a dataframe

I am newbie with programming and been thinking in doing it with python. Should I apply a machine learning algorithm to segment each region containing each line and get their measurement? Or is there a simpler and more intuitive way of doing it without having to train a ML algorithm?

lines image

CodePudding user response:

You probably want to break apart the curves into line segments. From there you can measure the length of each line segment to get the total length of the line. That'll give you the length in pixels. You can then convert from pixels to distance by using the scale you provided.

If you don't mind doing the process manually you can use tools like imageJ.

To automate finding the line segments you can try different ways even with opencv. Such as

If you segment each segment you can use contour detection to count the distance around.

CodePudding user response:

I wrote a code mostly following up the fmw42 suggestions and borrowing some ideas of plantcv:

I first set up a custom class to get filename from image since opencv read img as a np.array:

#define a custom class to read filename from cv.imread
class MyImage:
    def __init__(self, img_name):
        self.img = cv.imread(img_name, cv.IMREAD_GRAYSCALE)
        self.__name = img_name

    def __str__(self):
        return self.__name

Then I got scale calibration (I manually cropped the image to isolate only the scale):

def scale_calibration(img):
    #read img
    scale = cv.imread(img, cv.IMREAD_GRAYSCALE)
    #invert background
    scale = 255 - scale
    #binarize img
    _, scale_bin = cv.threshold(scale, 40, 255, cv.THRESH_BINARY)
    #get bounding rectangle to get width of the scale
    x, y, w, h = cv.boundingRect(scale_bin) 
    #define calibration (known distance / distance in pixels of scale)
    calibration = 0.5 / w
    #plot bounding rectangle to debug
    plt.imshow(cv.rectangle(scale, cv.boundingRect(scale_bin), (255, 255, 0), 2))
    
    return calibration

And after that I processed the image containing the curved lines and got their measurements based on the scale calibration:

def fiberLen(img, calibration, plot = True):

    #read img
    fiber = MyImage(img)
    
    #store filename
    filename = str(img)
    
    #invert background to get white pixels on black background
    fiber = 255 - fiber.img
    
    #dilate then erode to connect disconnected pixels
    fiber = cv.dilate(fiber, None, iterations = 1)
    fiber = cv.erode(fiber, None, iterations = 1)
    
    #threshold image
    _, fiber_bin = cv.threshold(fiber, 40, 255, cv.THRESH_BINARY)

    #binarize img transforming pixel values to 0s and 1s
    height, width = fiber_bin.shape
    for i in range(height):
        for j in range(width):
            fiber_bin[i][j] = 1 if fiber_bin[i][j] == 255 else 0

    #skeletonize img
    fiber_skel = pcv.morphology.skeletonize(fiber_bin) #pcv skeletonize returns 0 and 1 img / skimage skel returns True and False values
    
    #get contours
    contours, hierarchy = cv.findContours(fiber_skel, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE)
    
    #get only contours of fibers (which usually will be greater than 200)
    fiber_contours = [c for c in contours if cv.arcLength(c, False) > 200]
    
    #initialize lists to store variables
    measurement = []
    label_coord_x = []
    label_coord_y = []

    #get contour perimeter, divide it by 2 and multiply by calibration factor
    for i, cnt in enumerate(fiber_contours):
        measurement.append(float(cv.arcLength(fiber_contours[i], False) / 2) * calibration)
        #get coordinates if plot is True
        if plot is True:
            label_coord_x.append(fiber_contours[i][0][0][0]) #get first pixel of contours
            label_coord_y.append(fiber_contours[i][0][0][1]) #get second pixel of contours
        
    #plot fiber measurements if plot is True
    if plot is True:
        fiber_copy = fiber.copy()
        #loop through measurement values
        for i, value in enumerate(measurement):
            text = "{:.2f}".format(value)
            x = label_coord_x[i]
            y = label_coord_y[i]
            #put measurement labels in image
            cv.putText(fiber_copy, text = text, org = (x, y), 
                       fontFace = cv.FONT_HERSHEY_SIMPLEX, 
                       fontScale = 1,
                       color = (150, 150, 150),
                       thickness = 2)
        plt.imshow(fiber_copy)

        
    return [filename, measurement]

One issue that showed up is that since the scale in my image is approximately the same size of the lines, the algorithm is also considering the scale in the measurements, as per the image below:

img with measurements

Any thoughts on how to isolate only the curved lines and disconsider the scale? Also, suggestions on my code to improve performance are welcome.

  • Related