Вычислить нормали поверхности из изображения глубины, используя векторное произведение соседних пикселей

Как следует из названия, я хочу вычислить нормали поверхности заданного изображения глубины, используя перекрестное произведение соседних пикселей. Я хотел бы использовать для этого Opencv и избегать использования PCL, однако я не совсем понимаю процедуру, так как мои знания в этой области весьма ограничены. Поэтому буду признателен, если кто-нибудь подскажет. Упомянем здесь, что у меня нет никакой другой информации, кроме изображения глубины и соответствующего изображения RGB, поэтому нет информации о K матрице камеры.

Итак, допустим, что у нас есть следующее изображение глубины:

введите описание изображения здесь

и я хочу найти вектор нормали в соответствующей точке с соответствующим значением глубины, как на следующем изображении:

введите описание изображения здесь

Как я могу сделать это, используя перекрестное произведение соседних пикселей? Я не возражаю, если нормали не отличаются высокой точностью.

Спасибо.


Обновление:

Хорошо, я пытался следовать ответу @timday и портировать его код в Opencv. Со следующим кодом:

Mat depth = <my_depth_image> of type CV_32FC1
Mat normals(depth.size(), CV_32FC3);

for(int x = 0; x < depth.rows; ++x)
{
    for(int y = 0; y < depth.cols; ++y)
    {

        float dzdx = (depth.at<float>(x+1, y) - depth.at<float>(x-1, y)) / 2.0;
        float dzdy = (depth.at<float>(x, y+1) - depth.at<float>(x, y-1)) / 2.0;

        Vec3f d(-dzdx, -dzdy, 1.0f);
        Vec3f n = normalize(d);

        normals.at<Vec3f>(x, y) = n;
    }
}

imshow("depth", depth / 255);
imshow("normals", normals);

Я получаю правильный следующий результат (мне пришлось заменить double на float и Vecd на Vecf, хотя я не знаю, почему это имеет значение):

введите описание изображения здесь


person ttsesm    schedule 06.01.2016    source источник
comment
Зависит от того, что делает OpenCV, когда вы выгружаете изображение векторов. Не похоже, что ваш XYZ сопоставляется с RGB, и диапазон положительного/отрицательного компонента может не соответствовать значениям 0-255 пикселей без некоторого масштабирования. Вот почему мой код также включает простую модель затенения для создания изображения в оттенках серого из нормалей.   -  person timday    schedule 07.01.2016
comment
Привет @timday Я не думаю, что это проблема Opencv, потому что если я загружу нормали из скрипта Matlab в Opencv и imshow() их, то я получу хорошее изображение по сравнению с изображением выше.   -  person ttsesm    schedule 08.01.2016
comment
Вы можете найти это полезным.   -  person dhanushka    schedule 09.01.2016
comment
@dhanushka действительно интересная ссылка. Спасибо.   -  person ttsesm    schedule 10.01.2016
comment
Спасибо за обновления. Одно небольшое исправление заключается в том, что в большинстве случаев доступ к cv::Mat осуществляется (y,x), а точка изображения — cv::Point(x,y,z). Следовательно, в ваших строках есть несоответствие «x»? Сохраняя номенклатуру циклов прежней, нормаль должна быть «Vec3f d(-dzdy, -dzdx, 1.0f);».   -  person Jan-Michael Tressler    schedule 30.08.2018


Ответы (2)


На самом деле вам не нужно использовать перекрестное произведение для этого, но см. ниже.

Учтите, что ваше изображение диапазона является функцией z (x, y).

Нормаль к поверхности находится в направлении (-dz/dx,-dz/dy,1). (Где под dz/dx я подразумеваю дифференциал: скорость изменения z с x). А затем нормали условно нормируются к единице длины.

Между прочим, если вам интересно, откуда взялось это (-dz/dx, -dz/dy,1)... если вы возьмете 2 ортогональных касательных вектора в плоскости, параллельной осям x и y, это (1 ,0,dzdx) и (0,1,dzdy). Нормаль перпендикулярна касательным, поэтому должно быть (1,0,dzdx)X(0,1,dzdy) — где «X» — перекрестное произведение, которое равно (-dzdx, -dzdy,1). Таким образом, у вас есть полученное векторное произведение нормалей, но нет необходимости вычислять его так явно в коде, когда вы можете просто использовать полученное выражение для нормали напрямую.

Псевдокод для вычисления нормали единичной длины в точке (x, y) будет выглядеть примерно так:

dzdx=(z(x+1,y)-z(x-1,y))/2.0;
dzdy=(z(x,y+1)-z(x,y-1))/2.0;
direction=(-dzdx,-dzdy,1.0)
magnitude=sqrt(direction.x**2 + direction.y**2 + direction.z**2)
normal=direction/magnitude

В зависимости от того, что вы пытаетесь сделать, может иметь смысл заменить значения NaN просто большим числом.

Используя этот подход, из вашего изображения диапазона я могу получить следующее:

введите описание изображения здесь

(Затем я использую вычисленные направления нормалей, чтобы выполнить простое затенение; обратите внимание на «ступенчатый» вид из-за квантования изображения диапазона; в идеале у вас будет более высокая точность, чем 8-битная для данных реального диапазона).

Извините, это не код OpenCV или C++, а просто для полноты: полный код, создавший это изображение (GLSL, встроенный в файл Qt QML; может быть запущен с помощью qmlscene Qt5), приведен ниже. Приведенный выше псевдокод можно найти в функции main() фрагментного шейдера:

import QtQuick 2.2

Image {
  source: 'range.png'  // The provided image

  ShaderEffect {
    anchors.fill: parent
    blending: false

    property real dx: 1.0/parent.width
    property real dy: 1.0/parent.height
    property variant src: parent

    vertexShader: "
      uniform highp mat4 qt_Matrix;
      attribute highp vec4 qt_Vertex;
      attribute highp vec2 qt_MultiTexCoord0;
      varying highp vec2 coord;
      void main() {
        coord=qt_MultiTexCoord0;
        gl_Position=qt_Matrix*qt_Vertex;
      }"

   fragmentShader: "
     uniform highp float dx;
     uniform highp float dy;
     varying highp vec2 coord;
     uniform sampler2D src;
     void main() {
       highp float dzdx=( texture2D(src,coord+vec2(dx,0.0)).x - texture2D(src,coord+vec2(-dx,0.0)).x )/(2.0*dx);
       highp float dzdy=( texture2D(src,coord+vec2(0.0,dy)).x - texture2D(src,coord+vec2(0.0,-dy)).x )/(2.0*dy);
       highp vec3 d=vec3(-dzdx,-dzdy,1.0);
       highp vec3 n=normalize(d);
       highp vec3 lightDirection=vec3(1.0,-2.0,3.0);
       highp float shading=0.5+0.5*dot(n,normalize(lightDirection));
       gl_FragColor=vec4(shading,shading,shading,1.0);
     }"
  }
}
person timday    schedule 06.01.2016
comment
спасибо за ответ, сейчас я пытаюсь перенести ваш код в opencv. Однако, как бы я сделал то же самое, используя перекрестное произведение, поскольку мне нужно это сделать, но таким образом. - person ttsesm; 07.01.2016
comment
в вашем примере вы используете перекрестный продукт с 4 соседями, верно? Если бы я хотел использовать 8-соседей, я должен был бы применить ту же процедуру для диагональных пикселей, верно? - person ttsesm; 10.01.2016
comment
@theodore: Ну, один из способов думать об этом состоит в том, что входными данными для перекрестного произведения являются два касательных вектора, а перекрестное произведение генерирует перпендикулярную нормаль. Выше я эффективно использую 4 соседей как конечные точки двух касательных векторов. Не очевидно, как расширить это до 8 соседних точек... однако я знаю, что есть более продвинутые методы, которые подгоняют сплайны к большему количеству точек в окрестности, а затем используют нормаль к сплайновой поверхности (пример в bib.irb.hr/datoteka/150807.1-0239.pdf). - person timday; 10.01.2016
comment
спасибо, вижу. На самом деле я имел в виду что-то вроде этого: dzdx=(z(x+1,y)-z(x-1,y))/2.0; dzdy=(z(x,y+1)-z(x,y-1))/2.0; direction=(-dxdz,-dydz,1.0) magnitude=sqrt(direction.x**2 + direction.y**2 + direction.z**2) dzdx1=(z(x+1,y+1)-z(x-1,y-1))/2.0; dzdy1=(z(x-1,y+1)-z(x+1,y-1))/2.0; direction1=(-dxdz1,-dydz1,1.0) magnitude1=sqrt(direction1.x**2 + direction1.y**2 + direction1.z**2) normal=(direction/magnitude) + (direction1/magnitude1) но я не знаю, насколько это правильно. - person ttsesm; 10.01.2016
comment
Хм, я мог бы представить себе что-то вроде вычисления одной нормали с использованием 4 соседних точек оси x-y, а затем другой нормали с использованием 4 диагональных точек... и затем их усреднение (и перенормировка). Трудно предположить, насколько это может быть или не быть лучше, чем версия только с 4 соседними точками. Если ваши данные зашумлены, и вы пытаетесь устранить эффект ложных значений, то лучше применить какую-то предварительную фильтрацию, чем усложнять нормальное генерирование. - person timday; 10.01.2016
comment
хорошо, так что моя мысль выше в моем комментарии не совсем неверна. На данный момент это не связано с зашумленными данными или чем-то еще, а просто из любопытства, что они означают, вычисляя нормали из перекрестного произведения с использованием 4 соседних или 8 соседних пикселей соответственно. Однако я не знаю, хотите ли вы добавить подход с 8 соседними пикселями из своего комментария в своем ответе выше только для целей завершения для будущих читателей. В любом случае большое спасибо за помощь и подсказку ;-) - person ttsesm; 10.01.2016
comment
Верно ли это и для изображений дальности, полученных из проективной проекции? Я не могу установить связь со следующим объяснением: stackoverflow.com/questions/30993211/ Особенно атан для меня загадка... - person ASML; 08.05.2018
comment
@ASML: перспективная проекция немного изменит ситуацию, поскольку лучи не параллельны, а значение постоянного диапазона становится сферической поверхностью, а не плоской плоскостью. Поправочный коэффициент вполне может включать тангенс, если он выполнен в сферической геометрии. Вероятно, вам следует задать отдельный вопрос; комментарии не к месту. - person timday; 21.05.2018
comment
@timday: Спасибо! Некоторое время назад я задал отдельный вопрос, но не получил ответов: "> stackoverflow.com/questions/50241819/ - person ASML; 21.05.2018
comment
Почему в псевдокоде компонент z в векторе направления установлен равным 1? - person MonsieurBeilto; 30.09.2018
comment
@MonsieurBeilto, потому что, если бы, например, dxdz было 1,0 (а dydz было 0,0), то это был бы наклон в 45 градусов, и вы хотели бы сформировать прямоугольный равнобедренный треугольник (следовательно, с z = 1). Нормальное направление лежит вдоль гипотенузы этого треугольника, и вы хотите, чтобы он имел единичную длину, следовательно, последующий шаг нормализации. - person timday; 30.09.2018

Код (вычисление матрицы) я считаю правильным:

def normalization(data):
   mo_chang =np.sqrt(np.multiply(data[:,:,0],data[:,:,0])+np.multiply(data[:,:,1],data[:,:,1])+np.multiply(data[:,:,2],data[:,:,2]))
   mo_chang = np.dstack((mo_chang,mo_chang,mo_chang))
   return data/mo_chang

x,y=np.meshgrid(np.arange(0,width),np.arange(0,height))
x=x.reshape([-1])
y=y.reshape([-1])
xyz=np.vstack((x,y,np.ones_like(x)))
pts_3d=np.dot(np.linalg.inv(K),xyz*img1_depth.reshape([-1]))
pts_3d_world=pts_3d.reshape((3,height,width))
f= pts_3d_world[:,1:height-1,2:width]-pts_3d_world[:,1:height-1,1:width-1]
t= pts_3d_world[:,2:height,1:width-1]-pts_3d_world[:,1:height-1,1:width-1]
normal_map=np.cross(f,l,axisa=0,axisb=0)
normal_map=normalization(normal_map)
normal_map=normal_map*0.5+0.5
alpha = np.full((height-2,width-2,1), (1.), dtype="float32")
normal_map=np.concatenate((normal_map,alpha),axis=2)
  1. Мы должны использовать встроенные функции камеры с именем «K». Я думаю, что значения f и t основаны на трехмерных точках в координатах камеры.

  2. Для нормального вектора (-1,-1,100) и (255,255,100) имеют один и тот же цвет на 8-битных изображениях, но они совершенно разные в норме. Таким образом, мы должны сопоставить значения нормали с (0,1) на normal_map=normal_map*0.5+0.5.

Добро пожаловать в общение.

person Baichuan    schedule 23.08.2020