Fast RGB Thresholding in Python (possibly some smart OpenCV code?) - python

I need to do some fast thresholding of a large amount of images, with a specific range for each of the RGB channels, i.e. remove (make black) all R values not in [100;110], all G values not in [80;85] and all B values not in [120;140]
Using the python bindings to OpenCV gives me a fast thresholding, but it thresholds all three RGP channels to a single value:
cv.Threshold(cv_im,cv_im,threshold+5, 100,cv.CV_THRESH_TOZERO_INV)
cv.Threshold(cv_im,cv_im,threshold-5, 100,cv.CV_THRESH_TOZERO)
Alternatively I tried to do it manually by converting the image from PIL to numpy:
arr=np.array(np.asarray(Image.open(filename).convert('RGB')).astype('float'))
for x in range(img.size[1]):
for y in range(img.size[0]):
bla = 0
for j in range(3):
if arr[x,y][j] > threshold2[j] - 5 and arr[x,y][j] < threshold2[j] + 5 :
bla += 1
if bla == 3:
arr[x,y][0] = arr[x,y][1] = arr[x,y][2] = 200
else:
arr[x,y][0] = arr[x,y][1] = arr[x,y][2] = 0
While this works as intended, it is horribly slow!
Any ideas as to how I can get a fast implementation of this?
Many thanks in advance,
Bjarke

I think the inRange opencv method is what you are interested in. It will let you set multiple thresholds simultaneously.
So, with your example you would use
# Remember -> OpenCV stores things in BGR order
lowerBound = cv.Scalar(120, 80, 100);
upperBound = cv.Scalar(140, 85, 110);
# this gives you the mask for those in the ranges you specified,
# but you want the inverse, so we'll add bitwise_not...
cv.InRange(cv_im, lowerBound, upperBound, cv_rgb_thresh);
cv.Not(cv_rgb_thresh, cv_rgb_thresh);
Hope that helps!

You can do it with numpy in a much faster way if you don't use loops.
Here's what I came up with:
def better_way():
img = Image.open("rainbow.jpg").convert('RGB')
arr = np.array(np.asarray(img))
R = [(90,130),(60,150),(50,210)]
red_range = np.logical_and(R[0][0] < arr[:,:,0], arr[:,:,0] < R[0][1])
green_range = np.logical_and(R[1][0] < arr[:,:,0], arr[:,:,0] < R[1][1])
blue_range = np.logical_and(R[2][0] < arr[:,:,0], arr[:,:,0] < R[2][1])
valid_range = np.logical_and(red_range, green_range, blue_range)
arr[valid_range] = 200
arr[np.logical_not(valid_range)] = 0
outim = Image.fromarray(arr)
outim.save("rainbowout.jpg")
import timeit
t = timeit.Timer("your_way()", "from __main__ import your_way")
print t.timeit(number=1)
t = timeit.Timer("better_way()", "from __main__ import better_way")
print t.timeit(number=1)
The omitted your_way function was a slightly modified version of your code above. This way runs much faster:
$ python pyrgbrange.py
10.8999910355
0.0717720985413
That's 10.9 seconds vs. 0.07 seconds.

The PIL point function takes a table of 256 values for each band of the image and uses it as a mapping table. It should be pretty fast. Here's how you would apply it in this case:
def mask(low, high):
return [x if low <= x <= high else 0 for x in range(0, 256)]
img = img.point(mask(100,110)+mask(80,85)+mask(120,140))
Edit: The above doesn't produce the same output as your numpy example; I followed the description rather than the code. Here's an update:
def mask(low, high):
return [255 if low <= x <= high else 0 for x in range(0, 256)]
img = img.point(mask(100,110)+mask(80,85)+mask(120,140)).convert('L').point([0]*255+[200]).convert('RGB')
This does a few conversions on the image, making copies in the process, but it should still be faster than operating on individual pixels.

If you stick to using OpenCV, then just cv.Split the image into multiple channels first and then cv.Threshold each channel individually. I'd use something like this (untested):
# Temporary images for each color channel
b = cv.CreateImage(cv.GetSize(orig), orig.depth, 1)
g = cv.CloneImage(b)
r = cv.CloneImage(b)
cv.Split(orig, b, g, r, None)
# Threshold each channel using individual lo and hi thresholds
channels = [ b, g, r ]
thresh = [ (B_LO, B_HI), (G_LO, G_HI), (R_LO, R_HI) ]
for c, (lo, hi) in zip(channels, thresh):
cv.Threshold(ch, ch, hi, 100, cv.CV_THRESH_TOZERO_INV)
cv.Threshold(ch, ch, lo, 100, cv.CV_THRESH_TOZERO)
# Compose a new RGB image from the thresholded channels (if you need it)
dst = cv.CloneImage(orig)
cv.Merge(b, g, r, None, dst)
If your images are all the same size, then you can re-use the created images to save time.

Related

Pixel is there but .getpixel isn't detecting it

I'm currently having an issues with my program that im not too sure how to fix.
I am doing the following:
x = 0
y = 0
im = ImageGrab.grab()
time.sleep(1)
while True:
xy = (x, y)
x = x + 1
if im.getpixel(xy) == (0,158,187):
time.sleep(0.3)
pyautogui.click(x,y)
break
if x >= 1200:
x = 0
y = y + 1
print('cant find pixel')
if y >= 950:
y = 0
x = 0
And it works about 90% of the time and then theres this random time it just says it can't detect the pixel despite the pixel being there 100%.
EDIT: Managed to catch the following error in the 10% it happens:
AttributeError: 'NoneType' object has no attribute 'getpixel'
Which makes no sense since I'm doing im = ImageGrab.grab() beforehand and it works 90% of the time
You should check your ImageGrab() was successful before using the data, so something like:
im = ImageGrab.grab()
if im is not None:
processImage
You'll be there all day if you run double for loops over an image and call a function for every one! Try to get in the habit of using Numpy vectorised code for images in Python.
Basically, you appear to be testing if any pixel in your 1200x950 image matches all three RGB components (0,158,187).
You can do that with Numpy like this:
np.any(np.all(na==(0,158,187), axis=-1))
In the demo below the double for loops take 800ms and the Numpy test takes 20ms, so 40x faster.
#!/usr/bin/env python3
import numpy as np
from PIL import Image
def loopy(im):
for x in range(im.width):
for y in range(im.height):
if im.getpixel((x,y)) == crucialPixel:
return True
return False
def me(im):
# Make image into Numpy array
na = np.array(im)
# Test if there is any pixel where all RGB components match crucialPixel
return np.any(np.all(na==crucialPixel, axis=-1))
# Define our beloved crucial pixel
crucialPixel = (0,158,187)
# Construct a new, solid black image
im = Image.new('RGB', (1200,950))
# Neither should find crucialPixel in black image
result = loopy(im)
result = me(im)
# Insert the crucial pixel
im.putpixel((600,475), crucialPixel)
# Both should find crucialPixel
result = loopy(im)
result = me(im)

How Could I increase the speed

I am using below code for an image processing related study. The code works fine as functionality but it is too slow that one step takes up to 10 seconds.
I need faster process speed to reach at the aim.
import numpy
import glob, os
import cv2
import os
input = cv2.imread(path)
def nothing(x): # for trackbar
pass
windowName = "Image"
cv2.namedWindow(windowName)
cv2.createTrackbar("coef", windowName, 0, 25000, nothing)
condition = True
while (condition):
coef = cv2.getTrackbarPos("coef", windowName)
temp_img = input
row = temp_img.shape[0]
col = temp_img.shape[1]
print(coef)
red = []
green = []
for i in range(row):
for y in range(col):
# temp_img[i][y][0] = 0
temp_img[i][y][1] = temp_img[i][y][1]* (coef / 100)
temp_img[i][y][1] = temp_img[i][y][2] * (1 - (coef / 100))
# relative_diff = value_g - value_r
# temp =cv2.resize(temp,(1000,800))
cv2.imshow(windowName, temp_img)
# cv2.imwrite("output2.jpg", temp)
print("fin")
# cv2.waitKey(0)
if cv2.waitKey(30) >= 0:
condition = False
cv2.destroyAllWindows()
Is there anybody have an idea having faster result on the aim?
It's not entirely clear to me what object temp_img is exactly, but if it behaves like a numpy array, you could replace your loop by
temp_img[:,:,0] = temp_img[:,:,1]*(coef/100)
temp_img[:,:,1] = temp_img[:,:,2]*(1-coef/1000)
which should result in a significant speed up if your array is large. The implementation of such operations on arrays are optimised very well, whereas python loops are generally quite slow.
Edit based on comments:
Since you're working with large images and have some expensive operations that need an unscaled version but only need to be executed once, your code could get the following kind of structure
import... #do all your imports
def expensive_operations(image, *args, **kwargs):
#do all your expensive operations like object detection
def scale_image(image, scale):
#create a scaled version of image
def cheap_operations(scaled_image, windowName):
#perform cheap operations, e.g.
coef = cv2.getTrackbarPos("coef", windowName)
temp_img = np.copy(scaled_image)
temp_img[:,:,1] = temp_img[:,:,1]* (coef / 100)
temp_img[:,:,2] = temp_img[:,:,2] * (1 - (coef / 100))
cv2.imshow(windowName, temp_img)
input = cv2.imread(path)
windowName = "Image"
cv2.namedWindow(windowName)
cv2.createTrackbar("coef", windowName, 0, 25000, nothing)
condition = True
expensive_results = expensive_operations(input) #possibly with some more args and keyword args
scaled_image = scale_image(input)
while condition:
cheap_operations(scaled_image, windowName)
if cv2.waitKey(30) >= 0:
condition = False
cv2.destroyAllWindows()
I do this kind of thing in nip2. It's an image processing spreadsheet that can manipulate huge images quickly. It has no problems doing this kind of operation on any size image at 60fps.
I made you an example workspace: http://www.rollthepotato.net/~john/coeff.ws
Here's what it looks like working on a 1gb starfield image:
You can drag the slider to change coeff. The processed image updates instantly as you drag. You can zoom and pan around the processed image to check details and adjust coeff.
The underlying image processing library is libvips, which has a Python binding, pyvips. In pyvips, your program would be:
import pyvips
def adjust(image, coeff):
return image * [1, coeff / 100, 1 - coeff / 100]
Though that's without the GUI elements, of course.

I would like to know how to calculate the percentage of a color in an image

I would like to know how to calculate the percentage of a color in an image, the image below represents 100%:
already this, when the level decreases:
I wanted to learn correctly how do I get the percentage that the bar has at the moment, I tried to use the Matplotlib library, but I could not get the expected result, could anyone help me please? I do not need something ready, someone to teach me ...
I think you want to calculate the progress by looking at the image
I'm not sure if there's a library to this specific thing but here's my simple approach to it,
you can compare images to get until which column they are similar and then can calculate the % task done, let me demonstrate..
!wget https://i.stack.imgur.com/jnxX3.png
a = plt.imread( './jnxX3.png')
plt.imshow( a )
This shall load the image with 100% completion in variable a
c =a
c = c[: , 0:c.shape[1] - 50]
aa = np.zeros( dtype= float , shape=( 11,50, 3 ))
c = np.append( c, aa , axis= 1 )
plt.imshow( c)
plt.imshow( c )
made a sample incomplete image which you should have provided
def status( complete_img , part_image):
"""inputs must be numpy arrays """
complete_img = complete_img[:, 1: ] # as the first pixel column doesn't belong to % completion
part_image = part_image[:, 1:]
counter = 0
while(counter < part_image.shape[1] and counter < complete_img.shape[1]):
if (complete_img[:, counter ] == part_image[:,counter]).all():
counter += 1
else :
break
perc = 100*( float(counter) / complete_img.shape[1])
return
status( a ,c ) # this will return % columns similar in the two images
A proposition:
import numpy as np
from PIL import Image
from urllib.request import urlopen
full = np.asarray(Image.open(urlopen("https://i.stack.imgur.com/jnxX3.png")))
probe = np.asarray(Image.open(urlopen("https://i.stack.imgur.com/vx5zt.png")))
# crop the images to the same shape
# (this step should be avoided, best compare equal shaped arrays)
full = full[:,1:probe.shape[1]+1,:]
def get_percentage(full, probe, threshold):
def profile_red(im):
pr = im[:,:,0] - im[:,:,1]
return pr[pr.shape[0]//2]
def zero(arr):
z = np.argwhere(np.abs(np.diff(np.sign(arr))).astype(bool))
if len(z):
return z[0,0]
else:
return len(arr)
full_red = profile_red(full)
probe_red = profile_red(probe)
mask = full_red > threshold
diff = full_red[mask] - probe_red[mask]
x0 = zero(diff - threshold)
percentage = x0 / diff.size * 100
err = 2./diff.size * 100
return percentage, err
print("{:.1f} p\m {:.1f} %".format(*get_percentage(full, probe, 75.0)))
Result:
94.6 p\m 2.2 %
You're looking for the Pillow library. There are two ways to measure color, Hue, Saturation, Luminance (HSL), and Red, Blue, Green (RGB). There are functionsto do both in the library.

count of internal tables in a tabular image using python

I have been given an image ( a table which is in rectangular shape) and it is again divided into some internal tables. The question is to get the count of those internal tables using python. Could anyone suggest me how to proceed? Check the below sample image
example_image
I need to get count = 78 tables
You can convert the image to binary, and compute sum of pixel intensities over image rows, and count peaks in it, then compute the sum of pixel intensities over image columns, and count peaks in it. Those would be the number of horizontal and vertical lines found in the image. Then N = (num_of_horizontal_lines - 1) * (num_of_vertical_lines - 1).
Example code (using numpy and Pillow):
>>> from PIL import Image
>>> import numpy as np
>>> im = np.array(Image.open('cJyt5.png').convert('L'))
>>> threshold = np.histogram(im, 2)[1][1]
>>> im2 = im < threshold
>>> vert_lines = sum(np.mean(im2, axis=0) > 0.5)
>>> horiz_lines = sum(np.mean(im2, axis=1) > 0.5)
>>> (vert_lines - 1) * (horiz_lines - 1)
78
If the lines are more than 1 pixel thick, you need to slightly change the code to not take into account multiple consecutive True values in np.mean(...) > 0.5.
A simple trick would be to do sum(np.max(0, np.diff(np.mean(...) > 0.5))) to count the number of changes from False to True. (not tested)
I came up with a solution that works for your example image, but I cannot guarantee that it'll work for others, as it is untested and may be fragile.
Explanation:
Converts image to binary by setting all non-white values to 0 and all white values to 1.
Looks for the first horizontal and vertical line of pixels that contain both 0s and 1s.
Counts the "squashed" number of pixels with a value of 0 in that line and subtracts 1.
Multiplies the horizontal line value from step 3 by the vertical line value from step 3 to get the number of cells.
Dependencies:
PIL (pip install Pillow)
Execution:
python main.py ./path/to/grid.png
from collections import Counter
from itertools import dropwhile, groupby
import sys
from PIL import Image
def convert_to_binary(im):
# first converts to 8-bit values, then to binary.
return im.convert('L').point(lambda v: 0 if v < 255 else 1, mode='1')
def first_intersection(pixels2d):
# if it only has one pixel value it's a border
return next(dropwhile(lambda x: len(set(x)) == 1, pixels2d))
def cells_intersected(pixels1d):
# count the number of non-consecutive 0s and subtracts 1.
return Counter([g[0] for g in groupby(pixels1d)])[0] - 1
def transpose(lst):
return list(map(list, zip(*lst)))
def reshape2d(lst, size):
return [lst[i:i + size] for i in range(0, len(lst), size)]
def main():
with Image.open(sys.argv[1]) as img:
bin_data = list(convert_to_binary(img).getdata())
assert(sorted(Counter(bin_data).values()) ==
sorted(Counter(img.getdata()).values()))
bin_lst = reshape2d(bin_data, img.width)
horizontal = first_intersection(bin_lst)
vertical = first_intersection(transpose(bin_lst))
print(cells_intersected(horizontal) * cells_intersected(vertical))
if __name__ == '__main__':
main()

Use diagonal fill to eliminate 8-connectivity of the background in Python (similar to bwmorph diag in MATLAB)

I'm looking for a way to connect 8-connected pixels in Python, similar to MATLAB's bwmorph 'diag' function:
BW = bwmorph(BW, 'diag')
For example,
0 1 0 0 1 0
1 0 0 -> 1 1 0
0 0 0 0 0 0
Thanks in advance!
Misha
That works, thanks! Here's the python code:
def bwmorphDiag(bw):
# filter for 8-connectivity of the background
f = np.array(([1, -1, 0],[-1, 1, 0],[0, 0, 0]),dtype = np.int)
# initialize result with original image
bw = bw.astype(np.int)
res2 = bw.copy().astype(np.bool)
for ii in range(4): # all orientations
# add results where sum equals 2 -> two background pixels on the
# diagonal with 2 foreground pixels on the crossing mini-anti-diagonal
res2 = res2 | (ndimage.filters.convolve(np.invert(bw),f) == 2)
f = np.rot90(f) # rotate filter to next orientation
return res2
you can achieve the same result using simple image filtering. I did it in MATLAB, but it should be straight forward to do it in python as well:
% random binary image
bw = rand(50) > 0.5;
% result using bwmorph(bw,'diag')
res1 = bwmorph(bw,'diag');
% filter for 8-connectivity of the background
f = [1 -1 0;-1 1 0;0 0 0];
% initialize result with original image
res2 = bw;
for ii = 1:4 % all orientations
% add results where sum equels 2 -> two background pixels on the
% diagonal with 2 foreground pixels on the crossing mini-anti-diagonal
res2 = res2 | ( imfilter(double(~bw),f) == 2 );
f = rot90(f); % rotate filter to next orientation
end
isequal(res2,res1) % yes
I was actually looking for the same python equivalent, the bwmorph('diag') of MATLAB. But since I couldn't find it I eventually decided to code it. Please check the MATLAB help for bwmorph and the diag option to get further info about what it does.
import numpy as np
import scipy.ndimage.morphology as smorph
import skimage.morphology as skm
class bwmorph:
#staticmethod
def diag(imIn):
strl = np.array([
[[0,1,0],[1,0,0],[0,0,0]],
[[0,1,0],[0,0,1],[0,0,0]],
[[0,0,0],[1,0,0],[0,1,0]],
[[0,0,0],[0,0,1],[0,1,0]],
[[0,1,0],[1,0,0],[0,1,0]],
[[0,1,0],[1,0,1],[0,0,0]],
[[0,1,0],[0,0,1],[0,1,0]],
[[0,0,0],[1,0,1],[0,1,0]]
],dtype=np.uint8)
bwIm = np.zeros(imIn.shape,dtype=int)
imIn = np.array(imIn)
imIn = imIn/np.max(np.max(imIn)) #normalizing to be added later
for i in range(7):
bwIm = bwIm + smorph.binary_hit_or_miss(imIn,strl[i,:,:])
bwIm = ((bwIm>0) + imIn)>0
return bwIm # out put is boolean
I used 'hit or miss' transform, with the structural element 'strl' defined at the beginning. I guess that's a classic way to do it.
Please watch the #staticmethod is you're running it on older python.
Usage example would be bwmorph().diag(BinaryImage)
All the best ;)

Categories