基於Python深度圖生成3D點雲

語言: CN / TW / HK

theme: channing-cyan highlight: agate


攜手創作,共同成長!這是我參與「掘金日新計劃 · 8 月更文挑戰」的第2天,點選檢視活動詳情 >>

前言

廢話不多說,直接開造。這裡的話我們有兩個目標,第一個是如何把一個2維圖片上的點對映到3維空間。第二就是如何生成3D點雲。當然實際上這是一個大問題,因為只要解決了第一個問題,第二個問題就是“送分”

二維RGB影象

在說到3D點雲之前我們就不得不說到RGB影象,也就是一些二維影象。

影象以像RGB三個通道的形式進行儲存。也就是這樣: 在這裡插入圖片描述 平時我們看到的就是左邊的2D影象,實際上是以右邊的形式儲存的。

那麼從我們的矩陣角度來看的話,大概是這個樣子的:

```python [ [[r,g,b],[r,g,b],[r,g,b],[r,g,b]], [[r,g,b],[r,g,b],[r,g,b],[r,g,b]], ...

] ```

這裡面存在了兩組資訊, 第一組就是我們最容易忽略的位置資訊,也就是畫素點的位置。 第二組就是我們的色彩資訊。

顯然如果我們想要生成3D點雲的話,我們的色彩資訊是必要的。

而位置資訊,在二維平面上,是在3為空間上的對映

那麼要想得到3D點雲,那麼我們就必須得到完整的位置座標,也就是x,y,z

成像原理

那麼說到這裡的話,我們就不得不說到,攝像機是如何把咱們的影像映射出來的了。

大概他是這樣成像的: 在這裡插入圖片描述

這裡咱們有三個東西需要注意一下。

首先是我們的攝像機位置 之後是相片的位置也就是中間那個 最後是我們物品實際位置

所以我們實際上的一個二維影象,就是一個投影,在實際上的話,我們可以理解為深度為1的空間。 但是這個投影

所以的話,用數學矩陣的形式表示為:

在這裡插入圖片描述 其中u,v為二維平面上的任意點座標,1為深度,Zc就是咱們的二維平面上的點。

其中R,T是外參矩陣,並且我們的世界座標原點就是相機的座標原點 所以,R,T可以取下面的矩陣,(具體原理的話比較複雜,我也不是很懂為什麼會這樣,我回頭再補補數學) 在這裡插入圖片描述 帶入得到: 在這裡插入圖片描述

最後轉換得到這個公式: 在這裡插入圖片描述

那麼接下來咱們就對這個公式進行處理,來生成我們的點雲

資料準備

前面通過公式我們可以瞭解到,一個二維影象,具備了RGB資訊,還有對應的那個畫素點在二維空間的“投影”。我們需要將這個點重新還原到三維空間。所以這裡需要使用到深度資訊,而這個是需要一些專業攝像機才有的,比如iphone的,或者機器人的: 在這裡插入圖片描述

在這裡插入圖片描述

有了這個深度圖片,我們就可以還原座標了。其實說到這兒,我們其實可以發現,RGB圖片的話其實只是提供了一下色彩如果要還原為3D點雲的話,因為深度圖和對應的rgb的矩陣的位置是一一對應的。 在這裡插入圖片描述

圖片載入

說到這裡,我們就可以開始我們的正式編碼了。

首先是咱們的圖片載入階段,這裡的話主要是我們先需要一些資訊,和讀取圖片。

```python

def __init__(self, rgb_file, depth_file, save_ply, camera_intrinsics=[784.0, 779.0, 649.0, 405.0]):
    self.rgb_file = rgb_file
    self.depth_file = depth_file
    self.save_ply = save_ply

    self.rgb = cv2.imread(rgb_file)
    self.depth = cv2.imread(self.depth_file, -1)

    print("your depth image shape is:",self.depth.shape)

    self.width = self.rgb.shape[1]
    self.height = self.rgb.shape[0]

    self.camera_intrinsics = camera_intrinsics
    self.depth_scale = 1000

``` 下面的這個東西,就是咱們公式當中的u0,v0,dx,dy

python camera_intrinsics=[784.0, 779.0, 649.0, 405.0] 這個要注意的是,這個玩意呢,是不同的裝置相機有不同的引數,要根據自己的設定。

一般來說相機會給這樣的矩陣:

在這裡插入圖片描述

我們對應起來就可以了。

之後是要注意的是這個深度圖是uint16的,所以讀取的時候加個-1 在這裡插入圖片描述

這裡還有個東西: 深度圖與比例因子(scale_factor) 在程式碼中是:

python self.depth_scale = 1000

深度圖對應的尺度因子是深度圖中儲存的值與真實深度(單位為m)的比例 通常情況下,深度值以毫米為單位儲存在16位無符號整數中(0~65535),因此要獲得以米為單位的z值,深度圖的畫素需要除以比例因子1000。不過不同相機的的比例因子可能不同,我這裡的話是1000,這個看自己的實際的。

演算法實現

之後就是咱們的演算法實現了,這個主要是位置換算,得到真實的x,y,z

```python

    depth = np.asarray(self.depth, dtype=np.uint16).T
    # depth[depth==65535]=0
    self.Z = depth / self.depth_scale
    fx, fy, cx, cy = self.camera_intrinsics

    X = np.zeros((self.width, self.height))
    Y = np.zeros((self.width, self.height))
    for i in range(self.width):
        X[i, :] = np.full(X.shape[1], i)

    self.X = ((X - cx / 2) * self.Z) / fx
    for i in range(self.height):
        Y[:, i] = np.full(Y.shape[0], i)
    self.Y = ((Y - cy / 2) * self.Z) / fy

```

之後,x,y,z就算好了。

注意的的是我們計算完成後得到的x,y , x 的每一組向量都是 寬×高

生成點雲

現在咱們已經得到了具體的座標。那麼接下來是要生成點雲的,我們要把剩下的色彩資訊搞上去。 程式碼很簡單:

python data_ply = np.zeros((6, self.width * self.height)) data_ply[0] = self.X.T.reshape(-1) data_ply[1] = -self.Y.T.reshape(-1) data_ply[2] = -self.Z.T.reshape(-1) img = np.array(self.rgb, dtype=np.uint8) data_ply[3] = img[:, :, 0:1].reshape(-1) data_ply[4] = img[:, :, 1:2].reshape(-1) data_ply[5] = img[:, :, 2:3].reshape(-1) self.data_ply = data_ply t2 = time.time() print('calcualte 3d point cloud Done.', t2 - t1)

之後就是儲存檔案了。 目前點雲的主要儲存格式包括:pts、LAS、PCD、.xyz 和. pcap 等

例如: pts 點雲檔案格式是最簡便的點雲格式,直接按 XYZ 順序儲存點雲資料, 可以是整型或者浮點型。在這裡插入圖片描述 LAS 是鐳射雷達資料(LiDAR),儲存格式比 pts 複雜,旨在提供一種開放的格式標準,允許不同的硬體和軟體提供商輸出可互操作的統一格式。LAS 格式點雲截圖,其中 C:class(所屬類),F:flight(航線號),T:time(GPS 時間),I:intensity(回波強度),R:return(第幾次回波),N:number of return(回波次數),A:scan angle(掃描角),RGB:red green blue(RGB 顏色值)。

在這裡插入圖片描述

等等,我們這裡的是ply 全名為多邊形檔案(Polygon File Format)或 史丹佛三角形檔案(Stanford Triangle Format)。. 該格式主要用以儲存立體掃描結果的三維數值,透過多邊形片面的集合描述三維物體,與其他格式相較之下這是較為簡單的方法。. 它可以儲存的資訊包含顏色、透明度、表面法向量、材質座標與資料可信度,並能對多邊形的正反兩面設定不同的屬性。

格式為

頭部

頂點列表

面片列表

其他元素列表

在這裡插入圖片描述

```python start = time.time() float_formatter = lambda x: "%.4f" % x points = [] for i in self.data_ply.T: points.append("{} {} {} {} {} {} 0\n".format (float_formatter(i[0]), float_formatter(i[1]), float_formatter(i[2]), int(i[3]), int(i[4]), int(i[5])))

    file = open(self.save_ply, "w")
    file.write('''ply
    format ascii 1.0
    element vertex %d
    property float x
    property float y
    property float z
    property uchar red
    property uchar green
    property uchar blue
    property uchar alpha
    end_header
    %s
    ''' % (len(points), "".join(points)))
    file.close()

    end = time.time()
    print("Write into .ply file Done.", end - start)

```

點雲顯示

這個的話可以使用軟體 在這裡插入圖片描述 也可以使用open3d模組

完整程式碼

```python import cv2 import numpy as np import open3d as o3d

import time

class point_cloud_generator():

def __init__(self, rgb_file, depth_file, save_ply, camera_intrinsics=[784.0, 779.0, 649.0, 405.0]):
    self.rgb_file = rgb_file
    self.depth_file = depth_file
    self.save_ply = save_ply

    self.rgb = cv2.imread(rgb_file)
    self.depth = cv2.imread(self.depth_file, -1)

    print("your depth image shape is:",self.depth.shape)

    self.width = self.rgb.shape[1]
    self.height = self.rgb.shape[0]

    self.camera_intrinsics = camera_intrinsics
    self.depth_scale = 1000

def compute(self):
    t1 = time.time()

    depth = np.asarray(self.depth, dtype=np.uint16).T
    # depth[depth==65535]=0
    self.Z = depth / self.depth_scale
    fx, fy, cx, cy = self.camera_intrinsics

    X = np.zeros((self.width, self.height))
    Y = np.zeros((self.width, self.height))
    for i in range(self.width):
        X[i, :] = np.full(X.shape[1], i)

    self.X = ((X - cx / 2) * self.Z) / fx
    for i in range(self.height):
        Y[:, i] = np.full(Y.shape[0], i)
    self.Y = ((Y - cy / 2) * self.Z) / fy

    data_ply = np.zeros((6, self.width * self.height))
    data_ply[0] = self.X.T.reshape(-1)
    data_ply[1] = -self.Y.T.reshape(-1)
    data_ply[2] = -self.Z.T.reshape(-1)
    img = np.array(self.rgb, dtype=np.uint8)
    data_ply[3] = img[:, :, 0:1].reshape(-1)
    data_ply[4] = img[:, :, 1:2].reshape(-1)
    data_ply[5] = img[:, :, 2:3].reshape(-1)
    self.data_ply = data_ply
    t2 = time.time()
    print('calcualte 3d point cloud Done.', t2 - t1)

def write_ply(self):
    start = time.time()
    float_formatter = lambda x: "%.4f" % x
    points = []
    for i in self.data_ply.T:
        points.append("{} {} {} {} {} {} 0\n".format
                      (float_formatter(i[0]), float_formatter(i[1]), float_formatter(i[2]),
                       int(i[3]), int(i[4]), int(i[5])))

    file = open(self.save_ply, "w")
    file.write('''ply
    format ascii 1.0
    element vertex %d
    property float x
    property float y
    property float z
    property uchar red
    property uchar green
    property uchar blue
    property uchar alpha
    end_header
    %s
    ''' % (len(points), "".join(points)))
    file.close()

    end = time.time()
    print("Write into .ply file Done.", end - start)

def show_point_cloud(self):
    pcd = o3d.io.read_point_cloud(self.save_ply)
    o3d.visualization.draw([pcd])

if name == 'main': camera_intrinsics = [378.998657, 378.639862, 321.935120, 240.766663] rgb_file = "data/1.jpg" depth_file = "data/1.png" save_ply = "data.ply" a = point_cloud_generator(rgb_file=rgb_file, depth_file=depth_file, save_ply=save_ply, camera_intrinsics=camera_intrinsics ) a.compute() a.write_ply() a.show_point_cloud()

``` 效果如下: 在這裡插入圖片描述

總結

這裡的話其實還是從一張深度圖到3d點雲,實際上有時候我們可能只需一組座標然後還原,那麼這部分的話,要實現的話也不難,不過要重新做一點轉換,公式還是那個公式。這裡的話就不能在多說了,而且這裡還有一點值得一提的是這個通過這種方式得到的3d點雲其實怎麼說呢,並不是特別準,所以這邊還是要更加牛批的人工智慧演算法的,目前有個整合玩意可以玩玩的是middlepipe。這方面的話Google還是得是他。