Ускорить Least_square scipy

Как ускорить работу функции least_square? У нас есть шесть переменных (3 угла ориентации и 3 смещения осей), которые необходимо оптимизировать. На вход функции подаются два набора трехмерных точек, два набора точек на плоскости и матрица проекции.

dSeed = np.zeros(6)
optRes = least_squares(minReproj, dSeed, method='lm', max_nfev=600,
    args=(points_prev, points_cur, d3d_prev, d3d_cur, Proj1))

Эта функция минимизирует ошибку прямого-обратного проецирования точек.

def minReproj(dof, d2dPoints1, d2dPoints2, d3dPoints1, d3dPoints2, w2cMatrix):
    Rmat = genEulerZXZ(dof[0], dof[1], dof[2]) # my function
    translationArray = np.array([[dof[3]], [dof[4]], [dof[5]]])
    temp = np.hstack((Rmat, translationArray))
    perspectiveProj = np.vstack((temp, [0, 0, 0, 1]))

    numPoints = d2dPoints1.shape[0]
    errorA = np.zeros((numPoints,3))
    errorB = np.zeros((numPoints,3))

    forwardProj = np.matmul(w2cMatrix, perspectiveProj)
    backwardProj = np.matmul(w2cMatrix, np.linalg.inv(perspectiveProj))

    for i in range(numPoints):
        Ja = np.ones((3))
        Jb = np.ones((3))
        Wa = np.ones((4))
        Wb = np.ones((4))

        Ja[0:2] = d2dPoints1[i,:]
        Jb[0:2] = d2dPoints2[i,:]
        Wa[0:3] = d3dPoints1[i,:]
        Wb[0:3] = d3dPoints2[i,:]

        JaPred = np.matmul(forwardProj, Wb)
        JaPred /= JaPred[-1]
        e1 = Ja - JaPred

        JbPred = np.matmul(backwardProj, Wa)
        JbPred /= JbPred[-1]
        e2 = Jb - JbPred

        errorA[i,:] = e1
        errorB[i,:] = e2

    residual = np.vstack((errorA,errorB))
    return residual.flatten()
def genEulerZXZ(psi, theta, sigma):
    c1 = cos(psi)
    s1 = sin(psi)
    c2 = cos(theta)
    s2 = sin(theta)
    c3 = cos(sigma)
    s3 = sin(sigma)

    mat = np.zeros((3,3))

    mat[0,0] = (c1 * c3) - (s1 * c2 * s3)
    mat[0,1] = (-c1 * s3) - (s1 * c2 * c3)
    mat[0,2] = (s1 * s2)

    mat[1,0] = (s1 * c3) + (c1 * c2 * s3)
    mat[1,1] = (-s1 * s3) + (c1 * c2 * c3)
    mat[1,2] = (-c1 * s2)

    mat[2,0] = (s2 * s3)
    mat[2,1] = (s2 * c3)
    mat[2,2] = c2

    return mat

Эта оптимизация занимает от 0,2 до 0,4 секунды, и это слишком много. Может быть, вы знаете, как ускорить этот процесс? Или, может быть, есть другой способ найти относительное вращение и перемещение двух наборов точек? Для рполески:

       96    0.023    0.000   19.406    0.202 /usr/local/lib/python3.7/dist-packages/scipy/optimize/_lsq/least_squares.py:240(least_squares)
     4548    0.132    0.000   18.986    0.004 /usr/local/lib/python3.7/dist-packages/scipy/optimize/_lsq/least_squares.py:801(fun_wrapped)
       96    0.012    0.000   18.797    0.196 /usr/local/lib/python3.7/dist-packages/scipy/optimize/_lsq/least_squares.py:42(call_minpack)
     4548   11.102    0.002   18.789    0.004 /home/pi/helperFunctions.py:29(minimizeReprojection)

person user11278733    schedule 20.05.2020    source источник
comment
В вашем minReproj() отсутствует return утверждение. Вы уверены, что разместили правильный код?   -  person rpoleski    schedule 20.05.2020
comment
Добавил часть кода   -  person user11278733    schedule 20.05.2020
comment
Функция genEulerZXZ() не разглашается. Кроме того, я бы профилировал вашу minReproj() функцию и посмотрел, где находятся узкие места, поскольку большая часть least_square() времени, скорее всего, будет потрачена на вашу функцию. Я вполне уверен, что вы могли бы избежать/уменьшить пару относительно дорогих *stack(), некоторые выделения памяти и, возможно, некоторые matmul() и некоторые linalg.inv(). Во всяком случае, функция может быть JIT-компилирована с помощью Numba, что может дать вам некоторую скорость при циклическом выполнении точки.   -  person norok2    schedule 20.05.2020
comment
Я добавил genEulerZXZ. Я удалил это, чтобы не раздувать вопрос   -  person user11278733    schedule 20.05.2020
comment
Пробовали профилировать? Одна возможность python -m cProfile script.py. Если ваши углы находятся в диапазоне от 0 до pi/2, вы можете легко ускорить вычисления sin/cos, используя тригонометрическое тождество Пифагора.   -  person rpoleski    schedule 20.05.2020
comment
Я попытался. Я добавляю наибольший выход времени.   -  person user11278733    schedule 20.05.2020
comment
Вы должны проанализировать его, т. е. найти ту часть, которая вносит наибольший вклад.   -  person rpoleski    schedule 20.05.2020
comment
Я добавил вывод в вопросе. Насколько я понимаю, minReprojection занимает больше всего времени   -  person user11278733    schedule 20.05.2020
comment
cos и sin от math или от numpy или от чего-то еще?   -  person norok2    schedule 20.05.2020
comment
Из math библиотеки   -  person user11278733    schedule 20.05.2020
comment
Не могли бы вы дать некоторые идеи о форме входных аргументов minReproj()?   -  person norok2    schedule 20.05.2020
comment
Два набора 3d-точек из стереореконструкции в моменты времени A и B. Также два набора 2d-точек из левого изображения стереокамеры в моменты времени A и B. И матрица репроекции левой камеры.   -  person user11278733    schedule 20.05.2020


Ответы (1)


Скорее всего, во время scipy.optimize.least_squares() большая часть времени уходит на вычисление остатков, которые в вашем случае принимают форму кода в minReproj().

Однако код, который вы предоставили в minReproj(), похоже, имеет неоптимальное управление памятью, которое можно значительно улучшить с помощью предварительного выделения. Это приведет к значительному увеличению скорости.


Например, vstack() и hstack() страдают от необходимости копировать свои аргументы в память их конечного результата. Учтите, первые несколько строк minReproj(), которые я перегруппировал в gen_affine_OP(). Их можно переписать как gen_affine() с гораздо лучшими таймингами (я также переписал gen_euler_zxz(), чтобы не выделять новую память):

import numpy as np
from math import sin, cos


def gen_euler_zxz(result, psi, theta, sigma):
    c1 = cos(psi)
    s1 = sin(psi)
    c2 = cos(theta)
    s2 = sin(theta)
    c3 = cos(sigma)
    s3 = sin(sigma)
    result[0,0] = (c1 * c3) - (s1 * c2 * s3)
    result[0,1] = (-c1 * s3) - (s1 * c2 * c3)
    result[0,2] = (s1 * s2)
    result[1,0] = (s1 * c3) + (c1 * c2 * s3)
    result[1,1] = (-s1 * s3) + (c1 * c2 * c3)
    result[1,2] = (-c1 * s2)
    result[2,0] = (s2 * s3)
    result[2,1] = (s2 * c3)
    result[2,2] = c2
    return result


def gen_affine(dof):
    result = np.zeros((4, 4))
    gen_euler_zxz(result[:3, :3], dof[0], dof[1], dof[2])
    result[:3, 3] = dof[3:]
    result[3, 3] = 1
    return result


def gen_affine_OP(dof):
    Rmat = gen_euler_zxz(np.empty((3, 3)), dof[0], dof[1], dof[2])
    translationArray = np.array([[dof[3]], [dof[4]], [dof[5]]])
    temp = np.hstack((Rmat, translationArray))
    return np.vstack((temp, [0, 0, 0, 1]))


dof = 1, 2, 3, 4, 5, 6
%timeit gen_affine_OP(dof)
# 100000 loops, best of 3: 16.6 µs per loop
%timeit gen_affine(dof)
# 100000 loops, best of 3: 5.02 µs per loop

Точно так же можно избежать вызова residual = np.vstack((errorA,errorB)), определив больший остаток и предоставив его представление для errorA и errorB.

Другой пример — при создании Ja, Jb, Wa, Wb:

def func_OP(numPoints):
    for i in range(numPoints):
        Ja = np.ones((3))
        Jb = np.ones((3))
        Wa = np.ones((4))
        Wb = np.ones((4))


def func(n):
    Ja = np.empty(3)
    Jb = np.empty(3)
    Wa = np.empty(3)
    Wb = np.empty(3)
    for i in range(n):
        Ja[:] = 1
        Jb[:] = 1
        Wa[:] = 1
        Wb[:] = 1


%timeit func_OP(1000)
# 100 loops, best of 3: 9.31 ms per loop
%timeit func(1000)
# 100 loops, best of 3: 2.2 ms per loop

Кроме того, .flatten() делает копию, которая вам не нужна, а .ravel() просто возвращает представление, но этого достаточно для ваших нужд и происходит намного быстрее:

a = np.ones((300, 300))
%timeit a.ravel()
# 1000000 loops, best of 3: 215 ns per loop
%timeit a.flatten()
# 10000 loops, best of 3: 113 µs per loop

Последние замечания касаются ускорения вашего основного цикла. Я не разрабатываю для этого простую векторизованную перезапись, но вы можете ускорить процесс с помощью Numba (при условии, что он компилируется в необъектном режиме).

Для этого вам также необходимо JIT-декорировать gen_euler_zxz() и заменить вызовы np.matmul() на np.dot() (поскольку np.matmul() не поддерживается Numba.


В конце концов, исправленный minReproj() будет выглядеть так:

from math import sin, cos
import numpy as np
import numba as nb


matmul = np.dot


@nb.jit
def gen_euler_zxz(result, psi, theta, sigma):
    c1 = cos(psi)
    s1 = sin(psi)
    c2 = cos(theta)
    s2 = sin(theta)
    c3 = cos(sigma)
    s3 = sin(sigma)
    result[0, 0] = (c1 * c3) - (s1 * c2 * s3)
    result[0, 1] = (-c1 * s3) - (s1 * c2 * c3)
    result[0, 2] = (s1 * s2)
    result[1, 0] = (s1 * c3) + (c1 * c2 * s3)
    result[1, 1] = (-s1 * s3) + (c1 * c2 * c3)
    result[1, 2] = (-c1 * s2)
    result[2, 0] = (s2 * s3)
    result[2, 1] = (s2 * c3)
    result[2, 2] = c2
    return result


@nb.jit
def minReproj(dof, d2dPoints1, d2dPoints2, d3dPoints1, d3dPoints2, w2cMatrix):
    perspectiveProj = np.zeros((4, 4))
    gen_euler_zxz(perspectiveProj[:3, :3], dof[0], dof[1], dof[2])
    perspectiveProj[:3, 3] = dof[3:]
    perspectiveProj[3, 3] = 1

    numPoints = d2dPoints1.shape[0]
    residual = np.empty((2 * numPoints, 3))

    forwardProj = matmul(w2cMatrix, perspectiveProj)
    backwardProj = matmul(w2cMatrix, np.linalg.inv(perspectiveProj))

    Ja = np.empty(3)
    Jb = np.empty(3)
    Wa = np.empty(4)
    Wb = np.empty(4)
    for i in range(numPoints):
        j = i + numPoints

        Ja[2] = Jb[2] = 1.0
        Wa[3] = Wb[3] = 1.0
        Ja[0:2] = d2dPoints1[i, :]
        Jb[0:2] = d2dPoints2[i, :]
        Wa[0:3] = d3dPoints1[i, :]
        Wb[0:3] = d3dPoints2[i, :]

        JaPred = matmul(forwardProj, Wb)
        JaPred /= JaPred[-1]
        residual[i, :] = Ja - JaPred

        JbPred = matmul(backwardProj, Wa)
        JbPred /= JbPred[-1]
        residual[j, :] = Jb - JbPred
    return residual.ravel()

Тестирование на некоторых игрушечных данных показывает значительное ускорение, которое становится драматичным при правильном использовании Numba:

n = 10000
dof = 1, 2, 3, 4, 5, 6
d2dPoints1 = np.random.random((n, 2))
d2dPoints2 = np.random.random((n, 2))
d3dPoints1 = np.random.random((n, 3))
d3dPoints2 = np.random.random((n, 3))
w2cMatrix = np.random.random((3, 4))
x = minReproj_OP(dof, d2dPoints1, d2dPoints2, d3dPoints1, d3dPoints2, w2cMatrix)
y = minReproj(dof, d2dPoints1, d2dPoints2, d3dPoints1, d3dPoints2, w2cMatrix)
z = minReproj_nb(dof, d2dPoints1, d2dPoints2, d3dPoints1, d3dPoints2, w2cMatrix)
print(np.allclose(x, y))
# True
print(np.allclose(x, z))
# True


%timeit minReproj_OP(dof, d2dPoints1, d2dPoints2, d3dPoints1, d3dPoints2, w2cMatrix)
# 1 loop, best of 3: 277 ms per loop
%timeit minReproj(dof, d2dPoints1, d2dPoints2, d3dPoints1, d3dPoints2, w2cMatrix)
# 10 loops, best of 3: 177 ms per loop
%timeit minReproj_nb(dof, d2dPoints1, d2dPoints2, d3dPoints1, d3dPoints2, w2cMatrix)
# 100 loops, best of 3: 5.69 ms per loop
person norok2    schedule 20.05.2020
comment
@user11278733 user11278733, пожалуйста, отпишитесь, работает ли он правильно и какой прирост скорости вы наблюдаете - person norok2; 20.05.2020
comment
Спасибо за ваш ответ. Я использую RPI и получаю ошибку: Compilation is falling back to object mode WITH looplifting enabled because Func tion "minReproj" failed type inference due to: Untyped global name 'gen_euler_zxz': cannot determine Numba type of <class 'function'> - person user11278733; 20.05.2020
comment
@ user11278733 Украсьте gen_euler_zxz() тоже @nb.jit или удалите @nb.jit из minReproj(). Вы должны наблюдать некоторое ускорение независимо от Numba. - person norok2; 20.05.2020
comment
Если я вычисляю оптимальные углы ориентации в функции gen_euler_zxz(), как мне ее правильно вызвать? R = gen_euler_zxz(optRes.x[0], optRes.x[1], optRes.x[2]) - person user11278733; 20.05.2020
comment
@user11278733 user11278733 Вам необходимо указать подходящий массив NumPy в качестве первого аргумента. gen_euler_zxz() не выделяет память. Итак, что-то вроде R = gen_euler_zxz(np.empty((3, 3)), optRes.x[0], optRes.x[1], optRes.x[2]) или R = np.empty((3, 3)); gen_euler_zxz(R, optRes.x[0], optRes.x[1], optRes.x[2]) достаточно. - person norok2; 20.05.2020
comment
@ user11278733, но если вы хотите сгенерировать полную матрицу affinr, почему бы вам не использовать gen_affine(optRes.x) напрямую? - person norok2; 20.05.2020
comment
Протестируйте старый и новый вариант кода. Старый код занимает ~ 22,8 секунды, новый 21,2 секунды. Хотя камера находится в одном месте, могут возникнуть расхождения. - person user11278733; 20.05.2020
comment
@user11278733 user11278733 Я думал, что эта оптимизация занимает от 0,2 до 0,4 секунды. Откуда берутся эти 21,2 секунды? Вы уверены, что этот код является узким местом? Вы используете те же данные? В противном случае расхождения могут исходить из самих данных. - person norok2; 20.05.2020
comment
Я использую видеопоток, и расхождения могут быть небольшими, даже несмотря на то, что камера неподвижна. Если вы запускаете cProfile, процесс оптимизации занимает примерно 73% времени. - person user11278733; 20.05.2020
comment
@ user11278733 ознакомьтесь с обновленным кодом. Я проверил, что он дает 100% идентичные результаты, а также позволил Numba выполнить правильную оптимизацию. - person norok2; 20.05.2020
comment
Если я использую Numba, я получаю сообщение об ошибке. Да, результаты будут такими же. Но я работаю с реальным видеопотоком, поэтому возможны небольшие отклонения - person user11278733; 20.05.2020
comment
@ user11278733 У меня версия '0.48.0'. Если он у вас есть, он должен работать правильно. - person norok2; 20.05.2020
comment
Я не могу установить 0.48.0 версию на RPI, но могу установить 0.49.0 или 0.49.1. Я использую 0.49.1 версию. Но получаю ошибку Compilation is falling back to object mode WITH looplifting enabled because Function "minReproj" failed type inference due to: Invalid use of Function(<built-in function zeros>) with argument(s) of type(s): (UniTuple(Literal[int](4) x 2), dtype=Function(<class 'float'>)) - person user11278733; 20.05.2020
comment
@user11278733 user11278733 Вы скопировали и вставили код сверху? Похоже, у вас по-прежнему установлен параметр dtype в np.zeros(), который следует удалить. - person norok2; 20.05.2020
comment
Вы исправил код, я не заметил. Завтра проверю и отпишусь о результатах - person user11278733; 21.05.2020
comment
Код работает без ошибок, но теперь основная часть кода работает по-другому. Я конвертирую входное изображение в оттенки серого, но вижу цветное изображение. И код работает некорректно. Вероятно, это связано с тем, что numba изменяет исходный код. - person user11278733; 21.05.2020
comment
Если вы не сталкиваетесь с проблемами переполнения, ускоренный код Numba JIT должен работать точно так же, как и неускоренный. Я кратко протестировал его на случайных данных и не заметил никакой разницы. Если у вас есть доказательства иного поведения, сообщите об ошибке разработчикам Numba. Тем не менее, я считаю более вероятным, что вы упускаете из виду какую-то ошибку, которую вы добавили в код, который вы не показываете, код в сообщении, насколько говорят мои тесты, в порядке. - person norok2; 21.05.2020
comment
Может быть, вы можете отправить его лично? - person user11278733; 21.05.2020
comment
Что ты имеешь в виду? - person norok2; 21.05.2020
comment
Не знаю почему с утра странно работало, хотя файл точно обновлял. Не подскажете, как сделать jit-компиляцию в начале сборки файла? Потому что получается, что файл запускается 1 секунду, потом 10 секунд jit-компилируется, а потом снова работает. Пока функция не вызывается, она не компилируется. - person user11278733; 21.05.2020
comment
Точно. Чтобы запустить компиляцию, просто вызовите функцию с фиктивными входными данными один раз. - person norok2; 21.05.2020
comment
Подскажите, как лучше искать соответствия между стереоизображениями? У нас ректифицированные изображения, поэтому одинаковые точки должны быть на +- одной горизонтальной линии. - person user11278733; 21.05.2020
comment
Я думаю, вы можете задать новый вопрос (и, возможно, проголосовать/принять существующие ответы на текущий вопрос). Если вы считаете, что это уместно, вы можете сослаться на это. - person norok2; 21.05.2020