-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathdashcam2josm.py
441 lines (407 loc) · 21.6 KB
/
dashcam2josm.py
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
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
#
# dashcam2josm.py
# Version: 2023-09-16
# License: GPL3
#
#
# This dashcam2josm.py script is based on dashcam2josm.sh script from https://retiredtechie.fitchfamily.org/2018/05/13/dashcam-openstreetmap-mapping/
#
#
# This script generates geotaged jpg images from .MP4 video file recorded by Viofo dashcam
#
# Tested on files from Viofo A129 DUO dashcam (Novatek NTK96663 chip) (front and rear cams) and Mapillary script uploader.
#
# This script requires:
# - nvtk_mp42gpx.py script
# - ffmpeg tool (tested on version git-2020-07-13-7772666 from https://ffmpeg.org)
# - exiftool tool (tested on version 12.01 from https://exiftool.org)
#
# Tested on Linux and Windows 10
#
#
# Options:
#
# -i input .MP4 video file(s), globs (eg: *) or directory(ies)
#
# -c Crop images generated from all video files. Format: width:height:x:y
# -cf Crop images generated from front video files. (For *F.MP4 files.) Format: width:height:x:y
# -cr Crop images generated from rear video files. (For *R.MP4 files.) Format: width:height:x:y
# This options are useful if you don't want to share your sensitive data saved in the video by dashcam, or if your view is partially obscured.
# Caution: width can not be a multiply of height. It causes bugs in Mapillary. See: https://forum.mapillary.com/t/bug-report-i-uploaded-this-normal-pic-but-it-is-showed-as-an-360/2948/7
#
# -a Arrange output .jpg files from many input .mp4 files into one output folder per continuous camera sequence
#
# -f Do not skip frames not far enaugh (5m) from previous saved
# By default this script skips images with are too close (less than 5 meters) from previous saved image.
# This avoid generating many images from same position (in a traffic jam or at traffic lights)
#
# -df User provided directory with ffmpeg tool.
# -de User provided directory with exiftool tool.
#
# -d Deobfuscates coordinates. If the file only works with JMSPlayer use this flag.
# (param directly pass to nvtk_mp42gpx.py script)
# -c Specify on what to sort by.
# The -s f will sort the output by the file name.
# The -s d will sort the output by the GPS date (default).
# The -s n will not sort the output.
# (param directly pass to nvtk_mp42gpx.py script)
#
# -ne Do not exclude outliers. By default script removes impossible coordinates (too far from each other) due to errors in the GPS data.
# -nd Do not exclude duplicates. By default script merges duplicate track points (with same timestamp) into one, due to errors in the GPS data.
#
#
#
# Cautions:
#
# Output files (.jpg files and one .gpx file) are created to subdirectory named like .MP4 source file
#
# Images are generated every one second during video, but only if gps position is known.
#
# Images generated by rear cam (*R.MP4 files.) has bearing corrected by 180 degrees and moved 2 meters behind.
# It is useful at Mapillary app for separate two identical tracks (one from front and one from rear dashcam)
# (Remember to upload front and back series of images as separate Mapillary tracks.)
#
#
#
#
# Oryginal dashcam2josm.sh script description:
#
# Author: Tod Fitch (tod at fitchfamily dot org)
# License: GPL3
# Warranty: NONE! Use at your own risk!
# Disclaimer: Quick and dirty hack.
# Description: This script extract geo referenced images from
# Novatek generated MP4 files.
#
# Uses a python script to extract GPS data from the Viofo A119
# Script is available at:
# https://sergei.nz/extracting-gps-data-from-viofo-a119-and-other-novatek-powered-cameras/
#
# Also uses ffmpeg and exiftool.
#
# Output is to a subdirectory with a name based on the base file name for
# the video file.
#
import os, sys, argparse, glob, shutil, subprocess, datetime, calendar, locale
import xml.etree.ElementTree as ET
# bearing correction for images from rear camera [deg] (*R.MP4 files)
bearingCorrectionForRearCam = '180'
filesCount = 0;
def check_in_files(in_file):
in_files = []
for f in in_file:
# glob needed if for some reason quoted glob is passed, or script is run on the most popular proprietary inferior OS
for f1 in glob.glob(f):
if os.path.isdir(f1):
print("Skipping subdirectory '%s'" % f1)
elif os.path.isfile(f1):
if f1.upper().endswith(".MP4"):
print("Queueing file '%s' for processing..." % f1)
in_files.append(f1)
else:
print("File %s omitted. File name must end with .MP4" % f1)
else:
# Catch all for typos...
print("Skipping invalid input '%s'..." % f1)
global filesCount
filesCount = len(in_files)
print("Queueing total: '%s' files" % filesCount)
return in_files
def get_args():
parser = argparse.ArgumentParser(description='This script will attempt to extract geotaged images and GPS data from Novatek MP4 video files')
parser.add_argument('-i', metavar='input', nargs='+', help='input file(s), globs (eg: *) or directory(ies)')
parser.add_argument('-c', metavar='crop', help='Crop images generated from video files. Format: width:height:x:y')
parser.add_argument('-cf', metavar='cropFront', help='Crop images generated from front video file. Format: width:height:x:y')
parser.add_argument('-cr', metavar='cropRear', help='Crop images generated from rear video file. Format: width:height:x:y')
parser.add_argument('-a', action='store_true', help=' Arrange output .jpg files from many input .mp4 files into one output folder per continuous camera sequence')
parser.add_argument('-f', action='store_true', help='Do not skip frames not far enaugh (5m) from previous saved')
parser.add_argument('-df', metavar='ffmpegUserDir', help='User provided directory with ffmpeg tool. https://ffmpeg.org')
parser.add_argument('-de', metavar='exiftoolUserDir', help='User provided directory with exiftool tool. https://exiftool.org')
parser.add_argument('-d', action='store_true', help='Deobfuscates coordinates. If the file only works with JMSPlayer use this flag.')
parser.add_argument('-s', metavar='sorting', nargs='?', default='d', help=('Specify on what to sort by. '
'The \'-s f\' will sort the output by the file name. '
'The \'-s d\' will sort the output by the GPS date (default). '
'The \'-s n\' will not sort the output.'))
parser.add_argument('-ne', action='store_true', help='Do not exclude outliers. By default script removes impossible coordinates (too far from each other) due to errors in the GPS data.')
parser.add_argument('-nd', action='store_true', help='Do not exclude duplicates. By default script removes duplicate track points (with same timestamp) due to errors in the GPS data.')
args = parser.parse_args(sys.argv[1:])
crop = check_crop("c", args.c)
cropFront = check_crop("cf", args.cf)
cropRear = check_crop("cr", args.cr)
ffmpegUserDir = args.df
exiftoolUserDir = args.de
deobfuscate = args.d
sort_by = args.s
sort_flags = {
'd' : 'Sort coordinates by the GPS Date',
'f' : 'Sort coordinates by the input file name.',
'n' : 'Do not sort coordinates.',
}
if sort_by not in sort_flags.keys():
print("ERROR: unsupported sort flag '%s' (supported flags: %s)." % (sort_by, sort_flags))
parser.print_help()
sys.exit(1)
not_del_outliers = args.ne
not_del_duplicates = args.nd
arrange = args.a
try:
doNotSkip = args.f
in_files = check_in_files(args.i)
except:
print("Unexpected error:", sys.exc_info()[0])
parser.print_help()
sys.exit(1)
return (in_files, crop, cropFront, cropRear, arrange, doNotSkip, ffmpegUserDir, exiftoolUserDir, deobfuscate, sort_by, not_del_outliers, not_del_duplicates)
def check_crop(cropParam, cropStr):
if (not cropStr):
return cropStr
cropStrList = cropStr.split(":")
if (len(cropStrList) != 4):
print_error("Wrong format of crop (-" + cropParam + ") parameter - reqired format: width:height:x:y")
sys.exit(1)
w = cropStrList[0]
h = cropStrList[1]
x = cropStrList[2]
y = cropStrList[3]
if (not w.isdigit()):
print_error("First part (width) of crop (-" + cropParam + ") parameter must be positive number")
sys.exit(1)
if (not h.isdigit()):
print_error("Second part (height) of crop (-" + cropParam + ") parameter must be positive number")
sys.exit(1)
if (not x.isdigit()):
print_error("Third part (x) of crop (-" + cropParam + ") parameter must be positive number")
sys.exit(1)
if (not y.isdigit()):
print_error("Fourth part (y) of crop (-" + cropParam + ") parameter must be positive number")
sys.exit(1)
if ((int(w)-int(x))%(int(h)-int(y)) == 0):
print_error("In parameter -" + cropParam + " (Output images size), width is exactly multiply of height. It couses bugs in Mapillary. Please change this size. See: https://forum.mapillary.com/t/bug-report-i-uploaded-this-normal-pic-but-it-is-showed-as-an-360/2948/7")
sys.exit(1)
return cropStr
def print_error(errorStr):
print("===============================")
print("ERROR:")
print("%s" % errorStr)
print("===============================")
return
def create_subDir(subDirPath, subDirGpx, subDirMp4, sourceMp4):
if not os.path.isdir(subDirPath):
os.mkdir(subDirPath)
# remove *.jpg name.MP4 and name.gpx files from subdirectory
subDirJpgs = glob.glob(subDirPath+'/*.jpg')
for subDirJpg in subDirJpgs:
print("File %s removed." % subDirJpg)
os.remove(subDirJpg)
if os.path.exists(subDirGpx):
os.remove(subDirGpx)
if os.path.exists(subDirMp4):
os.remove(subDirMp4)
# copy source MP4 to subdir
shutil.copy(sourceMp4, subDirPath)
return
def create_gpx(name, subDirMp4, currentScriptDir, deobfuscate, sort_by, not_del_outliers, not_del_duplicates):
# run nvtk_mp42gpx.py script
print("START run script. %s " % os.path.join(currentScriptDir, 'nvtk_mp42gpx.py'))
command = ['python', os.path.join(currentScriptDir, 'nvtk_mp42gpx.py')]
command.append('-i')
command.append(subDirMp4)
command.append('-f')
command.append('-m')
command.append('-s')
command.append(sort_by)
if deobfuscate:
command.append('-d')
if name.upper().endswith("R"): # *R.MP4 files from rear camera (course rotated 180 deg and gps position moved 2m behind)
command.append('-b')
command.append(bearingCorrectionForRearCam)
if not not_del_outliers:
command.append('-e')
if not not_del_duplicates:
command.append('-u')
process = subprocess.Popen(command, universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
for line in iter(process.stdout.readline, ""):
sys.stdout.write(" >" + line)
sys.stdout.flush()
process.wait()
exitCode = process.returncode
print("END run script. out = %s" % exitCode)
if exitCode == 0:
return True
else:
return False
def utc_to_local(utc_dt):
# get integer timestamp to avoid precision lost
timestamp = calendar.timegm(utc_dt.timetuple())
local_dt = datetime.datetime.fromtimestamp(timestamp)
assert utc_dt.resolution >= datetime.timedelta(microseconds=1)
return local_dt.replace(microsecond=utc_dt.microsecond)
def create_jpgs(subDirPath, subDirGpx, name, subDirMp4, fileNum, crop, cropFront, cropRear, doNotSkip, ffmpegDir, exiftoolDir):
global filesCount
# read .gpx xml file
gpxXmlTree = ET.parse(subDirGpx)
gpxXmlRoot = gpxXmlTree.getroot()
n = 0
timestampPattern = '%Y-%m-%dT%H:%M:%SZ'
gpxTrkptElemList = gpxXmlRoot.findall('.//{http://www.topografix.com/GPX/1/0}trkpt')
s = len(gpxTrkptElemList)
for gpxTrkptElem in gpxTrkptElemList:
n = n + 1
paddedNumber = '{0:04d}'.format(n)
gpxDescElem = gpxTrkptElem.find('{http://www.topografix.com/GPX/1/0}desc')
gpxUtcTimeString = gpxTrkptElem.find('{http://www.topografix.com/GPX/1/0}time').text
gpxUtcTimeTs = datetime.datetime.strptime(gpxUtcTimeString, timestampPattern)
gpxSystemTimeTs = utc_to_local(gpxUtcTimeTs)
gpxUtcTimeString2 = gpxUtcTimeTs.strftime("%Y:%m:%d %H:%M:%S")
gpxSystemTimeString2 = gpxSystemTimeTs.strftime("%Y:%m:%d %H:%M:%S")
if n==1:
firstGpxTimeTs = gpxUtcTimeTs
if gpxDescElem is not None:
gpxDescList = gpxDescElem.text.split(";")
frameTimePosition = gpxDescList[0][len("frameTimePosition="):]
gpxFrameIsFarEnough = gpxDescList[1][len("frameIsFarEnough="):]
else:
frameTimePosition = gpxUtcTimeTs - firstGpxTimeTs
gpxFrameIsFarEnough = "Y"
subDirNewJpg = os.path.join(subDirPath, name + '_' + paddedNumber + '.jpg')
if gpxFrameIsFarEnough == "N" and not doNotSkip:
print('%s/%s:%s/%s: Do not generate .jpg file: at TimeUtc=%s TimeSystem=%s ss=%s. Frame gps location is too close from previous saved frame.' % (fileNum, filesCount, n, s, gpxUtcTimeString2, gpxSystemTimeString2, frameTimePosition))
continue
print('%s/%s:%s/%s: Generate .jpg file: at TimeUtc=%s TimeSystem=%s ss=%s -> %s' % (fileNum, filesCount, n, s, gpxUtcTimeString2, gpxSystemTimeString2, frameTimePosition, subDirNewJpg))
os.chdir(ffmpegDir)
if crop:
subprocess.call(['ffmpeg', '-loglevel', 'error', '-ss', str(frameTimePosition), '-i', subDirMp4, '-filter:v', 'crop='+crop, '-frames:v', '1', '-qscale:v', '1', subDirNewJpg])
elif cropRear and name.upper().endswith("R"):
subprocess.call(['ffmpeg', '-loglevel', 'error', '-ss', str(frameTimePosition), '-i', subDirMp4, '-filter:v', 'crop='+cropRear, '-frames:v', '1', '-qscale:v', '1', subDirNewJpg])
elif cropFront and not name.upper().endswith("R"):
subprocess.call(['ffmpeg', '-loglevel', 'error', '-ss', str(frameTimePosition), '-i', subDirMp4, '-filter:v', 'crop='+cropFront, '-frames:v', '1', '-qscale:v', '1', subDirNewJpg])
else:
subprocess.call(['ffmpeg', '-loglevel', 'error', '-ss', str(frameTimePosition), '-i', subDirMp4, '-frames:v', '1', '-qscale:v', '1', subDirNewJpg])
os.chdir(exiftoolDir)
subprocess.call(['exiftool', '-charset', 'FileName='+locale.getpreferredencoding(), '-CreateDate="'+gpxUtcTimeString2+'"', '-DateTimeOriginal="'+gpxUtcTimeString2+'"', '-FileModifyDate="'+gpxSystemTimeString2+'"', subDirNewJpg])
return
def geotag_jpgs(subDirPath, subDirGpx, name, exiftoolDir):
print("START exiftool -geotag")
os.chdir(exiftoolDir)
# use FileModifyDate saved assuming UTC datetime
# -geotag option writes exif data: GPS:GPSLatitude, GPS:GPSLongitude, GPS:GPSImgDirection (course)
subprocess.call(['exiftool', '-charset', 'FileName=' + locale.getpreferredencoding(), '-geotag', subDirGpx, '-Geotime<FileModifyDate', '-P', subDirPath])
print("END exiftool -geotag")
return
def clean_subDirMp4(subDirMp4):
os.remove(subDirMp4)
return
def clean_subDirOrginal(subDirPath):
subDirExiftoolTmps = glob.glob(subDirPath + '/*original')
for subDirExiftoolTmp in subDirExiftoolTmps:
os.remove(subDirExiftoolTmp)
return
def remove_subDir(subDirPath):
os.rmdir(subDirPath)
def findToolDir(userDir, toolName, currentScriptDir):
print("findToolDir: toolName = %s userDir = %s currentScriptDir = %s" % (toolName, userDir, currentScriptDir))
if (userDir and (os.path.exists(os.path.join(userDir,toolName)) or os.path.exists(os.path.join(userDir,toolName+'.exe')))):
#print("user provided directory contains tool file")
return userDir
if (os.path.exists(os.path.join(os.path.join(currentScriptDir, toolName), toolName)) or os.path.exists(os.path.join(os.path.join(currentScriptDir, toolName),toolName + '.exe'))):
#print("directory currentScriptDir\\toolName contains tool file")
return os.path.join(currentScriptDir, toolName)
#print("return currentScriptDir with hope that tool is on the system path")
return currentScriptDir
def arrangeOutputFiles(outputDir, gpxDirName, names, camName):
timestampPattern = '%Y-%m-%dT%H:%M:%SZ'
outputName = os.path.basename(outputDir)
sequenceNum = 0
currentSequenceLastTs = None
currentSequenceJpgDirName = None
print("arrangeOutputFiles. camName: %s" % (camName))
for name in names:
subDirPath = os.path.join(outputDir, name)
gpxFileName = name + '.gpx'
subDirGpx = os.path.join(subDirPath, gpxFileName)
if os.path.isfile(subDirGpx):
gpxXmlTree = ET.parse(subDirGpx)
gpxXmlRoot = gpxXmlTree.getroot()
gpxFirstTrkptElemList = gpxXmlRoot.findall('.//{http://www.topografix.com/GPX/1/0}trkpt[1]')
gpxLastTrkptElemList = gpxXmlRoot.findall('.//{http://www.topografix.com/GPX/1/0}trkpt[last()]')
if len(gpxFirstTrkptElemList) == 1 and len(gpxLastTrkptElemList) == 1:
gpxFirstUtcTimeString = gpxFirstTrkptElemList[0].find('{http://www.topografix.com/GPX/1/0}time').text
gpxLastUtcTimeString = gpxLastTrkptElemList[0].find('{http://www.topografix.com/GPX/1/0}time').text
gpxFirstUtcTimeTs = datetime.datetime.strptime(gpxFirstUtcTimeString, timestampPattern)
gpxLastUtcTimeTs = datetime.datetime.strptime(gpxLastUtcTimeString, timestampPattern)
if currentSequenceLastTs is None or (gpxFirstUtcTimeTs - currentSequenceLastTs).total_seconds() > 10:
sequenceNum = sequenceNum + 1
print("arrangeOutputFiles. Found new sequence %s starting from name: %s (new start timestamp %s, old end timestamp %s" % (sequenceNum, name, gpxFirstUtcTimeString, currentSequenceLastTs))
currentSequenceLastTs = gpxLastUtcTimeTs
currentSequenceJpgDirName = os.path.join(outputDir, outputName + '_' + str(sequenceNum) + '_' + camName)
if not os.path.isdir(currentSequenceJpgDirName):
os.mkdir(currentSequenceJpgDirName)
else:
currentSequenceLastTs = gpxLastUtcTimeTs
# copy gpx file from current subDir to gpx dir
shutil.move(subDirGpx, os.path.join(gpxDirName, gpxFileName))
# copy all jpg files from current subDir to sequence dir
allJpgFiles = glob.glob(os.path.join(subDirPath, '*.jpg'), recursive=True)
for jpgFilePath in allJpgFiles:
dstJpgFilePath = os.path.join(currentSequenceJpgDirName, os.path.basename(jpgFilePath))
shutil.move(jpgFilePath, dstJpgFilePath)
# delete old output subDir
os.rmdir(subDirPath)
else:
print("arrangeOutputFiles. WARNING. Wrong subDirGpx file: %s" % (subDirGpx))
def main():
startTs = datetime.datetime.now()
in_files, crop, cropFront, cropRear, arrange, doNotSkip, ffmpegUserDir, exiftoolUserDir, deobfuscate, sort_by, not_del_outliers, not_del_duplicates = get_args()
orginalDir = os.getcwd()
currentScriptDir = os.path.dirname(os.path.realpath(__file__))
ffmpegDir = findToolDir(ffmpegUserDir, 'ffmpeg', currentScriptDir)
exiftoolDir = findToolDir(exiftoolUserDir, 'exiftool', currentScriptDir)
print("currentScript = %s currentScriptDir = %s currentDirectory = %s ffmpegDir = %s exiftoolDir = %s systemencoding = %s" % (os.path.realpath(__file__), currentScriptDir, os.getcwd(), ffmpegDir, exiftoolDir, locale.getpreferredencoding()))
print('current video file/total video files:current frame/total video frames:')
fileNum = 0
outputDir = os.getcwd()
namesFront = []
namesRear = []
for in_file in in_files:
fileNum = fileNum + 1
name = os.path.splitext(os.path.basename(in_file))[0]
if name.upper().endswith("R"):
namesRear.append(name)
else:
namesFront.append(name)
sourceMp4 = os.path.join(os.getcwd(), in_file)
# make subdir for each .MP4 file
subDirPath = os.path.join(outputDir, name)
subDirGpx = os.path.join(subDirPath, name + '.gpx')
subDirMp4 = os.path.join(subDirPath, name + '.MP4')
print("in_file=%s name=%s subDirPath=%s" % (in_file, name, subDirPath))
# prepare subfolder for source MP4 file
create_subDir(subDirPath, subDirGpx, subDirMp4, sourceMp4)
# create .gpx file from source MP4 via nvtk_mp42gpx.py script
gpx_created = create_gpx(name, subDirMp4, currentScriptDir, deobfuscate, sort_by, not_del_outliers, not_del_duplicates)
# create .jpg files from source MP4 via ffmpeg and exiftool
if gpx_created:
create_jpgs(subDirPath, subDirGpx, name, subDirMp4, fileNum, crop, cropFront, cropRear, doNotSkip, ffmpegDir, exiftoolDir)
# remove tmp *.MP4 files
clean_subDirMp4(subDirMp4)
if gpx_created:
# geotag .jpg files from .gpx file via exiftool
geotag_jpgs(subDirPath, subDirGpx, name, exiftoolDir)
# remove tmp *.original files
clean_subDirOrginal(subDirPath)
else:
remove_subDir(subDirPath)
os.chdir(orginalDir)
if arrange:
outputName = os.path.basename(outputDir)
gpxDirName = os.path.join(outputDir, outputName + '_gpx')
if not os.path.isdir(gpxDirName):
os.mkdir(gpxDirName)
arrangeOutputFiles(outputDir, gpxDirName, namesFront, 'F')
arrangeOutputFiles(outputDir, gpxDirName, namesRear, 'R')
endTs = datetime.datetime.now()
print("")
print("Script completed. Time=%s" % (endTs-startTs))
return
if __name__ == "__main__":
main()