├── CSFtools.py ├── README.md ├── User Manual.pdf ├── csfclassify.py ├── csfcrown.py ├── csfdem.py ├── csfground.py ├── csflai.py ├── csfnormalize.py └── utils.py /CSFtools.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | class CSFtools: 5 | def __init__(self): 6 | self.parentPath = os.path.split(os.path.realpath(__file__))[0] 7 | 8 | def fullPath(self,script_name): 9 | return os.path.join(self.parentPath, script_name) 10 | 11 | def getPyExePath(self): 12 | return sys.executable 13 | 14 | def command(self,script): 15 | return self.getPyExePath()+ " " + self.fullPath(script) 16 | 17 | def parse_params(self, params): 18 | if len(params)%2 != 0: 19 | print("Parameter number is not correct.") 20 | cmdstr = "" 21 | for i in range(0, len(params),2): 22 | param_name = params[i] 23 | param_value = params[i+1] 24 | cmdstr += param_name + " " + str(param_value) + " " 25 | return cmdstr 26 | 27 | def csfground(self, params): 28 | cmdstr = self.parse_params(params) 29 | os.system(self.command("csfground.py " + cmdstr)) 30 | 31 | def csfdem(self, params): 32 | cmdstr = self.parse_params(params) 33 | os.system(self.command("csfdem.py "+cmdstr)) 34 | 35 | def csfnormalize(self, params): 36 | cmdstr = self.parse_params(params) 37 | os.system(self.command("csfnormalize.py " + cmdstr)) 38 | 39 | def csfclassify(self, params): 40 | cmdstr = self.parse_params(params) 41 | os.system(self.command("csfclassify.py " + cmdstr)) 42 | 43 | def csfcrown(self, params): 44 | cmdstr = self.parse_params(params) 45 | os.system(self.command("csfcrown.py " + cmdstr)) 46 | 47 | def csflai(self, params): 48 | cmdstr = self.parse_params(params) 49 | os.system(self.command("csflai.py " + cmdstr)) 50 | 51 | def csfheight(self, params): 52 | cmdstr = self.parse_params(params) 53 | os.system(self.command("csfheight.py " + cmdstr)) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CSFTools 2 | Tools to processing LiDAR point cloud based on CSF. 3 | 4 | CSFTools provides a set of Python based tools, including: 5 | 6 | - csfground.py: to filter a point cloud 7 | - csfdem.py: a simple gridding and interpolation algorithm to generate a DEM/DSM/CHM 8 | - csfnormalize.py: normalize point cloud 9 | - csfclassify.py: use a scalar field to classify the point cloud into 2 classes 10 | - csflai.py: compute leaf area index (LAI) from airborne discrete-return LiDAR data 11 | - csfcrown.py: segment tree crowns from CHM 12 | 13 | More details, please refer to User Manual. 14 | 15 | ## Installation 16 | 17 | This preprocessing tool requires a few python libraries, to make it easier to install, we recommend to use anaconda (python 3.6+), which has already been integrated with a few scientific computing libraries. 18 | Other libs: 19 | 20 | - laspy: supporting reading and writing of las file. https://github.com/laspy/laspy 21 | run: 22 | 23 | pip install laspy 24 | 25 | or download the source and run: 26 | 27 | python setup.py build 28 | python setup.py install 29 | 30 | - GDAL 31 | 32 | conda install gdal 33 | 34 | - joblib: supporting parallel computing for python 35 | 36 | pip install joblib 37 | 38 | - mahotas: computer vision library, supporting watershed transform, etc. 39 | 40 | conda config --add channels conda-forge 41 | conda install mahotas 42 | 43 | - CSF: ground filtering library, go to: https://github.com/jianboqi/CSF, and download all the source code: 44 | Under the folder python, run: 45 | 46 | python setup.py build 47 | python setup.py install 48 | -------------------------------------------------------------------------------- /User Manual.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jianboqi/CSFTools/c7ebe642432ca388ed807ca07ad3063c41b00ca7/User Manual.pdf -------------------------------------------------------------------------------- /csfclassify.py: -------------------------------------------------------------------------------- 1 | """ 2 | Classify point cloud according to Scalar Value or x, y, z 3 | Author: Jianbo Qi 4 | Date: 2017-4-25 5 | """ 6 | import argparse 7 | import laspy 8 | import math 9 | import time 10 | import numpy as np 11 | import joblib 12 | import tempfile 13 | import os 14 | import shutil 15 | 16 | 17 | def sub_fun_classify(_fieldList, _threshold, _class_arr, _seg_index, _seg_size): 18 | # corresponding interval 19 | lower = _seg_index * _seg_size 20 | upper = min((_seg_index+1) * _seg_size, len(_fieldList)) 21 | print("Processing from: ", lower, " to ", upper) 22 | # For each point, find its corresponding cell 23 | for i in range(lower, upper): 24 | if _fieldList[i] <= _threshold: 25 | _class_arr[i] = 0 26 | else: 27 | _class_arr[i] = 1 28 | 29 | 30 | if __name__ == "__main__": 31 | # parameter handling 32 | parse = argparse.ArgumentParser() 33 | parse.add_argument("-i", help="Input las file.", required=True) 34 | parse.add_argument("-field", help="Which one is the classification based on. field, x, y,z.", required=True) 35 | parse.add_argument("-value", help="threshold. < value will be 0, > value will be 1. ", required=True, type=float) 36 | parse.add_argument("-o", help="Output las file name (*.las).", required=True) 37 | parse.add_argument("-seg_size", help="How many points for each core to run parallelly. ", type=int, default=500000) 38 | args = parse.parse_args() 39 | 40 | input_las_file = args.i 41 | output_las_file = args.o 42 | field = args.field 43 | threshold = args.value 44 | 45 | start = time.clock() 46 | print("Reading data...") 47 | # read point cloud 48 | inFile = laspy.read(input_las_file) 49 | # field list 50 | fieldList = [] 51 | if field in ("x", "X"): 52 | fieldList = inFile.x 53 | elif field in ("y", "Y"): 54 | fieldList = inFile.y 55 | elif field in ("z", "Z"): 56 | fieldList = inFile.z 57 | elif field in ("intensity", "Intensity"): 58 | fieldList = inFile.intensity 59 | elif field in ("return_num", "Return_num"): 60 | fieldList = inFile.return_num 61 | elif field in ("num_returns", "Num_returns"): 62 | fieldList = inFile.num_returns 63 | elif field in ("scan_angle_rank", "Scan_angle_rank"): 64 | fieldList = inFile.scan_angle_rank 65 | elif field in ("time", "Time", "gps_time", "Gps_time"): 66 | fieldList = inFile.gps_time 67 | 68 | if len(fieldList) == 0: 69 | print("No field found.") 70 | import sys 71 | sys.exit(0) 72 | 73 | point_number = len(fieldList) 74 | print("Total points:", point_number) 75 | print("Start to classify...") 76 | 77 | # prepare for parallel computing 78 | # segment the array into multiple segmentation by define a maximum size of each part 79 | seg_size = args.seg_size # 500000 points for each core, parallel 80 | seg_num = int(math.ceil(point_number / float(seg_size))) 81 | # read DEM 82 | folder = tempfile.mkdtemp() 83 | class_out_name = os.path.join(folder, 'classify') 84 | class_arr = np.memmap(class_out_name, dtype=int, shape=(len(fieldList),), mode='w+') 85 | joblib.Parallel(n_jobs=joblib.cpu_count(), max_nbytes=1e4)(joblib.delayed(sub_fun_classify) 86 | (fieldList, threshold, class_arr, i, seg_size) 87 | for i in range(0, seg_num)) 88 | out_File = laspy.LasData(inFile.header) 89 | out_File.points = inFile.points 90 | out_File.classification = class_arr.tolist() 91 | out_File.write(output_las_file) 92 | print("Done.") 93 | del class_arr 94 | try: 95 | shutil.rmtree(folder) 96 | except OSError: 97 | print("Failed to delete: " + folder) 98 | end = time.clock() 99 | print("Time: ", "%.3fs" % (end - start)) -------------------------------------------------------------------------------- /csfcrown.py: -------------------------------------------------------------------------------- 1 | #coding: utf-8 2 | import gdal 3 | import mahotas 4 | import math 5 | import numpy as np 6 | import random 7 | import sys 8 | import argparse 9 | import joblib 10 | import tempfile 11 | import os 12 | from utils import saveToHdr 13 | from utils import read_img_to_arr_no_transform 14 | 15 | def log(*args): 16 | outstr = "" 17 | for i in args: 18 | outstr += str(i) 19 | print(outstr) 20 | sys.stdout.flush() 21 | 22 | 23 | def sub_fun(_nuclear, _areas, _detected_trees_pos, _seg_index, _real_coordinate, 24 | _offset_x, _offset_y, _pixel_size, _seg_size): 25 | # 每棵树用不用的数值标记 26 | lower = _seg_index * _seg_size 27 | upper = min((_seg_index + 1) * _seg_size, _areas.max()) 28 | log("Processing from: ", lower, " to ", upper) 29 | for i in range(lower+1, upper+1): 30 | treepixel = np.where(_areas == i) 31 | tmpmax = 0 32 | tx, ty = 0, 0 # row col 33 | tmp = 0 34 | crown = math.sqrt(len(treepixel[0]) * _pixel_size * _pixel_size/math.pi) * 2 35 | # tx,ty = sum(treepixel[0])/float(len(treepixel[0])),sum(treepixel[1])/float(len(treepixel[1])) 36 | for m in range(0, len(treepixel[0])): 37 | tmp += 1 38 | if _nuclear[treepixel[0][m]][treepixel[1][m]] > tmpmax: 39 | tmpmax = _nuclear[treepixel[0][m]][treepixel[1][m]] 40 | tx, ty = treepixel[0][m], treepixel[1][m] 41 | # maxHeight = nuclear[tx, ty] 42 | if _real_coordinate: 43 | x = _offset_x + ty * _pixel_size 44 | y = _offset_y + tx * _pixel_size 45 | _detected_trees_pos[i-1,:] = [x, y, crown] 46 | else: 47 | x = _offset_x + ty 48 | y = _offset_y + tx 49 | _detected_trees_pos[i-1, :] = [y, x, crown] # row col 50 | 51 | 52 | def save_as_random_color_img(_dataarr, filepath): 53 | rows, cols = _dataarr.shape 54 | re = np.zeros((rows, cols, 3),dtype=np.uint8) 55 | colormap = dict() 56 | colormap[0.0] = [0, 0, 0] 57 | for row in range(rows): 58 | for col in range(cols): 59 | if _dataarr[row][col] in colormap: 60 | re[row, col, :] = colormap[_dataarr[row][col]] 61 | else: 62 | color = [random.randint(0, 255),random.randint(0, 255),random.randint(0, 255)] 63 | re[row, col, :] = color 64 | colormap[_dataarr[row][col]] = color 65 | mahotas.imsave(filepath, re) 66 | 67 | 68 | if __name__ == "__main__": 69 | parser = argparse.ArgumentParser() 70 | parser.add_argument("-i", help="Input CHM file.",required=True) 71 | parser.add_argument("-o", help="Output file.", required=True) 72 | parser.add_argument("-color_img", help="Save a color image.", type=bool, default=False) 73 | parser.add_argument("-seg_size", help="Tree number for each core. ", type=int, default=2000) 74 | parser.add_argument("-subregion", help="Divide image into subregions (pixels). ", type=int, default=1000) 75 | parser.add_argument("-height_threshold", help="Threshold to remove grass, bushes etc. ", type=float, default=2) 76 | parser.add_argument("-window_size", help="Window size for segmentation. ", type=int, default=7) 77 | parser.add_argument("-real_coordinate", help="Whether output real coordinates or pixel positions. ", 78 | type=bool, default=True) 79 | args = parser.parse_args() 80 | 81 | import time 82 | start = time.clock() 83 | 84 | chm_hdr_path = args.i 85 | out_file = args.o 86 | subregion = args.subregion 87 | real_coordinate = args.real_coordinate 88 | seg_size = args.seg_size 89 | height_threshold = args.height_threshold 90 | window_size = args.window_size 91 | pixel_size = -1 92 | color_image = args.color_img 93 | 94 | idata_set = gdal.Open(chm_hdr_path) 95 | transform = idata_set.GetGeoTransform() 96 | if real_coordinate: 97 | if transform is None: 98 | log("ERROR: No geotransform found for file ", chm_hdr_path) 99 | sys.exit(0) 100 | else: 101 | pixel_size = abs(transform[1]) 102 | 103 | 104 | band = idata_set.GetRasterBand(1) 105 | banddata = band.ReadAsArray(0, 0, band.XSize, band.YSize) 106 | width = band.XSize # XSize是列数, YSize是行数 107 | height = band.YSize 108 | if (subregion == 0): # 一般情况不会进行分块计算 109 | subregion = max(width, height) 110 | num_width = int(math.ceil(width / float(subregion))) 111 | num_height = int(math.ceil(height / float(subregion))) 112 | total_tree_num = 0 113 | log("INFO: Number of sub regions: " + str(num_height) + " * " + str(num_width)) 114 | for num_r in range(0, num_height): # row 115 | for num_c in range(0, num_width): # col 116 | log("INFO: Region: " + str(num_r) + " " + str(num_c)) 117 | row_start = num_r * subregion 118 | row_end = min((num_r + 1) * subregion, height) 119 | col_start = num_c * subregion 120 | col_end = min((num_c + 1) * subregion, width) 121 | if real_coordinate: 122 | offset_x = num_c * subregion * pixel_size 123 | offset_y = num_r * subregion * pixel_size 124 | else: 125 | offset_x = num_c * subregion 126 | offset_y = num_r * subregion 127 | 128 | nuclear = banddata[row_start:row_end, col_start:col_end] 129 | # 不保存文件 再读取,检测的树木数量很少,有问题以后检查 130 | row_col_img = chm_hdr_path + "_seg_"+str(num_r)+"_"+str(num_c) 131 | 132 | saveToHdr(nuclear,row_col_img) 133 | nuclear = read_img_to_arr_no_transform(row_col_img) 134 | if not color_image: 135 | os.remove(row_col_img) 136 | if os.path.exists(row_col_img + ".hdr"): 137 | os.remove(row_col_img+".hdr") 138 | 139 | threshed = (nuclear > height_threshold) 140 | nuclear *= threshed 141 | bc = np.ones((window_size, window_size)) 142 | 143 | maxima = mahotas.morph.regmax(nuclear, Bc=bc) 144 | spots, n_spots = mahotas.label(maxima) 145 | 146 | surface = (nuclear.max() - nuclear) 147 | areas = mahotas.cwatershed(surface, spots) 148 | areas *= threshed 149 | if color_image: 150 | save_as_random_color_img(areas, chm_hdr_path + "_seg_"+str(num_r)+"_"+str(num_c)+"color.jpg") 151 | 152 | area_max = areas.max() 153 | seg_num = int(math.ceil(area_max / float(seg_size))) 154 | total_tree_num += area_max 155 | log("INFO: Sub region trees:", area_max) 156 | log("INFO: Start parallel computing...") 157 | # temp file 158 | folder = tempfile.mkdtemp() 159 | detected_trees_pos = np.memmap(os.path.join(folder, 'treedetect'), dtype=float, shape=(area_max, 3), 160 | mode='w+') 161 | 162 | joblib.Parallel(n_jobs=joblib.cpu_count()-1, max_nbytes=None)( 163 | joblib.delayed(sub_fun)(nuclear, areas, detected_trees_pos, i, 164 | real_coordinate, offset_x, offset_y, 165 | pixel_size, seg_size) for i in range(0, seg_num)) 166 | 167 | if num_r == 0 and num_c == 0: 168 | fw = open(out_file, 'w') 169 | else: 170 | fw = open(out_file, 'a') 171 | for i in range(0, len(detected_trees_pos)): 172 | outstr = str(detected_trees_pos[i][0]) + " " + str(detected_trees_pos[i][1])+\ 173 | " %.4f"%detected_trees_pos[i][2] 174 | fw.write(outstr + "\n") 175 | fw.close() 176 | del detected_trees_pos 177 | try: 178 | import shutil 179 | shutil.rmtree(folder) 180 | except OSError: 181 | log("Failed to delete: " + folder) 182 | log("INFO: Total detected trees: ", total_tree_num) 183 | log("Done.") 184 | end = time.clock() 185 | log("Time: ", "%.3fs" % (end - start)) -------------------------------------------------------------------------------- /csfdem.py: -------------------------------------------------------------------------------- 1 | """ 2 | DEM Generation from point cloud 3 | Author: Jianbo Qi 4 | Date: 2017-1-15 5 | """ 6 | import argparse 7 | import laspy 8 | import math 9 | import time 10 | import numpy as np 11 | import joblib 12 | import tempfile 13 | import os 14 | import shutil 15 | from utils import saveToHdr 16 | 17 | def compute_bounding_box(input_File, cell_resolution): 18 | mins = input_File.header.min 19 | maxs = input_File.header.max 20 | # add a small value to be more convenient to do gridding 21 | width = maxs[0] - mins[0] + 0.00001 22 | height = maxs[1] - mins[1] + 0.00001 23 | min_x, min_y = mins[0], mins[1] 24 | num_w = int(math.ceil(width / float(cell_resolution))) 25 | num_h = int(math.ceil(height / float(cell_resolution))) 26 | return min_x, min_y, width, height, num_w, num_h 27 | 28 | 29 | 30 | # interpolate the empty cells after rasterization 31 | def interpolate(data_arr, size=1): 32 | rows, cols = data_arr.shape 33 | rpos, cpos = np.where(data_arr==0) 34 | print("Processing empty cells... ") 35 | print("Total empty cells: ", len(rpos)) 36 | for i in range(0, len(rpos)): 37 | while True: 38 | left = max(0, cpos[i]-size) 39 | right = min(cols-1, cpos[i] + size) 40 | up = max(0, rpos[i] - size) 41 | down = min(rows-1, rpos[i] + size) 42 | empty = True 43 | t_w = 0 44 | interpolated_value = 0 45 | for m in range(up, down+1): 46 | for n in range(left, right+1): 47 | if data_arr[m][n] > 0: 48 | w = 1/float((m-rpos[i])**2+(n-cpos[i])**2) 49 | interpolated_value += data_arr[m][n]*w 50 | t_w += w 51 | empty = False 52 | if not empty: 53 | interpolated_value /= t_w 54 | data_arr[rpos[i]][cpos[i]] = interpolated_value 55 | break 56 | size += 1 57 | 58 | def fillHoleofchm(datarr, h): 59 | rows, cols = datarr.shape 60 | for i in range(0, rows): 61 | for j in range(0, cols): 62 | pv = datarr[i][j] 63 | left = max(0, j - 1) 64 | right = min(j + 1, cols-1) 65 | up = max(0, i - 1) 66 | down = min(i + 1, rows-1) 67 | total = np.array([datarr[i][left], datarr[i][right], datarr[up][j], datarr[down][j]]) 68 | diff = total - pv 69 | diff = diff > h 70 | if diff.sum() > 2: 71 | # if pv < left - t and pv < right-t and pv < up -t and pv < down-t: 72 | datarr[i][j] = total.sum() * 0.25 73 | 74 | 75 | def sub_fun_dem(d_xyz, output_arr_dem, _output_dem_num, _seg_index, _num_w, _num_h, _resolution, _search_length, _seg_size, _method): 76 | # corresponding interval 77 | lower = _seg_index * _seg_size 78 | upper = min((_seg_index+1) * _seg_size, len(d_xyz)) 79 | print("Processing from: ", lower, " to ", upper) 80 | # For each point, find its corresponding cell 81 | for i in range(lower, upper): 82 | row = int(d_xyz[i][1] / _resolution) # row of the corresponding cell 83 | col = int(d_xyz[i][0] / _resolution) 84 | # determine neighbors 85 | # the position of each cell's left bottom corner 86 | cell_x_start = col * _resolution 87 | cell_y_start = row * _resolution 88 | # position relative to cell left bottom corner 89 | # for each points, calculate the relatively position to the cell's left corner 90 | # thus this values should in [0, resolution] 91 | cell_relative_x = d_xyz[i][0] - cell_x_start 92 | cell_relative_y = d_xyz[i][1] - cell_y_start 93 | # compute influence region: for each point, computing the influenced cells. 94 | # down and up influence 95 | delta_y_down, delta_y_up = _search_length*0.5 - cell_relative_y, \ 96 | _search_length*0.5 - (_resolution - cell_relative_y) 97 | influence_down_num, influence_up_num = max(0,int(math.ceil(delta_y_down / float(_resolution)))), \ 98 | max(0,int(math.ceil(delta_y_up / float(_resolution)))) 99 | delta_x_left, delta_x_right = _search_length*0.5 - cell_relative_x, \ 100 | _search_length*0.5 - (_resolution - cell_relative_x) 101 | influence_left_num, influence_right_num = max(0,int(math.ceil(delta_x_left / float(_resolution)))), \ 102 | max(0,int(math.ceil(delta_x_right / float(_resolution)))) 103 | # print influence_right_num, influence_left_num, influence_down_num, influence_up_num 104 | for rr in range(row - influence_down_num, row + influence_up_num + 1): 105 | for cc in range(col - influence_left_num, col + influence_right_num + 1): 106 | if (-1 < rr < _num_h) and (-1 < cc < _num_w): 107 | _output_dem_num[_num_h - rr - 1][cc] += 1 108 | output_arr_dem[_num_h - rr - 1][cc] += d_xyz[i][2] 109 | # output_arr_dem[_num_h - rr - 1][cc] = min(output_arr_dem[_num_h - rr - 1][cc], d_xyz[i][2]) 110 | 111 | 112 | def sub_fun_dsm_dem(d_xyz, _classification, output_arr_dem, _output_dem_num, output_arr_dsm, _seg_index, _num_w, _num_h, _resolution, _search_length, _seg_size, _method): 113 | # corresponding interval 114 | lower = _seg_index * _seg_size 115 | upper = min((_seg_index+1) * _seg_size, len(d_xyz)) 116 | print("Processing from: ", lower, " to ", upper) 117 | for i in range(lower, upper): 118 | row = int(d_xyz[i][1] / _resolution) 119 | col = int(d_xyz[i][0] / _resolution) 120 | # determine neighbors 121 | # the position of each cell's left bottom corner 122 | cell_x_start = col * _resolution 123 | cell_y_start = row * _resolution 124 | # position relative to cell left bottom corner 125 | # for each points, calculate the relatively position to the cell's left corner 126 | # thus this values should in [0, resolution] 127 | cell_relative_x = d_xyz[i][0] - cell_x_start 128 | cell_relative_y = d_xyz[i][1] - cell_y_start 129 | # compute influence region: for each point, computing the influenced cells. 130 | # down and up influence 131 | delta_y_down, delta_y_up = _search_length*0.5 - cell_relative_y, \ 132 | _search_length*0.5 - (_resolution - cell_relative_y) 133 | influence_down_num, influence_up_num = max(0, int(math.ceil(delta_y_down / float(_resolution)))), \ 134 | max(0,int(math.ceil(delta_y_up / float(_resolution)))) 135 | # print delta_y_down, delta_y_up 136 | delta_x_left, delta_x_right = _search_length*0.5 - cell_relative_x, \ 137 | _search_length*0.5 - (_resolution - cell_relative_x) 138 | influence_left_num, influence_right_num = max(0,int(math.ceil(delta_x_left / float(_resolution)))), \ 139 | max(0,int(math.ceil(delta_x_right / float(_resolution)))) 140 | # print influence_right_num, influence_left_num, influence_down_num, influence_up_num 141 | for rr in range(row - influence_down_num, row + influence_up_num + 1): 142 | for cc in range(col - influence_left_num, col + influence_right_num + 1): 143 | if (-1 < rr < _num_h) and (-1 < cc < _num_w): 144 | if _classification[i] == 2: 145 | _output_dem_num[_num_h - rr - 1][cc] += 1 146 | output_arr_dem[_num_h - rr - 1][cc] += d_xyz[i][2] 147 | # output_arr_dem[_num_h - rr - 1][cc] = min(output_arr_dem[_num_h - rr - 1][cc],d_xyz[i][2]) 148 | output_arr_dsm[_num_h - row - 1][col] = max(output_arr_dsm[_num_h - row - 1][col], d_xyz[i][2]) 149 | # output_arr[_num_h - rr - 1][cc] = 2 150 | 151 | if __name__ == "__main__": 152 | # parameter handling 153 | parse = argparse.ArgumentParser() 154 | parse.add_argument("-i", help="Input las file.", required=True) 155 | parse.add_argument("-o", help="Output image file name (envi).", required=True) 156 | parse.add_argument("-dsm", help="Output dsm image file name (envi).") 157 | parse.add_argument("-chm", help="Output chm image file name (envi).") 158 | parse.add_argument("-fillholeofchm", help="Fill hole within corwn, specify a height difference.",type=float,default=3) 159 | parse.add_argument("-resolution", type=float, help="Resolution of final image.", required=True) 160 | parse.add_argument("-method", help="Method to estimate DEM value: min, max, mean.(No use now)", default="min") 161 | parse.add_argument("-box_radius", type=float, default=-1, 162 | help="Search radius used to determined height value of each pixel.") 163 | parse.add_argument("-fill_radius", help="Make point cloud denser by add points around each point.",type=float) 164 | parse.add_argument("-fill_num", help="How many points to fill around each point.", type=int, default=3) 165 | parse.add_argument("-seg_size", help="How many points for each core to run parallelly. ", type=int, default=500000) 166 | args = parse.parse_args() 167 | 168 | input_las_file = args.i 169 | output_img_file = args.o 170 | method = args.method 171 | resolution = args.resolution 172 | search_length = 2*args.resolution 173 | if args.box_radius != -1: 174 | search_length = args.box_radius*2 175 | has_dsm = False 176 | if args.dsm is not None: 177 | has_dsm = True 178 | 179 | start = time.clock() 180 | print("Reading data...") 181 | # read point cloud 182 | inFile = laspy.read(input_las_file) 183 | # x y z of each point 184 | xyz_total = np.vstack((inFile.x, inFile.y, inFile.z)).transpose() 185 | classification = inFile.classification 186 | geoTransform = (inFile.x.min(), resolution, 0, inFile.y.max(), 0, -resolution) 187 | if has_dsm: 188 | xyz = xyz_total 189 | else: 190 | xyz = xyz_total[classification == 2] 191 | point_number = len(xyz) 192 | print("Total points:", point_number) 193 | # offset: relative to the left and bottom corner. 194 | # computing the xy bounding box of the whole terrain, and number of cells according to resolution 195 | min_x, min_y, width, height, num_w, num_h = compute_bounding_box(inFile, resolution) 196 | # height value are no need to offset 197 | delta_xyz = xyz - np.array([min_x, min_y, 0]) 198 | # Radius fill 199 | if args.fill_radius is not None: 200 | print("Radius filling...: radius =", args.fill_radius) 201 | NUM = args.fill_num 202 | newXY = map(lambda x: [args.fill_radius*math.cos(x/float(NUM)*2*math.pi), 203 | args.fill_radius * math.sin(x / float(NUM) * 2*math.pi)], range(0, NUM)) 204 | rows,cols = delta_xyz.shape 205 | tmp_point = np.zeros((rows*NUM, cols)) 206 | tmp_classification = np.ones((rows*NUM,)) 207 | index = 0 208 | for i in range (0, rows): 209 | for xy in newXY: 210 | tmp_point[index] = np.array([xy[0] + delta_xyz[i][0], xy[1]+delta_xyz[i][1], delta_xyz[i][2]]) 211 | if classification[i] == 2: 212 | tmp_classification[index] = 2 213 | index += 1 214 | delta_xyz = np.vstack((delta_xyz, tmp_point)) 215 | classification = np.hstack((classification, tmp_classification)) 216 | 217 | print("Updated points: ", len(delta_xyz)) 218 | 219 | # delta_xy = xyz[:, 0:2] - np.array([min_x, min_y]) 220 | print("Start to calculate...") 221 | 222 | # prepare for parallel computing 223 | # segment the array into multiple segmentation by define a maximum size of each part 224 | seg_size = args.seg_size # 500000 points for each core, parallel 225 | seg_num = int(math.ceil(len(delta_xyz) / float(seg_size))) 226 | # define a memmap for output 227 | folder = tempfile.mkdtemp() 228 | dem_out_name = os.path.join(folder, 'dem') 229 | dem_out_num_name = os.path.join(folder, 'dem_num') 230 | dsm_out_name = os.path.join(folder, 'dsm') 231 | # this stores the final estimated DEM 232 | try: 233 | print("DEM size: ", "Width: ", num_w," Height: ", num_h) 234 | print("DEM resolution:", resolution) 235 | output_dem = np.memmap(dem_out_name, dtype=float, shape=(num_h, num_w), mode='w+') 236 | output_dem_num = np.memmap(dem_out_num_name, dtype=int, shape=(num_h, num_w), mode='w+') 237 | if has_dsm: 238 | output_dsm = np.memmap(dsm_out_name, dtype=float, shape=(num_h, num_w), mode='w+') 239 | joblib.Parallel(n_jobs=joblib.cpu_count(), max_nbytes=1e4)(joblib.delayed(sub_fun_dsm_dem)(delta_xyz, classification, output_dem, output_dem_num, output_dsm, i, 240 | num_w, num_h, resolution, search_length, seg_size, method) 241 | for i in range(0, seg_num)) 242 | interpolate(output_dsm) 243 | saveToHdr(output_dsm, args.dsm, geoTransform) 244 | else: 245 | joblib.Parallel(n_jobs=joblib.cpu_count(), max_nbytes=1e4)(joblib.delayed(sub_fun_dem)(delta_xyz, output_dem,output_dem_num, i, 246 | num_w, num_h, resolution, search_length, seg_size, 247 | method) 248 | for i in range(0, seg_num)) 249 | output_dem_num[output_dem_num == 0] = 1 250 | output_dem = output_dem / output_dem_num 251 | interpolate(output_dem) 252 | saveToHdr(output_dem, output_img_file,geoTransform) 253 | 254 | # chm 255 | if args.chm is not None and has_dsm: 256 | chm = output_dsm - output_dem 257 | chm[chm<0] = 0 258 | if args.fillholeofchm is not None: 259 | print("Filling holes with ") 260 | fillHoleofchm(chm, args.fillholeofchm) 261 | saveToHdr(chm, args.chm, geoTransform) 262 | if has_dsm: 263 | del output_dsm 264 | 265 | del output_dem 266 | del output_dem_num 267 | print("Done.") 268 | finally: 269 | try: 270 | shutil.rmtree(folder) 271 | except OSError: 272 | print("Failed to delete: " + folder) 273 | 274 | end = time.clock() 275 | print("Time: ", "%.3fs" % (end - start)) -------------------------------------------------------------------------------- /csfground.py: -------------------------------------------------------------------------------- 1 | """ 2 | Beare earth extraction 3 | Author: Jianbo Qi 4 | Date: 2017-1-15 5 | """ 6 | import argparse 7 | import laspy 8 | import CSF 9 | import numpy as np 10 | import time 11 | 12 | if __name__ == "__main__": 13 | parse = argparse.ArgumentParser() 14 | parse.add_argument("-i", help="Input las file.", required=True) 15 | parse.add_argument("-cloth_resolution", type=float, help="Cloth simulation grid resolution.",default=0.5) 16 | parse.add_argument("-bSlopeSmooth", type=bool, help="Handle steep slope after simulation.",default=True) 17 | parse.add_argument("-rigidness", type=int, help="Rigidness of cloth.", default=1) 18 | parse.add_argument("-classify_threshold", type=float, 19 | help="Distance used to classify point into ground and non-ground.",default=0.5) 20 | parse.add_argument("-o", help="Output las file of ground points.", required=True) 21 | parse.add_argument("-save_mode", help="Save only the ground part of the whole cloud: ground, non_ground, all", 22 | default="all") 23 | args = parse.parse_args() 24 | 25 | input_las_file = args.i 26 | output_las_file = args.o 27 | 28 | start = time.clock() 29 | 30 | csf = CSF.CSF() 31 | if args.cloth_resolution is not None: 32 | csf.params.cloth_resolution = args.cloth_resolution 33 | if args.bSlopeSmooth is not None: 34 | csf.params.bSlopeSmooth = args.bSlopeSmooth 35 | if args.rigidness is not None: 36 | csf.params.rigidness = args.rigidness 37 | if args.classify_threshold is not None: 38 | csf.params.class_threshold = args.classify_threshold 39 | 40 | input_File = laspy.read(input_las_file) 41 | xyz = np.vstack((input_File.x, input_File.y, input_File.z)).transpose() 42 | csf.setPointCloud(xyz) 43 | ground = CSF.VecInt() 44 | non_ground = CSF.VecInt() 45 | csf.do_filtering(ground, non_ground) 46 | points = input_File.points 47 | out_File = laspy.LasData(input_File.header) 48 | if args.save_mode == "ground": # save ground part 49 | ground_points = points[ground] 50 | out_File.points = ground_points 51 | classification = [2 for i in range(0, len(ground))] # 2 for ground 52 | out_File.classification = classification 53 | 54 | if args.save_mode == "non_ground": 55 | non_ground_points = points[non_ground] 56 | out_File.points = non_ground_points 57 | classification = [1 for i in range(0, len(non_ground))] # 1 for non-ground 58 | out_File.classification = classification 59 | 60 | if args.save_mode == "all": 61 | out_File.points = points 62 | classification = [1 for i in range(0, len(points))] # 1 for non-ground 63 | for i in range(0, len(ground)): 64 | classification[ground[i]] = 2 65 | for i in range(0, len(non_ground)): 66 | classification[non_ground[i]] = 1 67 | out_File.classification = classification 68 | 69 | out_File.write(output_las_file) 70 | 71 | end = time.clock() 72 | print("Done.") 73 | print("Time: ", "%.3fs" % (end - start)) -------------------------------------------------------------------------------- /csflai.py: -------------------------------------------------------------------------------- 1 | #coding: utf-8 2 | """ 3 | Estimate LAI from discrete Lidar 4 | Implementation of "Forest Leaf Area Index (LAI) Estimation Using Airborne Discrete‐Return Lidar Data" 5 | This method has three options for LAI inversion: uncorrected echo intensity, corrected echo intensity, echo counts 6 | Author: Jianbo Qi 7 | Date: 2017-4-25 8 | """ 9 | import argparse 10 | import time 11 | import laspy 12 | import numpy as np 13 | import math 14 | import tempfile 15 | import os 16 | import joblib 17 | from utils import saveToHdr 18 | 19 | 20 | def compute_bounding_box(input_File, cell_resolution): 21 | mins = input_File.header.min 22 | maxs = input_File.header.max 23 | # add a small value to be more convenient to do gridding 24 | width = maxs[0] - mins[0] + 0.00001 25 | height = maxs[1] - mins[1] + 0.00001 26 | min_x, min_y = mins[0], mins[1] 27 | num_w = int(math.ceil(width / float(cell_resolution))) 28 | num_h = int(math.ceil(height / float(cell_resolution))) 29 | return min_x, min_y, width, height, num_w, num_h 30 | 31 | def transmittance2lai_simple(arr): 32 | # assuming the LAD is spherical, and ingore the scanning angle 33 | G = 0.5 34 | LAI = -np.log(arr)/G 35 | return LAI 36 | 37 | def sub_fun_EC_lai(_delta_xyz, _classification, _output_ground_echo, _output_vegetation_echo, _seg_index, _num_h, _resolution, _seg_size): 38 | lower = _seg_index * _seg_size 39 | upper = min((_seg_index + 1) * _seg_size, len(_delta_xyz)) 40 | print("Processing from: ", lower, " to ", upper) 41 | for i in range(lower, upper): 42 | row = int(_delta_xyz[i][1] / _resolution) 43 | col = int(_delta_xyz[i][0] / _resolution) 44 | if _classification[i] == 0: 45 | _output_ground_echo[_num_h-row-1][col] += 1 46 | if _classification[i] == 1: 47 | _output_vegetation_echo[_num_h-row-1][col] += 1 48 | 49 | def sub_fun_UEI_lai(_delta_xyz, _classification, _intensity, _output_ground_echo, _output_vegetation_echo, _seg_index, _num_h, _resolution, _seg_size): 50 | lower = _seg_index * _seg_size 51 | upper = min((_seg_index + 1) * _seg_size, len(_delta_xyz)) 52 | print("Processing from: ", lower, " to ", upper) 53 | for i in range(lower, upper): 54 | row = int(_delta_xyz[i][1] / _resolution) 55 | col = int(_delta_xyz[i][0] / _resolution) 56 | if _classification[i] == 0: 57 | _output_ground_echo[_num_h-row-1][col] += _intensity[i] 58 | if _classification[i] == 1: 59 | _output_vegetation_echo[_num_h-row-1][col] += _intensity[i] 60 | 61 | def sub_fun_CEI_lai(_delta_xyz, _classification, _intensity, _scanning_angle, _avgHeight, _output_ground_echo, _output_vegetation_echo, _seg_index, _num_h, _resolution, _seg_size): 62 | lower = _seg_index * _seg_size 63 | upper = min((_seg_index + 1) * _seg_size, len(_delta_xyz)) 64 | print("Processing from: ", lower, " to ", upper) 65 | for i in range(lower, upper): 66 | row = int(_delta_xyz[i][1] / _resolution) 67 | col = int(_delta_xyz[i][0] / _resolution) 68 | # normalize intensity 69 | R = (_avgHeight - _delta_xyz[i][2])/math.cos(_scanning_angle[i]/180.0*math.pi) 70 | n_intensity = _intensity[i]*R*R/float(_avgHeight*_avgHeight) 71 | if _classification[i] == 0: 72 | _output_ground_echo[_num_h-row-1][col] += n_intensity 73 | if _classification[i] == 1: 74 | _output_vegetation_echo[_num_h-row-1][col] += n_intensity 75 | 76 | 77 | 78 | if __name__ == "__main__": 79 | # parameter handling 80 | parse = argparse.ArgumentParser() 81 | parse.add_argument("-i", help="Input las file, must be output of csfclassify.py.", required=True) 82 | parse.add_argument("-o", help="LAI product.", required=True) 83 | parse.add_argument("-resolution", help="Resolution of LAI.", required=True,type=float, default=10) 84 | parse.add_argument("-method", help="Inversion method: UEI, CEI, EC", required=True, default="EC") 85 | parse.add_argument("-avgFlightHeight", help="Average Flight Height (altitude, m)",type=float) 86 | parse.add_argument("-originlas", help="Original Las file.") 87 | parse.add_argument("-seg_size", help="How many points for each core to run parallelly. ", type=int, default=500000) 88 | args = parse.parse_args() 89 | 90 | input_las_file = args.i 91 | lai_out_file = args.o 92 | resolution = args.resolution 93 | method = args.method 94 | if method == "CEI": 95 | if args.avgFlightHeight is None: 96 | print("Please input the average flight height.") 97 | import sys 98 | sys.exit(0) 99 | if args.originlas is None: 100 | print("Please input the original las file.") 101 | import sys 102 | sys.exit(0) 103 | 104 | start = time.clock() 105 | print("Method:", method) 106 | print("Reading data...") 107 | # read point cloud 108 | inFile = laspy.read(input_las_file) 109 | # x y z of each point 110 | if method == "CEI": # CEI method need original Z value 111 | oinFile = laspy.file.File(args.originlas, mode='r') 112 | xyz_total = np.vstack((inFile.x, inFile.y, oinFile.z)).transpose() 113 | else: 114 | xyz_total = np.vstack((inFile.x, inFile.y, inFile.z)).transpose() 115 | 116 | classification = inFile.classification 117 | geoTransform = (inFile.x.min(), resolution, 0, inFile.y.max(), 0, -resolution) 118 | point_number = len(xyz_total) 119 | print("Total points:", point_number) 120 | 121 | min_x, min_y, width, height, num_w, num_h = compute_bounding_box(inFile, resolution) 122 | print("LAI product size: ", "Width: ", num_w, " Height: ", num_h) 123 | # height value are no need to offset 124 | delta_xyz = xyz_total - np.array([min_x, min_y, 0]) 125 | # prepare for parallel computing 126 | # segment the array into multiple segmentation by define a maximum size of each part 127 | seg_size = args.seg_size # 500000 points for each core, parallel 128 | seg_num = int(math.ceil(point_number / float(seg_size))) 129 | 130 | # define a memmap for output 131 | folder = tempfile.mkdtemp() 132 | output_ground_echo = np.memmap(os.path.join(folder, 'ground'), dtype=int, shape=(num_h, num_w), mode='w+') 133 | output_vegetation_echo = np.memmap(os.path.join(folder, 'vegetation'), dtype=int, shape=(num_h, num_w), mode='w+') 134 | 135 | if method == "EC": 136 | joblib.Parallel(n_jobs=joblib.cpu_count(), max_nbytes=1e4)( 137 | joblib.delayed(sub_fun_EC_lai)(delta_xyz,classification, output_ground_echo,output_vegetation_echo , i, num_h, resolution, seg_size) 138 | for i in range(0, seg_num)) 139 | 140 | if method == "UEI": 141 | echo_intensity = inFile.intensity 142 | joblib.Parallel(n_jobs=joblib.cpu_count(), max_nbytes=1e4)( 143 | joblib.delayed(sub_fun_UEI_lai)(delta_xyz, classification, echo_intensity.astype(int), output_ground_echo, output_vegetation_echo, i, num_h, resolution, seg_size) 144 | for i in range(0, seg_num)) 145 | 146 | if method == "CEI": 147 | avgHeight = args.avgFlightHeight 148 | echo_intensity = inFile.intensity 149 | scanning_angle = inFile.scan_angle_rank 150 | joblib.Parallel(n_jobs=joblib.cpu_count(), max_nbytes=1e4)( 151 | joblib.delayed(sub_fun_CEI_lai)(delta_xyz, classification, echo_intensity.astype(int),scanning_angle.astype(int),avgHeight, output_ground_echo, output_vegetation_echo, i, num_h, resolution, seg_size) 152 | for i in range(0, seg_num)) 153 | 154 | if method == "CEI": 155 | transmittance = output_ground_echo / (output_ground_echo + 0.5*output_vegetation_echo).astype(float) 156 | else: 157 | transmittance = output_ground_echo/(output_ground_echo+output_vegetation_echo).astype(float) 158 | transmittance[transmittance==0] = 0.5 159 | LAI = transmittance2lai_simple(transmittance) 160 | saveToHdr(LAI, lai_out_file, geoTransform) 161 | del output_ground_echo 162 | del output_vegetation_echo 163 | print("Done.") 164 | try: 165 | import shutil 166 | shutil.rmtree(folder) 167 | except OSError: 168 | print("Failed to delete: " + folder) 169 | 170 | end = time.clock() 171 | print("Time: ", "%.3fs" % (end - start)) 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | -------------------------------------------------------------------------------- /csfnormalize.py: -------------------------------------------------------------------------------- 1 | """ 2 | Normalize point cloud according to DEM 3 | Author: Jianbo Qi 4 | Date: 2017-4-25 5 | """ 6 | import argparse 7 | import laspy 8 | import math 9 | import time 10 | import numpy as np 11 | import joblib 12 | import tempfile 13 | import os 14 | import shutil 15 | from utils import read_file_to_arr 16 | 17 | 18 | 19 | 20 | def sub_fun_height(d_xyz, _zarr, dem_data, _seg_index, _num_w, _num_h, _resolution, _seg_size): 21 | # corresponding interval 22 | lower = _seg_index * _seg_size 23 | upper = min((_seg_index+1) * _seg_size, len(d_xyz)) 24 | print("Processing from: ", lower, " to ", upper) 25 | # For each point, find its corresponding cell 26 | for i in range(lower, upper): 27 | row = int(d_xyz[i][1] / _resolution) # row of the corresponding cell 28 | col = int(d_xyz[i][0] / _resolution) 29 | _zarr[i] = d_xyz[i][2] - dem_data[_num_h-row-1][col] 30 | if _zarr[i] < 0: 31 | _zarr[i] = 0 32 | 33 | if __name__ == "__main__": 34 | # parameter handling 35 | parse = argparse.ArgumentParser() 36 | parse.add_argument("-i", help="Input las file.", required=True) 37 | parse.add_argument("-dem", help="Input DEM file.", required=True) 38 | parse.add_argument("-o", help="Output las file name (*.las).", required=True) 39 | parse.add_argument("-seg_size", help="How many points for each core to run parallelly. ", type=int, default=500000) 40 | args = parse.parse_args() 41 | 42 | input_las_file = args.i 43 | output_las_file = args.o 44 | dem_file = args.dem 45 | 46 | start = time.clock() 47 | print("Reading data...") 48 | # read point cloud 49 | inFile = laspy.read(input_las_file) 50 | # x y z of each point 51 | xyz_total = np.vstack((inFile.x, inFile.y, inFile.z)).transpose() 52 | point_number = len(xyz_total) 53 | print("Total points:", point_number) 54 | # offset: relative to the left and bottom corner. 55 | # computing the xy bounding box of the whole terrain, and number of cells according to resolution 56 | min_x, min_y = inFile.x.min(), inFile.y.min() 57 | # height value are no need to offset 58 | delta_xyz = xyz_total - np.array([min_x, min_y, 0]) 59 | 60 | # delta_xy = xyz[:, 0:2] - np.array([min_x, min_y]) 61 | print("Start to calculate...") 62 | 63 | # prepare for parallel computing 64 | # segment the array into multiple segmentation by define a maximum size of each part 65 | seg_size = args.seg_size # 500000 points for each core, parallel 66 | seg_num = int(math.ceil(point_number / float(seg_size))) 67 | # read DEM 68 | width, height, resolution, demdata = read_file_to_arr(dem_file) 69 | 70 | print("DEM size: ", "Width: ", width," Height: ", height) 71 | folder = tempfile.mkdtemp() 72 | z_out_name = os.path.join(folder, 'point_z') 73 | zarr = np.memmap(z_out_name, dtype=float, shape=(len(delta_xyz),), mode='w+') 74 | joblib.Parallel(n_jobs=joblib.cpu_count(), max_nbytes=1e4)(joblib.delayed(sub_fun_height)(delta_xyz, zarr, demdata, i, 75 | width, height, resolution, seg_size) 76 | for i in range(0, seg_num)) 77 | out_File = laspy.LasData(inFile.header) 78 | out_File.points = inFile.points 79 | out_File.z = zarr 80 | out_File.write(output_las_file) 81 | print("Done.") 82 | del zarr 83 | try: 84 | shutil.rmtree(folder) 85 | except OSError: 86 | print("Failed to delete: " + folder) 87 | end = time.clock() 88 | print("Time: ", "%.3fs" % (end - start)) -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Some common function, such as file reading and writing 3 | Author: Jianbo Qi 4 | Date: 2017-1-15 5 | """ 6 | from osgeo import gdal 7 | 8 | # writing arr to ENVI standard file format 9 | def saveToHdr(npArray, dstFilePath, geoTransform=""): 10 | dshape = npArray.shape 11 | bandnum = 1 12 | format = "ENVI" 13 | driver = gdal.GetDriverByName(format) 14 | dst_ds = driver.Create(dstFilePath, dshape[1], dshape[0], bandnum, gdal.GDT_Float32) 15 | if not geoTransform == "": 16 | dst_ds.SetGeoTransform(geoTransform) 17 | # npArray = linear_stretch_3d(npArray) 18 | dst_ds.GetRasterBand(1).WriteArray(npArray) 19 | dst_ds = None 20 | 21 | 22 | # reading image to area 23 | def read_file_to_arr(img_file): 24 | dataset = gdal.Open(img_file) 25 | band = dataset.GetRasterBand(1) 26 | geoTransform = dataset.GetGeoTransform() 27 | dataarr = band.ReadAsArray(0, 0, band.XSize, band.YSize) 28 | return band.XSize, band.YSize,geoTransform[1], dataarr 29 | 30 | 31 | # only reading the data array 32 | def read_img_to_arr_no_transform(img_file): 33 | data_set = gdal.Open(img_file) 34 | band = data_set.GetRasterBand(1) 35 | arr = band.ReadAsArray(0, 0, band.XSize, band.YSize) 36 | return arr --------------------------------------------------------------------------------