Broadcasting in numpy with multiple dimensions - python

I have an image with a black background that contains different shapes in different colors. I want to generate an image per shape, in which the shape is white and the background is black. I have been able to do this with numpy, but I would like to optimize my code using vectorization. This is what I have so far:
import numpy as np
import cv2
image = cv2.imread('mask.png')
image.shape
# (720, 1280, 3)
# Get all colors that are not black
colors = np.unique(image.reshape(-1,3), axis=0)
colors = np.delete(colors, [0,0,0], axis=0)
colors.shape
# (5, 3)
# Example for one color. I could do a for-loop, but I want to vectorize instead
c = colors[0]
query = (image == c).all(axis=2)
# Make the image all black, except for the pixels that match the shape
image[query] = [255,255,255]
image[np.logical_not(query)] = [0,0,0]

Approach #1
You can save a lot on intermediate array data with extension of unique colors into higher dim and then comparing against original data array and then using the mask directly to get the final output -
# Get unique colors (remove black)
colors = np.unique(image.reshape(-1,3), axis=0)
colors = np.delete(colors, [0,0,0], axis=0)
mask = (colors[:,None,None,:]==image).all(-1)
out = mask[...,None]*np.array([255,255,255])
Approach #2
A better/memory-efficient way to get that mask would be with something like this -
u,ids = np.unique(image.reshape(-1,3), axis=0, return_inverse=1)
m,n = image.shape[:-1]
ids = ids.reshape(m,n)-1
mask = np.zeros((ids.max()+1,m,n),dtype=bool)
mask[ids,np.arange(m)[:,None],np.arange(n)] = ids>=0
and hence, a better way to get the final output, like so -
out = np.zeros(mask.shape + (3,), dtype=np.uint8)
out[mask] = [255,255,255]
and probably a better way to get ids would be with matrix-multiplication. Hence :
u,ids = np.unique(image.reshape(-1,3), axis=0, return_inverse=1)
could be replaced by :
image2D = np.tensordot(image,256**np.arange(3),axes=(-1,-1))
ids = np.unique(image2D,return_inverse=1)[1]

I was able to solve it the following way:
import numpy as np
import cv2
# Read the image
image = cv2.imread('0-mask.png')
# Get unique colors (remove black)
colors = np.unique(image.reshape(-1,3), axis=0)
colors = np.delete(colors, [0,0,0], axis=0)
# Get number of unique colors
instances = colors.shape[0]
# Reshape colors and image for broadcasting
colors = colors.reshape(instances,1,1,3)
image = image[np.newaxis]
# Generate multiple images, one per instance
mask = np.ones((instances, 1, 1, 1))
images = (image * mask)
# Run query with the original image
query = (image == colors).all(axis=3)
# For every image, color the shape white, everything else black
images[query] = [255,255,255]
images[np.logical_not(query)] = [0,0,0]

Related

How to remove CT bed/shadows in a CT image with python?

I am working with 3D CT images and trying to remove the lines from the bed.
A slice from the original Image:
Following is my code to generate the mask:
segmentation = morphology.dilation(image_norm, np.ones((1, 1, 1)))
labels, label_nb = ndimage.label(segmentation)
label_count = np.bincount(labels.ravel().astype(int))
label_count[0] = 0
mask = labels == label_count.argmax()
mask = morphology.dilation(mask, np.ones((40, 40, 40)))
mask = ndimage.morphology.binary_fill_holes(mask)
mask = morphology.dilation(mask, np.ones((1, 1, 1)))
This results in the following image:
As you can see, in the above image the CT scan as distorted as well.
If I change: mask = morphology.dilation(mask, np.ones((40, 40, 40))) to mask = morphology.dilation(mask, np.ones((100, 100, 100))), the resulting image is as follows:
How can I remove only the two lines under the image without changing the image area? Any help is appreciated.
You've probably found another solution by now. Regardless, I've seen similar CT processing questions on SO, and figured it would be helpful to demonstrate a Scikit-Image solution. Here's the end result.
Here's the code to produce the above images.
from skimage import io, filters, color, morphology
import matplotlib.pyplot as plt
import numpy as np
image = color.rgba2rgb(
io.imread("ctimage.png")[9:-23,32:-9]
)
gray = color.rgb2gray(image)
tgray = gray > filters.threshold_otsu(gray)
keep_mask = morphology.remove_small_objects(tgray,min_size=463)
keep_mask = morphology.remove_small_holes(keep_mask)
maskedimg = np.einsum('ijk,ij->ijk',image,keep_mask)
fig,axes = plt.subplots(ncols=3)
image_list = [image,keep_mask,maskedimg]
title_list = ["Original","Mask","Imgage w/mask"]
for i,ax in enumerate(axes):
ax.imshow(image_list[i])
ax.set_title(title_list[i])
ax.axis("off")
fig.tight_layout()
Notes on code
image = color.rgba2rgb(
io.imread("ctimage.png")[9:-23,32:-9]
)
gray = color.rgb2gray(image)
The image saved as RGBA when I loaded it from SO. It needs to be in grayscale for use in the threshold function.
Your image might already by in grayscale.
Also, the downloaded image showed axis markings. That's why I've trimmed the image.
maskedimg = np.einsum('ijk,ij->ijk',image,keep_mask)
I wanted to apply keep_mask to every channel of the RGB image. The mask is a 2D array, and the image is a 3D array. I referenced this previous question in order to apply the mask to the image.

How to slice and complie an image into a window effect using Python

I would like to slice up an image in python
and paste it back together again as a window.
The tiles measure as 8pixels by 9pixels and each row needs to skip 1 pixel
I would then need to merge the tiles back together again with a 1 pixel padding around each tile to give a windowed effect.
The image is black and white but for the example I have used color to show that the windowed effect would need to have a white background
input example
Desired Output
Update: change tiles dimension to bigger for illustration, you can adjust per your need
Use this:
import cv2
image = cv2.imread('test.jpg')
tiles_height = 50
tiles_width = 30
# white padding
padding_x = 10
padding_y = 20
num_y = int(image.shape[0]/tiles_height)
num_x = int(image.shape[1]/tiles_width)
new_img = np.full((image.shape[0] + num_y*padding_y, image.shape[1] + num_x*padding_x,3),255)
for incre_i,i in enumerate(range(0,image.shape[0],tiles_height)):
for incre_j,j in enumerate(range(0, image.shape[1], tiles_width)):
new_img[i+incre_i*padding_y:i+tiles_height+incre_i*padding_y
,j+incre_j*padding_x:j+tiles_width+incre_j*padding_x,:] = image[i:i+tiles_height,j:j+tiles_width,:]
cv2.imwrite('res.jpg',new_img)
print(image.shape, new_img.shape)
Update 1:
Because you want to latter remove tiles, I added code that can help you with that. Now all you have to do is changing variables in tiles config, white padding, tile index to be removed:
import cv2
image = cv2.imread('test.jpg')
# tiles config
tiles_height = 50
tiles_width = 30
# white padding
padding_x = 10
padding_y = 20
# tile index to be removed
remove_indices = [(0,0),(3,6)]
num_y = int(image.shape[0]/tiles_height)
num_x = int(image.shape[1]/tiles_width)
new_img = np.full((image.shape[0] + num_y*padding_y, image.shape[1] + num_x*padding_x,3),255)
for incre_i,i in enumerate(range(0,image.shape[0],tiles_height)):
for incre_j,j in enumerate(range(0, image.shape[1], tiles_width)):
if (incre_i,incre_j) in remove_indices:
new_img[i+incre_i*padding_y:i+tiles_height+incre_i*padding_y
,j+incre_j*padding_x:j+tiles_width+incre_j*padding_x,:] = 255
else:
new_img[i+incre_i*padding_y:i+tiles_height+incre_i*padding_y
,j+incre_j*padding_x:j+tiles_width+incre_j*padding_x,:] = image[i:i+tiles_height,j:j+tiles_width,:]
cv2.imwrite('remove_tiles.jpg',new_img)
print(image.shape, new_img.shape)
test.jpg
res.jpg
remove_tiles.jpg
print(image.shape, new_img.shape) gives (952, 1429, 3) (1332, 1899, 3)
You can try with skimage.utils.view_as_windows from the scikit-image package:
from skimage.util import view_as_windows
import matplotlib.pyplot as plt
import numpy as np
img = np.random.rand(90, 90, 1) # gray-scale image, you can change the channels accordingly
img[8::9,] = 0
tiles = view_as_windows(img, (9, 9, 1), (9, 9, 1)).squeeze(2) # squeeze out unneded dim
tiles = tiles[:, :, :-1, :, :] # Remove last row of each tile
# plot the original image
plt.axis("off")
plt.imshow(img.squeeze(2))
plt.show()
# plot the tiles
fig, axes = plt.subplots(10, 10)
for i in range(10):
for j in range(10):
axes[i, j].axis("off")
axes[i, j].imshow(tiles[i, j, ...].squeeze(-1))
plt.show()
Here is the result:
Original
Sliced
The torch.Tensor.unfold operator from PyTorch could be an option too.

Unique Color Detection and Storing images dynamically

If an image is given , find out the unique colors in that image and write output images corresponding to each unique color.
In that all other pixels which don't have that unique color should me marked white.
for eg , if an image has 3 colors - in the output folder there should be three images where each color is separated. Using Open CV & Python.
I've created the unique color list using my methods. What I want is to give a count of all those unique colors in the sample.png image and give the corresponding images output as per the question.
I believe the code below (with comments) should help you with this!
Feel free to follow up if any of the code is unclear!
import numpy as np
import cv2 as cv
import matplotlib.pyplot as plt
from copy import deepcopy
# Load image and convert it from BGR (opencv default) to RGB
fpath = "dog.png" # TODO: replace with your path
IMG = cv.cvtColor(cv.imread(fpath), cv.COLOR_BGR2RGB)
# Get dimensions and reshape into (H * W, C) vector - i.e. a long vector, where each element is a tuple corresponding to a color!
H, W, C = IMG.shape
IMG_FLATTENED = np.vstack([IMG[:, w, :] for w in range(W)])
# Get unique colors using np.unique function, and their counts
colors, counts = np.unique(IMG_FLATTENED, axis=0, return_counts = True)
# Jointly loop through colors and counts
for color, count in zip(colors, counts):
print("COLOR: {}, COUNT: {}".format(color, count))
# Create placeholder image and mark all pixels as white
SINGLE_COLOR = (255 * np.ones(IMG.shape)).astype(np.uint8) # Make sure casted to uint8
# Compute binary mask of pixel locations where color is, and set color in new image
color_idx = np.all(IMG[..., :] == color, axis=-1)
SINGLE_COLOR[color_idx, :] = color
# Write file to output with color and counts specified
cv.imwrite("color={}_count={}.png".format(color, count), SINGLE_COLOR)
Ack, he beat me to it. Well, here's what I've got.
Oh no, I don't think the line
blank[img == color] = img[img == color]
behaves how I think it does. I think it just coincidentally works for this case. I'll edit the code with a solution I'm more confident works for all cases.
Original Image
import cv2
import numpy as np
# load image
img = cv2.imread("circles.png");
# get uniques
unique_colors, counts = np.unique(img.reshape(-1, img.shape[-1]), axis=0, return_counts=True);
# split off each color
splits = [];
for a in range(len(unique_colors)):
# get the color
color = unique_colors[a];
blank = np.zeros_like(img);
mask = cv2.inRange(img, color, color); # edited line 1
blank[mask == 255] = img[mask == color]; # edited line 2
# show
cv2.imshow("Blank", blank);
cv2.waitKey(0);
# save each color with its count
file_str = "";
for b in range(3):
file_str += str(color[b]) + "_";
file_str += str(counts[a]) + ".png";
cv2.imwrite(file_str, blank);

How to change cmap based on array value in python

Current code:
# Mask array with mask pixels set to 1 and rest set to 0
mask = nib.load(os.path.join(mask_path, f))
# Grey scale MRI image slice
image = nib.load(os.path.join(images_path, f))
# Extracting pixel arrays
mask_data = mask.get_fdata()
image_data = image.get_fdata()
# Setting image pixels to 0 where mask pixels are set to 1
masked_image = np.where(mask_data == 0, image_data,0)
# Transposing and flipping to fix visual orientation
masked_image = masked_image.transpose((1,0))
masked_image = np.flip(masked_image,axis=0)
# Want something like this
# colors = np.where(mask_data == 1, 'autumn','gray')
fig,ax = plt.subplots()
ax.imshow(masked_image,cmap="gray")
Output image:
Currently my code takes in an image and a mask, I set the pixel values in the image_array to 0 based on the mask. I ideally want this to show up as red:
The only way I can see how to do this is a different colormap for all pixels with a value of 0. I don't know how to write a custom colour map based on pixel value. Is there any way to do this?
Completely forgot matplotlib draws on top of pre existing data if using the same figure. The below code works to do what I want without needing to create my own colormap.
plt.clf()
# Mask array with mask pixels set to 1 and rest set to 0
mask = nib.load(os.path.join(mask_path, f))
# Grey scale MRI image slice
image = nib.load(os.path.join(images_path, f))
# Extracting pixel arrays
mask_data = mask.get_fdata()
image_data = image.get_fdata()
# Transposing and flipping to fix visual orientation
image_data = image_data.transpose((1,0))
image_data = np.flip(image_data,axis=0)
mask_data = mask_data.transpose((1,0))
mask_data = np.flip(mask_data,axis=0)
mask_data[mask_data==0] = None
plt.imshow(image_data,cmap='gray')
plt.imshow(mask_data,cmap = 'autumn')
Result:

problem with getting each segment resulted through K means segmentation in seperate images using Python and Opencv

I am using python and openCv for a brain segmentation project. I have segmented the brain MRI image using K means segmentation. I want to get each segment resulted through k means segmentation in seperate images. please help me in this.
#k_means segmentation
epsilon = 0.01
number_of_iterations = 50
number_of_clusters = 4
print(criteria, 'Criteria K_means parameters')
#plt.imshow(criteria)
#k means segmentation
_, labels, centers =cv2.kmeans(kmeans_input, number_of_clusters, None,
flags)
print(labels.shape, 'k-means segmentation')
#plt.imshow(labels)
#Adopting the labels
labels = labels.flatten('F')
for x in range (number_of_clusters): labels[labels == x] = centers [x]
print(labels.shape, 'adopting the tables value')
#plt.imshow(labels)
I would do it using sklearn kmeans segmentation as follows. I show how to create the segmented image and then select one color to present. I create a mask from thresholding the one color and then apply the mask to blacken out the other colors in the segmented image. You can write a loop over each color to get them all. It is also possible to use the mask to make the non-color be transparent rather than black. But I do not show that here. Or you can just save the binary masks.
Input:
#!/bin/python3.7
from skimage import io
from sklearn import cluster
import sys
import cv2
# read input and convert to range 0-1
image = io.imread('barn.jpg')/255.0
h, w, c = image.shape
# reshape to 1D array
image_2d = image.reshape(h*w, c)
# set number of colors
numcolors = 6
# do kmeans processing
kmeans_cluster = cluster.KMeans(n_clusters=int(numcolors))
kmeans_cluster.fit(image_2d)
cluster_centers = kmeans_cluster.cluster_centers_
cluster_labels = kmeans_cluster.labels_
# need to scale result back to range 0-255
newimage = cluster_centers[cluster_labels].reshape(h, w, c)*255.0
newimage = newimage.astype('uint8')
io.imshow(newimage)
io.show()
# select cluster 3 (in range 1 to numcolors) and create mask
lower = cluster_centers[3]*255
upper = cluster_centers[3]*255
lower = lower.astype('uint8')
upper = upper.astype('uint8')
mask = cv2.inRange(newimage, lower, upper)
# apply mask to get layer 3
layer3 = newimage.copy()
layer3[mask == 0] = [0,0,0]
io.imshow(layer3)
io.show()
# save kmeans clustered image and layer 3
io.imsave('barn_kmeans.gif', newimage)
io.imsave('barn_kmeans_layer3.gif', layer3)
Clustered Image:
Result for color 3:
ADDITION:
For a grayscale image, the following works for me.
#!/bin/python3.7
from skimage import io
from sklearn import cluster
import sys
import cv2
# read input and convert to range 0-1
image = io.imread('barn_gray.jpg',as_gray=True)/255.0
h, w = image.shape
# reshape to 1D array
image_2d = image.reshape(h*w,1)
# set number of colors
numcolors = 6
# do kmeans processing
kmeans_cluster = cluster.KMeans(n_clusters=int(numcolors))
kmeans_cluster.fit(image_2d)
cluster_centers = kmeans_cluster.cluster_centers_
cluster_labels = kmeans_cluster.labels_
# need to scale result back to range 0-255
newimage = cluster_centers[cluster_labels].reshape(h, w)*255.0
newimage = newimage.astype('uint8')
io.imshow(newimage)
io.show()
# select cluster 3 (in range 1 to numcolors) and create mask
# note the cluster numbers and corresponding colors are not constant from run to run
lower = cluster_centers[3]*255
upper = cluster_centers[3]*255
lower = lower.astype('uint8')
upper = upper.astype('uint8')
print(lower)
print(upper)
mask = cv2.inRange(newimage, lower, upper)
# apply mask to get layer 3
layer3 = newimage.copy()
layer3[mask == 0] = [0]
io.imshow(layer3)
io.show()
# save kmeans clustered image and layer 3
io.imsave('barn_gray_kmeans.gif', newimage)
io.imsave('barn_gray_kmeans_layer3.gif', layer3)

Categories