Can I add a bias to Otsu thresholding in OpenCV? - python

Here's my example. Left to right:
original image
grayscale + (3,3) gaussian blur
Otsu thresholding + invert pixels
I want to capture more of the faint part of the pen stroke. I understand that Otsu Thresholding tries to apply the threshold point in between two peaks of the pixel intensity histogram, but I'd like to bias that a bit so I can capture some of the lighter pixels.
Is it possible out of the box? Or do I need to do something manual?

I have an answer courtesy of the rubber duck phenomenon.
th, th_img = cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY+cv2.THRESH_OTSU)
The 0th index of the returned tuple (th) is the threshold value that the Otsu Binarization algorithm chose. I can discard th_img and apply whatever bias I like to th before using it in regular binary thresholding.
desired_th = th*1.2
_, th_img = cv2.threshold(blur, desired_th, 255, cv2.THRESH_BINARY)
Here's what I get. By cleaning up the unwanted speckles that could appear on the outside, I'll get what I was looking for.

In C++, I often "tune out" the threshold value returned by the (otsu) thresholding function multiplying it by a factor and passing it back to the (fixed) thresholding function:
//get the threshold computed by otsu:
double otsuThresh = cv::threshold( inputImage, otsuBinary, 0, 255,cv::THRESH_OTSU );
//tune the threshold value:
otsuThresh = 0.5 * otsuThresh;
//threshold the input image with the new value:
cv::threshold( inputImage, binaryFixed, otsuThresh, 255, cv::THRESH_BINARY );

An alternative to a biased Otsu threshold is to do region based thresholding, something like this:
thr = .8
blur_hor = cv2.filter2D(img[:, :, 0], cv2.CV_32F, kernel=np.ones((11,1,1), np.float32)/11.0, borderType=cv2.BORDER_CONSTANT)
blur_vert = cv2.filter2D(img[:, :, 0], cv2.CV_32F, kernel=np.ones((1,11,1), np.float32)/11.0, borderType=cv2.BORDER_CONSTANT)
output = ((img[:,:,0]<blur_hor*thr) | (img[:,:,0]<blur_vert*thr)).astype(np.uint8)*255

Related

How to expand a filtering mask to cover more pixels of the area of interest in OpenCV?

Take a look at this image. I want to turn this card blue.
I use Python and OpenCV to perform image processing.
Here's how I do it now:
import cv2
import numpy as np
# Load the image
image = cv2.imread("input.jpg")
# Convert the image to the HSV color space
hsv_image = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
# Threshold the HSV image to get only the red colors
# Bitwise OR unites Hue value 170-179 and 0-10.
mask = cv2.bitwise_or(
cv2.inRange(hsv_image, np.array([0, 120, 100]), np.array([10, 255, 255])),
cv2.inRange(hsv_image, np.array([170, 120, 100]), np.array([180, 255, 255]))
)
# Perform median blurring
mask = cv2.medianBlur(mask, 5)
# Define a kernel for the morphological operations
kernel = np.ones((5, 5), np.uint8)
# Perform an opening operation to remove small objects
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel, iterations=2)
# Perform a closing operation to fill small holes
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel, iterations=2)
# Perform a gradient operation to extract object boundaries
gradient_mask = cv2.morphologyEx(mask, cv2.MORPH_GRADIENT, kernel)
# Modify hue value of every masked pixel
hsv_image[:, :, 0][mask != 0] = (hsv_image[:, :, 0][mask != 0].astype(int, copy=False) + 120) % 180
# Convert the HSV image back to BGR color space
result = cv2.cvtColor(hsv_image, cv2.COLOR_HSV2BGR)
# Display the result
cv2.namedWindow("output", cv2.WINDOW_NORMAL)
cv2.imshow("output", result)
# Save images of the mask, result and gradient
cv2.imwrite('mask.jpg', mask)
cv2.imwrite('result.jpg', result)
cv2.imwrite('gradient.jpg', gradient_mask)
# Wait for the window to close
while cv2.getWindowProperty('output', 0) >= 0:
cv2.waitKey(50)
cv2.destroyAllWindows()
It works well. The result, The filtering mask
But if you take a closer look you'll see the problem: link, link
Some red pixels is still here, and here's why. They do not fall in the filtered range of the red color: 170 <= Hue <= 10, Saturation >= 120, Value >= 100. Their HSV color is near to (178, 32, 60). So, the saturation and value fall out of the filter range.
Why I can't lower the range of saturation and value? That's because in this case there would be too much noise on another background that has more colors. The noise in this case is hard to avoid even using multiple iterations of opening morphological operation.
I don't have much experience in image processing and using OpenCV, so my ideas may be far from the best solution. It's okay if you propose another approach.
Possible solution. Would it be possible to perform the dilate morphological operation on the filtering mask (expand the filtering mask) but only to those pixels that fall in another, broader range of red (with saturation and value range equal to 10, hue range stays the same). So that all the red pixels that fall in the broader range of the red color AND that are adjacent to the pixels of the existing mask (so no pixels from the background is added creating noise).
If that is a good idea, how can I implement it, especially the part of dilating only to pixels that fall in the broader range of the red color? Maybe it's already implemented in OpenCV and I just don't know about that?
Also I would be glad to hear any suggestions or recommendations. I am a student, I want to learn more.
Thanks in advance!
I found myself how to do it, but it's still not the best mask.
I create a broader mask as follows:
broader_mask = cv2.bitwise_or(
cv2.inRange(hsv_image, np.array([0, 30, 30]), np.array([20, 255, 255])),
cv2.inRange(hsv_image, np.array([160, 30, 30]), np.array([180, 255, 255]))
)
And apply bitwise AND on the dilated primary mask and the broader mask to get the resulting mask:
mask = cv2.bitwise_and(cv2.dilate(mask, kernel, iterations=2), broader_mask)
It works much better, but now the mask might be bigger than I want in some cases. For example:
It all depends on the kernel and the number of iterations of the dilation operation. I don't think it's the best solution because the kernel and the number of iterations might be different for different image sizes, and I'm still looking for a better solution.

Remove black header section of image using Python OpenCV

I need to remove the blackened section in multiple parts of image using Python CV.
I tried with denoising which doesn't give satisfactory results.
Eg. I need to remove the blackened part in Table Header (below image) and convert the header background to white with contents as black.
Can anyone help me with choosing the correct library or solution to overcome this?
It is difficult to filter the dotted pattern, as you can see. It is clearly overlapping the text. I see at least two options: 1) Exploit the periodic nature of the pattern and carry out frequency filtering. 2) Try a simpler approach using a morphological hit or miss operation on the target pixels, aiming to isolate them.
Let's check out option 2. The noise has a very distinctive pattern. If you work with the binary image where all the blobs are colored in white, the pattern you are looking for is a white pixel (1) surrounded by 8 black pixels (0):
[ 0, 0, 0 ]
[ 0, 1, 0 ]
[ 0, 0, 0 ]
The hit and miss operation can be used to locate and isolate pixel patterns. Here's a good post if you want to learn more about it. For now, let’s work on the code:
//Read the input image, as normal:
std::string imagePath = "C://opencvImages//tableTest.png";
cv::Mat testImage = cv::readImage( imagePath );
//Convert the image to grayscale:
cv::Mat grayImage;
cv::cvtColor( testImage, grayImage, cv::COLOR_BGR2GRAY );
//Get the binary image via otsu:
cv::Mat binaryImage;
cv::threshold( grayImage, binaryImage, 0, 255,cv::THRESH_OTSU );
//Invert the image, as we will be working on white blobs:
binaryImage = 255 - binaryImage;
//Prepare the target kernel. This is where you define the pattern of
//pixels you are looking for
//Keep in mind that -1 -> black and 1 -> white
cv::Mat kernel = ( cv::Mat_<int>(3, 3) <<
-1, -1, -1,
-1, 1, -1,
-1, -1, -1
);
//perform the hit or miss operation:
cv::Mat hitMissMask;
cv::morphologyEx( binaryImage, hitMissMask, cv::MORPH_HITMISS, kernel );
This is the mask you get:
Now, just subtract this mask to the original (binary) image and you get this:
As you can see, part of the column header gets in the way of the operation. If you want a white background and black blobs, just invert the image:
Here's a modified version of #eldesgraciado's approach to filter the dotted pattern using a morphological hit or miss operation on the target pixels in Python. The difference is that instead of subtracting the mask with the binary image which decreases text quality, we dilate the binary image then bitwise-and to retain the text quality.
Obtain binary image. Load image, grayscale, Otsu's threshold
Perform morphological hit or miss operation. We create a dot pattern kernel with cv2.getStructuringElement then use cv2.filter2D to convolve the image
Remove dots. We cv2.bitwise-xor the mask with the binary image
Fix damaged text pixels. We cv2.dilate then cv2.bitwise_and the finalized mask with the input image and color background pixels white
Binary image
Dot mask
Remove dots
Dilate to fix damaged text pixels from the thresholding process
Result
Code
import cv2
import numpy as np
# Load image, grayscale, Otsu's threshold
image = cv2.imread('1.jpg')
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)[1]
# Perform morphological hit or miss operation
kernel = np.array([[-1,-1,-1], [-1,1,-1], [-1,-1,-1]])
dot_mask = cv2.filter2D(thresh, -1, kernel)
# Bitwise-xor mask with binary image to remove dots
result = cv2.bitwise_xor(thresh, dot_mask)
# Dilate to fix damaged text pixels
# since the text quality has decreased from thresholding
# then bitwise-and with input image
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (2,2))
dilate = cv2.dilate(result, kernel, iterations=1)
result = cv2.bitwise_and(image, image, mask=dilate)
result[dilate==0] = [255,255,255]
cv2.imshow('dot_mask', dot_mask)
cv2.imshow('thresh', thresh)
cv2.imshow('result', result)
cv2.imshow('dilate', dilate)
cv2.waitKey()

How to detect defect bean using python3

normal bean sample
defect bean sample-1
I want to judge whether the bean is normal or defect.
I tried to use Canny method(to find edge) and so on... but I failed.
I just solve it using shape.(cracked bean & unshaped bean ...)
Please, give me some idea to solve it.
Sorry for my English, it's not my first language.
One of the most widely used methods to "fill cracks" in an image is dilate-erode. Simply put, you make your binary image "grow" at the edges, so the cracks get filled, then you reverse the process and make it "shrink" at the edges - but, as the cracks have been filled, there is no information about them left in the image, so they stay filled. Perhaps, you can use that and then look at the difference between your original image and the image after dilate-erode: if there were little to no cracks, there would be little to no difference, and if there were plenty of cracks, there would be plenty of difference.
For example. Let's convert our image to a binary black and white mask:
def get_th_binary_mask(img):
gs = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ret, thresh = cv2.threshold(gs, 0, 255, cv2.THRESH_BINARY)
mask = np.zeros(thresh.shape, np.uint8)
mask[thresh == 255] = 0
mask[thresh == 0] = 1
return mask
Now, just sum the elements of the matrix, which gives us the number of white pixels, dilate-erode, sum again, subtract the sums:
def get_de_difference(binary_image):
s_before = np.sum(binary_image)
kernel = np.ones((17, 17), np.uint8)
d = cv2.dilate(binary_image, kernel, 1)
d = cv2.erode(d, kernel, 1)
s_after = np.sum(d)
return abs(s_after - s_before)
For the "good" bean it gives 72 pixels different, for the "bad" one it gives 1158.
Can be further improved by using a more sophisticated thresholding function, for example, based on Otsu and Grab cut:
def get_gc_binary_mask(img):
gs = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ret, thresh = cv2.threshold(gs, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
mask = np.zeros(thresh.shape, np.uint8)
mask[thresh == 255] = cv2.GC_PR_BGD
mask[thresh == 0] = cv2.GC_FGD
bgdModel = np.zeros((1,65),np.float64)
fgdModel = np.zeros((1,65),np.float64)
cv2.grabCut(img, mask, (0, 0, 1365, 767), bgdModel, fgdModel, 5, cv2.GC_INIT_WITH_MASK)
mask2 = np.where((mask==2)|(mask==0),0,1).astype('uint8')
return mask2
Using it instead of the previous one gives only 1 pixel difference for the "good" bean (despite an unfortunate artifact - see below), while for the "bad" bean it gives 741. Or, if you can change the background of your pictures, just put some bright green/blue sheet there before taking your pictures and use chroma keying.
This is how it looks, left to right: original images (column 1), basic threshold, dilate, erode, otsu/grabcut threshold, dilate, erode. The difference between columns 2 and 4 and between columns 5 and 7 is what matters.

How to take a threshold of only a part of the image with OTSU thresholding?

I'm using OTSU threshold on a dilated and eroded image as shown below:
k = np.ones((5,5),np.float32)/1
d = cv2.dilate(self.img, k, iterations=10)
e = cv2.erode(d, k, iterations=10)
self.thresh = cv2.threshold(e, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)[1]
This is the eroded and dilated image, and the one that gets thresholded:
I only want the circular bright region in the middle to be obtained from thresholding, but instead I'm getting this result:
How do I go about thresholding such that I only get the circular region in the middle, which also seems like the brightest (visually) part of the image?
Note: To avoid playing around with different values, I want to stick to OTSU thresholding, but I'm open to ideas.
You can use Dilate and Erode filters to this image, but in another order: Erode first and then Dialte. It will suppress bright areas from upper side of the image and threshold method will provide better result
You can try a gradient based approach. Below I've used the morphological gradient. I apply Otsu thresholding to this gradient image, followed by a similar amount of morphological closing (10 iterations), then take the morphological gradient of the resulting image.
Now the regions are easy to detect. You can filter the circular region from the contours, for example, using an area based approach: using bounding box dimensions of the contour, you can get an estimate of the radius, then compare the calculated area to the contour area.
Don't know how generic this approach would be for your collection.
Gradient image: intensity values scaled for visualization
Binarized gradient image
Closed image
Gradient
im = cv2.imread('LDxOj.jpg', 0)
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3))
morph = cv2.morphologyEx(im, cv2.MORPH_GRADIENT, kernel)
_, bw = cv2.threshold(morph, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)
morph2 = cv2.morphologyEx(bw, cv2.MORPH_CLOSE, kernel, anchor = (-1, -1), iterations = 10)
morph3 = cv2.morphologyEx(morph2, cv2.MORPH_GRADIENT, kernel)

Adaptive thresholding on image causing data loss

I am determining adaptive threshold using OTSU and then using determined threshold to convert image to black & white. On this processed image I want to carry out further steps of determining density of each circle, but my black & white image is over corrected and resulting in data loss. Any suggestions on how to tweak adaptive threshold.
im_gray = cv2.imread(img, cv2.CV_LOAD_IMAGE_GRAYSCALE)
(thresh, im_bw) = cv2.threshold(im_gray, 128, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)
img_bw = cv2.threshold(im_gray, thresh, 255, cv2.THRESH_BINARY)[1]
Try adaptiveThreshold() from openCV, It will calculate the threshold based on the intensities in the window. It seems that the OTSU method doesn’t work as expected in your case since adaptiveThreshold uses just an average (minus a constant) and works better (see image below) than OTSU that uses a more optimal criterion.
It is also not clear what is a spatial extent of the OTSU. If it is a whole image, then it should fail since the right side of the image is more blurred than the left and thus dark is washed out on the right. The adaptive threshold makes calculations in a window so it is locally adaptive. Note that the last two parameters in the function below are the size of the window and the value to subtract from the average when forming a threshold.
adaptiveThreshold(I, dst, 255, ADAPTIVE_THRESH_GAUSSIAN_C, THRESH_BINARY, 21, 15);
You may get better results when putting together OTSU and locally adaptive properties. However, the white color is typically oversampled and this causes a bias in the estimate. It is better to sample on the both sides of the gradient to get equal samples of white and dark. It is even better to take into account connectivity and color when thresholding, see grab cut;
Finally, the loss of information is always inevitable during thresholding.

Categories