-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathTimeSIFT_scripts_auto.py
More file actions
366 lines (314 loc) · 17.7 KB
/
Copy pathTimeSIFT_scripts_auto.py
File metadata and controls
366 lines (314 loc) · 17.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
# This script is based on the following work :
# Fabrice Vinatier, & Denis Feurer. (2023). Time-SIFT module for Agisoft Metashape software. Zenodo. https://doi.org/10.5281/zenodo.8359983
# The code of the original plugin is available using the following DOI : 10.5281/zenodo.8359982
import os
from os import path
import re
import numpy as np
import Metashape as scan
import time
import argparse
import shutil
#scan.License().activate('your_license_key')
parser = argparse.ArgumentParser()
parser.add_argument('--crs', type=str, default="EPSG::32622")
parser.add_argument('--pathDIR', type=str)
parser.add_argument('--out_dir_ortho', type=str)
parser.add_argument('--out_dir_dem', type=str, default = None,)
parser.add_argument('--out_dir_project', type=str, default = None)
parser.add_argument('--resol_ref', type=float, default = 0.05)
parser.add_argument('--data_type', type=str, default = 'RGB')
parser.add_argument('--site_name', type=str, default = '')
parser.add_argument('--calibrate_col', default = True)
parser.add_argument('--sun_sensor', default = False)
parser.add_argument('--group_by_flight', default = False)
parser.add_argument('--downscale_factor_alignement', type=int, default = 1)
parser.add_argument('--downscale_factor_depth_map', type=int, default = 2)
args = parser.parse_args()
def str2bool(v):
"""
Converts string to bool. Ex : str2bool('True') = True
"""
if v is None or isinstance(v, bool):
return v
if v.lower()=='none':
return None
elif v.lower() in ('yes', 'true', 't', 'y', '1'):
return True
elif v.lower() in ('no', 'false', 'f', 'n', '0'):
return False
else:
raise argparse.ArgumentTypeError('Boolean value expected.')
def add_all_chunks(doc, pathDIR=None):
"""
Loads all RGB photos into the project, one chunk per subfolder in pathDIR
"""
print(pathDIR)
os.chdir(pathDIR)
epochs = os.listdir(pathDIR)
# we select only the non-empty subfolders
list_files = []
for (dirpath, dirnames, filenames) in os.walk(pathDIR):
list_files += [os.path.join(dirpath, file) for file in filenames]
ep_relative_paths = np.unique([os.path.relpath(os.path.dirname(file), start=pathDIR) for file in list_files])
print("ep paths : ", ep_relative_paths)
epochs = [os.path.basename(dir_path) for dir_path in ep_relative_paths]
print("epochs : ", epochs)
# We remove all existing chunks and add them one by one
for chk in doc.chunks:
doc.remove(chk)
for i in range(len(ep_relative_paths)):
ep_name, ep_path = epochs[i], ep_relative_paths[i]
add_TimeSIFT_chunk(doc, ep_path = ep_path, epoch_name = ep_name)
def add_TimeSIFT_chunk(doc, ep_path="", epoch_name=""):
"""
Adds a single RGB chunk to the project
"""
if len([chk for chk in doc.chunks if re.search(epoch_name,chk.label) is not None])==0:
doc.addChunk()
chunk=doc.chunks[len(doc.chunks)-1]
chunk.label=epoch_name
[f for f in os.listdir(ep_path) if os.path.isfile(os.path.join(ep_path, f))]
dirName=ep_path
listOfFiles = list()
for (dirpath, dirnames, filenames) in os.walk(dirName):
listOfFiles += [os.path.join(dirpath, file) for file in filenames]
chunk.addPhotos(listOfFiles)
for cam in chunk.cameras:
cam.label = (str(chunk.label) + "_EPOCH_" + cam.label)
def merge_chunk_TimeSIFT(doc):
"""
Merges all chunks into a single one
"""
start_time = time.time()
for chk_sel in doc.chunks:
chunk_non_aligned = [chk.key for chk in doc.chunks if re.search("TimeSIFT", chk.label) is not None]
chunk_non_aligned.append(chk_sel.key)
doc.mergeChunks(chunks=chunk_non_aligned)
TS_chunk = [chk for chk in doc.chunks if re.search("TimeSIFT", chk.label) is not None]
doc.remove(TS_chunk)
merged_chunk = doc.chunks[-1]
merged_chunk.label = "TimeSIFT"
doc.remove(chk_sel)
for cam in [i for i in merged_chunk.cameras if re.search(chk_sel.label,i.label) is not None]:
cam.transform = None
print("Temps écoulé pour la fusion : ", time.time() - start_time)
def align_TimeSIFT_chunk(doc, downscale_factor = 1):
"""
Aligns all cameras in the merged chunk
"""
start_time = time.time()
TS_chunk=[chk for chk in doc.chunks if re.search("TimeSIFT",chk.label) is not None][0]
TS_chunk.matchPhotos(downscale=downscale_factor, generic_preselection=True, reference_preselection=True,
reference_preselection_mode=scan.ReferencePreselectionSource, keypoint_limit=100000,
tiepoint_limit=10000,keep_keypoints=False)
# boucle pour ré-aligner les photos non-alignées
nb_aligned_before = 0
nb_aligned_after = 100
while nb_aligned_after != nb_aligned_before:
nb_aligned_before = len([i for i in TS_chunk.cameras if i.transform])
TS_chunk.matchPhotos(downscale=downscale_factor, generic_preselection=True, reference_preselection=True,
reference_preselection_mode=scan.ReferencePreselectionSource, keypoint_limit=200000,
tiepoint_limit=20000,keep_keypoints=False)
TS_chunk.alignCameras()
nb_aligned_after = len([i for i in TS_chunk.cameras if i.transform])
aligned_cameras = [i for i in TS_chunk.cameras if i.transform]
print("------",str(len(aligned_cameras)),"cameras aligned out of",len(TS_chunk.cameras),"----")
TS_chunk.resetRegion()
TS_chunk.optimizeCameras()
TS_chunk.updateTransform()
print("Temps écoulé pour l'alignement : ", time.time() - start_time)
def split_TimeSIFT_chunk(doc, group_by_flight = False):
"""
After the alignement, splits the merged chunk into smaller chunks, each representing a date (default) or a flight
Parameters:
group_by_flight (bool):
If True, regroups data by flight. Else, regroup it by date (default)
"""
TS_chunk = [chk for chk in doc.chunks if (re.search("TimeSIFT", chk.label) is not None)][0]
if group_by_flight:
TS_chunk_names=np.unique([cam.label.split("_EPOCH_")[0] for cam in TS_chunk.cameras])
else:
#searching for a date in YYYYMMDD or YYYYMM format in cameras_names to do the slpit
pattern = r'\d{4}(0[1-9]|1[0-2])([0-2][0-9]|3[01])?'
TS_chunk_names=np.unique([re.search(pattern, cam.label).group() if re.search(pattern, cam.label) else cam.label for cam in TS_chunk.cameras]) #r'\d+'
print(TS_chunk_names)
for chk_name in TS_chunk_names:
if len([chk for chk in doc.chunks if re.search(chk_name,chk.label) is not None])==0:
NewChunk = TS_chunk.copy()
NewChunk.label=chk_name
#pattern = str(chk_name) + "_EPOCH_"
pattern = str(chk_name)
t = [pattern in cam.label for cam in NewChunk.cameras]
list_cameras = [NewChunk.cameras[i] for i, x in enumerate(t) if not x]
NewChunk.remove(list_cameras)
def merge_chunk_with_same_date(doc):
"""
Not useful anymore. Merging by date now done in split_TIMESift_chunk
"""
dates = []
Non_ts_chunks = [chk for chk in doc.chunks if re.search("TimeSIFT",chk.label) is None]
for chk in Non_ts_chunks:
chunk_date = chk.label[:8]
if chunk_date not in dates :
dates.append(chunk_date)
print("Dates : ", dates)
for date in dates:
chunks_to_merge = [chk.key for chk in Non_ts_chunks if chk.label[:8]==date]
doc.mergeChunks(chunks=chunks_to_merge)
merged_chunk = doc.chunks[-1]
merged_chunk.label = date
for chk in [chk for chk in Non_ts_chunks if chk.label[:8]==date]:
doc.remove(chk)
def process_splited_TimeSIFT_chunks_one_by_one(doc, out_dir_ortho = None, out_dir_DEM = None, site_name="", resol_ref = None, crs = None, downscale_factor_depth_map = 2):
"""
Generate depth map, dense cloud, DEM and orthomosaic for one image. Always saves orthomosaic and saves DEM if specified
Parameters:
out_dir_ortho (str):
Folder where the orthomosaics are saved
out_dir_DEM (str, optional):
Folder where the DEMs are saved. If no path is specified, the DEMs are not saved by default
site_name (str, optional):
Adds the data site name into the names of all created folders and files, to better separate generated data from different projects. If not specified, the names will stay generic
"""
TS_chunks = [chk for chk in doc.chunks if (re.search("TimeSIFT", chk.label) is None)]
TS_chunks = [chk for chk in TS_chunks if chk.enabled]
for chk in TS_chunks:
start_time = time.time()
NewChunk=chk
NewChunk.buildDepthMaps(downscale=downscale_factor_depth_map, filter_mode=scan.AggressiveFiltering)
t_depth_maps = time.time()
print(f"Time to build depth map : {t_depth_maps - start_time} seconds")
NewChunk.buildPointCloud(point_colors=True)
t_dense_cloud = time.time()
print(f"Time to build dense cloud : {t_dense_cloud - t_depth_maps} seconds")
NewChunk.buildDem(source_data=scan.PointCloudData,resolution=resol_ref)
t_DEM = time.time()
print(f"Time to build DEM : {t_DEM - t_dense_cloud} seconds")
NewChunk.buildOrthomosaic(surface_data=scan.ElevationData,resolution=resol_ref)
t_ortho = time.time()
print(f"Time to build ortho : {t_ortho - t_DEM} seconds")
print(f"Total process time for the image : {t_ortho - start_time} seconds")
proj = scan.OrthoProjection()
proj.type=scan.OrthoProjection.Type.Planar
proj.crs=scan.CoordinateSystem(crs)
img_compress=scan.ImageCompression(tiff_compression = scan.ImageCompression.TiffCompressionLZW, tiff_big = True)
img_compress.tiff_big = True #apparently necessary, maybe because of ImageCompression init that doesn't deal with bigTiff ?
doc.save(os.path.join(out_dir_ortho, '_temp_.psx'))
try :
NewChunk.exportRaster(os.path.join(out_dir_ortho, f"{str(NewChunk.label)}{site_name}_ORTHO.tif"),source_data=scan.OrthomosaicData, image_format=scan.ImageFormatTIFF,
projection=proj, resolution=resol_ref,clip_to_boundary=True,save_alpha=False, split_in_blocks = False, image_compression = img_compress)
# if the raster file is too big and bigTiff doesn't work for some reason, it will be divided into 10000*10000 blocks
except:
os.remove(os.path.join(out_dir_ortho, f"{str(NewChunk.label)}{site_name}_ORTHO.tif"))
NewChunk.exportRaster(os.path.join(out_dir_ortho, f"{str(NewChunk.label)}{site_name}_ORTHO.tif"),source_data=scan.OrthomosaicData, image_format=scan.ImageFormatTIFF,
projection=proj, resolution=resol_ref,clip_to_boundary=True,save_alpha=False, split_in_blocks = True, block_width=10000, block_height=10000, image_compression=img_compress)
if out_dir_DEM is not None:
NewChunk.exportRaster(os.path.join(out_dir_DEM, f"{str(NewChunk.label)}{site_name}_DEM.tif"),source_data=scan.ElevationData, image_format=scan.ImageFormatTIFF, image_compression = img_compress,
projection=proj, resolution=resol_ref,clip_to_boundary=True, save_alpha=False)
#TODO : confirm whether or not DEMs and project are to be saved by default
def Time_SIFT_process(pathDIR,
out_dir_ortho,
out_dir_DEM=None,
out_dir_project=None,
data_type="RGB",
resol_ref=0.05,
crs="EPSG::32622",
site_name = "",
calibrate_col = True,
sun_sensor = False,
group_by_flight = False,
downscale_factor_alignement = 1,
downscale_factor_depth_map = 2,
):
"""
Executes the complete Time_SIFT process : Loads all photos from the input folder and its subdirectories into a Metashape project . These photos will then be merged, aligned,
before the orthomosaic and (optionally) DEMs will be generated for each date (or for each flight)
:param str pathDIR: Path to the folder where the data is located. Inside this folder, there should be one subfolder per flight, with the date of the flight specified in the folder name in the YYYYMMDD or YYYYMM format.
:param str out_dir_ortho: Folder where the orthomosaics are saved.
:param str out_dir_DEM: Folder where the DEMs are saved. If no path is specified, the DEMs are not saved by default.
:param str out_dir_project: Folder where the Metashape project is saved. If no path is specified, the project is not saved by default.
:param str data_type: The type of the data used. Either 'RGB' (default) or 'MS' (for multispectral images).
:param float resol_ref: The resolution (in meters) used to generate DEMs and orthomosaics. Defaults to 0.05.
:param str crs: Coordinate system used, in a string format. Example: crs="EPSG::32622" (default value).
:param str site_name: Adds the data site name into the names of all created folders and files, to better separate generated data from different projects. If not specified, the names will stay generic.
:param bool calibrate_col: Whether or not to apply white balance. Defaults to True.
:param bool sun_sensor: Whether or not to calibrate the reflectance using the sun sensor. Only applies to multispectral images. Defaults to False.
:param bool group_by_flight: If True, regroups data by flight. Else, regroup it by date (default).
:param int downscale_factor_alignement: Alignment accuracy (0 - Highest, 1 - High, 2 - Medium, 4 - Low, 8 - Lowest). Defaults to 1.
:param int downscale_factor_depth_map: Depth map quality (1 - Ultra high, 2 - High, 4 - Medium, 8 - Low, 16 - Lowest). Defaults to 2.
:return: None
"""
doc = scan.Document()
assert data_type in ["RGB", "MS"]
#for file naming purposes
if site_name != "":
site_name = "_" + site_name
calibrate_col = str2bool(calibrate_col)
sun_sensor = str2bool(sun_sensor)
group_by_flight = str2bool(group_by_flight)
if not os.path.exists(out_dir_ortho):
os.mkdir(out_dir_ortho)
if out_dir_DEM is not None:
if out_dir_DEM == "" :
out_dir_DEM = os.path.join(os.path.dirname(out_dir_ortho), "DEM")
if not os.path.exists(out_dir_DEM):
os.mkdir(out_dir_DEM)
#TODO : store all times into log file, or add progress bars
start_time = time.time()
if data_type == "RGB" or data_type == "MS":
add_all_chunks(doc, pathDIR = pathDIR)
merge_chunk_TimeSIFT(doc)
t_add_data = time.time()
print(f"Temps écoulé pour le chargement et la fusion des photos : {t_add_data - start_time} seconds")
"""
elif data_type == "MS" :
add_all_MS_photos(doc, pathDIR = pathDIR)
t_add_data = time.time()
print("Temps écoulé pour le chargement des photos : ", t_add_data - start_time)
"""
if sun_sensor and data_type=='MS':
TS_chunk = [chk for chk in doc.chunks if (re.search("TimeSIFT", chk.label) is not None)][0]
TS_chunk.calibrateReflectance(use_sun_sensor=True)
align_TimeSIFT_chunk(doc, downscale_factor = downscale_factor_alignement)
t_align = time.time()
print(f"Temps écoulé pour l'alignement : {t_align - t_add_data} seconds")
#The project needs to be saved before building DEMs and orthomosaics
doc.save(os.path.join(out_dir_ortho, '_temp_.psx'))
#Color calibration
if calibrate_col and (data_type=='RGB' or data_type=='MS'):
TS_chunk = [chk for chk in doc.chunks if (re.search("TimeSIFT", chk.label) is not None)][0]
TS_chunk.calibrateColors(scan.TiePointsData, white_balance=True)
doc.save(os.path.join(out_dir_ortho, '_temp_.psx'))
split_TimeSIFT_chunk(doc, group_by_flight = group_by_flight)
#merge_chunk_with_same_date(doc)
t_split = time.time()
#print("Temps écoulé pour la division et regroupement par date : ", t_split - t_align)
process_splited_TimeSIFT_chunks_one_by_one(doc, out_dir_ortho = out_dir_ortho, out_dir_DEM = out_dir_DEM, site_name = site_name, resol_ref = resol_ref, crs = crs, downscale_factor_depth_map=downscale_factor_depth_map)
print(f"Temps écoulé pour le process final : {time.time() - t_split} seconds")
print(f"Temps écoulé pour la pipeline complète : {time.time() - start_time} seconds")
doc.save(os.path.join(out_dir_ortho, '_temp_.psx'))
if out_dir_project is not None :
if out_dir_project == "" :
out_dir_project = out_dir_ortho
if not os.path.exists(out_dir_project):
os.mkdir(out_dir_project)
doc.save(os.path.join(out_dir_project, f"Metashape_Project_{site_name}.psx"))
os.remove(os.path.join(out_dir_ortho, '_temp_.psx'))
shutil.rmtree(os.path.join(out_dir_ortho, '_temp_.files'))
if __name__ == '__main__':
print("args : ", args)
Time_SIFT_process(pathDIR = args.pathDIR,
out_dir_ortho = args.out_dir_ortho,
out_dir_DEM = args.out_dir_dem,
out_dir_project = args.out_dir_project,
data_type = args.data_type,
resol_ref = args.resol_ref,
crs = args.crs,
site_name = args.site_name,
calibrate_col = args.calibrate_col,
sun_sensor = args.sun_sensor,
group_by_flight = args.group_by_flight,
downscale_factor_alignement = args.downscale_factor_alignement,
downscale_factor_depth_map = args.downscale_factor_depth_map
)