OpenCV in Python provides the following code:
regions, hierarchy = cv2.findContours(binary_image, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
for region in regions:
x, y, w, h = cv2.boundingRect(region)
cv2.rectangle(image, (x, y), (x + w, y + h), (0, 255, 0), 1)
This gives some contours within contour. How to remove them in Python?
For that, you should take a look at this tutorial on how to use the hierarchy object returned by the method findContours .
The main point is that you should use cv2.RETR_TREE instead of cv2.RETR_LIST to get parent/child relationships between your clusters:
regions, hierarchy = cv2.findContours(binary_image, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
Then you can check whether a contour with index i is inside another by checking if hierarchy[0,i,3] equals -1 or not. If it is different from -1, then your contour is inside another.
In order to remove the contours inside a contour:
shapes, hierarchy = cv2.findContours(image=image, mode=cv2.RETR_EXTERNAL, method=cv2.CHAIN_APPROX_SIMPLE)
However, in some cases you may observe that a big contour is formed on the whole image, and applying the above returns you that one big contour.
In order to avoid this, try inverting the image:
image = cv2.imread("Image Path")
image = 255 - image
shapes, hierarchy = cv2.findContours(image=image, mode=cv2.RETR_EXTERNAL, method=cv2.CHAIN_APPROX_SIMPLE)
This will give you the desired result.
UPDATE:
The reason why hierarchy does not work if a big bounding box is approximated on the whole image is that the output of hierarchy[0,iteration,3] is -1 only for the one bounding box drawn on the whole image, as all other bounding boxes are inside this big bounding box, and hierarchy[0,iteration,3] is not equal to -1 for any of them. Thus, inverting the image will be required in order to comply with the following:
In OpenCV, finding contours is like finding white object from black background. So remember, object to be found should be white and background should be black.
However, as pointed out by #Jeru, this is not a generalized solution and one must visualize the image before inverting it.
Consider this image:
Running
shapes, hierarchy = cv2.findContours(image=image, mode=cv2.RETR_TREE, method=cv2.CHAIN_APPROX_SIMPLE)
results in
Now, only displaying the contour with hierarchy[0,iteration,3] = -1 results in
which is not correct. If we want to obtain the rectangle containing the shapes and the text shapes, we can do
image = 255 - image
shapes, hierarchy = cv2.findContours(image=thresh, mode=cv2.RETR_EXTERNAL, method=cv2.CHAIN_APPROX_SIMPLE)
In this case we get:
Code:
import cv2
from easyocr import Reader
import math
shape_number = 2
image = cv2.imread("Image Path")
deep_copy = image.copy()
image_gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
ret, thresh = cv2.threshold(image_gray, 210, 255, cv2.THRESH_BINARY)
thresh = 255 - thresh
shapes, hierarchy = cv2.findContours(image=thresh, mode=cv2.RETR_EXTERNAL, method=cv2.CHAIN_APPROX_SIMPLE)
cv2.drawContours(image=deep_copy, contours=shapes, contourIdx=-1, color=(0, 255, 0), thickness=2, lineType=cv2.LINE_AA)
for iteration, shape in enumerate(shapes):
if hierarchy[0,iteration,3] == -1:
print(hierarchy[0,iteration,3])
print(iteration)
cv2.imshow('Shapes', deep_copy)
cv2.waitKey(0)
cv2.destroyAllWindows()
img_output, contours, hierarchy = cv2.findContours(blank_image_firstImage, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
This removes the child contour
Related
I tried the code provided bellow to segment each digit in this image and put a contour around it then crop it out but it's giving me bad results, I'm not sure what I need to change or work on.
The best idea I can think of right now is filtering the 4 largest contours in the image except the image contour itself.
The code I'm working with:
import sys
import numpy as np
import cv2
im = cv2.imread('marks/mark28.png')
im3 = im.copy()
gray = cv2.cvtColor(im, cv2.COLOR_BGR2GRAY)
blur = cv2.GaussianBlur(gray, (5, 5), 0)
thresh = cv2.adaptiveThreshold(blur, 255, 1, 1, 11, 2)
################# Now finding Contours ###################
contours, hierarchy = cv2.findContours(thresh, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
samples = np.empty((0, 100))
responses = []
keys = [i for i in range(48, 58)]
for cnt in contours:
if cv2.contourArea(cnt) > 50:
[x, y, w, h] = cv2.boundingRect(cnt)
if h > 28:
cv2.rectangle(im, (x, y), (x + w, y + h), (0, 0, 255), 2)
roi = thresh[y:y + h, x:x + w]
roismall = cv2.resize(roi, (10, 10))
cv2.imshow('norm', im)
key = cv2.waitKey(0)
if key == 27: # (escape to quit)
sys.exit()
elif key in keys:
responses.append(int(chr(key)))
sample = roismall.reshape((1, 100))
samples = np.append(samples, sample, 0)
responses = np.array(responses, np.float32)
responses = responses.reshape((responses.size, 1))
print
"training complete"
np.savetxt('generalsamples.data', samples)
np.savetxt('generalresponses.data', responses)
I need to change the if condition on height probably but more importantly I need if conditions to get the 4 largest contours on the image. Sadly, I haven't managed to find what I'm supposed to be filtering.
This is the kind of results I get, I'm trying to escape getting those inner contours on the digit "zero"
Unprocessed images as requested: example 1 example 2
All I need is an idea on what I should filter for, don't write code please. Thank you community.
You almost have it. You have multiple bounding rectangles on each digit because you are retrieving every contour (external and internal). You are using cv2.findContours in RETR_LIST mode, which retrieves all the contours, but doesn't create any parent-child relationship. The parent-child relationship is what discriminates between inner (child) and outter (parent) contours, OpenCV calls this "Contour Hierarchy". Check out the docs for an overview of all hierarchy modes. Of particular interest is RETR_EXTERNAL mode. This mode fetches only external contours - so you don't get multiple contours and (by extension) multiple bounding boxes for each digit!
Also, it seems that your images have a red border. This will introduce noise while thresholding the image, and this border might be recognized as the top-level outer contour - thus, every other contour (the children of this parent contour) will not be fetched in RETR_EXTERNAL mode. Fortunately, the border position seems constant and we can eliminate it with a simple flood-fill, which pretty much fills a blob of a target color with a substitute color.
Let's check out the reworked code:
# Imports:
import cv2
import numpy as np
# Set image path
path = "D://opencvImages//"
fileName = "rhWM3.png"
# Read Input image
inputImage = cv2.imread(path+fileName)
# Deep copy for results:
inputImageCopy = inputImage.copy()
# Convert BGR to grayscale:
grayscaleImage = cv2.cvtColor(inputImage, cv2.COLOR_BGR2GRAY)
# Threshold via Otsu:
threshValue, binaryImage = cv2.threshold(grayscaleImage, 0, 255, cv2.THRESH_BINARY_INV+cv2.THRESH_OTSU)
The first step is to get the binary image with all the target blobs/contours. This is the result so far:
Notice the border is white. We have to delete this, a simple flood-filling at position (x=0,y=0) with black color will suffice:
# Flood-fill border, seed at (0,0) and use black (0) color:
cv2.floodFill(binaryImage, None, (0, 0), 0)
This is the filled image, no more border!
Now we can retrieve the external, outermost contours in RETR_EXTERNAL mode:
# Get each bounding box
# Find the big contours/blobs on the filtered image:
contours, hierarchy = cv2.findContours(binaryImage, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
Notice you also get each contour's hierarchy as second return value. This is useful if you want to check out if the current contour is a parent or a child. Alright, let's loop through the contours and get their bounding boxes. If you want to ignore contours below a minimum area threshold, you can also implement an area filter:
# Look for the outer bounding boxes (no children):
for _, c in enumerate(contours):
# Get the bounding rectangle of the current contour:
boundRect = cv2.boundingRect(c)
# Get the bounding rectangle data:
rectX = boundRect[0]
rectY = boundRect[1]
rectWidth = boundRect[2]
rectHeight = boundRect[3]
# Estimate the bounding rect area:
rectArea = rectWidth * rectHeight
# Set a min area threshold
minArea = 10
# Filter blobs by area:
if rectArea > minArea:
# Draw bounding box:
color = (0, 255, 0)
cv2.rectangle(inputImageCopy, (int(rectX), int(rectY)),
(int(rectX + rectWidth), int(rectY + rectHeight)), color, 2)
cv2.imshow("Bounding Boxes", inputImageCopy)
# Crop bounding box:
currentCrop = inputImage[rectY:rectY+rectHeight,rectX:rectX+rectWidth]
cv2.imshow("Current Crop", currentCrop)
cv2.waitKey(0)
The last three lines of the above snippet crop and show the current digit. This is the result of detected bounding boxes for both of your images (the bounding boxes are colored in green, the red border is part of the input images):
I would like to crop an image which has a hand drawn highlighted area in orange as shown below,
The result should be a cropped image along the major axis of the blob or contour with a rectangular bounding box, as shown below,
Here's what i have tried,
import numpy as np
import cv2
# load the image
image = cv2.imread("frame50.jpg", 1)
#color boundaries [B, G, R]
lower = [0, 3, 30]
upper = [30, 117, 253]
# create NumPy arrays from the boundaries
lower = np.array(lower, dtype="uint8")
upper = np.array(upper, dtype="uint8")
# find the colors within the specified boundaries and apply
# the mask
mask = cv2.inRange(image, lower, upper)
output = cv2.bitwise_and(image, image, mask=mask)
ret,thresh = cv2.threshold(mask, 50, 255, 0)
if (int(cv2.__version__[0]) > 3):
contours, hierarchy = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
else:
im2, contours, hierarchy = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
if len(contours) != 0:
# find the biggest countour (c) by the area
c = max(contours, key = cv2.contourArea)
x,y,w,h = cv2.boundingRect(c)
ROI = image[y:y+h, x:x+w]
cv2.imshow('ROI',ROI)
cv2.imwrite('ROI.png',ROI)
cv2.waitKey(0)
This does not seem to work most of the time.For some images, the following happens,
I would like to know if there is better way to go about this or how i can fix what i have right now.Note that the highlighted area is hand drawn and can be of any shape but it is closed and not left open and the colour of the highlight is that shade of orange itself in all cases.
And is there a way to only retain content inside the circle and blackout everything outside it?
EDIT1:
I was able to fix the wrong clipping by varying the threshold more. But my main query now is: is there a way to only retain content inside the circle and blackout everything outside it? I can see the mask as show below,
How do I fill this mask and retain content inside the circle and blackout everything outside it, with the same rectangular bounding box?
Have you tried
image[x:x+w, y:y+h]
And could you check bbox with below code
cv2.rectangle(thresh,(x,y),(x+w,y+h),(255,0,0),2)
First of all, it is always better to use an HSV image instead of BGR image for masking(extracting a color). You can do this by the following code.
HSV_Image = cv2.cvtColor(Image, cv2.COLOR_BGR2HSV)
ThreshImage = cv2.inRange(HSV_Image, np.array([0, 28, 191]), np.array([24, 255, 255]))
The range numbers here are found for orange color in this case.
Image is the input image and ThreshImage is the output image with the orange region colored as white and everything else as black.
Now finding the contour in ThreshImage with cv2.RETR_EXTERNAL flag will give only one contour that is the outer boundary of the orange region.
Contours, Hierarchy = cv2.findContours(ThreshImage, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
To crop the orange region:
BoundingRect = cv2.boundingRect(Contours[0])
(x, y, w, h) = BoundingRect
CroppedImage = Image[y:y+h, x:x+w].copy()
"CroppedImage" will store the cropped orange region as desired.
To get contents of only inside the contour:
Bitwise AND operation will be useful here as we already have detected the contour.
First, we have to create a black image of shape the same as that of input image and draw the contour with white color filled in it.
ContourFilledImage = np.zeros(Image.shape, dtype=np.uint8)
cv2.drawContours(ContourFilledImage, Contours, -1, (255, 255, 255), -1)
Now perform a bitwise AND operation on Input Image and "ContourFilledImage"
OnlyInnerData = cv2.bitwise_and(ContourFilledImage, Image)
"OnlyInnerData" image is the desired output image having only the content of inside the circle.
I want to fit an ellipse to a partly damaged object in a picture. (The pictures here are only simplified examples for illustration!)
Image with damaged elliptical object
By doing this
def sort(n):
return n.size
Image = cv2.imread('acA2500/1.jpg', cv2.IMREAD_GRAYSCALE)
#otsu binarization
_, binary = cv2.threshold(Image, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
#invert binary image for opencv findContours
inverse_binary = cv2.bitwise_not(binary)
#find contours
contours, _ = cv2.findContours(inverse_binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)
#sort contours by length
largest_contours = sorted(contours, key=sort, reverse=True)
#fit ellipse
contour = largest_contours[0]
ellipse = cv2.fitEllipseDirect(contour)
I get this result, which is not very satisfying.
Result of cv2.findContours and cv2.fitEllipse
So, I've built this loop to get rid of the contour points which are not on the ellipse.
contour = largest_contours[0]
newcontour = np.zeros([1, 1, 2])
newcontour = newcontour.astype(int)
for coordinate in contour:
if coordinate[0][0] < 600:
newcontour = np.insert(newcontour, 0, coordinate, 0)
newellipse = cv2.fitEllipse(newcontour)
And get this result, which is good.
Result after trimming the contour points
The Problem is, that I have to do a lot of these fits in a short time period. So far this does not reach the desired speed.
Is there any better/faster/nicer way to trim the contour points? Since I don't have a lot of coding experience, I would be happy to find some help here :-)
Edit:
I edited the example pictures so that it is now clear that unfortunatly a cv2.minEnclosingCircle approach does not work.
Also the pictures now demonstrate why I sort the contours. In my real code I fit an ellipse to the three longest contours an than see which one I want to use trough a different routine.
If I don't trimm the contour and pick the contour for cv2.fitEllipse by hand, the code needs around 0.5s. With contour trimming and three times cv2.fitEllipse it takes around 2s. It may only take 1s
If the object is an circle, then you can use cv2.minEnclosingCircle on the contour to capture it.
#!/usr/bin/python3
# 2019/02/13 08:50 (CST)
# https://stackoverflow.com/a/54661012/3547485
import cv2
img = cv2.imread('test.jpg')
## Convert to grayscale and threshed it
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
th, threshed = cv2.threshold(gray, 100, 255, cv2.THRESH_OTSU|cv2.THRESH_BINARY_INV)
## Find the max outers contour
cnts = cv2.findContours(threshed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)[-2]
cv2.drawContours(img, cnts, -1, (255, 0, 0), 2, cv2.LINE_AA)
## Use minEnclosingCircle
(cx,cy),r = cv2.minEnclosingCircle(cnts[0])
cv2.circle(img, (int(cx), int(cy)), int(r), (0, 255, 0), 1, cv2.LINE_AA)
## This it
cv2.imwrite("dst.jpg", img)
Here is my result.
I'm trying to isolate letters in a captcha, I managed to filter a captcha and that result in this black and white image:
But when I tried to separate the letters with findContours method of OpenCV it just found a external contour that wraps my entire image, resulting in this image (black contour outside image).
I'm using this code with Python 3 and OpenCV 3.4.2.17:
img = threshold_image(img)
cv2.imwrite("images/threshold.png", img)
image, contours, _ = cv2.findContours(img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
for i, contour in enumerate(contours):
area = cv2.contourArea(contour)
cv2.drawContours(img, contours, i, (0, 0, 0), 3)
cv2.imwrite('images/output3.png', img)
I just want my final result is 5 contours outside each character.
The contour to be extracted should be in white, background being black. I have modified your code a bit, eliminated the lines which were not adding any value.
import cv2
img = cv2.imread('image_to_be_read',0)
backup = img.copy() #taking backup of the input image
backup = 255-backup #colour inversion
I am using RETR_TREE as the contour retrieval mode, which retrieves all the contours and creates full family hierarchy list. Please find the documentation for the same here
_, contours, _ = cv2.findContours(backup, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)
In opencv4, the finContours method has been changed. Please use:
contours, _ = cv2.findContours(backup, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)
Then iterate through contours and draw the rectangle around the contours
for i, contour in enumerate(contours):
x, y, w, h = cv2.boundingRect(contour)
cv2.rectangle(img, (x, y), (x + w, y + h), (0, 255, 0), 1)
Save the image
cv2.imwrite('output3.png', img)
I got result which looks like this -
You used flag RETR_EXTERNAL, which means it is looking only for outermost contours of objects, but not holes. In your case the white object that covers a whole image with few holes (letters/digits) is found. You have two choices:
Inverse colors in your image with "bitwise_not"
Collect all contours with RETR_LIST flag. Note that it will also collect holes inside digits.
I am currently using opencv to try and detect the rect around each individual parking sign in a group of signs. Using "findContours" and "approxPolyDP" has gotten me close but I want consolidate the contours into a single rect. Using "boundingRect" has not worked since there are breaks in the shape.
Let me know if you have any advise on how to approach this problem.
Here is my code:
image = cv2.imread(sign_directory)
# find all the 'red' shapes in the image
lower = np.array([0, 0, 110])
upper = np.array([100, 100, 250])
shapeMask = cv2.inRange(image, lower, upper)
# find the contours in the mask
(img, cnts, _) = cv2.findContours(shapeMask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
for c in cnts:
x,y,w,h = cv2.boundingRect(c)
cv2.rectangle(image,(x,y),(x+w,y+h),(0,255,0),2)
cv2.imshow("Image", image)
cv2.waitKey(0)
This is the image it produces
I want to get something like this so that I can crop it
This is the original
Instead of passing each contour to cv2.boundingRect() you want to merge them all in a single array and then pass to that function, then you get the bounding box you need to crop. But before doing that you need to remove outliers/noise, the contours that are too far from the others.