Numpy: vectorized operations to create a 3D array - python

I am learning Python and would like to find an efficient way to solve this problem using Numpy.
I currently have a 4x8 array containing random integers:
import numpy as np
n = 3
k = np.random.randint(n, size = (4,8))
Each number represents a color defined by its RGB value in a nx3 array:
colors = np.array([[0 , 0 , 0 ],
[0 , 100, 255],
[255, 100, 0 ]])
I would like to use these numbers to create a new 4x8x3 array where the first two dimensions represent pixels locations, and the third dimension the color of each pixel. This could be thought of as number painting. For example, if k[3,4] = 2, then myArray[3,4,:] = [255 100 0].
I am getting familiar with Numpy tools, but I am unsure of what I should be looking for exactly. Since the array k will eventually be much larger (I'm thinking ~640x480) and contain more than n = 3 non-random colors, I would like to use vectorized operations in order to speed up the process (and learn a bit more about them). Is this the most efficient way to do it?

IIUC, all you need to do is index into colors with k:
>>> k = np.random.randint(n, size = (2,4))
>>> out = colors[k]
>>> out
array([[[ 0, 100, 255],
[255, 100, 0],
[255, 100, 0],
[255, 100, 0]],
[[ 0, 100, 255],
[ 0, 100, 255],
[255, 100, 0],
[255, 100, 0]]])
>>> out.shape
(2, 4, 3)
>>> all((out[i]==colors[c]).all() for i,c in np.ndenumerate(k))
True

Related

Replacing ones and zeros in a 2D numpy array with another array?

I have a simple problem that I am trying to solve using numpy in an efficient manner. The jist of it is that I have a simple 2D array containing ones and zeros representing an image mask.
What I want to do is convert these ones and zeros into their RGB equivalent where one is a white pixel [255, 255, 255] and zero is a black pixel [0, 0, 0].
How would I go about doing this using NumPy?
mask = [[0, 0, 1],
[1, 0, 0]]
# something
result = [
[[0, 0, 0], [0, 0, 0], [255, 255, 255]],
[[255, 255, 255], [0, 0, 0], [0, 0, 0]]
]
The intent is to take the result and feed it into PIL to save into a PNG.
I've tried using numpy.where but can't seem to coax it into broadcasting another array out.
A possible solution:
np.stack([255 * mask, 255 * mask, 255 * mask], axis=2)
Output:
array([[[ 0, 0, 0],
[ 0, 0, 0],
[255, 255, 255]],
[[255, 255, 255],
[ 0, 0, 0],
[ 0, 0, 0]]])
As your image contains only two colours, I would suggest you consider saving it as a palette image, a.k.a. an indexed image.
Rather than needlessly inflating your image by a factor of 3 to enable it to store 16.7 million colours, you can just store one byte per pixel which will still enable you to have 256 colours which seems plenty when you only have 2 "colours", namely black and white.
That looks like this:
import numpy as np
from PIL import Image
# Make Numpy array "na" from your list
na = np.array(mask, dtype=np.uint8)
# Make PIL Image from Numpy array - this image will be 'L' mode
im = Image.fromarray(na)
# Now push a palette into the image that says:
# index 0 => black, i.e. [0,0,0]
# index 1 => white, i.e. [255,255,255]
#  all other 254 indices are black
# Afterwards the image will be 'P' mode
im.putpalette([0,0,0, 255,255,255] + [0,0,0]*254)
# Save
im.save('result.png')
Since you need to repeat each item three times, np.repeat in conjunction with reshape could be used:
mask = np.array([[0, 0, 1], [1, 0, 0]])
255 * np.repeat(mask, 3, axis=1).reshape(*mask.shape, -1)
>>> array([[[ 0, 0, 0],
[ 0, 0, 0],
[255, 255, 255]],
[[255, 255, 255],
[ 0, 0, 0],
[ 0, 0, 0]]])

How get unique pixels from 2d numpy array?

I have 2d array with rgb pixel data (2 row with 3 pixel in a row).
[[[255, 255, 255],[3, 0, 2],[255, 255, 255]],[[255, 255, 255],[3, 0, 2],[255, 255, 255]]]
How can I get unique pixel? I want to get
[[255, 255, 255], [3, 0, 2]]
I am trying to use np.unique and np.transpose with np.reshape but I wasn't able to get the desired result.
Reshape the array to 2D and then use np.unique with axis=0
arr = np.array([[[255, 255, 255],[3, 0, 2],[255, 255, 255]],[[255, 255, 255],[3, 0, 2],[255, 255, 255]]])
shape = arr.shape
arr = arr.reshape((shape[0] * shape[1], shape[2]))
print(np.unique(arr, axis=0))
Output
[[ 3 0 2]
[255 255 255]]
How about this?
import itertools
np.unique(np.array(list(itertools.chain(*arr))), axis=0)
array([[ 3, 0, 2],
[255, 255, 255]])

Pythonic way to transform a 2d array into a RGB image, using dictionaries

Say I have a 2d Matrix like
[[80 80 80]
[ 0 50 0]
[ 0 0 50]
[ 0 50 0]
[ 30 30 30]]
and I have a dictionary like
color_dict = {
80: (255,255,0),
50: (255,0,0),
30: (0,0,255)
}
I would like to get an BGR image (very small in this case, but image anyway) that is the reflection of the matrix based on the color assigned to each value by the dictionary.
I can do it using a loop which is my first instinct. But is there a more pythonic way to do this?
I see two options in this case:
Option 1: Numpy indexing
# First of all you need to map your random values to a continuous discrete range:
# 0 -> 0, 30 -> 1, 50 -> 2, 80 -> 3, for this you can use basic indexing.
# Now we have an array containing the position of each pixel and his corresponding value
img = np.array([[0,1,2],
[2,1,0]])
# And another array containing the colormap for each value
val = np.array([[255,255,0], # -> 0
[255,0,100], # -> 1
[100,0,0]]) # -> 2
# If we index the second array with the first one we obtain a new 3D array, the final image:
res = val.T[:,img]
Which looks like this:
array([[[250, 255, 100],
[100, 255, 250]],
[[250, 0, 0],
[ 0, 0, 250]],
[[ 0, 100, 0],
[ 0, 100, 0]]])
Options 2: Indexed color
Some image formats support indexed color:
Where a colormap associate each value with a specific color. So using one of those format will directly solve your problem.

How do I add a value at specific indices in a numpy array with min/max clipping?

I'm trying to add a scalar value at certain indices in a numpy array, which is of type numpy.uint8. Is there a way for me to make sure the addition clips at a certain maximum, so that the sum doesn't overflow? Here's how achieve it with a for loop, but it is slow and inefficient. Is there a way to use np.add.at or another function to do this faster?
change_by = 80
indices = [(0,0), (100, 100), (23, 45)]
for idx in indices:
output_image[idx[0], idx[1]] = min(255, image[idx[0], idx[1]] + change_by)
This limits the max value for each element to be 255. Is there a more efficient way to achieve this? Thanks!
Try np.clip()
Something like
import numpy as np
array = np.array([-10,0,1,2,100,200,300])
min = 0
max = 255
np.clip(array, min, max)
https://numpy.org/doc/stable/reference/generated/numpy.clip.html
If you need to work with uint8 and add before clip:
>>> import numpy as np
>>> a = np.array(range(10),dtype=np.uint8)
>>> a
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], dtype=uint8)
>>> np.array(a+250,dtype=np.uint8)
array([250, 251, 252, 253, 254, 255, 0, 1, 2, 3], dtype=uint8)
Is this the type of problem you're seeing?
Try this maybe:
>>> np.array(np.clip(a.astype(np.int)+250,0,255),dtype=np.uint8)
array([250, 251, 252, 253, 254, 255, 255, 255, 255, 255], dtype=uint8)

Convert multi-dimensional Numpy array to 2-dimensional array based on color values

I have an image which is read as a uint8 array with the shape (512,512,3).
Now I would like to convert this array to a uint8 array of shape (512,512,1), where each pixel value in the third axis are converted from a color value [255,0,0] to a single class label value [3], based on the following color/class encoding:
1 : [0, 0, 0],
2 : [0, 0, 255],
3 : [255, 0, 0],
4 : [150, 30, 150],
5 : [255, 65, 255],
6 : [150, 80, 0],
7 : [170, 120, 65],
8 : [125, 125, 125],
9 : [255, 255, 0],
10 : [0, 255, 255],
11 : [255, 150, 0],
12 : [255, 225, 120],
13 : [255, 125, 125],
14 : [200, 100, 100],
15 : [0, 255, 0],
16 : [0, 150, 80],
17 : [215, 175, 125],
18 : [220, 180, 210],
19 : [125, 125, 255]
What is the most efficient way to do this? I thought of looping through all classes and using numpy.where, but this is obviously time-consuming.
You could use giant lookup table. Let cls be [[0,0,0], [0,0,255], ...] of dtype=np.uint8.
LUT = np.zeros(size=(256,256,256), dtype='u1')
LUT[cls[:,0],cls[:,1],cls[:,2]] = np.arange(cls.shape[1])+1
img_as_cls = LUT[img[...,0],img[...,1], img[...,2]]
This solution is O(1) per pixel. It is also quite cache efficient because a small part of entries in LUT are actually used. It takes circa 10ms to process 1000x1000 image on my machine.
The solution can be slightly improved by converting 3-color channels to 24-bit integers.
Here is the code
def scalarize(x):
# compute x[...,2]*65536+x[...,1]*256+x[...,0] in efficient way
y = x[...,2].astype('u4')
y <<= 8
y +=x[...,1]
y <<= 8
y += x[...,0]
return y
LUT = np.zeros(2**24, dtype='u1')
LUT[scalarize(cls)] = 1 + np.arange(cls.shape[0])
simg = scalarize(img)
img_to_cls = LUT[simg]
After optimization it takes about 5ms to process 1000x1000 image.
One way: separately create the boolean arrays with True values where the input's pixel value matches one of the palette values, and then use arithmetic to combine them. Thus:
palette = [
[0, 0, 0],
[0, 0, 255],
[255, 0, 0],
# etc.
]
def palettized(data, palette):
# Initialize result array
shape = list(data.shape)
shape[-1] = 1
result = np.zeros(shape)
# Loop and add each palette index component.
for value, colour in enumerate(palette, 1):
result += (data == colour).all(axis=2) * value
return result
Here's one based on views -
# https://stackoverflow.com/a/45313353/ #Divakar
def view1D(a, b): # a, b are arrays
# This function gets 1D view into 2D input arrays
a = np.ascontiguousarray(a)
b = np.ascontiguousarray(b)
void_dt = np.dtype((np.void, a.dtype.itemsize * a.shape[-1]))
return a.view(void_dt).ravel(), b.view(void_dt).ravel()
def img2label(a, maps):
# Get one-dimension reduced view into input image and map arrays.
# We need to reshape image to 2D, then feed it to view1D to get 1D
# outputs and then reshape 1D image to 2D
A,B = view1D(a.reshape(-1,a.shape[-1]),maps)
A = A.reshape(a.shape[:2])
# Trace back positions of A in B using searchsorted. This gives us
# original order, which is the final output.
sidx = B.argsort()
return sidx[np.searchsorted(B,A,sorter=sidx)]
Given that your labels start from 1, you might want to add 1 to the output.
Sample run -
In [100]: # Mapping array
...: maps = np.array([[0, 0, 0],[0, 0, 255],\
...: [255, 0, 0],[150, 30, 150]],dtype=np.uint8)
...:
...: # Setup random image array
...: idx = np.array([[0,2,1,3],[1,3,2,0]])
...: img = maps[idx]
In [101]: img2label(img, maps) # should retrieve back idx
Out[101]:
array([[0, 2, 1, 3],
[1, 3, 2, 0]])

Categories

Resources