This question already has an answer here:
Contour shows dots rather than a curve when retrieving it from the list, but shows the curve otherwise
(1 answer)
Closed 3 years ago.
I am trying to fill a contour which was obtained by separately thresholding 3 color channels.
image_original = cv2.imread(original_image_path)
image_contours = np.zeros((image_original.shape[0], image_original.shape[1], 1), dtype=np.uint8)
image_contour = np.zeros((image_original.shape[0], image_original.shape[1], 1), dtype=np.uint8)
image_binary = np.zeros((image_original.shape[0], image_original.shape[1], 1), dtype=np.uint8)
image_area = image_original.shape[0] * image_original.shape[1]
for channel in range(image_original.shape[2]):
ret, image_thresh = cv2.threshold(image_original[:, :, channel], 120, 255, cv2.THRESH_OTSU)
_, contours, hierarchy = cv2.findContours(image_thresh, 1, 1)
for index, contour in enumerate(contours):
if( cv2.contourArea( contour ) > image_area * background_remove_offset ):
del contours[index]
cv2.drawContours(image_contours, contours, -1, (255,255,255), 3)
_, contours, hierarchy = cv2.findContours(image_contours, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)
cv2.drawContours(image_contour, max(contours, key = cv2.contourArea), -1, (255, 255, 255), 1)
cv2.imwrite(output_contour_image_path, image_contour)
cv2.drawContours(image_binary, max(contours, key = cv2.contourArea), -1, (255, 255, 255), thickness=-1)
cv2.imwrite(output_binary_image_path, image_binary)
cv2.imshow("binary", image_binary)
which is supposed to work by setting the thickness=-1 but it only draws up the contour with 1 thickness same as that of thickness=1 specifically in the following line.
cv2.drawContours(image_binary, max(contours, key = cv2.contourArea), -1, (255, 255, 255), thickness=-1)
Results are as follows,
Which should come up with a binary filled image other than a one just with a contour of thickness=1
well, solved it it seems the cv2.drawContours() function need contours as list type, just changing the line
cv2.drawContours(image_binary, max(contours, key = cv2.contourArea), -1, 255, thickness=-1)
to
cv2.drawContours(image_binary, [max(contours, key = cv2.contourArea)], -1, 255, thickness=-1)
Solves it.
Related
Input:
The output should be:
How can I do this using OpenCV or any other method?
I tried this
img = cv2.imread('test2.JPG')
imgray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
ret, thresh = cv2.threshold(imgray, 150, 255, 0)
contours, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)
print("Number of contours = " + str(len(contours)))
print(contours[0])
# cv2.drawContours(img, contours, -1, (0, 255, 0), 1)
# cv2.drawContours(imgray, contours, -1, (0, 255, 0), 3)
for cnt in contours:
area = cv2.contourArea(cnt)
if area>20:
peri = cv2.arcLength(cnt,True)
approx = cv2.approxPolyDP(cnt, 0.02 * peri, True)
x,y,w,h = cv2.boundingRect(approx)
cv2.rectangle(img,(x,y-3),(x+w,y+h-3),(255,0,0),1)
You have found the contours and drawn only those above certain area. So far so good.
To capture each line as an individual entity, you need to find a way to connect the text in each line. Since the lines given in the image a straight, a simple approach would be to use a horizontal kernel ([1, 1, 1, 1, 1]) of certain length and perform morphology.
Code:
img = cv2.imread('text.jpg',1)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
th = cv2.threshold(gray,0,255,cv2.THRESH_BINARY_INV+cv2.THRESH_OTSU)[1]
Using horizontal kernel 8 pixels in length. This is the parameter you would need to change when trying out for other images of different font size and text length.
hor_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (8, 1))
# array([[1, 1, 1, 1, 1, 1, 1, 1]], dtype=uint8)
dilated = cv2.dilate(th, hor_kernel, iterations=1)
Looking at the image above, hope you have an idea of what dilation using a horizontal kernel does. From here on, we find outermost contours above certain area.
contours, hierarchy = cv2.findContours(dilated, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
img2 = img.copy()
for i, c in enumerate(contours):
area = cv2.contourArea(c)
if area > 100:
x,y,w,h = cv2.boundingRect(c)
img2 = cv2.rectangle(img2, (x, y), (x + w, y + h), (0,255,0), 1)
Using OpenCV for Python, I am trying to get a mask of the noise elements in a image to be later used as input for the cv.inpaint() function.
I am given a greyscale image (2D matrix with values from 0 to 255) in the input_mtx_8u variable, with noise (isolated polygons of very low values).
So far what I did was:
get the edges in which the gradient is above 25:
laplacian = cv2.Laplacian(input_mtx_8u, cv2.CV_8UC1)
lapl_bin, lapl_bin_val = cv2.threshold(laplacian, 25, 255, cv2.THRESH_BINARY)
get the contours of the artifacts
contours, _ = cv2.findContours(lapl_bin_val, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)
fill the contours identified
filled_mtx = input_mtx_8u.copy()
cv2.fillPoly(filled_mtx, contours, (255, 255, 0), 4)
For some reason, my 'filled polygons' are not completely filled (see figure).
What can I be doing wrong?
As pointed by #fmw42 , a solution to get the contours filled is using drawContours() instead of fillPoly().
The final working code I got is:
# input_mtx_8u = 2D matrix with uint8 values from 0 to 255
laplacian = cv2.Laplacian(input_mtx_8u, cv2.CV_8UC1)
lapl_bin, lapl_bin_val = cv2.threshold(laplacian, 25, 255, cv2.THRESH_BINARY)
contours, _ = cv2.findContours(lapl_bin_val, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)
inpaint_mask = np.zeros(input_mtx_8u.shape, dtype=np.uint8)
for contour in contours:
cv2.drawContours(inpaint_mask, [contour], -1, (255, 0, 0), thickness=-1)
# inpaint_mask = can be used as the mask for cv2.inpaint()
Note that for some reason:
cv2.drawContours(input_mtx_cont, contours, -1, (255, 0, 0), thickness=-1)
does not work. One must loop and draw contour by contour...
I am SUPER new to python coding and would like some help. I was able to segment each cell outline within a biological tissue (super cool!) and now I am trying to find the centroid of each cell within a tissue using this:
I am using this code:
img = cv2.imread('/Users/kate/Desktop/SegmenterTest/SegmentedCells/Seg1.png')
image = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
thresh = cv2.threshold(image, 60, 255, cv2.THRESH_BINARY)[1]
cnts = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = imutils.grab_contours(cnts)
# loop over the contours
for c in cnts:
# compute the center of the contour
M = cv2.moments(c)
cX = int(M["m10"] / M["m00"])
cY = int(M["m01"] / M["m00"])
# draw the contour and center of the shape on the image
cv2.drawContours(image, [c], -1, (0, 255, 0), 2)
cv2.circle(image, (cX, cY), 7, (255, 255, 255), -1)
cv2.putText(image, "center", (cX - 20, cY - 20),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 2)
# show the image
cv2.imshow("Image", image)
cv2.waitKey(0)
However, when I use this code, it is giving me the centroid of the ENTIRE object, and not each individual object to give this.
I have no idea where to go from here, so a nudge in the right direction would be greatly appreciated!
You can use the function regionprops from the module scikit-image in your case. Here is what I got.
This is the code I used.
import cv2
import matplotlib.pyplot as plt
from skimage import measure
import numpy as np
cells = cv2.imread('cells.png',0)
ret,thresh = cv2.threshold(cells,20,255,cv2.THRESH_BINARY_INV)
labels= measure.label(thresh, background=0)
bg_label = labels[0,0]
labels[labels==bg_label] = 0 # Assign background label to 0
props = measure.regionprops(labels)
fig,ax = plt.subplots(1,1)
plt.axis('off')
ax.imshow(cells,cmap='gray')
centroids = np.zeros(shape=(len(np.unique(labels)),2)) # Access the coordinates of centroids
for i,prop in enumerate(props):
my_centroid = prop.centroid
centroids[i,:]= my_centroid
ax.plot(my_centroid[1],my_centroid[0],'r.')
# print(centroids)
# fig.savefig('out.png', bbox_inches='tight', pad_inches=0)
plt.show()
Good luck with your research!
Problem
cv2.findContours uses an algorithm which has a few different 'retrieval modes'. These affect which contours are returned and how they are returned. This is documented here. These are given as the second argument to findContours. Your code uses cv2.RETR_EXTERNAL which means findContours will only return the outermost border of separate objects.
Solution
Changing this argument to cv2.RETR_LIST will give you all the contours in the image (including the one outermost border). This is the simplest solution.
E.g.
import cv2
import imutils
img = cv2.imread('/Users/kate/Desktop/SegmenterTest/SegmentedCells/Seg1.png')
image = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
thresh = cv2.threshold(image, 60, 255, cv2.THRESH_BINARY)[1]
cnts = cv2.findContours(thresh.copy(), cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
cnts = imutils.grab_contours(cnts)
# loop over the contours
for c in cnts:
# compute the center of the contour
M = cv2.moments(c)
cX = int(M["m10"] / M["m00"])
cY = int(M["m01"] / M["m00"])
# draw the contour and center of the shape on the image
cv2.drawContours(image, [c], -1, (0, 255, 0), 2)
cv2.circle(image, (cX, cY), 7, (255, 255, 255), -1)
cv2.putText(image, "center", (cX - 20, cY - 20),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 2)
# show the image
cv2.imshow("Image", image)
cv2.waitKey(0)
Selecting only the innermost objects
To reliably omit the outer contours you can take advantage of the ability of findContours to return a hierarchy of the contours it detects. To do this, you can change the retrieval mode argument once again to RETR_TREE, which will generate a full hierarchy.
The hierarchy is an array containing arrays of 4 values for each contour in the image. Each value is an index of a contour in the contour array. From the docs:
For each i-th contour contours[i], the elements hierarchy[i][0] ,
hierarchy[i][1] , hierarchy[i][2], and hierarchy[i][3] are set to
0-based indices in contours of the next and previous contours at the
same hierarchical level, the first child contour and the parent
contour, respectively. If for the contour i there are no next,
previous, parent, or nested contours, the corresponding elements of
hierarchy[i] will be negative.
When we say 'innermost', what we mean is contours that have no children (contours inside of them). So we want those contours whose entry in the hierarchy has a negative 3rd value. That is, contours[i], such that hierarchy[i][2] < 0
A small wrinkle is that although findContours returns a tuple which includes the hierarchy, imutils.grabContours discards the hierarchy and returns just the array of contours. All this means is that we have to do the work of grabContours ourselves, if we intend on working with different versions of OpenCV. This is just a simple if else statement.
res = cv2.findContours(thresh.copy(), cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
# switch for different versions of OpenCV
if len(cnts) == 3:
_, cnts, hierarchy = res
else:
cnts, hierarchy = res
Once you have hierarchy, checking if a contour, cnts[i] is 'innermost' can be done with hierarchy[0][i][2] < 0, which should be False for contours that contain other contours.
A full example based on your question's code:
import cv2
import imutils
img = cv2.imread('/Users/kate/Desktop/SegmenterTest/SegmentedCells/Seg1.png')
image = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
thresh = cv2.threshold(image, 60, 255, cv2.THRESH_BINARY)[1]
cnts = cv2.findContours(thresh.copy(), cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
# switch for different versions of OpenCV
if len(cnts) == 3:
_, cnts, hierarchy = cnts
else:
cnts, hierarchy = cnts
# loop over the contours
for i, c in enumerate(cnts):
# check that it is 'innermost'
if hierarchy[0][i][2] < 0:
# compute the center of the contour
M = cv2.moments(c)
cX = int(M["m10"] / M["m00"])
cY = int(M["m01"] / M["m00"])
# draw the contour and center of the shape on the image
cv2.drawContours(image, [c], -1, (0, 255, 0), 2)
cv2.circle(image, (cX, cY), 7, (255, 255, 255), -1)
cv2.putText(image, "center", (cX - 20, cY - 20),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 2)
# show the image
cv2.imshow("Image", image)
cv2.waitKey(0)
How to draw vertical line when the extreme point is detected and put some measurement like (inches,mm, cm etc):
This is the result of the code:
The below start point must draw a cv2.circle extreme point in the drawContour not in the cv2.rectangle:
import cv2
import numpy as np
# Load image, grayscale, Gaussian blur, threshold
image = cv2.imread('banana4.jpg')
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
blur = cv2.GaussianBlur(gray, (3,3), 0)
thresh = cv2.threshold(blur, 220, 255, cv2.THRESH_BINARY_INV)[1]
# Find contours
cnts = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = cnts[0] if len(cnts) == 2 else cnts[1]
c = max(cnts, key=cv2.contourArea)
# Obtain outer coordinates
left = tuple(c[c[:, :, 0].argmin()][0])
right = tuple(c[c[:, :, 0].argmax()][0])
top = tuple(c[c[:, :, 1].argmin()][0])
bottom = tuple(c[c[:, :, 1].argmax()][0])
# Draw dots onto image
cv2.drawContours(image, [c], -1, (36, 255, 12), 2)
cv2.circle(image, left, 8, (0, 50, 255), -1)
cv2.circle(image, right, 8, (0, 255, 255), -1)
cv2.circle(image, top, 8, (255, 50, 0), -1)
cv2.circle(image, bottom, 8, (255, 255, 0), -1)
print('left: {}'.format(left))
print('right: {}'.format(right))
print('top: {}'.format(top))
print('bottom: {}'.format(bottom))
cv2.imshow('thresh', thresh)
cv2.imshow('image', image)
cv2.waitKey()
You have multiple questions and problems here.
You should decrease the min value for the binary threshold in order to get rid of the shadow.
thresh = cv2.threshold(blur, 180, 255, cv2.THRESH_BINARY_INV)[1]
You already have the bounding box top/bottom/left/right, so you can draw a rectangle using cv2.rectangle.
cv2.Rectangle(image, (left[0], top[1]), (right[0], bottom[1]), (0, 255, 0), 2)
If you don't know any information about the camera or the object itself, it is not possible to give an estimate about the size.
I have a set of contours defined and filled in OpenCV, and I'm trying to use this as a mask to find the mean intensity in each ROI. I thought I could do this using the cv2.mean function with a defined mask. My code is (im2 is an image read from file):
msk = np.zeros(im2.shape, np.uint8)
cv2.bilateralFilter(im2, 5, 200, 5)
im2 = cv2.GaussianBlur(im2,(5,5),0
binImg = cv2.adaptiveThreshold(im2, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 55, -5)
contours, heir = cv2.findContours(binImg, cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)
cv2.drawContours(msk, contours, -1, 255, -1)
print len(contours)
print cv2.mean(im2, mask = msk)
This returns:
3361
(155.88012076286788, 0.0, 0.0, 0.0)
I thought that I would get a mean intensity per contour, but it looks like an overall mean intensity for each channel (the image is greyscale). Are my expectations incorrect, or is my code incorrect?
Just to follow up on this (and close it out), I did resolve it by iterating over contours, and using the contours as a mask for the original image. The code is:
msk = np.zeros(im2.shape, np.uint8)
cv2.bilateralFilter(im2, 5, 200, 5)
im2 = cv2.GaussianBlur(im2,(5,5),0)
binImg = cv2.adaptiveThreshold(im2, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 55, -5)
contours, heir = cv2.findContours(binImg, cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)
cv2.drawContours(msk, contours, -1, 255, -1)
for cnt in contours:
res = np.zeros(img.shape, np.uint8)
(x,y), radius = cv2.minEnclosingCircle(cnt)
ctr = (int(x), int(y))
rad = int(radius)
circ = cv2.circle(res, ctr, rad,1,-1)
print "Area: " + str(cv2.contourArea(cnt)), "Mean: " + str(float(cv2.meanStdDev(img, mask=res)[0]))
It should be noted that I'm using the meanStdDev (I did some editing and wanted to return Std Dev as well), rather than mean, but either should work for finding means. It's still not clear why mean seemed to return 4 results (for 4 channels?) on a greyscale image in the original example.