python opencv crop using contour hierarchy - python

I was looking to remove the borders from the below image
what I have tried till now is using OpenCV to get edges
code:
def autocrop(image, threshold=0):
"""Crops any edges below or equal to threshold
Crops blank image to 1x1.
Returns cropped image.
"""
if len(image.shape) == 3:
flatImage = np.max(image, 2)
else:
flatImage = image
assert len(flatImage.shape) == 2
rows = np.where(np.max(flatImage, 0) > threshold)[0]
if rows.size:
cols = np.where(np.max(flatImage, 1) > threshold)[0]
image = image[cols[0]: cols[-1] + 1, rows[0]: rows[-1] + 1]
else:
image = image[:1, :1]
return image
no_border = autocrop(new_image)
cv2.imwrite('no_border.png',no_border)
the result is this image , next how to remove those boxes
Update :
I have found that the solution works for a white background but when I change the background color border are not removed
Edited
I have tried the solution on this image
But the result was like this
How I can achieve a complete removal of the boundary boxes .

For this we use floodFill function.
import cv2
import numpy as np
if __name__ == '__main__':
# read image and convert to gray
img = cv2.imread('image.png',cv2.IMREAD_UNCHANGED)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# threshold the gray image to binarize, and negate it
_,binary = cv2.threshold(gray, 150, 255, cv2.THRESH_BINARY)
binary = cv2.bitwise_not(binary)
# find external contours of all shapes
_,contours,_ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
# create a mask for floodfill function, see documentation
h,w,_ = img.shape
mask = np.zeros((h+2,w+2), np.uint8)
# determine which contour belongs to a square or rectangle
for cnt in contours:
poly = cv2.approxPolyDP(cnt, 0.02*cv2.arcLength(cnt,True),True)
if len(poly) == 4:
# if the contour has 4 vertices then floodfill that contour with black color
cnt = np.vstack(cnt).squeeze()
_,binary,_,_ = cv2.floodFill(binary, mask, tuple(cnt[0]), 0)
# convert image back to original color
binary = cv2.bitwise_not(binary)
cv2.imshow('Image', binary)
cv2.waitKey(0)
cv2.destroyAllWindows()

There is another to find the characters within the image. This using the concept of hierarchy in contours.
The implementation is in python:
path = r'C:\Desktop\Stack'
filename = '2.png'
img = cv2.imread(os.path.join(path, filename), 1)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
_, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY|cv2.THRESH_OTSU)
_, contours2, hierarchy2 = cv2.findContours(binary, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_NONE)
Notice that in the cv2.findContours() function is passed in the RETR_CCOMP parameter to store contours according to their different levels of hierarchy. Hierarchy is useful when one contour lies inside another contour, thus enabling and parent-child relationship. RETR_CCOMP helps identify this relationship.
img2 = img.copy()
l = []
for h in hierarchy2[0]:
if h[0] > -1 and h[2] > -1:
l.append(h[2])
In the snippet above I am passing all contours that have a child into the list l. Using l I am drawing those contours in the snippet below.
for cnt in l:
if cnt > 0:
cv2.drawContours(img2, [contours2[cnt]], 0, (0,255,0), 2)
cv2.imshow('img2', img2)
Have a look at the DOCUMENTATION HERE to learn more about hierarchy in contours.

Related

Detecting the edge of a colorful picture OpenCV

I am new to CV and I just learned how to detect the edge of a paper. I want to try something more complicated. So I make a screenshot from a movie website and want to detect the poster from the website. It works well if the background color is different from the poster. But when they are similar in color, I can't find the edge of the picture by
cv2.findContours()
The original Picture is:
Poster
And what I do is:
img = cv2.imread('pic5.jpg')
orig = img.copy()
image = orig
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
binary = cv2.medianBlur(gray,3)
# blur = cv2.GaussianBlur(binary, (5, 5), 0)
# ret, binary = cv2.threshold(blur,127,255,cv2.THRESH_TRUNC)
edged = cv2.Canny(binary, 3, 30)
show(edged)
# detect edge
contours, hierarchy = cv2.findContours(edged.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
cnts = sorted(contours, key=cv2.contourArea, reverse=True)[:5]
#
for c in cnts:
# approx
peri = cv2.arcLength(c, True)
eps = 0.02
approx = cv2.approxPolyDP(c, eps*peri, True)
# detect square (4 points)
if len(approx) == 4:
screenCnt = approx
break
res = cv2.drawContours(image, [screenCnt], -1, (0, 255, 0), 2)
show(orig)
And the result is:
after preprocess
What I detect
I don't know if this method works. Is it possible to detect the square part based on the background color (regardless of the poster's color)?
You may continue with the edged result, and use closing morphological operation for closing small gaps.
Instead of searching for a rectangle using approxPolyDP, I suggest you to find the bounding rectangle of the largest connected component (or largest contour).
In my code sample, I replaced findContours with connectedComponentsWithStats due to the external boundary line.
You may use opening morphological operation to get rid of the external line (and use continue using findContours).
You may also use approxPolyDP for refining the result.
Here is the code sample:
import numpy as np
import cv2
img = cv2.imread('pic5.png')
orig = img.copy()
image = orig
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
binary = cv2.medianBlur(gray, 3)
edged = cv2.Canny(binary, 3, 30)
edged = cv2.morphologyEx(edged, cv2.MORPH_CLOSE, np.ones((5,5))) # Close small gaps
#contours, hierarchy = cv2.findContours(edged, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
#c = max(contours, key=cv2.contourArea) # Get the largest contour
#x, y, w, h = cv2.boundingRect(c) # Find bounding rectangle.
nb_components, output, stats, centroids = cv2.connectedComponentsWithStats(edged, 8) # finding components
# https://stackoverflow.com/a/61662694/4926757
# Find the largest non background component.
# Note: range() starts from 1 since 0 is the background label.
max_label, max_size = max([(i, stats[i, cv2.CC_STAT_AREA]) for i in range(1, nb_components)], key=lambda x: x[1])
# Find bounding rectangle of largest connected component.
x = stats[max_label, cv2.CC_STAT_LEFT]
y = stats[max_label, cv2.CC_STAT_TOP]
w = stats[max_label, cv2.CC_STAT_WIDTH]
h = stats[max_label, cv2.CC_STAT_HEIGHT]
res = image.copy()
cv2.rectangle(res, (x, y), (x+w, y+h), (0, 255, 0), 2) # Draw a rectangle
cv2.imshow('edged', edged)
cv2.imshow('res', res)
cv2.waitKey()
cv2.destroyAllWindows()
Results:
edged:
res:

How can I reliably choose only the outer contour of a maksed droplet image with python opencv?

I am writing a program which needs to detect the outer contour of a droplet and fit an ellipse to that shape.
I have a setup that has been working fine:
canny edge detection
find contours
pick longest contour
fit ellipse
But now the process needs to include a mask for the syringe that dispenses the droplet, which splits the outer contour in half.
I know how to mask the detected edges from the array returned from canny but I don't know how to proceed from there.
I need to use the two outer contours to fit the ellipse but I don' know how to reliably extract those.
Minimal working code:
from typing import Tuple
import cv2
import numpy as np
def evaluate_droplet(img, y_base, mask: Tuple[int,int,int,int] = None):
# crop img from baseline down (contains no useful information)
crop_img = img[:y_base,:]
shape = img.shape
height = shape[0]
width = shape[1]
# calculate thrresholds
thresh_high, thresh_im = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
thresh_low = 0.5*thresh_high
bw_edges = cv2.Canny(crop_img, thresh_low, thresh_high)
# block detection of syringe
if (not mask is None):
x,y,w,h = mask
bw_edges[y:y+h, x:x+w] = 0
cv2.imshow('bw',bw_edges)
cv2.waitKey(0)
# for testing purposes:
if __name__ == "__main__":
im = cv2.imread('untitled1.png', cv2.IMREAD_GRAYSCALE)
im = np.reshape(im, im.shape + (1,) )
(h,w,d) = np.shape(im)
try:
drp = evaluate_droplet(im, 250, (int(w/2-40), 0, 80, h))
except Exception as ex:
print(ex)
cv2.imshow('Test',im)
cv2.waitKey(0)
the image used:
I solved it like this for now:
Calculate the area of the bounding rect for each contour and pick largest two
def evaluate_droplet(img, y_base, mask: Tuple[int,int,int,int] = None):
# crop img from baseline down (contains no useful information)
crop_img = img[:y_base,:]
shape = img.shape
height = shape[0]
width = shape[1]
# calculate thrresholds
thresh_high, thresh_im = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
thresh_low = 0.5*thresh_high
bw_edges = cv2.Canny(crop_img, thresh_low, thresh_high)
# block detection of syringe
if (not mask is None):
x,y,w,h = mask
bw_edges[y:y+h, x:x+w] = 0
contours, hierarchy = cv2.findContours(bw_edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
if len(contours) == 0:
print("no contour found!")
cont_area_list = []
# match contours with their bounding rect area
for cont in contours:
x,y,w,h = cv2.boundingRect(cont)
cont_area_list.append((cont, w*h))
# sort combined list by area
cont_areas_sorted = sorted(cont_area_list, key=lambda item: item[1])
# largest 2 contours, assumes mask splits largest contour in the middle
if not mask is None:
largest_conts = [elem[0] for elem in cont_areas_sorted[-2:]]
# merge largest 2 contorus into one for handling purposes
contour = np.concatenate((largest_conts[0], largest_conts[1]))
# if no mask is used use only single largest contour
else:
contour = cont_areas_sorted[-1][0]
# display bounding rect of final contour
x,y,w,h = cv2.boundingRect(contour)
bw_edges = cv2.rectangle(bw_edges, (x,y), (x+w,y+h), (255,255,255))
cv2.imshow('bw',bw_edges)
cv2.waitKey(0)

How do I contour multiple largest objects in an image

I'm trying to extract the contours of multiple largest objects within an image. Currently I can only extract the one of the largest objects and the other objects are not contoured. This is the image after threshold, that i'm testing with.
cntrs = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
cntrs = cntrs[0] if len(cntrs) == 2 else cntrs[1]
# create black background image
result = np.zeros_like(src)
area_thresh = 0
for c in cntrs:
area = cv2.contourArea(src)
if area > area_thresh:
area_thresh = area
big_contour = c
This is the code I'm currently using that only extracts one object.
Try this:
import cv2
# Read the image
img=cv2.imread('test.jpg')
# Convert to Gray
imgGray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# Apply threshold and Dilate (to bring out the lines of the plane)
ImgThresh = cv2.threshold(imgGray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)[1]
ImgThreshDilation = cv2.dilate(ImgThresh,(3,3),iterations = 2)
# Find edges
imgEdges = cv2.Canny(ImgThreshDilation,100,200)
# Find contour
contours,hierarchy =cv2.findContours(imgEdges,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)
# Loop through contours and find the two biggest area.
for cont in contours:
area=cv2.contourArea(cont)
if area>300:
#print(area)
cv2.drawContours(img,cont,-1,(0,0,255),2)
cv2.imshow('Image with planes in Red',img)
Here is an edit of the above code.
import cv2
# Read the image
img=cv2.imread('test.jpg')
imgCont=img.copy()
# Convert to Gray
imgGray =255- cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# Find edges
imgEdges = cv2.Canny(imgGray,150,200)
# Find contour
contours,hierarchy =cv2.findContours(imgEdges,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)
# Loop through contours and find the two biggest area.
for cont in contours:
area=cv2.contourArea(cont)
if area>150:
#print(area)
cv2.drawContours(imgCont,cont,-1,(0,0,255),5)
# Save your pictures with the contour in red
cv2.imwrite('Image with planes in Red.jpg',imgCont)
The result:

Remove remains in a letter image with Python

I have a set of images that represent letters extracted from an image of a word. In some images there are remains of the adjacent letters and I want to eliminate them but I do not know how.
Some samples
I'm working with openCV and I've tried two ways and none works.
With findContours:
def is_contour_bad(c):
return len(c) < 50
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
edged = cv2.Canny(gray, 50, 100)
contours = cv2.findContours(edged.copy(), cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
contours = contours[0] if imutils.is_cv2() else contours[1]
mask = np.ones(image.shape[:2], dtype="uint8") * 255
for c in contours:
# if the c ontour is bad, draw it on the mask
if is_contour_bad(c):
cv2.drawContours(mask, [c], -1, 0, -1)
# remove the contours from the image and show the resulting images
image = cv2.bitwise_and(image, image, mask=mask)
cv2.imshow("After", image)
cv2.waitKey(0)
I think it does not work because the image is on the edge cv2.drawContours can not calculate the area correctly and does not eliminate the interior points
With connectedComponentsWithStats:
cv2.imshow("Image", img)
cv2.waitKey(0)
nb_components, output, stats, centroids = cv2.connectedComponentsWithStats(img)
sizes = stats[1:, -1];
nb_components = nb_components - 1
min_size = 150
img2 = np.zeros((output.shape))
for i in range(0, nb_components):
if sizes[i] >= min_size:
img2[output == i + 1] = 255
cv2.imshow("After", img2)
cv2.waitKey(0)
In this case I do not know why the small elements on the sides do not recognize them as connected components
Well..I would greatly appreciate any help!
In the very beginning of the question you have mentioned that letters have been extracted from an image of a word.
So as I think, You could have done the extraction correctly. Then you wouldn't have faced a problem like this. I can give you a solution which is applicable to either extracting letters from original image or extract and separate letters from the image you have given.
Solution:
You can use convex hull coordinates to separate characters like this.
code:
import cv2
import numpy as np
img = cv2.imread('test.png', 0)
cv2.bitwise_not(img,img)
img2 = img.copy()
ret, threshed_img = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY)
image, contours, hier = cv2.findContours(threshed_img, cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_NONE)
#--- Black image to be used to draw individual convex hull ---
black = np.zeros_like(img)
contours = sorted(contours, key=lambda ctr: cv2.boundingRect(ctr)[0])
for cnt in contours:
hull = cv2.convexHull(cnt)
img3 = img.copy()
black2 = black.copy()
#--- Here is where I am filling the contour after finding the convex hull ---
cv2.drawContours(black2, [hull], -1, (255, 255, 255), -1)
r, t2 = cv2.threshold(black2, 127, 255, cv2.THRESH_BINARY)
masked = cv2.bitwise_and(img2, img2, mask = t2)
cv2.imshow("masked.jpg", masked)
cv2.waitKey(0)
cv2.destroyAllWindows()
outputs:
So as I suggest, the better thing is to use this solution when you extract characters from original image rather than removing noises after extraction.
I would try the following:
Sum along the columns so that every image gets projected into a vector
Assuming that white=0 and black=1, find the first index value in that vector that = 0.
Remove the image columns to the left of the index value from step 2.
Reverse the summed vector from step 1
Find the first index value that =0 in the reversed vector from step four.
Remove the image columns to the right of the reversed index value from step 5.
This would work nicely for a binary image where white = 0 and black = 1 but if not, there are several methods around this including image threshholding or setting tolerance levels (e.g. for step 2. find first index value in vector that > tolerance...)

python opencv clear noise and remove boxes

I would like to clear noise from the upper part of the image as it is corrupt and in text recognition produce wrong results
I would like to make letter/characters clear and also remove the box surround letter
i used this code :
def remove_boxes(image,flag=False):
# threshold the gray image to binarize, and negate it
_,binary = cv2.threshold(image, 150, 255, cv2.THRESH_BINARY)
if flag :
binary = cv2.bitwise_not(binary)
# find external contours of all shapes
_,contours,_ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
# create a mask for floodfill function, see documentation
h,w,_= cropped_top.shape
mask = np.zeros((h+2,w+2), np.uint8)
# determine which contour belongs to a square or rectangle
for cnt in contours:
poly = cv2.approxPolyDP(cnt, 0.02*cv2.arcLength(cnt,True),True)
if len(poly) == 4:
# if the contour has 4 vertices then floodfill that contour with black color
cnt = np.vstack(cnt).squeeze()
_,binary,_,_ = cv2.floodFill(binary, mask, tuple(cnt[0]), 0)
# convert image back to original color
if flag:
binary = cv2.bitwise_not(binary)
return binary
but it failed in removing the box arround letter D

Categories

Resources