Posted on

I normally don't do premature performance optimizations, and I was planning on optimizing the whole eye tracker later, when I felt I had all of the functionality I wanted, but from time to time, you still want to check your code, particularly when it's suspiciously slow. So I did examine the code I had written so far using line_profiler (Abysmal documentation btw.) like so:

kernprof.py -l test.py && python -m line_profiler test.py.lprof

I have found that the most time is being spent in one function, getOrientationAndMagnitude, you'll recall it looked like this:

def getOrientationAndMagnitude(image, show=False):
    sobelHorizontal = cv2.Sobel(image, cv2.CV_32F, 1, 0)
    sobelVertical = cv2.Sobel(image, cv2.CV_32F, 0, 1)

    h = sobelHorizontal
    v = sobelVertical

    orientation = np.empty(image.shape)
    magnitude = np.empty(image.shape)

    height, width = h.shape
    for y in range(height):
        for x in range(width):
            orientation[y][x] = cv2.fastAtan2(h[y][x], v[y][x])

    magnitude = cv2.magnitude(h, v)

    return orientation, magnitude

I wrote this code some time ago, when I was less familiar with OpenCV than I am now, and you can quickly see the hotspot. Yes, the line 14. I probably did it like this because I hadn't noticed the phase() function OpenCV has.

But this will serve the purpose of showing how such a small oversight can have a dramatic effect on performance. Armed with the phase() function, we can do the following:

def getOrientationAndMagnitude(image, show=False):
    h = cv2.Sobel(image, cv2.CV_32F, 1, 0)
    v = cv2.Sobel(image, cv2.CV_32F, 0, 1)

    orientation = cv2.phase(h, v, angleInDegrees=True)
    magnitude = cv2.magnitude(h, v)

    return orientation, magnitude

Now this much shorter and simpler function will also run much faster, thanks to OpenCV. And here's how I tested it:

import cProfile

image = cv2.imread('eye.png')
image = cv2.cvtColor(image, cv2.cv.CV_BGR2GRAY)
image2 = np.copy(image)

cProfile.runctx("refGetOrientationAndMagnitude(image)", globals=globals(), locals=locals())
cProfile.runctx("newGetOrientationAndMagnitude(image)", globals=globals(), locals=locals())

Which yielded the following results:


201309 function calls in 1.022 seconds

Ordered by: standard name

ncalls  tottime  percall  cumtime  percall filename:lineno(function)
     1    0.000    0.000    1.022    1.022 <string>:1(<module>)
     1    0.823    0.823    1.022    1.022 test.py:7(refGetOrientationAndMagnitude)
     2    0.004    0.002    0.004    0.002 {cv2.Sobel}
201000    0.192    0.000    0.192    0.000 {cv2.fastAtan2}
     1    0.001    0.001    0.001    0.001 {cv2.magnitude}
     1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
     2    0.000    0.000    0.000    0.000 {numpy.core.multiarray.empty}
   301    0.002    0.000    0.002    0.000 {range}


7 function calls in 0.004 seconds

Ordered by: standard name

ncalls  tottime  percall  cumtime  percall filename:lineno(function)
    1    0.000    0.000    0.004    0.004 <string>:1(<module>)
    1    0.000    0.000    0.003    0.003 test.py:26(candidateGetOrientationAndMagnitude)
    2    0.002    0.001    0.002    0.001 {cv2.Sobel}
    1    0.000    0.000    0.000    0.000 {cv2.magnitude}
    1    0.001    0.001    0.001    0.001 {cv2.phase}
    1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}

So 1.022s to 0.004, 255× faster with just replacing a double for loop with an OpenCV call.