I've trouble using OpenCV's findContours(...) method to find the contours in a single channel image. The image is actually a numpy array with the shape (128, 128) and elements with real values between [0.0,1.0]. Initially the shape is (1,128,128,1) but I've used np.squeeze(...) to get rid of the first and last dimension. Keeping either of them doesn't solve my problem.
What I've tried:
image = np.squeeze(array) #using np.squeeze(array, [0]) won't help.
image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
contours, hierarchy = cv2.findContours(image, 1, 2)
The above code causes the following exception:
error: (-215) scn == 3 || scn == 4 in function cv::cvtColor
What I've also tried:
If I apply findContours(...) directly, so without using cvtColor(...), I get a different error:
error: (-210) [Start]FindContours supports only CV_8UC1 images when mode != CV_RETR_FLOODFILL otherwise supports CV_32SC1 images only in function cvStartFindContours_Impl
Some sources suggest to use a threshold to get an binary image which is required by findContours(...)[1]
ret, thresh = cv2.threshold(image, 1, 255, 0)
im2, contours, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
This won't help either and I receive the same exception complaining about the CV_8UC1 support.
The image is actually a numpy array with the shape (128, 128) and elements with real values between [0.0,1.0].
The error from cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) was due to the fact that you're trying to convert a single channel image from BGR (3 channels) to grayscale (1 channel). Your image is already grayscale, so this step is unnecessary.
The error from cv2.findContours was due to the wrong data type of the elements in the array. The documentation says the following about the input image:
Source, an 8-bit single-channel image. Non-zero pixels are treated as 1's. Zero pixels remain 0's, so the image is treated as binary . You can use compare, inRange, threshold , adaptiveThreshold, Canny, and others to create a binary image out of a grayscale or color one. If mode equals to RETR_CCOMP or RETR_FLOODFILL, the input can also be a 32-bit integer image of labels (CV_32SC1).
To fix this, you need to scale the values in your image to range [0.0,255.0], and then cast the result to np.uint8:
image_8bit = np.uint8(image * 255)
There are few other issue or quirks about the code in your question.
First of all, in one snippet cv2.findContours returns 2 values (OpenCV 2.x), and in the other it returns 3 values (OpenCV 3.x). Which version are you using?
Your first code sample contains the following:
contours, hierarchy = cv2.findContours(image, 1, 2)
Avoid using magic numbers. The 1 corresponds to cv2.RETR_LIST and the 2 corresponds to cv2.CHAIN_APPROX_SIMPLE. Since the RETR_LIST mode doesn't generate any hierarchy, you can ignored that return value:
contours, _ = cv2.findContours(binarized, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
Another problem is most likely the fact that originally you didn't explicitly binarize the image (e.g. using cv2.threshold). While this won't result in exceptions, the result will probably not make much sense -- findContours divides the pixels into two groups -- zeros, and then everything non-zero. You will most likely want them partitioned differently.
threshold_level = 127 # Set as you need...
_, binarized = cv2.threshold(image_8bit, threshold_level, 255, cv2.THRESH_BINARY)
Sample script (OpenCV 3.x):
import numpy as np
import cv2
# Generate random image matching your description:
# shape is (128,128), values are real numbers in range [0,1]
image = np.random.uniform(0, np.nextafter(1,2), (128,128))
# Scale and convert data type
image_8bit = np.uint8(image * 255)
threshold_level = 127 # Set as you need...
_, binarized = cv2.threshold(image_8bit, threshold_level, 255, cv2.THRESH_BINARY)
_, contours, hierarchy = cv2.findContours(binarized, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
# ... processing the contours, etc.
Try this:
cv2.threshold(image, 1, 255, cv2.THRESH_BINARY,image)
im2, contours, hierarchy = cv2.findContours(image, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
The cv2.findContours function accept thresholded images. The error is given because your input image is a grayscale image.
Related
How can I fix it?
import numpy as np
import cv2 as cv
im = cv.imread('good.jpg')
imgray = cv.cvtColor(im, cv.COLOR_BGR2GRAY)
ret, thresh = cv.threshold(imgray, 127, 255, 0)
im2, contours, hierarchy = cv.findContours(thresh, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE)
Error
ValueError: not enough values to unpack (expected 3, got 2)
The problem is in your last line. To better understand it, here's a quote taken from the documentation:
See, there are three arguments in cv2.findContours() function, first one is source image, second is contour retrieval mode, third is contour approximation method. And it outputs the contours and hierarchy. Contours is a Python list of all the contours in the image. Each individual contour is a Numpy array of (x,y) coordinates of boundary points of the object.
So clearly cv2.findContours() only return two arguments (not 3). To fix it just change the last line to:
contours, hierarchy = cv.findContours(thresh, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE)
I'm trying to create a script to count dots on an image (very original) using Python and OpenCV.
import numpy as np
import cv2
# Image read
img = cv2.imread("img.tif", 0)
# Denoising
denoisedImg = cv2.fastNlMeansDenoising(img);
# Threshold (binary image)
# thresh – threshold value.
# maxval – maximum value to use with the THRESH_BINARY and THRESH_BINARY_INV thresholding types.
# type – thresholding type
th, threshedImg = cv2.threshold(denoisedImg, 100, 255,cv2.THRESH_BINARY_INV|cv2.THRESH_OTSU) # src, thresh, maxval, type
# Perform morphological transformations using an erosion and dilation as basic operations
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (4,8))
morphImg = cv2.morphologyEx(threshedImg, cv2.MORPH_CLOSE, kernel)
# Find and draw contours
contours, hierarchy = cv2.findContours(morphImg, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
contoursImg = cv2.cvtColor(morphImg, cv2.COLOR_GRAY2RGB)
cv2.drawContours(contoursImg, contours, -1, (255,100,0), 3)
cv2.imwrite("image.tif", contoursImg)
print("Dots number: {}".format(len(contours)))
On the left image, the result I achieved using the code above. On the right image, what I want to achieve.
As you can see, I'm looking for a way to split dots which are a little connected into two separate dots and I was wondering if there was a "magical" function allowing me to do that.
Thanks in advance for your help.
I have an small project where I need to calculate the area of hair portions and tell which one covers greater area among two images. I have another code for hair extraction. However it is not also giving result as expected.You may have guessed already from image below. I will work on it later.
I am trying to calculate the area from contours which is giving me error like:
OpenCV(3.4.4) C:\projects\opencv-python\opencv\modules\imgproc\src\contours.cpp:195: error: (-210:Unsupported format or combination of formats) [Start]FindContours supports only CV_8UC1 images when mode != CV_RETR_FLOODFILL otherwise supports CV_32SC1 images only in function 'cvStartFindContours_Impl'
So, why is findContours not supporting my image?
Another approach:
I only need to find the area of hair portion. So, I thought of calculating area covered by all the white pixels and then subtract it from area of whole image too. In this case, I do not know how to calculate area covered by all white pixels. I thought this way because, hair color can vary, but background will always be white.
So, is this technique possible? Or please suggest some solution for above mentioned error?
My image:
My code:
import cv2
import numpy as np
img = cv2.imread("Hair.jpg")
_, contours, _ = cv2.findContours(img, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)
c = max(contours, key = cv2.contourArea)
cv2.drawContours(img, [c], -1, (255,255, 255), -1)
area = cv2.contourArea(c)
print(area)
cv2.imshow("contour", img)
cv2.waitKey(0)
cv2.destroyAllWindows()
Your error already tells what is wrong, specifically this part:
FindContours supports only CV_8UC1 images when mode
This means that it has to be a greyscale image. You pass an image loaded with:
img = cv2.imread("Hair.jpg")
Which by default returns the image in CV_8UC3 or in simple words, BGR colorspace. Even if your image only has black and white. Solution, load as greyscale:
img = cv2.imread("Hair.jpg", cv2.IMREAD_GRAYSCALE)
Also, I notice that this is a .jpg file, which may introduce some artifacts that you may not like/want. To remove them, use threshold:
ret,thresh1 = cv.threshold(img,127,255,cv.THRESH_BINARY)
I hope this helps you, if not, leave a comment
Update:
findContours function takes black as background and white as foreground. In your case is the other way around. But there is an easy way to solve this, just invert the image when it is being passed:
_, contours, _ = cv2.findContours(255-img, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)
255 is the max value the image can have, and this will turn black into white and white into black, giving you the correct contour.
I want to split the characters of an Image using OpenCV in order to train a Tesseract model.
I am using version 3.1.0 (because of a Macports upgrade - meh..), and the documentation (for Python) is still not very clear/well-documented.
Here is what I do:
Binarize the Image in order to be ready to find the contours
Find the contours (which works - at least I get non-zero results)
For each contour:
Create a mask (this probably fails - gets zeros)
Extract the part from the original image using the mask (should work but the mask fails, so this doesn't work yet)
The new version of OpenCV has somewhat different syntax as well, so this makes it even more tricky sometimes.
Here is my code:
def characterSplit(img):
"""
Splits the characters in an image using contours, ready to be labelled and saved for training with Tesseract
"""
# Apply Thresholding to binarize Image
img = cv2.GaussianBlur(img, (3,3), 0)
img = cv2.adaptiveThreshold(img, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 75, 10)
img = cv2.bitwise_not(img)
# Find Contours
contours = cv2.findContours(img, cv2.RETR_EXTERNAL , cv2.CHAIN_APPROX_TC89_KCOS, offset=(0,0))[1]
# Iterate through the contours
for c in xrange(len(contours)):
mask = numpy.zeros(img.size)
cv2.drawContours(mask, contours, c, (0, 255, 0), cv2.FILLED) # Mask is zeros - It might fail here!
# Where the result will be stored
res = numpy.zeros(mask.size)
# Make a Boolean-type numpy array for the Mask
amsk = mask != 0
# I use this to copy a part of the image using the generated mask.
# The result is zeros because the mask is also zeros
numpy.copyto(res, img.flatten(), where = amsk)
## (... Reshape, crop and save the result ...)
As far as I know, the mask should be of the same size as the original image. But should it also have the same shape? For instance, my image is 640x74 but the way I create my mask matrix, my mask is 1x47360. Maybe this is why it fails... (but doesn't throw any errors)
Any help is appreciated!
I ended up doing what Miki proposed in the comments. I used cv::connectedComponents to do the character splitting. Here is the corresponding code, for anyone who is interested:
def characterSplit(img, outputFolder=''):
# Splits the Image (OpenCV Object) into distinct characters and exports it in images withing the specified folder.
# Blurring the image with Gaussian before thresholding is important
img = cv2.GaussianBlur(img, (3,3), 0)
img = cv2.adaptiveThreshold(img, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 75, 10)
img = cv2.bitwise_not(img)
output = cv2.connectedComponentsWithStats(img, 8, cv2.CV_16S)
n_labels = output[0]
labels = output[1]
stats = output[2]
for c in xrange(n_labels):
# Mask is a boolean-type numpy array with True in the corresponding region
mask = labels == c
res = numpy.zeros(img.shape)
numpy.copyto(res, img, where=mask)
# The rectangle that bounds the region is stored in:
# stats[c][0:4] -> [x, y, w, h]
cv2.imwrite("region_{}.jpg".format(c), res)
Hope this helps!
I have been working on a binary image on opencv python. I need to get the largest region. I have used following code, but I am not getting desired output.
edged = cv2.Canny(im_bw, 35, 125)
(cnts, _) = cv2.findContours(edged.copy(), cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
c = max(cnts, key = cv2.contourArea)
You don't need to use the canny output to do this. Just do findContours on im_bw directly and you should get the desired results. If still not what you want, try to use different threshold values (given that your original image isn't BW itself)
(_, im_bw) = threshold(frame, 100, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)
(cnts, _) = cv2.findContours(im_bw.copy(), cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
c = max(cnts, key = cv2.contourArea)
You really didn't explain what are you looking for,"largest region"? The code you posted will give you the largest contour found but you need to understand what is an OpenCV contour here. Now depending of your image you can have a lot of noise and that makes OpenCV gives you not the "region" you are expecting, so you need to reduce the noise. Before apply the Canny or the threshold you can apply BLUR to the image, EROTION and/or DILATION.
The algorithm should be like this:
Get the frame / image
Grayscale it
Apply Blur / Erode / Dilate to reduce noise
Apply Canny or threshold
Find contours
Get the largest
Do what you need
Here you'll find good documentation in Python.
I am using the scikit-image package of python which measures the area of the islands and chooses the largest area as follows -
import skimage
from skimage import measure
labels_mask = measure.label(input_mask)
regions = measure.regionprops(labels_mask)
regions.sort(key=lambda x: x.area, reverse=True)
if len(regions) > 1:
for rg in regions[1:]:
labels_mask[rg.coords[:,0], rg.coords[:,1]] = 0
labels_mask[labels_mask!=0] = 1
mask = labels_mask
Input image -
Output image -