App下載

如何使用Python Multiprocessing庫(kù)處理3D數(shù)據(jù)?使用Python Multiprocessing庫(kù)處理3D數(shù)據(jù)方法!

小丸子的西瓜夢(mèng) 2021-09-30 10:03:54 瀏覽數(shù) (3076)
反饋

今天我們將介紹處理大量數(shù)據(jù)時(shí)非常方便的工具。我不會(huì)只告訴您可能在手冊(cè)中找到的一般信息,而是分享一些我發(fā)現(xiàn)的小技巧,例如tqdm與 multiprocessingimap一起使用、并行處理檔案、繪制和處理 3D 數(shù)據(jù)以及如何搜索如果您有點(diǎn)云,則用于對(duì)象網(wǎng)格中的類似對(duì)象。

那么我們?yōu)槭裁匆笾诓⑿杏?jì)算呢?如今,如果您處理任何類型的數(shù)據(jù),您可能會(huì)面臨與“大數(shù)據(jù)”相關(guān)的問(wèn)題。每次我們有不適合 RAM 的數(shù)據(jù)時(shí),我們都需要一塊一塊地處理它。幸運(yùn)的是,現(xiàn)代編程語(yǔ)言允許我們生成在多核處理器上完美運(yùn)行的多個(gè)進(jìn)程(甚至線程)。(注意:這并不意味著單核處理器不能處理多處理。  這是關(guān)于該主題的堆棧溢出線程。)

今天,我們將嘗試處理經(jīng)常發(fā)生的計(jì)算網(wǎng)格和點(diǎn)云之間距離的 3D 計(jì)算機(jī)視覺(jué)任務(wù)。例如,當(dāng)您需要在所有可用網(wǎng)格中找到定義與給定點(diǎn)云相同的 3D 對(duì)象的網(wǎng)格時(shí),您可能會(huì)遇到此問(wèn)題。

我們的數(shù)據(jù)由.obj存儲(chǔ)在.7z存檔中的文件組成,這在存儲(chǔ)效率方面非常出色。但是當(dāng)我們需要訪問(wèn)它的確切部分時(shí),我們應(yīng)該努力。在這里,我定義了包裝 7-zip 存檔并提供底層數(shù)據(jù)接口的類。

from io import BytesIO
import py7zlib

class MeshesArchive(object):
    def __init__(self, archive_path):
        fp = open(archive_path, 'rb')
        self.archive = py7zlib.Archive7z(fp)
        self.archive_path = archive_path
        self.names_list = self.archive.getnames()
        self.cur_id = 0
        
    def __len__(self):
        return len(self.names_list)
    
    def get(self, name):
        bytes_io = BytesIO(self.archive.getmember(name).read())
        return bytes_io

    def __getitem__(self, idx):
        return self.get(self.names[idx])
      
    def __iter__(self):
        return self

    def __next__(self):
      if self.cur_id >= len(self.names_list):
          raise StopIteration
      name = self.names_list[self.cur_id]
      self.cur_id += 1
      return self.get(name)

這個(gè)類幾乎不依賴py7zlib包,它允許我們?cè)诿看握{(diào)用get方法時(shí)解壓縮數(shù)據(jù),并為我們提供存檔中的文件數(shù)。我們還定義了__iter__這將幫助我們map像在可迭代對(duì)象上一樣在該對(duì)象上啟動(dòng)多處理。 

您可能知道,可以創(chuàng)建一個(gè) Python 類,從中可以實(shí)例化可迭代對(duì)象。該類應(yīng)滿足以下條件:覆蓋__getitem__返回self和__next__返回后續(xù)元素。我們絕對(duì)遵循這個(gè)規(guī)則。 

上面的定義為我們提供了遍歷存檔的可能性,但 它是否允許我們 對(duì)內(nèi)容進(jìn)行并行隨機(jī)訪問(wèn)?這是一個(gè)有趣的問(wèn)題,我在網(wǎng)上沒(méi)有找到答案,但我們可以研究源代碼py7zlib并嘗試自己回答。

在這里,我提供了來(lái)自pylzma的代碼片段:

class Archive7z(Base):
  def __init__(self, file, password=None):
    # ...
    self.files = {}
    # ...
    for info in files.files:
      # create an instance of ArchiveFile that knows location on disk
      file = ArchiveFile(info, pos, src_pos, folder, self, maxsize=maxsize)
      # ...
      self.files.append(file)
    # ...
    self.files_map.update([(x.filename, x) for x in self.files])
        
  # method that returns an ArchiveFile from files_map dictionary
  def getmember(self, name):
      if isinstance(name, (int, long)):
          try:
              return self.files[name]
          except IndexError:
              return None

      return self.files_map.get(name, None)
    
    
class Archive7z(Base):
  def read(self):
    # ...
    for level, coder in enumerate(self._folder.coders):
      # ...
      # get the decoder and decode the underlying data
      data = getattr(self, decoder)(coder, data, level, num_coders)

    return data

在代碼中,您可以看到在從存檔中讀取下一個(gè)對(duì)象期間調(diào)用的方法。我相信從上面可以清楚地看出,只要同時(shí)多次讀取存檔,就沒(méi)有理由阻止存檔。

接下來(lái),我們快速介紹一下什么是網(wǎng)格和點(diǎn)云。 

首先,網(wǎng)格是頂點(diǎn)、邊和面的集合。頂點(diǎn)由空間中的(x,y,z) 坐標(biāo)定義并分配有唯一編號(hào)。邊和面是相應(yīng)的點(diǎn)對(duì)和三元組的組,并用提到的唯一點(diǎn) id 定義。通常,當(dāng)我們談?wù)摗熬W(wǎng)格”時(shí),我們指的是“三角形網(wǎng)格”,即由三角形組成的表面。使用trimesh庫(kù)在 Python 中使用網(wǎng)格要容易得多。例如,它提供了.obj在內(nèi)存中加載文件的接口。要在jupyter notebook一個(gè)3D 對(duì)象中顯示和交互,可以使用k3d庫(kù)。

所以,用下面的代碼片段我回答這個(gè)問(wèn)題:“你怎么繪制trimesh的對(duì)象jupyter有k3d?”

import trimesh
import k3d

with open("./data/meshes/stanford-bunny.obj") as f:
    bunny_mesh = trimesh.load(f, 'obj')

plot = k3d.plot()
mesh = k3d.mesh(bunny_mesh.vertices, bunny_mesh.faces)
plot += mesh
plot.display()

Stanford Bunny mesh displayed by k3d

k3d 顯示的斯坦福兔子網(wǎng)格 
其次,點(diǎn)云是表示空間中對(duì)象的 3D 點(diǎn)數(shù)組。許多 3D 掃描儀生成點(diǎn)云作為掃描對(duì)象的表示。出于演示目的,我們可以讀取相同的網(wǎng)格并將其頂點(diǎn)顯示為點(diǎn)云。
import trimesh
import k3d

with open("./data/meshes/stanford-bunny.obj") as f:
    bunny_mesh = trimesh.load(f, 'obj')
    
plot = k3d.plot()
cloud = k3d.points(bunny_mesh.vertices, point_size=0.0001, shader="flat")
plot += cloud
plot.display()

Point cloud drawn by k3d

k3d繪制的
點(diǎn)云
如上所述,3D 掃描儀為我們提供了一個(gè)點(diǎn)云。假設(shè)我們有一個(gè)網(wǎng)格數(shù)據(jù)庫(kù),我們想在我們的數(shù)據(jù)庫(kù)中找到一個(gè)與掃描對(duì)象對(duì)齊的網(wǎng)格,也就是點(diǎn)云。
為了解決這個(gè)問(wèn)題,我們可以提出一種簡(jiǎn)單的方法。我們將從我們的檔案中搜索給定點(diǎn)云的點(diǎn)與每個(gè)網(wǎng)格之間的最大距離。
如果1e-4某些網(wǎng)格的距離更小,我們將認(rèn)為該網(wǎng)格與點(diǎn)云對(duì)齊。

最后,我們來(lái)到了多處理部分。請(qǐng)記住,我們的存檔中有大量文件可能無(wú)法放在一起放在內(nèi)存中,因?yàn)槲覀兏矚g并行處理它們。

為了實(shí)現(xiàn)這一點(diǎn),我們將使用 multiprocessing Pool,它使用mapimap/imap_unordered方法處理用戶定義函數(shù)的多次調(diào)用。

mapimap影響我們的區(qū)別在于,map在將其發(fā)送到工作進(jìn)程之前將其轉(zhuǎn)換為列表。如果存檔太大而無(wú)法寫(xiě)入 RAM,則不應(yīng)將其解壓縮到 Python 列表中。換句話說(shuō),兩者的執(zhí)行速度是相似的。

[加載網(wǎng)格:pool.map w/o manager] 4 個(gè)進(jìn)程池耗時(shí):37.213207403818764 秒
[加載網(wǎng)格:pool.imap_unordered w/o manager] 4 個(gè)進(jìn)程池耗時(shí):37.219303369522095 秒

在上面,您可以看到從適合內(nèi)存的網(wǎng)格檔案中簡(jiǎn)單讀取的結(jié)果。

更進(jìn)一步imap:讓我們討論如何實(shí)現(xiàn)找到靠近點(diǎn)云的網(wǎng)格的目標(biāo)。這是數(shù)據(jù)。我們有來(lái)自斯坦福模型的 5 個(gè)不同的網(wǎng)格。我們將通過(guò)向斯坦福兔子網(wǎng)格的頂點(diǎn)添加噪聲來(lái)模擬 3D 掃描。

import numpy as np
from numpy.random import default_rng

def normalize_pc(points):
    points = points - points.mean(axis=0)[None, :]
    dists = np.linalg.norm(points, axis=1)
    scaled_points = points / dists.max()
    return scaled_points


def load_bunny_pc(bunny_path):
    STD = 1e-3 
    with open(bunny_path) as f:
        bunny_mesh = load_mesh(f)
    # normalize point cloud 
    scaled_bunny = normalize_pc(bunny_mesh.vertices)
    # add some noise to point cloud
    rng = default_rng()
    noise = rng.normal(0.0, STD, scaled_bunny.shape)
    distorted_bunny = scaled_bunny + noise
    return distorted_bunny

當(dāng)然,我們之前在下面將點(diǎn)云和網(wǎng)格頂點(diǎn)歸一化,以在 3D 立方體中縮放它們。

要計(jì)算點(diǎn)云和網(wǎng)格之間的距離,我們將使用igl。為了完成,我們需要編寫(xiě)一個(gè)函數(shù)來(lái)調(diào)用每個(gè)進(jìn)程及其依賴項(xiàng)。讓我們用以下代碼段來(lái)總結(jié)。

import itertools
import time
import numpy as np
from numpy.random import default_rng
import trimesh
import igl
from tqdm import tqdm
from multiprocessing import Pool
def load_mesh(obj_file):
    mesh = trimesh.load(obj_file, 'obj')
    return mesh
def get_max_dist(base_mesh, point_cloud):
    distance_sq, mesh_face_indexes, _ = igl.point_mesh_squared_distance(
        point_cloud,
        base_mesh.vertices,
        base_mesh.faces
    )
    return distance_sq.max()
def load_mesh_get_distance(args):
    obj_file, point_cloud = args[0], args[1]
    mesh = load_mesh(obj_file)
    mesh.vertices = normalize_pc(mesh.vertices)
    max_dist = get_max_dist(mesh, point_cloud)
    return max_dist
def read_meshes_get_distances_pool_imap(archive_path, point_cloud, num_proc, num_iterations):
    # do the meshes processing within a pool
    elapsed_time = []
    for _ in range(num_iterations):
        archive = MeshesArchive(archive_path)
        pool = Pool(num_proc)
        start = time.time()
        result = list(tqdm(pool.imap(
            load_mesh_get_distance,
            zip(archive, itertools.repeat(point_cloud)),
        ), total=len(archive)))
        pool.close()
        pool.join()
        end = time.time()
        elapsed_time.append(end - start)
    print(f'[Process meshes: pool.imap] Pool of {num_proc} processes elapsed time: {np.array(elapsed_time).mean()} sec')   
    for name, dist in zip(archive.names_list, result):
        print(f"{name} {dist}")    
    return result  
 if __name__ == "__main__":
    bunny_path = "./data/meshes/stanford-bunny.obj"
    archive_path = "./data/meshes.7z"
    num_proc = 4
    num_iterations = 3
    point_cloud = load_bunny_pc(bunny_path)
    read_meshes_get_distances_pool_no_manager_imap(archive_path, point_cloud, num_proc, num_iterations)

這read_meshes_get_distances_pool_imap是一個(gè)中心函數(shù),其中完成以下操作:

  • MeshesArchive并multiprocessing.Pool初始化
  • tqdm 用于觀察池進(jìn)度,并手動(dòng)完成整個(gè)池的分析
  • 執(zhí)行結(jié)果的輸出

請(qǐng)注意我們?nèi)绾蝹鬟f參數(shù)以imap從archive和point_cloud使用zip(archive, itertools.repeat(point_cloud)). 這允許我們將點(diǎn)云數(shù)組粘貼到存檔的每個(gè)條目上,避免轉(zhuǎn)換archive為列表。

執(zhí)行結(jié)果如下:

100%|########################################### #####################| 5/5 [00:00<00:00, 5.14it/s] 
100%|########################### ####################################| 5/5 [00:00<00:00, 5.08it/s] 
100%|########################### ####################################| 5/5 [00:00 <0時(shí),5.18it /秒] 
[方法網(wǎng)眼:pool.imap W / O管理器] 4個(gè)過(guò)程的池經(jīng)過(guò)時(shí)間:1.0080536206563313秒
armadillo.obj 0.16176825266293382 
beast.obj 0.28608649819198073 
cow.obj 0.41653845909820164
現(xiàn)貨.obj 0.22739556571296735 
stanford-bunny.obj 2.3699851136074263e-05

我們可以注意到斯坦福兔子是最接近給定點(diǎn)云的網(wǎng)格。還可以看出,我們沒(méi)有使用大量數(shù)據(jù),但我們已經(jīng)證明,即使我們?cè)诖鏅n中有大量網(wǎng)格,該解決方案也能奏效。

多處理使數(shù)據(jù)科學(xué)家不僅在 3D 計(jì)算機(jī)視覺(jué)中而且在機(jī)器學(xué)習(xí)的其他領(lǐng)域中都取得了出色的表現(xiàn)。理解并行執(zhí)行比在循環(huán)內(nèi)執(zhí)行快得多是非常重要的。差異變得顯著,尤其是在正確編寫(xiě)算法時(shí)。大量數(shù)據(jù)揭示了如果沒(méi)有關(guān)于如何使用有限資源的創(chuàng)造性方法就無(wú)法解決的問(wèn)題。幸運(yùn)的是,Python 語(yǔ)言及其廣泛的庫(kù)集幫助我們數(shù)據(jù)科學(xué)家解決了這些問(wèn)題。


0 人點(diǎn)贊