Ускорение алгоритма векторизованного отслеживания глаз в numpy

Я пытаюсь реализовать алгоритм отслеживания глаз Фабиана Тимма [http://www.inb.uni-luebeck.de/publikationen/pdfs/TiBa11b.pdf] (находится здесь: [http://thume.ca/projects/2012/11/04/simple-accurate-eye-center-tracking-in-opencv/]) в numpy и OpenCV, и я столкнулся с проблемой. Я думаю, что достаточно прилично векторизовал свою реализацию, но она все еще недостаточно быстра, чтобы работать в режиме реального времени, и она не определяет зрачки с такой точностью, как я надеялся. Это мой первый раз, когда я использую numpy, поэтому я не уверен, что я сделал неправильно.

def find_pupil(eye):
    eye_len = np.arange(eye.shape[0])
    xx,yy = np.meshgrid(eye_len,eye_len) #coordinates
    XX,YY = np.meshgrid(xx.ravel(),yy.ravel()) #all distance vectors
    Dx,Dy = [YY-XX, YY-XX] #y2-y1, x2-x1 -- simpler this way because YY = XXT
    Dlen = np.sqrt(Dx**2+Dy**2)
    Dx,Dy = [Dx/Dlen, Dy/Dlen] #normalized

    Gx,Gy = np.gradient(eye)
    Gmagn = np.sqrt(Gx**2+Gy**2)

    Gx,Gy = [Gx/Gmagn,Gy/Gmagn] #normalized
    GX,GY = np.meshgrid(Gx.ravel(),Gy.ravel())

    X = (GX*Dx+GY*Dy)**2
    eye = cv2.bitwise_not(cv2.GaussianBlur(eye,(5,5),0.005*eye.shape[1])) #inverting and blurring eye for use as w
    eyem = np.repeat(eye.ravel()[np.newaxis,:],eye.size,0)
    C = (np.nansum(eyem*X, axis=0)/eye.size).reshape(eye.shape)

    return np.unravel_index(C.argmax(), C.shape)

и остальная часть кода:

def find_eyes(face):
    left_x, left_y = [int(floor(0.5 * face.shape[0])), int(floor(0.2 * face.shape[1]))]
    right_x, right_y = [int(floor(0.1 * face.shape[0])), int(floor(0.2 * face.shape[1]))]
    area = int(floor(0.2 * face.shape[0]))
    left_eye = (left_x, left_y, area, area)
    right_eye = (right_x, right_y, area, area)

    return [left_eye,right_eye]



faceCascade = cv2.CascadeClassifier("haarcascade_frontalface_default.xml")
video_capture = cv2.VideoCapture(0)

while True:
    # Capture frame-by-frame
    ret, frame = video_capture.read()

    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)

    faces = faceCascade.detectMultiScale(
        gray,
        scaleFactor=1.1,
        minNeighbors=5,
        minSize=(30, 30),
        flags=cv2.CASCADE_SCALE_IMAGE
    )

    # Draw a rectangle around the faces
    for (x, y, w, h) in faces:
        cv2.rectangle(frame, (x, y), (x+w, y+h), (0, 255, 0), 2)
        roi_gray = gray[y:y+h, x:x+w]
        roi_color = frame[y:y+h, x:x+w]
        eyes = find_eyes(roi_gray)
        for (ex,ey,ew,eh) in eyes:
            eye_gray = roi_gray[ey:ey+eh,ex:ex+ew]
            eye_color = roi_color[ey:ey+eh,ex:ex+ew]
            cv2.rectangle(roi_color,(ex,ey),(ex+ew,ey+eh),(255,0,0),2)
            px,py = find_pupil(eye_gray)
            cv2.rectangle(eye_color,(px,py),(px+1,py+1),(255,0,0),2)

    # Display the resulting frame
    cv2.imshow('Video', frame)

    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

# When everything is done, release the capture
video_capture.release()
cv2.destroyAllWindows()

person Ben    schedule 14.03.2016    source источник
comment
Первым шагом всегда является профилирование вашего кода (например, с помощью line_profiler). Узнайте, на какие строки он тратит большую часть своего времени, а затем сосредоточьтесь на их оптимизации. Было бы намного проще предложить помощь, если бы вы превратили свой код в MCVE — мы не сможем реально оценить точность или производительность, если мы у вас нет входных данных, необходимых для запуска вашего кода.   -  person ali_m    schedule 14.03.2016
comment
У нас также нет никакого контекста для этой функции. Если он вызывается повторно (например, в цикле), возможно, вы излишне пересчитываете многие локальные переменные, которые не меняются от вызова к вызову. Однако мы не можем легко сказать, что изменилось, а что нет, не имея доступа к некоторым примерам входных данных.   -  person ali_m    schedule 14.03.2016
comment
Спасибо, Али. Я профилирую свой код. Однако мне трудно сделать мой код автономным, не скопировав остальную его часть. Все, что я могу сказать, это то, что лицо — это квадратное изображение лица, снятое веб-камерой, а x, y, w, h — верхний левый угол и размеры квадрата, окружающего один из глаз. Могу ли я связать репозиторий Github?   -  person Ben    schedule 15.03.2016
comment
Это не идеально, но ссылка на репозиторий github лучше, чем ничего. Случайные входные данные, как в ответе Дивакара, могут дать некоторое представление о производительности, но, конечно, ничего не говорят нам о точности.   -  person ali_m    schedule 15.03.2016
comment
Вместо этого я добавил остальную часть своего кода. В любом случае, это было немного.   -  person Ben    schedule 15.03.2016


Ответы (1)


Вы можете выполнять многие из тех операций, которые сохраняют реплицированные элементы, а затем выполнять некоторые математические операции, непосредственно выполняя математические операции после создания одноэлементных измерений, которые позволили бы NumPy broadcasting. Таким образом, было бы два преимущества: операции «на лету» для экономии памяти рабочей области и повышение производительности. Также в конце мы можем заменить расчет nansum на упрощенную версию. Таким образом, с учетом всей этой философии, вот один модифицированный подход -

def find_pupil_v2(face, x, y, w, h):    
    eye = face[x:x+w,y:y+h]
    eye_len = np.arange(eye.shape[0])

    N = eye_len.size**2
    eye_len_diff = eye_len[:,None] - eye_len
    Dlen = np.sqrt(2*((eye_len_diff)**2))
    Dxy0 = eye_len_diff/Dlen 

    Gx0,Gy0 = np.gradient(eye)
    Gmagn = np.sqrt(Gx0**2+Gy0**2)
    Gx,Gy = [Gx0/Gmagn,Gy0/Gmagn] #normalized

    B0 = Gy[:,:,None]*Dxy0[:,None,:]
    C0 = Gx[:,None,:]*Dxy0
    X = ((C0.transpose(1,0,2)[:,None,:,:]+B0[:,:,None,:]).reshape(N,N))**2

    eye1 = cv2.bitwise_not(cv2.GaussianBlur(eye,(5,5),0.005*eye.shape[1]))
    C = (np.nansum(X,0)*eye1.ravel()/eye1.size).reshape(eye1.shape)

    return np.unravel_index(C.argmax(), C.shape)

Еще остался один repeat в Dxy. Возможно, можно было бы избежать этого шага, и Dxy0 можно было бы передать непосредственно в шаг, который использует Dxy для получения X, но я не работал с этим. Все преобразовано в broadcasting на основе!

Тестирование во время выполнения и проверка вывода -

In [539]: # Inputs with random elements
     ...: face = np.random.randint(0,10,(256,256)).astype('uint8')
     ...: x = 40
     ...: y = 60
     ...: w = 64
     ...: h = 64
     ...: 

In [540]: find_pupil(face,x,y,w,h)
Out[540]: (32, 63)

In [541]: find_pupil_v2(face,x,y,w,h)
Out[541]: (32, 63)

In [542]: %timeit find_pupil(face,x,y,w,h)
1 loops, best of 3: 4.15 s per loop

In [543]: %timeit find_pupil_v2(face,x,y,w,h)
1 loops, best of 3: 529 ms per loop

Кажется, мы приближаемся к ускорению 8x!

person Divakar    schedule 14.03.2016
comment
Большое спасибо, Дивакар! Это значительно ускорило мой код. Не могли бы вы пролить свет на то, что здесь происходит под капотом? - person Ben; 15.03.2016
comment
@Бен Рад помочь! Тем не менее, любопытно, какое ускорение вы получаете с вашим фактическим вариантом использования? Кроме того, если это решило вашу проблему, рассмотрите возможность принятия решения, нажав на пустую галочку рядом с решением. Подробнее об этом читайте здесь — мета. stackexchange.com/questions/5234/ - person Divakar; 15.03.2016
comment
Я замечаю, что скорость в два-три раза выше, чем раньше. Это гораздо ближе к реальному времени. Самое смешное, что timeit не замечает ускорения. Однако у меня все еще есть проблемы с точностью. Если это так быстро, как моя реализация может получить, я буду счастлив, но я все же предпочел бы быстрее. - person Ben; 15.03.2016
comment
@Ben Не могли бы вы уточнить вопрос точности? Например, сколько отклонений и т. д.? - person Divakar; 15.03.2016
comment
Для одного из моих глаз он попадает на зрачок примерно в 70% случаев, а для другого всегда низко и левее. Я отредактировал свой исходный пост со всем своим кодом, если вы хотите попробовать сами. - person Ben; 15.03.2016