I would like to transform a .jpg into a categorical array. For each pixel of the images I have RGB values and I would like to associate this values to a unique value (see images). Have you any idea to do this? I've made some research in scikit image and other image processing modules but without success.
The first part solution is found in https://stackoverflow.com/a/30524039/3104727). It is reproduced here in order to work it with this image
from PIL import Image
import operator
from collections import defaultdict
import numpy as np
input_path = 'TI_test.jpg'
output_path = 'TI_output.png'
size = (200,200)
# Then we declare the palette - this should contain all colours.
palette = [(112, 137, 98), #green
(96, 97, 115), #blue
(140, 129, 49), #gold
(184, 31, 36), #red
]
while len(palette) < 256:
palette.append((0, 0, 0))
# The code below will declare palette for PIL, since PIL needs flat
# array rather than array of tuples:
flat_palette = reduce(lambda a, b: a+b, palette)
assert len(flat_palette) == 768
# Now we can declare an image that will hold the palette. We'll use
# it to reduce the colours from the original image later.
palette_img = Image.new('P', (1, 1), 0)
palette_img.putpalette(flat_palette)
# Here we open the image and quantize it. We scale it to size eight
# times bigger than needed, since we're going to sample the average
# output later.
multiplier = 8
img = Image.open(input_path)
img = img.resize((size[0] * multiplier, size[1] * multiplier),Image.BICUBIC)
img = img.quantize(palette=palette_img) #reduce the palette
# We need to convert it back to RGB so that we can sample pixels now:
img = img.convert('RGB')
# Now we're going to construct our final image. To do this, we'll
# sample how many pixels of each palette color each square in the
# bigger image contains. Then we'll choose the color that occurs most
# often.
out = Image.new('RGB', size)
for x in range(size[0]):
for y in range(size[1]):
#sample at get average color in the corresponding square
histogram = defaultdict(int)
for x2 in range(x * multiplier, (x + 1) * multiplier):
for y2 in range(y * multiplier, (y + 1) * multiplier):
histogram[img.getpixel((x2,y2))] += 1
color = max(histogram.iteritems(),key=operator.itemgetter(1))[0]
out.putpixel((x, y), color)
The following code is added to transform RGB image in grayscale and then in an array of categorical value (0 to n colours).
out2 = out.convert('L')
List of unique grayscale values
color = list(set(list(out2.getdata())))
Associate categorical value (0 to n colours) to each pixel
for x in range(size[0]):
for y in range(size[1]):
if out2.getpixel((x,y)) == color[0]:
out2.putpixel((x,y),0)
elif out2.getpixel((x,y)) == color[1]:
out2.putpixel((x,y),1)
elif out2.getpixel((x,y)) == color[2]:
out2.putpixel((x,y),2)
else:
out2.putpixel((x,y),3)
Transform the image to a numpy array
pix = np.array(out2)
Related
I'm building a line in PIL on two coordinates, for each coordinate I need to choose a random color, and I can't do it in any way
from PIL import Image, ImageDraw
import numpy as np
N = 20
x = np.linspace(1,10,N)
y = x**2
z = np.random.rand(N)
im = Image.new('L', (256, 256), 255)
draw = ImageDraw.Draw(im)
for i in range(len(x)-1):
draw.line((x[i],y[i], x[i+1], y[i+1]),fill=0,width=2)
im.show()
# tried to change color with fill
#for i in range(len(x)-1):
# draw.line((x[i],y[i], x[i+1], y[i+1]),fill=z,width=2)
# there was such an error
# color must be int or tuple
Thanks in advance!
According to the error message, it expects an integer. But in your case, the input z is passed which is an array of shape (20,).
Since Image has been declared with mode L, it expects an 8-bit value in fill, i.e. a value between 0 and 255 (Modes in PIL)
Now since you want to choose a random color, I used np.random.randint(0,256) to choose one between 0-255 at every iteration:
for i in range(len(x)-1):
draw.line((x[i],y[i], x[i+1], y[i+1]),fill=np.random.randint(0,256),width=2)
Result:
The line drawn above is in grayscale.
For RGB image:
# set the mode to `RGB`
im2 = Image.new('RGB', (256, 256))
draw2 = ImageDraw.Draw(im2)
for i in range(len(x)-1):
# pass a tuple of 3 random values between 0-255
draw2.line((x[i],y[i], x[i+1], y[i+1]),fill=tuple(np.random.choice(range(255),size=3)),width=2)
Result:
The result is a random color line drawn between points.
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.
I currently have a numpy array 'images' containing 2000 photos. I am looking for an improved way of converting all the photos in 'images' to gray scale. The shape of the images is (2000, 100, 100, 3). This is what I have so far:
# Function takes index value and convert images to gray scale
def convert_gray(idx):
gray_img = np.uint8(np.mean(images[idx], axis=-1))
return gray_img
#create list
g = []
#loop though images
for i in range(0, 2000):
#call convert to gray function using index of image
gray_img = convert_gray(i)
#add grey image to list
g.append(gray_img)
#transform list of grey images back to array
gray_arr = np.array(g)
I wondered if anyone could suggest a more efficient way of doing this? I need the output in an array format
With your mean over the last axis you do right now:
Gray = 1/3 * Red + 1/3 * Green + 1/3 * Blue
But actually another conversion formula is more common (See this answer):
Gray = 299/1000 * Red + 587/1000 * Green + 114/1000 * Blue
The code provided by #unutbu also works for arrays of images:
import numpy as np
def rgb2gray(rgb):
return np.dot(rgb[...,:3], [0.2989, 0.5870, 0.1140])
rgb = np.random.random((100, 512, 512, 3))
gray = rgb2gray(rgb)
# shape: (100, 512, 512)
I want to create a mask based on certain pixel values. For example: every pixel where B > 200
The Image.load() method seems to be exactly what I need for identifying the pixels with these values, but I can't seem to figure out how to take all these pixels and create a mask image out of them.
R, G, B = 0, 1, 2
pixels = self.input_image.get_value().load()
width, height = self.input_image.get_value().size
for y in range(0, height):
for x in range(0, width):
if pixels[x, y][B] > 200:
print("%s - %s's blue is more than 200" % (x, y))
``
I meant for you to avoid for loops and just use Numpy. So, starting with this image:
from PIL import Image
import numpy as np
# Open image
im = Image.open('colorwheel.png')
# Make Numpy array
ni = np.array(im)
# Mask pixels where Blue > 200
blues = ni[:,:,2]>200
# Save logical mask as PNG
Image.fromarray((blues*255).astype(np.uint8)).save('result.png')
If you want to make the masked pixels black, use:
ni[blues] = 0
Image.fromarray(ni).save('result.png')
You can make more complex, compound tests against ranges like this:
#!/usr/bin/env python3
from PIL import Image
import numpy as np
# Open image
im = Image.open('colorwheel.png')
# Make Numpy array
ni = np.array(im)
# Mask pixels where 100 < Blue < 200
blues = ( ni[:,:,2]>100 ) & (ni[:,:,2]<200)
# Save logical mask as PNG
Image.fromarray((blues*255).astype(np.uint8)).save('result.png')
You can also make a condition on Reds, Greens and Blues and then use Numpy's np.logical_and() and np.logical_or() to make compound conditions, e.g.:
bluesHi = ni[:,:,2] > 200
redsLo = ni[:,:,0] < 50
mask = np.logical_and(bluesHi,redsLo)
Thanks to the reply from Mark Setchell, I solved by making a numpy array the same size as my image filled with zeroes. Then for every pixel where B > 200, I set the corresponding value in the array to 255. Finally I converted the numpy array to a PIL image in the same mode as my input image was.
R, G, B = 0, 1, 2
pixels = self.input_image.get_value().load()
width, height = self.input_image.get_value().size
mode = self.input_image.get_value().mode
mask = np.zeros((height, width))
for y in range(0, height):
for x in range(0, width):
if pixels[x, y][2] > 200:
mask[y][x] = 255
mask_image = Image.fromarray(mask).convert(mode)
I work with logos and other simple graphics, in which there are no gradients or complex patterns. My task is to extract from the logo segments with letters and other elements.
To do this, I define the background color, and then I go through the picture in order to segment the images. Here is my code for more understanding:
MAXIMUM_COLOR_TRANSITION_DELTA = 100 # 0 - 765
def expand_segment_recursive(image, unexplored_foreground, segment, point, color):
height, width, _ = image.shape
# Unpack coordinates from point
py, px = point
# Create list of pixels to check
neighbourhood_pixels = [(py, px + 1), (py, px - 1), (py + 1, px), (py - 1, px)]
allowed_zone = unexplored_foreground & np.invert(segment)
for y, x in neighbourhood_pixels:
# Add pixel to segment if its coordinates within the image shape and its color differs from segment color no
# more than MAXIMUM_COLOR_TRANSITION_DELTA
if y in range(height) and x in range(width) and allowed_zone[y, x]:
color_delta = np.sum(np.abs(image[y, x].astype(np.int) - color.astype(np.int)))
print(color_delta)
if color_delta <= MAXIMUM_COLOR_TRANSITION_DELTA:
segment[y, x] = True
segment = expand_segment_recursive(image, unexplored_foreground, segment, (y, x), color)
allowed_zone = unexplored_foreground & np.invert(segment)
return segment
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Pass image as the argument to use the tool")
exit(-1)
IMAGE_FILENAME = sys.argv[1]
print(IMAGE_FILENAME)
image = cv.imread(IMAGE_FILENAME)
height, width, _ = image.shape
# To filter the background I use median value of the image, as background in most cases takes > 50% of image area.
background_color = np.median(image, axis=(0, 1))
print("Background color: ", background_color)
# Create foreground mask to find segments in it (TODO: Optimize this part)
foreground = np.zeros(shape=(height, width, 1), dtype=np.bool)
for y in range(height):
for x in range(width):
if not np.array_equal(image[y, x], background_color):
foreground[y, x] = True
unexplored_foreground = foreground
for y in range(height):
for x in range(width):
if unexplored_foreground[y, x]:
segment = np.zeros(foreground.shape, foreground.dtype)
segment[y, x] = True
segment = expand_segment_recursive(image, unexplored_foreground, segment, (y, x), image[y, x])
cv.imshow("segment", segment.astype(np.uint8) * 255)
while cv.waitKey(0) != 27:
continue
Here is the desired result:
In the end of run-time I expect 13 extracted separated segments (for this particular image). But instead I got RecursionError: maximum recursion depth exceeded, which is not surprising as expand_segment_recursive() can be called for every pixel of the image. And since even with small image resolution of 600x500 i got at maximum 300K calls.
My question is how can I get rid of recursion in this case and possibly optimize the algorithm with Numpy or OpenCV algorithms?
You can actually use a thresholded image (binary) and connectedComponents to do this job in a couple of steps. Also, you may use findContours or other methods.
Here is the code:
import numpy as np
import cv2
# load image as greyscale
img = cv2.imread("hp.png", 0)
# puts 0 to the white (background) and 255 in other places (greyscale value < 250)
_, thresholded = cv2.threshold(img, 250, 255, cv2.THRESH_BINARY_INV)
# gets the labels and the amount of labels, label 0 is the background
amount, labels = cv2.connectedComponents(thresholded)
# lets draw it for visualization purposes
preview = np.zeros((img.shape[0], img.shape[2], 3), dtype=np.uint8)
print (amount) #should be 3 -> two components + background
# draw label 1 blue and label 2 green
preview[labels == 1] = (255, 0, 0)
preview[labels == 2] = (0, 255, 0)
cv2.imshow("frame", preview)
cv2.waitKey(0)
At the end, the thresholded image will look like this:
and the preview image (the one with the colored segments) will look like this:
With the mask you can always use numpy functions to get things like, coordinates of the segments you want or to color them (like I did with preview)
UPDATE
To get different colored segments, you may try to create a "border" between the segments. Since they are plain colors and not gradients, you can try to do an edge detector like canny and then put it black in the image....
import numpy as np
import cv2
img = cv2.imread("total.png", 0)
# background to black
img[img>=200] = 0
# get edges
canny = cv2.Canny(img, 60, 180)
# make them thicker
kernel = np.ones((3,3),np.uint8)
canny = cv2.morphologyEx(canny, cv2.MORPH_DILATE, kernel)
# apply edges as border in the image
img[canny==255] = 0
# same as before
amount, labels = cv2.connectedComponents(img)
preview = np.zeros((img.shape[0], img.shape[1], 3), dtype=np.uint8)
print (amount) #should be 14 -> 13 components + background
# color them randomly
for i in range(1, amount):
preview[labels == i] = np.random.randint(0,255, size=3, dtype=np.uint8)
cv2.imshow("frame", preview )
cv2.waitKey(0)
The result is: