How to overlap the white pixels from binary onto original image? - python

I have an aerial image:
I was able to get a binary image of the riverbed of the river part:
After applying a distance transform and some segmentation techniques I was able to get a binary image of the mean riverline:
My question is: how to overlay the white pixels from the riverline so that they're on "top" of the original image?
Here´s an example:

This is a very simple way to solve your problem. But it works.
import cv2
original = cv2.imread('original.png') # Orignal image
mask = cv2.imread('line.png') # binary mask image
result = original.copy()
for i in range(original.shape[0]):
for j in range(original.shape[1]):
result[i, j] = [255, 255, 255] if mask[i, j][0] == 255 else result[i, j]
cv2.imwrite('result.png', result) # saves modified image to result.png
Result

Let's assume your images are numpy arrays called img and mask. Let's also assume that img has shape (M, N, 3), while mask has shape (M, N). Finally, let's assume that img is off dtype np.uint8 while mask is of type np.bool_. If the last assumption isn't true, start with
mask = mask.astype(bool)
Now you can set your river channel to 255 directly:
img[mask, :] = 255
If img were a single grayscale image without a third dimension, as in your last example, you would just remove the : from the index expression above. In fact, you could write it to work for any number of dimensions with
img[mask, ...] = 255

Related

np.where select pixels that red value bigger than green value in an BGR image

It's my first time using np.where to select pixels from a bgr image, i have no idea how to select pixels that r>g from image by using np.where, I tried to do that by using codes like this:
bgr = cv2.imread('im.jpg')
bgr = np.where(bgr[1]>bgr[2],np.full_like(bgr,[255,255,255]),bgr)
cv2.imshow('result',bgr)
cv2.waitKey(0)
but it seems didn't work. Can anybody help me?
I think that it doesn't work because RGB is the last dimension of your image. Rewrite the slicing in np.where. Something like that:
bgr = cv2.imread('im.jpg')
print(bgr.shape) # (h, w, 3)
bgr = np.where(bgr[..., 1:2] > bgr[..., 2:3], # make sure that tensors are of shape 3
np.full_like(bgr, 255), bgr)
cv2.imshow('result', bgr)
cv2.waitKey(0)
Note that the ellipsis bgr[..., 1:2] means bgr[:, :, 1:2] here.
You seem to want to make all pixels white where red exceeds green and retain the original colours elsewhere. If so, I find it clearer this way:
# Load image
im = cv2.imread('image.png')
# Make simple synonyms for red and green
R = im[...,2]
G = im[...,1]
# Make True/False Boolean of pixels where red exceeds green
MoreRedThanGreen = R > G
# Make the image white wherever it is more red than green
im[MoreRedThanGreen,:] = [255,255,255]
# Save result
cv2.imwrite('result.png', im)
That makes this start image:
into this:
Note: Read the colon (:) in this line as meaning "all the channels":
im[MoreRedThanGreen,:] = [255,255,255]

Why I am not able to extract the object using the masked image?

Here are two images in the drive, one is masked and another one is real image:
https://drive.google.com/drive/folders/1hv3NLQeIHT5Iicgnt74S5qFRG5aJyMLw?usp=sharing
I want to get the masked object out of the real image, not in the white color but in the actual color that it does have.
Here is the code that I hav written:
import cv2
import numpy as np
img = cv2.imread('im010.jpg')
mask = cv2.imread('im010.png')
img_foreground = np.array((mask/255)*img)
cv2.imshow('', img_foreground)
cv2.waitKey()
I have converted all masked elements into one, and multiplied it with the actual image. Since the rest of the pixels that are not masked are black, there it will be zero. If any number multiply with it, it remains zero. Multiplication with one value element ends up the same value of the real image. But I have ploted it, it is showing the same masked img?
Can anyone show me the solution?
Here are 4 different ways to do that in Python/OpenCV/Numpy.
import cv2
import numpy as np
# read input
img = cv2.imread('im010.jpg')
# Method 1 -- Bitwise_And
mask = cv2.imread('im010.png',0)
result1 = cv2.bitwise_and(img, img, mask=mask)
cv2.imwrite('im010_masked1.jpg', result1)
cv2.imshow("result1", result1)
cv2.waitKey(0)
cv2.destroyAllWindows()
# Method 2 -- Python Multiplication
mask = cv2.imread('im010.png')
result2 = (img * mask.astype(np.float64)/255).clip(0,255).astype(np.uint8)
cv2.imwrite('im010_masked2.jpg', result2)
cv2.imshow("result2", result2)
cv2.waitKey(0)
cv2.destroyAllWindows()
# Method 3 -- Numpy Blackening
mask = cv2.imread('im010.png',0)
result3 = img.copy()
result3[mask==0] = (0,0,0)
cv2.imwrite('im010_masked3.jpg', result3)
cv2.imshow("result3", result3)
cv2.waitKey(0)
cv2.destroyAllWindows()
# Method 4 -- Numpy Where
mask = cv2.imread('im010.png')
result4 = np.where(mask==255, img, mask)
cv2.imwrite('im010_masked4.jpg', result4)
cv2.imshow("result4", result4)
cv2.waitKey(0)
cv2.destroyAllWindows()
They all produce the following result:
your code just needs an additional /255.
import cv2
import numpy as np
img = cv2.imread('im010.jpg')
mask = cv2.imread('im010.png')
img_foreground = np.array((mask/255)*(img/255))
cv2.imshow('', img_foreground)
cv2.waitKey()
that's because, when you look at the values, they're floats, right? and when they're floats, they must be in the range of 0.0 to 1.0, because that's what imshow expects.
when you give it values scaled differently, it still maps 1.0 to white. any larger value (like 2, 3...) is also white, even if they would be shown as very dark in a range of 0..255, which goes for uint8 type input.

How to change the colour of pixels in an image depending on their initial luminosity?

The aim is to take a coloured image, and change any pixels within a certain luminosity range to black. For example, if luminosity is the average of a pixel's RGB values, any pixel with a value under 50 is changed to black.
I’ve attempted to begin using PIL and converting to grayscale, but having trouble trying to find a solution that can identify luminosity value and use that info to manipulate a pixel map.
There are many ways to do this, but the simplest and probably fastest is with Numpy, which you should get accustomed to using with image processing in Python:
from PIL import Image
import numpy as np
# Load image and ensure RGB, not palette image
im = Image.open('start.png').convert('RGB')
# Make into Numpy array
na = np.array(im)
# Make all pixels of "na" where the mean of the R,G,B channels is less than 50 into black (0)
na[np.mean(na, axis=-1)<50] = 0
# Convert back to PIL Image to save or display
result = Image.fromarray(na)
result.show()
That turns this:
Into this:
Another slightly different way would be to convert the image to a more conventional greyscale, rather than averaging for the luminosity:
# Load image and ensure RGB
im = Image.open('start.png').convert('RGB')
# Calculate greyscale version
grey = im.convert('L')
# Point process over pixels to make mask of darker ones
mask = grey.point(lambda p: 255 if p<50 else 0)
# Paste black (i.e. 0) into image where mask indicates it is dark
im.paste(0, mask=mask)
Notice that the blue channel is given considerably less significance in the ITU-R 601-2 luma transform that PIL uses (see the lower 114 weighting for Blue versus 299 for Red and 587 for Green) in the formula:
L = R * 299/1000 + G * 587/1000 + B * 114/1000
so the blue shades are considered darker and become black.
Another way would be to make a greyscale and a mask as above. but then choose the darker pixel at each location when comparing the original and the mask:
from PIL import Image, ImageChops
im = Image.open('start.png').convert('RGB')
grey = im.convert('L')
mask = grey.point(lambda p: 0 if p<50 else 255)
res = ImageChops.darker(im, mask.convert('RGB'))
That gives the same result as above.
Another way, pure PIL and probably closest to what you actually asked, would be to derive a luminosity value by averaging the channels:
# Load image and ensure RGB
im = Image.open('start.png').convert('RGB')
# Calculate greyscale version by averaging R,G and B
grey = im.convert('L', matrix=(0.333, 0.333, 0.333, 0))
# Point process over pixels to make mask of darker ones
mask = grey.point(lambda p: 255 if p<50 else 0)
# Paste black (i.e. 0) into image where mask indicates it is dark
im.paste(0, mask=mask)
Another approach could be to split the image into its constituent RGB channels, evaluate a mathematical function over the channels and mask with the result:
from PIL import Image, ImageMath
# Load image and ensure RGB
im = Image.open('start.png').convert('RGB')
# Split into RGB channels
(R, G, B) = im.split()
# Evaluate mathematical function over channels
dark = ImageMath.eval('(((R+G+B)/3) <= 50) * 255', R=R, G=G, B=B)
# Paste black (i.e. 0) into image where mask indicates it is dark
im.paste(0, mask=dark)
I created a function that returns a list with True if the pixel has a luminosity of less than a parameter, and False if it doesn't. It includes an RGB or RGBA option (True or False)
def get_avg_lum(pic,avg=50,RGBA=False):
num=3
numd=4
if RGBA==False:
num=2
numd=3
li=[[[0]for y in range(0,pic.size[1])] for x in range(0,pic.size[0])]
for x in range(0,pic.size[0]):
for y in range(0,pic.size[1]):
if sum(pic.getpixel((x,y))[:num])/numd<avg:
li[x][y]=True
else:
li[x][y]=False
return(li)
a=get_avg_lum(im)
The pixels match in the list, so (0,10) on the image is [0][10] in the list.
Hopefully this helps. My module is for standard PIL objects.

Giving grayscale image color

I want to add color to a black and white image, I assume that changing the values of the pixels should work.
for rows in rgb:
for e in rows:
for i in range(len(e)):
max_val = e.max()
min_val = e.min()
if e[i] == max_val:
e[i] * 2.5
if e[i] == min_val:
e[i] * 0.75
else:
e[i] * 1.5
the code doesnt return an error, but it also doesnt change the values. I want the numbers to be multiplied and reassigned by the same array
Instead of manually iterating through each pixel which has an inefficient O(n^3) run-time, we can take advantage of Numpy's broadcasting feature.
We first split the grayscale image into individual BGR channels using cv2.split(). This will give us separate B, G, and R channels each with the same values. Next we multiply each channel with a scalar value using np.multiply(). Finally we combine each individual channel into a color image using cv2.merge() to create a single multi-channel array
Before
>>> print(before.shape)
(331, 500, 3)
You might be wondering why the image has three channels even though it's obviously grayscale. Well it's because each channel has the same values ranging from [0 ... 255]
After
>>> print(after.shape)
(331, 500, 3)
Again, same number of channels, but we modified each individual channel
TLDR: To add color to a black and white image, we have to extract each individual BGR channel, modify each channel, then reconstruct the image
import cv2
import numpy as np
before = cv2.imread('2.png')
b, g, r = cv2.split(before)
np.multiply(b, 1.5, out=b, casting="unsafe")
np.multiply(g, .75, out=g, casting="unsafe")
np.multiply(r, 1.25, out=r, casting="unsafe")
after = cv2.merge([b, g, r])
cv2.imshow('before', before)
cv2.imshow('after', after)
cv2.waitKey()
Here is one way to apply a gradient color to a grayscale image.
Load the grayscale image
Convert it to 3 equal channels
Create a 1 pixel red image
Create a 1 pixel blue image
Concatenate the two
Resize linearly to 256 pixels as a Lookup Table (LUT)
Apply the LUT
Input:
import cv2
import numpy as np
# load image as grayscale
img = cv2.imread('lena_gray.png', cv2.IMREAD_GRAYSCALE)
# convert to 3 equal channels
img = cv2.merge((img, img, img))
# create 1 pixel red image
red = np.zeros((1, 1, 3), np.uint8)
red[:] = (0,0,255)
# create 1 pixel blue image
blue = np.zeros((1, 1, 3), np.uint8)
blue[:] = (255,0,0)
# append the two images
lut = np.concatenate((red, blue), axis=0)
# resize lut to 256 values
lut = cv2.resize(lut, (1,256), interpolation=cv2.INTER_CUBIC)
# apply lut
result = cv2.LUT(img, lut)
# save result
cv2.imwrite('lena_red_blue_lut_mapped.png', result)
# display result
cv2.imshow('RESULT', result)
cv2.waitKey(0)
cv2.destroyAllWindows()
Concatenate as many different colored pixels as you want to form a rainbow LUT, if desired.

Creating ROI mask by using drawContours in OpenCV

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

Categories

Resources