Numpy Manipulation for Image Processing - python

I've created a script to shift the hue of an image around the colour wheel by any number of steps.
As you might imagine, when I import an image (using PIL) and convert it to a Numpy array, it is this shape: (x, y, (r,g,b)).
I convert this array from RGB to HSV colour space with the Skimage color module (after scaling the RGB values to the range [0,1.0]).
The trouble I am having is manipulation of only one of the HSV values (either H, S, or V) for all pixels. I'd like to efficiently add, multiply, or subtract any of these three dimensions for every 'pixel' in the array.
I have gotten it to work by splitting the HSV values into three separate arrays:
h,s,v = np.dsplit(hsv,3)
manipulating the array in the way I want:
h_new = np.multiply(h,.33)
and then reassembling the array:
hsv_new = np.stack((h_new,s,v))
This doesn't seem like the most efficient way to do this and so my question is:
How can I manipulate each of these dimensions without having to split the array into chunks?

hsv[:,:,0] *= 0.33
modifies the h component of hsv inplace.
hsv[:,:,0] is a "basic slice" of hsv and as such, is a view of the original array.
h, s, v = np.dsplit(hsv, 3)
creates 3 new arrays, h, s, v which copy data from hsv. Modifying h, s, v does not affect hsv. So modifying h then requires rebuilding hsv. This is therefore slower.
For notational convenience, you could replace
h,s,v = np.dsplit(hsv, 3)
with
h, s, v = hsv[:,:,0], hsv[:,:,1], hsv[:,:,2]
Then h, s, v will be views of hsv, and modifying h, s, v will automatically affect hsv itself. (So there is no need for hsv_new = np.stack((h_new,s,v))).
Note also that h,s,v = np.dsplit(hsv, 3) makes h, s and v have shape (n, m, 1). Whereas
h, s, v = hsv[:,:,0], hsv[:,:,1], hsv[:,:,2]
makes h, s and v have shape (n, m). That might affect your other code a little bit, but overall I think the latter is nicer.

Related

How can I plot a normalized RGB map

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?
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
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)
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.
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)
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)

Remove CMYK colors to keep only black from a PNG

I'm trying to remove the colors from a PNG there is a way to do it ? My goal is to import my image in a PDF using Python, I tryed first with an SVG file but impossible to import, nothing appears with no error. So I wanted to try with a PNG but still hard to import.
Now I have an image with these percentage of colors :
And my final result would be this :
I already tried with openCV but no result from it, I'm looking for a solution since few days.
file = "app\\static\\img\\Picto CE_MAROC_H_6mm.png"
src = cv2.imread(file, cv2.IMREAD_UNCHANGED)
src[:,:,2] = np.zeros([src.shape[0], src.shape[1]])
cv2.imwrite(file,src)
Thanks in advance for your help ! :)
What does it mean to only have a K channel?
Most applications use RGB or RGBA, whereas the CMYK color space is typically for printed material. We should translate what does it mean for an image to only use the K channel.
First, let's look the formulas to convert the CMYK colorspace to RGB. We will assume that C, M, K are on a 0-100 integer scale:
R = 255 * (1 - C/100) * (1 - K/100)
G = 255 * (1 - M/100) * (1 - K/100)
B = 255 * (1 - Y/100) * (1 - K/100)
Since we only care for the K channel, we will set C, Y, and M to 0. This simplifies the formulas to:
R = 255 * (1 - K/100)
G = 255 * (1 - K/100)
B = 255 * (1 - K/100)
Notice that R = G = B when only the K channel is set. This produces a gray monochrome throughout the image, effectively making it grayscale. As such, the goal would be to produce a grayscale image given a RGBA image input.
Converting color to grayscale
Converting a color to its grayscale component is simply done by preserving the luminance of the original image in a gray monochrome palette. To do so, a formula must be defined which takes in a RGB input and returns a single value Y, creating a YYY color on the gray monochrome scale. This can simply be done by assigning each color a coefficient to scale how much an effect each has on luminance. Since the human eye is most sensitive to G, R, then B, we would want to assign a high coefficient to G and a low coefficient to B. The most common grayscale calculation used is luma coding for color TV and video systems:
Y = round(0.229 * R + 0.587 * G + 0.114 * B)
Converting an image to only use the K channel in Python
Now knowing the above information, we can convert an image to only use the K channel. For this, we can use imageio which can provide pixel information in RGB format. Since image data is given as an n dimensional array, we can also use numpy to abstract any loops needed to apply a grayscale to every pixel.
I will be using the imageio.v3 module as that is the most recent API as of this post. Loading in the image can be done by calling imageio.v3.imread and passing in the location of the image.
First, we want to get a luminance value for each pixel in the image. This can be done by taking the dot product of the image and the coefficients of the luminance formula. This will produce a 2D array as (height, width, RGB) x (RGB) = (height, width). We also need to round the values and cast each to a unsigned 8-bit integer to get our values into the 0-255 integer color range.
import numpy as np
# For some image `im` loaded by `#imread`
# The coefficients for converting an RGB color to its luminance value
grayscale_coef = [0.299, 0.587, 0.114]
# Create a 2D array where any pixel (height, width) translates to a single luminance value
grayscale = np.dot(im, grayscale_coef)
# Round the each luminance value and convert to a 0-255 range
grayscale = np.round(grayscale).astype(np.uint8)
Saving as CMYK
Now that we have the value to put into the K channel, we need to reconstruct the 3D array setting the CMY channels to 0 and then outputting to an image format that supports CMYK (JPG, TIFF, etc.). For this, we can use pillow.
from PIL import Image
# Create the CMY channels initialized to 0
cmy = np.zeros(grayscale.shape + (3,))
# Stack the CMY and K channels together
# Cast type to unsigned byte to avoid channel turning completely black
cmyk = np.dstack((cmy, grayscale)).astype(np.uint8)
# Read image from CMYK array buffer
result = Image.fromarray(cmyk, mode="CMYK")
# Save image in a supported format
result.save("<filename_here>.jpg")

image looks overexposed (nearly all white) when using np.zeros with OpenCV imshow

I am writing a code in openCV python for copying an image for practice purpose only, though np.copy() command is already available. Code is as below:
import numpy as np
import cv2 as cv
img = cv.imread('Photos/image_1.jpg')
r, c, d = img.shape
img_copy = np.zeros((r, c, d))
for i in range(r):
for j in range(c):
for k in range(d):
img_copy[i, j, k] = img[i, j, k]
cv.imshow('original image', img)
cv.imshow('copied image', img_copy)
cv.waitKey(0)
The img_copy is not shown instead black image is shown. If I use img_copy = np.ones(rows, cols, 3), and apply same for loop, still then a white image is shown, original image is not shown. Can any one explain why this occures? Original and copied images are shown below.
This issue was caused by incompatible data types.
To determine the data type of the image, use
img = cv.imread('Photos/image_1.jpg')
print(img.dtype)
In my testing case, the output data type was uint8
Changing
img_copy = np.zeros((r, c, d))
to
img_copy = np.zeros((r, c, d), dtype=np.uint8)
will fix this issue
OpenCV's imshow() is sensitive to the element type of the array you pass it.
When the element type is np.uint8, the values must be in the range of 0 to 255.
When the element type is np.float32 or np.float64, the values must be in the range of 0.0 to 1.0.
You have two options:
Either scale your values: img_copy / 255
Or clip and convert your values: np.clip(img_copy, 0, 255).astype(np.uint8)

extract edge features with prewitt_h

I am trying to extract edge features like this:
img = io.imread('pic.jpg')
H, W, C = img.shape
features = custom_features(img)
assignments = kmeans_fast(features, num_segments)
segments = assignments.reshape((H, W))
# Display segmentation
plt.imshow(segments, cmap='viridis')
plt.axis('off')
plt.show()
custom_features:
from skimage.filters import prewitt_h,prewitt_v
def custom_features(image):
"""
Args:
img - array of shape (H, W, C)
Returns:
features - array of (H * W, C)
"""
edges_prewitt_horizontal = prewitt_h(image)
return edges_prewitt_horizontal
However, currently I get an error because the shape of the image is different than what is expected by the prewitt_h function.
ValueError: The parameter `image` must be a 2-dimensional array
How can I modify this inside the function such that the returned shape is as desired?
It looks like you need to give to prewitt a grayscale image. The prewitt transform applies a convolution with a 2-dimensional kernel, hence you need 2-dimensional image (and yours is 3-d, because you have colors (RGB, 3 Channels)).
You could add inside your custom_features method a conversion to grayscale (skimage that you are using already has a method for that, check it out )
from skimage.filters import prewitt_h,prewitt_v
from skimage.color import rgb2gray
def custom_features(image):
"""
Args:
image - array of shape (H, W, C)
Returns:
features - array of (H * W, C)
"""
grayscale = rgb2gray(image)
edges_prewitt_horizontal = prewitt_h(grayscale)
return edges_prewitt_horizontal
And this should do the trick (I assume the image that the custom_features methods receives in input is always an RGB image because of the shape you defined above).
In case you have different types you can add a check if C == 3: to convert only RGB images.
By default, skimage.io.imread returns the read JPEG image as a shape-(M, N, 3) array, representing an RGB color image. However, the prewitt functions expect that the input is a single channel image.
To fix this, convert the image to grayscale first with skimage.color.rgb2gray before filtering. Or you could read the image directly as grayscale with skimage.io.imread(f, as_gray=True).

Sorting a list of RGB triplets into a spectrum

I have a list of RGB triplets, and I'd like to plot them in such a way that they form something like a spectrum.
I've converted them to HSV, which people seem to recommend.
from PIL import Image, ImageDraw
import colorsys
def make_rainbow_rgb(colors, width, height):
"""colors is an array of RGB tuples, with values between 0 and 255"""
img = Image.new("RGBA", (width, height))
canvas = ImageDraw.Draw(img)
def hsl(x):
to_float = lambda x : x / 255.0
(r, g, b) = map(to_float, x)
h, s, l = colorsys.rgb_to_hsv(r,g,b)
h = h if 0 < h else 1 # 0 -> 1
return h, s, l
rainbow = sorted(colors, key=hsl)
dx = width / float(len(colors))
x = 0
y = height / 2.0
for rgb in rainbow:
canvas.line((x, y, x + dx, y), width=height, fill=rgb)
x += dx
img.show()
However, the result doesn't look very much like a nice rainbow-y spectrum. I suspect I need to either convert to a different color space or handle the HSL triplet differently.
Does anyone know what I need to do to make this data look roughly like a rainbow?
Update:
I was playing around with Hilbert curves and revisited this problem. Sorting the RGB values (same colors in both images) by their position along a Hilbert curve yields an interesting (if still not entirely satisfying) result:
You're trying to convert a three-dimensional space into a one-dimensional space. There's no guarantee that you can make a pleasing rainbow out of it, as Oli says.
What you can do is "bucket" the colors into a few different categories based on saturation and value/lightness, and then sort within the categories, to get several independent gradients. For example, high-saturation colors first for the classic rainbow, then mid-saturation high-value colors (pastels), then low-saturation (grays).
Alternately, if all you care about is the rainbow, convert to hsl, then slam saturation to 1.0 and value to 0.5, convert back to rgb and render that instead of the original color.
Presumably you are sorting by hue (i.e. H)? That will give a nice result if S and L (or V) are constant, but if they are varying independently, then you will get a bit of a mess!
An interesting method for reducing dimensionality of color spaces uses the space-filling Hilbert curve. Two relevant articles are:
Color Space Dimension Reduction - overview of several methods for reducing dimensionality of color data
Portrait of the Hilbert Curve - detailed article about Hilbert curves and application to color-space dimensionality reduction
They both consider 3d -> 2d reduction, but the intermediate step of mapping to the 1d curve could be a solution to your problem.
Here are some rainbows I made recently, you can modify the idea to do what you want
from PIL import Image, ImageDraw # pip install pillow
import numpy as np
from matplotlib import pyplot as plt
strip_h, strip_w = 100, 720
strip = 255*np.ones((strip_h,strip_w,3), dtype='uint8')
image_val = Image.fromarray(strip)
image_sat = Image.fromarray(strip)
draw0 = ImageDraw.Draw(image_val)
draw1 = ImageDraw.Draw(image_sat)
for y in range(strip_h):
for x in range(strip_w):
draw0.point([x, y], fill='hsl(%d,%d%%,%d%%)'%(x%360,y,50))
draw1.point([x, y], fill='hsl(%d,%d%%,%d%%)'%(x%360,100,y))
plt.subplot(2,1,1)
plt.imshow(image_val)
plt.subplot(2,1,2)
plt.imshow(image_sat)
plt.show()
This seems incorrect.
canvas.line((x, y, x + dx, y), width=height, fill=rgb)
Try this.
canvas.rectangle([(x, y), (x+dx, y+height)], fill=rgb)

Categories