OpenGL 按照三角形仿射变换并贴图渲染(正常渲染或离屏渲染以及异步优化)(一)

标签:CG opengl

转载请说明:http://blog.csdn.net/hust_sheng/article/details/75268410


需求

  在2转8路或者4转64路虚拟视点合成的项目中,需要根据真实相机的真实视点合成虚拟位置(虚拟相机)的虚拟视点。最后一步的绘制过程大致如下:

  其实就是将图像以三角形为单位,从原始图像(左侧)向目标图像(右侧)映射,也即warp。本质是仿射变换的过程。


怎么用OpenGL实现上述过程?

  OpenGL的贴图渲染会基于GPU使用shader进行,效率较高,对于加速来说比较合适,贴图渲染也是OpenGL较为核心的功能。下面进行大致的介绍:


常见的渲染一般会分为两种:(1)渲染至窗口(2)离屏渲染

  • 头文件
#include <GL/glew/glew.h>       // 必须在glut.h之前include
//
#include <GL/glut.h>
#include <GL/freeglut.h>
#include <GL/freeglut_ext.h>
#include <GL/freeglut_std.h>
  • 渲染至窗口
    使用glut即可

    • 初始化包括两个部分

      • 初始化窗口
      • 设置纹理对象(数据来源)以及相关参数
      void initOpenGL(int argc, char* argv[])
      {
      	// GLUT 初始化
      	glutInit(&argc, argv);
      	
      	glutInitDisplayMode(GLUT_RGB | GLUT_DOUBLE);        // 双缓冲
      	glutInitWindowPosition(0, 0); 
      	glutInitWindowSize(640, 480);
      	glutCreateWindow("X-project");
      
      	glEnable(GL_TEXTURE_2D);
      
      	int texture_ID;
      	glGenTextures(1, &texture_ID);  // 分配一个新的纹理编号
      	if (texture_ID == 0) {
      		return;
      	}
      
      	glBindTexture(GL_TEXTURE_2D, texture_ID);
      	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);		    // 双线性插值
      	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
      	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);		// 对应原图像中超出边缘的像素按照边界扩展(黑边)
      	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
      	glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
      
      	float border_color[4] = { 1.0f, 1.0f, 1.0f, 1.0f };
      	glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, border_color);
      	
      	return ;
      }
      

    需要说明的是,如果我们没有指定渲染的缓冲区,OpenGL默认的渲染缓冲区是窗口绑定的Frame Buffer Object。整个流程简单来说如下图所示:

    • 准备数据

      glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0,
      			 GL_BGR_EXT, GL_UNSIGNED_BYTE, tlImage->imageData);
      

      除了最后一个参数之外,其他参数是为了配置最后一个参数对应的图像数据(如果我们设置的是纹理的话,第一个参数是固定的)。最后一个参数:tlImage->imageData 表示贴图渲染的原图像,或者说是参考图像,我们的目的是得到贴图之后的图像。

    • 设置贴图渲染的像素点的对应关系(关键的一步)

    // 开始绑定
    glBegin(GL_TRIANGLES);          // 指定映射方法:三角形或者矩形
    
    ... // 设置原图像和贴图之后的目标图像对应的顶点
    
    glEnd();
    

    设置原图像和贴图之后的目标图像对应的顶点 这句话的意思是什么呢?OpenGL需要知道 srcImage -> dstImage 之间的映射关系,才能进行“贴图”操作,有下面几种常见映射方法:

    1. 矩形映射
      首先将映射方式设置为:GL_QUADS,之后可以直接设置当前图像和目标图像的四个图像顶点,即依次矩形映射即可完成贴图过程,我们也可以设置多组小矩形之间的映射类似于下图。

    2. 三角形映射
      如果将映射方式设置为:GL_TRIANGLES,之后可以直接设置当前图像和目标图像按照三角形换分之后的顶点,如下图所示:

      给一个例子:

      // 开始绑定
      glBegin(GL_TRIANGLES);
      
      // 上三角
      glTexCoord2d(u1_up, v1_up);		// src
      glVertex3d(x1_up, y1_up, 1);	// dst
      
      glTexCoord2d(u2_up, v2_up);
      glVertex3d(x2_up, y2_up, 1);
      
      glTexCoord2d(u3_up, v3_up);
      glVertex3d(x3_up, y3_up, 1);
      
      glEnd();
      

      glTexCoord2d 函数(纹理坐标,即原图像坐标)用于设置srcImage图像的三角形顶点,三组(x, y);
      glVertex3d 函数(顶点坐标,目标图像坐标)用于设置dstImage图像的三角形顶点,三组(x, y, 1),第三个参数表示法向量,2D设置为1即可。

    • 显示

      glutSwapBuffers();
      

      该函数的目的就是交换缓冲区,我们一开始会设置单缓冲或这是双缓冲(效率更高一点),上述函数就是将缓冲区中地数据交换到绑定至窗口的FBO中,我们就可以看到相应的图片了。

    • 数据读取

        ```
        pPixelData = (GLubyte*)malloc(PixelDataLength);
        if (pPixelData == 0)
        	exit(0);
        
        ...
        
        // 读取像素
        glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
        glReadPixels(0, 0, width, height, GL_BGR_EXT, GL_UNSIGNED_BYTE, pPixelData);
        ```
        
        基本思路是通过glReadPixels函数读取GPU中对应缓冲区的数据,将其保存在预先设计好的buffer(pPixelData)里面,之后将buffer保存在本地,具体的过程见 [链接](http://blog.csdn.net/hust_sheng/article/details/75268056)。
        
        <font color="red">需要说明的是:</font>
        &emsp;&emsp;调用glReadPixels函数之前,需要加上 `glReadBuffer(GL_FRONT);` ,因为对于渲染到窗口的情况来说,窗口相当于FRONT,这句话相当于说,我们读取的对象是“前端”的数据。现在思考另一个问题,既然我们设置的是双缓冲!那么,`glReadBuffer(GL_BACK);` 是否有用呢?事实证明是的,这里也有 `离屏渲染` 的思想<font color="red">(注意思想)</font>:
        &emsp;&emsp;*在调用glutSwapBuffers()函数之前,数据应该是在“后端”的,所以我们在此处就进行数据读取也是可以的,那么就需要设置 `glReadBuffer(GL_BACK);`。*
      

  • 离屏渲染

    相对于窗口渲染来说,头文件是一致的,主要基于glew。

    • 初始化包括四个部分
      • glut初始化

        // GLUT 初始化 
        glutInit(&argc, argv);
        glutInitDisplayMode(GLUT_RGB | GLUT_DOUBLE);
        glutInitWindowPosition(-100, -100);
        glutInitWindowSize(1, 1);
        glutCreateWindow("glew test"); 
        
      • glew初始化

        // GLEW 初始化
        GLenum  err;
        err = glewInit();
        
      • 创建原图像数据纹理

        glEnable(GL_TEXTURE_2D);    // 使用纹理之前需要开启
        
        glGenTextures(1, &m_srctex);
        glBindTexture(GL_TEXTURE_2D, m_srctex);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
        glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
        
      • 创建目的图像纹理并与FBO(也要创建)绑定

        glGenTextures(1, &m_dstfbotex);
        glBindTexture(GL_TEXTURE_2D, m_dstfbotex);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
        //glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, m_width, m_height, 0, GL_BGR, GL_UNSIGNED_BYTE, NULL);
        
        glEnable(GL_FRAMEBUFFER);
        glGenFramebuffers(1, &m_FboID);
        glBindFramebuffer(GL_FRAMEBUFFER, m_FboID);
        glFramebufferTexture2D(GL_FRAMEBUFFER,
        					   GL_COLOR_ATTACHMENT0,    // 把纹理对象绑定到FBO的0号绑定点
        					   GL_TEXTURE_2D, m_dstfbotex, 0);	// 0 表示使用原图像
        					   
        GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
        if (status != GL_FRAMEBUFFER_COMPLETE){
            exit(-1);
        }
        

        注意
          前面也说,OpenGL会将一个默认的FBO绑定到显示的窗口上面。这里我们自己创建一个FBO和一个纹理,再将两者绑定,这样系统就会使用我们自己创建的FBO,避免了屏幕渲染的过程,这就是离屏渲染

      • 保存状态变量

        glGetIntegerv(GL_FRAMEBUFFER_BINDING_EXT, &m_curbuff);
        
        glPushAttrib(GL_VIEWPORT_BIT);      // 保存状态变量
        glMatrixMode(GL_PROJECTION);
        glPushMatrix();
        glMatrixMode(GL_MODELVIEW);
        glPushMatrix();
        
        glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT);
        
        glViewport(0, 0, m_width, m_height);
        
        glMatrixMode(GL_PROJECTION);
        glLoadIdentity();
        glMatrixMode(GL_MODELVIEW);
        glLoadIdentity();
        gluOrtho2D(0.0, 1.0, 0.0, 1.0);
        
      • 准备数据

        非常关键!

        glBindTexture(GL_TEXTURE_2D, m_srctex);         // 纹理选择绑定m_srctex纹理
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0,
        	 GL_BGR_EXT, GL_UNSIGNED_BYTE, tlImage->imageData);     // 将原数据加载到m_srctex纹理
        
        ...     // 设置贴图渲染的像素点的对应关系(关键的一步)
        
      • 渲染至我们自己设置的FBO

          采用离屏渲染不需要 glutSwapBuffers(); 但是需要注意的是,在读取FBO中数据的时候需要加上下面代码,表示读取的是我们自己的m_FboID对应的FBO中的数据,很重要!

        glBindFramebuffer(GL_READ_FRAMEBUFFER, m_FboID);
        
      • 恢复状态变量

        glMatrixMode(GL_MODELVIEW);
        glPopMatrix();
        glMatrixMode(GL_PROJECTION);
        glPopMatrix();
        glPopAttrib();
        

性能优化(异步操作)

  针对上述离屏渲染过程进行性能分析,发现数据量大的时候,渲染过程和GPU->CPU的数据传输过程(glReadPixels函数)都会有较大的耗时,可以采取的解决方案是渲染过程和数据处理过程采用异步处理,查阅相关资料发现glReadPixels函数本身不支持异步操作,但是我们通过采用PBO可以实现类似的异步操作。下面简单介绍:

  • 初始化

    创建PBO:

    glGenBuffersARB(PBO_COUNT, pboIds);
    glBindBufferARB(GL_PIXEL_PACK_BUFFER_ARB, pboIds[0]);
    glBufferDataARB(GL_PIXEL_PACK_BUFFER_ARB, DATA_SIZE, 0, GL_STREAM_READ_ARB);
    
  • 数据读取操作

    glBindBufferARB(GL_PIXEL_PACK_BUFFER_ARB, pboIds[0]);
    // 注意glReadPixels函数的最后一个参数是0
    glReadPixels(0, 0, width, height * OPENGL_FBO_PARALLELNUM, GL_BGR_EXT, GL_UNSIGNED_BYTE, 0);
    
    // 异步读取
    GLubyte* image_src = (GLubyte*)glMapBufferARB(GL_PIXEL_PACK_BUFFER_ARB, GL_READ_ONLY_ARB);
    
    if (image_src){
    	glUnmapBufferARB(GL_PIXEL_PACK_BUFFER_ARB);
    }
    

关于如何将OpenGL渲染的图片保存到本地,见链接

  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要计算拟合出最小外接矩形2的四个顶点坐标,并进行仿射变换,可以按照以下步骤进行: 1. 首先,使用Halcon的`gen_rectangle2`函数计算出最小外接矩形的相关参数,包括中心点坐标、宽度、高度和旋转角度。假设这些参数分别为`Row`(中心点的行坐标)、`Column`(中心点的列坐标)、`Length1`(宽度)、`Length2`(高度)和`Phi`(旋转角度)。 2. 然后,根据最小外接矩形的参数,可以计算出四个顶点相对于中心点的相对坐标。根据矩形的宽度和高度以及旋转角度,可以得到四个顶点的相对坐标分别为: - 左上角:(-Length1/2, -Length2/2) - 右上角:(Length1/2, -Length2/2) - 右下角:(Length1/2, Length2/2) - 左下角:(-Length1/2, Length2/2) 3. 接下来,可以将相对坐标转换为绝对坐标,即将相对坐标加上中心点的坐标。四个顶点的绝对坐标分别为: - 左上角:(Row - Length2/2*cos(Phi) - Length1/2*sin(Phi), Column + Length2/2*sin(Phi) - Length1/2*cos(Phi)) - 右上角:(Row - Length2/2*cos(Phi) + Length1/2*sin(Phi), Column + Length2/2*sin(Phi) + Length1/2*cos(Phi)) - 右下角:(Row + Length2/2*cos(Phi) + Length1/2*sin(Phi), Column - Length2/2*sin(Phi) + Length1/2*cos(Phi)) - 左下角:(Row + Length2/2*cos(Phi) - Length1/2*sin(Phi), Column - Length2/2*sin(Phi) - Length1/2*cos(Phi)) 4. 最后,如果需要进行仿射变换,可以使用Halcon的`affine_trans_region`函数将最小外接矩形区域进行仿射变换。根据仿射变换的需求,可以设置变换矩阵,然后将最小外接矩形区域与变换矩阵一起传递给`affine_trans_region`函数。 以上是使用Halcon计算拟合出最小外接矩形2的四个顶点坐标并进行仿射变换的一般步骤,具体实现时需要根据实际情况进行调整和优化

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值