點擊參加51CTO網(wǎng)站內(nèi)容調(diào)查問卷
譯者 | 朱先忠
審校 | 重樓
簡介
在本文中,我提供了一個關(guān)于如何使用Python的Open3D庫(一個用于3D數(shù)據(jù)處理的開源庫)來探索、處理和可視化3D模型的快速演練。
使用Open3D可視化的3D模型(鏈接https://sketchfab.com/3d-models/tesla-model-s-plaid-9de8855fae324e6cbbb83c9b5288c961處可找到原始3D模型)
如果您正在考慮處理特定任務(wù)的3D數(shù)據(jù)/模型,例如訓(xùn)練3D模型分類和/或分割A(yù)I模型,那么您會發(fā)現(xiàn)本演練是很有幫助的?;ヂ?lián)網(wǎng)上的3D模型(在ShapeNet等數(shù)據(jù)集中)有多種格式,如.obj、.glb、.gltf等。使用Open3D等庫,可以輕松處理、可視化這些模型,并將其轉(zhuǎn)換為其他格式,如點云,因為這些格式更容易理解和解釋。
注意,這篇文章也可以作為Jupyter筆記本提供給那些希望跟隨并在本地運行代碼的人。包含Jupyter筆記本以及所有其他數(shù)據(jù)和有關(guān)資源的zip文件可以從下面的鏈接下載。
3D Data Processing with Open3D.zip。
在本教程中,我將完成以下任務(wù):
- 將三維模型加載為網(wǎng)格并將其可視化
- 通過采樣點將網(wǎng)格轉(zhuǎn)換為點云
- 從點云中刪除隱藏點
- 將點云轉(zhuǎn)換為數(shù)據(jù)幀
- 保存點云和數(shù)據(jù)幀
下面,讓我們從導(dǎo)入所有必要的庫開始:
# 導(dǎo)入open3d和所有其他必要的庫。
import open3d as o3d
import os
import copy
import numpy as np
import pandas as pd
from PIL import Image
np.random.seed(42)
#正在檢查open3d上安裝的版本。
o3d.__version__
# Open3D version used in this exercise: 0.16.0
將三維模型加載為網(wǎng)格并將其可視化
通過運行以下代碼行,可以將3D模型讀取為網(wǎng)格:
#定義三維模型文件的路徑。
mesh_path = "data/3d_model.obj"
# 使用open3d將三維模型文件讀取為三維網(wǎng)格。
mesh = o3d.io.read_triangle_mesh(mesh_path)
要可視化網(wǎng)格,請運行以下代碼行:
#可視化網(wǎng)格。
draw_geoms_list = [mesh]
o3d.visualization.draw_geometries(draw_geoms_list)
網(wǎng)格應(yīng)該在一個新窗口中打開,看起來應(yīng)該像下面的圖像(請注意,網(wǎng)格打開時是靜態(tài)圖像,而不是像這里顯示的動畫圖像)。可以使用鼠標(biāo)指針根據(jù)需要旋轉(zhuǎn)網(wǎng)格圖像。
可視化為網(wǎng)格的3D模型(在估計曲面法線之前)
如上所述,汽車網(wǎng)格看起來不像典型的3D模型,而是渲染成了統(tǒng)一的灰色。這是因為網(wǎng)格沒有任何關(guān)于三維模型中頂點和曲面的法線的信息。
什么是法線呢?-曲面在給定點處的法向量是垂直于該點處曲面的向量。法向量通常簡稱為“法線”。要閱讀更多關(guān)于此主題的內(nèi)容,可以參考以下兩個鏈接:法線向量和估計點云中的曲面法線。
可以通過運行以下代碼行來估計上面三維網(wǎng)格的法線:
# 計算網(wǎng)格的法線。
mesh.compute_vertex_normals()
#可視化網(wǎng)格。
draw_geoms_list = [mesh]
o3d.visualization.draw_geometries(draw_geoms_list)
一旦可視化,網(wǎng)格應(yīng)該如下圖所示出現(xiàn)。計算法線后,汽車將正確渲染,看起來像一個3D模型。
可視化為網(wǎng)格的3D模型(在估計表面法線之后)
現(xiàn)在,讓我們創(chuàng)建一個XYZ坐標(biāo)系,以了解這個汽車模型在歐幾里得空間中的方向。XYZ坐標(biāo)系可以覆蓋在上面的3D網(wǎng)格上,并通過運行以下代碼行進行可視化:
# 創(chuàng)建XYZ軸笛卡爾坐標(biāo)系的網(wǎng)格。
# 該網(wǎng)格將顯示X、Y和Z軸指向的方向,并且可以覆蓋在3D網(wǎng)格上,以可視化其在歐幾里得空間中的方向。
# X-axis : 紅色箭頭
# Y-axis : 綠色箭頭
# Z-axis : 藍色箭頭
mesh_coord_frame = o3d.geometry.TriangleMesh.create_coordinate_frame(size=5, origin=[0, 0, 0])
#使用坐標(biāo)系可視化網(wǎng)格,以了解方向。
draw_geoms_list = [mesh_coord_frame, mesh]
o3d.visualization.draw_geometries(draw_geoms_list)
使用XYZ坐標(biāo)系可視化的三維網(wǎng)格(X軸:紅色箭頭,Y軸:綠色箭頭,Z軸:藍色箭;[簡記為——XYZ::RGB])
從上面的可視化中,我們可以看到這個汽車網(wǎng)格的方向如下:
- XYZ軸的原點:在汽車模型的體積中心(在上圖中看不到,因為它在汽車網(wǎng)格內(nèi))。
- X軸(紅色箭頭):沿著汽車的長度尺寸,正X軸指向汽車的發(fā)動機罩(在上圖中看不到,因為它在汽車網(wǎng)格內(nèi))。
- Y軸(綠色箭頭):沿著汽車的高度尺寸,正Y軸指向汽車的車頂。
- Z軸(藍色箭頭):沿著汽車的寬度尺寸,正Z軸指向汽車的右側(cè)。
現(xiàn)在,讓我們來看看這個汽車模型里面有什么。為此,我們將在Z軸上裁剪網(wǎng)格,并移除汽車的右半部分(正Z軸)。
#使用其束框裁剪汽車網(wǎng)格以移除其右半部分(正Z軸)。
bbox = mesh.get_axis_aligned_bounding_box()
bbox_points = np.asarray(bbox.get_box_points())
bbox_points[:, 2] = np.clip(bbox_points[:, 2], a_min=None, a_max=0)
bbox_cropped = o3d.geometry.AxisAlignedBoundingBox.create_from_points(o3d.utility.Vector3dVector(bbox_points))
mesh_cropped = mesh.crop(bbox_cropped)
# 可視化裁剪的網(wǎng)格。
draw_geoms_list = [mesh_coord_frame, mesh_cropped]
o3d.visualization.draw_geometries(draw_geoms_list)
在移除汽車右半部分的情況下,在Z軸上裁剪三維網(wǎng)格(正Z軸)。裁剪后的網(wǎng)格顯示了此3D汽車模型中的詳細內(nèi)部
從上面的可視化中,我們可以看到這款車型有著詳細的內(nèi)飾。現(xiàn)在我們已經(jīng)看到了這個3D網(wǎng)格內(nèi)部的內(nèi)容,我們可以在移除屬于汽車內(nèi)部的“隱藏”點之前將其轉(zhuǎn)換為點云。
通過采樣點將網(wǎng)格轉(zhuǎn)換為點云
通過定義,我們希望從網(wǎng)格中采樣的點的數(shù)量,可以在Open3D中輕松地將網(wǎng)格轉(zhuǎn)換為點云。
#從網(wǎng)格中均勻采樣100000個點,將其轉(zhuǎn)換為點云。
n_pts = 100_000
pcd = mesh.sample_points_uniformly(n_pts)
#可視化點云。
draw_geoms_list = [mesh_coord_frame, pcd]
o3d.visualization.draw_geometries(draw_geoms_list)
通過從三維網(wǎng)格中均勻采樣100000個點創(chuàng)建的三維點云
請注意,上面點云中的顏色僅指示點沿Z軸的位置。
如果我們像裁剪上面的網(wǎng)格一樣裁剪點云,它會是這樣的:
#使用邊界框裁剪汽車點云以移除其右半部分(正Z軸)。
pcd_cropped = pcd.crop(bbox_cropped)
#可視化裁剪的點云。
draw_geoms_list = [mesh_coord_frame, pcd_cropped]
o3d.visualization.draw_geometries(draw_geoms_list)
在移除汽車右半部分的情況下在Z軸上裁剪的三維點云(正Z軸)。與上面裁剪的網(wǎng)格一樣,裁剪的點云也顯示了此3D汽車模型中的詳細內(nèi)部
我們在裁剪點云的可視化中看到,它還包含屬于汽車模型內(nèi)部的點。這是意料之中的,因為該點云是通過對整個網(wǎng)格中的點進行均勻采樣而創(chuàng)建的。在下一節(jié)中,我們將刪除這些屬于汽車內(nèi)部且不在點云外表面的“隱藏”點。
從點云中刪除隱藏點
想象一下,你把一盞燈指向汽車模型的右側(cè)。落在三維模型右外表面上的所有點都將被照亮,而點云中的所有其他點則不會被照亮。
顯示Open3D的隱藏點移除如何從給定的視點處理點云的插圖。所有被照亮的點都被視為“可見”,而所有其他點都被認(rèn)為是“隱藏”
現(xiàn)在,讓我們將這些照明點標(biāo)記為“可見”,將所有未照明點標(biāo)記“隱藏”。這些“隱藏”點還將包括屬于汽車內(nèi)部的所有點。
此操作在Open3D中稱為“隱藏點刪除”。為了使用Open3D在點云上執(zhí)行此操作,請運行以下代碼行:
# 定義隱藏點刪除操作的攝影機和半徑參數(shù)。
diameter = np.linalg.norm(np.asarray(pcd.get_min_bound()) - np.asarray(pcd.get_max_bound()))
camera = [0, 0, diameter]
radius = diameter * 100
# 使用上面定義的攝影機和半徑參數(shù)對點云執(zhí)行隱藏點刪除操作。
#輸出是可見點的索引列表。
_, pt_map = pcd.hidden_point_removal(camera, radius)
使用上面的可見點索引輸出列表,我們可以在可視化點云之前將可見點和隱藏點涂成不同的顏色。
# 將點云中的所有可見點繪制為藍色,將所有隱藏點繪制為紅色。
pcd_visible = pcd.select_by_index(pt_map)
pcd_visible.paint_uniform_color([0, 0, 1]) #藍色點是可見點(需要保留)。
print("No. of visible points : ", pcd_visible)
pcd_hidden = pcd.select_by_index(pt_map, invert=True)
pcd_hidden.paint_uniform_color([1, 0, 0]) # 紅色點是隱藏點(要刪除)。
print("No. of hidden points : ", pcd_hidden)
# 可視化點云中的可見(藍色)和隱藏(紅色)點。
draw_geoms_list = [mesh_coord_frame, pcd_visible, pcd_hidden]
o3d.visualization.draw_geometries(draw_geoms_list)
從上圖所示的攝影機視點移除隱藏點操作后的點云。“可見”點為藍色,而“隱藏”點為紅色
從上面的可視化中,我們可以看到隱藏點移除操作是如何從給定的相機視點工作的。該操作消除了背景中被來自給定相機視點的前景中的點遮擋的所有點。
為了更好地理解這一點,我們可以再次重復(fù)相同的操作,但這次是在稍微旋轉(zhuǎn)點云之后。實際上,我們正在努力改變這里的觀點。但是,我們將旋轉(zhuǎn)點云本身,而不是通過重新定義相機參數(shù)來改變它。
#定義將度數(shù)轉(zhuǎn)換為弧度的函數(shù)。
def deg2rad(deg):
return deg * np.pi/180
#將點云繞X軸旋轉(zhuǎn)90度。
x_theta = deg2rad(90)
y_theta = deg2rad(0)
z_theta = deg2rad(0)
tmp_pcd_r = copy.deepcopy(pcd)
R = tmp_pcd_r.get_rotation_matrix_from_axis_angle([x_theta, y_theta, z_theta])
tmp_pcd_r.rotate(R, center=(0, 0, 0))
#可視化旋轉(zhuǎn)的點云。
draw_geoms_list = [mesh_coord_frame, tmp_pcd_r]
o3d.visualization.draw_geometries(draw_geoms_list)
圍繞X軸旋轉(zhuǎn)90度的三維點云。請注意,與以前不同的是,現(xiàn)在Y軸(綠色箭頭)沿著汽車的寬度尺寸運行,Z軸(藍色箭頭)沿著車輛的高度尺寸運行。X軸(紅色箭頭)沒有變化,它仍然沿著汽車的長度方向運行
圖示顯示了隱藏點刪除操作如何從與前面相同的給定視點對旋轉(zhuǎn)的點云進行操作。如前所述,所有照明點都被視為“可見”,而所有其他點都被認(rèn)為是“隱藏”。
通過對旋轉(zhuǎn)的汽車模型再次重復(fù)相同的過程,我們可以看到,這一次,落在3D模型(車頂)上外表面的所有點都會被照亮,而點云中的所有其他點都不會被照亮。
我們可以通過運行以下代碼行,對旋轉(zhuǎn)的點云重復(fù)隱藏點刪除操作:
# 使用上面定義的相同攝影機和半徑參數(shù)對旋轉(zhuǎn)的點云執(zhí)行隱藏點移除操作。
#輸出是可見點的索引列表。
_, pt_map = tmp_pcd_r.hidden_point_removal(camera, radius)
# 將旋轉(zhuǎn)的點云中的所有可見點繪制為藍色,將所有隱藏點繪制為紅色。
pcd_visible = tmp_pcd_r.select_by_index(pt_map)
pcd_visible.paint_uniform_color([0, 0, 1]) # 藍色點是可見點(需要保留)。
print("No. of visible points : ", pcd_visible)
pcd_hidden = tmp_pcd_r.select_by_index(pt_map, invert=True)
pcd_hidden.paint_uniform_color([1, 0, 0]) #紅色點是隱藏點(要刪除)。
print("No. of hidden points : ", pcd_hidden)
#可視化旋轉(zhuǎn)的點云中的可見(藍色)和隱藏(紅色)點。
draw_geoms_list = [mesh_coord_frame, pcd_visible, pcd_hidden]
o3d.visualization.draw_geometries(draw_geoms_list)
從上圖所示的攝影機視點移除隱藏點操作后旋轉(zhuǎn)的點云。同樣,“可見”點為藍色,而“隱藏”點為紅色
上面旋轉(zhuǎn)的點云的可視化清楚地說明了隱藏點移除操作是如何工作的。因此,現(xiàn)在,為了從這個汽車點云中移除所有“隱藏”點,我們可以通過將點云圍繞所有三個軸從-90度到+90度稍微旋轉(zhuǎn)來依次執(zhí)行隱藏點移除操作。在每次刪除隱藏點操作之后,我們可以聚合點的索引的輸出列表。在所有隱藏點移除操作之后,點索引的聚合列表將包含所有未隱藏的點(即,位于點云的外表面上的點)。以下代碼執(zhí)行此順序隱藏點刪除操作:
# 定義一個函數(shù)以在X、Y和Z軸上旋轉(zhuǎn)點云。
def get_rotated_pcd(pcd, x_theta, y_theta, z_theta):
pcd_rotated = copy.deepcopy(pcd)
R = pcd_rotated.get_rotation_matrix_from_axis_angle([x_theta, y_theta, z_theta])
pcd_rotated.rotate(R, center=(0, 0, 0))
return pcd_rotated
# 定義一個函數(shù)以獲取隱藏點移除操作的點云的相機和半徑參數(shù)。
def get_hpr_camera_radius(pcd):
diameter = np.linalg.norm(np.asarray(pcd.get_min_bound()) - np.asarray(pcd.get_max_bound()))
camera = [0, 0, diameter]
radius = diameter * 100
return camera, radius
# 定義一個函數(shù),使用前面定義的攝影機和半徑參數(shù)對點云執(zhí)行隱藏點刪除操作。
#輸出是未隱藏的點的索引列表。
def get_hpr_pt_map(pcd, camera, radius):
_, pt_map = pcd.hidden_point_removal(camera, radius)
return pt_map
# 通過在三個軸中的每一個軸上將點云從-90度略微旋轉(zhuǎn)到+90度,依次執(zhí)行隱藏點移除操作,并在每次操作后聚合未隱藏的點的索引列表。
# 定義一個列表來存儲每個隱藏點刪除操作的聚合輸出列表。
pt_map_aggregated = []
# 定義旋轉(zhuǎn)點云的步長和角度值范圍。
theta_range = np.linspace(-90, 90, 7)
# 對順序操作的次數(shù)進行計數(shù)。
view_counter = 1
total_views = theta_range.shape[0] ** 3
# 獲取隱藏點移除操作的相機和半徑參數(shù)。
camera, radius = get_hpr_camera_radius(pcd)
# 循環(huán)使用上面為每個軸定義的角度值。
for x_theta_deg in theta_range:
for y_theta_deg in theta_range:
for z_theta_deg in theta_range:
print(f"Removing hidden points - processing view {view_counter} of {total_views}.")
#按給定的角度值旋轉(zhuǎn)點云。
x_theta = deg2rad(x_theta_deg)
y_theta = deg2rad(y_theta_deg)
z_theta = deg2rad(z_theta_deg)
pcd_rotated = get_rotated_pcd(pcd, x_theta, y_theta, z_theta)
# 使用上面定義的攝影機和半徑參數(shù)對旋轉(zhuǎn)的點云執(zhí)行隱藏點移除操作。
pt_map = get_hpr_pt_map(pcd_rotated, camera, radius)
# 聚合未隱藏的點的索引的輸出列表。
pt_map_aggregated += pt_map
view_counter += 1
# 通過將聚合列表轉(zhuǎn)換為集合,從聚合列表中刪除所有重復(fù)的點。
pt_map_aggregated = list(set(pt_map_aggregated))
# 將點云中的所有可見點繪制為藍色,將所有隱藏點繪制為紅色。
pcd_visible = pcd.select_by_index(pt_map_aggregated)
pcd_visible.paint_uniform_color([0, 0, 1]) # 藍色點是可見點(需要保留)。
print("No. of visible points : ", pcd_visible)
pcd_hidden = pcd.select_by_index(pt_map_aggregated, invert=True)
pcd_hidden.paint_uniform_color([1, 0, 0]) # 紅色點是隱藏點(要刪除)。
print("No. of hidden points : ", pcd_hidden)
# 可視化點云中的可見(藍色)和隱藏(紅色)點。
draw_geoms_list = [mesh_coord_frame, pcd_visible, pcd_hidden]
# draw_geoms_list = [mesh_coord_frame, pcd_visible]
# draw_geoms_list = [mesh_coord_frame, pcd_hidden]
o3d.visualization.draw_geometries(draw_geoms_list)
從同一攝影機視點執(zhí)行所有順序隱藏點移除操作后的點云。聚合的“可見”點(即點云外表面上的點)為藍色,而“隱藏”點(如不在點云外表面對的點)則為紅色
讓我們再次裁剪點云,看看屬于汽車內(nèi)部的點。
#使用先前定義的邊界框裁剪可見點的點云,以移除其右半部分(正Z軸)。
pcd_visible_cropped = pcd_visible.crop(bbox_cropped)
# 使用先前定義的邊界框裁剪隱藏點的點云,以移除其右半部分(正Z軸)。
pcd_hidden_cropped = pcd_hidden.crop(bbox_cropped)
# 可視化裁剪的點云。
draw_geoms_list = [mesh_coord_frame, pcd_visible_cropped, pcd_hidden_cropped]
o3d.visualization.draw_geometries(draw_geoms_list)
在所有順序的隱藏點移除操作之后裁剪的點云,以紅色顯示屬于3D汽車模型內(nèi)部的所有“隱藏”點
從隱藏點移除操作后裁剪的點云的上述可視化中,我們可以看到,屬于汽車模型內(nèi)部的所有“隱藏”點(紅色)現(xiàn)在都與點云外表面的“可見”點(藍色)分離。
將點云轉(zhuǎn)換為數(shù)據(jù)幀
正如人們所期望的那樣,點云中每個點的位置可以由三個數(shù)值定義——X、Y和Z坐標(biāo)。請記住,在上面的部分中,我們還估計了3D網(wǎng)格中每個點的曲面法線。當(dāng)我們從該網(wǎng)格中采樣點以創(chuàng)建點云時,點云中的每個點還包含與這些曲面法線相關(guān)的三個附加屬性——X、Y和Z方向上的法線單位向量坐標(biāo)。
因此,為了將點云轉(zhuǎn)換為數(shù)據(jù)幀,點云中的每個點都可以用以下七個屬性列在一行中來表示:
- X坐標(biāo)(浮動)
- Y坐標(biāo)(浮動)
- Z坐標(biāo)(浮動)
- X方向上的法向量坐標(biāo)(浮點)
- Y方向上的法向量坐標(biāo)(浮點)
- Z方向上的法向量坐標(biāo)(浮動)
- 可見點(布爾值True或False)
通過運行以下代碼行,可以將點云轉(zhuǎn)換為數(shù)據(jù)幀:
# 為點云創(chuàng)建一個數(shù)據(jù)幀,其中包含所有點的X、Y和Z位置坐標(biāo)以及X、Y、Z方向上的法向單位向量坐標(biāo)。
pcd_df = pd.DataFrame(np.concatenate((np.asarray(pcd.points), np.asarray(pcd.normals)), axis=1),
columns=["x", "y", "z", "norm-x", "norm-y", "norm-z"]
)
# 使用上面隱藏點刪除操作中的點索引聚合列表,添加一列以指示點是否可見。
pcd_df["point_visible"] = False
pcd_df.loc[pt_map_aggregated, "point_visible"] = True
這將返回如下所示的數(shù)據(jù)幀,其中每個點都是由上面解釋的七個屬性列表示的行。
轉(zhuǎn)換為數(shù)據(jù)幀的三維點云
保存點云和數(shù)據(jù)幀
現(xiàn)在可以通過運行以下代碼行來保存點云(刪除隱藏點之前和之后)和數(shù)據(jù)幀:
# 將整個點云保存為.pcd文件。
pcd_save_path = "data/3d_model.pcd"
o3d.io.write_point_cloud(pcd_save_path, pcd)
# 將刪除了隱藏點的點云保存為.pcd文件。
pcd_visible_save_path = "data/3d_model_hpr.pcd"
o3d.io.write_point_cloud(pcd_visible_save_path, pcd_visible)
# 將點云數(shù)據(jù)幀保存為.csv文件。
pcd_df_save_path = "data/3d_model.csv"
pcd_df.to_csv(pcd_df_save_path, index=False)
三維模型(頂部:整體,底部:裁剪)可視化為:1——網(wǎng)格;2——一個點云;3——刪除隱藏點后的點云
總結(jié)
希望本文演練能讓您更清楚地了解如何用Python處理3D數(shù)據(jù)。最后需要說明的是,本演練中使用的三維汽車模型已從原始文件中稍作修改,以適應(yīng)本練習(xí)的目的。這歸功于最初的創(chuàng)造者——“特斯拉Model S Plaid”,由ValentunW根據(jù)知識共享署名授權(quán)。
譯者介紹
朱先忠,51CTO社區(qū)編輯,51CTO專家博客、講師,濰坊一所高校計算機教師,自由編程界老兵一枚。
原文標(biāo)題:3D Data Processing with Open3D,作者:Prerak Agarwal