I want to draw a line across the center of a curved object.
As an example: an image of a banana is given, the orientation might change with different images and also there might be more than one curve in the object, but the task is the same.
To determine the length of the object, it's required to calculate (interpolate) the center line of the contour of the object from start to end, then I can calculate the length of the interpolated line. This is what I thought so far.
But now there is the tricky part, determining the contour of the object using python and cv2 is no problem, this works well. But calculating the center line to determine its length is a struggle.
Edit
The goal of the script should be to measure the length and area of the worm, so I don't have to measure the values manually for hundreds of images.
Input image:
My computation so far for the contour (green line):
What I want (only drawn by hand as an example):
Code used so far (without the "center line" I want, because I don't have any idea how to start). My idea using the convex hull, building the skeleton and using it does not work as expected, because the convex hull is to large (caused by the concave part), combining it with polyDP also does not work because the polyDP often misses parts of the worm and the combined result is also bad.
import numpy as np
import cv2
import os
draw_windows = True ## change fo False for no windows only calc
def calc_values(filename):
img = cv2.imread(filename)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
gray = cv2.GaussianBlur(gray, (7, 7), 0)
ret, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
drawWindow('thresh', thresh)
edged = cv2.Canny(gray, 50, 100)
edged = cv2.dilate(edged, None, iterations=1)
edged = cv2.erode(edged, None, iterations=1)
drawWindow('edged', edged)
contours, _ = cv2.findContours(edged, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
# Assume the largest contour corresponds to the worm
if contours:
largest_contour = max(contours, key=cv2.contourArea)
# Draw the contour on the original image
image_with_contour = cv2.cvtColor(edged, cv2.COLOR_GRAY2BGR)
cv2.drawContours(image_with_contour, [largest_contour], -1, (0, 255, 0), 2)
cv2.drawContours(image_with_contour, contours, -1, color=(255, 255, 255), thickness=cv2.FILLED)
# Display the original image with the detected contour
drawWindow('Worm with Contour', image_with_contour)
def drawWindow(window_name, image):
if draw_windows:
cv2.imshow(window_name, image)
cv2.waitKey(0)
cv2.destroyAllWindows()
def main():
directory = "input"
for filename in os.listdir(directory):
file = os.path.join(directory, filename)
calc_values(file)
if __name__ == "__main__":
main()
(I know code quality is not the best so far, but it started as a quick and dirty "project" :D)
I googled a lot to figure out what is available. I ended up with using input as above
input_77560561.jpg
:For the code, see the question and my previous answer and Python Image - Finding largest branch from image skeleton answer that uses
fil_finder
library FilFinder _ GitHub:Output in order of images, some of the images shown by the script are missing:
Tresh image:
edged image, missing shown in script.
Worm with contour, missing shown in script.
Worm contour filled:
Thinned worm contour filled:
Skeleton, that is thinned worm contour filled with removed branch by
FilFinder
, because of how the contour is detected, this is not the best line, but as mentioned in the previous answer by me to this post I wasn't able to produce a better worm contour filled, by working on this we should be able to get desidered results:Overlays to input and contour filled, line is
dilate
d by OpenCV:2 :
1 :
As in previous answer commenting out
# gray = cv2.GaussianBlur(gray, (7, 7), 0)
I can get a better single line:with overlays:
As for the length of the line*** could it be approximated by the total number of pixels of the skeleton (i.e.
print('\n\nskeleton_lenght_approx : ', np.sum(mask))
? If not it will have to be calculated starting by one of the ends and adding the distance (+1 for x+-1 or y+-1 , or 1.4 for x,y+-1 for diagonal pixels) unless filFinder has the data we need hidden somewhere.*** Note worth reading:
How different can the number of pixels in a straight line be to its real length
to calculate the skeleton length, here starting from saved file
skeleton.png
, I devised this code, probably not the best way but only one I was able to conceive:One consideration about
fil.FilFinder2D
and its resultsfil.analyze_skeletons
-->fil.skeleton_longpath
, using below image ,thinned_worm.png
:with following code:
I get following output:
Overlay of 5 of the 10 (0 to 9 results :
skeleton#.png"
) longest path in files:and magnification of a small part showing how the
fil.skeleton_longpath
changes with each run of the algorithm runs [do not know if I made any mistake somewhere so far, don't know how algorithm works]:ADDENDUM
Had a chat with filFinder developer , was really kind to me, and pointed me to lengths that is :
(it is an attribute of
class fil_finder.FilFinder2D(...)
),as explained to me its values is different from the geometric lenght of the
fil.skeleton_longpath
image because :