I've already opened my image and can access the individual pixels' RGB values, but what I'm trying to do now is apply a function to the RGB values of each of the pixels individually. That is, I don't want to apply it the same way across all the pixels in the entire image; I want to apply it differently depending on whether, for each individual pixel, the blue value is > red > green (rather than green > red > blue, etc, etc).
So my question is, how do I access the individual RGB elements within each pixel (as opposed to accessing all of the red, green, and blue values across the entire image at once)? Eventually my question will be "what's the fastest way to do this?" since it's obviously going to take a while to apply a function across each pixel individually but for now I'd be happy just to have any solution at all.
Thanks for any suggestions.
EDIT for clarity/more specificity:
I'm actually trying to apply a different set of instructions depending the ordering of 127.5 - abs(127.5 - red/green/blue)), not simply on the order of red>green>blue (as stated initially above, bc I was trying to simplify). Once that ordering is determined for a given pixel, then the appropriate set of instructions is applied. Again, this is pixel-by-pixel -- I'm not ordering things based on ALL red values across the image, just the rgbs of the individual pixels. So what I'm trying to do would look something like this (here I'm playing out just one of the six possible orders; I've omitted the five other possibilities for brevity):
def rgb_manip(red,green,blue):
r_max = int(127.5 - abs(127.5 - red))
g_max = int(127.5 - abs(127.5 - green))
b_max = int(127.5 - abs(127.5 - blue))
if r_max >= g_max >= b_max:
if r_max >= g_max + b_max:
new_red = red + g_max + b_max
new_green = green - g_max
new_blue = blue - b_max
else:
new_red = red + r_max
new_green = green - r_max + b_max
new_blue = blue - b_max
# elif... And so on, with a different set of instructions for each of the 6 possibilities depending on the order of the r_max, g_max, b_max values (e.g., r_max >= b_max >= g_max or g_max >= r_max >= b_max, etc, etc)
If you convert your image into an array, you can access the RGB values for one pixel, or one of the R, G, or B values for all pixels:
from __future__ import division
import numpy as np
from PIL import Image
im = Image.open(imfile)
arr = np.asarray(im)
arr[..., 0] # All Red values
arr[..., 1] # All Green values
arr[..., 2] # All Blue values
arr[0, 0] # RGB for first corner pixel
arr[m, n] # RGB for pixel at [m, n]
arr[m, n, 0] # R value for pixel [m, n]
arr[m, n, c] # value for color c at pixel [m, n]
You can get a ranking for each pixel using argsort, as in:
def transform(a, c=255/2):
return c - np.abs(c - a)
ranking = transform(arr).argsort(axis=-1)
which ranks the criterion values from smallest to largest value along the last (color) axis. So this gives a new array where each 'color' array instead of being the RGB values are the sorting of the transformed R, B, and G values (call them "R', B', G'"), so if the corner pixel had G' > B' > R', then ranking[0, 0] would be [0, 2, 1] because R' (0) is smallest, then next is B' (2), finally the largest is G' (1).
The advantage of doing the above is that you have an array that says which method to use on which pixel. It can have at most six orderings of the transformed channels. I suggest defining a separate function like so for each of orderings. Then, only one decision must be made within the function (the second nested if/else in your example), and it can be done with np.where which applies one thing to parts of an array where a condition is met, and another thing to the rest. This only works for two options, but if there are multiple options (if/elif/else), other techniques can work equally well.
def bgr(a):
""" for when B' < G' < R'
"""
t = transform(a)
red, green, blue = a.transpose([2,0,1])
# same as: red, green, blue = a[..., 0], a[..., 1], a[..., 2]
r_max, g_max, b_max = t.transpose([2,0,1])
assert np.all((b_max <= g_max) & (g_max <= r_max)), "doesn't match rank"
condition = r_max >= g_max + b_max
new_red = np.where(condition, red + g_max + b_max, red + r_max)
new_green = np.where(condition, green - g_max, green - r_max + b_max)
new_blue = blue - b_max
return np.dstack([new_red, new_green, new_blue])
this function only works for the first if in yours. I would make a new function for each of those six things, and fill them into a dict like so:
functions = {
(0, 1, 2) : rgb, # for R'<G'<B'
(0, 2, 1) : rbg, # for R'<B'<G'
#etc...
}
If your output has RGB values too:
out = np.empty_like(arr)
Then loop through all six rankings/functions:
for rank, func in functions.items():
mask = np.all(transform(arr).argsort(-1) == rank, -1)
out[mask] = func(arr[mask])
Related
I have some code, which works as intended, however takes about 4 and a half hours to run, I understand that there are about 50 billion calculations my poor pc needs to do but I thought it would be worth asking!
This code gets an image, and wants to find every possible region of 331*331 pixels in the given image, and find how many black pixels there are in each, I will use this data to create a heatmap of black pixel density, and also a list of all of the values found:
image = Image.open(self.selectedFile)
pixels = list(image.getdata())
width, height = image.size
pixels = [pixels[i * width:(i+1) * width] for i in range(height)]
#print(pixels)
rightShifts = width - 331
downShifts = height - 331
self.totalRegionsLabel['text'] = f'Total Regions: {rightShifts * downShifts}'
self.blackList = [0 for i in range(0, rightShifts*downShifts)]
self.heatMap = [[] for i in range(0, downShifts)]
for x in range(len(self.heatMap)):
self.heatMap[x] = [0 for i in range(0, rightShifts)]
for x in range(rightShifts):
for y in range(downShifts):
blackCount = 0
for z in range(x + 331):
for w in range(y + 331):
if pixels[z][w] == 0:
blackCount += 1
self.blackList[x+1*y] = blackCount
self.heatMap[x][y] = blackCount
print(self.blackList)
You have several problems here, as I pointed out. Your z/w loops are always starting at the upper left, so by the time you get towards the end, you're summing the entire image, not just a 331x331 subset. You also have much confusion in your axes. In an image, [y] is first, [x] is second. An image is rows of columns. You need to remember that.
Here's an implementation as I suggested above. For each column, I do a full sum on the top 331x331 block. Then, for every row below, I just subtract the top row and add the next row below.
self.heatMap = [[0]*rightShifts for i in range(downShifts)]
for x in range(rightShifts):
# Sum up the block at the top.
blackCount = 0
for row in range(331):
for col in range(331):
if pixels[row][x+col] == 0:
blackCount += 1
self.heatMap[0][x] = blackCount
for y in range(1,downShifts):
# To do the next block down, we subtract the top row and
# add the bottom.
for col in range(331):
blackCount += pixels[y+330][x+col] - pixels[y-1][x+col]
self.heatMap[y][x] = blackCount
You could tweak this even more by alternating the columns. So, at the bottom of the first column, scoot to the right by subtracting the first column and adding the next new column. then scoot back up to the top. That's a lot more trouble.
The two innermost for-loops seem to be transformable to some numpy code if using this package is not an issue. It would give something like:
pixels = image.get_data() # it is probably already a numpy array
# Get an array filled with either True or False, with True whenever pixel is black:
pixel_is_black = (pixels[x:(x+331), y:(y+331)] == 0)
pixel_is_black *= 1 # Transform True and False to respectively 1 and 0. Maybe not needed
self.blackList[x+y] = pixel_is_black.sum() # self explanatory
This is the simplest optimization I can think of, you probably can do much better with clever numpy tricks.
I would recommend using some efficient vector computations through the numpy and opencv libraries.
First, binarize your image so that black pixels are set to zero, and any other color pixels (gray to white) are set to 1. Then, apply a 2D filter to the image of shape 331 x 331 where each value in the filter kernel is (1 / (331 x 331) - this will take the average of all the values in each 331x331 area and assign it to the center pixel.
This gives you a heatmap, where each pixel value is the proportion of non-black pixels in the surrounding 331 x 331 region. A darker pixel (value closer to zero) means more pixels in that region are black.
For some background, this approach uses image processing techniques called image binarization and box blur
Example code:
import cv2
import numpy as np
# setting up a fake image, with some white spaces, gray spaces, and black spaces
img_dim = 10000
fake_img = np.full(shape=(img_dim, img_dim), fill_value=255, dtype=np.uint8) # white
fake_img[: img_dim // 3, : img_dim // 3] = 0 # top left black
fake_img[2 * img_dim // 3 :, 2 * img_dim // 3 :] = 0 # bottom right black
fake_img[img_dim // 3 : 2 * img_dim // 3, img_dim // 3 : 2 * img_dim // 3] = 127 # center gray
# show the fake image
cv2.imshow("", fake_img)
cv2.waitKey()
cv2.destroyAllWindows()
# solution to your problem
binarized = np.where(fake_img == 0, 0, 1) # have 0 values where black, 1 values else
my_filter = np.full(shape=(331, 331), fill_value=(1 / (331 * 331))) # set up filter
heatmap = cv2.filter2D(fake_img, 1, my_filter) # apply filter, which takes average of values in 331x331 block
# show the heatmap
cv2.imshow("", heatmap)
cv2.waitKey()
cv2.destroyAllWindows()
I ran this on my laptop, with a huge (fake) image of 10000 x 10000 pixels, almost instantly.
Sorry I should have deleted this post before you all put the effort in, however, some of these workarounds are really smart and interesting, I ended up coming up with a solution independently that is the same as what Tim Robbers first suggested, I used the array I had and built a second one on which every item in a row is the number of black cells preceding it, and then for each row in a region instead of scanning every item, just scan the preceding value and the final value and you are good:
image = Image.open(self.selectedFile).convert('L') #convert to luminance mode as RGB information is irrelevant
pixels = list(image.getdata()) #get the value of every pixel in the image
width, height = image.size
pixels = [pixels[i * width:(i+1) * width] for i in range(height)] #split the pixels array into a two dimensional array with the dimensions to match the image
#This program scans every possible 331*331 square starting from the top left, so it will move right width - 331 pixels and down height - 331 pixels
rightShifts = width - 331
downShifts = height - 331
self.totalRegionsLabel['text'] = f'Total Regions: {rightShifts * downShifts}' #This wont update till the function has completed running
#The process of asigning new values to values in an array is faster than appending them so this is why I prefilled the arrays:
self.heatMap = [[] for i in range(0, downShifts)]
for x in range(len(self.heatMap)):
self.heatMap[x] = [0 for i in range(0, rightShifts)]
cumulativeMatrix = [] #The cumulative matrix replaces each value in each row with how many zeros precede it
for y in range(len(pixels)):
cumulativeMatrix.append([])
cumulativeMatrix[y].append(0)
count = 0
for x in range(len(pixels[y])):
if pixels[y][x] == 0:
count += 1
cumulativeMatrix[y].append(count)
regionCount = 0
maxValue = 0 #this is the lowest possible maximum value
minValue = 109561 #this is the largest possible minimum value
self.blackList = []
#loop through all possible regions
for y in range(downShifts):
for x in range(rightShifts):
blackPixels = 0
for regionY in range(y, y + 331):
lowerLimit = cumulativeMatrix[regionY][x]
upperLimit = cumulativeMatrix[regionY][x+332]
blackPixels += (upperLimit - lowerLimit)
if blackPixels > maxValue:
maxValue = blackPixels
if blackPixels < minValue:
minValue = blackPixels
self.blackList.append(blackPixels)
self.heatMap[y][x] = blackPixels
regionCount += 1
This brought run time to under a minute and thus solved my problem, however, thank you for your contributions I have learned a lot from reading them!
Try to look into the map() function. It uses C to streamline iterations.
You can speed up your for loops like this:
pixels = list(map(lambda i: x[i*width:(i+1)*width], range(height)))
So I have an assignment where I have to replicate the image below using turtle. I know the basic concept of setting the pen to a color, then making a line, go 1 y coordinate down and repeat. But what I need to know is how do I know what color to make each line. I've heard about linear interpolation but I have no idea what that means.
assignment
Here's how you can do it with any starting and ending colors, without having to hard-code anything. Note that this is an example showing the operations done to each color channel; below I'll show how to do it in a more succinct way.
# the starting color
initial_color = (0.60156, 0, 0.99218) # (154, 0, 254)
# the final, target color
target_color = (0.86328, 0.47656, 0.31250) # (221, 122, 80)
number_of_rows=10 # how many rows we're painting
# get the total difference between each color channel
red_difference=target_color[0]-initial_color[0]
green_difference=target_color[1]-initial_color[1]
blue_difference=target_color[2]-initial_color[2]
# divide the difference by the number of rows, so each color changes by this amount per row
red_delta = red_difference/number_of_rows
green_delta = green_difference/number_of_rows
blue_delta = blue_difference/number_of_rows
# display the color for each row
for i in range(0, number_of_rows):
# apply the delta to the red, green and blue channels
interpolated_color=(initial_color[0] + (red_delta * i),
initial_color[1] + (green_delta * i),
initial_color[2] + (blue_delta * i))
print(interpolated_color)
Output:
(0.60156, 0.0, 0.99218)
(0.627732, 0.047656, 0.9242119999999999)
(0.653904, 0.095312, 0.856244)
(0.680076, 0.14296799999999998, 0.788276)
(0.706248, 0.190624, 0.720308)
(0.7324200000000001, 0.23828, 0.6523399999999999)
(0.758592, 0.28593599999999997, 0.5843719999999999)
(0.784764, 0.333592, 0.516404)
(0.8109360000000001, 0.381248, 0.44843599999999995)
(0.8371080000000001, 0.42890399999999995, 0.3804679999999999)
Note that this stops before the final color, you can either use the target color for your last row, or just increase the range to number_of_rows + 1.
Here's the above code but highly simplified - it gives the same output:
# simpler version - we can skip the diffs and just get the deltas, and store all 3 colors in a list
deltas=[(target_color[i] - initial_color[i])/number_of_rows for i in range(3)]
for j in range(0, number_of_rows):
interpolated_color=tuple([initial_color[i] + (deltas[i] * j) for i in range(3)])
print(interpolated_color)
alright so i was able to answer my question. What I did was take the difference between the 2 color's rgb code and then divide that by the amount I wanted the gradient to last, then added that to the rgb one line at a time.
import turtle
jeff = turtle.Turtle()
jeff.speed('fastest')
def gradient():
jeff.penup()
jeff.goto(-450, 200)
jeff.pendown()
r = 154.0
g = 0.0
b = 254.0
for i in range(125):
jeff.pencolor(r, g, b)
jeff.fd(900)
jeff.penup()
jeff.seth(270)
jeff.fd(1)
jeff.seth(180)
jeff.pendown()
r += 0.268
g += 0.488
b -= 0.696
jeff.pencolor(r, g, b)
jeff.fd(900)
jeff.penup()
jeff.seth(270)
jeff.fd(1)
jeff.seth(0)
jeff.pendown()
r += 0.268
g += 0.488
b -= 0.696
gradient():
It's a bit rudimentary, but it works
I am learning image processing and came across this following code. What it does is glow the red light of the trafficlights image. I did understood the code by don't know how the new_R variable is calculated. Although it is easy to interpret that it corresponds to value in R and G scale where the values of pixel is less than 40 in R and 10 in G.
I just want to know why G layer values are added and what if i want to glow the green light or orange light what should be the upper or lower bound in calculation of new_G or new_O as new_R is done.
from sklearn import io as ip
import numpy as np
I = ip.imread('trafficlights.png')
R = np.logical_and(I[:, :, 0] > 40, I[:, :, 1] < 10)
new_R = gaussian_filt(255 * R, 30)
J = I.copy()
J[:, :, 0] = ip.imadd(new_R, J[:, :, 0])
import matplotlib.pyplot as plt
fig, ax = plt.subplots(1, 2)
plt.sca(ax[0])
ip.imshow(I)
plt.sca(ax[1])
ip.imshow(J)
ip.imsave('traffic_redglow.png', J)
if you want to reproduce then here are the other functions
def imadd(I, K):
'''
Add to the pixels of I.
- If K is a number, adds K to each pixel value of I.
- If K is an image (NumPy array) of the same size as I,
the pixel values of K are added to the pixel values of I.
Clips the result to avoid overflow.
Example:
# Add 10 to each pixel of I
J = imadd(I, 10)
# Subtract 20 from each pixel of I
J = imadd(I, -20)
# Add images I1 and I2 together
J = imadd(I1, I2)
'''
import numbers
if isinstance(K, numbers.Number):
J = I.astype('int32')
J += K
elif isinstance(K, np.ndarray):
assert K.shape == I.shape, f'Cannot add images with sizes {I.shape} and {K.shape}.'
J = I.astype('int32') + K.astype('int32')
else:
raise TypeError('K must be a number or an array.')
np.clip(J, 0, 255, out=J)
J = J.astype('uint8')
return J
def gaussian_filt(I, sigma, pad=0):
'''
Filters the image I with a Gaussian filter,
with standard deviation sigma.
Returns a new filtered image.
Optional argument pad specifies the boundary options,
and can take the following values:
- if pad is a number (0-255, default 0), pixels outside the bounds of the image are assigned this value.
- 'reflect': outer pixel values are mirror-reflections of inner pixel values
across the image border.
- 'nearest': outer pixel values are equal to the nearest inner pixel values.
- 'wrap': outer pixel values are computed by assuming that the image is periodic (i.e. tiled).
Example:
# Use Gaussian filter on I with a standard deviation of 0.5,
# pad boundary pixels with white
J = gaussian_filt(I, 0.5, pad=255)
# Reflect boundary pixel values across the image border
J = gaussian_filt(I, 0.5, pad='reflect')
'''
import numbers
assert isinstance(pad, numbers.Number) or pad in ['reflect', 'nearest', 'wrap'], \
'Choose a correct value for pad: a number (0-255), ''reflect'', ''nearest'', or ''wrap''.'
if isinstance(pad, numbers.Number):
md = 'constant'
c = pad
else:
md = pad
c = 0
return gaussian_filter(I, sigma, mode=md, cval=c)
Thanks a lot for going through
I have an array ver of shape (9,8,2) which I extracted from an image.
What I'm searching to do is to iterate over the pixels of this array, check if Green and Blue values have a different parity, if so, put the result True in a list, and do the same for each lines.
ver = im_arr[:9,:8,1:3]
Here I took the first 9 lines of the image, the 8 first pixels, and I've extracted the columns to have only the Green and Blue values.
I now know how to with a 2D array like that :
cf = cf[:,1:3]
jou = (cf.sum(1)%2).astype(bool)
But in this case, with a 3D array, I really don't know how to do, I tried with three for loop, but it doesn't work :
for i in range(ver.shape[0]):
for y in range(ver.shape[1]):
for z in range(ver.shape[2]):
juju[i,y,z] = (ver.sum(1)%2).astype(bool)
import numpy as np
image = np.random.randint(0, 255, (100, 100, 3))
subset = image[:9, :8, 1:]
mask = ~np.all(np.remainder(subset, 2) == 0, axis=-1)
This code generates a random image and extracts the subset. Then it checks that the remainder of all numbers in the array are divisible by 2. The np.all call checks if across the last dimension, all values are True and returns True if so, False otherwise. To get the mask you want I just negate the result with ~
Edit to add a solution that checks whether both values across dimensions are either equal or odd.
import numpy as np
image = np.random.randint(0, 255, (100, 100, 3))
subset = image[:9, :8, 1:]
even = np.remainder(subset, 2) == 0
mask = ~np.equal(even[..., 0], even[..., 1])
If you mask the LSB of the Green and Blue channels and add them, you can only get:
0 + 0 = 0
0 + 1 = 1 <--- THIS
1 + 0 = 1 <--- THIS
1 + 1 = 2
So, you want to identify where they add up to 1, i.e. they are different:
mask = np.sum((ver & 1), axis=-1) == 1
I have an RGB image which I am loading into a 2D array using PIL
img = Image.open(path)
imgData = numpy.array(img)
I need to efficiently translate this into a 2D array of RGB tuples (in some sense a 3D array) the same size containing a rough 'classification' of each pixel - 'red', 'green', 'white' or 'other' - at each index based on which 'colour region' they lie within. This is for purposes of image recognition.
My current implementation uses a element-wise for loop but is very slow (an 8MP image takes 1+ minutes):
for i in range(cols): # for every col
for j in range(rows): # for every row
r,g,b = imgData[i,j]
if b > 220: # white
n = 3
elif r > 230: # red
n = 2
else: # green
n = 1
mapData[i,j] = n
(I realise that the order of the if statements here affects the precedence of the classifications - this is not a major issue for now although I would prefer to define the colour spaces exclusively)
I am running Python 3.6.4 and happy to use NumPy or not. Having done a bunch of research, it seems like there are a number of faster and more 'pythonic' and vectorised ways to do this but I have not been able to get any working.
Any help would be much appreciated
Thanks!
Using np.where makes this pretty fast.
mapData = np.where(imgData[:,:,2] > 220, 3, np.where(imgData[:,:,0]>230, 2, 1))
But when applying this to a picture the only results where ones. Did I miss anything or should the cases be made in a different way?
Your algorithm as of the moment can be captured like this:
r, g, b = imgData[...,0], imgData[...,1], imgData[...,2]
mapData = np.ones_like(r, dtype=int)
mapData[r > 230] = 2
mapData[b > 220] = 3
Note the order of operations in assigning these numbers.
Colour classification is usually done by treating RGB colours as vectors. Normalize each one to the magnitude, then find the distance to your target colour.
For example, the skin detector in smartcrop.js works like this (using pyvips):
def pythag(im):
return sum([x ** 2 for x in im]) ** 0.5
skin = [0.78, 0.57, 0.44]
score = 1 - pythag(img / pythag(img) - skin)
Now score is a float image with values in 0 - 1 which is 1 for pixels most likely to be skin-coloured. Note that it ignores brightness: you'll need another rule to chop off very dark areas.
In your case I guess you'd need an array set of target vectors, then compute all the colour probabilities, and finally label the output pixel with the index of the highest-scoring vector. Something like:
import sys
import pyvips
def pythag(im):
return sum([x ** 2 for x in im]) ** 0.5
def classify(img, target):
return 1 - pythag(img / pythag(img) - target)
# find [index, max] of an array of pyvips images
def argmax(ar):
if len(ar) == 1:
return [0, ar[0]]
else:
index, mx = argmax(ar[:-1])
return [(ar[-1] > mx).ifthenelse(len(ar) - 1, index),
(ar[-1] > mx).ifthenelse(ar[-1], mx)]
skin = [0.78, 0.57, 0.44]
red = [1, 0, 0]
green = [0, 1, 0]
targets = [red, green, skin]
# we're not doing any coordinate transformations, so we can stream the image
img = pyvips.Image.new_from_file(sys.argv[1], access="sequential")
scores = [classify(img, x) for x in targets]
index, mx = argmax(scores)
index.write_to_file(sys.argv[2])
(plug: pyvips is typically 2x or 3x faster than numpy and needs much less memory)