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