Editors comment:
How to count pixels occurences in an image?
I have a set of images where each pixel consists of 3 integers in the range 0-255.
I am interested in finding one pixel that is "representative" (as much as possible) for the entire pixel-population as a whole, and that pixel must occur in the pixel-population.
I am determining which pixel is the most common (the median mode) in my set of images makes the most sense.
I am using python, but I am not sure how to go about it.
The images are stored as an numpy array with dimensions [n, h, w, c], where n is the number of images, h is the height, w is the widthandc` is the channels (RGB).
I'm going to assume you need to find the most common element, which as Cris Luengo mentioned is called the mode. I'm also going to assume that the bit depth of the channels is 8-bit (value between 0 and 255, i.e. modulo 256).
Here is an implementation independent approach:
The aim is to maintain a count of all the different kinds of pixels encountered. It makes sense to use a dictionary for this, which would be of the form {pixel_value : count}.
Once this dictionary is populated, we can find the pixel with the highest count.
Now, 'pixels' are not hashable and hence cannot be stored in a dictionary directly. We need a way to assign an integer(which I'll be referring to as the pixel_value) to each unique pixel, i.e., you should be able to convert pixel_value <--> RGB value of a pixel
This function converts RGB values to an integer in the range of 0 to 16,777,215:
def get_pixel_value(pixel):
return pixel.red + 256*pixel.green + 256*256*pixel.blue
and to convert pixel_value back into RGB values:
def get_rgb_values(pixel_value):
red = pixel_value%256
pixel_value //= 256
green = pixel_value%256
pixel_value //= 256
blue = pixel_value
return [red,green,blue]
This function can find the most frequent pixel in an image:
def find_most_common_pixel(image):
histogram = {} #Dictionary keeps count of different kinds of pixels in image
for pixel in image:
pixel_val = get_pixel_value(pixel)
if pixel_val in histogram:
histogram[pixel_val] += 1 #Increment count
else:
histogram[pixel_val] = 1 #pixel_val encountered for the first time
mode_pixel_val = max(histogram, key = histogram.get) #Find pixel_val whose count is maximum
return get_rgb_values(mode_pixel_val) #Returna a list containing RGB Value of the median pixel
If you wish to find the most frequent pixel in a set of images, simply add another loop for image in image_set and populate the dictionary for all pixel_values in all images.
You can iterate over the x/y of the image.
a pixel will be img_array[x, y, :] (the : for the RBG channel)
you will add this to a Counter (from collections)
Here is an example of the concept over an Image
from PIL import Image
import numpy as np
from collections import Counter
# img_path is the path to your image
cnt = Counter()
img = Image.open(img_path)
img_arr = np.array(img)
for x in range(img_arr.shape[0]):
for y in range(img_arr.shape[1]):
cnt[str(img_arr[x, y, :])] += 1
print(cnt)
# Counter({'[255 255 255]': 89916, '[143 143 143]': 1491, '[0 0 0]': 891, '[211 208 209]': 185, ...
A More efficient way to do it is by using the power of numpy and some math manipulation (because we know values are bound [0, 255]
img = Image.open(img_path)
img_arr = np.array(img)
pixels_arr = (img_arr[:, :, 0] + img_arr[:, :, 1]*256 + img_arr[:, :, 2]*(256**2)).flatten()
cnt = Counter(pixels_arr)
# print(cnt)
# Counter({16777215: 89916, 9408399: 1491, 0: 891, 13750483: 185, 14803425: 177, 5263440: 122 ...
# print(cnt.most_common(1))
# [(16777215, 89916)]
pixel_value = cnt.most_common(1)[0][0]
Now a conversion back to the original 3 values is exactly like Aayush Mahajan have writte in his answer. But I've shorten it for the sake of simplicity:
r, b, g = pixel_value%256, (pixel_value//256)%256, pixel_value//(256**2)
So you are using the power of numpy fast computation (and it's significate improvement on run time.
You use Counter which is an extension of python dictionary, dedicated for counting.
Related
I have this set of images :
The leftmost one is the reference image.
I want to have a value telling me how close is any of the other images to the leftmost one.
I experimented with matchShapes(), by calling it for each contour and averaging the values, but I didn't get useful result (the rightmost one had a too high value, for example)
I would also want the matching to work only in the correct orientation.
If they're purely black and white images it would probably be easier to just AND the two pictures together and sum up the total pixels left in the result.
Something like this:
import cv2
import numpy as np
x = np.zeros((100,100))
y = np.zeros((100,100))
for i in range(25,75):
x[i][i] = 255
y[i][100-i] = 255
cv2.imshow('x', x)
cv2.imshow('y', y)
z = cv2.bitwise_and(x,y)
sum = 0
for i in range(0,z.shape[0]):
for j in range(0,z.shape[1]):
if z[i][j] == 255:
sum += 1
print(f"Similarity Score: {sum}")
cv2.imshow('z',z)
cv2.waitKey(0)
There probably exists some better library to perform this all in one line but if performance isn't much of a concern perhaps this could work.
It was difficult to not recognize images that were too different. With the methods proposed here, I always got really close values for images that I thought were too different to correspond.
In the end, I did a multistep process:
First I got the contour of the test image like so :
testContours, _ = cv.findContours(testImage, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)
Then, if the contour count between the test image and the original image are not the same, I abort.
If they have the same contour count, I then calculate the average between all shape distances of the contours :
distances = []
sd = cv2.createShapeContextDistanceExtractor()
for i in range(len(testContours)):
d2 = sd.computeDistance(testContours[i], originalContours[i])
distances.append(d2)
value = sum(distances) / len(distances)
Then, I count the number of white pixels after AND-ing the two images, divided by the total number of pixels in the source image (in case the contours match but are not placed correctly)
exactly_placed_ratio = cv.countNonZero(cv.bitwise_and(testImage, originalImage)) / cv.countNonZero(originalImage)
In the end I have two values, I can use the first one to check if the shapes are close enough, and the second one to check if they are in the right position relative to the whole image.
I've got a binary image where I need to select the nearest white pixel to a given set of pixel coordinates.
For example, if I click on a pixel I need to have Python search for the nearest pixel with a value greater than 0 then return that pixel's coordinates.
Any thoughts?
I figured I should add what I've done so far.
import cv2
import numpy as np
img = cv.imread('depth.png', 0) # reads image as grayscale
edges = cv2.Canny(img, 10, 50)
white_coords = np.argwhere(edges == 255) # finds all of the white pixel coordinates in the image and places them in a list.
# this is where I'm stuck
First use cv2.findNonZero to get a numpy array of coordinates of all the white pixels.
Next, calculate distance from the clicked target point (Pythagoras theorem)
Use numpy.argmin to find the position of the lowest distance.
Return the coordinates of the corresponding nonzero pixel.
Example script:
import cv2
import numpy as np
# Create a test image
img = np.zeros((1024,1024), np.uint8)
# Fill it with some white pixels
img[10,10] = 255
img[20,1000] = 255
img[:,800:] = 255
TARGET = (255,255)
def find_nearest_white(img, target):
nonzero = cv2.findNonZero(img)
distances = np.sqrt((nonzero[:,:,0] - target[0]) ** 2 + (nonzero[:,:,1] - target[1]) ** 2)
nearest_index = np.argmin(distances)
return nonzero[nearest_index]
print find_nearest_white(img, TARGET)
Which prints:
[[10 10]]
and takes around 4ms to complete, since it takes advantage of optimized cv2 and numpy functions.
Alternatively, you could go for a pure numpy solution, and as you have already attempted, use numpy.argwhere instead of cv2.findNonZero:
def find_nearest_white(img, target):
nonzero = np.argwhere(img == 255)
distances = np.sqrt((nonzero[:,0] - TARGET[0]) ** 2 + (nonzero[:,1] - TARGET[1]) ** 2)
nearest_index = np.argmin(distances)
return nonzero[nearest_index]
Which prints:
[10 10]
However, for me this is slightly slower, at around 9 ms per run.
Thoughts, yes! Start looping from the given pixel, in first iteration check for all the pixels surrounding the given pixel, if a white pixel is not found, increment the distance by 1 and check if any pixel in the circumscribing perimeter of the given pixel is white, continue looping until you find a white pixel or go out of bounds.
I am looking for a way to sum the color values of all pixels of an image. I require this to estimate the total flux of a bright source (say a distant galaxy) from its surface brightness image.
Would anyone please help me how can I sum the colour values of all pixels of an image.
For example:
Each pixel of the following image has a colour value in between 0 to 1.
But when I read the image with imread the colour values of each pixel I get is an array of 3 elements. I am very new in matplotlib and I do not know how can I convert that array to single values in the scale of 0 to 1 and add them.
If you have a PIL image, then you can convert to greyscale ("luminosity") like this:
from PIL import Image
col = Image.open('sample.jpg')
gry = col.convert('L') # returns grayscale version.
If you want ot have more control over how the colors are added, convert to a numpy array first:
arr = np.asarray(col)
tot = arr.sum(-1) # sum over color (last) axis
mn = arr.mean(-1) # or a mean, to keep the same normalization (0-1)
Or you can weight the colors differently:
wts = [.25, .25, .5] # in order: R, G, B
tot = (arr*wts).sum(-1) # now blue has twice the weight of red and green
For large arrays, this is equivalent to the last line and faster, but possibly harder to read:
tot = np.einsum('ijk, k -> ij', arr, wts)
All of the above adds up the colors of each pixel, to turn a color image into a grayscale (luminosity) image. The following will add up all the pixels together to see the integral of the entire image:
tot = arr.sum(0).sum(0) # first sums all the rows, second sums all the columns
If you have a color image, tot will still have three values. If your image is grayscale, it will be a single value. If you want the mean value, just replace sum with mean:
mn = arr.mean(0).mean(0)
I'm looping through this image pixel by pixel and it's really slow. I have the 2 images I'm comparing sliced and flattened so each element is a 3 dimensional rgb value named e1 and e2. It is very slow though. Is there some method using opencv or numpy that can speed this up?
What I'm doing here is performing pixel comparisons on images with binned colors (8 colors).
I'm reading from a jpeg though so what should be [255,0,0] becomes [230,12,11] so what clean_key does is threshold the values to the cleaner ones. Then I append the number of times this combination occurs to a dictionary. So for example dict["255,0,0 0,0,255"] might occur 300 times in this image which means there were 300 instances where im1 had a red pixel and im2 had a blue pixel.
for e1,e2 in itertools.izip(im1_slice.reshape(-1,3),im2_slice.reshape(-1,3)):
key = str(clean_key(e1_row)) + str(clean_key(e2_row))
if key in proportion_dict:
proportion_dict[key] += 1
else:
proportion_dict[key] = 1
return (proportion_dict,total)
The way you want to do this is first compare each image to the color you want to see in that image, which makes a boolean mask where that image is the given color. You don't need to flatten the images to do this. This can be done by saying:
image == color
This works fine for grayscale images, but if color is actually along a third dimension, you want to make sure everything along that dimension matches (i.e., you want all of the r, g, and b components to match, so you use np.all along the last axis (-1 gives the last axis):
np.all(image == color, axis=-1)
Which gives a 2d array of booleans where each element is True if that pixel matches color and False if not. Do this for both images (and both colors) and then you'll have a mask where the color matches both images:
np.all(im1==c1, -1) & np.all(im2==c2, -1)
This not only tells you how many pixels match, but where they are (you could plot the above line and see dot at the points where they match). If you just want the count, just use np.sum on the mask which counts True as 1, and False as 0. All together:
def compare_colors(im1, im2, c1, c2):
matches = np.all(im1==c1, -1) & np.all(im2==c2, -1)
return matches.sum()
And to use/test it with random data:
>>> a = np.random.choice([0, 255], (20,20,3))
>>> b = np.random.choice([0, 255], (20,20,3))
>>> compare_colors(a, b, [255, 0, 255], [0, 255, 0])
12
But before you do that, with your real input, you want to "clean" your colors by a threshold. You could easily do that with np.where which looks at each element of an array, and if a condition is met, gives one thing, and if not, gives another. Here, if the value is less than 128, it uses 0, and otherwise uses 255:
np.where(a<128, 0, 255)
In general, you could write a function like this, with the values above as defaults:
def clean(a, thresh=128, under=0, over=255):
return np.where(a<128, under, over)
Of course to build up your dict of counts, you still have to loop through each color combination, but that's a short loop (8*8). Here's a full run through:
# some fake data (has values between 0 and 255 for r, g, and b)
H, W = 20, 20
a = np.random.randint(0, 256, (H,W,3))
b = np.random.randint(0, 256, (H,W,3))
# clean the images:
ac = clean(a)
bc = clean(b)
# build a list of all pairs of all 8 colors using itertools.product:
col_combos = itertools.product(itertools.product((0,255), repeat=3), repeat=2)
# now apply the comparison to the images for each pair of colors
col_dict = { (c1,c2): compare_colors(ac, bc, c1, c2) for c1,c2 in col_combos }
Then, the keys for col_dict are actually tuples of tuples, which are much easier to deal with than strings, in my opinion. Here's how you'd access an example key:
>>> col_dict[((0, 255, 255), (255, 0, 255))]
8
I am using Image.point and Image.fromarray to do exactly the same operation on an image, increase the value of all pixels together by the same value. The thing is that i get to absolutelly different images.
using point
def getValue(val):
return math.floor(255*float(val)/100)
def func(i):
return int(i+getValue(50))
out = img.point(func)
using array and numpy
arr = np.array(np.asarray(img).astype('float'))
value = math.floor(255*float(50)/100)
arr[...,0] += value
arr[...,1] += value
arr[...,2] += value
out = Image.fromarray(arr.astype('uint8'), 'RGB')
I am using the same image (a jpg).
the initial image
the image with point
the image with arrays
How can they be so much different?
You have values greater than 255 in your array, which you then convert to uint8 ... what do you want those values to become in the image? If you want them to be 255, clip them first:
out_arr_clip = Image.fromarray(arr.clip(0,255).astype('uint8'), 'RGB')
By the way, there's no need to add to each color band separately:
arr = np.asarray(img, dtype=float) # also simplified
value = math.floor(255*float(50)/100)
arr += value # the same as doing this in three separate lines
If your value is different for each band, you can still do this because of broadcasting:
percentages = np.array([25., 50., 75.])
values = np.floor(255*percentages/100)
arr += values # the first will be added to the first channel, etc.
Fixxed it :)
Didn't take under consideration getting out of bounds. So i did
for i in range(3):
conditions = [arr[...,i] > 255, arr[...,i] < 0]
choices = [255, 0]
arr[...,i] = np.select(conditions, choices, default=arr[...,i]
Worked like a charm....:)