Question

I'm working on teaching myself the basics of computerized image processing, and I am teaching myself Python at the same time.

Given an image x of dimensions 2048x1354 with 3 channels, efficiently calculate the histogram of the pixel intensities.

import numpy as np, cv2 as cv

img = cv.imread("image.jpg")
bins = np.zeros(256, np.int32)

for i in range(0, img.shape[0]):
    for j in range(0, img.shape[1]):

        intensity = 0
        for k in range(0, len(img[i][j])):
            intensity += img[i][j][k]

        bins[intensity/3] += 1

print bins

My issue is that this code runs pretty slowly, as in ~30 seconds. How can I speed this up and be more Pythonic?

No correct solution

OTHER TIPS

You can use newer OpenCV python interface which natively uses numpy arrays and plot the histogram of the pixel intensities using matplotlib hist. It takes less than second on my computer.

import matplotlib.pyplot as plt
import cv2

im = cv2.imread('image.jpg')
# calculate mean value from RGB channels and flatten to 1D array
vals = im.mean(axis=2).flatten()
# plot histogram with 255 bins
b, bins, patches = plt.hist(vals, 255)
plt.xlim([0,255])
plt.show()

enter image description here

UPDATE: Above specified number of bins not always provide desired result as min and max are calculated from actual values. Moreover, counts for values 254 and 255 are summed in last bin. Here is updated code which always plot histogram correctly with bars centered on values 0..255

import numpy as np
import matplotlib.pyplot as plt
import cv2

# read image
im = cv2.imread('image.jpg')
# calculate mean value from RGB channels and flatten to 1D array
vals = im.mean(axis=2).flatten()
# calculate histogram
counts, bins = np.histogram(vals, range(257))
# plot histogram centered on values 0..255
plt.bar(bins[:-1] - 0.5, counts, width=1, edgecolor='none')
plt.xlim([-0.5, 255.5])
plt.show()

enter image description here

If you just want to count the number of occurences of each value in an array, numpy can do that for you using numpy.bincount. In your case:

arr  = numpy.asarray(img)
flat = arr.reshape(numpy.prod(arr.shape[:2]),-1)
bins = numpy.bincount(np.sum(flat,1)/flat.shape[1],minsize=256)

I'm using numpy.asarray here to make sure that img is a numpy array, so I can flatten it to the one-dimensional array bincount needs. If img is already an array, you can skip that step. The counting itself will be very fast. Most of the time here will probably be spent in converting the cv matrix to an array.

Edit: According to this answer, you may need to use numpy.asarray(img[:,:]) (or possibly img[:,:,:]) in order to successfully convert the image to an array. On the other hand, according to this, what you get out from newer versions of openCV is already a numpy array. So in that case you can skip the asarray completely.

it's impossible to do this(i.e without removing the for loop) in pure python. Python's for loop construct has too many things going on to be fast. If you really want to keep the for loop, the only solution is numba or cython but these have their own set of issues. Normally, such loops are written in c/c++(most straightforward in my opinion) and then called from python, it's main role being that of a scripting language.

Having said that, opencv+numpy provides enough useful routines so that in 90% of cases, it's possible to simply use built in functions without having to resort to writing your own pixel level code.

Here's a solution in numba without changing your looping code. on my computer it's about 150 times faster than pure python.

import numpy as np, cv2 as cv

from time import time
from numba import jit,int_,uint8 

@jit(argtypes=(uint8[:,:,:],int_[:]),
    locals=dict(intensity=int_),
    nopython=True
    )
def numba(img,bins):
    for i in range(0, img.shape[0]):
        for j in range(0, img.shape[1]):
            intensity = 0
            for k in range(0, len(img[i][j])):
                intensity += img[i][j][k]
            bins[intensity/3] += 1


def python(img,bins):
    for i in range(0, img.shape[0]):
        for j in range(0, img.shape[1]):
            intensity = 0
            for k in range(0, len(img[i][j])):
                intensity += img[i][j][k]
            bins[intensity/3] += 1

img = cv.imread("image.jpg")
bins = np.zeros(256, np.int32)

t0 = time()
numba(img,bins)
t1 = time()
#print bins
print t1 - t0

bins[...]=0
t0 = time()
python(img,bins)
t1 = time()
#print bins
print t1 - t0    

Take a look at MatPlotLib. This should take you through everything you want to do, and without the for loops.

From OpenCV docs:

One-channel histogram (image converted to grayscale):

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt
img = cv.imread('home.jpg',0)
plt.hist(img.ravel(),256,[0,256]); plt.show()

RGB histogram (each channel separately)

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt
img = cv.imread('home.jpg')
color = ('b','g','r')
for i,col in enumerate(color):
    histr = cv.calcHist([img],[i],None,[256],[0,256])
    plt.plot(histr,color = col)
    plt.xlim([0,256])
plt.show()
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top