このページでは「Pythonプログラムを用いて点群処理を行い、オルソ画像やDSMを作る方法」について解説します。
現在工事中:スタート(2024.06.05)
写真測量ではSfMにより点群やDSM、オルソ画像を生成できますが、研究活動をする上では生成されたデータの生成過程を把握しておく必要があります。またLiDARや衛星画像など、他の計測プラットフォームより得られたデータと重ね合わせを行うためには、自作プログラムにより自分で生成できる方が好ましいです。そこでこのページではPythonのプログラムを用いてDEMとオルソ画像を生成する過程を解説します。
[ 注 意 書 き ]
・参考 Program の環境(python 3.8)
・以下のプログラムをコピーする時は、全角やスペースが含まれていないか注意してください。
(1) モジュールのインポート
(2)I/O の設定
(3)ランダムな点群をボクセルに格納して均質化
(4)水平方向に値のないセルを内挿
(5)DSM画像とオルソ画像を保存
import cv2
import pandas as pd
import numpy as np
from tqdm import tqdm
from osgeo import gdal
from osgeo import osr
from osgeo import gdal_array
import matplotlib.pyplot as plt
# 処理時間計測
import time
# メモリの解放
import gc
#Jupyterでインライン表示するための宣言
%matplotlib inline
# 対象観測シーンID
SceneID = "Saoka-20220720-1422-X5S"
# パスの設定
Dir1 = SceneID+ "/"
Dir2 = Dir1 + "IntermediateData/"
# I/O
IFile1 = Dir1 + SceneID + "-RandomPointCloud.txt" # 入力ファイル1 - ランダムな点群
OFile1 = Dir2 + SceneID + "-DSMImage.tif" # 出力ファイル1 - DSM画像
OFile2 = Dir2 + SceneID + "-OltImage.tif" # 出力ファイル2 - オルソ画像
# ====================================================================
# 地上解像度(単位:m / ボクセルのセルサイズにも利用)
GSD = 0.20
# ボクセルの範囲(例:日本平面直角座標4系)
xmin,xmax = 19940 , 20440
ymin,ymax = 71440 , 71940
zmin,zmax = 100 , 260
# トリミング範囲
xmin2,xmax2 = 20050 , 20350
ymin2,ymax2 = 71550 , 71850
# XYX軸ごとのセル数を算出
xsize, ysize, zsize = int(round((xmax-xmin)/GSD)),int(round((ymax-ymin)/GSD)),int(round((zmax-zmin)/GSD))
# ランダムな点群データを整形
PointCloud_pd = pd.read_csv(IFile1, sep=' ', skiprows=0, names=("x","y","z","r","g","b","nx","ny","nz"))
PointCloud_pd = PointCloud_pd.drop(["nx","ny","nz"],axis=1) # 法線は使わないのでドロップ
PointCloud_pd["xid"] = round((PointCloud_pd["x"]-xmin)/GSD,4).astype(int) # ボクセル Xid
PointCloud_pd["yid"] = round((ymax-PointCloud_pd["y"])/GSD,4).astype(int) # ボクセル yid
PointCloud_pd["zid"] = round((PointCloud_pd["z"]-zmin)/GSD,4).astype(int) # ボクセル zid
PointCloud_np = PointCloud_pd.values # NumPy に変換
# メモリを解放(点群データは大きいので消しておく)
del PointCloud_pd
gc.collect()
# 各XYZ軸に入力する値のリスト(セルの中央座標を使用する)
shift2C = GSD/2 # セルの中央座標へシフトするための値
digitsN = len(str(GSD).split('.')[1]) + 1 # 桁数
list_xv = np.round(np.arange(xmin+shift2C, xmax, GSD), digitsN)
list_yv = np.round(np.arange(ymax-shift2C, ymin,-GSD), digitsN)
list_zv = np.round(np.arange(zmin+shift2C, zmax, GSD), digitsN)
# 均質化された点群が入るボクセル
xv, yv, zv = np.zeros((zsize, ysize, xsize)), np.zeros((zsize, ysize, xsize)), np.zeros((zsize, ysize, xsize))
rv, gv, bv = np.zeros((zsize, ysize, xsize)), np.zeros((zsize, ysize, xsize)), np.zeros((zsize, ysize, xsize))
nv = np.zeros((zsize, ysize, xsize))
# 表層の点群だけが入るボクセル
xzm, yzm, zzm = np.zeros((ysize, xsize)), np.zeros((ysize, xsize)), np.zeros((ysize, xsize))
rzm, gzm, bzm = np.zeros((ysize, xsize)), np.zeros((ysize, xsize)), np.zeros((ysize, xsize))
nzm = np.zeros((ysize, xsize))
print("中央座標へシフトする距離:",shift2C," 座標の小数部桁数",digitsN)
print("セル数と入力値の数が合っているか?","x",xsize,":",len(list_xv)," y",ysize,":",len(list_yv)," z",zsize,":",len(list_zv))
# 関数:ボクセルによるランダム点群の均質化と表層のセルの抽出
def voxel(PointCloud_np):
for point in tqdm(PointCloud_np):
x, y, z = float(point[0]),float(point[1]),float(point[2])
R, G, B = int(point[3]),int(point[4]),int(point[5])
Xid, Yid, Zid = int(point[6]),int(point[7]),int(point[8])
# (1)均質化された点群:解析エリアに入っている点に絞り込む
if 0 <= Xid < xsize and 0 <= Yid < ysize and 0 <= Zid < zsize:
xv[Zid][Yid][Xid], yv[Zid][Yid][Xid], zv[Zid][Yid][Xid] = list_xv[Xid], list_yv[Yid], list_zv[Zid]
rv[Zid][Yid][Xid] += R
gv[Zid][Yid][Xid] += G
bv[Zid][Yid][Xid] += B
nv[Zid][Yid][Xid] += 1
nzm[Yid][Xid] += 1
# (2)DSM点群:標高が最大値の時に格納する
if zzm[Yid][Xid] < z:
xzm[Yid][Xid], yzm[Yid][Xid], zzm[Yid][Xid] = list_xv[Xid], list_yv[Yid], z
rzm[Yid][Xid], gzm[Yid][Xid], bzm[Yid][Xid] = R, G, B
return xzm,yzm,zzm,rzm,gzm,bzm,nzm
# プログラム実行(ボクセルによるランダム点群の均質化)
start = time.time()
xzm,yzm,zzm,rzm,gzm,bzm,nzm = voxel(PointCloud_np)
print ("プログラム実行時間:{0}".format(time.time()-start) + "[sec]")
# Matplotlib で確認する
plt.figure(figsize=(3, 3), dpi=150)
plt.imshow(zzm,cmap="gray")
plt.show()
# Matplotlib で確認する
img_rgb = cv2.merge((rzm.astype(np.uint8), gzm.astype(np.uint8), bzm.astype(np.uint8)))
plt.figure(figsize=(3, 3), dpi=150)
plt.imshow(img_rgb)
plt.show()
# 値の無いセルに周辺から穴埋めした値を入れる箱
X2, Y2, Z2, N2 = np.zeros((ysize,xsize)), np.zeros((ysize,xsize)), np.zeros((ysize,xsize)), np.zeros((ysize,xsize))
R2, G2, B2 = np.zeros((ysize,xsize),np.uint32), np.zeros((ysize,xsize),np.uint32), np.zeros((ysize,xsize),np.uint32)
# 近隣から値を検索する範囲(セル数)
MaxArea = 15
def Interpolation():
for j in tqdm(range(0,ysize)):
for i in range(0,xsize):
if nzm[j][i] > 0: # 点が一つでもあったところはそのまま代入
X2[j][i], Y2[j][i], Z2[j][i] = xzm[j][i], yzm[j][i], zzm[j][i]
R2[j][i], G2[j][i], B2[j][i] = rzm[j][i], gzm[j][i], bzm[j][i]
N2[j][i] = nzm[j][i]
else: # 点が無いところは近傍の平均値を入れる
k = 1
Nsum = 0
while Nsum == 0: # 0以上になるまで周辺から計算する
jmin,jmax = j-k,j+k+1
imin,imax = i-k,i+k+1
jmin = 0 if jmin < 0 else jmin
jmax = ysize if jmax > ysize else jmax
imin = 0 if imin < 0 else imin
imax = xsize if imax > xsize else imax
Zsum, Zn = np.sum(zzm[jmin:jmax,imin:imax]), np.count_nonzero(zzm[jmin:jmax,imin:imax] > 0)
Nsum = np.sum(nzm[jmin:jmax,imin:imax])
if Zsum and Nsum > 0 :
Rsum, Rn = np.sum(rzm[jmin:jmax,imin:imax]), np.count_nonzero(rzm[jmin:jmax,imin:imax])
Gsum, Gn = np.sum(gzm[jmin:jmax,imin:imax]), np.count_nonzero(gzm[jmin:jmax,imin:imax])
Bsum, Bn = np.sum(bzm[jmin:jmax,imin:imax]), np.count_nonzero(bzm[jmin:jmax,imin:imax])
X2[j][i] = round(xmin + i*GSD + shift2C, 4)
Y2[j][i] = round(ymax - j*GSD - shift2C, 4)
Z2[j][i] = round(Zsum / Zn,5)
R2[j][i] = 0 if Rsum == 0 else round(Rsum / Rn,0)
G2[j][i] = 0 if Gsum == 0 else round(Gsum / Gn,0)
B2[j][i] = 0 if Bsum == 0 else round(Bsum / Bn,0)
N2[j][i] = 1
break
elif k == MaxArea:
break
else:
k += 1
return X2,Y2,Z2,R2,G2,B2,N2
X2,Y2,Z2,R2,G2,B2,N2 = Interpolation()
# Matplotlib で確認する
plt.figure(figsize=(3, 3), dpi=150)
plt.imshow(Z2,cmap="gray")
plt.show()
# Matplotlib で確認する
img_rgb = cv2.merge((R2.astype(np.uint8), G2.astype(np.uint8), B2.astype(np.uint8)))
plt.figure(figsize=(3, 3), dpi=150)
plt.imshow(img_rgb)
plt.show()
# トリミング範囲の設定
xmin2num,xmax2num = int(round((xmin2-xmin)/GSD, 0)),int(round((xmax2-xmin)/GSD, 0))
ymin2num,ymax2num = int(round((ymax-ymax2)/GSD, 0)),int(round((ymax-ymin2)/GSD, 0))
xsize2 ,ysize2 = int(round((xmax2-xmin2)/GSD)) ,int(round((ymax2-ymin2)/GSD))
# Nodata の地点を入れるための前処理
SetNodata = -9999
Z3 = np.where(N2==0,SetNodata, Z2)
R3 = np.where(N2==0,SetNodata, R2)
G3 = np.where(N2==0,SetNodata, G2)
B3 = np.where(N2==0,SetNodata, B2)
# DSM画像の保存
driver = gdal.GetDriverByName('GTiff')
dataset = driver.Create(OFile1,xsize,ysize,1,gdal.GDT_Float32) # 画素数と型の設定
dataset.SetGeoTransform((xmin,GSD,0,ymax,0,-GSD)) # 位置情報の設定
srs = osr.SpatialReference() # 空間参照情報
srs.ImportFromEPSG(2446) # 日本測地第4系に座標系を指定
dataset.SetProjection(srs.ExportToWkt()) # 空間情報を結合
dataset.GetRasterBand(1).SetNoDataValue(SetNodata)
dataset.GetRasterBand(1).WriteArray(Z3[ymin2num:ymax2num,xmin2num:xmax2num])
dataset.FlushCache()
dataset = None
# オルソ画像の保存
driver = gdal.GetDriverByName('GTiff')
dataset = driver.Create(OFile2,xsize2,ysize2,3,gdal.GDT_Byte) # 画素数と型の設定
dataset.SetGeoTransform((xmin2,GSD,0,ymax2,0,-GSD)) # 位置情報の設
srs = osr.SpatialReference() # 空間参照情報
srs.ImportFromEPSG(2446) # 日本測地第4系に座標系を指定
dataset.SetProjection(srs.ExportToWkt()) # 空間情報を結合
dataset.GetRasterBand(1).SetNoDataValue(SetNodata)
dataset.GetRasterBand(1).WriteArray(R3[ymin2num:ymax2num,xmin2num:xmax2num])
dataset.GetRasterBand(2).WriteArray(G3[ymin2num:ymax2num,xmin2num:xmax2num])
dataset.GetRasterBand(3).WriteArray(B3[ymin2num:ymax2num,xmin2num:xmax2num])
dataset.FlushCache()
# Matplotlib で確認する
plt.figure(figsize=(3, 3), dpi=150)
plt.imshow(Z3[ymin2num:ymax2num,xmin2num:xmax2num],cmap="gray")
plt.show()
# Matplotlib で確認する
img_rgb = cv2.merge((R3.astype(np.uint8), G3.astype(np.uint8), B3.astype(np.uint8)))
plt.figure(figsize=(3, 3), dpi=150)
plt.imshow(img_rgb[ymin2num:ymax2num,xmin2num:xmax2num])
plt.show()