Creating ROI mask by using drawContours in OpenCV - python

I am using drawContours to make a mask for extracting ROI.
I have already defined four points and a zero mask for drawing the contour.
The output mask of the drawContours function is sort of a trapezoid shape which is what I want.
However, when I use this mask to do bitwise_and with the image,
the result isn't really the same shape with the mask.
The edge of the shape is obviously jagged.
Here is my python code snippet:
hull2 = cv2.convexHull(crop1)
mask10 = np.zeros(image.shape[:2], dtype = "uint8")
print(len(hull2))
cv2.drawContours(mask10, [hull2], -1,255, -1,cv2.LINE_AA)
cv2.imshow("mask10",mask10)
cv2.waitKey(0)
crop = cv2.bitwise_and(image, image, mask=mask10)
cv2.imshow("crop",crop)
cv2.waitKey(0)
cv2.drawContours(image, [hull2], -1, (0, 255, 0), -1,cv2.LINE_AA)
cv2.imshow("mask+img",image)
cv2.waitKey(0)
And here is a picture showing the result: "crop" is the ROI result image
Thanks for anyone trying to help.

The reason you are getting jagged edges while your mask looks like it has smooth edges is because you are using the anti-aliasing flag on drawContours( - ,cv2.LINE_AA) which fills in the surrounding of the jagged edges with darker pixels creating a gradient that fools your eye into thinking its a smooth edge.
Why does this matter? when you use bitwise_and with a mask, any value in the mask greater than 0 is evaluated as "True" and the corresponding pixel in the image will be selected.
So those extra AA pixels despite being a smaller gray value than 255, are expanding the edge of the mask, creating your jagged edge in crop. To emulate this, do mask10[mask10 > 0] = 255; cv2.imshow('mask10', mask10) and it should have the same shape as crop.
Now as a possible solution to your problem, you could use alpha blending to use the gradient (darkened intensity) of those extra AA pixels to darken the crop image edge pixels.
mask_float = cv2.cvtColor(mask10, cv2.COLOR_GRAY2BGR).astype('float32') / 255
image_float = image.astype('float32')
crop = cv2.multiply(image_float, mask_float).astype('uint8')
First we convert mask10 to a 3 channel array so that we can apply the alpha blending to all 3 BGR channels of the image.
Then we normalize the mask to a [0-1] range as we will need to multiply the values in the next step and dtype uint8 doesnt allow greater than 255. So first converting to float32 then dividing by 255. (we could potentially use cv2.normalize() but numpy should be alot faster)
we then convert the image to float32 to allow for multiplication with the mask.
then we multiply the image with the mask to get an alpha blended image of foreground to a black background and convert it back to uint8 for opencv.
Now since the BGR values are converted from float32 to uint8, it will discard the decimal values which will cause a negligible change in color. Also, I'm not 100% sure but there might be a small change in color due to multiplying each channel individually by the same value (eg: 20%) or it could be fine and im just overthinking it? But that only applies to those darkened AA pixels, the effect should also be negligible and we are already modifying it from the original anyways so it should be fine!
As an alternative, you could also convert the image to HLS and multiply the mask to the L-channel only. I believe that should be more true to the image's colors on those edges if that is very important, and the slower speed is permissible

Related

Obtain the subtracted area of 2 images via Python OpenCV

I can subtract 2 images by the following codes:
import cv2
original = cv2.imread('image1.png', cv2.IMREAD_UNCHANGED)
tiled = cv2.imread('image2.png', cv2.IMREAD_UNCHANGED)
subtract = cv2.subtract(tiled, original)
cv2.imwrite('subtract.png', subtract)
However, how can I obtain the area (maybe in form of array of pixels, or shapes) that results in black (i.e. after subtraction, the pixel is black color)?
I can only think of looping through each pixel of an image to check whether the pixel value equals to an array of zeros.
Ultimately, I want to change those pixels in black color after subtraction to be transparent.

Merging each instance mask back to the original image Python

I am having a bunch of mask (object is white, non-object is black) bounded by their bounding box as a separate image, and I'm trying to put them back to their original positions on the original image. What I have in mind right now is:
Create a black image of the same size of the original image.
Add the value of each mask with the value of the coordinate of the bounding box on the original image together.
Could anyone tell me if I am heading in the right path, is there any better way to do this?.
Below is roughly my implementation
import cv2
black_img = np.zeros((height,width)) # A image that is of the size of the original but is all black
mask = cv2.imread("mask.png")
bbox = [x1, y1, x2, y2] # Pretend that this is a valid bounding box coordinate on the original image
black_img[y1:y2, x1:x2] += mask
For example:
I have the first image which is one of my masks. Its size is of the same of the bounding box on the original image. I'm trying merge each mask back together so that I achieved something like the second image.
One of the mask:
After merging all the mask:
I am assuming the mask is 0 and 1's and your image is grayscale. Also, for each small_mask, you have a corresponding bbox.
mask = np.zeros((height,width))
for small_mask, bbox in zip(masks, bboxes):
x1, y1, x2, y2 = bbox
mask[y1:y2, x1:x2] += small_mask
mask = ((mask>=1)*255.0).astype(np.uint8)
Now you combined all the small masks together.
The last line:
My assumption was somehow two masks may intersect. So those intersection may have values more than 1. mask >= 1 tells me that the pixels that are more than 0 are gonna be all on.
I multiplied that by 255.0 because I wanted to make it white. You won't be able to see 1's in a grayscale image.
(mask >= 1)*255.0 expanded the range from [0-1] to [0-255]. But this value is float which is not image type.
.astype(np.uint8) converts the float to uint8. Now you can do all the image operations without any problem. When it is float, you may face a lot of issues, like plotting, saving, all will cause some issues.

Get pixel location of binary image with intensity 255 in python opencv

I want to get the pixel coordinates of the blue dots in an image.
To get it, I first converted it to gray scale and use threshold function.
import numpy as np
import cv2
img = cv2.imread("dot.jpg")
img_g = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ret1,th1 = cv2.threshold(img_g,127,255,cv2.THRESH_BINARY_INV)
What to do next if I want to get the pixel location with intensity 255? Please tell if there is some simpler method to do the same.
I don't think this is going to work as you would expect.
Usually, in order to get a stable tracking over a shape with a specific color, you do that in RGB/HSV/HSL plane, you could start with HSV which is more robust in terms of lighting.
1-Convert to HSV using cv2.cvtColor()
2-Use cv2.inRagne(blue_lower, blue_upper) to "filter" all un-wanted colors.
Now you have a good-looking binary image with only blue color in it (assuming you have a static background or more filters should be added).
3-Now if you want to detect dots (which is usually more than one pixel) you could try cv2.findContours
4- You can get x,y pixel of contours using many methods(depends on the shape of what you want to detect) like this cv2.boundingRect()

Python and opencv: how do I convert the ALL of the background of this image to one colour or transparent

I created the following image (image 3) using a threshold mask (image 2) on image 1. I am trying to convert all the pixels outside of the central image of image 3 (of lungs) to one colour (for example black) using opencv. Basically so that I am left with just the image of the lungs against a uniform background (or even transparent). My problem has been the similarity of the very outer pixels to those inside the lungs on image 3. Is this possible to do using opencv?
Simply floodFill() the mask from the boundaries of the image with black. See the flood fill step in my answer here to see it used in another scenario.
Similarly, you can use floodFill() to find which pixels connect to the edges of the image, which means you can use it to put back the holes in the lungs from thresholding. See my answer here for a different example of this hole-filling process.
I copy and pasted the code straight from the above answers, only modifying the variable names:
import cv2
import numpy as np
img = cv2.imread('img.jpg', 0)
mask = cv2.imread('mask.png', 0)
# flood fill to remove mask at borders of the image
h, w = img.shape[:2]
for row in range(h):
if mask[row, 0] == 255:
cv2.floodFill(mask, None, (0, row), 0)
if mask[row, w-1] == 255:
cv2.floodFill(mask, None, (w-1, row), 0)
for col in range(w):
if mask[0, col] == 255:
cv2.floodFill(mask, None, (col, 0), 0)
if mask[h-1, col] == 255:
cv2.floodFill(mask, None, (col, h-1), 0)
# flood fill background to find inner holes
holes = mask.copy()
cv2.floodFill(holes, None, (0, 0), 255)
# invert holes mask, bitwise or with mask to fill in holes
holes = cv2.bitwise_not(holes)
mask = cv2.bitwise_or(mask, holes)
# display masked image
masked_img = cv2.bitwise_and(img, img, mask=mask)
masked_img_with_alpha = cv2.merge([img, img, img, mask])
cv2.imwrite('masked.png', masked_img)
cv2.imwrite('masked_transparent.png', masked_img_with_alpha)
Edit: As an aside, "transparency" is basically a mask: the values tell you how opaque each pixel is. If the pixel is 0, its totally transparent, if it's 255 (for uint8) then it's completely opaque, if it's in-between then it's partially transparent. So the exact same mask used here at the end could be stacked onto the image to create the fourth alpha channel (you can use cv2.merge or numpy to stack) where it will make every 0 pixel in the mask totally transparent; simply save the image as a png for the transparency. The above code creates an image with alpha transparency as well as an image with a black background.
Here the background looks white because it is transparent, but if you save the image to your system you'll see it actually is transparent. FYI OpenCV actually ignores the alpha channel during imshow() so you'll only see the transparency on saving the image.
Edit: One last note...here your thresholding has removed some bits of the lungs. I've added back in the holes from thresholding that occur inside the lungs but this misses some chunks along the boundary that were removed. If you do contour detection on the mask, you can actually smooth those out a bit as well if it's important. Check out the "Contour Approximation" section on OpenCV's contour features tutorial. Basically it will try to smooth the contour but stick within some certain epsilon distance from the actual contour. This might be useful and is easy to implement, so I figured I'd throw it as a suggestion at the end here.

Increase PNG masked area on a JPG with Python Pillow (or PIL)

I'm trying to mask a jpg image using a png black/transparent mask, but due to aliasing and border blur, i always have in output a contour line of the original jpg.
Since graphical precision is not required by the task, this could be easily solved by increasing the masked area by a few pixels.
So for example if the masked area allows a centered circle of 100px, simply "extending" the circle by some pixel, would solve the problem.
Is there a way to achieve this with Pillow ?
I found a solution; i write it down so that others may benefit if needed:
1) apply a gaussian blur to the mask. this will "expand" the borders with a shade
1b) convert in black/white colors only if needed
2) apply a transformation that converts each pixel in black or white based on a threshold. no other colors allowed
so something similar:
blackThreshold = 128.0
img = img.filter(ImageFilter.GaussianBlur(radius=3))
r,g,b,a = img.split() # supposing to have a RGBA PNG
gray = Image.merge('L',(a,)) #
gray = gray.point(lambda x: 0 if x<blackThreshold else 255)

Categories

Resources