I am trying to draw multiple contours on an image and by far I have managed to draw the contours by applying different thresholds. The only problem is that most of the contour regions are overlapping and I am stuck here on how to deal with it. What I would ideally want is that whenever there is an overlap it should divide the contours into individual regions. For instance, as in the Conceptual image there are 4 regions(contours) orange, green, blue and black. Whenever there is an overlap, it should divide into purple regions. It seems very tricky and I am not even sure if that is possible. If not, I would want all the overlapping to merge. Can anyone help with how to solve this issue?
Sample image
import numpy as np
import cv2 as cv
import matplotlib.pyplot as plt
im = cv.imread('images/sample.jpg')
imgray = cv.cvtColor(im, cv.COLOR_BGR2GRAY)
ret1, thresh1 = cv.threshold(imgray, 30, 80, 0)
ret2, thresh2 = cv.threshold(imgray, 80, 110, 0)
ret3, thresh3 = cv.threshold(imgray, 110, 150, 0)
ret4, thresh4 = cv.threshold(imgray, 150, 200, 0)
ret5, thresh5 = cv.threshold(imgray, 200, 255, 0)
_,contours1, hierarchy1 = cv.findContours(thresh1, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE)
_,contours2, hierarchy2 = cv2.findContours(thresh2,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)
_,contours3, hierarchy3 = cv2.findContours(thresh3,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)
_,contours4, hierarchy4 = cv2.findContours(thresh4,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)
_,contours5, hierarchy5 = cv2.findContours(thresh5,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)
cv2.drawContours(im, contours1, -1, (0, 0, 255), 1)
cv2.drawContours(im, contours2, -1, (0, 255, 0), 1)
cv2.drawContours(im, contours3, -1, (0, 0, 255), 1)
cv2.drawContours(im, contours4, -1, (10, 200, 200), 1)
cv2.drawContours(im, contours5, -1, (255, 255, 0), 1)
cv2.imshow("im",im)
cv2.waitKey(0)
As mentioned before, generating masks from your contours, and then calculate the pair-wise intersections as well as the "exclusive" parts from the original masks, will certainly give you the desired regions, but this approach will tend to be expensive as well. From your sample image and your code, I couldn't figure out, what you actually want to do, so I stick to some very basic example to illustrate that approach.
import cv2
import numpy as np
from matplotlib import pyplot as plt
# Generate some dummy images, whose (main) contours overlap
img1 = cv2.circle(np.zeros((400, 400, 3), np.uint8), (150, 150), 100, (0, 255, 0), cv2.FILLED)
img2 = cv2.rectangle(np.zeros((400, 400, 3), np.uint8), (175, 175), (325, 325), (0, 0, 255), cv2.FILLED)
# Find contours (OpenCV 4.x)
contours1, _ = cv2.findContours(cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
contours2, _ = cv2.findContours(cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
# Find contours (OpenCV 3.x)
#_, contours1, _ = cv2.findContours(cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
#_, contours2, _ = cv2.findContours(cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
# Generate masks of (main) contours; Attention: Hard-coded selection of first contour here!
mask1 = cv2.drawContours(np.zeros((400, 400), np.uint8), [contours1[0]], -1, 255, cv2.FILLED)
mask2 = cv2.drawContours(np.zeros((400, 400), np.uint8), [contours2[0]], -1, 255, cv2.FILLED)
# Find intersection of both masks
mask_combined = cv2.bitwise_and(mask1, mask2)
# Generate "exclusive" masks, i.e. masks without the intersection parts
mask1_excl = cv2.bitwise_xor(mask1, mask_combined)
mask2_excl = cv2.bitwise_xor(mask2, mask_combined)
# Visualization
plt.figure()
plt.subplot(3, 3, 1), plt.imshow(img1), plt.ylabel('img1')
plt.subplot(3, 3, 2), plt.imshow(img2), plt.ylabel('img2')
plt.subplot(3, 3, 3), plt.imshow(img1 + img2), plt.ylabel('img1 + img2')
plt.subplot(3, 3, 4), plt.imshow(mask1, cmap='gray'), plt.ylabel('mask1')
plt.subplot(3, 3, 5), plt.imshow(mask2, cmap='gray'), plt.ylabel('mask2')
plt.subplot(3, 3, 6), plt.imshow(mask_combined, cmap='gray'), plt.ylabel('mask_combined')
plt.subplot(3, 3, 7), plt.imshow(mask1_excl, cmap='gray'), plt.ylabel('mask1_excl')
plt.subplot(3, 3, 8), plt.imshow(mask2_excl, cmap='gray'), plt.ylabel('mask2_excl')
plt.subplot(3, 3, 9), plt.imshow(mask_combined, cmap='gray'), plt.ylabel('mask_combined')
plt.show()
Visualization:
Now, this has to be done for each tuple of contours - not only pairs, since you can have intersections of three or more contours. To keep track of all those resulting masks, etc. will be most likely memory-intensive, but not that computationally expensive. In the end, all approaches will somehow need to store the resulting regions as some kind of masks.
Hope that helps!
Related
I was working to reproduce an optical illusion that you find here(image) but I having trouble adding horizontal lines inside of the circles:
My attempt so far:
-Detect the certain colors of the circles
-Detect contours, and extract circle center points, and radius
-Then try to draw horizontal lines (which I failed)
Here is my code:
import numpy as np
import cv2
img = 255*np.ones((800, 800, 3), np.uint8)
height, width,_ = img.shape
#filling the image with lines
for i in range(0, height, 15):
cv2.line(img, (0, i+3), (width, i+3), (255, 0, 0), 4)
cv2.line(img, (0, i+8), (width, i+8), (0, 255, 0), 4)
cv2.line(img, (0, i+13), (width, i+13), (0, 0, 255), 4)
#adding 5 gray circles
for i in range(0, height, int(height/5)):
cv2.circle(img, (i+50, i+50), 75, (128, 128, 128), -1)
#finding rannge of gray circles
lower=np.array([127,127,127])
upper=np.array([129,129,129])
mask = cv2.inRange(img, lower, upper)
#contours
contours, _ = cv2.findContours(mask, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
for cnt in contours:
#draw circles around the contours
coordinates = cv2.minEnclosingCircle(cnt)
#coordinates and radius:
center = (int(coordinates[0][0]), int(coordinates[0][1]))
radius = int(coordinates[1])
#I wanted to do a sanity check before the for loop (I added a line the longest line should be 2*radius)
cv2.line(img, (center[0]-radius, center[1]), (center[0]+radius, center[1]), (0, 0, 0), 4)
for i in range(0, radius, int(radius/5)):
cv2.line(img, (center[0]-radius+i, center[1]+i), (center[0]+radius-i, center[1]+i), (0, 0, 0), 4)
cv2.line(img, (center[0]-radius+i, center[1]-i), (center[0]+radius-i, center[1]-i), (0, 0, 0), 4)
cv2.imwrite('munker.png',img)
And here is the result:
As you can see the values in the for loop are not proportional to the boundaries of the circle, so the lines are short(except the longest line). What am I missing here?
I tried the Hough transform but I had a similar problem.
For more clarity, I write a code to show what I wanted:
for i in range(0, 360, 15):
x = int(center[0] + radius * np.cos(np.deg2rad(i)))
y = int(center[1] + radius * np.sin(np.deg2rad(i)))
cv2.line(img, (x,y), (x, y), (0, 255, 255), 10)
I want to merge the yellow dots with horizontal lines. But my math is finished right here. Sorry, it's long, I was just trying to make things clear. Thank you for your time.
As #fmw42 pointed out in the comment, splitting the RGB channels and applying a mask is very effective at being able to fill the inside of the circles with horizontal lines.
import numpy as np
import cv2
img = 255*np.ones((800, 800, 3), np.uint8)
height, width,_ = img.shape
for i in range(0, height, 15):
cv2.line(img, (0, i+3), (width, i+3), (255, 0, 0), 4)
cv2.line(img, (0, i+8), (width, i+8), (0, 255, 0), 4)
cv2.line(img, (0, i+13), (width, i+13), (0, 0, 255), 4)
b, g, r = cv2.split(img)
mask_b = np.zeros((height, width), np.uint8)
mask_g = np.zeros((height, width), np.uint8)
mask_r = np.zeros((height, width), np.uint8)
for i in range(0, height, int(height/5)):
cv2.circle(mask_b, (i, i), 75, 255, -1)
cv2.circle(mask_g, (i, i), 75, 255, -1)
cv2.circle(mask_r, (i, i), 75, 255, -1)
#apply the mask to the channels
b = cv2.bitwise_and(b, b, mask=mask_b)
g = cv2.bitwise_and(g, g, mask=mask_g)
r = cv2.bitwise_and(r, r, mask=mask_r)
#merge the channels
img = cv2.merge((b, g, r))
cv2.imshow('image', img)
cv2.waitKey(0)
Facing problem while removing checkerboard pattern. I'm using cv2.Threshold but it selected unexpected pixels too (red marked) .
import cv2
import numpy as np
input = cv2.imread('image.png')
ret, logo_mask = cv2.threshold(input[:,:,0], 0, 255, cv2.THRESH_BINARY|cv2.THRESH_OTSU)
cv2.imshow(logo_mask)
Input image:
Output image:
Anyone can help?
Getting perfect results that covers all cases is challenging.
The following solution assumes that the white checkerboard color is (255, 255, 255), and gray is (230, 230, 230).
Another assumptions is that the clusters with that specific colors in the other parts of the image are very small.
We may use the following stages:
Find "white mask" and "gray mask" where color is (255, 255, 255) and (230, 230, 230).
Create unified mask using bitwise or.
Find contours, and remove small contours from the mask (assumed to be "noise").
Code sample:
import cv2
import numpy as np
input = cv2.imread('image.png')
white_mask = np.all(input == 255, 2).astype(np.uint8)*255 # cv2.inRange(input, (255, 255, 255), (255, 255, 255))
gray_mask = np.all(input == 230, 2).astype(np.uint8)*255 # gray_mask = cv2.inRange(input, (230, 230, 230), (230, 230, 230))
mask = cv2.bitwise_or(white_mask, gray_mask) # Create unified mask
ctns = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)[-2] # Find contours
# Remove small contours from mask
for c in ctns:
area = cv2.contourArea(c) # Find the area of each contours
if (area < 10): # Ignore small contours (assume noise).
cv2.drawContours(mask, [c], 0, 0, -1)
mask = cv2.dilate(mask, np.ones((3, 3), np.uint8)) # Dilate the mask - "cosmetics"
output = cv2.copyTo(input, 255-mask) # Put black color in the masked part.
# Show images for testing
cv2.imshow('input', input)
cv2.imshow('mask', mask)
cv2.imshow('output', output)
cv2.waitKey()
cv2.destroyAllWindows()
white_mask:
gray_mask:
mask:
output:
In case there are large white areas or gray areas in the foreground part, the above solution may not work.
I thought of a process for finding only the areas that overlaps a boundary between white and gray rectangle.
It's not working, because there are small parts between the tree branches that are excluded.
The following code may give you inspirations:
import cv2
import numpy as np
input = cv2.imread('image.png')
#ret, logo_mask = cv2.threshold(input[:,:,0], 0, 255, cv2.THRESH_BINARY|cv2.THRESH_OTSU)
white_mask = np.all(input == 255, 2).astype(np.uint8)*255 # cv2.inRange(input, (255, 255, 255), (255, 255, 255))
gray_mask = np.all(input == 230, 2).astype(np.uint8)*255 # gray_mask = cv2.inRange(input, (230, 230, 230), (230, 230, 230))
cv2.imwrite('white_mask.png', white_mask)
cv2.imwrite('gray_mask.png', gray_mask)
# Apply opening for removing small clusters
opened_white_mask = cv2.morphologyEx(white_mask, cv2.MORPH_OPEN, np.ones((3, 3), np.uint8))
opened_gray_mask = cv2.morphologyEx(gray_mask, cv2.MORPH_OPEN, np.ones((3, 3), np.uint8))
cv2.imwrite('opened_white_mask.png', opened_white_mask)
cv2.imwrite('opened_gray_mask.png', opened_gray_mask)
white_mask_shell = cv2.dilate(opened_white_mask, np.ones((3, 3), np.uint8)) - opened_white_mask # Dilate white_mask and keep only the "shell"
gray_mask_shell = cv2.dilate(opened_gray_mask, np.ones((3, 3), np.uint8)) - opened_gray_mask # Dilate gray_mask and keep only the "shell"
white_mask_shell = cv2.dilate(white_mask_shell, np.ones((3, 3), np.uint8)) # Dilate the "shell"
gray_mask_shell = cv2.dilate(gray_mask_shell, np.ones((3, 3), np.uint8)) # Dilate the "shell"
cv2.imwrite('white_mask_shell.png', white_mask_shell)
cv2.imwrite('gray_mask_shell.png', gray_mask_shell)
overlap_shell = cv2.bitwise_and(white_mask_shell, gray_mask_shell)
cv2.imwrite('overlap_shell.png', overlap_shell)
dilated_overlap_shell = cv2.dilate(overlap_shell, np.ones((17, 17), np.uint8))
mask = cv2.bitwise_or(cv2.bitwise_and(white_mask, dilated_overlap_shell), cv2.bitwise_and(gray_mask, dilated_overlap_shell))
cv2.imshow('input', input)
cv2.imshow('white_mask', white_mask)
cv2.imshow('gray_mask', gray_mask)
cv2.imshow('white_mask', white_mask)
cv2.imshow('gray_mask', gray_mask)
cv2.imshow('opened_white_mask', opened_white_mask)
cv2.imshow('opened_gray_mask', opened_gray_mask)
cv2.imshow('overlap_shell', overlap_shell)
cv2.imshow('dilated_overlap_shell', dilated_overlap_shell)
cv2.imshow('mask', mask)
cv2.waitKey()
cv2.destroyAllWindows()
Attemping to create a way to process images to count different types of tablets. The following code has been working well for circular objects, however oval shapes are creating issues that I cant find a workaround for.
kernel = np.ones((5,5),np.uint8)
image = cv2.imread('sample.jpg')
shifted = cv2.GaussianBlur(image, (15, 15), 1)
shifted = cv2.pyrMeanShiftFiltering(shifted, 21, 51)
shifted = cv2.erode(shifted,kernel,iterations=1)
shifted = cv2.dilate(shifted,kernel,iterations=1)
cv2.imwrite("step1.jpg", shifted)
gray = cv2.cvtColor(shifted, cv2.COLOR_BGR2GRAY)
thresh = cv2.threshold(gray, 0, 255,
cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
cv2.imwrite("step2.jpg", thresh)
thresh = cv2.morphologyEx(thresh,cv2.MORPH_OPEN,kernel, iterations = 2)
cv2.imwrite("step3.jpg", thresh)
thresh = cv2.bitwise_not(thresh)
thresh = cv2.erode(thresh,kernel,iterations=1)
cv2.imwrite("step4.jpg", thresh)
D = ndimage.distance_transform_edt(thresh)
localMax = peak_local_max(D, indices=False, min_distance=10,
labels=thresh)
markers = ndimage.label(localMax, structure=np.ones((3, 3)))[0]
labels = watershed(-D, markers, mask=thresh)
print("[INFO] {} unique segments found".format(len(np.unique(labels)) - 1))
for label in np.unique(labels):
if label == 0:
continue
mask = np.zeros(gray.shape, dtype="uint8")
mask[labels == label] = 255
cnts = cv2.findContours(mask.copy(), cv2.RETR_EXTERNAL,
cv2.CHAIN_APPROX_SIMPLE)
cnts = imutils.grab_contours(cnts)
c = max(cnts, key=cv2.contourArea)
((x, y), r) = cv2.minEnclosingCircle(c)
cv2.circle(image, (int(x), int(y)), int(r), (0, 255, 0), 2)
cv2.putText(image, "#{}".format(label), (int(x) - 10, int(y)),
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2)
cv2.imwrite("step5.jpg", image)
cv2.waitKey(0)
Image that is being used is :
https://imgur.com/a/1U49DeT
Output after filtering yields :
https://imgur.com/a/vXwrWlG
Any teaching points as to how to fix this would be greatly appreciated.
I think there is a better way to use the watershed operator.
It relies on having a good gradient, but if the images are similar to this one, you should be able to do this effectively. Also, there are very powerful edge detectors today, much better than the one I used on this demo.
import cv2
import numpy as np
import higra as hg
from skimage.segmentation import relabel_sequential
import matplotlib.pyplot as plt
def main():
img_path = "pills.jpg"
img = cv2.imread(img_path)
img = cv2.resize(img, (256, 256))
img = cv2.GaussianBlur(img, (9, 9), 0)
edges = cv2.Canny(img, 100, 100)
size = img.shape[:2]
graph = hg.get_4_adjacency_graph(size)
edge_weights = hg.weight_graph(graph, edges, hg.WeightFunction.mean)
tree, altitudes = hg.watershed_hierarchy_by_area(graph, edge_weights)
segments = hg.labelisation_horizontal_cut_from_threshold(tree, altitudes, 500)
segments, _, _ = relabel_sequential(segments)
print('The number of pills is ', segments.max() - 1)
plt.imshow(segments)
plt.show()
if __name__ == "__main__":
main()
Initially, I resize the image to speed up the computation and apply a blur to reduce the background gradient. I detect its edges (gradient) and create a graph with it as edge weights; then I compute the watershed hierarchy ordered by area and threshold it obtaining the connected component at that level, from this you can count the number of segments.
I am extracting the length of individual bars from a chart image. It works fine in most of the cases but in some cases the contour groups 2 bars as 1 which is detrimental to my cause. I tried different combinations of canny,dilate, erode, and color scheme. It improved the result only slightly. How can avoid the grouping? Here is the complete code and one image. You can run using this image too see the problem.
from scipy.spatial import distance as dist
from imutils import perspective
from imutils import contours
import numpy as np
import argparse
import imutils
import cv2
def midpoint(ptA, ptB):
return ((ptA[0] + ptB[0]) * 0.5, (ptA[1] + ptB[1]) * 0.5)
image = cv2.imread("somefile.png")
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
gray = cv2.GaussianBlur(gray, (7, 7), 0)
#edged=cv2.Laplacian(gray, cv2.CV_8U, gray, ksize=7)
edged = cv2.Canny(gray, 30, 50)
cv2.imwrite("test00.png", edged)
edged = cv2.dilate(edged, None, iterations=1)
cv2.imwrite("test01.png", edged)
edged = cv2.erode(edged, None, iterations=1)
cv2.imwrite("test02.png", edged)
cnts = cv2.findContours(edged.copy(), cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)
cnts = imutils.grab_contours(cnts)
pixelsPerMetric = 100
for c in cnts:
orig = image.copy()
box = cv2.minAreaRect(c)
box = cv2.cv.BoxPoints(box) if imutils.is_cv2() else cv2.boxPoints(box)
box = np.array(box, dtype="int")
print(box)
box = perspective.order_points(box)
cv2.drawContours(orig, [box.astype("int")], -1, (0, 255, 0), 2)
for (x, y) in box:
cv2.circle(orig, (int(x), int(y)), 5, (0, 0, 255), -1)
(tl, tr, br, bl) = box
(tltrX, tltrY) = midpoint(tl, tr)
(blbrX, blbrY) = midpoint(bl, br)
(tlblX, tlblY) = midpoint(tl, bl)
(trbrX, trbrY) = midpoint(tr, br)
cv2.circle(orig, (int(tltrX), int(tltrY)), 5, (255, 0, 0), -1)
cv2.circle(orig, (int(blbrX), int(blbrY)), 5, (255, 0, 0), -1)
cv2.circle(orig, (int(tlblX), int(tlblY)), 5, (255, 0, 0), -1)
cv2.circle(orig, (int(trbrX), int(trbrY)), 5, (255, 0, 0), -1)
cv2.line(orig, (int(tltrX), int(tltrY)), (int(blbrX), int(blbrY)),
(255, 0, 255), 2)
cv2.line(orig, (int(tlblX), int(tlblY)), (int(trbrX), int(trbrY)),
(255, 0, 255), 2)
dA = dist.euclidean((tltrX, tltrY), (blbrX, blbrY))
dB = dist.euclidean((tlblX, tlblY), (trbrX, trbrY))
dimA = dA / pixelsPerMetric
dimB = dB / pixelsPerMetric
cv2.putText(orig, "{:.1f}in".format(dimA),
(int(tltrX - 15), int(tltrY - 10)), cv2.FONT_HERSHEY_SIMPLEX,
0.65, (255, 255, 255), 2)
cv2.putText(orig, "{:.1f}in".format(dimB),
(int(trbrX + 10), int(trbrY)), cv2.FONT_HERSHEY_SIMPLEX,
0.65, (255, 255, 255), 2)
cv2.imshow("Image", orig)
cv2.waitKey(0)
This image is trivial to segment. The color of the bars is exactly RGB=(245,222,179). You can use OpenCV's function inRange to find pixels of this color. In this function, we need to give the color in BGR order, because that is how OpenCV reads in images by default. Here I'm picking a slightly larger range in case the image used JPEG compression (which is lossy and therefore changes pixel values slightly):
image = cv2.imread("somefile.png")
mask = cv2.inRange(image, (177, 220, 243), (181, 224, 247))
This image mask now has perfectly separated bars:
i'm trying to detect vertical lines where the pixels RGB has every color in less than 100 |Dark| , here is an example RGB (100,100,100).
import numpy as np
import cv2
img = cv2.imread('testD2.png')
lower = np.array([0, 0, 0], dtype = "uint8")
upper = np.array([100,100,100], dtype = "uint8")
mask = cv2.inRange(img, lower, upper)
img = cv2.bitwise_and(img, img, mask = mask)
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
edges = cv2.Canny(gray,50,150,apertureSize = 3)
minLineLength=img.shape[1]-300
lines = cv2.HoughLinesP(image=edges,rho=0.02,theta=np.pi/500, threshold=10,lines=np.array([]), minLineLength=minLineLength,maxLineGap=100)
if lines is not None:
a,b,c = lines.shape
for i in range(a):
cv2.line(img, (lines[i][0][0], lines[i][0][1]), (lines[i][0][2], lines[i][0][3]), (0, 0, 255), 3, cv2.LINE_AA)
cv2.imshow('edges', edges)
cv2.imshow('result', img)
cv2.waitKey(0)
cv2.destroyAllWindows()
i have to change the color of the end lines too,i mean the first and the last line.
Using cv2.findContours() may work better:
You can use cv2.findContours() and cv2.boundingRect() to identify the bars and return the information (x,y,h,w) that describes these rectangles. Here are a few examples.
If you want to only identify the lines and mark them you can do:
import cv2
import numpy as np
img = cv2.imread('oVKlP.png')
g = cv2.imread('oVKlP.png',0)
(T, mask) = cv2.threshold(g, 100, 255, cv2.THRESH_BINARY_INV)
_, contours, hierarchy = cv2.findContours(mask.copy(), cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
img = cv2.drawContours(img.copy(), contours, -1, (0,255,0), 2)
cv2.imwrite('just_contours.png',img)
Result:
If you want to display some of the line info like maybe the x value for a side of the bar you can do:
import cv2
import numpy as np
img = cv2.imread('oVKlP.png')
g = cv2.imread('oVKlP.png',0)
(T, mask) = cv2.threshold(g, 100, 255, cv2.THRESH_BINARY_INV)
_, contours, hierarchy = cv2.findContours(mask.copy(), cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
# bounds with x,y,h,w for each bar
bounds = [cv2.boundingRect(i) for i in contours]
bounds.reverse()
img = cv2.drawContours(img.copy(), contours, -1, (0,0,255), 2)
font = cv2.FONT_HERSHEY_SIMPLEX
n = 20
b = 0
for (x,y,w,h) in bounds:
cv2.circle(img, (x,y+n+10), 5, (0, 255, 0), -1, cv2.LINE_AA)
cv2.putText(img, '{0}'.format(x), (x-b, y+n), font, .6, (255, 0, 255), 2, cv2.LINE_AA)
n+=33
b+=3
cv2.imwrite('fancy_marks.png',img)
Result: