'Stretching' histograms (levels) in Numpy, Python - python

I have grayscale image whose background is, on a 0-255 color scale, a mid-white color with an average pixel color value of 246; the foreground is mid-grey with an average pixel-color value of 186.
I would like to 'shift' every pixel above 246 to 255, every pixel below 186 to zero, and 'stretch' everything between. Is there any ready-made algorithm/process to do this in numpy or python, or must the new levels/histogram be calculated 'manually' (as I have done thus far)?
This is the equivalent of, in Gimp or Photoshop, opening the levels window and selecting, with the white and black eyedropper respectively, a light region we want to make white and a darker region we want to make black: the application modifies the levels/histogram ('stretches' the values between the points selected) accordingly.
Some images of what I'm attempting:

Here's one way -
def stretch(a, lower_thresh, upper_thresh):
r = 255.0/(upper_thresh-lower_thresh+2) # unit of stretching
out = np.round(r*(a-lower_thresh+1)).astype(a.dtype) # stretched values
out[a<lower_thresh] = 0
out[a>upper_thresh] = 255
return out
As per OP, the criteria set was :
'shift' every pixel above 246 to 255, hence 247 and above should become 255.
every pixel below 186 to zero, hence 185 and below should become 0.
Hence, based on above mentioned two requirements, 186 should become something greater than 0 and so on, until 246 which should be lesser than 255.
Alternatively, we can also use np.where to make it a bit more compact -
def stretch(a, lower_thresh, upper_thresh):
r = 255.0/(upper_thresh-lower_thresh+2) # unit of stretching
out = np.round(r*np.where(a>=lower_thresh,a-lower_thresh+1,0)).clip(max=255)
return out.astype(a.dtype)
Sample run -
# check out first row input, output for variations
In [216]: a
Out[216]:
array([[186, 187, 188, 246, 247],
[251, 195, 103, 9, 211],
[ 21, 242, 36, 87, 70]], dtype=uint8)
In [217]: stretch(a, lower_thresh=186, upper_thresh=246)
Out[217]:
array([[ 4, 8, 12, 251, 255],
[255, 41, 0, 0, 107],
[ 0, 234, 0, 0, 0]], dtype=uint8)

If your picture is uint8 and typical picture size, one efficient method is setting up a lookup table:
L, H = 186, 246
lut = np.r_[0:0:(L-1)*1j, 0.5:255.5:(H-L+3)*1j, 255:255:(255-H-1)*1j].astype('u1')
# example
from scipy.misc import face
f = face()
rescaled = lut[f]
For smaller images it is faster (on my setup it crosses over at around 100,000 gray scale pixels) to transform directly:
fsmall = (f[::16, ::16].sum(2)//3).astype('u1')
slope = 255/(H-L+2)
rescaled = ((1-L+0.5/slope+fsmall)*slope).clip(0, 255).astype('u1')

Related

Increasing the speed of the color space conversion algorithm from HSV to RGB

I work on image pixels. I want to colorize my image in a specific formula and convert and save the image into RGB space after working with HSV. Opencv has functions to convert color spaces, But the color of the image changes my image. and I use the function to convert the pixel pixel, but my loop has a lot of time to run.
I wrote a piece of code in Python that contains a loop that takes a lot of time to execute.
What operational solutions do you have to reduce execution time?
I used the thread, but it is not implemented correctly and the time does not decrease.
Also, I used the np.apply_along_axis, but, this function increased the run time!
for i in range(row):
for j in range(col):
final[i][j][0], final[i][j][1], final[i][j][2] = colorir.HSV(final[i][j][0], final[i][j][1], final[i][j][2], max_sva=255).rgb()
When I convert the color with this code, my image is displayed in the correct color that I want, but when I use the following function, the image coloring is completely wrong:
final = cv2.cvtColor(final,cv2.COLOR_HSV2RGB)
or
final = matplotlib.colors.hsv_to_rgb(final)
Another question that occurred to me is that Is there a way to save an image that is in color space HSV without converting it to color space RGB? Without coloring it wrong? So that I don't have to use this snippet of runtime code above to convert?
Edit:
My image size is variable: for example 600x600
The execution time of these loops is about 15 seconds, which should be approximately less than 1 second.
I color the gray level image.
Below is my executable code:
import numpy as np
from numpy import newaxis
import cv2
from colorir import HSV, sRGB
zero1 = np.zeros((row, col), dtype=int)
new1 = np.dstack((zero1, zero1, num12))
new1 = np.where(num12 > 250, 0, num12)
newww = np.where(new1 < 0, 0, new1)
minus = np.subtract(num1, num2)
minus_2 = minus
minus = np.where(minus <=0, 33, minus)
minus = np.where(num2 >= np.multiply(num1 , 1.1), 33, minus)
minus = np.where(np.logical_and(num2 <= np.multiply(num1 , 1.1),num2 >= np.multiply(num1 , 1)), 107, minus)
minus = np.where(num2 < np.multiply(num1 , 1), 209, minus)
a_255 = np.full([row, col], 255, dtype=int)
final = np.dstack((minus, newww, a_255))
for i in range(row):
for j in range(col):
final[i][j][0], final[i][j][1], final[i][j][2] = HSV(final[i][j][0], final[i][j][1], final[i][j][2], max_sva=255).rgb()
My final image should only contain the green, blue, and orange colors I specified, but the image colored by functions 1 is pink and yellow, and unrelated colors.
A small example:
final = [[[105, 213, 235], [105, 213, 235], [105, 213, 235], [105, 213, 235], [105, 213, 235], [105, 213, 235]]]
final = np.asarray(final)
final = cv2.cvtColor(final,cv2.COLOR_HSV2BGR)
cv2.imshow("image", final)
cv2.waitKey(0)
When I run above sample code, with cv2.cvtColor(final,cv2.COLOR_HSV2BGR), I encounter:
error (Unsupported depth of input image: 'VDepth::contains(depth)' where 'depth' is 4 (CV_32S))
I have to use np.float32, but np.float32 color will spoil the final image.
There are two issues here:
OpenCV uses a range of 0-180 for the hue channel (because 360 doesn't fit in an 8-bit integer).
OpenCV defaults to BGR, but if you want RGB, you need to use cv2.COLOR_HSV2RGB, not cv2.COLOR_HSV2BGR.
Here is my version of your example at the end:
import numpy as np
import cv2
from colorir import HSV, sRGB
hsv = [[[105, 213, 235], [105, 213, 235], [105, 213, 235]]]
hsv = np.asarray(hsv)
rgb_colorir = np.zeros_like(hsv)
for i in range(hsv.shape[0]):
for j in range(hsv.shape[1]):
rgb_colorir[i][j][0], rgb_colorir[i][j][1], rgb_colorir[i][j][2] = HSV(hsv[i][j][0], hsv[i][j][1], hsv[i][j][2], max_sva=255).rgb()
hsv[:,:,0] //= 2 # OpenCV's different definition
hsv = hsv.astype(np.uint8) # OpenCV requires uint8
rgb_opencv = cv2.cvtColor(hsv, cv2.COLOR_HSV2RGB)
Examining the converted images:
>>> rgb_colorir
array([[[ 88, 235, 39],
[ 88, 235, 39],
[ 88, 235, 39]]])
>>> rgb_opencv
array([[[ 91, 235, 39],
[ 91, 235, 39],
[ 91, 235, 39]]], dtype=uint8)
So we can see that there's a small difference in the red channel, which is likely caused by rounding errors. For the OpenCV case we had to round the hue to the nearest even number of degrees (because the integer divide by 2). It should be fairly difficult to see this difference.
If you want to use the more precise conversion, note that colorir.HSV is just calling colorsys.hsv_to_rgb, and doing a whole lot of extra work like building an object. But it doesn't give you anything extra except a normalization. Using colorsys.hsv_to_rgb directly should be faster. Also, colorsys is part of the standard library, using it directly should be preferred.
Use np.apply_along_axis() to iterate over your image. This should be relatively fast, but it will not be as fast as the OpenCV solution.
import colorsys
rgb_colorsys = np.apply_along_axis(
lambda hsv: colorsys.hsv_to_rgb(hsv[0] / 360, hsv[1] / 255, hsv[2] / 255),
2,
hsv,
) * 255
The result:
>>> rgb_colorsys
array([[[ 87.77941176, 235. , 38.70588235],
[ 87.77941176, 235. , 38.70588235],
[ 87.77941176, 235. , 38.70588235]]])
Of course there are other libraries you could try, such as scikit-image:
import skimage.color
rgb_skimage = skimage.color.hsv2rgb(hsv / [360, 255, 255]) * 255
Scikit-image also has a different definition for how it stores the HSV values. It also works in floating-point format, to avoid rounding errors. Result:
>>> rgb_skimage
array([[[ 87.77941176, 235. , 38.70588235],
[ 87.77941176, 235. , 38.70588235],
[ 87.77941176, 235. , 38.70588235]]])
With DIPlib (disclosure: I'm an author):
import diplib as dip
hsv_dip = dip.Convert(dip.Image(hsv, tensor_axis=2), "SFLOAT")
hsv_dip.SetColorSpace("HSV")
rgb_dip = dip.ColorSpaceManager.Convert(hsv_dip / [1, 255, 1], "RGB")
Result:
>>> np.asarray(rgb_dip)
array([[[ 87.77941176, 235. , 38.70588235],
[ 87.77941176, 235. , 38.70588235],
[ 87.77941176, 235. , 38.70588235]]])
Notice how OpenCV is the only implementation that has significant rounding errors, the others produce identical values.
For the floating-point results, np.round(...).astype(np.uint8) will get you an RGB image that you can display normally in pyplot:
>>> np.round(rgb_dip).astype(np.uint8)
array([[[ 88, 235, 39],
[ 88, 235, 39],
[ 88, 235, 39]]], dtype=uint8)

Check if a color is an interpolation of other two colors

Here is my current code:
from PIL import ImageColor
import numpy as np
a=0
colors = [
([17, 15, 100], [50, 56, 200]),
([86, 31, 4], [220, 88, 50]),
([25, 146, 190], [62, 174, 250]),
([103, 86, 65], [145, 133, 128])
]
for (low, high) in colors:
low = np.array(low, dtype="uint8")
high = np.array(high, dtype="uint8")
#if np.array(ImageColor.getcolor('#300103', "RGB"), dtype="uint8") is in between low and high, a=1
and I want to know if a certain color is in between two colors, sort of like cv2.inRange(), but instead of seeing if a picture's pixels are in between two colors and keeping them in the picture if so, I want to see if a single color is inbetween two colors.
A color "in between" two others is a bit problematic, because there is not a single definition for that: if vary colors in the RGB space, as is, in practice, defined in your question, it is a matter of school mathematics: taking the first component (red), check the relative distance between the two colors at the ends (50 - 17 = 33, for your first line), pick the red component of the color being checked, see its distance to the lower of those - for the color #300103, the red component would be "48" in decimal - 48 - 17 = 31. use the factor 31/33, and check if the other 2 components are at the same proportional distance from the respective component in (17, 15, 100).
One might use math.close and allow a fluctuation of 1 or 2 units per component, due to rounding errors.
However, if you might have a set of colors that lie on the gray spectrum attending this criteria, and which have no resemblance to the colors at either edge. (say, if you start with (255, 0, 0) and (0, 0, 255), (128, 0, 128) will match, but it has half the saturation than either color.
Using an HSV color space, rather than RGB would probably get you to colors that perceptually attend better as "in between". But then without knowing your real objective it is hard to say if it makes any sense - for the example above, it would be even worse, as it would match green and other full saturated colors.
I will beg your pardon, but I won't fill in the exact code for the RGB interpolation here, it should not be hard - but it will take sometime to get right - and it might not work for you at all.

Replace tridimensional values with numpy with 0 in a colored image [duplicate]

I know this has been asked before but there doesn't seem to be anything for my specific use-case.
I have a numpy array obs which represents a color image and has shape (252, 288, 3).
I want to convert every pixel that is not pure black to pure white.
What I have tried is obs[obs != [0, 0, 0]] = [255, 255, 255] but it gives the following exception:
ValueError: NumPy boolean array indexing assignment cannot assign 3 input values to the 807 output values where the mask is true
The result is the same withobs[obs[:, :] != [0, 0, 0]] = [255, 255, 255]. Also, (obs[:, :] != [0, 0, 0]).shape is (252, 288, 3) and I do not understand why it isnt simply (252, 288) (a matrix of bools).
I thought about using obs[obs != 0] = 255 but that would not have the effect I want since a pixel that is pure green ([0, 255, 0]) would be processed component wise and would still be [0, 255, 0] after the filtering, instead of being actually white ([255, 255, 255]).
Why isn't what I have tried up until now working and how should I go about this?
Boolean indexing like obs[obs != [0, 0, 0]] return a 1D array with all the elements from obs that satisfy the given condition.
Look at the follwoing example:
obs = np.array([
[[88, 0,99],
[ 0, 0, 0]],
[[ 0, 0, 0],
[88,77,66]]
])
obs != [0, 0, 0] returns a boolean array:
array([[[ True, False, True],
[False, False, False]],
[[False, False, False],
[ True, True, True]]])
and obs[obs != [0, 0, 0]] then returns a 1D array with all the elements where the mask is True: array([88, 99, 88, 77, 66]).
So what you need is where to test if there's any color component not equal 0:
np.where(obs.any(axis=-1, keepdims=True), 255, obs)
Result:
array([[[255, 255, 255],
[ 0, 0, 0]],
[[ 0, 0, 0],
[255, 255, 255]]])
Note that you need keepdims=True to enable broadcasting to the original shape of obs. Otherwise you'd have to add the lost dimension by np.where(obs.any(-1)[...,np.newaxis], 255, obs) or np.where(np.atleast_3d(obs.any(-1)), 255, obs) which is less elegant.
There are a number of possibilities depending what you actually want to do. Let's do the set-up code (which is common to all possibilities) first so you can see what I mean.
#!/usr/bin/env python3
import numpy as np
# Make a repeatable random image
np.random.seed(764)
obs = np.random.randint(0,32,(252,288,3), dtype=np.uint8)
This image happens to have pure black pixels at the following locations for test purposes:
obs[ 21, 267]
obs[ 28, 252]
obs[ 69, 127]
obs[ 98, 0]
obs[124, 210]
obs[133, 98]
obs[160, 81]
obs[167, 48]
obs[217, 237]
Now, suppose you want a new, pure True/False boolean mask of black pixels, you can use:
mask = obs.any(axis=-1)
That solution has the following characteristics:
time: 876 µs
mask.shape: (252,288)
mask.nbytes: 72576
mask.dtype: 'bool'
You can subsequently use and re-use that mask like this:
# Make masked pixels red
obs[mask,:] = [255,0,0]
# Make unmasked pixels cyan
obs[~mask,:] = [0,255,255]
Now let's suppose you want a new, greyscale image, with black and white pixels, you can use:
grey = obs.any(axis=-1) * np.uint8(255)
That solution has the following characteristics:
time: 887 µs
grey.shape: (252,288)
grey.nbytes: 72576
grey.dtype: np.uint8
Now suppose you want in-place alteration of your already existing "obs" to pure black and white (but still RGB):
obs[obs.any(axis=-1),:] = [255,255,255]
That solution has the following characteristics:
time: 1.98 ms
obs.shape: (252,288,3)
obs.nbytes: 217728
obs.dtype: np.uint8

open cv: add with numpy with maximum value [duplicate]

I am trying to increase brightness of a grayscale image. cv2.imread() returns a numpy array. I am adding integer value to every element of the array. Theoretically, this would increase each of them. After that I would be able to put upper threshold of 255 and get the image with the higher brightness.
Here is the code:
grey = cv2.imread(path+file,0)
print type(grey)
print grey[0]
new = grey + value
print new[0]
res = np.hstack((grey, new))
cv2.imshow('image', res)
cv2.waitKey(0)
cv2.destroyAllWindows()
However, numpy addition apparently does something like that:
new_array = old_array % 256
Every pixel intensity value higher than 255 becomes a remainder of dividing by 256.
As a result, I am getting dark instead of completely white.
Here is the output:
<type 'numpy.ndarray'>
[115 114 121 ..., 170 169 167]
[215 214 221 ..., 14 13 11]
And here is the image:
How can I switch off this remainder mechanism? Is there any better way to increase brightness in OpenCV?
One idea would be to check before adding value whether the addition would result in an overflow by checking the difference between 255 and the current pixel value and checking if it's within value. If it does, we won't add value, we would directly set those at 255, otherwise we would do the addition. Now, this decision making could be eased up with a mask creation and would be -
mask = (255 - grey) < value
Then, feed this mask/boolean array to np.where to let it choose between 255 and grey+value based on the mask.
Thus, finally we would have the implementation as -
grey_new = np.where((255 - grey) < value,255,grey+value)
Sample run
Let's use a small representative example to demonstrate the steps.
In [340]: grey
Out[340]:
array([[125, 212, 104, 180, 244],
[105, 26, 132, 145, 157],
[126, 230, 225, 204, 91],
[226, 181, 43, 122, 125]], dtype=uint8)
In [341]: value = 100
In [342]: grey + 100 # Bad results (e.g. look at (0,1))
Out[342]:
array([[225, 56, 204, 24, 88],
[205, 126, 232, 245, 1],
[226, 74, 69, 48, 191],
[ 70, 25, 143, 222, 225]], dtype=uint8)
In [343]: np.where((255 - grey) < 100,255,grey+value) # Expected results
Out[343]:
array([[225, 255, 204, 255, 255],
[205, 126, 232, 245, 255],
[226, 255, 255, 255, 191],
[255, 255, 143, 222, 225]], dtype=uint8)
Testing on sample image
Using the sample image posted in the question to give us arr and using value as 50, we would have -
Here is another alternative:
# convert data type
gray = gray.astype('float32')
# shift pixel intensity by a constant
intensity_shift = 50
gray += intensity_shift
# another option is to use a factor value > 1:
# gray *= factor_intensity
# clip pixel intensity to be in range [0, 255]
gray = np.clip(gray, 0, 255)
# change type back to 'uint8'
gray = gray.astype('uint8)
Briefly, you should add 50 to each value, find maxBrightness, then thisPixel = int(255 * thisPixel / maxBrightness)
You have to run a check for an overflow for each pixel. The method suggested by Divakar is straightforward and fast. You actually might want to increment (by 50 in your case) each value and then normalize it to 255. This would preserve details in bright areas of your image.
Use OpenCV's functions. They implement "saturating" math.
new = cv.add(grey, value)
Documentation for cv.add
When you only write new = grey + value, that isn't OpenCV doing the work, that is numpy doing the work. And numpy does nothing special. Wrap-around for integers is standard behavior.
An alternate approach that worked efficiently for me is to "blend in" a white image to the original image using the blend function in the PIL>Image library.
from PIL import Image
correctionVal = 0.05 # fraction of white to add to the main image
img_file = Image.open(location_filename)
img_file_white = Image.new("RGB", (width, height), "white")
img_blended = Image.blend(img_file, img_file_white, correctionVal)
img_blended = img_file * (1 - correctionVal) + img_file_white * correctionVal
Hence, if correctionVal = 0, we get the original image, and if correctionVal = 1, we get pure white.
This function self-corrects for RGB values exceeding 255.
Blending in black (RGB 0, 0, 0) reduces brightness.
I ran into a similar issue, but instead of addition, it was scaling image pixels in non-uniform manner.
The 1-D version of this:
a=np.array([100,200,250,252,255],dtype=np.uint8)
scaling=array([ 1.1, 1.2, 1.4, 1.2, 1.1])
result=np.uint8(a*scaling)
This gets you the overflow issue, of course; the result:
array([110, 240, 94, 46, 24], dtype=uint8)
The np.where works:
result_lim=np.where(a*scaling<=255,a*scaling,255)
yields result_lim as:
array([ 110., 240., 255., 255., 255.])
I was wondering about timing, I did this test on a 4000 x 6000 image (instead of 1D array), and found the np.where(), at least for my conditions, took about 2.5x times as long. Didn't know if there was a better/faster way of doing this. The option of converting to float, doing the operation, and then clipping as noted above was a bit slower than the np.where() method.
Don't know if there are better methods for this.

How to get pixel coordinates if I know color(RGB)?

I use Python, opencv and PIL.
image = cv2.imread('image.jpg')
color = (235, 187, 7)
How can I get pixel coordinates(x, y) if I know pixels color?
Here is a numpythonic solution. Numpy library speeds up operations wherever possible.
Assuming the color to be: color = (235, 187, 7)
indices = np.where(img == color)
I used the numpy.where() method to retrieve a tuple indices of two arrays where the first array contains the x-coordinates of the pixels of color (235, 187, 7) and the second array contains the y-coordinates of those pixels.
Now indices returns something like the following:
(array([ 81, 81, 81, ..., 304, 304, 304], dtype=int64),
array([317, 317, 317, ..., 520, 520, 520], dtype=int64),
array([0, 1, 2, ..., 0, 1, 2], dtype=int64))
I then used the zip() method to get a list of tuples containing those points.
coordinates = zip(indices[0], indices[1])
But if you notice since this is a color image with three channels each coordinate will be repeated thrice. We have to keep only the unique coordinates. This can be accomplished using set() method.
unique_coordinates = list(set(list(coordinates)))
Try something like:
color = (235, 187, 7)
im = Image.open('image.gif')
rgb_im = im.convert('RGB')
for x in range(rgb_im.size()[0]):
for y in range(rgb_im.size()[1]):
r, g, b = rgb_im.getpixel((x, y))
if (r,g,b) == colour:
print(f"Found {colour} at {x},{y}!")
But getpixel can be slow, so look at using pixel access objects.
Also note that the value returned can depend on the image type. For example, a single value is returned with pix[1, 1] because GIF pixels refer to one of the 256 values in the GIF color palette.
See also this SO post: Python and PIL pixel values different for GIF and JPEG and this PIL Reference page contains more information on the convert() function.
By the way, your code would work just fine for .jpg images.
you can use following:
import numpy as np
# for color image
color = (75, 75, 75)
pixels = np.argwhere(img == color)
output(it repeats the same coordinates three times(number of colors)):
[[ 0 28 0]
[ 0 28 1]
[ 0 28 2]
[ 0 54 0]
[ 0 54 1]
[ 0 54 2]
................]
to avoid it do following(sorry for code readability):
pixels = pixels[::3][:, [0, 1]]
output:
[[ 0 28]
[ 0 54]
...........]
for gray scale image it looks better:
color = (75)
pixels = np.argwhere(img == color)
output:
[[ 0 28]
[ 0 54]
...........]
import PIL #The reason I use PIL and not opencv is that I find pillow
#(which is imported with 'PIL') a very useful library for image editing.
image = PIL.Image.open('Name_image') #the image is opened and named image
f = image.load() #I'm not sure what the load() operation exactly does, but it
# is necesarry.
color = (235, 187, 7) # the Red Green Blue values that you want to find the
#coordinates of
PixelCoordinates = [] # List in which all pixel coordinates that match
#requirements will be added.
#The lines of code below check for each pixel in the image if the RGB-values
# are equal to (235, 187, 7)
for x in image.size[0]:
for y in image.size[1]:
if f[x,y] == color:
PixelCoordinates.append([x,y])
Here is a solution using cv2 library only
import cv2
blue = int(input("Enter blue value: "))
green = int(input("Enter green value: "))
red = int(input("Enter red value: "))
path = str(input("Enter image path with image extension:"))
img = cv2.imread(path)
img= cv2.resize(img,(150,150))
x,y,z = img.shape
for i in range(x):
for j in range(y):
if img[i,j,0]==blue & img[i,j,1]==green & img[i,j,1]==red:
print("Found color at ",i,j)

Categories