Mastering Opencv ch3: markerless AR(三)

时间:2022-07-01 16:24:43

前面两篇求出了模板图像在相机坐标系的位姿变换矩阵,那么现在就有了透视矩阵(相机内参数,失真参数),和外参数(视景矩阵:位姿矩阵(旋转矩阵和平移矩阵)),这就基本把虚拟三维物体如何投射在显示屏幕上的变换矩阵基本确定了。
四要素:
1、 相机拍摄的最新的图像
2、 相机标定的矩阵
3、 3D的模式姿态(如果需要显示)
4、 与OpenGL相关的内部数据(纹理ID,等等)
下面分析下OpenGL是如何渲染一个三维目标。
1:OpenGL初始化。

ARDrawingContext drawingCtx("Markerless AR", frameSize, calibration);//这个应该是OpenGL初始化的阶段。

在ARDrawingContext.cpp中是这个函数的具体实现:

//在OpenGL的初始化中,调用namedWindow使用最后一个参数调用cv::WINDOW_OPENGL,把OpenGL接口到highgui的模块中,设置窗口大小。
//然后调用setOpenGLContext建立窗口关联(参数就是窗口名称),为了在这个窗口上画虚拟物体,需要使用回调函数,建立方法就是setOpenGLDrawCallback,
//注意这个函数第一个参数是窗口名称,第二个参数是回调函数名,第三个参数是回调函数的参数,因为我这里回调函数onDraw是无参函数,所以这里为NULL
//在需要重绘的时候还要调用updateWindow
ARDrawingContext::ARDrawingContext(std::string windowName, cv::Size frameSize, const CameraCalibration& c)
  : m_isTextureInitialized(false)
  , m_calibration(c)
  , m_windowName(windowName)
{
    // Create window with OpenGL support
    cv::namedWindow(windowName, cv::WINDOW_OPENGL);//OpenGL接口到highgui的模块中

    // Resize it exactly to video size
    cv::resizeWindow(windowName, frameSize.width, frameSize.height);//设置窗口大小,

    // Initialize OpenGL draw callback:
    cv::setOpenGlContext(windowName);
    cv::setOpenGlDrawCallback(windowName, ARDrawingContextDrawCallback, this);//这个this应该就是函数的第三个参数c,这个回调函数将在重画的窗口上被调用。第一个参数设置窗口的名字,第二个参数是一个回调函数,第三个可选参数将传递给回调函数。
}

回调函数是基于OpenGL的OpenCV绘制窗口必须的:

void ARDrawingContextDrawCallback(void* param)
{
    ARDrawingContext * ctx = static_cast<ARDrawingContext*>(param);//该运算符把param转换成ARDrawingContext*类型
    if (ctx)
    {
        ctx->draw();
    }
}

ARDrawingContext负责渲染增强的现实。要渲染的图像帧通过使用正交投影画一个背景开始。然后我们使用正确的透视投影和视景转换来渲染一个3D模型。下面实现了draw这个函数:
清屏和深度缓存之后,我们检查是否用来呈现的视频被初始化了。如果初始化了,我们进行画一个背景,否则我们通过调用glGenTextures来创建一个2D纹理。

void ARDrawingContext::draw() { glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT); // Clear entire screen:清除屏幕,这在OpenGL绘制前必须要干的事
  drawCameraFrame();                                  // Render background,渲染背景
  drawAugmentedScene();                               // Draw AR
  glFlush();
}

2:为了画一个背景,我们建立一个正交投影和包含屏幕所有视口的立体矩形。这个矩形限制在纹理边缘内。这个纹理使用m_backgoundImage对象的内容填充。它的内容预先上传到OpenGL的内存。这个函数与前一章的函数相同。

void ARDrawingContext::drawCameraFrame()
{
  // Initialize texture for background image
  if (!m_isTextureInitialized)
  {
    glGenTextures(1, &m_backgroundTextureId);//1:用来生成纹理的数量,&m_backgroundTextureId存储纹理索引的第一个元素指针,glDeleteTextures,用于销毁一个纹理
    glBindTexture(GL_TEXTURE_2D, m_backgroundTextureId);//实际上是改变了OpenGL的这个状态,它告诉OpenGL下面对纹理的任何操作都是对它所绑定的纹理对象的,比如 glBindTexture(GL_TEXTURE_2D,1)告诉OpenGL下面代码中对2D纹理的任何设置都是针对索引为1的纹理的。

    //图象从纹理图象空间映射到帧缓冲图象空间(映射需要重新构造纹理图像,这样就会造成应用到多边形上的图像失真),这时就可用glTexParmeteri()函数来确定如何把纹理象素映射成像素.这里是对2D纹理先进行缩小线性过滤,然后再进行放大线性过滤(使用距离当前渲染像素中心最近的4个纹素加权平均值)。
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);//纹理过滤函数
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

    m_isTextureInitialized = true;//这个是纹理是否已经进行初始化的标志。
  }

  int w = m_backgroundImage.cols;//获取行列值
  int h = m_backgroundImage.rows;

  glPixelStorei(GL_PACK_ALIGNMENT, 1);//设置像素存储模式,GL_PACK_ALIGNMENT,它影响将像素数据写回到主存的打包形式,对glReadPixels的调用产生影响;指定相应的pname设置为什么值,用于指定存储器中每个像素行有多少个字节对齐。对齐的字节数越高,系统就越能优化。
  glBindTexture(GL_TEXTURE_2D, m_backgroundTextureId);//下面继续对索引为1的纹理进行处理。

  // Upload new texture data:
  if (m_backgroundImage.channels() == 3)
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, w, h, 0, GL_BGR_EXT, GL_UNSIGNED_BYTE, m_backgroundImage.data);//根据指定的参数,生成一个2D纹理(Texture)。
  else if(m_backgroundImage.channels() == 4)
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, w, h, 0, GL_BGRA_EXT, GL_UNSIGNED_BYTE, m_backgroundImage.data);
  else if (m_backgroundImage.channels()==1)
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, w, h, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, m_backgroundImage.data);

  const GLfloat bgTextureVertices[] = { 0, 0, w, 0, 0, h, w, h };
  const GLfloat bgTextureCoords[]   = { 1, 0, 1, 1, 0, 0, 0, 1 };
  const GLfloat proj[]              = { 0, -2.f/w, 0, 0, -2.f/h, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1 };

  glMatrixMode(GL_PROJECTION);//导入投影矩阵
  glLoadMatrixf(proj);

  glMatrixMode(GL_MODELVIEW);//导入视景矩阵
  glLoadIdentity();

  glEnable(GL_TEXTURE_2D);
  glBindTexture(GL_TEXTURE_2D, m_backgroundTextureId);

  // Update attribute values.
  glEnableClientState(GL_VERTEX_ARRAY);//开启顶点数组功能
  glEnableClientState(GL_TEXTURE_COORD_ARRAY);//这个应该是纹理。

  glVertexPointer(2, GL_FLOAT, 0, bgTextureVertices);//用一个数组指定了每个顶点的坐标
  glTexCoordPointer(2, GL_FLOAT, 0, bgTextureCoords);//开启纹理属性

  glColor4f(1,1,1,1);
  glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);//当采用顶点数组方式绘制图形时,使用该函数。该函数根据顶点数组中的坐标数据和指定的模式,进行绘制。

  glDisableClientState(GL_VERTEX_ARRAY);//关闭顶点数组功能
  glDisableClientState(GL_TEXTURE_COORD_ARRAY);//关闭纹理数组
  glDisable(GL_TEXTURE_2D);//关闭2D纹理
}

3:最后就是绘制坐标轴和一个蓝色立方体。包括透视矩阵的构建。

void ARDrawingContext::drawAugmentedScene()
{
  // Init augmentation projection
  Matrix44 projectionMatrix;
  int w = m_backgroundImage.cols;
  int h = m_backgroundImage.rows;
  buildProjectionMatrix(m_calibration, w, h, projectionMatrix);

  glMatrixMode(GL_PROJECTION);//加载投影矩阵
  glLoadMatrixf(projectionMatrix.data);

  glMatrixMode(GL_MODELVIEW);//加载视景矩阵。
  glLoadIdentity();

  if (isPatternPresent)
  {
    // Set the pattern transformation
    Matrix44 glMatrix = patternPose.getMat44();
    glLoadMatrixf(reinterpret_cast<const GLfloat*>(&glMatrix.data[0]));

    // Render model
    drawCoordinateAxis();//绘制坐标轴
    drawCubeModel();//绘制一定透明度的蓝色四方体
  }
}

至此,本章代码分析结束。有问题请联系我。

运行本代码demo的方法:
我是在linux下,直接cmake ./ ; make;
1、 运行单个图像的调用
markerless_ar_demo pattern.png test_imge.png
2、 运行录制视频的调用
markerless_ar_demo pattern.png test_video.avi

3、 运行实时相机的调用:
markerless_ar_demo pattern.png

Mastering Opencv ch3: markerless AR(三)