[Reconstrucción 3D] [Aprendizaje profundo] Implementación de Pytorch del código NeRF: carga de datos (Parte 1)

[Reconstrucción 3D] [Aprendizaje profundo] Implementación de Pytorch del código NeRF: carga de datos (Parte 1)

El artículo propone un campo de radiación neuronal 5D como una representación implícita de escenas complejas, llamado NeRF, que ingresa imágenes dispersas de poses de múltiples ángulos para entrenar y obtener un modelo de campo de radiación neuronal. En pocas palabras, realiza un modelado implícito tridimensional de la escena ingresando imágenes bidimensionales y poses de la cámara desde diferentes perspectivas de la misma escena, y realiza la síntesis de imágenes de la escena desde cualquier perspectiva nueva a través de la ecuación de representación de vóxeles. Esta publicación de blog analizará el código del módulo de función específico en el proceso de carga de datos en función del proceso de ejecución del código.



Prefacio

Antes de analizar la red NeuS en detalle, la primera tarea es construir el entorno operativo requerido por NeRF [ tutorial de referencia en win10 ] y completar la capacitación y prueba del modelo, solo entonces tendrá sentido realizar trabajos de seguimiento.
Esta publicación de blog analiza algunos de los módulos de código funcional involucrados en el proceso de carga de datos de NeuS. Otros módulos de código se explicarán en publicaciones de blog posteriores.

El blogger ha analizado en detalle los códigos de cada módulo funcional en diferentes publicaciones de blog. Haga clic en [Tutorial de referencia en Win10] y el enlace del directorio de la publicación del blog se colocará en el prefacio.


cargar_llff_data

Se llama en la función de tren del archivo run_nerf.py.

# 加载llff格式数据集
images, poses, bds, render_poses, i_test = load_llff_data(args.datadir, args.factor,
                                                                  recenter=True, bd_factor=.75,
                                                                  spherify=args.spherify)

load_llff_data está en el archivo load_llff.py, debido a que hay demasiado contenido, el blogger explicará el código de esta función en segmentos. _load_data carga las imágenes, poses y límites del conjunto de datos; recenter_poses realiza transformación de rotación y traducción en todas las poses de la cámara para completar la normalización.

# poses[3,5,N] N是数据集个数, 3×3是旋转矩阵R,3×1(第4列)是平移矩阵T,3×1(第5列)是h,w,f
# bds[2,N] 采样far,near信息,即深度值范围
# imgs[h,w,c,N]
poses, bds, imgs = _load_data(basedir, factor=factor) # factor=8 downsamples original imgs by 8x
print('Loaded', basedir, bds.min(), bds.max())

# 列变换 [x,y,z,t,whf]--->[y,-x,z,t,whf]
poses = np.concatenate([poses[:, 1:2, :], -poses[:, 0:1, :], poses[:, 2:, :]], 1)  # [3,5,N]

# 变换维度:调换第-1轴到第0轴位置
poses = np.moveaxis(poses, -1, 0).astype(np.float32)    # [N,3,5]
imgs = np.moveaxis(imgs, -1, 0).astype(np.float32)      # [N,h,w,c]
images = imgs
bds = np.moveaxis(bds, -1, 0).astype(np.float32)        # [N,2]

# 获得缩放因子,以bds.min为基准,有点类似归一化
sc = 1. if bd_factor is None else 1./(bds.min() * bd_factor)
# 对位姿的平移矩阵t进行缩放
poses[:, :3, 3] *= sc
# 对边界进行缩放
bds *= sc

# 计算pose的均值,将所有pose做个均值逆转换
if recenter:
    poses = recenter_poses(poses)

El siguiente fragmento de código sirve para completar la conversión del formato de pose.

# 列变换 [x,y,z,t,whf]--->[y,-x,z,t,whf]
poses = np.concatenate([poses[:, 1:2, :], -poses[:, 0:1, :], poses[:, 2:, :]], 1)  # [3,5,N]

Diagrama esquemático del sistema de coordenadas de la cámara después de la transformación de columnas:


_Cargar datos

Cargue la imagen redimensionada (opcional), la pose y la información de límites de bds del conjunto de datos. La función _minify realiza una operación de cambio de tamaño en la imagen original.

def _load_data(basedir, factor=None, width=None, height=None, load_imgs=True):
    # 存放数据集的位姿和边界信息(深度范围) [images_num,17] eg:[20,17]
    poses_arr = np.load(os.path.join(basedir, 'poses_bounds.npy'))

    # [images_num,17]--->[3,5,images_num] eg:[3,5,20]
    # 3×3是旋转矩阵R,3×1(第4列)是平移矩阵T,3×1(第5列)是h,w,f
    poses = poses_arr[:, :-2].reshape([-1, 3, 5]).transpose([1, 2, 0])

    # bounds 边界(深度)范围[2,images_num] eg:[2,20]
    bds = poses_arr[:, -2:].transpose([1, 0])

    # 获取第一张图像的地址,图像必须是jpg或png格式
    img0 = [os.path.join(basedir, 'images', f) for f in sorted(os.listdir(os.path.join(basedir, 'images'))) \
            if f.endswith('JPG') or f.endswith('jpg') or f.endswith('png')][0]

    # 获取图像shape[h,w,c]
    sh = imageio.imread(img0).shape

    # 文件名后缀:
    sfx = ''
    # 按照要求resize图像
    # 下采样倍数
    if factor is not None:
        sfx = '_{}'.format(factor)
        _minify(basedir, factors=[factor])
        factor = factor
    # 指定分辨率中的高度
    elif height is not None:
        # 计算出原始尺寸高度和指定高度的比值
        factor = sh[0] / float(height)
        # 按照比值计算出对应的宽度
        width = int(sh[1] / factor)
        # 指定分辨率的方式resize
        _minify(basedir, resolutions=[[height, width]])
        sfx = '_{}x{}'.format(width, height)
    # 指定分辨率中的宽度
    elif width is not None:
        # 计算出原始尺寸高度和指定宽度的比值
        factor = sh[1] / float(width)
        # 按照比值计算出对应的高度
        height = int(sh[0] / factor)
        # 指定分辨率的方式resize
        _minify(basedir, resolutions=[[height, width]])
        sfx = '_{}x{}'.format(width, height)
    else:
        # 不进行尺寸上的任何处理
        factor = 1
    # 获取新尺寸图像的保存路径
    imgdir = os.path.join(basedir, 'images' + sfx)
    # 判断目录是否存在
    if not os.path.exists(imgdir):
        print(imgdir, 'does not exist, returning' )
        return
    # 获取所有新尺寸图像的文件路径
    imgfiles = [os.path.join(imgdir, f) for f in sorted(os.listdir(imgdir)) if f.endswith('JPG') or f.endswith('jpg') or f.endswith('png')]
    # 判断图片pose和新图片的数目是否一致
    if poses.shape[-1] != len(imgfiles):
        print('Mismatch between imgs {} and poses {} !!!!'.format(len(imgfiles), poses.shape[-1]) )
        return

    # 读取新图像的shape[h/factor,w/factor,c]
    sh = imageio.imread(imgfiles[0]).shape
    # 图像尺寸缩小了,相对于的w和h也要改变(替换)
    poses[:2, 4, :] = np.array(sh[:2]).reshape([2, 1])
    # 更新f值 f=f_ori/factor
    poses[2, 4, :] = poses[2, 4, :] * 1./factor

    # 只加载位姿和边界
    if not load_imgs:
        return poses, bds
    
    # 加载图片
    def imread(f):
        if f.endswith('png'):
            return imageio.imread(f, format="PNG-PIL", ignoregamma=True)
        else:
            return imageio.imread(f)
    # 读取所有新图像并归一化
    imgs = [imread(f)[..., :3]/255. for f in imgfiles]
    # 拼接所有图像[h/factor,w/factor,c,images_num]
    imgs = np.stack(imgs, -1)  
    print('Loaded image data', imgs.shape, poses[:,-1,0])
    return poses, bds, imgs

Representación de poses pose matriz 3×5:
[ R thwf ] \left[ {\begin{array}{cc} R&t&{\begin{array}{cc} h\\ w\\ f \end{array}} \ end {matriz}} \derecha] rthwf
R es la matriz de rotación 3 × 3, t es la matriz de desplazamiento 3 × 1 y el 3 × 1 restante es el tamaño de la imagen y la distancia focal.


_minificar

Cambie el tamaño de la imagen original de acuerdo con la reducción de resolución múltiple o la resolución especificada y guárdela en una nueva carpeta en formato png.

def _minify(basedir, factors=[], resolutions=[]):
    needtoload = False
    # 按照下采样倍数
    for r in factors:
        # 判断本地是否已经存有下采样factors的图像
        imgdir = os.path.join(basedir, 'images_{}'.format(r))
        if not os.path.exists(imgdir):
            needtoload = True
    # 按照分辨率下采样
    for r in resolutions:
        # 判断本地是否已经存有对应具体分辨率的图像
        imgdir = os.path.join(basedir, 'images_{}x{}'.format(r[1], r[0]))
        if not os.path.exists(imgdir):
            needtoload = True

    # 如果有直接退出
    if not needtoload:
        return
    # 如果没有需要重新加载

    # 汇制命令语句(操作系统自带此功能)
    from subprocess import check_output
    # 获取原始图片的路径
    imgdir = os.path.join(basedir, 'images')
    # 获取所有图片地址,并排除其他非图像文件
    imgs = [os.path.join(imgdir, f) for f in sorted(os.listdir(imgdir))]
    imgs = [f for f in imgs if any([f.endswith(ex) for ex in ['JPG', 'jpg', 'png', 'jpeg', 'PNG']])]
    imgdir_orig = imgdir.replace("\\", "/")
    # 获得执行py文件当前所在目录
    wd = os.getcwd()

    for r in factors + resolutions:
        # 下采样的倍数 int类型
        if isinstance(r, int):
            # 保存新尺寸图像的文件夹
            name = 'images_{}'.format(r)
            # resize的大小
            resizearg = '{}%'.format(100./r)
        # 指定分辨率 list类型
        else:
            name = 'images_{}x{}'.format(r[1], r[0])
            resizearg = '{}x{}'.format(r[1], r[0])
        # 新尺寸图像的保存路径
        imgdir = os.path.join(basedir, name).replace("\\", "/")
        if os.path.exists(imgdir):
            continue
        print('Minifying', r, basedir)
        # 创建新尺寸图像的保存文件夹
        os.makedirs(imgdir)

        # 将原始图片拷贝到指定新尺寸图像的保存文件夹下
        check_output('cp {}/* {}'.format(imgdir_orig, imgdir), shell=True)
        # 获取图片的数据格式
        ext = imgs[0].split('.')[-1]
        # 绘制执行命令语句
        args = ' '.join(['magick ', 'mogrify', '-resize', resizearg, '-format', 'png', '*.{}'.format(ext)])
        # 切换到新尺寸图像的保存路径
        os.chdir(imgdir)
        # 对新尺寸图像的保存路径中的原始图片进行resize,并用png格式保存
        check_output(args, shell=True)
        # 切回当前执行py文件所在目录
        os.chdir(wd)
        # 因为新尺寸图像的保存路径下除了png格式的新尺寸图像,还有原始尺寸图像需要删除,要是原始图像也是png格式则直接覆盖
        if ext != 'png':
            check_output('rm {}/*.{}'.format(imgdir, ext), shell=True)
            print('Removed duplicates')
        print('Done')

poses_recentradas

Pose de cámara centralizada, incluyendo posición y orientación.

  • La función poses_avg calcula la pose promedio de todas las cámaras.
  • La pose promedio de la cámara se multiplica por la inversa izquierda de todas las poses de la cámara para realizar una transformación de rotación y traslación para completar la normalización.
def recenter_poses(poses):
    poses_ = poses+0        # [N,3,5]
    # 作用是让平均相机位姿[3,4]变为[4,4]
    bottom = np.reshape([0,0,0,1.], [1, 4])     # [1,4]
    c2w = poses_avg(poses)  # [3,4]
    c2w = np.concatenate([c2w[:3, :4], bottom], -2)          # [4,4]

    # 作用是让所有相机位姿[3,4]变为[4,4]
    # bottom[1,1,4]--->对bottom对应维度进行复制扩展[images_num,1,1]==>[1*N,1*1,4*1]
    bottom = np.tile(np.reshape(bottom, [1, 1, 4]), [poses.shape[0], 1, 1])     # [N,1,4]
    # poses shape [images_num,4,4]
    poses = np.concatenate([poses[:, :3, :4], bottom], -2)      # [N,4,4]

    # 求c2w的逆矩阵,并与poses进行矩阵运算,目的是完成所有相机位姿的归一化
    poses = np.linalg.inv(c2w) @ poses      # [4,4]

    # 用新的旋转矩阵R替换原始的旋转矩阵R
    poses_[:, :3, :4] = poses[:, :3, :4]    # [4,4]
    # 对pose进行中心化处理
    poses = poses_
    return poses

A continuación se muestra un diagrama esquemático del código:

El sistema de coordenadas formado por las líneas continuas en la figura es la pose original, y el sistema de coordenadas formado por las líneas de puntos se ha transformado mediante rotación y traslación. El sistema de coordenadas después de la transformación Pose_mean coincide completamente con el sistema de coordenadas mundial.

La nueva pose (matriz unitaria) generada por la multiplicación inversa hacia la izquierda de la pose promedio de la cámara por la pose promedio de la cámara coincide completamente con el sistema de coordenadas mundial (el origen y los ejes de coordenadas coinciden), luego la misma rotación y traslación. Se utiliza una matriz de transformación (la inversa de la pose promedio de la cámara) se multiplica por la izquierda de todas las poses de la cámara para realizar una transformación global de rotación y traducción en todas las poses de la cámara para completar la normalización, de modo que la pose promedio de todas las cámaras después de la transformación está en el sistema de coordenadas mundial. El origen, el centro de masa de todos los centros ópticos (el centro óptico de la pose promedio) también se transforma desde un punto sin origen en el sistema de coordenadas mundial al origen, como se muestra en la la siguiente figura.

La matriz de traducción t puede entenderse como el centro óptico, porque t se traduce desde el origen. El punto negro es el centro óptico de la cámara y el punto naranja es el centro óptico de la pose promedio.


poses_avg

Calcule la media de las poses de la cámara correspondientes a todas las imágenes, incluida la posición y la orientación.

  • Posición: promedia los centros de todas las cámaras para obtener el centro.
  • Orientación: la suma del eje Z de todas las cámaras se normaliza y aplana para obtener vec2 (vector de dirección unitario promedio); la suma del eje Y de todas las cámaras es hacia arriba (vector de dirección principal). Los parámetros externos medios de todas las cámaras se obtienen a través de la función viewmatrix .
def poses_avg(poses):
    # 获取图像的尺寸和焦距
    hwf = poses[0, :3, -1:]             # [3,1]
    # 计算全部平移矩阵t的均值
    center = poses[:, :3, 3].mean(0)    # [N,3]
    # 全部旋转矩阵R的第三列求和并归一化(方向向量相加再归一化等效于平均单位方向向量)
    vec2 = normalize(poses[:, :3, 2].sum(0))    # [3]
    # 全部旋转矩阵R的第二列求和(方向向量相加理解成所有方向向量的主方向向量)
    up = poses[:, :3, 1].sum(0)         # [3]
    #
    c2w = np.concatenate([viewmatrix(vec2, up, center), hwf], 1)    # [3,5]
    return c2w

A continuación se muestra un diagrama esquemático del código:


vermatriz

Construya la función de la matriz de la cámara, calcule el vector de dirección unitario del eje x a través del vector de dirección unitario promedio del eje z y el vector de dirección principal del eje y, y luego calcule el eje y usando el el vector de dirección unitario promedio del eje z y el vector de dirección unitario del eje x. El vector de dirección unitario de y, finalmente, el vector de dirección tridimensional de los tres ejes xyz más el centro de cámara promedio construyen la pose promedio de todas las cámaras.

def viewmatrix(z, up, pos):
    # z轴平均单位方向向量
    vec2 = normalize(z)         # [3,1]
    # y轴主方向向量
    vec1_avg = up               # [3,1]
    # 计算出x的单位方向向量
    vec0 = normalize(np.cross(vec1_avg, vec2))              # [3,1]
    # 计算出y州的单位方向向量
    vec1 = normalize(np.cross(vec2, vec0))                  # [3,1]
    m = np.stack([vec0, vec1, vec2, pos], 1)    # [3,4]
    return m

A continuación se muestra un diagrama esquemático del código:

La línea de puntos representa el vector de dirección unitario calculado del nuevo eje. ¿Por qué es necesario recalcular el eje y aquí? Algunas personas piensan que esto se debe a que la y original no es perpendicular a z y hay un error de cálculo.


Resumir

Introduzca parte del código en el proceso de carga de datos de la forma más sencilla y detallada posible: _load_data carga datos y center_poses normaliza todas las poses de la cámara. El código de otros módulos funcionales se explicará más adelante.

Supongo que te gusta

Origin blog.csdn.net/yangyu0515/article/details/132445098
Recomendado
Clasificación