MVSNeRF:多视角立体图像的快速广义辐射场重建

MVSNeRF: Fast Generalizable Radiance Field Reconstruction
from Multi-View Stereo

https://github.com/apchenstu/mvsnerf

提出了一种新的神经渲染方法neural rendering approach  MVSNeRF,它可以有效地重建用于视图合成的神经辐射场。与先前关于神经辐射场的工作不同,这些工作考虑对密集捕获的图像进行逐场景优化,我们提出了一种通用的深度神经网络,该网络可以通过快速网络推理仅从三个附近的输入视图重建辐射场。我们的方法利用平面扫描成本体plane-swept cost volumes(广泛用于多视图立体multi-view stereo)进行几何感知场景推理,并将其与基于物理的体渲染相结合进行神经辐射场重建。我们在DTU数据集中的真实对象上训练我们的网络,并在三个不同的数据集上测试它以评估它的有效性和可推广性generalizability我们的方法可以跨场景(甚至室内场景,完全不同于我们的对象训练场景)进行推广generalize across scenes,并仅使用三幅输入图像生成逼真的视图合成结果,明显优于目前的广义辐射场重建generalizable radiance field reconstruction工作。此外,如果捕捉到密集图像dense images are captured,我们估计的辐射场表示可以容易地微调easily fine-tuned;这导致快速的逐场景重建fast per-scene reconstruction,比NeRF具有更高的渲染质量和更少的优化时间。

我们利用最近在深度多视图立体(MVS)deep multi- view stereo (MVS)上的成功[50,18,10]。这一系列工作可以通过对成本体积应用3D卷积applying 3D convolutions on cost volumes来训练用于3D重建任务的可概括的神经网络。与[50]类似,我们通过将来自附近输入视图的2D图像特征(由2D CNN推断)扭曲warping到参考视图的平截头体中的扫描平面上sweeping planes in the reference view’s frustrum,在输入参考视图处构建成本体。不像MVS方法[50,10]仅在这样的成本体积上进行深度推断depth inference,我们的网络推理关于场景几何形状和外观reasons about both scene geometry and appearance,并输出神经辐射场(见图2),实现视图合成。具体来说,利用3D CNN,我们重建(从成本体)神经场景编码体neural scene encoding volume,其由 编码关于局部场景几何形状和外观的信息的每个体素神经特征per-voxel neural features 组成。然后,我们利用多层感知器(MLP)在编码体积encoding volume内使用三线性插值神经特征tri-inearly interpolated neural features来解码任意连续位置处的体积密度volume density和辐射度radiance。本质上,编码体是辐射场的局部神经表示;一旦估计,该体积可以直接用于(丢弃3D CNN)通过可微分射线行进differentiable ray marching(如在[34]中)的最终渲染。

我们的方法结合了两个世界的优点,基于学习的MVS和神经渲染。与现有的MVS方法相比,我们实现了可微分神经渲染differentiable neural rendering,允许在没有3D监督和推理时间优化的情况下进行训练,以进一步提高质量。与现有的神经渲染作品相比,我们的类MVS架构可以自然地进行跨视图对应推理cross-view correspondence reasoning,有利于推广到未知的测试场景,也导致更好的神经场景重建和渲染。因此,我们的方法可以明显优于最近的并行可概括NeRF工作concurrent generalizable NeRF work[54,46],该工作主要考虑2D图像特征,而没有明确的几何感知3D结构(参见表。1和图4)。 我们证明,仅使用三个输入图像,我们从DTU数据集训练的网络可以在测试DTU场景上合成照片级逼真的图像,甚至可以在具有非常不同的场景分布的其他数据集上生成合理的结果。此外,我们估计的三个图像辐射场(神经编码体积)可以在新的测试场景上进一步轻松优化,以在捕获更多图像的情况下改善神经重建,从而产生与每个场景过拟合NeRF相当甚至更好的照片级逼真结果,尽管我们的优化时间比NeRF少得多。

该方法利用深度MVS的成功,在成本体上应用3D卷积来训练用于3D重建任务的可泛化神经网络。与MVS方法不同的是,MVS方法仅对这样的成本体进行深度推断,而该网络对场景几何和外观进行推理,并输出神经辐射场,从而实现视图合成。具体而言,利用3D CNN,重建(从成本体)神经场景编码体,由编码局部场景几何和外观信息的体素神经特征组成。然后,多层感知器(MLP)在编码体内用三线性插值的神经特征对任意连续位置处的体密度和辐射度进行解码。本质上,编码体是辐射场的局部神经表征;其一旦估计,可直接用于(丢弃3D CNN)可微分光线行进(ray-marching)进行最终渲染。

与现有的MVS方法相比,MVSNeRF启用可微分神经渲染,在无3D监督的情况下进行训练,并优化推断时间,以进一步提高质量。与现有的神经渲染方法相比,类似MVS的体系结构自然能够进行跨视图的对应推理,有助于对未见测试场景进行泛化,引向更好的神经场景重建和渲染。

如图1是MVSNeRF的概览:(a)基于摄像头参数,首先将2D图像特征warp(单应变换)到一个平面扫描(plane sweep)上,构建成本体;这种基于方差的成本体编码了不同输入视图之间的图像外观变化,解释了由场景几何和视图相关明暗效果引起的外观变化;(b)然后,用3D CNN重建逐体素神经特征的一个神经编码体;3D CNN 是一个3D UNet,可以有效地推断和传播场景外观信息,从而产生有意义的场景编码体;注:该编码体是无监督预测的,并在端到端训练中用体渲染进行推断;另外,还将原图像像素合并到下一个体回归阶段,这样可恢复下采样丢失的高频;(c)用MLP,通过编码体插值的特征,在任意位置回归体密度和RGB辐射度,这些体属性由可微分光线行进做最终的渲染。

SLAM、实时三维重建、SfM、多视角立体视觉MVS

SLAM(Simultaneous Localization And Mapping) 同时定位与地图构建

SLAM是Simultaneous Location and Mapping,同时定位与地图构建。是指搭载特定传感器的主体,在没有环境先验信息的情况下,于运动过程中建立环境的模型,同时估计自己的运动。
目的是解决自主机器人“定位”和“建图”两个问题。同时要求能够实时地,没有先验知识地进行。

  • 一般假设相机的内参已知;
  • 实时处理;
  • 以定位为主要目标
  • 输入数据包括RGB、RGB-D、激光、IMU等
  • 输出稀疏或半稠密地图
  • 机器人和计算机视觉研究领域
  • 典型软件:x-SLAM

实时三维重建

  • 一般假设相机的内参已知;
  • 实时处理;
  • 以建图为主体
  • 输入数据以RGB-D图像为主
  • 输出稠密地图
  • 计算机视觉和计算机图形研究领域
  • 典型软件:xFusion

SfM(Structure from Motion) 运动恢复结构

  • 估计相机内参;
  • 不实时处理;
  • 输入数据以RGB图像为主
  • 输出稠密地图
  • 计算机视觉和计算机图形研究领域
  • 典型软件:Agisoft PhotoScan、Agisoft Metashape、COLMAP

多视角立体视觉MVS

多视角立体视觉(Multiple View Stereo,MVS)是对立体视觉的推广,能够在多个视角(从外向里)观察和获取景物的图像,并以此完成匹配和深度估计。某种意义上讲,SLAM/SFM其实和MVS是类似的,只是前者是摄像头运动,后者是多个摄像头视角。也可以说,前者可以在环境里面“穿行”,而后者更像在环境外“旁观”。

  • 收集图像;
  • 针对每个图像计算相机参数;
  • 从图像集和相应的摄像机参数重建场景的3D几何图形;
  • 可选择地重建场景的形状和纹理颜色。

共同点

都需要估计和优化相机的位姿

基础:三维运动、相机模型、非线性优化

python 和 c++ 联合编译—ctypes库

Python 使用 ctypes 调用 C/C++ DLL 动态链接库

Python ctypes模块优点与适用场景

ctypes 有以下优点:

  • Python内建,不需要单独安装
  • 可以直接调用二进制的动态链接库,在Python一侧,不需要了解 c/c++ dll 内部的工作方式
  • 对C/C++与Python基本类型的相互映射有良好的支持。

ctypes 在下列场景可以发挥较大作用

  • 运算量大的操作可以写成 C/C++ dll, python 通过 ctypes 来调用, 大幅提升Python代码性能。
  • python可以直接使用 C/C++各类资源,如boost库等。
  • 第3方软件或硬件提供的SDK库,通过ctypes来实现调用对接。 理论上C/C++的库都可以对接,这比其它语言方便得多。

Python本身开发效率高,还具备丰富的生态资源,有了 ctypes 加持,还可以使用C/C++的优秀资源,因此,掌握了这个工具,相信我,在大部分项目上将给你带来惊喜。

ctypes 官方文档,更着重是一份产品说明书,而不是教程,阅读官方文档来学习比较耗精力。 本人旨在给提供一份优秀教程,尽可能有条理地讲清楚原理、通过实例代码演示如何使用,同时也涉及一些更复杂的使用场景,如回调函数等。

除了ctypes 外,还可以通过 python C API、Cython来编写C/C++代码模块,或者利用第3方工具包来完成融合,如
Swig
pybind11

1、Visual Studio 2022 把项目打包成DLL

在示例讲解之前,我们先用Visual Studio2022创建并编译1个DLL项目

1) 创建新项目, 选择dll项目

至此创建完成,包含源文件dllmain.cpp、pch.cpp,头文件framework.h、pch.h。 ![在这里插入图片描述]

2) 定义头文件 boFirst.h

#define IMPORT_DLL __declspec(dllimport) 定义了IMPORT_DLL 宏, __declspec(dllimport)表示函数将被编译为dll
extern "C" IMPORT_DLL  ==》 指定该函数可被外部调用。 
#pragma once

#ifdef DLL_EXPORT
#define IMPORT_DLL __declspec(dllexport)
#else
#define IMPORT_DLL __declspec(dllimport)
#endif

//自定义结构体
struct boShape
{
    char  shape[20];
    float width; 
    float height;
    float depth; 
};

// 自定义函数
extern "C" IMPORT_DLL int bo_add(int a, int b); 
//自定义函数,参数为结构体 boStruct 
extern "C" IMPORT_DLL float bo_shape_vol(boShape bs);

这里分别添加bo_add, bo_shape_vol 两个可供外部调用函数声明, 也可以更简化,只要函数前面有extern “C” __declspec(dllexport) 即可。

#ifndef _PY_LIST_2_C_ARRAY_H_
#define _PY_LIST_2_C_ARRAY_H_

#include <stdio.h>

typedef struct {
    int value;
    wchar_t* name;
} Item;

extern "C" __declspec(dllexport)
int sum_diagonal(Item** field, size_t size);

#endif

3)实现函数代码

// boFirst.cpp : Defines the exported functions for the DLL.
#include "pch.h" // use stdafx.h in Visual Studio 2017 and earlier
#include <utility>
#include <limits.h>
#include "boFirst.h"
# include <iostream>

int bo_add(int a, int b)
{
    int c;
    c = a + b; 
    return c; 
}

float bo_shape_vol(boShape bs)
{
    float volume = bs.width * bs.height * bs.depth;  
    std::cout << bs.shape << "volume is " << volume << std::endl; 
    return volume; 
}

4) 编译生成dll

选择build solution , 输出窗口可以看到,在x64\Debug\ 目录下生成了DLL_Test.dll 文件

Rebuild started...
1>------ Rebuild All started: Project: DLL_Test, Configuration: Debug x64 ------
1>pch.cpp
1>dllmain.cpp
1>boFirst.cpp
1>D:\workplace\temp\C++\DLL_Test\DLL_Test\boFirst.cpp(8,5): warning C4273: 'bo_add': inconsistent dll linkage
1>D:\workplace\temp\C++\DLL_Test\DLL_Test\boFirst.h(20,27): message : see previous definition of 'bo_add'
1>D:\workplace\temp\C++\DLL_Test\DLL_Test\boFirst.cpp(15,7): warning C4273: 'bo_shape_vol': inconsistent dll linkage
1>D:\workplace\temp\C++\DLL_Test\DLL_Test\boFirst.h(22,29): message : see previous definition of 'bo_shape_vol'
1>Generating Code...
1>   Creating library D:\workplace\temp\C++\DLL_Test\x64\Debug\DLL_Test.lib and object D:\workplace\temp\C++\DLL_Test\x64\Debug\DLL_Test.exp
1>DLL_Test.vcxproj -> D:\workplace\temp\C++\DLL_Test\x64\Debug\DLL_Test.dll
1>Done building project "DLL_Test.vcxproj".
========== Rebuild All: 1 succeeded, 0 failed, 0 skipped ==========

2、通过 ctypes 调用DLL的简单示例

在上一节中的dll有两个方法,其中1个输入参数使用了C++ Struct 结构体, python没有对应的数据结构。

第1种调用方式 (标准方式):

Step-1:

将DLL_Test.dll 文件拷贝至python文件所在目录, 该dll中包含如下可供调用的函数

int bo_add(int a, int b)
float bo_shape_vol(boShape bs); 其中boShape为自定义struct

Step-2:

编写代码,调用 bo_add()方法

from ctypes import *
dll = cdll.LoadLibrary("./DLL_Test.dll")
print("bo_add ", dll.bo_add(20, 80))

运行,即可以得到输出 100

cdll.LoadLibrary() 方法返回 cdll 对象,cdll 调用C/C++函数的方法遵从 cdecl 方式(C/C++函数调用的标准方式)。

上例是直接调用 dll中的函数.

第2种调用方式( 函数签名方式)

函数签名方式,相当于在python内重新申明1个C++函数的别名,申明内容包括用ctype指定函数形参与返回值的数据类型。

import ctypes
clibrary = ctypes.CDLL('clibrary.so')

# 定义1个python函数名,指向 C++ 函数
addTwoNumbers = clibrary.add

# 用ctypes 数据类型,定义函数的参数类与返回值类型, 
addTwoNumbers.argtypes = [ctypes.c_int, ctypes.c_int]
addTwoNumbers.restype = ctypes.c_int

# 使用签名函数
print("Sum of two numbers is :", addTwoNumbers(20, 10))

Output: Sum of two numbers is : 30

ctypes 数据类型 –> Python 数据类型的转换是程序在后台自动完成的。

3、ctypes 数据类型

C/C++的数据类型与 Python数据类型及使用方式都有较多差异,因此python调用C++函数,必须需要对DLL函数形参以及返回值类型进行转换,才能把数据传入DLL,以及解析DLL的返回值。ctypes 模块提供了中间数据类型,来帮助Python完成数据转换,这些类型在python中都是以对象方式出现。

Python 调用DLL函数分3步:

1) 在Python中重新申明 DLL 函数: 除了函数名之外,还要用ctypes的数据类型将 DLL 函数的形参与返回值重新申明一遍。

2) 实参准备:当实际调用该函数时,将数据赋值给ctypes参数后,调用该函数。

3) 解析返回值:如果有返回值,从 ctypes 类型中解析出数据。

其中,第1步,用ctypes 类型申明 DLL函数形参最为关键,可以认为这一步目的是将 C++的数据类型转换为Python数据类型,ctypes 起到桥梁作用。

下面介绍一下 ctypes 提供了哪些数据类型,以及如何使用。

1) ctypes 与c、python数据类型的对应关系

2) 整数型参数变量定义

i = c_int()

赋值 :

i = c_int(99) 
i.value=99
print(i) 
c_long(99)

3) 浮点数值参数

d1 = c_float()
d1.value=10.3
d1
c_float(10.300000190734863)
>>> d1.value
10.300000190734863

4) 字符串参数

用 char * 32 定义1个32个字符的字符串,赋值用 value属性

>>> str1 = c_char * 32
>>> str1
<class '__main__.c_char_Array_32'>
>>> str1.value = b"hello"
>>> str1
<class '__main__.c_char_Array_32'>
>>> str1.value
b'hello'

C/C++中字符串是用char* 指针或数组来代替,ctypes 提供对应的字符串指针类型 c_char_p,给c_char_p赋值通常有两种方式: – 把 python 字符串转为 bytes 类型, 使用endcode()方法。 – 直接使用bytes 类型字节串。

将字符串转换为 bytes 对象后,用c_char_p的value属性赋值, 用法示例 str2.value = x 赋值。

>>> x = b"abcdef"
>>> str2 = c_char_p()
>>> str2.value = x
>>> str2
c_char_p(140250436808304)
>>> str2.value
b'abcdef'
>>> str2 = str1
>>> str2
<class '__main__.c_char_Array_32'>
>>> str2.value
b'hello'

c_char_p 指向的数据类型必须是二进制编码,即Bytes类型,如果是中文,可以用utf-8编码,显示时再解码.

>>> y=bytes("hello, 小王","utf-8")
>>> str2.value=y
>>> str2.value
b'hello, \xe5\xb0\x8f\xe7\x8e\x8b'
>>> str2.value.decode(encoding="utf-8")
'hello, 小王'

ctypes 还提供了 create_string_buffer() 方法用于生成字符串。 格式: ctypes.create_string_buffer(init_or_size, size=None)

str3 = create_string_buffer(b’world’,32)
print(str3)
<main.c_char_Array_32 object at 0x7f8e9826eac0>
print(str3.value)
b’world’
print(str3.raw)
b’\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00′ repr(str3.raw)

create a 3 byte buffer, initialized to NUL bytes

p = create_string_buffer(3)

create a buffer containing a NUL terminated string

p = create_string_buffer(b'Hello') 
print(sizeof(p)
repr(p.raw))

create a 10 byte buffer

p = create_string_buffer(b'Hello', 10) print(sizeof(p), repr(p.raw))

5) 数组类型变量


数组的创建和C/C++语言的类似,给定数据类型和长度即可
如定义 c_int 类型数组, 长度为10. 先定义1个 INT_ARRAY 类型,再创建1个数组变量,其类型为 c_long_Array_10, 示例代码:

>>> INT_ARRAY = c_int * 10
>>> array1 = INT_ARRAY(10,20,30,40,50,60,70,80,90,100)
>>> array1
<__main__.c_long_Array_10 object at 0x00000283665D7040>
>>> array1[5]
60
>>> array1[7:]
[80, 90, 100]

创建数据变量过程可以将两步并为1步,

>>> array2 = (c_int * 10)(10,20,30,40,50,60,70,80,90,100)
>>> array2
<__main__.c_long_Array_10 object at 0x0000028366558B40>
>>> array2[7:]
[80, 90, 100]

6) Structure 变量

Python的dict类型虽然与C/C++ struct 结构体类型很相似,但不能直接拿来用。ctypes模块提供了 Structure 类来进行 C/C++ struct结构体类型转换。 示例,定义1个POINT 像素点结构体

class POINT(Structure):
...     _fields_ = [("x", c_int),
...                 ("y", c_int)]
>>> point = POINT(10, 20)
>>> print(point.x, point.y)
10 20

定义1个RECT 结构体,其字段为POINT结构体

>>> class RECT(Structure):
...     _fields_ = [("upperleft", POINT),
...                 ("lowerright", POINT)]
...
>>> rc = RECT(point)
>>> print(rc.upperleft.x, rc.upperleft.y)
0 5
>>> print(rc.lowerright.x, rc.lowerright.y)
0 0
>>>r = RECT(POINT(1, 2), POINT(3, 4))

1个更复杂的结构体, 字段 b , 为浮点数, “point_arrary” 为1个包含POINT结构体类型的Array

>>> class MyStruct(Structure):
...     _fields_ = [("a", c_int),
...                 ("b", c_float),
...                 ("point_array", POINT * 4)]

说明: python3 新增了struct 模块,基于序列化的原理,可将不同类型的数据打包进 ctypes bytes buffer变量,用于向c/c++ struct 变量传值, 但直观度不如 Structure变量,有兴趣者可以深入了解一下 python struct 模块的使用

7) 指针变量

ctypes提供了pointer()和POINTER()两种方法创建指针

pointer( object )用于将某个类型的对象转化为指针

其参数必须是ctypes的变量对象,不能是类型. 如下例,int_p 指向1个c_int 对象的指针,当前值为99

int_obj = c_int(99)
int_p = pointer(int_obj)
print(int_p)
使用contents方法访问指针
print(int_p.contents)
# 获取指针指向的值
print(int_p[0])

output 如下

<ctypes.wintypes.LP_c_long object at 0x00000217897102C0>
c_long(99)
99

POINTER()用于定义指针变量类型

POINTER() 的参数必须是类型名称,相当于定义了新的1个指针变量类型,但指向值的类型必须是指定的类型名称。然后再用这个新类型实例化1个指针变量。 而 pointer() 方法隐式地完成了POINTER()的工作。

# 指针类型
INT_P  = POINTER(c_int)  # 定义了1个新的指针变量类型
# 实例化
int_obj = c_int(4)
int_p_obj = INT_P(int_obj)   # 实例化1个指针变量
print(int_p_obj)
print(int_p_obj.contents)
print(int_p_obj[0])

output :

<__main__.LP_c_int object at 0x7f47df7f79e0>
c_int(4)
4

指针类型的转换

ctypes提供 cast() 方法将一个ctypes实例转换为指向另一个ctypes数据类型的指针,cast()接受两个参数,一个是ctypes对象,它是或可以转换成某种类型的指针,另一个是ctypes指针类型。它返回第二个参数的一个实例,该实例引用与第一个参数相同的内存块。

# 初始化 1个 c_int的指针,当前值为4
int_p = pointer(c_int(99))
print(int_p)
# 定义1个 c_char类型的指针类型
char_p_type = POINTER(c_char)
print(char_p_type)
# 转 c_int变量转换为c_char类型指针
cast_type = cast(int_p, char_p_type)
print(cast_type)

output:

<ctypes.wintypes.LP_c_long object at 0x0000021789710140>
<class 'ctypes.LP_c_char'>
<ctypes.LP_c_char object at 0x0000021789710E40>

8) enum 枚举类型

在 C/C++中还有1个经常用到的类型是 enum,ctypes 没有相关类型,但python有1个enum类. 下面介绍如何 将 python enum 类用于 dll 函数参数。 如 C++程序中定义了1个枚举类型

enum Priority {
        CRITICAL = 0x1,     
        IMPORTANT= 0x2,     
        NORMAL = 0x3,       
        MINOR = 0x4,    
        INFO = 0x5  
    };

有1个函数: long set_priority(Priority n) 在python中,也定义1个enum类

import enum 
class EnumPriority(enum.IntEnum):
    CRITICAL = 0x1
    IMPORTANT= 0x2
    NORMAL = 0x3
    MINOR = 0x4
    INFO = 0x5

    #注意必须将添加这个方法,ctyps 要求,将obj转为 init型
    @classmethod
    def from_param(cls, obj):
        return int(obj)

注意,必须要添加1个 from_param()方法., 将上面的枚举元素转为int型 下面将 python enum 做为参数传入dll

dll =  cdll.LoadLibrary(dll_name)
set_priority=dll.set_priority
set_priority.restype = c_init
set_priority.argtypes = [EnumPriority]
# 调用该函数
print("call back result is ",set_priority(EnumPriority.INFO))

4. 加载DLL库

Ctypes 提供有2种动态链接库的调用方式

ctypes.cdll. LoadLibrary(‘xxx.dll’) 加载 cdel调用方式的dll, 默认c++, vc++生成的dll库都是cdel方式。

ctypes.windll.LoadLibrary(‘xxx.dll’) 加载 win32调用方式(stdcall)的dll,所以很少使用此种调用方式

示例

def find_example_ctypes(required):
    '''
    Finds and loads example shared object of the required major
    '''
    # Importing ``ctypes`` should be in scope of this function to prevent failure
    from ctypes import util, cdll

    so_name = util.find_library('example.dll')
    if so_name is None:
        raise ExampleImportError('EXAMPLE shared object not found.')
    example = cdll.LoadLibrary(so_name)
    require_version(example.example_version(), required)
    return example

5. ctypes 向DLL函数传入参数

指定dll函数参数类型 通过argtypes属性来设置参数类型,

>>> strchr.restype = c_char_p
>>> strchr.argtypes = [c_char_p, c_char]

例2:

myadd = dll.bo_add
myadd.restype = c_int
myadd.argtypes = [c_int, c_int]
print("call bo_add() with function signature approach \n", myadd(20, 80))

传入指针参数 c++ 函数addx()的形参为两个指针, int addx( int p1, int p2), python 实例两个c_int型的指针传入

int_obj1 = c_int(20)
ptr1 = pointer(int_obj1)
int_obj2 = c_int(80)
ptr2 = pointer(int_obj2)
print("pass pointer to function addx(), ", dll.addx(ptr1, ptr2))

传入结构体参数

步骤: – 在python定义1个Ctypes.Structure 类, 字段也C++ Struct类型保持一致。 – 用此类构建参数值 – 传入dll函数,前面要加byref 示例 :

from ctypes import *

class MyStruct(Structure):
    _fields_ = [('shape', c_char*20),
                ('w', c_double),
                ('h', c_double),
                ('d', c_double),
                ]

第2种调用方式

myadd = dll.bo_add
myadd.restype = c_int
myadd.argtypes = [c_int, c_int]
print("another call method to bo_add() : ", myadd(20, 80))

传入结构体参数

dw, dh, dd = 4.0, 7.0, 3.0
dstr = b"cuboid "
s1 = MyStruct(dstr, dw, dh, dd)
print(s1.w, s1.h, s1.d)
print(s1.shape)
print(dll.bo_shape_vol(byref(s1)))

Output 4.0 7.0 3.0 b’cuboid ‘ cuboid volume is 84 233390192

问题: print(dll.bo_shape_vol(byref(s1))) 打印出的是地址,不是结果 84 解决办法:用类的调用方式,定义dll函数返回值类型

print("第2种调用方法") 
myVol = dll.bo_shape_vol
myVol.restype = c_double  #定义dll函数返回值类型
myVol.argtypes = [POINTER(MyStruct)]  #结构体参数是以地址方式传入,因此要转为指针
dx = myVol(byref(s1))
print(dx)

Output

第2种调用方法
cuboid volume is 84
84.0

下面是另1个例 子

from ctypes import *

class Passport(Structure):              
    _fields_ = [("name", c_char_p),    
                ("surname", c_char_p),
                ("var", c_int)]

lib_dll = cdll.LoadLibrary("DLL_example.dll")   
lib_dll.SetPassport.argtypes  = [POINTER(Passport)] 

lib_dll.GetPassport()          

lib_dll.SetName(c_char_p(b"Yury"))
lib_dll.SetSurname(c_char_p(b"Wang"))

lib_dll.GetPassport()         

name = str.encode(("Feng"))  
surname = c_char_p((b'Li')) 

passport = Passport(name, surname, 34) 

lib_dll.SetPassport(pointer(passport)) 

lib_dll.GetPassport()

Output:

Load DLL in Python
SetName
SetSurname
GetPassport: Default | Passport | 17
SetName
SetSurname
GetPassport: Yury | Orlov | 17
SetPassport
GetPassport: Vasiliy | Pupkin | 34
DETACH DLL

6. 对回调函数的支持

C++中,用函数指针非常容易地实现回调函数,python也可以实现。 第1步: 用 CFUNCTYPE() 定义1个函数指针,windows 使用 WINFUNCTYPE() 。 CFUNCTYPE()第1个参数是回调函数返回值类型,后面是函数形参。 第2步:将回调函数名赋给CFUNCTYPE变量,相当于将函数地址赋给函数指针。 第3步:将该回调函数指针做为另1个函数的形参使用。

示例如下:

from ctypes import *
import sys

# 回调函数类型定义
if 'linux' in sys.platform:
    fun_ctype = CFUNCTYPE
else:
    fun_ctype = WINFUNCTYPE

def add(int a,int b):
    return a+b

ADDFUNC = fun_ctype(c_int, c_int, c_int)
add_callback = ADDFUNC(add)
#将CFUNCTYPE变量做为函数形参
def test(x,y,ADDFUNC):
    print(f"data is {x} and {y} 
    return add_callback(x,y)

#实际测试时,把回调函数做为参数传入
test(10,20,add_callback)

总结:

1) ctypes 对c/c++ dll/so中的函数与形参,需要先申明才能使用

2)ctypes 提供了一套数据类型,用于C 数据类型与 python 数据类型的转换, 注意数组,指针类型的转换。

3) ctypes 调用 c/c++ dll/so 动态链接库函数的方法有两种

  • 函数名直接调用
  • 签名函数的方式调用

裸眼3D–原理介绍

裸眼3D基本上都是针对双目视差来说的。

什么是双目视差:人有两只眼睛,它们之间大约相隔65mm。当我们观看一个物体,两眼视轴辐合在这个物体上时,物体的映像将落在两眼网膜的对应点上。这时如果将两眼网膜重叠起来,它们的视像应该重合在一起,即看到单一、清晰的物体。根据这一事实,当两眼辐合到空间中的一点时,我们可以确定一个假想的平面,这个平面上的所有各点都将刺激两眼网膜的对应区域。这个表面就叫做视觉单像区(horopter)。它可以定义为在一定的辐合条件下,在视网膜对应区域的成像空间中所有各点的轨迹。位于视觉单像区的物体,都将落在视网膜对应点而形成单个的映像。

如果两眼成像的网膜部位相差太大,那么人们看到的将是双像,即把同一个物体看成两个。例如,我们用右手举起一支铅笔,让它和远处墙角的直线平行。这时如果我们注视远处墙角的直线,那么近处的铅笔就将出现双像;如果我们注视近处的铅笔,远处的墙角直线就将出现双像。

正因为双目视差,才会让我们看到的物体有纵深感和空间感。

裸眼3D是怎么做到蒙骗双眼来营造空间和纵深感呢,现在的3D视频或者图像都是通过区分左右眼来拍摄的两幅图,视差距约为65mm,通过让你左眼看到左眼的图像,右眼看到右眼的图像就可以让你的大脑合成一副有纵深感的立体画面。

人的两只眼睛相距约6cm,就像两部相距6cm放置的照相机,拍出的照片角度会有一点点不同(侈开)。

这种侈开在大脑里就可以融合成立体的感觉。

我们再抽丝剥茧制作一个最简单的侈开立体图:

越简单的图越容易说明原理,但观看起来越消耗眼睛“内功”。请您用原理一的透视方法,让左眼看左图,右眼看右图,当您能看到三个双圈的时候,中间那个小圆就会凸出纸面呈现立体感

静态库(静态链接库lib/a)和动态库(动态链接库dll/so)

方法库大体上可以分为两类:静态库和动态库(共享库)。

1. windows中静态库是以 .lib 为后缀的文件,动态库是以 .dll 为后缀的文件。

2. linux中静态库是以 .a 为后缀的文件,动态库是以 .so为后缀的文件。

静态链接:

  • 静态库 在链接阶段,会将汇编生成的目标文件.o与引用到的库一起链接打包到可执行文件中。因此对应的链接方式称为静态链接。
  • 静态库可以简单看成是一组目标文件(.o .obj文件)的集合, 将若干个.o文件转换为静态库的过程,称之为打包. Linux下是使用ar工具, Windows下是使用lib.exe。
  • Linux下静态链接库的后缀是.libWindows下静态链接库的后缀是.a

动态链接:

  • 动态库在程序编译时并不会被连接到目标代码中,而是在程序运行时才被载入。不同的应用程序如果调用相同的库,那么在内存里只需要有一份该共享库的实例,规避了空间浪费问题。动态库在程序运行是才被载入,也解决了静态库对程序的更新、部署和发布页会带来麻烦。用户只需要更新动态库即可,增量更新。
  • 在Windows系统下的执行文件格式是PE(Portable Executable)格式,动态库需要一个DllMain函数做出初始化的入口,通常在导出函数的声明时需要有_declspec(dllexport)关键字。 跟exe有个main或者WinMain入口函数一样,DLL也有一个入口函数,就是DllMain。根据编写规范,Windows必须查找并执行DLL里的DllMain函数作为加载DLL的依据,它使得DLL得以保留在内存里。这个函数并不属于导出函数,而是DLL的内部函数。这意味着不能直接在应用工程中引用DllMain函数,DllMain是自动被调用的。
    对于动态链接库,DllMain是一个可选的入口函数。一个动态链接库不一定要有DllMain函数,比如仅仅包含资源信息的DLL是没有DllMain函数的。
  • Linux下gcc编译的执行文件默认是ELF格式,不需要初始化入口,亦不需要函数做特别的声明,编写比较方便。
    无需打包工具,直接使用编译器即可创建动态库。
  • Linux下动态链接库的后缀是.so;Windows下动态链接库的后缀是.dll

一、动态链接库创建和使用

1.创建hello.so动态库

#include <stdio.h>void hello(){	printf("hello world\n");}编译:gcc -fPIC -shared hello.c -o libhello.so

2.hello.h头文件

void hello();

3.链接动态库

#include <stdio.h>#include "hello.h" int main(){	printf("call hello()");	hello();}编译:gcc main.c -L. -lhello -o main

这里-L的选项是指定编译器在搜索动态库时搜索的路径,告诉编译器hello库的位置。”.”意思是当前路径.

3.编译成够后执行./main,会提示:

In function `main':
 
main.c:(.text+0x1d): undefined reference to `hello'
collect2: ld returned 1 exit status

这是因为在链接hello动态库时,编译器没有找到。
解决方法:

sudo cp libhello.so /usr/lib/

这样,再次执行就成功输入:
call hello()

二、静态库 创建和使用

文件有:main.c、hello.c、hello.h
1.编译静态库hello.o: 

gcc hello.c -o hello.o  #这里没有使用-shared

2.把目标文档归档

ar -r libhello.a hello.o  #这里的ar相当于tar的作用,将多个目标打包。

程序ar配合参数-r创建一个新库libhello.a,并将命令行中列出的文件打包入其中。这种方法,如果libhello.a已经存在,将会覆盖现在文件,否则将新创建。

3.链接静态库

gcc main.c -lhello -L. -static -o main

这里的-static选项是告诉编译器,hello是静态库。
或者:

gcc main.c libhello.a -L. -o main

这样就可以不用加-static

4.执行./main

输出:call hello()

区别:

可执行文件大小不一样

从前面也可以观察到,静态链接的可执行文件要比动态链接的可执行文件要大得多,因为它将需要用到的代码从二进制文件中“拷贝”了一份,而动态库仅仅是复制了一些重定位和符号表信息。

占用磁盘大小不一样

如果有多个可执行文件,那么静态库中的同一个函数的代码就会被复制多份,而动态库只有一份,因此使用静态库占用的磁盘空间相对比动态库要大。

扩展性与兼容性不一样

如果静态库中某个函数的实现变了,那么可执行文件必须重新编译,而对于动态链接生成的可执行文件,只需要更新动态库本身即可,不需要重新编译可执行文件。正因如此,使用动态库的程序方便升级和部署。

依赖不一样

静态链接的可执行文件不需要依赖其他的内容即可运行,而动态链接的可执行文件必须依赖动态库的存在。所以如果你在安装一些软件的时候,提示某个动态库不存在的时候也就不奇怪了。

即便如此,系统中一般存在一些大量公用的库,所以使用动态库并不会有什么问题。

复杂性不一样

相对来讲,动态库的处理要比静态库要复杂,例如,如何在运行时确定地址?多个进程如何共享一个动态库?当然,作为调用者我们不需要关注。另外动态库版本的管理也是一项技术活。这也不在本文的讨论范围。

加载速度不一样

由于静态库在链接时就和可执行文件在一块了,而动态库在加载或者运行时才链接,因此,对于同样的程序,静态链接的要比动态链接加载更快。所以选择静态库还是动态库是空间和时间的考量。但是通常来说,牺牲这点性能来换取程序在空间上的节省和部署的灵活性时值得的。再加上局部性原理,牺牲的性能并不多。

Geometry-Free View Synthesis:基于transformer的大尺度转换的新视角合成

Geometry-Free View Synthesis: Transformers and no 3D Priors

引入一种基于 transformers 的概率方法,用于从具有大视角变化的单一源图像中进行新视图合成。作者对transformers 的各种显式和隐式 3D 感应偏置进行比较,结果表明,在架构中显式使用 3D 变换对其性能没有帮助。此外,即使没有深度信息作为输入,模型也能学会在其内部表示中推断深度。这两种隐式 transformer 方法在视觉质量和保真度上都比目前的技术状态有显着的改进。

将开源:https://github.com/CompVis/geometry-free-view-synthesis

论文:https://arxiv.org/abs/2104.07652

自回归transformer模型
问题:大角度变换,CNNs需要几何模型。
解决:不需要任何手工设计的三维偏差:(i)一种隐式学习源视图和目标视图之间的远程三维对应关系的全局注意机制来实现。 (ii)捕捉从单个图像预测新视图所固有的歧义所必需的概率公式,从而克服了以前局限于相对较小的视角变化的方法的局限性。(iii)发现以显式深度图的形式提供它们几何信息的好处相对较小,并研究了从transformer的层中恢复显式深度表示的能力,它已经学会了隐式地表示几何变换,而无需任何深度监督,甚至可以学习以无监督的方式预测深度

相关工作:本文从GPT-2架构构建自适应transformer,即,多块多头自注意,层规范和位置向MLP。自回归两阶段方法:是基于在神经离散表示学习(VQVAE)中的工作,它旨在通过向量量化或离散分配的软松弛来学习一个离散的压缩表示。这种训练范式提供了一个合适的空间来训练潜在表示上的自回归似然模型,并已被用于训练生成模型来训练分层,类条件图像合成、文本控制图像合成和音乐生成。最近,证明了VQVAE的对抗性训练在保持高保真度重建的同时提高了压缩性能,随后,我们能够在学习的潜在空间上有效地训练自回归transformer模型,(产生一个所谓的VQGAN)。我们直接在这项工作的基础上,使用VQGANs来表示源视图和目标视图,并在需要时表示深度图。
方法:由于不确定性,我们遵循一种概率方法,并从以下分布中采样新视角。

潜在空间中的概率视图合成:
为了学习上式中分布,需要捕获源视图和目标视图之间的远程交互的模型,以隐式地表示几何变换。由于基于相似的模型已经被证明直接在像素空间中建模图像时,在像素的短程交互上花费了太多的容量,我们遵循VQGAN并采用了两个阶段训练。第一阶段执行反向(对抗性)引导的离散表示学习(VQGAN),获得一个抽象的潜在空间,已被证明非常适合有效地训练生成式transformer。
建模条件图像模型
VQGAN包括一个编码器E,解码器G和一个离散表征zi(dz)的codebook Z。训练后的VQGAN允许编码任意x(HxWx3)到离散隐空间E(x)(h x w x dz)。以栅格扫描的顺序展开,这个潜在的表示形式对应于一个序列s(h x w x dz),可以等价地表示为一个整数序列,索引已学习的码本。按照通常的名称,我们将序列元素称为“tokens”。一个嵌入函数g=g(s) (hw x de)将每个tokens映射到transformer的嵌入空间中,并添加了可学习的位置编码。类似地,为了编码输入视图xsrc和照相机转换T,两者都由一个函数f映射到嵌入空间中:

其中,n表示条件处理序列的长度。通过使用不同的函数不同的归纳偏差被纳入架构。然后,transformer T处理所连接的序列[f(xsrc,T),g(sdst)]去学习以xsrc和T为条件的合理的新观点的分布。

编码Inductive Biases:比较几何变换的方法被显式地构建到条件函数f中,和没有使用这种转换的方法。
几何图像扭曲:这一部分说明显式几何变换怎么运用。我们假设有一个针孔照相机模型,这样三维点到齐次像素坐标的投影通过相机固有矩阵K确定。源坐标和目标坐标之间的转换是由旋转R和平移t组成的刚性运动给出的。这些参数一起指定了对要生成的新视图的所需控制,如T=(K,R,T)。

此关系定义了一个从源到目标的前向流场作为深度和相机参数的函数。然后,流场可以通过扭曲操作S将源图像xsrc扭曲到目标视图中。由于从流中获得的目标像素不一定是整数值,因此我们遵循,并通过跨越四个最近的目标像素的双线性溅射特征来实现S。当多个源像素映射到相同的目标像素时,我们使用它们的相对深度给予离相机更近的点权重——这是z缓冲的软变体。
在最简单的情况下,我们现在可以描述显式方法和隐式方法在接收关于源图像和期望的目标视图的信息的方式之间的区别。此处,显式方法接收使用相机参数扭曲的源信息,而隐式方法接收原始源图像和相机参数本身,如:

显式几何变换
在下面,我们用transformer的条件函数f来描述所有被考虑的变体。此外,e还表示一个可学习的嵌入映射离散的VQGAN代码E(x)到转换器的嵌入空间。相似地,e pos(n x de)指一个可学习的位置编码。流场F总是从xsrc计算,以提高可读性,我们从扭曲操作的参数中忽略它,S(.)=S(Fsrc->dst(K,R,t,d)).
(1)我们的第一个显式变体,expl.-img,会扭曲源图像,并以与目标图像相同的方式对其进行编码:

(2)受之前的作品的启发,我们包含了一个expl.-feat变体,它首先编码原始源图像,然后在这些特征之上应用warp。我们再次使用了VQGAN编码器E,以获得:

(3)解释上式中扭曲的特征保持固定状态。

隐式几何变换:接下来,我们描述我们用来分析的隐式变量,transformer能否同样好地处理所有位置,是否需要在模型中内置一个显式的几何转换。我们使用与显式变体相同的符号。
(4)第一个变体impl.-catdepth为transformer提供了显式变体中使用的所有相同组件:相机参数K、R、t、估计深度d和源图像xsrc。相机参数被拉平并连接到T^,通过Wcam(de x 1)映射到嵌入空间。深度和源图像被VQGAN编码器Ed和E编码来获得

与其他变体相比,这个序列大约长32倍,这是计算成本的两倍。
(5)因此,我们还包括了一个impl.-depth变体,它连接了深度和源图像的离散代码,并用一个矩阵W(de x 2dz)映射它们到嵌入空间以避免序列长度增加:

(6)隐式方法提供了一个有趣的可能性:因为它们不需要明确地估计该深度来执行扭曲操作S,所以它们在没有这样的深度估计的情况下具有解决该任务的潜力。因此,输入深度仅使用相机参数和源图像-根据我们的任务描述的最低限度。Impl.nodepth:

(7)最后,我们分析了显式方法和隐式方法是否提供了互补的优势。因此,我们添加了一个混合变体,其条件函数是方法(3)中expl.-emb和方法(5)中impl.-depth的f的结合。
深度读数输出:为了研究学习不同视图之间几何关系的隐式模型的能力,我们建议从一个训练过的模型中提取一个深度的显式估计。为此,我们使用线性探测,通常用于研究无监督方法的特征质量。更具体地说,我们假设一个由L层和impl.nodepth类型组成的transformer模型,它仅基于源框架和变换参数。接下来,我们指定一个特定的层0≤l≤L(其中l=0表示输入),并提取它的潜在表示el,对应于所提供的源框架xsrc的位置。然后,我们训练一个逐位置线性分类器W来预测深度编码器Ed的离散的、潜在的表示,通过一个来自el的交叉熵目标。请注意,transformer和VQGAN的权重均保持不变。
隐式,显式transformer比较

数据集: RealEstate and ACID.
其他实验:比较密度估算的质量,实现预测的可视化熵,测量图像质量和保真度,与以前的方法进行比较,对几何图形的探测

3D Photography using Context-aware Layered Depth Inpainting

CVPR 2020 | 3D Photography:一张照片也能动起来

https://arxiv.org/abs/2004.04727

3D 相片是这两年比较热门的研究方向,Facebook App 已经支持分享 3D 相片,相信在不久的未来大家便可以在朋友圈、微博上分享炫酷的 3D 相片。

本文作者来自弗吉尼亚理工学院、国立清华大学和 Facebook,作者提出了一种从 单张 RGB-D 图像生成 3D 照相 的方法,效果炫酷、惊艳,目前代码已开源。

下面先展示一下本文的一些结果,镜头晃动、zoom-in 效果,毫无违和感。

人物、动物各种场景全都不在话下,一张照片也能立刻动起来!

3D相片的生成主要基于重建和渲染技术,传统的方法需要基线较长的精密的多视角图片捕捉设备或者其他特殊装置,如 Lytro Immerge 和 Facebook Manifold camera。最近有更多的工作尝试从智能相机来生成3D相片,如 Facebook 3D Photos 只需用双摄智能手机拍摄图片,生成RGB-D图像 (彩色图+深度图) 来制作3D相片

本文同样考虑如何从输入的 RGB-D 图像来合成新的视角以生成3D照片。文章方法对深度图的质量要求并不高,只需要深度不连续处在彩色图和深度图中是合理对齐的即可。深度图可以从 双摄相机通过立体视觉的方式计算 得到,也可以借助 深度学习的方法从单张图片估计 得到,因此应用到智能手机完全没有问题,作者也对这两种来源的深度图进行了测试。

Method

Layered Depth Image

文章方法输入一张 RGB-D 图像,输出分层的深度图像 (Layered Depth Image, LDI),在原始图像中被遮挡的部位填补了颜色和深度。

LDI 类似普通的图像,区别在于每个像素点可以容纳零个或多个像素值,每个 LDI 像素存储一个颜色和一个深度值。与原始论文介绍的 LDI 不同,本文作者显式地表示了像素的局部连通性:每个像素在左右上下四个方向上都存储了零个或最多一个直接相邻的像素指针。LDI 像素与普通图像一样在光滑区域内是四连通的,但是在深度不连续处没有邻接像素。

LDI 是一种对3D相片非常有用的表达,主要在三个方面

  1. 其可以处理任意数量的深度层,可以根据需要适应深度复杂的情况;
  2. 其表达是稀疏的,具有更高的内存和存储效率;
  3. 其可以转换为轻量级的纹理网格表示,直接用于快速渲染。

Method Overview

给定输入的 RGB-D 图像,首先初始化一个单层的四连通的简单 LDI。然后进入预处理阶段,检测深度不连续像素点,并将其分组成简单的相连的深度边。文章算法反复选择深度边来进行修复,先断开边缘上的 LDI 像素,仅考虑边缘处的背景像素进行修复,从边缘的 “已知” 侧提取局部语境区域 (context region),并在 “未知” 侧生成一个合成区域 (synthesis region),合成的区域是一个包含新像素的连续2D区域。作者使用基于学习的方法根据给定的上下文生成其颜色和深度值。修复完成后再将合成的像素合并回 LDI。整个方法以这种方式反复进行,直到所有的深度边缘都经过处理。

Image Preprocessing

初始化阶段首先将输入的 RGB-D 图的深度通道归一化到 0-1 之间,并对深度图进行双边中值滤波,以使得边缘更加明显,再基于此图片生成初始 LDI。然后再根据给定阈值判断相邻像素的视差,找到深度不连续像素,并经过一些简化、处理得到最终的深度不连续边。

Preprocessing

Context and Synthesis Regions

接下来每次选择一条深度边借助填补算法来修复背景,首先在深度不连续处断开 LDI 像素连接,得到 (前景、背景) 轮廓像素,然后生成一个合成区域,使用洪水漫淹算法初始化颜色和深度值,再使用深度学习的方法填补该合成区域。

LDI Inpainting

Context-aware Color and Depth Inpainting

给定语境区域和合成区域,这里的目标是合成颜色值和深度值。作者的网络与 EdgeConnect[2] 方法类似,将整个修复任务分解成三个子网络:

  • 边修复网络 (edge inpainting network)
  • 颜色修复网络 (color inpainting network)
  • 深度修复网络 (depth inpainting network)

首先将语境区域的边作为输入,使用边修复网络预测合成区域中的深度边,先预测边信息能够推断 (基于边的) 结构 信息,有助于约束 (颜色和深度的) 内容 预测。然后使用修复的边和语境区域的颜色作为输入,使用颜色修复网络预测颜色。最后再使用同样的方法预测深度信息。

下图展示了边指导的深度修复能够准确地延拓深度结构,并能减轻预测的彩色 / 深度不对齐的问题。

Converting to 3D Textured Mesh

通过将所有修复好的颜色和深度值重新集成到原始 LDI 中,形成最终的 3D 纹理网格。使用网格表示可以快速渲染新的视图,而无需对每个视角进行推理,因此文章算法得到的3D表示可以在边缘设备上通过标准图形引擎轻松渲染。

Experimental Results

Visual Comparisons

下图展示了文章方法与其他基于 MPI (Multi-Plane Representation) 方法的对比,文章方法能够合成较为合理的边缘结构,StereoMag 和 PB-MPI 方法在深度不连续处存在缺陷,LLFF 在生成新视角时会有鬼影现象。

作者将文章方法与 Facebook 3D Photos 进行了比较。通过 iPhone X 采集的彩色图和估计的深度图作为两种方法的输入,下图是部分对比结果展示,文章方法能够合成更为合理的内容和结构信息。

代码测试

参考官方GitHub仓库,步骤如下

# 创建pytorch虚拟环境

# 下载代码
git clone https://github.com/vt-vl-lab/3d-photo-inpainting.git
cd 3d-photo-inpainting
./download.sh  # 下载预训练模型
(pytorch) $ python main.py --config argument.yml # 运行代码

参考

  1. 3D Photography using Context-aware Layered Depth Inpainting. Meng-Li Shih, Shih-Yang Su, Johannes Kopf, Jia-Bin Huang. CVPR, 2020.
  2. EdgeConnect: Generative Image Inpainting with Adversarial Edge Learning. Kamyar Nazeri, Eric Ng, Tony Joseph, Faisal Z. Qureshi, Mehran Ebrahimi. ICCV, 2019.

计算机图形学:变换矩阵

最近在研究3D建模和新视点合成,在渲染过程中需要选取新视点去合成新图。一般在接口处需要传递一个变换矩阵,用于控制视点的变化。

成像的过程实质上是几个坐标系的转换。首先空间中的一点由世界坐标系转换到 摄像机坐标系 ,然后再将其投影到成像平面 ( 图像物理坐标系 ) ,最后再将成像平面上的数据转换到图像平面 (图像像素坐标系 ) 。

以AdaMPI的代码为例:

# 定义新视角的角度和渲染的帧数
def gen_swing_path(num_frames=90, r_x=0.14, r_y=0., r_z=0.10):
    "Return a list of matrix [4, 4]"
    t = torch.arange(num_frames) / (num_frames - 1)
    poses = torch.eye(4).repeat(num_frames, 1, 1)
    poses[:, 0, 3] = r_x * torch.sin(2. * math.pi * t)
    poses[:, 1, 3] = r_y * torch.cos(2. * math.pi * t)
    poses[:, 2, 3] = r_z * (torch.cos(2. * math.pi * t) - 1.)
    return poses.unbind()

以Synsin代码为例:

# Parameters for the transformation
theta = -0.15
phi = -0.1
tx = 0
ty = 0
tz = 0.1

RT = torch.eye(4).unsqueeze(0)
# Set up rotation(旋转参数)
RT[0,0:3,0:3] = torch.Tensor(quaternion.as_rotation_matrix(quaternion.from_rotation_vector([phi, theta, 0])))
# Set up translation(平移参数)
RT[0,0:3,3] = torch.Tensor([tx, ty, tz])

一开始其实没有明白为什么需要对 r_x=0.14, r_y=0., r_z=0.10 进行处理,处理成4*4的矩阵形式,而不是直接使用,后来查阅资料发现应该是涉及到计算机图形学的变换矩阵的范畴。

计算机图形学中3D的变换是基于转换矩阵( 仿射空间 )进行的。那么为什么是4维的矩阵而不是3维:用第四维度标识向量 or 点。

模型的变换可以认为是空间中一堆点的变换,三维空间中,(x,y,z)可以认为是点,也可以认为是一个向量,因此,人们引入的第4个维度来标识是点还是向量,这个4维空间就叫 仿射空间,,在仿射空间中,(x,y,z,0)标识向量,而(x,y,z,1)表示点。

在图形学中,在做平移,旋转和缩放时,经常会用到矩阵,有缩放矩阵、平移矩阵和旋转矩阵。在三维空间中,变换矩阵都是一个四维矩阵,每一行分别表示x, y, z, w。

1. 缩放矩阵(scale)

上面的公式,左边的第一个操作数(四维矩阵)就是一个缩放矩阵,s1表示x轴的缩放倍数,s2表示y轴的缩放倍数,s3表示z轴的缩放倍数。第二个操作数表示空间中(x, y, z)点, w分量在缩放矩阵中没有用到,我们将其设为1。由右边的结果,可以看出(x, y, z)点经过缩放矩阵变换后,x、y、z分量都各自缩放了s(s1、s2、s3)倍。需要注意的是矩阵的乘法不具有交换律,这里点是用一维列矩阵表示的,作为矩阵乘法的右操作数。如果将其转换到乘法的左边,那么点应该用一维行矩阵表示:

缩放矩阵比较简单,不需要证明,只需要会简单的乘法,就可以看出x,y,z经过缩放矩阵的变换确实被缩放了。

2.平移矩阵(translation)

平移矩阵也称位移矩阵,平移矩阵用到了第四列(w列),这也是为什么三维空间变换矩阵为什么是四维的。平移矩阵也比较容易理解,因为可以通过结果看出想x 、y、z确实各自平移了T步。

3. 旋转矩阵

旋转矩阵,相对难一些,也不是那么容易理解,我们先看最基础的绕x、y、z轴旋转的旋转矩阵。

沿x轴:

沿y轴:

沿z轴:

引入了三角函数,我们无法从结果看出旋转矩阵是否正确,所以我们需要证明。下面我给出沿z轴旋转的变换矩阵证明过程,其他轴同理可证。

image-20210521112417466

假设有如图的点p1,因为绕z轴旋转,点的z值是不变的,我们将其设为0,这样可以将其模拟成二维平面xy中旋转。假设p1绕原点旋转b角度,初始角度为a。整个证明过程如下:

// 经过旋转后向量的长度不变为L(原点到p1和p2的距离相同)
// 由三角函数得到sin(a + b), cos(a + b)的值
cos(a + b) = x2 / L;
sin(a + b) = y2 / L;

// 展开sin(a + b)和cos(a + b)
cos(a) * cos(b) - sin(a) * sin(b) = x2 / L;
sin(a) * cos(b) + cos(a) * sin(b) = y2 / L;

// 用x和y表示cos(a)和sin(a)
x / L * cos(b) - y / L * sin(b) = x2 / L;
y / L * cos(b) + x / L * sin(b) = y2 / L;

// 等式两边同时乘以L
x * cos(b) - y * sin(b) = x2;
y * cos(b) + x * sin(b) = y2;

将x2和y2的结果与上面z轴旋转矩阵结果比较,发现是完全一样的。

按照上面的方法同理可证绕x轴旋转和绕z轴旋转的矩阵。

那么绕任意轴旋转的矩阵呢?learnOpengl_cn官网直接给出了绕任意轴旋转的矩阵,(Rx, Ry, Rz)表示任意轴,θ表示旋转的矩阵。这个矩阵证明比较复杂。

PyTorch3D:面向3D计算机视觉的PyTorch工具箱

PyTorch3D通过PyTorch为3D计算机视觉研究提供高效,可重复使用的组件。目前已基于此开发了:Mesh R-CNN、SynSin等模型。

Facebook开源了一个专门用于3D模型学习的库pytorch3d,说白了就是将3d中一些常用的操作封装起来了。那这个玩意到底有啥用呢?使用这个库又能完成什么事情呢?个人觉得这个库还是蛮有用的,它将一些常用的3D概念整理在一起,并且通过使用这个库可以完成一些基于3D的创作,对于学习入门3D的视觉生成、渲染、甚至是3d的目标检测、3维的姿态评估都大有裨益。

Pytorch3D_上手学习3D的AI模型

Accelerating 3D Deep Learning with PyTorch3D

文档:Welcome to PyTorch3D’s documentation!
项目链接:facebookresearch/pytorch3d
论文:https://arxiv.org/abs/2007.08501

PyTorch3D

主要功能包括:

  • 用于存储和操作 triangle meshes的数据结构
  • 在 triangle meshes上的有效操作(投影变换,图卷积,采样,损失函数)
  • 可微的mesh渲染器

PyTorch3D旨在与深度学习方法稳定集成,以预测和处理3D数据。 因此,PyTorch3D中的所有运算符:

  • 使用PyTorch张量实现
  • 可以处理小批量的异构数据
  • 可以differentiated
  • 可以利用GPU进行加速

深度学习已大大改善了2D图像识别。扩展到3D可能会推动许多新应用的发展,包括自动驾驶汽车,虚拟现实和增强现实,创作3D内容,甚至改善2D识别。然而,尽管兴趣日益浓厚,但3D深度学习仍相对未得到充分开发。我们认为,这种差异是由于3D深度学习所涉及的工程挑战所致,例如有效处理异构数据和将图形操作重构为可微的。

我们通过引入PyTorch3D来应对这些挑战,PyTorch3D是一个用于3D深度学习的模块化,高效且可微的运算符库。它包括一个用于网格和点云的快速,模块化,可微的渲染器,支持按合成进行分析的方法。

与其他可微的渲染器相比,PyTorch3D更具模块化和效率,允许用户更轻松地扩展它,同时还可以优雅地缩放到较大的网格和图像。我们将PyTorch3D运算符和渲染器与其他实现进行了比较,并展示了显著的速度和内存改进。我们还使用PyTorch3D改进了ShapeNet上2D图像的无监督3D网格和点云预测的最新技术。

PyTorch3D是开源的,我们希望它将有助于加速3D深度学习的研究。

实验结果

命令行从 Google Drive下载数据

本文介绍了通过wgetcurl从Google Drive上下载文件的脚本。因为大量的数据集都存放在Google Drive上,而计算任务通常都是在计算服务器上,直接从Google Drive这类云盘下载到服务器上显然是最合适的方式。

分享链接

Google Drive的分享链接格式通常为:

https://drive.google.com/file/d/<fileid>/view

其中这个<fileid>就是对应文件在服务器上的唯一标识符。

例如OfficeHome数据集在Google Drive上的链接即为:

https://drive.google.com/file/d/0B81rNlvomiwed0V1YUxQdC1uOTg/view

其中的<fileid>0B81rNlvomiwed0V1YUxQdC1uOTg,文件名可以自己取。

所以提取到的关键变量为:

filename=’OfficeHomeDataset_10072016.zip’
fileid=’0B81rNlvomiwed0V1YUxQdC1uOTg’

wget 下载指令

针对小文件:

wget –no-check-certificate “https://drive.google.com/uc?export=download&id=${fileid}” -O ${filename}

如果文件大的话,需要对cookie进行处理:

wget –load-cookies /tmp/cookies.txt “https://drive.google.com/uc?export=download&confirm=$(wget –quiet –save-cookies /tmp/cookies.txt –keep-session-cookies –no-check-certificate ‘https://drive.google.com/uc?export=download&id=${fileid}’ -O- | sed -rn ‘s/.confirm=([0-9A-Za-z_]+)./\1\n/p’)&id=${fileid}” -O ${filename} && rm -rf /tmp/cookies.txt

因为Google drive的大文件,无法通过安全查杀

整理成更方便的脚本示例为:

#!/bin/bash

# cd scratch place
cd data/

# Download zip dataset from Google Drive
filename=’OfficeHomeDataset_10072016.zip’
fileid=’0B81rNlvomiwed0V1YUxQdC1uOTg’
wget –load-cookies /tmp/cookies.txt “https://drive.google.com/uc?export=download&confirm=$(wget –quiet –save-cookies /tmp/cookies.txt –keep-session-cookies –no-check-certificate ‘https://drive.google.com/uc?export=download&id=${fileid}’ -O- | sed -rn ‘s/.confirm=([0-9A-Za-z_]+)./\1\n/p’)&id=${fileid}” -O ${filename} && rm -rf /tmp/cookies.txt

# Unzip
unzip -q ${filename}
rm ${filename}
cd

curl 下载指令

小文件 < 40MB:

curl -L -o ${filename} “https://drive.google.com/uc?export=download&id=${fileid}”

大文件 > 40MB:

curl -c ./cookie -s -L “https://drive.google.com/uc?export=download&id=${fileid}” > /dev/null
curl -Lb ./cookie “https://drive.google.com/uc?export=download&confirm=`awk ‘/download/ {print $NF}’ ./cookie`&id=${fileid}” -o ${filename}
rm ./cookie

整理成脚本:

#!/bin/bash

# cd scratch place
cd scratch/

# Download zip dataset from Google Drive
filename=’OfficeHomeDataset_10072016.zip’
fileid=’0B81rNlvomiwed0V1YUxQdC1uOTg’
curl -c ./cookie -s -L “https://drive.google.com/uc?export=download&id=${fileid}” > /dev/null
curl -Lb ./cookie “https://drive.google.com/uc?export=download&confirm=`awk ‘/download/ {print $NF}’ ./cookie`&id=${fileid}” -o ${filename}
rm ./cookie

# Unzip
unzip -q ${filename}
rm ${filename}

# cd out
cd

但是如果连不上外网,那就需要一个梯子的选项,即proxychains4这个命令行代理工具,或者需要通过其他机器进行联网的中转可以参考离线服务器通过PC中转连接公网

对于不能直接联网的机器运行脚本方式为:

proxychains4 sh download_google_drive.sh

参考资源