1

I have a numpy array where each element has 3 values (RGB) from 0 to 255, and it spans from [0, 0, 0] to [255, 255, 255] with 256 elements evenly spaced. I want to plot it as a 16 by 16 grid but have no idea how to map the colors (as the numpy array) to the data to create the grid.

import numpy as np

# create an evenly spaced RGB representation as integers
all_colors_int = np.linspace(0, (255 << 16) + (255 << 8) + 255, dtype=int)

# convert the evenly spaced integers to RGB representation
rgb_colors = np.array(tuple(((((255<<16)&k)>>16), ((255<<8)&k)>>8, (255)&k) for k in all_colors_int))

# data to fit the rgb_colors as colors into a plot as a 16 by 16 numpy array
data = np.array(tuple((k,p) for k in range(16) for p in range(16)))

So, how to map the rgb_colors as colors to the data data into a grid plot?

doomsk
  • 31
  • 5
  • 1
    When you add your [mcve], please include a couple of lines that initialise a representative array. Thank you. – Mark Setchell Jan 07 '23 at 14:56
  • What exactly does “no idea how to map the colors” mean? Please update the question to clarify. [0,255] is a valid RGB range; what ‘mapping’ needs to occur? – S3DEV Jan 07 '23 at 15:02
  • hope the code helps to clarify something – doomsk Jan 07 '23 at 15:48

2 Answers2

4

There's quite a bit going on here, and I think it's valuable to talk about it.

linspace

I suggest you read the linspace documentation. https://numpy.org/doc/stable/reference/generated/numpy.linspace.html

If you want a 16x16 grid, then you should start by generating 16x16=256 values, however if you inspect the shape of the all_colors_int array, you'll notice that it's only generated 50 values, which is the default value of the linspace num argument.

all_colors_int = np.linspace(0, (255 << 16) + (255 << 8) + 255, dtype=int)
print(all_colors_int.shape) # (50,)

Make sure you specify this third 'num' argument to generate the correct quantity of RGB pixels.

As a further side note, (255 << 16) + (255 << 8) + 255 is equivalent to (2^24)-1. The 2^N-1 formula is usually what's used to fill the first N bits of an integer with 1's.

numpy is faster

On your next line, your for loop manually iterates over all of the elements in python.

rgb_colors = np.array(tuple(((((255<<16)&k)>>16), ((255<<8)&k)>>8, (255)&k) for k in all_colors_int))

While this might work, this isn't considered the correct way to use numpy arrays.

You can directly perform bitwise operations to the entire numpy array without the python for loop. For example, to extract bits [16, 24) (which is usually the red channel in an RGB integer):

# Shift over so the 16th bit is now bit 0, then select only the first 8 bits.
RedChannel = (all_colors_int >> 16) & 255

Building the grid

There are many ways to do this in numpy, however I would suggest this approach.

Images are usually represented with a 3-dimensional numpy array, usually of the form

(HEIGHT, WIDTH, CHANNELS)

First, reshape your numpy int array into the 16x16 grid that you want.

reshaped = all_colors_int.reshape((16, 16))

Again, the numpy documentation is really great, give it a read:

https://numpy.org/doc/stable/reference/generated/numpy.reshape.html

Now, extract the red, green and blue channels, as described above, from this reshaped array. If you operate directly on the numpy array, you won't need a nested for-loop to iterate over the 16x16 grid, numpy will handle this for you.

RedChannel = (reshaped >> 16) & 255
GreenChannel = ... # TODO
BlueChannel = ... # TODO

And then finally, we can convert our 3, 16x16 grids, into a 16x16x3 grid, using the numpy stack function

https://numpy.org/doc/stable/reference/generated/numpy.stack.html

grid_rgb = np.stack((
    RedChannel,
    GreenChannel,
    BlueChannel
), axis=2).astype(np.uint8)

Notice two things here

  1. When we 'stack' arrays, we create a new dimension. The axis=2 argument tells numpy to add this new dimension at index 2 (e.g. the third axis). Without this, the shape of our grid would be (3, 16, 16) instead of (16, 16, 3)
  2. The .astype(np.uint8) casts all of the values in this numpy array into a uint8 data type. This is so the grid is compatible with other image manipulation libraries, such as openCV, and PIL.

Show the image

We can use PIL for this. If you want to use OpenCV, then remember that OpenCV interprets images as BGR not RGB and so your channels will be inverted.

# Show Image
from PIL import Image
Image.fromarray(grid_rgb).show()

If you've done everything right, you'll see an image... And it's all gray.

Why is it gray?

There are over 16 million possible colours. Selecting only 256 of them just so happens to select only pixels with the same R, G and B values which results in an image without any color.

If you want to see some colours, you'll need to either show a bigger image (e.g. 256x256), or alternatively, you can use a dimension that's not a power of two. For example, try a prime number, as this will add a small amount of pseudo-randomness to the RGB selection, e.g. try 17.

Best of luck.

Jamezo97
  • 78
  • 5
1

Based solely on the title 'How to plot a normalized RGB map' rather than the approach you've provided, it appears that you'd like to plot a colour spectrum in RGB.

The following approach can be taken to manually construct this.

import cv2
import matplotlib.pyplot as plt
import numpy as np

h = np.repeat(np.arange(0, 180), 180).reshape(180, 180)
s = np.ones((180, 180))*255
v = np.ones((180, 180))*255

hsv = np.stack((h, s, v), axis=2).astype('uint8')
rgb = cv2.cvtColor(hsv, cv2.COLOR_HSV2RGB)

plt.imshow(rgb)

enter image description here

Explanation:

It's generally easier to construct (and decompose) a colour palette using the HSV (hue, saturation, value) colour scale; where hue is the colour itself, saturation can be thought of as the intensity and value as the distance from black. Therefore, there's really only one value to worry about, hue. Saturation and value can be set to 255, for 'full intensity'.

cv2 is used here to simply convert the constructed HSV colourscale to RGB and matplotlib is used to plot the image. (I didn't use cv2 for plotting as it doesn't play nicely with Jupyter.)

The actual spectrum values are constructed in numpy.

Breakdown:

Create the colour spectrum of hue and plug 255 in for the saturation and value. Why is 180 used?

h = np.repeat(np.arange(0, 180), 180).reshape(180, 180)
s = np.ones((180, 180))*255
v = np.ones((180, 180))*255

Stack the three channels H+S+V into a 3-dimensional array, convert the array values to unsigned 8-bit integers, and have cv2 convert from HSV to RGB for us, to be lazy and save us working out the math.

hsv = np.stack((h, s, v), axis=2).astype('uint8')
rgb = cv2.cvtColor(hsv, cv2.COLOR_HSV2RGB)

Plot the RGB image.

plt.imshow(rgb)
S3DEV
  • 8,768
  • 3
  • 31
  • 42
  • thank for your answer, s3dev. Although @Jamezo97 answer is more aligned to what I actually wanted, hence the "normalized" and not "color spectrum" in the title. – doomsk Jan 10 '23 at 14:20