Get the count of rectangles in an image - python

I have an image like the one below, and I want to determine the number of rectangles in the image. I do know how to do this if they were filled.
contours = cv2.findContours(image.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
contours = contours[0] if imutils.is_cv2() else contours[1]
print len(contours)
But this does not work if the rectangle is empty.
I also do not know how to fill the rectangles in the image. I know how to fill the contours if they are drawn using OpenCV, but I do not know how to fill empty rectangles already present in the image.

Assuming you have tried shape detectors, line detections, etc and not succeeded here is another way of solving this problem.
If this is a grayscale PNG image, you can use segmentation by color to achieve this.
I would approach it like so:
count = 0
For each pixel in the image:
if color(pixel) == white /*255*/
count++
floodfill using this pixel as a seed pixel and target color as count
no_of_rectangles = count - 1 /* subtract 1 since the background will be colored too*/
This assumes the rectangles have continuous lines, else the floodfill will leak into other rectangles.

Filled or not should not make a difference if you find the outer contours (RETR_EXTERNAL). Following code will give you number 3.
canvas = np.zeros(img.shape, np.uint8)
img2gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
ret,thresh = cv2.threshold(img2gray,128,255,cv2.THRESH_BINARY_INV)
im2,contours,hierarchy = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
print(len(contours))
for cont in contours:
cv2.drawContours(canvas, cont, -1, (0, 255, 0), 3)
cv2.imshow('contours',canvas)
cv2.waitKey(30000)
cv2.destroyAllWindows()
Notice if you use RETR_TREE as the 2nd parameter in findContours(), you get all 6 contours, including the inner ones.
Obviously, this assumes that the image only contains rectangles and it doesn't distinguish different shapes.

Related

cv2.boundingRect creating issue

I am working on preprocessing images of digits. Each image contains a single number and some unwanted contours .
This noise or contours can be in the form of lines or small dots. My task is to remove these contours. So I decided to use the following algorithm
Find all contours
Sort the contours according to the area in descending order
Iterate through contours from index 2 to last
create a boundingRect and fill that part of the image with 255
cv2.findContours(image, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
contours = sorted(contours, key = cv2.contourArea, reverse = True)
for c in contours[2:]:
(x, y, w, h) = cv2.boundingRect(c)
image[y:y + h, x:x + w] = 255
Now the problem is that in case of first image boundingRect is returning (14,14,150,150) which covers the digit too. Now my question is that is there any better alternative to boundingRect so that only contour area can be replaced.
Output images were following:
Original image files are following
I'm able to achieve the desired output by these steps:
Inverse the color of the image (Or change the retrieving method to RETR_INTERNAL while finding contours). White pixels have value of 1 and blacks have the value of 0, thus black contours can not be detected by RETR_EXTERNAL operation.
Sort te contours according to area as you did
Fill the contours starting from the second largest contour with white.
Code:
# Images I copied from here were not single channel, so convert to single channel to be able to find contours.
img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# Copy the original image to draw on it later on
img_copy = img.copy()
# Invert the color if using RETR_EXTERNAL
img = cv2.bitwise_not(img)
# Note that findContours function returns hierarchy as well as the contours in OpenCV4.x, in OpenCV3.x it used to return 3 values.
contours, hierarchy = cv2.findContours(img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# Sort the contours descending according to their area
contours = sorted(contours, key =lambda x: cv2.contourArea(x), reverse = True)
# Fill the detected contours starting from second largest
for c in contours[1:]:
print(c)
img_copy = cv2.fillPoly(img_copy, [c], color=(255,255,255))
Output: (Black bar between windows is my background)

Find contours of touching objects as two distinct contours

I'm wondering how to count objects that are connected to each other as two distinct objects using cv2.findContours in Python
For example this image :
contours, hierarchy = cv2.findContours(img, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
Will output one contour.
What can I do to get two contours instead ?
This can be done by converting your input image to edge image and then detecting contours. But in this case, there is a break in edge image(I tried with canny) at the intersection of the two objects as shown below.
Break in Canny:
Whereas the expected edge image should have all the pixels at the boundary as white.
Expected edge image:
Thus to get this perfect edge image, I have created an algo shared below(This algo will work only on a binary image like this with objects filled with white color).
Before using this algo, make sure that the object does not lie on the boundary, that is, all the boundary pixels of the image should be black. If not black, add a border on all sides of the image of black colour and length 1 pixel.
# Creating black image of same shape and single channel
edge = np.zeros(img.shape, dtype = np.uint8)
h, w = img.shape[:2]
# Iterating over each pixel except those at the boundary
for i in range(1, h-1):
for j in range(1, w-1):
# if current pixel is white
if img[i][j] == 255:
# roi is the image of 9 pixel from around the current pixel
roi = img[i-1:i+2, j-1:j+2].copy()
# Counting number of black pixel in the neighbourhood of current pixel
blackCount = np.sum(roi == 0)
# if a neighbouring pixel is black, then current pixel is a boundary pixel.
if blackCount > 0:
edge[i][j] = 255
After finding the edge image, get all the contours in the image:
cont, hier = cv2.findContours(edge, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)
For this image, you will get 3 contours, 2 for the two objects and 1 for the two objects combined. To eliminate the contour of both the objects combined, use the hierarchy information.
# I am taking only those contours which do not have a child contour.
finalContours = np.asarray([cont[i] for i in range(len(cont)) if hier[0][i][2] == -1])
"finalContours" will have the 2 contours for the two objects.
Refer to this link for more information about the parent-child relationship of the contours

Crop simple bounding box with openCV

I have some images with a black background and some text in the corner:
I'm trying to do a rectangular crop to make it look like:
The text on the sides as well as the window dimensions are of varying sizes. My code isn't cropping correctly, what am I doing wrong?
I've tried removing the text in the bottom right corner first and cropping, that doesn't work either.
def crop_cont(img):
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
_,thresh = cv2.threshold(gray,15,255,cv2.THRESH_BINARY)
_, contours, _= cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnt = contours[0]
x,y,w,h = cv2.boundingRect(cnt)
crop = img[y:y+h,x:x+w]
return crop
Your code is in general ok. The issue is that you are using contours[0]. You have to find the right contour (there are more than one). In this particular example, the right contour is the biggest one. Just iterate all found contours and find the one with the biggest area.

Is it possible to use OpenCV contour in python in a way that corners are not cut off?

I am using the OpenCV contour function in python. For example on an image like this:
contours, _ = cv2.findContours(img_expanded_padded, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)
It works well except that it cuts off the corners on the inside of the contour as seen above. Are there any options that would leave this corner in?
Travelling along the contours and filling them in manually will be too computationally expensive. The above is only an example. This will be performed many times on images 5400x5400 or more...
I can find the edges with the code below, and have filled corners as a result but then I need to extract these as contours again.
# FIND ALL HORIZONTAL AND VERTICAL EDGES AND COMBINE THEM
edges_expanded_x = np.absolute(cv2.Sobel(img_expanded_padded,cv2.CV_64F, 1, 0, ksize=3))
edges_expanded_y = np.absolute(cv2.Sobel(img_expanded_padded,cv2.CV_64F, 0, 1, ksize=3))
edges_expanded = np.logical_or(edges_expanded_x, edges_expanded_y)
# GET RID OF DOUBLE EDGE THAT RESULTS FROM SOBEL FILTER
edges_expanded = np.multiply(img_expanded_padded,edges_expanded)
Are there any OpenCV settings or functions I can use to accomplish this?
EDIT:
I should clarify, that my goal is to have a single pixel continous contour. I need the contours and not an array of the entire image including the contours.
EDIT: The above images are zoomed into my test image. The actual pixels are as shown by the red grids in the images below.
There is no need to use cv2.Sobel you can simply draw contours with cv2.drawContours on a black background. The black background can be drawn with the help of np.zeros.
img = cv2.imread('contouring.png',0)
contours, _ = cv2.findContours(img, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
bgr = np.zeros((img.shape[0], img.shape[1]), dtype= 'uint8')
cv2.drawContours(bgr, contours, -1, (255,255,255), 1)
If you want the contour lines to be thick then you use cv2.dilate for that. Then to prevent the cutting of corners cv2.bitwise_and can be used along with cv2.bitwise_not as shown below
bgr = cv2.dilate(bgr, np.ones((31, 31), np.uint8), iterations=1)
bgr = cv2.bitwise_and(bgr, cv2.bitwise_not(img))
This gives contours which are 15 pixels thick.
EDIT- The first image of thin contours is still cutting corners. To obtain single pixel contours which do not corners we can use a kernel size of 3*3.
img = cv2.imread('contouring.png',0)
contours, _ = cv2.findContours(img, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
bgr = np.zeros((img.shape[0], img.shape[1]), dtype= 'uint8')
cv2.drawContours(bgr, contours, -1, (255,255,255), 1)
bgr = cv2.dilate(bgr, np.ones((3, 3), np.uint8), iterations=1)
bgr = cv2.bitwise_and(bgr, cv2.bitwise_not(img))
This gives us
I have checked it by using cv2.bitwise_and between bgr and img and I obtain a black image indicating that no white pixels are cutting corners.

Copy area inside contours to another image

I have the following code in Python to find contours in my image:
import cv2
im = cv2.imread('test.jpg')
imgray = cv2.cvtColor(im, cv2.COLOR_BGR2GRAY)
ret, thresh = cv2.threshold(imgray, 127, 255, 0)
im2, contours, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
Now I want to copy the area inside the first contour to another image, but I can't find any tutorial or example code that shows how to do that.
Here's a fully working example. It's a bit overkill in that it outputs all the contours but I think you may find a way to tweak it to your liking. Also not sure what you mean by copying, so I'll assume you just want the contours outputted to a file.
We will start with an image like so (in this case you will notice I don't need to threshold the image). The script below can be broken down into 6 major steps:
Canny filter to find the edges
cv2.findContours to keep track of our contours, note that we only need outer contours hence the cv2.RETR_EXTERNAL flag.
cv2.drawContours draws the shapes of each contour to our image
Loop through all contours and put bounding boxes around them.
Use the x,y,w,h information of our boxes to help us crop every contour
Write the cropped image to a file.
import cv2
image = cv2.imread('images/blobs1.png')
edged = cv2.Canny(image, 175, 200)
contours, hierarchy = cv2.findContours(edged, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cv2.drawContours(image, contours, -1, (0,255,0), 3)
cv2.imshow("Show contour", image)
cv2.waitKey(0)
cv2.destroyAllWindows()
for i,c in enumerate(contours):
rect = cv2.boundingRect(c)
x,y,w,h = rect
box = cv2.rectangle(image, (x,y), (x+w,y+h), (0,0,255), 2)
cropped = image[y: y+h, x: x+w]
cv2.imshow("Show Boxes", cropped)
cv2.waitKey(0)
cv2.destroyAllWindows()
cv2.imwrite("blobby"+str(i)+".png", cropped)
cv2.imshow("Show Boxes", image)
cv2.waitKey(0)
cv2.destroyAllWindows()
Not familiar with cv2.findContours but I imagine that a contour is represented by an array of points with row/column (x/y values. If this is the case and the contour is a single pixel in width then there should be two points for every row - one each on the left and right extremes of the contour.
For each row in the contour
*select* all the points in the image that are between the two contour points for that row
save those points to a new array.
As #DanMaĆĄek points out, if the points in the contour array describe a simple shape with with only the ends, corners or breakpoints represented then you would need to fill in the gaps to use the method above.
Also if the contour shape is something like a star you would need to figure out a different method for determining if an image point is inside of the contour. The method I posted is a bit naive - but might be a good starting point. For a convoluted shape like a star there might be multiple points per row of the contour but it seems like the points would come in pairs and the points you are interested in would be between the pairs.
.

Categories